Extract plugin sdk runtime helpers
This commit is contained in:
parent
385d14184f
commit
af3bd09496
6 changed files with 108 additions and 19 deletions
|
|
@ -48,6 +48,7 @@ const behaviorTests = [
|
|||
".test-dist/tests/agent-setup-support.test.js",
|
||||
".test-dist/tests/plugin-config.test.js",
|
||||
".test-dist/tests/plugin-sdk-bridge.test.js",
|
||||
".test-dist/tests/plugin-sdk-runtime-helpers.test.js",
|
||||
".test-dist/tests/plugin-sdk-surface-boundaries.test.js",
|
||||
".test-dist/tests/plugin-state.test.js",
|
||||
".test-dist/tests/plugin-runtime.test.js",
|
||||
|
|
|
|||
|
|
@ -118,6 +118,8 @@ const agentSetupSource = readFileSync(join(appDir, "src", "agent-setup.ts"), "ut
|
|||
const agentSetupCommandContextSource = readFileSync(join(appDir, "src", "agent-setup-command-context.ts"), "utf8");
|
||||
const agentSetupEditorToolsSource = readFileSync(join(appDir, "src", "agent-setup-editor-tools.ts"), "utf8");
|
||||
const agentSetupSupportSource = readFileSync(join(appDir, "src", "agent-setup-support.ts"), "utf8");
|
||||
const pluginSdkBridgeSource = readFileSync(join(appDir, "src", "plugin-sdk-bridge.ts"), "utf8");
|
||||
const pluginSdkRuntimeHelpersSource = readFileSync(join(appDir, "src", "plugin-sdk-runtime-helpers.ts"), "utf8");
|
||||
const pluginServiceSource = readFileSync(join(appDir, "src", "plugin-service.ts"), "utf8");
|
||||
const pluginServiceDefaultPetSource = readFileSync(join(appDir, "src", "plugin-service-default-pet.ts"), "utf8");
|
||||
const pluginServiceSnapshotSource = readFileSync(join(appDir, "src", "plugin-service-snapshot.ts"), "utf8");
|
||||
|
|
@ -274,6 +276,8 @@ assert.match(controlCenterSettingsCombinedSource, /API key or token|OpenAI API k
|
|||
assert.match(pluginServiceSource, /from "\.\/plugin-service-default-pet(?:\.js)?"/, "plugin-service must import the extracted default-pet helper seam.");
|
||||
assert.match(pluginServiceSource, /from "\.\/plugin-service-snapshot(?:\.js)?"/, "plugin-service must import the extracted snapshot helper seam.");
|
||||
assert.match(pluginServiceSource, /from "\.\/plugin-service-text-helpers(?:\.js)?"/, "plugin-service must import the extracted plugin text helper seam.");
|
||||
assert.match(pluginSdkBridgeSource, /from "\.\/plugin-sdk-runtime-helpers(?:\.js)?"/, "plugin-sdk-bridge must import the extracted runtime helper seam.");
|
||||
assert.match(pluginSdkRuntimeHelpersSource, /export function nextScheduleDelayMs/, "plugin-sdk runtime helper seam must own schedule delay calculation.");
|
||||
assert.match(pluginServiceDefaultPetSource, /export async function buildDefaultPetPluginCommands/, "plugin-service default-pet helper must export default familiar command shaping.");
|
||||
assert.match(pluginServiceSnapshotSource, /export async function buildSafePluginRecord/, "plugin-service snapshot helper must export safe plugin snapshot shaping.");
|
||||
assert.match(pluginServiceSnapshotSource, /export function validatePluginServiceConfigReplacement/, "plugin-service snapshot helper must export config validation delegation.");
|
||||
|
|
|
|||
|
|
@ -120,7 +120,7 @@ main.ts → initializePluginService(userData, defaultPluginPetApi, appVersion, E
|
|||
├── plugin-runtime.ts reloads enabled manifests
|
||||
│ ├── declarative runtime schedules timer triggers
|
||||
│ ├── plugin-js-host.ts starts hidden sandboxed BrowserWindow hosts for JavaScript plugins
|
||||
│ └── plugin-sdk-bridge.ts → plugin-familiar-api.ts/default-familiar-controller plus schedules, storage, commands, status, logs, and restricted network
|
||||
│ └── plugin-sdk-bridge.ts → plugin-sdk-runtime-helpers.ts plus plugin-familiar-api.ts/default-familiar-controller for schedules, storage, commands, status, logs, and restricted network
|
||||
├── plugin-service.ts orchestrates UI actions, permission confirmation, config validation, install/update/uninstall/load-local, and runtime reloads
|
||||
└── lifecycle.ts → stopPluginService() on quit
|
||||
|
||||
|
|
@ -231,7 +231,8 @@ plugin-local-loader.ts validates selected folder manifest and snapshots only fam
|
|||
- `plugin-package.ts`: Catalog plugin package download/install with HTTPS host/path allowlist, SHA-256 verification, ZIP size/entry restrictions, manifest/catalog consistency checks, and safe uninstall path resolution.
|
||||
- `plugin-local-loader.ts`: Developer loader that validates a selected local folder and snapshots only the manifest into `plugins-dev` with symlink/path/size protections.
|
||||
- `plugin-js-host.ts`: Sandboxed hidden BrowserWindow host for JavaScript plugin entry modules with per-plugin session partitioning, navigation/window-open hardening, SDK IPC tokening, registration handshake, config listener cleanup, and teardown.
|
||||
- `plugin-sdk-bridge.ts`: Permission-checked JavaScript plugin SDK for familiar speech/reactions, one-shot/repeating/daily schedules, storage with quotas, config listeners, commands, status, logs, and HTTPS-only public-host fetch.
|
||||
- `plugin-sdk-bridge.ts`: Permission-checked JavaScript plugin SDK for familiar speech/reactions, storage with quotas, config listeners, commands, status, logs, and HTTPS-only public-host fetch.
|
||||
- `plugin-sdk-runtime-helpers.ts`: Extracted schedule timing, timeout wrapping, bubble counting, and callback error normalization helpers for the JavaScript plugin SDK bridge.
|
||||
|
||||
**Agent Integration**:
|
||||
- `agent-setup.ts`: Claude detection, MCP configuration orchestration, hooks management, action journaling
|
||||
|
|
|
|||
|
|
@ -24,6 +24,13 @@ import {
|
|||
validateNetOptions,
|
||||
} from "./plugin-sdk-network.js";
|
||||
import { pluginSdkQuotas } from "./plugin-sdk-quotas.js";
|
||||
import {
|
||||
countActiveBubbles,
|
||||
nextScheduleDelayMs,
|
||||
parseDaily,
|
||||
safeError,
|
||||
withTimeout,
|
||||
} from "./plugin-sdk-runtime-helpers.js";
|
||||
import {
|
||||
createPluginAudioApi,
|
||||
createPluginBusApi,
|
||||
|
|
@ -40,7 +47,6 @@ import {
|
|||
commandIdPattern,
|
||||
isRecord,
|
||||
namedHostIcons,
|
||||
nextCronRunMs,
|
||||
normalizeJson,
|
||||
parseCronExpression,
|
||||
renderLimitedMarkdown,
|
||||
|
|
@ -681,19 +687,3 @@ export class PluginSdkBridge {
|
|||
return state;
|
||||
}
|
||||
}
|
||||
|
||||
function countActiveBubbles(state: PluginRuntimeState): number { return state.bubbles.size; }
|
||||
function parseDaily(spec: string | { time: string; days?: number[] }): { time: string; days?: number[] } { const value = typeof spec === "string" ? { time: spec } : spec; const m = /^(\d{2}):(\d{2})$/.exec(value.time); if (!m || Number(m[1]) > 23 || Number(m[2]) > 59) throw new Error("Daily schedule time must be HH:mm between 00:00 and 23:59."); if (value.days && (!Array.isArray(value.days) || value.days.some((d) => !Number.isInteger(d) || d < 0 || d > 6))) throw new Error("Daily schedule days must be weekdays 0-6."); return value; }
|
||||
function msUntilDaily(spec: { time: string; days?: number[] }): number { const [hour, minute] = spec.time.split(":").map(Number); const now = new Date(); for (let add = 0; add <= 7; add += 1) { const next = new Date(now); next.setDate(now.getDate() + add); next.setHours(hour ?? 0, minute ?? 0, 0, 0); if (next > now && (!spec.days || spec.days.includes(next.getDay()))) return next.getTime() - now.getTime(); } return 24 * 60 * 60 * 1000; }
|
||||
|
||||
function nextScheduleDelayMs(spec: ScheduleSpec): number | null {
|
||||
if (spec.type === "once") return Math.max(1, spec.delayMs);
|
||||
if (spec.type === "every") return Math.max(1, spec.intervalMs);
|
||||
if (spec.type === "daily") return msUntilDaily(spec.daily);
|
||||
if (spec.type === "at") return Math.max(1_000, spec.timestamp - Date.now());
|
||||
const next = nextCronRunMs(spec.expr, Date.now());
|
||||
return next === null ? null : Math.max(1_000, next - Date.now());
|
||||
}
|
||||
|
||||
function withTimeout<T>(promise: Promise<T>, timeoutMs: number): Promise<T> { return new Promise((resolve, reject) => { const timeout = setTimeout(() => reject(new Error("Plugin command timed out.")), timeoutMs); promise.then((v) => { clearTimeout(timeout); resolve(v); }, (e) => { clearTimeout(timeout); reject(e); }); }); }
|
||||
function safeError(error: unknown): string { return error instanceof Error ? error.message : "Plugin SDK callback failed."; }
|
||||
|
|
|
|||
61
apps/desktop/src/plugin-sdk-runtime-helpers.ts
Normal file
61
apps/desktop/src/plugin-sdk-runtime-helpers.ts
Normal file
|
|
@ -0,0 +1,61 @@
|
|||
import type { PluginRuntimeState, ScheduleSpec } from "./plugin-sdk-state.js";
|
||||
import { nextCronRunMs } from "./plugin-sdk-validators.js";
|
||||
|
||||
export function countActiveBubbles(state: PluginRuntimeState): number {
|
||||
return state.bubbles.size;
|
||||
}
|
||||
|
||||
export function parseDaily(spec: string | { time: string; days?: number[] }): { time: string; days?: number[] } {
|
||||
const value = typeof spec === "string" ? { time: spec } : spec;
|
||||
const match = /^(\d{2}):(\d{2})$/.exec(value.time);
|
||||
if (!match || Number(match[1]) > 23 || Number(match[2]) > 59) {
|
||||
throw new Error("Daily schedule time must be HH:mm between 00:00 and 23:59.");
|
||||
}
|
||||
if (value.days && (!Array.isArray(value.days) || value.days.some((day) => !Number.isInteger(day) || day < 0 || day > 6))) {
|
||||
throw new Error("Daily schedule days must be weekdays 0-6.");
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
export function nextScheduleDelayMs(spec: ScheduleSpec): number | null {
|
||||
if (spec.type === "once") return Math.max(1, spec.delayMs);
|
||||
if (spec.type === "every") return Math.max(1, spec.intervalMs);
|
||||
if (spec.type === "daily") return msUntilDaily(spec.daily);
|
||||
if (spec.type === "at") return Math.max(1_000, spec.timestamp - Date.now());
|
||||
const next = nextCronRunMs(spec.expr, Date.now());
|
||||
return next === null ? null : Math.max(1_000, next - Date.now());
|
||||
}
|
||||
|
||||
export function withTimeout<T>(promise: Promise<T>, timeoutMs: number): Promise<T> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const timeout = setTimeout(() => reject(new Error("Plugin command timed out.")), timeoutMs);
|
||||
promise.then(
|
||||
(value) => {
|
||||
clearTimeout(timeout);
|
||||
resolve(value);
|
||||
},
|
||||
(error) => {
|
||||
clearTimeout(timeout);
|
||||
reject(error);
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
export function safeError(error: unknown): string {
|
||||
return error instanceof Error ? error.message : "Plugin SDK callback failed.";
|
||||
}
|
||||
|
||||
function msUntilDaily(spec: { time: string; days?: number[] }): number {
|
||||
const [hour, minute] = spec.time.split(":").map(Number);
|
||||
const now = new Date();
|
||||
for (let add = 0; add <= 7; add += 1) {
|
||||
const next = new Date(now);
|
||||
next.setDate(now.getDate() + add);
|
||||
next.setHours(hour ?? 0, minute ?? 0, 0, 0);
|
||||
if (next > now && (!spec.days || spec.days.includes(next.getDay()))) {
|
||||
return next.getTime() - now.getTime();
|
||||
}
|
||||
}
|
||||
return 24 * 60 * 60 * 1000;
|
||||
}
|
||||
32
apps/desktop/tests/plugin-sdk-runtime-helpers.test.ts
Normal file
32
apps/desktop/tests/plugin-sdk-runtime-helpers.test.ts
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
import assert from "node:assert/strict";
|
||||
import { readFileSync } from "node:fs";
|
||||
import { dirname, resolve } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
import { countActiveBubbles, nextScheduleDelayMs, parseDaily, safeError, withTimeout } from "../src/plugin-sdk-runtime-helpers.js";
|
||||
|
||||
const desktopRoot = process.env.FAMILIAROS_DESKTOP_ROOT ?? resolve(dirname(fileURLToPath(import.meta.url)), "..");
|
||||
const pluginSdkBridgeSource = readFileSync(resolve(desktopRoot, "src/plugin-sdk-bridge.ts"), "utf8");
|
||||
const pluginSdkRuntimeHelpersSource = readFileSync(resolve(desktopRoot, "src/plugin-sdk-runtime-helpers.ts"), "utf8");
|
||||
|
||||
assert.match(pluginSdkBridgeSource, /from "\.\/plugin-sdk-runtime-helpers\.js"/, "Plugin SDK bridge must import the extracted runtime helper seam.");
|
||||
assert.match(pluginSdkRuntimeHelpersSource, /export function nextScheduleDelayMs/, "Plugin SDK runtime helpers must export schedule delay calculation.");
|
||||
assert.match(pluginSdkRuntimeHelpersSource, /export function withTimeout/, "Plugin SDK runtime helpers must export timeout wrapping.");
|
||||
|
||||
assert.deepEqual(parseDaily("09:15"), { time: "09:15" });
|
||||
assert.deepEqual(parseDaily({ time: "18:45", days: [1, 3, 5] }), { time: "18:45", days: [1, 3, 5] });
|
||||
assert.throws(() => parseDaily("25:00"), /Daily schedule time must be HH:mm/);
|
||||
assert.throws(() => parseDaily({ time: "12:30", days: [7] }), /Daily schedule days must be weekdays 0-6/);
|
||||
|
||||
assert.equal(nextScheduleDelayMs({ type: "once", delayMs: 0 }), 1);
|
||||
assert.equal(nextScheduleDelayMs({ type: "every", intervalMs: 25 }), 25);
|
||||
assert.equal(nextScheduleDelayMs({ type: "at", timestamp: Date.now() - 5_000 }), 1_000);
|
||||
|
||||
assert.equal(safeError(new Error("boom")), "boom");
|
||||
assert.equal(safeError("boom"), "Plugin SDK callback failed.");
|
||||
assert.equal(countActiveBubbles({ bubbles: new Map([["one", {}], ["two", {}]]) } as never), 2);
|
||||
|
||||
await assert.rejects(() => withTimeout(new Promise(() => undefined), 5), /Plugin command timed out\./);
|
||||
await assert.doesNotReject(() => withTimeout(Promise.resolve("ok"), 50));
|
||||
|
||||
console.error("Plugin SDK runtime helper validation passed.");
|
||||
Loading…
Add table
Reference in a new issue