@sandagent/runner-cli 0.1.1 → 0.1.2-beta.2

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
@@ -3,12 +3,244 @@
3
3
  // src/cli.ts
4
4
  import { parseArgs } from "node:util";
5
5
 
6
+ // ../../packages/runner-claude/dist/ai-sdk-stream.js
7
+ import { writeFile } from "node:fs/promises";
8
+ import { join } from "node:path";
9
+ var UNKNOWN_TOOL_NAME = "unknown-tool";
10
+ function formatDataStream(data) {
11
+ return `data: ${JSON.stringify(data)}
12
+
13
+ `;
14
+ }
15
+ function generateId() {
16
+ return `${Date.now()}-${Math.random().toString(36).slice(2, 9)}`;
17
+ }
18
+ function convertUsageToAISDK(usage) {
19
+ const inputTokens = usage.input_tokens ?? 0;
20
+ const outputTokens = usage.output_tokens ?? 0;
21
+ const cacheWrite = usage.cache_creation_input_tokens ?? 0;
22
+ const cacheRead = usage.cache_read_input_tokens ?? 0;
23
+ return {
24
+ inputTokens: {
25
+ total: inputTokens + cacheWrite + cacheRead,
26
+ noCache: inputTokens,
27
+ cacheRead,
28
+ cacheWrite
29
+ },
30
+ outputTokens: { total: outputTokens },
31
+ raw: usage
32
+ };
33
+ }
34
+ function mapFinishReason(subtype, isError) {
35
+ if (isError)
36
+ return "error";
37
+ switch (subtype) {
38
+ case "success":
39
+ return "stop";
40
+ case "error_max_turns":
41
+ return "length";
42
+ case "error_during_execution":
43
+ case "error_max_structured_output_retries":
44
+ return "error";
45
+ case void 0:
46
+ return "stop";
47
+ default:
48
+ return "other";
49
+ }
50
+ }
51
+ function extractToolUses(content) {
52
+ if (!Array.isArray(content))
53
+ return [];
54
+ return content.filter((item) => typeof item === "object" && item !== null && "type" in item && item.type === "tool_use").map((item) => ({
55
+ id: item.id || generateId(),
56
+ name: item.name || UNKNOWN_TOOL_NAME,
57
+ input: item.input
58
+ }));
59
+ }
60
+ var AISDKStreamConverter = class {
61
+ systemMessage;
62
+ hasEmittedStart = false;
63
+ accumulatedText = "";
64
+ textPartId;
65
+ streamedTextLength = 0;
66
+ // Track text already emitted via stream_events
67
+ hasReceivedStreamEvents = false;
68
+ sessionId;
69
+ partIdMap = /* @__PURE__ */ new Map();
70
+ /**
71
+ * Get the current session ID from the stream
72
+ */
73
+ get currentSessionId() {
74
+ if (!this.sessionId) {
75
+ throw new Error("Session ID is not set");
76
+ }
77
+ return this.sessionId;
78
+ }
79
+ /**
80
+ * Helper to emit SSE data
81
+ */
82
+ emit(data) {
83
+ return formatDataStream(data);
84
+ }
85
+ setPartId(index, partId) {
86
+ const partIdKey = `${this.currentSessionId}-${index}`;
87
+ this.partIdMap.set(partIdKey, partId);
88
+ }
89
+ getPartId(index) {
90
+ const partIdKey = `${this.currentSessionId}-${index}`;
91
+ if (this.partIdMap.has(partIdKey)) {
92
+ return this.partIdMap.get(partIdKey) ?? "";
93
+ }
94
+ throw new Error("Part ID not found");
95
+ }
96
+ /**
97
+ * Helper to emit tool call
98
+ */
99
+ *emitToolCall(message) {
100
+ const event = message.event;
101
+ if (event.type === "content_block_start" && event.content_block.type === "tool_use") {
102
+ const toolCallId = event.content_block.id;
103
+ this.setPartId(event.index, toolCallId);
104
+ yield this.emit({
105
+ type: "tool-input-start",
106
+ toolCallId,
107
+ toolName: event.content_block.name,
108
+ dynamic: true,
109
+ providerExecuted: true
110
+ });
111
+ }
112
+ if (event.type === "content_block_delta" && event.delta?.type === "input_json_delta") {
113
+ yield this.emit({
114
+ type: "tool-input-delta",
115
+ toolCallId: this.getPartId(event.index),
116
+ inputTextDelta: event.delta.partial_json
117
+ });
118
+ }
119
+ }
120
+ *emitTextBlockEvent(event) {
121
+ if (event.type === "content_block_start" && event.content_block.type === "text") {
122
+ const partId = `text_${generateId()}`;
123
+ this.setPartId(event.index, partId);
124
+ yield this.emit({
125
+ type: "text-start",
126
+ id: partId
127
+ });
128
+ }
129
+ if (event.type === "content_block_delta" && event.delta?.type === "text_delta") {
130
+ yield this.emit({
131
+ type: "text-delta",
132
+ id: this.getPartId(event.index),
133
+ delta: event.delta.text
134
+ });
135
+ }
136
+ if (event.type === "content_block_stop") {
137
+ const partId = this.getPartId(event.index);
138
+ if (partId.startsWith("text_")) {
139
+ yield this.emit({ type: "text-end", id: partId });
140
+ }
141
+ }
142
+ }
143
+ /**
144
+ * Stream SDK messages and convert to AI SDK UI Data Stream format
145
+ */
146
+ async *stream(messageIterator, options) {
147
+ const debugMessages = [];
148
+ const debugFile = `ai-sdk-stream-debug-${Date.now()}.json`;
149
+ try {
150
+ for await (const message of messageIterator) {
151
+ debugMessages.push(message);
152
+ if (message.type === "system" && message.subtype === "init") {
153
+ this.systemMessage = message;
154
+ this.sessionId = this.systemMessage.session_id;
155
+ }
156
+ if (message.type === "stream_event") {
157
+ const streamEvent = message;
158
+ const event = streamEvent.event;
159
+ if (event.type === "message_start" && !this.hasEmittedStart) {
160
+ this.hasEmittedStart = true;
161
+ yield this.emit({ type: "start", messageId: event.message.id });
162
+ yield this.emit({
163
+ type: "message-metadata",
164
+ messageMetadata: {
165
+ tools: this.systemMessage?.tools,
166
+ model: this.systemMessage?.model,
167
+ sessionId: this.systemMessage?.session_id,
168
+ agents: this.systemMessage?.agents,
169
+ skills: this.systemMessage?.skills
170
+ }
171
+ });
172
+ }
173
+ yield* this.emitTextBlockEvent(event);
174
+ yield* this.emitToolCall(streamEvent);
175
+ }
176
+ if (message.type === "assistant") {
177
+ const assistantMsg = message;
178
+ const content = assistantMsg.message?.content;
179
+ if (!content)
180
+ continue;
181
+ const tools = extractToolUses(content);
182
+ for (const tool of tools) {
183
+ yield this.emit({
184
+ type: "tool-input-available",
185
+ toolCallId: tool.id,
186
+ toolName: tool.name,
187
+ input: tool.input,
188
+ dynamic: true,
189
+ providerExecuted: true
190
+ });
191
+ }
192
+ }
193
+ if (message.type === "user") {
194
+ const userMsg = message;
195
+ const content = userMsg.message?.content;
196
+ for (const part of content) {
197
+ if (part.type === "tool_result") {
198
+ yield this.emit({
199
+ type: "tool-output-available",
200
+ toolCallId: part.tool_use_id,
201
+ output: message.tool_use_result || part.content,
202
+ dynamic: true,
203
+ providerExecuted: true
204
+ });
205
+ }
206
+ }
207
+ }
208
+ if (message.type === "result") {
209
+ const resultMsg = message;
210
+ const finishTime = (/* @__PURE__ */ new Date()).toISOString();
211
+ console.error(`[AISDKStream] Processing result message at ${finishTime}`);
212
+ const finishEvent = this.emit({
213
+ type: "finish",
214
+ finishReason: mapFinishReason(resultMsg.subtype, resultMsg.is_error),
215
+ messageMetadata: {
216
+ usage: convertUsageToAISDK(resultMsg.usage ?? {})
217
+ }
218
+ });
219
+ console.error(`[AISDKStream] Emitting finish event at ${(/* @__PURE__ */ new Date()).toISOString()}`);
220
+ yield finishEvent;
221
+ console.error(`[AISDKStream] Emitted [DONE] at ${(/* @__PURE__ */ new Date()).toISOString()}`);
222
+ }
223
+ }
224
+ } catch (error) {
225
+ debugMessages.push(error);
226
+ } finally {
227
+ if (debugMessages.length > 0) {
228
+ writeFile(join(process.cwd(), debugFile), JSON.stringify(debugMessages, null, 2), "utf-8").catch((writeError) => {
229
+ console.error(`[AISDKStream] Failed to write debug file:`, writeError);
230
+ });
231
+ }
232
+ options?.onCleanup?.();
233
+ yield `data: [DONE]
234
+
235
+ `;
236
+ }
237
+ }
238
+ };
239
+ function streamSDKMessagesToAISDKUI(messageIterator, options) {
240
+ return new AISDKStreamConverter().stream(messageIterator, options);
241
+ }
242
+
6
243
  // ../../packages/runner-claude/dist/claude-runner.js
7
- var SettingSource;
8
- (function(SettingSource2) {
9
- SettingSource2["user"] = "user";
10
- SettingSource2["project"] = "project";
11
- })(SettingSource || (SettingSource = {}));
12
244
  function createCanUseToolCallback(claudeOptions) {
13
245
  return async (toolName, input, options) => {
14
246
  const { toolUseID } = options;
@@ -48,7 +280,7 @@ function createCanUseToolCallback(claudeOptions) {
48
280
  }
49
281
  };
50
282
  }
51
- } catch (error) {
283
+ } catch {
52
284
  }
53
285
  await new Promise((resolve) => setTimeout(resolve, 500));
54
286
  }
@@ -83,22 +315,19 @@ var OPTIONAL_MODULES = {
83
315
  };
84
316
  function createClaudeRunner(options) {
85
317
  return {
86
- async *run(userInput, signal) {
87
- if (signal?.aborted) {
88
- return;
89
- }
318
+ async *run(userInput) {
90
319
  const apiKey = process.env.ANTHROPIC_API_KEY || process.env.AWS_BEARER_TOKEN_BEDROCK;
91
320
  if (!apiKey) {
92
321
  console.error("[SandAgent] Warning: ANTHROPIC_API_KEY or AWS_BEARER_TOKEN_BEDROCK not set. Using mock response.\nTo use the real Claude Agent SDK:\n1. Set ANTHROPIC_API_KEY or AWS_BEARER_TOKEN_BEDROCK environment variable\n2. Install the SDK: npm install @anthropic-ai/claude-agent-sdk");
93
- yield* runMockAgent(options, userInput, signal);
322
+ yield* runMockAgent(options, userInput, options.abortController?.signal);
94
323
  return;
95
324
  }
96
325
  const sdk = await loadClaudeAgentSDK();
97
326
  if (sdk) {
98
- yield* runWithClaudeAgentSDK(sdk, options, userInput, signal);
327
+ yield* runWithClaudeAgentSDK(sdk, options, userInput);
99
328
  } else {
100
329
  console.error("[SandAgent] Warning: @anthropic-ai/claude-agent-sdk not installed. Using mock response.\nInstall the SDK: npm install @anthropic-ai/claude-agent-sdk");
101
- yield* runMockAgent(options, userInput, signal);
330
+ yield* runMockAgent(options, userInput, options.abortController?.signal);
102
331
  }
103
332
  }
104
333
  };
@@ -115,31 +344,52 @@ async function loadClaudeAgentSDK() {
115
344
  return null;
116
345
  }
117
346
  }
118
- async function* runWithClaudeAgentSDK(sdk, options, userInput, signal) {
119
- const usage = { inputTokens: 0, outputTokens: 0 };
120
- let systemMessage;
121
- let messageId;
122
- const sdkOptions = {
347
+ async function* runWithClaudeAgentSDK(sdk, options, userInput) {
348
+ const outputFormat = options.outputFormat || "stream-json";
349
+ switch (outputFormat) {
350
+ case "text":
351
+ yield* runWithTextOutput(sdk, options, userInput);
352
+ break;
353
+ case "json":
354
+ yield* runWithJSONOutput(sdk, options, userInput);
355
+ break;
356
+ case "stream-json":
357
+ yield* runWithStreamJSONOutput(sdk, options, userInput);
358
+ break;
359
+ // case "stream":
360
+ default:
361
+ yield* runWithAISDKUIOutput(sdk, options, userInput);
362
+ break;
363
+ }
364
+ }
365
+ function createSDKOptions(options) {
366
+ return {
123
367
  model: options.model,
124
368
  systemPrompt: options.systemPrompt,
125
369
  maxTurns: options.maxTurns,
126
- allowedTools: [...options.allowedTools ?? [], "Skill"],
370
+ allowedTools: [
371
+ ...options.allowedTools ?? [],
372
+ "Skill",
373
+ "WebSearch",
374
+ "WebFetch"
375
+ ],
127
376
  cwd: options.cwd,
128
377
  env: options.env,
129
378
  resume: options.resume,
130
- settingSources: [SettingSource.project, SettingSource.user],
379
+ settingSources: ["project", "user"],
131
380
  canUseTool: createCanUseToolCallback(options),
132
381
  // Bypass all permission checks for automated execution
133
382
  permissionMode: "bypassPermissions",
134
- allowDangerouslySkipPermissions: true
383
+ allowDangerouslySkipPermissions: true,
384
+ // Enable partial messages for streaming
385
+ includePartialMessages: options.includePartialMessages
135
386
  };
136
- const queryIterator = sdk.query({
137
- prompt: userInput,
138
- options: sdkOptions
139
- });
387
+ }
388
+ function setupAbortHandler(queryIterator, signal) {
140
389
  const abortHandler = async () => {
141
- console.error("[ClaudeRunner] Operation aborted, calling query.interrupt()");
390
+ console.error("[ClaudeRunner] Abort signal received, will call query.interrupt()...");
142
391
  await queryIterator.interrupt();
392
+ console.error("[ClaudeRunner] query.interrupt() completed");
143
393
  };
144
394
  if (signal) {
145
395
  console.error("[ClaudeRunner] Signal provided, adding abort listener");
@@ -150,163 +400,71 @@ async function* runWithClaudeAgentSDK(sdk, options, userInput, signal) {
150
400
  } else {
151
401
  console.error("[ClaudeRunner] No signal provided");
152
402
  }
403
+ return abortHandler;
404
+ }
405
+ async function* runWithTextOutput(sdk, options, userInput, signal) {
406
+ const sdkOptions = createSDKOptions(options);
407
+ const queryIterator = sdk.query({ prompt: userInput, options: sdkOptions });
408
+ const abortHandler = setupAbortHandler(queryIterator, options.abortController?.signal);
153
409
  try {
410
+ let resultText = "";
154
411
  for await (const message of queryIterator) {
155
- if (message.type === "system" && message.subtype === "init") {
156
- systemMessage = message;
157
- continue;
158
- }
159
- if (message.type === "assistant" && !messageId && systemMessage) {
160
- messageId = message.message.id;
161
- yield formatDataStream({
162
- type: "start",
163
- messageId
164
- });
165
- yield formatDataStream({
166
- type: "message-metadata",
167
- messageMetadata: {
168
- tools: systemMessage.tools,
169
- model: systemMessage.model,
170
- sessionId: systemMessage.session_id
171
- }
172
- });
173
- }
174
- const chunks = convertSDKMessageToAISDKUI(message);
175
- for (const chunk of chunks) {
176
- yield chunk;
412
+ if (message.type === "result") {
413
+ const resultMsg = message;
414
+ if (resultMsg.subtype === "success") {
415
+ resultText = resultMsg.result || "";
416
+ }
177
417
  }
178
418
  }
179
- } catch (error) {
180
- const errorMessage = error instanceof Error ? error.message : "Unknown error occurred";
181
- console.error("[ClaudeRunner] Error:", errorMessage);
182
- yield formatDataStream({ type: "error", errorText: errorMessage });
183
- yield formatDataStream({
184
- type: "finish",
185
- finishReason: "error",
186
- usage: {
187
- promptTokens: usage.inputTokens,
188
- completionTokens: usage.outputTokens
189
- }
190
- });
419
+ yield resultText;
191
420
  } finally {
192
421
  if (signal) {
193
422
  signal.removeEventListener("abort", abortHandler);
194
423
  }
195
- yield `data: [DONE]
196
-
197
- `;
198
424
  }
199
425
  }
200
- function convertSDKMessageToAISDKUI(message) {
201
- const chunks = [];
202
- switch (message.type) {
203
- case "assistant": {
204
- const assistantMsg = message;
205
- if (assistantMsg.error) {
206
- const errorDetail = message.message.content.map((c) => c.text).join("\n");
207
- chunks.push(formatDataStream({
208
- type: "error",
209
- errorText: `${assistantMsg.error}: ${errorDetail}`
210
- }));
211
- break;
212
- }
213
- if (assistantMsg.message) {
214
- if (typeof assistantMsg.message === "string") {
215
- const textId = generateId();
216
- chunks.push(formatDataStream({ type: "text-start", id: textId }));
217
- chunks.push(formatDataStream({
218
- type: "text-delta",
219
- id: textId,
220
- delta: assistantMsg.message
221
- }));
222
- chunks.push(formatDataStream({ type: "text-end", id: textId }));
223
- } else if (assistantMsg.message.content && Array.isArray(assistantMsg.message.content)) {
224
- for (const block of assistantMsg.message.content) {
225
- if (block.type === "text" && block.text) {
226
- const textId = generateId();
227
- chunks.push(formatDataStream({ type: "text-start", id: textId }));
228
- chunks.push(formatDataStream({
229
- type: "text-delta",
230
- id: textId,
231
- delta: block.text
232
- }));
233
- chunks.push(formatDataStream({ type: "text-end", id: textId }));
234
- } else if (block.type === "tool_use") {
235
- const toolCallId = block.id || generateId();
236
- chunks.push(formatDataStream({
237
- type: "tool-input-start",
238
- toolCallId,
239
- toolName: block.name,
240
- dynamic: true
241
- }));
242
- if (block.input) {
243
- chunks.push(formatDataStream({
244
- type: "tool-input-available",
245
- toolCallId,
246
- toolName: block.name,
247
- dynamic: true,
248
- input: block.input
249
- }));
250
- }
251
- }
252
- }
253
- }
426
+ async function* runWithJSONOutput(sdk, options, userInput, signal) {
427
+ const sdkOptions = createSDKOptions(options);
428
+ const queryIterator = sdk.query({ prompt: userInput, options: sdkOptions });
429
+ const abortHandler = setupAbortHandler(queryIterator, signal);
430
+ try {
431
+ let resultMessage = null;
432
+ for await (const message of queryIterator) {
433
+ if (message.type === "result") {
434
+ resultMessage = message;
254
435
  }
255
- break;
256
436
  }
257
- case "result": {
258
- const resultMsg = message;
259
- chunks.push(formatDataStream({
260
- type: "finish",
261
- finishReason: resultMsg.is_error ? "error" : "stop",
262
- messageMetadata: {
263
- usage: resultMsg.usage,
264
- duration_ms: resultMsg.duration_ms,
265
- num_turns: resultMsg.num_turns,
266
- total_cost_usd: resultMsg.total_cost_usd
267
- }
268
- }));
269
- break;
437
+ if (resultMessage) {
438
+ yield JSON.stringify(resultMessage) + "\n";
270
439
  }
271
- case "user": {
272
- const usrMsg = message;
273
- if (usrMsg.isSynthetic) {
274
- break;
275
- }
276
- const contentArray = usrMsg.message.content;
277
- if (usrMsg.tool_use_result || Array.isArray(contentArray) && contentArray.some((c) => c.type === "tool_result")) {
278
- for (const tool of contentArray) {
279
- if (tool.is_error) {
280
- chunks.push(formatDataStream({
281
- type: "tool-output-error",
282
- toolCallId: tool.tool_use_id,
283
- errorText: tool.content,
284
- dynamic: true
285
- }));
286
- } else {
287
- chunks.push(formatDataStream({
288
- type: "tool-output-available",
289
- toolCallId: tool.tool_use_id,
290
- output: usrMsg.tool_use_result || tool.content,
291
- dynamic: true
292
- }));
293
- }
294
- }
295
- }
296
- break;
440
+ } finally {
441
+ if (signal) {
442
+ signal.removeEventListener("abort", abortHandler);
297
443
  }
298
- default:
299
- break;
300
444
  }
301
- return chunks;
302
445
  }
303
- function formatDataStream(data) {
304
- return `data: ${JSON.stringify(data)}
305
-
306
- `;
446
+ async function* runWithStreamJSONOutput(sdk, options, userInput, signal) {
447
+ const sdkOptions = createSDKOptions(options);
448
+ const queryIterator = sdk.query({ prompt: userInput, options: sdkOptions });
449
+ const abortHandler = setupAbortHandler(queryIterator, signal);
450
+ try {
451
+ for await (const message of queryIterator) {
452
+ yield JSON.stringify(message) + "\n";
453
+ }
454
+ } finally {
455
+ if (signal) {
456
+ signal.removeEventListener("abort", abortHandler);
457
+ }
458
+ }
307
459
  }
308
- function generateId() {
309
- return `${Date.now()}-${Math.random().toString(36).substring(2, 9)}`;
460
+ async function* runWithAISDKUIOutput(sdk, options, userInput) {
461
+ const sdkOptions = createSDKOptions({
462
+ ...options,
463
+ includePartialMessages: true
464
+ });
465
+ const queryIterator = sdk.query({ prompt: userInput, options: sdkOptions });
466
+ setupAbortHandler(queryIterator, options.abortController?.signal);
467
+ yield* streamSDKMessagesToAISDKUI(queryIterator);
310
468
  }
311
469
  async function* runMockAgent(options, userInput, signal) {
312
470
  if (signal?.aborted) {
@@ -314,6 +472,11 @@ async function* runMockAgent(options, userInput, signal) {
314
472
  return;
315
473
  }
316
474
  try {
475
+ const messageId = generateId();
476
+ yield formatDataStream({
477
+ type: "start",
478
+ messageId
479
+ });
317
480
  const response = `I received your request: "${userInput}"
318
481
 
319
482
  Model: ${options.model}
@@ -341,7 +504,13 @@ Documentation: https://platform.claude.com/docs/en/agent-sdk/typescript-v2-previ
341
504
  yield formatDataStream({ type: "text-end", id: textId });
342
505
  yield formatDataStream({
343
506
  type: "finish",
344
- finishReason: "stop"
507
+ finishReason: mapFinishReason("success"),
508
+ usage: convertUsageToAISDK({
509
+ input_tokens: 0,
510
+ output_tokens: 0,
511
+ cache_creation_input_tokens: 0,
512
+ cache_read_input_tokens: 0
513
+ })
345
514
  });
346
515
  yield `data: [DONE]
347
516
 
@@ -352,7 +521,13 @@ Documentation: https://platform.claude.com/docs/en/agent-sdk/typescript-v2-previ
352
521
  yield formatDataStream({ type: "error", errorText: errorMessage });
353
522
  yield formatDataStream({
354
523
  type: "finish",
355
- finishReason: "error"
524
+ finishReason: mapFinishReason("error_during_execution", true),
525
+ usage: convertUsageToAISDK({
526
+ input_tokens: 0,
527
+ output_tokens: 0,
528
+ cache_creation_input_tokens: 0,
529
+ cache_read_input_tokens: 0
530
+ })
356
531
  });
357
532
  yield `data: [DONE]
358
533
 
@@ -363,28 +538,47 @@ Documentation: https://platform.claude.com/docs/en/agent-sdk/typescript-v2-previ
363
538
  // src/runner.ts
364
539
  async function runAgent(options) {
365
540
  const abortController = new AbortController();
366
- const signalHandler = () => {
541
+ const signalHandler = async () => {
367
542
  console.error("[Runner] Received termination signal, stopping...");
368
543
  abortController.abort();
369
- console.error("[Runner] AbortController.abort() called");
544
+ console.error(
545
+ "[Runner] AbortController.abort() completed (listeners triggered)"
546
+ );
370
547
  };
371
548
  process.on("SIGTERM", signalHandler);
372
549
  process.on("SIGINT", signalHandler);
373
550
  console.error("[Runner] Signal handlers registered");
374
551
  try {
375
- const runnerOptions = {
376
- model: options.model,
377
- systemPrompt: options.systemPrompt,
378
- maxTurns: options.maxTurns,
379
- allowedTools: options.allowedTools,
380
- resume: options.resume,
381
- approvalDir: options.approvalDir
382
- };
383
- const runner = createClaudeRunner(runnerOptions);
384
- for await (const chunk of runner.run(
385
- options.userInput,
386
- abortController.signal
387
- )) {
552
+ let runner;
553
+ switch (options.runner) {
554
+ case "claude": {
555
+ const runnerOptions = {
556
+ model: options.model,
557
+ systemPrompt: options.systemPrompt,
558
+ maxTurns: options.maxTurns,
559
+ allowedTools: options.allowedTools,
560
+ resume: options.resume,
561
+ approvalDir: options.approvalDir,
562
+ outputFormat: options.outputFormat,
563
+ abortController
564
+ };
565
+ runner = createClaudeRunner(runnerOptions);
566
+ break;
567
+ }
568
+ case "codex":
569
+ throw new Error(
570
+ "Codex runner not yet implemented. Use --runner=claude for now."
571
+ );
572
+ case "copilot":
573
+ throw new Error(
574
+ "Copilot runner not yet implemented. Use --runner=claude for now."
575
+ );
576
+ default:
577
+ throw new Error(
578
+ `Unknown runner: ${options.runner}. Supported runners: claude, codex, copilot`
579
+ );
580
+ }
581
+ for await (const chunk of runner.run(options.userInput)) {
388
582
  process.stdout.write(chunk);
389
583
  }
390
584
  } finally {
@@ -397,6 +591,11 @@ async function runAgent(options) {
397
591
  function parseCliArgs() {
398
592
  const { values, positionals } = parseArgs({
399
593
  options: {
594
+ runner: {
595
+ type: "string",
596
+ short: "r",
597
+ default: "claude"
598
+ },
400
599
  model: {
401
600
  type: "string",
402
601
  short: "m",
@@ -407,11 +606,6 @@ function parseCliArgs() {
407
606
  short: "c",
408
607
  default: process.env.SANDAGENT_WORKSPACE ?? process.cwd()
409
608
  },
410
- template: {
411
- type: "string",
412
- short: "T",
413
- default: process.env.SANDAGENT_TEMPLATE ?? "default"
414
- },
415
609
  "system-prompt": {
416
610
  type: "string",
417
611
  short: "s"
@@ -431,6 +625,10 @@ function parseCliArgs() {
431
625
  "approval-dir": {
432
626
  type: "string"
433
627
  },
628
+ "output-format": {
629
+ type: "string",
630
+ short: "o"
631
+ },
434
632
  help: {
435
633
  type: "boolean",
436
634
  short: "h"
@@ -460,15 +658,30 @@ function parseCliArgs() {
460
658
  console.error('Usage: sandagent run [options] -- "<user input>"');
461
659
  process.exit(1);
462
660
  }
661
+ const runner = values.runner;
662
+ if (!["claude", "codex", "copilot"].includes(runner)) {
663
+ console.error(
664
+ 'Error: --runner must be one of: "claude", "codex", "copilot"'
665
+ );
666
+ process.exit(1);
667
+ }
668
+ const outputFormat = values["output-format"];
669
+ if (outputFormat && !["text", "json", "stream-json", "stream"].includes(outputFormat)) {
670
+ console.error(
671
+ 'Error: --output-format must be one of: "text", "json", "stream-json", "stream"'
672
+ );
673
+ process.exit(1);
674
+ }
463
675
  return {
676
+ runner,
464
677
  model: values.model,
465
678
  cwd: values.cwd,
466
- template: values.template,
467
679
  systemPrompt: values["system-prompt"],
468
680
  maxTurns: values["max-turns"] ? Number.parseInt(values["max-turns"], 10) : void 0,
469
681
  allowedTools: values["allowed-tools"]?.split(",").map((t) => t.trim()),
470
682
  resume: values.resume,
471
683
  approvalDir: values["approval-dir"],
684
+ outputFormat: outputFormat ?? "stream",
472
685
  userInput
473
686
  };
474
687
  }
@@ -482,43 +695,29 @@ Streams AI SDK UI messages directly to stdout.
482
695
  Usage:
483
696
  sandagent run [options] -- "<user input>"
484
697
 
485
- # Or run from a template directory:
486
- cd templates/coder
487
- sandagent run -- "Build a REST API"
488
-
489
698
  Options:
699
+ -r, --runner <runner> Runner to use: claude, codex, copilot (default: claude)
490
700
  -m, --model <model> Model to use (default: claude-sonnet-4-20250514)
491
701
  -c, --cwd <path> Working directory (default: current directory)
492
- -T, --template <name> Template to use (default: default)
493
- Available: default, coder, analyst, researcher
494
- -s, --system-prompt <prompt> Custom system prompt (overrides template)
702
+ -s, --system-prompt <prompt> Custom system prompt
495
703
  -t, --max-turns <n> Maximum conversation turns
496
704
  -a, --allowed-tools <tools> Comma-separated list of allowed tools
497
705
  -r, --resume <session-id> Resume a previous session
706
+ -o, --output-format <format> Output format (default: stream)
707
+ Available: text, json(single result), stream-json(realtime streaming), stream(ai sdk ui sse format)
498
708
  -h, --help Show this help message
499
709
 
500
710
  Environment Variables:
501
711
  ANTHROPIC_API_KEY Anthropic API key (required)
502
712
  SANDAGENT_WORKSPACE Default workspace path
503
- SANDAGENT_TEMPLATE Default template to use
504
713
  SANDAGENT_LOG_LEVEL Logging level (debug, info, warn, error)
505
714
 
506
- Templates:
507
- default General-purpose assistant
508
- coder Optimized for software development
509
- analyst Optimized for data analysis
510
- researcher Optimized for research tasks
511
-
512
715
  Examples:
513
- # Run with default template
716
+ # Run with default settings
514
717
  sandagent run -- "Create a hello world script"
515
718
 
516
- # Run from a template directory (recommended)
517
- cd templates/coder
518
- sandagent run -- "Build a REST API with Express"
519
-
520
- # Use a specific template
521
- sandagent run --template analyst -- "Analyze sales.csv"
719
+ # Run with custom system prompt
720
+ sandagent run --system-prompt "You are a coding assistant" -- "Build a REST API with Express"
522
721
 
523
722
  # Specify working directory
524
723
  sandagent run --cwd ./my-project -- "Fix the bug in main.ts"
@@ -528,14 +727,15 @@ async function main() {
528
727
  const args = parseCliArgs();
529
728
  process.chdir(args.cwd);
530
729
  await runAgent({
730
+ runner: args.runner,
531
731
  model: args.model,
532
- template: args.template,
533
732
  userInput: args.userInput,
534
733
  systemPrompt: args.systemPrompt,
535
734
  maxTurns: args.maxTurns,
536
735
  allowedTools: args.allowedTools,
537
736
  resume: args.resume,
538
- approvalDir: args.approvalDir
737
+ approvalDir: args.approvalDir,
738
+ outputFormat: args.outputFormat
539
739
  });
540
740
  }
541
741
  main().catch((error) => {