@gilangranggaid/kilua-agent 1.2.0 → 1.3.0

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.
Files changed (3) hide show
  1. package/bin/kilua.js +538 -93
  2. package/bin/kilua.mjs +538 -93
  3. package/package.json +1 -1
package/bin/kilua.mjs CHANGED
@@ -12,6 +12,7 @@ const SETTINGS_PATH = path.join(KILUA_DIR, "settings.json");
12
12
  const SKILLS_DIR = path.join(KILUA_DIR, "skills");
13
13
  const MAX_STEPS = 40; // ponytail: naik dari 16; agent butuh banyak langkah untuk self-fix loop
14
14
  const MAX_INJECTED_SKILLS = 5; // sama seperti web: cukup 5 skill per pesan biar konteks hemat
15
+ let PLAN_MODE = false; // read-only mode: semua tool yang mengubah state diblokir
15
16
 
16
17
  // ponytail: match pakai substring ternormalisasi ("docker-compose" == "docker compose").
17
18
  // Regex mentah dari web terlalu kaku: trigger "docker-compose" tak pernah kena "docker compose".
@@ -207,6 +208,10 @@ const TOOLS_ANTHROPIC = TOOLS_OPENAI.map(t => ({
207
208
 
208
209
  function executeLocalTool(name, args) {
209
210
  const cwd = process.cwd();
211
+ // ponytail: plan mode = read-only. Blokir semua efek samping, bukan hanya tulis file.
212
+ if (PLAN_MODE && ["write_file", "edit_file", "delete_file", "run_cmd"].includes(name)) {
213
+ return "Error: Plan mode aktif — aksi yang mengubah state diblokir. Susun rencana dulu; user mematikan /plan untuk eksekusi.";
214
+ }
210
215
  try {
211
216
  if (name === "read_file") {
212
217
  const full = path.resolve(cwd, args.path);
@@ -273,6 +278,359 @@ function promptQuestion(rl, query) {
273
278
  return new Promise(res => rl.question(query, res));
274
279
  }
275
280
 
281
+ // Prompt sekali pakai untuk slash command di luar composer.
282
+ function promptLine(query) {
283
+ return new Promise(res => {
284
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
285
+ rl.question(query, ans => { rl.close(); res(ans); });
286
+ });
287
+ }
288
+
289
+ // ===================== TEMA =====================
290
+ const THEMES = {
291
+ purple: { name: "purple", accent: "\x1b[35m", sel: "\x1b[36m", dim: "\x1b[90m", ok: "\x1b[32m", warn: "\x1b[33m", err: "\x1b[31m", reset: "\x1b[0m", bold: "\x1b[1m" },
292
+ blue: { name: "blue", accent: "\x1b[34m", sel: "\x1b[36m", dim: "\x1b[90m", ok: "\x1b[32m", warn: "\x1b[33m", err: "\x1b[31m", reset: "\x1b[0m", bold: "\x1b[1m" },
293
+ green: { name: "green", accent: "\x1b[32m", sel: "\x1b[36m", dim: "\x1b[90m", ok: "\x1b[32m", warn: "\x1b[33m", err: "\x1b[31m", reset: "\x1b[0m", bold: "\x1b[1m" },
294
+ mono: { name: "mono", accent: "\x1b[1m", sel: "\x1b[1m", dim: "\x1b[2m", ok: "", warn: "", err: "", reset: "\x1b[0m", bold: "" },
295
+ };
296
+ function themeOf(settings) {
297
+ return THEMES[settings?.theme] || THEMES.purple;
298
+ }
299
+
300
+ // ===================== SLASH MENU =====================
301
+ const SLASH_COMMANDS = [
302
+ { name: "/model", desc: "Ganti model AI aktif" },
303
+ { name: "/config", desc: "Ubah API key, provider, base URL" },
304
+ { name: "/theme", desc: "Ganti tema warna tampilan CLI" },
305
+ { name: "/skills", desc: "Lihat daftar skills terpasang" },
306
+ { name: "/mcp", desc: "Kelola server MCP" },
307
+ { name: "/memory", desc: "Lihat & tambah memori proyek (KILUA.md)" },
308
+ { name: "/plan", desc: "Nyalakan/matikan plan mode (read-only)" },
309
+ { name: "/clear", desc: "Bersihkan riwayat percakapan" },
310
+ { name: "/help", desc: "Panduan perintah" },
311
+ { name: "/exit", desc: "Keluar dari Kilua" },
312
+ ];
313
+ const MENU_VISIBLE = 8;
314
+
315
+ // ===================== CLIPBOARD (Windows) =====================
316
+ function readClipboard() {
317
+ if (process.platform !== "win32") return null;
318
+ // ponytail: gambar clipboard butuh WinForms STA; platform lain = teks saja.
319
+ try {
320
+ const tmp = path.join(os.tmpdir(), `kilua-paste-${Date.now()}.png`).replace(/'/g, "''");
321
+ const ps = "Add-Type -AssemblyName System.Windows.Forms,System.Drawing;"
322
+ + "$i=[System.Windows.Forms.Clipboard]::GetImage();"
323
+ + `if($i){$i.Save('${tmp}',[System.Drawing.Imaging.ImageFormat]::Png);`
324
+ + "@{type='image';path='" + tmp + "'}|ConvertTo-Json -Compress}"
325
+ + "else{@{type='text';text=[System.Windows.Forms.Clipboard]::GetText()}|ConvertTo-Json -Compress}";
326
+ const out = execSync(`powershell -NoProfile -STA -Command "${ps.replace(/"/g, '\\"')}"`, {
327
+ encoding: "utf8", timeout: 10000, windowsHide: true, stdio: ["ignore", "pipe", "ignore"],
328
+ }).trim();
329
+ const j = JSON.parse(out.slice(out.indexOf("{")));
330
+ if (j.type === "image" && fs.existsSync(j.path)) {
331
+ return { type: "image", data: fs.readFileSync(j.path).toString("base64"), mediaType: "image/png" };
332
+ }
333
+ if (j.type === "text" && j.text) return { type: "text", text: j.text };
334
+ } catch {}
335
+ return null;
336
+ }
337
+
338
+ // ===================== COMPOSER (raw-mode editor) =====================
339
+ const PASTE_AS_TOKEN_CHARS = 300;
340
+ const PASTE_AS_TOKEN_LINES = 3;
341
+
342
+ function promptComposer(settings, history) {
343
+ const t = themeOf(settings);
344
+ return new Promise(resolve => {
345
+ const stdin = process.stdin;
346
+ const stdout = process.stdout;
347
+ let items = []; // string (1 char) | {type:'paste'|'image', label, ...}
348
+ let pos = 0;
349
+ let menuSel = 0;
350
+ let histIdx = -1;
351
+ let lastLines = 0;
352
+ let pasteN = 0;
353
+ let imageN = 0;
354
+ let escBuf = "";
355
+ let inPaste = false;
356
+ let pasteBuf = "";
357
+ let probe = null;
358
+ let escTimer = null;
359
+ let closed = false;
360
+
361
+ const display = () => items.map(it => (typeof it === "string" ? it : it.label)).join("");
362
+ const displayText = () => items.map(it => (typeof it === "string" ? it : it.type === "paste" ? it.text : "")).join("");
363
+ const images = () => items.filter(it => it && it.type === "image");
364
+
365
+ function insertChars(str) {
366
+ for (const ch of str) { items.splice(pos, 0, ch); pos++; }
367
+ }
368
+ function insertPaste(text) {
369
+ const lines = text.split("\n").length;
370
+ if (text.length < PASTE_AS_TOKEN_CHARS && lines < PASTE_AS_TOKEN_LINES) return insertChars(text);
371
+ pasteN++;
372
+ items.splice(pos, 0, { type: "paste", label: `[Pasted text #${pasteN} +${lines} lines]`, text });
373
+ pos++;
374
+ }
375
+ function insertImage(cls) {
376
+ imageN++;
377
+ items.splice(pos, 0, { type: "image", label: `[Image #${imageN}]`, data: cls.data, mediaType: cls.mediaType });
378
+ pos++;
379
+ }
380
+
381
+ function menu() {
382
+ const d = display();
383
+ if (!d.startsWith("/") || /\s/.test(d)) return null;
384
+ const q = d.toLowerCase();
385
+ const list = SLASH_COMMANDS.filter(c => c.name.startsWith(q));
386
+ return list.length ? list : null;
387
+ }
388
+
389
+ function layout(plain, cols, cursorIdx) {
390
+ const lines = [""];
391
+ let caret = null;
392
+ for (let i = 0; i <= plain.length; i++) {
393
+ if (i === cursorIdx) caret = { line: lines.length - 1, col: lines[lines.length - 1].length };
394
+ if (i === plain.length) break;
395
+ const ch = plain[i];
396
+ if (ch === "\n") { lines.push(""); continue; }
397
+ if (lines[lines.length - 1].length >= cols) lines.push("");
398
+ lines[lines.length - 1] += ch;
399
+ }
400
+ if (!caret) caret = { line: lines.length - 1, col: lines[lines.length - 1].length };
401
+ if (lines[lines.length - 1].length >= cols) lines.push("");
402
+ return { lines, ...caret };
403
+ }
404
+
405
+ function render() {
406
+ const cols = Math.max(20, stdout.columns || 80);
407
+ const list = menu();
408
+ let menuLines = [];
409
+ if (list) {
410
+ const win = Math.min(MENU_VISIBLE, list.length);
411
+ menuSel = Math.max(0, Math.min(menuSel, list.length - 1));
412
+ let start = Math.max(0, Math.min(menuSel - win + 1, list.length - win));
413
+ const nameW = Math.max(...list.map(c => c.name.length)) + 2;
414
+ for (let i = start; i < start + win; i++) {
415
+ const c = list[i];
416
+ if (i === menuSel) menuLines.push(`${t.accent}❯${t.reset} ${t.sel}${c.name.padEnd(nameW)}${t.reset}${t.dim}${c.desc}${t.reset}`);
417
+ else menuLines.push(` ${t.dim}${c.name.padEnd(nameW)}${c.desc}${t.reset}`);
418
+ }
419
+ if (list.length > win) menuLines.push(`${t.dim} ↑↓ scroll · ${list.length - win} lainnya${t.reset}`);
420
+ }
421
+
422
+ let before = 0;
423
+ for (let i = 0; i < pos; i++) before += typeof items[i] === "string" ? 1 : items[i].label.length;
424
+ const plain = "❯ " + display();
425
+ const { lines, line: cLine, col: cCol } = layout(plain, cols, 2 + before);
426
+ const colored = lines.map((ln, idx) => {
427
+ const body = idx === 0 ? ln.slice(2) : ln;
428
+ const prefix = idx === 0 ? `${t.accent}❯ ${t.reset}` : "";
429
+ const painted = body
430
+ .replace(/\[Pasted text #\d+ \+\d+ lines\]/g, m => `\x1b[36m${m}${t.reset}`)
431
+ .replace(/\[Image #\d+\]/g, m => `\x1b[35m${m}${t.reset}`);
432
+ return prefix + painted;
433
+ });
434
+
435
+ if (lastLines) stdout.write(`\x1b[${lastLines - 1}A\r\x1b[0J`);
436
+ else stdout.write("\r\x1b[0J");
437
+ const block = [...menuLines, ...colored];
438
+ stdout.write(block.join("\n"));
439
+ lastLines = block.length;
440
+
441
+ const up = menuLines.length + colored.length - 1 - (menuLines.length + cLine);
442
+ if (up > 0) stdout.write(`\x1b[${up}A`);
443
+ stdout.write(`\r${cCol > 0 ? `\x1b[${cCol}C` : ""}`);
444
+ }
445
+
446
+ function restore() {
447
+ clearProbe();
448
+ clearEsc();
449
+ closed = true;
450
+ stdin.removeListener("data", onData);
451
+ process.removeAllListeners("SIGINT");
452
+ if (lastLines) stdout.write(`\x1b[${lastLines - 1}A\r\x1b[0J`);
453
+ lastLines = 0;
454
+ stdout.write("\x1b[?2004l");
455
+ try { stdin.setRawMode(false); } catch {}
456
+ stdin.pause();
457
+ }
458
+
459
+ function finish() {
460
+ const text = displayText().trim();
461
+ const imgs = images();
462
+ if (!text && !imgs.length) { render(); return; }
463
+ const summary = display();
464
+ restore();
465
+ let shown = summary.replace(/\n/g, " ⏎ ");
466
+ if (shown.length > 160) shown = shown.slice(0, 157) + "...";
467
+ stdout.write(`${t.accent}❯${t.reset} ${shown}\n`);
468
+ resolve({ text, images: imgs });
469
+ }
470
+
471
+ // ---- clipboard ----
472
+ function clearProbe() {
473
+ if (probe) { clearTimeout(probe); probe = null; }
474
+ }
475
+ // ESC sendirian = tutup menu; tapi ESC juga awal arrow key, jadi tunggu sebentar.
476
+ function armEsc() {
477
+ if (escTimer) return;
478
+ escTimer = setTimeout(() => {
479
+ escTimer = null;
480
+ escBuf = "";
481
+ menuSel = 0;
482
+ items = items.filter(it => typeof it !== "string" || it !== "\x1b");
483
+ render();
484
+ }, 30);
485
+ }
486
+ function clearEsc() {
487
+ if (escTimer) { clearTimeout(escTimer); escTimer = null; }
488
+ }
489
+ function tryClipboard() {
490
+ const clip = readClipboard();
491
+ if (!clip) return false;
492
+ if (clip.type === "image") insertImage(clip);
493
+ else insertPaste(clip.text);
494
+ return true;
495
+ }
496
+ // Ctrl+V: tunggu data bracket-paste dulu; kalau kosong, berarti gambar.
497
+ function ctrlV() {
498
+ if (probe) return;
499
+ probe = setTimeout(() => { probe = null; tryClipboard(); render(); }, 150);
500
+ }
501
+
502
+ // ---- key handling ----
503
+ function moveTo(newPos) { pos = Math.max(0, Math.min(newPos, items.length)); render(); }
504
+
505
+ function submitOrComplete() {
506
+ const list = menu();
507
+ if (list) {
508
+ const pick = list[Math.max(0, Math.min(menuSel, list.length - 1))];
509
+ items = []; pos = 0;
510
+ insertChars(pick.name + " ");
511
+ render();
512
+ return;
513
+ }
514
+ finish();
515
+ }
516
+
517
+ function onPlain(ch) {
518
+ if (ch === "\r" || ch === "\n") return submitOrComplete();
519
+ if (ch === "\x7f") { // backspace
520
+ if (pos > 0) { items.splice(pos - 1, 1); pos--; }
521
+ return render();
522
+ }
523
+ if (ch === "\x03" || ch === "\x04") {
524
+ if (!display()) {
525
+ restore();
526
+ stdout.write("\n");
527
+ process.exit(0);
528
+ }
529
+ items = []; pos = 0; menuSel = 0;
530
+ return render();
531
+ }
532
+ if (ch === "\x16") return ctrlV(); // ctrl+v
533
+ if (ch === "\x1bv" || ch === "\x1bV") { // alt+v
534
+ if (!tryClipboard()) stdout.write("\x07");
535
+ return render();
536
+ }
537
+ if (ch === "\t") {
538
+ const list = menu();
539
+ if (list) { items = []; pos = 0; insertChars(list[menuSel].name + " "); }
540
+ return render();
541
+ }
542
+ if (ch === "\x1b") return render(); // esc: tutup menu via reset
543
+ if (ch < " " && ch !== "\n") return;
544
+ insertChars(ch);
545
+ menuSel = 0;
546
+ render();
547
+ }
548
+
549
+ function onKey(seq) {
550
+ switch (seq) {
551
+ case "\x1b[A": if (menu()) { menuSel--; } else { navHistory(-1); } return render();
552
+ case "\x1b[B": if (menu()) { menuSel++; } else { navHistory(1); } return render();
553
+ case "\x1b[C": return moveTo(pos + 1);
554
+ case "\x1b[D": return moveTo(pos - 1);
555
+ case "\x1b[H": return moveTo(0);
556
+ case "\x1b[F": return moveTo(items.length);
557
+ case "\x1b[3~": if (pos < items.length) { items.splice(pos, 1); } return render();
558
+ case "\x1b\r": case "\x1b\n": items.splice(pos, 0, "\n"); pos++; return render();
559
+ default: return render();
560
+ }
561
+ }
562
+
563
+ function navHistory(dir) {
564
+ if (!history.length) return;
565
+ histIdx = Math.max(-1, Math.min(history.length - 1, histIdx - dir));
566
+ items = []; pos = 0;
567
+ if (histIdx >= 0) insertChars(history[history.length - 1 - histIdx]);
568
+ else pos = 0;
569
+ }
570
+
571
+ function feed(str) {
572
+ escBuf += str;
573
+ while (escBuf.length) {
574
+ if (inPaste) {
575
+ const end = escBuf.indexOf("\x1b[201~");
576
+ if (end === -1) { pasteBuf += escBuf; escBuf = ""; return; }
577
+ pasteBuf += escBuf.slice(0, end);
578
+ escBuf = escBuf.slice(end + 6);
579
+ inPaste = false;
580
+ insertPaste(pasteBuf.replace(/\r\n?/g, "\n"));
581
+ pasteBuf = "";
582
+ menuSel = 0;
583
+ render();
584
+ continue;
585
+ }
586
+ const pStart = escBuf.indexOf("\x1b[200~");
587
+ if (pStart > 0) {
588
+ const head = escBuf.slice(0, pStart);
589
+ escBuf = escBuf.slice(pStart);
590
+ for (const ch of head) onPlain(ch);
591
+ continue;
592
+ }
593
+ if (pStart === 0) { escBuf = escBuf.slice(6); inPaste = true; continue; }
594
+
595
+ if (escBuf.startsWith("\x1b")) {
596
+ const seqs = ["\x1b[A", "\x1b[B", "\x1b[C", "\x1b[D", "\x1b[H", "\x1b[F", "\x1b[3~", "\x1b[Z", "\x1b\r", "\x1b\n", "\x1bv", "\x1bV"];
597
+ const hit = seqs.find(s => escBuf.startsWith(s));
598
+ if (hit) { clearEsc(); escBuf = escBuf.slice(hit.length); onKey(hit); continue; }
599
+ // sequence CSI lengkap tapi tidak dikenal (mis. Ctrl+↑ = "\x1b[1;5A") — buang utuh, jangan ketik sebagai teks
600
+ const m = escBuf.match(/^\x1b\[[0-9;]*[A-Za-z~]/);
601
+ if (m) { clearEsc(); escBuf = escBuf.slice(m[0].length); render(); continue; }
602
+ if (escBuf.length < 8) { armEsc(); return; } // tunggu sisa byte sequence
603
+ clearEsc();
604
+ escBuf = escBuf.slice(1);
605
+ render();
606
+ continue;
607
+ }
608
+ const ch = escBuf[0];
609
+ escBuf = escBuf.slice(1);
610
+ onPlain(ch);
611
+ }
612
+ }
613
+
614
+ function onData(chunk) {
615
+ if (closed) return;
616
+ clearProbe();
617
+ feed(chunk.toString("utf8"));
618
+ }
619
+
620
+ // ---- start ----
621
+ const onSigint = () => {
622
+ if (!closed) { restore(); stdout.write("\n"); process.exit(0); }
623
+ };
624
+ stdout.write("\x1b[?2004h"); // bracketed paste
625
+ try { stdin.setRawMode(true); } catch {} // pipa/non-TTY: lanjut saja, input tetap terbaca
626
+ stdin.resume();
627
+ stdin.setEncoding("utf8");
628
+ stdin.on("data", onData);
629
+ process.on("SIGINT", onSigint);
630
+ render();
631
+ });
632
+ }
633
+
276
634
  // ponytail: status spinner minimal tanpa dependency — tampilkan langkah + detik berjalan
277
635
  let spinnerTimer = null;
278
636
  let spinnerStart = 0;
@@ -420,12 +778,33 @@ async function readSseStream(res, onChunk) {
420
778
  }
421
779
  }
422
780
 
423
- function buildSystemPrompt(convo) {
781
+ function readMemory() {
782
+ try {
783
+ const f = path.join(process.cwd(), "KILUA.md");
784
+ return fs.existsSync(f) ? fs.readFileSync(f, "utf8").slice(0, 4000) : "";
785
+ } catch { return ""; }
786
+ }
787
+
788
+ function buildSystemPrompt(convo, settings = {}) {
424
789
  let lastUser = "";
425
790
  for (let i = (convo?.length ?? 0) - 1; i >= 0; i--) {
426
- if (convo[i].role === "user" && typeof convo[i].content === "string") { lastUser = convo[i].content; break; }
791
+ const c = convo[i]?.content;
792
+ if (convo[i].role === "user" && typeof c === "string") { lastUser = c; break; }
793
+ if (convo[i].role === "user" && Array.isArray(c)) {
794
+ const t = c.find(b => b.type === "text");
795
+ if (t?.text) { lastUser = t.text; break; }
796
+ }
427
797
  }
428
798
  const skillsInfo = getSkillsPrompt(lastUser);
799
+ const mem = readMemory().trim();
800
+ const memBlock = mem ? `\n\n=== MEMORI PROYEK (KILUA.md) ===\n${mem}` : "";
801
+ const mcpList = Array.isArray(settings.mcp) ? settings.mcp : [];
802
+ const mcpBlock = mcpList.length
803
+ ? `\n\nMCP servers terdaftar (info saja, eksekusi tool MCP belum aktif di CLI):\n${mcpList.map(m => `- ${m.name}: ${m.command || ""} ${(m.args || []).join(" ")}`).join("\n")}`
804
+ : "";
805
+ const planBlock = PLAN_MODE
806
+ ? "\n\nPLAN MODE AKTIF: hanya baca & analisa. JANGAN mengubah file atau menjalankan perintah yang mengubah state. Susun rencana langkah demi langkah, lalu minta user mematikan plan mode (/plan) untuk eksekusi."
807
+ : "";
429
808
  return `Kamu KILUA, AI Coding Agent otonom di terminal lokal. Folder aktif: ${process.cwd()}.
430
809
  KEMAMPUAN TOOLS:
431
810
  - read_file, write_file, edit_file, list_dir
@@ -441,7 +820,7 @@ CARA KERJA (WAJIB DIIKUTI):
441
820
  6. Baru setelah terbukti jalan tanpa error, laporkan hasil + cara menjalankannya ke pengguna.
442
821
  7. Jika pengguna menyuruh install skill di chat (misal: "install skill vercel-labs/skills@find-skills"), jalankan run_cmd:
443
822
  npx skills add <nama-skill> --cwd "${SKILLS_DIR}"
444
- lalu beri tahu skill sudah siap dipakai.${skillsInfo}
823
+ lalu beri tahu skill sudah siap dipakai.${skillsInfo}${memBlock}${mcpBlock}${planBlock}
445
824
 
446
825
  Gaya: ringkas, langsung bertindak, jangan minta konfirmasi untuk langkah yang sudah jelas.`;
447
826
  }
@@ -530,7 +909,7 @@ async function fetchWithRetry(url, init) {
530
909
 
531
910
  // Loop untuk Anthropic / 9router local (/v1/messages)
532
911
  async function runAnthropicLoop(settings, convo) {
533
- const SYSTEM = buildSystemPrompt(convo);
912
+ const SYSTEM = buildSystemPrompt(convo, settings);
534
913
 
535
914
  let endpoint = settings.base_url.replace(/\/+$/, "");
536
915
  if (!endpoint.endsWith("/messages")) endpoint += "/messages";
@@ -646,7 +1025,7 @@ async function runAnthropicLoop(settings, convo) {
646
1025
 
647
1026
  // Loop untuk OpenAI / TokenHarbor / Gemini OpenAI protocol
648
1027
  async function runOpenAiLoop(settings, convo) {
649
- const SYSTEM = buildSystemPrompt(convo);
1028
+ const SYSTEM = buildSystemPrompt(convo, settings);
650
1029
 
651
1030
  let endpoint = settings.base_url.replace(/\/+$/, "");
652
1031
  if (endpoint.includes("generativelanguage.googleapis.com") && !endpoint.includes("/openai")) {
@@ -742,89 +1121,147 @@ async function runOpenAiLoop(settings, convo) {
742
1121
  }
743
1122
  }
744
1123
 
745
- async function runAgent(settings, userPrompt, convo) {
1124
+ async function runAgent(settings, userPrompt, convo, images = []) {
746
1125
  const isAnthropic = settings.provider === "anthropic" || settings.base_url.includes(":20128") || settings.model === "harobor";
747
- if (isAnthropic) {
748
- convo.push({ role: "user", content: userPrompt });
749
- await runAnthropicLoop(settings, convo);
1126
+ if (images.length) {
1127
+ if (isAnthropic) {
1128
+ const blocks = images.map(im => ({ type: "image", source: { type: "base64", media_type: im.mediaType, data: im.data } }));
1129
+ convo.push({ role: "user", content: [...blocks, { type: "text", text: userPrompt || "Jelaskan gambar ini." }] });
1130
+ } else {
1131
+ const blocks = images.map(im => ({ type: "image_url", image_url: { url: `data:${im.mediaType};base64,${im.data}` } }));
1132
+ convo.push({ role: "user", content: [...blocks, { type: "text", text: userPrompt || "Jelaskan gambar ini." }] });
1133
+ }
750
1134
  } else {
751
1135
  convo.push({ role: "user", content: userPrompt });
752
- await runOpenAiLoop(settings, convo);
753
1136
  }
1137
+ if (isAnthropic) await runAnthropicLoop(settings, convo);
1138
+ else await runOpenAiLoop(settings, convo);
754
1139
  }
755
1140
 
756
- function printHelp() {
757
- console.log(`
758
- \x1b[1m\x1b[35m=== PERINTAH SLASH KILUA (/) ===\x1b[0m
759
- \x1b[36m/help\x1b[0m Lihat panduan perintah ini
760
- \x1b[36m/models\x1b[0m Lihat dan ganti model AI saat ini
761
- \x1b[36m/skills\x1b[0m Lihat daftar skills yang terpasang di Kilua
762
- \x1b[36m/clear\x1b[0m Bersihkan riwayat percakapan sesi ini
763
- \x1b[36m/config\x1b[0m Ubah API Key, Provider, atau Base URL
764
- \x1b[36m/exit\x1b[0m Keluar dari Kilua Agent
765
-
766
- \x1b[90mTips: Untuk menginstall skill baru, cukup suruh Kilua di chat (misal: "tolong install skill find-skills")!\x1b[0m
767
- `);
1141
+ function printHelp(settings) {
1142
+ const t = themeOf(settings);
1143
+ console.log(`\n${t.bold}${t.accent}=== PERINTAH SLASH KILUA (/) ===${t.reset}`);
1144
+ for (const c of SLASH_COMMANDS) console.log(` ${t.sel}${c.name.padEnd(10)}${t.reset} ${t.dim}${c.desc}${t.reset}`);
1145
+ console.log(`\n${t.dim}Tips: ketik "/" lalu ↑↓ untuk pilih perintah. Install skill baru cukup minta di chat.${t.reset}`);
1146
+ console.log(`${t.dim}Ctrl+V / Alt+V = paste teks (ringkas otomatis) atau gambar dari clipboard.${t.reset}\n`);
768
1147
  }
769
1148
 
770
- async function handleSlashCommand(cmd, settings, convo, rl) {
771
- const parts = cmd.trim().split(/\s+/);
772
- const action = parts[0].toLowerCase();
773
-
774
- if (action === "/help") {
775
- printHelp();
1149
+ async function cmdModel(settings) {
1150
+ const t = themeOf(settings);
1151
+ if (!settings.base_url || !settings.api_key) {
1152
+ console.log(`\n${t.warn}[!] Belum terhubung ke config provider. Jalankan /config dulu.${t.reset}\n`);
776
1153
  return settings;
777
1154
  }
778
-
779
- if (action === "/models" || action === "/model") {
780
- if (!settings.base_url || !settings.api_key) {
781
- console.log(`\n\x1b[33m[!] Belum terhubung ke config provider. Jalankan /config terlebih dahulu.\x1b[0m\n`);
782
- return settings;
783
- }
784
- console.log(`\n\x1b[33mModel aktif saat ini:\x1b[0m \x1b[32m${settings.model}\x1b[0m (Provider: ${settings.base_url})`);
785
- const newModel = (await promptQuestion(rl, `Ganti nama model (Enter untuk tetap '${settings.model}'): `)).trim();
786
- if (newModel) {
787
- settings.model = newModel;
788
- saveSettings(settings);
789
- console.log(`\x1b[32m✓ Model diubah menjadi ${newModel}\x1b[0m\n`);
790
- }
791
- return settings;
1155
+ console.log(`\n${t.warn}Model aktif:${t.reset} ${t.ok}${settings.model}${t.reset} ${t.dim}(${settings.base_url})${t.reset}`);
1156
+ const newModel = (await promptLine(`Ganti nama model (Enter untuk tetap '${settings.model}'): `)).trim();
1157
+ if (newModel && newModel !== settings.model) {
1158
+ settings.model = newModel;
1159
+ saveSettings(settings);
1160
+ console.log(`${t.ok}✓ Model diubah menjadi ${newModel}${t.reset}\n`);
792
1161
  }
1162
+ return settings;
1163
+ }
793
1164
 
794
- if (action === "/skills" || action === "/skill") {
795
- const list = listInstalledSkills();
796
- const builtin = loadBuiltinSkills();
797
- console.log(`\n\x1b[35m=== SKILLS TERPASANG (${SKILLS_DIR}) ===\x1b[0m`);
798
- if (!list.length) {
799
- console.log(`Belum ada skill tambahan terpasang.`);
800
- console.log(`Suruh Kilua di chat untuk install, misal: \x1b[36m"Kilua, tolong install skill vercel-labs/skills@find-skills"\x1b[0m`);
801
- } else {
802
- for (const s of list) {
803
- console.log(`- \x1b[32m${s.name}\x1b[0m: ${s.desc}`);
804
- }
805
- }
806
- console.log(`\x1b[90m+ ${builtin.length} skill bawaan aktif otomatis (disuntik maks ${MAX_INJECTED_SKILLS}/pesan sesuai tugas)\x1b[0m`);
807
- console.log();
808
- return settings;
809
- }
1165
+ function cmdSkills(settings) {
1166
+ const t = themeOf(settings);
1167
+ const list = listInstalledSkills();
1168
+ const builtin = loadBuiltinSkills();
1169
+ console.log(`\n${t.bold}${t.accent}=== SKILLS TERPASANG (${SKILLS_DIR}) ===${t.reset}`);
1170
+ if (!list.length) {
1171
+ console.log(`Belum ada skill tambahan terpasang.`);
1172
+ console.log(`Suruh Kilua di chat, misal: ${t.sel}"Kilua, install skill vercel-labs/skills@find-skills"${t.reset}`);
1173
+ } else for (const s of list) console.log(`- ${t.ok}${s.name}${t.reset}: ${s.desc}`);
1174
+ console.log(`${t.dim}+ ${builtin.length} skill bawaan aktif otomatis (maks ${MAX_INJECTED_SKILLS}/pesan)${t.reset}\n`);
1175
+ return settings;
1176
+ }
810
1177
 
811
- if (action === "/clear") {
812
- convo.length = 0;
813
- console.clear();
814
- console.log(`\x1b[32m✓ Riwayat percakapan dibersihkan.\x1b[0m\n`);
815
- return settings;
1178
+ async function cmdTheme(settings) {
1179
+ const t = themeOf(settings);
1180
+ const names = Object.keys(THEMES);
1181
+ console.log(`\n${t.bold}${t.accent}=== TEMA ===${t.reset}`);
1182
+ names.forEach((n, i) => {
1183
+ const mark = n === (settings.theme || "purple") ? `${t.ok}●${t.reset}` : " ";
1184
+ console.log(` ${mark} ${i + 1}) ${THEMES[n].accent}${n}${t.reset}`);
1185
+ });
1186
+ const pick = (await promptLine(`\nPilih tema [1-${names.length}] (Enter = batal): `)).trim();
1187
+ const idx = Number(pick) - 1;
1188
+ if (names[idx]) {
1189
+ settings.theme = names[idx];
1190
+ saveSettings(settings);
1191
+ console.log(`${themeOf(settings).ok}✓ Tema: ${settings.theme}${t.reset}\n`);
816
1192
  }
1193
+ return settings;
1194
+ }
817
1195
 
818
- if (action === "/config") {
819
- rl.close();
820
- const newSettings = await runInteractiveConfig();
821
- return newSettings;
1196
+ async function cmdMcp(settings) {
1197
+ const t = themeOf(settings);
1198
+ if (!Array.isArray(settings.mcp)) settings.mcp = [];
1199
+ while (true) {
1200
+ console.log(`\n${t.bold}${t.accent}=== MCP SERVERS ===${t.reset}`);
1201
+ if (!settings.mcp.length) console.log(`${t.dim}Belum ada server MCP.${t.reset}`);
1202
+ settings.mcp.forEach((m, i) => console.log(` ${i + 1}) ${t.ok}${m.name}${t.reset} ${t.dim}${m.command || ""} ${(m.args || []).join(" ")}${t.reset}`));
1203
+ const pick = (await promptLine(`\n[a] tambah · [d] hapus · [Enter] selesai: `)).trim().toLowerCase();
1204
+ if (!pick) break;
1205
+ if (pick === "a") {
1206
+ const name = (await promptLine("Nama server: ")).trim();
1207
+ const command = (await promptLine("Command (mis. npx): ")).trim();
1208
+ const argsRaw = (await promptLine("Args (pisah spasi, opsional): ")).trim();
1209
+ if (!name || !command) { console.log(`${t.warn}[!] Nama & command wajib.${t.reset}`); continue; }
1210
+ settings.mcp.push({ name, command, args: argsRaw ? argsRaw.split(/\s+/) : [] });
1211
+ saveSettings(settings);
1212
+ console.log(`${t.ok}✓ ${name} ditambahkan.${t.reset}`);
1213
+ } else if (pick === "d") {
1214
+ const n = Number((await promptLine("Nomor yang dihapus: ")).trim()) - 1;
1215
+ if (settings.mcp[n]) { const [x] = settings.mcp.splice(n, 1); saveSettings(settings); console.log(`${t.ok}✓ ${x.name} dihapus.${t.reset}`); }
1216
+ else console.log(`${t.warn}[!] Nomor tidak valid.${t.reset}`);
1217
+ } else break;
822
1218
  }
1219
+ return settings;
1220
+ }
1221
+
1222
+ async function cmdMemory(settings) {
1223
+ const t = themeOf(settings);
1224
+ const file = path.join(process.cwd(), "KILUA.md");
1225
+ const body = readMemory();
1226
+ console.log(`\n${t.bold}${t.accent}=== MEMORI PROYEK ===${t.reset} ${t.dim}${file}${t.reset}`);
1227
+ console.log(body.trim() ? body.trim() : `${t.dim}(kosong — memori ini selalu disuntik ke system prompt)${t.reset}`);
1228
+ const add = (await promptLine(`\nTambah catatan memori (Enter = batal): `)).trim();
1229
+ if (add) {
1230
+ fs.appendFileSync(file, (fs.existsSync(file) ? "\n" : "# Memori Proyek (KILUA.md)\n") + "- " + add + "\n", "utf8");
1231
+ console.log(`${t.ok}✓ Disimpan ke KILUA.md${t.reset}\n`);
1232
+ } else console.log();
1233
+ return settings;
1234
+ }
823
1235
 
824
- console.log(`Perintah tidak dikenal: ${action}. Ketik \x1b[36m/help\x1b[0m untuk daftar perintah.`);
1236
+ function cmdPlan(settings) {
1237
+ const t = themeOf(settings);
1238
+ PLAN_MODE = !PLAN_MODE;
1239
+ console.log(`\n${PLAN_MODE ? t.warn + "⏸ Plan mode ON" : t.ok + "▶ Plan mode OFF"}${t.reset} ${t.dim}— ${PLAN_MODE ? "hanya baca & menyusun rencana, aksi tulis diblokir" : "agent bebas mengeksekusi"}.${t.reset}\n`);
825
1240
  return settings;
826
1241
  }
827
1242
 
1243
+ async function handleSlashCommand(cmd, settings, convo) {
1244
+ const action = cmd.trim().split(/\s+/)[0].toLowerCase();
1245
+ switch (action) {
1246
+ case "/help": return printHelp(settings), settings;
1247
+ case "/model": case "/models": return await cmdModel(settings);
1248
+ case "/skills": case "/skill": return cmdSkills(settings);
1249
+ case "/theme": return await cmdTheme(settings);
1250
+ case "/mcp": return await cmdMcp(settings);
1251
+ case "/memory": return await cmdMemory(settings);
1252
+ case "/plan": return cmdPlan(settings);
1253
+ case "/clear":
1254
+ convo.length = 0;
1255
+ console.clear();
1256
+ console.log(`${themeOf(settings).ok}✓ Riwayat percakapan dibersihkan.${themeOf(settings).reset}\n`);
1257
+ return settings;
1258
+ case "/config": return await runInteractiveConfig();
1259
+ default:
1260
+ console.log(`Perintah tidak dikenal: ${action}. Ketik ${themeOf(settings).sel}/help${themeOf(settings).reset}.`);
1261
+ return settings;
1262
+ }
1263
+ }
1264
+
828
1265
  async function main() {
829
1266
  const args = process.argv.slice(2);
830
1267
  const oneShot = args.join(" ").trim();
@@ -835,11 +1272,23 @@ async function main() {
835
1272
  }
836
1273
 
837
1274
  let settings = loadSettings();
1275
+ if (args[0] === "--version" || args[0] === "-v") {
1276
+ const here = path.dirname(fileURLToPath(import.meta.url));
1277
+ const candidates = [path.join(here, "..", "packages", "kilua-agent", "package.json"), path.join(here, "..", "package.json")];
1278
+ let v = "1.3.0";
1279
+ for (const p of candidates) {
1280
+ try {
1281
+ const j = JSON.parse(fs.readFileSync(p, "utf8"));
1282
+ if (j.name === "kilua-ai" && j.private) continue; // root app, bukan paket CLI
1283
+ if (j.version) { v = j.version; break; }
1284
+ } catch {}
1285
+ }
1286
+ console.log(`kilua-agent ${v}`);
1287
+ process.exit(0);
1288
+ }
838
1289
 
839
- // Validasi ketat: jika belum ada base_url atau api_key, WAJIB setup dulu
840
1290
  const isLocalProxy = settings.base_url?.includes("127.0.0.1") || settings.base_url?.includes("localhost");
841
1291
  const hasValidConfig = settings.base_url && (settings.api_key || isLocalProxy);
842
-
843
1292
  if (!hasValidConfig) {
844
1293
  console.log("\x1b[33m[!] Belum ada konfigurasi AI key / provider.\x1b[0m");
845
1294
  settings = await runInteractiveConfig();
@@ -851,29 +1300,25 @@ async function main() {
851
1300
  process.exit(0);
852
1301
  }
853
1302
 
854
- console.log("\x1b[1m\x1b[35m=== KILUA AI CLI AGENT ===\x1b[0m");
855
- console.log(`CWD : \x1b[33m${process.cwd()}\x1b[0m`);
856
- console.log(`Provider: \x1b[32m${settings.base_url}\x1b[0m (${settings.model})`);
857
- console.log(`Config : \x1b[90m${SETTINGS_PATH}\x1b[0m (ketik \x1b[36m/help\x1b[0m untuk panduan)\n`);
858
-
859
- let rl = readline.createInterface({ input: process.stdin, output: process.stdout });
860
- const ask = () => {
861
- rl.question("\x1b[1mKAMU > \x1b[0m", async input => {
862
- const q = input.trim();
863
- if (!q) return ask();
864
- if (q === "exit" || q === "quit" || q === "/exit") { rl.close(); process.exit(0); }
865
- if (q.startsWith("/")) {
866
- settings = await handleSlashCommand(q, settings, convo, rl);
867
- if (q === "/config") {
868
- rl = readline.createInterface({ input: process.stdin, output: process.stdout });
869
- }
870
- return ask();
871
- }
872
- await runAgent(settings, q, convo);
873
- ask();
874
- });
875
- };
876
- ask();
1303
+ const t = themeOf(settings);
1304
+ console.log(`${t.bold}${t.accent}=== KILUA AI CLI AGENT ===${t.reset}`);
1305
+ console.log(`${t.dim}CWD :${t.reset} ${t.warn}${process.cwd()}${t.reset}`);
1306
+ console.log(`${t.dim}Provider:${t.reset} ${t.ok}${settings.base_url}${t.reset} (${settings.model})`);
1307
+ console.log(`${t.dim}Bantuan :${t.reset} ${t.sel}/${t.reset} perintah · ${t.sel}Ctrl+V${t.reset} paste teks/gambar\n`);
1308
+
1309
+ const history = [];
1310
+ while (true) {
1311
+ const { text, images } = await promptComposer(settings, history);
1312
+ if (!text && !images.length) continue;
1313
+ if (text === "exit" || text === "quit" || text === "/exit") break;
1314
+ if (images.length && !text) { await runAgent(settings, "", convo, images); continue; }
1315
+ if (text.startsWith("/")) { settings = await handleSlashCommand(text, settings, convo); continue; }
1316
+ history.push(text);
1317
+ if (history.length > 100) history.shift();
1318
+ await runAgent(settings, text, convo, images);
1319
+ }
1320
+ console.log(`${t.dim}Sampai jumpa.${t.reset}`);
1321
+ process.exit(0);
877
1322
  }
878
1323
 
879
1324
  main();