@botconnector/bccli 0.4.0-beta.13 → 0.4.0-beta.15

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
@@ -8,11 +8,11 @@ import {
8
8
  loadSkills,
9
9
  parseFrontmatter,
10
10
  runPrint
11
- } from "./chunk-KV27KTOX.js";
11
+ } from "./chunk-K5J2GGXT.js";
12
12
  import {
13
- HELP_TEXT,
13
+ helpText,
14
14
  parseCliArgs
15
- } from "./chunk-TRMRDQ43.js";
15
+ } from "./chunk-6AVRFRFU.js";
16
16
  import {
17
17
  Permissions
18
18
  } from "./chunk-ELML4KVQ.js";
@@ -20,14 +20,15 @@ import {
20
20
  addProviderKey,
21
21
  createProvider,
22
22
  listAllModels
23
- } from "./chunk-U7GK73CK.js";
23
+ } from "./chunk-RY7NCJXH.js";
24
24
  import {
25
- providerName
26
- } from "./chunk-7S6JXQCH.js";
27
- import "./chunk-CLN4Z5E3.js";
25
+ providerName,
26
+ readGlobalConfigFile
27
+ } from "./chunk-D7CF42EW.js";
28
+ import "./chunk-VEZFR7M7.js";
28
29
  import {
29
30
  runLogin
30
- } from "./chunk-6BLZELE6.js";
31
+ } from "./chunk-4KMOCORM.js";
31
32
  import {
32
33
  Session,
33
34
  pruneSessions
@@ -36,8 +37,11 @@ import {
36
37
  ConfigError,
37
38
  bccliHome,
38
39
  loadConfig,
39
- resolveModel
40
- } from "./chunk-2MGINW4O.js";
40
+ resolveLanguage,
41
+ resolveModel,
42
+ setLanguage,
43
+ t
44
+ } from "./chunk-DDA5TJ7T.js";
41
45
 
42
46
  // src/textToolCalls.ts
43
47
  var PAIR = /(?:<arg_key>\s*|`)?([A-Za-z_]\w*)(?:\s*<\/arg_key>)?`?\s*(?:=\s*`|<arg_value>)([\s\S]*?)<\/arg_value>/g;
@@ -56,17 +60,17 @@ function coerce(value, type) {
56
60
  return value.replace(/^\n+|\n+$/g, "");
57
61
  }
58
62
  function pickTool(name, keys, tools) {
59
- const named = name && tools.find((t) => t.function.name === name);
63
+ const named = name && tools.find((t2) => t2.function.name === name);
60
64
  if (named) return named;
61
65
  let best;
62
66
  let bestScore = 0;
63
67
  let tie = false;
64
- for (const t of tools) {
65
- const params = t.function.parameters;
68
+ for (const t2 of tools) {
69
+ const params = t2.function.parameters;
66
70
  const props = Object.keys(params.properties ?? {});
67
71
  if (!(params.required ?? []).every((r) => keys.includes(r))) continue;
68
72
  const score = keys.filter((k) => props.includes(k)).length;
69
- if (score > bestScore) [best, bestScore, tie] = [t, score, false];
73
+ if (score > bestScore) [best, bestScore, tie] = [t2, score, false];
70
74
  else if (score === bestScore && score > 0) tie = true;
71
75
  }
72
76
  return tie ? void 0 : best;
@@ -202,7 +206,7 @@ function runCommand(command, opts) {
202
206
  clearTimeout(timer);
203
207
  opts.signal.removeEventListener("abort", onAbort);
204
208
  const output = dropped ? `${head}
205
- \u2026 [${dropped} karakter dipotong] \u2026
209
+ \u2026 [${dropped} characters truncated] \u2026
206
210
  ${tail}` : head + tail;
207
211
  resolve2({ output: output + extra, exitCode, timedOut, aborted });
208
212
  };
@@ -221,7 +225,7 @@ var bashTool = defineTool({
221
225
  target: (input) => input.command,
222
226
  async run(input, ctx) {
223
227
  const r = await runCommand(input.command, { cwd: ctx.cwd, timeoutMs: input.timeout_ms ?? 12e4, signal: ctx.signal });
224
- 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}]`;
225
229
  return { output: `${r.output.trimEnd()}
226
230
  ${status}`.trimStart(), isError: r.exitCode !== 0 };
227
231
  }
@@ -280,7 +284,7 @@ function countOccurrences(haystack, needle) {
280
284
  }
281
285
  function applyEdit(content, input) {
282
286
  if (input.old_string === input.new_string) {
283
- 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 };
284
288
  }
285
289
  const eol = content.includes("\r\n") ? "\r\n" : "\n";
286
290
  const toEol = (s) => s.replace(/\r?\n/g, eol);
@@ -291,13 +295,13 @@ function applyEdit(content, input) {
291
295
  if (count === 0) continue;
292
296
  if (count > 1 && !input.replace_all) {
293
297
  return {
294
- 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.`,
295
299
  isError: true
296
300
  };
297
301
  }
298
302
  return input.replace_all ? content.split(oldText).join(newText) : content.replace(oldText, () => newText);
299
303
  }
300
- 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 };
301
305
  }
302
306
  var editTool = defineTool({
303
307
  name: "edit",
@@ -307,12 +311,12 @@ var editTool = defineTool({
307
311
  target: (input) => input.path,
308
312
  async validate(input, ctx) {
309
313
  const abs = resolvePath(ctx.cwd, input.path);
310
- 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.`;
311
315
  let content;
312
316
  try {
313
317
  content = await readFile(abs, "utf8");
314
318
  } catch (error) {
315
- return `Tidak bisa membaca ${input.path}: ${errorCode(error)}`;
319
+ return `Cannot read ${input.path}: ${errorCode(error)}`;
316
320
  }
317
321
  const next = applyEdit(content, input);
318
322
  return typeof next === "string" ? void 0 : next.output;
@@ -326,19 +330,19 @@ var editTool = defineTool({
326
330
  async run(input, ctx) {
327
331
  const abs = resolvePath(ctx.cwd, input.path);
328
332
  if (!ctx.readFiles.has(abs)) {
329
- 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 };
330
334
  }
331
335
  let content;
332
336
  try {
333
337
  content = await readFile(abs, "utf8");
334
338
  } catch (error) {
335
- return { output: `Tidak bisa membaca ${input.path}: ${errorCode(error)}`, isError: true };
339
+ return { output: `Cannot read ${input.path}: ${errorCode(error)}`, isError: true };
336
340
  }
337
341
  const next = applyEdit(content, input);
338
342
  if (typeof next !== "string") return next;
339
343
  await ctx.checkpoint?.(abs);
340
344
  await writeFile(abs, next);
341
- return { output: `Mengedit ${input.path}.`, display: formatDiff(diffLines(content, next)) };
345
+ return { output: `Edited ${input.path}.`, display: formatDiff(diffLines(content, next)) };
342
346
  }
343
347
  });
344
348
 
@@ -375,13 +379,13 @@ function describeFetchError(error) {
375
379
  const cause = error.cause;
376
380
  const code = cause?.code ?? "";
377
381
  const detail = cause?.message ?? error.message;
378
- if (error.name === "TimeoutError") return "tidak ada jawaban dalam 30 detik";
379
- if (code === "ENOTFOUND" || code === "EAI_AGAIN") return `${detail} \u2014 DNS tidak menemukan host (salah ketik, offline, atau diblokir DNS/ISP)`;
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)`;
380
384
  if (/CERT|SELF_SIGNED|UNABLE_TO/.test(code)) {
381
- return `${detail} \u2014 sertifikat TLS ditolak; sering karena antivirus/proxy yang memeriksa HTTPS. Coba jalankan dengan NODE_OPTIONS=--use-system-ca`;
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`;
382
386
  }
383
387
  if (/ECONNRESET|ECONNREFUSED|ETIMEDOUT|EHOSTUNREACH|ENETUNREACH|UND_ERR_CONNECT_TIMEOUT|UND_ERR_SOCKET/.test(code)) {
384
- return `${detail} \u2014 koneksi gagal (firewall, proxy, atau situs diblokir jaringan)`;
388
+ return `${detail} \u2014 connection failed (firewall, proxy, or the site is blocked by the network)`;
385
389
  }
386
390
  return code ? `${detail} (${code})` : detail;
387
391
  }
@@ -415,14 +419,14 @@ function pickRelevant(text, query, count = FOCUS_CHUNKS, size = FOCUS_SIZE) {
415
419
  const q = [...new Set(words(query))];
416
420
  const docs = chunks.map(words);
417
421
  const avg = docs.reduce((n, d) => n + d.length, 0) / (docs.length || 1) || 1;
418
- const idf = q.map((t) => {
419
- const df = docs.filter((d) => d.includes(t)).length;
422
+ const idf = q.map((t2) => {
423
+ const df = docs.filter((d) => d.includes(t2)).length;
420
424
  return Math.log(1 + (docs.length - df + 0.5) / (df + 0.5));
421
425
  });
422
426
  const scored = docs.map((d, i) => ({
423
427
  i,
424
- score: q.reduce((sum, t, k) => {
425
- const tf = d.filter((w) => w === t).length;
428
+ score: q.reduce((sum, t2, k) => {
429
+ const tf = d.filter((w) => w === t2).length;
426
430
  return sum + idf[k] * tf * 2.2 / (tf + 1.2 * (0.25 + 0.75 * d.length / avg));
427
431
  }, 0)
428
432
  })).filter((c) => c.score > 0).sort((a, b) => b.score - a.score).slice(0, count).sort((a, b) => a.i - b.i);
@@ -444,7 +448,7 @@ function recall(key) {
444
448
  return hit;
445
449
  }
446
450
  var ACCEPT = "text/markdown, text/html;q=0.9, text/plain;q=0.8, */*;q=0.5";
447
- var withCost = (text, what) => `${what} \xB7 ~${tokensOf(text)} token`;
451
+ var withCost = (text, what) => `${what} \xB7 ${t("~{n} tokens", { n: tokensOf(text) })}`;
448
452
  function createFetchTool(opts = {}) {
449
453
  const keenableURL = opts.keenableURL === void 0 ? "https://api.keenable.ai/v1/fetch/public" : opts.keenableURL;
450
454
  const isPrivate = opts.isPrivate ?? isPrivateHost;
@@ -462,7 +466,7 @@ function createFetchTool(opts = {}) {
462
466
  kind: "fetch",
463
467
  target: (input) => input.url,
464
468
  async run(input, ctx) {
465
- if (!/^https?:\/\//i.test(input.url)) return { output: "URL harus diawali http:// atau https://", isError: true };
469
+ if (!/^https?:\/\//i.test(input.url)) return { output: "URL must start with http:// or https://", isError: true };
466
470
  const target = new URL(input.url);
467
471
  const prompt = input.prompt?.trim();
468
472
  const askKeenable = !!prompt && !input.offset && !!keenableURL && !target.username && !target.password && !isPrivate(target.hostname);
@@ -471,18 +475,22 @@ function createFetchTool(opts = {}) {
471
475
  ${input.url}
472
476
  ${prompt}`;
473
477
  const hit = recall(key);
474
- if (hit) return { output: hit, display: withCost(hit, "kutipan fokus \xB7 cache") };
478
+ if (hit) {
479
+ ctx.refundFetch?.();
480
+ return { output: hit, display: withCost(hit, t("focused excerpt \xB7 cache")) };
481
+ }
475
482
  const out = await extractWithKeenable(keenableURL, input.url, prompt, ctx.signal);
476
483
  if (out) {
477
- const output2 = `[Kutipan dari ${input.url} untuk "${prompt}" (diekstrak Keenable). Kalau jawabannya "tidak ada", coba fetch tanpa prompt.]
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.]
478
485
  ${out}`;
479
486
  remember(key, output2, output2.length);
480
- return { output: output2, display: withCost(output2, "kutipan fokus (Keenable)") };
487
+ return { output: output2, display: withCost(output2, t("focused excerpt (Keenable)")) };
481
488
  }
482
489
  }
483
490
  let page = recall(`p
484
491
  ${input.url}`);
485
492
  const cached = !!page;
493
+ if (cached) ctx.refundFetch?.();
486
494
  if (!page) {
487
495
  const loaded = await loadPage(input.url, ctx.signal);
488
496
  if ("error" in loaded) return { output: loaded.error, isError: true };
@@ -495,23 +503,23 @@ ${input.url}`, page, page.text.length);
495
503
  if (prompt && !input.offset) {
496
504
  const picked = pickRelevant(text, prompt);
497
505
  if (picked) {
498
- const output2 = `[Kutipan paling relevan dari ${input.url} untuk "${prompt}" (${text.length} karakter total). Halaman penuh: fetch tanpa prompt.]
506
+ const output2 = `[Most relevant excerpt from ${input.url} for "${prompt}" (${text.length} characters total). Full page: fetch without a prompt.]
499
507
  ${picked}`;
500
- return { output: output2, display: withCost(output2, `kutipan fokus (lokal)${via}`) };
508
+ return { output: output2, display: withCost(output2, `${t("focused excerpt (local)")}${via}`) };
501
509
  }
502
510
  }
503
511
  const offset = input.offset ?? 0;
504
512
  if (offset >= text.length && text.length > 0) {
505
- return { output: `offset ${offset} melewati akhir halaman (${text.length} karakter).`, isError: true };
513
+ return { output: `offset ${offset} is past the end of the page (${text.length} characters).`, isError: true };
506
514
  }
507
515
  const end = Math.min(offset + MAX_CHARS, text.length);
508
516
  let output = text.slice(offset, end);
509
517
  if (end < text.length || truncated) {
510
518
  const total = truncated ? `${text.length}+` : text.length;
511
519
  output += `
512
- \u2026 [dipotong: karakter ${offset}-${end} dari ${total}. Lanjut dengan offset=${end}, atau pakai prompt untuk kutipan terfokus.]`;
520
+ \u2026 [truncated: characters ${offset}-${end} of ${total}. Continue with offset=${end}, or use prompt for a focused excerpt.]`;
513
521
  }
514
- const size = truncated ? "lebih dari 5 MB, dipotong" : `${text.length} karakter`;
522
+ const size = truncated ? t("over 5 MB, truncated") : t("{n} characters", { n: text.length });
515
523
  return { output, display: withCost(output, `${size}${via}`) };
516
524
  }
517
525
  });
@@ -542,19 +550,19 @@ async function loadPage(inputUrl, signal) {
542
550
  const next = new URL(location, url);
543
551
  if (next.host !== url.host) {
544
552
  await res.body?.cancel();
545
- return { error: `Redirect ke host lain: ${next.href}. Panggil fetch lagi dengan URL itu kalau memang perlu (akan diminta izin).` };
553
+ return { error: `Redirect to another host: ${next.href}. Call fetch again with that URL if needed (permission will be requested).` };
546
554
  }
547
555
  await res.body?.cancel();
548
556
  url = next;
549
557
  }
550
558
  } catch (error) {
551
- return { error: `Gagal mengambil ${inputUrl}: ${describeFetchError(error)}` };
559
+ return { error: `Failed to fetch ${inputUrl}: ${describeFetchError(error)}` };
552
560
  }
553
- if (!res) return { error: `Gagal mengambil ${inputUrl}` };
561
+ if (!res) return { error: `Failed to fetch ${inputUrl}` };
554
562
  const { text: body, truncated } = await readCapped(res);
555
563
  const type = res.headers.get("content-type") ?? "";
556
564
  const text = type.includes("html") ? htmlToText(body) : body;
557
- if (!res.ok) return { error: `HTTP ${res.status} dari ${inputUrl}: ${text.slice(0, 500)}` };
565
+ if (!res.ok) return { error: `HTTP ${res.status} from ${inputUrl}: ${text.slice(0, 500)}` };
558
566
  return { text, truncated, at: Date.now() };
559
567
  }
560
568
  var fetchTool = createFetchTool();
@@ -574,10 +582,10 @@ var globTool = defineTool({
574
582
  async run(input, ctx) {
575
583
  const root = resolvePath(ctx.cwd, input.path ?? ".");
576
584
  const files = (await fg(input.pattern, { cwd: root, ignore: ignorePatterns(root), onlyFiles: true, dot: true })).sort();
577
- if (!files.length) return { output: "Tidak ada file yang cocok." };
585
+ if (!files.length) return { output: "No matching files." };
578
586
  const shown = files.slice(0, 500);
579
587
  const more = files.length > shown.length ? `
580
- \u2026 ${files.length - shown.length} file lagi` : "";
588
+ \u2026 ${files.length - shown.length} more files` : "";
581
589
  return { output: shown.join("\n") + more, display: `${files.length} file` };
582
590
  }
583
591
  });
@@ -639,11 +647,11 @@ var grepTool = defineTool({
639
647
  try {
640
648
  new RegExp(input.pattern);
641
649
  } catch (error) {
642
- return { output: `Regex tidak valid: ${error.message}`, isError: true };
650
+ return { output: `Invalid regex: ${error.message}`, isError: true };
643
651
  }
644
652
  const matches = await grepRg(input.pattern, opts) ?? await grepJs(input.pattern, opts);
645
- if (!matches.length) return { output: "Tidak ada yang cocok." };
646
- 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 }) };
647
655
  }
648
656
  });
649
657
 
@@ -667,25 +675,25 @@ var readTool = defineTool({
667
675
  try {
668
676
  buffer = await readFile3(abs);
669
677
  } catch (error) {
670
- return { output: `Tidak bisa membaca ${input.path}: ${errorCode(error)}`, isError: true };
678
+ return { output: `Cannot read ${input.path}: ${errorCode(error)}`, isError: true };
671
679
  }
672
680
  if (buffer.subarray(0, 8e3).includes(0)) {
673
- return { output: `${input.path} adalah file biner, tidak dibaca.`, isError: true };
681
+ return { output: `${input.path} is a binary file; not read.`, isError: true };
674
682
  }
675
683
  const lines = buffer.toString("utf8").split(/\r?\n/);
676
684
  const start = (input.offset ?? 1) - 1;
677
685
  const limit = input.limit ?? 2e3;
678
686
  const slice = lines.slice(start, start + limit);
679
687
  const body = slice.map((line, i) => {
680
- 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;
681
689
  return `${String(start + i + 1).padStart(6)} ${text}`;
682
690
  }).join("\n");
683
691
  const remaining = lines.length - start - slice.length;
684
692
  ctx.readFiles.add(abs);
685
693
  return {
686
694
  output: remaining > 0 ? `${body}
687
- \u2026 ${remaining} baris lagi (pakai offset)` : body,
688
- display: `${slice.length} baris`
695
+ \u2026 ${remaining} more lines (use offset)` : body,
696
+ display: t("{n} lines", { n: slice.length })
689
697
  };
690
698
  }
691
699
  });
@@ -706,7 +714,7 @@ var writeTool = defineTool({
706
714
  target: (input) => input.path,
707
715
  async validate(input, ctx) {
708
716
  const abs = resolvePath(ctx.cwd, input.path);
709
- 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;
710
718
  },
711
719
  async preview(input, ctx) {
712
720
  const abs = resolvePath(ctx.cwd, input.path);
@@ -716,18 +724,18 @@ var writeTool = defineTool({
716
724
  async run(input, ctx) {
717
725
  const abs = resolvePath(ctx.cwd, input.path);
718
726
  if (existsSync(abs) && !ctx.readFiles.has(abs)) {
719
- 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 };
720
728
  }
721
729
  try {
722
730
  await ctx.checkpoint?.(abs);
723
731
  await mkdir(dirname(abs), { recursive: true });
724
732
  await writeFile2(abs, input.content);
725
733
  } catch (error) {
726
- return { output: `Gagal menulis ${input.path}: ${errorCode(error)}`, isError: true };
734
+ return { output: `Failed to write ${input.path}: ${errorCode(error)}`, isError: true };
727
735
  }
728
736
  ctx.readFiles.add(abs);
729
737
  const lines = input.content.split("\n").length;
730
- 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 }) };
731
739
  }
732
740
  });
733
741
 
@@ -743,7 +751,7 @@ function toolDefinitions(tools) {
743
751
  // src/webBudget.ts
744
752
  var KEEP_TOKENS = 4e4;
745
753
  var PRUNE_MIN_TOKENS = 2e4;
746
- var STUB = "[hasil tool lama dihapus:";
754
+ var STUB = "[old tool result removed:";
747
755
  var tokens = (text) => Math.ceil(text.length / 4);
748
756
  var isStub = (content) => content.startsWith(STUB);
749
757
  function pruneOldFetches(messages) {
@@ -774,7 +782,7 @@ function pruneOldFetches(messages) {
774
782
  if (oldTokens <= PRUNE_MIN_TOKENS) return false;
775
783
  for (const i of old) {
776
784
  const m = messages[i];
777
- messages[i] = { ...m, content: `${STUB} fetch ${urls.get(m.tool_call_id)}. Ambil lagi dengan fetch (pakai prompt untuk kutipan terfokus) kalau masih perlu.]` };
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.]` };
778
786
  }
779
787
  return true;
780
788
  }
@@ -790,6 +798,10 @@ var WebBudget = class {
790
798
  startTurn() {
791
799
  this.fetches = 0;
792
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
+ }
793
805
  reset() {
794
806
  this.fetches = 0;
795
807
  this.chars = 0;
@@ -797,10 +809,10 @@ var WebBudget = class {
797
809
  /** Reserves one fetch; returns the refusal to show the model when over budget. */
798
810
  take() {
799
811
  if (this.fetches >= this.perTurn) {
800
- return `Batas ${this.perTurn} fetch per pertanyaan tercapai (tiap fetch memakai token user). Rangkum jawaban dari hasil yang sudah ada; kalau memang kurang, jelaskan ke user apa yang belum ketemu.`;
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.`;
801
813
  }
802
814
  if (this.chars >= this.sessionChars) {
803
- return `Batas total halaman web sesi ini tercapai (~${Math.round(this.sessionChars / 4e3)}k token). Rangkum dari hasil yang sudah ada dan tanyakan ke user apakah perlu mencari lebih banyak.`;
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.`;
804
816
  }
805
817
  this.fetches++;
806
818
  return void 0;
@@ -857,7 +869,7 @@ var Agent = class {
857
869
  this.definitions = toolDefinitions(tools);
858
870
  }
859
871
  isParallelSafe(call) {
860
- const tool = this.turnTools.find((t) => t.name === call.name);
872
+ const tool = this.turnTools.find((t2) => t2.name === call.name);
861
873
  if (!tool?.parallelSafe) return false;
862
874
  try {
863
875
  const parsed = tool.schema.safeParse(JSON.parse(call.arguments || "{}"));
@@ -893,9 +905,9 @@ var Agent = class {
893
905
  reasoning: this.reasoning
894
906
  });
895
907
  this.clear();
896
- this.push({ role: "user", content: `Ringkasan percakapan sebelumnya:
908
+ this.push({ role: "user", content: `Summary of the previous conversation:
897
909
  ${completion.text}` });
898
- this.push({ role: "assistant", content: "Oke, saya lanjutkan dari ringkasan ini." });
910
+ this.push({ role: "assistant", content: "OK, I will continue from this summary." });
899
911
  this.onEvent({ type: "compacted" });
900
912
  }
901
913
  async run(text, signal) {
@@ -909,7 +921,7 @@ ${completion.text}` });
909
921
  for (let step = 0; step < this.maxSteps; step++) {
910
922
  if (step > 0 && this.lastInputTokens > this.contextWindow * 0.8) {
911
923
  await this.compact(signal);
912
- 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}` });
913
925
  }
914
926
  pruneOldFetches(this.messages);
915
927
  let completion = await this.provider.chat({
@@ -950,7 +962,7 @@ ${completion.text}` });
950
962
  this.onEvent({ type: "textReplace", text: completion.text });
951
963
  this.onEvent({
952
964
  type: "error",
953
- 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.")
954
966
  });
955
967
  return;
956
968
  }
@@ -962,7 +974,7 @@ ${completion.text}` });
962
974
  let i = 0;
963
975
  while (i < calls.length) {
964
976
  if (signal.aborted) {
965
- 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." });
966
978
  this.onEvent({ type: "aborted" });
967
979
  return;
968
980
  }
@@ -991,7 +1003,7 @@ ${completion.text}` });
991
1003
  this.onEvent({
992
1004
  type: "error",
993
1005
  message: noTools ? `${message}
994
- 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
995
1007
  });
996
1008
  }
997
1009
  }
@@ -1001,17 +1013,17 @@ Model ini sepertinya tidak mendukung tool calling. Coba model lain dengan /model
1001
1013
  this.onEvent({ type: "toolEnd", id: call.id, tool: call.name, output, isError: true });
1002
1014
  return output;
1003
1015
  };
1004
- const tool = this.turnTools.find((t) => t.name === call.name);
1005
- 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(", ")}`);
1006
1018
  let args;
1007
1019
  try {
1008
1020
  args = JSON.parse(call.arguments || "{}");
1009
1021
  } catch {
1010
- return fail(`Argumen bukan JSON valid: ${call.arguments.slice(0, 200)}`);
1022
+ return fail(`Arguments are not valid JSON: ${call.arguments.slice(0, 200)}`);
1011
1023
  }
1012
1024
  const parsed = tool.schema.safeParse(args);
1013
1025
  if (!parsed.success) {
1014
- 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("; ")}`);
1015
1027
  }
1016
1028
  const input = parsed.data;
1017
1029
  const target = tool.target(input);
@@ -1025,6 +1037,7 @@ Model ini sepertinya tidak mendukung tool calling. Coba model lain dengan /model
1025
1037
  emit: (event) => this.onEvent(event),
1026
1038
  ask: (req) => this.askPermission(req),
1027
1039
  checkpoint: this.opts.checkpoint,
1040
+ refundFetch: () => this.webBudget.refund(),
1028
1041
  addUsage: (u) => {
1029
1042
  this.totalUsage.inputTokens += u.inputTokens;
1030
1043
  this.totalUsage.outputTokens += u.outputTokens;
@@ -1052,7 +1065,7 @@ Model ini sepertinya tidak mendukung tool calling. Coba model lain dengan /model
1052
1065
  decision = answer === "no" ? "deny" : "allow";
1053
1066
  }
1054
1067
  if (decision === "deny") {
1055
- 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.";
1056
1069
  this.onEvent({ type: "toolEnd", id: call.id, tool: tool.name, output, isError: true });
1057
1070
  return output;
1058
1071
  }
@@ -1092,7 +1105,7 @@ var CheckpointStore = class {
1092
1105
  while (this.turns() > MAX_TURNS) this.history.shift();
1093
1106
  }
1094
1107
  turns() {
1095
- return this.history.filter((t) => t.size).length;
1108
+ return this.history.filter((t2) => t2.size).length;
1096
1109
  }
1097
1110
  async undo() {
1098
1111
  while (this.history.length && !this.history.at(-1)?.size) this.history.pop();
@@ -1138,7 +1151,7 @@ function mcpToolName(server, tool, taken) {
1138
1151
  function withTimeout(promise, ms) {
1139
1152
  let timer;
1140
1153
  const timeout = new Promise((_, reject) => {
1141
- 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);
1142
1155
  });
1143
1156
  return Promise.race([promise, timeout]).finally(() => clearTimeout(timer));
1144
1157
  }
@@ -1175,7 +1188,7 @@ var McpManager = class {
1175
1188
  });
1176
1189
  return;
1177
1190
  }
1178
- entry.tools = tools.map((t) => this.wrap(spec.name, client, t));
1191
+ entry.tools = tools.map((t2) => this.wrap(spec.name, client, t2));
1179
1192
  entry.state = { ...entry.state, status: "ready", tools: entry.tools.length };
1180
1193
  } catch (error) {
1181
1194
  entry.state = { ...entry.state, status: "error", error: error.message };
@@ -1199,9 +1212,9 @@ var McpManager = class {
1199
1212
  async run(input, ctx) {
1200
1213
  try {
1201
1214
  const result = await client.callTool({ name: tool.name, arguments: input }, void 0, { signal: ctx.signal });
1202
- 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)";
1203
1216
  return { output: output.length > 5e4 ? `${output.slice(0, 5e4)}
1204
- \u2026 [dipotong]` : output, isError: !!result.isError };
1217
+ \u2026 [truncated]` : output, isError: !!result.isError };
1205
1218
  } catch (error) {
1206
1219
  return { output: `Server MCP ${server} error: ${error.message}`, isError: true };
1207
1220
  }
@@ -1212,8 +1225,8 @@ var McpManager = class {
1212
1225
  const entry = this.servers.get(name);
1213
1226
  if (!entry) return;
1214
1227
  this.servers.delete(name);
1215
- for (const t of entry.tools) this.taken.delete(t.name);
1216
- 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));
1217
1230
  await entry.client?.close().catch(() => {
1218
1231
  });
1219
1232
  this.opts.onChange?.();
@@ -1240,15 +1253,15 @@ function createExitPlanTool(opts) {
1240
1253
  description: "Plan mode only: present the finished plan to the user for approval. If approved you may start implementing it.",
1241
1254
  schema: z10.object({ plan: z10.string().describe("The plan, in markdown") }),
1242
1255
  kind: "read",
1243
- target: () => "rencana",
1256
+ target: () => t("plan"),
1244
1257
  async run(input) {
1245
- 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 };
1246
1259
  const decision = await opts.interaction.approvePlan(input.plan);
1247
1260
  if (decision === "no") {
1248
- 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." };
1249
1262
  }
1250
1263
  opts.permissions.mode = decision;
1251
- 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.` };
1252
1265
  }
1253
1266
  });
1254
1267
  }
@@ -1265,7 +1278,7 @@ function createSkillTool(skills) {
1265
1278
  target: (input) => input.name,
1266
1279
  async run(input) {
1267
1280
  const skill = skills.find((s) => s.name === input.name);
1268
- 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 };
1269
1282
  const { body } = parseFrontmatter(readFileSync(skill.file, "utf8"));
1270
1283
  return { output: `Base directory for this skill: ${skill.dir}
1271
1284
 
@@ -1325,12 +1338,12 @@ function createWebSearchTool(opts) {
1325
1338
  }
1326
1339
  }
1327
1340
  if (!results.length) {
1328
- return { output: errors.length ? `Web search gagal (${errors.join("; ")}).` : "Tidak ada hasil.", isError: errors.length > 0 };
1341
+ return { output: errors.length ? `Web search failed (${errors.join("; ")}).` : "No results.", isError: errors.length > 0 };
1329
1342
  }
1330
1343
  const output = results.map((r, i) => `${i + 1}. ${r.title}
1331
1344
  ${r.url}${r.snippet ? `
1332
1345
  ${r.snippet}` : ""}`).join("\n");
1333
- return { output, display: `${results.length} hasil` };
1346
+ return { output, display: t("{n} results", { n: results.length }) };
1334
1347
  }
1335
1348
  });
1336
1349
  }
@@ -1356,16 +1369,16 @@ ${list}`,
1356
1369
  const allowed = byName.get(input.agent)?.tools;
1357
1370
  if (!allowed) return false;
1358
1371
  const base = opts.baseTools();
1359
- 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");
1360
1373
  },
1361
1374
  async run(input, ctx) {
1362
1375
  const def = byName.get(input.agent);
1363
- if (!def) return { output: `Agent "${input.agent}" tidak ada. Tersedia: ${[...byName.keys()].join(", ")}`, isError: true };
1364
- const base = opts.baseTools().filter((t) => t.name !== "task" && t.name !== "exit_plan");
1365
- const unknown = (def.tools ?? []).filter((n) => !base.some((t) => t.name === n));
1366
- 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 };
1367
1380
  const allowed = def.tools;
1368
- const tools = allowed ? base.filter((t) => allowed.includes(t.name)) : base;
1381
+ const tools = allowed ? base.filter((t2) => allowed.includes(t2.name)) : base;
1369
1382
  let provider;
1370
1383
  try {
1371
1384
  provider = def.model ? opts.providerFor(def.model) : opts.provider();
@@ -1398,14 +1411,14 @@ ${def.prompt}`,
1398
1411
  await child.run(input.prompt, ctx.signal);
1399
1412
  ctx.addUsage?.(child.totalUsage);
1400
1413
  const tokens2 = child.totalUsage.inputTokens + child.totalUsage.outputTokens;
1401
- const display = `${steps} langkah \xB7 ${tokens2 >= 1e3 ? `${Math.round(tokens2 / 1e3)}k` : tokens2} token`;
1402
- if (outcome?.type === "error") return { output: `Subagent ${def.name} gagal: ${outcome.message}`, isError: true, display };
1403
- if (outcome?.type === "aborted") return { output: `Subagent ${def.name} dibatalkan.`, isError: true, display };
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 };
1404
1417
  const last = [...child.messages].reverse().find((m) => m.role === "assistant" && m.content);
1405
- const report = last && typeof last.content === "string" ? last.content : "(tidak ada laporan)";
1418
+ const report = last && typeof last.content === "string" ? last.content : "(no report)";
1406
1419
  const suffix = outcome?.type === "stepLimit" ? `
1407
1420
 
1408
- (Subagent berhenti di batas ${def.maxSteps ?? 50} langkah.)` : "";
1421
+ (The subagent stopped at its ${def.maxSteps ?? 50}-step limit.)` : "";
1409
1422
  return { output: report + suffix, display };
1410
1423
  }
1411
1424
  });
@@ -1435,12 +1448,12 @@ function createTodoTool(store) {
1435
1448
  kind: "read",
1436
1449
  target: (input) => `${input.todos.length} item`,
1437
1450
  async run(input) {
1438
- if (input.todos.filter((t) => t.status === "in_progress").length > 1) {
1439
- 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 };
1440
1453
  }
1441
1454
  store.set(input.todos);
1442
- const done = input.todos.filter((t) => t.status === "completed").length;
1443
- 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.` };
1444
1457
  }
1445
1458
  });
1446
1459
  }
@@ -1459,7 +1472,7 @@ function createRuntime(opts) {
1459
1472
  };
1460
1473
  const provider = makeProvider(modelRef);
1461
1474
  const mode = opts.args.allowAll ? "allowAll" : opts.args.permissionMode ?? config.permissionMode;
1462
- 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)];
1463
1476
  const permissions = new Permissions(mode, rules, opts.cwd);
1464
1477
  const roots = { cwd: opts.cwd, home, userHome: opts.userHome ?? homedir() };
1465
1478
  const skills = loadSkills(roots);
@@ -1493,7 +1506,7 @@ function createRuntime(opts) {
1493
1506
  const task = createTaskTool({
1494
1507
  agents: [...BUILTIN_AGENTS, ...agentDefs],
1495
1508
  // Subagents get whatever the main agent has right now (incl. MCP tools), minus task itself.
1496
- baseTools: () => agent.tools.filter((t) => t.name !== "task"),
1509
+ baseTools: () => agent.tools.filter((t2) => t2.name !== "task"),
1497
1510
  permissions,
1498
1511
  provider: () => agent.provider,
1499
1512
  providerFor: makeProvider,
@@ -1591,33 +1604,42 @@ function createRuntime(opts) {
1591
1604
  }
1592
1605
 
1593
1606
  // src/version.ts
1594
- var VERSION = true ? "0.4.0-beta.13" : "0.0.0-dev";
1607
+ var VERSION = true ? "0.4.0-beta.15" : "0.0.0-dev";
1595
1608
 
1596
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
+ }
1597
1618
  async function main() {
1619
+ chooseLanguage(process.argv.slice(2));
1598
1620
  const args = parseCliArgs(process.argv.slice(2));
1599
1621
  if (args.version) {
1600
1622
  console.log(VERSION);
1601
1623
  return 0;
1602
1624
  }
1603
1625
  if (args.help) {
1604
- console.log(HELP_TEXT);
1626
+ console.log(helpText());
1605
1627
  return 0;
1606
1628
  }
1607
1629
  const cwd = process.cwd();
1608
1630
  if (args.command === "login") return runLogin(args.loginProvider ?? "bc-cloud", cwd);
1609
1631
  if (args.command === "mcp") {
1610
- const { runMcpCommand } = await import("./mcpCli-DZWQPTOS.js");
1611
- const { readSecret } = await import("./login-N2CJC6RU.js");
1632
+ const { runMcpCommand } = await import("./mcpCli-QJE7P76I.js");
1633
+ const { readSecret } = await import("./login-O2BQX6LZ.js");
1612
1634
  return runMcpCommand(args, { env: process.env, cwd, out: (s) => console.log(s), err: (s) => console.error(s), readSecret });
1613
1635
  }
1614
1636
  if (args.command === "provider") {
1615
- const { runProviderCommand } = await import("./providerCli-Q7LRM2NP.js");
1616
- const { readSecret } = await import("./login-N2CJC6RU.js");
1637
+ const { runProviderCommand } = await import("./providerCli-3FJAUH7C.js");
1638
+ const { readSecret } = await import("./login-O2BQX6LZ.js");
1617
1639
  return runProviderCommand(args, { env: process.env, cwd, out: (s) => console.log(s), err: (s) => console.error(s), readSecret });
1618
1640
  }
1619
1641
  if (args.command === "connect" || args.command === "disconnect" || args.command === "integrations") {
1620
- const { runIntegrationCommand } = await import("./integrationCli-3CGHHTM2.js");
1642
+ const { runIntegrationCommand } = await import("./integrationCli-RQO3HHQE.js");
1621
1643
  return runIntegrationCommand(args, {
1622
1644
  env: process.env,
1623
1645
  cwd,
@@ -1639,11 +1661,11 @@ async function main() {
1639
1661
  } catch {
1640
1662
  }
1641
1663
  if (args.print) {
1642
- if (!args.prompt) throw new ConfigError('Mode -p butuh tugas, contoh: bccli -p "jelaskan repo ini"');
1643
- const { serversToStart } = await import("./config-YUNKPCNB.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-CGYYFECG.js");
1644
1666
  const plan = serversToStart(rt.home, cwd);
1645
1667
  for (const s of plan.needTrust) {
1646
- 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 }));
1647
1669
  }
1648
1670
  await rt.startMcp(plan.start);
1649
1671
  try {
@@ -1652,8 +1674,8 @@ async function main() {
1652
1674
  await rt.mcp.stop();
1653
1675
  }
1654
1676
  }
1655
- if (!process.stdin.isTTY) throw new ConfigError('Mode interaktif butuh terminal. Untuk skrip/CI pakai: bccli -p "tugas"');
1656
- const { startInteractive } = await import("./ui-JD5YWGPE.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-HVH3TR3I.js");
1657
1679
  return startInteractive(rt, { initialPrompt: args.prompt, resume: args.resume, version: VERSION });
1658
1680
  }
1659
1681
  main().then(