@gilangranggaid/kilua-agent 1.1.3 → 1.3.0
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/README.md +3 -0
- package/bin/kilua.js +803 -119
- package/bin/kilua.mjs +803 -119
- package/bin/skills.json +1 -0
- package/package.json +1 -1
package/bin/kilua.mjs
CHANGED
|
@@ -4,13 +4,81 @@ import fs from "node:fs";
|
|
|
4
4
|
import path from "node:path";
|
|
5
5
|
import os from "node:os";
|
|
6
6
|
import { execSync } from "node:child_process";
|
|
7
|
+
import { fileURLToPath } from "node:url";
|
|
7
8
|
|
|
8
9
|
// ponytail: autonomous single-file AI Coding Agent CLI with slash commands, local skills & test execution.
|
|
9
10
|
const KILUA_DIR = path.join(os.homedir(), ".kilua");
|
|
10
11
|
const SETTINGS_PATH = path.join(KILUA_DIR, "settings.json");
|
|
11
12
|
const SKILLS_DIR = path.join(KILUA_DIR, "skills");
|
|
12
13
|
const MAX_STEPS = 40; // ponytail: naik dari 16; agent butuh banyak langkah untuk self-fix loop
|
|
13
|
-
const
|
|
14
|
+
const MAX_INJECTED_SKILLS = 5; // sama seperti web: cukup 5 skill per pesan biar konteks hemat
|
|
15
|
+
let PLAN_MODE = false; // read-only mode: semua tool yang mengubah state diblokir
|
|
16
|
+
|
|
17
|
+
// ponytail: match pakai substring ternormalisasi ("docker-compose" == "docker compose").
|
|
18
|
+
// Regex mentah dari web terlalu kaku: trigger "docker-compose" tak pernah kena "docker compose".
|
|
19
|
+
const STOPWORDS = new Set(("yang dan untuk dengan pada dari ini itu saya aku kamu anda bikin buat buatkan "
|
|
20
|
+
+ "tolong cara gimana bagaimana kenapa apa apakah mana di ke the a an of for to in on "
|
|
21
|
+
+ "with and or is are how what why make create build help please my our your it this that "
|
|
22
|
+
+ "setup set up install pasang pakai pake integrasi terus trus juga bisa gak nggak udah sudah "
|
|
23
|
+
+ "dong sih nih deh ya aja sama kalo kalau biar masih mau pengen pingin file kode code project").split(/\s+/));
|
|
24
|
+
|
|
25
|
+
function normText(s) {
|
|
26
|
+
return String(s).toLowerCase().replace(/[-_/]+/g, " ").replace(/[^a-z0-9.+#\s]/g, " ")
|
|
27
|
+
.replace(/\s+/g, " ").trim();
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function tokens(s) {
|
|
31
|
+
return normText(s).split(" ").filter(w => w.length >= 3 && !STOPWORDS.has(w));
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Katalog skill bawaan (1.498, port dari web). Dibaca sekali lalu di-cache.
|
|
35
|
+
let BUILTIN_SKILLS = null;
|
|
36
|
+
function loadBuiltinSkills() {
|
|
37
|
+
if (BUILTIN_SKILLS) return BUILTIN_SKILLS;
|
|
38
|
+
try {
|
|
39
|
+
const here = path.dirname(fileURLToPath(import.meta.url));
|
|
40
|
+
// ponytail: repo root bin/ pakai file yang sama, jangan duplikat 300KB
|
|
41
|
+
const candidates = [
|
|
42
|
+
path.join(here, "skills.json"),
|
|
43
|
+
path.join(here, "..", "packages", "kilua-agent", "bin", "skills.json"),
|
|
44
|
+
];
|
|
45
|
+
const file = candidates.find(p => fs.existsSync(p));
|
|
46
|
+
const raw = JSON.parse(fs.readFileSync(file, "utf8"));
|
|
47
|
+
const skills = raw.map(([id, triggerStr, domain, focus]) => ({
|
|
48
|
+
name: id, domain, focus,
|
|
49
|
+
keys: [...new Set(String(triggerStr).split("|").flatMap(k => tokens(k)))],
|
|
50
|
+
}));
|
|
51
|
+
// IDF: token langka (mis. "stripe") lebih menentukan daripada token umum (mis. "api").
|
|
52
|
+
const df = new Map();
|
|
53
|
+
for (const s of skills) for (const k of s.keys) df.set(k, (df.get(k) || 0) + 1);
|
|
54
|
+
for (const s of skills) {
|
|
55
|
+
s.score = new Map();
|
|
56
|
+
for (const k of s.keys) s.score.set(k, Math.log(skills.length / (1 + (df.get(k) || 0))));
|
|
57
|
+
}
|
|
58
|
+
BUILTIN_SKILLS = skills;
|
|
59
|
+
} catch { BUILTIN_SKILLS = []; }
|
|
60
|
+
return BUILTIN_SKILLS;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Relevansi skill untuk satu pesan. Ambang relatif 0.55 membuang hasil nyasar.
|
|
64
|
+
function selectBuiltinSkills(userPrompt, limit = MAX_INJECTED_SKILLS) {
|
|
65
|
+
const skills = loadBuiltinSkills();
|
|
66
|
+
if (!skills.length || !userPrompt) return [];
|
|
67
|
+
const wanted = new Set(tokens(userPrompt));
|
|
68
|
+
if (!wanted.size) return [];
|
|
69
|
+
const hay = normText(userPrompt);
|
|
70
|
+
const scored = [];
|
|
71
|
+
for (const s of skills) {
|
|
72
|
+
let score = 0;
|
|
73
|
+
for (const k of s.keys) if (wanted.has(k)) score += s.score.get(k);
|
|
74
|
+
if (score > 0 && hay.includes(normText(s.name))) score += 2;
|
|
75
|
+
if (score > 0.4) scored.push({ s, score });
|
|
76
|
+
}
|
|
77
|
+
if (!scored.length) return [];
|
|
78
|
+
scored.sort((a, b) => b.score - a.score);
|
|
79
|
+
const top = scored[0].score;
|
|
80
|
+
return scored.filter(x => x.score >= top * 0.55).slice(0, limit).map(x => x.s);
|
|
81
|
+
}
|
|
14
82
|
|
|
15
83
|
function ensureKiluaDir() {
|
|
16
84
|
if (!fs.existsSync(KILUA_DIR)) fs.mkdirSync(KILUA_DIR, { recursive: true });
|
|
@@ -47,12 +115,20 @@ function listInstalledSkills() {
|
|
|
47
115
|
return skills;
|
|
48
116
|
}
|
|
49
117
|
|
|
50
|
-
function getSkillsPrompt() {
|
|
118
|
+
function getSkillsPrompt(userPrompt) {
|
|
51
119
|
const list = listInstalledSkills();
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
prompt += `- ${s.name}: ${s.desc}\n`;
|
|
120
|
+
let prompt = "";
|
|
121
|
+
if (list.length) {
|
|
122
|
+
prompt += "\n\n=== INSTALLED SKILLS ===\n";
|
|
123
|
+
for (const s of list) prompt += `- ${s.name}: ${s.desc}\n`;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// ponytail: 1.498 skill bawaan — suntik maks 5 yang paling cocok per pesan.
|
|
127
|
+
const hits = selectBuiltinSkills(userPrompt);
|
|
128
|
+
if (hits.length) {
|
|
129
|
+
prompt += `\n\n=== RELEVANT SKILLS (built-in, ${BUILTIN_SKILLS.length} total) ===\n`;
|
|
130
|
+
for (const s of hits) prompt += `- ${s.name} (${s.domain}): ${s.focus}\n`;
|
|
131
|
+
prompt += "Pakai panduan di atas kalau relevan dengan tugas ini.\n";
|
|
56
132
|
}
|
|
57
133
|
return prompt;
|
|
58
134
|
}
|
|
@@ -106,6 +182,14 @@ const TOOLS_OPENAI = [
|
|
|
106
182
|
}
|
|
107
183
|
}
|
|
108
184
|
},
|
|
185
|
+
{
|
|
186
|
+
type: "function",
|
|
187
|
+
function: {
|
|
188
|
+
name: "delete_file",
|
|
189
|
+
description: "Menghapus file lokal.",
|
|
190
|
+
parameters: { type: "object", properties: { path: { type: "string" } }, required: ["path"] }
|
|
191
|
+
}
|
|
192
|
+
},
|
|
109
193
|
{
|
|
110
194
|
type: "function",
|
|
111
195
|
function: {
|
|
@@ -124,6 +208,10 @@ const TOOLS_ANTHROPIC = TOOLS_OPENAI.map(t => ({
|
|
|
124
208
|
|
|
125
209
|
function executeLocalTool(name, args) {
|
|
126
210
|
const cwd = process.cwd();
|
|
211
|
+
// ponytail: plan mode = read-only. Blokir semua efek samping, bukan hanya tulis file.
|
|
212
|
+
if (PLAN_MODE && ["write_file", "edit_file", "delete_file", "run_cmd"].includes(name)) {
|
|
213
|
+
return "Error: Plan mode aktif — aksi yang mengubah state diblokir. Susun rencana dulu; user mematikan /plan untuk eksekusi.";
|
|
214
|
+
}
|
|
127
215
|
try {
|
|
128
216
|
if (name === "read_file") {
|
|
129
217
|
const full = path.resolve(cwd, args.path);
|
|
@@ -131,9 +219,10 @@ function executeLocalTool(name, args) {
|
|
|
131
219
|
}
|
|
132
220
|
if (name === "write_file") {
|
|
133
221
|
const full = path.resolve(cwd, args.path);
|
|
222
|
+
const existed = fs.existsSync(full);
|
|
134
223
|
fs.mkdirSync(path.dirname(full), { recursive: true });
|
|
135
224
|
fs.writeFileSync(full, args.content, "utf8");
|
|
136
|
-
return
|
|
225
|
+
return `\x1b[32m● Added\x1b[0m ${args.path}${existed ? " (overwritten)" : ` (${args.content.split("\n").length} lines)`}`;
|
|
137
226
|
}
|
|
138
227
|
if (name === "edit_file") {
|
|
139
228
|
const full = path.resolve(cwd, args.path);
|
|
@@ -141,7 +230,15 @@ function executeLocalTool(name, args) {
|
|
|
141
230
|
const src = fs.readFileSync(full, "utf8");
|
|
142
231
|
if (!src.includes(args.old_string)) return `Error: old_string not matched in ${args.path}.`;
|
|
143
232
|
fs.writeFileSync(full, src.replace(args.old_string, args.new_string), "utf8");
|
|
144
|
-
|
|
233
|
+
const added = args.new_string.split("\n").length;
|
|
234
|
+
const removed = args.old_string.split("\n").length;
|
|
235
|
+
return `\x1b[32m+${added}\x1b[0m \x1b[31m-${removed}\x1b[0m ${args.path}`;
|
|
236
|
+
}
|
|
237
|
+
if (name === "delete_file") {
|
|
238
|
+
const full = path.resolve(cwd, args.path);
|
|
239
|
+
if (!fs.existsSync(full)) return `Error: ${args.path} not found.`;
|
|
240
|
+
fs.rmSync(full, { recursive: true, force: true });
|
|
241
|
+
return `\x1b[31m✖ Deleted\x1b[0m ${args.path}`;
|
|
145
242
|
}
|
|
146
243
|
if (name === "list_dir") {
|
|
147
244
|
const target = path.resolve(cwd, args.path || ".");
|
|
@@ -181,6 +278,420 @@ function promptQuestion(rl, query) {
|
|
|
181
278
|
return new Promise(res => rl.question(query, res));
|
|
182
279
|
}
|
|
183
280
|
|
|
281
|
+
// Prompt sekali pakai untuk slash command di luar composer.
|
|
282
|
+
function promptLine(query) {
|
|
283
|
+
return new Promise(res => {
|
|
284
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
285
|
+
rl.question(query, ans => { rl.close(); res(ans); });
|
|
286
|
+
});
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
// ===================== TEMA =====================
|
|
290
|
+
const THEMES = {
|
|
291
|
+
purple: { name: "purple", accent: "\x1b[35m", sel: "\x1b[36m", dim: "\x1b[90m", ok: "\x1b[32m", warn: "\x1b[33m", err: "\x1b[31m", reset: "\x1b[0m", bold: "\x1b[1m" },
|
|
292
|
+
blue: { name: "blue", accent: "\x1b[34m", sel: "\x1b[36m", dim: "\x1b[90m", ok: "\x1b[32m", warn: "\x1b[33m", err: "\x1b[31m", reset: "\x1b[0m", bold: "\x1b[1m" },
|
|
293
|
+
green: { name: "green", accent: "\x1b[32m", sel: "\x1b[36m", dim: "\x1b[90m", ok: "\x1b[32m", warn: "\x1b[33m", err: "\x1b[31m", reset: "\x1b[0m", bold: "\x1b[1m" },
|
|
294
|
+
mono: { name: "mono", accent: "\x1b[1m", sel: "\x1b[1m", dim: "\x1b[2m", ok: "", warn: "", err: "", reset: "\x1b[0m", bold: "" },
|
|
295
|
+
};
|
|
296
|
+
function themeOf(settings) {
|
|
297
|
+
return THEMES[settings?.theme] || THEMES.purple;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
// ===================== SLASH MENU =====================
|
|
301
|
+
const SLASH_COMMANDS = [
|
|
302
|
+
{ name: "/model", desc: "Ganti model AI aktif" },
|
|
303
|
+
{ name: "/config", desc: "Ubah API key, provider, base URL" },
|
|
304
|
+
{ name: "/theme", desc: "Ganti tema warna tampilan CLI" },
|
|
305
|
+
{ name: "/skills", desc: "Lihat daftar skills terpasang" },
|
|
306
|
+
{ name: "/mcp", desc: "Kelola server MCP" },
|
|
307
|
+
{ name: "/memory", desc: "Lihat & tambah memori proyek (KILUA.md)" },
|
|
308
|
+
{ name: "/plan", desc: "Nyalakan/matikan plan mode (read-only)" },
|
|
309
|
+
{ name: "/clear", desc: "Bersihkan riwayat percakapan" },
|
|
310
|
+
{ name: "/help", desc: "Panduan perintah" },
|
|
311
|
+
{ name: "/exit", desc: "Keluar dari Kilua" },
|
|
312
|
+
];
|
|
313
|
+
const MENU_VISIBLE = 8;
|
|
314
|
+
|
|
315
|
+
// ===================== CLIPBOARD (Windows) =====================
|
|
316
|
+
function readClipboard() {
|
|
317
|
+
if (process.platform !== "win32") return null;
|
|
318
|
+
// ponytail: gambar clipboard butuh WinForms STA; platform lain = teks saja.
|
|
319
|
+
try {
|
|
320
|
+
const tmp = path.join(os.tmpdir(), `kilua-paste-${Date.now()}.png`).replace(/'/g, "''");
|
|
321
|
+
const ps = "Add-Type -AssemblyName System.Windows.Forms,System.Drawing;"
|
|
322
|
+
+ "$i=[System.Windows.Forms.Clipboard]::GetImage();"
|
|
323
|
+
+ `if($i){$i.Save('${tmp}',[System.Drawing.Imaging.ImageFormat]::Png);`
|
|
324
|
+
+ "@{type='image';path='" + tmp + "'}|ConvertTo-Json -Compress}"
|
|
325
|
+
+ "else{@{type='text';text=[System.Windows.Forms.Clipboard]::GetText()}|ConvertTo-Json -Compress}";
|
|
326
|
+
const out = execSync(`powershell -NoProfile -STA -Command "${ps.replace(/"/g, '\\"')}"`, {
|
|
327
|
+
encoding: "utf8", timeout: 10000, windowsHide: true, stdio: ["ignore", "pipe", "ignore"],
|
|
328
|
+
}).trim();
|
|
329
|
+
const j = JSON.parse(out.slice(out.indexOf("{")));
|
|
330
|
+
if (j.type === "image" && fs.existsSync(j.path)) {
|
|
331
|
+
return { type: "image", data: fs.readFileSync(j.path).toString("base64"), mediaType: "image/png" };
|
|
332
|
+
}
|
|
333
|
+
if (j.type === "text" && j.text) return { type: "text", text: j.text };
|
|
334
|
+
} catch {}
|
|
335
|
+
return null;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
// ===================== COMPOSER (raw-mode editor) =====================
|
|
339
|
+
const PASTE_AS_TOKEN_CHARS = 300;
|
|
340
|
+
const PASTE_AS_TOKEN_LINES = 3;
|
|
341
|
+
|
|
342
|
+
function promptComposer(settings, history) {
|
|
343
|
+
const t = themeOf(settings);
|
|
344
|
+
return new Promise(resolve => {
|
|
345
|
+
const stdin = process.stdin;
|
|
346
|
+
const stdout = process.stdout;
|
|
347
|
+
let items = []; // string (1 char) | {type:'paste'|'image', label, ...}
|
|
348
|
+
let pos = 0;
|
|
349
|
+
let menuSel = 0;
|
|
350
|
+
let histIdx = -1;
|
|
351
|
+
let lastLines = 0;
|
|
352
|
+
let pasteN = 0;
|
|
353
|
+
let imageN = 0;
|
|
354
|
+
let escBuf = "";
|
|
355
|
+
let inPaste = false;
|
|
356
|
+
let pasteBuf = "";
|
|
357
|
+
let probe = null;
|
|
358
|
+
let escTimer = null;
|
|
359
|
+
let closed = false;
|
|
360
|
+
|
|
361
|
+
const display = () => items.map(it => (typeof it === "string" ? it : it.label)).join("");
|
|
362
|
+
const displayText = () => items.map(it => (typeof it === "string" ? it : it.type === "paste" ? it.text : "")).join("");
|
|
363
|
+
const images = () => items.filter(it => it && it.type === "image");
|
|
364
|
+
|
|
365
|
+
function insertChars(str) {
|
|
366
|
+
for (const ch of str) { items.splice(pos, 0, ch); pos++; }
|
|
367
|
+
}
|
|
368
|
+
function insertPaste(text) {
|
|
369
|
+
const lines = text.split("\n").length;
|
|
370
|
+
if (text.length < PASTE_AS_TOKEN_CHARS && lines < PASTE_AS_TOKEN_LINES) return insertChars(text);
|
|
371
|
+
pasteN++;
|
|
372
|
+
items.splice(pos, 0, { type: "paste", label: `[Pasted text #${pasteN} +${lines} lines]`, text });
|
|
373
|
+
pos++;
|
|
374
|
+
}
|
|
375
|
+
function insertImage(cls) {
|
|
376
|
+
imageN++;
|
|
377
|
+
items.splice(pos, 0, { type: "image", label: `[Image #${imageN}]`, data: cls.data, mediaType: cls.mediaType });
|
|
378
|
+
pos++;
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
function menu() {
|
|
382
|
+
const d = display();
|
|
383
|
+
if (!d.startsWith("/") || /\s/.test(d)) return null;
|
|
384
|
+
const q = d.toLowerCase();
|
|
385
|
+
const list = SLASH_COMMANDS.filter(c => c.name.startsWith(q));
|
|
386
|
+
return list.length ? list : null;
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
function layout(plain, cols, cursorIdx) {
|
|
390
|
+
const lines = [""];
|
|
391
|
+
let caret = null;
|
|
392
|
+
for (let i = 0; i <= plain.length; i++) {
|
|
393
|
+
if (i === cursorIdx) caret = { line: lines.length - 1, col: lines[lines.length - 1].length };
|
|
394
|
+
if (i === plain.length) break;
|
|
395
|
+
const ch = plain[i];
|
|
396
|
+
if (ch === "\n") { lines.push(""); continue; }
|
|
397
|
+
if (lines[lines.length - 1].length >= cols) lines.push("");
|
|
398
|
+
lines[lines.length - 1] += ch;
|
|
399
|
+
}
|
|
400
|
+
if (!caret) caret = { line: lines.length - 1, col: lines[lines.length - 1].length };
|
|
401
|
+
if (lines[lines.length - 1].length >= cols) lines.push("");
|
|
402
|
+
return { lines, ...caret };
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
function render() {
|
|
406
|
+
const cols = Math.max(20, stdout.columns || 80);
|
|
407
|
+
const list = menu();
|
|
408
|
+
let menuLines = [];
|
|
409
|
+
if (list) {
|
|
410
|
+
const win = Math.min(MENU_VISIBLE, list.length);
|
|
411
|
+
menuSel = Math.max(0, Math.min(menuSel, list.length - 1));
|
|
412
|
+
let start = Math.max(0, Math.min(menuSel - win + 1, list.length - win));
|
|
413
|
+
const nameW = Math.max(...list.map(c => c.name.length)) + 2;
|
|
414
|
+
for (let i = start; i < start + win; i++) {
|
|
415
|
+
const c = list[i];
|
|
416
|
+
if (i === menuSel) menuLines.push(`${t.accent}❯${t.reset} ${t.sel}${c.name.padEnd(nameW)}${t.reset}${t.dim}${c.desc}${t.reset}`);
|
|
417
|
+
else menuLines.push(` ${t.dim}${c.name.padEnd(nameW)}${c.desc}${t.reset}`);
|
|
418
|
+
}
|
|
419
|
+
if (list.length > win) menuLines.push(`${t.dim} ↑↓ scroll · ${list.length - win} lainnya${t.reset}`);
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
let before = 0;
|
|
423
|
+
for (let i = 0; i < pos; i++) before += typeof items[i] === "string" ? 1 : items[i].label.length;
|
|
424
|
+
const plain = "❯ " + display();
|
|
425
|
+
const { lines, line: cLine, col: cCol } = layout(plain, cols, 2 + before);
|
|
426
|
+
const colored = lines.map((ln, idx) => {
|
|
427
|
+
const body = idx === 0 ? ln.slice(2) : ln;
|
|
428
|
+
const prefix = idx === 0 ? `${t.accent}❯ ${t.reset}` : "";
|
|
429
|
+
const painted = body
|
|
430
|
+
.replace(/\[Pasted text #\d+ \+\d+ lines\]/g, m => `\x1b[36m${m}${t.reset}`)
|
|
431
|
+
.replace(/\[Image #\d+\]/g, m => `\x1b[35m${m}${t.reset}`);
|
|
432
|
+
return prefix + painted;
|
|
433
|
+
});
|
|
434
|
+
|
|
435
|
+
if (lastLines) stdout.write(`\x1b[${lastLines - 1}A\r\x1b[0J`);
|
|
436
|
+
else stdout.write("\r\x1b[0J");
|
|
437
|
+
const block = [...menuLines, ...colored];
|
|
438
|
+
stdout.write(block.join("\n"));
|
|
439
|
+
lastLines = block.length;
|
|
440
|
+
|
|
441
|
+
const up = menuLines.length + colored.length - 1 - (menuLines.length + cLine);
|
|
442
|
+
if (up > 0) stdout.write(`\x1b[${up}A`);
|
|
443
|
+
stdout.write(`\r${cCol > 0 ? `\x1b[${cCol}C` : ""}`);
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
function restore() {
|
|
447
|
+
clearProbe();
|
|
448
|
+
clearEsc();
|
|
449
|
+
closed = true;
|
|
450
|
+
stdin.removeListener("data", onData);
|
|
451
|
+
process.removeAllListeners("SIGINT");
|
|
452
|
+
if (lastLines) stdout.write(`\x1b[${lastLines - 1}A\r\x1b[0J`);
|
|
453
|
+
lastLines = 0;
|
|
454
|
+
stdout.write("\x1b[?2004l");
|
|
455
|
+
try { stdin.setRawMode(false); } catch {}
|
|
456
|
+
stdin.pause();
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
function finish() {
|
|
460
|
+
const text = displayText().trim();
|
|
461
|
+
const imgs = images();
|
|
462
|
+
if (!text && !imgs.length) { render(); return; }
|
|
463
|
+
const summary = display();
|
|
464
|
+
restore();
|
|
465
|
+
let shown = summary.replace(/\n/g, " ⏎ ");
|
|
466
|
+
if (shown.length > 160) shown = shown.slice(0, 157) + "...";
|
|
467
|
+
stdout.write(`${t.accent}❯${t.reset} ${shown}\n`);
|
|
468
|
+
resolve({ text, images: imgs });
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
// ---- clipboard ----
|
|
472
|
+
function clearProbe() {
|
|
473
|
+
if (probe) { clearTimeout(probe); probe = null; }
|
|
474
|
+
}
|
|
475
|
+
// ESC sendirian = tutup menu; tapi ESC juga awal arrow key, jadi tunggu sebentar.
|
|
476
|
+
function armEsc() {
|
|
477
|
+
if (escTimer) return;
|
|
478
|
+
escTimer = setTimeout(() => {
|
|
479
|
+
escTimer = null;
|
|
480
|
+
escBuf = "";
|
|
481
|
+
menuSel = 0;
|
|
482
|
+
items = items.filter(it => typeof it !== "string" || it !== "\x1b");
|
|
483
|
+
render();
|
|
484
|
+
}, 30);
|
|
485
|
+
}
|
|
486
|
+
function clearEsc() {
|
|
487
|
+
if (escTimer) { clearTimeout(escTimer); escTimer = null; }
|
|
488
|
+
}
|
|
489
|
+
function tryClipboard() {
|
|
490
|
+
const clip = readClipboard();
|
|
491
|
+
if (!clip) return false;
|
|
492
|
+
if (clip.type === "image") insertImage(clip);
|
|
493
|
+
else insertPaste(clip.text);
|
|
494
|
+
return true;
|
|
495
|
+
}
|
|
496
|
+
// Ctrl+V: tunggu data bracket-paste dulu; kalau kosong, berarti gambar.
|
|
497
|
+
function ctrlV() {
|
|
498
|
+
if (probe) return;
|
|
499
|
+
probe = setTimeout(() => { probe = null; tryClipboard(); render(); }, 150);
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
// ---- key handling ----
|
|
503
|
+
function moveTo(newPos) { pos = Math.max(0, Math.min(newPos, items.length)); render(); }
|
|
504
|
+
|
|
505
|
+
function submitOrComplete() {
|
|
506
|
+
const list = menu();
|
|
507
|
+
if (list) {
|
|
508
|
+
const pick = list[Math.max(0, Math.min(menuSel, list.length - 1))];
|
|
509
|
+
items = []; pos = 0;
|
|
510
|
+
insertChars(pick.name + " ");
|
|
511
|
+
render();
|
|
512
|
+
return;
|
|
513
|
+
}
|
|
514
|
+
finish();
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
function onPlain(ch) {
|
|
518
|
+
if (ch === "\r" || ch === "\n") return submitOrComplete();
|
|
519
|
+
if (ch === "\x7f") { // backspace
|
|
520
|
+
if (pos > 0) { items.splice(pos - 1, 1); pos--; }
|
|
521
|
+
return render();
|
|
522
|
+
}
|
|
523
|
+
if (ch === "\x03" || ch === "\x04") {
|
|
524
|
+
if (!display()) {
|
|
525
|
+
restore();
|
|
526
|
+
stdout.write("\n");
|
|
527
|
+
process.exit(0);
|
|
528
|
+
}
|
|
529
|
+
items = []; pos = 0; menuSel = 0;
|
|
530
|
+
return render();
|
|
531
|
+
}
|
|
532
|
+
if (ch === "\x16") return ctrlV(); // ctrl+v
|
|
533
|
+
if (ch === "\x1bv" || ch === "\x1bV") { // alt+v
|
|
534
|
+
if (!tryClipboard()) stdout.write("\x07");
|
|
535
|
+
return render();
|
|
536
|
+
}
|
|
537
|
+
if (ch === "\t") {
|
|
538
|
+
const list = menu();
|
|
539
|
+
if (list) { items = []; pos = 0; insertChars(list[menuSel].name + " "); }
|
|
540
|
+
return render();
|
|
541
|
+
}
|
|
542
|
+
if (ch === "\x1b") return render(); // esc: tutup menu via reset
|
|
543
|
+
if (ch < " " && ch !== "\n") return;
|
|
544
|
+
insertChars(ch);
|
|
545
|
+
menuSel = 0;
|
|
546
|
+
render();
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
function onKey(seq) {
|
|
550
|
+
switch (seq) {
|
|
551
|
+
case "\x1b[A": if (menu()) { menuSel--; } else { navHistory(-1); } return render();
|
|
552
|
+
case "\x1b[B": if (menu()) { menuSel++; } else { navHistory(1); } return render();
|
|
553
|
+
case "\x1b[C": return moveTo(pos + 1);
|
|
554
|
+
case "\x1b[D": return moveTo(pos - 1);
|
|
555
|
+
case "\x1b[H": return moveTo(0);
|
|
556
|
+
case "\x1b[F": return moveTo(items.length);
|
|
557
|
+
case "\x1b[3~": if (pos < items.length) { items.splice(pos, 1); } return render();
|
|
558
|
+
case "\x1b\r": case "\x1b\n": items.splice(pos, 0, "\n"); pos++; return render();
|
|
559
|
+
default: return render();
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
function navHistory(dir) {
|
|
564
|
+
if (!history.length) return;
|
|
565
|
+
histIdx = Math.max(-1, Math.min(history.length - 1, histIdx - dir));
|
|
566
|
+
items = []; pos = 0;
|
|
567
|
+
if (histIdx >= 0) insertChars(history[history.length - 1 - histIdx]);
|
|
568
|
+
else pos = 0;
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
function feed(str) {
|
|
572
|
+
escBuf += str;
|
|
573
|
+
while (escBuf.length) {
|
|
574
|
+
if (inPaste) {
|
|
575
|
+
const end = escBuf.indexOf("\x1b[201~");
|
|
576
|
+
if (end === -1) { pasteBuf += escBuf; escBuf = ""; return; }
|
|
577
|
+
pasteBuf += escBuf.slice(0, end);
|
|
578
|
+
escBuf = escBuf.slice(end + 6);
|
|
579
|
+
inPaste = false;
|
|
580
|
+
insertPaste(pasteBuf.replace(/\r\n?/g, "\n"));
|
|
581
|
+
pasteBuf = "";
|
|
582
|
+
menuSel = 0;
|
|
583
|
+
render();
|
|
584
|
+
continue;
|
|
585
|
+
}
|
|
586
|
+
const pStart = escBuf.indexOf("\x1b[200~");
|
|
587
|
+
if (pStart > 0) {
|
|
588
|
+
const head = escBuf.slice(0, pStart);
|
|
589
|
+
escBuf = escBuf.slice(pStart);
|
|
590
|
+
for (const ch of head) onPlain(ch);
|
|
591
|
+
continue;
|
|
592
|
+
}
|
|
593
|
+
if (pStart === 0) { escBuf = escBuf.slice(6); inPaste = true; continue; }
|
|
594
|
+
|
|
595
|
+
if (escBuf.startsWith("\x1b")) {
|
|
596
|
+
const seqs = ["\x1b[A", "\x1b[B", "\x1b[C", "\x1b[D", "\x1b[H", "\x1b[F", "\x1b[3~", "\x1b[Z", "\x1b\r", "\x1b\n", "\x1bv", "\x1bV"];
|
|
597
|
+
const hit = seqs.find(s => escBuf.startsWith(s));
|
|
598
|
+
if (hit) { clearEsc(); escBuf = escBuf.slice(hit.length); onKey(hit); continue; }
|
|
599
|
+
// sequence CSI lengkap tapi tidak dikenal (mis. Ctrl+↑ = "\x1b[1;5A") — buang utuh, jangan ketik sebagai teks
|
|
600
|
+
const m = escBuf.match(/^\x1b\[[0-9;]*[A-Za-z~]/);
|
|
601
|
+
if (m) { clearEsc(); escBuf = escBuf.slice(m[0].length); render(); continue; }
|
|
602
|
+
if (escBuf.length < 8) { armEsc(); return; } // tunggu sisa byte sequence
|
|
603
|
+
clearEsc();
|
|
604
|
+
escBuf = escBuf.slice(1);
|
|
605
|
+
render();
|
|
606
|
+
continue;
|
|
607
|
+
}
|
|
608
|
+
const ch = escBuf[0];
|
|
609
|
+
escBuf = escBuf.slice(1);
|
|
610
|
+
onPlain(ch);
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
function onData(chunk) {
|
|
615
|
+
if (closed) return;
|
|
616
|
+
clearProbe();
|
|
617
|
+
feed(chunk.toString("utf8"));
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
// ---- start ----
|
|
621
|
+
const onSigint = () => {
|
|
622
|
+
if (!closed) { restore(); stdout.write("\n"); process.exit(0); }
|
|
623
|
+
};
|
|
624
|
+
stdout.write("\x1b[?2004h"); // bracketed paste
|
|
625
|
+
try { stdin.setRawMode(true); } catch {} // pipa/non-TTY: lanjut saja, input tetap terbaca
|
|
626
|
+
stdin.resume();
|
|
627
|
+
stdin.setEncoding("utf8");
|
|
628
|
+
stdin.on("data", onData);
|
|
629
|
+
process.on("SIGINT", onSigint);
|
|
630
|
+
render();
|
|
631
|
+
});
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
// ponytail: status spinner minimal tanpa dependency — tampilkan langkah + detik berjalan
|
|
635
|
+
let spinnerTimer = null;
|
|
636
|
+
let spinnerStart = 0;
|
|
637
|
+
const SPINNER_FRAMES = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
|
|
638
|
+
|
|
639
|
+
function startStatus(label) {
|
|
640
|
+
stopStatus();
|
|
641
|
+
spinnerStart = Date.now();
|
|
642
|
+
let i = 0;
|
|
643
|
+
const paint = () => {
|
|
644
|
+
const secs = Math.floor((Date.now() - spinnerStart) / 1000);
|
|
645
|
+
const frame = SPINNER_FRAMES[i++ % SPINNER_FRAMES.length];
|
|
646
|
+
process.stdout.write(`\r\x1b[2K\x1b[35m${frame} KILUA\x1b[0m \x1b[90m${label}\x1b[0m \x1b[90m(${secs}s)\x1b[0m`);
|
|
647
|
+
};
|
|
648
|
+
paint();
|
|
649
|
+
spinnerTimer = setInterval(paint, 120);
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
function stopStatus() {
|
|
653
|
+
if (spinnerTimer) {
|
|
654
|
+
clearInterval(spinnerTimer);
|
|
655
|
+
spinnerTimer = null;
|
|
656
|
+
process.stdout.write("\r\x1b[2K");
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
function stepLabel(step, max) {
|
|
661
|
+
return step === 0 ? "Thinking…" : `Running step ${step}/${max}…`;
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
// ponytail: file ops tampil berwarna (hijau tambah, merah hapus), lainnya ringkas abu-abu
|
|
665
|
+
const FILE_OPS = new Set(["write_file", "edit_file", "delete_file"]);
|
|
666
|
+
|
|
667
|
+
function printToolCall(name, args, output) {
|
|
668
|
+
if (FILE_OPS.has(name)) {
|
|
669
|
+
// output sudah berisi ANSI sendiri, jangan dibungkus abu-abu
|
|
670
|
+
console.log(` ${output}`);
|
|
671
|
+
return;
|
|
672
|
+
}
|
|
673
|
+
const brief = String(output).slice(0, 160).replace(/\n/g, " ");
|
|
674
|
+
console.log(`\x1b[36m⚡ ${name}\x1b[0m \x1b[90m${brief}${String(output).length > 160 ? "..." : ""}\x1b[0m`);
|
|
675
|
+
}
|
|
676
|
+
function fmtTokens(n) {
|
|
677
|
+
return n >= 1000 ? `${(n / 1000).toFixed(1)}k` : String(n);
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
function printTokenLine(usage, startedAt) {
|
|
681
|
+
const secs = ((Date.now() - startedAt) / 1000).toFixed(1);
|
|
682
|
+
if (!usage) {
|
|
683
|
+
console.log(`\x1b[90m⏱ ${secs}s · tokens not reported by provider\x1b[0m`);
|
|
684
|
+
return;
|
|
685
|
+
}
|
|
686
|
+
const inTok = usage.input_tokens ?? usage.prompt_tokens ?? 0;
|
|
687
|
+
const outTok = usage.output_tokens ?? usage.completion_tokens ?? 0;
|
|
688
|
+
const total = usage.total_tokens ?? inTok + outTok;
|
|
689
|
+
let line = `\x1b[90m⏱ ${secs}s · ↑ ${fmtTokens(inTok)} in · ↓ ${fmtTokens(outTok)} out`;
|
|
690
|
+
if (usage.cache_read_input_tokens) line += ` · ⚡ ${fmtTokens(usage.cache_read_input_tokens)} cached`;
|
|
691
|
+
line += ` · Σ ${fmtTokens(total)}\x1b[0m`;
|
|
692
|
+
console.log(line);
|
|
693
|
+
}
|
|
694
|
+
|
|
184
695
|
async function runInteractiveConfig() {
|
|
185
696
|
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
186
697
|
|
|
@@ -267,8 +778,33 @@ async function readSseStream(res, onChunk) {
|
|
|
267
778
|
}
|
|
268
779
|
}
|
|
269
780
|
|
|
270
|
-
function
|
|
271
|
-
|
|
781
|
+
function readMemory() {
|
|
782
|
+
try {
|
|
783
|
+
const f = path.join(process.cwd(), "KILUA.md");
|
|
784
|
+
return fs.existsSync(f) ? fs.readFileSync(f, "utf8").slice(0, 4000) : "";
|
|
785
|
+
} catch { return ""; }
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
function buildSystemPrompt(convo, settings = {}) {
|
|
789
|
+
let lastUser = "";
|
|
790
|
+
for (let i = (convo?.length ?? 0) - 1; i >= 0; i--) {
|
|
791
|
+
const c = convo[i]?.content;
|
|
792
|
+
if (convo[i].role === "user" && typeof c === "string") { lastUser = c; break; }
|
|
793
|
+
if (convo[i].role === "user" && Array.isArray(c)) {
|
|
794
|
+
const t = c.find(b => b.type === "text");
|
|
795
|
+
if (t?.text) { lastUser = t.text; break; }
|
|
796
|
+
}
|
|
797
|
+
}
|
|
798
|
+
const skillsInfo = getSkillsPrompt(lastUser);
|
|
799
|
+
const mem = readMemory().trim();
|
|
800
|
+
const memBlock = mem ? `\n\n=== MEMORI PROYEK (KILUA.md) ===\n${mem}` : "";
|
|
801
|
+
const mcpList = Array.isArray(settings.mcp) ? settings.mcp : [];
|
|
802
|
+
const mcpBlock = mcpList.length
|
|
803
|
+
? `\n\nMCP servers terdaftar (info saja, eksekusi tool MCP belum aktif di CLI):\n${mcpList.map(m => `- ${m.name}: ${m.command || ""} ${(m.args || []).join(" ")}`).join("\n")}`
|
|
804
|
+
: "";
|
|
805
|
+
const planBlock = PLAN_MODE
|
|
806
|
+
? "\n\nPLAN MODE AKTIF: hanya baca & analisa. JANGAN mengubah file atau menjalankan perintah yang mengubah state. Susun rencana langkah demi langkah, lalu minta user mematikan plan mode (/plan) untuk eksekusi."
|
|
807
|
+
: "";
|
|
272
808
|
return `Kamu KILUA, AI Coding Agent otonom di terminal lokal. Folder aktif: ${process.cwd()}.
|
|
273
809
|
KEMAMPUAN TOOLS:
|
|
274
810
|
- read_file, write_file, edit_file, list_dir
|
|
@@ -284,30 +820,87 @@ CARA KERJA (WAJIB DIIKUTI):
|
|
|
284
820
|
6. Baru setelah terbukti jalan tanpa error, laporkan hasil + cara menjalankannya ke pengguna.
|
|
285
821
|
7. Jika pengguna menyuruh install skill di chat (misal: "install skill vercel-labs/skills@find-skills"), jalankan run_cmd:
|
|
286
822
|
npx skills add <nama-skill> --cwd "${SKILLS_DIR}"
|
|
287
|
-
lalu beri tahu skill sudah siap dipakai.${skillsInfo}
|
|
823
|
+
lalu beri tahu skill sudah siap dipakai.${skillsInfo}${memBlock}${mcpBlock}${planBlock}
|
|
288
824
|
|
|
289
825
|
Gaya: ringkas, langsung bertindak, jangan minta konfirmasi untuk langkah yang sudah jelas.`;
|
|
290
826
|
}
|
|
291
827
|
|
|
828
|
+
// ponytail: bedakan token habis vs koneksi putus. Token habis = berhenti + beritahu.
|
|
829
|
+
// Koneksi putus = terus coba sampai nyambung lagi, maksimal ~5 menit lalu beritahu.
|
|
830
|
+
const TOKEN_EXHAUSTED_RE = /insufficient_quota|quota exceeded|exceeded your current quota|credit balance|billing|payment required|no credit|saldo|quota habis/i;
|
|
831
|
+
const MAX_RECONNECT_ATTEMPTS = 12;
|
|
832
|
+
|
|
833
|
+
function isTokenExhausted(status, errText) {
|
|
834
|
+
if (TOKEN_EXHAUSTED_RE.test(errText || "")) return true;
|
|
835
|
+
if (status === 402) return true;
|
|
836
|
+
return false;
|
|
837
|
+
}
|
|
838
|
+
|
|
839
|
+
function isBadCredentials(status, errText) {
|
|
840
|
+
if (status === 403) return true;
|
|
841
|
+
return status === 401 && /api key|unauthorized|invalid.*key|authentication/i.test(errText || "");
|
|
842
|
+
}
|
|
843
|
+
|
|
844
|
+
function reportTokenExhausted(status, errText, badCredentials) {
|
|
845
|
+
stopStatus();
|
|
846
|
+
const hint = String(errText || "").slice(0, 200).replace(/\n/g, " ");
|
|
847
|
+
if (badCredentials) {
|
|
848
|
+
console.error(`\n\x1b[31m[✖] API KEY DITOLAK (HTTP ${status}).\x1b[0m`);
|
|
849
|
+
console.error(`\x1b[90m ${hint}\x1b[0m`);
|
|
850
|
+
console.error(`\x1b[90m Perbaiki key lewat: /config (riwayat chat tetap ada, tinggal minta "lanjut").\x1b[0m\n`);
|
|
851
|
+
return;
|
|
852
|
+
}
|
|
853
|
+
console.error(`\n\x1b[31m[✖] TOKEN / KUOTA HABIS (HTTP ${status}).\x1b[0m`);
|
|
854
|
+
console.error(`\x1b[33m Tugas dihentikan karena provider menolak permintaan.\x1b[0m`);
|
|
855
|
+
if (hint) console.error(`\x1b[90m ${hint}\x1b[0m`);
|
|
856
|
+
console.error(`\x1b[90m Isi ulang kuota / ganti provider lalu ketik: /config (riwayat chat tetap ada, tinggal minta "lanjut").\x1b[0m\n`);
|
|
857
|
+
}
|
|
858
|
+
|
|
859
|
+
function reportConnectionLost(reason) {
|
|
860
|
+
stopStatus();
|
|
861
|
+
console.error(`\n\x1b[31m[✖] KONEKSI TERPUTUS: ${reason}\x1b[0m`);
|
|
862
|
+
console.error(`\x1b[33m Kilua berhenti setelah ${MAX_RECONNECT_ATTEMPTS} percobaan.\x1b[0m`);
|
|
863
|
+
console.error(`\x1b[90m Begitu internet/provider normal, minta Kilua "lanjut" — riwayat langkahnya masih tersimpan.\x1b[0m\n`);
|
|
864
|
+
}
|
|
865
|
+
|
|
866
|
+
// true = sudah dilaporkan, loop harus berhenti.
|
|
867
|
+
function handleHttpError(status, errText) {
|
|
868
|
+
if (isTokenExhausted(status, errText) || isBadCredentials(status, errText)) {
|
|
869
|
+
reportTokenExhausted(status, errText, isBadCredentials(status, errText));
|
|
870
|
+
return true;
|
|
871
|
+
}
|
|
872
|
+
stopStatus();
|
|
873
|
+
console.error(`\n\x1b[31m[✖] Provider error (HTTP ${status}).\x1b[0m`);
|
|
874
|
+
console.error(`\x1b[90m ${String(errText).slice(0, 300).replace(/\n/g, " ")}\x1b[0m\n`);
|
|
875
|
+
return true;
|
|
876
|
+
}
|
|
877
|
+
|
|
292
878
|
// ponytail: retry untuk 429/5xx/network — provider gratis sering rate-limit
|
|
293
879
|
async function fetchWithRetry(url, init) {
|
|
294
880
|
let lastErr;
|
|
295
|
-
|
|
881
|
+
let hadFailure = false;
|
|
882
|
+
for (let attempt = 0; attempt <= MAX_RECONNECT_ATTEMPTS; attempt++) {
|
|
296
883
|
try {
|
|
297
884
|
const res = await fetch(url, init);
|
|
298
885
|
if (res.status === 429 || res.status === 503 || res.status >= 500) {
|
|
299
|
-
if (attempt ===
|
|
886
|
+
if (attempt === MAX_RECONNECT_ATTEMPTS) return res;
|
|
887
|
+
hadFailure = true;
|
|
300
888
|
const wait = Math.min(30000, 1500 * Math.pow(2, attempt));
|
|
301
|
-
|
|
889
|
+
startStatus(`Connection busy (${res.status}) — retry ${attempt + 1}/${MAX_RECONNECT_ATTEMPTS} in ${Math.round(wait / 1000)}s...`);
|
|
302
890
|
await new Promise(r => setTimeout(r, wait));
|
|
303
891
|
continue;
|
|
304
892
|
}
|
|
893
|
+
if (hadFailure) {
|
|
894
|
+
stopStatus();
|
|
895
|
+
console.log("\x1b[32m[✓] Connection restored — resuming task.\x1b[0m");
|
|
896
|
+
}
|
|
305
897
|
return res;
|
|
306
898
|
} catch (e) {
|
|
307
899
|
lastErr = e;
|
|
308
|
-
if (attempt ===
|
|
309
|
-
|
|
310
|
-
|
|
900
|
+
if (attempt === MAX_RECONNECT_ATTEMPTS) throw e;
|
|
901
|
+
hadFailure = true;
|
|
902
|
+
const wait = Math.min(30000, 1500 * Math.pow(2, attempt));
|
|
903
|
+
startStatus(`Connection lost — reconnecting in ${Math.round(wait / 1000)}s (${attempt + 1}/${MAX_RECONNECT_ATTEMPTS})...`);
|
|
311
904
|
await new Promise(r => setTimeout(r, wait));
|
|
312
905
|
}
|
|
313
906
|
}
|
|
@@ -316,13 +909,16 @@ async function fetchWithRetry(url, init) {
|
|
|
316
909
|
|
|
317
910
|
// Loop untuk Anthropic / 9router local (/v1/messages)
|
|
318
911
|
async function runAnthropicLoop(settings, convo) {
|
|
319
|
-
const SYSTEM = buildSystemPrompt();
|
|
912
|
+
const SYSTEM = buildSystemPrompt(convo, settings);
|
|
320
913
|
|
|
321
914
|
let endpoint = settings.base_url.replace(/\/+$/, "");
|
|
322
915
|
if (!endpoint.endsWith("/messages")) endpoint += "/messages";
|
|
323
916
|
|
|
917
|
+
let finished = false;
|
|
324
918
|
for (let step = 0; step < MAX_STEPS; step++) {
|
|
325
|
-
|
|
919
|
+
startStatus(stepLabel(step, MAX_STEPS));
|
|
920
|
+
const turnStart = Date.now();
|
|
921
|
+
let usage = null;
|
|
326
922
|
let res;
|
|
327
923
|
try {
|
|
328
924
|
res = await fetchWithRetry(endpoint, {
|
|
@@ -343,13 +939,12 @@ async function runAnthropicLoop(settings, convo) {
|
|
|
343
939
|
})
|
|
344
940
|
});
|
|
345
941
|
} catch (e) {
|
|
346
|
-
|
|
942
|
+
reportConnectionLost(e.message || "network error");
|
|
347
943
|
return;
|
|
348
944
|
}
|
|
349
945
|
|
|
350
946
|
if (!res.ok) {
|
|
351
|
-
|
|
352
|
-
console.error(`\n[!] Error (${res.status}):`, errText.slice(0, 200));
|
|
947
|
+
handleHttpError(res.status, await res.text().catch(() => ""));
|
|
353
948
|
return;
|
|
354
949
|
}
|
|
355
950
|
|
|
@@ -358,6 +953,9 @@ async function runAnthropicLoop(settings, convo) {
|
|
|
358
953
|
const toolBlocks = [];
|
|
359
954
|
|
|
360
955
|
await readSseStream(res, (evt) => {
|
|
956
|
+
if (evt.type === "message_start" && evt.message?.usage) usage = { ...evt.message.usage };
|
|
957
|
+
if (evt.type === "message_delta" && evt.usage) usage = { ...usage, ...evt.usage };
|
|
958
|
+
|
|
361
959
|
let deltaText = "";
|
|
362
960
|
if (typeof evt.delta?.text === "string") deltaText = evt.delta.text;
|
|
363
961
|
else if (typeof evt.delta?.content === "string") deltaText = evt.delta.content;
|
|
@@ -366,6 +964,7 @@ async function runAnthropicLoop(settings, convo) {
|
|
|
366
964
|
|
|
367
965
|
if (deltaText) {
|
|
368
966
|
if (!printedHeader) {
|
|
967
|
+
stopStatus();
|
|
369
968
|
process.stdout.write("\r\x1b[2KKILUA > ");
|
|
370
969
|
printedHeader = true;
|
|
371
970
|
}
|
|
@@ -389,8 +988,10 @@ async function runAnthropicLoop(settings, convo) {
|
|
|
389
988
|
}
|
|
390
989
|
});
|
|
391
990
|
|
|
991
|
+
stopStatus();
|
|
392
992
|
if (printedHeader) console.log();
|
|
393
993
|
else process.stdout.write("\r\x1b[2K");
|
|
994
|
+
printTokenLine(usage, turnStart);
|
|
394
995
|
|
|
395
996
|
const activeTools = toolBlocks.filter(Boolean);
|
|
396
997
|
const assistantBlocks = [];
|
|
@@ -399,15 +1000,14 @@ async function runAnthropicLoop(settings, convo) {
|
|
|
399
1000
|
|
|
400
1001
|
convo.push({ role: "assistant", content: assistantBlocks.length ? assistantBlocks : fullText });
|
|
401
1002
|
|
|
402
|
-
if (!activeTools.length) break;
|
|
1003
|
+
if (!activeTools.length) { finished = true; break; }
|
|
403
1004
|
|
|
404
1005
|
const toolResultContent = [];
|
|
405
1006
|
for (const tu of activeTools) {
|
|
406
1007
|
const name = tu.name;
|
|
407
1008
|
const args = tu.input || {};
|
|
408
|
-
console.log(`\x1b[36m⚡ Tool: ${name}\x1b[0m(${JSON.stringify(args)})`);
|
|
409
1009
|
const output = executeLocalTool(name, args);
|
|
410
|
-
|
|
1010
|
+
printToolCall(name, args, output);
|
|
411
1011
|
toolResultContent.push({
|
|
412
1012
|
type: "tool_result",
|
|
413
1013
|
tool_use_id: tu.id,
|
|
@@ -416,11 +1016,16 @@ async function runAnthropicLoop(settings, convo) {
|
|
|
416
1016
|
}
|
|
417
1017
|
convo.push({ role: "user", content: toolResultContent });
|
|
418
1018
|
}
|
|
1019
|
+
if (!finished) {
|
|
1020
|
+
stopStatus();
|
|
1021
|
+
console.log(`\x1b[33m[!] Batas ${MAX_STEPS} langkah tercapai — tugas belum tuntas.\x1b[0m`);
|
|
1022
|
+
console.log(`\x1b[90m Ketik "lanjut" untuk meneruskan (riwayat tetap tersimpan).\x1b[0m\n`);
|
|
1023
|
+
}
|
|
419
1024
|
}
|
|
420
1025
|
|
|
421
1026
|
// Loop untuk OpenAI / TokenHarbor / Gemini OpenAI protocol
|
|
422
1027
|
async function runOpenAiLoop(settings, convo) {
|
|
423
|
-
const SYSTEM = buildSystemPrompt();
|
|
1028
|
+
const SYSTEM = buildSystemPrompt(convo, settings);
|
|
424
1029
|
|
|
425
1030
|
let endpoint = settings.base_url.replace(/\/+$/, "");
|
|
426
1031
|
if (endpoint.includes("generativelanguage.googleapis.com") && !endpoint.includes("/openai")) {
|
|
@@ -430,8 +1035,11 @@ async function runOpenAiLoop(settings, convo) {
|
|
|
430
1035
|
endpoint += "/chat/completions";
|
|
431
1036
|
}
|
|
432
1037
|
|
|
1038
|
+
let finished = false;
|
|
433
1039
|
for (let step = 0; step < MAX_STEPS; step++) {
|
|
434
|
-
|
|
1040
|
+
startStatus(stepLabel(step, MAX_STEPS));
|
|
1041
|
+
const turnStart = Date.now();
|
|
1042
|
+
let usage = null;
|
|
435
1043
|
let res;
|
|
436
1044
|
try {
|
|
437
1045
|
res = await fetchWithRetry(endpoint, {
|
|
@@ -444,19 +1052,19 @@ async function runOpenAiLoop(settings, convo) {
|
|
|
444
1052
|
body: JSON.stringify({
|
|
445
1053
|
model: settings.model,
|
|
446
1054
|
stream: true,
|
|
1055
|
+
stream_options: { include_usage: true },
|
|
447
1056
|
messages: [{ role: "system", content: SYSTEM }, ...convo],
|
|
448
1057
|
tools: TOOLS_OPENAI,
|
|
449
1058
|
tool_choice: "auto"
|
|
450
1059
|
})
|
|
451
1060
|
});
|
|
452
1061
|
} catch (e) {
|
|
453
|
-
|
|
1062
|
+
reportConnectionLost(e.message || "network error");
|
|
454
1063
|
return;
|
|
455
1064
|
}
|
|
456
1065
|
|
|
457
1066
|
if (!res.ok) {
|
|
458
|
-
|
|
459
|
-
console.error(`\n[!] Error (${res.status}):`, errText.slice(0, 200));
|
|
1067
|
+
handleHttpError(res.status, await res.text().catch(() => ""));
|
|
460
1068
|
return;
|
|
461
1069
|
}
|
|
462
1070
|
|
|
@@ -465,10 +1073,12 @@ async function runOpenAiLoop(settings, convo) {
|
|
|
465
1073
|
const toolCallsMap = {};
|
|
466
1074
|
|
|
467
1075
|
await readSseStream(res, (evt) => {
|
|
1076
|
+
if (evt.usage) usage = evt.usage;
|
|
468
1077
|
const choice = evt.choices?.[0];
|
|
469
1078
|
const delta = choice?.delta;
|
|
470
1079
|
if (delta?.content) {
|
|
471
1080
|
if (!printedHeader) {
|
|
1081
|
+
stopStatus();
|
|
472
1082
|
process.stdout.write("\r\x1b[2KKILUA > ");
|
|
473
1083
|
printedHeader = true;
|
|
474
1084
|
}
|
|
@@ -485,107 +1095,173 @@ async function runOpenAiLoop(settings, convo) {
|
|
|
485
1095
|
}
|
|
486
1096
|
});
|
|
487
1097
|
|
|
1098
|
+
stopStatus();
|
|
488
1099
|
if (printedHeader) console.log();
|
|
489
1100
|
else process.stdout.write("\r\x1b[2K");
|
|
1101
|
+
printTokenLine(usage, turnStart);
|
|
490
1102
|
|
|
491
1103
|
const toolCalls = Object.values(toolCallsMap);
|
|
492
1104
|
convo.push({ role: "assistant", content: fullText, tool_calls: toolCalls.length ? toolCalls : undefined });
|
|
493
1105
|
|
|
494
|
-
if (!toolCalls.length) break;
|
|
1106
|
+
if (!toolCalls.length) { finished = true; break; }
|
|
495
1107
|
|
|
496
1108
|
for (const tc of toolCalls) {
|
|
497
1109
|
const name = tc.function.name;
|
|
498
1110
|
let args = {};
|
|
499
1111
|
try { args = JSON.parse(tc.function.arguments || "{}"); } catch {}
|
|
500
|
-
console.log(`\x1b[36m⚡ Tool: ${name}\x1b[0m(${JSON.stringify(args)})`);
|
|
501
1112
|
const output = executeLocalTool(name, args);
|
|
502
|
-
|
|
1113
|
+
printToolCall(name, args, output);
|
|
503
1114
|
convo.push({ role: "tool", tool_call_id: tc.id, name, content: output });
|
|
504
1115
|
}
|
|
505
1116
|
}
|
|
1117
|
+
if (!finished) {
|
|
1118
|
+
stopStatus();
|
|
1119
|
+
console.log(`\x1b[33m[!] Batas ${MAX_STEPS} langkah tercapai — tugas belum tuntas.\x1b[0m`);
|
|
1120
|
+
console.log(`\x1b[90m Ketik "lanjut" untuk meneruskan (riwayat tetap tersimpan).\x1b[0m\n`);
|
|
1121
|
+
}
|
|
506
1122
|
}
|
|
507
1123
|
|
|
508
|
-
async function runAgent(settings, userPrompt, convo) {
|
|
1124
|
+
async function runAgent(settings, userPrompt, convo, images = []) {
|
|
509
1125
|
const isAnthropic = settings.provider === "anthropic" || settings.base_url.includes(":20128") || settings.model === "harobor";
|
|
510
|
-
if (
|
|
511
|
-
|
|
512
|
-
|
|
1126
|
+
if (images.length) {
|
|
1127
|
+
if (isAnthropic) {
|
|
1128
|
+
const blocks = images.map(im => ({ type: "image", source: { type: "base64", media_type: im.mediaType, data: im.data } }));
|
|
1129
|
+
convo.push({ role: "user", content: [...blocks, { type: "text", text: userPrompt || "Jelaskan gambar ini." }] });
|
|
1130
|
+
} else {
|
|
1131
|
+
const blocks = images.map(im => ({ type: "image_url", image_url: { url: `data:${im.mediaType};base64,${im.data}` } }));
|
|
1132
|
+
convo.push({ role: "user", content: [...blocks, { type: "text", text: userPrompt || "Jelaskan gambar ini." }] });
|
|
1133
|
+
}
|
|
513
1134
|
} else {
|
|
514
1135
|
convo.push({ role: "user", content: userPrompt });
|
|
515
|
-
await runOpenAiLoop(settings, convo);
|
|
516
1136
|
}
|
|
1137
|
+
if (isAnthropic) await runAnthropicLoop(settings, convo);
|
|
1138
|
+
else await runOpenAiLoop(settings, convo);
|
|
517
1139
|
}
|
|
518
1140
|
|
|
519
|
-
function printHelp() {
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
\x1b[36m/clear\x1b[0m Bersihkan riwayat percakapan sesi ini
|
|
526
|
-
\x1b[36m/config\x1b[0m Ubah API Key, Provider, atau Base URL
|
|
527
|
-
\x1b[36m/exit\x1b[0m Keluar dari Kilua Agent
|
|
528
|
-
|
|
529
|
-
\x1b[90mTips: Untuk menginstall skill baru, cukup suruh Kilua di chat (misal: "tolong install skill find-skills")!\x1b[0m
|
|
530
|
-
`);
|
|
1141
|
+
function printHelp(settings) {
|
|
1142
|
+
const t = themeOf(settings);
|
|
1143
|
+
console.log(`\n${t.bold}${t.accent}=== PERINTAH SLASH KILUA (/) ===${t.reset}`);
|
|
1144
|
+
for (const c of SLASH_COMMANDS) console.log(` ${t.sel}${c.name.padEnd(10)}${t.reset} ${t.dim}${c.desc}${t.reset}`);
|
|
1145
|
+
console.log(`\n${t.dim}Tips: ketik "/" lalu ↑↓ untuk pilih perintah. Install skill baru cukup minta di chat.${t.reset}`);
|
|
1146
|
+
console.log(`${t.dim}Ctrl+V / Alt+V = paste teks (ringkas otomatis) atau gambar dari clipboard.${t.reset}\n`);
|
|
531
1147
|
}
|
|
532
1148
|
|
|
533
|
-
async function
|
|
534
|
-
const
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
if (action === "/help") {
|
|
538
|
-
printHelp();
|
|
1149
|
+
async function cmdModel(settings) {
|
|
1150
|
+
const t = themeOf(settings);
|
|
1151
|
+
if (!settings.base_url || !settings.api_key) {
|
|
1152
|
+
console.log(`\n${t.warn}[!] Belum terhubung ke config provider. Jalankan /config dulu.${t.reset}\n`);
|
|
539
1153
|
return settings;
|
|
540
1154
|
}
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
}
|
|
547
|
-
console.log(`\n\x1b[33mModel aktif saat ini:\x1b[0m \x1b[32m${settings.model}\x1b[0m (Provider: ${settings.base_url})`);
|
|
548
|
-
const newModel = (await promptQuestion(rl, `Ganti nama model (Enter untuk tetap '${settings.model}'): `)).trim();
|
|
549
|
-
if (newModel) {
|
|
550
|
-
settings.model = newModel;
|
|
551
|
-
saveSettings(settings);
|
|
552
|
-
console.log(`\x1b[32m✓ Model diubah menjadi ${newModel}\x1b[0m\n`);
|
|
553
|
-
}
|
|
554
|
-
return settings;
|
|
1155
|
+
console.log(`\n${t.warn}Model aktif:${t.reset} ${t.ok}${settings.model}${t.reset} ${t.dim}(${settings.base_url})${t.reset}`);
|
|
1156
|
+
const newModel = (await promptLine(`Ganti nama model (Enter untuk tetap '${settings.model}'): `)).trim();
|
|
1157
|
+
if (newModel && newModel !== settings.model) {
|
|
1158
|
+
settings.model = newModel;
|
|
1159
|
+
saveSettings(settings);
|
|
1160
|
+
console.log(`${t.ok}✓ Model diubah menjadi ${newModel}${t.reset}\n`);
|
|
555
1161
|
}
|
|
1162
|
+
return settings;
|
|
1163
|
+
}
|
|
556
1164
|
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
return settings;
|
|
570
|
-
}
|
|
1165
|
+
function cmdSkills(settings) {
|
|
1166
|
+
const t = themeOf(settings);
|
|
1167
|
+
const list = listInstalledSkills();
|
|
1168
|
+
const builtin = loadBuiltinSkills();
|
|
1169
|
+
console.log(`\n${t.bold}${t.accent}=== SKILLS TERPASANG (${SKILLS_DIR}) ===${t.reset}`);
|
|
1170
|
+
if (!list.length) {
|
|
1171
|
+
console.log(`Belum ada skill tambahan terpasang.`);
|
|
1172
|
+
console.log(`Suruh Kilua di chat, misal: ${t.sel}"Kilua, install skill vercel-labs/skills@find-skills"${t.reset}`);
|
|
1173
|
+
} else for (const s of list) console.log(`- ${t.ok}${s.name}${t.reset}: ${s.desc}`);
|
|
1174
|
+
console.log(`${t.dim}+ ${builtin.length} skill bawaan aktif otomatis (maks ${MAX_INJECTED_SKILLS}/pesan)${t.reset}\n`);
|
|
1175
|
+
return settings;
|
|
1176
|
+
}
|
|
571
1177
|
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
1178
|
+
async function cmdTheme(settings) {
|
|
1179
|
+
const t = themeOf(settings);
|
|
1180
|
+
const names = Object.keys(THEMES);
|
|
1181
|
+
console.log(`\n${t.bold}${t.accent}=== TEMA ===${t.reset}`);
|
|
1182
|
+
names.forEach((n, i) => {
|
|
1183
|
+
const mark = n === (settings.theme || "purple") ? `${t.ok}●${t.reset}` : " ";
|
|
1184
|
+
console.log(` ${mark} ${i + 1}) ${THEMES[n].accent}${n}${t.reset}`);
|
|
1185
|
+
});
|
|
1186
|
+
const pick = (await promptLine(`\nPilih tema [1-${names.length}] (Enter = batal): `)).trim();
|
|
1187
|
+
const idx = Number(pick) - 1;
|
|
1188
|
+
if (names[idx]) {
|
|
1189
|
+
settings.theme = names[idx];
|
|
1190
|
+
saveSettings(settings);
|
|
1191
|
+
console.log(`${themeOf(settings).ok}✓ Tema: ${settings.theme}${t.reset}\n`);
|
|
577
1192
|
}
|
|
1193
|
+
return settings;
|
|
1194
|
+
}
|
|
578
1195
|
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
1196
|
+
async function cmdMcp(settings) {
|
|
1197
|
+
const t = themeOf(settings);
|
|
1198
|
+
if (!Array.isArray(settings.mcp)) settings.mcp = [];
|
|
1199
|
+
while (true) {
|
|
1200
|
+
console.log(`\n${t.bold}${t.accent}=== MCP SERVERS ===${t.reset}`);
|
|
1201
|
+
if (!settings.mcp.length) console.log(`${t.dim}Belum ada server MCP.${t.reset}`);
|
|
1202
|
+
settings.mcp.forEach((m, i) => console.log(` ${i + 1}) ${t.ok}${m.name}${t.reset} ${t.dim}${m.command || ""} ${(m.args || []).join(" ")}${t.reset}`));
|
|
1203
|
+
const pick = (await promptLine(`\n[a] tambah · [d] hapus · [Enter] selesai: `)).trim().toLowerCase();
|
|
1204
|
+
if (!pick) break;
|
|
1205
|
+
if (pick === "a") {
|
|
1206
|
+
const name = (await promptLine("Nama server: ")).trim();
|
|
1207
|
+
const command = (await promptLine("Command (mis. npx): ")).trim();
|
|
1208
|
+
const argsRaw = (await promptLine("Args (pisah spasi, opsional): ")).trim();
|
|
1209
|
+
if (!name || !command) { console.log(`${t.warn}[!] Nama & command wajib.${t.reset}`); continue; }
|
|
1210
|
+
settings.mcp.push({ name, command, args: argsRaw ? argsRaw.split(/\s+/) : [] });
|
|
1211
|
+
saveSettings(settings);
|
|
1212
|
+
console.log(`${t.ok}✓ ${name} ditambahkan.${t.reset}`);
|
|
1213
|
+
} else if (pick === "d") {
|
|
1214
|
+
const n = Number((await promptLine("Nomor yang dihapus: ")).trim()) - 1;
|
|
1215
|
+
if (settings.mcp[n]) { const [x] = settings.mcp.splice(n, 1); saveSettings(settings); console.log(`${t.ok}✓ ${x.name} dihapus.${t.reset}`); }
|
|
1216
|
+
else console.log(`${t.warn}[!] Nomor tidak valid.${t.reset}`);
|
|
1217
|
+
} else break;
|
|
583
1218
|
}
|
|
1219
|
+
return settings;
|
|
1220
|
+
}
|
|
1221
|
+
|
|
1222
|
+
async function cmdMemory(settings) {
|
|
1223
|
+
const t = themeOf(settings);
|
|
1224
|
+
const file = path.join(process.cwd(), "KILUA.md");
|
|
1225
|
+
const body = readMemory();
|
|
1226
|
+
console.log(`\n${t.bold}${t.accent}=== MEMORI PROYEK ===${t.reset} ${t.dim}${file}${t.reset}`);
|
|
1227
|
+
console.log(body.trim() ? body.trim() : `${t.dim}(kosong — memori ini selalu disuntik ke system prompt)${t.reset}`);
|
|
1228
|
+
const add = (await promptLine(`\nTambah catatan memori (Enter = batal): `)).trim();
|
|
1229
|
+
if (add) {
|
|
1230
|
+
fs.appendFileSync(file, (fs.existsSync(file) ? "\n" : "# Memori Proyek (KILUA.md)\n") + "- " + add + "\n", "utf8");
|
|
1231
|
+
console.log(`${t.ok}✓ Disimpan ke KILUA.md${t.reset}\n`);
|
|
1232
|
+
} else console.log();
|
|
1233
|
+
return settings;
|
|
1234
|
+
}
|
|
584
1235
|
|
|
585
|
-
|
|
1236
|
+
function cmdPlan(settings) {
|
|
1237
|
+
const t = themeOf(settings);
|
|
1238
|
+
PLAN_MODE = !PLAN_MODE;
|
|
1239
|
+
console.log(`\n${PLAN_MODE ? t.warn + "⏸ Plan mode ON" : t.ok + "▶ Plan mode OFF"}${t.reset} ${t.dim}— ${PLAN_MODE ? "hanya baca & menyusun rencana, aksi tulis diblokir" : "agent bebas mengeksekusi"}.${t.reset}\n`);
|
|
586
1240
|
return settings;
|
|
587
1241
|
}
|
|
588
1242
|
|
|
1243
|
+
async function handleSlashCommand(cmd, settings, convo) {
|
|
1244
|
+
const action = cmd.trim().split(/\s+/)[0].toLowerCase();
|
|
1245
|
+
switch (action) {
|
|
1246
|
+
case "/help": return printHelp(settings), settings;
|
|
1247
|
+
case "/model": case "/models": return await cmdModel(settings);
|
|
1248
|
+
case "/skills": case "/skill": return cmdSkills(settings);
|
|
1249
|
+
case "/theme": return await cmdTheme(settings);
|
|
1250
|
+
case "/mcp": return await cmdMcp(settings);
|
|
1251
|
+
case "/memory": return await cmdMemory(settings);
|
|
1252
|
+
case "/plan": return cmdPlan(settings);
|
|
1253
|
+
case "/clear":
|
|
1254
|
+
convo.length = 0;
|
|
1255
|
+
console.clear();
|
|
1256
|
+
console.log(`${themeOf(settings).ok}✓ Riwayat percakapan dibersihkan.${themeOf(settings).reset}\n`);
|
|
1257
|
+
return settings;
|
|
1258
|
+
case "/config": return await runInteractiveConfig();
|
|
1259
|
+
default:
|
|
1260
|
+
console.log(`Perintah tidak dikenal: ${action}. Ketik ${themeOf(settings).sel}/help${themeOf(settings).reset}.`);
|
|
1261
|
+
return settings;
|
|
1262
|
+
}
|
|
1263
|
+
}
|
|
1264
|
+
|
|
589
1265
|
async function main() {
|
|
590
1266
|
const args = process.argv.slice(2);
|
|
591
1267
|
const oneShot = args.join(" ").trim();
|
|
@@ -596,11 +1272,23 @@ async function main() {
|
|
|
596
1272
|
}
|
|
597
1273
|
|
|
598
1274
|
let settings = loadSettings();
|
|
1275
|
+
if (args[0] === "--version" || args[0] === "-v") {
|
|
1276
|
+
const here = path.dirname(fileURLToPath(import.meta.url));
|
|
1277
|
+
const candidates = [path.join(here, "..", "packages", "kilua-agent", "package.json"), path.join(here, "..", "package.json")];
|
|
1278
|
+
let v = "1.3.0";
|
|
1279
|
+
for (const p of candidates) {
|
|
1280
|
+
try {
|
|
1281
|
+
const j = JSON.parse(fs.readFileSync(p, "utf8"));
|
|
1282
|
+
if (j.name === "kilua-ai" && j.private) continue; // root app, bukan paket CLI
|
|
1283
|
+
if (j.version) { v = j.version; break; }
|
|
1284
|
+
} catch {}
|
|
1285
|
+
}
|
|
1286
|
+
console.log(`kilua-agent ${v}`);
|
|
1287
|
+
process.exit(0);
|
|
1288
|
+
}
|
|
599
1289
|
|
|
600
|
-
// Validasi ketat: jika belum ada base_url atau api_key, WAJIB setup dulu
|
|
601
1290
|
const isLocalProxy = settings.base_url?.includes("127.0.0.1") || settings.base_url?.includes("localhost");
|
|
602
1291
|
const hasValidConfig = settings.base_url && (settings.api_key || isLocalProxy);
|
|
603
|
-
|
|
604
1292
|
if (!hasValidConfig) {
|
|
605
1293
|
console.log("\x1b[33m[!] Belum ada konfigurasi AI key / provider.\x1b[0m");
|
|
606
1294
|
settings = await runInteractiveConfig();
|
|
@@ -612,29 +1300,25 @@ async function main() {
|
|
|
612
1300
|
process.exit(0);
|
|
613
1301
|
}
|
|
614
1302
|
|
|
615
|
-
|
|
616
|
-
console.log(
|
|
617
|
-
console.log(
|
|
618
|
-
console.log(
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
const
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
ask();
|
|
635
|
-
});
|
|
636
|
-
};
|
|
637
|
-
ask();
|
|
1303
|
+
const t = themeOf(settings);
|
|
1304
|
+
console.log(`${t.bold}${t.accent}=== KILUA AI CLI AGENT ===${t.reset}`);
|
|
1305
|
+
console.log(`${t.dim}CWD :${t.reset} ${t.warn}${process.cwd()}${t.reset}`);
|
|
1306
|
+
console.log(`${t.dim}Provider:${t.reset} ${t.ok}${settings.base_url}${t.reset} (${settings.model})`);
|
|
1307
|
+
console.log(`${t.dim}Bantuan :${t.reset} ${t.sel}/${t.reset} perintah · ${t.sel}Ctrl+V${t.reset} paste teks/gambar\n`);
|
|
1308
|
+
|
|
1309
|
+
const history = [];
|
|
1310
|
+
while (true) {
|
|
1311
|
+
const { text, images } = await promptComposer(settings, history);
|
|
1312
|
+
if (!text && !images.length) continue;
|
|
1313
|
+
if (text === "exit" || text === "quit" || text === "/exit") break;
|
|
1314
|
+
if (images.length && !text) { await runAgent(settings, "", convo, images); continue; }
|
|
1315
|
+
if (text.startsWith("/")) { settings = await handleSlashCommand(text, settings, convo); continue; }
|
|
1316
|
+
history.push(text);
|
|
1317
|
+
if (history.length > 100) history.shift();
|
|
1318
|
+
await runAgent(settings, text, convo, images);
|
|
1319
|
+
}
|
|
1320
|
+
console.log(`${t.dim}Sampai jumpa.${t.reset}`);
|
|
1321
|
+
process.exit(0);
|
|
638
1322
|
}
|
|
639
1323
|
|
|
640
1324
|
main();
|