@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/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((resolve2) => setTimeout(resolve2, 500));
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
- const outputFormat = options.outputFormat || "stream-json";
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((resolve2) => setTimeout(resolve2, 20));
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
- const runnerOptions = {
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
- outputFormat: options.outputFormat,
1465
+ env: process.env,
796
1466
  abortController
797
- };
798
- runner = createClaudeRunner(runnerOptions);
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: --output-format must be one of: "text", "json", "stream-json", "stream"'
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 (required)
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
  }