@botconnector/bccli 0.4.1 → 0.4.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (69) hide show
  1. package/dist/{acp-3PHBAI64.js → acp-UYC6APR5.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-BKGFA2PT.js → chunk-266SY3MC.js} +1 -1
  8. package/dist/{chunk-MFHCQQEN.js → chunk-3I5BW3ZT.js} +3 -2
  9. package/dist/{chunk-UZWJQF67.js → chunk-3WHI4LN3.js} +2 -1
  10. package/dist/{chunk-WNACAUDK.js → chunk-5YJ3ZQVR.js} +66 -9
  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-R5RE46CW.js → chunk-C64KM37J.js} +36 -6
  14. package/dist/{chunk-GN4W3T6E.js → chunk-H7XJDKJY.js} +4 -4
  15. package/dist/{chunk-JHPGOAXU.js → chunk-HQW4BROS.js} +4 -5
  16. package/dist/{chunk-75BECX4F.js → chunk-IYR5PYA3.js} +4 -4
  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-R72OV7RF.js} +421 -108
  22. package/dist/{chunk-FLQPE4S5.js → chunk-RX4UQX43.js} +4 -1
  23. package/dist/{chunk-BC7UXFE4.js → chunk-SVD4LHJC.js} +64 -3
  24. package/dist/{chunk-BXQQCUAC.js → chunk-TIMSYZIR.js} +7 -7
  25. package/dist/{chunk-MHIYLXJC.js → chunk-WFZKJY3R.js} +8 -4
  26. package/dist/{chunk-BGXNR74O.js → chunk-ZEPTUKNW.js} +119 -31
  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-TSLRORJJ.js} +21 -20
  33. package/dist/{integrationCli-Q2YPDTPG.js → integrationCli-HXCSEWH2.js} +2 -2
  34. package/dist/{login-FP3BB44A.js → login-RYESFZG6.js} +4 -4
  35. package/dist/lsp.d.ts +3 -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-OP7CDZC7.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-5OZI2GQQ.js +15 -0
  46. package/dist/sdk.d.ts +5 -1
  47. package/dist/sdk.js +733 -176
  48. package/dist/{serve-X2ZV4S5U.js → serve-4J7HA4JO.js} +17 -17
  49. package/dist/{serve-KWOECK5W.js → serve-K5AH4MGO.js} +3 -3
  50. package/dist/session-YQXCCKXI.js +12 -0
  51. package/dist/session.d.ts +3 -0
  52. package/dist/setup-S4AJT6XE.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/edit.d.ts +7 -2
  57. package/dist/tools/grep.d.ts +14 -2
  58. package/dist/tools/ignore.d.ts +2 -2
  59. package/dist/tools/paths.d.ts +2 -0
  60. package/dist/{transferCli-YUFPI6AH.js → transferCli-R2KEIWQR.js} +8 -6
  61. package/dist/ui/complete.d.ts +5 -0
  62. package/dist/ui/vim.d.ts +1 -1
  63. package/dist/{ui-IVIFNUCC.js → ui-36ZEK65V.js} +177 -54
  64. package/dist/{update-TZ2G6KQ3.js → update-FSS7X5WV.js} +2 -2
  65. package/package.json +1 -1
  66. package/dist/args-W6BNKY7K.js +0 -13
  67. package/dist/providerCli-NMUFXAAS.js +0 -15
  68. package/dist/session-GKEXDJKZ.js +0 -12
  69. 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.2";
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.2"
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,7 +2262,9 @@ 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
@@ -2383,7 +2453,7 @@ function offlineEnv(env = process.env) {
2383
2453
  };
2384
2454
  }
2385
2455
  function runCommand(command, opts) {
2386
- return new Promise((resolve4) => {
2456
+ return new Promise((resolve5) => {
2387
2457
  const child = spawnChild(command, opts.cwd, opts.env, opts.shell ?? defaultShell());
2388
2458
  if (!exitHookInstalled) {
2389
2459
  process.on("exit", killAllCommands);
@@ -2396,7 +2466,14 @@ function runCommand(command, opts) {
2396
2466
  let dropped = 0;
2397
2467
  let timedOut = false;
2398
2468
  let aborted = false;
2469
+ const fullChunks = [];
2470
+ let fullBytes = 0;
2471
+ const MAX_SPILL_BYTES = 10 * 1024 * 1024;
2399
2472
  const onData = (data) => {
2473
+ if (fullBytes < MAX_SPILL_BYTES) {
2474
+ fullChunks.push(data);
2475
+ fullBytes += data.length;
2476
+ }
2400
2477
  let s = data;
2401
2478
  if (head.length < HALF) {
2402
2479
  const take = s.slice(0, HALF - head.length);
@@ -2435,10 +2512,19 @@ function runCommand(command, opts) {
2435
2512
  active.delete(killAsAborted);
2436
2513
  clearTimeout(timer);
2437
2514
  opts.signal.removeEventListener("abort", onAbort);
2515
+ let spillNote = "";
2516
+ if (dropped && fullChunks.length) {
2517
+ try {
2518
+ const spillPath = join2(tmpdir(), `bccli-bash-${Date.now()}-${Math.random().toString(36).slice(2, 8)}.log`);
2519
+ writeFileSync2(spillPath, fullChunks.join(""), "utf8");
2520
+ spillNote = `; full output saved to ${spillPath}`;
2521
+ } catch {
2522
+ }
2523
+ }
2438
2524
  const output = dropped ? `${head}
2439
- \u2026 [${dropped} characters truncated] \u2026
2525
+ \u2026 [${dropped} characters truncated${spillNote}] \u2026
2440
2526
  ${tail}` : head + tail;
2441
- resolve4({ output: output + extra, exitCode, timedOut, aborted });
2527
+ resolve5({ output: output + extra, exitCode, timedOut, aborted });
2442
2528
  };
2443
2529
  child.on("error", (error) => finish(null, error.message));
2444
2530
  child.on("close", (code) => finish(code));
@@ -2560,46 +2646,139 @@ function formatDiff(lines) {
2560
2646
  }
2561
2647
 
2562
2648
  // src/tools/paths.ts
2649
+ import { statSync } from "fs";
2563
2650
  import { isAbsolute as isAbsolute2, resolve as resolve2 } from "path";
2651
+ var readMtimes = /* @__PURE__ */ new WeakMap();
2564
2652
  function resolvePath(cwd, path) {
2565
2653
  return isAbsolute2(path) ? path : resolve2(cwd, path);
2566
2654
  }
2567
2655
  function errorCode(error) {
2568
2656
  return error.code ?? error.message;
2569
2657
  }
2658
+ function recordFileMtime(readFiles, abs) {
2659
+ readFiles.add(abs);
2660
+ try {
2661
+ let map = readMtimes.get(readFiles);
2662
+ if (!map) {
2663
+ map = /* @__PURE__ */ new Map();
2664
+ readMtimes.set(readFiles, map);
2665
+ }
2666
+ map.set(abs, statSync(abs).mtimeMs);
2667
+ } catch {
2668
+ }
2669
+ }
2670
+ function isFileModifiedSinceRead(readFiles, abs) {
2671
+ const recorded = readMtimes.get(readFiles)?.get(abs);
2672
+ if (recorded === void 0) return false;
2673
+ try {
2674
+ return statSync(abs).mtimeMs > recorded + 50;
2675
+ } catch {
2676
+ return false;
2677
+ }
2678
+ }
2570
2679
 
2571
2680
  // src/tools/edit.ts
2572
- var schema = z2.object({
2573
- path: z2.string().describe("File path, relative to the project or absolute"),
2681
+ var editEntrySchema = z2.object({
2574
2682
  old_string: z2.string().describe("Exact text to replace; must be unique unless replace_all"),
2575
2683
  new_string: z2.string().describe("Replacement text"),
2576
2684
  replace_all: z2.boolean().optional().describe("Replace every occurrence")
2577
2685
  });
2686
+ var schema = z2.object({
2687
+ path: z2.string().describe("File path, relative to the project or absolute"),
2688
+ old_string: z2.string().optional().describe("Exact text to replace; must be unique unless replace_all"),
2689
+ new_string: z2.string().optional().describe("Replacement text"),
2690
+ replace_all: z2.boolean().optional().describe("Replace every occurrence"),
2691
+ edits: z2.array(editEntrySchema).optional().describe("Optional list of sequential edits to apply atomically to the same file")
2692
+ });
2578
2693
  function countOccurrences(haystack, needle) {
2579
2694
  let count = 0;
2580
2695
  for (let i = haystack.indexOf(needle); i !== -1; i = haystack.indexOf(needle, i + needle.length)) count++;
2581
2696
  return count;
2582
2697
  }
2583
- function applyEdit(content, input) {
2698
+ function normalizeTypographic(s) {
2699
+ return s.replace(/[\u2018\u2019\u201A\u201B]/g, "'").replace(/[\u201C\u201D\u201E\u201F]/g, '"').replace(/[\u2013\u2014]/g, "-").replace(/\u00A0/g, " ");
2700
+ }
2701
+ function stripReadLinePrefixes(s) {
2702
+ const lines = s.split(/\r?\n/);
2703
+ if (lines.every((l) => !l.trim() || /^\s*\d+\t/.test(l))) {
2704
+ return lines.map((l) => l.replace(/^\s*\d+\t/, "")).join("\n");
2705
+ }
2706
+ return s;
2707
+ }
2708
+ function matchByNormalizedLines(content, oldText, newText, replaceAll) {
2709
+ const eol = content.includes("\r\n") ? "\r\n" : "\n";
2710
+ const fileLines = content.split(/\r?\n/);
2711
+ const oldLines = stripReadLinePrefixes(oldText).split(/\r?\n/);
2712
+ if (!oldLines.length) return { count: 0 };
2713
+ const normLine = (l) => normalizeTypographic(l).trimEnd();
2714
+ const normOld = oldLines.map(normLine);
2715
+ const matches = [];
2716
+ for (let i = 0; i + oldLines.length <= fileLines.length; i++) {
2717
+ let ok = true;
2718
+ for (let j = 0; j < oldLines.length; j++) {
2719
+ if (normLine(fileLines[i + j]) !== normOld[j]) {
2720
+ ok = false;
2721
+ break;
2722
+ }
2723
+ }
2724
+ if (ok) {
2725
+ matches.push(i);
2726
+ if (replaceAll) i += oldLines.length - 1;
2727
+ }
2728
+ }
2729
+ if (matches.length === 0) return { count: 0 };
2730
+ if (matches.length > 1 && !replaceAll) return { count: matches.length };
2731
+ const replacementLines = stripReadLinePrefixes(newText).split(/\r?\n/);
2732
+ const outLines = [...fileLines];
2733
+ for (let m = matches.length - 1; m >= 0; m--) {
2734
+ outLines.splice(matches[m], oldLines.length, ...replacementLines);
2735
+ }
2736
+ return { count: matches.length, result: outLines.join(eol) };
2737
+ }
2738
+ function applySingleEdit(content, path, input) {
2584
2739
  if (input.old_string === input.new_string) {
2585
2740
  return { output: "old_string and new_string are identical; nothing to change.", isError: true };
2586
2741
  }
2587
2742
  const eol = content.includes("\r\n") ? "\r\n" : "\n";
2588
2743
  const toEol = (s) => s.replace(/\r?\n/g, eol);
2744
+ const strippedOld = stripReadLinePrefixes(input.old_string);
2745
+ const strippedNew = stripReadLinePrefixes(input.new_string);
2589
2746
  const attempts = [[input.old_string, input.new_string]];
2590
2747
  if (toEol(input.old_string) !== input.old_string) attempts.push([toEol(input.old_string), toEol(input.new_string)]);
2748
+ if (strippedOld !== input.old_string) {
2749
+ attempts.push([strippedOld, strippedNew], [toEol(strippedOld), toEol(strippedNew)]);
2750
+ }
2591
2751
  for (const [oldText, newText] of attempts) {
2592
2752
  const count = oldText ? countOccurrences(content, oldText) : 0;
2593
2753
  if (count === 0) continue;
2594
2754
  if (count > 1 && !input.replace_all) {
2595
2755
  return {
2596
- output: `old_string appears ${count} times in ${input.path}. Add more context to make it unique, or use replace_all.`,
2756
+ output: `old_string appears ${count} times in ${path}. Add more context to make it unique, or use replace_all.`,
2597
2757
  isError: true
2598
2758
  };
2599
2759
  }
2600
2760
  return input.replace_all ? content.split(oldText).join(newText) : content.replace(oldText, () => newText);
2601
2761
  }
2602
- return { output: `old_string not found in ${input.path}. Re-read the file.`, isError: true };
2762
+ const fuzzy = matchByNormalizedLines(content, input.old_string, input.new_string, input.replace_all);
2763
+ if (fuzzy.count > 1 && !input.replace_all) {
2764
+ return {
2765
+ output: `old_string appears ${fuzzy.count} times in ${path}. Add more context to make it unique, or use replace_all.`,
2766
+ isError: true
2767
+ };
2768
+ }
2769
+ if (fuzzy.count > 0 && fuzzy.result !== void 0) return fuzzy.result;
2770
+ return { output: `old_string not found in ${path}. Re-read the file.`, isError: true };
2771
+ }
2772
+ function applyEdit(content, input) {
2773
+ 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 }] : [];
2774
+ if (!list2.length) return { output: "Provide old_string and new_string (or edits array).", isError: true };
2775
+ let current2 = content;
2776
+ for (const item of list2) {
2777
+ const next = applySingleEdit(current2, input.path, item);
2778
+ if (typeof next !== "string") return next;
2779
+ current2 = next;
2780
+ }
2781
+ return current2;
2603
2782
  }
2604
2783
  var editTool = defineTool({
2605
2784
  name: "edit",
@@ -2610,6 +2789,9 @@ var editTool = defineTool({
2610
2789
  async validate(input, ctx) {
2611
2790
  const abs = resolvePath(ctx.cwd, input.path);
2612
2791
  if (!ctx.readFiles.has(abs)) return `Read ${input.path} with read before editing it.`;
2792
+ if (isFileModifiedSinceRead(ctx.readFiles, abs)) {
2793
+ return `${input.path} has been modified since it was last read. Read it again before editing.`;
2794
+ }
2613
2795
  let content;
2614
2796
  try {
2615
2797
  content = await readFile(abs, "utf8");
@@ -2630,6 +2812,9 @@ var editTool = defineTool({
2630
2812
  if (!ctx.readFiles.has(abs)) {
2631
2813
  return { output: `Read ${input.path} with read before editing it.`, isError: true };
2632
2814
  }
2815
+ if (isFileModifiedSinceRead(ctx.readFiles, abs)) {
2816
+ return { output: `${input.path} has been modified since it was last read. Read it again before editing.`, isError: true };
2817
+ }
2633
2818
  let content;
2634
2819
  try {
2635
2820
  content = await readFile(abs, "utf8");
@@ -2640,6 +2825,7 @@ var editTool = defineTool({
2640
2825
  if (typeof next !== "string") return next;
2641
2826
  await ctx.checkpoint?.(abs);
2642
2827
  await writeFile(abs, next);
2828
+ recordFileMtime(ctx.readFiles, abs);
2643
2829
  return { output: `Edited ${input.path}.`, display: formatDiff(diffLines(content, next)) };
2644
2830
  }
2645
2831
  });
@@ -2911,20 +3097,22 @@ async function loadPage(inputUrl, signal, pinned, fetchPage) {
2911
3097
  var fetchTool = createFetchTool();
2912
3098
 
2913
3099
  // src/tools/glob.ts
3100
+ import { statSync as statSync2 } from "fs";
2914
3101
  import { glob as nativeGlob } from "fs/promises";
3102
+ import { join as join4 } from "path";
2915
3103
  import { z as z4 } from "zod";
2916
3104
 
2917
3105
  // src/abort.ts
2918
3106
  var ABORTED = /* @__PURE__ */ Symbol("aborted");
2919
3107
  function raceAbort(promise, signal) {
2920
3108
  if (signal.aborted) return Promise.resolve(ABORTED);
2921
- return new Promise((resolve4, reject) => {
2922
- const onAbort = () => resolve4(ABORTED);
3109
+ return new Promise((resolve5, reject) => {
3110
+ const onAbort = () => resolve5(ABORTED);
2923
3111
  signal.addEventListener("abort", onAbort, { once: true });
2924
3112
  promise.then(
2925
3113
  (value) => {
2926
3114
  signal.removeEventListener("abort", onAbort);
2927
- resolve4(value);
3115
+ resolve5(value);
2928
3116
  },
2929
3117
  (error) => {
2930
3118
  signal.removeEventListener("abort", onAbort);
@@ -2950,22 +3138,38 @@ async function collectUntilAbort(iterable, signal, onItem) {
2950
3138
 
2951
3139
  // src/tools/ignore.ts
2952
3140
  import { readFileSync as readFileSync2 } from "fs";
2953
- import { join as join2 } from "path";
2954
- function ignorePatterns(root) {
3141
+ import { dirname as dirname2, join as join3, resolve as resolve3 } from "path";
3142
+ function ignorePatterns(root, projectRoot) {
2955
3143
  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}/**`);
3144
+ const seen = /* @__PURE__ */ new Set();
3145
+ const dirs2 = [];
3146
+ let cur = resolve3(root);
3147
+ const stop = projectRoot ? resolve3(projectRoot) : cur;
3148
+ while (true) {
3149
+ if (!seen.has(cur)) {
3150
+ seen.add(cur);
3151
+ dirs2.push(cur);
3152
+ }
3153
+ if (cur === stop || !cur.startsWith(stop)) break;
3154
+ const parent = dirname2(cur);
3155
+ if (parent === cur) break;
3156
+ cur = parent;
3157
+ }
3158
+ for (const dir of dirs2) {
3159
+ let lines = [];
3160
+ try {
3161
+ lines = readFileSync2(join3(dir, ".gitignore"), "utf8").split(/\r?\n/);
3162
+ } catch {
3163
+ continue;
3164
+ }
3165
+ for (const raw of lines) {
3166
+ const line = raw.trim();
3167
+ if (!line || line.startsWith("#") || line.startsWith("!")) continue;
3168
+ const p = line.replace(/^\//, "").replace(/\/$/, "");
3169
+ patterns.push(`**/${p}`, `**/${p}/**`);
3170
+ }
2967
3171
  }
2968
- return patterns;
3172
+ return [...new Set(patterns)];
2969
3173
  }
2970
3174
 
2971
3175
  // src/tools/globSafety.ts
@@ -3007,19 +3211,30 @@ var globTool = defineTool({
3007
3211
  path: z4.string().optional().describe("Base directory, default project root")
3008
3212
  }),
3009
3213
  kind: "read",
3010
- target: (input) => input.pattern,
3214
+ target: (input) => input.path ? `${input.pattern} ${input.path}` : input.pattern,
3011
3215
  async run(input, ctx) {
3012
3216
  const invalid = validateGlobPattern(input.pattern);
3013
3217
  if (invalid) return { output: invalid, isError: true };
3014
3218
  const root = resolvePath(ctx.cwd, input.path ?? ".");
3015
3219
  const files = [];
3016
3220
  const finished = await collectUntilAbort(
3017
- nativeGlob(input.pattern, { cwd: root, exclude: ignorePatterns(root) }),
3221
+ nativeGlob(input.pattern, { cwd: root, exclude: ignorePatterns(root, ctx.cwd) }),
3018
3222
  ctx.signal,
3019
3223
  (file) => files.push(normalizeGlobPath(file))
3020
3224
  );
3021
3225
  if (!finished) return { output: "Cancelled by the user.", isError: true };
3022
- files.sort();
3226
+ const mtime = (rel) => {
3227
+ try {
3228
+ return statSync2(join4(root, rel)).mtimeMs;
3229
+ } catch {
3230
+ return 0;
3231
+ }
3232
+ };
3233
+ const mtimes = new Map(files.map((f) => [f, mtime(f)]));
3234
+ files.sort((a, b) => {
3235
+ const diff = (mtimes.get(b) ?? 0) - (mtimes.get(a) ?? 0);
3236
+ return Math.abs(diff) >= 50 ? diff : a.localeCompare(b);
3237
+ });
3023
3238
  if (!files.length) return { output: "No matching files." };
3024
3239
  const first = files.slice(0, 500);
3025
3240
  const shown = [];
@@ -3040,75 +3255,141 @@ var globTool = defineTool({
3040
3255
 
3041
3256
  // src/tools/grep.ts
3042
3257
  import { execFile } from "child_process";
3258
+ import { statSync as statSync3 } from "fs";
3043
3259
  import { readFile as readFile2 } from "fs/promises";
3044
3260
  import { glob as nativeGlob2 } from "fs/promises";
3261
+ import { basename, dirname as dirname3 } from "path";
3045
3262
  import { z as z5 } from "zod";
3046
3263
  var MAX_MATCHES = 200;
3047
3264
  var MAX_OUTPUT_CHARS2 = 24e3;
3265
+ function escapeRegExp(s) {
3266
+ return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
3267
+ }
3268
+ function isSingleFile(p) {
3269
+ try {
3270
+ return statSync3(p).isFile();
3271
+ } catch {
3272
+ return false;
3273
+ }
3274
+ }
3048
3275
  async function grepJs(pattern, opts) {
3049
- const regex = new RegExp(pattern, opts.ignoreCase ? "i" : "");
3276
+ const source = opts.literal ? escapeRegExp(pattern) : pattern;
3277
+ const regex = new RegExp(source, opts.ignoreCase ? "i" : "");
3278
+ const single = isSingleFile(opts.root);
3050
3279
  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
- );
3280
+ if (single) {
3281
+ files.push(basename(opts.root));
3282
+ } else {
3283
+ const finished = await collectUntilAbort(
3284
+ nativeGlob2(opts.glob ?? "**/*", { cwd: opts.root, exclude: ignorePatterns(opts.root, opts.projectRoot) }),
3285
+ opts.signal,
3286
+ (file) => files.push(normalizeGlobPath(file))
3287
+ );
3288
+ if (!finished) return [];
3289
+ }
3290
+ const baseDir = single ? dirname3(opts.root) : opts.root;
3291
+ const ctxLines = Math.max(0, Math.min(10, Math.floor(opts.context ?? 0)));
3292
+ const mode = opts.outputMode ?? "content";
3056
3293
  const out = [];
3057
- if (!finished) return out;
3058
3294
  for (const file of files.sort()) {
3059
3295
  if (opts.signal?.aborted) break;
3060
3296
  let text;
3061
3297
  try {
3062
- const buffer = await readFile2(resolvePath(opts.root, file));
3298
+ const buffer = await readFile2(resolvePath(baseDir, file));
3063
3299
  if (buffer.subarray(0, 8e3).includes(0)) continue;
3064
3300
  text = buffer.toString("utf8");
3065
3301
  } catch {
3066
3302
  continue;
3067
3303
  }
3068
3304
  const lines = text.split(/\r?\n/);
3305
+ let fileMatches = 0;
3069
3306
  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)}`);
3307
+ if (regex.test(lines[i])) {
3308
+ fileMatches++;
3309
+ if (mode === "files_with_matches") {
3310
+ out.push(file);
3311
+ break;
3312
+ }
3313
+ if (mode === "content") {
3314
+ const from = Math.max(0, i - ctxLines);
3315
+ const to = Math.min(lines.length - 1, i + ctxLines);
3316
+ for (let j = from; j <= to && out.length < MAX_MATCHES; j++) {
3317
+ const sep2 = j === i ? ":" : "-";
3318
+ out.push(`${file}${sep2}${j + 1}${sep2}${lines[j].slice(0, 300)}`);
3319
+ }
3320
+ }
3321
+ }
3071
3322
  }
3323
+ if (mode === "count" && fileMatches > 0) out.push(`${file}:${fileMatches}`);
3072
3324
  if (out.length >= MAX_MATCHES) break;
3073
3325
  }
3074
3326
  return out;
3075
3327
  }
3076
3328
  function grepRg(pattern, opts) {
3077
- const args = ["--line-number", "--no-heading", "--color", "never", "--max-columns", "300", "-e", pattern];
3329
+ const single = isSingleFile(opts.root);
3330
+ const cwd = single ? dirname3(opts.root) : opts.root;
3331
+ const target = single ? basename(opts.root) : ".";
3332
+ const mode = opts.outputMode ?? "content";
3333
+ const args = ["--color", "never"];
3334
+ if (mode === "files_with_matches") {
3335
+ args.push("--files-with-matches");
3336
+ } else if (mode === "count") {
3337
+ args.push("--count");
3338
+ } else {
3339
+ args.push("--line-number", "--no-heading", "--max-columns", "300");
3340
+ if (opts.context && opts.context > 0) args.push("-C", String(Math.min(10, Math.floor(opts.context))));
3341
+ }
3342
+ if (opts.literal) args.push("-F");
3078
3343
  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([]);
3344
+ if (opts.glob && !single) args.push("-g", opts.glob);
3345
+ args.push("-e", pattern, target);
3346
+ return new Promise((resolve5) => {
3347
+ execFile("rg", args, { cwd, maxBuffer: 10 * 1024 * 1024, signal: opts.signal }, (error, stdout) => {
3348
+ if (opts.signal?.aborted) return resolve5([]);
3084
3349
  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));
3350
+ if (code === "ENOENT") return resolve5(void 0);
3351
+ if (error && error.code !== 1) return resolve5(void 0);
3352
+ resolve5(
3353
+ stdout.split(/\r?\n/).filter(Boolean).map((l) => l.replace(/^\.[\\/]/, "").replace(/\\/g, "/")).slice(0, MAX_MATCHES)
3354
+ );
3088
3355
  });
3089
3356
  });
3090
3357
  }
3091
3358
  var grepTool = defineTool({
3092
3359
  name: "grep",
3093
- description: "Search file contents with a regular expression. Respects .gitignore. Returns path:line:text (max 200).",
3360
+ description: "Search file contents with a regular expression or literal text. Respects .gitignore. Returns path:line:text (max 200).",
3094
3361
  schema: z5.object({
3095
- pattern: z5.string().describe("Regular expression"),
3096
- path: z5.string().optional().describe("Directory to search, default project root"),
3362
+ pattern: z5.string().describe("Regular expression or literal string"),
3363
+ path: z5.string().optional().describe("File or directory to search, default project root"),
3097
3364
  glob: z5.string().optional().describe('Only files matching this glob, e.g. "**/*.ts"'),
3098
- ignore_case: z5.boolean().optional()
3365
+ ignore_case: z5.boolean().optional(),
3366
+ literal: z5.boolean().optional().describe("Treat pattern as literal text instead of regex"),
3367
+ context: z5.number().int().min(0).max(10).optional().describe("Number of context lines before and after each match"),
3368
+ output_mode: z5.enum(["content", "files_with_matches", "count"]).optional().describe("Output format, default content")
3099
3369
  }),
3100
3370
  kind: "read",
3101
- target: (input) => input.pattern,
3371
+ target: (input) => input.path ? `${input.pattern} ${input.path}` : input.pattern,
3102
3372
  async run(input, ctx) {
3103
3373
  if (input.glob) {
3104
3374
  const invalid = validateGlobPattern(input.glob);
3105
3375
  if (invalid) return { output: invalid, isError: true };
3106
3376
  }
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 };
3377
+ const opts = {
3378
+ root: resolvePath(ctx.cwd, input.path ?? "."),
3379
+ projectRoot: ctx.cwd,
3380
+ glob: input.glob,
3381
+ ignoreCase: input.ignore_case,
3382
+ literal: input.literal,
3383
+ context: input.context,
3384
+ outputMode: input.output_mode,
3385
+ signal: ctx.signal
3386
+ };
3387
+ if (!input.literal) {
3388
+ try {
3389
+ new RegExp(input.pattern);
3390
+ } catch (error) {
3391
+ return { output: `Invalid regex: ${error.message}`, isError: true };
3392
+ }
3112
3393
  }
3113
3394
  const matches = await grepRg(input.pattern, opts) ?? await grepJs(input.pattern, opts);
3114
3395
  if (ctx.signal.aborted) return { output: "Cancelled by the user.", isError: true };
@@ -3130,6 +3411,7 @@ var grepTool = defineTool({
3130
3411
  });
3131
3412
 
3132
3413
  // src/tools/read.ts
3414
+ import { readdirSync, statSync as statSync4 } from "fs";
3133
3415
  import { readFile as readFile3 } from "fs/promises";
3134
3416
  import { z as z6 } from "zod";
3135
3417
  var MAX_LINE = 2e3;
@@ -3213,6 +3495,16 @@ function createReadTool(options = {}) {
3213
3495
  target: (input) => input.path,
3214
3496
  async run(input, ctx) {
3215
3497
  const abs = resolvePath(ctx.cwd, input.path);
3498
+ try {
3499
+ if (statSync4(abs).isDirectory()) {
3500
+ const entries = readdirSync(abs, { withFileTypes: true }).map((e) => e.isDirectory() ? `${e.name}/` : e.name).sort((a, b) => a.localeCompare(b));
3501
+ const start = (input.offset ?? 1) - 1;
3502
+ const limit = input.limit ?? 2e3;
3503
+ const slice = entries.slice(start, start + limit);
3504
+ return { output: slice.join("\n") || "(empty directory)", display: t("{n} lines", { n: slice.length }) };
3505
+ }
3506
+ } catch {
3507
+ }
3216
3508
  let buffer;
3217
3509
  try {
3218
3510
  buffer = await readFile3(abs);
@@ -3221,13 +3513,13 @@ function createReadTool(options = {}) {
3221
3513
  }
3222
3514
  const mediaType = vision ? imageMediaType(buffer) : void 0;
3223
3515
  if (mediaType) {
3224
- ctx.readFiles.add(abs);
3516
+ recordFileMtime(ctx.readFiles, abs);
3225
3517
  return readImageResult(input, buffer, mediaType);
3226
3518
  }
3227
3519
  if (buffer.subarray(0, 8e3).includes(0)) {
3228
3520
  return { output: `${input.path} is a binary file; not read.`, isError: true };
3229
3521
  }
3230
- ctx.readFiles.add(abs);
3522
+ recordFileMtime(ctx.readFiles, abs);
3231
3523
  return readTextResult(input, buffer);
3232
3524
  }
3233
3525
  });
@@ -3237,7 +3529,7 @@ var readTool = createReadTool({ vision: true });
3237
3529
  // src/tools/write.ts
3238
3530
  import { existsSync as existsSync4 } from "fs";
3239
3531
  import { mkdir, readFile as readFile4, writeFile as writeFile2 } from "fs/promises";
3240
- import { dirname as dirname2 } from "path";
3532
+ import { dirname as dirname4 } from "path";
3241
3533
  import { z as z7 } from "zod";
3242
3534
  var writeTool = defineTool({
3243
3535
  name: "write",
@@ -3264,12 +3556,12 @@ var writeTool = defineTool({
3264
3556
  }
3265
3557
  try {
3266
3558
  await ctx.checkpoint?.(abs);
3267
- await mkdir(dirname2(abs), { recursive: true });
3559
+ await mkdir(dirname4(abs), { recursive: true });
3268
3560
  await writeFile2(abs, input.content);
3269
3561
  } catch (error) {
3270
3562
  return { output: `Failed to write ${input.path}: ${errorCode(error)}`, isError: true };
3271
3563
  }
3272
- ctx.readFiles.add(abs);
3564
+ recordFileMtime(ctx.readFiles, abs);
3273
3565
  const lines = input.content.split("\n").length;
3274
3566
  return { output: `Wrote ${input.path} (${lines} lines).`, display: t("{lines} lines written", { lines }) };
3275
3567
  }
@@ -3379,12 +3671,36 @@ function budgetStatus(usage, cap, model) {
3379
3671
  }
3380
3672
 
3381
3673
  // src/tokenBudget.ts
3674
+ var IMAGE_TOKEN_COST = 1e3;
3382
3675
  var estimateTokens = (text) => Math.ceil(text.length / 4);
3383
- var estimateMessages = (messages) => estimateTokens(JSON.stringify(messages));
3676
+ function isImageAttachmentMessage(m) {
3677
+ if (m.role !== "user" || !Array.isArray(m.content)) return false;
3678
+ const hasImage = m.content.some((p) => p.type === "image_url");
3679
+ const firstText = m.content.find((p) => p.type === "text");
3680
+ return hasImage && !!firstText && firstText.type === "text" && firstText.text.startsWith("Image read from ");
3681
+ }
3682
+ function isUserTurnBoundary(m) {
3683
+ return m.role === "user" && !isImageAttachmentMessage(m);
3684
+ }
3685
+ function estimateMessages(messages) {
3686
+ let total = 0;
3687
+ for (const m of messages) {
3688
+ if (m.role === "user" && Array.isArray(m.content)) {
3689
+ for (const part of m.content) {
3690
+ if (part.type === "image_url") total += IMAGE_TOKEN_COST;
3691
+ else total += estimateTokens(part.text);
3692
+ }
3693
+ total += 8;
3694
+ } else {
3695
+ total += estimateTokens(JSON.stringify(m));
3696
+ }
3697
+ }
3698
+ return total;
3699
+ }
3384
3700
  function trimToFit(messages, maxTokens) {
3385
3701
  let out = messages;
3386
3702
  while (estimateMessages(out) > maxTokens) {
3387
- const boundary = out.findIndex((m, i) => i > 0 && m.role === "user");
3703
+ const boundary = out.findIndex((m, i) => i > 0 && isUserTurnBoundary(m));
3388
3704
  if (boundary === -1) return out;
3389
3705
  out = out.slice(boundary);
3390
3706
  }
@@ -3394,13 +3710,13 @@ function splitKeepRecent(messages, keepTokens) {
3394
3710
  if (messages.length === 0) return [[], []];
3395
3711
  let start = messages.length;
3396
3712
  for (let i = messages.length - 1; i >= 0; i--) {
3397
- if (i > 0 && messages[i].role !== "user") continue;
3713
+ if (i > 0 && !isUserTurnBoundary(messages[i])) continue;
3398
3714
  if (estimateMessages(messages.slice(i)) <= keepTokens) start = i;
3399
3715
  else break;
3400
3716
  }
3401
3717
  if (start === messages.length) {
3402
3718
  for (let i = messages.length - 1; i >= 0; i--) {
3403
- if (messages[i].role === "user") {
3719
+ if (isUserTurnBoundary(messages[i])) {
3404
3720
  start = i;
3405
3721
  break;
3406
3722
  }
@@ -3448,6 +3764,7 @@ var Agent2 = class {
3448
3764
  this.messages = [...opts.history ?? []];
3449
3765
  this.maxSteps = typeof opts.maxSteps === "number" && Number.isFinite(opts.maxSteps) && opts.maxSteps > 0 ? Math.max(1, Math.floor(opts.maxSteps)) : void 0;
3450
3766
  this.contextWindow = opts.contextWindow ?? 128e3;
3767
+ this.budgetModel = opts.budgetModel ?? "";
3451
3768
  this.systemPrompt = opts.systemPrompt;
3452
3769
  this.reasoning = opts.reasoning ?? "auto";
3453
3770
  }
@@ -3471,6 +3788,7 @@ var Agent2 = class {
3471
3788
  maxSteps;
3472
3789
  // One window for every model; resolved from contextWindowFor() (catalog + env override).
3473
3790
  contextWindow;
3791
+ budgetModel;
3474
3792
  systemPrompt;
3475
3793
  setSystemPrompt(text) {
3476
3794
  this.systemPrompt = text;
@@ -3478,6 +3796,15 @@ var Agent2 = class {
3478
3796
  setContextWindow(window) {
3479
3797
  this.contextWindow = window;
3480
3798
  }
3799
+ getContextWindow() {
3800
+ return this.contextWindow;
3801
+ }
3802
+ setBudgetModel(model) {
3803
+ this.budgetModel = model;
3804
+ }
3805
+ getBudgetModel() {
3806
+ return this.budgetModel;
3807
+ }
3481
3808
  setTools(tools) {
3482
3809
  this.tools = tools;
3483
3810
  this.definitions = toolDefinitions(tools);
@@ -3678,7 +4005,7 @@ ${t("This model may not support tool calling. Try another model with /model.")}`
3678
4005
  }
3679
4006
  /** Current cap status; undefined means the budget is not exhausted. */
3680
4007
  overBudget() {
3681
- return budgetStatus(this.totalUsage, this.opts.usageCap, this.opts.budgetModel ?? "");
4008
+ return budgetStatus(this.totalUsage, this.opts.usageCap, this.budgetModel);
3682
4009
  }
3683
4010
  /** Every tool call needs an answer in the history, or the next request to the model is rejected. */
3684
4011
  cancelCalls(calls) {
@@ -3798,7 +4125,7 @@ ${t("This model may not support tool calling. Try another model with /model.")}`
3798
4125
  };
3799
4126
 
3800
4127
  // src/checkpoints.ts
3801
- import { existsSync as existsSync5, statSync } from "fs";
4128
+ import { existsSync as existsSync5, statSync as statSync5 } from "fs";
3802
4129
  import { readFile as readFile5, rm, writeFile as writeFile3 } from "fs/promises";
3803
4130
  var MAX_TURNS = 20;
3804
4131
  var MAX_BYTES2 = 2 * 1024 * 1024;
@@ -3825,8 +4152,8 @@ var CheckpointStore = class {
3825
4152
  history = [];
3826
4153
  current;
3827
4154
  pendingRedo;
4155
+ offset = 0;
3828
4156
  beginTurn() {
3829
- if (this.current?.size === 0) return;
3830
4157
  this.current = /* @__PURE__ */ new Map();
3831
4158
  this.history.push(this.current);
3832
4159
  this.pendingRedo = void 0;
@@ -3837,16 +4164,19 @@ var CheckpointStore = class {
3837
4164
  const turn = this.current;
3838
4165
  if (turn.has(path)) return;
3839
4166
  if (!existsSync5(path)) turn.set(path, null);
3840
- else if (statSync(path).size > MAX_BYTES2) turn.set(path, "skipped");
4167
+ else if (statSync5(path).size > MAX_BYTES2) turn.set(path, "skipped");
3841
4168
  else turn.set(path, await readFile5(path));
3842
- while (this.turns() > MAX_TURNS) this.history.shift();
4169
+ while (this.turns() > MAX_TURNS) {
4170
+ this.history.shift();
4171
+ this.offset++;
4172
+ }
3843
4173
  }
3844
4174
  turns() {
3845
4175
  return this.history.filter((t2) => t2.size).length;
3846
4176
  }
3847
4177
  /** Total entries including empty ones; used to align file snapshots with conversation turns. */
3848
4178
  entries() {
3849
- return this.history.length;
4179
+ return this.offset + this.history.length;
3850
4180
  }
3851
4181
  async undo() {
3852
4182
  while (this.history.length && !this.history.at(-1)?.size) this.history.pop();
@@ -3866,7 +4196,7 @@ var CheckpointStore = class {
3866
4196
  * pairs conversation turns with the entry count recorded when each turn started.
3867
4197
  */
3868
4198
  async undoTo(keep) {
3869
- const target = Math.max(0, Math.min(keep, this.history.length));
4199
+ const target = Math.max(0, Math.min(keep - this.offset, this.history.length));
3870
4200
  const result = emptyResult();
3871
4201
  const redo = /* @__PURE__ */ new Map();
3872
4202
  let applied = false;
@@ -3891,8 +4221,11 @@ var CheckpointStore = class {
3891
4221
  const turn = this.pendingRedo;
3892
4222
  if (!turn) return void 0;
3893
4223
  this.pendingRedo = void 0;
4224
+ const undoEntry = /* @__PURE__ */ new Map();
4225
+ await this.captureFor(turn, undoEntry);
3894
4226
  const result = emptyResult();
3895
4227
  await restoreTurn(turn, result);
4228
+ if (undoEntry.size) this.history.push(undoEntry);
3896
4229
  return result;
3897
4230
  }
3898
4231
  /** Records the on-disk state of a turn's paths so redo() can put it back. */
@@ -3905,7 +4238,7 @@ var CheckpointStore = class {
3905
4238
  }
3906
4239
  try {
3907
4240
  if (!existsSync5(path)) into.set(path, null);
3908
- else if (statSync(path).size > MAX_BYTES2) into.set(path, "skipped");
4241
+ else if (statSync5(path).size > MAX_BYTES2) into.set(path, "skipped");
3909
4242
  else into.set(path, await readFile5(path));
3910
4243
  } catch {
3911
4244
  into.set(path, "skipped");
@@ -3916,6 +4249,7 @@ var CheckpointStore = class {
3916
4249
  this.history.length = 0;
3917
4250
  this.current = void 0;
3918
4251
  this.pendingRedo = void 0;
4252
+ this.offset = 0;
3919
4253
  }
3920
4254
  };
3921
4255
 
@@ -3964,7 +4298,7 @@ import { z as z9 } from "zod";
3964
4298
  // src/mcp/config.ts
3965
4299
  import { createHash } from "crypto";
3966
4300
  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";
4301
+ import { dirname as dirname5, join as join5, resolve as resolve4 } from "path";
3968
4302
  var ENV_REF = /\$\{([A-Za-z_][A-Za-z0-9_]*)(?::-([^}]*))?\}/g;
3969
4303
  function expandEnvVars(config, env) {
3970
4304
  const expand = (value) => value.replace(ENV_REF, (match, name, fallback) => {
@@ -3988,9 +4322,9 @@ function expandEnvVars(config, env) {
3988
4322
  import { spawn as spawn3 } from "child_process";
3989
4323
  import { existsSync as existsSync7, mkdirSync as mkdirSync3, readFileSync as readFileSync4 } from "fs";
3990
4324
  import { createServer } from "http";
3991
- import { dirname as dirname4, join as join4 } from "path";
4325
+ import { dirname as dirname6, join as join6 } from "path";
3992
4326
  import { auth } from "@modelcontextprotocol/sdk/client/auth.js";
3993
- var oauthStorePath = (home) => join4(home, "mcp-oauth.json");
4327
+ var oauthStorePath = (home) => join6(home, "mcp-oauth.json");
3994
4328
  function readStore(home) {
3995
4329
  const path = oauthStorePath(home);
3996
4330
  if (!existsSync7(path)) return {};
@@ -4003,7 +4337,7 @@ function readStore(home) {
4003
4337
  }
4004
4338
  function writeStore(home, store) {
4005
4339
  const path = oauthStorePath(home);
4006
- mkdirSync3(dirname4(path), { recursive: true });
4340
+ mkdirSync3(dirname6(path), { recursive: true });
4007
4341
  writeFileAtomic(path, `${JSON.stringify(store, null, 2)}
4008
4342
  `, 384);
4009
4343
  }
@@ -4097,7 +4431,28 @@ var FileOAuthProvider = class {
4097
4431
  };
4098
4432
 
4099
4433
  // src/mcp/manager.ts
4100
- var MCP_OUTPUT_MAX_CHARS = 32e3;
4434
+ function formatMcpContent(content) {
4435
+ const parts = [];
4436
+ const images = [];
4437
+ for (const c of content ?? []) {
4438
+ if (c.type === "text") {
4439
+ parts.push(c.text ?? "");
4440
+ } else if (c.type === "image" && c.data) {
4441
+ const mediaType = c.mimeType || "image/png";
4442
+ images.push({ mediaType, data: c.data, path: "mcp:image" });
4443
+ parts.push(`[image attached: ${mediaType}]`);
4444
+ } else if (c.type === "resource" && c.resource?.text) {
4445
+ parts.push(c.resource.uri ? `[${c.resource.uri}]
4446
+ ${c.resource.text}` : c.resource.text);
4447
+ } else {
4448
+ parts.push(`[${c.type}]`);
4449
+ }
4450
+ }
4451
+ return {
4452
+ output: parts.join("\n") || "(empty)",
4453
+ ...images.length ? { images } : {}
4454
+ };
4455
+ }
4101
4456
  var CHILD_ENV_KEYS = [
4102
4457
  // portable
4103
4458
  "PATH",
@@ -4226,11 +4581,13 @@ var McpManager = class {
4226
4581
  async run(input, ctx) {
4227
4582
  try {
4228
4583
  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)";
4584
+ const formatted = formatMcpContent(result.content);
4585
+ const output = formatted.output;
4230
4586
  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
4587
+ output: output.length > 32e3 ? `${output.slice(0, 32e3)}
4588
+ \u2026 [truncated at 32000 characters; narrow the MCP request if possible]` : output,
4589
+ isError: !!result.isError,
4590
+ ...formatted.images ? { images: formatted.images } : {}
4234
4591
  };
4235
4592
  } catch (error) {
4236
4593
  return { output: `Server MCP ${server} error: ${error.message}`, isError: true };
@@ -4264,7 +4621,7 @@ import { homedir as homedir2 } from "os";
4264
4621
 
4265
4622
  // src/context.ts
4266
4623
  import { existsSync as existsSync8, readFileSync as readFileSync5 } from "fs";
4267
- import { dirname as dirname5, join as join5 } from "path";
4624
+ import { dirname as dirname7, join as join7 } from "path";
4268
4625
  var FILES = ["AGENTS.md", "CLAUDE.md", "BCCLI.md"];
4269
4626
  function readIf(path) {
4270
4627
  try {
@@ -4275,11 +4632,11 @@ function readIf(path) {
4275
4632
  }
4276
4633
  function instructionPaths(cwd, home) {
4277
4634
  const dirs2 = [];
4278
- for (let dir = cwd; ; dir = dirname5(dir)) {
4635
+ for (let dir = cwd; ; dir = dirname7(dir)) {
4279
4636
  dirs2.unshift(dir);
4280
- if (dirname5(dir) === dir) break;
4637
+ if (dirname7(dir) === dir) break;
4281
4638
  }
4282
- return [join5(home, "BCCLI.md"), ...dirs2.flatMap((dir) => FILES.map((name) => join5(dir, name)))];
4639
+ return [join7(home, "BCCLI.md"), ...dirs2.flatMap((dir) => FILES.map((name) => join7(dir, name)))];
4283
4640
  }
4284
4641
  function loadInstructions(cwd, home) {
4285
4642
  const parts = [];
@@ -4291,9 +4648,9 @@ ${text}`);
4291
4648
  return parts.join("\n\n");
4292
4649
  }
4293
4650
  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;
4651
+ for (let dir = cwd; ; dir = dirname7(dir)) {
4652
+ if (existsSync8(join7(dir, ".git"))) return true;
4653
+ if (dirname7(dir) === dir) return false;
4297
4654
  }
4298
4655
  }
4299
4656
  function buildSystemPrompt(opts) {
@@ -4336,16 +4693,34 @@ ${instructions}` : ""}`;
4336
4693
  }
4337
4694
 
4338
4695
  // 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";
4696
+ import { existsSync as existsSync9, readdirSync as readdirSync2, readFileSync as readFileSync6, statSync as statSync6 } from "fs";
4697
+ import { basename as basename2, join as join8 } from "path";
4341
4698
  function parseFrontmatter(text) {
4342
4699
  const match = /^---\r?\n([\s\S]*?)\r?\n---\r?\n?/.exec(text);
4343
4700
  if (!match) return { data: {}, body: text };
4344
4701
  const data = {};
4702
+ let currentListKey;
4345
4703
  for (const line of match[1].split(/\r?\n/)) {
4704
+ const listItem = /^\s*-\s+(.+)$/.exec(line);
4705
+ if (listItem && currentListKey) {
4706
+ const item = listItem[1].trim().replace(/^["']|["']$/g, "");
4707
+ const prev = data[currentListKey];
4708
+ if (Array.isArray(prev)) prev.push(item);
4709
+ else data[currentListKey] = [item];
4710
+ continue;
4711
+ }
4346
4712
  const kv = /^([A-Za-z0-9_-]+):\s*(.*)$/.exec(line);
4347
- if (!kv) continue;
4713
+ if (!kv) {
4714
+ if (line.trim()) currentListKey = void 0;
4715
+ continue;
4716
+ }
4348
4717
  let value = kv[2].trim();
4718
+ if (!value) {
4719
+ currentListKey = kv[1];
4720
+ data[kv[1]] = "";
4721
+ continue;
4722
+ }
4723
+ currentListKey = void 0;
4349
4724
  if (value.startsWith("[") && value.endsWith("]")) {
4350
4725
  data[kv[1]] = value.slice(1, -1).split(",").map((s) => s.trim().replace(/^["']|["']$/g, "")).filter(Boolean);
4351
4726
  continue;
@@ -4357,11 +4732,11 @@ function parseFrontmatter(text) {
4357
4732
  }
4358
4733
  var str = (v) => Array.isArray(v) ? v.join(", ") : v;
4359
4734
  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)];
4735
+ return [join8(r.userHome, ".claude", kind), join8(r.home, kind), join8(r.cwd, ".claude", kind), join8(r.cwd, ".bccli", kind)];
4361
4736
  }
4362
4737
  function list(dir) {
4363
4738
  try {
4364
- return readdirSync(dir).sort();
4739
+ return readdirSync2(dir).sort();
4365
4740
  } catch {
4366
4741
  return [];
4367
4742
  }
@@ -4370,11 +4745,11 @@ function loadSkills(r) {
4370
4745
  const byName = /* @__PURE__ */ new Map();
4371
4746
  for (const dir of dirs(r, "skills")) {
4372
4747
  for (const entry of list(dir)) {
4373
- const file = join6(dir, entry, "SKILL.md");
4748
+ const file = join8(dir, entry, "SKILL.md");
4374
4749
  if (!existsSync9(file)) continue;
4375
4750
  const { data } = parseFrontmatter(readFileSync6(file, "utf8"));
4376
4751
  const name = str(data.name) || entry;
4377
- byName.set(name, { name, description: str(data.description) ?? "", dir: join6(dir, entry), file });
4752
+ byName.set(name, { name, description: str(data.description) ?? "", dir: join8(dir, entry), file });
4378
4753
  }
4379
4754
  }
4380
4755
  return [...byName.values()];
@@ -4383,10 +4758,10 @@ function loadCommands(r) {
4383
4758
  const byName = /* @__PURE__ */ new Map();
4384
4759
  for (const dir of dirs(r, "commands")) {
4385
4760
  for (const entry of list(dir)) {
4386
- const file = join6(dir, entry);
4387
- if (!entry.endsWith(".md") || !statSync2(file).isFile()) continue;
4761
+ const file = join8(dir, entry);
4762
+ if (!entry.endsWith(".md") || !statSync6(file).isFile()) continue;
4388
4763
  const { data, body } = parseFrontmatter(readFileSync6(file, "utf8"));
4389
- const def = { name: basename(entry, ".md"), body: body.trim() };
4764
+ const def = { name: basename2(entry, ".md"), body: body.trim() };
4390
4765
  if (data.description) def.description = str(data.description);
4391
4766
  if (data["argument-hint"]) def.argumentHint = str(data["argument-hint"]);
4392
4767
  byName.set(def.name, def);
@@ -4399,8 +4774,8 @@ function loadAgentDefs(r) {
4399
4774
  for (const dir of dirs(r, "agents")) {
4400
4775
  for (const entry of list(dir)) {
4401
4776
  if (!entry.endsWith(".md")) continue;
4402
- const { data, body } = parseFrontmatter(readFileSync6(join6(dir, entry), "utf8"));
4403
- const name = str(data.name) || basename(entry, ".md");
4777
+ const { data, body } = parseFrontmatter(readFileSync6(join8(dir, entry), "utf8"));
4778
+ const name = str(data.name) || basename2(entry, ".md");
4404
4779
  const def = { name, description: str(data.description) ?? "", prompt: body.trim() };
4405
4780
  if (data.tools)
4406
4781
  def.tools = Array.isArray(data.tools) ? data.tools : String(data.tools).split(",").map((s) => s.trim()).filter(Boolean);
@@ -4418,8 +4793,7 @@ function loadAgentDefs(r) {
4418
4793
  }
4419
4794
 
4420
4795
  // src/providers.ts
4421
- import { mkdirSync as mkdirSync4, writeFileSync as writeFileSync2 } from "fs";
4422
- import { join as join7 } from "path";
4796
+ import { join as join9 } from "path";
4423
4797
  function providerName(config, id) {
4424
4798
  return PRESETS.find((p) => p.id === id)?.name ?? config.providers[id]?.name ?? id;
4425
4799
  }
@@ -4478,11 +4852,11 @@ async function addProviderKey(providerId, key, deps) {
4478
4852
 
4479
4853
  // src/session.ts
4480
4854
  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";
4855
+ import { appendFileSync, existsSync as existsSync10, mkdirSync as mkdirSync4, readdirSync as readdirSync3, readFileSync as readFileSync7, rmSync as rmSync2, statSync as statSync7 } from "fs";
4856
+ import { basename as basename3, join as join10 } from "path";
4483
4857
  function projectDir(home, cwd) {
4484
4858
  const slug = cwd.replace(/[^a-zA-Z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(-100) || "root";
4485
- return join8(home, "sessions", slug);
4859
+ return join10(home, "sessions", slug);
4486
4860
  }
4487
4861
  function repairTail(messages) {
4488
4862
  const out = [...messages];
@@ -4500,19 +4874,22 @@ var Session = class _Session {
4500
4874
  this.file = file;
4501
4875
  }
4502
4876
  file;
4877
+ get id() {
4878
+ return basename3(this.file, ".jsonl");
4879
+ }
4503
4880
  static create(home, cwd, now = /* @__PURE__ */ new Date()) {
4504
4881
  const dir = projectDir(home, cwd);
4505
- mkdirSync5(dir, { recursive: true });
4882
+ mkdirSync4(dir, { recursive: true });
4506
4883
  const stamp = now.toISOString().replace(/[:.]/g, "-");
4507
- return new _Session(join8(dir, `${stamp}-${randomBytes2(3).toString("hex")}.jsonl`));
4884
+ return new _Session(join10(dir, `${stamp}-${randomBytes2(3).toString("hex")}.jsonl`));
4508
4885
  }
4509
4886
  static list(home, cwd) {
4510
4887
  const dir = projectDir(home, cwd);
4511
4888
  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) : "" };
4889
+ return readdirSync3(dir).filter((f) => f.endsWith(".jsonl")).map((f) => {
4890
+ const session = new _Session(join10(dir, f));
4891
+ const first = session.loadHistory().find((m) => m.role === "user");
4892
+ return { session, mtime: statSync7(session.file).mtime, preview: first ? contentText(first.content).slice(0, 80) : "" };
4516
4893
  }).sort((a, b) => b.mtime.getTime() - a.mtime.getTime() || b.session.file.localeCompare(a.session.file));
4517
4894
  }
4518
4895
  static latest(home, cwd) {
@@ -4561,6 +4938,35 @@ var Session = class _Session {
4561
4938
  }
4562
4939
  return void 0;
4563
4940
  }
4941
+ /** Accumulated token usage since the last non-compaction reset. */
4942
+ totalUsage() {
4943
+ const segments = [{ msgs: [], usage: { inputTokens: 0, outputTokens: 0 } }];
4944
+ for (const record of this.records()) {
4945
+ const cur = segments[segments.length - 1];
4946
+ if (record.t === "reset") {
4947
+ segments.push({ msgs: [], usage: { inputTokens: 0, outputTokens: 0 } });
4948
+ } else if (record.t === "msg") {
4949
+ cur.msgs.push(record.m);
4950
+ } else if (record.t === "usage") {
4951
+ cur.usage.inputTokens += record.inputTokens;
4952
+ cur.usage.outputTokens += record.outputTokens;
4953
+ if (record.cachedInputTokens) cur.usage.cachedInputTokens = (cur.usage.cachedInputTokens ?? 0) + record.cachedInputTokens;
4954
+ if (record.cacheWriteTokens) cur.usage.cacheWriteTokens = (cur.usage.cacheWriteTokens ?? 0) + record.cacheWriteTokens;
4955
+ }
4956
+ }
4957
+ let total = { ...segments[0].usage };
4958
+ for (const seg of segments.slice(1)) {
4959
+ if (!isCompactionSummary(seg.msgs[0])) {
4960
+ total = { ...seg.usage };
4961
+ } else {
4962
+ total.inputTokens += seg.usage.inputTokens;
4963
+ total.outputTokens += seg.usage.outputTokens;
4964
+ if (seg.usage.cachedInputTokens) total.cachedInputTokens = (total.cachedInputTokens ?? 0) + seg.usage.cachedInputTokens;
4965
+ if (seg.usage.cacheWriteTokens) total.cacheWriteTokens = (total.cacheWriteTokens ?? 0) + seg.usage.cacheWriteTokens;
4966
+ }
4967
+ }
4968
+ return total;
4969
+ }
4564
4970
  /**
4565
4971
  * What the user sees on resume: the whole conversation. Compaction only shrinks what the model is sent, so
4566
4972
  * its reset does not hide the older turns here. A real reset (/clear, rewind) still starts over, the
@@ -4573,12 +4979,22 @@ var Session = class _Session {
4573
4979
  else if (record.t === "msg") segments[segments.length - 1].push(record.m);
4574
4980
  }
4575
4981
  let shown = segments[0];
4982
+ let lastSummary;
4576
4983
  for (const segment of segments.slice(1)) {
4577
4984
  if (!isCompactionSummary(segment[0])) {
4578
4985
  shown = segment;
4986
+ lastSummary = void 0;
4579
4987
  continue;
4580
4988
  }
4581
4989
  const kept = segment.slice(2);
4990
+ if (lastSummary && sameMessage2(segment[0], lastSummary)) {
4991
+ const subIdx = findContiguousSlice(shown, kept);
4992
+ if (subIdx !== -1) {
4993
+ shown = shown.slice(0, subIdx + kept.length);
4994
+ continue;
4995
+ }
4996
+ }
4997
+ lastSummary = segment[0];
4582
4998
  let at = 0;
4583
4999
  while (at <= shown.length && !endsWithPrefixOf(shown, at, kept)) at++;
4584
5000
  shown = [...shown, ...kept.slice(shown.length - at)];
@@ -4605,6 +5021,20 @@ function endsWithPrefixOf(shown, at, kept) {
4605
5021
  for (let j = 0; j < n; j++) if (!sameMessage2(shown[at + j], kept[j])) return false;
4606
5022
  return true;
4607
5023
  }
5024
+ function findContiguousSlice(shown, slice) {
5025
+ if (slice.length === 0) return shown.length;
5026
+ for (let i = 0; i + slice.length <= shown.length; i++) {
5027
+ let ok = true;
5028
+ for (let j = 0; j < slice.length; j++) {
5029
+ if (!sameMessage2(shown[i + j], slice[j])) {
5030
+ ok = false;
5031
+ break;
5032
+ }
5033
+ }
5034
+ if (ok) return i;
5035
+ }
5036
+ return -1;
5037
+ }
4608
5038
 
4609
5039
  // src/tools/plan.ts
4610
5040
  import { z as z10 } from "zod";
@@ -4714,6 +5144,7 @@ import { z as z13 } from "zod";
4714
5144
  function createTaskTool(opts) {
4715
5145
  const byName = new Map(opts.agents.map((a) => [a.name, a]));
4716
5146
  const list2 = opts.agents.map((a) => `- ${a.name}: ${a.description}`).join("\n");
5147
+ const sessions = /* @__PURE__ */ new Map();
4717
5148
  let askChain = Promise.resolve();
4718
5149
  const enqueueAsk = (fn) => {
4719
5150
  const next = askChain.then(
@@ -4733,7 +5164,8 @@ ${list2}`,
4733
5164
  schema: z13.object({
4734
5165
  agent: z13.string().describe("Agent name"),
4735
5166
  description: z13.string().describe("3-6 word summary shown to the user"),
4736
- prompt: z13.string().describe("Full instructions; the subagent sees nothing else")
5167
+ prompt: z13.string().describe("Full instructions; the subagent sees nothing else"),
5168
+ session_id: z13.string().optional().describe("Optional session id to continue a previous subagent conversation")
4737
5169
  }),
4738
5170
  kind: "read",
4739
5171
  target: (input) => `[${input.agent}] ${input.description}`,
@@ -4760,6 +5192,7 @@ ${list2}`,
4760
5192
  } catch (error) {
4761
5193
  return { output: `Agent "${def.name}": ${error.message}`, isError: true };
4762
5194
  }
5195
+ const priorHistory = input.session_id ? sessions.get(input.session_id) : void 0;
4763
5196
  const child = new Agent2({
4764
5197
  provider,
4765
5198
  tools,
@@ -4769,6 +5202,7 @@ ${list2}`,
4769
5202
  # Your role
4770
5203
  ${def.prompt}`,
4771
5204
  cwd: opts.cwd,
5205
+ ...priorHistory ? { history: priorHistory } : {},
4772
5206
  // Every subagent runs with a step cap so a looping child cannot burn tokens forever.
4773
5207
  maxSteps: def.maxSteps ?? 50,
4774
5208
  label: def.name,
@@ -4789,12 +5223,29 @@ ${def.prompt}`,
4789
5223
  const ask = ctx.ask;
4790
5224
  if (ask) child.askPermission = (req) => enqueueAsk(() => ask(req));
4791
5225
  await child.run(input.prompt, ctx.signal);
5226
+ if (input.session_id) sessions.set(input.session_id, [...child.messages]);
4792
5227
  ctx.addUsage?.(child.totalUsage);
4793
5228
  const tokens2 = child.totalUsage.inputTokens + child.totalUsage.outputTokens;
4794
5229
  const display = t("{steps} steps \xB7 {tokens} tokens", { steps, tokens: tokens2 >= 1e3 ? `${Math.round(tokens2 / 1e3)}k` : tokens2 });
4795
5230
  if (outcome?.type === "error") return { output: `Subagent ${def.name} failed: ${outcome.message}`, isError: true, display };
4796
5231
  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);
5232
+ let last = [...child.messages].reverse().find((m) => m.role === "assistant" && m.content);
5233
+ if (!last && outcome?.type === "stepLimit" && !ctx.signal.aborted) {
5234
+ try {
5235
+ const synthesis = await provider.chat({
5236
+ messages: [
5237
+ ...child.messages,
5238
+ { role: "user", content: "Step limit reached. Summarize all findings gathered so far concisely." }
5239
+ ],
5240
+ signal: ctx.signal,
5241
+ reasoning: opts.reasoning?.() ?? "auto"
5242
+ });
5243
+ if (synthesis.text.trim()) {
5244
+ last = { role: "assistant", content: synthesis.text.trim() };
5245
+ }
5246
+ } catch {
5247
+ }
5248
+ }
4798
5249
  const report = last && typeof last.content === "string" ? last.content : "(no report)";
4799
5250
  const suffix = outcome?.type === "stepLimit" ? `
4800
5251
 
@@ -4840,7 +5291,7 @@ function createTodoTool(store) {
4840
5291
 
4841
5292
  // src/tools/memory.ts
4842
5293
  import { existsSync as existsSync12, readFileSync as readFileSync10 } from "fs";
4843
- import { join as join9 } from "path";
5294
+ import { join as join11 } from "path";
4844
5295
  import { z as z15 } from "zod";
4845
5296
 
4846
5297
  // src/slash/memory.ts
@@ -4863,11 +5314,11 @@ function createSaveMemoryTool(opts) {
4863
5314
  kind: "edit",
4864
5315
  // The target is what the prompt shows and what the permission model judges: the
4865
5316
  // 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",
5317
+ target: (input) => input.scope === "global" ? join11(opts.home, "BCCLI.md") : "AGENTS.md",
4867
5318
  async run(input, ctx) {
4868
5319
  const content = input.content.trim();
4869
5320
  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");
5321
+ const file = input.scope === "global" ? join11(opts.home, "BCCLI.md") : join11(ctx.cwd, "AGENTS.md");
4871
5322
  if (existsSync12(file)) {
4872
5323
  const lines = readFileSync10(file, "utf8").split("\n");
4873
5324
  if (lines.some((line) => line.trim() === `- ${content}`)) {
@@ -4885,18 +5336,21 @@ function createSaveMemoryTool(opts) {
4885
5336
  import { z as z16 } from "zod";
4886
5337
 
4887
5338
  // 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";
5339
+ import { readdirSync as readdirSync4, readFileSync as readFileSync11 } from "fs";
5340
+ import { extname, join as join12, relative as relative2, sep } from "path";
4890
5341
  var SKIP_DIRS = /* @__PURE__ */ new Set([
4891
5342
  "node_modules",
4892
5343
  ".git",
4893
5344
  "dist",
4894
5345
  "build",
5346
+ "out",
4895
5347
  "coverage",
4896
5348
  ".next",
5349
+ ".turbo",
4897
5350
  "target",
4898
5351
  "vendor",
4899
5352
  "__pycache__",
5353
+ "venv",
4900
5354
  ".venv",
4901
5355
  ".cache"
4902
5356
  ]);
@@ -4922,21 +5376,22 @@ var CODE_EXTS = /* @__PURE__ */ new Set([
4922
5376
  ".ps1"
4923
5377
  ]);
4924
5378
  var MAX_FILES = 400;
5379
+ var MAX_SCAN_FILES = 5e3;
4925
5380
  var MAX_FILE_BYTES = 2e5;
4926
5381
  var MAX_SYMBOLS_PER_FILE = 12;
4927
5382
  var FAMILY = {
4928
5383
  ts: [
4929
- /^\s*(?:export\s+)?(?:default\s+)?(?:declare\s+)?(?:abstract\s+)?(?:async\s+)?(?:function|class|interface|type|enum)\s+([A-Za-z0-9_$]+)/,
5384
+ /^\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
5385
  /^\s*export\s+(?:const|let)\s+([A-Za-z0-9_$]+)/
4931
5386
  ],
4932
- py: [/^\s*(?:async\s+)?def\s+(\w+)/, /^\s*class\s+(\w+)/],
5387
+ py: [/^\s*(?:async\s+)?def\s+(\w+)/, /^\s*(?:module|class)\s+(\w+)/],
4933
5388
  go: [/^func\s+(?:\([^)]*\)\s*)?(\w+)/, /^type\s+(\w+)/],
4934
5389
  rs: [/^\s*(?:pub(?:\([^)]*\))?\s+)?(?:unsafe\s+)?(?:async\s+)?(?:fn|struct|enum|trait|type)\s+(\w+)/],
4935
5390
  c: [/^\s*(?:static\s+|inline\s+|extern\s+)*[A-Za-z_][A-Za-z0-9_\s*]*\b(\w+)\s*\([^;]*\)\s*\{?\s*$/]
4936
5391
  };
4937
5392
  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;
5393
+ if ([".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs", ".java", ".cs", ".php"].includes(ext)) return FAMILY.ts;
5394
+ if (ext === ".py" || ext === ".rb" || ext === ".sh" || ext === ".ps1") return FAMILY.py;
4940
5395
  if (ext === ".go") return FAMILY.go;
4941
5396
  if (ext === ".rs") return FAMILY.rs;
4942
5397
  return FAMILY.c;
@@ -4956,16 +5411,16 @@ function extractSymbols(text, ext) {
4956
5411
  return out;
4957
5412
  }
4958
5413
  function walk(dir, root, files) {
4959
- if (files.length >= MAX_FILES) return;
5414
+ if (files.length >= MAX_SCAN_FILES) return;
4960
5415
  let entries;
4961
5416
  try {
4962
- entries = readdirSync3(dir, { withFileTypes: true });
5417
+ entries = readdirSync4(dir, { withFileTypes: true });
4963
5418
  } catch {
4964
5419
  return;
4965
5420
  }
4966
5421
  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);
5422
+ if (files.length >= MAX_SCAN_FILES) return;
5423
+ const full = join12(dir, e.name);
4969
5424
  if (e.isDirectory()) {
4970
5425
  if (SKIP_DIRS.has(e.name) || e.name.startsWith(".") && full !== root) continue;
4971
5426
  walk(full, root, files);
@@ -4976,16 +5431,17 @@ function walk(dir, root, files) {
4976
5431
  }
4977
5432
  function buildRepoMapSync(cwd, opts = {}) {
4978
5433
  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)
5434
+ const scanned = [];
5435
+ walk(cwd, cwd, scanned);
5436
+ scanned.sort(
5437
+ (a, b) => relative2(cwd, a).split(sep).length - relative2(cwd, b).split(sep).length || a.localeCompare(b)
4983
5438
  );
5439
+ const files = scanned.slice(0, MAX_FILES);
4984
5440
  const lines = [];
4985
5441
  let used = 0;
4986
5442
  let truncated = false;
4987
5443
  for (const file of files) {
4988
- const rel = relative2(cwd, file).split(sep2).join("/");
5444
+ const rel = relative2(cwd, file).split(sep).join("/");
4989
5445
  let symbols = [];
4990
5446
  try {
4991
5447
  const text = readFileSync11(file, "utf8");
@@ -5020,13 +5476,50 @@ function createRepoMapTool() {
5020
5476
  }
5021
5477
 
5022
5478
  // src/tools/diagnostics.ts
5023
- import { extname as extname2 } from "path";
5479
+ import { extname as extname3 } from "path";
5024
5480
  import { z as z17 } from "zod";
5025
5481
 
5026
5482
  // src/lsp.ts
5027
5483
  import { spawn as spawn4 } from "child_process";
5028
5484
  import { readFileSync as readFileSync12 } from "fs";
5485
+ import { extname as extname2 } from "path";
5029
5486
  import { pathToFileURL } from "url";
5487
+ var LANGUAGE_IDS = {
5488
+ ".ts": "typescript",
5489
+ ".tsx": "typescriptreact",
5490
+ ".mts": "typescript",
5491
+ ".cts": "typescript",
5492
+ ".js": "javascript",
5493
+ ".jsx": "javascriptreact",
5494
+ ".mjs": "javascript",
5495
+ ".cjs": "javascript",
5496
+ ".py": "python",
5497
+ ".rs": "rust",
5498
+ ".go": "go",
5499
+ ".java": "java",
5500
+ ".c": "c",
5501
+ ".h": "c",
5502
+ ".cpp": "cpp",
5503
+ ".hpp": "cpp",
5504
+ ".cs": "csharp",
5505
+ ".rb": "ruby",
5506
+ ".php": "php",
5507
+ ".json": "json",
5508
+ ".md": "markdown",
5509
+ ".sh": "shellscript",
5510
+ ".ps1": "powershell",
5511
+ ".yaml": "yaml",
5512
+ ".yml": "yaml",
5513
+ ".toml": "toml",
5514
+ ".html": "html",
5515
+ ".css": "css"
5516
+ };
5517
+ function languageIdFor(file) {
5518
+ return LANGUAGE_IDS[extname2(file).toLowerCase()] ?? "plaintext";
5519
+ }
5520
+ function useShellForLsp(command, platform = process.platform) {
5521
+ return platform === "win32" && !/\.(exe|mjs|cjs|js)$/i.test(command.trim());
5522
+ }
5030
5523
  function encodeLsp(msg) {
5031
5524
  const body = Buffer.from(JSON.stringify(msg), "utf8");
5032
5525
  return Buffer.concat([Buffer.from(`Content-Length: ${body.length}\r
@@ -5059,8 +5552,12 @@ function createLspParser(onMessage) {
5059
5552
  };
5060
5553
  }
5061
5554
  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"] });
5555
+ return new Promise((resolve5) => {
5556
+ const child = spawn4(opts.server.command, opts.server.args ?? [], {
5557
+ cwd: opts.cwd,
5558
+ stdio: ["pipe", "pipe", "ignore"],
5559
+ shell: useShellForLsp(opts.server.command)
5560
+ });
5064
5561
  const uri = pathToFileURL(opts.file).href;
5065
5562
  let settled = false;
5066
5563
  let nextId = 2;
@@ -5074,6 +5571,7 @@ function getDiagnostics(opts) {
5074
5571
  if (settled) return;
5075
5572
  settled = true;
5076
5573
  clearTimeout(timer);
5574
+ opts.signal?.removeEventListener("abort", onAbort);
5077
5575
  send({ jsonrpc: "2.0", id: nextId++, method: "shutdown" });
5078
5576
  send({ jsonrpc: "2.0", method: "exit" });
5079
5577
  const killer = setTimeout(() => {
@@ -5083,8 +5581,11 @@ function getDiagnostics(opts) {
5083
5581
  }
5084
5582
  }, 500);
5085
5583
  killer.unref?.();
5086
- resolve4(diags);
5584
+ resolve5(diags);
5087
5585
  };
5586
+ const onAbort = () => finish([]);
5587
+ if (opts.signal?.aborted) return finish([]);
5588
+ opts.signal?.addEventListener("abort", onAbort, { once: true });
5088
5589
  const timer = setTimeout(() => finish([]), opts.timeoutMs ?? 8e3);
5089
5590
  const parser = createLspParser((msg) => {
5090
5591
  if (msg.id === 1 && msg.result) {
@@ -5097,7 +5598,7 @@ function getDiagnostics(opts) {
5097
5598
  send({
5098
5599
  jsonrpc: "2.0",
5099
5600
  method: "textDocument/didOpen",
5100
- params: { textDocument: { uri, languageId: "plaintext", version: 1, text } }
5601
+ params: { textDocument: { uri, languageId: languageIdFor(opts.file), version: 1, text } }
5101
5602
  });
5102
5603
  }
5103
5604
  if (msg.method === "textDocument/publishDiagnostics" && msg.params?.uri === uri) {
@@ -5135,7 +5636,7 @@ function createDiagnosticsTool(servers) {
5135
5636
  target: (input) => input.path,
5136
5637
  async run(input, ctx) {
5137
5638
  const file = resolvePath(ctx.cwd, input.path);
5138
- const ext = extname2(file).toLowerCase();
5639
+ const ext = extname3(file).toLowerCase();
5139
5640
  const server = servers.find((s) => s.extensions.includes(ext));
5140
5641
  if (!server) {
5141
5642
  return { output: t("No LSP server is configured for {ext} files.", { ext: ext || "(none)" }), isError: true };
@@ -5151,7 +5652,7 @@ function createDiagnosticsTool(servers) {
5151
5652
  // src/plugins.ts
5152
5653
  import { existsSync as existsSync13 } from "fs";
5153
5654
  import { createRequire } from "module";
5154
- import { isAbsolute as isAbsolute3, join as join11 } from "path";
5655
+ import { isAbsolute as isAbsolute3, join as join13 } from "path";
5155
5656
  import { z as z18 } from "zod";
5156
5657
  var require_ = createRequire(import.meta.url);
5157
5658
  function pluginLog(message) {
@@ -5176,7 +5677,7 @@ function loadPlugins(specs, opts) {
5176
5677
  for (const spec of specs) {
5177
5678
  let file;
5178
5679
  try {
5179
- file = isAbsolute3(spec) ? spec : spec.startsWith(".") ? join11(opts.home, spec) : require_.resolve(spec);
5680
+ file = isAbsolute3(spec) ? spec : spec.startsWith(".") ? join13(opts.home, spec) : require_.resolve(spec);
5180
5681
  } catch {
5181
5682
  throw new ConfigError(t("Plugin not found: {spec}", { spec }));
5182
5683
  }
@@ -5253,7 +5754,7 @@ function createRuntime(opts) {
5253
5754
  let config = loadConfig(opts.cwd, env);
5254
5755
  let modelRef = opts.args.model ?? config.model;
5255
5756
  let reasoning = opts.args.reasoning ?? config.reasoning;
5256
- const makeProvider = (ref) => {
5757
+ const makeSingleProvider = (ref) => {
5257
5758
  const resolved = resolveModel(config, ref, env);
5258
5759
  return opts.provider ?? createProvider({
5259
5760
  baseURL: resolved.baseURL,
@@ -5265,6 +5766,25 @@ function createRuntime(opts) {
5265
5766
  enableProgrammaticToolCalling: lunaPtcAutoEnabled(env)
5266
5767
  });
5267
5768
  };
5769
+ const makeProvider = (ref) => {
5770
+ const primary = makeSingleProvider(ref);
5771
+ if (opts.provider) return primary;
5772
+ const fallbackRefs = [
5773
+ ...opts.args.fallbackModel ? [opts.args.fallbackModel] : [],
5774
+ ...config.fallbackModels ?? []
5775
+ ].filter((r) => r && r !== ref);
5776
+ if (!fallbackRefs.length) return primary;
5777
+ return withFallbackProvider(
5778
+ primary,
5779
+ () => fallbackRefs.flatMap((fbRef) => {
5780
+ try {
5781
+ return [makeSingleProvider(fbRef)];
5782
+ } catch {
5783
+ return [];
5784
+ }
5785
+ })
5786
+ );
5787
+ };
5268
5788
  const provider = makeProvider(modelRef);
5269
5789
  const mode = opts.args.allowAll ? "allowAll" : opts.args.permissionMode ?? config.permissionMode;
5270
5790
  const rules = [...config.allow, ...opts.args.allowedTools.map((t2) => TOOL_RULES[t2] ?? t2)];
@@ -5286,6 +5806,10 @@ function createRuntime(opts) {
5286
5806
  if (latest) {
5287
5807
  session = latest;
5288
5808
  history = latest.load();
5809
+ if (!opts.args.model) {
5810
+ const prevModel = latest.lastModel();
5811
+ if (prevModel) modelRef = prevModel;
5812
+ }
5289
5813
  }
5290
5814
  }
5291
5815
  const repoMap = config.repoMap ? buildRepoMapSync(opts.cwd) : void 0;
@@ -5329,7 +5853,7 @@ function createRuntime(opts) {
5329
5853
  // Subagents get whatever the main agent has right now (incl. MCP tools), minus task itself.
5330
5854
  baseTools: () => agent.tools.filter((t2) => t2.name !== "task"),
5331
5855
  permissions,
5332
- provider: () => agent.provider,
5856
+ provider: () => opts.provider ? agent.provider : makeProvider(modelRef),
5333
5857
  providerFor: makeProvider,
5334
5858
  systemPrompt: (childModelRef) => systemFor(childModelRef ?? modelRef),
5335
5859
  cwd: opts.cwd,
@@ -5415,6 +5939,20 @@ function createRuntime(opts) {
5415
5939
  session = previous;
5416
5940
  agent.load(previous.load());
5417
5941
  agent.totalUsage = { inputTokens: 0, outputTokens: 0 };
5942
+ if (!opts.args.model) {
5943
+ const prevModel = previous.lastModel();
5944
+ if (prevModel) {
5945
+ try {
5946
+ const nextProvider = makeProvider(prevModel);
5947
+ modelRef = prevModel;
5948
+ agent.provider = nextProvider;
5949
+ agent.setBudgetModel(prevModel);
5950
+ agent.setContextWindow(contextWindowFor(prevModel, env));
5951
+ agent.setSystemPrompt(systemFor(modelRef));
5952
+ } catch {
5953
+ }
5954
+ }
5955
+ }
5418
5956
  todos.set([]);
5419
5957
  checkpoints.clear();
5420
5958
  turnMarks.length = 0;
@@ -5463,6 +6001,7 @@ function createRuntime(opts) {
5463
6001
  const nextProvider = makeProvider(ref);
5464
6002
  modelRef = ref;
5465
6003
  agent.provider = nextProvider;
6004
+ agent.setBudgetModel(ref);
5466
6005
  agent.setContextWindow(contextWindowFor(ref, env));
5467
6006
  agent.setSystemPrompt(systemFor(modelRef));
5468
6007
  },
@@ -5488,6 +6027,7 @@ async function* streamTask(opts) {
5488
6027
  let finished = false;
5489
6028
  let text = "";
5490
6029
  let stopReason = "done";
6030
+ let errorMessage2;
5491
6031
  const usage = { inputTokens: 0, outputTokens: 0 };
5492
6032
  const toolCalls = [];
5493
6033
  const byId = /* @__PURE__ */ new Map();
@@ -5537,28 +6077,45 @@ async function* streamTask(opts) {
5537
6077
  case "aborted":
5538
6078
  stopReason = "aborted";
5539
6079
  break;
6080
+ case "error":
6081
+ stopReason = "error";
6082
+ errorMessage2 = event.message;
6083
+ push({ type: "error", message: event.message });
6084
+ break;
5540
6085
  default:
5541
6086
  break;
5542
6087
  }
5543
6088
  };
5544
- const running = rt.agent.run(opts.prompt, opts.signal ?? new AbortController().signal).catch(() => {
5545
- stopReason = "aborted";
6089
+ const controller = new AbortController();
6090
+ const onParentAbort = () => controller.abort();
6091
+ if (opts.signal) {
6092
+ if (opts.signal.aborted) controller.abort();
6093
+ else opts.signal.addEventListener("abort", onParentAbort, { once: true });
6094
+ }
6095
+ const running = rt.agent.run(opts.prompt, controller.signal).catch((err) => {
6096
+ stopReason = "error";
6097
+ errorMessage2 = err instanceof Error ? err.message : String(err);
5546
6098
  }).finally(() => {
5547
6099
  finished = true;
5548
6100
  wake?.();
5549
6101
  });
5550
- while (!finished || queue.length) {
5551
- if (queue.length) {
5552
- yield queue.shift();
5553
- continue;
6102
+ try {
6103
+ while (!finished || queue.length) {
6104
+ if (queue.length) {
6105
+ yield queue.shift();
6106
+ continue;
6107
+ }
6108
+ await new Promise((resolve5) => {
6109
+ wake = resolve5;
6110
+ });
6111
+ wake = null;
5554
6112
  }
5555
- await new Promise((resolve4) => {
5556
- wake = resolve4;
5557
- });
5558
- wake = null;
6113
+ await running;
6114
+ yield { type: "result", result: { text, toolCalls, usage, stopReason, ...errorMessage2 ? { error: errorMessage2 } : {} } };
6115
+ } finally {
6116
+ opts.signal?.removeEventListener("abort", onParentAbort);
6117
+ if (!finished) controller.abort();
5559
6118
  }
5560
- await running;
5561
- yield { type: "result", result: { text, toolCalls, usage, stopReason } };
5562
6119
  }
5563
6120
  async function runTask(opts) {
5564
6121
  let out = { text: "", toolCalls: [], usage: { inputTokens: 0, outputTokens: 0 }, stopReason: "done" };