@gilangranggaid/kilua-agent 1.3.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 +200 -7
- package/bin/kilua.mjs +200 -7
- package/package.json +1 -1
package/bin/kilua.js
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.
|
|
@@ -14,6 +14,119 @@ const MAX_STEPS = 40; // ponytail: naik dari 16; agent butuh banyak langkah untu
|
|
|
14
14
|
const MAX_INJECTED_SKILLS = 5; // sama seperti web: cukup 5 skill per pesan biar konteks hemat
|
|
15
15
|
let PLAN_MODE = false; // read-only mode: semua tool yang mengubah state diblokir
|
|
16
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
|
+
}
|
|
129
|
+
|
|
17
130
|
// ponytail: match pakai substring ternormalisasi ("docker-compose" == "docker compose").
|
|
18
131
|
// Regex mentah dari web terlalu kaku: trigger "docker-compose" tak pernah kena "docker compose".
|
|
19
132
|
const STOPWORDS = new Set(("yang dan untuk dengan pada dari ini itu saya aku kamu anda bikin buat buatkan "
|
|
@@ -261,11 +374,15 @@ function executeLocalTool(name, args) {
|
|
|
261
374
|
? `grep -rn --include='${glob}' '${pattern.replace(/'/g, "'\\''")}' '${target}' | head -200`
|
|
262
375
|
: `grep -rn --exclude-dir=node_modules --exclude-dir=.git '${pattern.replace(/'/g, "'\\''")}' '${target}' | head -200`;
|
|
263
376
|
}
|
|
264
|
-
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"] });
|
|
265
378
|
return out.trim() || "(tidak ada hasil)";
|
|
266
379
|
}
|
|
267
380
|
if (name === "run_cmd") {
|
|
268
|
-
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
|
+
}
|
|
269
386
|
return out.trim() || "(Done with exit code 0)";
|
|
270
387
|
}
|
|
271
388
|
} catch (err) {
|
|
@@ -312,6 +429,50 @@ const SLASH_COMMANDS = [
|
|
|
312
429
|
];
|
|
313
430
|
const MENU_VISIBLE = 8;
|
|
314
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
|
+
|
|
315
476
|
// ===================== CLIPBOARD (Windows) =====================
|
|
316
477
|
function readClipboard() {
|
|
317
478
|
if (process.platform !== "win32") return null;
|
|
@@ -432,8 +593,17 @@ function promptComposer(settings, history) {
|
|
|
432
593
|
return prefix + painted;
|
|
433
594
|
});
|
|
434
595
|
|
|
435
|
-
if (lastLines)
|
|
436
|
-
|
|
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
|
+
}
|
|
437
607
|
const block = [...menuLines, ...colored];
|
|
438
608
|
stdout.write(block.join("\n"));
|
|
439
609
|
lastLines = block.length;
|
|
@@ -449,7 +619,14 @@ function promptComposer(settings, history) {
|
|
|
449
619
|
closed = true;
|
|
450
620
|
stdin.removeListener("data", onData);
|
|
451
621
|
process.removeAllListeners("SIGINT");
|
|
452
|
-
if (lastLines)
|
|
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
|
+
}
|
|
453
630
|
lastLines = 0;
|
|
454
631
|
stdout.write("\x1b[?2004l");
|
|
455
632
|
try { stdin.setRawMode(false); } catch {}
|
|
@@ -464,6 +641,8 @@ function promptComposer(settings, history) {
|
|
|
464
641
|
restore();
|
|
465
642
|
let shown = summary.replace(/\n/g, " ⏎ ");
|
|
466
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`);
|
|
467
646
|
stdout.write(`${t.accent}❯${t.reset} ${shown}\n`);
|
|
468
647
|
resolve({ text, images: imgs });
|
|
469
648
|
}
|
|
@@ -965,6 +1144,8 @@ async function runAnthropicLoop(settings, convo) {
|
|
|
965
1144
|
if (deltaText) {
|
|
966
1145
|
if (!printedHeader) {
|
|
967
1146
|
stopStatus();
|
|
1147
|
+
// Print separator line before AI response
|
|
1148
|
+
process.stdout.write(`\n${t.dim}────────────────────────────────────────${t.reset}\n`);
|
|
968
1149
|
process.stdout.write("\r\x1b[2KKILUA > ");
|
|
969
1150
|
printedHeader = true;
|
|
970
1151
|
}
|
|
@@ -1079,6 +1260,8 @@ async function runOpenAiLoop(settings, convo) {
|
|
|
1079
1260
|
if (delta?.content) {
|
|
1080
1261
|
if (!printedHeader) {
|
|
1081
1262
|
stopStatus();
|
|
1263
|
+
// Print separator line before AI response
|
|
1264
|
+
process.stdout.write(`\n${t.dim}────────────────────────────────────────${t.reset}\n`);
|
|
1082
1265
|
process.stdout.write("\r\x1b[2KKILUA > ");
|
|
1083
1266
|
printedHeader = true;
|
|
1084
1267
|
}
|
|
@@ -1200,8 +1383,11 @@ async function cmdMcp(settings) {
|
|
|
1200
1383
|
console.log(`\n${t.bold}${t.accent}=== MCP SERVERS ===${t.reset}`);
|
|
1201
1384
|
if (!settings.mcp.length) console.log(`${t.dim}Belum ada server MCP.${t.reset}`);
|
|
1202
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}`));
|
|
1203
|
-
|
|
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();
|
|
1204
1389
|
if (!pick) break;
|
|
1390
|
+
|
|
1205
1391
|
if (pick === "a") {
|
|
1206
1392
|
const name = (await promptLine("Nama server: ")).trim();
|
|
1207
1393
|
const command = (await promptLine("Command (mis. npx): ")).trim();
|
|
@@ -1214,6 +1400,13 @@ async function cmdMcp(settings) {
|
|
|
1214
1400
|
const n = Number((await promptLine("Nomor yang dihapus: ")).trim()) - 1;
|
|
1215
1401
|
if (settings.mcp[n]) { const [x] = settings.mcp.splice(n, 1); saveSettings(settings); console.log(`${t.ok}✓ ${x.name} dihapus.${t.reset}`); }
|
|
1216
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}`);
|
|
1217
1410
|
} else break;
|
|
1218
1411
|
}
|
|
1219
1412
|
return settings;
|
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.
|
|
@@ -14,6 +14,119 @@ const MAX_STEPS = 40; // ponytail: naik dari 16; agent butuh banyak langkah untu
|
|
|
14
14
|
const MAX_INJECTED_SKILLS = 5; // sama seperti web: cukup 5 skill per pesan biar konteks hemat
|
|
15
15
|
let PLAN_MODE = false; // read-only mode: semua tool yang mengubah state diblokir
|
|
16
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
|
+
}
|
|
129
|
+
|
|
17
130
|
// ponytail: match pakai substring ternormalisasi ("docker-compose" == "docker compose").
|
|
18
131
|
// Regex mentah dari web terlalu kaku: trigger "docker-compose" tak pernah kena "docker compose".
|
|
19
132
|
const STOPWORDS = new Set(("yang dan untuk dengan pada dari ini itu saya aku kamu anda bikin buat buatkan "
|
|
@@ -261,11 +374,15 @@ function executeLocalTool(name, args) {
|
|
|
261
374
|
? `grep -rn --include='${glob}' '${pattern.replace(/'/g, "'\\''")}' '${target}' | head -200`
|
|
262
375
|
: `grep -rn --exclude-dir=node_modules --exclude-dir=.git '${pattern.replace(/'/g, "'\\''")}' '${target}' | head -200`;
|
|
263
376
|
}
|
|
264
|
-
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"] });
|
|
265
378
|
return out.trim() || "(tidak ada hasil)";
|
|
266
379
|
}
|
|
267
380
|
if (name === "run_cmd") {
|
|
268
|
-
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
|
+
}
|
|
269
386
|
return out.trim() || "(Done with exit code 0)";
|
|
270
387
|
}
|
|
271
388
|
} catch (err) {
|
|
@@ -312,6 +429,50 @@ const SLASH_COMMANDS = [
|
|
|
312
429
|
];
|
|
313
430
|
const MENU_VISIBLE = 8;
|
|
314
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
|
+
|
|
315
476
|
// ===================== CLIPBOARD (Windows) =====================
|
|
316
477
|
function readClipboard() {
|
|
317
478
|
if (process.platform !== "win32") return null;
|
|
@@ -432,8 +593,17 @@ function promptComposer(settings, history) {
|
|
|
432
593
|
return prefix + painted;
|
|
433
594
|
});
|
|
434
595
|
|
|
435
|
-
if (lastLines)
|
|
436
|
-
|
|
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
|
+
}
|
|
437
607
|
const block = [...menuLines, ...colored];
|
|
438
608
|
stdout.write(block.join("\n"));
|
|
439
609
|
lastLines = block.length;
|
|
@@ -449,7 +619,14 @@ function promptComposer(settings, history) {
|
|
|
449
619
|
closed = true;
|
|
450
620
|
stdin.removeListener("data", onData);
|
|
451
621
|
process.removeAllListeners("SIGINT");
|
|
452
|
-
if (lastLines)
|
|
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
|
+
}
|
|
453
630
|
lastLines = 0;
|
|
454
631
|
stdout.write("\x1b[?2004l");
|
|
455
632
|
try { stdin.setRawMode(false); } catch {}
|
|
@@ -464,6 +641,8 @@ function promptComposer(settings, history) {
|
|
|
464
641
|
restore();
|
|
465
642
|
let shown = summary.replace(/\n/g, " ⏎ ");
|
|
466
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`);
|
|
467
646
|
stdout.write(`${t.accent}❯${t.reset} ${shown}\n`);
|
|
468
647
|
resolve({ text, images: imgs });
|
|
469
648
|
}
|
|
@@ -965,6 +1144,8 @@ async function runAnthropicLoop(settings, convo) {
|
|
|
965
1144
|
if (deltaText) {
|
|
966
1145
|
if (!printedHeader) {
|
|
967
1146
|
stopStatus();
|
|
1147
|
+
// Print separator line before AI response
|
|
1148
|
+
process.stdout.write(`\n${t.dim}────────────────────────────────────────${t.reset}\n`);
|
|
968
1149
|
process.stdout.write("\r\x1b[2KKILUA > ");
|
|
969
1150
|
printedHeader = true;
|
|
970
1151
|
}
|
|
@@ -1079,6 +1260,8 @@ async function runOpenAiLoop(settings, convo) {
|
|
|
1079
1260
|
if (delta?.content) {
|
|
1080
1261
|
if (!printedHeader) {
|
|
1081
1262
|
stopStatus();
|
|
1263
|
+
// Print separator line before AI response
|
|
1264
|
+
process.stdout.write(`\n${t.dim}────────────────────────────────────────${t.reset}\n`);
|
|
1082
1265
|
process.stdout.write("\r\x1b[2KKILUA > ");
|
|
1083
1266
|
printedHeader = true;
|
|
1084
1267
|
}
|
|
@@ -1200,8 +1383,11 @@ async function cmdMcp(settings) {
|
|
|
1200
1383
|
console.log(`\n${t.bold}${t.accent}=== MCP SERVERS ===${t.reset}`);
|
|
1201
1384
|
if (!settings.mcp.length) console.log(`${t.dim}Belum ada server MCP.${t.reset}`);
|
|
1202
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}`));
|
|
1203
|
-
|
|
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();
|
|
1204
1389
|
if (!pick) break;
|
|
1390
|
+
|
|
1205
1391
|
if (pick === "a") {
|
|
1206
1392
|
const name = (await promptLine("Nama server: ")).trim();
|
|
1207
1393
|
const command = (await promptLine("Command (mis. npx): ")).trim();
|
|
@@ -1214,6 +1400,13 @@ async function cmdMcp(settings) {
|
|
|
1214
1400
|
const n = Number((await promptLine("Nomor yang dihapus: ")).trim()) - 1;
|
|
1215
1401
|
if (settings.mcp[n]) { const [x] = settings.mcp.splice(n, 1); saveSettings(settings); console.log(`${t.ok}✓ ${x.name} dihapus.${t.reset}`); }
|
|
1216
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}`);
|
|
1217
1410
|
} else break;
|
|
1218
1411
|
}
|
|
1219
1412
|
return settings;
|