@clinebot/core 0.0.11 → 0.0.12

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (53) hide show
  1. package/dist/agents/agent-config-loader.d.ts +1 -1
  2. package/dist/agents/agent-config-parser.d.ts +5 -2
  3. package/dist/agents/index.d.ts +1 -1
  4. package/dist/agents/plugin-config-loader.d.ts +4 -0
  5. package/dist/agents/plugin-sandbox-bootstrap.js +446 -0
  6. package/dist/agents/plugin-sandbox.d.ts +4 -0
  7. package/dist/index.node.d.ts +1 -0
  8. package/dist/index.node.js +658 -407
  9. package/dist/runtime/sandbox/subprocess-sandbox.d.ts +8 -1
  10. package/dist/session/default-session-manager.d.ts +5 -0
  11. package/dist/session/session-config-builder.d.ts +4 -1
  12. package/dist/session/session-manager.d.ts +1 -0
  13. package/dist/session/unified-session-persistence-service.d.ts +6 -0
  14. package/dist/session/utils/types.d.ts +9 -0
  15. package/dist/tools/definitions.d.ts +2 -2
  16. package/dist/tools/presets.d.ts +3 -3
  17. package/dist/tools/schemas.d.ts +14 -14
  18. package/dist/types/config.d.ts +5 -0
  19. package/dist/types/events.d.ts +22 -0
  20. package/package.json +5 -4
  21. package/src/agents/agent-config-loader.test.ts +2 -0
  22. package/src/agents/agent-config-loader.ts +1 -0
  23. package/src/agents/agent-config-parser.ts +12 -5
  24. package/src/agents/index.ts +1 -0
  25. package/src/agents/plugin-config-loader.test.ts +49 -0
  26. package/src/agents/plugin-config-loader.ts +10 -73
  27. package/src/agents/plugin-loader.test.ts +128 -2
  28. package/src/agents/plugin-loader.ts +70 -5
  29. package/src/agents/plugin-sandbox-bootstrap.ts +445 -0
  30. package/src/agents/plugin-sandbox.test.ts +198 -1
  31. package/src/agents/plugin-sandbox.ts +223 -353
  32. package/src/index.node.ts +4 -0
  33. package/src/runtime/hook-file-hooks.test.ts +1 -1
  34. package/src/runtime/hook-file-hooks.ts +16 -6
  35. package/src/runtime/runtime-builder.test.ts +67 -0
  36. package/src/runtime/runtime-builder.ts +70 -16
  37. package/src/runtime/sandbox/subprocess-sandbox.ts +35 -11
  38. package/src/session/default-session-manager.e2e.test.ts +20 -1
  39. package/src/session/default-session-manager.test.ts +453 -1
  40. package/src/session/default-session-manager.ts +200 -0
  41. package/src/session/session-config-builder.ts +2 -0
  42. package/src/session/session-manager.ts +1 -0
  43. package/src/session/session-team-coordination.ts +30 -0
  44. package/src/session/unified-session-persistence-service.ts +45 -0
  45. package/src/session/utils/types.ts +10 -0
  46. package/src/storage/sqlite-team-store.ts +16 -5
  47. package/src/tools/definitions.test.ts +87 -8
  48. package/src/tools/definitions.ts +89 -70
  49. package/src/tools/presets.test.ts +2 -3
  50. package/src/tools/presets.ts +3 -3
  51. package/src/tools/schemas.ts +23 -22
  52. package/src/types/config.ts +5 -0
  53. package/src/types/events.ts +23 -0
@@ -1,12 +1,16 @@
1
- import { mkdtemp, rm, writeFile } from "node:fs/promises";
1
+ import { mkdir, mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
2
2
  import { tmpdir } from "node:os";
3
- import { join } from "node:path";
3
+ import { join, resolve } from "node:path";
4
+ import { fileURLToPath } from "node:url";
4
5
  import { describe, expect, it } from "vitest";
5
6
  import {
6
7
  loadAgentPluginFromPath,
7
8
  loadAgentPluginsFromPaths,
8
9
  } from "./plugin-loader";
9
10
 
11
+ const TEST_DIR = fileURLToPath(new URL(".", import.meta.url));
12
+ const REPO_ROOT = resolve(TEST_DIR, "..", "..", "..", "..");
13
+
10
14
  describe("plugin-loader", () => {
11
15
  it("loads default-exported plugin from path", async () => {
12
16
  const dir = await mkdtemp(join(tmpdir(), "core-plugin-loader-"));
@@ -82,6 +86,128 @@ describe("plugin-loader", () => {
82
86
  }
83
87
  });
84
88
 
89
+ it("loads TypeScript plugins from file paths", async () => {
90
+ const dir = await mkdtemp(join(tmpdir(), "core-plugin-loader-"));
91
+ try {
92
+ const pluginPath = join(dir, "plugin-ts.ts");
93
+ await writeFile(
94
+ pluginPath,
95
+ [
96
+ "const name: string = 'plugin-ts';",
97
+ "export default {",
98
+ " name,",
99
+ " manifest: { capabilities: ['tools'] },",
100
+ "};",
101
+ ].join("\n"),
102
+ "utf8",
103
+ );
104
+
105
+ const plugin = await loadAgentPluginFromPath(pluginPath);
106
+ expect(plugin.name).toBe("plugin-ts");
107
+ } finally {
108
+ await rm(dir, { recursive: true, force: true });
109
+ }
110
+ });
111
+
112
+ it("resolves plugin-local dependencies from the plugin path", async () => {
113
+ const dir = await mkdtemp(join(tmpdir(), "core-plugin-loader-"));
114
+ try {
115
+ const depDir = join(dir, "node_modules", "plugin-local-dep");
116
+ await mkdir(depDir, { recursive: true });
117
+ await writeFile(
118
+ join(depDir, "package.json"),
119
+ JSON.stringify({
120
+ name: "plugin-local-dep",
121
+ type: "module",
122
+ exports: "./index.js",
123
+ }),
124
+ "utf8",
125
+ );
126
+ await writeFile(
127
+ join(depDir, "index.js"),
128
+ "export const depName = 'plugin-local-dep';\n",
129
+ "utf8",
130
+ );
131
+ const pluginPath = join(dir, "plugin-with-dep.ts");
132
+ await writeFile(
133
+ pluginPath,
134
+ [
135
+ "import { depName } from 'plugin-local-dep';",
136
+ "export default {",
137
+ " name: depName,",
138
+ " manifest: { capabilities: ['tools'] },",
139
+ "};",
140
+ ].join("\n"),
141
+ "utf8",
142
+ );
143
+
144
+ const plugin = await loadAgentPluginFromPath(pluginPath, { cwd: dir });
145
+ expect(plugin.name).toBe("plugin-local-dep");
146
+ } finally {
147
+ await rm(dir, { recursive: true, force: true });
148
+ }
149
+ });
150
+
151
+ it("prefers plugin-installed SDK packages over workspace aliases", async () => {
152
+ const dir = await mkdtemp(join(tmpdir(), "core-plugin-loader-"));
153
+ try {
154
+ const depDir = join(dir, "node_modules", "@clinebot", "shared");
155
+ await mkdir(depDir, { recursive: true });
156
+ await writeFile(
157
+ join(depDir, "package.json"),
158
+ JSON.stringify({
159
+ name: "@clinebot/shared",
160
+ type: "module",
161
+ exports: "./index.js",
162
+ }),
163
+ "utf8",
164
+ );
165
+ await writeFile(
166
+ join(depDir, "index.js"),
167
+ "export const sdkMarker = 'plugin-installed-sdk';\n",
168
+ "utf8",
169
+ );
170
+ const pluginPath = join(dir, "plugin-with-sdk-dep.ts");
171
+ await writeFile(
172
+ pluginPath,
173
+ [
174
+ "import { sdkMarker } from '@clinebot/shared';",
175
+ "export default {",
176
+ " name: sdkMarker,",
177
+ " manifest: { capabilities: ['tools'] },",
178
+ "};",
179
+ ].join("\n"),
180
+ "utf8",
181
+ );
182
+
183
+ const plugin = await loadAgentPluginFromPath(pluginPath, { cwd: dir });
184
+ expect(plugin.name).toBe("plugin-installed-sdk");
185
+ } finally {
186
+ await rm(dir, { recursive: true, force: true });
187
+ }
188
+ });
189
+
190
+ it("requires copied plugins to provide their own non-SDK dependencies", async () => {
191
+ const dir = await mkdtemp(join(tmpdir(), "core-plugin-loader-copy-"));
192
+ try {
193
+ const pluginPath = join(dir, "portable-subagents.ts");
194
+ await writeFile(
195
+ pluginPath,
196
+ await readFile(
197
+ resolve(REPO_ROOT, "apps/examples/subagent-plugin/index.ts"),
198
+ "utf8",
199
+ ),
200
+ "utf8",
201
+ );
202
+
203
+ await expect(
204
+ loadAgentPluginFromPath(pluginPath, { cwd: dir }),
205
+ ).rejects.toThrow(/Cannot find (package|module) 'yaml'/i);
206
+ } finally {
207
+ await rm(dir, { recursive: true, force: true });
208
+ }
209
+ });
210
+
85
211
  it("rejects invalid plugin export missing manifest", async () => {
86
212
  const dir = await mkdtemp(join(tmpdir(), "core-plugin-loader-"));
87
213
  try {
@@ -1,6 +1,9 @@
1
- import { resolve } from "node:path";
2
- import { pathToFileURL } from "node:url";
1
+ import { existsSync } from "node:fs";
2
+ import { builtinModules, createRequire } from "node:module";
3
+ import { dirname, resolve } from "node:path";
4
+ import { fileURLToPath } from "node:url";
3
5
  import type { AgentConfig } from "@clinebot/agents";
6
+ import createJiti from "jiti";
4
7
 
5
8
  type AgentPlugin = NonNullable<AgentConfig["extensions"]>[number];
6
9
  type PluginLike = {
@@ -16,6 +19,12 @@ export interface LoadAgentPluginFromPathOptions {
16
19
  cwd?: string;
17
20
  }
18
21
 
22
+ const MODULE_DIR = dirname(fileURLToPath(import.meta.url));
23
+ const WORKSPACE_ALIASES = collectWorkspaceAliases(MODULE_DIR);
24
+ const BUILTIN_MODULES = new Set(
25
+ builtinModules.flatMap((id) => [id, id.replace(/^node:/, "")]),
26
+ );
27
+
19
28
  function isObject(value: unknown): value is Record<string, unknown> {
20
29
  return typeof value === "object" && value !== null;
21
30
  }
@@ -77,14 +86,70 @@ function validatePluginExport(
77
86
  validatePluginManifest(plugin as PluginLike, absolutePath);
78
87
  }
79
88
 
89
+ function collectWorkspaceAliases(startDir: string): Record<string, string> {
90
+ const root = resolve(startDir, "..", "..", "..", "..");
91
+ const aliases: Record<string, string> = {};
92
+ const candidates: Record<string, string> = {
93
+ "@clinebot/agents": resolve(root, "packages/agents/src/index.ts"),
94
+ "@clinebot/core": resolve(root, "packages/core/src/index.node.ts"),
95
+ "@clinebot/llms": resolve(root, "packages/llms/src/index.ts"),
96
+ "@clinebot/rpc": resolve(root, "packages/rpc/src/index.ts"),
97
+ "@clinebot/scheduler": resolve(root, "packages/scheduler/src/index.ts"),
98
+ "@clinebot/shared": resolve(root, "packages/shared/src/index.ts"),
99
+ "@clinebot/shared/storage": resolve(
100
+ root,
101
+ "packages/shared/src/storage/index.ts",
102
+ ),
103
+ "@clinebot/shared/db": resolve(root, "packages/shared/src/db/index.ts"),
104
+ };
105
+ for (const [key, value] of Object.entries(candidates)) {
106
+ if (existsSync(value)) {
107
+ aliases[key] = value;
108
+ }
109
+ }
110
+ return aliases;
111
+ }
112
+
113
+ function collectPluginImportAliases(
114
+ pluginPath: string,
115
+ ): Record<string, string> {
116
+ const pluginRequire = createRequire(pluginPath);
117
+ const aliases: Record<string, string> = {};
118
+ for (const [specifier, sourcePath] of Object.entries(WORKSPACE_ALIASES)) {
119
+ try {
120
+ pluginRequire.resolve(specifier);
121
+ continue;
122
+ } catch {
123
+ // Use the workspace source only when the plugin package does not provide
124
+ // its own installed SDK dependency.
125
+ }
126
+ aliases[specifier] = sourcePath;
127
+ }
128
+ return aliases;
129
+ }
130
+
131
+ async function importPluginModule(
132
+ absolutePath: string,
133
+ ): Promise<Record<string, unknown>> {
134
+ const aliases = collectPluginImportAliases(absolutePath);
135
+ const jiti = createJiti(absolutePath, {
136
+ alias: aliases,
137
+ cache: false,
138
+ requireCache: false,
139
+ esmResolve: true,
140
+ interopDefault: false,
141
+ nativeModules: [...BUILTIN_MODULES],
142
+ transformModules: Object.keys(aliases),
143
+ });
144
+ return (await jiti.import(absolutePath, {})) as Record<string, unknown>;
145
+ }
146
+
80
147
  export async function loadAgentPluginFromPath(
81
148
  pluginPath: string,
82
149
  options: LoadAgentPluginFromPathOptions = {},
83
150
  ): Promise<AgentPlugin> {
84
151
  const absolutePath = resolve(options.cwd ?? process.cwd(), pluginPath);
85
- const moduleExports = (await import(
86
- pathToFileURL(absolutePath).href
87
- )) as Record<string, unknown>;
152
+ const moduleExports = await importPluginModule(absolutePath);
88
153
  const exportName = options.exportName ?? "plugin";
89
154
  const plugin = (moduleExports.default ??
90
155
  moduleExports[exportName]) as unknown;
@@ -0,0 +1,445 @@
1
+ import { existsSync } from "node:fs";
2
+ import { builtinModules, createRequire } from "node:module";
3
+ import { dirname, resolve } from "node:path";
4
+ /**
5
+ * Bootstrap script for the plugin sandbox subprocess.
6
+ *
7
+ * This file runs inside an isolated Node.js child process spawned by
8
+ * {@link SubprocessSandbox}. It receives RPC calls over IPC and dynamically
9
+ * imports plugin modules, wiring up their contributions (tools, commands,
10
+ * shortcuts, flags, renderers, providers) and lifecycle hooks.
11
+ *
12
+ * Because it executes in a separate process it must be self-contained — no
13
+ * imports from the rest of the codebase are allowed.
14
+ */
15
+
16
+ import { fileURLToPath } from "node:url";
17
+ import createJiti from "jiti";
18
+
19
+ // ---------------------------------------------------------------------------
20
+ // Types (intentionally minimal – mirrors only what the RPC protocol needs)
21
+ // ---------------------------------------------------------------------------
22
+
23
+ interface PluginTool {
24
+ name: string;
25
+ description?: string;
26
+ inputSchema?: unknown;
27
+ timeoutMs?: number;
28
+ retryable?: boolean;
29
+ execute: (input: unknown, context: unknown) => Promise<unknown>;
30
+ }
31
+
32
+ interface PluginCommand {
33
+ name: string;
34
+ description?: string;
35
+ handler?: (input: string) => Promise<string>;
36
+ }
37
+
38
+ interface PluginShortcut {
39
+ name: string;
40
+ value: string;
41
+ description?: string;
42
+ }
43
+
44
+ interface PluginFlag {
45
+ name: string;
46
+ description?: string;
47
+ defaultValue?: boolean | string | number;
48
+ }
49
+
50
+ interface PluginMessageRenderer {
51
+ name: string;
52
+ render: (message: unknown) => string;
53
+ }
54
+
55
+ interface PluginProvider {
56
+ name: string;
57
+ description?: string;
58
+ metadata?: Record<string, unknown>;
59
+ }
60
+
61
+ interface PluginApi {
62
+ registerTool(tool: PluginTool): void;
63
+ registerCommand(command: PluginCommand): void;
64
+ registerShortcut(shortcut: PluginShortcut): void;
65
+ registerFlag(flag: PluginFlag): void;
66
+ registerMessageRenderer(renderer: PluginMessageRenderer): void;
67
+ registerProvider(provider: PluginProvider): void;
68
+ }
69
+
70
+ interface PluginModule {
71
+ name: string;
72
+ manifest: Record<string, unknown>;
73
+ setup?: (api: PluginApi) => void | Promise<void>;
74
+ [hookName: string]: unknown;
75
+ }
76
+
77
+ interface ContributionDescriptor {
78
+ id: string;
79
+ name: string;
80
+ description?: string;
81
+ inputSchema?: unknown;
82
+ timeoutMs?: number;
83
+ retryable?: boolean;
84
+ value?: string;
85
+ defaultValue?: boolean | string | number;
86
+ metadata?: Record<string, unknown>;
87
+ }
88
+
89
+ interface PluginDescriptor {
90
+ pluginId: string;
91
+ name: string;
92
+ manifest: Record<string, unknown>;
93
+ contributions: {
94
+ tools: ContributionDescriptor[];
95
+ commands: ContributionDescriptor[];
96
+ shortcuts: ContributionDescriptor[];
97
+ flags: ContributionDescriptor[];
98
+ messageRenderers: ContributionDescriptor[];
99
+ providers: ContributionDescriptor[];
100
+ };
101
+ }
102
+
103
+ interface PluginState {
104
+ plugin: PluginModule;
105
+ handlers: {
106
+ tools: Map<string, PluginTool["execute"]>;
107
+ commands: Map<string, NonNullable<PluginCommand["handler"]>>;
108
+ messageRenderers: Map<string, PluginMessageRenderer["render"]>;
109
+ };
110
+ }
111
+
112
+ function isObject(value: unknown): value is Record<string, unknown> {
113
+ return typeof value === "object" && value !== null;
114
+ }
115
+
116
+ function assertValidPluginModule(
117
+ plugin: unknown,
118
+ pluginPath: string,
119
+ ): asserts plugin is PluginModule {
120
+ if (!isObject(plugin)) {
121
+ throw new Error(`Invalid plugin module: ${pluginPath}`);
122
+ }
123
+ if (typeof plugin.name !== "string" || !plugin.name) {
124
+ throw new Error(`Invalid plugin name: ${pluginPath}`);
125
+ }
126
+ if (!isObject(plugin.manifest)) {
127
+ throw new Error(`Invalid plugin manifest: ${pluginPath}`);
128
+ }
129
+ }
130
+
131
+ // ---------------------------------------------------------------------------
132
+ // State
133
+ // ---------------------------------------------------------------------------
134
+
135
+ let pluginCounter = 0;
136
+ const pluginState = new Map<string, PluginState>();
137
+ const MODULE_DIR = dirname(fileURLToPath(import.meta.url));
138
+ const WORKSPACE_ALIASES = collectWorkspaceAliases(MODULE_DIR);
139
+ const BUILTIN_MODULES = new Set(
140
+ builtinModules.flatMap((id) => [id, id.replace(/^node:/, "")]),
141
+ );
142
+
143
+ // ---------------------------------------------------------------------------
144
+ // IPC helpers
145
+ // ---------------------------------------------------------------------------
146
+
147
+ function toErrorPayload(error: unknown): { message: string; stack?: string } {
148
+ const message = error instanceof Error ? error.message : String(error);
149
+ const stack = error instanceof Error ? error.stack : undefined;
150
+ return { message, stack };
151
+ }
152
+
153
+ function sendResponse(
154
+ id: string,
155
+ ok: boolean,
156
+ result: unknown,
157
+ error?: { message: string; stack?: string },
158
+ ): void {
159
+ if (!process.send) return;
160
+ process.send({ type: "response", id, ok, result, error });
161
+ }
162
+
163
+ function emitEvent(name: string, payload?: unknown): void {
164
+ if (!process.send) return;
165
+ process.send({ type: "event", name, payload });
166
+ }
167
+
168
+ // Expose event emitter to plugins.
169
+ (globalThis as Record<string, unknown>).__clinePluginHost = { emitEvent };
170
+
171
+ // ---------------------------------------------------------------------------
172
+ // Helpers
173
+ // ---------------------------------------------------------------------------
174
+
175
+ function sanitizeObject(value: unknown): Record<string, unknown> {
176
+ if (!value || typeof value !== "object") return {};
177
+ return value as Record<string, unknown>;
178
+ }
179
+
180
+ function makeId(pluginId: string, prefix: string): string {
181
+ return `${pluginId}_${prefix}_${Math.random().toString(36).slice(2, 10)}`;
182
+ }
183
+
184
+ function getPlugin(pluginId: string): PluginState {
185
+ const state = pluginState.get(pluginId);
186
+ if (!state) {
187
+ throw new Error(`Unknown sandbox plugin id: ${pluginId}`);
188
+ }
189
+ return state;
190
+ }
191
+
192
+ function collectWorkspaceAliases(startDir: string): Record<string, string> {
193
+ const root = resolve(startDir, "..", "..", "..", "..");
194
+ const aliases: Record<string, string> = {};
195
+ const candidates: Record<string, string> = {
196
+ "@clinebot/agents": resolve(root, "packages/agents/src/index.ts"),
197
+ "@clinebot/core": resolve(root, "packages/core/src/index.node.ts"),
198
+ "@clinebot/llms": resolve(root, "packages/llms/src/index.ts"),
199
+ "@clinebot/rpc": resolve(root, "packages/rpc/src/index.ts"),
200
+ "@clinebot/scheduler": resolve(root, "packages/scheduler/src/index.ts"),
201
+ "@clinebot/shared": resolve(root, "packages/shared/src/index.ts"),
202
+ "@clinebot/shared/storage": resolve(
203
+ root,
204
+ "packages/shared/src/storage/index.ts",
205
+ ),
206
+ "@clinebot/shared/db": resolve(root, "packages/shared/src/db/index.ts"),
207
+ };
208
+ for (const [key, value] of Object.entries(candidates)) {
209
+ if (existsSync(value)) {
210
+ aliases[key] = value;
211
+ }
212
+ }
213
+ return aliases;
214
+ }
215
+
216
+ function collectPluginImportAliases(
217
+ pluginPath: string,
218
+ ): Record<string, string> {
219
+ const pluginRequire = createRequire(pluginPath);
220
+ const aliases: Record<string, string> = {};
221
+ for (const [specifier, sourcePath] of Object.entries(WORKSPACE_ALIASES)) {
222
+ try {
223
+ pluginRequire.resolve(specifier);
224
+ continue;
225
+ } catch {
226
+ // Use the workspace source only when the plugin package does not provide
227
+ // its own installed SDK dependency.
228
+ }
229
+ aliases[specifier] = sourcePath;
230
+ }
231
+ return aliases;
232
+ }
233
+
234
+ async function importPluginModule(
235
+ pluginPath: string,
236
+ ): Promise<Record<string, unknown>> {
237
+ const aliases = collectPluginImportAliases(pluginPath);
238
+ const jiti = createJiti(pluginPath, {
239
+ alias: aliases,
240
+ cache: false,
241
+ requireCache: false,
242
+ esmResolve: true,
243
+ interopDefault: false,
244
+ nativeModules: [...BUILTIN_MODULES],
245
+ transformModules: Object.keys(aliases),
246
+ });
247
+ return (await jiti.import(pluginPath, {})) as Record<string, unknown>;
248
+ }
249
+
250
+ // ---------------------------------------------------------------------------
251
+ // RPC methods
252
+ // ---------------------------------------------------------------------------
253
+
254
+ async function initialize(args: {
255
+ pluginPaths?: string[];
256
+ exportName?: string;
257
+ }): Promise<PluginDescriptor[]> {
258
+ const descriptors: PluginDescriptor[] = [];
259
+ const exportName = args.exportName || "plugin";
260
+
261
+ for (const pluginPath of args.pluginPaths || []) {
262
+ const moduleExports = await importPluginModule(pluginPath);
263
+ const plugin = (moduleExports.default ??
264
+ moduleExports[exportName]) as unknown;
265
+ assertValidPluginModule(plugin, pluginPath);
266
+
267
+ const pluginId = `plugin_${++pluginCounter}`;
268
+ const contributions: PluginDescriptor["contributions"] = {
269
+ tools: [],
270
+ commands: [],
271
+ shortcuts: [],
272
+ flags: [],
273
+ messageRenderers: [],
274
+ providers: [],
275
+ };
276
+ const handlers: PluginState["handlers"] = {
277
+ tools: new Map(),
278
+ commands: new Map(),
279
+ messageRenderers: new Map(),
280
+ };
281
+
282
+ const api: PluginApi = {
283
+ registerTool: (tool) => {
284
+ const id = makeId(pluginId, "tool");
285
+ handlers.tools.set(id, tool.execute);
286
+ contributions.tools.push({
287
+ id,
288
+ name: tool.name,
289
+ description: tool.description,
290
+ inputSchema: tool.inputSchema,
291
+ timeoutMs: tool.timeoutMs,
292
+ retryable: tool.retryable,
293
+ });
294
+ },
295
+ registerCommand: (command) => {
296
+ const id = makeId(pluginId, "command");
297
+ if (typeof command.handler === "function") {
298
+ handlers.commands.set(id, command.handler);
299
+ }
300
+ contributions.commands.push({
301
+ id,
302
+ name: command.name,
303
+ description: command.description,
304
+ });
305
+ },
306
+ registerShortcut: (shortcut) => {
307
+ contributions.shortcuts.push({
308
+ id: makeId(pluginId, "shortcut"),
309
+ name: shortcut.name,
310
+ value: shortcut.value,
311
+ description: shortcut.description,
312
+ });
313
+ },
314
+ registerFlag: (flag) => {
315
+ contributions.flags.push({
316
+ id: makeId(pluginId, "flag"),
317
+ name: flag.name,
318
+ description: flag.description,
319
+ defaultValue: flag.defaultValue,
320
+ });
321
+ },
322
+ registerMessageRenderer: (renderer) => {
323
+ const id = makeId(pluginId, "renderer");
324
+ handlers.messageRenderers.set(id, renderer.render);
325
+ contributions.messageRenderers.push({ id, name: renderer.name });
326
+ },
327
+ registerProvider: (provider) => {
328
+ contributions.providers.push({
329
+ id: makeId(pluginId, "provider"),
330
+ name: provider.name,
331
+ description: provider.description,
332
+ metadata: sanitizeObject(provider.metadata),
333
+ });
334
+ },
335
+ };
336
+
337
+ if (typeof plugin.setup === "function") {
338
+ await plugin.setup(api);
339
+ }
340
+
341
+ pluginState.set(pluginId, { plugin, handlers });
342
+ descriptors.push({
343
+ pluginId,
344
+ name: plugin.name,
345
+ manifest: plugin.manifest,
346
+ contributions,
347
+ });
348
+ }
349
+
350
+ return descriptors;
351
+ }
352
+
353
+ async function invokeHook(args: {
354
+ pluginId: string;
355
+ hookName: string;
356
+ payload: unknown;
357
+ }): Promise<unknown> {
358
+ const state = getPlugin(args.pluginId);
359
+ const handler = state.plugin[args.hookName];
360
+ if (typeof handler !== "function") {
361
+ return undefined;
362
+ }
363
+ return await (handler as (payload: unknown) => Promise<unknown>)(
364
+ args.payload,
365
+ );
366
+ }
367
+
368
+ async function executeTool(args: {
369
+ pluginId: string;
370
+ contributionId: string;
371
+ input: unknown;
372
+ context: unknown;
373
+ }): Promise<unknown> {
374
+ const state = getPlugin(args.pluginId);
375
+ const handler = state.handlers.tools.get(args.contributionId);
376
+ if (typeof handler !== "function") {
377
+ throw new Error("Unknown sandbox tool contribution");
378
+ }
379
+ return await handler(args.input, args.context);
380
+ }
381
+
382
+ async function executeCommand(args: {
383
+ pluginId: string;
384
+ contributionId: string;
385
+ input: string;
386
+ }): Promise<string> {
387
+ const state = getPlugin(args.pluginId);
388
+ const handler = state.handlers.commands.get(args.contributionId);
389
+ if (typeof handler !== "function") {
390
+ return "";
391
+ }
392
+ return await handler(args.input);
393
+ }
394
+
395
+ async function renderMessage(args: {
396
+ pluginId: string;
397
+ contributionId: string;
398
+ message: unknown;
399
+ }): Promise<string> {
400
+ const state = getPlugin(args.pluginId);
401
+ const handler = state.handlers.messageRenderers.get(args.contributionId);
402
+ if (typeof handler !== "function") {
403
+ return "";
404
+ }
405
+ return await handler(args.message);
406
+ }
407
+
408
+ // ---------------------------------------------------------------------------
409
+ // Message dispatch
410
+ // ---------------------------------------------------------------------------
411
+
412
+ const methods: Record<string, (args: never) => Promise<unknown>> = {
413
+ initialize,
414
+ invokeHook,
415
+ executeTool,
416
+ executeCommand,
417
+ renderMessage,
418
+ };
419
+
420
+ process.on(
421
+ "message",
422
+ async (message: {
423
+ type: string;
424
+ id: string;
425
+ method: string;
426
+ args?: unknown;
427
+ }) => {
428
+ if (!message || message.type !== "call") {
429
+ return;
430
+ }
431
+ const method = methods[message.method];
432
+ if (!method) {
433
+ sendResponse(message.id, false, undefined, {
434
+ message: `Unknown method: ${String(message.method)}`,
435
+ });
436
+ return;
437
+ }
438
+ try {
439
+ const result = await method((message.args || {}) as never);
440
+ sendResponse(message.id, true, result);
441
+ } catch (error) {
442
+ sendResponse(message.id, false, undefined, toErrorPayload(error));
443
+ }
444
+ },
445
+ );