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.
- package/README.md +126 -198
- package/dist/agent-loop.d.ts +21 -3
- package/dist/agent-loop.d.ts.map +1 -1
- package/dist/agent-loop.js +279 -90
- package/dist/agents/index.js +1079 -124
- package/dist/agents/tools.d.ts.map +1 -1
- package/dist/agents/tools.js +1079 -124
- package/dist/agents/types.d.ts +4 -0
- package/dist/agents/types.d.ts.map +1 -1
- package/dist/api.d.ts +8 -5
- package/dist/api.d.ts.map +1 -1
- package/dist/api.js +1663 -430
- package/dist/hooks.d.ts +19 -1
- package/dist/hooks.d.ts.map +1 -1
- package/dist/hooks.js +27 -2
- package/dist/index.d.ts +8 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1671 -431
- package/dist/mcp/client.d.ts +8 -1
- package/dist/mcp/client.d.ts.map +1 -1
- package/dist/mcp/client.js +134 -13
- package/dist/mcp/index.js +134 -13
- package/dist/mcp/types.d.ts +21 -1
- package/dist/mcp/types.d.ts.map +1 -1
- package/dist/permissions.d.ts.map +1 -1
- package/dist/permissions.js +7 -3
- package/dist/providers/anthropic.d.ts.map +1 -1
- package/dist/providers/anthropic.js +41 -2
- package/dist/providers/openai.d.ts +6 -0
- package/dist/providers/openai.d.ts.map +1 -1
- package/dist/providers/openai.js +36 -6
- package/dist/providers/registry.js +76 -8
- package/dist/providers/types.d.ts +4 -1
- package/dist/providers/types.d.ts.map +1 -1
- package/dist/query.d.ts +21 -2
- package/dist/query.d.ts.map +1 -1
- package/dist/query.js +69 -1
- package/dist/skills/index.js +23 -1
- package/dist/skills/skills.d.ts +16 -0
- package/dist/skills/skills.d.ts.map +1 -1
- package/dist/skills/skills.js +23 -1
- package/dist/tools/ask-user-question.d.ts +7 -0
- package/dist/tools/ask-user-question.d.ts.map +1 -0
- package/dist/tools/ask-user-question.js +48 -0
- package/dist/tools/bash.d.ts.map +1 -1
- package/dist/tools/bash.js +47 -2
- package/dist/tools/config.d.ts +7 -0
- package/dist/tools/config.d.ts.map +1 -0
- package/dist/tools/config.js +114 -0
- package/dist/tools/exit-plan-mode.d.ts +7 -0
- package/dist/tools/exit-plan-mode.d.ts.map +1 -0
- package/dist/tools/exit-plan-mode.js +34 -0
- package/dist/tools/index.d.ts +7 -0
- package/dist/tools/index.d.ts.map +1 -1
- package/dist/tools/index.js +506 -9
- package/dist/tools/notebook-edit.d.ts +7 -0
- package/dist/tools/notebook-edit.d.ts.map +1 -0
- package/dist/tools/notebook-edit.js +83 -0
- package/dist/tools/presets.d.ts +2 -1
- package/dist/tools/presets.d.ts.map +1 -1
- package/dist/tools/presets.js +22 -4
- package/dist/tools/read.d.ts.map +1 -1
- package/dist/tools/read.js +12 -1
- package/dist/tools/registry.d.ts +2 -0
- package/dist/tools/registry.d.ts.map +1 -1
- package/dist/tools/registry.js +10 -0
- package/dist/tools/todo-write.d.ts +7 -0
- package/dist/tools/todo-write.d.ts.map +1 -0
- package/dist/tools/todo-write.js +69 -0
- package/dist/tools/web-fetch.d.ts +6 -0
- package/dist/tools/web-fetch.d.ts.map +1 -0
- package/dist/tools/web-fetch.js +85 -0
- package/dist/tools/web-search.d.ts +7 -0
- package/dist/tools/web-search.d.ts.map +1 -0
- package/dist/tools/web-search.js +78 -0
- package/dist/types.d.ts +344 -42
- package/dist/types.d.ts.map +1 -1
- package/dist/utils/session-store.d.ts +1 -1
- package/dist/utils/session-store.d.ts.map +1 -1
- package/dist/utils/session-store.js +49 -2
- package/dist/utils/system-prompt.d.ts +2 -0
- package/dist/utils/system-prompt.d.ts.map +1 -1
- package/dist/utils/system-prompt.js +33 -4
- package/package.json +3 -2
package/dist/index.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
|
-
|
|
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: "
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
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("
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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 (
|
|
574
|
-
yield {
|
|
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 (
|
|
579
|
-
yield {
|
|
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
|
-
|
|
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(
|
|
753
|
+
const turnCost = provider.calculateCost(activeModel, turnUsage);
|
|
601
754
|
costUsd += turnCost;
|
|
602
|
-
if (!modelUsage[
|
|
603
|
-
modelUsage[
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
modelUsage[
|
|
612
|
-
modelUsage[
|
|
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
|
-
|
|
634
|
-
|
|
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", {
|
|
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", {
|
|
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
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
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
|
-
|
|
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", {
|
|
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
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
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", {
|
|
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", {
|
|
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.
|
|
2128
|
-
if (
|
|
2129
|
-
return
|
|
2409
|
+
return PRESETS.claude_code;
|
|
2410
|
+
if (Array.isArray(tools)) {
|
|
2411
|
+
return tools;
|
|
2130
2412
|
}
|
|
2131
|
-
|
|
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 {
|
|
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: {
|
|
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/
|
|
2652
|
-
|
|
2653
|
-
|
|
2654
|
-
|
|
2655
|
-
|
|
2656
|
-
|
|
2657
|
-
|
|
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
|
-
|
|
2675
|
-
|
|
2676
|
-
|
|
2677
|
-
|
|
2678
|
-
|
|
2679
|
-
|
|
2680
|
-
|
|
2681
|
-
|
|
2682
|
-
|
|
2683
|
-
|
|
2684
|
-
|
|
2685
|
-
|
|
2686
|
-
|
|
2687
|
-
|
|
2688
|
-
|
|
2689
|
-
|
|
2690
|
-
|
|
2691
|
-
|
|
2692
|
-
|
|
2693
|
-
|
|
2694
|
-
|
|
2695
|
-
|
|
2696
|
-
|
|
2697
|
-
|
|
2698
|
-
|
|
2699
|
-
|
|
2700
|
-
|
|
2701
|
-
|
|
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
|
-
|
|
2705
|
-
|
|
2706
|
-
|
|
2707
|
-
if (
|
|
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
|
-
|
|
2710
|
-
|
|
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
|
-
|
|
2714
|
-
|
|
2715
|
-
|
|
2716
|
-
|
|
2717
|
-
|
|
2718
|
-
|
|
2719
|
-
|
|
2720
|
-
}
|
|
2721
|
-
|
|
2722
|
-
|
|
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
|
-
|
|
2733
|
-
|
|
2734
|
-
|
|
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(/&/g, "&").replace(/"/g, '"').replace(/'/g, "'").replace(/</g, "<").replace(/>/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
|
|
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 =
|
|
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
|
|
3632
|
+
return join5(homedir3(), ".claude", "settings.json");
|
|
2882
3633
|
case "project":
|
|
2883
|
-
return
|
|
3634
|
+
return join5(this.cwd, ".claude", "settings.json");
|
|
2884
3635
|
case "local":
|
|
2885
|
-
return
|
|
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
|
|
3642
|
+
return join5(homedir3(), ".claude", "settings.json");
|
|
2892
3643
|
case "projectSettings":
|
|
2893
|
-
return
|
|
3644
|
+
return join5(this.cwd, ".claude", "settings.json");
|
|
2894
3645
|
case "localSettings":
|
|
2895
|
-
return
|
|
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
|
|
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 =
|
|
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 =
|
|
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
|
|
3850
|
+
return join6(homedir4(), trimmed.slice(2));
|
|
3081
3851
|
if (trimmed.startsWith("~"))
|
|
3082
|
-
return
|
|
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 =
|
|
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
|
|
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
|
-
|
|
3254
|
-
|
|
3255
|
-
|
|
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(
|
|
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
|
|
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
|
|
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: {
|
|
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: {
|
|
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
|
|
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
|
-
|
|
3488
|
-
|
|
3489
|
-
|
|
3490
|
-
|
|
3491
|
-
|
|
3492
|
-
|
|
3493
|
-
|
|
3494
|
-
|
|
3495
|
-
|
|
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
|
-
|
|
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
|
-
|
|
4674
|
+
continue;
|
|
3525
4675
|
}
|
|
3526
|
-
|
|
3527
|
-
|
|
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
|
|
3592
|
-
|
|
3593
|
-
|
|
3594
|
-
|
|
3595
|
-
}
|
|
3596
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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 === "
|
|
3628
|
-
|
|
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.
|
|
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
|
|
3895
|
-
import { join as
|
|
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 =
|
|
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
|
|
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
|
|
3996
|
-
await
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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 {
|
|
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 =
|
|
4223
|
-
|
|
4224
|
-
|
|
4225
|
-
|
|
4226
|
-
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
|
4337
5570
|
// src/mcp/server.ts
|
|
@@ -4373,16 +5606,23 @@ export {
|
|
|
4373
5606
|
createMemoryHandler,
|
|
4374
5607
|
createMcpServer,
|
|
4375
5608
|
WriteTool,
|
|
5609
|
+
WebSearchTool,
|
|
5610
|
+
WebFetchTool,
|
|
4376
5611
|
ToolRegistry,
|
|
5612
|
+
TodoWriteTool,
|
|
4377
5613
|
TaskManager,
|
|
4378
5614
|
SettingsManager,
|
|
4379
5615
|
ReadTool,
|
|
4380
5616
|
PRESETS,
|
|
5617
|
+
NotebookEditTool,
|
|
4381
5618
|
McpClientManager,
|
|
4382
5619
|
HookManager,
|
|
4383
5620
|
HOOK_EVENTS,
|
|
4384
5621
|
GrepTool,
|
|
4385
5622
|
GlobTool,
|
|
5623
|
+
ExitPlanModeTool,
|
|
4386
5624
|
EditTool,
|
|
4387
|
-
|
|
5625
|
+
ConfigTool,
|
|
5626
|
+
BashTool,
|
|
5627
|
+
AskUserQuestionTool
|
|
4388
5628
|
};
|