@gilangranggaid/kilua-agent 1.0.2 → 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 +92 -23
- 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,23 +133,14 @@ 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)
|
|
137
|
-
console.log("2)
|
|
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
140
|
const choice = (await promptQuestion(rl, "Pilihan [1/2/3] (default 1): ")).trim() || "1";
|
|
141
141
|
let settings = {};
|
|
142
142
|
|
|
143
|
-
if (choice === "
|
|
144
|
-
const key = (await promptQuestion(rl, "Masukkan GEMINI_API_KEY: ")).trim();
|
|
145
|
-
const model = (await promptQuestion(rl, "Nama Model (gemini-2.5-flash): ")).trim() || "gemini-2.5-flash";
|
|
146
|
-
settings = {
|
|
147
|
-
provider: "openai",
|
|
148
|
-
base_url: "https://generativelanguage.googleapis.com/v1beta/openai",
|
|
149
|
-
api_key: key,
|
|
150
|
-
model
|
|
151
|
-
};
|
|
152
|
-
} else if (choice === "3") {
|
|
143
|
+
if (choice === "1") {
|
|
153
144
|
const defaultUrl = claudeCfg?.base_url || "http://127.0.0.1:20128/v1";
|
|
154
145
|
const defaultKey = claudeCfg?.api_key || "";
|
|
155
146
|
const defaultModel = claudeCfg?.model || "harobor";
|
|
@@ -164,6 +155,15 @@ async function runInteractiveConfig() {
|
|
|
164
155
|
api_key: key,
|
|
165
156
|
model
|
|
166
157
|
};
|
|
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
167
|
} else {
|
|
168
168
|
console.log("Contoh Base URL: https://tokenharbor.ai/v1 atau https://api.9router.com/v1");
|
|
169
169
|
const baseUrl = (await promptQuestion(rl, "Base URL (https://tokenharbor.ai/v1): ")).trim() || "https://tokenharbor.ai/v1";
|
|
@@ -183,6 +183,52 @@ async function runInteractiveConfig() {
|
|
|
183
183
|
return settings;
|
|
184
184
|
}
|
|
185
185
|
|
|
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
|
+
|
|
186
232
|
// Loop untuk Anthropic / 9router local (/v1/messages)
|
|
187
233
|
async function runAnthropicLoop(settings, convo) {
|
|
188
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.`;
|
|
@@ -205,6 +251,7 @@ async function runAnthropicLoop(settings, convo) {
|
|
|
205
251
|
body: JSON.stringify({
|
|
206
252
|
model: settings.model,
|
|
207
253
|
max_tokens: 4096,
|
|
254
|
+
stream: false,
|
|
208
255
|
system: SYSTEM,
|
|
209
256
|
messages: convo,
|
|
210
257
|
tools: TOOLS_ANTHROPIC
|
|
@@ -216,13 +263,13 @@ async function runAnthropicLoop(settings, convo) {
|
|
|
216
263
|
}
|
|
217
264
|
|
|
218
265
|
if (!res.ok) {
|
|
219
|
-
const
|
|
220
|
-
console.error(`\n[!] Error (${res.status}):`,
|
|
266
|
+
const errText = await res.text().catch(() => "");
|
|
267
|
+
console.error(`\n[!] Error (${res.status}):`, errText.slice(0, 200));
|
|
221
268
|
return;
|
|
222
269
|
}
|
|
223
270
|
|
|
224
|
-
const data = await res
|
|
225
|
-
const blocks = data
|
|
271
|
+
const data = await parseAnthropicResponse(res);
|
|
272
|
+
const blocks = data?.content || [];
|
|
226
273
|
const textBlock = blocks.find(b => b.type === "text");
|
|
227
274
|
if (textBlock?.text) {
|
|
228
275
|
process.stdout.write(`\r\x1b[2KKILUA > ${textBlock.text}\n`);
|
|
@@ -251,6 +298,28 @@ async function runAnthropicLoop(settings, convo) {
|
|
|
251
298
|
console.log();
|
|
252
299
|
}
|
|
253
300
|
|
|
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
|
+
|
|
254
323
|
// Loop untuk OpenAI / TokenHarbor / Gemini OpenAI protocol
|
|
255
324
|
async function runOpenAiLoop(settings, convo) {
|
|
256
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.`;
|
|
@@ -274,7 +343,7 @@ async function runOpenAiLoop(settings, convo) {
|
|
|
274
343
|
Authorization: `Bearer ${settings.api_key}`,
|
|
275
344
|
"x-goog-api-key": settings.api_key
|
|
276
345
|
},
|
|
277
|
-
body: JSON.stringify({ model: settings.model, messages: [{ role: "system", content: SYSTEM }, ...convo], tools: TOOLS_OPENAI, tool_choice: "auto" })
|
|
346
|
+
body: JSON.stringify({ model: settings.model, stream: false, messages: [{ role: "system", content: SYSTEM }, ...convo], tools: TOOLS_OPENAI, tool_choice: "auto" })
|
|
278
347
|
});
|
|
279
348
|
} catch (e) {
|
|
280
349
|
console.error("\n[!] Connection error:", e.message);
|
|
@@ -282,13 +351,13 @@ async function runOpenAiLoop(settings, convo) {
|
|
|
282
351
|
}
|
|
283
352
|
|
|
284
353
|
if (!res.ok) {
|
|
285
|
-
const
|
|
286
|
-
console.error(`\n[!] Error (${res.status}):`,
|
|
354
|
+
const errText = await res.text().catch(() => "");
|
|
355
|
+
console.error(`\n[!] Error (${res.status}):`, errText.slice(0, 200));
|
|
287
356
|
return;
|
|
288
357
|
}
|
|
289
358
|
|
|
290
|
-
const data = await res
|
|
291
|
-
const message = data
|
|
359
|
+
const data = await parseOpenAiResponse(res);
|
|
360
|
+
const message = data?.choices?.[0]?.message;
|
|
292
361
|
if (!message) break;
|
|
293
362
|
convo.push(message);
|
|
294
363
|
|