@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.
- package/bin/kilua.js +147 -53
- package/bin/kilua.mjs +147 -53
- 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, 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:
|
|
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 /
|
|
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 =
|
|
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 (
|
|
150
|
-
const model = (await promptQuestion(rl, `Model (
|
|
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-
|
|
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
|
|
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 =
|
|
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,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
|
-
//
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
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
|
|
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
|
|
466
|
-
rl
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
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
|
|
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, 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:
|
|
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 /
|
|
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 =
|
|
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 (
|
|
150
|
-
const model = (await promptQuestion(rl, `Model (
|
|
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-
|
|
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
|
|
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 =
|
|
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,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
|
-
//
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
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
|
|
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
|
|
466
|
-
rl
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
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);
|