@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
@@ -5,66 +5,71 @@ import {
5
5
  pruneOldToolOutputs,
6
6
  splitKeepRecent,
7
7
  trimToFit
8
- } from "./chunk-QZKLZELZ.js";
8
+ } from "./chunk-OK2USAXX.js";
9
9
  import {
10
10
  BUILTIN_AGENTS,
11
11
  appendMemory,
12
12
  bashTool,
13
13
  buildSystemPrompt,
14
14
  createBashTool,
15
+ errorCode,
15
16
  ignorePatterns,
17
+ isFileModifiedSinceRead,
16
18
  loadAgentDefs,
17
19
  loadCommands,
18
20
  loadSkills,
19
21
  normalizeGlobPath,
20
22
  parseFrontmatter,
23
+ recordFileMtime,
24
+ resolvePath,
21
25
  runHooks,
22
26
  validateGlobPattern
23
- } from "./chunk-M7CALSP3.js";
27
+ } from "./chunk-ZEPTUKNW.js";
24
28
  import {
25
29
  createWebSearchTool,
26
30
  fetchTool
27
- } from "./chunk-45SDLMTV.js";
31
+ } from "./chunk-JXM6REER.js";
28
32
  import {
29
33
  defineTool
30
34
  } from "./chunk-PSO7R5AD.js";
31
35
  import {
32
36
  Permissions
33
- } from "./chunk-O7M3TGIW.js";
37
+ } from "./chunk-WFZKJY3R.js";
34
38
  import {
35
39
  addProviderKey
36
- } from "./chunk-JL5ZCSP2.js";
40
+ } from "./chunk-H7XJDKJY.js";
37
41
  import {
38
42
  listAllModels
39
- } from "./chunk-XBO5GACQ.js";
43
+ } from "./chunk-IYR5PYA3.js";
40
44
  import {
41
45
  providerName
42
- } from "./chunk-SPRNLS7K.js";
46
+ } from "./chunk-HQW4BROS.js";
43
47
  import {
44
48
  FileOAuthProvider,
45
49
  hasStoredTokens
46
- } from "./chunk-7F3IWBPQ.js";
50
+ } from "./chunk-3I5BW3ZT.js";
47
51
  import {
48
52
  Session
49
- } from "./chunk-QDQU6VY2.js";
53
+ } from "./chunk-SVD4LHJC.js";
50
54
  import {
51
55
  completionAssistantMessage,
52
56
  createProvider,
53
57
  hasPendingProgrammaticReplay,
54
- imageUserMessage
55
- } from "./chunk-PUN4HMAG.js";
58
+ imageUserMessage,
59
+ withFallbackProvider
60
+ } from "./chunk-5YJ3ZQVR.js";
56
61
  import {
57
62
  expandEnvVars
58
- } from "./chunk-L7QI6JN2.js";
63
+ } from "./chunk-6M55UPES.js";
59
64
  import {
60
65
  ConfigError,
61
66
  bccliHome,
62
67
  loadConfig,
63
68
  resolveModel
64
- } from "./chunk-LACUWBWS.js";
69
+ } from "./chunk-RX4UQX43.js";
65
70
  import {
66
71
  t
67
- } from "./chunk-UZWJQF67.js";
72
+ } from "./chunk-3WHI4LN3.js";
68
73
 
69
74
  // src/textToolCalls.ts
70
75
  var PAIR = /(?:<arg_key>\s*|`)?([A-Za-z_]\w*)(?:\s*<\/arg_key>)?`?\s*(?:=\s*`|<arg_value>)([\s\S]*?)<\/arg_value>/g;
@@ -164,47 +169,108 @@ function formatDiff(lines) {
164
169
  return lines.map((l) => `${String(l.line).padStart(5)} ${mark[l.kind]} ${l.text}`).join("\n");
165
170
  }
166
171
 
167
- // src/tools/paths.ts
168
- import { isAbsolute, resolve } from "path";
169
- function resolvePath(cwd, path) {
170
- return isAbsolute(path) ? path : resolve(cwd, path);
171
- }
172
- function errorCode(error) {
173
- return error.code ?? error.message;
174
- }
175
-
176
172
  // src/tools/edit.ts
177
- var schema = z.object({
178
- path: z.string().describe("File path, relative to the project or absolute"),
173
+ var editEntrySchema = z.object({
179
174
  old_string: z.string().describe("Exact text to replace; must be unique unless replace_all"),
180
175
  new_string: z.string().describe("Replacement text"),
181
176
  replace_all: z.boolean().optional().describe("Replace every occurrence")
182
177
  });
178
+ var schema = z.object({
179
+ path: z.string().describe("File path, relative to the project or absolute"),
180
+ old_string: z.string().optional().describe("Exact text to replace; must be unique unless replace_all"),
181
+ new_string: z.string().optional().describe("Replacement text"),
182
+ replace_all: z.boolean().optional().describe("Replace every occurrence"),
183
+ edits: z.array(editEntrySchema).optional().describe("Optional list of sequential edits to apply atomically to the same file")
184
+ });
183
185
  function countOccurrences(haystack, needle) {
184
186
  let count = 0;
185
187
  for (let i = haystack.indexOf(needle); i !== -1; i = haystack.indexOf(needle, i + needle.length)) count++;
186
188
  return count;
187
189
  }
188
- function applyEdit(content, input) {
190
+ function normalizeTypographic(s) {
191
+ return s.replace(/[\u2018\u2019\u201A\u201B]/g, "'").replace(/[\u201C\u201D\u201E\u201F]/g, '"').replace(/[\u2013\u2014]/g, "-").replace(/\u00A0/g, " ");
192
+ }
193
+ function stripReadLinePrefixes(s) {
194
+ const lines = s.split(/\r?\n/);
195
+ if (lines.every((l) => !l.trim() || /^\s*\d+\t/.test(l))) {
196
+ return lines.map((l) => l.replace(/^\s*\d+\t/, "")).join("\n");
197
+ }
198
+ return s;
199
+ }
200
+ function matchByNormalizedLines(content, oldText, newText, replaceAll) {
201
+ const eol = content.includes("\r\n") ? "\r\n" : "\n";
202
+ const fileLines = content.split(/\r?\n/);
203
+ const oldLines = stripReadLinePrefixes(oldText).split(/\r?\n/);
204
+ if (!oldLines.length) return { count: 0 };
205
+ const normLine = (l) => normalizeTypographic(l).trimEnd();
206
+ const normOld = oldLines.map(normLine);
207
+ const matches = [];
208
+ for (let i = 0; i + oldLines.length <= fileLines.length; i++) {
209
+ let ok = true;
210
+ for (let j = 0; j < oldLines.length; j++) {
211
+ if (normLine(fileLines[i + j]) !== normOld[j]) {
212
+ ok = false;
213
+ break;
214
+ }
215
+ }
216
+ if (ok) {
217
+ matches.push(i);
218
+ if (replaceAll) i += oldLines.length - 1;
219
+ }
220
+ }
221
+ if (matches.length === 0) return { count: 0 };
222
+ if (matches.length > 1 && !replaceAll) return { count: matches.length };
223
+ const replacementLines = stripReadLinePrefixes(newText).split(/\r?\n/);
224
+ const outLines = [...fileLines];
225
+ for (let m = matches.length - 1; m >= 0; m--) {
226
+ outLines.splice(matches[m], oldLines.length, ...replacementLines);
227
+ }
228
+ return { count: matches.length, result: outLines.join(eol) };
229
+ }
230
+ function applySingleEdit(content, path, input) {
189
231
  if (input.old_string === input.new_string) {
190
232
  return { output: "old_string and new_string are identical; nothing to change.", isError: true };
191
233
  }
192
234
  const eol = content.includes("\r\n") ? "\r\n" : "\n";
193
235
  const toEol = (s) => s.replace(/\r?\n/g, eol);
236
+ const strippedOld = stripReadLinePrefixes(input.old_string);
237
+ const strippedNew = stripReadLinePrefixes(input.new_string);
194
238
  const attempts = [[input.old_string, input.new_string]];
195
239
  if (toEol(input.old_string) !== input.old_string) attempts.push([toEol(input.old_string), toEol(input.new_string)]);
240
+ if (strippedOld !== input.old_string) {
241
+ attempts.push([strippedOld, strippedNew], [toEol(strippedOld), toEol(strippedNew)]);
242
+ }
196
243
  for (const [oldText, newText] of attempts) {
197
244
  const count = oldText ? countOccurrences(content, oldText) : 0;
198
245
  if (count === 0) continue;
199
246
  if (count > 1 && !input.replace_all) {
200
247
  return {
201
- output: `old_string appears ${count} times in ${input.path}. Add more context to make it unique, or use replace_all.`,
248
+ output: `old_string appears ${count} times in ${path}. Add more context to make it unique, or use replace_all.`,
202
249
  isError: true
203
250
  };
204
251
  }
205
252
  return input.replace_all ? content.split(oldText).join(newText) : content.replace(oldText, () => newText);
206
253
  }
207
- return { output: `old_string not found in ${input.path}. Re-read the file.`, isError: true };
254
+ const fuzzy = matchByNormalizedLines(content, input.old_string, input.new_string, input.replace_all);
255
+ if (fuzzy.count > 1 && !input.replace_all) {
256
+ return {
257
+ output: `old_string appears ${fuzzy.count} times in ${path}. Add more context to make it unique, or use replace_all.`,
258
+ isError: true
259
+ };
260
+ }
261
+ if (fuzzy.count > 0 && fuzzy.result !== void 0) return fuzzy.result;
262
+ return { output: `old_string not found in ${path}. Re-read the file.`, isError: true };
263
+ }
264
+ function applyEdit(content, input) {
265
+ const list = 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 }] : [];
266
+ if (!list.length) return { output: "Provide old_string and new_string (or edits array).", isError: true };
267
+ let current = content;
268
+ for (const item of list) {
269
+ const next = applySingleEdit(current, input.path, item);
270
+ if (typeof next !== "string") return next;
271
+ current = next;
272
+ }
273
+ return current;
208
274
  }
209
275
  var editTool = defineTool({
210
276
  name: "edit",
@@ -215,6 +281,9 @@ var editTool = defineTool({
215
281
  async validate(input, ctx) {
216
282
  const abs = resolvePath(ctx.cwd, input.path);
217
283
  if (!ctx.readFiles.has(abs)) return `Read ${input.path} with read before editing it.`;
284
+ if (isFileModifiedSinceRead(ctx.readFiles, abs)) {
285
+ return `${input.path} has been modified since it was last read. Read it again before editing.`;
286
+ }
218
287
  let content;
219
288
  try {
220
289
  content = await readFile(abs, "utf8");
@@ -235,6 +304,9 @@ var editTool = defineTool({
235
304
  if (!ctx.readFiles.has(abs)) {
236
305
  return { output: `Read ${input.path} with read before editing it.`, isError: true };
237
306
  }
307
+ if (isFileModifiedSinceRead(ctx.readFiles, abs)) {
308
+ return { output: `${input.path} has been modified since it was last read. Read it again before editing.`, isError: true };
309
+ }
238
310
  let content;
239
311
  try {
240
312
  content = await readFile(abs, "utf8");
@@ -245,25 +317,28 @@ var editTool = defineTool({
245
317
  if (typeof next !== "string") return next;
246
318
  await ctx.checkpoint?.(abs);
247
319
  await writeFile(abs, next);
320
+ recordFileMtime(ctx.readFiles, abs);
248
321
  return { output: `Edited ${input.path}.`, display: formatDiff(diffLines(content, next)) };
249
322
  }
250
323
  });
251
324
 
252
325
  // src/tools/glob.ts
326
+ import { statSync } from "fs";
253
327
  import { glob as nativeGlob } from "fs/promises";
328
+ import { join } from "path";
254
329
  import { z as z2 } from "zod";
255
330
 
256
331
  // src/abort.ts
257
332
  var ABORTED = /* @__PURE__ */ Symbol("aborted");
258
333
  function raceAbort(promise, signal) {
259
334
  if (signal.aborted) return Promise.resolve(ABORTED);
260
- return new Promise((resolve2, reject) => {
261
- const onAbort = () => resolve2(ABORTED);
335
+ return new Promise((resolve, reject) => {
336
+ const onAbort = () => resolve(ABORTED);
262
337
  signal.addEventListener("abort", onAbort, { once: true });
263
338
  promise.then(
264
339
  (value) => {
265
340
  signal.removeEventListener("abort", onAbort);
266
- resolve2(value);
341
+ resolve(value);
267
342
  },
268
343
  (error) => {
269
344
  signal.removeEventListener("abort", onAbort);
@@ -297,19 +372,30 @@ var globTool = defineTool({
297
372
  path: z2.string().optional().describe("Base directory, default project root")
298
373
  }),
299
374
  kind: "read",
300
- target: (input) => input.pattern,
375
+ target: (input) => input.path ? `${input.pattern} ${input.path}` : input.pattern,
301
376
  async run(input, ctx) {
302
377
  const invalid = validateGlobPattern(input.pattern);
303
378
  if (invalid) return { output: invalid, isError: true };
304
379
  const root = resolvePath(ctx.cwd, input.path ?? ".");
305
380
  const files = [];
306
381
  const finished = await collectUntilAbort(
307
- nativeGlob(input.pattern, { cwd: root, exclude: ignorePatterns(root) }),
382
+ nativeGlob(input.pattern, { cwd: root, exclude: ignorePatterns(root, ctx.cwd) }),
308
383
  ctx.signal,
309
384
  (file) => files.push(normalizeGlobPath(file))
310
385
  );
311
386
  if (!finished) return { output: "Cancelled by the user.", isError: true };
312
- files.sort();
387
+ const mtime = (rel) => {
388
+ try {
389
+ return statSync(join(root, rel)).mtimeMs;
390
+ } catch {
391
+ return 0;
392
+ }
393
+ };
394
+ const mtimes = new Map(files.map((f) => [f, mtime(f)]));
395
+ files.sort((a, b) => {
396
+ const diff = (mtimes.get(b) ?? 0) - (mtimes.get(a) ?? 0);
397
+ return Math.abs(diff) >= 50 ? diff : a.localeCompare(b);
398
+ });
313
399
  if (!files.length) return { output: "No matching files." };
314
400
  const first = files.slice(0, 500);
315
401
  const shown = [];
@@ -330,75 +416,141 @@ var globTool = defineTool({
330
416
 
331
417
  // src/tools/grep.ts
332
418
  import { execFile } from "child_process";
419
+ import { statSync as statSync2 } from "fs";
333
420
  import { readFile as readFile2 } from "fs/promises";
334
421
  import { glob as nativeGlob2 } from "fs/promises";
422
+ import { basename, dirname } from "path";
335
423
  import { z as z3 } from "zod";
336
424
  var MAX_MATCHES = 200;
337
425
  var MAX_OUTPUT_CHARS2 = 24e3;
426
+ function escapeRegExp(s) {
427
+ return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
428
+ }
429
+ function isSingleFile(p) {
430
+ try {
431
+ return statSync2(p).isFile();
432
+ } catch {
433
+ return false;
434
+ }
435
+ }
338
436
  async function grepJs(pattern, opts) {
339
- const regex = new RegExp(pattern, opts.ignoreCase ? "i" : "");
437
+ const source = opts.literal ? escapeRegExp(pattern) : pattern;
438
+ const regex = new RegExp(source, opts.ignoreCase ? "i" : "");
439
+ const single = isSingleFile(opts.root);
340
440
  const files = [];
341
- const finished = await collectUntilAbort(
342
- nativeGlob2(opts.glob ?? "**/*", { cwd: opts.root, exclude: ignorePatterns(opts.root) }),
343
- opts.signal,
344
- (file) => files.push(normalizeGlobPath(file))
345
- );
441
+ if (single) {
442
+ files.push(basename(opts.root));
443
+ } else {
444
+ const finished = await collectUntilAbort(
445
+ nativeGlob2(opts.glob ?? "**/*", { cwd: opts.root, exclude: ignorePatterns(opts.root, opts.projectRoot) }),
446
+ opts.signal,
447
+ (file) => files.push(normalizeGlobPath(file))
448
+ );
449
+ if (!finished) return [];
450
+ }
451
+ const baseDir = single ? dirname(opts.root) : opts.root;
452
+ const ctxLines = Math.max(0, Math.min(10, Math.floor(opts.context ?? 0)));
453
+ const mode = opts.outputMode ?? "content";
346
454
  const out = [];
347
- if (!finished) return out;
348
455
  for (const file of files.sort()) {
349
456
  if (opts.signal?.aborted) break;
350
457
  let text;
351
458
  try {
352
- const buffer = await readFile2(resolvePath(opts.root, file));
459
+ const buffer = await readFile2(resolvePath(baseDir, file));
353
460
  if (buffer.subarray(0, 8e3).includes(0)) continue;
354
461
  text = buffer.toString("utf8");
355
462
  } catch {
356
463
  continue;
357
464
  }
358
465
  const lines = text.split(/\r?\n/);
466
+ let fileMatches = 0;
359
467
  for (let i = 0; i < lines.length && out.length < MAX_MATCHES; i++) {
360
- if (regex.test(lines[i])) out.push(`${file}:${i + 1}:${lines[i].slice(0, 300)}`);
468
+ if (regex.test(lines[i])) {
469
+ fileMatches++;
470
+ if (mode === "files_with_matches") {
471
+ out.push(file);
472
+ break;
473
+ }
474
+ if (mode === "content") {
475
+ const from = Math.max(0, i - ctxLines);
476
+ const to = Math.min(lines.length - 1, i + ctxLines);
477
+ for (let j = from; j <= to && out.length < MAX_MATCHES; j++) {
478
+ const sep2 = j === i ? ":" : "-";
479
+ out.push(`${file}${sep2}${j + 1}${sep2}${lines[j].slice(0, 300)}`);
480
+ }
481
+ }
482
+ }
361
483
  }
484
+ if (mode === "count" && fileMatches > 0) out.push(`${file}:${fileMatches}`);
362
485
  if (out.length >= MAX_MATCHES) break;
363
486
  }
364
487
  return out;
365
488
  }
366
489
  function grepRg(pattern, opts) {
367
- const args = ["--line-number", "--no-heading", "--color", "never", "--max-columns", "300", "-e", pattern];
490
+ const single = isSingleFile(opts.root);
491
+ const cwd = single ? dirname(opts.root) : opts.root;
492
+ const target = single ? basename(opts.root) : ".";
493
+ const mode = opts.outputMode ?? "content";
494
+ const args = ["--color", "never"];
495
+ if (mode === "files_with_matches") {
496
+ args.push("--files-with-matches");
497
+ } else if (mode === "count") {
498
+ args.push("--count");
499
+ } else {
500
+ args.push("--line-number", "--no-heading", "--max-columns", "300");
501
+ if (opts.context && opts.context > 0) args.push("-C", String(Math.min(10, Math.floor(opts.context))));
502
+ }
503
+ if (opts.literal) args.push("-F");
368
504
  if (opts.ignoreCase) args.push("-i");
369
- if (opts.glob) args.push("-g", opts.glob);
370
- args.push(".");
371
- return new Promise((resolve2) => {
372
- execFile("rg", args, { cwd: opts.root, maxBuffer: 10 * 1024 * 1024, signal: opts.signal }, (error, stdout) => {
373
- if (opts.signal?.aborted) return resolve2([]);
505
+ if (opts.glob && !single) args.push("-g", opts.glob);
506
+ args.push("-e", pattern, target);
507
+ return new Promise((resolve) => {
508
+ execFile("rg", args, { cwd, maxBuffer: 10 * 1024 * 1024, signal: opts.signal }, (error, stdout) => {
509
+ if (opts.signal?.aborted) return resolve([]);
374
510
  const code = error?.code;
375
- if (code === "ENOENT") return resolve2(void 0);
376
- if (error && error.code !== 1) return resolve2(void 0);
377
- resolve2(stdout.split("\n").filter(Boolean).map((l) => l.replace(/^\.\//, "")).slice(0, MAX_MATCHES));
511
+ if (code === "ENOENT") return resolve(void 0);
512
+ if (error && error.code !== 1) return resolve(void 0);
513
+ resolve(
514
+ stdout.split(/\r?\n/).filter(Boolean).map((l) => l.replace(/^\.[\\/]/, "").replace(/\\/g, "/")).slice(0, MAX_MATCHES)
515
+ );
378
516
  });
379
517
  });
380
518
  }
381
519
  var grepTool = defineTool({
382
520
  name: "grep",
383
- description: "Search file contents with a regular expression. Respects .gitignore. Returns path:line:text (max 200).",
521
+ description: "Search file contents with a regular expression or literal text. Respects .gitignore. Returns path:line:text (max 200).",
384
522
  schema: z3.object({
385
- pattern: z3.string().describe("Regular expression"),
386
- path: z3.string().optional().describe("Directory to search, default project root"),
523
+ pattern: z3.string().describe("Regular expression or literal string"),
524
+ path: z3.string().optional().describe("File or directory to search, default project root"),
387
525
  glob: z3.string().optional().describe('Only files matching this glob, e.g. "**/*.ts"'),
388
- ignore_case: z3.boolean().optional()
526
+ ignore_case: z3.boolean().optional(),
527
+ literal: z3.boolean().optional().describe("Treat pattern as literal text instead of regex"),
528
+ context: z3.number().int().min(0).max(10).optional().describe("Number of context lines before and after each match"),
529
+ output_mode: z3.enum(["content", "files_with_matches", "count"]).optional().describe("Output format, default content")
389
530
  }),
390
531
  kind: "read",
391
- target: (input) => input.pattern,
532
+ target: (input) => input.path ? `${input.pattern} ${input.path}` : input.pattern,
392
533
  async run(input, ctx) {
393
534
  if (input.glob) {
394
535
  const invalid = validateGlobPattern(input.glob);
395
536
  if (invalid) return { output: invalid, isError: true };
396
537
  }
397
- const opts = { root: resolvePath(ctx.cwd, input.path ?? "."), glob: input.glob, ignoreCase: input.ignore_case, signal: ctx.signal };
398
- try {
399
- new RegExp(input.pattern);
400
- } catch (error) {
401
- return { output: `Invalid regex: ${error.message}`, isError: true };
538
+ const opts = {
539
+ root: resolvePath(ctx.cwd, input.path ?? "."),
540
+ projectRoot: ctx.cwd,
541
+ glob: input.glob,
542
+ ignoreCase: input.ignore_case,
543
+ literal: input.literal,
544
+ context: input.context,
545
+ outputMode: input.output_mode,
546
+ signal: ctx.signal
547
+ };
548
+ if (!input.literal) {
549
+ try {
550
+ new RegExp(input.pattern);
551
+ } catch (error) {
552
+ return { output: `Invalid regex: ${error.message}`, isError: true };
553
+ }
402
554
  }
403
555
  const matches = await grepRg(input.pattern, opts) ?? await grepJs(input.pattern, opts);
404
556
  if (ctx.signal.aborted) return { output: "Cancelled by the user.", isError: true };
@@ -420,6 +572,7 @@ var grepTool = defineTool({
420
572
  });
421
573
 
422
574
  // src/tools/read.ts
575
+ import { readdirSync, statSync as statSync3 } from "fs";
423
576
  import { readFile as readFile3 } from "fs/promises";
424
577
  import { z as z4 } from "zod";
425
578
  var MAX_LINE = 2e3;
@@ -503,6 +656,16 @@ function createReadTool(options = {}) {
503
656
  target: (input) => input.path,
504
657
  async run(input, ctx) {
505
658
  const abs = resolvePath(ctx.cwd, input.path);
659
+ try {
660
+ if (statSync3(abs).isDirectory()) {
661
+ const entries = readdirSync(abs, { withFileTypes: true }).map((e) => e.isDirectory() ? `${e.name}/` : e.name).sort((a, b) => a.localeCompare(b));
662
+ const start = (input.offset ?? 1) - 1;
663
+ const limit = input.limit ?? 2e3;
664
+ const slice = entries.slice(start, start + limit);
665
+ return { output: slice.join("\n") || "(empty directory)", display: t("{n} lines", { n: slice.length }) };
666
+ }
667
+ } catch {
668
+ }
506
669
  let buffer;
507
670
  try {
508
671
  buffer = await readFile3(abs);
@@ -511,13 +674,13 @@ function createReadTool(options = {}) {
511
674
  }
512
675
  const mediaType = vision ? imageMediaType(buffer) : void 0;
513
676
  if (mediaType) {
514
- ctx.readFiles.add(abs);
677
+ recordFileMtime(ctx.readFiles, abs);
515
678
  return readImageResult(input, buffer, mediaType);
516
679
  }
517
680
  if (buffer.subarray(0, 8e3).includes(0)) {
518
681
  return { output: `${input.path} is a binary file; not read.`, isError: true };
519
682
  }
520
- ctx.readFiles.add(abs);
683
+ recordFileMtime(ctx.readFiles, abs);
521
684
  return readTextResult(input, buffer);
522
685
  }
523
686
  });
@@ -527,7 +690,7 @@ var readTool = createReadTool({ vision: true });
527
690
  // src/tools/write.ts
528
691
  import { existsSync } from "fs";
529
692
  import { mkdir, readFile as readFile4, writeFile as writeFile2 } from "fs/promises";
530
- import { dirname } from "path";
693
+ import { dirname as dirname2 } from "path";
531
694
  import { z as z5 } from "zod";
532
695
  var writeTool = defineTool({
533
696
  name: "write",
@@ -554,12 +717,12 @@ var writeTool = defineTool({
554
717
  }
555
718
  try {
556
719
  await ctx.checkpoint?.(abs);
557
- await mkdir(dirname(abs), { recursive: true });
720
+ await mkdir(dirname2(abs), { recursive: true });
558
721
  await writeFile2(abs, input.content);
559
722
  } catch (error) {
560
723
  return { output: `Failed to write ${input.path}: ${errorCode(error)}`, isError: true };
561
724
  }
562
- ctx.readFiles.add(abs);
725
+ recordFileMtime(ctx.readFiles, abs);
563
726
  const lines = input.content.split("\n").length;
564
727
  return { output: `Wrote ${input.path} (${lines} lines).`, display: t("{lines} lines written", { lines }) };
565
728
  }
@@ -683,6 +846,7 @@ var Agent = class {
683
846
  this.messages = [...opts.history ?? []];
684
847
  this.maxSteps = typeof opts.maxSteps === "number" && Number.isFinite(opts.maxSteps) && opts.maxSteps > 0 ? Math.max(1, Math.floor(opts.maxSteps)) : void 0;
685
848
  this.contextWindow = opts.contextWindow ?? 128e3;
849
+ this.budgetModel = opts.budgetModel ?? "";
686
850
  this.systemPrompt = opts.systemPrompt;
687
851
  this.reasoning = opts.reasoning ?? "auto";
688
852
  }
@@ -706,6 +870,7 @@ var Agent = class {
706
870
  maxSteps;
707
871
  // One window for every model; resolved from contextWindowFor() (catalog + env override).
708
872
  contextWindow;
873
+ budgetModel;
709
874
  systemPrompt;
710
875
  setSystemPrompt(text) {
711
876
  this.systemPrompt = text;
@@ -713,6 +878,15 @@ var Agent = class {
713
878
  setContextWindow(window) {
714
879
  this.contextWindow = window;
715
880
  }
881
+ getContextWindow() {
882
+ return this.contextWindow;
883
+ }
884
+ setBudgetModel(model) {
885
+ this.budgetModel = model;
886
+ }
887
+ getBudgetModel() {
888
+ return this.budgetModel;
889
+ }
716
890
  setTools(tools) {
717
891
  this.tools = tools;
718
892
  this.definitions = toolDefinitions(tools);
@@ -913,7 +1087,7 @@ ${t("This model may not support tool calling. Try another model with /model.")}`
913
1087
  }
914
1088
  /** Current cap status; undefined means the budget is not exhausted. */
915
1089
  overBudget() {
916
- return budgetStatus(this.totalUsage, this.opts.usageCap, this.opts.budgetModel ?? "");
1090
+ return budgetStatus(this.totalUsage, this.opts.usageCap, this.budgetModel);
917
1091
  }
918
1092
  /** Every tool call needs an answer in the history, or the next request to the model is rejected. */
919
1093
  cancelCalls(calls) {
@@ -1033,7 +1207,7 @@ ${t("This model may not support tool calling. Try another model with /model.")}`
1033
1207
  };
1034
1208
 
1035
1209
  // src/checkpoints.ts
1036
- import { existsSync as existsSync2, statSync } from "fs";
1210
+ import { existsSync as existsSync2, statSync as statSync4 } from "fs";
1037
1211
  import { readFile as readFile5, rm, writeFile as writeFile3 } from "fs/promises";
1038
1212
  var MAX_TURNS = 20;
1039
1213
  var MAX_BYTES = 2 * 1024 * 1024;
@@ -1060,8 +1234,8 @@ var CheckpointStore = class {
1060
1234
  history = [];
1061
1235
  current;
1062
1236
  pendingRedo;
1237
+ offset = 0;
1063
1238
  beginTurn() {
1064
- if (this.current?.size === 0) return;
1065
1239
  this.current = /* @__PURE__ */ new Map();
1066
1240
  this.history.push(this.current);
1067
1241
  this.pendingRedo = void 0;
@@ -1072,16 +1246,19 @@ var CheckpointStore = class {
1072
1246
  const turn = this.current;
1073
1247
  if (turn.has(path)) return;
1074
1248
  if (!existsSync2(path)) turn.set(path, null);
1075
- else if (statSync(path).size > MAX_BYTES) turn.set(path, "skipped");
1249
+ else if (statSync4(path).size > MAX_BYTES) turn.set(path, "skipped");
1076
1250
  else turn.set(path, await readFile5(path));
1077
- while (this.turns() > MAX_TURNS) this.history.shift();
1251
+ while (this.turns() > MAX_TURNS) {
1252
+ this.history.shift();
1253
+ this.offset++;
1254
+ }
1078
1255
  }
1079
1256
  turns() {
1080
1257
  return this.history.filter((t2) => t2.size).length;
1081
1258
  }
1082
1259
  /** Total entries including empty ones; used to align file snapshots with conversation turns. */
1083
1260
  entries() {
1084
- return this.history.length;
1261
+ return this.offset + this.history.length;
1085
1262
  }
1086
1263
  async undo() {
1087
1264
  while (this.history.length && !this.history.at(-1)?.size) this.history.pop();
@@ -1101,7 +1278,7 @@ var CheckpointStore = class {
1101
1278
  * pairs conversation turns with the entry count recorded when each turn started.
1102
1279
  */
1103
1280
  async undoTo(keep) {
1104
- const target = Math.max(0, Math.min(keep, this.history.length));
1281
+ const target = Math.max(0, Math.min(keep - this.offset, this.history.length));
1105
1282
  const result = emptyResult();
1106
1283
  const redo = /* @__PURE__ */ new Map();
1107
1284
  let applied = false;
@@ -1126,8 +1303,11 @@ var CheckpointStore = class {
1126
1303
  const turn = this.pendingRedo;
1127
1304
  if (!turn) return void 0;
1128
1305
  this.pendingRedo = void 0;
1306
+ const undoEntry = /* @__PURE__ */ new Map();
1307
+ await this.captureFor(turn, undoEntry);
1129
1308
  const result = emptyResult();
1130
1309
  await restoreTurn(turn, result);
1310
+ if (undoEntry.size) this.history.push(undoEntry);
1131
1311
  return result;
1132
1312
  }
1133
1313
  /** Records the on-disk state of a turn's paths so redo() can put it back. */
@@ -1140,7 +1320,7 @@ var CheckpointStore = class {
1140
1320
  }
1141
1321
  try {
1142
1322
  if (!existsSync2(path)) into.set(path, null);
1143
- else if (statSync(path).size > MAX_BYTES) into.set(path, "skipped");
1323
+ else if (statSync4(path).size > MAX_BYTES) into.set(path, "skipped");
1144
1324
  else into.set(path, await readFile5(path));
1145
1325
  } catch {
1146
1326
  into.set(path, "skipped");
@@ -1151,6 +1331,7 @@ var CheckpointStore = class {
1151
1331
  this.history.length = 0;
1152
1332
  this.current = void 0;
1153
1333
  this.pendingRedo = void 0;
1334
+ this.offset = 0;
1154
1335
  }
1155
1336
  };
1156
1337
 
@@ -1176,7 +1357,28 @@ import { UnauthorizedError } from "@modelcontextprotocol/sdk/client/auth.js";
1176
1357
  import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
1177
1358
  import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
1178
1359
  import { z as z7 } from "zod";
1179
- var MCP_OUTPUT_MAX_CHARS = 32e3;
1360
+ function formatMcpContent(content) {
1361
+ const parts = [];
1362
+ const images = [];
1363
+ for (const c of content ?? []) {
1364
+ if (c.type === "text") {
1365
+ parts.push(c.text ?? "");
1366
+ } else if (c.type === "image" && c.data) {
1367
+ const mediaType = c.mimeType || "image/png";
1368
+ images.push({ mediaType, data: c.data, path: "mcp:image" });
1369
+ parts.push(`[image attached: ${mediaType}]`);
1370
+ } else if (c.type === "resource" && c.resource?.text) {
1371
+ parts.push(c.resource.uri ? `[${c.resource.uri}]
1372
+ ${c.resource.text}` : c.resource.text);
1373
+ } else {
1374
+ parts.push(`[${c.type}]`);
1375
+ }
1376
+ }
1377
+ return {
1378
+ output: parts.join("\n") || "(empty)",
1379
+ ...images.length ? { images } : {}
1380
+ };
1381
+ }
1180
1382
  var CHILD_ENV_KEYS = [
1181
1383
  // portable
1182
1384
  "PATH",
@@ -1305,11 +1507,13 @@ var McpManager = class {
1305
1507
  async run(input, ctx) {
1306
1508
  try {
1307
1509
  const result = await client.callTool({ name: tool.name, arguments: input }, void 0, { signal: ctx.signal });
1308
- const output = (result.content ?? []).map((c) => c.type === "text" ? c.text ?? "" : `[${c.type}]`).join("\n") || "(empty)";
1510
+ const formatted = formatMcpContent(result.content);
1511
+ const output = formatted.output;
1309
1512
  return {
1310
- output: output.length > MCP_OUTPUT_MAX_CHARS ? `${output.slice(0, MCP_OUTPUT_MAX_CHARS)}
1311
- \u2026 [truncated at ${MCP_OUTPUT_MAX_CHARS} characters; narrow the MCP request if possible]` : output,
1312
- isError: !!result.isError
1513
+ output: output.length > 32e3 ? `${output.slice(0, 32e3)}
1514
+ \u2026 [truncated at 32000 characters; narrow the MCP request if possible]` : output,
1515
+ isError: !!result.isError,
1516
+ ...formatted.images ? { images: formatted.images } : {}
1313
1517
  };
1314
1518
  } catch (error) {
1315
1519
  return { output: `Server MCP ${server} error: ${error.message}`, isError: true };
@@ -1388,6 +1592,7 @@ import { z as z10 } from "zod";
1388
1592
  function createTaskTool(opts) {
1389
1593
  const byName = new Map(opts.agents.map((a) => [a.name, a]));
1390
1594
  const list = opts.agents.map((a) => `- ${a.name}: ${a.description}`).join("\n");
1595
+ const sessions = /* @__PURE__ */ new Map();
1391
1596
  let askChain = Promise.resolve();
1392
1597
  const enqueueAsk = (fn) => {
1393
1598
  const next = askChain.then(
@@ -1407,7 +1612,8 @@ ${list}`,
1407
1612
  schema: z10.object({
1408
1613
  agent: z10.string().describe("Agent name"),
1409
1614
  description: z10.string().describe("3-6 word summary shown to the user"),
1410
- prompt: z10.string().describe("Full instructions; the subagent sees nothing else")
1615
+ prompt: z10.string().describe("Full instructions; the subagent sees nothing else"),
1616
+ session_id: z10.string().optional().describe("Optional session id to continue a previous subagent conversation")
1411
1617
  }),
1412
1618
  kind: "read",
1413
1619
  target: (input) => `[${input.agent}] ${input.description}`,
@@ -1434,6 +1640,7 @@ ${list}`,
1434
1640
  } catch (error) {
1435
1641
  return { output: `Agent "${def.name}": ${error.message}`, isError: true };
1436
1642
  }
1643
+ const priorHistory = input.session_id ? sessions.get(input.session_id) : void 0;
1437
1644
  const child = new Agent({
1438
1645
  provider,
1439
1646
  tools,
@@ -1443,6 +1650,7 @@ ${list}`,
1443
1650
  # Your role
1444
1651
  ${def.prompt}`,
1445
1652
  cwd: opts.cwd,
1653
+ ...priorHistory ? { history: priorHistory } : {},
1446
1654
  // Every subagent runs with a step cap so a looping child cannot burn tokens forever.
1447
1655
  maxSteps: def.maxSteps ?? 50,
1448
1656
  label: def.name,
@@ -1463,12 +1671,29 @@ ${def.prompt}`,
1463
1671
  const ask = ctx.ask;
1464
1672
  if (ask) child.askPermission = (req) => enqueueAsk(() => ask(req));
1465
1673
  await child.run(input.prompt, ctx.signal);
1674
+ if (input.session_id) sessions.set(input.session_id, [...child.messages]);
1466
1675
  ctx.addUsage?.(child.totalUsage);
1467
1676
  const tokens2 = child.totalUsage.inputTokens + child.totalUsage.outputTokens;
1468
1677
  const display = t("{steps} steps \xB7 {tokens} tokens", { steps, tokens: tokens2 >= 1e3 ? `${Math.round(tokens2 / 1e3)}k` : tokens2 });
1469
1678
  if (outcome?.type === "error") return { output: `Subagent ${def.name} failed: ${outcome.message}`, isError: true, display };
1470
1679
  if (outcome?.type === "aborted") return { output: `Subagent ${def.name} was cancelled.`, isError: true, display };
1471
- const last = [...child.messages].reverse().find((m) => m.role === "assistant" && m.content);
1680
+ let last = [...child.messages].reverse().find((m) => m.role === "assistant" && m.content);
1681
+ if (!last && outcome?.type === "stepLimit" && !ctx.signal.aborted) {
1682
+ try {
1683
+ const synthesis = await provider.chat({
1684
+ messages: [
1685
+ ...child.messages,
1686
+ { role: "user", content: "Step limit reached. Summarize all findings gathered so far concisely." }
1687
+ ],
1688
+ signal: ctx.signal,
1689
+ reasoning: opts.reasoning?.() ?? "auto"
1690
+ });
1691
+ if (synthesis.text.trim()) {
1692
+ last = { role: "assistant", content: synthesis.text.trim() };
1693
+ }
1694
+ } catch {
1695
+ }
1696
+ }
1472
1697
  const report = last && typeof last.content === "string" ? last.content : "(no report)";
1473
1698
  const suffix = outcome?.type === "stepLimit" ? `
1474
1699
 
@@ -1514,7 +1739,7 @@ function createTodoTool(store) {
1514
1739
 
1515
1740
  // src/tools/memory.ts
1516
1741
  import { existsSync as existsSync3, readFileSync as readFileSync2 } from "fs";
1517
- import { join } from "path";
1742
+ import { join as join2 } from "path";
1518
1743
  import { z as z12 } from "zod";
1519
1744
  function createSaveMemoryTool(opts) {
1520
1745
  return defineTool({
@@ -1527,11 +1752,11 @@ function createSaveMemoryTool(opts) {
1527
1752
  kind: "edit",
1528
1753
  // The target is what the prompt shows and what the permission model judges: the
1529
1754
  // global store really lives outside the project, so it must not look like one.
1530
- target: (input) => input.scope === "global" ? join(opts.home, "BCCLI.md") : "AGENTS.md",
1755
+ target: (input) => input.scope === "global" ? join2(opts.home, "BCCLI.md") : "AGENTS.md",
1531
1756
  async run(input, ctx) {
1532
1757
  const content = input.content.trim();
1533
1758
  if (!content) return { output: "Memory content is empty.", isError: true };
1534
- const file = input.scope === "global" ? join(opts.home, "BCCLI.md") : join(ctx.cwd, "AGENTS.md");
1759
+ const file = input.scope === "global" ? join2(opts.home, "BCCLI.md") : join2(ctx.cwd, "AGENTS.md");
1535
1760
  if (existsSync3(file)) {
1536
1761
  const lines = readFileSync2(file, "utf8").split("\n");
1537
1762
  if (lines.some((line) => line.trim() === `- ${content}`)) {
@@ -1549,18 +1774,21 @@ function createSaveMemoryTool(opts) {
1549
1774
  import { z as z13 } from "zod";
1550
1775
 
1551
1776
  // src/repomap.ts
1552
- import { readdirSync, readFileSync as readFileSync3 } from "fs";
1553
- import { extname, join as join2, relative, sep } from "path";
1777
+ import { readdirSync as readdirSync2, readFileSync as readFileSync3 } from "fs";
1778
+ import { extname, join as join3, relative, sep } from "path";
1554
1779
  var SKIP_DIRS = /* @__PURE__ */ new Set([
1555
1780
  "node_modules",
1556
1781
  ".git",
1557
1782
  "dist",
1558
1783
  "build",
1784
+ "out",
1559
1785
  "coverage",
1560
1786
  ".next",
1787
+ ".turbo",
1561
1788
  "target",
1562
1789
  "vendor",
1563
1790
  "__pycache__",
1791
+ "venv",
1564
1792
  ".venv",
1565
1793
  ".cache"
1566
1794
  ]);
@@ -1586,21 +1814,22 @@ var CODE_EXTS = /* @__PURE__ */ new Set([
1586
1814
  ".ps1"
1587
1815
  ]);
1588
1816
  var MAX_FILES = 400;
1817
+ var MAX_SCAN_FILES = 5e3;
1589
1818
  var MAX_FILE_BYTES = 2e5;
1590
1819
  var MAX_SYMBOLS_PER_FILE = 12;
1591
1820
  var FAMILY = {
1592
1821
  ts: [
1593
- /^\s*(?:export\s+)?(?:default\s+)?(?:declare\s+)?(?:abstract\s+)?(?:async\s+)?(?:function|class|interface|type|enum)\s+([A-Za-z0-9_$]+)/,
1822
+ /^\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_$]+)/,
1594
1823
  /^\s*export\s+(?:const|let)\s+([A-Za-z0-9_$]+)/
1595
1824
  ],
1596
- py: [/^\s*(?:async\s+)?def\s+(\w+)/, /^\s*class\s+(\w+)/],
1825
+ py: [/^\s*(?:async\s+)?def\s+(\w+)/, /^\s*(?:module|class)\s+(\w+)/],
1597
1826
  go: [/^func\s+(?:\([^)]*\)\s*)?(\w+)/, /^type\s+(\w+)/],
1598
1827
  rs: [/^\s*(?:pub(?:\([^)]*\))?\s+)?(?:unsafe\s+)?(?:async\s+)?(?:fn|struct|enum|trait|type)\s+(\w+)/],
1599
1828
  c: [/^\s*(?:static\s+|inline\s+|extern\s+)*[A-Za-z_][A-Za-z0-9_\s*]*\b(\w+)\s*\([^;]*\)\s*\{?\s*$/]
1600
1829
  };
1601
1830
  function familyFor(ext) {
1602
- if ([".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs", ".java", ".cs", ".php", ".rb"].includes(ext)) return FAMILY.ts;
1603
- if (ext === ".py" || ext === ".sh" || ext === ".ps1") return FAMILY.py;
1831
+ if ([".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs", ".java", ".cs", ".php"].includes(ext)) return FAMILY.ts;
1832
+ if (ext === ".py" || ext === ".rb" || ext === ".sh" || ext === ".ps1") return FAMILY.py;
1604
1833
  if (ext === ".go") return FAMILY.go;
1605
1834
  if (ext === ".rs") return FAMILY.rs;
1606
1835
  return FAMILY.c;
@@ -1620,16 +1849,16 @@ function extractSymbols(text, ext) {
1620
1849
  return out;
1621
1850
  }
1622
1851
  function walk(dir, root, files) {
1623
- if (files.length >= MAX_FILES) return;
1852
+ if (files.length >= MAX_SCAN_FILES) return;
1624
1853
  let entries;
1625
1854
  try {
1626
- entries = readdirSync(dir, { withFileTypes: true });
1855
+ entries = readdirSync2(dir, { withFileTypes: true });
1627
1856
  } catch {
1628
1857
  return;
1629
1858
  }
1630
1859
  for (const e of entries.sort((a, b) => a.name.localeCompare(b.name))) {
1631
- if (files.length >= MAX_FILES) return;
1632
- const full = join2(dir, e.name);
1860
+ if (files.length >= MAX_SCAN_FILES) return;
1861
+ const full = join3(dir, e.name);
1633
1862
  if (e.isDirectory()) {
1634
1863
  if (SKIP_DIRS.has(e.name) || e.name.startsWith(".") && full !== root) continue;
1635
1864
  walk(full, root, files);
@@ -1640,11 +1869,12 @@ function walk(dir, root, files) {
1640
1869
  }
1641
1870
  function buildRepoMapSync(cwd, opts = {}) {
1642
1871
  const maxTokens = opts.maxTokens ?? 2048;
1643
- const files = [];
1644
- walk(cwd, cwd, files);
1645
- files.sort(
1872
+ const scanned = [];
1873
+ walk(cwd, cwd, scanned);
1874
+ scanned.sort(
1646
1875
  (a, b) => relative(cwd, a).split(sep).length - relative(cwd, b).split(sep).length || a.localeCompare(b)
1647
1876
  );
1877
+ const files = scanned.slice(0, MAX_FILES);
1648
1878
  const lines = [];
1649
1879
  let used = 0;
1650
1880
  let truncated = false;
@@ -1684,13 +1914,50 @@ function createRepoMapTool() {
1684
1914
  }
1685
1915
 
1686
1916
  // src/tools/diagnostics.ts
1687
- import { extname as extname2 } from "path";
1917
+ import { extname as extname3 } from "path";
1688
1918
  import { z as z14 } from "zod";
1689
1919
 
1690
1920
  // src/lsp.ts
1691
1921
  import { spawn } from "child_process";
1692
1922
  import { readFileSync as readFileSync4 } from "fs";
1923
+ import { extname as extname2 } from "path";
1693
1924
  import { pathToFileURL } from "url";
1925
+ var LANGUAGE_IDS = {
1926
+ ".ts": "typescript",
1927
+ ".tsx": "typescriptreact",
1928
+ ".mts": "typescript",
1929
+ ".cts": "typescript",
1930
+ ".js": "javascript",
1931
+ ".jsx": "javascriptreact",
1932
+ ".mjs": "javascript",
1933
+ ".cjs": "javascript",
1934
+ ".py": "python",
1935
+ ".rs": "rust",
1936
+ ".go": "go",
1937
+ ".java": "java",
1938
+ ".c": "c",
1939
+ ".h": "c",
1940
+ ".cpp": "cpp",
1941
+ ".hpp": "cpp",
1942
+ ".cs": "csharp",
1943
+ ".rb": "ruby",
1944
+ ".php": "php",
1945
+ ".json": "json",
1946
+ ".md": "markdown",
1947
+ ".sh": "shellscript",
1948
+ ".ps1": "powershell",
1949
+ ".yaml": "yaml",
1950
+ ".yml": "yaml",
1951
+ ".toml": "toml",
1952
+ ".html": "html",
1953
+ ".css": "css"
1954
+ };
1955
+ function languageIdFor(file) {
1956
+ return LANGUAGE_IDS[extname2(file).toLowerCase()] ?? "plaintext";
1957
+ }
1958
+ function useShellForLsp(command, platform = process.platform) {
1959
+ return platform === "win32" && !/\.(exe|mjs|cjs|js)$/i.test(command.trim());
1960
+ }
1694
1961
  function encodeLsp(msg) {
1695
1962
  const body = Buffer.from(JSON.stringify(msg), "utf8");
1696
1963
  return Buffer.concat([Buffer.from(`Content-Length: ${body.length}\r
@@ -1723,8 +1990,12 @@ function createLspParser(onMessage) {
1723
1990
  };
1724
1991
  }
1725
1992
  function getDiagnostics(opts) {
1726
- return new Promise((resolve2) => {
1727
- const child = spawn(opts.server.command, opts.server.args ?? [], { cwd: opts.cwd, stdio: ["pipe", "pipe", "ignore"] });
1993
+ return new Promise((resolve) => {
1994
+ const child = spawn(opts.server.command, opts.server.args ?? [], {
1995
+ cwd: opts.cwd,
1996
+ stdio: ["pipe", "pipe", "ignore"],
1997
+ shell: useShellForLsp(opts.server.command)
1998
+ });
1728
1999
  const uri = pathToFileURL(opts.file).href;
1729
2000
  let settled = false;
1730
2001
  let nextId = 2;
@@ -1738,6 +2009,7 @@ function getDiagnostics(opts) {
1738
2009
  if (settled) return;
1739
2010
  settled = true;
1740
2011
  clearTimeout(timer);
2012
+ opts.signal?.removeEventListener("abort", onAbort);
1741
2013
  send({ jsonrpc: "2.0", id: nextId++, method: "shutdown" });
1742
2014
  send({ jsonrpc: "2.0", method: "exit" });
1743
2015
  const killer = setTimeout(() => {
@@ -1747,8 +2019,11 @@ function getDiagnostics(opts) {
1747
2019
  }
1748
2020
  }, 500);
1749
2021
  killer.unref?.();
1750
- resolve2(diags);
2022
+ resolve(diags);
1751
2023
  };
2024
+ const onAbort = () => finish([]);
2025
+ if (opts.signal?.aborted) return finish([]);
2026
+ opts.signal?.addEventListener("abort", onAbort, { once: true });
1752
2027
  const timer = setTimeout(() => finish([]), opts.timeoutMs ?? 8e3);
1753
2028
  const parser = createLspParser((msg) => {
1754
2029
  if (msg.id === 1 && msg.result) {
@@ -1761,7 +2036,7 @@ function getDiagnostics(opts) {
1761
2036
  send({
1762
2037
  jsonrpc: "2.0",
1763
2038
  method: "textDocument/didOpen",
1764
- params: { textDocument: { uri, languageId: "plaintext", version: 1, text } }
2039
+ params: { textDocument: { uri, languageId: languageIdFor(opts.file), version: 1, text } }
1765
2040
  });
1766
2041
  }
1767
2042
  if (msg.method === "textDocument/publishDiagnostics" && msg.params?.uri === uri) {
@@ -1799,7 +2074,7 @@ function createDiagnosticsTool(servers) {
1799
2074
  target: (input) => input.path,
1800
2075
  async run(input, ctx) {
1801
2076
  const file = resolvePath(ctx.cwd, input.path);
1802
- const ext = extname2(file).toLowerCase();
2077
+ const ext = extname3(file).toLowerCase();
1803
2078
  const server = servers.find((s) => s.extensions.includes(ext));
1804
2079
  if (!server) {
1805
2080
  return { output: t("No LSP server is configured for {ext} files.", { ext: ext || "(none)" }), isError: true };
@@ -1815,7 +2090,7 @@ function createDiagnosticsTool(servers) {
1815
2090
  // src/plugins.ts
1816
2091
  import { existsSync as existsSync4 } from "fs";
1817
2092
  import { createRequire } from "module";
1818
- import { isAbsolute as isAbsolute2, join as join3 } from "path";
2093
+ import { isAbsolute, join as join4 } from "path";
1819
2094
  import { z as z15 } from "zod";
1820
2095
  var require_ = createRequire(import.meta.url);
1821
2096
  function pluginLog(message) {
@@ -1840,7 +2115,7 @@ function loadPlugins(specs, opts) {
1840
2115
  for (const spec of specs) {
1841
2116
  let file;
1842
2117
  try {
1843
- file = isAbsolute2(spec) ? spec : spec.startsWith(".") ? join3(opts.home, spec) : require_.resolve(spec);
2118
+ file = isAbsolute(spec) ? spec : spec.startsWith(".") ? join4(opts.home, spec) : require_.resolve(spec);
1844
2119
  } catch {
1845
2120
  throw new ConfigError(t("Plugin not found: {spec}", { spec }));
1846
2121
  }
@@ -1917,7 +2192,7 @@ function createRuntime(opts) {
1917
2192
  let config = loadConfig(opts.cwd, env);
1918
2193
  let modelRef = opts.args.model ?? config.model;
1919
2194
  let reasoning = opts.args.reasoning ?? config.reasoning;
1920
- const makeProvider = (ref) => {
2195
+ const makeSingleProvider = (ref) => {
1921
2196
  const resolved = resolveModel(config, ref, env);
1922
2197
  return opts.provider ?? createProvider({
1923
2198
  baseURL: resolved.baseURL,
@@ -1929,6 +2204,25 @@ function createRuntime(opts) {
1929
2204
  enableProgrammaticToolCalling: lunaPtcAutoEnabled(env)
1930
2205
  });
1931
2206
  };
2207
+ const makeProvider = (ref) => {
2208
+ const primary = makeSingleProvider(ref);
2209
+ if (opts.provider) return primary;
2210
+ const fallbackRefs = [
2211
+ ...opts.args.fallbackModel ? [opts.args.fallbackModel] : [],
2212
+ ...config.fallbackModels ?? []
2213
+ ].filter((r) => r && r !== ref);
2214
+ if (!fallbackRefs.length) return primary;
2215
+ return withFallbackProvider(
2216
+ primary,
2217
+ () => fallbackRefs.flatMap((fbRef) => {
2218
+ try {
2219
+ return [makeSingleProvider(fbRef)];
2220
+ } catch {
2221
+ return [];
2222
+ }
2223
+ })
2224
+ );
2225
+ };
1932
2226
  const provider = makeProvider(modelRef);
1933
2227
  const mode = opts.args.allowAll ? "allowAll" : opts.args.permissionMode ?? config.permissionMode;
1934
2228
  const rules = [...config.allow, ...opts.args.allowedTools.map((t2) => TOOL_RULES[t2] ?? t2)];
@@ -1950,6 +2244,10 @@ function createRuntime(opts) {
1950
2244
  if (latest) {
1951
2245
  session = latest;
1952
2246
  history = latest.load();
2247
+ if (!opts.args.model) {
2248
+ const prevModel = latest.lastModel();
2249
+ if (prevModel) modelRef = prevModel;
2250
+ }
1953
2251
  }
1954
2252
  }
1955
2253
  const repoMap = config.repoMap ? buildRepoMapSync(opts.cwd) : void 0;
@@ -1993,7 +2291,7 @@ function createRuntime(opts) {
1993
2291
  // Subagents get whatever the main agent has right now (incl. MCP tools), minus task itself.
1994
2292
  baseTools: () => agent.tools.filter((t2) => t2.name !== "task"),
1995
2293
  permissions,
1996
- provider: () => agent.provider,
2294
+ provider: () => opts.provider ? agent.provider : makeProvider(modelRef),
1997
2295
  providerFor: makeProvider,
1998
2296
  systemPrompt: (childModelRef) => systemFor(childModelRef ?? modelRef),
1999
2297
  cwd: opts.cwd,
@@ -2079,6 +2377,20 @@ function createRuntime(opts) {
2079
2377
  session = previous;
2080
2378
  agent.load(previous.load());
2081
2379
  agent.totalUsage = { inputTokens: 0, outputTokens: 0 };
2380
+ if (!opts.args.model) {
2381
+ const prevModel = previous.lastModel();
2382
+ if (prevModel) {
2383
+ try {
2384
+ const nextProvider = makeProvider(prevModel);
2385
+ modelRef = prevModel;
2386
+ agent.provider = nextProvider;
2387
+ agent.setBudgetModel(prevModel);
2388
+ agent.setContextWindow(contextWindowFor(prevModel, env));
2389
+ agent.setSystemPrompt(systemFor(modelRef));
2390
+ } catch {
2391
+ }
2392
+ }
2393
+ }
2082
2394
  todos.set([]);
2083
2395
  checkpoints.clear();
2084
2396
  turnMarks.length = 0;
@@ -2127,6 +2439,7 @@ function createRuntime(opts) {
2127
2439
  const nextProvider = makeProvider(ref);
2128
2440
  modelRef = ref;
2129
2441
  agent.provider = nextProvider;
2442
+ agent.setBudgetModel(ref);
2130
2443
  agent.setContextWindow(contextWindowFor(ref, env));
2131
2444
  agent.setSystemPrompt(systemFor(modelRef));
2132
2445
  },