@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.
@@ -5,7 +5,7 @@ import {
5
5
  instructionPaths,
6
6
  parseFrontmatter,
7
7
  toolLabel
8
- } from "./chunk-KV27KTOX.js";
8
+ } from "./chunk-K5J2GGXT.js";
9
9
  import {
10
10
  nextMode
11
11
  } from "./chunk-ELML4KVQ.js";
@@ -13,30 +13,35 @@ import {
13
13
  hasKey,
14
14
  providerName,
15
15
  writeGlobalConfig
16
- } from "./chunk-7S6JXQCH.js";
16
+ } from "./chunk-D7CF42EW.js";
17
17
  import {
18
18
  REASONING_LEVELS,
19
19
  assertReasoningSupported,
20
20
  parseReasoningLevel,
21
21
  supportedReasoningLevels
22
- } from "./chunk-CLN4Z5E3.js";
22
+ } from "./chunk-VEZFR7M7.js";
23
23
  import {
24
24
  Session
25
25
  } from "./chunk-SJH3YLW4.js";
26
26
  import {
27
27
  CATALOG,
28
28
  fillTemplate
29
- } from "./chunk-DX7CZTBB.js";
29
+ } from "./chunk-RQGMYLVH.js";
30
30
  import {
31
31
  addGlobalServer,
32
32
  globalMcpPath,
33
33
  readMcpFile,
34
34
  removeGlobalServer
35
- } from "./chunk-BFZXRKW5.js";
35
+ } from "./chunk-ERVDVPKJ.js";
36
36
  import {
37
37
  PRESETS,
38
- removeCredential
39
- } from "./chunk-2MGINW4O.js";
38
+ getLanguage,
39
+ locale,
40
+ parseLang,
41
+ removeCredential,
42
+ setLanguage,
43
+ t
44
+ } from "./chunk-DDA5TJ7T.js";
40
45
 
41
46
  // src/ui/index.tsx
42
47
  import { render } from "ink";
@@ -51,31 +56,32 @@ import { useCallback, useEffect as useEffect2, useRef, useState as useState8 } f
51
56
 
52
57
  // src/commands.ts
53
58
  var SLASH_COMMANDS = [
54
- { name: "help", description: "daftar perintah dan pintasan" },
55
- { name: "model", description: "ganti model" },
56
- { name: "reasoning", description: "atur reasoning: auto/off/low/medium/high/max" },
57
- { name: "provider", description: "tambah/pilih provider AI" },
58
- { name: "mcp", description: "pasang/kelola server MCP" },
59
- { name: "new", description: "sesi baru (sesi lama tetap tersimpan)" },
60
- { name: "resume", description: "lanjutkan sesi lain di folder ini" },
61
- { name: "session", description: "info sesi ini" },
62
- { name: "status", description: "versi, model, mode izin, MCP, konteks" },
63
- { name: "permissions", description: "lihat/cabut izin" },
64
- { name: "undo", description: "batalkan edit file giliran terakhir" },
65
- { name: "diff", description: "git diff project" },
66
- { name: "copy", description: "salin jawaban terakhir" },
67
- { name: "export", description: "simpan percakapan ke markdown" },
68
- { name: "memory", description: "instruksi project (/memory <teks>, /memory global <teks>)" },
69
- { name: "init", description: "buat/perbarui AGENTS.md untuk project ini" },
70
- { name: "agents", description: "daftar subagent" },
71
- { name: "skills", description: "daftar skill dan perintah custom" },
72
- { name: "doctor", description: "cek kesehatan instalasi" },
73
- { name: "login", description: "simpan API key provider aktif" },
74
- { name: "logout", description: "hapus API key provider aktif" },
75
- { name: "clear", description: "mulai percakapan baru" },
76
- { name: "compact", description: "ringkas percakapan" },
77
- { name: "cost", description: "pemakaian token sesi ini" },
78
- { name: "exit", description: "keluar" }
59
+ { name: "help", description: "list of commands and shortcuts" },
60
+ { name: "model", description: "change model" },
61
+ { name: "reasoning", description: "set reasoning: auto/off/low/medium/high/max" },
62
+ { name: "provider", description: "add/choose an AI provider" },
63
+ { name: "mcp", description: "install/manage MCP servers" },
64
+ { name: "new", description: "new session (the old one stays saved)" },
65
+ { name: "resume", description: "continue another session in this folder" },
66
+ { name: "session", description: "info about this session" },
67
+ { name: "status", description: "version, model, permission mode, MCP, context" },
68
+ { name: "permissions", description: "view/revoke permissions" },
69
+ { name: "undo", description: "undo the file edits of the last turn" },
70
+ { name: "diff", description: "git diff of the project" },
71
+ { name: "copy", description: "copy the last answer" },
72
+ { name: "export", description: "save the conversation as markdown" },
73
+ { name: "memory", description: "project instructions (/memory <text>, /memory global <text>)" },
74
+ { name: "init", description: "create/update AGENTS.md for this project" },
75
+ { name: "agents", description: "list subagents" },
76
+ { name: "skills", description: "list skills and custom commands" },
77
+ { name: "doctor", description: "check installation health" },
78
+ { name: "login", description: "save the API key of the current provider" },
79
+ { name: "logout", description: "delete the API key of the current provider" },
80
+ { name: "language", description: "set the interface language: en/id" },
81
+ { name: "clear", description: "start a new conversation" },
82
+ { name: "compact", description: "summarize the conversation" },
83
+ { name: "cost", description: "token usage of this session" },
84
+ { name: "exit", description: "quit" }
79
85
  ];
80
86
  function parseSlash(text) {
81
87
  if (!text.startsWith("/")) return void 0;
@@ -109,8 +115,8 @@ function git(cwd, args, bin = "git") {
109
115
  }
110
116
  async function gitDiff(cwd, maxLines = 400, bin = "git") {
111
117
  const inside = await git(cwd, ["rev-parse", "--is-inside-work-tree"], bin);
112
- if (inside.missing) return "git tidak terpasang atau tidak ada di PATH, jadi /diff tidak bisa dipakai.";
113
- if (!inside.ok || inside.out.trim() !== "true") return "Folder ini bukan repository git, jadi tidak ada diff.";
118
+ if (inside.missing) return t("git is not installed or not on PATH, so /diff cannot be used.");
119
+ if (!inside.ok || inside.out.trim() !== "true") return t("This folder is not a git repository, so there is no diff.");
114
120
  const base = (await git(cwd, ["rev-parse", "--verify", "-q", "HEAD"])).ok ? "HEAD" : "4b825dc642cb6eb9a060e54bf8d69288fbee4904";
115
121
  const [stat, diff, untracked] = await Promise.all([
116
122
  git(cwd, ["diff", "--stat", base]),
@@ -118,23 +124,23 @@ async function gitDiff(cwd, maxLines = 400, bin = "git") {
118
124
  git(cwd, ["ls-files", "--others", "--exclude-standard"])
119
125
  ]);
120
126
  const fresh = untracked.out.split("\n").filter(Boolean);
121
- if (!diff.out.trim() && !fresh.length) return "Tidak ada perubahan yang belum di-commit.";
127
+ if (!diff.out.trim() && !fresh.length) return t("No uncommitted changes.");
122
128
  const lines = diff.out.trimEnd().split("\n");
123
129
  const more = lines.length > maxLines ? `
124
- \u2026 ${lines.length - maxLines} baris lagi` : "";
125
- return [stat.out.trim(), lines.slice(0, maxLines).join("\n") + more, fresh.length ? `File baru (belum di-track): ${fresh.join(", ")}` : ""].filter(Boolean).join("\n\n");
130
+ ${t("\u2026 {n} more lines", { n: lines.length - maxLines })}` : "";
131
+ return [stat.out.trim(), lines.slice(0, maxLines).join("\n") + more, fresh.length ? t("New files (untracked): {files}", { files: fresh.join(", ") }) : ""].filter(Boolean).join("\n\n");
126
132
  }
127
133
 
128
134
  // src/slash/doctor.ts
129
135
  async function doctorText(input, deps) {
130
136
  const major = Number(deps.nodeVersion.replace(/^v/, "").split(".")[0]);
131
- const lines = [major >= 22 ? `\u2713 Node ${deps.nodeVersion}` : `\u2717 Node ${deps.nodeVersion} (butuh \u2265 22)`];
132
- for (const p of input.providers) lines.push(p.ready ? `\u2713 ${p.name}` : `\u25CB ${p.name} (belum ada key)`);
137
+ const lines = [major >= 22 ? `\u2713 Node ${deps.nodeVersion}` : t("\u2717 Node {version} (needs \u2265 22)", { version: deps.nodeVersion })];
138
+ for (const p of input.providers) lines.push(p.ready ? `\u2713 ${p.name}` : t("\u25CB {name} (no key yet)", { name: p.name }));
133
139
  const cmds = ["rg", "git", "npx", "uvx"];
134
140
  const [connection, found] = await Promise.all([
135
141
  deps.listModels().then(
136
- (models) => `\u2713 Koneksi ${input.activeProvider} (${models.length} model)`,
137
- (error) => `\u2717 Koneksi ${input.activeProvider}: ${error.message}`
142
+ (models) => t("\u2713 Connection {provider} ({n} models)", { provider: input.activeProvider, n: models.length }),
143
+ (error) => t("\u2717 Connection {provider}: {error}", { provider: input.activeProvider, error: error.message })
138
144
  ),
139
145
  Promise.all(cmds.map(async (cmd) => deps.which(cmd)))
140
146
  ]);
@@ -165,31 +171,31 @@ function statusText(s) {
165
171
  return [
166
172
  `BCCLI ${s.version}`,
167
173
  `Model: ${s.modelRef} (${s.providerLabel})`,
168
- `Mode izin: ${s.mode}`,
169
- `Folder: ${s.cwd}`,
170
- `MCP: ${ready} aktif, ${errors} error`,
171
- `Token sesi: ${k(s.usage.inputTokens)} masuk \xB7 ${k(s.usage.outputTokens)} keluar`,
172
- `Konteks: \xB1${Math.round(s.lastInputTokens / 128e3 * 100)}% dari 128k`
174
+ t("Permission mode: {mode}", { mode: s.mode }),
175
+ t("Folder: {cwd}", { cwd: s.cwd }),
176
+ t("MCP: {ready} active, {errors} errors", { ready, errors }),
177
+ t("Session tokens: {input} in \xB7 {output} out", { input: k(s.usage.inputTokens), output: k(s.usage.outputTokens) }),
178
+ t("Context: ~{pct}% of 128k", { pct: Math.round(s.lastInputTokens / 128e3 * 100) })
173
179
  ].join("\n");
174
180
  }
175
181
  function agentsText(defs) {
176
- return defs.map((d) => `${d.name} \u2014 ${d.description} \xB7 alat: ${d.tools ? d.tools.join(", ") : "semua"} \xB7 model: ${d.model ?? "ikut utama"}`).join("\n");
182
+ return defs.map((d) => t("{name} \u2014 {description} \xB7 tools: {tools} \xB7 model: {model}", { name: d.name, description: d.description, tools: d.tools ? d.tools.join(", ") : t("all"), model: d.model ?? t("same as main") })).join("\n");
177
183
  }
178
184
  function skillsText(skills, commands) {
179
- const s = skills.length ? skills.map((x) => `${x.name} \u2014 ${x.description.slice(0, 80)} (${x.dir})`).join("\n") : "(tidak ada skill)";
180
- const c = commands.length ? commands.map((x) => `/${x.name} \u2014 ${x.description ?? ""}`).join("\n") : "(tidak ada perintah custom)";
181
- return `Skill:
185
+ const s = skills.length ? skills.map((x) => `${x.name} \u2014 ${x.description.slice(0, 80)} (${x.dir})`).join("\n") : t("(no skills)");
186
+ const c = commands.length ? commands.map((x) => `/${x.name} \u2014 ${x.description ?? ""}`).join("\n") : t("(no custom commands)");
187
+ return `${t("Skills")}:
182
188
  ${s}
183
189
 
184
- Perintah custom:
190
+ ${t("Custom commands")}:
185
191
  ${c}`;
186
192
  }
187
193
  function sessionText(s) {
188
194
  return [
189
- `Mulai: ${s.started.toLocaleString("id-ID")}`,
190
- `${s.messages} pesan \xB7 ${k(s.usage.inputTokens)} token masuk \xB7 ${k(s.usage.outputTokens)} keluar`,
195
+ t("Started: {when}", { when: s.started.toLocaleString(locale()) }),
196
+ t("{messages} messages \xB7 {input} tokens in \xB7 {output} out", { messages: s.messages, input: k(s.usage.inputTokens), output: k(s.usage.outputTokens) }),
191
197
  `Model: ${s.modelRef}`,
192
- `File: ${s.file}`
198
+ t("File: {file}", { file: s.file })
193
199
  ].join("\n");
194
200
  }
195
201
 
@@ -267,7 +273,7 @@ function LinePrompt({ label, mask, onSubmit, onCancel }) {
267
273
  `${label}: `,
268
274
  shown,
269
275
  /* @__PURE__ */ jsx2(Text2, { inverse: true, children: " " }),
270
- /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: " enter simpan \xB7 esc batal" })
276
+ /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: t(" enter save \xB7 esc cancel") })
271
277
  ] }) });
272
278
  }
273
279
 
@@ -296,12 +302,12 @@ function ListPicker({ title, items, onPick }) {
296
302
  });
297
303
  const start = Math.max(0, Math.min(cursor - 5, shown.length - VISIBLE));
298
304
  return /* @__PURE__ */ jsxs2(Box3, { flexDirection: "column", borderStyle: "round", borderColor: color("cyan"), paddingX: 1, children: [
299
- /* @__PURE__ */ jsx3(Text3, { bold: true, children: `${title} (\u2191\u2193 enter \xB7 ketik untuk menyaring${filter ? `: ${filter}` : ""} \xB7 esc batal)` }),
305
+ /* @__PURE__ */ jsx3(Text3, { bold: true, children: t("{title} (\u2191\u2193 enter \xB7 type to filter{filter} \xB7 esc cancel)", { title, filter: filter ? `: ${filter}` : "" }) }),
300
306
  shown.slice(start, start + VISIBLE).map((item, i) => /* @__PURE__ */ jsxs2(Text3, { color: start + i === cursor && !item.disabled ? color("green") : void 0, dimColor: item.disabled, children: [
301
307
  `${start + i === cursor ? "\u203A" : " "} ${item.label}`,
302
308
  item.hint ? /* @__PURE__ */ jsx3(Text3, { dimColor: true, children: ` ${item.hint}` }) : null
303
309
  ] }, item.id)),
304
- shown.length ? null : /* @__PURE__ */ jsx3(Text3, { dimColor: true, children: "Tidak ada yang cocok." })
310
+ shown.length ? null : /* @__PURE__ */ jsx3(Text3, { dimColor: true, children: t("No matches.") })
305
311
  ] });
306
312
  }
307
313
 
@@ -341,11 +347,11 @@ function ModelPicker({ groups, current, onPick }) {
341
347
  });
342
348
  const start = Math.max(0, Math.min(cursor - 5, rows.length - VISIBLE2));
343
349
  return /* @__PURE__ */ jsxs3(Box4, { flexDirection: "column", borderStyle: "round", borderColor: color("cyan"), paddingX: 1, children: [
344
- /* @__PURE__ */ jsx4(Text4, { bold: true, children: `Pilih model (ketik untuk menyaring${filter ? `: ${filter}` : ""} \xB7 \u2191\u2193 enter \xB7 esc batal)` }),
350
+ /* @__PURE__ */ jsx4(Text4, { bold: true, children: t("Choose a model (type to filter{filter} \xB7 \u2191\u2193 enter \xB7 esc cancel)", { filter: filter ? `: ${filter}` : "" }) }),
345
351
  rows.slice(start, start + VISIBLE2).map(
346
352
  (row, i) => row.kind === "header" ? /* @__PURE__ */ jsx4(Text4, { dimColor: true, children: `\u2500\u2500 ${row.text} ` }, `h${start + i}`) : /* @__PURE__ */ jsx4(Text4, { color: start + i === cursor ? color("green") : void 0, children: `${start + i === cursor ? "\u203A" : " "} ${row.label}${row.ref === current ? " (aktif)" : ""}` }, row.ref)
347
353
  ),
348
- !selectable.length ? /* @__PURE__ */ jsx4(Text4, { dimColor: true, children: "Tidak ada model yang cocok." }) : null,
354
+ !selectable.length ? /* @__PURE__ */ jsx4(Text4, { dimColor: true, children: t("No matching models.") }) : null,
349
355
  groups.filter((g) => g.error).map((g) => /* @__PURE__ */ jsx4(Text4, { color: color("yellow"), children: `${g.providerName} \u2014 ${g.error}` }, `e-${g.providerId}`))
350
356
  ] });
351
357
  }
@@ -364,7 +370,7 @@ function ProviderMenu({ entries, onPick }) {
364
370
  else if (key.escape) onPick(void 0);
365
371
  });
366
372
  return /* @__PURE__ */ jsxs4(Box5, { flexDirection: "column", borderStyle: "round", borderColor: color("cyan"), paddingX: 1, children: [
367
- /* @__PURE__ */ jsx5(Text5, { bold: true, children: "Provider (\u2191\u2193 enter, esc batal) \xB7 \u2713 siap \xB7 \u25CB butuh API key" }),
373
+ /* @__PURE__ */ jsx5(Text5, { bold: true, children: t("Provider (\u2191\u2193 enter, esc cancel) \xB7 \u2713 ready \xB7 \u25CB needs API key") }),
368
374
  entries.map((e, i) => /* @__PURE__ */ jsxs4(Text5, { color: i === index ? color("green") : void 0, children: [
369
375
  `${i === index ? "\u203A" : " "} ${e.ready ? "\u2713" : "\u25CB"} ${e.name}`,
370
376
  /* @__PURE__ */ jsx5(Text5, { dimColor: true, children: ` ${e.baseURL}` })
@@ -389,7 +395,7 @@ function DiffView({ diff }) {
389
395
 
390
396
  // src/ui/PermissionPrompt.tsx
391
397
  import { jsx as jsx7, jsxs as jsxs5 } from "react/jsx-runtime";
392
- var VERB = { edit: "edit", write: "tulis", bash: "jalankan", fetch: "ambil" };
398
+ var VERB = { edit: "edit", write: "write", bash: "run", fetch: "fetch" };
393
399
  function PermissionPrompt({
394
400
  request,
395
401
  onAnswer
@@ -402,16 +408,16 @@ function PermissionPrompt({
402
408
  else if (k2 === "n" || key.escape) onAnswer("no");
403
409
  });
404
410
  const rules = request.sessionRules ?? [];
405
- const sessionLabel = !rules.length ? null : request.kind === "edit" ? "ya untuk semua edit di project ini, sesi ini" : `ya sesi ini untuk ${rules.join(", ")}`;
411
+ const sessionLabel = !rules.length ? null : request.kind === "edit" ? t("yes for all edits in this project, this session") : t("yes this session for {rules}", { rules: rules.join(", ") });
406
412
  return /* @__PURE__ */ jsxs5(Box7, { flexDirection: "column", borderStyle: "round", borderColor: color("yellow"), paddingX: 1, children: [
407
- /* @__PURE__ */ jsx7(Text7, { bold: true, children: `${request.agent ? `[${request.agent}] ` : ""}Izinkan ${VERB[request.tool] ?? request.tool} ${request.target}?` }),
413
+ /* @__PURE__ */ jsx7(Text7, { bold: true, children: `${request.agent ? `[${request.agent}] ` : ""}${t("Allow {verb} {target}?", { verb: t(VERB[request.tool] ?? request.tool), target: request.target })}` }),
408
414
  request.preview ? /* @__PURE__ */ jsx7(DiffView, { diff: request.preview }) : null,
409
415
  /* @__PURE__ */ jsxs5(Text7, { children: [
410
- /* @__PURE__ */ jsx7(Text7, { color: color("green"), children: "[y] ya" }),
416
+ /* @__PURE__ */ jsx7(Text7, { color: color("green"), children: t("[y] yes") }),
411
417
  " ",
412
418
  sessionLabel ? /* @__PURE__ */ jsx7(Text7, { color: color("cyan"), children: `[a] ${sessionLabel} ` }) : null,
413
- /* @__PURE__ */ jsx7(Text7, { color: color("yellow"), children: "[s] ya semua " }),
414
- /* @__PURE__ */ jsx7(Text7, { color: color("red"), children: "[n] tidak" })
419
+ /* @__PURE__ */ jsx7(Text7, { color: color("yellow"), children: t("[s] yes to all ") }),
420
+ /* @__PURE__ */ jsx7(Text7, { color: color("red"), children: t("[n] no") })
415
421
  ] })
416
422
  ] });
417
423
  }
@@ -514,7 +520,7 @@ function PromptInput({ disabled, history, cwd, onSubmit, extraCommands }) {
514
520
  value,
515
521
  disabled ? "" : /* @__PURE__ */ jsx8(Text8, { inverse: true, children: " " })
516
522
  ] }) }),
517
- suggestions.map((s, i) => /* @__PURE__ */ jsx8(Text8, { color: i === highlighted ? color("green") : void 0, dimColor: i !== highlighted, children: `${i === highlighted ? "\u203A" : " "} /${s.name} ${s.description}` }, s.name))
523
+ suggestions.map((s, i) => /* @__PURE__ */ jsx8(Text8, { color: i === highlighted ? color("green") : void 0, dimColor: i !== highlighted, children: `${i === highlighted ? "\u203A" : " "} /${s.name} ${t(s.description)}` }, s.name))
518
524
  ] });
519
525
  }
520
526
 
@@ -526,11 +532,11 @@ var FRAMES = ["\xB7", "\u2722", "\u2733", "\u2736", "\u273B", "\u273D"];
526
532
  function Spinner({ label, startedAt }) {
527
533
  const [tick, setTick] = useState6(0);
528
534
  useEffect(() => {
529
- const id = setInterval(() => setTick((t) => t + 1), 120);
535
+ const id = setInterval(() => setTick((t2) => t2 + 1), 120);
530
536
  return () => clearInterval(id);
531
537
  }, []);
532
538
  const seconds = Math.floor((Date.now() - startedAt) / 1e3);
533
- return /* @__PURE__ */ jsx9(Text9, { color: color(ACCENT), children: `${FRAMES[tick % FRAMES.length]} ${label}\u2026 (${seconds} dtk \xB7 esc batal)` });
539
+ return /* @__PURE__ */ jsx9(Text9, { color: color(ACCENT), children: `${FRAMES[tick % FRAMES.length]} ${t("{label}\u2026 ({seconds}s \xB7 esc to cancel)", { label, seconds })}` });
534
540
  }
535
541
 
536
542
  // src/ui/StatusBar.tsx
@@ -547,7 +553,7 @@ function formatTokens(n) {
547
553
  }
548
554
  function StatusBar({ mode, tokens, busy, model, reasoning = "auto" }) {
549
555
  const danger = mode === "allowAll";
550
- return /* @__PURE__ */ jsx10(Text10, { color: danger ? color("red") : void 0, dimColor: !danger, children: ` ${LABEL[mode]}${model ? ` \xB7 ${model}` : ""} \xB7 reasoning: ${reasoning} \xB7 shift+tab ganti mode \xB7 ${formatTokens(tokens)} token \xB7 ${busy ? "esc batal" : "/ perintah"}` });
556
+ return /* @__PURE__ */ jsx10(Text10, { color: danger ? color("red") : void 0, dimColor: !danger, children: ` ${LABEL[mode]}${model ? ` \xB7 ${model}` : ""} \xB7 reasoning: ${reasoning} \xB7 ${t("shift+tab switch mode")} \xB7 ${t("{n} tokens", { n: formatTokens(tokens) })} \xB7 ${busy ? t("esc to cancel") : t("/ for commands")}` });
551
557
  }
552
558
 
553
559
  // src/ui/McpMenu.tsx
@@ -564,7 +570,7 @@ function McpMenu({ items, onPick }) {
564
570
  else if (key.escape) onPick(void 0);
565
571
  });
566
572
  return /* @__PURE__ */ jsxs7(Box9, { flexDirection: "column", borderStyle: "round", borderColor: color("cyan"), paddingX: 1, children: [
567
- /* @__PURE__ */ jsx11(Text11, { bold: true, children: "Server MCP (enter: pasang / hapus \xB7 esc tutup) \xB7 \u2713 aktif \u25CB belum \u2717 error" }),
573
+ /* @__PURE__ */ jsx11(Text11, { bold: true, children: t("MCP servers (enter: install / remove \xB7 esc close) \xB7 \u2713 active \u25CB not installed \u2717 error") }),
568
574
  items.map((item, i) => /* @__PURE__ */ jsxs7(Text11, { color: i === index ? color("green") : void 0, children: [
569
575
  `${i === index ? "\u203A" : " "} ${MARK[item.status]} ${item.name.padEnd(12)}`,
570
576
  /* @__PURE__ */ jsx11(Text11, { dimColor: true, children: item.status === "error" ? ` ${item.error ?? ""}` : ` ${item.description}` })
@@ -583,14 +589,14 @@ function PlanApproval({ plan, onAnswer }) {
583
589
  else if (k2 === "n" || key.escape) onAnswer("no");
584
590
  });
585
591
  return /* @__PURE__ */ jsxs8(Box10, { flexDirection: "column", borderStyle: "round", borderColor: color("cyan"), paddingX: 1, children: [
586
- /* @__PURE__ */ jsx12(Text12, { bold: true, children: "Rencana dari agent" }),
592
+ /* @__PURE__ */ jsx12(Text12, { bold: true, children: t("Plan from the agent") }),
587
593
  /* @__PURE__ */ jsx12(Markdown, { text: plan }),
588
594
  /* @__PURE__ */ jsxs8(Text12, { children: [
589
- /* @__PURE__ */ jsx12(Text12, { color: color("green"), children: "[a] ya, edit otomatis" }),
595
+ /* @__PURE__ */ jsx12(Text12, { color: color("green"), children: t("[a] yes, auto-edit") }),
590
596
  " ",
591
- /* @__PURE__ */ jsx12(Text12, { color: color("cyan"), children: "[y] ya, tanya tiap langkah" }),
597
+ /* @__PURE__ */ jsx12(Text12, { color: color("cyan"), children: t("[y] yes, ask at each step") }),
592
598
  " ",
593
- /* @__PURE__ */ jsx12(Text12, { color: color("red"), children: "[n] belum, perbaiki dulu" })
599
+ /* @__PURE__ */ jsx12(Text12, { color: color("red"), children: t("[n] not yet, revise first") })
594
600
  ] })
595
601
  ] });
596
602
  }
@@ -600,8 +606,8 @@ import { Box as Box11, Text as Text13 } from "ink";
600
606
  import { jsx as jsx13 } from "react/jsx-runtime";
601
607
  var MARK2 = { completed: "\u2611", in_progress: "\u25FC", pending: "\u2610" };
602
608
  function TodoList({ items }) {
603
- if (!items.length || items.every((t) => t.status === "completed")) return null;
604
- return /* @__PURE__ */ jsx13(Box11, { flexDirection: "column", marginLeft: 2, marginTop: 1, children: items.map((t, i) => /* @__PURE__ */ jsx13(Text13, { dimColor: t.status === "completed", color: t.status === "in_progress" ? color("yellow") : void 0, children: `${MARK2[t.status]} ${t.content}` }, i)) });
609
+ if (!items.length || items.every((t2) => t2.status === "completed")) return null;
610
+ return /* @__PURE__ */ jsx13(Box11, { flexDirection: "column", marginLeft: 2, marginTop: 1, children: items.map((t2, i) => /* @__PURE__ */ jsx13(Text13, { dimColor: t2.status === "completed", color: t2.status === "in_progress" ? color("yellow") : void 0, children: `${MARK2[t2.status]} ${t2.content}` }, i)) });
605
611
  }
606
612
 
607
613
  // src/ui/ToolBlock.tsx
@@ -622,12 +628,12 @@ function ToolBlock({ tool, target, output, display, isError, done, expanded, sub
622
628
  ] }),
623
629
  sub?.length ? /* @__PURE__ */ jsxs9(Box12, { flexDirection: "column", marginLeft: 3, children: [
624
630
  sub.slice(-6).map((s) => /* @__PURE__ */ jsx14(Text14, { dimColor: s.done && !s.isError, color: s.isError ? color("red") : void 0, children: `\u23BF ${toolLabel(s.tool)} ${s.target}` }, s.id)),
625
- sub.length > 6 ? /* @__PURE__ */ jsx14(Text14, { dimColor: true, children: `\u2026 ${sub.length - 6} langkah sebelumnya` }) : null
631
+ sub.length > 6 ? /* @__PURE__ */ jsx14(Text14, { dimColor: true, children: t("\u2026 {n} earlier steps", { n: sub.length - 6 }) }) : null
626
632
  ] }) : null,
627
- isTask && done ? /* @__PURE__ */ jsx14(Box12, { marginLeft: 3, children: /* @__PURE__ */ jsx14(Text14, { color: isError ? color("red") : color("green"), children: `${isError ? "\u2717 gagal" : "\u2713 selesai"}${display ? ` \xB7 ${display}` : ""}` }) }) : null,
633
+ isTask && done ? /* @__PURE__ */ jsx14(Box12, { marginLeft: 3, children: /* @__PURE__ */ jsx14(Text14, { color: isError ? color("red") : color("green"), children: `${isError ? t("\u2717 failed") : t("\u2713 done")}${display ? ` \xB7 ${display}` : ""}` }) }) : null,
628
634
  isDiff ? /* @__PURE__ */ jsx14(Box12, { marginLeft: 3, children: /* @__PURE__ */ jsx14(DiffView, { diff: display }) }) : output && (isError || tool === "bash") && !(isTask && !isError) ? /* @__PURE__ */ jsxs9(Box12, { flexDirection: "column", marginLeft: 3, children: [
629
635
  shown.map((line, i) => /* @__PURE__ */ jsx14(Text14, { dimColor: !isError, color: isError ? color("red") : void 0, children: line }, i)),
630
- lines.length > shown.length ? /* @__PURE__ */ jsx14(Text14, { dimColor: true, children: `\u2026 ${lines.length - shown.length} baris lagi (ctrl+o)` }) : null
636
+ lines.length > shown.length ? /* @__PURE__ */ jsx14(Text14, { dimColor: true, children: t("\u2026 {n} more lines (ctrl+o)", { n: lines.length - shown.length }) }) : null
631
637
  ] }) : null
632
638
  ] });
633
639
  }
@@ -636,13 +642,13 @@ function ToolBlock({ tool, target, output, display, isError, done, expanded, sub
636
642
  var nextId = 0;
637
643
  var entry = (e) => ({ ...e, id: nextId++ });
638
644
  var NOTICES = {
639
- compacted: ["Percakapan diringkas agar muat di konteks model.", "info"],
640
- stepLimit: ['Batas 50 langkah tercapai. Ketik "lanjut" untuk meneruskan.', "warn"],
641
- aborted: ["Dibatalkan.", "warn"]
645
+ compacted: ["Conversation summarized to fit the model context.", "info"],
646
+ stepLimit: ['Step limit of 50 reached. Type "continue" to keep going.', "warn"],
647
+ aborted: ["Cancelled.", "warn"]
642
648
  };
643
649
  var isEmptyCont = (e) => e.kind === "assistant" && e.cont && !e.text;
644
- function endTurn(t) {
645
- return { done: [...t.done, ...t.live.filter((e) => !isEmptyCont(e))], live: [] };
650
+ function endTurn(t2) {
651
+ return { done: [...t2.done, ...t2.live.filter((e) => !isEmptyCont(e))], live: [] };
646
652
  }
647
653
  var TOOL_MARKUP = /<\/?tool_call>|<\/?arg_(?:key|value)>|Tool call list|^\s*\d+\.\s*Tool:|`\s*=\s*`/;
648
654
  function finishedCut(text) {
@@ -658,62 +664,62 @@ function finishedCut(text) {
658
664
  }
659
665
  return cut;
660
666
  }
661
- function flushLines(t) {
662
- const last = t.live.at(-1);
663
- if (last?.kind !== "assistant") return t;
667
+ function flushLines(t2) {
668
+ const last = t2.live.at(-1);
669
+ if (last?.kind !== "assistant") return t2;
664
670
  const cut = finishedCut(last.text);
665
- if (cut < 0) return t;
671
+ if (cut < 0) return t2;
666
672
  const chunk = last.text.slice(0, cut);
667
- if (!last.cont && !chunk.trim()) return t;
673
+ if (!last.cont && !chunk.trim()) return t2;
668
674
  const rest = entry({ kind: "assistant", text: last.text.slice(cut + 1), cont: true, flushed: (last.flushed ?? 0) + cut + 1 });
669
- return { done: [...t.done, ...t.live.slice(0, -1).filter((e) => !isEmptyCont(e)), { ...last, text: chunk || " " }], live: [rest] };
675
+ return { done: [...t2.done, ...t2.live.slice(0, -1).filter((e) => !isEmptyCont(e)), { ...last, text: chunk || " " }], live: [rest] };
670
676
  }
671
- function applyEvent(t, event) {
677
+ function applyEvent(t2, event) {
672
678
  switch (event.type) {
673
679
  case "text": {
674
- const last = t.live.at(-1);
675
- if (last?.kind === "assistant") return flushLines({ ...t, live: [...t.live.slice(0, -1), { ...last, text: last.text + event.delta }] });
676
- return flushLines({ ...t, live: [...t.live, entry({ kind: "assistant", text: event.delta })] });
680
+ const last = t2.live.at(-1);
681
+ if (last?.kind === "assistant") return flushLines({ ...t2, live: [...t2.live.slice(0, -1), { ...last, text: last.text + event.delta }] });
682
+ return flushLines({ ...t2, live: [...t2.live, entry({ kind: "assistant", text: event.delta })] });
677
683
  }
678
684
  case "thinking": {
679
- const last = t.live.at(-1);
680
- if (last?.kind === "thinking") return { ...t, live: [...t.live.slice(0, -1), { ...last, text: last.text + event.delta }] };
681
- return { ...t, live: [...t.live, entry({ kind: "thinking", text: event.delta })] };
685
+ const last = t2.live.at(-1);
686
+ if (last?.kind === "thinking") return { ...t2, live: [...t2.live.slice(0, -1), { ...last, text: last.text + event.delta }] };
687
+ return { ...t2, live: [...t2.live, entry({ kind: "thinking", text: event.delta })] };
682
688
  }
683
689
  case "textReplace": {
684
- const last = t.live.at(-1);
685
- if (last?.kind !== "assistant") return t;
686
- return { ...t, live: [...t.live.slice(0, -1), { ...last, text: event.text.slice(last.flushed ?? 0) }] };
690
+ const last = t2.live.at(-1);
691
+ if (last?.kind !== "assistant") return t2;
692
+ return { ...t2, live: [...t2.live.slice(0, -1), { ...last, text: event.text.slice(last.flushed ?? 0) }] };
687
693
  }
688
694
  case "toolStart": {
689
- const running = t.live.some((e) => e.kind === "tool" && !e.done);
695
+ const running = t2.live.some((e) => e.kind === "tool" && !e.done);
690
696
  const tool = entry({ kind: "tool", callId: event.id, tool: event.tool, target: event.target, done: false });
691
- return running ? { ...t, live: [...t.live, tool] } : { done: [...t.done, ...t.live.filter((e) => !isEmptyCont(e))], live: [tool] };
697
+ return running ? { ...t2, live: [...t2.live, tool] } : { done: [...t2.done, ...t2.live.filter((e) => !isEmptyCont(e))], live: [tool] };
692
698
  }
693
699
  case "toolEnd": {
694
- const live = t.live.map(
700
+ const live = t2.live.map(
695
701
  (e) => e.kind === "tool" && e.callId === event.id ? { ...e, output: event.output, display: event.display, isError: event.isError, done: true } : e
696
702
  );
697
703
  const firstRunning = live.findIndex((e) => e.kind === "tool" && !e.done);
698
704
  const cut = firstRunning === -1 ? live.length : firstRunning;
699
- return { done: [...t.done, ...live.slice(0, cut)], live: live.slice(cut) };
705
+ return { done: [...t2.done, ...live.slice(0, cut)], live: live.slice(cut) };
700
706
  }
701
707
  case "subagent": {
702
708
  const inner = event.event;
703
- if (inner.type !== "toolStart" && inner.type !== "toolEnd") return t;
709
+ if (inner.type !== "toolStart" && inner.type !== "toolEnd") return t2;
704
710
  const update = (e) => {
705
711
  if (e.kind !== "tool" || e.callId !== event.parentId) return e;
706
712
  const sub = e.sub ?? [];
707
713
  if (inner.type === "toolStart") return { ...e, sub: [...sub, { id: inner.id, tool: inner.tool, target: inner.target, done: false }] };
708
714
  return { ...e, sub: sub.map((s) => s.id === inner.id ? { ...s, done: true, isError: inner.isError } : s) };
709
715
  };
710
- return { ...t, live: t.live.map(update) };
716
+ return { ...t2, live: t2.live.map(update) };
711
717
  }
712
718
  case "error":
713
- return { ...t, live: [...t.live, entry({ kind: "notice", text: `Error: ${event.message}`, tone: "error" })] };
719
+ return { ...t2, live: [...t2.live, entry({ kind: "notice", text: `Error: ${event.message}`, tone: "error" })] };
714
720
  default: {
715
721
  const notice = NOTICES[event.type];
716
- return notice ? { ...t, live: [...t.live, entry({ kind: "notice", text: notice[0], tone: notice[1] })] } : t;
722
+ return notice ? { ...t2, live: [...t2.live, entry({ kind: "notice", text: t(notice[0]), tone: notice[1] })] } : t2;
717
723
  }
718
724
  }
719
725
  }
@@ -723,17 +729,17 @@ import { jsx as jsx15, jsxs as jsxs10 } from "react/jsx-runtime";
723
729
  function helpText(runtime) {
724
730
  const custom = runtime.commands.filter((c) => !SLASH_COMMANDS.some((b) => b.name === c.name));
725
731
  return [
726
- ...SLASH_COMMANDS.map((c) => `/${c.name.padEnd(12)} ${c.description}`),
727
- ...custom.length ? ["", "Perintah custom:", ...custom.map((c) => `/${c.name.padEnd(8)} ${c.description ?? ""}`)] : [],
728
- ...runtime.skills.length ? ["", "Skill:", ...runtime.skills.map((s) => `/${s.name.padEnd(8)} ${s.description.slice(0, 60)}`)] : [],
732
+ ...SLASH_COMMANDS.map((c) => `/${c.name.padEnd(12)} ${t(c.description)}`),
733
+ ...custom.length ? ["", t("Custom commands:"), ...custom.map((c) => `/${c.name.padEnd(8)} ${c.description ?? ""}`)] : [],
734
+ ...runtime.skills.length ? ["", t("Skills:"), ...runtime.skills.map((s) => `/${s.name.padEnd(8)} ${s.description.slice(0, 60)}`)] : [],
729
735
  "",
730
- "shift+tab ganti mode izin esc batalkan giliran",
731
- "ctrl+o output alat terakhir lengkap \\ + enter baris baru",
732
- "ctrl+t buka/tutup thinking model",
733
- "@file + tab lengkapi nama file \u2191\u2193 riwayat input"
736
+ t("shift+tab switch permission mode esc cancel the turn"),
737
+ t("ctrl+o full output of the last tool \\ + enter new line"),
738
+ t("ctrl+t show/hide the model\u2019s thinking"),
739
+ t("@file + tab complete file names \u2191\u2193 input history")
734
740
  ].join("\n");
735
741
  }
736
- var INIT_PROMPT = "Pelajari project ini (struktur folder, file package/build, perintah test dan lint, konvensi kode yang terlihat). Lalu buat AGENTS.md di root project, atau perbarui bila sudah ada, berisi: cara build, test, dan lint; struktur singkat; konvensi penting. Ringkas dan faktual, hanya yang benar-benar ada di project.";
742
+ var INIT_PROMPT = "Study this project (folder structure, package/build files, test and lint commands, visible code conventions). Then create AGENTS.md in the project root, or update it if it already exists, covering: how to build, test, and lint; a short structure overview; important conventions. Keep it concise and factual, only what actually exists in the project.";
737
743
  var labelFor = (runtime, ref) => `${ref.slice(ref.indexOf("/") + 1)} \xB7 ${runtime.providerLabel()}`;
738
744
  function App({ runtime, initialPrompt, version }) {
739
745
  const { exit } = useApp();
@@ -761,13 +767,13 @@ function App({ runtime, initialPrompt, version }) {
761
767
  const [showThinking, setShowThinking] = useState8(false);
762
768
  useEffect2(() => runtime.todos.subscribe(setTodos), [runtime]);
763
769
  const extraCommands = [
764
- ...runtime.commands.filter((c) => !SLASH_COMMANDS.some((b) => b.name === c.name.toLowerCase())).map((c) => ({ name: c.name.toLowerCase(), description: c.description ?? "perintah custom" })),
765
- ...runtime.skills.filter((s) => !runtime.commands.some((c) => c.name.toLowerCase() === s.name.toLowerCase())).filter((s) => !SLASH_COMMANDS.some((b) => b.name === s.name.toLowerCase())).map((s) => ({ name: s.name, description: `skill \xB7 ${s.description.slice(0, 50)}` }))
770
+ ...runtime.commands.filter((c) => !SLASH_COMMANDS.some((b) => b.name === c.name.toLowerCase())).map((c) => ({ name: c.name.toLowerCase(), description: c.description ?? t("custom command") })),
771
+ ...runtime.skills.filter((s) => !runtime.commands.some((c) => c.name.toLowerCase() === s.name.toLowerCase())).filter((s) => !SLASH_COMMANDS.some((b) => b.name === s.name.toLowerCase())).map((s) => ({ name: s.name, description: t("skill \xB7 {description}", { description: s.description.slice(0, 50) }) }))
766
772
  ];
767
773
  const lastTool = useRef(null);
768
774
  const toolTargets = useRef(/* @__PURE__ */ new Map());
769
775
  const notice = useCallback((text, tone = "info") => {
770
- setTranscript((t) => ({ ...t, done: [...t.done, entry({ kind: "notice", text, tone })] }));
776
+ setTranscript((t2) => ({ ...t2, done: [...t2.done, entry({ kind: "notice", text, tone })] }));
771
777
  }, []);
772
778
  const onEvent = useCallback((event) => {
773
779
  if (event.type === "toolStart") toolTargets.current.set(event.id, event.target);
@@ -775,7 +781,7 @@ function App({ runtime, initialPrompt, version }) {
775
781
  lastTool.current = { tool: event.tool, target: toolTargets.current.get(event.id) ?? "", output: event.output };
776
782
  }
777
783
  if (event.type === "usage") setTokens(event.inputTokens + event.outputTokens);
778
- setTranscript((t) => applyEvent(t, event));
784
+ setTranscript((t2) => applyEvent(t2, event));
779
785
  }, []);
780
786
  useEffect2(() => {
781
787
  runtime.agent.onEvent = onEvent;
@@ -784,7 +790,7 @@ function App({ runtime, initialPrompt, version }) {
784
790
  }, [runtime, onEvent]);
785
791
  const runTurn = useCallback(
786
792
  async (text) => {
787
- setTranscript((t) => ({ ...t, done: [...t.done, entry({ kind: "user", text })] }));
793
+ setTranscript((t2) => ({ ...t2, done: [...t2.done, entry({ kind: "user", text })] }));
788
794
  setBusy(true);
789
795
  setStartedAt(Date.now());
790
796
  controller.current = new AbortController();
@@ -807,11 +813,11 @@ function App({ runtime, initialPrompt, version }) {
807
813
  case "clear":
808
814
  runtime.agent.clear();
809
815
  setTokens(0);
810
- notice("Percakapan dikosongkan.");
816
+ notice(t("Conversation cleared."));
811
817
  return;
812
818
  case "cost": {
813
819
  const u = runtime.agent.totalUsage;
814
- notice(`${u.inputTokens.toLocaleString("id-ID")} token masuk \xB7 ${u.outputTokens.toLocaleString("id-ID")} token keluar`);
820
+ notice(t("{input} tokens in \xB7 {output} tokens out", { input: u.inputTokens.toLocaleString(locale()), output: u.outputTokens.toLocaleString(locale()) }));
815
821
  return;
816
822
  }
817
823
  case "compact": {
@@ -821,7 +827,7 @@ function App({ runtime, initialPrompt, version }) {
821
827
  try {
822
828
  await runtime.agent.compact(controller.current.signal);
823
829
  } catch (error) {
824
- notice(`Gagal meringkas: ${error.message}`, "error");
830
+ notice(t("Could not summarize: {error}", { error: error.message }), "error");
825
831
  }
826
832
  controller.current = null;
827
833
  setBusy(false);
@@ -836,7 +842,7 @@ function App({ runtime, initialPrompt, version }) {
836
842
  runtime.setReasoning(level);
837
843
  writeGlobalConfig({ reasoning: level }, runtime.env);
838
844
  setReasoning(level);
839
- notice(`Reasoning: ${level} (tersimpan sebagai default)`);
845
+ notice(t("Reasoning: {level} (saved as default)", { level }));
840
846
  } catch (error) {
841
847
  notice(error.message, "error");
842
848
  }
@@ -853,7 +859,7 @@ function App({ runtime, initialPrompt, version }) {
853
859
  id: level,
854
860
  label: level === "auto" ? "Auto (model default)" : level.charAt(0).toUpperCase() + level.slice(1),
855
861
  disabled: !supported.includes(level),
856
- hint: supported.includes(level) ? void 0 : "tidak didukung provider ini"
862
+ hint: supported.includes(level) ? void 0 : t("not supported by this provider")
857
863
  })),
858
864
  onPick: (id) => {
859
865
  setListPicker(null);
@@ -868,7 +874,7 @@ function App({ runtime, initialPrompt, version }) {
868
874
  runtime.setModel(args);
869
875
  writeGlobalConfig({ model: args }, runtime.env);
870
876
  setModelLabel(labelFor(runtime, args));
871
- notice(`Model: ${args} (tersimpan sebagai default)`);
877
+ notice(t("Model: {model} (saved as default)", { model: args }));
872
878
  } catch (error) {
873
879
  notice(error.message, "error");
874
880
  }
@@ -877,7 +883,7 @@ function App({ runtime, initialPrompt, version }) {
877
883
  try {
878
884
  setPicker(await runtime.listModels());
879
885
  } catch (error) {
880
- notice(`Tidak bisa mengambil daftar model: ${error.message}`, "error");
886
+ notice(t("Cannot fetch the model list: {error}", { error: error.message }), "error");
881
887
  }
882
888
  return;
883
889
  }
@@ -885,7 +891,7 @@ function App({ runtime, initialPrompt, version }) {
885
891
  const installed = readMcpFile(globalMcpPath(runtime.home));
886
892
  const states = runtime.mcp.states();
887
893
  const items = [
888
- ...CATALOG.map((c) => ({ name: c.name, description: c.description, installed: !!installed[c.name] })),
894
+ ...CATALOG.map((c) => ({ name: c.name, description: t(c.description), installed: !!installed[c.name] })),
889
895
  ...Object.keys(installed).filter((n) => !CATALOG.some((c) => c.name === n)).map((n) => ({ name: n, description: "(custom)", installed: true }))
890
896
  ].map((i) => {
891
897
  const s = states.find((st) => st.name === i.name);
@@ -897,7 +903,7 @@ function App({ runtime, initialPrompt, version }) {
897
903
  case "new":
898
904
  runtime.newSession();
899
905
  setTokens(0);
900
- notice("Sesi baru dimulai. Sesi sebelumnya tetap tersimpan (/resume).");
906
+ notice(t("New session started. The previous session stays saved (/resume)."));
901
907
  return;
902
908
  case "session":
903
909
  notice(
@@ -913,15 +919,15 @@ function App({ runtime, initialPrompt, version }) {
913
919
  case "resume": {
914
920
  const others = Session.list(runtime.home, runtime.cwd).filter((s) => s.session.file !== runtime.session.file);
915
921
  if (!others.length) {
916
- notice("Belum ada sesi lain di folder ini.");
922
+ notice(t("No other sessions in this folder yet."));
917
923
  return;
918
924
  }
919
925
  setListPicker({
920
- title: "Lanjutkan sesi",
926
+ title: t("Continue a session"),
921
927
  items: others.map((s) => ({
922
928
  id: s.session.file,
923
- label: `${s.mtime.toLocaleString("id-ID")} ${s.preview || "(kosong)"}`,
924
- hint: `${s.session.load().length} pesan`
929
+ label: `${s.mtime.toLocaleString(locale())} ${s.preview || t("(empty)")}`,
930
+ hint: t("{n} messages", { n: s.session.load().length })
925
931
  })),
926
932
  onPick: (id) => {
927
933
  setListPicker(null);
@@ -937,7 +943,7 @@ function App({ runtime, initialPrompt, version }) {
937
943
  entry({ kind: "header" }),
938
944
  entry({
939
945
  kind: "notice",
940
- text: `Melanjutkan sesi (${runtime.agent.messages.length} pesan tersimpan \xB7 ${chat.length} chat user/assistant ditampilkan).`,
946
+ text: t("Resuming the session ({messages} messages saved \xB7 {chat} user/assistant chats shown).", { messages: runtime.agent.messages.length, chat: chat.length }),
941
947
  tone: "info"
942
948
  }),
943
949
  ...chat.map(
@@ -971,7 +977,7 @@ function App({ runtime, initialPrompt, version }) {
971
977
  notice(skillsText(runtime.skills, runtime.commands));
972
978
  return;
973
979
  case "doctor": {
974
- notice("Memeriksa\u2026");
980
+ notice(t("Checking\u2026"));
975
981
  const c = runtime.config;
976
982
  notice(
977
983
  await doctorText(
@@ -992,20 +998,20 @@ function App({ runtime, initialPrompt, version }) {
992
998
  case "permissions": {
993
999
  const rules = runtime.agent.permissions.list();
994
1000
  if (!rules.length) {
995
- notice("Belum ada izin tersimpan. Izin dari jawaban [a] dan dari config akan muncul di sini.");
1001
+ notice(t("No saved permissions yet. Permissions from [a] answers and from the config will appear here."));
996
1002
  return;
997
1003
  }
998
1004
  setListPicker({
999
- title: "Izin aktif (enter: cabut izin sesi)",
1000
- items: rules.map((r) => ({ id: r.rule, label: r.rule, hint: r.source === "config" ? "config" : "sesi ini" })),
1005
+ title: t("Active permissions (enter: revoke a session permission)"),
1006
+ items: rules.map((r) => ({ id: r.rule, label: r.rule, hint: r.source === "config" ? "config" : t("this session") })),
1001
1007
  onPick: (id) => {
1002
1008
  setListPicker(null);
1003
1009
  const rule = rules.find((r) => r.rule === id);
1004
1010
  if (!rule) return;
1005
- if (rule.source === "config") notice(`${rule.rule} berasal dari config/flag; hapus dari ~/.bccli/config.json untuk mencabutnya.`, "warn");
1011
+ if (rule.source === "config") notice(t("{rule} comes from the config/flag; remove it from ~/.bccli/config.json to revoke it.", { rule: rule.rule }), "warn");
1006
1012
  else {
1007
1013
  runtime.agent.permissions.revoke(rule.rule);
1008
- notice(`Izin ${rule.rule} dicabut.`);
1014
+ notice(t("Permission {rule} revoked.", { rule: rule.rule }));
1009
1015
  }
1010
1016
  }
1011
1017
  });
@@ -1014,12 +1020,12 @@ function App({ runtime, initialPrompt, version }) {
1014
1020
  case "memory": {
1015
1021
  if (!args) {
1016
1022
  const files = instructionFiles(runtime.cwd, runtime.home);
1017
- notice(files.length ? files.map((f) => `${f.path} (${f.lines} baris)`).join("\n") : "Belum ada AGENTS.md / BCCLI.md. Tambah dengan /memory <teks>.");
1023
+ notice(files.length ? files.map((f) => t("{path} ({lines} lines)", { path: f.path, lines: f.lines })).join("\n") : t("No AGENTS.md / BCCLI.md yet. Add one with /memory <text>."));
1018
1024
  return;
1019
1025
  }
1020
1026
  const { global, text } = parseMemoryArgs(args);
1021
1027
  if (!text) {
1022
- notice("Tulis teksnya: /memory global <teks>.", "warn");
1028
+ notice(t("Write the text: /memory global <text>."), "warn");
1023
1029
  return;
1024
1030
  }
1025
1031
  const file = global ? join(runtime.home, "BCCLI.md") : join(runtime.cwd, "AGENTS.md");
@@ -1027,19 +1033,38 @@ function App({ runtime, initialPrompt, version }) {
1027
1033
  mkdirSync(dirname(file), { recursive: true });
1028
1034
  appendMemory(file, text);
1029
1035
  } catch (error) {
1030
- notice(`Gagal menulis ${file}: ${error.message}`, "error");
1036
+ notice(t("Could not write {file}: {error}", { file, error: error.message }), "error");
1031
1037
  return;
1032
1038
  }
1033
1039
  runtime.rebuildSystemPrompt();
1034
- notice(`Ditambahkan ke ${file}.`);
1040
+ notice(t("Added to {file}.", { file }));
1035
1041
  return;
1036
1042
  }
1037
1043
  case "init":
1038
1044
  void runTurn(INIT_PROMPT);
1039
1045
  return;
1046
+ case "language": {
1047
+ if (!args) {
1048
+ notice(t("Language: {lang}. Change it with /language en or /language id.", { lang: getLanguage() }));
1049
+ return;
1050
+ }
1051
+ const wanted = parseLang(args);
1052
+ if (!wanted) {
1053
+ notice(t('Unknown language "{lang}". Use en or id.', { lang: args }), "warn");
1054
+ return;
1055
+ }
1056
+ setLanguage(wanted);
1057
+ try {
1058
+ writeGlobalConfig({ language: wanted }, runtime.env);
1059
+ } catch (error) {
1060
+ notice(t("Could not save the language: {error}", { error: error.message }), "error");
1061
+ }
1062
+ notice(t("Language set to {lang} (saved as default).", { lang: wanted }));
1063
+ return;
1064
+ }
1040
1065
  case "login": {
1041
1066
  const id = runtime.modelRef.slice(0, runtime.modelRef.indexOf("/"));
1042
- const key = await ask(`API key untuk ${runtime.providerLabel()}`, true);
1067
+ const key = await ask(t("API key for {provider}", { provider: runtime.providerLabel() }), true);
1043
1068
  if (!key) return;
1044
1069
  const result = await runtime.addProviderKey(id, key);
1045
1070
  if (!result.ok) {
@@ -1049,10 +1074,10 @@ function App({ runtime, initialPrompt, version }) {
1049
1074
  runtime.setModel(runtime.modelRef);
1050
1075
  const envName = runtime.config.providers[id]?.apiKeyEnv;
1051
1076
  if (envName && runtime.env[envName]) {
1052
- notice(`Key ${runtime.providerLabel()} tersimpan, tapi ${envName} di environment lebih diutamakan dan tetap dipakai.`, "warn");
1077
+ notice(t("Key for {provider} saved, but {env} in the environment takes precedence and is still used.", { provider: runtime.providerLabel(), env: envName }), "warn");
1053
1078
  return;
1054
1079
  }
1055
- notice(`Key ${runtime.providerLabel()} tersimpan \xB7 ${result.models} model.`);
1080
+ notice(t("Key for {provider} saved \xB7 {n} models.", { provider: runtime.providerLabel(), n: result.models }));
1056
1081
  return;
1057
1082
  }
1058
1083
  case "logout": {
@@ -1061,13 +1086,13 @@ function App({ runtime, initialPrompt, version }) {
1061
1086
  const envName = runtime.config.providers[id]?.apiKeyEnv;
1062
1087
  if (envName && runtime.env[envName]) {
1063
1088
  notice(
1064
- `${removed ? "Key tersimpan dihapus, tapi" : "Tidak ada key tersimpan;"} ${envName} masih ada di environment dan tetap dipakai.`,
1089
+ removed ? t("Stored key deleted, but {env} is still set in the environment and is still used.", { env: envName }) : t("No stored key; {env} is still set in the environment and is still used.", { env: envName }),
1065
1090
  "warn"
1066
1091
  );
1067
1092
  return;
1068
1093
  }
1069
1094
  if (!removed) {
1070
- notice(`Tidak ada key tersimpan untuk ${runtime.providerLabel()}.`);
1095
+ notice(t("No stored key for {provider}.", { provider: runtime.providerLabel() }));
1071
1096
  return;
1072
1097
  }
1073
1098
  try {
@@ -1078,7 +1103,7 @@ function App({ runtime, initialPrompt, version }) {
1078
1103
  };
1079
1104
  runtime.agent.provider = { chat: fail, listModels: fail };
1080
1105
  }
1081
- notice(`Key ${runtime.providerLabel()} dihapus. Pakai /login atau /provider untuk menambah lagi.`);
1106
+ notice(t("Key for {provider} deleted. Use /login or /provider to add one again.", { provider: runtime.providerLabel() }));
1082
1107
  return;
1083
1108
  }
1084
1109
  case "diff":
@@ -1087,17 +1112,17 @@ function App({ runtime, initialPrompt, version }) {
1087
1112
  case "undo": {
1088
1113
  const result = await runtime.checkpoints.undo();
1089
1114
  if (!result) {
1090
- notice("Tidak ada edit file yang bisa dibatalkan.");
1115
+ notice(t("No file edits to undo."));
1091
1116
  return;
1092
1117
  }
1093
1118
  const rel = (p) => relative(runtime.cwd, p) || p;
1094
1119
  notice(
1095
1120
  [
1096
- result.failed.length ? `Gagal dikembalikan: ${result.failed.map(rel).join(", ")}` : "",
1097
- result.restored.length ? `Dikembalikan: ${result.restored.map(rel).join(", ")}` : "",
1098
- result.deleted.length ? `Dihapus (file baru): ${result.deleted.map(rel).join(", ")}` : "",
1099
- result.skipped.length ? `Terlalu besar untuk disimpan, tidak diubah: ${result.skipped.map(rel).join(", ")}` : "",
1100
- "Catatan: perubahan lewat perintah bash tidak bisa di-undo."
1121
+ result.failed.length ? t("Could not restore: {files}", { files: result.failed.map(rel).join(", ") }) : "",
1122
+ result.restored.length ? t("Restored: {files}", { files: result.restored.map(rel).join(", ") }) : "",
1123
+ result.deleted.length ? t("Deleted (new files): {files}", { files: result.deleted.map(rel).join(", ") }) : "",
1124
+ result.skipped.length ? t("Too large to back up, left unchanged: {files}", { files: result.skipped.map(rel).join(", ") }) : "",
1125
+ t("Note: changes made through bash commands cannot be undone.")
1101
1126
  ].filter(Boolean).join("\n")
1102
1127
  );
1103
1128
  return;
@@ -1105,33 +1130,33 @@ function App({ runtime, initialPrompt, version }) {
1105
1130
  case "copy": {
1106
1131
  const last = runtime.agent.messages.findLast((m) => m.role === "assistant" && typeof m.content === "string" && m.content.trim());
1107
1132
  if (!last || typeof last.content !== "string") {
1108
- notice("Belum ada jawaban untuk disalin.");
1133
+ notice(t("No answer to copy yet."));
1109
1134
  return;
1110
1135
  }
1111
1136
  process.stdout.write(osc52(last.content, !!process.env.TMUX));
1112
1137
  const file = join(runtime.home, "last-answer.md");
1113
1138
  mkdirSync(runtime.home, { recursive: true });
1114
1139
  writeFileSync(file, last.content);
1115
- notice(`Disalin ke clipboard (bila terminal mendukung OSC 52) dan disimpan di ${file}.`);
1140
+ notice(t("Copied to the clipboard (if the terminal supports OSC 52) and saved to {file}.", { file }));
1116
1141
  return;
1117
1142
  }
1118
1143
  case "export": {
1119
1144
  const name2 = args || `bccli-${(/* @__PURE__ */ new Date()).toISOString().slice(0, 16).replace(/[:T]/g, "-")}.md`;
1120
1145
  const file = resolve(runtime.cwd, name2);
1121
1146
  if (existsSync2(file)) {
1122
- const answer2 = await ask(`${file} sudah ada. Timpa? (y/N)`);
1147
+ const answer2 = await ask(t("{file} already exists. Overwrite? (y/N)", { file }));
1123
1148
  if (!answer2 || !/^y(a|es)?$/i.test(answer2.trim())) {
1124
- notice("Ekspor dibatalkan.");
1149
+ notice(t("Export cancelled."));
1125
1150
  return;
1126
1151
  }
1127
1152
  }
1128
1153
  try {
1129
- writeFileSync(file, exportMarkdown(runtime.agent.messages, `Sesi BCCLI ${runtime.startedAt.toLocaleString("id-ID")}`));
1154
+ writeFileSync(file, exportMarkdown(runtime.agent.messages, t("BCCLI session {when}", { when: runtime.startedAt.toLocaleString(locale()) })));
1130
1155
  } catch (error) {
1131
- notice(`Gagal menulis ${file}: ${error.message}`, "error");
1156
+ notice(t("Could not write {file}: {error}", { file, error: error.message }), "error");
1132
1157
  return;
1133
1158
  }
1134
- notice(`Percakapan disimpan di ${file}.`);
1159
+ notice(t("Conversation saved to {file}.", { file }));
1135
1160
  return;
1136
1161
  }
1137
1162
  case "provider": {
@@ -1142,7 +1167,7 @@ function App({ runtime, initialPrompt, version }) {
1142
1167
  return;
1143
1168
  }
1144
1169
  default:
1145
- notice(`Perintah tidak dikenal: /${name}. Ketik /help.`, "warn");
1170
+ notice(t("Unknown command: /{name}. Type /help.", { name }), "warn");
1146
1171
  }
1147
1172
  },
1148
1173
  [runtime, notice, exit, version, runTurn, ask]
@@ -1168,7 +1193,7 @@ function App({ runtime, initialPrompt, version }) {
1168
1193
  try {
1169
1194
  body = parseFrontmatter(readFileSync2(skill.file, "utf8")).body;
1170
1195
  } catch (error) {
1171
- notice(`Skill ${skill.name} tidak bisa dibaca: ${error.code ?? error.message}`, "error");
1196
+ notice(t("Skill {name} cannot be read: {error}", { name: skill.name, error: error.code ?? error.message }), "error");
1172
1197
  return;
1173
1198
  }
1174
1199
  void runTurn(`${body}
@@ -1207,27 +1232,28 @@ ARGUMENTS: ${slash.args}`);
1207
1232
  controller.current?.abort();
1208
1233
  if (queued.length) {
1209
1234
  setQueued([]);
1210
- notice(`${queued.length} pesan antrian dibatalkan.`, "warn");
1235
+ notice(t("{n} queued messages cancelled.", { n: queued.length }), "warn");
1211
1236
  }
1212
1237
  } else if (key.ctrl && input === "t") {
1213
1238
  const next = !showThinking;
1239
+ const toggled = next ? t("Thinking shown (ctrl+t to hide).") : t("Thinking hidden (ctrl+t to show).");
1214
1240
  setShowThinking(next);
1215
- setTranscript((t) => {
1216
- const last = [...t.done, ...t.live].findLast((e) => e.kind === "thinking");
1217
- const reprint = next && last?.kind === "thinking" && t.done.includes(last);
1241
+ setTranscript((t2) => {
1242
+ const last = [...t2.done, ...t2.live].findLast((e) => e.kind === "thinking");
1243
+ const reprint = next && last?.kind === "thinking" && t2.done.includes(last);
1218
1244
  return {
1219
- ...t,
1245
+ ...t2,
1220
1246
  done: [
1221
- ...t.done,
1247
+ ...t2.done,
1222
1248
  ...reprint ? [entry({ kind: "thinking", text: last.text, open: true })] : [],
1223
- entry({ kind: "notice", text: next ? "Thinking ditampilkan (ctrl+t untuk menutup)." : "Thinking disembunyikan (ctrl+t untuk membuka).", tone: "info" })
1249
+ entry({ kind: "notice", text: toggled, tone: "info" })
1224
1250
  ]
1225
1251
  };
1226
1252
  });
1227
1253
  } else if (key.ctrl && input === "o" && lastTool.current) {
1228
- const t = lastTool.current;
1229
- notice(`\u23BF ${t.tool} ${t.target}
1230
- ${t.output}`);
1254
+ const t2 = lastTool.current;
1255
+ notice(`\u23BF ${t2.tool} ${t2.target}
1256
+ ${t2.output}`);
1231
1257
  }
1232
1258
  });
1233
1259
  const pickMcp = async (name) => {
@@ -1237,24 +1263,24 @@ ${t.output}`);
1237
1263
  if (item.installed) {
1238
1264
  removeGlobalServer(runtime.home, item.name);
1239
1265
  await runtime.mcp.remove(item.name);
1240
- notice(`MCP ${item.name} dihapus.`);
1266
+ notice(t("MCP {name} removed.", { name: item.name }));
1241
1267
  return;
1242
1268
  }
1243
1269
  const entry2 = CATALOG.find((c) => c.name === item.name);
1244
1270
  if (!entry2) return;
1245
1271
  const values = {};
1246
1272
  for (const input of entry2.inputs ?? []) {
1247
- const v = await ask(input.label, !!input.secret);
1273
+ const v = await ask(t(input.label), !!input.secret);
1248
1274
  if (!v) return;
1249
1275
  values[input.key] = v;
1250
1276
  }
1251
1277
  const config = fillTemplate(entry2.config, values);
1252
1278
  addGlobalServer(runtime.home, entry2.name, config);
1253
- notice(`MCP ${entry2.name} dipasang, menghubungkan\u2026`);
1279
+ notice(t("MCP {name} installed, connecting\u2026", { name: entry2.name }));
1254
1280
  await runtime.mcp.add({ name: entry2.name, config, source: "global" });
1255
1281
  const state = runtime.mcp.states().find((s) => s.name === entry2.name);
1256
- if (state?.status === "ready") notice(`MCP ${entry2.name} aktif \xB7 ${state.tools} alat.`);
1257
- else notice(`MCP ${entry2.name} gagal: ${state?.error ?? "tidak diketahui"}`, "error");
1282
+ if (state?.status === "ready") notice(t("MCP {name} active \xB7 {n} tools.", { name: entry2.name, n: state.tools }));
1283
+ else notice(t("MCP {name} failed: {error}", { name: entry2.name, error: state?.error ?? t("unknown") }), "error");
1258
1284
  };
1259
1285
  const pickProvider = async (id) => {
1260
1286
  setProviderMenu(null);
@@ -1271,7 +1297,7 @@ ${t.output}`);
1271
1297
  const c = runtime.config;
1272
1298
  const keyless = !c.providers[target].apiKeyEnv && PRESETS.some((p) => p.id === target);
1273
1299
  if (!hasKey(c, target, runtime.env) || id === "custom") {
1274
- const key = keyless ? "" : await ask(`API key untuk ${providerName(c, target)}`, true);
1300
+ const key = keyless ? "" : await ask(t("API key for {provider}", { provider: providerName(c, target) }), true);
1275
1301
  if (key === void 0) return;
1276
1302
  const result = await runtime.addProviderKey(target, key);
1277
1303
  if (!result.ok) {
@@ -1312,10 +1338,10 @@ ${t.output}`);
1312
1338
  if (!text) return null;
1313
1339
  if (!(e.open ?? showThinking)) {
1314
1340
  const lines = text.split("\n").length;
1315
- return /* @__PURE__ */ jsx15(Box13, { marginTop: 1, children: /* @__PURE__ */ jsx15(Text15, { dimColor: true, children: `\u273B Berpikir \xB7 ${lines} baris \xB7 ctrl+t buka` }) }, e.id);
1341
+ return /* @__PURE__ */ jsx15(Box13, { marginTop: 1, children: /* @__PURE__ */ jsx15(Text15, { dimColor: true, children: t("\u273B Thinking \xB7 {n} lines \xB7 ctrl+t to show", { n: lines }) }) }, e.id);
1316
1342
  }
1317
1343
  return /* @__PURE__ */ jsxs10(Box13, { marginTop: 1, flexDirection: "column", children: [
1318
- /* @__PURE__ */ jsx15(Text15, { dimColor: true, children: "\u273B Berpikir (ctrl+t tutup)" }),
1344
+ /* @__PURE__ */ jsx15(Text15, { dimColor: true, children: t("\u273B Thinking (ctrl+t to hide)") }),
1319
1345
  /* @__PURE__ */ jsx15(Box13, { paddingLeft: 2, children: /* @__PURE__ */ jsx15(Text15, { dimColor: true, italic: true, children: text }) })
1320
1346
  ] }, e.id);
1321
1347
  }
@@ -1328,7 +1354,7 @@ ${t.output}`);
1328
1354
  return /* @__PURE__ */ jsxs10(Box13, { flexDirection: "column", children: [
1329
1355
  /* @__PURE__ */ jsx15(Static, { items: transcript.done, children: renderEntry }),
1330
1356
  transcript.live.map(renderEntry),
1331
- busy && !pending && !planAsk ? /* @__PURE__ */ jsx15(Box13, { marginTop: 1, children: /* @__PURE__ */ jsx15(Spinner, { label: "Berpikir", startedAt }) }) : null,
1357
+ busy && !pending && !planAsk ? /* @__PURE__ */ jsx15(Box13, { marginTop: 1, children: /* @__PURE__ */ jsx15(Spinner, { label: t("Thinking"), startedAt }) }) : null,
1332
1358
  pending ? /* @__PURE__ */ jsx15(PermissionPrompt, { request: pending.request, onAnswer: answer }, pending.id) : null,
1333
1359
  picker ? /* @__PURE__ */ jsx15(
1334
1360
  ModelPicker,
@@ -1371,7 +1397,7 @@ ${t.output}`);
1371
1397
  }
1372
1398
  ) : null,
1373
1399
  /* @__PURE__ */ jsx15(TodoList, { items: todos }),
1374
- queued.map((q, i) => /* @__PURE__ */ jsx15(Text15, { dimColor: true, children: ` \u23F3 antri: ${q}` }, `${i}-${q}`)),
1400
+ queued.map((q, i) => /* @__PURE__ */ jsx15(Text15, { dimColor: true, children: t(" \u23F3 queued: {q}", { q }) }, `${i}-${q}`)),
1375
1401
  /* @__PURE__ */ jsx15(PromptInput, { disabled: !!pending || !!picker || !!providerMenu || !!prompt || !!planAsk || !!mcpMenu || !!listPicker, history, cwd: runtime.cwd, onSubmit: onPrompt, extraCommands }),
1376
1402
  /* @__PURE__ */ jsx15(StatusBar, { mode, tokens, busy, model: modelLabel, reasoning })
1377
1403
  ] });
@@ -1383,27 +1409,27 @@ async function startInteractive(rt, opts) {
1383
1409
  if (opts.resume) {
1384
1410
  const { Session: Session2 } = await import("./session-7UB77CEG.js");
1385
1411
  const list = Session2.list(rt.home, rt.cwd).slice(0, 20);
1386
- if (!list.length) console.log("Belum ada sesi di folder ini; memulai sesi baru.");
1412
+ if (!list.length) console.log(t("No sessions in this folder yet; starting a new one."));
1387
1413
  else {
1388
1414
  list.forEach((s, i) => {
1389
- console.log(`${String(i + 1).padStart(2)}. ${s.mtime.toLocaleString("id-ID")} ${s.preview}`);
1415
+ console.log(`${String(i + 1).padStart(2)}. ${s.mtime.toLocaleString(locale())} ${s.preview}`);
1390
1416
  });
1391
1417
  const { createInterface } = await import("readline/promises");
1392
1418
  const rl = createInterface({ input: process.stdin, output: process.stdout });
1393
- const answer = Number(await rl.question("Nomor sesi (enter = baru): "));
1419
+ const answer = Number(await rl.question(t("Session number (enter = new): ")));
1394
1420
  rl.close();
1395
1421
  const chosen = list[answer - 1];
1396
1422
  if (chosen) rt.resume(chosen.session);
1397
1423
  }
1398
1424
  }
1399
- const { serversToStart, setProjectTrust } = await import("./config-YUNKPCNB.js");
1425
+ const { serversToStart, setProjectTrust } = await import("./config-CGYYFECG.js");
1400
1426
  const plan = serversToStart(rt.home, rt.cwd);
1401
1427
  if (plan.needTrust.length) {
1402
1428
  const { createInterface } = await import("readline/promises");
1403
1429
  const rl = createInterface({ input: process.stdin, output: process.stdout });
1404
1430
  for (const s of plan.needTrust) {
1405
1431
  const what = s.config.type === "http" ? s.config.url : [s.config.command, ...s.config.args ?? []].join(" ");
1406
- const answer = (await rl.question(`Repo ini ingin menjalankan server MCP "${s.name}" (${what}). Izinkan? [y/N] `)).trim().toLowerCase();
1432
+ const answer = (await rl.question(t('This repo wants to run the MCP server "{name}" ({what}). Allow? [y/N] ', { name: s.name, what }))).trim().toLowerCase();
1407
1433
  setProjectTrust(rt.home, rt.cwd, s.name, answer === "y", s.config);
1408
1434
  if (answer === "y") plan.start.push(s);
1409
1435
  }