@gilangranggaid/kilua-agent 1.0.2 → 1.0.4

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 +139 -38
  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 supporting real-time streaming 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,8 +155,17 @@ 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
- console.log("Contoh Base URL: https://tokenharbor.ai/v1 atau https://api.9router.com/v1");
168
+ console.log("Contoh Base URL: https://tokenharbor.ai/v1");
169
169
  const baseUrl = (await promptQuestion(rl, "Base URL (https://tokenharbor.ai/v1): ")).trim() || "https://tokenharbor.ai/v1";
170
170
  const key = (await promptQuestion(rl, "API Key: ")).trim();
171
171
  const model = (await promptQuestion(rl, "Model (deepseek-v4.1-flash:free): ")).trim() || "deepseek-v4.1-flash:free";
@@ -183,6 +183,41 @@ async function runInteractiveConfig() {
183
183
  return settings;
184
184
  }
185
185
 
186
+ // Stream reader universal untuk SSE (Anthropic & 9router)
187
+ async function readSseStream(res, onChunk) {
188
+ const reader = res.body.getReader();
189
+ const decoder = new TextDecoder("utf-8");
190
+ let buffer = "";
191
+
192
+ while (true) {
193
+ const { done, value } = await reader.read();
194
+ if (done) break;
195
+ buffer += decoder.decode(value, { stream: true });
196
+ const lines = buffer.split("\n");
197
+ buffer = lines.pop() || "";
198
+
199
+ for (const rawLine of lines) {
200
+ const line = rawLine.trim();
201
+ if (!line || line.startsWith("event:") || line.startsWith(":")) continue;
202
+ if (line.startsWith("data:")) {
203
+ const jsonStr = line.slice(5).trim();
204
+ if (!jsonStr || jsonStr === "[DONE]") continue;
205
+ try {
206
+ const parsed = JSON.parse(jsonStr);
207
+ onChunk(parsed);
208
+ } catch {}
209
+ }
210
+ }
211
+ }
212
+
213
+ if (buffer.trim().startsWith("data:")) {
214
+ try {
215
+ const parsed = JSON.parse(buffer.trim().slice(5).trim());
216
+ onChunk(parsed);
217
+ } catch {}
218
+ }
219
+ }
220
+
186
221
  // Loop untuk Anthropic / 9router local (/v1/messages)
187
222
  async function runAnthropicLoop(settings, convo) {
188
223
  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 +240,7 @@ async function runAnthropicLoop(settings, convo) {
205
240
  body: JSON.stringify({
206
241
  model: settings.model,
207
242
  max_tokens: 4096,
243
+ stream: true,
208
244
  system: SYSTEM,
209
245
  messages: convo,
210
246
  tools: TOOLS_ANTHROPIC
@@ -216,25 +252,63 @@ async function runAnthropicLoop(settings, convo) {
216
252
  }
217
253
 
218
254
  if (!res.ok) {
219
- const err = await res.json().catch(() => ({}));
220
- console.error(`\n[!] Error (${res.status}):`, err.error?.message || err.message || res.statusText);
255
+ const errText = await res.text().catch(() => "");
256
+ console.error(`\n[!] Error (${res.status}):`, errText.slice(0, 200));
221
257
  return;
222
258
  }
223
259
 
224
- const data = await res.json();
225
- const blocks = data.content || [];
226
- const textBlock = blocks.find(b => b.type === "text");
227
- if (textBlock?.text) {
228
- process.stdout.write(`\r\x1b[2KKILUA > ${textBlock.text}\n`);
229
- }
260
+ let printedHeader = false;
261
+ let fullText = "";
262
+ const toolBlocks = [];
263
+
264
+ await readSseStream(res, (evt) => {
265
+ // 1. Text deltas
266
+ let deltaText = "";
267
+ if (typeof evt.delta?.text === "string") deltaText = evt.delta.text;
268
+ else if (typeof evt.delta?.content === "string") deltaText = evt.delta.content;
269
+ else if (evt.choices?.[0]?.delta?.content) deltaText = evt.choices[0].delta.content;
270
+ else if (evt.content_block?.text) deltaText = evt.content_block.text;
271
+
272
+ if (deltaText) {
273
+ if (!printedHeader) {
274
+ process.stdout.write("\r\x1b[2KKILUA > ");
275
+ printedHeader = true;
276
+ }
277
+ process.stdout.write(deltaText);
278
+ fullText += deltaText;
279
+ }
280
+
281
+ // 2. Tool deltas
282
+ if (evt.type === "content_block_start" && evt.content_block?.type === "tool_use") {
283
+ toolBlocks[evt.index] = { ...evt.content_block, _raw_json: "" };
284
+ } else if (evt.delta?.type === "input_json_delta") {
285
+ const idx = evt.index;
286
+ if (toolBlocks[idx]) {
287
+ toolBlocks[idx]._raw_json = (toolBlocks[idx]._raw_json || "") + (evt.delta.partial_json || "");
288
+ }
289
+ } else if (evt.type === "content_block_stop") {
290
+ const idx = evt.index;
291
+ if (toolBlocks[idx]?._raw_json) {
292
+ try { toolBlocks[idx].input = JSON.parse(toolBlocks[idx]._raw_json); } catch {}
293
+ delete toolBlocks[idx]._raw_json;
294
+ }
295
+ }
296
+ });
297
+
298
+ if (printedHeader) console.log();
299
+ else process.stdout.write("\r\x1b[2K");
300
+
301
+ const activeTools = toolBlocks.filter(Boolean);
302
+ const assistantBlocks = [];
303
+ if (fullText) assistantBlocks.push({ type: "text", text: fullText });
304
+ for (const tb of activeTools) assistantBlocks.push(tb);
230
305
 
231
- convo.push({ role: "assistant", content: blocks });
306
+ convo.push({ role: "assistant", content: assistantBlocks.length ? assistantBlocks : fullText });
232
307
 
233
- const toolUseBlocks = blocks.filter(b => b.type === "tool_use");
234
- if (!toolUseBlocks.length) break;
308
+ if (!activeTools.length) break;
235
309
 
236
310
  const toolResultContent = [];
237
- for (const tu of toolUseBlocks) {
311
+ for (const tu of activeTools) {
238
312
  const name = tu.name;
239
313
  const args = tu.input || {};
240
314
  console.log(`\x1b[36m⚡ Tool: ${name}\x1b[0m(${JSON.stringify(args)})`);
@@ -248,7 +322,6 @@ async function runAnthropicLoop(settings, convo) {
248
322
  }
249
323
  convo.push({ role: "user", content: toolResultContent });
250
324
  }
251
- console.log();
252
325
  }
253
326
 
254
327
  // Loop untuk OpenAI / TokenHarbor / Gemini OpenAI protocol
@@ -274,7 +347,13 @@ async function runOpenAiLoop(settings, convo) {
274
347
  Authorization: `Bearer ${settings.api_key}`,
275
348
  "x-goog-api-key": settings.api_key
276
349
  },
277
- body: JSON.stringify({ model: settings.model, messages: [{ role: "system", content: SYSTEM }, ...convo], tools: TOOLS_OPENAI, tool_choice: "auto" })
350
+ body: JSON.stringify({
351
+ model: settings.model,
352
+ stream: true,
353
+ messages: [{ role: "system", content: SYSTEM }, ...convo],
354
+ tools: TOOLS_OPENAI,
355
+ tool_choice: "auto"
356
+ })
278
357
  });
279
358
  } catch (e) {
280
359
  console.error("\n[!] Connection error:", e.message);
@@ -282,19 +361,42 @@ async function runOpenAiLoop(settings, convo) {
282
361
  }
283
362
 
284
363
  if (!res.ok) {
285
- const err = await res.json().catch(() => ({}));
286
- console.error(`\n[!] Error (${res.status}):`, err.error?.message || err.message || res.statusText);
364
+ const errText = await res.text().catch(() => "");
365
+ console.error(`\n[!] Error (${res.status}):`, errText.slice(0, 200));
287
366
  return;
288
367
  }
289
368
 
290
- const data = await res.json();
291
- const message = data.choices?.[0]?.message;
292
- if (!message) break;
293
- convo.push(message);
369
+ let printedHeader = false;
370
+ let fullText = "";
371
+ const toolCallsMap = {};
372
+
373
+ await readSseStream(res, (evt) => {
374
+ const choice = evt.choices?.[0];
375
+ const delta = choice?.delta;
376
+ if (delta?.content) {
377
+ if (!printedHeader) {
378
+ process.stdout.write("\r\x1b[2KKILUA > ");
379
+ printedHeader = true;
380
+ }
381
+ process.stdout.write(delta.content);
382
+ fullText += delta.content;
383
+ }
384
+ if (delta?.tool_calls) {
385
+ for (const tc of delta.tool_calls) {
386
+ const idx = tc.index ?? 0;
387
+ if (!toolCallsMap[idx]) toolCallsMap[idx] = { id: tc.id, function: { name: tc.function?.name || "", arguments: "" } };
388
+ if (tc.function?.name) toolCallsMap[idx].function.name = tc.function.name;
389
+ if (tc.function?.arguments) toolCallsMap[idx].function.arguments += tc.function.arguments;
390
+ }
391
+ }
392
+ });
393
+
394
+ if (printedHeader) console.log();
395
+ else process.stdout.write("\r\x1b[2K");
294
396
 
295
- if (message.content) process.stdout.write(`\r\x1b[2KKILUA > ${message.content}\n`);
397
+ const toolCalls = Object.values(toolCallsMap);
398
+ convo.push({ role: "assistant", content: fullText, tool_calls: toolCalls.length ? toolCalls : undefined });
296
399
 
297
- const toolCalls = message.tool_calls || [];
298
400
  if (!toolCalls.length) break;
299
401
 
300
402
  for (const tc of toolCalls) {
@@ -307,7 +409,6 @@ async function runOpenAiLoop(settings, convo) {
307
409
  convo.push({ role: "tool", tool_call_id: tc.id, name, content: output });
308
410
  }
309
411
  }
310
- console.log();
311
412
  }
312
413
 
313
414
  async function runAgent(settings, userPrompt, convo) {
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.4",
4
4
  "description": "Autonomous AI Coding Agent for your terminal and VS Code",
5
5
  "type": "module",
6
6
  "main": "bin/kilua.mjs",