@botconnector/bccli 0.4.9 → 0.4.11

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 (55) hide show
  1. package/dist/{acp-VEXFXT5P.js → acp-IPHWC6CO.js} +20 -5
  2. package/dist/acp.d.ts +17 -1
  3. package/dist/args-K5AJFDCZ.js +13 -0
  4. package/dist/args.d.ts +6 -0
  5. package/dist/budget.d.ts +4 -1
  6. package/dist/{chunk-2YG6L3YO.js → chunk-2DFY43RP.js} +2 -2
  7. package/dist/{chunk-KREGVQ63.js → chunk-6BXW7DGQ.js} +47 -18
  8. package/dist/{chunk-WSSJH765.js → chunk-6KZIOICR.js} +1 -1
  9. package/dist/{chunk-OUJRD7GG.js → chunk-6M5T7N7A.js} +8 -3
  10. package/dist/{chunk-DRJSANKH.js → chunk-C2FUGQJX.js} +9 -7
  11. package/dist/{chunk-OETLWNGS.js → chunk-F54FDJBD.js} +3 -3
  12. package/dist/{chunk-XCVYR5BK.js → chunk-G5TG43R4.js} +15 -1
  13. package/dist/{chunk-RRECJTOG.js → chunk-IHQGXDSG.js} +1 -1
  14. package/dist/{chunk-AWBEZY4D.js → chunk-JFYQCPXL.js} +7 -5
  15. package/dist/{chunk-6H6MPG7L.js → chunk-MBAMDCCV.js} +23 -9
  16. package/dist/{chunk-HCW67S5M.js → chunk-N6APAYSF.js} +1 -1
  17. package/dist/{chunk-TLCFBQ6E.js → chunk-NT2G5FSX.js} +2 -2
  18. package/dist/{chunk-CSIQDFEF.js → chunk-OAXD3X4R.js} +34 -50
  19. package/dist/{chunk-EOJNFGOP.js → chunk-OOOS5RCT.js} +98 -28
  20. package/dist/{chunk-Z2ELYJ5O.js → chunk-WSZF2CTU.js} +11 -4
  21. package/dist/{chunk-62UTSCWF.js → chunk-X24APPXO.js} +1 -1
  22. package/dist/{chunk-EAR2C6TS.js → chunk-XZ2TI5HV.js} +1 -1
  23. package/dist/{chunk-HPYI7L4W.js → chunk-Y6AZGGH7.js} +4 -4
  24. package/dist/{chunk-WFZKJY3R.js → chunk-YYTFJUO2.js} +3 -2
  25. package/dist/cli.js +40 -40
  26. package/dist/{completion-FETH4OGC.js → completion-QVDGAYUY.js} +1 -1
  27. package/dist/{config-KR5JI5RA.js → config-7TQ2KVVX.js} +3 -3
  28. package/dist/{config-RVMC23W3.js → config-RZY3XQZJ.js} +2 -2
  29. package/dist/{ghBotCli-Z6GWXCJ6.js → ghBotCli-HHJ4ANTH.js} +18 -16
  30. package/dist/{integrationCli-GPRR6J6W.js → integrationCli-LNNZDGZX.js} +40 -5
  31. package/dist/{login-DFSN77L3.js → login-MQEZGF2C.js} +6 -4
  32. package/dist/login.d.ts +2 -0
  33. package/dist/{mcpCli-64P3SVUE.js → mcpCli-CUIOYJ2A.js} +22 -7
  34. package/dist/{modelsCli-PFQY3G2Q.js → modelsCli-AAFHFI4H.js} +6 -6
  35. package/dist/{oauthLogin-NR22NWEV.js → oauthLogin-YFL2N4QA.js} +1 -1
  36. package/dist/{pluginsCli-UKRJ3AHN.js → pluginsCli-2QXXFLQA.js} +6 -2
  37. package/dist/{prCli-6FCI36AJ.js → prCli-KSA73RCS.js} +1 -1
  38. package/dist/print.d.ts +1 -0
  39. package/dist/providerCli-TXMVPSW5.js +15 -0
  40. package/dist/sdk.js +196 -87
  41. package/dist/{serve-PKUTKQQT.js → serve-EIQHEUYD.js} +3 -3
  42. package/dist/{serve-JYXPQAVY.js → serve-QYDBFTHC.js} +16 -16
  43. package/dist/session-CQCQ7KAP.js +12 -0
  44. package/dist/setup-DBSGMWHM.js +24 -0
  45. package/dist/setup.d.ts +5 -0
  46. package/dist/{statsCli-FR7GOKWO.js → statsCli-EHNJIQ2Z.js} +2 -2
  47. package/dist/tools/bash.d.ts +2 -0
  48. package/dist/{transferCli-GIE4MEJ4.js → transferCli-IXJ2O74Q.js} +4 -4
  49. package/dist/{ui-NZVOCQSU.js → ui-A2LASWDK.js} +14 -14
  50. package/dist/{update-DIYDD7LT.js → update-TR62JV3A.js} +2 -2
  51. package/package.json +1 -1
  52. package/dist/args-6XMUZDAK.js +0 -13
  53. package/dist/providerCli-PBSZECBM.js +0 -15
  54. package/dist/session-XABMBIIF.js +0 -12
  55. package/dist/setup-CLPQLTM7.js +0 -24
package/dist/sdk.js CHANGED
@@ -178,6 +178,7 @@ Options:
178
178
  -r, --resume pick a session to continue
179
179
  --allow-all run all tools without asking for permission
180
180
  --allowed-tools <a,b> tools allowed without asking: bash, edit, fetch
181
+ --fail-on-tool-error with -p: exit 1 when any tool call fails (for CI)
181
182
  -w, --worktree <name> run in git worktree ../<repo>.worktrees/<name> (created, or reused if it exists)
182
183
  --output-format <fmt> text | json | stream-json for -p (machine-readable output)
183
184
  --permission-mode <mode> default | acceptEdits | plan | allowAll
@@ -208,6 +209,7 @@ Opsi:
208
209
  -r, --resume pilih sesi untuk dilanjutkan
209
210
  --allow-all jalankan semua alat tanpa minta izin
210
211
  --allowed-tools <a,b> alat yang boleh tanpa izin: bash, edit, fetch
212
+ --fail-on-tool-error dengan -p: keluar 1 bila ada tool gagal (untuk CI)
211
213
  -w, --worktree <name> jalankan di git worktree ../<repo>.worktrees/<name> (dibuat, atau dipakai ulang jika ada)
212
214
  --output-format <fmt> text | json | stream-json untuk -p (output mesin)
213
215
  --permission-mode <mode> default | acceptEdits | plan | allowAll
@@ -225,6 +227,7 @@ Opsi:
225
227
  "usageCap.usd must be a number greater than 0.": "usageCap.usd harus angka lebih dari 0.",
226
228
  "usageCap.usd requires usageCap.prices (USD per 1M tokens).": "usageCap.usd memerlukan usageCap.prices (USD per 1 juta token).",
227
229
  'usageCap.prices["{key}"] must have numeric input and output (USD per 1M tokens).': 'usageCap.prices["{key}"] harus punya input dan output numerik (USD per 1 juta token).',
230
+ 'usageCap.prices["{key}"].cachedInput must be a number >= 0 (USD per 1M tokens).': 'usageCap.prices["{key}"].cachedInput harus angka >= 0 (USD per 1 juta token).',
228
231
  "usageCap needs at least one of: tokens, usd.": "usageCap memerlukan setidaknya salah satu dari: tokens, usd.",
229
232
  'updateCheck must be "on" or "off".': 'updateCheck harus "on" atau "off".',
230
233
  "repoMap must be true or false.": "repoMap harus true atau false.",
@@ -266,6 +269,7 @@ Opsi:
266
269
  'npm removed the files of {name} but the config entry {entry} remains: {reason}. bccli will refuse to start while that entry is listed \u2014 fix the config error at {path}, then run "bccli plugins remove {name}" again (safe to repeat), or delete the entry manually.': 'npm sudah menghapus file {name} tetapi entry config {entry} tertinggal: {reason}. bccli menolak start selama entry itu terdaftar \u2014 perbaiki error config di {path}, lalu jalankan lagi "bccli plugins remove {name}" (aman diulang), atau hapus entry secara manual.',
267
270
  "Skipping malformed entry: {entry} \u2014 fix it manually in {path}.": "Melewatkan entry rusak: {entry} \u2014 perbaiki manual di {path}.",
268
271
  "No managed plugins to update.": "Tidak ada plugin terelola untuk diperbarui.",
272
+ "No plugins are installed. Install one with: bccli plugins install <spec>": "Belum ada plugin terpasang. Pasang dengan: bccli plugins install <spec>",
269
273
  "update requires a plugin name or no argument: bccli plugins update [name]": "update butuh nama plugin atau tanpa argumen: bccli plugins update [nama]",
270
274
  "{name} updated to latest.": "{name} diperbarui ke versi terbaru.",
271
275
  "Use either --key or --logout, not both.": "Pakai --key atau --logout, jangan keduanya.",
@@ -323,6 +327,13 @@ Opsi:
323
327
  "No API key for {id} yet. Run `bccli login` or set the env var {env}.": "API key untuk {id} belum ada. Jalankan `bccli login` atau set env {env}.",
324
328
  "Error from the provider: {message}": "Error dari provider: {message}",
325
329
  "Cannot connect to {url}: {error}": "Tidak bisa terhubung ke {url}: {error}",
330
+ "The API key was rejected. Check the key for this provider (bccli login) or its API key environment variable.": "Kunci API ditolak. Periksa kunci provider ini (bccli login) atau env var kuncinya.",
331
+ "Model or endpoint not found. Check the model name and the provider base URL.": "Model atau endpoint tidak ditemukan. Periksa nama model dan base URL provider.",
332
+ "Rate limit reached. Wait a moment, or switch model with /model.": "Batas laju tercapai. Tunggu sebentar, atau ganti model dengan /model.",
333
+ "The provider is having trouble. Try again, or switch model with /model.": "Penyedia sedang bermasalah. Coba lagi, atau ganti model dengan /model.",
334
+ "The provider did not respond within {seconds}s. Check the connection, then try again.": "Penyedia tidak merespons dalam {seconds} detik. Periksa koneksi, lalu coba lagi.",
335
+ "The provider stopped sending data for {seconds}s. The answer above may be incomplete; try again.": "Penyedia berhenti mengirim data selama {seconds} detik. Jawaban di atas mungkin tidak lengkap; coba lagi.",
336
+ "The response ended before it finished (no finish_reason or [DONE]). The answer above is incomplete; try again.": "Respons berakhir sebelum selesai (tanpa finish_reason atau [DONE]). Jawaban di atas tidak lengkap; coba lagi.",
326
337
  "{status} from {url}: {message}": "{status} dari {url}: {message}",
327
338
  "Request failed": "Permintaan gagal",
328
339
  "Connection closed before the response finished.": "Koneksi ditutup sebelum respons selesai.",
@@ -347,7 +358,8 @@ Opsi:
347
358
  "Usage: bccli mcp {action} <name>": "Pemakaian: bccli mcp {action} <nama>",
348
359
  "{name} removed.": "{name} dihapus.",
349
360
  "{name} installed ({url}).": "{name} terpasang ({url}).",
350
- "{name} is not in the catalog. For another remote server use: bccli mcp add {name} --url <url>": "{name} tidak ada di katalog. Untuk server remote lain pakai: bccli mcp add {name} --url <url>",
361
+ "{name} installed (stdio: {command}).": "{name} terpasang (stdio: {command}).",
362
+ "{name} is not in the catalog. For another server use: bccli mcp add {name} --url <url> (HTTP) or bccli mcp add {name} --command <cmd> (stdio).": "{name} tidak ada di katalog. Untuk server lain pakai: bccli mcp add {name} --url <url> (HTTP) atau bccli mcp add {name} --command <cmd> (stdio).",
351
363
  "{name} installed. Active in the next bccli session.": "{name} terpasang. Aktif di sesi bccli berikutnya.",
352
364
  "control a browser (open pages, click, fill forms, screenshot)": "kontrol browser (buka halaman, klik, isi form, screenshot)",
353
365
  "up-to-date library documentation": "dokumentasi library terbaru",
@@ -401,6 +413,8 @@ Opsi:
401
413
  "API key: env {env} only (via key_env; a plaintext key in config.yaml is never written).": "API key: hanya env {env} (melalui key_env; key plaintext tidak pernah ditulis ke config.yaml).",
402
414
  "{target}: not managed by BCCLI yet.": "{target}: belum dikelola oleh BCCLI.",
403
415
  "{target}: the BotConnector integration was removed and the config restored.": "{target}: integrasi BotConnector dilepas dan config dipulihkan.",
416
+ '{target}: {path} changed after BCCLI connected it, so the old backup was not copied over it. Remove the BCCLI part (provider "botconnector" or the marked block) from that file by hand.': '{target}: {path} berubah setelah BCCLI menyambungkannya, jadi backup lama tidak ditimpa ke file itu. Hapus bagian BCCLI (provider "botconnector" atau blok bertanda) dari file tersebut secara manual.',
417
+ "{target}: the BotConnector integration was removed. {n} file(s) were left untouched (see above); delete the BCCLI entries in them by hand for a clean removal.": "{target}: integrasi BotConnector dilepas. {n} file tidak disentuh (lihat di atas); hapus entri BCCLI di dalamnya secara manual agar bersih.",
404
418
  "Custom commands:": "Perintah custom:",
405
419
  "Skills:": "Skill:",
406
420
  "shift+tab switch permission mode esc cancel the turn": "shift+tab ganti mode izin esc batalkan giliran",
@@ -606,6 +620,9 @@ function resolvedUsageCap(global) {
606
620
  if (!price || !nonNegative(price.input) || !nonNegative(price.output)) {
607
621
  throw new ConfigError(t('usageCap.prices["{key}"] must have numeric input and output (USD per 1M tokens).', { key }));
608
622
  }
623
+ if (price.cachedInput !== void 0 && !nonNegative(price.cachedInput)) {
624
+ throw new ConfigError(t('usageCap.prices["{key}"].cachedInput must be a number >= 0 (USD per 1M tokens).', { key }));
625
+ }
609
626
  }
610
627
  }
611
628
  if (tokens2 === void 0 && usd === void 0) throw new ConfigError(t("usageCap needs at least one of: tokens, usd."));
@@ -693,7 +710,7 @@ function loadConfig(cwd, env = process.env) {
693
710
  const project = readJsonConfig(join(cwd, ".bccli", "config.json"));
694
711
  const known = { ...DEFAULT_CONFIG.providers, ...global.providers };
695
712
  const fromProject = untrustedProviders(project, known);
696
- const rawFallbacks = project.fallbackModels ?? global.fallbackModels;
713
+ const rawFallbacks = global.fallbackModels;
697
714
  const fallbackModels = Array.isArray(rawFallbacks) ? rawFallbacks.filter((m) => typeof m === "string" && m.includes("/")) : void 0;
698
715
  return {
699
716
  model: project.model ?? global.model ?? DEFAULT_CONFIG.model,
@@ -773,7 +790,9 @@ function resolveModel(config, modelRef, env = process.env) {
773
790
  if (!provider) {
774
791
  throw new ConfigError(t('Provider "{id}" is not in the config. Available: {list}', { id: providerId, list: Object.keys(config.providers).join(", ") }));
775
792
  }
776
- const apiKey = readCredentials(env)[providerId] || (provider.apiKeyEnv ? env[provider.apiKeyEnv] : void 0) || void 0;
793
+ const untrusted = config.projectProviders?.includes(providerId) ?? false;
794
+ const stored = untrusted ? void 0 : readCredentials(env)[providerId];
795
+ const apiKey = stored || (provider.apiKeyEnv ? env[provider.apiKeyEnv] : void 0) || void 0;
777
796
  if (provider.apiKeyEnv && !apiKey) {
778
797
  throw new ConfigError(t("No API key for {id} yet. Run `bccli login` or set the env var {env}.", { id: providerId, env: provider.apiKeyEnv }));
779
798
  }
@@ -871,7 +890,7 @@ var Permissions = class {
871
890
  }
872
891
  /** Rules that "[a] allow for this session" would add; undefined when it must always be asked. */
873
892
  rulesFor(req) {
874
- if (requiresSensitiveToolApproval(req) && (req.kind === "read" || process.env.BC_TOOL_POLICY_ENFORCE === "true"))
893
+ if (requiresSensitiveToolApproval(req) && (req.kind === "read" || req.kind === "edit" || process.env.BC_TOOL_POLICY_ENFORCE === "true"))
875
894
  return void 0;
876
895
  if (req.kind === "edit") return this.editInProject(req.target) ? ["edit(project)"] : void 0;
877
896
  if (req.kind === "fetch") {
@@ -893,7 +912,8 @@ var Permissions = class {
893
912
  }
894
913
  check(req) {
895
914
  if (this.mode === "plan" && req.kind !== "read") return "deny";
896
- if (req.kind === "read" && this.mode !== "allowAll" && requiresSensitiveToolApproval(req)) return "ask";
915
+ if ((req.kind === "read" || req.kind === "edit") && this.mode !== "allowAll" && requiresSensitiveToolApproval(req))
916
+ return "ask";
897
917
  if (process.env.BC_TOOL_POLICY_ENFORCE === "true" && requiresSensitiveToolApproval(req)) return "ask";
898
918
  if (req.kind === "read") return "allow";
899
919
  if (this.mode === "allowAll") return "allow";
@@ -988,6 +1008,9 @@ function parseCliArgs(argv) {
988
1008
  continue: { type: "boolean", short: "c" },
989
1009
  resume: { type: "boolean", short: "r" },
990
1010
  "allow-all": { type: "boolean" },
1011
+ command: { type: "string" },
1012
+ args: { type: "string" },
1013
+ "fail-on-tool-error": { type: "boolean" },
991
1014
  "allowed-tools": { type: "string" },
992
1015
  worktree: { type: "string", short: "w" },
993
1016
  "output-format": { type: "string" },
@@ -1061,6 +1084,7 @@ function parseCliArgs(argv) {
1061
1084
  continue: !!values.continue,
1062
1085
  resume: !!values.resume,
1063
1086
  allowAll: !!values["allow-all"],
1087
+ failOnToolError: !!values["fail-on-tool-error"],
1064
1088
  allowedTools: (values["allowed-tools"] ?? "").split(",").map((s) => s.trim()).filter(Boolean),
1065
1089
  worktree: values.worktree,
1066
1090
  outputFormat,
@@ -1072,6 +1096,8 @@ function parseCliArgs(argv) {
1072
1096
  logout: !!values.logout,
1073
1097
  subArgs: ["models", "plugins", "gh-bot", "provider", "mcp", "connect", "disconnect", "integrations", "completion", "stats", "export", "import", "pr"].includes(command) ? rest : [],
1074
1098
  url: values.url,
1099
+ stdioCommand: values.command,
1100
+ stdioArgs: values.args,
1075
1101
  name: values.name,
1076
1102
  keyEnv: values["key-env"],
1077
1103
  values: values.value ?? [],
@@ -1160,6 +1186,17 @@ function outputReasoningSummary(items) {
1160
1186
  }).map((part) => part && typeof part === "object" && "text" in part && typeof part.text === "string" ? part.text : "").filter(Boolean).join("\n");
1161
1187
  }
1162
1188
  var retryable = (status) => status === 429 || status >= 500;
1189
+ function httpIdleMs() {
1190
+ const raw = Number(process.env.BCCLI_HTTP_IDLE_TIMEOUT_MS);
1191
+ return Number.isFinite(raw) && raw > 0 ? raw : 6e4;
1192
+ }
1193
+ function statusHint(status) {
1194
+ if (status === 401 || status === 403) return t("The API key was rejected. Check the key for this provider (bccli login) or its API key environment variable.");
1195
+ if (status === 404) return t("Model or endpoint not found. Check the model name and the provider base URL.");
1196
+ if (status === 429) return t("Rate limit reached. Wait a moment, or switch model with /model.");
1197
+ if (status >= 500) return t("The provider is having trouble. Try again, or switch model with /model.");
1198
+ return void 0;
1199
+ }
1163
1200
  function detectRepetition(text) {
1164
1201
  const tail = text.slice(-600);
1165
1202
  for (let len = 1; len <= 12; len++) {
@@ -1202,7 +1239,7 @@ var toUsage = (u) => u ? {
1202
1239
  ...u.prompt_tokens_details?.cached_tokens != null ? { cachedInputTokens: u.prompt_tokens_details.cached_tokens } : {},
1203
1240
  ...u.prompt_tokens_details?.cache_write_tokens != null ? { cacheWriteTokens: u.prompt_tokens_details.cache_write_tokens } : {}
1204
1241
  } : void 0;
1205
- async function readStream(res, onText, onThinking) {
1242
+ async function readStream(res, onText, onThinking, signal) {
1206
1243
  let text = "";
1207
1244
  let thinking = "";
1208
1245
  const splitter = new ThinkSplitter();
@@ -1219,6 +1256,7 @@ async function readStream(res, onText, onThinking) {
1219
1256
  };
1220
1257
  let usage;
1221
1258
  let finishReason;
1259
+ let sawDone = false;
1222
1260
  const calls = [];
1223
1261
  const byIndex = /* @__PURE__ */ new Map();
1224
1262
  let current2;
@@ -1228,7 +1266,11 @@ async function readStream(res, onText, onThinking) {
1228
1266
  const handle = (line) => {
1229
1267
  if (!line.startsWith("data:")) return;
1230
1268
  const data = line.slice(5).trim();
1231
- if (!data || data === "[DONE]") return;
1269
+ if (!data) return;
1270
+ if (data === "[DONE]") {
1271
+ sawDone = true;
1272
+ return;
1273
+ }
1232
1274
  let event;
1233
1275
  try {
1234
1276
  event = JSON.parse(data);
@@ -1257,18 +1299,45 @@ async function readStream(res, onText, onThinking) {
1257
1299
  if (tc.function?.arguments) target.arguments += tc.function.arguments;
1258
1300
  }
1259
1301
  };
1260
- for await (const chunk of res.body) {
1261
- buffer += decoder.decode(chunk, { stream: true });
1262
- let newline = buffer.indexOf("\n");
1263
- while (newline >= 0 && loopAt < 0) {
1264
- handle(buffer.slice(0, newline).trim());
1265
- buffer = buffer.slice(newline + 1);
1266
- newline = buffer.indexOf("\n");
1302
+ const iterator = res.body[Symbol.asyncIterator]();
1303
+ const idle = httpIdleMs();
1304
+ const onAbort = () => void iterator.return?.();
1305
+ if (signal?.aborted) onAbort();
1306
+ else signal?.addEventListener("abort", onAbort);
1307
+ try {
1308
+ for (; ; ) {
1309
+ let timer;
1310
+ const stalled = new Promise((_, reject) => {
1311
+ timer = setTimeout(
1312
+ () => reject(new ProviderError(t("The provider stopped sending data for {seconds}s. The answer above may be incomplete; try again.", { seconds: String(Math.round(idle / 1e3)) }))),
1313
+ idle
1314
+ );
1315
+ });
1316
+ let step;
1317
+ try {
1318
+ step = await Promise.race([iterator.next(), stalled]);
1319
+ } finally {
1320
+ if (timer) clearTimeout(timer);
1321
+ }
1322
+ if (step.done) break;
1323
+ buffer += decoder.decode(step.value, { stream: true });
1324
+ let newline = buffer.indexOf("\n");
1325
+ while (newline >= 0 && loopAt < 0) {
1326
+ handle(buffer.slice(0, newline).trim());
1327
+ buffer = buffer.slice(newline + 1);
1328
+ newline = buffer.indexOf("\n");
1329
+ }
1330
+ if (loopAt >= 0) return { text: text.slice(0, loopAt), toolCalls: [], thinking: thinking || void 0, usage, finishReason: "repetition" };
1267
1331
  }
1268
- if (loopAt >= 0) return { text: text.slice(0, loopAt), toolCalls: [], thinking: thinking || void 0, usage, finishReason: "repetition" };
1332
+ } finally {
1333
+ signal?.removeEventListener("abort", onAbort);
1334
+ void iterator.return?.();
1269
1335
  }
1270
1336
  handle(buffer.trim());
1271
1337
  emit(splitter.flush());
1338
+ if (!sawDone && !finishReason) {
1339
+ throw new ProviderError(t("The response ended before it finished (no finish_reason or [DONE]). The answer above is incomplete; try again."));
1340
+ }
1272
1341
  const toolCalls = calls.map((call, index) => ({ ...call, id: call.id || `call_${index}` }));
1273
1342
  return { text, toolCalls, thinking: thinking || void 0, usage, finishReason };
1274
1343
  }
@@ -1479,7 +1548,7 @@ function hasPendingProgrammaticReplay(messages) {
1479
1548
  }
1480
1549
  return pending;
1481
1550
  }
1482
- async function readResponsesStream(res, onText, onThinking) {
1551
+ async function readResponsesStream(res, onText, onThinking, signal) {
1483
1552
  let text = "";
1484
1553
  let thinking = "";
1485
1554
  let usage;
@@ -1581,17 +1650,28 @@ async function readResponsesStream(res, onText, onThinking) {
1581
1650
  );
1582
1651
  }
1583
1652
  };
1584
- for await (const chunk of res.body) {
1585
- buffer += decoder.decode(chunk, { stream: true });
1586
- let newline = buffer.indexOf("\n");
1587
- while (newline >= 0 && loopAt < 0) {
1588
- handle(buffer.slice(0, newline).trim());
1589
- buffer = buffer.slice(newline + 1);
1590
- newline = buffer.indexOf("\n");
1591
- }
1592
- if (loopAt >= 0) {
1593
- return { text: text.slice(0, loopAt), toolCalls: [], usage, finishReason: "repetition" };
1653
+ const iterator = res.body[Symbol.asyncIterator]();
1654
+ const onAbort = () => void iterator.return?.();
1655
+ if (signal?.aborted) onAbort();
1656
+ else signal?.addEventListener("abort", onAbort);
1657
+ try {
1658
+ for (; ; ) {
1659
+ const step = await iterator.next();
1660
+ if (step.done) break;
1661
+ buffer += decoder.decode(step.value, { stream: true });
1662
+ let newline = buffer.indexOf("\n");
1663
+ while (newline >= 0 && loopAt < 0) {
1664
+ handle(buffer.slice(0, newline).trim());
1665
+ buffer = buffer.slice(newline + 1);
1666
+ newline = buffer.indexOf("\n");
1667
+ }
1668
+ if (loopAt >= 0) {
1669
+ return { text: text.slice(0, loopAt), toolCalls: [], usage, finishReason: "repetition" };
1670
+ }
1594
1671
  }
1672
+ } finally {
1673
+ signal?.removeEventListener("abort", onAbort);
1674
+ void iterator.return?.();
1595
1675
  }
1596
1676
  handle(buffer.trim());
1597
1677
  return {
@@ -1964,12 +2044,26 @@ function createProvider(options) {
1964
2044
  await sleep(delay, signal);
1965
2045
  }
1966
2046
  let res;
2047
+ const idleAbort = new AbortController();
2048
+ const forwardAbort = () => idleAbort.abort(signal?.reason);
2049
+ if (signal?.aborted) forwardAbort();
2050
+ else signal?.addEventListener("abort", forwardAbort);
2051
+ const idle = httpIdleMs();
2052
+ const headerTimer = setTimeout(() => idleAbort.abort(new Error("idle-timeout")), idle);
1967
2053
  try {
1968
- res = await doFetch(`${baseURL}${path}`, { method: "POST", headers, body: JSON.stringify(body), signal });
2054
+ res = await doFetch(`${baseURL}${path}`, { method: "POST", headers, body: JSON.stringify(body), signal: idleAbort.signal });
1969
2055
  } catch (error) {
1970
2056
  if (signal?.aborted) throw error;
2057
+ if (idleAbort.signal.aborted) {
2058
+ throw new ProviderError(
2059
+ t("The provider did not respond within {seconds}s. Check the connection, then try again.", { seconds: String(Math.round(idle / 1e3)) })
2060
+ );
2061
+ }
1971
2062
  lastError = new ProviderError(t("Cannot connect to {url}: {error}", { url: baseURL, error: error.message }));
1972
2063
  continue;
2064
+ } finally {
2065
+ clearTimeout(headerTimer);
2066
+ signal?.removeEventListener("abort", forwardAbort);
1973
2067
  }
1974
2068
  if (res.ok) return res;
1975
2069
  const retryHeader = res.headers.get("retry-after");
@@ -1978,7 +2072,9 @@ function createProvider(options) {
1978
2072
  if (Number.isFinite(secs) && secs > 0 && secs <= 30) retryAfterWaitMs = Math.ceil(secs * 1e3);
1979
2073
  }
1980
2074
  const text = await res.text().catch(() => "");
1981
- lastError = new ProviderError(t("{status} from {url}: {message}", { status: res.status, url: baseURL, message: errorMessage(text) }), res.status);
2075
+ const raw = t("{status} from {url}: {message}", { status: res.status, url: baseURL, message: errorMessage(text) });
2076
+ const hint = statusHint(res.status);
2077
+ lastError = new ProviderError(hint ? `${hint} (${raw})` : raw, res.status);
1982
2078
  const deterministicQuota429 = res.status === 429 && /"code"\s*:\s*"(?:luna_monthly_quota_exhausted|plan_allowance_exhausted)"/i.test(text);
1983
2079
  if (!retryable(res.status) || deterministicQuota429) throw lastError;
1984
2080
  }
@@ -2019,7 +2115,7 @@ function createProvider(options) {
2019
2115
  );
2020
2116
  if ((res2.headers.get("content-type") ?? "").includes("application/json")) return readResponsesJson(res2, onThinking);
2021
2117
  if (!res2.body) throw new ProviderError(t("Empty response from the provider"));
2022
- return readResponsesStream(res2, onText, onThinking);
2118
+ return readResponsesStream(res2, onText, onThinking, signal);
2023
2119
  }
2024
2120
  const res = await post(
2025
2121
  "/chat/completions",
@@ -2028,7 +2124,7 @@ function createProvider(options) {
2028
2124
  );
2029
2125
  if ((res.headers.get("content-type") ?? "").includes("application/json")) return readJson(res, onText, onThinking);
2030
2126
  if (!res.body) throw new ProviderError(t("Empty response from the provider"));
2031
- return readStream(res, onText, onThinking);
2127
+ return readStream(res, onText, onThinking, signal);
2032
2128
  },
2033
2129
  async listModels() {
2034
2130
  const res = await doFetch(`${baseURL}/models`, { headers });
@@ -2301,60 +2397,39 @@ function darwinProfile(cwd, network) {
2301
2397
  network ? "(allow network*)" : "(deny network*)"
2302
2398
  ].join("\n");
2303
2399
  }
2400
+ function bwrapArgv(cwd, network, command) {
2401
+ return [
2402
+ "bwrap",
2403
+ "--ro-bind",
2404
+ "/",
2405
+ "/",
2406
+ "--dev-bind",
2407
+ "/dev",
2408
+ "/dev",
2409
+ "--proc",
2410
+ "/proc",
2411
+ "--bind",
2412
+ cwd,
2413
+ cwd,
2414
+ "--tmpfs",
2415
+ "/tmp",
2416
+ "--die-with-parent",
2417
+ ...network ? [] : ["--unshare-net"],
2418
+ "--",
2419
+ "/bin/bash",
2420
+ "-c",
2421
+ command
2422
+ ];
2423
+ }
2304
2424
  function sandboxArgv(command, opts) {
2305
2425
  if (opts.platform === "darwin") {
2306
2426
  return ["sandbox-exec", "-p", darwinProfile(opts.cwd, opts.network), "/bin/bash", "-c", command];
2307
2427
  }
2308
2428
  if (opts.platform === "linux") {
2309
- return [
2310
- "bwrap",
2311
- "--ro-bind",
2312
- "/",
2313
- "/",
2314
- "--dev-bind",
2315
- "/dev",
2316
- "/dev",
2317
- "--proc",
2318
- "/proc",
2319
- "--bind",
2320
- opts.cwd,
2321
- opts.cwd,
2322
- "--tmpfs",
2323
- "/tmp",
2324
- "--die-with-parent",
2325
- ...opts.network ? [] : ["--unshare-net"],
2326
- "--",
2327
- "/bin/bash",
2328
- "-c",
2329
- command
2330
- ];
2429
+ return bwrapArgv(opts.cwd, opts.network, command);
2331
2430
  }
2332
2431
  if (opts.platform === "win32" && (opts.wslBwrap || process.env.BCCLI_WIN32_WSL_SANDBOX === "1")) {
2333
- const wslCwd = winPathToWsl(opts.cwd);
2334
- return [
2335
- "wsl.exe",
2336
- "--",
2337
- "bwrap",
2338
- "--ro-bind",
2339
- "/",
2340
- "/",
2341
- "--dev-bind",
2342
- "/dev",
2343
- "/dev",
2344
- "--proc",
2345
- "/proc",
2346
- "--bind",
2347
- wslCwd,
2348
- wslCwd,
2349
- "--tmpfs",
2350
- "/tmp",
2351
- "--die-with-parent",
2352
- ...opts.network ? [] : ["--unshare-net"],
2353
- "--",
2354
- "/bin/bash",
2355
- "-c",
2356
- command
2357
- ];
2432
+ return ["wsl.exe", "--", ...bwrapArgv(winPathToWsl(opts.cwd), opts.network, command)];
2358
2433
  }
2359
2434
  return void 0;
2360
2435
  }
@@ -2394,7 +2469,7 @@ function killAllCommands() {
2394
2469
  }
2395
2470
  var backgrounds = /* @__PURE__ */ new Map();
2396
2471
  var nextBackgroundId = 1;
2397
- function startBackground(command, cwd, env, shell) {
2472
+ function startBackground(command, cwd, env, shell, maxRetained) {
2398
2473
  const child = spawnChild(command, cwd, env, shell);
2399
2474
  const task = {
2400
2475
  id: nextBackgroundId++,
@@ -2415,6 +2490,10 @@ function startBackground(command, cwd, env, shell) {
2415
2490
  if (child.pid !== void 0) killProcessTree(child.pid);
2416
2491
  };
2417
2492
  backgrounds.set(task.id, task);
2493
+ for (const [id, other] of backgrounds) {
2494
+ if (backgrounds.size <= maxRetained) break;
2495
+ if (id !== task.id && other.status === "exited") backgrounds.delete(id);
2496
+ }
2418
2497
  const onData = (data) => {
2419
2498
  let s = data;
2420
2499
  if (task.head.length < HALF) {
@@ -2613,6 +2692,7 @@ var SHELL_NOTE = {
2613
2692
  function createBashTool(options = {}) {
2614
2693
  const offline = options.networkPolicy === "offline";
2615
2694
  const sandbox = options.sandbox === true;
2695
+ const maxRetained = options.maxRetainedTasks ?? 50;
2616
2696
  const platform = options.platform ?? process.platform;
2617
2697
  const shell = resolveShell({ platform, env: process.env, exists: existsSync3, ...options.shellDeps });
2618
2698
  const childEnv = offline ? offlineEnv() : void 0;
@@ -2659,7 +2739,7 @@ Network access is off (networkPolicy "offline"): internet commands such as curl,
2659
2739
  }
2660
2740
  if (input.background) {
2661
2741
  if (!input.command) return { output: 'The "command" argument is required to start a background task.', isError: true };
2662
- const task = startBackground(exec, ctx.cwd, childEnv, shell);
2742
+ const task = startBackground(exec, ctx.cwd, childEnv, shell, maxRetained);
2663
2743
  return {
2664
2744
  output: `Background task #${task.id} started: ${input.command}
2665
2745
  Poll it with {task_id: ${task.id}}; stop it with {task_id: ${task.id}, kill: true}.`
@@ -3529,6 +3609,12 @@ async function readTextResult(input, buffer) {
3529
3609
  const lines = buffer.toString("utf8").split(/\r?\n/);
3530
3610
  const start = (input.offset ?? 1) - 1;
3531
3611
  const limit = input.limit ?? 2e3;
3612
+ if (start >= lines.length) {
3613
+ return {
3614
+ output: `offset ${input.offset} is past the end of ${input.path} (${lines.length} line(s)); start at 1.`,
3615
+ isError: true
3616
+ };
3617
+ }
3532
3618
  const requested = lines.slice(start, start + limit);
3533
3619
  const shown = [];
3534
3620
  let size = 0;
@@ -3610,7 +3696,9 @@ var writeTool = defineTool({
3610
3696
  target: (input) => input.path,
3611
3697
  async validate(input, ctx) {
3612
3698
  const abs = resolvePath(ctx.cwd, input.path);
3613
- return existsSync4(abs) && !ctx.readFiles.has(abs) ? `${input.path} already exists. Read it with read before overwriting it.` : void 0;
3699
+ if (existsSync4(abs) && !ctx.readFiles.has(abs)) return `${input.path} already exists. Read it with read before overwriting it.`;
3700
+ if (isFileModifiedSinceRead(ctx.readFiles, abs)) return `${input.path} has been modified since it was last read. Read it again before overwriting it.`;
3701
+ return void 0;
3614
3702
  },
3615
3703
  async preview(input, ctx) {
3616
3704
  const abs = resolvePath(ctx.cwd, input.path);
@@ -3622,6 +3710,9 @@ var writeTool = defineTool({
3622
3710
  if (existsSync4(abs) && !ctx.readFiles.has(abs)) {
3623
3711
  return { output: `${input.path} already exists. Read it with read before overwriting it.`, isError: true };
3624
3712
  }
3713
+ if (isFileModifiedSinceRead(ctx.readFiles, abs)) {
3714
+ return { output: `${input.path} has been modified since it was last read. Read it again before overwriting it.`, isError: true };
3715
+ }
3625
3716
  try {
3626
3717
  await ctx.checkpoint?.(abs);
3627
3718
  await mkdir(dirname4(abs), { recursive: true });
@@ -3731,7 +3822,9 @@ function budgetStatus(usage, cap, model) {
3731
3822
  if (cap.usd !== void 0) {
3732
3823
  const price = priceFor(cap, model);
3733
3824
  if (price) {
3734
- const usd = (usage.inputTokens * price.input + usage.outputTokens * price.output) / 1e6;
3825
+ const cached = Math.max(0, Math.min(usage.cachedInputTokens ?? 0, usage.inputTokens));
3826
+ const cachedRate = price.cachedInput ?? price.input;
3827
+ const usd = ((usage.inputTokens - cached) * price.input + cached * cachedRate + usage.outputTokens * price.output) / 1e6;
3735
3828
  if (usd >= cap.usd) return { kind: "usd", used: usd, limit: cap.usd };
3736
3829
  }
3737
3830
  }
@@ -4921,7 +5014,7 @@ async function addProviderKey(providerId, key, deps) {
4921
5014
  // src/session.ts
4922
5015
  import { randomBytes as randomBytes2 } from "crypto";
4923
5016
  import { appendFileSync, existsSync as existsSync10, mkdirSync as mkdirSync4, readdirSync as readdirSync3, readFileSync as readFileSync7, rmSync as rmSync2, statSync as statSync7 } from "fs";
4924
- import { basename as basename3, join as join10 } from "path";
5017
+ import { basename as basename3, dirname as dirname8, join as join10 } from "path";
4925
5018
  function projectDir(home, cwd) {
4926
5019
  const slug = cwd.replace(/[^a-zA-Z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(-100) || "root";
4927
5020
  return join10(home, "sessions", slug);
@@ -4947,7 +5040,6 @@ var Session = class _Session {
4947
5040
  }
4948
5041
  static create(home, cwd, now = /* @__PURE__ */ new Date()) {
4949
5042
  const dir = projectDir(home, cwd);
4950
- mkdirSync4(dir, { recursive: true });
4951
5043
  const stamp = now.toISOString().replace(/[:.]/g, "-");
4952
5044
  return new _Session(join10(dir, `${stamp}-${randomBytes2(3).toString("hex")}.jsonl`));
4953
5045
  }
@@ -4964,6 +5056,7 @@ var Session = class _Session {
4964
5056
  return _Session.list(home, cwd)[0]?.session;
4965
5057
  }
4966
5058
  write(record) {
5059
+ mkdirSync4(dirname8(this.file), { recursive: true });
4967
5060
  appendFileSync(this.file, `${JSON.stringify(record)}
4968
5061
  `);
4969
5062
  }
@@ -5996,6 +6089,15 @@ function loadPlugins(specs, opts) {
5996
6089
 
5997
6090
  // src/setup.ts
5998
6091
  var TOOL_RULES = { bash: "bash", edit: "edit", write: "edit", fetch: "fetch" };
6092
+ var pluginCache = /* @__PURE__ */ new Map();
6093
+ function loadPluginsOnce(specs, opts) {
6094
+ const key = JSON.stringify({ specs, home: opts.home, cwd: opts.cwd });
6095
+ const cached = pluginCache.get(key);
6096
+ if (cached) return cached;
6097
+ const loaded = loadPlugins(specs, opts);
6098
+ pluginCache.set(key, loaded);
6099
+ return loaded;
6100
+ }
5999
6101
  function lunaPtcAutoEnabled(env) {
6000
6102
  const value = String(env.BCCLI_LUNA_PTC ?? "").trim().toLowerCase();
6001
6103
  return !["0", "false", "off", "no"].includes(value);
@@ -6003,6 +6105,7 @@ function lunaPtcAutoEnabled(env) {
6003
6105
  function createRuntime(opts) {
6004
6106
  const env = opts.env ?? process.env;
6005
6107
  const home = bccliHome(env);
6108
+ const persistSession = opts.persistSession !== false;
6006
6109
  let config = loadConfig(opts.cwd, env);
6007
6110
  let modelRef = opts.args.model ?? config.model;
6008
6111
  let reasoning = opts.args.reasoning ?? config.reasoning;
@@ -6091,7 +6194,7 @@ function createRuntime(opts) {
6091
6194
  createRepoMapTool()
6092
6195
  ];
6093
6196
  if (config.lsp?.servers.length) baseTools.push(createDiagnosticsTool(config.lsp.servers));
6094
- const plugins = loadPlugins(config.plugins ?? [], {
6197
+ const plugins = loadPluginsOnce(config.plugins ?? [], {
6095
6198
  home,
6096
6199
  cwd: opts.cwd,
6097
6200
  env,
@@ -6129,9 +6232,15 @@ function createRuntime(opts) {
6129
6232
  usageCap: config.usageCap,
6130
6233
  budgetModel: modelRef,
6131
6234
  contextWindow: contextWindowFor(modelRef, env),
6132
- onMessage: (m) => session.append(m),
6133
- onUsage: (u) => session.appendUsage({ ...u, model: modelRef }),
6134
- onReset: () => session.reset(),
6235
+ onMessage: (m) => {
6236
+ if (persistSession) session.append(m);
6237
+ },
6238
+ onUsage: (u) => {
6239
+ if (persistSession) session.appendUsage({ ...u, model: modelRef });
6240
+ },
6241
+ onReset: () => {
6242
+ if (persistSession) session.reset();
6243
+ },
6135
6244
  onTurnStart: () => {
6136
6245
  for (let i = turnMarks.length - 1; i >= 0 && turnMarks[i] > agent.messages.length; i--) {
6137
6246
  turnMarks.splice(i, 1);
@@ -3,15 +3,15 @@ import {
3
3
  createFetchTool,
4
4
  createWebSearchTool,
5
5
  isPrivateHost
6
- } from "./chunk-WSSJH765.js";
6
+ } from "./chunk-6KZIOICR.js";
7
7
  import "./chunk-PSO7R5AD.js";
8
8
  import {
9
9
  loadConfig,
10
10
  readCredentials
11
- } from "./chunk-DRJSANKH.js";
11
+ } from "./chunk-C2FUGQJX.js";
12
12
  import {
13
13
  t
14
- } from "./chunk-XCVYR5BK.js";
14
+ } from "./chunk-G5TG43R4.js";
15
15
 
16
16
  // src/mcpServer/serve.ts
17
17
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
@@ -1,14 +1,14 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  createRuntime
4
- } from "./chunk-KREGVQ63.js";
4
+ } from "./chunk-6BXW7DGQ.js";
5
5
  import "./chunk-OK2USAXX.js";
6
- import "./chunk-CSIQDFEF.js";
7
- import "./chunk-WSSJH765.js";
6
+ import "./chunk-OAXD3X4R.js";
7
+ import "./chunk-6KZIOICR.js";
8
8
  import "./chunk-PSO7R5AD.js";
9
9
  import {
10
10
  VERSION
11
- } from "./chunk-HCW67S5M.js";
11
+ } from "./chunk-N6APAYSF.js";
12
12
  import {
13
13
  LOOPBACK_BIND,
14
14
  MAX_FRAME_BYTES,
@@ -17,25 +17,25 @@ import {
17
17
  parseClientFrame,
18
18
  randomToken,
19
19
  trustFence
20
- } from "./chunk-Z2ELYJ5O.js";
21
- import "./chunk-WFZKJY3R.js";
22
- import "./chunk-AWBEZY4D.js";
20
+ } from "./chunk-WSZF2CTU.js";
21
+ import "./chunk-YYTFJUO2.js";
22
+ import "./chunk-JFYQCPXL.js";
23
23
  import {
24
24
  listAllModels
25
- } from "./chunk-HPYI7L4W.js";
26
- import "./chunk-62UTSCWF.js";
27
- import "./chunk-RRECJTOG.js";
25
+ } from "./chunk-Y6AZGGH7.js";
26
+ import "./chunk-X24APPXO.js";
27
+ import "./chunk-IHQGXDSG.js";
28
28
  import {
29
29
  Session
30
- } from "./chunk-OETLWNGS.js";
31
- import "./chunk-EOJNFGOP.js";
32
- import "./chunk-EAR2C6TS.js";
33
- import "./chunk-2YG6L3YO.js";
30
+ } from "./chunk-F54FDJBD.js";
31
+ import "./chunk-OOOS5RCT.js";
32
+ import "./chunk-XZ2TI5HV.js";
33
+ import "./chunk-2DFY43RP.js";
34
34
  import {
35
35
  bccliHome,
36
36
  loadConfig
37
- } from "./chunk-DRJSANKH.js";
38
- import "./chunk-XCVYR5BK.js";
37
+ } from "./chunk-C2FUGQJX.js";
38
+ import "./chunk-G5TG43R4.js";
39
39
 
40
40
  // src/serve.ts
41
41
  import { randomBytes } from "crypto";