@wanghuimvp/axon 0.4.1 → 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/dist/cli.js DELETED
@@ -1,1256 +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/ui/permissionController.ts
838
- function createPermissionController() {
839
- const sessionAllow = /* @__PURE__ */ new Set();
840
- const subscribers = /* @__PURE__ */ new Set();
841
- const queue = [];
842
- let active = null;
843
- let currentPending = null;
844
- const notify = (p) => {
845
- currentPending = p;
846
- for (const fn of subscribers) fn(p);
847
- };
848
- const pump = () => {
849
- if (active || queue.length === 0) return;
850
- active = queue.shift();
851
- notify({ req: active.req });
852
- };
853
- const gate = (req) => {
854
- if (sessionAllow.has(req.name)) {
855
- return Promise.resolve({ allow: true, reason: "allowed (remembered this session)" });
856
- }
857
- return new Promise((res) => {
858
- queue.push({ req, settle: res });
859
- pump();
860
- });
861
- };
862
- const resolve3 = (decision) => {
863
- if (!active) return;
864
- const { req, settle } = active;
865
- active = null;
866
- if (decision === "always") sessionAllow.add(req.name);
867
- settle({
868
- allow: decision !== "deny",
869
- reason: decision === "deny" ? `permission denied by user: ${req.name}` : `allowed (${decision})`
870
- });
871
- pump();
872
- if (!active) notify(null);
873
- };
874
- const subscribe = (fn) => {
875
- subscribers.add(fn);
876
- return () => {
877
- subscribers.delete(fn);
878
- };
879
- };
880
- const getPending = () => currentPending;
881
- return { gate, subscribe, getPending, resolve: resolve3 };
882
- }
883
-
884
- // src/config/credentials.ts
885
- function hasUsableKey(cfg) {
886
- const k = cfg.providers[cfg.provider]?.apiKey;
887
- return typeof k === "string" && k.trim().length > 0;
888
- }
889
- var INFO = {
890
- anthropic: { envVar: "ANTHROPIC_API_KEY", url: "https://console.anthropic.com/settings/keys" },
891
- openai: { envVar: "OPENAI_API_KEY", url: "https://platform.openai.com/api-keys" },
892
- gemini: { envVar: "GEMINI_API_KEY", url: "https://aistudio.google.com/apikey" }
893
- };
894
- function keyProviderInfo(provider) {
895
- return INFO[provider] ?? { envVar: `${provider.toUpperCase()}_API_KEY`, url: "" };
896
- }
897
-
898
- // src/ui/app.tsx
899
- import { useEffect, useState, useSyncExternalStore, useCallback } from "react";
900
- import { Box as Box4, Text as Text4, useInput as useInput2 } from "ink";
901
- import TextInput from "ink-text-input";
902
-
903
- // src/ui/components/MessageView.tsx
904
- import { Box, Text } from "ink";
905
- import { jsx, jsxs } from "react/jsx-runtime";
906
- function toolIcon(status) {
907
- if (status === "running") return "\u23F3";
908
- return status === "ok" ? "\u2705" : "\u274C";
909
- }
910
- function truncate3(s, max = 300) {
911
- return s.length > max ? `${s.slice(0, max)}\u2026` : s;
912
- }
913
- function MessageView({ items }) {
914
- return /* @__PURE__ */ jsx(Box, { flexDirection: "column", children: items.map((it, i) => {
915
- if (it.kind === "user") {
916
- return /* @__PURE__ */ jsxs(Text, { children: [
917
- /* @__PURE__ */ jsxs(Text, { color: "cyan", bold: true, children: [
918
- "You",
919
- " "
920
- ] }),
921
- it.text
922
- ] }, i);
923
- }
924
- if (it.kind === "assistant") {
925
- return /* @__PURE__ */ jsxs(Text, { children: [
926
- /* @__PURE__ */ jsxs(Text, { color: "green", bold: true, children: [
927
- "Axon",
928
- " "
929
- ] }),
930
- it.text
931
- ] }, i);
932
- }
933
- if (it.kind === "error") {
934
- return /* @__PURE__ */ jsxs(Text, { color: "red", children: [
935
- "\u{1F4A5} ",
936
- it.text
937
- ] }, i);
938
- }
939
- if (it.kind === "tool") {
940
- return /* @__PURE__ */ jsxs(Text, { color: "gray", children: [
941
- toolIcon(it.status),
942
- " ",
943
- it.name,
944
- "(",
945
- truncate3(JSON.stringify(it.args ?? {}), 60),
946
- ")",
947
- it.output ? ` \u2014 ${truncate3(it.output)}` : ""
948
- ] }, i);
949
- }
950
- return null;
951
- }) });
952
- }
953
-
954
- // src/ui/components/PermissionPrompt.tsx
955
- import { Box as Box2, Text as Text2, useInput } from "ink";
956
- import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
957
- function summarize(args) {
958
- const s = JSON.stringify(args ?? {});
959
- return s.length > 80 ? `${s.slice(0, 80)}\u2026` : s;
960
- }
961
- function decisionForKey(input) {
962
- if (input === "a") return "once";
963
- if (input === "A") return "always";
964
- if (input === "d") return "deny";
965
- return null;
966
- }
967
- function PermissionPrompt({ req, onDecide }) {
968
- useInput((input) => {
969
- const d = decisionForKey(input);
970
- if (d) onDecide(d);
971
- });
972
- return /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", children: [
973
- /* @__PURE__ */ jsxs2(Text2, { color: "yellow", children: [
974
- "\u{1F512} ",
975
- req.name,
976
- "(",
977
- summarize(req.args),
978
- ")"
979
- ] }),
980
- /* @__PURE__ */ jsx2(Text2, { color: "yellow", children: "[a] allow once / [A] always this session / [d] deny" })
981
- ] });
982
- }
983
-
984
- // src/ui/components/StatusBar.tsx
985
- import { Box as Box3, Text as Text3 } from "ink";
986
- import { jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
987
- function StatusBar({ provider, model, running, yolo }) {
988
- return /* @__PURE__ */ jsx3(Box3, { children: /* @__PURE__ */ jsxs3(Text3, { color: "gray", children: [
989
- "[axon: ",
990
- provider,
991
- "/",
992
- model,
993
- yolo ? " \xB7 yolo" : "",
994
- " \xB7 ",
995
- running ? "working\u2026" : "ready",
996
- " \xB7 ^C quit]"
997
- ] }) });
998
- }
999
-
1000
- // src/ui/app.tsx
1001
- import { jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
1002
- function reduceEvent(items, e) {
1003
- switch (e.type) {
1004
- case "text_delta": {
1005
- const last = items[items.length - 1];
1006
- if (last && last.kind === "assistant") {
1007
- return [...items.slice(0, -1), { kind: "assistant", text: last.text + e.text }];
1008
- }
1009
- return [...items, { kind: "assistant", text: e.text }];
1010
- }
1011
- case "tool_start":
1012
- return [...items, { kind: "tool", id: e.id, name: e.name, args: e.args, status: "running" }];
1013
- case "tool_end":
1014
- return items.map(
1015
- (it) => it.kind === "tool" && it.id === e.id ? { ...it, status: e.ok ? "ok" : "fail", output: e.output } : it
1016
- );
1017
- default:
1018
- return items;
1019
- }
1020
- }
1021
- function usePendingPermission(controller) {
1022
- const subscribe = useCallback((onChange) => controller.subscribe(() => onChange()), [controller]);
1023
- const getSnapshot = useCallback(() => controller.getPending(), [controller]);
1024
- const pending = useSyncExternalStore(subscribe, getSnapshot, getSnapshot);
1025
- return pending ? pending.req : null;
1026
- }
1027
- function App({ engine, controller, provider, model, yolo }) {
1028
- const [items, setItems] = useState([]);
1029
- const [input, setInput] = useState("");
1030
- const [running, setRunning] = useState(false);
1031
- const pending = usePendingPermission(controller);
1032
- useEffect(() => {
1033
- engine.on((e) => setItems((prev) => reduceEvent(prev, e)));
1034
- }, [engine]);
1035
- useInput2(
1036
- (_input, key) => {
1037
- if (key.escape && !running) setInput("");
1038
- },
1039
- { isActive: !pending }
1040
- );
1041
- const handleSubmit = (text) => {
1042
- if (!text.trim() || running) return;
1043
- setItems((prev) => [...prev, { kind: "user", text }]);
1044
- setInput("");
1045
- setRunning(true);
1046
- engine.submit(text).catch((err) => {
1047
- const msg = err instanceof Error ? err.message : String(err);
1048
- setItems((prev) => [...prev, { kind: "error", text: msg }]);
1049
- }).finally(() => setRunning(false));
1050
- };
1051
- return /* @__PURE__ */ jsxs4(Box4, { flexDirection: "column", children: [
1052
- /* @__PURE__ */ jsx4(MessageView, { items }),
1053
- 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: [
1054
- /* @__PURE__ */ jsx4(Text4, { color: "cyan", children: "\u203A " }),
1055
- /* @__PURE__ */ jsx4(TextInput, { value: input, onChange: setInput, onSubmit: handleSubmit })
1056
- ] }),
1057
- /* @__PURE__ */ jsx4(StatusBar, { provider, model, running, yolo })
1058
- ] });
1059
- }
1060
-
1061
- // src/ui/Setup.tsx
1062
- import { useState as useState2 } from "react";
1063
- import { Box as Box5, Text as Text5, useInput as useInput3 } from "ink";
1064
- import TextInput2 from "ink-text-input";
1065
- import { jsx as jsx5, jsxs as jsxs5 } from "react/jsx-runtime";
1066
- var SETUP_PROVIDERS = ["anthropic", "openai", "gemini"];
1067
- function trimmedKey(v) {
1068
- const t = v.trim();
1069
- return t ? t : null;
1070
- }
1071
- function providerForDigit(input) {
1072
- const idx = Number(input) - 1;
1073
- return Number.isInteger(idx) && idx >= 0 && idx < SETUP_PROVIDERS.length ? SETUP_PROVIDERS[idx] : null;
1074
- }
1075
- function Setup({
1076
- initialProvider,
1077
- onSubmit
1078
- }) {
1079
- const start = SETUP_PROVIDERS.includes(initialProvider) ? initialProvider : "anthropic";
1080
- const [provider, setProvider] = useState2(start);
1081
- const [value, setValue] = useState2("");
1082
- useInput3((input) => {
1083
- const picked = providerForDigit(input);
1084
- if (picked) setProvider(picked);
1085
- });
1086
- const info = keyProviderInfo(provider);
1087
- const submit = (v) => {
1088
- const k = trimmedKey(v);
1089
- if (k) onSubmit(provider, k);
1090
- };
1091
- return /* @__PURE__ */ jsxs5(Box5, { flexDirection: "column", children: [
1092
- /* @__PURE__ */ jsx5(Text5, { color: "yellow", children: "Welcome to Axon. Pick a provider, then paste its API key." }),
1093
- SETUP_PROVIDERS.map((p, i) => /* @__PURE__ */ jsxs5(Text5, { color: p === provider ? "cyan" : "gray", children: [
1094
- p === provider ? "\u276F" : " ",
1095
- " [",
1096
- i + 1,
1097
- "] ",
1098
- p
1099
- ] }, p)),
1100
- /* @__PURE__ */ jsxs5(Text5, { children: [
1101
- "Active: ",
1102
- /* @__PURE__ */ jsx5(Text5, { bold: true, children: provider }),
1103
- " \u2014 set ",
1104
- /* @__PURE__ */ jsx5(Text5, { bold: true, children: info.envVar }),
1105
- ", or paste a key below to save it to ~/.axon/config.json."
1106
- ] }),
1107
- info.url ? /* @__PURE__ */ jsxs5(Text5, { color: "gray", children: [
1108
- "Get a ",
1109
- provider,
1110
- " key at: ",
1111
- info.url
1112
- ] }) : null,
1113
- /* @__PURE__ */ jsxs5(Box5, { children: [
1114
- /* @__PURE__ */ jsx5(Text5, { color: "cyan", children: "key \u203A " }),
1115
- /* @__PURE__ */ jsx5(TextInput2, { value, onChange: setValue, onSubmit: submit, mask: "*" })
1116
- ] })
1117
- ] });
1118
- }
1119
-
1120
- // src/ui/runTui.tsx
1121
- import { jsx as jsx6, jsxs as jsxs6 } from "react/jsx-runtime";
1122
- var TUI_SYSTEM = `You are Axon, an interactive agentic coding assistant. 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. Explain briefly what you are doing.`;
1123
- function Root({ deps }) {
1124
- const { cfg, controller, yolo, buildEngine, persistKey } = deps;
1125
- const [ready, setReady] = useState3(hasUsableKey(cfg));
1126
- const engineRef = useRef(null);
1127
- if (ready && !engineRef.current) engineRef.current = buildEngine();
1128
- if (!ready || !engineRef.current) {
1129
- return /* @__PURE__ */ jsx6(
1130
- Setup,
1131
- {
1132
- initialProvider: cfg.provider,
1133
- onSubmit: (provider, key) => {
1134
- persistKey(provider, key);
1135
- cfg.provider = provider;
1136
- cfg.providers[provider] = { ...cfg.providers[provider] ?? {}, apiKey: key };
1137
- setReady(true);
1138
- }
1139
- }
1140
- );
1141
- }
1142
- return /* @__PURE__ */ jsxs6(Box6, { flexDirection: "column", children: [
1143
- /* @__PURE__ */ jsxs6(Text6, { color: "green", children: [
1144
- "Axon \xB7 ",
1145
- cfg.provider,
1146
- "/",
1147
- resolveModel(cfg),
1148
- " \xB7 type a request, Ctrl+C to quit"
1149
- ] }),
1150
- /* @__PURE__ */ jsx6(
1151
- App,
1152
- {
1153
- engine: engineRef.current,
1154
- controller,
1155
- provider: cfg.provider,
1156
- model: resolveModel(cfg),
1157
- yolo
1158
- }
1159
- )
1160
- ] });
1161
- }
1162
- function runTui(opts) {
1163
- const cfg = loadConfig();
1164
- if (opts.provider) cfg.provider = opts.provider;
1165
- if (opts.model) cfg.model = opts.model;
1166
- const controller = createPermissionController();
1167
- const deps = {
1168
- cfg,
1169
- controller,
1170
- yolo: Boolean(opts.yolo),
1171
- persistKey: (provider, key) => {
1172
- try {
1173
- setApiKey(provider, key);
1174
- setConfigValue("provider", provider);
1175
- } catch {
1176
- }
1177
- },
1178
- buildEngine: () => {
1179
- const provider = createProvider(cfg);
1180
- const tools = buildAllTools();
1181
- const gate = opts.yolo ? allowAllGate : controller.gate;
1182
- const context = loadProjectContext(process.cwd());
1183
- const system = TUI_SYSTEM + (context ? `
1184
-
1185
- Project context:
1186
- ${context}` : "");
1187
- return new Engine({ provider, tools, system, cwd: process.cwd(), gate });
1188
- }
1189
- };
1190
- if (!process.stdin.isTTY) {
1191
- process.stderr.write('axon: the interactive chat needs a terminal. For non-interactive use, run: axon -p "your prompt"\n');
1192
- process.exit(1);
1193
- }
1194
- render(/* @__PURE__ */ jsx6(Root, { deps }));
1195
- }
1196
-
1197
- // src/cli.ts
1198
- var READONLY_SYSTEM = `You are Axon, an agentic coding assistant. Use the read-only tools \u2014 read_file, list_dir, glob, grep \u2014 to inspect the project and answer precisely. When done, stop calling tools.`;
1199
- var YOLO_SYSTEM = `You are Axon, an agentic coding assistant. Use the provided 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). Prefer edit_file for surgical changes. When done, stop calling tools.`;
1200
- function redactKeys(cfg) {
1201
- const providers = cfg.providers;
1202
- if (!providers || typeof providers !== "object") return cfg;
1203
- const masked = {};
1204
- for (const [name, p] of Object.entries(providers)) {
1205
- masked[name] = p && typeof p === "object" && "apiKey" in p && p.apiKey ? { ...p, apiKey: "***redacted***" } : p;
1206
- }
1207
- return { ...cfg, providers: masked };
1208
- }
1209
- var program = new Command();
1210
- 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)");
1211
- 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) => {
1212
- if (action === "get") {
1213
- process.stdout.write(JSON.stringify(redactKeys(readConfigFile()), null, 2) + "\n");
1214
- return;
1215
- }
1216
- if (action === "set") {
1217
- if (!key || value === void 0) throw new Error("Usage: axon config set <key> <value>");
1218
- setConfigValue(key, value);
1219
- process.stdout.write(`set ${key} = ${value}
1220
- `);
1221
- return;
1222
- }
1223
- throw new Error(`Unknown config action "${action}". Use get or set.`);
1224
- });
1225
- program.action(() => {
1226
- const opts = program.opts();
1227
- main(opts).catch((err) => {
1228
- process.stderr.write(`\u{1F4A5} ${err instanceof Error ? err.message : String(err)}
1229
- `);
1230
- process.exit(1);
1231
- });
1232
- });
1233
- program.parse();
1234
- async function main(opts) {
1235
- if (!opts.print) {
1236
- runTui({ provider: opts.provider, model: opts.model, yolo: opts.yolo });
1237
- return;
1238
- }
1239
- const cfg = loadConfig();
1240
- if (opts.provider) cfg.provider = opts.provider;
1241
- if (opts.model) cfg.model = opts.model;
1242
- const provider = createProvider(cfg);
1243
- const tools = opts.yolo ? buildAllTools() : buildReadOnlyTools();
1244
- const gate = opts.yolo ? allowAllGate : denyGate;
1245
- const baseSystem = opts.yolo ? YOLO_SYSTEM : READONLY_SYSTEM;
1246
- const context = loadProjectContext(process.cwd());
1247
- const system = baseSystem + (context ? `
1248
-
1249
- Project context:
1250
- ${context}` : "");
1251
- const engine = new Engine({ provider, tools, system, cwd: process.cwd(), gate });
1252
- printRunner(engine, (s) => process.stdout.write(s));
1253
- process.stderr.write(`[axon: ${cfg.provider} / ${resolveModel(cfg)}${opts.yolo ? " / yolo" : ""}]
1254
- `);
1255
- await engine.submit(opts.print);
1256
- }