@botconnector/bccli 0.4.0-beta.2 → 0.4.0-beta.21
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-XQYEBOKB.js +12 -0
- package/dist/chunk-34STZH4U.js +787 -0
- package/dist/{chunk-NGLKANMD.js → chunk-44MXWCFN.js} +4 -3
- package/dist/{chunk-UESQNU4A.js → chunk-BEXQSEBI.js} +11 -10
- package/dist/chunk-EO7IVVX4.js +443 -0
- package/dist/{chunk-OSWQPSA4.js → chunk-HP3J6JEY.js} +13 -4
- package/dist/{chunk-U7DLMVPZ.js → chunk-K5JNR3HL.js} +2 -1
- package/dist/chunk-ON6JYPE7.js +103 -0
- package/dist/chunk-SN3ODJZN.js +63 -0
- package/dist/{chunk-L7L4RGKG.js → chunk-VNIUYPVN.js} +2 -2
- package/dist/cli.js +569 -146
- package/dist/{config-NPSYFAFD.js → config-PBEZ3IGD.js} +2 -2
- package/dist/integrationCli-ZTZSZLO7.js +586 -0
- package/dist/{login-KHU7ONSR.js → login-TG5LQP3I.js} +1 -1
- package/dist/{mcpCli-Q2H63ISP.js → mcpCli-J43FHF2H.js} +11 -10
- package/dist/providerCli-6FXMBKQL.js +12 -0
- package/dist/{ui-A32PTF4F.js → ui-SRXPL7KU.js} +773 -232
- package/package.json +5 -1
- 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,
|
|
@@ -20,13 +8,27 @@ import {
|
|
|
20
8
|
loadSkills,
|
|
21
9
|
parseFrontmatter,
|
|
22
10
|
runPrint
|
|
23
|
-
} from "./chunk-
|
|
11
|
+
} from "./chunk-HP3J6JEY.js";
|
|
12
|
+
import {
|
|
13
|
+
helpText,
|
|
14
|
+
parseCliArgs
|
|
15
|
+
} from "./chunk-ON6JYPE7.js";
|
|
24
16
|
import {
|
|
25
17
|
Permissions
|
|
26
18
|
} from "./chunk-ELML4KVQ.js";
|
|
27
19
|
import {
|
|
28
|
-
|
|
29
|
-
|
|
20
|
+
addProviderKey,
|
|
21
|
+
createProvider,
|
|
22
|
+
listAllModels
|
|
23
|
+
} from "./chunk-34STZH4U.js";
|
|
24
|
+
import {
|
|
25
|
+
providerName,
|
|
26
|
+
readGlobalConfigFile
|
|
27
|
+
} from "./chunk-K5JNR3HL.js";
|
|
28
|
+
import "./chunk-SN3ODJZN.js";
|
|
29
|
+
import {
|
|
30
|
+
runLogin
|
|
31
|
+
} from "./chunk-VNIUYPVN.js";
|
|
30
32
|
import {
|
|
31
33
|
Session,
|
|
32
34
|
pruneSessions
|
|
@@ -35,8 +37,76 @@ import {
|
|
|
35
37
|
ConfigError,
|
|
36
38
|
bccliHome,
|
|
37
39
|
loadConfig,
|
|
38
|
-
|
|
39
|
-
|
|
40
|
+
resolveLanguage,
|
|
41
|
+
resolveModel,
|
|
42
|
+
setLanguage,
|
|
43
|
+
t
|
|
44
|
+
} from "./chunk-EO7IVVX4.js";
|
|
45
|
+
|
|
46
|
+
// src/textToolCalls.ts
|
|
47
|
+
var PAIR = /(?:<arg_key>\s*|`)?([A-Za-z_]\w*)(?:\s*<\/arg_key>)?`?\s*(?:=\s*`|<arg_value>)([\s\S]*?)<\/arg_value>/g;
|
|
48
|
+
var CALL_START = /<tool_call>|\n?[^\n]*\bTool call list\b[^\n]*|\n\s*\d+\.\s*Tool:/g;
|
|
49
|
+
function coerce(value, type) {
|
|
50
|
+
const types = Array.isArray(type) ? type : [type];
|
|
51
|
+
const v = value.trim();
|
|
52
|
+
if ((types.includes("integer") || types.includes("number")) && v !== "" && !Number.isNaN(Number(v))) return Number(v);
|
|
53
|
+
if (types.includes("boolean") && /^(true|false)$/i.test(v)) return v.toLowerCase() === "true";
|
|
54
|
+
if ((types.includes("object") || types.includes("array")) && /^[[{]/.test(v)) {
|
|
55
|
+
try {
|
|
56
|
+
return JSON.parse(v);
|
|
57
|
+
} catch {
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
return value.replace(/^\n+|\n+$/g, "");
|
|
61
|
+
}
|
|
62
|
+
function pickTool(name, keys, tools) {
|
|
63
|
+
const named = name && tools.find((t2) => t2.function.name === name);
|
|
64
|
+
if (named) return named;
|
|
65
|
+
let best;
|
|
66
|
+
let bestScore = 0;
|
|
67
|
+
let tie = false;
|
|
68
|
+
for (const t2 of tools) {
|
|
69
|
+
const params = t2.function.parameters;
|
|
70
|
+
const props = Object.keys(params.properties ?? {});
|
|
71
|
+
if (!(params.required ?? []).every((r) => keys.includes(r))) continue;
|
|
72
|
+
const score = keys.filter((k) => props.includes(k)).length;
|
|
73
|
+
if (score > bestScore) [best, bestScore, tie] = [t2, score, false];
|
|
74
|
+
else if (score === bestScore && score > 0) tie = true;
|
|
75
|
+
}
|
|
76
|
+
return tie ? void 0 : best;
|
|
77
|
+
}
|
|
78
|
+
function recoverTextToolCalls(text, tools) {
|
|
79
|
+
if (!/<\/arg_value>|<tool_call>/.test(text)) return null;
|
|
80
|
+
const starts = [...text.matchAll(CALL_START)].map((m) => m.index ?? 0);
|
|
81
|
+
const firstPair = text.search(PAIR);
|
|
82
|
+
const cut = Math.min(...[...starts, firstPair].filter((i) => i >= 0));
|
|
83
|
+
const bounds = starts.filter((i) => i >= cut);
|
|
84
|
+
if (!bounds.length || bounds[0] > cut) bounds.unshift(cut);
|
|
85
|
+
const toolCalls = [];
|
|
86
|
+
bounds.forEach((start, n) => {
|
|
87
|
+
const segment = text.slice(start, bounds[n + 1] ?? text.length);
|
|
88
|
+
const name = /<tool_call>\s*([A-Za-z_][\w.-]*)/.exec(segment)?.[1] ?? /Tool:\s*`?([A-Za-z_][\w.-]*)`?/.exec(segment)?.[1];
|
|
89
|
+
let args = [];
|
|
90
|
+
const flush = () => {
|
|
91
|
+
if (!args.length) return;
|
|
92
|
+
const keys = args.map(([k]) => k);
|
|
93
|
+
const tool = pickTool(name, keys, tools);
|
|
94
|
+
if (tool) {
|
|
95
|
+
const props = tool.function.parameters.properties ?? {};
|
|
96
|
+
const input = Object.fromEntries(args.filter(([k]) => k in props).map(([k, v]) => [k, coerce(v, props[k].type)]));
|
|
97
|
+
toolCalls.push({ id: `text_call_${toolCalls.length}`, name: tool.function.name, arguments: JSON.stringify(input) });
|
|
98
|
+
}
|
|
99
|
+
args = [];
|
|
100
|
+
};
|
|
101
|
+
for (const m of segment.matchAll(PAIR)) {
|
|
102
|
+
if (args.some(([k]) => k === m[1])) flush();
|
|
103
|
+
args.push([m[1], m[2]]);
|
|
104
|
+
}
|
|
105
|
+
flush();
|
|
106
|
+
});
|
|
107
|
+
if (!toolCalls.length) return null;
|
|
108
|
+
return { text: text.slice(0, cut).trim(), toolCalls };
|
|
109
|
+
}
|
|
40
110
|
|
|
41
111
|
// src/tools/index.ts
|
|
42
112
|
import { z as z8 } from "zod";
|
|
@@ -136,7 +206,7 @@ function runCommand(command, opts) {
|
|
|
136
206
|
clearTimeout(timer);
|
|
137
207
|
opts.signal.removeEventListener("abort", onAbort);
|
|
138
208
|
const output = dropped ? `${head}
|
|
139
|
-
\u2026 [${dropped}
|
|
209
|
+
\u2026 [${dropped} characters truncated] \u2026
|
|
140
210
|
${tail}` : head + tail;
|
|
141
211
|
resolve2({ output: output + extra, exitCode, timedOut, aborted });
|
|
142
212
|
};
|
|
@@ -155,7 +225,7 @@ var bashTool = defineTool({
|
|
|
155
225
|
target: (input) => input.command,
|
|
156
226
|
async run(input, ctx) {
|
|
157
227
|
const r = await runCommand(input.command, { cwd: ctx.cwd, timeoutMs: input.timeout_ms ?? 12e4, signal: ctx.signal });
|
|
158
|
-
const status = r.timedOut ? `[
|
|
228
|
+
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
229
|
return { output: `${r.output.trimEnd()}
|
|
160
230
|
${status}`.trimStart(), isError: r.exitCode !== 0 };
|
|
161
231
|
}
|
|
@@ -214,7 +284,7 @@ function countOccurrences(haystack, needle) {
|
|
|
214
284
|
}
|
|
215
285
|
function applyEdit(content, input) {
|
|
216
286
|
if (input.old_string === input.new_string) {
|
|
217
|
-
return { output: "old_string
|
|
287
|
+
return { output: "old_string and new_string are identical; nothing to change.", isError: true };
|
|
218
288
|
}
|
|
219
289
|
const eol = content.includes("\r\n") ? "\r\n" : "\n";
|
|
220
290
|
const toEol = (s) => s.replace(/\r?\n/g, eol);
|
|
@@ -225,13 +295,13 @@ function applyEdit(content, input) {
|
|
|
225
295
|
if (count === 0) continue;
|
|
226
296
|
if (count > 1 && !input.replace_all) {
|
|
227
297
|
return {
|
|
228
|
-
output: `old_string
|
|
298
|
+
output: `old_string appears ${count} times in ${input.path}. Add more context to make it unique, or use replace_all.`,
|
|
229
299
|
isError: true
|
|
230
300
|
};
|
|
231
301
|
}
|
|
232
302
|
return input.replace_all ? content.split(oldText).join(newText) : content.replace(oldText, () => newText);
|
|
233
303
|
}
|
|
234
|
-
return { output: `old_string
|
|
304
|
+
return { output: `old_string not found in ${input.path}. Re-read the file.`, isError: true };
|
|
235
305
|
}
|
|
236
306
|
var editTool = defineTool({
|
|
237
307
|
name: "edit",
|
|
@@ -241,12 +311,12 @@ var editTool = defineTool({
|
|
|
241
311
|
target: (input) => input.path,
|
|
242
312
|
async validate(input, ctx) {
|
|
243
313
|
const abs = resolvePath(ctx.cwd, input.path);
|
|
244
|
-
if (!ctx.readFiles.has(abs)) return `
|
|
314
|
+
if (!ctx.readFiles.has(abs)) return `Read ${input.path} with read before editing it.`;
|
|
245
315
|
let content;
|
|
246
316
|
try {
|
|
247
317
|
content = await readFile(abs, "utf8");
|
|
248
318
|
} catch (error) {
|
|
249
|
-
return `
|
|
319
|
+
return `Cannot read ${input.path}: ${errorCode(error)}`;
|
|
250
320
|
}
|
|
251
321
|
const next = applyEdit(content, input);
|
|
252
322
|
return typeof next === "string" ? void 0 : next.output;
|
|
@@ -260,25 +330,30 @@ var editTool = defineTool({
|
|
|
260
330
|
async run(input, ctx) {
|
|
261
331
|
const abs = resolvePath(ctx.cwd, input.path);
|
|
262
332
|
if (!ctx.readFiles.has(abs)) {
|
|
263
|
-
return { output: `
|
|
333
|
+
return { output: `Read ${input.path} with read before editing it.`, isError: true };
|
|
264
334
|
}
|
|
265
335
|
let content;
|
|
266
336
|
try {
|
|
267
337
|
content = await readFile(abs, "utf8");
|
|
268
338
|
} catch (error) {
|
|
269
|
-
return { output: `
|
|
339
|
+
return { output: `Cannot read ${input.path}: ${errorCode(error)}`, isError: true };
|
|
270
340
|
}
|
|
271
341
|
const next = applyEdit(content, input);
|
|
272
342
|
if (typeof next !== "string") return next;
|
|
273
343
|
await ctx.checkpoint?.(abs);
|
|
274
344
|
await writeFile(abs, next);
|
|
275
|
-
return { output: `
|
|
345
|
+
return { output: `Edited ${input.path}.`, display: formatDiff(diffLines(content, next)) };
|
|
276
346
|
}
|
|
277
347
|
});
|
|
278
348
|
|
|
279
349
|
// src/tools/fetch.ts
|
|
280
350
|
import { z as z3 } from "zod";
|
|
281
|
-
var MAX_CHARS =
|
|
351
|
+
var MAX_CHARS = 12e3;
|
|
352
|
+
var FOCUS_CHUNKS = 3;
|
|
353
|
+
var FOCUS_SIZE = 800;
|
|
354
|
+
var CACHE_MAX = 16;
|
|
355
|
+
var CACHE_TTL_MS = 15 * 6e4;
|
|
356
|
+
var CACHE_MAX_CHARS = 2e6;
|
|
282
357
|
var MAX_BYTES = 5 * 1024 * 1024;
|
|
283
358
|
async function readCapped(res) {
|
|
284
359
|
const reader = res.body?.getReader();
|
|
@@ -300,45 +375,197 @@ async function readCapped(res) {
|
|
|
300
375
|
function htmlToText(html) {
|
|
301
376
|
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
377
|
}
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
378
|
+
function describeFetchError(error) {
|
|
379
|
+
const cause = error.cause;
|
|
380
|
+
const code = cause?.code ?? "";
|
|
381
|
+
const detail = cause?.message ?? error.message;
|
|
382
|
+
if (error.name === "TimeoutError") return "no response within 30 seconds";
|
|
383
|
+
if (code === "ENOTFOUND" || code === "EAI_AGAIN") return `${detail} \u2014 DNS could not find the host (typo, offline, or blocked by DNS/ISP)`;
|
|
384
|
+
if (/CERT|SELF_SIGNED|UNABLE_TO/.test(code)) {
|
|
385
|
+
return `${detail} \u2014 TLS certificate rejected; often caused by an antivirus or proxy that inspects HTTPS. Try running with NODE_OPTIONS=--use-system-ca`;
|
|
386
|
+
}
|
|
387
|
+
if (/ECONNRESET|ECONNREFUSED|ETIMEDOUT|EHOSTUNREACH|ENETUNREACH|UND_ERR_CONNECT_TIMEOUT|UND_ERR_SOCKET/.test(code)) {
|
|
388
|
+
return `${detail} \u2014 connection failed (firewall, proxy, or the site is blocked by the network)`;
|
|
389
|
+
}
|
|
390
|
+
return code ? `${detail} (${code})` : detail;
|
|
391
|
+
}
|
|
392
|
+
var tokensOf = (text) => Math.ceil(text.length / 4);
|
|
393
|
+
function isPrivateHost(host) {
|
|
394
|
+
const h = host.toLowerCase().replace(/^\[|\]$/g, "");
|
|
395
|
+
if (h === "localhost" || /\.(local|internal|lan|localdomain|home\.arpa)$/.test(h)) return true;
|
|
396
|
+
if (h.includes(":")) return /^(::1?$|f[cd]|fe[89ab]|::ffff:)/.test(h);
|
|
397
|
+
const v4 = h.match(/^(\d+)\.(\d+)\.\d+\.\d+$/);
|
|
398
|
+
if (v4) {
|
|
399
|
+
const [a, b] = [Number(v4[1]), Number(v4[2])];
|
|
400
|
+
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;
|
|
401
|
+
}
|
|
402
|
+
return !h.includes(".");
|
|
403
|
+
}
|
|
404
|
+
var words = (s) => s.toLowerCase().match(/[\p{L}\p{N}]{2,}/gu) ?? [];
|
|
405
|
+
function pickRelevant(text, query, count = FOCUS_CHUNKS, size = FOCUS_SIZE) {
|
|
406
|
+
const chunks = [];
|
|
407
|
+
let cur = "";
|
|
408
|
+
for (const line of text.split("\n")) {
|
|
409
|
+
for (let i = 0; i < Math.max(line.length, 1); i += size) {
|
|
410
|
+
cur += `${line.slice(i, i + size)}
|
|
411
|
+
`;
|
|
412
|
+
if (cur.length >= size) {
|
|
413
|
+
chunks.push(cur.trimEnd());
|
|
414
|
+
cur = "";
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
if (cur.trim()) chunks.push(cur.trimEnd());
|
|
419
|
+
const q = [...new Set(words(query))];
|
|
420
|
+
const docs = chunks.map(words);
|
|
421
|
+
const avg = docs.reduce((n, d) => n + d.length, 0) / (docs.length || 1) || 1;
|
|
422
|
+
const idf = q.map((t2) => {
|
|
423
|
+
const df = docs.filter((d) => d.includes(t2)).length;
|
|
424
|
+
return Math.log(1 + (docs.length - df + 0.5) / (df + 0.5));
|
|
425
|
+
});
|
|
426
|
+
const scored = docs.map((d, i) => ({
|
|
427
|
+
i,
|
|
428
|
+
score: q.reduce((sum, t2, k) => {
|
|
429
|
+
const tf = d.filter((w) => w === t2).length;
|
|
430
|
+
return sum + idf[k] * tf * 2.2 / (tf + 1.2 * (0.25 + 0.75 * d.length / avg));
|
|
431
|
+
}, 0)
|
|
432
|
+
})).filter((c) => c.score > 0).sort((a, b) => b.score - a.score).slice(0, count).sort((a, b) => a.i - b.i);
|
|
433
|
+
return scored.length ? scored.map((c) => chunks[c.i]).join("\n\u2026\n") : null;
|
|
434
|
+
}
|
|
435
|
+
var cache = /* @__PURE__ */ new Map();
|
|
436
|
+
function remember(key, value, size) {
|
|
437
|
+
if (size > CACHE_MAX_CHARS) return;
|
|
438
|
+
cache.delete(key);
|
|
439
|
+
cache.set(key, value);
|
|
440
|
+
if (cache.size > CACHE_MAX) cache.delete(cache.keys().next().value);
|
|
441
|
+
}
|
|
442
|
+
function recall(key) {
|
|
443
|
+
const hit = cache.get(key);
|
|
444
|
+
if (typeof hit === "object" && Date.now() - hit.at > CACHE_TTL_MS) {
|
|
445
|
+
cache.delete(key);
|
|
446
|
+
return void 0;
|
|
447
|
+
}
|
|
448
|
+
return hit;
|
|
449
|
+
}
|
|
450
|
+
var ACCEPT = "text/markdown, text/html;q=0.9, text/plain;q=0.8, */*;q=0.5";
|
|
451
|
+
var withCost = (text, what) => `${what} \xB7 ${t("~{n} tokens", { n: tokensOf(text) })}`;
|
|
452
|
+
function createFetchTool(opts = {}) {
|
|
453
|
+
const keenableURL = opts.keenableURL === void 0 ? "https://api.keenable.ai/v1/fetch/public" : opts.keenableURL;
|
|
454
|
+
const isPrivate = opts.isPrivate ?? isPrivateHost;
|
|
455
|
+
return defineTool({
|
|
456
|
+
name: "fetch",
|
|
457
|
+
description: `Fetch a URL (http/https) as text/markdown. Costs the user tokens, so ask for as little as you need:
|
|
458
|
+
- 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.
|
|
459
|
+
- Without \`prompt\` you get the first ${MAX_CHARS} characters; the reply tells you the \`offset\` to continue from.
|
|
460
|
+
- Fetching a page that is already in this conversation is wasteful; reuse what you have.`,
|
|
461
|
+
schema: z3.object({
|
|
462
|
+
url: z3.string().describe("Absolute http(s) URL"),
|
|
463
|
+
prompt: z3.string().max(2e3).optional().describe("What to look for in the page; returns only the relevant excerpt"),
|
|
464
|
+
offset: z3.number().int().min(0).optional().describe("Start reading at this character (to continue a truncated page)")
|
|
465
|
+
}),
|
|
466
|
+
kind: "fetch",
|
|
467
|
+
target: (input) => input.url,
|
|
468
|
+
async run(input, ctx) {
|
|
469
|
+
if (!/^https?:\/\//i.test(input.url)) return { output: "URL must start with http:// or https://", isError: true };
|
|
470
|
+
const target = new URL(input.url);
|
|
471
|
+
const prompt = input.prompt?.trim();
|
|
472
|
+
const askKeenable = !!prompt && !input.offset && !!keenableURL && !target.username && !target.password && !isPrivate(target.hostname);
|
|
473
|
+
if (askKeenable) {
|
|
474
|
+
const key = `x
|
|
475
|
+
${input.url}
|
|
476
|
+
${prompt}`;
|
|
477
|
+
const hit = recall(key);
|
|
478
|
+
if (hit) {
|
|
479
|
+
ctx.refundFetch?.();
|
|
480
|
+
return { output: hit, display: withCost(hit, t("focused excerpt \xB7 cache")) };
|
|
481
|
+
}
|
|
482
|
+
const out = await extractWithKeenable(keenableURL, input.url, prompt, ctx.signal);
|
|
483
|
+
if (out) {
|
|
484
|
+
const output2 = `[Excerpt from ${input.url} for "${prompt}" (extracted by Keenable). If it says the answer is not there, try fetch without a prompt.]
|
|
485
|
+
${out}`;
|
|
486
|
+
remember(key, output2, output2.length);
|
|
487
|
+
return { output: output2, display: withCost(output2, t("focused excerpt (Keenable)")) };
|
|
325
488
|
}
|
|
489
|
+
}
|
|
490
|
+
let page = recall(`p
|
|
491
|
+
${input.url}`);
|
|
492
|
+
const cached = !!page;
|
|
493
|
+
if (cached) ctx.refundFetch?.();
|
|
494
|
+
if (!page) {
|
|
495
|
+
const loaded = await loadPage(input.url, ctx.signal);
|
|
496
|
+
if ("error" in loaded) return { output: loaded.error, isError: true };
|
|
497
|
+
page = loaded;
|
|
498
|
+
remember(`p
|
|
499
|
+
${input.url}`, page, page.text.length);
|
|
500
|
+
}
|
|
501
|
+
const { text, truncated } = page;
|
|
502
|
+
const via = cached ? " \xB7 cache" : "";
|
|
503
|
+
if (prompt && !input.offset) {
|
|
504
|
+
const picked = pickRelevant(text, prompt);
|
|
505
|
+
if (picked) {
|
|
506
|
+
const output2 = `[Most relevant excerpt from ${input.url} for "${prompt}" (${text.length} characters total). Full page: fetch without a prompt.]
|
|
507
|
+
${picked}`;
|
|
508
|
+
return { output: output2, display: withCost(output2, `${t("focused excerpt (local)")}${via}`) };
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
const offset = input.offset ?? 0;
|
|
512
|
+
if (offset >= text.length && text.length > 0) {
|
|
513
|
+
return { output: `offset ${offset} is past the end of the page (${text.length} characters).`, isError: true };
|
|
514
|
+
}
|
|
515
|
+
const end = Math.min(offset + MAX_CHARS, text.length);
|
|
516
|
+
let output = text.slice(offset, end);
|
|
517
|
+
if (end < text.length || truncated) {
|
|
518
|
+
const total = truncated ? `${text.length}+` : text.length;
|
|
519
|
+
output += `
|
|
520
|
+
\u2026 [truncated: characters ${offset}-${end} of ${total}. Continue with offset=${end}, or use prompt for a focused excerpt.]`;
|
|
521
|
+
}
|
|
522
|
+
const size = truncated ? t("over 5 MB, truncated") : t("{n} characters", { n: text.length });
|
|
523
|
+
return { output, display: withCost(output, `${size}${via}`) };
|
|
524
|
+
}
|
|
525
|
+
});
|
|
526
|
+
}
|
|
527
|
+
async function extractWithKeenable(endpoint, url, prompt, signal) {
|
|
528
|
+
try {
|
|
529
|
+
const u = new URL(endpoint);
|
|
530
|
+
u.searchParams.set("url", url);
|
|
531
|
+
u.searchParams.set("prompt", prompt);
|
|
532
|
+
u.searchParams.set("live", "true");
|
|
533
|
+
u.searchParams.set("max_chars", String(MAX_CHARS));
|
|
534
|
+
const res = await fetch(u, { headers: { "X-Keenable-Title": "BotConnector" }, signal: AbortSignal.any([signal, AbortSignal.timeout(25e3)]) });
|
|
535
|
+
if (!res.ok) return void 0;
|
|
536
|
+
const content = (await res.json()).content;
|
|
537
|
+
return typeof content === "string" && content.trim() ? content.slice(0, MAX_CHARS) : void 0;
|
|
538
|
+
} catch {
|
|
539
|
+
return void 0;
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
async function loadPage(inputUrl, signal) {
|
|
543
|
+
let url = new URL(inputUrl);
|
|
544
|
+
let res;
|
|
545
|
+
try {
|
|
546
|
+
for (let hop = 0; hop < 5; hop++) {
|
|
547
|
+
res = await fetch(url, { headers: { accept: ACCEPT }, signal: AbortSignal.any([signal, AbortSignal.timeout(3e4)]), redirect: "manual" });
|
|
548
|
+
const location = res.status >= 300 && res.status < 400 ? res.headers.get("location") : null;
|
|
549
|
+
if (!location) break;
|
|
550
|
+
const next = new URL(location, url);
|
|
551
|
+
if (next.host !== url.host) {
|
|
326
552
|
await res.body?.cancel();
|
|
327
|
-
|
|
553
|
+
return { error: `Redirect to another host: ${next.href}. Call fetch again with that URL if needed (permission will be requested).` };
|
|
328
554
|
}
|
|
329
|
-
|
|
330
|
-
|
|
555
|
+
await res.body?.cancel();
|
|
556
|
+
url = next;
|
|
331
557
|
}
|
|
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 };
|
|
558
|
+
} catch (error) {
|
|
559
|
+
return { error: `Failed to fetch ${inputUrl}: ${describeFetchError(error)}` };
|
|
340
560
|
}
|
|
341
|
-
}
|
|
561
|
+
if (!res) return { error: `Failed to fetch ${inputUrl}` };
|
|
562
|
+
const { text: body, truncated } = await readCapped(res);
|
|
563
|
+
const type = res.headers.get("content-type") ?? "";
|
|
564
|
+
const text = type.includes("html") ? htmlToText(body) : body;
|
|
565
|
+
if (!res.ok) return { error: `HTTP ${res.status} from ${inputUrl}: ${text.slice(0, 500)}` };
|
|
566
|
+
return { text, truncated, at: Date.now() };
|
|
567
|
+
}
|
|
568
|
+
var fetchTool = createFetchTool();
|
|
342
569
|
|
|
343
570
|
// src/tools/glob.ts
|
|
344
571
|
import fg from "fast-glob";
|
|
@@ -355,10 +582,10 @@ var globTool = defineTool({
|
|
|
355
582
|
async run(input, ctx) {
|
|
356
583
|
const root = resolvePath(ctx.cwd, input.path ?? ".");
|
|
357
584
|
const files = (await fg(input.pattern, { cwd: root, ignore: ignorePatterns(root), onlyFiles: true, dot: true })).sort();
|
|
358
|
-
if (!files.length) return { output: "
|
|
585
|
+
if (!files.length) return { output: "No matching files." };
|
|
359
586
|
const shown = files.slice(0, 500);
|
|
360
587
|
const more = files.length > shown.length ? `
|
|
361
|
-
\u2026 ${files.length - shown.length}
|
|
588
|
+
\u2026 ${files.length - shown.length} more files` : "";
|
|
362
589
|
return { output: shown.join("\n") + more, display: `${files.length} file` };
|
|
363
590
|
}
|
|
364
591
|
});
|
|
@@ -420,11 +647,11 @@ var grepTool = defineTool({
|
|
|
420
647
|
try {
|
|
421
648
|
new RegExp(input.pattern);
|
|
422
649
|
} catch (error) {
|
|
423
|
-
return { output: `
|
|
650
|
+
return { output: `Invalid regex: ${error.message}`, isError: true };
|
|
424
651
|
}
|
|
425
652
|
const matches = await grepRg(input.pattern, opts) ?? await grepJs(input.pattern, opts);
|
|
426
|
-
if (!matches.length) return { output: "
|
|
427
|
-
return { output: matches.join("\n"), display:
|
|
653
|
+
if (!matches.length) return { output: "No matches." };
|
|
654
|
+
return { output: matches.join("\n"), display: t("{n} results", { n: matches.length }) };
|
|
428
655
|
}
|
|
429
656
|
});
|
|
430
657
|
|
|
@@ -448,25 +675,25 @@ var readTool = defineTool({
|
|
|
448
675
|
try {
|
|
449
676
|
buffer = await readFile3(abs);
|
|
450
677
|
} catch (error) {
|
|
451
|
-
return { output: `
|
|
678
|
+
return { output: `Cannot read ${input.path}: ${errorCode(error)}`, isError: true };
|
|
452
679
|
}
|
|
453
680
|
if (buffer.subarray(0, 8e3).includes(0)) {
|
|
454
|
-
return { output: `${input.path}
|
|
681
|
+
return { output: `${input.path} is a binary file; not read.`, isError: true };
|
|
455
682
|
}
|
|
456
683
|
const lines = buffer.toString("utf8").split(/\r?\n/);
|
|
457
684
|
const start = (input.offset ?? 1) - 1;
|
|
458
685
|
const limit = input.limit ?? 2e3;
|
|
459
686
|
const slice = lines.slice(start, start + limit);
|
|
460
687
|
const body = slice.map((line, i) => {
|
|
461
|
-
const text = line.length > MAX_LINE ? `${line.slice(0, MAX_LINE)}\u2026 [
|
|
688
|
+
const text = line.length > MAX_LINE ? `${line.slice(0, MAX_LINE)}\u2026 [line truncated]` : line;
|
|
462
689
|
return `${String(start + i + 1).padStart(6)} ${text}`;
|
|
463
690
|
}).join("\n");
|
|
464
691
|
const remaining = lines.length - start - slice.length;
|
|
465
692
|
ctx.readFiles.add(abs);
|
|
466
693
|
return {
|
|
467
694
|
output: remaining > 0 ? `${body}
|
|
468
|
-
\u2026 ${remaining}
|
|
469
|
-
display:
|
|
695
|
+
\u2026 ${remaining} more lines (use offset)` : body,
|
|
696
|
+
display: t("{n} lines", { n: slice.length })
|
|
470
697
|
};
|
|
471
698
|
}
|
|
472
699
|
});
|
|
@@ -487,7 +714,7 @@ var writeTool = defineTool({
|
|
|
487
714
|
target: (input) => input.path,
|
|
488
715
|
async validate(input, ctx) {
|
|
489
716
|
const abs = resolvePath(ctx.cwd, input.path);
|
|
490
|
-
return existsSync(abs) && !ctx.readFiles.has(abs) ? `${input.path}
|
|
717
|
+
return existsSync(abs) && !ctx.readFiles.has(abs) ? `${input.path} already exists. Read it with read before overwriting it.` : void 0;
|
|
491
718
|
},
|
|
492
719
|
async preview(input, ctx) {
|
|
493
720
|
const abs = resolvePath(ctx.cwd, input.path);
|
|
@@ -497,18 +724,18 @@ var writeTool = defineTool({
|
|
|
497
724
|
async run(input, ctx) {
|
|
498
725
|
const abs = resolvePath(ctx.cwd, input.path);
|
|
499
726
|
if (existsSync(abs) && !ctx.readFiles.has(abs)) {
|
|
500
|
-
return { output: `${input.path}
|
|
727
|
+
return { output: `${input.path} already exists. Read it with read before overwriting it.`, isError: true };
|
|
501
728
|
}
|
|
502
729
|
try {
|
|
503
730
|
await ctx.checkpoint?.(abs);
|
|
504
731
|
await mkdir(dirname(abs), { recursive: true });
|
|
505
732
|
await writeFile2(abs, input.content);
|
|
506
733
|
} catch (error) {
|
|
507
|
-
return { output: `
|
|
734
|
+
return { output: `Failed to write ${input.path}: ${errorCode(error)}`, isError: true };
|
|
508
735
|
}
|
|
509
736
|
ctx.readFiles.add(abs);
|
|
510
737
|
const lines = input.content.split("\n").length;
|
|
511
|
-
return { output: `
|
|
738
|
+
return { output: `Wrote ${input.path} (${lines} lines).`, display: t("{lines} lines written", { lines }) };
|
|
512
739
|
}
|
|
513
740
|
});
|
|
514
741
|
|
|
@@ -521,6 +748,80 @@ function toolDefinitions(tools) {
|
|
|
521
748
|
});
|
|
522
749
|
}
|
|
523
750
|
|
|
751
|
+
// src/webBudget.ts
|
|
752
|
+
var KEEP_TOKENS = 4e4;
|
|
753
|
+
var PRUNE_MIN_TOKENS = 2e4;
|
|
754
|
+
var STUB = "[old tool result removed:";
|
|
755
|
+
var tokens = (text) => Math.ceil(text.length / 4);
|
|
756
|
+
var isStub = (content) => content.startsWith(STUB);
|
|
757
|
+
function pruneOldFetches(messages) {
|
|
758
|
+
const urls = /* @__PURE__ */ new Map();
|
|
759
|
+
for (const m of messages) {
|
|
760
|
+
if (m.role !== "assistant") continue;
|
|
761
|
+
for (const c of m.tool_calls ?? []) {
|
|
762
|
+
if (c.function.name !== "fetch") continue;
|
|
763
|
+
try {
|
|
764
|
+
urls.set(c.id, String(JSON.parse(c.function.arguments).url ?? ""));
|
|
765
|
+
} catch {
|
|
766
|
+
urls.set(c.id, "");
|
|
767
|
+
}
|
|
768
|
+
}
|
|
769
|
+
}
|
|
770
|
+
let kept = 0;
|
|
771
|
+
let oldTokens = 0;
|
|
772
|
+
const old = [];
|
|
773
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
774
|
+
const m = messages[i];
|
|
775
|
+
if (m.role !== "tool" || !urls.has(m.tool_call_id) || isStub(m.content)) continue;
|
|
776
|
+
kept += tokens(m.content);
|
|
777
|
+
if (kept > KEEP_TOKENS) {
|
|
778
|
+
old.push(i);
|
|
779
|
+
oldTokens += tokens(m.content);
|
|
780
|
+
}
|
|
781
|
+
}
|
|
782
|
+
if (oldTokens <= PRUNE_MIN_TOKENS) return false;
|
|
783
|
+
for (const i of old) {
|
|
784
|
+
const m = messages[i];
|
|
785
|
+
messages[i] = { ...m, content: `${STUB} fetch ${urls.get(m.tool_call_id)}. Fetch it again (use prompt for a focused excerpt) if still needed.]` };
|
|
786
|
+
}
|
|
787
|
+
return true;
|
|
788
|
+
}
|
|
789
|
+
var WebBudget = class {
|
|
790
|
+
fetches = 0;
|
|
791
|
+
chars = 0;
|
|
792
|
+
perTurn;
|
|
793
|
+
sessionChars;
|
|
794
|
+
constructor(limits = {}) {
|
|
795
|
+
this.perTurn = limits.perTurn ?? 8;
|
|
796
|
+
this.sessionChars = limits.sessionChars ?? 3e5;
|
|
797
|
+
}
|
|
798
|
+
startTurn() {
|
|
799
|
+
this.fetches = 0;
|
|
800
|
+
}
|
|
801
|
+
/** Gives back a reservation, e.g. when the fetch was served from cache and cost nothing. */
|
|
802
|
+
refund() {
|
|
803
|
+
if (this.fetches > 0) this.fetches--;
|
|
804
|
+
}
|
|
805
|
+
reset() {
|
|
806
|
+
this.fetches = 0;
|
|
807
|
+
this.chars = 0;
|
|
808
|
+
}
|
|
809
|
+
/** Reserves one fetch; returns the refusal to show the model when over budget. */
|
|
810
|
+
take() {
|
|
811
|
+
if (this.fetches >= this.perTurn) {
|
|
812
|
+
return `Limit of ${this.perTurn} fetches per question reached (each fetch uses the user's tokens). Answer from the results you already have; if that is not enough, tell the user what is still missing.`;
|
|
813
|
+
}
|
|
814
|
+
if (this.chars >= this.sessionChars) {
|
|
815
|
+
return `Total web page limit for this session reached (~${Math.round(this.sessionChars / 4e3)}k tokens). Answer from the results you already have and ask the user whether to search more.`;
|
|
816
|
+
}
|
|
817
|
+
this.fetches++;
|
|
818
|
+
return void 0;
|
|
819
|
+
}
|
|
820
|
+
record(chars) {
|
|
821
|
+
this.chars += chars;
|
|
822
|
+
}
|
|
823
|
+
};
|
|
824
|
+
|
|
524
825
|
// src/agent.ts
|
|
525
826
|
var estimateTokens = (text) => Math.ceil(text.length / 4);
|
|
526
827
|
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.";
|
|
@@ -537,6 +838,7 @@ var Agent = class {
|
|
|
537
838
|
this.maxSteps = opts.maxSteps ?? 50;
|
|
538
839
|
this.contextWindow = opts.contextWindow ?? 128e3;
|
|
539
840
|
this.systemPrompt = opts.systemPrompt;
|
|
841
|
+
this.reasoning = opts.reasoning ?? "auto";
|
|
540
842
|
}
|
|
541
843
|
opts;
|
|
542
844
|
provider;
|
|
@@ -546,6 +848,7 @@ var Agent = class {
|
|
|
546
848
|
lastInputTokens = 0;
|
|
547
849
|
onEvent = () => {
|
|
548
850
|
};
|
|
851
|
+
reasoning;
|
|
549
852
|
askPermission = async () => "no";
|
|
550
853
|
tools;
|
|
551
854
|
definitions;
|
|
@@ -553,6 +856,7 @@ var Agent = class {
|
|
|
553
856
|
turnTools;
|
|
554
857
|
turnDefinitions;
|
|
555
858
|
readFiles = /* @__PURE__ */ new Set();
|
|
859
|
+
webBudget = new WebBudget();
|
|
556
860
|
maxSteps;
|
|
557
861
|
// ponytail: one window for every model; read it from /v1/models metadata when providers expose it.
|
|
558
862
|
contextWindow;
|
|
@@ -565,7 +869,7 @@ var Agent = class {
|
|
|
565
869
|
this.definitions = toolDefinitions(tools);
|
|
566
870
|
}
|
|
567
871
|
isParallelSafe(call) {
|
|
568
|
-
const tool = this.turnTools.find((
|
|
872
|
+
const tool = this.turnTools.find((t2) => t2.name === call.name);
|
|
569
873
|
if (!tool?.parallelSafe) return false;
|
|
570
874
|
try {
|
|
571
875
|
const parsed = tool.schema.safeParse(JSON.parse(call.arguments || "{}"));
|
|
@@ -582,11 +886,13 @@ var Agent = class {
|
|
|
582
886
|
load(messages) {
|
|
583
887
|
this.messages = messages;
|
|
584
888
|
this.readFiles.clear();
|
|
889
|
+
this.webBudget.reset();
|
|
585
890
|
this.lastInputTokens = 0;
|
|
586
891
|
}
|
|
587
892
|
clear() {
|
|
588
893
|
this.messages = [];
|
|
589
894
|
this.readFiles.clear();
|
|
895
|
+
this.webBudget.reset();
|
|
590
896
|
this.lastInputTokens = 0;
|
|
591
897
|
this.opts.onReset?.();
|
|
592
898
|
}
|
|
@@ -595,32 +901,44 @@ var Agent = class {
|
|
|
595
901
|
messages: [{ role: "system", content: this.systemPrompt }, ...this.messages, { role: "user", content: COMPACT_PROMPT }],
|
|
596
902
|
// Some gateways reject tool_calls in history when no tools are declared.
|
|
597
903
|
tools: this.definitions,
|
|
598
|
-
signal
|
|
904
|
+
signal,
|
|
905
|
+
reasoning: this.reasoning
|
|
599
906
|
});
|
|
600
907
|
this.clear();
|
|
601
|
-
this.push({ role: "user", content: `
|
|
908
|
+
this.push({ role: "user", content: `Summary of the previous conversation:
|
|
602
909
|
${completion.text}` });
|
|
603
|
-
this.push({ role: "assistant", content: "
|
|
910
|
+
this.push({ role: "assistant", content: "OK, I will continue from this summary." });
|
|
604
911
|
this.onEvent({ type: "compacted" });
|
|
605
912
|
}
|
|
606
913
|
async run(text, signal) {
|
|
607
914
|
this.turnTools = this.tools;
|
|
608
915
|
this.turnDefinitions = this.definitions;
|
|
609
916
|
this.opts.onTurnStart?.();
|
|
917
|
+
this.webBudget.startTurn();
|
|
610
918
|
try {
|
|
611
919
|
if (this.lastInputTokens > this.contextWindow * 0.8) await this.compact(signal);
|
|
612
920
|
this.push({ role: "user", content: text });
|
|
613
921
|
for (let step = 0; step < this.maxSteps; step++) {
|
|
614
922
|
if (step > 0 && this.lastInputTokens > this.contextWindow * 0.8) {
|
|
615
923
|
await this.compact(signal);
|
|
616
|
-
this.push({ role: "user", content: `
|
|
924
|
+
this.push({ role: "user", content: `Continue this task using the summary above: ${text}` });
|
|
617
925
|
}
|
|
618
|
-
|
|
926
|
+
pruneOldFetches(this.messages);
|
|
927
|
+
let completion = await this.provider.chat({
|
|
619
928
|
messages: [{ role: "system", content: this.systemPrompt }, ...this.messages],
|
|
620
929
|
tools: this.turnDefinitions,
|
|
621
930
|
signal,
|
|
622
|
-
onText: (delta) => this.onEvent({ type: "text", delta })
|
|
931
|
+
onText: (delta) => this.onEvent({ type: "text", delta }),
|
|
932
|
+
onThinking: (delta) => this.onEvent({ type: "thinking", delta }),
|
|
933
|
+
reasoning: this.reasoning
|
|
623
934
|
});
|
|
935
|
+
if (!completion.toolCalls.length && completion.finishReason !== "repetition") {
|
|
936
|
+
const recovered = recoverTextToolCalls(completion.text, this.turnDefinitions);
|
|
937
|
+
if (recovered) {
|
|
938
|
+
completion = { ...completion, ...recovered };
|
|
939
|
+
this.onEvent({ type: "textReplace", text: recovered.text });
|
|
940
|
+
}
|
|
941
|
+
}
|
|
624
942
|
const usage = completion.usage ?? {
|
|
625
943
|
inputTokens: estimateTokens(this.systemPrompt) + estimateTokens(JSON.stringify(this.messages)),
|
|
626
944
|
outputTokens: estimateTokens(completion.text) + (completion.toolCalls.length ? estimateTokens(JSON.stringify(completion.toolCalls)) : 0)
|
|
@@ -644,7 +962,7 @@ ${completion.text}` });
|
|
|
644
962
|
this.onEvent({ type: "textReplace", text: completion.text });
|
|
645
963
|
this.onEvent({
|
|
646
964
|
type: "error",
|
|
647
|
-
message: "
|
|
965
|
+
message: t("The model got stuck repeating text, so the answer was stopped. Try again or switch models with /model.")
|
|
648
966
|
});
|
|
649
967
|
return;
|
|
650
968
|
}
|
|
@@ -656,7 +974,7 @@ ${completion.text}` });
|
|
|
656
974
|
let i = 0;
|
|
657
975
|
while (i < calls.length) {
|
|
658
976
|
if (signal.aborted) {
|
|
659
|
-
for (const pending of calls.slice(i)) this.push({ role: "tool", tool_call_id: pending.id, content: "
|
|
977
|
+
for (const pending of calls.slice(i)) this.push({ role: "tool", tool_call_id: pending.id, content: "Cancelled by the user." });
|
|
660
978
|
this.onEvent({ type: "aborted" });
|
|
661
979
|
return;
|
|
662
980
|
}
|
|
@@ -685,7 +1003,7 @@ ${completion.text}` });
|
|
|
685
1003
|
this.onEvent({
|
|
686
1004
|
type: "error",
|
|
687
1005
|
message: noTools ? `${message}
|
|
688
|
-
|
|
1006
|
+
${t("This model may not support tool calling. Try another model with /model.")}` : message
|
|
689
1007
|
});
|
|
690
1008
|
}
|
|
691
1009
|
}
|
|
@@ -695,20 +1013,22 @@ Model ini sepertinya tidak mendukung tool calling. Coba model lain dengan /model
|
|
|
695
1013
|
this.onEvent({ type: "toolEnd", id: call.id, tool: call.name, output, isError: true });
|
|
696
1014
|
return output;
|
|
697
1015
|
};
|
|
698
|
-
const tool = this.turnTools.find((
|
|
699
|
-
if (!tool) return fail(`
|
|
1016
|
+
const tool = this.turnTools.find((t2) => t2.name === call.name);
|
|
1017
|
+
if (!tool) return fail(`Tool "${call.name}" does not exist. Available tools: ${this.turnTools.map((t2) => t2.name).join(", ")}`);
|
|
700
1018
|
let args;
|
|
701
1019
|
try {
|
|
702
1020
|
args = JSON.parse(call.arguments || "{}");
|
|
703
1021
|
} catch {
|
|
704
|
-
return fail(`
|
|
1022
|
+
return fail(`Arguments are not valid JSON: ${call.arguments.slice(0, 200)}`);
|
|
705
1023
|
}
|
|
706
1024
|
const parsed = tool.schema.safeParse(args);
|
|
707
1025
|
if (!parsed.success) {
|
|
708
|
-
return fail(`
|
|
1026
|
+
return fail(`Invalid arguments: ${parsed.error.issues.map((i) => `${i.path.join(".") || "(root)"}: ${i.message}`).join("; ")}`);
|
|
709
1027
|
}
|
|
710
1028
|
const input = parsed.data;
|
|
711
1029
|
const target = tool.target(input);
|
|
1030
|
+
const overBudget = tool.name === "fetch" ? this.webBudget.take() : void 0;
|
|
1031
|
+
if (overBudget) return fail(overBudget, target);
|
|
712
1032
|
const ctx = {
|
|
713
1033
|
cwd: this.opts.cwd,
|
|
714
1034
|
signal,
|
|
@@ -717,6 +1037,7 @@ Model ini sepertinya tidak mendukung tool calling. Coba model lain dengan /model
|
|
|
717
1037
|
emit: (event) => this.onEvent(event),
|
|
718
1038
|
ask: (req) => this.askPermission(req),
|
|
719
1039
|
checkpoint: this.opts.checkpoint,
|
|
1040
|
+
refundFetch: () => this.webBudget.refund(),
|
|
720
1041
|
addUsage: (u) => {
|
|
721
1042
|
this.totalUsage.inputTokens += u.inputTokens;
|
|
722
1043
|
this.totalUsage.outputTokens += u.outputTokens;
|
|
@@ -744,7 +1065,7 @@ Model ini sepertinya tidak mendukung tool calling. Coba model lain dengan /model
|
|
|
744
1065
|
decision = answer === "no" ? "deny" : "allow";
|
|
745
1066
|
}
|
|
746
1067
|
if (decision === "deny") {
|
|
747
|
-
const output = this.permissions.mode === "plan" ? "
|
|
1068
|
+
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.";
|
|
748
1069
|
this.onEvent({ type: "toolEnd", id: call.id, tool: tool.name, output, isError: true });
|
|
749
1070
|
return output;
|
|
750
1071
|
}
|
|
@@ -754,6 +1075,7 @@ Model ini sepertinya tidak mendukung tool calling. Coba model lain dengan /model
|
|
|
754
1075
|
} catch (error) {
|
|
755
1076
|
result = { output: `Error: ${error.message}`, isError: true };
|
|
756
1077
|
}
|
|
1078
|
+
if (tool.name === "fetch" && !result.isError) this.webBudget.record(result.output.length);
|
|
757
1079
|
this.onEvent({ type: "toolEnd", id: call.id, tool: tool.name, output: result.output, display: result.display, isError: !!result.isError });
|
|
758
1080
|
return result.output;
|
|
759
1081
|
}
|
|
@@ -783,7 +1105,7 @@ var CheckpointStore = class {
|
|
|
783
1105
|
while (this.turns() > MAX_TURNS) this.history.shift();
|
|
784
1106
|
}
|
|
785
1107
|
turns() {
|
|
786
|
-
return this.history.filter((
|
|
1108
|
+
return this.history.filter((t2) => t2.size).length;
|
|
787
1109
|
}
|
|
788
1110
|
async undo() {
|
|
789
1111
|
while (this.history.length && !this.history.at(-1)?.size) this.history.pop();
|
|
@@ -819,8 +1141,8 @@ import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js"
|
|
|
819
1141
|
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
|
|
820
1142
|
import { z as z9 } from "zod";
|
|
821
1143
|
function mcpToolName(server, tool, taken) {
|
|
822
|
-
const
|
|
823
|
-
const base = `mcp__${
|
|
1144
|
+
const clean2 = (s) => s.replace(/[^A-Za-z0-9_-]+/g, "_");
|
|
1145
|
+
const base = `mcp__${clean2(server)}__${clean2(tool)}`.slice(0, 64);
|
|
824
1146
|
let name = base;
|
|
825
1147
|
for (let n = 2; taken.has(name); n++) name = `${base.slice(0, 64 - String(n).length - 1)}_${n}`;
|
|
826
1148
|
taken.add(name);
|
|
@@ -829,7 +1151,7 @@ function mcpToolName(server, tool, taken) {
|
|
|
829
1151
|
function withTimeout(promise, ms) {
|
|
830
1152
|
let timer;
|
|
831
1153
|
const timeout = new Promise((_, reject) => {
|
|
832
|
-
timer = setTimeout(() => reject(new Error(
|
|
1154
|
+
timer = setTimeout(() => reject(new Error(t("timeout after {n} seconds", { n: Math.round(ms / 1e3) }))), ms);
|
|
833
1155
|
});
|
|
834
1156
|
return Promise.race([promise, timeout]).finally(() => clearTimeout(timer));
|
|
835
1157
|
}
|
|
@@ -866,7 +1188,7 @@ var McpManager = class {
|
|
|
866
1188
|
});
|
|
867
1189
|
return;
|
|
868
1190
|
}
|
|
869
|
-
entry.tools = tools.map((
|
|
1191
|
+
entry.tools = tools.map((t2) => this.wrap(spec.name, client, t2));
|
|
870
1192
|
entry.state = { ...entry.state, status: "ready", tools: entry.tools.length };
|
|
871
1193
|
} catch (error) {
|
|
872
1194
|
entry.state = { ...entry.state, status: "error", error: error.message };
|
|
@@ -890,9 +1212,9 @@ var McpManager = class {
|
|
|
890
1212
|
async run(input, ctx) {
|
|
891
1213
|
try {
|
|
892
1214
|
const result = await client.callTool({ name: tool.name, arguments: input }, void 0, { signal: ctx.signal });
|
|
893
|
-
const output = (result.content ?? []).map((c) => c.type === "text" ? c.text ?? "" : `[${c.type}]`).join("\n") || "(
|
|
1215
|
+
const output = (result.content ?? []).map((c) => c.type === "text" ? c.text ?? "" : `[${c.type}]`).join("\n") || "(empty)";
|
|
894
1216
|
return { output: output.length > 5e4 ? `${output.slice(0, 5e4)}
|
|
895
|
-
\u2026 [
|
|
1217
|
+
\u2026 [truncated]` : output, isError: !!result.isError };
|
|
896
1218
|
} catch (error) {
|
|
897
1219
|
return { output: `Server MCP ${server} error: ${error.message}`, isError: true };
|
|
898
1220
|
}
|
|
@@ -903,14 +1225,14 @@ var McpManager = class {
|
|
|
903
1225
|
const entry = this.servers.get(name);
|
|
904
1226
|
if (!entry) return;
|
|
905
1227
|
this.servers.delete(name);
|
|
906
|
-
for (const
|
|
907
|
-
if (entry.tools.length) this.opts.onToolsRemoved?.(entry.tools.map((
|
|
1228
|
+
for (const t2 of entry.tools) this.taken.delete(t2.name);
|
|
1229
|
+
if (entry.tools.length) this.opts.onToolsRemoved?.(entry.tools.map((t2) => t2.name));
|
|
908
1230
|
await entry.client?.close().catch(() => {
|
|
909
1231
|
});
|
|
910
1232
|
this.opts.onChange?.();
|
|
911
1233
|
}
|
|
912
1234
|
tools() {
|
|
913
|
-
return [...this.servers.values()].flatMap((s) => s.tools);
|
|
1235
|
+
return [...this.servers.values()].flatMap((s) => s.tools).sort((a, b) => a.name.localeCompare(b.name));
|
|
914
1236
|
}
|
|
915
1237
|
states() {
|
|
916
1238
|
return [...this.servers.values()].map((s) => s.state);
|
|
@@ -931,15 +1253,15 @@ function createExitPlanTool(opts) {
|
|
|
931
1253
|
description: "Plan mode only: present the finished plan to the user for approval. If approved you may start implementing it.",
|
|
932
1254
|
schema: z10.object({ plan: z10.string().describe("The plan, in markdown") }),
|
|
933
1255
|
kind: "read",
|
|
934
|
-
target: () => "
|
|
1256
|
+
target: () => t("plan"),
|
|
935
1257
|
async run(input) {
|
|
936
|
-
if (opts.permissions.mode !== "plan") return { output: "exit_plan
|
|
1258
|
+
if (opts.permissions.mode !== "plan") return { output: "exit_plan is only used in plan mode.", isError: true };
|
|
937
1259
|
const decision = await opts.interaction.approvePlan(input.plan);
|
|
938
1260
|
if (decision === "no") {
|
|
939
|
-
return { output: "
|
|
1261
|
+
return { output: "The user has not approved yet. Ask what needs to change, revise the plan, then call exit_plan again." };
|
|
940
1262
|
}
|
|
941
1263
|
opts.permissions.mode = decision;
|
|
942
|
-
return { output: `
|
|
1264
|
+
return { output: `Plan approved. The mode is now ${decision}. Start carrying out the plan now.` };
|
|
943
1265
|
}
|
|
944
1266
|
});
|
|
945
1267
|
}
|
|
@@ -956,7 +1278,7 @@ function createSkillTool(skills) {
|
|
|
956
1278
|
target: (input) => input.name,
|
|
957
1279
|
async run(input) {
|
|
958
1280
|
const skill = skills.find((s) => s.name === input.name);
|
|
959
|
-
if (!skill) return { output: `Skill "${input.name}"
|
|
1281
|
+
if (!skill) return { output: `Skill "${input.name}" does not exist. Available: ${skills.map((s) => s.name).join(", ") || "(none)"}`, isError: true };
|
|
960
1282
|
const { body } = parseFrontmatter(readFileSync(skill.file, "utf8"));
|
|
961
1283
|
return { output: `Base directory for this skill: ${skill.dir}
|
|
962
1284
|
|
|
@@ -965,8 +1287,69 @@ ${body}`, display: skill.description.slice(0, 60) };
|
|
|
965
1287
|
});
|
|
966
1288
|
}
|
|
967
1289
|
|
|
968
|
-
// src/tools/
|
|
1290
|
+
// src/tools/websearch.ts
|
|
969
1291
|
import { z as z12 } from "zod";
|
|
1292
|
+
var clean = (items, max) => (Array.isArray(items) ? items : []).filter((r) => typeof r?.url === "string" && /^https?:\/\//.test(r.url)).slice(0, max).map((r) => ({
|
|
1293
|
+
title: String(r.title || r.url).slice(0, 200),
|
|
1294
|
+
url: r.url,
|
|
1295
|
+
snippet: String(r.snippet || r.description || "").replace(/\s+/g, " ").trim().slice(0, 600)
|
|
1296
|
+
}));
|
|
1297
|
+
async function postJson(url, body, headers, signal) {
|
|
1298
|
+
const res = await fetch(url, {
|
|
1299
|
+
method: "POST",
|
|
1300
|
+
headers: { "content-type": "application/json", ...headers },
|
|
1301
|
+
body: JSON.stringify(body),
|
|
1302
|
+
signal: AbortSignal.any([signal, AbortSignal.timeout(3e4)])
|
|
1303
|
+
});
|
|
1304
|
+
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
|
1305
|
+
return res.json();
|
|
1306
|
+
}
|
|
1307
|
+
function createWebSearchTool(opts) {
|
|
1308
|
+
const keenableURL = opts.keenableURL ?? "https://api.keenable.ai/v1/search/public";
|
|
1309
|
+
return defineTool({
|
|
1310
|
+
name: "web_search",
|
|
1311
|
+
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.",
|
|
1312
|
+
schema: z12.object({
|
|
1313
|
+
query: z12.string().min(1).max(500).describe("Search query"),
|
|
1314
|
+
max_results: z12.number().int().min(1).max(10).optional().describe("Number of results (default 6)")
|
|
1315
|
+
}),
|
|
1316
|
+
kind: "read",
|
|
1317
|
+
target: (input) => input.query,
|
|
1318
|
+
parallelSafe: () => true,
|
|
1319
|
+
async run(input, ctx) {
|
|
1320
|
+
const max = input.max_results ?? 6;
|
|
1321
|
+
const errors = [];
|
|
1322
|
+
let results = [];
|
|
1323
|
+
const bc = opts.botconnector();
|
|
1324
|
+
if (bc) {
|
|
1325
|
+
try {
|
|
1326
|
+
const data = await postJson(`${bc.baseURL.replace(/\/+$/, "")}/web/search`, { query: input.query, max_results: max }, { authorization: `Bearer ${bc.apiKey}` }, ctx.signal);
|
|
1327
|
+
results = clean(data.results, max);
|
|
1328
|
+
} catch (error) {
|
|
1329
|
+
errors.push(`BotConnector: ${error.message}`);
|
|
1330
|
+
}
|
|
1331
|
+
}
|
|
1332
|
+
if (!results.length) {
|
|
1333
|
+
try {
|
|
1334
|
+
const data = await postJson(keenableURL, { query: input.query }, { "x-keenable-title": "BotConnector" }, ctx.signal);
|
|
1335
|
+
results = clean(data.results, max);
|
|
1336
|
+
} catch (error) {
|
|
1337
|
+
errors.push(`Keenable: ${error.message}`);
|
|
1338
|
+
}
|
|
1339
|
+
}
|
|
1340
|
+
if (!results.length) {
|
|
1341
|
+
return { output: errors.length ? `Web search failed (${errors.join("; ")}).` : "No results.", isError: errors.length > 0 };
|
|
1342
|
+
}
|
|
1343
|
+
const output = results.map((r, i) => `${i + 1}. ${r.title}
|
|
1344
|
+
${r.url}${r.snippet ? `
|
|
1345
|
+
${r.snippet}` : ""}`).join("\n");
|
|
1346
|
+
return { output, display: t("{n} results", { n: results.length }) };
|
|
1347
|
+
}
|
|
1348
|
+
});
|
|
1349
|
+
}
|
|
1350
|
+
|
|
1351
|
+
// src/tools/task.ts
|
|
1352
|
+
import { z as z13 } from "zod";
|
|
970
1353
|
function createTaskTool(opts) {
|
|
971
1354
|
const byName = new Map(opts.agents.map((a) => [a.name, a]));
|
|
972
1355
|
const list = opts.agents.map((a) => `- ${a.name}: ${a.description}`).join("\n");
|
|
@@ -974,10 +1357,10 @@ function createTaskTool(opts) {
|
|
|
974
1357
|
name: "task",
|
|
975
1358
|
description: `Delegate a self-contained sub-task to a subagent. It starts with an empty context and returns only its final report. Available agents:
|
|
976
1359
|
${list}`,
|
|
977
|
-
schema:
|
|
978
|
-
agent:
|
|
979
|
-
description:
|
|
980
|
-
prompt:
|
|
1360
|
+
schema: z13.object({
|
|
1361
|
+
agent: z13.string().describe("Agent name"),
|
|
1362
|
+
description: z13.string().describe("3-6 word summary shown to the user"),
|
|
1363
|
+
prompt: z13.string().describe("Full instructions; the subagent sees nothing else")
|
|
981
1364
|
}),
|
|
982
1365
|
kind: "read",
|
|
983
1366
|
target: (input) => `[${input.agent}] ${input.description}`,
|
|
@@ -986,16 +1369,16 @@ ${list}`,
|
|
|
986
1369
|
const allowed = byName.get(input.agent)?.tools;
|
|
987
1370
|
if (!allowed) return false;
|
|
988
1371
|
const base = opts.baseTools();
|
|
989
|
-
return allowed.every((name) => base.find((
|
|
1372
|
+
return allowed.every((name) => base.find((t2) => t2.name === name)?.kind === "read");
|
|
990
1373
|
},
|
|
991
1374
|
async run(input, ctx) {
|
|
992
1375
|
const def = byName.get(input.agent);
|
|
993
|
-
if (!def) return { output: `Agent "${input.agent}"
|
|
994
|
-
const base = opts.baseTools().filter((
|
|
995
|
-
const unknown = (def.tools ?? []).filter((n) => !base.some((
|
|
996
|
-
if (unknown.length) return { output: `Agent "${def.name}":
|
|
1376
|
+
if (!def) return { output: `Agent "${input.agent}" does not exist. Available: ${[...byName.keys()].join(", ")}`, isError: true };
|
|
1377
|
+
const base = opts.baseTools().filter((t2) => t2.name !== "task" && t2.name !== "exit_plan");
|
|
1378
|
+
const unknown = (def.tools ?? []).filter((n) => !base.some((t2) => t2.name === n));
|
|
1379
|
+
if (unknown.length) return { output: `Agent "${def.name}": unknown tools: ${unknown.join(", ")}`, isError: true };
|
|
997
1380
|
const allowed = def.tools;
|
|
998
|
-
const tools = allowed ? base.filter((
|
|
1381
|
+
const tools = allowed ? base.filter((t2) => allowed.includes(t2.name)) : base;
|
|
999
1382
|
let provider;
|
|
1000
1383
|
try {
|
|
1001
1384
|
provider = def.model ? opts.providerFor(def.model) : opts.provider();
|
|
@@ -1013,6 +1396,7 @@ ${def.prompt}`,
|
|
|
1013
1396
|
cwd: opts.cwd,
|
|
1014
1397
|
maxSteps: def.maxSteps ?? 50,
|
|
1015
1398
|
label: def.name,
|
|
1399
|
+
reasoning: opts.reasoning?.() ?? "auto",
|
|
1016
1400
|
// Subagent edits belong to the parent's turn so /undo reverts them too.
|
|
1017
1401
|
checkpoint: ctx.checkpoint
|
|
1018
1402
|
});
|
|
@@ -1026,22 +1410,22 @@ ${def.prompt}`,
|
|
|
1026
1410
|
if (ctx.ask) child.askPermission = ctx.ask;
|
|
1027
1411
|
await child.run(input.prompt, ctx.signal);
|
|
1028
1412
|
ctx.addUsage?.(child.totalUsage);
|
|
1029
|
-
const
|
|
1030
|
-
const display =
|
|
1031
|
-
if (outcome?.type === "error") return { output: `Subagent ${def.name}
|
|
1032
|
-
if (outcome?.type === "aborted") return { output: `Subagent ${def.name}
|
|
1413
|
+
const tokens2 = child.totalUsage.inputTokens + child.totalUsage.outputTokens;
|
|
1414
|
+
const display = t("{steps} steps \xB7 {tokens} tokens", { steps, tokens: tokens2 >= 1e3 ? `${Math.round(tokens2 / 1e3)}k` : tokens2 });
|
|
1415
|
+
if (outcome?.type === "error") return { output: `Subagent ${def.name} failed: ${outcome.message}`, isError: true, display };
|
|
1416
|
+
if (outcome?.type === "aborted") return { output: `Subagent ${def.name} was cancelled.`, isError: true, display };
|
|
1033
1417
|
const last = [...child.messages].reverse().find((m) => m.role === "assistant" && m.content);
|
|
1034
|
-
const report = last && typeof last.content === "string" ? last.content : "(
|
|
1418
|
+
const report = last && typeof last.content === "string" ? last.content : "(no report)";
|
|
1035
1419
|
const suffix = outcome?.type === "stepLimit" ? `
|
|
1036
1420
|
|
|
1037
|
-
(
|
|
1421
|
+
(The subagent stopped at its ${def.maxSteps ?? 50}-step limit.)` : "";
|
|
1038
1422
|
return { output: report + suffix, display };
|
|
1039
1423
|
}
|
|
1040
1424
|
});
|
|
1041
1425
|
}
|
|
1042
1426
|
|
|
1043
1427
|
// src/tools/todo.ts
|
|
1044
|
-
import { z as
|
|
1428
|
+
import { z as z14 } from "zod";
|
|
1045
1429
|
var TodoStore = class {
|
|
1046
1430
|
items = [];
|
|
1047
1431
|
listeners = /* @__PURE__ */ new Set();
|
|
@@ -1058,18 +1442,18 @@ function createTodoTool(store) {
|
|
|
1058
1442
|
return defineTool({
|
|
1059
1443
|
name: "todo_write",
|
|
1060
1444
|
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.",
|
|
1061
|
-
schema:
|
|
1062
|
-
todos:
|
|
1445
|
+
schema: z14.object({
|
|
1446
|
+
todos: z14.array(z14.object({ content: z14.string(), status: z14.enum(["pending", "in_progress", "completed"]) }))
|
|
1063
1447
|
}),
|
|
1064
1448
|
kind: "read",
|
|
1065
1449
|
target: (input) => `${input.todos.length} item`,
|
|
1066
1450
|
async run(input) {
|
|
1067
|
-
if (input.todos.filter((
|
|
1068
|
-
return { output: "
|
|
1451
|
+
if (input.todos.filter((t2) => t2.status === "in_progress").length > 1) {
|
|
1452
|
+
return { output: "Only one item may be in_progress.", isError: true };
|
|
1069
1453
|
}
|
|
1070
1454
|
store.set(input.todos);
|
|
1071
|
-
const done = input.todos.filter((
|
|
1072
|
-
return { output: `Todo
|
|
1455
|
+
const done = input.todos.filter((t2) => t2.status === "completed").length;
|
|
1456
|
+
return { output: `Todo updated: ${done}/${input.todos.length} completed.` };
|
|
1073
1457
|
}
|
|
1074
1458
|
});
|
|
1075
1459
|
}
|
|
@@ -1081,13 +1465,14 @@ function createRuntime(opts) {
|
|
|
1081
1465
|
const home = bccliHome(env);
|
|
1082
1466
|
let config = loadConfig(opts.cwd, env);
|
|
1083
1467
|
let modelRef = opts.args.model ?? config.model;
|
|
1468
|
+
let reasoning = opts.args.reasoning ?? config.reasoning;
|
|
1084
1469
|
const makeProvider = (ref) => {
|
|
1085
1470
|
const resolved = resolveModel(config, ref, env);
|
|
1086
|
-
return opts.provider ?? createProvider({ baseURL: resolved.baseURL, apiKey: resolved.apiKey, model: resolved.model });
|
|
1471
|
+
return opts.provider ?? createProvider({ baseURL: resolved.baseURL, apiKey: resolved.apiKey, model: resolved.model, providerId: resolved.providerId });
|
|
1087
1472
|
};
|
|
1088
1473
|
const provider = makeProvider(modelRef);
|
|
1089
1474
|
const mode = opts.args.allowAll ? "allowAll" : opts.args.permissionMode ?? config.permissionMode;
|
|
1090
|
-
const rules = [...config.allow, ...opts.args.allowedTools.map((
|
|
1475
|
+
const rules = [...config.allow, ...opts.args.allowedTools.map((t2) => TOOL_RULES[t2] ?? t2)];
|
|
1091
1476
|
const permissions = new Permissions(mode, rules, opts.cwd);
|
|
1092
1477
|
const roots = { cwd: opts.cwd, home, userHome: opts.userHome ?? homedir() };
|
|
1093
1478
|
const skills = loadSkills(roots);
|
|
@@ -1107,16 +1492,27 @@ function createRuntime(opts) {
|
|
|
1107
1492
|
}
|
|
1108
1493
|
}
|
|
1109
1494
|
const systemPrompt = buildSystemPrompt({ cwd: opts.cwd, home, model: modelRef, skills });
|
|
1110
|
-
const
|
|
1495
|
+
const webSearch = createWebSearchTool({
|
|
1496
|
+
botconnector: () => {
|
|
1497
|
+
try {
|
|
1498
|
+
const bc = resolveModel(config, "bc-cloud/search", env);
|
|
1499
|
+
return bc.apiKey ? { baseURL: bc.baseURL, apiKey: bc.apiKey } : void 0;
|
|
1500
|
+
} catch {
|
|
1501
|
+
return void 0;
|
|
1502
|
+
}
|
|
1503
|
+
}
|
|
1504
|
+
});
|
|
1505
|
+
const baseTools = [...ALL_TOOLS, webSearch, createSkillTool(skills), createTodoTool(todos), createExitPlanTool({ permissions, interaction })];
|
|
1111
1506
|
const task = createTaskTool({
|
|
1112
1507
|
agents: [...BUILTIN_AGENTS, ...agentDefs],
|
|
1113
1508
|
// Subagents get whatever the main agent has right now (incl. MCP tools), minus task itself.
|
|
1114
|
-
baseTools: () => agent.tools.filter((
|
|
1509
|
+
baseTools: () => agent.tools.filter((t2) => t2.name !== "task"),
|
|
1115
1510
|
permissions,
|
|
1116
1511
|
provider: () => agent.provider,
|
|
1117
1512
|
providerFor: makeProvider,
|
|
1118
1513
|
systemPrompt,
|
|
1119
|
-
cwd: opts.cwd
|
|
1514
|
+
cwd: opts.cwd,
|
|
1515
|
+
reasoning: () => reasoning
|
|
1120
1516
|
});
|
|
1121
1517
|
const agent = new Agent({
|
|
1122
1518
|
provider,
|
|
@@ -1125,6 +1521,7 @@ function createRuntime(opts) {
|
|
|
1125
1521
|
systemPrompt,
|
|
1126
1522
|
cwd: opts.cwd,
|
|
1127
1523
|
history,
|
|
1524
|
+
reasoning,
|
|
1128
1525
|
onMessage: (m) => session.append(m),
|
|
1129
1526
|
onReset: () => session.reset(),
|
|
1130
1527
|
onTurnStart: () => checkpoints.beginTurn(),
|
|
@@ -1166,6 +1563,9 @@ function createRuntime(opts) {
|
|
|
1166
1563
|
get modelRef() {
|
|
1167
1564
|
return modelRef;
|
|
1168
1565
|
},
|
|
1566
|
+
get reasoning() {
|
|
1567
|
+
return reasoning;
|
|
1568
|
+
},
|
|
1169
1569
|
agent,
|
|
1170
1570
|
get session() {
|
|
1171
1571
|
return session;
|
|
@@ -1195,36 +1595,59 @@ function createRuntime(opts) {
|
|
|
1195
1595
|
setModel(ref) {
|
|
1196
1596
|
agent.provider = makeProvider(ref);
|
|
1197
1597
|
modelRef = ref;
|
|
1598
|
+
},
|
|
1599
|
+
setReasoning(level) {
|
|
1600
|
+
reasoning = level;
|
|
1601
|
+
agent.reasoning = level;
|
|
1198
1602
|
}
|
|
1199
1603
|
};
|
|
1200
1604
|
}
|
|
1201
1605
|
|
|
1202
1606
|
// src/version.ts
|
|
1203
|
-
var VERSION = true ? "0.4.0-beta.
|
|
1607
|
+
var VERSION = true ? "0.4.0-beta.21" : "0.0.0-dev";
|
|
1204
1608
|
|
|
1205
1609
|
// src/cli.ts
|
|
1610
|
+
function chooseLanguage(argv) {
|
|
1611
|
+
let fromConfig;
|
|
1612
|
+
try {
|
|
1613
|
+
fromConfig = readGlobalConfigFile(process.env).language;
|
|
1614
|
+
} catch {
|
|
1615
|
+
}
|
|
1616
|
+
setLanguage(resolveLanguage(argv, process.env, fromConfig));
|
|
1617
|
+
}
|
|
1206
1618
|
async function main() {
|
|
1619
|
+
chooseLanguage(process.argv.slice(2));
|
|
1207
1620
|
const args = parseCliArgs(process.argv.slice(2));
|
|
1208
1621
|
if (args.version) {
|
|
1209
1622
|
console.log(VERSION);
|
|
1210
1623
|
return 0;
|
|
1211
1624
|
}
|
|
1212
1625
|
if (args.help) {
|
|
1213
|
-
console.log(
|
|
1626
|
+
console.log(helpText());
|
|
1214
1627
|
return 0;
|
|
1215
1628
|
}
|
|
1216
1629
|
const cwd = process.cwd();
|
|
1217
1630
|
if (args.command === "login") return runLogin(args.loginProvider ?? "bc-cloud", cwd);
|
|
1218
1631
|
if (args.command === "mcp") {
|
|
1219
|
-
const { runMcpCommand } = await import("./mcpCli-
|
|
1220
|
-
const { readSecret } = await import("./login-
|
|
1632
|
+
const { runMcpCommand } = await import("./mcpCli-J43FHF2H.js");
|
|
1633
|
+
const { readSecret } = await import("./login-TG5LQP3I.js");
|
|
1221
1634
|
return runMcpCommand(args, { env: process.env, cwd, out: (s) => console.log(s), err: (s) => console.error(s), readSecret });
|
|
1222
1635
|
}
|
|
1223
1636
|
if (args.command === "provider") {
|
|
1224
|
-
const { runProviderCommand } = await import("./providerCli-
|
|
1225
|
-
const { readSecret } = await import("./login-
|
|
1637
|
+
const { runProviderCommand } = await import("./providerCli-6FXMBKQL.js");
|
|
1638
|
+
const { readSecret } = await import("./login-TG5LQP3I.js");
|
|
1226
1639
|
return runProviderCommand(args, { env: process.env, cwd, out: (s) => console.log(s), err: (s) => console.error(s), readSecret });
|
|
1227
1640
|
}
|
|
1641
|
+
if (args.command === "connect" || args.command === "disconnect" || args.command === "integrations") {
|
|
1642
|
+
const { runIntegrationCommand } = await import("./integrationCli-ZTZSZLO7.js");
|
|
1643
|
+
return runIntegrationCommand(args, {
|
|
1644
|
+
env: process.env,
|
|
1645
|
+
cwd,
|
|
1646
|
+
out: (s) => console.log(s),
|
|
1647
|
+
err: (s) => console.error(s),
|
|
1648
|
+
fetch: globalThis.fetch
|
|
1649
|
+
});
|
|
1650
|
+
}
|
|
1228
1651
|
if (args.command === "models") {
|
|
1229
1652
|
const config = loadConfig(cwd);
|
|
1230
1653
|
const resolved = resolveModel(config, args.model ?? config.model);
|
|
@@ -1238,11 +1661,11 @@ async function main() {
|
|
|
1238
1661
|
} catch {
|
|
1239
1662
|
}
|
|
1240
1663
|
if (args.print) {
|
|
1241
|
-
if (!args.prompt) throw new ConfigError('
|
|
1242
|
-
const { serversToStart } = await import("./config-
|
|
1664
|
+
if (!args.prompt) throw new ConfigError(t('-p mode needs a task, e.g. bccli -p "explain this repo"'));
|
|
1665
|
+
const { serversToStart } = await import("./config-PBEZ3IGD.js");
|
|
1243
1666
|
const plan = serversToStart(rt.home, cwd);
|
|
1244
1667
|
for (const s of plan.needTrust) {
|
|
1245
|
-
console.error(
|
|
1668
|
+
console.error(t('Project MCP server "{name}" skipped (not approved yet; run interactive bccli once to approve it).', { name: s.name }));
|
|
1246
1669
|
}
|
|
1247
1670
|
await rt.startMcp(plan.start);
|
|
1248
1671
|
try {
|
|
@@ -1251,8 +1674,8 @@ async function main() {
|
|
|
1251
1674
|
await rt.mcp.stop();
|
|
1252
1675
|
}
|
|
1253
1676
|
}
|
|
1254
|
-
if (!process.stdin.isTTY) throw new ConfigError('
|
|
1255
|
-
const { startInteractive } = await import("./ui-
|
|
1677
|
+
if (!process.stdin.isTTY) throw new ConfigError(t('Interactive mode needs a terminal. For scripts/CI use: bccli -p "task"'));
|
|
1678
|
+
const { startInteractive } = await import("./ui-SRXPL7KU.js");
|
|
1256
1679
|
return startInteractive(rt, { initialPrompt: args.prompt, resume: args.resume, version: VERSION });
|
|
1257
1680
|
}
|
|
1258
1681
|
main().then(
|