@botconnector/bccli 0.4.0 → 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 (173) hide show
  1. package/README.en.md +13 -2
  2. package/README.md +13 -2
  3. package/dist/abort.d.ts +5 -0
  4. package/dist/{acp-ATIL4JOZ.js → acp-UYC6APR5.js} +30 -6
  5. package/dist/acp.d.ts +28 -0
  6. package/dist/agent.d.ts +147 -0
  7. package/dist/agents.d.ts +4 -0
  8. package/dist/args-NRTZWWEV.js +13 -0
  9. package/dist/args.d.ts +53 -0
  10. package/dist/budget.d.ts +23 -0
  11. package/dist/checkpoints.d.ts +35 -0
  12. package/dist/{chunk-6M5X2KZE.js → chunk-266SY3MC.js} +1 -1
  13. package/dist/{chunk-7F3IWBPQ.js → chunk-3I5BW3ZT.js} +20 -9
  14. package/dist/{chunk-UZWJQF67.js → chunk-3WHI4LN3.js} +2 -1
  15. package/dist/{chunk-PUN4HMAG.js → chunk-5YJ3ZQVR.js} +73 -10
  16. package/dist/{chunk-L7QI6JN2.js → chunk-6M55UPES.js} +7 -7
  17. package/dist/{chunk-KEMYIGCP.js → chunk-6UO5GX55.js} +26 -12
  18. package/dist/{chunk-TLPFQIS2.js → chunk-C64KM37J.js} +36 -6
  19. package/dist/{chunk-JL5ZCSP2.js → chunk-H7XJDKJY.js} +4 -4
  20. package/dist/{chunk-SPRNLS7K.js → chunk-HQW4BROS.js} +4 -5
  21. package/dist/{chunk-XBO5GACQ.js → chunk-IYR5PYA3.js} +4 -4
  22. package/dist/{chunk-45SDLMTV.js → chunk-JXM6REER.js} +1 -1
  23. package/dist/{chunk-BLOYFIAD.js → chunk-KPTML72Q.js} +1 -1
  24. package/dist/{chunk-BQ2IGPBR.js → chunk-NQOGVP3I.js} +2 -2
  25. package/dist/{chunk-QZKLZELZ.js → chunk-OK2USAXX.js} +28 -4
  26. package/dist/{chunk-TVALKHFB.js → chunk-R72OV7RF.js} +421 -108
  27. package/dist/{chunk-LACUWBWS.js → chunk-RX4UQX43.js} +25 -10
  28. package/dist/{chunk-QDQU6VY2.js → chunk-SVD4LHJC.js} +73 -3
  29. package/dist/{chunk-BNA74HPN.js → chunk-TIMSYZIR.js} +7 -7
  30. package/dist/{chunk-O7M3TGIW.js → chunk-WFZKJY3R.js} +43 -4
  31. package/dist/{chunk-M7CALSP3.js → chunk-ZEPTUKNW.js} +120 -32
  32. package/dist/cli.d.ts +1 -0
  33. package/dist/cli.js +61 -45
  34. package/dist/clipboardImage.d.ts +17 -0
  35. package/dist/commands.d.ts +16 -0
  36. package/dist/{completion-HPPFDAPB.js → completion-D2BSBTQO.js} +1 -1
  37. package/dist/completion.d.ts +14 -0
  38. package/dist/{config-COPNMMV6.js → config-ENDRMOTP.js} +3 -3
  39. package/dist/{config-RZ5JONMX.js → config-LAINORU5.js} +8 -4
  40. package/dist/config.d.ts +99 -0
  41. package/dist/context.d.ts +13 -0
  42. package/dist/contextWindow.d.ts +3 -0
  43. package/dist/diff.d.ts +7 -0
  44. package/dist/exit.d.ts +9 -0
  45. package/dist/extensions.d.ts +34 -0
  46. package/dist/{ghBotCli-IBCZIWT5.js → ghBotCli-TSLRORJJ.js} +21 -20
  47. package/dist/ghBotCli.d.ts +9 -0
  48. package/dist/ghBotDispatch.d.ts +68 -0
  49. package/dist/ghBotRunner.d.ts +82 -0
  50. package/dist/ghBotServe.d.ts +21 -0
  51. package/dist/ghBotWebhook.d.ts +45 -0
  52. package/dist/hooks.d.ts +27 -0
  53. package/dist/i18n/id.d.ts +1 -0
  54. package/dist/i18n.d.ts +16 -0
  55. package/dist/{integrationCli-P75R2VSC.js → integrationCli-HXCSEWH2.js} +2 -2
  56. package/dist/integrationCli.d.ts +9 -0
  57. package/dist/keybinds.d.ts +22 -0
  58. package/dist/{login-SYIQP46J.js → login-RYESFZG6.js} +4 -4
  59. package/dist/login.d.ts +19 -0
  60. package/dist/lsp.d.ts +50 -0
  61. package/dist/mcp/catalog.d.ts +14 -0
  62. package/dist/mcp/config.d.ts +36 -0
  63. package/dist/mcp/manager.d.ts +52 -0
  64. package/dist/mcp/oauth.d.ts +82 -0
  65. package/dist/{mcpCli-QUV5SD64.js → mcpCli-SETTCL3K.js} +6 -6
  66. package/dist/mcpCli.d.ts +3 -0
  67. package/dist/mcpServer/serve.d.ts +8 -0
  68. package/dist/mcpServer/server.d.ts +19 -0
  69. package/dist/models.d.ts +13 -0
  70. package/dist/{modelsCli-TRRB5LX4.js → modelsCli-OP7CDZC7.js} +6 -6
  71. package/dist/modelsCli.d.ts +15 -0
  72. package/dist/npmRunner.d.ts +21 -0
  73. package/dist/{oauthLogin-6FWQABJ7.js → oauthLogin-XWY7OHCG.js} +1 -1
  74. package/dist/oauthLogin.d.ts +47 -0
  75. package/dist/permissions.d.ts +30 -0
  76. package/dist/pluginSpec.d.ts +25 -0
  77. package/dist/plugins.d.ts +45 -0
  78. package/dist/{pluginsCli-GNW4TPVV.js → pluginsCli-ZQNKWZHJ.js} +2 -2
  79. package/dist/pluginsCli.d.ts +16 -0
  80. package/dist/{prCli-5IMV3VNQ.js → prCli-6PUWTMTP.js} +20 -4
  81. package/dist/prCli.d.ts +46 -0
  82. package/dist/presets.d.ts +8 -0
  83. package/dist/print.d.ts +16 -0
  84. package/dist/provider.d.ts +145 -0
  85. package/dist/providerCli-5OZI2GQQ.js +15 -0
  86. package/dist/providerCli.d.ts +17 -0
  87. package/dist/providers.d.ts +6 -0
  88. package/dist/reasoning.d.ts +12 -0
  89. package/dist/repomap.d.ts +10 -0
  90. package/dist/sdk.d.ts +61 -0
  91. package/dist/sdk.js +829 -191
  92. package/dist/{serve-JG7RLIUD.js → serve-4J7HA4JO.js} +24 -18
  93. package/dist/{serve-6QLBJUYW.js → serve-K5AH4MGO.js} +3 -3
  94. package/dist/serve.d.ts +37 -0
  95. package/dist/serveHeartbeat.d.ts +14 -0
  96. package/dist/serveProtocol.d.ts +47 -0
  97. package/dist/session-YQXCCKXI.js +12 -0
  98. package/dist/session.d.ts +33 -0
  99. package/dist/setup-S4AJT6XE.js +24 -0
  100. package/dist/setup.d.ts +74 -0
  101. package/dist/slash/copy.d.ts +1 -0
  102. package/dist/slash/diff.d.ts +1 -0
  103. package/dist/slash/doctor.d.ts +18 -0
  104. package/dist/slash/export.d.ts +2 -0
  105. package/dist/slash/info.d.ts +46 -0
  106. package/dist/slash/memory.d.ts +10 -0
  107. package/dist/{statsCli-CF3R7ATL.js → statsCli-XR6BEZQQ.js} +2 -2
  108. package/dist/statsCli.d.ts +62 -0
  109. package/dist/textToolCalls.d.ts +6 -0
  110. package/dist/thinking.d.ts +17 -0
  111. package/dist/tokenBudget.d.ts +22 -0
  112. package/dist/tools/bash.d.ts +52 -0
  113. package/dist/tools/diagnostics.d.ts +5 -0
  114. package/dist/tools/edit.d.ts +12 -0
  115. package/dist/tools/fetch.d.ts +41 -0
  116. package/dist/tools/glob.d.ts +5 -0
  117. package/dist/tools/globSafety.d.ts +7 -0
  118. package/dist/tools/grep.d.ts +25 -0
  119. package/dist/tools/ignore.d.ts +2 -0
  120. package/dist/tools/index.d.ts +12 -0
  121. package/dist/tools/memory.d.ts +13 -0
  122. package/dist/tools/paths.d.ts +4 -0
  123. package/dist/tools/plan.d.ts +10 -0
  124. package/dist/tools/read.d.ts +16 -0
  125. package/dist/tools/repomap.d.ts +2 -0
  126. package/dist/tools/sandbox.d.ts +12 -0
  127. package/dist/tools/shell.d.ts +18 -0
  128. package/dist/tools/skill.d.ts +3 -0
  129. package/dist/tools/task.d.ts +28 -0
  130. package/dist/tools/todo.d.ts +13 -0
  131. package/dist/tools/types.d.ts +52 -0
  132. package/dist/tools/websearch.d.ts +9 -0
  133. package/dist/tools/write.d.ts +5 -0
  134. package/dist/{transferCli-TK2GSVRK.js → transferCli-R2KEIWQR.js} +8 -6
  135. package/dist/transferCli.d.ts +23 -0
  136. package/dist/ui/App.d.ts +7 -0
  137. package/dist/ui/DiffView.d.ts +3 -0
  138. package/dist/ui/EditableText.d.ts +7 -0
  139. package/dist/ui/LinePrompt.d.ts +6 -0
  140. package/dist/ui/ListPicker.d.ts +11 -0
  141. package/dist/ui/Markdown.d.ts +8 -0
  142. package/dist/ui/McpMenu.d.ts +11 -0
  143. package/dist/ui/ModelPicker.d.ts +6 -0
  144. package/dist/ui/PermissionPrompt.d.ts +5 -0
  145. package/dist/ui/PlanApproval.d.ts +5 -0
  146. package/dist/ui/PromptInput.d.ts +18 -0
  147. package/dist/ui/ProviderMenu.d.ts +10 -0
  148. package/dist/ui/Spinner.d.ts +4 -0
  149. package/dist/ui/StatusBar.d.ts +9 -0
  150. package/dist/ui/TodoList.d.ts +4 -0
  151. package/dist/ui/ToolBlock.d.ts +14 -0
  152. package/dist/ui/complete.d.ts +6 -0
  153. package/dist/ui/focusReport.d.ts +1 -0
  154. package/dist/ui/index.d.ts +7 -0
  155. package/dist/ui/lineEdit.d.ts +42 -0
  156. package/dist/ui/replay.d.ts +4 -0
  157. package/dist/ui/table.d.ts +20 -0
  158. package/dist/ui/theme.d.ts +5 -0
  159. package/dist/ui/thinkingMouse.d.ts +33 -0
  160. package/dist/ui/transcript.d.ts +55 -0
  161. package/dist/ui/vim.d.ts +26 -0
  162. package/dist/{ui-B3CJ6TGE.js → ui-36ZEK65V.js} +179 -54
  163. package/dist/{update-K5IQ2MYR.js → update-FSS7X5WV.js} +2 -2
  164. package/dist/update.d.ts +30 -0
  165. package/dist/verify.d.ts +16 -0
  166. package/dist/version.d.ts +1 -0
  167. package/dist/webBudget.d.ts +25 -0
  168. package/dist/worktree.d.ts +12 -0
  169. package/package.json +26 -13
  170. package/dist/args-J5ONVGIF.js +0 -13
  171. package/dist/providerCli-FAOZHOKW.js +0 -15
  172. package/dist/session-KBAQ4BZ5.js +0 -12
  173. package/dist/setup-FJAWDWV4.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,
@@ -715,21 +719,46 @@ function loadConfig(cwd, env = process.env) {
715
719
  allow: [...global.allow ?? []]
716
720
  };
717
721
  }
722
+ function writeFileAtomic(path, data, mode) {
723
+ const tmp = `${path}.tmp-${process.pid}-${Date.now()}`;
724
+ try {
725
+ writeFileSync(tmp, data, mode === void 0 ? void 0 : { mode });
726
+ renameSync(tmp, path);
727
+ if (mode !== void 0) chmodSync(path, mode);
728
+ } catch (error) {
729
+ try {
730
+ rmSync(tmp, { force: true });
731
+ } catch {
732
+ }
733
+ throw error;
734
+ }
735
+ }
736
+ function quarantineCorruptFile(path, error) {
737
+ const stamp = Date.now();
738
+ let dest = `${path}.corrupt-${stamp}`;
739
+ for (let n = 1; existsSync(dest); n++) dest = `${path}.corrupt-${stamp}-${n}`;
740
+ try {
741
+ renameSync(path, dest);
742
+ } catch {
743
+ return void 0;
744
+ }
745
+ console.error(`${path} is not valid JSON (${error.message}); moved it to ${dest} instead of overwriting it.`);
746
+ return dest;
747
+ }
718
748
  function readCredentials(env = process.env) {
719
749
  const path = join(bccliHome(env), "credentials");
720
750
  if (!existsSync(path)) return {};
721
751
  try {
722
752
  return JSON.parse(readFileSync(path, "utf8"));
723
- } catch {
753
+ } catch (error) {
754
+ quarantineCorruptFile(path, error);
724
755
  return {};
725
756
  }
726
757
  }
727
758
  function saveCredential(providerId, key, env = process.env) {
728
759
  const home = bccliHome(env);
729
760
  mkdirSync(home, { recursive: true });
730
- const path = join(home, "credentials");
731
- writeFileSync(path, JSON.stringify({ ...readCredentials(env), [providerId]: key }, null, 2), { mode: 384 });
732
- chmodSync(path, 384);
761
+ writeFileAtomic(join(home, "credentials"), JSON.stringify({ ...readCredentials(env), [providerId]: key }, null, 2), 384);
733
762
  }
734
763
  function resolveModel(config, modelRef, env = process.env) {
735
764
  const slash = modelRef.indexOf("/");
@@ -749,7 +778,7 @@ function resolveModel(config, modelRef, env = process.env) {
749
778
  }
750
779
 
751
780
  // src/permissions.ts
752
- import { isAbsolute, relative, resolve, sep } from "path";
781
+ import { isAbsolute, relative, resolve } from "path";
753
782
  var MODE_ORDER = ["default", "acceptEdits", "plan", "allowAll"];
754
783
  var ALWAYS_ASK_COMMANDS = /* @__PURE__ */ new Set(["sudo", "sh", "bash", "zsh", "su"]);
755
784
  function isAlwaysAsk(segment) {
@@ -765,14 +794,51 @@ function isAlwaysAsk(segment) {
765
794
  }
766
795
  return false;
767
796
  }
797
+ var RUNNER_COMMANDS = /* @__PURE__ */ new Set([
798
+ "npm",
799
+ "npx",
800
+ "pnpm",
801
+ "pnpx",
802
+ "yarn",
803
+ "bun",
804
+ "bunx",
805
+ "deno",
806
+ "node",
807
+ "tsx",
808
+ "ts-node",
809
+ "python",
810
+ "python3",
811
+ "py",
812
+ "uv",
813
+ "uvx",
814
+ "pip",
815
+ "pip3",
816
+ "pipx",
817
+ "pytest",
818
+ "make",
819
+ "cargo",
820
+ "go",
821
+ "dotnet",
822
+ "java",
823
+ "ruby",
824
+ "bundle",
825
+ "rake",
826
+ "gradle",
827
+ "mvn",
828
+ "php",
829
+ "composer"
830
+ ]);
768
831
  function commandKey(segment) {
769
832
  const words2 = segment.trim().split(/\s+/);
833
+ if (RUNNER_COMMANDS.has(words2[0] ?? "") && words2[2] && !words2[2].startsWith("-")) return words2.slice(0, 3).join(" ");
770
834
  return words2[1] ? `${words2[0]} ${words2[1]}` : words2[0];
771
835
  }
772
836
  function requiresSensitiveToolApproval(req) {
773
837
  const target = req.target.trim();
774
838
  if (req.kind === "read" || req.kind === "edit") {
775
- 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);
776
842
  }
777
843
  if (req.kind === "bash") {
778
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);
@@ -797,7 +863,8 @@ var Permissions = class {
797
863
  editInProject(target) {
798
864
  const rel = relative(this.cwd, resolve(this.cwd, target));
799
865
  if (!rel || rel.startsWith("..") || isAbsolute(rel)) return false;
800
- return !rel.split(sep).includes(".git") && !rel.split("/").includes(".git");
866
+ const parts = rel.split(/[/\\]/).map((p) => p.toLowerCase());
867
+ return !parts.includes(".git");
801
868
  }
802
869
  /** Rules that "[a] allow for this session" would add; undefined when it must always be asked. */
803
870
  rulesFor(req) {
@@ -815,7 +882,7 @@ var Permissions = class {
815
882
  if (/`|\$\(|[<>]|^\s*[({]/.test(req.target)) return void 0;
816
883
  const segments = req.target.split(/&&|\|\||[;&|\n]/).map((s) => s.trim()).filter(Boolean);
817
884
  if (!segments.length) return void 0;
818
- if (segments.some(isAlwaysAsk)) return void 0;
885
+ if (segments.some((s) => /^[({]/.test(s) || isAlwaysAsk(s))) return void 0;
819
886
  return [...new Set(segments.map((s) => `bash(${commandKey(s)})`))];
820
887
  }
821
888
  if (req.kind === "mcp") return [`mcp(${req.tool})`];
@@ -893,9 +960,12 @@ var LOOPBACK_BIND = /* @__PURE__ */ new Set(["127.0.0.1", "localhost", "::1"]);
893
960
 
894
961
  // src/args.ts
895
962
  function parseCliArgs(argv) {
896
- const expanded = argv.map(
897
- (a, i) => a === "--models" && (i + 1 >= argv.length || argv[i + 1].startsWith("-")) ? "--models=all" : a
898
- );
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
+ });
899
969
  const { values, positionals } = parseArgs({
900
970
  args: expanded,
901
971
  allowPositionals: true,
@@ -927,7 +997,8 @@ function parseCliArgs(argv) {
927
997
  tools: { type: "string" },
928
998
  models: { type: "string" },
929
999
  project: { type: "string" },
930
- repo: { type: "string" }
1000
+ repo: { type: "string" },
1001
+ "fallback-model": { type: "string" }
931
1002
  }
932
1003
  });
933
1004
  const reasoning = values.reasoning;
@@ -1001,7 +1072,8 @@ function parseCliArgs(argv) {
1001
1072
  tools,
1002
1073
  models,
1003
1074
  project: values.project,
1004
- repo: values.repo
1075
+ repo: values.repo,
1076
+ fallbackModel: values["fallback-model"]
1005
1077
  };
1006
1078
  }
1007
1079
 
@@ -1093,14 +1165,14 @@ function detectRepetition(text) {
1093
1165
  return -1;
1094
1166
  }
1095
1167
  function sleep(ms, signal) {
1096
- return new Promise((resolve4, reject) => {
1168
+ return new Promise((resolve5, reject) => {
1097
1169
  const onAbort = () => {
1098
1170
  clearTimeout(timer);
1099
1171
  reject(signal?.reason);
1100
1172
  };
1101
1173
  const timer = setTimeout(() => {
1102
1174
  signal?.removeEventListener("abort", onAbort);
1103
- resolve4();
1175
+ resolve5();
1104
1176
  }, ms);
1105
1177
  signal?.addEventListener("abort", onAbort, { once: true });
1106
1178
  });
@@ -1170,7 +1242,7 @@ async function readStream(res, onText, onThinking) {
1170
1242
  }
1171
1243
  current2 = target;
1172
1244
  if (tc.id) target.id = tc.id;
1173
- 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;
1174
1246
  if (tc.function?.arguments) target.arguments += tc.function.arguments;
1175
1247
  }
1176
1248
  };
@@ -1377,7 +1449,16 @@ function responsesFinishReason(items, toolCallCount, incompleteReason) {
1377
1449
  function hasPendingProgrammaticReplay(messages) {
1378
1450
  let pending = false;
1379
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
+ }
1380
1460
  if (message.role !== "assistant") continue;
1461
+ if (message.content) pending = false;
1381
1462
  for (const item of message.responses_output_items ?? []) {
1382
1463
  if (item.type === "program") pending = true;
1383
1464
  else if (item.type === "message" && pending) pending = false;
@@ -1451,7 +1532,7 @@ async function readResponsesStream(res, onText, onThinking) {
1451
1532
  calls.set(index, {
1452
1533
  id: String(event.item.call_id || current2?.id || event.item.id || `call_${index}`),
1453
1534
  name: String(event.item.name || current2?.name || ""),
1454
- arguments: String(event.item.arguments ?? current2?.arguments ?? ""),
1535
+ arguments: String(event.item.arguments || current2?.arguments || ""),
1455
1536
  ...programCaller(event.item) ?? current2?.caller ? { caller: programCaller(event.item) ?? current2?.caller } : {}
1456
1537
  });
1457
1538
  }
@@ -1687,7 +1768,7 @@ async function readResponsesWebSocketTurn(events, ws, onText, onThinking, signal
1687
1768
  calls.set(index, {
1688
1769
  id: String(event.item.call_id || current2?.id || event.item.id || `call_${index}`),
1689
1770
  name: String(event.item.name || current2?.name || ""),
1690
- arguments: String(event.item.arguments ?? current2?.arguments ?? ""),
1771
+ arguments: String(event.item.arguments || current2?.arguments || ""),
1691
1772
  ...programCaller(event.item) ?? current2?.caller ? { caller: programCaller(event.item) ?? current2?.caller } : {}
1692
1773
  });
1693
1774
  }
@@ -1748,7 +1829,7 @@ function createProvider(options) {
1748
1829
  const isBotConnectorCloud = /^https:\/\/api\.botconnector\.id\/v1\/?$/.test(baseURL) && apiKey?.startsWith("bc_live_");
1749
1830
  if (isBotConnectorCloud) {
1750
1831
  headers["x-botconnector-client"] = "bccli";
1751
- headers["x-botconnector-client-version"] = "0.4.0";
1832
+ headers["x-botconnector-client-version"] = "0.4.2";
1752
1833
  }
1753
1834
  const useNativeResponses = isBotConnectorCloud && providerId === "bc-cloud" && model === "gpt-6-luna";
1754
1835
  const useResponsesWebSocket = useNativeResponses && Boolean(apiKey) && (fetchOverride === void 0 || responsesWebSocketFactory !== void 0);
@@ -1779,7 +1860,7 @@ function createProvider(options) {
1779
1860
  baseURL: baseURL.replace(/\/+$/, ""),
1780
1861
  defaultHeaders: {
1781
1862
  "x-botconnector-client": "bccli",
1782
- "x-botconnector-client-version": "0.4.0"
1863
+ "x-botconnector-client-version": "0.4.2"
1783
1864
  }
1784
1865
  });
1785
1866
  lunaWs = responsesWebSocketFactory ? responsesWebSocketFactory(client) : new ResponsesWS(client, { reconnect: null });
@@ -1842,8 +1923,13 @@ function createProvider(options) {
1842
1923
  } catch (error) {
1843
1924
  const err = error instanceof ProviderError ? error : new ProviderError(error.message);
1844
1925
  const lostPrevious = err.code === "previous_response_not_found";
1926
+ const handshakeStatus = /unexpected server response:\s*(\d{3})\b/i.exec(err.message);
1927
+ const handshakeRejected = Boolean(handshakeStatus && Number(handshakeStatus[1]) >= 400);
1845
1928
  const connectionLost = /websocket|connection closed|socket|network/i.test(err.message) || err.code === "websocket_connection_limit_reached";
1846
1929
  resetLunaWebSocket();
1930
+ if (!emittedText && !signal?.aborted && handshakeRejected) {
1931
+ throw new ProviderError(err.message, Number(handshakeStatus?.[1]), "bc_ws_http_fallback");
1932
+ }
1847
1933
  if (attempt === 0 && !emittedText && !signal?.aborted && (plan.incremental || lostPrevious || connectionLost)) {
1848
1934
  continue;
1849
1935
  }
@@ -1857,8 +1943,13 @@ function createProvider(options) {
1857
1943
  }
1858
1944
  async function post(path, body, signal) {
1859
1945
  let lastError;
1946
+ let retryAfterWaitMs;
1860
1947
  for (let attempt = 0; attempt < 4; attempt++) {
1861
- 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
+ }
1862
1953
  let res;
1863
1954
  try {
1864
1955
  res = await doFetch(`${baseURL}${path}`, { method: "POST", headers, body: JSON.stringify(body), signal });
@@ -1868,9 +1959,15 @@ function createProvider(options) {
1868
1959
  continue;
1869
1960
  }
1870
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
+ }
1871
1967
  const text = await res.text().catch(() => "");
1872
1968
  lastError = new ProviderError(t("{status} from {url}: {message}", { status: res.status, url: baseURL, message: errorMessage(text) }), res.status);
1873
- if (!retryable(res.status)) throw lastError;
1969
+ const deterministicQuota429 = res.status === 429 && /"code"\s*:\s*"(?:luna_monthly_quota_exhausted|plan_allowance_exhausted)"/i.test(text);
1970
+ if (!retryable(res.status) || deterministicQuota429) throw lastError;
1874
1971
  }
1875
1972
  throw lastError ?? new ProviderError(t("Request failed"));
1876
1973
  }
@@ -1929,6 +2026,43 @@ function createProvider(options) {
1929
2026
  }
1930
2027
  };
1931
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
+ }
1932
2066
 
1933
2067
  // src/textToolCalls.ts
1934
2068
  var PAIR = /(?:<arg_key>\s*|`)?([A-Za-z_]\w*)(?:\s*<\/arg_key>)?`?\s*(?:=\s*`|<arg_value>)([\s\S]*?)<\/arg_value>/g;
@@ -2048,7 +2182,7 @@ function runOne(command, event, payload, opts) {
2048
2182
  ...payload.input !== void 0 ? { BCCLI_HOOK_INPUT: JSON.stringify(payload.input) } : {},
2049
2183
  ...payload.output !== void 0 ? { BCCLI_HOOK_OUTPUT: payload.output } : {}
2050
2184
  };
2051
- return new Promise((resolve4) => {
2185
+ return new Promise((resolve5) => {
2052
2186
  let stdout = "";
2053
2187
  let stderr = "";
2054
2188
  let timedOut = false;
@@ -2057,7 +2191,7 @@ function runOne(command, event, payload, opts) {
2057
2191
  if (settled) return;
2058
2192
  settled = true;
2059
2193
  clearTimeout(timer);
2060
- resolve4(result);
2194
+ resolve5(result);
2061
2195
  };
2062
2196
  let child;
2063
2197
  const shell = defaultShell();
@@ -2073,7 +2207,7 @@ function runOne(command, event, payload, opts) {
2073
2207
  child = spawn(command, { shell: true, env, cwd: opts.cwd ?? process.cwd(), stdio: ["ignore", "pipe", "pipe"] });
2074
2208
  }
2075
2209
  } catch (error) {
2076
- resolve4({ failure: error.message });
2210
+ resolve5({ failure: error.message });
2077
2211
  return;
2078
2212
  }
2079
2213
  const timer = setTimeout(() => {
@@ -2128,7 +2262,9 @@ import { z as z8 } from "zod";
2128
2262
 
2129
2263
  // src/tools/bash.ts
2130
2264
  import { spawn as spawn2 } from "child_process";
2131
- 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";
2132
2268
  import { z } from "zod";
2133
2269
 
2134
2270
  // src/tools/sandbox.ts
@@ -2317,7 +2453,7 @@ function offlineEnv(env = process.env) {
2317
2453
  };
2318
2454
  }
2319
2455
  function runCommand(command, opts) {
2320
- return new Promise((resolve4) => {
2456
+ return new Promise((resolve5) => {
2321
2457
  const child = spawnChild(command, opts.cwd, opts.env, opts.shell ?? defaultShell());
2322
2458
  if (!exitHookInstalled) {
2323
2459
  process.on("exit", killAllCommands);
@@ -2330,7 +2466,14 @@ function runCommand(command, opts) {
2330
2466
  let dropped = 0;
2331
2467
  let timedOut = false;
2332
2468
  let aborted = false;
2469
+ const fullChunks = [];
2470
+ let fullBytes = 0;
2471
+ const MAX_SPILL_BYTES = 10 * 1024 * 1024;
2333
2472
  const onData = (data) => {
2473
+ if (fullBytes < MAX_SPILL_BYTES) {
2474
+ fullChunks.push(data);
2475
+ fullBytes += data.length;
2476
+ }
2334
2477
  let s = data;
2335
2478
  if (head.length < HALF) {
2336
2479
  const take = s.slice(0, HALF - head.length);
@@ -2369,10 +2512,19 @@ function runCommand(command, opts) {
2369
2512
  active.delete(killAsAborted);
2370
2513
  clearTimeout(timer);
2371
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
+ }
2372
2524
  const output = dropped ? `${head}
2373
- \u2026 [${dropped} characters truncated] \u2026
2525
+ \u2026 [${dropped} characters truncated${spillNote}] \u2026
2374
2526
  ${tail}` : head + tail;
2375
- resolve4({ output: output + extra, exitCode, timedOut, aborted });
2527
+ resolve5({ output: output + extra, exitCode, timedOut, aborted });
2376
2528
  };
2377
2529
  child.on("error", (error) => finish(null, error.message));
2378
2530
  child.on("close", (code) => finish(code));
@@ -2494,46 +2646,139 @@ function formatDiff(lines) {
2494
2646
  }
2495
2647
 
2496
2648
  // src/tools/paths.ts
2649
+ import { statSync } from "fs";
2497
2650
  import { isAbsolute as isAbsolute2, resolve as resolve2 } from "path";
2651
+ var readMtimes = /* @__PURE__ */ new WeakMap();
2498
2652
  function resolvePath(cwd, path) {
2499
2653
  return isAbsolute2(path) ? path : resolve2(cwd, path);
2500
2654
  }
2501
2655
  function errorCode(error) {
2502
2656
  return error.code ?? error.message;
2503
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
+ }
2504
2679
 
2505
2680
  // src/tools/edit.ts
2506
- var schema = z2.object({
2507
- path: z2.string().describe("File path, relative to the project or absolute"),
2681
+ var editEntrySchema = z2.object({
2508
2682
  old_string: z2.string().describe("Exact text to replace; must be unique unless replace_all"),
2509
2683
  new_string: z2.string().describe("Replacement text"),
2510
2684
  replace_all: z2.boolean().optional().describe("Replace every occurrence")
2511
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
+ });
2512
2693
  function countOccurrences(haystack, needle) {
2513
2694
  let count = 0;
2514
2695
  for (let i = haystack.indexOf(needle); i !== -1; i = haystack.indexOf(needle, i + needle.length)) count++;
2515
2696
  return count;
2516
2697
  }
2517
- 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) {
2518
2739
  if (input.old_string === input.new_string) {
2519
2740
  return { output: "old_string and new_string are identical; nothing to change.", isError: true };
2520
2741
  }
2521
2742
  const eol = content.includes("\r\n") ? "\r\n" : "\n";
2522
2743
  const toEol = (s) => s.replace(/\r?\n/g, eol);
2744
+ const strippedOld = stripReadLinePrefixes(input.old_string);
2745
+ const strippedNew = stripReadLinePrefixes(input.new_string);
2523
2746
  const attempts = [[input.old_string, input.new_string]];
2524
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
+ }
2525
2751
  for (const [oldText, newText] of attempts) {
2526
2752
  const count = oldText ? countOccurrences(content, oldText) : 0;
2527
2753
  if (count === 0) continue;
2528
2754
  if (count > 1 && !input.replace_all) {
2529
2755
  return {
2530
- 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.`,
2531
2757
  isError: true
2532
2758
  };
2533
2759
  }
2534
2760
  return input.replace_all ? content.split(oldText).join(newText) : content.replace(oldText, () => newText);
2535
2761
  }
2536
- 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;
2537
2782
  }
2538
2783
  var editTool = defineTool({
2539
2784
  name: "edit",
@@ -2544,6 +2789,9 @@ var editTool = defineTool({
2544
2789
  async validate(input, ctx) {
2545
2790
  const abs = resolvePath(ctx.cwd, input.path);
2546
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
+ }
2547
2795
  let content;
2548
2796
  try {
2549
2797
  content = await readFile(abs, "utf8");
@@ -2564,6 +2812,9 @@ var editTool = defineTool({
2564
2812
  if (!ctx.readFiles.has(abs)) {
2565
2813
  return { output: `Read ${input.path} with read before editing it.`, isError: true };
2566
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
+ }
2567
2818
  let content;
2568
2819
  try {
2569
2820
  content = await readFile(abs, "utf8");
@@ -2574,6 +2825,7 @@ var editTool = defineTool({
2574
2825
  if (typeof next !== "string") return next;
2575
2826
  await ctx.checkpoint?.(abs);
2576
2827
  await writeFile(abs, next);
2828
+ recordFileMtime(ctx.readFiles, abs);
2577
2829
  return { output: `Edited ${input.path}.`, display: formatDiff(diffLines(content, next)) };
2578
2830
  }
2579
2831
  });
@@ -2845,20 +3097,22 @@ async function loadPage(inputUrl, signal, pinned, fetchPage) {
2845
3097
  var fetchTool = createFetchTool();
2846
3098
 
2847
3099
  // src/tools/glob.ts
3100
+ import { statSync as statSync2 } from "fs";
2848
3101
  import { glob as nativeGlob } from "fs/promises";
3102
+ import { join as join4 } from "path";
2849
3103
  import { z as z4 } from "zod";
2850
3104
 
2851
3105
  // src/abort.ts
2852
3106
  var ABORTED = /* @__PURE__ */ Symbol("aborted");
2853
3107
  function raceAbort(promise, signal) {
2854
3108
  if (signal.aborted) return Promise.resolve(ABORTED);
2855
- return new Promise((resolve4, reject) => {
2856
- const onAbort = () => resolve4(ABORTED);
3109
+ return new Promise((resolve5, reject) => {
3110
+ const onAbort = () => resolve5(ABORTED);
2857
3111
  signal.addEventListener("abort", onAbort, { once: true });
2858
3112
  promise.then(
2859
3113
  (value) => {
2860
3114
  signal.removeEventListener("abort", onAbort);
2861
- resolve4(value);
3115
+ resolve5(value);
2862
3116
  },
2863
3117
  (error) => {
2864
3118
  signal.removeEventListener("abort", onAbort);
@@ -2884,22 +3138,38 @@ async function collectUntilAbort(iterable, signal, onItem) {
2884
3138
 
2885
3139
  // src/tools/ignore.ts
2886
3140
  import { readFileSync as readFileSync2 } from "fs";
2887
- import { join as join2 } from "path";
2888
- function ignorePatterns(root) {
3141
+ import { dirname as dirname2, join as join3, resolve as resolve3 } from "path";
3142
+ function ignorePatterns(root, projectRoot) {
2889
3143
  const patterns = ["**/node_modules/**", "**/.git/**"];
2890
- let lines = [];
2891
- try {
2892
- lines = readFileSync2(join2(root, ".gitignore"), "utf8").split(/\r?\n/);
2893
- } catch {
2894
- return patterns;
2895
- }
2896
- for (const raw of lines) {
2897
- const line = raw.trim();
2898
- if (!line || line.startsWith("#") || line.startsWith("!")) continue;
2899
- const p = line.replace(/^\//, "").replace(/\/$/, "");
2900
- 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
+ }
2901
3171
  }
2902
- return patterns;
3172
+ return [...new Set(patterns)];
2903
3173
  }
2904
3174
 
2905
3175
  // src/tools/globSafety.ts
@@ -2941,19 +3211,30 @@ var globTool = defineTool({
2941
3211
  path: z4.string().optional().describe("Base directory, default project root")
2942
3212
  }),
2943
3213
  kind: "read",
2944
- target: (input) => input.pattern,
3214
+ target: (input) => input.path ? `${input.pattern} ${input.path}` : input.pattern,
2945
3215
  async run(input, ctx) {
2946
3216
  const invalid = validateGlobPattern(input.pattern);
2947
3217
  if (invalid) return { output: invalid, isError: true };
2948
3218
  const root = resolvePath(ctx.cwd, input.path ?? ".");
2949
3219
  const files = [];
2950
3220
  const finished = await collectUntilAbort(
2951
- nativeGlob(input.pattern, { cwd: root, exclude: ignorePatterns(root) }),
3221
+ nativeGlob(input.pattern, { cwd: root, exclude: ignorePatterns(root, ctx.cwd) }),
2952
3222
  ctx.signal,
2953
3223
  (file) => files.push(normalizeGlobPath(file))
2954
3224
  );
2955
3225
  if (!finished) return { output: "Cancelled by the user.", isError: true };
2956
- 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
+ });
2957
3238
  if (!files.length) return { output: "No matching files." };
2958
3239
  const first = files.slice(0, 500);
2959
3240
  const shown = [];
@@ -2974,75 +3255,141 @@ var globTool = defineTool({
2974
3255
 
2975
3256
  // src/tools/grep.ts
2976
3257
  import { execFile } from "child_process";
3258
+ import { statSync as statSync3 } from "fs";
2977
3259
  import { readFile as readFile2 } from "fs/promises";
2978
3260
  import { glob as nativeGlob2 } from "fs/promises";
3261
+ import { basename, dirname as dirname3 } from "path";
2979
3262
  import { z as z5 } from "zod";
2980
3263
  var MAX_MATCHES = 200;
2981
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
+ }
2982
3275
  async function grepJs(pattern, opts) {
2983
- 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);
2984
3279
  const files = [];
2985
- const finished = await collectUntilAbort(
2986
- nativeGlob2(opts.glob ?? "**/*", { cwd: opts.root, exclude: ignorePatterns(opts.root) }),
2987
- opts.signal,
2988
- (file) => files.push(normalizeGlobPath(file))
2989
- );
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";
2990
3293
  const out = [];
2991
- if (!finished) return out;
2992
3294
  for (const file of files.sort()) {
2993
3295
  if (opts.signal?.aborted) break;
2994
3296
  let text;
2995
3297
  try {
2996
- const buffer = await readFile2(resolvePath(opts.root, file));
3298
+ const buffer = await readFile2(resolvePath(baseDir, file));
2997
3299
  if (buffer.subarray(0, 8e3).includes(0)) continue;
2998
3300
  text = buffer.toString("utf8");
2999
3301
  } catch {
3000
3302
  continue;
3001
3303
  }
3002
3304
  const lines = text.split(/\r?\n/);
3305
+ let fileMatches = 0;
3003
3306
  for (let i = 0; i < lines.length && out.length < MAX_MATCHES; i++) {
3004
- 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
+ }
3005
3322
  }
3323
+ if (mode === "count" && fileMatches > 0) out.push(`${file}:${fileMatches}`);
3006
3324
  if (out.length >= MAX_MATCHES) break;
3007
3325
  }
3008
3326
  return out;
3009
3327
  }
3010
3328
  function grepRg(pattern, opts) {
3011
- 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");
3012
3343
  if (opts.ignoreCase) args.push("-i");
3013
- if (opts.glob) args.push("-g", opts.glob);
3014
- args.push(".");
3015
- return new Promise((resolve4) => {
3016
- execFile("rg", args, { cwd: opts.root, maxBuffer: 10 * 1024 * 1024, signal: opts.signal }, (error, stdout) => {
3017
- 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([]);
3018
3349
  const code = error?.code;
3019
- if (code === "ENOENT") return resolve4(void 0);
3020
- if (error && error.code !== 1) return resolve4(void 0);
3021
- 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
+ );
3022
3355
  });
3023
3356
  });
3024
3357
  }
3025
3358
  var grepTool = defineTool({
3026
3359
  name: "grep",
3027
- 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).",
3028
3361
  schema: z5.object({
3029
- pattern: z5.string().describe("Regular expression"),
3030
- 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"),
3031
3364
  glob: z5.string().optional().describe('Only files matching this glob, e.g. "**/*.ts"'),
3032
- 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")
3033
3369
  }),
3034
3370
  kind: "read",
3035
- target: (input) => input.pattern,
3371
+ target: (input) => input.path ? `${input.pattern} ${input.path}` : input.pattern,
3036
3372
  async run(input, ctx) {
3037
3373
  if (input.glob) {
3038
3374
  const invalid = validateGlobPattern(input.glob);
3039
3375
  if (invalid) return { output: invalid, isError: true };
3040
3376
  }
3041
- const opts = { root: resolvePath(ctx.cwd, input.path ?? "."), glob: input.glob, ignoreCase: input.ignore_case, signal: ctx.signal };
3042
- try {
3043
- new RegExp(input.pattern);
3044
- } catch (error) {
3045
- 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
+ }
3046
3393
  }
3047
3394
  const matches = await grepRg(input.pattern, opts) ?? await grepJs(input.pattern, opts);
3048
3395
  if (ctx.signal.aborted) return { output: "Cancelled by the user.", isError: true };
@@ -3064,6 +3411,7 @@ var grepTool = defineTool({
3064
3411
  });
3065
3412
 
3066
3413
  // src/tools/read.ts
3414
+ import { readdirSync, statSync as statSync4 } from "fs";
3067
3415
  import { readFile as readFile3 } from "fs/promises";
3068
3416
  import { z as z6 } from "zod";
3069
3417
  var MAX_LINE = 2e3;
@@ -3147,6 +3495,16 @@ function createReadTool(options = {}) {
3147
3495
  target: (input) => input.path,
3148
3496
  async run(input, ctx) {
3149
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
+ }
3150
3508
  let buffer;
3151
3509
  try {
3152
3510
  buffer = await readFile3(abs);
@@ -3155,13 +3513,13 @@ function createReadTool(options = {}) {
3155
3513
  }
3156
3514
  const mediaType = vision ? imageMediaType(buffer) : void 0;
3157
3515
  if (mediaType) {
3158
- ctx.readFiles.add(abs);
3516
+ recordFileMtime(ctx.readFiles, abs);
3159
3517
  return readImageResult(input, buffer, mediaType);
3160
3518
  }
3161
3519
  if (buffer.subarray(0, 8e3).includes(0)) {
3162
3520
  return { output: `${input.path} is a binary file; not read.`, isError: true };
3163
3521
  }
3164
- ctx.readFiles.add(abs);
3522
+ recordFileMtime(ctx.readFiles, abs);
3165
3523
  return readTextResult(input, buffer);
3166
3524
  }
3167
3525
  });
@@ -3171,7 +3529,7 @@ var readTool = createReadTool({ vision: true });
3171
3529
  // src/tools/write.ts
3172
3530
  import { existsSync as existsSync4 } from "fs";
3173
3531
  import { mkdir, readFile as readFile4, writeFile as writeFile2 } from "fs/promises";
3174
- import { dirname as dirname2 } from "path";
3532
+ import { dirname as dirname4 } from "path";
3175
3533
  import { z as z7 } from "zod";
3176
3534
  var writeTool = defineTool({
3177
3535
  name: "write",
@@ -3198,12 +3556,12 @@ var writeTool = defineTool({
3198
3556
  }
3199
3557
  try {
3200
3558
  await ctx.checkpoint?.(abs);
3201
- await mkdir(dirname2(abs), { recursive: true });
3559
+ await mkdir(dirname4(abs), { recursive: true });
3202
3560
  await writeFile2(abs, input.content);
3203
3561
  } catch (error) {
3204
3562
  return { output: `Failed to write ${input.path}: ${errorCode(error)}`, isError: true };
3205
3563
  }
3206
- ctx.readFiles.add(abs);
3564
+ recordFileMtime(ctx.readFiles, abs);
3207
3565
  const lines = input.content.split("\n").length;
3208
3566
  return { output: `Wrote ${input.path} (${lines} lines).`, display: t("{lines} lines written", { lines }) };
3209
3567
  }
@@ -3313,12 +3671,36 @@ function budgetStatus(usage, cap, model) {
3313
3671
  }
3314
3672
 
3315
3673
  // src/tokenBudget.ts
3674
+ var IMAGE_TOKEN_COST = 1e3;
3316
3675
  var estimateTokens = (text) => Math.ceil(text.length / 4);
3317
- 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
+ }
3318
3700
  function trimToFit(messages, maxTokens) {
3319
3701
  let out = messages;
3320
3702
  while (estimateMessages(out) > maxTokens) {
3321
- const boundary = out.findIndex((m, i) => i > 0 && m.role === "user");
3703
+ const boundary = out.findIndex((m, i) => i > 0 && isUserTurnBoundary(m));
3322
3704
  if (boundary === -1) return out;
3323
3705
  out = out.slice(boundary);
3324
3706
  }
@@ -3328,13 +3710,13 @@ function splitKeepRecent(messages, keepTokens) {
3328
3710
  if (messages.length === 0) return [[], []];
3329
3711
  let start = messages.length;
3330
3712
  for (let i = messages.length - 1; i >= 0; i--) {
3331
- if (i > 0 && messages[i].role !== "user") continue;
3713
+ if (i > 0 && !isUserTurnBoundary(messages[i])) continue;
3332
3714
  if (estimateMessages(messages.slice(i)) <= keepTokens) start = i;
3333
3715
  else break;
3334
3716
  }
3335
3717
  if (start === messages.length) {
3336
3718
  for (let i = messages.length - 1; i >= 0; i--) {
3337
- if (messages[i].role === "user") {
3719
+ if (isUserTurnBoundary(messages[i])) {
3338
3720
  start = i;
3339
3721
  break;
3340
3722
  }
@@ -3382,6 +3764,7 @@ var Agent2 = class {
3382
3764
  this.messages = [...opts.history ?? []];
3383
3765
  this.maxSteps = typeof opts.maxSteps === "number" && Number.isFinite(opts.maxSteps) && opts.maxSteps > 0 ? Math.max(1, Math.floor(opts.maxSteps)) : void 0;
3384
3766
  this.contextWindow = opts.contextWindow ?? 128e3;
3767
+ this.budgetModel = opts.budgetModel ?? "";
3385
3768
  this.systemPrompt = opts.systemPrompt;
3386
3769
  this.reasoning = opts.reasoning ?? "auto";
3387
3770
  }
@@ -3405,6 +3788,7 @@ var Agent2 = class {
3405
3788
  maxSteps;
3406
3789
  // One window for every model; resolved from contextWindowFor() (catalog + env override).
3407
3790
  contextWindow;
3791
+ budgetModel;
3408
3792
  systemPrompt;
3409
3793
  setSystemPrompt(text) {
3410
3794
  this.systemPrompt = text;
@@ -3412,6 +3796,15 @@ var Agent2 = class {
3412
3796
  setContextWindow(window) {
3413
3797
  this.contextWindow = window;
3414
3798
  }
3799
+ getContextWindow() {
3800
+ return this.contextWindow;
3801
+ }
3802
+ setBudgetModel(model) {
3803
+ this.budgetModel = model;
3804
+ }
3805
+ getBudgetModel() {
3806
+ return this.budgetModel;
3807
+ }
3415
3808
  setTools(tools) {
3416
3809
  this.tools = tools;
3417
3810
  this.definitions = toolDefinitions(tools);
@@ -3612,7 +4005,7 @@ ${t("This model may not support tool calling. Try another model with /model.")}`
3612
4005
  }
3613
4006
  /** Current cap status; undefined means the budget is not exhausted. */
3614
4007
  overBudget() {
3615
- return budgetStatus(this.totalUsage, this.opts.usageCap, this.opts.budgetModel ?? "");
4008
+ return budgetStatus(this.totalUsage, this.opts.usageCap, this.budgetModel);
3616
4009
  }
3617
4010
  /** Every tool call needs an answer in the history, or the next request to the model is rejected. */
3618
4011
  cancelCalls(calls) {
@@ -3732,7 +4125,7 @@ ${t("This model may not support tool calling. Try another model with /model.")}`
3732
4125
  };
3733
4126
 
3734
4127
  // src/checkpoints.ts
3735
- import { existsSync as existsSync5, statSync } from "fs";
4128
+ import { existsSync as existsSync5, statSync as statSync5 } from "fs";
3736
4129
  import { readFile as readFile5, rm, writeFile as writeFile3 } from "fs/promises";
3737
4130
  var MAX_TURNS = 20;
3738
4131
  var MAX_BYTES2 = 2 * 1024 * 1024;
@@ -3759,8 +4152,8 @@ var CheckpointStore = class {
3759
4152
  history = [];
3760
4153
  current;
3761
4154
  pendingRedo;
4155
+ offset = 0;
3762
4156
  beginTurn() {
3763
- if (this.current?.size === 0) return;
3764
4157
  this.current = /* @__PURE__ */ new Map();
3765
4158
  this.history.push(this.current);
3766
4159
  this.pendingRedo = void 0;
@@ -3771,16 +4164,19 @@ var CheckpointStore = class {
3771
4164
  const turn = this.current;
3772
4165
  if (turn.has(path)) return;
3773
4166
  if (!existsSync5(path)) turn.set(path, null);
3774
- else if (statSync(path).size > MAX_BYTES2) turn.set(path, "skipped");
4167
+ else if (statSync5(path).size > MAX_BYTES2) turn.set(path, "skipped");
3775
4168
  else turn.set(path, await readFile5(path));
3776
- while (this.turns() > MAX_TURNS) this.history.shift();
4169
+ while (this.turns() > MAX_TURNS) {
4170
+ this.history.shift();
4171
+ this.offset++;
4172
+ }
3777
4173
  }
3778
4174
  turns() {
3779
4175
  return this.history.filter((t2) => t2.size).length;
3780
4176
  }
3781
4177
  /** Total entries including empty ones; used to align file snapshots with conversation turns. */
3782
4178
  entries() {
3783
- return this.history.length;
4179
+ return this.offset + this.history.length;
3784
4180
  }
3785
4181
  async undo() {
3786
4182
  while (this.history.length && !this.history.at(-1)?.size) this.history.pop();
@@ -3800,7 +4196,7 @@ var CheckpointStore = class {
3800
4196
  * pairs conversation turns with the entry count recorded when each turn started.
3801
4197
  */
3802
4198
  async undoTo(keep) {
3803
- 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));
3804
4200
  const result = emptyResult();
3805
4201
  const redo = /* @__PURE__ */ new Map();
3806
4202
  let applied = false;
@@ -3825,8 +4221,11 @@ var CheckpointStore = class {
3825
4221
  const turn = this.pendingRedo;
3826
4222
  if (!turn) return void 0;
3827
4223
  this.pendingRedo = void 0;
4224
+ const undoEntry = /* @__PURE__ */ new Map();
4225
+ await this.captureFor(turn, undoEntry);
3828
4226
  const result = emptyResult();
3829
4227
  await restoreTurn(turn, result);
4228
+ if (undoEntry.size) this.history.push(undoEntry);
3830
4229
  return result;
3831
4230
  }
3832
4231
  /** Records the on-disk state of a turn's paths so redo() can put it back. */
@@ -3839,7 +4238,7 @@ var CheckpointStore = class {
3839
4238
  }
3840
4239
  try {
3841
4240
  if (!existsSync5(path)) into.set(path, null);
3842
- else if (statSync(path).size > MAX_BYTES2) into.set(path, "skipped");
4241
+ else if (statSync5(path).size > MAX_BYTES2) into.set(path, "skipped");
3843
4242
  else into.set(path, await readFile5(path));
3844
4243
  } catch {
3845
4244
  into.set(path, "skipped");
@@ -3850,6 +4249,7 @@ var CheckpointStore = class {
3850
4249
  this.history.length = 0;
3851
4250
  this.current = void 0;
3852
4251
  this.pendingRedo = void 0;
4252
+ this.offset = 0;
3853
4253
  }
3854
4254
  };
3855
4255
 
@@ -3897,8 +4297,8 @@ import { z as z9 } from "zod";
3897
4297
 
3898
4298
  // src/mcp/config.ts
3899
4299
  import { createHash } from "crypto";
3900
- import { chmodSync as chmodSync2, existsSync as existsSync6, mkdirSync as mkdirSync2, readFileSync as readFileSync3, writeFileSync as writeFileSync2 } from "fs";
3901
- import { dirname as dirname3, join as join3, resolve as resolve3 } from "path";
4300
+ import { existsSync as existsSync6, mkdirSync as mkdirSync2, readFileSync as readFileSync3 } from "fs";
4301
+ import { dirname as dirname5, join as join5, resolve as resolve4 } from "path";
3902
4302
  var ENV_REF = /\$\{([A-Za-z_][A-Za-z0-9_]*)(?::-([^}]*))?\}/g;
3903
4303
  function expandEnvVars(config, env) {
3904
4304
  const expand = (value) => value.replace(ENV_REF, (match, name, fallback) => {
@@ -3920,33 +4320,39 @@ function expandEnvVars(config, env) {
3920
4320
 
3921
4321
  // src/mcp/oauth.ts
3922
4322
  import { spawn as spawn3 } from "child_process";
3923
- import { chmodSync as chmodSync3, existsSync as existsSync7, mkdirSync as mkdirSync3, readFileSync as readFileSync4, writeFileSync as writeFileSync3 } from "fs";
4323
+ import { existsSync as existsSync7, mkdirSync as mkdirSync3, readFileSync as readFileSync4 } from "fs";
3924
4324
  import { createServer } from "http";
3925
- import { dirname as dirname4, join as join4 } from "path";
4325
+ import { dirname as dirname6, join as join6 } from "path";
3926
4326
  import { auth } from "@modelcontextprotocol/sdk/client/auth.js";
3927
- var oauthStorePath = (home) => join4(home, "mcp-oauth.json");
4327
+ var oauthStorePath = (home) => join6(home, "mcp-oauth.json");
3928
4328
  function readStore(home) {
3929
4329
  const path = oauthStorePath(home);
3930
4330
  if (!existsSync7(path)) return {};
3931
4331
  try {
3932
4332
  return JSON.parse(readFileSync4(path, "utf8"));
3933
- } catch {
4333
+ } catch (error) {
4334
+ quarantineCorruptFile(path, error);
3934
4335
  return {};
3935
4336
  }
3936
4337
  }
3937
4338
  function writeStore(home, store) {
3938
4339
  const path = oauthStorePath(home);
3939
- mkdirSync3(dirname4(path), { recursive: true });
3940
- writeFileSync3(path, `${JSON.stringify(store, null, 2)}
3941
- `, { mode: 384 });
3942
- chmodSync3(path, 384);
4340
+ mkdirSync3(dirname6(path), { recursive: true });
4341
+ writeFileAtomic(path, `${JSON.stringify(store, null, 2)}
4342
+ `, 384);
3943
4343
  }
3944
4344
  function hasStoredTokens(home, serverUrl) {
3945
4345
  return Boolean(readStore(home)[serverUrl]?.tokens);
3946
4346
  }
3947
- function defaultOpen(url) {
3948
- const target = url.href;
3949
- const child = process.platform === "win32" ? spawn3("cmd", ["/c", "start", "", target], { detached: true, stdio: "ignore" }) : spawn3(process.platform === "darwin" ? "open" : "xdg-open", [target], { detached: true, stdio: "ignore" });
4347
+ function openCommand(url, platform) {
4348
+ if (url.protocol !== "http:" && url.protocol !== "https:") return void 0;
4349
+ if (platform === "win32") return { command: "rundll32.exe", args: ["url.dll,FileProtocolHandler", url.href] };
4350
+ return { command: platform === "darwin" ? "open" : "xdg-open", args: [url.href] };
4351
+ }
4352
+ function defaultOpen(url, deps = {}) {
4353
+ const plan = openCommand(url, deps.platform ?? process.platform);
4354
+ if (!plan) return;
4355
+ const child = (deps.spawn ?? spawn3)(plan.command, plan.args, { detached: true, stdio: "ignore" });
3950
4356
  child.unref();
3951
4357
  child.on("error", () => {
3952
4358
  });
@@ -4025,7 +4431,28 @@ var FileOAuthProvider = class {
4025
4431
  };
4026
4432
 
4027
4433
  // src/mcp/manager.ts
4028
- 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
+ }
4029
4456
  var CHILD_ENV_KEYS = [
4030
4457
  // portable
4031
4458
  "PATH",
@@ -4154,11 +4581,13 @@ var McpManager = class {
4154
4581
  async run(input, ctx) {
4155
4582
  try {
4156
4583
  const result = await client.callTool({ name: tool.name, arguments: input }, void 0, { signal: ctx.signal });
4157
- 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;
4158
4586
  return {
4159
- output: output.length > MCP_OUTPUT_MAX_CHARS ? `${output.slice(0, MCP_OUTPUT_MAX_CHARS)}
4160
- \u2026 [truncated at ${MCP_OUTPUT_MAX_CHARS} characters; narrow the MCP request if possible]` : output,
4161
- 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 } : {}
4162
4591
  };
4163
4592
  } catch (error) {
4164
4593
  return { output: `Server MCP ${server} error: ${error.message}`, isError: true };
@@ -4192,8 +4621,8 @@ import { homedir as homedir2 } from "os";
4192
4621
 
4193
4622
  // src/context.ts
4194
4623
  import { existsSync as existsSync8, readFileSync as readFileSync5 } from "fs";
4195
- import { dirname as dirname5, join as join5 } from "path";
4196
- var FILES = ["AGENTS.md", "BCCLI.md"];
4624
+ import { dirname as dirname7, join as join7 } from "path";
4625
+ var FILES = ["AGENTS.md", "CLAUDE.md", "BCCLI.md"];
4197
4626
  function readIf(path) {
4198
4627
  try {
4199
4628
  return existsSync8(path) ? readFileSync5(path, "utf8").trim() : void 0;
@@ -4203,11 +4632,11 @@ function readIf(path) {
4203
4632
  }
4204
4633
  function instructionPaths(cwd, home) {
4205
4634
  const dirs2 = [];
4206
- for (let dir = cwd; ; dir = dirname5(dir)) {
4635
+ for (let dir = cwd; ; dir = dirname7(dir)) {
4207
4636
  dirs2.unshift(dir);
4208
- if (dirname5(dir) === dir) break;
4637
+ if (dirname7(dir) === dir) break;
4209
4638
  }
4210
- 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)))];
4211
4640
  }
4212
4641
  function loadInstructions(cwd, home) {
4213
4642
  const parts = [];
@@ -4219,9 +4648,9 @@ ${text}`);
4219
4648
  return parts.join("\n\n");
4220
4649
  }
4221
4650
  function isGitRepo(cwd) {
4222
- for (let dir = cwd; ; dir = dirname5(dir)) {
4223
- if (existsSync8(join5(dir, ".git"))) return true;
4224
- 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;
4225
4654
  }
4226
4655
  }
4227
4656
  function buildSystemPrompt(opts) {
@@ -4264,16 +4693,34 @@ ${instructions}` : ""}`;
4264
4693
  }
4265
4694
 
4266
4695
  // src/extensions.ts
4267
- import { existsSync as existsSync9, readdirSync, readFileSync as readFileSync6, statSync as statSync2 } from "fs";
4268
- 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";
4269
4698
  function parseFrontmatter(text) {
4270
4699
  const match = /^---\r?\n([\s\S]*?)\r?\n---\r?\n?/.exec(text);
4271
4700
  if (!match) return { data: {}, body: text };
4272
4701
  const data = {};
4702
+ let currentListKey;
4273
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
+ }
4274
4712
  const kv = /^([A-Za-z0-9_-]+):\s*(.*)$/.exec(line);
4275
- if (!kv) continue;
4713
+ if (!kv) {
4714
+ if (line.trim()) currentListKey = void 0;
4715
+ continue;
4716
+ }
4276
4717
  let value = kv[2].trim();
4718
+ if (!value) {
4719
+ currentListKey = kv[1];
4720
+ data[kv[1]] = "";
4721
+ continue;
4722
+ }
4723
+ currentListKey = void 0;
4277
4724
  if (value.startsWith("[") && value.endsWith("]")) {
4278
4725
  data[kv[1]] = value.slice(1, -1).split(",").map((s) => s.trim().replace(/^["']|["']$/g, "")).filter(Boolean);
4279
4726
  continue;
@@ -4285,11 +4732,11 @@ function parseFrontmatter(text) {
4285
4732
  }
4286
4733
  var str = (v) => Array.isArray(v) ? v.join(", ") : v;
4287
4734
  function dirs(r, kind) {
4288
- 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)];
4289
4736
  }
4290
4737
  function list(dir) {
4291
4738
  try {
4292
- return readdirSync(dir).sort();
4739
+ return readdirSync2(dir).sort();
4293
4740
  } catch {
4294
4741
  return [];
4295
4742
  }
@@ -4298,11 +4745,11 @@ function loadSkills(r) {
4298
4745
  const byName = /* @__PURE__ */ new Map();
4299
4746
  for (const dir of dirs(r, "skills")) {
4300
4747
  for (const entry of list(dir)) {
4301
- const file = join6(dir, entry, "SKILL.md");
4748
+ const file = join8(dir, entry, "SKILL.md");
4302
4749
  if (!existsSync9(file)) continue;
4303
4750
  const { data } = parseFrontmatter(readFileSync6(file, "utf8"));
4304
4751
  const name = str(data.name) || entry;
4305
- 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 });
4306
4753
  }
4307
4754
  }
4308
4755
  return [...byName.values()];
@@ -4311,10 +4758,10 @@ function loadCommands(r) {
4311
4758
  const byName = /* @__PURE__ */ new Map();
4312
4759
  for (const dir of dirs(r, "commands")) {
4313
4760
  for (const entry of list(dir)) {
4314
- const file = join6(dir, entry);
4315
- if (!entry.endsWith(".md") || !statSync2(file).isFile()) continue;
4761
+ const file = join8(dir, entry);
4762
+ if (!entry.endsWith(".md") || !statSync6(file).isFile()) continue;
4316
4763
  const { data, body } = parseFrontmatter(readFileSync6(file, "utf8"));
4317
- const def = { name: basename(entry, ".md"), body: body.trim() };
4764
+ const def = { name: basename2(entry, ".md"), body: body.trim() };
4318
4765
  if (data.description) def.description = str(data.description);
4319
4766
  if (data["argument-hint"]) def.argumentHint = str(data["argument-hint"]);
4320
4767
  byName.set(def.name, def);
@@ -4327,8 +4774,8 @@ function loadAgentDefs(r) {
4327
4774
  for (const dir of dirs(r, "agents")) {
4328
4775
  for (const entry of list(dir)) {
4329
4776
  if (!entry.endsWith(".md")) continue;
4330
- const { data, body } = parseFrontmatter(readFileSync6(join6(dir, entry), "utf8"));
4331
- 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");
4332
4779
  const def = { name, description: str(data.description) ?? "", prompt: body.trim() };
4333
4780
  if (data.tools)
4334
4781
  def.tools = Array.isArray(data.tools) ? data.tools : String(data.tools).split(",").map((s) => s.trim()).filter(Boolean);
@@ -4346,8 +4793,7 @@ function loadAgentDefs(r) {
4346
4793
  }
4347
4794
 
4348
4795
  // src/providers.ts
4349
- import { mkdirSync as mkdirSync4, writeFileSync as writeFileSync4 } from "fs";
4350
- import { join as join7 } from "path";
4796
+ import { join as join9 } from "path";
4351
4797
  function providerName(config, id) {
4352
4798
  return PRESETS.find((p) => p.id === id)?.name ?? config.providers[id]?.name ?? id;
4353
4799
  }
@@ -4406,11 +4852,11 @@ async function addProviderKey(providerId, key, deps) {
4406
4852
 
4407
4853
  // src/session.ts
4408
4854
  import { randomBytes as randomBytes2 } from "crypto";
4409
- import { appendFileSync, existsSync as existsSync10, mkdirSync as mkdirSync5, readdirSync as readdirSync2, readFileSync as readFileSync7, rmSync as rmSync2, statSync as statSync3 } from "fs";
4410
- 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";
4411
4857
  function projectDir(home, cwd) {
4412
4858
  const slug = cwd.replace(/[^a-zA-Z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(-100) || "root";
4413
- return join8(home, "sessions", slug);
4859
+ return join10(home, "sessions", slug);
4414
4860
  }
4415
4861
  function repairTail(messages) {
4416
4862
  const out = [...messages];
@@ -4428,19 +4874,22 @@ var Session = class _Session {
4428
4874
  this.file = file;
4429
4875
  }
4430
4876
  file;
4877
+ get id() {
4878
+ return basename3(this.file, ".jsonl");
4879
+ }
4431
4880
  static create(home, cwd, now = /* @__PURE__ */ new Date()) {
4432
4881
  const dir = projectDir(home, cwd);
4433
- mkdirSync5(dir, { recursive: true });
4882
+ mkdirSync4(dir, { recursive: true });
4434
4883
  const stamp = now.toISOString().replace(/[:.]/g, "-");
4435
- return new _Session(join8(dir, `${stamp}-${randomBytes2(3).toString("hex")}.jsonl`));
4884
+ return new _Session(join10(dir, `${stamp}-${randomBytes2(3).toString("hex")}.jsonl`));
4436
4885
  }
4437
4886
  static list(home, cwd) {
4438
4887
  const dir = projectDir(home, cwd);
4439
4888
  if (!existsSync10(dir)) return [];
4440
- return readdirSync2(dir).filter((f) => f.endsWith(".jsonl")).map((f) => {
4441
- const session = new _Session(join8(dir, f));
4442
- const first = session.load().find((m) => m.role === "user");
4443
- 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) : "" };
4444
4893
  }).sort((a, b) => b.mtime.getTime() - a.mtime.getTime() || b.session.file.localeCompare(a.session.file));
4445
4894
  }
4446
4895
  static latest(home, cwd) {
@@ -4480,6 +4929,44 @@ var Session = class _Session {
4480
4929
  }
4481
4930
  return repairTail(messages);
4482
4931
  }
4932
+ /** Model of the most recent turn that recorded usage — serve attach continues a session with its own model. */
4933
+ lastModel() {
4934
+ const records = this.records();
4935
+ for (let i = records.length - 1; i >= 0; i--) {
4936
+ const record = records[i];
4937
+ if (record.t === "usage" && record.model) return record.model;
4938
+ }
4939
+ return void 0;
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
+ }
4483
4970
  /**
4484
4971
  * What the user sees on resume: the whole conversation. Compaction only shrinks what the model is sent, so
4485
4972
  * its reset does not hide the older turns here. A real reset (/clear, rewind) still starts over, the
@@ -4492,12 +4979,22 @@ var Session = class _Session {
4492
4979
  else if (record.t === "msg") segments[segments.length - 1].push(record.m);
4493
4980
  }
4494
4981
  let shown = segments[0];
4982
+ let lastSummary;
4495
4983
  for (const segment of segments.slice(1)) {
4496
4984
  if (!isCompactionSummary(segment[0])) {
4497
4985
  shown = segment;
4986
+ lastSummary = void 0;
4498
4987
  continue;
4499
4988
  }
4500
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];
4501
4998
  let at = 0;
4502
4999
  while (at <= shown.length && !endsWithPrefixOf(shown, at, kept)) at++;
4503
5000
  shown = [...shown, ...kept.slice(shown.length - at)];
@@ -4524,6 +5021,20 @@ function endsWithPrefixOf(shown, at, kept) {
4524
5021
  for (let j = 0; j < n; j++) if (!sameMessage2(shown[at + j], kept[j])) return false;
4525
5022
  return true;
4526
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
+ }
4527
5038
 
4528
5039
  // src/tools/plan.ts
4529
5040
  import { z as z10 } from "zod";
@@ -4633,6 +5144,7 @@ import { z as z13 } from "zod";
4633
5144
  function createTaskTool(opts) {
4634
5145
  const byName = new Map(opts.agents.map((a) => [a.name, a]));
4635
5146
  const list2 = opts.agents.map((a) => `- ${a.name}: ${a.description}`).join("\n");
5147
+ const sessions = /* @__PURE__ */ new Map();
4636
5148
  let askChain = Promise.resolve();
4637
5149
  const enqueueAsk = (fn) => {
4638
5150
  const next = askChain.then(
@@ -4652,7 +5164,8 @@ ${list2}`,
4652
5164
  schema: z13.object({
4653
5165
  agent: z13.string().describe("Agent name"),
4654
5166
  description: z13.string().describe("3-6 word summary shown to the user"),
4655
- 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")
4656
5169
  }),
4657
5170
  kind: "read",
4658
5171
  target: (input) => `[${input.agent}] ${input.description}`,
@@ -4679,6 +5192,7 @@ ${list2}`,
4679
5192
  } catch (error) {
4680
5193
  return { output: `Agent "${def.name}": ${error.message}`, isError: true };
4681
5194
  }
5195
+ const priorHistory = input.session_id ? sessions.get(input.session_id) : void 0;
4682
5196
  const child = new Agent2({
4683
5197
  provider,
4684
5198
  tools,
@@ -4688,6 +5202,7 @@ ${list2}`,
4688
5202
  # Your role
4689
5203
  ${def.prompt}`,
4690
5204
  cwd: opts.cwd,
5205
+ ...priorHistory ? { history: priorHistory } : {},
4691
5206
  // Every subagent runs with a step cap so a looping child cannot burn tokens forever.
4692
5207
  maxSteps: def.maxSteps ?? 50,
4693
5208
  label: def.name,
@@ -4708,12 +5223,29 @@ ${def.prompt}`,
4708
5223
  const ask = ctx.ask;
4709
5224
  if (ask) child.askPermission = (req) => enqueueAsk(() => ask(req));
4710
5225
  await child.run(input.prompt, ctx.signal);
5226
+ if (input.session_id) sessions.set(input.session_id, [...child.messages]);
4711
5227
  ctx.addUsage?.(child.totalUsage);
4712
5228
  const tokens2 = child.totalUsage.inputTokens + child.totalUsage.outputTokens;
4713
5229
  const display = t("{steps} steps \xB7 {tokens} tokens", { steps, tokens: tokens2 >= 1e3 ? `${Math.round(tokens2 / 1e3)}k` : tokens2 });
4714
5230
  if (outcome?.type === "error") return { output: `Subagent ${def.name} failed: ${outcome.message}`, isError: true, display };
4715
5231
  if (outcome?.type === "aborted") return { output: `Subagent ${def.name} was cancelled.`, isError: true, display };
4716
- 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
+ }
4717
5249
  const report = last && typeof last.content === "string" ? last.content : "(no report)";
4718
5250
  const suffix = outcome?.type === "stepLimit" ? `
4719
5251
 
@@ -4759,7 +5291,7 @@ function createTodoTool(store) {
4759
5291
 
4760
5292
  // src/tools/memory.ts
4761
5293
  import { existsSync as existsSync12, readFileSync as readFileSync10 } from "fs";
4762
- import { join as join9 } from "path";
5294
+ import { join as join11 } from "path";
4763
5295
  import { z as z15 } from "zod";
4764
5296
 
4765
5297
  // src/slash/memory.ts
@@ -4782,11 +5314,11 @@ function createSaveMemoryTool(opts) {
4782
5314
  kind: "edit",
4783
5315
  // The target is what the prompt shows and what the permission model judges: the
4784
5316
  // global store really lives outside the project, so it must not look like one.
4785
- 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",
4786
5318
  async run(input, ctx) {
4787
5319
  const content = input.content.trim();
4788
5320
  if (!content) return { output: "Memory content is empty.", isError: true };
4789
- 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");
4790
5322
  if (existsSync12(file)) {
4791
5323
  const lines = readFileSync10(file, "utf8").split("\n");
4792
5324
  if (lines.some((line) => line.trim() === `- ${content}`)) {
@@ -4804,18 +5336,21 @@ function createSaveMemoryTool(opts) {
4804
5336
  import { z as z16 } from "zod";
4805
5337
 
4806
5338
  // src/repomap.ts
4807
- import { readdirSync as readdirSync3, readFileSync as readFileSync11 } from "fs";
4808
- 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";
4809
5341
  var SKIP_DIRS = /* @__PURE__ */ new Set([
4810
5342
  "node_modules",
4811
5343
  ".git",
4812
5344
  "dist",
4813
5345
  "build",
5346
+ "out",
4814
5347
  "coverage",
4815
5348
  ".next",
5349
+ ".turbo",
4816
5350
  "target",
4817
5351
  "vendor",
4818
5352
  "__pycache__",
5353
+ "venv",
4819
5354
  ".venv",
4820
5355
  ".cache"
4821
5356
  ]);
@@ -4841,21 +5376,22 @@ var CODE_EXTS = /* @__PURE__ */ new Set([
4841
5376
  ".ps1"
4842
5377
  ]);
4843
5378
  var MAX_FILES = 400;
5379
+ var MAX_SCAN_FILES = 5e3;
4844
5380
  var MAX_FILE_BYTES = 2e5;
4845
5381
  var MAX_SYMBOLS_PER_FILE = 12;
4846
5382
  var FAMILY = {
4847
5383
  ts: [
4848
- /^\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_$]+)/,
4849
5385
  /^\s*export\s+(?:const|let)\s+([A-Za-z0-9_$]+)/
4850
5386
  ],
4851
- py: [/^\s*(?:async\s+)?def\s+(\w+)/, /^\s*class\s+(\w+)/],
5387
+ py: [/^\s*(?:async\s+)?def\s+(\w+)/, /^\s*(?:module|class)\s+(\w+)/],
4852
5388
  go: [/^func\s+(?:\([^)]*\)\s*)?(\w+)/, /^type\s+(\w+)/],
4853
5389
  rs: [/^\s*(?:pub(?:\([^)]*\))?\s+)?(?:unsafe\s+)?(?:async\s+)?(?:fn|struct|enum|trait|type)\s+(\w+)/],
4854
5390
  c: [/^\s*(?:static\s+|inline\s+|extern\s+)*[A-Za-z_][A-Za-z0-9_\s*]*\b(\w+)\s*\([^;]*\)\s*\{?\s*$/]
4855
5391
  };
4856
5392
  function familyFor(ext) {
4857
- if ([".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs", ".java", ".cs", ".php", ".rb"].includes(ext)) return FAMILY.ts;
4858
- 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;
4859
5395
  if (ext === ".go") return FAMILY.go;
4860
5396
  if (ext === ".rs") return FAMILY.rs;
4861
5397
  return FAMILY.c;
@@ -4875,16 +5411,16 @@ function extractSymbols(text, ext) {
4875
5411
  return out;
4876
5412
  }
4877
5413
  function walk(dir, root, files) {
4878
- if (files.length >= MAX_FILES) return;
5414
+ if (files.length >= MAX_SCAN_FILES) return;
4879
5415
  let entries;
4880
5416
  try {
4881
- entries = readdirSync3(dir, { withFileTypes: true });
5417
+ entries = readdirSync4(dir, { withFileTypes: true });
4882
5418
  } catch {
4883
5419
  return;
4884
5420
  }
4885
5421
  for (const e of entries.sort((a, b) => a.name.localeCompare(b.name))) {
4886
- if (files.length >= MAX_FILES) return;
4887
- const full = join10(dir, e.name);
5422
+ if (files.length >= MAX_SCAN_FILES) return;
5423
+ const full = join12(dir, e.name);
4888
5424
  if (e.isDirectory()) {
4889
5425
  if (SKIP_DIRS.has(e.name) || e.name.startsWith(".") && full !== root) continue;
4890
5426
  walk(full, root, files);
@@ -4895,16 +5431,17 @@ function walk(dir, root, files) {
4895
5431
  }
4896
5432
  function buildRepoMapSync(cwd, opts = {}) {
4897
5433
  const maxTokens = opts.maxTokens ?? 2048;
4898
- const files = [];
4899
- walk(cwd, cwd, files);
4900
- files.sort(
4901
- (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)
4902
5438
  );
5439
+ const files = scanned.slice(0, MAX_FILES);
4903
5440
  const lines = [];
4904
5441
  let used = 0;
4905
5442
  let truncated = false;
4906
5443
  for (const file of files) {
4907
- const rel = relative2(cwd, file).split(sep2).join("/");
5444
+ const rel = relative2(cwd, file).split(sep).join("/");
4908
5445
  let symbols = [];
4909
5446
  try {
4910
5447
  const text = readFileSync11(file, "utf8");
@@ -4939,13 +5476,50 @@ function createRepoMapTool() {
4939
5476
  }
4940
5477
 
4941
5478
  // src/tools/diagnostics.ts
4942
- import { extname as extname2 } from "path";
5479
+ import { extname as extname3 } from "path";
4943
5480
  import { z as z17 } from "zod";
4944
5481
 
4945
5482
  // src/lsp.ts
4946
5483
  import { spawn as spawn4 } from "child_process";
4947
5484
  import { readFileSync as readFileSync12 } from "fs";
5485
+ import { extname as extname2 } from "path";
4948
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
+ }
4949
5523
  function encodeLsp(msg) {
4950
5524
  const body = Buffer.from(JSON.stringify(msg), "utf8");
4951
5525
  return Buffer.concat([Buffer.from(`Content-Length: ${body.length}\r
@@ -4978,8 +5552,12 @@ function createLspParser(onMessage) {
4978
5552
  };
4979
5553
  }
4980
5554
  function getDiagnostics(opts) {
4981
- return new Promise((resolve4) => {
4982
- 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
+ });
4983
5561
  const uri = pathToFileURL(opts.file).href;
4984
5562
  let settled = false;
4985
5563
  let nextId = 2;
@@ -4993,6 +5571,7 @@ function getDiagnostics(opts) {
4993
5571
  if (settled) return;
4994
5572
  settled = true;
4995
5573
  clearTimeout(timer);
5574
+ opts.signal?.removeEventListener("abort", onAbort);
4996
5575
  send({ jsonrpc: "2.0", id: nextId++, method: "shutdown" });
4997
5576
  send({ jsonrpc: "2.0", method: "exit" });
4998
5577
  const killer = setTimeout(() => {
@@ -5002,8 +5581,11 @@ function getDiagnostics(opts) {
5002
5581
  }
5003
5582
  }, 500);
5004
5583
  killer.unref?.();
5005
- resolve4(diags);
5584
+ resolve5(diags);
5006
5585
  };
5586
+ const onAbort = () => finish([]);
5587
+ if (opts.signal?.aborted) return finish([]);
5588
+ opts.signal?.addEventListener("abort", onAbort, { once: true });
5007
5589
  const timer = setTimeout(() => finish([]), opts.timeoutMs ?? 8e3);
5008
5590
  const parser = createLspParser((msg) => {
5009
5591
  if (msg.id === 1 && msg.result) {
@@ -5016,7 +5598,7 @@ function getDiagnostics(opts) {
5016
5598
  send({
5017
5599
  jsonrpc: "2.0",
5018
5600
  method: "textDocument/didOpen",
5019
- params: { textDocument: { uri, languageId: "plaintext", version: 1, text } }
5601
+ params: { textDocument: { uri, languageId: languageIdFor(opts.file), version: 1, text } }
5020
5602
  });
5021
5603
  }
5022
5604
  if (msg.method === "textDocument/publishDiagnostics" && msg.params?.uri === uri) {
@@ -5054,7 +5636,7 @@ function createDiagnosticsTool(servers) {
5054
5636
  target: (input) => input.path,
5055
5637
  async run(input, ctx) {
5056
5638
  const file = resolvePath(ctx.cwd, input.path);
5057
- const ext = extname2(file).toLowerCase();
5639
+ const ext = extname3(file).toLowerCase();
5058
5640
  const server = servers.find((s) => s.extensions.includes(ext));
5059
5641
  if (!server) {
5060
5642
  return { output: t("No LSP server is configured for {ext} files.", { ext: ext || "(none)" }), isError: true };
@@ -5070,7 +5652,7 @@ function createDiagnosticsTool(servers) {
5070
5652
  // src/plugins.ts
5071
5653
  import { existsSync as existsSync13 } from "fs";
5072
5654
  import { createRequire } from "module";
5073
- import { isAbsolute as isAbsolute3, join as join11 } from "path";
5655
+ import { isAbsolute as isAbsolute3, join as join13 } from "path";
5074
5656
  import { z as z18 } from "zod";
5075
5657
  var require_ = createRequire(import.meta.url);
5076
5658
  function pluginLog(message) {
@@ -5095,7 +5677,7 @@ function loadPlugins(specs, opts) {
5095
5677
  for (const spec of specs) {
5096
5678
  let file;
5097
5679
  try {
5098
- 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);
5099
5681
  } catch {
5100
5682
  throw new ConfigError(t("Plugin not found: {spec}", { spec }));
5101
5683
  }
@@ -5172,7 +5754,7 @@ function createRuntime(opts) {
5172
5754
  let config = loadConfig(opts.cwd, env);
5173
5755
  let modelRef = opts.args.model ?? config.model;
5174
5756
  let reasoning = opts.args.reasoning ?? config.reasoning;
5175
- const makeProvider = (ref) => {
5757
+ const makeSingleProvider = (ref) => {
5176
5758
  const resolved = resolveModel(config, ref, env);
5177
5759
  return opts.provider ?? createProvider({
5178
5760
  baseURL: resolved.baseURL,
@@ -5184,6 +5766,25 @@ function createRuntime(opts) {
5184
5766
  enableProgrammaticToolCalling: lunaPtcAutoEnabled(env)
5185
5767
  });
5186
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
+ };
5187
5788
  const provider = makeProvider(modelRef);
5188
5789
  const mode = opts.args.allowAll ? "allowAll" : opts.args.permissionMode ?? config.permissionMode;
5189
5790
  const rules = [...config.allow, ...opts.args.allowedTools.map((t2) => TOOL_RULES[t2] ?? t2)];
@@ -5205,6 +5806,10 @@ function createRuntime(opts) {
5205
5806
  if (latest) {
5206
5807
  session = latest;
5207
5808
  history = latest.load();
5809
+ if (!opts.args.model) {
5810
+ const prevModel = latest.lastModel();
5811
+ if (prevModel) modelRef = prevModel;
5812
+ }
5208
5813
  }
5209
5814
  }
5210
5815
  const repoMap = config.repoMap ? buildRepoMapSync(opts.cwd) : void 0;
@@ -5248,7 +5853,7 @@ function createRuntime(opts) {
5248
5853
  // Subagents get whatever the main agent has right now (incl. MCP tools), minus task itself.
5249
5854
  baseTools: () => agent.tools.filter((t2) => t2.name !== "task"),
5250
5855
  permissions,
5251
- provider: () => agent.provider,
5856
+ provider: () => opts.provider ? agent.provider : makeProvider(modelRef),
5252
5857
  providerFor: makeProvider,
5253
5858
  systemPrompt: (childModelRef) => systemFor(childModelRef ?? modelRef),
5254
5859
  cwd: opts.cwd,
@@ -5334,6 +5939,20 @@ function createRuntime(opts) {
5334
5939
  session = previous;
5335
5940
  agent.load(previous.load());
5336
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
+ }
5337
5956
  todos.set([]);
5338
5957
  checkpoints.clear();
5339
5958
  turnMarks.length = 0;
@@ -5382,6 +6001,7 @@ function createRuntime(opts) {
5382
6001
  const nextProvider = makeProvider(ref);
5383
6002
  modelRef = ref;
5384
6003
  agent.provider = nextProvider;
6004
+ agent.setBudgetModel(ref);
5385
6005
  agent.setContextWindow(contextWindowFor(ref, env));
5386
6006
  agent.setSystemPrompt(systemFor(modelRef));
5387
6007
  },
@@ -5407,6 +6027,7 @@ async function* streamTask(opts) {
5407
6027
  let finished = false;
5408
6028
  let text = "";
5409
6029
  let stopReason = "done";
6030
+ let errorMessage2;
5410
6031
  const usage = { inputTokens: 0, outputTokens: 0 };
5411
6032
  const toolCalls = [];
5412
6033
  const byId = /* @__PURE__ */ new Map();
@@ -5456,28 +6077,45 @@ async function* streamTask(opts) {
5456
6077
  case "aborted":
5457
6078
  stopReason = "aborted";
5458
6079
  break;
6080
+ case "error":
6081
+ stopReason = "error";
6082
+ errorMessage2 = event.message;
6083
+ push({ type: "error", message: event.message });
6084
+ break;
5459
6085
  default:
5460
6086
  break;
5461
6087
  }
5462
6088
  };
5463
- const running = rt.agent.run(opts.prompt, opts.signal ?? new AbortController().signal).catch(() => {
5464
- 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);
5465
6098
  }).finally(() => {
5466
6099
  finished = true;
5467
6100
  wake?.();
5468
6101
  });
5469
- while (!finished || queue.length) {
5470
- if (queue.length) {
5471
- yield queue.shift();
5472
- 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;
5473
6112
  }
5474
- await new Promise((resolve4) => {
5475
- wake = resolve4;
5476
- });
5477
- 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();
5478
6118
  }
5479
- await running;
5480
- yield { type: "result", result: { text, toolCalls, usage, stopReason } };
5481
6119
  }
5482
6120
  async function runTask(opts) {
5483
6121
  let out = { text: "", toolCalls: [], usage: { inputTokens: 0, outputTokens: 0 }, stopReason: "done" };