@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/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-OSWQPSA4.js";
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
- providerName
29
- } from "./chunk-U7DLMVPZ.js";
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
- resolveModel
39
- } from "./chunk-RZUJYAQV.js";
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} karakter dipotong] \u2026
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 ? `[dihentikan: timeout ${Math.round((input.timeout_ms ?? 12e4) / 1e3)} detik]` : r.aborted ? "[dibatalkan user]" : `[exit code ${r.exitCode}]`;
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 dan new_string sama; tidak ada perubahan.", isError: true };
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 muncul ${count} kali di ${input.path}. Tambah konteks agar unik, atau pakai replace_all.`,
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 tidak ditemukan di ${input.path}. Baca ulang file-nya.`, isError: true };
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 `Baca ${input.path} dulu dengan read sebelum mengedit.`;
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 `Tidak bisa membaca ${input.path}: ${errorCode(error)}`;
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: `Baca ${input.path} dulu dengan read sebelum mengedit.`, isError: true };
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: `Tidak bisa membaca ${input.path}: ${errorCode(error)}`, isError: true };
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: `Mengedit ${input.path}.`, display: formatDiff(diffLines(content, next)) };
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 = 5e4;
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(/&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
377
  }
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
- };
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
- url = next;
553
+ return { error: `Redirect to another host: ${next.href}. Call fetch again with that URL if needed (permission will be requested).` };
328
554
  }
329
- } catch (error) {
330
- return { output: `Gagal mengambil ${input.url}: ${error.message}`, isError: true };
555
+ await res.body?.cancel();
556
+ url = next;
331
557
  }
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 };
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: "Tidak ada file yang cocok." };
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} file lagi` : "";
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: `Regex tidak valid: ${error.message}`, isError: true };
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: "Tidak ada yang cocok." };
427
- return { output: matches.join("\n"), display: `${matches.length} hasil` };
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: `Tidak bisa membaca ${input.path}: ${errorCode(error)}`, isError: true };
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} adalah file biner, tidak dibaca.`, isError: true };
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 [baris dipotong]` : line;
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} baris lagi (pakai offset)` : body,
469
- display: `${slice.length} baris`
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} sudah ada. Baca dulu dengan read sebelum menimpanya.` : void 0;
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} sudah ada. Baca dulu dengan read sebelum menimpanya.`, isError: true };
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: `Gagal menulis ${input.path}: ${errorCode(error)}`, isError: true };
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: `Menulis ${input.path} (${lines} baris).`, display: `${lines} baris ditulis` };
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((t) => t.name === call.name);
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: `Ringkasan percakapan sebelumnya:
908
+ this.push({ role: "user", content: `Summary of the previous conversation:
602
909
  ${completion.text}` });
603
- this.push({ role: "assistant", content: "Oke, saya lanjutkan dari ringkasan ini." });
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: `Lanjutkan tugas ini sesuai ringkasan di atas: ${text}` });
924
+ this.push({ role: "user", content: `Continue this task using the summary above: ${text}` });
617
925
  }
618
- const completion = await this.provider.chat({
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: "Model terjebak mengulang teks, jawaban dihentikan. Coba ulangi atau ganti model dengan /model."
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: "Dibatalkan oleh user." });
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
- Model ini sepertinya tidak mendukung tool calling. Coba model lain dengan /model.` : message
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((t) => t.name === call.name);
699
- if (!tool) return fail(`Alat "${call.name}" tidak ada. Alat yang tersedia: ${this.turnTools.map((t) => t.name).join(", ")}`);
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(`Argumen bukan JSON valid: ${call.arguments.slice(0, 200)}`);
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(`Argumen tidak valid: ${parsed.error.issues.map((i) => `${i.path.join(".") || "(root)"}: ${i.message}`).join("; ")}`);
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" ? "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.";
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((t) => t.size).length;
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 clean = (s) => s.replace(/[^A-Za-z0-9_-]+/g, "_");
823
- const base = `mcp__${clean(server)}__${clean(tool)}`.slice(0, 64);
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(`timeout ${Math.round(ms / 1e3)} detik`)), ms);
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((t) => this.wrap(spec.name, client, t));
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") || "(kosong)";
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 [dipotong]` : output, isError: !!result.isError };
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 t of entry.tools) this.taken.delete(t.name);
907
- if (entry.tools.length) this.opts.onToolsRemoved?.(entry.tools.map((t) => t.name));
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: () => "rencana",
1256
+ target: () => t("plan"),
935
1257
  async run(input) {
936
- if (opts.permissions.mode !== "plan") return { output: "exit_plan hanya dipakai di mode plan.", isError: true };
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: "User belum menyetujui. Tanyakan apa yang perlu diperbaiki, perbaiki rencananya, lalu panggil exit_plan lagi." };
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: `Rencana disetujui. Mode sekarang ${decision}. Mulai kerjakan rencananya sekarang.` };
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}" tidak ada. Tersedia: ${skills.map((s) => s.name).join(", ") || "(tidak ada)"}`, isError: true };
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/task.ts
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: z12.object({
978
- agent: z12.string().describe("Agent name"),
979
- description: z12.string().describe("3-6 word summary shown to the user"),
980
- prompt: z12.string().describe("Full instructions; the subagent sees nothing else")
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((t) => t.name === name)?.kind === "read");
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}" tidak ada. Tersedia: ${[...byName.keys()].join(", ")}`, isError: true };
994
- const base = opts.baseTools().filter((t) => t.name !== "task" && t.name !== "exit_plan");
995
- const unknown = (def.tools ?? []).filter((n) => !base.some((t) => t.name === n));
996
- if (unknown.length) return { output: `Agent "${def.name}": alat tidak dikenal: ${unknown.join(", ")}`, isError: true };
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((t) => allowed.includes(t.name)) : base;
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 tokens = child.totalUsage.inputTokens + child.totalUsage.outputTokens;
1030
- const display = `${steps} langkah \xB7 ${tokens >= 1e3 ? `${Math.round(tokens / 1e3)}k` : tokens} token`;
1031
- if (outcome?.type === "error") return { output: `Subagent ${def.name} gagal: ${outcome.message}`, isError: true, display };
1032
- if (outcome?.type === "aborted") return { output: `Subagent ${def.name} dibatalkan.`, isError: true, display };
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 : "(tidak ada laporan)";
1418
+ const report = last && typeof last.content === "string" ? last.content : "(no report)";
1035
1419
  const suffix = outcome?.type === "stepLimit" ? `
1036
1420
 
1037
- (Subagent berhenti di batas ${def.maxSteps ?? 50} langkah.)` : "";
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 z13 } from "zod";
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: z13.object({
1062
- todos: z13.array(z13.object({ content: z13.string(), status: z13.enum(["pending", "in_progress", "completed"]) }))
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((t) => t.status === "in_progress").length > 1) {
1068
- return { output: "Hanya boleh satu item in_progress.", isError: true };
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((t) => t.status === "completed").length;
1072
- return { output: `Todo diperbarui: ${done}/${input.todos.length} selesai.` };
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((t) => TOOL_RULES[t] ?? t)];
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 baseTools = [...ALL_TOOLS, createSkillTool(skills), createTodoTool(todos), createExitPlanTool({ permissions, interaction })];
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((t) => t.name !== "task"),
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.2" : "0.0.0-dev";
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(HELP_TEXT);
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-Q2H63ISP.js");
1220
- const { readSecret } = await import("./login-KHU7ONSR.js");
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-G3X7KENO.js");
1225
- const { readSecret } = await import("./login-KHU7ONSR.js");
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('Mode -p butuh tugas, contoh: bccli -p "jelaskan repo ini"');
1242
- const { serversToStart } = await import("./config-NPSYFAFD.js");
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(`Server MCP project "${s.name}" dilewati (belum diizinkan; jalankan bccli interaktif sekali untuk menyetujuinya).`);
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('Mode interaktif butuh terminal. Untuk skrip/CI pakai: bccli -p "tugas"');
1255
- const { startInteractive } = await import("./ui-A32PTF4F.js");
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(