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

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,39 +1,47 @@
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
- } from "./chunk-5C6NQFSI.js";
16
+ } from "./chunk-U7DLMVPZ.js";
17
+ import {
18
+ Session
19
+ } from "./chunk-SJH3YLW4.js";
15
20
  import {
16
21
  CATALOG,
17
22
  fillTemplate
18
- } from "./chunk-TV6LEHOH.js";
23
+ } from "./chunk-UESQNU4A.js";
19
24
  import {
20
25
  addGlobalServer,
21
26
  globalMcpPath,
22
27
  readMcpFile,
23
28
  removeGlobalServer
24
- } from "./chunk-XQLXE666.js";
29
+ } from "./chunk-NGLKANMD.js";
25
30
  import {
26
- PRESETS
27
- } from "./chunk-Z5JM76EL.js";
31
+ PRESETS,
32
+ removeCredential
33
+ } from "./chunk-RZUJYAQV.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 { execFile as execFile2 } from "child_process";
40
+ import { existsSync as existsSync2, 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,125 @@ 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, bin = "git") {
94
+ return new Promise((resolve2) => {
95
+ execFile(
96
+ bin,
97
+ args,
98
+ { cwd, maxBuffer: 20 * 1024 * 1024 },
99
+ (error, stdout, stderr) => resolve2({ ok: !error, out: error ? stderr : stdout, missing: error?.code === "ENOENT" })
100
+ );
101
+ });
102
+ }
103
+ async function gitDiff(cwd, maxLines = 400, bin = "git") {
104
+ const inside = await git(cwd, ["rev-parse", "--is-inside-work-tree"], bin);
105
+ if (inside.missing) return "git tidak terpasang atau tidak ada di PATH, jadi /diff tidak bisa dipakai.";
106
+ if (!inside.ok || inside.out.trim() !== "true") return "Folder ini bukan repository git, jadi tidak ada diff.";
107
+ const base = (await git(cwd, ["rev-parse", "--verify", "-q", "HEAD"])).ok ? "HEAD" : "4b825dc642cb6eb9a060e54bf8d69288fbee4904";
108
+ const [stat, diff, untracked] = await Promise.all([
109
+ git(cwd, ["diff", "--stat", base]),
110
+ git(cwd, ["diff", base]),
111
+ git(cwd, ["ls-files", "--others", "--exclude-standard"])
112
+ ]);
113
+ const fresh = untracked.out.split("\n").filter(Boolean);
114
+ if (!diff.out.trim() && !fresh.length) return "Tidak ada perubahan yang belum di-commit.";
115
+ const lines = diff.out.trimEnd().split("\n");
116
+ const more = lines.length > maxLines ? `
117
+ \u2026 ${lines.length - maxLines} baris lagi` : "";
118
+ 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");
119
+ }
120
+
121
+ // src/slash/doctor.ts
122
+ async function doctorText(input, deps) {
123
+ const major = Number(deps.nodeVersion.replace(/^v/, "").split(".")[0]);
124
+ const lines = [major >= 22 ? `\u2713 Node ${deps.nodeVersion}` : `\u2717 Node ${deps.nodeVersion} (butuh \u2265 22)`];
125
+ for (const p of input.providers) lines.push(p.ready ? `\u2713 ${p.name}` : `\u25CB ${p.name} (belum ada key)`);
126
+ const cmds = ["rg", "git", "npx", "uvx"];
127
+ const [connection, found] = await Promise.all([
128
+ deps.listModels().then(
129
+ (models) => `\u2713 Koneksi ${input.activeProvider} (${models.length} model)`,
130
+ (error) => `\u2717 Koneksi ${input.activeProvider}: ${error.message}`
131
+ ),
132
+ Promise.all(cmds.map(async (cmd) => deps.which(cmd)))
133
+ ]);
134
+ lines.push(connection);
135
+ for (const m of input.mcp) lines.push(m.status === "error" ? `\u2717 MCP ${m.name}: ${m.error ?? ""}` : `\u2713 MCP ${m.name} (${m.status})`);
136
+ for (const [i, cmd] of cmds.entries()) lines.push(`${found[i] ? "\u2713" : "\u2717"} ${cmd}`);
137
+ return lines.join("\n");
138
+ }
139
+
140
+ // src/slash/export.ts
141
+ function exportMarkdown(messages, title) {
142
+ const parts = [`# ${title}`, ""];
143
+ for (const m of messages) {
144
+ if (m.role === "user") parts.push("## User", "", String(m.content), "");
145
+ else if (m.role === "assistant") {
146
+ for (const c of m.tool_calls ?? []) parts.push(`\u23BF ${c.function.name} ${c.function.arguments.slice(0, 200)}`, "");
147
+ if (m.content) parts.push("## BCCLI", "", m.content, "");
148
+ }
149
+ }
150
+ return parts.join("\n");
151
+ }
152
+
153
+ // src/slash/info.ts
154
+ var k = (n) => n >= 1e3 ? `${Math.round(n / 1e3)}k` : String(n);
155
+ function statusText(s) {
156
+ const ready = s.mcp.filter((m) => m.status === "ready").length;
157
+ const errors = s.mcp.filter((m) => m.status === "error").length;
158
+ return [
159
+ `BCCLI ${s.version}`,
160
+ `Model: ${s.modelRef} (${s.providerLabel})`,
161
+ `Mode izin: ${s.mode}`,
162
+ `Folder: ${s.cwd}`,
163
+ `MCP: ${ready} aktif, ${errors} error`,
164
+ `Token sesi: ${k(s.usage.inputTokens)} masuk \xB7 ${k(s.usage.outputTokens)} keluar`,
165
+ `Konteks: \xB1${Math.round(s.lastInputTokens / 128e3 * 100)}% dari 128k`
166
+ ].join("\n");
167
+ }
168
+ function agentsText(defs) {
169
+ 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");
170
+ }
171
+ function skillsText(skills, commands) {
172
+ const s = skills.length ? skills.map((x) => `${x.name} \u2014 ${x.description.slice(0, 80)} (${x.dir})`).join("\n") : "(tidak ada skill)";
173
+ const c = commands.length ? commands.map((x) => `/${x.name} \u2014 ${x.description ?? ""}`).join("\n") : "(tidak ada perintah custom)";
174
+ return `Skill:
175
+ ${s}
176
+
177
+ Perintah custom:
178
+ ${c}`;
179
+ }
180
+ function sessionText(s) {
181
+ return [
182
+ `Mulai: ${s.started.toLocaleString("id-ID")}`,
183
+ `${s.messages} pesan \xB7 ${k(s.usage.inputTokens)} token masuk \xB7 ${k(s.usage.outputTokens)} keluar`,
184
+ `Model: ${s.modelRef}`,
185
+ `File: ${s.file}`
186
+ ].join("\n");
187
+ }
188
+
189
+ // src/slash/memory.ts
190
+ import { appendFileSync, existsSync, readFileSync } from "fs";
191
+ function instructionFiles(cwd, home) {
192
+ return instructionPaths(cwd, home).filter((path) => existsSync(path)).map((path) => ({ path, lines: readFileSync(path, "utf8").split("\n").filter(Boolean).length }));
193
+ }
194
+ function appendMemory(file, text) {
195
+ const prefix = existsSync(file) && !readFileSync(file, "utf8").endsWith("\n") ? "\n" : "";
196
+ appendFileSync(file, `${prefix}- ${text.trim()}
197
+ `);
198
+ }
199
+ function parseMemoryArgs(args) {
200
+ const match = /^global(?:\s+|$)/i.exec(args.trim());
201
+ return match ? { global: true, text: args.trim().slice(match[0].length).trim() } : { global: false, text: args.trim() };
202
+ }
203
+
61
204
  // src/ui/Markdown.tsx
62
205
  import { Box, Text } from "ink";
63
206
 
@@ -116,14 +259,48 @@ function LinePrompt({ label, mask, onSubmit, onCancel }) {
116
259
  ] }) });
117
260
  }
118
261
 
119
- // src/ui/ModelPicker.tsx
262
+ // src/ui/ListPicker.tsx
120
263
  import { Box as Box3, Text as Text3, useInput as useInput2 } from "ink";
121
264
  import { useState as useState2 } from "react";
122
265
  import { jsx as jsx3, jsxs as jsxs2 } from "react/jsx-runtime";
123
- var VISIBLE = 14;
124
- function ModelPicker({ groups, current, onPick }) {
266
+ var VISIBLE = 12;
267
+ function ListPicker({ title, items, onPick }) {
125
268
  const [filter, setFilter] = useState2("");
126
269
  const [index, setIndex] = useState2(0);
270
+ const shown = items.filter((i) => i.label.toLowerCase().includes(filter.toLowerCase()));
271
+ const cursor = Math.min(index, Math.max(0, shown.length - 1));
272
+ useInput2((input, key) => {
273
+ if (key.upArrow) setIndex(Math.max(0, cursor - 1));
274
+ else if (key.downArrow) setIndex(Math.min(shown.length - 1, cursor + 1));
275
+ else if (key.return) onPick(shown[cursor]?.id);
276
+ else if (key.escape) onPick(void 0);
277
+ else if (key.backspace || key.delete) {
278
+ setFilter((f) => f.slice(0, -1));
279
+ setIndex(0);
280
+ } else if (!key.ctrl && !key.meta && !key.tab && input) {
281
+ setFilter((f) => f + input);
282
+ setIndex(0);
283
+ }
284
+ });
285
+ const start = Math.max(0, Math.min(cursor - 5, shown.length - VISIBLE));
286
+ return /* @__PURE__ */ jsxs2(Box3, { flexDirection: "column", borderStyle: "round", borderColor: color("cyan"), paddingX: 1, children: [
287
+ /* @__PURE__ */ jsx3(Text3, { bold: true, children: `${title} (\u2191\u2193 enter \xB7 ketik untuk menyaring${filter ? `: ${filter}` : ""} \xB7 esc batal)` }),
288
+ shown.slice(start, start + VISIBLE).map((item, i) => /* @__PURE__ */ jsxs2(Text3, { color: start + i === cursor ? color("green") : void 0, children: [
289
+ `${start + i === cursor ? "\u203A" : " "} ${item.label}`,
290
+ item.hint ? /* @__PURE__ */ jsx3(Text3, { dimColor: true, children: ` ${item.hint}` }) : null
291
+ ] }, item.id)),
292
+ shown.length ? null : /* @__PURE__ */ jsx3(Text3, { dimColor: true, children: "Tidak ada yang cocok." })
293
+ ] });
294
+ }
295
+
296
+ // src/ui/ModelPicker.tsx
297
+ import { Box as Box4, Text as Text4, useInput as useInput3 } from "ink";
298
+ import { useState as useState3 } from "react";
299
+ import { jsx as jsx4, jsxs as jsxs3 } from "react/jsx-runtime";
300
+ var VISIBLE2 = 14;
301
+ function ModelPicker({ groups, current, onPick }) {
302
+ const [filter, setFilter] = useState3("");
303
+ const [index, setIndex] = useState3(0);
127
304
  const needle = filter.toLowerCase();
128
305
  const rows = [];
129
306
  for (const g of groups) {
@@ -135,7 +312,7 @@ function ModelPicker({ groups, current, onPick }) {
135
312
  }
136
313
  const selectable = rows.flatMap((r, i) => r.kind === "model" ? [i] : []);
137
314
  const cursor = selectable[Math.min(index, selectable.length - 1)] ?? -1;
138
- useInput2((input, key) => {
315
+ useInput3((input, key) => {
139
316
  if (key.upArrow) setIndex((i) => Math.max(0, i - 1));
140
317
  else if (key.downArrow) setIndex((i) => Math.min(selectable.length - 1, i + 1));
141
318
  else if (key.return) {
@@ -150,86 +327,86 @@ function ModelPicker({ groups, current, onPick }) {
150
327
  setIndex(0);
151
328
  }
152
329
  });
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)
330
+ const start = Math.max(0, Math.min(cursor - 5, rows.length - VISIBLE2));
331
+ return /* @__PURE__ */ jsxs3(Box4, { flexDirection: "column", borderStyle: "round", borderColor: color("cyan"), paddingX: 1, children: [
332
+ /* @__PURE__ */ jsx4(Text4, { bold: true, children: `Pilih model (ketik untuk menyaring${filter ? `: ${filter}` : ""} \xB7 \u2191\u2193 enter \xB7 esc batal)` }),
333
+ rows.slice(start, start + VISIBLE2).map(
334
+ (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
335
  ),
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}`))
336
+ !selectable.length ? /* @__PURE__ */ jsx4(Text4, { dimColor: true, children: "Tidak ada model yang cocok." }) : null,
337
+ groups.filter((g) => g.error).map((g) => /* @__PURE__ */ jsx4(Text4, { color: color("yellow"), children: `${g.providerName} \u2014 ${g.error}` }, `e-${g.providerId}`))
161
338
  ] });
162
339
  }
163
340
 
164
341
  // 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";
342
+ import { Box as Box5, Text as Text5, useInput as useInput4 } from "ink";
343
+ import { useState as useState4 } from "react";
344
+ import { jsx as jsx5, jsxs as jsxs4 } from "react/jsx-runtime";
168
345
  function ProviderMenu({ entries, onPick }) {
169
346
  const rows = [...entries.map((e) => e.id), "custom"];
170
- const [index, setIndex] = useState3(0);
171
- useInput3((_input, key) => {
347
+ const [index, setIndex] = useState4(0);
348
+ useInput4((_input, key) => {
172
349
  if (key.upArrow) setIndex((i) => Math.max(0, i - 1));
173
350
  else if (key.downArrow) setIndex((i) => Math.min(rows.length - 1, i + 1));
174
351
  else if (key.return) onPick(rows[index]);
175
352
  else if (key.escape) onPick(void 0);
176
353
  });
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: [
354
+ return /* @__PURE__ */ jsxs4(Box5, { flexDirection: "column", borderStyle: "round", borderColor: color("cyan"), paddingX: 1, children: [
355
+ /* @__PURE__ */ jsx5(Text5, { bold: true, children: "Provider (\u2191\u2193 enter, esc batal) \xB7 \u2713 siap \xB7 \u25CB butuh API key" }),
356
+ entries.map((e, i) => /* @__PURE__ */ jsxs4(Text5, { color: i === index ? color("green") : void 0, children: [
180
357
  `${i === index ? "\u203A" : " "} ${e.ready ? "\u2713" : "\u25CB"} ${e.name}`,
181
- /* @__PURE__ */ jsx4(Text4, { dimColor: true, children: ` ${e.baseURL}` })
358
+ /* @__PURE__ */ jsx5(Text5, { dimColor: true, children: ` ${e.baseURL}` })
182
359
  ] }, e.id)),
183
- /* @__PURE__ */ jsx4(Text4, { color: index === rows.length - 1 ? color("green") : void 0, children: `${index === rows.length - 1 ? "\u203A" : " "} Custom\u2026` })
360
+ /* @__PURE__ */ jsx5(Text5, { color: index === rows.length - 1 ? color("green") : void 0, children: `${index === rows.length - 1 ? "\u203A" : " "} Custom\u2026` })
184
361
  ] });
185
362
  }
186
363
 
187
364
  // src/ui/PermissionPrompt.tsx
188
- import { Box as Box6, Text as Text6, useInput as useInput4 } from "ink";
365
+ import { Box as Box7, Text as Text7, useInput as useInput5 } from "ink";
189
366
 
190
367
  // src/ui/DiffView.tsx
191
- import { Box as Box5, Text as Text5 } from "ink";
192
- import { jsx as jsx5 } from "react/jsx-runtime";
368
+ import { Box as Box6, Text as Text6 } from "ink";
369
+ import { jsx as jsx6 } from "react/jsx-runtime";
193
370
  function DiffView({ diff }) {
194
- return /* @__PURE__ */ jsx5(Box5, { flexDirection: "column", children: diff.split("\n").map((line, i) => {
371
+ return /* @__PURE__ */ jsx6(Box6, { flexDirection: "column", children: diff.split("\n").map((line, i) => {
195
372
  const mark = line[6];
196
373
  const c = mark === "+" ? color("green") : mark === "-" ? color("red") : void 0;
197
- return /* @__PURE__ */ jsx5(Text5, { color: c, dimColor: mark !== "+" && mark !== "-", children: line }, i);
374
+ return /* @__PURE__ */ jsx6(Text6, { color: c, dimColor: mark !== "+" && mark !== "-", children: line }, i);
198
375
  }) });
199
376
  }
200
377
 
201
378
  // src/ui/PermissionPrompt.tsx
202
- import { jsx as jsx6, jsxs as jsxs4 } from "react/jsx-runtime";
379
+ import { jsx as jsx7, jsxs as jsxs5 } from "react/jsx-runtime";
203
380
  var VERB = { edit: "edit", write: "tulis", bash: "jalankan", fetch: "ambil" };
204
381
  function PermissionPrompt({
205
382
  request,
206
383
  onAnswer
207
384
  }) {
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");
385
+ useInput5((input, key) => {
386
+ const k2 = input.toLowerCase();
387
+ if (k2 === "y") onAnswer("yes");
388
+ else if (k2 === "a" && request.sessionRules?.length) onAnswer("session");
389
+ else if (k2 === "s") onAnswer("all");
390
+ else if (k2 === "n" || key.escape) onAnswer("no");
214
391
  });
215
392
  const rules = request.sessionRules ?? [];
216
393
  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" }),
394
+ return /* @__PURE__ */ jsxs5(Box7, { flexDirection: "column", borderStyle: "round", borderColor: color("yellow"), paddingX: 1, children: [
395
+ /* @__PURE__ */ jsx7(Text7, { bold: true, children: `${request.agent ? `[${request.agent}] ` : ""}Izinkan ${VERB[request.tool] ?? request.tool} ${request.target}?` }),
396
+ request.preview ? /* @__PURE__ */ jsx7(DiffView, { diff: request.preview }) : null,
397
+ /* @__PURE__ */ jsxs5(Text7, { children: [
398
+ /* @__PURE__ */ jsx7(Text7, { color: color("green"), children: "[y] ya" }),
222
399
  " ",
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" })
400
+ sessionLabel ? /* @__PURE__ */ jsx7(Text7, { color: color("cyan"), children: `[a] ${sessionLabel} ` }) : null,
401
+ /* @__PURE__ */ jsx7(Text7, { color: color("yellow"), children: "[s] ya semua " }),
402
+ /* @__PURE__ */ jsx7(Text7, { color: color("red"), children: "[n] tidak" })
226
403
  ] })
227
404
  ] });
228
405
  }
229
406
 
230
407
  // src/ui/PromptInput.tsx
231
- import { Box as Box7, Text as Text7, useInput as useInput5 } from "ink";
232
- import { useState as useState4 } from "react";
408
+ import { Box as Box8, Text as Text8, useInput as useInput6 } from "ink";
409
+ import { useState as useState5 } from "react";
233
410
 
234
411
  // src/ui/complete.ts
235
412
  import fg from "fast-glob";
@@ -245,11 +422,11 @@ function completeFile(value, cwd) {
245
422
  }
246
423
 
247
424
  // src/ui/PromptInput.tsx
248
- import { jsx as jsx7, jsxs as jsxs5 } from "react/jsx-runtime";
425
+ import { jsx as jsx8, jsxs as jsxs6 } from "react/jsx-runtime";
249
426
  function PromptInput({ disabled, history, cwd, onSubmit, extraCommands }) {
250
- const [value, setValue] = useState4("");
251
- const [historyIndex, setHistoryIndex] = useState4(null);
252
- const [selected, setSelected] = useState4(0);
427
+ const [value, setValue] = useState5("");
428
+ const [historyIndex, setHistoryIndex] = useState5(null);
429
+ const [selected, setSelected] = useState5(0);
253
430
  const all = [...SLASH_COMMANDS, ...(extraCommands ?? []).filter((c) => !SLASH_COMMANDS.some((b) => b.name === c.name))];
254
431
  const suggestions = value.startsWith("/") && !/\s/.test(value) ? all.filter((c) => c.name.startsWith(value.slice(1))).slice(0, 8) : [];
255
432
  const highlighted = Math.min(selected, Math.max(0, suggestions.length - 1));
@@ -257,7 +434,7 @@ function PromptInput({ disabled, history, cwd, onSubmit, extraCommands }) {
257
434
  setValue(next);
258
435
  setSelected(0);
259
436
  };
260
- useInput5(
437
+ useInput6(
261
438
  (input, key) => {
262
439
  if (suggestions.length) {
263
440
  if (key.upArrow) {
@@ -319,34 +496,34 @@ function PromptInput({ disabled, history, cwd, onSubmit, extraCommands }) {
319
496
  },
320
497
  { isActive: !disabled }
321
498
  );
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: "> " }),
499
+ return /* @__PURE__ */ jsxs6(Box8, { flexDirection: "column", children: [
500
+ /* @__PURE__ */ jsx8(Box8, { borderStyle: "round", borderColor: color("gray"), paddingX: 1, children: /* @__PURE__ */ jsxs6(Text8, { children: [
501
+ /* @__PURE__ */ jsx8(Text8, { color: color("green"), children: "> " }),
325
502
  value,
326
- disabled ? "" : /* @__PURE__ */ jsx7(Text7, { inverse: true, children: " " })
503
+ disabled ? "" : /* @__PURE__ */ jsx8(Text8, { inverse: true, children: " " })
327
504
  ] }) }),
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))
505
+ 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
506
  ] });
330
507
  }
331
508
 
332
509
  // 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";
510
+ import { Text as Text9 } from "ink";
511
+ import { useEffect, useState as useState6 } from "react";
512
+ import { jsx as jsx9 } from "react/jsx-runtime";
336
513
  var FRAMES = ["\xB7", "\u2722", "\u2733", "\u2736", "\u273B", "\u273D"];
337
514
  function Spinner({ label, startedAt }) {
338
- const [tick, setTick] = useState5(0);
515
+ const [tick, setTick] = useState6(0);
339
516
  useEffect(() => {
340
517
  const id = setInterval(() => setTick((t) => t + 1), 120);
341
518
  return () => clearInterval(id);
342
519
  }, []);
343
520
  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)` });
521
+ return /* @__PURE__ */ jsx9(Text9, { color: color(ACCENT), children: `${FRAMES[tick % FRAMES.length]} ${label}\u2026 (${seconds} dtk \xB7 esc batal)` });
345
522
  }
346
523
 
347
524
  // src/ui/StatusBar.tsx
348
- import { Text as Text9 } from "ink";
349
- import { jsx as jsx9 } from "react/jsx-runtime";
525
+ import { Text as Text10 } from "ink";
526
+ import { jsx as jsx10 } from "react/jsx-runtime";
350
527
  var LABEL = {
351
528
  default: "\u23F5 default",
352
529
  acceptEdits: "\u23F5 accept edits",
@@ -358,87 +535,87 @@ function formatTokens(n) {
358
535
  }
359
536
  function StatusBar({ mode, tokens, busy, model }) {
360
537
  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"}` });
538
+ 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
539
  }
363
540
 
364
541
  // 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";
542
+ import { Box as Box9, Text as Text11, useInput as useInput7 } from "ink";
543
+ import { useState as useState7 } from "react";
544
+ import { jsx as jsx11, jsxs as jsxs7 } from "react/jsx-runtime";
368
545
  var MARK = { ready: "\u2713", error: "\u2717", starting: "\u2026", off: "\u25CB" };
369
546
  function McpMenu({ items, onPick }) {
370
- const [index, setIndex] = useState6(0);
371
- useInput6((_input, key) => {
547
+ const [index, setIndex] = useState7(0);
548
+ useInput7((_input, key) => {
372
549
  if (key.upArrow) setIndex((i) => Math.max(0, i - 1));
373
550
  else if (key.downArrow) setIndex((i) => Math.min(items.length - 1, i + 1));
374
551
  else if (key.return) onPick(items[index]?.name);
375
552
  else if (key.escape) onPick(void 0);
376
553
  });
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: [
554
+ return /* @__PURE__ */ jsxs7(Box9, { flexDirection: "column", borderStyle: "round", borderColor: color("cyan"), paddingX: 1, children: [
555
+ /* @__PURE__ */ jsx11(Text11, { bold: true, children: "Server MCP (enter: pasang / hapus \xB7 esc tutup) \xB7 \u2713 aktif \u25CB belum \u2717 error" }),
556
+ items.map((item, i) => /* @__PURE__ */ jsxs7(Text11, { color: i === index ? color("green") : void 0, children: [
380
557
  `${i === index ? "\u203A" : " "} ${MARK[item.status]} ${item.name.padEnd(12)}`,
381
- /* @__PURE__ */ jsx10(Text10, { dimColor: true, children: item.status === "error" ? ` ${item.error ?? ""}` : ` ${item.description}` })
558
+ /* @__PURE__ */ jsx11(Text11, { dimColor: true, children: item.status === "error" ? ` ${item.error ?? ""}` : ` ${item.description}` })
382
559
  ] }, item.name))
383
560
  ] });
384
561
  }
385
562
 
386
563
  // 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";
564
+ import { Box as Box10, Text as Text12, useInput as useInput8 } from "ink";
565
+ import { jsx as jsx12, jsxs as jsxs8 } from "react/jsx-runtime";
389
566
  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");
567
+ useInput8((input, key) => {
568
+ const k2 = input.toLowerCase();
569
+ if (k2 === "a") onAnswer("acceptEdits");
570
+ else if (k2 === "y") onAnswer("default");
571
+ else if (k2 === "n" || key.escape) onAnswer("no");
395
572
  });
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" }),
573
+ return /* @__PURE__ */ jsxs8(Box10, { flexDirection: "column", borderStyle: "round", borderColor: color("cyan"), paddingX: 1, children: [
574
+ /* @__PURE__ */ jsx12(Text12, { bold: true, children: "Rencana dari agent" }),
575
+ /* @__PURE__ */ jsx12(Markdown, { text: plan }),
576
+ /* @__PURE__ */ jsxs8(Text12, { children: [
577
+ /* @__PURE__ */ jsx12(Text12, { color: color("green"), children: "[a] ya, edit otomatis" }),
401
578
  " ",
402
- /* @__PURE__ */ jsx11(Text11, { color: color("cyan"), children: "[y] ya, tanya tiap langkah" }),
579
+ /* @__PURE__ */ jsx12(Text12, { color: color("cyan"), children: "[y] ya, tanya tiap langkah" }),
403
580
  " ",
404
- /* @__PURE__ */ jsx11(Text11, { color: color("red"), children: "[n] belum, perbaiki dulu" })
581
+ /* @__PURE__ */ jsx12(Text12, { color: color("red"), children: "[n] belum, perbaiki dulu" })
405
582
  ] })
406
583
  ] });
407
584
  }
408
585
 
409
586
  // src/ui/TodoList.tsx
410
- import { Box as Box10, Text as Text12 } from "ink";
411
- import { jsx as jsx12 } from "react/jsx-runtime";
587
+ import { Box as Box11, Text as Text13 } from "ink";
588
+ import { jsx as jsx13 } from "react/jsx-runtime";
412
589
  var MARK2 = { completed: "\u2611", in_progress: "\u25FC", pending: "\u2610" };
413
590
  function TodoList({ items }) {
414
591
  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)) });
592
+ 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
593
  }
417
594
 
418
595
  // 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";
596
+ import { Box as Box12, Text as Text14 } from "ink";
597
+ import { jsx as jsx14, jsxs as jsxs9 } from "react/jsx-runtime";
421
598
  var PREVIEW_LINES = 5;
422
599
  function ToolBlock({ tool, target, output, display, isError, done, expanded, sub }) {
423
600
  const isTask = tool === "task";
424
601
  const isDiff = !!display && /^\s*\d+ [+\- ] /.test(display);
425
602
  const lines = (output ?? "").split("\n");
426
603
  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
604
+ return /* @__PURE__ */ jsxs9(Box12, { flexDirection: "column", marginLeft: 2, children: [
605
+ /* @__PURE__ */ jsxs9(Text14, { children: [
606
+ /* @__PURE__ */ jsx14(Text14, { color: isError ? color("red") : done ? color("green") : color("yellow"), children: "\u23BF " }),
607
+ /* @__PURE__ */ jsx14(Text14, { bold: true, children: toolLabel(tool) }),
608
+ /* @__PURE__ */ jsx14(Text14, { children: ` ${target}` }),
609
+ display && !isDiff && !isTask ? /* @__PURE__ */ jsx14(Text14, { dimColor: true, children: ` (${display})` }) : null
433
610
  ] }),
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
611
+ sub?.length ? /* @__PURE__ */ jsxs9(Box12, { flexDirection: "column", marginLeft: 3, children: [
612
+ 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)),
613
+ sub.length > 6 ? /* @__PURE__ */ jsx14(Text14, { dimColor: true, children: `\u2026 ${sub.length - 6} langkah sebelumnya` }) : null
437
614
  ] }) : 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
615
+ 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,
616
+ 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: [
617
+ shown.map((line, i) => /* @__PURE__ */ jsx14(Text14, { dimColor: !isError, color: isError ? color("red") : void 0, children: line }, i)),
618
+ lines.length > shown.length ? /* @__PURE__ */ jsx14(Text14, { dimColor: true, children: `\u2026 ${lines.length - shown.length} baris lagi (ctrl+o)` }) : null
442
619
  ] }) : null
443
620
  ] });
444
621
  }
@@ -500,11 +677,11 @@ function applyEvent(t, event) {
500
677
  }
501
678
 
502
679
  // src/ui/App.tsx
503
- import { jsx as jsx14, jsxs as jsxs9 } from "react/jsx-runtime";
680
+ import { jsx as jsx15, jsxs as jsxs10 } from "react/jsx-runtime";
504
681
  function helpText(runtime) {
505
682
  const custom = runtime.commands.filter((c) => !SLASH_COMMANDS.some((b) => b.name === c.name));
506
683
  return [
507
- ...SLASH_COMMANDS.map((c) => `/${c.name.padEnd(8)} ${c.description}`),
684
+ ...SLASH_COMMANDS.map((c) => `/${c.name.padEnd(12)} ${c.description}`),
508
685
  ...custom.length ? ["", "Perintah custom:", ...custom.map((c) => `/${c.name.padEnd(8)} ${c.description ?? ""}`)] : [],
509
686
  ...runtime.skills.length ? ["", "Skill:", ...runtime.skills.map((s) => `/${s.name.padEnd(8)} ${s.description.slice(0, 60)}`)] : [],
510
687
  "",
@@ -513,27 +690,29 @@ function helpText(runtime) {
513
690
  "@file + tab lengkapi nama file \u2191\u2193 riwayat input"
514
691
  ].join("\n");
515
692
  }
693
+ 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.";
516
694
  var labelFor = (runtime, ref) => `${ref.slice(ref.indexOf("/") + 1)} \xB7 ${runtime.providerLabel()}`;
517
695
  function App({ runtime, initialPrompt, version }) {
518
696
  const { exit } = useApp();
519
- const [transcript, setTranscript] = useState7(() => ({ done: [entry({ kind: "header" })], live: [] }));
520
- const [busy, setBusy] = useState7(false);
521
- const [startedAt, setStartedAt] = useState7(0);
522
- const [mode, setMode] = useState7(runtime.agent.permissions.mode);
523
- const [tokens, setTokens] = useState7(runtime.agent.totalUsage.inputTokens + runtime.agent.totalUsage.outputTokens);
524
- const [asks, setAsks] = useState7([]);
697
+ const [transcript, setTranscript] = useState8(() => ({ done: [entry({ kind: "header" })], live: [] }));
698
+ const [busy, setBusy] = useState8(false);
699
+ const [startedAt, setStartedAt] = useState8(0);
700
+ const [mode, setMode] = useState8(runtime.agent.permissions.mode);
701
+ const [tokens, setTokens] = useState8(runtime.agent.totalUsage.inputTokens + runtime.agent.totalUsage.outputTokens);
702
+ const [asks, setAsks] = useState8([]);
525
703
  const pending = asks[0] ?? null;
526
704
  const askId = useRef(0);
527
- const [picker, setPicker] = useState7(null);
528
- const [providerMenu, setProviderMenu] = useState7(null);
529
- const [prompt, setPrompt] = useState7(null);
530
- const ask = (label, mask = false) => new Promise((resolve) => setPrompt({ label, mask, resolve }));
531
- const [modelLabel, setModelLabel] = useState7(() => labelFor(runtime, runtime.modelRef));
532
- const [history, setHistory] = useState7([]);
705
+ const [picker, setPicker] = useState8(null);
706
+ const [providerMenu, setProviderMenu] = useState8(null);
707
+ const [prompt, setPrompt] = useState8(null);
708
+ const ask = useCallback((label, mask = false) => new Promise((resolve2) => setPrompt({ label, mask, resolve: resolve2 })), []);
709
+ const [modelLabel, setModelLabel] = useState8(() => labelFor(runtime, runtime.modelRef));
710
+ const [history, setHistory] = useState8([]);
533
711
  const controller = useRef(null);
534
- const [todos, setTodos] = useState7(runtime.todos.items);
535
- const [mcpMenu, setMcpMenu] = useState7(null);
536
- const [planAsk, setPlanAsk] = useState7(null);
712
+ const [todos, setTodos] = useState8(runtime.todos.items);
713
+ const [mcpMenu, setMcpMenu] = useState8(null);
714
+ const [listPicker, setListPicker] = useState8(null);
715
+ const [planAsk, setPlanAsk] = useState8(null);
537
716
  useEffect2(() => runtime.todos.subscribe(setTodos), [runtime]);
538
717
  const extraCommands = [
539
718
  ...runtime.commands.filter((c) => !SLASH_COMMANDS.some((b) => b.name === c.name.toLowerCase())).map((c) => ({ name: c.name.toLowerCase(), description: c.description ?? "perintah custom" })),
@@ -554,8 +733,8 @@ function App({ runtime, initialPrompt, version }) {
554
733
  }, []);
555
734
  useEffect2(() => {
556
735
  runtime.agent.onEvent = onEvent;
557
- runtime.agent.askPermission = (request) => new Promise((resolve) => setAsks((q) => [...q, { id: askId.current++, request, resolve }]));
558
- runtime.interaction.approvePlan = (plan) => new Promise((resolve) => setPlanAsk({ plan, resolve }));
736
+ runtime.agent.askPermission = (request) => new Promise((resolve2) => setAsks((q) => [...q, { id: askId.current++, request, resolve: resolve2 }]));
737
+ runtime.interaction.approvePlan = (plan) => new Promise((resolve2) => setPlanAsk({ plan, resolve: resolve2 }));
559
738
  }, [runtime, onEvent]);
560
739
  const runTurn = useCallback(
561
740
  async (text) => {
@@ -570,7 +749,7 @@ function App({ runtime, initialPrompt, version }) {
570
749
  },
571
750
  [runtime]
572
751
  );
573
- const runSlash = useCallback(
752
+ const runSlashCommand = useCallback(
574
753
  async (name, args) => {
575
754
  switch (name) {
576
755
  case "help":
@@ -634,6 +813,235 @@ function App({ runtime, initialPrompt, version }) {
634
813
  setMcpMenu(items);
635
814
  return;
636
815
  }
816
+ case "new":
817
+ runtime.newSession();
818
+ setTokens(0);
819
+ notice("Sesi baru dimulai. Sesi sebelumnya tetap tersimpan (/resume).");
820
+ return;
821
+ case "session":
822
+ notice(
823
+ sessionText({
824
+ file: runtime.session.file,
825
+ started: runtime.startedAt,
826
+ messages: runtime.agent.messages.length,
827
+ usage: runtime.agent.totalUsage,
828
+ modelRef: runtime.modelRef
829
+ })
830
+ );
831
+ return;
832
+ case "resume": {
833
+ const others = Session.list(runtime.home, runtime.cwd).filter((s) => s.session.file !== runtime.session.file);
834
+ if (!others.length) {
835
+ notice("Belum ada sesi lain di folder ini.");
836
+ return;
837
+ }
838
+ setListPicker({
839
+ title: "Lanjutkan sesi",
840
+ items: others.map((s) => ({
841
+ id: s.session.file,
842
+ label: `${s.mtime.toLocaleString("id-ID")} ${s.preview || "(kosong)"}`,
843
+ hint: `${s.session.load().length} pesan`
844
+ })),
845
+ onPick: (id) => {
846
+ setListPicker(null);
847
+ const chosen = others.find((s) => s.session.file === id);
848
+ if (!chosen) return;
849
+ runtime.resume(chosen.session);
850
+ setTokens(0);
851
+ const tail = runtime.agent.messages.filter((m) => (m.role === "user" || m.role === "assistant") && m.content).slice(-3);
852
+ notice(
853
+ [`Melanjutkan sesi (${runtime.agent.messages.length} pesan):`, ...tail.map((m) => `${m.role === "user" ? ">" : "\u25CF"} ${String(m.content).slice(0, 200)}`)].join(
854
+ "\n"
855
+ )
856
+ );
857
+ }
858
+ });
859
+ return;
860
+ }
861
+ case "status":
862
+ notice(
863
+ statusText({
864
+ version,
865
+ modelRef: runtime.modelRef,
866
+ providerLabel: runtime.providerLabel(),
867
+ mode: runtime.agent.permissions.mode,
868
+ cwd: runtime.cwd,
869
+ mcp: runtime.mcp.states(),
870
+ usage: runtime.agent.totalUsage,
871
+ lastInputTokens: runtime.agent.lastInputTokens
872
+ })
873
+ );
874
+ return;
875
+ case "agents":
876
+ notice(agentsText([...BUILTIN_AGENTS, ...runtime.agentDefs]));
877
+ return;
878
+ case "skills":
879
+ notice(skillsText(runtime.skills, runtime.commands));
880
+ return;
881
+ case "doctor": {
882
+ notice("Memeriksa\u2026");
883
+ const c = runtime.config;
884
+ notice(
885
+ await doctorText(
886
+ {
887
+ providers: Object.keys(c.providers).map((id) => ({ id, name: providerName(c, id), ready: hasKey(c, id, runtime.env) })),
888
+ mcp: runtime.mcp.states(),
889
+ activeProvider: runtime.providerLabel()
890
+ },
891
+ {
892
+ which: (cmd) => new Promise((done) => execFile2(process.platform === "win32" ? "where" : "which", [cmd], (error) => done(!error))),
893
+ nodeVersion: process.version,
894
+ listModels: () => runtime.agent.provider.listModels()
895
+ }
896
+ )
897
+ );
898
+ return;
899
+ }
900
+ case "permissions": {
901
+ const rules = runtime.agent.permissions.list();
902
+ if (!rules.length) {
903
+ notice("Belum ada izin tersimpan. Izin dari jawaban [a] dan dari config akan muncul di sini.");
904
+ return;
905
+ }
906
+ setListPicker({
907
+ title: "Izin aktif (enter: cabut izin sesi)",
908
+ items: rules.map((r) => ({ id: r.rule, label: r.rule, hint: r.source === "config" ? "config" : "sesi ini" })),
909
+ onPick: (id) => {
910
+ setListPicker(null);
911
+ const rule = rules.find((r) => r.rule === id);
912
+ if (!rule) return;
913
+ if (rule.source === "config") notice(`${rule.rule} berasal dari config/flag; hapus dari ~/.bccli/config.json untuk mencabutnya.`, "warn");
914
+ else {
915
+ runtime.agent.permissions.revoke(rule.rule);
916
+ notice(`Izin ${rule.rule} dicabut.`);
917
+ }
918
+ }
919
+ });
920
+ return;
921
+ }
922
+ case "memory": {
923
+ if (!args) {
924
+ const files = instructionFiles(runtime.cwd, runtime.home);
925
+ notice(files.length ? files.map((f) => `${f.path} (${f.lines} baris)`).join("\n") : "Belum ada AGENTS.md / BCCLI.md. Tambah dengan /memory <teks>.");
926
+ return;
927
+ }
928
+ const { global, text } = parseMemoryArgs(args);
929
+ if (!text) {
930
+ notice("Tulis teksnya: /memory global <teks>.", "warn");
931
+ return;
932
+ }
933
+ const file = global ? join(runtime.home, "BCCLI.md") : join(runtime.cwd, "AGENTS.md");
934
+ try {
935
+ mkdirSync(dirname(file), { recursive: true });
936
+ appendMemory(file, text);
937
+ } catch (error) {
938
+ notice(`Gagal menulis ${file}: ${error.message}`, "error");
939
+ return;
940
+ }
941
+ runtime.rebuildSystemPrompt();
942
+ notice(`Ditambahkan ke ${file}.`);
943
+ return;
944
+ }
945
+ case "init":
946
+ void runTurn(INIT_PROMPT);
947
+ return;
948
+ case "login": {
949
+ const id = runtime.modelRef.slice(0, runtime.modelRef.indexOf("/"));
950
+ const key = await ask(`API key untuk ${runtime.providerLabel()}`, true);
951
+ if (!key) return;
952
+ const result = await runtime.addProviderKey(id, key);
953
+ if (!result.ok) {
954
+ notice(result.error, "error");
955
+ return;
956
+ }
957
+ runtime.setModel(runtime.modelRef);
958
+ const envName = runtime.config.providers[id]?.apiKeyEnv;
959
+ if (envName && runtime.env[envName]) {
960
+ notice(`Key ${runtime.providerLabel()} tersimpan, tapi ${envName} di environment lebih diutamakan dan tetap dipakai.`, "warn");
961
+ return;
962
+ }
963
+ notice(`Key ${runtime.providerLabel()} tersimpan \xB7 ${result.models} model.`);
964
+ return;
965
+ }
966
+ case "logout": {
967
+ const id = runtime.modelRef.slice(0, runtime.modelRef.indexOf("/"));
968
+ const removed = removeCredential(id, runtime.env);
969
+ const envName = runtime.config.providers[id]?.apiKeyEnv;
970
+ if (envName && runtime.env[envName]) {
971
+ notice(
972
+ `${removed ? "Key tersimpan dihapus, tapi" : "Tidak ada key tersimpan;"} ${envName} masih ada di environment dan tetap dipakai.`,
973
+ "warn"
974
+ );
975
+ return;
976
+ }
977
+ if (!removed) {
978
+ notice(`Tidak ada key tersimpan untuk ${runtime.providerLabel()}.`);
979
+ return;
980
+ }
981
+ try {
982
+ runtime.setModel(runtime.modelRef);
983
+ } catch (error) {
984
+ const fail = async () => {
985
+ throw error;
986
+ };
987
+ runtime.agent.provider = { chat: fail, listModels: fail };
988
+ }
989
+ notice(`Key ${runtime.providerLabel()} dihapus. Pakai /login atau /provider untuk menambah lagi.`);
990
+ return;
991
+ }
992
+ case "diff":
993
+ notice(await gitDiff(runtime.cwd));
994
+ return;
995
+ case "undo": {
996
+ const result = await runtime.checkpoints.undo();
997
+ if (!result) {
998
+ notice("Tidak ada edit file yang bisa dibatalkan.");
999
+ return;
1000
+ }
1001
+ const rel = (p) => relative(runtime.cwd, p) || p;
1002
+ notice(
1003
+ [
1004
+ result.failed.length ? `Gagal dikembalikan: ${result.failed.map(rel).join(", ")}` : "",
1005
+ result.restored.length ? `Dikembalikan: ${result.restored.map(rel).join(", ")}` : "",
1006
+ result.deleted.length ? `Dihapus (file baru): ${result.deleted.map(rel).join(", ")}` : "",
1007
+ result.skipped.length ? `Terlalu besar untuk disimpan, tidak diubah: ${result.skipped.map(rel).join(", ")}` : "",
1008
+ "Catatan: perubahan lewat perintah bash tidak bisa di-undo."
1009
+ ].filter(Boolean).join("\n")
1010
+ );
1011
+ return;
1012
+ }
1013
+ case "copy": {
1014
+ const last = runtime.agent.messages.findLast((m) => m.role === "assistant" && typeof m.content === "string" && m.content.trim());
1015
+ if (!last || typeof last.content !== "string") {
1016
+ notice("Belum ada jawaban untuk disalin.");
1017
+ return;
1018
+ }
1019
+ process.stdout.write(osc52(last.content, !!process.env.TMUX));
1020
+ const file = join(runtime.home, "last-answer.md");
1021
+ mkdirSync(runtime.home, { recursive: true });
1022
+ writeFileSync(file, last.content);
1023
+ notice(`Disalin ke clipboard (bila terminal mendukung OSC 52) dan disimpan di ${file}.`);
1024
+ return;
1025
+ }
1026
+ case "export": {
1027
+ const name2 = args || `bccli-${(/* @__PURE__ */ new Date()).toISOString().slice(0, 16).replace(/[:T]/g, "-")}.md`;
1028
+ const file = resolve(runtime.cwd, name2);
1029
+ if (existsSync2(file)) {
1030
+ const answer2 = await ask(`${file} sudah ada. Timpa? (y/N)`);
1031
+ if (!answer2 || !/^y(a|es)?$/i.test(answer2.trim())) {
1032
+ notice("Ekspor dibatalkan.");
1033
+ return;
1034
+ }
1035
+ }
1036
+ try {
1037
+ writeFileSync(file, exportMarkdown(runtime.agent.messages, `Sesi BCCLI ${runtime.startedAt.toLocaleString("id-ID")}`));
1038
+ } catch (error) {
1039
+ notice(`Gagal menulis ${file}: ${error.message}`, "error");
1040
+ return;
1041
+ }
1042
+ notice(`Percakapan disimpan di ${file}.`);
1043
+ return;
1044
+ }
637
1045
  case "provider": {
638
1046
  const c = runtime.config;
639
1047
  setProviderMenu(
@@ -645,7 +1053,11 @@ function App({ runtime, initialPrompt, version }) {
645
1053
  notice(`Perintah tidak dikenal: /${name}. Ketik /help.`, "warn");
646
1054
  }
647
1055
  },
648
- [runtime, notice, exit]
1056
+ [runtime, notice, exit, version, runTurn, ask]
1057
+ );
1058
+ const runSlash = useCallback(
1059
+ (name, args) => runSlashCommand(name, args).catch((error) => notice(`/${name}: ${error.message}`, "error")),
1060
+ [runSlashCommand, notice]
649
1061
  );
650
1062
  const submit = useCallback(
651
1063
  (text) => {
@@ -662,7 +1074,7 @@ function App({ runtime, initialPrompt, version }) {
662
1074
  else if (skill) {
663
1075
  let body;
664
1076
  try {
665
- body = parseFrontmatter(readFileSync(skill.file, "utf8")).body;
1077
+ body = parseFrontmatter(readFileSync2(skill.file, "utf8")).body;
666
1078
  } catch (error) {
667
1079
  notice(`Skill ${skill.name} tidak bisa dibaca: ${error.code ?? error.message}`, "error");
668
1080
  return;
@@ -681,7 +1093,7 @@ ARGUMENTS: ${slash.args}`);
681
1093
  submit(initialPrompt);
682
1094
  }
683
1095
  }, [initialPrompt, submit]);
684
- useInput8((input, key) => {
1096
+ useInput9((input, key) => {
685
1097
  if (key.tab && key.shift) {
686
1098
  const next = nextMode(runtime.agent.permissions.mode);
687
1099
  runtime.agent.permissions.mode = next;
@@ -755,31 +1167,31 @@ ${t.output}`);
755
1167
  const renderEntry = (e) => {
756
1168
  switch (e.kind) {
757
1169
  case "header":
758
- return /* @__PURE__ */ jsx14(Box12, { marginBottom: 1, children: /* @__PURE__ */ jsxs9(Text14, { children: [
759
- /* @__PURE__ */ jsx14(Text14, { color: color(ACCENT), bold: true, children: " \u273B " }),
760
- /* @__PURE__ */ jsx14(Text14, { bold: true, children: `BCCLI ${version}` }),
761
- /* @__PURE__ */ jsx14(Text14, { dimColor: true, children: ` \xB7 ${runtime.modelRef} (${runtime.providerLabel()}) \xB7 ${cwd}` })
1170
+ return /* @__PURE__ */ jsx15(Box13, { marginBottom: 1, children: /* @__PURE__ */ jsxs10(Text15, { children: [
1171
+ /* @__PURE__ */ jsx15(Text15, { color: color(ACCENT), bold: true, children: " \u273B " }),
1172
+ /* @__PURE__ */ jsx15(Text15, { bold: true, children: `BCCLI ${version}` }),
1173
+ /* @__PURE__ */ jsx15(Text15, { dimColor: true, children: ` \xB7 ${runtime.modelRef} (${runtime.providerLabel()}) \xB7 ${cwd}` })
762
1174
  ] }) }, e.id);
763
1175
  case "user":
764
- return /* @__PURE__ */ jsx14(Box12, { marginTop: 1, children: /* @__PURE__ */ jsx14(Text14, { dimColor: true, children: `> ${e.text}` }) }, e.id);
1176
+ return /* @__PURE__ */ jsx15(Box13, { marginTop: 1, children: /* @__PURE__ */ jsx15(Text15, { dimColor: true, children: `> ${e.text}` }) }, e.id);
765
1177
  case "assistant":
766
1178
  if (!e.text.trim()) return null;
767
- return /* @__PURE__ */ jsxs9(Box12, { marginTop: 1, children: [
768
- /* @__PURE__ */ jsx14(Text14, { color: color(ACCENT), children: "\u25CF " }),
769
- /* @__PURE__ */ jsx14(Markdown, { text: e.text.replace(/^\s+/, "") })
1179
+ return /* @__PURE__ */ jsxs10(Box13, { marginTop: 1, children: [
1180
+ /* @__PURE__ */ jsx15(Text15, { color: color(ACCENT), children: "\u25CF " }),
1181
+ /* @__PURE__ */ jsx15(Markdown, { text: e.text.replace(/^\s+/, "") })
770
1182
  ] }, e.id);
771
1183
  case "tool":
772
- 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);
1184
+ 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);
773
1185
  case "notice":
774
- 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);
1186
+ 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);
775
1187
  }
776
1188
  };
777
- return /* @__PURE__ */ jsxs9(Box12, { flexDirection: "column", children: [
778
- /* @__PURE__ */ jsx14(Static, { items: transcript.done, children: renderEntry }),
1189
+ return /* @__PURE__ */ jsxs10(Box13, { flexDirection: "column", children: [
1190
+ /* @__PURE__ */ jsx15(Static, { items: transcript.done, children: renderEntry }),
779
1191
  transcript.live.map(renderEntry),
780
- busy && !pending && !planAsk ? /* @__PURE__ */ jsx14(Box12, { marginTop: 1, children: /* @__PURE__ */ jsx14(Spinner, { label: "Berpikir", startedAt }) }) : null,
781
- pending ? /* @__PURE__ */ jsx14(PermissionPrompt, { request: pending.request, onAnswer: answer }, pending.id) : null,
782
- picker ? /* @__PURE__ */ jsx14(
1192
+ busy && !pending && !planAsk ? /* @__PURE__ */ jsx15(Box13, { marginTop: 1, children: /* @__PURE__ */ jsx15(Spinner, { label: "Berpikir", startedAt }) }) : null,
1193
+ pending ? /* @__PURE__ */ jsx15(PermissionPrompt, { request: pending.request, onAnswer: answer }, pending.id) : null,
1194
+ picker ? /* @__PURE__ */ jsx15(
783
1195
  ModelPicker,
784
1196
  {
785
1197
  groups: picker,
@@ -790,9 +1202,10 @@ ${t.output}`);
790
1202
  }
791
1203
  }
792
1204
  ) : null,
793
- mcpMenu ? /* @__PURE__ */ jsx14(McpMenu, { items: mcpMenu, onPick: (n) => void pickMcp(n) }) : null,
794
- providerMenu ? /* @__PURE__ */ jsx14(ProviderMenu, { entries: providerMenu, onPick: (id) => void pickProvider(id) }) : null,
795
- prompt ? /* @__PURE__ */ jsx14(
1205
+ listPicker ? /* @__PURE__ */ jsx15(ListPicker, { title: listPicker.title, items: listPicker.items, onPick: listPicker.onPick }) : null,
1206
+ mcpMenu ? /* @__PURE__ */ jsx15(McpMenu, { items: mcpMenu, onPick: (n) => void pickMcp(n) }) : null,
1207
+ providerMenu ? /* @__PURE__ */ jsx15(ProviderMenu, { entries: providerMenu, onPick: (id) => void pickProvider(id) }) : null,
1208
+ prompt ? /* @__PURE__ */ jsx15(
796
1209
  LinePrompt,
797
1210
  {
798
1211
  label: prompt.label,
@@ -807,7 +1220,7 @@ ${t.output}`);
807
1220
  }
808
1221
  }
809
1222
  ) : null,
810
- planAsk ? /* @__PURE__ */ jsx14(
1223
+ planAsk ? /* @__PURE__ */ jsx15(
811
1224
  PlanApproval,
812
1225
  {
813
1226
  plan: planAsk.plan,
@@ -818,18 +1231,18 @@ ${t.output}`);
818
1231
  }
819
1232
  }
820
1233
  ) : null,
821
- /* @__PURE__ */ jsx14(TodoList, { items: todos }),
822
- /* @__PURE__ */ jsx14(PromptInput, { disabled: busy || !!pending || !!picker || !!providerMenu || !!prompt || !!planAsk || !!mcpMenu, history, cwd: runtime.cwd, onSubmit: submit, extraCommands }),
823
- /* @__PURE__ */ jsx14(StatusBar, { mode, tokens, busy, model: modelLabel })
1234
+ /* @__PURE__ */ jsx15(TodoList, { items: todos }),
1235
+ /* @__PURE__ */ jsx15(PromptInput, { disabled: busy || !!pending || !!picker || !!providerMenu || !!prompt || !!planAsk || !!mcpMenu || !!listPicker, history, cwd: runtime.cwd, onSubmit: submit, extraCommands }),
1236
+ /* @__PURE__ */ jsx15(StatusBar, { mode, tokens, busy, model: modelLabel })
824
1237
  ] });
825
1238
  }
826
1239
 
827
1240
  // src/ui/index.tsx
828
- import { jsx as jsx15 } from "react/jsx-runtime";
1241
+ import { jsx as jsx16 } from "react/jsx-runtime";
829
1242
  async function startInteractive(rt, opts) {
830
1243
  if (opts.resume) {
831
- const { Session } = await import("./session-7UB77CEG.js");
832
- const list = Session.list(rt.home, rt.cwd).slice(0, 20);
1244
+ const { Session: Session2 } = await import("./session-7UB77CEG.js");
1245
+ const list = Session2.list(rt.home, rt.cwd).slice(0, 20);
833
1246
  if (!list.length) console.log("Belum ada sesi di folder ini; memulai sesi baru.");
834
1247
  else {
835
1248
  list.forEach((s, i) => {
@@ -843,7 +1256,7 @@ async function startInteractive(rt, opts) {
843
1256
  if (chosen) rt.resume(chosen.session);
844
1257
  }
845
1258
  }
846
- const { serversToStart, setProjectTrust } = await import("./config-XPWPGNZJ.js");
1259
+ const { serversToStart, setProjectTrust } = await import("./config-NPSYFAFD.js");
847
1260
  const plan = serversToStart(rt.home, rt.cwd);
848
1261
  if (plan.needTrust.length) {
849
1262
  const { createInterface } = await import("readline/promises");
@@ -857,7 +1270,7 @@ async function startInteractive(rt, opts) {
857
1270
  rl.close();
858
1271
  }
859
1272
  void rt.startMcp(plan.start);
860
- const instance = render(/* @__PURE__ */ jsx15(App, { runtime: rt, initialPrompt: opts.initialPrompt, version: opts.version }), { exitOnCtrlC: true });
1273
+ const instance = render(/* @__PURE__ */ jsx16(App, { runtime: rt, initialPrompt: opts.initialPrompt, version: opts.version }), { exitOnCtrlC: true });
861
1274
  await instance.waitUntilExit();
862
1275
  await rt.mcp.stop();
863
1276
  return 0;