@botconnector/bccli 0.4.0-beta.9 → 0.4.1

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 (183) hide show
  1. package/README.en.md +253 -0
  2. package/README.md +93 -6
  3. package/dist/abort.d.ts +5 -0
  4. package/dist/acp-3PHBAI64.js +182 -0
  5. package/dist/acp.d.ts +27 -0
  6. package/dist/agent.d.ts +143 -0
  7. package/dist/agents.d.ts +4 -0
  8. package/dist/args-W6BNKY7K.js +13 -0
  9. package/dist/args.d.ts +51 -0
  10. package/dist/budget.d.ts +23 -0
  11. package/dist/checkpoints.d.ts +34 -0
  12. package/dist/{chunk-BFZXRKW5.js → chunk-24JN75FZ.js} +29 -7
  13. package/dist/chunk-45SDLMTV.js +341 -0
  14. package/dist/chunk-75BECX4F.js +56 -0
  15. package/dist/{chunk-SJH3YLW4.js → chunk-BC7UXFE4.js} +69 -6
  16. package/dist/chunk-BGXNR74O.js +733 -0
  17. package/dist/chunk-BKGFA2PT.js +8 -0
  18. package/dist/{chunk-CLN4Z5E3.js → chunk-BLOYFIAD.js} +6 -3
  19. package/dist/chunk-BXQQCUAC.js +145 -0
  20. package/dist/chunk-FLQPE4S5.js +373 -0
  21. package/dist/chunk-GN4W3T6E.js +76 -0
  22. package/dist/{chunk-7S6JXQCH.js → chunk-JHPGOAXU.js} +2 -1
  23. package/dist/chunk-MFHCQQEN.js +201 -0
  24. package/dist/chunk-MHIYLXJC.js +143 -0
  25. package/dist/chunk-PSO7R5AD.js +10 -0
  26. package/dist/chunk-PZRKU4UM.js +2143 -0
  27. package/dist/chunk-QZKLZELZ.js +64 -0
  28. package/dist/chunk-R5RE46CW.js +231 -0
  29. package/dist/chunk-UZWJQF67.js +500 -0
  30. package/dist/{chunk-DX7CZTBB.js → chunk-WI22M2ID.js} +14 -9
  31. package/dist/chunk-WNACAUDK.js +946 -0
  32. package/dist/chunk-XV7ZVL4J.js +235 -0
  33. package/dist/cli.d.ts +1 -0
  34. package/dist/cli.js +150 -1382
  35. package/dist/clipboardImage.d.ts +17 -0
  36. package/dist/commands.d.ts +16 -0
  37. package/dist/completion-HPPFDAPB.js +125 -0
  38. package/dist/completion.d.ts +14 -0
  39. package/dist/{config-YUNKPCNB.js → config-DIDLDONU.js} +5 -2
  40. package/dist/config-IU76CPK2.js +34 -0
  41. package/dist/config.d.ts +97 -0
  42. package/dist/context.d.ts +13 -0
  43. package/dist/contextWindow.d.ts +3 -0
  44. package/dist/diff.d.ts +7 -0
  45. package/dist/exit.d.ts +9 -0
  46. package/dist/extensions.d.ts +34 -0
  47. package/dist/ghBotCli-E3VPHNZY.js +677 -0
  48. package/dist/ghBotCli.d.ts +9 -0
  49. package/dist/ghBotDispatch.d.ts +68 -0
  50. package/dist/ghBotRunner.d.ts +82 -0
  51. package/dist/ghBotServe.d.ts +21 -0
  52. package/dist/ghBotWebhook.d.ts +45 -0
  53. package/dist/hooks.d.ts +27 -0
  54. package/dist/i18n/id.d.ts +1 -0
  55. package/dist/i18n.d.ts +16 -0
  56. package/dist/integrationCli-Q2YPDTPG.js +685 -0
  57. package/dist/integrationCli.d.ts +9 -0
  58. package/dist/keybinds.d.ts +22 -0
  59. package/dist/login-FP3BB44A.js +16 -0
  60. package/dist/login.d.ts +19 -0
  61. package/dist/lsp.d.ts +47 -0
  62. package/dist/mcp/catalog.d.ts +14 -0
  63. package/dist/mcp/config.d.ts +36 -0
  64. package/dist/mcp/manager.d.ts +34 -0
  65. package/dist/mcp/oauth.d.ts +82 -0
  66. package/dist/{mcpCli-DZWQPTOS.js → mcpCli-SNBDUWJN.js} +42 -9
  67. package/dist/mcpCli.d.ts +3 -0
  68. package/dist/mcpServer/serve.d.ts +8 -0
  69. package/dist/mcpServer/server.d.ts +19 -0
  70. package/dist/models.d.ts +13 -0
  71. package/dist/modelsCli-CZ2HNGKR.js +36 -0
  72. package/dist/modelsCli.d.ts +15 -0
  73. package/dist/npmRunner-LU43GM3W.js +46 -0
  74. package/dist/npmRunner.d.ts +21 -0
  75. package/dist/oauthLogin-6FWQABJ7.js +210 -0
  76. package/dist/oauthLogin.d.ts +47 -0
  77. package/dist/permissions.d.ts +30 -0
  78. package/dist/pluginSpec.d.ts +25 -0
  79. package/dist/plugins.d.ts +45 -0
  80. package/dist/pluginsCli-LPIUUDWB.js +284 -0
  81. package/dist/pluginsCli.d.ts +16 -0
  82. package/dist/prCli-5IMV3VNQ.js +78 -0
  83. package/dist/prCli.d.ts +33 -0
  84. package/dist/presets.d.ts +8 -0
  85. package/dist/print.d.ts +14 -0
  86. package/dist/provider.d.ts +140 -0
  87. package/dist/providerCli-NMUFXAAS.js +15 -0
  88. package/dist/providerCli.d.ts +17 -0
  89. package/dist/providers.d.ts +6 -0
  90. package/dist/reasoning.d.ts +12 -0
  91. package/dist/repomap.d.ts +10 -0
  92. package/dist/sdk.d.ts +57 -0
  93. package/dist/sdk.js +5571 -0
  94. package/dist/serve-KWOECK5W.js +178 -0
  95. package/dist/serve-X2ZV4S5U.js +521 -0
  96. package/dist/serve.d.ts +37 -0
  97. package/dist/serveHeartbeat.d.ts +14 -0
  98. package/dist/serveProtocol.d.ts +47 -0
  99. package/dist/session-GKEXDJKZ.js +12 -0
  100. package/dist/session.d.ts +30 -0
  101. package/dist/setup-OV3NWQN5.js +24 -0
  102. package/dist/setup.d.ts +74 -0
  103. package/dist/slash/copy.d.ts +1 -0
  104. package/dist/slash/diff.d.ts +1 -0
  105. package/dist/slash/doctor.d.ts +18 -0
  106. package/dist/slash/export.d.ts +2 -0
  107. package/dist/slash/info.d.ts +45 -0
  108. package/dist/slash/memory.d.ts +10 -0
  109. package/dist/statsCli-CF3R7ATL.js +353 -0
  110. package/dist/statsCli.d.ts +62 -0
  111. package/dist/textToolCalls.d.ts +6 -0
  112. package/dist/thinking.d.ts +17 -0
  113. package/dist/tokenBudget.d.ts +22 -0
  114. package/dist/tools/bash.d.ts +52 -0
  115. package/dist/tools/diagnostics.d.ts +5 -0
  116. package/dist/tools/edit.d.ts +7 -0
  117. package/dist/tools/fetch.d.ts +41 -0
  118. package/dist/tools/glob.d.ts +5 -0
  119. package/dist/tools/globSafety.d.ts +7 -0
  120. package/dist/tools/grep.d.ts +13 -0
  121. package/dist/tools/ignore.d.ts +2 -0
  122. package/dist/tools/index.d.ts +12 -0
  123. package/dist/tools/memory.d.ts +13 -0
  124. package/dist/tools/paths.d.ts +2 -0
  125. package/dist/tools/plan.d.ts +10 -0
  126. package/dist/tools/read.d.ts +16 -0
  127. package/dist/tools/repomap.d.ts +2 -0
  128. package/dist/tools/sandbox.d.ts +12 -0
  129. package/dist/tools/shell.d.ts +18 -0
  130. package/dist/tools/skill.d.ts +3 -0
  131. package/dist/tools/task.d.ts +28 -0
  132. package/dist/tools/todo.d.ts +13 -0
  133. package/dist/tools/types.d.ts +52 -0
  134. package/dist/tools/websearch.d.ts +9 -0
  135. package/dist/tools/write.d.ts +5 -0
  136. package/dist/transferCli-YUFPI6AH.js +71 -0
  137. package/dist/transferCli.d.ts +23 -0
  138. package/dist/ui/App.d.ts +7 -0
  139. package/dist/ui/DiffView.d.ts +3 -0
  140. package/dist/ui/EditableText.d.ts +7 -0
  141. package/dist/ui/LinePrompt.d.ts +6 -0
  142. package/dist/ui/ListPicker.d.ts +11 -0
  143. package/dist/ui/Markdown.d.ts +8 -0
  144. package/dist/ui/McpMenu.d.ts +11 -0
  145. package/dist/ui/ModelPicker.d.ts +6 -0
  146. package/dist/ui/PermissionPrompt.d.ts +5 -0
  147. package/dist/ui/PlanApproval.d.ts +5 -0
  148. package/dist/ui/PromptInput.d.ts +18 -0
  149. package/dist/ui/ProviderMenu.d.ts +10 -0
  150. package/dist/ui/Spinner.d.ts +4 -0
  151. package/dist/ui/StatusBar.d.ts +9 -0
  152. package/dist/ui/TodoList.d.ts +4 -0
  153. package/dist/ui/ToolBlock.d.ts +14 -0
  154. package/dist/ui/complete.d.ts +1 -0
  155. package/dist/ui/focusReport.d.ts +1 -0
  156. package/dist/ui/index.d.ts +7 -0
  157. package/dist/ui/lineEdit.d.ts +42 -0
  158. package/dist/ui/replay.d.ts +4 -0
  159. package/dist/ui/table.d.ts +20 -0
  160. package/dist/ui/theme.d.ts +5 -0
  161. package/dist/ui/thinkingMouse.d.ts +33 -0
  162. package/dist/ui/transcript.d.ts +55 -0
  163. package/dist/ui/vim.d.ts +26 -0
  164. package/dist/ui-IVIFNUCC.js +2367 -0
  165. package/dist/update-TZ2G6KQ3.js +145 -0
  166. package/dist/update.d.ts +30 -0
  167. package/dist/verify.d.ts +16 -0
  168. package/dist/version.d.ts +1 -0
  169. package/dist/webBudget.d.ts +25 -0
  170. package/dist/worktree.d.ts +12 -0
  171. package/package.json +42 -8
  172. package/dist/args-JPVSGCGY.js +0 -12
  173. package/dist/chunk-2MGINW4O.js +0 -114
  174. package/dist/chunk-ELML4KVQ.js +0 -76
  175. package/dist/chunk-ISGBGFSU.js +0 -52
  176. package/dist/chunk-KV27KTOX.js +0 -254
  177. package/dist/chunk-U7GK73CK.js +0 -341
  178. package/dist/chunk-ZYCYTEKD.js +0 -96
  179. package/dist/integrationCli-BGT5UZU4.js +0 -223
  180. package/dist/login-6K32PIXV.js +0 -9
  181. package/dist/providerCli-Q7LRM2NP.js +0 -12
  182. package/dist/session-7UB77CEG.js +0 -9
  183. package/dist/ui-6GTQUJOE.js +0 -1415
@@ -0,0 +1,2143 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ estimateMessages,
4
+ estimateTokens,
5
+ pruneOldToolOutputs,
6
+ splitKeepRecent,
7
+ trimToFit
8
+ } from "./chunk-QZKLZELZ.js";
9
+ import {
10
+ BUILTIN_AGENTS,
11
+ appendMemory,
12
+ bashTool,
13
+ buildSystemPrompt,
14
+ createBashTool,
15
+ ignorePatterns,
16
+ loadAgentDefs,
17
+ loadCommands,
18
+ loadSkills,
19
+ normalizeGlobPath,
20
+ parseFrontmatter,
21
+ runHooks,
22
+ validateGlobPattern
23
+ } from "./chunk-BGXNR74O.js";
24
+ import {
25
+ createWebSearchTool,
26
+ fetchTool
27
+ } from "./chunk-45SDLMTV.js";
28
+ import {
29
+ defineTool
30
+ } from "./chunk-PSO7R5AD.js";
31
+ import {
32
+ Permissions
33
+ } from "./chunk-MHIYLXJC.js";
34
+ import {
35
+ addProviderKey
36
+ } from "./chunk-GN4W3T6E.js";
37
+ import {
38
+ listAllModels
39
+ } from "./chunk-75BECX4F.js";
40
+ import {
41
+ providerName
42
+ } from "./chunk-JHPGOAXU.js";
43
+ import {
44
+ FileOAuthProvider,
45
+ hasStoredTokens
46
+ } from "./chunk-MFHCQQEN.js";
47
+ import {
48
+ Session
49
+ } from "./chunk-BC7UXFE4.js";
50
+ import {
51
+ completionAssistantMessage,
52
+ createProvider,
53
+ hasPendingProgrammaticReplay,
54
+ imageUserMessage
55
+ } from "./chunk-WNACAUDK.js";
56
+ import {
57
+ expandEnvVars
58
+ } from "./chunk-24JN75FZ.js";
59
+ import {
60
+ ConfigError,
61
+ bccliHome,
62
+ loadConfig,
63
+ resolveModel
64
+ } from "./chunk-FLQPE4S5.js";
65
+ import {
66
+ t
67
+ } from "./chunk-UZWJQF67.js";
68
+
69
+ // src/textToolCalls.ts
70
+ var PAIR = /(?:<arg_key>\s*|`)?([A-Za-z_]\w*)(?:\s*<\/arg_key>)?`?\s*(?:=\s*`|<arg_value>)([\s\S]*?)<\/arg_value>/g;
71
+ var CALL_START = /<tool_call>|\n?[^\n]*\bTool call list\b[^\n]*|\n\s*\d+\.\s*Tool:/g;
72
+ function coerce(value, type) {
73
+ const types = Array.isArray(type) ? type : [type];
74
+ const v = value.trim();
75
+ if ((types.includes("integer") || types.includes("number")) && v !== "" && !Number.isNaN(Number(v))) return Number(v);
76
+ if (types.includes("boolean") && /^(true|false)$/i.test(v)) return v.toLowerCase() === "true";
77
+ if ((types.includes("object") || types.includes("array")) && /^[[{]/.test(v)) {
78
+ try {
79
+ return JSON.parse(v);
80
+ } catch {
81
+ }
82
+ }
83
+ return value.replace(/^\n+|\n+$/g, "");
84
+ }
85
+ function pickTool(name, keys, tools) {
86
+ const named = name && tools.find((t2) => t2.function.name === name);
87
+ if (named) return named;
88
+ let best;
89
+ let bestScore = 0;
90
+ let tie = false;
91
+ for (const t2 of tools) {
92
+ const params = t2.function.parameters;
93
+ const props = Object.keys(params.properties ?? {});
94
+ if (!(params.required ?? []).every((r) => keys.includes(r))) continue;
95
+ const score = keys.filter((k) => props.includes(k)).length;
96
+ if (score > bestScore) [best, bestScore, tie] = [t2, score, false];
97
+ else if (score === bestScore && score > 0) tie = true;
98
+ }
99
+ return tie ? void 0 : best;
100
+ }
101
+ function recoverTextToolCalls(text, tools) {
102
+ if (!/<\/arg_value>|<tool_call>/.test(text)) return null;
103
+ const starts = [...text.matchAll(CALL_START)].map((m) => m.index ?? 0);
104
+ const firstPair = text.search(PAIR);
105
+ const cut = Math.min(...[...starts, firstPair].filter((i) => i >= 0));
106
+ const bounds = starts.filter((i) => i >= cut);
107
+ if (!bounds.length || bounds[0] > cut) bounds.unshift(cut);
108
+ const toolCalls = [];
109
+ bounds.forEach((start, n) => {
110
+ const segment = text.slice(start, bounds[n + 1] ?? text.length);
111
+ const name = /<tool_call>\s*([A-Za-z_][\w.-]*)/.exec(segment)?.[1] ?? /Tool:\s*`?([A-Za-z_][\w.-]*)`?/.exec(segment)?.[1];
112
+ let args = [];
113
+ const flush = () => {
114
+ if (!args.length) return;
115
+ const keys = args.map(([k]) => k);
116
+ const tool = pickTool(name, keys, tools);
117
+ if (tool) {
118
+ const props = tool.function.parameters.properties ?? {};
119
+ const input = Object.fromEntries(args.filter(([k]) => k in props).map(([k, v]) => [k, coerce(v, props[k].type)]));
120
+ toolCalls.push({ id: `text_call_${toolCalls.length}`, name: tool.function.name, arguments: JSON.stringify(input) });
121
+ }
122
+ args = [];
123
+ };
124
+ for (const m of segment.matchAll(PAIR)) {
125
+ if (args.some(([k]) => k === m[1])) flush();
126
+ args.push([m[1], m[2]]);
127
+ }
128
+ flush();
129
+ });
130
+ if (!toolCalls.length) return null;
131
+ return { text: text.slice(0, cut).trim(), toolCalls };
132
+ }
133
+
134
+ // src/tools/index.ts
135
+ import { z as z6 } from "zod";
136
+
137
+ // src/tools/edit.ts
138
+ import { readFile, writeFile } from "fs/promises";
139
+ import { z } from "zod";
140
+
141
+ // src/diff.ts
142
+ import { structuredPatch } from "diff";
143
+ function diffLines(oldText, newText) {
144
+ const patch = structuredPatch("a", "b", oldText, newText, "", "", { context: 2 });
145
+ const out = [];
146
+ for (const hunk of patch.hunks) {
147
+ let oldLine = hunk.oldStart;
148
+ let newLine = hunk.newStart;
149
+ for (const raw of hunk.lines) {
150
+ const text = raw.slice(1);
151
+ if (raw[0] === "-") out.push({ kind: "del", line: oldLine++, text });
152
+ else if (raw[0] === "+") out.push({ kind: "add", line: newLine++, text });
153
+ else if (raw[0] === " ") {
154
+ out.push({ kind: "ctx", line: newLine, text });
155
+ oldLine++;
156
+ newLine++;
157
+ }
158
+ }
159
+ }
160
+ return out;
161
+ }
162
+ function formatDiff(lines) {
163
+ const mark = { add: "+", del: "-", ctx: " " };
164
+ return lines.map((l) => `${String(l.line).padStart(5)} ${mark[l.kind]} ${l.text}`).join("\n");
165
+ }
166
+
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
+ // src/tools/edit.ts
177
+ var schema = z.object({
178
+ path: z.string().describe("File path, relative to the project or absolute"),
179
+ old_string: z.string().describe("Exact text to replace; must be unique unless replace_all"),
180
+ new_string: z.string().describe("Replacement text"),
181
+ replace_all: z.boolean().optional().describe("Replace every occurrence")
182
+ });
183
+ function countOccurrences(haystack, needle) {
184
+ let count = 0;
185
+ for (let i = haystack.indexOf(needle); i !== -1; i = haystack.indexOf(needle, i + needle.length)) count++;
186
+ return count;
187
+ }
188
+ function applyEdit(content, input) {
189
+ if (input.old_string === input.new_string) {
190
+ return { output: "old_string and new_string are identical; nothing to change.", isError: true };
191
+ }
192
+ const eol = content.includes("\r\n") ? "\r\n" : "\n";
193
+ const toEol = (s) => s.replace(/\r?\n/g, eol);
194
+ const attempts = [[input.old_string, input.new_string]];
195
+ if (toEol(input.old_string) !== input.old_string) attempts.push([toEol(input.old_string), toEol(input.new_string)]);
196
+ for (const [oldText, newText] of attempts) {
197
+ const count = oldText ? countOccurrences(content, oldText) : 0;
198
+ if (count === 0) continue;
199
+ if (count > 1 && !input.replace_all) {
200
+ return {
201
+ output: `old_string appears ${count} times in ${input.path}. Add more context to make it unique, or use replace_all.`,
202
+ isError: true
203
+ };
204
+ }
205
+ return input.replace_all ? content.split(oldText).join(newText) : content.replace(oldText, () => newText);
206
+ }
207
+ return { output: `old_string not found in ${input.path}. Re-read the file.`, isError: true };
208
+ }
209
+ var editTool = defineTool({
210
+ name: "edit",
211
+ description: "Replace exact text in a file. The file must have been read first. old_string must match exactly (including indentation) and be unique unless replace_all is true.",
212
+ schema,
213
+ kind: "edit",
214
+ target: (input) => input.path,
215
+ async validate(input, ctx) {
216
+ const abs = resolvePath(ctx.cwd, input.path);
217
+ if (!ctx.readFiles.has(abs)) return `Read ${input.path} with read before editing it.`;
218
+ let content;
219
+ try {
220
+ content = await readFile(abs, "utf8");
221
+ } catch (error) {
222
+ return `Cannot read ${input.path}: ${errorCode(error)}`;
223
+ }
224
+ const next = applyEdit(content, input);
225
+ return typeof next === "string" ? void 0 : next.output;
226
+ },
227
+ async preview(input, ctx) {
228
+ const abs = resolvePath(ctx.cwd, input.path);
229
+ const content = await readFile(abs, "utf8");
230
+ const next = applyEdit(content, input);
231
+ return typeof next === "string" ? formatDiff(diffLines(content, next)) : void 0;
232
+ },
233
+ async run(input, ctx) {
234
+ const abs = resolvePath(ctx.cwd, input.path);
235
+ if (!ctx.readFiles.has(abs)) {
236
+ return { output: `Read ${input.path} with read before editing it.`, isError: true };
237
+ }
238
+ let content;
239
+ try {
240
+ content = await readFile(abs, "utf8");
241
+ } catch (error) {
242
+ return { output: `Cannot read ${input.path}: ${errorCode(error)}`, isError: true };
243
+ }
244
+ const next = applyEdit(content, input);
245
+ if (typeof next !== "string") return next;
246
+ await ctx.checkpoint?.(abs);
247
+ await writeFile(abs, next);
248
+ return { output: `Edited ${input.path}.`, display: formatDiff(diffLines(content, next)) };
249
+ }
250
+ });
251
+
252
+ // src/tools/glob.ts
253
+ import { glob as nativeGlob } from "fs/promises";
254
+ import { z as z2 } from "zod";
255
+
256
+ // src/abort.ts
257
+ var ABORTED = /* @__PURE__ */ Symbol("aborted");
258
+ function raceAbort(promise, signal) {
259
+ if (signal.aborted) return Promise.resolve(ABORTED);
260
+ return new Promise((resolve2, reject) => {
261
+ const onAbort = () => resolve2(ABORTED);
262
+ signal.addEventListener("abort", onAbort, { once: true });
263
+ promise.then(
264
+ (value) => {
265
+ signal.removeEventListener("abort", onAbort);
266
+ resolve2(value);
267
+ },
268
+ (error) => {
269
+ signal.removeEventListener("abort", onAbort);
270
+ reject(error);
271
+ }
272
+ );
273
+ });
274
+ }
275
+ async function collectUntilAbort(iterable, signal, onItem) {
276
+ const iterator = iterable[Symbol.asyncIterator]();
277
+ try {
278
+ for (; ; ) {
279
+ const next = signal ? await raceAbort(iterator.next(), signal) : await iterator.next();
280
+ if (next === ABORTED) return false;
281
+ if (next.done) return true;
282
+ onItem(next.value);
283
+ }
284
+ } finally {
285
+ Promise.resolve(iterator.return?.(void 0)).catch(() => {
286
+ });
287
+ }
288
+ }
289
+
290
+ // src/tools/glob.ts
291
+ var MAX_OUTPUT_CHARS = 24e3;
292
+ var globTool = defineTool({
293
+ name: "glob",
294
+ description: 'Find files by glob pattern, e.g. "src/**/*.ts". Respects .gitignore. Max 500 results.',
295
+ schema: z2.object({
296
+ pattern: z2.string().describe("Glob pattern"),
297
+ path: z2.string().optional().describe("Base directory, default project root")
298
+ }),
299
+ kind: "read",
300
+ target: (input) => input.pattern,
301
+ async run(input, ctx) {
302
+ const invalid = validateGlobPattern(input.pattern);
303
+ if (invalid) return { output: invalid, isError: true };
304
+ const root = resolvePath(ctx.cwd, input.path ?? ".");
305
+ const files = [];
306
+ const finished = await collectUntilAbort(
307
+ nativeGlob(input.pattern, { cwd: root, exclude: ignorePatterns(root) }),
308
+ ctx.signal,
309
+ (file) => files.push(normalizeGlobPath(file))
310
+ );
311
+ if (!finished) return { output: "Cancelled by the user.", isError: true };
312
+ files.sort();
313
+ if (!files.length) return { output: "No matching files." };
314
+ const first = files.slice(0, 500);
315
+ const shown = [];
316
+ let size = 0;
317
+ for (const file of first) {
318
+ const extra = file.length + (shown.length ? 1 : 0);
319
+ if (shown.length && size + extra > MAX_OUTPUT_CHARS) break;
320
+ shown.push(file);
321
+ size += extra;
322
+ if (size >= MAX_OUTPUT_CHARS) break;
323
+ }
324
+ const omitted = files.length - shown.length;
325
+ const more = omitted > 0 ? `
326
+ \u2026 ${omitted} more files omitted (narrow pattern/path)` : "";
327
+ return { output: shown.join("\n") + more, display: `${files.length} file` };
328
+ }
329
+ });
330
+
331
+ // src/tools/grep.ts
332
+ import { execFile } from "child_process";
333
+ import { readFile as readFile2 } from "fs/promises";
334
+ import { glob as nativeGlob2 } from "fs/promises";
335
+ import { z as z3 } from "zod";
336
+ var MAX_MATCHES = 200;
337
+ var MAX_OUTPUT_CHARS2 = 24e3;
338
+ async function grepJs(pattern, opts) {
339
+ const regex = new RegExp(pattern, opts.ignoreCase ? "i" : "");
340
+ 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
+ );
346
+ const out = [];
347
+ if (!finished) return out;
348
+ for (const file of files.sort()) {
349
+ if (opts.signal?.aborted) break;
350
+ let text;
351
+ try {
352
+ const buffer = await readFile2(resolvePath(opts.root, file));
353
+ if (buffer.subarray(0, 8e3).includes(0)) continue;
354
+ text = buffer.toString("utf8");
355
+ } catch {
356
+ continue;
357
+ }
358
+ const lines = text.split(/\r?\n/);
359
+ 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)}`);
361
+ }
362
+ if (out.length >= MAX_MATCHES) break;
363
+ }
364
+ return out;
365
+ }
366
+ function grepRg(pattern, opts) {
367
+ const args = ["--line-number", "--no-heading", "--color", "never", "--max-columns", "300", "-e", pattern];
368
+ 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([]);
374
+ 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));
378
+ });
379
+ });
380
+ }
381
+ var grepTool = defineTool({
382
+ name: "grep",
383
+ description: "Search file contents with a regular expression. Respects .gitignore. Returns path:line:text (max 200).",
384
+ schema: z3.object({
385
+ pattern: z3.string().describe("Regular expression"),
386
+ path: z3.string().optional().describe("Directory to search, default project root"),
387
+ glob: z3.string().optional().describe('Only files matching this glob, e.g. "**/*.ts"'),
388
+ ignore_case: z3.boolean().optional()
389
+ }),
390
+ kind: "read",
391
+ target: (input) => input.pattern,
392
+ async run(input, ctx) {
393
+ if (input.glob) {
394
+ const invalid = validateGlobPattern(input.glob);
395
+ if (invalid) return { output: invalid, isError: true };
396
+ }
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 };
402
+ }
403
+ const matches = await grepRg(input.pattern, opts) ?? await grepJs(input.pattern, opts);
404
+ if (ctx.signal.aborted) return { output: "Cancelled by the user.", isError: true };
405
+ if (!matches.length) return { output: "No matches." };
406
+ const shown = [];
407
+ let size = 0;
408
+ for (const match of matches) {
409
+ const extra = match.length + (shown.length ? 1 : 0);
410
+ if (shown.length && size + extra > MAX_OUTPUT_CHARS2) break;
411
+ shown.push(match);
412
+ size += extra;
413
+ if (size >= MAX_OUTPUT_CHARS2) break;
414
+ }
415
+ const omitted = matches.length - shown.length;
416
+ const suffix = omitted > 0 ? `
417
+ \u2026 ${omitted} more matches omitted (narrow pattern/path/glob)` : "";
418
+ return { output: shown.join("\n") + suffix, display: t("{n} results", { n: matches.length }) };
419
+ }
420
+ });
421
+
422
+ // src/tools/read.ts
423
+ import { readFile as readFile3 } from "fs/promises";
424
+ import { z as z4 } from "zod";
425
+ var MAX_LINE = 2e3;
426
+ var MAX_OUTPUT_CHARS3 = 4e4;
427
+ var MAX_IMAGE_BYTES = 4 * 1024 * 1024;
428
+ var IMAGE_HEADERS = [
429
+ {
430
+ mediaType: "image/png",
431
+ match: (h) => h.subarray(0, 8).equals(Buffer.from([137, 80, 78, 71, 13, 10, 26, 10]))
432
+ },
433
+ { mediaType: "image/jpeg", match: (h) => h[0] === 255 && h[1] === 216 && h[2] === 255 },
434
+ { mediaType: "image/gif", match: (h) => h.subarray(0, 6).toString("ascii").startsWith("GIF8") },
435
+ {
436
+ mediaType: "image/webp",
437
+ match: (h) => h.subarray(0, 4).toString("ascii") === "RIFF" && h.subarray(8, 12).toString("ascii") === "WEBP"
438
+ }
439
+ ];
440
+ var MEDIA_LABEL = {
441
+ "image/png": "png",
442
+ "image/jpeg": "jpeg",
443
+ "image/gif": "gif",
444
+ "image/webp": "webp"
445
+ };
446
+ function imageMediaType(buffer) {
447
+ return IMAGE_HEADERS.find((entry) => entry.match(buffer))?.mediaType;
448
+ }
449
+ var schema2 = z4.object({
450
+ path: z4.string().describe("File path, relative to the project or absolute"),
451
+ offset: z4.number().int().min(1).optional().describe("First line to read, 1-based"),
452
+ limit: z4.number().int().min(1).optional().describe("Number of lines, default 2000")
453
+ });
454
+ async function readImageResult(input, buffer, mediaType) {
455
+ if (buffer.length > MAX_IMAGE_BYTES) {
456
+ return {
457
+ output: `Cannot attach ${input.path}: image is ${(buffer.length / (1024 * 1024)).toFixed(1)} MB (limit 4 MB).`,
458
+ isError: true
459
+ };
460
+ }
461
+ const label = MEDIA_LABEL[mediaType] ?? mediaType;
462
+ return {
463
+ output: `[image attached] ${input.path} (${label}, ${(buffer.length / 1024).toFixed(1)} KB).`,
464
+ display: `${input.path} \xB7 image`,
465
+ images: [{ mediaType, data: buffer.toString("base64"), path: input.path }]
466
+ };
467
+ }
468
+ async function readTextResult(input, buffer) {
469
+ const lines = buffer.toString("utf8").split(/\r?\n/);
470
+ const start = (input.offset ?? 1) - 1;
471
+ const limit = input.limit ?? 2e3;
472
+ const requested = lines.slice(start, start + limit);
473
+ const shown = [];
474
+ let size = 0;
475
+ for (let i = 0; i < requested.length; i++) {
476
+ const line = requested[i];
477
+ const text = line.length > MAX_LINE ? `${line.slice(0, MAX_LINE)}\u2026 [line truncated]` : line;
478
+ const rendered = `${String(start + i + 1).padStart(6)} ${text}`;
479
+ const extra = rendered.length + (shown.length ? 1 : 0);
480
+ if (shown.length && size + extra > MAX_OUTPUT_CHARS3) break;
481
+ shown.push(rendered);
482
+ size += extra;
483
+ if (size >= MAX_OUTPUT_CHARS3) break;
484
+ }
485
+ const body = shown.join("\n");
486
+ const remaining = lines.length - start - shown.length;
487
+ const tokenBudgetHit = shown.length < requested.length;
488
+ const nextOffset = start + shown.length + 1;
489
+ return {
490
+ output: remaining > 0 ? tokenBudgetHit ? `${body}
491
+ \u2026 ${remaining} more lines (token budget reached; continue with offset=${nextOffset})` : `${body}
492
+ \u2026 ${remaining} more lines (use offset)` : body,
493
+ display: t("{n} lines", { n: shown.length })
494
+ };
495
+ }
496
+ function createReadTool(options = {}) {
497
+ const vision = options.vision !== false;
498
+ return defineTool({
499
+ name: "read",
500
+ description: vision ? "Read a text file, or an image (png/jpeg/gif/webp) which is attached for the model to see. Returns numbered lines (default first 2000). Use offset/limit for large files. Always read a file before editing it." : "Read a text file. Returns numbered lines (default first 2000). Use offset/limit for large files. Always read a file before editing it.",
501
+ schema: schema2,
502
+ kind: "read",
503
+ target: (input) => input.path,
504
+ async run(input, ctx) {
505
+ const abs = resolvePath(ctx.cwd, input.path);
506
+ let buffer;
507
+ try {
508
+ buffer = await readFile3(abs);
509
+ } catch (error) {
510
+ return { output: `Cannot read ${input.path}: ${errorCode(error)}`, isError: true };
511
+ }
512
+ const mediaType = vision ? imageMediaType(buffer) : void 0;
513
+ if (mediaType) {
514
+ ctx.readFiles.add(abs);
515
+ return readImageResult(input, buffer, mediaType);
516
+ }
517
+ if (buffer.subarray(0, 8e3).includes(0)) {
518
+ return { output: `${input.path} is a binary file; not read.`, isError: true };
519
+ }
520
+ ctx.readFiles.add(abs);
521
+ return readTextResult(input, buffer);
522
+ }
523
+ });
524
+ }
525
+ var readTool = createReadTool({ vision: true });
526
+
527
+ // src/tools/write.ts
528
+ import { existsSync } from "fs";
529
+ import { mkdir, readFile as readFile4, writeFile as writeFile2 } from "fs/promises";
530
+ import { dirname } from "path";
531
+ import { z as z5 } from "zod";
532
+ var writeTool = defineTool({
533
+ name: "write",
534
+ description: "Create or overwrite a file with the given content. Read an existing file first. Prefer edit for changes.",
535
+ schema: z5.object({
536
+ path: z5.string().describe("File path, relative to the project or absolute"),
537
+ content: z5.string().describe("Full new file content")
538
+ }),
539
+ kind: "edit",
540
+ target: (input) => input.path,
541
+ async validate(input, ctx) {
542
+ const abs = resolvePath(ctx.cwd, input.path);
543
+ return existsSync(abs) && !ctx.readFiles.has(abs) ? `${input.path} already exists. Read it with read before overwriting it.` : void 0;
544
+ },
545
+ async preview(input, ctx) {
546
+ const abs = resolvePath(ctx.cwd, input.path);
547
+ const old = existsSync(abs) ? await readFile4(abs, "utf8") : "";
548
+ return formatDiff(diffLines(old, input.content));
549
+ },
550
+ async run(input, ctx) {
551
+ const abs = resolvePath(ctx.cwd, input.path);
552
+ if (existsSync(abs) && !ctx.readFiles.has(abs)) {
553
+ return { output: `${input.path} already exists. Read it with read before overwriting it.`, isError: true };
554
+ }
555
+ try {
556
+ await ctx.checkpoint?.(abs);
557
+ await mkdir(dirname(abs), { recursive: true });
558
+ await writeFile2(abs, input.content);
559
+ } catch (error) {
560
+ return { output: `Failed to write ${input.path}: ${errorCode(error)}`, isError: true };
561
+ }
562
+ ctx.readFiles.add(abs);
563
+ const lines = input.content.split("\n").length;
564
+ return { output: `Wrote ${input.path} (${lines} lines).`, display: t("{lines} lines written", { lines }) };
565
+ }
566
+ });
567
+
568
+ // src/tools/index.ts
569
+ var ALL_TOOLS = [readTool, writeTool, editTool, bashTool, grepTool, globTool, fetchTool];
570
+ function toolDefinitions(tools) {
571
+ return tools.map((tool) => {
572
+ const { $schema: _ignored, ...parameters } = tool.jsonSchema ?? z6.toJSONSchema(tool.schema);
573
+ return { type: "function", function: { name: tool.name, description: tool.description, parameters, ...tool.outputJsonSchema ? { output_schema: tool.outputJsonSchema } : {} } };
574
+ });
575
+ }
576
+
577
+ // src/webBudget.ts
578
+ var KEEP_TOKENS = 4e4;
579
+ var PRUNE_MIN_TOKENS = 2e4;
580
+ var STUB = "[old tool result removed:";
581
+ var tokens = (text) => Math.ceil(text.length / 4);
582
+ var isStub = (content) => content.startsWith(STUB);
583
+ function pruneOldFetches(messages) {
584
+ const urls = /* @__PURE__ */ new Map();
585
+ for (const m of messages) {
586
+ if (m.role !== "assistant") continue;
587
+ for (const c of m.tool_calls ?? []) {
588
+ if (c.function.name !== "fetch") continue;
589
+ try {
590
+ urls.set(c.id, String(JSON.parse(c.function.arguments).url ?? ""));
591
+ } catch {
592
+ urls.set(c.id, "");
593
+ }
594
+ }
595
+ }
596
+ let kept = 0;
597
+ let oldTokens = 0;
598
+ const old = [];
599
+ for (let i = messages.length - 1; i >= 0; i--) {
600
+ const m = messages[i];
601
+ if (m.role !== "tool" || !urls.has(m.tool_call_id) || isStub(m.content)) continue;
602
+ kept += tokens(m.content);
603
+ if (kept > KEEP_TOKENS) {
604
+ old.push(i);
605
+ oldTokens += tokens(m.content);
606
+ }
607
+ }
608
+ if (oldTokens <= PRUNE_MIN_TOKENS) return false;
609
+ for (const i of old) {
610
+ const m = messages[i];
611
+ messages[i] = { ...m, content: `${STUB} fetch ${urls.get(m.tool_call_id)}. Fetch it again (use prompt for a focused excerpt) if still needed.]` };
612
+ }
613
+ return true;
614
+ }
615
+ var WebBudget = class {
616
+ fetches = 0;
617
+ chars = 0;
618
+ perTurn;
619
+ sessionChars;
620
+ constructor(limits = {}) {
621
+ this.perTurn = limits.perTurn ?? 8;
622
+ this.sessionChars = limits.sessionChars ?? 3e5;
623
+ }
624
+ startTurn() {
625
+ this.fetches = 0;
626
+ }
627
+ /** Gives back a reservation, e.g. when the fetch was served from cache and cost nothing. */
628
+ refund() {
629
+ if (this.fetches > 0) this.fetches--;
630
+ }
631
+ reset() {
632
+ this.fetches = 0;
633
+ this.chars = 0;
634
+ }
635
+ /** Reserves one fetch; returns the refusal to show the model when over budget. */
636
+ take() {
637
+ if (this.fetches >= this.perTurn) {
638
+ return `Limit of ${this.perTurn} fetches per question reached (each fetch uses the user's tokens). Answer from the results you already have; if that is not enough, tell the user what is still missing.`;
639
+ }
640
+ if (this.chars >= this.sessionChars) {
641
+ return `Total web page limit for this session reached (~${Math.round(this.sessionChars / 4e3)}k tokens). Answer from the results you already have and ask the user whether to search more.`;
642
+ }
643
+ this.fetches++;
644
+ return void 0;
645
+ }
646
+ record(chars) {
647
+ this.chars += chars;
648
+ }
649
+ };
650
+
651
+ // src/budget.ts
652
+ function priceFor(cap, model) {
653
+ if (!cap.prices) return void 0;
654
+ const key = Object.keys(cap.prices).find((k) => model.includes(k));
655
+ return key ? cap.prices[key] : void 0;
656
+ }
657
+ function budgetStatus(usage, cap, model) {
658
+ if (!cap) return void 0;
659
+ const total = usage.inputTokens + usage.outputTokens;
660
+ if (cap.tokens !== void 0 && total >= cap.tokens) return { kind: "tokens", used: total, limit: cap.tokens };
661
+ if (cap.usd !== void 0) {
662
+ const price = priceFor(cap, model);
663
+ if (price) {
664
+ const usd = (usage.inputTokens * price.input + usage.outputTokens * price.output) / 1e6;
665
+ if (usd >= cap.usd) return { kind: "usd", used: usd, limit: cap.usd };
666
+ }
667
+ }
668
+ return void 0;
669
+ }
670
+
671
+ // src/agent.ts
672
+ var CANCELLED = "Cancelled by the user.";
673
+ var COMPACT_PROMPT = "Summarize the conversation so far for yourself so you can continue the work with no other context. Include: the user goals, decisions made, files touched and their current state, commands run and results, the task in progress and the next step. Be concise but complete.";
674
+ var Agent = class {
675
+ constructor(opts) {
676
+ this.opts = opts;
677
+ this.provider = opts.provider;
678
+ this.permissions = opts.permissions;
679
+ this.tools = opts.tools;
680
+ this.definitions = toolDefinitions(opts.tools);
681
+ this.turnTools = this.tools;
682
+ this.turnDefinitions = this.definitions;
683
+ this.messages = [...opts.history ?? []];
684
+ this.maxSteps = typeof opts.maxSteps === "number" && Number.isFinite(opts.maxSteps) && opts.maxSteps > 0 ? Math.max(1, Math.floor(opts.maxSteps)) : void 0;
685
+ this.contextWindow = opts.contextWindow ?? 128e3;
686
+ this.systemPrompt = opts.systemPrompt;
687
+ this.reasoning = opts.reasoning ?? "auto";
688
+ }
689
+ opts;
690
+ provider;
691
+ permissions;
692
+ messages;
693
+ totalUsage = { inputTokens: 0, outputTokens: 0 };
694
+ lastInputTokens = 0;
695
+ onEvent = () => {
696
+ };
697
+ reasoning;
698
+ askPermission = async () => "no";
699
+ tools;
700
+ definitions;
701
+ // Snapshot for the running turn: tools added/removed mid-turn (e.g. an MCP server connecting) apply from the next turn.
702
+ turnTools;
703
+ turnDefinitions;
704
+ readFiles = /* @__PURE__ */ new Set();
705
+ webBudget = new WebBudget();
706
+ maxSteps;
707
+ // One window for every model; resolved from contextWindowFor() (catalog + env override).
708
+ contextWindow;
709
+ systemPrompt;
710
+ setSystemPrompt(text) {
711
+ this.systemPrompt = text;
712
+ }
713
+ setContextWindow(window) {
714
+ this.contextWindow = window;
715
+ }
716
+ setTools(tools) {
717
+ this.tools = tools;
718
+ this.definitions = toolDefinitions(tools);
719
+ }
720
+ isParallelSafe(call) {
721
+ const tool = this.turnTools.find((t2) => t2.name === call.name);
722
+ if (!tool?.parallelSafe) return false;
723
+ try {
724
+ const parsed = tool.schema.safeParse(JSON.parse(call.arguments || "{}"));
725
+ return parsed.success && tool.parallelSafe(parsed.data);
726
+ } catch {
727
+ return false;
728
+ }
729
+ }
730
+ push(message) {
731
+ this.messages.push(message);
732
+ this.opts.onMessage?.(message);
733
+ }
734
+ /** Swaps in another conversation (resume, new session) without writing to the session log. */
735
+ load(messages) {
736
+ this.messages = messages;
737
+ this.readFiles.clear();
738
+ this.webBudget.reset();
739
+ this.lastInputTokens = estimateMessages(messages) + estimateTokens(this.systemPrompt);
740
+ }
741
+ clear() {
742
+ this.messages = [];
743
+ this.readFiles.clear();
744
+ this.webBudget.reset();
745
+ this.lastInputTokens = 0;
746
+ this.opts.onReset?.();
747
+ }
748
+ async compact(signal) {
749
+ pruneOldToolOutputs(this.messages);
750
+ const keepTokens = Math.min(15e3, Math.floor(this.contextWindow * 0.1));
751
+ const [older, keep] = splitKeepRecent(this.messages, keepTokens);
752
+ const history = trimToFit(older, Math.floor(this.contextWindow * 0.8));
753
+ const pending = this.provider.chat({
754
+ messages: [{ role: "system", content: this.systemPrompt }, ...history, { role: "user", content: COMPACT_PROMPT }],
755
+ // Some gateways reject tool_calls in history when no tools are declared.
756
+ tools: this.definitions,
757
+ signal,
758
+ reasoning: this.reasoning
759
+ });
760
+ const completion = await raceAbort(pending, signal);
761
+ if (completion === ABORTED) throw new Error("aborted");
762
+ this.clear();
763
+ this.push({ role: "user", content: `Summary of the previous conversation:
764
+ ${completion.text}` });
765
+ this.push({ role: "assistant", content: "OK, I will continue from this summary." });
766
+ for (const m of keep) this.push(m);
767
+ this.onEvent({ type: "compacted" });
768
+ }
769
+ /**
770
+ * Events of one tool execution. A cancelled turn gives up on a tool that does not stop, and whatever that tool
771
+ * reports later must not reach the screen or a newer turn, so this is per execution and not per call id (ids repeat).
772
+ */
773
+ report(ticket, event) {
774
+ if (ticket.abandoned) return;
775
+ if (event.type === "toolStart") ticket.open = true;
776
+ if (event.type === "toolEnd") ticket.open = false;
777
+ this.onEvent(event);
778
+ }
779
+ async run(text, signal) {
780
+ this.turnTools = this.tools;
781
+ this.turnDefinitions = this.definitions;
782
+ this.opts.onTurnStart?.();
783
+ this.webBudget.startTurn();
784
+ try {
785
+ const over0 = this.overBudget();
786
+ if (over0) {
787
+ this.onEvent({ type: "budgetExceeded", ...over0 });
788
+ return;
789
+ }
790
+ if (this.lastInputTokens > this.contextWindow * 0.8 && !hasPendingProgrammaticReplay(this.messages)) await this.compact(signal);
791
+ this.push({ role: "user", content: text });
792
+ for (let step = 0; this.maxSteps === void 0 || step < this.maxSteps; step++) {
793
+ if (step > 0 && this.lastInputTokens > this.contextWindow * 0.8 && !hasPendingProgrammaticReplay(this.messages)) {
794
+ await this.compact(signal);
795
+ this.push({ role: "user", content: `Continue this task using the summary above: ${text}` });
796
+ }
797
+ pruneOldFetches(this.messages);
798
+ const over = this.overBudget();
799
+ if (over) {
800
+ this.onEvent({ type: "budgetExceeded", ...over });
801
+ return;
802
+ }
803
+ const pendingCompletion = this.provider.chat({
804
+ messages: [{ role: "system", content: this.systemPrompt }, ...this.messages],
805
+ tools: this.turnDefinitions,
806
+ programmaticToolNames: this.turnTools.filter((tool) => tool.programmaticSafe).map((tool) => tool.name),
807
+ signal,
808
+ // Late output from a cancelled provider must not leak into the next turn.
809
+ onText: (delta) => {
810
+ if (!signal.aborted) this.onEvent({ type: "text", delta });
811
+ },
812
+ onThinking: (delta) => {
813
+ if (!signal.aborted) this.onEvent({ type: "thinking", delta });
814
+ },
815
+ reasoning: this.reasoning
816
+ });
817
+ const outcome = await raceAbort(pendingCompletion, signal);
818
+ if (outcome === ABORTED) {
819
+ this.onEvent({ type: "aborted" });
820
+ return;
821
+ }
822
+ let completion = outcome;
823
+ if (!completion.toolCalls.length && completion.finishReason !== "repetition") {
824
+ const recovered = recoverTextToolCalls(completion.text, this.turnDefinitions);
825
+ if (recovered) {
826
+ completion = { ...completion, ...recovered };
827
+ this.onEvent({ type: "textReplace", text: recovered.text });
828
+ }
829
+ }
830
+ const usage = completion.usage ?? {
831
+ inputTokens: estimateTokens(this.systemPrompt) + estimateTokens(JSON.stringify(this.messages)),
832
+ outputTokens: estimateTokens(completion.text) + (completion.toolCalls.length ? estimateTokens(JSON.stringify(completion.toolCalls)) : 0)
833
+ };
834
+ this.totalUsage.inputTokens += usage.inputTokens;
835
+ this.totalUsage.outputTokens += usage.outputTokens;
836
+ this.totalUsage.cachedInputTokens = (this.totalUsage.cachedInputTokens ?? 0) + (usage.cachedInputTokens ?? 0);
837
+ this.totalUsage.cacheWriteTokens = (this.totalUsage.cacheWriteTokens ?? 0) + (usage.cacheWriteTokens ?? 0);
838
+ this.lastInputTokens = usage.inputTokens;
839
+ this.onEvent({ type: "usage", ...this.totalUsage });
840
+ this.opts.onUsage?.(usage);
841
+ this.push(completionAssistantMessage(completion));
842
+ if (completion.finishReason === "repetition") {
843
+ this.onEvent({ type: "textReplace", text: completion.text });
844
+ this.onEvent({
845
+ type: "error",
846
+ message: t("The model got stuck repeating text, so the answer was stopped. Try again or switch models with /model.")
847
+ });
848
+ return;
849
+ }
850
+ if (!completion.toolCalls.length) {
851
+ if (completion.finishReason === "continue") continue;
852
+ this.onEvent({ type: "done" });
853
+ return;
854
+ }
855
+ const calls = completion.toolCalls;
856
+ let i = 0;
857
+ const roundImages = [];
858
+ while (i < calls.length) {
859
+ if (signal.aborted) {
860
+ this.cancelCalls(calls.slice(i));
861
+ this.onEvent({ type: "aborted" });
862
+ return;
863
+ }
864
+ let j = i + 1;
865
+ if (this.isParallelSafe(calls[i])) while (j < calls.length && this.isParallelSafe(calls[j])) j++;
866
+ const batch = calls.slice(i, j);
867
+ const tickets = batch.map(() => ({ abandoned: false, open: false }));
868
+ const running = Promise.all(batch.map((c, k) => this.runTool(c, signal, tickets[k])));
869
+ const outcome2 = await raceAbort(running, signal);
870
+ if (outcome2 === ABORTED) {
871
+ batch.forEach((c, k) => {
872
+ const ticket = tickets[k];
873
+ ticket.abandoned = true;
874
+ if (ticket.open) this.onEvent({ type: "toolEnd", id: c.id, tool: c.name, output: CANCELLED, isError: true });
875
+ });
876
+ running.catch(() => {
877
+ });
878
+ this.cancelCalls(calls.slice(i));
879
+ this.onEvent({ type: "aborted" });
880
+ return;
881
+ }
882
+ batch.forEach((c, k) => {
883
+ roundImages.push(...outcome2[k].images ?? []);
884
+ this.push({
885
+ role: "tool",
886
+ tool_call_id: c.id,
887
+ content: outcome2[k].output,
888
+ ...c.caller ? { responses_caller: c.caller } : {}
889
+ });
890
+ });
891
+ i = j;
892
+ }
893
+ if (roundImages.length) this.push(imageUserMessage(roundImages));
894
+ if (signal.aborted) {
895
+ this.onEvent({ type: "aborted" });
896
+ return;
897
+ }
898
+ }
899
+ if (this.maxSteps !== void 0) this.onEvent({ type: "stepLimit", maxSteps: this.maxSteps });
900
+ } catch (error) {
901
+ if (signal.aborted) {
902
+ this.onEvent({ type: "aborted" });
903
+ return;
904
+ }
905
+ const message = error.message;
906
+ const noTools = error.status === 400 && /tool/i.test(message);
907
+ this.onEvent({
908
+ type: "error",
909
+ message: noTools ? `${message}
910
+ ${t("This model may not support tool calling. Try another model with /model.")}` : message
911
+ });
912
+ }
913
+ }
914
+ /** Current cap status; undefined means the budget is not exhausted. */
915
+ overBudget() {
916
+ return budgetStatus(this.totalUsage, this.opts.usageCap, this.opts.budgetModel ?? "");
917
+ }
918
+ /** Every tool call needs an answer in the history, or the next request to the model is rejected. */
919
+ cancelCalls(calls) {
920
+ for (const pending of calls)
921
+ this.push({
922
+ role: "tool",
923
+ tool_call_id: pending.id,
924
+ content: CANCELLED,
925
+ ...pending.caller ? { responses_caller: pending.caller } : {}
926
+ });
927
+ }
928
+ /**
929
+ * Every tool call must resolve with text: a rejected one would leave the assistant tool_calls
930
+ * without answers, and the next request to the model would be rejected until a restart.
931
+ */
932
+ async runTool(call, signal, ticket) {
933
+ const emit = (event) => this.report(ticket, event);
934
+ try {
935
+ return await this.executeTool(call, signal, ticket);
936
+ } catch (error) {
937
+ const message = `Error: ${error?.message || String(error)}`;
938
+ if (ticket.open) {
939
+ emit({ type: "toolEnd", id: call.id, tool: call.name, output: message, isError: true });
940
+ } else {
941
+ emit({ type: "toolStart", id: call.id, tool: call.name, target: "" });
942
+ emit({ type: "toolEnd", id: call.id, tool: call.name, output: message, isError: true });
943
+ }
944
+ return { output: message };
945
+ }
946
+ }
947
+ async executeTool(call, signal, ticket) {
948
+ const emit = (event) => this.report(ticket, event);
949
+ const fail = (output, target2 = "") => {
950
+ emit({ type: "toolStart", id: call.id, tool: call.name, target: target2 });
951
+ emit({ type: "toolEnd", id: call.id, tool: call.name, output, isError: true });
952
+ return { output };
953
+ };
954
+ const tool = this.turnTools.find((t2) => t2.name === call.name);
955
+ if (!tool) return fail(`Tool "${call.name}" does not exist. Available tools: ${this.turnTools.map((t2) => t2.name).join(", ")}`);
956
+ let args;
957
+ try {
958
+ args = JSON.parse(call.arguments || "{}");
959
+ } catch {
960
+ return fail(`Arguments are not valid JSON: ${call.arguments.slice(0, 200)}`);
961
+ }
962
+ const parsed = tool.schema.safeParse(args);
963
+ if (!parsed.success) {
964
+ return fail(`Invalid arguments: ${parsed.error.issues.map((i) => `${i.path.join(".") || "(root)"}: ${i.message}`).join("; ")}`);
965
+ }
966
+ const input = parsed.data;
967
+ const target = tool.target(input);
968
+ const overBudget = tool.name === "fetch" ? this.webBudget.take() : void 0;
969
+ if (overBudget) return fail(overBudget, target);
970
+ await this.opts.pluginEmit?.("PreToolUse", { tool: tool.name, input });
971
+ const pre = await runHooks(this.opts.hooks, "PreToolUse", { tool: tool.name, input }, { env: this.opts.env, cwd: this.opts.cwd });
972
+ if (pre.blocked) return fail(pre.blocked, target);
973
+ const ctx = {
974
+ cwd: this.opts.cwd,
975
+ signal,
976
+ readFiles: this.readFiles,
977
+ callId: call.id,
978
+ emit,
979
+ ask: (req) => this.askPermission(req),
980
+ checkpoint: this.opts.checkpoint,
981
+ refundFetch: () => this.webBudget.refund(),
982
+ addUsage: (u) => {
983
+ this.totalUsage.inputTokens += u.inputTokens;
984
+ this.totalUsage.outputTokens += u.outputTokens;
985
+ this.totalUsage.cachedInputTokens = (this.totalUsage.cachedInputTokens ?? 0) + (u.cachedInputTokens ?? 0);
986
+ this.totalUsage.cacheWriteTokens = (this.totalUsage.cacheWriteTokens ?? 0) + (u.cacheWriteTokens ?? 0);
987
+ this.onEvent({ type: "usage", ...this.totalUsage });
988
+ this.opts.onUsage?.(u);
989
+ }
990
+ };
991
+ emit({ type: "toolStart", id: call.id, tool: tool.name, target });
992
+ const invalid = await tool.validate?.(input, ctx).catch((error) => error.message);
993
+ if (invalid) {
994
+ emit({ type: "toolEnd", id: call.id, tool: tool.name, output: invalid, isError: true });
995
+ return { output: invalid };
996
+ }
997
+ const request = { tool: tool.name, kind: tool.kind, target };
998
+ let decision = this.permissions.check(request);
999
+ if (decision === "ask") {
1000
+ const preview = await tool.preview?.(input, ctx).catch(() => void 0);
1001
+ const answer = await this.askPermission({
1002
+ ...request,
1003
+ preview,
1004
+ sessionRules: this.permissions.rulesFor(request),
1005
+ ...this.opts.label ? { agent: this.opts.label } : {}
1006
+ });
1007
+ if (answer === "session") this.permissions.allowForSession(request);
1008
+ if (answer === "all") this.permissions.mode = "allowAll";
1009
+ decision = answer === "no" ? "deny" : "allow";
1010
+ }
1011
+ if (decision === "deny") {
1012
+ const output = this.permissions.mode === "plan" ? "Denied: plan mode only allows reading and searching. Write the plan for the user without changing anything." : "The user declined to run this tool. Ask the user what they want, or try another approach.";
1013
+ emit({ type: "toolEnd", id: call.id, tool: tool.name, output, isError: true });
1014
+ return { output };
1015
+ }
1016
+ let result;
1017
+ try {
1018
+ result = await tool.run(input, ctx);
1019
+ } catch (error) {
1020
+ result = { output: `Error: ${error.message}`, isError: true };
1021
+ }
1022
+ if (tool.name === "fetch" && !result.isError) this.webBudget.record(result.output.length);
1023
+ await runHooks(
1024
+ this.opts.hooks,
1025
+ "PostToolUse",
1026
+ { tool: tool.name, input, output: result.output },
1027
+ { env: this.opts.env, cwd: this.opts.cwd }
1028
+ );
1029
+ await this.opts.pluginEmit?.("PostToolUse", { tool: tool.name, input, output: result.output });
1030
+ emit({ type: "toolEnd", id: call.id, tool: tool.name, output: result.output, display: result.display, isError: !!result.isError });
1031
+ return result;
1032
+ }
1033
+ };
1034
+
1035
+ // src/checkpoints.ts
1036
+ import { existsSync as existsSync2, statSync } from "fs";
1037
+ import { readFile as readFile5, rm, writeFile as writeFile3 } from "fs/promises";
1038
+ var MAX_TURNS = 20;
1039
+ var MAX_BYTES = 2 * 1024 * 1024;
1040
+ function emptyResult() {
1041
+ return { restored: [], deleted: [], skipped: [], failed: [] };
1042
+ }
1043
+ async function restoreTurn(turn, result) {
1044
+ for (const [path, original] of turn) {
1045
+ try {
1046
+ if (original === "skipped") result.skipped.push(path);
1047
+ else if (original === null) {
1048
+ await rm(path, { force: true });
1049
+ result.deleted.push(path);
1050
+ } else {
1051
+ await writeFile3(path, original);
1052
+ result.restored.push(path);
1053
+ }
1054
+ } catch {
1055
+ result.failed.push(path);
1056
+ }
1057
+ }
1058
+ }
1059
+ var CheckpointStore = class {
1060
+ history = [];
1061
+ current;
1062
+ pendingRedo;
1063
+ beginTurn() {
1064
+ if (this.current?.size === 0) return;
1065
+ this.current = /* @__PURE__ */ new Map();
1066
+ this.history.push(this.current);
1067
+ this.pendingRedo = void 0;
1068
+ }
1069
+ /** Call before writing a file; only the first snapshot per file per turn is kept. */
1070
+ async snapshot(path) {
1071
+ if (!this.current) this.beginTurn();
1072
+ const turn = this.current;
1073
+ if (turn.has(path)) return;
1074
+ if (!existsSync2(path)) turn.set(path, null);
1075
+ else if (statSync(path).size > MAX_BYTES) turn.set(path, "skipped");
1076
+ else turn.set(path, await readFile5(path));
1077
+ while (this.turns() > MAX_TURNS) this.history.shift();
1078
+ }
1079
+ turns() {
1080
+ return this.history.filter((t2) => t2.size).length;
1081
+ }
1082
+ /** Total entries including empty ones; used to align file snapshots with conversation turns. */
1083
+ entries() {
1084
+ return this.history.length;
1085
+ }
1086
+ async undo() {
1087
+ while (this.history.length && !this.history.at(-1)?.size) this.history.pop();
1088
+ const turn = this.history.pop();
1089
+ this.current = void 0;
1090
+ if (!turn) return void 0;
1091
+ const result = emptyResult();
1092
+ const redo = /* @__PURE__ */ new Map();
1093
+ await this.captureFor(turn, redo);
1094
+ await restoreTurn(turn, result);
1095
+ this.pendingRedo = redo;
1096
+ return result;
1097
+ }
1098
+ /**
1099
+ * Revert every snapshot taken at or after the `keep`-th entry, newest first, so the files
1100
+ * end in the state from before the turn that created entry `keep`. Used by /rewind, which
1101
+ * pairs conversation turns with the entry count recorded when each turn started.
1102
+ */
1103
+ async undoTo(keep) {
1104
+ const target = Math.max(0, Math.min(keep, this.history.length));
1105
+ const result = emptyResult();
1106
+ const redo = /* @__PURE__ */ new Map();
1107
+ let applied = false;
1108
+ while (this.history.length > target) {
1109
+ const turn = this.history.pop();
1110
+ if (turn?.size) {
1111
+ await this.captureFor(turn, redo);
1112
+ await restoreTurn(turn, result);
1113
+ applied = true;
1114
+ }
1115
+ }
1116
+ this.current = void 0;
1117
+ this.pendingRedo = applied ? redo : void 0;
1118
+ return applied ? result : void 0;
1119
+ }
1120
+ /**
1121
+ * Re-apply the file state captured right before the last undo/undoTo.
1122
+ * Only file edits are redone, never conversation history. Invalidated by
1123
+ * any new snapshot turn or clear(), so redo can't resurrect stale content.
1124
+ */
1125
+ async redo() {
1126
+ const turn = this.pendingRedo;
1127
+ if (!turn) return void 0;
1128
+ this.pendingRedo = void 0;
1129
+ const result = emptyResult();
1130
+ await restoreTurn(turn, result);
1131
+ return result;
1132
+ }
1133
+ /** Records the on-disk state of a turn's paths so redo() can put it back. */
1134
+ async captureFor(turn, into) {
1135
+ for (const [path, original] of turn) {
1136
+ if (into.has(path)) continue;
1137
+ if (original === "skipped") {
1138
+ into.set(path, "skipped");
1139
+ continue;
1140
+ }
1141
+ try {
1142
+ if (!existsSync2(path)) into.set(path, null);
1143
+ else if (statSync(path).size > MAX_BYTES) into.set(path, "skipped");
1144
+ else into.set(path, await readFile5(path));
1145
+ } catch {
1146
+ into.set(path, "skipped");
1147
+ }
1148
+ }
1149
+ }
1150
+ clear() {
1151
+ this.history.length = 0;
1152
+ this.current = void 0;
1153
+ this.pendingRedo = void 0;
1154
+ }
1155
+ };
1156
+
1157
+ // src/contextWindow.ts
1158
+ var DEFAULT_CONTEXT_WINDOW = 128e3;
1159
+ var WINDOWS = [
1160
+ // BotConnector's Luna request cap (see LUNA_AUTO_COMPACT_THRESHOLD in provider.ts).
1161
+ [/gpt-6-luna/i, 272e3],
1162
+ [/claude-(opus|sonnet|haiku)-\d/i, 2e5],
1163
+ [/gemini-\d/i, 1048576]
1164
+ ];
1165
+ function contextWindowFor(modelRef, env = process.env) {
1166
+ const override = Number(env.BCCLI_CONTEXT_WINDOW);
1167
+ if (Number.isInteger(override) && override > 0) return override;
1168
+ const model = modelRef.slice(modelRef.indexOf("/") + 1);
1169
+ for (const [pattern, window] of WINDOWS) if (pattern.test(model)) return window;
1170
+ return DEFAULT_CONTEXT_WINDOW;
1171
+ }
1172
+
1173
+ // src/mcp/manager.ts
1174
+ import { Client } from "@modelcontextprotocol/sdk/client/index.js";
1175
+ import { UnauthorizedError } from "@modelcontextprotocol/sdk/client/auth.js";
1176
+ import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
1177
+ import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
1178
+ import { z as z7 } from "zod";
1179
+ var MCP_OUTPUT_MAX_CHARS = 32e3;
1180
+ var CHILD_ENV_KEYS = [
1181
+ // portable
1182
+ "PATH",
1183
+ "HOME",
1184
+ "USERPROFILE",
1185
+ "LANG",
1186
+ "LC_ALL",
1187
+ "TZ",
1188
+ "TMP",
1189
+ "TEMP",
1190
+ "TMPDIR",
1191
+ // windows
1192
+ "SYSTEMROOT",
1193
+ "SYSTEMDRIVE",
1194
+ "COMSPEC",
1195
+ "PATHEXT",
1196
+ "APPDATA",
1197
+ "LOCALAPPDATA",
1198
+ "HOMEDRIVE",
1199
+ "HOMEPATH",
1200
+ "USERNAME",
1201
+ "PROGRAMFILES",
1202
+ "PROCESSOR_ARCHITECTURE",
1203
+ // posix
1204
+ "LOGNAME",
1205
+ "SHELL",
1206
+ "TERM",
1207
+ "USER"
1208
+ ];
1209
+ function mcpChildEnv(extra) {
1210
+ const env = {};
1211
+ for (const key of CHILD_ENV_KEYS) {
1212
+ const value = process.env[key];
1213
+ if (value !== void 0) env[key] = value;
1214
+ }
1215
+ return { ...env, ...extra };
1216
+ }
1217
+ function mcpToolName(server, tool, taken) {
1218
+ const clean = (s) => s.replace(/[^A-Za-z0-9_-]+/g, "_");
1219
+ const base = `mcp__${clean(server)}__${clean(tool)}`.slice(0, 64);
1220
+ let name = base;
1221
+ for (let n = 2; taken.has(name); n++) name = `${base.slice(0, 64 - String(n).length - 1)}_${n}`;
1222
+ taken.add(name);
1223
+ return name;
1224
+ }
1225
+ function withTimeout(promise, ms) {
1226
+ let timer;
1227
+ const timeout = new Promise((_, reject) => {
1228
+ timer = setTimeout(() => reject(new Error(t("timeout after {n} seconds", { n: Math.round(ms / 1e3) }))), ms);
1229
+ });
1230
+ return Promise.race([promise, timeout]).finally(() => clearTimeout(timer));
1231
+ }
1232
+ var McpManager = class {
1233
+ constructor(opts = {}) {
1234
+ this.opts = opts;
1235
+ }
1236
+ opts;
1237
+ servers = /* @__PURE__ */ new Map();
1238
+ taken = /* @__PURE__ */ new Set();
1239
+ async start(specs) {
1240
+ await Promise.all(specs.map((spec) => this.add(spec)));
1241
+ }
1242
+ async add(spec) {
1243
+ await this.remove(spec.name);
1244
+ const entry = { spec, state: { name: spec.name, source: spec.source, status: "starting", tools: 0 }, tools: [] };
1245
+ this.servers.set(spec.name, entry);
1246
+ this.opts.onChange?.();
1247
+ const client = new Client({ name: "bccli", version: "0.3.0" });
1248
+ entry.client = client;
1249
+ const timeoutMs = this.opts.connectTimeoutMs ?? 3e4;
1250
+ try {
1251
+ const config = expandEnvVars(spec.config, process.env);
1252
+ let transport;
1253
+ if (config.type === "http") {
1254
+ const oauth = this.opts.home && (config.auth === "oauth" || hasStoredTokens(this.opts.home, config.url)) ? new FileOAuthProvider({ home: this.opts.home, serverUrl: config.url, open: this.opts.open }) : void 0;
1255
+ transport = new StreamableHTTPClientTransport(new URL(config.url), {
1256
+ requestInit: { headers: config.headers ?? {} },
1257
+ ...oauth ? { authProvider: oauth } : {}
1258
+ });
1259
+ } else {
1260
+ transport = new StdioClientTransport({
1261
+ command: config.command,
1262
+ args: config.args ?? [],
1263
+ env: mcpChildEnv(config.env),
1264
+ stderr: "ignore"
1265
+ });
1266
+ }
1267
+ await withTimeout(client.connect(transport), timeoutMs);
1268
+ const { tools } = await withTimeout(client.listTools(), timeoutMs);
1269
+ if (this.servers.get(spec.name) !== entry) {
1270
+ await client.close().catch(() => {
1271
+ });
1272
+ return;
1273
+ }
1274
+ entry.tools = tools.map((t2) => this.wrap(spec.name, client, t2));
1275
+ entry.state = { ...entry.state, status: "ready", tools: entry.tools.length };
1276
+ } catch (error) {
1277
+ if (error instanceof UnauthorizedError) {
1278
+ entry.state = {
1279
+ ...entry.state,
1280
+ status: "error",
1281
+ error: t("OAuth required for {name}: run bccli mcp auth {name}", { name: spec.name })
1282
+ };
1283
+ } else {
1284
+ entry.state = { ...entry.state, status: "error", error: error.message };
1285
+ }
1286
+ await client.close().catch(() => {
1287
+ });
1288
+ if (this.servers.get(spec.name) !== entry) return;
1289
+ }
1290
+ this.opts.onChange?.();
1291
+ }
1292
+ wrap(server, client, tool) {
1293
+ return {
1294
+ name: mcpToolName(server, tool.name, this.taken),
1295
+ description: `[MCP ${server}] ${tool.description ?? tool.name}`.slice(0, 1024),
1296
+ schema: z7.record(z7.string(), z7.unknown()),
1297
+ jsonSchema: tool.inputSchema,
1298
+ ...tool.outputSchema ? { outputJsonSchema: tool.outputSchema } : {},
1299
+ kind: "mcp",
1300
+ programmaticSafe: tool.annotations?.readOnlyHint === true,
1301
+ target: (input) => {
1302
+ const text = JSON.stringify(input);
1303
+ return text.length > 80 ? `${text.slice(0, 77)}\u2026` : text;
1304
+ },
1305
+ async run(input, ctx) {
1306
+ try {
1307
+ 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)";
1309
+ 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
1313
+ };
1314
+ } catch (error) {
1315
+ return { output: `Server MCP ${server} error: ${error.message}`, isError: true };
1316
+ }
1317
+ }
1318
+ };
1319
+ }
1320
+ async remove(name) {
1321
+ const entry = this.servers.get(name);
1322
+ if (!entry) return;
1323
+ this.servers.delete(name);
1324
+ for (const t2 of entry.tools) this.taken.delete(t2.name);
1325
+ if (entry.tools.length) this.opts.onToolsRemoved?.(entry.tools.map((t2) => t2.name));
1326
+ await entry.client?.close().catch(() => {
1327
+ });
1328
+ this.opts.onChange?.();
1329
+ }
1330
+ tools() {
1331
+ return [...this.servers.values()].flatMap((s) => s.tools).sort((a, b) => a.name.localeCompare(b.name));
1332
+ }
1333
+ states() {
1334
+ return [...this.servers.values()].map((s) => s.state);
1335
+ }
1336
+ async stop() {
1337
+ await Promise.all([...this.servers.keys()].map((name) => this.remove(name)));
1338
+ }
1339
+ };
1340
+
1341
+ // src/setup.ts
1342
+ import { homedir } from "os";
1343
+
1344
+ // src/tools/plan.ts
1345
+ import { z as z8 } from "zod";
1346
+ function createExitPlanTool(opts) {
1347
+ return defineTool({
1348
+ name: "exit_plan",
1349
+ description: "Plan mode only: present the finished plan to the user for approval. If approved you may start implementing it.",
1350
+ schema: z8.object({ plan: z8.string().describe("The plan, in markdown") }),
1351
+ kind: "read",
1352
+ target: () => t("plan"),
1353
+ async run(input) {
1354
+ if (opts.permissions.mode !== "plan") return { output: "exit_plan is only used in plan mode.", isError: true };
1355
+ const decision = await opts.interaction.approvePlan(input.plan);
1356
+ if (decision === "no") {
1357
+ return { output: "The user has not approved yet. Ask what needs to change, revise the plan, then call exit_plan again." };
1358
+ }
1359
+ opts.permissions.mode = decision;
1360
+ return { output: `Plan approved. The mode is now ${decision}. Start carrying out the plan now.` };
1361
+ }
1362
+ });
1363
+ }
1364
+
1365
+ // src/tools/skill.ts
1366
+ import { readFileSync } from "fs";
1367
+ import { z as z9 } from "zod";
1368
+ function createSkillTool(skills) {
1369
+ return defineTool({
1370
+ name: "skill",
1371
+ description: "Load a skill (instructions for a kind of task) by name. Use it when a listed skill matches the task, then follow it.",
1372
+ schema: z9.object({ name: z9.string().describe("Skill name from the list in the system prompt") }),
1373
+ kind: "read",
1374
+ target: (input) => input.name,
1375
+ async run(input) {
1376
+ const skill = skills.find((s) => s.name === input.name);
1377
+ if (!skill) return { output: `Skill "${input.name}" does not exist. Available: ${skills.map((s) => s.name).join(", ") || "(none)"}`, isError: true };
1378
+ const { body } = parseFrontmatter(readFileSync(skill.file, "utf8"));
1379
+ return { output: `Base directory for this skill: ${skill.dir}
1380
+
1381
+ ${body}`, display: skill.description.slice(0, 60) };
1382
+ }
1383
+ });
1384
+ }
1385
+
1386
+ // src/tools/task.ts
1387
+ import { z as z10 } from "zod";
1388
+ function createTaskTool(opts) {
1389
+ const byName = new Map(opts.agents.map((a) => [a.name, a]));
1390
+ const list = opts.agents.map((a) => `- ${a.name}: ${a.description}`).join("\n");
1391
+ let askChain = Promise.resolve();
1392
+ const enqueueAsk = (fn) => {
1393
+ const next = askChain.then(
1394
+ () => fn(),
1395
+ () => fn()
1396
+ );
1397
+ askChain = next.then(
1398
+ () => void 0,
1399
+ () => void 0
1400
+ );
1401
+ return next;
1402
+ };
1403
+ return defineTool({
1404
+ name: "task",
1405
+ description: `Delegate a self-contained sub-task to a subagent. It starts with an empty context and returns only its final report. Available agents:
1406
+ ${list}`,
1407
+ schema: z10.object({
1408
+ agent: z10.string().describe("Agent name"),
1409
+ description: z10.string().describe("3-6 word summary shown to the user"),
1410
+ prompt: z10.string().describe("Full instructions; the subagent sees nothing else")
1411
+ }),
1412
+ kind: "read",
1413
+ target: (input) => `[${input.agent}] ${input.description}`,
1414
+ // parallel: true opts a write-capable agent in; otherwise only all-read agents run concurrently
1415
+ // (a project file may redefine "explore").
1416
+ parallelSafe: (input) => {
1417
+ const def = byName.get(input.agent);
1418
+ if (def?.parallel) return true;
1419
+ if (!def?.tools) return false;
1420
+ const base = opts.baseTools();
1421
+ return def.tools.every((name) => base.find((t2) => t2.name === name)?.kind === "read");
1422
+ },
1423
+ async run(input, ctx) {
1424
+ const def = byName.get(input.agent);
1425
+ if (!def) return { output: `Agent "${input.agent}" does not exist. Available: ${[...byName.keys()].join(", ")}`, isError: true };
1426
+ const base = opts.baseTools().filter((t2) => t2.name !== "task" && t2.name !== "exit_plan");
1427
+ const unknown = (def.tools ?? []).filter((n) => !base.some((t2) => t2.name === n));
1428
+ if (unknown.length) return { output: `Agent "${def.name}": unknown tools: ${unknown.join(", ")}`, isError: true };
1429
+ const allowed = def.tools;
1430
+ const tools = allowed ? base.filter((t2) => allowed.includes(t2.name)) : base;
1431
+ let provider;
1432
+ try {
1433
+ provider = def.model ? opts.providerFor(def.model) : opts.provider();
1434
+ } catch (error) {
1435
+ return { output: `Agent "${def.name}": ${error.message}`, isError: true };
1436
+ }
1437
+ const child = new Agent({
1438
+ provider,
1439
+ tools,
1440
+ permissions: opts.permissions,
1441
+ systemPrompt: `${typeof opts.systemPrompt === "function" ? opts.systemPrompt(def.model) : opts.systemPrompt}
1442
+
1443
+ # Your role
1444
+ ${def.prompt}`,
1445
+ cwd: opts.cwd,
1446
+ // Every subagent runs with a step cap so a looping child cannot burn tokens forever.
1447
+ maxSteps: def.maxSteps ?? 50,
1448
+ label: def.name,
1449
+ reasoning: opts.reasoning?.() ?? "auto",
1450
+ hooks: opts.hooks,
1451
+ pluginEmit: opts.pluginEmit,
1452
+ env: opts.env,
1453
+ // Subagent edits belong to the parent's turn so /undo reverts them too.
1454
+ checkpoint: ctx.checkpoint
1455
+ });
1456
+ let outcome;
1457
+ let steps = 0;
1458
+ child.onEvent = (event) => {
1459
+ if (event.type === "toolStart") steps++;
1460
+ if (event.type === "error" || event.type === "stepLimit" || event.type === "aborted" || event.type === "done") outcome = event;
1461
+ if (event.type !== "usage" && ctx.callId) ctx.emit?.({ type: "subagent", parentId: ctx.callId, agent: def.name, event });
1462
+ };
1463
+ const ask = ctx.ask;
1464
+ if (ask) child.askPermission = (req) => enqueueAsk(() => ask(req));
1465
+ await child.run(input.prompt, ctx.signal);
1466
+ ctx.addUsage?.(child.totalUsage);
1467
+ const tokens2 = child.totalUsage.inputTokens + child.totalUsage.outputTokens;
1468
+ const display = t("{steps} steps \xB7 {tokens} tokens", { steps, tokens: tokens2 >= 1e3 ? `${Math.round(tokens2 / 1e3)}k` : tokens2 });
1469
+ if (outcome?.type === "error") return { output: `Subagent ${def.name} failed: ${outcome.message}`, isError: true, display };
1470
+ 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);
1472
+ const report = last && typeof last.content === "string" ? last.content : "(no report)";
1473
+ const suffix = outcome?.type === "stepLimit" ? `
1474
+
1475
+ (The subagent stopped at its ${outcome.maxSteps}-step limit.)` : "";
1476
+ return { output: report + suffix, display };
1477
+ }
1478
+ });
1479
+ }
1480
+
1481
+ // src/tools/todo.ts
1482
+ import { z as z11 } from "zod";
1483
+ var TodoStore = class {
1484
+ items = [];
1485
+ listeners = /* @__PURE__ */ new Set();
1486
+ set(items) {
1487
+ this.items = items;
1488
+ for (const fn of this.listeners) fn(items);
1489
+ }
1490
+ subscribe(fn) {
1491
+ this.listeners.add(fn);
1492
+ return () => this.listeners.delete(fn);
1493
+ }
1494
+ };
1495
+ function createTodoTool(store) {
1496
+ return defineTool({
1497
+ name: "todo_write",
1498
+ description: "Write the task list for multi-step work (3+ steps). Send the whole list every time; exactly one item in_progress while working. Mark items completed as soon as they are done.",
1499
+ schema: z11.object({
1500
+ todos: z11.array(z11.object({ content: z11.string(), status: z11.enum(["pending", "in_progress", "completed"]) }))
1501
+ }),
1502
+ kind: "read",
1503
+ target: (input) => `${input.todos.length} item`,
1504
+ async run(input) {
1505
+ if (input.todos.filter((t2) => t2.status === "in_progress").length > 1) {
1506
+ return { output: "Only one item may be in_progress.", isError: true };
1507
+ }
1508
+ store.set(input.todos);
1509
+ const done = input.todos.filter((t2) => t2.status === "completed").length;
1510
+ return { output: `Todo updated: ${done}/${input.todos.length} completed.` };
1511
+ }
1512
+ });
1513
+ }
1514
+
1515
+ // src/tools/memory.ts
1516
+ import { existsSync as existsSync3, readFileSync as readFileSync2 } from "fs";
1517
+ import { join } from "path";
1518
+ import { z as z12 } from "zod";
1519
+ function createSaveMemoryTool(opts) {
1520
+ return defineTool({
1521
+ name: "save_memory",
1522
+ description: 'Save a durable fact, decision, or convention to the instruction files so future sessions remember it. Scope "project" (default) appends to AGENTS.md in the project; "global" appends to your personal BCCLI.md. Use for things worth remembering across sessions, not temporary task notes.',
1523
+ schema: z12.object({
1524
+ content: z12.string().min(1).max(500).describe("One short line, without a leading bullet (it is added for you)"),
1525
+ scope: z12.enum(["project", "global"]).optional().describe("project (default) or global")
1526
+ }),
1527
+ kind: "edit",
1528
+ // The target is what the prompt shows and what the permission model judges: the
1529
+ // 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",
1531
+ async run(input, ctx) {
1532
+ const content = input.content.trim();
1533
+ 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");
1535
+ if (existsSync3(file)) {
1536
+ const lines = readFileSync2(file, "utf8").split("\n");
1537
+ if (lines.some((line) => line.trim() === `- ${content}`)) {
1538
+ return { output: `Already saved to ${file}.` };
1539
+ }
1540
+ }
1541
+ appendMemory(file, content);
1542
+ opts.onSaved?.();
1543
+ return { output: `Saved to ${file}.` };
1544
+ }
1545
+ });
1546
+ }
1547
+
1548
+ // src/tools/repomap.ts
1549
+ import { z as z13 } from "zod";
1550
+
1551
+ // src/repomap.ts
1552
+ import { readdirSync, readFileSync as readFileSync3 } from "fs";
1553
+ import { extname, join as join2, relative, sep } from "path";
1554
+ var SKIP_DIRS = /* @__PURE__ */ new Set([
1555
+ "node_modules",
1556
+ ".git",
1557
+ "dist",
1558
+ "build",
1559
+ "coverage",
1560
+ ".next",
1561
+ "target",
1562
+ "vendor",
1563
+ "__pycache__",
1564
+ ".venv",
1565
+ ".cache"
1566
+ ]);
1567
+ var CODE_EXTS = /* @__PURE__ */ new Set([
1568
+ ".ts",
1569
+ ".tsx",
1570
+ ".js",
1571
+ ".jsx",
1572
+ ".mjs",
1573
+ ".cjs",
1574
+ ".py",
1575
+ ".go",
1576
+ ".rs",
1577
+ ".java",
1578
+ ".c",
1579
+ ".h",
1580
+ ".cpp",
1581
+ ".hpp",
1582
+ ".cs",
1583
+ ".rb",
1584
+ ".php",
1585
+ ".sh",
1586
+ ".ps1"
1587
+ ]);
1588
+ var MAX_FILES = 400;
1589
+ var MAX_FILE_BYTES = 2e5;
1590
+ var MAX_SYMBOLS_PER_FILE = 12;
1591
+ var FAMILY = {
1592
+ ts: [
1593
+ /^\s*(?:export\s+)?(?:default\s+)?(?:declare\s+)?(?:abstract\s+)?(?:async\s+)?(?:function|class|interface|type|enum)\s+([A-Za-z0-9_$]+)/,
1594
+ /^\s*export\s+(?:const|let)\s+([A-Za-z0-9_$]+)/
1595
+ ],
1596
+ py: [/^\s*(?:async\s+)?def\s+(\w+)/, /^\s*class\s+(\w+)/],
1597
+ go: [/^func\s+(?:\([^)]*\)\s*)?(\w+)/, /^type\s+(\w+)/],
1598
+ rs: [/^\s*(?:pub(?:\([^)]*\))?\s+)?(?:unsafe\s+)?(?:async\s+)?(?:fn|struct|enum|trait|type)\s+(\w+)/],
1599
+ c: [/^\s*(?:static\s+|inline\s+|extern\s+)*[A-Za-z_][A-Za-z0-9_\s*]*\b(\w+)\s*\([^;]*\)\s*\{?\s*$/]
1600
+ };
1601
+ 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;
1604
+ if (ext === ".go") return FAMILY.go;
1605
+ if (ext === ".rs") return FAMILY.rs;
1606
+ return FAMILY.c;
1607
+ }
1608
+ function extractSymbols(text, ext) {
1609
+ const out = [];
1610
+ for (const line of text.split("\n")) {
1611
+ for (const re of familyFor(ext)) {
1612
+ const m = re.exec(line);
1613
+ if (m) {
1614
+ if (!out.includes(m[1])) out.push(m[1]);
1615
+ break;
1616
+ }
1617
+ }
1618
+ if (out.length >= MAX_SYMBOLS_PER_FILE) break;
1619
+ }
1620
+ return out;
1621
+ }
1622
+ function walk(dir, root, files) {
1623
+ if (files.length >= MAX_FILES) return;
1624
+ let entries;
1625
+ try {
1626
+ entries = readdirSync(dir, { withFileTypes: true });
1627
+ } catch {
1628
+ return;
1629
+ }
1630
+ 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);
1633
+ if (e.isDirectory()) {
1634
+ if (SKIP_DIRS.has(e.name) || e.name.startsWith(".") && full !== root) continue;
1635
+ walk(full, root, files);
1636
+ } else if (e.isFile() && CODE_EXTS.has(extname(e.name).toLowerCase())) {
1637
+ files.push(full);
1638
+ }
1639
+ }
1640
+ }
1641
+ function buildRepoMapSync(cwd, opts = {}) {
1642
+ const maxTokens = opts.maxTokens ?? 2048;
1643
+ const files = [];
1644
+ walk(cwd, cwd, files);
1645
+ files.sort(
1646
+ (a, b) => relative(cwd, a).split(sep).length - relative(cwd, b).split(sep).length || a.localeCompare(b)
1647
+ );
1648
+ const lines = [];
1649
+ let used = 0;
1650
+ let truncated = false;
1651
+ for (const file of files) {
1652
+ const rel = relative(cwd, file).split(sep).join("/");
1653
+ let symbols = [];
1654
+ try {
1655
+ const text = readFileSync3(file, "utf8");
1656
+ if (text.length <= MAX_FILE_BYTES) symbols = extractSymbols(text, extname(file).toLowerCase());
1657
+ } catch {
1658
+ symbols = [];
1659
+ }
1660
+ const line = symbols.length ? `${rel}: ${symbols.join(", ")}` : rel;
1661
+ const cost = estimateTokens(line);
1662
+ if (used + cost > maxTokens) {
1663
+ truncated = true;
1664
+ break;
1665
+ }
1666
+ used += cost;
1667
+ lines.push(line);
1668
+ }
1669
+ if (truncated) lines.push(`\u2026 truncated (repo map limited to ${maxTokens} tokens)`);
1670
+ return lines.join("\n");
1671
+ }
1672
+
1673
+ // src/tools/repomap.ts
1674
+ function createRepoMapTool() {
1675
+ return defineTool({
1676
+ name: "repo_map",
1677
+ description: "Show a map of this repository: code files (node_modules/.git excluded) with their top-level functions, classes and types. Use it to orient yourself in an unfamiliar repo before searching or reading files.",
1678
+ schema: z13.object({}),
1679
+ kind: "read",
1680
+ target: () => "(repo)",
1681
+ programmaticSafe: true,
1682
+ run: async (_input, ctx) => ({ output: buildRepoMapSync(ctx.cwd) || "(no code files found)" })
1683
+ });
1684
+ }
1685
+
1686
+ // src/tools/diagnostics.ts
1687
+ import { extname as extname2 } from "path";
1688
+ import { z as z14 } from "zod";
1689
+
1690
+ // src/lsp.ts
1691
+ import { spawn } from "child_process";
1692
+ import { readFileSync as readFileSync4 } from "fs";
1693
+ import { pathToFileURL } from "url";
1694
+ function encodeLsp(msg) {
1695
+ const body = Buffer.from(JSON.stringify(msg), "utf8");
1696
+ return Buffer.concat([Buffer.from(`Content-Length: ${body.length}\r
1697
+ \r
1698
+ `, "ascii"), body]);
1699
+ }
1700
+ function createLspParser(onMessage) {
1701
+ let buf = Buffer.alloc(0);
1702
+ return {
1703
+ push(chunk) {
1704
+ buf = Buffer.concat([buf, chunk]);
1705
+ for (; ; ) {
1706
+ const idx = buf.indexOf("\r\n\r\n");
1707
+ if (idx < 0) return;
1708
+ const m = /Content-Length:\s*(\d+)/i.exec(buf.subarray(0, idx).toString("ascii"));
1709
+ if (!m) {
1710
+ buf = buf.subarray(idx + 4);
1711
+ continue;
1712
+ }
1713
+ const len = Number(m[1]);
1714
+ if (buf.length < idx + 4 + len) return;
1715
+ const body = buf.subarray(idx + 4, idx + 4 + len);
1716
+ buf = buf.subarray(idx + 4 + len);
1717
+ try {
1718
+ onMessage(JSON.parse(body.toString("utf8")));
1719
+ } catch {
1720
+ }
1721
+ }
1722
+ }
1723
+ };
1724
+ }
1725
+ 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"] });
1728
+ const uri = pathToFileURL(opts.file).href;
1729
+ let settled = false;
1730
+ let nextId = 2;
1731
+ const send = (msg) => {
1732
+ try {
1733
+ child.stdin.write(encodeLsp(msg));
1734
+ } catch {
1735
+ }
1736
+ };
1737
+ const finish = (diags) => {
1738
+ if (settled) return;
1739
+ settled = true;
1740
+ clearTimeout(timer);
1741
+ send({ jsonrpc: "2.0", id: nextId++, method: "shutdown" });
1742
+ send({ jsonrpc: "2.0", method: "exit" });
1743
+ const killer = setTimeout(() => {
1744
+ try {
1745
+ child.kill();
1746
+ } catch {
1747
+ }
1748
+ }, 500);
1749
+ killer.unref?.();
1750
+ resolve2(diags);
1751
+ };
1752
+ const timer = setTimeout(() => finish([]), opts.timeoutMs ?? 8e3);
1753
+ const parser = createLspParser((msg) => {
1754
+ if (msg.id === 1 && msg.result) {
1755
+ send({ jsonrpc: "2.0", method: "initialized", params: {} });
1756
+ let text = "";
1757
+ try {
1758
+ text = readFileSync4(opts.file, "utf8");
1759
+ } catch {
1760
+ }
1761
+ send({
1762
+ jsonrpc: "2.0",
1763
+ method: "textDocument/didOpen",
1764
+ params: { textDocument: { uri, languageId: "plaintext", version: 1, text } }
1765
+ });
1766
+ }
1767
+ if (msg.method === "textDocument/publishDiagnostics" && msg.params?.uri === uri) {
1768
+ finish(
1769
+ (msg.params.diagnostics ?? []).map((d) => ({
1770
+ line: d.range?.start?.line ?? 0,
1771
+ character: d.range?.start?.character ?? 0,
1772
+ severity: d.severity ?? 1,
1773
+ message: String(d.message ?? "")
1774
+ }))
1775
+ );
1776
+ }
1777
+ });
1778
+ child.stdout?.on("data", (c) => parser.push(c));
1779
+ child.on("error", () => finish([]));
1780
+ child.on("close", () => finish([]));
1781
+ send({
1782
+ jsonrpc: "2.0",
1783
+ id: 1,
1784
+ method: "initialize",
1785
+ params: { processId: process.pid, capabilities: {}, rootUri: pathToFileURL(opts.cwd).href }
1786
+ });
1787
+ });
1788
+ }
1789
+
1790
+ // src/tools/diagnostics.ts
1791
+ var SEVERITY = { 1: "error", 2: "warning", 3: "info", 4: "hint" };
1792
+ var SCHEMA = z14.object({ path: z14.string().describe("File to check (absolute or relative to the working directory)") });
1793
+ function createDiagnosticsTool(servers) {
1794
+ return defineTool({
1795
+ name: "diagnostics",
1796
+ description: 'Run the configured LSP server(s) on one file and return its diagnostics (errors/warnings). Configure servers under "lsp.servers" in ~/.bccli/config.json, e.g. { "extensions": [".ts"], "command": "typescript-language-server", "args": ["--stdio"] }.',
1797
+ schema: SCHEMA,
1798
+ kind: "read",
1799
+ target: (input) => input.path,
1800
+ async run(input, ctx) {
1801
+ const file = resolvePath(ctx.cwd, input.path);
1802
+ const ext = extname2(file).toLowerCase();
1803
+ const server = servers.find((s) => s.extensions.includes(ext));
1804
+ if (!server) {
1805
+ return { output: t("No LSP server is configured for {ext} files.", { ext: ext || "(none)" }), isError: true };
1806
+ }
1807
+ const diags = await getDiagnostics({ server, cwd: ctx.cwd, file, timeoutMs: 15e3 });
1808
+ if (!diags.length) return { output: "No diagnostics." };
1809
+ const lines = diags.map((d) => `${input.path}:${d.line + 1}:${d.character + 1} ${SEVERITY[d.severity] ?? "error"} ${d.message}`);
1810
+ return { output: lines.join("\n"), isError: diags.some((d) => d.severity === 1) };
1811
+ }
1812
+ });
1813
+ }
1814
+
1815
+ // src/plugins.ts
1816
+ import { existsSync as existsSync4 } from "fs";
1817
+ import { createRequire } from "module";
1818
+ import { isAbsolute as isAbsolute2, join as join3 } from "path";
1819
+ import { z as z15 } from "zod";
1820
+ var require_ = createRequire(import.meta.url);
1821
+ function pluginLog(message) {
1822
+ process.stderr.write(`[plugin] ${message}
1823
+ `);
1824
+ }
1825
+ function normalizeTool(def) {
1826
+ return {
1827
+ name: def.name,
1828
+ description: def.description,
1829
+ // Raw-args passthrough: a plugin without its own zod still receives what the model sent.
1830
+ schema: def.schema ?? z15.record(z15.string(), z15.unknown()),
1831
+ kind: def.kind ?? "read",
1832
+ target: def.target ?? (() => def.name),
1833
+ run: def.run
1834
+ };
1835
+ }
1836
+ function loadPlugins(specs, opts) {
1837
+ const tools = [];
1838
+ const taken = new Set(opts.builtinNames);
1839
+ const listeners = /* @__PURE__ */ new Map();
1840
+ for (const spec of specs) {
1841
+ let file;
1842
+ try {
1843
+ file = isAbsolute2(spec) ? spec : spec.startsWith(".") ? join3(opts.home, spec) : require_.resolve(spec);
1844
+ } catch {
1845
+ throw new ConfigError(t("Plugin not found: {spec}", { spec }));
1846
+ }
1847
+ if (!existsSync4(file)) throw new ConfigError(t("Plugin not found: {spec}", { spec }));
1848
+ let mod;
1849
+ try {
1850
+ mod = require_(file);
1851
+ } catch (error) {
1852
+ throw new ConfigError(t("Plugin failed to load: {spec}: {reason}", { spec, reason: error.message }));
1853
+ }
1854
+ const asRecord = mod ?? {};
1855
+ const activate = typeof mod === "function" ? mod : typeof asRecord.default === "function" ? asRecord.default : typeof asRecord.activate === "function" ? asRecord.activate : void 0;
1856
+ if (!activate) {
1857
+ throw new ConfigError(
1858
+ t("Plugin failed to load: {spec}: {reason}", {
1859
+ spec,
1860
+ reason: "no activate function (use export default fn, export function activate, or module.exports = fn)"
1861
+ })
1862
+ );
1863
+ }
1864
+ const ctx = {
1865
+ cwd: opts.cwd,
1866
+ env: opts.env,
1867
+ config: opts.config,
1868
+ registerTool(tool) {
1869
+ if (taken.has(tool.name)) {
1870
+ throw new ConfigError(
1871
+ t("Plugin {spec} registered a tool name that is already in use: {name}", { spec, name: tool.name })
1872
+ );
1873
+ }
1874
+ taken.add(tool.name);
1875
+ tools.push(normalizeTool(tool));
1876
+ },
1877
+ on(event, handler) {
1878
+ const list = listeners.get(event) ?? [];
1879
+ list.push(handler);
1880
+ listeners.set(event, list);
1881
+ },
1882
+ log: pluginLog
1883
+ };
1884
+ try {
1885
+ const result = activate(ctx);
1886
+ if (result && typeof result.catch === "function") {
1887
+ ;
1888
+ result.catch((error) => pluginLog(`activation error in ${spec}: ${String(error)}`));
1889
+ }
1890
+ } catch (error) {
1891
+ throw new ConfigError(t("Plugin failed to load: {spec}: {reason}", { spec, reason: error.message }));
1892
+ }
1893
+ }
1894
+ return {
1895
+ tools,
1896
+ async emit(event, payload = {}) {
1897
+ for (const handler of listeners.get(event) ?? []) {
1898
+ try {
1899
+ await handler(payload);
1900
+ } catch (error) {
1901
+ pluginLog(`handler for ${event} failed: ${String(error)}`);
1902
+ }
1903
+ }
1904
+ }
1905
+ };
1906
+ }
1907
+
1908
+ // src/setup.ts
1909
+ var TOOL_RULES = { bash: "bash", edit: "edit", write: "edit", fetch: "fetch" };
1910
+ function lunaPtcAutoEnabled(env) {
1911
+ const value = String(env.BCCLI_LUNA_PTC ?? "").trim().toLowerCase();
1912
+ return !["0", "false", "off", "no"].includes(value);
1913
+ }
1914
+ function createRuntime(opts) {
1915
+ const env = opts.env ?? process.env;
1916
+ const home = bccliHome(env);
1917
+ let config = loadConfig(opts.cwd, env);
1918
+ let modelRef = opts.args.model ?? config.model;
1919
+ let reasoning = opts.args.reasoning ?? config.reasoning;
1920
+ const makeProvider = (ref) => {
1921
+ const resolved = resolveModel(config, ref, env);
1922
+ return opts.provider ?? createProvider({
1923
+ baseURL: resolved.baseURL,
1924
+ apiKey: resolved.apiKey,
1925
+ model: resolved.model,
1926
+ providerId: resolved.providerId,
1927
+ fetch: opts.fetch,
1928
+ // diteruskan penuh (spec gh-bot §6.6 — juga memperbaiki sdk fetch-injection)
1929
+ enableProgrammaticToolCalling: lunaPtcAutoEnabled(env)
1930
+ });
1931
+ };
1932
+ const provider = makeProvider(modelRef);
1933
+ const mode = opts.args.allowAll ? "allowAll" : opts.args.permissionMode ?? config.permissionMode;
1934
+ const rules = [...config.allow, ...opts.args.allowedTools.map((t2) => TOOL_RULES[t2] ?? t2)];
1935
+ const permissions = new Permissions(mode, rules, opts.cwd);
1936
+ const roots = { cwd: opts.cwd, home, userHome: opts.userHome ?? homedir() };
1937
+ const skills = loadSkills(roots);
1938
+ const commands = loadCommands(roots);
1939
+ const agentDefs = loadAgentDefs(roots);
1940
+ const todos = new TodoStore();
1941
+ const checkpoints = new CheckpointStore();
1942
+ const turnMarks = [];
1943
+ const checkpointMarks = [];
1944
+ let startedAt = /* @__PURE__ */ new Date();
1945
+ const interaction = { approvePlan: async () => "no" };
1946
+ let session = Session.create(home, opts.cwd);
1947
+ let history = opts.history;
1948
+ if (!history && opts.args.continue) {
1949
+ const latest = Session.latest(home, opts.cwd);
1950
+ if (latest) {
1951
+ session = latest;
1952
+ history = latest.load();
1953
+ }
1954
+ }
1955
+ const repoMap = config.repoMap ? buildRepoMapSync(opts.cwd) : void 0;
1956
+ const systemFor = (model) => buildSystemPrompt({ cwd: opts.cwd, home, model, skills, repoMap });
1957
+ const systemPrompt = systemFor(modelRef);
1958
+ const webSearch = createWebSearchTool({
1959
+ botconnector: () => {
1960
+ try {
1961
+ const bc = resolveModel(config, "bc-cloud/search", env);
1962
+ return bc.apiKey ? { baseURL: bc.baseURL, apiKey: bc.apiKey } : void 0;
1963
+ } catch {
1964
+ return void 0;
1965
+ }
1966
+ }
1967
+ });
1968
+ const rebuildPrompt = () => agent.setSystemPrompt(systemFor(modelRef));
1969
+ const saveMemory = createSaveMemoryTool({ home, onSaved: rebuildPrompt });
1970
+ const baseTools = [
1971
+ createReadTool({ vision: config.vision }),
1972
+ createBashTool({ networkPolicy: config.networkPolicy, sandbox: config.sandbox === "on" }),
1973
+ ...ALL_TOOLS.filter((tool) => tool.name !== "read" && tool.name !== "bash"),
1974
+ webSearch,
1975
+ createSkillTool(skills),
1976
+ createTodoTool(todos),
1977
+ createExitPlanTool({ permissions, interaction }),
1978
+ saveMemory,
1979
+ createRepoMapTool()
1980
+ ];
1981
+ if (config.lsp?.servers.length) baseTools.push(createDiagnosticsTool(config.lsp.servers));
1982
+ const plugins = loadPlugins(config.plugins ?? [], {
1983
+ home,
1984
+ cwd: opts.cwd,
1985
+ env,
1986
+ config,
1987
+ builtinNames: [...baseTools.map((tool) => tool.name), "task"]
1988
+ });
1989
+ baseTools.push(...plugins.tools);
1990
+ const pluginEmit = (event, payload) => plugins.emit(event, payload);
1991
+ const task = createTaskTool({
1992
+ agents: [...BUILTIN_AGENTS, ...agentDefs],
1993
+ // Subagents get whatever the main agent has right now (incl. MCP tools), minus task itself.
1994
+ baseTools: () => agent.tools.filter((t2) => t2.name !== "task"),
1995
+ permissions,
1996
+ provider: () => agent.provider,
1997
+ providerFor: makeProvider,
1998
+ systemPrompt: (childModelRef) => systemFor(childModelRef ?? modelRef),
1999
+ cwd: opts.cwd,
2000
+ reasoning: () => reasoning,
2001
+ hooks: config.hooks,
2002
+ pluginEmit,
2003
+ env
2004
+ });
2005
+ const agent = new Agent({
2006
+ provider,
2007
+ tools: [...baseTools, task],
2008
+ permissions,
2009
+ systemPrompt,
2010
+ cwd: opts.cwd,
2011
+ history,
2012
+ reasoning,
2013
+ maxSteps: opts.maxSteps ?? config.maxSteps,
2014
+ hooks: config.hooks,
2015
+ pluginEmit,
2016
+ env,
2017
+ usageCap: config.usageCap,
2018
+ budgetModel: modelRef,
2019
+ contextWindow: contextWindowFor(modelRef, env),
2020
+ onMessage: (m) => session.append(m),
2021
+ onUsage: (u) => session.appendUsage({ ...u, model: modelRef }),
2022
+ onReset: () => session.reset(),
2023
+ onTurnStart: () => {
2024
+ for (let i = turnMarks.length - 1; i >= 0 && turnMarks[i] > agent.messages.length; i--) {
2025
+ turnMarks.splice(i, 1);
2026
+ checkpointMarks.splice(i, 1);
2027
+ }
2028
+ turnMarks.push(agent.messages.length);
2029
+ checkpointMarks.push(checkpoints.entries());
2030
+ checkpoints.beginTurn();
2031
+ },
2032
+ checkpoint: (path) => checkpoints.snapshot(path)
2033
+ });
2034
+ const mcp = new McpManager({
2035
+ home,
2036
+ onChange: () => agent.setTools([...baseTools, task, ...mcp.tools()]),
2037
+ // A later server may reuse a sanitized name; it must not inherit this one's "[a]" grants.
2038
+ onToolsRemoved: (names) => {
2039
+ for (const name of names) permissions.revoke(`mcp(${name})`);
2040
+ }
2041
+ });
2042
+ return {
2043
+ cwd: opts.cwd,
2044
+ home,
2045
+ env,
2046
+ skills,
2047
+ commands,
2048
+ agentDefs,
2049
+ todos,
2050
+ interaction,
2051
+ mcp,
2052
+ startMcp: (specs) => mcp.start(specs),
2053
+ get config() {
2054
+ return config;
2055
+ },
2056
+ reloadConfig() {
2057
+ config = loadConfig(opts.cwd, env);
2058
+ },
2059
+ providerLabel() {
2060
+ return providerName(config, modelRef.slice(0, modelRef.indexOf("/")));
2061
+ },
2062
+ listModels: (only) => listAllModels(config, env, { fetch: opts.fetch, only }),
2063
+ addProviderKey(id, key) {
2064
+ return addProviderKey(id, key, { env, cwd: opts.cwd, out: () => {
2065
+ }, err: () => {
2066
+ }, readSecret: async () => "", fetch: opts.fetch });
2067
+ },
2068
+ get modelRef() {
2069
+ return modelRef;
2070
+ },
2071
+ get reasoning() {
2072
+ return reasoning;
2073
+ },
2074
+ agent,
2075
+ get session() {
2076
+ return session;
2077
+ },
2078
+ resume(previous) {
2079
+ session = previous;
2080
+ agent.load(previous.load());
2081
+ agent.totalUsage = { inputTokens: 0, outputTokens: 0 };
2082
+ todos.set([]);
2083
+ checkpoints.clear();
2084
+ turnMarks.length = 0;
2085
+ checkpointMarks.length = 0;
2086
+ },
2087
+ newSession() {
2088
+ session = Session.create(home, opts.cwd);
2089
+ agent.load([]);
2090
+ agent.totalUsage = { inputTokens: 0, outputTokens: 0 };
2091
+ todos.set([]);
2092
+ checkpoints.clear();
2093
+ turnMarks.length = 0;
2094
+ checkpointMarks.length = 0;
2095
+ startedAt = /* @__PURE__ */ new Date();
2096
+ },
2097
+ rebuildSystemPrompt() {
2098
+ rebuildPrompt();
2099
+ },
2100
+ emitPlugins: (event, payload) => plugins.emit(event, payload),
2101
+ checkpoints,
2102
+ async rewindTurns(n) {
2103
+ if (turnMarks.length === 0) return void 0;
2104
+ const count = Math.min(Math.max(1, Math.floor(n) || 1), turnMarks.length);
2105
+ const from = turnMarks.length - count;
2106
+ const messageTarget = turnMarks[from];
2107
+ const checkpointTarget = checkpointMarks[from];
2108
+ turnMarks.splice(from);
2109
+ checkpointMarks.splice(from);
2110
+ const files = await checkpoints.undoTo(checkpointTarget);
2111
+ const kept = agent.messages.slice(0, Math.min(messageTarget, agent.messages.length));
2112
+ agent.load(kept);
2113
+ session.reset();
2114
+ for (const message of kept) session.append(message);
2115
+ return {
2116
+ turns: count,
2117
+ restored: files?.restored ?? [],
2118
+ deleted: files?.deleted ?? [],
2119
+ skipped: files?.skipped ?? [],
2120
+ failed: files?.failed ?? []
2121
+ };
2122
+ },
2123
+ get startedAt() {
2124
+ return startedAt;
2125
+ },
2126
+ setModel(ref) {
2127
+ const nextProvider = makeProvider(ref);
2128
+ modelRef = ref;
2129
+ agent.provider = nextProvider;
2130
+ agent.setContextWindow(contextWindowFor(ref, env));
2131
+ agent.setSystemPrompt(systemFor(modelRef));
2132
+ },
2133
+ setReasoning(level) {
2134
+ reasoning = level;
2135
+ agent.reasoning = level;
2136
+ }
2137
+ };
2138
+ }
2139
+
2140
+ export {
2141
+ lunaPtcAutoEnabled,
2142
+ createRuntime
2143
+ };