@botconnector/bccli 0.4.1 → 0.4.3

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 (71) hide show
  1. package/dist/{acp-3PHBAI64.js → acp-NU4HZHQO.js} +30 -6
  2. package/dist/acp.d.ts +1 -0
  3. package/dist/agent.d.ts +5 -1
  4. package/dist/args-NRTZWWEV.js +13 -0
  5. package/dist/args.d.ts +2 -0
  6. package/dist/checkpoints.d.ts +1 -0
  7. package/dist/{chunk-MFHCQQEN.js → chunk-3I5BW3ZT.js} +3 -2
  8. package/dist/{chunk-UZWJQF67.js → chunk-3WHI4LN3.js} +2 -1
  9. package/dist/{chunk-BGXNR74O.js → chunk-4W5SKQDC.js} +152 -31
  10. package/dist/{chunk-BC7UXFE4.js → chunk-6AUROP2E.js} +64 -3
  11. package/dist/{chunk-24JN75FZ.js → chunk-6M55UPES.js} +2 -2
  12. package/dist/{chunk-XV7ZVL4J.js → chunk-6UO5GX55.js} +26 -12
  13. package/dist/{chunk-75BECX4F.js → chunk-7I2ZGPUS.js} +4 -4
  14. package/dist/{chunk-BKGFA2PT.js → chunk-FUJGHCWN.js} +1 -1
  15. package/dist/{chunk-GN4W3T6E.js → chunk-GWVLPN4B.js} +4 -4
  16. package/dist/{chunk-JHPGOAXU.js → chunk-HQW4BROS.js} +4 -5
  17. package/dist/{chunk-45SDLMTV.js → chunk-JXM6REER.js} +1 -1
  18. package/dist/{chunk-BLOYFIAD.js → chunk-KPTML72Q.js} +1 -1
  19. package/dist/{chunk-WI22M2ID.js → chunk-NQOGVP3I.js} +2 -2
  20. package/dist/{chunk-QZKLZELZ.js → chunk-OK2USAXX.js} +28 -4
  21. package/dist/{chunk-PZRKU4UM.js → chunk-QNUJHKEL.js} +659 -162
  22. package/dist/{chunk-FLQPE4S5.js → chunk-RX4UQX43.js} +4 -1
  23. package/dist/{chunk-R5RE46CW.js → chunk-WDX62DFH.js} +40 -6
  24. package/dist/{chunk-MHIYLXJC.js → chunk-WFZKJY3R.js} +8 -4
  25. package/dist/{chunk-WNACAUDK.js → chunk-XTOATEI6.js} +66 -9
  26. package/dist/{chunk-BXQQCUAC.js → chunk-Z5NI5FIK.js} +7 -7
  27. package/dist/cli.js +60 -43
  28. package/dist/{completion-HPPFDAPB.js → completion-D2BSBTQO.js} +1 -1
  29. package/dist/{config-DIDLDONU.js → config-ENDRMOTP.js} +3 -3
  30. package/dist/{config-IU76CPK2.js → config-LAINORU5.js} +2 -2
  31. package/dist/config.d.ts +2 -0
  32. package/dist/{ghBotCli-E3VPHNZY.js → ghBotCli-GCIUUMKQ.js} +21 -20
  33. package/dist/{integrationCli-Q2YPDTPG.js → integrationCli-HXCSEWH2.js} +2 -2
  34. package/dist/{login-FP3BB44A.js → login-YXSSB2HQ.js} +4 -4
  35. package/dist/lsp.d.ts +26 -0
  36. package/dist/mcp/manager.d.ts +18 -0
  37. package/dist/{mcpCli-SNBDUWJN.js → mcpCli-SETTCL3K.js} +6 -6
  38. package/dist/{modelsCli-CZ2HNGKR.js → modelsCli-656AKYZD.js} +6 -6
  39. package/dist/{oauthLogin-6FWQABJ7.js → oauthLogin-XWY7OHCG.js} +1 -1
  40. package/dist/{pluginsCli-LPIUUDWB.js → pluginsCli-ZQNKWZHJ.js} +2 -2
  41. package/dist/{prCli-5IMV3VNQ.js → prCli-6PUWTMTP.js} +20 -4
  42. package/dist/prCli.d.ts +13 -0
  43. package/dist/print.d.ts +2 -0
  44. package/dist/provider.d.ts +5 -0
  45. package/dist/providerCli-V2576JXE.js +15 -0
  46. package/dist/sdk.d.ts +5 -1
  47. package/dist/sdk.js +1003 -230
  48. package/dist/{serve-KWOECK5W.js → serve-K5AH4MGO.js} +3 -3
  49. package/dist/{serve-X2ZV4S5U.js → serve-XPAGXIGS.js} +17 -17
  50. package/dist/session-V2VCAF7E.js +12 -0
  51. package/dist/session.d.ts +3 -0
  52. package/dist/setup-HVVHOIL4.js +24 -0
  53. package/dist/slash/info.d.ts +1 -0
  54. package/dist/{statsCli-CF3R7ATL.js → statsCli-XR6BEZQQ.js} +2 -2
  55. package/dist/tokenBudget.d.ts +1 -1
  56. package/dist/tools/diagnostics.d.ts +8 -0
  57. package/dist/tools/edit.d.ts +7 -2
  58. package/dist/tools/grep.d.ts +14 -2
  59. package/dist/tools/ignore.d.ts +2 -2
  60. package/dist/tools/paths.d.ts +2 -0
  61. package/dist/tools/sandbox.d.ts +3 -0
  62. package/dist/{transferCli-YUFPI6AH.js → transferCli-C6XS6QYH.js} +8 -6
  63. package/dist/ui/complete.d.ts +5 -0
  64. package/dist/ui/vim.d.ts +1 -1
  65. package/dist/{ui-IVIFNUCC.js → ui-43C6Y57K.js} +199 -54
  66. package/dist/{update-TZ2G6KQ3.js → update-AVZO4XID.js} +2 -2
  67. package/package.json +1 -1
  68. package/dist/args-W6BNKY7K.js +0 -13
  69. package/dist/providerCli-NMUFXAAS.js +0 -15
  70. package/dist/session-GKEXDJKZ.js +0 -12
  71. package/dist/setup-OV3NWQN5.js +0 -24
package/dist/sdk.js CHANGED
@@ -500,7 +500,8 @@ Opsi:
500
500
  "gh is not available ({error}); install GitHub CLI first": "gh tidak tersedia ({error}); pasang GitHub CLI dulu",
501
501
  "gh pr checkout {n} failed (exit {code})": "gh pr checkout {n} gagal (exit {code})",
502
502
  "checked out PR #{n}": "checkout PR #{n} selesai",
503
- "--allow-all: the PR task runs every tool without asking": "--allow-all: tugas PR menjalankan semua alat tanpa bertanya"
503
+ "--allow-all: the PR task runs every tool without asking": "--allow-all: tugas PR menjalankan semua alat tanpa bertanya",
504
+ "view or change configuration settings": "lihat atau ubah pengaturan konfigurasi"
504
505
  };
505
506
 
506
507
  // src/i18n.ts
@@ -689,8 +690,11 @@ function loadConfig(cwd, env = process.env) {
689
690
  const project = readJsonConfig(join(cwd, ".bccli", "config.json"));
690
691
  const known = { ...DEFAULT_CONFIG.providers, ...global.providers };
691
692
  const fromProject = untrustedProviders(project, known);
693
+ const rawFallbacks = project.fallbackModels ?? global.fallbackModels;
694
+ const fallbackModels = Array.isArray(rawFallbacks) ? rawFallbacks.filter((m) => typeof m === "string" && m.includes("/")) : void 0;
692
695
  return {
693
696
  model: project.model ?? global.model ?? DEFAULT_CONFIG.model,
697
+ ...fallbackModels?.length ? { fallbackModels } : {},
694
698
  permissionMode: global.permissionMode ?? DEFAULT_CONFIG.permissionMode,
695
699
  reasoning: global.reasoning ?? DEFAULT_CONFIG.reasoning,
696
700
  thinkingVisible: global.thinkingVisible === true,
@@ -774,7 +778,7 @@ function resolveModel(config, modelRef, env = process.env) {
774
778
  }
775
779
 
776
780
  // src/permissions.ts
777
- import { isAbsolute, relative, resolve, sep } from "path";
781
+ import { isAbsolute, relative, resolve } from "path";
778
782
  var MODE_ORDER = ["default", "acceptEdits", "plan", "allowAll"];
779
783
  var ALWAYS_ASK_COMMANDS = /* @__PURE__ */ new Set(["sudo", "sh", "bash", "zsh", "su"]);
780
784
  function isAlwaysAsk(segment) {
@@ -832,7 +836,9 @@ function commandKey(segment) {
832
836
  function requiresSensitiveToolApproval(req) {
833
837
  const target = req.target.trim();
834
838
  if (req.kind === "read" || req.kind === "edit") {
835
- return /(?:^|[/\\])(?:\.env(?:\..*)?|\.ssh|\.aws|\.config\/gcloud|secrets?|credentials?)(?:[/\\]|$)/i.test(target) || /(?:id_rsa|id_ed25519|\.pem|\.p12|\.key)$/i.test(target);
839
+ return /(?:^|[\s/\\])(?:\.env(?:\.[^\s/\\]+)?|\.ssh|\.aws|\.config[/\\]gcloud|\.npmrc|\.netrc|\.git-credentials|(?:secrets?|credentials?)(?:\.[a-z0-9_-]+)?)(?:[\s/\\]|$)/i.test(
840
+ target
841
+ ) || /(?:id_rsa|id_ed25519|\.pem|\.p12|\.key)(?:\s|$)/i.test(target);
836
842
  }
837
843
  if (req.kind === "bash") {
838
844
  return /\b(?:rm\s+-[a-z]*r[a-z]*f[a-z]*\s|rm\s+-[a-z]*f[a-z]*r[a-z]*\s|mkfs(?:\.|\s)|wipefs\b|shred\s|git\s+push\s+(?:--force|-f)\b|docker\s+(?:system\s+prune|volume\s+rm)|kubectl\s+delete\b|drop\s+(?:database|table)|npm\s+publish\b)/i.test(target) || /\b(?:deploy-service\.sh|aws\s+secretsmanager|cat\s+[^;&|]*\.env\b)/i.test(target);
@@ -857,7 +863,8 @@ var Permissions = class {
857
863
  editInProject(target) {
858
864
  const rel = relative(this.cwd, resolve(this.cwd, target));
859
865
  if (!rel || rel.startsWith("..") || isAbsolute(rel)) return false;
860
- return !rel.split(sep).includes(".git") && !rel.split("/").includes(".git");
866
+ const parts = rel.split(/[/\\]/).map((p) => p.toLowerCase());
867
+ return !parts.includes(".git");
861
868
  }
862
869
  /** Rules that "[a] allow for this session" would add; undefined when it must always be asked. */
863
870
  rulesFor(req) {
@@ -875,7 +882,7 @@ var Permissions = class {
875
882
  if (/`|\$\(|[<>]|^\s*[({]/.test(req.target)) return void 0;
876
883
  const segments = req.target.split(/&&|\|\||[;&|\n]/).map((s) => s.trim()).filter(Boolean);
877
884
  if (!segments.length) return void 0;
878
- if (segments.some(isAlwaysAsk)) return void 0;
885
+ if (segments.some((s) => /^[({]/.test(s) || isAlwaysAsk(s))) return void 0;
879
886
  return [...new Set(segments.map((s) => `bash(${commandKey(s)})`))];
880
887
  }
881
888
  if (req.kind === "mcp") return [`mcp(${req.tool})`];
@@ -953,9 +960,12 @@ var LOOPBACK_BIND = /* @__PURE__ */ new Set(["127.0.0.1", "localhost", "::1"]);
953
960
 
954
961
  // src/args.ts
955
962
  function parseCliArgs(argv) {
956
- const expanded = argv.map(
957
- (a, i) => a === "--models" && (i + 1 >= argv.length || argv[i + 1].startsWith("-")) ? "--models=all" : a
958
- );
963
+ const expanded = argv.map((a, i) => {
964
+ const nextIsFlagOrEnd = i + 1 >= argv.length || argv[i + 1].startsWith("-");
965
+ if (a === "--models" && nextIsFlagOrEnd) return "--models=all";
966
+ if (a === "--project" && nextIsFlagOrEnd) return "--project=";
967
+ return a;
968
+ });
959
969
  const { values, positionals } = parseArgs({
960
970
  args: expanded,
961
971
  allowPositionals: true,
@@ -987,7 +997,8 @@ function parseCliArgs(argv) {
987
997
  tools: { type: "string" },
988
998
  models: { type: "string" },
989
999
  project: { type: "string" },
990
- repo: { type: "string" }
1000
+ repo: { type: "string" },
1001
+ "fallback-model": { type: "string" }
991
1002
  }
992
1003
  });
993
1004
  const reasoning = values.reasoning;
@@ -1061,7 +1072,8 @@ function parseCliArgs(argv) {
1061
1072
  tools,
1062
1073
  models,
1063
1074
  project: values.project,
1064
- repo: values.repo
1075
+ repo: values.repo,
1076
+ fallbackModel: values["fallback-model"]
1065
1077
  };
1066
1078
  }
1067
1079
 
@@ -1153,14 +1165,14 @@ function detectRepetition(text) {
1153
1165
  return -1;
1154
1166
  }
1155
1167
  function sleep(ms, signal) {
1156
- return new Promise((resolve4, reject) => {
1168
+ return new Promise((resolve5, reject) => {
1157
1169
  const onAbort = () => {
1158
1170
  clearTimeout(timer);
1159
1171
  reject(signal?.reason);
1160
1172
  };
1161
1173
  const timer = setTimeout(() => {
1162
1174
  signal?.removeEventListener("abort", onAbort);
1163
- resolve4();
1175
+ resolve5();
1164
1176
  }, ms);
1165
1177
  signal?.addEventListener("abort", onAbort, { once: true });
1166
1178
  });
@@ -1230,7 +1242,7 @@ async function readStream(res, onText, onThinking) {
1230
1242
  }
1231
1243
  current2 = target;
1232
1244
  if (tc.id) target.id = tc.id;
1233
- if (tc.function?.name && !target.name) target.name = tc.function.name;
1245
+ if (tc.function?.name && target.name !== tc.function.name) target.name += tc.function.name;
1234
1246
  if (tc.function?.arguments) target.arguments += tc.function.arguments;
1235
1247
  }
1236
1248
  };
@@ -1437,7 +1449,16 @@ function responsesFinishReason(items, toolCallCount, incompleteReason) {
1437
1449
  function hasPendingProgrammaticReplay(messages) {
1438
1450
  let pending = false;
1439
1451
  for (const message of messages) {
1452
+ if (message.role === "user" && typeof message.content === "string") {
1453
+ pending = false;
1454
+ continue;
1455
+ }
1456
+ if (message.role === "tool" && message.content === "Cancelled by the user.") {
1457
+ pending = false;
1458
+ continue;
1459
+ }
1440
1460
  if (message.role !== "assistant") continue;
1461
+ if (message.content) pending = false;
1441
1462
  for (const item of message.responses_output_items ?? []) {
1442
1463
  if (item.type === "program") pending = true;
1443
1464
  else if (item.type === "message" && pending) pending = false;
@@ -1511,7 +1532,7 @@ async function readResponsesStream(res, onText, onThinking) {
1511
1532
  calls.set(index, {
1512
1533
  id: String(event.item.call_id || current2?.id || event.item.id || `call_${index}`),
1513
1534
  name: String(event.item.name || current2?.name || ""),
1514
- arguments: String(event.item.arguments ?? current2?.arguments ?? ""),
1535
+ arguments: String(event.item.arguments || current2?.arguments || ""),
1515
1536
  ...programCaller(event.item) ?? current2?.caller ? { caller: programCaller(event.item) ?? current2?.caller } : {}
1516
1537
  });
1517
1538
  }
@@ -1747,7 +1768,7 @@ async function readResponsesWebSocketTurn(events, ws, onText, onThinking, signal
1747
1768
  calls.set(index, {
1748
1769
  id: String(event.item.call_id || current2?.id || event.item.id || `call_${index}`),
1749
1770
  name: String(event.item.name || current2?.name || ""),
1750
- arguments: String(event.item.arguments ?? current2?.arguments ?? ""),
1771
+ arguments: String(event.item.arguments || current2?.arguments || ""),
1751
1772
  ...programCaller(event.item) ?? current2?.caller ? { caller: programCaller(event.item) ?? current2?.caller } : {}
1752
1773
  });
1753
1774
  }
@@ -1808,7 +1829,7 @@ function createProvider(options) {
1808
1829
  const isBotConnectorCloud = /^https:\/\/api\.botconnector\.id\/v1\/?$/.test(baseURL) && apiKey?.startsWith("bc_live_");
1809
1830
  if (isBotConnectorCloud) {
1810
1831
  headers["x-botconnector-client"] = "bccli";
1811
- headers["x-botconnector-client-version"] = "0.4.1";
1832
+ headers["x-botconnector-client-version"] = "0.4.3";
1812
1833
  }
1813
1834
  const useNativeResponses = isBotConnectorCloud && providerId === "bc-cloud" && model === "gpt-6-luna";
1814
1835
  const useResponsesWebSocket = useNativeResponses && Boolean(apiKey) && (fetchOverride === void 0 || responsesWebSocketFactory !== void 0);
@@ -1839,7 +1860,7 @@ function createProvider(options) {
1839
1860
  baseURL: baseURL.replace(/\/+$/, ""),
1840
1861
  defaultHeaders: {
1841
1862
  "x-botconnector-client": "bccli",
1842
- "x-botconnector-client-version": "0.4.1"
1863
+ "x-botconnector-client-version": "0.4.3"
1843
1864
  }
1844
1865
  });
1845
1866
  lunaWs = responsesWebSocketFactory ? responsesWebSocketFactory(client) : new ResponsesWS(client, { reconnect: null });
@@ -1922,8 +1943,13 @@ function createProvider(options) {
1922
1943
  }
1923
1944
  async function post(path, body, signal) {
1924
1945
  let lastError;
1946
+ let retryAfterWaitMs;
1925
1947
  for (let attempt = 0; attempt < 4; attempt++) {
1926
- if (attempt > 0) await sleep(retryDelayMs * 2 ** (attempt - 1), signal);
1948
+ if (attempt > 0) {
1949
+ const delay = retryAfterWaitMs ?? retryDelayMs * 2 ** (attempt - 1);
1950
+ retryAfterWaitMs = void 0;
1951
+ await sleep(delay, signal);
1952
+ }
1927
1953
  let res;
1928
1954
  try {
1929
1955
  res = await doFetch(`${baseURL}${path}`, { method: "POST", headers, body: JSON.stringify(body), signal });
@@ -1933,6 +1959,11 @@ function createProvider(options) {
1933
1959
  continue;
1934
1960
  }
1935
1961
  if (res.ok) return res;
1962
+ const retryHeader = res.headers.get("retry-after");
1963
+ if (retryHeader) {
1964
+ const secs = Number(retryHeader);
1965
+ if (Number.isFinite(secs) && secs > 0 && secs <= 30) retryAfterWaitMs = Math.ceil(secs * 1e3);
1966
+ }
1936
1967
  const text = await res.text().catch(() => "");
1937
1968
  lastError = new ProviderError(t("{status} from {url}: {message}", { status: res.status, url: baseURL, message: errorMessage(text) }), res.status);
1938
1969
  const deterministicQuota429 = res.status === 429 && /"code"\s*:\s*"(?:luna_monthly_quota_exhausted|plan_allowance_exhausted)"/i.test(text);
@@ -1995,6 +2026,43 @@ function createProvider(options) {
1995
2026
  }
1996
2027
  };
1997
2028
  }
2029
+ function withFallbackProvider(primary, fallbacks) {
2030
+ return {
2031
+ async chat(req) {
2032
+ let emitted = false;
2033
+ const wrappedReq = {
2034
+ ...req,
2035
+ onText: req.onText ? (delta) => {
2036
+ emitted = true;
2037
+ req.onText?.(delta);
2038
+ } : () => {
2039
+ emitted = true;
2040
+ }
2041
+ };
2042
+ try {
2043
+ return await primary.chat(wrappedReq);
2044
+ } catch (primaryError) {
2045
+ if (emitted || req.signal?.aborted) throw primaryError;
2046
+ const status = primaryError instanceof ProviderError ? primaryError.status : void 0;
2047
+ const canFallback = status === void 0 || status === 429 || status === 408 || status >= 500;
2048
+ if (!canFallback) throw primaryError;
2049
+ const chain = fallbacks();
2050
+ if (!chain.length) throw primaryError;
2051
+ let lastError = primaryError;
2052
+ for (const fb of chain) {
2053
+ if (req.signal?.aborted) throw lastError;
2054
+ try {
2055
+ return await fb.chat(req);
2056
+ } catch (fbError) {
2057
+ lastError = fbError;
2058
+ }
2059
+ }
2060
+ throw lastError;
2061
+ }
2062
+ },
2063
+ listModels: () => primary.listModels()
2064
+ };
2065
+ }
1998
2066
 
1999
2067
  // src/textToolCalls.ts
2000
2068
  var PAIR = /(?:<arg_key>\s*|`)?([A-Za-z_]\w*)(?:\s*<\/arg_key>)?`?\s*(?:=\s*`|<arg_value>)([\s\S]*?)<\/arg_value>/g;
@@ -2114,7 +2182,7 @@ function runOne(command, event, payload, opts) {
2114
2182
  ...payload.input !== void 0 ? { BCCLI_HOOK_INPUT: JSON.stringify(payload.input) } : {},
2115
2183
  ...payload.output !== void 0 ? { BCCLI_HOOK_OUTPUT: payload.output } : {}
2116
2184
  };
2117
- return new Promise((resolve4) => {
2185
+ return new Promise((resolve5) => {
2118
2186
  let stdout = "";
2119
2187
  let stderr = "";
2120
2188
  let timedOut = false;
@@ -2123,7 +2191,7 @@ function runOne(command, event, payload, opts) {
2123
2191
  if (settled) return;
2124
2192
  settled = true;
2125
2193
  clearTimeout(timer);
2126
- resolve4(result);
2194
+ resolve5(result);
2127
2195
  };
2128
2196
  let child;
2129
2197
  const shell = defaultShell();
@@ -2139,7 +2207,7 @@ function runOne(command, event, payload, opts) {
2139
2207
  child = spawn(command, { shell: true, env, cwd: opts.cwd ?? process.cwd(), stdio: ["ignore", "pipe", "pipe"] });
2140
2208
  }
2141
2209
  } catch (error) {
2142
- resolve4({ failure: error.message });
2210
+ resolve5({ failure: error.message });
2143
2211
  return;
2144
2212
  }
2145
2213
  const timer = setTimeout(() => {
@@ -2194,10 +2262,17 @@ import { z as z8 } from "zod";
2194
2262
 
2195
2263
  // src/tools/bash.ts
2196
2264
  import { spawn as spawn2 } from "child_process";
2197
- import { existsSync as existsSync3 } from "fs";
2265
+ import { existsSync as existsSync3, writeFileSync as writeFileSync2 } from "fs";
2266
+ import { tmpdir } from "os";
2267
+ import { join as join2 } from "path";
2198
2268
  import { z } from "zod";
2199
2269
 
2200
2270
  // src/tools/sandbox.ts
2271
+ function winPathToWsl(winPath) {
2272
+ const normalized = winPath.replace(/\\/g, "/");
2273
+ const m = /^([A-Za-z]):\/(.*)$/.exec(normalized);
2274
+ return m ? `/mnt/${m[1].toLowerCase()}/${m[2]}` : normalized;
2275
+ }
2201
2276
  function darwinProfile(cwd, network) {
2202
2277
  const writable = [cwd, "/tmp", "/private/tmp", "/dev", "/var/folders"].map((p) => `(subpath "${p}")`).join(" ");
2203
2278
  return [
@@ -2241,6 +2316,33 @@ function sandboxArgv(command, opts) {
2241
2316
  command
2242
2317
  ];
2243
2318
  }
2319
+ if (opts.platform === "win32" && (opts.wslBwrap || process.env.BCCLI_WIN32_WSL_SANDBOX === "1")) {
2320
+ const wslCwd = winPathToWsl(opts.cwd);
2321
+ return [
2322
+ "wsl.exe",
2323
+ "--",
2324
+ "bwrap",
2325
+ "--ro-bind",
2326
+ "/",
2327
+ "/",
2328
+ "--dev-bind",
2329
+ "/dev",
2330
+ "/dev",
2331
+ "--proc",
2332
+ "/proc",
2333
+ "--bind",
2334
+ wslCwd,
2335
+ wslCwd,
2336
+ "--tmpfs",
2337
+ "/tmp",
2338
+ "--die-with-parent",
2339
+ ...opts.network ? [] : ["--unshare-net"],
2340
+ "--",
2341
+ "/bin/bash",
2342
+ "-c",
2343
+ command
2344
+ ];
2345
+ }
2244
2346
  return void 0;
2245
2347
  }
2246
2348
 
@@ -2383,7 +2485,7 @@ function offlineEnv(env = process.env) {
2383
2485
  };
2384
2486
  }
2385
2487
  function runCommand(command, opts) {
2386
- return new Promise((resolve4) => {
2488
+ return new Promise((resolve5) => {
2387
2489
  const child = spawnChild(command, opts.cwd, opts.env, opts.shell ?? defaultShell());
2388
2490
  if (!exitHookInstalled) {
2389
2491
  process.on("exit", killAllCommands);
@@ -2396,7 +2498,14 @@ function runCommand(command, opts) {
2396
2498
  let dropped = 0;
2397
2499
  let timedOut = false;
2398
2500
  let aborted = false;
2501
+ const fullChunks = [];
2502
+ let fullBytes = 0;
2503
+ const MAX_SPILL_BYTES = 10 * 1024 * 1024;
2399
2504
  const onData = (data) => {
2505
+ if (fullBytes < MAX_SPILL_BYTES) {
2506
+ fullChunks.push(data);
2507
+ fullBytes += data.length;
2508
+ }
2400
2509
  let s = data;
2401
2510
  if (head.length < HALF) {
2402
2511
  const take = s.slice(0, HALF - head.length);
@@ -2435,10 +2544,19 @@ function runCommand(command, opts) {
2435
2544
  active.delete(killAsAborted);
2436
2545
  clearTimeout(timer);
2437
2546
  opts.signal.removeEventListener("abort", onAbort);
2547
+ let spillNote = "";
2548
+ if (dropped && fullChunks.length) {
2549
+ try {
2550
+ const spillPath = join2(tmpdir(), `bccli-bash-${Date.now()}-${Math.random().toString(36).slice(2, 8)}.log`);
2551
+ writeFileSync2(spillPath, fullChunks.join(""), "utf8");
2552
+ spillNote = `; full output saved to ${spillPath}`;
2553
+ } catch {
2554
+ }
2555
+ }
2438
2556
  const output = dropped ? `${head}
2439
- \u2026 [${dropped} characters truncated] \u2026
2557
+ \u2026 [${dropped} characters truncated${spillNote}] \u2026
2440
2558
  ${tail}` : head + tail;
2441
- resolve4({ output: output + extra, exitCode, timedOut, aborted });
2559
+ resolve5({ output: output + extra, exitCode, timedOut, aborted });
2442
2560
  };
2443
2561
  child.on("error", (error) => finish(null, error.message));
2444
2562
  child.on("close", (code) => finish(code));
@@ -2560,46 +2678,139 @@ function formatDiff(lines) {
2560
2678
  }
2561
2679
 
2562
2680
  // src/tools/paths.ts
2681
+ import { statSync } from "fs";
2563
2682
  import { isAbsolute as isAbsolute2, resolve as resolve2 } from "path";
2683
+ var readMtimes = /* @__PURE__ */ new WeakMap();
2564
2684
  function resolvePath(cwd, path) {
2565
2685
  return isAbsolute2(path) ? path : resolve2(cwd, path);
2566
2686
  }
2567
2687
  function errorCode(error) {
2568
2688
  return error.code ?? error.message;
2569
2689
  }
2690
+ function recordFileMtime(readFiles, abs) {
2691
+ readFiles.add(abs);
2692
+ try {
2693
+ let map = readMtimes.get(readFiles);
2694
+ if (!map) {
2695
+ map = /* @__PURE__ */ new Map();
2696
+ readMtimes.set(readFiles, map);
2697
+ }
2698
+ map.set(abs, statSync(abs).mtimeMs);
2699
+ } catch {
2700
+ }
2701
+ }
2702
+ function isFileModifiedSinceRead(readFiles, abs) {
2703
+ const recorded = readMtimes.get(readFiles)?.get(abs);
2704
+ if (recorded === void 0) return false;
2705
+ try {
2706
+ return statSync(abs).mtimeMs > recorded + 50;
2707
+ } catch {
2708
+ return false;
2709
+ }
2710
+ }
2570
2711
 
2571
2712
  // src/tools/edit.ts
2572
- var schema = z2.object({
2573
- path: z2.string().describe("File path, relative to the project or absolute"),
2713
+ var editEntrySchema = z2.object({
2574
2714
  old_string: z2.string().describe("Exact text to replace; must be unique unless replace_all"),
2575
2715
  new_string: z2.string().describe("Replacement text"),
2576
2716
  replace_all: z2.boolean().optional().describe("Replace every occurrence")
2577
2717
  });
2718
+ var schema = z2.object({
2719
+ path: z2.string().describe("File path, relative to the project or absolute"),
2720
+ old_string: z2.string().optional().describe("Exact text to replace; must be unique unless replace_all"),
2721
+ new_string: z2.string().optional().describe("Replacement text"),
2722
+ replace_all: z2.boolean().optional().describe("Replace every occurrence"),
2723
+ edits: z2.array(editEntrySchema).optional().describe("Optional list of sequential edits to apply atomically to the same file")
2724
+ });
2578
2725
  function countOccurrences(haystack, needle) {
2579
2726
  let count = 0;
2580
2727
  for (let i = haystack.indexOf(needle); i !== -1; i = haystack.indexOf(needle, i + needle.length)) count++;
2581
2728
  return count;
2582
2729
  }
2583
- function applyEdit(content, input) {
2730
+ function normalizeTypographic(s) {
2731
+ return s.replace(/[\u2018\u2019\u201A\u201B]/g, "'").replace(/[\u201C\u201D\u201E\u201F]/g, '"').replace(/[\u2013\u2014]/g, "-").replace(/\u00A0/g, " ");
2732
+ }
2733
+ function stripReadLinePrefixes(s) {
2734
+ const lines = s.split(/\r?\n/);
2735
+ if (lines.every((l) => !l.trim() || /^\s*\d+\t/.test(l))) {
2736
+ return lines.map((l) => l.replace(/^\s*\d+\t/, "")).join("\n");
2737
+ }
2738
+ return s;
2739
+ }
2740
+ function matchByNormalizedLines(content, oldText, newText, replaceAll) {
2741
+ const eol = content.includes("\r\n") ? "\r\n" : "\n";
2742
+ const fileLines = content.split(/\r?\n/);
2743
+ const oldLines = stripReadLinePrefixes(oldText).split(/\r?\n/);
2744
+ if (!oldLines.length) return { count: 0 };
2745
+ const normLine = (l) => normalizeTypographic(l).trimEnd();
2746
+ const normOld = oldLines.map(normLine);
2747
+ const matches = [];
2748
+ for (let i = 0; i + oldLines.length <= fileLines.length; i++) {
2749
+ let ok = true;
2750
+ for (let j = 0; j < oldLines.length; j++) {
2751
+ if (normLine(fileLines[i + j]) !== normOld[j]) {
2752
+ ok = false;
2753
+ break;
2754
+ }
2755
+ }
2756
+ if (ok) {
2757
+ matches.push(i);
2758
+ if (replaceAll) i += oldLines.length - 1;
2759
+ }
2760
+ }
2761
+ if (matches.length === 0) return { count: 0 };
2762
+ if (matches.length > 1 && !replaceAll) return { count: matches.length };
2763
+ const replacementLines = stripReadLinePrefixes(newText).split(/\r?\n/);
2764
+ const outLines = [...fileLines];
2765
+ for (let m = matches.length - 1; m >= 0; m--) {
2766
+ outLines.splice(matches[m], oldLines.length, ...replacementLines);
2767
+ }
2768
+ return { count: matches.length, result: outLines.join(eol) };
2769
+ }
2770
+ function applySingleEdit(content, path, input) {
2584
2771
  if (input.old_string === input.new_string) {
2585
2772
  return { output: "old_string and new_string are identical; nothing to change.", isError: true };
2586
2773
  }
2587
2774
  const eol = content.includes("\r\n") ? "\r\n" : "\n";
2588
2775
  const toEol = (s) => s.replace(/\r?\n/g, eol);
2776
+ const strippedOld = stripReadLinePrefixes(input.old_string);
2777
+ const strippedNew = stripReadLinePrefixes(input.new_string);
2589
2778
  const attempts = [[input.old_string, input.new_string]];
2590
2779
  if (toEol(input.old_string) !== input.old_string) attempts.push([toEol(input.old_string), toEol(input.new_string)]);
2780
+ if (strippedOld !== input.old_string) {
2781
+ attempts.push([strippedOld, strippedNew], [toEol(strippedOld), toEol(strippedNew)]);
2782
+ }
2591
2783
  for (const [oldText, newText] of attempts) {
2592
2784
  const count = oldText ? countOccurrences(content, oldText) : 0;
2593
2785
  if (count === 0) continue;
2594
2786
  if (count > 1 && !input.replace_all) {
2595
2787
  return {
2596
- output: `old_string appears ${count} times in ${input.path}. Add more context to make it unique, or use replace_all.`,
2788
+ output: `old_string appears ${count} times in ${path}. Add more context to make it unique, or use replace_all.`,
2597
2789
  isError: true
2598
2790
  };
2599
2791
  }
2600
2792
  return input.replace_all ? content.split(oldText).join(newText) : content.replace(oldText, () => newText);
2601
2793
  }
2602
- return { output: `old_string not found in ${input.path}. Re-read the file.`, isError: true };
2794
+ const fuzzy = matchByNormalizedLines(content, input.old_string, input.new_string, input.replace_all);
2795
+ if (fuzzy.count > 1 && !input.replace_all) {
2796
+ return {
2797
+ output: `old_string appears ${fuzzy.count} times in ${path}. Add more context to make it unique, or use replace_all.`,
2798
+ isError: true
2799
+ };
2800
+ }
2801
+ if (fuzzy.count > 0 && fuzzy.result !== void 0) return fuzzy.result;
2802
+ return { output: `old_string not found in ${path}. Re-read the file.`, isError: true };
2803
+ }
2804
+ function applyEdit(content, input) {
2805
+ const list2 = input.edits?.length ? input.edits : input.old_string !== void 0 && input.new_string !== void 0 ? [{ old_string: input.old_string, new_string: input.new_string, replace_all: input.replace_all }] : [];
2806
+ if (!list2.length) return { output: "Provide old_string and new_string (or edits array).", isError: true };
2807
+ let current2 = content;
2808
+ for (const item of list2) {
2809
+ const next = applySingleEdit(current2, input.path, item);
2810
+ if (typeof next !== "string") return next;
2811
+ current2 = next;
2812
+ }
2813
+ return current2;
2603
2814
  }
2604
2815
  var editTool = defineTool({
2605
2816
  name: "edit",
@@ -2610,6 +2821,9 @@ var editTool = defineTool({
2610
2821
  async validate(input, ctx) {
2611
2822
  const abs = resolvePath(ctx.cwd, input.path);
2612
2823
  if (!ctx.readFiles.has(abs)) return `Read ${input.path} with read before editing it.`;
2824
+ if (isFileModifiedSinceRead(ctx.readFiles, abs)) {
2825
+ return `${input.path} has been modified since it was last read. Read it again before editing.`;
2826
+ }
2613
2827
  let content;
2614
2828
  try {
2615
2829
  content = await readFile(abs, "utf8");
@@ -2630,6 +2844,9 @@ var editTool = defineTool({
2630
2844
  if (!ctx.readFiles.has(abs)) {
2631
2845
  return { output: `Read ${input.path} with read before editing it.`, isError: true };
2632
2846
  }
2847
+ if (isFileModifiedSinceRead(ctx.readFiles, abs)) {
2848
+ return { output: `${input.path} has been modified since it was last read. Read it again before editing.`, isError: true };
2849
+ }
2633
2850
  let content;
2634
2851
  try {
2635
2852
  content = await readFile(abs, "utf8");
@@ -2640,6 +2857,7 @@ var editTool = defineTool({
2640
2857
  if (typeof next !== "string") return next;
2641
2858
  await ctx.checkpoint?.(abs);
2642
2859
  await writeFile(abs, next);
2860
+ recordFileMtime(ctx.readFiles, abs);
2643
2861
  return { output: `Edited ${input.path}.`, display: formatDiff(diffLines(content, next)) };
2644
2862
  }
2645
2863
  });
@@ -2911,20 +3129,22 @@ async function loadPage(inputUrl, signal, pinned, fetchPage) {
2911
3129
  var fetchTool = createFetchTool();
2912
3130
 
2913
3131
  // src/tools/glob.ts
3132
+ import { statSync as statSync2 } from "fs";
2914
3133
  import { glob as nativeGlob } from "fs/promises";
3134
+ import { join as join4 } from "path";
2915
3135
  import { z as z4 } from "zod";
2916
3136
 
2917
3137
  // src/abort.ts
2918
3138
  var ABORTED = /* @__PURE__ */ Symbol("aborted");
2919
3139
  function raceAbort(promise, signal) {
2920
3140
  if (signal.aborted) return Promise.resolve(ABORTED);
2921
- return new Promise((resolve4, reject) => {
2922
- const onAbort = () => resolve4(ABORTED);
3141
+ return new Promise((resolve5, reject) => {
3142
+ const onAbort = () => resolve5(ABORTED);
2923
3143
  signal.addEventListener("abort", onAbort, { once: true });
2924
3144
  promise.then(
2925
3145
  (value) => {
2926
3146
  signal.removeEventListener("abort", onAbort);
2927
- resolve4(value);
3147
+ resolve5(value);
2928
3148
  },
2929
3149
  (error) => {
2930
3150
  signal.removeEventListener("abort", onAbort);
@@ -2950,22 +3170,38 @@ async function collectUntilAbort(iterable, signal, onItem) {
2950
3170
 
2951
3171
  // src/tools/ignore.ts
2952
3172
  import { readFileSync as readFileSync2 } from "fs";
2953
- import { join as join2 } from "path";
2954
- function ignorePatterns(root) {
3173
+ import { dirname as dirname2, join as join3, resolve as resolve3 } from "path";
3174
+ function ignorePatterns(root, projectRoot) {
2955
3175
  const patterns = ["**/node_modules/**", "**/.git/**"];
2956
- let lines = [];
2957
- try {
2958
- lines = readFileSync2(join2(root, ".gitignore"), "utf8").split(/\r?\n/);
2959
- } catch {
2960
- return patterns;
2961
- }
2962
- for (const raw of lines) {
2963
- const line = raw.trim();
2964
- if (!line || line.startsWith("#") || line.startsWith("!")) continue;
2965
- const p = line.replace(/^\//, "").replace(/\/$/, "");
2966
- patterns.push(`**/${p}`, `**/${p}/**`);
3176
+ const seen = /* @__PURE__ */ new Set();
3177
+ const dirs2 = [];
3178
+ let cur = resolve3(root);
3179
+ const stop = projectRoot ? resolve3(projectRoot) : cur;
3180
+ while (true) {
3181
+ if (!seen.has(cur)) {
3182
+ seen.add(cur);
3183
+ dirs2.push(cur);
3184
+ }
3185
+ if (cur === stop || !cur.startsWith(stop)) break;
3186
+ const parent = dirname2(cur);
3187
+ if (parent === cur) break;
3188
+ cur = parent;
3189
+ }
3190
+ for (const dir of dirs2) {
3191
+ let lines = [];
3192
+ try {
3193
+ lines = readFileSync2(join3(dir, ".gitignore"), "utf8").split(/\r?\n/);
3194
+ } catch {
3195
+ continue;
3196
+ }
3197
+ for (const raw of lines) {
3198
+ const line = raw.trim();
3199
+ if (!line || line.startsWith("#") || line.startsWith("!")) continue;
3200
+ const p = line.replace(/^\//, "").replace(/\/$/, "");
3201
+ patterns.push(`**/${p}`, `**/${p}/**`);
3202
+ }
2967
3203
  }
2968
- return patterns;
3204
+ return [...new Set(patterns)];
2969
3205
  }
2970
3206
 
2971
3207
  // src/tools/globSafety.ts
@@ -3007,19 +3243,30 @@ var globTool = defineTool({
3007
3243
  path: z4.string().optional().describe("Base directory, default project root")
3008
3244
  }),
3009
3245
  kind: "read",
3010
- target: (input) => input.pattern,
3246
+ target: (input) => input.path ? `${input.pattern} ${input.path}` : input.pattern,
3011
3247
  async run(input, ctx) {
3012
3248
  const invalid = validateGlobPattern(input.pattern);
3013
3249
  if (invalid) return { output: invalid, isError: true };
3014
3250
  const root = resolvePath(ctx.cwd, input.path ?? ".");
3015
3251
  const files = [];
3016
3252
  const finished = await collectUntilAbort(
3017
- nativeGlob(input.pattern, { cwd: root, exclude: ignorePatterns(root) }),
3253
+ nativeGlob(input.pattern, { cwd: root, exclude: ignorePatterns(root, ctx.cwd) }),
3018
3254
  ctx.signal,
3019
3255
  (file) => files.push(normalizeGlobPath(file))
3020
3256
  );
3021
3257
  if (!finished) return { output: "Cancelled by the user.", isError: true };
3022
- files.sort();
3258
+ const mtime = (rel) => {
3259
+ try {
3260
+ return statSync2(join4(root, rel)).mtimeMs;
3261
+ } catch {
3262
+ return 0;
3263
+ }
3264
+ };
3265
+ const mtimes = new Map(files.map((f) => [f, mtime(f)]));
3266
+ files.sort((a, b) => {
3267
+ const diff = (mtimes.get(b) ?? 0) - (mtimes.get(a) ?? 0);
3268
+ return Math.abs(diff) >= 50 ? diff : a.localeCompare(b);
3269
+ });
3023
3270
  if (!files.length) return { output: "No matching files." };
3024
3271
  const first = files.slice(0, 500);
3025
3272
  const shown = [];
@@ -3040,75 +3287,141 @@ var globTool = defineTool({
3040
3287
 
3041
3288
  // src/tools/grep.ts
3042
3289
  import { execFile } from "child_process";
3290
+ import { statSync as statSync3 } from "fs";
3043
3291
  import { readFile as readFile2 } from "fs/promises";
3044
3292
  import { glob as nativeGlob2 } from "fs/promises";
3293
+ import { basename, dirname as dirname3 } from "path";
3045
3294
  import { z as z5 } from "zod";
3046
3295
  var MAX_MATCHES = 200;
3047
3296
  var MAX_OUTPUT_CHARS2 = 24e3;
3297
+ function escapeRegExp(s) {
3298
+ return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
3299
+ }
3300
+ function isSingleFile(p) {
3301
+ try {
3302
+ return statSync3(p).isFile();
3303
+ } catch {
3304
+ return false;
3305
+ }
3306
+ }
3048
3307
  async function grepJs(pattern, opts) {
3049
- const regex = new RegExp(pattern, opts.ignoreCase ? "i" : "");
3308
+ const source = opts.literal ? escapeRegExp(pattern) : pattern;
3309
+ const regex = new RegExp(source, opts.ignoreCase ? "i" : "");
3310
+ const single = isSingleFile(opts.root);
3050
3311
  const files = [];
3051
- const finished = await collectUntilAbort(
3052
- nativeGlob2(opts.glob ?? "**/*", { cwd: opts.root, exclude: ignorePatterns(opts.root) }),
3053
- opts.signal,
3054
- (file) => files.push(normalizeGlobPath(file))
3055
- );
3312
+ if (single) {
3313
+ files.push(basename(opts.root));
3314
+ } else {
3315
+ const finished = await collectUntilAbort(
3316
+ nativeGlob2(opts.glob ?? "**/*", { cwd: opts.root, exclude: ignorePatterns(opts.root, opts.projectRoot) }),
3317
+ opts.signal,
3318
+ (file) => files.push(normalizeGlobPath(file))
3319
+ );
3320
+ if (!finished) return [];
3321
+ }
3322
+ const baseDir = single ? dirname3(opts.root) : opts.root;
3323
+ const ctxLines = Math.max(0, Math.min(10, Math.floor(opts.context ?? 0)));
3324
+ const mode = opts.outputMode ?? "content";
3056
3325
  const out = [];
3057
- if (!finished) return out;
3058
3326
  for (const file of files.sort()) {
3059
3327
  if (opts.signal?.aborted) break;
3060
3328
  let text;
3061
3329
  try {
3062
- const buffer = await readFile2(resolvePath(opts.root, file));
3330
+ const buffer = await readFile2(resolvePath(baseDir, file));
3063
3331
  if (buffer.subarray(0, 8e3).includes(0)) continue;
3064
3332
  text = buffer.toString("utf8");
3065
3333
  } catch {
3066
3334
  continue;
3067
3335
  }
3068
3336
  const lines = text.split(/\r?\n/);
3337
+ let fileMatches = 0;
3069
3338
  for (let i = 0; i < lines.length && out.length < MAX_MATCHES; i++) {
3070
- if (regex.test(lines[i])) out.push(`${file}:${i + 1}:${lines[i].slice(0, 300)}`);
3339
+ if (regex.test(lines[i])) {
3340
+ fileMatches++;
3341
+ if (mode === "files_with_matches") {
3342
+ out.push(file);
3343
+ break;
3344
+ }
3345
+ if (mode === "content") {
3346
+ const from = Math.max(0, i - ctxLines);
3347
+ const to = Math.min(lines.length - 1, i + ctxLines);
3348
+ for (let j = from; j <= to && out.length < MAX_MATCHES; j++) {
3349
+ const sep2 = j === i ? ":" : "-";
3350
+ out.push(`${file}${sep2}${j + 1}${sep2}${lines[j].slice(0, 300)}`);
3351
+ }
3352
+ }
3353
+ }
3071
3354
  }
3355
+ if (mode === "count" && fileMatches > 0) out.push(`${file}:${fileMatches}`);
3072
3356
  if (out.length >= MAX_MATCHES) break;
3073
3357
  }
3074
3358
  return out;
3075
3359
  }
3076
3360
  function grepRg(pattern, opts) {
3077
- const args = ["--line-number", "--no-heading", "--color", "never", "--max-columns", "300", "-e", pattern];
3361
+ const single = isSingleFile(opts.root);
3362
+ const cwd = single ? dirname3(opts.root) : opts.root;
3363
+ const target = single ? basename(opts.root) : ".";
3364
+ const mode = opts.outputMode ?? "content";
3365
+ const args = ["--color", "never"];
3366
+ if (mode === "files_with_matches") {
3367
+ args.push("--files-with-matches");
3368
+ } else if (mode === "count") {
3369
+ args.push("--count");
3370
+ } else {
3371
+ args.push("--line-number", "--no-heading", "--max-columns", "300");
3372
+ if (opts.context && opts.context > 0) args.push("-C", String(Math.min(10, Math.floor(opts.context))));
3373
+ }
3374
+ if (opts.literal) args.push("-F");
3078
3375
  if (opts.ignoreCase) args.push("-i");
3079
- if (opts.glob) args.push("-g", opts.glob);
3080
- args.push(".");
3081
- return new Promise((resolve4) => {
3082
- execFile("rg", args, { cwd: opts.root, maxBuffer: 10 * 1024 * 1024, signal: opts.signal }, (error, stdout) => {
3083
- if (opts.signal?.aborted) return resolve4([]);
3376
+ if (opts.glob && !single) args.push("-g", opts.glob);
3377
+ args.push("-e", pattern, target);
3378
+ return new Promise((resolve5) => {
3379
+ execFile("rg", args, { cwd, maxBuffer: 10 * 1024 * 1024, signal: opts.signal }, (error, stdout) => {
3380
+ if (opts.signal?.aborted) return resolve5([]);
3084
3381
  const code = error?.code;
3085
- if (code === "ENOENT") return resolve4(void 0);
3086
- if (error && error.code !== 1) return resolve4(void 0);
3087
- resolve4(stdout.split("\n").filter(Boolean).map((l) => l.replace(/^\.\//, "")).slice(0, MAX_MATCHES));
3382
+ if (code === "ENOENT") return resolve5(void 0);
3383
+ if (error && error.code !== 1) return resolve5(void 0);
3384
+ resolve5(
3385
+ stdout.split(/\r?\n/).filter(Boolean).map((l) => l.replace(/^\.[\\/]/, "").replace(/\\/g, "/")).slice(0, MAX_MATCHES)
3386
+ );
3088
3387
  });
3089
3388
  });
3090
3389
  }
3091
3390
  var grepTool = defineTool({
3092
3391
  name: "grep",
3093
- description: "Search file contents with a regular expression. Respects .gitignore. Returns path:line:text (max 200).",
3392
+ description: "Search file contents with a regular expression or literal text. Respects .gitignore. Returns path:line:text (max 200).",
3094
3393
  schema: z5.object({
3095
- pattern: z5.string().describe("Regular expression"),
3096
- path: z5.string().optional().describe("Directory to search, default project root"),
3394
+ pattern: z5.string().describe("Regular expression or literal string"),
3395
+ path: z5.string().optional().describe("File or directory to search, default project root"),
3097
3396
  glob: z5.string().optional().describe('Only files matching this glob, e.g. "**/*.ts"'),
3098
- ignore_case: z5.boolean().optional()
3397
+ ignore_case: z5.boolean().optional(),
3398
+ literal: z5.boolean().optional().describe("Treat pattern as literal text instead of regex"),
3399
+ context: z5.number().int().min(0).max(10).optional().describe("Number of context lines before and after each match"),
3400
+ output_mode: z5.enum(["content", "files_with_matches", "count"]).optional().describe("Output format, default content")
3099
3401
  }),
3100
3402
  kind: "read",
3101
- target: (input) => input.pattern,
3403
+ target: (input) => input.path ? `${input.pattern} ${input.path}` : input.pattern,
3102
3404
  async run(input, ctx) {
3103
3405
  if (input.glob) {
3104
3406
  const invalid = validateGlobPattern(input.glob);
3105
3407
  if (invalid) return { output: invalid, isError: true };
3106
3408
  }
3107
- const opts = { root: resolvePath(ctx.cwd, input.path ?? "."), glob: input.glob, ignoreCase: input.ignore_case, signal: ctx.signal };
3108
- try {
3109
- new RegExp(input.pattern);
3110
- } catch (error) {
3111
- return { output: `Invalid regex: ${error.message}`, isError: true };
3409
+ const opts = {
3410
+ root: resolvePath(ctx.cwd, input.path ?? "."),
3411
+ projectRoot: ctx.cwd,
3412
+ glob: input.glob,
3413
+ ignoreCase: input.ignore_case,
3414
+ literal: input.literal,
3415
+ context: input.context,
3416
+ outputMode: input.output_mode,
3417
+ signal: ctx.signal
3418
+ };
3419
+ if (!input.literal) {
3420
+ try {
3421
+ new RegExp(input.pattern);
3422
+ } catch (error) {
3423
+ return { output: `Invalid regex: ${error.message}`, isError: true };
3424
+ }
3112
3425
  }
3113
3426
  const matches = await grepRg(input.pattern, opts) ?? await grepJs(input.pattern, opts);
3114
3427
  if (ctx.signal.aborted) return { output: "Cancelled by the user.", isError: true };
@@ -3130,6 +3443,7 @@ var grepTool = defineTool({
3130
3443
  });
3131
3444
 
3132
3445
  // src/tools/read.ts
3446
+ import { readdirSync, statSync as statSync4 } from "fs";
3133
3447
  import { readFile as readFile3 } from "fs/promises";
3134
3448
  import { z as z6 } from "zod";
3135
3449
  var MAX_LINE = 2e3;
@@ -3213,6 +3527,16 @@ function createReadTool(options = {}) {
3213
3527
  target: (input) => input.path,
3214
3528
  async run(input, ctx) {
3215
3529
  const abs = resolvePath(ctx.cwd, input.path);
3530
+ try {
3531
+ if (statSync4(abs).isDirectory()) {
3532
+ const entries = readdirSync(abs, { withFileTypes: true }).map((e) => e.isDirectory() ? `${e.name}/` : e.name).sort((a, b) => a.localeCompare(b));
3533
+ const start = (input.offset ?? 1) - 1;
3534
+ const limit = input.limit ?? 2e3;
3535
+ const slice = entries.slice(start, start + limit);
3536
+ return { output: slice.join("\n") || "(empty directory)", display: t("{n} lines", { n: slice.length }) };
3537
+ }
3538
+ } catch {
3539
+ }
3216
3540
  let buffer;
3217
3541
  try {
3218
3542
  buffer = await readFile3(abs);
@@ -3221,13 +3545,13 @@ function createReadTool(options = {}) {
3221
3545
  }
3222
3546
  const mediaType = vision ? imageMediaType(buffer) : void 0;
3223
3547
  if (mediaType) {
3224
- ctx.readFiles.add(abs);
3548
+ recordFileMtime(ctx.readFiles, abs);
3225
3549
  return readImageResult(input, buffer, mediaType);
3226
3550
  }
3227
3551
  if (buffer.subarray(0, 8e3).includes(0)) {
3228
3552
  return { output: `${input.path} is a binary file; not read.`, isError: true };
3229
3553
  }
3230
- ctx.readFiles.add(abs);
3554
+ recordFileMtime(ctx.readFiles, abs);
3231
3555
  return readTextResult(input, buffer);
3232
3556
  }
3233
3557
  });
@@ -3237,7 +3561,7 @@ var readTool = createReadTool({ vision: true });
3237
3561
  // src/tools/write.ts
3238
3562
  import { existsSync as existsSync4 } from "fs";
3239
3563
  import { mkdir, readFile as readFile4, writeFile as writeFile2 } from "fs/promises";
3240
- import { dirname as dirname2 } from "path";
3564
+ import { dirname as dirname4 } from "path";
3241
3565
  import { z as z7 } from "zod";
3242
3566
  var writeTool = defineTool({
3243
3567
  name: "write",
@@ -3264,12 +3588,12 @@ var writeTool = defineTool({
3264
3588
  }
3265
3589
  try {
3266
3590
  await ctx.checkpoint?.(abs);
3267
- await mkdir(dirname2(abs), { recursive: true });
3591
+ await mkdir(dirname4(abs), { recursive: true });
3268
3592
  await writeFile2(abs, input.content);
3269
3593
  } catch (error) {
3270
3594
  return { output: `Failed to write ${input.path}: ${errorCode(error)}`, isError: true };
3271
3595
  }
3272
- ctx.readFiles.add(abs);
3596
+ recordFileMtime(ctx.readFiles, abs);
3273
3597
  const lines = input.content.split("\n").length;
3274
3598
  return { output: `Wrote ${input.path} (${lines} lines).`, display: t("{lines} lines written", { lines }) };
3275
3599
  }
@@ -3379,12 +3703,36 @@ function budgetStatus(usage, cap, model) {
3379
3703
  }
3380
3704
 
3381
3705
  // src/tokenBudget.ts
3706
+ var IMAGE_TOKEN_COST = 1e3;
3382
3707
  var estimateTokens = (text) => Math.ceil(text.length / 4);
3383
- var estimateMessages = (messages) => estimateTokens(JSON.stringify(messages));
3708
+ function isImageAttachmentMessage(m) {
3709
+ if (m.role !== "user" || !Array.isArray(m.content)) return false;
3710
+ const hasImage = m.content.some((p) => p.type === "image_url");
3711
+ const firstText = m.content.find((p) => p.type === "text");
3712
+ return hasImage && !!firstText && firstText.type === "text" && firstText.text.startsWith("Image read from ");
3713
+ }
3714
+ function isUserTurnBoundary(m) {
3715
+ return m.role === "user" && !isImageAttachmentMessage(m);
3716
+ }
3717
+ function estimateMessages(messages) {
3718
+ let total = 0;
3719
+ for (const m of messages) {
3720
+ if (m.role === "user" && Array.isArray(m.content)) {
3721
+ for (const part of m.content) {
3722
+ if (part.type === "image_url") total += IMAGE_TOKEN_COST;
3723
+ else total += estimateTokens(part.text);
3724
+ }
3725
+ total += 8;
3726
+ } else {
3727
+ total += estimateTokens(JSON.stringify(m));
3728
+ }
3729
+ }
3730
+ return total;
3731
+ }
3384
3732
  function trimToFit(messages, maxTokens) {
3385
3733
  let out = messages;
3386
3734
  while (estimateMessages(out) > maxTokens) {
3387
- const boundary = out.findIndex((m, i) => i > 0 && m.role === "user");
3735
+ const boundary = out.findIndex((m, i) => i > 0 && isUserTurnBoundary(m));
3388
3736
  if (boundary === -1) return out;
3389
3737
  out = out.slice(boundary);
3390
3738
  }
@@ -3394,13 +3742,13 @@ function splitKeepRecent(messages, keepTokens) {
3394
3742
  if (messages.length === 0) return [[], []];
3395
3743
  let start = messages.length;
3396
3744
  for (let i = messages.length - 1; i >= 0; i--) {
3397
- if (i > 0 && messages[i].role !== "user") continue;
3745
+ if (i > 0 && !isUserTurnBoundary(messages[i])) continue;
3398
3746
  if (estimateMessages(messages.slice(i)) <= keepTokens) start = i;
3399
3747
  else break;
3400
3748
  }
3401
3749
  if (start === messages.length) {
3402
3750
  for (let i = messages.length - 1; i >= 0; i--) {
3403
- if (messages[i].role === "user") {
3751
+ if (isUserTurnBoundary(messages[i])) {
3404
3752
  start = i;
3405
3753
  break;
3406
3754
  }
@@ -3448,6 +3796,7 @@ var Agent2 = class {
3448
3796
  this.messages = [...opts.history ?? []];
3449
3797
  this.maxSteps = typeof opts.maxSteps === "number" && Number.isFinite(opts.maxSteps) && opts.maxSteps > 0 ? Math.max(1, Math.floor(opts.maxSteps)) : void 0;
3450
3798
  this.contextWindow = opts.contextWindow ?? 128e3;
3799
+ this.budgetModel = opts.budgetModel ?? "";
3451
3800
  this.systemPrompt = opts.systemPrompt;
3452
3801
  this.reasoning = opts.reasoning ?? "auto";
3453
3802
  }
@@ -3471,6 +3820,7 @@ var Agent2 = class {
3471
3820
  maxSteps;
3472
3821
  // One window for every model; resolved from contextWindowFor() (catalog + env override).
3473
3822
  contextWindow;
3823
+ budgetModel;
3474
3824
  systemPrompt;
3475
3825
  setSystemPrompt(text) {
3476
3826
  this.systemPrompt = text;
@@ -3478,6 +3828,15 @@ var Agent2 = class {
3478
3828
  setContextWindow(window) {
3479
3829
  this.contextWindow = window;
3480
3830
  }
3831
+ getContextWindow() {
3832
+ return this.contextWindow;
3833
+ }
3834
+ setBudgetModel(model) {
3835
+ this.budgetModel = model;
3836
+ }
3837
+ getBudgetModel() {
3838
+ return this.budgetModel;
3839
+ }
3481
3840
  setTools(tools) {
3482
3841
  this.tools = tools;
3483
3842
  this.definitions = toolDefinitions(tools);
@@ -3678,7 +4037,7 @@ ${t("This model may not support tool calling. Try another model with /model.")}`
3678
4037
  }
3679
4038
  /** Current cap status; undefined means the budget is not exhausted. */
3680
4039
  overBudget() {
3681
- return budgetStatus(this.totalUsage, this.opts.usageCap, this.opts.budgetModel ?? "");
4040
+ return budgetStatus(this.totalUsage, this.opts.usageCap, this.budgetModel);
3682
4041
  }
3683
4042
  /** Every tool call needs an answer in the history, or the next request to the model is rejected. */
3684
4043
  cancelCalls(calls) {
@@ -3798,7 +4157,7 @@ ${t("This model may not support tool calling. Try another model with /model.")}`
3798
4157
  };
3799
4158
 
3800
4159
  // src/checkpoints.ts
3801
- import { existsSync as existsSync5, statSync } from "fs";
4160
+ import { existsSync as existsSync5, statSync as statSync5 } from "fs";
3802
4161
  import { readFile as readFile5, rm, writeFile as writeFile3 } from "fs/promises";
3803
4162
  var MAX_TURNS = 20;
3804
4163
  var MAX_BYTES2 = 2 * 1024 * 1024;
@@ -3825,8 +4184,8 @@ var CheckpointStore = class {
3825
4184
  history = [];
3826
4185
  current;
3827
4186
  pendingRedo;
4187
+ offset = 0;
3828
4188
  beginTurn() {
3829
- if (this.current?.size === 0) return;
3830
4189
  this.current = /* @__PURE__ */ new Map();
3831
4190
  this.history.push(this.current);
3832
4191
  this.pendingRedo = void 0;
@@ -3837,16 +4196,19 @@ var CheckpointStore = class {
3837
4196
  const turn = this.current;
3838
4197
  if (turn.has(path)) return;
3839
4198
  if (!existsSync5(path)) turn.set(path, null);
3840
- else if (statSync(path).size > MAX_BYTES2) turn.set(path, "skipped");
4199
+ else if (statSync5(path).size > MAX_BYTES2) turn.set(path, "skipped");
3841
4200
  else turn.set(path, await readFile5(path));
3842
- while (this.turns() > MAX_TURNS) this.history.shift();
4201
+ while (this.turns() > MAX_TURNS) {
4202
+ this.history.shift();
4203
+ this.offset++;
4204
+ }
3843
4205
  }
3844
4206
  turns() {
3845
4207
  return this.history.filter((t2) => t2.size).length;
3846
4208
  }
3847
4209
  /** Total entries including empty ones; used to align file snapshots with conversation turns. */
3848
4210
  entries() {
3849
- return this.history.length;
4211
+ return this.offset + this.history.length;
3850
4212
  }
3851
4213
  async undo() {
3852
4214
  while (this.history.length && !this.history.at(-1)?.size) this.history.pop();
@@ -3866,7 +4228,7 @@ var CheckpointStore = class {
3866
4228
  * pairs conversation turns with the entry count recorded when each turn started.
3867
4229
  */
3868
4230
  async undoTo(keep) {
3869
- const target = Math.max(0, Math.min(keep, this.history.length));
4231
+ const target = Math.max(0, Math.min(keep - this.offset, this.history.length));
3870
4232
  const result = emptyResult();
3871
4233
  const redo = /* @__PURE__ */ new Map();
3872
4234
  let applied = false;
@@ -3891,8 +4253,11 @@ var CheckpointStore = class {
3891
4253
  const turn = this.pendingRedo;
3892
4254
  if (!turn) return void 0;
3893
4255
  this.pendingRedo = void 0;
4256
+ const undoEntry = /* @__PURE__ */ new Map();
4257
+ await this.captureFor(turn, undoEntry);
3894
4258
  const result = emptyResult();
3895
4259
  await restoreTurn(turn, result);
4260
+ if (undoEntry.size) this.history.push(undoEntry);
3896
4261
  return result;
3897
4262
  }
3898
4263
  /** Records the on-disk state of a turn's paths so redo() can put it back. */
@@ -3905,7 +4270,7 @@ var CheckpointStore = class {
3905
4270
  }
3906
4271
  try {
3907
4272
  if (!existsSync5(path)) into.set(path, null);
3908
- else if (statSync(path).size > MAX_BYTES2) into.set(path, "skipped");
4273
+ else if (statSync5(path).size > MAX_BYTES2) into.set(path, "skipped");
3909
4274
  else into.set(path, await readFile5(path));
3910
4275
  } catch {
3911
4276
  into.set(path, "skipped");
@@ -3916,6 +4281,7 @@ var CheckpointStore = class {
3916
4281
  this.history.length = 0;
3917
4282
  this.current = void 0;
3918
4283
  this.pendingRedo = void 0;
4284
+ this.offset = 0;
3919
4285
  }
3920
4286
  };
3921
4287
 
@@ -3964,7 +4330,7 @@ import { z as z9 } from "zod";
3964
4330
  // src/mcp/config.ts
3965
4331
  import { createHash } from "crypto";
3966
4332
  import { existsSync as existsSync6, mkdirSync as mkdirSync2, readFileSync as readFileSync3 } from "fs";
3967
- import { dirname as dirname3, join as join3, resolve as resolve3 } from "path";
4333
+ import { dirname as dirname5, join as join5, resolve as resolve4 } from "path";
3968
4334
  var ENV_REF = /\$\{([A-Za-z_][A-Za-z0-9_]*)(?::-([^}]*))?\}/g;
3969
4335
  function expandEnvVars(config, env) {
3970
4336
  const expand = (value) => value.replace(ENV_REF, (match, name, fallback) => {
@@ -3988,9 +4354,9 @@ function expandEnvVars(config, env) {
3988
4354
  import { spawn as spawn3 } from "child_process";
3989
4355
  import { existsSync as existsSync7, mkdirSync as mkdirSync3, readFileSync as readFileSync4 } from "fs";
3990
4356
  import { createServer } from "http";
3991
- import { dirname as dirname4, join as join4 } from "path";
4357
+ import { dirname as dirname6, join as join6 } from "path";
3992
4358
  import { auth } from "@modelcontextprotocol/sdk/client/auth.js";
3993
- var oauthStorePath = (home) => join4(home, "mcp-oauth.json");
4359
+ var oauthStorePath = (home) => join6(home, "mcp-oauth.json");
3994
4360
  function readStore(home) {
3995
4361
  const path = oauthStorePath(home);
3996
4362
  if (!existsSync7(path)) return {};
@@ -4003,7 +4369,7 @@ function readStore(home) {
4003
4369
  }
4004
4370
  function writeStore(home, store) {
4005
4371
  const path = oauthStorePath(home);
4006
- mkdirSync3(dirname4(path), { recursive: true });
4372
+ mkdirSync3(dirname6(path), { recursive: true });
4007
4373
  writeFileAtomic(path, `${JSON.stringify(store, null, 2)}
4008
4374
  `, 384);
4009
4375
  }
@@ -4097,7 +4463,28 @@ var FileOAuthProvider = class {
4097
4463
  };
4098
4464
 
4099
4465
  // src/mcp/manager.ts
4100
- var MCP_OUTPUT_MAX_CHARS = 32e3;
4466
+ function formatMcpContent(content) {
4467
+ const parts = [];
4468
+ const images = [];
4469
+ for (const c of content ?? []) {
4470
+ if (c.type === "text") {
4471
+ parts.push(c.text ?? "");
4472
+ } else if (c.type === "image" && c.data) {
4473
+ const mediaType = c.mimeType || "image/png";
4474
+ images.push({ mediaType, data: c.data, path: "mcp:image" });
4475
+ parts.push(`[image attached: ${mediaType}]`);
4476
+ } else if (c.type === "resource" && c.resource?.text) {
4477
+ parts.push(c.resource.uri ? `[${c.resource.uri}]
4478
+ ${c.resource.text}` : c.resource.text);
4479
+ } else {
4480
+ parts.push(`[${c.type}]`);
4481
+ }
4482
+ }
4483
+ return {
4484
+ output: parts.join("\n") || "(empty)",
4485
+ ...images.length ? { images } : {}
4486
+ };
4487
+ }
4101
4488
  var CHILD_ENV_KEYS = [
4102
4489
  // portable
4103
4490
  "PATH",
@@ -4226,11 +4613,13 @@ var McpManager = class {
4226
4613
  async run(input, ctx) {
4227
4614
  try {
4228
4615
  const result = await client.callTool({ name: tool.name, arguments: input }, void 0, { signal: ctx.signal });
4229
- const output = (result.content ?? []).map((c) => c.type === "text" ? c.text ?? "" : `[${c.type}]`).join("\n") || "(empty)";
4616
+ const formatted = formatMcpContent(result.content);
4617
+ const output = formatted.output;
4230
4618
  return {
4231
- output: output.length > MCP_OUTPUT_MAX_CHARS ? `${output.slice(0, MCP_OUTPUT_MAX_CHARS)}
4232
- \u2026 [truncated at ${MCP_OUTPUT_MAX_CHARS} characters; narrow the MCP request if possible]` : output,
4233
- isError: !!result.isError
4619
+ output: output.length > 32e3 ? `${output.slice(0, 32e3)}
4620
+ \u2026 [truncated at 32000 characters; narrow the MCP request if possible]` : output,
4621
+ isError: !!result.isError,
4622
+ ...formatted.images ? { images: formatted.images } : {}
4234
4623
  };
4235
4624
  } catch (error) {
4236
4625
  return { output: `Server MCP ${server} error: ${error.message}`, isError: true };
@@ -4264,7 +4653,7 @@ import { homedir as homedir2 } from "os";
4264
4653
 
4265
4654
  // src/context.ts
4266
4655
  import { existsSync as existsSync8, readFileSync as readFileSync5 } from "fs";
4267
- import { dirname as dirname5, join as join5 } from "path";
4656
+ import { dirname as dirname7, join as join7 } from "path";
4268
4657
  var FILES = ["AGENTS.md", "CLAUDE.md", "BCCLI.md"];
4269
4658
  function readIf(path) {
4270
4659
  try {
@@ -4275,11 +4664,11 @@ function readIf(path) {
4275
4664
  }
4276
4665
  function instructionPaths(cwd, home) {
4277
4666
  const dirs2 = [];
4278
- for (let dir = cwd; ; dir = dirname5(dir)) {
4667
+ for (let dir = cwd; ; dir = dirname7(dir)) {
4279
4668
  dirs2.unshift(dir);
4280
- if (dirname5(dir) === dir) break;
4669
+ if (dirname7(dir) === dir) break;
4281
4670
  }
4282
- return [join5(home, "BCCLI.md"), ...dirs2.flatMap((dir) => FILES.map((name) => join5(dir, name)))];
4671
+ return [join7(home, "BCCLI.md"), ...dirs2.flatMap((dir) => FILES.map((name) => join7(dir, name)))];
4283
4672
  }
4284
4673
  function loadInstructions(cwd, home) {
4285
4674
  const parts = [];
@@ -4291,9 +4680,9 @@ ${text}`);
4291
4680
  return parts.join("\n\n");
4292
4681
  }
4293
4682
  function isGitRepo(cwd) {
4294
- for (let dir = cwd; ; dir = dirname5(dir)) {
4295
- if (existsSync8(join5(dir, ".git"))) return true;
4296
- if (dirname5(dir) === dir) return false;
4683
+ for (let dir = cwd; ; dir = dirname7(dir)) {
4684
+ if (existsSync8(join7(dir, ".git"))) return true;
4685
+ if (dirname7(dir) === dir) return false;
4297
4686
  }
4298
4687
  }
4299
4688
  function buildSystemPrompt(opts) {
@@ -4336,16 +4725,34 @@ ${instructions}` : ""}`;
4336
4725
  }
4337
4726
 
4338
4727
  // src/extensions.ts
4339
- import { existsSync as existsSync9, readdirSync, readFileSync as readFileSync6, statSync as statSync2 } from "fs";
4340
- import { basename, join as join6 } from "path";
4728
+ import { existsSync as existsSync9, readdirSync as readdirSync2, readFileSync as readFileSync6, statSync as statSync6 } from "fs";
4729
+ import { basename as basename2, join as join8 } from "path";
4341
4730
  function parseFrontmatter(text) {
4342
4731
  const match = /^---\r?\n([\s\S]*?)\r?\n---\r?\n?/.exec(text);
4343
4732
  if (!match) return { data: {}, body: text };
4344
4733
  const data = {};
4734
+ let currentListKey;
4345
4735
  for (const line of match[1].split(/\r?\n/)) {
4736
+ const listItem = /^\s*-\s+(.+)$/.exec(line);
4737
+ if (listItem && currentListKey) {
4738
+ const item = listItem[1].trim().replace(/^["']|["']$/g, "");
4739
+ const prev = data[currentListKey];
4740
+ if (Array.isArray(prev)) prev.push(item);
4741
+ else data[currentListKey] = [item];
4742
+ continue;
4743
+ }
4346
4744
  const kv = /^([A-Za-z0-9_-]+):\s*(.*)$/.exec(line);
4347
- if (!kv) continue;
4745
+ if (!kv) {
4746
+ if (line.trim()) currentListKey = void 0;
4747
+ continue;
4748
+ }
4348
4749
  let value = kv[2].trim();
4750
+ if (!value) {
4751
+ currentListKey = kv[1];
4752
+ data[kv[1]] = "";
4753
+ continue;
4754
+ }
4755
+ currentListKey = void 0;
4349
4756
  if (value.startsWith("[") && value.endsWith("]")) {
4350
4757
  data[kv[1]] = value.slice(1, -1).split(",").map((s) => s.trim().replace(/^["']|["']$/g, "")).filter(Boolean);
4351
4758
  continue;
@@ -4357,11 +4764,11 @@ function parseFrontmatter(text) {
4357
4764
  }
4358
4765
  var str = (v) => Array.isArray(v) ? v.join(", ") : v;
4359
4766
  function dirs(r, kind) {
4360
- return [join6(r.userHome, ".claude", kind), join6(r.home, kind), join6(r.cwd, ".claude", kind), join6(r.cwd, ".bccli", kind)];
4767
+ return [join8(r.userHome, ".claude", kind), join8(r.home, kind), join8(r.cwd, ".claude", kind), join8(r.cwd, ".bccli", kind)];
4361
4768
  }
4362
4769
  function list(dir) {
4363
4770
  try {
4364
- return readdirSync(dir).sort();
4771
+ return readdirSync2(dir).sort();
4365
4772
  } catch {
4366
4773
  return [];
4367
4774
  }
@@ -4370,11 +4777,11 @@ function loadSkills(r) {
4370
4777
  const byName = /* @__PURE__ */ new Map();
4371
4778
  for (const dir of dirs(r, "skills")) {
4372
4779
  for (const entry of list(dir)) {
4373
- const file = join6(dir, entry, "SKILL.md");
4780
+ const file = join8(dir, entry, "SKILL.md");
4374
4781
  if (!existsSync9(file)) continue;
4375
4782
  const { data } = parseFrontmatter(readFileSync6(file, "utf8"));
4376
4783
  const name = str(data.name) || entry;
4377
- byName.set(name, { name, description: str(data.description) ?? "", dir: join6(dir, entry), file });
4784
+ byName.set(name, { name, description: str(data.description) ?? "", dir: join8(dir, entry), file });
4378
4785
  }
4379
4786
  }
4380
4787
  return [...byName.values()];
@@ -4383,10 +4790,10 @@ function loadCommands(r) {
4383
4790
  const byName = /* @__PURE__ */ new Map();
4384
4791
  for (const dir of dirs(r, "commands")) {
4385
4792
  for (const entry of list(dir)) {
4386
- const file = join6(dir, entry);
4387
- if (!entry.endsWith(".md") || !statSync2(file).isFile()) continue;
4793
+ const file = join8(dir, entry);
4794
+ if (!entry.endsWith(".md") || !statSync6(file).isFile()) continue;
4388
4795
  const { data, body } = parseFrontmatter(readFileSync6(file, "utf8"));
4389
- const def = { name: basename(entry, ".md"), body: body.trim() };
4796
+ const def = { name: basename2(entry, ".md"), body: body.trim() };
4390
4797
  if (data.description) def.description = str(data.description);
4391
4798
  if (data["argument-hint"]) def.argumentHint = str(data["argument-hint"]);
4392
4799
  byName.set(def.name, def);
@@ -4399,8 +4806,8 @@ function loadAgentDefs(r) {
4399
4806
  for (const dir of dirs(r, "agents")) {
4400
4807
  for (const entry of list(dir)) {
4401
4808
  if (!entry.endsWith(".md")) continue;
4402
- const { data, body } = parseFrontmatter(readFileSync6(join6(dir, entry), "utf8"));
4403
- const name = str(data.name) || basename(entry, ".md");
4809
+ const { data, body } = parseFrontmatter(readFileSync6(join8(dir, entry), "utf8"));
4810
+ const name = str(data.name) || basename2(entry, ".md");
4404
4811
  const def = { name, description: str(data.description) ?? "", prompt: body.trim() };
4405
4812
  if (data.tools)
4406
4813
  def.tools = Array.isArray(data.tools) ? data.tools : String(data.tools).split(",").map((s) => s.trim()).filter(Boolean);
@@ -4418,8 +4825,7 @@ function loadAgentDefs(r) {
4418
4825
  }
4419
4826
 
4420
4827
  // src/providers.ts
4421
- import { mkdirSync as mkdirSync4, writeFileSync as writeFileSync2 } from "fs";
4422
- import { join as join7 } from "path";
4828
+ import { join as join9 } from "path";
4423
4829
  function providerName(config, id) {
4424
4830
  return PRESETS.find((p) => p.id === id)?.name ?? config.providers[id]?.name ?? id;
4425
4831
  }
@@ -4478,11 +4884,11 @@ async function addProviderKey(providerId, key, deps) {
4478
4884
 
4479
4885
  // src/session.ts
4480
4886
  import { randomBytes as randomBytes2 } from "crypto";
4481
- import { appendFileSync, existsSync as existsSync10, mkdirSync as mkdirSync5, readdirSync as readdirSync2, readFileSync as readFileSync7, rmSync as rmSync2, statSync as statSync3 } from "fs";
4482
- import { join as join8 } from "path";
4887
+ import { appendFileSync, existsSync as existsSync10, mkdirSync as mkdirSync4, readdirSync as readdirSync3, readFileSync as readFileSync7, rmSync as rmSync2, statSync as statSync7 } from "fs";
4888
+ import { basename as basename3, join as join10 } from "path";
4483
4889
  function projectDir(home, cwd) {
4484
4890
  const slug = cwd.replace(/[^a-zA-Z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(-100) || "root";
4485
- return join8(home, "sessions", slug);
4891
+ return join10(home, "sessions", slug);
4486
4892
  }
4487
4893
  function repairTail(messages) {
4488
4894
  const out = [...messages];
@@ -4500,19 +4906,22 @@ var Session = class _Session {
4500
4906
  this.file = file;
4501
4907
  }
4502
4908
  file;
4909
+ get id() {
4910
+ return basename3(this.file, ".jsonl");
4911
+ }
4503
4912
  static create(home, cwd, now = /* @__PURE__ */ new Date()) {
4504
4913
  const dir = projectDir(home, cwd);
4505
- mkdirSync5(dir, { recursive: true });
4914
+ mkdirSync4(dir, { recursive: true });
4506
4915
  const stamp = now.toISOString().replace(/[:.]/g, "-");
4507
- return new _Session(join8(dir, `${stamp}-${randomBytes2(3).toString("hex")}.jsonl`));
4916
+ return new _Session(join10(dir, `${stamp}-${randomBytes2(3).toString("hex")}.jsonl`));
4508
4917
  }
4509
4918
  static list(home, cwd) {
4510
4919
  const dir = projectDir(home, cwd);
4511
4920
  if (!existsSync10(dir)) return [];
4512
- return readdirSync2(dir).filter((f) => f.endsWith(".jsonl")).map((f) => {
4513
- const session = new _Session(join8(dir, f));
4514
- const first = session.load().find((m) => m.role === "user");
4515
- return { session, mtime: statSync3(session.file).mtime, preview: first ? contentText(first.content).slice(0, 80) : "" };
4921
+ return readdirSync3(dir).filter((f) => f.endsWith(".jsonl")).map((f) => {
4922
+ const session = new _Session(join10(dir, f));
4923
+ const first = session.loadHistory().find((m) => m.role === "user");
4924
+ return { session, mtime: statSync7(session.file).mtime, preview: first ? contentText(first.content).slice(0, 80) : "" };
4516
4925
  }).sort((a, b) => b.mtime.getTime() - a.mtime.getTime() || b.session.file.localeCompare(a.session.file));
4517
4926
  }
4518
4927
  static latest(home, cwd) {
@@ -4561,6 +4970,35 @@ var Session = class _Session {
4561
4970
  }
4562
4971
  return void 0;
4563
4972
  }
4973
+ /** Accumulated token usage since the last non-compaction reset. */
4974
+ totalUsage() {
4975
+ const segments = [{ msgs: [], usage: { inputTokens: 0, outputTokens: 0 } }];
4976
+ for (const record of this.records()) {
4977
+ const cur = segments[segments.length - 1];
4978
+ if (record.t === "reset") {
4979
+ segments.push({ msgs: [], usage: { inputTokens: 0, outputTokens: 0 } });
4980
+ } else if (record.t === "msg") {
4981
+ cur.msgs.push(record.m);
4982
+ } else if (record.t === "usage") {
4983
+ cur.usage.inputTokens += record.inputTokens;
4984
+ cur.usage.outputTokens += record.outputTokens;
4985
+ if (record.cachedInputTokens) cur.usage.cachedInputTokens = (cur.usage.cachedInputTokens ?? 0) + record.cachedInputTokens;
4986
+ if (record.cacheWriteTokens) cur.usage.cacheWriteTokens = (cur.usage.cacheWriteTokens ?? 0) + record.cacheWriteTokens;
4987
+ }
4988
+ }
4989
+ let total = { ...segments[0].usage };
4990
+ for (const seg of segments.slice(1)) {
4991
+ if (!isCompactionSummary(seg.msgs[0])) {
4992
+ total = { ...seg.usage };
4993
+ } else {
4994
+ total.inputTokens += seg.usage.inputTokens;
4995
+ total.outputTokens += seg.usage.outputTokens;
4996
+ if (seg.usage.cachedInputTokens) total.cachedInputTokens = (total.cachedInputTokens ?? 0) + seg.usage.cachedInputTokens;
4997
+ if (seg.usage.cacheWriteTokens) total.cacheWriteTokens = (total.cacheWriteTokens ?? 0) + seg.usage.cacheWriteTokens;
4998
+ }
4999
+ }
5000
+ return total;
5001
+ }
4564
5002
  /**
4565
5003
  * What the user sees on resume: the whole conversation. Compaction only shrinks what the model is sent, so
4566
5004
  * its reset does not hide the older turns here. A real reset (/clear, rewind) still starts over, the
@@ -4573,12 +5011,22 @@ var Session = class _Session {
4573
5011
  else if (record.t === "msg") segments[segments.length - 1].push(record.m);
4574
5012
  }
4575
5013
  let shown = segments[0];
5014
+ let lastSummary;
4576
5015
  for (const segment of segments.slice(1)) {
4577
5016
  if (!isCompactionSummary(segment[0])) {
4578
5017
  shown = segment;
5018
+ lastSummary = void 0;
4579
5019
  continue;
4580
5020
  }
4581
5021
  const kept = segment.slice(2);
5022
+ if (lastSummary && sameMessage2(segment[0], lastSummary)) {
5023
+ const subIdx = findContiguousSlice(shown, kept);
5024
+ if (subIdx !== -1) {
5025
+ shown = shown.slice(0, subIdx + kept.length);
5026
+ continue;
5027
+ }
5028
+ }
5029
+ lastSummary = segment[0];
4582
5030
  let at = 0;
4583
5031
  while (at <= shown.length && !endsWithPrefixOf(shown, at, kept)) at++;
4584
5032
  shown = [...shown, ...kept.slice(shown.length - at)];
@@ -4605,6 +5053,20 @@ function endsWithPrefixOf(shown, at, kept) {
4605
5053
  for (let j = 0; j < n; j++) if (!sameMessage2(shown[at + j], kept[j])) return false;
4606
5054
  return true;
4607
5055
  }
5056
+ function findContiguousSlice(shown, slice) {
5057
+ if (slice.length === 0) return shown.length;
5058
+ for (let i = 0; i + slice.length <= shown.length; i++) {
5059
+ let ok = true;
5060
+ for (let j = 0; j < slice.length; j++) {
5061
+ if (!sameMessage2(shown[i + j], slice[j])) {
5062
+ ok = false;
5063
+ break;
5064
+ }
5065
+ }
5066
+ if (ok) return i;
5067
+ }
5068
+ return -1;
5069
+ }
4608
5070
 
4609
5071
  // src/tools/plan.ts
4610
5072
  import { z as z10 } from "zod";
@@ -4714,6 +5176,7 @@ import { z as z13 } from "zod";
4714
5176
  function createTaskTool(opts) {
4715
5177
  const byName = new Map(opts.agents.map((a) => [a.name, a]));
4716
5178
  const list2 = opts.agents.map((a) => `- ${a.name}: ${a.description}`).join("\n");
5179
+ const sessions = /* @__PURE__ */ new Map();
4717
5180
  let askChain = Promise.resolve();
4718
5181
  const enqueueAsk = (fn) => {
4719
5182
  const next = askChain.then(
@@ -4733,7 +5196,8 @@ ${list2}`,
4733
5196
  schema: z13.object({
4734
5197
  agent: z13.string().describe("Agent name"),
4735
5198
  description: z13.string().describe("3-6 word summary shown to the user"),
4736
- prompt: z13.string().describe("Full instructions; the subagent sees nothing else")
5199
+ prompt: z13.string().describe("Full instructions; the subagent sees nothing else"),
5200
+ session_id: z13.string().optional().describe("Optional session id to continue a previous subagent conversation")
4737
5201
  }),
4738
5202
  kind: "read",
4739
5203
  target: (input) => `[${input.agent}] ${input.description}`,
@@ -4760,6 +5224,7 @@ ${list2}`,
4760
5224
  } catch (error) {
4761
5225
  return { output: `Agent "${def.name}": ${error.message}`, isError: true };
4762
5226
  }
5227
+ const priorHistory = input.session_id ? sessions.get(input.session_id) : void 0;
4763
5228
  const child = new Agent2({
4764
5229
  provider,
4765
5230
  tools,
@@ -4769,6 +5234,7 @@ ${list2}`,
4769
5234
  # Your role
4770
5235
  ${def.prompt}`,
4771
5236
  cwd: opts.cwd,
5237
+ ...priorHistory ? { history: priorHistory } : {},
4772
5238
  // Every subagent runs with a step cap so a looping child cannot burn tokens forever.
4773
5239
  maxSteps: def.maxSteps ?? 50,
4774
5240
  label: def.name,
@@ -4789,12 +5255,29 @@ ${def.prompt}`,
4789
5255
  const ask = ctx.ask;
4790
5256
  if (ask) child.askPermission = (req) => enqueueAsk(() => ask(req));
4791
5257
  await child.run(input.prompt, ctx.signal);
5258
+ if (input.session_id) sessions.set(input.session_id, [...child.messages]);
4792
5259
  ctx.addUsage?.(child.totalUsage);
4793
5260
  const tokens2 = child.totalUsage.inputTokens + child.totalUsage.outputTokens;
4794
5261
  const display = t("{steps} steps \xB7 {tokens} tokens", { steps, tokens: tokens2 >= 1e3 ? `${Math.round(tokens2 / 1e3)}k` : tokens2 });
4795
5262
  if (outcome?.type === "error") return { output: `Subagent ${def.name} failed: ${outcome.message}`, isError: true, display };
4796
5263
  if (outcome?.type === "aborted") return { output: `Subagent ${def.name} was cancelled.`, isError: true, display };
4797
- const last = [...child.messages].reverse().find((m) => m.role === "assistant" && m.content);
5264
+ let last = [...child.messages].reverse().find((m) => m.role === "assistant" && m.content);
5265
+ if (!last && outcome?.type === "stepLimit" && !ctx.signal.aborted) {
5266
+ try {
5267
+ const synthesis = await provider.chat({
5268
+ messages: [
5269
+ ...child.messages,
5270
+ { role: "user", content: "Step limit reached. Summarize all findings gathered so far concisely." }
5271
+ ],
5272
+ signal: ctx.signal,
5273
+ reasoning: opts.reasoning?.() ?? "auto"
5274
+ });
5275
+ if (synthesis.text.trim()) {
5276
+ last = { role: "assistant", content: synthesis.text.trim() };
5277
+ }
5278
+ } catch {
5279
+ }
5280
+ }
4798
5281
  const report = last && typeof last.content === "string" ? last.content : "(no report)";
4799
5282
  const suffix = outcome?.type === "stepLimit" ? `
4800
5283
 
@@ -4840,7 +5323,7 @@ function createTodoTool(store) {
4840
5323
 
4841
5324
  // src/tools/memory.ts
4842
5325
  import { existsSync as existsSync12, readFileSync as readFileSync10 } from "fs";
4843
- import { join as join9 } from "path";
5326
+ import { join as join11 } from "path";
4844
5327
  import { z as z15 } from "zod";
4845
5328
 
4846
5329
  // src/slash/memory.ts
@@ -4863,11 +5346,11 @@ function createSaveMemoryTool(opts) {
4863
5346
  kind: "edit",
4864
5347
  // The target is what the prompt shows and what the permission model judges: the
4865
5348
  // global store really lives outside the project, so it must not look like one.
4866
- target: (input) => input.scope === "global" ? join9(opts.home, "BCCLI.md") : "AGENTS.md",
5349
+ target: (input) => input.scope === "global" ? join11(opts.home, "BCCLI.md") : "AGENTS.md",
4867
5350
  async run(input, ctx) {
4868
5351
  const content = input.content.trim();
4869
5352
  if (!content) return { output: "Memory content is empty.", isError: true };
4870
- const file = input.scope === "global" ? join9(opts.home, "BCCLI.md") : join9(ctx.cwd, "AGENTS.md");
5353
+ const file = input.scope === "global" ? join11(opts.home, "BCCLI.md") : join11(ctx.cwd, "AGENTS.md");
4871
5354
  if (existsSync12(file)) {
4872
5355
  const lines = readFileSync10(file, "utf8").split("\n");
4873
5356
  if (lines.some((line) => line.trim() === `- ${content}`)) {
@@ -4885,18 +5368,21 @@ function createSaveMemoryTool(opts) {
4885
5368
  import { z as z16 } from "zod";
4886
5369
 
4887
5370
  // src/repomap.ts
4888
- import { readdirSync as readdirSync3, readFileSync as readFileSync11 } from "fs";
4889
- import { extname, join as join10, relative as relative2, sep as sep2 } from "path";
5371
+ import { readdirSync as readdirSync4, readFileSync as readFileSync11 } from "fs";
5372
+ import { extname, join as join12, relative as relative2, sep } from "path";
4890
5373
  var SKIP_DIRS = /* @__PURE__ */ new Set([
4891
5374
  "node_modules",
4892
5375
  ".git",
4893
5376
  "dist",
4894
5377
  "build",
5378
+ "out",
4895
5379
  "coverage",
4896
5380
  ".next",
5381
+ ".turbo",
4897
5382
  "target",
4898
5383
  "vendor",
4899
5384
  "__pycache__",
5385
+ "venv",
4900
5386
  ".venv",
4901
5387
  ".cache"
4902
5388
  ]);
@@ -4922,21 +5408,22 @@ var CODE_EXTS = /* @__PURE__ */ new Set([
4922
5408
  ".ps1"
4923
5409
  ]);
4924
5410
  var MAX_FILES = 400;
5411
+ var MAX_SCAN_FILES = 5e3;
4925
5412
  var MAX_FILE_BYTES = 2e5;
4926
5413
  var MAX_SYMBOLS_PER_FILE = 12;
4927
5414
  var FAMILY = {
4928
5415
  ts: [
4929
- /^\s*(?:export\s+)?(?:default\s+)?(?:declare\s+)?(?:abstract\s+)?(?:async\s+)?(?:function|class|interface|type|enum)\s+([A-Za-z0-9_$]+)/,
5416
+ /^\s*(?:export\s+)?(?:default\s+)?(?:declare\s+)?(?:public\s+|private\s+|protected\s+|internal\s+)?(?:static\s+)?(?:abstract\s+|final\s+|sealed\s+)?(?:async\s+)?(?:function|class|interface|type|enum)\s+([A-Za-z0-9_$]+)/,
4930
5417
  /^\s*export\s+(?:const|let)\s+([A-Za-z0-9_$]+)/
4931
5418
  ],
4932
- py: [/^\s*(?:async\s+)?def\s+(\w+)/, /^\s*class\s+(\w+)/],
5419
+ py: [/^\s*(?:async\s+)?def\s+(\w+)/, /^\s*(?:module|class)\s+(\w+)/],
4933
5420
  go: [/^func\s+(?:\([^)]*\)\s*)?(\w+)/, /^type\s+(\w+)/],
4934
5421
  rs: [/^\s*(?:pub(?:\([^)]*\))?\s+)?(?:unsafe\s+)?(?:async\s+)?(?:fn|struct|enum|trait|type)\s+(\w+)/],
4935
5422
  c: [/^\s*(?:static\s+|inline\s+|extern\s+)*[A-Za-z_][A-Za-z0-9_\s*]*\b(\w+)\s*\([^;]*\)\s*\{?\s*$/]
4936
5423
  };
4937
5424
  function familyFor(ext) {
4938
- if ([".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs", ".java", ".cs", ".php", ".rb"].includes(ext)) return FAMILY.ts;
4939
- if (ext === ".py" || ext === ".sh" || ext === ".ps1") return FAMILY.py;
5425
+ if ([".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs", ".java", ".cs", ".php"].includes(ext)) return FAMILY.ts;
5426
+ if (ext === ".py" || ext === ".rb" || ext === ".sh" || ext === ".ps1") return FAMILY.py;
4940
5427
  if (ext === ".go") return FAMILY.go;
4941
5428
  if (ext === ".rs") return FAMILY.rs;
4942
5429
  return FAMILY.c;
@@ -4956,16 +5443,16 @@ function extractSymbols(text, ext) {
4956
5443
  return out;
4957
5444
  }
4958
5445
  function walk(dir, root, files) {
4959
- if (files.length >= MAX_FILES) return;
5446
+ if (files.length >= MAX_SCAN_FILES) return;
4960
5447
  let entries;
4961
5448
  try {
4962
- entries = readdirSync3(dir, { withFileTypes: true });
5449
+ entries = readdirSync4(dir, { withFileTypes: true });
4963
5450
  } catch {
4964
5451
  return;
4965
5452
  }
4966
5453
  for (const e of entries.sort((a, b) => a.name.localeCompare(b.name))) {
4967
- if (files.length >= MAX_FILES) return;
4968
- const full = join10(dir, e.name);
5454
+ if (files.length >= MAX_SCAN_FILES) return;
5455
+ const full = join12(dir, e.name);
4969
5456
  if (e.isDirectory()) {
4970
5457
  if (SKIP_DIRS.has(e.name) || e.name.startsWith(".") && full !== root) continue;
4971
5458
  walk(full, root, files);
@@ -4976,16 +5463,17 @@ function walk(dir, root, files) {
4976
5463
  }
4977
5464
  function buildRepoMapSync(cwd, opts = {}) {
4978
5465
  const maxTokens = opts.maxTokens ?? 2048;
4979
- const files = [];
4980
- walk(cwd, cwd, files);
4981
- files.sort(
4982
- (a, b) => relative2(cwd, a).split(sep2).length - relative2(cwd, b).split(sep2).length || a.localeCompare(b)
5466
+ const scanned = [];
5467
+ walk(cwd, cwd, scanned);
5468
+ scanned.sort(
5469
+ (a, b) => relative2(cwd, a).split(sep).length - relative2(cwd, b).split(sep).length || a.localeCompare(b)
4983
5470
  );
5471
+ const files = scanned.slice(0, MAX_FILES);
4984
5472
  const lines = [];
4985
5473
  let used = 0;
4986
5474
  let truncated = false;
4987
5475
  for (const file of files) {
4988
- const rel = relative2(cwd, file).split(sep2).join("/");
5476
+ const rel = relative2(cwd, file).split(sep).join("/");
4989
5477
  let symbols = [];
4990
5478
  try {
4991
5479
  const text = readFileSync11(file, "utf8");
@@ -5020,13 +5508,50 @@ function createRepoMapTool() {
5020
5508
  }
5021
5509
 
5022
5510
  // src/tools/diagnostics.ts
5023
- import { extname as extname2 } from "path";
5511
+ import { extname as extname3, relative as relative3 } from "path";
5024
5512
  import { z as z17 } from "zod";
5025
5513
 
5026
5514
  // src/lsp.ts
5027
5515
  import { spawn as spawn4 } from "child_process";
5028
5516
  import { readFileSync as readFileSync12 } from "fs";
5029
- import { pathToFileURL } from "url";
5517
+ import { extname as extname2 } from "path";
5518
+ import { fileURLToPath, pathToFileURL } from "url";
5519
+ var LANGUAGE_IDS = {
5520
+ ".ts": "typescript",
5521
+ ".tsx": "typescriptreact",
5522
+ ".mts": "typescript",
5523
+ ".cts": "typescript",
5524
+ ".js": "javascript",
5525
+ ".jsx": "javascriptreact",
5526
+ ".mjs": "javascript",
5527
+ ".cjs": "javascript",
5528
+ ".py": "python",
5529
+ ".rs": "rust",
5530
+ ".go": "go",
5531
+ ".java": "java",
5532
+ ".c": "c",
5533
+ ".h": "c",
5534
+ ".cpp": "cpp",
5535
+ ".hpp": "cpp",
5536
+ ".cs": "csharp",
5537
+ ".rb": "ruby",
5538
+ ".php": "php",
5539
+ ".json": "json",
5540
+ ".md": "markdown",
5541
+ ".sh": "shellscript",
5542
+ ".ps1": "powershell",
5543
+ ".yaml": "yaml",
5544
+ ".yml": "yaml",
5545
+ ".toml": "toml",
5546
+ ".html": "html",
5547
+ ".css": "css"
5548
+ };
5549
+ function languageIdFor(file) {
5550
+ return LANGUAGE_IDS[extname2(file).toLowerCase()] ?? "plaintext";
5551
+ }
5552
+ function useShellForLsp(command, platform = process.platform) {
5553
+ return platform === "win32" && !/\.(exe|mjs|cjs|js)$/i.test(command.trim());
5554
+ }
5030
5555
  function encodeLsp(msg) {
5031
5556
  const body = Buffer.from(JSON.stringify(msg), "utf8");
5032
5557
  return Buffer.concat([Buffer.from(`Content-Length: ${body.length}\r
@@ -5058,89 +5583,281 @@ function createLspParser(onMessage) {
5058
5583
  }
5059
5584
  };
5060
5585
  }
5061
- function getDiagnostics(opts) {
5062
- return new Promise((resolve4) => {
5063
- const child = spawn4(opts.server.command, opts.server.args ?? [], { cwd: opts.cwd, stdio: ["pipe", "pipe", "ignore"] });
5064
- const uri = pathToFileURL(opts.file).href;
5065
- let settled = false;
5066
- let nextId = 2;
5067
- const send = (msg) => {
5068
- try {
5069
- child.stdin.write(encodeLsp(msg));
5070
- } catch {
5071
- }
5072
- };
5073
- const finish = (diags) => {
5074
- if (settled) return;
5075
- settled = true;
5076
- clearTimeout(timer);
5077
- send({ jsonrpc: "2.0", id: nextId++, method: "shutdown" });
5078
- send({ jsonrpc: "2.0", method: "exit" });
5079
- const killer = setTimeout(() => {
5080
- try {
5081
- child.kill();
5082
- } catch {
5586
+ function extractHoverText(contents) {
5587
+ if (!contents) return "";
5588
+ if (typeof contents === "string") return contents.trim();
5589
+ if (Array.isArray(contents)) return contents.map(extractHoverText).filter(Boolean).join("\n\n");
5590
+ if (typeof contents === "object" && "value" in contents && typeof contents.value === "string") {
5591
+ return contents.value.trim();
5592
+ }
5593
+ return "";
5594
+ }
5595
+ function normalizeLocations(raw) {
5596
+ const items = Array.isArray(raw) ? raw : raw ? [raw] : [];
5597
+ const out = [];
5598
+ for (const item of items) {
5599
+ if (!item || typeof item !== "object") continue;
5600
+ const rec = item;
5601
+ const uri = rec.uri ?? rec.targetUri;
5602
+ const start = rec.range?.start ?? rec.targetSelectionRange?.start ?? rec.targetRange?.start;
5603
+ if (!uri) continue;
5604
+ let path = uri;
5605
+ try {
5606
+ if (uri.startsWith("file:")) path = fileURLToPath(uri);
5607
+ } catch {
5608
+ }
5609
+ out.push({
5610
+ uri,
5611
+ path,
5612
+ line: start?.line ?? 0,
5613
+ character: start?.character ?? 0
5614
+ });
5615
+ }
5616
+ return out;
5617
+ }
5618
+ function createWarmLspClient(opts) {
5619
+ let child;
5620
+ let initPromise;
5621
+ let nextId = 2;
5622
+ const opened = /* @__PURE__ */ new Map();
5623
+ const pendingRpc = /* @__PURE__ */ new Map();
5624
+ const diagWaiters = /* @__PURE__ */ new Map();
5625
+ const send = (msg) => {
5626
+ try {
5627
+ child?.stdin?.write(encodeLsp(msg));
5628
+ } catch {
5629
+ }
5630
+ };
5631
+ const resetState = () => {
5632
+ child = void 0;
5633
+ initPromise = void 0;
5634
+ opened.clear();
5635
+ for (const [, resolve5] of pendingRpc) resolve5(void 0);
5636
+ pendingRpc.clear();
5637
+ for (const [, waiters] of diagWaiters) {
5638
+ for (const w of waiters) w([]);
5639
+ }
5640
+ diagWaiters.clear();
5641
+ };
5642
+ const ensureStarted = () => {
5643
+ if (initPromise && child && !child.killed) return initPromise;
5644
+ opened.clear();
5645
+ initPromise = new Promise((resolveInit) => {
5646
+ const proc = spawn4(opts.server.command, opts.server.args ?? [], {
5647
+ cwd: opts.cwd,
5648
+ stdio: ["pipe", "pipe", "ignore"],
5649
+ shell: useShellForLsp(opts.server.command)
5650
+ });
5651
+ child = proc;
5652
+ let initSettled = false;
5653
+ const settleInit = () => {
5654
+ if (!initSettled) {
5655
+ initSettled = true;
5656
+ resolveInit();
5083
5657
  }
5084
- }, 500);
5085
- killer.unref?.();
5086
- resolve4(diags);
5087
- };
5088
- const timer = setTimeout(() => finish([]), opts.timeoutMs ?? 8e3);
5089
- const parser = createLspParser((msg) => {
5090
- if (msg.id === 1 && msg.result) {
5091
- send({ jsonrpc: "2.0", method: "initialized", params: {} });
5092
- let text = "";
5093
- try {
5094
- text = readFileSync12(opts.file, "utf8");
5095
- } catch {
5658
+ };
5659
+ const parser = createLspParser((msg) => {
5660
+ if (msg.id === 1 && msg.result !== void 0) {
5661
+ send({ jsonrpc: "2.0", method: "initialized", params: {} });
5662
+ settleInit();
5663
+ return;
5096
5664
  }
5097
- send({
5098
- jsonrpc: "2.0",
5099
- method: "textDocument/didOpen",
5100
- params: { textDocument: { uri, languageId: "plaintext", version: 1, text } }
5101
- });
5102
- }
5103
- if (msg.method === "textDocument/publishDiagnostics" && msg.params?.uri === uri) {
5104
- finish(
5105
- (msg.params.diagnostics ?? []).map((d) => ({
5665
+ if (msg.id !== void 0 && pendingRpc.has(msg.id)) {
5666
+ const fn = pendingRpc.get(msg.id);
5667
+ pendingRpc.delete(msg.id);
5668
+ fn?.(msg.result);
5669
+ return;
5670
+ }
5671
+ if (msg.method === "textDocument/publishDiagnostics" && msg.params?.uri) {
5672
+ const uri = msg.params.uri;
5673
+ const diags = (msg.params.diagnostics ?? []).map((d) => ({
5106
5674
  line: d.range?.start?.line ?? 0,
5107
5675
  character: d.range?.start?.character ?? 0,
5108
5676
  severity: d.severity ?? 1,
5109
5677
  message: String(d.message ?? "")
5110
- }))
5111
- );
5112
- }
5678
+ }));
5679
+ const list2 = diagWaiters.get(uri);
5680
+ if (list2?.length) {
5681
+ diagWaiters.delete(uri);
5682
+ for (const w of list2) w(diags);
5683
+ }
5684
+ }
5685
+ });
5686
+ proc.stdout?.on("data", (c) => parser.push(c));
5687
+ proc.on("error", () => {
5688
+ resetState();
5689
+ settleInit();
5690
+ });
5691
+ proc.on("close", () => {
5692
+ resetState();
5693
+ settleInit();
5694
+ });
5695
+ send({
5696
+ jsonrpc: "2.0",
5697
+ id: 1,
5698
+ method: "initialize",
5699
+ params: { processId: process.pid, capabilities: {}, rootUri: pathToFileURL(opts.cwd).href }
5700
+ });
5113
5701
  });
5114
- child.stdout?.on("data", (c) => parser.push(c));
5115
- child.on("error", () => finish([]));
5116
- child.on("close", () => finish([]));
5117
- send({
5118
- jsonrpc: "2.0",
5119
- id: 1,
5120
- method: "initialize",
5121
- params: { processId: process.pid, capabilities: {}, rootUri: pathToFileURL(opts.cwd).href }
5702
+ return initPromise;
5703
+ };
5704
+ const syncDocument = (file) => {
5705
+ const uri = pathToFileURL(file).href;
5706
+ let text = "";
5707
+ try {
5708
+ text = readFileSync12(file, "utf8");
5709
+ } catch {
5710
+ }
5711
+ const prevVersion = opened.get(uri);
5712
+ if (prevVersion === void 0) {
5713
+ opened.set(uri, 1);
5714
+ send({
5715
+ jsonrpc: "2.0",
5716
+ method: "textDocument/didOpen",
5717
+ params: { textDocument: { uri, languageId: languageIdFor(file), version: 1, text } }
5718
+ });
5719
+ } else {
5720
+ const version = prevVersion + 1;
5721
+ opened.set(uri, version);
5722
+ send({
5723
+ jsonrpc: "2.0",
5724
+ method: "textDocument/didChange",
5725
+ params: {
5726
+ textDocument: { uri, version },
5727
+ contentChanges: [{ text }]
5728
+ }
5729
+ });
5730
+ }
5731
+ return uri;
5732
+ };
5733
+ const request = (method, params, timeoutMs = opts.timeoutMs ?? 8e3) => new Promise((resolve5) => {
5734
+ const id = nextId++;
5735
+ const timer = setTimeout(() => {
5736
+ pendingRpc.delete(id);
5737
+ resolve5(void 0);
5738
+ }, timeoutMs);
5739
+ pendingRpc.set(id, (res) => {
5740
+ clearTimeout(timer);
5741
+ resolve5(res);
5122
5742
  });
5743
+ send({ jsonrpc: "2.0", id, method, params });
5123
5744
  });
5745
+ return {
5746
+ async diagnostics(file, timeoutMs = opts.timeoutMs ?? 8e3) {
5747
+ await ensureStarted();
5748
+ if (!child) return [];
5749
+ const uri = pathToFileURL(file).href;
5750
+ return new Promise((resolve5) => {
5751
+ const timer = setTimeout(() => {
5752
+ const list3 = diagWaiters.get(uri) ?? [];
5753
+ diagWaiters.set(
5754
+ uri,
5755
+ list3.filter((w) => w !== onDiag)
5756
+ );
5757
+ resolve5([]);
5758
+ }, timeoutMs);
5759
+ const onDiag = (diags) => {
5760
+ clearTimeout(timer);
5761
+ resolve5(diags);
5762
+ };
5763
+ const list2 = diagWaiters.get(uri) ?? [];
5764
+ list2.push(onDiag);
5765
+ diagWaiters.set(uri, list2);
5766
+ syncDocument(file);
5767
+ });
5768
+ },
5769
+ async hover(file, line, character, timeoutMs) {
5770
+ await ensureStarted();
5771
+ if (!child) return "";
5772
+ const uri = syncDocument(file);
5773
+ const res = await request("textDocument/hover", { textDocument: { uri }, position: { line, character } }, timeoutMs);
5774
+ return extractHoverText(res?.contents);
5775
+ },
5776
+ async definition(file, line, character, timeoutMs) {
5777
+ await ensureStarted();
5778
+ if (!child) return [];
5779
+ const uri = syncDocument(file);
5780
+ const res = await request("textDocument/definition", { textDocument: { uri }, position: { line, character } }, timeoutMs);
5781
+ return normalizeLocations(res);
5782
+ },
5783
+ async references(file, line, character, timeoutMs) {
5784
+ await ensureStarted();
5785
+ if (!child) return [];
5786
+ const uri = syncDocument(file);
5787
+ const res = await request(
5788
+ "textDocument/references",
5789
+ { textDocument: { uri }, position: { line, character }, context: { includeDeclaration: true } },
5790
+ timeoutMs
5791
+ );
5792
+ return normalizeLocations(res);
5793
+ },
5794
+ async close() {
5795
+ if (!child) return;
5796
+ const proc = child;
5797
+ send({ jsonrpc: "2.0", id: nextId++, method: "shutdown" });
5798
+ send({ jsonrpc: "2.0", method: "exit" });
5799
+ resetState();
5800
+ const killer = setTimeout(() => {
5801
+ try {
5802
+ proc.kill();
5803
+ } catch {
5804
+ }
5805
+ }, 200);
5806
+ killer.unref?.();
5807
+ }
5808
+ };
5124
5809
  }
5125
5810
 
5126
5811
  // src/tools/diagnostics.ts
5127
5812
  var SEVERITY = { 1: "error", 2: "warning", 3: "info", 4: "hint" };
5128
- var SCHEMA = z17.object({ path: z17.string().describe("File to check (absolute or relative to the working directory)") });
5813
+ var SCHEMA = z17.object({
5814
+ path: z17.string().describe("File to check (absolute or relative to the working directory)"),
5815
+ action: z17.enum(["diagnostics", "hover", "definition", "references"]).optional().describe("LSP query action, default diagnostics"),
5816
+ line: z17.number().int().min(1).optional().describe("1-based line number for hover, definition, or references"),
5817
+ character: z17.number().int().min(1).optional().describe("1-based column number for hover, definition, or references")
5818
+ });
5129
5819
  function createDiagnosticsTool(servers) {
5820
+ const warmPool = /* @__PURE__ */ new Map();
5821
+ const clientFor = (server, cwd) => {
5822
+ const key = `${cwd}::${server.command}::${(server.args ?? []).join("\0")}`;
5823
+ let client = warmPool.get(key);
5824
+ if (!client) {
5825
+ client = createWarmLspClient({ server, cwd, timeoutMs: 15e3 });
5826
+ warmPool.set(key, client);
5827
+ }
5828
+ return client;
5829
+ };
5130
5830
  return defineTool({
5131
5831
  name: "diagnostics",
5132
- description: 'Run the configured LSP server(s) on one file and return its diagnostics (errors/warnings). Configure servers under "lsp.servers" in ~/.bccli/config.json, e.g. { "extensions": [".ts"], "command": "typescript-language-server", "args": ["--stdio"] }.',
5832
+ description: 'Query the configured LSP server(s) on a file for diagnostics (errors/warnings), hover docs, definition, or references. Configure servers under "lsp.servers" in ~/.bccli/config.json, e.g. { "extensions": [".ts"], "command": "typescript-language-server", "args": ["--stdio"] }.',
5133
5833
  schema: SCHEMA,
5134
5834
  kind: "read",
5135
5835
  target: (input) => input.path,
5136
5836
  async run(input, ctx) {
5137
5837
  const file = resolvePath(ctx.cwd, input.path);
5138
- const ext = extname2(file).toLowerCase();
5838
+ const ext = extname3(file).toLowerCase();
5139
5839
  const server = servers.find((s) => s.extensions.includes(ext));
5140
5840
  if (!server) {
5141
5841
  return { output: t("No LSP server is configured for {ext} files.", { ext: ext || "(none)" }), isError: true };
5142
5842
  }
5143
- const diags = await getDiagnostics({ server, cwd: ctx.cwd, file, timeoutMs: 15e3 });
5843
+ const client = clientFor(server, ctx.cwd);
5844
+ const action = input.action ?? "diagnostics";
5845
+ const line0 = Math.max(0, (input.line ?? 1) - 1);
5846
+ const char0 = Math.max(0, (input.character ?? 1) - 1);
5847
+ if (action === "hover") {
5848
+ const text = await client.hover(file, line0, char0, 15e3);
5849
+ return { output: text || "No hover information." };
5850
+ }
5851
+ if (action === "definition" || action === "references") {
5852
+ const locs = action === "definition" ? await client.definition(file, line0, char0, 15e3) : await client.references(file, line0, char0, 15e3);
5853
+ if (!locs.length) return { output: `No ${action} found.` };
5854
+ const formatted = locs.map((l) => {
5855
+ const rel = relative3(ctx.cwd, l.path).replace(/\\/g, "/") || input.path;
5856
+ return `${rel}:${l.line + 1}:${l.character + 1}`;
5857
+ });
5858
+ return { output: formatted.join("\n") };
5859
+ }
5860
+ const diags = await client.diagnostics(file, 15e3);
5144
5861
  if (!diags.length) return { output: "No diagnostics." };
5145
5862
  const lines = diags.map((d) => `${input.path}:${d.line + 1}:${d.character + 1} ${SEVERITY[d.severity] ?? "error"} ${d.message}`);
5146
5863
  return { output: lines.join("\n"), isError: diags.some((d) => d.severity === 1) };
@@ -5151,7 +5868,7 @@ function createDiagnosticsTool(servers) {
5151
5868
  // src/plugins.ts
5152
5869
  import { existsSync as existsSync13 } from "fs";
5153
5870
  import { createRequire } from "module";
5154
- import { isAbsolute as isAbsolute3, join as join11 } from "path";
5871
+ import { isAbsolute as isAbsolute3, join as join13 } from "path";
5155
5872
  import { z as z18 } from "zod";
5156
5873
  var require_ = createRequire(import.meta.url);
5157
5874
  function pluginLog(message) {
@@ -5176,7 +5893,7 @@ function loadPlugins(specs, opts) {
5176
5893
  for (const spec of specs) {
5177
5894
  let file;
5178
5895
  try {
5179
- file = isAbsolute3(spec) ? spec : spec.startsWith(".") ? join11(opts.home, spec) : require_.resolve(spec);
5896
+ file = isAbsolute3(spec) ? spec : spec.startsWith(".") ? join13(opts.home, spec) : require_.resolve(spec);
5180
5897
  } catch {
5181
5898
  throw new ConfigError(t("Plugin not found: {spec}", { spec }));
5182
5899
  }
@@ -5253,7 +5970,7 @@ function createRuntime(opts) {
5253
5970
  let config = loadConfig(opts.cwd, env);
5254
5971
  let modelRef = opts.args.model ?? config.model;
5255
5972
  let reasoning = opts.args.reasoning ?? config.reasoning;
5256
- const makeProvider = (ref) => {
5973
+ const makeSingleProvider = (ref) => {
5257
5974
  const resolved = resolveModel(config, ref, env);
5258
5975
  return opts.provider ?? createProvider({
5259
5976
  baseURL: resolved.baseURL,
@@ -5265,6 +5982,25 @@ function createRuntime(opts) {
5265
5982
  enableProgrammaticToolCalling: lunaPtcAutoEnabled(env)
5266
5983
  });
5267
5984
  };
5985
+ const makeProvider = (ref) => {
5986
+ const primary = makeSingleProvider(ref);
5987
+ if (opts.provider) return primary;
5988
+ const fallbackRefs = [
5989
+ ...opts.args.fallbackModel ? [opts.args.fallbackModel] : [],
5990
+ ...config.fallbackModels ?? []
5991
+ ].filter((r) => r && r !== ref);
5992
+ if (!fallbackRefs.length) return primary;
5993
+ return withFallbackProvider(
5994
+ primary,
5995
+ () => fallbackRefs.flatMap((fbRef) => {
5996
+ try {
5997
+ return [makeSingleProvider(fbRef)];
5998
+ } catch {
5999
+ return [];
6000
+ }
6001
+ })
6002
+ );
6003
+ };
5268
6004
  const provider = makeProvider(modelRef);
5269
6005
  const mode = opts.args.allowAll ? "allowAll" : opts.args.permissionMode ?? config.permissionMode;
5270
6006
  const rules = [...config.allow, ...opts.args.allowedTools.map((t2) => TOOL_RULES[t2] ?? t2)];
@@ -5286,6 +6022,10 @@ function createRuntime(opts) {
5286
6022
  if (latest) {
5287
6023
  session = latest;
5288
6024
  history = latest.load();
6025
+ if (!opts.args.model) {
6026
+ const prevModel = latest.lastModel();
6027
+ if (prevModel) modelRef = prevModel;
6028
+ }
5289
6029
  }
5290
6030
  }
5291
6031
  const repoMap = config.repoMap ? buildRepoMapSync(opts.cwd) : void 0;
@@ -5329,7 +6069,7 @@ function createRuntime(opts) {
5329
6069
  // Subagents get whatever the main agent has right now (incl. MCP tools), minus task itself.
5330
6070
  baseTools: () => agent.tools.filter((t2) => t2.name !== "task"),
5331
6071
  permissions,
5332
- provider: () => agent.provider,
6072
+ provider: () => opts.provider ? agent.provider : makeProvider(modelRef),
5333
6073
  providerFor: makeProvider,
5334
6074
  systemPrompt: (childModelRef) => systemFor(childModelRef ?? modelRef),
5335
6075
  cwd: opts.cwd,
@@ -5415,6 +6155,20 @@ function createRuntime(opts) {
5415
6155
  session = previous;
5416
6156
  agent.load(previous.load());
5417
6157
  agent.totalUsage = { inputTokens: 0, outputTokens: 0 };
6158
+ if (!opts.args.model) {
6159
+ const prevModel = previous.lastModel();
6160
+ if (prevModel) {
6161
+ try {
6162
+ const nextProvider = makeProvider(prevModel);
6163
+ modelRef = prevModel;
6164
+ agent.provider = nextProvider;
6165
+ agent.setBudgetModel(prevModel);
6166
+ agent.setContextWindow(contextWindowFor(prevModel, env));
6167
+ agent.setSystemPrompt(systemFor(modelRef));
6168
+ } catch {
6169
+ }
6170
+ }
6171
+ }
5418
6172
  todos.set([]);
5419
6173
  checkpoints.clear();
5420
6174
  turnMarks.length = 0;
@@ -5463,6 +6217,7 @@ function createRuntime(opts) {
5463
6217
  const nextProvider = makeProvider(ref);
5464
6218
  modelRef = ref;
5465
6219
  agent.provider = nextProvider;
6220
+ agent.setBudgetModel(ref);
5466
6221
  agent.setContextWindow(contextWindowFor(ref, env));
5467
6222
  agent.setSystemPrompt(systemFor(modelRef));
5468
6223
  },
@@ -5488,6 +6243,7 @@ async function* streamTask(opts) {
5488
6243
  let finished = false;
5489
6244
  let text = "";
5490
6245
  let stopReason = "done";
6246
+ let errorMessage2;
5491
6247
  const usage = { inputTokens: 0, outputTokens: 0 };
5492
6248
  const toolCalls = [];
5493
6249
  const byId = /* @__PURE__ */ new Map();
@@ -5537,28 +6293,45 @@ async function* streamTask(opts) {
5537
6293
  case "aborted":
5538
6294
  stopReason = "aborted";
5539
6295
  break;
6296
+ case "error":
6297
+ stopReason = "error";
6298
+ errorMessage2 = event.message;
6299
+ push({ type: "error", message: event.message });
6300
+ break;
5540
6301
  default:
5541
6302
  break;
5542
6303
  }
5543
6304
  };
5544
- const running = rt.agent.run(opts.prompt, opts.signal ?? new AbortController().signal).catch(() => {
5545
- stopReason = "aborted";
6305
+ const controller = new AbortController();
6306
+ const onParentAbort = () => controller.abort();
6307
+ if (opts.signal) {
6308
+ if (opts.signal.aborted) controller.abort();
6309
+ else opts.signal.addEventListener("abort", onParentAbort, { once: true });
6310
+ }
6311
+ const running = rt.agent.run(opts.prompt, controller.signal).catch((err) => {
6312
+ stopReason = "error";
6313
+ errorMessage2 = err instanceof Error ? err.message : String(err);
5546
6314
  }).finally(() => {
5547
6315
  finished = true;
5548
6316
  wake?.();
5549
6317
  });
5550
- while (!finished || queue.length) {
5551
- if (queue.length) {
5552
- yield queue.shift();
5553
- continue;
6318
+ try {
6319
+ while (!finished || queue.length) {
6320
+ if (queue.length) {
6321
+ yield queue.shift();
6322
+ continue;
6323
+ }
6324
+ await new Promise((resolve5) => {
6325
+ wake = resolve5;
6326
+ });
6327
+ wake = null;
5554
6328
  }
5555
- await new Promise((resolve4) => {
5556
- wake = resolve4;
5557
- });
5558
- wake = null;
6329
+ await running;
6330
+ yield { type: "result", result: { text, toolCalls, usage, stopReason, ...errorMessage2 ? { error: errorMessage2 } : {} } };
6331
+ } finally {
6332
+ opts.signal?.removeEventListener("abort", onParentAbort);
6333
+ if (!finished) controller.abort();
5559
6334
  }
5560
- await running;
5561
- yield { type: "result", result: { text, toolCalls, usage, stopReason } };
5562
6335
  }
5563
6336
  async function runTask(opts) {
5564
6337
  let out = { text: "", toolCalls: [], usage: { inputTokens: 0, outputTokens: 0 }, stopReason: "done" };