@gilangranggaid/kilua-agent 1.0.0 → 1.0.1

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 +144 -25
  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 both OpenAI & Anthropic (9router) protocols.
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,20 +130,42 @@ 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) 9Router / Claude Code Proxy (Port 20128)");
137
+ console.log("2) TokenHarbor / OpenAI Compatible");
138
+ console.log("3) Custom Provider\n");
139
+
140
+ const choice = (await promptQuestion(rl, "Pilihan [1/2/3] (1): ")).trim() || "1";
113
141
  let settings = {};
114
142
 
115
- 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 };
119
- } else {
143
+ if (choice === "1") {
144
+ const defaultUrl = claudeCfg?.base_url || "http://127.0.0.1:20128/v1";
145
+ const defaultKey = claudeCfg?.api_key || "";
146
+ const defaultModel = claudeCfg?.model || "harobor";
147
+
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;
150
+ const model = (await promptQuestion(rl, `Model (${defaultModel}): `)).trim() || defaultModel;
151
+
152
+ settings = {
153
+ provider: "anthropic",
154
+ base_url: baseUrl.replace(/\/+$/, ""),
155
+ api_key: key,
156
+ model
157
+ };
158
+ } else if (choice === "2") {
120
159
  const baseUrl = (await promptQuestion(rl, "Base URL (https://tokenharbor.ai/v1): ")).trim() || "https://tokenharbor.ai/v1";
121
160
  const key = (await promptQuestion(rl, "API Key: ")).trim();
122
161
  const model = (await promptQuestion(rl, "Model (deepseek-v4.1-flash:free): ")).trim() || "deepseek-v4.1-flash:free";
123
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 };
124
169
  }
125
170
 
126
171
  saveSettings(settings);
@@ -129,9 +174,74 @@ async function runInteractiveConfig() {
129
174
  return settings;
130
175
  }
131
176
 
132
- async function runAgent(settings, userPrompt, convo) {
177
+ // Loop untuk Anthropic / 9router protocol (/v1/messages)
178
+ async function runAnthropicLoop(settings, convo) {
179
+ 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
+
181
+ for (let step = 0; step < 12; step++) {
182
+ process.stdout.write("\r\x1b[2K\x1b[35m[KILUA Berpikir...]\x1b[0m ");
183
+ let res;
184
+ try {
185
+ res = await fetch(`${settings.base_url}/messages`, {
186
+ method: "POST",
187
+ headers: {
188
+ "Content-Type": "application/json",
189
+ "x-api-key": settings.api_key,
190
+ Authorization: `Bearer ${settings.api_key}`,
191
+ "anthropic-version": "2023-06-01"
192
+ },
193
+ body: JSON.stringify({
194
+ model: settings.model,
195
+ max_tokens: 4096,
196
+ system: SYSTEM,
197
+ messages: convo,
198
+ tools: TOOLS_ANTHROPIC
199
+ })
200
+ });
201
+ } catch (e) {
202
+ console.error("\n[!] Connection error:", e.message);
203
+ return;
204
+ }
205
+
206
+ if (!res.ok) {
207
+ const err = await res.json().catch(() => ({}));
208
+ console.error(`\n[!] Error (${res.status}):`, err.error?.message || err.message || res.statusText);
209
+ return;
210
+ }
211
+
212
+ const data = await res.json();
213
+ const blocks = data.content || [];
214
+ const textBlock = blocks.find(b => b.type === "text");
215
+ if (textBlock?.text) {
216
+ process.stdout.write(`\r\x1b[2KKILUA > ${textBlock.text}\n`);
217
+ }
218
+
219
+ convo.push({ role: "assistant", content: blocks });
220
+
221
+ const toolUseBlocks = blocks.filter(b => b.type === "tool_use");
222
+ if (!toolUseBlocks.length) break;
223
+
224
+ const toolResultContent = [];
225
+ for (const tu of toolUseBlocks) {
226
+ const name = tu.name;
227
+ const args = tu.input || {};
228
+ console.log(`\x1b[36m⚡ Tool: ${name}\x1b[0m(${JSON.stringify(args)})`);
229
+ const output = executeLocalTool(name, args);
230
+ console.log(`\x1b[90m↳ ${output.slice(0, 140).replace(/\n/g, " ")}${output.length > 140 ? "..." : ""}\x1b[0m`);
231
+ toolResultContent.push({
232
+ type: "tool_result",
233
+ tool_use_id: tu.id,
234
+ content: output
235
+ });
236
+ }
237
+ convo.push({ role: "user", content: toolResultContent });
238
+ }
239
+ console.log();
240
+ }
241
+
242
+ // Loop untuk OpenAI / TokenHarbor protocol (/v1/chat/completions)
243
+ async function runOpenAiLoop(settings, convo) {
133
244
  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 });
135
245
 
136
246
  for (let step = 0; step < 12; step++) {
137
247
  process.stdout.write("\r\x1b[2K\x1b[35m[KILUA Berpikir...]\x1b[0m ");
@@ -140,7 +250,7 @@ async function runAgent(settings, userPrompt, convo) {
140
250
  res = await fetch(`${settings.base_url}/chat/completions`, {
141
251
  method: "POST",
142
252
  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" })
253
+ body: JSON.stringify({ model: settings.model, messages: [{ role: "system", content: SYSTEM }, ...convo], tools: TOOLS_OPENAI, tool_choice: "auto" })
144
254
  });
145
255
  } catch (e) {
146
256
  console.error("\n[!] Connection error:", e.message);
@@ -176,6 +286,17 @@ async function runAgent(settings, userPrompt, convo) {
176
286
  console.log();
177
287
  }
178
288
 
289
+ async function runAgent(settings, userPrompt, convo) {
290
+ const isAnthropic = settings.provider === "anthropic" || settings.base_url.includes(":20128") || settings.model === "harobor";
291
+ if (isAnthropic) {
292
+ convo.push({ role: "user", content: userPrompt });
293
+ await runAnthropicLoop(settings, convo);
294
+ } else {
295
+ convo.push({ role: "user", content: userPrompt });
296
+ await runOpenAiLoop(settings, convo);
297
+ }
298
+ }
299
+
179
300
  async function main() {
180
301
  const args = process.argv.slice(2);
181
302
  const oneShot = args.join(" ").trim();
@@ -186,19 +307,17 @@ async function main() {
186
307
  }
187
308
 
188
309
  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
310
 
311
+ // Auto-import dari Claude Code jika settings kilua belum ada
199
312
  if (!settings.api_key) {
200
- console.log("\x1b[33m[!] Belum ada API key terpasang.\x1b[0m");
201
- settings = await runInteractiveConfig();
313
+ const claudeCfg = readClaudeSettings();
314
+ if (claudeCfg) {
315
+ settings = claudeCfg;
316
+ saveSettings(settings);
317
+ } else {
318
+ console.log("\x1b[33m[!] Belum ada konfigurasi AI key.\x1b[0m");
319
+ settings = await runInteractiveConfig();
320
+ }
202
321
  }
203
322
 
204
323
  const convo = [];
@@ -209,8 +328,8 @@ async function main() {
209
328
 
210
329
  console.log("\x1b[1m\x1b[35m=== KILUA AI CLI AGENT ===\x1b[0m");
211
330
  console.log(`CWD : \x1b[33m${process.cwd()}\x1b[0m`);
212
- 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`);
331
+ console.log(`Provider: \x1b[32m${settings.base_url}\x1b[0m (${settings.model}) [${settings.provider || "openai"}]`);
332
+ console.log(`Config : \x1b[90m${SETTINGS_PATH}\x1b[0m (ketik 'config' untuk ganti)\n`);
214
333
 
215
334
  const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
216
335
  const ask = () => {
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.1",
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",