@botconnector/bccli 0.4.16 → 0.5.0-rc.4

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 (185) hide show
  1. package/LICENSE +203 -21
  2. package/THIRD_PARTY_NOTICES.md +9 -0
  3. package/branding.mjs +347 -0
  4. package/cli.mjs +120 -0
  5. package/core.mjs +260 -0
  6. package/credentials.mjs +52 -0
  7. package/gateway.mjs +2 -0
  8. package/model-command.mjs +61 -0
  9. package/package.json +35 -59
  10. package/provider-cli.mjs +77 -0
  11. package/providers.mjs +166 -0
  12. package/proxy.mjs +242 -0
  13. package/startup-menu.mjs +120 -0
  14. package/README.en.md +0 -253
  15. package/README.md +0 -250
  16. package/dist/abort.d.ts +0 -5
  17. package/dist/acp-JZW3XHE5.js +0 -221
  18. package/dist/acp.d.ts +0 -44
  19. package/dist/agent.d.ts +0 -147
  20. package/dist/agents.d.ts +0 -4
  21. package/dist/args-XVO7TB4C.js +0 -13
  22. package/dist/args.d.ts +0 -59
  23. package/dist/budget.d.ts +0 -26
  24. package/dist/checkpoints.d.ts +0 -35
  25. package/dist/chunk-3CYQYHPK.js +0 -202
  26. package/dist/chunk-6JK4IBGK.js +0 -1075
  27. package/dist/chunk-6SDRWEPN.js +0 -50
  28. package/dist/chunk-7GM6NN5E.js +0 -341
  29. package/dist/chunk-7XD47KN5.js +0 -71
  30. package/dist/chunk-INMSTUB2.js +0 -56
  31. package/dist/chunk-ISOKH5WB.js +0 -256
  32. package/dist/chunk-JUC3ZOCV.js +0 -50
  33. package/dist/chunk-KDVL2NP6.js +0 -861
  34. package/dist/chunk-KEKDRROL.js +0 -216
  35. package/dist/chunk-KJIXA6MP.js +0 -159
  36. package/dist/chunk-OK2USAXX.js +0 -88
  37. package/dist/chunk-PSO7R5AD.js +0 -10
  38. package/dist/chunk-Q42ZGVAQ.js +0 -91
  39. package/dist/chunk-R754XIBJ.js +0 -523
  40. package/dist/chunk-S3QEZDKB.js +0 -270
  41. package/dist/chunk-SWNRA3EW.js +0 -78
  42. package/dist/chunk-TLCL6DC7.js +0 -378
  43. package/dist/chunk-V5D6PJQN.js +0 -8
  44. package/dist/chunk-YYTFJUO2.js +0 -148
  45. package/dist/chunk-ZJRZTMJF.js +0 -2669
  46. package/dist/cli.d.ts +0 -1
  47. package/dist/cli.js +0 -233
  48. package/dist/clipboardImage.d.ts +0 -17
  49. package/dist/commands.d.ts +0 -16
  50. package/dist/completion-Q7VHOPPV.js +0 -125
  51. package/dist/completion.d.ts +0 -14
  52. package/dist/config-T7MKGJVG.js +0 -34
  53. package/dist/config-YYEAEN2C.js +0 -25
  54. package/dist/config.d.ts +0 -99
  55. package/dist/context.d.ts +0 -13
  56. package/dist/contextWindow.d.ts +0 -3
  57. package/dist/diff.d.ts +0 -7
  58. package/dist/exit.d.ts +0 -9
  59. package/dist/extensions.d.ts +0 -34
  60. package/dist/ghBotCli-7VUGI63S.js +0 -680
  61. package/dist/ghBotCli.d.ts +0 -9
  62. package/dist/ghBotDispatch.d.ts +0 -68
  63. package/dist/ghBotRunner.d.ts +0 -82
  64. package/dist/ghBotServe.d.ts +0 -21
  65. package/dist/ghBotWebhook.d.ts +0 -45
  66. package/dist/hooks.d.ts +0 -27
  67. package/dist/i18n/id.d.ts +0 -1
  68. package/dist/i18n.d.ts +0 -16
  69. package/dist/integrationCli-FBRSTF5F.js +0 -876
  70. package/dist/integrationCli.d.ts +0 -9
  71. package/dist/keybinds.d.ts +0 -22
  72. package/dist/login-BSMUGW7L.js +0 -18
  73. package/dist/login.d.ts +0 -21
  74. package/dist/lsp.d.ts +0 -73
  75. package/dist/mcp/catalog.d.ts +0 -14
  76. package/dist/mcp/config.d.ts +0 -36
  77. package/dist/mcp/manager.d.ts +0 -52
  78. package/dist/mcp/oauth.d.ts +0 -82
  79. package/dist/mcpCli-ZT7CRYMP.js +0 -113
  80. package/dist/mcpCli.d.ts +0 -3
  81. package/dist/mcpServer/serve.d.ts +0 -8
  82. package/dist/mcpServer/server.d.ts +0 -19
  83. package/dist/models.d.ts +0 -13
  84. package/dist/modelsCli-2ZRW4CC4.js +0 -36
  85. package/dist/modelsCli.d.ts +0 -15
  86. package/dist/npmRunner-LU43GM3W.js +0 -46
  87. package/dist/npmRunner.d.ts +0 -21
  88. package/dist/oauthLogin-THSM6FE7.js +0 -210
  89. package/dist/oauthLogin.d.ts +0 -47
  90. package/dist/permissions.d.ts +0 -30
  91. package/dist/pluginSpec.d.ts +0 -25
  92. package/dist/plugins.d.ts +0 -45
  93. package/dist/pluginsCli-EKQI4CJW.js +0 -288
  94. package/dist/pluginsCli.d.ts +0 -16
  95. package/dist/prCli-FMQTOBIV.js +0 -94
  96. package/dist/prCli.d.ts +0 -46
  97. package/dist/presets.d.ts +0 -8
  98. package/dist/print.d.ts +0 -17
  99. package/dist/provider.d.ts +0 -145
  100. package/dist/providerCli-3TFEKV4C.js +0 -15
  101. package/dist/providerCli.d.ts +0 -17
  102. package/dist/providers.d.ts +0 -6
  103. package/dist/reasoning.d.ts +0 -12
  104. package/dist/repomap.d.ts +0 -10
  105. package/dist/sdk.d.ts +0 -61
  106. package/dist/sdk.js +0 -6494
  107. package/dist/serve-3BODPEPR.js +0 -532
  108. package/dist/serve-V6YVZTNR.js +0 -178
  109. package/dist/serve.d.ts +0 -37
  110. package/dist/serveHeartbeat.d.ts +0 -14
  111. package/dist/serveProtocol.d.ts +0 -47
  112. package/dist/session-LX4UNPCD.js +0 -12
  113. package/dist/session.d.ts +0 -33
  114. package/dist/setup-MFG3BWEU.js +0 -24
  115. package/dist/setup.d.ts +0 -79
  116. package/dist/slash/copy.d.ts +0 -1
  117. package/dist/slash/diff.d.ts +0 -1
  118. package/dist/slash/doctor.d.ts +0 -18
  119. package/dist/slash/export.d.ts +0 -2
  120. package/dist/slash/info.d.ts +0 -46
  121. package/dist/slash/memory.d.ts +0 -10
  122. package/dist/statsCli-7MIQHLVW.js +0 -353
  123. package/dist/statsCli.d.ts +0 -62
  124. package/dist/textToolCalls.d.ts +0 -6
  125. package/dist/thinking.d.ts +0 -17
  126. package/dist/tokenBudget.d.ts +0 -22
  127. package/dist/tools/bash.d.ts +0 -54
  128. package/dist/tools/diagnostics.d.ts +0 -13
  129. package/dist/tools/edit.d.ts +0 -12
  130. package/dist/tools/fetch.d.ts +0 -41
  131. package/dist/tools/glob.d.ts +0 -5
  132. package/dist/tools/globSafety.d.ts +0 -7
  133. package/dist/tools/grep.d.ts +0 -25
  134. package/dist/tools/ignore.d.ts +0 -2
  135. package/dist/tools/index.d.ts +0 -12
  136. package/dist/tools/memory.d.ts +0 -13
  137. package/dist/tools/paths.d.ts +0 -4
  138. package/dist/tools/plan.d.ts +0 -10
  139. package/dist/tools/read.d.ts +0 -16
  140. package/dist/tools/repomap.d.ts +0 -2
  141. package/dist/tools/sandbox.d.ts +0 -15
  142. package/dist/tools/shell.d.ts +0 -18
  143. package/dist/tools/skill.d.ts +0 -3
  144. package/dist/tools/task.d.ts +0 -28
  145. package/dist/tools/todo.d.ts +0 -13
  146. package/dist/tools/types.d.ts +0 -52
  147. package/dist/tools/websearch.d.ts +0 -9
  148. package/dist/tools/write.d.ts +0 -5
  149. package/dist/transferCli-SRSYQLSJ.js +0 -73
  150. package/dist/transferCli.d.ts +0 -23
  151. package/dist/ui/App.d.ts +0 -7
  152. package/dist/ui/DiffView.d.ts +0 -3
  153. package/dist/ui/EditableText.d.ts +0 -7
  154. package/dist/ui/LinePrompt.d.ts +0 -6
  155. package/dist/ui/ListPicker.d.ts +0 -11
  156. package/dist/ui/Markdown.d.ts +0 -8
  157. package/dist/ui/McpMenu.d.ts +0 -11
  158. package/dist/ui/ModelPicker.d.ts +0 -6
  159. package/dist/ui/PermissionPrompt.d.ts +0 -5
  160. package/dist/ui/PlanApproval.d.ts +0 -5
  161. package/dist/ui/PromptInput.d.ts +0 -20
  162. package/dist/ui/ProviderMenu.d.ts +0 -10
  163. package/dist/ui/Spinner.d.ts +0 -4
  164. package/dist/ui/StatusBar.d.ts +0 -9
  165. package/dist/ui/TodoList.d.ts +0 -4
  166. package/dist/ui/ToolBlock.d.ts +0 -14
  167. package/dist/ui/complete.d.ts +0 -8
  168. package/dist/ui/index.d.ts +0 -7
  169. package/dist/ui/lineEdit.d.ts +0 -42
  170. package/dist/ui/liveCap.d.ts +0 -9
  171. package/dist/ui/replay.d.ts +0 -4
  172. package/dist/ui/stdinFilter.d.ts +0 -18
  173. package/dist/ui/table.d.ts +0 -20
  174. package/dist/ui/theme.d.ts +0 -5
  175. package/dist/ui/thinkingMouse.d.ts +0 -38
  176. package/dist/ui/transcript.d.ts +0 -55
  177. package/dist/ui/tty.d.ts +0 -11
  178. package/dist/ui/vim.d.ts +0 -26
  179. package/dist/ui-LZ4JGF2V.js +0 -2913
  180. package/dist/update-P7MU4ZM7.js +0 -145
  181. package/dist/update.d.ts +0 -30
  182. package/dist/verify.d.ts +0 -16
  183. package/dist/version.d.ts +0 -1
  184. package/dist/webBudget.d.ts +0 -25
  185. package/dist/worktree.d.ts +0 -12
@@ -1,2669 +0,0 @@
1
- #!/usr/bin/env node
2
- import {
3
- estimateMessages,
4
- estimateTokens,
5
- pruneOldToolOutputs,
6
- splitKeepRecent,
7
- trimToFit
8
- } from "./chunk-OK2USAXX.js";
9
- import {
10
- BUILTIN_AGENTS,
11
- appendMemory,
12
- bashTool,
13
- buildSystemPrompt,
14
- createBashTool,
15
- errorCode,
16
- ignorePatterns,
17
- isFileModifiedSinceRead,
18
- loadAgentDefs,
19
- loadCommands,
20
- loadSkills,
21
- normalizeGlobPath,
22
- parseFrontmatter,
23
- recordFileMtime,
24
- resolvePath,
25
- runHooks,
26
- validateGlobPattern
27
- } from "./chunk-KDVL2NP6.js";
28
- import {
29
- createWebSearchTool,
30
- fetchTool
31
- } from "./chunk-7GM6NN5E.js";
32
- import {
33
- defineTool
34
- } from "./chunk-PSO7R5AD.js";
35
- import {
36
- Permissions
37
- } from "./chunk-YYTFJUO2.js";
38
- import {
39
- addProviderKey
40
- } from "./chunk-SWNRA3EW.js";
41
- import {
42
- listAllModels
43
- } from "./chunk-INMSTUB2.js";
44
- import {
45
- providerName
46
- } from "./chunk-6SDRWEPN.js";
47
- import {
48
- FileOAuthProvider,
49
- hasStoredTokens
50
- } from "./chunk-3CYQYHPK.js";
51
- import {
52
- Session
53
- } from "./chunk-KEKDRROL.js";
54
- import {
55
- completionAssistantMessage,
56
- createProvider,
57
- hasPendingProgrammaticReplay,
58
- imageUserMessage,
59
- withFallbackProvider
60
- } from "./chunk-6JK4IBGK.js";
61
- import {
62
- expandEnvVars
63
- } from "./chunk-Q42ZGVAQ.js";
64
- import {
65
- ConfigError,
66
- bccliHome,
67
- loadConfig,
68
- resolveModel
69
- } from "./chunk-TLCL6DC7.js";
70
- import {
71
- t
72
- } from "./chunk-R754XIBJ.js";
73
-
74
- // src/textToolCalls.ts
75
- var PAIR = /(?:<arg_key>\s*|`)?([A-Za-z_]\w*)(?:\s*<\/arg_key>)?`?\s*(?:=\s*`|<arg_value>)([\s\S]*?)<\/arg_value>/g;
76
- var CALL_START = /<tool_call>|\n?[^\n]*\bTool call list\b[^\n]*|\n\s*\d+\.\s*Tool:/g;
77
- function coerce(value, type) {
78
- const types = Array.isArray(type) ? type : [type];
79
- const v = value.trim();
80
- if ((types.includes("integer") || types.includes("number")) && v !== "" && !Number.isNaN(Number(v))) return Number(v);
81
- if (types.includes("boolean") && /^(true|false)$/i.test(v)) return v.toLowerCase() === "true";
82
- if ((types.includes("object") || types.includes("array")) && /^[[{]/.test(v)) {
83
- try {
84
- return JSON.parse(v);
85
- } catch {
86
- }
87
- }
88
- return value.replace(/^\n+|\n+$/g, "");
89
- }
90
- function pickTool(name, keys, tools) {
91
- const named = name && tools.find((t2) => t2.function.name === name);
92
- if (named) return named;
93
- let best;
94
- let bestScore = 0;
95
- let tie = false;
96
- for (const t2 of tools) {
97
- const params = t2.function.parameters;
98
- const props = Object.keys(params.properties ?? {});
99
- if (!(params.required ?? []).every((r) => keys.includes(r))) continue;
100
- const score = keys.filter((k) => props.includes(k)).length;
101
- if (score > bestScore) [best, bestScore, tie] = [t2, score, false];
102
- else if (score === bestScore && score > 0) tie = true;
103
- }
104
- return tie ? void 0 : best;
105
- }
106
- function recoverTextToolCalls(text, tools) {
107
- if (!/<\/arg_value>|<tool_call>/.test(text)) return null;
108
- const starts = [...text.matchAll(CALL_START)].map((m) => m.index ?? 0);
109
- const firstPair = text.search(PAIR);
110
- const cut = Math.min(...[...starts, firstPair].filter((i) => i >= 0));
111
- const bounds = starts.filter((i) => i >= cut);
112
- if (!bounds.length || bounds[0] > cut) bounds.unshift(cut);
113
- const toolCalls = [];
114
- bounds.forEach((start, n) => {
115
- const segment = text.slice(start, bounds[n + 1] ?? text.length);
116
- const name = /<tool_call>\s*([A-Za-z_][\w.-]*)/.exec(segment)?.[1] ?? /Tool:\s*`?([A-Za-z_][\w.-]*)`?/.exec(segment)?.[1];
117
- let args = [];
118
- const flush = () => {
119
- if (!args.length) return;
120
- const keys = args.map(([k]) => k);
121
- const tool = pickTool(name, keys, tools);
122
- if (tool) {
123
- const props = tool.function.parameters.properties ?? {};
124
- const input = Object.fromEntries(args.filter(([k]) => k in props).map(([k, v]) => [k, coerce(v, props[k].type)]));
125
- toolCalls.push({ id: `text_call_${toolCalls.length}`, name: tool.function.name, arguments: JSON.stringify(input) });
126
- }
127
- args = [];
128
- };
129
- for (const m of segment.matchAll(PAIR)) {
130
- if (args.some(([k]) => k === m[1])) flush();
131
- args.push([m[1], m[2]]);
132
- }
133
- flush();
134
- });
135
- if (!toolCalls.length) return null;
136
- return { text: text.slice(0, cut).trim(), toolCalls };
137
- }
138
-
139
- // src/tools/index.ts
140
- import { z as z6 } from "zod";
141
-
142
- // src/tools/edit.ts
143
- import { readFile, writeFile } from "fs/promises";
144
- import { z } from "zod";
145
-
146
- // src/diff.ts
147
- import { structuredPatch } from "diff";
148
- function diffLines(oldText, newText) {
149
- const patch = structuredPatch("a", "b", oldText, newText, "", "", { context: 2 });
150
- const out = [];
151
- for (const hunk of patch.hunks) {
152
- let oldLine = hunk.oldStart;
153
- let newLine = hunk.newStart;
154
- for (const raw of hunk.lines) {
155
- const text = raw.slice(1);
156
- if (raw[0] === "-") out.push({ kind: "del", line: oldLine++, text });
157
- else if (raw[0] === "+") out.push({ kind: "add", line: newLine++, text });
158
- else if (raw[0] === " ") {
159
- out.push({ kind: "ctx", line: newLine, text });
160
- oldLine++;
161
- newLine++;
162
- }
163
- }
164
- }
165
- return out;
166
- }
167
- function formatDiff(lines) {
168
- const mark = { add: "+", del: "-", ctx: " " };
169
- return lines.map((l) => `${String(l.line).padStart(5)} ${mark[l.kind]} ${l.text}`).join("\n");
170
- }
171
-
172
- // src/tools/edit.ts
173
- var editEntrySchema = z.object({
174
- old_string: z.string().describe("Exact text to replace; must be unique unless replace_all"),
175
- new_string: z.string().describe("Replacement text"),
176
- replace_all: z.boolean().optional().describe("Replace every occurrence")
177
- });
178
- var schema = z.object({
179
- path: z.string().describe("File path, relative to the project or absolute"),
180
- old_string: z.string().optional().describe("Exact text to replace; must be unique unless replace_all"),
181
- new_string: z.string().optional().describe("Replacement text"),
182
- replace_all: z.boolean().optional().describe("Replace every occurrence"),
183
- edits: z.array(editEntrySchema).optional().describe("Optional list of sequential edits to apply atomically to the same file")
184
- });
185
- function countOccurrences(haystack, needle) {
186
- let count = 0;
187
- for (let i = haystack.indexOf(needle); i !== -1; i = haystack.indexOf(needle, i + needle.length)) count++;
188
- return count;
189
- }
190
- function normalizeTypographic(s) {
191
- return s.replace(/[\u2018\u2019\u201A\u201B]/g, "'").replace(/[\u201C\u201D\u201E\u201F]/g, '"').replace(/[\u2013\u2014]/g, "-").replace(/\u00A0/g, " ");
192
- }
193
- function stripReadLinePrefixes(s) {
194
- const lines = s.split(/\r?\n/);
195
- if (lines.every((l) => !l.trim() || /^\s*\d+\t/.test(l))) {
196
- return lines.map((l) => l.replace(/^\s*\d+\t/, "")).join("\n");
197
- }
198
- return s;
199
- }
200
- function matchByNormalizedLines(content, oldText, newText, replaceAll) {
201
- const eol = content.includes("\r\n") ? "\r\n" : "\n";
202
- const fileLines = content.split(/\r?\n/);
203
- const oldLines = stripReadLinePrefixes(oldText).split(/\r?\n/);
204
- if (!oldLines.length) return { count: 0 };
205
- const normLine = (l) => normalizeTypographic(l).trimEnd();
206
- const normOld = oldLines.map(normLine);
207
- const matches = [];
208
- for (let i = 0; i + oldLines.length <= fileLines.length; i++) {
209
- let ok = true;
210
- for (let j = 0; j < oldLines.length; j++) {
211
- if (normLine(fileLines[i + j]) !== normOld[j]) {
212
- ok = false;
213
- break;
214
- }
215
- }
216
- if (ok) {
217
- matches.push(i);
218
- if (replaceAll) i += oldLines.length - 1;
219
- }
220
- }
221
- if (matches.length === 0) return { count: 0 };
222
- if (matches.length > 1 && !replaceAll) return { count: matches.length };
223
- const replacementLines = stripReadLinePrefixes(newText).split(/\r?\n/);
224
- const outLines = [...fileLines];
225
- for (let m = matches.length - 1; m >= 0; m--) {
226
- outLines.splice(matches[m], oldLines.length, ...replacementLines);
227
- }
228
- return { count: matches.length, result: outLines.join(eol) };
229
- }
230
- function applySingleEdit(content, path, input) {
231
- if (input.old_string === input.new_string) {
232
- return { output: "old_string and new_string are identical; nothing to change.", isError: true };
233
- }
234
- const eol = content.includes("\r\n") ? "\r\n" : "\n";
235
- const toEol = (s) => s.replace(/\r?\n/g, eol);
236
- const strippedOld = stripReadLinePrefixes(input.old_string);
237
- const strippedNew = stripReadLinePrefixes(input.new_string);
238
- const attempts = [[input.old_string, input.new_string]];
239
- if (toEol(input.old_string) !== input.old_string) attempts.push([toEol(input.old_string), toEol(input.new_string)]);
240
- if (strippedOld !== input.old_string) {
241
- attempts.push([strippedOld, strippedNew], [toEol(strippedOld), toEol(strippedNew)]);
242
- }
243
- for (const [oldText, newText] of attempts) {
244
- const count = oldText ? countOccurrences(content, oldText) : 0;
245
- if (count === 0) continue;
246
- if (count > 1 && !input.replace_all) {
247
- return {
248
- output: `old_string appears ${count} times in ${path}. Add more context to make it unique, or use replace_all.`,
249
- isError: true
250
- };
251
- }
252
- return input.replace_all ? content.split(oldText).join(newText) : content.replace(oldText, () => newText);
253
- }
254
- const fuzzy = matchByNormalizedLines(content, input.old_string, input.new_string, input.replace_all);
255
- if (fuzzy.count > 1 && !input.replace_all) {
256
- return {
257
- output: `old_string appears ${fuzzy.count} times in ${path}. Add more context to make it unique, or use replace_all.`,
258
- isError: true
259
- };
260
- }
261
- if (fuzzy.count > 0 && fuzzy.result !== void 0) return fuzzy.result;
262
- return { output: `old_string not found in ${path}. Re-read the file.`, isError: true };
263
- }
264
- function applyEdit(content, input) {
265
- const list = input.edits?.length ? input.edits : input.old_string !== void 0 && input.new_string !== void 0 ? [{ old_string: input.old_string, new_string: input.new_string, replace_all: input.replace_all }] : [];
266
- if (!list.length) return { output: "Provide old_string and new_string (or edits array).", isError: true };
267
- let current = content;
268
- for (const item of list) {
269
- const next = applySingleEdit(current, input.path, item);
270
- if (typeof next !== "string") return next;
271
- current = next;
272
- }
273
- return current;
274
- }
275
- var editTool = defineTool({
276
- name: "edit",
277
- 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.",
278
- schema,
279
- kind: "edit",
280
- target: (input) => input.path,
281
- async validate(input, ctx) {
282
- const abs = resolvePath(ctx.cwd, input.path);
283
- if (!ctx.readFiles.has(abs)) return `Read ${input.path} with read before editing it.`;
284
- if (isFileModifiedSinceRead(ctx.readFiles, abs)) {
285
- return `${input.path} has been modified since it was last read. Read it again before editing.`;
286
- }
287
- let content;
288
- try {
289
- content = await readFile(abs, "utf8");
290
- } catch (error) {
291
- return `Cannot read ${input.path}: ${errorCode(error)}`;
292
- }
293
- const next = applyEdit(content, input);
294
- return typeof next === "string" ? void 0 : next.output;
295
- },
296
- async preview(input, ctx) {
297
- const abs = resolvePath(ctx.cwd, input.path);
298
- const content = await readFile(abs, "utf8");
299
- const next = applyEdit(content, input);
300
- return typeof next === "string" ? formatDiff(diffLines(content, next)) : void 0;
301
- },
302
- async run(input, ctx) {
303
- const abs = resolvePath(ctx.cwd, input.path);
304
- if (!ctx.readFiles.has(abs)) {
305
- return { output: `Read ${input.path} with read before editing it.`, isError: true };
306
- }
307
- if (isFileModifiedSinceRead(ctx.readFiles, abs)) {
308
- return { output: `${input.path} has been modified since it was last read. Read it again before editing.`, isError: true };
309
- }
310
- let content;
311
- try {
312
- content = await readFile(abs, "utf8");
313
- } catch (error) {
314
- return { output: `Cannot read ${input.path}: ${errorCode(error)}`, isError: true };
315
- }
316
- const next = applyEdit(content, input);
317
- if (typeof next !== "string") return next;
318
- await ctx.checkpoint?.(abs);
319
- await writeFile(abs, next);
320
- recordFileMtime(ctx.readFiles, abs);
321
- return { output: `Edited ${input.path}.`, display: formatDiff(diffLines(content, next)) };
322
- }
323
- });
324
-
325
- // src/tools/glob.ts
326
- import { statSync } from "fs";
327
- import { glob as nativeGlob } from "fs/promises";
328
- import { join } from "path";
329
- import { z as z2 } from "zod";
330
-
331
- // src/abort.ts
332
- var ABORTED = /* @__PURE__ */ Symbol("aborted");
333
- function raceAbort(promise, signal) {
334
- if (signal.aborted) return Promise.resolve(ABORTED);
335
- return new Promise((resolve, reject) => {
336
- const onAbort = () => resolve(ABORTED);
337
- signal.addEventListener("abort", onAbort, { once: true });
338
- promise.then(
339
- (value) => {
340
- signal.removeEventListener("abort", onAbort);
341
- resolve(value);
342
- },
343
- (error) => {
344
- signal.removeEventListener("abort", onAbort);
345
- reject(error);
346
- }
347
- );
348
- });
349
- }
350
- async function collectUntilAbort(iterable, signal, onItem) {
351
- const iterator = iterable[Symbol.asyncIterator]();
352
- try {
353
- for (; ; ) {
354
- const next = signal ? await raceAbort(iterator.next(), signal) : await iterator.next();
355
- if (next === ABORTED) return false;
356
- if (next.done) return true;
357
- onItem(next.value);
358
- }
359
- } finally {
360
- Promise.resolve(iterator.return?.(void 0)).catch(() => {
361
- });
362
- }
363
- }
364
-
365
- // src/tools/glob.ts
366
- var MAX_OUTPUT_CHARS = 24e3;
367
- var globTool = defineTool({
368
- name: "glob",
369
- description: 'Find files by glob pattern, e.g. "src/**/*.ts". Respects .gitignore. Max 500 results.',
370
- schema: z2.object({
371
- pattern: z2.string().describe("Glob pattern"),
372
- path: z2.string().optional().describe("Base directory, default project root")
373
- }),
374
- kind: "read",
375
- target: (input) => input.path ? `${input.pattern} ${input.path}` : input.pattern,
376
- async run(input, ctx) {
377
- const invalid = validateGlobPattern(input.pattern);
378
- if (invalid) return { output: invalid, isError: true };
379
- const root = resolvePath(ctx.cwd, input.path ?? ".");
380
- const files = [];
381
- const finished = await collectUntilAbort(
382
- nativeGlob(input.pattern, { cwd: root, exclude: ignorePatterns(root, ctx.cwd) }),
383
- ctx.signal,
384
- (file) => files.push(normalizeGlobPath(file))
385
- );
386
- if (!finished) return { output: "Cancelled by the user.", isError: true };
387
- const mtime = (rel) => {
388
- try {
389
- return statSync(join(root, rel)).mtimeMs;
390
- } catch {
391
- return 0;
392
- }
393
- };
394
- const mtimes = new Map(files.map((f) => [f, mtime(f)]));
395
- files.sort((a, b) => {
396
- const diff = (mtimes.get(b) ?? 0) - (mtimes.get(a) ?? 0);
397
- return Math.abs(diff) >= 50 ? diff : a.localeCompare(b);
398
- });
399
- if (!files.length) return { output: "No matching files." };
400
- const first = files.slice(0, 500);
401
- const shown = [];
402
- let size = 0;
403
- for (const file of first) {
404
- const extra = file.length + (shown.length ? 1 : 0);
405
- if (shown.length && size + extra > MAX_OUTPUT_CHARS) break;
406
- shown.push(file);
407
- size += extra;
408
- if (size >= MAX_OUTPUT_CHARS) break;
409
- }
410
- const omitted = files.length - shown.length;
411
- const more = omitted > 0 ? `
412
- \u2026 ${omitted} more files omitted (narrow pattern/path)` : "";
413
- return { output: shown.join("\n") + more, display: `${files.length} file` };
414
- }
415
- });
416
-
417
- // src/tools/grep.ts
418
- import { execFile } from "child_process";
419
- import { statSync as statSync2 } from "fs";
420
- import { readFile as readFile2 } from "fs/promises";
421
- import { glob as nativeGlob2 } from "fs/promises";
422
- import { basename, dirname } from "path";
423
- import { z as z3 } from "zod";
424
- var MAX_MATCHES = 200;
425
- var MAX_OUTPUT_CHARS2 = 24e3;
426
- function escapeRegExp(s) {
427
- return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
428
- }
429
- function isSingleFile(p) {
430
- try {
431
- return statSync2(p).isFile();
432
- } catch {
433
- return false;
434
- }
435
- }
436
- async function grepJs(pattern, opts) {
437
- const source = opts.literal ? escapeRegExp(pattern) : pattern;
438
- const regex = new RegExp(source, opts.ignoreCase ? "i" : "");
439
- const single = isSingleFile(opts.root);
440
- const files = [];
441
- if (single) {
442
- files.push(basename(opts.root));
443
- } else {
444
- const finished = await collectUntilAbort(
445
- nativeGlob2(opts.glob ?? "**/*", { cwd: opts.root, exclude: ignorePatterns(opts.root, opts.projectRoot) }),
446
- opts.signal,
447
- (file) => files.push(normalizeGlobPath(file))
448
- );
449
- if (!finished) return [];
450
- }
451
- const baseDir = single ? dirname(opts.root) : opts.root;
452
- const ctxLines = Math.max(0, Math.min(10, Math.floor(opts.context ?? 0)));
453
- const mode = opts.outputMode ?? "content";
454
- const out = [];
455
- for (const file of files.sort()) {
456
- if (opts.signal?.aborted) break;
457
- let text;
458
- try {
459
- const buffer = await readFile2(resolvePath(baseDir, file));
460
- if (buffer.subarray(0, 8e3).includes(0)) continue;
461
- text = buffer.toString("utf8");
462
- } catch {
463
- continue;
464
- }
465
- const lines = text.split(/\r?\n/);
466
- let fileMatches = 0;
467
- for (let i = 0; i < lines.length && out.length < MAX_MATCHES; i++) {
468
- if (regex.test(lines[i])) {
469
- fileMatches++;
470
- if (mode === "files_with_matches") {
471
- out.push(file);
472
- break;
473
- }
474
- if (mode === "content") {
475
- const from = Math.max(0, i - ctxLines);
476
- const to = Math.min(lines.length - 1, i + ctxLines);
477
- for (let j = from; j <= to && out.length < MAX_MATCHES; j++) {
478
- const sep2 = j === i ? ":" : "-";
479
- out.push(`${file}${sep2}${j + 1}${sep2}${lines[j].slice(0, 300)}`);
480
- }
481
- }
482
- }
483
- }
484
- if (mode === "count" && fileMatches > 0) out.push(`${file}:${fileMatches}`);
485
- if (out.length >= MAX_MATCHES) break;
486
- }
487
- return out;
488
- }
489
- function grepRg(pattern, opts) {
490
- const single = isSingleFile(opts.root);
491
- const cwd = single ? dirname(opts.root) : opts.root;
492
- const target = single ? basename(opts.root) : ".";
493
- const mode = opts.outputMode ?? "content";
494
- const args = ["--color", "never"];
495
- if (mode === "files_with_matches") {
496
- args.push("--files-with-matches");
497
- } else if (mode === "count") {
498
- args.push("--count");
499
- } else {
500
- args.push("--line-number", "--no-heading", "--max-columns", "300");
501
- if (opts.context && opts.context > 0) args.push("-C", String(Math.min(10, Math.floor(opts.context))));
502
- }
503
- if (opts.literal) args.push("-F");
504
- if (opts.ignoreCase) args.push("-i");
505
- if (opts.glob && !single) args.push("-g", opts.glob);
506
- args.push("-e", pattern, target);
507
- return new Promise((resolve) => {
508
- execFile("rg", args, { cwd, maxBuffer: 10 * 1024 * 1024, signal: opts.signal }, (error, stdout) => {
509
- if (opts.signal?.aborted) return resolve([]);
510
- const code = error?.code;
511
- if (code === "ENOENT") return resolve(void 0);
512
- if (error && error.code !== 1) return resolve(void 0);
513
- resolve(
514
- stdout.split(/\r?\n/).filter(Boolean).map((l) => l.replace(/^\.[\\/]/, "").replace(/\\/g, "/")).slice(0, MAX_MATCHES)
515
- );
516
- });
517
- });
518
- }
519
- var grepTool = defineTool({
520
- name: "grep",
521
- description: "Search file contents with a regular expression or literal text. Respects .gitignore. Returns path:line:text (max 200).",
522
- schema: z3.object({
523
- pattern: z3.string().describe("Regular expression or literal string"),
524
- path: z3.string().optional().describe("File or directory to search, default project root"),
525
- glob: z3.string().optional().describe('Only files matching this glob, e.g. "**/*.ts"'),
526
- ignore_case: z3.boolean().optional(),
527
- literal: z3.boolean().optional().describe("Treat pattern as literal text instead of regex"),
528
- context: z3.number().int().min(0).max(10).optional().describe("Number of context lines before and after each match"),
529
- output_mode: z3.enum(["content", "files_with_matches", "count"]).optional().describe("Output format, default content")
530
- }),
531
- kind: "read",
532
- target: (input) => input.path ? `${input.pattern} ${input.path}` : input.pattern,
533
- async run(input, ctx) {
534
- if (input.glob) {
535
- const invalid = validateGlobPattern(input.glob);
536
- if (invalid) return { output: invalid, isError: true };
537
- }
538
- const opts = {
539
- root: resolvePath(ctx.cwd, input.path ?? "."),
540
- projectRoot: ctx.cwd,
541
- glob: input.glob,
542
- ignoreCase: input.ignore_case,
543
- literal: input.literal,
544
- context: input.context,
545
- outputMode: input.output_mode,
546
- signal: ctx.signal
547
- };
548
- if (!input.literal) {
549
- try {
550
- new RegExp(input.pattern);
551
- } catch (error) {
552
- return { output: `Invalid regex: ${error.message}`, isError: true };
553
- }
554
- }
555
- const matches = await grepRg(input.pattern, opts) ?? await grepJs(input.pattern, opts);
556
- if (ctx.signal.aborted) return { output: "Cancelled by the user.", isError: true };
557
- if (!matches.length) return { output: "No matches." };
558
- const shown = [];
559
- let size = 0;
560
- for (const match of matches) {
561
- const extra = match.length + (shown.length ? 1 : 0);
562
- if (shown.length && size + extra > MAX_OUTPUT_CHARS2) break;
563
- shown.push(match);
564
- size += extra;
565
- if (size >= MAX_OUTPUT_CHARS2) break;
566
- }
567
- const omitted = matches.length - shown.length;
568
- const suffix = omitted > 0 ? `
569
- \u2026 ${omitted} more matches omitted (narrow pattern/path/glob)` : "";
570
- return { output: shown.join("\n") + suffix, display: t("{n} results", { n: matches.length }) };
571
- }
572
- });
573
-
574
- // src/tools/read.ts
575
- import { readdirSync, statSync as statSync3 } from "fs";
576
- import { readFile as readFile3 } from "fs/promises";
577
- import { z as z4 } from "zod";
578
- var MAX_LINE = 2e3;
579
- var MAX_OUTPUT_CHARS3 = 4e4;
580
- var MAX_IMAGE_BYTES = 4 * 1024 * 1024;
581
- var IMAGE_HEADERS = [
582
- {
583
- mediaType: "image/png",
584
- match: (h) => h.subarray(0, 8).equals(Buffer.from([137, 80, 78, 71, 13, 10, 26, 10]))
585
- },
586
- { mediaType: "image/jpeg", match: (h) => h[0] === 255 && h[1] === 216 && h[2] === 255 },
587
- { mediaType: "image/gif", match: (h) => h.subarray(0, 6).toString("ascii").startsWith("GIF8") },
588
- {
589
- mediaType: "image/webp",
590
- match: (h) => h.subarray(0, 4).toString("ascii") === "RIFF" && h.subarray(8, 12).toString("ascii") === "WEBP"
591
- }
592
- ];
593
- var MEDIA_LABEL = {
594
- "image/png": "png",
595
- "image/jpeg": "jpeg",
596
- "image/gif": "gif",
597
- "image/webp": "webp"
598
- };
599
- function imageMediaType(buffer) {
600
- return IMAGE_HEADERS.find((entry) => entry.match(buffer))?.mediaType;
601
- }
602
- var schema2 = z4.object({
603
- path: z4.string().describe("File path, relative to the project or absolute"),
604
- offset: z4.number().int().min(1).optional().describe("First line to read, 1-based"),
605
- limit: z4.number().int().min(1).optional().describe("Number of lines, default 2000")
606
- });
607
- async function readImageResult(input, buffer, mediaType) {
608
- if (buffer.length > MAX_IMAGE_BYTES) {
609
- return {
610
- output: `Cannot attach ${input.path}: image is ${(buffer.length / (1024 * 1024)).toFixed(1)} MB (limit 4 MB).`,
611
- isError: true
612
- };
613
- }
614
- const label = MEDIA_LABEL[mediaType] ?? mediaType;
615
- return {
616
- output: `[image attached] ${input.path} (${label}, ${(buffer.length / 1024).toFixed(1)} KB).`,
617
- display: `${input.path} \xB7 image`,
618
- images: [{ mediaType, data: buffer.toString("base64"), path: input.path }]
619
- };
620
- }
621
- async function readTextResult(input, buffer) {
622
- const lines = buffer.toString("utf8").split(/\r?\n/);
623
- const start = (input.offset ?? 1) - 1;
624
- const limit = input.limit ?? 2e3;
625
- if (start >= lines.length) {
626
- return {
627
- output: `offset ${input.offset} is past the end of ${input.path} (${lines.length} line(s)); start at 1.`,
628
- isError: true
629
- };
630
- }
631
- const requested = lines.slice(start, start + limit);
632
- const shown = [];
633
- let size = 0;
634
- for (let i = 0; i < requested.length; i++) {
635
- const line = requested[i];
636
- const text = line.length > MAX_LINE ? `${line.slice(0, MAX_LINE)}\u2026 [line truncated]` : line;
637
- const rendered = `${String(start + i + 1).padStart(6)} ${text}`;
638
- const extra = rendered.length + (shown.length ? 1 : 0);
639
- if (shown.length && size + extra > MAX_OUTPUT_CHARS3) break;
640
- shown.push(rendered);
641
- size += extra;
642
- if (size >= MAX_OUTPUT_CHARS3) break;
643
- }
644
- const body = shown.join("\n");
645
- const remaining = lines.length - start - shown.length;
646
- const tokenBudgetHit = shown.length < requested.length;
647
- const nextOffset = start + shown.length + 1;
648
- return {
649
- output: remaining > 0 ? tokenBudgetHit ? `${body}
650
- \u2026 ${remaining} more lines (token budget reached; continue with offset=${nextOffset})` : `${body}
651
- \u2026 ${remaining} more lines (use offset)` : body,
652
- display: t("{n} lines", { n: shown.length })
653
- };
654
- }
655
- function createReadTool(options = {}) {
656
- const vision = options.vision !== false;
657
- return defineTool({
658
- name: "read",
659
- 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.",
660
- schema: schema2,
661
- kind: "read",
662
- target: (input) => input.path,
663
- async run(input, ctx) {
664
- const abs = resolvePath(ctx.cwd, input.path);
665
- try {
666
- if (statSync3(abs).isDirectory()) {
667
- const entries = readdirSync(abs, { withFileTypes: true }).map((e) => e.isDirectory() ? `${e.name}/` : e.name).sort((a, b) => a.localeCompare(b));
668
- const start = (input.offset ?? 1) - 1;
669
- const limit = input.limit ?? 2e3;
670
- const slice = entries.slice(start, start + limit);
671
- return { output: slice.join("\n") || "(empty directory)", display: t("{n} lines", { n: slice.length }) };
672
- }
673
- } catch {
674
- }
675
- let buffer;
676
- try {
677
- buffer = await readFile3(abs);
678
- } catch (error) {
679
- return { output: `Cannot read ${input.path}: ${errorCode(error)}`, isError: true };
680
- }
681
- const mediaType = vision ? imageMediaType(buffer) : void 0;
682
- if (mediaType) {
683
- recordFileMtime(ctx.readFiles, abs);
684
- return readImageResult(input, buffer, mediaType);
685
- }
686
- if (buffer.subarray(0, 8e3).includes(0)) {
687
- return { output: `${input.path} is a binary file; not read.`, isError: true };
688
- }
689
- recordFileMtime(ctx.readFiles, abs);
690
- return readTextResult(input, buffer);
691
- }
692
- });
693
- }
694
- var readTool = createReadTool({ vision: true });
695
-
696
- // src/tools/write.ts
697
- import { existsSync } from "fs";
698
- import { mkdir, readFile as readFile4, writeFile as writeFile2 } from "fs/promises";
699
- import { dirname as dirname2 } from "path";
700
- import { z as z5 } from "zod";
701
- var writeTool = defineTool({
702
- name: "write",
703
- description: "Create or overwrite a file with the given content. Read an existing file first. Prefer edit for changes.",
704
- schema: z5.object({
705
- path: z5.string().describe("File path, relative to the project or absolute"),
706
- content: z5.string().describe("Full new file content")
707
- }),
708
- kind: "edit",
709
- target: (input) => input.path,
710
- async validate(input, ctx) {
711
- const abs = resolvePath(ctx.cwd, input.path);
712
- if (existsSync(abs) && !ctx.readFiles.has(abs)) return `${input.path} already exists. Read it with read before overwriting it.`;
713
- if (isFileModifiedSinceRead(ctx.readFiles, abs)) return `${input.path} has been modified since it was last read. Read it again before overwriting it.`;
714
- return void 0;
715
- },
716
- async preview(input, ctx) {
717
- const abs = resolvePath(ctx.cwd, input.path);
718
- const old = existsSync(abs) ? await readFile4(abs, "utf8") : "";
719
- return formatDiff(diffLines(old, input.content));
720
- },
721
- async run(input, ctx) {
722
- const abs = resolvePath(ctx.cwd, input.path);
723
- if (existsSync(abs) && !ctx.readFiles.has(abs)) {
724
- return { output: `${input.path} already exists. Read it with read before overwriting it.`, isError: true };
725
- }
726
- if (isFileModifiedSinceRead(ctx.readFiles, abs)) {
727
- return { output: `${input.path} has been modified since it was last read. Read it again before overwriting it.`, isError: true };
728
- }
729
- try {
730
- await ctx.checkpoint?.(abs);
731
- await mkdir(dirname2(abs), { recursive: true });
732
- await writeFile2(abs, input.content);
733
- } catch (error) {
734
- return { output: `Failed to write ${input.path}: ${errorCode(error)}`, isError: true };
735
- }
736
- recordFileMtime(ctx.readFiles, abs);
737
- const lines = input.content.split("\n").length;
738
- return { output: `Wrote ${input.path} (${lines} lines).`, display: t("{lines} lines written", { lines }) };
739
- }
740
- });
741
-
742
- // src/tools/index.ts
743
- var ALL_TOOLS = [readTool, writeTool, editTool, bashTool, grepTool, globTool, fetchTool];
744
- function toolDefinitions(tools) {
745
- return tools.map((tool) => {
746
- const { $schema: _ignored, ...parameters } = tool.jsonSchema ?? z6.toJSONSchema(tool.schema);
747
- return { type: "function", function: { name: tool.name, description: tool.description, parameters, ...tool.outputJsonSchema ? { output_schema: tool.outputJsonSchema } : {} } };
748
- });
749
- }
750
-
751
- // src/webBudget.ts
752
- var KEEP_TOKENS = 4e4;
753
- var PRUNE_MIN_TOKENS = 2e4;
754
- var STUB = "[old tool result removed:";
755
- var tokens = (text) => Math.ceil(text.length / 4);
756
- var isStub = (content) => content.startsWith(STUB);
757
- function pruneOldFetches(messages) {
758
- const urls = /* @__PURE__ */ new Map();
759
- for (const m of messages) {
760
- if (m.role !== "assistant") continue;
761
- for (const c of m.tool_calls ?? []) {
762
- if (c.function.name !== "fetch") continue;
763
- try {
764
- urls.set(c.id, String(JSON.parse(c.function.arguments).url ?? ""));
765
- } catch {
766
- urls.set(c.id, "");
767
- }
768
- }
769
- }
770
- let kept = 0;
771
- let oldTokens = 0;
772
- const old = [];
773
- for (let i = messages.length - 1; i >= 0; i--) {
774
- const m = messages[i];
775
- if (m.role !== "tool" || !urls.has(m.tool_call_id) || isStub(m.content)) continue;
776
- kept += tokens(m.content);
777
- if (kept > KEEP_TOKENS) {
778
- old.push(i);
779
- oldTokens += tokens(m.content);
780
- }
781
- }
782
- if (oldTokens <= PRUNE_MIN_TOKENS) return false;
783
- for (const i of old) {
784
- const m = messages[i];
785
- messages[i] = { ...m, content: `${STUB} fetch ${urls.get(m.tool_call_id)}. Fetch it again (use prompt for a focused excerpt) if still needed.]` };
786
- }
787
- return true;
788
- }
789
- var WebBudget = class {
790
- fetches = 0;
791
- chars = 0;
792
- perTurn;
793
- sessionChars;
794
- constructor(limits = {}) {
795
- this.perTurn = limits.perTurn ?? 8;
796
- this.sessionChars = limits.sessionChars ?? 3e5;
797
- }
798
- startTurn() {
799
- this.fetches = 0;
800
- }
801
- /** Gives back a reservation, e.g. when the fetch was served from cache and cost nothing. */
802
- refund() {
803
- if (this.fetches > 0) this.fetches--;
804
- }
805
- reset() {
806
- this.fetches = 0;
807
- this.chars = 0;
808
- }
809
- /** Reserves one fetch; returns the refusal to show the model when over budget. */
810
- take() {
811
- if (this.fetches >= this.perTurn) {
812
- 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.`;
813
- }
814
- if (this.chars >= this.sessionChars) {
815
- 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.`;
816
- }
817
- this.fetches++;
818
- return void 0;
819
- }
820
- record(chars) {
821
- this.chars += chars;
822
- }
823
- };
824
-
825
- // src/budget.ts
826
- function priceFor(cap, model) {
827
- if (!cap.prices) return void 0;
828
- const key = Object.keys(cap.prices).find((k) => model.includes(k));
829
- return key ? cap.prices[key] : void 0;
830
- }
831
- function budgetStatus(usage, cap, model) {
832
- if (!cap) return void 0;
833
- const total = usage.inputTokens + usage.outputTokens;
834
- if (cap.tokens !== void 0 && total >= cap.tokens) return { kind: "tokens", used: total, limit: cap.tokens };
835
- if (cap.usd !== void 0) {
836
- const price = priceFor(cap, model);
837
- if (price) {
838
- const cached = Math.max(0, Math.min(usage.cachedInputTokens ?? 0, usage.inputTokens));
839
- const cachedRate = price.cachedInput ?? price.input;
840
- const usd = ((usage.inputTokens - cached) * price.input + cached * cachedRate + usage.outputTokens * price.output) / 1e6;
841
- if (usd >= cap.usd) return { kind: "usd", used: usd, limit: cap.usd };
842
- }
843
- }
844
- return void 0;
845
- }
846
-
847
- // src/agent.ts
848
- var CANCELLED = "Cancelled by the user.";
849
- 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.";
850
- var Agent = class {
851
- constructor(opts) {
852
- this.opts = opts;
853
- this.provider = opts.provider;
854
- this.permissions = opts.permissions;
855
- this.tools = opts.tools;
856
- this.definitions = toolDefinitions(opts.tools);
857
- this.turnTools = this.tools;
858
- this.turnDefinitions = this.definitions;
859
- this.messages = [...opts.history ?? []];
860
- this.maxSteps = typeof opts.maxSteps === "number" && Number.isFinite(opts.maxSteps) && opts.maxSteps > 0 ? Math.max(1, Math.floor(opts.maxSteps)) : void 0;
861
- this.contextWindow = opts.contextWindow ?? 128e3;
862
- this.budgetModel = opts.budgetModel ?? "";
863
- this.systemPrompt = opts.systemPrompt;
864
- this.reasoning = opts.reasoning ?? "auto";
865
- }
866
- opts;
867
- provider;
868
- permissions;
869
- messages;
870
- totalUsage = { inputTokens: 0, outputTokens: 0 };
871
- lastInputTokens = 0;
872
- onEvent = () => {
873
- };
874
- reasoning;
875
- askPermission = async () => "no";
876
- tools;
877
- definitions;
878
- // Snapshot for the running turn: tools added/removed mid-turn (e.g. an MCP server connecting) apply from the next turn.
879
- turnTools;
880
- turnDefinitions;
881
- readFiles = /* @__PURE__ */ new Set();
882
- webBudget = new WebBudget();
883
- maxSteps;
884
- // One window for every model; resolved from contextWindowFor() (catalog + env override).
885
- contextWindow;
886
- budgetModel;
887
- systemPrompt;
888
- setSystemPrompt(text) {
889
- this.systemPrompt = text;
890
- }
891
- setContextWindow(window) {
892
- this.contextWindow = window;
893
- }
894
- getContextWindow() {
895
- return this.contextWindow;
896
- }
897
- setBudgetModel(model) {
898
- this.budgetModel = model;
899
- }
900
- getBudgetModel() {
901
- return this.budgetModel;
902
- }
903
- setTools(tools) {
904
- this.tools = tools;
905
- this.definitions = toolDefinitions(tools);
906
- }
907
- isParallelSafe(call) {
908
- const tool = this.turnTools.find((t2) => t2.name === call.name);
909
- if (!tool?.parallelSafe) return false;
910
- try {
911
- const parsed = tool.schema.safeParse(JSON.parse(call.arguments || "{}"));
912
- return parsed.success && tool.parallelSafe(parsed.data);
913
- } catch {
914
- return false;
915
- }
916
- }
917
- push(message) {
918
- this.messages.push(message);
919
- this.opts.onMessage?.(message);
920
- }
921
- /** Swaps in another conversation (resume, new session) without writing to the session log. */
922
- load(messages) {
923
- this.messages = messages;
924
- this.readFiles.clear();
925
- this.webBudget.reset();
926
- this.lastInputTokens = estimateMessages(messages) + estimateTokens(this.systemPrompt);
927
- }
928
- clear() {
929
- this.messages = [];
930
- this.readFiles.clear();
931
- this.webBudget.reset();
932
- this.lastInputTokens = 0;
933
- this.opts.onReset?.();
934
- }
935
- async compact(signal) {
936
- pruneOldToolOutputs(this.messages);
937
- const keepTokens = Math.min(15e3, Math.floor(this.contextWindow * 0.1));
938
- const [older, keep] = splitKeepRecent(this.messages, keepTokens);
939
- const history = trimToFit(older, Math.floor(this.contextWindow * 0.8));
940
- const pending = this.provider.chat({
941
- messages: [{ role: "system", content: this.systemPrompt }, ...history, { role: "user", content: COMPACT_PROMPT }],
942
- // Some gateways reject tool_calls in history when no tools are declared.
943
- tools: this.definitions,
944
- signal,
945
- reasoning: this.reasoning
946
- });
947
- const completion = await raceAbort(pending, signal);
948
- if (completion === ABORTED) throw new Error("aborted");
949
- this.clear();
950
- this.push({ role: "user", content: `Summary of the previous conversation:
951
- ${completion.text}` });
952
- this.push({ role: "assistant", content: "OK, I will continue from this summary." });
953
- for (const m of keep) this.push(m);
954
- this.onEvent({ type: "compacted" });
955
- }
956
- /**
957
- * Events of one tool execution. A cancelled turn gives up on a tool that does not stop, and whatever that tool
958
- * reports later must not reach the screen or a newer turn, so this is per execution and not per call id (ids repeat).
959
- */
960
- report(ticket, event) {
961
- if (ticket.abandoned) return;
962
- if (event.type === "toolStart") ticket.open = true;
963
- if (event.type === "toolEnd") ticket.open = false;
964
- this.onEvent(event);
965
- }
966
- async run(text, signal) {
967
- this.turnTools = this.tools;
968
- this.turnDefinitions = this.definitions;
969
- this.opts.onTurnStart?.();
970
- this.webBudget.startTurn();
971
- try {
972
- const over0 = this.overBudget();
973
- if (over0) {
974
- this.onEvent({ type: "budgetExceeded", ...over0 });
975
- return;
976
- }
977
- if (this.lastInputTokens > this.contextWindow * 0.8 && !hasPendingProgrammaticReplay(this.messages)) await this.compact(signal);
978
- this.push({ role: "user", content: text });
979
- for (let step = 0; this.maxSteps === void 0 || step < this.maxSteps; step++) {
980
- if (step > 0 && this.lastInputTokens > this.contextWindow * 0.8 && !hasPendingProgrammaticReplay(this.messages)) {
981
- await this.compact(signal);
982
- this.push({ role: "user", content: `Continue this task using the summary above: ${text}` });
983
- }
984
- pruneOldFetches(this.messages);
985
- const over = this.overBudget();
986
- if (over) {
987
- this.onEvent({ type: "budgetExceeded", ...over });
988
- return;
989
- }
990
- const pendingCompletion = this.provider.chat({
991
- messages: [{ role: "system", content: this.systemPrompt }, ...this.messages],
992
- tools: this.turnDefinitions,
993
- programmaticToolNames: this.turnTools.filter((tool) => tool.programmaticSafe).map((tool) => tool.name),
994
- signal,
995
- // Late output from a cancelled provider must not leak into the next turn.
996
- onText: (delta) => {
997
- if (!signal.aborted) this.onEvent({ type: "text", delta });
998
- },
999
- onThinking: (delta) => {
1000
- if (!signal.aborted) this.onEvent({ type: "thinking", delta });
1001
- },
1002
- reasoning: this.reasoning
1003
- });
1004
- const outcome = await raceAbort(pendingCompletion, signal);
1005
- if (outcome === ABORTED) {
1006
- this.onEvent({ type: "aborted" });
1007
- return;
1008
- }
1009
- let completion = outcome;
1010
- if (!completion.toolCalls.length && completion.finishReason !== "repetition") {
1011
- const recovered = recoverTextToolCalls(completion.text, this.turnDefinitions);
1012
- if (recovered) {
1013
- completion = { ...completion, ...recovered };
1014
- this.onEvent({ type: "textReplace", text: recovered.text });
1015
- }
1016
- }
1017
- const usage = completion.usage ?? {
1018
- inputTokens: estimateTokens(this.systemPrompt) + estimateTokens(JSON.stringify(this.messages)),
1019
- outputTokens: estimateTokens(completion.text) + (completion.toolCalls.length ? estimateTokens(JSON.stringify(completion.toolCalls)) : 0)
1020
- };
1021
- this.totalUsage.inputTokens += usage.inputTokens;
1022
- this.totalUsage.outputTokens += usage.outputTokens;
1023
- this.totalUsage.cachedInputTokens = (this.totalUsage.cachedInputTokens ?? 0) + (usage.cachedInputTokens ?? 0);
1024
- this.totalUsage.cacheWriteTokens = (this.totalUsage.cacheWriteTokens ?? 0) + (usage.cacheWriteTokens ?? 0);
1025
- this.lastInputTokens = usage.inputTokens;
1026
- this.onEvent({ type: "usage", ...this.totalUsage });
1027
- this.opts.onUsage?.(usage);
1028
- this.push(completionAssistantMessage(completion));
1029
- if (completion.finishReason === "repetition") {
1030
- this.onEvent({ type: "textReplace", text: completion.text });
1031
- this.onEvent({
1032
- type: "error",
1033
- message: t("The model got stuck repeating text, so the answer was stopped. Try again or switch models with /model.")
1034
- });
1035
- return;
1036
- }
1037
- if (!completion.toolCalls.length) {
1038
- if (completion.finishReason === "continue") continue;
1039
- this.onEvent({ type: "done" });
1040
- return;
1041
- }
1042
- const calls = completion.toolCalls;
1043
- let i = 0;
1044
- const roundImages = [];
1045
- while (i < calls.length) {
1046
- if (signal.aborted) {
1047
- this.cancelCalls(calls.slice(i));
1048
- this.onEvent({ type: "aborted" });
1049
- return;
1050
- }
1051
- let j = i + 1;
1052
- if (this.isParallelSafe(calls[i])) while (j < calls.length && this.isParallelSafe(calls[j])) j++;
1053
- const batch = calls.slice(i, j);
1054
- const tickets = batch.map(() => ({ abandoned: false, open: false }));
1055
- const running = Promise.all(batch.map((c, k) => this.runTool(c, signal, tickets[k])));
1056
- const outcome2 = await raceAbort(running, signal);
1057
- if (outcome2 === ABORTED) {
1058
- batch.forEach((c, k) => {
1059
- const ticket = tickets[k];
1060
- ticket.abandoned = true;
1061
- if (ticket.open) this.onEvent({ type: "toolEnd", id: c.id, tool: c.name, output: CANCELLED, isError: true });
1062
- });
1063
- running.catch(() => {
1064
- });
1065
- this.cancelCalls(calls.slice(i));
1066
- this.onEvent({ type: "aborted" });
1067
- return;
1068
- }
1069
- batch.forEach((c, k) => {
1070
- roundImages.push(...outcome2[k].images ?? []);
1071
- this.push({
1072
- role: "tool",
1073
- tool_call_id: c.id,
1074
- content: outcome2[k].output,
1075
- ...c.caller ? { responses_caller: c.caller } : {}
1076
- });
1077
- });
1078
- i = j;
1079
- }
1080
- if (roundImages.length) this.push(imageUserMessage(roundImages));
1081
- if (signal.aborted) {
1082
- this.onEvent({ type: "aborted" });
1083
- return;
1084
- }
1085
- }
1086
- if (this.maxSteps !== void 0) this.onEvent({ type: "stepLimit", maxSteps: this.maxSteps });
1087
- } catch (error) {
1088
- if (signal.aborted) {
1089
- this.onEvent({ type: "aborted" });
1090
- return;
1091
- }
1092
- const message = error.message;
1093
- const noTools = error.status === 400 && /tool/i.test(message);
1094
- this.onEvent({
1095
- type: "error",
1096
- message: noTools ? `${message}
1097
- ${t("This model may not support tool calling. Try another model with /model.")}` : message
1098
- });
1099
- }
1100
- }
1101
- /** Current cap status; undefined means the budget is not exhausted. */
1102
- overBudget() {
1103
- return budgetStatus(this.totalUsage, this.opts.usageCap, this.budgetModel);
1104
- }
1105
- /** Every tool call needs an answer in the history, or the next request to the model is rejected. */
1106
- cancelCalls(calls) {
1107
- for (const pending of calls)
1108
- this.push({
1109
- role: "tool",
1110
- tool_call_id: pending.id,
1111
- content: CANCELLED,
1112
- ...pending.caller ? { responses_caller: pending.caller } : {}
1113
- });
1114
- }
1115
- /**
1116
- * Every tool call must resolve with text: a rejected one would leave the assistant tool_calls
1117
- * without answers, and the next request to the model would be rejected until a restart.
1118
- */
1119
- async runTool(call, signal, ticket) {
1120
- const emit = (event) => this.report(ticket, event);
1121
- try {
1122
- return await this.executeTool(call, signal, ticket);
1123
- } catch (error) {
1124
- const message = `Error: ${error?.message || String(error)}`;
1125
- if (ticket.open) {
1126
- emit({ type: "toolEnd", id: call.id, tool: call.name, output: message, isError: true });
1127
- } else {
1128
- emit({ type: "toolStart", id: call.id, tool: call.name, target: "" });
1129
- emit({ type: "toolEnd", id: call.id, tool: call.name, output: message, isError: true });
1130
- }
1131
- return { output: message };
1132
- }
1133
- }
1134
- async executeTool(call, signal, ticket) {
1135
- const emit = (event) => this.report(ticket, event);
1136
- const fail = (output, target2 = "") => {
1137
- emit({ type: "toolStart", id: call.id, tool: call.name, target: target2 });
1138
- emit({ type: "toolEnd", id: call.id, tool: call.name, output, isError: true });
1139
- return { output };
1140
- };
1141
- const tool = this.turnTools.find((t2) => t2.name === call.name);
1142
- if (!tool) return fail(`Tool "${call.name}" does not exist. Available tools: ${this.turnTools.map((t2) => t2.name).join(", ")}`);
1143
- let args;
1144
- try {
1145
- args = JSON.parse(call.arguments || "{}");
1146
- } catch {
1147
- return fail(`Arguments are not valid JSON: ${call.arguments.slice(0, 200)}`);
1148
- }
1149
- const parsed = tool.schema.safeParse(args);
1150
- if (!parsed.success) {
1151
- return fail(`Invalid arguments: ${parsed.error.issues.map((i) => `${i.path.join(".") || "(root)"}: ${i.message}`).join("; ")}`);
1152
- }
1153
- const input = parsed.data;
1154
- const target = tool.target(input);
1155
- const overBudget = tool.name === "fetch" ? this.webBudget.take() : void 0;
1156
- if (overBudget) return fail(overBudget, target);
1157
- await this.opts.pluginEmit?.("PreToolUse", { tool: tool.name, input });
1158
- const pre = await runHooks(this.opts.hooks, "PreToolUse", { tool: tool.name, input }, { env: this.opts.env, cwd: this.opts.cwd });
1159
- if (pre.blocked) return fail(pre.blocked, target);
1160
- const ctx = {
1161
- cwd: this.opts.cwd,
1162
- signal,
1163
- readFiles: this.readFiles,
1164
- callId: call.id,
1165
- emit,
1166
- ask: (req) => this.askPermission(req),
1167
- checkpoint: this.opts.checkpoint,
1168
- refundFetch: () => this.webBudget.refund(),
1169
- addUsage: (u) => {
1170
- this.totalUsage.inputTokens += u.inputTokens;
1171
- this.totalUsage.outputTokens += u.outputTokens;
1172
- this.totalUsage.cachedInputTokens = (this.totalUsage.cachedInputTokens ?? 0) + (u.cachedInputTokens ?? 0);
1173
- this.totalUsage.cacheWriteTokens = (this.totalUsage.cacheWriteTokens ?? 0) + (u.cacheWriteTokens ?? 0);
1174
- this.onEvent({ type: "usage", ...this.totalUsage });
1175
- this.opts.onUsage?.(u);
1176
- }
1177
- };
1178
- emit({ type: "toolStart", id: call.id, tool: tool.name, target });
1179
- const invalid = await tool.validate?.(input, ctx).catch((error) => error.message);
1180
- if (invalid) {
1181
- emit({ type: "toolEnd", id: call.id, tool: tool.name, output: invalid, isError: true });
1182
- return { output: invalid };
1183
- }
1184
- const request = { tool: tool.name, kind: tool.kind, target };
1185
- let decision = this.permissions.check(request);
1186
- if (decision === "ask") {
1187
- const preview = await tool.preview?.(input, ctx).catch(() => void 0);
1188
- const answer = await this.askPermission({
1189
- ...request,
1190
- preview,
1191
- sessionRules: this.permissions.rulesFor(request),
1192
- ...this.opts.label ? { agent: this.opts.label } : {}
1193
- });
1194
- if (answer === "session") this.permissions.allowForSession(request);
1195
- if (answer === "all") this.permissions.mode = "allowAll";
1196
- decision = answer === "no" ? "deny" : "allow";
1197
- }
1198
- if (decision === "deny") {
1199
- 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.";
1200
- emit({ type: "toolEnd", id: call.id, tool: tool.name, output, isError: true });
1201
- return { output };
1202
- }
1203
- let result;
1204
- try {
1205
- result = await tool.run(input, ctx);
1206
- } catch (error) {
1207
- result = { output: `Error: ${error.message}`, isError: true };
1208
- }
1209
- if (tool.name === "fetch" && !result.isError) this.webBudget.record(result.output.length);
1210
- await runHooks(
1211
- this.opts.hooks,
1212
- "PostToolUse",
1213
- { tool: tool.name, input, output: result.output },
1214
- { env: this.opts.env, cwd: this.opts.cwd }
1215
- );
1216
- await this.opts.pluginEmit?.("PostToolUse", { tool: tool.name, input, output: result.output });
1217
- emit({ type: "toolEnd", id: call.id, tool: tool.name, output: result.output, display: result.display, isError: !!result.isError });
1218
- return result;
1219
- }
1220
- };
1221
-
1222
- // src/checkpoints.ts
1223
- import { existsSync as existsSync2, statSync as statSync4 } from "fs";
1224
- import { readFile as readFile5, rm, writeFile as writeFile3 } from "fs/promises";
1225
- var MAX_TURNS = 20;
1226
- var MAX_BYTES = 2 * 1024 * 1024;
1227
- function emptyResult() {
1228
- return { restored: [], deleted: [], skipped: [], failed: [] };
1229
- }
1230
- async function restoreTurn(turn, result) {
1231
- for (const [path, original] of turn) {
1232
- try {
1233
- if (original === "skipped") result.skipped.push(path);
1234
- else if (original === null) {
1235
- await rm(path, { force: true });
1236
- result.deleted.push(path);
1237
- } else {
1238
- await writeFile3(path, original);
1239
- result.restored.push(path);
1240
- }
1241
- } catch {
1242
- result.failed.push(path);
1243
- }
1244
- }
1245
- }
1246
- var CheckpointStore = class {
1247
- history = [];
1248
- current;
1249
- pendingRedo;
1250
- offset = 0;
1251
- beginTurn() {
1252
- this.current = /* @__PURE__ */ new Map();
1253
- this.history.push(this.current);
1254
- this.pendingRedo = void 0;
1255
- }
1256
- /** Call before writing a file; only the first snapshot per file per turn is kept. */
1257
- async snapshot(path) {
1258
- if (!this.current) this.beginTurn();
1259
- const turn = this.current;
1260
- if (turn.has(path)) return;
1261
- if (!existsSync2(path)) turn.set(path, null);
1262
- else if (statSync4(path).size > MAX_BYTES) turn.set(path, "skipped");
1263
- else turn.set(path, await readFile5(path));
1264
- while (this.turns() > MAX_TURNS) {
1265
- this.history.shift();
1266
- this.offset++;
1267
- }
1268
- }
1269
- turns() {
1270
- return this.history.filter((t2) => t2.size).length;
1271
- }
1272
- /** Total entries including empty ones; used to align file snapshots with conversation turns. */
1273
- entries() {
1274
- return this.offset + this.history.length;
1275
- }
1276
- async undo() {
1277
- while (this.history.length && !this.history.at(-1)?.size) this.history.pop();
1278
- const turn = this.history.pop();
1279
- this.current = void 0;
1280
- if (!turn) return void 0;
1281
- const result = emptyResult();
1282
- const redo = /* @__PURE__ */ new Map();
1283
- await this.captureFor(turn, redo);
1284
- await restoreTurn(turn, result);
1285
- this.pendingRedo = redo;
1286
- return result;
1287
- }
1288
- /**
1289
- * Revert every snapshot taken at or after the `keep`-th entry, newest first, so the files
1290
- * end in the state from before the turn that created entry `keep`. Used by /rewind, which
1291
- * pairs conversation turns with the entry count recorded when each turn started.
1292
- */
1293
- async undoTo(keep) {
1294
- const target = Math.max(0, Math.min(keep - this.offset, this.history.length));
1295
- const result = emptyResult();
1296
- const redo = /* @__PURE__ */ new Map();
1297
- let applied = false;
1298
- while (this.history.length > target) {
1299
- const turn = this.history.pop();
1300
- if (turn?.size) {
1301
- await this.captureFor(turn, redo);
1302
- await restoreTurn(turn, result);
1303
- applied = true;
1304
- }
1305
- }
1306
- this.current = void 0;
1307
- this.pendingRedo = applied ? redo : void 0;
1308
- return applied ? result : void 0;
1309
- }
1310
- /**
1311
- * Re-apply the file state captured right before the last undo/undoTo.
1312
- * Only file edits are redone, never conversation history. Invalidated by
1313
- * any new snapshot turn or clear(), so redo can't resurrect stale content.
1314
- */
1315
- async redo() {
1316
- const turn = this.pendingRedo;
1317
- if (!turn) return void 0;
1318
- this.pendingRedo = void 0;
1319
- const undoEntry = /* @__PURE__ */ new Map();
1320
- await this.captureFor(turn, undoEntry);
1321
- const result = emptyResult();
1322
- await restoreTurn(turn, result);
1323
- if (undoEntry.size) this.history.push(undoEntry);
1324
- return result;
1325
- }
1326
- /** Records the on-disk state of a turn's paths so redo() can put it back. */
1327
- async captureFor(turn, into) {
1328
- for (const [path, original] of turn) {
1329
- if (into.has(path)) continue;
1330
- if (original === "skipped") {
1331
- into.set(path, "skipped");
1332
- continue;
1333
- }
1334
- try {
1335
- if (!existsSync2(path)) into.set(path, null);
1336
- else if (statSync4(path).size > MAX_BYTES) into.set(path, "skipped");
1337
- else into.set(path, await readFile5(path));
1338
- } catch {
1339
- into.set(path, "skipped");
1340
- }
1341
- }
1342
- }
1343
- clear() {
1344
- this.history.length = 0;
1345
- this.current = void 0;
1346
- this.pendingRedo = void 0;
1347
- this.offset = 0;
1348
- }
1349
- };
1350
-
1351
- // src/contextWindow.ts
1352
- var DEFAULT_CONTEXT_WINDOW = 128e3;
1353
- var WINDOWS = [
1354
- // BotConnector's Luna request cap (see LUNA_AUTO_COMPACT_THRESHOLD in provider.ts).
1355
- [/gpt-6-luna/i, 272e3],
1356
- [/claude-(opus|sonnet|haiku)-\d/i, 2e5],
1357
- [/gemini-\d/i, 1048576]
1358
- ];
1359
- function contextWindowFor(modelRef, env = process.env) {
1360
- const override = Number(env.BCCLI_CONTEXT_WINDOW);
1361
- if (Number.isInteger(override) && override > 0) return override;
1362
- const model = modelRef.slice(modelRef.indexOf("/") + 1);
1363
- for (const [pattern, window] of WINDOWS) if (pattern.test(model)) return window;
1364
- return DEFAULT_CONTEXT_WINDOW;
1365
- }
1366
-
1367
- // src/mcp/manager.ts
1368
- import { Client } from "@modelcontextprotocol/sdk/client/index.js";
1369
- import { UnauthorizedError } from "@modelcontextprotocol/sdk/client/auth.js";
1370
- import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
1371
- import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
1372
- import { z as z7 } from "zod";
1373
- function formatMcpContent(content) {
1374
- const parts = [];
1375
- const images = [];
1376
- for (const c of content ?? []) {
1377
- if (c.type === "text") {
1378
- parts.push(c.text ?? "");
1379
- } else if (c.type === "image" && c.data) {
1380
- const mediaType = c.mimeType || "image/png";
1381
- images.push({ mediaType, data: c.data, path: "mcp:image" });
1382
- parts.push(`[image attached: ${mediaType}]`);
1383
- } else if (c.type === "resource" && c.resource?.text) {
1384
- parts.push(c.resource.uri ? `[${c.resource.uri}]
1385
- ${c.resource.text}` : c.resource.text);
1386
- } else {
1387
- parts.push(`[${c.type}]`);
1388
- }
1389
- }
1390
- return {
1391
- output: parts.join("\n") || "(empty)",
1392
- ...images.length ? { images } : {}
1393
- };
1394
- }
1395
- var CHILD_ENV_KEYS = [
1396
- // portable
1397
- "PATH",
1398
- "HOME",
1399
- "USERPROFILE",
1400
- "LANG",
1401
- "LC_ALL",
1402
- "TZ",
1403
- "TMP",
1404
- "TEMP",
1405
- "TMPDIR",
1406
- // windows
1407
- "SYSTEMROOT",
1408
- "SYSTEMDRIVE",
1409
- "COMSPEC",
1410
- "PATHEXT",
1411
- "APPDATA",
1412
- "LOCALAPPDATA",
1413
- "HOMEDRIVE",
1414
- "HOMEPATH",
1415
- "USERNAME",
1416
- "PROGRAMFILES",
1417
- "PROCESSOR_ARCHITECTURE",
1418
- // posix
1419
- "LOGNAME",
1420
- "SHELL",
1421
- "TERM",
1422
- "USER"
1423
- ];
1424
- function mcpChildEnv(extra) {
1425
- const env = {};
1426
- for (const key of CHILD_ENV_KEYS) {
1427
- const value = process.env[key];
1428
- if (value !== void 0) env[key] = value;
1429
- }
1430
- return { ...env, ...extra };
1431
- }
1432
- function mcpToolName(server, tool, taken) {
1433
- const clean = (s) => s.replace(/[^A-Za-z0-9_-]+/g, "_");
1434
- const base = `mcp__${clean(server)}__${clean(tool)}`.slice(0, 64);
1435
- let name = base;
1436
- for (let n = 2; taken.has(name); n++) name = `${base.slice(0, 64 - String(n).length - 1)}_${n}`;
1437
- taken.add(name);
1438
- return name;
1439
- }
1440
- function withTimeout(promise, ms) {
1441
- let timer;
1442
- const timeout = new Promise((_, reject) => {
1443
- timer = setTimeout(() => reject(new Error(t("timeout after {n} seconds", { n: Math.round(ms / 1e3) }))), ms);
1444
- });
1445
- return Promise.race([promise, timeout]).finally(() => clearTimeout(timer));
1446
- }
1447
- var McpManager = class {
1448
- constructor(opts = {}) {
1449
- this.opts = opts;
1450
- }
1451
- opts;
1452
- servers = /* @__PURE__ */ new Map();
1453
- taken = /* @__PURE__ */ new Set();
1454
- async start(specs) {
1455
- await Promise.all(specs.map((spec) => this.add(spec)));
1456
- }
1457
- async add(spec) {
1458
- await this.remove(spec.name);
1459
- const entry = { spec, state: { name: spec.name, source: spec.source, status: "starting", tools: 0 }, tools: [] };
1460
- this.servers.set(spec.name, entry);
1461
- this.opts.onChange?.();
1462
- const client = new Client({ name: "bccli", version: "0.3.0" });
1463
- entry.client = client;
1464
- const timeoutMs = this.opts.connectTimeoutMs ?? 3e4;
1465
- try {
1466
- const config = expandEnvVars(spec.config, process.env);
1467
- let transport;
1468
- if (config.type === "http") {
1469
- 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;
1470
- transport = new StreamableHTTPClientTransport(new URL(config.url), {
1471
- requestInit: { headers: config.headers ?? {} },
1472
- ...oauth ? { authProvider: oauth } : {}
1473
- });
1474
- } else {
1475
- transport = new StdioClientTransport({
1476
- command: config.command,
1477
- args: config.args ?? [],
1478
- env: mcpChildEnv(config.env),
1479
- stderr: "ignore"
1480
- });
1481
- }
1482
- await withTimeout(client.connect(transport), timeoutMs);
1483
- const { tools } = await withTimeout(client.listTools(), timeoutMs);
1484
- if (this.servers.get(spec.name) !== entry) {
1485
- await client.close().catch(() => {
1486
- });
1487
- return;
1488
- }
1489
- entry.tools = tools.map((t2) => this.wrap(spec.name, client, t2));
1490
- entry.state = { ...entry.state, status: "ready", tools: entry.tools.length };
1491
- } catch (error) {
1492
- if (error instanceof UnauthorizedError) {
1493
- entry.state = {
1494
- ...entry.state,
1495
- status: "error",
1496
- error: t("OAuth required for {name}: run bccli mcp auth {name}", { name: spec.name })
1497
- };
1498
- } else {
1499
- entry.state = { ...entry.state, status: "error", error: error.message };
1500
- }
1501
- await client.close().catch(() => {
1502
- });
1503
- if (this.servers.get(spec.name) !== entry) return;
1504
- }
1505
- this.opts.onChange?.();
1506
- }
1507
- wrap(server, client, tool) {
1508
- return {
1509
- name: mcpToolName(server, tool.name, this.taken),
1510
- description: `[MCP ${server}] ${tool.description ?? tool.name}`.slice(0, 1024),
1511
- schema: z7.record(z7.string(), z7.unknown()),
1512
- jsonSchema: tool.inputSchema,
1513
- ...tool.outputSchema ? { outputJsonSchema: tool.outputSchema } : {},
1514
- kind: "mcp",
1515
- programmaticSafe: tool.annotations?.readOnlyHint === true,
1516
- target: (input) => {
1517
- const text = JSON.stringify(input);
1518
- return text.length > 80 ? `${text.slice(0, 77)}\u2026` : text;
1519
- },
1520
- async run(input, ctx) {
1521
- try {
1522
- const result = await client.callTool({ name: tool.name, arguments: input }, void 0, { signal: ctx.signal });
1523
- const formatted = formatMcpContent(result.content);
1524
- const output = formatted.output;
1525
- return {
1526
- output: output.length > 32e3 ? `${output.slice(0, 32e3)}
1527
- \u2026 [truncated at 32000 characters; narrow the MCP request if possible]` : output,
1528
- isError: !!result.isError,
1529
- ...formatted.images ? { images: formatted.images } : {}
1530
- };
1531
- } catch (error) {
1532
- return { output: `Server MCP ${server} error: ${error.message}`, isError: true };
1533
- }
1534
- }
1535
- };
1536
- }
1537
- async remove(name) {
1538
- const entry = this.servers.get(name);
1539
- if (!entry) return;
1540
- this.servers.delete(name);
1541
- for (const t2 of entry.tools) this.taken.delete(t2.name);
1542
- if (entry.tools.length) this.opts.onToolsRemoved?.(entry.tools.map((t2) => t2.name));
1543
- await entry.client?.close().catch(() => {
1544
- });
1545
- this.opts.onChange?.();
1546
- }
1547
- tools() {
1548
- return [...this.servers.values()].flatMap((s) => s.tools).sort((a, b) => a.name.localeCompare(b.name));
1549
- }
1550
- states() {
1551
- return [...this.servers.values()].map((s) => s.state);
1552
- }
1553
- async stop() {
1554
- await Promise.all([...this.servers.keys()].map((name) => this.remove(name)));
1555
- }
1556
- };
1557
-
1558
- // src/setup.ts
1559
- import { homedir } from "os";
1560
-
1561
- // src/tools/plan.ts
1562
- import { z as z8 } from "zod";
1563
- function createExitPlanTool(opts) {
1564
- return defineTool({
1565
- name: "exit_plan",
1566
- description: "Plan mode only: present the finished plan to the user for approval. If approved you may start implementing it.",
1567
- schema: z8.object({ plan: z8.string().describe("The plan, in markdown") }),
1568
- kind: "read",
1569
- target: () => t("plan"),
1570
- async run(input) {
1571
- if (opts.permissions.mode !== "plan") return { output: "exit_plan is only used in plan mode.", isError: true };
1572
- const decision = await opts.interaction.approvePlan(input.plan);
1573
- if (decision === "no") {
1574
- return { output: "The user has not approved yet. Ask what needs to change, revise the plan, then call exit_plan again." };
1575
- }
1576
- opts.permissions.mode = decision;
1577
- return { output: `Plan approved. The mode is now ${decision}. Start carrying out the plan now.` };
1578
- }
1579
- });
1580
- }
1581
-
1582
- // src/tools/skill.ts
1583
- import { readFileSync } from "fs";
1584
- import { z as z9 } from "zod";
1585
- function createSkillTool(skills) {
1586
- return defineTool({
1587
- name: "skill",
1588
- description: "Load a skill (instructions for a kind of task) by name. Use it when a listed skill matches the task, then follow it.",
1589
- schema: z9.object({ name: z9.string().describe("Skill name from the list in the system prompt") }),
1590
- kind: "read",
1591
- target: (input) => input.name,
1592
- async run(input) {
1593
- const skill = skills.find((s) => s.name === input.name);
1594
- if (!skill) return { output: `Skill "${input.name}" does not exist. Available: ${skills.map((s) => s.name).join(", ") || "(none)"}`, isError: true };
1595
- const { body } = parseFrontmatter(readFileSync(skill.file, "utf8"));
1596
- return { output: `Base directory for this skill: ${skill.dir}
1597
-
1598
- ${body}`, display: skill.description.slice(0, 60) };
1599
- }
1600
- });
1601
- }
1602
-
1603
- // src/tools/task.ts
1604
- import { z as z10 } from "zod";
1605
- function createTaskTool(opts) {
1606
- const byName = new Map(opts.agents.map((a) => [a.name, a]));
1607
- const list = opts.agents.map((a) => `- ${a.name}: ${a.description}`).join("\n");
1608
- const sessions = /* @__PURE__ */ new Map();
1609
- let askChain = Promise.resolve();
1610
- const enqueueAsk = (fn) => {
1611
- const next = askChain.then(
1612
- () => fn(),
1613
- () => fn()
1614
- );
1615
- askChain = next.then(
1616
- () => void 0,
1617
- () => void 0
1618
- );
1619
- return next;
1620
- };
1621
- return defineTool({
1622
- name: "task",
1623
- description: `Delegate a self-contained sub-task to a subagent. It starts with an empty context and returns only its final report. Available agents:
1624
- ${list}`,
1625
- schema: z10.object({
1626
- agent: z10.string().describe("Agent name"),
1627
- description: z10.string().describe("3-6 word summary shown to the user"),
1628
- prompt: z10.string().describe("Full instructions; the subagent sees nothing else"),
1629
- session_id: z10.string().optional().describe("Optional session id to continue a previous subagent conversation")
1630
- }),
1631
- kind: "read",
1632
- target: (input) => `[${input.agent}] ${input.description}`,
1633
- // parallel: true opts a write-capable agent in; otherwise only all-read agents run concurrently
1634
- // (a project file may redefine "explore").
1635
- parallelSafe: (input) => {
1636
- const def = byName.get(input.agent);
1637
- if (def?.parallel) return true;
1638
- if (!def?.tools) return false;
1639
- const base = opts.baseTools();
1640
- return def.tools.every((name) => base.find((t2) => t2.name === name)?.kind === "read");
1641
- },
1642
- async run(input, ctx) {
1643
- const def = byName.get(input.agent);
1644
- if (!def) return { output: `Agent "${input.agent}" does not exist. Available: ${[...byName.keys()].join(", ")}`, isError: true };
1645
- const base = opts.baseTools().filter((t2) => t2.name !== "task" && t2.name !== "exit_plan");
1646
- const unknown = (def.tools ?? []).filter((n) => !base.some((t2) => t2.name === n));
1647
- if (unknown.length) return { output: `Agent "${def.name}": unknown tools: ${unknown.join(", ")}`, isError: true };
1648
- const allowed = def.tools;
1649
- const tools = allowed ? base.filter((t2) => allowed.includes(t2.name)) : base;
1650
- let provider;
1651
- try {
1652
- provider = def.model ? opts.providerFor(def.model) : opts.provider();
1653
- } catch (error) {
1654
- return { output: `Agent "${def.name}": ${error.message}`, isError: true };
1655
- }
1656
- const priorHistory = input.session_id ? sessions.get(input.session_id) : void 0;
1657
- const child = new Agent({
1658
- provider,
1659
- tools,
1660
- permissions: opts.permissions,
1661
- systemPrompt: `${typeof opts.systemPrompt === "function" ? opts.systemPrompt(def.model) : opts.systemPrompt}
1662
-
1663
- # Your role
1664
- ${def.prompt}`,
1665
- cwd: opts.cwd,
1666
- ...priorHistory ? { history: priorHistory } : {},
1667
- // Every subagent runs with a step cap so a looping child cannot burn tokens forever.
1668
- maxSteps: def.maxSteps ?? 50,
1669
- label: def.name,
1670
- reasoning: opts.reasoning?.() ?? "auto",
1671
- hooks: opts.hooks,
1672
- pluginEmit: opts.pluginEmit,
1673
- env: opts.env,
1674
- // Subagent edits belong to the parent's turn so /undo reverts them too.
1675
- checkpoint: ctx.checkpoint
1676
- });
1677
- let outcome;
1678
- let steps = 0;
1679
- child.onEvent = (event) => {
1680
- if (event.type === "toolStart") steps++;
1681
- if (event.type === "error" || event.type === "stepLimit" || event.type === "aborted" || event.type === "done") outcome = event;
1682
- if (event.type !== "usage" && ctx.callId) ctx.emit?.({ type: "subagent", parentId: ctx.callId, agent: def.name, event });
1683
- };
1684
- const ask = ctx.ask;
1685
- if (ask) child.askPermission = (req) => enqueueAsk(() => ask(req));
1686
- await child.run(input.prompt, ctx.signal);
1687
- if (input.session_id) sessions.set(input.session_id, [...child.messages]);
1688
- ctx.addUsage?.(child.totalUsage);
1689
- const tokens2 = child.totalUsage.inputTokens + child.totalUsage.outputTokens;
1690
- const display = t("{steps} steps \xB7 {tokens} tokens", { steps, tokens: tokens2 >= 1e3 ? `${Math.round(tokens2 / 1e3)}k` : tokens2 });
1691
- if (outcome?.type === "error") return { output: `Subagent ${def.name} failed: ${outcome.message}`, isError: true, display };
1692
- if (outcome?.type === "aborted") return { output: `Subagent ${def.name} was cancelled.`, isError: true, display };
1693
- let last = [...child.messages].reverse().find((m) => m.role === "assistant" && m.content);
1694
- if (!last && outcome?.type === "stepLimit" && !ctx.signal.aborted) {
1695
- try {
1696
- const synthesis = await provider.chat({
1697
- messages: [
1698
- ...child.messages,
1699
- { role: "user", content: "Step limit reached. Summarize all findings gathered so far concisely." }
1700
- ],
1701
- signal: ctx.signal,
1702
- reasoning: opts.reasoning?.() ?? "auto"
1703
- });
1704
- if (synthesis.text.trim()) {
1705
- last = { role: "assistant", content: synthesis.text.trim() };
1706
- }
1707
- } catch {
1708
- }
1709
- }
1710
- const report = last && typeof last.content === "string" ? last.content : "(no report)";
1711
- const suffix = outcome?.type === "stepLimit" ? `
1712
-
1713
- (The subagent stopped at its ${outcome.maxSteps}-step limit.)` : "";
1714
- return { output: report + suffix, display };
1715
- }
1716
- });
1717
- }
1718
-
1719
- // src/tools/todo.ts
1720
- import { z as z11 } from "zod";
1721
- var TodoStore = class {
1722
- items = [];
1723
- listeners = /* @__PURE__ */ new Set();
1724
- set(items) {
1725
- this.items = items;
1726
- for (const fn of this.listeners) fn(items);
1727
- }
1728
- subscribe(fn) {
1729
- this.listeners.add(fn);
1730
- return () => this.listeners.delete(fn);
1731
- }
1732
- };
1733
- function createTodoTool(store) {
1734
- return defineTool({
1735
- name: "todo_write",
1736
- 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.",
1737
- schema: z11.object({
1738
- todos: z11.array(z11.object({ content: z11.string(), status: z11.enum(["pending", "in_progress", "completed"]) }))
1739
- }),
1740
- kind: "read",
1741
- target: (input) => `${input.todos.length} item`,
1742
- async run(input) {
1743
- if (input.todos.filter((t2) => t2.status === "in_progress").length > 1) {
1744
- return { output: "Only one item may be in_progress.", isError: true };
1745
- }
1746
- store.set(input.todos);
1747
- const done = input.todos.filter((t2) => t2.status === "completed").length;
1748
- return { output: `Todo updated: ${done}/${input.todos.length} completed.` };
1749
- }
1750
- });
1751
- }
1752
-
1753
- // src/tools/memory.ts
1754
- import { existsSync as existsSync3, readFileSync as readFileSync2 } from "fs";
1755
- import { join as join2 } from "path";
1756
- import { z as z12 } from "zod";
1757
- function createSaveMemoryTool(opts) {
1758
- return defineTool({
1759
- name: "save_memory",
1760
- 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.',
1761
- schema: z12.object({
1762
- content: z12.string().min(1).max(500).describe("One short line, without a leading bullet (it is added for you)"),
1763
- scope: z12.enum(["project", "global"]).optional().describe("project (default) or global")
1764
- }),
1765
- kind: "edit",
1766
- // The target is what the prompt shows and what the permission model judges: the
1767
- // global store really lives outside the project, so it must not look like one.
1768
- target: (input) => input.scope === "global" ? join2(opts.home, "BCCLI.md") : "AGENTS.md",
1769
- async run(input, ctx) {
1770
- const content = input.content.trim();
1771
- if (!content) return { output: "Memory content is empty.", isError: true };
1772
- const file = input.scope === "global" ? join2(opts.home, "BCCLI.md") : join2(ctx.cwd, "AGENTS.md");
1773
- if (existsSync3(file)) {
1774
- const lines = readFileSync2(file, "utf8").split("\n");
1775
- if (lines.some((line) => line.trim() === `- ${content}`)) {
1776
- return { output: `Already saved to ${file}.` };
1777
- }
1778
- }
1779
- appendMemory(file, content);
1780
- opts.onSaved?.();
1781
- return { output: `Saved to ${file}.` };
1782
- }
1783
- });
1784
- }
1785
-
1786
- // src/tools/repomap.ts
1787
- import { z as z13 } from "zod";
1788
-
1789
- // src/repomap.ts
1790
- import { readdirSync as readdirSync2, readFileSync as readFileSync3 } from "fs";
1791
- import { extname, join as join3, relative, sep } from "path";
1792
- var SKIP_DIRS = /* @__PURE__ */ new Set([
1793
- "node_modules",
1794
- ".git",
1795
- "dist",
1796
- "build",
1797
- "out",
1798
- "coverage",
1799
- ".next",
1800
- ".turbo",
1801
- "target",
1802
- "vendor",
1803
- "__pycache__",
1804
- "venv",
1805
- ".venv",
1806
- ".cache"
1807
- ]);
1808
- var CODE_EXTS = /* @__PURE__ */ new Set([
1809
- ".ts",
1810
- ".tsx",
1811
- ".js",
1812
- ".jsx",
1813
- ".mjs",
1814
- ".cjs",
1815
- ".py",
1816
- ".go",
1817
- ".rs",
1818
- ".java",
1819
- ".c",
1820
- ".h",
1821
- ".cpp",
1822
- ".hpp",
1823
- ".cs",
1824
- ".rb",
1825
- ".php",
1826
- ".sh",
1827
- ".ps1"
1828
- ]);
1829
- var MAX_FILES = 400;
1830
- var MAX_SCAN_FILES = 5e3;
1831
- var MAX_FILE_BYTES = 2e5;
1832
- var MAX_SYMBOLS_PER_FILE = 12;
1833
- var FAMILY = {
1834
- ts: [
1835
- /^\s*(?:export\s+)?(?:default\s+)?(?:declare\s+)?(?:public\s+|private\s+|protected\s+|internal\s+)?(?:static\s+)?(?:abstract\s+|final\s+|sealed\s+)?(?:async\s+)?(?:function|class|interface|type|enum)\s+([A-Za-z0-9_$]+)/,
1836
- /^\s*export\s+(?:const|let)\s+([A-Za-z0-9_$]+)/
1837
- ],
1838
- py: [/^\s*(?:async\s+)?def\s+(\w+)/, /^\s*(?:module|class)\s+(\w+)/],
1839
- go: [/^func\s+(?:\([^)]*\)\s*)?(\w+)/, /^type\s+(\w+)/],
1840
- rs: [/^\s*(?:pub(?:\([^)]*\))?\s+)?(?:unsafe\s+)?(?:async\s+)?(?:fn|struct|enum|trait|type)\s+(\w+)/],
1841
- c: [/^\s*(?:static\s+|inline\s+|extern\s+)*[A-Za-z_][A-Za-z0-9_\s*]*\b(\w+)\s*\([^;]*\)\s*\{?\s*$/]
1842
- };
1843
- function familyFor(ext) {
1844
- if ([".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs", ".java", ".cs", ".php"].includes(ext)) return FAMILY.ts;
1845
- if (ext === ".py" || ext === ".rb" || ext === ".sh" || ext === ".ps1") return FAMILY.py;
1846
- if (ext === ".go") return FAMILY.go;
1847
- if (ext === ".rs") return FAMILY.rs;
1848
- return FAMILY.c;
1849
- }
1850
- function extractSymbols(text, ext) {
1851
- const out = [];
1852
- for (const line of text.split("\n")) {
1853
- for (const re of familyFor(ext)) {
1854
- const m = re.exec(line);
1855
- if (m) {
1856
- if (!out.includes(m[1])) out.push(m[1]);
1857
- break;
1858
- }
1859
- }
1860
- if (out.length >= MAX_SYMBOLS_PER_FILE) break;
1861
- }
1862
- return out;
1863
- }
1864
- function walk(dir, root, files) {
1865
- if (files.length >= MAX_SCAN_FILES) return;
1866
- let entries;
1867
- try {
1868
- entries = readdirSync2(dir, { withFileTypes: true });
1869
- } catch {
1870
- return;
1871
- }
1872
- for (const e of entries.sort((a, b) => a.name.localeCompare(b.name))) {
1873
- if (files.length >= MAX_SCAN_FILES) return;
1874
- const full = join3(dir, e.name);
1875
- if (e.isDirectory()) {
1876
- if (SKIP_DIRS.has(e.name) || e.name.startsWith(".") && full !== root) continue;
1877
- walk(full, root, files);
1878
- } else if (e.isFile() && CODE_EXTS.has(extname(e.name).toLowerCase())) {
1879
- files.push(full);
1880
- }
1881
- }
1882
- }
1883
- function buildRepoMapSync(cwd, opts = {}) {
1884
- const maxTokens = opts.maxTokens ?? 2048;
1885
- const scanned = [];
1886
- walk(cwd, cwd, scanned);
1887
- scanned.sort(
1888
- (a, b) => relative(cwd, a).split(sep).length - relative(cwd, b).split(sep).length || a.localeCompare(b)
1889
- );
1890
- const files = scanned.slice(0, MAX_FILES);
1891
- const lines = [];
1892
- let used = 0;
1893
- let truncated = false;
1894
- for (const file of files) {
1895
- const rel = relative(cwd, file).split(sep).join("/");
1896
- let symbols = [];
1897
- try {
1898
- const text = readFileSync3(file, "utf8");
1899
- if (text.length <= MAX_FILE_BYTES) symbols = extractSymbols(text, extname(file).toLowerCase());
1900
- } catch {
1901
- symbols = [];
1902
- }
1903
- const line = symbols.length ? `${rel}: ${symbols.join(", ")}` : rel;
1904
- const cost = estimateTokens(line);
1905
- if (used + cost > maxTokens) {
1906
- truncated = true;
1907
- break;
1908
- }
1909
- used += cost;
1910
- lines.push(line);
1911
- }
1912
- if (truncated) lines.push(`\u2026 truncated (repo map limited to ${maxTokens} tokens)`);
1913
- return lines.join("\n");
1914
- }
1915
-
1916
- // src/tools/repomap.ts
1917
- function createRepoMapTool() {
1918
- return defineTool({
1919
- name: "repo_map",
1920
- 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.",
1921
- schema: z13.object({}),
1922
- kind: "read",
1923
- target: () => "(repo)",
1924
- programmaticSafe: true,
1925
- run: async (_input, ctx) => ({ output: buildRepoMapSync(ctx.cwd) || "(no code files found)" })
1926
- });
1927
- }
1928
-
1929
- // src/tools/diagnostics.ts
1930
- import { extname as extname3, relative as relative2 } from "path";
1931
- import { z as z14 } from "zod";
1932
-
1933
- // src/lsp.ts
1934
- import { spawn } from "child_process";
1935
- import { readFileSync as readFileSync4 } from "fs";
1936
- import { extname as extname2 } from "path";
1937
- import { fileURLToPath, pathToFileURL } from "url";
1938
- var LANGUAGE_IDS = {
1939
- ".ts": "typescript",
1940
- ".tsx": "typescriptreact",
1941
- ".mts": "typescript",
1942
- ".cts": "typescript",
1943
- ".js": "javascript",
1944
- ".jsx": "javascriptreact",
1945
- ".mjs": "javascript",
1946
- ".cjs": "javascript",
1947
- ".py": "python",
1948
- ".rs": "rust",
1949
- ".go": "go",
1950
- ".java": "java",
1951
- ".c": "c",
1952
- ".h": "c",
1953
- ".cpp": "cpp",
1954
- ".hpp": "cpp",
1955
- ".cs": "csharp",
1956
- ".rb": "ruby",
1957
- ".php": "php",
1958
- ".json": "json",
1959
- ".md": "markdown",
1960
- ".sh": "shellscript",
1961
- ".ps1": "powershell",
1962
- ".yaml": "yaml",
1963
- ".yml": "yaml",
1964
- ".toml": "toml",
1965
- ".html": "html",
1966
- ".css": "css"
1967
- };
1968
- function languageIdFor(file) {
1969
- return LANGUAGE_IDS[extname2(file).toLowerCase()] ?? "plaintext";
1970
- }
1971
- function useShellForLsp(command, platform = process.platform) {
1972
- return platform === "win32" && !/\.(exe|mjs|cjs|js)$/i.test(command.trim());
1973
- }
1974
- function encodeLsp(msg) {
1975
- const body = Buffer.from(JSON.stringify(msg), "utf8");
1976
- return Buffer.concat([Buffer.from(`Content-Length: ${body.length}\r
1977
- \r
1978
- `, "ascii"), body]);
1979
- }
1980
- function createLspParser(onMessage) {
1981
- let buf = Buffer.alloc(0);
1982
- return {
1983
- push(chunk) {
1984
- buf = Buffer.concat([buf, chunk]);
1985
- for (; ; ) {
1986
- const idx = buf.indexOf("\r\n\r\n");
1987
- if (idx < 0) return;
1988
- const m = /Content-Length:\s*(\d+)/i.exec(buf.subarray(0, idx).toString("ascii"));
1989
- if (!m) {
1990
- buf = buf.subarray(idx + 4);
1991
- continue;
1992
- }
1993
- const len = Number(m[1]);
1994
- if (buf.length < idx + 4 + len) return;
1995
- const body = buf.subarray(idx + 4, idx + 4 + len);
1996
- buf = buf.subarray(idx + 4 + len);
1997
- try {
1998
- onMessage(JSON.parse(body.toString("utf8")));
1999
- } catch {
2000
- }
2001
- }
2002
- }
2003
- };
2004
- }
2005
- function extractHoverText(contents) {
2006
- if (!contents) return "";
2007
- if (typeof contents === "string") return contents.trim();
2008
- if (Array.isArray(contents)) return contents.map(extractHoverText).filter(Boolean).join("\n\n");
2009
- if (typeof contents === "object" && "value" in contents && typeof contents.value === "string") {
2010
- return contents.value.trim();
2011
- }
2012
- return "";
2013
- }
2014
- function normalizeLocations(raw) {
2015
- const items = Array.isArray(raw) ? raw : raw ? [raw] : [];
2016
- const out = [];
2017
- for (const item of items) {
2018
- if (!item || typeof item !== "object") continue;
2019
- const rec = item;
2020
- const uri = rec.uri ?? rec.targetUri;
2021
- const start = rec.range?.start ?? rec.targetSelectionRange?.start ?? rec.targetRange?.start;
2022
- if (!uri) continue;
2023
- let path = uri;
2024
- try {
2025
- if (uri.startsWith("file:")) path = fileURLToPath(uri);
2026
- } catch {
2027
- }
2028
- out.push({
2029
- uri,
2030
- path,
2031
- line: start?.line ?? 0,
2032
- character: start?.character ?? 0
2033
- });
2034
- }
2035
- return out;
2036
- }
2037
- function createWarmLspClient(opts) {
2038
- let child;
2039
- let initPromise;
2040
- let nextId = 2;
2041
- const opened = /* @__PURE__ */ new Map();
2042
- const pendingRpc = /* @__PURE__ */ new Map();
2043
- const diagWaiters = /* @__PURE__ */ new Map();
2044
- const send = (msg) => {
2045
- try {
2046
- child?.stdin?.write(encodeLsp(msg));
2047
- } catch {
2048
- }
2049
- };
2050
- const resetState = () => {
2051
- child = void 0;
2052
- initPromise = void 0;
2053
- opened.clear();
2054
- for (const [, resolve] of pendingRpc) resolve(void 0);
2055
- pendingRpc.clear();
2056
- for (const [, waiters] of diagWaiters) {
2057
- for (const w of waiters) w([]);
2058
- }
2059
- diagWaiters.clear();
2060
- };
2061
- const ensureStarted = () => {
2062
- if (initPromise && child && !child.killed) return initPromise;
2063
- opened.clear();
2064
- initPromise = new Promise((resolveInit) => {
2065
- const proc = spawn(opts.server.command, opts.server.args ?? [], {
2066
- cwd: opts.cwd,
2067
- stdio: ["pipe", "pipe", "ignore"],
2068
- shell: useShellForLsp(opts.server.command)
2069
- });
2070
- child = proc;
2071
- let initSettled = false;
2072
- const settleInit = () => {
2073
- if (!initSettled) {
2074
- initSettled = true;
2075
- resolveInit();
2076
- }
2077
- };
2078
- const parser = createLspParser((msg) => {
2079
- if (msg.id === 1 && msg.result !== void 0) {
2080
- send({ jsonrpc: "2.0", method: "initialized", params: {} });
2081
- settleInit();
2082
- return;
2083
- }
2084
- if (msg.id !== void 0 && pendingRpc.has(msg.id)) {
2085
- const fn = pendingRpc.get(msg.id);
2086
- pendingRpc.delete(msg.id);
2087
- fn?.(msg.result);
2088
- return;
2089
- }
2090
- if (msg.method === "textDocument/publishDiagnostics" && msg.params?.uri) {
2091
- const uri = msg.params.uri;
2092
- const diags = (msg.params.diagnostics ?? []).map((d) => ({
2093
- line: d.range?.start?.line ?? 0,
2094
- character: d.range?.start?.character ?? 0,
2095
- severity: d.severity ?? 1,
2096
- message: String(d.message ?? "")
2097
- }));
2098
- const list = diagWaiters.get(uri);
2099
- if (list?.length) {
2100
- diagWaiters.delete(uri);
2101
- for (const w of list) w(diags);
2102
- }
2103
- }
2104
- });
2105
- proc.stdout?.on("data", (c) => parser.push(c));
2106
- proc.on("error", () => {
2107
- resetState();
2108
- settleInit();
2109
- });
2110
- proc.on("close", () => {
2111
- resetState();
2112
- settleInit();
2113
- });
2114
- send({
2115
- jsonrpc: "2.0",
2116
- id: 1,
2117
- method: "initialize",
2118
- params: { processId: process.pid, capabilities: {}, rootUri: pathToFileURL(opts.cwd).href }
2119
- });
2120
- });
2121
- return initPromise;
2122
- };
2123
- const syncDocument = (file) => {
2124
- const uri = pathToFileURL(file).href;
2125
- let text = "";
2126
- try {
2127
- text = readFileSync4(file, "utf8");
2128
- } catch {
2129
- }
2130
- const prevVersion = opened.get(uri);
2131
- if (prevVersion === void 0) {
2132
- opened.set(uri, 1);
2133
- send({
2134
- jsonrpc: "2.0",
2135
- method: "textDocument/didOpen",
2136
- params: { textDocument: { uri, languageId: languageIdFor(file), version: 1, text } }
2137
- });
2138
- } else {
2139
- const version = prevVersion + 1;
2140
- opened.set(uri, version);
2141
- send({
2142
- jsonrpc: "2.0",
2143
- method: "textDocument/didChange",
2144
- params: {
2145
- textDocument: { uri, version },
2146
- contentChanges: [{ text }]
2147
- }
2148
- });
2149
- }
2150
- return uri;
2151
- };
2152
- const request = (method, params, timeoutMs = opts.timeoutMs ?? 8e3) => new Promise((resolve) => {
2153
- const id = nextId++;
2154
- const timer = setTimeout(() => {
2155
- pendingRpc.delete(id);
2156
- resolve(void 0);
2157
- }, timeoutMs);
2158
- pendingRpc.set(id, (res) => {
2159
- clearTimeout(timer);
2160
- resolve(res);
2161
- });
2162
- send({ jsonrpc: "2.0", id, method, params });
2163
- });
2164
- return {
2165
- async diagnostics(file, timeoutMs = opts.timeoutMs ?? 8e3) {
2166
- await ensureStarted();
2167
- if (!child) return [];
2168
- const uri = pathToFileURL(file).href;
2169
- return new Promise((resolve) => {
2170
- const timer = setTimeout(() => {
2171
- const list2 = diagWaiters.get(uri) ?? [];
2172
- diagWaiters.set(
2173
- uri,
2174
- list2.filter((w) => w !== onDiag)
2175
- );
2176
- resolve([]);
2177
- }, timeoutMs);
2178
- const onDiag = (diags) => {
2179
- clearTimeout(timer);
2180
- resolve(diags);
2181
- };
2182
- const list = diagWaiters.get(uri) ?? [];
2183
- list.push(onDiag);
2184
- diagWaiters.set(uri, list);
2185
- syncDocument(file);
2186
- });
2187
- },
2188
- async hover(file, line, character, timeoutMs) {
2189
- await ensureStarted();
2190
- if (!child) return "";
2191
- const uri = syncDocument(file);
2192
- const res = await request("textDocument/hover", { textDocument: { uri }, position: { line, character } }, timeoutMs);
2193
- return extractHoverText(res?.contents);
2194
- },
2195
- async definition(file, line, character, timeoutMs) {
2196
- await ensureStarted();
2197
- if (!child) return [];
2198
- const uri = syncDocument(file);
2199
- const res = await request("textDocument/definition", { textDocument: { uri }, position: { line, character } }, timeoutMs);
2200
- return normalizeLocations(res);
2201
- },
2202
- async references(file, line, character, timeoutMs) {
2203
- await ensureStarted();
2204
- if (!child) return [];
2205
- const uri = syncDocument(file);
2206
- const res = await request(
2207
- "textDocument/references",
2208
- { textDocument: { uri }, position: { line, character }, context: { includeDeclaration: true } },
2209
- timeoutMs
2210
- );
2211
- return normalizeLocations(res);
2212
- },
2213
- async close() {
2214
- if (!child) return;
2215
- const proc = child;
2216
- send({ jsonrpc: "2.0", id: nextId++, method: "shutdown" });
2217
- send({ jsonrpc: "2.0", method: "exit" });
2218
- resetState();
2219
- const killer = setTimeout(() => {
2220
- try {
2221
- proc.kill();
2222
- } catch {
2223
- }
2224
- }, 200);
2225
- killer.unref?.();
2226
- }
2227
- };
2228
- }
2229
-
2230
- // src/tools/diagnostics.ts
2231
- var SEVERITY = { 1: "error", 2: "warning", 3: "info", 4: "hint" };
2232
- var SCHEMA = z14.object({
2233
- path: z14.string().describe("File to check (absolute or relative to the working directory)"),
2234
- action: z14.enum(["diagnostics", "hover", "definition", "references"]).optional().describe("LSP query action, default diagnostics"),
2235
- line: z14.number().int().min(1).optional().describe("1-based line number for hover, definition, or references"),
2236
- character: z14.number().int().min(1).optional().describe("1-based column number for hover, definition, or references")
2237
- });
2238
- function createDiagnosticsTool(servers) {
2239
- const warmPool = /* @__PURE__ */ new Map();
2240
- const clientFor = (server, cwd) => {
2241
- const key = `${cwd}::${server.command}::${(server.args ?? []).join("\0")}`;
2242
- let client = warmPool.get(key);
2243
- if (!client) {
2244
- client = createWarmLspClient({ server, cwd, timeoutMs: 15e3 });
2245
- warmPool.set(key, client);
2246
- }
2247
- return client;
2248
- };
2249
- return defineTool({
2250
- name: "diagnostics",
2251
- description: 'Query the configured LSP server(s) on a file for diagnostics (errors/warnings), hover docs, definition, or references. Configure servers under "lsp.servers" in ~/.bccli/config.json, e.g. { "extensions": [".ts"], "command": "typescript-language-server", "args": ["--stdio"] }.',
2252
- schema: SCHEMA,
2253
- kind: "read",
2254
- target: (input) => input.path,
2255
- async run(input, ctx) {
2256
- const file = resolvePath(ctx.cwd, input.path);
2257
- const ext = extname3(file).toLowerCase();
2258
- const server = servers.find((s) => s.extensions.includes(ext));
2259
- if (!server) {
2260
- return { output: t("No LSP server is configured for {ext} files.", { ext: ext || "(none)" }), isError: true };
2261
- }
2262
- const client = clientFor(server, ctx.cwd);
2263
- const action = input.action ?? "diagnostics";
2264
- const line0 = Math.max(0, (input.line ?? 1) - 1);
2265
- const char0 = Math.max(0, (input.character ?? 1) - 1);
2266
- if (action === "hover") {
2267
- const text = await client.hover(file, line0, char0, 15e3);
2268
- return { output: text || "No hover information." };
2269
- }
2270
- if (action === "definition" || action === "references") {
2271
- const locs = action === "definition" ? await client.definition(file, line0, char0, 15e3) : await client.references(file, line0, char0, 15e3);
2272
- if (!locs.length) return { output: `No ${action} found.` };
2273
- const formatted = locs.map((l) => {
2274
- const rel = relative2(ctx.cwd, l.path).replace(/\\/g, "/") || input.path;
2275
- return `${rel}:${l.line + 1}:${l.character + 1}`;
2276
- });
2277
- return { output: formatted.join("\n") };
2278
- }
2279
- const diags = await client.diagnostics(file, 15e3);
2280
- if (!diags.length) return { output: "No diagnostics." };
2281
- const lines = diags.map((d) => `${input.path}:${d.line + 1}:${d.character + 1} ${SEVERITY[d.severity] ?? "error"} ${d.message}`);
2282
- return { output: lines.join("\n"), isError: diags.some((d) => d.severity === 1) };
2283
- }
2284
- });
2285
- }
2286
-
2287
- // src/plugins.ts
2288
- import { existsSync as existsSync4 } from "fs";
2289
- import { createRequire } from "module";
2290
- import { isAbsolute, join as join4 } from "path";
2291
- import { z as z15 } from "zod";
2292
- var require_ = createRequire(import.meta.url);
2293
- function pluginLog(message) {
2294
- process.stderr.write(`[plugin] ${message}
2295
- `);
2296
- }
2297
- function normalizeTool(def) {
2298
- return {
2299
- name: def.name,
2300
- description: def.description,
2301
- // Raw-args passthrough: a plugin without its own zod still receives what the model sent.
2302
- schema: def.schema ?? z15.record(z15.string(), z15.unknown()),
2303
- kind: def.kind ?? "read",
2304
- target: def.target ?? (() => def.name),
2305
- run: def.run
2306
- };
2307
- }
2308
- function loadPlugins(specs, opts) {
2309
- const tools = [];
2310
- const taken = new Set(opts.builtinNames);
2311
- const listeners = /* @__PURE__ */ new Map();
2312
- for (const spec of specs) {
2313
- let file;
2314
- try {
2315
- file = isAbsolute(spec) ? spec : spec.startsWith(".") ? join4(opts.home, spec) : require_.resolve(spec);
2316
- } catch {
2317
- throw new ConfigError(t("Plugin not found: {spec}", { spec }));
2318
- }
2319
- if (!existsSync4(file)) throw new ConfigError(t("Plugin not found: {spec}", { spec }));
2320
- let mod;
2321
- try {
2322
- mod = require_(file);
2323
- } catch (error) {
2324
- throw new ConfigError(t("Plugin failed to load: {spec}: {reason}", { spec, reason: error.message }));
2325
- }
2326
- const asRecord = mod ?? {};
2327
- const activate = typeof mod === "function" ? mod : typeof asRecord.default === "function" ? asRecord.default : typeof asRecord.activate === "function" ? asRecord.activate : void 0;
2328
- if (!activate) {
2329
- throw new ConfigError(
2330
- t("Plugin failed to load: {spec}: {reason}", {
2331
- spec,
2332
- reason: "no activate function (use export default fn, export function activate, or module.exports = fn)"
2333
- })
2334
- );
2335
- }
2336
- const ctx = {
2337
- cwd: opts.cwd,
2338
- env: opts.env,
2339
- config: opts.config,
2340
- registerTool(tool) {
2341
- if (taken.has(tool.name)) {
2342
- throw new ConfigError(
2343
- t("Plugin {spec} registered a tool name that is already in use: {name}", { spec, name: tool.name })
2344
- );
2345
- }
2346
- taken.add(tool.name);
2347
- tools.push(normalizeTool(tool));
2348
- },
2349
- on(event, handler) {
2350
- const list = listeners.get(event) ?? [];
2351
- list.push(handler);
2352
- listeners.set(event, list);
2353
- },
2354
- log: pluginLog
2355
- };
2356
- try {
2357
- const result = activate(ctx);
2358
- if (result && typeof result.catch === "function") {
2359
- ;
2360
- result.catch((error) => pluginLog(`activation error in ${spec}: ${String(error)}`));
2361
- }
2362
- } catch (error) {
2363
- throw new ConfigError(t("Plugin failed to load: {spec}: {reason}", { spec, reason: error.message }));
2364
- }
2365
- }
2366
- return {
2367
- tools,
2368
- async emit(event, payload = {}) {
2369
- for (const handler of listeners.get(event) ?? []) {
2370
- try {
2371
- await handler(payload);
2372
- } catch (error) {
2373
- pluginLog(`handler for ${event} failed: ${String(error)}`);
2374
- }
2375
- }
2376
- }
2377
- };
2378
- }
2379
-
2380
- // src/setup.ts
2381
- var TOOL_RULES = { bash: "bash", edit: "edit", write: "edit", fetch: "fetch" };
2382
- var pluginCache = /* @__PURE__ */ new Map();
2383
- function loadPluginsOnce(specs, opts) {
2384
- const key = JSON.stringify({ specs, home: opts.home, cwd: opts.cwd });
2385
- const cached = pluginCache.get(key);
2386
- if (cached) return cached;
2387
- const loaded = loadPlugins(specs, opts);
2388
- pluginCache.set(key, loaded);
2389
- return loaded;
2390
- }
2391
- function lunaPtcAutoEnabled(env) {
2392
- const value = String(env.BCCLI_LUNA_PTC ?? "").trim().toLowerCase();
2393
- return !["0", "false", "off", "no"].includes(value);
2394
- }
2395
- function createRuntime(opts) {
2396
- const env = opts.env ?? process.env;
2397
- const home = bccliHome(env);
2398
- const persistSession = opts.persistSession !== false;
2399
- let config = loadConfig(opts.cwd, env);
2400
- let modelRef = opts.args.model ?? config.model;
2401
- let reasoning = opts.args.reasoning ?? config.reasoning;
2402
- const makeSingleProvider = (ref) => {
2403
- const resolved = resolveModel(config, ref, env);
2404
- return opts.provider ?? createProvider({
2405
- baseURL: resolved.baseURL,
2406
- apiKey: resolved.apiKey,
2407
- model: resolved.model,
2408
- providerId: resolved.providerId,
2409
- fetch: opts.fetch,
2410
- // diteruskan penuh (spec gh-bot §6.6 — juga memperbaiki sdk fetch-injection)
2411
- enableProgrammaticToolCalling: lunaPtcAutoEnabled(env)
2412
- });
2413
- };
2414
- const makeProvider = (ref) => {
2415
- const primary = makeSingleProvider(ref);
2416
- if (opts.provider) return primary;
2417
- const fallbackRefs = [
2418
- ...opts.args.fallbackModel ? [opts.args.fallbackModel] : [],
2419
- ...config.fallbackModels ?? []
2420
- ].filter((r) => r && r !== ref);
2421
- if (!fallbackRefs.length) return primary;
2422
- return withFallbackProvider(
2423
- primary,
2424
- () => fallbackRefs.flatMap((fbRef) => {
2425
- try {
2426
- return [makeSingleProvider(fbRef)];
2427
- } catch {
2428
- return [];
2429
- }
2430
- })
2431
- );
2432
- };
2433
- const provider = makeProvider(modelRef);
2434
- const mode = opts.args.allowAll ? "allowAll" : opts.args.permissionMode ?? config.permissionMode;
2435
- const rules = [...config.allow, ...opts.args.allowedTools.map((t2) => TOOL_RULES[t2] ?? t2)];
2436
- const permissions = new Permissions(mode, rules, opts.cwd);
2437
- const roots = { cwd: opts.cwd, home, userHome: opts.userHome ?? homedir() };
2438
- const skills = loadSkills(roots);
2439
- const commands = loadCommands(roots);
2440
- const agentDefs = loadAgentDefs(roots);
2441
- const todos = new TodoStore();
2442
- const checkpoints = new CheckpointStore();
2443
- const turnMarks = [];
2444
- const checkpointMarks = [];
2445
- let startedAt = /* @__PURE__ */ new Date();
2446
- const interaction = { approvePlan: async () => "no" };
2447
- let session = Session.create(home, opts.cwd);
2448
- let history = opts.history;
2449
- if (!history && opts.args.continue) {
2450
- const latest = Session.latest(home, opts.cwd);
2451
- if (latest) {
2452
- session = latest;
2453
- history = latest.load();
2454
- if (!opts.args.model) {
2455
- const prevModel = latest.lastModel();
2456
- if (prevModel) modelRef = prevModel;
2457
- }
2458
- }
2459
- }
2460
- const repoMap = config.repoMap ? buildRepoMapSync(opts.cwd) : void 0;
2461
- const systemFor = (model) => buildSystemPrompt({ cwd: opts.cwd, home, model, skills, repoMap });
2462
- const systemPrompt = systemFor(modelRef);
2463
- const webSearch = createWebSearchTool({
2464
- botconnector: () => {
2465
- try {
2466
- const bc = resolveModel(config, "bc-cloud/search", env);
2467
- return bc.apiKey ? { baseURL: bc.baseURL, apiKey: bc.apiKey } : void 0;
2468
- } catch {
2469
- return void 0;
2470
- }
2471
- }
2472
- });
2473
- const rebuildPrompt = () => agent.setSystemPrompt(systemFor(modelRef));
2474
- const saveMemory = createSaveMemoryTool({ home, onSaved: rebuildPrompt });
2475
- const baseTools = [
2476
- createReadTool({ vision: config.vision }),
2477
- createBashTool({ networkPolicy: config.networkPolicy, sandbox: config.sandbox === "on" }),
2478
- ...ALL_TOOLS.filter((tool) => tool.name !== "read" && tool.name !== "bash"),
2479
- webSearch,
2480
- createSkillTool(skills),
2481
- createTodoTool(todos),
2482
- createExitPlanTool({ permissions, interaction }),
2483
- saveMemory,
2484
- createRepoMapTool()
2485
- ];
2486
- if (config.lsp?.servers.length) baseTools.push(createDiagnosticsTool(config.lsp.servers));
2487
- const plugins = loadPluginsOnce(config.plugins ?? [], {
2488
- home,
2489
- cwd: opts.cwd,
2490
- env,
2491
- config,
2492
- builtinNames: [...baseTools.map((tool) => tool.name), "task"]
2493
- });
2494
- baseTools.push(...plugins.tools);
2495
- const pluginEmit = (event, payload) => plugins.emit(event, payload);
2496
- const task = createTaskTool({
2497
- agents: [...BUILTIN_AGENTS, ...agentDefs],
2498
- // Subagents get whatever the main agent has right now (incl. MCP tools), minus task itself.
2499
- baseTools: () => agent.tools.filter((t2) => t2.name !== "task"),
2500
- permissions,
2501
- provider: () => opts.provider ? agent.provider : makeProvider(modelRef),
2502
- providerFor: makeProvider,
2503
- systemPrompt: (childModelRef) => systemFor(childModelRef ?? modelRef),
2504
- cwd: opts.cwd,
2505
- reasoning: () => reasoning,
2506
- hooks: config.hooks,
2507
- pluginEmit,
2508
- env
2509
- });
2510
- const agent = new Agent({
2511
- provider,
2512
- tools: [...baseTools, task],
2513
- permissions,
2514
- systemPrompt,
2515
- cwd: opts.cwd,
2516
- history,
2517
- reasoning,
2518
- maxSteps: opts.maxSteps ?? config.maxSteps,
2519
- hooks: config.hooks,
2520
- pluginEmit,
2521
- env,
2522
- usageCap: config.usageCap,
2523
- budgetModel: modelRef,
2524
- contextWindow: contextWindowFor(modelRef, env),
2525
- onMessage: (m) => {
2526
- if (persistSession) session.append(m);
2527
- },
2528
- onUsage: (u) => {
2529
- if (persistSession) session.appendUsage({ ...u, model: modelRef });
2530
- },
2531
- onReset: () => {
2532
- if (persistSession) session.reset();
2533
- },
2534
- onTurnStart: () => {
2535
- for (let i = turnMarks.length - 1; i >= 0 && turnMarks[i] > agent.messages.length; i--) {
2536
- turnMarks.splice(i, 1);
2537
- checkpointMarks.splice(i, 1);
2538
- }
2539
- turnMarks.push(agent.messages.length);
2540
- checkpointMarks.push(checkpoints.entries());
2541
- checkpoints.beginTurn();
2542
- },
2543
- checkpoint: (path) => checkpoints.snapshot(path)
2544
- });
2545
- const mcp = new McpManager({
2546
- home,
2547
- onChange: () => agent.setTools([...baseTools, task, ...mcp.tools()]),
2548
- // A later server may reuse a sanitized name; it must not inherit this one's "[a]" grants.
2549
- onToolsRemoved: (names) => {
2550
- for (const name of names) permissions.revoke(`mcp(${name})`);
2551
- }
2552
- });
2553
- return {
2554
- cwd: opts.cwd,
2555
- home,
2556
- env,
2557
- skills,
2558
- commands,
2559
- agentDefs,
2560
- todos,
2561
- interaction,
2562
- mcp,
2563
- startMcp: (specs) => mcp.start(specs),
2564
- get config() {
2565
- return config;
2566
- },
2567
- reloadConfig() {
2568
- config = loadConfig(opts.cwd, env);
2569
- },
2570
- providerLabel() {
2571
- return providerName(config, modelRef.slice(0, modelRef.indexOf("/")));
2572
- },
2573
- listModels: (only) => listAllModels(config, env, { fetch: opts.fetch, only }),
2574
- addProviderKey(id, key) {
2575
- return addProviderKey(id, key, { env, cwd: opts.cwd, out: () => {
2576
- }, err: () => {
2577
- }, readSecret: async () => "", fetch: opts.fetch });
2578
- },
2579
- get modelRef() {
2580
- return modelRef;
2581
- },
2582
- get reasoning() {
2583
- return reasoning;
2584
- },
2585
- agent,
2586
- get session() {
2587
- return session;
2588
- },
2589
- resume(previous) {
2590
- session = previous;
2591
- agent.load(previous.load());
2592
- agent.totalUsage = { inputTokens: 0, outputTokens: 0 };
2593
- if (!opts.args.model) {
2594
- const prevModel = previous.lastModel();
2595
- if (prevModel) {
2596
- try {
2597
- const nextProvider = makeProvider(prevModel);
2598
- modelRef = prevModel;
2599
- agent.provider = nextProvider;
2600
- agent.setBudgetModel(prevModel);
2601
- agent.setContextWindow(contextWindowFor(prevModel, env));
2602
- agent.setSystemPrompt(systemFor(modelRef));
2603
- } catch {
2604
- }
2605
- }
2606
- }
2607
- todos.set([]);
2608
- checkpoints.clear();
2609
- turnMarks.length = 0;
2610
- checkpointMarks.length = 0;
2611
- },
2612
- newSession() {
2613
- session = Session.create(home, opts.cwd);
2614
- agent.load([]);
2615
- agent.totalUsage = { inputTokens: 0, outputTokens: 0 };
2616
- todos.set([]);
2617
- checkpoints.clear();
2618
- turnMarks.length = 0;
2619
- checkpointMarks.length = 0;
2620
- startedAt = /* @__PURE__ */ new Date();
2621
- },
2622
- rebuildSystemPrompt() {
2623
- rebuildPrompt();
2624
- },
2625
- emitPlugins: (event, payload) => plugins.emit(event, payload),
2626
- checkpoints,
2627
- async rewindTurns(n) {
2628
- if (turnMarks.length === 0) return void 0;
2629
- const count = Math.min(Math.max(1, Math.floor(n) || 1), turnMarks.length);
2630
- const from = turnMarks.length - count;
2631
- const messageTarget = turnMarks[from];
2632
- const checkpointTarget = checkpointMarks[from];
2633
- turnMarks.splice(from);
2634
- checkpointMarks.splice(from);
2635
- const files = await checkpoints.undoTo(checkpointTarget);
2636
- const kept = agent.messages.slice(0, Math.min(messageTarget, agent.messages.length));
2637
- agent.load(kept);
2638
- session.reset();
2639
- for (const message of kept) session.append(message);
2640
- return {
2641
- turns: count,
2642
- restored: files?.restored ?? [],
2643
- deleted: files?.deleted ?? [],
2644
- skipped: files?.skipped ?? [],
2645
- failed: files?.failed ?? []
2646
- };
2647
- },
2648
- get startedAt() {
2649
- return startedAt;
2650
- },
2651
- setModel(ref) {
2652
- const nextProvider = makeProvider(ref);
2653
- modelRef = ref;
2654
- agent.provider = nextProvider;
2655
- agent.setBudgetModel(ref);
2656
- agent.setContextWindow(contextWindowFor(ref, env));
2657
- agent.setSystemPrompt(systemFor(modelRef));
2658
- },
2659
- setReasoning(level) {
2660
- reasoning = level;
2661
- agent.reasoning = level;
2662
- }
2663
- };
2664
- }
2665
-
2666
- export {
2667
- lunaPtcAutoEnabled,
2668
- createRuntime
2669
- };