@botconnector/bccli 0.4.0-beta.3 → 0.4.0-beta.30

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/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
- } from "./chunk-OSWQPSA4.js";
11
+ runPrint,
12
+ validateGlobPattern
13
+ } from "./chunk-22CEHQVR.js";
14
+ import {
15
+ helpText,
16
+ parseCliArgs
17
+ } from "./chunk-ON6JYPE7.js";
24
18
  import {
25
19
  Permissions
26
20
  } from "./chunk-ELML4KVQ.js";
27
21
  import {
28
- providerName
29
- } from "./chunk-U7DLMVPZ.js";
22
+ addProviderKey,
23
+ completionAssistantMessage,
24
+ createProvider,
25
+ hasPendingProgrammaticReplay,
26
+ listAllModels
27
+ } from "./chunk-WWQVALA5.js";
28
+ import {
29
+ providerName,
30
+ readGlobalConfigFile
31
+ } from "./chunk-K5JNR3HL.js";
32
+ import "./chunk-SN3ODJZN.js";
33
+ import {
34
+ runLogin
35
+ } from "./chunk-DJR4EAVL.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
- resolveModel
39
- } from "./chunk-RZUJYAQV.js";
44
+ resolveLanguage,
45
+ resolveModel,
46
+ setLanguage,
47
+ t
48
+ } from "./chunk-EO7IVVX4.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} karakter dipotong] \u2026
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 ? `[dihentikan: timeout ${Math.round((input.timeout_ms ?? 12e4) / 1e3)} detik]` : r.aborted ? "[dibatalkan user]" : `[exit code ${r.exitCode}]`;
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 dan new_string sama; tidak ada perubahan.", isError: true };
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 muncul ${count} kali di ${input.path}. Tambah konteks agar unik, atau pakai replace_all.`,
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 tidak ditemukan di ${input.path}. Baca ulang file-nya.`, isError: true };
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 `Baca ${input.path} dulu dengan read sebelum mengedit.`;
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 `Tidak bisa membaca ${input.path}: ${errorCode(error)}`;
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: `Baca ${input.path} dulu dengan read sebelum mengedit.`, isError: true };
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: `Tidak bisa membaca ${input.path}: ${errorCode(error)}`, isError: true };
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: `Mengedit ${input.path}.`, display: formatDiff(diffLines(content, next)) };
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 = 5e4;
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,202 @@ 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(/&nbsp;/g, " ").replace(/&lt;/g, "<").replace(/&gt;/g, ">").replace(/&quot;/g, '"').replace(/&#39;/g, "'").replace(/&amp;/g, "&").split("\n").map((l) => l.replace(/\s+/g, " ").trim()).filter(Boolean).join("\n");
302
381
  }
303
- var fetchTool = defineTool({
304
- name: "fetch",
305
- description: "Fetch a URL (http/https). HTML is converted to plain text. Max 50k characters.",
306
- schema: z3.object({ url: z3.string().describe("Absolute http(s) URL") }),
307
- kind: "fetch",
308
- target: (input) => input.url,
309
- async run(input, ctx) {
310
- if (!/^https?:\/\//i.test(input.url)) return { output: "URL harus diawali http:// atau https://", isError: true };
311
- let url = new URL(input.url);
312
- let res;
313
- try {
314
- for (let hop = 0; hop < 5; hop++) {
315
- res = await fetch(url, { signal: AbortSignal.any([ctx.signal, AbortSignal.timeout(3e4)]), redirect: "manual" });
316
- const location = res.status >= 300 && res.status < 400 ? res.headers.get("location") : null;
317
- if (!location) break;
318
- const next = new URL(location, url);
319
- if (next.host !== url.host) {
320
- await res.body?.cancel();
321
- return {
322
- output: `Redirect ke host lain: ${next.href}. Panggil fetch lagi dengan URL itu kalau memang perlu (akan diminta izin).`,
323
- isError: true
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")) };
325
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}`) };
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
- url = next;
557
+ return { error: `Redirect to another host: ${next.href}. Call fetch again with that URL if needed (permission will be requested).` };
328
558
  }
329
- } catch (error) {
330
- return { output: `Gagal mengambil ${input.url}: ${error.message}`, isError: true };
559
+ await res.body?.cancel();
560
+ url = next;
331
561
  }
332
- if (!res) return { output: `Gagal mengambil ${input.url}`, isError: true };
333
- const { text: body, truncated } = await readCapped(res);
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 fg from "fast-glob";
575
+ import { glob as nativeGlob } from "fs/promises";
345
576
  import { z as z4 } from "zod";
577
+ var MAX_OUTPUT_CHARS = 24e3;
346
578
  var globTool = defineTool({
347
579
  name: "glob",
348
580
  description: 'Find files by glob pattern, e.g. "src/**/*.ts". Respects .gitignore. Max 500 results.',
@@ -353,12 +585,28 @@ var globTool = defineTool({
353
585
  kind: "read",
354
586
  target: (input) => input.pattern,
355
587
  async run(input, ctx) {
588
+ const invalid = validateGlobPattern(input.pattern);
589
+ if (invalid) return { output: invalid, isError: true };
356
590
  const root = resolvePath(ctx.cwd, input.path ?? ".");
357
- const files = (await fg(input.pattern, { cwd: root, ignore: ignorePatterns(root), onlyFiles: true, dot: true })).sort();
358
- if (!files.length) return { output: "Tidak ada file yang cocok." };
359
- const shown = files.slice(0, 500);
360
- const more = files.length > shown.length ? `
361
- \u2026 ${files.length - shown.length} file lagi` : "";
591
+ const files = [];
592
+ for await (const file of nativeGlob(input.pattern, { cwd: root, exclude: ignorePatterns(root) })) {
593
+ files.push(normalizeGlobPath(file));
594
+ }
595
+ files.sort();
596
+ if (!files.length) return { output: "No matching files." };
597
+ const first = files.slice(0, 500);
598
+ const shown = [];
599
+ let size = 0;
600
+ for (const file of first) {
601
+ const extra = file.length + (shown.length ? 1 : 0);
602
+ if (shown.length && size + extra > MAX_OUTPUT_CHARS) break;
603
+ shown.push(file);
604
+ size += extra;
605
+ if (size >= MAX_OUTPUT_CHARS) break;
606
+ }
607
+ const omitted = files.length - shown.length;
608
+ const more = omitted > 0 ? `
609
+ \u2026 ${omitted} more files omitted (narrow pattern/path)` : "";
362
610
  return { output: shown.join("\n") + more, display: `${files.length} file` };
363
611
  }
364
612
  });
@@ -366,12 +614,16 @@ var globTool = defineTool({
366
614
  // src/tools/grep.ts
367
615
  import { execFile } from "child_process";
368
616
  import { readFile as readFile2 } from "fs/promises";
369
- import fg2 from "fast-glob";
617
+ import { glob as nativeGlob2 } from "fs/promises";
370
618
  import { z as z5 } from "zod";
371
619
  var MAX_MATCHES = 200;
620
+ var MAX_OUTPUT_CHARS2 = 24e3;
372
621
  async function grepJs(pattern, opts) {
373
622
  const regex = new RegExp(pattern, opts.ignoreCase ? "i" : "");
374
- const files = await fg2(opts.glob ?? "**/*", { cwd: opts.root, ignore: ignorePatterns(opts.root), onlyFiles: true, dot: true });
623
+ const files = [];
624
+ for await (const file of nativeGlob2(opts.glob ?? "**/*", { cwd: opts.root, exclude: ignorePatterns(opts.root) })) {
625
+ files.push(normalizeGlobPath(file));
626
+ }
375
627
  const out = [];
376
628
  for (const file of files.sort()) {
377
629
  let text;
@@ -416,15 +668,31 @@ var grepTool = defineTool({
416
668
  kind: "read",
417
669
  target: (input) => input.pattern,
418
670
  async run(input, ctx) {
671
+ if (input.glob) {
672
+ const invalid = validateGlobPattern(input.glob);
673
+ if (invalid) return { output: invalid, isError: true };
674
+ }
419
675
  const opts = { root: resolvePath(ctx.cwd, input.path ?? "."), glob: input.glob, ignoreCase: input.ignore_case };
420
676
  try {
421
677
  new RegExp(input.pattern);
422
678
  } catch (error) {
423
- return { output: `Regex tidak valid: ${error.message}`, isError: true };
679
+ return { output: `Invalid regex: ${error.message}`, isError: true };
424
680
  }
425
681
  const matches = await grepRg(input.pattern, opts) ?? await grepJs(input.pattern, opts);
426
- if (!matches.length) return { output: "Tidak ada yang cocok." };
427
- return { output: matches.join("\n"), display: `${matches.length} hasil` };
682
+ if (!matches.length) return { output: "No matches." };
683
+ const shown = [];
684
+ let size = 0;
685
+ for (const match of matches) {
686
+ const extra = match.length + (shown.length ? 1 : 0);
687
+ if (shown.length && size + extra > MAX_OUTPUT_CHARS2) break;
688
+ shown.push(match);
689
+ size += extra;
690
+ if (size >= MAX_OUTPUT_CHARS2) break;
691
+ }
692
+ const omitted = matches.length - shown.length;
693
+ const suffix = omitted > 0 ? `
694
+ \u2026 ${omitted} more matches omitted (narrow pattern/path/glob)` : "";
695
+ return { output: shown.join("\n") + suffix, display: t("{n} results", { n: matches.length }) };
428
696
  }
429
697
  });
430
698
 
@@ -432,6 +700,7 @@ var grepTool = defineTool({
432
700
  import { readFile as readFile3 } from "fs/promises";
433
701
  import { z as z6 } from "zod";
434
702
  var MAX_LINE = 2e3;
703
+ var MAX_OUTPUT_CHARS3 = 4e4;
435
704
  var readTool = defineTool({
436
705
  name: "read",
437
706
  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 +717,37 @@ var readTool = defineTool({
448
717
  try {
449
718
  buffer = await readFile3(abs);
450
719
  } catch (error) {
451
- return { output: `Tidak bisa membaca ${input.path}: ${errorCode(error)}`, isError: true };
720
+ return { output: `Cannot read ${input.path}: ${errorCode(error)}`, isError: true };
452
721
  }
453
722
  if (buffer.subarray(0, 8e3).includes(0)) {
454
- return { output: `${input.path} adalah file biner, tidak dibaca.`, isError: true };
723
+ return { output: `${input.path} is a binary file; not read.`, isError: true };
455
724
  }
456
725
  const lines = buffer.toString("utf8").split(/\r?\n/);
457
726
  const start = (input.offset ?? 1) - 1;
458
727
  const limit = input.limit ?? 2e3;
459
- const slice = lines.slice(start, start + limit);
460
- const body = slice.map((line, i) => {
461
- const text = line.length > MAX_LINE ? `${line.slice(0, MAX_LINE)}\u2026 [baris dipotong]` : line;
462
- return `${String(start + i + 1).padStart(6)} ${text}`;
463
- }).join("\n");
464
- const remaining = lines.length - start - slice.length;
728
+ const requested = lines.slice(start, start + limit);
729
+ const shown = [];
730
+ let size = 0;
731
+ for (let i = 0; i < requested.length; i++) {
732
+ const line = requested[i];
733
+ const text = line.length > MAX_LINE ? `${line.slice(0, MAX_LINE)}\u2026 [line truncated]` : line;
734
+ const rendered = `${String(start + i + 1).padStart(6)} ${text}`;
735
+ const extra = rendered.length + (shown.length ? 1 : 0);
736
+ if (shown.length && size + extra > MAX_OUTPUT_CHARS3) break;
737
+ shown.push(rendered);
738
+ size += extra;
739
+ if (size >= MAX_OUTPUT_CHARS3) break;
740
+ }
741
+ const body = shown.join("\n");
742
+ const remaining = lines.length - start - shown.length;
743
+ const tokenBudgetHit = shown.length < requested.length;
744
+ const nextOffset = start + shown.length + 1;
465
745
  ctx.readFiles.add(abs);
466
746
  return {
467
- output: remaining > 0 ? `${body}
468
- \u2026 ${remaining} baris lagi (pakai offset)` : body,
469
- display: `${slice.length} baris`
747
+ output: remaining > 0 ? tokenBudgetHit ? `${body}
748
+ \u2026 ${remaining} more lines (token budget reached; continue with offset=${nextOffset})` : `${body}
749
+ \u2026 ${remaining} more lines (use offset)` : body,
750
+ display: t("{n} lines", { n: shown.length })
470
751
  };
471
752
  }
472
753
  });
@@ -487,7 +768,7 @@ var writeTool = defineTool({
487
768
  target: (input) => input.path,
488
769
  async validate(input, ctx) {
489
770
  const abs = resolvePath(ctx.cwd, input.path);
490
- return existsSync(abs) && !ctx.readFiles.has(abs) ? `${input.path} sudah ada. Baca dulu dengan read sebelum menimpanya.` : void 0;
771
+ return existsSync(abs) && !ctx.readFiles.has(abs) ? `${input.path} already exists. Read it with read before overwriting it.` : void 0;
491
772
  },
492
773
  async preview(input, ctx) {
493
774
  const abs = resolvePath(ctx.cwd, input.path);
@@ -497,18 +778,18 @@ var writeTool = defineTool({
497
778
  async run(input, ctx) {
498
779
  const abs = resolvePath(ctx.cwd, input.path);
499
780
  if (existsSync(abs) && !ctx.readFiles.has(abs)) {
500
- return { output: `${input.path} sudah ada. Baca dulu dengan read sebelum menimpanya.`, isError: true };
781
+ return { output: `${input.path} already exists. Read it with read before overwriting it.`, isError: true };
501
782
  }
502
783
  try {
503
784
  await ctx.checkpoint?.(abs);
504
785
  await mkdir(dirname(abs), { recursive: true });
505
786
  await writeFile2(abs, input.content);
506
787
  } catch (error) {
507
- return { output: `Gagal menulis ${input.path}: ${errorCode(error)}`, isError: true };
788
+ return { output: `Failed to write ${input.path}: ${errorCode(error)}`, isError: true };
508
789
  }
509
790
  ctx.readFiles.add(abs);
510
791
  const lines = input.content.split("\n").length;
511
- return { output: `Menulis ${input.path} (${lines} baris).`, display: `${lines} baris ditulis` };
792
+ return { output: `Wrote ${input.path} (${lines} lines).`, display: t("{lines} lines written", { lines }) };
512
793
  }
513
794
  });
514
795
 
@@ -517,74 +798,83 @@ var ALL_TOOLS = [readTool, writeTool, editTool, bashTool, grepTool, globTool, fe
517
798
  function toolDefinitions(tools) {
518
799
  return tools.map((tool) => {
519
800
  const { $schema: _ignored, ...parameters } = tool.jsonSchema ?? z8.toJSONSchema(tool.schema);
520
- return { type: "function", function: { name: tool.name, description: tool.description, parameters } };
801
+ return { type: "function", function: { name: tool.name, description: tool.description, parameters, ...tool.outputJsonSchema ? { output_schema: tool.outputJsonSchema } : {} } };
521
802
  });
522
803
  }
523
804
 
524
- // src/textToolCalls.ts
525
- var PAIR = /(?:<arg_key>\s*|`)?([A-Za-z_]\w*)(?:\s*<\/arg_key>)?`?\s*(?:=\s*`|<arg_value>)([\s\S]*?)<\/arg_value>/g;
526
- var CALL_START = /<tool_call>|\n?[^\n]*\bTool call list\b[^\n]*|\n\s*\d+\.\s*Tool:/g;
527
- function coerce(value, type) {
528
- const types = Array.isArray(type) ? type : [type];
529
- const v = value.trim();
530
- if ((types.includes("integer") || types.includes("number")) && v !== "" && !Number.isNaN(Number(v))) return Number(v);
531
- if (types.includes("boolean") && /^(true|false)$/i.test(v)) return v.toLowerCase() === "true";
532
- if ((types.includes("object") || types.includes("array")) && /^[[{]/.test(v)) {
533
- try {
534
- return JSON.parse(v);
535
- } catch {
805
+ // src/webBudget.ts
806
+ var KEEP_TOKENS = 4e4;
807
+ var PRUNE_MIN_TOKENS = 2e4;
808
+ var STUB = "[old tool result removed:";
809
+ var tokens = (text) => Math.ceil(text.length / 4);
810
+ var isStub = (content) => content.startsWith(STUB);
811
+ function pruneOldFetches(messages) {
812
+ const urls = /* @__PURE__ */ new Map();
813
+ for (const m of messages) {
814
+ if (m.role !== "assistant") continue;
815
+ for (const c of m.tool_calls ?? []) {
816
+ if (c.function.name !== "fetch") continue;
817
+ try {
818
+ urls.set(c.id, String(JSON.parse(c.function.arguments).url ?? ""));
819
+ } catch {
820
+ urls.set(c.id, "");
821
+ }
536
822
  }
537
823
  }
538
- return value.replace(/^\n+|\n+$/g, "");
539
- }
540
- function pickTool(name, keys, tools) {
541
- const named = name && tools.find((t) => t.function.name === name);
542
- if (named) return named;
543
- let best;
544
- let bestScore = 0;
545
- let tie = false;
546
- for (const t of tools) {
547
- const params = t.function.parameters;
548
- const props = Object.keys(params.properties ?? {});
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;
824
+ let kept = 0;
825
+ let oldTokens = 0;
826
+ const old = [];
827
+ for (let i = messages.length - 1; i >= 0; i--) {
828
+ const m = messages[i];
829
+ if (m.role !== "tool" || !urls.has(m.tool_call_id) || isStub(m.content)) continue;
830
+ kept += tokens(m.content);
831
+ if (kept > KEEP_TOKENS) {
832
+ old.push(i);
833
+ oldTokens += tokens(m.content);
834
+ }
553
835
  }
554
- return tie ? void 0 : best;
836
+ if (oldTokens <= PRUNE_MIN_TOKENS) return false;
837
+ for (const i of old) {
838
+ const m = messages[i];
839
+ messages[i] = { ...m, content: `${STUB} fetch ${urls.get(m.tool_call_id)}. Fetch it again (use prompt for a focused excerpt) if still needed.]` };
840
+ }
841
+ return true;
555
842
  }
556
- function recoverTextToolCalls(text, tools) {
557
- if (!/<\/arg_value>|<tool_call>/.test(text)) return null;
558
- const starts = [...text.matchAll(CALL_START)].map((m) => m.index ?? 0);
559
- const firstPair = text.search(PAIR);
560
- const cut = Math.min(...[...starts, firstPair].filter((i) => i >= 0));
561
- const bounds = starts.filter((i) => i >= cut);
562
- if (!bounds.length || bounds[0] > cut) bounds.unshift(cut);
563
- const toolCalls = [];
564
- bounds.forEach((start, n) => {
565
- const segment = text.slice(start, bounds[n + 1] ?? text.length);
566
- const name = /<tool_call>\s*([A-Za-z_][\w.-]*)/.exec(segment)?.[1] ?? /Tool:\s*`?([A-Za-z_][\w.-]*)`?/.exec(segment)?.[1];
567
- let args = [];
568
- const flush = () => {
569
- if (!args.length) return;
570
- const keys = args.map(([k]) => k);
571
- const tool = pickTool(name, keys, tools);
572
- if (tool) {
573
- const props = tool.function.parameters.properties ?? {};
574
- const input = Object.fromEntries(args.filter(([k]) => k in props).map(([k, v]) => [k, coerce(v, props[k].type)]));
575
- toolCalls.push({ id: `text_call_${toolCalls.length}`, name: tool.function.name, arguments: JSON.stringify(input) });
576
- }
577
- args = [];
578
- };
579
- for (const m of segment.matchAll(PAIR)) {
580
- if (args.some(([k]) => k === m[1])) flush();
581
- args.push([m[1], m[2]]);
843
+ var WebBudget = class {
844
+ fetches = 0;
845
+ chars = 0;
846
+ perTurn;
847
+ sessionChars;
848
+ constructor(limits = {}) {
849
+ this.perTurn = limits.perTurn ?? 8;
850
+ this.sessionChars = limits.sessionChars ?? 3e5;
851
+ }
852
+ startTurn() {
853
+ this.fetches = 0;
854
+ }
855
+ /** Gives back a reservation, e.g. when the fetch was served from cache and cost nothing. */
856
+ refund() {
857
+ if (this.fetches > 0) this.fetches--;
858
+ }
859
+ reset() {
860
+ this.fetches = 0;
861
+ this.chars = 0;
862
+ }
863
+ /** Reserves one fetch; returns the refusal to show the model when over budget. */
864
+ take() {
865
+ if (this.fetches >= this.perTurn) {
866
+ 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
867
  }
583
- flush();
584
- });
585
- if (!toolCalls.length) return null;
586
- return { text: text.slice(0, cut).trim(), toolCalls };
587
- }
868
+ if (this.chars >= this.sessionChars) {
869
+ 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.`;
870
+ }
871
+ this.fetches++;
872
+ return void 0;
873
+ }
874
+ record(chars) {
875
+ this.chars += chars;
876
+ }
877
+ };
588
878
 
589
879
  // src/agent.ts
590
880
  var estimateTokens = (text) => Math.ceil(text.length / 4);
@@ -602,6 +892,7 @@ var Agent = class {
602
892
  this.maxSteps = opts.maxSteps ?? 50;
603
893
  this.contextWindow = opts.contextWindow ?? 128e3;
604
894
  this.systemPrompt = opts.systemPrompt;
895
+ this.reasoning = opts.reasoning ?? "auto";
605
896
  }
606
897
  opts;
607
898
  provider;
@@ -611,6 +902,7 @@ var Agent = class {
611
902
  lastInputTokens = 0;
612
903
  onEvent = () => {
613
904
  };
905
+ reasoning;
614
906
  askPermission = async () => "no";
615
907
  tools;
616
908
  definitions;
@@ -618,6 +910,7 @@ var Agent = class {
618
910
  turnTools;
619
911
  turnDefinitions;
620
912
  readFiles = /* @__PURE__ */ new Set();
913
+ webBudget = new WebBudget();
621
914
  maxSteps;
622
915
  // ponytail: one window for every model; read it from /v1/models metadata when providers expose it.
623
916
  contextWindow;
@@ -630,7 +923,7 @@ var Agent = class {
630
923
  this.definitions = toolDefinitions(tools);
631
924
  }
632
925
  isParallelSafe(call) {
633
- const tool = this.turnTools.find((t) => t.name === call.name);
926
+ const tool = this.turnTools.find((t2) => t2.name === call.name);
634
927
  if (!tool?.parallelSafe) return false;
635
928
  try {
636
929
  const parsed = tool.schema.safeParse(JSON.parse(call.arguments || "{}"));
@@ -647,11 +940,13 @@ var Agent = class {
647
940
  load(messages) {
648
941
  this.messages = messages;
649
942
  this.readFiles.clear();
943
+ this.webBudget.reset();
650
944
  this.lastInputTokens = 0;
651
945
  }
652
946
  clear() {
653
947
  this.messages = [];
654
948
  this.readFiles.clear();
949
+ this.webBudget.reset();
655
950
  this.lastInputTokens = 0;
656
951
  this.opts.onReset?.();
657
952
  }
@@ -660,31 +955,37 @@ var Agent = class {
660
955
  messages: [{ role: "system", content: this.systemPrompt }, ...this.messages, { role: "user", content: COMPACT_PROMPT }],
661
956
  // Some gateways reject tool_calls in history when no tools are declared.
662
957
  tools: this.definitions,
663
- signal
958
+ signal,
959
+ reasoning: this.reasoning
664
960
  });
665
961
  this.clear();
666
- this.push({ role: "user", content: `Ringkasan percakapan sebelumnya:
962
+ this.push({ role: "user", content: `Summary of the previous conversation:
667
963
  ${completion.text}` });
668
- this.push({ role: "assistant", content: "Oke, saya lanjutkan dari ringkasan ini." });
964
+ this.push({ role: "assistant", content: "OK, I will continue from this summary." });
669
965
  this.onEvent({ type: "compacted" });
670
966
  }
671
967
  async run(text, signal) {
672
968
  this.turnTools = this.tools;
673
969
  this.turnDefinitions = this.definitions;
674
970
  this.opts.onTurnStart?.();
971
+ this.webBudget.startTurn();
675
972
  try {
676
- if (this.lastInputTokens > this.contextWindow * 0.8) await this.compact(signal);
973
+ if (this.lastInputTokens > this.contextWindow * 0.8 && !hasPendingProgrammaticReplay(this.messages)) await this.compact(signal);
677
974
  this.push({ role: "user", content: text });
678
975
  for (let step = 0; step < this.maxSteps; step++) {
679
- if (step > 0 && this.lastInputTokens > this.contextWindow * 0.8) {
976
+ if (step > 0 && this.lastInputTokens > this.contextWindow * 0.8 && !hasPendingProgrammaticReplay(this.messages)) {
680
977
  await this.compact(signal);
681
- this.push({ role: "user", content: `Lanjutkan tugas ini sesuai ringkasan di atas: ${text}` });
978
+ this.push({ role: "user", content: `Continue this task using the summary above: ${text}` });
682
979
  }
980
+ pruneOldFetches(this.messages);
683
981
  let completion = await this.provider.chat({
684
982
  messages: [{ role: "system", content: this.systemPrompt }, ...this.messages],
685
983
  tools: this.turnDefinitions,
984
+ programmaticToolNames: this.turnTools.filter((tool) => tool.programmaticSafe).map((tool) => tool.name),
686
985
  signal,
687
- onText: (delta) => this.onEvent({ type: "text", delta })
986
+ onText: (delta) => this.onEvent({ type: "text", delta }),
987
+ onThinking: (delta) => this.onEvent({ type: "thinking", delta }),
988
+ reasoning: this.reasoning
688
989
  });
689
990
  if (!completion.toolCalls.length && completion.finishReason !== "repetition") {
690
991
  const recovered = recoverTextToolCalls(completion.text, this.turnDefinitions);
@@ -701,26 +1002,17 @@ ${completion.text}` });
701
1002
  this.totalUsage.outputTokens += usage.outputTokens;
702
1003
  this.lastInputTokens = usage.inputTokens;
703
1004
  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
- });
1005
+ this.push(completionAssistantMessage(completion));
715
1006
  if (completion.finishReason === "repetition") {
716
1007
  this.onEvent({ type: "textReplace", text: completion.text });
717
1008
  this.onEvent({
718
1009
  type: "error",
719
- message: "Model terjebak mengulang teks, jawaban dihentikan. Coba ulangi atau ganti model dengan /model."
1010
+ message: t("The model got stuck repeating text, so the answer was stopped. Try again or switch models with /model.")
720
1011
  });
721
1012
  return;
722
1013
  }
723
1014
  if (!completion.toolCalls.length) {
1015
+ if (completion.finishReason === "continue") continue;
724
1016
  this.onEvent({ type: "done" });
725
1017
  return;
726
1018
  }
@@ -728,7 +1020,13 @@ ${completion.text}` });
728
1020
  let i = 0;
729
1021
  while (i < calls.length) {
730
1022
  if (signal.aborted) {
731
- for (const pending of calls.slice(i)) this.push({ role: "tool", tool_call_id: pending.id, content: "Dibatalkan oleh user." });
1023
+ for (const pending of calls.slice(i))
1024
+ this.push({
1025
+ role: "tool",
1026
+ tool_call_id: pending.id,
1027
+ content: "Cancelled by the user.",
1028
+ ...pending.caller ? { responses_caller: pending.caller } : {}
1029
+ });
732
1030
  this.onEvent({ type: "aborted" });
733
1031
  return;
734
1032
  }
@@ -737,7 +1035,12 @@ ${completion.text}` });
737
1035
  const batch = calls.slice(i, j);
738
1036
  const results = await Promise.all(batch.map((c) => this.runTool(c, signal)));
739
1037
  batch.forEach((c, k) => {
740
- this.push({ role: "tool", tool_call_id: c.id, content: results[k] });
1038
+ this.push({
1039
+ role: "tool",
1040
+ tool_call_id: c.id,
1041
+ content: results[k],
1042
+ ...c.caller ? { responses_caller: c.caller } : {}
1043
+ });
741
1044
  });
742
1045
  i = j;
743
1046
  }
@@ -757,7 +1060,7 @@ ${completion.text}` });
757
1060
  this.onEvent({
758
1061
  type: "error",
759
1062
  message: noTools ? `${message}
760
- Model ini sepertinya tidak mendukung tool calling. Coba model lain dengan /model.` : message
1063
+ ${t("This model may not support tool calling. Try another model with /model.")}` : message
761
1064
  });
762
1065
  }
763
1066
  }
@@ -767,20 +1070,22 @@ Model ini sepertinya tidak mendukung tool calling. Coba model lain dengan /model
767
1070
  this.onEvent({ type: "toolEnd", id: call.id, tool: call.name, output, isError: true });
768
1071
  return output;
769
1072
  };
770
- const tool = this.turnTools.find((t) => t.name === call.name);
771
- if (!tool) return fail(`Alat "${call.name}" tidak ada. Alat yang tersedia: ${this.turnTools.map((t) => t.name).join(", ")}`);
1073
+ const tool = this.turnTools.find((t2) => t2.name === call.name);
1074
+ if (!tool) return fail(`Tool "${call.name}" does not exist. Available tools: ${this.turnTools.map((t2) => t2.name).join(", ")}`);
772
1075
  let args;
773
1076
  try {
774
1077
  args = JSON.parse(call.arguments || "{}");
775
1078
  } catch {
776
- return fail(`Argumen bukan JSON valid: ${call.arguments.slice(0, 200)}`);
1079
+ return fail(`Arguments are not valid JSON: ${call.arguments.slice(0, 200)}`);
777
1080
  }
778
1081
  const parsed = tool.schema.safeParse(args);
779
1082
  if (!parsed.success) {
780
- return fail(`Argumen tidak valid: ${parsed.error.issues.map((i) => `${i.path.join(".") || "(root)"}: ${i.message}`).join("; ")}`);
1083
+ return fail(`Invalid arguments: ${parsed.error.issues.map((i) => `${i.path.join(".") || "(root)"}: ${i.message}`).join("; ")}`);
781
1084
  }
782
1085
  const input = parsed.data;
783
1086
  const target = tool.target(input);
1087
+ const overBudget = tool.name === "fetch" ? this.webBudget.take() : void 0;
1088
+ if (overBudget) return fail(overBudget, target);
784
1089
  const ctx = {
785
1090
  cwd: this.opts.cwd,
786
1091
  signal,
@@ -789,6 +1094,7 @@ Model ini sepertinya tidak mendukung tool calling. Coba model lain dengan /model
789
1094
  emit: (event) => this.onEvent(event),
790
1095
  ask: (req) => this.askPermission(req),
791
1096
  checkpoint: this.opts.checkpoint,
1097
+ refundFetch: () => this.webBudget.refund(),
792
1098
  addUsage: (u) => {
793
1099
  this.totalUsage.inputTokens += u.inputTokens;
794
1100
  this.totalUsage.outputTokens += u.outputTokens;
@@ -816,7 +1122,7 @@ Model ini sepertinya tidak mendukung tool calling. Coba model lain dengan /model
816
1122
  decision = answer === "no" ? "deny" : "allow";
817
1123
  }
818
1124
  if (decision === "deny") {
819
- const output = this.permissions.mode === "plan" ? "Ditolak: mode plan hanya boleh membaca dan mencari. Susun rencana untuk user tanpa mengubah apa pun." : "User menolak menjalankan alat ini. Tanyakan ke user apa yang diinginkan, atau coba cara lain.";
1125
+ 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.";
820
1126
  this.onEvent({ type: "toolEnd", id: call.id, tool: tool.name, output, isError: true });
821
1127
  return output;
822
1128
  }
@@ -826,6 +1132,7 @@ Model ini sepertinya tidak mendukung tool calling. Coba model lain dengan /model
826
1132
  } catch (error) {
827
1133
  result = { output: `Error: ${error.message}`, isError: true };
828
1134
  }
1135
+ if (tool.name === "fetch" && !result.isError) this.webBudget.record(result.output.length);
829
1136
  this.onEvent({ type: "toolEnd", id: call.id, tool: tool.name, output: result.output, display: result.display, isError: !!result.isError });
830
1137
  return result.output;
831
1138
  }
@@ -855,7 +1162,7 @@ var CheckpointStore = class {
855
1162
  while (this.turns() > MAX_TURNS) this.history.shift();
856
1163
  }
857
1164
  turns() {
858
- return this.history.filter((t) => t.size).length;
1165
+ return this.history.filter((t2) => t2.size).length;
859
1166
  }
860
1167
  async undo() {
861
1168
  while (this.history.length && !this.history.at(-1)?.size) this.history.pop();
@@ -890,9 +1197,10 @@ import { Client } from "@modelcontextprotocol/sdk/client/index.js";
890
1197
  import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
891
1198
  import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
892
1199
  import { z as z9 } from "zod";
1200
+ var MCP_OUTPUT_MAX_CHARS = 32e3;
893
1201
  function mcpToolName(server, tool, taken) {
894
- const clean = (s) => s.replace(/[^A-Za-z0-9_-]+/g, "_");
895
- const base = `mcp__${clean(server)}__${clean(tool)}`.slice(0, 64);
1202
+ const clean2 = (s) => s.replace(/[^A-Za-z0-9_-]+/g, "_");
1203
+ const base = `mcp__${clean2(server)}__${clean2(tool)}`.slice(0, 64);
896
1204
  let name = base;
897
1205
  for (let n = 2; taken.has(name); n++) name = `${base.slice(0, 64 - String(n).length - 1)}_${n}`;
898
1206
  taken.add(name);
@@ -901,7 +1209,7 @@ function mcpToolName(server, tool, taken) {
901
1209
  function withTimeout(promise, ms) {
902
1210
  let timer;
903
1211
  const timeout = new Promise((_, reject) => {
904
- timer = setTimeout(() => reject(new Error(`timeout ${Math.round(ms / 1e3)} detik`)), ms);
1212
+ timer = setTimeout(() => reject(new Error(t("timeout after {n} seconds", { n: Math.round(ms / 1e3) }))), ms);
905
1213
  });
906
1214
  return Promise.race([promise, timeout]).finally(() => clearTimeout(timer));
907
1215
  }
@@ -938,7 +1246,7 @@ var McpManager = class {
938
1246
  });
939
1247
  return;
940
1248
  }
941
- entry.tools = tools.map((t) => this.wrap(spec.name, client, t));
1249
+ entry.tools = tools.map((t2) => this.wrap(spec.name, client, t2));
942
1250
  entry.state = { ...entry.state, status: "ready", tools: entry.tools.length };
943
1251
  } catch (error) {
944
1252
  entry.state = { ...entry.state, status: "error", error: error.message };
@@ -954,7 +1262,9 @@ var McpManager = class {
954
1262
  description: `[MCP ${server}] ${tool.description ?? tool.name}`.slice(0, 1024),
955
1263
  schema: z9.record(z9.string(), z9.unknown()),
956
1264
  jsonSchema: tool.inputSchema,
1265
+ ...tool.outputSchema ? { outputJsonSchema: tool.outputSchema } : {},
957
1266
  kind: "mcp",
1267
+ programmaticSafe: tool.annotations?.readOnlyHint === true,
958
1268
  target: (input) => {
959
1269
  const text = JSON.stringify(input);
960
1270
  return text.length > 80 ? `${text.slice(0, 77)}\u2026` : text;
@@ -962,9 +1272,12 @@ var McpManager = class {
962
1272
  async run(input, ctx) {
963
1273
  try {
964
1274
  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") || "(kosong)";
966
- return { output: output.length > 5e4 ? `${output.slice(0, 5e4)}
967
- \u2026 [dipotong]` : output, isError: !!result.isError };
1275
+ const output = (result.content ?? []).map((c) => c.type === "text" ? c.text ?? "" : `[${c.type}]`).join("\n") || "(empty)";
1276
+ return {
1277
+ output: output.length > MCP_OUTPUT_MAX_CHARS ? `${output.slice(0, MCP_OUTPUT_MAX_CHARS)}
1278
+ \u2026 [truncated at ${MCP_OUTPUT_MAX_CHARS} characters; narrow the MCP request if possible]` : output,
1279
+ isError: !!result.isError
1280
+ };
968
1281
  } catch (error) {
969
1282
  return { output: `Server MCP ${server} error: ${error.message}`, isError: true };
970
1283
  }
@@ -975,14 +1288,14 @@ var McpManager = class {
975
1288
  const entry = this.servers.get(name);
976
1289
  if (!entry) return;
977
1290
  this.servers.delete(name);
978
- for (const t of entry.tools) this.taken.delete(t.name);
979
- if (entry.tools.length) this.opts.onToolsRemoved?.(entry.tools.map((t) => t.name));
1291
+ for (const t2 of entry.tools) this.taken.delete(t2.name);
1292
+ if (entry.tools.length) this.opts.onToolsRemoved?.(entry.tools.map((t2) => t2.name));
980
1293
  await entry.client?.close().catch(() => {
981
1294
  });
982
1295
  this.opts.onChange?.();
983
1296
  }
984
1297
  tools() {
985
- return [...this.servers.values()].flatMap((s) => s.tools);
1298
+ return [...this.servers.values()].flatMap((s) => s.tools).sort((a, b) => a.name.localeCompare(b.name));
986
1299
  }
987
1300
  states() {
988
1301
  return [...this.servers.values()].map((s) => s.state);
@@ -1003,15 +1316,15 @@ function createExitPlanTool(opts) {
1003
1316
  description: "Plan mode only: present the finished plan to the user for approval. If approved you may start implementing it.",
1004
1317
  schema: z10.object({ plan: z10.string().describe("The plan, in markdown") }),
1005
1318
  kind: "read",
1006
- target: () => "rencana",
1319
+ target: () => t("plan"),
1007
1320
  async run(input) {
1008
- if (opts.permissions.mode !== "plan") return { output: "exit_plan hanya dipakai di mode plan.", isError: true };
1321
+ if (opts.permissions.mode !== "plan") return { output: "exit_plan is only used in plan mode.", isError: true };
1009
1322
  const decision = await opts.interaction.approvePlan(input.plan);
1010
1323
  if (decision === "no") {
1011
- return { output: "User belum menyetujui. Tanyakan apa yang perlu diperbaiki, perbaiki rencananya, lalu panggil exit_plan lagi." };
1324
+ return { output: "The user has not approved yet. Ask what needs to change, revise the plan, then call exit_plan again." };
1012
1325
  }
1013
1326
  opts.permissions.mode = decision;
1014
- return { output: `Rencana disetujui. Mode sekarang ${decision}. Mulai kerjakan rencananya sekarang.` };
1327
+ return { output: `Plan approved. The mode is now ${decision}. Start carrying out the plan now.` };
1015
1328
  }
1016
1329
  });
1017
1330
  }
@@ -1028,7 +1341,7 @@ function createSkillTool(skills) {
1028
1341
  target: (input) => input.name,
1029
1342
  async run(input) {
1030
1343
  const skill = skills.find((s) => s.name === input.name);
1031
- if (!skill) return { output: `Skill "${input.name}" tidak ada. Tersedia: ${skills.map((s) => s.name).join(", ") || "(tidak ada)"}`, isError: true };
1344
+ if (!skill) return { output: `Skill "${input.name}" does not exist. Available: ${skills.map((s) => s.name).join(", ") || "(none)"}`, isError: true };
1032
1345
  const { body } = parseFrontmatter(readFileSync(skill.file, "utf8"));
1033
1346
  return { output: `Base directory for this skill: ${skill.dir}
1034
1347
 
@@ -1037,8 +1350,69 @@ ${body}`, display: skill.description.slice(0, 60) };
1037
1350
  });
1038
1351
  }
1039
1352
 
1040
- // src/tools/task.ts
1353
+ // src/tools/websearch.ts
1041
1354
  import { z as z12 } from "zod";
1355
+ var clean = (items, max) => (Array.isArray(items) ? items : []).filter((r) => typeof r?.url === "string" && /^https?:\/\//.test(r.url)).slice(0, max).map((r) => ({
1356
+ title: String(r.title || r.url).slice(0, 200),
1357
+ url: r.url,
1358
+ snippet: String(r.snippet || r.description || "").replace(/\s+/g, " ").trim().slice(0, 600)
1359
+ }));
1360
+ async function postJson(url, body, headers, signal) {
1361
+ const res = await fetch(url, {
1362
+ method: "POST",
1363
+ headers: { "content-type": "application/json", ...headers },
1364
+ body: JSON.stringify(body),
1365
+ signal: AbortSignal.any([signal, AbortSignal.timeout(3e4)])
1366
+ });
1367
+ if (!res.ok) throw new Error(`HTTP ${res.status}`);
1368
+ return res.json();
1369
+ }
1370
+ function createWebSearchTool(opts) {
1371
+ const keenableURL = opts.keenableURL ?? "https://api.keenable.ai/v1/search/public";
1372
+ return defineTool({
1373
+ name: "web_search",
1374
+ 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.",
1375
+ schema: z12.object({
1376
+ query: z12.string().min(1).max(500).describe("Search query"),
1377
+ max_results: z12.number().int().min(1).max(10).optional().describe("Number of results (default 6)")
1378
+ }),
1379
+ kind: "read",
1380
+ target: (input) => input.query,
1381
+ parallelSafe: () => true,
1382
+ async run(input, ctx) {
1383
+ const max = input.max_results ?? 6;
1384
+ const errors = [];
1385
+ let results = [];
1386
+ const bc = opts.botconnector();
1387
+ if (bc) {
1388
+ try {
1389
+ const data = await postJson(`${bc.baseURL.replace(/\/+$/, "")}/web/search`, { query: input.query, max_results: max }, { authorization: `Bearer ${bc.apiKey}` }, ctx.signal);
1390
+ results = clean(data.results, max);
1391
+ } catch (error) {
1392
+ errors.push(`BotConnector: ${error.message}`);
1393
+ }
1394
+ }
1395
+ if (!results.length) {
1396
+ try {
1397
+ const data = await postJson(keenableURL, { query: input.query }, { "x-keenable-title": "BotConnector" }, ctx.signal);
1398
+ results = clean(data.results, max);
1399
+ } catch (error) {
1400
+ errors.push(`Keenable: ${error.message}`);
1401
+ }
1402
+ }
1403
+ if (!results.length) {
1404
+ return { output: errors.length ? `Web search failed (${errors.join("; ")}).` : "No results.", isError: errors.length > 0 };
1405
+ }
1406
+ const output = results.map((r, i) => `${i + 1}. ${r.title}
1407
+ ${r.url}${r.snippet ? `
1408
+ ${r.snippet}` : ""}`).join("\n");
1409
+ return { output, display: t("{n} results", { n: results.length }) };
1410
+ }
1411
+ });
1412
+ }
1413
+
1414
+ // src/tools/task.ts
1415
+ import { z as z13 } from "zod";
1042
1416
  function createTaskTool(opts) {
1043
1417
  const byName = new Map(opts.agents.map((a) => [a.name, a]));
1044
1418
  const list = opts.agents.map((a) => `- ${a.name}: ${a.description}`).join("\n");
@@ -1046,10 +1420,10 @@ function createTaskTool(opts) {
1046
1420
  name: "task",
1047
1421
  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
1422
  ${list}`,
1049
- schema: z12.object({
1050
- agent: z12.string().describe("Agent name"),
1051
- description: z12.string().describe("3-6 word summary shown to the user"),
1052
- prompt: z12.string().describe("Full instructions; the subagent sees nothing else")
1423
+ schema: z13.object({
1424
+ agent: z13.string().describe("Agent name"),
1425
+ description: z13.string().describe("3-6 word summary shown to the user"),
1426
+ prompt: z13.string().describe("Full instructions; the subagent sees nothing else")
1053
1427
  }),
1054
1428
  kind: "read",
1055
1429
  target: (input) => `[${input.agent}] ${input.description}`,
@@ -1058,16 +1432,16 @@ ${list}`,
1058
1432
  const allowed = byName.get(input.agent)?.tools;
1059
1433
  if (!allowed) return false;
1060
1434
  const base = opts.baseTools();
1061
- return allowed.every((name) => base.find((t) => t.name === name)?.kind === "read");
1435
+ return allowed.every((name) => base.find((t2) => t2.name === name)?.kind === "read");
1062
1436
  },
1063
1437
  async run(input, ctx) {
1064
1438
  const def = byName.get(input.agent);
1065
- if (!def) return { output: `Agent "${input.agent}" tidak ada. Tersedia: ${[...byName.keys()].join(", ")}`, isError: true };
1066
- const base = opts.baseTools().filter((t) => t.name !== "task" && t.name !== "exit_plan");
1067
- const unknown = (def.tools ?? []).filter((n) => !base.some((t) => t.name === n));
1068
- if (unknown.length) return { output: `Agent "${def.name}": alat tidak dikenal: ${unknown.join(", ")}`, isError: true };
1439
+ if (!def) return { output: `Agent "${input.agent}" does not exist. Available: ${[...byName.keys()].join(", ")}`, isError: true };
1440
+ const base = opts.baseTools().filter((t2) => t2.name !== "task" && t2.name !== "exit_plan");
1441
+ const unknown = (def.tools ?? []).filter((n) => !base.some((t2) => t2.name === n));
1442
+ if (unknown.length) return { output: `Agent "${def.name}": unknown tools: ${unknown.join(", ")}`, isError: true };
1069
1443
  const allowed = def.tools;
1070
- const tools = allowed ? base.filter((t) => allowed.includes(t.name)) : base;
1444
+ const tools = allowed ? base.filter((t2) => allowed.includes(t2.name)) : base;
1071
1445
  let provider;
1072
1446
  try {
1073
1447
  provider = def.model ? opts.providerFor(def.model) : opts.provider();
@@ -1085,6 +1459,7 @@ ${def.prompt}`,
1085
1459
  cwd: opts.cwd,
1086
1460
  maxSteps: def.maxSteps ?? 50,
1087
1461
  label: def.name,
1462
+ reasoning: opts.reasoning?.() ?? "auto",
1088
1463
  // Subagent edits belong to the parent's turn so /undo reverts them too.
1089
1464
  checkpoint: ctx.checkpoint
1090
1465
  });
@@ -1098,22 +1473,22 @@ ${def.prompt}`,
1098
1473
  if (ctx.ask) child.askPermission = ctx.ask;
1099
1474
  await child.run(input.prompt, ctx.signal);
1100
1475
  ctx.addUsage?.(child.totalUsage);
1101
- const tokens = child.totalUsage.inputTokens + child.totalUsage.outputTokens;
1102
- const display = `${steps} langkah \xB7 ${tokens >= 1e3 ? `${Math.round(tokens / 1e3)}k` : tokens} token`;
1103
- if (outcome?.type === "error") return { output: `Subagent ${def.name} gagal: ${outcome.message}`, isError: true, display };
1104
- if (outcome?.type === "aborted") return { output: `Subagent ${def.name} dibatalkan.`, isError: true, display };
1476
+ const tokens2 = child.totalUsage.inputTokens + child.totalUsage.outputTokens;
1477
+ const display = t("{steps} steps \xB7 {tokens} tokens", { steps, tokens: tokens2 >= 1e3 ? `${Math.round(tokens2 / 1e3)}k` : tokens2 });
1478
+ if (outcome?.type === "error") return { output: `Subagent ${def.name} failed: ${outcome.message}`, isError: true, display };
1479
+ if (outcome?.type === "aborted") return { output: `Subagent ${def.name} was cancelled.`, isError: true, display };
1105
1480
  const last = [...child.messages].reverse().find((m) => m.role === "assistant" && m.content);
1106
- const report = last && typeof last.content === "string" ? last.content : "(tidak ada laporan)";
1481
+ const report = last && typeof last.content === "string" ? last.content : "(no report)";
1107
1482
  const suffix = outcome?.type === "stepLimit" ? `
1108
1483
 
1109
- (Subagent berhenti di batas ${def.maxSteps ?? 50} langkah.)` : "";
1484
+ (The subagent stopped at its ${def.maxSteps ?? 50}-step limit.)` : "";
1110
1485
  return { output: report + suffix, display };
1111
1486
  }
1112
1487
  });
1113
1488
  }
1114
1489
 
1115
1490
  // src/tools/todo.ts
1116
- import { z as z13 } from "zod";
1491
+ import { z as z14 } from "zod";
1117
1492
  var TodoStore = class {
1118
1493
  items = [];
1119
1494
  listeners = /* @__PURE__ */ new Set();
@@ -1130,36 +1505,47 @@ function createTodoTool(store) {
1130
1505
  return defineTool({
1131
1506
  name: "todo_write",
1132
1507
  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: z13.object({
1134
- todos: z13.array(z13.object({ content: z13.string(), status: z13.enum(["pending", "in_progress", "completed"]) }))
1508
+ schema: z14.object({
1509
+ todos: z14.array(z14.object({ content: z14.string(), status: z14.enum(["pending", "in_progress", "completed"]) }))
1135
1510
  }),
1136
1511
  kind: "read",
1137
1512
  target: (input) => `${input.todos.length} item`,
1138
1513
  async run(input) {
1139
- if (input.todos.filter((t) => t.status === "in_progress").length > 1) {
1140
- return { output: "Hanya boleh satu item in_progress.", isError: true };
1514
+ if (input.todos.filter((t2) => t2.status === "in_progress").length > 1) {
1515
+ return { output: "Only one item may be in_progress.", isError: true };
1141
1516
  }
1142
1517
  store.set(input.todos);
1143
- const done = input.todos.filter((t) => t.status === "completed").length;
1144
- return { output: `Todo diperbarui: ${done}/${input.todos.length} selesai.` };
1518
+ const done = input.todos.filter((t2) => t2.status === "completed").length;
1519
+ return { output: `Todo updated: ${done}/${input.todos.length} completed.` };
1145
1520
  }
1146
1521
  });
1147
1522
  }
1148
1523
 
1149
1524
  // src/setup.ts
1150
1525
  var TOOL_RULES = { bash: "bash", edit: "edit", write: "edit", fetch: "fetch" };
1526
+ function lunaPtcAutoEnabled(env) {
1527
+ const value = String(env.BCCLI_LUNA_PTC ?? "").trim().toLowerCase();
1528
+ return !["0", "false", "off", "no"].includes(value);
1529
+ }
1151
1530
  function createRuntime(opts) {
1152
1531
  const env = opts.env ?? process.env;
1153
1532
  const home = bccliHome(env);
1154
1533
  let config = loadConfig(opts.cwd, env);
1155
1534
  let modelRef = opts.args.model ?? config.model;
1535
+ let reasoning = opts.args.reasoning ?? config.reasoning;
1156
1536
  const makeProvider = (ref) => {
1157
1537
  const resolved = resolveModel(config, ref, env);
1158
- return opts.provider ?? createProvider({ baseURL: resolved.baseURL, apiKey: resolved.apiKey, model: resolved.model });
1538
+ return opts.provider ?? createProvider({
1539
+ baseURL: resolved.baseURL,
1540
+ apiKey: resolved.apiKey,
1541
+ model: resolved.model,
1542
+ providerId: resolved.providerId,
1543
+ enableProgrammaticToolCalling: lunaPtcAutoEnabled(env)
1544
+ });
1159
1545
  };
1160
1546
  const provider = makeProvider(modelRef);
1161
1547
  const mode = opts.args.allowAll ? "allowAll" : opts.args.permissionMode ?? config.permissionMode;
1162
- const rules = [...config.allow, ...opts.args.allowedTools.map((t) => TOOL_RULES[t] ?? t)];
1548
+ const rules = [...config.allow, ...opts.args.allowedTools.map((t2) => TOOL_RULES[t2] ?? t2)];
1163
1549
  const permissions = new Permissions(mode, rules, opts.cwd);
1164
1550
  const roots = { cwd: opts.cwd, home, userHome: opts.userHome ?? homedir() };
1165
1551
  const skills = loadSkills(roots);
@@ -1179,16 +1565,27 @@ function createRuntime(opts) {
1179
1565
  }
1180
1566
  }
1181
1567
  const systemPrompt = buildSystemPrompt({ cwd: opts.cwd, home, model: modelRef, skills });
1182
- const baseTools = [...ALL_TOOLS, createSkillTool(skills), createTodoTool(todos), createExitPlanTool({ permissions, interaction })];
1568
+ const webSearch = createWebSearchTool({
1569
+ botconnector: () => {
1570
+ try {
1571
+ const bc = resolveModel(config, "bc-cloud/search", env);
1572
+ return bc.apiKey ? { baseURL: bc.baseURL, apiKey: bc.apiKey } : void 0;
1573
+ } catch {
1574
+ return void 0;
1575
+ }
1576
+ }
1577
+ });
1578
+ const baseTools = [...ALL_TOOLS, webSearch, createSkillTool(skills), createTodoTool(todos), createExitPlanTool({ permissions, interaction })];
1183
1579
  const task = createTaskTool({
1184
1580
  agents: [...BUILTIN_AGENTS, ...agentDefs],
1185
1581
  // Subagents get whatever the main agent has right now (incl. MCP tools), minus task itself.
1186
- baseTools: () => agent.tools.filter((t) => t.name !== "task"),
1582
+ baseTools: () => agent.tools.filter((t2) => t2.name !== "task"),
1187
1583
  permissions,
1188
1584
  provider: () => agent.provider,
1189
1585
  providerFor: makeProvider,
1190
1586
  systemPrompt,
1191
- cwd: opts.cwd
1587
+ cwd: opts.cwd,
1588
+ reasoning: () => reasoning
1192
1589
  });
1193
1590
  const agent = new Agent({
1194
1591
  provider,
@@ -1197,6 +1594,7 @@ function createRuntime(opts) {
1197
1594
  systemPrompt,
1198
1595
  cwd: opts.cwd,
1199
1596
  history,
1597
+ reasoning,
1200
1598
  onMessage: (m) => session.append(m),
1201
1599
  onReset: () => session.reset(),
1202
1600
  onTurnStart: () => checkpoints.beginTurn(),
@@ -1238,6 +1636,9 @@ function createRuntime(opts) {
1238
1636
  get modelRef() {
1239
1637
  return modelRef;
1240
1638
  },
1639
+ get reasoning() {
1640
+ return reasoning;
1641
+ },
1241
1642
  agent,
1242
1643
  get session() {
1243
1644
  return session;
@@ -1267,36 +1668,59 @@ function createRuntime(opts) {
1267
1668
  setModel(ref) {
1268
1669
  agent.provider = makeProvider(ref);
1269
1670
  modelRef = ref;
1671
+ },
1672
+ setReasoning(level) {
1673
+ reasoning = level;
1674
+ agent.reasoning = level;
1270
1675
  }
1271
1676
  };
1272
1677
  }
1273
1678
 
1274
1679
  // src/version.ts
1275
- var VERSION = true ? "0.4.0-beta.3" : "0.0.0-dev";
1680
+ var VERSION = true ? "0.4.0-beta.30" : "0.0.0-dev";
1276
1681
 
1277
1682
  // src/cli.ts
1683
+ function chooseLanguage(argv) {
1684
+ let fromConfig;
1685
+ try {
1686
+ fromConfig = readGlobalConfigFile(process.env).language;
1687
+ } catch {
1688
+ }
1689
+ setLanguage(resolveLanguage(argv, process.env, fromConfig));
1690
+ }
1278
1691
  async function main() {
1692
+ chooseLanguage(process.argv.slice(2));
1279
1693
  const args = parseCliArgs(process.argv.slice(2));
1280
1694
  if (args.version) {
1281
1695
  console.log(VERSION);
1282
1696
  return 0;
1283
1697
  }
1284
1698
  if (args.help) {
1285
- console.log(HELP_TEXT);
1699
+ console.log(helpText());
1286
1700
  return 0;
1287
1701
  }
1288
1702
  const cwd = process.cwd();
1289
1703
  if (args.command === "login") return runLogin(args.loginProvider ?? "bc-cloud", cwd);
1290
1704
  if (args.command === "mcp") {
1291
- const { runMcpCommand } = await import("./mcpCli-Q2H63ISP.js");
1292
- const { readSecret } = await import("./login-KHU7ONSR.js");
1705
+ const { runMcpCommand } = await import("./mcpCli-J43FHF2H.js");
1706
+ const { readSecret } = await import("./login-HUPT4247.js");
1293
1707
  return runMcpCommand(args, { env: process.env, cwd, out: (s) => console.log(s), err: (s) => console.error(s), readSecret });
1294
1708
  }
1295
1709
  if (args.command === "provider") {
1296
- const { runProviderCommand } = await import("./providerCli-G3X7KENO.js");
1297
- const { readSecret } = await import("./login-KHU7ONSR.js");
1710
+ const { runProviderCommand } = await import("./providerCli-II7TQDYQ.js");
1711
+ const { readSecret } = await import("./login-HUPT4247.js");
1298
1712
  return runProviderCommand(args, { env: process.env, cwd, out: (s) => console.log(s), err: (s) => console.error(s), readSecret });
1299
1713
  }
1714
+ if (args.command === "connect" || args.command === "disconnect" || args.command === "integrations") {
1715
+ const { runIntegrationCommand } = await import("./integrationCli-ZTZSZLO7.js");
1716
+ return runIntegrationCommand(args, {
1717
+ env: process.env,
1718
+ cwd,
1719
+ out: (s) => console.log(s),
1720
+ err: (s) => console.error(s),
1721
+ fetch: globalThis.fetch
1722
+ });
1723
+ }
1300
1724
  if (args.command === "models") {
1301
1725
  const config = loadConfig(cwd);
1302
1726
  const resolved = resolveModel(config, args.model ?? config.model);
@@ -1310,11 +1734,11 @@ async function main() {
1310
1734
  } catch {
1311
1735
  }
1312
1736
  if (args.print) {
1313
- if (!args.prompt) throw new ConfigError('Mode -p butuh tugas, contoh: bccli -p "jelaskan repo ini"');
1314
- const { serversToStart } = await import("./config-NPSYFAFD.js");
1737
+ if (!args.prompt) throw new ConfigError(t('-p mode needs a task, e.g. bccli -p "explain this repo"'));
1738
+ const { serversToStart } = await import("./config-PBEZ3IGD.js");
1315
1739
  const plan = serversToStart(rt.home, cwd);
1316
1740
  for (const s of plan.needTrust) {
1317
- console.error(`Server MCP project "${s.name}" dilewati (belum diizinkan; jalankan bccli interaktif sekali untuk menyetujuinya).`);
1741
+ console.error(t('Project MCP server "{name}" skipped (not approved yet; run interactive bccli once to approve it).', { name: s.name }));
1318
1742
  }
1319
1743
  await rt.startMcp(plan.start);
1320
1744
  try {
@@ -1323,8 +1747,8 @@ async function main() {
1323
1747
  await rt.mcp.stop();
1324
1748
  }
1325
1749
  }
1326
- if (!process.stdin.isTTY) throw new ConfigError('Mode interaktif butuh terminal. Untuk skrip/CI pakai: bccli -p "tugas"');
1327
- const { startInteractive } = await import("./ui-A32PTF4F.js");
1750
+ if (!process.stdin.isTTY) throw new ConfigError(t('Interactive mode needs a terminal. For scripts/CI use: bccli -p "task"'));
1751
+ const { startInteractive } = await import("./ui-HO3Z5LYD.js");
1328
1752
  return startInteractive(rt, { initialPrompt: args.prompt, resume: args.resume, version: VERSION });
1329
1753
  }
1330
1754
  main().then(