@wanghuimvp/axon 0.0.1 → 0.1.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/README.md +97 -56
- package/dist/cli.js +274 -19
- package/package.json +58 -33
package/README.md
CHANGED
|
@@ -1,56 +1,97 @@
|
|
|
1
|
-
# Axon
|
|
2
|
-
|
|
3
|
-
An agentic coding CLI. Axon streams from Anthropic and runs a multi-step tool loop over your codebase — it reads, searches, and reasons across your files to answer a prompt.
|
|
4
|
-
|
|
5
|
-
> Foundation release (`0.0.x`). Non-interactive `axon -p` mode with read-only tools. Interactive TUI
|
|
6
|
-
|
|
7
|
-
## Install
|
|
8
|
-
|
|
9
|
-
```bash
|
|
10
|
-
npm install -g @wanghuimvp/axon
|
|
11
|
-
```
|
|
12
|
-
|
|
13
|
-
##
|
|
14
|
-
|
|
15
|
-
Axon
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
```bash
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
1
|
+
# Axon
|
|
2
|
+
|
|
3
|
+
An agentic coding CLI. Axon streams from Anthropic and runs a multi-step tool loop over your codebase — it reads, searches, and reasons across your files to answer a prompt.
|
|
4
|
+
|
|
5
|
+
> Foundation release (`0.0.x`). Non-interactive `axon -p` mode with read-only tools. Interactive TUI and write/edit/shell tools are on the roadmap.
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npm install -g @wanghuimvp/axon
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Providers
|
|
14
|
+
|
|
15
|
+
Axon speaks to three backends. The OpenAI-compatible provider also drives any
|
|
16
|
+
OpenAI-protocol endpoint (DeepSeek, Qwen, Kimi, Groq, OpenRouter, ollama, LM Studio, …)
|
|
17
|
+
by pointing `baseUrl` at it.
|
|
18
|
+
|
|
19
|
+
| Provider | `provider` value | API key env | Notes |
|
|
20
|
+
|------------|------------------|---------------------|-------|
|
|
21
|
+
| Anthropic | `anthropic` | `ANTHROPIC_API_KEY` | Claude models |
|
|
22
|
+
| OpenAI | `openai` | `OPENAI_API_KEY` | GPT + any OpenAI-compatible endpoint via `baseUrl` |
|
|
23
|
+
| Gemini | `gemini` | `GEMINI_API_KEY` | Google Gemini models |
|
|
24
|
+
|
|
25
|
+
Set the relevant API key in your environment before running:
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
# macOS / Linux
|
|
29
|
+
export OPENAI_API_KEY=sk-...
|
|
30
|
+
# Windows (PowerShell)
|
|
31
|
+
$env:OPENAI_API_KEY="sk-..."
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
### Select a provider/model
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
# Per run:
|
|
38
|
+
axon --provider openai --model gpt-4.1 -p "explain engine.ts"
|
|
39
|
+
|
|
40
|
+
# Persisted:
|
|
41
|
+
axon config set provider gemini
|
|
42
|
+
axon config set gemini.model gemini-2.5-pro
|
|
43
|
+
axon config get
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
### Point at an OpenAI-compatible endpoint (e.g. DeepSeek, ollama)
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
axon config set provider openai
|
|
50
|
+
axon config set openai.baseUrl https://api.deepseek.com/v1
|
|
51
|
+
axon config set openai.model deepseek-chat
|
|
52
|
+
export OPENAI_API_KEY=sk-... # the endpoint's key
|
|
53
|
+
axon -p "summarize src/"
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
For a local ollama server:
|
|
57
|
+
```bash
|
|
58
|
+
axon config set openai.baseUrl http://localhost:11434/v1
|
|
59
|
+
axon config set openai.model qwen2.5-coder
|
|
60
|
+
export OPENAI_API_KEY=ollama # ollama ignores the key but the field must be non-empty
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
## Usage
|
|
64
|
+
|
|
65
|
+
```bash
|
|
66
|
+
axon -p "list the TypeScript files in src and explain what engine.ts does"
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
Axon will stream the model's reasoning, run read-only tools as needed (`read_file`, `list_dir`, `glob`, `grep`), and print the result.
|
|
70
|
+
|
|
71
|
+
### Options
|
|
72
|
+
|
|
73
|
+
- `-p, --print <prompt>` — run one prompt non-interactively and stream the result.
|
|
74
|
+
- `--provider <name>` — select a provider (anthropic, openai, gemini) for this run.
|
|
75
|
+
- `--model <name>` — select a model for this run.
|
|
76
|
+
- `--version` — print the version.
|
|
77
|
+
|
|
78
|
+
## Configuration
|
|
79
|
+
|
|
80
|
+
Optional config file at `~/.axon/config.json`:
|
|
81
|
+
|
|
82
|
+
```json
|
|
83
|
+
{
|
|
84
|
+
"provider": "anthropic",
|
|
85
|
+
"providers": {
|
|
86
|
+
"anthropic": { "model": "claude-opus-4-8" },
|
|
87
|
+
"openai": { "model": "gpt-4.1", "baseUrl": "https://api.openai.com/v1" },
|
|
88
|
+
"gemini": { "model": "gemini-2.5-pro" }
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
API keys are read from environment variables (`ANTHROPIC_API_KEY`, `OPENAI_API_KEY`, `GEMINI_API_KEY`) and never written to disk.
|
|
94
|
+
|
|
95
|
+
## License
|
|
96
|
+
|
|
97
|
+
MIT
|
package/dist/cli.js
CHANGED
|
@@ -10,13 +10,26 @@ var VERSION = "0.0.1";
|
|
|
10
10
|
import { readFileSync } from "node:fs";
|
|
11
11
|
import { homedir } from "node:os";
|
|
12
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
|
+
};
|
|
13
18
|
var DEFAULTS = {
|
|
14
19
|
provider: "anthropic",
|
|
15
|
-
model: "claude-opus-4-8",
|
|
16
20
|
providers: {
|
|
17
|
-
anthropic: { apiKey: "env:ANTHROPIC_API_KEY" }
|
|
21
|
+
anthropic: { apiKey: "env:ANTHROPIC_API_KEY" },
|
|
22
|
+
openai: { apiKey: "env:OPENAI_API_KEY" },
|
|
23
|
+
gemini: { apiKey: "env:GEMINI_API_KEY" }
|
|
18
24
|
}
|
|
19
25
|
};
|
|
26
|
+
function resolveModel(cfg) {
|
|
27
|
+
const model = cfg.model ?? cfg.providers[cfg.provider]?.model ?? DEFAULT_MODELS[cfg.provider];
|
|
28
|
+
if (!model) {
|
|
29
|
+
throw new Error(`No model configured for provider "${cfg.provider}". Set "model" in ~/.axon/config.json or pass --model.`);
|
|
30
|
+
}
|
|
31
|
+
return model;
|
|
32
|
+
}
|
|
20
33
|
function resolveEnvRefs(cfg) {
|
|
21
34
|
const providers = {};
|
|
22
35
|
for (const [name, p] of Object.entries(cfg.providers)) {
|
|
@@ -34,14 +47,50 @@ function loadConfig() {
|
|
|
34
47
|
}
|
|
35
48
|
const merged = {
|
|
36
49
|
provider: fileCfg.provider ?? DEFAULTS.provider,
|
|
37
|
-
model: fileCfg.model
|
|
50
|
+
model: fileCfg.model,
|
|
38
51
|
providers: { ...DEFAULTS.providers, ...fileCfg.providers ?? {} }
|
|
39
52
|
};
|
|
40
53
|
return resolveEnvRefs(merged);
|
|
41
54
|
}
|
|
42
55
|
|
|
56
|
+
// src/config/configFile.ts
|
|
57
|
+
import { readFileSync as readFileSync2, writeFileSync, mkdirSync } from "node:fs";
|
|
58
|
+
import { homedir as homedir2 } from "node:os";
|
|
59
|
+
import { join as join2, dirname } from "node:path";
|
|
60
|
+
function configPath() {
|
|
61
|
+
return join2(homedir2(), ".axon", "config.json");
|
|
62
|
+
}
|
|
63
|
+
function readConfigFile() {
|
|
64
|
+
try {
|
|
65
|
+
return JSON.parse(readFileSync2(configPath(), "utf8"));
|
|
66
|
+
} catch {
|
|
67
|
+
return {};
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
function setConfigValue(key, value) {
|
|
71
|
+
const cfg = readConfigFile();
|
|
72
|
+
if (key === "provider" || key === "model") {
|
|
73
|
+
cfg[key] = value;
|
|
74
|
+
} else if (key.includes(".")) {
|
|
75
|
+
const [provider, field] = key.split(".", 2);
|
|
76
|
+
const ALLOWED = /* @__PURE__ */ new Set(["baseUrl", "model"]);
|
|
77
|
+
if (!ALLOWED.has(field)) {
|
|
78
|
+
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.`);
|
|
79
|
+
}
|
|
80
|
+
const providers = cfg.providers ??= {};
|
|
81
|
+
(providers[provider] ??= {})[field] = value;
|
|
82
|
+
} else {
|
|
83
|
+
throw new Error(`Unknown config key "${key}". Use "provider", "model", or "<provider>.<baseUrl|model>".`);
|
|
84
|
+
}
|
|
85
|
+
const path = configPath();
|
|
86
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
87
|
+
writeFileSync(path, JSON.stringify(cfg, null, 2) + "\n");
|
|
88
|
+
}
|
|
89
|
+
|
|
43
90
|
// src/providers/registry.ts
|
|
44
91
|
import Anthropic from "@anthropic-ai/sdk";
|
|
92
|
+
import OpenAI from "openai";
|
|
93
|
+
import { GoogleGenAI } from "@google/genai";
|
|
45
94
|
|
|
46
95
|
// src/providers/anthropic.ts
|
|
47
96
|
function mapStop(reason) {
|
|
@@ -131,20 +180,206 @@ var AnthropicProvider = class {
|
|
|
131
180
|
}
|
|
132
181
|
};
|
|
133
182
|
|
|
183
|
+
// src/providers/openai.ts
|
|
184
|
+
function textOf(blocks) {
|
|
185
|
+
return blocks.filter((b) => b.type === "text").map((b) => b.text).join("");
|
|
186
|
+
}
|
|
187
|
+
function toOpenAIMessages(system, messages) {
|
|
188
|
+
const out = [{ role: "system", content: system }];
|
|
189
|
+
for (const m of messages) {
|
|
190
|
+
if (m.role === "tool") {
|
|
191
|
+
out.push({ role: "tool", tool_call_id: m.toolCallId, content: m.content });
|
|
192
|
+
continue;
|
|
193
|
+
}
|
|
194
|
+
if (m.role === "user") {
|
|
195
|
+
out.push({ role: "user", content: textOf(m.content) });
|
|
196
|
+
continue;
|
|
197
|
+
}
|
|
198
|
+
const toolCalls = m.content.filter((b) => b.type === "tool_call").map((b) => {
|
|
199
|
+
const t = b;
|
|
200
|
+
return { id: t.id, type: "function", function: { name: t.name, arguments: JSON.stringify(t.args ?? {}) } };
|
|
201
|
+
});
|
|
202
|
+
const msg = { role: "assistant", content: textOf(m.content) };
|
|
203
|
+
if (toolCalls.length) msg.tool_calls = toolCalls;
|
|
204
|
+
out.push(msg);
|
|
205
|
+
}
|
|
206
|
+
return out;
|
|
207
|
+
}
|
|
208
|
+
function toOpenAITools(tools) {
|
|
209
|
+
if (!tools.length) return void 0;
|
|
210
|
+
return tools.map((t) => ({ type: "function", function: { name: t.name, description: t.description, parameters: t.parameters } }));
|
|
211
|
+
}
|
|
212
|
+
var OpenAIProvider = class {
|
|
213
|
+
constructor(deps) {
|
|
214
|
+
this.deps = deps;
|
|
215
|
+
}
|
|
216
|
+
callCounter = 0;
|
|
217
|
+
async *stream(req) {
|
|
218
|
+
const tools = toOpenAITools(req.tools);
|
|
219
|
+
const stream = await this.deps.client.chat.completions.create({
|
|
220
|
+
model: this.deps.model,
|
|
221
|
+
stream: true,
|
|
222
|
+
messages: toOpenAIMessages(req.system, req.messages),
|
|
223
|
+
...tools ? { tools } : {}
|
|
224
|
+
});
|
|
225
|
+
const calls = /* @__PURE__ */ new Map();
|
|
226
|
+
let finishReason = null;
|
|
227
|
+
for await (const chunk of stream) {
|
|
228
|
+
const choice = chunk.choices?.[0];
|
|
229
|
+
if (!choice) continue;
|
|
230
|
+
const delta = choice.delta ?? {};
|
|
231
|
+
if (typeof delta.content === "string" && delta.content.length) {
|
|
232
|
+
yield { type: "text_delta", text: delta.content };
|
|
233
|
+
}
|
|
234
|
+
for (const tc of delta.tool_calls ?? []) {
|
|
235
|
+
const cur = calls.get(tc.index) ?? { id: "", name: "", args: "" };
|
|
236
|
+
if (tc.id) cur.id = tc.id;
|
|
237
|
+
if (tc.function?.name) cur.name = tc.function.name;
|
|
238
|
+
if (tc.function?.arguments) cur.args += tc.function.arguments;
|
|
239
|
+
calls.set(tc.index, cur);
|
|
240
|
+
}
|
|
241
|
+
if (choice.finish_reason) finishReason = choice.finish_reason;
|
|
242
|
+
}
|
|
243
|
+
for (const c of [...calls.entries()].sort((a, b) => a[0] - b[0]).map((e) => e[1])) {
|
|
244
|
+
const id = c.id || `openai-call-${this.callCounter++}`;
|
|
245
|
+
let args;
|
|
246
|
+
try {
|
|
247
|
+
args = c.args.trim() ? JSON.parse(c.args) : {};
|
|
248
|
+
} catch (err) {
|
|
249
|
+
throw new Error(`Tool "${c.name}" (id=${id}): invalid tool-call JSON: ${JSON.stringify(c.args)}`, { cause: err });
|
|
250
|
+
}
|
|
251
|
+
yield { type: "tool_call", id, name: c.name, args };
|
|
252
|
+
}
|
|
253
|
+
const stopReason = calls.size ? "tool_use" : finishReason === "length" ? "max_tokens" : "end";
|
|
254
|
+
yield { type: "done", stopReason };
|
|
255
|
+
}
|
|
256
|
+
};
|
|
257
|
+
|
|
258
|
+
// src/providers/gemini.ts
|
|
259
|
+
function textOf2(blocks) {
|
|
260
|
+
return blocks.filter((b) => b.type === "text").map((b) => b.text).join("");
|
|
261
|
+
}
|
|
262
|
+
function buildIdToName(messages) {
|
|
263
|
+
const map = /* @__PURE__ */ new Map();
|
|
264
|
+
for (const m of messages) {
|
|
265
|
+
if (m.role === "assistant") {
|
|
266
|
+
for (const b of m.content) {
|
|
267
|
+
if (b.type === "tool_call") map.set(b.id, b.name);
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
return map;
|
|
272
|
+
}
|
|
273
|
+
function toGeminiContents(messages) {
|
|
274
|
+
const idToName = buildIdToName(messages);
|
|
275
|
+
const out = [];
|
|
276
|
+
let i = 0;
|
|
277
|
+
while (i < messages.length) {
|
|
278
|
+
const m = messages[i];
|
|
279
|
+
if (m.role === "tool") {
|
|
280
|
+
const parts2 = [];
|
|
281
|
+
while (i < messages.length && messages[i].role === "tool") {
|
|
282
|
+
const t = messages[i];
|
|
283
|
+
parts2.push({ functionResponse: { name: idToName.get(t.toolCallId) ?? "unknown", response: { result: t.content } } });
|
|
284
|
+
i++;
|
|
285
|
+
}
|
|
286
|
+
out.push({ role: "user", parts: parts2 });
|
|
287
|
+
continue;
|
|
288
|
+
}
|
|
289
|
+
if (m.role === "user") {
|
|
290
|
+
out.push({ role: "user", parts: [{ text: textOf2(m.content) }] });
|
|
291
|
+
i++;
|
|
292
|
+
continue;
|
|
293
|
+
}
|
|
294
|
+
const parts = [];
|
|
295
|
+
for (const b of m.content) {
|
|
296
|
+
if (b.type === "text") parts.push({ text: b.text });
|
|
297
|
+
else if (b.type === "tool_call") parts.push({ functionCall: { name: b.name, args: b.args } });
|
|
298
|
+
}
|
|
299
|
+
out.push({ role: "model", parts });
|
|
300
|
+
i++;
|
|
301
|
+
}
|
|
302
|
+
return out;
|
|
303
|
+
}
|
|
304
|
+
function toGeminiTools(tools) {
|
|
305
|
+
if (!tools.length) return void 0;
|
|
306
|
+
return [{ functionDeclarations: tools.map((t) => ({ name: t.name, description: t.description, parameters: t.parameters })) }];
|
|
307
|
+
}
|
|
308
|
+
var GeminiProvider = class {
|
|
309
|
+
constructor(deps) {
|
|
310
|
+
this.deps = deps;
|
|
311
|
+
}
|
|
312
|
+
callCounter = 0;
|
|
313
|
+
async *stream(req) {
|
|
314
|
+
const stream = await this.deps.client.models.generateContentStream({
|
|
315
|
+
model: this.deps.model,
|
|
316
|
+
contents: toGeminiContents(req.messages),
|
|
317
|
+
config: {
|
|
318
|
+
systemInstruction: req.system,
|
|
319
|
+
tools: toGeminiTools(req.tools)
|
|
320
|
+
}
|
|
321
|
+
});
|
|
322
|
+
let sawToolCall = false;
|
|
323
|
+
let finishReason;
|
|
324
|
+
for await (const chunk of stream) {
|
|
325
|
+
const cand = chunk.candidates?.[0];
|
|
326
|
+
const parts = cand?.content?.parts ?? [];
|
|
327
|
+
for (const part of parts) {
|
|
328
|
+
if (typeof part.text === "string" && part.text.length) {
|
|
329
|
+
yield { type: "text_delta", text: part.text };
|
|
330
|
+
} else if (part.functionCall) {
|
|
331
|
+
sawToolCall = true;
|
|
332
|
+
yield {
|
|
333
|
+
type: "tool_call",
|
|
334
|
+
id: `gemini-call-${this.callCounter++}`,
|
|
335
|
+
name: part.functionCall.name,
|
|
336
|
+
args: part.functionCall.args ?? {}
|
|
337
|
+
};
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
if (cand?.finishReason) finishReason = cand.finishReason;
|
|
341
|
+
}
|
|
342
|
+
const stopReason = sawToolCall ? "tool_use" : finishReason === "MAX_TOKENS" ? "max_tokens" : "end";
|
|
343
|
+
yield { type: "done", stopReason };
|
|
344
|
+
}
|
|
345
|
+
};
|
|
346
|
+
|
|
134
347
|
// src/providers/registry.ts
|
|
348
|
+
function requireKey(provider, apiKey) {
|
|
349
|
+
if (!apiKey || !apiKey.trim()) {
|
|
350
|
+
throw new Error(`Missing ${provider} API key. Set it via env or ~/.axon/config.json.`);
|
|
351
|
+
}
|
|
352
|
+
return apiKey;
|
|
353
|
+
}
|
|
135
354
|
function createProvider(cfg) {
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
355
|
+
const pc = cfg.providers[cfg.provider] ?? {};
|
|
356
|
+
switch (cfg.provider) {
|
|
357
|
+
case "anthropic": {
|
|
358
|
+
const apiKey = requireKey("Anthropic", pc.apiKey);
|
|
359
|
+
const model = resolveModel(cfg);
|
|
360
|
+
const client = new Anthropic({ apiKey });
|
|
361
|
+
return new AnthropicProvider({ client, model });
|
|
362
|
+
}
|
|
363
|
+
case "openai": {
|
|
364
|
+
const apiKey = requireKey("OpenAI", pc.apiKey);
|
|
365
|
+
const model = resolveModel(cfg);
|
|
366
|
+
const client = new OpenAI({ apiKey, baseURL: pc.baseUrl });
|
|
367
|
+
return new OpenAIProvider({ client, model });
|
|
368
|
+
}
|
|
369
|
+
case "gemini": {
|
|
370
|
+
const apiKey = requireKey("Gemini", pc.apiKey);
|
|
371
|
+
const model = resolveModel(cfg);
|
|
372
|
+
const client = new GoogleGenAI({ apiKey });
|
|
373
|
+
return new GeminiProvider({ client, model });
|
|
374
|
+
}
|
|
375
|
+
default:
|
|
376
|
+
throw new Error(`Unsupported provider: ${cfg.provider}`);
|
|
141
377
|
}
|
|
142
|
-
throw new Error(`Unsupported provider: ${cfg.provider}`);
|
|
143
378
|
}
|
|
144
379
|
|
|
145
380
|
// src/tools/fs.ts
|
|
146
381
|
import { readFile, readdir } from "node:fs/promises";
|
|
147
|
-
import { join as
|
|
382
|
+
import { join as join3, resolve as resolve2 } from "node:path";
|
|
148
383
|
import fg from "fast-glob";
|
|
149
384
|
|
|
150
385
|
// src/tools/paths.ts
|
|
@@ -249,7 +484,7 @@ var grepTool = {
|
|
|
249
484
|
const files = await fg(glob, { cwd: root, onlyFiles: true, dot: false });
|
|
250
485
|
const hits = [];
|
|
251
486
|
for (const rel of files) {
|
|
252
|
-
const text = await readFile(
|
|
487
|
+
const text = await readFile(join3(root, rel), "utf8").catch(() => "");
|
|
253
488
|
text.split("\n").forEach((line, i) => {
|
|
254
489
|
if (re.test(line)) hits.push(`${rel}:${i + 1}:${line}`);
|
|
255
490
|
});
|
|
@@ -399,23 +634,43 @@ function truncate(s, max = 500) {
|
|
|
399
634
|
// src/cli.ts
|
|
400
635
|
var SYSTEM = `You are Axon, an agentic coding assistant. Use the provided tools to inspect the project and answer precisely. When done, stop calling tools.`;
|
|
401
636
|
var program = new Command();
|
|
402
|
-
program.name("axon").version(VERSION).option("-p, --print <prompt>", "run one prompt non-interactively and stream the result");
|
|
637
|
+
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");
|
|
638
|
+
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) => {
|
|
639
|
+
if (action === "get") {
|
|
640
|
+
process.stdout.write(JSON.stringify(readConfigFile(), null, 2) + "\n");
|
|
641
|
+
return;
|
|
642
|
+
}
|
|
643
|
+
if (action === "set") {
|
|
644
|
+
if (!key || value === void 0) throw new Error("Usage: axon config set <key> <value>");
|
|
645
|
+
setConfigValue(key, value);
|
|
646
|
+
process.stdout.write(`set ${key} = ${value}
|
|
647
|
+
`);
|
|
648
|
+
return;
|
|
649
|
+
}
|
|
650
|
+
throw new Error(`Unknown config action "${action}". Use get or set.`);
|
|
651
|
+
});
|
|
652
|
+
program.action(() => {
|
|
653
|
+
const opts = program.opts();
|
|
654
|
+
main(opts).catch((err) => {
|
|
655
|
+
process.stderr.write(`\u{1F4A5} ${err instanceof Error ? err.message : String(err)}
|
|
656
|
+
`);
|
|
657
|
+
process.exit(1);
|
|
658
|
+
});
|
|
659
|
+
});
|
|
403
660
|
program.parse();
|
|
404
|
-
|
|
405
|
-
async function main() {
|
|
661
|
+
async function main(opts) {
|
|
406
662
|
if (!opts.print) {
|
|
407
663
|
process.stdout.write('Interactive TUI not built yet \u2014 use: axon -p "your prompt"\n');
|
|
408
664
|
return;
|
|
409
665
|
}
|
|
410
666
|
const cfg = loadConfig();
|
|
667
|
+
if (opts.provider) cfg.provider = opts.provider;
|
|
668
|
+
if (opts.model) cfg.model = opts.model;
|
|
411
669
|
const provider = createProvider(cfg);
|
|
412
670
|
const tools = buildReadOnlyTools();
|
|
413
671
|
const engine = new Engine({ provider, tools, system: SYSTEM, cwd: process.cwd() });
|
|
414
672
|
printRunner(engine, (s) => process.stdout.write(s));
|
|
673
|
+
process.stderr.write(`[axon: ${cfg.provider} / ${resolveModel(cfg)}]
|
|
674
|
+
`);
|
|
415
675
|
await engine.submit(opts.print);
|
|
416
676
|
}
|
|
417
|
-
main().catch((err) => {
|
|
418
|
-
process.stderr.write(`\u{1F4A5} ${err instanceof Error ? err.message : String(err)}
|
|
419
|
-
`);
|
|
420
|
-
process.exit(1);
|
|
421
|
-
});
|
package/package.json
CHANGED
|
@@ -1,33 +1,58 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "@wanghuimvp/axon",
|
|
3
|
-
"version": "0.0
|
|
4
|
-
"description": "Axon —
|
|
5
|
-
"type": "module",
|
|
6
|
-
"bin": {
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
"
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
"
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
"
|
|
16
|
-
|
|
17
|
-
"
|
|
18
|
-
"
|
|
19
|
-
"
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
"
|
|
23
|
-
"
|
|
24
|
-
"
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
"
|
|
29
|
-
"
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
1
|
+
{
|
|
2
|
+
"name": "@wanghuimvp/axon",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Axon — a multi-provider agentic coding CLI (Anthropic, OpenAI + OpenAI-compatible endpoints, Gemini). Runs a multi-step tool loop over your codebase.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"axon": "dist/cli.js"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"dist"
|
|
11
|
+
],
|
|
12
|
+
"engines": {
|
|
13
|
+
"node": ">=20"
|
|
14
|
+
},
|
|
15
|
+
"license": "MIT",
|
|
16
|
+
"keywords": [
|
|
17
|
+
"cli",
|
|
18
|
+
"agent",
|
|
19
|
+
"agentic",
|
|
20
|
+
"coding",
|
|
21
|
+
"anthropic",
|
|
22
|
+
"claude",
|
|
23
|
+
"ai",
|
|
24
|
+
"assistant",
|
|
25
|
+
"axon"
|
|
26
|
+
],
|
|
27
|
+
"repository": {
|
|
28
|
+
"type": "git",
|
|
29
|
+
"url": "git+https://github.com/Wade-DevCode/axon.git"
|
|
30
|
+
},
|
|
31
|
+
"homepage": "https://github.com/Wade-DevCode/axon#readme",
|
|
32
|
+
"bugs": {
|
|
33
|
+
"url": "https://github.com/Wade-DevCode/axon/issues"
|
|
34
|
+
},
|
|
35
|
+
"publishConfig": {
|
|
36
|
+
"access": "public"
|
|
37
|
+
},
|
|
38
|
+
"scripts": {
|
|
39
|
+
"dev": "tsx src/cli.ts",
|
|
40
|
+
"test": "vitest run",
|
|
41
|
+
"build": "esbuild src/cli.ts --bundle --platform=node --format=esm --outfile=dist/cli.js --packages=external",
|
|
42
|
+
"prepublishOnly": "npm test && npm run build"
|
|
43
|
+
},
|
|
44
|
+
"dependencies": {
|
|
45
|
+
"@anthropic-ai/sdk": "^0.40.0",
|
|
46
|
+
"@google/genai": "^1.52.0",
|
|
47
|
+
"commander": "^12.0.0",
|
|
48
|
+
"fast-glob": "^3.3.0",
|
|
49
|
+
"openai": "^4.104.0"
|
|
50
|
+
},
|
|
51
|
+
"devDependencies": {
|
|
52
|
+
"@types/node": "^20.0.0",
|
|
53
|
+
"esbuild": "^0.23.0",
|
|
54
|
+
"tsx": "^4.0.0",
|
|
55
|
+
"typescript": "^5.5.0",
|
|
56
|
+
"vitest": "^2.0.0"
|
|
57
|
+
}
|
|
58
|
+
}
|