@gilangranggaid/kilua-agent 1.2.0 → 1.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/kilua.js +734 -96
- package/bin/kilua.mjs +734 -96
- package/package.json +1 -1
package/bin/kilua.mjs
CHANGED
|
@@ -3,7 +3,7 @@ import readline from "node:readline";
|
|
|
3
3
|
import fs from "node:fs";
|
|
4
4
|
import path from "node:path";
|
|
5
5
|
import os from "node:os";
|
|
6
|
-
import { execSync } from "node:child_process";
|
|
6
|
+
import { execSync, spawn } from "node:child_process";
|
|
7
7
|
import { fileURLToPath } from "node:url";
|
|
8
8
|
|
|
9
9
|
// ponytail: autonomous single-file AI Coding Agent CLI with slash commands, local skills & test execution.
|
|
@@ -12,6 +12,120 @@ 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
|
|
16
|
+
|
|
17
|
+
// ===================== MCP CLIENT =====================
|
|
18
|
+
const MCP_CLIENTS = new Map(); // name -> { proc, tools, requestId }
|
|
19
|
+
|
|
20
|
+
async function startMcpServer(name, command, args = []) {
|
|
21
|
+
if (MCP_CLIENTS.has(name)) return MCP_CLIENTS.get(name);
|
|
22
|
+
|
|
23
|
+
const proc = spawn(command, args, {
|
|
24
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
25
|
+
env: { ...process.env },
|
|
26
|
+
windowsHide: true,
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
let buffer = "";
|
|
30
|
+
let requestId = 0;
|
|
31
|
+
const pending = new Map();
|
|
32
|
+
|
|
33
|
+
proc.stdout.on("data", (data) => {
|
|
34
|
+
buffer += data.toString();
|
|
35
|
+
const lines = buffer.split("\n");
|
|
36
|
+
buffer = lines.pop() || "";
|
|
37
|
+
for (const line of lines) {
|
|
38
|
+
if (!line.trim()) continue;
|
|
39
|
+
try {
|
|
40
|
+
const msg = JSON.parse(line);
|
|
41
|
+
if (msg.id && pending.has(msg.id)) {
|
|
42
|
+
const { resolve, reject } = pending.get(msg.id);
|
|
43
|
+
pending.delete(msg.id);
|
|
44
|
+
if (msg.error) reject(new Error(msg.error.message || JSON.stringify(msg.error)));
|
|
45
|
+
else resolve(msg.result);
|
|
46
|
+
}
|
|
47
|
+
} catch {}
|
|
48
|
+
}
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
proc.stderr.on("data", (data) => {
|
|
52
|
+
console.error(`\x1b[90m[MCP:${name}] ${data.toString().trim()}\x1b[0m`);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
proc.on("exit", (code) => {
|
|
56
|
+
console.error(`\x1b[31m[MCP:${name}] exited with code ${code}\x1b[0m`);
|
|
57
|
+
MCP_CLIENTS.delete(name);
|
|
58
|
+
for (const [, { reject }] of pending) reject(new Error("MCP server exited"));
|
|
59
|
+
pending.clear();
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
// Initialize
|
|
63
|
+
const initResult = await sendMcpRequest(proc, pending, ++requestId, "initialize", {
|
|
64
|
+
protocolVersion: "2024-11-05",
|
|
65
|
+
capabilities: { tools: {} },
|
|
66
|
+
clientInfo: { name: "kilua", version: "1.3.0" },
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
// List tools
|
|
70
|
+
const toolsResult = await sendMcpRequest(proc, pending, ++requestId, "tools/list", {});
|
|
71
|
+
const tools = (toolsResult?.tools || []).map(t => ({
|
|
72
|
+
name: `mcp_${name}_${t.name}`,
|
|
73
|
+
description: `[MCP:${name}] ${t.description}`,
|
|
74
|
+
input_schema: t.inputSchema,
|
|
75
|
+
}));
|
|
76
|
+
|
|
77
|
+
const client = { proc, tools, requestId, pending };
|
|
78
|
+
MCP_CLIENTS.set(name, client);
|
|
79
|
+
return client;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function sendMcpRequest(proc, pending, id, method, params) {
|
|
83
|
+
return new Promise((resolve, reject) => {
|
|
84
|
+
pending.set(id, { resolve, reject });
|
|
85
|
+
proc.stdin.write(JSON.stringify({ jsonrpc: "2.0", id, method, params }) + "\n");
|
|
86
|
+
// Timeout
|
|
87
|
+
setTimeout(() => {
|
|
88
|
+
if (pending.has(id)) {
|
|
89
|
+
pending.delete(id);
|
|
90
|
+
reject(new Error(`MCP request timeout: ${method}`));
|
|
91
|
+
}
|
|
92
|
+
}, 30000);
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
async function callMcpTool(name, toolName, args) {
|
|
97
|
+
const client = MCP_CLIENTS.get(name);
|
|
98
|
+
if (!client) throw new Error(`MCP server ${name} not started`);
|
|
99
|
+
|
|
100
|
+
const mcpToolName = toolName.replace(`mcp_${name}_`, "");
|
|
101
|
+
const result = await sendMcpRequest(client.proc, client.pending, ++client.requestId, "tools/call", {
|
|
102
|
+
name: mcpToolName,
|
|
103
|
+
arguments: args,
|
|
104
|
+
});
|
|
105
|
+
return result?.content?.[0]?.text || JSON.stringify(result);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
async function initializeMcpServers(settings) {
|
|
109
|
+
const mcpList = Array.isArray(settings.mcp) ? settings.mcp : [];
|
|
110
|
+
const allTools = [];
|
|
111
|
+
for (const mcp of mcpList) {
|
|
112
|
+
try {
|
|
113
|
+
const client = await startMcpServer(mcp.name, mcp.command, mcp.args || []);
|
|
114
|
+
allTools.push(...client.tools);
|
|
115
|
+
console.log(`\x1b[32m[✓] MCP ${mcp.name}: ${client.tools.length} tools loaded\x1b[0m`);
|
|
116
|
+
} catch (e) {
|
|
117
|
+
console.error(`\x1b[31m[✖] MCP ${mcp.name} failed: ${e.message}\x1b[0m`);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
return allTools;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function shutdownMcpServers() {
|
|
124
|
+
for (const [name, client] of MCP_CLIENTS) {
|
|
125
|
+
try { client.proc.kill(); } catch {}
|
|
126
|
+
}
|
|
127
|
+
MCP_CLIENTS.clear();
|
|
128
|
+
}
|
|
15
129
|
|
|
16
130
|
// ponytail: match pakai substring ternormalisasi ("docker-compose" == "docker compose").
|
|
17
131
|
// Regex mentah dari web terlalu kaku: trigger "docker-compose" tak pernah kena "docker compose".
|
|
@@ -207,6 +321,10 @@ const TOOLS_ANTHROPIC = TOOLS_OPENAI.map(t => ({
|
|
|
207
321
|
|
|
208
322
|
function executeLocalTool(name, args) {
|
|
209
323
|
const cwd = process.cwd();
|
|
324
|
+
// ponytail: plan mode = read-only. Blokir semua efek samping, bukan hanya tulis file.
|
|
325
|
+
if (PLAN_MODE && ["write_file", "edit_file", "delete_file", "run_cmd"].includes(name)) {
|
|
326
|
+
return "Error: Plan mode aktif — aksi yang mengubah state diblokir. Susun rencana dulu; user mematikan /plan untuk eksekusi.";
|
|
327
|
+
}
|
|
210
328
|
try {
|
|
211
329
|
if (name === "read_file") {
|
|
212
330
|
const full = path.resolve(cwd, args.path);
|
|
@@ -256,11 +374,15 @@ function executeLocalTool(name, args) {
|
|
|
256
374
|
? `grep -rn --include='${glob}' '${pattern.replace(/'/g, "'\\''")}' '${target}' | head -200`
|
|
257
375
|
: `grep -rn --exclude-dir=node_modules --exclude-dir=.git '${pattern.replace(/'/g, "'\\''")}' '${target}' | head -200`;
|
|
258
376
|
}
|
|
259
|
-
const out = execSync(cmd, { cwd, encoding: "utf8", timeout: 60000, stdio: ["pipe", "pipe", "pipe"] });
|
|
377
|
+
const out = execSync(cmd, { cwd, encoding: "utf8", timeout: 60000, windowsHide: true, stdio: ["pipe", "pipe", "pipe"] });
|
|
260
378
|
return out.trim() || "(tidak ada hasil)";
|
|
261
379
|
}
|
|
262
380
|
if (name === "run_cmd") {
|
|
263
|
-
const out = execSync(args.command, { cwd, encoding: "utf8", timeout: 180000, stdio: ["pipe", "pipe", "pipe"], maxBuffer: 10 * 1024 * 1024 });
|
|
381
|
+
const out = execSync(args.command, { cwd, encoding: "utf8", timeout: 180000, windowsHide: true, stdio: ["pipe", "pipe", "pipe"], maxBuffer: 10 * 1024 * 1024 });
|
|
382
|
+
// Jika command menjalankan npx skills add atau sejenisnya ke SKILLS_DIR, pastikan folder ~/.kilua/skills ada
|
|
383
|
+
if (args.command.includes("skills add") || args.command.includes("skill")) {
|
|
384
|
+
ensureKiluaDir();
|
|
385
|
+
}
|
|
264
386
|
return out.trim() || "(Done with exit code 0)";
|
|
265
387
|
}
|
|
266
388
|
} catch (err) {
|
|
@@ -273,6 +395,421 @@ function promptQuestion(rl, query) {
|
|
|
273
395
|
return new Promise(res => rl.question(query, res));
|
|
274
396
|
}
|
|
275
397
|
|
|
398
|
+
// Prompt sekali pakai untuk slash command di luar composer.
|
|
399
|
+
function promptLine(query) {
|
|
400
|
+
return new Promise(res => {
|
|
401
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
402
|
+
rl.question(query, ans => { rl.close(); res(ans); });
|
|
403
|
+
});
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
// ===================== TEMA =====================
|
|
407
|
+
const THEMES = {
|
|
408
|
+
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" },
|
|
409
|
+
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" },
|
|
410
|
+
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" },
|
|
411
|
+
mono: { name: "mono", accent: "\x1b[1m", sel: "\x1b[1m", dim: "\x1b[2m", ok: "", warn: "", err: "", reset: "\x1b[0m", bold: "" },
|
|
412
|
+
};
|
|
413
|
+
function themeOf(settings) {
|
|
414
|
+
return THEMES[settings?.theme] || THEMES.purple;
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
// ===================== SLASH MENU =====================
|
|
418
|
+
const SLASH_COMMANDS = [
|
|
419
|
+
{ name: "/model", desc: "Ganti model AI aktif" },
|
|
420
|
+
{ name: "/config", desc: "Ubah API key, provider, base URL" },
|
|
421
|
+
{ name: "/theme", desc: "Ganti tema warna tampilan CLI" },
|
|
422
|
+
{ name: "/skills", desc: "Lihat daftar skills terpasang" },
|
|
423
|
+
{ name: "/mcp", desc: "Kelola server MCP" },
|
|
424
|
+
{ name: "/memory", desc: "Lihat & tambah memori proyek (KILUA.md)" },
|
|
425
|
+
{ name: "/plan", desc: "Nyalakan/matikan plan mode (read-only)" },
|
|
426
|
+
{ name: "/clear", desc: "Bersihkan riwayat percakapan" },
|
|
427
|
+
{ name: "/help", desc: "Panduan perintah" },
|
|
428
|
+
{ name: "/exit", desc: "Keluar dari Kilua" },
|
|
429
|
+
];
|
|
430
|
+
const MENU_VISIBLE = 8;
|
|
431
|
+
|
|
432
|
+
// ===================== FILE SELECTOR =====================
|
|
433
|
+
const FILE_SELECTOR_VISIBLE = 10;
|
|
434
|
+
let fileSelectorCache = null;
|
|
435
|
+
let fileSelectorCacheTime = 0;
|
|
436
|
+
const FILE_SELECTOR_CACHE_TTL = 5000; // 5 detik cache
|
|
437
|
+
|
|
438
|
+
async function getFileList(cwd) {
|
|
439
|
+
const now = Date.now();
|
|
440
|
+
if (fileSelectorCache && now - fileSelectorCacheTime < FILE_SELECTOR_CACHE_TTL) {
|
|
441
|
+
return fileSelectorCache;
|
|
442
|
+
}
|
|
443
|
+
const files = [];
|
|
444
|
+
function walk(dir, prefix = "") {
|
|
445
|
+
try {
|
|
446
|
+
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
447
|
+
for (const entry of entries) {
|
|
448
|
+
if (entry.name.startsWith(".") || entry.name === "node_modules") continue;
|
|
449
|
+
const full = path.join(dir, entry.name);
|
|
450
|
+
const rel = path.relative(cwd, full);
|
|
451
|
+
if (entry.isDirectory()) {
|
|
452
|
+
walk(full, rel + path.sep);
|
|
453
|
+
} else {
|
|
454
|
+
files.push(rel);
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
} catch {}
|
|
458
|
+
}
|
|
459
|
+
walk(cwd);
|
|
460
|
+
fileSelectorCache = files.sort();
|
|
461
|
+
fileSelectorCacheTime = now;
|
|
462
|
+
return fileSelectorCache;
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
function fileSelector() {
|
|
466
|
+
const d = display();
|
|
467
|
+
const atIdx = d.lastIndexOf("@");
|
|
468
|
+
if (atIdx === -1) return null;
|
|
469
|
+
// hanya trigger kalau @ di akhir atau diikuti spasi/baru
|
|
470
|
+
const afterAt = d.slice(atIdx + 1);
|
|
471
|
+
if (afterAt.includes(" ") || afterAt.includes("\n")) return null;
|
|
472
|
+
const query = afterAt.toLowerCase();
|
|
473
|
+
return query;
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
// ===================== CLIPBOARD (Windows) =====================
|
|
477
|
+
function readClipboard() {
|
|
478
|
+
if (process.platform !== "win32") return null;
|
|
479
|
+
// ponytail: gambar clipboard butuh WinForms STA; platform lain = teks saja.
|
|
480
|
+
try {
|
|
481
|
+
const tmp = path.join(os.tmpdir(), `kilua-paste-${Date.now()}.png`).replace(/'/g, "''");
|
|
482
|
+
const ps = "Add-Type -AssemblyName System.Windows.Forms,System.Drawing;"
|
|
483
|
+
+ "$i=[System.Windows.Forms.Clipboard]::GetImage();"
|
|
484
|
+
+ `if($i){$i.Save('${tmp}',[System.Drawing.Imaging.ImageFormat]::Png);`
|
|
485
|
+
+ "@{type='image';path='" + tmp + "'}|ConvertTo-Json -Compress}"
|
|
486
|
+
+ "else{@{type='text';text=[System.Windows.Forms.Clipboard]::GetText()}|ConvertTo-Json -Compress}";
|
|
487
|
+
const out = execSync(`powershell -NoProfile -STA -Command "${ps.replace(/"/g, '\\"')}"`, {
|
|
488
|
+
encoding: "utf8", timeout: 10000, windowsHide: true, stdio: ["ignore", "pipe", "ignore"],
|
|
489
|
+
}).trim();
|
|
490
|
+
const j = JSON.parse(out.slice(out.indexOf("{")));
|
|
491
|
+
if (j.type === "image" && fs.existsSync(j.path)) {
|
|
492
|
+
return { type: "image", data: fs.readFileSync(j.path).toString("base64"), mediaType: "image/png" };
|
|
493
|
+
}
|
|
494
|
+
if (j.type === "text" && j.text) return { type: "text", text: j.text };
|
|
495
|
+
} catch {}
|
|
496
|
+
return null;
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
// ===================== COMPOSER (raw-mode editor) =====================
|
|
500
|
+
const PASTE_AS_TOKEN_CHARS = 300;
|
|
501
|
+
const PASTE_AS_TOKEN_LINES = 3;
|
|
502
|
+
|
|
503
|
+
function promptComposer(settings, history) {
|
|
504
|
+
const t = themeOf(settings);
|
|
505
|
+
return new Promise(resolve => {
|
|
506
|
+
const stdin = process.stdin;
|
|
507
|
+
const stdout = process.stdout;
|
|
508
|
+
let items = []; // string (1 char) | {type:'paste'|'image', label, ...}
|
|
509
|
+
let pos = 0;
|
|
510
|
+
let menuSel = 0;
|
|
511
|
+
let histIdx = -1;
|
|
512
|
+
let lastLines = 0;
|
|
513
|
+
let pasteN = 0;
|
|
514
|
+
let imageN = 0;
|
|
515
|
+
let escBuf = "";
|
|
516
|
+
let inPaste = false;
|
|
517
|
+
let pasteBuf = "";
|
|
518
|
+
let probe = null;
|
|
519
|
+
let escTimer = null;
|
|
520
|
+
let closed = false;
|
|
521
|
+
|
|
522
|
+
const display = () => items.map(it => (typeof it === "string" ? it : it.label)).join("");
|
|
523
|
+
const displayText = () => items.map(it => (typeof it === "string" ? it : it.type === "paste" ? it.text : "")).join("");
|
|
524
|
+
const images = () => items.filter(it => it && it.type === "image");
|
|
525
|
+
|
|
526
|
+
function insertChars(str) {
|
|
527
|
+
for (const ch of str) { items.splice(pos, 0, ch); pos++; }
|
|
528
|
+
}
|
|
529
|
+
function insertPaste(text) {
|
|
530
|
+
const lines = text.split("\n").length;
|
|
531
|
+
if (text.length < PASTE_AS_TOKEN_CHARS && lines < PASTE_AS_TOKEN_LINES) return insertChars(text);
|
|
532
|
+
pasteN++;
|
|
533
|
+
items.splice(pos, 0, { type: "paste", label: `[Pasted text #${pasteN} +${lines} lines]`, text });
|
|
534
|
+
pos++;
|
|
535
|
+
}
|
|
536
|
+
function insertImage(cls) {
|
|
537
|
+
imageN++;
|
|
538
|
+
items.splice(pos, 0, { type: "image", label: `[Image #${imageN}]`, data: cls.data, mediaType: cls.mediaType });
|
|
539
|
+
pos++;
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
function menu() {
|
|
543
|
+
const d = display();
|
|
544
|
+
if (!d.startsWith("/") || /\s/.test(d)) return null;
|
|
545
|
+
const q = d.toLowerCase();
|
|
546
|
+
const list = SLASH_COMMANDS.filter(c => c.name.startsWith(q));
|
|
547
|
+
return list.length ? list : null;
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
function layout(plain, cols, cursorIdx) {
|
|
551
|
+
const lines = [""];
|
|
552
|
+
let caret = null;
|
|
553
|
+
for (let i = 0; i <= plain.length; i++) {
|
|
554
|
+
if (i === cursorIdx) caret = { line: lines.length - 1, col: lines[lines.length - 1].length };
|
|
555
|
+
if (i === plain.length) break;
|
|
556
|
+
const ch = plain[i];
|
|
557
|
+
if (ch === "\n") { lines.push(""); continue; }
|
|
558
|
+
if (lines[lines.length - 1].length >= cols) lines.push("");
|
|
559
|
+
lines[lines.length - 1] += ch;
|
|
560
|
+
}
|
|
561
|
+
if (!caret) caret = { line: lines.length - 1, col: lines[lines.length - 1].length };
|
|
562
|
+
if (lines[lines.length - 1].length >= cols) lines.push("");
|
|
563
|
+
return { lines, ...caret };
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
function render() {
|
|
567
|
+
const cols = Math.max(20, stdout.columns || 80);
|
|
568
|
+
const list = menu();
|
|
569
|
+
let menuLines = [];
|
|
570
|
+
if (list) {
|
|
571
|
+
const win = Math.min(MENU_VISIBLE, list.length);
|
|
572
|
+
menuSel = Math.max(0, Math.min(menuSel, list.length - 1));
|
|
573
|
+
let start = Math.max(0, Math.min(menuSel - win + 1, list.length - win));
|
|
574
|
+
const nameW = Math.max(...list.map(c => c.name.length)) + 2;
|
|
575
|
+
for (let i = start; i < start + win; i++) {
|
|
576
|
+
const c = list[i];
|
|
577
|
+
if (i === menuSel) menuLines.push(`${t.accent}❯${t.reset} ${t.sel}${c.name.padEnd(nameW)}${t.reset}${t.dim}${c.desc}${t.reset}`);
|
|
578
|
+
else menuLines.push(` ${t.dim}${c.name.padEnd(nameW)}${c.desc}${t.reset}`);
|
|
579
|
+
}
|
|
580
|
+
if (list.length > win) menuLines.push(`${t.dim} ↑↓ scroll · ${list.length - win} lainnya${t.reset}`);
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
let before = 0;
|
|
584
|
+
for (let i = 0; i < pos; i++) before += typeof items[i] === "string" ? 1 : items[i].label.length;
|
|
585
|
+
const plain = "❯ " + display();
|
|
586
|
+
const { lines, line: cLine, col: cCol } = layout(plain, cols, 2 + before);
|
|
587
|
+
const colored = lines.map((ln, idx) => {
|
|
588
|
+
const body = idx === 0 ? ln.slice(2) : ln;
|
|
589
|
+
const prefix = idx === 0 ? `${t.accent}❯ ${t.reset}` : "";
|
|
590
|
+
const painted = body
|
|
591
|
+
.replace(/\[Pasted text #\d+ \+\d+ lines\]/g, m => `\x1b[36m${m}${t.reset}`)
|
|
592
|
+
.replace(/\[Image #\d+\]/g, m => `\x1b[35m${m}${t.reset}`);
|
|
593
|
+
return prefix + painted;
|
|
594
|
+
});
|
|
595
|
+
|
|
596
|
+
if (lastLines) {
|
|
597
|
+
// Clear only the lines we previously drew (menu + input), not the chat history above
|
|
598
|
+
stdout.write(`\x1b[${lastLines}A`); // move to first line of our block
|
|
599
|
+
for (let i = 0; i < lastLines; i++) {
|
|
600
|
+
stdout.write(`\r\x1b[2K`); // clear entire line
|
|
601
|
+
if (i < lastLines - 1) stdout.write(`\x1b[1B`); // move down
|
|
602
|
+
}
|
|
603
|
+
stdout.write(`\x1b[${lastLines}A`); // back to first line
|
|
604
|
+
} else {
|
|
605
|
+
stdout.write("\r\x1b[2K"); // first render: just clear current line
|
|
606
|
+
}
|
|
607
|
+
const block = [...menuLines, ...colored];
|
|
608
|
+
stdout.write(block.join("\n"));
|
|
609
|
+
lastLines = block.length;
|
|
610
|
+
|
|
611
|
+
const up = menuLines.length + colored.length - 1 - (menuLines.length + cLine);
|
|
612
|
+
if (up > 0) stdout.write(`\x1b[${up}A`);
|
|
613
|
+
stdout.write(`\r${cCol > 0 ? `\x1b[${cCol}C` : ""}`);
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
function restore() {
|
|
617
|
+
clearProbe();
|
|
618
|
+
clearEsc();
|
|
619
|
+
closed = true;
|
|
620
|
+
stdin.removeListener("data", onData);
|
|
621
|
+
process.removeAllListeners("SIGINT");
|
|
622
|
+
if (lastLines) {
|
|
623
|
+
stdout.write(`\x1b[${lastLines}A`);
|
|
624
|
+
for (let i = 0; i < lastLines; i++) {
|
|
625
|
+
stdout.write(`\r\x1b[2K`);
|
|
626
|
+
if (i < lastLines - 1) stdout.write(`\x1b[1B`);
|
|
627
|
+
}
|
|
628
|
+
stdout.write(`\x1b[${lastLines}A`);
|
|
629
|
+
}
|
|
630
|
+
lastLines = 0;
|
|
631
|
+
stdout.write("\x1b[?2004l");
|
|
632
|
+
try { stdin.setRawMode(false); } catch {}
|
|
633
|
+
stdin.pause();
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
function finish() {
|
|
637
|
+
const text = displayText().trim();
|
|
638
|
+
const imgs = images();
|
|
639
|
+
if (!text && !imgs.length) { render(); return; }
|
|
640
|
+
const summary = display();
|
|
641
|
+
restore();
|
|
642
|
+
let shown = summary.replace(/\n/g, " ⏎ ");
|
|
643
|
+
if (shown.length > 160) shown = shown.slice(0, 157) + "...";
|
|
644
|
+
// Print separator line before user message
|
|
645
|
+
stdout.write(`${t.dim}────────────────────────────────────────${t.reset}\n`);
|
|
646
|
+
stdout.write(`${t.accent}❯${t.reset} ${shown}\n`);
|
|
647
|
+
resolve({ text, images: imgs });
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
// ---- clipboard ----
|
|
651
|
+
function clearProbe() {
|
|
652
|
+
if (probe) { clearTimeout(probe); probe = null; }
|
|
653
|
+
}
|
|
654
|
+
// ESC sendirian = tutup menu; tapi ESC juga awal arrow key, jadi tunggu sebentar.
|
|
655
|
+
function armEsc() {
|
|
656
|
+
if (escTimer) return;
|
|
657
|
+
escTimer = setTimeout(() => {
|
|
658
|
+
escTimer = null;
|
|
659
|
+
escBuf = "";
|
|
660
|
+
menuSel = 0;
|
|
661
|
+
items = items.filter(it => typeof it !== "string" || it !== "\x1b");
|
|
662
|
+
render();
|
|
663
|
+
}, 30);
|
|
664
|
+
}
|
|
665
|
+
function clearEsc() {
|
|
666
|
+
if (escTimer) { clearTimeout(escTimer); escTimer = null; }
|
|
667
|
+
}
|
|
668
|
+
function tryClipboard() {
|
|
669
|
+
const clip = readClipboard();
|
|
670
|
+
if (!clip) return false;
|
|
671
|
+
if (clip.type === "image") insertImage(clip);
|
|
672
|
+
else insertPaste(clip.text);
|
|
673
|
+
return true;
|
|
674
|
+
}
|
|
675
|
+
// Ctrl+V: tunggu data bracket-paste dulu; kalau kosong, berarti gambar.
|
|
676
|
+
function ctrlV() {
|
|
677
|
+
if (probe) return;
|
|
678
|
+
probe = setTimeout(() => { probe = null; tryClipboard(); render(); }, 150);
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
// ---- key handling ----
|
|
682
|
+
function moveTo(newPos) { pos = Math.max(0, Math.min(newPos, items.length)); render(); }
|
|
683
|
+
|
|
684
|
+
function submitOrComplete() {
|
|
685
|
+
const list = menu();
|
|
686
|
+
if (list) {
|
|
687
|
+
const pick = list[Math.max(0, Math.min(menuSel, list.length - 1))];
|
|
688
|
+
items = []; pos = 0;
|
|
689
|
+
insertChars(pick.name + " ");
|
|
690
|
+
render();
|
|
691
|
+
return;
|
|
692
|
+
}
|
|
693
|
+
finish();
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
function onPlain(ch) {
|
|
697
|
+
if (ch === "\r" || ch === "\n") return submitOrComplete();
|
|
698
|
+
if (ch === "\x7f") { // backspace
|
|
699
|
+
if (pos > 0) { items.splice(pos - 1, 1); pos--; }
|
|
700
|
+
return render();
|
|
701
|
+
}
|
|
702
|
+
if (ch === "\x03" || ch === "\x04") {
|
|
703
|
+
if (!display()) {
|
|
704
|
+
restore();
|
|
705
|
+
stdout.write("\n");
|
|
706
|
+
process.exit(0);
|
|
707
|
+
}
|
|
708
|
+
items = []; pos = 0; menuSel = 0;
|
|
709
|
+
return render();
|
|
710
|
+
}
|
|
711
|
+
if (ch === "\x16") return ctrlV(); // ctrl+v
|
|
712
|
+
if (ch === "\x1bv" || ch === "\x1bV") { // alt+v
|
|
713
|
+
if (!tryClipboard()) stdout.write("\x07");
|
|
714
|
+
return render();
|
|
715
|
+
}
|
|
716
|
+
if (ch === "\t") {
|
|
717
|
+
const list = menu();
|
|
718
|
+
if (list) { items = []; pos = 0; insertChars(list[menuSel].name + " "); }
|
|
719
|
+
return render();
|
|
720
|
+
}
|
|
721
|
+
if (ch === "\x1b") return render(); // esc: tutup menu via reset
|
|
722
|
+
if (ch < " " && ch !== "\n") return;
|
|
723
|
+
insertChars(ch);
|
|
724
|
+
menuSel = 0;
|
|
725
|
+
render();
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
function onKey(seq) {
|
|
729
|
+
switch (seq) {
|
|
730
|
+
case "\x1b[A": if (menu()) { menuSel--; } else { navHistory(-1); } return render();
|
|
731
|
+
case "\x1b[B": if (menu()) { menuSel++; } else { navHistory(1); } return render();
|
|
732
|
+
case "\x1b[C": return moveTo(pos + 1);
|
|
733
|
+
case "\x1b[D": return moveTo(pos - 1);
|
|
734
|
+
case "\x1b[H": return moveTo(0);
|
|
735
|
+
case "\x1b[F": return moveTo(items.length);
|
|
736
|
+
case "\x1b[3~": if (pos < items.length) { items.splice(pos, 1); } return render();
|
|
737
|
+
case "\x1b\r": case "\x1b\n": items.splice(pos, 0, "\n"); pos++; return render();
|
|
738
|
+
default: return render();
|
|
739
|
+
}
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
function navHistory(dir) {
|
|
743
|
+
if (!history.length) return;
|
|
744
|
+
histIdx = Math.max(-1, Math.min(history.length - 1, histIdx - dir));
|
|
745
|
+
items = []; pos = 0;
|
|
746
|
+
if (histIdx >= 0) insertChars(history[history.length - 1 - histIdx]);
|
|
747
|
+
else pos = 0;
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
function feed(str) {
|
|
751
|
+
escBuf += str;
|
|
752
|
+
while (escBuf.length) {
|
|
753
|
+
if (inPaste) {
|
|
754
|
+
const end = escBuf.indexOf("\x1b[201~");
|
|
755
|
+
if (end === -1) { pasteBuf += escBuf; escBuf = ""; return; }
|
|
756
|
+
pasteBuf += escBuf.slice(0, end);
|
|
757
|
+
escBuf = escBuf.slice(end + 6);
|
|
758
|
+
inPaste = false;
|
|
759
|
+
insertPaste(pasteBuf.replace(/\r\n?/g, "\n"));
|
|
760
|
+
pasteBuf = "";
|
|
761
|
+
menuSel = 0;
|
|
762
|
+
render();
|
|
763
|
+
continue;
|
|
764
|
+
}
|
|
765
|
+
const pStart = escBuf.indexOf("\x1b[200~");
|
|
766
|
+
if (pStart > 0) {
|
|
767
|
+
const head = escBuf.slice(0, pStart);
|
|
768
|
+
escBuf = escBuf.slice(pStart);
|
|
769
|
+
for (const ch of head) onPlain(ch);
|
|
770
|
+
continue;
|
|
771
|
+
}
|
|
772
|
+
if (pStart === 0) { escBuf = escBuf.slice(6); inPaste = true; continue; }
|
|
773
|
+
|
|
774
|
+
if (escBuf.startsWith("\x1b")) {
|
|
775
|
+
const seqs = ["\x1b[A", "\x1b[B", "\x1b[C", "\x1b[D", "\x1b[H", "\x1b[F", "\x1b[3~", "\x1b[Z", "\x1b\r", "\x1b\n", "\x1bv", "\x1bV"];
|
|
776
|
+
const hit = seqs.find(s => escBuf.startsWith(s));
|
|
777
|
+
if (hit) { clearEsc(); escBuf = escBuf.slice(hit.length); onKey(hit); continue; }
|
|
778
|
+
// sequence CSI lengkap tapi tidak dikenal (mis. Ctrl+↑ = "\x1b[1;5A") — buang utuh, jangan ketik sebagai teks
|
|
779
|
+
const m = escBuf.match(/^\x1b\[[0-9;]*[A-Za-z~]/);
|
|
780
|
+
if (m) { clearEsc(); escBuf = escBuf.slice(m[0].length); render(); continue; }
|
|
781
|
+
if (escBuf.length < 8) { armEsc(); return; } // tunggu sisa byte sequence
|
|
782
|
+
clearEsc();
|
|
783
|
+
escBuf = escBuf.slice(1);
|
|
784
|
+
render();
|
|
785
|
+
continue;
|
|
786
|
+
}
|
|
787
|
+
const ch = escBuf[0];
|
|
788
|
+
escBuf = escBuf.slice(1);
|
|
789
|
+
onPlain(ch);
|
|
790
|
+
}
|
|
791
|
+
}
|
|
792
|
+
|
|
793
|
+
function onData(chunk) {
|
|
794
|
+
if (closed) return;
|
|
795
|
+
clearProbe();
|
|
796
|
+
feed(chunk.toString("utf8"));
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
// ---- start ----
|
|
800
|
+
const onSigint = () => {
|
|
801
|
+
if (!closed) { restore(); stdout.write("\n"); process.exit(0); }
|
|
802
|
+
};
|
|
803
|
+
stdout.write("\x1b[?2004h"); // bracketed paste
|
|
804
|
+
try { stdin.setRawMode(true); } catch {} // pipa/non-TTY: lanjut saja, input tetap terbaca
|
|
805
|
+
stdin.resume();
|
|
806
|
+
stdin.setEncoding("utf8");
|
|
807
|
+
stdin.on("data", onData);
|
|
808
|
+
process.on("SIGINT", onSigint);
|
|
809
|
+
render();
|
|
810
|
+
});
|
|
811
|
+
}
|
|
812
|
+
|
|
276
813
|
// ponytail: status spinner minimal tanpa dependency — tampilkan langkah + detik berjalan
|
|
277
814
|
let spinnerTimer = null;
|
|
278
815
|
let spinnerStart = 0;
|
|
@@ -420,12 +957,33 @@ async function readSseStream(res, onChunk) {
|
|
|
420
957
|
}
|
|
421
958
|
}
|
|
422
959
|
|
|
423
|
-
function
|
|
960
|
+
function readMemory() {
|
|
961
|
+
try {
|
|
962
|
+
const f = path.join(process.cwd(), "KILUA.md");
|
|
963
|
+
return fs.existsSync(f) ? fs.readFileSync(f, "utf8").slice(0, 4000) : "";
|
|
964
|
+
} catch { return ""; }
|
|
965
|
+
}
|
|
966
|
+
|
|
967
|
+
function buildSystemPrompt(convo, settings = {}) {
|
|
424
968
|
let lastUser = "";
|
|
425
969
|
for (let i = (convo?.length ?? 0) - 1; i >= 0; i--) {
|
|
426
|
-
|
|
970
|
+
const c = convo[i]?.content;
|
|
971
|
+
if (convo[i].role === "user" && typeof c === "string") { lastUser = c; break; }
|
|
972
|
+
if (convo[i].role === "user" && Array.isArray(c)) {
|
|
973
|
+
const t = c.find(b => b.type === "text");
|
|
974
|
+
if (t?.text) { lastUser = t.text; break; }
|
|
975
|
+
}
|
|
427
976
|
}
|
|
428
977
|
const skillsInfo = getSkillsPrompt(lastUser);
|
|
978
|
+
const mem = readMemory().trim();
|
|
979
|
+
const memBlock = mem ? `\n\n=== MEMORI PROYEK (KILUA.md) ===\n${mem}` : "";
|
|
980
|
+
const mcpList = Array.isArray(settings.mcp) ? settings.mcp : [];
|
|
981
|
+
const mcpBlock = mcpList.length
|
|
982
|
+
? `\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")}`
|
|
983
|
+
: "";
|
|
984
|
+
const planBlock = PLAN_MODE
|
|
985
|
+
? "\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."
|
|
986
|
+
: "";
|
|
429
987
|
return `Kamu KILUA, AI Coding Agent otonom di terminal lokal. Folder aktif: ${process.cwd()}.
|
|
430
988
|
KEMAMPUAN TOOLS:
|
|
431
989
|
- read_file, write_file, edit_file, list_dir
|
|
@@ -441,7 +999,7 @@ CARA KERJA (WAJIB DIIKUTI):
|
|
|
441
999
|
6. Baru setelah terbukti jalan tanpa error, laporkan hasil + cara menjalankannya ke pengguna.
|
|
442
1000
|
7. Jika pengguna menyuruh install skill di chat (misal: "install skill vercel-labs/skills@find-skills"), jalankan run_cmd:
|
|
443
1001
|
npx skills add <nama-skill> --cwd "${SKILLS_DIR}"
|
|
444
|
-
lalu beri tahu skill sudah siap dipakai.${skillsInfo}
|
|
1002
|
+
lalu beri tahu skill sudah siap dipakai.${skillsInfo}${memBlock}${mcpBlock}${planBlock}
|
|
445
1003
|
|
|
446
1004
|
Gaya: ringkas, langsung bertindak, jangan minta konfirmasi untuk langkah yang sudah jelas.`;
|
|
447
1005
|
}
|
|
@@ -530,7 +1088,7 @@ async function fetchWithRetry(url, init) {
|
|
|
530
1088
|
|
|
531
1089
|
// Loop untuk Anthropic / 9router local (/v1/messages)
|
|
532
1090
|
async function runAnthropicLoop(settings, convo) {
|
|
533
|
-
const SYSTEM = buildSystemPrompt(convo);
|
|
1091
|
+
const SYSTEM = buildSystemPrompt(convo, settings);
|
|
534
1092
|
|
|
535
1093
|
let endpoint = settings.base_url.replace(/\/+$/, "");
|
|
536
1094
|
if (!endpoint.endsWith("/messages")) endpoint += "/messages";
|
|
@@ -586,6 +1144,8 @@ async function runAnthropicLoop(settings, convo) {
|
|
|
586
1144
|
if (deltaText) {
|
|
587
1145
|
if (!printedHeader) {
|
|
588
1146
|
stopStatus();
|
|
1147
|
+
// Print separator line before AI response
|
|
1148
|
+
process.stdout.write(`\n${t.dim}────────────────────────────────────────${t.reset}\n`);
|
|
589
1149
|
process.stdout.write("\r\x1b[2KKILUA > ");
|
|
590
1150
|
printedHeader = true;
|
|
591
1151
|
}
|
|
@@ -646,7 +1206,7 @@ async function runAnthropicLoop(settings, convo) {
|
|
|
646
1206
|
|
|
647
1207
|
// Loop untuk OpenAI / TokenHarbor / Gemini OpenAI protocol
|
|
648
1208
|
async function runOpenAiLoop(settings, convo) {
|
|
649
|
-
const SYSTEM = buildSystemPrompt(convo);
|
|
1209
|
+
const SYSTEM = buildSystemPrompt(convo, settings);
|
|
650
1210
|
|
|
651
1211
|
let endpoint = settings.base_url.replace(/\/+$/, "");
|
|
652
1212
|
if (endpoint.includes("generativelanguage.googleapis.com") && !endpoint.includes("/openai")) {
|
|
@@ -700,6 +1260,8 @@ async function runOpenAiLoop(settings, convo) {
|
|
|
700
1260
|
if (delta?.content) {
|
|
701
1261
|
if (!printedHeader) {
|
|
702
1262
|
stopStatus();
|
|
1263
|
+
// Print separator line before AI response
|
|
1264
|
+
process.stdout.write(`\n${t.dim}────────────────────────────────────────${t.reset}\n`);
|
|
703
1265
|
process.stdout.write("\r\x1b[2KKILUA > ");
|
|
704
1266
|
printedHeader = true;
|
|
705
1267
|
}
|
|
@@ -742,89 +1304,157 @@ async function runOpenAiLoop(settings, convo) {
|
|
|
742
1304
|
}
|
|
743
1305
|
}
|
|
744
1306
|
|
|
745
|
-
async function runAgent(settings, userPrompt, convo) {
|
|
1307
|
+
async function runAgent(settings, userPrompt, convo, images = []) {
|
|
746
1308
|
const isAnthropic = settings.provider === "anthropic" || settings.base_url.includes(":20128") || settings.model === "harobor";
|
|
747
|
-
if (
|
|
748
|
-
|
|
749
|
-
|
|
1309
|
+
if (images.length) {
|
|
1310
|
+
if (isAnthropic) {
|
|
1311
|
+
const blocks = images.map(im => ({ type: "image", source: { type: "base64", media_type: im.mediaType, data: im.data } }));
|
|
1312
|
+
convo.push({ role: "user", content: [...blocks, { type: "text", text: userPrompt || "Jelaskan gambar ini." }] });
|
|
1313
|
+
} else {
|
|
1314
|
+
const blocks = images.map(im => ({ type: "image_url", image_url: { url: `data:${im.mediaType};base64,${im.data}` } }));
|
|
1315
|
+
convo.push({ role: "user", content: [...blocks, { type: "text", text: userPrompt || "Jelaskan gambar ini." }] });
|
|
1316
|
+
}
|
|
750
1317
|
} else {
|
|
751
1318
|
convo.push({ role: "user", content: userPrompt });
|
|
752
|
-
await runOpenAiLoop(settings, convo);
|
|
753
1319
|
}
|
|
1320
|
+
if (isAnthropic) await runAnthropicLoop(settings, convo);
|
|
1321
|
+
else await runOpenAiLoop(settings, convo);
|
|
754
1322
|
}
|
|
755
1323
|
|
|
756
|
-
function printHelp() {
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
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
|
-
`);
|
|
1324
|
+
function printHelp(settings) {
|
|
1325
|
+
const t = themeOf(settings);
|
|
1326
|
+
console.log(`\n${t.bold}${t.accent}=== PERINTAH SLASH KILUA (/) ===${t.reset}`);
|
|
1327
|
+
for (const c of SLASH_COMMANDS) console.log(` ${t.sel}${c.name.padEnd(10)}${t.reset} ${t.dim}${c.desc}${t.reset}`);
|
|
1328
|
+
console.log(`\n${t.dim}Tips: ketik "/" lalu ↑↓ untuk pilih perintah. Install skill baru cukup minta di chat.${t.reset}`);
|
|
1329
|
+
console.log(`${t.dim}Ctrl+V / Alt+V = paste teks (ringkas otomatis) atau gambar dari clipboard.${t.reset}\n`);
|
|
768
1330
|
}
|
|
769
1331
|
|
|
770
|
-
async function
|
|
771
|
-
const
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
if (action === "/help") {
|
|
775
|
-
printHelp();
|
|
1332
|
+
async function cmdModel(settings) {
|
|
1333
|
+
const t = themeOf(settings);
|
|
1334
|
+
if (!settings.base_url || !settings.api_key) {
|
|
1335
|
+
console.log(`\n${t.warn}[!] Belum terhubung ke config provider. Jalankan /config dulu.${t.reset}\n`);
|
|
776
1336
|
return settings;
|
|
777
1337
|
}
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
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;
|
|
1338
|
+
console.log(`\n${t.warn}Model aktif:${t.reset} ${t.ok}${settings.model}${t.reset} ${t.dim}(${settings.base_url})${t.reset}`);
|
|
1339
|
+
const newModel = (await promptLine(`Ganti nama model (Enter untuk tetap '${settings.model}'): `)).trim();
|
|
1340
|
+
if (newModel && newModel !== settings.model) {
|
|
1341
|
+
settings.model = newModel;
|
|
1342
|
+
saveSettings(settings);
|
|
1343
|
+
console.log(`${t.ok}✓ Model diubah menjadi ${newModel}${t.reset}\n`);
|
|
792
1344
|
}
|
|
1345
|
+
return settings;
|
|
1346
|
+
}
|
|
793
1347
|
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
}
|
|
802
|
-
|
|
803
|
-
|
|
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
|
-
}
|
|
1348
|
+
function cmdSkills(settings) {
|
|
1349
|
+
const t = themeOf(settings);
|
|
1350
|
+
const list = listInstalledSkills();
|
|
1351
|
+
const builtin = loadBuiltinSkills();
|
|
1352
|
+
console.log(`\n${t.bold}${t.accent}=== SKILLS TERPASANG (${SKILLS_DIR}) ===${t.reset}`);
|
|
1353
|
+
if (!list.length) {
|
|
1354
|
+
console.log(`Belum ada skill tambahan terpasang.`);
|
|
1355
|
+
console.log(`Suruh Kilua di chat, misal: ${t.sel}"Kilua, install skill vercel-labs/skills@find-skills"${t.reset}`);
|
|
1356
|
+
} else for (const s of list) console.log(`- ${t.ok}${s.name}${t.reset}: ${s.desc}`);
|
|
1357
|
+
console.log(`${t.dim}+ ${builtin.length} skill bawaan aktif otomatis (maks ${MAX_INJECTED_SKILLS}/pesan)${t.reset}\n`);
|
|
1358
|
+
return settings;
|
|
1359
|
+
}
|
|
810
1360
|
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
1361
|
+
async function cmdTheme(settings) {
|
|
1362
|
+
const t = themeOf(settings);
|
|
1363
|
+
const names = Object.keys(THEMES);
|
|
1364
|
+
console.log(`\n${t.bold}${t.accent}=== TEMA ===${t.reset}`);
|
|
1365
|
+
names.forEach((n, i) => {
|
|
1366
|
+
const mark = n === (settings.theme || "purple") ? `${t.ok}●${t.reset}` : " ";
|
|
1367
|
+
console.log(` ${mark} ${i + 1}) ${THEMES[n].accent}${n}${t.reset}`);
|
|
1368
|
+
});
|
|
1369
|
+
const pick = (await promptLine(`\nPilih tema [1-${names.length}] (Enter = batal): `)).trim();
|
|
1370
|
+
const idx = Number(pick) - 1;
|
|
1371
|
+
if (names[idx]) {
|
|
1372
|
+
settings.theme = names[idx];
|
|
1373
|
+
saveSettings(settings);
|
|
1374
|
+
console.log(`${themeOf(settings).ok}✓ Tema: ${settings.theme}${t.reset}\n`);
|
|
816
1375
|
}
|
|
1376
|
+
return settings;
|
|
1377
|
+
}
|
|
817
1378
|
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
1379
|
+
async function cmdMcp(settings) {
|
|
1380
|
+
const t = themeOf(settings);
|
|
1381
|
+
if (!Array.isArray(settings.mcp)) settings.mcp = [];
|
|
1382
|
+
while (true) {
|
|
1383
|
+
console.log(`\n${t.bold}${t.accent}=== MCP SERVERS ===${t.reset}`);
|
|
1384
|
+
if (!settings.mcp.length) console.log(`${t.dim}Belum ada server MCP.${t.reset}`);
|
|
1385
|
+
settings.mcp.forEach((m, i) => console.log(` ${i + 1}) ${t.ok}${m.name}${t.reset} ${t.dim}${m.command || ""} ${(m.args || []).join(" ")}${t.reset}`));
|
|
1386
|
+
|
|
1387
|
+
// NEW: Add option to start MCP server
|
|
1388
|
+
const pick = (await promptLine(`\n[a] tambah · [d] hapus · [s] mulai · [Enter] selesai: `)).trim().toLowerCase();
|
|
1389
|
+
if (!pick) break;
|
|
1390
|
+
|
|
1391
|
+
if (pick === "a") {
|
|
1392
|
+
const name = (await promptLine("Nama server: ")).trim();
|
|
1393
|
+
const command = (await promptLine("Command (mis. npx): ")).trim();
|
|
1394
|
+
const argsRaw = (await promptLine("Args (pisah spasi, opsional): ")).trim();
|
|
1395
|
+
if (!name || !command) { console.log(`${t.warn}[!] Nama & command wajib.${t.reset}`); continue; }
|
|
1396
|
+
settings.mcp.push({ name, command, args: argsRaw ? argsRaw.split(/\s+/) : [] });
|
|
1397
|
+
saveSettings(settings);
|
|
1398
|
+
console.log(`${t.ok}✓ ${name} ditambahkan.${t.reset}`);
|
|
1399
|
+
} else if (pick === "d") {
|
|
1400
|
+
const n = Number((await promptLine("Nomor yang dihapus: ")).trim()) - 1;
|
|
1401
|
+
if (settings.mcp[n]) { const [x] = settings.mcp.splice(n, 1); saveSettings(settings); console.log(`${t.ok}✓ ${x.name} dihapus.${t.reset}`); }
|
|
1402
|
+
else console.log(`${t.warn}[!] Nomor tidak valid.${t.reset}`);
|
|
1403
|
+
} else if (pick === "s") {
|
|
1404
|
+
const name = (await promptLine("Nama server: ")).trim();
|
|
1405
|
+
const command = (await promptLine("Command (mis. npx): ")).trim();
|
|
1406
|
+
const argsRaw = (await promptLine("Args (pisah spasi, opsional): ")).trim();
|
|
1407
|
+
if (!name || !command) { console.log(`${t.warn}[!] Nama & command wajib.${t.reset}`); continue; }
|
|
1408
|
+
await startMcpServer(name, command, argsRaw ? argsRaw.split(/\s+/) : []);
|
|
1409
|
+
console.log(`${t.ok}✓ ${name} dimulai.${t.reset}`);
|
|
1410
|
+
} else break;
|
|
822
1411
|
}
|
|
1412
|
+
return settings;
|
|
1413
|
+
}
|
|
823
1414
|
|
|
824
|
-
|
|
1415
|
+
async function cmdMemory(settings) {
|
|
1416
|
+
const t = themeOf(settings);
|
|
1417
|
+
const file = path.join(process.cwd(), "KILUA.md");
|
|
1418
|
+
const body = readMemory();
|
|
1419
|
+
console.log(`\n${t.bold}${t.accent}=== MEMORI PROYEK ===${t.reset} ${t.dim}${file}${t.reset}`);
|
|
1420
|
+
console.log(body.trim() ? body.trim() : `${t.dim}(kosong — memori ini selalu disuntik ke system prompt)${t.reset}`);
|
|
1421
|
+
const add = (await promptLine(`\nTambah catatan memori (Enter = batal): `)).trim();
|
|
1422
|
+
if (add) {
|
|
1423
|
+
fs.appendFileSync(file, (fs.existsSync(file) ? "\n" : "# Memori Proyek (KILUA.md)\n") + "- " + add + "\n", "utf8");
|
|
1424
|
+
console.log(`${t.ok}✓ Disimpan ke KILUA.md${t.reset}\n`);
|
|
1425
|
+
} else console.log();
|
|
825
1426
|
return settings;
|
|
826
1427
|
}
|
|
827
1428
|
|
|
1429
|
+
function cmdPlan(settings) {
|
|
1430
|
+
const t = themeOf(settings);
|
|
1431
|
+
PLAN_MODE = !PLAN_MODE;
|
|
1432
|
+
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`);
|
|
1433
|
+
return settings;
|
|
1434
|
+
}
|
|
1435
|
+
|
|
1436
|
+
async function handleSlashCommand(cmd, settings, convo) {
|
|
1437
|
+
const action = cmd.trim().split(/\s+/)[0].toLowerCase();
|
|
1438
|
+
switch (action) {
|
|
1439
|
+
case "/help": return printHelp(settings), settings;
|
|
1440
|
+
case "/model": case "/models": return await cmdModel(settings);
|
|
1441
|
+
case "/skills": case "/skill": return cmdSkills(settings);
|
|
1442
|
+
case "/theme": return await cmdTheme(settings);
|
|
1443
|
+
case "/mcp": return await cmdMcp(settings);
|
|
1444
|
+
case "/memory": return await cmdMemory(settings);
|
|
1445
|
+
case "/plan": return cmdPlan(settings);
|
|
1446
|
+
case "/clear":
|
|
1447
|
+
convo.length = 0;
|
|
1448
|
+
console.clear();
|
|
1449
|
+
console.log(`${themeOf(settings).ok}✓ Riwayat percakapan dibersihkan.${themeOf(settings).reset}\n`);
|
|
1450
|
+
return settings;
|
|
1451
|
+
case "/config": return await runInteractiveConfig();
|
|
1452
|
+
default:
|
|
1453
|
+
console.log(`Perintah tidak dikenal: ${action}. Ketik ${themeOf(settings).sel}/help${themeOf(settings).reset}.`);
|
|
1454
|
+
return settings;
|
|
1455
|
+
}
|
|
1456
|
+
}
|
|
1457
|
+
|
|
828
1458
|
async function main() {
|
|
829
1459
|
const args = process.argv.slice(2);
|
|
830
1460
|
const oneShot = args.join(" ").trim();
|
|
@@ -835,11 +1465,23 @@ async function main() {
|
|
|
835
1465
|
}
|
|
836
1466
|
|
|
837
1467
|
let settings = loadSettings();
|
|
1468
|
+
if (args[0] === "--version" || args[0] === "-v") {
|
|
1469
|
+
const here = path.dirname(fileURLToPath(import.meta.url));
|
|
1470
|
+
const candidates = [path.join(here, "..", "packages", "kilua-agent", "package.json"), path.join(here, "..", "package.json")];
|
|
1471
|
+
let v = "1.3.0";
|
|
1472
|
+
for (const p of candidates) {
|
|
1473
|
+
try {
|
|
1474
|
+
const j = JSON.parse(fs.readFileSync(p, "utf8"));
|
|
1475
|
+
if (j.name === "kilua-ai" && j.private) continue; // root app, bukan paket CLI
|
|
1476
|
+
if (j.version) { v = j.version; break; }
|
|
1477
|
+
} catch {}
|
|
1478
|
+
}
|
|
1479
|
+
console.log(`kilua-agent ${v}`);
|
|
1480
|
+
process.exit(0);
|
|
1481
|
+
}
|
|
838
1482
|
|
|
839
|
-
// Validasi ketat: jika belum ada base_url atau api_key, WAJIB setup dulu
|
|
840
1483
|
const isLocalProxy = settings.base_url?.includes("127.0.0.1") || settings.base_url?.includes("localhost");
|
|
841
1484
|
const hasValidConfig = settings.base_url && (settings.api_key || isLocalProxy);
|
|
842
|
-
|
|
843
1485
|
if (!hasValidConfig) {
|
|
844
1486
|
console.log("\x1b[33m[!] Belum ada konfigurasi AI key / provider.\x1b[0m");
|
|
845
1487
|
settings = await runInteractiveConfig();
|
|
@@ -851,29 +1493,25 @@ async function main() {
|
|
|
851
1493
|
process.exit(0);
|
|
852
1494
|
}
|
|
853
1495
|
|
|
854
|
-
|
|
855
|
-
console.log(
|
|
856
|
-
console.log(
|
|
857
|
-
console.log(
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
const
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
ask();
|
|
874
|
-
});
|
|
875
|
-
};
|
|
876
|
-
ask();
|
|
1496
|
+
const t = themeOf(settings);
|
|
1497
|
+
console.log(`${t.bold}${t.accent}=== KILUA AI CLI AGENT ===${t.reset}`);
|
|
1498
|
+
console.log(`${t.dim}CWD :${t.reset} ${t.warn}${process.cwd()}${t.reset}`);
|
|
1499
|
+
console.log(`${t.dim}Provider:${t.reset} ${t.ok}${settings.base_url}${t.reset} (${settings.model})`);
|
|
1500
|
+
console.log(`${t.dim}Bantuan :${t.reset} ${t.sel}/${t.reset} perintah · ${t.sel}Ctrl+V${t.reset} paste teks/gambar\n`);
|
|
1501
|
+
|
|
1502
|
+
const history = [];
|
|
1503
|
+
while (true) {
|
|
1504
|
+
const { text, images } = await promptComposer(settings, history);
|
|
1505
|
+
if (!text && !images.length) continue;
|
|
1506
|
+
if (text === "exit" || text === "quit" || text === "/exit") break;
|
|
1507
|
+
if (images.length && !text) { await runAgent(settings, "", convo, images); continue; }
|
|
1508
|
+
if (text.startsWith("/")) { settings = await handleSlashCommand(text, settings, convo); continue; }
|
|
1509
|
+
history.push(text);
|
|
1510
|
+
if (history.length > 100) history.shift();
|
|
1511
|
+
await runAgent(settings, text, convo, images);
|
|
1512
|
+
}
|
|
1513
|
+
console.log(`${t.dim}Sampai jumpa.${t.reset}`);
|
|
1514
|
+
process.exit(0);
|
|
877
1515
|
}
|
|
878
1516
|
|
|
879
1517
|
main();
|