@sandagent/runner-cli 0.2.24 → 0.5.1-beta.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +201 -0
- package/README.md +17 -33
- package/dist/__tests__/build-image.test.js +1 -1
- package/dist/__tests__/build-image.test.js.map +1 -1
- package/dist/__tests__/runner-cli.integration.test.js +23 -4
- package/dist/__tests__/runner-cli.integration.test.js.map +1 -1
- package/dist/__tests__/runner.test.js +38 -0
- package/dist/__tests__/runner.test.js.map +1 -1
- package/dist/bundle.mjs +795 -93
- package/dist/cli.js +14 -14
- package/dist/cli.js.map +1 -1
- package/dist/runner.d.ts +2 -2
- package/dist/runner.d.ts.map +1 -1
- package/dist/runner.js +52 -12
- package/dist/runner.js.map +1 -1
- package/package.json +28 -14
package/dist/bundle.mjs
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// src/cli.ts
|
|
4
|
+
import { resolve as resolve2 } from "node:path";
|
|
5
|
+
import { config } from "dotenv";
|
|
4
6
|
import { parseArgs } from "node:util";
|
|
5
7
|
|
|
6
8
|
// src/build-image.ts
|
|
@@ -372,7 +374,7 @@ var AISDKStreamConverter = class {
|
|
|
372
374
|
const userMsg = message;
|
|
373
375
|
const content = userMsg.message?.content;
|
|
374
376
|
for (const part of content) {
|
|
375
|
-
if (part.type === "tool_result") {
|
|
377
|
+
if (typeof part !== "string" && part.type === "tool_result") {
|
|
376
378
|
yield this.emit({
|
|
377
379
|
type: "tool-output-available",
|
|
378
380
|
toolCallId: part.tool_use_id,
|
|
@@ -501,7 +503,7 @@ function createCanUseToolCallback(claudeOptions) {
|
|
|
501
503
|
}
|
|
502
504
|
} catch {
|
|
503
505
|
}
|
|
504
|
-
await new Promise((
|
|
506
|
+
await new Promise((resolve3) => setTimeout(resolve3, 500));
|
|
505
507
|
}
|
|
506
508
|
try {
|
|
507
509
|
fs.unlinkSync(approvalFile);
|
|
@@ -577,22 +579,7 @@ async function loadClaudeAgentSDK() {
|
|
|
577
579
|
}
|
|
578
580
|
}
|
|
579
581
|
async function* runWithClaudeAgentSDK(sdk, options, userInput) {
|
|
580
|
-
|
|
581
|
-
switch (outputFormat) {
|
|
582
|
-
case "text":
|
|
583
|
-
yield* runWithTextOutput(sdk, options, userInput);
|
|
584
|
-
break;
|
|
585
|
-
case "json":
|
|
586
|
-
yield* runWithJSONOutput(sdk, options, userInput);
|
|
587
|
-
break;
|
|
588
|
-
case "stream-json":
|
|
589
|
-
yield* runWithStreamJSONOutput(sdk, options, userInput);
|
|
590
|
-
break;
|
|
591
|
-
// case "stream":
|
|
592
|
-
default:
|
|
593
|
-
yield* runWithAISDKUIOutput(sdk, options, userInput);
|
|
594
|
-
break;
|
|
595
|
-
}
|
|
582
|
+
yield* runWithAISDKUIOutput(sdk, options, userInput);
|
|
596
583
|
}
|
|
597
584
|
function createSDKOptions(options) {
|
|
598
585
|
const isRoot = typeof process.getuid === "function" && process.getuid() === 0;
|
|
@@ -638,55 +625,6 @@ function setupAbortHandler(queryIterator, signal) {
|
|
|
638
625
|
}
|
|
639
626
|
};
|
|
640
627
|
}
|
|
641
|
-
async function* runWithTextOutput(sdk, options, userInput) {
|
|
642
|
-
const sdkOptions = createSDKOptions(options);
|
|
643
|
-
const queryIterator = sdk.query({ prompt: userInput, options: sdkOptions });
|
|
644
|
-
const cleanup = setupAbortHandler(queryIterator, options.abortController?.signal);
|
|
645
|
-
try {
|
|
646
|
-
let resultText = "";
|
|
647
|
-
for await (const message of queryIterator) {
|
|
648
|
-
if (message.type === "result") {
|
|
649
|
-
const resultMsg = message;
|
|
650
|
-
if (resultMsg.subtype === "success") {
|
|
651
|
-
resultText = resultMsg.result || "";
|
|
652
|
-
}
|
|
653
|
-
}
|
|
654
|
-
}
|
|
655
|
-
yield resultText;
|
|
656
|
-
} finally {
|
|
657
|
-
cleanup();
|
|
658
|
-
}
|
|
659
|
-
}
|
|
660
|
-
async function* runWithJSONOutput(sdk, options, userInput) {
|
|
661
|
-
const sdkOptions = createSDKOptions(options);
|
|
662
|
-
const queryIterator = sdk.query({ prompt: userInput, options: sdkOptions });
|
|
663
|
-
const cleanup = setupAbortHandler(queryIterator, options.abortController?.signal);
|
|
664
|
-
try {
|
|
665
|
-
let resultMessage = null;
|
|
666
|
-
for await (const message of queryIterator) {
|
|
667
|
-
if (message.type === "result") {
|
|
668
|
-
resultMessage = message;
|
|
669
|
-
}
|
|
670
|
-
}
|
|
671
|
-
if (resultMessage) {
|
|
672
|
-
yield JSON.stringify(resultMessage) + "\n";
|
|
673
|
-
}
|
|
674
|
-
} finally {
|
|
675
|
-
cleanup();
|
|
676
|
-
}
|
|
677
|
-
}
|
|
678
|
-
async function* runWithStreamJSONOutput(sdk, options, userInput) {
|
|
679
|
-
const sdkOptions = createSDKOptions(options);
|
|
680
|
-
const queryIterator = sdk.query({ prompt: userInput, options: sdkOptions });
|
|
681
|
-
const cleanup = setupAbortHandler(queryIterator, options.abortController?.signal);
|
|
682
|
-
try {
|
|
683
|
-
for await (const message of queryIterator) {
|
|
684
|
-
yield JSON.stringify(message) + "\n";
|
|
685
|
-
}
|
|
686
|
-
} finally {
|
|
687
|
-
cleanup();
|
|
688
|
-
}
|
|
689
|
-
}
|
|
690
628
|
async function* runWithAISDKUIOutput(sdk, options, userInput) {
|
|
691
629
|
const sdkOptions = createSDKOptions({
|
|
692
630
|
...options,
|
|
@@ -733,7 +671,7 @@ Documentation: https://platform.claude.com/docs/en/agent-sdk/typescript-v2-previ
|
|
|
733
671
|
id: textId,
|
|
734
672
|
delta: word + " "
|
|
735
673
|
});
|
|
736
|
-
await new Promise((
|
|
674
|
+
await new Promise((resolve3) => setTimeout(resolve3, 20));
|
|
737
675
|
}
|
|
738
676
|
yield formatDataStream({ type: "text-end", id: textId });
|
|
739
677
|
yield formatDataStream({
|
|
@@ -769,6 +707,738 @@ Documentation: https://platform.claude.com/docs/en/agent-sdk/typescript-v2-previ
|
|
|
769
707
|
}
|
|
770
708
|
}
|
|
771
709
|
|
|
710
|
+
// ../../packages/runner-codex/dist/codex-runner.js
|
|
711
|
+
import { Codex } from "@openai/codex-sdk";
|
|
712
|
+
function normalizeCodexModel(model) {
|
|
713
|
+
const trimmed = model.trim();
|
|
714
|
+
const withoutProvider = trimmed.startsWith("openai:") ? trimmed.slice("openai:".length) : trimmed;
|
|
715
|
+
if (/^\d+(\.\d+)?$/.test(withoutProvider)) {
|
|
716
|
+
return `gpt-${withoutProvider}`;
|
|
717
|
+
}
|
|
718
|
+
return withoutProvider;
|
|
719
|
+
}
|
|
720
|
+
function stringifyUnknown(value) {
|
|
721
|
+
if (typeof value === "string") {
|
|
722
|
+
return value;
|
|
723
|
+
}
|
|
724
|
+
try {
|
|
725
|
+
return JSON.stringify(value);
|
|
726
|
+
} catch {
|
|
727
|
+
return String(value);
|
|
728
|
+
}
|
|
729
|
+
}
|
|
730
|
+
function toToolStartPayload(event) {
|
|
731
|
+
if (event.type !== "item.started") {
|
|
732
|
+
return null;
|
|
733
|
+
}
|
|
734
|
+
const item = event.item;
|
|
735
|
+
if (item.type === "command_execution") {
|
|
736
|
+
return {
|
|
737
|
+
toolCallId: item.id,
|
|
738
|
+
toolName: "shell",
|
|
739
|
+
args: { command: item.command }
|
|
740
|
+
};
|
|
741
|
+
}
|
|
742
|
+
if (item.type === "mcp_tool_call") {
|
|
743
|
+
return {
|
|
744
|
+
toolCallId: item.id,
|
|
745
|
+
toolName: `${item.server}:${item.tool}`,
|
|
746
|
+
args: item.arguments
|
|
747
|
+
};
|
|
748
|
+
}
|
|
749
|
+
if (item.type === "web_search") {
|
|
750
|
+
return {
|
|
751
|
+
toolCallId: item.id,
|
|
752
|
+
toolName: "web_search",
|
|
753
|
+
args: { query: item.query }
|
|
754
|
+
};
|
|
755
|
+
}
|
|
756
|
+
return null;
|
|
757
|
+
}
|
|
758
|
+
function toToolEndPayload(event) {
|
|
759
|
+
if (event.type !== "item.completed") {
|
|
760
|
+
return null;
|
|
761
|
+
}
|
|
762
|
+
const item = event.item;
|
|
763
|
+
if (item.type === "command_execution") {
|
|
764
|
+
return {
|
|
765
|
+
toolCallId: item.id,
|
|
766
|
+
result: {
|
|
767
|
+
status: item.status,
|
|
768
|
+
exitCode: item.exit_code,
|
|
769
|
+
output: item.aggregated_output
|
|
770
|
+
}
|
|
771
|
+
};
|
|
772
|
+
}
|
|
773
|
+
if (item.type === "mcp_tool_call") {
|
|
774
|
+
return {
|
|
775
|
+
toolCallId: item.id,
|
|
776
|
+
result: item.result ?? item.error ?? { status: item.status }
|
|
777
|
+
};
|
|
778
|
+
}
|
|
779
|
+
if (item.type === "web_search") {
|
|
780
|
+
return {
|
|
781
|
+
toolCallId: item.id,
|
|
782
|
+
result: { query: item.query }
|
|
783
|
+
};
|
|
784
|
+
}
|
|
785
|
+
return null;
|
|
786
|
+
}
|
|
787
|
+
function toAssistantText(event) {
|
|
788
|
+
if (event.type === "item.completed" && event.item.type === "agent_message") {
|
|
789
|
+
return event.item.text;
|
|
790
|
+
}
|
|
791
|
+
if (event.type === "item.completed" && event.item.type === "reasoning") {
|
|
792
|
+
return `[Reasoning] ${event.item.text}`;
|
|
793
|
+
}
|
|
794
|
+
if (event.type === "item.completed" && event.item.type === "error") {
|
|
795
|
+
return `[Error] ${event.item.message}`;
|
|
796
|
+
}
|
|
797
|
+
return null;
|
|
798
|
+
}
|
|
799
|
+
function createCodexRunner(options) {
|
|
800
|
+
const codex = new Codex({
|
|
801
|
+
apiKey: process.env.CODEX_API_KEY || process.env.OPENAI_API_KEY,
|
|
802
|
+
baseUrl: process.env.OPENAI_BASE_URL,
|
|
803
|
+
env: options.env
|
|
804
|
+
});
|
|
805
|
+
return {
|
|
806
|
+
async *run(userInput) {
|
|
807
|
+
const threadOptions = {
|
|
808
|
+
model: normalizeCodexModel(options.model),
|
|
809
|
+
sandboxMode: options.sandboxMode,
|
|
810
|
+
workingDirectory: options.cwd || process.cwd(),
|
|
811
|
+
skipGitRepoCheck: options.skipGitRepoCheck ?? true,
|
|
812
|
+
modelReasoningEffort: options.modelReasoningEffort,
|
|
813
|
+
networkAccessEnabled: options.networkAccessEnabled,
|
|
814
|
+
webSearchMode: options.webSearchMode,
|
|
815
|
+
approvalPolicy: options.approvalPolicy
|
|
816
|
+
};
|
|
817
|
+
const thread = options.resume ? codex.resumeThread(options.resume, threadOptions) : codex.startThread(threadOptions);
|
|
818
|
+
const streamedTurn = await thread.runStreamed(userInput, {
|
|
819
|
+
signal: options.abortController?.signal
|
|
820
|
+
});
|
|
821
|
+
for await (const event of streamedTurn.events) {
|
|
822
|
+
const assistantText = toAssistantText(event);
|
|
823
|
+
if (assistantText) {
|
|
824
|
+
yield `data: ${JSON.stringify({ type: "text-delta", delta: assistantText })}
|
|
825
|
+
|
|
826
|
+
`;
|
|
827
|
+
}
|
|
828
|
+
const toolStart = toToolStartPayload(event);
|
|
829
|
+
if (toolStart) {
|
|
830
|
+
yield `data: ${JSON.stringify({ type: "tool-input-start", toolCallId: toolStart.toolCallId, toolName: toolStart.toolName })}
|
|
831
|
+
|
|
832
|
+
`;
|
|
833
|
+
yield `data: ${JSON.stringify({ type: "tool-input-available", toolCallId: toolStart.toolCallId, toolName: toolStart.toolName, input: toolStart.args })}
|
|
834
|
+
|
|
835
|
+
`;
|
|
836
|
+
}
|
|
837
|
+
const toolEnd = toToolEndPayload(event);
|
|
838
|
+
if (toolEnd) {
|
|
839
|
+
yield `data: ${JSON.stringify({ type: "tool-output-available", toolCallId: toolEnd.toolCallId, output: toolEnd.result })}
|
|
840
|
+
|
|
841
|
+
`;
|
|
842
|
+
}
|
|
843
|
+
if (event.type === "turn.completed") {
|
|
844
|
+
yield `data: ${JSON.stringify({ type: "finish", finishReason: "stop", usage: event.usage })}
|
|
845
|
+
|
|
846
|
+
`;
|
|
847
|
+
yield `data: [DONE]
|
|
848
|
+
|
|
849
|
+
`;
|
|
850
|
+
}
|
|
851
|
+
if (event.type === "turn.failed") {
|
|
852
|
+
yield `data: ${JSON.stringify({ type: "error", errorText: event.error.message })}
|
|
853
|
+
|
|
854
|
+
`;
|
|
855
|
+
yield `data: ${JSON.stringify({ type: "finish", finishReason: "error" })}
|
|
856
|
+
|
|
857
|
+
`;
|
|
858
|
+
yield `data: [DONE]
|
|
859
|
+
|
|
860
|
+
`;
|
|
861
|
+
}
|
|
862
|
+
if (event.type === "error") {
|
|
863
|
+
yield `data: ${JSON.stringify({ type: "error", errorText: stringifyUnknown(event.message) })}
|
|
864
|
+
|
|
865
|
+
`;
|
|
866
|
+
yield `data: ${JSON.stringify({ type: "finish", finishReason: "error" })}
|
|
867
|
+
|
|
868
|
+
`;
|
|
869
|
+
yield `data: [DONE]
|
|
870
|
+
|
|
871
|
+
`;
|
|
872
|
+
}
|
|
873
|
+
}
|
|
874
|
+
}
|
|
875
|
+
};
|
|
876
|
+
}
|
|
877
|
+
|
|
878
|
+
// ../../packages/runner-gemini/dist/gemini-runner.js
|
|
879
|
+
import { spawn } from "node:child_process";
|
|
880
|
+
function createGeminiRunner(options = {}) {
|
|
881
|
+
const cwd = options.cwd || process.cwd();
|
|
882
|
+
let currentProcess = null;
|
|
883
|
+
return {
|
|
884
|
+
async *run(userInput) {
|
|
885
|
+
if (options.abortController?.signal.aborted) {
|
|
886
|
+
yield `data: ${JSON.stringify({ type: "error", errorText: "Run aborted before start." })}
|
|
887
|
+
|
|
888
|
+
`;
|
|
889
|
+
yield `data: ${JSON.stringify({ type: "finish", finishReason: "error" })}
|
|
890
|
+
|
|
891
|
+
`;
|
|
892
|
+
yield "data: [DONE]\n\n";
|
|
893
|
+
return;
|
|
894
|
+
}
|
|
895
|
+
const args = ["--experimental-acp"];
|
|
896
|
+
if (options.model)
|
|
897
|
+
args.push("--model", options.model);
|
|
898
|
+
let aborted = false;
|
|
899
|
+
let completed = false;
|
|
900
|
+
currentProcess = spawn("gemini", args, {
|
|
901
|
+
cwd,
|
|
902
|
+
env: { ...process.env, ...options.env },
|
|
903
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
904
|
+
});
|
|
905
|
+
if (!currentProcess.stdin || !currentProcess.stdout)
|
|
906
|
+
throw new Error("Failed to spawn gemini");
|
|
907
|
+
const abortSignal = options.abortController?.signal;
|
|
908
|
+
const abortHandler = () => {
|
|
909
|
+
aborted = true;
|
|
910
|
+
currentProcess?.kill();
|
|
911
|
+
};
|
|
912
|
+
if (abortSignal) {
|
|
913
|
+
abortSignal.addEventListener("abort", abortHandler);
|
|
914
|
+
}
|
|
915
|
+
let msgId = 1;
|
|
916
|
+
const send = (method, params, id) => {
|
|
917
|
+
const msg = JSON.stringify({
|
|
918
|
+
jsonrpc: "2.0",
|
|
919
|
+
method,
|
|
920
|
+
params,
|
|
921
|
+
...id ? { id } : {}
|
|
922
|
+
});
|
|
923
|
+
currentProcess.stdin.write(msg + "\n");
|
|
924
|
+
};
|
|
925
|
+
send("initialize", { protocolVersion: 1, clientCapabilities: {} }, msgId++);
|
|
926
|
+
let sessionId = null;
|
|
927
|
+
const messageId = `msg_${Date.now()}_${Math.random().toString(36).slice(2)}`;
|
|
928
|
+
const textId = `text_${Date.now()}_${Math.random().toString(36).slice(2)}`;
|
|
929
|
+
let hasStarted = false;
|
|
930
|
+
let hasTextStarted = false;
|
|
931
|
+
try {
|
|
932
|
+
let buffer = "";
|
|
933
|
+
for await (const chunk of currentProcess.stdout) {
|
|
934
|
+
buffer += chunk.toString();
|
|
935
|
+
const lines = buffer.split("\n");
|
|
936
|
+
buffer = lines.pop() || "";
|
|
937
|
+
for (const line of lines) {
|
|
938
|
+
if (!line.trim())
|
|
939
|
+
continue;
|
|
940
|
+
let msg;
|
|
941
|
+
try {
|
|
942
|
+
msg = JSON.parse(line);
|
|
943
|
+
} catch {
|
|
944
|
+
continue;
|
|
945
|
+
}
|
|
946
|
+
if (msg.id === 1 && msg.result) {
|
|
947
|
+
send("session/new", { cwd, mcpServers: [] }, msgId++);
|
|
948
|
+
}
|
|
949
|
+
if (msg.id === 2 && msg.result) {
|
|
950
|
+
const result = msg.result;
|
|
951
|
+
sessionId = result.sessionId;
|
|
952
|
+
send("session/prompt", {
|
|
953
|
+
sessionId,
|
|
954
|
+
prompt: [{ type: "text", text: userInput }]
|
|
955
|
+
}, msgId++);
|
|
956
|
+
}
|
|
957
|
+
if (msg.id === 3 && "result" in msg) {
|
|
958
|
+
if (hasTextStarted)
|
|
959
|
+
yield `data: ${JSON.stringify({ type: "text-end", id: textId })}
|
|
960
|
+
|
|
961
|
+
`;
|
|
962
|
+
yield `data: ${JSON.stringify({ type: "finish", finishReason: "stop" })}
|
|
963
|
+
|
|
964
|
+
`;
|
|
965
|
+
yield `data: [DONE]
|
|
966
|
+
|
|
967
|
+
`;
|
|
968
|
+
completed = true;
|
|
969
|
+
currentProcess.kill();
|
|
970
|
+
return;
|
|
971
|
+
}
|
|
972
|
+
if (msg.method === "session/update" && msg.params?.update) {
|
|
973
|
+
const update = msg.params.update;
|
|
974
|
+
if (!hasStarted) {
|
|
975
|
+
yield `data: ${JSON.stringify({ type: "start", messageId })}
|
|
976
|
+
|
|
977
|
+
`;
|
|
978
|
+
hasStarted = true;
|
|
979
|
+
}
|
|
980
|
+
if (update.sessionUpdate === "agent_message_chunk" && update.content?.type === "text" && update.content.text) {
|
|
981
|
+
if (!hasTextStarted) {
|
|
982
|
+
yield `data: ${JSON.stringify({ type: "text-start", id: textId })}
|
|
983
|
+
|
|
984
|
+
`;
|
|
985
|
+
hasTextStarted = true;
|
|
986
|
+
}
|
|
987
|
+
yield `data: ${JSON.stringify({ type: "text-delta", id: textId, delta: update.content.text })}
|
|
988
|
+
|
|
989
|
+
`;
|
|
990
|
+
}
|
|
991
|
+
}
|
|
992
|
+
}
|
|
993
|
+
}
|
|
994
|
+
if (!completed) {
|
|
995
|
+
const errorText = aborted ? "Gemini run aborted by signal." : "Gemini ACP process exited before completion.";
|
|
996
|
+
yield `data: ${JSON.stringify({ type: "error", errorText })}
|
|
997
|
+
|
|
998
|
+
`;
|
|
999
|
+
yield `data: ${JSON.stringify({ type: "finish", finishReason: "error" })}
|
|
1000
|
+
|
|
1001
|
+
`;
|
|
1002
|
+
yield "data: [DONE]\n\n";
|
|
1003
|
+
}
|
|
1004
|
+
} finally {
|
|
1005
|
+
if (abortSignal) {
|
|
1006
|
+
abortSignal.removeEventListener("abort", abortHandler);
|
|
1007
|
+
}
|
|
1008
|
+
currentProcess = null;
|
|
1009
|
+
}
|
|
1010
|
+
},
|
|
1011
|
+
abort() {
|
|
1012
|
+
currentProcess?.kill();
|
|
1013
|
+
currentProcess = null;
|
|
1014
|
+
}
|
|
1015
|
+
};
|
|
1016
|
+
}
|
|
1017
|
+
|
|
1018
|
+
// ../../packages/runner-opencode/dist/opencode-runner.js
|
|
1019
|
+
import { spawn as spawn2 } from "node:child_process";
|
|
1020
|
+
function createOpenCodeRunner(options = {}) {
|
|
1021
|
+
const cwd = options.cwd || process.cwd();
|
|
1022
|
+
let currentProcess = null;
|
|
1023
|
+
return {
|
|
1024
|
+
async *run(userInput) {
|
|
1025
|
+
if (options.abortController?.signal.aborted) {
|
|
1026
|
+
yield `data: ${JSON.stringify({ type: "error", errorText: "Run aborted before start." })}
|
|
1027
|
+
|
|
1028
|
+
`;
|
|
1029
|
+
yield `data: ${JSON.stringify({ type: "finish", finishReason: "error" })}
|
|
1030
|
+
|
|
1031
|
+
`;
|
|
1032
|
+
yield "data: [DONE]\n\n";
|
|
1033
|
+
return;
|
|
1034
|
+
}
|
|
1035
|
+
const args = ["acp"];
|
|
1036
|
+
if (options.model)
|
|
1037
|
+
args.push("--model", options.model);
|
|
1038
|
+
let aborted = false;
|
|
1039
|
+
let completed = false;
|
|
1040
|
+
currentProcess = spawn2("opencode", args, {
|
|
1041
|
+
cwd,
|
|
1042
|
+
env: { ...process.env, ...options.env },
|
|
1043
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
1044
|
+
});
|
|
1045
|
+
if (!currentProcess.stdin || !currentProcess.stdout)
|
|
1046
|
+
throw new Error("Failed to spawn opencode");
|
|
1047
|
+
const abortSignal = options.abortController?.signal;
|
|
1048
|
+
const abortHandler = () => {
|
|
1049
|
+
aborted = true;
|
|
1050
|
+
currentProcess?.kill();
|
|
1051
|
+
};
|
|
1052
|
+
if (abortSignal) {
|
|
1053
|
+
abortSignal.addEventListener("abort", abortHandler);
|
|
1054
|
+
}
|
|
1055
|
+
let msgId = 1;
|
|
1056
|
+
const send = (method, params, id) => {
|
|
1057
|
+
const msg = JSON.stringify({
|
|
1058
|
+
jsonrpc: "2.0",
|
|
1059
|
+
method,
|
|
1060
|
+
params,
|
|
1061
|
+
...id ? { id } : {}
|
|
1062
|
+
});
|
|
1063
|
+
currentProcess.stdin.write(msg + "\n");
|
|
1064
|
+
};
|
|
1065
|
+
send("initialize", { protocolVersion: 1, clientCapabilities: {} }, msgId++);
|
|
1066
|
+
let sessionId = null;
|
|
1067
|
+
const messageId = `msg_${Date.now()}_${Math.random().toString(36).slice(2)}`;
|
|
1068
|
+
const textId = `text_${Date.now()}_${Math.random().toString(36).slice(2)}`;
|
|
1069
|
+
let hasStarted = false;
|
|
1070
|
+
let hasTextStarted = false;
|
|
1071
|
+
try {
|
|
1072
|
+
let buffer = "";
|
|
1073
|
+
for await (const chunk of currentProcess.stdout) {
|
|
1074
|
+
buffer += chunk.toString();
|
|
1075
|
+
const lines = buffer.split("\n");
|
|
1076
|
+
buffer = lines.pop() || "";
|
|
1077
|
+
for (const line of lines) {
|
|
1078
|
+
if (!line.trim())
|
|
1079
|
+
continue;
|
|
1080
|
+
let msg;
|
|
1081
|
+
try {
|
|
1082
|
+
msg = JSON.parse(line);
|
|
1083
|
+
} catch {
|
|
1084
|
+
continue;
|
|
1085
|
+
}
|
|
1086
|
+
if (msg.id === 1 && msg.result) {
|
|
1087
|
+
send("session/new", { cwd, mcpServers: [] }, msgId++);
|
|
1088
|
+
}
|
|
1089
|
+
if (msg.id === 2 && msg.result) {
|
|
1090
|
+
const result = msg.result;
|
|
1091
|
+
sessionId = result.sessionId;
|
|
1092
|
+
send("session/prompt", {
|
|
1093
|
+
sessionId,
|
|
1094
|
+
prompt: [{ type: "text", text: userInput }]
|
|
1095
|
+
}, msgId++);
|
|
1096
|
+
}
|
|
1097
|
+
if (msg.id === 3 && "result" in msg) {
|
|
1098
|
+
if (hasTextStarted)
|
|
1099
|
+
yield `data: ${JSON.stringify({ type: "text-end", id: textId })}
|
|
1100
|
+
|
|
1101
|
+
`;
|
|
1102
|
+
yield `data: ${JSON.stringify({ type: "finish", finishReason: "stop" })}
|
|
1103
|
+
|
|
1104
|
+
`;
|
|
1105
|
+
yield `data: [DONE]
|
|
1106
|
+
|
|
1107
|
+
`;
|
|
1108
|
+
completed = true;
|
|
1109
|
+
currentProcess.kill();
|
|
1110
|
+
return;
|
|
1111
|
+
}
|
|
1112
|
+
if (msg.method === "session/update" && msg.params?.update) {
|
|
1113
|
+
const update = msg.params.update;
|
|
1114
|
+
if (!hasStarted) {
|
|
1115
|
+
yield `data: ${JSON.stringify({ type: "start", messageId })}
|
|
1116
|
+
|
|
1117
|
+
`;
|
|
1118
|
+
hasStarted = true;
|
|
1119
|
+
}
|
|
1120
|
+
if (update.sessionUpdate === "agent_message_chunk" && update.content?.type === "text" && update.content.text) {
|
|
1121
|
+
if (!hasTextStarted) {
|
|
1122
|
+
yield `data: ${JSON.stringify({ type: "text-start", id: textId })}
|
|
1123
|
+
|
|
1124
|
+
`;
|
|
1125
|
+
hasTextStarted = true;
|
|
1126
|
+
}
|
|
1127
|
+
yield `data: ${JSON.stringify({ type: "text-delta", id: textId, delta: update.content.text })}
|
|
1128
|
+
|
|
1129
|
+
`;
|
|
1130
|
+
}
|
|
1131
|
+
}
|
|
1132
|
+
}
|
|
1133
|
+
}
|
|
1134
|
+
if (!completed) {
|
|
1135
|
+
const errorText = aborted ? "OpenCode run aborted by signal." : "OpenCode ACP process exited before completion.";
|
|
1136
|
+
yield `data: ${JSON.stringify({ type: "error", errorText })}
|
|
1137
|
+
|
|
1138
|
+
`;
|
|
1139
|
+
yield `data: ${JSON.stringify({ type: "finish", finishReason: "error" })}
|
|
1140
|
+
|
|
1141
|
+
`;
|
|
1142
|
+
yield "data: [DONE]\n\n";
|
|
1143
|
+
}
|
|
1144
|
+
} finally {
|
|
1145
|
+
if (abortSignal) {
|
|
1146
|
+
abortSignal.removeEventListener("abort", abortHandler);
|
|
1147
|
+
}
|
|
1148
|
+
currentProcess = null;
|
|
1149
|
+
}
|
|
1150
|
+
},
|
|
1151
|
+
abort() {
|
|
1152
|
+
currentProcess?.kill();
|
|
1153
|
+
currentProcess = null;
|
|
1154
|
+
}
|
|
1155
|
+
};
|
|
1156
|
+
}
|
|
1157
|
+
|
|
1158
|
+
// ../../packages/runner-pi/dist/pi-runner.js
|
|
1159
|
+
import { appendFileSync as appendFileSync2, existsSync as existsSync3, unlinkSync as unlinkSync2 } from "node:fs";
|
|
1160
|
+
import { join as join3 } from "node:path";
|
|
1161
|
+
import { getModel } from "@mariozechner/pi-ai";
|
|
1162
|
+
import { SessionManager, createAgentSession } from "@mariozechner/pi-coding-agent";
|
|
1163
|
+
function parseModelSpec(model) {
|
|
1164
|
+
const trimmed = model.trim();
|
|
1165
|
+
const separator = trimmed.indexOf(":");
|
|
1166
|
+
if (separator <= 0 || separator === trimmed.length - 1) {
|
|
1167
|
+
throw new Error(`Invalid pi model "${model}". Expected format "<provider>:<model>", for example "google:gemini-2.5-pro".`);
|
|
1168
|
+
}
|
|
1169
|
+
return {
|
|
1170
|
+
provider: trimmed.slice(0, separator),
|
|
1171
|
+
modelName: trimmed.slice(separator + 1)
|
|
1172
|
+
};
|
|
1173
|
+
}
|
|
1174
|
+
function getEnvValue(optionsEnv, name) {
|
|
1175
|
+
return optionsEnv?.[name] ?? process.env[name];
|
|
1176
|
+
}
|
|
1177
|
+
function applyModelOverrides(model, provider, optionsEnv) {
|
|
1178
|
+
if (model == null)
|
|
1179
|
+
return;
|
|
1180
|
+
const openAiBaseUrl = getEnvValue(optionsEnv, "OPENAI_BASE_URL");
|
|
1181
|
+
const geminiBaseUrl = getEnvValue(optionsEnv, "GEMINI_BASE_URL");
|
|
1182
|
+
const anthropicBaseUrl = getEnvValue(optionsEnv, "ANTHROPIC_BASE_URL");
|
|
1183
|
+
if (provider === "openai" && openAiBaseUrl) {
|
|
1184
|
+
model.baseUrl = openAiBaseUrl;
|
|
1185
|
+
} else if (provider === "google" && geminiBaseUrl) {
|
|
1186
|
+
model.baseUrl = geminiBaseUrl;
|
|
1187
|
+
} else if (provider === "anthropic" && anthropicBaseUrl) {
|
|
1188
|
+
model.baseUrl = anthropicBaseUrl;
|
|
1189
|
+
}
|
|
1190
|
+
}
|
|
1191
|
+
function emitStreamError(errorText) {
|
|
1192
|
+
return [
|
|
1193
|
+
`data: ${JSON.stringify({ type: "error", errorText })}
|
|
1194
|
+
|
|
1195
|
+
`,
|
|
1196
|
+
`data: ${JSON.stringify({ type: "finish", finishReason: "error" })}
|
|
1197
|
+
|
|
1198
|
+
`,
|
|
1199
|
+
"data: [DONE]\n\n"
|
|
1200
|
+
];
|
|
1201
|
+
}
|
|
1202
|
+
function usageToMessageMetadata(usage) {
|
|
1203
|
+
return {
|
|
1204
|
+
input_tokens: usage.input,
|
|
1205
|
+
output_tokens: usage.output,
|
|
1206
|
+
cache_read_input_tokens: usage.cacheRead,
|
|
1207
|
+
cache_creation_input_tokens: usage.cacheWrite
|
|
1208
|
+
};
|
|
1209
|
+
}
|
|
1210
|
+
function getUsageFromAgentEndMessages(messages) {
|
|
1211
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
1212
|
+
const m = messages[i];
|
|
1213
|
+
if (m.role === "assistant" && m.usage != null) {
|
|
1214
|
+
return m.usage;
|
|
1215
|
+
}
|
|
1216
|
+
}
|
|
1217
|
+
return void 0;
|
|
1218
|
+
}
|
|
1219
|
+
function getErrorFromAgentEndMessages(messages) {
|
|
1220
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
1221
|
+
const m = messages[i];
|
|
1222
|
+
if (m.role === "assistant" && m.errorMessage) {
|
|
1223
|
+
return m.errorMessage;
|
|
1224
|
+
}
|
|
1225
|
+
}
|
|
1226
|
+
return void 0;
|
|
1227
|
+
}
|
|
1228
|
+
function traceRawMessage(debugCwd, data, reset = false) {
|
|
1229
|
+
const enabled = process.env.DEBUG === "true" || process.env.DEBUG === "1";
|
|
1230
|
+
if (!enabled)
|
|
1231
|
+
return;
|
|
1232
|
+
try {
|
|
1233
|
+
const file = join3(debugCwd, "pi-message-stream-debug.json");
|
|
1234
|
+
if (reset && existsSync3(file))
|
|
1235
|
+
unlinkSync2(file);
|
|
1236
|
+
const type = data !== null && typeof data === "object" ? data.type : void 0;
|
|
1237
|
+
let payload = data;
|
|
1238
|
+
try {
|
|
1239
|
+
payload = data !== void 0 ? JSON.parse(JSON.stringify(data)) : void 0;
|
|
1240
|
+
} catch {
|
|
1241
|
+
payload = "[non-serializable]";
|
|
1242
|
+
}
|
|
1243
|
+
const entry = { _t: (/* @__PURE__ */ new Date()).toISOString(), type, payload };
|
|
1244
|
+
appendFileSync2(file, JSON.stringify(entry, null, 2) + ",\n");
|
|
1245
|
+
} catch {
|
|
1246
|
+
}
|
|
1247
|
+
}
|
|
1248
|
+
function createPiRunner(options = {}) {
|
|
1249
|
+
const modelSpec = options.model;
|
|
1250
|
+
if (modelSpec == null || modelSpec.trim() === "") {
|
|
1251
|
+
throw new Error("Pi runner: model is required. Pass a model in the form <provider>:<model>, e.g. openai:gpt-4o or google:gemini-2.5-flash.");
|
|
1252
|
+
}
|
|
1253
|
+
const { provider, modelName } = parseModelSpec(modelSpec.trim());
|
|
1254
|
+
const cwd = options.cwd || process.cwd();
|
|
1255
|
+
const model = getModel(provider, modelName);
|
|
1256
|
+
if (model == null) {
|
|
1257
|
+
throw new Error(`Pi runner: unsupported model "${modelSpec}". getModel("${provider}", "${modelName}") returned undefined. Use a model from the pi-ai catalog; supported providers are typically: google, openai.`);
|
|
1258
|
+
}
|
|
1259
|
+
applyModelOverrides(model, provider, options.env);
|
|
1260
|
+
return {
|
|
1261
|
+
async *run(userInput) {
|
|
1262
|
+
const resume = options.sessionId?.trim();
|
|
1263
|
+
const sessionManager = await (async () => {
|
|
1264
|
+
if (resume !== void 0 && resume !== "") {
|
|
1265
|
+
if (resume.includes("/")) {
|
|
1266
|
+
return SessionManager.open(resume);
|
|
1267
|
+
}
|
|
1268
|
+
const sessions = await SessionManager.list(cwd);
|
|
1269
|
+
const found = sessions.find((s) => s.id === resume);
|
|
1270
|
+
return found ? SessionManager.open(found.path) : SessionManager.continueRecent(cwd);
|
|
1271
|
+
}
|
|
1272
|
+
return SessionManager.continueRecent(cwd);
|
|
1273
|
+
})();
|
|
1274
|
+
const { session } = await createAgentSession({
|
|
1275
|
+
cwd,
|
|
1276
|
+
model,
|
|
1277
|
+
sessionManager
|
|
1278
|
+
});
|
|
1279
|
+
if (options.systemPrompt != null && options.systemPrompt !== "") {
|
|
1280
|
+
session.agent.setSystemPrompt(options.systemPrompt);
|
|
1281
|
+
} else {
|
|
1282
|
+
session.agent.setSystemPrompt("You are a helpful coding assistant.");
|
|
1283
|
+
}
|
|
1284
|
+
const eventQueue = [];
|
|
1285
|
+
let isComplete = false;
|
|
1286
|
+
let aborted = false;
|
|
1287
|
+
let wakeConsumer = null;
|
|
1288
|
+
const notify = () => {
|
|
1289
|
+
wakeConsumer?.();
|
|
1290
|
+
wakeConsumer = null;
|
|
1291
|
+
};
|
|
1292
|
+
const unsubscribe = session.subscribe((e) => {
|
|
1293
|
+
eventQueue.push(e);
|
|
1294
|
+
if (e.type === "agent_end") {
|
|
1295
|
+
isComplete = true;
|
|
1296
|
+
}
|
|
1297
|
+
notify();
|
|
1298
|
+
});
|
|
1299
|
+
const abortSignal = options.abortController?.signal;
|
|
1300
|
+
const abortHandler = () => {
|
|
1301
|
+
aborted = true;
|
|
1302
|
+
isComplete = true;
|
|
1303
|
+
void session.abort();
|
|
1304
|
+
notify();
|
|
1305
|
+
};
|
|
1306
|
+
if (abortSignal) {
|
|
1307
|
+
abortSignal.addEventListener("abort", abortHandler);
|
|
1308
|
+
if (abortSignal.aborted) {
|
|
1309
|
+
abortHandler();
|
|
1310
|
+
}
|
|
1311
|
+
}
|
|
1312
|
+
try {
|
|
1313
|
+
traceRawMessage(cwd, null, true);
|
|
1314
|
+
const promptPromise = session.prompt(userInput);
|
|
1315
|
+
const messageId = `msg_${Date.now()}_${Math.random().toString(36).slice(2)}`;
|
|
1316
|
+
const textId = `text_${Date.now()}_${Math.random().toString(36).slice(2)}`;
|
|
1317
|
+
let hasStarted = false;
|
|
1318
|
+
let hasTextStarted = false;
|
|
1319
|
+
let hasFinished = false;
|
|
1320
|
+
const ensureStartEvent = async function* () {
|
|
1321
|
+
if (!hasStarted) {
|
|
1322
|
+
yield `data: ${JSON.stringify({ type: "start", messageId })}
|
|
1323
|
+
|
|
1324
|
+
`;
|
|
1325
|
+
yield `data: ${JSON.stringify({
|
|
1326
|
+
type: "message-metadata",
|
|
1327
|
+
messageMetadata: { sessionId: session.sessionId }
|
|
1328
|
+
})}
|
|
1329
|
+
|
|
1330
|
+
`;
|
|
1331
|
+
hasStarted = true;
|
|
1332
|
+
}
|
|
1333
|
+
};
|
|
1334
|
+
const finishSuccess = async function* (usage) {
|
|
1335
|
+
if (hasTextStarted) {
|
|
1336
|
+
yield `data: ${JSON.stringify({ type: "text-end", id: textId })}
|
|
1337
|
+
|
|
1338
|
+
`;
|
|
1339
|
+
}
|
|
1340
|
+
const finishPayload = { type: "finish", finishReason: "stop" };
|
|
1341
|
+
if (usage != null) {
|
|
1342
|
+
finishPayload.messageMetadata = {
|
|
1343
|
+
usage: usageToMessageMetadata(usage)
|
|
1344
|
+
};
|
|
1345
|
+
}
|
|
1346
|
+
yield `data: ${JSON.stringify(finishPayload)}
|
|
1347
|
+
|
|
1348
|
+
`;
|
|
1349
|
+
yield "data: [DONE]\n\n";
|
|
1350
|
+
hasFinished = true;
|
|
1351
|
+
};
|
|
1352
|
+
const finishError = async function* (errorText) {
|
|
1353
|
+
for (const chunk of emitStreamError(errorText)) {
|
|
1354
|
+
yield chunk;
|
|
1355
|
+
}
|
|
1356
|
+
hasFinished = true;
|
|
1357
|
+
};
|
|
1358
|
+
while (!isComplete || eventQueue.length > 0) {
|
|
1359
|
+
while (eventQueue.length > 0) {
|
|
1360
|
+
const event = eventQueue.shift();
|
|
1361
|
+
traceRawMessage(cwd, event);
|
|
1362
|
+
yield* ensureStartEvent();
|
|
1363
|
+
if (event.type === "message_update") {
|
|
1364
|
+
if (event.assistantMessageEvent.type === "text_delta") {
|
|
1365
|
+
const delta = event.assistantMessageEvent.delta;
|
|
1366
|
+
if (delta) {
|
|
1367
|
+
if (!hasTextStarted) {
|
|
1368
|
+
yield `data: ${JSON.stringify({ type: "text-start", id: textId })}
|
|
1369
|
+
|
|
1370
|
+
`;
|
|
1371
|
+
hasTextStarted = true;
|
|
1372
|
+
}
|
|
1373
|
+
yield `data: ${JSON.stringify({ type: "text-delta", id: textId, delta })}
|
|
1374
|
+
|
|
1375
|
+
`;
|
|
1376
|
+
}
|
|
1377
|
+
}
|
|
1378
|
+
} else if (event.type === "tool_execution_start") {
|
|
1379
|
+
yield `data: ${JSON.stringify({ type: "tool-input-start", toolCallId: event.toolCallId, toolName: event.toolName, dynamic: true })}
|
|
1380
|
+
|
|
1381
|
+
`;
|
|
1382
|
+
yield `data: ${JSON.stringify({ type: "tool-input-available", toolCallId: event.toolCallId, toolName: event.toolName, input: event.args, dynamic: true })}
|
|
1383
|
+
|
|
1384
|
+
`;
|
|
1385
|
+
} else if (event.type === "tool_execution_end") {
|
|
1386
|
+
yield `data: ${JSON.stringify({ type: "tool-output-available", toolCallId: event.toolCallId, output: event.result, dynamic: true })}
|
|
1387
|
+
|
|
1388
|
+
`;
|
|
1389
|
+
} else if (event.type === "agent_end") {
|
|
1390
|
+
if (aborted) {
|
|
1391
|
+
yield* finishError("Run aborted by signal.");
|
|
1392
|
+
} else {
|
|
1393
|
+
const errorMsg = getErrorFromAgentEndMessages(event.messages);
|
|
1394
|
+
if (errorMsg) {
|
|
1395
|
+
yield* finishError(errorMsg);
|
|
1396
|
+
} else {
|
|
1397
|
+
const usage = getUsageFromAgentEndMessages(event.messages);
|
|
1398
|
+
yield* finishSuccess(usage);
|
|
1399
|
+
}
|
|
1400
|
+
}
|
|
1401
|
+
}
|
|
1402
|
+
}
|
|
1403
|
+
if (aborted && !hasFinished) {
|
|
1404
|
+
yield* ensureStartEvent();
|
|
1405
|
+
yield* finishError("Run aborted by signal.");
|
|
1406
|
+
break;
|
|
1407
|
+
}
|
|
1408
|
+
if (!isComplete && eventQueue.length === 0) {
|
|
1409
|
+
await new Promise((resolve3) => {
|
|
1410
|
+
wakeConsumer = resolve3;
|
|
1411
|
+
});
|
|
1412
|
+
}
|
|
1413
|
+
}
|
|
1414
|
+
if (hasFinished) {
|
|
1415
|
+
return;
|
|
1416
|
+
}
|
|
1417
|
+
try {
|
|
1418
|
+
await promptPromise;
|
|
1419
|
+
} catch (error) {
|
|
1420
|
+
if (!hasFinished) {
|
|
1421
|
+
yield* ensureStartEvent();
|
|
1422
|
+
const message = error instanceof Error ? error.message : "Pi agent run failed.";
|
|
1423
|
+
yield* finishError(message);
|
|
1424
|
+
}
|
|
1425
|
+
return;
|
|
1426
|
+
}
|
|
1427
|
+
if (!hasFinished && session.agent.state.error) {
|
|
1428
|
+
yield* ensureStartEvent();
|
|
1429
|
+
yield* finishError(session.agent.state.error);
|
|
1430
|
+
}
|
|
1431
|
+
} finally {
|
|
1432
|
+
if (abortSignal) {
|
|
1433
|
+
abortSignal.removeEventListener("abort", abortHandler);
|
|
1434
|
+
}
|
|
1435
|
+
unsubscribe();
|
|
1436
|
+
session.dispose();
|
|
1437
|
+
}
|
|
1438
|
+
}
|
|
1439
|
+
};
|
|
1440
|
+
}
|
|
1441
|
+
|
|
772
1442
|
// src/runner.ts
|
|
773
1443
|
async function runAgent(options) {
|
|
774
1444
|
const abortController = new AbortController();
|
|
@@ -786,29 +1456,66 @@ async function runAgent(options) {
|
|
|
786
1456
|
let runner;
|
|
787
1457
|
switch (options.runner) {
|
|
788
1458
|
case "claude": {
|
|
789
|
-
|
|
1459
|
+
runner = createClaudeRunner({
|
|
790
1460
|
model: options.model,
|
|
791
1461
|
systemPrompt: options.systemPrompt,
|
|
792
1462
|
maxTurns: options.maxTurns,
|
|
793
1463
|
allowedTools: options.allowedTools,
|
|
794
1464
|
resume: options.resume,
|
|
795
|
-
|
|
1465
|
+
env: process.env,
|
|
796
1466
|
abortController
|
|
797
|
-
};
|
|
798
|
-
|
|
1467
|
+
});
|
|
1468
|
+
break;
|
|
1469
|
+
}
|
|
1470
|
+
case "codex": {
|
|
1471
|
+
runner = createCodexRunner({
|
|
1472
|
+
model: options.model,
|
|
1473
|
+
systemPrompt: options.systemPrompt,
|
|
1474
|
+
maxTurns: options.maxTurns,
|
|
1475
|
+
allowedTools: options.allowedTools,
|
|
1476
|
+
resume: options.resume,
|
|
1477
|
+
cwd: process.cwd(),
|
|
1478
|
+
env: process.env,
|
|
1479
|
+
abortController
|
|
1480
|
+
});
|
|
799
1481
|
break;
|
|
800
1482
|
}
|
|
801
|
-
case "codex":
|
|
802
|
-
throw new Error(
|
|
803
|
-
"Codex runner not yet implemented. Use --runner=claude for now."
|
|
804
|
-
);
|
|
805
1483
|
case "copilot":
|
|
806
1484
|
throw new Error(
|
|
807
1485
|
"Copilot runner not yet implemented. Use --runner=claude for now."
|
|
808
1486
|
);
|
|
1487
|
+
case "gemini": {
|
|
1488
|
+
runner = createGeminiRunner({
|
|
1489
|
+
model: options.model,
|
|
1490
|
+
cwd: process.cwd(),
|
|
1491
|
+
env: process.env,
|
|
1492
|
+
abortController
|
|
1493
|
+
});
|
|
1494
|
+
break;
|
|
1495
|
+
}
|
|
1496
|
+
case "pi": {
|
|
1497
|
+
runner = createPiRunner({
|
|
1498
|
+
model: options.model,
|
|
1499
|
+
systemPrompt: options.systemPrompt,
|
|
1500
|
+
cwd: process.cwd(),
|
|
1501
|
+
env: process.env,
|
|
1502
|
+
abortController,
|
|
1503
|
+
sessionId: options.resume
|
|
1504
|
+
});
|
|
1505
|
+
break;
|
|
1506
|
+
}
|
|
1507
|
+
case "opencode": {
|
|
1508
|
+
runner = createOpenCodeRunner({
|
|
1509
|
+
model: options.model,
|
|
1510
|
+
cwd: process.cwd(),
|
|
1511
|
+
env: process.env,
|
|
1512
|
+
abortController
|
|
1513
|
+
});
|
|
1514
|
+
break;
|
|
1515
|
+
}
|
|
809
1516
|
default:
|
|
810
1517
|
throw new Error(
|
|
811
|
-
`Unknown runner: ${options.runner}. Supported runners: claude, codex, copilot`
|
|
1518
|
+
`Unknown runner: ${options.runner}. Supported runners: claude, codex, gemini, opencode, copilot, pi`
|
|
812
1519
|
);
|
|
813
1520
|
}
|
|
814
1521
|
for await (const chunk of runner.run(options.userInput)) {
|
|
@@ -821,6 +1528,9 @@ async function runAgent(options) {
|
|
|
821
1528
|
}
|
|
822
1529
|
|
|
823
1530
|
// src/cli.ts
|
|
1531
|
+
config({ path: resolve2(process.cwd(), ".env") });
|
|
1532
|
+
config({ path: resolve2(process.cwd(), "../.env") });
|
|
1533
|
+
config({ path: resolve2(process.cwd(), "../../.env") });
|
|
824
1534
|
function getSubcommand() {
|
|
825
1535
|
for (let i = 2; i < process.argv.length; i++) {
|
|
826
1536
|
const a = process.argv[i];
|
|
@@ -870,7 +1580,6 @@ function parseRunArgs() {
|
|
|
870
1580
|
"max-turns": { type: "string", short: "t" },
|
|
871
1581
|
"allowed-tools": { type: "string", short: "a" },
|
|
872
1582
|
resume: { type: "string" },
|
|
873
|
-
"output-format": { type: "string", short: "o" },
|
|
874
1583
|
help: { type: "boolean", short: "h" }
|
|
875
1584
|
},
|
|
876
1585
|
allowPositionals: true,
|
|
@@ -893,16 +1602,9 @@ function parseRunArgs() {
|
|
|
893
1602
|
process.exit(1);
|
|
894
1603
|
}
|
|
895
1604
|
const runner = values.runner;
|
|
896
|
-
if (!["claude", "codex", "copilot"].includes(runner)) {
|
|
897
|
-
console.error(
|
|
898
|
-
'Error: --runner must be one of: "claude", "codex", "copilot"'
|
|
899
|
-
);
|
|
900
|
-
process.exit(1);
|
|
901
|
-
}
|
|
902
|
-
const outputFormat = values["output-format"];
|
|
903
|
-
if (outputFormat && !["text", "json", "stream-json", "stream"].includes(outputFormat)) {
|
|
1605
|
+
if (!["claude", "codex", "gemini", "opencode", "copilot", "pi"].includes(runner)) {
|
|
904
1606
|
console.error(
|
|
905
|
-
'Error: --
|
|
1607
|
+
'Error: --runner must be one of: "claude", "codex", "gemini", "opencode", "copilot", "pi"'
|
|
906
1608
|
);
|
|
907
1609
|
process.exit(1);
|
|
908
1610
|
}
|
|
@@ -914,7 +1616,6 @@ function parseRunArgs() {
|
|
|
914
1616
|
maxTurns: values["max-turns"] ? Number.parseInt(values["max-turns"], 10) : void 0,
|
|
915
1617
|
allowedTools: values["allowed-tools"]?.split(",").map((t) => t.trim()),
|
|
916
1618
|
resume: values.resume,
|
|
917
|
-
outputFormat: outputFormat ?? "stream",
|
|
918
1619
|
userInput
|
|
919
1620
|
};
|
|
920
1621
|
}
|
|
@@ -956,18 +1657,20 @@ Usage:
|
|
|
956
1657
|
sandagent run [options] -- "<user input>"
|
|
957
1658
|
|
|
958
1659
|
Options:
|
|
959
|
-
-r, --runner <runner> Runner: claude, codex, copilot (default: claude)
|
|
1660
|
+
-r, --runner <runner> Runner: claude, codex, gemini, opencode, copilot, pi (default: claude)
|
|
960
1661
|
-m, --model <model> Model (default: claude-sonnet-4-20250514)
|
|
961
1662
|
-c, --cwd <path> Working directory (default: cwd)
|
|
962
1663
|
-s, --system-prompt <prompt> Custom system prompt
|
|
963
1664
|
-t, --max-turns <n> Max conversation turns
|
|
964
1665
|
-a, --allowed-tools <tools> Comma-separated allowed tools
|
|
965
1666
|
--resume <session-id> Resume a previous session
|
|
966
|
-
-o, --output-format <fmt> text | json | stream-json | stream (default: stream)
|
|
967
1667
|
-h, --help Show this help
|
|
968
1668
|
|
|
969
1669
|
Environment:
|
|
970
|
-
ANTHROPIC_API_KEY Anthropic API key (
|
|
1670
|
+
ANTHROPIC_API_KEY Anthropic API key (for claude runner)
|
|
1671
|
+
OPENAI_API_KEY OpenAI API key (for codex runner)
|
|
1672
|
+
CODEX_API_KEY OpenAI API key alias (for codex runner)
|
|
1673
|
+
GEMINI_API_KEY Gemini API key (for gemini runner)
|
|
971
1674
|
SANDAGENT_WORKSPACE Default workspace path
|
|
972
1675
|
`);
|
|
973
1676
|
}
|
|
@@ -1042,8 +1745,7 @@ async function main() {
|
|
|
1042
1745
|
systemPrompt: args.systemPrompt,
|
|
1043
1746
|
maxTurns: args.maxTurns,
|
|
1044
1747
|
allowedTools: args.allowedTools,
|
|
1045
|
-
resume: args.resume
|
|
1046
|
-
outputFormat: args.outputFormat
|
|
1748
|
+
resume: args.resume
|
|
1047
1749
|
});
|
|
1048
1750
|
break;
|
|
1049
1751
|
}
|