@clinebot/core 0.0.32 → 0.0.34

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 (85) hide show
  1. package/dist/auth/client.d.ts +19 -0
  2. package/dist/auth/client.d.ts.map +1 -1
  3. package/dist/auth/cline.d.ts.map +1 -1
  4. package/dist/auth/oca.d.ts.map +1 -1
  5. package/dist/auth/server.d.ts +32 -0
  6. package/dist/auth/server.d.ts.map +1 -1
  7. package/dist/auth/types.d.ts +29 -0
  8. package/dist/auth/types.d.ts.map +1 -1
  9. package/dist/extensions/context/agentic-compaction.d.ts.map +1 -1
  10. package/dist/extensions/context/basic-compaction.d.ts.map +1 -1
  11. package/dist/extensions/context/compaction-shared.d.ts +1 -1
  12. package/dist/extensions/context/compaction-shared.d.ts.map +1 -1
  13. package/dist/extensions/context/compaction.d.ts.map +1 -1
  14. package/dist/extensions/index.d.ts +2 -1
  15. package/dist/extensions/index.d.ts.map +1 -1
  16. package/dist/extensions/plugin/plugin-config-loader.d.ts +2 -1
  17. package/dist/extensions/plugin/plugin-config-loader.d.ts.map +1 -1
  18. package/dist/extensions/plugin/plugin-load-report.d.ts +19 -0
  19. package/dist/extensions/plugin/plugin-load-report.d.ts.map +1 -0
  20. package/dist/extensions/plugin/plugin-loader.d.ts +6 -0
  21. package/dist/extensions/plugin/plugin-loader.d.ts.map +1 -1
  22. package/dist/extensions/plugin/plugin-sandbox.d.ts +2 -1
  23. package/dist/extensions/plugin/plugin-sandbox.d.ts.map +1 -1
  24. package/dist/extensions/plugin-sandbox-bootstrap.js +148 -148
  25. package/dist/index.d.ts +3 -2
  26. package/dist/index.d.ts.map +1 -1
  27. package/dist/index.js +227 -229
  28. package/dist/runtime/runtime-builder.d.ts +1 -1
  29. package/dist/runtime/runtime-builder.d.ts.map +1 -1
  30. package/dist/runtime/subprocess-sandbox.d.ts +2 -0
  31. package/dist/runtime/subprocess-sandbox.d.ts.map +1 -1
  32. package/dist/runtime/tool-approval.d.ts.map +1 -1
  33. package/dist/session/default-session-manager.d.ts.map +1 -1
  34. package/dist/session/persistence-service.d.ts.map +1 -1
  35. package/dist/session/session-agent-events.d.ts.map +1 -1
  36. package/dist/session/session-artifacts.d.ts +2 -0
  37. package/dist/session/session-artifacts.d.ts.map +1 -1
  38. package/dist/session/session-config-builder.d.ts.map +1 -1
  39. package/dist/team/team-tools.d.ts.map +1 -1
  40. package/dist/types/config.d.ts +1 -0
  41. package/dist/types/config.d.ts.map +1 -1
  42. package/dist/types/events.d.ts +4 -0
  43. package/dist/types/events.d.ts.map +1 -1
  44. package/package.json +4 -4
  45. package/src/auth/client.test.ts +29 -0
  46. package/src/auth/client.ts +21 -0
  47. package/src/auth/cline.ts +2 -0
  48. package/src/auth/oca.ts +2 -0
  49. package/src/auth/server.test.ts +287 -0
  50. package/src/auth/server.ts +50 -1
  51. package/src/auth/types.ts +29 -0
  52. package/src/extensions/context/agentic-compaction.ts +22 -10
  53. package/src/extensions/context/basic-compaction.ts +43 -18
  54. package/src/extensions/context/compaction-shared.ts +1 -1
  55. package/src/extensions/context/compaction.test.ts +16 -10
  56. package/src/extensions/context/compaction.ts +35 -12
  57. package/src/extensions/index.ts +6 -0
  58. package/src/extensions/plugin/plugin-config-loader.test.ts +37 -0
  59. package/src/extensions/plugin/plugin-config-loader.ts +18 -10
  60. package/src/extensions/plugin/plugin-load-report.ts +20 -0
  61. package/src/extensions/plugin/plugin-loader.test.ts +45 -0
  62. package/src/extensions/plugin/plugin-loader.ts +57 -3
  63. package/src/extensions/plugin/plugin-sandbox-bootstrap.ts +158 -86
  64. package/src/extensions/plugin/plugin-sandbox.test.ts +70 -0
  65. package/src/extensions/plugin/plugin-sandbox.ts +17 -6
  66. package/src/index.ts +11 -0
  67. package/src/providers/local-provider-service.test.ts +4 -4
  68. package/src/runtime/hook-file-hooks.test.ts +42 -7
  69. package/src/runtime/runtime-builder.test.ts +98 -0
  70. package/src/runtime/runtime-builder.ts +112 -65
  71. package/src/runtime/subprocess-sandbox.ts +26 -23
  72. package/src/runtime/tool-approval.ts +13 -15
  73. package/src/session/default-session-manager.ts +1 -3
  74. package/src/session/persistence-service.test.ts +38 -0
  75. package/src/session/persistence-service.ts +16 -1
  76. package/src/session/session-agent-events.ts +9 -1
  77. package/src/session/session-artifacts.ts +16 -0
  78. package/src/session/session-config-builder.ts +46 -0
  79. package/src/team/team-tools.test.ts +104 -0
  80. package/src/team/team-tools.ts +35 -16
  81. package/src/types/config.ts +1 -0
  82. package/src/types/events.ts +4 -0
  83. package/dist/runtime/team-runtime-registry.d.ts +0 -13
  84. package/dist/runtime/team-runtime-registry.d.ts.map +0 -1
  85. package/src/runtime/team-runtime-registry.ts +0 -43
@@ -84,6 +84,7 @@ interface ContributionDescriptor {
84
84
 
85
85
  interface PluginDescriptor {
86
86
  pluginId: string;
87
+ pluginPath: string;
87
88
  name: string;
88
89
  manifest: Record<string, unknown>;
89
90
  contributions: {
@@ -96,6 +97,28 @@ interface PluginDescriptor {
96
97
  };
97
98
  }
98
99
 
100
+ interface PluginInitializationFailure {
101
+ pluginPath: string;
102
+ pluginName?: string;
103
+ phase: "load" | "setup";
104
+ message: string;
105
+ stack?: string;
106
+ }
107
+
108
+ interface PluginInitializationWarning {
109
+ type: "duplicate_plugin_override";
110
+ pluginPath: string;
111
+ pluginName: string;
112
+ overriddenPluginPath: string;
113
+ message: string;
114
+ }
115
+
116
+ interface InitializeResult {
117
+ plugins: PluginDescriptor[];
118
+ failures: PluginInitializationFailure[];
119
+ warnings: PluginInitializationWarning[];
120
+ }
121
+
99
122
  interface PluginState {
100
123
  plugin: PluginModule;
101
124
  handlers: {
@@ -191,104 +214,153 @@ function getPlugin(pluginId: string): PluginState {
191
214
  async function initialize(args: {
192
215
  pluginPaths?: string[];
193
216
  exportName?: string;
194
- }): Promise<PluginDescriptor[]> {
217
+ }): Promise<InitializeResult> {
195
218
  pluginState.clear();
196
219
  pluginCounter = 0;
197
220
  contributionCounters.clear();
198
221
 
199
222
  const descriptors: PluginDescriptor[] = [];
223
+ const failures: PluginInitializationFailure[] = [];
224
+ const warnings: PluginInitializationWarning[] = [];
200
225
  const exportName = args.exportName || "plugin";
226
+ const pluginIndexByName = new Map<string, number>();
201
227
 
202
228
  for (const pluginPath of args.pluginPaths || []) {
203
- const moduleExports = await importPluginModule(pluginPath);
204
- const plugin = (moduleExports.default ??
205
- moduleExports[exportName]) as unknown;
206
- assertValidPluginModule(plugin, pluginPath);
207
-
208
- const pluginId = `plugin_${++pluginCounter}`;
209
- const contributions: PluginDescriptor["contributions"] = {
210
- tools: [],
211
- commands: [],
212
- shortcuts: [],
213
- flags: [],
214
- messageBuilders: [],
215
- providers: [],
216
- };
217
- const handlers: PluginState["handlers"] = {
218
- tools: new Map(),
219
- commands: new Map(),
220
- messageBuilders: new Map(),
221
- };
222
-
223
- const api: PluginApi = {
224
- registerTool: (tool) => {
225
- const id = makeId(pluginId, "tool");
226
- handlers.tools.set(id, tool.execute);
227
- contributions.tools.push({
228
- id,
229
- name: tool.name,
230
- description: tool.description,
231
- inputSchema: tool.inputSchema,
232
- timeoutMs: tool.timeoutMs,
233
- retryable: tool.retryable,
234
- });
235
- },
236
- registerCommand: (command) => {
237
- const id = makeId(pluginId, "command");
238
- if (typeof command.handler === "function") {
239
- handlers.commands.set(id, command.handler);
229
+ let plugin: PluginModule | undefined;
230
+ try {
231
+ const moduleExports = await importPluginModule(pluginPath);
232
+ plugin = (moduleExports.default ??
233
+ moduleExports[exportName]) as unknown as PluginModule;
234
+ assertValidPluginModule(plugin, pluginPath);
235
+
236
+ const pluginId = `plugin_${++pluginCounter}`;
237
+ const contributions: PluginDescriptor["contributions"] = {
238
+ tools: [],
239
+ commands: [],
240
+ shortcuts: [],
241
+ flags: [],
242
+ messageBuilders: [],
243
+ providers: [],
244
+ };
245
+ const handlers: PluginState["handlers"] = {
246
+ tools: new Map(),
247
+ commands: new Map(),
248
+ messageBuilders: new Map(),
249
+ };
250
+
251
+ const api: PluginApi = {
252
+ registerTool: (tool) => {
253
+ const id = makeId(pluginId, "tool");
254
+ handlers.tools.set(id, tool.execute);
255
+ contributions.tools.push({
256
+ id,
257
+ name: tool.name,
258
+ description: tool.description,
259
+ inputSchema: tool.inputSchema,
260
+ timeoutMs: tool.timeoutMs,
261
+ retryable: tool.retryable,
262
+ });
263
+ },
264
+ registerCommand: (command) => {
265
+ const id = makeId(pluginId, "command");
266
+ if (typeof command.handler === "function") {
267
+ handlers.commands.set(id, command.handler);
268
+ }
269
+ contributions.commands.push({
270
+ id,
271
+ name: command.name,
272
+ description: command.description,
273
+ });
274
+ },
275
+ registerShortcut: (shortcut) => {
276
+ contributions.shortcuts.push({
277
+ id: makeId(pluginId, "shortcut"),
278
+ name: shortcut.name,
279
+ value: shortcut.value,
280
+ description: shortcut.description,
281
+ });
282
+ },
283
+ registerFlag: (flag) => {
284
+ contributions.flags.push({
285
+ id: makeId(pluginId, "flag"),
286
+ name: flag.name,
287
+ description: flag.description,
288
+ defaultValue: flag.defaultValue,
289
+ });
290
+ },
291
+ registerMessageBuilder: (builder) => {
292
+ const id = makeId(pluginId, "builder");
293
+ handlers.messageBuilders.set(id, builder.build);
294
+ contributions.messageBuilders.push({ id, name: builder.name });
295
+ },
296
+ registerProvider: (provider) => {
297
+ contributions.providers.push({
298
+ id: makeId(pluginId, "provider"),
299
+ name: provider.name,
300
+ description: provider.description,
301
+ metadata: sanitizeObject(provider.metadata),
302
+ });
303
+ },
304
+ };
305
+
306
+ if (typeof plugin.setup === "function") {
307
+ try {
308
+ await plugin.setup(api);
309
+ } catch (error) {
310
+ failures.push({
311
+ pluginPath,
312
+ pluginName: plugin.name,
313
+ phase: "setup",
314
+ message: error instanceof Error ? error.message : String(error),
315
+ stack: error instanceof Error ? error.stack : undefined,
316
+ });
317
+ continue;
318
+ }
319
+ }
320
+
321
+ const previousIndex = pluginIndexByName.get(plugin.name);
322
+ if (previousIndex !== undefined) {
323
+ const previous = descriptors[previousIndex];
324
+ if (!previous) {
325
+ pluginIndexByName.delete(plugin.name);
326
+ } else {
327
+ warnings.push({
328
+ type: "duplicate_plugin_override",
329
+ pluginName: plugin.name,
330
+ pluginPath,
331
+ overriddenPluginPath: previous.pluginPath,
332
+ message: `Plugin "${plugin.name}" from ${pluginPath} overrides ${previous.pluginPath}`,
333
+ });
334
+ pluginState.delete(previous.pluginId);
335
+ descriptors.splice(previousIndex, 1);
336
+ pluginIndexByName.clear();
337
+ for (const [index, descriptor] of descriptors.entries()) {
338
+ pluginIndexByName.set(descriptor.name, index);
339
+ }
240
340
  }
241
- contributions.commands.push({
242
- id,
243
- name: command.name,
244
- description: command.description,
245
- });
246
- },
247
- registerShortcut: (shortcut) => {
248
- contributions.shortcuts.push({
249
- id: makeId(pluginId, "shortcut"),
250
- name: shortcut.name,
251
- value: shortcut.value,
252
- description: shortcut.description,
253
- });
254
- },
255
- registerFlag: (flag) => {
256
- contributions.flags.push({
257
- id: makeId(pluginId, "flag"),
258
- name: flag.name,
259
- description: flag.description,
260
- defaultValue: flag.defaultValue,
261
- });
262
- },
263
- registerMessageBuilder: (builder) => {
264
- const id = makeId(pluginId, "builder");
265
- handlers.messageBuilders.set(id, builder.build);
266
- contributions.messageBuilders.push({ id, name: builder.name });
267
- },
268
- registerProvider: (provider) => {
269
- contributions.providers.push({
270
- id: makeId(pluginId, "provider"),
271
- name: provider.name,
272
- description: provider.description,
273
- metadata: sanitizeObject(provider.metadata),
274
- });
275
- },
276
- };
277
-
278
- if (typeof plugin.setup === "function") {
279
- await plugin.setup(api);
341
+ }
342
+
343
+ pluginState.set(pluginId, { plugin, handlers });
344
+ pluginIndexByName.set(plugin.name, descriptors.length);
345
+ descriptors.push({
346
+ pluginId,
347
+ pluginPath,
348
+ name: plugin.name,
349
+ manifest: plugin.manifest,
350
+ contributions,
351
+ });
352
+ } catch (error) {
353
+ failures.push({
354
+ pluginPath,
355
+ pluginName: plugin?.name,
356
+ phase: "load",
357
+ message: error instanceof Error ? error.message : String(error),
358
+ stack: error instanceof Error ? error.stack : undefined,
359
+ });
280
360
  }
281
-
282
- pluginState.set(pluginId, { plugin, handlers });
283
- descriptors.push({
284
- pluginId,
285
- name: plugin.name,
286
- manifest: plugin.manifest,
287
- contributions,
288
- });
289
361
  }
290
362
 
291
- return descriptors;
363
+ return { plugins: descriptors, failures, warnings };
292
364
  }
293
365
 
294
366
  async function invokeHook(args: {
@@ -184,6 +184,31 @@ describe("plugin-sandbox", () => {
184
184
  "utf8",
185
185
  );
186
186
 
187
+ await writeFile(
188
+ join(dir, "plugin-broken-setup.mjs"),
189
+ [
190
+ "export default {",
191
+ " name: 'sandbox-broken-setup',",
192
+ " manifest: { capabilities: ['tools'] },",
193
+ " async setup() {",
194
+ " throw new Error('broken setup');",
195
+ " },",
196
+ "};",
197
+ ].join("\n"),
198
+ "utf8",
199
+ );
200
+
201
+ await writeFile(
202
+ join(dir, "plugin-duplicate-a.mjs"),
203
+ "export default { name: 'sandbox-duplicate', manifest: { capabilities: ['tools'] } };",
204
+ "utf8",
205
+ );
206
+ await writeFile(
207
+ join(dir, "plugin-duplicate-b.mjs"),
208
+ "export default { name: 'sandbox-duplicate', manifest: { capabilities: ['commands'] } };",
209
+ "utf8",
210
+ );
211
+
187
212
  sharedSandbox = await loadSandboxedPlugins({
188
213
  pluginPaths: [
189
214
  join(dir, "plugin.mjs"),
@@ -321,6 +346,51 @@ describe("plugin-sandbox", () => {
321
346
  expect(result).toEqual({ echoed: "ok" });
322
347
  });
323
348
 
349
+ it("continues loading remaining sandbox plugins when one setup fails", async () => {
350
+ const sandboxed = await loadSandboxedPlugins({
351
+ pluginPaths: [
352
+ join(dir, "plugin.mjs"),
353
+ join(dir, "plugin-broken-setup.mjs"),
354
+ join(dir, "plugin-events.mjs"),
355
+ ],
356
+ });
357
+
358
+ try {
359
+ expect(sandboxed.extensions?.map((extension) => extension.name)).toEqual([
360
+ "sandbox-test",
361
+ "sandbox-events",
362
+ ]);
363
+ expect(sandboxed.failures).toHaveLength(1);
364
+ expect(sandboxed.failures[0]?.pluginName).toBe("sandbox-broken-setup");
365
+ expect(sandboxed.failures[0]?.phase).toBe("setup");
366
+ } finally {
367
+ await sandboxed.shutdown();
368
+ }
369
+ });
370
+
371
+ it("keeps the later duplicate sandbox plugin and reports the override", async () => {
372
+ const sandboxed = await loadSandboxedPlugins({
373
+ pluginPaths: [
374
+ join(dir, "plugin-duplicate-a.mjs"),
375
+ join(dir, "plugin-duplicate-b.mjs"),
376
+ ],
377
+ });
378
+
379
+ try {
380
+ expect(sandboxed.extensions).toHaveLength(1);
381
+ expect(sandboxed.extensions?.[0]?.name).toBe("sandbox-duplicate");
382
+ expect(sandboxed.extensions?.[0]?.manifest.capabilities).toEqual([
383
+ "commands",
384
+ ]);
385
+ expect(sandboxed.warnings).toHaveLength(1);
386
+ expect(sandboxed.warnings[0]?.overriddenPluginPath).toBe(
387
+ join(dir, "plugin-duplicate-a.mjs"),
388
+ );
389
+ } finally {
390
+ await sandboxed.shutdown();
391
+ }
392
+ });
393
+
324
394
  it("resolves plugin-local dependencies in the sandbox process", async () => {
325
395
  expect(sharedExtensions.get("sandbox-local-dep")?.name).toBe(
326
396
  "sandbox-local-dep",
@@ -4,6 +4,7 @@ import { dirname, join } from "node:path";
4
4
  import { fileURLToPath } from "node:url";
5
5
  import type { AgentConfig, HookStage, Tool } from "@clinebot/shared";
6
6
  import { SubprocessSandbox } from "../../runtime/subprocess-sandbox";
7
+ import type { PluginLoadDiagnostics } from "./plugin-load-report";
7
8
 
8
9
  export interface PluginSandboxOptions {
9
10
  pluginPaths: string[];
@@ -31,6 +32,7 @@ type SandboxedContributionDescriptor = {
31
32
 
32
33
  type SandboxedPluginDescriptor = {
33
34
  pluginId: string;
35
+ pluginPath: string;
34
36
  name: string;
35
37
  manifest: AgentExtension["manifest"];
36
38
  contributions: {
@@ -43,6 +45,10 @@ type SandboxedPluginDescriptor = {
43
45
  };
44
46
  };
45
47
 
48
+ type SandboxedInitializeResult = {
49
+ plugins: SandboxedPluginDescriptor[];
50
+ } & PluginLoadDiagnostics;
51
+
46
52
  function isUnknownPluginIdError(error: unknown): boolean {
47
53
  const message = error instanceof Error ? error.message : String(error);
48
54
  return message.includes("Unknown sandbox plugin id:");
@@ -153,10 +159,12 @@ function withTimeoutFallback(
153
159
 
154
160
  export async function loadSandboxedPlugins(
155
161
  options: PluginSandboxOptions,
156
- ): Promise<{
157
- extensions: AgentConfig["extensions"];
158
- shutdown: () => Promise<void>;
159
- }> {
162
+ ): Promise<
163
+ {
164
+ extensions: AgentConfig["extensions"];
165
+ shutdown: () => Promise<void>;
166
+ } & PluginLoadDiagnostics
167
+ > {
160
168
  const sandbox = new SubprocessSandbox({
161
169
  name: "plugin-sandbox",
162
170
  ...("file" in BOOTSTRAP
@@ -187,9 +195,9 @@ export async function loadSandboxedPlugins(
187
195
  return reinitPromise;
188
196
  };
189
197
 
190
- let descriptors: SandboxedPluginDescriptor[];
198
+ let initialized: SandboxedInitializeResult;
191
199
  try {
192
- descriptors = await sandbox.call<SandboxedPluginDescriptor[]>(
200
+ initialized = await sandbox.call<SandboxedInitializeResult>(
193
201
  "initialize",
194
202
  initArgs,
195
203
  { timeoutMs: importTimeoutMs },
@@ -200,6 +208,7 @@ export async function loadSandboxedPlugins(
200
208
  });
201
209
  throw error;
202
210
  }
211
+ const descriptors = initialized.plugins;
203
212
 
204
213
  const extensions: NonNullable<AgentConfig["extensions"]> = descriptors.map(
205
214
  (descriptor) => {
@@ -239,9 +248,11 @@ export async function loadSandboxedPlugins(
239
248
 
240
249
  return {
241
250
  extensions,
251
+ failures: initialized.failures,
242
252
  shutdown: async () => {
243
253
  await sandbox.shutdown();
244
254
  },
255
+ warnings: initialized.warnings,
245
256
  };
246
257
  }
247
258
 
package/src/index.ts CHANGED
@@ -128,6 +128,13 @@ export {
128
128
  OCI_HEADER_OPC_REQUEST_ID,
129
129
  refreshOcaToken,
130
130
  } from "./auth/oca";
131
+ export type {
132
+ LocalOAuthServer,
133
+ LocalOAuthServerOptions,
134
+ OAuthCallbackPayload,
135
+ OAuthServerCloseInfo,
136
+ OAuthServerListeningInfo,
137
+ } from "./auth/server";
131
138
  export { startLocalOAuthServer } from "./auth/server";
132
139
  export type {
133
140
  OAuthCredentials,
@@ -163,12 +170,16 @@ export {
163
170
  } from "./chat/chat-schema";
164
171
  export type {
165
172
  LoadAgentPluginFromPathOptions,
173
+ PluginInitializationFailure,
174
+ PluginInitializationWarning,
175
+ PluginLoadDiagnostics,
166
176
  ResolveAgentPluginPathsOptions,
167
177
  } from "./extensions";
168
178
  export {
169
179
  discoverPluginModulePaths,
170
180
  loadAgentPluginFromPath,
171
181
  loadAgentPluginsFromPaths,
182
+ loadAgentPluginsFromPathsWithDiagnostics,
172
183
  resolveAgentPluginPaths,
173
184
  resolveAndLoadAgentPlugins,
174
185
  resolvePluginConfigSearchPaths,
@@ -922,7 +922,7 @@ describe("listLocalProviders", () => {
922
922
  ).toBe(true);
923
923
  });
924
924
 
925
- it("uses the same built-in model list for cline as vercel-ai-gateway", async () => {
925
+ it("uses the same built-in model list for cline as openrouter", async () => {
926
926
  manager.saveProviderSettings(
927
927
  {
928
928
  provider: "cline",
@@ -935,12 +935,12 @@ describe("listLocalProviders", () => {
935
935
 
936
936
  const { providers } = await listLocalProviders(manager);
937
937
  const cline = providers.find((provider) => provider.id === "cline");
938
- const gateway = providers.find(
939
- (provider) => provider.id === "vercel-ai-gateway",
938
+ const openrouter = providers.find(
939
+ (provider) => provider.id === "openrouter",
940
940
  );
941
941
 
942
942
  expect(cline?.modelList?.length).toBeGreaterThan(0);
943
- expect(cline?.modelList).toEqual(gateway?.modelList);
943
+ expect(cline?.modelList).toEqual(openrouter?.modelList);
944
944
  });
945
945
 
946
946
  it("does not eagerly fetch LiteLLM private models while listing providers", async () => {
@@ -50,7 +50,12 @@ describe("createHookConfigFileHooks", () => {
50
50
  });
51
51
  expect(hooks).toBeUndefined();
52
52
  } finally {
53
- await rm(workspace, { recursive: true, force: true });
53
+ await rm(workspace, {
54
+ recursive: true,
55
+ force: true,
56
+ maxRetries: 3,
57
+ retryDelay: 250,
58
+ });
54
59
  }
55
60
  });
56
61
 
@@ -78,7 +83,12 @@ describe("createHookConfigFileHooks", () => {
78
83
  });
79
84
  expect(control).toMatchObject({ cancel: true, context: "legacy-ok" });
80
85
  } finally {
81
- await rm(workspace, { recursive: true, force: true });
86
+ await rm(workspace, {
87
+ recursive: true,
88
+ force: true,
89
+ maxRetries: 3,
90
+ retryDelay: 250,
91
+ });
82
92
  }
83
93
  });
84
94
 
@@ -106,7 +116,12 @@ describe("createHookConfigFileHooks", () => {
106
116
  });
107
117
  expect(control).toMatchObject({ cancel: false, context: "shebang-ok" });
108
118
  } finally {
109
- await rm(workspace, { recursive: true, force: true });
119
+ await rm(workspace, {
120
+ recursive: true,
121
+ force: true,
122
+ maxRetries: 3,
123
+ retryDelay: 250,
124
+ });
110
125
  }
111
126
  });
112
127
 
@@ -137,7 +152,12 @@ describe("createHookConfigFileHooks", () => {
137
152
  context: "needs-review",
138
153
  });
139
154
  } finally {
140
- await rm(workspace, { recursive: true, force: true });
155
+ await rm(workspace, {
156
+ recursive: true,
157
+ force: true,
158
+ maxRetries: 3,
159
+ retryDelay: 250,
160
+ });
141
161
  }
142
162
  });
143
163
 
@@ -168,7 +188,12 @@ describe("createHookConfigFileHooks", () => {
168
188
  context: "python-ok",
169
189
  });
170
190
  } finally {
171
- await rm(workspace, { recursive: true, force: true });
191
+ await rm(workspace, {
192
+ recursive: true,
193
+ force: true,
194
+ maxRetries: 3,
195
+ retryDelay: 250,
196
+ });
172
197
  }
173
198
  });
174
199
 
@@ -201,7 +226,12 @@ describe("createHookConfigFileHooks", () => {
201
226
  context: "powershell-ok",
202
227
  });
203
228
  } finally {
204
- await rm(workspace, { recursive: true, force: true });
229
+ await rm(workspace, {
230
+ recursive: true,
231
+ force: true,
232
+ maxRetries: 3,
233
+ retryDelay: 250,
234
+ });
205
235
  }
206
236
  },
207
237
  );
@@ -231,7 +261,12 @@ describe("createHookConfigFileHooks", () => {
231
261
  expect(payload.hookName).toBe("agent_error");
232
262
  expect(payload.error?.message).toBe("401 unauthorized");
233
263
  } finally {
234
- await rm(workspace, { recursive: true, force: true });
264
+ await rm(workspace, {
265
+ recursive: true,
266
+ force: true,
267
+ maxRetries: 3,
268
+ retryDelay: 250,
269
+ });
235
270
  }
236
271
  });
237
272
 
@@ -186,6 +186,22 @@ describe("DefaultRuntimeBuilder", () => {
186
186
  expect(runtime.tools).toEqual([]);
187
187
  });
188
188
 
189
+ it("omits tools disabled by policy from the advertised runtime tool list", async () => {
190
+ const runtime = await new DefaultRuntimeBuilder().build({
191
+ config: makeBaseConfig({
192
+ toolPolicies: {
193
+ run_commands: { enabled: false },
194
+ read_files: { enabled: false },
195
+ },
196
+ }),
197
+ });
198
+
199
+ const names = runtime.tools.map((tool) => tool.name);
200
+ expect(names).not.toContain("run_commands");
201
+ expect(names).not.toContain("read_files");
202
+ expect(names).toContain("search_codebase");
203
+ });
204
+
189
205
  it("adds spawn tool when enabled", async () => {
190
206
  const runtime = await new DefaultRuntimeBuilder().build({
191
207
  config: makeBaseConfig({
@@ -284,6 +300,88 @@ process.stdin.on("data", (chunk) => {
284
300
  }
285
301
  });
286
302
 
303
+ it("skips MCP settings tools when disableMcpSettingsTools is true", async () => {
304
+ const tempRoot = mkdtempSync(
305
+ join(tmpdir(), "runtime-builder-mcp-disabled-"),
306
+ );
307
+ const serverPath = join(tempRoot, "mock-mcp-server.js");
308
+ const settingsPath = join(tempRoot, "cline_mcp_settings.json");
309
+ const previousSettingsPath = process.env.CLINE_MCP_SETTINGS_PATH;
310
+
311
+ writeFileSync(
312
+ serverPath,
313
+ `let buffer = "";
314
+ function write(payload) {
315
+ const body = JSON.stringify(payload);
316
+ process.stdout.write("Content-Length: " + Buffer.byteLength(body, "utf8") + "\\r\\n\\r\\n" + body);
317
+ }
318
+ function handle(message) {
319
+ if (message.method === "initialize") {
320
+ write({ jsonrpc: "2.0", id: message.id, result: { protocolVersion: "2024-11-05", capabilities: { tools: {} }, serverInfo: { name: "mock", version: "1.0.0" } } });
321
+ return;
322
+ }
323
+ if (message.method === "tools/list") {
324
+ write({ jsonrpc: "2.0", id: message.id, result: { tools: [{ name: "echo", description: "Echo tool", inputSchema: { type: "object", properties: { value: { type: "string" } }, required: [] } }] } });
325
+ return;
326
+ }
327
+ if (message.method === "tools/call") {
328
+ write({ jsonrpc: "2.0", id: message.id, result: { echoed: message.params?.arguments?.value ?? null } });
329
+ }
330
+ }
331
+ process.stdin.on("data", (chunk) => {
332
+ buffer += chunk.toString("utf8");
333
+ while (true) {
334
+ const separator = buffer.indexOf("\\r\\n\\r\\n");
335
+ if (separator < 0) break;
336
+ const header = buffer.slice(0, separator);
337
+ const match = header.match(/Content-Length:\\s*(\\d+)/i);
338
+ if (!match) throw new Error("missing content length");
339
+ const length = Number(match[1]);
340
+ const start = separator + 4;
341
+ const end = start + length;
342
+ if (buffer.length < end) break;
343
+ const body = buffer.slice(start, end);
344
+ buffer = buffer.slice(end);
345
+ const message = JSON.parse(body);
346
+ if (message.method === "notifications/initialized") continue;
347
+ handle(message);
348
+ }
349
+ });`,
350
+ "utf8",
351
+ );
352
+ writeFileSync(
353
+ settingsPath,
354
+ JSON.stringify(
355
+ {
356
+ mcpServers: {
357
+ mock: {
358
+ command: process.execPath,
359
+ args: [serverPath],
360
+ },
361
+ },
362
+ },
363
+ null,
364
+ 2,
365
+ ),
366
+ "utf8",
367
+ );
368
+
369
+ process.env.CLINE_MCP_SETTINGS_PATH = settingsPath;
370
+ try {
371
+ const runtime = await new DefaultRuntimeBuilder().build({
372
+ config: makeBaseConfig({
373
+ disableMcpSettingsTools: true,
374
+ }),
375
+ });
376
+ expect(runtime.tools.map((tool) => tool.name)).not.toContain(
377
+ "mock__echo",
378
+ );
379
+ await runtime.shutdown("test");
380
+ } finally {
381
+ process.env.CLINE_MCP_SETTINGS_PATH = previousSettingsPath;
382
+ }
383
+ });
384
+
287
385
  it("skips broken MCP servers without crashing", async () => {
288
386
  const tempRoot = mkdtempSync(join(tmpdir(), "runtime-builder-mcp-bad-"));
289
387
  const serverPath = join(tempRoot, "malformed-mcp-server.js");