@botconnector/bccli 0.4.0-beta.3 → 0.4.0-beta.31
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 +71 -1
- package/dist/args-EFFR44JW.js +12 -0
- package/dist/{chunk-U7DLMVPZ.js → chunk-AOVIQXXO.js} +2 -1
- package/dist/{chunk-NGLKANMD.js → chunk-CDUPYHNL.js} +4 -3
- package/dist/chunk-CX5U4EHX.js +952 -0
- package/dist/{chunk-UESQNU4A.js → chunk-GEG6QT5H.js} +11 -10
- package/dist/chunk-IJGTX7RC.js +444 -0
- package/dist/chunk-NM3C75BT.js +103 -0
- package/dist/{chunk-OSWQPSA4.js → chunk-NWVFCTTO.js} +44 -4
- package/dist/chunk-UJH2JT6H.js +63 -0
- package/dist/{chunk-L7L4RGKG.js → chunk-Z37BR5H4.js} +2 -2
- package/dist/cli.js +743 -244
- package/dist/{config-NPSYFAFD.js → config-DMF4AO7E.js} +2 -2
- package/dist/integrationCli-G4C6XQUE.js +586 -0
- package/dist/{login-KHU7ONSR.js → login-AP2ODLJX.js} +1 -1
- package/dist/{mcpCli-Q2H63ISP.js → mcpCli-UGNEDBMU.js} +11 -10
- package/dist/providerCli-J4Y7D6PF.js +12 -0
- package/dist/{ui-A32PTF4F.js → ui-EFY7SNIW.js} +796 -238
- package/package.json +20 -3
- package/dist/args-FCLPQ7TO.js +0 -11
- package/dist/chunk-2JBRLGKD.js +0 -266
- package/dist/chunk-RZUJYAQV.js +0 -112
- package/dist/chunk-UN7QXPCB.js +0 -82
- package/dist/providerCli-G3X7KENO.js +0 -11
package/dist/cli.js
CHANGED
|
@@ -1,16 +1,4 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import {
|
|
3
|
-
HELP_TEXT,
|
|
4
|
-
parseCliArgs
|
|
5
|
-
} from "./chunk-UN7QXPCB.js";
|
|
6
|
-
import {
|
|
7
|
-
addProviderKey,
|
|
8
|
-
createProvider,
|
|
9
|
-
listAllModels
|
|
10
|
-
} from "./chunk-2JBRLGKD.js";
|
|
11
|
-
import {
|
|
12
|
-
runLogin
|
|
13
|
-
} from "./chunk-L7L4RGKG.js";
|
|
14
2
|
import {
|
|
15
3
|
BUILTIN_AGENTS,
|
|
16
4
|
buildSystemPrompt,
|
|
@@ -18,15 +6,33 @@ import {
|
|
|
18
6
|
loadAgentDefs,
|
|
19
7
|
loadCommands,
|
|
20
8
|
loadSkills,
|
|
9
|
+
normalizeGlobPath,
|
|
21
10
|
parseFrontmatter,
|
|
22
|
-
runPrint
|
|
23
|
-
|
|
11
|
+
runPrint,
|
|
12
|
+
validateGlobPattern
|
|
13
|
+
} from "./chunk-NWVFCTTO.js";
|
|
14
|
+
import {
|
|
15
|
+
helpText,
|
|
16
|
+
parseCliArgs
|
|
17
|
+
} from "./chunk-NM3C75BT.js";
|
|
24
18
|
import {
|
|
25
19
|
Permissions
|
|
26
20
|
} from "./chunk-ELML4KVQ.js";
|
|
27
21
|
import {
|
|
28
|
-
|
|
29
|
-
|
|
22
|
+
addProviderKey,
|
|
23
|
+
completionAssistantMessage,
|
|
24
|
+
createProvider,
|
|
25
|
+
hasPendingProgrammaticReplay,
|
|
26
|
+
listAllModels
|
|
27
|
+
} from "./chunk-CX5U4EHX.js";
|
|
28
|
+
import {
|
|
29
|
+
providerName,
|
|
30
|
+
readGlobalConfigFile
|
|
31
|
+
} from "./chunk-AOVIQXXO.js";
|
|
32
|
+
import "./chunk-UJH2JT6H.js";
|
|
33
|
+
import {
|
|
34
|
+
runLogin
|
|
35
|
+
} from "./chunk-Z37BR5H4.js";
|
|
30
36
|
import {
|
|
31
37
|
Session,
|
|
32
38
|
pruneSessions
|
|
@@ -35,8 +41,76 @@ import {
|
|
|
35
41
|
ConfigError,
|
|
36
42
|
bccliHome,
|
|
37
43
|
loadConfig,
|
|
38
|
-
|
|
39
|
-
|
|
44
|
+
resolveLanguage,
|
|
45
|
+
resolveModel,
|
|
46
|
+
setLanguage,
|
|
47
|
+
t
|
|
48
|
+
} from "./chunk-IJGTX7RC.js";
|
|
49
|
+
|
|
50
|
+
// src/textToolCalls.ts
|
|
51
|
+
var PAIR = /(?:<arg_key>\s*|`)?([A-Za-z_]\w*)(?:\s*<\/arg_key>)?`?\s*(?:=\s*`|<arg_value>)([\s\S]*?)<\/arg_value>/g;
|
|
52
|
+
var CALL_START = /<tool_call>|\n?[^\n]*\bTool call list\b[^\n]*|\n\s*\d+\.\s*Tool:/g;
|
|
53
|
+
function coerce(value, type) {
|
|
54
|
+
const types = Array.isArray(type) ? type : [type];
|
|
55
|
+
const v = value.trim();
|
|
56
|
+
if ((types.includes("integer") || types.includes("number")) && v !== "" && !Number.isNaN(Number(v))) return Number(v);
|
|
57
|
+
if (types.includes("boolean") && /^(true|false)$/i.test(v)) return v.toLowerCase() === "true";
|
|
58
|
+
if ((types.includes("object") || types.includes("array")) && /^[[{]/.test(v)) {
|
|
59
|
+
try {
|
|
60
|
+
return JSON.parse(v);
|
|
61
|
+
} catch {
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
return value.replace(/^\n+|\n+$/g, "");
|
|
65
|
+
}
|
|
66
|
+
function pickTool(name, keys, tools) {
|
|
67
|
+
const named = name && tools.find((t2) => t2.function.name === name);
|
|
68
|
+
if (named) return named;
|
|
69
|
+
let best;
|
|
70
|
+
let bestScore = 0;
|
|
71
|
+
let tie = false;
|
|
72
|
+
for (const t2 of tools) {
|
|
73
|
+
const params = t2.function.parameters;
|
|
74
|
+
const props = Object.keys(params.properties ?? {});
|
|
75
|
+
if (!(params.required ?? []).every((r) => keys.includes(r))) continue;
|
|
76
|
+
const score = keys.filter((k) => props.includes(k)).length;
|
|
77
|
+
if (score > bestScore) [best, bestScore, tie] = [t2, score, false];
|
|
78
|
+
else if (score === bestScore && score > 0) tie = true;
|
|
79
|
+
}
|
|
80
|
+
return tie ? void 0 : best;
|
|
81
|
+
}
|
|
82
|
+
function recoverTextToolCalls(text, tools) {
|
|
83
|
+
if (!/<\/arg_value>|<tool_call>/.test(text)) return null;
|
|
84
|
+
const starts = [...text.matchAll(CALL_START)].map((m) => m.index ?? 0);
|
|
85
|
+
const firstPair = text.search(PAIR);
|
|
86
|
+
const cut = Math.min(...[...starts, firstPair].filter((i) => i >= 0));
|
|
87
|
+
const bounds = starts.filter((i) => i >= cut);
|
|
88
|
+
if (!bounds.length || bounds[0] > cut) bounds.unshift(cut);
|
|
89
|
+
const toolCalls = [];
|
|
90
|
+
bounds.forEach((start, n) => {
|
|
91
|
+
const segment = text.slice(start, bounds[n + 1] ?? text.length);
|
|
92
|
+
const name = /<tool_call>\s*([A-Za-z_][\w.-]*)/.exec(segment)?.[1] ?? /Tool:\s*`?([A-Za-z_][\w.-]*)`?/.exec(segment)?.[1];
|
|
93
|
+
let args = [];
|
|
94
|
+
const flush = () => {
|
|
95
|
+
if (!args.length) return;
|
|
96
|
+
const keys = args.map(([k]) => k);
|
|
97
|
+
const tool = pickTool(name, keys, tools);
|
|
98
|
+
if (tool) {
|
|
99
|
+
const props = tool.function.parameters.properties ?? {};
|
|
100
|
+
const input = Object.fromEntries(args.filter(([k]) => k in props).map(([k, v]) => [k, coerce(v, props[k].type)]));
|
|
101
|
+
toolCalls.push({ id: `text_call_${toolCalls.length}`, name: tool.function.name, arguments: JSON.stringify(input) });
|
|
102
|
+
}
|
|
103
|
+
args = [];
|
|
104
|
+
};
|
|
105
|
+
for (const m of segment.matchAll(PAIR)) {
|
|
106
|
+
if (args.some(([k]) => k === m[1])) flush();
|
|
107
|
+
args.push([m[1], m[2]]);
|
|
108
|
+
}
|
|
109
|
+
flush();
|
|
110
|
+
});
|
|
111
|
+
if (!toolCalls.length) return null;
|
|
112
|
+
return { text: text.slice(0, cut).trim(), toolCalls };
|
|
113
|
+
}
|
|
40
114
|
|
|
41
115
|
// src/tools/index.ts
|
|
42
116
|
import { z as z8 } from "zod";
|
|
@@ -136,7 +210,7 @@ function runCommand(command, opts) {
|
|
|
136
210
|
clearTimeout(timer);
|
|
137
211
|
opts.signal.removeEventListener("abort", onAbort);
|
|
138
212
|
const output = dropped ? `${head}
|
|
139
|
-
\u2026 [${dropped}
|
|
213
|
+
\u2026 [${dropped} characters truncated] \u2026
|
|
140
214
|
${tail}` : head + tail;
|
|
141
215
|
resolve2({ output: output + extra, exitCode, timedOut, aborted });
|
|
142
216
|
};
|
|
@@ -155,7 +229,7 @@ var bashTool = defineTool({
|
|
|
155
229
|
target: (input) => input.command,
|
|
156
230
|
async run(input, ctx) {
|
|
157
231
|
const r = await runCommand(input.command, { cwd: ctx.cwd, timeoutMs: input.timeout_ms ?? 12e4, signal: ctx.signal });
|
|
158
|
-
const status = r.timedOut ? `[
|
|
232
|
+
const status = r.timedOut ? `[stopped: timeout after ${Math.round((input.timeout_ms ?? 12e4) / 1e3)} seconds]` : r.aborted ? "[cancelled by the user]" : `[exit code ${r.exitCode}]`;
|
|
159
233
|
return { output: `${r.output.trimEnd()}
|
|
160
234
|
${status}`.trimStart(), isError: r.exitCode !== 0 };
|
|
161
235
|
}
|
|
@@ -214,7 +288,7 @@ function countOccurrences(haystack, needle) {
|
|
|
214
288
|
}
|
|
215
289
|
function applyEdit(content, input) {
|
|
216
290
|
if (input.old_string === input.new_string) {
|
|
217
|
-
return { output: "old_string
|
|
291
|
+
return { output: "old_string and new_string are identical; nothing to change.", isError: true };
|
|
218
292
|
}
|
|
219
293
|
const eol = content.includes("\r\n") ? "\r\n" : "\n";
|
|
220
294
|
const toEol = (s) => s.replace(/\r?\n/g, eol);
|
|
@@ -225,13 +299,13 @@ function applyEdit(content, input) {
|
|
|
225
299
|
if (count === 0) continue;
|
|
226
300
|
if (count > 1 && !input.replace_all) {
|
|
227
301
|
return {
|
|
228
|
-
output: `old_string
|
|
302
|
+
output: `old_string appears ${count} times in ${input.path}. Add more context to make it unique, or use replace_all.`,
|
|
229
303
|
isError: true
|
|
230
304
|
};
|
|
231
305
|
}
|
|
232
306
|
return input.replace_all ? content.split(oldText).join(newText) : content.replace(oldText, () => newText);
|
|
233
307
|
}
|
|
234
|
-
return { output: `old_string
|
|
308
|
+
return { output: `old_string not found in ${input.path}. Re-read the file.`, isError: true };
|
|
235
309
|
}
|
|
236
310
|
var editTool = defineTool({
|
|
237
311
|
name: "edit",
|
|
@@ -241,12 +315,12 @@ var editTool = defineTool({
|
|
|
241
315
|
target: (input) => input.path,
|
|
242
316
|
async validate(input, ctx) {
|
|
243
317
|
const abs = resolvePath(ctx.cwd, input.path);
|
|
244
|
-
if (!ctx.readFiles.has(abs)) return `
|
|
318
|
+
if (!ctx.readFiles.has(abs)) return `Read ${input.path} with read before editing it.`;
|
|
245
319
|
let content;
|
|
246
320
|
try {
|
|
247
321
|
content = await readFile(abs, "utf8");
|
|
248
322
|
} catch (error) {
|
|
249
|
-
return `
|
|
323
|
+
return `Cannot read ${input.path}: ${errorCode(error)}`;
|
|
250
324
|
}
|
|
251
325
|
const next = applyEdit(content, input);
|
|
252
326
|
return typeof next === "string" ? void 0 : next.output;
|
|
@@ -260,25 +334,30 @@ var editTool = defineTool({
|
|
|
260
334
|
async run(input, ctx) {
|
|
261
335
|
const abs = resolvePath(ctx.cwd, input.path);
|
|
262
336
|
if (!ctx.readFiles.has(abs)) {
|
|
263
|
-
return { output: `
|
|
337
|
+
return { output: `Read ${input.path} with read before editing it.`, isError: true };
|
|
264
338
|
}
|
|
265
339
|
let content;
|
|
266
340
|
try {
|
|
267
341
|
content = await readFile(abs, "utf8");
|
|
268
342
|
} catch (error) {
|
|
269
|
-
return { output: `
|
|
343
|
+
return { output: `Cannot read ${input.path}: ${errorCode(error)}`, isError: true };
|
|
270
344
|
}
|
|
271
345
|
const next = applyEdit(content, input);
|
|
272
346
|
if (typeof next !== "string") return next;
|
|
273
347
|
await ctx.checkpoint?.(abs);
|
|
274
348
|
await writeFile(abs, next);
|
|
275
|
-
return { output: `
|
|
349
|
+
return { output: `Edited ${input.path}.`, display: formatDiff(diffLines(content, next)) };
|
|
276
350
|
}
|
|
277
351
|
});
|
|
278
352
|
|
|
279
353
|
// src/tools/fetch.ts
|
|
280
354
|
import { z as z3 } from "zod";
|
|
281
|
-
var MAX_CHARS =
|
|
355
|
+
var MAX_CHARS = 12e3;
|
|
356
|
+
var FOCUS_CHUNKS = 3;
|
|
357
|
+
var FOCUS_SIZE = 800;
|
|
358
|
+
var CACHE_MAX = 16;
|
|
359
|
+
var CACHE_TTL_MS = 15 * 6e4;
|
|
360
|
+
var CACHE_MAX_CHARS = 2e6;
|
|
282
361
|
var MAX_BYTES = 5 * 1024 * 1024;
|
|
283
362
|
async function readCapped(res) {
|
|
284
363
|
const reader = res.body?.getReader();
|
|
@@ -300,49 +379,238 @@ async function readCapped(res) {
|
|
|
300
379
|
function htmlToText(html) {
|
|
301
380
|
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");
|
|
302
381
|
}
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
382
|
+
function describeFetchError(error) {
|
|
383
|
+
const cause = error.cause;
|
|
384
|
+
const code = cause?.code ?? "";
|
|
385
|
+
const detail = cause?.message ?? error.message;
|
|
386
|
+
if (error.name === "TimeoutError") return "no response within 30 seconds";
|
|
387
|
+
if (code === "ENOTFOUND" || code === "EAI_AGAIN") return `${detail} \u2014 DNS could not find the host (typo, offline, or blocked by DNS/ISP)`;
|
|
388
|
+
if (/CERT|SELF_SIGNED|UNABLE_TO/.test(code)) {
|
|
389
|
+
return `${detail} \u2014 TLS certificate rejected; often caused by an antivirus or proxy that inspects HTTPS. Try running with NODE_OPTIONS=--use-system-ca`;
|
|
390
|
+
}
|
|
391
|
+
if (/ECONNRESET|ECONNREFUSED|ETIMEDOUT|EHOSTUNREACH|ENETUNREACH|UND_ERR_CONNECT_TIMEOUT|UND_ERR_SOCKET/.test(code)) {
|
|
392
|
+
return `${detail} \u2014 connection failed (firewall, proxy, or the site is blocked by the network)`;
|
|
393
|
+
}
|
|
394
|
+
return code ? `${detail} (${code})` : detail;
|
|
395
|
+
}
|
|
396
|
+
var tokensOf = (text) => Math.ceil(text.length / 4);
|
|
397
|
+
function isPrivateHost(host) {
|
|
398
|
+
const h = host.toLowerCase().replace(/^\[|\]$/g, "");
|
|
399
|
+
if (h === "localhost" || /\.(local|internal|lan|localdomain|home\.arpa)$/.test(h)) return true;
|
|
400
|
+
if (h.includes(":")) return /^(::1?$|f[cd]|fe[89ab]|::ffff:)/.test(h);
|
|
401
|
+
const v4 = h.match(/^(\d+)\.(\d+)\.\d+\.\d+$/);
|
|
402
|
+
if (v4) {
|
|
403
|
+
const [a, b] = [Number(v4[1]), Number(v4[2])];
|
|
404
|
+
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;
|
|
405
|
+
}
|
|
406
|
+
return !h.includes(".");
|
|
407
|
+
}
|
|
408
|
+
var words = (s) => s.toLowerCase().match(/[\p{L}\p{N}]{2,}/gu) ?? [];
|
|
409
|
+
function pickRelevant(text, query, count = FOCUS_CHUNKS, size = FOCUS_SIZE) {
|
|
410
|
+
const chunks = [];
|
|
411
|
+
let cur = "";
|
|
412
|
+
for (const line of text.split("\n")) {
|
|
413
|
+
for (let i = 0; i < Math.max(line.length, 1); i += size) {
|
|
414
|
+
cur += `${line.slice(i, i + size)}
|
|
415
|
+
`;
|
|
416
|
+
if (cur.length >= size) {
|
|
417
|
+
chunks.push(cur.trimEnd());
|
|
418
|
+
cur = "";
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
if (cur.trim()) chunks.push(cur.trimEnd());
|
|
423
|
+
const q = [...new Set(words(query))];
|
|
424
|
+
const docs = chunks.map(words);
|
|
425
|
+
const avg = docs.reduce((n, d) => n + d.length, 0) / (docs.length || 1) || 1;
|
|
426
|
+
const idf = q.map((t2) => {
|
|
427
|
+
const df = docs.filter((d) => d.includes(t2)).length;
|
|
428
|
+
return Math.log(1 + (docs.length - df + 0.5) / (df + 0.5));
|
|
429
|
+
});
|
|
430
|
+
const scored = docs.map((d, i) => ({
|
|
431
|
+
i,
|
|
432
|
+
score: q.reduce((sum, t2, k) => {
|
|
433
|
+
const tf = d.filter((w) => w === t2).length;
|
|
434
|
+
return sum + idf[k] * tf * 2.2 / (tf + 1.2 * (0.25 + 0.75 * d.length / avg));
|
|
435
|
+
}, 0)
|
|
436
|
+
})).filter((c) => c.score > 0).sort((a, b) => b.score - a.score).slice(0, count).sort((a, b) => a.i - b.i);
|
|
437
|
+
return scored.length ? scored.map((c) => chunks[c.i]).join("\n\u2026\n") : null;
|
|
438
|
+
}
|
|
439
|
+
var cache = /* @__PURE__ */ new Map();
|
|
440
|
+
function remember(key, value, size) {
|
|
441
|
+
if (size > CACHE_MAX_CHARS) return;
|
|
442
|
+
cache.delete(key);
|
|
443
|
+
cache.set(key, value);
|
|
444
|
+
if (cache.size > CACHE_MAX) cache.delete(cache.keys().next().value);
|
|
445
|
+
}
|
|
446
|
+
function recall(key) {
|
|
447
|
+
const hit = cache.get(key);
|
|
448
|
+
if (typeof hit === "object" && Date.now() - hit.at > CACHE_TTL_MS) {
|
|
449
|
+
cache.delete(key);
|
|
450
|
+
return void 0;
|
|
451
|
+
}
|
|
452
|
+
return hit;
|
|
453
|
+
}
|
|
454
|
+
var ACCEPT = "text/markdown, text/html;q=0.9, text/plain;q=0.8, */*;q=0.5";
|
|
455
|
+
var withCost = (text, what) => `${what} \xB7 ${t("~{n} tokens", { n: tokensOf(text) })}`;
|
|
456
|
+
function createFetchTool(opts = {}) {
|
|
457
|
+
const keenableURL = opts.keenableURL === void 0 ? "https://api.keenable.ai/v1/fetch/public" : opts.keenableURL;
|
|
458
|
+
const isPrivate = opts.isPrivate ?? isPrivateHost;
|
|
459
|
+
return defineTool({
|
|
460
|
+
name: "fetch",
|
|
461
|
+
description: `Fetch a URL (http/https) as text/markdown. Costs the user tokens, so ask for as little as you need:
|
|
462
|
+
- 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.
|
|
463
|
+
- Without \`prompt\` you get the first ${MAX_CHARS} characters; the reply tells you the \`offset\` to continue from.
|
|
464
|
+
- Fetching a page that is already in this conversation is wasteful; reuse what you have.`,
|
|
465
|
+
schema: z3.object({
|
|
466
|
+
url: z3.string().describe("Absolute http(s) URL"),
|
|
467
|
+
prompt: z3.string().max(2e3).optional().describe("What to look for in the page; returns only the relevant excerpt"),
|
|
468
|
+
offset: z3.number().int().min(0).optional().describe("Start reading at this character (to continue a truncated page)")
|
|
469
|
+
}),
|
|
470
|
+
kind: "fetch",
|
|
471
|
+
target: (input) => input.url,
|
|
472
|
+
async run(input, ctx) {
|
|
473
|
+
if (!/^https?:\/\//i.test(input.url)) return { output: "URL must start with http:// or https://", isError: true };
|
|
474
|
+
const target = new URL(input.url);
|
|
475
|
+
const prompt = input.prompt?.trim();
|
|
476
|
+
const askKeenable = !!prompt && !input.offset && !!keenableURL && !target.username && !target.password && !isPrivate(target.hostname);
|
|
477
|
+
if (askKeenable) {
|
|
478
|
+
const key = `x
|
|
479
|
+
${input.url}
|
|
480
|
+
${prompt}`;
|
|
481
|
+
const hit = recall(key);
|
|
482
|
+
if (hit) {
|
|
483
|
+
ctx.refundFetch?.();
|
|
484
|
+
return { output: hit, display: withCost(hit, t("focused excerpt \xB7 cache")) };
|
|
485
|
+
}
|
|
486
|
+
const out = await extractWithKeenable(keenableURL, input.url, prompt, ctx.signal);
|
|
487
|
+
if (out) {
|
|
488
|
+
const output2 = `[Excerpt from ${input.url} for "${prompt}" (extracted by Keenable). If it says the answer is not there, try fetch without a prompt.]
|
|
489
|
+
${out}`;
|
|
490
|
+
remember(key, output2, output2.length);
|
|
491
|
+
return { output: output2, display: withCost(output2, t("focused excerpt (Keenable)")) };
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
let page = recall(`p
|
|
495
|
+
${input.url}`);
|
|
496
|
+
const cached = !!page;
|
|
497
|
+
if (cached) ctx.refundFetch?.();
|
|
498
|
+
if (!page) {
|
|
499
|
+
const loaded = await loadPage(input.url, ctx.signal);
|
|
500
|
+
if ("error" in loaded) return { output: loaded.error, isError: true };
|
|
501
|
+
page = loaded;
|
|
502
|
+
remember(`p
|
|
503
|
+
${input.url}`, page, page.text.length);
|
|
504
|
+
}
|
|
505
|
+
const { text, truncated } = page;
|
|
506
|
+
const via = cached ? " \xB7 cache" : "";
|
|
507
|
+
if (prompt && !input.offset) {
|
|
508
|
+
const picked = pickRelevant(text, prompt);
|
|
509
|
+
if (picked) {
|
|
510
|
+
const output2 = `[Most relevant excerpt from ${input.url} for "${prompt}" (${text.length} characters total). Full page: fetch without a prompt.]
|
|
511
|
+
${picked}`;
|
|
512
|
+
return { output: output2, display: withCost(output2, `${t("focused excerpt (local)")}${via}`) };
|
|
325
513
|
}
|
|
514
|
+
}
|
|
515
|
+
const offset = input.offset ?? 0;
|
|
516
|
+
if (offset >= text.length && text.length > 0) {
|
|
517
|
+
return { output: `offset ${offset} is past the end of the page (${text.length} characters).`, isError: true };
|
|
518
|
+
}
|
|
519
|
+
const end = Math.min(offset + MAX_CHARS, text.length);
|
|
520
|
+
let output = text.slice(offset, end);
|
|
521
|
+
if (end < text.length || truncated) {
|
|
522
|
+
const total = truncated ? `${text.length}+` : text.length;
|
|
523
|
+
output += `
|
|
524
|
+
\u2026 [truncated: characters ${offset}-${end} of ${total}. Continue with offset=${end}, or use prompt for a focused excerpt.]`;
|
|
525
|
+
}
|
|
526
|
+
const size = truncated ? t("over 5 MB, truncated") : t("{n} characters", { n: text.length });
|
|
527
|
+
return { output, display: withCost(output, `${size}${via}`) };
|
|
528
|
+
}
|
|
529
|
+
});
|
|
530
|
+
}
|
|
531
|
+
async function extractWithKeenable(endpoint, url, prompt, signal) {
|
|
532
|
+
try {
|
|
533
|
+
const u = new URL(endpoint);
|
|
534
|
+
u.searchParams.set("url", url);
|
|
535
|
+
u.searchParams.set("prompt", prompt);
|
|
536
|
+
u.searchParams.set("live", "true");
|
|
537
|
+
u.searchParams.set("max_chars", String(MAX_CHARS));
|
|
538
|
+
const res = await fetch(u, { headers: { "X-Keenable-Title": "BotConnector" }, signal: AbortSignal.any([signal, AbortSignal.timeout(25e3)]) });
|
|
539
|
+
if (!res.ok) return void 0;
|
|
540
|
+
const content = (await res.json()).content;
|
|
541
|
+
return typeof content === "string" && content.trim() ? content.slice(0, MAX_CHARS) : void 0;
|
|
542
|
+
} catch {
|
|
543
|
+
return void 0;
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
async function loadPage(inputUrl, signal) {
|
|
547
|
+
let url = new URL(inputUrl);
|
|
548
|
+
let res;
|
|
549
|
+
try {
|
|
550
|
+
for (let hop = 0; hop < 5; hop++) {
|
|
551
|
+
res = await fetch(url, { headers: { accept: ACCEPT }, signal: AbortSignal.any([signal, AbortSignal.timeout(3e4)]), redirect: "manual" });
|
|
552
|
+
const location = res.status >= 300 && res.status < 400 ? res.headers.get("location") : null;
|
|
553
|
+
if (!location) break;
|
|
554
|
+
const next = new URL(location, url);
|
|
555
|
+
if (next.host !== url.host) {
|
|
326
556
|
await res.body?.cancel();
|
|
327
|
-
|
|
557
|
+
return { error: `Redirect to another host: ${next.href}. Call fetch again with that URL if needed (permission will be requested).` };
|
|
328
558
|
}
|
|
329
|
-
|
|
330
|
-
|
|
559
|
+
await res.body?.cancel();
|
|
560
|
+
url = next;
|
|
331
561
|
}
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
if (!res.ok) return { output: `HTTP ${res.status} dari ${input.url}: ${body.slice(0, 500)}`, isError: true };
|
|
335
|
-
const text = (res.headers.get("content-type") ?? "").includes("html") ? htmlToText(body) : body;
|
|
336
|
-
const clipped = text.length > MAX_CHARS || truncated ? `${text.slice(0, MAX_CHARS)}
|
|
337
|
-
\u2026 [dipotong]` : text;
|
|
338
|
-
const display = truncated ? "lebih dari 5 MB, dipotong" : `${text.length} karakter`;
|
|
339
|
-
return { output: clipped, display };
|
|
562
|
+
} catch (error) {
|
|
563
|
+
return { error: `Failed to fetch ${inputUrl}: ${describeFetchError(error)}` };
|
|
340
564
|
}
|
|
341
|
-
}
|
|
565
|
+
if (!res) return { error: `Failed to fetch ${inputUrl}` };
|
|
566
|
+
const { text: body, truncated } = await readCapped(res);
|
|
567
|
+
const type = res.headers.get("content-type") ?? "";
|
|
568
|
+
const text = type.includes("html") ? htmlToText(body) : body;
|
|
569
|
+
if (!res.ok) return { error: `HTTP ${res.status} from ${inputUrl}: ${text.slice(0, 500)}` };
|
|
570
|
+
return { text, truncated, at: Date.now() };
|
|
571
|
+
}
|
|
572
|
+
var fetchTool = createFetchTool();
|
|
342
573
|
|
|
343
574
|
// src/tools/glob.ts
|
|
344
|
-
import
|
|
575
|
+
import { glob as nativeGlob } from "fs/promises";
|
|
345
576
|
import { z as z4 } from "zod";
|
|
577
|
+
|
|
578
|
+
// src/abort.ts
|
|
579
|
+
var ABORTED = /* @__PURE__ */ Symbol("aborted");
|
|
580
|
+
function raceAbort(promise, signal) {
|
|
581
|
+
if (signal.aborted) return Promise.resolve(ABORTED);
|
|
582
|
+
return new Promise((resolve2, reject) => {
|
|
583
|
+
const onAbort = () => resolve2(ABORTED);
|
|
584
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
585
|
+
promise.then(
|
|
586
|
+
(value) => {
|
|
587
|
+
signal.removeEventListener("abort", onAbort);
|
|
588
|
+
resolve2(value);
|
|
589
|
+
},
|
|
590
|
+
(error) => {
|
|
591
|
+
signal.removeEventListener("abort", onAbort);
|
|
592
|
+
reject(error);
|
|
593
|
+
}
|
|
594
|
+
);
|
|
595
|
+
});
|
|
596
|
+
}
|
|
597
|
+
async function collectUntilAbort(iterable, signal, onItem) {
|
|
598
|
+
const iterator = iterable[Symbol.asyncIterator]();
|
|
599
|
+
try {
|
|
600
|
+
for (; ; ) {
|
|
601
|
+
const next = signal ? await raceAbort(iterator.next(), signal) : await iterator.next();
|
|
602
|
+
if (next === ABORTED) return false;
|
|
603
|
+
if (next.done) return true;
|
|
604
|
+
onItem(next.value);
|
|
605
|
+
}
|
|
606
|
+
} finally {
|
|
607
|
+
Promise.resolve(iterator.return?.(void 0)).catch(() => {
|
|
608
|
+
});
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
// src/tools/glob.ts
|
|
613
|
+
var MAX_OUTPUT_CHARS = 24e3;
|
|
346
614
|
var globTool = defineTool({
|
|
347
615
|
name: "glob",
|
|
348
616
|
description: 'Find files by glob pattern, e.g. "src/**/*.ts". Respects .gitignore. Max 500 results.',
|
|
@@ -353,12 +621,31 @@ var globTool = defineTool({
|
|
|
353
621
|
kind: "read",
|
|
354
622
|
target: (input) => input.pattern,
|
|
355
623
|
async run(input, ctx) {
|
|
624
|
+
const invalid = validateGlobPattern(input.pattern);
|
|
625
|
+
if (invalid) return { output: invalid, isError: true };
|
|
356
626
|
const root = resolvePath(ctx.cwd, input.path ?? ".");
|
|
357
|
-
const files =
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
627
|
+
const files = [];
|
|
628
|
+
const finished = await collectUntilAbort(
|
|
629
|
+
nativeGlob(input.pattern, { cwd: root, exclude: ignorePatterns(root) }),
|
|
630
|
+
ctx.signal,
|
|
631
|
+
(file) => files.push(normalizeGlobPath(file))
|
|
632
|
+
);
|
|
633
|
+
if (!finished) return { output: "Cancelled by the user.", isError: true };
|
|
634
|
+
files.sort();
|
|
635
|
+
if (!files.length) return { output: "No matching files." };
|
|
636
|
+
const first = files.slice(0, 500);
|
|
637
|
+
const shown = [];
|
|
638
|
+
let size = 0;
|
|
639
|
+
for (const file of first) {
|
|
640
|
+
const extra = file.length + (shown.length ? 1 : 0);
|
|
641
|
+
if (shown.length && size + extra > MAX_OUTPUT_CHARS) break;
|
|
642
|
+
shown.push(file);
|
|
643
|
+
size += extra;
|
|
644
|
+
if (size >= MAX_OUTPUT_CHARS) break;
|
|
645
|
+
}
|
|
646
|
+
const omitted = files.length - shown.length;
|
|
647
|
+
const more = omitted > 0 ? `
|
|
648
|
+
\u2026 ${omitted} more files omitted (narrow pattern/path)` : "";
|
|
362
649
|
return { output: shown.join("\n") + more, display: `${files.length} file` };
|
|
363
650
|
}
|
|
364
651
|
});
|
|
@@ -366,14 +653,22 @@ var globTool = defineTool({
|
|
|
366
653
|
// src/tools/grep.ts
|
|
367
654
|
import { execFile } from "child_process";
|
|
368
655
|
import { readFile as readFile2 } from "fs/promises";
|
|
369
|
-
import
|
|
656
|
+
import { glob as nativeGlob2 } from "fs/promises";
|
|
370
657
|
import { z as z5 } from "zod";
|
|
371
658
|
var MAX_MATCHES = 200;
|
|
659
|
+
var MAX_OUTPUT_CHARS2 = 24e3;
|
|
372
660
|
async function grepJs(pattern, opts) {
|
|
373
661
|
const regex = new RegExp(pattern, opts.ignoreCase ? "i" : "");
|
|
374
|
-
const files =
|
|
662
|
+
const files = [];
|
|
663
|
+
const finished = await collectUntilAbort(
|
|
664
|
+
nativeGlob2(opts.glob ?? "**/*", { cwd: opts.root, exclude: ignorePatterns(opts.root) }),
|
|
665
|
+
opts.signal,
|
|
666
|
+
(file) => files.push(normalizeGlobPath(file))
|
|
667
|
+
);
|
|
375
668
|
const out = [];
|
|
669
|
+
if (!finished) return out;
|
|
376
670
|
for (const file of files.sort()) {
|
|
671
|
+
if (opts.signal?.aborted) break;
|
|
377
672
|
let text;
|
|
378
673
|
try {
|
|
379
674
|
const buffer = await readFile2(resolvePath(opts.root, file));
|
|
@@ -396,7 +691,8 @@ function grepRg(pattern, opts) {
|
|
|
396
691
|
if (opts.glob) args.push("-g", opts.glob);
|
|
397
692
|
args.push(".");
|
|
398
693
|
return new Promise((resolve2) => {
|
|
399
|
-
execFile("rg", args, { cwd: opts.root, maxBuffer: 10 * 1024 * 1024 }, (error, stdout) => {
|
|
694
|
+
execFile("rg", args, { cwd: opts.root, maxBuffer: 10 * 1024 * 1024, signal: opts.signal }, (error, stdout) => {
|
|
695
|
+
if (opts.signal?.aborted) return resolve2([]);
|
|
400
696
|
const code = error?.code;
|
|
401
697
|
if (code === "ENOENT") return resolve2(void 0);
|
|
402
698
|
if (error && error.code !== 1) return resolve2(void 0);
|
|
@@ -416,15 +712,32 @@ var grepTool = defineTool({
|
|
|
416
712
|
kind: "read",
|
|
417
713
|
target: (input) => input.pattern,
|
|
418
714
|
async run(input, ctx) {
|
|
419
|
-
|
|
715
|
+
if (input.glob) {
|
|
716
|
+
const invalid = validateGlobPattern(input.glob);
|
|
717
|
+
if (invalid) return { output: invalid, isError: true };
|
|
718
|
+
}
|
|
719
|
+
const opts = { root: resolvePath(ctx.cwd, input.path ?? "."), glob: input.glob, ignoreCase: input.ignore_case, signal: ctx.signal };
|
|
420
720
|
try {
|
|
421
721
|
new RegExp(input.pattern);
|
|
422
722
|
} catch (error) {
|
|
423
|
-
return { output: `
|
|
723
|
+
return { output: `Invalid regex: ${error.message}`, isError: true };
|
|
424
724
|
}
|
|
425
725
|
const matches = await grepRg(input.pattern, opts) ?? await grepJs(input.pattern, opts);
|
|
426
|
-
if (
|
|
427
|
-
return { output:
|
|
726
|
+
if (ctx.signal.aborted) return { output: "Cancelled by the user.", isError: true };
|
|
727
|
+
if (!matches.length) return { output: "No matches." };
|
|
728
|
+
const shown = [];
|
|
729
|
+
let size = 0;
|
|
730
|
+
for (const match of matches) {
|
|
731
|
+
const extra = match.length + (shown.length ? 1 : 0);
|
|
732
|
+
if (shown.length && size + extra > MAX_OUTPUT_CHARS2) break;
|
|
733
|
+
shown.push(match);
|
|
734
|
+
size += extra;
|
|
735
|
+
if (size >= MAX_OUTPUT_CHARS2) break;
|
|
736
|
+
}
|
|
737
|
+
const omitted = matches.length - shown.length;
|
|
738
|
+
const suffix = omitted > 0 ? `
|
|
739
|
+
\u2026 ${omitted} more matches omitted (narrow pattern/path/glob)` : "";
|
|
740
|
+
return { output: shown.join("\n") + suffix, display: t("{n} results", { n: matches.length }) };
|
|
428
741
|
}
|
|
429
742
|
});
|
|
430
743
|
|
|
@@ -432,6 +745,7 @@ var grepTool = defineTool({
|
|
|
432
745
|
import { readFile as readFile3 } from "fs/promises";
|
|
433
746
|
import { z as z6 } from "zod";
|
|
434
747
|
var MAX_LINE = 2e3;
|
|
748
|
+
var MAX_OUTPUT_CHARS3 = 4e4;
|
|
435
749
|
var readTool = defineTool({
|
|
436
750
|
name: "read",
|
|
437
751
|
description: "Read a text file. Returns numbered lines (default first 2000). Use offset/limit for large files. Always read a file before editing it.",
|
|
@@ -448,25 +762,37 @@ var readTool = defineTool({
|
|
|
448
762
|
try {
|
|
449
763
|
buffer = await readFile3(abs);
|
|
450
764
|
} catch (error) {
|
|
451
|
-
return { output: `
|
|
765
|
+
return { output: `Cannot read ${input.path}: ${errorCode(error)}`, isError: true };
|
|
452
766
|
}
|
|
453
767
|
if (buffer.subarray(0, 8e3).includes(0)) {
|
|
454
|
-
return { output: `${input.path}
|
|
768
|
+
return { output: `${input.path} is a binary file; not read.`, isError: true };
|
|
455
769
|
}
|
|
456
770
|
const lines = buffer.toString("utf8").split(/\r?\n/);
|
|
457
771
|
const start = (input.offset ?? 1) - 1;
|
|
458
772
|
const limit = input.limit ?? 2e3;
|
|
459
|
-
const
|
|
460
|
-
const
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
773
|
+
const requested = lines.slice(start, start + limit);
|
|
774
|
+
const shown = [];
|
|
775
|
+
let size = 0;
|
|
776
|
+
for (let i = 0; i < requested.length; i++) {
|
|
777
|
+
const line = requested[i];
|
|
778
|
+
const text = line.length > MAX_LINE ? `${line.slice(0, MAX_LINE)}\u2026 [line truncated]` : line;
|
|
779
|
+
const rendered = `${String(start + i + 1).padStart(6)} ${text}`;
|
|
780
|
+
const extra = rendered.length + (shown.length ? 1 : 0);
|
|
781
|
+
if (shown.length && size + extra > MAX_OUTPUT_CHARS3) break;
|
|
782
|
+
shown.push(rendered);
|
|
783
|
+
size += extra;
|
|
784
|
+
if (size >= MAX_OUTPUT_CHARS3) break;
|
|
785
|
+
}
|
|
786
|
+
const body = shown.join("\n");
|
|
787
|
+
const remaining = lines.length - start - shown.length;
|
|
788
|
+
const tokenBudgetHit = shown.length < requested.length;
|
|
789
|
+
const nextOffset = start + shown.length + 1;
|
|
465
790
|
ctx.readFiles.add(abs);
|
|
466
791
|
return {
|
|
467
|
-
output: remaining > 0 ? `${body}
|
|
468
|
-
\u2026 ${remaining}
|
|
469
|
-
|
|
792
|
+
output: remaining > 0 ? tokenBudgetHit ? `${body}
|
|
793
|
+
\u2026 ${remaining} more lines (token budget reached; continue with offset=${nextOffset})` : `${body}
|
|
794
|
+
\u2026 ${remaining} more lines (use offset)` : body,
|
|
795
|
+
display: t("{n} lines", { n: shown.length })
|
|
470
796
|
};
|
|
471
797
|
}
|
|
472
798
|
});
|
|
@@ -487,7 +813,7 @@ var writeTool = defineTool({
|
|
|
487
813
|
target: (input) => input.path,
|
|
488
814
|
async validate(input, ctx) {
|
|
489
815
|
const abs = resolvePath(ctx.cwd, input.path);
|
|
490
|
-
return existsSync(abs) && !ctx.readFiles.has(abs) ? `${input.path}
|
|
816
|
+
return existsSync(abs) && !ctx.readFiles.has(abs) ? `${input.path} already exists. Read it with read before overwriting it.` : void 0;
|
|
491
817
|
},
|
|
492
818
|
async preview(input, ctx) {
|
|
493
819
|
const abs = resolvePath(ctx.cwd, input.path);
|
|
@@ -497,18 +823,18 @@ var writeTool = defineTool({
|
|
|
497
823
|
async run(input, ctx) {
|
|
498
824
|
const abs = resolvePath(ctx.cwd, input.path);
|
|
499
825
|
if (existsSync(abs) && !ctx.readFiles.has(abs)) {
|
|
500
|
-
return { output: `${input.path}
|
|
826
|
+
return { output: `${input.path} already exists. Read it with read before overwriting it.`, isError: true };
|
|
501
827
|
}
|
|
502
828
|
try {
|
|
503
829
|
await ctx.checkpoint?.(abs);
|
|
504
830
|
await mkdir(dirname(abs), { recursive: true });
|
|
505
831
|
await writeFile2(abs, input.content);
|
|
506
832
|
} catch (error) {
|
|
507
|
-
return { output: `
|
|
833
|
+
return { output: `Failed to write ${input.path}: ${errorCode(error)}`, isError: true };
|
|
508
834
|
}
|
|
509
835
|
ctx.readFiles.add(abs);
|
|
510
836
|
const lines = input.content.split("\n").length;
|
|
511
|
-
return { output: `
|
|
837
|
+
return { output: `Wrote ${input.path} (${lines} lines).`, display: t("{lines} lines written", { lines }) };
|
|
512
838
|
}
|
|
513
839
|
});
|
|
514
840
|
|
|
@@ -517,76 +843,86 @@ var ALL_TOOLS = [readTool, writeTool, editTool, bashTool, grepTool, globTool, fe
|
|
|
517
843
|
function toolDefinitions(tools) {
|
|
518
844
|
return tools.map((tool) => {
|
|
519
845
|
const { $schema: _ignored, ...parameters } = tool.jsonSchema ?? z8.toJSONSchema(tool.schema);
|
|
520
|
-
return { type: "function", function: { name: tool.name, description: tool.description, parameters } };
|
|
846
|
+
return { type: "function", function: { name: tool.name, description: tool.description, parameters, ...tool.outputJsonSchema ? { output_schema: tool.outputJsonSchema } : {} } };
|
|
521
847
|
});
|
|
522
848
|
}
|
|
523
849
|
|
|
524
|
-
// src/
|
|
525
|
-
var
|
|
526
|
-
var
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
850
|
+
// src/webBudget.ts
|
|
851
|
+
var KEEP_TOKENS = 4e4;
|
|
852
|
+
var PRUNE_MIN_TOKENS = 2e4;
|
|
853
|
+
var STUB = "[old tool result removed:";
|
|
854
|
+
var tokens = (text) => Math.ceil(text.length / 4);
|
|
855
|
+
var isStub = (content) => content.startsWith(STUB);
|
|
856
|
+
function pruneOldFetches(messages) {
|
|
857
|
+
const urls = /* @__PURE__ */ new Map();
|
|
858
|
+
for (const m of messages) {
|
|
859
|
+
if (m.role !== "assistant") continue;
|
|
860
|
+
for (const c of m.tool_calls ?? []) {
|
|
861
|
+
if (c.function.name !== "fetch") continue;
|
|
862
|
+
try {
|
|
863
|
+
urls.set(c.id, String(JSON.parse(c.function.arguments).url ?? ""));
|
|
864
|
+
} catch {
|
|
865
|
+
urls.set(c.id, "");
|
|
866
|
+
}
|
|
536
867
|
}
|
|
537
868
|
}
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
if (!(params.required ?? []).every((r) => keys.includes(r))) continue;
|
|
550
|
-
const score = keys.filter((k) => props.includes(k)).length;
|
|
551
|
-
if (score > bestScore) [best, bestScore, tie] = [t, score, false];
|
|
552
|
-
else if (score === bestScore && score > 0) tie = true;
|
|
869
|
+
let kept = 0;
|
|
870
|
+
let oldTokens = 0;
|
|
871
|
+
const old = [];
|
|
872
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
873
|
+
const m = messages[i];
|
|
874
|
+
if (m.role !== "tool" || !urls.has(m.tool_call_id) || isStub(m.content)) continue;
|
|
875
|
+
kept += tokens(m.content);
|
|
876
|
+
if (kept > KEEP_TOKENS) {
|
|
877
|
+
old.push(i);
|
|
878
|
+
oldTokens += tokens(m.content);
|
|
879
|
+
}
|
|
553
880
|
}
|
|
554
|
-
|
|
881
|
+
if (oldTokens <= PRUNE_MIN_TOKENS) return false;
|
|
882
|
+
for (const i of old) {
|
|
883
|
+
const m = messages[i];
|
|
884
|
+
messages[i] = { ...m, content: `${STUB} fetch ${urls.get(m.tool_call_id)}. Fetch it again (use prompt for a focused excerpt) if still needed.]` };
|
|
885
|
+
}
|
|
886
|
+
return true;
|
|
555
887
|
}
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
if (args.some(([k]) => k === m[1])) flush();
|
|
581
|
-
args.push([m[1], m[2]]);
|
|
888
|
+
var WebBudget = class {
|
|
889
|
+
fetches = 0;
|
|
890
|
+
chars = 0;
|
|
891
|
+
perTurn;
|
|
892
|
+
sessionChars;
|
|
893
|
+
constructor(limits = {}) {
|
|
894
|
+
this.perTurn = limits.perTurn ?? 8;
|
|
895
|
+
this.sessionChars = limits.sessionChars ?? 3e5;
|
|
896
|
+
}
|
|
897
|
+
startTurn() {
|
|
898
|
+
this.fetches = 0;
|
|
899
|
+
}
|
|
900
|
+
/** Gives back a reservation, e.g. when the fetch was served from cache and cost nothing. */
|
|
901
|
+
refund() {
|
|
902
|
+
if (this.fetches > 0) this.fetches--;
|
|
903
|
+
}
|
|
904
|
+
reset() {
|
|
905
|
+
this.fetches = 0;
|
|
906
|
+
this.chars = 0;
|
|
907
|
+
}
|
|
908
|
+
/** Reserves one fetch; returns the refusal to show the model when over budget. */
|
|
909
|
+
take() {
|
|
910
|
+
if (this.fetches >= this.perTurn) {
|
|
911
|
+
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.`;
|
|
582
912
|
}
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
913
|
+
if (this.chars >= this.sessionChars) {
|
|
914
|
+
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.`;
|
|
915
|
+
}
|
|
916
|
+
this.fetches++;
|
|
917
|
+
return void 0;
|
|
918
|
+
}
|
|
919
|
+
record(chars) {
|
|
920
|
+
this.chars += chars;
|
|
921
|
+
}
|
|
922
|
+
};
|
|
588
923
|
|
|
589
924
|
// src/agent.ts
|
|
925
|
+
var CANCELLED = "Cancelled by the user.";
|
|
590
926
|
var estimateTokens = (text) => Math.ceil(text.length / 4);
|
|
591
927
|
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.";
|
|
592
928
|
var Agent = class {
|
|
@@ -602,6 +938,7 @@ var Agent = class {
|
|
|
602
938
|
this.maxSteps = opts.maxSteps ?? 50;
|
|
603
939
|
this.contextWindow = opts.contextWindow ?? 128e3;
|
|
604
940
|
this.systemPrompt = opts.systemPrompt;
|
|
941
|
+
this.reasoning = opts.reasoning ?? "auto";
|
|
605
942
|
}
|
|
606
943
|
opts;
|
|
607
944
|
provider;
|
|
@@ -611,6 +948,7 @@ var Agent = class {
|
|
|
611
948
|
lastInputTokens = 0;
|
|
612
949
|
onEvent = () => {
|
|
613
950
|
};
|
|
951
|
+
reasoning;
|
|
614
952
|
askPermission = async () => "no";
|
|
615
953
|
tools;
|
|
616
954
|
definitions;
|
|
@@ -618,6 +956,7 @@ var Agent = class {
|
|
|
618
956
|
turnTools;
|
|
619
957
|
turnDefinitions;
|
|
620
958
|
readFiles = /* @__PURE__ */ new Set();
|
|
959
|
+
webBudget = new WebBudget();
|
|
621
960
|
maxSteps;
|
|
622
961
|
// ponytail: one window for every model; read it from /v1/models metadata when providers expose it.
|
|
623
962
|
contextWindow;
|
|
@@ -630,7 +969,7 @@ var Agent = class {
|
|
|
630
969
|
this.definitions = toolDefinitions(tools);
|
|
631
970
|
}
|
|
632
971
|
isParallelSafe(call) {
|
|
633
|
-
const tool = this.turnTools.find((
|
|
972
|
+
const tool = this.turnTools.find((t2) => t2.name === call.name);
|
|
634
973
|
if (!tool?.parallelSafe) return false;
|
|
635
974
|
try {
|
|
636
975
|
const parsed = tool.schema.safeParse(JSON.parse(call.arguments || "{}"));
|
|
@@ -647,11 +986,13 @@ var Agent = class {
|
|
|
647
986
|
load(messages) {
|
|
648
987
|
this.messages = messages;
|
|
649
988
|
this.readFiles.clear();
|
|
989
|
+
this.webBudget.reset();
|
|
650
990
|
this.lastInputTokens = 0;
|
|
651
991
|
}
|
|
652
992
|
clear() {
|
|
653
993
|
this.messages = [];
|
|
654
994
|
this.readFiles.clear();
|
|
995
|
+
this.webBudget.reset();
|
|
655
996
|
this.lastInputTokens = 0;
|
|
656
997
|
this.opts.onReset?.();
|
|
657
998
|
}
|
|
@@ -660,31 +1001,47 @@ var Agent = class {
|
|
|
660
1001
|
messages: [{ role: "system", content: this.systemPrompt }, ...this.messages, { role: "user", content: COMPACT_PROMPT }],
|
|
661
1002
|
// Some gateways reject tool_calls in history when no tools are declared.
|
|
662
1003
|
tools: this.definitions,
|
|
663
|
-
signal
|
|
1004
|
+
signal,
|
|
1005
|
+
reasoning: this.reasoning
|
|
664
1006
|
});
|
|
665
1007
|
this.clear();
|
|
666
|
-
this.push({ role: "user", content: `
|
|
1008
|
+
this.push({ role: "user", content: `Summary of the previous conversation:
|
|
667
1009
|
${completion.text}` });
|
|
668
|
-
this.push({ role: "assistant", content: "
|
|
1010
|
+
this.push({ role: "assistant", content: "OK, I will continue from this summary." });
|
|
669
1011
|
this.onEvent({ type: "compacted" });
|
|
670
1012
|
}
|
|
1013
|
+
/**
|
|
1014
|
+
* Events of one tool execution. A cancelled turn gives up on a tool that does not stop, and whatever that tool
|
|
1015
|
+
* reports later must not reach the screen or a newer turn, so this is per execution and not per call id (ids repeat).
|
|
1016
|
+
*/
|
|
1017
|
+
report(ticket, event) {
|
|
1018
|
+
if (ticket.abandoned) return;
|
|
1019
|
+
if (event.type === "toolStart") ticket.open = true;
|
|
1020
|
+
if (event.type === "toolEnd") ticket.open = false;
|
|
1021
|
+
this.onEvent(event);
|
|
1022
|
+
}
|
|
671
1023
|
async run(text, signal) {
|
|
672
1024
|
this.turnTools = this.tools;
|
|
673
1025
|
this.turnDefinitions = this.definitions;
|
|
674
1026
|
this.opts.onTurnStart?.();
|
|
1027
|
+
this.webBudget.startTurn();
|
|
675
1028
|
try {
|
|
676
|
-
if (this.lastInputTokens > this.contextWindow * 0.8) await this.compact(signal);
|
|
1029
|
+
if (this.lastInputTokens > this.contextWindow * 0.8 && !hasPendingProgrammaticReplay(this.messages)) await this.compact(signal);
|
|
677
1030
|
this.push({ role: "user", content: text });
|
|
678
1031
|
for (let step = 0; step < this.maxSteps; step++) {
|
|
679
|
-
if (step > 0 && this.lastInputTokens > this.contextWindow * 0.8) {
|
|
1032
|
+
if (step > 0 && this.lastInputTokens > this.contextWindow * 0.8 && !hasPendingProgrammaticReplay(this.messages)) {
|
|
680
1033
|
await this.compact(signal);
|
|
681
|
-
this.push({ role: "user", content: `
|
|
1034
|
+
this.push({ role: "user", content: `Continue this task using the summary above: ${text}` });
|
|
682
1035
|
}
|
|
1036
|
+
pruneOldFetches(this.messages);
|
|
683
1037
|
let completion = await this.provider.chat({
|
|
684
1038
|
messages: [{ role: "system", content: this.systemPrompt }, ...this.messages],
|
|
685
1039
|
tools: this.turnDefinitions,
|
|
1040
|
+
programmaticToolNames: this.turnTools.filter((tool) => tool.programmaticSafe).map((tool) => tool.name),
|
|
686
1041
|
signal,
|
|
687
|
-
onText: (delta) => this.onEvent({ type: "text", delta })
|
|
1042
|
+
onText: (delta) => this.onEvent({ type: "text", delta }),
|
|
1043
|
+
onThinking: (delta) => this.onEvent({ type: "thinking", delta }),
|
|
1044
|
+
reasoning: this.reasoning
|
|
688
1045
|
});
|
|
689
1046
|
if (!completion.toolCalls.length && completion.finishReason !== "repetition") {
|
|
690
1047
|
const recovered = recoverTextToolCalls(completion.text, this.turnDefinitions);
|
|
@@ -701,26 +1058,17 @@ ${completion.text}` });
|
|
|
701
1058
|
this.totalUsage.outputTokens += usage.outputTokens;
|
|
702
1059
|
this.lastInputTokens = usage.inputTokens;
|
|
703
1060
|
this.onEvent({ type: "usage", ...this.totalUsage });
|
|
704
|
-
this.push(
|
|
705
|
-
role: "assistant",
|
|
706
|
-
content: completion.text || null,
|
|
707
|
-
...completion.toolCalls.length ? {
|
|
708
|
-
tool_calls: completion.toolCalls.map((c) => ({
|
|
709
|
-
id: c.id,
|
|
710
|
-
type: "function",
|
|
711
|
-
function: { name: c.name, arguments: c.arguments }
|
|
712
|
-
}))
|
|
713
|
-
} : {}
|
|
714
|
-
});
|
|
1061
|
+
this.push(completionAssistantMessage(completion));
|
|
715
1062
|
if (completion.finishReason === "repetition") {
|
|
716
1063
|
this.onEvent({ type: "textReplace", text: completion.text });
|
|
717
1064
|
this.onEvent({
|
|
718
1065
|
type: "error",
|
|
719
|
-
message: "
|
|
1066
|
+
message: t("The model got stuck repeating text, so the answer was stopped. Try again or switch models with /model.")
|
|
720
1067
|
});
|
|
721
1068
|
return;
|
|
722
1069
|
}
|
|
723
1070
|
if (!completion.toolCalls.length) {
|
|
1071
|
+
if (completion.finishReason === "continue") continue;
|
|
724
1072
|
this.onEvent({ type: "done" });
|
|
725
1073
|
return;
|
|
726
1074
|
}
|
|
@@ -728,16 +1076,35 @@ ${completion.text}` });
|
|
|
728
1076
|
let i = 0;
|
|
729
1077
|
while (i < calls.length) {
|
|
730
1078
|
if (signal.aborted) {
|
|
731
|
-
|
|
1079
|
+
this.cancelCalls(calls.slice(i));
|
|
732
1080
|
this.onEvent({ type: "aborted" });
|
|
733
1081
|
return;
|
|
734
1082
|
}
|
|
735
1083
|
let j = i + 1;
|
|
736
1084
|
if (this.isParallelSafe(calls[i])) while (j < calls.length && this.isParallelSafe(calls[j])) j++;
|
|
737
1085
|
const batch = calls.slice(i, j);
|
|
738
|
-
const
|
|
1086
|
+
const tickets = batch.map(() => ({ abandoned: false, open: false }));
|
|
1087
|
+
const running = Promise.all(batch.map((c, k) => this.runTool(c, signal, tickets[k])));
|
|
1088
|
+
const outcome = await raceAbort(running, signal);
|
|
1089
|
+
if (outcome === ABORTED) {
|
|
1090
|
+
batch.forEach((c, k) => {
|
|
1091
|
+
const ticket = tickets[k];
|
|
1092
|
+
ticket.abandoned = true;
|
|
1093
|
+
if (ticket.open) this.onEvent({ type: "toolEnd", id: c.id, tool: c.name, output: CANCELLED, isError: true });
|
|
1094
|
+
});
|
|
1095
|
+
running.catch(() => {
|
|
1096
|
+
});
|
|
1097
|
+
this.cancelCalls(calls.slice(i));
|
|
1098
|
+
this.onEvent({ type: "aborted" });
|
|
1099
|
+
return;
|
|
1100
|
+
}
|
|
739
1101
|
batch.forEach((c, k) => {
|
|
740
|
-
this.push({
|
|
1102
|
+
this.push({
|
|
1103
|
+
role: "tool",
|
|
1104
|
+
tool_call_id: c.id,
|
|
1105
|
+
content: outcome[k],
|
|
1106
|
+
...c.caller ? { responses_caller: c.caller } : {}
|
|
1107
|
+
});
|
|
741
1108
|
});
|
|
742
1109
|
i = j;
|
|
743
1110
|
}
|
|
@@ -757,48 +1124,62 @@ ${completion.text}` });
|
|
|
757
1124
|
this.onEvent({
|
|
758
1125
|
type: "error",
|
|
759
1126
|
message: noTools ? `${message}
|
|
760
|
-
|
|
1127
|
+
${t("This model may not support tool calling. Try another model with /model.")}` : message
|
|
761
1128
|
});
|
|
762
1129
|
}
|
|
763
1130
|
}
|
|
764
|
-
|
|
1131
|
+
/** Every tool call needs an answer in the history, or the next request to the model is rejected. */
|
|
1132
|
+
cancelCalls(calls) {
|
|
1133
|
+
for (const pending of calls)
|
|
1134
|
+
this.push({
|
|
1135
|
+
role: "tool",
|
|
1136
|
+
tool_call_id: pending.id,
|
|
1137
|
+
content: CANCELLED,
|
|
1138
|
+
...pending.caller ? { responses_caller: pending.caller } : {}
|
|
1139
|
+
});
|
|
1140
|
+
}
|
|
1141
|
+
async runTool(call, signal, ticket) {
|
|
1142
|
+
const emit = (event) => this.report(ticket, event);
|
|
765
1143
|
const fail = (output, target2 = "") => {
|
|
766
|
-
|
|
767
|
-
|
|
1144
|
+
emit({ type: "toolStart", id: call.id, tool: call.name, target: target2 });
|
|
1145
|
+
emit({ type: "toolEnd", id: call.id, tool: call.name, output, isError: true });
|
|
768
1146
|
return output;
|
|
769
1147
|
};
|
|
770
|
-
const tool = this.turnTools.find((
|
|
771
|
-
if (!tool) return fail(`
|
|
1148
|
+
const tool = this.turnTools.find((t2) => t2.name === call.name);
|
|
1149
|
+
if (!tool) return fail(`Tool "${call.name}" does not exist. Available tools: ${this.turnTools.map((t2) => t2.name).join(", ")}`);
|
|
772
1150
|
let args;
|
|
773
1151
|
try {
|
|
774
1152
|
args = JSON.parse(call.arguments || "{}");
|
|
775
1153
|
} catch {
|
|
776
|
-
return fail(`
|
|
1154
|
+
return fail(`Arguments are not valid JSON: ${call.arguments.slice(0, 200)}`);
|
|
777
1155
|
}
|
|
778
1156
|
const parsed = tool.schema.safeParse(args);
|
|
779
1157
|
if (!parsed.success) {
|
|
780
|
-
return fail(`
|
|
1158
|
+
return fail(`Invalid arguments: ${parsed.error.issues.map((i) => `${i.path.join(".") || "(root)"}: ${i.message}`).join("; ")}`);
|
|
781
1159
|
}
|
|
782
1160
|
const input = parsed.data;
|
|
783
1161
|
const target = tool.target(input);
|
|
1162
|
+
const overBudget = tool.name === "fetch" ? this.webBudget.take() : void 0;
|
|
1163
|
+
if (overBudget) return fail(overBudget, target);
|
|
784
1164
|
const ctx = {
|
|
785
1165
|
cwd: this.opts.cwd,
|
|
786
1166
|
signal,
|
|
787
1167
|
readFiles: this.readFiles,
|
|
788
1168
|
callId: call.id,
|
|
789
|
-
emit
|
|
1169
|
+
emit,
|
|
790
1170
|
ask: (req) => this.askPermission(req),
|
|
791
1171
|
checkpoint: this.opts.checkpoint,
|
|
1172
|
+
refundFetch: () => this.webBudget.refund(),
|
|
792
1173
|
addUsage: (u) => {
|
|
793
1174
|
this.totalUsage.inputTokens += u.inputTokens;
|
|
794
1175
|
this.totalUsage.outputTokens += u.outputTokens;
|
|
795
1176
|
this.onEvent({ type: "usage", ...this.totalUsage });
|
|
796
1177
|
}
|
|
797
1178
|
};
|
|
798
|
-
|
|
1179
|
+
emit({ type: "toolStart", id: call.id, tool: tool.name, target });
|
|
799
1180
|
const invalid = await tool.validate?.(input, ctx).catch((error) => error.message);
|
|
800
1181
|
if (invalid) {
|
|
801
|
-
|
|
1182
|
+
emit({ type: "toolEnd", id: call.id, tool: tool.name, output: invalid, isError: true });
|
|
802
1183
|
return invalid;
|
|
803
1184
|
}
|
|
804
1185
|
const request = { tool: tool.name, kind: tool.kind, target };
|
|
@@ -816,8 +1197,8 @@ Model ini sepertinya tidak mendukung tool calling. Coba model lain dengan /model
|
|
|
816
1197
|
decision = answer === "no" ? "deny" : "allow";
|
|
817
1198
|
}
|
|
818
1199
|
if (decision === "deny") {
|
|
819
|
-
const output = this.permissions.mode === "plan" ? "
|
|
820
|
-
|
|
1200
|
+
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.";
|
|
1201
|
+
emit({ type: "toolEnd", id: call.id, tool: tool.name, output, isError: true });
|
|
821
1202
|
return output;
|
|
822
1203
|
}
|
|
823
1204
|
let result;
|
|
@@ -826,7 +1207,8 @@ Model ini sepertinya tidak mendukung tool calling. Coba model lain dengan /model
|
|
|
826
1207
|
} catch (error) {
|
|
827
1208
|
result = { output: `Error: ${error.message}`, isError: true };
|
|
828
1209
|
}
|
|
829
|
-
|
|
1210
|
+
if (tool.name === "fetch" && !result.isError) this.webBudget.record(result.output.length);
|
|
1211
|
+
emit({ type: "toolEnd", id: call.id, tool: tool.name, output: result.output, display: result.display, isError: !!result.isError });
|
|
830
1212
|
return result.output;
|
|
831
1213
|
}
|
|
832
1214
|
};
|
|
@@ -855,7 +1237,7 @@ var CheckpointStore = class {
|
|
|
855
1237
|
while (this.turns() > MAX_TURNS) this.history.shift();
|
|
856
1238
|
}
|
|
857
1239
|
turns() {
|
|
858
|
-
return this.history.filter((
|
|
1240
|
+
return this.history.filter((t2) => t2.size).length;
|
|
859
1241
|
}
|
|
860
1242
|
async undo() {
|
|
861
1243
|
while (this.history.length && !this.history.at(-1)?.size) this.history.pop();
|
|
@@ -890,9 +1272,10 @@ import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
|
|
890
1272
|
import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
|
|
891
1273
|
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
|
|
892
1274
|
import { z as z9 } from "zod";
|
|
1275
|
+
var MCP_OUTPUT_MAX_CHARS = 32e3;
|
|
893
1276
|
function mcpToolName(server, tool, taken) {
|
|
894
|
-
const
|
|
895
|
-
const base = `mcp__${
|
|
1277
|
+
const clean2 = (s) => s.replace(/[^A-Za-z0-9_-]+/g, "_");
|
|
1278
|
+
const base = `mcp__${clean2(server)}__${clean2(tool)}`.slice(0, 64);
|
|
896
1279
|
let name = base;
|
|
897
1280
|
for (let n = 2; taken.has(name); n++) name = `${base.slice(0, 64 - String(n).length - 1)}_${n}`;
|
|
898
1281
|
taken.add(name);
|
|
@@ -901,7 +1284,7 @@ function mcpToolName(server, tool, taken) {
|
|
|
901
1284
|
function withTimeout(promise, ms) {
|
|
902
1285
|
let timer;
|
|
903
1286
|
const timeout = new Promise((_, reject) => {
|
|
904
|
-
timer = setTimeout(() => reject(new Error(
|
|
1287
|
+
timer = setTimeout(() => reject(new Error(t("timeout after {n} seconds", { n: Math.round(ms / 1e3) }))), ms);
|
|
905
1288
|
});
|
|
906
1289
|
return Promise.race([promise, timeout]).finally(() => clearTimeout(timer));
|
|
907
1290
|
}
|
|
@@ -938,7 +1321,7 @@ var McpManager = class {
|
|
|
938
1321
|
});
|
|
939
1322
|
return;
|
|
940
1323
|
}
|
|
941
|
-
entry.tools = tools.map((
|
|
1324
|
+
entry.tools = tools.map((t2) => this.wrap(spec.name, client, t2));
|
|
942
1325
|
entry.state = { ...entry.state, status: "ready", tools: entry.tools.length };
|
|
943
1326
|
} catch (error) {
|
|
944
1327
|
entry.state = { ...entry.state, status: "error", error: error.message };
|
|
@@ -954,7 +1337,9 @@ var McpManager = class {
|
|
|
954
1337
|
description: `[MCP ${server}] ${tool.description ?? tool.name}`.slice(0, 1024),
|
|
955
1338
|
schema: z9.record(z9.string(), z9.unknown()),
|
|
956
1339
|
jsonSchema: tool.inputSchema,
|
|
1340
|
+
...tool.outputSchema ? { outputJsonSchema: tool.outputSchema } : {},
|
|
957
1341
|
kind: "mcp",
|
|
1342
|
+
programmaticSafe: tool.annotations?.readOnlyHint === true,
|
|
958
1343
|
target: (input) => {
|
|
959
1344
|
const text = JSON.stringify(input);
|
|
960
1345
|
return text.length > 80 ? `${text.slice(0, 77)}\u2026` : text;
|
|
@@ -962,9 +1347,12 @@ var McpManager = class {
|
|
|
962
1347
|
async run(input, ctx) {
|
|
963
1348
|
try {
|
|
964
1349
|
const result = await client.callTool({ name: tool.name, arguments: input }, void 0, { signal: ctx.signal });
|
|
965
|
-
const output = (result.content ?? []).map((c) => c.type === "text" ? c.text ?? "" : `[${c.type}]`).join("\n") || "(
|
|
966
|
-
return {
|
|
967
|
-
|
|
1350
|
+
const output = (result.content ?? []).map((c) => c.type === "text" ? c.text ?? "" : `[${c.type}]`).join("\n") || "(empty)";
|
|
1351
|
+
return {
|
|
1352
|
+
output: output.length > MCP_OUTPUT_MAX_CHARS ? `${output.slice(0, MCP_OUTPUT_MAX_CHARS)}
|
|
1353
|
+
\u2026 [truncated at ${MCP_OUTPUT_MAX_CHARS} characters; narrow the MCP request if possible]` : output,
|
|
1354
|
+
isError: !!result.isError
|
|
1355
|
+
};
|
|
968
1356
|
} catch (error) {
|
|
969
1357
|
return { output: `Server MCP ${server} error: ${error.message}`, isError: true };
|
|
970
1358
|
}
|
|
@@ -975,14 +1363,14 @@ var McpManager = class {
|
|
|
975
1363
|
const entry = this.servers.get(name);
|
|
976
1364
|
if (!entry) return;
|
|
977
1365
|
this.servers.delete(name);
|
|
978
|
-
for (const
|
|
979
|
-
if (entry.tools.length) this.opts.onToolsRemoved?.(entry.tools.map((
|
|
1366
|
+
for (const t2 of entry.tools) this.taken.delete(t2.name);
|
|
1367
|
+
if (entry.tools.length) this.opts.onToolsRemoved?.(entry.tools.map((t2) => t2.name));
|
|
980
1368
|
await entry.client?.close().catch(() => {
|
|
981
1369
|
});
|
|
982
1370
|
this.opts.onChange?.();
|
|
983
1371
|
}
|
|
984
1372
|
tools() {
|
|
985
|
-
return [...this.servers.values()].flatMap((s) => s.tools);
|
|
1373
|
+
return [...this.servers.values()].flatMap((s) => s.tools).sort((a, b) => a.name.localeCompare(b.name));
|
|
986
1374
|
}
|
|
987
1375
|
states() {
|
|
988
1376
|
return [...this.servers.values()].map((s) => s.state);
|
|
@@ -1003,15 +1391,15 @@ function createExitPlanTool(opts) {
|
|
|
1003
1391
|
description: "Plan mode only: present the finished plan to the user for approval. If approved you may start implementing it.",
|
|
1004
1392
|
schema: z10.object({ plan: z10.string().describe("The plan, in markdown") }),
|
|
1005
1393
|
kind: "read",
|
|
1006
|
-
target: () => "
|
|
1394
|
+
target: () => t("plan"),
|
|
1007
1395
|
async run(input) {
|
|
1008
|
-
if (opts.permissions.mode !== "plan") return { output: "exit_plan
|
|
1396
|
+
if (opts.permissions.mode !== "plan") return { output: "exit_plan is only used in plan mode.", isError: true };
|
|
1009
1397
|
const decision = await opts.interaction.approvePlan(input.plan);
|
|
1010
1398
|
if (decision === "no") {
|
|
1011
|
-
return { output: "
|
|
1399
|
+
return { output: "The user has not approved yet. Ask what needs to change, revise the plan, then call exit_plan again." };
|
|
1012
1400
|
}
|
|
1013
1401
|
opts.permissions.mode = decision;
|
|
1014
|
-
return { output: `
|
|
1402
|
+
return { output: `Plan approved. The mode is now ${decision}. Start carrying out the plan now.` };
|
|
1015
1403
|
}
|
|
1016
1404
|
});
|
|
1017
1405
|
}
|
|
@@ -1028,7 +1416,7 @@ function createSkillTool(skills) {
|
|
|
1028
1416
|
target: (input) => input.name,
|
|
1029
1417
|
async run(input) {
|
|
1030
1418
|
const skill = skills.find((s) => s.name === input.name);
|
|
1031
|
-
if (!skill) return { output: `Skill "${input.name}"
|
|
1419
|
+
if (!skill) return { output: `Skill "${input.name}" does not exist. Available: ${skills.map((s) => s.name).join(", ") || "(none)"}`, isError: true };
|
|
1032
1420
|
const { body } = parseFrontmatter(readFileSync(skill.file, "utf8"));
|
|
1033
1421
|
return { output: `Base directory for this skill: ${skill.dir}
|
|
1034
1422
|
|
|
@@ -1037,8 +1425,69 @@ ${body}`, display: skill.description.slice(0, 60) };
|
|
|
1037
1425
|
});
|
|
1038
1426
|
}
|
|
1039
1427
|
|
|
1040
|
-
// src/tools/
|
|
1428
|
+
// src/tools/websearch.ts
|
|
1041
1429
|
import { z as z12 } from "zod";
|
|
1430
|
+
var clean = (items, max) => (Array.isArray(items) ? items : []).filter((r) => typeof r?.url === "string" && /^https?:\/\//.test(r.url)).slice(0, max).map((r) => ({
|
|
1431
|
+
title: String(r.title || r.url).slice(0, 200),
|
|
1432
|
+
url: r.url,
|
|
1433
|
+
snippet: String(r.snippet || r.description || "").replace(/\s+/g, " ").trim().slice(0, 600)
|
|
1434
|
+
}));
|
|
1435
|
+
async function postJson(url, body, headers, signal) {
|
|
1436
|
+
const res = await fetch(url, {
|
|
1437
|
+
method: "POST",
|
|
1438
|
+
headers: { "content-type": "application/json", ...headers },
|
|
1439
|
+
body: JSON.stringify(body),
|
|
1440
|
+
signal: AbortSignal.any([signal, AbortSignal.timeout(3e4)])
|
|
1441
|
+
});
|
|
1442
|
+
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
|
1443
|
+
return res.json();
|
|
1444
|
+
}
|
|
1445
|
+
function createWebSearchTool(opts) {
|
|
1446
|
+
const keenableURL = opts.keenableURL ?? "https://api.keenable.ai/v1/search/public";
|
|
1447
|
+
return defineTool({
|
|
1448
|
+
name: "web_search",
|
|
1449
|
+
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.",
|
|
1450
|
+
schema: z12.object({
|
|
1451
|
+
query: z12.string().min(1).max(500).describe("Search query"),
|
|
1452
|
+
max_results: z12.number().int().min(1).max(10).optional().describe("Number of results (default 6)")
|
|
1453
|
+
}),
|
|
1454
|
+
kind: "read",
|
|
1455
|
+
target: (input) => input.query,
|
|
1456
|
+
parallelSafe: () => true,
|
|
1457
|
+
async run(input, ctx) {
|
|
1458
|
+
const max = input.max_results ?? 6;
|
|
1459
|
+
const errors = [];
|
|
1460
|
+
let results = [];
|
|
1461
|
+
const bc = opts.botconnector();
|
|
1462
|
+
if (bc) {
|
|
1463
|
+
try {
|
|
1464
|
+
const data = await postJson(`${bc.baseURL.replace(/\/+$/, "")}/web/search`, { query: input.query, max_results: max }, { authorization: `Bearer ${bc.apiKey}` }, ctx.signal);
|
|
1465
|
+
results = clean(data.results, max);
|
|
1466
|
+
} catch (error) {
|
|
1467
|
+
errors.push(`BotConnector: ${error.message}`);
|
|
1468
|
+
}
|
|
1469
|
+
}
|
|
1470
|
+
if (!results.length) {
|
|
1471
|
+
try {
|
|
1472
|
+
const data = await postJson(keenableURL, { query: input.query }, { "x-keenable-title": "BotConnector" }, ctx.signal);
|
|
1473
|
+
results = clean(data.results, max);
|
|
1474
|
+
} catch (error) {
|
|
1475
|
+
errors.push(`Keenable: ${error.message}`);
|
|
1476
|
+
}
|
|
1477
|
+
}
|
|
1478
|
+
if (!results.length) {
|
|
1479
|
+
return { output: errors.length ? `Web search failed (${errors.join("; ")}).` : "No results.", isError: errors.length > 0 };
|
|
1480
|
+
}
|
|
1481
|
+
const output = results.map((r, i) => `${i + 1}. ${r.title}
|
|
1482
|
+
${r.url}${r.snippet ? `
|
|
1483
|
+
${r.snippet}` : ""}`).join("\n");
|
|
1484
|
+
return { output, display: t("{n} results", { n: results.length }) };
|
|
1485
|
+
}
|
|
1486
|
+
});
|
|
1487
|
+
}
|
|
1488
|
+
|
|
1489
|
+
// src/tools/task.ts
|
|
1490
|
+
import { z as z13 } from "zod";
|
|
1042
1491
|
function createTaskTool(opts) {
|
|
1043
1492
|
const byName = new Map(opts.agents.map((a) => [a.name, a]));
|
|
1044
1493
|
const list = opts.agents.map((a) => `- ${a.name}: ${a.description}`).join("\n");
|
|
@@ -1046,10 +1495,10 @@ function createTaskTool(opts) {
|
|
|
1046
1495
|
name: "task",
|
|
1047
1496
|
description: `Delegate a self-contained sub-task to a subagent. It starts with an empty context and returns only its final report. Available agents:
|
|
1048
1497
|
${list}`,
|
|
1049
|
-
schema:
|
|
1050
|
-
agent:
|
|
1051
|
-
description:
|
|
1052
|
-
prompt:
|
|
1498
|
+
schema: z13.object({
|
|
1499
|
+
agent: z13.string().describe("Agent name"),
|
|
1500
|
+
description: z13.string().describe("3-6 word summary shown to the user"),
|
|
1501
|
+
prompt: z13.string().describe("Full instructions; the subagent sees nothing else")
|
|
1053
1502
|
}),
|
|
1054
1503
|
kind: "read",
|
|
1055
1504
|
target: (input) => `[${input.agent}] ${input.description}`,
|
|
@@ -1058,16 +1507,16 @@ ${list}`,
|
|
|
1058
1507
|
const allowed = byName.get(input.agent)?.tools;
|
|
1059
1508
|
if (!allowed) return false;
|
|
1060
1509
|
const base = opts.baseTools();
|
|
1061
|
-
return allowed.every((name) => base.find((
|
|
1510
|
+
return allowed.every((name) => base.find((t2) => t2.name === name)?.kind === "read");
|
|
1062
1511
|
},
|
|
1063
1512
|
async run(input, ctx) {
|
|
1064
1513
|
const def = byName.get(input.agent);
|
|
1065
|
-
if (!def) return { output: `Agent "${input.agent}"
|
|
1066
|
-
const base = opts.baseTools().filter((
|
|
1067
|
-
const unknown = (def.tools ?? []).filter((n) => !base.some((
|
|
1068
|
-
if (unknown.length) return { output: `Agent "${def.name}":
|
|
1514
|
+
if (!def) return { output: `Agent "${input.agent}" does not exist. Available: ${[...byName.keys()].join(", ")}`, isError: true };
|
|
1515
|
+
const base = opts.baseTools().filter((t2) => t2.name !== "task" && t2.name !== "exit_plan");
|
|
1516
|
+
const unknown = (def.tools ?? []).filter((n) => !base.some((t2) => t2.name === n));
|
|
1517
|
+
if (unknown.length) return { output: `Agent "${def.name}": unknown tools: ${unknown.join(", ")}`, isError: true };
|
|
1069
1518
|
const allowed = def.tools;
|
|
1070
|
-
const tools = allowed ? base.filter((
|
|
1519
|
+
const tools = allowed ? base.filter((t2) => allowed.includes(t2.name)) : base;
|
|
1071
1520
|
let provider;
|
|
1072
1521
|
try {
|
|
1073
1522
|
provider = def.model ? opts.providerFor(def.model) : opts.provider();
|
|
@@ -1085,6 +1534,7 @@ ${def.prompt}`,
|
|
|
1085
1534
|
cwd: opts.cwd,
|
|
1086
1535
|
maxSteps: def.maxSteps ?? 50,
|
|
1087
1536
|
label: def.name,
|
|
1537
|
+
reasoning: opts.reasoning?.() ?? "auto",
|
|
1088
1538
|
// Subagent edits belong to the parent's turn so /undo reverts them too.
|
|
1089
1539
|
checkpoint: ctx.checkpoint
|
|
1090
1540
|
});
|
|
@@ -1098,22 +1548,22 @@ ${def.prompt}`,
|
|
|
1098
1548
|
if (ctx.ask) child.askPermission = ctx.ask;
|
|
1099
1549
|
await child.run(input.prompt, ctx.signal);
|
|
1100
1550
|
ctx.addUsage?.(child.totalUsage);
|
|
1101
|
-
const
|
|
1102
|
-
const display =
|
|
1103
|
-
if (outcome?.type === "error") return { output: `Subagent ${def.name}
|
|
1104
|
-
if (outcome?.type === "aborted") return { output: `Subagent ${def.name}
|
|
1551
|
+
const tokens2 = child.totalUsage.inputTokens + child.totalUsage.outputTokens;
|
|
1552
|
+
const display = t("{steps} steps \xB7 {tokens} tokens", { steps, tokens: tokens2 >= 1e3 ? `${Math.round(tokens2 / 1e3)}k` : tokens2 });
|
|
1553
|
+
if (outcome?.type === "error") return { output: `Subagent ${def.name} failed: ${outcome.message}`, isError: true, display };
|
|
1554
|
+
if (outcome?.type === "aborted") return { output: `Subagent ${def.name} was cancelled.`, isError: true, display };
|
|
1105
1555
|
const last = [...child.messages].reverse().find((m) => m.role === "assistant" && m.content);
|
|
1106
|
-
const report = last && typeof last.content === "string" ? last.content : "(
|
|
1556
|
+
const report = last && typeof last.content === "string" ? last.content : "(no report)";
|
|
1107
1557
|
const suffix = outcome?.type === "stepLimit" ? `
|
|
1108
1558
|
|
|
1109
|
-
(
|
|
1559
|
+
(The subagent stopped at its ${def.maxSteps ?? 50}-step limit.)` : "";
|
|
1110
1560
|
return { output: report + suffix, display };
|
|
1111
1561
|
}
|
|
1112
1562
|
});
|
|
1113
1563
|
}
|
|
1114
1564
|
|
|
1115
1565
|
// src/tools/todo.ts
|
|
1116
|
-
import { z as
|
|
1566
|
+
import { z as z14 } from "zod";
|
|
1117
1567
|
var TodoStore = class {
|
|
1118
1568
|
items = [];
|
|
1119
1569
|
listeners = /* @__PURE__ */ new Set();
|
|
@@ -1130,36 +1580,47 @@ function createTodoTool(store) {
|
|
|
1130
1580
|
return defineTool({
|
|
1131
1581
|
name: "todo_write",
|
|
1132
1582
|
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.",
|
|
1133
|
-
schema:
|
|
1134
|
-
todos:
|
|
1583
|
+
schema: z14.object({
|
|
1584
|
+
todos: z14.array(z14.object({ content: z14.string(), status: z14.enum(["pending", "in_progress", "completed"]) }))
|
|
1135
1585
|
}),
|
|
1136
1586
|
kind: "read",
|
|
1137
1587
|
target: (input) => `${input.todos.length} item`,
|
|
1138
1588
|
async run(input) {
|
|
1139
|
-
if (input.todos.filter((
|
|
1140
|
-
return { output: "
|
|
1589
|
+
if (input.todos.filter((t2) => t2.status === "in_progress").length > 1) {
|
|
1590
|
+
return { output: "Only one item may be in_progress.", isError: true };
|
|
1141
1591
|
}
|
|
1142
1592
|
store.set(input.todos);
|
|
1143
|
-
const done = input.todos.filter((
|
|
1144
|
-
return { output: `Todo
|
|
1593
|
+
const done = input.todos.filter((t2) => t2.status === "completed").length;
|
|
1594
|
+
return { output: `Todo updated: ${done}/${input.todos.length} completed.` };
|
|
1145
1595
|
}
|
|
1146
1596
|
});
|
|
1147
1597
|
}
|
|
1148
1598
|
|
|
1149
1599
|
// src/setup.ts
|
|
1150
1600
|
var TOOL_RULES = { bash: "bash", edit: "edit", write: "edit", fetch: "fetch" };
|
|
1601
|
+
function lunaPtcAutoEnabled(env) {
|
|
1602
|
+
const value = String(env.BCCLI_LUNA_PTC ?? "").trim().toLowerCase();
|
|
1603
|
+
return !["0", "false", "off", "no"].includes(value);
|
|
1604
|
+
}
|
|
1151
1605
|
function createRuntime(opts) {
|
|
1152
1606
|
const env = opts.env ?? process.env;
|
|
1153
1607
|
const home = bccliHome(env);
|
|
1154
1608
|
let config = loadConfig(opts.cwd, env);
|
|
1155
1609
|
let modelRef = opts.args.model ?? config.model;
|
|
1610
|
+
let reasoning = opts.args.reasoning ?? config.reasoning;
|
|
1156
1611
|
const makeProvider = (ref) => {
|
|
1157
1612
|
const resolved = resolveModel(config, ref, env);
|
|
1158
|
-
return opts.provider ?? createProvider({
|
|
1613
|
+
return opts.provider ?? createProvider({
|
|
1614
|
+
baseURL: resolved.baseURL,
|
|
1615
|
+
apiKey: resolved.apiKey,
|
|
1616
|
+
model: resolved.model,
|
|
1617
|
+
providerId: resolved.providerId,
|
|
1618
|
+
enableProgrammaticToolCalling: lunaPtcAutoEnabled(env)
|
|
1619
|
+
});
|
|
1159
1620
|
};
|
|
1160
1621
|
const provider = makeProvider(modelRef);
|
|
1161
1622
|
const mode = opts.args.allowAll ? "allowAll" : opts.args.permissionMode ?? config.permissionMode;
|
|
1162
|
-
const rules = [...config.allow, ...opts.args.allowedTools.map((
|
|
1623
|
+
const rules = [...config.allow, ...opts.args.allowedTools.map((t2) => TOOL_RULES[t2] ?? t2)];
|
|
1163
1624
|
const permissions = new Permissions(mode, rules, opts.cwd);
|
|
1164
1625
|
const roots = { cwd: opts.cwd, home, userHome: opts.userHome ?? homedir() };
|
|
1165
1626
|
const skills = loadSkills(roots);
|
|
@@ -1179,16 +1640,27 @@ function createRuntime(opts) {
|
|
|
1179
1640
|
}
|
|
1180
1641
|
}
|
|
1181
1642
|
const systemPrompt = buildSystemPrompt({ cwd: opts.cwd, home, model: modelRef, skills });
|
|
1182
|
-
const
|
|
1643
|
+
const webSearch = createWebSearchTool({
|
|
1644
|
+
botconnector: () => {
|
|
1645
|
+
try {
|
|
1646
|
+
const bc = resolveModel(config, "bc-cloud/search", env);
|
|
1647
|
+
return bc.apiKey ? { baseURL: bc.baseURL, apiKey: bc.apiKey } : void 0;
|
|
1648
|
+
} catch {
|
|
1649
|
+
return void 0;
|
|
1650
|
+
}
|
|
1651
|
+
}
|
|
1652
|
+
});
|
|
1653
|
+
const baseTools = [...ALL_TOOLS, webSearch, createSkillTool(skills), createTodoTool(todos), createExitPlanTool({ permissions, interaction })];
|
|
1183
1654
|
const task = createTaskTool({
|
|
1184
1655
|
agents: [...BUILTIN_AGENTS, ...agentDefs],
|
|
1185
1656
|
// Subagents get whatever the main agent has right now (incl. MCP tools), minus task itself.
|
|
1186
|
-
baseTools: () => agent.tools.filter((
|
|
1657
|
+
baseTools: () => agent.tools.filter((t2) => t2.name !== "task"),
|
|
1187
1658
|
permissions,
|
|
1188
1659
|
provider: () => agent.provider,
|
|
1189
1660
|
providerFor: makeProvider,
|
|
1190
1661
|
systemPrompt,
|
|
1191
|
-
cwd: opts.cwd
|
|
1662
|
+
cwd: opts.cwd,
|
|
1663
|
+
reasoning: () => reasoning
|
|
1192
1664
|
});
|
|
1193
1665
|
const agent = new Agent({
|
|
1194
1666
|
provider,
|
|
@@ -1197,6 +1669,7 @@ function createRuntime(opts) {
|
|
|
1197
1669
|
systemPrompt,
|
|
1198
1670
|
cwd: opts.cwd,
|
|
1199
1671
|
history,
|
|
1672
|
+
reasoning,
|
|
1200
1673
|
onMessage: (m) => session.append(m),
|
|
1201
1674
|
onReset: () => session.reset(),
|
|
1202
1675
|
onTurnStart: () => checkpoints.beginTurn(),
|
|
@@ -1238,6 +1711,9 @@ function createRuntime(opts) {
|
|
|
1238
1711
|
get modelRef() {
|
|
1239
1712
|
return modelRef;
|
|
1240
1713
|
},
|
|
1714
|
+
get reasoning() {
|
|
1715
|
+
return reasoning;
|
|
1716
|
+
},
|
|
1241
1717
|
agent,
|
|
1242
1718
|
get session() {
|
|
1243
1719
|
return session;
|
|
@@ -1267,36 +1743,59 @@ function createRuntime(opts) {
|
|
|
1267
1743
|
setModel(ref) {
|
|
1268
1744
|
agent.provider = makeProvider(ref);
|
|
1269
1745
|
modelRef = ref;
|
|
1746
|
+
},
|
|
1747
|
+
setReasoning(level) {
|
|
1748
|
+
reasoning = level;
|
|
1749
|
+
agent.reasoning = level;
|
|
1270
1750
|
}
|
|
1271
1751
|
};
|
|
1272
1752
|
}
|
|
1273
1753
|
|
|
1274
1754
|
// src/version.ts
|
|
1275
|
-
var VERSION = true ? "0.4.0-beta.
|
|
1755
|
+
var VERSION = true ? "0.4.0-beta.31" : "0.0.0-dev";
|
|
1276
1756
|
|
|
1277
1757
|
// src/cli.ts
|
|
1758
|
+
function chooseLanguage(argv) {
|
|
1759
|
+
let fromConfig;
|
|
1760
|
+
try {
|
|
1761
|
+
fromConfig = readGlobalConfigFile(process.env).language;
|
|
1762
|
+
} catch {
|
|
1763
|
+
}
|
|
1764
|
+
setLanguage(resolveLanguage(argv, process.env, fromConfig));
|
|
1765
|
+
}
|
|
1278
1766
|
async function main() {
|
|
1767
|
+
chooseLanguage(process.argv.slice(2));
|
|
1279
1768
|
const args = parseCliArgs(process.argv.slice(2));
|
|
1280
1769
|
if (args.version) {
|
|
1281
1770
|
console.log(VERSION);
|
|
1282
1771
|
return 0;
|
|
1283
1772
|
}
|
|
1284
1773
|
if (args.help) {
|
|
1285
|
-
console.log(
|
|
1774
|
+
console.log(helpText());
|
|
1286
1775
|
return 0;
|
|
1287
1776
|
}
|
|
1288
1777
|
const cwd = process.cwd();
|
|
1289
1778
|
if (args.command === "login") return runLogin(args.loginProvider ?? "bc-cloud", cwd);
|
|
1290
1779
|
if (args.command === "mcp") {
|
|
1291
|
-
const { runMcpCommand } = await import("./mcpCli-
|
|
1292
|
-
const { readSecret } = await import("./login-
|
|
1780
|
+
const { runMcpCommand } = await import("./mcpCli-UGNEDBMU.js");
|
|
1781
|
+
const { readSecret } = await import("./login-AP2ODLJX.js");
|
|
1293
1782
|
return runMcpCommand(args, { env: process.env, cwd, out: (s) => console.log(s), err: (s) => console.error(s), readSecret });
|
|
1294
1783
|
}
|
|
1295
1784
|
if (args.command === "provider") {
|
|
1296
|
-
const { runProviderCommand } = await import("./providerCli-
|
|
1297
|
-
const { readSecret } = await import("./login-
|
|
1785
|
+
const { runProviderCommand } = await import("./providerCli-J4Y7D6PF.js");
|
|
1786
|
+
const { readSecret } = await import("./login-AP2ODLJX.js");
|
|
1298
1787
|
return runProviderCommand(args, { env: process.env, cwd, out: (s) => console.log(s), err: (s) => console.error(s), readSecret });
|
|
1299
1788
|
}
|
|
1789
|
+
if (args.command === "connect" || args.command === "disconnect" || args.command === "integrations") {
|
|
1790
|
+
const { runIntegrationCommand } = await import("./integrationCli-G4C6XQUE.js");
|
|
1791
|
+
return runIntegrationCommand(args, {
|
|
1792
|
+
env: process.env,
|
|
1793
|
+
cwd,
|
|
1794
|
+
out: (s) => console.log(s),
|
|
1795
|
+
err: (s) => console.error(s),
|
|
1796
|
+
fetch: globalThis.fetch
|
|
1797
|
+
});
|
|
1798
|
+
}
|
|
1300
1799
|
if (args.command === "models") {
|
|
1301
1800
|
const config = loadConfig(cwd);
|
|
1302
1801
|
const resolved = resolveModel(config, args.model ?? config.model);
|
|
@@ -1310,11 +1809,11 @@ async function main() {
|
|
|
1310
1809
|
} catch {
|
|
1311
1810
|
}
|
|
1312
1811
|
if (args.print) {
|
|
1313
|
-
if (!args.prompt) throw new ConfigError('
|
|
1314
|
-
const { serversToStart } = await import("./config-
|
|
1812
|
+
if (!args.prompt) throw new ConfigError(t('-p mode needs a task, e.g. bccli -p "explain this repo"'));
|
|
1813
|
+
const { serversToStart } = await import("./config-DMF4AO7E.js");
|
|
1315
1814
|
const plan = serversToStart(rt.home, cwd);
|
|
1316
1815
|
for (const s of plan.needTrust) {
|
|
1317
|
-
console.error(
|
|
1816
|
+
console.error(t('Project MCP server "{name}" skipped (not approved yet; run interactive bccli once to approve it).', { name: s.name }));
|
|
1318
1817
|
}
|
|
1319
1818
|
await rt.startMcp(plan.start);
|
|
1320
1819
|
try {
|
|
@@ -1323,8 +1822,8 @@ async function main() {
|
|
|
1323
1822
|
await rt.mcp.stop();
|
|
1324
1823
|
}
|
|
1325
1824
|
}
|
|
1326
|
-
if (!process.stdin.isTTY) throw new ConfigError('
|
|
1327
|
-
const { startInteractive } = await import("./ui-
|
|
1825
|
+
if (!process.stdin.isTTY) throw new ConfigError(t('Interactive mode needs a terminal. For scripts/CI use: bccli -p "task"'));
|
|
1826
|
+
const { startInteractive } = await import("./ui-EFY7SNIW.js");
|
|
1328
1827
|
return startInteractive(rt, { initialPrompt: args.prompt, resume: args.resume, version: VERSION });
|
|
1329
1828
|
}
|
|
1330
1829
|
main().then(
|