@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.
- package/bin/kilua.mjs +139 -38
- 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
|
|
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)
|
|
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,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
|
|
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
|
|
220
|
-
console.error(`\n[!] Error (${res.status}):`,
|
|
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
|
-
|
|
225
|
-
|
|
226
|
-
const
|
|
227
|
-
|
|
228
|
-
|
|
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:
|
|
306
|
+
convo.push({ role: "assistant", content: assistantBlocks.length ? assistantBlocks : fullText });
|
|
232
307
|
|
|
233
|
-
|
|
234
|
-
if (!toolUseBlocks.length) break;
|
|
308
|
+
if (!activeTools.length) break;
|
|
235
309
|
|
|
236
310
|
const toolResultContent = [];
|
|
237
|
-
for (const tu of
|
|
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({
|
|
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
|
|
286
|
-
console.error(`\n[!] Error (${res.status}):`,
|
|
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
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
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
|
-
|
|
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) {
|