@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.
- package/bin/kilua.js +151 -46
- package/bin/kilua.mjs +151 -46
- 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
|
|
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
|
|
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
|
|
28
|
+
function listInstalledSkills() {
|
|
29
|
+
ensureKiluaDir();
|
|
30
|
+
const skills = [];
|
|
28
31
|
try {
|
|
29
|
-
const
|
|
30
|
-
const
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
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
|
|
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:
|
|
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 =
|
|
145
|
-
const
|
|
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 (
|
|
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-
|
|
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 =
|
|
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 <
|
|
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,
|
|
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 =
|
|
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 <
|
|
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,
|
|
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
|
-
|
|
439
|
-
|
|
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
|
|
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
|
|
466
|
-
rl
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
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
|
|
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
|
|
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
|
|
28
|
+
function listInstalledSkills() {
|
|
29
|
+
ensureKiluaDir();
|
|
30
|
+
const skills = [];
|
|
28
31
|
try {
|
|
29
|
-
const
|
|
30
|
-
const
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
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
|
|
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:
|
|
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 =
|
|
145
|
-
const
|
|
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 (
|
|
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-
|
|
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 =
|
|
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 <
|
|
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,
|
|
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 =
|
|
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 <
|
|
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,
|
|
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
|
-
|
|
439
|
-
|
|
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
|
|
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
|
|
466
|
-
rl
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
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);
|