@sandagent/runner-cli 0.2.24 → 0.5.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
@@ -227,8 +229,6 @@ var AISDKStreamConverter = class {
227
229
  systemMessage;
228
230
  hasEmittedStart = false;
229
231
  sessionId;
230
- /** True after we emitted an error from a result message (e.g. API 400). Avoids emitting a second generic "exited with code 1" that would hide the real error. */
231
- errorEmitted = false;
232
232
  partIdMap = /* @__PURE__ */ new Map();
233
233
  /**
234
234
  * Get the current session ID from the stream
@@ -256,13 +256,6 @@ var AISDKStreamConverter = class {
256
256
  }
257
257
  throw new Error("Part ID not found");
258
258
  }
259
- /** Returns part ID for index or undefined if not tracked (e.g. content_block_stop without a prior start). */
260
- tryGetPartId(index) {
261
- if (!this.sessionId)
262
- return void 0;
263
- const partIdKey = `${this.sessionId}-${index}`;
264
- return this.partIdMap.get(partIdKey);
265
- }
266
259
  /**
267
260
  * Helper to emit tool call
268
261
  */
@@ -303,18 +296,9 @@ var AISDKStreamConverter = class {
303
296
  delta: event.delta.text
304
297
  });
305
298
  }
306
- if (event.type === "content_block_start" && event.content_block.type === "thinking") {
307
- const partId = `reasoning_${generateId()}`;
308
- this.setPartId(event.index, partId);
309
- }
310
- if (event.type === "content_block_delta" && event.delta?.type === "thinking_delta") {
311
- if (event.delta.thinking) {
312
- yield this.emit({ type: "reasoning", text: event.delta.thinking });
313
- }
314
- }
315
299
  if (event.type === "content_block_stop") {
316
- const partId = this.tryGetPartId(event.index);
317
- if (partId?.startsWith("text_")) {
300
+ const partId = this.getPartId(event.index);
301
+ if (partId.startsWith("text_")) {
318
302
  yield this.emit({ type: "text-end", id: partId });
319
303
  }
320
304
  }
@@ -372,7 +356,7 @@ var AISDKStreamConverter = class {
372
356
  const userMsg = message;
373
357
  const content = userMsg.message?.content;
374
358
  for (const part of content) {
375
- if (part.type === "tool_result") {
359
+ if (typeof part !== "string" && part.type === "tool_result") {
376
360
  yield this.emit({
377
361
  type: "tool-output-available",
378
362
  toolCallId: part.tool_use_id,
@@ -386,7 +370,6 @@ var AISDKStreamConverter = class {
386
370
  if (message.type === "result") {
387
371
  const resultMsg = message;
388
372
  if (resultMsg.is_error) {
389
- this.errorEmitted = true;
390
373
  const errorText = resultMsg.result || "Unknown error";
391
374
  yield this.emit({
392
375
  type: "error",
@@ -404,53 +387,21 @@ var AISDKStreamConverter = class {
404
387
  }
405
388
  }
406
389
  } catch (error) {
407
- if (process.env.DEBUG === "true") {
408
- const errPayload = {
409
- error: error instanceof Error ? error.message : String(error)
410
- };
411
- if (error instanceof Error) {
412
- if (error.stack)
413
- errPayload.stack = error.stack;
414
- if (error.cause !== void 0) {
415
- errPayload.cause = error.cause instanceof Error ? {
416
- message: error.cause.message,
417
- stack: error.cause.stack
418
- } : String(error.cause);
419
- }
420
- }
421
- trace(errPayload);
422
- } else {
423
- trace({ error: String(error) });
424
- }
390
+ trace({ error: String(error) });
425
391
  if (isAbortError(error)) {
426
392
  console.error("[AISDKStream] Operation aborted");
427
393
  } else {
428
394
  const errorMessage = error instanceof Error ? error.message : "Unknown error";
429
395
  console.error("[AISDKStream] Error:", errorMessage);
430
- if (process.env.DEBUG === "true") {
431
- if (error instanceof Error && error.stack) {
432
- console.error("[AISDKStream] Stack:", error.stack);
396
+ yield this.emit({ type: "error", errorText: errorMessage });
397
+ yield this.emit({
398
+ type: "finish",
399
+ finishReason: mapFinishReason("error_during_execution", true),
400
+ messageMetadata: {
401
+ usage: convertUsageToAISDK({}),
402
+ sessionId: this.sessionId
433
403
  }
434
- if (error instanceof Error && error.cause) {
435
- console.error("[AISDKStream] Cause:", error.cause);
436
- }
437
- }
438
- if ((errorMessage.includes("exited with code") || errorMessage.includes("process exited")) && this.errorEmitted) {
439
- console.error("[AISDKStream] (Skipping duplicate error \u2014 already sent API/result error above. Check the first error in the stream.)");
440
- } else if (errorMessage.includes("exited with code") || errorMessage.includes("process exited")) {
441
- console.error("[AISDKStream] Hint: Verify ANTHROPIC_API_KEY, --model (proxy must support it), and network.");
442
- }
443
- if (!this.errorEmitted) {
444
- yield this.emit({ type: "error", errorText: errorMessage });
445
- yield this.emit({
446
- type: "finish",
447
- finishReason: mapFinishReason("error_during_execution", true),
448
- messageMetadata: {
449
- usage: convertUsageToAISDK({}),
450
- sessionId: this.sessionId
451
- }
452
- });
453
- }
404
+ });
454
405
  }
455
406
  } finally {
456
407
  yield `data: [DONE]
@@ -501,7 +452,7 @@ function createCanUseToolCallback(claudeOptions) {
501
452
  }
502
453
  } catch {
503
454
  }
504
- await new Promise((resolve2) => setTimeout(resolve2, 500));
455
+ await new Promise((resolve3) => setTimeout(resolve3, 500));
505
456
  }
506
457
  try {
507
458
  fs.unlinkSync(approvalFile);
@@ -577,25 +528,9 @@ async function loadClaudeAgentSDK() {
577
528
  }
578
529
  }
579
530
  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
- }
531
+ yield* runWithAISDKUIOutput(sdk, options, userInput);
596
532
  }
597
533
  function createSDKOptions(options) {
598
- const isRoot = typeof process.getuid === "function" && process.getuid() === 0;
599
534
  return {
600
535
  model: options.model,
601
536
  systemPrompt: options.systemPrompt,
@@ -611,8 +546,10 @@ function createSDKOptions(options) {
611
546
  resume: options.resume,
612
547
  settingSources: ["project", "user"],
613
548
  canUseTool: createCanUseToolCallback(options),
614
- permissionMode: isRoot ? "default" : "bypassPermissions",
615
- allowDangerouslySkipPermissions: !isRoot,
549
+ // Bypass all permission checks for automated execution
550
+ permissionMode: "bypassPermissions",
551
+ allowDangerouslySkipPermissions: true,
552
+ // Enable partial messages for streaming
616
553
  includePartialMessages: options.includePartialMessages
617
554
  };
618
555
  }
@@ -638,55 +575,6 @@ function setupAbortHandler(queryIterator, signal) {
638
575
  }
639
576
  };
640
577
  }
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
578
  async function* runWithAISDKUIOutput(sdk, options, userInput) {
691
579
  const sdkOptions = createSDKOptions({
692
580
  ...options,
@@ -733,7 +621,7 @@ Documentation: https://platform.claude.com/docs/en/agent-sdk/typescript-v2-previ
733
621
  id: textId,
734
622
  delta: word + " "
735
623
  });
736
- await new Promise((resolve2) => setTimeout(resolve2, 20));
624
+ await new Promise((resolve3) => setTimeout(resolve3, 20));
737
625
  }
738
626
  yield formatDataStream({ type: "text-end", id: textId });
739
627
  yield formatDataStream({
@@ -769,6 +657,647 @@ Documentation: https://platform.claude.com/docs/en/agent-sdk/typescript-v2-previ
769
657
  }
770
658
  }
771
659
 
660
+ // ../../packages/runner-codex/dist/codex-runner.js
661
+ import { Codex } from "@openai/codex-sdk";
662
+ function normalizeCodexModel(model) {
663
+ const trimmed = model.trim();
664
+ const withoutProvider = trimmed.startsWith("openai:") ? trimmed.slice("openai:".length) : trimmed;
665
+ if (/^\d+(\.\d+)?$/.test(withoutProvider)) {
666
+ return `gpt-${withoutProvider}`;
667
+ }
668
+ return withoutProvider;
669
+ }
670
+ function stringifyUnknown(value) {
671
+ if (typeof value === "string") {
672
+ return value;
673
+ }
674
+ try {
675
+ return JSON.stringify(value);
676
+ } catch {
677
+ return String(value);
678
+ }
679
+ }
680
+ function toToolStartPayload(event) {
681
+ if (event.type !== "item.started") {
682
+ return null;
683
+ }
684
+ const item = event.item;
685
+ if (item.type === "command_execution") {
686
+ return {
687
+ toolCallId: item.id,
688
+ toolName: "shell",
689
+ args: { command: item.command }
690
+ };
691
+ }
692
+ if (item.type === "mcp_tool_call") {
693
+ return {
694
+ toolCallId: item.id,
695
+ toolName: `${item.server}:${item.tool}`,
696
+ args: item.arguments
697
+ };
698
+ }
699
+ if (item.type === "web_search") {
700
+ return {
701
+ toolCallId: item.id,
702
+ toolName: "web_search",
703
+ args: { query: item.query }
704
+ };
705
+ }
706
+ return null;
707
+ }
708
+ function toToolEndPayload(event) {
709
+ if (event.type !== "item.completed") {
710
+ return null;
711
+ }
712
+ const item = event.item;
713
+ if (item.type === "command_execution") {
714
+ return {
715
+ toolCallId: item.id,
716
+ result: {
717
+ status: item.status,
718
+ exitCode: item.exit_code,
719
+ output: item.aggregated_output
720
+ }
721
+ };
722
+ }
723
+ if (item.type === "mcp_tool_call") {
724
+ return {
725
+ toolCallId: item.id,
726
+ result: item.result ?? item.error ?? { status: item.status }
727
+ };
728
+ }
729
+ if (item.type === "web_search") {
730
+ return {
731
+ toolCallId: item.id,
732
+ result: { query: item.query }
733
+ };
734
+ }
735
+ return null;
736
+ }
737
+ function toAssistantText(event) {
738
+ if (event.type === "item.completed" && event.item.type === "agent_message") {
739
+ return event.item.text;
740
+ }
741
+ if (event.type === "item.completed" && event.item.type === "reasoning") {
742
+ return `[Reasoning] ${event.item.text}`;
743
+ }
744
+ if (event.type === "item.completed" && event.item.type === "error") {
745
+ return `[Error] ${event.item.message}`;
746
+ }
747
+ return null;
748
+ }
749
+ function createCodexRunner(options) {
750
+ const codex = new Codex({
751
+ apiKey: process.env.CODEX_API_KEY || process.env.OPENAI_API_KEY,
752
+ baseUrl: process.env.OPENAI_BASE_URL,
753
+ env: options.env
754
+ });
755
+ return {
756
+ async *run(userInput) {
757
+ const threadOptions = {
758
+ model: normalizeCodexModel(options.model),
759
+ sandboxMode: options.sandboxMode,
760
+ workingDirectory: options.cwd || process.cwd(),
761
+ skipGitRepoCheck: options.skipGitRepoCheck ?? true,
762
+ modelReasoningEffort: options.modelReasoningEffort,
763
+ networkAccessEnabled: options.networkAccessEnabled,
764
+ webSearchMode: options.webSearchMode,
765
+ approvalPolicy: options.approvalPolicy
766
+ };
767
+ const thread = options.resume ? codex.resumeThread(options.resume, threadOptions) : codex.startThread(threadOptions);
768
+ const streamedTurn = await thread.runStreamed(userInput, {
769
+ signal: options.abortController?.signal
770
+ });
771
+ for await (const event of streamedTurn.events) {
772
+ const assistantText = toAssistantText(event);
773
+ if (assistantText) {
774
+ yield `data: ${JSON.stringify({ type: "text-delta", delta: assistantText })}
775
+
776
+ `;
777
+ }
778
+ const toolStart = toToolStartPayload(event);
779
+ if (toolStart) {
780
+ yield `data: ${JSON.stringify({ type: "tool-input-start", toolCallId: toolStart.toolCallId, toolName: toolStart.toolName })}
781
+
782
+ `;
783
+ yield `data: ${JSON.stringify({ type: "tool-input-available", toolCallId: toolStart.toolCallId, toolName: toolStart.toolName, input: toolStart.args })}
784
+
785
+ `;
786
+ }
787
+ const toolEnd = toToolEndPayload(event);
788
+ if (toolEnd) {
789
+ yield `data: ${JSON.stringify({ type: "tool-output-available", toolCallId: toolEnd.toolCallId, output: toolEnd.result })}
790
+
791
+ `;
792
+ }
793
+ if (event.type === "turn.completed") {
794
+ yield `data: ${JSON.stringify({ type: "finish", finishReason: "stop", usage: event.usage })}
795
+
796
+ `;
797
+ yield `data: [DONE]
798
+
799
+ `;
800
+ }
801
+ if (event.type === "turn.failed") {
802
+ yield `data: ${JSON.stringify({ type: "error", errorText: event.error.message })}
803
+
804
+ `;
805
+ yield `data: ${JSON.stringify({ type: "finish", finishReason: "error" })}
806
+
807
+ `;
808
+ yield `data: [DONE]
809
+
810
+ `;
811
+ }
812
+ if (event.type === "error") {
813
+ yield `data: ${JSON.stringify({ type: "error", errorText: stringifyUnknown(event.message) })}
814
+
815
+ `;
816
+ yield `data: ${JSON.stringify({ type: "finish", finishReason: "error" })}
817
+
818
+ `;
819
+ yield `data: [DONE]
820
+
821
+ `;
822
+ }
823
+ }
824
+ }
825
+ };
826
+ }
827
+
828
+ // ../../packages/runner-gemini/dist/gemini-runner.js
829
+ import { spawn } from "node:child_process";
830
+ function createGeminiRunner(options = {}) {
831
+ const cwd = options.cwd || process.cwd();
832
+ let currentProcess = null;
833
+ return {
834
+ async *run(userInput) {
835
+ if (options.abortController?.signal.aborted) {
836
+ yield `data: ${JSON.stringify({ type: "error", errorText: "Run aborted before start." })}
837
+
838
+ `;
839
+ yield `data: ${JSON.stringify({ type: "finish", finishReason: "error" })}
840
+
841
+ `;
842
+ yield "data: [DONE]\n\n";
843
+ return;
844
+ }
845
+ const args = ["--experimental-acp"];
846
+ if (options.model)
847
+ args.push("--model", options.model);
848
+ let aborted = false;
849
+ let completed = false;
850
+ currentProcess = spawn("gemini", args, {
851
+ cwd,
852
+ env: { ...process.env, ...options.env },
853
+ stdio: ["pipe", "pipe", "pipe"]
854
+ });
855
+ if (!currentProcess.stdin || !currentProcess.stdout)
856
+ throw new Error("Failed to spawn gemini");
857
+ const abortSignal = options.abortController?.signal;
858
+ const abortHandler = () => {
859
+ aborted = true;
860
+ currentProcess?.kill();
861
+ };
862
+ if (abortSignal) {
863
+ abortSignal.addEventListener("abort", abortHandler);
864
+ }
865
+ let msgId = 1;
866
+ const send = (method, params, id) => {
867
+ const msg = JSON.stringify({
868
+ jsonrpc: "2.0",
869
+ method,
870
+ params,
871
+ ...id ? { id } : {}
872
+ });
873
+ currentProcess.stdin.write(msg + "\n");
874
+ };
875
+ send("initialize", { protocolVersion: 1, clientCapabilities: {} }, msgId++);
876
+ let sessionId = null;
877
+ const messageId = `msg_${Date.now()}_${Math.random().toString(36).slice(2)}`;
878
+ const textId = `text_${Date.now()}_${Math.random().toString(36).slice(2)}`;
879
+ let hasStarted = false;
880
+ let hasTextStarted = false;
881
+ try {
882
+ let buffer = "";
883
+ for await (const chunk of currentProcess.stdout) {
884
+ buffer += chunk.toString();
885
+ const lines = buffer.split("\n");
886
+ buffer = lines.pop() || "";
887
+ for (const line of lines) {
888
+ if (!line.trim())
889
+ continue;
890
+ let msg;
891
+ try {
892
+ msg = JSON.parse(line);
893
+ } catch {
894
+ continue;
895
+ }
896
+ if (msg.id === 1 && msg.result) {
897
+ send("session/new", { cwd, mcpServers: [] }, msgId++);
898
+ }
899
+ if (msg.id === 2 && msg.result) {
900
+ const result = msg.result;
901
+ sessionId = result.sessionId;
902
+ send("session/prompt", {
903
+ sessionId,
904
+ prompt: [{ type: "text", text: userInput }]
905
+ }, msgId++);
906
+ }
907
+ if (msg.id === 3 && "result" in msg) {
908
+ if (hasTextStarted)
909
+ yield `data: ${JSON.stringify({ type: "text-end", id: textId })}
910
+
911
+ `;
912
+ yield `data: ${JSON.stringify({ type: "finish", finishReason: "stop" })}
913
+
914
+ `;
915
+ yield `data: [DONE]
916
+
917
+ `;
918
+ completed = true;
919
+ currentProcess.kill();
920
+ return;
921
+ }
922
+ if (msg.method === "session/update" && msg.params?.update) {
923
+ const update = msg.params.update;
924
+ if (!hasStarted) {
925
+ yield `data: ${JSON.stringify({ type: "start", messageId })}
926
+
927
+ `;
928
+ hasStarted = true;
929
+ }
930
+ if (update.sessionUpdate === "agent_message_chunk" && update.content?.type === "text" && update.content.text) {
931
+ if (!hasTextStarted) {
932
+ yield `data: ${JSON.stringify({ type: "text-start", id: textId })}
933
+
934
+ `;
935
+ hasTextStarted = true;
936
+ }
937
+ yield `data: ${JSON.stringify({ type: "text-delta", id: textId, delta: update.content.text })}
938
+
939
+ `;
940
+ }
941
+ }
942
+ }
943
+ }
944
+ if (!completed) {
945
+ const errorText = aborted ? "Gemini run aborted by signal." : "Gemini ACP process exited before completion.";
946
+ yield `data: ${JSON.stringify({ type: "error", errorText })}
947
+
948
+ `;
949
+ yield `data: ${JSON.stringify({ type: "finish", finishReason: "error" })}
950
+
951
+ `;
952
+ yield "data: [DONE]\n\n";
953
+ }
954
+ } finally {
955
+ if (abortSignal) {
956
+ abortSignal.removeEventListener("abort", abortHandler);
957
+ }
958
+ currentProcess = null;
959
+ }
960
+ },
961
+ abort() {
962
+ currentProcess?.kill();
963
+ currentProcess = null;
964
+ }
965
+ };
966
+ }
967
+
968
+ // ../../packages/runner-opencode/dist/opencode-runner.js
969
+ import { spawn as spawn2 } from "node:child_process";
970
+ function createOpenCodeRunner(options = {}) {
971
+ const cwd = options.cwd || process.cwd();
972
+ let currentProcess = null;
973
+ return {
974
+ async *run(userInput) {
975
+ if (options.abortController?.signal.aborted) {
976
+ yield `data: ${JSON.stringify({ type: "error", errorText: "Run aborted before start." })}
977
+
978
+ `;
979
+ yield `data: ${JSON.stringify({ type: "finish", finishReason: "error" })}
980
+
981
+ `;
982
+ yield "data: [DONE]\n\n";
983
+ return;
984
+ }
985
+ const args = ["acp"];
986
+ if (options.model)
987
+ args.push("--model", options.model);
988
+ let aborted = false;
989
+ let completed = false;
990
+ currentProcess = spawn2("opencode", args, {
991
+ cwd,
992
+ env: { ...process.env, ...options.env },
993
+ stdio: ["pipe", "pipe", "pipe"]
994
+ });
995
+ if (!currentProcess.stdin || !currentProcess.stdout)
996
+ throw new Error("Failed to spawn opencode");
997
+ const abortSignal = options.abortController?.signal;
998
+ const abortHandler = () => {
999
+ aborted = true;
1000
+ currentProcess?.kill();
1001
+ };
1002
+ if (abortSignal) {
1003
+ abortSignal.addEventListener("abort", abortHandler);
1004
+ }
1005
+ let msgId = 1;
1006
+ const send = (method, params, id) => {
1007
+ const msg = JSON.stringify({
1008
+ jsonrpc: "2.0",
1009
+ method,
1010
+ params,
1011
+ ...id ? { id } : {}
1012
+ });
1013
+ currentProcess.stdin.write(msg + "\n");
1014
+ };
1015
+ send("initialize", { protocolVersion: 1, clientCapabilities: {} }, msgId++);
1016
+ let sessionId = null;
1017
+ const messageId = `msg_${Date.now()}_${Math.random().toString(36).slice(2)}`;
1018
+ const textId = `text_${Date.now()}_${Math.random().toString(36).slice(2)}`;
1019
+ let hasStarted = false;
1020
+ let hasTextStarted = false;
1021
+ try {
1022
+ let buffer = "";
1023
+ for await (const chunk of currentProcess.stdout) {
1024
+ buffer += chunk.toString();
1025
+ const lines = buffer.split("\n");
1026
+ buffer = lines.pop() || "";
1027
+ for (const line of lines) {
1028
+ if (!line.trim())
1029
+ continue;
1030
+ let msg;
1031
+ try {
1032
+ msg = JSON.parse(line);
1033
+ } catch {
1034
+ continue;
1035
+ }
1036
+ if (msg.id === 1 && msg.result) {
1037
+ send("session/new", { cwd, mcpServers: [] }, msgId++);
1038
+ }
1039
+ if (msg.id === 2 && msg.result) {
1040
+ const result = msg.result;
1041
+ sessionId = result.sessionId;
1042
+ send("session/prompt", {
1043
+ sessionId,
1044
+ prompt: [{ type: "text", text: userInput }]
1045
+ }, msgId++);
1046
+ }
1047
+ if (msg.id === 3 && "result" in msg) {
1048
+ if (hasTextStarted)
1049
+ yield `data: ${JSON.stringify({ type: "text-end", id: textId })}
1050
+
1051
+ `;
1052
+ yield `data: ${JSON.stringify({ type: "finish", finishReason: "stop" })}
1053
+
1054
+ `;
1055
+ yield `data: [DONE]
1056
+
1057
+ `;
1058
+ completed = true;
1059
+ currentProcess.kill();
1060
+ return;
1061
+ }
1062
+ if (msg.method === "session/update" && msg.params?.update) {
1063
+ const update = msg.params.update;
1064
+ if (!hasStarted) {
1065
+ yield `data: ${JSON.stringify({ type: "start", messageId })}
1066
+
1067
+ `;
1068
+ hasStarted = true;
1069
+ }
1070
+ if (update.sessionUpdate === "agent_message_chunk" && update.content?.type === "text" && update.content.text) {
1071
+ if (!hasTextStarted) {
1072
+ yield `data: ${JSON.stringify({ type: "text-start", id: textId })}
1073
+
1074
+ `;
1075
+ hasTextStarted = true;
1076
+ }
1077
+ yield `data: ${JSON.stringify({ type: "text-delta", id: textId, delta: update.content.text })}
1078
+
1079
+ `;
1080
+ }
1081
+ }
1082
+ }
1083
+ }
1084
+ if (!completed) {
1085
+ const errorText = aborted ? "OpenCode run aborted by signal." : "OpenCode ACP process exited before completion.";
1086
+ yield `data: ${JSON.stringify({ type: "error", errorText })}
1087
+
1088
+ `;
1089
+ yield `data: ${JSON.stringify({ type: "finish", finishReason: "error" })}
1090
+
1091
+ `;
1092
+ yield "data: [DONE]\n\n";
1093
+ }
1094
+ } finally {
1095
+ if (abortSignal) {
1096
+ abortSignal.removeEventListener("abort", abortHandler);
1097
+ }
1098
+ currentProcess = null;
1099
+ }
1100
+ },
1101
+ abort() {
1102
+ currentProcess?.kill();
1103
+ currentProcess = null;
1104
+ }
1105
+ };
1106
+ }
1107
+
1108
+ // ../../packages/runner-pi/dist/pi-runner.js
1109
+ import { Agent } from "@mariozechner/pi-agent-core";
1110
+ import { getModel } from "@mariozechner/pi-ai";
1111
+ import { createCodingTools } from "@mariozechner/pi-coding-agent";
1112
+ function parseModelSpec(model) {
1113
+ const trimmed = model.trim();
1114
+ const separator = trimmed.indexOf(":");
1115
+ if (separator <= 0 || separator === trimmed.length - 1) {
1116
+ throw new Error(`Invalid pi model "${model}". Expected format "<provider>:<model>", for example "google:gemini-2.5-pro".`);
1117
+ }
1118
+ return {
1119
+ provider: trimmed.slice(0, separator),
1120
+ modelName: trimmed.slice(separator + 1)
1121
+ };
1122
+ }
1123
+ function getEnvValue(optionsEnv, name) {
1124
+ return optionsEnv?.[name] ?? process.env[name];
1125
+ }
1126
+ function applyModelOverrides(model, provider, optionsEnv) {
1127
+ const openAiBaseUrl = getEnvValue(optionsEnv, "OPENAI_BASE_URL");
1128
+ const geminiBaseUrl = getEnvValue(optionsEnv, "GEMINI_BASE_URL");
1129
+ const anthropicBaseUrl = getEnvValue(optionsEnv, "ANTHROPIC_BASE_URL");
1130
+ if (provider === "openai" && openAiBaseUrl) {
1131
+ model.baseUrl = openAiBaseUrl;
1132
+ } else if (provider === "google" && geminiBaseUrl) {
1133
+ model.baseUrl = geminiBaseUrl;
1134
+ } else if (provider === "anthropic" && anthropicBaseUrl) {
1135
+ model.baseUrl = anthropicBaseUrl;
1136
+ }
1137
+ }
1138
+ function emitStreamError(errorText) {
1139
+ return [
1140
+ `data: ${JSON.stringify({ type: "error", errorText })}
1141
+
1142
+ `,
1143
+ `data: ${JSON.stringify({ type: "finish", finishReason: "error" })}
1144
+
1145
+ `,
1146
+ "data: [DONE]\n\n"
1147
+ ];
1148
+ }
1149
+ function createPiRunner(options = {}) {
1150
+ const modelSpec = options.model || "google:gemini-2.5-flash-lite-preview-06-17";
1151
+ const { provider, modelName } = parseModelSpec(modelSpec);
1152
+ const cwd = options.cwd || process.cwd();
1153
+ const model = getModel(provider, modelName);
1154
+ applyModelOverrides(model, provider, options.env);
1155
+ const agent = new Agent({
1156
+ initialState: {
1157
+ systemPrompt: options.systemPrompt || "You are a helpful coding assistant.",
1158
+ model,
1159
+ tools: createCodingTools(cwd)
1160
+ }
1161
+ });
1162
+ return {
1163
+ async *run(userInput) {
1164
+ const eventQueue = [];
1165
+ let isComplete = false;
1166
+ let aborted = false;
1167
+ let wakeConsumer = null;
1168
+ const notify = () => {
1169
+ wakeConsumer?.();
1170
+ wakeConsumer = null;
1171
+ };
1172
+ const unsubscribe = agent.subscribe((e) => {
1173
+ eventQueue.push(e);
1174
+ if (e.type === "agent_end") {
1175
+ isComplete = true;
1176
+ }
1177
+ notify();
1178
+ });
1179
+ const abortSignal = options.abortController?.signal;
1180
+ const abortHandler = () => {
1181
+ aborted = true;
1182
+ isComplete = true;
1183
+ agent.abort();
1184
+ notify();
1185
+ };
1186
+ if (abortSignal) {
1187
+ abortSignal.addEventListener("abort", abortHandler);
1188
+ if (abortSignal.aborted) {
1189
+ abortHandler();
1190
+ }
1191
+ }
1192
+ try {
1193
+ const promptPromise = agent.prompt(userInput);
1194
+ const messageId = `msg_${Date.now()}_${Math.random().toString(36).slice(2)}`;
1195
+ const textId = `text_${Date.now()}_${Math.random().toString(36).slice(2)}`;
1196
+ let hasStarted = false;
1197
+ let hasTextStarted = false;
1198
+ let hasFinished = false;
1199
+ const ensureStartEvent = async function* () {
1200
+ if (!hasStarted) {
1201
+ yield `data: ${JSON.stringify({ type: "start", messageId })}
1202
+
1203
+ `;
1204
+ hasStarted = true;
1205
+ }
1206
+ };
1207
+ const finishSuccess = async function* () {
1208
+ if (hasTextStarted) {
1209
+ yield `data: ${JSON.stringify({ type: "text-end", id: textId })}
1210
+
1211
+ `;
1212
+ }
1213
+ yield `data: ${JSON.stringify({ type: "finish", finishReason: "stop" })}
1214
+
1215
+ `;
1216
+ yield "data: [DONE]\n\n";
1217
+ hasFinished = true;
1218
+ };
1219
+ const finishError = async function* (errorText) {
1220
+ for (const chunk of emitStreamError(errorText)) {
1221
+ yield chunk;
1222
+ }
1223
+ hasFinished = true;
1224
+ };
1225
+ while (!isComplete || eventQueue.length > 0) {
1226
+ while (eventQueue.length > 0) {
1227
+ const event = eventQueue.shift();
1228
+ yield* ensureStartEvent();
1229
+ if (event.type === "message_update") {
1230
+ if (event.assistantMessageEvent.type === "text_delta") {
1231
+ const delta = event.assistantMessageEvent.delta;
1232
+ if (delta) {
1233
+ if (!hasTextStarted) {
1234
+ yield `data: ${JSON.stringify({ type: "text-start", id: textId })}
1235
+
1236
+ `;
1237
+ hasTextStarted = true;
1238
+ }
1239
+ yield `data: ${JSON.stringify({ type: "text-delta", id: textId, delta })}
1240
+
1241
+ `;
1242
+ }
1243
+ }
1244
+ } else if (event.type === "tool_execution_start") {
1245
+ yield `data: ${JSON.stringify({ type: "tool-input-start", toolCallId: event.toolCallId, toolName: event.toolName })}
1246
+
1247
+ `;
1248
+ yield `data: ${JSON.stringify({ type: "tool-input-available", toolCallId: event.toolCallId, toolName: event.toolName, input: event.args })}
1249
+
1250
+ `;
1251
+ } else if (event.type === "tool_execution_end") {
1252
+ yield `data: ${JSON.stringify({ type: "tool-output-available", toolCallId: event.toolCallId, output: event.result })}
1253
+
1254
+ `;
1255
+ } else if (event.type === "agent_end") {
1256
+ if (aborted) {
1257
+ yield* finishError("Run aborted by signal.");
1258
+ } else {
1259
+ yield* finishSuccess();
1260
+ }
1261
+ }
1262
+ }
1263
+ if (aborted && !hasFinished) {
1264
+ yield* ensureStartEvent();
1265
+ yield* finishError("Run aborted by signal.");
1266
+ break;
1267
+ }
1268
+ if (!isComplete && eventQueue.length === 0) {
1269
+ await new Promise((resolve3) => {
1270
+ wakeConsumer = resolve3;
1271
+ });
1272
+ }
1273
+ }
1274
+ if (hasFinished) {
1275
+ return;
1276
+ }
1277
+ try {
1278
+ await promptPromise;
1279
+ } catch (error) {
1280
+ if (!hasFinished) {
1281
+ yield* ensureStartEvent();
1282
+ const message = error instanceof Error ? error.message : "Pi agent run failed.";
1283
+ yield* finishError(message);
1284
+ }
1285
+ return;
1286
+ }
1287
+ if (!hasFinished && agent.state.error) {
1288
+ yield* ensureStartEvent();
1289
+ yield* finishError(agent.state.error);
1290
+ }
1291
+ } finally {
1292
+ if (abortSignal) {
1293
+ abortSignal.removeEventListener("abort", abortHandler);
1294
+ }
1295
+ unsubscribe();
1296
+ }
1297
+ }
1298
+ };
1299
+ }
1300
+
772
1301
  // src/runner.ts
773
1302
  async function runAgent(options) {
774
1303
  const abortController = new AbortController();
@@ -786,29 +1315,65 @@ async function runAgent(options) {
786
1315
  let runner;
787
1316
  switch (options.runner) {
788
1317
  case "claude": {
789
- const runnerOptions = {
1318
+ runner = createClaudeRunner({
790
1319
  model: options.model,
791
1320
  systemPrompt: options.systemPrompt,
792
1321
  maxTurns: options.maxTurns,
793
1322
  allowedTools: options.allowedTools,
794
1323
  resume: options.resume,
795
- outputFormat: options.outputFormat,
1324
+ env: process.env,
796
1325
  abortController
797
- };
798
- runner = createClaudeRunner(runnerOptions);
1326
+ });
1327
+ break;
1328
+ }
1329
+ case "codex": {
1330
+ runner = createCodexRunner({
1331
+ model: options.model,
1332
+ systemPrompt: options.systemPrompt,
1333
+ maxTurns: options.maxTurns,
1334
+ allowedTools: options.allowedTools,
1335
+ resume: options.resume,
1336
+ cwd: process.cwd(),
1337
+ env: process.env,
1338
+ abortController
1339
+ });
799
1340
  break;
800
1341
  }
801
- case "codex":
802
- throw new Error(
803
- "Codex runner not yet implemented. Use --runner=claude for now."
804
- );
805
1342
  case "copilot":
806
1343
  throw new Error(
807
1344
  "Copilot runner not yet implemented. Use --runner=claude for now."
808
1345
  );
1346
+ case "gemini": {
1347
+ runner = createGeminiRunner({
1348
+ model: options.model,
1349
+ cwd: process.cwd(),
1350
+ env: process.env,
1351
+ abortController
1352
+ });
1353
+ break;
1354
+ }
1355
+ case "pi": {
1356
+ runner = createPiRunner({
1357
+ model: options.model,
1358
+ systemPrompt: options.systemPrompt,
1359
+ cwd: process.cwd(),
1360
+ env: process.env,
1361
+ abortController
1362
+ });
1363
+ break;
1364
+ }
1365
+ case "opencode": {
1366
+ runner = createOpenCodeRunner({
1367
+ model: options.model,
1368
+ cwd: process.cwd(),
1369
+ env: process.env,
1370
+ abortController
1371
+ });
1372
+ break;
1373
+ }
809
1374
  default:
810
1375
  throw new Error(
811
- `Unknown runner: ${options.runner}. Supported runners: claude, codex, copilot`
1376
+ `Unknown runner: ${options.runner}. Supported runners: claude, codex, gemini, opencode, copilot, pi`
812
1377
  );
813
1378
  }
814
1379
  for await (const chunk of runner.run(options.userInput)) {
@@ -821,6 +1386,9 @@ async function runAgent(options) {
821
1386
  }
822
1387
 
823
1388
  // src/cli.ts
1389
+ config({ path: resolve2(process.cwd(), ".env") });
1390
+ config({ path: resolve2(process.cwd(), "../.env") });
1391
+ config({ path: resolve2(process.cwd(), "../../.env") });
824
1392
  function getSubcommand() {
825
1393
  for (let i = 2; i < process.argv.length; i++) {
826
1394
  const a = process.argv[i];
@@ -870,7 +1438,6 @@ function parseRunArgs() {
870
1438
  "max-turns": { type: "string", short: "t" },
871
1439
  "allowed-tools": { type: "string", short: "a" },
872
1440
  resume: { type: "string" },
873
- "output-format": { type: "string", short: "o" },
874
1441
  help: { type: "boolean", short: "h" }
875
1442
  },
876
1443
  allowPositionals: true,
@@ -893,16 +1460,9 @@ function parseRunArgs() {
893
1460
  process.exit(1);
894
1461
  }
895
1462
  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)) {
1463
+ if (!["claude", "codex", "gemini", "opencode", "copilot", "pi"].includes(runner)) {
904
1464
  console.error(
905
- 'Error: --output-format must be one of: "text", "json", "stream-json", "stream"'
1465
+ 'Error: --runner must be one of: "claude", "codex", "gemini", "opencode", "copilot", "pi"'
906
1466
  );
907
1467
  process.exit(1);
908
1468
  }
@@ -914,7 +1474,6 @@ function parseRunArgs() {
914
1474
  maxTurns: values["max-turns"] ? Number.parseInt(values["max-turns"], 10) : void 0,
915
1475
  allowedTools: values["allowed-tools"]?.split(",").map((t) => t.trim()),
916
1476
  resume: values.resume,
917
- outputFormat: outputFormat ?? "stream",
918
1477
  userInput
919
1478
  };
920
1479
  }
@@ -956,18 +1515,20 @@ Usage:
956
1515
  sandagent run [options] -- "<user input>"
957
1516
 
958
1517
  Options:
959
- -r, --runner <runner> Runner: claude, codex, copilot (default: claude)
1518
+ -r, --runner <runner> Runner: claude, codex, gemini, opencode, copilot, pi (default: claude)
960
1519
  -m, --model <model> Model (default: claude-sonnet-4-20250514)
961
1520
  -c, --cwd <path> Working directory (default: cwd)
962
1521
  -s, --system-prompt <prompt> Custom system prompt
963
1522
  -t, --max-turns <n> Max conversation turns
964
1523
  -a, --allowed-tools <tools> Comma-separated allowed tools
965
1524
  --resume <session-id> Resume a previous session
966
- -o, --output-format <fmt> text | json | stream-json | stream (default: stream)
967
1525
  -h, --help Show this help
968
1526
 
969
1527
  Environment:
970
- ANTHROPIC_API_KEY Anthropic API key (required)
1528
+ ANTHROPIC_API_KEY Anthropic API key (for claude runner)
1529
+ OPENAI_API_KEY OpenAI API key (for codex runner)
1530
+ CODEX_API_KEY OpenAI API key alias (for codex runner)
1531
+ GEMINI_API_KEY Gemini API key (for gemini runner)
971
1532
  SANDAGENT_WORKSPACE Default workspace path
972
1533
  `);
973
1534
  }
@@ -1042,8 +1603,7 @@ async function main() {
1042
1603
  systemPrompt: args.systemPrompt,
1043
1604
  maxTurns: args.maxTurns,
1044
1605
  allowedTools: args.allowedTools,
1045
- resume: args.resume,
1046
- outputFormat: args.outputFormat
1606
+ resume: args.resume
1047
1607
  });
1048
1608
  break;
1049
1609
  }