@gilangranggaid/kilua-agent 1.0.5 → 1.0.7

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 +151 -46
  2. package/bin/kilua.mjs +151 -46
  3. package/package.json +1 -1
package/bin/kilua.js CHANGED
@@ -5,13 +5,14 @@ import path from "node:path";
5
5
  import os from "node:os";
6
6
  import { execSync } from "node:child_process";
7
7
 
8
- // ponytail: single-file agent CLI supporting real-time streaming for 9router, Anthropic & OpenAI proxies.
8
+ // ponytail: autonomous single-file AI Coding Agent CLI with slash commands, local skills & test execution.
9
9
  const KILUA_DIR = path.join(os.homedir(), ".kilua");
10
10
  const SETTINGS_PATH = path.join(KILUA_DIR, "settings.json");
11
- const CLAUDE_SETTINGS_PATH = path.join(os.homedir(), ".claude", "settings.json");
11
+ const SKILLS_DIR = path.join(KILUA_DIR, "skills");
12
12
 
13
13
  function ensureKiluaDir() {
14
14
  if (!fs.existsSync(KILUA_DIR)) fs.mkdirSync(KILUA_DIR, { recursive: true });
15
+ if (!fs.existsSync(SKILLS_DIR)) fs.mkdirSync(SKILLS_DIR, { recursive: true });
15
16
  }
16
17
 
17
18
  function loadSettings() {
@@ -24,20 +25,34 @@ function saveSettings(settings) {
24
25
  fs.writeFileSync(SETTINGS_PATH, JSON.stringify(settings, null, 2), "utf8");
25
26
  }
26
27
 
27
- function readClaudeSettings() {
28
+ function listInstalledSkills() {
29
+ ensureKiluaDir();
30
+ const skills = [];
28
31
  try {
29
- const raw = JSON.parse(fs.readFileSync(CLAUDE_SETTINGS_PATH, "utf8"));
30
- const env = raw.env || {};
31
- if (env.ANTHROPIC_BASE_URL || env.ANTHROPIC_AUTH_TOKEN) {
32
- return {
33
- base_url: env.ANTHROPIC_BASE_URL || "http://127.0.0.1:20128/v1",
34
- api_key: env.ANTHROPIC_AUTH_TOKEN || "",
35
- model: env.ANTHROPIC_DEFAULT_OPUS_MODEL || env.ANTHROPIC_DEFAULT_SONNET_MODEL || "harobor",
36
- provider: "anthropic"
37
- };
32
+ const entries = fs.readdirSync(SKILLS_DIR, { withFileTypes: true });
33
+ for (const entry of entries) {
34
+ const full = path.join(SKILLS_DIR, entry.name);
35
+ if (entry.isDirectory()) {
36
+ const readme = path.join(full, "SKILL.md");
37
+ const doc = fs.existsSync(readme) ? fs.readFileSync(readme, "utf8").slice(0, 300) : "No description";
38
+ skills.push({ name: entry.name, desc: doc.split("\n")[0] || entry.name });
39
+ } else if (entry.name.endsWith(".md") || entry.name.endsWith(".txt")) {
40
+ const doc = fs.readFileSync(full, "utf8").slice(0, 300);
41
+ skills.push({ name: entry.name.replace(/\.[^.]+$/, ""), desc: doc.split("\n")[0] || entry.name });
42
+ }
38
43
  }
39
44
  } catch {}
40
- return null;
45
+ return skills;
46
+ }
47
+
48
+ function getSkillsPrompt() {
49
+ const list = listInstalledSkills();
50
+ if (!list.length) return "";
51
+ let prompt = "\n\n=== INSTALLED SKILLS ===\n";
52
+ for (const s of list) {
53
+ prompt += `- ${s.name}: ${s.desc}\n`;
54
+ }
55
+ return prompt;
41
56
  }
42
57
 
43
58
  const TOOLS_OPENAI = [
@@ -77,7 +92,7 @@ const TOOLS_OPENAI = [
77
92
  type: "function",
78
93
  function: {
79
94
  name: "run_cmd",
80
- description: "Menjalankan perintah terminal lokal.",
95
+ description: "Menjalankan perintah terminal lokal untuk instalasi dependencies, test, build, atau verifikasi project.",
81
96
  parameters: { type: "object", properties: { command: { type: "string" } }, required: ["command"] }
82
97
  }
83
98
  }
@@ -115,8 +130,8 @@ function executeLocalTool(name, args) {
115
130
  return fs.readdirSync(target, { withFileTypes: true }).map(it => `${it.isDirectory() ? "[DIR]" : "[FILE]"} ${it.name}`).join("\n");
116
131
  }
117
132
  if (name === "run_cmd") {
118
- const out = execSync(args.command, { cwd, encoding: "utf8", timeout: 60000, stdio: ["pipe", "pipe", "pipe"] });
119
- return out.trim() || "(Done)";
133
+ const out = execSync(args.command, { cwd, encoding: "utf8", timeout: 120000, stdio: ["pipe", "pipe", "pipe"] });
134
+ return out.trim() || "(Done with exit code 0)";
120
135
  }
121
136
  } catch (err) {
122
137
  return `Error: ${err.stderr || err.message}`;
@@ -130,7 +145,6 @@ function promptQuestion(rl, query) {
130
145
 
131
146
  async function runInteractiveConfig() {
132
147
  const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
133
- const claudeCfg = readClaudeSettings();
134
148
 
135
149
  console.log("\x1b[35m=== KILUA CONFIG ===\x1b[0m\nPath: " + SETTINGS_PATH);
136
150
  console.log("1) 9Router Local / Claude Code Proxy (Port 20128)");
@@ -141,12 +155,11 @@ async function runInteractiveConfig() {
141
155
  let settings = {};
142
156
 
143
157
  if (choice === "1") {
144
- const defaultUrl = claudeCfg?.base_url || "http://127.0.0.1:20128/v1";
145
- const defaultKey = claudeCfg?.api_key || "";
146
- const defaultModel = claudeCfg?.model || "harobor";
158
+ const defaultUrl = "http://127.0.0.1:20128/v1";
159
+ const defaultModel = "harobor";
147
160
 
148
161
  const baseUrl = (await promptQuestion(rl, `Base URL (${defaultUrl}): `)).trim() || defaultUrl;
149
- const key = (await promptQuestion(rl, `API Key/Token (${defaultKey ? defaultKey.slice(0, 10) + "..." : "masukkan token"}): `)).trim() || defaultKey;
162
+ const key = (await promptQuestion(rl, `API Key/Token (kosongkan jika tidak perlu): `)).trim();
150
163
  const model = (await promptQuestion(rl, `Model (${defaultModel}): `)).trim() || defaultModel;
151
164
 
152
165
  settings = {
@@ -157,7 +170,7 @@ async function runInteractiveConfig() {
157
170
  };
158
171
  } else if (choice === "3") {
159
172
  const key = (await promptQuestion(rl, "Masukkan GEMINI_API_KEY: ")).trim();
160
- const model = (await promptQuestion(rl, "Nama Model (gemini-2.5-flash): ")).trim() || "gemini-2.5-flash";
173
+ const model = (await promptQuestion(rl, "Nama Model (gemini-3.5-flash-lite): ")).trim() || "gemini-3.5-flash-lite";
161
174
  settings = {
162
175
  provider: "openai",
163
176
  base_url: "https://generativelanguage.googleapis.com/v1beta/openai",
@@ -218,14 +231,30 @@ async function readSseStream(res, onChunk) {
218
231
  }
219
232
  }
220
233
 
234
+ function buildSystemPrompt() {
235
+ const skillsInfo = getSkillsPrompt();
236
+ return `Kamu KILUA, AI Coding Agent otonom di terminal lokal. Folder aktif: ${process.cwd()}.
237
+ KEMAMPUAN TOOLS:
238
+ - read_file, write_file, edit_file, list_dir
239
+ - run_cmd: menjalankan perintah terminal lokal (npm, npx, git, node, python, build, test, curl, dsb).
240
+
241
+ ATURAN OTONOM KILUA (SELF-TESTING):
242
+ 1. Jika pengguna meminta membuat project/fitur/skrip, JANGAN langsung serahkan begitu saja. Kamu HARUS mencoba atau memvalidasinya sendiri terlebih dahulu menggunakan run_cmd (misal: jalankan skrip, test, build, atau cek sintaks).
243
+ 2. Jika ada error saat kamu mencoba, perbaiki langsung menggunakan edit_file/write_file sampai berhasil.
244
+ 3. Setelah berhasil dicoba dan dipastikan tidak error, barulah beri tahu pengguna hasil dan cara menjalankannya.
245
+ 4. Kamu dapat menginstall skill baru atau CLI menggunakan run_cmd (misalnya: npx skills add <nama-skill> atau npm i).${skillsInfo}
246
+
247
+ Respon ringkas, padat, dan bertindak langsung.`;
248
+ }
249
+
221
250
  // Loop untuk Anthropic / 9router local (/v1/messages)
222
251
  async function runAnthropicLoop(settings, convo) {
223
- const SYSTEM = `Kamu KILUA, AI Coding Agent di terminal lokal. Folder: ${process.cwd()}. Gunakan tools (read_file, write_file, edit_file, list_dir, run_cmd) untuk inspeksi dan eksekusi tugas coding mandiri. Respon ringkas to the point.`;
252
+ const SYSTEM = buildSystemPrompt();
224
253
 
225
254
  let endpoint = settings.base_url.replace(/\/+$/, "");
226
255
  if (!endpoint.endsWith("/messages")) endpoint += "/messages";
227
256
 
228
- for (let step = 0; step < 12; step++) {
257
+ for (let step = 0; step < 16; step++) {
229
258
  process.stdout.write("\r\x1b[2K\x1b[35m[KILUA Berpikir...]\x1b[0m ");
230
259
  let res;
231
260
  try {
@@ -262,7 +291,6 @@ async function runAnthropicLoop(settings, convo) {
262
291
  const toolBlocks = [];
263
292
 
264
293
  await readSseStream(res, (evt) => {
265
- // 1. Text deltas
266
294
  let deltaText = "";
267
295
  if (typeof evt.delta?.text === "string") deltaText = evt.delta.text;
268
296
  else if (typeof evt.delta?.content === "string") deltaText = evt.delta.content;
@@ -278,7 +306,6 @@ async function runAnthropicLoop(settings, convo) {
278
306
  fullText += deltaText;
279
307
  }
280
308
 
281
- // 2. Tool deltas
282
309
  if (evt.type === "content_block_start" && evt.content_block?.type === "tool_use") {
283
310
  toolBlocks[evt.index] = { ...evt.content_block, _raw_json: "" };
284
311
  } else if (evt.delta?.type === "input_json_delta") {
@@ -313,7 +340,7 @@ async function runAnthropicLoop(settings, convo) {
313
340
  const args = tu.input || {};
314
341
  console.log(`\x1b[36m⚡ Tool: ${name}\x1b[0m(${JSON.stringify(args)})`);
315
342
  const output = executeLocalTool(name, args);
316
- console.log(`\x1b[90m↳ ${output.slice(0, 140).replace(/\n/g, " ")}${output.length > 140 ? "..." : ""}\x1b[0m`);
343
+ console.log(`\x1b[90m↳ ${output.slice(0, 160).replace(/\n/g, " ")}${output.length > 160 ? "..." : ""}\x1b[0m`);
317
344
  toolResultContent.push({
318
345
  type: "tool_result",
319
346
  tool_use_id: tu.id,
@@ -326,7 +353,7 @@ async function runAnthropicLoop(settings, convo) {
326
353
 
327
354
  // Loop untuk OpenAI / TokenHarbor / Gemini OpenAI protocol
328
355
  async function runOpenAiLoop(settings, convo) {
329
- const SYSTEM = `Kamu KILUA, AI Coding Agent di terminal lokal. Folder: ${process.cwd()}. Gunakan tools (read_file, write_file, edit_file, list_dir, run_cmd) untuk inspeksi dan eksekusi tugas coding mandiri. Respon ringkas to the point.`;
356
+ const SYSTEM = buildSystemPrompt();
330
357
 
331
358
  let endpoint = settings.base_url.replace(/\/+$/, "");
332
359
  if (endpoint.includes("generativelanguage.googleapis.com") && !endpoint.includes("/openai")) {
@@ -336,7 +363,7 @@ async function runOpenAiLoop(settings, convo) {
336
363
  endpoint += "/chat/completions";
337
364
  }
338
365
 
339
- for (let step = 0; step < 12; step++) {
366
+ for (let step = 0; step < 16; step++) {
340
367
  process.stdout.write("\r\x1b[2K\x1b[35m[KILUA Berpikir...]\x1b[0m ");
341
368
  let res;
342
369
  try {
@@ -405,7 +432,7 @@ async function runOpenAiLoop(settings, convo) {
405
432
  try { args = JSON.parse(tc.function.arguments || "{}"); } catch {}
406
433
  console.log(`\x1b[36m⚡ Tool: ${name}\x1b[0m(${JSON.stringify(args)})`);
407
434
  const output = executeLocalTool(name, args);
408
- console.log(`\x1b[90m↳ ${output.slice(0, 140).replace(/\n/g, " ")}${output.length > 140 ? "..." : ""}\x1b[0m`);
435
+ console.log(`\x1b[90m↳ ${output.slice(0, 160).replace(/\n/g, " ")}${output.length > 160 ? "..." : ""}\x1b[0m`);
409
436
  convo.push({ role: "tool", tool_call_id: tc.id, name, content: output });
410
437
  }
411
438
  }
@@ -422,6 +449,91 @@ async function runAgent(settings, userPrompt, convo) {
422
449
  }
423
450
  }
424
451
 
452
+ function printHelp() {
453
+ console.log(`
454
+ \x1b[1m\x1b[35m=== PERINTAH SLASH KILUA (/) ===\x1b[0m
455
+ \x1b[36m/help\x1b[0m Lihat panduan perintah ini
456
+ \x1b[36m/models\x1b[0m Lihat dan ganti model AI saat ini
457
+ \x1b[36m/skills\x1b[0m Lihat daftar skills yang terpasang
458
+ \x1b[36m/install-skill\x1b[0m Pasang skill baru (misal: /install-skill vercel-labs/skills@find-skills)
459
+ \x1b[36m/clear\x1b[0m Bersihkan riwayat percakapan sesi ini
460
+ \x1b[36m/config\x1b[0m Ubah API Key, Provider, atau Base URL
461
+ \x1b[36m/exit\x1b[0m Keluar dari Kilua Agent
462
+ `);
463
+ }
464
+
465
+ async function handleSlashCommand(cmd, settings, convo, rl) {
466
+ const parts = cmd.trim().split(/\s+/);
467
+ const action = parts[0].toLowerCase();
468
+
469
+ if (action === "/help") {
470
+ printHelp();
471
+ return settings;
472
+ }
473
+
474
+ if (action === "/models" || action === "/model") {
475
+ console.log(`\n\x1b[33mModel aktif saat ini:\x1b[0m \x1b[32m${settings.model}\x1b[0m`);
476
+ console.log(`Rekomendasi Model:`);
477
+ console.log(`- TokenHarbor : deepseek-v4.1-flash:free`);
478
+ console.log(`- Gemini : gemini-3.5-flash-lite, gemini-2.5-flash`);
479
+ console.log(`- 9Router : harobor, claude-3-5-sonnet-20241022\n`);
480
+ const newModel = (await promptQuestion(rl, `Ganti model (Enter untuk tetap '${settings.model}'): `)).trim();
481
+ if (newModel) {
482
+ settings.model = newModel;
483
+ saveSettings(settings);
484
+ console.log(`\x1b[32m✓ Model diubah menjadi ${newModel}\x1b[0m\n`);
485
+ }
486
+ return settings;
487
+ }
488
+
489
+ if (action === "/skills" || action === "/skill") {
490
+ const list = listInstalledSkills();
491
+ console.log(`\n\x1b[35m=== SKILLS TERPASANG (${SKILLS_DIR}) ===\x1b[0m`);
492
+ if (!list.length) {
493
+ console.log(`(Belum ada skill tambahan terpasang)`);
494
+ console.log(`Ketik \x1b[36m/install-skill <nama-skill>\x1b[0m untuk menambah skill.`);
495
+ } else {
496
+ for (const s of list) {
497
+ console.log(`- \x1b[32m${s.name}\x1b[0m: ${s.desc}`);
498
+ }
499
+ }
500
+ console.log();
501
+ return settings;
502
+ }
503
+
504
+ if (action === "/install-skill") {
505
+ const target = parts.slice(1).join(" ").trim();
506
+ if (!target) {
507
+ console.log(`\x1b[33mContoh penggunaan: /install-skill vercel-labs/skills@find-skills\x1b[0m`);
508
+ return settings;
509
+ }
510
+ console.log(`\x1b[35m[Memasang skill ${target}...]\x1b[0m`);
511
+ try {
512
+ execSync(`npx skills add ${target}`, { cwd: SKILLS_DIR, stdio: "inherit" });
513
+ console.log(`\x1b[32m✓ Skill ${target} berhasil dipasang!\x1b[0m\n`);
514
+ } catch (e) {
515
+ console.error(`[!] Gagal memasang skill:`, e.message);
516
+ }
517
+ return settings;
518
+ }
519
+
520
+ if (action === "/clear") {
521
+ convo.length = 0;
522
+ console.clear();
523
+ console.log(`\x1b[32m✓ Riwayat percakapan dibersihkan.\x1b[0m\n`);
524
+ return settings;
525
+ }
526
+
527
+ if (action === "/config") {
528
+ rl.close();
529
+ const newSettings = await runInteractiveConfig();
530
+ return newSettings;
531
+ }
532
+
533
+ console.log(`Perintah tidak dikenal: ${action}. Ketik \x1b[36m/help\x1b[0m untuk daftar perintah.`);
534
+ return settings;
535
+ }
536
+
425
537
  async function main() {
426
538
  const args = process.argv.slice(2);
427
539
  const oneShot = args.join(" ").trim();
@@ -433,16 +545,9 @@ async function main() {
433
545
 
434
546
  let settings = loadSettings();
435
547
 
436
- // Auto-import dari Claude Code jika settings kilua belum ada
437
548
  if (!settings.api_key) {
438
- const claudeCfg = readClaudeSettings();
439
- if (claudeCfg) {
440
- settings = claudeCfg;
441
- saveSettings(settings);
442
- } else {
443
- console.log("\x1b[33m[!] Belum ada konfigurasi AI key.\x1b[0m");
444
- settings = await runInteractiveConfig();
445
- }
549
+ console.log("\x1b[33m[!] Belum ada konfigurasi AI key.\x1b[0m");
550
+ settings = await runInteractiveConfig();
446
551
  }
447
552
 
448
553
  const convo = [];
@@ -454,19 +559,19 @@ async function main() {
454
559
  console.log("\x1b[1m\x1b[35m=== KILUA AI CLI AGENT ===\x1b[0m");
455
560
  console.log(`CWD : \x1b[33m${process.cwd()}\x1b[0m`);
456
561
  console.log(`Provider: \x1b[32m${settings.base_url}\x1b[0m (${settings.model})`);
457
- console.log(`Config : \x1b[90m${SETTINGS_PATH}\x1b[0m (ketik 'config' untuk ganti)\n`);
562
+ console.log(`Config : \x1b[90m${SETTINGS_PATH}\x1b[0m (ketik \x1b[36m/help\x1b[0m untuk perintah slash)\n`);
458
563
 
459
564
  let rl = readline.createInterface({ input: process.stdin, output: process.stdout });
460
565
  const ask = () => {
461
566
  rl.question("\x1b[1mKAMU > \x1b[0m", async input => {
462
567
  const q = input.trim();
463
568
  if (!q) return ask();
464
- if (q === "exit" || q === "quit") { rl.close(); process.exit(0); }
465
- if (q === "config" || q === "/config") {
466
- rl.close();
467
- await runInteractiveConfig();
468
- settings = loadSettings();
469
- rl = readline.createInterface({ input: process.stdin, output: process.stdout });
569
+ if (q === "exit" || q === "quit" || q === "/exit") { rl.close(); process.exit(0); }
570
+ if (q.startsWith("/")) {
571
+ settings = await handleSlashCommand(q, settings, convo, rl);
572
+ if (q === "/config") {
573
+ rl = readline.createInterface({ input: process.stdin, output: process.stdout });
574
+ }
470
575
  return ask();
471
576
  }
472
577
  await runAgent(settings, q, convo);
package/bin/kilua.mjs CHANGED
@@ -5,13 +5,14 @@ import path from "node:path";
5
5
  import os from "node:os";
6
6
  import { execSync } from "node:child_process";
7
7
 
8
- // ponytail: single-file agent CLI supporting real-time streaming for 9router, Anthropic & OpenAI proxies.
8
+ // ponytail: autonomous single-file AI Coding Agent CLI with slash commands, local skills & test execution.
9
9
  const KILUA_DIR = path.join(os.homedir(), ".kilua");
10
10
  const SETTINGS_PATH = path.join(KILUA_DIR, "settings.json");
11
- const CLAUDE_SETTINGS_PATH = path.join(os.homedir(), ".claude", "settings.json");
11
+ const SKILLS_DIR = path.join(KILUA_DIR, "skills");
12
12
 
13
13
  function ensureKiluaDir() {
14
14
  if (!fs.existsSync(KILUA_DIR)) fs.mkdirSync(KILUA_DIR, { recursive: true });
15
+ if (!fs.existsSync(SKILLS_DIR)) fs.mkdirSync(SKILLS_DIR, { recursive: true });
15
16
  }
16
17
 
17
18
  function loadSettings() {
@@ -24,20 +25,34 @@ function saveSettings(settings) {
24
25
  fs.writeFileSync(SETTINGS_PATH, JSON.stringify(settings, null, 2), "utf8");
25
26
  }
26
27
 
27
- function readClaudeSettings() {
28
+ function listInstalledSkills() {
29
+ ensureKiluaDir();
30
+ const skills = [];
28
31
  try {
29
- const raw = JSON.parse(fs.readFileSync(CLAUDE_SETTINGS_PATH, "utf8"));
30
- const env = raw.env || {};
31
- if (env.ANTHROPIC_BASE_URL || env.ANTHROPIC_AUTH_TOKEN) {
32
- return {
33
- base_url: env.ANTHROPIC_BASE_URL || "http://127.0.0.1:20128/v1",
34
- api_key: env.ANTHROPIC_AUTH_TOKEN || "",
35
- model: env.ANTHROPIC_DEFAULT_OPUS_MODEL || env.ANTHROPIC_DEFAULT_SONNET_MODEL || "harobor",
36
- provider: "anthropic"
37
- };
32
+ const entries = fs.readdirSync(SKILLS_DIR, { withFileTypes: true });
33
+ for (const entry of entries) {
34
+ const full = path.join(SKILLS_DIR, entry.name);
35
+ if (entry.isDirectory()) {
36
+ const readme = path.join(full, "SKILL.md");
37
+ const doc = fs.existsSync(readme) ? fs.readFileSync(readme, "utf8").slice(0, 300) : "No description";
38
+ skills.push({ name: entry.name, desc: doc.split("\n")[0] || entry.name });
39
+ } else if (entry.name.endsWith(".md") || entry.name.endsWith(".txt")) {
40
+ const doc = fs.readFileSync(full, "utf8").slice(0, 300);
41
+ skills.push({ name: entry.name.replace(/\.[^.]+$/, ""), desc: doc.split("\n")[0] || entry.name });
42
+ }
38
43
  }
39
44
  } catch {}
40
- return null;
45
+ return skills;
46
+ }
47
+
48
+ function getSkillsPrompt() {
49
+ const list = listInstalledSkills();
50
+ if (!list.length) return "";
51
+ let prompt = "\n\n=== INSTALLED SKILLS ===\n";
52
+ for (const s of list) {
53
+ prompt += `- ${s.name}: ${s.desc}\n`;
54
+ }
55
+ return prompt;
41
56
  }
42
57
 
43
58
  const TOOLS_OPENAI = [
@@ -77,7 +92,7 @@ const TOOLS_OPENAI = [
77
92
  type: "function",
78
93
  function: {
79
94
  name: "run_cmd",
80
- description: "Menjalankan perintah terminal lokal.",
95
+ description: "Menjalankan perintah terminal lokal untuk instalasi dependencies, test, build, atau verifikasi project.",
81
96
  parameters: { type: "object", properties: { command: { type: "string" } }, required: ["command"] }
82
97
  }
83
98
  }
@@ -115,8 +130,8 @@ function executeLocalTool(name, args) {
115
130
  return fs.readdirSync(target, { withFileTypes: true }).map(it => `${it.isDirectory() ? "[DIR]" : "[FILE]"} ${it.name}`).join("\n");
116
131
  }
117
132
  if (name === "run_cmd") {
118
- const out = execSync(args.command, { cwd, encoding: "utf8", timeout: 60000, stdio: ["pipe", "pipe", "pipe"] });
119
- return out.trim() || "(Done)";
133
+ const out = execSync(args.command, { cwd, encoding: "utf8", timeout: 120000, stdio: ["pipe", "pipe", "pipe"] });
134
+ return out.trim() || "(Done with exit code 0)";
120
135
  }
121
136
  } catch (err) {
122
137
  return `Error: ${err.stderr || err.message}`;
@@ -130,7 +145,6 @@ function promptQuestion(rl, query) {
130
145
 
131
146
  async function runInteractiveConfig() {
132
147
  const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
133
- const claudeCfg = readClaudeSettings();
134
148
 
135
149
  console.log("\x1b[35m=== KILUA CONFIG ===\x1b[0m\nPath: " + SETTINGS_PATH);
136
150
  console.log("1) 9Router Local / Claude Code Proxy (Port 20128)");
@@ -141,12 +155,11 @@ async function runInteractiveConfig() {
141
155
  let settings = {};
142
156
 
143
157
  if (choice === "1") {
144
- const defaultUrl = claudeCfg?.base_url || "http://127.0.0.1:20128/v1";
145
- const defaultKey = claudeCfg?.api_key || "";
146
- const defaultModel = claudeCfg?.model || "harobor";
158
+ const defaultUrl = "http://127.0.0.1:20128/v1";
159
+ const defaultModel = "harobor";
147
160
 
148
161
  const baseUrl = (await promptQuestion(rl, `Base URL (${defaultUrl}): `)).trim() || defaultUrl;
149
- const key = (await promptQuestion(rl, `API Key/Token (${defaultKey ? defaultKey.slice(0, 10) + "..." : "masukkan token"}): `)).trim() || defaultKey;
162
+ const key = (await promptQuestion(rl, `API Key/Token (kosongkan jika tidak perlu): `)).trim();
150
163
  const model = (await promptQuestion(rl, `Model (${defaultModel}): `)).trim() || defaultModel;
151
164
 
152
165
  settings = {
@@ -157,7 +170,7 @@ async function runInteractiveConfig() {
157
170
  };
158
171
  } else if (choice === "3") {
159
172
  const key = (await promptQuestion(rl, "Masukkan GEMINI_API_KEY: ")).trim();
160
- const model = (await promptQuestion(rl, "Nama Model (gemini-2.5-flash): ")).trim() || "gemini-2.5-flash";
173
+ const model = (await promptQuestion(rl, "Nama Model (gemini-3.5-flash-lite): ")).trim() || "gemini-3.5-flash-lite";
161
174
  settings = {
162
175
  provider: "openai",
163
176
  base_url: "https://generativelanguage.googleapis.com/v1beta/openai",
@@ -218,14 +231,30 @@ async function readSseStream(res, onChunk) {
218
231
  }
219
232
  }
220
233
 
234
+ function buildSystemPrompt() {
235
+ const skillsInfo = getSkillsPrompt();
236
+ return `Kamu KILUA, AI Coding Agent otonom di terminal lokal. Folder aktif: ${process.cwd()}.
237
+ KEMAMPUAN TOOLS:
238
+ - read_file, write_file, edit_file, list_dir
239
+ - run_cmd: menjalankan perintah terminal lokal (npm, npx, git, node, python, build, test, curl, dsb).
240
+
241
+ ATURAN OTONOM KILUA (SELF-TESTING):
242
+ 1. Jika pengguna meminta membuat project/fitur/skrip, JANGAN langsung serahkan begitu saja. Kamu HARUS mencoba atau memvalidasinya sendiri terlebih dahulu menggunakan run_cmd (misal: jalankan skrip, test, build, atau cek sintaks).
243
+ 2. Jika ada error saat kamu mencoba, perbaiki langsung menggunakan edit_file/write_file sampai berhasil.
244
+ 3. Setelah berhasil dicoba dan dipastikan tidak error, barulah beri tahu pengguna hasil dan cara menjalankannya.
245
+ 4. Kamu dapat menginstall skill baru atau CLI menggunakan run_cmd (misalnya: npx skills add <nama-skill> atau npm i).${skillsInfo}
246
+
247
+ Respon ringkas, padat, dan bertindak langsung.`;
248
+ }
249
+
221
250
  // Loop untuk Anthropic / 9router local (/v1/messages)
222
251
  async function runAnthropicLoop(settings, convo) {
223
- const SYSTEM = `Kamu KILUA, AI Coding Agent di terminal lokal. Folder: ${process.cwd()}. Gunakan tools (read_file, write_file, edit_file, list_dir, run_cmd) untuk inspeksi dan eksekusi tugas coding mandiri. Respon ringkas to the point.`;
252
+ const SYSTEM = buildSystemPrompt();
224
253
 
225
254
  let endpoint = settings.base_url.replace(/\/+$/, "");
226
255
  if (!endpoint.endsWith("/messages")) endpoint += "/messages";
227
256
 
228
- for (let step = 0; step < 12; step++) {
257
+ for (let step = 0; step < 16; step++) {
229
258
  process.stdout.write("\r\x1b[2K\x1b[35m[KILUA Berpikir...]\x1b[0m ");
230
259
  let res;
231
260
  try {
@@ -262,7 +291,6 @@ async function runAnthropicLoop(settings, convo) {
262
291
  const toolBlocks = [];
263
292
 
264
293
  await readSseStream(res, (evt) => {
265
- // 1. Text deltas
266
294
  let deltaText = "";
267
295
  if (typeof evt.delta?.text === "string") deltaText = evt.delta.text;
268
296
  else if (typeof evt.delta?.content === "string") deltaText = evt.delta.content;
@@ -278,7 +306,6 @@ async function runAnthropicLoop(settings, convo) {
278
306
  fullText += deltaText;
279
307
  }
280
308
 
281
- // 2. Tool deltas
282
309
  if (evt.type === "content_block_start" && evt.content_block?.type === "tool_use") {
283
310
  toolBlocks[evt.index] = { ...evt.content_block, _raw_json: "" };
284
311
  } else if (evt.delta?.type === "input_json_delta") {
@@ -313,7 +340,7 @@ async function runAnthropicLoop(settings, convo) {
313
340
  const args = tu.input || {};
314
341
  console.log(`\x1b[36m⚡ Tool: ${name}\x1b[0m(${JSON.stringify(args)})`);
315
342
  const output = executeLocalTool(name, args);
316
- console.log(`\x1b[90m↳ ${output.slice(0, 140).replace(/\n/g, " ")}${output.length > 140 ? "..." : ""}\x1b[0m`);
343
+ console.log(`\x1b[90m↳ ${output.slice(0, 160).replace(/\n/g, " ")}${output.length > 160 ? "..." : ""}\x1b[0m`);
317
344
  toolResultContent.push({
318
345
  type: "tool_result",
319
346
  tool_use_id: tu.id,
@@ -326,7 +353,7 @@ async function runAnthropicLoop(settings, convo) {
326
353
 
327
354
  // Loop untuk OpenAI / TokenHarbor / Gemini OpenAI protocol
328
355
  async function runOpenAiLoop(settings, convo) {
329
- const SYSTEM = `Kamu KILUA, AI Coding Agent di terminal lokal. Folder: ${process.cwd()}. Gunakan tools (read_file, write_file, edit_file, list_dir, run_cmd) untuk inspeksi dan eksekusi tugas coding mandiri. Respon ringkas to the point.`;
356
+ const SYSTEM = buildSystemPrompt();
330
357
 
331
358
  let endpoint = settings.base_url.replace(/\/+$/, "");
332
359
  if (endpoint.includes("generativelanguage.googleapis.com") && !endpoint.includes("/openai")) {
@@ -336,7 +363,7 @@ async function runOpenAiLoop(settings, convo) {
336
363
  endpoint += "/chat/completions";
337
364
  }
338
365
 
339
- for (let step = 0; step < 12; step++) {
366
+ for (let step = 0; step < 16; step++) {
340
367
  process.stdout.write("\r\x1b[2K\x1b[35m[KILUA Berpikir...]\x1b[0m ");
341
368
  let res;
342
369
  try {
@@ -405,7 +432,7 @@ async function runOpenAiLoop(settings, convo) {
405
432
  try { args = JSON.parse(tc.function.arguments || "{}"); } catch {}
406
433
  console.log(`\x1b[36m⚡ Tool: ${name}\x1b[0m(${JSON.stringify(args)})`);
407
434
  const output = executeLocalTool(name, args);
408
- console.log(`\x1b[90m↳ ${output.slice(0, 140).replace(/\n/g, " ")}${output.length > 140 ? "..." : ""}\x1b[0m`);
435
+ console.log(`\x1b[90m↳ ${output.slice(0, 160).replace(/\n/g, " ")}${output.length > 160 ? "..." : ""}\x1b[0m`);
409
436
  convo.push({ role: "tool", tool_call_id: tc.id, name, content: output });
410
437
  }
411
438
  }
@@ -422,6 +449,91 @@ async function runAgent(settings, userPrompt, convo) {
422
449
  }
423
450
  }
424
451
 
452
+ function printHelp() {
453
+ console.log(`
454
+ \x1b[1m\x1b[35m=== PERINTAH SLASH KILUA (/) ===\x1b[0m
455
+ \x1b[36m/help\x1b[0m Lihat panduan perintah ini
456
+ \x1b[36m/models\x1b[0m Lihat dan ganti model AI saat ini
457
+ \x1b[36m/skills\x1b[0m Lihat daftar skills yang terpasang
458
+ \x1b[36m/install-skill\x1b[0m Pasang skill baru (misal: /install-skill vercel-labs/skills@find-skills)
459
+ \x1b[36m/clear\x1b[0m Bersihkan riwayat percakapan sesi ini
460
+ \x1b[36m/config\x1b[0m Ubah API Key, Provider, atau Base URL
461
+ \x1b[36m/exit\x1b[0m Keluar dari Kilua Agent
462
+ `);
463
+ }
464
+
465
+ async function handleSlashCommand(cmd, settings, convo, rl) {
466
+ const parts = cmd.trim().split(/\s+/);
467
+ const action = parts[0].toLowerCase();
468
+
469
+ if (action === "/help") {
470
+ printHelp();
471
+ return settings;
472
+ }
473
+
474
+ if (action === "/models" || action === "/model") {
475
+ console.log(`\n\x1b[33mModel aktif saat ini:\x1b[0m \x1b[32m${settings.model}\x1b[0m`);
476
+ console.log(`Rekomendasi Model:`);
477
+ console.log(`- TokenHarbor : deepseek-v4.1-flash:free`);
478
+ console.log(`- Gemini : gemini-3.5-flash-lite, gemini-2.5-flash`);
479
+ console.log(`- 9Router : harobor, claude-3-5-sonnet-20241022\n`);
480
+ const newModel = (await promptQuestion(rl, `Ganti model (Enter untuk tetap '${settings.model}'): `)).trim();
481
+ if (newModel) {
482
+ settings.model = newModel;
483
+ saveSettings(settings);
484
+ console.log(`\x1b[32m✓ Model diubah menjadi ${newModel}\x1b[0m\n`);
485
+ }
486
+ return settings;
487
+ }
488
+
489
+ if (action === "/skills" || action === "/skill") {
490
+ const list = listInstalledSkills();
491
+ console.log(`\n\x1b[35m=== SKILLS TERPASANG (${SKILLS_DIR}) ===\x1b[0m`);
492
+ if (!list.length) {
493
+ console.log(`(Belum ada skill tambahan terpasang)`);
494
+ console.log(`Ketik \x1b[36m/install-skill <nama-skill>\x1b[0m untuk menambah skill.`);
495
+ } else {
496
+ for (const s of list) {
497
+ console.log(`- \x1b[32m${s.name}\x1b[0m: ${s.desc}`);
498
+ }
499
+ }
500
+ console.log();
501
+ return settings;
502
+ }
503
+
504
+ if (action === "/install-skill") {
505
+ const target = parts.slice(1).join(" ").trim();
506
+ if (!target) {
507
+ console.log(`\x1b[33mContoh penggunaan: /install-skill vercel-labs/skills@find-skills\x1b[0m`);
508
+ return settings;
509
+ }
510
+ console.log(`\x1b[35m[Memasang skill ${target}...]\x1b[0m`);
511
+ try {
512
+ execSync(`npx skills add ${target}`, { cwd: SKILLS_DIR, stdio: "inherit" });
513
+ console.log(`\x1b[32m✓ Skill ${target} berhasil dipasang!\x1b[0m\n`);
514
+ } catch (e) {
515
+ console.error(`[!] Gagal memasang skill:`, e.message);
516
+ }
517
+ return settings;
518
+ }
519
+
520
+ if (action === "/clear") {
521
+ convo.length = 0;
522
+ console.clear();
523
+ console.log(`\x1b[32m✓ Riwayat percakapan dibersihkan.\x1b[0m\n`);
524
+ return settings;
525
+ }
526
+
527
+ if (action === "/config") {
528
+ rl.close();
529
+ const newSettings = await runInteractiveConfig();
530
+ return newSettings;
531
+ }
532
+
533
+ console.log(`Perintah tidak dikenal: ${action}. Ketik \x1b[36m/help\x1b[0m untuk daftar perintah.`);
534
+ return settings;
535
+ }
536
+
425
537
  async function main() {
426
538
  const args = process.argv.slice(2);
427
539
  const oneShot = args.join(" ").trim();
@@ -433,16 +545,9 @@ async function main() {
433
545
 
434
546
  let settings = loadSettings();
435
547
 
436
- // Auto-import dari Claude Code jika settings kilua belum ada
437
548
  if (!settings.api_key) {
438
- const claudeCfg = readClaudeSettings();
439
- if (claudeCfg) {
440
- settings = claudeCfg;
441
- saveSettings(settings);
442
- } else {
443
- console.log("\x1b[33m[!] Belum ada konfigurasi AI key.\x1b[0m");
444
- settings = await runInteractiveConfig();
445
- }
549
+ console.log("\x1b[33m[!] Belum ada konfigurasi AI key.\x1b[0m");
550
+ settings = await runInteractiveConfig();
446
551
  }
447
552
 
448
553
  const convo = [];
@@ -454,19 +559,19 @@ async function main() {
454
559
  console.log("\x1b[1m\x1b[35m=== KILUA AI CLI AGENT ===\x1b[0m");
455
560
  console.log(`CWD : \x1b[33m${process.cwd()}\x1b[0m`);
456
561
  console.log(`Provider: \x1b[32m${settings.base_url}\x1b[0m (${settings.model})`);
457
- console.log(`Config : \x1b[90m${SETTINGS_PATH}\x1b[0m (ketik 'config' untuk ganti)\n`);
562
+ console.log(`Config : \x1b[90m${SETTINGS_PATH}\x1b[0m (ketik \x1b[36m/help\x1b[0m untuk perintah slash)\n`);
458
563
 
459
564
  let rl = readline.createInterface({ input: process.stdin, output: process.stdout });
460
565
  const ask = () => {
461
566
  rl.question("\x1b[1mKAMU > \x1b[0m", async input => {
462
567
  const q = input.trim();
463
568
  if (!q) return ask();
464
- if (q === "exit" || q === "quit") { rl.close(); process.exit(0); }
465
- if (q === "config" || q === "/config") {
466
- rl.close();
467
- await runInteractiveConfig();
468
- settings = loadSettings();
469
- rl = readline.createInterface({ input: process.stdin, output: process.stdout });
569
+ if (q === "exit" || q === "quit" || q === "/exit") { rl.close(); process.exit(0); }
570
+ if (q.startsWith("/")) {
571
+ settings = await handleSlashCommand(q, settings, convo, rl);
572
+ if (q === "/config") {
573
+ rl = readline.createInterface({ input: process.stdin, output: process.stdout });
574
+ }
470
575
  return ask();
471
576
  }
472
577
  await runAgent(settings, q, convo);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gilangranggaid/kilua-agent",
3
- "version": "1.0.5",
3
+ "version": "1.0.7",
4
4
  "description": "Autonomous AI Coding Agent for your terminal and VS Code",
5
5
  "type": "module",
6
6
  "main": "bin/kilua.js",