@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.
Files changed (2) hide show
  1. package/bin/kilua.mjs +125 -31
  2. 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 supporting both OpenAI & Anthropic (9router) protocols.
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 Compatible");
138
- console.log("3) Custom Provider\n");
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) + "..." : "kosongkan jika sama"}): `)).trim() || defaultKey;
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 === "2") {
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 = { provider: "openai", api_key: key, base_url: baseUrl.replace(/\/+$/, ""), model };
163
- } else {
164
- const provider = (await promptQuestion(rl, "Tipe API [openai/anthropic] (openai): ")).trim() || "openai";
165
- const baseUrl = (await promptQuestion(rl, "Base URL: ")).trim();
166
- const key = (await promptQuestion(rl, "API Key: ")).trim();
167
- const model = (await promptQuestion(rl, "Model: ")).trim();
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✓ Saved to " + SETTINGS_PATH + "\x1b[0m\n");
182
+ console.log("\x1b[32m✓ Pengaturan disimpan di " + SETTINGS_PATH + "\x1b[0m\n");
174
183
  return settings;
175
184
  }
176
185
 
177
- // Loop untuk Anthropic / 9router protocol (/v1/messages)
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(`${settings.base_url}/messages`, {
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 err = await res.json().catch(() => ({}));
208
- console.error(`\n[!] Error (${res.status}):`, err.error?.message || err.message || res.statusText);
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.json();
213
- const blocks = data.content || [];
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
- // Loop untuk OpenAI / TokenHarbor protocol (/v1/chat/completions)
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(`${settings.base_url}/chat/completions`, {
339
+ res = await fetch(endpoint, {
251
340
  method: "POST",
252
- headers: { "Content-Type": "application/json", Authorization: `Bearer ${settings.api_key}` },
253
- body: JSON.stringify({ model: settings.model, messages: [{ role: "system", content: SYSTEM }, ...convo], tools: TOOLS_OPENAI, tool_choice: "auto" })
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 err = await res.json().catch(() => ({}));
262
- console.error(`\n[!] Error (${res.status}):`, err.error?.message || err.message || res.statusText);
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.json();
267
- const message = data.choices?.[0]?.message;
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}) [${settings.provider || "openai"}]`);
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
- const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
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);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gilangranggaid/kilua-agent",
3
- "version": "1.0.1",
3
+ "version": "1.0.3",
4
4
  "description": "Autonomous AI Coding Agent for your terminal and VS Code",
5
5
  "type": "module",
6
6
  "main": "bin/kilua.mjs",