fourmis-agents-sdk 0.3.1 → 0.4.1

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 (84) hide show
  1. package/README.md +126 -198
  2. package/dist/agent-loop.d.ts +21 -3
  3. package/dist/agent-loop.d.ts.map +1 -1
  4. package/dist/agent-loop.js +279 -90
  5. package/dist/agents/index.js +1079 -124
  6. package/dist/agents/tools.d.ts.map +1 -1
  7. package/dist/agents/tools.js +1079 -124
  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 +1663 -430
  13. package/dist/hooks.d.ts +19 -1
  14. package/dist/hooks.d.ts.map +1 -1
  15. package/dist/hooks.js +27 -2
  16. package/dist/index.d.ts +8 -1
  17. package/dist/index.d.ts.map +1 -1
  18. package/dist/index.js +1671 -431
  19. package/dist/mcp/client.d.ts +8 -1
  20. package/dist/mcp/client.d.ts.map +1 -1
  21. package/dist/mcp/client.js +134 -13
  22. package/dist/mcp/index.js +134 -13
  23. package/dist/mcp/types.d.ts +21 -1
  24. package/dist/mcp/types.d.ts.map +1 -1
  25. package/dist/permissions.d.ts.map +1 -1
  26. package/dist/permissions.js +7 -3
  27. package/dist/providers/anthropic.d.ts.map +1 -1
  28. package/dist/providers/anthropic.js +41 -2
  29. package/dist/providers/openai.d.ts +6 -0
  30. package/dist/providers/openai.d.ts.map +1 -1
  31. package/dist/providers/openai.js +36 -6
  32. package/dist/providers/registry.js +76 -8
  33. package/dist/providers/types.d.ts +4 -1
  34. package/dist/providers/types.d.ts.map +1 -1
  35. package/dist/query.d.ts +21 -2
  36. package/dist/query.d.ts.map +1 -1
  37. package/dist/query.js +69 -1
  38. package/dist/skills/index.js +23 -1
  39. package/dist/skills/skills.d.ts +16 -0
  40. package/dist/skills/skills.d.ts.map +1 -1
  41. package/dist/skills/skills.js +23 -1
  42. package/dist/tools/ask-user-question.d.ts +7 -0
  43. package/dist/tools/ask-user-question.d.ts.map +1 -0
  44. package/dist/tools/ask-user-question.js +48 -0
  45. package/dist/tools/bash.d.ts.map +1 -1
  46. package/dist/tools/bash.js +47 -2
  47. package/dist/tools/config.d.ts +7 -0
  48. package/dist/tools/config.d.ts.map +1 -0
  49. package/dist/tools/config.js +114 -0
  50. package/dist/tools/exit-plan-mode.d.ts +7 -0
  51. package/dist/tools/exit-plan-mode.d.ts.map +1 -0
  52. package/dist/tools/exit-plan-mode.js +34 -0
  53. package/dist/tools/index.d.ts +7 -0
  54. package/dist/tools/index.d.ts.map +1 -1
  55. package/dist/tools/index.js +506 -9
  56. package/dist/tools/notebook-edit.d.ts +7 -0
  57. package/dist/tools/notebook-edit.d.ts.map +1 -0
  58. package/dist/tools/notebook-edit.js +83 -0
  59. package/dist/tools/presets.d.ts +2 -1
  60. package/dist/tools/presets.d.ts.map +1 -1
  61. package/dist/tools/presets.js +22 -4
  62. package/dist/tools/read.d.ts.map +1 -1
  63. package/dist/tools/read.js +12 -1
  64. package/dist/tools/registry.d.ts +2 -0
  65. package/dist/tools/registry.d.ts.map +1 -1
  66. package/dist/tools/registry.js +10 -0
  67. package/dist/tools/todo-write.d.ts +7 -0
  68. package/dist/tools/todo-write.d.ts.map +1 -0
  69. package/dist/tools/todo-write.js +69 -0
  70. package/dist/tools/web-fetch.d.ts +6 -0
  71. package/dist/tools/web-fetch.d.ts.map +1 -0
  72. package/dist/tools/web-fetch.js +85 -0
  73. package/dist/tools/web-search.d.ts +7 -0
  74. package/dist/tools/web-search.d.ts.map +1 -0
  75. package/dist/tools/web-search.js +78 -0
  76. package/dist/types.d.ts +344 -42
  77. package/dist/types.d.ts.map +1 -1
  78. package/dist/utils/session-store.d.ts +1 -1
  79. package/dist/utils/session-store.d.ts.map +1 -1
  80. package/dist/utils/session-store.js +49 -2
  81. package/dist/utils/system-prompt.d.ts +2 -0
  82. package/dist/utils/system-prompt.d.ts.map +1 -1
  83. package/dist/utils/system-prompt.js +33 -4
  84. package/package.json +3 -2
package/dist/api.js CHANGED
@@ -483,10 +483,57 @@ function mergeUsage(a, b) {
483
483
  }
484
484
 
485
485
  // src/agent-loop.ts
486
+ function makeModelUsageEntry() {
487
+ return {
488
+ inputTokens: 0,
489
+ outputTokens: 0,
490
+ cacheReadInputTokens: 0,
491
+ cacheCreationInputTokens: 0,
492
+ totalCostUsd: 0
493
+ };
494
+ }
495
+ function makeErrorResult(params) {
496
+ return {
497
+ type: "result",
498
+ subtype: params.subtype,
499
+ duration_ms: Date.now() - params.startTime,
500
+ duration_api_ms: params.apiTimeMs,
501
+ is_error: true,
502
+ num_turns: params.turns,
503
+ stop_reason: null,
504
+ total_cost_usd: params.costUsd,
505
+ usage: params.usage,
506
+ modelUsage: params.modelUsage,
507
+ permission_denials: params.permissionDenials,
508
+ errors: params.errors,
509
+ uuid: uuid(),
510
+ session_id: params.sessionId
511
+ };
512
+ }
513
+ function extractStructuredJson(text) {
514
+ const trimmed = text.trim();
515
+ if (!trimmed) {
516
+ return { ok: false, error: "Empty result text; expected JSON output." };
517
+ }
518
+ const fenced = trimmed.match(/```(?:json)?\s*([\s\S]*?)\s*```/i);
519
+ const candidate = fenced ? fenced[1].trim() : trimmed;
520
+ try {
521
+ return { ok: true, value: JSON.parse(candidate) };
522
+ } catch (err) {
523
+ const message = err instanceof Error ? err.message : String(err);
524
+ return { ok: false, error: `Invalid JSON output: ${message}` };
525
+ }
526
+ }
486
527
  async function* agentLoop(prompt, options) {
487
528
  const {
488
529
  provider,
489
530
  model,
531
+ fallbackModel,
532
+ modelState,
533
+ maxThinkingTokensState,
534
+ thinking,
535
+ effort,
536
+ outputFormat,
490
537
  systemPrompt,
491
538
  tools,
492
539
  permissions,
@@ -494,7 +541,7 @@ async function* agentLoop(prompt, options) {
494
541
  sessionId,
495
542
  maxTurns,
496
543
  maxBudgetUsd,
497
- includeStreamEvents,
544
+ includePartialMessages,
498
545
  signal,
499
546
  env,
500
547
  debug,
@@ -502,14 +549,17 @@ async function* agentLoop(prompt, options) {
502
549
  mcpClient,
503
550
  previousMessages,
504
551
  sessionLogger,
505
- nativeMemoryTool
552
+ nativeMemoryTool,
553
+ initMeta
506
554
  } = options;
555
+ const effectiveModelState = modelState ?? { current: model };
507
556
  const startTime = Date.now();
508
557
  let apiTimeMs = 0;
509
558
  let turns = 0;
510
559
  let totalUsage = emptyTokenUsage();
511
560
  let costUsd = 0;
512
561
  const modelUsage = {};
562
+ const permissionDenials = [];
513
563
  if (mcpClient) {
514
564
  await mcpClient.connectAll();
515
565
  for (const tool of mcpClient.getTools()) {
@@ -527,56 +577,135 @@ async function* agentLoop(prompt, options) {
527
577
  sessionLogger("user", prompt, null);
528
578
  }
529
579
  yield {
530
- type: "init",
531
- sessionId,
532
- model,
533
- provider: provider.name,
580
+ type: "system",
581
+ subtype: "init",
582
+ apiKeySource: "user",
583
+ claude_code_version: "fourmis-agent-sdk",
584
+ session_id: sessionId,
585
+ model: effectiveModelState.current,
534
586
  tools: tools.list(),
535
587
  cwd,
588
+ mcp_servers: (mcpClient?.status() ?? []).map((s) => ({ name: s.name, status: s.status })),
589
+ permissionMode: permissions.getMode(),
590
+ agents: initMeta?.agents,
591
+ betas: initMeta?.betas,
592
+ slash_commands: initMeta?.slashCommands ?? [],
593
+ output_style: initMeta?.outputStyle ?? "default",
594
+ skills: initMeta?.skills ?? [],
595
+ plugins: (initMeta?.plugins ?? []).map((p) => ({ name: p.path.split("/").pop() ?? p.path, path: p.path })),
536
596
  uuid: uuid()
537
597
  };
538
598
  if (hooks) {
539
- await hooks.fire("SessionStart", { event: "SessionStart", session_id: sessionId }, undefined, { signal });
599
+ await hooks.fire("Setup", {
600
+ event: "Setup",
601
+ hook_event_name: "Setup",
602
+ trigger: "init",
603
+ session_id: sessionId,
604
+ cwd,
605
+ permission_mode: permissions.getMode()
606
+ }, undefined, { signal });
607
+ }
608
+ if (hooks) {
609
+ await hooks.fire("SessionStart", {
610
+ event: "SessionStart",
611
+ hook_event_name: "SessionStart",
612
+ session_id: sessionId,
613
+ source: "startup",
614
+ model: effectiveModelState.current,
615
+ cwd,
616
+ permission_mode: permissions.getMode()
617
+ }, undefined, { signal });
540
618
  }
541
619
  while (true) {
542
620
  if (signal.aborted) {
543
- yield makeError("error_execution", ["Aborted"], turns, costUsd, sessionId, startTime);
621
+ yield makeErrorResult({
622
+ subtype: "error_during_execution",
623
+ errors: ["Aborted"],
624
+ turns,
625
+ costUsd,
626
+ sessionId,
627
+ startTime,
628
+ apiTimeMs,
629
+ usage: totalUsage,
630
+ modelUsage,
631
+ permissionDenials
632
+ });
544
633
  return;
545
634
  }
546
635
  if (turns >= maxTurns) {
547
- yield makeError("error_max_turns", [`Reached maximum turns (${maxTurns})`], turns, costUsd, sessionId, startTime);
636
+ yield makeErrorResult({
637
+ subtype: "error_max_turns",
638
+ errors: [`Reached maximum turns (${maxTurns})`],
639
+ turns,
640
+ costUsd,
641
+ sessionId,
642
+ startTime,
643
+ apiTimeMs,
644
+ usage: totalUsage,
645
+ modelUsage,
646
+ permissionDenials
647
+ });
548
648
  return;
549
649
  }
550
650
  if (maxBudgetUsd > 0 && costUsd >= maxBudgetUsd) {
551
- yield makeError("error_max_budget", [`Reached budget limit ($${maxBudgetUsd})`], turns, costUsd, sessionId, startTime);
651
+ yield makeErrorResult({
652
+ subtype: "error_max_budget_usd",
653
+ errors: [],
654
+ turns,
655
+ costUsd,
656
+ sessionId,
657
+ startTime,
658
+ apiTimeMs,
659
+ usage: totalUsage,
660
+ modelUsage,
661
+ permissionDenials
662
+ });
552
663
  return;
553
664
  }
665
+ const activeModel = effectiveModelState.current;
554
666
  const toolDefs = tools.getDefinitions();
555
667
  const apiStart = Date.now();
556
668
  let assistantTextParts = [];
557
- let toolCalls = [];
669
+ const toolCalls = [];
558
670
  let turnUsage = emptyTokenUsage();
671
+ let turnStopReason = null;
559
672
  const nativeTools = nativeMemoryTool ? [nativeMemoryTool.definition] : undefined;
560
673
  try {
561
674
  const chunks = provider.chat({
562
- model,
675
+ model: activeModel,
563
676
  messages,
564
677
  tools: toolDefs.length > 0 ? toolDefs : undefined,
565
678
  systemPrompt,
566
679
  signal,
567
- nativeTools
680
+ nativeTools,
681
+ thinkingBudget: maxThinkingTokensState?.current,
682
+ thinking,
683
+ effort,
684
+ outputFormat
568
685
  });
569
686
  for await (const chunk of chunks) {
570
687
  switch (chunk.type) {
571
688
  case "text_delta":
572
689
  assistantTextParts.push(chunk.text);
573
- if (includeStreamEvents) {
574
- yield { type: "stream", subtype: "text_delta", text: chunk.text, uuid: uuid() };
690
+ if (includePartialMessages) {
691
+ yield {
692
+ type: "stream_event",
693
+ event: { type: "text_delta", text: chunk.text },
694
+ parent_tool_use_id: null,
695
+ uuid: uuid(),
696
+ session_id: sessionId
697
+ };
575
698
  }
576
699
  break;
577
700
  case "thinking_delta":
578
- if (includeStreamEvents) {
579
- yield { type: "stream", subtype: "thinking_delta", text: chunk.text, uuid: uuid() };
701
+ if (includePartialMessages) {
702
+ yield {
703
+ type: "stream_event",
704
+ event: { type: "thinking_delta", thinking: chunk.text },
705
+ parent_tool_use_id: null,
706
+ uuid: uuid(),
707
+ session_id: sessionId
708
+ };
580
709
  }
581
710
  break;
582
711
  case "tool_call":
@@ -586,33 +715,54 @@ async function* agentLoop(prompt, options) {
586
715
  turnUsage = mergeUsage(turnUsage, chunk.usage);
587
716
  break;
588
717
  case "done":
718
+ turnStopReason = chunk.stopReason ?? null;
589
719
  break;
590
720
  }
591
721
  }
592
722
  } catch (err) {
593
723
  const message = err instanceof Error ? err.message : String(err);
594
- yield makeError("error_execution", [`API error: ${message}`], turns, costUsd, sessionId, startTime);
724
+ if (fallbackModel && activeModel !== fallbackModel) {
725
+ effectiveModelState.current = fallbackModel;
726
+ yield {
727
+ type: "system",
728
+ subtype: "status",
729
+ status: null,
730
+ permissionMode: permissions.getMode(),
731
+ uuid: uuid(),
732
+ session_id: sessionId
733
+ };
734
+ continue;
735
+ }
736
+ yield makeErrorResult({
737
+ subtype: "error_during_execution",
738
+ errors: [`API error: ${message}`],
739
+ turns,
740
+ costUsd,
741
+ sessionId,
742
+ startTime,
743
+ apiTimeMs,
744
+ usage: totalUsage,
745
+ modelUsage,
746
+ permissionDenials
747
+ });
595
748
  return;
596
749
  }
597
750
  apiTimeMs += Date.now() - apiStart;
598
751
  turns++;
599
752
  totalUsage = mergeUsage(totalUsage, turnUsage);
600
- const turnCost = provider.calculateCost(model, turnUsage);
753
+ const turnCost = provider.calculateCost(activeModel, turnUsage);
601
754
  costUsd += turnCost;
602
- if (!modelUsage[model]) {
603
- modelUsage[model] = {
604
- inputTokens: 0,
605
- outputTokens: 0,
606
- cacheReadInputTokens: 0,
607
- cacheCreationInputTokens: 0,
608
- totalCostUsd: 0
609
- };
610
- }
611
- modelUsage[model].inputTokens += turnUsage.inputTokens;
612
- modelUsage[model].outputTokens += turnUsage.outputTokens;
613
- modelUsage[model].cacheReadInputTokens += turnUsage.cacheReadInputTokens;
614
- modelUsage[model].cacheCreationInputTokens += turnUsage.cacheCreationInputTokens;
615
- modelUsage[model].totalCostUsd += turnCost;
755
+ if (!modelUsage[activeModel]) {
756
+ modelUsage[activeModel] = makeModelUsageEntry();
757
+ }
758
+ modelUsage[activeModel].inputTokens += turnUsage.inputTokens;
759
+ modelUsage[activeModel].outputTokens += turnUsage.outputTokens;
760
+ modelUsage[activeModel].cacheReadInputTokens += turnUsage.cacheReadInputTokens;
761
+ modelUsage[activeModel].cacheCreationInputTokens += turnUsage.cacheCreationInputTokens;
762
+ modelUsage[activeModel].totalCostUsd += turnCost;
763
+ modelUsage[activeModel].webSearchRequests = (modelUsage[activeModel].webSearchRequests ?? 0) + (turnUsage.webSearchRequests ?? 0);
764
+ modelUsage[activeModel].costUSD = modelUsage[activeModel].totalCostUsd;
765
+ modelUsage[activeModel].contextWindow = provider.getContextWindow(activeModel);
616
766
  const assistantText = assistantTextParts.join("");
617
767
  const assistantContent = [];
618
768
  if (assistantText) {
@@ -630,28 +780,72 @@ async function* agentLoop(prompt, options) {
630
780
  if (sessionLogger) {
631
781
  sessionLogger("assistant", assistantContent, null);
632
782
  }
633
- if (assistantText) {
634
- yield { type: "text", text: assistantText, uuid: uuid() };
635
- }
783
+ yield {
784
+ type: "assistant",
785
+ message: {
786
+ role: "assistant",
787
+ content: assistantContent
788
+ },
789
+ parent_tool_use_id: null,
790
+ uuid: uuid(),
791
+ session_id: sessionId
792
+ };
636
793
  if (toolCalls.length === 0) {
794
+ let structuredOutput;
795
+ if (outputFormat?.type === "json_schema") {
796
+ const parsed = extractStructuredJson(assistantText);
797
+ if (!parsed.ok) {
798
+ yield makeErrorResult({
799
+ subtype: "error_max_structured_output_retries",
800
+ errors: [parsed.error],
801
+ turns,
802
+ costUsd,
803
+ sessionId,
804
+ startTime,
805
+ apiTimeMs,
806
+ usage: totalUsage,
807
+ modelUsage,
808
+ permissionDenials
809
+ });
810
+ return;
811
+ }
812
+ structuredOutput = parsed.value;
813
+ }
637
814
  if (hooks) {
638
- await hooks.fire("Stop", { event: "Stop", session_id: sessionId, text: assistantText || undefined }, undefined, { signal });
815
+ await hooks.fire("Stop", {
816
+ event: "Stop",
817
+ hook_event_name: "Stop",
818
+ session_id: sessionId,
819
+ text: assistantText || undefined,
820
+ stop_reason: turnStopReason ?? undefined
821
+ }, undefined, {
822
+ signal
823
+ });
639
824
  }
640
825
  if (hooks) {
641
- await hooks.fire("SessionEnd", { event: "SessionEnd", session_id: sessionId }, undefined, { signal });
826
+ await hooks.fire("SessionEnd", {
827
+ event: "SessionEnd",
828
+ hook_event_name: "SessionEnd",
829
+ session_id: sessionId,
830
+ reason: "other"
831
+ }, undefined, { signal });
642
832
  }
643
833
  yield {
644
834
  type: "result",
645
835
  subtype: "success",
646
- text: assistantText || null,
647
- turns,
648
- costUsd,
649
- durationMs: Date.now() - startTime,
650
- durationApiMs: apiTimeMs,
651
- sessionId,
836
+ duration_ms: Date.now() - startTime,
837
+ duration_api_ms: apiTimeMs,
838
+ is_error: false,
839
+ num_turns: turns,
840
+ result: assistantText,
841
+ stop_reason: turnStopReason,
842
+ total_cost_usd: costUsd,
652
843
  usage: totalUsage,
653
844
  modelUsage,
654
- uuid: uuid()
845
+ permission_denials: permissionDenials,
846
+ structured_output: structuredOutput,
847
+ uuid: uuid(),
848
+ session_id: sessionId
655
849
  };
656
850
  return;
657
851
  }
@@ -660,7 +854,13 @@ async function* agentLoop(prompt, options) {
660
854
  let hookDenied = false;
661
855
  let hookUpdatedInput;
662
856
  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 });
857
+ const hookResult = await hooks.fire("PreToolUse", {
858
+ event: "PreToolUse",
859
+ hook_event_name: "PreToolUse",
860
+ tool_name: call.name,
861
+ tool_input: call.input,
862
+ session_id: sessionId
863
+ }, call.id, { signal });
664
864
  if (hookResult) {
665
865
  if (hookResult.permissionDecision === "deny") {
666
866
  hookDenied = true;
@@ -672,14 +872,6 @@ async function* agentLoop(prompt, options) {
672
872
  }
673
873
  if (hookDenied) {
674
874
  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
875
  toolResults.push({
684
876
  type: "tool_result",
685
877
  tool_use_id: call.id,
@@ -695,14 +887,11 @@ async function* agentLoop(prompt, options) {
695
887
  const permResult = await permissions.check(call.name, inputAfterHook ?? {}, { signal, toolUseId: call.id });
696
888
  if (permResult.behavior === "deny") {
697
889
  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
- };
890
+ permissionDenials.push({
891
+ tool_name: call.name,
892
+ tool_use_id: call.id,
893
+ tool_input: inputAfterHook ?? {}
894
+ });
706
895
  toolResults.push({
707
896
  type: "tool_result",
708
897
  tool_use_id: call.id,
@@ -715,13 +904,6 @@ async function* agentLoop(prompt, options) {
715
904
  continue;
716
905
  }
717
906
  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
907
  let result;
726
908
  if (call.name === "memory" && nativeMemoryTool) {
727
909
  try {
@@ -740,28 +922,36 @@ async function* agentLoop(prompt, options) {
740
922
  };
741
923
  result = await tools.execute(call.name, toolInput, toolCtx);
742
924
  }
925
+ if (call.name === "ExitPlanMode") {
926
+ permissions.setMode("default");
927
+ }
743
928
  if (debug) {
744
929
  console.error(`[debug] Tool ${call.name}: ${result.isError ? "ERROR" : "OK"} (${result.content.length} chars)`);
745
930
  }
746
931
  if (hooks) {
747
932
  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 });
933
+ await hooks.fire("PostToolUseFailure", {
934
+ event: "PostToolUseFailure",
935
+ hook_event_name: "PostToolUseFailure",
936
+ tool_name: call.name,
937
+ tool_result: result.content,
938
+ tool_error: true,
939
+ session_id: sessionId
940
+ }, call.id, { signal });
749
941
  } else {
750
- const postResult = await hooks.fire("PostToolUse", { event: "PostToolUse", tool_name: call.name, tool_result: result.content, session_id: sessionId }, call.id, { signal });
942
+ const postResult = await hooks.fire("PostToolUse", {
943
+ event: "PostToolUse",
944
+ hook_event_name: "PostToolUse",
945
+ tool_name: call.name,
946
+ tool_result: result.content,
947
+ session_id: sessionId
948
+ }, call.id, { signal });
751
949
  if (postResult?.additionalContext) {
752
950
  result.content += `
753
951
  ${postResult.additionalContext}`;
754
952
  }
755
953
  }
756
954
  }
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
955
  toolResults.push({
766
956
  type: "tool_result",
767
957
  tool_use_id: call.id,
@@ -773,20 +963,19 @@ ${postResult.additionalContext}`;
773
963
  if (sessionLogger) {
774
964
  sessionLogger("user", toolResults, null);
775
965
  }
966
+ yield {
967
+ type: "user",
968
+ message: {
969
+ role: "user",
970
+ content: toolResults
971
+ },
972
+ parent_tool_use_id: null,
973
+ isSynthetic: true,
974
+ uuid: uuid(),
975
+ session_id: sessionId
976
+ };
776
977
  }
777
978
  }
778
- function makeError(subtype, errors, turns, costUsd, sessionId, startTime) {
779
- return {
780
- type: "result",
781
- subtype,
782
- errors,
783
- turns,
784
- costUsd,
785
- durationMs: Date.now() - startTime,
786
- sessionId,
787
- uuid: uuid()
788
- };
789
- }
790
979
 
791
980
  // src/utils/cost.ts
792
981
  var ANTHROPIC_PRICING = {
@@ -1043,6 +1232,44 @@ class AnthropicAdapter {
1043
1232
  max_tokens: maxTokens,
1044
1233
  stream: true
1045
1234
  };
1235
+ if (request.thinking) {
1236
+ switch (request.thinking.type) {
1237
+ case "adaptive":
1238
+ params.thinking = { type: "adaptive" };
1239
+ break;
1240
+ case "disabled":
1241
+ params.thinking = { type: "disabled" };
1242
+ break;
1243
+ case "enabled":
1244
+ params.thinking = {
1245
+ type: "enabled",
1246
+ budget_tokens: request.thinking.budgetTokens
1247
+ };
1248
+ break;
1249
+ }
1250
+ } else if (request.thinkingBudget !== undefined) {
1251
+ if (request.thinkingBudget <= 0) {
1252
+ params.thinking = { type: "disabled" };
1253
+ } else {
1254
+ params.thinking = {
1255
+ type: "enabled",
1256
+ budget_tokens: Math.max(1024, request.thinkingBudget)
1257
+ };
1258
+ }
1259
+ }
1260
+ const outputConfig = {};
1261
+ if (request.effort) {
1262
+ outputConfig.effort = request.effort;
1263
+ }
1264
+ if (request.outputFormat?.type === "json_schema") {
1265
+ outputConfig.format = {
1266
+ type: "json_schema",
1267
+ schema: request.outputFormat.schema
1268
+ };
1269
+ }
1270
+ if (Object.keys(outputConfig).length > 0) {
1271
+ params.output_config = outputConfig;
1272
+ }
1046
1273
  if (this.oauthMode) {
1047
1274
  const systemBlocks = [
1048
1275
  { type: "text", text: "You are Claude Code, Anthropic's official CLI for Claude." }
@@ -1167,11 +1394,12 @@ class AnthropicAdapter {
1167
1394
  switch (feature) {
1168
1395
  case "streaming":
1169
1396
  case "tool_calling":
1170
- case "image_input":
1171
- case "pdf_input":
1172
1397
  case "thinking":
1173
1398
  case "structured_output":
1174
1399
  return true;
1400
+ case "image_input":
1401
+ case "pdf_input":
1402
+ return false;
1175
1403
  default:
1176
1404
  return false;
1177
1405
  }
@@ -1242,6 +1470,22 @@ var CODEX_MODELS = new Set([
1242
1470
  "gpt-5-codex",
1243
1471
  "gpt-5-codex-mini"
1244
1472
  ]);
1473
+ var OPENAI_MAX_TOOL_NAME = 64;
1474
+ function sanitizeToolName(name) {
1475
+ const clean = name.replace(/[^a-zA-Z0-9_-]/g, "_");
1476
+ if (clean.length <= OPENAI_MAX_TOOL_NAME)
1477
+ return clean;
1478
+ const hash = simpleHash(name);
1479
+ return clean.slice(0, OPENAI_MAX_TOOL_NAME - 7) + "_" + hash;
1480
+ }
1481
+ function simpleHash(s) {
1482
+ let h = 2166136261;
1483
+ for (let i = 0;i < s.length; i++) {
1484
+ h ^= s.charCodeAt(i);
1485
+ h = Math.imul(h, 16777619);
1486
+ }
1487
+ return (h >>> 0).toString(16).padStart(8, "0").slice(0, 6);
1488
+ }
1245
1489
 
1246
1490
  class OpenAIAdapter {
1247
1491
  name = "openai";
@@ -1249,6 +1493,7 @@ class OpenAIAdapter {
1249
1493
  codexMode;
1250
1494
  accountId;
1251
1495
  currentAccessToken;
1496
+ toolNameMap = new Map;
1252
1497
  constructor(options) {
1253
1498
  const key = options?.apiKey ?? process.env.OPENAI_API_KEY;
1254
1499
  if (key) {
@@ -1281,6 +1526,15 @@ class OpenAIAdapter {
1281
1526
  }
1282
1527
  }
1283
1528
  async* chat(request) {
1529
+ this.toolNameMap.clear();
1530
+ if (request.tools) {
1531
+ for (const tool of request.tools) {
1532
+ const sanitized = sanitizeToolName(tool.name);
1533
+ if (sanitized !== tool.name) {
1534
+ this.toolNameMap.set(sanitized, tool.name);
1535
+ }
1536
+ }
1537
+ }
1284
1538
  if (this.codexMode) {
1285
1539
  yield* this.chatResponses(request);
1286
1540
  } else {
@@ -1382,7 +1636,7 @@ class OpenAIAdapter {
1382
1636
  yield {
1383
1637
  type: "tool_call",
1384
1638
  id: buf.id,
1385
- name: buf.name,
1639
+ name: this.resolveToolName(buf.name),
1386
1640
  input
1387
1641
  };
1388
1642
  }
@@ -1426,7 +1680,7 @@ class OpenAIAdapter {
1426
1680
  yield {
1427
1681
  type: "tool_call",
1428
1682
  id: item.call_id,
1429
- name: item.name,
1683
+ name: this.resolveToolName(item.name),
1430
1684
  input: parsedInput
1431
1685
  };
1432
1686
  }
@@ -1493,7 +1747,7 @@ class OpenAIAdapter {
1493
1747
  id: block.id,
1494
1748
  type: "function",
1495
1749
  function: {
1496
- name: block.name,
1750
+ name: sanitizeToolName(block.name),
1497
1751
  arguments: JSON.stringify(block.input)
1498
1752
  }
1499
1753
  });
@@ -1558,7 +1812,7 @@ class OpenAIAdapter {
1558
1812
  result.push({
1559
1813
  type: "function_call",
1560
1814
  call_id: tu.id,
1561
- name: tu.name,
1815
+ name: sanitizeToolName(tu.name),
1562
1816
  arguments: JSON.stringify(tu.input)
1563
1817
  });
1564
1818
  }
@@ -1590,7 +1844,7 @@ class OpenAIAdapter {
1590
1844
  return tools.map((tool) => ({
1591
1845
  type: "function",
1592
1846
  function: {
1593
- name: tool.name,
1847
+ name: sanitizeToolName(tool.name),
1594
1848
  description: tool.description,
1595
1849
  parameters: tool.inputSchema
1596
1850
  }
@@ -1599,12 +1853,15 @@ class OpenAIAdapter {
1599
1853
  convertToolsForResponses(tools) {
1600
1854
  return tools.map((tool) => ({
1601
1855
  type: "function",
1602
- name: tool.name,
1856
+ name: sanitizeToolName(tool.name),
1603
1857
  description: tool.description,
1604
1858
  parameters: tool.inputSchema,
1605
1859
  strict: false
1606
1860
  }));
1607
1861
  }
1862
+ resolveToolName(name) {
1863
+ return this.toolNameMap.get(name) ?? name;
1864
+ }
1608
1865
  mapStopReason(reason) {
1609
1866
  switch (reason) {
1610
1867
  case "stop":
@@ -2086,6 +2343,16 @@ class ToolRegistry {
2086
2343
  register(tool) {
2087
2344
  this.tools.set(tool.name, tool);
2088
2345
  }
2346
+ unregister(name) {
2347
+ this.tools.delete(name);
2348
+ }
2349
+ clearByPrefix(prefix) {
2350
+ for (const name of this.tools.keys()) {
2351
+ if (name.startsWith(prefix)) {
2352
+ this.tools.delete(name);
2353
+ }
2354
+ }
2355
+ }
2089
2356
  get(name) {
2090
2357
  return this.tools.get(name);
2091
2358
  }
@@ -2119,16 +2386,34 @@ class ToolRegistry {
2119
2386
  // src/tools/presets.ts
2120
2387
  var PRESETS = {
2121
2388
  coding: ["Bash", "Read", "Write", "Edit", "Glob", "Grep"],
2389
+ claude_code: [
2390
+ "Bash",
2391
+ "Read",
2392
+ "Write",
2393
+ "Edit",
2394
+ "Glob",
2395
+ "Grep",
2396
+ "NotebookEdit",
2397
+ "WebFetch",
2398
+ "WebSearch",
2399
+ "TodoWrite",
2400
+ "Config",
2401
+ "AskUserQuestion",
2402
+ "ExitPlanMode"
2403
+ ],
2122
2404
  readonly: ["Read", "Glob", "Grep"],
2123
2405
  minimal: ["Read", "Write", "Edit", "Glob", "Grep"]
2124
2406
  };
2125
2407
  function resolveToolNames(tools) {
2126
2408
  if (!tools)
2127
- return PRESETS.coding;
2128
- if (typeof tools === "string") {
2129
- return PRESETS[tools] ?? [tools];
2409
+ return PRESETS.claude_code;
2410
+ if (Array.isArray(tools)) {
2411
+ return tools;
2130
2412
  }
2131
- return tools;
2413
+ if (tools.type === "preset") {
2414
+ return PRESETS[tools.preset] ?? PRESETS.claude_code;
2415
+ }
2416
+ throw new Error("Invalid tools option. Expected string[] or { type: 'preset', preset: 'claude_code' }.");
2132
2417
  }
2133
2418
 
2134
2419
  // src/tools/bash.ts
@@ -2152,17 +2437,58 @@ var BashTool = {
2152
2437
  timeout: {
2153
2438
  type: "number",
2154
2439
  description: "Timeout in milliseconds (max 600000)"
2440
+ },
2441
+ run_in_background: {
2442
+ type: "boolean",
2443
+ description: "Run command asynchronously and return immediately."
2444
+ },
2445
+ dangerouslyDisableSandbox: {
2446
+ type: "boolean",
2447
+ description: "If true, explicitly request unsandboxed execution."
2448
+ },
2449
+ _simulatedSedEdit: {
2450
+ type: "object",
2451
+ properties: {
2452
+ filePath: { type: "string" },
2453
+ newContent: { type: "string" }
2454
+ },
2455
+ description: "Internal field for precomputed edit previews."
2155
2456
  }
2156
2457
  },
2157
2458
  required: ["command"]
2158
2459
  },
2159
2460
  async execute(input, ctx) {
2160
- const { command, timeout: timeoutMs, description } = input;
2461
+ const {
2462
+ command,
2463
+ timeout: timeoutMs,
2464
+ run_in_background,
2465
+ description,
2466
+ dangerouslyDisableSandbox,
2467
+ _simulatedSedEdit
2468
+ } = input;
2161
2469
  if (!command || typeof command !== "string") {
2162
2470
  return { content: "Error: command is required", isError: true };
2163
2471
  }
2164
2472
  const timeout = Math.min(timeoutMs ?? DEFAULT_TIMEOUT, MAX_TIMEOUT);
2165
2473
  try {
2474
+ if (run_in_background) {
2475
+ const proc2 = Bun.spawn(["bash", "-c", command], {
2476
+ cwd: ctx.cwd,
2477
+ stdout: "ignore",
2478
+ stderr: "ignore",
2479
+ stdin: "ignore",
2480
+ env: { ...process.env, ...ctx.env }
2481
+ });
2482
+ return {
2483
+ content: `Background command started (pid ${proc2.pid ?? "unknown"}).`,
2484
+ metadata: {
2485
+ pid: proc2.pid ?? null,
2486
+ run_in_background: true,
2487
+ dangerouslyDisableSandbox: dangerouslyDisableSandbox === true,
2488
+ hasSimulatedSedEdit: !!_simulatedSedEdit
2489
+ }
2490
+ };
2491
+ }
2166
2492
  const proc = Bun.spawn(["bash", "-c", command], {
2167
2493
  cwd: ctx.cwd,
2168
2494
  stdout: "pipe",
@@ -2194,7 +2520,11 @@ var BashTool = {
2194
2520
  return {
2195
2521
  content: output,
2196
2522
  isError: exitCode !== 0 ? true : undefined,
2197
- metadata: { exitCode }
2523
+ metadata: {
2524
+ exitCode,
2525
+ dangerouslyDisableSandbox: dangerouslyDisableSandbox === true,
2526
+ hasSimulatedSedEdit: !!_simulatedSedEdit
2527
+ }
2198
2528
  };
2199
2529
  } catch (err) {
2200
2530
  const message = err instanceof Error ? err.message : String(err);
@@ -2223,12 +2553,17 @@ var ReadTool = {
2223
2553
  limit: {
2224
2554
  type: "number",
2225
2555
  description: "Number of lines to read"
2556
+ },
2557
+ pages: {
2558
+ type: "array",
2559
+ items: { type: "number" },
2560
+ description: "Optional PDF page numbers (1-based)."
2226
2561
  }
2227
2562
  },
2228
2563
  required: ["file_path"]
2229
2564
  },
2230
2565
  async execute(input, ctx) {
2231
- const { file_path, offset, limit } = input;
2566
+ const { file_path, offset, limit, pages } = input;
2232
2567
  if (!file_path) {
2233
2568
  return { content: "Error: file_path is required", isError: true };
2234
2569
  }
@@ -2239,6 +2574,12 @@ var ReadTool = {
2239
2574
  if (!exists) {
2240
2575
  return { content: `Error: File not found: ${resolvedPath}`, isError: true };
2241
2576
  }
2577
+ if (Array.isArray(pages) && pages.length > 0 && resolvedPath.toLowerCase().endsWith(".pdf")) {
2578
+ return {
2579
+ content: "Error: PDF page extraction is not implemented in this runtime.",
2580
+ isError: true
2581
+ };
2582
+ }
2242
2583
  const text = await file.text();
2243
2584
  const lines = text.split(`
2244
2585
  `);
@@ -2626,112 +2967,522 @@ async function collectFiles(dir, globPattern) {
2626
2967
  }
2627
2968
  return files;
2628
2969
  }
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
2970
 
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;
2971
+ // src/tools/notebook-edit.ts
2972
+ import { readFile, writeFile } from "fs/promises";
2973
+ function toSourceLines(text) {
2974
+ const lines = text.split(`
2975
+ `);
2976
+ return lines.map((line, idx) => idx < lines.length - 1 ? `${line}
2977
+ ` : line);
2672
2978
  }
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
- };
2979
+ var NotebookEditTool = {
2980
+ name: "NotebookEdit",
2981
+ description: "Edit a specific Jupyter notebook cell by id or index.",
2982
+ inputSchema: {
2983
+ type: "object",
2984
+ properties: {
2985
+ notebook_path: { type: "string", description: "Path to .ipynb file." },
2986
+ cell_id: { type: "string", description: "Cell id to edit." },
2987
+ cell_index: { type: "number", description: "Cell index to edit if id is not provided." },
2988
+ new_source: { type: "string", description: "New cell source content." }
2989
+ },
2990
+ required: ["notebook_path", "new_source"]
2991
+ },
2992
+ async execute(input, ctx) {
2993
+ const {
2994
+ notebook_path,
2995
+ cell_id,
2996
+ cell_index,
2997
+ new_source
2998
+ } = input ?? {};
2999
+ if (!notebook_path)
3000
+ return { content: "Error: notebook_path is required", isError: true };
3001
+ if (new_source === undefined)
3002
+ return { content: "Error: new_source is required", isError: true };
3003
+ const filePath = notebook_path.startsWith("/") ? notebook_path : `${ctx.cwd}/${notebook_path}`;
3004
+ try {
3005
+ const raw = await readFile(filePath, "utf-8");
3006
+ const notebook = JSON.parse(raw);
3007
+ if (!Array.isArray(notebook.cells)) {
3008
+ return { content: "Error: notebook has no cells array", isError: true };
2703
3009
  }
2704
- return { behavior: "allow" };
2705
- }
2706
- if (this.mode === "delegate") {
2707
- if (!DELEGATE_TOOLS.has(toolName) && !SAFE_TOOLS.has(toolName)) {
3010
+ let targetIndex = -1;
3011
+ if (cell_id) {
3012
+ targetIndex = notebook.cells.findIndex((c) => c.id === cell_id);
3013
+ } else if (typeof cell_index === "number") {
3014
+ targetIndex = cell_index;
3015
+ } else {
3016
+ targetIndex = 0;
3017
+ }
3018
+ if (targetIndex < 0 || targetIndex >= notebook.cells.length) {
2708
3019
  return {
2709
- behavior: "deny",
2710
- message: `Tool "${toolName}" is not allowed in delegate mode. Only Teammate, Task, and read-only tools are available.`
3020
+ content: `Error: cell not found (id=${cell_id ?? "n/a"}, index=${String(cell_index ?? "n/a")})`,
3021
+ isError: true
2711
3022
  };
2712
3023
  }
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
- }
3024
+ const cell = notebook.cells[targetIndex];
3025
+ cell.source = toSourceLines(new_source);
3026
+ await writeFile(filePath, JSON.stringify(notebook, null, 2) + `
3027
+ `, "utf-8");
3028
+ return {
3029
+ content: `Updated notebook cell ${targetIndex} in ${filePath}`
3030
+ };
3031
+ } catch (err) {
3032
+ const message = err instanceof Error ? err.message : String(err);
3033
+ return { content: `Error editing notebook: ${message}`, isError: true };
2731
3034
  }
2732
- if (this.canUseTool) {
2733
- const result = await this.canUseTool(toolName, input, options);
2734
- if (result.behavior === "allow" && result.updatedPermissions) {
3035
+ }
3036
+ };
3037
+
3038
+ // src/tools/web-fetch.ts
3039
+ var DEFAULT_TIMEOUT_MS = 20000;
3040
+ var MAX_OUTPUT = 80000;
3041
+ var WebFetchTool = {
3042
+ name: "WebFetch",
3043
+ description: "Fetches a URL and returns response text.",
3044
+ inputSchema: {
3045
+ type: "object",
3046
+ properties: {
3047
+ url: {
3048
+ type: "string",
3049
+ description: "The URL to fetch."
3050
+ },
3051
+ prompt: {
3052
+ type: "string",
3053
+ description: "Optional fetch intent/instructions."
3054
+ },
3055
+ timeout_ms: {
3056
+ type: "number",
3057
+ description: "Timeout in milliseconds (default 20000)."
3058
+ },
3059
+ max_length: {
3060
+ type: "number",
3061
+ description: "Maximum output length (default 80000)."
3062
+ }
3063
+ },
3064
+ required: ["url"]
3065
+ },
3066
+ async execute(input) {
3067
+ const { url, timeout_ms, max_length } = input ?? {};
3068
+ if (!url)
3069
+ return { content: "Error: url is required", isError: true };
3070
+ const timeout = Math.max(1000, timeout_ms ?? DEFAULT_TIMEOUT_MS);
3071
+ const outLimit = Math.max(1000, max_length ?? MAX_OUTPUT);
3072
+ const controller = new AbortController;
3073
+ const timer = setTimeout(() => controller.abort(), timeout);
3074
+ try {
3075
+ const res = await fetch(url, {
3076
+ method: "GET",
3077
+ signal: controller.signal,
3078
+ headers: {
3079
+ "user-agent": "fourmis-agent-sdk/1.0"
3080
+ }
3081
+ });
3082
+ const contentType = res.headers.get("content-type") ?? "unknown";
3083
+ let body = await res.text();
3084
+ if (body.length > outLimit) {
3085
+ body = body.slice(0, outLimit) + `
3086
+ ... (truncated)`;
3087
+ }
3088
+ return {
3089
+ content: [
3090
+ `Status: ${res.status} ${res.statusText}`,
3091
+ `Content-Type: ${contentType}`,
3092
+ "",
3093
+ body
3094
+ ].join(`
3095
+ `),
3096
+ isError: res.ok ? undefined : true
3097
+ };
3098
+ } catch (err) {
3099
+ const message = err instanceof Error ? err.message : String(err);
3100
+ return { content: `Error fetching URL: ${message}`, isError: true };
3101
+ } finally {
3102
+ clearTimeout(timer);
3103
+ }
3104
+ }
3105
+ };
3106
+
3107
+ // src/tools/web-search.ts
3108
+ var SEARCH_ENDPOINT = "https://duckduckgo.com/html/";
3109
+ function stripTags(input) {
3110
+ return input.replace(/<[^>]+>/g, "").replace(/&amp;/g, "&").replace(/&quot;/g, '"').replace(/&#39;/g, "'").replace(/&lt;/g, "<").replace(/&gt;/g, ">").trim();
3111
+ }
3112
+ var WebSearchTool = {
3113
+ name: "WebSearch",
3114
+ description: "Searches the web and returns top result links.",
3115
+ inputSchema: {
3116
+ type: "object",
3117
+ properties: {
3118
+ query: {
3119
+ type: "string",
3120
+ description: "Search query."
3121
+ },
3122
+ max_results: {
3123
+ type: "number",
3124
+ description: "Maximum results to return (default 5)."
3125
+ }
3126
+ },
3127
+ required: ["query"]
3128
+ },
3129
+ async execute(input) {
3130
+ const { query, max_results } = input ?? {};
3131
+ if (!query) {
3132
+ return { content: "Error: query is required", isError: true };
3133
+ }
3134
+ const limit = Math.max(1, Math.min(20, max_results ?? 5));
3135
+ try {
3136
+ const url = `${SEARCH_ENDPOINT}?q=${encodeURIComponent(query)}`;
3137
+ const res = await fetch(url, {
3138
+ headers: {
3139
+ "user-agent": "fourmis-agent-sdk/1.0"
3140
+ }
3141
+ });
3142
+ if (!res.ok) {
3143
+ return {
3144
+ content: `Error searching web: ${res.status} ${res.statusText}`,
3145
+ isError: true
3146
+ };
3147
+ }
3148
+ const html = await res.text();
3149
+ const matches = [...html.matchAll(/<a[^>]*class="result__a"[^>]*href="([^"]+)"[^>]*>([\s\S]*?)<\/a>/g)];
3150
+ if (matches.length === 0) {
3151
+ return { content: "No search results found." };
3152
+ }
3153
+ const lines = [];
3154
+ for (let i = 0;i < Math.min(limit, matches.length); i++) {
3155
+ const href = stripTags(matches[i][1]);
3156
+ const title = stripTags(matches[i][2]);
3157
+ lines.push(`${i + 1}. ${title}
3158
+ ${href}`);
3159
+ }
3160
+ return { content: lines.join(`
3161
+ `) };
3162
+ } catch (err) {
3163
+ const message = err instanceof Error ? err.message : String(err);
3164
+ return { content: `Error searching web: ${message}`, isError: true };
3165
+ }
3166
+ }
3167
+ };
3168
+
3169
+ // src/tools/ask-user-question.ts
3170
+ var AskUserQuestionTool = {
3171
+ name: "AskUserQuestion",
3172
+ description: "Ask the user a clarifying question and wait for their response.",
3173
+ inputSchema: {
3174
+ type: "object",
3175
+ properties: {
3176
+ question: {
3177
+ type: "string",
3178
+ description: "Question to ask the user."
3179
+ },
3180
+ options: {
3181
+ type: "array",
3182
+ items: { type: "string" },
3183
+ description: "Optional fixed choices."
3184
+ }
3185
+ },
3186
+ required: ["question"]
3187
+ },
3188
+ async execute(input) {
3189
+ const { question, options } = input ?? {};
3190
+ if (!question) {
3191
+ return { content: "Error: question is required", isError: true };
3192
+ }
3193
+ const choices = Array.isArray(options) && options.length > 0 ? ` Choices: ${options.join(" | ")}` : "";
3194
+ return {
3195
+ content: `User interaction is not available in this runtime. Unanswered question: ${question}.${choices}`,
3196
+ isError: true
3197
+ };
3198
+ }
3199
+ };
3200
+
3201
+ // src/tools/todo-write.ts
3202
+ import { mkdir as mkdir2, writeFile as writeFile2 } from "fs/promises";
3203
+ import { dirname as dirname2, join as join3 } from "path";
3204
+ var TodoWriteTool = {
3205
+ name: "TodoWrite",
3206
+ description: "Write/update task todo items for the current session.",
3207
+ inputSchema: {
3208
+ type: "object",
3209
+ properties: {
3210
+ todos: {
3211
+ type: "array",
3212
+ items: {
3213
+ type: "object",
3214
+ properties: {
3215
+ content: { type: "string" },
3216
+ status: { type: "string", enum: ["pending", "in_progress", "completed"] },
3217
+ activeForm: { type: "string" }
3218
+ },
3219
+ required: ["content", "status"]
3220
+ }
3221
+ }
3222
+ },
3223
+ required: ["todos"]
3224
+ },
3225
+ async execute(input, ctx) {
3226
+ const { todos } = input ?? {};
3227
+ if (!Array.isArray(todos)) {
3228
+ return { content: "Error: todos must be an array", isError: true };
3229
+ }
3230
+ for (const todo of todos) {
3231
+ if (!todo?.content || !todo?.status) {
3232
+ return { content: "Error: each todo requires content and status", isError: true };
3233
+ }
3234
+ }
3235
+ const filePath = join3(ctx.cwd, ".claude", "todos.json");
3236
+ try {
3237
+ await mkdir2(dirname2(filePath), { recursive: true });
3238
+ const payload = {
3239
+ updatedAt: new Date().toISOString(),
3240
+ todos
3241
+ };
3242
+ await writeFile2(filePath, JSON.stringify(payload, null, 2) + `
3243
+ `, "utf-8");
3244
+ return {
3245
+ content: `Saved ${todos.length} todo item(s) to ${filePath}`
3246
+ };
3247
+ } catch (err) {
3248
+ const message = err instanceof Error ? err.message : String(err);
3249
+ return { content: `Error writing todos: ${message}`, isError: true };
3250
+ }
3251
+ }
3252
+ };
3253
+
3254
+ // src/tools/config.ts
3255
+ import { mkdir as mkdir3, readFile as readFile2, writeFile as writeFile3 } from "fs/promises";
3256
+ import { join as join4, dirname as dirname3 } from "path";
3257
+ function scopePath(cwd, scope) {
3258
+ if (scope === "project")
3259
+ return join4(cwd, ".claude", "settings.json");
3260
+ return join4(cwd, ".claude", "settings.local.json");
3261
+ }
3262
+ function setByPath(obj, keyPath, value) {
3263
+ const keys = keyPath.split(".").filter(Boolean);
3264
+ if (keys.length === 0)
3265
+ return;
3266
+ let current = obj;
3267
+ for (let i = 0;i < keys.length - 1; i++) {
3268
+ const key = keys[i];
3269
+ const next = current[key];
3270
+ if (!next || typeof next !== "object" || Array.isArray(next)) {
3271
+ current[key] = {};
3272
+ }
3273
+ current = current[key];
3274
+ }
3275
+ current[keys[keys.length - 1]] = value;
3276
+ }
3277
+ function getByPath(obj, keyPath) {
3278
+ const keys = keyPath.split(".").filter(Boolean);
3279
+ let current = obj;
3280
+ for (const key of keys) {
3281
+ if (!current || typeof current !== "object" || Array.isArray(current))
3282
+ return;
3283
+ current = current[key];
3284
+ }
3285
+ return current;
3286
+ }
3287
+ var ConfigTool = {
3288
+ name: "Config",
3289
+ description: "Read or update .claude settings values.",
3290
+ inputSchema: {
3291
+ type: "object",
3292
+ properties: {
3293
+ action: {
3294
+ type: "string",
3295
+ enum: ["get", "set", "list"]
3296
+ },
3297
+ key: {
3298
+ type: "string",
3299
+ description: "Dot-path key (for get/set)."
3300
+ },
3301
+ value: {
3302
+ description: "Value for set action."
3303
+ },
3304
+ scope: {
3305
+ type: "string",
3306
+ enum: ["local", "project"]
3307
+ }
3308
+ },
3309
+ required: ["action"]
3310
+ },
3311
+ async execute(input, ctx) {
3312
+ const {
3313
+ action,
3314
+ key,
3315
+ value,
3316
+ scope = "local"
3317
+ } = input ?? {};
3318
+ if (!action) {
3319
+ return { content: "Error: action is required", isError: true };
3320
+ }
3321
+ const filePath = scopePath(ctx.cwd, scope);
3322
+ let data = {};
3323
+ try {
3324
+ const raw = await readFile2(filePath, "utf-8");
3325
+ data = JSON.parse(raw);
3326
+ } catch {
3327
+ data = {};
3328
+ }
3329
+ if (action === "list") {
3330
+ return { content: JSON.stringify(data, null, 2) };
3331
+ }
3332
+ if (!key) {
3333
+ return { content: "Error: key is required for get/set", isError: true };
3334
+ }
3335
+ if (action === "get") {
3336
+ const out = getByPath(data, key);
3337
+ return { content: out === undefined ? "undefined" : JSON.stringify(out, null, 2) };
3338
+ }
3339
+ setByPath(data, key, value);
3340
+ try {
3341
+ await mkdir3(dirname3(filePath), { recursive: true });
3342
+ await writeFile3(filePath, JSON.stringify(data, null, 2) + `
3343
+ `, "utf-8");
3344
+ return { content: `Updated ${key} in ${filePath}` };
3345
+ } catch (err) {
3346
+ const message = err instanceof Error ? err.message : String(err);
3347
+ return { content: `Error writing config: ${message}`, isError: true };
3348
+ }
3349
+ }
3350
+ };
3351
+
3352
+ // src/tools/exit-plan-mode.ts
3353
+ var ExitPlanModeTool = {
3354
+ name: "ExitPlanMode",
3355
+ description: "Exit plan mode and resume normal execution permissions.",
3356
+ inputSchema: {
3357
+ type: "object",
3358
+ properties: {}
3359
+ },
3360
+ async execute() {
3361
+ return {
3362
+ content: "Exiting plan mode.",
3363
+ metadata: {
3364
+ setPermissionMode: "default"
3365
+ }
3366
+ };
3367
+ }
3368
+ };
3369
+ // src/tools/index.ts
3370
+ var ALL_TOOLS = {
3371
+ Bash: BashTool,
3372
+ Read: ReadTool,
3373
+ Write: WriteTool,
3374
+ Edit: EditTool,
3375
+ Glob: GlobTool,
3376
+ Grep: GrepTool,
3377
+ NotebookEdit: NotebookEditTool,
3378
+ WebFetch: WebFetchTool,
3379
+ WebSearch: WebSearchTool,
3380
+ AskUserQuestion: AskUserQuestionTool,
3381
+ TodoWrite: TodoWriteTool,
3382
+ Config: ConfigTool,
3383
+ ExitPlanMode: ExitPlanModeTool
3384
+ };
3385
+ function buildToolRegistry(toolNames, allowedTools, disallowedTools) {
3386
+ const registry = new ToolRegistry;
3387
+ for (const name of toolNames) {
3388
+ if (disallowedTools?.includes(name))
3389
+ continue;
3390
+ const tool = ALL_TOOLS[name];
3391
+ if (tool) {
3392
+ registry.register(tool);
3393
+ }
3394
+ }
3395
+ return registry;
3396
+ }
3397
+
3398
+ // src/permissions.ts
3399
+ var SAFE_TOOLS = new Set(["Read", "Glob", "Grep", "WebFetch", "WebSearch"]);
3400
+ var EDIT_TOOLS = new Set(["Write", "Edit", "NotebookEdit", "TodoWrite", "Config"]);
3401
+ var FS_COMMANDS = ["mkdir", "touch", "rm", "mv", "cp"];
3402
+ var DELEGATE_TOOLS = new Set(["Teammate", "Task", "TaskOutput", "TaskStop"]);
3403
+ function normalizeRules(rules) {
3404
+ if (!rules)
3405
+ return [];
3406
+ return rules.map((r) => typeof r === "string" ? { toolName: r } : r);
3407
+ }
3408
+ function matchesRule(rules, toolName, input) {
3409
+ for (const rule of rules) {
3410
+ if (rule.toolName !== toolName)
3411
+ continue;
3412
+ if (!rule.ruleContent)
3413
+ return true;
3414
+ const inputStr = toolName === "Bash" ? String(input?.command ?? "") : JSON.stringify(input ?? {});
3415
+ if (inputStr.includes(rule.ruleContent))
3416
+ return true;
3417
+ }
3418
+ return false;
3419
+ }
3420
+
3421
+ class PermissionManager {
3422
+ mode;
3423
+ canUseTool;
3424
+ allowRules;
3425
+ denyRules;
3426
+ settingsManager;
3427
+ constructor(mode = "default", canUseTool, permissions, settingsManager) {
3428
+ this.mode = mode;
3429
+ this.canUseTool = canUseTool;
3430
+ this.allowRules = normalizeRules(permissions?.allow);
3431
+ this.denyRules = normalizeRules(permissions?.deny);
3432
+ this.settingsManager = settingsManager;
3433
+ }
3434
+ async check(toolName, input, options) {
3435
+ if (this.mode === "bypassPermissions") {
3436
+ return { behavior: "allow" };
3437
+ }
3438
+ if (matchesRule(this.denyRules, toolName, input)) {
3439
+ return {
3440
+ behavior: "deny",
3441
+ message: `Tool "${toolName}" is denied by permissions config.`
3442
+ };
3443
+ }
3444
+ if (this.mode === "plan") {
3445
+ if (!SAFE_TOOLS.has(toolName)) {
3446
+ return {
3447
+ behavior: "deny",
3448
+ message: `Tool "${toolName}" is not allowed in plan mode. Only read-only tools are available.`
3449
+ };
3450
+ }
3451
+ return { behavior: "allow" };
3452
+ }
3453
+ if (this.mode === "delegate") {
3454
+ if (!DELEGATE_TOOLS.has(toolName) && !SAFE_TOOLS.has(toolName)) {
3455
+ return {
3456
+ behavior: "deny",
3457
+ message: `Tool "${toolName}" is not allowed in delegate mode. Only Teammate, Task, and read-only tools are available.`
3458
+ };
3459
+ }
3460
+ return { behavior: "allow" };
3461
+ }
3462
+ if (matchesRule(this.allowRules, toolName, input)) {
3463
+ return { behavior: "allow" };
3464
+ }
3465
+ if (SAFE_TOOLS.has(toolName)) {
3466
+ return { behavior: "allow" };
3467
+ }
3468
+ if (this.mode === "acceptEdits") {
3469
+ if (EDIT_TOOLS.has(toolName)) {
3470
+ return { behavior: "allow" };
3471
+ }
3472
+ if (toolName === "Bash") {
3473
+ const cmd = String(input?.command ?? "").trimStart();
3474
+ if (FS_COMMANDS.some((fc) => cmd.startsWith(fc + " ") || cmd === fc)) {
3475
+ return { behavior: "allow" };
3476
+ }
3477
+ }
3478
+ }
3479
+ if (this.canUseTool) {
3480
+ const result = await this.canUseTool(toolName, input, {
3481
+ ...options,
3482
+ toolUseID: options.toolUseId,
3483
+ agentID: options.agentId
3484
+ });
3485
+ if (result.behavior === "allow" && result.updatedPermissions) {
2735
3486
  this.applyPermissionUpdates(result.updatedPermissions);
2736
3487
  }
2737
3488
  return result;
@@ -2791,7 +3542,7 @@ class PermissionManager {
2791
3542
 
2792
3543
  // src/settings.ts
2793
3544
  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";
3545
+ import { dirname as dirname4, join as join5 } from "path";
2795
3546
  import { homedir as homedir3 } from "os";
2796
3547
 
2797
3548
  class SettingsManager {
@@ -2869,7 +3620,7 @@ class SettingsManager {
2869
3620
  default:
2870
3621
  return;
2871
3622
  }
2872
- const dir = dirname2(path);
3623
+ const dir = dirname4(path);
2873
3624
  if (!existsSync3(dir))
2874
3625
  mkdirSync2(dir, { recursive: true });
2875
3626
  writeFileSync3(path, JSON.stringify(data, null, 2) + `
@@ -2878,21 +3629,21 @@ class SettingsManager {
2878
3629
  sourceToPath(source) {
2879
3630
  switch (source) {
2880
3631
  case "user":
2881
- return join3(homedir3(), ".claude", "settings.json");
3632
+ return join5(homedir3(), ".claude", "settings.json");
2882
3633
  case "project":
2883
- return join3(this.cwd, ".claude", "settings.json");
3634
+ return join5(this.cwd, ".claude", "settings.json");
2884
3635
  case "local":
2885
- return join3(this.cwd, ".claude", "settings.local.json");
3636
+ return join5(this.cwd, ".claude", "settings.local.json");
2886
3637
  }
2887
3638
  }
2888
3639
  destinationToPath(destination) {
2889
3640
  switch (destination) {
2890
3641
  case "userSettings":
2891
- return join3(homedir3(), ".claude", "settings.json");
3642
+ return join5(homedir3(), ".claude", "settings.json");
2892
3643
  case "projectSettings":
2893
- return join3(this.cwd, ".claude", "settings.json");
3644
+ return join5(this.cwd, ".claude", "settings.json");
2894
3645
  case "localSettings":
2895
- return join3(this.cwd, ".claude", "settings.local.json");
3646
+ return join5(this.cwd, ".claude", "settings.local.json");
2896
3647
  default:
2897
3648
  return null;
2898
3649
  }
@@ -2953,9 +3704,10 @@ function stripFrontmatter(content) {
2953
3704
  // src/skills/skills.ts
2954
3705
  import { existsSync as existsSync4, readdirSync, readFileSync as readFileSync4, realpathSync, statSync } from "fs";
2955
3706
  import { homedir as homedir4 } from "os";
2956
- import { basename, dirname as dirname3, isAbsolute, join as join4, resolve } from "path";
3707
+ import { basename, dirname as dirname5, isAbsolute, join as join6, resolve } from "path";
2957
3708
  var MAX_NAME_LENGTH = 64;
2958
3709
  var MAX_DESCRIPTION_LENGTH = 1024;
3710
+ var MAX_COMPATIBILITY_LENGTH = 500;
2959
3711
  var CONFIG_DIR_NAME = ".claude";
2960
3712
  function shouldIgnore(name) {
2961
3713
  return name.startsWith(".") || name === "node_modules";
@@ -2988,6 +3740,14 @@ function validateDescription(description) {
2988
3740
  }
2989
3741
  return errors;
2990
3742
  }
3743
+ function validateCompatibility(compatibility) {
3744
+ if (!compatibility)
3745
+ return [];
3746
+ if (compatibility.length > MAX_COMPATIBILITY_LENGTH) {
3747
+ return [`compatibility exceeds ${MAX_COMPATIBILITY_LENGTH} characters (${compatibility.length})`];
3748
+ }
3749
+ return [];
3750
+ }
2991
3751
  function loadSkillsFromDir(options) {
2992
3752
  return loadSkillsFromDirInternal(options.dir, options.source, true);
2993
3753
  }
@@ -3003,7 +3763,7 @@ function loadSkillsFromDirInternal(dir, source, includeRootFiles) {
3003
3763
  if (shouldIgnore(entry.name)) {
3004
3764
  continue;
3005
3765
  }
3006
- const fullPath = join4(dir, entry.name);
3766
+ const fullPath = join6(dir, entry.name);
3007
3767
  let isDirectory = entry.isDirectory();
3008
3768
  let isFile = entry.isFile();
3009
3769
  if (entry.isSymbolicLink()) {
@@ -3041,7 +3801,7 @@ function loadSkillFromFile(filePath, source) {
3041
3801
  try {
3042
3802
  const rawContent = readFileSync4(filePath, "utf-8");
3043
3803
  const { frontmatter } = parseFrontmatter(rawContent);
3044
- const skillDir = dirname3(filePath);
3804
+ const skillDir = dirname5(filePath);
3045
3805
  const parentDirName = basename(skillDir);
3046
3806
  const descErrors = validateDescription(frontmatter.description);
3047
3807
  for (const error of descErrors) {
@@ -3052,9 +3812,15 @@ function loadSkillFromFile(filePath, source) {
3052
3812
  for (const error of nameErrors) {
3053
3813
  diagnostics.push({ type: "warning", message: error, path: filePath });
3054
3814
  }
3815
+ const compatErrors = validateCompatibility(frontmatter.compatibility);
3816
+ for (const error of compatErrors) {
3817
+ diagnostics.push({ type: "warning", message: error, path: filePath });
3818
+ }
3055
3819
  if (!frontmatter.description || frontmatter.description.trim() === "") {
3056
3820
  return { skill: null, diagnostics };
3057
3821
  }
3822
+ const allowedToolsRaw = frontmatter["allowed-tools"];
3823
+ const allowedTools = allowedToolsRaw ? allowedToolsRaw.split(/\s+/).filter(Boolean) : undefined;
3058
3824
  return {
3059
3825
  skill: {
3060
3826
  name,
@@ -3062,7 +3828,11 @@ function loadSkillFromFile(filePath, source) {
3062
3828
  filePath,
3063
3829
  baseDir: skillDir,
3064
3830
  source,
3065
- disableModelInvocation: frontmatter["disable-model-invocation"] === true
3831
+ disableModelInvocation: frontmatter["disable-model-invocation"] === true,
3832
+ license: frontmatter.license,
3833
+ compatibility: frontmatter.compatibility,
3834
+ metadata: frontmatter.metadata,
3835
+ allowedTools
3066
3836
  },
3067
3837
  diagnostics
3068
3838
  };
@@ -3077,9 +3847,9 @@ function normalizePath(input) {
3077
3847
  if (trimmed === "~")
3078
3848
  return homedir4();
3079
3849
  if (trimmed.startsWith("~/"))
3080
- return join4(homedir4(), trimmed.slice(2));
3850
+ return join6(homedir4(), trimmed.slice(2));
3081
3851
  if (trimmed.startsWith("~"))
3082
- return join4(homedir4(), trimmed.slice(1));
3852
+ return join6(homedir4(), trimmed.slice(1));
3083
3853
  return trimmed;
3084
3854
  }
3085
3855
  function resolveSkillPath(p, cwd) {
@@ -3123,7 +3893,7 @@ function loadSkills(options = {}) {
3123
3893
  }
3124
3894
  }
3125
3895
  if (includeDefaults) {
3126
- const userSkillsDir = join4(homedir4(), CONFIG_DIR_NAME, "skills");
3896
+ const userSkillsDir = join6(homedir4(), CONFIG_DIR_NAME, "skills");
3127
3897
  const projectSkillsDir = resolve(cwd, CONFIG_DIR_NAME, "skills");
3128
3898
  addSkills(loadSkillsFromDirInternal(userSkillsDir, "user", true));
3129
3899
  addSkills(loadSkillsFromDirInternal(projectSkillsDir, "project", true));
@@ -3178,6 +3948,9 @@ function formatSkillsForPrompt(skills) {
3178
3948
  lines.push(` <name>${escapeXml(skill.name)}</name>`);
3179
3949
  lines.push(` <description>${escapeXml(skill.description)}</description>`);
3180
3950
  lines.push(` <location>${escapeXml(skill.filePath)}</location>`);
3951
+ if (skill.allowedTools?.length) {
3952
+ lines.push(` <allowed-tools>${escapeXml(skill.allowedTools.join(" "))}</allowed-tools>`);
3953
+ }
3181
3954
  lines.push(" </skill>");
3182
3955
  }
3183
3956
  lines.push("</available_skills>");
@@ -3186,7 +3959,7 @@ function formatSkillsForPrompt(skills) {
3186
3959
  }
3187
3960
  // src/utils/system-prompt.ts
3188
3961
  import { readFileSync as readFileSync5 } from "fs";
3189
- import { join as join5 } from "path";
3962
+ import { join as join7 } from "path";
3190
3963
  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
3964
 
3192
3965
  You are highly capable and can help users complete complex tasks that would otherwise be too difficult or time-consuming.`;
@@ -3250,11 +4023,18 @@ function buildSystemPrompt(context) {
3250
4023
  sections.push(`# Environment
3251
4024
 
3252
4025
  Working directory: ${context.cwd}`);
3253
- const instructions = readProjectInstructions(context.cwd);
3254
- if (instructions) {
3255
- sections.push(`# Project Instructions
4026
+ if (context.additionalDirectories && context.additionalDirectories.length > 0) {
4027
+ sections.push(`Additional allowed directories:
4028
+ ${context.additionalDirectories.map((d) => `- ${d}`).join(`
4029
+ `)}`);
4030
+ }
4031
+ if (context.loadProjectInstructions) {
4032
+ const instructions = readProjectInstructions(context.cwd);
4033
+ if (instructions) {
4034
+ sections.push(`# Project Instructions
3256
4035
 
3257
4036
  ${instructions}`);
4037
+ }
3258
4038
  }
3259
4039
  }
3260
4040
  if (context.skills && context.skills.length > 0) {
@@ -3275,7 +4055,7 @@ ${skillsPrompt}`);
3275
4055
  function readProjectInstructions(cwd) {
3276
4056
  for (const name of ["CLAUDE.md", "AGENTS.md"]) {
3277
4057
  try {
3278
- const content = readFileSync5(join5(cwd, name), "utf-8").trim();
4058
+ const content = readFileSync5(join7(cwd, name), "utf-8").trim();
3279
4059
  if (content)
3280
4060
  return content;
3281
4061
  } catch {}
@@ -3284,7 +4064,10 @@ function readProjectInstructions(cwd) {
3284
4064
  }
3285
4065
 
3286
4066
  // src/query.ts
3287
- function createQuery(generator, abortController) {
4067
+ function unsupported(methodName) {
4068
+ return Promise.reject(new Error(`Query.${methodName} is not supported in this runtime yet.`));
4069
+ }
4070
+ function createQuery(generator, abortController, controls) {
3288
4071
  const query = {
3289
4072
  next: generator.next.bind(generator),
3290
4073
  return: generator.return.bind(generator),
@@ -3295,6 +4078,71 @@ function createQuery(generator, abortController) {
3295
4078
  async interrupt() {
3296
4079
  abortController.abort();
3297
4080
  },
4081
+ async setPermissionMode(mode) {
4082
+ if (!controls?.setPermissionMode)
4083
+ return unsupported("setPermissionMode");
4084
+ await controls.setPermissionMode(mode);
4085
+ },
4086
+ async setModel(model) {
4087
+ if (!controls?.setModel)
4088
+ return unsupported("setModel");
4089
+ await controls.setModel(model);
4090
+ },
4091
+ async setMaxThinkingTokens(maxThinkingTokens) {
4092
+ if (!controls?.setMaxThinkingTokens)
4093
+ return unsupported("setMaxThinkingTokens");
4094
+ await controls.setMaxThinkingTokens(maxThinkingTokens);
4095
+ },
4096
+ async initializationResult() {
4097
+ if (!controls?.initializationResult)
4098
+ return unsupported("initializationResult");
4099
+ return controls.initializationResult();
4100
+ },
4101
+ async supportedCommands() {
4102
+ if (!controls?.supportedCommands)
4103
+ return unsupported("supportedCommands");
4104
+ return controls.supportedCommands();
4105
+ },
4106
+ async supportedModels() {
4107
+ if (!controls?.supportedModels)
4108
+ return unsupported("supportedModels");
4109
+ return controls.supportedModels();
4110
+ },
4111
+ async mcpServerStatus() {
4112
+ if (!controls?.mcpServerStatus)
4113
+ return unsupported("mcpServerStatus");
4114
+ return controls.mcpServerStatus();
4115
+ },
4116
+ async accountInfo() {
4117
+ if (!controls?.accountInfo)
4118
+ return unsupported("accountInfo");
4119
+ return controls.accountInfo();
4120
+ },
4121
+ async rewindFiles(userMessageId, options) {
4122
+ if (!controls?.rewindFiles)
4123
+ return unsupported("rewindFiles");
4124
+ return controls.rewindFiles(userMessageId, options);
4125
+ },
4126
+ async reconnectMcpServer(serverName) {
4127
+ if (!controls?.reconnectMcpServer)
4128
+ return unsupported("reconnectMcpServer");
4129
+ await controls.reconnectMcpServer(serverName);
4130
+ },
4131
+ async toggleMcpServer(serverName, enabled) {
4132
+ if (!controls?.toggleMcpServer)
4133
+ return unsupported("toggleMcpServer");
4134
+ await controls.toggleMcpServer(serverName, enabled);
4135
+ },
4136
+ async setMcpServers(servers) {
4137
+ if (!controls?.setMcpServers)
4138
+ return unsupported("setMcpServers");
4139
+ return controls.setMcpServers(servers);
4140
+ },
4141
+ async streamInput(stream) {
4142
+ if (!controls?.streamInput)
4143
+ return unsupported("streamInput");
4144
+ await controls.streamInput(stream);
4145
+ },
3298
4146
  close() {
3299
4147
  abortController.abort();
3300
4148
  generator.return(undefined);
@@ -3316,7 +4164,10 @@ var HOOK_EVENTS = [
3316
4164
  "SubagentStart",
3317
4165
  "SubagentStop",
3318
4166
  "PreCompact",
3319
- "PermissionRequest"
4167
+ "PermissionRequest",
4168
+ "Setup",
4169
+ "TeammateIdle",
4170
+ "TaskCompleted"
3320
4171
  ];
3321
4172
  var TOOL_EVENTS = new Set(["PreToolUse", "PostToolUse", "PostToolUseFailure"]);
3322
4173
 
@@ -3348,9 +4199,31 @@ class HookManager {
3348
4199
  }
3349
4200
  }
3350
4201
  for (const callback of entry.hooks) {
3351
- const result = await callback(input, toolUseId, { signal });
4202
+ const timeoutMs = typeof entry.timeout === "number" && entry.timeout > 0 ? Math.floor(entry.timeout * 1000) : null;
4203
+ const invoke = callback(input, toolUseId, { signal });
4204
+ const result = timeoutMs ? await Promise.race([
4205
+ invoke,
4206
+ new Promise((resolve2) => {
4207
+ setTimeout(() => resolve2({}), timeoutMs);
4208
+ })
4209
+ ]) : await invoke;
3352
4210
  if (result) {
3353
4211
  hasOutput = true;
4212
+ if (result.continue === false && !result.permissionDecision) {
4213
+ merged.permissionDecision = "deny";
4214
+ }
4215
+ if (result.decision) {
4216
+ if (result.decision.behavior === "deny") {
4217
+ merged.permissionDecision = "deny";
4218
+ } else {
4219
+ if (!merged.permissionDecision) {
4220
+ merged.permissionDecision = "allow";
4221
+ }
4222
+ if (result.decision.updatedInput !== undefined) {
4223
+ merged.updatedInput = result.decision.updatedInput;
4224
+ }
4225
+ }
4226
+ }
3354
4227
  if (result.permissionDecision) {
3355
4228
  if (!merged.permissionDecision || result.permissionDecision === "deny") {
3356
4229
  merged.permissionDecision = result.permissionDecision;
@@ -3380,14 +4253,29 @@ import { InMemoryTransport } from "@modelcontextprotocol/sdk/inMemory.js";
3380
4253
  class McpClientManager {
3381
4254
  configs;
3382
4255
  servers = new Map;
4256
+ disabled = new Set;
3383
4257
  constructor(configs) {
3384
- this.configs = configs;
4258
+ this.configs = { ...configs };
3385
4259
  }
3386
4260
  async connectAll() {
3387
4261
  const entries = Object.entries(this.configs);
3388
4262
  await Promise.all(entries.map(([name, config]) => this.connectOne(name, config)));
3389
4263
  }
3390
4264
  async connectOne(name, config) {
4265
+ if (this.disabled.has(name)) {
4266
+ this.servers.set(name, {
4267
+ name,
4268
+ client: null,
4269
+ tools: [],
4270
+ status: {
4271
+ name,
4272
+ status: "disabled",
4273
+ config: this.toStatusConfig(config),
4274
+ scope: config.type === "sdk" ? "sdk" : "session"
4275
+ }
4276
+ });
4277
+ return;
4278
+ }
3391
4279
  try {
3392
4280
  const client = new Client({ name: `fourmis-${name}`, version: "1.0.0" });
3393
4281
  if (config.type === "sdk") {
@@ -3421,13 +4309,25 @@ class McpClientManager {
3421
4309
  const tools = (toolsResult.tools ?? []).map((t) => ({
3422
4310
  name: t.name,
3423
4311
  description: t.description,
3424
- inputSchema: t.inputSchema
4312
+ inputSchema: t.inputSchema,
4313
+ annotations: t.annotations ? {
4314
+ readOnly: t.annotations.readOnly,
4315
+ destructive: t.annotations.destructive,
4316
+ openWorld: t.annotations.openWorld
4317
+ } : undefined
3425
4318
  }));
3426
4319
  this.servers.set(name, {
3427
4320
  name,
3428
4321
  client,
3429
4322
  tools,
3430
- status: { name, status: "connected", tools }
4323
+ status: {
4324
+ name,
4325
+ status: "connected",
4326
+ tools,
4327
+ serverInfo: { name, version: "1.0.0" },
4328
+ config: this.toStatusConfig(config),
4329
+ scope: config.type === "sdk" ? "sdk" : "session"
4330
+ }
3431
4331
  });
3432
4332
  } catch (err) {
3433
4333
  const error = err instanceof Error ? err.message : String(err);
@@ -3435,17 +4335,31 @@ class McpClientManager {
3435
4335
  name,
3436
4336
  client: null,
3437
4337
  tools: [],
3438
- status: { name, status: "failed", error }
4338
+ status: {
4339
+ name,
4340
+ status: "failed",
4341
+ error,
4342
+ config: this.toStatusConfig(config),
4343
+ scope: config.type === "sdk" ? "sdk" : "session"
4344
+ }
3439
4345
  });
3440
4346
  }
3441
4347
  }
4348
+ toStatusConfig(config) {
4349
+ if (config.type === "sdk") {
4350
+ return { type: "sdk", name: config.name };
4351
+ }
4352
+ return config;
4353
+ }
3442
4354
  getTools() {
3443
4355
  const result = [];
3444
4356
  for (const [serverName, server] of this.servers) {
3445
4357
  if (server.status.status !== "connected")
3446
4358
  continue;
3447
4359
  for (const tool of server.tools) {
3448
- const namespacedName = `mcp__${serverName}__${tool.name}`;
4360
+ const config = this.configs[serverName];
4361
+ const prefix = "toolPrefix" in config ? config.toolPrefix : serverName;
4362
+ const namespacedName = prefix === "" ? tool.name : `${prefix}__${tool.name}`;
3449
4363
  result.push({
3450
4364
  name: namespacedName,
3451
4365
  description: tool.description ?? `MCP tool ${tool.name} from ${serverName}`,
@@ -3478,64 +4392,294 @@ class McpClientManager {
3478
4392
  return { content: `MCP tool error: ${message}`, isError: true };
3479
4393
  }
3480
4394
  }
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")
4395
+ async listResources(serverName) {
4396
+ const result = [];
4397
+ const serversToQuery = serverName ? [this.servers.get(serverName)].filter(Boolean) : [...this.servers.values()];
4398
+ for (const server of serversToQuery) {
4399
+ if (server.status.status !== "connected")
4400
+ continue;
4401
+ try {
4402
+ const resources = await server.client.listResources();
4403
+ for (const r of resources.resources ?? []) {
4404
+ result.push({
4405
+ uri: r.uri,
4406
+ name: r.name,
4407
+ description: r.description,
4408
+ mimeType: r.mimeType,
4409
+ server: server.name
4410
+ });
4411
+ }
4412
+ } catch {}
4413
+ }
4414
+ return result;
4415
+ }
4416
+ async readResource(serverName, uri) {
4417
+ const server = this.servers.get(serverName);
4418
+ if (!server || server.status.status !== "connected") {
4419
+ throw new Error(`MCP server "${serverName}" is not connected`);
4420
+ }
4421
+ const result = await server.client.readResource({ uri });
4422
+ const contents = result.contents ?? [];
4423
+ return contents.map((c) => {
4424
+ if ("text" in c)
4425
+ return c.text;
4426
+ if ("blob" in c)
4427
+ return `[binary data: ${c.mimeType ?? "unknown"}]`;
4428
+ return "";
4429
+ }).join("");
4430
+ }
4431
+ status() {
4432
+ const result = [];
4433
+ for (const [name, config] of Object.entries(this.configs)) {
4434
+ const server = this.servers.get(name);
4435
+ if (server) {
4436
+ result.push(server.status);
4437
+ } else if (this.disabled.has(name)) {
4438
+ result.push({
4439
+ name,
4440
+ status: "disabled",
4441
+ config: this.toStatusConfig(config),
4442
+ scope: config.type === "sdk" ? "sdk" : "session"
4443
+ });
4444
+ } else {
4445
+ result.push({
4446
+ name,
4447
+ status: "pending",
4448
+ config: this.toStatusConfig(config),
4449
+ scope: config.type === "sdk" ? "sdk" : "session"
4450
+ });
4451
+ }
4452
+ }
4453
+ return result;
4454
+ }
4455
+ async reconnectServer(serverName) {
4456
+ const config = this.configs[serverName];
4457
+ if (!config) {
4458
+ throw new Error(`MCP server "${serverName}" is not configured`);
4459
+ }
4460
+ await this.closeOne(serverName);
4461
+ await this.connectOne(serverName, config);
4462
+ const status = this.servers.get(serverName)?.status;
4463
+ if (!status || status.status !== "connected") {
4464
+ throw new Error(status?.error ?? `Failed to reconnect MCP server "${serverName}"`);
4465
+ }
4466
+ }
4467
+ async toggleServer(serverName, enabled) {
4468
+ const config = this.configs[serverName];
4469
+ if (!config) {
4470
+ throw new Error(`MCP server "${serverName}" is not configured`);
4471
+ }
4472
+ if (!enabled) {
4473
+ this.disabled.add(serverName);
4474
+ await this.closeOne(serverName);
4475
+ this.servers.set(serverName, {
4476
+ name: serverName,
4477
+ client: null,
4478
+ tools: [],
4479
+ status: {
4480
+ name: serverName,
4481
+ status: "disabled",
4482
+ config: this.toStatusConfig(config),
4483
+ scope: config.type === "sdk" ? "sdk" : "session"
4484
+ }
4485
+ });
4486
+ return;
4487
+ }
4488
+ this.disabled.delete(serverName);
4489
+ await this.reconnectServer(serverName);
4490
+ }
4491
+ async setServers(servers) {
4492
+ const prevNames = new Set(Object.keys(this.configs));
4493
+ const nextNames = new Set(Object.keys(servers));
4494
+ const added = [...nextNames].filter((n) => !prevNames.has(n));
4495
+ const removed = [...prevNames].filter((n) => !nextNames.has(n));
4496
+ const errors = {};
4497
+ for (const name of removed) {
4498
+ await this.closeOne(name);
4499
+ delete this.configs[name];
4500
+ this.disabled.delete(name);
4501
+ this.servers.delete(name);
4502
+ }
4503
+ for (const [name, config] of Object.entries(servers)) {
4504
+ const prev = this.configs[name];
4505
+ this.configs[name] = config;
4506
+ if (this.disabled.has(name))
4507
+ continue;
4508
+ if (!prev || JSON.stringify(this.toStatusConfig(prev)) !== JSON.stringify(this.toStatusConfig(config))) {
4509
+ await this.closeOne(name);
4510
+ await this.connectOne(name, config);
4511
+ }
4512
+ const status = this.servers.get(name)?.status;
4513
+ if (!status || status.status === "failed") {
4514
+ errors[name] = status?.error ?? "Failed to connect";
4515
+ }
4516
+ }
4517
+ return { added, removed, errors };
4518
+ }
4519
+ async closeOne(serverName) {
4520
+ const existing = this.servers.get(serverName);
4521
+ if (existing?.client) {
4522
+ try {
4523
+ await existing.client.close();
4524
+ } catch {}
4525
+ }
4526
+ }
4527
+ async closeAll() {
4528
+ for (const [name] of this.servers) {
4529
+ await this.closeOne(name);
4530
+ }
4531
+ this.servers.clear();
4532
+ }
4533
+ }
4534
+
4535
+ // src/utils/session-store.ts
4536
+ import { readFileSync as readFileSync6, appendFileSync, mkdirSync as mkdirSync3, readdirSync as readdirSync2, statSync as statSync2 } from "fs";
4537
+ import { join as join8 } from "path";
4538
+ import { homedir as homedir5 } from "os";
4539
+ function safeStringify(value) {
4540
+ try {
4541
+ return JSON.stringify(value);
4542
+ } catch {
4543
+ return String(value);
4544
+ }
4545
+ }
4546
+ function sanitizeCwd(cwd) {
4547
+ return cwd.replace(/[/.]/g, "-");
4548
+ }
4549
+ function sessionsDir(cwd) {
4550
+ return join8(homedir5(), ".claude", "projects", sanitizeCwd(cwd));
4551
+ }
4552
+ function ensureDir(dir) {
4553
+ mkdirSync3(dir, { recursive: true });
4554
+ }
4555
+ function logMessage(dir, sessionId, entry) {
4556
+ ensureDir(dir);
4557
+ const filePath = join8(dir, `${sessionId}.jsonl`);
4558
+ appendFileSync(filePath, JSON.stringify(entry) + `
4559
+ `);
4560
+ }
4561
+ function createSessionLogger(cwd, sessionId, model) {
4562
+ const dir = sessionsDir(cwd);
4563
+ let lastUuid = null;
4564
+ return (role, content, parentUuid) => {
4565
+ const entryUuid = uuid();
4566
+ let normalizedContent = content;
4567
+ if (role === "user" && typeof content === "string") {
4568
+ normalizedContent = [{ type: "text", text: content }];
4569
+ }
4570
+ const entry = {
4571
+ type: role,
4572
+ uuid: entryUuid,
4573
+ parentUuid: parentUuid ?? lastUuid,
4574
+ sessionId,
4575
+ timestamp: new Date().toISOString(),
4576
+ cwd,
4577
+ isSidechain: false,
4578
+ userType: "external",
4579
+ message: {
4580
+ role,
4581
+ content: normalizedContent,
4582
+ ...role === "assistant" && model ? { model } : {}
4583
+ },
4584
+ ...role === "user" ? { permissionMode: "default" } : {}
4585
+ };
4586
+ logMessage(dir, sessionId, entry);
4587
+ lastUuid = entryUuid;
4588
+ return entryUuid;
4589
+ };
4590
+ }
4591
+ function findLatestSession(cwd) {
4592
+ const dir = sessionsDir(cwd);
4593
+ try {
4594
+ const files = readdirSync2(dir).filter((f) => f.endsWith(".jsonl")).map((f) => {
4595
+ const filePath = join8(dir, f);
4596
+ try {
4597
+ return { name: f, mtime: statSync2(filePath).mtimeMs };
4598
+ } catch {
4599
+ return null;
4600
+ }
4601
+ }).filter((f) => f !== null).sort((a, b) => b.mtime - a.mtime);
4602
+ if (files.length === 0)
4603
+ return null;
4604
+ return files[0].name.replace(/\.jsonl$/, "");
4605
+ } catch {
4606
+ return null;
4607
+ }
4608
+ }
4609
+ function loadSessionMessages(cwd, sessionId, resumeSessionAt) {
4610
+ const dir = sessionsDir(cwd);
4611
+ const filePath = join8(dir, `${sessionId}.jsonl`);
4612
+ let lines;
4613
+ try {
4614
+ lines = readFileSync6(filePath, "utf-8").trim().split(`
4615
+ `).filter(Boolean);
4616
+ } catch {
4617
+ return [];
4618
+ }
4619
+ const messages = [];
4620
+ let reachedResumePoint = false;
4621
+ for (const line of lines) {
4622
+ if (resumeSessionAt && reachedResumePoint)
4623
+ break;
4624
+ try {
4625
+ const entry = JSON.parse(line);
4626
+ if (entry.type !== "user" && entry.type !== "assistant")
3486
4627
  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
4628
+ if (entry.isMeta === true)
4629
+ continue;
4630
+ const message = entry.message;
4631
+ if (!message)
4632
+ continue;
4633
+ const role = entry.type === "user" ? "user" : "assistant";
4634
+ let content;
4635
+ if (typeof message.content === "string") {
4636
+ content = message.content;
4637
+ } else if (Array.isArray(message.content)) {
4638
+ const normalizedBlocks = [];
4639
+ for (const c of message.content) {
4640
+ if (!c || typeof c !== "object")
4641
+ continue;
4642
+ const block = c;
4643
+ if (typeof block.type !== "string")
4644
+ continue;
4645
+ if (block.type === "text" && typeof block.text === "string") {
4646
+ normalizedBlocks.push({ type: "text", text: block.text });
4647
+ continue;
4648
+ }
4649
+ if (block.type === "tool_use" && typeof block.id === "string" && typeof block.name === "string") {
4650
+ normalizedBlocks.push({
4651
+ type: "tool_use",
4652
+ id: block.id,
4653
+ name: block.name,
4654
+ input: block.input ?? {}
4655
+ });
4656
+ continue;
4657
+ }
4658
+ if (block.type === "tool_result" && typeof block.tool_use_id === "string") {
4659
+ normalizedBlocks.push({
4660
+ type: "tool_result",
4661
+ tool_use_id: block.tool_use_id,
4662
+ content: typeof block.content === "string" ? block.content : safeStringify(block.content),
4663
+ is_error: typeof block.is_error === "boolean" ? block.is_error : undefined
4664
+ });
4665
+ continue;
4666
+ }
4667
+ normalizedBlocks.push({
4668
+ type: "text",
4669
+ text: `[session:${block.type}] ${safeStringify(block)}`
3496
4670
  });
3497
4671
  }
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);
4672
+ content = normalizedBlocks;
3523
4673
  } else {
3524
- result.push({ name, status: "pending" });
4674
+ continue;
3525
4675
  }
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 {}
4676
+ messages.push({ role, content });
4677
+ if (resumeSessionAt && entry.uuid === resumeSessionAt) {
4678
+ reachedResumePoint = true;
3535
4679
  }
3536
- }
3537
- this.servers.clear();
4680
+ } catch {}
3538
4681
  }
4682
+ return messages;
3539
4683
  }
3540
4684
 
3541
4685
  // src/agents/tools.ts
@@ -3558,6 +4702,15 @@ function createTaskTool(ctx) {
3558
4702
  type: "string",
3559
4703
  description: "The type of agent to use. Must match a registered agent definition."
3560
4704
  },
4705
+ model: {
4706
+ type: "string",
4707
+ enum: ["sonnet", "opus", "haiku"],
4708
+ description: "Optional model family hint for this subagent."
4709
+ },
4710
+ resume: {
4711
+ type: "string",
4712
+ description: "Optional session ID to resume this subagent from."
4713
+ },
3561
4714
  run_in_background: {
3562
4715
  type: "boolean",
3563
4716
  description: "If true, run the task in the background and return a task ID."
@@ -3565,16 +4718,35 @@ function createTaskTool(ctx) {
3565
4718
  max_turns: {
3566
4719
  type: "number",
3567
4720
  description: "Maximum number of turns for the subagent."
4721
+ },
4722
+ name: {
4723
+ type: "string",
4724
+ description: "Optional display name for the spawned subagent."
4725
+ },
4726
+ team_name: {
4727
+ type: "string",
4728
+ description: "Optional team name context for this subagent."
4729
+ },
4730
+ mode: {
4731
+ type: "string",
4732
+ enum: ["acceptEdits", "bypassPermissions", "default", "delegate", "dontAsk", "plan"],
4733
+ description: "Permission mode hint for the spawned subagent."
3568
4734
  }
3569
4735
  },
3570
- required: ["prompt", "subagent_type"]
4736
+ required: ["description", "prompt", "subagent_type"]
3571
4737
  },
3572
4738
  async execute(input, toolCtx) {
3573
4739
  const {
4740
+ description,
3574
4741
  prompt,
3575
4742
  subagent_type,
4743
+ model: requestedModel,
4744
+ resume,
3576
4745
  run_in_background,
3577
- max_turns
4746
+ max_turns,
4747
+ name,
4748
+ team_name,
4749
+ mode
3578
4750
  } = input;
3579
4751
  const agentDef = ctx.agents[subagent_type];
3580
4752
  if (!agentDef) {
@@ -3588,28 +4760,53 @@ function createTaskTool(ctx) {
3588
4760
  await ctx.parentHooks.fire("SubagentStart", { event: "SubagentStart", agent_type: subagent_type, session_id: toolCtx.sessionId }, undefined, { signal: toolCtx.signal });
3589
4761
  }
3590
4762
  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
- }
4763
+ const modelAliases = {
4764
+ sonnet: "claude-sonnet-4-5-20250929",
4765
+ opus: "claude-opus-4-5-20251101",
4766
+ haiku: "claude-haiku-4-5-20251001"
4767
+ };
4768
+ const model = requestedModel ? modelAliases[requestedModel] : agentDef.model ?? ctx.parentModel;
4769
+ const baseTools = agentDef.tools ?? resolveToolNames({ type: "preset", preset: "claude_code" });
4770
+ const subTools = buildToolRegistry(baseTools, undefined, agentDef.disallowedTools);
3598
4771
  const maxTurns = max_turns ?? agentDef.maxTurns ?? 10;
3599
- const sessionId = uuid();
4772
+ const sessionId = resume ?? uuid();
4773
+ const previousMessages = resume ? loadSessionMessages(ctx.parentCwd, resume) : undefined;
3600
4774
  const abortController = new AbortController;
3601
4775
  if (toolCtx.signal) {
3602
4776
  toolCtx.signal.addEventListener("abort", () => abortController.abort(), { once: true });
3603
4777
  }
3604
- const systemPrompt = `${agentDef.prompt}
4778
+ const systemPromptParts = [
4779
+ agentDef.prompt,
4780
+ `You are a subagent of type "${subagent_type}". ${agentDef.description}`,
4781
+ `Task summary: ${description}`
4782
+ ];
4783
+ if (agentDef.criticalSystemReminder_EXPERIMENTAL) {
4784
+ systemPromptParts.push(`Critical reminder: ${agentDef.criticalSystemReminder_EXPERIMENTAL}`);
4785
+ }
4786
+ if (agentDef.skills && agentDef.skills.length > 0) {
4787
+ systemPromptParts.push(`Available skills:
4788
+ ${agentDef.skills.map((s) => `- ${s}`).join(`
4789
+ `)}`);
4790
+ }
4791
+ if (name) {
4792
+ systemPromptParts.push(`Subagent name: ${name}`);
4793
+ }
4794
+ if (team_name) {
4795
+ systemPromptParts.push(`Team context: ${team_name}`);
4796
+ }
4797
+ if (mode) {
4798
+ systemPromptParts.push(`Permission mode hint: ${mode}`);
4799
+ }
4800
+ const systemPrompt = systemPromptParts.join(`
3605
4801
 
3606
- You are a subagent of type "${subagent_type}". ${agentDef.description}`;
4802
+ `);
3607
4803
  const runAgent = async () => {
3608
4804
  const messages = [];
3609
4805
  let resultText = "";
3610
4806
  for await (const msg of agentLoop(prompt, {
3611
4807
  provider,
3612
4808
  model,
4809
+ modelState: { current: model },
3613
4810
  systemPrompt,
3614
4811
  tools: subTools,
3615
4812
  permissions: ctx.parentPermissions,
@@ -3617,18 +4814,23 @@ You are a subagent of type "${subagent_type}". ${agentDef.description}`;
3617
4814
  sessionId,
3618
4815
  maxTurns,
3619
4816
  maxBudgetUsd: 5,
3620
- includeStreamEvents: false,
4817
+ includePartialMessages: false,
3621
4818
  signal: abortController.signal,
3622
4819
  env: ctx.parentEnv,
3623
4820
  debug: ctx.parentDebug,
3624
- hooks: ctx.parentHooks
4821
+ hooks: ctx.parentHooks,
4822
+ previousMessages
3625
4823
  })) {
3626
4824
  messages.push(msg);
3627
- if (msg.type === "text") {
3628
- resultText += msg.text;
4825
+ if (msg.type === "assistant") {
4826
+ for (const block of msg.message.content) {
4827
+ if (block.type === "text") {
4828
+ resultText += block.text;
4829
+ }
4830
+ }
3629
4831
  }
3630
4832
  if (msg.type === "result" && msg.subtype === "success") {
3631
- resultText = msg.text ?? resultText;
4833
+ resultText = msg.result || resultText;
3632
4834
  }
3633
4835
  }
3634
4836
  return resultText || "Subagent completed with no text output.";
@@ -3787,112 +4989,9 @@ class TaskManager {
3787
4989
  }
3788
4990
  }
3789
4991
 
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
4992
  // 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";
4993
+ import { readdir, stat, readFile as readFile3, writeFile as writeFile4, rm, rename, mkdir as mkdir4 } from "fs/promises";
4994
+ import { join as join9, resolve as resolve2, relative as relative2 } from "path";
3896
4995
  import { existsSync as existsSync5 } from "fs";
3897
4996
  function createMemoryHandler(memoryDir) {
3898
4997
  const absMemoryDir = resolve2(memoryDir);
@@ -3938,7 +5037,7 @@ function createMemoryHandler(memoryDir) {
3938
5037
  for (const entry of entries.sort((a, b) => a.name.localeCompare(b.name))) {
3939
5038
  if (entry.name.startsWith(".") || entry.name === "node_modules")
3940
5039
  continue;
3941
- const entryPath = join7(dirPath, entry.name);
5040
+ const entryPath = join9(dirPath, entry.name);
3942
5041
  const entryStat = await stat(entryPath);
3943
5042
  lines.push(`${formatSize(entryStat.size)} ${toLogicalPath(entryPath)}`);
3944
5043
  if (entry.isDirectory()) {
@@ -3981,7 +5080,7 @@ function createMemoryHandler(memoryDir) {
3981
5080
  ${lines.join(`
3982
5081
  `)}`;
3983
5082
  }
3984
- const content = await readFile(absPath, "utf-8");
5083
+ const content = await readFile3(absPath, "utf-8");
3985
5084
  const formatted = formatFileContent(content, cmd.view_range);
3986
5085
  return `Here's the content of ${cmd.path} with line numbers:
3987
5086
  ${formatted}`;
@@ -3992,8 +5091,8 @@ ${formatted}`;
3992
5091
  return `Error: File ${cmd.path} already exists`;
3993
5092
  }
3994
5093
  const parentDir = resolve2(absPath, "..");
3995
- await mkdir2(parentDir, { recursive: true });
3996
- await writeFile(absPath, cmd.file_text, "utf-8");
5094
+ await mkdir4(parentDir, { recursive: true });
5095
+ await writeFile4(absPath, cmd.file_text, "utf-8");
3997
5096
  return `File created successfully at: ${cmd.path}`;
3998
5097
  }
3999
5098
  async function handleStrReplace(cmd) {
@@ -4005,7 +5104,7 @@ ${formatted}`;
4005
5104
  if (s.isDirectory()) {
4006
5105
  return `Error: The path ${cmd.path} does not exist. Please provide a valid path.`;
4007
5106
  }
4008
- const content = await readFile(absPath, "utf-8");
5107
+ const content = await readFile3(absPath, "utf-8");
4009
5108
  const lines = content.split(`
4010
5109
  `);
4011
5110
  const matchingLines = [];
@@ -4028,7 +5127,7 @@ ${formatted}`;
4028
5127
  return `No replacement was performed. Multiple occurrences of old_str \`${cmd.old_str}\` in lines: ${matchingLines.join(", ")}. Please ensure it is unique`;
4029
5128
  }
4030
5129
  const newContent = content.replace(cmd.old_str, cmd.new_str);
4031
- await writeFile(absPath, newContent, "utf-8");
5130
+ await writeFile4(absPath, newContent, "utf-8");
4032
5131
  const newLines = newContent.split(`
4033
5132
  `);
4034
5133
  const replaceLine = matchingLines[0];
@@ -4048,7 +5147,7 @@ ${snippet}`;
4048
5147
  if (s.isDirectory()) {
4049
5148
  return `Error: The path ${cmd.path} does not exist`;
4050
5149
  }
4051
- const content = await readFile(absPath, "utf-8");
5150
+ const content = await readFile3(absPath, "utf-8");
4052
5151
  const lines = content.split(`
4053
5152
  `);
4054
5153
  if (cmd.insert_line < 0 || cmd.insert_line > lines.length) {
@@ -4057,7 +5156,7 @@ ${snippet}`;
4057
5156
  const insertLines = cmd.insert_text.split(`
4058
5157
  `);
4059
5158
  lines.splice(cmd.insert_line, 0, ...insertLines);
4060
- await writeFile(absPath, lines.join(`
5159
+ await writeFile4(absPath, lines.join(`
4061
5160
  `), "utf-8");
4062
5161
  return `The file ${cmd.path} has been edited.`;
4063
5162
  }
@@ -4079,13 +5178,13 @@ ${snippet}`;
4079
5178
  return `Error: The destination ${cmd.new_path} already exists`;
4080
5179
  }
4081
5180
  const parentDir = resolve2(newAbs, "..");
4082
- await mkdir2(parentDir, { recursive: true });
5181
+ await mkdir4(parentDir, { recursive: true });
4083
5182
  await rename(oldAbs, newAbs);
4084
5183
  return `Successfully renamed ${cmd.old_path} to ${cmd.new_path}`;
4085
5184
  }
4086
5185
  async function execute(cmd) {
4087
5186
  if (!existsSync5(absMemoryDir)) {
4088
- await mkdir2(absMemoryDir, { recursive: true });
5187
+ await mkdir4(absMemoryDir, { recursive: true });
4089
5188
  }
4090
5189
  switch (cmd.command) {
4091
5190
  case "view":
@@ -4190,19 +5289,41 @@ function createMemoryTool(config) {
4190
5289
  }
4191
5290
  };
4192
5291
  }
4193
-
4194
5292
  // src/api.ts
4195
5293
  var DEFAULT_MODEL = "claude-sonnet-4-5-20250929";
4196
5294
  var DEFAULT_MAX_TURNS = 10;
4197
5295
  var DEFAULT_MAX_BUDGET_USD = 5;
4198
5296
  function query(params) {
4199
- const { prompt, options = {} } = params;
5297
+ const { options = {} } = params;
5298
+ const prompt = params.prompt;
5299
+ if (typeof prompt !== "string") {
5300
+ throw new Error("query({ prompt: AsyncIterable }) is not supported in single-prompt mode yet. Use Query.streamInput() in a streaming session.");
5301
+ }
4200
5302
  const providerName = options.provider ?? "anthropic";
4201
5303
  const provider = getProvider(providerName, {
4202
5304
  apiKey: options.apiKey,
4203
5305
  baseUrl: options.baseUrl
4204
5306
  });
4205
5307
  const model = options.model ?? DEFAULT_MODEL;
5308
+ const fallbackModel = options.fallbackModel;
5309
+ const modelState = { current: model };
5310
+ const resolvedThinkingBudget = (() => {
5311
+ if (options.thinking) {
5312
+ switch (options.thinking.type) {
5313
+ case "disabled":
5314
+ return 0;
5315
+ case "enabled":
5316
+ return options.thinking.budgetTokens;
5317
+ case "adaptive":
5318
+ return;
5319
+ }
5320
+ }
5321
+ return options.maxThinkingTokens;
5322
+ })();
5323
+ const maxThinkingTokensState = { current: resolvedThinkingBudget };
5324
+ if (options.permissionMode === "bypassPermissions" && options.allowDangerouslySkipPermissions !== true) {
5325
+ throw new Error('permissionMode "bypassPermissions" requires allowDangerouslySkipPermissions: true');
5326
+ }
4206
5327
  const toolNames = resolveToolNames(options.tools);
4207
5328
  const registry = buildToolRegistry(toolNames, undefined, options.disallowedTools);
4208
5329
  let skills = [];
@@ -4219,12 +5340,22 @@ function query(params) {
4219
5340
  }
4220
5341
  }
4221
5342
  }
4222
- const systemPrompt = options.systemPrompt ?? buildSystemPrompt({
4223
- tools: registry.list(),
4224
- cwd: options.cwd,
4225
- customPrompt: options.appendSystemPrompt,
4226
- skills
4227
- });
5343
+ const systemPrompt = (() => {
5344
+ if (typeof options.systemPrompt === "string") {
5345
+ return options.systemPrompt;
5346
+ }
5347
+ const appendedPrompt = typeof options.systemPrompt === "object" ? [options.systemPrompt.append, options.appendSystemPrompt].filter(Boolean).join(`
5348
+
5349
+ `) : options.appendSystemPrompt;
5350
+ return buildSystemPrompt({
5351
+ tools: registry.list(),
5352
+ cwd: options.cwd,
5353
+ additionalDirectories: options.additionalDirectories,
5354
+ loadProjectInstructions: Array.isArray(options.settingSources) && options.settingSources.includes("project"),
5355
+ customPrompt: appendedPrompt,
5356
+ skills
5357
+ });
5358
+ })();
4228
5359
  let settingsManager;
4229
5360
  let mergedPermissions = options.permissions;
4230
5361
  if (options.settingSources && options.settingSources.length > 0) {
@@ -4270,7 +5401,7 @@ function query(params) {
4270
5401
  }
4271
5402
  }
4272
5403
  } else if (options.resume) {
4273
- previousMessages = loadSessionMessages(cwd, options.resume);
5404
+ previousMessages = loadSessionMessages(cwd, options.resume, options.resumeSessionAt);
4274
5405
  if (options.forkSession) {
4275
5406
  sessionId = uuid();
4276
5407
  } else {
@@ -4279,18 +5410,28 @@ function query(params) {
4279
5410
  }
4280
5411
  const persistSession = options.persistSession !== false;
4281
5412
  const sessionLogger = persistSession ? createSessionLogger(cwd, sessionId, model) : undefined;
4282
- const abortController = new AbortController;
5413
+ const abortController = options.abortController ?? new AbortController;
4283
5414
  if (options.signal) {
4284
5415
  options.signal.addEventListener("abort", () => abortController.abort(), { once: true });
4285
5416
  }
4286
5417
  const hookManager = options.hooks ? new HookManager(options.hooks) : undefined;
4287
5418
  const mcpClient = options.mcpServers && Object.keys(options.mcpServers).length > 0 ? new McpClientManager(options.mcpServers) : undefined;
5419
+ const syncMcpTools = () => {
5420
+ if (!mcpClient)
5421
+ return;
5422
+ registry.clearByPrefix("mcp__");
5423
+ for (const tool of mcpClient.getTools()) {
5424
+ registry.register(tool);
5425
+ }
5426
+ registry.register(createListMcpResourcesTool(mcpClient));
5427
+ registry.register(createReadMcpResourceTool(mcpClient));
5428
+ };
4288
5429
  if (options.agents && Object.keys(options.agents).length > 0) {
4289
5430
  const taskManager = new TaskManager;
4290
5431
  const agentCtx = {
4291
5432
  agents: options.agents,
4292
5433
  parentProvider: provider,
4293
- parentModel: model,
5434
+ parentModel: modelState.current,
4294
5435
  parentPermissions: permissions,
4295
5436
  parentHooks: hookManager,
4296
5437
  parentCwd: cwd,
@@ -4314,6 +5455,12 @@ function query(params) {
4314
5455
  const generator = agentLoop(prompt, {
4315
5456
  provider,
4316
5457
  model,
5458
+ modelState,
5459
+ maxThinkingTokensState,
5460
+ fallbackModel,
5461
+ thinking: options.thinking,
5462
+ effort: options.effort,
5463
+ outputFormat: options.outputFormat,
4317
5464
  systemPrompt,
4318
5465
  tools: registry,
4319
5466
  permissions,
@@ -4321,7 +5468,7 @@ function query(params) {
4321
5468
  sessionId,
4322
5469
  maxTurns: options.maxTurns ?? DEFAULT_MAX_TURNS,
4323
5470
  maxBudgetUsd: options.maxBudgetUsd ?? DEFAULT_MAX_BUDGET_USD,
4324
- includeStreamEvents: options.includeStreamEvents ?? false,
5471
+ includePartialMessages: options.includePartialMessages ?? false,
4325
5472
  signal: abortController.signal,
4326
5473
  env: options.env,
4327
5474
  debug: options.debug,
@@ -4329,9 +5476,95 @@ function query(params) {
4329
5476
  mcpClient,
4330
5477
  previousMessages,
4331
5478
  sessionLogger,
4332
- nativeMemoryTool
5479
+ nativeMemoryTool,
5480
+ initMeta: {
5481
+ betas: options.betas,
5482
+ outputStyle: "default",
5483
+ slashCommands: skills.map((s) => `/${s.name}`),
5484
+ skills: skills.map((s) => s.name),
5485
+ plugins: options.plugins,
5486
+ agents: options.agents ? Object.keys(options.agents) : undefined
5487
+ }
4333
5488
  });
4334
- return createQuery(generator, abortController);
5489
+ const controls = {
5490
+ async setPermissionMode(mode) {
5491
+ permissions.setMode(mode);
5492
+ },
5493
+ async setModel(nextModel) {
5494
+ modelState.current = nextModel ?? model;
5495
+ },
5496
+ async setMaxThinkingTokens(maxThinkingTokens) {
5497
+ maxThinkingTokensState.current = maxThinkingTokens ?? undefined;
5498
+ },
5499
+ async initializationResult() {
5500
+ const models = provider.listModels ? await provider.listModels() : [];
5501
+ return {
5502
+ commands: skills.map((s) => ({
5503
+ name: s.name,
5504
+ description: s.description,
5505
+ argumentHint: ""
5506
+ })),
5507
+ output_style: "default",
5508
+ available_output_styles: ["default"],
5509
+ models,
5510
+ account: {}
5511
+ };
5512
+ },
5513
+ async supportedCommands() {
5514
+ return skills.map((s) => ({
5515
+ name: s.name,
5516
+ description: s.description,
5517
+ argumentHint: ""
5518
+ }));
5519
+ },
5520
+ async supportedModels() {
5521
+ return provider.listModels ? await provider.listModels() : [];
5522
+ },
5523
+ async mcpServerStatus() {
5524
+ return mcpClient ? mcpClient.status() : [];
5525
+ },
5526
+ async accountInfo() {
5527
+ return {
5528
+ tokenSource: options.apiKey ? "api-key" : "runtime",
5529
+ apiKeySource: options.apiKey ? "explicit" : "env_or_oauth"
5530
+ };
5531
+ },
5532
+ async rewindFiles(_userMessageId, _options) {
5533
+ if (!options.enableFileCheckpointing) {
5534
+ return {
5535
+ canRewind: false,
5536
+ error: "File checkpointing is disabled. Set enableFileCheckpointing: true."
5537
+ };
5538
+ }
5539
+ return {
5540
+ canRewind: false,
5541
+ error: "File checkpoint rewind is not implemented in fourmis-agent-sdk yet."
5542
+ };
5543
+ },
5544
+ async reconnectMcpServer(serverName) {
5545
+ if (!mcpClient)
5546
+ throw new Error("No MCP servers are configured for this query.");
5547
+ await mcpClient.reconnectServer(serverName);
5548
+ syncMcpTools();
5549
+ },
5550
+ async toggleMcpServer(serverName, enabled) {
5551
+ if (!mcpClient)
5552
+ throw new Error("No MCP servers are configured for this query.");
5553
+ await mcpClient.toggleServer(serverName, enabled);
5554
+ syncMcpTools();
5555
+ },
5556
+ async setMcpServers(servers) {
5557
+ if (!mcpClient)
5558
+ throw new Error("No MCP client is available for this query.");
5559
+ const result = await mcpClient.setServers(servers);
5560
+ syncMcpTools();
5561
+ return result;
5562
+ },
5563
+ async streamInput() {
5564
+ throw new Error("Query.streamInput is not implemented for single-prompt query mode.");
5565
+ }
5566
+ };
5567
+ return createQuery(generator, abortController, controls);
4335
5568
  }
4336
5569
  export {
4337
5570
  query