@gilangranggaid/kilua-agent 1.0.4 → 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 +584 -0
- package/bin/kilua.mjs +151 -46
- package/package.json +4 -4
package/bin/kilua.js
ADDED
|
@@ -0,0 +1,584 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import readline from "node:readline";
|
|
3
|
+
import fs from "node:fs";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import os from "node:os";
|
|
6
|
+
import { execSync } from "node:child_process";
|
|
7
|
+
|
|
8
|
+
// ponytail: autonomous single-file AI Coding Agent CLI with slash commands, local skills & test execution.
|
|
9
|
+
const KILUA_DIR = path.join(os.homedir(), ".kilua");
|
|
10
|
+
const SETTINGS_PATH = path.join(KILUA_DIR, "settings.json");
|
|
11
|
+
const SKILLS_DIR = path.join(KILUA_DIR, "skills");
|
|
12
|
+
|
|
13
|
+
function ensureKiluaDir() {
|
|
14
|
+
if (!fs.existsSync(KILUA_DIR)) fs.mkdirSync(KILUA_DIR, { recursive: true });
|
|
15
|
+
if (!fs.existsSync(SKILLS_DIR)) fs.mkdirSync(SKILLS_DIR, { recursive: true });
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function loadSettings() {
|
|
19
|
+
ensureKiluaDir();
|
|
20
|
+
try { return JSON.parse(fs.readFileSync(SETTINGS_PATH, "utf8")); } catch { return {}; }
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function saveSettings(settings) {
|
|
24
|
+
ensureKiluaDir();
|
|
25
|
+
fs.writeFileSync(SETTINGS_PATH, JSON.stringify(settings, null, 2), "utf8");
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function listInstalledSkills() {
|
|
29
|
+
ensureKiluaDir();
|
|
30
|
+
const skills = [];
|
|
31
|
+
try {
|
|
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
|
+
}
|
|
43
|
+
}
|
|
44
|
+
} catch {}
|
|
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;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const TOOLS_OPENAI = [
|
|
59
|
+
{
|
|
60
|
+
type: "function",
|
|
61
|
+
function: {
|
|
62
|
+
name: "read_file",
|
|
63
|
+
description: "Membaca isi file lokal.",
|
|
64
|
+
parameters: { type: "object", properties: { path: { type: "string" } }, required: ["path"] }
|
|
65
|
+
}
|
|
66
|
+
},
|
|
67
|
+
{
|
|
68
|
+
type: "function",
|
|
69
|
+
function: {
|
|
70
|
+
name: "write_file",
|
|
71
|
+
description: "Menulis atau membuat file lokal.",
|
|
72
|
+
parameters: { type: "object", properties: { path: { type: "string" }, content: { type: "string" } }, required: ["path", "content"] }
|
|
73
|
+
}
|
|
74
|
+
},
|
|
75
|
+
{
|
|
76
|
+
type: "function",
|
|
77
|
+
function: {
|
|
78
|
+
name: "edit_file",
|
|
79
|
+
description: "Mengganti teks persis di file lokal.",
|
|
80
|
+
parameters: { type: "object", properties: { path: { type: "string" }, old_string: { type: "string" }, new_string: { type: "string" } }, required: ["path", "old_string", "new_string"] }
|
|
81
|
+
}
|
|
82
|
+
},
|
|
83
|
+
{
|
|
84
|
+
type: "function",
|
|
85
|
+
function: {
|
|
86
|
+
name: "list_dir",
|
|
87
|
+
description: "Melihat daftar file di folder.",
|
|
88
|
+
parameters: { type: "object", properties: { path: { type: "string" } } }
|
|
89
|
+
}
|
|
90
|
+
},
|
|
91
|
+
{
|
|
92
|
+
type: "function",
|
|
93
|
+
function: {
|
|
94
|
+
name: "run_cmd",
|
|
95
|
+
description: "Menjalankan perintah terminal lokal untuk instalasi dependencies, test, build, atau verifikasi project.",
|
|
96
|
+
parameters: { type: "object", properties: { command: { type: "string" } }, required: ["command"] }
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
];
|
|
100
|
+
|
|
101
|
+
const TOOLS_ANTHROPIC = TOOLS_OPENAI.map(t => ({
|
|
102
|
+
name: t.function.name,
|
|
103
|
+
description: t.function.description,
|
|
104
|
+
input_schema: t.function.parameters
|
|
105
|
+
}));
|
|
106
|
+
|
|
107
|
+
function executeLocalTool(name, args) {
|
|
108
|
+
const cwd = process.cwd();
|
|
109
|
+
try {
|
|
110
|
+
if (name === "read_file") {
|
|
111
|
+
const full = path.resolve(cwd, args.path);
|
|
112
|
+
return fs.existsSync(full) ? fs.readFileSync(full, "utf8").slice(0, 40000) : `Error: ${args.path} not found.`;
|
|
113
|
+
}
|
|
114
|
+
if (name === "write_file") {
|
|
115
|
+
const full = path.resolve(cwd, args.path);
|
|
116
|
+
fs.mkdirSync(path.dirname(full), { recursive: true });
|
|
117
|
+
fs.writeFileSync(full, args.content, "utf8");
|
|
118
|
+
return `✓ Written: ${args.path}`;
|
|
119
|
+
}
|
|
120
|
+
if (name === "edit_file") {
|
|
121
|
+
const full = path.resolve(cwd, args.path);
|
|
122
|
+
if (!fs.existsSync(full)) return `Error: ${args.path} not found.`;
|
|
123
|
+
const src = fs.readFileSync(full, "utf8");
|
|
124
|
+
if (!src.includes(args.old_string)) return `Error: old_string not matched in ${args.path}.`;
|
|
125
|
+
fs.writeFileSync(full, src.replace(args.old_string, args.new_string), "utf8");
|
|
126
|
+
return `✓ Edited: ${args.path}`;
|
|
127
|
+
}
|
|
128
|
+
if (name === "list_dir") {
|
|
129
|
+
const target = path.resolve(cwd, args.path || ".");
|
|
130
|
+
return fs.readdirSync(target, { withFileTypes: true }).map(it => `${it.isDirectory() ? "[DIR]" : "[FILE]"} ${it.name}`).join("\n");
|
|
131
|
+
}
|
|
132
|
+
if (name === "run_cmd") {
|
|
133
|
+
const out = execSync(args.command, { cwd, encoding: "utf8", timeout: 120000, stdio: ["pipe", "pipe", "pipe"] });
|
|
134
|
+
return out.trim() || "(Done with exit code 0)";
|
|
135
|
+
}
|
|
136
|
+
} catch (err) {
|
|
137
|
+
return `Error: ${err.stderr || err.message}`;
|
|
138
|
+
}
|
|
139
|
+
return `Error: Unknown tool ${name}`;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function promptQuestion(rl, query) {
|
|
143
|
+
return new Promise(res => rl.question(query, res));
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
async function runInteractiveConfig() {
|
|
147
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
148
|
+
|
|
149
|
+
console.log("\x1b[35m=== KILUA CONFIG ===\x1b[0m\nPath: " + SETTINGS_PATH);
|
|
150
|
+
console.log("1) 9Router Local / Claude Code Proxy (Port 20128)");
|
|
151
|
+
console.log("2) TokenHarbor / 9Router / OpenRouter (OpenAI-compatible)");
|
|
152
|
+
console.log("3) Google Gemini (AI Studio Key)\n");
|
|
153
|
+
|
|
154
|
+
const choice = (await promptQuestion(rl, "Pilihan [1/2/3] (default 1): ")).trim() || "1";
|
|
155
|
+
let settings = {};
|
|
156
|
+
|
|
157
|
+
if (choice === "1") {
|
|
158
|
+
const defaultUrl = "http://127.0.0.1:20128/v1";
|
|
159
|
+
const defaultModel = "harobor";
|
|
160
|
+
|
|
161
|
+
const baseUrl = (await promptQuestion(rl, `Base URL (${defaultUrl}): `)).trim() || defaultUrl;
|
|
162
|
+
const key = (await promptQuestion(rl, `API Key/Token (kosongkan jika tidak perlu): `)).trim();
|
|
163
|
+
const model = (await promptQuestion(rl, `Model (${defaultModel}): `)).trim() || defaultModel;
|
|
164
|
+
|
|
165
|
+
settings = {
|
|
166
|
+
provider: "anthropic",
|
|
167
|
+
base_url: baseUrl.replace(/\/+$/, ""),
|
|
168
|
+
api_key: key,
|
|
169
|
+
model
|
|
170
|
+
};
|
|
171
|
+
} else if (choice === "3") {
|
|
172
|
+
const key = (await promptQuestion(rl, "Masukkan GEMINI_API_KEY: ")).trim();
|
|
173
|
+
const model = (await promptQuestion(rl, "Nama Model (gemini-3.5-flash-lite): ")).trim() || "gemini-3.5-flash-lite";
|
|
174
|
+
settings = {
|
|
175
|
+
provider: "openai",
|
|
176
|
+
base_url: "https://generativelanguage.googleapis.com/v1beta/openai",
|
|
177
|
+
api_key: key,
|
|
178
|
+
model
|
|
179
|
+
};
|
|
180
|
+
} else {
|
|
181
|
+
console.log("Contoh Base URL: https://tokenharbor.ai/v1");
|
|
182
|
+
const baseUrl = (await promptQuestion(rl, "Base URL (https://tokenharbor.ai/v1): ")).trim() || "https://tokenharbor.ai/v1";
|
|
183
|
+
const key = (await promptQuestion(rl, "API Key: ")).trim();
|
|
184
|
+
const model = (await promptQuestion(rl, "Model (deepseek-v4.1-flash:free): ")).trim() || "deepseek-v4.1-flash:free";
|
|
185
|
+
settings = {
|
|
186
|
+
provider: "openai",
|
|
187
|
+
api_key: key,
|
|
188
|
+
base_url: baseUrl.replace(/\/+$/, ""),
|
|
189
|
+
model
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
saveSettings(settings);
|
|
194
|
+
rl.close();
|
|
195
|
+
console.log("\x1b[32m✓ Pengaturan disimpan di " + SETTINGS_PATH + "\x1b[0m\n");
|
|
196
|
+
return settings;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// Stream reader universal untuk SSE (Anthropic & 9router)
|
|
200
|
+
async function readSseStream(res, onChunk) {
|
|
201
|
+
const reader = res.body.getReader();
|
|
202
|
+
const decoder = new TextDecoder("utf-8");
|
|
203
|
+
let buffer = "";
|
|
204
|
+
|
|
205
|
+
while (true) {
|
|
206
|
+
const { done, value } = await reader.read();
|
|
207
|
+
if (done) break;
|
|
208
|
+
buffer += decoder.decode(value, { stream: true });
|
|
209
|
+
const lines = buffer.split("\n");
|
|
210
|
+
buffer = lines.pop() || "";
|
|
211
|
+
|
|
212
|
+
for (const rawLine of lines) {
|
|
213
|
+
const line = rawLine.trim();
|
|
214
|
+
if (!line || line.startsWith("event:") || line.startsWith(":")) continue;
|
|
215
|
+
if (line.startsWith("data:")) {
|
|
216
|
+
const jsonStr = line.slice(5).trim();
|
|
217
|
+
if (!jsonStr || jsonStr === "[DONE]") continue;
|
|
218
|
+
try {
|
|
219
|
+
const parsed = JSON.parse(jsonStr);
|
|
220
|
+
onChunk(parsed);
|
|
221
|
+
} catch {}
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
if (buffer.trim().startsWith("data:")) {
|
|
227
|
+
try {
|
|
228
|
+
const parsed = JSON.parse(buffer.trim().slice(5).trim());
|
|
229
|
+
onChunk(parsed);
|
|
230
|
+
} catch {}
|
|
231
|
+
}
|
|
232
|
+
}
|
|
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
|
+
|
|
250
|
+
// Loop untuk Anthropic / 9router local (/v1/messages)
|
|
251
|
+
async function runAnthropicLoop(settings, convo) {
|
|
252
|
+
const SYSTEM = buildSystemPrompt();
|
|
253
|
+
|
|
254
|
+
let endpoint = settings.base_url.replace(/\/+$/, "");
|
|
255
|
+
if (!endpoint.endsWith("/messages")) endpoint += "/messages";
|
|
256
|
+
|
|
257
|
+
for (let step = 0; step < 16; step++) {
|
|
258
|
+
process.stdout.write("\r\x1b[2K\x1b[35m[KILUA Berpikir...]\x1b[0m ");
|
|
259
|
+
let res;
|
|
260
|
+
try {
|
|
261
|
+
res = await fetch(endpoint, {
|
|
262
|
+
method: "POST",
|
|
263
|
+
headers: {
|
|
264
|
+
"Content-Type": "application/json",
|
|
265
|
+
"x-api-key": settings.api_key,
|
|
266
|
+
Authorization: `Bearer ${settings.api_key}`,
|
|
267
|
+
"anthropic-version": "2023-06-01"
|
|
268
|
+
},
|
|
269
|
+
body: JSON.stringify({
|
|
270
|
+
model: settings.model,
|
|
271
|
+
max_tokens: 4096,
|
|
272
|
+
stream: true,
|
|
273
|
+
system: SYSTEM,
|
|
274
|
+
messages: convo,
|
|
275
|
+
tools: TOOLS_ANTHROPIC
|
|
276
|
+
})
|
|
277
|
+
});
|
|
278
|
+
} catch (e) {
|
|
279
|
+
console.error("\n[!] Connection error:", e.message);
|
|
280
|
+
return;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
if (!res.ok) {
|
|
284
|
+
const errText = await res.text().catch(() => "");
|
|
285
|
+
console.error(`\n[!] Error (${res.status}):`, errText.slice(0, 200));
|
|
286
|
+
return;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
let printedHeader = false;
|
|
290
|
+
let fullText = "";
|
|
291
|
+
const toolBlocks = [];
|
|
292
|
+
|
|
293
|
+
await readSseStream(res, (evt) => {
|
|
294
|
+
let deltaText = "";
|
|
295
|
+
if (typeof evt.delta?.text === "string") deltaText = evt.delta.text;
|
|
296
|
+
else if (typeof evt.delta?.content === "string") deltaText = evt.delta.content;
|
|
297
|
+
else if (evt.choices?.[0]?.delta?.content) deltaText = evt.choices[0].delta.content;
|
|
298
|
+
else if (evt.content_block?.text) deltaText = evt.content_block.text;
|
|
299
|
+
|
|
300
|
+
if (deltaText) {
|
|
301
|
+
if (!printedHeader) {
|
|
302
|
+
process.stdout.write("\r\x1b[2KKILUA > ");
|
|
303
|
+
printedHeader = true;
|
|
304
|
+
}
|
|
305
|
+
process.stdout.write(deltaText);
|
|
306
|
+
fullText += deltaText;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
if (evt.type === "content_block_start" && evt.content_block?.type === "tool_use") {
|
|
310
|
+
toolBlocks[evt.index] = { ...evt.content_block, _raw_json: "" };
|
|
311
|
+
} else if (evt.delta?.type === "input_json_delta") {
|
|
312
|
+
const idx = evt.index;
|
|
313
|
+
if (toolBlocks[idx]) {
|
|
314
|
+
toolBlocks[idx]._raw_json = (toolBlocks[idx]._raw_json || "") + (evt.delta.partial_json || "");
|
|
315
|
+
}
|
|
316
|
+
} else if (evt.type === "content_block_stop") {
|
|
317
|
+
const idx = evt.index;
|
|
318
|
+
if (toolBlocks[idx]?._raw_json) {
|
|
319
|
+
try { toolBlocks[idx].input = JSON.parse(toolBlocks[idx]._raw_json); } catch {}
|
|
320
|
+
delete toolBlocks[idx]._raw_json;
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
if (printedHeader) console.log();
|
|
326
|
+
else process.stdout.write("\r\x1b[2K");
|
|
327
|
+
|
|
328
|
+
const activeTools = toolBlocks.filter(Boolean);
|
|
329
|
+
const assistantBlocks = [];
|
|
330
|
+
if (fullText) assistantBlocks.push({ type: "text", text: fullText });
|
|
331
|
+
for (const tb of activeTools) assistantBlocks.push(tb);
|
|
332
|
+
|
|
333
|
+
convo.push({ role: "assistant", content: assistantBlocks.length ? assistantBlocks : fullText });
|
|
334
|
+
|
|
335
|
+
if (!activeTools.length) break;
|
|
336
|
+
|
|
337
|
+
const toolResultContent = [];
|
|
338
|
+
for (const tu of activeTools) {
|
|
339
|
+
const name = tu.name;
|
|
340
|
+
const args = tu.input || {};
|
|
341
|
+
console.log(`\x1b[36m⚡ Tool: ${name}\x1b[0m(${JSON.stringify(args)})`);
|
|
342
|
+
const output = executeLocalTool(name, args);
|
|
343
|
+
console.log(`\x1b[90m↳ ${output.slice(0, 160).replace(/\n/g, " ")}${output.length > 160 ? "..." : ""}\x1b[0m`);
|
|
344
|
+
toolResultContent.push({
|
|
345
|
+
type: "tool_result",
|
|
346
|
+
tool_use_id: tu.id,
|
|
347
|
+
content: output
|
|
348
|
+
});
|
|
349
|
+
}
|
|
350
|
+
convo.push({ role: "user", content: toolResultContent });
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
// Loop untuk OpenAI / TokenHarbor / Gemini OpenAI protocol
|
|
355
|
+
async function runOpenAiLoop(settings, convo) {
|
|
356
|
+
const SYSTEM = buildSystemPrompt();
|
|
357
|
+
|
|
358
|
+
let endpoint = settings.base_url.replace(/\/+$/, "");
|
|
359
|
+
if (endpoint.includes("generativelanguage.googleapis.com") && !endpoint.includes("/openai")) {
|
|
360
|
+
endpoint += "/openai";
|
|
361
|
+
}
|
|
362
|
+
if (!endpoint.endsWith("/chat/completions")) {
|
|
363
|
+
endpoint += "/chat/completions";
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
for (let step = 0; step < 16; step++) {
|
|
367
|
+
process.stdout.write("\r\x1b[2K\x1b[35m[KILUA Berpikir...]\x1b[0m ");
|
|
368
|
+
let res;
|
|
369
|
+
try {
|
|
370
|
+
res = await fetch(endpoint, {
|
|
371
|
+
method: "POST",
|
|
372
|
+
headers: {
|
|
373
|
+
"Content-Type": "application/json",
|
|
374
|
+
Authorization: `Bearer ${settings.api_key}`,
|
|
375
|
+
"x-goog-api-key": settings.api_key
|
|
376
|
+
},
|
|
377
|
+
body: JSON.stringify({
|
|
378
|
+
model: settings.model,
|
|
379
|
+
stream: true,
|
|
380
|
+
messages: [{ role: "system", content: SYSTEM }, ...convo],
|
|
381
|
+
tools: TOOLS_OPENAI,
|
|
382
|
+
tool_choice: "auto"
|
|
383
|
+
})
|
|
384
|
+
});
|
|
385
|
+
} catch (e) {
|
|
386
|
+
console.error("\n[!] Connection error:", e.message);
|
|
387
|
+
return;
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
if (!res.ok) {
|
|
391
|
+
const errText = await res.text().catch(() => "");
|
|
392
|
+
console.error(`\n[!] Error (${res.status}):`, errText.slice(0, 200));
|
|
393
|
+
return;
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
let printedHeader = false;
|
|
397
|
+
let fullText = "";
|
|
398
|
+
const toolCallsMap = {};
|
|
399
|
+
|
|
400
|
+
await readSseStream(res, (evt) => {
|
|
401
|
+
const choice = evt.choices?.[0];
|
|
402
|
+
const delta = choice?.delta;
|
|
403
|
+
if (delta?.content) {
|
|
404
|
+
if (!printedHeader) {
|
|
405
|
+
process.stdout.write("\r\x1b[2KKILUA > ");
|
|
406
|
+
printedHeader = true;
|
|
407
|
+
}
|
|
408
|
+
process.stdout.write(delta.content);
|
|
409
|
+
fullText += delta.content;
|
|
410
|
+
}
|
|
411
|
+
if (delta?.tool_calls) {
|
|
412
|
+
for (const tc of delta.tool_calls) {
|
|
413
|
+
const idx = tc.index ?? 0;
|
|
414
|
+
if (!toolCallsMap[idx]) toolCallsMap[idx] = { id: tc.id, function: { name: tc.function?.name || "", arguments: "" } };
|
|
415
|
+
if (tc.function?.name) toolCallsMap[idx].function.name = tc.function.name;
|
|
416
|
+
if (tc.function?.arguments) toolCallsMap[idx].function.arguments += tc.function.arguments;
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
});
|
|
420
|
+
|
|
421
|
+
if (printedHeader) console.log();
|
|
422
|
+
else process.stdout.write("\r\x1b[2K");
|
|
423
|
+
|
|
424
|
+
const toolCalls = Object.values(toolCallsMap);
|
|
425
|
+
convo.push({ role: "assistant", content: fullText, tool_calls: toolCalls.length ? toolCalls : undefined });
|
|
426
|
+
|
|
427
|
+
if (!toolCalls.length) break;
|
|
428
|
+
|
|
429
|
+
for (const tc of toolCalls) {
|
|
430
|
+
const name = tc.function.name;
|
|
431
|
+
let args = {};
|
|
432
|
+
try { args = JSON.parse(tc.function.arguments || "{}"); } catch {}
|
|
433
|
+
console.log(`\x1b[36m⚡ Tool: ${name}\x1b[0m(${JSON.stringify(args)})`);
|
|
434
|
+
const output = executeLocalTool(name, args);
|
|
435
|
+
console.log(`\x1b[90m↳ ${output.slice(0, 160).replace(/\n/g, " ")}${output.length > 160 ? "..." : ""}\x1b[0m`);
|
|
436
|
+
convo.push({ role: "tool", tool_call_id: tc.id, name, content: output });
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
async function runAgent(settings, userPrompt, convo) {
|
|
442
|
+
const isAnthropic = settings.provider === "anthropic" || settings.base_url.includes(":20128") || settings.model === "harobor";
|
|
443
|
+
if (isAnthropic) {
|
|
444
|
+
convo.push({ role: "user", content: userPrompt });
|
|
445
|
+
await runAnthropicLoop(settings, convo);
|
|
446
|
+
} else {
|
|
447
|
+
convo.push({ role: "user", content: userPrompt });
|
|
448
|
+
await runOpenAiLoop(settings, convo);
|
|
449
|
+
}
|
|
450
|
+
}
|
|
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
|
+
|
|
537
|
+
async function main() {
|
|
538
|
+
const args = process.argv.slice(2);
|
|
539
|
+
const oneShot = args.join(" ").trim();
|
|
540
|
+
|
|
541
|
+
if (args[0] === "config" || args[0] === "setup") {
|
|
542
|
+
await runInteractiveConfig();
|
|
543
|
+
process.exit(0);
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
let settings = loadSettings();
|
|
547
|
+
|
|
548
|
+
if (!settings.api_key) {
|
|
549
|
+
console.log("\x1b[33m[!] Belum ada konfigurasi AI key.\x1b[0m");
|
|
550
|
+
settings = await runInteractiveConfig();
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
const convo = [];
|
|
554
|
+
if (oneShot) {
|
|
555
|
+
await runAgent(settings, oneShot, convo);
|
|
556
|
+
process.exit(0);
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
console.log("\x1b[1m\x1b[35m=== KILUA AI CLI AGENT ===\x1b[0m");
|
|
560
|
+
console.log(`CWD : \x1b[33m${process.cwd()}\x1b[0m`);
|
|
561
|
+
console.log(`Provider: \x1b[32m${settings.base_url}\x1b[0m (${settings.model})`);
|
|
562
|
+
console.log(`Config : \x1b[90m${SETTINGS_PATH}\x1b[0m (ketik \x1b[36m/help\x1b[0m untuk perintah slash)\n`);
|
|
563
|
+
|
|
564
|
+
let rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
565
|
+
const ask = () => {
|
|
566
|
+
rl.question("\x1b[1mKAMU > \x1b[0m", async input => {
|
|
567
|
+
const q = input.trim();
|
|
568
|
+
if (!q) return ask();
|
|
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
|
+
}
|
|
575
|
+
return ask();
|
|
576
|
+
}
|
|
577
|
+
await runAgent(settings, q, convo);
|
|
578
|
+
ask();
|
|
579
|
+
});
|
|
580
|
+
};
|
|
581
|
+
ask();
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
main();
|
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);
|
package/package.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gilangranggaid/kilua-agent",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.7",
|
|
4
4
|
"description": "Autonomous AI Coding Agent for your terminal and VS Code",
|
|
5
5
|
"type": "module",
|
|
6
|
-
"main": "bin/kilua.
|
|
6
|
+
"main": "bin/kilua.js",
|
|
7
7
|
"bin": {
|
|
8
|
-
"kilua": "./bin/kilua.
|
|
9
|
-
"kilua-agent": "./bin/kilua.
|
|
8
|
+
"kilua": "./bin/kilua.js",
|
|
9
|
+
"kilua-agent": "./bin/kilua.js"
|
|
10
10
|
},
|
|
11
11
|
"files": [
|
|
12
12
|
"bin",
|