@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/LICENSE +201 -0
- package/README.md +16 -33
- 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 +718 -158
- 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 +49 -10
- package/dist/runner.js.map +1 -1
- package/package.json +27 -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
|
|
@@ -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.
|
|
317
|
-
if (partId
|
|
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
|
-
|
|
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
|
-
|
|
431
|
-
|
|
432
|
-
|
|
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
|
-
|
|
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((
|
|
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
|
-
|
|
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
|
-
|
|
615
|
-
|
|
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((
|
|
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
|
-
|
|
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
|
-
|
|
1324
|
+
env: process.env,
|
|
796
1325
|
abortController
|
|
797
|
-
};
|
|
798
|
-
|
|
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: --
|
|
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 (
|
|
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
|
}
|