@bunny-agent/runner-cli 0.9.39 → 0.9.41

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.
Files changed (2) hide show
  1. package/dist/bundle.mjs +321 -40
  2. package/package.json +4 -4
package/dist/bundle.mjs CHANGED
@@ -1277,16 +1277,283 @@ function createOpenCodeRunner(options = {}) {
1277
1277
  };
1278
1278
  }
1279
1279
 
1280
+ // ../../packages/runner-pi/dist/ask-user-question-tool.js
1281
+ import { createHash } from "node:crypto";
1282
+ import { existsSync as existsSync4, mkdirSync as mkdirSync2, readFileSync as readFileSync3, unlinkSync as unlinkSync3, writeFileSync as writeFileSync3 } from "node:fs";
1283
+ import { join as join5 } from "node:path";
1284
+ var TOOL_NAME = "ask_user_question";
1285
+ var APPROVAL_DIR = join5(".bunny-agent", "approvals");
1286
+ var ASK_USER_QUESTION_TIMEOUT_MS = 12e4;
1287
+ var ASK_USER_QUESTION_POLL_MS = 500;
1288
+ var MAX_QUESTIONS = 4;
1289
+ var MIN_OPTIONS = 2;
1290
+ var MAX_OPTIONS = 4;
1291
+ var MAX_HEADER_LEN = 12;
1292
+ var MAX_QUESTION_LEN = 500;
1293
+ var MAX_OPTION_LABEL_LEN = 80;
1294
+ var MAX_OPTION_DESCRIPTION_LEN = 240;
1295
+ var ESC = String.fromCharCode(27);
1296
+ var CSI = String.fromCharCode(155);
1297
+ var ST = String.fromCharCode(156);
1298
+ var OSC = String.fromCharCode(157);
1299
+ var BEL = String.fromCharCode(7);
1300
+ var ANSI_OSC_RE = new RegExp(`(?:${ESC}\\]|${OSC})[^${ESC}${ST}${BEL}]*(?:${BEL}|${ESC}\\\\|${ST})`, "g");
1301
+ var ANSI_CTL_RE = new RegExp(`(?:${ESC}\\[[0-?]*[ -/]*[@-~]|${CSI}[0-?]*[ -/]*[@-~]|${ESC}[@-Z\\\\\\-_])`, "g");
1302
+ var CTL_CHAR_RE = new RegExp(`[${String.fromCharCode(0)}-${String.fromCharCode(31)}${String.fromCharCode(127)}-${String.fromCharCode(159)}]+`, "g");
1303
+ var askUserQuestionSchema = {
1304
+ type: "object",
1305
+ required: ["questions"],
1306
+ properties: {
1307
+ questions: {
1308
+ type: "array",
1309
+ minItems: 1,
1310
+ maxItems: MAX_QUESTIONS,
1311
+ description: `Between 1 and ${MAX_QUESTIONS} questions to ask the user.`,
1312
+ items: {
1313
+ type: "object",
1314
+ required: ["question", "header", "multiSelect", "options"],
1315
+ properties: {
1316
+ question: {
1317
+ type: "string",
1318
+ description: "The full question to ask the user. Should be specific, end with a question mark, and avoid jargon the user has not used."
1319
+ },
1320
+ header: {
1321
+ type: "string",
1322
+ description: `Short label (max ${MAX_HEADER_LEN} chars) shown as a chip/tag in the UI. Examples: "Library", "Auth method", "Approach".`
1323
+ },
1324
+ multiSelect: {
1325
+ type: "boolean",
1326
+ description: "Set true when the choices are not mutually exclusive and the user may select more than one."
1327
+ },
1328
+ options: {
1329
+ type: "array",
1330
+ minItems: MIN_OPTIONS,
1331
+ maxItems: MAX_OPTIONS,
1332
+ description: `Between ${MIN_OPTIONS} and ${MAX_OPTIONS} mutually exclusive options. Do not add an "Other" option \u2014 the host adds one automatically.`,
1333
+ items: {
1334
+ type: "object",
1335
+ required: ["label", "description"],
1336
+ properties: {
1337
+ label: {
1338
+ type: "string",
1339
+ description: "Display text for the option (1-5 words). Should clearly describe the choice."
1340
+ },
1341
+ description: {
1342
+ type: "string",
1343
+ description: "What this option means or what happens if chosen \u2014 call out trade-offs."
1344
+ }
1345
+ }
1346
+ }
1347
+ }
1348
+ }
1349
+ }
1350
+ }
1351
+ }
1352
+ };
1353
+ var TOOL_DESCRIPTION = `Ask the user one or more multiple-choice questions when the task is genuinely ambiguous and you need user input to proceed (choosing between libraries, design approaches, trade-offs, etc.). Each question carries 2-${MAX_OPTIONS} options; up to ${MAX_QUESTIONS} questions per call. The user can also select an automatically-provided "Other" option to give free-form input. Use sparingly \u2014 prefer making a sensible default choice when the task is clear.`;
1354
+ function sanitizeText(value) {
1355
+ if (typeof value !== "string")
1356
+ return "";
1357
+ return value.replace(ANSI_OSC_RE, "").replace(ANSI_CTL_RE, "").replace(CTL_CHAR_RE, " ").replace(/\s+/g, " ").trim();
1358
+ }
1359
+ function truncate(value, max) {
1360
+ return value.length <= max ? value : `${value.slice(0, Math.max(0, max - 3))}...`;
1361
+ }
1362
+ function sanitizeQuestions(questions) {
1363
+ return questions.map((q) => ({
1364
+ question: truncate(sanitizeText(q.question), MAX_QUESTION_LEN),
1365
+ header: truncate(sanitizeText(q.header), MAX_HEADER_LEN),
1366
+ multiSelect: !!q.multiSelect,
1367
+ options: q.options.map((o) => ({
1368
+ label: truncate(sanitizeText(o.label), MAX_OPTION_LABEL_LEN),
1369
+ description: truncate(sanitizeText(o.description), MAX_OPTION_DESCRIPTION_LEN)
1370
+ }))
1371
+ }));
1372
+ }
1373
+ function readApproval(file) {
1374
+ try {
1375
+ const raw = readFileSync3(file, "utf-8");
1376
+ return JSON.parse(raw);
1377
+ } catch {
1378
+ return void 0;
1379
+ }
1380
+ }
1381
+ var TOOL_CALL_ID_PREFIX_LEN = 32;
1382
+ var TOOL_CALL_ID_HASH_LEN = 16;
1383
+ function sanitizeToolCallId(toolCallId) {
1384
+ const safePrefix = toolCallId.replace(/[^A-Za-z0-9_-]/g, "_").slice(0, TOOL_CALL_ID_PREFIX_LEN);
1385
+ const hash = createHash("sha1").update(toolCallId).digest("hex").slice(0, TOOL_CALL_ID_HASH_LEN);
1386
+ return `${safePrefix}-${hash}`;
1387
+ }
1388
+ function safeUnlink(file) {
1389
+ try {
1390
+ if (existsSync4(file))
1391
+ unlinkSync3(file);
1392
+ } catch {
1393
+ }
1394
+ }
1395
+ async function pollApprovalFile(approvalFile, signal, options = {}) {
1396
+ const timeoutMs = options.timeoutMs ?? ASK_USER_QUESTION_TIMEOUT_MS;
1397
+ const pollMs = options.pollMs ?? ASK_USER_QUESTION_POLL_MS;
1398
+ const deadline = Date.now() + timeoutMs;
1399
+ let lastApproval;
1400
+ while (true) {
1401
+ if (signal?.aborted) {
1402
+ return { kind: "aborted", answers: lastApproval?.answers ?? {} };
1403
+ }
1404
+ const approval = readApproval(approvalFile);
1405
+ if (approval)
1406
+ lastApproval = approval;
1407
+ if (approval && approval.status !== "pending") {
1408
+ const answers = approval.answers ?? {};
1409
+ if (approval.status === "completed" && Object.keys(answers).length > 0) {
1410
+ return { kind: "answered", answers };
1411
+ }
1412
+ if (approval.status === "declined") {
1413
+ return { kind: "declined", answers, reason: approval.reason };
1414
+ }
1415
+ if (approval.status === "cancelled") {
1416
+ return { kind: "cancelled", answers, reason: approval.reason };
1417
+ }
1418
+ return {
1419
+ kind: "declined",
1420
+ answers,
1421
+ reason: approval.reason ?? "no_answer_provided"
1422
+ };
1423
+ }
1424
+ if (Date.now() >= deadline) {
1425
+ const partial = lastApproval?.answers ?? {};
1426
+ return {
1427
+ kind: Object.keys(partial).length > 0 ? "answered" : "timeout",
1428
+ answers: partial
1429
+ };
1430
+ }
1431
+ await sleepWithAbort(Math.min(pollMs, Math.max(0, deadline - Date.now())), signal);
1432
+ }
1433
+ }
1434
+ function sleepWithAbort(ms, signal) {
1435
+ return new Promise((resolve4) => {
1436
+ if (ms <= 0) {
1437
+ resolve4();
1438
+ return;
1439
+ }
1440
+ if (signal?.aborted) {
1441
+ resolve4();
1442
+ return;
1443
+ }
1444
+ const onAbort = () => {
1445
+ clearTimeout(timer);
1446
+ resolve4();
1447
+ };
1448
+ const timer = setTimeout(() => {
1449
+ signal?.removeEventListener("abort", onAbort);
1450
+ resolve4();
1451
+ }, ms);
1452
+ signal?.addEventListener("abort", onAbort, { once: true });
1453
+ });
1454
+ }
1455
+ function formatAcceptedText(questions, answers) {
1456
+ const lines = ["User answered:"];
1457
+ for (const q of questions) {
1458
+ const raw = answers[q.question];
1459
+ const formatted = Array.isArray(raw) ? raw.join(", ") : typeof raw === "string" && raw.length > 0 ? raw : "(no answer)";
1460
+ lines.push(`Q: ${q.question}`);
1461
+ lines.push(`A: ${formatted}`);
1462
+ }
1463
+ return lines.join("\n");
1464
+ }
1465
+ function formatDeclinedText(reason, partial) {
1466
+ const partialNote = Object.keys(partial).length > 0 ? ` Partial answers received: ${JSON.stringify(partial)}.` : "";
1467
+ return `User did not answer the questions (reason: ${reason}). Make a sensible default choice and continue, or ask again if the choice is genuinely required.${partialNote}`;
1468
+ }
1469
+ function buildAskUserQuestionTool(opts) {
1470
+ const { cwd, timeoutMs, pollMs } = opts;
1471
+ return {
1472
+ name: TOOL_NAME,
1473
+ label: "ask_user_question",
1474
+ description: TOOL_DESCRIPTION,
1475
+ promptSnippet: "ask_user_question: ask the user 1-4 multiple-choice questions when the task is genuinely ambiguous.",
1476
+ promptGuidelines: [
1477
+ "Use ask_user_question only when the task is genuinely ambiguous and a user choice is required to proceed; otherwise pick a sensible default.",
1478
+ "Each question must carry 2-4 mutually exclusive options. Do not add an 'Other' option \u2014 the host adds one automatically."
1479
+ ],
1480
+ // Pause other tool calls while a user prompt is open; otherwise pi may
1481
+ // surface output the user has not yet had a chance to react to.
1482
+ executionMode: "sequential",
1483
+ // biome-ignore lint/suspicious/noExplicitAny: TypeBox accepts plain JSON Schema literals here, see image-tools.ts.
1484
+ parameters: askUserQuestionSchema,
1485
+ async execute(toolCallId, params, signal) {
1486
+ const raw = params.questions ?? [];
1487
+ const sanitized = sanitizeQuestions(raw);
1488
+ const sanitizedInput = { questions: sanitized };
1489
+ const approvalDir = join5(cwd, APPROVAL_DIR);
1490
+ const approvalFile = join5(approvalDir, `${sanitizeToolCallId(toolCallId)}.json`);
1491
+ try {
1492
+ mkdirSync2(approvalDir, { recursive: true });
1493
+ const pending = {
1494
+ status: "pending",
1495
+ toolName: TOOL_NAME,
1496
+ input: sanitizedInput,
1497
+ questions: sanitized,
1498
+ answers: {}
1499
+ };
1500
+ writeFileSync3(approvalFile, JSON.stringify(pending));
1501
+ } catch (error) {
1502
+ const message = error instanceof Error ? error.message : "Unknown error";
1503
+ return {
1504
+ content: [
1505
+ {
1506
+ type: "text",
1507
+ text: `ask_user_question failed to create approval file: ${message}`
1508
+ }
1509
+ ],
1510
+ details: void 0
1511
+ };
1512
+ }
1513
+ let outcome;
1514
+ try {
1515
+ outcome = await pollApprovalFile(approvalFile, signal, {
1516
+ timeoutMs,
1517
+ pollMs
1518
+ });
1519
+ } finally {
1520
+ safeUnlink(approvalFile);
1521
+ }
1522
+ if (outcome.kind === "answered") {
1523
+ return {
1524
+ content: [
1525
+ {
1526
+ type: "text",
1527
+ text: formatAcceptedText(sanitized, outcome.answers)
1528
+ }
1529
+ ],
1530
+ details: void 0
1531
+ };
1532
+ }
1533
+ const reason = outcome.kind === "timeout" ? "timeout" : outcome.kind === "aborted" ? "run_aborted" : outcome.reason ?? outcome.kind;
1534
+ return {
1535
+ content: [
1536
+ {
1537
+ type: "text",
1538
+ text: formatDeclinedText(reason, outcome.answers)
1539
+ }
1540
+ ],
1541
+ details: void 0
1542
+ };
1543
+ }
1544
+ };
1545
+ }
1546
+
1280
1547
  // ../../packages/runner-pi/dist/pi-runner.js
1281
- import { appendFileSync as appendFileSync2, existsSync as existsSync5, unlinkSync as unlinkSync3 } from "node:fs";
1282
- import { join as join8 } from "node:path";
1548
+ import { appendFileSync as appendFileSync2, existsSync as existsSync6, unlinkSync as unlinkSync4 } from "node:fs";
1549
+ import { join as join9 } from "node:path";
1283
1550
  import { getModel } from "@earendil-works/pi-ai";
1284
1551
  import { AuthStorage, createAgentSession, ModelRegistry, SessionManager as SessionManager2 } from "@earendil-works/pi-coding-agent";
1285
1552
 
1286
1553
  // ../../packages/runner-pi/dist/bunny-agent-resource-loader.js
1287
- import { existsSync as existsSync4 } from "node:fs";
1554
+ import { existsSync as existsSync5 } from "node:fs";
1288
1555
  import { homedir } from "node:os";
1289
- import { isAbsolute, join as join5, resolve as resolve2 } from "node:path";
1556
+ import { isAbsolute, join as join6, resolve as resolve2 } from "node:path";
1290
1557
  import { DefaultResourceLoader, loadSkills } from "@earendil-works/pi-coding-agent";
1291
1558
  var LOG_PREFIX = "[bunny-agent:pi]";
1292
1559
  function logSkillLoad(cwd, agentDir, skillPaths, result) {
@@ -1298,7 +1565,7 @@ function logSkillLoad(cwd, agentDir, skillPaths, result) {
1298
1565
  ];
1299
1566
  for (const raw of skillPaths) {
1300
1567
  const abs = isAbsolute(raw) ? raw : resolve2(cwd, raw);
1301
- lines.push(` ${raw} -> ${abs} (exists: ${existsSync4(abs) ? "yes" : "no"})`);
1568
+ lines.push(` ${raw} -> ${abs} (exists: ${existsSync5(abs) ? "yes" : "no"})`);
1302
1569
  }
1303
1570
  lines.push(` loaded skills: ${result.skills.length}`);
1304
1571
  if (result.skills.length > 0) {
@@ -1316,7 +1583,7 @@ function logSkillLoad(cwd, agentDir, skillPaths, result) {
1316
1583
  var BunnyAgentResourceLoader = class {
1317
1584
  constructor(options = {}) {
1318
1585
  this.cwd = options.cwd ?? process.cwd();
1319
- this.agentDir = options.agentDir ?? join5(homedir(), ".bunny", "agent");
1586
+ this.agentDir = options.agentDir ?? join6(homedir(), ".bunny", "agent");
1320
1587
  this.skillPaths = options.skillPaths ?? [];
1321
1588
  this.extraAppendPrompt = options.appendSystemPrompt;
1322
1589
  this.delegate = new DefaultResourceLoader({
@@ -1373,8 +1640,8 @@ var BunnyAgentResourceLoader = class {
1373
1640
  };
1374
1641
 
1375
1642
  // ../../packages/runner-pi/dist/image-tools.js
1376
- import { mkdirSync as mkdirSync2, writeFileSync as writeFileSync3 } from "node:fs";
1377
- import { dirname as dirname3, extname, join as join6 } from "node:path";
1643
+ import { mkdirSync as mkdirSync3, writeFileSync as writeFileSync4 } from "node:fs";
1644
+ import { dirname as dirname3, extname, join as join7 } from "node:path";
1378
1645
  var generateImageSchema = {
1379
1646
  type: "object",
1380
1647
  properties: {
@@ -1607,8 +1874,8 @@ async function saveImageItem(item, filePath, apiKey) {
1607
1874
  const b64 = await resolveB64(item, apiKey);
1608
1875
  if (!b64)
1609
1876
  return void 0;
1610
- mkdirSync2(dirname3(filePath), { recursive: true });
1611
- writeFileSync3(filePath, Buffer.from(b64, "base64"));
1877
+ mkdirSync3(dirname3(filePath), { recursive: true });
1878
+ writeFileSync4(filePath, Buffer.from(b64, "base64"));
1612
1879
  return filePath;
1613
1880
  }
1614
1881
  function buildImageGenerateTool(cwd, imageModelId, baseUrl, apiKey) {
@@ -1635,7 +1902,7 @@ function buildImageGenerateTool(cwd, imageModelId, baseUrl, apiKey) {
1635
1902
  const imageSize = p.imageSize;
1636
1903
  const rawFilename = p.filename;
1637
1904
  const filename = rawFilename ? extname(rawFilename) ? rawFilename : `${rawFilename}.png` : `image_${Date.now()}.png`;
1638
- const filePath = join6(cwd, filename.replace(/[^a-zA-Z0-9_\-./]/g, "_"));
1905
+ const filePath = join7(cwd, filename.replace(/[^a-zA-Z0-9_\-./]/g, "_"));
1639
1906
  try {
1640
1907
  const url = `${baseUrl.replace(/\/$/, "")}/v1/images/generations`;
1641
1908
  const res = await fetch(url, {
@@ -1783,7 +2050,7 @@ function buildImageEditTool(cwd, imageModelId, baseUrl, apiKey) {
1783
2050
  // biome-ignore lint/suspicious/noExplicitAny: plain JSON Schema compatible with TypeBox TSchema
1784
2051
  parameters: editImageSchema,
1785
2052
  async execute(_toolCallId, params, signal, _onUpdate) {
1786
- const { readFileSync: readFileSync4, existsSync: existsSync8 } = await import("node:fs");
2053
+ const { readFileSync: readFileSync5, existsSync: existsSync9 } = await import("node:fs");
1787
2054
  const { resolve: resolve4, basename: basename2 } = await import("node:path");
1788
2055
  const p = params;
1789
2056
  const imagePath = p.image;
@@ -1796,7 +2063,7 @@ function buildImageEditTool(cwd, imageModelId, baseUrl, apiKey) {
1796
2063
  const rawFilename = p.filename;
1797
2064
  const safePrompt = buildPolicySafeEditPrompt(prompt);
1798
2065
  const resolvedImage = resolve4(cwd, imagePath);
1799
- if (!existsSync8(resolvedImage)) {
2066
+ if (!existsSync9(resolvedImage)) {
1800
2067
  return {
1801
2068
  content: [
1802
2069
  {
@@ -1808,9 +2075,9 @@ function buildImageEditTool(cwd, imageModelId, baseUrl, apiKey) {
1808
2075
  };
1809
2076
  }
1810
2077
  const filename = rawFilename ? extname(rawFilename) ? rawFilename : `${rawFilename}.png` : `edited_${Date.now()}.png`;
1811
- const filePath = join6(cwd, filename.replace(/[^a-zA-Z0-9_\-./]/g, "_"));
2078
+ const filePath = join7(cwd, filename.replace(/[^a-zA-Z0-9_\-./]/g, "_"));
1812
2079
  try {
1813
- const imageBuffer = readFileSync4(resolvedImage);
2080
+ const imageBuffer = readFileSync5(resolvedImage);
1814
2081
  const fields = [
1815
2082
  { name: "model", value: imageModelId },
1816
2083
  { name: "prompt", value: safePrompt.prompt },
@@ -1840,11 +2107,11 @@ function buildImageEditTool(cwd, imageModelId, baseUrl, apiKey) {
1840
2107
  ];
1841
2108
  if (maskPath) {
1842
2109
  const resolvedMask = resolve4(cwd, maskPath);
1843
- if (existsSync8(resolvedMask)) {
2110
+ if (existsSync9(resolvedMask)) {
1844
2111
  files.push({
1845
2112
  name: "mask",
1846
2113
  filename: basename2(resolvedMask),
1847
- buffer: readFileSync4(resolvedMask),
2114
+ buffer: readFileSync5(resolvedMask),
1848
2115
  mime: detectImageMime(resolvedMask)
1849
2116
  });
1850
2117
  }
@@ -1907,7 +2174,7 @@ function buildImageEditTool(cwd, imageModelId, baseUrl, apiKey) {
1907
2174
 
1908
2175
  // ../../packages/runner-pi/dist/session-utils.js
1909
2176
  import { closeSync, fstatSync, openSync, readdirSync as readdirSync2, readSync, statSync as statSync2 } from "node:fs";
1910
- import { join as join7 } from "node:path";
2177
+ import { join as join8 } from "node:path";
1911
2178
  import { SessionManager } from "@earendil-works/pi-coding-agent";
1912
2179
  var MAX_SESSION_FILE_BYTES = Number(process.env.SANDAGENT_MAX_SESSION_BYTES) || 10 * 1024 * 1024;
1913
2180
  function resolveSessionPathById(cwd, sessionId) {
@@ -1916,7 +2183,7 @@ function resolveSessionPathById(cwd, sessionId) {
1916
2183
  try {
1917
2184
  const suffix = `_${sessionId}.jsonl`;
1918
2185
  const match = readdirSync2(sessionsDir).find((f) => f.endsWith(suffix));
1919
- return match ? join7(sessionsDir, match) : void 0;
2186
+ return match ? join8(sessionsDir, match) : void 0;
1920
2187
  } catch {
1921
2188
  return void 0;
1922
2189
  }
@@ -2096,15 +2363,16 @@ var PiAISDKStreamConverter = class {
2096
2363
  }
2097
2364
  if (event.type === "tool_execution_start") {
2098
2365
  chunks.push(...this.endTextStreamIfOpen());
2366
+ const toolCallId = sanitizeToolCallId(event.toolCallId);
2099
2367
  chunks.push(sseData({
2100
2368
  type: "tool-input-start",
2101
- toolCallId: event.toolCallId,
2369
+ toolCallId,
2102
2370
  toolName: event.toolName,
2103
2371
  dynamic: true,
2104
2372
  providerExecuted: true
2105
2373
  }), sseData({
2106
2374
  type: "tool-input-available",
2107
- toolCallId: event.toolCallId,
2375
+ toolCallId,
2108
2376
  toolName: event.toolName,
2109
2377
  input: event.args,
2110
2378
  dynamic: true,
@@ -2117,10 +2385,11 @@ var PiAISDKStreamConverter = class {
2117
2385
  const raw = event.result?.details?.usage?.raw;
2118
2386
  if (raw != null)
2119
2387
  accumulateToolUsage(this.toolUsageTally, raw);
2388
+ const toolCallId = sanitizeToolCallId(event.toolCallId);
2120
2389
  if (event.isError) {
2121
2390
  chunks.push(sseData({
2122
2391
  type: "tool-output-error",
2123
- toolCallId: event.toolCallId,
2392
+ toolCallId,
2124
2393
  errorText: output,
2125
2394
  dynamic: true,
2126
2395
  providerExecuted: true
@@ -2128,7 +2397,7 @@ var PiAISDKStreamConverter = class {
2128
2397
  } else {
2129
2398
  chunks.push(sseData({
2130
2399
  type: "tool-output-available",
2131
- toolCallId: event.toolCallId,
2400
+ toolCallId,
2132
2401
  output,
2133
2402
  dynamic: true,
2134
2403
  providerExecuted: true
@@ -5351,6 +5620,12 @@ function resolveImageModelName(chatProvider, env) {
5351
5620
  function getEnvValue(optionsEnv, name) {
5352
5621
  return optionsEnv?.[name] ?? process.env[name];
5353
5622
  }
5623
+ function parsePositiveInt(value) {
5624
+ if (value === void 0 || value === "")
5625
+ return void 0;
5626
+ const n = Number(value);
5627
+ return Number.isFinite(n) && n > 0 ? Math.floor(n) : void 0;
5628
+ }
5354
5629
  function applyModelOverrides(model, provider, optionsEnv) {
5355
5630
  if (model == null)
5356
5631
  return;
@@ -5380,9 +5655,9 @@ function traceRawMessage(debugCwd, data, reset = false, optionsEnv) {
5380
5655
  if (!enabled)
5381
5656
  return;
5382
5657
  try {
5383
- const file = join8(debugCwd, "pi-message-stream-debug.json");
5384
- if (reset && existsSync5(file))
5385
- unlinkSync3(file);
5658
+ const file = join9(debugCwd, "pi-message-stream-debug.json");
5659
+ if (reset && existsSync6(file))
5660
+ unlinkSync4(file);
5386
5661
  const type = data !== null && typeof data === "object" ? data.type : void 0;
5387
5662
  let payload = data;
5388
5663
  try {
@@ -5485,6 +5760,12 @@ function createPiRunner(options = {}) {
5485
5760
  customTools.push(buildImageGenerateTool(cwd, imageModelName, model.baseUrl, apiKey), buildImageEditTool(cwd, imageModelName, model.baseUrl, apiKey));
5486
5761
  }
5487
5762
  const toolRefDefinitions = options.toolRefs && options.toolRefs.length > 0 ? buildToolDefinitionsFromRefs(options.toolRefs) : [];
5763
+ const askTimeoutSeconds = parsePositiveInt(getEnvValue(options.env, "ASK_USER_QUESTION_TOOL_TIMEOUT"));
5764
+ const askUserQuestionTool = buildAskUserQuestionTool({
5765
+ cwd,
5766
+ timeoutMs: askTimeoutSeconds !== void 0 ? askTimeoutSeconds * 1e3 : void 0
5767
+ });
5768
+ customTools.push(askUserQuestionTool);
5488
5769
  const { session } = await createAgentSession({
5489
5770
  cwd,
5490
5771
  model,
@@ -5593,47 +5874,47 @@ function createPiRunner(options = {}) {
5593
5874
  }
5594
5875
 
5595
5876
  // ../../packages/runner-harness/dist/session.js
5596
- import { existsSync as existsSync6, mkdirSync as mkdirSync3, readFileSync as readFileSync3, writeFileSync as writeFileSync4 } from "node:fs";
5597
- import { join as join9 } from "node:path";
5877
+ import { existsSync as existsSync7, mkdirSync as mkdirSync4, readFileSync as readFileSync4, writeFileSync as writeFileSync5 } from "node:fs";
5878
+ import { join as join10 } from "node:path";
5598
5879
  var DIR = ".bunny-agent";
5599
5880
  var FILE = "session-id";
5600
5881
  function sessionPath(cwd) {
5601
- return join9(cwd, DIR, FILE);
5882
+ return join10(cwd, DIR, FILE);
5602
5883
  }
5603
5884
  function readSessionId(cwd) {
5604
5885
  try {
5605
5886
  const p = sessionPath(cwd);
5606
- if (!existsSync6(p))
5887
+ if (!existsSync7(p))
5607
5888
  return void 0;
5608
- return readFileSync3(p, "utf8").trim() || void 0;
5889
+ return readFileSync4(p, "utf8").trim() || void 0;
5609
5890
  } catch {
5610
5891
  return void 0;
5611
5892
  }
5612
5893
  }
5613
5894
  function writeSessionId(cwd, id) {
5614
5895
  try {
5615
- mkdirSync3(join9(cwd, DIR), { recursive: true });
5616
- writeFileSync4(sessionPath(cwd), id, "utf8");
5896
+ mkdirSync4(join10(cwd, DIR), { recursive: true });
5897
+ writeFileSync5(sessionPath(cwd), id, "utf8");
5617
5898
  } catch {
5618
5899
  }
5619
5900
  }
5620
5901
 
5621
5902
  // ../../packages/runner-harness/dist/skills.js
5622
- import { existsSync as existsSync7, readdirSync as readdirSync3, statSync as statSync3 } from "node:fs";
5903
+ import { existsSync as existsSync8, readdirSync as readdirSync3, statSync as statSync3 } from "node:fs";
5623
5904
  import { homedir as homedir2 } from "node:os";
5624
- import { join as join10 } from "node:path";
5905
+ import { join as join11 } from "node:path";
5625
5906
  function discoverSkillPaths(cwd) {
5626
5907
  const paths = [];
5627
5908
  for (const base of [
5628
- join10(cwd, "skills"),
5629
- join10(homedir2(), ".bunny-agent", "skills")
5909
+ join11(cwd, "skills"),
5910
+ join11(homedir2(), ".bunny-agent", "skills")
5630
5911
  ]) {
5631
- if (!existsSync7(base))
5912
+ if (!existsSync8(base))
5632
5913
  continue;
5633
5914
  try {
5634
5915
  for (const entry of readdirSync3(base)) {
5635
- const full = join10(base, entry);
5636
- if (statSync3(full).isDirectory() && existsSync7(join10(full, "SKILL.md"))) {
5916
+ const full = join11(base, entry);
5917
+ if (statSync3(full).isDirectory() && existsSync8(join11(full, "SKILL.md"))) {
5637
5918
  paths.push(full);
5638
5919
  }
5639
5920
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bunny-agent/runner-cli",
3
- "version": "0.9.39",
3
+ "version": "0.9.41",
4
4
  "description": "BunnyAgent Runner CLI - Like gemini-cli or claude-code, runs in your local terminal with AI SDK UI streaming",
5
5
  "type": "module",
6
6
  "bin": {
@@ -53,12 +53,12 @@
53
53
  "esbuild": "^0.27.2",
54
54
  "typescript": "^5.3.0",
55
55
  "vitest": "^1.6.1",
56
- "@bunny-agent/runner-harness": "0.1.1-beta.0",
57
56
  "@bunny-agent/runner-claude": "0.6.2",
58
57
  "@bunny-agent/runner-codex": "0.6.2",
58
+ "@bunny-agent/runner-gemini": "0.6.2",
59
+ "@bunny-agent/runner-harness": "0.1.1-beta.0",
59
60
  "@bunny-agent/runner-opencode": "0.6.2",
60
- "@bunny-agent/runner-pi": "0.6.4-beta.0",
61
- "@bunny-agent/runner-gemini": "0.6.2"
61
+ "@bunny-agent/runner-pi": "0.6.4-beta.0"
62
62
  },
63
63
  "scripts": {
64
64
  "build": "tsc && pnpm bundle",