@wanghuimvp/axon 0.4.2 → 0.5.0
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/LICENSE +21 -21
- package/README.md +10 -131
- package/bin/axon.exe +10 -0
- package/package.json +23 -63
- package/postinstall.mjs +189 -0
- package/dist/cli.js +0 -1266
package/dist/cli.js
DELETED
|
@@ -1,1266 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
// src/cli.ts
|
|
4
|
-
import { Command } from "commander";
|
|
5
|
-
|
|
6
|
-
// src/version.ts
|
|
7
|
-
var VERSION = "0.0.1";
|
|
8
|
-
|
|
9
|
-
// src/config/config.ts
|
|
10
|
-
import { readFileSync } from "node:fs";
|
|
11
|
-
import { homedir } from "node:os";
|
|
12
|
-
import { join } from "node:path";
|
|
13
|
-
var DEFAULT_MODELS = {
|
|
14
|
-
anthropic: "claude-opus-4-8",
|
|
15
|
-
openai: "gpt-4.1",
|
|
16
|
-
gemini: "gemini-2.5-pro"
|
|
17
|
-
};
|
|
18
|
-
var DEFAULTS = {
|
|
19
|
-
provider: "anthropic",
|
|
20
|
-
providers: {
|
|
21
|
-
anthropic: { apiKey: "env:ANTHROPIC_API_KEY" },
|
|
22
|
-
openai: { apiKey: "env:OPENAI_API_KEY" },
|
|
23
|
-
gemini: { apiKey: "env:GEMINI_API_KEY" }
|
|
24
|
-
}
|
|
25
|
-
};
|
|
26
|
-
var ENV_VARS = {
|
|
27
|
-
anthropic: "ANTHROPIC_API_KEY",
|
|
28
|
-
openai: "OPENAI_API_KEY",
|
|
29
|
-
gemini: "GEMINI_API_KEY"
|
|
30
|
-
};
|
|
31
|
-
function detectProvider(fileCfg) {
|
|
32
|
-
for (const name of ["anthropic", "openai", "gemini"]) {
|
|
33
|
-
const literal = fileCfg.providers?.[name]?.apiKey;
|
|
34
|
-
const hasLiteral = typeof literal === "string" && !literal.startsWith("env:") && literal.trim().length > 0;
|
|
35
|
-
const hasEnv = (process.env[ENV_VARS[name]] ?? "").trim().length > 0;
|
|
36
|
-
if (hasLiteral || hasEnv) return name;
|
|
37
|
-
}
|
|
38
|
-
return "anthropic";
|
|
39
|
-
}
|
|
40
|
-
function resolveModel(cfg) {
|
|
41
|
-
const model = cfg.model ?? cfg.providers[cfg.provider]?.model ?? DEFAULT_MODELS[cfg.provider];
|
|
42
|
-
if (!model) {
|
|
43
|
-
throw new Error(`No model configured for provider "${cfg.provider}". Set "model" in ~/.axon/config.json or pass --model.`);
|
|
44
|
-
}
|
|
45
|
-
return model;
|
|
46
|
-
}
|
|
47
|
-
function resolveEnvRefs(cfg) {
|
|
48
|
-
const providers = {};
|
|
49
|
-
for (const [name, p] of Object.entries(cfg.providers)) {
|
|
50
|
-
const apiKey = p.apiKey?.startsWith("env:") ? process.env[p.apiKey.slice(4)] : p.apiKey;
|
|
51
|
-
providers[name] = { ...p, apiKey };
|
|
52
|
-
}
|
|
53
|
-
return { ...cfg, providers };
|
|
54
|
-
}
|
|
55
|
-
function loadConfig() {
|
|
56
|
-
const path = join(homedir(), ".axon", "config.json");
|
|
57
|
-
let fileCfg = {};
|
|
58
|
-
try {
|
|
59
|
-
fileCfg = JSON.parse(readFileSync(path, "utf8"));
|
|
60
|
-
} catch {
|
|
61
|
-
}
|
|
62
|
-
const merged = {
|
|
63
|
-
provider: fileCfg.provider ?? detectProvider(fileCfg),
|
|
64
|
-
model: fileCfg.model,
|
|
65
|
-
providers: { ...DEFAULTS.providers, ...fileCfg.providers ?? {} }
|
|
66
|
-
};
|
|
67
|
-
return resolveEnvRefs(merged);
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
// src/config/configFile.ts
|
|
71
|
-
import { readFileSync as readFileSync2, writeFileSync, mkdirSync, chmodSync } from "node:fs";
|
|
72
|
-
import { homedir as homedir2 } from "node:os";
|
|
73
|
-
import { join as join2, dirname } from "node:path";
|
|
74
|
-
function configPath() {
|
|
75
|
-
return join2(homedir2(), ".axon", "config.json");
|
|
76
|
-
}
|
|
77
|
-
function readConfigFile() {
|
|
78
|
-
try {
|
|
79
|
-
return JSON.parse(readFileSync2(configPath(), "utf8"));
|
|
80
|
-
} catch {
|
|
81
|
-
return {};
|
|
82
|
-
}
|
|
83
|
-
}
|
|
84
|
-
function setConfigValue(key, value) {
|
|
85
|
-
const cfg = readConfigFile();
|
|
86
|
-
if (key === "provider" || key === "model") {
|
|
87
|
-
cfg[key] = value;
|
|
88
|
-
} else if (key.includes(".")) {
|
|
89
|
-
const [provider, field] = key.split(".", 2);
|
|
90
|
-
const ALLOWED = /* @__PURE__ */ new Set(["baseUrl", "model"]);
|
|
91
|
-
if (!ALLOWED.has(field)) {
|
|
92
|
-
throw new Error(`Cannot set "${field}" via config. Set API keys via the provider's env var (e.g. ANTHROPIC_API_KEY); only baseUrl and model are settable here.`);
|
|
93
|
-
}
|
|
94
|
-
const providers = cfg.providers ??= {};
|
|
95
|
-
(providers[provider] ??= {})[field] = value;
|
|
96
|
-
} else {
|
|
97
|
-
throw new Error(`Unknown config key "${key}". Use "provider", "model", or "<provider>.<baseUrl|model>".`);
|
|
98
|
-
}
|
|
99
|
-
const path = configPath();
|
|
100
|
-
mkdirSync(dirname(path), { recursive: true });
|
|
101
|
-
writeFileSync(path, JSON.stringify(cfg, null, 2) + "\n");
|
|
102
|
-
}
|
|
103
|
-
function setApiKey(provider, key) {
|
|
104
|
-
const cfg = readConfigFile();
|
|
105
|
-
const providers = cfg.providers ??= {};
|
|
106
|
-
(providers[provider] ??= {}).apiKey = key;
|
|
107
|
-
const path = configPath();
|
|
108
|
-
mkdirSync(dirname(path), { recursive: true });
|
|
109
|
-
writeFileSync(path, JSON.stringify(cfg, null, 2) + "\n", { mode: 384 });
|
|
110
|
-
try {
|
|
111
|
-
chmodSync(path, 384);
|
|
112
|
-
} catch {
|
|
113
|
-
}
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
// src/providers/registry.ts
|
|
117
|
-
import Anthropic from "@anthropic-ai/sdk";
|
|
118
|
-
import OpenAI from "openai";
|
|
119
|
-
import { GoogleGenAI } from "@google/genai";
|
|
120
|
-
|
|
121
|
-
// src/providers/anthropic.ts
|
|
122
|
-
function mapStop(reason) {
|
|
123
|
-
if (reason === "tool_use") return "tool_use";
|
|
124
|
-
if (reason === "max_tokens") return "max_tokens";
|
|
125
|
-
return "end";
|
|
126
|
-
}
|
|
127
|
-
function blockToAnthropic(b) {
|
|
128
|
-
if (b.type === "text") return { type: "text", text: b.text };
|
|
129
|
-
return { type: "tool_use", id: b.id, name: b.name, input: b.args };
|
|
130
|
-
}
|
|
131
|
-
function toAnthropicMessages(messages) {
|
|
132
|
-
const out = [];
|
|
133
|
-
let i = 0;
|
|
134
|
-
while (i < messages.length) {
|
|
135
|
-
const m = messages[i];
|
|
136
|
-
if (m.role === "tool") {
|
|
137
|
-
const results = [];
|
|
138
|
-
while (i < messages.length && messages[i].role === "tool") {
|
|
139
|
-
const t = messages[i];
|
|
140
|
-
results.push({ type: "tool_result", tool_use_id: t.toolCallId, content: t.content });
|
|
141
|
-
i++;
|
|
142
|
-
}
|
|
143
|
-
out.push({ role: "user", content: results });
|
|
144
|
-
} else {
|
|
145
|
-
out.push({ role: m.role, content: m.content.map(blockToAnthropic) });
|
|
146
|
-
i++;
|
|
147
|
-
}
|
|
148
|
-
}
|
|
149
|
-
return out;
|
|
150
|
-
}
|
|
151
|
-
function toAnthropicTool(t) {
|
|
152
|
-
return { name: t.name, description: t.description, input_schema: t.parameters };
|
|
153
|
-
}
|
|
154
|
-
var AnthropicProvider = class {
|
|
155
|
-
constructor(deps) {
|
|
156
|
-
this.deps = deps;
|
|
157
|
-
}
|
|
158
|
-
async *stream(req) {
|
|
159
|
-
const stream = await this.deps.client.messages.create({
|
|
160
|
-
model: this.deps.model,
|
|
161
|
-
max_tokens: 4096,
|
|
162
|
-
stream: true,
|
|
163
|
-
system: req.system,
|
|
164
|
-
messages: toAnthropicMessages(req.messages),
|
|
165
|
-
tools: req.tools.map(toAnthropicTool)
|
|
166
|
-
});
|
|
167
|
-
const toolBlocks = /* @__PURE__ */ new Map();
|
|
168
|
-
let stopReason = "end";
|
|
169
|
-
for await (const ev of stream) {
|
|
170
|
-
switch (ev.type) {
|
|
171
|
-
case "content_block_start":
|
|
172
|
-
if (ev.content_block?.type === "tool_use") {
|
|
173
|
-
toolBlocks.set(ev.index, { id: ev.content_block.id, name: ev.content_block.name, json: "" });
|
|
174
|
-
}
|
|
175
|
-
break;
|
|
176
|
-
case "content_block_delta":
|
|
177
|
-
if (ev.delta?.type === "text_delta") {
|
|
178
|
-
yield { type: "text_delta", text: ev.delta.text };
|
|
179
|
-
} else if (ev.delta?.type === "input_json_delta") {
|
|
180
|
-
const t = toolBlocks.get(ev.index);
|
|
181
|
-
if (t) t.json += ev.delta.partial_json;
|
|
182
|
-
}
|
|
183
|
-
break;
|
|
184
|
-
case "content_block_stop": {
|
|
185
|
-
const t = toolBlocks.get(ev.index);
|
|
186
|
-
if (t) {
|
|
187
|
-
let args;
|
|
188
|
-
try {
|
|
189
|
-
args = t.json.trim() ? JSON.parse(t.json) : {};
|
|
190
|
-
} catch (err) {
|
|
191
|
-
throw new Error(`Tool "${t.name}" (id=${t.id}): invalid tool-call JSON: ${JSON.stringify(t.json)}`, { cause: err });
|
|
192
|
-
}
|
|
193
|
-
yield { type: "tool_call", id: t.id, name: t.name, args };
|
|
194
|
-
toolBlocks.delete(ev.index);
|
|
195
|
-
}
|
|
196
|
-
break;
|
|
197
|
-
}
|
|
198
|
-
case "message_delta":
|
|
199
|
-
stopReason = mapStop(ev.delta?.stop_reason);
|
|
200
|
-
break;
|
|
201
|
-
case "message_stop":
|
|
202
|
-
yield { type: "done", stopReason };
|
|
203
|
-
break;
|
|
204
|
-
}
|
|
205
|
-
}
|
|
206
|
-
}
|
|
207
|
-
};
|
|
208
|
-
|
|
209
|
-
// src/providers/openai.ts
|
|
210
|
-
function textOf(blocks) {
|
|
211
|
-
return blocks.filter((b) => b.type === "text").map((b) => b.text).join("");
|
|
212
|
-
}
|
|
213
|
-
function toOpenAIMessages(system, messages) {
|
|
214
|
-
const out = [{ role: "system", content: system }];
|
|
215
|
-
for (const m of messages) {
|
|
216
|
-
if (m.role === "tool") {
|
|
217
|
-
out.push({ role: "tool", tool_call_id: m.toolCallId, content: m.content });
|
|
218
|
-
continue;
|
|
219
|
-
}
|
|
220
|
-
if (m.role === "user") {
|
|
221
|
-
out.push({ role: "user", content: textOf(m.content) });
|
|
222
|
-
continue;
|
|
223
|
-
}
|
|
224
|
-
const toolCalls = m.content.filter((b) => b.type === "tool_call").map((b) => {
|
|
225
|
-
const t = b;
|
|
226
|
-
return { id: t.id, type: "function", function: { name: t.name, arguments: JSON.stringify(t.args ?? {}) } };
|
|
227
|
-
});
|
|
228
|
-
const msg = { role: "assistant", content: textOf(m.content) };
|
|
229
|
-
if (toolCalls.length) msg.tool_calls = toolCalls;
|
|
230
|
-
out.push(msg);
|
|
231
|
-
}
|
|
232
|
-
return out;
|
|
233
|
-
}
|
|
234
|
-
function toOpenAITools(tools) {
|
|
235
|
-
if (!tools.length) return void 0;
|
|
236
|
-
return tools.map((t) => ({ type: "function", function: { name: t.name, description: t.description, parameters: t.parameters } }));
|
|
237
|
-
}
|
|
238
|
-
var OpenAIProvider = class {
|
|
239
|
-
constructor(deps) {
|
|
240
|
-
this.deps = deps;
|
|
241
|
-
}
|
|
242
|
-
callCounter = 0;
|
|
243
|
-
async *stream(req) {
|
|
244
|
-
const tools = toOpenAITools(req.tools);
|
|
245
|
-
const stream = await this.deps.client.chat.completions.create({
|
|
246
|
-
model: this.deps.model,
|
|
247
|
-
stream: true,
|
|
248
|
-
messages: toOpenAIMessages(req.system, req.messages),
|
|
249
|
-
...tools ? { tools } : {}
|
|
250
|
-
});
|
|
251
|
-
const calls = /* @__PURE__ */ new Map();
|
|
252
|
-
let finishReason = null;
|
|
253
|
-
for await (const chunk of stream) {
|
|
254
|
-
const choice = chunk.choices?.[0];
|
|
255
|
-
if (!choice) continue;
|
|
256
|
-
const delta = choice.delta ?? {};
|
|
257
|
-
if (typeof delta.content === "string" && delta.content.length) {
|
|
258
|
-
yield { type: "text_delta", text: delta.content };
|
|
259
|
-
}
|
|
260
|
-
for (const tc of delta.tool_calls ?? []) {
|
|
261
|
-
const cur = calls.get(tc.index) ?? { id: "", name: "", args: "" };
|
|
262
|
-
if (tc.id) cur.id = tc.id;
|
|
263
|
-
if (tc.function?.name) cur.name = tc.function.name;
|
|
264
|
-
if (tc.function?.arguments) cur.args += tc.function.arguments;
|
|
265
|
-
calls.set(tc.index, cur);
|
|
266
|
-
}
|
|
267
|
-
if (choice.finish_reason) finishReason = choice.finish_reason;
|
|
268
|
-
}
|
|
269
|
-
for (const c of [...calls.entries()].sort((a, b) => a[0] - b[0]).map((e) => e[1])) {
|
|
270
|
-
const id = c.id || `openai-call-${this.callCounter++}`;
|
|
271
|
-
let args;
|
|
272
|
-
try {
|
|
273
|
-
args = c.args.trim() ? JSON.parse(c.args) : {};
|
|
274
|
-
} catch (err) {
|
|
275
|
-
throw new Error(`Tool "${c.name}" (id=${id}): invalid tool-call JSON: ${JSON.stringify(c.args)}`, { cause: err });
|
|
276
|
-
}
|
|
277
|
-
yield { type: "tool_call", id, name: c.name, args };
|
|
278
|
-
}
|
|
279
|
-
const stopReason = calls.size ? "tool_use" : finishReason === "length" ? "max_tokens" : "end";
|
|
280
|
-
yield { type: "done", stopReason };
|
|
281
|
-
}
|
|
282
|
-
};
|
|
283
|
-
|
|
284
|
-
// src/providers/gemini.ts
|
|
285
|
-
function textOf2(blocks) {
|
|
286
|
-
return blocks.filter((b) => b.type === "text").map((b) => b.text).join("");
|
|
287
|
-
}
|
|
288
|
-
function buildIdToName(messages) {
|
|
289
|
-
const map = /* @__PURE__ */ new Map();
|
|
290
|
-
for (const m of messages) {
|
|
291
|
-
if (m.role === "assistant") {
|
|
292
|
-
for (const b of m.content) {
|
|
293
|
-
if (b.type === "tool_call") map.set(b.id, b.name);
|
|
294
|
-
}
|
|
295
|
-
}
|
|
296
|
-
}
|
|
297
|
-
return map;
|
|
298
|
-
}
|
|
299
|
-
function toGeminiContents(messages) {
|
|
300
|
-
const idToName = buildIdToName(messages);
|
|
301
|
-
const out = [];
|
|
302
|
-
let i = 0;
|
|
303
|
-
while (i < messages.length) {
|
|
304
|
-
const m = messages[i];
|
|
305
|
-
if (m.role === "tool") {
|
|
306
|
-
const parts2 = [];
|
|
307
|
-
while (i < messages.length && messages[i].role === "tool") {
|
|
308
|
-
const t = messages[i];
|
|
309
|
-
parts2.push({ functionResponse: { name: idToName.get(t.toolCallId) ?? "unknown", response: { result: t.content } } });
|
|
310
|
-
i++;
|
|
311
|
-
}
|
|
312
|
-
out.push({ role: "user", parts: parts2 });
|
|
313
|
-
continue;
|
|
314
|
-
}
|
|
315
|
-
if (m.role === "user") {
|
|
316
|
-
out.push({ role: "user", parts: [{ text: textOf2(m.content) }] });
|
|
317
|
-
i++;
|
|
318
|
-
continue;
|
|
319
|
-
}
|
|
320
|
-
const parts = [];
|
|
321
|
-
for (const b of m.content) {
|
|
322
|
-
if (b.type === "text") parts.push({ text: b.text });
|
|
323
|
-
else if (b.type === "tool_call") parts.push({ functionCall: { name: b.name, args: b.args } });
|
|
324
|
-
}
|
|
325
|
-
out.push({ role: "model", parts });
|
|
326
|
-
i++;
|
|
327
|
-
}
|
|
328
|
-
return out;
|
|
329
|
-
}
|
|
330
|
-
function toGeminiTools(tools) {
|
|
331
|
-
if (!tools.length) return void 0;
|
|
332
|
-
return [{ functionDeclarations: tools.map((t) => ({ name: t.name, description: t.description, parameters: t.parameters })) }];
|
|
333
|
-
}
|
|
334
|
-
var GeminiProvider = class {
|
|
335
|
-
constructor(deps) {
|
|
336
|
-
this.deps = deps;
|
|
337
|
-
}
|
|
338
|
-
callCounter = 0;
|
|
339
|
-
async *stream(req) {
|
|
340
|
-
const stream = await this.deps.client.models.generateContentStream({
|
|
341
|
-
model: this.deps.model,
|
|
342
|
-
contents: toGeminiContents(req.messages),
|
|
343
|
-
config: {
|
|
344
|
-
systemInstruction: req.system,
|
|
345
|
-
tools: toGeminiTools(req.tools)
|
|
346
|
-
}
|
|
347
|
-
});
|
|
348
|
-
let sawToolCall = false;
|
|
349
|
-
let finishReason;
|
|
350
|
-
for await (const chunk of stream) {
|
|
351
|
-
const cand = chunk.candidates?.[0];
|
|
352
|
-
const parts = cand?.content?.parts ?? [];
|
|
353
|
-
for (const part of parts) {
|
|
354
|
-
if (typeof part.text === "string" && part.text.length) {
|
|
355
|
-
yield { type: "text_delta", text: part.text };
|
|
356
|
-
} else if (part.functionCall) {
|
|
357
|
-
sawToolCall = true;
|
|
358
|
-
yield {
|
|
359
|
-
type: "tool_call",
|
|
360
|
-
id: `gemini-call-${this.callCounter++}`,
|
|
361
|
-
name: part.functionCall.name,
|
|
362
|
-
args: part.functionCall.args ?? {}
|
|
363
|
-
};
|
|
364
|
-
}
|
|
365
|
-
}
|
|
366
|
-
if (cand?.finishReason) finishReason = cand.finishReason;
|
|
367
|
-
}
|
|
368
|
-
const stopReason = sawToolCall ? "tool_use" : finishReason === "MAX_TOKENS" ? "max_tokens" : "end";
|
|
369
|
-
yield { type: "done", stopReason };
|
|
370
|
-
}
|
|
371
|
-
};
|
|
372
|
-
|
|
373
|
-
// src/providers/registry.ts
|
|
374
|
-
function requireKey(provider, apiKey) {
|
|
375
|
-
if (!apiKey || !apiKey.trim()) {
|
|
376
|
-
throw new Error(`Missing ${provider} API key. Set it via env or ~/.axon/config.json.`);
|
|
377
|
-
}
|
|
378
|
-
return apiKey;
|
|
379
|
-
}
|
|
380
|
-
function createProvider(cfg) {
|
|
381
|
-
const pc = cfg.providers[cfg.provider] ?? {};
|
|
382
|
-
switch (cfg.provider) {
|
|
383
|
-
case "anthropic": {
|
|
384
|
-
const apiKey = requireKey("Anthropic", pc.apiKey);
|
|
385
|
-
const model = resolveModel(cfg);
|
|
386
|
-
const client = new Anthropic({ apiKey });
|
|
387
|
-
return new AnthropicProvider({ client, model });
|
|
388
|
-
}
|
|
389
|
-
case "openai": {
|
|
390
|
-
const apiKey = requireKey("OpenAI", pc.apiKey);
|
|
391
|
-
const model = resolveModel(cfg);
|
|
392
|
-
const client = new OpenAI({ apiKey, baseURL: pc.baseUrl });
|
|
393
|
-
return new OpenAIProvider({ client, model });
|
|
394
|
-
}
|
|
395
|
-
case "gemini": {
|
|
396
|
-
const apiKey = requireKey("Gemini", pc.apiKey);
|
|
397
|
-
const model = resolveModel(cfg);
|
|
398
|
-
const client = new GoogleGenAI({ apiKey });
|
|
399
|
-
return new GeminiProvider({ client, model });
|
|
400
|
-
}
|
|
401
|
-
default:
|
|
402
|
-
throw new Error(`Unsupported provider: ${cfg.provider}`);
|
|
403
|
-
}
|
|
404
|
-
}
|
|
405
|
-
|
|
406
|
-
// src/tools/fs.ts
|
|
407
|
-
import { readFile, readdir } from "node:fs/promises";
|
|
408
|
-
import { join as join3, resolve as resolve2 } from "node:path";
|
|
409
|
-
import fg from "fast-glob";
|
|
410
|
-
|
|
411
|
-
// src/tools/paths.ts
|
|
412
|
-
import { resolve, relative, isAbsolute, sep, dirname as dirname2 } from "node:path";
|
|
413
|
-
import { realpathSync, existsSync } from "node:fs";
|
|
414
|
-
function resolveInside(cwd, p) {
|
|
415
|
-
const root = resolve(cwd);
|
|
416
|
-
const full = isAbsolute(p) ? resolve(p) : resolve(root, p);
|
|
417
|
-
const rel = relative(root, full);
|
|
418
|
-
if (rel === ".." || rel.startsWith(".." + sep) || isAbsolute(rel)) {
|
|
419
|
-
throw new Error(`path escapes project root: ${p}`);
|
|
420
|
-
}
|
|
421
|
-
return full;
|
|
422
|
-
}
|
|
423
|
-
function assertSafeGlob(pattern) {
|
|
424
|
-
const norm = pattern.replace(/\\/g, "/");
|
|
425
|
-
if (isAbsolute(pattern) || norm === ".." || norm.startsWith("../") || norm.includes("/../")) {
|
|
426
|
-
throw new Error(`glob pattern escapes project root: ${pattern}`);
|
|
427
|
-
}
|
|
428
|
-
}
|
|
429
|
-
function assertRealInside(cwd, full) {
|
|
430
|
-
const root = realpathSync(resolve(cwd));
|
|
431
|
-
let p = full;
|
|
432
|
-
while (!existsSync(p)) {
|
|
433
|
-
const parent = dirname2(p);
|
|
434
|
-
if (parent === p) throw new Error(`cannot resolve path: ${full}`);
|
|
435
|
-
p = parent;
|
|
436
|
-
}
|
|
437
|
-
const real = realpathSync(p);
|
|
438
|
-
const rel = relative(root, real);
|
|
439
|
-
if (rel === ".." || rel.startsWith(".." + sep) || isAbsolute(rel)) {
|
|
440
|
-
throw new Error(`path escapes project root via symlink: ${full}`);
|
|
441
|
-
}
|
|
442
|
-
}
|
|
443
|
-
|
|
444
|
-
// src/tools/fs.ts
|
|
445
|
-
function fail(err) {
|
|
446
|
-
return { ok: false, output: err instanceof Error ? err.message : String(err) };
|
|
447
|
-
}
|
|
448
|
-
var readFileTool = {
|
|
449
|
-
name: "read_file",
|
|
450
|
-
dangerous: false,
|
|
451
|
-
schema: {
|
|
452
|
-
name: "read_file",
|
|
453
|
-
description: "Read a file, returns content with 1-based line numbers.",
|
|
454
|
-
parameters: { type: "object", properties: { path: { type: "string" } }, required: ["path"] }
|
|
455
|
-
},
|
|
456
|
-
async run(args, ctx) {
|
|
457
|
-
try {
|
|
458
|
-
const { path } = args;
|
|
459
|
-
const full = resolveInside(ctx.cwd, path);
|
|
460
|
-
const text = await readFile(full, "utf8");
|
|
461
|
-
const numbered = text.replace(/\n$/, "").split("\n").map((line, i) => `${i + 1} ${line}`).join("\n");
|
|
462
|
-
return { ok: true, output: numbered };
|
|
463
|
-
} catch (err) {
|
|
464
|
-
return fail(err);
|
|
465
|
-
}
|
|
466
|
-
}
|
|
467
|
-
};
|
|
468
|
-
var listDirTool = {
|
|
469
|
-
name: "list_dir",
|
|
470
|
-
dangerous: false,
|
|
471
|
-
schema: {
|
|
472
|
-
name: "list_dir",
|
|
473
|
-
description: "List the entries of a directory. Directories get a trailing slash.",
|
|
474
|
-
parameters: { type: "object", properties: { path: { type: "string" } }, required: ["path"] }
|
|
475
|
-
},
|
|
476
|
-
async run(args, ctx) {
|
|
477
|
-
try {
|
|
478
|
-
const { path } = args;
|
|
479
|
-
const full = resolveInside(ctx.cwd, path);
|
|
480
|
-
const entries = await readdir(full, { withFileTypes: true });
|
|
481
|
-
const out = entries.map((e) => e.isDirectory() ? `${e.name}/` : e.name).join("\n");
|
|
482
|
-
return { ok: true, output: out };
|
|
483
|
-
} catch (err) {
|
|
484
|
-
return fail(err);
|
|
485
|
-
}
|
|
486
|
-
}
|
|
487
|
-
};
|
|
488
|
-
var globTool = {
|
|
489
|
-
name: "glob",
|
|
490
|
-
dangerous: false,
|
|
491
|
-
schema: {
|
|
492
|
-
name: "glob",
|
|
493
|
-
description: "Find files matching a glob pattern (relative paths from project root).",
|
|
494
|
-
parameters: { type: "object", properties: { pattern: { type: "string" } }, required: ["pattern"] }
|
|
495
|
-
},
|
|
496
|
-
async run(args, ctx) {
|
|
497
|
-
try {
|
|
498
|
-
const { pattern } = args;
|
|
499
|
-
assertSafeGlob(pattern);
|
|
500
|
-
const matches = await fg(pattern, { cwd: resolve2(ctx.cwd), onlyFiles: true, dot: false });
|
|
501
|
-
return { ok: true, output: matches.join("\n") };
|
|
502
|
-
} catch (err) {
|
|
503
|
-
return fail(err);
|
|
504
|
-
}
|
|
505
|
-
}
|
|
506
|
-
};
|
|
507
|
-
var grepTool = {
|
|
508
|
-
name: "grep",
|
|
509
|
-
dangerous: false,
|
|
510
|
-
schema: {
|
|
511
|
-
name: "grep",
|
|
512
|
-
description: "Search file contents by regex. Returns file:line:text for each match.",
|
|
513
|
-
parameters: {
|
|
514
|
-
type: "object",
|
|
515
|
-
properties: { pattern: { type: "string" }, glob: { type: "string" } },
|
|
516
|
-
required: ["pattern"]
|
|
517
|
-
}
|
|
518
|
-
},
|
|
519
|
-
async run(args, ctx) {
|
|
520
|
-
try {
|
|
521
|
-
const { pattern, glob = "**/*" } = args;
|
|
522
|
-
assertSafeGlob(glob);
|
|
523
|
-
const root = resolve2(ctx.cwd);
|
|
524
|
-
const re = new RegExp(pattern);
|
|
525
|
-
const files = await fg(glob, { cwd: root, onlyFiles: true, dot: false });
|
|
526
|
-
const hits = [];
|
|
527
|
-
for (const rel of files) {
|
|
528
|
-
const text = await readFile(join3(root, rel), "utf8").catch(() => "");
|
|
529
|
-
text.split("\n").forEach((line, i) => {
|
|
530
|
-
if (re.test(line)) hits.push(`${rel}:${i + 1}:${line}`);
|
|
531
|
-
});
|
|
532
|
-
}
|
|
533
|
-
return { ok: true, output: hits.join("\n") };
|
|
534
|
-
} catch (err) {
|
|
535
|
-
return fail(err);
|
|
536
|
-
}
|
|
537
|
-
}
|
|
538
|
-
};
|
|
539
|
-
|
|
540
|
-
// src/tools/edit.ts
|
|
541
|
-
import { readFile as readFile2, writeFile, mkdir } from "node:fs/promises";
|
|
542
|
-
import { dirname as dirname3 } from "node:path";
|
|
543
|
-
function fail2(err) {
|
|
544
|
-
return { ok: false, output: err instanceof Error ? err.message : String(err) };
|
|
545
|
-
}
|
|
546
|
-
var writeFileTool = {
|
|
547
|
-
name: "write_file",
|
|
548
|
-
dangerous: true,
|
|
549
|
-
schema: {
|
|
550
|
-
name: "write_file",
|
|
551
|
-
description: "Create or overwrite a file (relative to the project root). Creates parent directories as needed.",
|
|
552
|
-
parameters: {
|
|
553
|
-
type: "object",
|
|
554
|
-
properties: { path: { type: "string" }, content: { type: "string" } },
|
|
555
|
-
required: ["path", "content"]
|
|
556
|
-
}
|
|
557
|
-
},
|
|
558
|
-
async run(args, ctx) {
|
|
559
|
-
try {
|
|
560
|
-
const { path, content } = args;
|
|
561
|
-
const full = resolveInside(ctx.cwd, path);
|
|
562
|
-
assertRealInside(ctx.cwd, full);
|
|
563
|
-
await mkdir(dirname3(full), { recursive: true });
|
|
564
|
-
await writeFile(full, content, "utf8");
|
|
565
|
-
return { ok: true, output: `wrote ${Buffer.byteLength(content, "utf8")} bytes to ${path}` };
|
|
566
|
-
} catch (err) {
|
|
567
|
-
return fail2(err);
|
|
568
|
-
}
|
|
569
|
-
}
|
|
570
|
-
};
|
|
571
|
-
var editFileTool = {
|
|
572
|
-
name: "edit_file",
|
|
573
|
-
dangerous: true,
|
|
574
|
-
schema: {
|
|
575
|
-
name: "edit_file",
|
|
576
|
-
description: "Replace an exact, unique occurrence of old_string with new_string in a file.",
|
|
577
|
-
parameters: {
|
|
578
|
-
type: "object",
|
|
579
|
-
properties: { path: { type: "string" }, old_string: { type: "string" }, new_string: { type: "string" } },
|
|
580
|
-
required: ["path", "old_string", "new_string"]
|
|
581
|
-
}
|
|
582
|
-
},
|
|
583
|
-
async run(args, ctx) {
|
|
584
|
-
try {
|
|
585
|
-
const { path, old_string, new_string } = args;
|
|
586
|
-
const full = resolveInside(ctx.cwd, path);
|
|
587
|
-
assertRealInside(ctx.cwd, full);
|
|
588
|
-
if (old_string === "") return { ok: false, output: "old_string must not be empty" };
|
|
589
|
-
const text = await readFile2(full, "utf8");
|
|
590
|
-
const count = text.split(old_string).length - 1;
|
|
591
|
-
if (count === 0) return { ok: false, output: `old_string not found in ${path}` };
|
|
592
|
-
if (count > 1) return { ok: false, output: `old_string matches ${count} occurrences in ${path}; make it unique` };
|
|
593
|
-
const updated = text.split(old_string).join(new_string);
|
|
594
|
-
await writeFile(full, updated, "utf8");
|
|
595
|
-
return { ok: true, output: `edited ${path}` };
|
|
596
|
-
} catch (err) {
|
|
597
|
-
return fail2(err);
|
|
598
|
-
}
|
|
599
|
-
}
|
|
600
|
-
};
|
|
601
|
-
|
|
602
|
-
// src/tools/shell.ts
|
|
603
|
-
import { exec } from "node:child_process";
|
|
604
|
-
import { promisify } from "node:util";
|
|
605
|
-
var pexec = promisify(exec);
|
|
606
|
-
function combine(stdout, stderr) {
|
|
607
|
-
return [stdout, stderr].filter((s) => s.trim()).join("\n").trim();
|
|
608
|
-
}
|
|
609
|
-
var shellTool = {
|
|
610
|
-
name: "shell",
|
|
611
|
-
dangerous: true,
|
|
612
|
-
schema: {
|
|
613
|
-
name: "shell",
|
|
614
|
-
description: "Run a shell command in the project root. Returns combined stdout+stderr; ok is false on a nonzero exit or timeout.",
|
|
615
|
-
parameters: {
|
|
616
|
-
type: "object",
|
|
617
|
-
properties: { command: { type: "string" } },
|
|
618
|
-
required: ["command"]
|
|
619
|
-
}
|
|
620
|
-
},
|
|
621
|
-
async run(args, ctx) {
|
|
622
|
-
const { command } = args;
|
|
623
|
-
try {
|
|
624
|
-
const { stdout, stderr } = await pexec(command, {
|
|
625
|
-
cwd: ctx.cwd,
|
|
626
|
-
timeout: 12e4,
|
|
627
|
-
maxBuffer: 10 * 1024 * 1024,
|
|
628
|
-
windowsHide: true
|
|
629
|
-
});
|
|
630
|
-
return { ok: true, output: combine(stdout, stderr) || "(no output)" };
|
|
631
|
-
} catch (err) {
|
|
632
|
-
const e = err;
|
|
633
|
-
const body = combine(e.stdout ?? "", e.stderr ?? "");
|
|
634
|
-
const status = e.killed && e.code == null ? "timed out" : e.killed ? "killed by signal" : `exit ${e.code ?? "?"}`;
|
|
635
|
-
return { ok: false, output: `[${status}] ${body || e.message}`.trim() };
|
|
636
|
-
}
|
|
637
|
-
}
|
|
638
|
-
};
|
|
639
|
-
|
|
640
|
-
// src/tools/registry.ts
|
|
641
|
-
function toMap(tools) {
|
|
642
|
-
return new Map(tools.map((t) => [t.name, t]));
|
|
643
|
-
}
|
|
644
|
-
function buildReadOnlyTools() {
|
|
645
|
-
return toMap([readFileTool, listDirTool, globTool, grepTool]);
|
|
646
|
-
}
|
|
647
|
-
function buildMutatingTools() {
|
|
648
|
-
return toMap([writeFileTool, editFileTool, shellTool]);
|
|
649
|
-
}
|
|
650
|
-
function buildAllTools() {
|
|
651
|
-
return toMap([...buildReadOnlyTools().values(), ...buildMutatingTools().values()]);
|
|
652
|
-
}
|
|
653
|
-
|
|
654
|
-
// src/permission/gate.ts
|
|
655
|
-
var denyGate = async (req) => ({
|
|
656
|
-
allow: false,
|
|
657
|
-
reason: `permission denied: "${req.name}" is a write/exec action and non-interactive mode blocks it. Re-run with --yolo to allow.`
|
|
658
|
-
});
|
|
659
|
-
var allowAllGate = async () => ({
|
|
660
|
-
allow: true,
|
|
661
|
-
reason: "allowed (--yolo)"
|
|
662
|
-
});
|
|
663
|
-
|
|
664
|
-
// src/core/conversation.ts
|
|
665
|
-
var Conversation = class {
|
|
666
|
-
msgs = [];
|
|
667
|
-
get messages() {
|
|
668
|
-
return [...this.msgs];
|
|
669
|
-
}
|
|
670
|
-
pushUser(text) {
|
|
671
|
-
this.msgs.push({ role: "user", content: [{ type: "text", text }] });
|
|
672
|
-
}
|
|
673
|
-
pushAssistant(content) {
|
|
674
|
-
this.msgs.push({ role: "assistant", content });
|
|
675
|
-
}
|
|
676
|
-
pushToolResult(toolCallId, content) {
|
|
677
|
-
this.msgs.push({ role: "tool", toolCallId, content });
|
|
678
|
-
}
|
|
679
|
-
};
|
|
680
|
-
|
|
681
|
-
// src/core/engine.ts
|
|
682
|
-
var Engine = class {
|
|
683
|
-
constructor(deps) {
|
|
684
|
-
this.deps = deps;
|
|
685
|
-
}
|
|
686
|
-
convo = new Conversation();
|
|
687
|
-
listeners = [];
|
|
688
|
-
on(fn) {
|
|
689
|
-
this.listeners.push(fn);
|
|
690
|
-
}
|
|
691
|
-
/** Read-only snapshot of the conversation transcript. */
|
|
692
|
-
get history() {
|
|
693
|
-
return this.convo.messages;
|
|
694
|
-
}
|
|
695
|
-
emit(e) {
|
|
696
|
-
for (const l of this.listeners) l(e);
|
|
697
|
-
}
|
|
698
|
-
// NOTE: the `error` EngineEvent is reserved for the Plan 3 interactive/TUI path;
|
|
699
|
-
// stream failures currently propagate to the CLI's top-level handler in the non-interactive slice.
|
|
700
|
-
async submit(text) {
|
|
701
|
-
this.convo.pushUser(text);
|
|
702
|
-
const maxSteps = this.deps.maxSteps ?? 50;
|
|
703
|
-
for (let step = 0; step < maxSteps; step++) {
|
|
704
|
-
const blocks = [];
|
|
705
|
-
const calls = [];
|
|
706
|
-
let textBlock = null;
|
|
707
|
-
let stopReason = "end";
|
|
708
|
-
for await (const ev of this.deps.provider.stream({
|
|
709
|
-
system: this.deps.system,
|
|
710
|
-
messages: this.convo.messages,
|
|
711
|
-
tools: [...this.deps.tools.values()].map((t) => t.schema)
|
|
712
|
-
})) {
|
|
713
|
-
if (ev.type === "text_delta") {
|
|
714
|
-
if (!textBlock) {
|
|
715
|
-
textBlock = { type: "text", text: "" };
|
|
716
|
-
blocks.push(textBlock);
|
|
717
|
-
}
|
|
718
|
-
textBlock.text += ev.text;
|
|
719
|
-
this.emit({ type: "text_delta", text: ev.text });
|
|
720
|
-
} else if (ev.type === "tool_call") {
|
|
721
|
-
textBlock = null;
|
|
722
|
-
blocks.push({ type: "tool_call", id: ev.id, name: ev.name, args: ev.args });
|
|
723
|
-
calls.push({ id: ev.id, name: ev.name, args: ev.args });
|
|
724
|
-
} else if (ev.type === "done") {
|
|
725
|
-
stopReason = ev.stopReason;
|
|
726
|
-
}
|
|
727
|
-
}
|
|
728
|
-
this.convo.pushAssistant(blocks);
|
|
729
|
-
if (calls.length === 0) {
|
|
730
|
-
this.emit({ type: "turn_done", stopReason });
|
|
731
|
-
return;
|
|
732
|
-
}
|
|
733
|
-
for (const call of calls) {
|
|
734
|
-
this.emit({ type: "tool_start", id: call.id, name: call.name, args: call.args });
|
|
735
|
-
const tool = this.deps.tools.get(call.name);
|
|
736
|
-
if (!tool) {
|
|
737
|
-
this.emit({ type: "tool_end", id: call.id, ok: false, output: `unknown tool: ${call.name}` });
|
|
738
|
-
this.convo.pushToolResult(call.id, `unknown tool: ${call.name}`);
|
|
739
|
-
continue;
|
|
740
|
-
}
|
|
741
|
-
if (tool.dangerous) {
|
|
742
|
-
const gate = this.deps.gate ?? denyGate;
|
|
743
|
-
const verdict = await gate({ id: call.id, name: call.name, args: call.args });
|
|
744
|
-
if (!verdict.allow) {
|
|
745
|
-
this.emit({ type: "tool_end", id: call.id, ok: false, output: verdict.reason });
|
|
746
|
-
this.convo.pushToolResult(call.id, verdict.reason);
|
|
747
|
-
continue;
|
|
748
|
-
}
|
|
749
|
-
}
|
|
750
|
-
let result;
|
|
751
|
-
try {
|
|
752
|
-
result = await tool.run(call.args, { cwd: this.deps.cwd });
|
|
753
|
-
} catch (err) {
|
|
754
|
-
result = { ok: false, output: String(err) };
|
|
755
|
-
}
|
|
756
|
-
this.emit({ type: "tool_end", id: call.id, ok: result.ok, output: result.output });
|
|
757
|
-
this.convo.pushToolResult(call.id, result.output);
|
|
758
|
-
}
|
|
759
|
-
}
|
|
760
|
-
this.emit({ type: "turn_done", stopReason: "max_steps" });
|
|
761
|
-
}
|
|
762
|
-
};
|
|
763
|
-
|
|
764
|
-
// src/ui/printRunner.ts
|
|
765
|
-
function printRunner(engine, write) {
|
|
766
|
-
engine.on((e) => {
|
|
767
|
-
switch (e.type) {
|
|
768
|
-
case "text_delta":
|
|
769
|
-
write(e.text);
|
|
770
|
-
break;
|
|
771
|
-
case "tool_start":
|
|
772
|
-
write(`
|
|
773
|
-
\u23F3 ${e.name}(${JSON.stringify(e.args)})
|
|
774
|
-
`);
|
|
775
|
-
break;
|
|
776
|
-
case "tool_end":
|
|
777
|
-
write(`${e.ok ? "\u2705" : "\u274C"} ${truncate(e.output)}
|
|
778
|
-
`);
|
|
779
|
-
break;
|
|
780
|
-
case "permission_request":
|
|
781
|
-
write(`
|
|
782
|
-
\u{1F512} ${e.action}: ${e.detail}
|
|
783
|
-
`);
|
|
784
|
-
break;
|
|
785
|
-
case "turn_done":
|
|
786
|
-
write(`
|
|
787
|
-
[done: ${e.stopReason}]
|
|
788
|
-
`);
|
|
789
|
-
break;
|
|
790
|
-
case "error":
|
|
791
|
-
write(`
|
|
792
|
-
\u{1F4A5} ${e.message}
|
|
793
|
-
`);
|
|
794
|
-
break;
|
|
795
|
-
}
|
|
796
|
-
});
|
|
797
|
-
}
|
|
798
|
-
function truncate(s, max = 500) {
|
|
799
|
-
return s.length > max ? `${s.slice(0, max)}\u2026` : s;
|
|
800
|
-
}
|
|
801
|
-
|
|
802
|
-
// src/ui/runTui.tsx
|
|
803
|
-
import { useState as useState3, useRef } from "react";
|
|
804
|
-
import { Box as Box6, Text as Text6, render } from "ink";
|
|
805
|
-
|
|
806
|
-
// src/core/projectContext.ts
|
|
807
|
-
import { readFileSync as readFileSync3 } from "node:fs";
|
|
808
|
-
import { join as join4 } from "node:path";
|
|
809
|
-
function truncate2(s, max) {
|
|
810
|
-
return s.length > max ? `${s.slice(0, max)}\u2026` : s;
|
|
811
|
-
}
|
|
812
|
-
function loadProjectContext(cwd) {
|
|
813
|
-
const parts = [];
|
|
814
|
-
try {
|
|
815
|
-
const pkg = JSON.parse(readFileSync3(join4(cwd, "package.json"), "utf8"));
|
|
816
|
-
const bits = [];
|
|
817
|
-
if (pkg.name) bits.push(`name: ${pkg.name}`);
|
|
818
|
-
if (pkg.description) bits.push(`description: ${pkg.description}`);
|
|
819
|
-
if (pkg.scripts && typeof pkg.scripts === "object") bits.push(`scripts: ${Object.keys(pkg.scripts).join(", ")}`);
|
|
820
|
-
if (bits.length) parts.push(`package.json \u2014 ${bits.join("; ")}`);
|
|
821
|
-
} catch {
|
|
822
|
-
}
|
|
823
|
-
for (const name of ["AGENTS.md", "CLAUDE.md", "README.md"]) {
|
|
824
|
-
try {
|
|
825
|
-
const text = readFileSync3(join4(cwd, name), "utf8").trim();
|
|
826
|
-
if (text) {
|
|
827
|
-
parts.push(`${name}:
|
|
828
|
-
${truncate2(text, 2e3)}`);
|
|
829
|
-
break;
|
|
830
|
-
}
|
|
831
|
-
} catch {
|
|
832
|
-
}
|
|
833
|
-
}
|
|
834
|
-
return parts.join("\n\n");
|
|
835
|
-
}
|
|
836
|
-
|
|
837
|
-
// src/core/systemPrompt.ts
|
|
838
|
-
function buildSystemPrompt(opts) {
|
|
839
|
-
const identity = `You are the coding agent inside Axon, a command-line coding tool. Axon is the CLI program \u2014 it is NOT a language model. You are powered by the model "${opts.model}" via the ${opts.provider} provider. If the user asks which model or LLM you are, answer honestly: "${opts.model}" (via ${opts.provider}), running inside the Axon CLI. Never claim that "Axon" is a model.`;
|
|
840
|
-
const toolLine = opts.tools === "all" ? `Use the tools to inspect AND modify the project: read_file, list_dir, glob, grep (read-only) and write_file, edit_file, shell (these change the workspace; the user is prompted to approve each). Prefer edit_file for surgical changes.` : `Use the read-only tools to inspect the project: read_file, list_dir, glob, grep.`;
|
|
841
|
-
const closing = `Explain briefly what you are doing. When the task is done, stop calling tools.`;
|
|
842
|
-
let prompt = `${identity}
|
|
843
|
-
|
|
844
|
-
${toolLine} ${closing}`;
|
|
845
|
-
if (opts.context) prompt += `
|
|
846
|
-
|
|
847
|
-
Project context:
|
|
848
|
-
${opts.context}`;
|
|
849
|
-
return prompt;
|
|
850
|
-
}
|
|
851
|
-
|
|
852
|
-
// src/ui/permissionController.ts
|
|
853
|
-
function createPermissionController() {
|
|
854
|
-
const sessionAllow = /* @__PURE__ */ new Set();
|
|
855
|
-
const subscribers = /* @__PURE__ */ new Set();
|
|
856
|
-
const queue = [];
|
|
857
|
-
let active = null;
|
|
858
|
-
let currentPending = null;
|
|
859
|
-
const notify = (p) => {
|
|
860
|
-
currentPending = p;
|
|
861
|
-
for (const fn of subscribers) fn(p);
|
|
862
|
-
};
|
|
863
|
-
const pump = () => {
|
|
864
|
-
if (active || queue.length === 0) return;
|
|
865
|
-
active = queue.shift();
|
|
866
|
-
notify({ req: active.req });
|
|
867
|
-
};
|
|
868
|
-
const gate = (req) => {
|
|
869
|
-
if (sessionAllow.has(req.name)) {
|
|
870
|
-
return Promise.resolve({ allow: true, reason: "allowed (remembered this session)" });
|
|
871
|
-
}
|
|
872
|
-
return new Promise((res) => {
|
|
873
|
-
queue.push({ req, settle: res });
|
|
874
|
-
pump();
|
|
875
|
-
});
|
|
876
|
-
};
|
|
877
|
-
const resolve3 = (decision) => {
|
|
878
|
-
if (!active) return;
|
|
879
|
-
const { req, settle } = active;
|
|
880
|
-
active = null;
|
|
881
|
-
if (decision === "always") sessionAllow.add(req.name);
|
|
882
|
-
settle({
|
|
883
|
-
allow: decision !== "deny",
|
|
884
|
-
reason: decision === "deny" ? `permission denied by user: ${req.name}` : `allowed (${decision})`
|
|
885
|
-
});
|
|
886
|
-
pump();
|
|
887
|
-
if (!active) notify(null);
|
|
888
|
-
};
|
|
889
|
-
const subscribe = (fn) => {
|
|
890
|
-
subscribers.add(fn);
|
|
891
|
-
return () => {
|
|
892
|
-
subscribers.delete(fn);
|
|
893
|
-
};
|
|
894
|
-
};
|
|
895
|
-
const getPending = () => currentPending;
|
|
896
|
-
return { gate, subscribe, getPending, resolve: resolve3 };
|
|
897
|
-
}
|
|
898
|
-
|
|
899
|
-
// src/config/credentials.ts
|
|
900
|
-
function hasUsableKey(cfg) {
|
|
901
|
-
const k = cfg.providers[cfg.provider]?.apiKey;
|
|
902
|
-
return typeof k === "string" && k.trim().length > 0;
|
|
903
|
-
}
|
|
904
|
-
var INFO = {
|
|
905
|
-
anthropic: { envVar: "ANTHROPIC_API_KEY", url: "https://console.anthropic.com/settings/keys" },
|
|
906
|
-
openai: { envVar: "OPENAI_API_KEY", url: "https://platform.openai.com/api-keys" },
|
|
907
|
-
gemini: { envVar: "GEMINI_API_KEY", url: "https://aistudio.google.com/apikey" }
|
|
908
|
-
};
|
|
909
|
-
function keyProviderInfo(provider) {
|
|
910
|
-
return INFO[provider] ?? { envVar: `${provider.toUpperCase()}_API_KEY`, url: "" };
|
|
911
|
-
}
|
|
912
|
-
|
|
913
|
-
// src/ui/app.tsx
|
|
914
|
-
import { useEffect, useState, useSyncExternalStore, useCallback } from "react";
|
|
915
|
-
import { Box as Box4, Text as Text4, useInput as useInput2 } from "ink";
|
|
916
|
-
import TextInput from "ink-text-input";
|
|
917
|
-
|
|
918
|
-
// src/ui/components/MessageView.tsx
|
|
919
|
-
import { Box, Text } from "ink";
|
|
920
|
-
import { jsx, jsxs } from "react/jsx-runtime";
|
|
921
|
-
function toolIcon(status) {
|
|
922
|
-
if (status === "running") return "\u23F3";
|
|
923
|
-
return status === "ok" ? "\u2705" : "\u274C";
|
|
924
|
-
}
|
|
925
|
-
function truncate3(s, max = 300) {
|
|
926
|
-
return s.length > max ? `${s.slice(0, max)}\u2026` : s;
|
|
927
|
-
}
|
|
928
|
-
function MessageView({ items }) {
|
|
929
|
-
return /* @__PURE__ */ jsx(Box, { flexDirection: "column", children: items.map((it, i) => {
|
|
930
|
-
if (it.kind === "user") {
|
|
931
|
-
return /* @__PURE__ */ jsxs(Text, { children: [
|
|
932
|
-
/* @__PURE__ */ jsxs(Text, { color: "cyan", bold: true, children: [
|
|
933
|
-
"You",
|
|
934
|
-
" "
|
|
935
|
-
] }),
|
|
936
|
-
it.text
|
|
937
|
-
] }, i);
|
|
938
|
-
}
|
|
939
|
-
if (it.kind === "assistant") {
|
|
940
|
-
return /* @__PURE__ */ jsxs(Text, { children: [
|
|
941
|
-
/* @__PURE__ */ jsxs(Text, { color: "green", bold: true, children: [
|
|
942
|
-
"Axon",
|
|
943
|
-
" "
|
|
944
|
-
] }),
|
|
945
|
-
it.text
|
|
946
|
-
] }, i);
|
|
947
|
-
}
|
|
948
|
-
if (it.kind === "error") {
|
|
949
|
-
return /* @__PURE__ */ jsxs(Text, { color: "red", children: [
|
|
950
|
-
"\u{1F4A5} ",
|
|
951
|
-
it.text
|
|
952
|
-
] }, i);
|
|
953
|
-
}
|
|
954
|
-
if (it.kind === "tool") {
|
|
955
|
-
return /* @__PURE__ */ jsxs(Text, { color: "gray", children: [
|
|
956
|
-
toolIcon(it.status),
|
|
957
|
-
" ",
|
|
958
|
-
it.name,
|
|
959
|
-
"(",
|
|
960
|
-
truncate3(JSON.stringify(it.args ?? {}), 60),
|
|
961
|
-
")",
|
|
962
|
-
it.output ? ` \u2014 ${truncate3(it.output)}` : ""
|
|
963
|
-
] }, i);
|
|
964
|
-
}
|
|
965
|
-
return null;
|
|
966
|
-
}) });
|
|
967
|
-
}
|
|
968
|
-
|
|
969
|
-
// src/ui/components/PermissionPrompt.tsx
|
|
970
|
-
import { Box as Box2, Text as Text2, useInput } from "ink";
|
|
971
|
-
import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
|
|
972
|
-
function summarize(args) {
|
|
973
|
-
const s = JSON.stringify(args ?? {});
|
|
974
|
-
return s.length > 80 ? `${s.slice(0, 80)}\u2026` : s;
|
|
975
|
-
}
|
|
976
|
-
function decisionForKey(input) {
|
|
977
|
-
if (input === "a") return "once";
|
|
978
|
-
if (input === "A") return "always";
|
|
979
|
-
if (input === "d") return "deny";
|
|
980
|
-
return null;
|
|
981
|
-
}
|
|
982
|
-
function PermissionPrompt({ req, onDecide }) {
|
|
983
|
-
useInput((input) => {
|
|
984
|
-
const d = decisionForKey(input);
|
|
985
|
-
if (d) onDecide(d);
|
|
986
|
-
});
|
|
987
|
-
return /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", children: [
|
|
988
|
-
/* @__PURE__ */ jsxs2(Text2, { color: "yellow", children: [
|
|
989
|
-
"\u{1F512} ",
|
|
990
|
-
req.name,
|
|
991
|
-
"(",
|
|
992
|
-
summarize(req.args),
|
|
993
|
-
")"
|
|
994
|
-
] }),
|
|
995
|
-
/* @__PURE__ */ jsx2(Text2, { color: "yellow", children: "[a] allow once / [A] always this session / [d] deny" })
|
|
996
|
-
] });
|
|
997
|
-
}
|
|
998
|
-
|
|
999
|
-
// src/ui/components/StatusBar.tsx
|
|
1000
|
-
import { Box as Box3, Text as Text3 } from "ink";
|
|
1001
|
-
import { jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
|
|
1002
|
-
function StatusBar({ provider, model, running, yolo }) {
|
|
1003
|
-
return /* @__PURE__ */ jsx3(Box3, { children: /* @__PURE__ */ jsxs3(Text3, { color: "gray", children: [
|
|
1004
|
-
"[axon: ",
|
|
1005
|
-
provider,
|
|
1006
|
-
"/",
|
|
1007
|
-
model,
|
|
1008
|
-
yolo ? " \xB7 yolo" : "",
|
|
1009
|
-
" \xB7 ",
|
|
1010
|
-
running ? "working\u2026" : "ready",
|
|
1011
|
-
" \xB7 ^C quit]"
|
|
1012
|
-
] }) });
|
|
1013
|
-
}
|
|
1014
|
-
|
|
1015
|
-
// src/ui/app.tsx
|
|
1016
|
-
import { jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
|
|
1017
|
-
function reduceEvent(items, e) {
|
|
1018
|
-
switch (e.type) {
|
|
1019
|
-
case "text_delta": {
|
|
1020
|
-
const last = items[items.length - 1];
|
|
1021
|
-
if (last && last.kind === "assistant") {
|
|
1022
|
-
return [...items.slice(0, -1), { kind: "assistant", text: last.text + e.text }];
|
|
1023
|
-
}
|
|
1024
|
-
return [...items, { kind: "assistant", text: e.text }];
|
|
1025
|
-
}
|
|
1026
|
-
case "tool_start":
|
|
1027
|
-
return [...items, { kind: "tool", id: e.id, name: e.name, args: e.args, status: "running" }];
|
|
1028
|
-
case "tool_end":
|
|
1029
|
-
return items.map(
|
|
1030
|
-
(it) => it.kind === "tool" && it.id === e.id ? { ...it, status: e.ok ? "ok" : "fail", output: e.output } : it
|
|
1031
|
-
);
|
|
1032
|
-
default:
|
|
1033
|
-
return items;
|
|
1034
|
-
}
|
|
1035
|
-
}
|
|
1036
|
-
function usePendingPermission(controller) {
|
|
1037
|
-
const subscribe = useCallback((onChange) => controller.subscribe(() => onChange()), [controller]);
|
|
1038
|
-
const getSnapshot = useCallback(() => controller.getPending(), [controller]);
|
|
1039
|
-
const pending = useSyncExternalStore(subscribe, getSnapshot, getSnapshot);
|
|
1040
|
-
return pending ? pending.req : null;
|
|
1041
|
-
}
|
|
1042
|
-
function App({ engine, controller, provider, model, yolo }) {
|
|
1043
|
-
const [items, setItems] = useState([]);
|
|
1044
|
-
const [input, setInput] = useState("");
|
|
1045
|
-
const [running, setRunning] = useState(false);
|
|
1046
|
-
const pending = usePendingPermission(controller);
|
|
1047
|
-
useEffect(() => {
|
|
1048
|
-
engine.on((e) => setItems((prev) => reduceEvent(prev, e)));
|
|
1049
|
-
}, [engine]);
|
|
1050
|
-
useInput2(
|
|
1051
|
-
(_input, key) => {
|
|
1052
|
-
if (key.escape && !running) setInput("");
|
|
1053
|
-
},
|
|
1054
|
-
{ isActive: !pending }
|
|
1055
|
-
);
|
|
1056
|
-
const handleSubmit = (text) => {
|
|
1057
|
-
if (!text.trim() || running) return;
|
|
1058
|
-
setItems((prev) => [...prev, { kind: "user", text }]);
|
|
1059
|
-
setInput("");
|
|
1060
|
-
setRunning(true);
|
|
1061
|
-
engine.submit(text).catch((err) => {
|
|
1062
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
1063
|
-
setItems((prev) => [...prev, { kind: "error", text: msg }]);
|
|
1064
|
-
}).finally(() => setRunning(false));
|
|
1065
|
-
};
|
|
1066
|
-
return /* @__PURE__ */ jsxs4(Box4, { flexDirection: "column", children: [
|
|
1067
|
-
/* @__PURE__ */ jsx4(MessageView, { items }),
|
|
1068
|
-
pending ? /* @__PURE__ */ jsx4(PermissionPrompt, { req: pending, onDecide: (d) => controller.resolve(d) }) : running ? /* @__PURE__ */ jsx4(Text4, { color: "gray", children: "\u2026 working (Ctrl+C to quit)" }) : /* @__PURE__ */ jsxs4(Box4, { children: [
|
|
1069
|
-
/* @__PURE__ */ jsx4(Text4, { color: "cyan", children: "\u203A " }),
|
|
1070
|
-
/* @__PURE__ */ jsx4(TextInput, { value: input, onChange: setInput, onSubmit: handleSubmit })
|
|
1071
|
-
] }),
|
|
1072
|
-
/* @__PURE__ */ jsx4(StatusBar, { provider, model, running, yolo })
|
|
1073
|
-
] });
|
|
1074
|
-
}
|
|
1075
|
-
|
|
1076
|
-
// src/ui/Setup.tsx
|
|
1077
|
-
import { useState as useState2 } from "react";
|
|
1078
|
-
import { Box as Box5, Text as Text5, useInput as useInput3 } from "ink";
|
|
1079
|
-
import TextInput2 from "ink-text-input";
|
|
1080
|
-
import { jsx as jsx5, jsxs as jsxs5 } from "react/jsx-runtime";
|
|
1081
|
-
var SETUP_PROVIDERS = ["anthropic", "openai", "gemini"];
|
|
1082
|
-
function trimmedKey(v) {
|
|
1083
|
-
const t = v.trim();
|
|
1084
|
-
return t ? t : null;
|
|
1085
|
-
}
|
|
1086
|
-
function providerForDigit(input) {
|
|
1087
|
-
const idx = Number(input) - 1;
|
|
1088
|
-
return Number.isInteger(idx) && idx >= 0 && idx < SETUP_PROVIDERS.length ? SETUP_PROVIDERS[idx] : null;
|
|
1089
|
-
}
|
|
1090
|
-
function Setup({
|
|
1091
|
-
initialProvider,
|
|
1092
|
-
onSubmit
|
|
1093
|
-
}) {
|
|
1094
|
-
const start = SETUP_PROVIDERS.includes(initialProvider) ? initialProvider : "anthropic";
|
|
1095
|
-
const [provider, setProvider] = useState2(start);
|
|
1096
|
-
const [value, setValue] = useState2("");
|
|
1097
|
-
useInput3((input) => {
|
|
1098
|
-
const picked = providerForDigit(input);
|
|
1099
|
-
if (picked) setProvider(picked);
|
|
1100
|
-
});
|
|
1101
|
-
const info = keyProviderInfo(provider);
|
|
1102
|
-
const submit = (v) => {
|
|
1103
|
-
const k = trimmedKey(v);
|
|
1104
|
-
if (k) onSubmit(provider, k);
|
|
1105
|
-
};
|
|
1106
|
-
return /* @__PURE__ */ jsxs5(Box5, { flexDirection: "column", children: [
|
|
1107
|
-
/* @__PURE__ */ jsx5(Text5, { color: "yellow", children: "Welcome to Axon. Pick a provider, then paste its API key." }),
|
|
1108
|
-
SETUP_PROVIDERS.map((p, i) => /* @__PURE__ */ jsxs5(Text5, { color: p === provider ? "cyan" : "gray", children: [
|
|
1109
|
-
p === provider ? "\u276F" : " ",
|
|
1110
|
-
" [",
|
|
1111
|
-
i + 1,
|
|
1112
|
-
"] ",
|
|
1113
|
-
p
|
|
1114
|
-
] }, p)),
|
|
1115
|
-
/* @__PURE__ */ jsxs5(Text5, { children: [
|
|
1116
|
-
"Active: ",
|
|
1117
|
-
/* @__PURE__ */ jsx5(Text5, { bold: true, children: provider }),
|
|
1118
|
-
" \u2014 set ",
|
|
1119
|
-
/* @__PURE__ */ jsx5(Text5, { bold: true, children: info.envVar }),
|
|
1120
|
-
", or paste a key below to save it to ~/.axon/config.json."
|
|
1121
|
-
] }),
|
|
1122
|
-
info.url ? /* @__PURE__ */ jsxs5(Text5, { color: "gray", children: [
|
|
1123
|
-
"Get a ",
|
|
1124
|
-
provider,
|
|
1125
|
-
" key at: ",
|
|
1126
|
-
info.url
|
|
1127
|
-
] }) : null,
|
|
1128
|
-
/* @__PURE__ */ jsxs5(Box5, { children: [
|
|
1129
|
-
/* @__PURE__ */ jsx5(Text5, { color: "cyan", children: "key \u203A " }),
|
|
1130
|
-
/* @__PURE__ */ jsx5(TextInput2, { value, onChange: setValue, onSubmit: submit, mask: "*" })
|
|
1131
|
-
] })
|
|
1132
|
-
] });
|
|
1133
|
-
}
|
|
1134
|
-
|
|
1135
|
-
// src/ui/runTui.tsx
|
|
1136
|
-
import { jsx as jsx6, jsxs as jsxs6 } from "react/jsx-runtime";
|
|
1137
|
-
function Root({ deps }) {
|
|
1138
|
-
const { cfg, controller, yolo, buildEngine, persistKey } = deps;
|
|
1139
|
-
const [ready, setReady] = useState3(hasUsableKey(cfg));
|
|
1140
|
-
const engineRef = useRef(null);
|
|
1141
|
-
if (ready && !engineRef.current) engineRef.current = buildEngine();
|
|
1142
|
-
if (!ready || !engineRef.current) {
|
|
1143
|
-
return /* @__PURE__ */ jsx6(
|
|
1144
|
-
Setup,
|
|
1145
|
-
{
|
|
1146
|
-
initialProvider: cfg.provider,
|
|
1147
|
-
onSubmit: (provider, key) => {
|
|
1148
|
-
persistKey(provider, key);
|
|
1149
|
-
cfg.provider = provider;
|
|
1150
|
-
cfg.providers[provider] = { ...cfg.providers[provider] ?? {}, apiKey: key };
|
|
1151
|
-
setReady(true);
|
|
1152
|
-
}
|
|
1153
|
-
}
|
|
1154
|
-
);
|
|
1155
|
-
}
|
|
1156
|
-
return /* @__PURE__ */ jsxs6(Box6, { flexDirection: "column", children: [
|
|
1157
|
-
/* @__PURE__ */ jsxs6(Text6, { color: "green", children: [
|
|
1158
|
-
"Axon \xB7 ",
|
|
1159
|
-
cfg.provider,
|
|
1160
|
-
"/",
|
|
1161
|
-
resolveModel(cfg),
|
|
1162
|
-
" \xB7 type a request, Ctrl+C to quit"
|
|
1163
|
-
] }),
|
|
1164
|
-
/* @__PURE__ */ jsx6(
|
|
1165
|
-
App,
|
|
1166
|
-
{
|
|
1167
|
-
engine: engineRef.current,
|
|
1168
|
-
controller,
|
|
1169
|
-
provider: cfg.provider,
|
|
1170
|
-
model: resolveModel(cfg),
|
|
1171
|
-
yolo
|
|
1172
|
-
}
|
|
1173
|
-
)
|
|
1174
|
-
] });
|
|
1175
|
-
}
|
|
1176
|
-
function runTui(opts) {
|
|
1177
|
-
const cfg = loadConfig();
|
|
1178
|
-
if (opts.provider) cfg.provider = opts.provider;
|
|
1179
|
-
if (opts.model) cfg.model = opts.model;
|
|
1180
|
-
const controller = createPermissionController();
|
|
1181
|
-
const deps = {
|
|
1182
|
-
cfg,
|
|
1183
|
-
controller,
|
|
1184
|
-
yolo: Boolean(opts.yolo),
|
|
1185
|
-
persistKey: (provider, key) => {
|
|
1186
|
-
try {
|
|
1187
|
-
setApiKey(provider, key);
|
|
1188
|
-
setConfigValue("provider", provider);
|
|
1189
|
-
} catch {
|
|
1190
|
-
}
|
|
1191
|
-
},
|
|
1192
|
-
buildEngine: () => {
|
|
1193
|
-
const provider = createProvider(cfg);
|
|
1194
|
-
const tools = buildAllTools();
|
|
1195
|
-
const gate = opts.yolo ? allowAllGate : controller.gate;
|
|
1196
|
-
const context = loadProjectContext(process.cwd());
|
|
1197
|
-
const system = buildSystemPrompt({ provider: cfg.provider, model: resolveModel(cfg), tools: "all", context });
|
|
1198
|
-
return new Engine({ provider, tools, system, cwd: process.cwd(), gate });
|
|
1199
|
-
}
|
|
1200
|
-
};
|
|
1201
|
-
if (!process.stdin.isTTY) {
|
|
1202
|
-
process.stderr.write('axon: the interactive chat needs a terminal. For non-interactive use, run: axon -p "your prompt"\n');
|
|
1203
|
-
process.exit(1);
|
|
1204
|
-
}
|
|
1205
|
-
render(/* @__PURE__ */ jsx6(Root, { deps }));
|
|
1206
|
-
}
|
|
1207
|
-
|
|
1208
|
-
// src/cli.ts
|
|
1209
|
-
function redactKeys(cfg) {
|
|
1210
|
-
const providers = cfg.providers;
|
|
1211
|
-
if (!providers || typeof providers !== "object") return cfg;
|
|
1212
|
-
const masked = {};
|
|
1213
|
-
for (const [name, p] of Object.entries(providers)) {
|
|
1214
|
-
masked[name] = p && typeof p === "object" && "apiKey" in p && p.apiKey ? { ...p, apiKey: "***redacted***" } : p;
|
|
1215
|
-
}
|
|
1216
|
-
return { ...cfg, providers: masked };
|
|
1217
|
-
}
|
|
1218
|
-
var program = new Command();
|
|
1219
|
-
program.name("axon").version(VERSION).option("-p, --print <prompt>", "run one prompt non-interactively and stream the result").option("--provider <name>", "override the provider for this run (anthropic | openai | gemini)").option("--model <name>", "override the model for this run").option("--yolo", "allow write/edit/shell tools without prompting (non-interactive)");
|
|
1220
|
-
program.command("config").argument("<action>", "get | set").argument("[key]", "config key (provider | model | <provider>.<baseUrl|model>)").argument("[value]", "value to set").action((action, key, value) => {
|
|
1221
|
-
if (action === "get") {
|
|
1222
|
-
process.stdout.write(JSON.stringify(redactKeys(readConfigFile()), null, 2) + "\n");
|
|
1223
|
-
return;
|
|
1224
|
-
}
|
|
1225
|
-
if (action === "set") {
|
|
1226
|
-
if (!key || value === void 0) throw new Error("Usage: axon config set <key> <value>");
|
|
1227
|
-
setConfigValue(key, value);
|
|
1228
|
-
process.stdout.write(`set ${key} = ${value}
|
|
1229
|
-
`);
|
|
1230
|
-
return;
|
|
1231
|
-
}
|
|
1232
|
-
throw new Error(`Unknown config action "${action}". Use get or set.`);
|
|
1233
|
-
});
|
|
1234
|
-
program.action(() => {
|
|
1235
|
-
const opts = program.opts();
|
|
1236
|
-
main(opts).catch((err) => {
|
|
1237
|
-
process.stderr.write(`\u{1F4A5} ${err instanceof Error ? err.message : String(err)}
|
|
1238
|
-
`);
|
|
1239
|
-
process.exit(1);
|
|
1240
|
-
});
|
|
1241
|
-
});
|
|
1242
|
-
program.parse();
|
|
1243
|
-
async function main(opts) {
|
|
1244
|
-
if (!opts.print) {
|
|
1245
|
-
runTui({ provider: opts.provider, model: opts.model, yolo: opts.yolo });
|
|
1246
|
-
return;
|
|
1247
|
-
}
|
|
1248
|
-
const cfg = loadConfig();
|
|
1249
|
-
if (opts.provider) cfg.provider = opts.provider;
|
|
1250
|
-
if (opts.model) cfg.model = opts.model;
|
|
1251
|
-
const provider = createProvider(cfg);
|
|
1252
|
-
const tools = opts.yolo ? buildAllTools() : buildReadOnlyTools();
|
|
1253
|
-
const gate = opts.yolo ? allowAllGate : denyGate;
|
|
1254
|
-
const context = loadProjectContext(process.cwd());
|
|
1255
|
-
const system = buildSystemPrompt({
|
|
1256
|
-
provider: cfg.provider,
|
|
1257
|
-
model: resolveModel(cfg),
|
|
1258
|
-
tools: opts.yolo ? "all" : "readonly",
|
|
1259
|
-
context
|
|
1260
|
-
});
|
|
1261
|
-
const engine = new Engine({ provider, tools, system, cwd: process.cwd(), gate });
|
|
1262
|
-
printRunner(engine, (s) => process.stdout.write(s));
|
|
1263
|
-
process.stderr.write(`[axon: ${cfg.provider} / ${resolveModel(cfg)}${opts.yolo ? " / yolo" : ""}]
|
|
1264
|
-
`);
|
|
1265
|
-
await engine.submit(opts.print);
|
|
1266
|
-
}
|