@gilangranggaid/kilua-agent 1.0.5 → 1.0.8

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 +147 -53
  2. package/bin/kilua.mjs +147 -53
  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, install skill (npx skills add), 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,34 +145,30 @@ 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)");
137
- console.log("2) TokenHarbor / 9Router / OpenRouter (OpenAI-compatible)");
151
+ console.log("2) TokenHarbor / OpenRouter / Custom (OpenAI-compatible)");
138
152
  console.log("3) Google Gemini (AI Studio Key)\n");
139
153
 
140
154
  const choice = (await promptQuestion(rl, "Pilihan [1/2/3] (default 1): ")).trim() || "1";
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";
147
-
158
+ const defaultUrl = "http://127.0.0.1:20128/v1";
148
159
  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;
150
- const model = (await promptQuestion(rl, `Model (${defaultModel}): `)).trim() || defaultModel;
160
+ const key = (await promptQuestion(rl, `API Key/Token (kosongkan jika lokal): `)).trim();
161
+ const model = (await promptQuestion(rl, `Nama Model yang ingin dipakai di 9Router (contoh: harobor): `)).trim();
151
162
 
152
163
  settings = {
153
164
  provider: "anthropic",
154
165
  base_url: baseUrl.replace(/\/+$/, ""),
155
166
  api_key: key,
156
- model
167
+ model: model || "harobor"
157
168
  };
158
169
  } else if (choice === "3") {
159
170
  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";
171
+ const model = (await promptQuestion(rl, "Nama Model (contoh: gemini-3.5-flash-lite): ")).trim() || "gemini-3.5-flash-lite";
161
172
  settings = {
162
173
  provider: "openai",
163
174
  base_url: "https://generativelanguage.googleapis.com/v1beta/openai",
@@ -166,9 +177,9 @@ async function runInteractiveConfig() {
166
177
  };
167
178
  } else {
168
179
  console.log("Contoh Base URL: https://tokenharbor.ai/v1");
169
- const baseUrl = (await promptQuestion(rl, "Base URL (https://tokenharbor.ai/v1): ")).trim() || "https://tokenharbor.ai/v1";
180
+ const baseUrl = (await promptQuestion(rl, "Base URL: ")).trim() || "https://tokenharbor.ai/v1";
170
181
  const key = (await promptQuestion(rl, "API Key: ")).trim();
171
- const model = (await promptQuestion(rl, "Model (deepseek-v4.1-flash:free): ")).trim() || "deepseek-v4.1-flash:free";
182
+ const model = (await promptQuestion(rl, "Nama Model (contoh: deepseek-v4.1-flash:free): ")).trim() || "deepseek-v4.1-flash:free";
172
183
  settings = {
173
184
  provider: "openai",
174
185
  api_key: key,
@@ -218,14 +229,32 @@ async function readSseStream(res, onChunk) {
218
229
  }
219
230
  }
220
231
 
232
+ function buildSystemPrompt() {
233
+ const skillsInfo = getSkillsPrompt();
234
+ return `Kamu KILUA, AI Coding Agent otonom di terminal lokal. Folder aktif: ${process.cwd()}.
235
+ KEMAMPUAN TOOLS:
236
+ - read_file, write_file, edit_file, list_dir
237
+ - run_cmd: menjalankan perintah terminal lokal (npm, npx, git, node, python, build, test, curl, install skill, dsb).
238
+
239
+ ATURAN OTONOM KILUA (SELF-TESTING & AUTO-INSTALL SKILL):
240
+ 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).
241
+ 2. Jika ada error saat kamu mencoba, perbaiki langsung menggunakan edit_file/write_file sampai berhasil.
242
+ 3. Setelah berhasil dicoba dan dipastikan tidak error, barulah beri tahu pengguna hasil dan cara menjalankannya.
243
+ 4. Jika pengguna di chat menyuruh kamu install skill (misal: "install skill vercel-labs/skills@find-skills" atau skill lain), jalankan run_cmd dengan perintah:
244
+ npx skills add <nama-skill> --cwd "${SKILLS_DIR}"
245
+ Lalu beri tahu pengguna bahwa skill tersebut telah siap dipakai.${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,76 @@ 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 di Kilua
458
+ \x1b[36m/clear\x1b[0m Bersihkan riwayat percakapan sesi ini
459
+ \x1b[36m/config\x1b[0m Ubah API Key, Provider, atau Base URL
460
+ \x1b[36m/exit\x1b[0m Keluar dari Kilua Agent
461
+
462
+ \x1b[90mTips: Untuk menginstall skill baru, cukup suruh Kilua di chat (misal: "tolong install skill find-skills")!\x1b[0m
463
+ `);
464
+ }
465
+
466
+ async function handleSlashCommand(cmd, settings, convo, rl) {
467
+ const parts = cmd.trim().split(/\s+/);
468
+ const action = parts[0].toLowerCase();
469
+
470
+ if (action === "/help") {
471
+ printHelp();
472
+ return settings;
473
+ }
474
+
475
+ if (action === "/models" || action === "/model") {
476
+ if (!settings.base_url || !settings.api_key) {
477
+ console.log(`\n\x1b[33m[!] Belum terhubung ke config provider. Jalankan /config terlebih dahulu.\x1b[0m\n`);
478
+ return settings;
479
+ }
480
+ console.log(`\n\x1b[33mModel aktif saat ini:\x1b[0m \x1b[32m${settings.model}\x1b[0m (Provider: ${settings.base_url})`);
481
+ const newModel = (await promptQuestion(rl, `Ganti nama model (Enter untuk tetap '${settings.model}'): `)).trim();
482
+ if (newModel) {
483
+ settings.model = newModel;
484
+ saveSettings(settings);
485
+ console.log(`\x1b[32m✓ Model diubah menjadi ${newModel}\x1b[0m\n`);
486
+ }
487
+ return settings;
488
+ }
489
+
490
+ if (action === "/skills" || action === "/skill") {
491
+ const list = listInstalledSkills();
492
+ console.log(`\n\x1b[35m=== SKILLS TERPASANG (${SKILLS_DIR}) ===\x1b[0m`);
493
+ if (!list.length) {
494
+ console.log(`Belum ada skill tambahan terpasang.`);
495
+ console.log(`Suruh Kilua di chat untuk install, misal: \x1b[36m"Kilua, tolong install skill vercel-labs/skills@find-skills"\x1b[0m`);
496
+ } else {
497
+ for (const s of list) {
498
+ console.log(`- \x1b[32m${s.name}\x1b[0m: ${s.desc}`);
499
+ }
500
+ }
501
+ console.log();
502
+ return settings;
503
+ }
504
+
505
+ if (action === "/clear") {
506
+ convo.length = 0;
507
+ console.clear();
508
+ console.log(`\x1b[32m✓ Riwayat percakapan dibersihkan.\x1b[0m\n`);
509
+ return settings;
510
+ }
511
+
512
+ if (action === "/config") {
513
+ rl.close();
514
+ const newSettings = await runInteractiveConfig();
515
+ return newSettings;
516
+ }
517
+
518
+ console.log(`Perintah tidak dikenal: ${action}. Ketik \x1b[36m/help\x1b[0m untuk daftar perintah.`);
519
+ return settings;
520
+ }
521
+
425
522
  async function main() {
426
523
  const args = process.argv.slice(2);
427
524
  const oneShot = args.join(" ").trim();
@@ -433,16 +530,13 @@ async function main() {
433
530
 
434
531
  let settings = loadSettings();
435
532
 
436
- // Auto-import dari Claude Code jika settings kilua belum ada
437
- 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
- }
533
+ // Validasi ketat: jika belum ada base_url atau api_key, WAJIB setup dulu
534
+ const isLocalProxy = settings.base_url?.includes("127.0.0.1") || settings.base_url?.includes("localhost");
535
+ const hasValidConfig = settings.base_url && (settings.api_key || isLocalProxy);
536
+
537
+ if (!hasValidConfig) {
538
+ console.log("\x1b[33m[!] Belum ada konfigurasi AI key / provider.\x1b[0m");
539
+ settings = await runInteractiveConfig();
446
540
  }
447
541
 
448
542
  const convo = [];
@@ -454,19 +548,19 @@ async function main() {
454
548
  console.log("\x1b[1m\x1b[35m=== KILUA AI CLI AGENT ===\x1b[0m");
455
549
  console.log(`CWD : \x1b[33m${process.cwd()}\x1b[0m`);
456
550
  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`);
551
+ console.log(`Config : \x1b[90m${SETTINGS_PATH}\x1b[0m (ketik \x1b[36m/help\x1b[0m untuk panduan)\n`);
458
552
 
459
553
  let rl = readline.createInterface({ input: process.stdin, output: process.stdout });
460
554
  const ask = () => {
461
555
  rl.question("\x1b[1mKAMU > \x1b[0m", async input => {
462
556
  const q = input.trim();
463
557
  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 });
558
+ if (q === "exit" || q === "quit" || q === "/exit") { rl.close(); process.exit(0); }
559
+ if (q.startsWith("/")) {
560
+ settings = await handleSlashCommand(q, settings, convo, rl);
561
+ if (q === "/config") {
562
+ rl = readline.createInterface({ input: process.stdin, output: process.stdout });
563
+ }
470
564
  return ask();
471
565
  }
472
566
  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, install skill (npx skills add), 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,34 +145,30 @@ 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)");
137
- console.log("2) TokenHarbor / 9Router / OpenRouter (OpenAI-compatible)");
151
+ console.log("2) TokenHarbor / OpenRouter / Custom (OpenAI-compatible)");
138
152
  console.log("3) Google Gemini (AI Studio Key)\n");
139
153
 
140
154
  const choice = (await promptQuestion(rl, "Pilihan [1/2/3] (default 1): ")).trim() || "1";
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";
147
-
158
+ const defaultUrl = "http://127.0.0.1:20128/v1";
148
159
  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;
150
- const model = (await promptQuestion(rl, `Model (${defaultModel}): `)).trim() || defaultModel;
160
+ const key = (await promptQuestion(rl, `API Key/Token (kosongkan jika lokal): `)).trim();
161
+ const model = (await promptQuestion(rl, `Nama Model yang ingin dipakai di 9Router (contoh: harobor): `)).trim();
151
162
 
152
163
  settings = {
153
164
  provider: "anthropic",
154
165
  base_url: baseUrl.replace(/\/+$/, ""),
155
166
  api_key: key,
156
- model
167
+ model: model || "harobor"
157
168
  };
158
169
  } else if (choice === "3") {
159
170
  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";
171
+ const model = (await promptQuestion(rl, "Nama Model (contoh: gemini-3.5-flash-lite): ")).trim() || "gemini-3.5-flash-lite";
161
172
  settings = {
162
173
  provider: "openai",
163
174
  base_url: "https://generativelanguage.googleapis.com/v1beta/openai",
@@ -166,9 +177,9 @@ async function runInteractiveConfig() {
166
177
  };
167
178
  } else {
168
179
  console.log("Contoh Base URL: https://tokenharbor.ai/v1");
169
- const baseUrl = (await promptQuestion(rl, "Base URL (https://tokenharbor.ai/v1): ")).trim() || "https://tokenharbor.ai/v1";
180
+ const baseUrl = (await promptQuestion(rl, "Base URL: ")).trim() || "https://tokenharbor.ai/v1";
170
181
  const key = (await promptQuestion(rl, "API Key: ")).trim();
171
- const model = (await promptQuestion(rl, "Model (deepseek-v4.1-flash:free): ")).trim() || "deepseek-v4.1-flash:free";
182
+ const model = (await promptQuestion(rl, "Nama Model (contoh: deepseek-v4.1-flash:free): ")).trim() || "deepseek-v4.1-flash:free";
172
183
  settings = {
173
184
  provider: "openai",
174
185
  api_key: key,
@@ -218,14 +229,32 @@ async function readSseStream(res, onChunk) {
218
229
  }
219
230
  }
220
231
 
232
+ function buildSystemPrompt() {
233
+ const skillsInfo = getSkillsPrompt();
234
+ return `Kamu KILUA, AI Coding Agent otonom di terminal lokal. Folder aktif: ${process.cwd()}.
235
+ KEMAMPUAN TOOLS:
236
+ - read_file, write_file, edit_file, list_dir
237
+ - run_cmd: menjalankan perintah terminal lokal (npm, npx, git, node, python, build, test, curl, install skill, dsb).
238
+
239
+ ATURAN OTONOM KILUA (SELF-TESTING & AUTO-INSTALL SKILL):
240
+ 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).
241
+ 2. Jika ada error saat kamu mencoba, perbaiki langsung menggunakan edit_file/write_file sampai berhasil.
242
+ 3. Setelah berhasil dicoba dan dipastikan tidak error, barulah beri tahu pengguna hasil dan cara menjalankannya.
243
+ 4. Jika pengguna di chat menyuruh kamu install skill (misal: "install skill vercel-labs/skills@find-skills" atau skill lain), jalankan run_cmd dengan perintah:
244
+ npx skills add <nama-skill> --cwd "${SKILLS_DIR}"
245
+ Lalu beri tahu pengguna bahwa skill tersebut telah siap dipakai.${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,76 @@ 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 di Kilua
458
+ \x1b[36m/clear\x1b[0m Bersihkan riwayat percakapan sesi ini
459
+ \x1b[36m/config\x1b[0m Ubah API Key, Provider, atau Base URL
460
+ \x1b[36m/exit\x1b[0m Keluar dari Kilua Agent
461
+
462
+ \x1b[90mTips: Untuk menginstall skill baru, cukup suruh Kilua di chat (misal: "tolong install skill find-skills")!\x1b[0m
463
+ `);
464
+ }
465
+
466
+ async function handleSlashCommand(cmd, settings, convo, rl) {
467
+ const parts = cmd.trim().split(/\s+/);
468
+ const action = parts[0].toLowerCase();
469
+
470
+ if (action === "/help") {
471
+ printHelp();
472
+ return settings;
473
+ }
474
+
475
+ if (action === "/models" || action === "/model") {
476
+ if (!settings.base_url || !settings.api_key) {
477
+ console.log(`\n\x1b[33m[!] Belum terhubung ke config provider. Jalankan /config terlebih dahulu.\x1b[0m\n`);
478
+ return settings;
479
+ }
480
+ console.log(`\n\x1b[33mModel aktif saat ini:\x1b[0m \x1b[32m${settings.model}\x1b[0m (Provider: ${settings.base_url})`);
481
+ const newModel = (await promptQuestion(rl, `Ganti nama model (Enter untuk tetap '${settings.model}'): `)).trim();
482
+ if (newModel) {
483
+ settings.model = newModel;
484
+ saveSettings(settings);
485
+ console.log(`\x1b[32m✓ Model diubah menjadi ${newModel}\x1b[0m\n`);
486
+ }
487
+ return settings;
488
+ }
489
+
490
+ if (action === "/skills" || action === "/skill") {
491
+ const list = listInstalledSkills();
492
+ console.log(`\n\x1b[35m=== SKILLS TERPASANG (${SKILLS_DIR}) ===\x1b[0m`);
493
+ if (!list.length) {
494
+ console.log(`Belum ada skill tambahan terpasang.`);
495
+ console.log(`Suruh Kilua di chat untuk install, misal: \x1b[36m"Kilua, tolong install skill vercel-labs/skills@find-skills"\x1b[0m`);
496
+ } else {
497
+ for (const s of list) {
498
+ console.log(`- \x1b[32m${s.name}\x1b[0m: ${s.desc}`);
499
+ }
500
+ }
501
+ console.log();
502
+ return settings;
503
+ }
504
+
505
+ if (action === "/clear") {
506
+ convo.length = 0;
507
+ console.clear();
508
+ console.log(`\x1b[32m✓ Riwayat percakapan dibersihkan.\x1b[0m\n`);
509
+ return settings;
510
+ }
511
+
512
+ if (action === "/config") {
513
+ rl.close();
514
+ const newSettings = await runInteractiveConfig();
515
+ return newSettings;
516
+ }
517
+
518
+ console.log(`Perintah tidak dikenal: ${action}. Ketik \x1b[36m/help\x1b[0m untuk daftar perintah.`);
519
+ return settings;
520
+ }
521
+
425
522
  async function main() {
426
523
  const args = process.argv.slice(2);
427
524
  const oneShot = args.join(" ").trim();
@@ -433,16 +530,13 @@ async function main() {
433
530
 
434
531
  let settings = loadSettings();
435
532
 
436
- // Auto-import dari Claude Code jika settings kilua belum ada
437
- 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
- }
533
+ // Validasi ketat: jika belum ada base_url atau api_key, WAJIB setup dulu
534
+ const isLocalProxy = settings.base_url?.includes("127.0.0.1") || settings.base_url?.includes("localhost");
535
+ const hasValidConfig = settings.base_url && (settings.api_key || isLocalProxy);
536
+
537
+ if (!hasValidConfig) {
538
+ console.log("\x1b[33m[!] Belum ada konfigurasi AI key / provider.\x1b[0m");
539
+ settings = await runInteractiveConfig();
446
540
  }
447
541
 
448
542
  const convo = [];
@@ -454,19 +548,19 @@ async function main() {
454
548
  console.log("\x1b[1m\x1b[35m=== KILUA AI CLI AGENT ===\x1b[0m");
455
549
  console.log(`CWD : \x1b[33m${process.cwd()}\x1b[0m`);
456
550
  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`);
551
+ console.log(`Config : \x1b[90m${SETTINGS_PATH}\x1b[0m (ketik \x1b[36m/help\x1b[0m untuk panduan)\n`);
458
552
 
459
553
  let rl = readline.createInterface({ input: process.stdin, output: process.stdout });
460
554
  const ask = () => {
461
555
  rl.question("\x1b[1mKAMU > \x1b[0m", async input => {
462
556
  const q = input.trim();
463
557
  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 });
558
+ if (q === "exit" || q === "quit" || q === "/exit") { rl.close(); process.exit(0); }
559
+ if (q.startsWith("/")) {
560
+ settings = await handleSlashCommand(q, settings, convo, rl);
561
+ if (q === "/config") {
562
+ rl = readline.createInterface({ input: process.stdin, output: process.stdout });
563
+ }
470
564
  return ask();
471
565
  }
472
566
  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.8",
4
4
  "description": "Autonomous AI Coding Agent for your terminal and VS Code",
5
5
  "type": "module",
6
6
  "main": "bin/kilua.js",