fourmis-agents-sdk 0.3.0 → 0.4.0

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 (102) hide show
  1. package/dist/agent-loop.d.ts +21 -3
  2. package/dist/agent-loop.d.ts.map +1 -1
  3. package/dist/agent-loop.js +294 -90
  4. package/dist/agents/index.js +2798 -1857
  5. package/dist/agents/task-manager.js +15 -0
  6. package/dist/agents/tools.d.ts.map +1 -1
  7. package/dist/agents/tools.js +2798 -1857
  8. package/dist/agents/types.d.ts +4 -0
  9. package/dist/agents/types.d.ts.map +1 -1
  10. package/dist/api.d.ts +8 -5
  11. package/dist/api.d.ts.map +1 -1
  12. package/dist/api.js +2394 -886
  13. package/dist/auth/gemini-oauth.js +15 -0
  14. package/dist/auth/login-openai.js +15 -0
  15. package/dist/auth/openai-oauth.js +15 -0
  16. package/dist/hooks.d.ts +19 -1
  17. package/dist/hooks.d.ts.map +1 -1
  18. package/dist/hooks.js +42 -2
  19. package/dist/index.d.ts +10 -1
  20. package/dist/index.d.ts.map +1 -1
  21. package/dist/index.js +2407 -887
  22. package/dist/mcp/client.d.ts +7 -0
  23. package/dist/mcp/client.d.ts.map +1 -1
  24. package/dist/mcp/client.js +146 -12
  25. package/dist/mcp/index.js +146 -12
  26. package/dist/mcp/server.js +15 -0
  27. package/dist/mcp/types.d.ts +19 -1
  28. package/dist/mcp/types.d.ts.map +1 -1
  29. package/dist/memory/index.js +15 -0
  30. package/dist/memory/memory-handler.js +15 -0
  31. package/dist/permissions.d.ts.map +1 -1
  32. package/dist/permissions.js +22 -3
  33. package/dist/providers/anthropic.d.ts.map +1 -1
  34. package/dist/providers/anthropic.js +56 -2
  35. package/dist/providers/gemini.js +15 -0
  36. package/dist/providers/openai.js +15 -0
  37. package/dist/providers/registry.js +56 -2
  38. package/dist/providers/types.d.ts +4 -1
  39. package/dist/providers/types.d.ts.map +1 -1
  40. package/dist/query.d.ts +21 -2
  41. package/dist/query.d.ts.map +1 -1
  42. package/dist/query.js +84 -1
  43. package/dist/settings.js +15 -0
  44. package/dist/skills/frontmatter.d.ts +15 -0
  45. package/dist/skills/frontmatter.d.ts.map +1 -0
  46. package/dist/skills/frontmatter.js +66 -0
  47. package/dist/skills/index.d.ts +8 -0
  48. package/dist/skills/index.d.ts.map +1 -0
  49. package/dist/skills/index.js +326 -0
  50. package/dist/skills/skills.d.ts +94 -0
  51. package/dist/skills/skills.d.ts.map +1 -0
  52. package/dist/skills/skills.js +324 -0
  53. package/dist/tools/ask-user-question.d.ts +7 -0
  54. package/dist/tools/ask-user-question.d.ts.map +1 -0
  55. package/dist/tools/ask-user-question.js +63 -0
  56. package/dist/tools/bash.d.ts.map +1 -1
  57. package/dist/tools/bash.js +62 -2
  58. package/dist/tools/config.d.ts +7 -0
  59. package/dist/tools/config.d.ts.map +1 -0
  60. package/dist/tools/config.js +129 -0
  61. package/dist/tools/edit.js +15 -0
  62. package/dist/tools/exit-plan-mode.d.ts +7 -0
  63. package/dist/tools/exit-plan-mode.d.ts.map +1 -0
  64. package/dist/tools/exit-plan-mode.js +49 -0
  65. package/dist/tools/glob.js +15 -0
  66. package/dist/tools/grep.js +15 -0
  67. package/dist/tools/index.d.ts +7 -0
  68. package/dist/tools/index.d.ts.map +1 -1
  69. package/dist/tools/index.js +521 -9
  70. package/dist/tools/mcp-resources.js +15 -0
  71. package/dist/tools/notebook-edit.d.ts +7 -0
  72. package/dist/tools/notebook-edit.d.ts.map +1 -0
  73. package/dist/tools/notebook-edit.js +98 -0
  74. package/dist/tools/presets.d.ts +2 -1
  75. package/dist/tools/presets.d.ts.map +1 -1
  76. package/dist/tools/presets.js +37 -4
  77. package/dist/tools/read.d.ts.map +1 -1
  78. package/dist/tools/read.js +27 -1
  79. package/dist/tools/registry.d.ts +2 -0
  80. package/dist/tools/registry.d.ts.map +1 -1
  81. package/dist/tools/registry.js +25 -0
  82. package/dist/tools/todo-write.d.ts +7 -0
  83. package/dist/tools/todo-write.d.ts.map +1 -0
  84. package/dist/tools/todo-write.js +84 -0
  85. package/dist/tools/web-fetch.d.ts +6 -0
  86. package/dist/tools/web-fetch.d.ts.map +1 -0
  87. package/dist/tools/web-fetch.js +100 -0
  88. package/dist/tools/web-search.d.ts +7 -0
  89. package/dist/tools/web-search.d.ts.map +1 -0
  90. package/dist/tools/web-search.js +93 -0
  91. package/dist/tools/write.js +15 -0
  92. package/dist/types.d.ts +360 -42
  93. package/dist/types.d.ts.map +1 -1
  94. package/dist/types.js +15 -0
  95. package/dist/utils/cost.js +15 -0
  96. package/dist/utils/session-store.d.ts +1 -1
  97. package/dist/utils/session-store.d.ts.map +1 -1
  98. package/dist/utils/session-store.js +64 -2
  99. package/dist/utils/system-prompt.d.ts +4 -0
  100. package/dist/utils/system-prompt.d.ts.map +1 -1
  101. package/dist/utils/system-prompt.js +326 -6
  102. package/package.json +4 -2
package/dist/index.js CHANGED
@@ -1,5 +1,20 @@
1
1
  // @bun
2
+ var __create = Object.create;
3
+ var __getProtoOf = Object.getPrototypeOf;
2
4
  var __defProp = Object.defineProperty;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
7
+ var __toESM = (mod, isNodeMode, target) => {
8
+ target = mod != null ? __create(__getProtoOf(mod)) : {};
9
+ const to = isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target;
10
+ for (let key of __getOwnPropNames(mod))
11
+ if (!__hasOwnProp.call(to, key))
12
+ __defProp(to, key, {
13
+ get: () => mod[key],
14
+ enumerable: true
15
+ });
16
+ return to;
17
+ };
3
18
  var __export = (target, all) => {
4
19
  for (var name in all)
5
20
  __defProp(target, name, {
@@ -12,76 +27,6 @@ var __export = (target, all) => {
12
27
  var __esm = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
13
28
  var __require = import.meta.require;
14
29
 
15
- // src/tools/mcp-resources.ts
16
- var exports_mcp_resources = {};
17
- __export(exports_mcp_resources, {
18
- createReadMcpResourceTool: () => createReadMcpResourceTool,
19
- createListMcpResourcesTool: () => createListMcpResourcesTool
20
- });
21
- function createListMcpResourcesTool(mcpClient) {
22
- return {
23
- name: "mcp__list_resources",
24
- description: "List available resources from MCP servers.",
25
- inputSchema: {
26
- type: "object",
27
- properties: {
28
- server: {
29
- type: "string",
30
- description: "Optional server name to filter by. If omitted, lists resources from all servers."
31
- }
32
- }
33
- },
34
- async execute(input) {
35
- const { server } = input ?? {};
36
- try {
37
- const resources = await mcpClient.listResources(server);
38
- if (resources.length === 0) {
39
- return { content: "No resources available." };
40
- }
41
- const lines = resources.map((r) => `[${r.server}] ${r.uri} - ${r.name}${r.description ? `: ${r.description}` : ""}`);
42
- return { content: lines.join(`
43
- `) };
44
- } catch (err) {
45
- const message = err instanceof Error ? err.message : String(err);
46
- return { content: `Error listing resources: ${message}`, isError: true };
47
- }
48
- }
49
- };
50
- }
51
- function createReadMcpResourceTool(mcpClient) {
52
- return {
53
- name: "mcp__read_resource",
54
- description: "Read a specific resource from an MCP server by URI.",
55
- inputSchema: {
56
- type: "object",
57
- properties: {
58
- server: {
59
- type: "string",
60
- description: "The MCP server name that hosts the resource."
61
- },
62
- uri: {
63
- type: "string",
64
- description: "The resource URI to read."
65
- }
66
- },
67
- required: ["server", "uri"]
68
- },
69
- async execute(input) {
70
- const { server, uri } = input;
71
- if (!server || !uri) {
72
- return { content: "Both 'server' and 'uri' are required.", isError: true };
73
- }
74
- try {
75
- const content = await mcpClient.readResource(server, uri);
76
- return { content };
77
- } catch (err) {
78
- const message = err instanceof Error ? err.message : String(err);
79
- return { content: `Error reading resource: ${message}`, isError: true };
80
- }
81
- }
82
- };
83
- }
84
-
85
30
  // src/auth/openai-oauth.ts
86
31
  var exports_openai_oauth = {};
87
32
  __export(exports_openai_oauth, {
@@ -95,25 +40,25 @@ __export(exports_openai_oauth, {
95
40
  decodeJwtPayload: () => decodeJwtPayload
96
41
  });
97
42
  import { randomBytes, createHash } from "crypto";
98
- import { readFileSync, writeFileSync, mkdirSync, existsSync } from "fs";
99
- import { join } from "path";
100
- import { homedir } from "os";
43
+ import { readFileSync as readFileSync2, writeFileSync as writeFileSync2, mkdirSync as mkdirSync2, existsSync as existsSync2 } from "fs";
44
+ import { join as join2 } from "path";
45
+ import { homedir as homedir2 } from "os";
101
46
  function getHome() {
102
- return process.env.HOME ?? homedir();
47
+ return process.env.HOME ?? homedir2();
103
48
  }
104
49
  function tokenDir() {
105
- return join(getHome(), ".fourmis");
50
+ return join2(getHome(), ".fourmis");
106
51
  }
107
52
  function tokenPath() {
108
- return join(tokenDir(), "openai-auth.json");
53
+ return join2(tokenDir(), "openai-auth.json");
109
54
  }
110
55
  function codexFallbackPath() {
111
- return join(getHome(), ".codex", "auth.json");
56
+ return join2(getHome(), ".codex", "auth.json");
112
57
  }
113
58
  function loadTokens() {
114
59
  for (const p of [tokenPath(), codexFallbackPath()]) {
115
60
  try {
116
- const raw = readFileSync(p, "utf-8");
61
+ const raw = readFileSync2(p, "utf-8");
117
62
  const data = JSON.parse(raw);
118
63
  if (data.access_token && data.account_id) {
119
64
  return data;
@@ -124,10 +69,10 @@ function loadTokens() {
124
69
  }
125
70
  function saveTokens(tokens) {
126
71
  const dir = tokenDir();
127
- if (!existsSync(dir)) {
128
- mkdirSync(dir, { recursive: true });
72
+ if (!existsSync2(dir)) {
73
+ mkdirSync2(dir, { recursive: true });
129
74
  }
130
- writeFileSync(tokenPath(), JSON.stringify(tokens, null, 2), { mode: 384 });
75
+ writeFileSync2(tokenPath(), JSON.stringify(tokens, null, 2), { mode: 384 });
131
76
  }
132
77
  function generateCodeVerifier() {
133
78
  return randomBytes(64).toString("base64url");
@@ -377,19 +322,19 @@ __export(exports_gemini_oauth, {
377
322
  isLoggedIn: () => isLoggedIn2,
378
323
  getValidToken: () => getValidToken2
379
324
  });
380
- import { readFileSync as readFileSync2, writeFileSync as writeFileSync2, existsSync as existsSync2 } from "fs";
381
- import { join as join2 } from "path";
382
- import { homedir as homedir2 } from "os";
325
+ import { readFileSync as readFileSync3, writeFileSync as writeFileSync3, existsSync as existsSync3 } from "fs";
326
+ import { join as join3 } from "path";
327
+ import { homedir as homedir3 } from "os";
383
328
  function getHome2() {
384
- return process.env.HOME ?? homedir2();
329
+ return process.env.HOME ?? homedir3();
385
330
  }
386
331
  function tokenPath2() {
387
- return join2(getHome2(), ".gemini", "oauth_creds.json");
332
+ return join3(getHome2(), ".gemini", "oauth_creds.json");
388
333
  }
389
334
  function loadTokens2() {
390
335
  const p = tokenPath2();
391
336
  try {
392
- const raw = readFileSync2(p, "utf-8");
337
+ const raw = readFileSync3(p, "utf-8");
393
338
  const data = JSON.parse(raw);
394
339
  if (data.access_token && data.refresh_token) {
395
340
  return data;
@@ -402,12 +347,12 @@ function loadTokensSync2() {
402
347
  }
403
348
  function saveTokens2(tokens) {
404
349
  const p = tokenPath2();
405
- const dir = join2(getHome2(), ".gemini");
406
- if (!existsSync2(dir)) {
407
- const { mkdirSync: mkdirSync2 } = __require("fs");
408
- mkdirSync2(dir, { recursive: true });
350
+ const dir = join3(getHome2(), ".gemini");
351
+ if (!existsSync3(dir)) {
352
+ const { mkdirSync: mkdirSync3 } = __require("fs");
353
+ mkdirSync3(dir, { recursive: true });
409
354
  }
410
- writeFileSync2(p, JSON.stringify(tokens, null, 2), { mode: 384 });
355
+ writeFileSync3(p, JSON.stringify(tokens, null, 2), { mode: 384 });
411
356
  }
412
357
  async function refreshAccessToken2(refreshToken) {
413
358
  const res = await fetch(GOOGLE_TOKEN_URL, {
@@ -461,330 +406,366 @@ var init_gemini_oauth = __esm(() => {
461
406
  GEMINI_CLIENT_SECRET = process.env.GEMINI_OAUTH_CLIENT_SECRET ?? ["GOCSPX", "4uHgMPm", "1o7Sk", "geV6Cu5clXFsxl"].join("-");
462
407
  });
463
408
 
464
- // src/types.ts
465
- function uuid() {
466
- return crypto.randomUUID();
467
- }
468
- function emptyTokenUsage() {
409
+ // src/tools/mcp-resources.ts
410
+ var exports_mcp_resources = {};
411
+ __export(exports_mcp_resources, {
412
+ createReadMcpResourceTool: () => createReadMcpResourceTool,
413
+ createListMcpResourcesTool: () => createListMcpResourcesTool
414
+ });
415
+ function createListMcpResourcesTool(mcpClient) {
469
416
  return {
470
- inputTokens: 0,
471
- outputTokens: 0,
472
- cacheReadInputTokens: 0,
473
- cacheCreationInputTokens: 0
417
+ name: "mcp__list_resources",
418
+ description: "List available resources from MCP servers.",
419
+ inputSchema: {
420
+ type: "object",
421
+ properties: {
422
+ server: {
423
+ type: "string",
424
+ description: "Optional server name to filter by. If omitted, lists resources from all servers."
425
+ }
426
+ }
427
+ },
428
+ async execute(input) {
429
+ const { server } = input ?? {};
430
+ try {
431
+ const resources = await mcpClient.listResources(server);
432
+ if (resources.length === 0) {
433
+ return { content: "No resources available." };
434
+ }
435
+ const lines = resources.map((r) => `[${r.server}] ${r.uri} - ${r.name}${r.description ? `: ${r.description}` : ""}`);
436
+ return { content: lines.join(`
437
+ `) };
438
+ } catch (err) {
439
+ const message = err instanceof Error ? err.message : String(err);
440
+ return { content: `Error listing resources: ${message}`, isError: true };
441
+ }
442
+ }
474
443
  };
475
444
  }
476
- function mergeUsage(a, b) {
445
+ function createReadMcpResourceTool(mcpClient) {
477
446
  return {
478
- inputTokens: a.inputTokens + b.inputTokens,
479
- outputTokens: a.outputTokens + b.outputTokens,
480
- cacheReadInputTokens: a.cacheReadInputTokens + b.cacheReadInputTokens,
481
- cacheCreationInputTokens: a.cacheCreationInputTokens + b.cacheCreationInputTokens
447
+ name: "mcp__read_resource",
448
+ description: "Read a specific resource from an MCP server by URI.",
449
+ inputSchema: {
450
+ type: "object",
451
+ properties: {
452
+ server: {
453
+ type: "string",
454
+ description: "The MCP server name that hosts the resource."
455
+ },
456
+ uri: {
457
+ type: "string",
458
+ description: "The resource URI to read."
459
+ }
460
+ },
461
+ required: ["server", "uri"]
462
+ },
463
+ async execute(input) {
464
+ const { server, uri } = input;
465
+ if (!server || !uri) {
466
+ return { content: "Both 'server' and 'uri' are required.", isError: true };
467
+ }
468
+ try {
469
+ const content = await mcpClient.readResource(server, uri);
470
+ return { content };
471
+ } catch (err) {
472
+ const message = err instanceof Error ? err.message : String(err);
473
+ return { content: `Error reading resource: ${message}`, isError: true };
474
+ }
475
+ }
482
476
  };
483
477
  }
484
478
 
485
- // src/agent-loop.ts
486
- async function* agentLoop(prompt, options) {
487
- const {
488
- provider,
489
- model,
490
- systemPrompt,
491
- tools,
492
- permissions,
493
- cwd,
494
- sessionId,
495
- maxTurns,
496
- maxBudgetUsd,
497
- includeStreamEvents,
498
- signal,
499
- env,
500
- debug,
501
- hooks,
502
- mcpClient,
503
- previousMessages,
504
- sessionLogger,
505
- nativeMemoryTool
506
- } = options;
507
- const startTime = Date.now();
508
- let apiTimeMs = 0;
509
- let turns = 0;
510
- let totalUsage = emptyTokenUsage();
511
- let costUsd = 0;
512
- const modelUsage = {};
513
- if (mcpClient) {
514
- await mcpClient.connectAll();
515
- for (const tool of mcpClient.getTools()) {
516
- tools.register(tool);
517
- }
518
- const { createListMcpResourcesTool: createListMcpResourcesTool2, createReadMcpResourceTool: createReadMcpResourceTool2 } = await Promise.resolve().then(() => exports_mcp_resources);
519
- tools.register(createListMcpResourcesTool2(mcpClient));
520
- tools.register(createReadMcpResourceTool2(mcpClient));
521
- }
522
- const messages = [
523
- ...previousMessages ?? [],
524
- { role: "user", content: prompt }
525
- ];
526
- if (sessionLogger) {
527
- sessionLogger("user", prompt, null);
479
+ // src/permissions.ts
480
+ var SAFE_TOOLS = new Set(["Read", "Glob", "Grep", "WebFetch", "WebSearch"]);
481
+ var EDIT_TOOLS = new Set(["Write", "Edit", "NotebookEdit", "TodoWrite", "Config"]);
482
+ var FS_COMMANDS = ["mkdir", "touch", "rm", "mv", "cp"];
483
+ var DELEGATE_TOOLS = new Set(["Teammate", "Task", "TaskOutput", "TaskStop"]);
484
+ function normalizeRules(rules) {
485
+ if (!rules)
486
+ return [];
487
+ return rules.map((r) => typeof r === "string" ? { toolName: r } : r);
488
+ }
489
+ function matchesRule(rules, toolName, input) {
490
+ for (const rule of rules) {
491
+ if (rule.toolName !== toolName)
492
+ continue;
493
+ if (!rule.ruleContent)
494
+ return true;
495
+ const inputStr = toolName === "Bash" ? String(input?.command ?? "") : JSON.stringify(input ?? {});
496
+ if (inputStr.includes(rule.ruleContent))
497
+ return true;
528
498
  }
529
- yield {
530
- type: "init",
531
- sessionId,
532
- model,
533
- provider: provider.name,
534
- tools: tools.list(),
535
- cwd,
536
- uuid: uuid()
537
- };
538
- if (hooks) {
539
- await hooks.fire("SessionStart", { event: "SessionStart", session_id: sessionId }, undefined, { signal });
499
+ return false;
500
+ }
501
+
502
+ class PermissionManager {
503
+ mode;
504
+ canUseTool;
505
+ allowRules;
506
+ denyRules;
507
+ settingsManager;
508
+ constructor(mode = "default", canUseTool, permissions, settingsManager) {
509
+ this.mode = mode;
510
+ this.canUseTool = canUseTool;
511
+ this.allowRules = normalizeRules(permissions?.allow);
512
+ this.denyRules = normalizeRules(permissions?.deny);
513
+ this.settingsManager = settingsManager;
540
514
  }
541
- while (true) {
542
- if (signal.aborted) {
543
- yield makeError("error_execution", ["Aborted"], turns, costUsd, sessionId, startTime);
544
- return;
515
+ async check(toolName, input, options) {
516
+ if (this.mode === "bypassPermissions") {
517
+ return { behavior: "allow" };
545
518
  }
546
- if (turns >= maxTurns) {
547
- yield makeError("error_max_turns", [`Reached maximum turns (${maxTurns})`], turns, costUsd, sessionId, startTime);
548
- return;
519
+ if (matchesRule(this.denyRules, toolName, input)) {
520
+ return {
521
+ behavior: "deny",
522
+ message: `Tool "${toolName}" is denied by permissions config.`
523
+ };
549
524
  }
550
- if (maxBudgetUsd > 0 && costUsd >= maxBudgetUsd) {
551
- yield makeError("error_max_budget", [`Reached budget limit ($${maxBudgetUsd})`], turns, costUsd, sessionId, startTime);
552
- return;
553
- }
554
- const toolDefs = tools.getDefinitions();
555
- const apiStart = Date.now();
556
- let assistantTextParts = [];
557
- let toolCalls = [];
558
- let turnUsage = emptyTokenUsage();
559
- const nativeTools = nativeMemoryTool ? [nativeMemoryTool.definition] : undefined;
560
- try {
561
- const chunks = provider.chat({
562
- model,
563
- messages,
564
- tools: toolDefs.length > 0 ? toolDefs : undefined,
565
- systemPrompt,
566
- signal,
567
- nativeTools
568
- });
569
- for await (const chunk of chunks) {
570
- switch (chunk.type) {
571
- case "text_delta":
572
- assistantTextParts.push(chunk.text);
573
- if (includeStreamEvents) {
574
- yield { type: "stream", subtype: "text_delta", text: chunk.text, uuid: uuid() };
575
- }
576
- break;
577
- case "thinking_delta":
578
- if (includeStreamEvents) {
579
- yield { type: "stream", subtype: "thinking_delta", text: chunk.text, uuid: uuid() };
580
- }
581
- break;
582
- case "tool_call":
583
- toolCalls.push({ id: chunk.id, name: chunk.name, input: chunk.input });
584
- break;
585
- case "usage":
586
- turnUsage = mergeUsage(turnUsage, chunk.usage);
587
- break;
588
- case "done":
589
- break;
590
- }
525
+ if (this.mode === "plan") {
526
+ if (!SAFE_TOOLS.has(toolName)) {
527
+ return {
528
+ behavior: "deny",
529
+ message: `Tool "${toolName}" is not allowed in plan mode. Only read-only tools are available.`
530
+ };
591
531
  }
592
- } catch (err) {
593
- const message = err instanceof Error ? err.message : String(err);
594
- yield makeError("error_execution", [`API error: ${message}`], turns, costUsd, sessionId, startTime);
595
- return;
596
- }
597
- apiTimeMs += Date.now() - apiStart;
598
- turns++;
599
- totalUsage = mergeUsage(totalUsage, turnUsage);
600
- const turnCost = provider.calculateCost(model, turnUsage);
601
- costUsd += turnCost;
602
- if (!modelUsage[model]) {
603
- modelUsage[model] = {
604
- inputTokens: 0,
605
- outputTokens: 0,
606
- cacheReadInputTokens: 0,
607
- cacheCreationInputTokens: 0,
608
- totalCostUsd: 0
609
- };
610
- }
611
- modelUsage[model].inputTokens += turnUsage.inputTokens;
612
- modelUsage[model].outputTokens += turnUsage.outputTokens;
613
- modelUsage[model].cacheReadInputTokens += turnUsage.cacheReadInputTokens;
614
- modelUsage[model].cacheCreationInputTokens += turnUsage.cacheCreationInputTokens;
615
- modelUsage[model].totalCostUsd += turnCost;
616
- const assistantText = assistantTextParts.join("");
617
- const assistantContent = [];
618
- if (assistantText) {
619
- assistantContent.push({ type: "text", text: assistantText });
532
+ return { behavior: "allow" };
620
533
  }
621
- for (const call of toolCalls) {
622
- assistantContent.push({
623
- type: "tool_use",
624
- id: call.id,
625
- name: call.name,
626
- input: call.input
627
- });
534
+ if (this.mode === "delegate") {
535
+ if (!DELEGATE_TOOLS.has(toolName) && !SAFE_TOOLS.has(toolName)) {
536
+ return {
537
+ behavior: "deny",
538
+ message: `Tool "${toolName}" is not allowed in delegate mode. Only Teammate, Task, and read-only tools are available.`
539
+ };
540
+ }
541
+ return { behavior: "allow" };
628
542
  }
629
- messages.push({ role: "assistant", content: assistantContent });
630
- if (sessionLogger) {
631
- sessionLogger("assistant", assistantContent, null);
543
+ if (matchesRule(this.allowRules, toolName, input)) {
544
+ return { behavior: "allow" };
632
545
  }
633
- if (assistantText) {
634
- yield { type: "text", text: assistantText, uuid: uuid() };
546
+ if (SAFE_TOOLS.has(toolName)) {
547
+ return { behavior: "allow" };
635
548
  }
636
- if (toolCalls.length === 0) {
637
- if (hooks) {
638
- await hooks.fire("Stop", { event: "Stop", session_id: sessionId, text: assistantText || undefined }, undefined, { signal });
549
+ if (this.mode === "acceptEdits") {
550
+ if (EDIT_TOOLS.has(toolName)) {
551
+ return { behavior: "allow" };
639
552
  }
640
- if (hooks) {
641
- await hooks.fire("SessionEnd", { event: "SessionEnd", session_id: sessionId }, undefined, { signal });
553
+ if (toolName === "Bash") {
554
+ const cmd = String(input?.command ?? "").trimStart();
555
+ if (FS_COMMANDS.some((fc) => cmd.startsWith(fc + " ") || cmd === fc)) {
556
+ return { behavior: "allow" };
557
+ }
642
558
  }
643
- yield {
644
- type: "result",
645
- subtype: "success",
646
- text: assistantText || null,
647
- turns,
648
- costUsd,
649
- durationMs: Date.now() - startTime,
650
- durationApiMs: apiTimeMs,
651
- sessionId,
652
- usage: totalUsage,
653
- modelUsage,
654
- uuid: uuid()
559
+ }
560
+ if (this.canUseTool) {
561
+ const result = await this.canUseTool(toolName, input, {
562
+ ...options,
563
+ toolUseID: options.toolUseId,
564
+ agentID: options.agentId
565
+ });
566
+ if (result.behavior === "allow" && result.updatedPermissions) {
567
+ this.applyPermissionUpdates(result.updatedPermissions);
568
+ }
569
+ return result;
570
+ }
571
+ if (this.mode === "dontAsk") {
572
+ return {
573
+ behavior: "deny",
574
+ message: `Tool "${toolName}" requires approval. In dontAsk mode, tools must be pre-approved via permissions config.`
655
575
  };
656
- return;
657
576
  }
658
- const toolResults = [];
659
- for (const call of toolCalls) {
660
- let hookDenied = false;
661
- let hookUpdatedInput;
662
- if (hooks) {
663
- const hookResult = await hooks.fire("PreToolUse", { event: "PreToolUse", tool_name: call.name, tool_input: call.input, session_id: sessionId }, call.id, { signal });
664
- if (hookResult) {
665
- if (hookResult.permissionDecision === "deny") {
666
- hookDenied = true;
577
+ return { behavior: "allow" };
578
+ }
579
+ applyPermissionUpdates(updates) {
580
+ for (const update of updates) {
581
+ if (this.settingsManager && update.destination !== "session" && update.destination !== "cliArg") {
582
+ this.settingsManager.persistUpdate(update);
583
+ }
584
+ switch (update.type) {
585
+ case "addRules":
586
+ for (const rule of update.rules) {
587
+ if (update.behavior === "allow") {
588
+ this.allowRules.push(rule);
589
+ } else if (update.behavior === "deny") {
590
+ this.denyRules.push(rule);
591
+ }
667
592
  }
668
- if (hookResult.updatedInput !== undefined) {
669
- hookUpdatedInput = hookResult.updatedInput;
593
+ break;
594
+ case "removeRules":
595
+ for (const rule of update.rules) {
596
+ if (update.behavior === "allow") {
597
+ this.allowRules = this.allowRules.filter((r) => !(r.toolName === rule.toolName && r.ruleContent === rule.ruleContent));
598
+ } else if (update.behavior === "deny") {
599
+ this.denyRules = this.denyRules.filter((r) => !(r.toolName === rule.toolName && r.ruleContent === rule.ruleContent));
600
+ }
670
601
  }
671
- }
602
+ break;
603
+ case "replaceRules":
604
+ if (update.behavior === "allow") {
605
+ this.allowRules = [...update.rules];
606
+ } else if (update.behavior === "deny") {
607
+ this.denyRules = [...update.rules];
608
+ }
609
+ break;
610
+ case "setMode":
611
+ this.mode = update.mode;
612
+ break;
672
613
  }
673
- if (hookDenied) {
674
- const denyContent = "Denied by hook";
675
- yield {
676
- type: "tool_result",
677
- id: call.id,
678
- name: call.name,
679
- content: denyContent,
680
- isError: true,
681
- uuid: uuid()
682
- };
683
- toolResults.push({
684
- type: "tool_result",
685
- tool_use_id: call.id,
686
- content: denyContent,
687
- is_error: true
688
- });
689
- if (hooks) {
690
- await hooks.fire("PostToolUseFailure", { event: "PostToolUseFailure", tool_name: call.name, tool_result: denyContent, tool_error: true, session_id: sessionId }, call.id, { signal });
691
- }
614
+ }
615
+ }
616
+ setMode(mode) {
617
+ this.mode = mode;
618
+ }
619
+ getMode() {
620
+ return this.mode;
621
+ }
622
+ }
623
+
624
+ // src/settings.ts
625
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
626
+ import { dirname, join } from "path";
627
+ import { homedir } from "os";
628
+
629
+ class SettingsManager {
630
+ cwd;
631
+ constructor(cwd) {
632
+ this.cwd = cwd;
633
+ }
634
+ loadPermissions(sources) {
635
+ const allAllow = [];
636
+ const allDeny = [];
637
+ for (const source of sources) {
638
+ const path = this.sourceToPath(source);
639
+ const data = this.readJson(path);
640
+ if (!data?.permissions)
692
641
  continue;
693
- }
694
- const inputAfterHook = hookUpdatedInput !== undefined ? hookUpdatedInput : call.input;
695
- const permResult = await permissions.check(call.name, inputAfterHook ?? {}, { signal, toolUseId: call.id });
696
- if (permResult.behavior === "deny") {
697
- const denyContent = `Permission denied: ${permResult.message}`;
698
- yield {
699
- type: "tool_result",
700
- id: call.id,
701
- name: call.name,
702
- content: denyContent,
703
- isError: true,
704
- uuid: uuid()
705
- };
706
- toolResults.push({
707
- type: "tool_result",
708
- tool_use_id: call.id,
709
- content: denyContent,
710
- is_error: true
711
- });
712
- if (hooks) {
713
- await hooks.fire("PostToolUseFailure", { event: "PostToolUseFailure", tool_name: call.name, tool_result: denyContent, tool_error: true, session_id: sessionId }, call.id, { signal });
642
+ const perms = data.permissions;
643
+ if (Array.isArray(perms.allow)) {
644
+ for (const rule of perms.allow) {
645
+ if (typeof rule === "string") {
646
+ allAllow.push(this.parseRule(rule));
647
+ }
714
648
  }
715
- continue;
716
649
  }
717
- const toolInput = permResult.behavior === "allow" && permResult.updatedInput ? permResult.updatedInput : inputAfterHook;
718
- yield {
719
- type: "tool_use",
720
- id: call.id,
721
- name: call.name,
722
- input: toolInput,
723
- uuid: uuid()
724
- };
725
- let result;
726
- if (call.name === "memory" && nativeMemoryTool) {
727
- try {
728
- const content = await nativeMemoryTool.execute(toolInput);
729
- result = { content, isError: content.startsWith("Error:") };
730
- } catch (err) {
731
- const message = err instanceof Error ? err.message : String(err);
732
- result = { content: `Error: ${message}`, isError: true };
650
+ if (Array.isArray(perms.deny)) {
651
+ for (const rule of perms.deny) {
652
+ if (typeof rule === "string") {
653
+ allDeny.push(this.parseRule(rule));
654
+ }
733
655
  }
734
- } else {
735
- const toolCtx = {
736
- cwd,
737
- signal,
738
- sessionId,
739
- env
740
- };
741
- result = await tools.execute(call.name, toolInput, toolCtx);
742
656
  }
743
- if (debug) {
744
- console.error(`[debug] Tool ${call.name}: ${result.isError ? "ERROR" : "OK"} (${result.content.length} chars)`);
657
+ }
658
+ const result = {};
659
+ if (allAllow.length > 0)
660
+ result.allow = allAllow;
661
+ if (allDeny.length > 0)
662
+ result.deny = allDeny;
663
+ return result;
664
+ }
665
+ persistUpdate(update) {
666
+ const path = this.destinationToPath(update.destination);
667
+ if (!path)
668
+ return;
669
+ const data = this.readJson(path) ?? {};
670
+ if (!data.permissions)
671
+ data.permissions = {};
672
+ const perms = data.permissions;
673
+ switch (update.type) {
674
+ case "addRules": {
675
+ const key = update.behavior;
676
+ const existing = Array.isArray(perms[key]) ? perms[key] : [];
677
+ const newRules = update.rules.map((r) => this.serializeRule(r));
678
+ const set = new Set(existing);
679
+ for (const rule of newRules)
680
+ set.add(rule);
681
+ perms[key] = [...set];
682
+ break;
745
683
  }
746
- if (hooks) {
747
- if (result.isError) {
748
- await hooks.fire("PostToolUseFailure", { event: "PostToolUseFailure", tool_name: call.name, tool_result: result.content, tool_error: true, session_id: sessionId }, call.id, { signal });
749
- } else {
750
- const postResult = await hooks.fire("PostToolUse", { event: "PostToolUse", tool_name: call.name, tool_result: result.content, session_id: sessionId }, call.id, { signal });
751
- if (postResult?.additionalContext) {
752
- result.content += `
753
- ${postResult.additionalContext}`;
754
- }
755
- }
684
+ case "removeRules": {
685
+ const key = update.behavior;
686
+ if (!Array.isArray(perms[key]))
687
+ break;
688
+ const toRemove = new Set(update.rules.map((r) => this.serializeRule(r)));
689
+ perms[key] = perms[key].filter((r) => !toRemove.has(r));
690
+ break;
756
691
  }
757
- yield {
758
- type: "tool_result",
759
- id: call.id,
760
- name: call.name,
761
- content: result.content,
762
- isError: result.isError,
763
- uuid: uuid()
764
- };
765
- toolResults.push({
766
- type: "tool_result",
767
- tool_use_id: call.id,
768
- content: result.content,
769
- is_error: result.isError
770
- });
692
+ case "replaceRules": {
693
+ const key = update.behavior;
694
+ perms[key] = update.rules.map((r) => this.serializeRule(r));
695
+ break;
696
+ }
697
+ case "setMode": {
698
+ perms.defaultMode = update.mode;
699
+ break;
700
+ }
701
+ default:
702
+ return;
771
703
  }
772
- messages.push({ role: "user", content: toolResults });
773
- if (sessionLogger) {
774
- sessionLogger("user", toolResults, null);
704
+ const dir = dirname(path);
705
+ if (!existsSync(dir))
706
+ mkdirSync(dir, { recursive: true });
707
+ writeFileSync(path, JSON.stringify(data, null, 2) + `
708
+ `);
709
+ }
710
+ sourceToPath(source) {
711
+ switch (source) {
712
+ case "user":
713
+ return join(homedir(), ".claude", "settings.json");
714
+ case "project":
715
+ return join(this.cwd, ".claude", "settings.json");
716
+ case "local":
717
+ return join(this.cwd, ".claude", "settings.local.json");
718
+ }
719
+ }
720
+ destinationToPath(destination) {
721
+ switch (destination) {
722
+ case "userSettings":
723
+ return join(homedir(), ".claude", "settings.json");
724
+ case "projectSettings":
725
+ return join(this.cwd, ".claude", "settings.json");
726
+ case "localSettings":
727
+ return join(this.cwd, ".claude", "settings.local.json");
728
+ default:
729
+ return null;
730
+ }
731
+ }
732
+ readJson(path) {
733
+ try {
734
+ const content = readFileSync(path, "utf-8");
735
+ return JSON.parse(content);
736
+ } catch {
737
+ return null;
775
738
  }
776
739
  }
740
+ parseRule(s) {
741
+ const match = s.match(/^([^(]+)\((.+)\)$/);
742
+ if (match)
743
+ return { toolName: match[1], ruleContent: match[2] };
744
+ return { toolName: s };
745
+ }
746
+ serializeRule(rule) {
747
+ return rule.ruleContent ? `${rule.toolName}(${rule.ruleContent})` : rule.toolName;
748
+ }
749
+ }
750
+
751
+ // src/types.ts
752
+ function uuid() {
753
+ return crypto.randomUUID();
777
754
  }
778
- function makeError(subtype, errors, turns, costUsd, sessionId, startTime) {
755
+ function emptyTokenUsage() {
779
756
  return {
780
- type: "result",
781
- subtype,
782
- errors,
783
- turns,
784
- costUsd,
785
- durationMs: Date.now() - startTime,
786
- sessionId,
787
- uuid: uuid()
757
+ inputTokens: 0,
758
+ outputTokens: 0,
759
+ cacheReadInputTokens: 0,
760
+ cacheCreationInputTokens: 0
761
+ };
762
+ }
763
+ function mergeUsage(a, b) {
764
+ return {
765
+ inputTokens: a.inputTokens + b.inputTokens,
766
+ outputTokens: a.outputTokens + b.outputTokens,
767
+ cacheReadInputTokens: a.cacheReadInputTokens + b.cacheReadInputTokens,
768
+ cacheCreationInputTokens: a.cacheCreationInputTokens + b.cacheCreationInputTokens
788
769
  };
789
770
  }
790
771
 
@@ -1043,6 +1024,44 @@ class AnthropicAdapter {
1043
1024
  max_tokens: maxTokens,
1044
1025
  stream: true
1045
1026
  };
1027
+ if (request.thinking) {
1028
+ switch (request.thinking.type) {
1029
+ case "adaptive":
1030
+ params.thinking = { type: "adaptive" };
1031
+ break;
1032
+ case "disabled":
1033
+ params.thinking = { type: "disabled" };
1034
+ break;
1035
+ case "enabled":
1036
+ params.thinking = {
1037
+ type: "enabled",
1038
+ budget_tokens: request.thinking.budgetTokens
1039
+ };
1040
+ break;
1041
+ }
1042
+ } else if (request.thinkingBudget !== undefined) {
1043
+ if (request.thinkingBudget <= 0) {
1044
+ params.thinking = { type: "disabled" };
1045
+ } else {
1046
+ params.thinking = {
1047
+ type: "enabled",
1048
+ budget_tokens: Math.max(1024, request.thinkingBudget)
1049
+ };
1050
+ }
1051
+ }
1052
+ const outputConfig = {};
1053
+ if (request.effort) {
1054
+ outputConfig.effort = request.effort;
1055
+ }
1056
+ if (request.outputFormat?.type === "json_schema") {
1057
+ outputConfig.format = {
1058
+ type: "json_schema",
1059
+ schema: request.outputFormat.schema
1060
+ };
1061
+ }
1062
+ if (Object.keys(outputConfig).length > 0) {
1063
+ params.output_config = outputConfig;
1064
+ }
1046
1065
  if (this.oauthMode) {
1047
1066
  const systemBlocks = [
1048
1067
  { type: "text", text: "You are Claude Code, Anthropic's official CLI for Claude." }
@@ -1167,11 +1186,12 @@ class AnthropicAdapter {
1167
1186
  switch (feature) {
1168
1187
  case "streaming":
1169
1188
  case "tool_calling":
1170
- case "image_input":
1171
- case "pdf_input":
1172
1189
  case "thinking":
1173
1190
  case "structured_output":
1174
1191
  return true;
1192
+ case "image_input":
1193
+ case "pdf_input":
1194
+ return false;
1175
1195
  default:
1176
1196
  return false;
1177
1197
  }
@@ -2086,6 +2106,16 @@ class ToolRegistry {
2086
2106
  register(tool) {
2087
2107
  this.tools.set(tool.name, tool);
2088
2108
  }
2109
+ unregister(name) {
2110
+ this.tools.delete(name);
2111
+ }
2112
+ clearByPrefix(prefix) {
2113
+ for (const name of this.tools.keys()) {
2114
+ if (name.startsWith(prefix)) {
2115
+ this.tools.delete(name);
2116
+ }
2117
+ }
2118
+ }
2089
2119
  get(name) {
2090
2120
  return this.tools.get(name);
2091
2121
  }
@@ -2119,16 +2149,34 @@ class ToolRegistry {
2119
2149
  // src/tools/presets.ts
2120
2150
  var PRESETS = {
2121
2151
  coding: ["Bash", "Read", "Write", "Edit", "Glob", "Grep"],
2152
+ claude_code: [
2153
+ "Bash",
2154
+ "Read",
2155
+ "Write",
2156
+ "Edit",
2157
+ "Glob",
2158
+ "Grep",
2159
+ "NotebookEdit",
2160
+ "WebFetch",
2161
+ "WebSearch",
2162
+ "TodoWrite",
2163
+ "Config",
2164
+ "AskUserQuestion",
2165
+ "ExitPlanMode"
2166
+ ],
2122
2167
  readonly: ["Read", "Glob", "Grep"],
2123
2168
  minimal: ["Read", "Write", "Edit", "Glob", "Grep"]
2124
2169
  };
2125
2170
  function resolveToolNames(tools) {
2126
2171
  if (!tools)
2127
- return PRESETS.coding;
2128
- if (typeof tools === "string") {
2129
- return PRESETS[tools] ?? [tools];
2172
+ return PRESETS.claude_code;
2173
+ if (Array.isArray(tools)) {
2174
+ return tools;
2175
+ }
2176
+ if (tools.type === "preset") {
2177
+ return PRESETS[tools.preset] ?? PRESETS.claude_code;
2130
2178
  }
2131
- return tools;
2179
+ throw new Error("Invalid tools option. Expected string[] or { type: 'preset', preset: 'claude_code' }.");
2132
2180
  }
2133
2181
 
2134
2182
  // src/tools/bash.ts
@@ -2152,17 +2200,58 @@ var BashTool = {
2152
2200
  timeout: {
2153
2201
  type: "number",
2154
2202
  description: "Timeout in milliseconds (max 600000)"
2203
+ },
2204
+ run_in_background: {
2205
+ type: "boolean",
2206
+ description: "Run command asynchronously and return immediately."
2207
+ },
2208
+ dangerouslyDisableSandbox: {
2209
+ type: "boolean",
2210
+ description: "If true, explicitly request unsandboxed execution."
2211
+ },
2212
+ _simulatedSedEdit: {
2213
+ type: "object",
2214
+ properties: {
2215
+ filePath: { type: "string" },
2216
+ newContent: { type: "string" }
2217
+ },
2218
+ description: "Internal field for precomputed edit previews."
2155
2219
  }
2156
2220
  },
2157
2221
  required: ["command"]
2158
2222
  },
2159
2223
  async execute(input, ctx) {
2160
- const { command, timeout: timeoutMs, description } = input;
2224
+ const {
2225
+ command,
2226
+ timeout: timeoutMs,
2227
+ run_in_background,
2228
+ description,
2229
+ dangerouslyDisableSandbox,
2230
+ _simulatedSedEdit
2231
+ } = input;
2161
2232
  if (!command || typeof command !== "string") {
2162
2233
  return { content: "Error: command is required", isError: true };
2163
2234
  }
2164
2235
  const timeout = Math.min(timeoutMs ?? DEFAULT_TIMEOUT, MAX_TIMEOUT);
2165
2236
  try {
2237
+ if (run_in_background) {
2238
+ const proc2 = Bun.spawn(["bash", "-c", command], {
2239
+ cwd: ctx.cwd,
2240
+ stdout: "ignore",
2241
+ stderr: "ignore",
2242
+ stdin: "ignore",
2243
+ env: { ...process.env, ...ctx.env }
2244
+ });
2245
+ return {
2246
+ content: `Background command started (pid ${proc2.pid ?? "unknown"}).`,
2247
+ metadata: {
2248
+ pid: proc2.pid ?? null,
2249
+ run_in_background: true,
2250
+ dangerouslyDisableSandbox: dangerouslyDisableSandbox === true,
2251
+ hasSimulatedSedEdit: !!_simulatedSedEdit
2252
+ }
2253
+ };
2254
+ }
2166
2255
  const proc = Bun.spawn(["bash", "-c", command], {
2167
2256
  cwd: ctx.cwd,
2168
2257
  stdout: "pipe",
@@ -2194,7 +2283,11 @@ var BashTool = {
2194
2283
  return {
2195
2284
  content: output,
2196
2285
  isError: exitCode !== 0 ? true : undefined,
2197
- metadata: { exitCode }
2286
+ metadata: {
2287
+ exitCode,
2288
+ dangerouslyDisableSandbox: dangerouslyDisableSandbox === true,
2289
+ hasSimulatedSedEdit: !!_simulatedSedEdit
2290
+ }
2198
2291
  };
2199
2292
  } catch (err) {
2200
2293
  const message = err instanceof Error ? err.message : String(err);
@@ -2223,12 +2316,17 @@ var ReadTool = {
2223
2316
  limit: {
2224
2317
  type: "number",
2225
2318
  description: "Number of lines to read"
2319
+ },
2320
+ pages: {
2321
+ type: "array",
2322
+ items: { type: "number" },
2323
+ description: "Optional PDF page numbers (1-based)."
2226
2324
  }
2227
2325
  },
2228
2326
  required: ["file_path"]
2229
2327
  },
2230
2328
  async execute(input, ctx) {
2231
- const { file_path, offset, limit } = input;
2329
+ const { file_path, offset, limit, pages } = input;
2232
2330
  if (!file_path) {
2233
2331
  return { content: "Error: file_path is required", isError: true };
2234
2332
  }
@@ -2239,6 +2337,12 @@ var ReadTool = {
2239
2337
  if (!exists) {
2240
2338
  return { content: `Error: File not found: ${resolvedPath}`, isError: true };
2241
2339
  }
2340
+ if (Array.isArray(pages) && pages.length > 0 && resolvedPath.toLowerCase().endsWith(".pdf")) {
2341
+ return {
2342
+ content: "Error: PDF page extraction is not implemented in this runtime.",
2343
+ isError: true
2344
+ };
2345
+ }
2242
2346
  const text = await file.text();
2243
2347
  const lines = text.split(`
2244
2348
  `);
@@ -2270,7 +2374,7 @@ function resolvePath(filePath, cwd) {
2270
2374
 
2271
2375
  // src/tools/write.ts
2272
2376
  import { mkdir } from "fs/promises";
2273
- import { dirname } from "path";
2377
+ import { dirname as dirname2 } from "path";
2274
2378
  var WriteTool = {
2275
2379
  name: "Write",
2276
2380
  description: "Writes content to a file. Creates parent directories if needed. " + "Overwrites existing files.",
@@ -2298,7 +2402,7 @@ var WriteTool = {
2298
2402
  }
2299
2403
  const resolvedPath = file_path.startsWith("/") ? file_path : `${ctx.cwd}/${file_path}`;
2300
2404
  try {
2301
- const dir = dirname(resolvedPath);
2405
+ const dir = dirname2(resolvedPath);
2302
2406
  await mkdir(dir, { recursive: true });
2303
2407
  await Bun.write(resolvedPath, content);
2304
2408
  const lines = content.split(`
@@ -2626,6 +2730,405 @@ async function collectFiles(dir, globPattern) {
2626
2730
  }
2627
2731
  return files;
2628
2732
  }
2733
+
2734
+ // src/tools/notebook-edit.ts
2735
+ import { readFile, writeFile } from "fs/promises";
2736
+ function toSourceLines(text) {
2737
+ const lines = text.split(`
2738
+ `);
2739
+ return lines.map((line, idx) => idx < lines.length - 1 ? `${line}
2740
+ ` : line);
2741
+ }
2742
+ var NotebookEditTool = {
2743
+ name: "NotebookEdit",
2744
+ description: "Edit a specific Jupyter notebook cell by id or index.",
2745
+ inputSchema: {
2746
+ type: "object",
2747
+ properties: {
2748
+ notebook_path: { type: "string", description: "Path to .ipynb file." },
2749
+ cell_id: { type: "string", description: "Cell id to edit." },
2750
+ cell_index: { type: "number", description: "Cell index to edit if id is not provided." },
2751
+ new_source: { type: "string", description: "New cell source content." }
2752
+ },
2753
+ required: ["notebook_path", "new_source"]
2754
+ },
2755
+ async execute(input, ctx) {
2756
+ const {
2757
+ notebook_path,
2758
+ cell_id,
2759
+ cell_index,
2760
+ new_source
2761
+ } = input ?? {};
2762
+ if (!notebook_path)
2763
+ return { content: "Error: notebook_path is required", isError: true };
2764
+ if (new_source === undefined)
2765
+ return { content: "Error: new_source is required", isError: true };
2766
+ const filePath = notebook_path.startsWith("/") ? notebook_path : `${ctx.cwd}/${notebook_path}`;
2767
+ try {
2768
+ const raw = await readFile(filePath, "utf-8");
2769
+ const notebook = JSON.parse(raw);
2770
+ if (!Array.isArray(notebook.cells)) {
2771
+ return { content: "Error: notebook has no cells array", isError: true };
2772
+ }
2773
+ let targetIndex = -1;
2774
+ if (cell_id) {
2775
+ targetIndex = notebook.cells.findIndex((c) => c.id === cell_id);
2776
+ } else if (typeof cell_index === "number") {
2777
+ targetIndex = cell_index;
2778
+ } else {
2779
+ targetIndex = 0;
2780
+ }
2781
+ if (targetIndex < 0 || targetIndex >= notebook.cells.length) {
2782
+ return {
2783
+ content: `Error: cell not found (id=${cell_id ?? "n/a"}, index=${String(cell_index ?? "n/a")})`,
2784
+ isError: true
2785
+ };
2786
+ }
2787
+ const cell = notebook.cells[targetIndex];
2788
+ cell.source = toSourceLines(new_source);
2789
+ await writeFile(filePath, JSON.stringify(notebook, null, 2) + `
2790
+ `, "utf-8");
2791
+ return {
2792
+ content: `Updated notebook cell ${targetIndex} in ${filePath}`
2793
+ };
2794
+ } catch (err) {
2795
+ const message = err instanceof Error ? err.message : String(err);
2796
+ return { content: `Error editing notebook: ${message}`, isError: true };
2797
+ }
2798
+ }
2799
+ };
2800
+
2801
+ // src/tools/web-fetch.ts
2802
+ var DEFAULT_TIMEOUT_MS = 20000;
2803
+ var MAX_OUTPUT = 80000;
2804
+ var WebFetchTool = {
2805
+ name: "WebFetch",
2806
+ description: "Fetches a URL and returns response text.",
2807
+ inputSchema: {
2808
+ type: "object",
2809
+ properties: {
2810
+ url: {
2811
+ type: "string",
2812
+ description: "The URL to fetch."
2813
+ },
2814
+ prompt: {
2815
+ type: "string",
2816
+ description: "Optional fetch intent/instructions."
2817
+ },
2818
+ timeout_ms: {
2819
+ type: "number",
2820
+ description: "Timeout in milliseconds (default 20000)."
2821
+ },
2822
+ max_length: {
2823
+ type: "number",
2824
+ description: "Maximum output length (default 80000)."
2825
+ }
2826
+ },
2827
+ required: ["url"]
2828
+ },
2829
+ async execute(input) {
2830
+ const { url, timeout_ms, max_length } = input ?? {};
2831
+ if (!url)
2832
+ return { content: "Error: url is required", isError: true };
2833
+ const timeout = Math.max(1000, timeout_ms ?? DEFAULT_TIMEOUT_MS);
2834
+ const outLimit = Math.max(1000, max_length ?? MAX_OUTPUT);
2835
+ const controller = new AbortController;
2836
+ const timer = setTimeout(() => controller.abort(), timeout);
2837
+ try {
2838
+ const res = await fetch(url, {
2839
+ method: "GET",
2840
+ signal: controller.signal,
2841
+ headers: {
2842
+ "user-agent": "fourmis-agent-sdk/1.0"
2843
+ }
2844
+ });
2845
+ const contentType = res.headers.get("content-type") ?? "unknown";
2846
+ let body = await res.text();
2847
+ if (body.length > outLimit) {
2848
+ body = body.slice(0, outLimit) + `
2849
+ ... (truncated)`;
2850
+ }
2851
+ return {
2852
+ content: [
2853
+ `Status: ${res.status} ${res.statusText}`,
2854
+ `Content-Type: ${contentType}`,
2855
+ "",
2856
+ body
2857
+ ].join(`
2858
+ `),
2859
+ isError: res.ok ? undefined : true
2860
+ };
2861
+ } catch (err) {
2862
+ const message = err instanceof Error ? err.message : String(err);
2863
+ return { content: `Error fetching URL: ${message}`, isError: true };
2864
+ } finally {
2865
+ clearTimeout(timer);
2866
+ }
2867
+ }
2868
+ };
2869
+
2870
+ // src/tools/web-search.ts
2871
+ var SEARCH_ENDPOINT = "https://duckduckgo.com/html/";
2872
+ function stripTags(input) {
2873
+ return input.replace(/<[^>]+>/g, "").replace(/&amp;/g, "&").replace(/&quot;/g, '"').replace(/&#39;/g, "'").replace(/&lt;/g, "<").replace(/&gt;/g, ">").trim();
2874
+ }
2875
+ var WebSearchTool = {
2876
+ name: "WebSearch",
2877
+ description: "Searches the web and returns top result links.",
2878
+ inputSchema: {
2879
+ type: "object",
2880
+ properties: {
2881
+ query: {
2882
+ type: "string",
2883
+ description: "Search query."
2884
+ },
2885
+ max_results: {
2886
+ type: "number",
2887
+ description: "Maximum results to return (default 5)."
2888
+ }
2889
+ },
2890
+ required: ["query"]
2891
+ },
2892
+ async execute(input) {
2893
+ const { query, max_results } = input ?? {};
2894
+ if (!query) {
2895
+ return { content: "Error: query is required", isError: true };
2896
+ }
2897
+ const limit = Math.max(1, Math.min(20, max_results ?? 5));
2898
+ try {
2899
+ const url = `${SEARCH_ENDPOINT}?q=${encodeURIComponent(query)}`;
2900
+ const res = await fetch(url, {
2901
+ headers: {
2902
+ "user-agent": "fourmis-agent-sdk/1.0"
2903
+ }
2904
+ });
2905
+ if (!res.ok) {
2906
+ return {
2907
+ content: `Error searching web: ${res.status} ${res.statusText}`,
2908
+ isError: true
2909
+ };
2910
+ }
2911
+ const html = await res.text();
2912
+ const matches = [...html.matchAll(/<a[^>]*class="result__a"[^>]*href="([^"]+)"[^>]*>([\s\S]*?)<\/a>/g)];
2913
+ if (matches.length === 0) {
2914
+ return { content: "No search results found." };
2915
+ }
2916
+ const lines = [];
2917
+ for (let i = 0;i < Math.min(limit, matches.length); i++) {
2918
+ const href = stripTags(matches[i][1]);
2919
+ const title = stripTags(matches[i][2]);
2920
+ lines.push(`${i + 1}. ${title}
2921
+ ${href}`);
2922
+ }
2923
+ return { content: lines.join(`
2924
+ `) };
2925
+ } catch (err) {
2926
+ const message = err instanceof Error ? err.message : String(err);
2927
+ return { content: `Error searching web: ${message}`, isError: true };
2928
+ }
2929
+ }
2930
+ };
2931
+
2932
+ // src/tools/ask-user-question.ts
2933
+ var AskUserQuestionTool = {
2934
+ name: "AskUserQuestion",
2935
+ description: "Ask the user a clarifying question and wait for their response.",
2936
+ inputSchema: {
2937
+ type: "object",
2938
+ properties: {
2939
+ question: {
2940
+ type: "string",
2941
+ description: "Question to ask the user."
2942
+ },
2943
+ options: {
2944
+ type: "array",
2945
+ items: { type: "string" },
2946
+ description: "Optional fixed choices."
2947
+ }
2948
+ },
2949
+ required: ["question"]
2950
+ },
2951
+ async execute(input) {
2952
+ const { question, options } = input ?? {};
2953
+ if (!question) {
2954
+ return { content: "Error: question is required", isError: true };
2955
+ }
2956
+ const choices = Array.isArray(options) && options.length > 0 ? ` Choices: ${options.join(" | ")}` : "";
2957
+ return {
2958
+ content: `User interaction is not available in this runtime. Unanswered question: ${question}.${choices}`,
2959
+ isError: true
2960
+ };
2961
+ }
2962
+ };
2963
+
2964
+ // src/tools/todo-write.ts
2965
+ import { mkdir as mkdir2, writeFile as writeFile2 } from "fs/promises";
2966
+ import { dirname as dirname3, join as join4 } from "path";
2967
+ var TodoWriteTool = {
2968
+ name: "TodoWrite",
2969
+ description: "Write/update task todo items for the current session.",
2970
+ inputSchema: {
2971
+ type: "object",
2972
+ properties: {
2973
+ todos: {
2974
+ type: "array",
2975
+ items: {
2976
+ type: "object",
2977
+ properties: {
2978
+ content: { type: "string" },
2979
+ status: { type: "string", enum: ["pending", "in_progress", "completed"] },
2980
+ activeForm: { type: "string" }
2981
+ },
2982
+ required: ["content", "status"]
2983
+ }
2984
+ }
2985
+ },
2986
+ required: ["todos"]
2987
+ },
2988
+ async execute(input, ctx) {
2989
+ const { todos } = input ?? {};
2990
+ if (!Array.isArray(todos)) {
2991
+ return { content: "Error: todos must be an array", isError: true };
2992
+ }
2993
+ for (const todo of todos) {
2994
+ if (!todo?.content || !todo?.status) {
2995
+ return { content: "Error: each todo requires content and status", isError: true };
2996
+ }
2997
+ }
2998
+ const filePath = join4(ctx.cwd, ".claude", "todos.json");
2999
+ try {
3000
+ await mkdir2(dirname3(filePath), { recursive: true });
3001
+ const payload = {
3002
+ updatedAt: new Date().toISOString(),
3003
+ todos
3004
+ };
3005
+ await writeFile2(filePath, JSON.stringify(payload, null, 2) + `
3006
+ `, "utf-8");
3007
+ return {
3008
+ content: `Saved ${todos.length} todo item(s) to ${filePath}`
3009
+ };
3010
+ } catch (err) {
3011
+ const message = err instanceof Error ? err.message : String(err);
3012
+ return { content: `Error writing todos: ${message}`, isError: true };
3013
+ }
3014
+ }
3015
+ };
3016
+
3017
+ // src/tools/config.ts
3018
+ import { mkdir as mkdir3, readFile as readFile2, writeFile as writeFile3 } from "fs/promises";
3019
+ import { join as join5, dirname as dirname4 } from "path";
3020
+ function scopePath(cwd, scope) {
3021
+ if (scope === "project")
3022
+ return join5(cwd, ".claude", "settings.json");
3023
+ return join5(cwd, ".claude", "settings.local.json");
3024
+ }
3025
+ function setByPath(obj, keyPath, value) {
3026
+ const keys = keyPath.split(".").filter(Boolean);
3027
+ if (keys.length === 0)
3028
+ return;
3029
+ let current = obj;
3030
+ for (let i = 0;i < keys.length - 1; i++) {
3031
+ const key = keys[i];
3032
+ const next = current[key];
3033
+ if (!next || typeof next !== "object" || Array.isArray(next)) {
3034
+ current[key] = {};
3035
+ }
3036
+ current = current[key];
3037
+ }
3038
+ current[keys[keys.length - 1]] = value;
3039
+ }
3040
+ function getByPath(obj, keyPath) {
3041
+ const keys = keyPath.split(".").filter(Boolean);
3042
+ let current = obj;
3043
+ for (const key of keys) {
3044
+ if (!current || typeof current !== "object" || Array.isArray(current))
3045
+ return;
3046
+ current = current[key];
3047
+ }
3048
+ return current;
3049
+ }
3050
+ var ConfigTool = {
3051
+ name: "Config",
3052
+ description: "Read or update .claude settings values.",
3053
+ inputSchema: {
3054
+ type: "object",
3055
+ properties: {
3056
+ action: {
3057
+ type: "string",
3058
+ enum: ["get", "set", "list"]
3059
+ },
3060
+ key: {
3061
+ type: "string",
3062
+ description: "Dot-path key (for get/set)."
3063
+ },
3064
+ value: {
3065
+ description: "Value for set action."
3066
+ },
3067
+ scope: {
3068
+ type: "string",
3069
+ enum: ["local", "project"]
3070
+ }
3071
+ },
3072
+ required: ["action"]
3073
+ },
3074
+ async execute(input, ctx) {
3075
+ const {
3076
+ action,
3077
+ key,
3078
+ value,
3079
+ scope = "local"
3080
+ } = input ?? {};
3081
+ if (!action) {
3082
+ return { content: "Error: action is required", isError: true };
3083
+ }
3084
+ const filePath = scopePath(ctx.cwd, scope);
3085
+ let data = {};
3086
+ try {
3087
+ const raw = await readFile2(filePath, "utf-8");
3088
+ data = JSON.parse(raw);
3089
+ } catch {
3090
+ data = {};
3091
+ }
3092
+ if (action === "list") {
3093
+ return { content: JSON.stringify(data, null, 2) };
3094
+ }
3095
+ if (!key) {
3096
+ return { content: "Error: key is required for get/set", isError: true };
3097
+ }
3098
+ if (action === "get") {
3099
+ const out = getByPath(data, key);
3100
+ return { content: out === undefined ? "undefined" : JSON.stringify(out, null, 2) };
3101
+ }
3102
+ setByPath(data, key, value);
3103
+ try {
3104
+ await mkdir3(dirname4(filePath), { recursive: true });
3105
+ await writeFile3(filePath, JSON.stringify(data, null, 2) + `
3106
+ `, "utf-8");
3107
+ return { content: `Updated ${key} in ${filePath}` };
3108
+ } catch (err) {
3109
+ const message = err instanceof Error ? err.message : String(err);
3110
+ return { content: `Error writing config: ${message}`, isError: true };
3111
+ }
3112
+ }
3113
+ };
3114
+
3115
+ // src/tools/exit-plan-mode.ts
3116
+ var ExitPlanModeTool = {
3117
+ name: "ExitPlanMode",
3118
+ description: "Exit plan mode and resume normal execution permissions.",
3119
+ inputSchema: {
3120
+ type: "object",
3121
+ properties: {}
3122
+ },
3123
+ async execute() {
3124
+ return {
3125
+ content: "Exiting plan mode.",
3126
+ metadata: {
3127
+ setPermissionMode: "default"
3128
+ }
3129
+ };
3130
+ }
3131
+ };
2629
3132
  // src/tools/index.ts
2630
3133
  var ALL_TOOLS = {
2631
3134
  Bash: BashTool,
@@ -2633,7 +3136,14 @@ var ALL_TOOLS = {
2633
3136
  Write: WriteTool,
2634
3137
  Edit: EditTool,
2635
3138
  Glob: GlobTool,
2636
- Grep: GrepTool
3139
+ Grep: GrepTool,
3140
+ NotebookEdit: NotebookEditTool,
3141
+ WebFetch: WebFetchTool,
3142
+ WebSearch: WebSearchTool,
3143
+ AskUserQuestion: AskUserQuestionTool,
3144
+ TodoWrite: TodoWriteTool,
3145
+ Config: ConfigTool,
3146
+ ExitPlanMode: ExitPlanModeTool
2637
3147
  };
2638
3148
  function buildToolRegistry(toolNames, allowedTools, disallowedTools) {
2639
3149
  const registry = new ToolRegistry;
@@ -2648,277 +3158,299 @@ function buildToolRegistry(toolNames, allowedTools, disallowedTools) {
2648
3158
  return registry;
2649
3159
  }
2650
3160
 
2651
- // src/permissions.ts
2652
- var SAFE_TOOLS = new Set(["Read", "Glob", "Grep"]);
2653
- var EDIT_TOOLS = new Set(["Write", "Edit"]);
2654
- var FS_COMMANDS = ["mkdir", "touch", "rm", "mv", "cp"];
2655
- var DELEGATE_TOOLS = new Set(["Teammate", "Task", "TaskOutput", "TaskStop"]);
2656
- function normalizeRules(rules) {
2657
- if (!rules)
2658
- return [];
2659
- return rules.map((r) => typeof r === "string" ? { toolName: r } : r);
3161
+ // src/skills/frontmatter.ts
3162
+ import { parse } from "yaml";
3163
+ function normalizeNewlines(value) {
3164
+ return value.replace(/\r\n/g, `
3165
+ `).replace(/\r/g, `
3166
+ `);
2660
3167
  }
2661
- function matchesRule(rules, toolName, input) {
2662
- for (const rule of rules) {
2663
- if (rule.toolName !== toolName)
2664
- continue;
2665
- if (!rule.ruleContent)
2666
- return true;
2667
- const inputStr = toolName === "Bash" ? String(input?.command ?? "") : JSON.stringify(input ?? {});
2668
- if (inputStr.includes(rule.ruleContent))
2669
- return true;
3168
+ function extractFrontmatter(content) {
3169
+ const normalized = normalizeNewlines(content);
3170
+ if (!normalized.startsWith("---")) {
3171
+ return { yamlString: null, body: normalized };
2670
3172
  }
2671
- return false;
3173
+ const endIndex = normalized.indexOf(`
3174
+ ---`, 3);
3175
+ if (endIndex === -1) {
3176
+ return { yamlString: null, body: normalized };
3177
+ }
3178
+ return {
3179
+ yamlString: normalized.slice(4, endIndex),
3180
+ body: normalized.slice(endIndex + 4).trim()
3181
+ };
3182
+ }
3183
+ function parseFrontmatter(content) {
3184
+ const { yamlString, body } = extractFrontmatter(content);
3185
+ if (!yamlString) {
3186
+ return { frontmatter: {}, body };
3187
+ }
3188
+ const parsed = parse(yamlString);
3189
+ return { frontmatter: parsed ?? {}, body };
3190
+ }
3191
+ function stripFrontmatter(content) {
3192
+ return parseFrontmatter(content).body;
2672
3193
  }
2673
3194
 
2674
- class PermissionManager {
2675
- mode;
2676
- canUseTool;
2677
- allowRules;
2678
- denyRules;
2679
- settingsManager;
2680
- constructor(mode = "default", canUseTool, permissions, settingsManager) {
2681
- this.mode = mode;
2682
- this.canUseTool = canUseTool;
2683
- this.allowRules = normalizeRules(permissions?.allow);
2684
- this.denyRules = normalizeRules(permissions?.deny);
2685
- this.settingsManager = settingsManager;
3195
+ // src/skills/skills.ts
3196
+ import { existsSync as existsSync4, readdirSync, readFileSync as readFileSync4, realpathSync, statSync } from "fs";
3197
+ import { homedir as homedir4 } from "os";
3198
+ import { basename, dirname as dirname5, isAbsolute, join as join6, resolve } from "path";
3199
+ var MAX_NAME_LENGTH = 64;
3200
+ var MAX_DESCRIPTION_LENGTH = 1024;
3201
+ var MAX_COMPATIBILITY_LENGTH = 500;
3202
+ var CONFIG_DIR_NAME = ".claude";
3203
+ function shouldIgnore(name) {
3204
+ return name.startsWith(".") || name === "node_modules";
3205
+ }
3206
+ function validateName(name, parentDirName) {
3207
+ const errors = [];
3208
+ if (name !== parentDirName) {
3209
+ errors.push(`name "${name}" does not match parent directory "${parentDirName}"`);
2686
3210
  }
2687
- async check(toolName, input, options) {
2688
- if (this.mode === "bypassPermissions") {
2689
- return { behavior: "allow" };
2690
- }
2691
- if (matchesRule(this.denyRules, toolName, input)) {
2692
- return {
2693
- behavior: "deny",
2694
- message: `Tool "${toolName}" is denied by permissions config.`
2695
- };
2696
- }
2697
- if (this.mode === "plan") {
2698
- if (!SAFE_TOOLS.has(toolName)) {
2699
- return {
2700
- behavior: "deny",
2701
- message: `Tool "${toolName}" is not allowed in plan mode. Only read-only tools are available.`
2702
- };
2703
- }
2704
- return { behavior: "allow" };
2705
- }
2706
- if (this.mode === "delegate") {
2707
- if (!DELEGATE_TOOLS.has(toolName) && !SAFE_TOOLS.has(toolName)) {
2708
- return {
2709
- behavior: "deny",
2710
- message: `Tool "${toolName}" is not allowed in delegate mode. Only Teammate, Task, and read-only tools are available.`
2711
- };
2712
- }
2713
- return { behavior: "allow" };
2714
- }
2715
- if (matchesRule(this.allowRules, toolName, input)) {
2716
- return { behavior: "allow" };
2717
- }
2718
- if (SAFE_TOOLS.has(toolName)) {
2719
- return { behavior: "allow" };
2720
- }
2721
- if (this.mode === "acceptEdits") {
2722
- if (EDIT_TOOLS.has(toolName)) {
2723
- return { behavior: "allow" };
2724
- }
2725
- if (toolName === "Bash") {
2726
- const cmd = String(input?.command ?? "").trimStart();
2727
- if (FS_COMMANDS.some((fc) => cmd.startsWith(fc + " ") || cmd === fc)) {
2728
- return { behavior: "allow" };
2729
- }
2730
- }
2731
- }
2732
- if (this.canUseTool) {
2733
- const result = await this.canUseTool(toolName, input, options);
2734
- if (result.behavior === "allow" && result.updatedPermissions) {
2735
- this.applyPermissionUpdates(result.updatedPermissions);
2736
- }
2737
- return result;
2738
- }
2739
- if (this.mode === "dontAsk") {
2740
- return {
2741
- behavior: "deny",
2742
- message: `Tool "${toolName}" requires approval. In dontAsk mode, tools must be pre-approved via permissions config.`
2743
- };
2744
- }
2745
- return { behavior: "allow" };
3211
+ if (name.length > MAX_NAME_LENGTH) {
3212
+ errors.push(`name exceeds ${MAX_NAME_LENGTH} characters (${name.length})`);
2746
3213
  }
2747
- applyPermissionUpdates(updates) {
2748
- for (const update of updates) {
2749
- if (this.settingsManager && update.destination !== "session" && update.destination !== "cliArg") {
2750
- this.settingsManager.persistUpdate(update);
2751
- }
2752
- switch (update.type) {
2753
- case "addRules":
2754
- for (const rule of update.rules) {
2755
- if (update.behavior === "allow") {
2756
- this.allowRules.push(rule);
2757
- } else if (update.behavior === "deny") {
2758
- this.denyRules.push(rule);
2759
- }
2760
- }
2761
- break;
2762
- case "removeRules":
2763
- for (const rule of update.rules) {
2764
- if (update.behavior === "allow") {
2765
- this.allowRules = this.allowRules.filter((r) => !(r.toolName === rule.toolName && r.ruleContent === rule.ruleContent));
2766
- } else if (update.behavior === "deny") {
2767
- this.denyRules = this.denyRules.filter((r) => !(r.toolName === rule.toolName && r.ruleContent === rule.ruleContent));
2768
- }
2769
- }
2770
- break;
2771
- case "replaceRules":
2772
- if (update.behavior === "allow") {
2773
- this.allowRules = [...update.rules];
2774
- } else if (update.behavior === "deny") {
2775
- this.denyRules = [...update.rules];
2776
- }
2777
- break;
2778
- case "setMode":
2779
- this.mode = update.mode;
2780
- break;
2781
- }
2782
- }
3214
+ if (!/^[a-z0-9-]+$/.test(name)) {
3215
+ errors.push(`name contains invalid characters (must be lowercase a-z, 0-9, hyphens only)`);
2783
3216
  }
2784
- setMode(mode) {
2785
- this.mode = mode;
3217
+ if (name.startsWith("-") || name.endsWith("-")) {
3218
+ errors.push(`name must not start or end with a hyphen`);
2786
3219
  }
2787
- getMode() {
2788
- return this.mode;
3220
+ if (name.includes("--")) {
3221
+ errors.push(`name must not contain consecutive hyphens`);
2789
3222
  }
3223
+ return errors;
2790
3224
  }
2791
-
2792
- // src/settings.ts
2793
- import { existsSync as existsSync3, mkdirSync as mkdirSync2, readFileSync as readFileSync3, writeFileSync as writeFileSync3 } from "fs";
2794
- import { dirname as dirname2, join as join3 } from "path";
2795
- import { homedir as homedir3 } from "os";
2796
-
2797
- class SettingsManager {
2798
- cwd;
2799
- constructor(cwd) {
2800
- this.cwd = cwd;
3225
+ function validateDescription(description) {
3226
+ const errors = [];
3227
+ if (!description || description.trim() === "") {
3228
+ errors.push("description is required");
3229
+ } else if (description.length > MAX_DESCRIPTION_LENGTH) {
3230
+ errors.push(`description exceeds ${MAX_DESCRIPTION_LENGTH} characters (${description.length})`);
3231
+ }
3232
+ return errors;
3233
+ }
3234
+ function validateCompatibility(compatibility) {
3235
+ if (!compatibility)
3236
+ return [];
3237
+ if (compatibility.length > MAX_COMPATIBILITY_LENGTH) {
3238
+ return [`compatibility exceeds ${MAX_COMPATIBILITY_LENGTH} characters (${compatibility.length})`];
2801
3239
  }
2802
- loadPermissions(sources) {
2803
- const allAllow = [];
2804
- const allDeny = [];
2805
- for (const source of sources) {
2806
- const path = this.sourceToPath(source);
2807
- const data = this.readJson(path);
2808
- if (!data?.permissions)
3240
+ return [];
3241
+ }
3242
+ function loadSkillsFromDir(options) {
3243
+ return loadSkillsFromDirInternal(options.dir, options.source, true);
3244
+ }
3245
+ function loadSkillsFromDirInternal(dir, source, includeRootFiles) {
3246
+ const skills = [];
3247
+ const diagnostics = [];
3248
+ if (!existsSync4(dir)) {
3249
+ return { skills, diagnostics };
3250
+ }
3251
+ try {
3252
+ const entries = readdirSync(dir, { withFileTypes: true });
3253
+ for (const entry of entries) {
3254
+ if (shouldIgnore(entry.name)) {
2809
3255
  continue;
2810
- const perms = data.permissions;
2811
- if (Array.isArray(perms.allow)) {
2812
- for (const rule of perms.allow) {
2813
- if (typeof rule === "string") {
2814
- allAllow.push(this.parseRule(rule));
2815
- }
2816
- }
2817
3256
  }
2818
- if (Array.isArray(perms.deny)) {
2819
- for (const rule of perms.deny) {
2820
- if (typeof rule === "string") {
2821
- allDeny.push(this.parseRule(rule));
2822
- }
3257
+ const fullPath = join6(dir, entry.name);
3258
+ let isDirectory = entry.isDirectory();
3259
+ let isFile = entry.isFile();
3260
+ if (entry.isSymbolicLink()) {
3261
+ try {
3262
+ const stats = statSync(fullPath);
3263
+ isDirectory = stats.isDirectory();
3264
+ isFile = stats.isFile();
3265
+ } catch {
3266
+ continue;
2823
3267
  }
2824
3268
  }
2825
- }
2826
- const result = {};
2827
- if (allAllow.length > 0)
2828
- result.allow = allAllow;
2829
- if (allDeny.length > 0)
2830
- result.deny = allDeny;
2831
- return result;
2832
- }
2833
- persistUpdate(update) {
2834
- const path = this.destinationToPath(update.destination);
2835
- if (!path)
2836
- return;
2837
- const data = this.readJson(path) ?? {};
2838
- if (!data.permissions)
2839
- data.permissions = {};
2840
- const perms = data.permissions;
2841
- switch (update.type) {
2842
- case "addRules": {
2843
- const key = update.behavior;
2844
- const existing = Array.isArray(perms[key]) ? perms[key] : [];
2845
- const newRules = update.rules.map((r) => this.serializeRule(r));
2846
- const set = new Set(existing);
2847
- for (const rule of newRules)
2848
- set.add(rule);
2849
- perms[key] = [...set];
2850
- break;
2851
- }
2852
- case "removeRules": {
2853
- const key = update.behavior;
2854
- if (!Array.isArray(perms[key]))
2855
- break;
2856
- const toRemove = new Set(update.rules.map((r) => this.serializeRule(r)));
2857
- perms[key] = perms[key].filter((r) => !toRemove.has(r));
2858
- break;
2859
- }
2860
- case "replaceRules": {
2861
- const key = update.behavior;
2862
- perms[key] = update.rules.map((r) => this.serializeRule(r));
2863
- break;
3269
+ if (isDirectory) {
3270
+ const subResult = loadSkillsFromDirInternal(fullPath, source, false);
3271
+ skills.push(...subResult.skills);
3272
+ diagnostics.push(...subResult.diagnostics);
3273
+ continue;
2864
3274
  }
2865
- case "setMode": {
2866
- perms.defaultMode = update.mode;
2867
- break;
3275
+ if (!isFile)
3276
+ continue;
3277
+ const isRootMd = includeRootFiles && entry.name.endsWith(".md");
3278
+ const isSkillMd = !includeRootFiles && entry.name === "SKILL.md";
3279
+ if (!isRootMd && !isSkillMd)
3280
+ continue;
3281
+ const result = loadSkillFromFile(fullPath, source);
3282
+ if (result.skill) {
3283
+ skills.push(result.skill);
2868
3284
  }
2869
- default:
2870
- return;
2871
- }
2872
- const dir = dirname2(path);
2873
- if (!existsSync3(dir))
2874
- mkdirSync2(dir, { recursive: true });
2875
- writeFileSync3(path, JSON.stringify(data, null, 2) + `
2876
- `);
2877
- }
2878
- sourceToPath(source) {
2879
- switch (source) {
2880
- case "user":
2881
- return join3(homedir3(), ".claude", "settings.json");
2882
- case "project":
2883
- return join3(this.cwd, ".claude", "settings.json");
2884
- case "local":
2885
- return join3(this.cwd, ".claude", "settings.local.json");
2886
- }
2887
- }
2888
- destinationToPath(destination) {
2889
- switch (destination) {
2890
- case "userSettings":
2891
- return join3(homedir3(), ".claude", "settings.json");
2892
- case "projectSettings":
2893
- return join3(this.cwd, ".claude", "settings.json");
2894
- case "localSettings":
2895
- return join3(this.cwd, ".claude", "settings.local.json");
2896
- default:
2897
- return null;
3285
+ diagnostics.push(...result.diagnostics);
2898
3286
  }
3287
+ } catch {}
3288
+ return { skills, diagnostics };
3289
+ }
3290
+ function loadSkillFromFile(filePath, source) {
3291
+ const diagnostics = [];
3292
+ try {
3293
+ const rawContent = readFileSync4(filePath, "utf-8");
3294
+ const { frontmatter } = parseFrontmatter(rawContent);
3295
+ const skillDir = dirname5(filePath);
3296
+ const parentDirName = basename(skillDir);
3297
+ const descErrors = validateDescription(frontmatter.description);
3298
+ for (const error of descErrors) {
3299
+ diagnostics.push({ type: "warning", message: error, path: filePath });
3300
+ }
3301
+ const name = frontmatter.name || parentDirName;
3302
+ const nameErrors = validateName(name, parentDirName);
3303
+ for (const error of nameErrors) {
3304
+ diagnostics.push({ type: "warning", message: error, path: filePath });
3305
+ }
3306
+ const compatErrors = validateCompatibility(frontmatter.compatibility);
3307
+ for (const error of compatErrors) {
3308
+ diagnostics.push({ type: "warning", message: error, path: filePath });
3309
+ }
3310
+ if (!frontmatter.description || frontmatter.description.trim() === "") {
3311
+ return { skill: null, diagnostics };
3312
+ }
3313
+ const allowedToolsRaw = frontmatter["allowed-tools"];
3314
+ const allowedTools = allowedToolsRaw ? allowedToolsRaw.split(/\s+/).filter(Boolean) : undefined;
3315
+ return {
3316
+ skill: {
3317
+ name,
3318
+ description: frontmatter.description,
3319
+ filePath,
3320
+ baseDir: skillDir,
3321
+ source,
3322
+ disableModelInvocation: frontmatter["disable-model-invocation"] === true,
3323
+ license: frontmatter.license,
3324
+ compatibility: frontmatter.compatibility,
3325
+ metadata: frontmatter.metadata,
3326
+ allowedTools
3327
+ },
3328
+ diagnostics
3329
+ };
3330
+ } catch (error) {
3331
+ const message = error instanceof Error ? error.message : "failed to parse skill file";
3332
+ diagnostics.push({ type: "warning", message, path: filePath });
3333
+ return { skill: null, diagnostics };
2899
3334
  }
2900
- readJson(path) {
2901
- try {
2902
- const content = readFileSync3(path, "utf-8");
2903
- return JSON.parse(content);
2904
- } catch {
2905
- return null;
3335
+ }
3336
+ function normalizePath(input) {
3337
+ const trimmed = input.trim();
3338
+ if (trimmed === "~")
3339
+ return homedir4();
3340
+ if (trimmed.startsWith("~/"))
3341
+ return join6(homedir4(), trimmed.slice(2));
3342
+ if (trimmed.startsWith("~"))
3343
+ return join6(homedir4(), trimmed.slice(1));
3344
+ return trimmed;
3345
+ }
3346
+ function resolveSkillPath(p, cwd) {
3347
+ const normalized = normalizePath(p);
3348
+ return isAbsolute(normalized) ? normalized : resolve(cwd, normalized);
3349
+ }
3350
+ function loadSkills(options = {}) {
3351
+ const { cwd = process.cwd(), skillPaths = [], includeDefaults = true } = options;
3352
+ const skillMap = new Map;
3353
+ const realPathSet = new Set;
3354
+ const allDiagnostics = [];
3355
+ const collisionDiagnostics = [];
3356
+ function addSkills(result) {
3357
+ allDiagnostics.push(...result.diagnostics);
3358
+ for (const skill of result.skills) {
3359
+ let realPath;
3360
+ try {
3361
+ realPath = realpathSync(skill.filePath);
3362
+ } catch {
3363
+ realPath = skill.filePath;
3364
+ }
3365
+ if (realPathSet.has(realPath))
3366
+ continue;
3367
+ const existing = skillMap.get(skill.name);
3368
+ if (existing) {
3369
+ collisionDiagnostics.push({
3370
+ type: "collision",
3371
+ message: `name "${skill.name}" collision`,
3372
+ path: skill.filePath,
3373
+ collision: {
3374
+ resourceType: "skill",
3375
+ name: skill.name,
3376
+ winnerPath: existing.filePath,
3377
+ loserPath: skill.filePath
3378
+ }
3379
+ });
3380
+ } else {
3381
+ skillMap.set(skill.name, skill);
3382
+ realPathSet.add(realPath);
3383
+ }
2906
3384
  }
2907
3385
  }
2908
- parseRule(s) {
2909
- const match = s.match(/^([^(]+)\((.+)\)$/);
2910
- if (match)
2911
- return { toolName: match[1], ruleContent: match[2] };
2912
- return { toolName: s };
3386
+ if (includeDefaults) {
3387
+ const userSkillsDir = join6(homedir4(), CONFIG_DIR_NAME, "skills");
3388
+ const projectSkillsDir = resolve(cwd, CONFIG_DIR_NAME, "skills");
3389
+ addSkills(loadSkillsFromDirInternal(userSkillsDir, "user", true));
3390
+ addSkills(loadSkillsFromDirInternal(projectSkillsDir, "project", true));
2913
3391
  }
2914
- serializeRule(rule) {
2915
- return rule.ruleContent ? `${rule.toolName}(${rule.ruleContent})` : rule.toolName;
3392
+ for (const rawPath of skillPaths) {
3393
+ const resolvedPath = resolveSkillPath(rawPath, cwd);
3394
+ if (!existsSync4(resolvedPath)) {
3395
+ allDiagnostics.push({ type: "warning", message: "skill path does not exist", path: resolvedPath });
3396
+ continue;
3397
+ }
3398
+ try {
3399
+ const stats = statSync(resolvedPath);
3400
+ if (stats.isDirectory()) {
3401
+ addSkills(loadSkillsFromDirInternal(resolvedPath, "path", true));
3402
+ } else if (stats.isFile() && resolvedPath.endsWith(".md")) {
3403
+ const result = loadSkillFromFile(resolvedPath, "path");
3404
+ if (result.skill) {
3405
+ addSkills({ skills: [result.skill], diagnostics: result.diagnostics });
3406
+ } else {
3407
+ allDiagnostics.push(...result.diagnostics);
3408
+ }
3409
+ } else {
3410
+ allDiagnostics.push({ type: "warning", message: "skill path is not a markdown file", path: resolvedPath });
3411
+ }
3412
+ } catch (error) {
3413
+ const message = error instanceof Error ? error.message : "failed to read skill path";
3414
+ allDiagnostics.push({ type: "warning", message, path: resolvedPath });
3415
+ }
2916
3416
  }
3417
+ return {
3418
+ skills: Array.from(skillMap.values()),
3419
+ diagnostics: [...allDiagnostics, ...collisionDiagnostics]
3420
+ };
3421
+ }
3422
+ function escapeXml(str) {
3423
+ return str.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&apos;");
3424
+ }
3425
+ function formatSkillsForPrompt(skills) {
3426
+ const visibleSkills = skills.filter((s) => !s.disableModelInvocation);
3427
+ if (visibleSkills.length === 0) {
3428
+ return "";
3429
+ }
3430
+ const lines = [
3431
+ "The following skills provide specialized instructions for specific tasks.",
3432
+ "Use the read tool to load a skill's file when the task matches its description.",
3433
+ "When a skill file references a relative path, resolve it against the skill directory (parent of SKILL.md / dirname of the path) and use that absolute path in tool commands.",
3434
+ "",
3435
+ "<available_skills>"
3436
+ ];
3437
+ for (const skill of visibleSkills) {
3438
+ lines.push(" <skill>");
3439
+ lines.push(` <name>${escapeXml(skill.name)}</name>`);
3440
+ lines.push(` <description>${escapeXml(skill.description)}</description>`);
3441
+ lines.push(` <location>${escapeXml(skill.filePath)}</location>`);
3442
+ if (skill.allowedTools?.length) {
3443
+ lines.push(` <allowed-tools>${escapeXml(skill.allowedTools.join(" "))}</allowed-tools>`);
3444
+ }
3445
+ lines.push(" </skill>");
3446
+ }
3447
+ lines.push("</available_skills>");
3448
+ return lines.join(`
3449
+ `);
2917
3450
  }
2918
-
2919
3451
  // src/utils/system-prompt.ts
2920
- import { readFileSync as readFileSync4 } from "fs";
2921
- import { join as join4 } from "path";
3452
+ import { readFileSync as readFileSync5 } from "fs";
3453
+ import { join as join7 } from "path";
2922
3454
  var CORE_IDENTITY = `You are an AI coding agent. You help users with software engineering tasks by reading, writing, and modifying code. You have access to tools that let you interact with the filesystem and execute commands.
2923
3455
 
2924
3456
  You are highly capable and can help users complete complex tasks that would otherwise be too difficult or time-consuming.`;
@@ -2976,39 +3508,552 @@ function buildSystemPrompt(context) {
2976
3508
  if (guidelines) {
2977
3509
  sections.push(guidelines);
2978
3510
  }
2979
- }
2980
- sections.push(CODING_GUIDELINES);
2981
- if (context.cwd) {
2982
- sections.push(`# Environment
2983
-
2984
- Working directory: ${context.cwd}`);
2985
- const instructions = readProjectInstructions(context.cwd);
2986
- if (instructions) {
2987
- sections.push(`# Project Instructions
2988
-
2989
- ${instructions}`);
3511
+ }
3512
+ sections.push(CODING_GUIDELINES);
3513
+ if (context.cwd) {
3514
+ sections.push(`# Environment
3515
+
3516
+ Working directory: ${context.cwd}`);
3517
+ if (context.additionalDirectories && context.additionalDirectories.length > 0) {
3518
+ sections.push(`Additional allowed directories:
3519
+ ${context.additionalDirectories.map((d) => `- ${d}`).join(`
3520
+ `)}`);
3521
+ }
3522
+ if (context.loadProjectInstructions) {
3523
+ const instructions = readProjectInstructions(context.cwd);
3524
+ if (instructions) {
3525
+ sections.push(`# Project Instructions
3526
+
3527
+ ${instructions}`);
3528
+ }
3529
+ }
3530
+ }
3531
+ if (context.skills && context.skills.length > 0) {
3532
+ const skillsPrompt = formatSkillsForPrompt(context.skills);
3533
+ if (skillsPrompt) {
3534
+ sections.push(`# Skills
3535
+
3536
+ ${skillsPrompt}`);
3537
+ }
3538
+ }
3539
+ if (context.customPrompt) {
3540
+ sections.push(context.customPrompt);
3541
+ }
3542
+ return sections.join(`
3543
+
3544
+ `);
3545
+ }
3546
+ function readProjectInstructions(cwd) {
3547
+ for (const name of ["CLAUDE.md", "AGENTS.md"]) {
3548
+ try {
3549
+ const content = readFileSync5(join7(cwd, name), "utf-8").trim();
3550
+ if (content)
3551
+ return content;
3552
+ } catch {}
3553
+ }
3554
+ return null;
3555
+ }
3556
+
3557
+ // src/agent-loop.ts
3558
+ function makeModelUsageEntry() {
3559
+ return {
3560
+ inputTokens: 0,
3561
+ outputTokens: 0,
3562
+ cacheReadInputTokens: 0,
3563
+ cacheCreationInputTokens: 0,
3564
+ totalCostUsd: 0
3565
+ };
3566
+ }
3567
+ function makeErrorResult(params) {
3568
+ return {
3569
+ type: "result",
3570
+ subtype: params.subtype,
3571
+ duration_ms: Date.now() - params.startTime,
3572
+ duration_api_ms: params.apiTimeMs,
3573
+ is_error: true,
3574
+ num_turns: params.turns,
3575
+ stop_reason: null,
3576
+ total_cost_usd: params.costUsd,
3577
+ usage: params.usage,
3578
+ modelUsage: params.modelUsage,
3579
+ permission_denials: params.permissionDenials,
3580
+ errors: params.errors,
3581
+ uuid: uuid(),
3582
+ session_id: params.sessionId
3583
+ };
3584
+ }
3585
+ function extractStructuredJson(text) {
3586
+ const trimmed = text.trim();
3587
+ if (!trimmed) {
3588
+ return { ok: false, error: "Empty result text; expected JSON output." };
3589
+ }
3590
+ const fenced = trimmed.match(/```(?:json)?\s*([\s\S]*?)\s*```/i);
3591
+ const candidate = fenced ? fenced[1].trim() : trimmed;
3592
+ try {
3593
+ return { ok: true, value: JSON.parse(candidate) };
3594
+ } catch (err) {
3595
+ const message = err instanceof Error ? err.message : String(err);
3596
+ return { ok: false, error: `Invalid JSON output: ${message}` };
3597
+ }
3598
+ }
3599
+ async function* agentLoop(prompt, options) {
3600
+ const {
3601
+ provider,
3602
+ model,
3603
+ fallbackModel,
3604
+ modelState,
3605
+ maxThinkingTokensState,
3606
+ thinking,
3607
+ effort,
3608
+ outputFormat,
3609
+ systemPrompt,
3610
+ tools,
3611
+ permissions,
3612
+ cwd,
3613
+ sessionId,
3614
+ maxTurns,
3615
+ maxBudgetUsd,
3616
+ includePartialMessages,
3617
+ signal,
3618
+ env,
3619
+ debug,
3620
+ hooks,
3621
+ mcpClient,
3622
+ previousMessages,
3623
+ sessionLogger,
3624
+ nativeMemoryTool,
3625
+ initMeta
3626
+ } = options;
3627
+ const effectiveModelState = modelState ?? { current: model };
3628
+ const startTime = Date.now();
3629
+ let apiTimeMs = 0;
3630
+ let turns = 0;
3631
+ let totalUsage = emptyTokenUsage();
3632
+ let costUsd = 0;
3633
+ const modelUsage = {};
3634
+ const permissionDenials = [];
3635
+ if (mcpClient) {
3636
+ await mcpClient.connectAll();
3637
+ for (const tool of mcpClient.getTools()) {
3638
+ tools.register(tool);
3639
+ }
3640
+ const { createListMcpResourcesTool: createListMcpResourcesTool2, createReadMcpResourceTool: createReadMcpResourceTool2 } = await Promise.resolve().then(() => exports_mcp_resources);
3641
+ tools.register(createListMcpResourcesTool2(mcpClient));
3642
+ tools.register(createReadMcpResourceTool2(mcpClient));
3643
+ }
3644
+ const messages = [
3645
+ ...previousMessages ?? [],
3646
+ { role: "user", content: prompt }
3647
+ ];
3648
+ if (sessionLogger) {
3649
+ sessionLogger("user", prompt, null);
3650
+ }
3651
+ yield {
3652
+ type: "system",
3653
+ subtype: "init",
3654
+ apiKeySource: "user",
3655
+ claude_code_version: "fourmis-agent-sdk",
3656
+ session_id: sessionId,
3657
+ model: effectiveModelState.current,
3658
+ tools: tools.list(),
3659
+ cwd,
3660
+ mcp_servers: (mcpClient?.status() ?? []).map((s) => ({ name: s.name, status: s.status })),
3661
+ permissionMode: permissions.getMode(),
3662
+ agents: initMeta?.agents,
3663
+ betas: initMeta?.betas,
3664
+ slash_commands: initMeta?.slashCommands ?? [],
3665
+ output_style: initMeta?.outputStyle ?? "default",
3666
+ skills: initMeta?.skills ?? [],
3667
+ plugins: (initMeta?.plugins ?? []).map((p) => ({ name: p.path.split("/").pop() ?? p.path, path: p.path })),
3668
+ uuid: uuid()
3669
+ };
3670
+ if (hooks) {
3671
+ await hooks.fire("Setup", {
3672
+ event: "Setup",
3673
+ hook_event_name: "Setup",
3674
+ trigger: "init",
3675
+ session_id: sessionId,
3676
+ cwd,
3677
+ permission_mode: permissions.getMode()
3678
+ }, undefined, { signal });
3679
+ }
3680
+ if (hooks) {
3681
+ await hooks.fire("SessionStart", {
3682
+ event: "SessionStart",
3683
+ hook_event_name: "SessionStart",
3684
+ session_id: sessionId,
3685
+ source: "startup",
3686
+ model: effectiveModelState.current,
3687
+ cwd,
3688
+ permission_mode: permissions.getMode()
3689
+ }, undefined, { signal });
3690
+ }
3691
+ while (true) {
3692
+ if (signal.aborted) {
3693
+ yield makeErrorResult({
3694
+ subtype: "error_during_execution",
3695
+ errors: ["Aborted"],
3696
+ turns,
3697
+ costUsd,
3698
+ sessionId,
3699
+ startTime,
3700
+ apiTimeMs,
3701
+ usage: totalUsage,
3702
+ modelUsage,
3703
+ permissionDenials
3704
+ });
3705
+ return;
3706
+ }
3707
+ if (turns >= maxTurns) {
3708
+ yield makeErrorResult({
3709
+ subtype: "error_max_turns",
3710
+ errors: [`Reached maximum turns (${maxTurns})`],
3711
+ turns,
3712
+ costUsd,
3713
+ sessionId,
3714
+ startTime,
3715
+ apiTimeMs,
3716
+ usage: totalUsage,
3717
+ modelUsage,
3718
+ permissionDenials
3719
+ });
3720
+ return;
3721
+ }
3722
+ if (maxBudgetUsd > 0 && costUsd >= maxBudgetUsd) {
3723
+ yield makeErrorResult({
3724
+ subtype: "error_max_budget_usd",
3725
+ errors: [],
3726
+ turns,
3727
+ costUsd,
3728
+ sessionId,
3729
+ startTime,
3730
+ apiTimeMs,
3731
+ usage: totalUsage,
3732
+ modelUsage,
3733
+ permissionDenials
3734
+ });
3735
+ return;
3736
+ }
3737
+ const activeModel = effectiveModelState.current;
3738
+ const toolDefs = tools.getDefinitions();
3739
+ const apiStart = Date.now();
3740
+ let assistantTextParts = [];
3741
+ const toolCalls = [];
3742
+ let turnUsage = emptyTokenUsage();
3743
+ let turnStopReason = null;
3744
+ const nativeTools = nativeMemoryTool ? [nativeMemoryTool.definition] : undefined;
3745
+ try {
3746
+ const chunks = provider.chat({
3747
+ model: activeModel,
3748
+ messages,
3749
+ tools: toolDefs.length > 0 ? toolDefs : undefined,
3750
+ systemPrompt,
3751
+ signal,
3752
+ nativeTools,
3753
+ thinkingBudget: maxThinkingTokensState?.current,
3754
+ thinking,
3755
+ effort,
3756
+ outputFormat
3757
+ });
3758
+ for await (const chunk of chunks) {
3759
+ switch (chunk.type) {
3760
+ case "text_delta":
3761
+ assistantTextParts.push(chunk.text);
3762
+ if (includePartialMessages) {
3763
+ yield {
3764
+ type: "stream_event",
3765
+ event: { type: "text_delta", text: chunk.text },
3766
+ parent_tool_use_id: null,
3767
+ uuid: uuid(),
3768
+ session_id: sessionId
3769
+ };
3770
+ }
3771
+ break;
3772
+ case "thinking_delta":
3773
+ if (includePartialMessages) {
3774
+ yield {
3775
+ type: "stream_event",
3776
+ event: { type: "thinking_delta", thinking: chunk.text },
3777
+ parent_tool_use_id: null,
3778
+ uuid: uuid(),
3779
+ session_id: sessionId
3780
+ };
3781
+ }
3782
+ break;
3783
+ case "tool_call":
3784
+ toolCalls.push({ id: chunk.id, name: chunk.name, input: chunk.input });
3785
+ break;
3786
+ case "usage":
3787
+ turnUsage = mergeUsage(turnUsage, chunk.usage);
3788
+ break;
3789
+ case "done":
3790
+ turnStopReason = chunk.stopReason ?? null;
3791
+ break;
3792
+ }
3793
+ }
3794
+ } catch (err) {
3795
+ const message = err instanceof Error ? err.message : String(err);
3796
+ if (fallbackModel && activeModel !== fallbackModel) {
3797
+ effectiveModelState.current = fallbackModel;
3798
+ yield {
3799
+ type: "system",
3800
+ subtype: "status",
3801
+ status: null,
3802
+ permissionMode: permissions.getMode(),
3803
+ uuid: uuid(),
3804
+ session_id: sessionId
3805
+ };
3806
+ continue;
3807
+ }
3808
+ yield makeErrorResult({
3809
+ subtype: "error_during_execution",
3810
+ errors: [`API error: ${message}`],
3811
+ turns,
3812
+ costUsd,
3813
+ sessionId,
3814
+ startTime,
3815
+ apiTimeMs,
3816
+ usage: totalUsage,
3817
+ modelUsage,
3818
+ permissionDenials
3819
+ });
3820
+ return;
3821
+ }
3822
+ apiTimeMs += Date.now() - apiStart;
3823
+ turns++;
3824
+ totalUsage = mergeUsage(totalUsage, turnUsage);
3825
+ const turnCost = provider.calculateCost(activeModel, turnUsage);
3826
+ costUsd += turnCost;
3827
+ if (!modelUsage[activeModel]) {
3828
+ modelUsage[activeModel] = makeModelUsageEntry();
3829
+ }
3830
+ modelUsage[activeModel].inputTokens += turnUsage.inputTokens;
3831
+ modelUsage[activeModel].outputTokens += turnUsage.outputTokens;
3832
+ modelUsage[activeModel].cacheReadInputTokens += turnUsage.cacheReadInputTokens;
3833
+ modelUsage[activeModel].cacheCreationInputTokens += turnUsage.cacheCreationInputTokens;
3834
+ modelUsage[activeModel].totalCostUsd += turnCost;
3835
+ modelUsage[activeModel].webSearchRequests = (modelUsage[activeModel].webSearchRequests ?? 0) + (turnUsage.webSearchRequests ?? 0);
3836
+ modelUsage[activeModel].costUSD = modelUsage[activeModel].totalCostUsd;
3837
+ modelUsage[activeModel].contextWindow = provider.getContextWindow(activeModel);
3838
+ const assistantText = assistantTextParts.join("");
3839
+ const assistantContent = [];
3840
+ if (assistantText) {
3841
+ assistantContent.push({ type: "text", text: assistantText });
3842
+ }
3843
+ for (const call of toolCalls) {
3844
+ assistantContent.push({
3845
+ type: "tool_use",
3846
+ id: call.id,
3847
+ name: call.name,
3848
+ input: call.input
3849
+ });
3850
+ }
3851
+ messages.push({ role: "assistant", content: assistantContent });
3852
+ if (sessionLogger) {
3853
+ sessionLogger("assistant", assistantContent, null);
3854
+ }
3855
+ yield {
3856
+ type: "assistant",
3857
+ message: {
3858
+ role: "assistant",
3859
+ content: assistantContent
3860
+ },
3861
+ parent_tool_use_id: null,
3862
+ uuid: uuid(),
3863
+ session_id: sessionId
3864
+ };
3865
+ if (toolCalls.length === 0) {
3866
+ let structuredOutput;
3867
+ if (outputFormat?.type === "json_schema") {
3868
+ const parsed = extractStructuredJson(assistantText);
3869
+ if (!parsed.ok) {
3870
+ yield makeErrorResult({
3871
+ subtype: "error_max_structured_output_retries",
3872
+ errors: [parsed.error],
3873
+ turns,
3874
+ costUsd,
3875
+ sessionId,
3876
+ startTime,
3877
+ apiTimeMs,
3878
+ usage: totalUsage,
3879
+ modelUsage,
3880
+ permissionDenials
3881
+ });
3882
+ return;
3883
+ }
3884
+ structuredOutput = parsed.value;
3885
+ }
3886
+ if (hooks) {
3887
+ await hooks.fire("Stop", {
3888
+ event: "Stop",
3889
+ hook_event_name: "Stop",
3890
+ session_id: sessionId,
3891
+ text: assistantText || undefined,
3892
+ stop_reason: turnStopReason ?? undefined
3893
+ }, undefined, {
3894
+ signal
3895
+ });
3896
+ }
3897
+ if (hooks) {
3898
+ await hooks.fire("SessionEnd", {
3899
+ event: "SessionEnd",
3900
+ hook_event_name: "SessionEnd",
3901
+ session_id: sessionId,
3902
+ reason: "other"
3903
+ }, undefined, { signal });
3904
+ }
3905
+ yield {
3906
+ type: "result",
3907
+ subtype: "success",
3908
+ duration_ms: Date.now() - startTime,
3909
+ duration_api_ms: apiTimeMs,
3910
+ is_error: false,
3911
+ num_turns: turns,
3912
+ result: assistantText,
3913
+ stop_reason: turnStopReason,
3914
+ total_cost_usd: costUsd,
3915
+ usage: totalUsage,
3916
+ modelUsage,
3917
+ permission_denials: permissionDenials,
3918
+ structured_output: structuredOutput,
3919
+ uuid: uuid(),
3920
+ session_id: sessionId
3921
+ };
3922
+ return;
3923
+ }
3924
+ const toolResults = [];
3925
+ for (const call of toolCalls) {
3926
+ let hookDenied = false;
3927
+ let hookUpdatedInput;
3928
+ if (hooks) {
3929
+ const hookResult = await hooks.fire("PreToolUse", {
3930
+ event: "PreToolUse",
3931
+ hook_event_name: "PreToolUse",
3932
+ tool_name: call.name,
3933
+ tool_input: call.input,
3934
+ session_id: sessionId
3935
+ }, call.id, { signal });
3936
+ if (hookResult) {
3937
+ if (hookResult.permissionDecision === "deny") {
3938
+ hookDenied = true;
3939
+ }
3940
+ if (hookResult.updatedInput !== undefined) {
3941
+ hookUpdatedInput = hookResult.updatedInput;
3942
+ }
3943
+ }
3944
+ }
3945
+ if (hookDenied) {
3946
+ const denyContent = "Denied by hook";
3947
+ toolResults.push({
3948
+ type: "tool_result",
3949
+ tool_use_id: call.id,
3950
+ content: denyContent,
3951
+ is_error: true
3952
+ });
3953
+ if (hooks) {
3954
+ await hooks.fire("PostToolUseFailure", { event: "PostToolUseFailure", tool_name: call.name, tool_result: denyContent, tool_error: true, session_id: sessionId }, call.id, { signal });
3955
+ }
3956
+ continue;
3957
+ }
3958
+ const inputAfterHook = hookUpdatedInput !== undefined ? hookUpdatedInput : call.input;
3959
+ const permResult = await permissions.check(call.name, inputAfterHook ?? {}, { signal, toolUseId: call.id });
3960
+ if (permResult.behavior === "deny") {
3961
+ const denyContent = `Permission denied: ${permResult.message}`;
3962
+ permissionDenials.push({
3963
+ tool_name: call.name,
3964
+ tool_use_id: call.id,
3965
+ tool_input: inputAfterHook ?? {}
3966
+ });
3967
+ toolResults.push({
3968
+ type: "tool_result",
3969
+ tool_use_id: call.id,
3970
+ content: denyContent,
3971
+ is_error: true
3972
+ });
3973
+ if (hooks) {
3974
+ await hooks.fire("PostToolUseFailure", { event: "PostToolUseFailure", tool_name: call.name, tool_result: denyContent, tool_error: true, session_id: sessionId }, call.id, { signal });
3975
+ }
3976
+ continue;
3977
+ }
3978
+ const toolInput = permResult.behavior === "allow" && permResult.updatedInput ? permResult.updatedInput : inputAfterHook;
3979
+ let result;
3980
+ if (call.name === "memory" && nativeMemoryTool) {
3981
+ try {
3982
+ const content = await nativeMemoryTool.execute(toolInput);
3983
+ result = { content, isError: content.startsWith("Error:") };
3984
+ } catch (err) {
3985
+ const message = err instanceof Error ? err.message : String(err);
3986
+ result = { content: `Error: ${message}`, isError: true };
3987
+ }
3988
+ } else {
3989
+ const toolCtx = {
3990
+ cwd,
3991
+ signal,
3992
+ sessionId,
3993
+ env
3994
+ };
3995
+ result = await tools.execute(call.name, toolInput, toolCtx);
3996
+ }
3997
+ if (call.name === "ExitPlanMode") {
3998
+ permissions.setMode("default");
3999
+ }
4000
+ if (debug) {
4001
+ console.error(`[debug] Tool ${call.name}: ${result.isError ? "ERROR" : "OK"} (${result.content.length} chars)`);
4002
+ }
4003
+ if (hooks) {
4004
+ if (result.isError) {
4005
+ await hooks.fire("PostToolUseFailure", {
4006
+ event: "PostToolUseFailure",
4007
+ hook_event_name: "PostToolUseFailure",
4008
+ tool_name: call.name,
4009
+ tool_result: result.content,
4010
+ tool_error: true,
4011
+ session_id: sessionId
4012
+ }, call.id, { signal });
4013
+ } else {
4014
+ const postResult = await hooks.fire("PostToolUse", {
4015
+ event: "PostToolUse",
4016
+ hook_event_name: "PostToolUse",
4017
+ tool_name: call.name,
4018
+ tool_result: result.content,
4019
+ session_id: sessionId
4020
+ }, call.id, { signal });
4021
+ if (postResult?.additionalContext) {
4022
+ result.content += `
4023
+ ${postResult.additionalContext}`;
4024
+ }
4025
+ }
4026
+ }
4027
+ toolResults.push({
4028
+ type: "tool_result",
4029
+ tool_use_id: call.id,
4030
+ content: result.content,
4031
+ is_error: result.isError
4032
+ });
4033
+ }
4034
+ messages.push({ role: "user", content: toolResults });
4035
+ if (sessionLogger) {
4036
+ sessionLogger("user", toolResults, null);
2990
4037
  }
4038
+ yield {
4039
+ type: "user",
4040
+ message: {
4041
+ role: "user",
4042
+ content: toolResults
4043
+ },
4044
+ parent_tool_use_id: null,
4045
+ isSynthetic: true,
4046
+ uuid: uuid(),
4047
+ session_id: sessionId
4048
+ };
2991
4049
  }
2992
- if (context.customPrompt) {
2993
- sections.push(context.customPrompt);
2994
- }
2995
- return sections.join(`
2996
-
2997
- `);
2998
- }
2999
- function readProjectInstructions(cwd) {
3000
- for (const name of ["CLAUDE.md", "AGENTS.md"]) {
3001
- try {
3002
- const content = readFileSync4(join4(cwd, name), "utf-8").trim();
3003
- if (content)
3004
- return content;
3005
- } catch {}
3006
- }
3007
- return null;
3008
4050
  }
3009
4051
 
3010
4052
  // src/query.ts
3011
- function createQuery(generator, abortController) {
4053
+ function unsupported(methodName) {
4054
+ return Promise.reject(new Error(`Query.${methodName} is not supported in this runtime yet.`));
4055
+ }
4056
+ function createQuery(generator, abortController, controls) {
3012
4057
  const query = {
3013
4058
  next: generator.next.bind(generator),
3014
4059
  return: generator.return.bind(generator),
@@ -3019,6 +4064,71 @@ function createQuery(generator, abortController) {
3019
4064
  async interrupt() {
3020
4065
  abortController.abort();
3021
4066
  },
4067
+ async setPermissionMode(mode) {
4068
+ if (!controls?.setPermissionMode)
4069
+ return unsupported("setPermissionMode");
4070
+ await controls.setPermissionMode(mode);
4071
+ },
4072
+ async setModel(model) {
4073
+ if (!controls?.setModel)
4074
+ return unsupported("setModel");
4075
+ await controls.setModel(model);
4076
+ },
4077
+ async setMaxThinkingTokens(maxThinkingTokens) {
4078
+ if (!controls?.setMaxThinkingTokens)
4079
+ return unsupported("setMaxThinkingTokens");
4080
+ await controls.setMaxThinkingTokens(maxThinkingTokens);
4081
+ },
4082
+ async initializationResult() {
4083
+ if (!controls?.initializationResult)
4084
+ return unsupported("initializationResult");
4085
+ return controls.initializationResult();
4086
+ },
4087
+ async supportedCommands() {
4088
+ if (!controls?.supportedCommands)
4089
+ return unsupported("supportedCommands");
4090
+ return controls.supportedCommands();
4091
+ },
4092
+ async supportedModels() {
4093
+ if (!controls?.supportedModels)
4094
+ return unsupported("supportedModels");
4095
+ return controls.supportedModels();
4096
+ },
4097
+ async mcpServerStatus() {
4098
+ if (!controls?.mcpServerStatus)
4099
+ return unsupported("mcpServerStatus");
4100
+ return controls.mcpServerStatus();
4101
+ },
4102
+ async accountInfo() {
4103
+ if (!controls?.accountInfo)
4104
+ return unsupported("accountInfo");
4105
+ return controls.accountInfo();
4106
+ },
4107
+ async rewindFiles(userMessageId, options) {
4108
+ if (!controls?.rewindFiles)
4109
+ return unsupported("rewindFiles");
4110
+ return controls.rewindFiles(userMessageId, options);
4111
+ },
4112
+ async reconnectMcpServer(serverName) {
4113
+ if (!controls?.reconnectMcpServer)
4114
+ return unsupported("reconnectMcpServer");
4115
+ await controls.reconnectMcpServer(serverName);
4116
+ },
4117
+ async toggleMcpServer(serverName, enabled) {
4118
+ if (!controls?.toggleMcpServer)
4119
+ return unsupported("toggleMcpServer");
4120
+ await controls.toggleMcpServer(serverName, enabled);
4121
+ },
4122
+ async setMcpServers(servers) {
4123
+ if (!controls?.setMcpServers)
4124
+ return unsupported("setMcpServers");
4125
+ return controls.setMcpServers(servers);
4126
+ },
4127
+ async streamInput(stream) {
4128
+ if (!controls?.streamInput)
4129
+ return unsupported("streamInput");
4130
+ await controls.streamInput(stream);
4131
+ },
3022
4132
  close() {
3023
4133
  abortController.abort();
3024
4134
  generator.return(undefined);
@@ -3040,7 +4150,10 @@ var HOOK_EVENTS = [
3040
4150
  "SubagentStart",
3041
4151
  "SubagentStop",
3042
4152
  "PreCompact",
3043
- "PermissionRequest"
4153
+ "PermissionRequest",
4154
+ "Setup",
4155
+ "TeammateIdle",
4156
+ "TaskCompleted"
3044
4157
  ];
3045
4158
  var TOOL_EVENTS = new Set(["PreToolUse", "PostToolUse", "PostToolUseFailure"]);
3046
4159
 
@@ -3072,9 +4185,31 @@ class HookManager {
3072
4185
  }
3073
4186
  }
3074
4187
  for (const callback of entry.hooks) {
3075
- const result = await callback(input, toolUseId, { signal });
4188
+ const timeoutMs = typeof entry.timeout === "number" && entry.timeout > 0 ? Math.floor(entry.timeout * 1000) : null;
4189
+ const invoke = callback(input, toolUseId, { signal });
4190
+ const result = timeoutMs ? await Promise.race([
4191
+ invoke,
4192
+ new Promise((resolve2) => {
4193
+ setTimeout(() => resolve2({}), timeoutMs);
4194
+ })
4195
+ ]) : await invoke;
3076
4196
  if (result) {
3077
4197
  hasOutput = true;
4198
+ if (result.continue === false && !result.permissionDecision) {
4199
+ merged.permissionDecision = "deny";
4200
+ }
4201
+ if (result.decision) {
4202
+ if (result.decision.behavior === "deny") {
4203
+ merged.permissionDecision = "deny";
4204
+ } else {
4205
+ if (!merged.permissionDecision) {
4206
+ merged.permissionDecision = "allow";
4207
+ }
4208
+ if (result.decision.updatedInput !== undefined) {
4209
+ merged.updatedInput = result.decision.updatedInput;
4210
+ }
4211
+ }
4212
+ }
3078
4213
  if (result.permissionDecision) {
3079
4214
  if (!merged.permissionDecision || result.permissionDecision === "deny") {
3080
4215
  merged.permissionDecision = result.permissionDecision;
@@ -3104,14 +4239,29 @@ import { InMemoryTransport } from "@modelcontextprotocol/sdk/inMemory.js";
3104
4239
  class McpClientManager {
3105
4240
  configs;
3106
4241
  servers = new Map;
4242
+ disabled = new Set;
3107
4243
  constructor(configs) {
3108
- this.configs = configs;
4244
+ this.configs = { ...configs };
3109
4245
  }
3110
4246
  async connectAll() {
3111
4247
  const entries = Object.entries(this.configs);
3112
4248
  await Promise.all(entries.map(([name, config]) => this.connectOne(name, config)));
3113
4249
  }
3114
4250
  async connectOne(name, config) {
4251
+ if (this.disabled.has(name)) {
4252
+ this.servers.set(name, {
4253
+ name,
4254
+ client: null,
4255
+ tools: [],
4256
+ status: {
4257
+ name,
4258
+ status: "disabled",
4259
+ config: this.toStatusConfig(config),
4260
+ scope: config.type === "sdk" ? "sdk" : "session"
4261
+ }
4262
+ });
4263
+ return;
4264
+ }
3115
4265
  try {
3116
4266
  const client = new Client({ name: `fourmis-${name}`, version: "1.0.0" });
3117
4267
  if (config.type === "sdk") {
@@ -3145,13 +4295,25 @@ class McpClientManager {
3145
4295
  const tools = (toolsResult.tools ?? []).map((t) => ({
3146
4296
  name: t.name,
3147
4297
  description: t.description,
3148
- inputSchema: t.inputSchema
4298
+ inputSchema: t.inputSchema,
4299
+ annotations: t.annotations ? {
4300
+ readOnly: t.annotations.readOnly,
4301
+ destructive: t.annotations.destructive,
4302
+ openWorld: t.annotations.openWorld
4303
+ } : undefined
3149
4304
  }));
3150
4305
  this.servers.set(name, {
3151
4306
  name,
3152
4307
  client,
3153
4308
  tools,
3154
- status: { name, status: "connected", tools }
4309
+ status: {
4310
+ name,
4311
+ status: "connected",
4312
+ tools,
4313
+ serverInfo: { name, version: "1.0.0" },
4314
+ config: this.toStatusConfig(config),
4315
+ scope: config.type === "sdk" ? "sdk" : "session"
4316
+ }
3155
4317
  });
3156
4318
  } catch (err) {
3157
4319
  const error = err instanceof Error ? err.message : String(err);
@@ -3159,10 +4321,22 @@ class McpClientManager {
3159
4321
  name,
3160
4322
  client: null,
3161
4323
  tools: [],
3162
- status: { name, status: "failed", error }
4324
+ status: {
4325
+ name,
4326
+ status: "failed",
4327
+ error,
4328
+ config: this.toStatusConfig(config),
4329
+ scope: config.type === "sdk" ? "sdk" : "session"
4330
+ }
3163
4331
  });
3164
4332
  }
3165
4333
  }
4334
+ toStatusConfig(config) {
4335
+ if (config.type === "sdk") {
4336
+ return { type: "sdk", name: config.name };
4337
+ }
4338
+ return config;
4339
+ }
3166
4340
  getTools() {
3167
4341
  const result = [];
3168
4342
  for (const [serverName, server] of this.servers) {
@@ -3202,64 +4376,294 @@ class McpClientManager {
3202
4376
  return { content: `MCP tool error: ${message}`, isError: true };
3203
4377
  }
3204
4378
  }
3205
- async listResources(serverName) {
3206
- const result = [];
3207
- const serversToQuery = serverName ? [this.servers.get(serverName)].filter(Boolean) : [...this.servers.values()];
3208
- for (const server of serversToQuery) {
3209
- if (server.status.status !== "connected")
4379
+ async listResources(serverName) {
4380
+ const result = [];
4381
+ const serversToQuery = serverName ? [this.servers.get(serverName)].filter(Boolean) : [...this.servers.values()];
4382
+ for (const server of serversToQuery) {
4383
+ if (server.status.status !== "connected")
4384
+ continue;
4385
+ try {
4386
+ const resources = await server.client.listResources();
4387
+ for (const r of resources.resources ?? []) {
4388
+ result.push({
4389
+ uri: r.uri,
4390
+ name: r.name,
4391
+ description: r.description,
4392
+ mimeType: r.mimeType,
4393
+ server: server.name
4394
+ });
4395
+ }
4396
+ } catch {}
4397
+ }
4398
+ return result;
4399
+ }
4400
+ async readResource(serverName, uri) {
4401
+ const server = this.servers.get(serverName);
4402
+ if (!server || server.status.status !== "connected") {
4403
+ throw new Error(`MCP server "${serverName}" is not connected`);
4404
+ }
4405
+ const result = await server.client.readResource({ uri });
4406
+ const contents = result.contents ?? [];
4407
+ return contents.map((c) => {
4408
+ if ("text" in c)
4409
+ return c.text;
4410
+ if ("blob" in c)
4411
+ return `[binary data: ${c.mimeType ?? "unknown"}]`;
4412
+ return "";
4413
+ }).join("");
4414
+ }
4415
+ status() {
4416
+ const result = [];
4417
+ for (const [name, config] of Object.entries(this.configs)) {
4418
+ const server = this.servers.get(name);
4419
+ if (server) {
4420
+ result.push(server.status);
4421
+ } else if (this.disabled.has(name)) {
4422
+ result.push({
4423
+ name,
4424
+ status: "disabled",
4425
+ config: this.toStatusConfig(config),
4426
+ scope: config.type === "sdk" ? "sdk" : "session"
4427
+ });
4428
+ } else {
4429
+ result.push({
4430
+ name,
4431
+ status: "pending",
4432
+ config: this.toStatusConfig(config),
4433
+ scope: config.type === "sdk" ? "sdk" : "session"
4434
+ });
4435
+ }
4436
+ }
4437
+ return result;
4438
+ }
4439
+ async reconnectServer(serverName) {
4440
+ const config = this.configs[serverName];
4441
+ if (!config) {
4442
+ throw new Error(`MCP server "${serverName}" is not configured`);
4443
+ }
4444
+ await this.closeOne(serverName);
4445
+ await this.connectOne(serverName, config);
4446
+ const status = this.servers.get(serverName)?.status;
4447
+ if (!status || status.status !== "connected") {
4448
+ throw new Error(status?.error ?? `Failed to reconnect MCP server "${serverName}"`);
4449
+ }
4450
+ }
4451
+ async toggleServer(serverName, enabled) {
4452
+ const config = this.configs[serverName];
4453
+ if (!config) {
4454
+ throw new Error(`MCP server "${serverName}" is not configured`);
4455
+ }
4456
+ if (!enabled) {
4457
+ this.disabled.add(serverName);
4458
+ await this.closeOne(serverName);
4459
+ this.servers.set(serverName, {
4460
+ name: serverName,
4461
+ client: null,
4462
+ tools: [],
4463
+ status: {
4464
+ name: serverName,
4465
+ status: "disabled",
4466
+ config: this.toStatusConfig(config),
4467
+ scope: config.type === "sdk" ? "sdk" : "session"
4468
+ }
4469
+ });
4470
+ return;
4471
+ }
4472
+ this.disabled.delete(serverName);
4473
+ await this.reconnectServer(serverName);
4474
+ }
4475
+ async setServers(servers) {
4476
+ const prevNames = new Set(Object.keys(this.configs));
4477
+ const nextNames = new Set(Object.keys(servers));
4478
+ const added = [...nextNames].filter((n) => !prevNames.has(n));
4479
+ const removed = [...prevNames].filter((n) => !nextNames.has(n));
4480
+ const errors = {};
4481
+ for (const name of removed) {
4482
+ await this.closeOne(name);
4483
+ delete this.configs[name];
4484
+ this.disabled.delete(name);
4485
+ this.servers.delete(name);
4486
+ }
4487
+ for (const [name, config] of Object.entries(servers)) {
4488
+ const prev = this.configs[name];
4489
+ this.configs[name] = config;
4490
+ if (this.disabled.has(name))
4491
+ continue;
4492
+ if (!prev || JSON.stringify(this.toStatusConfig(prev)) !== JSON.stringify(this.toStatusConfig(config))) {
4493
+ await this.closeOne(name);
4494
+ await this.connectOne(name, config);
4495
+ }
4496
+ const status = this.servers.get(name)?.status;
4497
+ if (!status || status.status === "failed") {
4498
+ errors[name] = status?.error ?? "Failed to connect";
4499
+ }
4500
+ }
4501
+ return { added, removed, errors };
4502
+ }
4503
+ async closeOne(serverName) {
4504
+ const existing = this.servers.get(serverName);
4505
+ if (existing?.client) {
4506
+ try {
4507
+ await existing.client.close();
4508
+ } catch {}
4509
+ }
4510
+ }
4511
+ async closeAll() {
4512
+ for (const [name] of this.servers) {
4513
+ await this.closeOne(name);
4514
+ }
4515
+ this.servers.clear();
4516
+ }
4517
+ }
4518
+
4519
+ // src/utils/session-store.ts
4520
+ import { readFileSync as readFileSync6, appendFileSync, mkdirSync as mkdirSync3, readdirSync as readdirSync2, statSync as statSync2 } from "fs";
4521
+ import { join as join8 } from "path";
4522
+ import { homedir as homedir5 } from "os";
4523
+ function safeStringify(value) {
4524
+ try {
4525
+ return JSON.stringify(value);
4526
+ } catch {
4527
+ return String(value);
4528
+ }
4529
+ }
4530
+ function sanitizeCwd(cwd) {
4531
+ return cwd.replace(/[/.]/g, "-");
4532
+ }
4533
+ function sessionsDir(cwd) {
4534
+ return join8(homedir5(), ".claude", "projects", sanitizeCwd(cwd));
4535
+ }
4536
+ function ensureDir(dir) {
4537
+ mkdirSync3(dir, { recursive: true });
4538
+ }
4539
+ function logMessage(dir, sessionId, entry) {
4540
+ ensureDir(dir);
4541
+ const filePath = join8(dir, `${sessionId}.jsonl`);
4542
+ appendFileSync(filePath, JSON.stringify(entry) + `
4543
+ `);
4544
+ }
4545
+ function createSessionLogger(cwd, sessionId, model) {
4546
+ const dir = sessionsDir(cwd);
4547
+ let lastUuid = null;
4548
+ return (role, content, parentUuid) => {
4549
+ const entryUuid = uuid();
4550
+ let normalizedContent = content;
4551
+ if (role === "user" && typeof content === "string") {
4552
+ normalizedContent = [{ type: "text", text: content }];
4553
+ }
4554
+ const entry = {
4555
+ type: role,
4556
+ uuid: entryUuid,
4557
+ parentUuid: parentUuid ?? lastUuid,
4558
+ sessionId,
4559
+ timestamp: new Date().toISOString(),
4560
+ cwd,
4561
+ isSidechain: false,
4562
+ userType: "external",
4563
+ message: {
4564
+ role,
4565
+ content: normalizedContent,
4566
+ ...role === "assistant" && model ? { model } : {}
4567
+ },
4568
+ ...role === "user" ? { permissionMode: "default" } : {}
4569
+ };
4570
+ logMessage(dir, sessionId, entry);
4571
+ lastUuid = entryUuid;
4572
+ return entryUuid;
4573
+ };
4574
+ }
4575
+ function findLatestSession(cwd) {
4576
+ const dir = sessionsDir(cwd);
4577
+ try {
4578
+ const files = readdirSync2(dir).filter((f) => f.endsWith(".jsonl")).map((f) => {
4579
+ const filePath = join8(dir, f);
4580
+ try {
4581
+ return { name: f, mtime: statSync2(filePath).mtimeMs };
4582
+ } catch {
4583
+ return null;
4584
+ }
4585
+ }).filter((f) => f !== null).sort((a, b) => b.mtime - a.mtime);
4586
+ if (files.length === 0)
4587
+ return null;
4588
+ return files[0].name.replace(/\.jsonl$/, "");
4589
+ } catch {
4590
+ return null;
4591
+ }
4592
+ }
4593
+ function loadSessionMessages(cwd, sessionId, resumeSessionAt) {
4594
+ const dir = sessionsDir(cwd);
4595
+ const filePath = join8(dir, `${sessionId}.jsonl`);
4596
+ let lines;
4597
+ try {
4598
+ lines = readFileSync6(filePath, "utf-8").trim().split(`
4599
+ `).filter(Boolean);
4600
+ } catch {
4601
+ return [];
4602
+ }
4603
+ const messages = [];
4604
+ let reachedResumePoint = false;
4605
+ for (const line of lines) {
4606
+ if (resumeSessionAt && reachedResumePoint)
4607
+ break;
4608
+ try {
4609
+ const entry = JSON.parse(line);
4610
+ if (entry.type !== "user" && entry.type !== "assistant")
3210
4611
  continue;
3211
- try {
3212
- const resources = await server.client.listResources();
3213
- for (const r of resources.resources ?? []) {
3214
- result.push({
3215
- uri: r.uri,
3216
- name: r.name,
3217
- description: r.description,
3218
- mimeType: r.mimeType,
3219
- server: server.name
4612
+ if (entry.isMeta === true)
4613
+ continue;
4614
+ const message = entry.message;
4615
+ if (!message)
4616
+ continue;
4617
+ const role = entry.type === "user" ? "user" : "assistant";
4618
+ let content;
4619
+ if (typeof message.content === "string") {
4620
+ content = message.content;
4621
+ } else if (Array.isArray(message.content)) {
4622
+ const normalizedBlocks = [];
4623
+ for (const c of message.content) {
4624
+ if (!c || typeof c !== "object")
4625
+ continue;
4626
+ const block = c;
4627
+ if (typeof block.type !== "string")
4628
+ continue;
4629
+ if (block.type === "text" && typeof block.text === "string") {
4630
+ normalizedBlocks.push({ type: "text", text: block.text });
4631
+ continue;
4632
+ }
4633
+ if (block.type === "tool_use" && typeof block.id === "string" && typeof block.name === "string") {
4634
+ normalizedBlocks.push({
4635
+ type: "tool_use",
4636
+ id: block.id,
4637
+ name: block.name,
4638
+ input: block.input ?? {}
4639
+ });
4640
+ continue;
4641
+ }
4642
+ if (block.type === "tool_result" && typeof block.tool_use_id === "string") {
4643
+ normalizedBlocks.push({
4644
+ type: "tool_result",
4645
+ tool_use_id: block.tool_use_id,
4646
+ content: typeof block.content === "string" ? block.content : safeStringify(block.content),
4647
+ is_error: typeof block.is_error === "boolean" ? block.is_error : undefined
4648
+ });
4649
+ continue;
4650
+ }
4651
+ normalizedBlocks.push({
4652
+ type: "text",
4653
+ text: `[session:${block.type}] ${safeStringify(block)}`
3220
4654
  });
3221
4655
  }
3222
- } catch {}
3223
- }
3224
- return result;
3225
- }
3226
- async readResource(serverName, uri) {
3227
- const server = this.servers.get(serverName);
3228
- if (!server || server.status.status !== "connected") {
3229
- throw new Error(`MCP server "${serverName}" is not connected`);
3230
- }
3231
- const result = await server.client.readResource({ uri });
3232
- const contents = result.contents ?? [];
3233
- return contents.map((c) => {
3234
- if ("text" in c)
3235
- return c.text;
3236
- if ("blob" in c)
3237
- return `[binary data: ${c.mimeType ?? "unknown"}]`;
3238
- return "";
3239
- }).join("");
3240
- }
3241
- status() {
3242
- const result = [];
3243
- for (const [name] of Object.entries(this.configs)) {
3244
- const server = this.servers.get(name);
3245
- if (server) {
3246
- result.push(server.status);
4656
+ content = normalizedBlocks;
3247
4657
  } else {
3248
- result.push({ name, status: "pending" });
4658
+ continue;
3249
4659
  }
3250
- }
3251
- return result;
3252
- }
3253
- async closeAll() {
3254
- for (const [, server] of this.servers) {
3255
- if (server.client) {
3256
- try {
3257
- await server.client.close();
3258
- } catch {}
4660
+ messages.push({ role, content });
4661
+ if (resumeSessionAt && entry.uuid === resumeSessionAt) {
4662
+ reachedResumePoint = true;
3259
4663
  }
3260
- }
3261
- this.servers.clear();
4664
+ } catch {}
3262
4665
  }
4666
+ return messages;
3263
4667
  }
3264
4668
 
3265
4669
  // src/agents/tools.ts
@@ -3282,6 +4686,15 @@ function createTaskTool(ctx) {
3282
4686
  type: "string",
3283
4687
  description: "The type of agent to use. Must match a registered agent definition."
3284
4688
  },
4689
+ model: {
4690
+ type: "string",
4691
+ enum: ["sonnet", "opus", "haiku"],
4692
+ description: "Optional model family hint for this subagent."
4693
+ },
4694
+ resume: {
4695
+ type: "string",
4696
+ description: "Optional session ID to resume this subagent from."
4697
+ },
3285
4698
  run_in_background: {
3286
4699
  type: "boolean",
3287
4700
  description: "If true, run the task in the background and return a task ID."
@@ -3289,16 +4702,35 @@ function createTaskTool(ctx) {
3289
4702
  max_turns: {
3290
4703
  type: "number",
3291
4704
  description: "Maximum number of turns for the subagent."
4705
+ },
4706
+ name: {
4707
+ type: "string",
4708
+ description: "Optional display name for the spawned subagent."
4709
+ },
4710
+ team_name: {
4711
+ type: "string",
4712
+ description: "Optional team name context for this subagent."
4713
+ },
4714
+ mode: {
4715
+ type: "string",
4716
+ enum: ["acceptEdits", "bypassPermissions", "default", "delegate", "dontAsk", "plan"],
4717
+ description: "Permission mode hint for the spawned subagent."
3292
4718
  }
3293
4719
  },
3294
- required: ["prompt", "subagent_type"]
4720
+ required: ["description", "prompt", "subagent_type"]
3295
4721
  },
3296
4722
  async execute(input, toolCtx) {
3297
4723
  const {
4724
+ description,
3298
4725
  prompt,
3299
4726
  subagent_type,
4727
+ model: requestedModel,
4728
+ resume,
3300
4729
  run_in_background,
3301
- max_turns
4730
+ max_turns,
4731
+ name,
4732
+ team_name,
4733
+ mode
3302
4734
  } = input;
3303
4735
  const agentDef = ctx.agents[subagent_type];
3304
4736
  if (!agentDef) {
@@ -3312,28 +4744,53 @@ function createTaskTool(ctx) {
3312
4744
  await ctx.parentHooks.fire("SubagentStart", { event: "SubagentStart", agent_type: subagent_type, session_id: toolCtx.sessionId }, undefined, { signal: toolCtx.signal });
3313
4745
  }
3314
4746
  const provider = agentDef.provider ? getProvider(agentDef.provider) : ctx.parentProvider;
3315
- const model = agentDef.model ?? ctx.parentModel;
3316
- let subTools;
3317
- if (agentDef.tools) {
3318
- subTools = buildToolRegistry(agentDef.tools);
3319
- } else {
3320
- subTools = buildToolRegistry(resolveToolNames("coding"));
3321
- }
4747
+ const modelAliases = {
4748
+ sonnet: "claude-sonnet-4-5-20250929",
4749
+ opus: "claude-opus-4-5-20251101",
4750
+ haiku: "claude-haiku-4-5-20251001"
4751
+ };
4752
+ const model = requestedModel ? modelAliases[requestedModel] : agentDef.model ?? ctx.parentModel;
4753
+ const baseTools = agentDef.tools ?? resolveToolNames({ type: "preset", preset: "claude_code" });
4754
+ const subTools = buildToolRegistry(baseTools, undefined, agentDef.disallowedTools);
3322
4755
  const maxTurns = max_turns ?? agentDef.maxTurns ?? 10;
3323
- const sessionId = uuid();
4756
+ const sessionId = resume ?? uuid();
4757
+ const previousMessages = resume ? loadSessionMessages(ctx.parentCwd, resume) : undefined;
3324
4758
  const abortController = new AbortController;
3325
4759
  if (toolCtx.signal) {
3326
4760
  toolCtx.signal.addEventListener("abort", () => abortController.abort(), { once: true });
3327
4761
  }
3328
- const systemPrompt = `${agentDef.prompt}
4762
+ const systemPromptParts = [
4763
+ agentDef.prompt,
4764
+ `You are a subagent of type "${subagent_type}". ${agentDef.description}`,
4765
+ `Task summary: ${description}`
4766
+ ];
4767
+ if (agentDef.criticalSystemReminder_EXPERIMENTAL) {
4768
+ systemPromptParts.push(`Critical reminder: ${agentDef.criticalSystemReminder_EXPERIMENTAL}`);
4769
+ }
4770
+ if (agentDef.skills && agentDef.skills.length > 0) {
4771
+ systemPromptParts.push(`Available skills:
4772
+ ${agentDef.skills.map((s) => `- ${s}`).join(`
4773
+ `)}`);
4774
+ }
4775
+ if (name) {
4776
+ systemPromptParts.push(`Subagent name: ${name}`);
4777
+ }
4778
+ if (team_name) {
4779
+ systemPromptParts.push(`Team context: ${team_name}`);
4780
+ }
4781
+ if (mode) {
4782
+ systemPromptParts.push(`Permission mode hint: ${mode}`);
4783
+ }
4784
+ const systemPrompt = systemPromptParts.join(`
3329
4785
 
3330
- You are a subagent of type "${subagent_type}". ${agentDef.description}`;
4786
+ `);
3331
4787
  const runAgent = async () => {
3332
4788
  const messages = [];
3333
4789
  let resultText = "";
3334
4790
  for await (const msg of agentLoop(prompt, {
3335
4791
  provider,
3336
4792
  model,
4793
+ modelState: { current: model },
3337
4794
  systemPrompt,
3338
4795
  tools: subTools,
3339
4796
  permissions: ctx.parentPermissions,
@@ -3341,18 +4798,23 @@ You are a subagent of type "${subagent_type}". ${agentDef.description}`;
3341
4798
  sessionId,
3342
4799
  maxTurns,
3343
4800
  maxBudgetUsd: 5,
3344
- includeStreamEvents: false,
4801
+ includePartialMessages: false,
3345
4802
  signal: abortController.signal,
3346
4803
  env: ctx.parentEnv,
3347
4804
  debug: ctx.parentDebug,
3348
- hooks: ctx.parentHooks
4805
+ hooks: ctx.parentHooks,
4806
+ previousMessages
3349
4807
  })) {
3350
4808
  messages.push(msg);
3351
- if (msg.type === "text") {
3352
- resultText += msg.text;
4809
+ if (msg.type === "assistant") {
4810
+ for (const block of msg.message.content) {
4811
+ if (block.type === "text") {
4812
+ resultText += block.text;
4813
+ }
4814
+ }
3353
4815
  }
3354
4816
  if (msg.type === "result" && msg.subtype === "success") {
3355
- resultText = msg.text ?? resultText;
4817
+ resultText = msg.result || resultText;
3356
4818
  }
3357
4819
  }
3358
4820
  return resultText || "Subagent completed with no text output.";
@@ -3490,7 +4952,7 @@ class TaskManager {
3490
4952
  if (!block) {
3491
4953
  return `Task "${id}" is still running.`;
3492
4954
  }
3493
- const timeoutPromise = new Promise((resolve) => setTimeout(resolve, timeoutMs));
4955
+ const timeoutPromise = new Promise((resolve2) => setTimeout(resolve2, timeoutMs));
3494
4956
  await Promise.race([task.promise, timeoutPromise]);
3495
4957
  if (task.status === "running") {
3496
4958
  return `Task "${id}" is still running (timed out after ${timeoutMs}ms).`;
@@ -3511,115 +4973,12 @@ class TaskManager {
3511
4973
  }
3512
4974
  }
3513
4975
 
3514
- // src/utils/session-store.ts
3515
- import { readFileSync as readFileSync5, appendFileSync, mkdirSync as mkdirSync3, readdirSync, statSync } from "fs";
3516
- import { join as join5 } from "path";
3517
- import { homedir as homedir4 } from "os";
3518
- function sanitizeCwd(cwd) {
3519
- return cwd.replace(/[/.]/g, "-");
3520
- }
3521
- function sessionsDir(cwd) {
3522
- return join5(homedir4(), ".claude", "projects", sanitizeCwd(cwd));
3523
- }
3524
- function ensureDir(dir) {
3525
- mkdirSync3(dir, { recursive: true });
3526
- }
3527
- function logMessage(dir, sessionId, entry) {
3528
- ensureDir(dir);
3529
- const filePath = join5(dir, `${sessionId}.jsonl`);
3530
- appendFileSync(filePath, JSON.stringify(entry) + `
3531
- `);
3532
- }
3533
- function createSessionLogger(cwd, sessionId, model) {
3534
- const dir = sessionsDir(cwd);
3535
- let lastUuid = null;
3536
- return (role, content, parentUuid) => {
3537
- const entryUuid = uuid();
3538
- let normalizedContent = content;
3539
- if (role === "user" && typeof content === "string") {
3540
- normalizedContent = [{ type: "text", text: content }];
3541
- }
3542
- const entry = {
3543
- type: role,
3544
- uuid: entryUuid,
3545
- parentUuid: parentUuid ?? lastUuid,
3546
- sessionId,
3547
- timestamp: new Date().toISOString(),
3548
- cwd,
3549
- isSidechain: false,
3550
- userType: "external",
3551
- message: {
3552
- role,
3553
- content: normalizedContent,
3554
- ...role === "assistant" && model ? { model } : {}
3555
- },
3556
- ...role === "user" ? { permissionMode: "default" } : {}
3557
- };
3558
- logMessage(dir, sessionId, entry);
3559
- lastUuid = entryUuid;
3560
- return entryUuid;
3561
- };
3562
- }
3563
- function findLatestSession(cwd) {
3564
- const dir = sessionsDir(cwd);
3565
- try {
3566
- const files = readdirSync(dir).filter((f) => f.endsWith(".jsonl")).map((f) => {
3567
- const filePath = join5(dir, f);
3568
- try {
3569
- return { name: f, mtime: statSync(filePath).mtimeMs };
3570
- } catch {
3571
- return null;
3572
- }
3573
- }).filter((f) => f !== null).sort((a, b) => b.mtime - a.mtime);
3574
- if (files.length === 0)
3575
- return null;
3576
- return files[0].name.replace(/\.jsonl$/, "");
3577
- } catch {
3578
- return null;
3579
- }
3580
- }
3581
- function loadSessionMessages(cwd, sessionId) {
3582
- const dir = sessionsDir(cwd);
3583
- const filePath = join5(dir, `${sessionId}.jsonl`);
3584
- let lines;
3585
- try {
3586
- lines = readFileSync5(filePath, "utf-8").trim().split(`
3587
- `).filter(Boolean);
3588
- } catch {
3589
- return [];
3590
- }
3591
- const messages = [];
3592
- for (const line of lines) {
3593
- try {
3594
- const entry = JSON.parse(line);
3595
- if (entry.type !== "user" && entry.type !== "assistant")
3596
- continue;
3597
- if (entry.isMeta === true)
3598
- continue;
3599
- const message = entry.message;
3600
- if (!message)
3601
- continue;
3602
- const role = entry.type === "user" ? "user" : "assistant";
3603
- let content;
3604
- if (typeof message.content === "string") {
3605
- content = message.content;
3606
- } else if (Array.isArray(message.content)) {
3607
- content = message.content.filter((c) => c.type === "text" || c.type === "tool_use" || c.type === "tool_result").map((c) => c);
3608
- } else {
3609
- continue;
3610
- }
3611
- messages.push({ role, content });
3612
- } catch {}
3613
- }
3614
- return messages;
3615
- }
3616
-
3617
4976
  // src/memory/memory-handler.ts
3618
- import { readdir, stat, readFile, writeFile, rm, rename, mkdir as mkdir2 } from "fs/promises";
3619
- import { join as join6, resolve, relative } from "path";
3620
- import { existsSync as existsSync4 } from "fs";
4977
+ import { readdir, stat, readFile as readFile3, writeFile as writeFile4, rm, rename, mkdir as mkdir4 } from "fs/promises";
4978
+ import { join as join9, resolve as resolve2, relative as relative2 } from "path";
4979
+ import { existsSync as existsSync5 } from "fs";
3621
4980
  function createMemoryHandler(memoryDir) {
3622
- const absMemoryDir = resolve(memoryDir);
4981
+ const absMemoryDir = resolve2(memoryDir);
3623
4982
  function resolvePath2(logicalPath) {
3624
4983
  let cleaned = logicalPath;
3625
4984
  if (cleaned.startsWith("/memories")) {
@@ -3631,15 +4990,15 @@ function createMemoryHandler(memoryDir) {
3631
4990
  if (cleaned.includes("..") || cleaned.includes("%2e") || cleaned.includes("%2E")) {
3632
4991
  throw new Error(`Path traversal detected: ${logicalPath}`);
3633
4992
  }
3634
- const absPath = cleaned === "" ? absMemoryDir : resolve(absMemoryDir, cleaned);
3635
- const rel = relative(absMemoryDir, absPath);
3636
- if (rel.startsWith("..") || resolve(absPath) !== absPath && !absPath.startsWith(absMemoryDir)) {
4993
+ const absPath = cleaned === "" ? absMemoryDir : resolve2(absMemoryDir, cleaned);
4994
+ const rel = relative2(absMemoryDir, absPath);
4995
+ if (rel.startsWith("..") || resolve2(absPath) !== absPath && !absPath.startsWith(absMemoryDir)) {
3637
4996
  throw new Error(`Path traversal detected: ${logicalPath}`);
3638
4997
  }
3639
4998
  return absPath;
3640
4999
  }
3641
5000
  function toLogicalPath(absPath) {
3642
- const rel = relative(absMemoryDir, absPath);
5001
+ const rel = relative2(absMemoryDir, absPath);
3643
5002
  return rel === "" ? "/memories" : `/memories/${rel}`;
3644
5003
  }
3645
5004
  function formatSize(bytes) {
@@ -3662,7 +5021,7 @@ function createMemoryHandler(memoryDir) {
3662
5021
  for (const entry of entries.sort((a, b) => a.name.localeCompare(b.name))) {
3663
5022
  if (entry.name.startsWith(".") || entry.name === "node_modules")
3664
5023
  continue;
3665
- const entryPath = join6(dirPath, entry.name);
5024
+ const entryPath = join9(dirPath, entry.name);
3666
5025
  const entryStat = await stat(entryPath);
3667
5026
  lines.push(`${formatSize(entryStat.size)} ${toLogicalPath(entryPath)}`);
3668
5027
  if (entry.isDirectory()) {
@@ -3695,7 +5054,7 @@ function createMemoryHandler(memoryDir) {
3695
5054
  }
3696
5055
  async function handleView(cmd) {
3697
5056
  const absPath = resolvePath2(cmd.path);
3698
- if (!existsSync4(absPath)) {
5057
+ if (!existsSync5(absPath)) {
3699
5058
  return `The path ${cmd.path} does not exist. Please provide a valid path.`;
3700
5059
  }
3701
5060
  const s = await stat(absPath);
@@ -3705,31 +5064,31 @@ function createMemoryHandler(memoryDir) {
3705
5064
  ${lines.join(`
3706
5065
  `)}`;
3707
5066
  }
3708
- const content = await readFile(absPath, "utf-8");
5067
+ const content = await readFile3(absPath, "utf-8");
3709
5068
  const formatted = formatFileContent(content, cmd.view_range);
3710
5069
  return `Here's the content of ${cmd.path} with line numbers:
3711
5070
  ${formatted}`;
3712
5071
  }
3713
5072
  async function handleCreate(cmd) {
3714
5073
  const absPath = resolvePath2(cmd.path);
3715
- if (existsSync4(absPath)) {
5074
+ if (existsSync5(absPath)) {
3716
5075
  return `Error: File ${cmd.path} already exists`;
3717
5076
  }
3718
- const parentDir = resolve(absPath, "..");
3719
- await mkdir2(parentDir, { recursive: true });
3720
- await writeFile(absPath, cmd.file_text, "utf-8");
5077
+ const parentDir = resolve2(absPath, "..");
5078
+ await mkdir4(parentDir, { recursive: true });
5079
+ await writeFile4(absPath, cmd.file_text, "utf-8");
3721
5080
  return `File created successfully at: ${cmd.path}`;
3722
5081
  }
3723
5082
  async function handleStrReplace(cmd) {
3724
5083
  const absPath = resolvePath2(cmd.path);
3725
- if (!existsSync4(absPath)) {
5084
+ if (!existsSync5(absPath)) {
3726
5085
  return `Error: The path ${cmd.path} does not exist. Please provide a valid path.`;
3727
5086
  }
3728
5087
  const s = await stat(absPath);
3729
5088
  if (s.isDirectory()) {
3730
5089
  return `Error: The path ${cmd.path} does not exist. Please provide a valid path.`;
3731
5090
  }
3732
- const content = await readFile(absPath, "utf-8");
5091
+ const content = await readFile3(absPath, "utf-8");
3733
5092
  const lines = content.split(`
3734
5093
  `);
3735
5094
  const matchingLines = [];
@@ -3752,7 +5111,7 @@ ${formatted}`;
3752
5111
  return `No replacement was performed. Multiple occurrences of old_str \`${cmd.old_str}\` in lines: ${matchingLines.join(", ")}. Please ensure it is unique`;
3753
5112
  }
3754
5113
  const newContent = content.replace(cmd.old_str, cmd.new_str);
3755
- await writeFile(absPath, newContent, "utf-8");
5114
+ await writeFile4(absPath, newContent, "utf-8");
3756
5115
  const newLines = newContent.split(`
3757
5116
  `);
3758
5117
  const replaceLine = matchingLines[0];
@@ -3765,14 +5124,14 @@ ${snippet}`;
3765
5124
  }
3766
5125
  async function handleInsert(cmd) {
3767
5126
  const absPath = resolvePath2(cmd.path);
3768
- if (!existsSync4(absPath)) {
5127
+ if (!existsSync5(absPath)) {
3769
5128
  return `Error: The path ${cmd.path} does not exist`;
3770
5129
  }
3771
5130
  const s = await stat(absPath);
3772
5131
  if (s.isDirectory()) {
3773
5132
  return `Error: The path ${cmd.path} does not exist`;
3774
5133
  }
3775
- const content = await readFile(absPath, "utf-8");
5134
+ const content = await readFile3(absPath, "utf-8");
3776
5135
  const lines = content.split(`
3777
5136
  `);
3778
5137
  if (cmd.insert_line < 0 || cmd.insert_line > lines.length) {
@@ -3781,13 +5140,13 @@ ${snippet}`;
3781
5140
  const insertLines = cmd.insert_text.split(`
3782
5141
  `);
3783
5142
  lines.splice(cmd.insert_line, 0, ...insertLines);
3784
- await writeFile(absPath, lines.join(`
5143
+ await writeFile4(absPath, lines.join(`
3785
5144
  `), "utf-8");
3786
5145
  return `The file ${cmd.path} has been edited.`;
3787
5146
  }
3788
5147
  async function handleDelete(cmd) {
3789
5148
  const absPath = resolvePath2(cmd.path);
3790
- if (!existsSync4(absPath)) {
5149
+ if (!existsSync5(absPath)) {
3791
5150
  return `Error: The path ${cmd.path} does not exist`;
3792
5151
  }
3793
5152
  await rm(absPath, { recursive: true, force: true });
@@ -3796,20 +5155,20 @@ ${snippet}`;
3796
5155
  async function handleRename(cmd) {
3797
5156
  const oldAbs = resolvePath2(cmd.old_path);
3798
5157
  const newAbs = resolvePath2(cmd.new_path);
3799
- if (!existsSync4(oldAbs)) {
5158
+ if (!existsSync5(oldAbs)) {
3800
5159
  return `Error: The path ${cmd.old_path} does not exist`;
3801
5160
  }
3802
- if (existsSync4(newAbs)) {
5161
+ if (existsSync5(newAbs)) {
3803
5162
  return `Error: The destination ${cmd.new_path} already exists`;
3804
5163
  }
3805
- const parentDir = resolve(newAbs, "..");
3806
- await mkdir2(parentDir, { recursive: true });
5164
+ const parentDir = resolve2(newAbs, "..");
5165
+ await mkdir4(parentDir, { recursive: true });
3807
5166
  await rename(oldAbs, newAbs);
3808
5167
  return `Successfully renamed ${cmd.old_path} to ${cmd.new_path}`;
3809
5168
  }
3810
5169
  async function execute(cmd) {
3811
- if (!existsSync4(absMemoryDir)) {
3812
- await mkdir2(absMemoryDir, { recursive: true });
5170
+ if (!existsSync5(absMemoryDir)) {
5171
+ await mkdir4(absMemoryDir, { recursive: true });
3813
5172
  }
3814
5173
  switch (cmd.command) {
3815
5174
  case "view":
@@ -3914,26 +5273,73 @@ function createMemoryTool(config) {
3914
5273
  }
3915
5274
  };
3916
5275
  }
3917
-
3918
5276
  // src/api.ts
3919
5277
  var DEFAULT_MODEL = "claude-sonnet-4-5-20250929";
3920
5278
  var DEFAULT_MAX_TURNS = 10;
3921
5279
  var DEFAULT_MAX_BUDGET_USD = 5;
3922
5280
  function query(params) {
3923
- const { prompt, options = {} } = params;
5281
+ const { options = {} } = params;
5282
+ const prompt = params.prompt;
5283
+ if (typeof prompt !== "string") {
5284
+ throw new Error("query({ prompt: AsyncIterable }) is not supported in single-prompt mode yet. Use Query.streamInput() in a streaming session.");
5285
+ }
3924
5286
  const providerName = options.provider ?? "anthropic";
3925
5287
  const provider = getProvider(providerName, {
3926
5288
  apiKey: options.apiKey,
3927
5289
  baseUrl: options.baseUrl
3928
5290
  });
3929
5291
  const model = options.model ?? DEFAULT_MODEL;
5292
+ const fallbackModel = options.fallbackModel;
5293
+ const modelState = { current: model };
5294
+ const resolvedThinkingBudget = (() => {
5295
+ if (options.thinking) {
5296
+ switch (options.thinking.type) {
5297
+ case "disabled":
5298
+ return 0;
5299
+ case "enabled":
5300
+ return options.thinking.budgetTokens;
5301
+ case "adaptive":
5302
+ return;
5303
+ }
5304
+ }
5305
+ return options.maxThinkingTokens;
5306
+ })();
5307
+ const maxThinkingTokensState = { current: resolvedThinkingBudget };
5308
+ if (options.permissionMode === "bypassPermissions" && options.allowDangerouslySkipPermissions !== true) {
5309
+ throw new Error('permissionMode "bypassPermissions" requires allowDangerouslySkipPermissions: true');
5310
+ }
3930
5311
  const toolNames = resolveToolNames(options.tools);
3931
5312
  const registry = buildToolRegistry(toolNames, undefined, options.disallowedTools);
3932
- const systemPrompt = options.systemPrompt ?? buildSystemPrompt({
3933
- tools: registry.list(),
3934
- cwd: options.cwd,
3935
- customPrompt: options.appendSystemPrompt
3936
- });
5313
+ let skills = [];
5314
+ {
5315
+ const skillsResult = loadSkills({
5316
+ cwd: options.cwd,
5317
+ skillPaths: options.skillPaths,
5318
+ includeDefaults: options.includeDefaultSkills !== false
5319
+ });
5320
+ skills = skillsResult.skills;
5321
+ if (options.debug && skillsResult.diagnostics.length > 0) {
5322
+ for (const d of skillsResult.diagnostics) {
5323
+ console.warn(`[skills] ${d.type}: ${d.message} (${d.path})`);
5324
+ }
5325
+ }
5326
+ }
5327
+ const systemPrompt = (() => {
5328
+ if (typeof options.systemPrompt === "string") {
5329
+ return options.systemPrompt;
5330
+ }
5331
+ const appendedPrompt = typeof options.systemPrompt === "object" ? [options.systemPrompt.append, options.appendSystemPrompt].filter(Boolean).join(`
5332
+
5333
+ `) : options.appendSystemPrompt;
5334
+ return buildSystemPrompt({
5335
+ tools: registry.list(),
5336
+ cwd: options.cwd,
5337
+ additionalDirectories: options.additionalDirectories,
5338
+ loadProjectInstructions: Array.isArray(options.settingSources) && options.settingSources.includes("project"),
5339
+ customPrompt: appendedPrompt,
5340
+ skills
5341
+ });
5342
+ })();
3937
5343
  let settingsManager;
3938
5344
  let mergedPermissions = options.permissions;
3939
5345
  if (options.settingSources && options.settingSources.length > 0) {
@@ -3979,7 +5385,7 @@ function query(params) {
3979
5385
  }
3980
5386
  }
3981
5387
  } else if (options.resume) {
3982
- previousMessages = loadSessionMessages(cwd, options.resume);
5388
+ previousMessages = loadSessionMessages(cwd, options.resume, options.resumeSessionAt);
3983
5389
  if (options.forkSession) {
3984
5390
  sessionId = uuid();
3985
5391
  } else {
@@ -3988,18 +5394,28 @@ function query(params) {
3988
5394
  }
3989
5395
  const persistSession = options.persistSession !== false;
3990
5396
  const sessionLogger = persistSession ? createSessionLogger(cwd, sessionId, model) : undefined;
3991
- const abortController = new AbortController;
5397
+ const abortController = options.abortController ?? new AbortController;
3992
5398
  if (options.signal) {
3993
5399
  options.signal.addEventListener("abort", () => abortController.abort(), { once: true });
3994
5400
  }
3995
5401
  const hookManager = options.hooks ? new HookManager(options.hooks) : undefined;
3996
5402
  const mcpClient = options.mcpServers && Object.keys(options.mcpServers).length > 0 ? new McpClientManager(options.mcpServers) : undefined;
5403
+ const syncMcpTools = () => {
5404
+ if (!mcpClient)
5405
+ return;
5406
+ registry.clearByPrefix("mcp__");
5407
+ for (const tool of mcpClient.getTools()) {
5408
+ registry.register(tool);
5409
+ }
5410
+ registry.register(createListMcpResourcesTool(mcpClient));
5411
+ registry.register(createReadMcpResourceTool(mcpClient));
5412
+ };
3997
5413
  if (options.agents && Object.keys(options.agents).length > 0) {
3998
5414
  const taskManager = new TaskManager;
3999
5415
  const agentCtx = {
4000
5416
  agents: options.agents,
4001
5417
  parentProvider: provider,
4002
- parentModel: model,
5418
+ parentModel: modelState.current,
4003
5419
  parentPermissions: permissions,
4004
5420
  parentHooks: hookManager,
4005
5421
  parentCwd: cwd,
@@ -4023,6 +5439,12 @@ function query(params) {
4023
5439
  const generator = agentLoop(prompt, {
4024
5440
  provider,
4025
5441
  model,
5442
+ modelState,
5443
+ maxThinkingTokensState,
5444
+ fallbackModel,
5445
+ thinking: options.thinking,
5446
+ effort: options.effort,
5447
+ outputFormat: options.outputFormat,
4026
5448
  systemPrompt,
4027
5449
  tools: registry,
4028
5450
  permissions,
@@ -4030,7 +5452,7 @@ function query(params) {
4030
5452
  sessionId,
4031
5453
  maxTurns: options.maxTurns ?? DEFAULT_MAX_TURNS,
4032
5454
  maxBudgetUsd: options.maxBudgetUsd ?? DEFAULT_MAX_BUDGET_USD,
4033
- includeStreamEvents: options.includeStreamEvents ?? false,
5455
+ includePartialMessages: options.includePartialMessages ?? false,
4034
5456
  signal: abortController.signal,
4035
5457
  env: options.env,
4036
5458
  debug: options.debug,
@@ -4038,9 +5460,95 @@ function query(params) {
4038
5460
  mcpClient,
4039
5461
  previousMessages,
4040
5462
  sessionLogger,
4041
- nativeMemoryTool
5463
+ nativeMemoryTool,
5464
+ initMeta: {
5465
+ betas: options.betas,
5466
+ outputStyle: "default",
5467
+ slashCommands: skills.map((s) => `/${s.name}`),
5468
+ skills: skills.map((s) => s.name),
5469
+ plugins: options.plugins,
5470
+ agents: options.agents ? Object.keys(options.agents) : undefined
5471
+ }
4042
5472
  });
4043
- return createQuery(generator, abortController);
5473
+ const controls = {
5474
+ async setPermissionMode(mode) {
5475
+ permissions.setMode(mode);
5476
+ },
5477
+ async setModel(nextModel) {
5478
+ modelState.current = nextModel ?? model;
5479
+ },
5480
+ async setMaxThinkingTokens(maxThinkingTokens) {
5481
+ maxThinkingTokensState.current = maxThinkingTokens ?? undefined;
5482
+ },
5483
+ async initializationResult() {
5484
+ const models = provider.listModels ? await provider.listModels() : [];
5485
+ return {
5486
+ commands: skills.map((s) => ({
5487
+ name: s.name,
5488
+ description: s.description,
5489
+ argumentHint: ""
5490
+ })),
5491
+ output_style: "default",
5492
+ available_output_styles: ["default"],
5493
+ models,
5494
+ account: {}
5495
+ };
5496
+ },
5497
+ async supportedCommands() {
5498
+ return skills.map((s) => ({
5499
+ name: s.name,
5500
+ description: s.description,
5501
+ argumentHint: ""
5502
+ }));
5503
+ },
5504
+ async supportedModels() {
5505
+ return provider.listModels ? await provider.listModels() : [];
5506
+ },
5507
+ async mcpServerStatus() {
5508
+ return mcpClient ? mcpClient.status() : [];
5509
+ },
5510
+ async accountInfo() {
5511
+ return {
5512
+ tokenSource: options.apiKey ? "api-key" : "runtime",
5513
+ apiKeySource: options.apiKey ? "explicit" : "env_or_oauth"
5514
+ };
5515
+ },
5516
+ async rewindFiles(_userMessageId, _options) {
5517
+ if (!options.enableFileCheckpointing) {
5518
+ return {
5519
+ canRewind: false,
5520
+ error: "File checkpointing is disabled. Set enableFileCheckpointing: true."
5521
+ };
5522
+ }
5523
+ return {
5524
+ canRewind: false,
5525
+ error: "File checkpoint rewind is not implemented in fourmis-agent-sdk yet."
5526
+ };
5527
+ },
5528
+ async reconnectMcpServer(serverName) {
5529
+ if (!mcpClient)
5530
+ throw new Error("No MCP servers are configured for this query.");
5531
+ await mcpClient.reconnectServer(serverName);
5532
+ syncMcpTools();
5533
+ },
5534
+ async toggleMcpServer(serverName, enabled) {
5535
+ if (!mcpClient)
5536
+ throw new Error("No MCP servers are configured for this query.");
5537
+ await mcpClient.toggleServer(serverName, enabled);
5538
+ syncMcpTools();
5539
+ },
5540
+ async setMcpServers(servers) {
5541
+ if (!mcpClient)
5542
+ throw new Error("No MCP client is available for this query.");
5543
+ const result = await mcpClient.setServers(servers);
5544
+ syncMcpTools();
5545
+ return result;
5546
+ },
5547
+ async streamInput() {
5548
+ throw new Error("Query.streamInput is not implemented for single-prompt query mode.");
5549
+ }
5550
+ };
5551
+ return createQuery(generator, abortController, controls);
4044
5552
  }
4045
5553
 
4046
5554
  // src/mcp/server.ts
@@ -4068,25 +5576,37 @@ function createMcpServer(opts) {
4068
5576
  };
4069
5577
  }
4070
5578
  export {
5579
+ stripFrontmatter,
4071
5580
  registerProvider,
4072
5581
  query,
5582
+ parseFrontmatter,
4073
5583
  tool as mcpTool,
5584
+ loadSkillsFromDir,
5585
+ loadSkills,
4074
5586
  getProvider,
5587
+ formatSkillsForPrompt,
4075
5588
  createNativeMemoryTool,
4076
5589
  createMemoryTool,
4077
5590
  createMemoryHandler,
4078
5591
  createMcpServer,
4079
5592
  WriteTool,
5593
+ WebSearchTool,
5594
+ WebFetchTool,
4080
5595
  ToolRegistry,
5596
+ TodoWriteTool,
4081
5597
  TaskManager,
4082
5598
  SettingsManager,
4083
5599
  ReadTool,
4084
5600
  PRESETS,
5601
+ NotebookEditTool,
4085
5602
  McpClientManager,
4086
5603
  HookManager,
4087
5604
  HOOK_EVENTS,
4088
5605
  GrepTool,
4089
5606
  GlobTool,
5607
+ ExitPlanModeTool,
4090
5608
  EditTool,
4091
- BashTool
5609
+ ConfigTool,
5610
+ BashTool,
5611
+ AskUserQuestionTool
4092
5612
  };