@botconnector/bccli 0.3.0-beta.2 → 0.4.0-beta.1

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.
@@ -1,17 +1,22 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
+ BUILTIN_AGENTS,
3
4
  ignorePatterns,
5
+ instructionPaths,
4
6
  parseFrontmatter,
5
7
  toolLabel
6
- } from "./chunk-TBPPXOOZ.js";
8
+ } from "./chunk-OSWQPSA4.js";
7
9
  import {
8
10
  nextMode
9
- } from "./chunk-5XN44Q55.js";
11
+ } from "./chunk-ELML4KVQ.js";
10
12
  import {
11
13
  hasKey,
12
14
  providerName,
13
15
  writeGlobalConfig
14
16
  } from "./chunk-5C6NQFSI.js";
17
+ import {
18
+ Session
19
+ } from "./chunk-SJH3YLW4.js";
15
20
  import {
16
21
  CATALOG,
17
22
  fillTemplate
@@ -23,17 +28,20 @@ import {
23
28
  removeGlobalServer
24
29
  } from "./chunk-XQLXE666.js";
25
30
  import {
26
- PRESETS
31
+ PRESETS,
32
+ removeCredential
27
33
  } from "./chunk-Z5JM76EL.js";
28
34
 
29
35
  // src/ui/index.tsx
30
36
  import { render } from "ink";
31
37
 
32
38
  // src/ui/App.tsx
39
+ import { spawnSync } from "child_process";
40
+ import { mkdirSync, readFileSync as readFileSync2, writeFileSync } from "fs";
33
41
  import { homedir } from "os";
34
- import { Box as Box12, Static, Text as Text14, useApp, useInput as useInput8 } from "ink";
35
- import { useCallback, useEffect as useEffect2, useRef, useState as useState7 } from "react";
36
- import { readFileSync } from "fs";
42
+ import { dirname, join, relative, resolve } from "path";
43
+ import { Box as Box13, Static, Text as Text15, useApp, useInput as useInput9 } from "ink";
44
+ import { useCallback, useEffect as useEffect2, useRef, useState as useState8 } from "react";
37
45
 
38
46
  // src/commands.ts
39
47
  var SLASH_COMMANDS = [
@@ -41,6 +49,22 @@ var SLASH_COMMANDS = [
41
49
  { name: "model", description: "ganti model" },
42
50
  { name: "provider", description: "tambah/pilih provider AI" },
43
51
  { name: "mcp", description: "pasang/kelola server MCP" },
52
+ { name: "new", description: "sesi baru (sesi lama tetap tersimpan)" },
53
+ { name: "resume", description: "lanjutkan sesi lain di folder ini" },
54
+ { name: "session", description: "info sesi ini" },
55
+ { name: "status", description: "versi, model, mode izin, MCP, konteks" },
56
+ { name: "permissions", description: "lihat/cabut izin" },
57
+ { name: "undo", description: "batalkan edit file giliran terakhir" },
58
+ { name: "diff", description: "git diff project" },
59
+ { name: "copy", description: "salin jawaban terakhir" },
60
+ { name: "export", description: "simpan percakapan ke markdown" },
61
+ { name: "memory", description: "instruksi project (/memory <teks>, /memory global <teks>)" },
62
+ { name: "init", description: "buat/perbarui AGENTS.md untuk project ini" },
63
+ { name: "agents", description: "daftar subagent" },
64
+ { name: "skills", description: "daftar skill dan perintah custom" },
65
+ { name: "doctor", description: "cek kesehatan instalasi" },
66
+ { name: "login", description: "simpan API key provider aktif" },
67
+ { name: "logout", description: "hapus API key provider aktif" },
44
68
  { name: "clear", description: "mulai percakapan baru" },
45
69
  { name: "compact", description: "ringkas percakapan" },
46
70
  { name: "cost", description: "pemakaian token sesi ini" },
@@ -58,6 +82,111 @@ function expandCommand(def, args) {
58
82
  ${args}` : def.body;
59
83
  }
60
84
 
85
+ // src/slash/copy.ts
86
+ function osc52(text, inTmux) {
87
+ const seq = `\x1B]52;c;${Buffer.from(text).toString("base64")}\x07`;
88
+ return inTmux ? `\x1BPtmux;${seq.replaceAll("\x1B", "\x1B\x1B")}\x1B\\` : seq;
89
+ }
90
+
91
+ // src/slash/diff.ts
92
+ import { execFile } from "child_process";
93
+ function git(cwd, args) {
94
+ return new Promise((resolve2) => {
95
+ execFile("git", args, { cwd, maxBuffer: 20 * 1024 * 1024 }, (error, stdout, stderr) => resolve2({ ok: !error, out: error ? stderr : stdout }));
96
+ });
97
+ }
98
+ async function gitDiff(cwd, maxLines = 400) {
99
+ const inside = await git(cwd, ["rev-parse", "--is-inside-work-tree"]);
100
+ if (!inside.ok || inside.out.trim() !== "true") return "Folder ini bukan repository git, jadi tidak ada diff.";
101
+ const base = (await git(cwd, ["rev-parse", "--verify", "-q", "HEAD"])).ok ? "HEAD" : "4b825dc642cb6eb9a060e54bf8d69288fbee4904";
102
+ const [stat, diff, untracked] = await Promise.all([
103
+ git(cwd, ["diff", "--stat", base]),
104
+ git(cwd, ["diff", base]),
105
+ git(cwd, ["ls-files", "--others", "--exclude-standard"])
106
+ ]);
107
+ const fresh = untracked.out.split("\n").filter(Boolean);
108
+ if (!diff.out.trim() && !fresh.length) return "Tidak ada perubahan yang belum di-commit.";
109
+ const lines = diff.out.trimEnd().split("\n");
110
+ const more = lines.length > maxLines ? `
111
+ \u2026 ${lines.length - maxLines} baris lagi` : "";
112
+ 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");
113
+ }
114
+
115
+ // src/slash/doctor.ts
116
+ async function doctorText(input, deps) {
117
+ const major = Number(deps.nodeVersion.replace(/^v/, "").split(".")[0]);
118
+ const lines = [major >= 22 ? `\u2713 Node ${deps.nodeVersion}` : `\u2717 Node ${deps.nodeVersion} (butuh \u2265 22)`];
119
+ for (const p of input.providers) lines.push(p.ready ? `\u2713 ${p.name}` : `\u25CB ${p.name} (belum ada key)`);
120
+ try {
121
+ lines.push(`\u2713 Koneksi ${input.activeProvider} (${(await deps.listModels()).length} model)`);
122
+ } catch (error) {
123
+ lines.push(`\u2717 Koneksi ${input.activeProvider}: ${error.message}`);
124
+ }
125
+ for (const m of input.mcp) lines.push(m.status === "error" ? `\u2717 MCP ${m.name}: ${m.error ?? ""}` : `\u2713 MCP ${m.name} (${m.status})`);
126
+ for (const cmd of ["rg", "git", "npx", "uvx"]) lines.push(`${deps.which(cmd) ? "\u2713" : "\u2717"} ${cmd}`);
127
+ return lines.join("\n");
128
+ }
129
+
130
+ // src/slash/export.ts
131
+ function exportMarkdown(messages, title) {
132
+ const parts = [`# ${title}`, ""];
133
+ for (const m of messages) {
134
+ if (m.role === "user") parts.push("## User", "", String(m.content), "");
135
+ else if (m.role === "assistant") {
136
+ for (const c of m.tool_calls ?? []) parts.push(`\u23BF ${c.function.name} ${c.function.arguments.slice(0, 200)}`, "");
137
+ if (m.content) parts.push("## BCCLI", "", m.content, "");
138
+ }
139
+ }
140
+ return parts.join("\n");
141
+ }
142
+
143
+ // src/slash/info.ts
144
+ var k = (n) => n >= 1e3 ? `${Math.round(n / 1e3)}k` : String(n);
145
+ function statusText(s) {
146
+ const ready = s.mcp.filter((m) => m.status === "ready").length;
147
+ const errors = s.mcp.filter((m) => m.status === "error").length;
148
+ return [
149
+ `BCCLI ${s.version}`,
150
+ `Model: ${s.modelRef} (${s.providerLabel})`,
151
+ `Mode izin: ${s.mode}`,
152
+ `Folder: ${s.cwd}`,
153
+ `MCP: ${ready} aktif, ${errors} error`,
154
+ `Token sesi: ${k(s.usage.inputTokens)} masuk \xB7 ${k(s.usage.outputTokens)} keluar`,
155
+ `Konteks: \xB1${Math.round(s.lastInputTokens / 128e3 * 100)}% dari 128k`
156
+ ].join("\n");
157
+ }
158
+ function agentsText(defs) {
159
+ 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");
160
+ }
161
+ function skillsText(skills, commands) {
162
+ const s = skills.length ? skills.map((x) => `${x.name} \u2014 ${x.description.slice(0, 80)} (${x.dir})`).join("\n") : "(tidak ada skill)";
163
+ const c = commands.length ? commands.map((x) => `/${x.name} \u2014 ${x.description ?? ""}`).join("\n") : "(tidak ada perintah custom)";
164
+ return `Skill:
165
+ ${s}
166
+
167
+ Perintah custom:
168
+ ${c}`;
169
+ }
170
+ function sessionText(s) {
171
+ return [
172
+ `Mulai: ${s.started.toLocaleString("id-ID")}`,
173
+ `${s.messages} pesan \xB7 ${k(s.usage.inputTokens)} token masuk \xB7 ${k(s.usage.outputTokens)} keluar`,
174
+ `Model: ${s.modelRef}`,
175
+ `File: ${s.file}`
176
+ ].join("\n");
177
+ }
178
+
179
+ // src/slash/memory.ts
180
+ import { appendFileSync, existsSync, readFileSync } from "fs";
181
+ function instructionFiles(cwd, home) {
182
+ return instructionPaths(cwd, home).filter((path) => existsSync(path)).map((path) => ({ path, lines: readFileSync(path, "utf8").split("\n").filter(Boolean).length }));
183
+ }
184
+ function appendMemory(file, text) {
185
+ const prefix = existsSync(file) && !readFileSync(file, "utf8").endsWith("\n") ? "\n" : "";
186
+ appendFileSync(file, `${prefix}- ${text.trim()}
187
+ `);
188
+ }
189
+
61
190
  // src/ui/Markdown.tsx
62
191
  import { Box, Text } from "ink";
63
192
 
@@ -116,14 +245,48 @@ function LinePrompt({ label, mask, onSubmit, onCancel }) {
116
245
  ] }) });
117
246
  }
118
247
 
119
- // src/ui/ModelPicker.tsx
248
+ // src/ui/ListPicker.tsx
120
249
  import { Box as Box3, Text as Text3, useInput as useInput2 } from "ink";
121
250
  import { useState as useState2 } from "react";
122
251
  import { jsx as jsx3, jsxs as jsxs2 } from "react/jsx-runtime";
123
- var VISIBLE = 14;
124
- function ModelPicker({ groups, current, onPick }) {
252
+ var VISIBLE = 12;
253
+ function ListPicker({ title, items, onPick }) {
125
254
  const [filter, setFilter] = useState2("");
126
255
  const [index, setIndex] = useState2(0);
256
+ const shown = items.filter((i) => i.label.toLowerCase().includes(filter.toLowerCase()));
257
+ const cursor = Math.min(index, Math.max(0, shown.length - 1));
258
+ useInput2((input, key) => {
259
+ if (key.upArrow) setIndex(Math.max(0, cursor - 1));
260
+ else if (key.downArrow) setIndex(Math.min(shown.length - 1, cursor + 1));
261
+ else if (key.return) onPick(shown[cursor]?.id);
262
+ else if (key.escape) onPick(void 0);
263
+ else if (key.backspace || key.delete) {
264
+ setFilter((f) => f.slice(0, -1));
265
+ setIndex(0);
266
+ } else if (!key.ctrl && !key.meta && !key.tab && input) {
267
+ setFilter((f) => f + input);
268
+ setIndex(0);
269
+ }
270
+ });
271
+ const start = Math.max(0, Math.min(cursor - 5, shown.length - VISIBLE));
272
+ return /* @__PURE__ */ jsxs2(Box3, { flexDirection: "column", borderStyle: "round", borderColor: color("cyan"), paddingX: 1, children: [
273
+ /* @__PURE__ */ jsx3(Text3, { bold: true, children: `${title} (\u2191\u2193 enter \xB7 ketik untuk menyaring${filter ? `: ${filter}` : ""} \xB7 esc batal)` }),
274
+ shown.slice(start, start + VISIBLE).map((item, i) => /* @__PURE__ */ jsxs2(Text3, { color: start + i === cursor ? color("green") : void 0, children: [
275
+ `${start + i === cursor ? "\u203A" : " "} ${item.label}`,
276
+ item.hint ? /* @__PURE__ */ jsx3(Text3, { dimColor: true, children: ` ${item.hint}` }) : null
277
+ ] }, item.id)),
278
+ shown.length ? null : /* @__PURE__ */ jsx3(Text3, { dimColor: true, children: "Tidak ada yang cocok." })
279
+ ] });
280
+ }
281
+
282
+ // src/ui/ModelPicker.tsx
283
+ import { Box as Box4, Text as Text4, useInput as useInput3 } from "ink";
284
+ import { useState as useState3 } from "react";
285
+ import { jsx as jsx4, jsxs as jsxs3 } from "react/jsx-runtime";
286
+ var VISIBLE2 = 14;
287
+ function ModelPicker({ groups, current, onPick }) {
288
+ const [filter, setFilter] = useState3("");
289
+ const [index, setIndex] = useState3(0);
127
290
  const needle = filter.toLowerCase();
128
291
  const rows = [];
129
292
  for (const g of groups) {
@@ -135,7 +298,7 @@ function ModelPicker({ groups, current, onPick }) {
135
298
  }
136
299
  const selectable = rows.flatMap((r, i) => r.kind === "model" ? [i] : []);
137
300
  const cursor = selectable[Math.min(index, selectable.length - 1)] ?? -1;
138
- useInput2((input, key) => {
301
+ useInput3((input, key) => {
139
302
  if (key.upArrow) setIndex((i) => Math.max(0, i - 1));
140
303
  else if (key.downArrow) setIndex((i) => Math.min(selectable.length - 1, i + 1));
141
304
  else if (key.return) {
@@ -150,86 +313,86 @@ function ModelPicker({ groups, current, onPick }) {
150
313
  setIndex(0);
151
314
  }
152
315
  });
153
- const start = Math.max(0, Math.min(cursor - 5, rows.length - VISIBLE));
154
- return /* @__PURE__ */ jsxs2(Box3, { flexDirection: "column", borderStyle: "round", borderColor: color("cyan"), paddingX: 1, children: [
155
- /* @__PURE__ */ jsx3(Text3, { bold: true, children: `Pilih model (ketik untuk menyaring${filter ? `: ${filter}` : ""} \xB7 \u2191\u2193 enter \xB7 esc batal)` }),
156
- rows.slice(start, start + VISIBLE).map(
157
- (row, i) => row.kind === "header" ? /* @__PURE__ */ jsx3(Text3, { dimColor: true, children: `\u2500\u2500 ${row.text} ` }, `h${start + i}`) : /* @__PURE__ */ jsx3(Text3, { color: start + i === cursor ? color("green") : void 0, children: `${start + i === cursor ? "\u203A" : " "} ${row.label}${row.ref === current ? " (aktif)" : ""}` }, row.ref)
316
+ const start = Math.max(0, Math.min(cursor - 5, rows.length - VISIBLE2));
317
+ return /* @__PURE__ */ jsxs3(Box4, { flexDirection: "column", borderStyle: "round", borderColor: color("cyan"), paddingX: 1, children: [
318
+ /* @__PURE__ */ jsx4(Text4, { bold: true, children: `Pilih model (ketik untuk menyaring${filter ? `: ${filter}` : ""} \xB7 \u2191\u2193 enter \xB7 esc batal)` }),
319
+ rows.slice(start, start + VISIBLE2).map(
320
+ (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)
158
321
  ),
159
- !selectable.length ? /* @__PURE__ */ jsx3(Text3, { dimColor: true, children: "Tidak ada model yang cocok." }) : null,
160
- groups.filter((g) => g.error).map((g) => /* @__PURE__ */ jsx3(Text3, { color: color("yellow"), children: `${g.providerName} \u2014 ${g.error}` }, `e-${g.providerId}`))
322
+ !selectable.length ? /* @__PURE__ */ jsx4(Text4, { dimColor: true, children: "Tidak ada model yang cocok." }) : null,
323
+ groups.filter((g) => g.error).map((g) => /* @__PURE__ */ jsx4(Text4, { color: color("yellow"), children: `${g.providerName} \u2014 ${g.error}` }, `e-${g.providerId}`))
161
324
  ] });
162
325
  }
163
326
 
164
327
  // src/ui/ProviderMenu.tsx
165
- import { Box as Box4, Text as Text4, useInput as useInput3 } from "ink";
166
- import { useState as useState3 } from "react";
167
- import { jsx as jsx4, jsxs as jsxs3 } from "react/jsx-runtime";
328
+ import { Box as Box5, Text as Text5, useInput as useInput4 } from "ink";
329
+ import { useState as useState4 } from "react";
330
+ import { jsx as jsx5, jsxs as jsxs4 } from "react/jsx-runtime";
168
331
  function ProviderMenu({ entries, onPick }) {
169
332
  const rows = [...entries.map((e) => e.id), "custom"];
170
- const [index, setIndex] = useState3(0);
171
- useInput3((_input, key) => {
333
+ const [index, setIndex] = useState4(0);
334
+ useInput4((_input, key) => {
172
335
  if (key.upArrow) setIndex((i) => Math.max(0, i - 1));
173
336
  else if (key.downArrow) setIndex((i) => Math.min(rows.length - 1, i + 1));
174
337
  else if (key.return) onPick(rows[index]);
175
338
  else if (key.escape) onPick(void 0);
176
339
  });
177
- return /* @__PURE__ */ jsxs3(Box4, { flexDirection: "column", borderStyle: "round", borderColor: color("cyan"), paddingX: 1, children: [
178
- /* @__PURE__ */ jsx4(Text4, { bold: true, children: "Provider (\u2191\u2193 enter, esc batal) \xB7 \u2713 siap \xB7 \u25CB butuh API key" }),
179
- entries.map((e, i) => /* @__PURE__ */ jsxs3(Text4, { color: i === index ? color("green") : void 0, children: [
340
+ return /* @__PURE__ */ jsxs4(Box5, { flexDirection: "column", borderStyle: "round", borderColor: color("cyan"), paddingX: 1, children: [
341
+ /* @__PURE__ */ jsx5(Text5, { bold: true, children: "Provider (\u2191\u2193 enter, esc batal) \xB7 \u2713 siap \xB7 \u25CB butuh API key" }),
342
+ entries.map((e, i) => /* @__PURE__ */ jsxs4(Text5, { color: i === index ? color("green") : void 0, children: [
180
343
  `${i === index ? "\u203A" : " "} ${e.ready ? "\u2713" : "\u25CB"} ${e.name}`,
181
- /* @__PURE__ */ jsx4(Text4, { dimColor: true, children: ` ${e.baseURL}` })
344
+ /* @__PURE__ */ jsx5(Text5, { dimColor: true, children: ` ${e.baseURL}` })
182
345
  ] }, e.id)),
183
- /* @__PURE__ */ jsx4(Text4, { color: index === rows.length - 1 ? color("green") : void 0, children: `${index === rows.length - 1 ? "\u203A" : " "} Custom\u2026` })
346
+ /* @__PURE__ */ jsx5(Text5, { color: index === rows.length - 1 ? color("green") : void 0, children: `${index === rows.length - 1 ? "\u203A" : " "} Custom\u2026` })
184
347
  ] });
185
348
  }
186
349
 
187
350
  // src/ui/PermissionPrompt.tsx
188
- import { Box as Box6, Text as Text6, useInput as useInput4 } from "ink";
351
+ import { Box as Box7, Text as Text7, useInput as useInput5 } from "ink";
189
352
 
190
353
  // src/ui/DiffView.tsx
191
- import { Box as Box5, Text as Text5 } from "ink";
192
- import { jsx as jsx5 } from "react/jsx-runtime";
354
+ import { Box as Box6, Text as Text6 } from "ink";
355
+ import { jsx as jsx6 } from "react/jsx-runtime";
193
356
  function DiffView({ diff }) {
194
- return /* @__PURE__ */ jsx5(Box5, { flexDirection: "column", children: diff.split("\n").map((line, i) => {
357
+ return /* @__PURE__ */ jsx6(Box6, { flexDirection: "column", children: diff.split("\n").map((line, i) => {
195
358
  const mark = line[6];
196
359
  const c = mark === "+" ? color("green") : mark === "-" ? color("red") : void 0;
197
- return /* @__PURE__ */ jsx5(Text5, { color: c, dimColor: mark !== "+" && mark !== "-", children: line }, i);
360
+ return /* @__PURE__ */ jsx6(Text6, { color: c, dimColor: mark !== "+" && mark !== "-", children: line }, i);
198
361
  }) });
199
362
  }
200
363
 
201
364
  // src/ui/PermissionPrompt.tsx
202
- import { jsx as jsx6, jsxs as jsxs4 } from "react/jsx-runtime";
365
+ import { jsx as jsx7, jsxs as jsxs5 } from "react/jsx-runtime";
203
366
  var VERB = { edit: "edit", write: "tulis", bash: "jalankan", fetch: "ambil" };
204
367
  function PermissionPrompt({
205
368
  request,
206
369
  onAnswer
207
370
  }) {
208
- useInput4((input, key) => {
209
- const k = input.toLowerCase();
210
- if (k === "y") onAnswer("yes");
211
- else if (k === "a" && request.sessionRules?.length) onAnswer("session");
212
- else if (k === "s") onAnswer("all");
213
- else if (k === "n" || key.escape) onAnswer("no");
371
+ useInput5((input, key) => {
372
+ const k2 = input.toLowerCase();
373
+ if (k2 === "y") onAnswer("yes");
374
+ else if (k2 === "a" && request.sessionRules?.length) onAnswer("session");
375
+ else if (k2 === "s") onAnswer("all");
376
+ else if (k2 === "n" || key.escape) onAnswer("no");
214
377
  });
215
378
  const rules = request.sessionRules ?? [];
216
379
  const sessionLabel = !rules.length ? null : request.kind === "edit" ? "ya untuk semua edit di project ini, sesi ini" : `ya sesi ini untuk ${rules.join(", ")}`;
217
- return /* @__PURE__ */ jsxs4(Box6, { flexDirection: "column", borderStyle: "round", borderColor: color("yellow"), paddingX: 1, children: [
218
- /* @__PURE__ */ jsx6(Text6, { bold: true, children: `${request.agent ? `[${request.agent}] ` : ""}Izinkan ${VERB[request.tool] ?? request.tool} ${request.target}?` }),
219
- request.preview ? /* @__PURE__ */ jsx6(DiffView, { diff: request.preview }) : null,
220
- /* @__PURE__ */ jsxs4(Text6, { children: [
221
- /* @__PURE__ */ jsx6(Text6, { color: color("green"), children: "[y] ya" }),
380
+ return /* @__PURE__ */ jsxs5(Box7, { flexDirection: "column", borderStyle: "round", borderColor: color("yellow"), paddingX: 1, children: [
381
+ /* @__PURE__ */ jsx7(Text7, { bold: true, children: `${request.agent ? `[${request.agent}] ` : ""}Izinkan ${VERB[request.tool] ?? request.tool} ${request.target}?` }),
382
+ request.preview ? /* @__PURE__ */ jsx7(DiffView, { diff: request.preview }) : null,
383
+ /* @__PURE__ */ jsxs5(Text7, { children: [
384
+ /* @__PURE__ */ jsx7(Text7, { color: color("green"), children: "[y] ya" }),
222
385
  " ",
223
- sessionLabel ? /* @__PURE__ */ jsx6(Text6, { color: color("cyan"), children: `[a] ${sessionLabel} ` }) : null,
224
- /* @__PURE__ */ jsx6(Text6, { color: color("yellow"), children: "[s] ya semua " }),
225
- /* @__PURE__ */ jsx6(Text6, { color: color("red"), children: "[n] tidak" })
386
+ sessionLabel ? /* @__PURE__ */ jsx7(Text7, { color: color("cyan"), children: `[a] ${sessionLabel} ` }) : null,
387
+ /* @__PURE__ */ jsx7(Text7, { color: color("yellow"), children: "[s] ya semua " }),
388
+ /* @__PURE__ */ jsx7(Text7, { color: color("red"), children: "[n] tidak" })
226
389
  ] })
227
390
  ] });
228
391
  }
229
392
 
230
393
  // src/ui/PromptInput.tsx
231
- import { Box as Box7, Text as Text7, useInput as useInput5 } from "ink";
232
- import { useState as useState4 } from "react";
394
+ import { Box as Box8, Text as Text8, useInput as useInput6 } from "ink";
395
+ import { useState as useState5 } from "react";
233
396
 
234
397
  // src/ui/complete.ts
235
398
  import fg from "fast-glob";
@@ -245,11 +408,11 @@ function completeFile(value, cwd) {
245
408
  }
246
409
 
247
410
  // src/ui/PromptInput.tsx
248
- import { jsx as jsx7, jsxs as jsxs5 } from "react/jsx-runtime";
411
+ import { jsx as jsx8, jsxs as jsxs6 } from "react/jsx-runtime";
249
412
  function PromptInput({ disabled, history, cwd, onSubmit, extraCommands }) {
250
- const [value, setValue] = useState4("");
251
- const [historyIndex, setHistoryIndex] = useState4(null);
252
- const [selected, setSelected] = useState4(0);
413
+ const [value, setValue] = useState5("");
414
+ const [historyIndex, setHistoryIndex] = useState5(null);
415
+ const [selected, setSelected] = useState5(0);
253
416
  const all = [...SLASH_COMMANDS, ...(extraCommands ?? []).filter((c) => !SLASH_COMMANDS.some((b) => b.name === c.name))];
254
417
  const suggestions = value.startsWith("/") && !/\s/.test(value) ? all.filter((c) => c.name.startsWith(value.slice(1))).slice(0, 8) : [];
255
418
  const highlighted = Math.min(selected, Math.max(0, suggestions.length - 1));
@@ -257,7 +420,7 @@ function PromptInput({ disabled, history, cwd, onSubmit, extraCommands }) {
257
420
  setValue(next);
258
421
  setSelected(0);
259
422
  };
260
- useInput5(
423
+ useInput6(
261
424
  (input, key) => {
262
425
  if (suggestions.length) {
263
426
  if (key.upArrow) {
@@ -319,34 +482,34 @@ function PromptInput({ disabled, history, cwd, onSubmit, extraCommands }) {
319
482
  },
320
483
  { isActive: !disabled }
321
484
  );
322
- return /* @__PURE__ */ jsxs5(Box7, { flexDirection: "column", children: [
323
- /* @__PURE__ */ jsx7(Box7, { borderStyle: "round", borderColor: color("gray"), paddingX: 1, children: /* @__PURE__ */ jsxs5(Text7, { children: [
324
- /* @__PURE__ */ jsx7(Text7, { color: color("green"), children: "> " }),
485
+ return /* @__PURE__ */ jsxs6(Box8, { flexDirection: "column", children: [
486
+ /* @__PURE__ */ jsx8(Box8, { borderStyle: "round", borderColor: color("gray"), paddingX: 1, children: /* @__PURE__ */ jsxs6(Text8, { children: [
487
+ /* @__PURE__ */ jsx8(Text8, { color: color("green"), children: "> " }),
325
488
  value,
326
- disabled ? "" : /* @__PURE__ */ jsx7(Text7, { inverse: true, children: " " })
489
+ disabled ? "" : /* @__PURE__ */ jsx8(Text8, { inverse: true, children: " " })
327
490
  ] }) }),
328
- suggestions.map((s, i) => /* @__PURE__ */ jsx7(Text7, { color: i === highlighted ? color("green") : void 0, dimColor: i !== highlighted, children: `${i === highlighted ? "\u203A" : " "} /${s.name} ${s.description}` }, s.name))
491
+ 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))
329
492
  ] });
330
493
  }
331
494
 
332
495
  // src/ui/Spinner.tsx
333
- import { Text as Text8 } from "ink";
334
- import { useEffect, useState as useState5 } from "react";
335
- import { jsx as jsx8 } from "react/jsx-runtime";
496
+ import { Text as Text9 } from "ink";
497
+ import { useEffect, useState as useState6 } from "react";
498
+ import { jsx as jsx9 } from "react/jsx-runtime";
336
499
  var FRAMES = ["\xB7", "\u2722", "\u2733", "\u2736", "\u273B", "\u273D"];
337
500
  function Spinner({ label, startedAt }) {
338
- const [tick, setTick] = useState5(0);
501
+ const [tick, setTick] = useState6(0);
339
502
  useEffect(() => {
340
503
  const id = setInterval(() => setTick((t) => t + 1), 120);
341
504
  return () => clearInterval(id);
342
505
  }, []);
343
506
  const seconds = Math.floor((Date.now() - startedAt) / 1e3);
344
- return /* @__PURE__ */ jsx8(Text8, { color: color(ACCENT), children: `${FRAMES[tick % FRAMES.length]} ${label}\u2026 (${seconds} dtk \xB7 esc batal)` });
507
+ return /* @__PURE__ */ jsx9(Text9, { color: color(ACCENT), children: `${FRAMES[tick % FRAMES.length]} ${label}\u2026 (${seconds} dtk \xB7 esc batal)` });
345
508
  }
346
509
 
347
510
  // src/ui/StatusBar.tsx
348
- import { Text as Text9 } from "ink";
349
- import { jsx as jsx9 } from "react/jsx-runtime";
511
+ import { Text as Text10 } from "ink";
512
+ import { jsx as jsx10 } from "react/jsx-runtime";
350
513
  var LABEL = {
351
514
  default: "\u23F5 default",
352
515
  acceptEdits: "\u23F5 accept edits",
@@ -358,87 +521,87 @@ function formatTokens(n) {
358
521
  }
359
522
  function StatusBar({ mode, tokens, busy, model }) {
360
523
  const danger = mode === "allowAll";
361
- return /* @__PURE__ */ jsx9(Text9, { color: danger ? color("red") : void 0, dimColor: !danger, children: ` ${LABEL[mode]}${model ? ` \xB7 ${model}` : ""} \xB7 shift+tab ganti mode \xB7 ${formatTokens(tokens)} token \xB7 ${busy ? "esc batal" : "/ perintah"}` });
524
+ return /* @__PURE__ */ jsx10(Text10, { color: danger ? color("red") : void 0, dimColor: !danger, children: ` ${LABEL[mode]}${model ? ` \xB7 ${model}` : ""} \xB7 shift+tab ganti mode \xB7 ${formatTokens(tokens)} token \xB7 ${busy ? "esc batal" : "/ perintah"}` });
362
525
  }
363
526
 
364
527
  // src/ui/McpMenu.tsx
365
- import { Box as Box8, Text as Text10, useInput as useInput6 } from "ink";
366
- import { useState as useState6 } from "react";
367
- import { jsx as jsx10, jsxs as jsxs6 } from "react/jsx-runtime";
528
+ import { Box as Box9, Text as Text11, useInput as useInput7 } from "ink";
529
+ import { useState as useState7 } from "react";
530
+ import { jsx as jsx11, jsxs as jsxs7 } from "react/jsx-runtime";
368
531
  var MARK = { ready: "\u2713", error: "\u2717", starting: "\u2026", off: "\u25CB" };
369
532
  function McpMenu({ items, onPick }) {
370
- const [index, setIndex] = useState6(0);
371
- useInput6((_input, key) => {
533
+ const [index, setIndex] = useState7(0);
534
+ useInput7((_input, key) => {
372
535
  if (key.upArrow) setIndex((i) => Math.max(0, i - 1));
373
536
  else if (key.downArrow) setIndex((i) => Math.min(items.length - 1, i + 1));
374
537
  else if (key.return) onPick(items[index]?.name);
375
538
  else if (key.escape) onPick(void 0);
376
539
  });
377
- return /* @__PURE__ */ jsxs6(Box8, { flexDirection: "column", borderStyle: "round", borderColor: color("cyan"), paddingX: 1, children: [
378
- /* @__PURE__ */ jsx10(Text10, { bold: true, children: "Server MCP (enter: pasang / hapus \xB7 esc tutup) \xB7 \u2713 aktif \u25CB belum \u2717 error" }),
379
- items.map((item, i) => /* @__PURE__ */ jsxs6(Text10, { color: i === index ? color("green") : void 0, children: [
540
+ return /* @__PURE__ */ jsxs7(Box9, { flexDirection: "column", borderStyle: "round", borderColor: color("cyan"), paddingX: 1, children: [
541
+ /* @__PURE__ */ jsx11(Text11, { bold: true, children: "Server MCP (enter: pasang / hapus \xB7 esc tutup) \xB7 \u2713 aktif \u25CB belum \u2717 error" }),
542
+ items.map((item, i) => /* @__PURE__ */ jsxs7(Text11, { color: i === index ? color("green") : void 0, children: [
380
543
  `${i === index ? "\u203A" : " "} ${MARK[item.status]} ${item.name.padEnd(12)}`,
381
- /* @__PURE__ */ jsx10(Text10, { dimColor: true, children: item.status === "error" ? ` ${item.error ?? ""}` : ` ${item.description}` })
544
+ /* @__PURE__ */ jsx11(Text11, { dimColor: true, children: item.status === "error" ? ` ${item.error ?? ""}` : ` ${item.description}` })
382
545
  ] }, item.name))
383
546
  ] });
384
547
  }
385
548
 
386
549
  // src/ui/PlanApproval.tsx
387
- import { Box as Box9, Text as Text11, useInput as useInput7 } from "ink";
388
- import { jsx as jsx11, jsxs as jsxs7 } from "react/jsx-runtime";
550
+ import { Box as Box10, Text as Text12, useInput as useInput8 } from "ink";
551
+ import { jsx as jsx12, jsxs as jsxs8 } from "react/jsx-runtime";
389
552
  function PlanApproval({ plan, onAnswer }) {
390
- useInput7((input, key) => {
391
- const k = input.toLowerCase();
392
- if (k === "a") onAnswer("acceptEdits");
393
- else if (k === "y") onAnswer("default");
394
- else if (k === "n" || key.escape) onAnswer("no");
553
+ useInput8((input, key) => {
554
+ const k2 = input.toLowerCase();
555
+ if (k2 === "a") onAnswer("acceptEdits");
556
+ else if (k2 === "y") onAnswer("default");
557
+ else if (k2 === "n" || key.escape) onAnswer("no");
395
558
  });
396
- return /* @__PURE__ */ jsxs7(Box9, { flexDirection: "column", borderStyle: "round", borderColor: color("cyan"), paddingX: 1, children: [
397
- /* @__PURE__ */ jsx11(Text11, { bold: true, children: "Rencana dari agent" }),
398
- /* @__PURE__ */ jsx11(Markdown, { text: plan }),
399
- /* @__PURE__ */ jsxs7(Text11, { children: [
400
- /* @__PURE__ */ jsx11(Text11, { color: color("green"), children: "[a] ya, edit otomatis" }),
559
+ return /* @__PURE__ */ jsxs8(Box10, { flexDirection: "column", borderStyle: "round", borderColor: color("cyan"), paddingX: 1, children: [
560
+ /* @__PURE__ */ jsx12(Text12, { bold: true, children: "Rencana dari agent" }),
561
+ /* @__PURE__ */ jsx12(Markdown, { text: plan }),
562
+ /* @__PURE__ */ jsxs8(Text12, { children: [
563
+ /* @__PURE__ */ jsx12(Text12, { color: color("green"), children: "[a] ya, edit otomatis" }),
401
564
  " ",
402
- /* @__PURE__ */ jsx11(Text11, { color: color("cyan"), children: "[y] ya, tanya tiap langkah" }),
565
+ /* @__PURE__ */ jsx12(Text12, { color: color("cyan"), children: "[y] ya, tanya tiap langkah" }),
403
566
  " ",
404
- /* @__PURE__ */ jsx11(Text11, { color: color("red"), children: "[n] belum, perbaiki dulu" })
567
+ /* @__PURE__ */ jsx12(Text12, { color: color("red"), children: "[n] belum, perbaiki dulu" })
405
568
  ] })
406
569
  ] });
407
570
  }
408
571
 
409
572
  // src/ui/TodoList.tsx
410
- import { Box as Box10, Text as Text12 } from "ink";
411
- import { jsx as jsx12 } from "react/jsx-runtime";
573
+ import { Box as Box11, Text as Text13 } from "ink";
574
+ import { jsx as jsx13 } from "react/jsx-runtime";
412
575
  var MARK2 = { completed: "\u2611", in_progress: "\u25FC", pending: "\u2610" };
413
576
  function TodoList({ items }) {
414
577
  if (!items.length || items.every((t) => t.status === "completed")) return null;
415
- return /* @__PURE__ */ jsx12(Box10, { flexDirection: "column", marginLeft: 2, marginTop: 1, children: items.map((t, i) => /* @__PURE__ */ jsx12(Text12, { dimColor: t.status === "completed", color: t.status === "in_progress" ? color("yellow") : void 0, children: `${MARK2[t.status]} ${t.content}` }, i)) });
578
+ 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)) });
416
579
  }
417
580
 
418
581
  // src/ui/ToolBlock.tsx
419
- import { Box as Box11, Text as Text13 } from "ink";
420
- import { jsx as jsx13, jsxs as jsxs8 } from "react/jsx-runtime";
582
+ import { Box as Box12, Text as Text14 } from "ink";
583
+ import { jsx as jsx14, jsxs as jsxs9 } from "react/jsx-runtime";
421
584
  var PREVIEW_LINES = 5;
422
585
  function ToolBlock({ tool, target, output, display, isError, done, expanded, sub }) {
423
586
  const isTask = tool === "task";
424
587
  const isDiff = !!display && /^\s*\d+ [+\- ] /.test(display);
425
588
  const lines = (output ?? "").split("\n");
426
589
  const shown = expanded ? lines : lines.slice(0, PREVIEW_LINES);
427
- return /* @__PURE__ */ jsxs8(Box11, { flexDirection: "column", marginLeft: 2, children: [
428
- /* @__PURE__ */ jsxs8(Text13, { children: [
429
- /* @__PURE__ */ jsx13(Text13, { color: isError ? color("red") : done ? color("green") : color("yellow"), children: "\u23BF " }),
430
- /* @__PURE__ */ jsx13(Text13, { bold: true, children: toolLabel(tool) }),
431
- /* @__PURE__ */ jsx13(Text13, { children: ` ${target}` }),
432
- display && !isDiff && !isTask ? /* @__PURE__ */ jsx13(Text13, { dimColor: true, children: ` (${display})` }) : null
590
+ return /* @__PURE__ */ jsxs9(Box12, { flexDirection: "column", marginLeft: 2, children: [
591
+ /* @__PURE__ */ jsxs9(Text14, { children: [
592
+ /* @__PURE__ */ jsx14(Text14, { color: isError ? color("red") : done ? color("green") : color("yellow"), children: "\u23BF " }),
593
+ /* @__PURE__ */ jsx14(Text14, { bold: true, children: toolLabel(tool) }),
594
+ /* @__PURE__ */ jsx14(Text14, { children: ` ${target}` }),
595
+ display && !isDiff && !isTask ? /* @__PURE__ */ jsx14(Text14, { dimColor: true, children: ` (${display})` }) : null
433
596
  ] }),
434
- sub?.length ? /* @__PURE__ */ jsxs8(Box11, { flexDirection: "column", marginLeft: 3, children: [
435
- sub.slice(-6).map((s) => /* @__PURE__ */ jsx13(Text13, { dimColor: s.done && !s.isError, color: s.isError ? color("red") : void 0, children: `\u23BF ${toolLabel(s.tool)} ${s.target}` }, s.id)),
436
- sub.length > 6 ? /* @__PURE__ */ jsx13(Text13, { dimColor: true, children: `\u2026 ${sub.length - 6} langkah sebelumnya` }) : null
597
+ sub?.length ? /* @__PURE__ */ jsxs9(Box12, { flexDirection: "column", marginLeft: 3, children: [
598
+ 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)),
599
+ sub.length > 6 ? /* @__PURE__ */ jsx14(Text14, { dimColor: true, children: `\u2026 ${sub.length - 6} langkah sebelumnya` }) : null
437
600
  ] }) : null,
438
- isTask && done ? /* @__PURE__ */ jsx13(Box11, { marginLeft: 3, children: /* @__PURE__ */ jsx13(Text13, { color: isError ? color("red") : color("green"), children: `${isError ? "\u2717 gagal" : "\u2713 selesai"}${display ? ` \xB7 ${display}` : ""}` }) }) : null,
439
- isDiff ? /* @__PURE__ */ jsx13(Box11, { marginLeft: 3, children: /* @__PURE__ */ jsx13(DiffView, { diff: display }) }) : output && (isError || tool === "bash") && !(isTask && !isError) ? /* @__PURE__ */ jsxs8(Box11, { flexDirection: "column", marginLeft: 3, children: [
440
- shown.map((line, i) => /* @__PURE__ */ jsx13(Text13, { dimColor: !isError, color: isError ? color("red") : void 0, children: line }, i)),
441
- lines.length > shown.length ? /* @__PURE__ */ jsx13(Text13, { dimColor: true, children: `\u2026 ${lines.length - shown.length} baris lagi (ctrl+o)` }) : null
601
+ 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,
602
+ 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: [
603
+ shown.map((line, i) => /* @__PURE__ */ jsx14(Text14, { dimColor: !isError, color: isError ? color("red") : void 0, children: line }, i)),
604
+ lines.length > shown.length ? /* @__PURE__ */ jsx14(Text14, { dimColor: true, children: `\u2026 ${lines.length - shown.length} baris lagi (ctrl+o)` }) : null
442
605
  ] }) : null
443
606
  ] });
444
607
  }
@@ -461,6 +624,11 @@ function applyEvent(t, event) {
461
624
  if (last?.kind === "assistant") return { ...t, live: [...t.live.slice(0, -1), { ...last, text: last.text + event.delta }] };
462
625
  return { ...t, live: [...t.live, entry({ kind: "assistant", text: event.delta })] };
463
626
  }
627
+ case "textReplace": {
628
+ const last = t.live.at(-1);
629
+ if (last?.kind !== "assistant") return t;
630
+ return { ...t, live: [...t.live.slice(0, -1), { ...last, text: event.text }] };
631
+ }
464
632
  case "toolStart": {
465
633
  const running = t.live.some((e) => e.kind === "tool" && !e.done);
466
634
  const tool = entry({ kind: "tool", callId: event.id, tool: event.tool, target: event.target, done: false });
@@ -495,11 +663,11 @@ function applyEvent(t, event) {
495
663
  }
496
664
 
497
665
  // src/ui/App.tsx
498
- import { jsx as jsx14, jsxs as jsxs9 } from "react/jsx-runtime";
666
+ import { jsx as jsx15, jsxs as jsxs10 } from "react/jsx-runtime";
499
667
  function helpText(runtime) {
500
668
  const custom = runtime.commands.filter((c) => !SLASH_COMMANDS.some((b) => b.name === c.name));
501
669
  return [
502
- ...SLASH_COMMANDS.map((c) => `/${c.name.padEnd(8)} ${c.description}`),
670
+ ...SLASH_COMMANDS.map((c) => `/${c.name.padEnd(12)} ${c.description}`),
503
671
  ...custom.length ? ["", "Perintah custom:", ...custom.map((c) => `/${c.name.padEnd(8)} ${c.description ?? ""}`)] : [],
504
672
  ...runtime.skills.length ? ["", "Skill:", ...runtime.skills.map((s) => `/${s.name.padEnd(8)} ${s.description.slice(0, 60)}`)] : [],
505
673
  "",
@@ -508,27 +676,29 @@ function helpText(runtime) {
508
676
  "@file + tab lengkapi nama file \u2191\u2193 riwayat input"
509
677
  ].join("\n");
510
678
  }
679
+ 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.";
511
680
  var labelFor = (runtime, ref) => `${ref.slice(ref.indexOf("/") + 1)} \xB7 ${runtime.providerLabel()}`;
512
681
  function App({ runtime, initialPrompt, version }) {
513
682
  const { exit } = useApp();
514
- const [transcript, setTranscript] = useState7(() => ({ done: [entry({ kind: "header" })], live: [] }));
515
- const [busy, setBusy] = useState7(false);
516
- const [startedAt, setStartedAt] = useState7(0);
517
- const [mode, setMode] = useState7(runtime.agent.permissions.mode);
518
- const [tokens, setTokens] = useState7(runtime.agent.totalUsage.inputTokens + runtime.agent.totalUsage.outputTokens);
519
- const [asks, setAsks] = useState7([]);
683
+ const [transcript, setTranscript] = useState8(() => ({ done: [entry({ kind: "header" })], live: [] }));
684
+ const [busy, setBusy] = useState8(false);
685
+ const [startedAt, setStartedAt] = useState8(0);
686
+ const [mode, setMode] = useState8(runtime.agent.permissions.mode);
687
+ const [tokens, setTokens] = useState8(runtime.agent.totalUsage.inputTokens + runtime.agent.totalUsage.outputTokens);
688
+ const [asks, setAsks] = useState8([]);
520
689
  const pending = asks[0] ?? null;
521
690
  const askId = useRef(0);
522
- const [picker, setPicker] = useState7(null);
523
- const [providerMenu, setProviderMenu] = useState7(null);
524
- const [prompt, setPrompt] = useState7(null);
525
- const ask = (label, mask = false) => new Promise((resolve) => setPrompt({ label, mask, resolve }));
526
- const [modelLabel, setModelLabel] = useState7(() => labelFor(runtime, runtime.modelRef));
527
- const [history, setHistory] = useState7([]);
691
+ const [picker, setPicker] = useState8(null);
692
+ const [providerMenu, setProviderMenu] = useState8(null);
693
+ const [prompt, setPrompt] = useState8(null);
694
+ const ask = useCallback((label, mask = false) => new Promise((resolve2) => setPrompt({ label, mask, resolve: resolve2 })), []);
695
+ const [modelLabel, setModelLabel] = useState8(() => labelFor(runtime, runtime.modelRef));
696
+ const [history, setHistory] = useState8([]);
528
697
  const controller = useRef(null);
529
- const [todos, setTodos] = useState7(runtime.todos.items);
530
- const [mcpMenu, setMcpMenu] = useState7(null);
531
- const [planAsk, setPlanAsk] = useState7(null);
698
+ const [todos, setTodos] = useState8(runtime.todos.items);
699
+ const [mcpMenu, setMcpMenu] = useState8(null);
700
+ const [listPicker, setListPicker] = useState8(null);
701
+ const [planAsk, setPlanAsk] = useState8(null);
532
702
  useEffect2(() => runtime.todos.subscribe(setTodos), [runtime]);
533
703
  const extraCommands = [
534
704
  ...runtime.commands.filter((c) => !SLASH_COMMANDS.some((b) => b.name === c.name.toLowerCase())).map((c) => ({ name: c.name.toLowerCase(), description: c.description ?? "perintah custom" })),
@@ -549,8 +719,8 @@ function App({ runtime, initialPrompt, version }) {
549
719
  }, []);
550
720
  useEffect2(() => {
551
721
  runtime.agent.onEvent = onEvent;
552
- runtime.agent.askPermission = (request) => new Promise((resolve) => setAsks((q) => [...q, { id: askId.current++, request, resolve }]));
553
- runtime.interaction.approvePlan = (plan) => new Promise((resolve) => setPlanAsk({ plan, resolve }));
722
+ runtime.agent.askPermission = (request) => new Promise((resolve2) => setAsks((q) => [...q, { id: askId.current++, request, resolve: resolve2 }]));
723
+ runtime.interaction.approvePlan = (plan) => new Promise((resolve2) => setPlanAsk({ plan, resolve: resolve2 }));
554
724
  }, [runtime, onEvent]);
555
725
  const runTurn = useCallback(
556
726
  async (text) => {
@@ -565,7 +735,7 @@ function App({ runtime, initialPrompt, version }) {
565
735
  },
566
736
  [runtime]
567
737
  );
568
- const runSlash = useCallback(
738
+ const runSlashCommand = useCallback(
569
739
  async (name, args) => {
570
740
  switch (name) {
571
741
  case "help":
@@ -629,6 +799,212 @@ function App({ runtime, initialPrompt, version }) {
629
799
  setMcpMenu(items);
630
800
  return;
631
801
  }
802
+ case "new":
803
+ runtime.newSession();
804
+ setTokens(0);
805
+ notice("Sesi baru dimulai. Sesi sebelumnya tetap tersimpan (/resume).");
806
+ return;
807
+ case "session":
808
+ notice(
809
+ sessionText({
810
+ file: runtime.session.file,
811
+ started: runtime.startedAt,
812
+ messages: runtime.agent.messages.length,
813
+ usage: runtime.agent.totalUsage,
814
+ modelRef: runtime.modelRef
815
+ })
816
+ );
817
+ return;
818
+ case "resume": {
819
+ const others = Session.list(runtime.home, runtime.cwd).filter((s) => s.session.file !== runtime.session.file);
820
+ if (!others.length) {
821
+ notice("Belum ada sesi lain di folder ini.");
822
+ return;
823
+ }
824
+ setListPicker({
825
+ title: "Lanjutkan sesi",
826
+ items: others.map((s) => ({
827
+ id: s.session.file,
828
+ label: `${s.mtime.toLocaleString("id-ID")} ${s.preview || "(kosong)"}`,
829
+ hint: `${s.session.load().length} pesan`
830
+ })),
831
+ onPick: (id) => {
832
+ setListPicker(null);
833
+ const chosen = others.find((s) => s.session.file === id);
834
+ if (!chosen) return;
835
+ runtime.resume(chosen.session);
836
+ const tail = runtime.agent.messages.filter((m) => (m.role === "user" || m.role === "assistant") && m.content).slice(-3);
837
+ notice(
838
+ [`Melanjutkan sesi (${runtime.agent.messages.length} pesan):`, ...tail.map((m) => `${m.role === "user" ? ">" : "\u25CF"} ${String(m.content).slice(0, 200)}`)].join(
839
+ "\n"
840
+ )
841
+ );
842
+ }
843
+ });
844
+ return;
845
+ }
846
+ case "status":
847
+ notice(
848
+ statusText({
849
+ version,
850
+ modelRef: runtime.modelRef,
851
+ providerLabel: runtime.providerLabel(),
852
+ mode: runtime.agent.permissions.mode,
853
+ cwd: runtime.cwd,
854
+ mcp: runtime.mcp.states(),
855
+ usage: runtime.agent.totalUsage,
856
+ lastInputTokens: runtime.agent.lastInputTokens
857
+ })
858
+ );
859
+ return;
860
+ case "agents":
861
+ notice(agentsText([...BUILTIN_AGENTS, ...runtime.agentDefs]));
862
+ return;
863
+ case "skills":
864
+ notice(skillsText(runtime.skills, runtime.commands));
865
+ return;
866
+ case "doctor": {
867
+ notice("Memeriksa\u2026");
868
+ const c = runtime.config;
869
+ notice(
870
+ await doctorText(
871
+ {
872
+ providers: Object.keys(c.providers).map((id) => ({ id, name: providerName(c, id), ready: hasKey(c, id, runtime.env) })),
873
+ mcp: runtime.mcp.states(),
874
+ activeProvider: runtime.providerLabel()
875
+ },
876
+ {
877
+ which: (cmd) => spawnSync(process.platform === "win32" ? "where" : "which", [cmd]).status === 0,
878
+ nodeVersion: process.version,
879
+ listModels: () => runtime.agent.provider.listModels()
880
+ }
881
+ )
882
+ );
883
+ return;
884
+ }
885
+ case "permissions": {
886
+ const rules = runtime.agent.permissions.list();
887
+ if (!rules.length) {
888
+ notice("Belum ada izin tersimpan. Izin dari jawaban [a] dan dari config akan muncul di sini.");
889
+ return;
890
+ }
891
+ setListPicker({
892
+ title: "Izin aktif (enter: cabut izin sesi)",
893
+ items: rules.map((r) => ({ id: r.rule, label: r.rule, hint: r.source === "config" ? "config" : "sesi ini" })),
894
+ onPick: (id) => {
895
+ setListPicker(null);
896
+ const rule = rules.find((r) => r.rule === id);
897
+ if (!rule) return;
898
+ if (rule.source === "config") notice(`${rule.rule} berasal dari config/flag; hapus dari ~/.bccli/config.json untuk mencabutnya.`, "warn");
899
+ else {
900
+ runtime.agent.permissions.revoke(rule.rule);
901
+ notice(`Izin ${rule.rule} dicabut.`);
902
+ }
903
+ }
904
+ });
905
+ return;
906
+ }
907
+ case "memory": {
908
+ if (!args) {
909
+ const files = instructionFiles(runtime.cwd, runtime.home);
910
+ notice(files.length ? files.map((f) => `${f.path} (${f.lines} baris)`).join("\n") : "Belum ada AGENTS.md / BCCLI.md. Tambah dengan /memory <teks>.");
911
+ return;
912
+ }
913
+ const global = /^global\s/i.test(args);
914
+ const text = global ? args.replace(/^global\s+/i, "") : args;
915
+ const file = global ? join(runtime.home, "BCCLI.md") : join(runtime.cwd, "AGENTS.md");
916
+ try {
917
+ mkdirSync(dirname(file), { recursive: true });
918
+ appendMemory(file, text);
919
+ } catch (error) {
920
+ notice(`Gagal menulis ${file}: ${error.message}`, "error");
921
+ return;
922
+ }
923
+ runtime.rebuildSystemPrompt();
924
+ notice(`Ditambahkan ke ${file}.`);
925
+ return;
926
+ }
927
+ case "init":
928
+ void runTurn(INIT_PROMPT);
929
+ return;
930
+ case "login": {
931
+ const id = runtime.modelRef.slice(0, runtime.modelRef.indexOf("/"));
932
+ const key = await ask(`API key untuk ${runtime.providerLabel()}`, true);
933
+ if (!key) return;
934
+ const result = await runtime.addProviderKey(id, key);
935
+ if (!result.ok) {
936
+ notice(result.error, "error");
937
+ return;
938
+ }
939
+ runtime.setModel(runtime.modelRef);
940
+ notice(`Key ${runtime.providerLabel()} tersimpan \xB7 ${result.models} model.`);
941
+ return;
942
+ }
943
+ case "logout": {
944
+ const id = runtime.modelRef.slice(0, runtime.modelRef.indexOf("/"));
945
+ removeCredential(id, runtime.env);
946
+ const envName = runtime.config.providers[id]?.apiKeyEnv;
947
+ if (envName && runtime.env[envName]) {
948
+ notice(`Key tersimpan dihapus, tapi ${envName} masih ada di environment dan tetap dipakai.`, "warn");
949
+ return;
950
+ }
951
+ try {
952
+ runtime.setModel(runtime.modelRef);
953
+ } catch (error) {
954
+ const fail = async () => {
955
+ throw error;
956
+ };
957
+ runtime.agent.provider = { chat: fail, listModels: fail };
958
+ }
959
+ notice(`Key ${runtime.providerLabel()} dihapus. Pakai /login atau /provider untuk menambah lagi.`);
960
+ return;
961
+ }
962
+ case "diff":
963
+ notice(await gitDiff(runtime.cwd));
964
+ return;
965
+ case "undo": {
966
+ const result = await runtime.checkpoints.undo();
967
+ if (!result) {
968
+ notice("Tidak ada edit file yang bisa dibatalkan.");
969
+ return;
970
+ }
971
+ const rel = (p) => relative(runtime.cwd, p) || p;
972
+ notice(
973
+ [
974
+ result.failed.length ? `Gagal dikembalikan: ${result.failed.map(rel).join(", ")}` : "",
975
+ result.restored.length ? `Dikembalikan: ${result.restored.map(rel).join(", ")}` : "",
976
+ result.deleted.length ? `Dihapus (file baru): ${result.deleted.map(rel).join(", ")}` : "",
977
+ result.skipped.length ? `Terlalu besar untuk disimpan, tidak diubah: ${result.skipped.map(rel).join(", ")}` : "",
978
+ "Catatan: perubahan lewat perintah bash tidak bisa di-undo."
979
+ ].filter(Boolean).join("\n")
980
+ );
981
+ return;
982
+ }
983
+ case "copy": {
984
+ const last = runtime.agent.messages.findLast((m) => m.role === "assistant" && typeof m.content === "string" && m.content.trim());
985
+ if (!last || typeof last.content !== "string") {
986
+ notice("Belum ada jawaban untuk disalin.");
987
+ return;
988
+ }
989
+ process.stdout.write(osc52(last.content, !!process.env.TMUX));
990
+ const file = join(runtime.home, "last-answer.md");
991
+ mkdirSync(runtime.home, { recursive: true });
992
+ writeFileSync(file, last.content);
993
+ notice(`Disalin ke clipboard (bila terminal mendukung OSC 52) dan disimpan di ${file}.`);
994
+ return;
995
+ }
996
+ case "export": {
997
+ const name2 = args || `bccli-${(/* @__PURE__ */ new Date()).toISOString().slice(0, 16).replace(/[:T]/g, "-")}.md`;
998
+ const file = resolve(runtime.cwd, name2);
999
+ try {
1000
+ writeFileSync(file, exportMarkdown(runtime.agent.messages, `Sesi BCCLI ${runtime.startedAt.toLocaleString("id-ID")}`));
1001
+ } catch (error) {
1002
+ notice(`Gagal menulis ${file}: ${error.message}`, "error");
1003
+ return;
1004
+ }
1005
+ notice(`Percakapan disimpan di ${file}.`);
1006
+ return;
1007
+ }
632
1008
  case "provider": {
633
1009
  const c = runtime.config;
634
1010
  setProviderMenu(
@@ -640,7 +1016,11 @@ function App({ runtime, initialPrompt, version }) {
640
1016
  notice(`Perintah tidak dikenal: /${name}. Ketik /help.`, "warn");
641
1017
  }
642
1018
  },
643
- [runtime, notice, exit]
1019
+ [runtime, notice, exit, version, runTurn, ask]
1020
+ );
1021
+ const runSlash = useCallback(
1022
+ (name, args) => runSlashCommand(name, args).catch((error) => notice(`/${name}: ${error.message}`, "error")),
1023
+ [runSlashCommand, notice]
644
1024
  );
645
1025
  const submit = useCallback(
646
1026
  (text) => {
@@ -657,7 +1037,7 @@ function App({ runtime, initialPrompt, version }) {
657
1037
  else if (skill) {
658
1038
  let body;
659
1039
  try {
660
- body = parseFrontmatter(readFileSync(skill.file, "utf8")).body;
1040
+ body = parseFrontmatter(readFileSync2(skill.file, "utf8")).body;
661
1041
  } catch (error) {
662
1042
  notice(`Skill ${skill.name} tidak bisa dibaca: ${error.code ?? error.message}`, "error");
663
1043
  return;
@@ -676,7 +1056,7 @@ ARGUMENTS: ${slash.args}`);
676
1056
  submit(initialPrompt);
677
1057
  }
678
1058
  }, [initialPrompt, submit]);
679
- useInput8((input, key) => {
1059
+ useInput9((input, key) => {
680
1060
  if (key.tab && key.shift) {
681
1061
  const next = nextMode(runtime.agent.permissions.mode);
682
1062
  runtime.agent.permissions.mode = next;
@@ -750,31 +1130,31 @@ ${t.output}`);
750
1130
  const renderEntry = (e) => {
751
1131
  switch (e.kind) {
752
1132
  case "header":
753
- return /* @__PURE__ */ jsx14(Box12, { marginBottom: 1, children: /* @__PURE__ */ jsxs9(Text14, { children: [
754
- /* @__PURE__ */ jsx14(Text14, { color: color(ACCENT), bold: true, children: " \u273B " }),
755
- /* @__PURE__ */ jsx14(Text14, { bold: true, children: `BCCLI ${version}` }),
756
- /* @__PURE__ */ jsx14(Text14, { dimColor: true, children: ` \xB7 ${runtime.modelRef} (${runtime.providerLabel()}) \xB7 ${cwd}` })
1133
+ return /* @__PURE__ */ jsx15(Box13, { marginBottom: 1, children: /* @__PURE__ */ jsxs10(Text15, { children: [
1134
+ /* @__PURE__ */ jsx15(Text15, { color: color(ACCENT), bold: true, children: " \u273B " }),
1135
+ /* @__PURE__ */ jsx15(Text15, { bold: true, children: `BCCLI ${version}` }),
1136
+ /* @__PURE__ */ jsx15(Text15, { dimColor: true, children: ` \xB7 ${runtime.modelRef} (${runtime.providerLabel()}) \xB7 ${cwd}` })
757
1137
  ] }) }, e.id);
758
1138
  case "user":
759
- return /* @__PURE__ */ jsx14(Box12, { marginTop: 1, children: /* @__PURE__ */ jsx14(Text14, { dimColor: true, children: `> ${e.text}` }) }, e.id);
1139
+ return /* @__PURE__ */ jsx15(Box13, { marginTop: 1, children: /* @__PURE__ */ jsx15(Text15, { dimColor: true, children: `> ${e.text}` }) }, e.id);
760
1140
  case "assistant":
761
1141
  if (!e.text.trim()) return null;
762
- return /* @__PURE__ */ jsxs9(Box12, { marginTop: 1, children: [
763
- /* @__PURE__ */ jsx14(Text14, { color: color(ACCENT), children: "\u25CF " }),
764
- /* @__PURE__ */ jsx14(Markdown, { text: e.text.replace(/^\s+/, "") })
1142
+ return /* @__PURE__ */ jsxs10(Box13, { marginTop: 1, children: [
1143
+ /* @__PURE__ */ jsx15(Text15, { color: color(ACCENT), children: "\u25CF " }),
1144
+ /* @__PURE__ */ jsx15(Markdown, { text: e.text.replace(/^\s+/, "") })
765
1145
  ] }, e.id);
766
1146
  case "tool":
767
- return /* @__PURE__ */ jsx14(ToolBlock, { tool: e.tool, target: e.target, output: e.output, display: e.display, isError: e.isError, done: e.done, sub: e.sub }, e.id);
1147
+ return /* @__PURE__ */ jsx15(ToolBlock, { tool: e.tool, target: e.target, output: e.output, display: e.display, isError: e.isError, done: e.done, sub: e.sub }, e.id);
768
1148
  case "notice":
769
- return /* @__PURE__ */ jsx14(Box12, { marginTop: 1, children: /* @__PURE__ */ jsx14(Text14, { color: e.tone === "error" ? color("red") : e.tone === "warn" ? color("yellow") : void 0, dimColor: e.tone === "info", children: e.text }) }, e.id);
1149
+ return /* @__PURE__ */ jsx15(Box13, { marginTop: 1, children: /* @__PURE__ */ jsx15(Text15, { color: e.tone === "error" ? color("red") : e.tone === "warn" ? color("yellow") : void 0, dimColor: e.tone === "info", children: e.text }) }, e.id);
770
1150
  }
771
1151
  };
772
- return /* @__PURE__ */ jsxs9(Box12, { flexDirection: "column", children: [
773
- /* @__PURE__ */ jsx14(Static, { items: transcript.done, children: renderEntry }),
1152
+ return /* @__PURE__ */ jsxs10(Box13, { flexDirection: "column", children: [
1153
+ /* @__PURE__ */ jsx15(Static, { items: transcript.done, children: renderEntry }),
774
1154
  transcript.live.map(renderEntry),
775
- busy && !pending && !planAsk ? /* @__PURE__ */ jsx14(Box12, { marginTop: 1, children: /* @__PURE__ */ jsx14(Spinner, { label: "Berpikir", startedAt }) }) : null,
776
- pending ? /* @__PURE__ */ jsx14(PermissionPrompt, { request: pending.request, onAnswer: answer }, pending.id) : null,
777
- picker ? /* @__PURE__ */ jsx14(
1155
+ busy && !pending && !planAsk ? /* @__PURE__ */ jsx15(Box13, { marginTop: 1, children: /* @__PURE__ */ jsx15(Spinner, { label: "Berpikir", startedAt }) }) : null,
1156
+ pending ? /* @__PURE__ */ jsx15(PermissionPrompt, { request: pending.request, onAnswer: answer }, pending.id) : null,
1157
+ picker ? /* @__PURE__ */ jsx15(
778
1158
  ModelPicker,
779
1159
  {
780
1160
  groups: picker,
@@ -785,9 +1165,10 @@ ${t.output}`);
785
1165
  }
786
1166
  }
787
1167
  ) : null,
788
- mcpMenu ? /* @__PURE__ */ jsx14(McpMenu, { items: mcpMenu, onPick: (n) => void pickMcp(n) }) : null,
789
- providerMenu ? /* @__PURE__ */ jsx14(ProviderMenu, { entries: providerMenu, onPick: (id) => void pickProvider(id) }) : null,
790
- prompt ? /* @__PURE__ */ jsx14(
1168
+ listPicker ? /* @__PURE__ */ jsx15(ListPicker, { title: listPicker.title, items: listPicker.items, onPick: listPicker.onPick }) : null,
1169
+ mcpMenu ? /* @__PURE__ */ jsx15(McpMenu, { items: mcpMenu, onPick: (n) => void pickMcp(n) }) : null,
1170
+ providerMenu ? /* @__PURE__ */ jsx15(ProviderMenu, { entries: providerMenu, onPick: (id) => void pickProvider(id) }) : null,
1171
+ prompt ? /* @__PURE__ */ jsx15(
791
1172
  LinePrompt,
792
1173
  {
793
1174
  label: prompt.label,
@@ -802,7 +1183,7 @@ ${t.output}`);
802
1183
  }
803
1184
  }
804
1185
  ) : null,
805
- planAsk ? /* @__PURE__ */ jsx14(
1186
+ planAsk ? /* @__PURE__ */ jsx15(
806
1187
  PlanApproval,
807
1188
  {
808
1189
  plan: planAsk.plan,
@@ -813,18 +1194,18 @@ ${t.output}`);
813
1194
  }
814
1195
  }
815
1196
  ) : null,
816
- /* @__PURE__ */ jsx14(TodoList, { items: todos }),
817
- /* @__PURE__ */ jsx14(PromptInput, { disabled: busy || !!pending || !!picker || !!providerMenu || !!prompt || !!planAsk || !!mcpMenu, history, cwd: runtime.cwd, onSubmit: submit, extraCommands }),
818
- /* @__PURE__ */ jsx14(StatusBar, { mode, tokens, busy, model: modelLabel })
1197
+ /* @__PURE__ */ jsx15(TodoList, { items: todos }),
1198
+ /* @__PURE__ */ jsx15(PromptInput, { disabled: busy || !!pending || !!picker || !!providerMenu || !!prompt || !!planAsk || !!mcpMenu || !!listPicker, history, cwd: runtime.cwd, onSubmit: submit, extraCommands }),
1199
+ /* @__PURE__ */ jsx15(StatusBar, { mode, tokens, busy, model: modelLabel })
819
1200
  ] });
820
1201
  }
821
1202
 
822
1203
  // src/ui/index.tsx
823
- import { jsx as jsx15 } from "react/jsx-runtime";
1204
+ import { jsx as jsx16 } from "react/jsx-runtime";
824
1205
  async function startInteractive(rt, opts) {
825
1206
  if (opts.resume) {
826
- const { Session } = await import("./session-7UB77CEG.js");
827
- const list = Session.list(rt.home, rt.cwd).slice(0, 20);
1207
+ const { Session: Session2 } = await import("./session-7UB77CEG.js");
1208
+ const list = Session2.list(rt.home, rt.cwd).slice(0, 20);
828
1209
  if (!list.length) console.log("Belum ada sesi di folder ini; memulai sesi baru.");
829
1210
  else {
830
1211
  list.forEach((s, i) => {
@@ -852,7 +1233,7 @@ async function startInteractive(rt, opts) {
852
1233
  rl.close();
853
1234
  }
854
1235
  void rt.startMcp(plan.start);
855
- const instance = render(/* @__PURE__ */ jsx15(App, { runtime: rt, initialPrompt: opts.initialPrompt, version: opts.version }), { exitOnCtrlC: true });
1236
+ const instance = render(/* @__PURE__ */ jsx16(App, { runtime: rt, initialPrompt: opts.initialPrompt, version: opts.version }), { exitOnCtrlC: true });
856
1237
  await instance.waitUntilExit();
857
1238
  await rt.mcp.stop();
858
1239
  return 0;