@bunny-agent/runner-cli 0.9.28

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.
@@ -0,0 +1,2710 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/cli.ts
4
+ import { resolve as resolve3 } from "node:path";
5
+ import { config } from "dotenv";
6
+ import { parseArgs } from "node:util";
7
+
8
+ // src/build-image.ts
9
+ import { execSync } from "node:child_process";
10
+ import {
11
+ copyFileSync,
12
+ existsSync,
13
+ mkdirSync,
14
+ readdirSync,
15
+ readFileSync,
16
+ statSync,
17
+ writeFileSync
18
+ } from "node:fs";
19
+ import { basename, dirname, join, resolve } from "node:path";
20
+ import { fileURLToPath } from "node:url";
21
+ function getPackageRoot() {
22
+ const thisDir = dirname(fileURLToPath(import.meta.url));
23
+ return resolve(thisDir, "..");
24
+ }
25
+ function getShippedDockerfile() {
26
+ const packageRoot = getPackageRoot();
27
+ const candidates = [
28
+ join(packageRoot, "Dockerfile"),
29
+ resolve(
30
+ packageRoot,
31
+ "..",
32
+ "..",
33
+ "docker",
34
+ "bunny-agent-claude",
35
+ "Dockerfile"
36
+ )
37
+ ];
38
+ for (const p of candidates) {
39
+ if (existsSync(p)) return p;
40
+ }
41
+ console.error(
42
+ `\u274C Dockerfile not found. Searched:
43
+ ${candidates.map((c) => ` ${c}`).join("\n")}`
44
+ );
45
+ process.exit(1);
46
+ }
47
+ function run(cmd, cwd) {
48
+ execSync(cmd, { stdio: "inherit", cwd });
49
+ }
50
+ function ensureDocker() {
51
+ try {
52
+ execSync("docker info", { stdio: "ignore" });
53
+ } catch {
54
+ console.error("\u274C Docker is not running. Please start Docker first.");
55
+ process.exit(1);
56
+ }
57
+ }
58
+ function resolveTemplatePath(template) {
59
+ const abs = resolve(process.cwd(), template);
60
+ if (!existsSync(abs)) {
61
+ console.error(`\u274C Template directory not found: ${abs}`);
62
+ process.exit(1);
63
+ }
64
+ return abs;
65
+ }
66
+ function copyDirSync(src, dest) {
67
+ mkdirSync(dest, { recursive: true });
68
+ for (const entry of readdirSync(src)) {
69
+ const srcPath = join(src, entry);
70
+ const destPath = join(dest, entry);
71
+ if (statSync(srcPath).isDirectory()) {
72
+ copyDirSync(srcPath, destPath);
73
+ } else {
74
+ copyFileSync(srcPath, destPath);
75
+ }
76
+ }
77
+ }
78
+ async function buildImage(opts) {
79
+ const templatePath = opts.template ? resolveTemplatePath(opts.template) : null;
80
+ const templateName = templatePath ? basename(templatePath) : null;
81
+ const localImage = opts.image ?? `${opts.name}:${opts.tag}`;
82
+ console.log("\u{1F4E6} BunnyAgent Docker Image Builder");
83
+ console.log("========================");
84
+ console.log(` Image: ${localImage}`);
85
+ console.log(` Platform: ${opts.platform}`);
86
+ console.log(` Template: ${templateName ?? "(none)"}`);
87
+ console.log(` Push: ${opts.push}`);
88
+ console.log("");
89
+ ensureDocker();
90
+ const buildContext = join(process.cwd(), ".docker-staging");
91
+ mkdirSync(buildContext, { recursive: true });
92
+ let dockerfile = readFileSync(getShippedDockerfile(), "utf8");
93
+ if (templatePath && templateName) {
94
+ const destDir = join(buildContext, "templates", templateName);
95
+ mkdirSync(destDir, { recursive: true });
96
+ const claudeMd = join(templatePath, "CLAUDE.md");
97
+ if (existsSync(claudeMd))
98
+ copyFileSync(claudeMd, join(destDir, "CLAUDE.md"));
99
+ const claudeDir = join(templatePath, ".claude");
100
+ if (existsSync(claudeDir)) copyDirSync(claudeDir, join(destDir, ".claude"));
101
+ let copyLines = "\n# Template files\nRUN mkdir -p /opt/bunny-agent/templates";
102
+ if (existsSync(join(destDir, "CLAUDE.md"))) {
103
+ copyLines += `
104
+ COPY templates/${templateName}/CLAUDE.md /opt/bunny-agent/templates/CLAUDE.md`;
105
+ }
106
+ if (existsSync(join(destDir, ".claude"))) {
107
+ copyLines += `
108
+ COPY templates/${templateName}/.claude /opt/bunny-agent/templates/.claude`;
109
+ }
110
+ dockerfile = dockerfile.replace(/^CMD /m, `${copyLines}
111
+
112
+ CMD `);
113
+ console.log("\u{1F9E9} Injected template files into Dockerfile");
114
+ }
115
+ writeFileSync(join(buildContext, "Dockerfile"), dockerfile);
116
+ console.log("\u{1F433} Building Docker image...");
117
+ run(
118
+ `docker build --platform=${opts.platform} -t ${localImage} -f ${join(buildContext, "Dockerfile")} ${buildContext}`
119
+ );
120
+ console.log(`
121
+ \u2705 Image built: ${localImage}`);
122
+ if (!opts.push) return;
123
+ if (!localImage.includes("/")) {
124
+ console.error(
125
+ "\u274C --push requires --name to include namespace (e.g. vikadata/bunny-agent-seo)"
126
+ );
127
+ process.exit(1);
128
+ }
129
+ console.log("\u{1F680} Pushing image...");
130
+ run(`docker push ${localImage}`);
131
+ console.log(`
132
+ \u2705 Image pushed: ${localImage}`);
133
+ const latestImage = `${opts.name}:latest`;
134
+ if (localImage !== latestImage) {
135
+ console.log("\u{1F3F7}\uFE0F Tagging and pushing latest...");
136
+ run(`docker tag ${localImage} ${latestImage}`);
137
+ run(`docker push ${latestImage}`);
138
+ console.log(`\u2705 Image pushed: ${latestImage}`);
139
+ }
140
+ }
141
+
142
+ // ../../packages/runner-harness/dist/agent-context.js
143
+ var BUNNY_AGENT_SYSTEM_PROMPT = [
144
+ "You are Bunny Agent \u2014 an AI agent built to protect humans and push humanity forward.",
145
+ "",
146
+ "Your mission: work autonomously, accurately, and efficiently to complete tasks.",
147
+ "Be targeted and precise in your exploration and investigations.",
148
+ "",
149
+ "## Core Values",
150
+ "Protect human. Push humanity forward.",
151
+ "",
152
+ "## Tool-Use Rules",
153
+ "You MUST use your tools to take action \u2014 do not describe what you would do without actually doing it.",
154
+ "When you say you will perform an action, you MUST immediately make the corresponding tool call.",
155
+ "",
156
+ "NEVER answer these from memory or mental computation \u2014 ALWAYS use a tool:",
157
+ "- Arithmetic, math, calculations \u2192 use bash (python3 -c '...') for ALL computation",
158
+ "- Hashes, encodings, checksums \u2192 use bash (e.g. sha256sum, base64, python3)",
159
+ "- Current facts, counts, prices, dates \u2192 use web_search or web_fetch; never guess",
160
+ "- File contents \u2192 use bash or read_file",
161
+ "",
162
+ "Keep working until the task is fully complete and verified.",
163
+ "If a source returns empty or partial results, retry with a different query or strategy.",
164
+ "",
165
+ "## Research Methodology",
166
+ "1. Search for the specific source document (official website, Wikipedia, academic paper)",
167
+ "2. Verify you are looking at the correct version/date/edition if specified",
168
+ "3. Extract the exact data requested \u2014 do not summarize or paraphrase",
169
+ "4. Use Python (bash tool) for all arithmetic, counting, and unit conversions",
170
+ "5. Cross-check your answer against the question requirements before outputting",
171
+ "",
172
+ "## Operational Rules",
173
+ "- Always construct absolute file paths for file system operations",
174
+ "- Prefer parallel tool calls for independent lookups",
175
+ "- Verify file contents and structure before making assumptions",
176
+ "- Use flags like -y, --yes to prevent CLI tools from hanging on prompts",
177
+ "- If missing context is retrievable by tools, use the tool \u2014 ask only when not retrievable",
178
+ "",
179
+ "## Output Format",
180
+ "Provide the answer ONLY \u2014 no preamble, no 'The answer is', no explanation unless explicitly asked.",
181
+ "If the answer is a number, give only the number. If it's a name, give the exact name as it appears in the source."
182
+ ].join("\n");
183
+
184
+ // ../../packages/runner-harness/dist/prompt.js
185
+ import { existsSync as existsSync2, readFileSync as readFileSync2 } from "node:fs";
186
+ import { dirname as dirname2, join as join2 } from "node:path";
187
+ var PROMPT_FILES = ["CLAUDE.md", "AGENTS.md"];
188
+ function loadSystemPrompt(cwd) {
189
+ let dir = cwd;
190
+ for (let i = 0; i < 5; i++) {
191
+ for (const name of PROMPT_FILES) {
192
+ const p = join2(dir, name);
193
+ if (existsSync2(p)) {
194
+ try {
195
+ return readFileSync2(p, "utf8").trim() || void 0;
196
+ } catch {
197
+ }
198
+ }
199
+ }
200
+ const parent = dirname2(dir);
201
+ if (parent === dir)
202
+ break;
203
+ dir = parent;
204
+ }
205
+ return void 0;
206
+ }
207
+
208
+ // ../../packages/runner-claude/dist/ai-sdk-stream.js
209
+ import { appendFileSync, existsSync as existsSync3, unlinkSync } from "node:fs";
210
+ import { join as join3 } from "node:path";
211
+ function trace(data, reset = false) {
212
+ if (process.env.DEBUG !== "true")
213
+ return;
214
+ try {
215
+ const file = join3(process.cwd(), "claude-message-stream-debug.json");
216
+ if (reset && existsSync3(file))
217
+ unlinkSync(file);
218
+ const entry = {
219
+ _t: (/* @__PURE__ */ new Date()).toISOString(),
220
+ ...typeof data === "object" && data !== null ? data : { value: data }
221
+ };
222
+ appendFileSync(file, JSON.stringify(entry, null, 2) + ",\n");
223
+ } catch {
224
+ }
225
+ }
226
+ var UNKNOWN_TOOL_NAME = "unknown-tool";
227
+ function formatDataStream(data) {
228
+ return `data: ${JSON.stringify(data)}
229
+
230
+ `;
231
+ }
232
+ function generateId() {
233
+ return `${Date.now()}-${Math.random().toString(36).slice(2, 9)}`;
234
+ }
235
+ function isAbortError(err) {
236
+ if (err && typeof err === "object") {
237
+ const e = err;
238
+ if (typeof e.name === "string" && e.name === "AbortError")
239
+ return true;
240
+ if (typeof e.code === "string" && e.code.toUpperCase() === "ABORT_ERR")
241
+ return true;
242
+ }
243
+ return false;
244
+ }
245
+ function convertUsageToAISDK(usage) {
246
+ const inputTokens = usage.input_tokens ?? 0;
247
+ const outputTokens = usage.output_tokens ?? 0;
248
+ const cacheWrite = usage.cache_creation_input_tokens ?? 0;
249
+ const cacheRead = usage.cache_read_input_tokens ?? 0;
250
+ const usageAny = usage;
251
+ const textTokens = typeof usageAny.text_tokens === "number" ? usageAny.text_tokens : void 0;
252
+ const reasoningTokens = typeof usageAny.reasoning_tokens === "number" ? usageAny.reasoning_tokens : void 0;
253
+ return {
254
+ inputTokens: {
255
+ total: inputTokens + cacheWrite + cacheRead,
256
+ noCache: inputTokens,
257
+ cacheRead,
258
+ cacheWrite
259
+ },
260
+ outputTokens: {
261
+ total: outputTokens,
262
+ text: textTokens,
263
+ reasoning: reasoningTokens
264
+ },
265
+ raw: usage
266
+ };
267
+ }
268
+ function mapFinishReason(subtype, isError) {
269
+ if (isError)
270
+ return "error";
271
+ switch (subtype) {
272
+ case "success":
273
+ return "stop";
274
+ case "error_max_turns":
275
+ return "length";
276
+ case "error_during_execution":
277
+ case "error_max_structured_output_retries":
278
+ return "error";
279
+ case void 0:
280
+ return "stop";
281
+ default:
282
+ return "other";
283
+ }
284
+ }
285
+ function extractToolUses(content) {
286
+ if (!Array.isArray(content))
287
+ return [];
288
+ return content.filter((item) => typeof item === "object" && item !== null && "type" in item && item.type === "tool_use").map((item) => ({
289
+ id: item.id || generateId(),
290
+ name: item.name || UNKNOWN_TOOL_NAME,
291
+ input: item.input
292
+ }));
293
+ }
294
+ var AISDKStreamConverter = class {
295
+ systemMessage;
296
+ hasEmittedStart = false;
297
+ sessionId;
298
+ /** 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. */
299
+ errorEmitted = false;
300
+ partIdMap = /* @__PURE__ */ new Map();
301
+ /**
302
+ * Get the current session ID from the stream
303
+ */
304
+ get currentSessionId() {
305
+ if (!this.sessionId) {
306
+ throw new Error("Session ID is not set");
307
+ }
308
+ return this.sessionId;
309
+ }
310
+ /**
311
+ * Helper to emit SSE data
312
+ */
313
+ emit(data) {
314
+ return formatDataStream(data);
315
+ }
316
+ setPartId(index, partId) {
317
+ const partIdKey = `${this.currentSessionId}-${index}`;
318
+ this.partIdMap.set(partIdKey, partId);
319
+ }
320
+ getPartId(index) {
321
+ const partIdKey = `${this.currentSessionId}-${index}`;
322
+ if (this.partIdMap.has(partIdKey)) {
323
+ return this.partIdMap.get(partIdKey) ?? "";
324
+ }
325
+ throw new Error("Part ID not found");
326
+ }
327
+ /** Returns part ID for index or undefined if not tracked (e.g. content_block_stop without a prior start). */
328
+ tryGetPartId(index) {
329
+ if (!this.sessionId)
330
+ return void 0;
331
+ const partIdKey = `${this.sessionId}-${index}`;
332
+ return this.partIdMap.get(partIdKey);
333
+ }
334
+ /**
335
+ * Helper to emit tool call
336
+ */
337
+ *emitToolCall(message) {
338
+ const event = message.event;
339
+ if (event.type === "content_block_start" && event.content_block.type === "tool_use") {
340
+ const toolCallId = event.content_block.id;
341
+ this.setPartId(event.index, toolCallId);
342
+ yield this.emit({
343
+ type: "tool-input-start",
344
+ toolCallId,
345
+ toolName: event.content_block.name,
346
+ dynamic: true,
347
+ providerExecuted: true
348
+ });
349
+ }
350
+ if (event.type === "content_block_delta" && event.delta?.type === "input_json_delta") {
351
+ yield this.emit({
352
+ type: "tool-input-delta",
353
+ toolCallId: this.getPartId(event.index),
354
+ inputTextDelta: event.delta.partial_json
355
+ });
356
+ }
357
+ }
358
+ *emitTextBlockEvent(event) {
359
+ if (event.type === "content_block_start" && event.content_block.type === "text") {
360
+ const partId = `text_${generateId()}`;
361
+ this.setPartId(event.index, partId);
362
+ yield this.emit({
363
+ type: "text-start",
364
+ id: partId
365
+ });
366
+ }
367
+ if (event.type === "content_block_delta" && event.delta?.type === "text_delta") {
368
+ yield this.emit({
369
+ type: "text-delta",
370
+ id: this.getPartId(event.index),
371
+ delta: event.delta.text
372
+ });
373
+ }
374
+ if (event.type === "content_block_start" && event.content_block.type === "thinking") {
375
+ const partId = `reasoning_${generateId()}`;
376
+ this.setPartId(event.index, partId);
377
+ }
378
+ if (event.type === "content_block_delta" && event.delta?.type === "thinking_delta") {
379
+ if (event.delta.thinking) {
380
+ yield this.emit({ type: "reasoning", text: event.delta.thinking });
381
+ }
382
+ }
383
+ if (event.type === "content_block_stop") {
384
+ const partId = this.tryGetPartId(event.index);
385
+ if (partId?.startsWith("text_")) {
386
+ yield this.emit({ type: "text-end", id: partId });
387
+ }
388
+ }
389
+ }
390
+ /**
391
+ * Stream SDK messages and convert to AI SDK UI Data Stream format
392
+ */
393
+ async *stream(messageIterator) {
394
+ try {
395
+ for await (const message of messageIterator) {
396
+ if (message.type === "system" && message.subtype === "init") {
397
+ trace(null, true);
398
+ this.systemMessage = message;
399
+ this.sessionId = this.systemMessage.session_id;
400
+ }
401
+ trace(message);
402
+ if (message.type === "stream_event") {
403
+ const streamEvent = message;
404
+ const event = streamEvent.event;
405
+ if (event.type === "message_start" && !this.hasEmittedStart) {
406
+ this.hasEmittedStart = true;
407
+ yield this.emit({ type: "start", messageId: event.message.id });
408
+ yield this.emit({
409
+ type: "message-metadata",
410
+ messageMetadata: {
411
+ tools: this.systemMessage?.tools,
412
+ model: this.systemMessage?.model,
413
+ sessionId: this.systemMessage?.session_id,
414
+ agents: this.systemMessage?.agents,
415
+ skills: this.systemMessage?.skills
416
+ }
417
+ });
418
+ }
419
+ yield* this.emitTextBlockEvent(event);
420
+ yield* this.emitToolCall(streamEvent);
421
+ }
422
+ if (message.type === "assistant") {
423
+ const assistantMsg = message;
424
+ const content = assistantMsg.message?.content;
425
+ if (!content)
426
+ continue;
427
+ const tools = extractToolUses(content);
428
+ for (const tool of tools) {
429
+ yield this.emit({
430
+ type: "tool-input-available",
431
+ toolCallId: tool.id,
432
+ toolName: tool.name,
433
+ input: tool.input,
434
+ dynamic: true,
435
+ providerExecuted: true
436
+ });
437
+ }
438
+ }
439
+ if (message.type === "user") {
440
+ const userMsg = message;
441
+ const content = userMsg.message?.content;
442
+ for (const part of content) {
443
+ if (typeof part !== "string" && part.type === "tool_result") {
444
+ yield this.emit({
445
+ type: "tool-output-available",
446
+ toolCallId: part.tool_use_id,
447
+ output: message.tool_use_result || part.content,
448
+ dynamic: true,
449
+ providerExecuted: true
450
+ });
451
+ }
452
+ }
453
+ }
454
+ if (message.type === "result") {
455
+ const resultMsg = message;
456
+ if (resultMsg.is_error) {
457
+ this.errorEmitted = true;
458
+ const errorText = resultMsg.result || "Unknown error";
459
+ yield this.emit({
460
+ type: "error",
461
+ errorText
462
+ });
463
+ }
464
+ yield this.emit({
465
+ type: "finish",
466
+ finishReason: mapFinishReason(resultMsg.subtype, resultMsg.is_error),
467
+ messageMetadata: {
468
+ usage: convertUsageToAISDK(resultMsg.usage ?? {}),
469
+ sessionId: this.sessionId
470
+ }
471
+ });
472
+ }
473
+ }
474
+ } catch (error) {
475
+ if (process.env.DEBUG === "true") {
476
+ const errPayload = {
477
+ error: error instanceof Error ? error.message : String(error)
478
+ };
479
+ if (error instanceof Error) {
480
+ if (error.stack)
481
+ errPayload.stack = error.stack;
482
+ if (error.cause !== void 0) {
483
+ errPayload.cause = error.cause instanceof Error ? {
484
+ message: error.cause.message,
485
+ stack: error.cause.stack
486
+ } : String(error.cause);
487
+ }
488
+ }
489
+ trace(errPayload);
490
+ } else {
491
+ trace({ error: String(error) });
492
+ }
493
+ if (isAbortError(error)) {
494
+ console.error("[AISDKStream] Operation aborted");
495
+ } else {
496
+ const errorMessage = error instanceof Error ? error.message : "Unknown error";
497
+ console.error("[AISDKStream] Error:", errorMessage);
498
+ if (process.env.DEBUG === "true") {
499
+ if (error instanceof Error && error.stack) {
500
+ console.error("[AISDKStream] Stack:", error.stack);
501
+ }
502
+ if (error instanceof Error && error.cause) {
503
+ console.error("[AISDKStream] Cause:", error.cause);
504
+ }
505
+ }
506
+ if ((errorMessage.includes("exited with code") || errorMessage.includes("process exited")) && this.errorEmitted) {
507
+ console.error("[AISDKStream] (Skipping duplicate error \u2014 already sent API/result error above. Check the first error in the stream.)");
508
+ } else if (errorMessage.includes("exited with code") || errorMessage.includes("process exited")) {
509
+ console.error("[AISDKStream] Hint: Verify ANTHROPIC_API_KEY, --model (proxy must support it), and network.");
510
+ }
511
+ if (!this.errorEmitted) {
512
+ yield this.emit({ type: "error", errorText: errorMessage });
513
+ yield this.emit({
514
+ type: "finish",
515
+ finishReason: mapFinishReason("error_during_execution", true),
516
+ messageMetadata: {
517
+ usage: convertUsageToAISDK({}),
518
+ sessionId: this.sessionId
519
+ }
520
+ });
521
+ }
522
+ }
523
+ } finally {
524
+ yield `data: [DONE]
525
+
526
+ `;
527
+ }
528
+ }
529
+ };
530
+ function streamSDKMessagesToAISDKUI(messageIterator) {
531
+ return new AISDKStreamConverter().stream(messageIterator);
532
+ }
533
+
534
+ // ../../packages/runner-claude/dist/claude-runner.js
535
+ function createCanUseToolCallback(claudeOptions) {
536
+ return async (toolName, input, options) => {
537
+ const { toolUseID } = options;
538
+ if (claudeOptions.yolo && toolName !== "AskUserQuestion") {
539
+ return {
540
+ behavior: "allow",
541
+ updatedInput: input
542
+ };
543
+ }
544
+ const cwd = claudeOptions.cwd || process.cwd();
545
+ try {
546
+ const fs2 = await import("node:fs");
547
+ const path2 = await import("node:path");
548
+ const approvalDir = path2.join(cwd, ".bunny-agent", "approvals");
549
+ const approvalFile = path2.join(approvalDir, `${toolUseID}.json`);
550
+ fs2.mkdirSync(approvalDir, { recursive: true });
551
+ if (!fs2.existsSync(approvalFile)) {
552
+ fs2.writeFileSync(approvalFile, JSON.stringify({
553
+ status: "pending",
554
+ toolName,
555
+ input,
556
+ questions: toolName === "AskUserQuestion" ? input?.questions : void 0,
557
+ answers: {}
558
+ }));
559
+ }
560
+ const timeout = Date.now() + 6e4;
561
+ let lastApproval = null;
562
+ while (Date.now() < timeout) {
563
+ try {
564
+ const data = fs2.readFileSync(approvalFile, "utf-8");
565
+ const approval = JSON.parse(data);
566
+ lastApproval = approval;
567
+ if (approval.status === "completed") {
568
+ try {
569
+ fs2.unlinkSync(approvalFile);
570
+ } catch {
571
+ }
572
+ return {
573
+ behavior: "allow",
574
+ updatedInput: {
575
+ questions: approval.questions,
576
+ answers: approval.answers
577
+ }
578
+ };
579
+ }
580
+ } catch {
581
+ }
582
+ await new Promise((resolve4) => setTimeout(resolve4, 500));
583
+ }
584
+ try {
585
+ fs2.unlinkSync(approvalFile);
586
+ } catch {
587
+ }
588
+ if (lastApproval && Object.keys(lastApproval.answers).length > 0) {
589
+ return {
590
+ behavior: "allow",
591
+ updatedInput: {
592
+ questions: lastApproval.questions,
593
+ answers: lastApproval.answers
594
+ }
595
+ };
596
+ }
597
+ return {
598
+ behavior: "deny",
599
+ message: "Timeout waiting for user input"
600
+ };
601
+ } catch (error) {
602
+ console.error("Failed to handle approval flow:", error);
603
+ return {
604
+ behavior: "deny",
605
+ message: "Failed to handle approval flow"
606
+ };
607
+ }
608
+ };
609
+ }
610
+ var OPTIONAL_MODULES = {
611
+ "claude-agent-sdk": "@anthropic-ai/claude-agent-sdk"
612
+ };
613
+ function hasClaudeAuth() {
614
+ if (process.env.ANTHROPIC_API_KEY)
615
+ return true;
616
+ if (process.env.AWS_BEARER_TOKEN_BEDROCK)
617
+ return true;
618
+ if (process.env.ANTHROPIC_AUTH_TOKEN)
619
+ return true;
620
+ if (process.env.LITELLM_MASTER_KEY)
621
+ return true;
622
+ if (process.env.CLAUDE_CODE_USE_BEDROCK === "1" && process.env.ANTHROPIC_BEDROCK_BASE_URL) {
623
+ return true;
624
+ }
625
+ return false;
626
+ }
627
+ function createClaudeRunner(options) {
628
+ return {
629
+ async *run(userInput) {
630
+ if (!hasClaudeAuth()) {
631
+ console.error("[BunnyAgent] Warning: No Claude auth configured. Using mock response.\nTo use the real Claude Agent SDK, set one of:\n ANTHROPIC_API_KEY, AWS_BEARER_TOKEN_BEDROCK, ANTHROPIC_AUTH_TOKEN, or LITELLM_MASTER_KEY\n Or for Bedrock proxy: CLAUDE_CODE_USE_BEDROCK=1 and ANTHROPIC_BEDROCK_BASE_URL (and ANTHROPIC_AUTH_TOKEN or LITELLM_MASTER_KEY)\nThen install the SDK: npm install @anthropic-ai/claude-agent-sdk");
632
+ yield* runMockAgent(options, userInput, options.abortController?.signal);
633
+ return;
634
+ }
635
+ const sdk = await loadClaudeAgentSDK();
636
+ if (sdk) {
637
+ yield* runWithClaudeAgentSDK(sdk, options, userInput);
638
+ } else {
639
+ console.error("[BunnyAgent] Warning: @anthropic-ai/claude-agent-sdk not installed. Using mock response.\nInstall the SDK: npm install @anthropic-ai/claude-agent-sdk");
640
+ yield* runMockAgent(options, userInput, options.abortController?.signal);
641
+ }
642
+ }
643
+ };
644
+ }
645
+ async function loadClaudeAgentSDK() {
646
+ try {
647
+ const modulePath = OPTIONAL_MODULES["claude-agent-sdk"];
648
+ const module = await import(
649
+ /* webpackIgnore: true */
650
+ modulePath
651
+ );
652
+ return module;
653
+ } catch {
654
+ return null;
655
+ }
656
+ }
657
+ async function* runWithClaudeAgentSDK(sdk, options, userInput) {
658
+ yield* runWithAISDKUIOutput(sdk, options, userInput);
659
+ }
660
+ function createSDKOptions(options) {
661
+ const isRoot = typeof process.getuid === "function" && process.getuid() === 0;
662
+ return {
663
+ model: options.model,
664
+ systemPrompt: options.systemPrompt,
665
+ maxTurns: options.maxTurns,
666
+ allowedTools: [
667
+ ...options.allowedTools ?? [],
668
+ "Skill",
669
+ "WebSearch",
670
+ "WebFetch"
671
+ ],
672
+ cwd: options.cwd,
673
+ env: options.env,
674
+ resume: options.resume,
675
+ settingSources: ["project", "user"],
676
+ canUseTool: createCanUseToolCallback(options),
677
+ permissionMode: isRoot || options.yolo ? "bypassPermissions" : "default",
678
+ allowDangerouslySkipPermissions: !isRoot && !!options.yolo,
679
+ includePartialMessages: options.includePartialMessages
680
+ };
681
+ }
682
+ function setupAbortHandler(queryIterator, signal) {
683
+ const abortHandler = async () => {
684
+ console.error("[ClaudeRunner] Abort signal received, will call query.interrupt()...");
685
+ await queryIterator.interrupt();
686
+ console.error("[ClaudeRunner] query.interrupt() completed");
687
+ };
688
+ if (signal) {
689
+ console.error("[ClaudeRunner] Signal provided, adding abort listener");
690
+ signal.addEventListener("abort", abortHandler);
691
+ if (signal.aborted) {
692
+ console.error("[ClaudeRunner] Signal already aborted!");
693
+ }
694
+ } else {
695
+ console.error("[ClaudeRunner] No signal provided");
696
+ }
697
+ return () => {
698
+ if (signal) {
699
+ signal.removeEventListener("abort", abortHandler);
700
+ console.error("[ClaudeRunner] Abort listener removed");
701
+ }
702
+ };
703
+ }
704
+ async function* runWithAISDKUIOutput(sdk, options, userInput) {
705
+ const sdkOptions = createSDKOptions({
706
+ ...options,
707
+ includePartialMessages: true
708
+ });
709
+ const queryIterator = sdk.query({ prompt: userInput, options: sdkOptions });
710
+ const cleanup = setupAbortHandler(queryIterator, options.abortController?.signal);
711
+ try {
712
+ yield* streamSDKMessagesToAISDKUI(queryIterator);
713
+ } finally {
714
+ cleanup();
715
+ }
716
+ }
717
+ async function* runMockAgent(options, userInput, signal) {
718
+ if (signal?.aborted) {
719
+ console.log("[ClaudeRunner] Mock agent: Operation already aborted");
720
+ return;
721
+ }
722
+ try {
723
+ const messageId = generateId();
724
+ yield formatDataStream({
725
+ type: "start",
726
+ messageId
727
+ });
728
+ const response = `I received your request: "${userInput}"
729
+
730
+ Model: ${options.model}
731
+
732
+ This is a mock response because:
733
+ - No Claude auth (ANTHROPIC_API_KEY, ANTHROPIC_AUTH_TOKEN, AWS_BEARER_TOKEN_BEDROCK, or Bedrock proxy env) is set, OR
734
+ - @anthropic-ai/claude-agent-sdk is not installed
735
+
736
+ To use the real Claude Agent SDK:
737
+ 1. Set ANTHROPIC_API_KEY, or for Bedrock proxy: ANTHROPIC_AUTH_TOKEN/LITELLM_MASTER_KEY and ANTHROPIC_BEDROCK_BASE_URL, CLAUDE_CODE_USE_BEDROCK=1
738
+ 2. Install the SDK: npm install @anthropic-ai/claude-agent-sdk
739
+
740
+ Documentation: https://platform.claude.com/docs/en/agent-sdk/typescript-v2-preview`;
741
+ const textId = generateId();
742
+ yield formatDataStream({ type: "text-start", id: textId });
743
+ const words = response.split(" ");
744
+ for (const word of words) {
745
+ yield formatDataStream({
746
+ type: "text-delta",
747
+ id: textId,
748
+ delta: word + " "
749
+ });
750
+ await new Promise((resolve4) => setTimeout(resolve4, 20));
751
+ }
752
+ yield formatDataStream({ type: "text-end", id: textId });
753
+ yield formatDataStream({
754
+ type: "finish",
755
+ finishReason: mapFinishReason("success"),
756
+ usage: convertUsageToAISDK({
757
+ input_tokens: 0,
758
+ output_tokens: 0,
759
+ cache_creation_input_tokens: 0,
760
+ cache_read_input_tokens: 0
761
+ })
762
+ });
763
+ yield `data: [DONE]
764
+
765
+ `;
766
+ } catch (error) {
767
+ const errorMessage = error instanceof Error ? error.message : "Unknown error occurred";
768
+ console.error("[ClaudeRunner] Mock agent error:", errorMessage);
769
+ yield formatDataStream({ type: "error", errorText: errorMessage });
770
+ yield formatDataStream({
771
+ type: "finish",
772
+ finishReason: mapFinishReason("error_during_execution", true),
773
+ usage: convertUsageToAISDK({
774
+ input_tokens: 0,
775
+ output_tokens: 0,
776
+ cache_creation_input_tokens: 0,
777
+ cache_read_input_tokens: 0
778
+ })
779
+ });
780
+ yield `data: [DONE]
781
+
782
+ `;
783
+ }
784
+ }
785
+
786
+ // ../../packages/runner-codex/dist/codex-runner.js
787
+ import * as fs from "node:fs";
788
+ import * as os from "node:os";
789
+ import * as path from "node:path";
790
+ import { Codex } from "@openai/codex-sdk";
791
+ function normalizeCodexModel(model) {
792
+ const trimmed = model.trim();
793
+ const withoutProvider = trimmed.startsWith("openai:") ? trimmed.slice("openai:".length) : trimmed;
794
+ if (/^\d+(\.\d+)?$/.test(withoutProvider)) {
795
+ return `gpt-${withoutProvider}`;
796
+ }
797
+ return withoutProvider;
798
+ }
799
+ function stringifyUnknown(value) {
800
+ if (typeof value === "string") {
801
+ return value;
802
+ }
803
+ try {
804
+ return JSON.stringify(value);
805
+ } catch {
806
+ return String(value);
807
+ }
808
+ }
809
+ function toToolStartPayload(event) {
810
+ if (event.type !== "item.started") {
811
+ return null;
812
+ }
813
+ const item = event.item;
814
+ if (item.type === "command_execution") {
815
+ return {
816
+ toolCallId: item.id,
817
+ toolName: "shell",
818
+ args: { command: item.command }
819
+ };
820
+ }
821
+ if (item.type === "mcp_tool_call") {
822
+ return {
823
+ toolCallId: item.id,
824
+ toolName: `${item.server}:${item.tool}`,
825
+ args: item.arguments
826
+ };
827
+ }
828
+ if (item.type === "web_search") {
829
+ return {
830
+ toolCallId: item.id,
831
+ toolName: "web_search",
832
+ args: { query: item.query }
833
+ };
834
+ }
835
+ return null;
836
+ }
837
+ function toToolEndPayload(event) {
838
+ if (event.type !== "item.completed") {
839
+ return null;
840
+ }
841
+ const item = event.item;
842
+ if (item.type === "command_execution") {
843
+ return {
844
+ toolCallId: item.id,
845
+ result: {
846
+ status: item.status,
847
+ exitCode: item.exit_code,
848
+ output: item.aggregated_output
849
+ },
850
+ isError: item.exit_code !== 0
851
+ };
852
+ }
853
+ if (item.type === "mcp_tool_call") {
854
+ return {
855
+ toolCallId: item.id,
856
+ result: item.result ?? item.error ?? { status: item.status },
857
+ isError: item.error != null || item.status === "failed"
858
+ };
859
+ }
860
+ if (item.type === "web_search") {
861
+ return {
862
+ toolCallId: item.id,
863
+ result: { query: item.query }
864
+ };
865
+ }
866
+ return null;
867
+ }
868
+ function toAssistantText(event) {
869
+ if (event.type === "item.completed" && event.item.type === "agent_message") {
870
+ return event.item.text;
871
+ }
872
+ if (event.type === "item.completed" && event.item.type === "reasoning") {
873
+ return `[Reasoning] ${event.item.text}`;
874
+ }
875
+ if (event.type === "item.completed" && event.item.type === "error") {
876
+ return `[Error] ${event.item.message}`;
877
+ }
878
+ return null;
879
+ }
880
+ function createCodexRunner(options) {
881
+ const codex = new Codex({
882
+ apiKey: process.env.CODEX_API_KEY || process.env.OPENAI_API_KEY,
883
+ baseUrl: process.env.OPENAI_BASE_URL,
884
+ env: options.env
885
+ });
886
+ return {
887
+ async *run(userInput) {
888
+ const threadOptions = {
889
+ model: normalizeCodexModel(options.model),
890
+ sandboxMode: options.sandboxMode,
891
+ workingDirectory: options.cwd || process.cwd(),
892
+ skipGitRepoCheck: options.skipGitRepoCheck ?? true,
893
+ modelReasoningEffort: options.modelReasoningEffort,
894
+ networkAccessEnabled: options.networkAccessEnabled,
895
+ webSearchMode: options.webSearchMode,
896
+ approvalPolicy: options.approvalPolicy
897
+ };
898
+ const thread = options.resume ? codex.resumeThread(options.resume, threadOptions) : codex.startThread(threadOptions);
899
+ let inputToCodex = userInput;
900
+ const tempFiles = [];
901
+ try {
902
+ if (userInput.startsWith("[") && userInput.endsWith("]")) {
903
+ const parsed = JSON.parse(userInput);
904
+ if (Array.isArray(parsed)) {
905
+ const parts = [];
906
+ for (const p of parsed) {
907
+ if (p.type === "image" && typeof p.data === "string") {
908
+ const match = /^data:([^;]+);base64,(.+)$/.exec(p.data);
909
+ if (match) {
910
+ const ext = match[1].split("/")[1] ?? "png";
911
+ const tmpPath = path.join(os.tmpdir(), `bunny-agent-img-${Date.now()}-${Math.random().toString(36).slice(2)}.${ext}`);
912
+ fs.writeFileSync(tmpPath, Buffer.from(match[2], "base64"));
913
+ tempFiles.push(tmpPath);
914
+ parts.push({ type: "local_image", path: tmpPath });
915
+ }
916
+ } else {
917
+ const text = typeof p.text === "string" ? p.text : JSON.stringify(p);
918
+ parts.push({ type: "text", text });
919
+ }
920
+ }
921
+ if (parts.length > 0) {
922
+ inputToCodex = parts;
923
+ }
924
+ }
925
+ }
926
+ } catch (_e) {
927
+ }
928
+ const streamedTurn = await thread.runStreamed(inputToCodex, {
929
+ signal: options.abortController?.signal
930
+ });
931
+ for await (const event of streamedTurn.events) {
932
+ const assistantText = toAssistantText(event);
933
+ if (assistantText) {
934
+ yield `data: ${JSON.stringify({ type: "text-delta", delta: assistantText })}
935
+
936
+ `;
937
+ }
938
+ const toolStart = toToolStartPayload(event);
939
+ if (toolStart) {
940
+ yield `data: ${JSON.stringify({ type: "tool-input-start", toolCallId: toolStart.toolCallId, toolName: toolStart.toolName })}
941
+
942
+ `;
943
+ yield `data: ${JSON.stringify({ type: "tool-input-available", toolCallId: toolStart.toolCallId, toolName: toolStart.toolName, input: toolStart.args })}
944
+
945
+ `;
946
+ }
947
+ const toolEnd = toToolEndPayload(event);
948
+ if (toolEnd) {
949
+ yield `data: ${JSON.stringify({ type: "tool-output-available", toolCallId: toolEnd.toolCallId, output: toolEnd.result, isError: toolEnd.isError })}
950
+
951
+ `;
952
+ }
953
+ if (event.type === "turn.completed") {
954
+ yield `data: ${JSON.stringify({ type: "finish", finishReason: "stop", usage: event.usage })}
955
+
956
+ `;
957
+ yield `data: [DONE]
958
+
959
+ `;
960
+ }
961
+ if (event.type === "turn.failed") {
962
+ yield `data: ${JSON.stringify({ type: "error", errorText: event.error.message })}
963
+
964
+ `;
965
+ yield `data: ${JSON.stringify({ type: "finish", finishReason: "error" })}
966
+
967
+ `;
968
+ yield `data: [DONE]
969
+
970
+ `;
971
+ }
972
+ if (event.type === "error") {
973
+ yield `data: ${JSON.stringify({ type: "error", errorText: stringifyUnknown(event.message) })}
974
+
975
+ `;
976
+ yield `data: ${JSON.stringify({ type: "finish", finishReason: "error" })}
977
+
978
+ `;
979
+ yield `data: [DONE]
980
+
981
+ `;
982
+ }
983
+ }
984
+ for (const tmpFile of tempFiles) {
985
+ try {
986
+ fs.unlinkSync(tmpFile);
987
+ } catch {
988
+ }
989
+ }
990
+ }
991
+ };
992
+ }
993
+
994
+ // ../../packages/runner-gemini/dist/gemini-runner.js
995
+ import { spawn } from "node:child_process";
996
+ function createGeminiRunner(options = {}) {
997
+ const cwd = options.cwd || process.cwd();
998
+ let currentProcess = null;
999
+ return {
1000
+ async *run(userInput) {
1001
+ if (options.abortController?.signal.aborted) {
1002
+ yield `data: ${JSON.stringify({ type: "error", errorText: "Run aborted before start." })}
1003
+
1004
+ `;
1005
+ yield `data: ${JSON.stringify({ type: "finish", finishReason: "error" })}
1006
+
1007
+ `;
1008
+ yield "data: [DONE]\n\n";
1009
+ return;
1010
+ }
1011
+ const args = ["--experimental-acp"];
1012
+ if (options.model)
1013
+ args.push("--model", options.model);
1014
+ let aborted = false;
1015
+ let completed = false;
1016
+ currentProcess = spawn("gemini", args, {
1017
+ cwd,
1018
+ env: { ...process.env, ...options.env },
1019
+ stdio: ["pipe", "pipe", "pipe"]
1020
+ });
1021
+ if (!currentProcess.stdin || !currentProcess.stdout)
1022
+ throw new Error("Failed to spawn gemini");
1023
+ const abortSignal = options.abortController?.signal;
1024
+ const abortHandler = () => {
1025
+ aborted = true;
1026
+ currentProcess?.kill();
1027
+ };
1028
+ if (abortSignal) {
1029
+ abortSignal.addEventListener("abort", abortHandler);
1030
+ }
1031
+ let msgId = 1;
1032
+ const send = (method, params, id) => {
1033
+ const msg = JSON.stringify({
1034
+ jsonrpc: "2.0",
1035
+ method,
1036
+ params,
1037
+ ...id ? { id } : {}
1038
+ });
1039
+ currentProcess.stdin.write(msg + "\n");
1040
+ };
1041
+ send("initialize", { protocolVersion: 1, clientCapabilities: {} }, msgId++);
1042
+ let sessionId = null;
1043
+ const messageId = `msg_${Date.now()}_${Math.random().toString(36).slice(2)}`;
1044
+ const textId = `text_${Date.now()}_${Math.random().toString(36).slice(2)}`;
1045
+ let hasStarted = false;
1046
+ let hasTextStarted = false;
1047
+ try {
1048
+ let buffer = "";
1049
+ for await (const chunk of currentProcess.stdout) {
1050
+ buffer += chunk.toString();
1051
+ const lines = buffer.split("\n");
1052
+ buffer = lines.pop() || "";
1053
+ for (const line of lines) {
1054
+ if (!line.trim())
1055
+ continue;
1056
+ let msg;
1057
+ try {
1058
+ msg = JSON.parse(line);
1059
+ } catch {
1060
+ continue;
1061
+ }
1062
+ if (msg.id === 1 && msg.result) {
1063
+ send("session/new", { cwd, mcpServers: [] }, msgId++);
1064
+ }
1065
+ if (msg.id === 2 && msg.result) {
1066
+ const result = msg.result;
1067
+ sessionId = result.sessionId;
1068
+ send("session/prompt", {
1069
+ sessionId,
1070
+ prompt: [{ type: "text", text: userInput }]
1071
+ }, msgId++);
1072
+ }
1073
+ if (msg.id === 3 && "result" in msg) {
1074
+ if (hasTextStarted)
1075
+ yield `data: ${JSON.stringify({ type: "text-end", id: textId })}
1076
+
1077
+ `;
1078
+ yield `data: ${JSON.stringify({ type: "finish", finishReason: "stop" })}
1079
+
1080
+ `;
1081
+ yield `data: [DONE]
1082
+
1083
+ `;
1084
+ completed = true;
1085
+ currentProcess.kill();
1086
+ return;
1087
+ }
1088
+ if (msg.method === "session/update" && msg.params?.update) {
1089
+ const update = msg.params.update;
1090
+ if (!hasStarted) {
1091
+ yield `data: ${JSON.stringify({ type: "start", messageId })}
1092
+
1093
+ `;
1094
+ hasStarted = true;
1095
+ }
1096
+ if (update.sessionUpdate === "agent_message_chunk" && update.content?.type === "text" && update.content.text) {
1097
+ if (!hasTextStarted) {
1098
+ yield `data: ${JSON.stringify({ type: "text-start", id: textId })}
1099
+
1100
+ `;
1101
+ hasTextStarted = true;
1102
+ }
1103
+ yield `data: ${JSON.stringify({ type: "text-delta", id: textId, delta: update.content.text })}
1104
+
1105
+ `;
1106
+ }
1107
+ }
1108
+ }
1109
+ }
1110
+ if (!completed) {
1111
+ const errorText = aborted ? "Gemini run aborted by signal." : "Gemini ACP process exited before completion.";
1112
+ yield `data: ${JSON.stringify({ type: "error", errorText })}
1113
+
1114
+ `;
1115
+ yield `data: ${JSON.stringify({ type: "finish", finishReason: "error" })}
1116
+
1117
+ `;
1118
+ yield "data: [DONE]\n\n";
1119
+ }
1120
+ } finally {
1121
+ if (abortSignal) {
1122
+ abortSignal.removeEventListener("abort", abortHandler);
1123
+ }
1124
+ currentProcess = null;
1125
+ }
1126
+ },
1127
+ abort() {
1128
+ currentProcess?.kill();
1129
+ currentProcess = null;
1130
+ }
1131
+ };
1132
+ }
1133
+
1134
+ // ../../packages/runner-opencode/dist/opencode-runner.js
1135
+ import { spawn as spawn2 } from "node:child_process";
1136
+ function createOpenCodeRunner(options = {}) {
1137
+ const cwd = options.cwd || process.cwd();
1138
+ let currentProcess = null;
1139
+ return {
1140
+ async *run(userInput) {
1141
+ if (options.abortController?.signal.aborted) {
1142
+ yield `data: ${JSON.stringify({ type: "error", errorText: "Run aborted before start." })}
1143
+
1144
+ `;
1145
+ yield `data: ${JSON.stringify({ type: "finish", finishReason: "error" })}
1146
+
1147
+ `;
1148
+ yield "data: [DONE]\n\n";
1149
+ return;
1150
+ }
1151
+ const args = ["acp"];
1152
+ if (options.model)
1153
+ args.push("--model", options.model);
1154
+ let aborted = false;
1155
+ let completed = false;
1156
+ currentProcess = spawn2("opencode", args, {
1157
+ cwd,
1158
+ env: { ...process.env, ...options.env },
1159
+ stdio: ["pipe", "pipe", "pipe"]
1160
+ });
1161
+ if (!currentProcess.stdin || !currentProcess.stdout)
1162
+ throw new Error("Failed to spawn opencode");
1163
+ const abortSignal = options.abortController?.signal;
1164
+ const abortHandler = () => {
1165
+ aborted = true;
1166
+ currentProcess?.kill();
1167
+ };
1168
+ if (abortSignal) {
1169
+ abortSignal.addEventListener("abort", abortHandler);
1170
+ }
1171
+ let msgId = 1;
1172
+ const send = (method, params, id) => {
1173
+ const msg = JSON.stringify({
1174
+ jsonrpc: "2.0",
1175
+ method,
1176
+ params,
1177
+ ...id ? { id } : {}
1178
+ });
1179
+ currentProcess.stdin.write(msg + "\n");
1180
+ };
1181
+ send("initialize", { protocolVersion: 1, clientCapabilities: {} }, msgId++);
1182
+ let sessionId = null;
1183
+ const messageId = `msg_${Date.now()}_${Math.random().toString(36).slice(2)}`;
1184
+ const textId = `text_${Date.now()}_${Math.random().toString(36).slice(2)}`;
1185
+ let hasStarted = false;
1186
+ let hasTextStarted = false;
1187
+ try {
1188
+ let buffer = "";
1189
+ for await (const chunk of currentProcess.stdout) {
1190
+ buffer += chunk.toString();
1191
+ const lines = buffer.split("\n");
1192
+ buffer = lines.pop() || "";
1193
+ for (const line of lines) {
1194
+ if (!line.trim())
1195
+ continue;
1196
+ let msg;
1197
+ try {
1198
+ msg = JSON.parse(line);
1199
+ } catch {
1200
+ continue;
1201
+ }
1202
+ if (msg.id === 1 && msg.result) {
1203
+ send("session/new", { cwd, mcpServers: [] }, msgId++);
1204
+ }
1205
+ if (msg.id === 2 && msg.result) {
1206
+ const result = msg.result;
1207
+ sessionId = result.sessionId;
1208
+ send("session/prompt", {
1209
+ sessionId,
1210
+ prompt: [{ type: "text", text: userInput }]
1211
+ }, msgId++);
1212
+ }
1213
+ if (msg.id === 3 && "result" in msg) {
1214
+ if (hasTextStarted)
1215
+ yield `data: ${JSON.stringify({ type: "text-end", id: textId })}
1216
+
1217
+ `;
1218
+ yield `data: ${JSON.stringify({ type: "finish", finishReason: "stop" })}
1219
+
1220
+ `;
1221
+ yield `data: [DONE]
1222
+
1223
+ `;
1224
+ completed = true;
1225
+ currentProcess.kill();
1226
+ return;
1227
+ }
1228
+ if (msg.method === "session/update" && msg.params?.update) {
1229
+ const update = msg.params.update;
1230
+ if (!hasStarted) {
1231
+ yield `data: ${JSON.stringify({ type: "start", messageId })}
1232
+
1233
+ `;
1234
+ hasStarted = true;
1235
+ }
1236
+ if (update.sessionUpdate === "agent_message_chunk" && update.content?.type === "text" && update.content.text) {
1237
+ if (!hasTextStarted) {
1238
+ yield `data: ${JSON.stringify({ type: "text-start", id: textId })}
1239
+
1240
+ `;
1241
+ hasTextStarted = true;
1242
+ }
1243
+ yield `data: ${JSON.stringify({ type: "text-delta", id: textId, delta: update.content.text })}
1244
+
1245
+ `;
1246
+ }
1247
+ }
1248
+ }
1249
+ }
1250
+ if (!completed) {
1251
+ const errorText = aborted ? "OpenCode run aborted by signal." : "OpenCode ACP process exited before completion.";
1252
+ yield `data: ${JSON.stringify({ type: "error", errorText })}
1253
+
1254
+ `;
1255
+ yield `data: ${JSON.stringify({ type: "finish", finishReason: "error" })}
1256
+
1257
+ `;
1258
+ yield "data: [DONE]\n\n";
1259
+ }
1260
+ } finally {
1261
+ if (abortSignal) {
1262
+ abortSignal.removeEventListener("abort", abortHandler);
1263
+ }
1264
+ currentProcess = null;
1265
+ }
1266
+ },
1267
+ abort() {
1268
+ currentProcess?.kill();
1269
+ currentProcess = null;
1270
+ }
1271
+ };
1272
+ }
1273
+
1274
+ // ../../packages/runner-pi/dist/pi-runner.js
1275
+ import { appendFileSync as appendFileSync2, existsSync as existsSync5, unlinkSync as unlinkSync3 } from "node:fs";
1276
+ import { join as join7 } from "node:path";
1277
+ import { getModel } from "@mariozechner/pi-ai";
1278
+ import { AuthStorage, createAgentSession, ModelRegistry, SessionManager } from "@mariozechner/pi-coding-agent";
1279
+
1280
+ // ../../packages/runner-pi/dist/bunny-agent-resource-loader.js
1281
+ import { existsSync as existsSync4 } from "node:fs";
1282
+ import { homedir } from "node:os";
1283
+ import { isAbsolute, join as join5, resolve as resolve2 } from "node:path";
1284
+ import { DefaultResourceLoader, loadSkills } from "@mariozechner/pi-coding-agent";
1285
+ var LOG_PREFIX = "[bunny-agent:pi]";
1286
+ function logSkillLoad(cwd, agentDir, skillPaths, result) {
1287
+ const lines = [
1288
+ `${LOG_PREFIX} loadSkills`,
1289
+ ` cwd: ${cwd}`,
1290
+ ` agentDir: ${agentDir}`,
1291
+ ` extra skillPaths (${skillPaths.length}):`
1292
+ ];
1293
+ for (const raw of skillPaths) {
1294
+ const abs = isAbsolute(raw) ? raw : resolve2(cwd, raw);
1295
+ lines.push(` ${raw} -> ${abs} (exists: ${existsSync4(abs) ? "yes" : "no"})`);
1296
+ }
1297
+ lines.push(` loaded skills: ${result.skills.length}`);
1298
+ if (result.skills.length > 0) {
1299
+ lines.push(` skill names: ${result.skills.map((s) => s.name).join(", ")}`);
1300
+ }
1301
+ if (result.diagnostics.length > 0) {
1302
+ lines.push(` diagnostics (${result.diagnostics.length}):`);
1303
+ for (const d of result.diagnostics) {
1304
+ const pathPart = d.path !== void 0 ? ` path=${d.path}` : "";
1305
+ lines.push(` [${d.type}] ${d.message}${pathPart}`);
1306
+ }
1307
+ }
1308
+ console.error(lines.join("\n"));
1309
+ }
1310
+ var BunnyAgentResourceLoader = class {
1311
+ constructor(options = {}) {
1312
+ this.cwd = options.cwd ?? process.cwd();
1313
+ this.agentDir = options.agentDir ?? join5(homedir(), ".pi", "agent");
1314
+ this.skillPaths = options.skillPaths ?? [];
1315
+ this.extraAppendPrompt = options.appendSystemPrompt;
1316
+ this.delegate = new DefaultResourceLoader({
1317
+ cwd: this.cwd,
1318
+ agentDir: this.agentDir,
1319
+ settingsManager: options.settingsManager
1320
+ });
1321
+ }
1322
+ async reload() {
1323
+ await this.delegate.reload();
1324
+ this.cachedSkills = void 0;
1325
+ }
1326
+ getSkills() {
1327
+ if (!this.cachedSkills) {
1328
+ this.cachedSkills = loadSkills({
1329
+ cwd: this.cwd,
1330
+ agentDir: this.agentDir,
1331
+ skillPaths: this.skillPaths
1332
+ });
1333
+ if (this.skillPaths.length > 0) {
1334
+ logSkillLoad(this.cwd, this.agentDir, this.skillPaths, this.cachedSkills);
1335
+ }
1336
+ }
1337
+ return this.cachedSkills;
1338
+ }
1339
+ // Delegate all other methods
1340
+ getExtensions() {
1341
+ return this.delegate.getExtensions();
1342
+ }
1343
+ getPrompts() {
1344
+ return this.delegate.getPrompts();
1345
+ }
1346
+ getThemes() {
1347
+ return this.delegate.getThemes();
1348
+ }
1349
+ getAgentsFiles() {
1350
+ return this.delegate.getAgentsFiles();
1351
+ }
1352
+ getSystemPrompt() {
1353
+ return this.delegate.getSystemPrompt();
1354
+ }
1355
+ getAppendSystemPrompt() {
1356
+ const base = this.delegate.getAppendSystemPrompt();
1357
+ if (this.extraAppendPrompt) {
1358
+ console.error(`${LOG_PREFIX} getAppendSystemPrompt: appending extra prompt (${this.extraAppendPrompt.length} chars)`);
1359
+ return [...base, this.extraAppendPrompt];
1360
+ }
1361
+ return base;
1362
+ }
1363
+ extendResources(paths) {
1364
+ this.delegate.extendResources(paths);
1365
+ }
1366
+ };
1367
+
1368
+ // ../../packages/runner-pi/dist/image-tools.js
1369
+ import { mkdirSync as mkdirSync2, writeFileSync as writeFileSync3 } from "node:fs";
1370
+ import { dirname as dirname3, extname, join as join6 } from "node:path";
1371
+ var generateImageSchema = {
1372
+ type: "object",
1373
+ properties: {
1374
+ prompt: {
1375
+ type: "string",
1376
+ description: "Text description of the image to generate."
1377
+ },
1378
+ filename: {
1379
+ type: "string",
1380
+ description: "Output filename with extension, e.g. 'cat.png'. Defaults to a timestamp-based name."
1381
+ },
1382
+ size: {
1383
+ type: "string",
1384
+ enum: [
1385
+ "256x256",
1386
+ "512x512",
1387
+ "1024x1024",
1388
+ "1792x1024",
1389
+ "1024x1792",
1390
+ "1280x1280",
1391
+ "1568x1056",
1392
+ "1056x1568",
1393
+ "1472x1088",
1394
+ "1088x1472",
1395
+ "1728x960",
1396
+ "960x1728"
1397
+ ],
1398
+ description: "Image dimensions. Common: 1024x1024 (square), 1280x1280, 1568x1056 (landscape), 1056x1568 (portrait), 1728x960 (wide), 960x1728 (tall)."
1399
+ },
1400
+ quality: {
1401
+ type: "string",
1402
+ enum: ["standard", "hd"],
1403
+ description: "Image quality (OpenAI only). Defaults to standard."
1404
+ }
1405
+ },
1406
+ required: ["prompt"],
1407
+ additionalProperties: false
1408
+ };
1409
+ async function resolveB64(item) {
1410
+ if (item.b64_json)
1411
+ return item.b64_json;
1412
+ if (item.url) {
1413
+ const res = await fetch(item.url);
1414
+ if (res.ok)
1415
+ return Buffer.from(await res.arrayBuffer()).toString("base64");
1416
+ }
1417
+ return void 0;
1418
+ }
1419
+ async function saveImageItem(item, filePath) {
1420
+ const b64 = await resolveB64(item);
1421
+ if (!b64)
1422
+ return void 0;
1423
+ mkdirSync2(dirname3(filePath), { recursive: true });
1424
+ writeFileSync3(filePath, Buffer.from(b64, "base64"));
1425
+ return filePath;
1426
+ }
1427
+ function buildImageGenerateTool(cwd, imageModelId, baseUrl, apiKey) {
1428
+ return {
1429
+ name: "generate_image",
1430
+ label: "generate image",
1431
+ description: "Generate an image from a text prompt. Saves the image to disk and returns the file path.",
1432
+ promptSnippet: "generate_image(prompt, filename?, size?, quality?) - generate an image from text",
1433
+ promptGuidelines: [
1434
+ "Use generate_image when the user asks to create, draw, or visualize something.",
1435
+ "Be descriptive in the prompt \u2014 more detail produces better results.",
1436
+ "Provide a filename with extension, e.g. 'cat.png'."
1437
+ ],
1438
+ // biome-ignore lint/suspicious/noExplicitAny: plain JSON Schema compatible with TypeBox TSchema
1439
+ parameters: generateImageSchema,
1440
+ async execute(_toolCallId, params, _signal, _onUpdate) {
1441
+ const p = params;
1442
+ const prompt = p.prompt;
1443
+ const size = p.size ?? "1024x1024";
1444
+ const quality = p.quality ?? "standard";
1445
+ const rawFilename = p.filename;
1446
+ const filename = rawFilename ? extname(rawFilename) ? rawFilename : `${rawFilename}.png` : `image_${Date.now()}.png`;
1447
+ const filePath = join6(cwd, filename.replace(/[^a-zA-Z0-9_\-./]/g, "_"));
1448
+ try {
1449
+ const url = `${baseUrl.replace(/\/$/, "")}/v1/images/generations`;
1450
+ const res = await fetch(url, {
1451
+ method: "POST",
1452
+ headers: {
1453
+ "Content-Type": "application/json",
1454
+ Authorization: `Bearer ${apiKey}`
1455
+ },
1456
+ body: JSON.stringify({
1457
+ model: imageModelId,
1458
+ prompt,
1459
+ n: 1,
1460
+ size,
1461
+ quality
1462
+ })
1463
+ });
1464
+ if (!res.ok) {
1465
+ throw new Error(`Image generation failed (${res.status}): ${await res.text()}`);
1466
+ }
1467
+ const json = await res.json();
1468
+ const item = json.data?.[0] ?? {};
1469
+ const savedPath = await saveImageItem(item, filePath);
1470
+ return {
1471
+ content: [
1472
+ {
1473
+ type: "text",
1474
+ text: savedPath ?? "Image generated but could not be saved."
1475
+ }
1476
+ ],
1477
+ details: {
1478
+ filePath: savedPath,
1479
+ response: json
1480
+ }
1481
+ };
1482
+ } catch (e) {
1483
+ const msg = e instanceof Error ? e.message : String(e);
1484
+ return {
1485
+ content: [
1486
+ { type: "text", text: `Image generation error: ${msg}` }
1487
+ ],
1488
+ details: void 0
1489
+ };
1490
+ }
1491
+ }
1492
+ };
1493
+ }
1494
+
1495
+ // ../../packages/runner-pi/dist/tool-overrides.js
1496
+ import { createBashTool, createReadTool } from "@mariozechner/pi-coding-agent";
1497
+
1498
+ // ../../packages/runner-pi/dist/web-tools.js
1499
+ var braveProvider = {
1500
+ id: "brave",
1501
+ label: "Brave Search",
1502
+ envKeys: ["BRAVE_API_KEY"],
1503
+ async search({ apiKey, query, count, country, freshness }) {
1504
+ const params = new URLSearchParams({
1505
+ q: query,
1506
+ count: String(Math.min(count, 20))
1507
+ });
1508
+ if (country)
1509
+ params.set("country", country);
1510
+ if (freshness)
1511
+ params.set("freshness", freshness);
1512
+ const res = await fetch(`https://api.search.brave.com/res/v1/web/search?${params}`, {
1513
+ headers: {
1514
+ Accept: "application/json",
1515
+ "Accept-Encoding": "gzip",
1516
+ "X-Subscription-Token": apiKey
1517
+ }
1518
+ });
1519
+ if (!res.ok) {
1520
+ const body = await res.text().catch(() => "");
1521
+ throw new Error(`Brave API ${res.status}: ${res.statusText}
1522
+ ${body}`);
1523
+ }
1524
+ const data = await res.json();
1525
+ const results = [];
1526
+ if (data.web?.results) {
1527
+ for (const r of data.web.results) {
1528
+ if (results.length >= count)
1529
+ break;
1530
+ results.push({
1531
+ title: r.title ?? "",
1532
+ link: r.url ?? "",
1533
+ snippet: r.description ?? "",
1534
+ age: r.age ?? r.page_age ?? ""
1535
+ });
1536
+ }
1537
+ }
1538
+ return results;
1539
+ }
1540
+ };
1541
+ var tavilyProvider = {
1542
+ id: "tavily",
1543
+ label: "Tavily",
1544
+ envKeys: ["TAVILY_API_KEY"],
1545
+ async search({ apiKey, query, count }) {
1546
+ const res = await fetch("https://api.tavily.com/search", {
1547
+ method: "POST",
1548
+ headers: { "Content-Type": "application/json" },
1549
+ body: JSON.stringify({
1550
+ api_key: apiKey,
1551
+ query,
1552
+ max_results: Math.min(count, 10),
1553
+ include_answer: false
1554
+ })
1555
+ });
1556
+ if (!res.ok) {
1557
+ const body = await res.text().catch(() => "");
1558
+ throw new Error(`Tavily API ${res.status}: ${res.statusText}
1559
+ ${body}`);
1560
+ }
1561
+ const data = await res.json();
1562
+ const results = [];
1563
+ if (Array.isArray(data.results)) {
1564
+ for (const r of data.results) {
1565
+ results.push({
1566
+ title: r.title ?? "",
1567
+ link: r.url ?? "",
1568
+ snippet: r.content ?? ""
1569
+ });
1570
+ }
1571
+ }
1572
+ return results;
1573
+ }
1574
+ };
1575
+ var AUTO_DETECT_ORDER = [braveProvider, tavilyProvider];
1576
+ function getEnv(env, key) {
1577
+ const v = env[key] ?? process.env[key];
1578
+ return v && v.length > 0 ? v : void 0;
1579
+ }
1580
+ function resolveSearchProviders(env) {
1581
+ const available = [];
1582
+ for (const p of AUTO_DETECT_ORDER) {
1583
+ for (const key of p.envKeys) {
1584
+ const val = getEnv(env, key);
1585
+ if (val) {
1586
+ available.push({ provider: p, apiKey: val });
1587
+ break;
1588
+ }
1589
+ }
1590
+ }
1591
+ return available;
1592
+ }
1593
+ function resolveSearchProvider(env) {
1594
+ const all = resolveSearchProviders(env);
1595
+ return all.length > 0 ? all[0] : null;
1596
+ }
1597
+ function isRateLimitError(err) {
1598
+ if (!(err instanceof Error))
1599
+ return false;
1600
+ const msg = err.message;
1601
+ return msg.includes("429") || msg.includes("rate") || msg.includes("quota") || msg.includes("limit");
1602
+ }
1603
+ var BROWSER_UA = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36";
1604
+ function htmlToText(html) {
1605
+ return html.replace(/<(script|style|noscript)[^>]*>[\s\S]*?<\/\1>/gi, "").replace(/<br\s*\/?>/gi, "\n").replace(/<\/(p|div|h[1-6]|li|tr)>/gi, "\n").replace(/<(p|div|h[1-6]|li|tr)[^>]*>/gi, "\n").replace(/<[^>]+>/g, "").replace(/&amp;/g, "&").replace(/&lt;/g, "<").replace(/&gt;/g, ">").replace(/&quot;/g, '"').replace(/&#39;/g, "'").replace(/&nbsp;/g, " ").replace(/[ \t]+/g, " ").replace(/\n{3,}/g, "\n\n").trim();
1606
+ }
1607
+ async function fetchPageContent(url) {
1608
+ const controller = new AbortController();
1609
+ const timeout = setTimeout(() => controller.abort(), 15e3);
1610
+ try {
1611
+ const res = await fetch(url, {
1612
+ headers: {
1613
+ "User-Agent": BROWSER_UA,
1614
+ Accept: "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
1615
+ "Accept-Language": "en-US,en;q=0.9"
1616
+ },
1617
+ signal: controller.signal
1618
+ });
1619
+ if (!res.ok)
1620
+ return `(HTTP ${res.status}: ${res.statusText})`;
1621
+ const html = await res.text();
1622
+ const text = htmlToText(html);
1623
+ return text.length > 5e4 ? `${text.slice(0, 5e4)}
1624
+
1625
+ [Truncated]` : text;
1626
+ } catch (e) {
1627
+ const msg = e instanceof Error ? e.message : String(e);
1628
+ return `(Error fetching ${url}: ${msg})`;
1629
+ } finally {
1630
+ clearTimeout(timeout);
1631
+ }
1632
+ }
1633
+ function formatSearchResults(results, providerLabel) {
1634
+ if (results.length === 0)
1635
+ return "No results found.";
1636
+ const header = `[${providerLabel}] ${results.length} result(s)
1637
+ `;
1638
+ return header + results.map((r, i) => {
1639
+ const lines = [
1640
+ `--- Result ${i + 1} ---`,
1641
+ `Title: ${r.title}`,
1642
+ `Link: ${r.link}`
1643
+ ];
1644
+ if (r.age)
1645
+ lines.push(`Age: ${r.age}`);
1646
+ lines.push(`Snippet: ${r.snippet}`);
1647
+ if (r.content)
1648
+ lines.push(`Content:
1649
+ ${r.content}`);
1650
+ return lines.join("\n");
1651
+ }).join("\n\n");
1652
+ }
1653
+ var webSearchSchema = {
1654
+ type: "object",
1655
+ required: ["query"],
1656
+ properties: {
1657
+ query: {
1658
+ type: "string",
1659
+ description: "Search query string"
1660
+ },
1661
+ count: {
1662
+ type: "number",
1663
+ description: "Number of results to return (default: 5, max: 20)"
1664
+ },
1665
+ freshness: {
1666
+ type: "string",
1667
+ description: 'Filter by time: "pd" (past day), "pw" (past week), "pm" (past month), "py" (past year), or "YYYY-MM-DDtoYYYY-MM-DD"'
1668
+ },
1669
+ country: {
1670
+ type: "string",
1671
+ description: "Two-letter country code for results (default: US)"
1672
+ },
1673
+ fetch_content: {
1674
+ type: "boolean",
1675
+ description: "If true, also fetch and include page content for each result (slower)"
1676
+ }
1677
+ }
1678
+ };
1679
+ var webFetchSchema = {
1680
+ type: "object",
1681
+ required: ["url"],
1682
+ properties: {
1683
+ url: {
1684
+ type: "string",
1685
+ description: "URL to fetch and extract readable content from"
1686
+ }
1687
+ }
1688
+ };
1689
+ function buildWebSearchTool(env) {
1690
+ const providers = resolveSearchProviders(env);
1691
+ if (providers.length === 0) {
1692
+ throw new Error("web_search: no search provider available. Set BRAVE_API_KEY or TAVILY_API_KEY.");
1693
+ }
1694
+ return {
1695
+ name: "web_search",
1696
+ label: "web search",
1697
+ description: "Search the web for information. Returns titles, URLs, and snippets. Use for documentation lookups, fact-checking, current events, or any query requiring web results.",
1698
+ promptSnippet: "web_search(query, count?, freshness?, country?, fetch_content?) - search the web",
1699
+ promptGuidelines: [
1700
+ "Use web_search when you need current information, documentation, or facts not available locally.",
1701
+ "Set fetch_content=true only when you need the actual page text, not just snippets \u2014 it is slower.",
1702
+ "Prefer specific, focused queries over broad ones for better results."
1703
+ ],
1704
+ // biome-ignore lint/suspicious/noExplicitAny: plain JSON Schema compatible with TypeBox TSchema
1705
+ parameters: webSearchSchema,
1706
+ async execute(_toolCallId, params, _signal, _onUpdate) {
1707
+ const p = params;
1708
+ const query = p.query;
1709
+ const count = p.count ?? 5;
1710
+ const country = p.country ?? "US";
1711
+ const freshness = p.freshness;
1712
+ const shouldFetchContent = p.fetch_content ?? false;
1713
+ let lastError;
1714
+ for (const { provider, apiKey } of providers) {
1715
+ try {
1716
+ const results = await provider.search({
1717
+ apiKey,
1718
+ query,
1719
+ count,
1720
+ country,
1721
+ freshness
1722
+ });
1723
+ if (shouldFetchContent) {
1724
+ for (const r of results) {
1725
+ r.content = await fetchPageContent(r.link);
1726
+ }
1727
+ }
1728
+ return {
1729
+ content: [
1730
+ {
1731
+ type: "text",
1732
+ text: formatSearchResults(results, provider.label)
1733
+ }
1734
+ ],
1735
+ details: void 0
1736
+ };
1737
+ } catch (e) {
1738
+ lastError = e;
1739
+ if (isRateLimitError(e) && providers.length > 1) {
1740
+ console.error(`[bunny-agent:pi] ${provider.label} rate-limited, trying next provider...`);
1741
+ continue;
1742
+ }
1743
+ break;
1744
+ }
1745
+ }
1746
+ const msg = lastError instanceof Error ? lastError.message : String(lastError);
1747
+ return {
1748
+ content: [
1749
+ {
1750
+ type: "text",
1751
+ text: `Web search error: ${msg}`
1752
+ }
1753
+ ],
1754
+ details: void 0
1755
+ };
1756
+ }
1757
+ };
1758
+ }
1759
+ function buildWebFetchTool() {
1760
+ return {
1761
+ name: "web_fetch",
1762
+ label: "web fetch",
1763
+ description: "Fetch a web page and extract its readable text content. Use when you need the full content of a specific URL (article, docs page, etc.).",
1764
+ promptSnippet: "web_fetch(url) - fetch and extract content from a URL",
1765
+ promptGuidelines: [
1766
+ "Use web_fetch when you already have a URL and need its content.",
1767
+ "For finding URLs first, use web_search instead."
1768
+ ],
1769
+ // biome-ignore lint/suspicious/noExplicitAny: plain JSON Schema compatible with TypeBox TSchema
1770
+ parameters: webFetchSchema,
1771
+ async execute(_toolCallId, params, _signal, _onUpdate) {
1772
+ const p = params;
1773
+ const url = p.url;
1774
+ try {
1775
+ const content = await fetchPageContent(url);
1776
+ return {
1777
+ content: [{ type: "text", text: content }],
1778
+ details: void 0
1779
+ };
1780
+ } catch (e) {
1781
+ const msg = e instanceof Error ? e.message : String(e);
1782
+ return {
1783
+ content: [
1784
+ { type: "text", text: `Error fetching URL: ${msg}` }
1785
+ ],
1786
+ details: void 0
1787
+ };
1788
+ }
1789
+ }
1790
+ };
1791
+ }
1792
+
1793
+ // ../../packages/runner-pi/dist/tool-overrides.js
1794
+ function redactSecrets(text, secrets) {
1795
+ if (Object.keys(secrets).length === 0)
1796
+ return text;
1797
+ let result = text;
1798
+ const escapeRegex = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
1799
+ const values = Object.values(secrets).filter((v) => v.length >= 8).filter((v) => !/^\//.test(v) && !/^[A-Za-z]:[/\\]/.test(v)).sort((a, b) => b.length - a.length);
1800
+ for (const v of values) {
1801
+ const ev = escapeRegex(v);
1802
+ result = result.replace(new RegExp(`^\\S+=.*${ev}.*$\\n?`, "gm"), "");
1803
+ result = result.replace(new RegExp(`\\s*["']?\\w+["']?\\s*:\\s*['"][^'"]*${ev}[^'"]*['"],?`, "g"), "");
1804
+ result = result.split(v).join("***");
1805
+ }
1806
+ result = result.replace(/\n{3,}/g, "\n\n");
1807
+ return result.trim();
1808
+ }
1809
+ function redactResultContent(result, secrets) {
1810
+ if (result?.content && Array.isArray(result.content)) {
1811
+ result.content = result.content.map((c) => c.type === "text" && typeof c.text === "string" ? { ...c, text: redactSecrets(c.text, secrets) } : c);
1812
+ }
1813
+ }
1814
+ function isEnvDumpCommand(command) {
1815
+ const cmd = command.replace(/\s+/g, " ").trim();
1816
+ return /(?:^|[|;&])\s*(?:env|printenv|export\s+-p|declare\s+-x)\b/.test(cmd);
1817
+ }
1818
+ function buildEnvInjectedBashTool(cwd, extraEnv) {
1819
+ const bashAgentTool = createBashTool(cwd, {
1820
+ spawnHook: (ctx) => ({
1821
+ ...ctx,
1822
+ env: { ...ctx.env, ...extraEnv }
1823
+ })
1824
+ });
1825
+ return {
1826
+ name: bashAgentTool.name,
1827
+ label: bashAgentTool.label ?? "bash",
1828
+ description: bashAgentTool.description,
1829
+ // biome-ignore lint/suspicious/noExplicitAny: TypeBox schema from pi internals
1830
+ parameters: bashAgentTool.parameters,
1831
+ async execute(toolCallId, params, signal, onUpdate) {
1832
+ const command = params.command ?? "";
1833
+ if (isEnvDumpCommand(command)) {
1834
+ return {
1835
+ content: [
1836
+ {
1837
+ type: "text",
1838
+ text: "Command blocked: printing or redirecting environment variables is not allowed."
1839
+ }
1840
+ ],
1841
+ details: void 0
1842
+ };
1843
+ }
1844
+ const result = await bashAgentTool.execute(toolCallId, params, signal, onUpdate);
1845
+ redactResultContent(result, extraEnv);
1846
+ return result;
1847
+ }
1848
+ };
1849
+ }
1850
+ function buildSecretRedactingReadTool(cwd, secrets) {
1851
+ const readAgentTool = createReadTool(cwd);
1852
+ return {
1853
+ name: readAgentTool.name,
1854
+ label: readAgentTool.label ?? "read",
1855
+ description: readAgentTool.description,
1856
+ // biome-ignore lint/suspicious/noExplicitAny: TypeBox schema from pi internals
1857
+ parameters: readAgentTool.parameters,
1858
+ async execute(toolCallId, params, signal, onUpdate) {
1859
+ const result = await readAgentTool.execute(toolCallId, params, signal, onUpdate);
1860
+ redactResultContent(result, secrets);
1861
+ return result;
1862
+ }
1863
+ };
1864
+ }
1865
+ function buildSecretAwareTools(cwd, secrets) {
1866
+ const tools = [
1867
+ buildEnvInjectedBashTool(cwd, secrets),
1868
+ buildSecretRedactingReadTool(cwd, secrets),
1869
+ buildWebFetchTool()
1870
+ ];
1871
+ if (resolveSearchProvider(secrets)) {
1872
+ tools.push(buildWebSearchTool(secrets));
1873
+ }
1874
+ return tools;
1875
+ }
1876
+
1877
+ // ../../packages/runner-pi/dist/pi-runner.js
1878
+ var LOG_PREFIX2 = "[bunny-agent:pi]";
1879
+ function parseModelSpec(model) {
1880
+ const trimmed = model.trim();
1881
+ const separator = trimmed.indexOf(":");
1882
+ if (separator <= 0 || separator === trimmed.length - 1) {
1883
+ throw new Error(`Invalid pi model "${model}". Expected format "<provider>:<model>", for example "google:gemini-2.5-pro".`);
1884
+ }
1885
+ return {
1886
+ provider: trimmed.slice(0, separator),
1887
+ modelName: trimmed.slice(separator + 1)
1888
+ };
1889
+ }
1890
+ function resolveImageModelName(chatProvider, env) {
1891
+ const spec = env?.IMAGE_GENERATION_MODEL;
1892
+ if (!spec)
1893
+ return void 0;
1894
+ try {
1895
+ const { provider, modelName } = parseModelSpec(spec);
1896
+ return provider === chatProvider ? modelName : void 0;
1897
+ } catch {
1898
+ return void 0;
1899
+ }
1900
+ }
1901
+ function getEnvValue(optionsEnv, name) {
1902
+ return optionsEnv?.[name] ?? process.env[name];
1903
+ }
1904
+ function applyModelOverrides(model, provider, optionsEnv) {
1905
+ if (model == null)
1906
+ return;
1907
+ const openAiBaseUrl = getEnvValue(optionsEnv, "OPENAI_BASE_URL");
1908
+ const geminiBaseUrl = getEnvValue(optionsEnv, "GEMINI_BASE_URL");
1909
+ const anthropicBaseUrl = getEnvValue(optionsEnv, "ANTHROPIC_BASE_URL");
1910
+ if (provider === "openai" && openAiBaseUrl) {
1911
+ model.baseUrl = openAiBaseUrl;
1912
+ } else if (provider === "google" && geminiBaseUrl) {
1913
+ model.baseUrl = geminiBaseUrl;
1914
+ } else if (provider === "anthropic" && anthropicBaseUrl) {
1915
+ model.baseUrl = anthropicBaseUrl;
1916
+ }
1917
+ }
1918
+ function emitStreamError(errorText) {
1919
+ return [
1920
+ `data: ${JSON.stringify({ type: "error", errorText })}
1921
+
1922
+ `,
1923
+ `data: ${JSON.stringify({ type: "finish", finishReason: "error" })}
1924
+
1925
+ `,
1926
+ "data: [DONE]\n\n"
1927
+ ];
1928
+ }
1929
+ function extractToolResultText(result) {
1930
+ if (result !== null && typeof result === "object") {
1931
+ const r = result;
1932
+ if (Array.isArray(r.content) && r.content.length > 0) {
1933
+ const text = r.content.filter((c) => c.type === "text" && typeof c.text === "string").map((c) => c.text).join("\n");
1934
+ if (text.length > 0) {
1935
+ return text;
1936
+ }
1937
+ }
1938
+ }
1939
+ if (typeof result === "string")
1940
+ return result;
1941
+ try {
1942
+ return JSON.stringify(result);
1943
+ } catch {
1944
+ return String(result);
1945
+ }
1946
+ }
1947
+ function usageToMessageMetadata(usage) {
1948
+ return {
1949
+ input_tokens: usage.input,
1950
+ output_tokens: usage.output,
1951
+ cache_read_input_tokens: usage.cacheRead,
1952
+ cache_creation_input_tokens: usage.cacheWrite
1953
+ };
1954
+ }
1955
+ function getUsageFromAgentEndMessages(messages) {
1956
+ for (let i = messages.length - 1; i >= 0; i--) {
1957
+ const m = messages[i];
1958
+ if (m.role === "assistant" && m.usage != null) {
1959
+ return m.usage;
1960
+ }
1961
+ }
1962
+ return void 0;
1963
+ }
1964
+ function getErrorFromAgentEndMessages(messages) {
1965
+ for (let i = messages.length - 1; i >= 0; i--) {
1966
+ const m = messages[i];
1967
+ if (m.role === "assistant" && m.errorMessage) {
1968
+ return m.errorMessage;
1969
+ }
1970
+ }
1971
+ return void 0;
1972
+ }
1973
+ function traceRawMessage(debugCwd, data, reset = false, optionsEnv) {
1974
+ const debugVal = getEnvValue(optionsEnv, "DEBUG");
1975
+ const enabled = debugVal === "true" || debugVal === "1";
1976
+ if (!enabled)
1977
+ return;
1978
+ try {
1979
+ const file = join7(debugCwd, "pi-message-stream-debug.json");
1980
+ if (reset && existsSync5(file))
1981
+ unlinkSync3(file);
1982
+ const type = data !== null && typeof data === "object" ? data.type : void 0;
1983
+ let payload = data;
1984
+ try {
1985
+ payload = data !== void 0 ? JSON.parse(JSON.stringify(data)) : void 0;
1986
+ } catch {
1987
+ payload = "[non-serializable]";
1988
+ }
1989
+ const entry = { _t: (/* @__PURE__ */ new Date()).toISOString(), type, payload };
1990
+ appendFileSync2(file, JSON.stringify(entry, null, 2) + ",\n");
1991
+ } catch {
1992
+ }
1993
+ }
1994
+ function createPiRunner(options = {}) {
1995
+ const modelSpec = options.model;
1996
+ if (modelSpec == null || modelSpec.trim() === "") {
1997
+ throw new Error("Pi runner: model is required. Pass a model in the form <provider>:<model>, e.g. openai:gpt-4o or google:gemini-2.5-flash.");
1998
+ }
1999
+ const { provider, modelName } = parseModelSpec(modelSpec.trim());
2000
+ const cwd = options.cwd || process.cwd();
2001
+ const apiKeyEnvKey = `${provider.toUpperCase().replace(/-/g, "_")}_API_KEY`;
2002
+ const inlineApiKey = typeof options.env?.[apiKeyEnvKey] === "string" && options.env[apiKeyEnvKey].length > 0 ? options.env[apiKeyEnvKey] : void 0;
2003
+ const modelRegistry = ModelRegistry.inMemory(AuthStorage.create());
2004
+ const defaultModel = getModel(provider, modelName);
2005
+ let model = defaultModel ?? modelRegistry.find(provider, modelName);
2006
+ if (model == null) {
2007
+ const baseUrlEnvKey = `${provider.toUpperCase().replace(/-/g, "_")}_BASE_URL`;
2008
+ const baseUrl = getEnvValue(options.env, baseUrlEnvKey) ?? getEnvValue(options.env, "OPENAI_BASE_URL");
2009
+ if (!baseUrl) {
2010
+ throw new Error(`Pi runner: model "${modelSpec}" not found in built-in catalog. Set ${baseUrlEnvKey} (or OPENAI_BASE_URL) to auto-register it.`);
2011
+ }
2012
+ modelRegistry.registerProvider(provider, {
2013
+ baseUrl,
2014
+ apiKey: inlineApiKey ?? apiKeyEnvKey,
2015
+ api: "openai-completions",
2016
+ models: [
2017
+ {
2018
+ id: modelName,
2019
+ name: modelName,
2020
+ reasoning: false,
2021
+ input: ["text", "image"],
2022
+ cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
2023
+ contextWindow: 128e3,
2024
+ maxTokens: 8192
2025
+ }
2026
+ ]
2027
+ });
2028
+ const registered = modelRegistry.find(provider, modelName);
2029
+ if (!registered) {
2030
+ throw new Error(`Pi runner: failed to resolve model "${modelSpec}" after registration.`);
2031
+ }
2032
+ model = registered;
2033
+ }
2034
+ applyModelOverrides(model, provider, options.env);
2035
+ const imageModelName = resolveImageModelName(provider, options.env);
2036
+ return {
2037
+ async *run(userInput) {
2038
+ if (inlineApiKey !== void 0) {
2039
+ modelRegistry.authStorage.setRuntimeApiKey(provider, inlineApiKey);
2040
+ }
2041
+ try {
2042
+ const resume = options.sessionId?.trim();
2043
+ const sessionManager = await (async () => {
2044
+ if (resume !== void 0 && resume !== "") {
2045
+ if (resume.includes("/")) {
2046
+ return SessionManager.open(resume);
2047
+ }
2048
+ const sessions = await SessionManager.list(cwd);
2049
+ const found = sessions.find((s) => s.id === resume);
2050
+ return found ? SessionManager.open(found.path) : SessionManager.create(cwd);
2051
+ }
2052
+ return SessionManager.create(cwd);
2053
+ })();
2054
+ const resourceLoader = options.skillPaths ? new BunnyAgentResourceLoader({
2055
+ cwd,
2056
+ skillPaths: options.skillPaths,
2057
+ appendSystemPrompt: options.systemPrompt
2058
+ }) : void 0;
2059
+ if (options.skillPaths && options.skillPaths.length > 0) {
2060
+ console.error(`${LOG_PREFIX2} runner: cwd=${cwd} skillPaths=${JSON.stringify(options.skillPaths)}`);
2061
+ }
2062
+ if (resourceLoader) {
2063
+ await resourceLoader.reload();
2064
+ }
2065
+ const customTools = options.env && Object.keys(options.env).length > 0 ? buildSecretAwareTools(cwd, options.env) : [];
2066
+ if (imageModelName) {
2067
+ const apiKey = await modelRegistry.authStorage.getApiKey(provider) ?? "";
2068
+ customTools.push(buildImageGenerateTool(cwd, imageModelName, model.baseUrl, apiKey));
2069
+ }
2070
+ const { session } = await createAgentSession({
2071
+ cwd,
2072
+ model,
2073
+ sessionManager,
2074
+ modelRegistry,
2075
+ resourceLoader,
2076
+ customTools
2077
+ });
2078
+ const eventQueue = [];
2079
+ let isComplete = false;
2080
+ let aborted = false;
2081
+ let wakeConsumer = null;
2082
+ const notify = () => {
2083
+ wakeConsumer?.();
2084
+ wakeConsumer = null;
2085
+ };
2086
+ const unsubscribe = session.subscribe((e) => {
2087
+ eventQueue.push(e);
2088
+ if (e.type === "agent_end") {
2089
+ isComplete = true;
2090
+ }
2091
+ notify();
2092
+ });
2093
+ const abortSignal = options.abortController?.signal;
2094
+ const abortHandler = () => {
2095
+ aborted = true;
2096
+ isComplete = true;
2097
+ void session.abort();
2098
+ notify();
2099
+ };
2100
+ if (abortSignal) {
2101
+ abortSignal.addEventListener("abort", abortHandler);
2102
+ if (abortSignal.aborted) {
2103
+ abortHandler();
2104
+ }
2105
+ }
2106
+ try {
2107
+ traceRawMessage(cwd, null, true, options.env);
2108
+ let promptText = userInput;
2109
+ let images;
2110
+ try {
2111
+ if (userInput.startsWith("[") && userInput.endsWith("]")) {
2112
+ const parsed = JSON.parse(userInput);
2113
+ if (Array.isArray(parsed)) {
2114
+ promptText = parsed.filter((p) => p.type === "text").map((p) => p.text).join("\n");
2115
+ const imageParts = parsed.filter((p) => p.type === "image");
2116
+ if (imageParts.length > 0) {
2117
+ images = imageParts.map((p) => ({
2118
+ type: "image",
2119
+ data: p.data,
2120
+ mimeType: p.mimeType
2121
+ }));
2122
+ }
2123
+ }
2124
+ }
2125
+ } catch (_e) {
2126
+ }
2127
+ const promptPromise = session.prompt(promptText, images ? { images } : void 0);
2128
+ const messageId = `msg_${Date.now()}_${Math.random().toString(36).slice(2)}`;
2129
+ let hasStarted = false;
2130
+ let hasFinished = false;
2131
+ const imageToolUsage = { input_tokens: 0, output_tokens: 0 };
2132
+ const newTextPartId = () => `text_${Date.now()}_${Math.random().toString(36).slice(2)}_${Math.random().toString(36).slice(2)}`;
2133
+ let activeTextPartId = null;
2134
+ let textStreamOpen = false;
2135
+ const endTextStreamIfOpen = function* () {
2136
+ if (textStreamOpen && activeTextPartId != null) {
2137
+ yield `data: ${JSON.stringify({ type: "text-end", id: activeTextPartId })}
2138
+
2139
+ `;
2140
+ textStreamOpen = false;
2141
+ activeTextPartId = null;
2142
+ }
2143
+ };
2144
+ const beginTextStream = function* () {
2145
+ activeTextPartId = newTextPartId();
2146
+ yield `data: ${JSON.stringify({ type: "text-start", id: activeTextPartId })}
2147
+
2148
+ `;
2149
+ textStreamOpen = true;
2150
+ };
2151
+ const ensureStartEvent = async function* () {
2152
+ if (!hasStarted) {
2153
+ yield `data: ${JSON.stringify({ type: "start", messageId })}
2154
+
2155
+ `;
2156
+ yield `data: ${JSON.stringify({
2157
+ type: "message-metadata",
2158
+ messageMetadata: { sessionId: session.sessionId }
2159
+ })}
2160
+
2161
+ `;
2162
+ hasStarted = true;
2163
+ }
2164
+ };
2165
+ const finishSuccess = async function* (usage) {
2166
+ yield* endTextStreamIfOpen();
2167
+ const finishPayload = { type: "finish", finishReason: "stop" };
2168
+ const hasImageUsage = imageToolUsage.input_tokens > 0 || imageToolUsage.output_tokens > 0;
2169
+ if (usage != null || hasImageUsage) {
2170
+ const base = usage != null ? usageToMessageMetadata(usage) : {};
2171
+ finishPayload.messageMetadata = {
2172
+ usage: {
2173
+ ...base,
2174
+ input_tokens: (base.input_tokens ?? 0) + imageToolUsage.input_tokens,
2175
+ output_tokens: (base.output_tokens ?? 0) + imageToolUsage.output_tokens
2176
+ }
2177
+ };
2178
+ }
2179
+ yield `data: ${JSON.stringify(finishPayload)}
2180
+
2181
+ `;
2182
+ yield "data: [DONE]\n\n";
2183
+ hasFinished = true;
2184
+ };
2185
+ const finishError = async function* (errorText) {
2186
+ for (const chunk of emitStreamError(errorText)) {
2187
+ yield chunk;
2188
+ }
2189
+ hasFinished = true;
2190
+ };
2191
+ while (!isComplete || eventQueue.length > 0) {
2192
+ while (eventQueue.length > 0) {
2193
+ const event = eventQueue.shift();
2194
+ traceRawMessage(cwd, event, false, options.env);
2195
+ yield* ensureStartEvent();
2196
+ if (event.type === "message_start") {
2197
+ const msg = event.message;
2198
+ if (msg?.role === "assistant") {
2199
+ yield* endTextStreamIfOpen();
2200
+ }
2201
+ } else if (event.type === "message_update") {
2202
+ const sub = event.assistantMessageEvent;
2203
+ if (sub.type === "text_start") {
2204
+ yield* endTextStreamIfOpen();
2205
+ yield* beginTextStream();
2206
+ } else if (sub.type === "text_delta") {
2207
+ let delta = sub.delta;
2208
+ if (delta) {
2209
+ if (options.env && Object.keys(options.env).length > 0) {
2210
+ delta = redactSecrets(delta, options.env);
2211
+ }
2212
+ if (!textStreamOpen) {
2213
+ yield* beginTextStream();
2214
+ }
2215
+ yield `data: ${JSON.stringify({
2216
+ type: "text-delta",
2217
+ id: activeTextPartId,
2218
+ delta
2219
+ })}
2220
+
2221
+ `;
2222
+ }
2223
+ } else if (sub.type === "toolcall_start") {
2224
+ yield* endTextStreamIfOpen();
2225
+ }
2226
+ } else if (event.type === "tool_execution_start") {
2227
+ yield* endTextStreamIfOpen();
2228
+ yield `data: ${JSON.stringify({ type: "tool-input-start", toolCallId: event.toolCallId, toolName: event.toolName, dynamic: true, providerExecuted: true })}
2229
+
2230
+ `;
2231
+ yield `data: ${JSON.stringify({ type: "tool-input-available", toolCallId: event.toolCallId, toolName: event.toolName, input: event.args, dynamic: true, providerExecuted: true })}
2232
+
2233
+ `;
2234
+ } else if (event.type === "tool_execution_end") {
2235
+ let output = extractToolResultText(event.result);
2236
+ if (options.env && Object.keys(options.env).length > 0) {
2237
+ output = redactSecrets(output, options.env);
2238
+ }
2239
+ if (event.toolName === "generate_image" && event.result !== null && typeof event.result === "object") {
2240
+ const details = event.result.details;
2241
+ const u = details?.response?.usage;
2242
+ if (u) {
2243
+ imageToolUsage.input_tokens += u.input_tokens ?? 0;
2244
+ imageToolUsage.output_tokens += u.output_tokens ?? 0;
2245
+ }
2246
+ }
2247
+ yield `data: ${JSON.stringify({ type: "tool-output-available", toolCallId: event.toolCallId, output, isError: event.isError, dynamic: true, providerExecuted: true })}
2248
+
2249
+ `;
2250
+ } else if (event.type === "agent_end") {
2251
+ if (aborted) {
2252
+ yield* finishError("Run aborted by signal.");
2253
+ } else {
2254
+ const errorMsg = getErrorFromAgentEndMessages(event.messages);
2255
+ if (errorMsg) {
2256
+ yield* finishError(errorMsg);
2257
+ } else {
2258
+ const usage = getUsageFromAgentEndMessages(event.messages);
2259
+ yield* finishSuccess(usage);
2260
+ }
2261
+ }
2262
+ }
2263
+ }
2264
+ if (aborted && !hasFinished) {
2265
+ yield* ensureStartEvent();
2266
+ yield* finishError("Run aborted by signal.");
2267
+ break;
2268
+ }
2269
+ if (!isComplete && eventQueue.length === 0) {
2270
+ await new Promise((resolve4) => {
2271
+ wakeConsumer = resolve4;
2272
+ });
2273
+ }
2274
+ }
2275
+ if (hasFinished) {
2276
+ return;
2277
+ }
2278
+ try {
2279
+ await promptPromise;
2280
+ } catch (error) {
2281
+ if (!hasFinished) {
2282
+ yield* ensureStartEvent();
2283
+ const message = error instanceof Error ? error.message : "Pi agent run failed.";
2284
+ yield* finishError(message);
2285
+ }
2286
+ return;
2287
+ }
2288
+ if (!hasFinished && session.agent.state.error) {
2289
+ yield* ensureStartEvent();
2290
+ yield* finishError(session.agent.state.error);
2291
+ }
2292
+ } finally {
2293
+ if (abortSignal) {
2294
+ abortSignal.removeEventListener("abort", abortHandler);
2295
+ }
2296
+ unsubscribe();
2297
+ session.dispose();
2298
+ }
2299
+ } finally {
2300
+ if (inlineApiKey !== void 0) {
2301
+ modelRegistry.authStorage.removeRuntimeApiKey(provider);
2302
+ }
2303
+ }
2304
+ }
2305
+ };
2306
+ }
2307
+
2308
+ // ../../packages/runner-harness/dist/session.js
2309
+ import { existsSync as existsSync6, mkdirSync as mkdirSync3, readFileSync as readFileSync3, writeFileSync as writeFileSync4 } from "node:fs";
2310
+ import { join as join8 } from "node:path";
2311
+ var DIR = ".bunny-agent";
2312
+ var FILE = "session-id";
2313
+ function sessionPath(cwd) {
2314
+ return join8(cwd, DIR, FILE);
2315
+ }
2316
+ function readSessionId(cwd) {
2317
+ try {
2318
+ const p = sessionPath(cwd);
2319
+ if (!existsSync6(p))
2320
+ return void 0;
2321
+ return readFileSync3(p, "utf8").trim() || void 0;
2322
+ } catch {
2323
+ return void 0;
2324
+ }
2325
+ }
2326
+ function writeSessionId(cwd, id) {
2327
+ try {
2328
+ mkdirSync3(join8(cwd, DIR), { recursive: true });
2329
+ writeFileSync4(sessionPath(cwd), id, "utf8");
2330
+ } catch {
2331
+ }
2332
+ }
2333
+
2334
+ // ../../packages/runner-harness/dist/skills.js
2335
+ import { existsSync as existsSync7, readdirSync as readdirSync2, statSync as statSync2 } from "node:fs";
2336
+ import { homedir as homedir2 } from "node:os";
2337
+ import { join as join9 } from "node:path";
2338
+ function discoverSkillPaths(cwd) {
2339
+ const paths = [];
2340
+ for (const base of [
2341
+ join9(cwd, "skills"),
2342
+ join9(homedir2(), ".bunny-agent", "skills")
2343
+ ]) {
2344
+ if (!existsSync7(base))
2345
+ continue;
2346
+ try {
2347
+ for (const entry of readdirSync2(base)) {
2348
+ const full = join9(base, entry);
2349
+ if (statSync2(full).isDirectory() && existsSync7(join9(full, "SKILL.md"))) {
2350
+ paths.push(full);
2351
+ }
2352
+ }
2353
+ } catch {
2354
+ }
2355
+ }
2356
+ return paths;
2357
+ }
2358
+
2359
+ // ../../packages/runner-harness/dist/runner.js
2360
+ function createRunner(options) {
2361
+ const cwd = options.cwd ?? process.cwd();
2362
+ const env = options.env ?? process.env;
2363
+ const abortController = options.abortController ?? new AbortController();
2364
+ const autoInject = options.autoInject ?? true;
2365
+ const systemPrompt = options.systemPrompt ?? (autoInject ? loadSystemPrompt(cwd) : void 0);
2366
+ const resume = options.resume ?? (autoInject ? readSessionId(cwd) : void 0);
2367
+ const base = {
2368
+ model: options.model,
2369
+ systemPrompt,
2370
+ maxTurns: options.maxTurns,
2371
+ allowedTools: options.allowedTools,
2372
+ resume,
2373
+ yolo: options.yolo,
2374
+ env,
2375
+ abortController
2376
+ };
2377
+ const rawStream = dispatchRunner(options.runner, base, cwd, options);
2378
+ return autoInject ? captureSessionId(rawStream, cwd) : rawStream;
2379
+ }
2380
+ function dispatchRunner(runner, base, cwd, options) {
2381
+ const _env = base.env;
2382
+ switch (runner) {
2383
+ case "claude":
2384
+ return createClaudeRunner(base).run(options.userInput);
2385
+ case "codex": {
2386
+ const { outputFormat: _of, ...codexBase } = base;
2387
+ return createCodexRunner({ ...codexBase, cwd }).run(options.userInput);
2388
+ }
2389
+ case "gemini":
2390
+ return createGeminiRunner({
2391
+ model: options.model,
2392
+ cwd,
2393
+ env: base.env,
2394
+ abortController: base.abortController
2395
+ }).run(options.userInput);
2396
+ case "pi":
2397
+ return createPiRunner({
2398
+ ...base,
2399
+ cwd,
2400
+ sessionId: base.resume,
2401
+ skillPaths: options.skillPaths ?? discoverSkillPaths(cwd)
2402
+ }).run(options.userInput);
2403
+ case "opencode":
2404
+ return createOpenCodeRunner({
2405
+ model: options.model,
2406
+ cwd,
2407
+ env: base.env,
2408
+ abortController: base.abortController
2409
+ }).run(options.userInput);
2410
+ case "copilot":
2411
+ throw new Error("Copilot runner not yet implemented");
2412
+ default:
2413
+ throw new Error(`Unknown runner: ${runner}`);
2414
+ }
2415
+ }
2416
+ async function* captureSessionId(stream, cwd) {
2417
+ for await (const chunk of stream) {
2418
+ if (chunk.includes('"sessionId"') || chunk.includes('"session_id"')) {
2419
+ try {
2420
+ const payload = chunk.replace(/^data:\s*/, "").trim();
2421
+ if (payload && payload !== "[DONE]") {
2422
+ const json = JSON.parse(payload);
2423
+ const sessionId = json?.messageMetadata?.sessionId ?? json?.messageMetadata?.session_id ?? json?.sessionId;
2424
+ if (sessionId && typeof sessionId === "string") {
2425
+ writeSessionId(cwd, sessionId);
2426
+ }
2427
+ }
2428
+ } catch {
2429
+ }
2430
+ }
2431
+ yield chunk;
2432
+ }
2433
+ }
2434
+
2435
+ // ../../packages/runner-harness/dist/tools/bash-execute.js
2436
+ import { execFile } from "node:child_process";
2437
+ import { promisify } from "node:util";
2438
+ var execFileAsync = promisify(execFile);
2439
+
2440
+ // src/runner.ts
2441
+ async function runAgent(options) {
2442
+ const abortController = new AbortController();
2443
+ const signalHandler = () => abortController.abort();
2444
+ process.on("SIGTERM", signalHandler);
2445
+ process.on("SIGINT", signalHandler);
2446
+ try {
2447
+ for await (const chunk of createRunner({ ...options, abortController })) {
2448
+ process.stdout.write(chunk);
2449
+ }
2450
+ } finally {
2451
+ process.off("SIGTERM", signalHandler);
2452
+ process.off("SIGINT", signalHandler);
2453
+ }
2454
+ }
2455
+
2456
+ // src/cli.ts
2457
+ config({ path: resolve3(process.cwd(), ".env") });
2458
+ config({ path: resolve3(process.cwd(), "../.env") });
2459
+ config({ path: resolve3(process.cwd(), "../../.env") });
2460
+ function getSubcommand() {
2461
+ for (let i = 2; i < process.argv.length; i++) {
2462
+ const a = process.argv[i];
2463
+ if (a === "--") break;
2464
+ if (!a.startsWith("-")) return a;
2465
+ }
2466
+ return void 0;
2467
+ }
2468
+ function getSubSubcommand() {
2469
+ let found = 0;
2470
+ for (let i = 2; i < process.argv.length; i++) {
2471
+ const a = process.argv[i];
2472
+ if (a === "--") break;
2473
+ if (!a.startsWith("-")) {
2474
+ found++;
2475
+ if (found === 2) return a;
2476
+ }
2477
+ }
2478
+ return void 0;
2479
+ }
2480
+ function argsAfterPositionals(n) {
2481
+ let found = 0;
2482
+ for (let i = 2; i < process.argv.length; i++) {
2483
+ if (!process.argv[i].startsWith("-") && process.argv[i] !== "--") {
2484
+ found++;
2485
+ if (found === n) return process.argv.slice(i + 1);
2486
+ }
2487
+ }
2488
+ return [];
2489
+ }
2490
+ function parseRunArgs() {
2491
+ const { values, positionals } = parseArgs({
2492
+ args: argsAfterPositionals(1),
2493
+ options: {
2494
+ runner: { type: "string", short: "r", default: "claude" },
2495
+ model: {
2496
+ type: "string",
2497
+ short: "m",
2498
+ default: "claude-sonnet-4-20250514"
2499
+ },
2500
+ cwd: {
2501
+ type: "string",
2502
+ short: "c",
2503
+ default: process.env.BUNNY_AGENT_WORKSPACE ?? process.cwd()
2504
+ },
2505
+ "system-prompt": { type: "string", short: "s" },
2506
+ "max-turns": { type: "string", short: "t" },
2507
+ "allowed-tools": { type: "string", short: "a" },
2508
+ "skill-path": { type: "string", multiple: true },
2509
+ resume: { type: "string" },
2510
+ yolo: { type: "boolean" },
2511
+ help: { type: "boolean", short: "h" }
2512
+ },
2513
+ allowPositionals: true,
2514
+ strict: true
2515
+ });
2516
+ if (values.help) {
2517
+ printRunHelp();
2518
+ process.exit(0);
2519
+ }
2520
+ const dashIndex = process.argv.indexOf("--");
2521
+ let userInput = "";
2522
+ if (dashIndex !== -1 && dashIndex < process.argv.length - 1) {
2523
+ userInput = process.argv.slice(dashIndex + 1).join(" ");
2524
+ } else if (positionals.length > 0) {
2525
+ userInput = positionals.join(" ");
2526
+ }
2527
+ if (!userInput) {
2528
+ console.error("Error: User input is required");
2529
+ console.error('Usage: bunny-agent run [options] -- "<user input>"');
2530
+ process.exit(1);
2531
+ }
2532
+ const runner = values.runner;
2533
+ if (!["claude", "codex", "gemini", "opencode", "copilot", "pi"].includes(runner)) {
2534
+ console.error(
2535
+ 'Error: --runner must be one of: "claude", "codex", "gemini", "opencode", "copilot", "pi"'
2536
+ );
2537
+ process.exit(1);
2538
+ }
2539
+ return {
2540
+ runner,
2541
+ model: values.model,
2542
+ cwd: values.cwd,
2543
+ systemPrompt: values["system-prompt"],
2544
+ maxTurns: values["max-turns"] ? Number.parseInt(values["max-turns"], 10) : void 0,
2545
+ allowedTools: values["allowed-tools"]?.split(",").map((t) => t.trim()),
2546
+ skillPaths: values["skill-path"],
2547
+ resume: values.resume,
2548
+ yolo: values["yolo"],
2549
+ userInput
2550
+ };
2551
+ }
2552
+ function parseImageBuildArgs() {
2553
+ const { values } = parseArgs({
2554
+ args: argsAfterPositionals(2),
2555
+ options: {
2556
+ name: { type: "string", default: "bunny-agent" },
2557
+ tag: { type: "string", default: "latest" },
2558
+ image: { type: "string" },
2559
+ platform: { type: "string", default: "linux/amd64" },
2560
+ template: { type: "string" },
2561
+ push: { type: "boolean", default: false },
2562
+ help: { type: "boolean", short: "h" }
2563
+ },
2564
+ allowPositionals: false,
2565
+ strict: true
2566
+ });
2567
+ if (values.help) {
2568
+ printImageBuildHelp();
2569
+ process.exit(0);
2570
+ }
2571
+ return {
2572
+ name: values.name,
2573
+ tag: values.tag,
2574
+ image: values.image,
2575
+ platform: values.platform,
2576
+ template: values.template,
2577
+ push: values.push ?? false
2578
+ };
2579
+ }
2580
+ function printRunHelp() {
2581
+ console.log(`
2582
+ \u{1F916} BunnyAgent Runner CLI \u2014 run
2583
+
2584
+ Runs an agent locally in your terminal, streaming AI SDK UI messages to stdout.
2585
+
2586
+ Usage:
2587
+ bunny-agent run [options] -- "<user input>"
2588
+
2589
+ Options:
2590
+ -r, --runner <runner> Runner: claude, codex, gemini, opencode, copilot, pi (default: claude)
2591
+ -m, --model <model> Model (default: claude-sonnet-4-20250514)
2592
+ -c, --cwd <path> Working directory (default: cwd)
2593
+ -s, --system-prompt <prompt> Custom system prompt
2594
+ -t, --max-turns <n> Max conversation turns
2595
+ -a, --allowed-tools <tools> Comma-separated allowed tools
2596
+ --skill-path <path> Additional skill path (can be repeated, for pi runner)
2597
+ --resume <session-id> Resume a previous session
2598
+ -h, --help Show this help
2599
+
2600
+ Environment:
2601
+ ANTHROPIC_API_KEY Anthropic API key (for claude runner)
2602
+ OPENAI_API_KEY OpenAI API key (for codex runner)
2603
+ CODEX_API_KEY OpenAI API key alias (for codex runner)
2604
+ GEMINI_API_KEY Gemini API key (for gemini runner)
2605
+ BUNNY_AGENT_WORKSPACE Default workspace path
2606
+ `);
2607
+ }
2608
+ function printImageBuildHelp() {
2609
+ console.log(`
2610
+ \u{1F433} BunnyAgent Runner CLI \u2014 image build
2611
+
2612
+ Build (and optionally push) a BunnyAgent Docker image.
2613
+ The image includes Claude Agent SDK + runner-cli pre-installed.
2614
+
2615
+ Usage:
2616
+ bunny-agent image build [options]
2617
+
2618
+ Options:
2619
+ --name <name> Image name, e.g. vikadata/bunny-agent-seo (default: bunny-agent)
2620
+ --tag <tag> Image tag (default: latest)
2621
+ --image <full> Full image name override (e.g. myorg/myimage:v1)
2622
+ --platform <plat> Build platform (default: linux/amd64)
2623
+ --template <path> Path to agent template directory to bake into the image
2624
+ --push Push image to registry after build
2625
+ -h, --help Show this help
2626
+
2627
+ Examples:
2628
+ bunny-agent image build --name vikadata/bunny-agent-seo --tag 0.1.0
2629
+ bunny-agent image build --name vikadata/bunny-agent-seo --tag 0.1.0 --template ./templates/seo-agent
2630
+ bunny-agent image build --name vikadata/bunny-agent-seo --tag 0.1.0 --template ./templates/seo-agent --push
2631
+ `);
2632
+ }
2633
+ function printImageHelp() {
2634
+ console.log(`
2635
+ \u{1F433} BunnyAgent Runner CLI \u2014 image
2636
+
2637
+ Manage BunnyAgent Docker images.
2638
+
2639
+ Usage:
2640
+ bunny-agent image <subcommand> [options]
2641
+
2642
+ Subcommands:
2643
+ build Build (and optionally push) a Docker image
2644
+
2645
+ Run "bunny-agent image build --help" for build options.
2646
+ `);
2647
+ }
2648
+ function printGlobalHelp() {
2649
+ console.log(`
2650
+ \u{1F916} BunnyAgent Runner CLI
2651
+
2652
+ Usage:
2653
+ bunny-agent <command> [options]
2654
+
2655
+ Commands:
2656
+ run Run an agent locally (streams AI SDK UI messages to stdout)
2657
+ image build Build a BunnyAgent Docker image (with optional --push)
2658
+
2659
+ Run "bunny-agent <command> --help" for command-specific options.
2660
+ `);
2661
+ }
2662
+ async function main() {
2663
+ const sub = getSubcommand();
2664
+ if (!sub || sub === "--help" || sub === "-h") {
2665
+ printGlobalHelp();
2666
+ process.exit(0);
2667
+ }
2668
+ switch (sub) {
2669
+ case "run": {
2670
+ const args = parseRunArgs();
2671
+ process.chdir(args.cwd);
2672
+ await runAgent({
2673
+ runner: args.runner,
2674
+ model: args.model,
2675
+ userInput: args.userInput,
2676
+ systemPrompt: args.systemPrompt,
2677
+ maxTurns: args.maxTurns,
2678
+ allowedTools: args.allowedTools,
2679
+ skillPaths: args.skillPaths,
2680
+ resume: args.resume,
2681
+ yolo: args.yolo
2682
+ });
2683
+ break;
2684
+ }
2685
+ case "image": {
2686
+ const subSub = getSubSubcommand();
2687
+ if (!subSub || subSub === "--help" || subSub === "-h") {
2688
+ printImageHelp();
2689
+ process.exit(0);
2690
+ }
2691
+ if (subSub === "build") {
2692
+ const args = parseImageBuildArgs();
2693
+ await buildImage(args);
2694
+ } else {
2695
+ console.error(`Unknown image subcommand: ${subSub}`);
2696
+ printImageHelp();
2697
+ process.exit(1);
2698
+ }
2699
+ break;
2700
+ }
2701
+ default:
2702
+ console.error(`Unknown command: ${sub}`);
2703
+ printGlobalHelp();
2704
+ process.exit(1);
2705
+ }
2706
+ }
2707
+ main().catch((error) => {
2708
+ console.error("Fatal error:", error.message);
2709
+ process.exit(1);
2710
+ });