@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.
Files changed (2) hide show
  1. package/bin/kilua.mjs +92 -23
  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 OpenAI, Gemini OpenAI endpoint & Anthropic/9router.
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) TokenHarbor / 9Router / OpenRouter (OpenAI-compatible)");
137
- console.log("2) Google Gemini (AI Studio Key)");
138
- console.log("3) 9Router Local / Claude Code Proxy (Port 20128)\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
140
  const choice = (await promptQuestion(rl, "Pilihan [1/2/3] (default 1): ")).trim() || "1";
141
141
  let settings = {};
142
142
 
143
- if (choice === "2") {
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 err = await res.json().catch(() => ({}));
220
- 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));
221
268
  return;
222
269
  }
223
270
 
224
- const data = await res.json();
225
- const blocks = data.content || [];
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 err = await res.json().catch(() => ({}));
286
- 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));
287
356
  return;
288
357
  }
289
358
 
290
- const data = await res.json();
291
- const message = data.choices?.[0]?.message;
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
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gilangranggaid/kilua-agent",
3
- "version": "1.0.2",
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",