@gilangranggaid/kilua-agent 1.0.1 → 1.0.3
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.mjs +125 -31
- package/package.json +1 -1
package/bin/kilua.mjs
CHANGED
|
@@ -5,7 +5,7 @@ import path from "node:path";
|
|
|
5
5
|
import os from "node:os";
|
|
6
6
|
import { execSync } from "node:child_process";
|
|
7
7
|
|
|
8
|
-
// ponytail: single-file agent CLI
|
|
8
|
+
// ponytail: single-file agent CLI with robust SSE & JSON parsing for 9router, Anthropic & OpenAI proxies.
|
|
9
9
|
const KILUA_DIR = path.join(os.homedir(), ".kilua");
|
|
10
10
|
const SETTINGS_PATH = path.join(KILUA_DIR, "settings.json");
|
|
11
11
|
const CLAUDE_SETTINGS_PATH = path.join(os.homedir(), ".claude", "settings.json");
|
|
@@ -133,11 +133,11 @@ async function runInteractiveConfig() {
|
|
|
133
133
|
const claudeCfg = readClaudeSettings();
|
|
134
134
|
|
|
135
135
|
console.log("\x1b[35m=== KILUA CONFIG ===\x1b[0m\nPath: " + SETTINGS_PATH);
|
|
136
|
-
console.log("1) 9Router / Claude Code Proxy (Port 20128)");
|
|
137
|
-
console.log("2) TokenHarbor / OpenAI
|
|
138
|
-
console.log("3)
|
|
136
|
+
console.log("1) 9Router Local / Claude Code Proxy (Port 20128)");
|
|
137
|
+
console.log("2) TokenHarbor / 9Router / OpenRouter (OpenAI-compatible)");
|
|
138
|
+
console.log("3) Google Gemini (AI Studio Key)\n");
|
|
139
139
|
|
|
140
|
-
const choice = (await promptQuestion(rl, "Pilihan [1/2/3] (1): ")).trim() || "1";
|
|
140
|
+
const choice = (await promptQuestion(rl, "Pilihan [1/2/3] (default 1): ")).trim() || "1";
|
|
141
141
|
let settings = {};
|
|
142
142
|
|
|
143
143
|
if (choice === "1") {
|
|
@@ -146,7 +146,7 @@ async function runInteractiveConfig() {
|
|
|
146
146
|
const defaultModel = claudeCfg?.model || "harobor";
|
|
147
147
|
|
|
148
148
|
const baseUrl = (await promptQuestion(rl, `Base URL (${defaultUrl}): `)).trim() || defaultUrl;
|
|
149
|
-
const key = (await promptQuestion(rl, `API Key/Token (${defaultKey ? defaultKey.slice(0, 10) + "..." : "
|
|
149
|
+
const key = (await promptQuestion(rl, `API Key/Token (${defaultKey ? defaultKey.slice(0, 10) + "..." : "masukkan token"}): `)).trim() || defaultKey;
|
|
150
150
|
const model = (await promptQuestion(rl, `Model (${defaultModel}): `)).trim() || defaultModel;
|
|
151
151
|
|
|
152
152
|
settings = {
|
|
@@ -155,34 +155,92 @@ async function runInteractiveConfig() {
|
|
|
155
155
|
api_key: key,
|
|
156
156
|
model
|
|
157
157
|
};
|
|
158
|
-
} else if (choice === "
|
|
158
|
+
} else if (choice === "3") {
|
|
159
|
+
const key = (await promptQuestion(rl, "Masukkan GEMINI_API_KEY: ")).trim();
|
|
160
|
+
const model = (await promptQuestion(rl, "Nama Model (gemini-2.5-flash): ")).trim() || "gemini-2.5-flash";
|
|
161
|
+
settings = {
|
|
162
|
+
provider: "openai",
|
|
163
|
+
base_url: "https://generativelanguage.googleapis.com/v1beta/openai",
|
|
164
|
+
api_key: key,
|
|
165
|
+
model
|
|
166
|
+
};
|
|
167
|
+
} else {
|
|
168
|
+
console.log("Contoh Base URL: https://tokenharbor.ai/v1 atau https://api.9router.com/v1");
|
|
159
169
|
const baseUrl = (await promptQuestion(rl, "Base URL (https://tokenharbor.ai/v1): ")).trim() || "https://tokenharbor.ai/v1";
|
|
160
170
|
const key = (await promptQuestion(rl, "API Key: ")).trim();
|
|
161
171
|
const model = (await promptQuestion(rl, "Model (deepseek-v4.1-flash:free): ")).trim() || "deepseek-v4.1-flash:free";
|
|
162
|
-
settings = {
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
settings = { provider, api_key: key, base_url: baseUrl.replace(/\/+$/, ""), model };
|
|
172
|
+
settings = {
|
|
173
|
+
provider: "openai",
|
|
174
|
+
api_key: key,
|
|
175
|
+
base_url: baseUrl.replace(/\/+$/, ""),
|
|
176
|
+
model
|
|
177
|
+
};
|
|
169
178
|
}
|
|
170
179
|
|
|
171
180
|
saveSettings(settings);
|
|
172
181
|
rl.close();
|
|
173
|
-
console.log("\x1b[32m✓
|
|
182
|
+
console.log("\x1b[32m✓ Pengaturan disimpan di " + SETTINGS_PATH + "\x1b[0m\n");
|
|
174
183
|
return settings;
|
|
175
184
|
}
|
|
176
185
|
|
|
177
|
-
//
|
|
186
|
+
// Parser tangguh untuk SSE (Server-Sent Events) maupun raw JSON dari Anthropic/9router
|
|
187
|
+
async function parseAnthropicResponse(res) {
|
|
188
|
+
const text = await res.text();
|
|
189
|
+
const trimmed = text.trim();
|
|
190
|
+
|
|
191
|
+
// Jika bukan SSE (JSON murni)
|
|
192
|
+
if (!trimmed.startsWith("event:") && !trimmed.startsWith("data:")) {
|
|
193
|
+
try { return JSON.parse(trimmed); } catch {}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// Jika berupa SSE stream
|
|
197
|
+
const lines = text.split("\n");
|
|
198
|
+
const blocks = [];
|
|
199
|
+
|
|
200
|
+
for (const line of lines) {
|
|
201
|
+
if (!line.startsWith("data:")) continue;
|
|
202
|
+
const jsonStr = line.slice(5).trim();
|
|
203
|
+
if (!jsonStr || jsonStr === "[DONE]") continue;
|
|
204
|
+
|
|
205
|
+
let evt = {};
|
|
206
|
+
try { evt = JSON.parse(jsonStr); } catch { continue; }
|
|
207
|
+
|
|
208
|
+
if (evt.type === "content_block_start") {
|
|
209
|
+
const blk = { ...evt.content_block };
|
|
210
|
+
if (blk.type === "tool_use") blk._raw_json = "";
|
|
211
|
+
blocks[evt.index] = blk;
|
|
212
|
+
} else if (evt.type === "content_block_delta") {
|
|
213
|
+
const idx = evt.index;
|
|
214
|
+
if (!blocks[idx]) blocks[idx] = { type: "text", text: "" };
|
|
215
|
+
if (evt.delta?.type === "text_delta") {
|
|
216
|
+
blocks[idx].text = (blocks[idx].text || "") + (evt.delta.text || "");
|
|
217
|
+
} else if (evt.delta?.type === "input_json_delta") {
|
|
218
|
+
blocks[idx]._raw_json = (blocks[idx]._raw_json || "") + (evt.delta.partial_json || "");
|
|
219
|
+
}
|
|
220
|
+
} else if (evt.type === "content_block_stop") {
|
|
221
|
+
const idx = evt.index;
|
|
222
|
+
if (blocks[idx]?.type === "tool_use" && blocks[idx]._raw_json) {
|
|
223
|
+
try { blocks[idx].input = JSON.parse(blocks[idx]._raw_json); } catch {}
|
|
224
|
+
delete blocks[idx]._raw_json;
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
return { content: blocks.filter(Boolean) };
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// Loop untuk Anthropic / 9router local (/v1/messages)
|
|
178
233
|
async function runAnthropicLoop(settings, convo) {
|
|
179
234
|
const SYSTEM = `Kamu KILUA, AI Coding Agent di terminal lokal. Folder: ${process.cwd()}. Gunakan tools (read_file, write_file, edit_file, list_dir, run_cmd) untuk inspeksi dan eksekusi tugas coding mandiri. Respon ringkas to the point.`;
|
|
180
235
|
|
|
236
|
+
let endpoint = settings.base_url.replace(/\/+$/, "");
|
|
237
|
+
if (!endpoint.endsWith("/messages")) endpoint += "/messages";
|
|
238
|
+
|
|
181
239
|
for (let step = 0; step < 12; step++) {
|
|
182
240
|
process.stdout.write("\r\x1b[2K\x1b[35m[KILUA Berpikir...]\x1b[0m ");
|
|
183
241
|
let res;
|
|
184
242
|
try {
|
|
185
|
-
res = await fetch(
|
|
243
|
+
res = await fetch(endpoint, {
|
|
186
244
|
method: "POST",
|
|
187
245
|
headers: {
|
|
188
246
|
"Content-Type": "application/json",
|
|
@@ -193,6 +251,7 @@ async function runAnthropicLoop(settings, convo) {
|
|
|
193
251
|
body: JSON.stringify({
|
|
194
252
|
model: settings.model,
|
|
195
253
|
max_tokens: 4096,
|
|
254
|
+
stream: false,
|
|
196
255
|
system: SYSTEM,
|
|
197
256
|
messages: convo,
|
|
198
257
|
tools: TOOLS_ANTHROPIC
|
|
@@ -204,13 +263,13 @@ async function runAnthropicLoop(settings, convo) {
|
|
|
204
263
|
}
|
|
205
264
|
|
|
206
265
|
if (!res.ok) {
|
|
207
|
-
const
|
|
208
|
-
console.error(`\n[!] Error (${res.status}):`,
|
|
266
|
+
const errText = await res.text().catch(() => "");
|
|
267
|
+
console.error(`\n[!] Error (${res.status}):`, errText.slice(0, 200));
|
|
209
268
|
return;
|
|
210
269
|
}
|
|
211
270
|
|
|
212
|
-
const data = await res
|
|
213
|
-
const blocks = data
|
|
271
|
+
const data = await parseAnthropicResponse(res);
|
|
272
|
+
const blocks = data?.content || [];
|
|
214
273
|
const textBlock = blocks.find(b => b.type === "text");
|
|
215
274
|
if (textBlock?.text) {
|
|
216
275
|
process.stdout.write(`\r\x1b[2KKILUA > ${textBlock.text}\n`);
|
|
@@ -239,18 +298,52 @@ async function runAnthropicLoop(settings, convo) {
|
|
|
239
298
|
console.log();
|
|
240
299
|
}
|
|
241
300
|
|
|
242
|
-
//
|
|
301
|
+
// Parser tangguh untuk OpenAI format
|
|
302
|
+
async function parseOpenAiResponse(res) {
|
|
303
|
+
const text = await res.text();
|
|
304
|
+
const trimmed = text.trim();
|
|
305
|
+
if (!trimmed.startsWith("data:")) {
|
|
306
|
+
try { return JSON.parse(trimmed); } catch {}
|
|
307
|
+
}
|
|
308
|
+
let content = "";
|
|
309
|
+
const lines = text.split("\n");
|
|
310
|
+
for (const line of lines) {
|
|
311
|
+
if (!line.startsWith("data:")) continue;
|
|
312
|
+
const jsonStr = line.slice(5).trim();
|
|
313
|
+
if (!jsonStr || jsonStr === "[DONE]") continue;
|
|
314
|
+
try {
|
|
315
|
+
const parsed = JSON.parse(jsonStr);
|
|
316
|
+
const delta = parsed.choices?.[0]?.delta?.content;
|
|
317
|
+
if (delta) content += delta;
|
|
318
|
+
} catch {}
|
|
319
|
+
}
|
|
320
|
+
return { choices: [{ message: { role: "assistant", content } }] };
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
// Loop untuk OpenAI / TokenHarbor / Gemini OpenAI protocol
|
|
243
324
|
async function runOpenAiLoop(settings, convo) {
|
|
244
325
|
const SYSTEM = `Kamu KILUA, AI Coding Agent di terminal lokal. Folder: ${process.cwd()}. Gunakan tools (read_file, write_file, edit_file, list_dir, run_cmd) untuk inspeksi dan eksekusi tugas coding mandiri. Respon ringkas to the point.`;
|
|
245
326
|
|
|
327
|
+
let endpoint = settings.base_url.replace(/\/+$/, "");
|
|
328
|
+
if (endpoint.includes("generativelanguage.googleapis.com") && !endpoint.includes("/openai")) {
|
|
329
|
+
endpoint += "/openai";
|
|
330
|
+
}
|
|
331
|
+
if (!endpoint.endsWith("/chat/completions")) {
|
|
332
|
+
endpoint += "/chat/completions";
|
|
333
|
+
}
|
|
334
|
+
|
|
246
335
|
for (let step = 0; step < 12; step++) {
|
|
247
336
|
process.stdout.write("\r\x1b[2K\x1b[35m[KILUA Berpikir...]\x1b[0m ");
|
|
248
337
|
let res;
|
|
249
338
|
try {
|
|
250
|
-
res = await fetch(
|
|
339
|
+
res = await fetch(endpoint, {
|
|
251
340
|
method: "POST",
|
|
252
|
-
headers: {
|
|
253
|
-
|
|
341
|
+
headers: {
|
|
342
|
+
"Content-Type": "application/json",
|
|
343
|
+
Authorization: `Bearer ${settings.api_key}`,
|
|
344
|
+
"x-goog-api-key": settings.api_key
|
|
345
|
+
},
|
|
346
|
+
body: JSON.stringify({ model: settings.model, stream: false, messages: [{ role: "system", content: SYSTEM }, ...convo], tools: TOOLS_OPENAI, tool_choice: "auto" })
|
|
254
347
|
});
|
|
255
348
|
} catch (e) {
|
|
256
349
|
console.error("\n[!] Connection error:", e.message);
|
|
@@ -258,13 +351,13 @@ async function runOpenAiLoop(settings, convo) {
|
|
|
258
351
|
}
|
|
259
352
|
|
|
260
353
|
if (!res.ok) {
|
|
261
|
-
const
|
|
262
|
-
console.error(`\n[!] Error (${res.status}):`,
|
|
354
|
+
const errText = await res.text().catch(() => "");
|
|
355
|
+
console.error(`\n[!] Error (${res.status}):`, errText.slice(0, 200));
|
|
263
356
|
return;
|
|
264
357
|
}
|
|
265
358
|
|
|
266
|
-
const data = await res
|
|
267
|
-
const message = data
|
|
359
|
+
const data = await parseOpenAiResponse(res);
|
|
360
|
+
const message = data?.choices?.[0]?.message;
|
|
268
361
|
if (!message) break;
|
|
269
362
|
convo.push(message);
|
|
270
363
|
|
|
@@ -328,10 +421,10 @@ async function main() {
|
|
|
328
421
|
|
|
329
422
|
console.log("\x1b[1m\x1b[35m=== KILUA AI CLI AGENT ===\x1b[0m");
|
|
330
423
|
console.log(`CWD : \x1b[33m${process.cwd()}\x1b[0m`);
|
|
331
|
-
console.log(`Provider: \x1b[32m${settings.base_url}\x1b[0m (${settings.model})
|
|
424
|
+
console.log(`Provider: \x1b[32m${settings.base_url}\x1b[0m (${settings.model})`);
|
|
332
425
|
console.log(`Config : \x1b[90m${SETTINGS_PATH}\x1b[0m (ketik 'config' untuk ganti)\n`);
|
|
333
426
|
|
|
334
|
-
|
|
427
|
+
let rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
335
428
|
const ask = () => {
|
|
336
429
|
rl.question("\x1b[1mKAMU > \x1b[0m", async input => {
|
|
337
430
|
const q = input.trim();
|
|
@@ -341,6 +434,7 @@ async function main() {
|
|
|
341
434
|
rl.close();
|
|
342
435
|
await runInteractiveConfig();
|
|
343
436
|
settings = loadSettings();
|
|
437
|
+
rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
344
438
|
return ask();
|
|
345
439
|
}
|
|
346
440
|
await runAgent(settings, q, convo);
|