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