@gilangranggaid/kilua-agent 1.0.0 → 1.0.2

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 +171 -27
  2. package/package.json +3 -3
package/bin/kilua.mjs CHANGED
@@ -5,9 +5,10 @@ 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 CLI for zero-dep npx distribution. Split to modules if exceeds 500 lines.
8
+ // ponytail: single-file agent CLI supporting OpenAI, Gemini OpenAI endpoint & Anthropic/9router.
9
9
  const KILUA_DIR = path.join(os.homedir(), ".kilua");
10
10
  const SETTINGS_PATH = path.join(KILUA_DIR, "settings.json");
11
+ const CLAUDE_SETTINGS_PATH = path.join(os.homedir(), ".claude", "settings.json");
11
12
 
12
13
  function ensureKiluaDir() {
13
14
  if (!fs.existsSync(KILUA_DIR)) fs.mkdirSync(KILUA_DIR, { recursive: true });
@@ -23,7 +24,23 @@ function saveSettings(settings) {
23
24
  fs.writeFileSync(SETTINGS_PATH, JSON.stringify(settings, null, 2), "utf8");
24
25
  }
25
26
 
26
- const TOOLS = [
27
+ function readClaudeSettings() {
28
+ try {
29
+ const raw = JSON.parse(fs.readFileSync(CLAUDE_SETTINGS_PATH, "utf8"));
30
+ const env = raw.env || {};
31
+ if (env.ANTHROPIC_BASE_URL || env.ANTHROPIC_AUTH_TOKEN) {
32
+ return {
33
+ base_url: env.ANTHROPIC_BASE_URL || "http://127.0.0.1:20128/v1",
34
+ api_key: env.ANTHROPIC_AUTH_TOKEN || "",
35
+ model: env.ANTHROPIC_DEFAULT_OPUS_MODEL || env.ANTHROPIC_DEFAULT_SONNET_MODEL || "harobor",
36
+ provider: "anthropic"
37
+ };
38
+ }
39
+ } catch {}
40
+ return null;
41
+ }
42
+
43
+ const TOOLS_OPENAI = [
27
44
  {
28
45
  type: "function",
29
46
  function: {
@@ -66,6 +83,12 @@ const TOOLS = [
66
83
  }
67
84
  ];
68
85
 
86
+ const TOOLS_ANTHROPIC = TOOLS_OPENAI.map(t => ({
87
+ name: t.function.name,
88
+ description: t.function.description,
89
+ input_schema: t.function.parameters
90
+ }));
91
+
69
92
  function executeLocalTool(name, args) {
70
93
  const cwd = process.cwd();
71
94
  try {
@@ -107,40 +130,151 @@ function promptQuestion(rl, query) {
107
130
 
108
131
  async function runInteractiveConfig() {
109
132
  const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
133
+ const claudeCfg = readClaudeSettings();
134
+
110
135
  console.log("\x1b[35m=== KILUA CONFIG ===\x1b[0m\nPath: " + SETTINGS_PATH);
111
- console.log("1) TokenHarbor / 9Router / OpenAI Compatible (Rekomendasi)\n2) Gemini AI Studio\n");
112
- const choice = (await promptQuestion(rl, "Pilihan [1/2] (1): ")).trim() || "1";
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");
139
+
140
+ const choice = (await promptQuestion(rl, "Pilihan [1/2/3] (default 1): ")).trim() || "1";
113
141
  let settings = {};
114
142
 
115
143
  if (choice === "2") {
116
- const key = (await promptQuestion(rl, "GEMINI_API_KEY: ")).trim();
117
- const model = (await promptQuestion(rl, "Model (gemini-2.5-flash): ")).trim() || "gemini-2.5-flash";
118
- settings = { provider: "gemini", api_key: key, base_url: "https://generativelanguage.googleapis.com/v1beta", model };
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") {
153
+ const defaultUrl = claudeCfg?.base_url || "http://127.0.0.1:20128/v1";
154
+ const defaultKey = claudeCfg?.api_key || "";
155
+ const defaultModel = claudeCfg?.model || "harobor";
156
+
157
+ const baseUrl = (await promptQuestion(rl, `Base URL (${defaultUrl}): `)).trim() || defaultUrl;
158
+ const key = (await promptQuestion(rl, `API Key/Token (${defaultKey ? defaultKey.slice(0, 10) + "..." : "masukkan token"}): `)).trim() || defaultKey;
159
+ const model = (await promptQuestion(rl, `Model (${defaultModel}): `)).trim() || defaultModel;
160
+
161
+ settings = {
162
+ provider: "anthropic",
163
+ base_url: baseUrl.replace(/\/+$/, ""),
164
+ api_key: key,
165
+ model
166
+ };
119
167
  } else {
168
+ console.log("Contoh Base URL: https://tokenharbor.ai/v1 atau https://api.9router.com/v1");
120
169
  const baseUrl = (await promptQuestion(rl, "Base URL (https://tokenharbor.ai/v1): ")).trim() || "https://tokenharbor.ai/v1";
121
170
  const key = (await promptQuestion(rl, "API Key: ")).trim();
122
171
  const model = (await promptQuestion(rl, "Model (deepseek-v4.1-flash:free): ")).trim() || "deepseek-v4.1-flash:free";
123
- settings = { provider: "openai", 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
+ };
124
178
  }
125
179
 
126
180
  saveSettings(settings);
127
181
  rl.close();
128
- console.log("\x1b[32m✓ Saved to " + SETTINGS_PATH + "\x1b[0m\n");
182
+ console.log("\x1b[32m✓ Pengaturan disimpan di " + SETTINGS_PATH + "\x1b[0m\n");
129
183
  return settings;
130
184
  }
131
185
 
132
- async function runAgent(settings, userPrompt, convo) {
186
+ // Loop untuk Anthropic / 9router local (/v1/messages)
187
+ async function runAnthropicLoop(settings, convo) {
133
188
  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.`;
134
- convo.push({ role: "user", content: userPrompt });
189
+
190
+ let endpoint = settings.base_url.replace(/\/+$/, "");
191
+ if (!endpoint.endsWith("/messages")) endpoint += "/messages";
135
192
 
136
193
  for (let step = 0; step < 12; step++) {
137
194
  process.stdout.write("\r\x1b[2K\x1b[35m[KILUA Berpikir...]\x1b[0m ");
138
195
  let res;
139
196
  try {
140
- res = await fetch(`${settings.base_url}/chat/completions`, {
197
+ res = await fetch(endpoint, {
141
198
  method: "POST",
142
- headers: { "Content-Type": "application/json", Authorization: `Bearer ${settings.api_key}` },
143
- body: JSON.stringify({ model: settings.model, messages: [{ role: "system", content: SYSTEM }, ...convo], tools: TOOLS, tool_choice: "auto" })
199
+ headers: {
200
+ "Content-Type": "application/json",
201
+ "x-api-key": settings.api_key,
202
+ Authorization: `Bearer ${settings.api_key}`,
203
+ "anthropic-version": "2023-06-01"
204
+ },
205
+ body: JSON.stringify({
206
+ model: settings.model,
207
+ max_tokens: 4096,
208
+ system: SYSTEM,
209
+ messages: convo,
210
+ tools: TOOLS_ANTHROPIC
211
+ })
212
+ });
213
+ } catch (e) {
214
+ console.error("\n[!] Connection error:", e.message);
215
+ return;
216
+ }
217
+
218
+ if (!res.ok) {
219
+ const err = await res.json().catch(() => ({}));
220
+ console.error(`\n[!] Error (${res.status}):`, err.error?.message || err.message || res.statusText);
221
+ return;
222
+ }
223
+
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
+ }
230
+
231
+ convo.push({ role: "assistant", content: blocks });
232
+
233
+ const toolUseBlocks = blocks.filter(b => b.type === "tool_use");
234
+ if (!toolUseBlocks.length) break;
235
+
236
+ const toolResultContent = [];
237
+ for (const tu of toolUseBlocks) {
238
+ const name = tu.name;
239
+ const args = tu.input || {};
240
+ console.log(`\x1b[36m⚡ Tool: ${name}\x1b[0m(${JSON.stringify(args)})`);
241
+ const output = executeLocalTool(name, args);
242
+ console.log(`\x1b[90m↳ ${output.slice(0, 140).replace(/\n/g, " ")}${output.length > 140 ? "..." : ""}\x1b[0m`);
243
+ toolResultContent.push({
244
+ type: "tool_result",
245
+ tool_use_id: tu.id,
246
+ content: output
247
+ });
248
+ }
249
+ convo.push({ role: "user", content: toolResultContent });
250
+ }
251
+ console.log();
252
+ }
253
+
254
+ // Loop untuk OpenAI / TokenHarbor / Gemini OpenAI protocol
255
+ async function runOpenAiLoop(settings, convo) {
256
+ 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.`;
257
+
258
+ let endpoint = settings.base_url.replace(/\/+$/, "");
259
+ if (endpoint.includes("generativelanguage.googleapis.com") && !endpoint.includes("/openai")) {
260
+ endpoint += "/openai";
261
+ }
262
+ if (!endpoint.endsWith("/chat/completions")) {
263
+ endpoint += "/chat/completions";
264
+ }
265
+
266
+ for (let step = 0; step < 12; step++) {
267
+ process.stdout.write("\r\x1b[2K\x1b[35m[KILUA Berpikir...]\x1b[0m ");
268
+ let res;
269
+ try {
270
+ res = await fetch(endpoint, {
271
+ method: "POST",
272
+ headers: {
273
+ "Content-Type": "application/json",
274
+ Authorization: `Bearer ${settings.api_key}`,
275
+ "x-goog-api-key": settings.api_key
276
+ },
277
+ body: JSON.stringify({ model: settings.model, messages: [{ role: "system", content: SYSTEM }, ...convo], tools: TOOLS_OPENAI, tool_choice: "auto" })
144
278
  });
145
279
  } catch (e) {
146
280
  console.error("\n[!] Connection error:", e.message);
@@ -176,6 +310,17 @@ async function runAgent(settings, userPrompt, convo) {
176
310
  console.log();
177
311
  }
178
312
 
313
+ async function runAgent(settings, userPrompt, convo) {
314
+ const isAnthropic = settings.provider === "anthropic" || settings.base_url.includes(":20128") || settings.model === "harobor";
315
+ if (isAnthropic) {
316
+ convo.push({ role: "user", content: userPrompt });
317
+ await runAnthropicLoop(settings, convo);
318
+ } else {
319
+ convo.push({ role: "user", content: userPrompt });
320
+ await runOpenAiLoop(settings, convo);
321
+ }
322
+ }
323
+
179
324
  async function main() {
180
325
  const args = process.argv.slice(2);
181
326
  const oneShot = args.join(" ").trim();
@@ -186,19 +331,17 @@ async function main() {
186
331
  }
187
332
 
188
333
  let settings = loadSettings();
189
- if (process.env.TOKEN_HARBOR_API_KEY) {
190
- settings.api_key = process.env.TOKEN_HARBOR_API_KEY;
191
- settings.base_url = process.env.TOKEN_HARBOR_BASE_URL || "https://tokenharbor.ai/v1";
192
- settings.model = process.env.TOKEN_HARBOR_MODEL || "deepseek-v4.1-flash:free";
193
- } else if (process.env.KILUA_API_KEY) {
194
- settings.api_key = process.env.KILUA_API_KEY;
195
- settings.base_url = process.env.KILUA_BASE_URL || settings.base_url || "https://tokenharbor.ai/v1";
196
- settings.model = process.env.KILUA_MODEL || settings.model || "deepseek-v4.1-flash:free";
197
- }
198
334
 
335
+ // Auto-import dari Claude Code jika settings kilua belum ada
199
336
  if (!settings.api_key) {
200
- console.log("\x1b[33m[!] Belum ada API key terpasang.\x1b[0m");
201
- settings = await runInteractiveConfig();
337
+ const claudeCfg = readClaudeSettings();
338
+ if (claudeCfg) {
339
+ settings = claudeCfg;
340
+ saveSettings(settings);
341
+ } else {
342
+ console.log("\x1b[33m[!] Belum ada konfigurasi AI key.\x1b[0m");
343
+ settings = await runInteractiveConfig();
344
+ }
202
345
  }
203
346
 
204
347
  const convo = [];
@@ -210,9 +353,9 @@ async function main() {
210
353
  console.log("\x1b[1m\x1b[35m=== KILUA AI CLI AGENT ===\x1b[0m");
211
354
  console.log(`CWD : \x1b[33m${process.cwd()}\x1b[0m`);
212
355
  console.log(`Provider: \x1b[32m${settings.base_url}\x1b[0m (${settings.model})`);
213
- console.log(`Config : \x1b[90m${SETTINGS_PATH}\x1b[0m (ketik 'config' untuk ganti key/provider)\n`);
356
+ console.log(`Config : \x1b[90m${SETTINGS_PATH}\x1b[0m (ketik 'config' untuk ganti)\n`);
214
357
 
215
- const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
358
+ let rl = readline.createInterface({ input: process.stdin, output: process.stdout });
216
359
  const ask = () => {
217
360
  rl.question("\x1b[1mKAMU > \x1b[0m", async input => {
218
361
  const q = input.trim();
@@ -222,6 +365,7 @@ async function main() {
222
365
  rl.close();
223
366
  await runInteractiveConfig();
224
367
  settings = loadSettings();
368
+ rl = readline.createInterface({ input: process.stdin, output: process.stdout });
225
369
  return ask();
226
370
  }
227
371
  await runAgent(settings, q, convo);
package/package.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@gilangranggaid/kilua-agent",
3
- "version": "1.0.0",
3
+ "version": "1.0.2",
4
4
  "description": "Autonomous AI Coding Agent for your terminal and VS Code",
5
5
  "type": "module",
6
6
  "main": "bin/kilua.mjs",
7
7
  "bin": {
8
- "kilua": "bin/kilua.mjs",
9
- "kilua-agent": "bin/kilua.mjs"
8
+ "kilua": "./bin/kilua.mjs",
9
+ "kilua-agent": "./bin/kilua.mjs"
10
10
  },
11
11
  "files": [
12
12
  "bin",