@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.
Files changed (3) hide show
  1. package/bin/kilua.js +200 -7
  2. package/bin/kilua.mjs +200 -7
  3. 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) stdout.write(`\x1b[${lastLines - 1}A\r\x1b[0J`);
436
- else stdout.write("\r\x1b[0J");
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) stdout.write(`\x1b[${lastLines - 1}A\r\x1b[0J`);
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
- const pick = (await promptLine(`\n[a] tambah · [d] hapus · [Enter] selesai: `)).trim().toLowerCase();
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) stdout.write(`\x1b[${lastLines - 1}A\r\x1b[0J`);
436
- else stdout.write("\r\x1b[0J");
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) stdout.write(`\x1b[${lastLines - 1}A\r\x1b[0J`);
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
- const pick = (await promptLine(`\n[a] tambah · [d] hapus · [Enter] selesai: `)).trim().toLowerCase();
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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gilangranggaid/kilua-agent",
3
- "version": "1.3.0",
3
+ "version": "1.3.1",
4
4
  "description": "Autonomous AI Coding Agent for your terminal and VS Code",
5
5
  "type": "module",
6
6
  "main": "bin/kilua.js",