fourmis-agents-sdk 0.3.1 → 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 (98) 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 +2140 -923
  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 +8 -1
  20. package/dist/index.d.ts.map +1 -1
  21. package/dist/index.js +2148 -924
  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.js +15 -0
  45. package/dist/skills/index.js +38 -1
  46. package/dist/skills/skills.d.ts +16 -0
  47. package/dist/skills/skills.d.ts.map +1 -1
  48. package/dist/skills/skills.js +38 -1
  49. package/dist/tools/ask-user-question.d.ts +7 -0
  50. package/dist/tools/ask-user-question.d.ts.map +1 -0
  51. package/dist/tools/ask-user-question.js +63 -0
  52. package/dist/tools/bash.d.ts.map +1 -1
  53. package/dist/tools/bash.js +62 -2
  54. package/dist/tools/config.d.ts +7 -0
  55. package/dist/tools/config.d.ts.map +1 -0
  56. package/dist/tools/config.js +129 -0
  57. package/dist/tools/edit.js +15 -0
  58. package/dist/tools/exit-plan-mode.d.ts +7 -0
  59. package/dist/tools/exit-plan-mode.d.ts.map +1 -0
  60. package/dist/tools/exit-plan-mode.js +49 -0
  61. package/dist/tools/glob.js +15 -0
  62. package/dist/tools/grep.js +15 -0
  63. package/dist/tools/index.d.ts +7 -0
  64. package/dist/tools/index.d.ts.map +1 -1
  65. package/dist/tools/index.js +521 -9
  66. package/dist/tools/mcp-resources.js +15 -0
  67. package/dist/tools/notebook-edit.d.ts +7 -0
  68. package/dist/tools/notebook-edit.d.ts.map +1 -0
  69. package/dist/tools/notebook-edit.js +98 -0
  70. package/dist/tools/presets.d.ts +2 -1
  71. package/dist/tools/presets.d.ts.map +1 -1
  72. package/dist/tools/presets.js +37 -4
  73. package/dist/tools/read.d.ts.map +1 -1
  74. package/dist/tools/read.js +27 -1
  75. package/dist/tools/registry.d.ts +2 -0
  76. package/dist/tools/registry.d.ts.map +1 -1
  77. package/dist/tools/registry.js +25 -0
  78. package/dist/tools/todo-write.d.ts +7 -0
  79. package/dist/tools/todo-write.d.ts.map +1 -0
  80. package/dist/tools/todo-write.js +84 -0
  81. package/dist/tools/web-fetch.d.ts +6 -0
  82. package/dist/tools/web-fetch.d.ts.map +1 -0
  83. package/dist/tools/web-fetch.js +100 -0
  84. package/dist/tools/web-search.d.ts +7 -0
  85. package/dist/tools/web-search.d.ts.map +1 -0
  86. package/dist/tools/web-search.js +93 -0
  87. package/dist/tools/write.js +15 -0
  88. package/dist/types.d.ts +344 -42
  89. package/dist/types.d.ts.map +1 -1
  90. package/dist/types.js +15 -0
  91. package/dist/utils/cost.js +15 -0
  92. package/dist/utils/session-store.d.ts +1 -1
  93. package/dist/utils/session-store.d.ts.map +1 -1
  94. package/dist/utils/session-store.js +64 -2
  95. package/dist/utils/system-prompt.d.ts +2 -0
  96. package/dist/utils/system-prompt.d.ts.map +1 -1
  97. package/dist/utils/system-prompt.js +48 -4
  98. package/package.json +3 -2
package/dist/api.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;
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
+ };
531
+ }
532
+ return { behavior: "allow" };
553
533
  }
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
- }
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
+ };
591
540
  }
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;
541
+ return { behavior: "allow" };
596
542
  }
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
- };
543
+ if (matchesRule(this.allowRules, toolName, input)) {
544
+ return { behavior: "allow" };
610
545
  }
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 });
546
+ if (SAFE_TOOLS.has(toolName)) {
547
+ return { behavior: "allow" };
620
548
  }
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
- });
549
+ if (this.mode === "acceptEdits") {
550
+ if (EDIT_TOOLS.has(toolName)) {
551
+ return { behavior: "allow" };
552
+ }
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
+ }
558
+ }
628
559
  }
629
- messages.push({ role: "assistant", content: assistantContent });
630
- if (sessionLogger) {
631
- sessionLogger("assistant", assistantContent, null);
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;
632
570
  }
633
- if (assistantText) {
634
- yield { type: "text", text: assistantText, uuid: uuid() };
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.`
575
+ };
635
576
  }
636
- if (toolCalls.length === 0) {
637
- if (hooks) {
638
- await hooks.fire("Stop", { event: "Stop", session_id: sessionId, text: assistantText || undefined }, undefined, { signal });
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);
639
583
  }
640
- if (hooks) {
641
- await hooks.fire("SessionEnd", { event: "SessionEnd", session_id: sessionId }, undefined, { signal });
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
+ }
592
+ }
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
+ }
601
+ }
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;
642
613
  }
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()
655
- };
656
- return;
657
614
  }
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;
667
- }
668
- if (hookResult.updatedInput !== undefined) {
669
- hookUpdatedInput = hookResult.updatedInput;
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)
641
+ continue;
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));
670
647
  }
671
648
  }
672
649
  }
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 });
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
+ }
691
655
  }
692
- continue;
693
656
  }
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 });
714
- }
715
- continue;
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;
716
683
  }
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 };
733
- }
734
- } else {
735
- const toolCtx = {
736
- cwd,
737
- signal,
738
- sessionId,
739
- env
740
- };
741
- result = await tools.execute(call.name, toolInput, toolCtx);
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;
742
691
  }
743
- if (debug) {
744
- console.error(`[debug] Tool ${call.name}: ${result.isError ? "ERROR" : "OK"} (${result.content.length} chars)`);
692
+ case "replaceRules": {
693
+ const key = update.behavior;
694
+ perms[key] = update.rules.map((r) => this.serializeRule(r));
695
+ break;
745
696
  }
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
- }
697
+ case "setMode": {
698
+ perms.defaultMode = update.mode;
699
+ break;
756
700
  }
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
- });
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(`
@@ -2593,327 +2697,465 @@ async function runJsGrep(opts, searchPath, mode, ctx) {
2593
2697
  totalCount += matchedLines.length;
2594
2698
  const relativePath = filePath.startsWith(ctx.cwd) ? filePath.slice(ctx.cwd.length + 1) : filePath;
2595
2699
  if (mode === "files_with_matches") {
2596
- results.push(relativePath);
2597
- } else if (mode === "count") {
2598
- results.push(`${relativePath}:${matchedLines.length}`);
2599
- } else {
2600
- for (const { num, line } of matchedLines) {
2601
- results.push(`${relativePath}:${num}:${line}`);
2602
- }
2603
- }
2604
- } catch {}
2605
- if (opts.head_limit && results.length >= opts.head_limit) {
2606
- break;
2607
- }
2608
- }
2609
- let output = results.join(`
2610
- `);
2611
- if (opts.head_limit) {
2612
- const entries = output.split(`
2613
- `).slice(0, opts.head_limit);
2614
- output = entries.join(`
2615
- `);
2616
- }
2617
- return { content: output || "No matches found." };
2618
- }
2619
- async function collectFiles(dir, globPattern) {
2620
- const { Glob: Glob2 } = await Promise.resolve(globalThis.Bun);
2621
- const pattern = globPattern ?? "**/*";
2622
- const glob = new Glob2(pattern);
2623
- const files = [];
2624
- for await (const path of glob.scan({ cwd: dir, dot: false, onlyFiles: true })) {
2625
- files.push(`${dir}/${path}`);
2626
- }
2627
- return files;
2628
- }
2629
- // src/tools/index.ts
2630
- var ALL_TOOLS = {
2631
- Bash: BashTool,
2632
- Read: ReadTool,
2633
- Write: WriteTool,
2634
- Edit: EditTool,
2635
- Glob: GlobTool,
2636
- Grep: GrepTool
2637
- };
2638
- function buildToolRegistry(toolNames, allowedTools, disallowedTools) {
2639
- const registry = new ToolRegistry;
2640
- for (const name of toolNames) {
2641
- if (disallowedTools?.includes(name))
2642
- continue;
2643
- const tool = ALL_TOOLS[name];
2644
- if (tool) {
2645
- registry.register(tool);
2646
- }
2647
- }
2648
- return registry;
2649
- }
2650
-
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);
2660
- }
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;
2670
- }
2671
- return false;
2672
- }
2673
-
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;
2686
- }
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" };
2746
- }
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;
2700
+ results.push(relativePath);
2701
+ } else if (mode === "count") {
2702
+ results.push(`${relativePath}:${matchedLines.length}`);
2703
+ } else {
2704
+ for (const { num, line } of matchedLines) {
2705
+ results.push(`${relativePath}:${num}:${line}`);
2706
+ }
2781
2707
  }
2708
+ } catch {}
2709
+ if (opts.head_limit && results.length >= opts.head_limit) {
2710
+ break;
2782
2711
  }
2783
2712
  }
2784
- setMode(mode) {
2785
- this.mode = mode;
2713
+ let output = results.join(`
2714
+ `);
2715
+ if (opts.head_limit) {
2716
+ const entries = output.split(`
2717
+ `).slice(0, opts.head_limit);
2718
+ output = entries.join(`
2719
+ `);
2786
2720
  }
2787
- getMode() {
2788
- return this.mode;
2721
+ return { content: output || "No matches found." };
2722
+ }
2723
+ async function collectFiles(dir, globPattern) {
2724
+ const { Glob: Glob2 } = await Promise.resolve(globalThis.Bun);
2725
+ const pattern = globPattern ?? "**/*";
2726
+ const glob = new Glob2(pattern);
2727
+ const files = [];
2728
+ for await (const path of glob.scan({ cwd: dir, dot: false, onlyFiles: true })) {
2729
+ files.push(`${dir}/${path}`);
2789
2730
  }
2731
+ return files;
2790
2732
  }
2791
2733
 
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;
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
+ }
2801
2798
  }
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)
2809
- 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
- }
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)."
2817
2825
  }
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
- }
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"
2823
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)`;
2824
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);
2825
2866
  }
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
2867
  }
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;
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)."
2851
2888
  }
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;
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
+ };
2859
2910
  }
2860
- case "replaceRules": {
2861
- const key = update.behavior;
2862
- perms[key] = update.rules.map((r) => this.serializeRule(r));
2863
- break;
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." };
2864
2915
  }
2865
- case "setMode": {
2866
- perms.defaultMode = update.mode;
2867
- break;
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}`);
2868
2922
  }
2869
- default:
2870
- return;
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 };
2871
2928
  }
2872
- const dir = dirname2(path);
2873
- if (!existsSync3(dir))
2874
- mkdirSync2(dir, { recursive: true });
2875
- writeFileSync3(path, JSON.stringify(data, null, 2) + `
2876
- `);
2877
2929
  }
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");
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 };
2886
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
+ };
2887
2961
  }
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;
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 };
2898
3013
  }
2899
3014
  }
2900
- readJson(path) {
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 = {};
2901
3086
  try {
2902
- const content = readFileSync3(path, "utf-8");
2903
- return JSON.parse(content);
3087
+ const raw = await readFile2(filePath, "utf-8");
3088
+ data = JSON.parse(raw);
2904
3089
  } catch {
2905
- return null;
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
+ };
3132
+ // src/tools/index.ts
3133
+ var ALL_TOOLS = {
3134
+ Bash: BashTool,
3135
+ Read: ReadTool,
3136
+ Write: WriteTool,
3137
+ Edit: EditTool,
3138
+ Glob: GlobTool,
3139
+ Grep: GrepTool,
3140
+ NotebookEdit: NotebookEditTool,
3141
+ WebFetch: WebFetchTool,
3142
+ WebSearch: WebSearchTool,
3143
+ AskUserQuestion: AskUserQuestionTool,
3144
+ TodoWrite: TodoWriteTool,
3145
+ Config: ConfigTool,
3146
+ ExitPlanMode: ExitPlanModeTool
3147
+ };
3148
+ function buildToolRegistry(toolNames, allowedTools, disallowedTools) {
3149
+ const registry = new ToolRegistry;
3150
+ for (const name of toolNames) {
3151
+ if (disallowedTools?.includes(name))
3152
+ continue;
3153
+ const tool = ALL_TOOLS[name];
3154
+ if (tool) {
3155
+ registry.register(tool);
2906
3156
  }
2907
3157
  }
2908
- parseRule(s) {
2909
- const match = s.match(/^([^(]+)\((.+)\)$/);
2910
- if (match)
2911
- return { toolName: match[1], ruleContent: match[2] };
2912
- return { toolName: s };
2913
- }
2914
- serializeRule(rule) {
2915
- return rule.ruleContent ? `${rule.toolName}(${rule.ruleContent})` : rule.toolName;
2916
- }
3158
+ return registry;
2917
3159
  }
2918
3160
 
2919
3161
  // src/skills/frontmatter.ts
@@ -2953,9 +3195,10 @@ function stripFrontmatter(content) {
2953
3195
  // src/skills/skills.ts
2954
3196
  import { existsSync as existsSync4, readdirSync, readFileSync as readFileSync4, realpathSync, statSync } from "fs";
2955
3197
  import { homedir as homedir4 } from "os";
2956
- import { basename, dirname as dirname3, isAbsolute, join as join4, resolve } from "path";
3198
+ import { basename, dirname as dirname5, isAbsolute, join as join6, resolve } from "path";
2957
3199
  var MAX_NAME_LENGTH = 64;
2958
3200
  var MAX_DESCRIPTION_LENGTH = 1024;
3201
+ var MAX_COMPATIBILITY_LENGTH = 500;
2959
3202
  var CONFIG_DIR_NAME = ".claude";
2960
3203
  function shouldIgnore(name) {
2961
3204
  return name.startsWith(".") || name === "node_modules";
@@ -2988,6 +3231,14 @@ function validateDescription(description) {
2988
3231
  }
2989
3232
  return errors;
2990
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})`];
3239
+ }
3240
+ return [];
3241
+ }
2991
3242
  function loadSkillsFromDir(options) {
2992
3243
  return loadSkillsFromDirInternal(options.dir, options.source, true);
2993
3244
  }
@@ -3003,7 +3254,7 @@ function loadSkillsFromDirInternal(dir, source, includeRootFiles) {
3003
3254
  if (shouldIgnore(entry.name)) {
3004
3255
  continue;
3005
3256
  }
3006
- const fullPath = join4(dir, entry.name);
3257
+ const fullPath = join6(dir, entry.name);
3007
3258
  let isDirectory = entry.isDirectory();
3008
3259
  let isFile = entry.isFile();
3009
3260
  if (entry.isSymbolicLink()) {
@@ -3041,7 +3292,7 @@ function loadSkillFromFile(filePath, source) {
3041
3292
  try {
3042
3293
  const rawContent = readFileSync4(filePath, "utf-8");
3043
3294
  const { frontmatter } = parseFrontmatter(rawContent);
3044
- const skillDir = dirname3(filePath);
3295
+ const skillDir = dirname5(filePath);
3045
3296
  const parentDirName = basename(skillDir);
3046
3297
  const descErrors = validateDescription(frontmatter.description);
3047
3298
  for (const error of descErrors) {
@@ -3052,9 +3303,15 @@ function loadSkillFromFile(filePath, source) {
3052
3303
  for (const error of nameErrors) {
3053
3304
  diagnostics.push({ type: "warning", message: error, path: filePath });
3054
3305
  }
3306
+ const compatErrors = validateCompatibility(frontmatter.compatibility);
3307
+ for (const error of compatErrors) {
3308
+ diagnostics.push({ type: "warning", message: error, path: filePath });
3309
+ }
3055
3310
  if (!frontmatter.description || frontmatter.description.trim() === "") {
3056
3311
  return { skill: null, diagnostics };
3057
3312
  }
3313
+ const allowedToolsRaw = frontmatter["allowed-tools"];
3314
+ const allowedTools = allowedToolsRaw ? allowedToolsRaw.split(/\s+/).filter(Boolean) : undefined;
3058
3315
  return {
3059
3316
  skill: {
3060
3317
  name,
@@ -3062,7 +3319,11 @@ function loadSkillFromFile(filePath, source) {
3062
3319
  filePath,
3063
3320
  baseDir: skillDir,
3064
3321
  source,
3065
- disableModelInvocation: frontmatter["disable-model-invocation"] === true
3322
+ disableModelInvocation: frontmatter["disable-model-invocation"] === true,
3323
+ license: frontmatter.license,
3324
+ compatibility: frontmatter.compatibility,
3325
+ metadata: frontmatter.metadata,
3326
+ allowedTools
3066
3327
  },
3067
3328
  diagnostics
3068
3329
  };
@@ -3077,9 +3338,9 @@ function normalizePath(input) {
3077
3338
  if (trimmed === "~")
3078
3339
  return homedir4();
3079
3340
  if (trimmed.startsWith("~/"))
3080
- return join4(homedir4(), trimmed.slice(2));
3341
+ return join6(homedir4(), trimmed.slice(2));
3081
3342
  if (trimmed.startsWith("~"))
3082
- return join4(homedir4(), trimmed.slice(1));
3343
+ return join6(homedir4(), trimmed.slice(1));
3083
3344
  return trimmed;
3084
3345
  }
3085
3346
  function resolveSkillPath(p, cwd) {
@@ -3123,7 +3384,7 @@ function loadSkills(options = {}) {
3123
3384
  }
3124
3385
  }
3125
3386
  if (includeDefaults) {
3126
- const userSkillsDir = join4(homedir4(), CONFIG_DIR_NAME, "skills");
3387
+ const userSkillsDir = join6(homedir4(), CONFIG_DIR_NAME, "skills");
3127
3388
  const projectSkillsDir = resolve(cwd, CONFIG_DIR_NAME, "skills");
3128
3389
  addSkills(loadSkillsFromDirInternal(userSkillsDir, "user", true));
3129
3390
  addSkills(loadSkillsFromDirInternal(projectSkillsDir, "project", true));
@@ -3178,6 +3439,9 @@ function formatSkillsForPrompt(skills) {
3178
3439
  lines.push(` <name>${escapeXml(skill.name)}</name>`);
3179
3440
  lines.push(` <description>${escapeXml(skill.description)}</description>`);
3180
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
+ }
3181
3445
  lines.push(" </skill>");
3182
3446
  }
3183
3447
  lines.push("</available_skills>");
@@ -3186,7 +3450,7 @@ function formatSkillsForPrompt(skills) {
3186
3450
  }
3187
3451
  // src/utils/system-prompt.ts
3188
3452
  import { readFileSync as readFileSync5 } from "fs";
3189
- import { join as join5 } from "path";
3453
+ import { join as join7 } from "path";
3190
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.
3191
3455
 
3192
3456
  You are highly capable and can help users complete complex tasks that would otherwise be too difficult or time-consuming.`;
@@ -3250,41 +3514,546 @@ function buildSystemPrompt(context) {
3250
3514
  sections.push(`# Environment
3251
3515
 
3252
3516
  Working directory: ${context.cwd}`);
3253
- const instructions = readProjectInstructions(context.cwd);
3254
- if (instructions) {
3255
- sections.push(`# Project Instructions
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
3256
3526
 
3257
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
+ });
3258
4033
  }
3259
- }
3260
- if (context.skills && context.skills.length > 0) {
3261
- const skillsPrompt = formatSkillsForPrompt(context.skills);
3262
- if (skillsPrompt) {
3263
- sections.push(`# Skills
3264
-
3265
- ${skillsPrompt}`);
4034
+ messages.push({ role: "user", content: toolResults });
4035
+ if (sessionLogger) {
4036
+ sessionLogger("user", toolResults, null);
3266
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
+ };
3267
4049
  }
3268
- if (context.customPrompt) {
3269
- sections.push(context.customPrompt);
3270
- }
3271
- return sections.join(`
3272
-
3273
- `);
3274
- }
3275
- function readProjectInstructions(cwd) {
3276
- for (const name of ["CLAUDE.md", "AGENTS.md"]) {
3277
- try {
3278
- const content = readFileSync5(join5(cwd, name), "utf-8").trim();
3279
- if (content)
3280
- return content;
3281
- } catch {}
3282
- }
3283
- return null;
3284
4050
  }
3285
4051
 
3286
4052
  // src/query.ts
3287
- 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) {
3288
4057
  const query = {
3289
4058
  next: generator.next.bind(generator),
3290
4059
  return: generator.return.bind(generator),
@@ -3295,6 +4064,71 @@ function createQuery(generator, abortController) {
3295
4064
  async interrupt() {
3296
4065
  abortController.abort();
3297
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
+ },
3298
4132
  close() {
3299
4133
  abortController.abort();
3300
4134
  generator.return(undefined);
@@ -3316,7 +4150,10 @@ var HOOK_EVENTS = [
3316
4150
  "SubagentStart",
3317
4151
  "SubagentStop",
3318
4152
  "PreCompact",
3319
- "PermissionRequest"
4153
+ "PermissionRequest",
4154
+ "Setup",
4155
+ "TeammateIdle",
4156
+ "TaskCompleted"
3320
4157
  ];
3321
4158
  var TOOL_EVENTS = new Set(["PreToolUse", "PostToolUse", "PostToolUseFailure"]);
3322
4159
 
@@ -3348,9 +4185,31 @@ class HookManager {
3348
4185
  }
3349
4186
  }
3350
4187
  for (const callback of entry.hooks) {
3351
- 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;
3352
4196
  if (result) {
3353
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
+ }
3354
4213
  if (result.permissionDecision) {
3355
4214
  if (!merged.permissionDecision || result.permissionDecision === "deny") {
3356
4215
  merged.permissionDecision = result.permissionDecision;
@@ -3380,14 +4239,29 @@ import { InMemoryTransport } from "@modelcontextprotocol/sdk/inMemory.js";
3380
4239
  class McpClientManager {
3381
4240
  configs;
3382
4241
  servers = new Map;
4242
+ disabled = new Set;
3383
4243
  constructor(configs) {
3384
- this.configs = configs;
4244
+ this.configs = { ...configs };
3385
4245
  }
3386
4246
  async connectAll() {
3387
4247
  const entries = Object.entries(this.configs);
3388
4248
  await Promise.all(entries.map(([name, config]) => this.connectOne(name, config)));
3389
4249
  }
3390
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
+ }
3391
4265
  try {
3392
4266
  const client = new Client({ name: `fourmis-${name}`, version: "1.0.0" });
3393
4267
  if (config.type === "sdk") {
@@ -3421,13 +4295,25 @@ class McpClientManager {
3421
4295
  const tools = (toolsResult.tools ?? []).map((t) => ({
3422
4296
  name: t.name,
3423
4297
  description: t.description,
3424
- 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
3425
4304
  }));
3426
4305
  this.servers.set(name, {
3427
4306
  name,
3428
4307
  client,
3429
4308
  tools,
3430
- 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
+ }
3431
4317
  });
3432
4318
  } catch (err) {
3433
4319
  const error = err instanceof Error ? err.message : String(err);
@@ -3435,10 +4321,22 @@ class McpClientManager {
3435
4321
  name,
3436
4322
  client: null,
3437
4323
  tools: [],
3438
- 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
+ }
3439
4331
  });
3440
4332
  }
3441
4333
  }
4334
+ toStatusConfig(config) {
4335
+ if (config.type === "sdk") {
4336
+ return { type: "sdk", name: config.name };
4337
+ }
4338
+ return config;
4339
+ }
3442
4340
  getTools() {
3443
4341
  const result = [];
3444
4342
  for (const [serverName, server] of this.servers) {
@@ -3478,64 +4376,294 @@ class McpClientManager {
3478
4376
  return { content: `MCP tool error: ${message}`, isError: true };
3479
4377
  }
3480
4378
  }
3481
- async listResources(serverName) {
3482
- const result = [];
3483
- const serversToQuery = serverName ? [this.servers.get(serverName)].filter(Boolean) : [...this.servers.values()];
3484
- for (const server of serversToQuery) {
3485
- 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")
3486
4611
  continue;
3487
- try {
3488
- const resources = await server.client.listResources();
3489
- for (const r of resources.resources ?? []) {
3490
- result.push({
3491
- uri: r.uri,
3492
- name: r.name,
3493
- description: r.description,
3494
- mimeType: r.mimeType,
3495
- 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)}`
3496
4654
  });
3497
4655
  }
3498
- } catch {}
3499
- }
3500
- return result;
3501
- }
3502
- async readResource(serverName, uri) {
3503
- const server = this.servers.get(serverName);
3504
- if (!server || server.status.status !== "connected") {
3505
- throw new Error(`MCP server "${serverName}" is not connected`);
3506
- }
3507
- const result = await server.client.readResource({ uri });
3508
- const contents = result.contents ?? [];
3509
- return contents.map((c) => {
3510
- if ("text" in c)
3511
- return c.text;
3512
- if ("blob" in c)
3513
- return `[binary data: ${c.mimeType ?? "unknown"}]`;
3514
- return "";
3515
- }).join("");
3516
- }
3517
- status() {
3518
- const result = [];
3519
- for (const [name] of Object.entries(this.configs)) {
3520
- const server = this.servers.get(name);
3521
- if (server) {
3522
- result.push(server.status);
4656
+ content = normalizedBlocks;
3523
4657
  } else {
3524
- result.push({ name, status: "pending" });
4658
+ continue;
3525
4659
  }
3526
- }
3527
- return result;
3528
- }
3529
- async closeAll() {
3530
- for (const [, server] of this.servers) {
3531
- if (server.client) {
3532
- try {
3533
- await server.client.close();
3534
- } catch {}
4660
+ messages.push({ role, content });
4661
+ if (resumeSessionAt && entry.uuid === resumeSessionAt) {
4662
+ reachedResumePoint = true;
3535
4663
  }
3536
- }
3537
- this.servers.clear();
4664
+ } catch {}
3538
4665
  }
4666
+ return messages;
3539
4667
  }
3540
4668
 
3541
4669
  // src/agents/tools.ts
@@ -3558,6 +4686,15 @@ function createTaskTool(ctx) {
3558
4686
  type: "string",
3559
4687
  description: "The type of agent to use. Must match a registered agent definition."
3560
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
+ },
3561
4698
  run_in_background: {
3562
4699
  type: "boolean",
3563
4700
  description: "If true, run the task in the background and return a task ID."
@@ -3565,16 +4702,35 @@ function createTaskTool(ctx) {
3565
4702
  max_turns: {
3566
4703
  type: "number",
3567
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."
3568
4718
  }
3569
4719
  },
3570
- required: ["prompt", "subagent_type"]
4720
+ required: ["description", "prompt", "subagent_type"]
3571
4721
  },
3572
4722
  async execute(input, toolCtx) {
3573
4723
  const {
4724
+ description,
3574
4725
  prompt,
3575
4726
  subagent_type,
4727
+ model: requestedModel,
4728
+ resume,
3576
4729
  run_in_background,
3577
- max_turns
4730
+ max_turns,
4731
+ name,
4732
+ team_name,
4733
+ mode
3578
4734
  } = input;
3579
4735
  const agentDef = ctx.agents[subagent_type];
3580
4736
  if (!agentDef) {
@@ -3588,28 +4744,53 @@ function createTaskTool(ctx) {
3588
4744
  await ctx.parentHooks.fire("SubagentStart", { event: "SubagentStart", agent_type: subagent_type, session_id: toolCtx.sessionId }, undefined, { signal: toolCtx.signal });
3589
4745
  }
3590
4746
  const provider = agentDef.provider ? getProvider(agentDef.provider) : ctx.parentProvider;
3591
- const model = agentDef.model ?? ctx.parentModel;
3592
- let subTools;
3593
- if (agentDef.tools) {
3594
- subTools = buildToolRegistry(agentDef.tools);
3595
- } else {
3596
- subTools = buildToolRegistry(resolveToolNames("coding"));
3597
- }
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);
3598
4755
  const maxTurns = max_turns ?? agentDef.maxTurns ?? 10;
3599
- const sessionId = uuid();
4756
+ const sessionId = resume ?? uuid();
4757
+ const previousMessages = resume ? loadSessionMessages(ctx.parentCwd, resume) : undefined;
3600
4758
  const abortController = new AbortController;
3601
4759
  if (toolCtx.signal) {
3602
4760
  toolCtx.signal.addEventListener("abort", () => abortController.abort(), { once: true });
3603
4761
  }
3604
- 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(`
3605
4785
 
3606
- You are a subagent of type "${subagent_type}". ${agentDef.description}`;
4786
+ `);
3607
4787
  const runAgent = async () => {
3608
4788
  const messages = [];
3609
4789
  let resultText = "";
3610
4790
  for await (const msg of agentLoop(prompt, {
3611
4791
  provider,
3612
4792
  model,
4793
+ modelState: { current: model },
3613
4794
  systemPrompt,
3614
4795
  tools: subTools,
3615
4796
  permissions: ctx.parentPermissions,
@@ -3617,18 +4798,23 @@ You are a subagent of type "${subagent_type}". ${agentDef.description}`;
3617
4798
  sessionId,
3618
4799
  maxTurns,
3619
4800
  maxBudgetUsd: 5,
3620
- includeStreamEvents: false,
4801
+ includePartialMessages: false,
3621
4802
  signal: abortController.signal,
3622
4803
  env: ctx.parentEnv,
3623
4804
  debug: ctx.parentDebug,
3624
- hooks: ctx.parentHooks
4805
+ hooks: ctx.parentHooks,
4806
+ previousMessages
3625
4807
  })) {
3626
4808
  messages.push(msg);
3627
- if (msg.type === "text") {
3628
- 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
+ }
3629
4815
  }
3630
4816
  if (msg.type === "result" && msg.subtype === "success") {
3631
- resultText = msg.text ?? resultText;
4817
+ resultText = msg.result || resultText;
3632
4818
  }
3633
4819
  }
3634
4820
  return resultText || "Subagent completed with no text output.";
@@ -3787,112 +4973,9 @@ class TaskManager {
3787
4973
  }
3788
4974
  }
3789
4975
 
3790
- // src/utils/session-store.ts
3791
- import { readFileSync as readFileSync6, appendFileSync, mkdirSync as mkdirSync3, readdirSync as readdirSync2, statSync as statSync2 } from "fs";
3792
- import { join as join6 } from "path";
3793
- import { homedir as homedir5 } from "os";
3794
- function sanitizeCwd(cwd) {
3795
- return cwd.replace(/[/.]/g, "-");
3796
- }
3797
- function sessionsDir(cwd) {
3798
- return join6(homedir5(), ".claude", "projects", sanitizeCwd(cwd));
3799
- }
3800
- function ensureDir(dir) {
3801
- mkdirSync3(dir, { recursive: true });
3802
- }
3803
- function logMessage(dir, sessionId, entry) {
3804
- ensureDir(dir);
3805
- const filePath = join6(dir, `${sessionId}.jsonl`);
3806
- appendFileSync(filePath, JSON.stringify(entry) + `
3807
- `);
3808
- }
3809
- function createSessionLogger(cwd, sessionId, model) {
3810
- const dir = sessionsDir(cwd);
3811
- let lastUuid = null;
3812
- return (role, content, parentUuid) => {
3813
- const entryUuid = uuid();
3814
- let normalizedContent = content;
3815
- if (role === "user" && typeof content === "string") {
3816
- normalizedContent = [{ type: "text", text: content }];
3817
- }
3818
- const entry = {
3819
- type: role,
3820
- uuid: entryUuid,
3821
- parentUuid: parentUuid ?? lastUuid,
3822
- sessionId,
3823
- timestamp: new Date().toISOString(),
3824
- cwd,
3825
- isSidechain: false,
3826
- userType: "external",
3827
- message: {
3828
- role,
3829
- content: normalizedContent,
3830
- ...role === "assistant" && model ? { model } : {}
3831
- },
3832
- ...role === "user" ? { permissionMode: "default" } : {}
3833
- };
3834
- logMessage(dir, sessionId, entry);
3835
- lastUuid = entryUuid;
3836
- return entryUuid;
3837
- };
3838
- }
3839
- function findLatestSession(cwd) {
3840
- const dir = sessionsDir(cwd);
3841
- try {
3842
- const files = readdirSync2(dir).filter((f) => f.endsWith(".jsonl")).map((f) => {
3843
- const filePath = join6(dir, f);
3844
- try {
3845
- return { name: f, mtime: statSync2(filePath).mtimeMs };
3846
- } catch {
3847
- return null;
3848
- }
3849
- }).filter((f) => f !== null).sort((a, b) => b.mtime - a.mtime);
3850
- if (files.length === 0)
3851
- return null;
3852
- return files[0].name.replace(/\.jsonl$/, "");
3853
- } catch {
3854
- return null;
3855
- }
3856
- }
3857
- function loadSessionMessages(cwd, sessionId) {
3858
- const dir = sessionsDir(cwd);
3859
- const filePath = join6(dir, `${sessionId}.jsonl`);
3860
- let lines;
3861
- try {
3862
- lines = readFileSync6(filePath, "utf-8").trim().split(`
3863
- `).filter(Boolean);
3864
- } catch {
3865
- return [];
3866
- }
3867
- const messages = [];
3868
- for (const line of lines) {
3869
- try {
3870
- const entry = JSON.parse(line);
3871
- if (entry.type !== "user" && entry.type !== "assistant")
3872
- continue;
3873
- if (entry.isMeta === true)
3874
- continue;
3875
- const message = entry.message;
3876
- if (!message)
3877
- continue;
3878
- const role = entry.type === "user" ? "user" : "assistant";
3879
- let content;
3880
- if (typeof message.content === "string") {
3881
- content = message.content;
3882
- } else if (Array.isArray(message.content)) {
3883
- content = message.content.filter((c) => c.type === "text" || c.type === "tool_use" || c.type === "tool_result").map((c) => c);
3884
- } else {
3885
- continue;
3886
- }
3887
- messages.push({ role, content });
3888
- } catch {}
3889
- }
3890
- return messages;
3891
- }
3892
-
3893
4976
  // src/memory/memory-handler.ts
3894
- import { readdir, stat, readFile, writeFile, rm, rename, mkdir as mkdir2 } from "fs/promises";
3895
- import { join as join7, resolve as resolve2, relative as relative2 } from "path";
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";
3896
4979
  import { existsSync as existsSync5 } from "fs";
3897
4980
  function createMemoryHandler(memoryDir) {
3898
4981
  const absMemoryDir = resolve2(memoryDir);
@@ -3938,7 +5021,7 @@ function createMemoryHandler(memoryDir) {
3938
5021
  for (const entry of entries.sort((a, b) => a.name.localeCompare(b.name))) {
3939
5022
  if (entry.name.startsWith(".") || entry.name === "node_modules")
3940
5023
  continue;
3941
- const entryPath = join7(dirPath, entry.name);
5024
+ const entryPath = join9(dirPath, entry.name);
3942
5025
  const entryStat = await stat(entryPath);
3943
5026
  lines.push(`${formatSize(entryStat.size)} ${toLogicalPath(entryPath)}`);
3944
5027
  if (entry.isDirectory()) {
@@ -3981,7 +5064,7 @@ function createMemoryHandler(memoryDir) {
3981
5064
  ${lines.join(`
3982
5065
  `)}`;
3983
5066
  }
3984
- const content = await readFile(absPath, "utf-8");
5067
+ const content = await readFile3(absPath, "utf-8");
3985
5068
  const formatted = formatFileContent(content, cmd.view_range);
3986
5069
  return `Here's the content of ${cmd.path} with line numbers:
3987
5070
  ${formatted}`;
@@ -3992,8 +5075,8 @@ ${formatted}`;
3992
5075
  return `Error: File ${cmd.path} already exists`;
3993
5076
  }
3994
5077
  const parentDir = resolve2(absPath, "..");
3995
- await mkdir2(parentDir, { recursive: true });
3996
- await writeFile(absPath, cmd.file_text, "utf-8");
5078
+ await mkdir4(parentDir, { recursive: true });
5079
+ await writeFile4(absPath, cmd.file_text, "utf-8");
3997
5080
  return `File created successfully at: ${cmd.path}`;
3998
5081
  }
3999
5082
  async function handleStrReplace(cmd) {
@@ -4005,7 +5088,7 @@ ${formatted}`;
4005
5088
  if (s.isDirectory()) {
4006
5089
  return `Error: The path ${cmd.path} does not exist. Please provide a valid path.`;
4007
5090
  }
4008
- const content = await readFile(absPath, "utf-8");
5091
+ const content = await readFile3(absPath, "utf-8");
4009
5092
  const lines = content.split(`
4010
5093
  `);
4011
5094
  const matchingLines = [];
@@ -4028,7 +5111,7 @@ ${formatted}`;
4028
5111
  return `No replacement was performed. Multiple occurrences of old_str \`${cmd.old_str}\` in lines: ${matchingLines.join(", ")}. Please ensure it is unique`;
4029
5112
  }
4030
5113
  const newContent = content.replace(cmd.old_str, cmd.new_str);
4031
- await writeFile(absPath, newContent, "utf-8");
5114
+ await writeFile4(absPath, newContent, "utf-8");
4032
5115
  const newLines = newContent.split(`
4033
5116
  `);
4034
5117
  const replaceLine = matchingLines[0];
@@ -4048,7 +5131,7 @@ ${snippet}`;
4048
5131
  if (s.isDirectory()) {
4049
5132
  return `Error: The path ${cmd.path} does not exist`;
4050
5133
  }
4051
- const content = await readFile(absPath, "utf-8");
5134
+ const content = await readFile3(absPath, "utf-8");
4052
5135
  const lines = content.split(`
4053
5136
  `);
4054
5137
  if (cmd.insert_line < 0 || cmd.insert_line > lines.length) {
@@ -4057,7 +5140,7 @@ ${snippet}`;
4057
5140
  const insertLines = cmd.insert_text.split(`
4058
5141
  `);
4059
5142
  lines.splice(cmd.insert_line, 0, ...insertLines);
4060
- await writeFile(absPath, lines.join(`
5143
+ await writeFile4(absPath, lines.join(`
4061
5144
  `), "utf-8");
4062
5145
  return `The file ${cmd.path} has been edited.`;
4063
5146
  }
@@ -4079,13 +5162,13 @@ ${snippet}`;
4079
5162
  return `Error: The destination ${cmd.new_path} already exists`;
4080
5163
  }
4081
5164
  const parentDir = resolve2(newAbs, "..");
4082
- await mkdir2(parentDir, { recursive: true });
5165
+ await mkdir4(parentDir, { recursive: true });
4083
5166
  await rename(oldAbs, newAbs);
4084
5167
  return `Successfully renamed ${cmd.old_path} to ${cmd.new_path}`;
4085
5168
  }
4086
5169
  async function execute(cmd) {
4087
5170
  if (!existsSync5(absMemoryDir)) {
4088
- await mkdir2(absMemoryDir, { recursive: true });
5171
+ await mkdir4(absMemoryDir, { recursive: true });
4089
5172
  }
4090
5173
  switch (cmd.command) {
4091
5174
  case "view":
@@ -4190,19 +5273,41 @@ function createMemoryTool(config) {
4190
5273
  }
4191
5274
  };
4192
5275
  }
4193
-
4194
5276
  // src/api.ts
4195
5277
  var DEFAULT_MODEL = "claude-sonnet-4-5-20250929";
4196
5278
  var DEFAULT_MAX_TURNS = 10;
4197
5279
  var DEFAULT_MAX_BUDGET_USD = 5;
4198
5280
  function query(params) {
4199
- 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
+ }
4200
5286
  const providerName = options.provider ?? "anthropic";
4201
5287
  const provider = getProvider(providerName, {
4202
5288
  apiKey: options.apiKey,
4203
5289
  baseUrl: options.baseUrl
4204
5290
  });
4205
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
+ }
4206
5311
  const toolNames = resolveToolNames(options.tools);
4207
5312
  const registry = buildToolRegistry(toolNames, undefined, options.disallowedTools);
4208
5313
  let skills = [];
@@ -4219,12 +5324,22 @@ function query(params) {
4219
5324
  }
4220
5325
  }
4221
5326
  }
4222
- const systemPrompt = options.systemPrompt ?? buildSystemPrompt({
4223
- tools: registry.list(),
4224
- cwd: options.cwd,
4225
- customPrompt: options.appendSystemPrompt,
4226
- skills
4227
- });
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
+ })();
4228
5343
  let settingsManager;
4229
5344
  let mergedPermissions = options.permissions;
4230
5345
  if (options.settingSources && options.settingSources.length > 0) {
@@ -4270,7 +5385,7 @@ function query(params) {
4270
5385
  }
4271
5386
  }
4272
5387
  } else if (options.resume) {
4273
- previousMessages = loadSessionMessages(cwd, options.resume);
5388
+ previousMessages = loadSessionMessages(cwd, options.resume, options.resumeSessionAt);
4274
5389
  if (options.forkSession) {
4275
5390
  sessionId = uuid();
4276
5391
  } else {
@@ -4279,18 +5394,28 @@ function query(params) {
4279
5394
  }
4280
5395
  const persistSession = options.persistSession !== false;
4281
5396
  const sessionLogger = persistSession ? createSessionLogger(cwd, sessionId, model) : undefined;
4282
- const abortController = new AbortController;
5397
+ const abortController = options.abortController ?? new AbortController;
4283
5398
  if (options.signal) {
4284
5399
  options.signal.addEventListener("abort", () => abortController.abort(), { once: true });
4285
5400
  }
4286
5401
  const hookManager = options.hooks ? new HookManager(options.hooks) : undefined;
4287
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
+ };
4288
5413
  if (options.agents && Object.keys(options.agents).length > 0) {
4289
5414
  const taskManager = new TaskManager;
4290
5415
  const agentCtx = {
4291
5416
  agents: options.agents,
4292
5417
  parentProvider: provider,
4293
- parentModel: model,
5418
+ parentModel: modelState.current,
4294
5419
  parentPermissions: permissions,
4295
5420
  parentHooks: hookManager,
4296
5421
  parentCwd: cwd,
@@ -4314,6 +5439,12 @@ function query(params) {
4314
5439
  const generator = agentLoop(prompt, {
4315
5440
  provider,
4316
5441
  model,
5442
+ modelState,
5443
+ maxThinkingTokensState,
5444
+ fallbackModel,
5445
+ thinking: options.thinking,
5446
+ effort: options.effort,
5447
+ outputFormat: options.outputFormat,
4317
5448
  systemPrompt,
4318
5449
  tools: registry,
4319
5450
  permissions,
@@ -4321,7 +5452,7 @@ function query(params) {
4321
5452
  sessionId,
4322
5453
  maxTurns: options.maxTurns ?? DEFAULT_MAX_TURNS,
4323
5454
  maxBudgetUsd: options.maxBudgetUsd ?? DEFAULT_MAX_BUDGET_USD,
4324
- includeStreamEvents: options.includeStreamEvents ?? false,
5455
+ includePartialMessages: options.includePartialMessages ?? false,
4325
5456
  signal: abortController.signal,
4326
5457
  env: options.env,
4327
5458
  debug: options.debug,
@@ -4329,9 +5460,95 @@ function query(params) {
4329
5460
  mcpClient,
4330
5461
  previousMessages,
4331
5462
  sessionLogger,
4332
- 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
+ }
4333
5472
  });
4334
- 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);
4335
5552
  }
4336
5553
  export {
4337
5554
  query