@wanghuimvp/axon 0.1.0 → 0.3.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 +32 -2
- package/dist/cli.js +393 -9
- package/package.json +8 -3
package/README.md
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
# Axon
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
A multi-provider agentic coding CLI. Axon runs a multi-step tool loop over your codebase — it reads, searches, edits, and runs commands to carry out a request — with an interactive chat or a one-shot non-interactive mode.
|
|
4
4
|
|
|
5
|
-
>
|
|
5
|
+
> `0.x` — interactive TUI (`axon`) and non-interactive (`axon -p`), multi-provider (Anthropic / OpenAI + OpenAI-compatible / Gemini), read + write/edit/shell tools.
|
|
6
6
|
|
|
7
7
|
## Install
|
|
8
8
|
|
|
@@ -10,6 +10,25 @@ An agentic coding CLI. Axon streams from Anthropic and runs a multi-step tool lo
|
|
|
10
10
|
npm install -g @wanghuimvp/axon
|
|
11
11
|
```
|
|
12
12
|
|
|
13
|
+
## Interactive mode
|
|
14
|
+
|
|
15
|
+
Run `axon` with no arguments to open the interactive chat:
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
axon
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
Type a request and press Enter. Axon streams its reasoning and tool calls. When it wants to write a file or run a command, you get an inline prompt:
|
|
22
|
+
|
|
23
|
+
```
|
|
24
|
+
🔒 write_file({"path":"NOTES.md", …})
|
|
25
|
+
[a] allow once / [A] always this session / [d] deny
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
Press `a` to allow once, `A` to allow that tool for the rest of the session, or `d` to deny (the model is told and adapts). `Ctrl+C` quits. Pass `--yolo` to skip all prompts.
|
|
29
|
+
|
|
30
|
+
For one-shot, non-interactive use, see [Usage](#usage) (`axon -p`).
|
|
31
|
+
|
|
13
32
|
## Providers
|
|
14
33
|
|
|
15
34
|
Axon speaks to three backends. The OpenAI-compatible provider also drives any
|
|
@@ -73,8 +92,19 @@ Axon will stream the model's reasoning, run read-only tools as needed (`read_fil
|
|
|
73
92
|
- `-p, --print <prompt>` — run one prompt non-interactively and stream the result.
|
|
74
93
|
- `--provider <name>` — select a provider (anthropic, openai, gemini) for this run.
|
|
75
94
|
- `--model <name>` — select a model for this run.
|
|
95
|
+
- `--yolo` — enable the `write_file`/`edit_file`/`shell` tools without prompting (non-interactive). Off by default: these tools are not even offered to the model, so it sticks to read-only inspection.
|
|
76
96
|
- `--version` — print the version.
|
|
77
97
|
|
|
98
|
+
### Changing files
|
|
99
|
+
|
|
100
|
+
Axon can modify your workspace with `write_file`, `edit_file`, and run commands with `shell`. In non-interactive (`-p`) mode these tools are **only available with `--yolo`** — without the flag they are not offered to the model at all (and a permission gate denies them as defense-in-depth).
|
|
101
|
+
|
|
102
|
+
```bash
|
|
103
|
+
axon --yolo -p "add a CHANGELOG.md with an initial entry, then run the tests"
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
Read/search tools (`read_file`, `list_dir`, `glob`, `grep`) always run without a prompt. `write_file` and `edit_file` are confined to the project root (path-escape and symlink-escape protected). `shell` is **not** sandboxed — under `--yolo` it runs arbitrary commands and can affect anything your shell can. Only pass `--yolo` to runs you trust.
|
|
107
|
+
|
|
78
108
|
## Configuration
|
|
79
109
|
|
|
80
110
|
Optional config file at `~/.axon/config.json`:
|
package/dist/cli.js
CHANGED
|
@@ -383,7 +383,8 @@ import { join as join3, resolve as resolve2 } from "node:path";
|
|
|
383
383
|
import fg from "fast-glob";
|
|
384
384
|
|
|
385
385
|
// src/tools/paths.ts
|
|
386
|
-
import { resolve, relative, isAbsolute, sep } from "node:path";
|
|
386
|
+
import { resolve, relative, isAbsolute, sep, dirname as dirname2 } from "node:path";
|
|
387
|
+
import { realpathSync, existsSync } from "node:fs";
|
|
387
388
|
function resolveInside(cwd, p) {
|
|
388
389
|
const root = resolve(cwd);
|
|
389
390
|
const full = isAbsolute(p) ? resolve(p) : resolve(root, p);
|
|
@@ -399,6 +400,20 @@ function assertSafeGlob(pattern) {
|
|
|
399
400
|
throw new Error(`glob pattern escapes project root: ${pattern}`);
|
|
400
401
|
}
|
|
401
402
|
}
|
|
403
|
+
function assertRealInside(cwd, full) {
|
|
404
|
+
const root = realpathSync(resolve(cwd));
|
|
405
|
+
let p = full;
|
|
406
|
+
while (!existsSync(p)) {
|
|
407
|
+
const parent = dirname2(p);
|
|
408
|
+
if (parent === p) throw new Error(`cannot resolve path: ${full}`);
|
|
409
|
+
p = parent;
|
|
410
|
+
}
|
|
411
|
+
const real = realpathSync(p);
|
|
412
|
+
const rel = relative(root, real);
|
|
413
|
+
if (rel === ".." || rel.startsWith(".." + sep) || isAbsolute(rel)) {
|
|
414
|
+
throw new Error(`path escapes project root via symlink: ${full}`);
|
|
415
|
+
}
|
|
416
|
+
}
|
|
402
417
|
|
|
403
418
|
// src/tools/fs.ts
|
|
404
419
|
function fail(err) {
|
|
@@ -496,11 +511,129 @@ var grepTool = {
|
|
|
496
511
|
}
|
|
497
512
|
};
|
|
498
513
|
|
|
514
|
+
// src/tools/edit.ts
|
|
515
|
+
import { readFile as readFile2, writeFile, mkdir } from "node:fs/promises";
|
|
516
|
+
import { dirname as dirname3 } from "node:path";
|
|
517
|
+
function fail2(err) {
|
|
518
|
+
return { ok: false, output: err instanceof Error ? err.message : String(err) };
|
|
519
|
+
}
|
|
520
|
+
var writeFileTool = {
|
|
521
|
+
name: "write_file",
|
|
522
|
+
dangerous: true,
|
|
523
|
+
schema: {
|
|
524
|
+
name: "write_file",
|
|
525
|
+
description: "Create or overwrite a file (relative to the project root). Creates parent directories as needed.",
|
|
526
|
+
parameters: {
|
|
527
|
+
type: "object",
|
|
528
|
+
properties: { path: { type: "string" }, content: { type: "string" } },
|
|
529
|
+
required: ["path", "content"]
|
|
530
|
+
}
|
|
531
|
+
},
|
|
532
|
+
async run(args, ctx) {
|
|
533
|
+
try {
|
|
534
|
+
const { path, content } = args;
|
|
535
|
+
const full = resolveInside(ctx.cwd, path);
|
|
536
|
+
assertRealInside(ctx.cwd, full);
|
|
537
|
+
await mkdir(dirname3(full), { recursive: true });
|
|
538
|
+
await writeFile(full, content, "utf8");
|
|
539
|
+
return { ok: true, output: `wrote ${Buffer.byteLength(content, "utf8")} bytes to ${path}` };
|
|
540
|
+
} catch (err) {
|
|
541
|
+
return fail2(err);
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
};
|
|
545
|
+
var editFileTool = {
|
|
546
|
+
name: "edit_file",
|
|
547
|
+
dangerous: true,
|
|
548
|
+
schema: {
|
|
549
|
+
name: "edit_file",
|
|
550
|
+
description: "Replace an exact, unique occurrence of old_string with new_string in a file.",
|
|
551
|
+
parameters: {
|
|
552
|
+
type: "object",
|
|
553
|
+
properties: { path: { type: "string" }, old_string: { type: "string" }, new_string: { type: "string" } },
|
|
554
|
+
required: ["path", "old_string", "new_string"]
|
|
555
|
+
}
|
|
556
|
+
},
|
|
557
|
+
async run(args, ctx) {
|
|
558
|
+
try {
|
|
559
|
+
const { path, old_string, new_string } = args;
|
|
560
|
+
const full = resolveInside(ctx.cwd, path);
|
|
561
|
+
assertRealInside(ctx.cwd, full);
|
|
562
|
+
if (old_string === "") return { ok: false, output: "old_string must not be empty" };
|
|
563
|
+
const text = await readFile2(full, "utf8");
|
|
564
|
+
const count = text.split(old_string).length - 1;
|
|
565
|
+
if (count === 0) return { ok: false, output: `old_string not found in ${path}` };
|
|
566
|
+
if (count > 1) return { ok: false, output: `old_string matches ${count} occurrences in ${path}; make it unique` };
|
|
567
|
+
const updated = text.split(old_string).join(new_string);
|
|
568
|
+
await writeFile(full, updated, "utf8");
|
|
569
|
+
return { ok: true, output: `edited ${path}` };
|
|
570
|
+
} catch (err) {
|
|
571
|
+
return fail2(err);
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
};
|
|
575
|
+
|
|
576
|
+
// src/tools/shell.ts
|
|
577
|
+
import { exec } from "node:child_process";
|
|
578
|
+
import { promisify } from "node:util";
|
|
579
|
+
var pexec = promisify(exec);
|
|
580
|
+
function combine(stdout, stderr) {
|
|
581
|
+
return [stdout, stderr].filter((s) => s.trim()).join("\n").trim();
|
|
582
|
+
}
|
|
583
|
+
var shellTool = {
|
|
584
|
+
name: "shell",
|
|
585
|
+
dangerous: true,
|
|
586
|
+
schema: {
|
|
587
|
+
name: "shell",
|
|
588
|
+
description: "Run a shell command in the project root. Returns combined stdout+stderr; ok is false on a nonzero exit or timeout.",
|
|
589
|
+
parameters: {
|
|
590
|
+
type: "object",
|
|
591
|
+
properties: { command: { type: "string" } },
|
|
592
|
+
required: ["command"]
|
|
593
|
+
}
|
|
594
|
+
},
|
|
595
|
+
async run(args, ctx) {
|
|
596
|
+
const { command } = args;
|
|
597
|
+
try {
|
|
598
|
+
const { stdout, stderr } = await pexec(command, {
|
|
599
|
+
cwd: ctx.cwd,
|
|
600
|
+
timeout: 12e4,
|
|
601
|
+
maxBuffer: 10 * 1024 * 1024,
|
|
602
|
+
windowsHide: true
|
|
603
|
+
});
|
|
604
|
+
return { ok: true, output: combine(stdout, stderr) || "(no output)" };
|
|
605
|
+
} catch (err) {
|
|
606
|
+
const e = err;
|
|
607
|
+
const body = combine(e.stdout ?? "", e.stderr ?? "");
|
|
608
|
+
const status = e.killed && e.code == null ? "timed out" : e.killed ? "killed by signal" : `exit ${e.code ?? "?"}`;
|
|
609
|
+
return { ok: false, output: `[${status}] ${body || e.message}`.trim() };
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
};
|
|
613
|
+
|
|
499
614
|
// src/tools/registry.ts
|
|
500
|
-
function
|
|
501
|
-
const tools = [readFileTool, listDirTool, globTool, grepTool];
|
|
615
|
+
function toMap(tools) {
|
|
502
616
|
return new Map(tools.map((t) => [t.name, t]));
|
|
503
617
|
}
|
|
618
|
+
function buildReadOnlyTools() {
|
|
619
|
+
return toMap([readFileTool, listDirTool, globTool, grepTool]);
|
|
620
|
+
}
|
|
621
|
+
function buildMutatingTools() {
|
|
622
|
+
return toMap([writeFileTool, editFileTool, shellTool]);
|
|
623
|
+
}
|
|
624
|
+
function buildAllTools() {
|
|
625
|
+
return toMap([...buildReadOnlyTools().values(), ...buildMutatingTools().values()]);
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
// src/permission/gate.ts
|
|
629
|
+
var denyGate = async (req) => ({
|
|
630
|
+
allow: false,
|
|
631
|
+
reason: `permission denied: "${req.name}" is a write/exec action and non-interactive mode blocks it. Re-run with --yolo to allow.`
|
|
632
|
+
});
|
|
633
|
+
var allowAllGate = async () => ({
|
|
634
|
+
allow: true,
|
|
635
|
+
reason: "allowed (--yolo)"
|
|
636
|
+
});
|
|
504
637
|
|
|
505
638
|
// src/core/conversation.ts
|
|
506
639
|
var Conversation = class {
|
|
@@ -579,6 +712,15 @@ var Engine = class {
|
|
|
579
712
|
this.convo.pushToolResult(call.id, `unknown tool: ${call.name}`);
|
|
580
713
|
continue;
|
|
581
714
|
}
|
|
715
|
+
if (tool.dangerous) {
|
|
716
|
+
const gate = this.deps.gate ?? denyGate;
|
|
717
|
+
const verdict = await gate({ id: call.id, name: call.name, args: call.args });
|
|
718
|
+
if (!verdict.allow) {
|
|
719
|
+
this.emit({ type: "tool_end", id: call.id, ok: false, output: verdict.reason });
|
|
720
|
+
this.convo.pushToolResult(call.id, verdict.reason);
|
|
721
|
+
continue;
|
|
722
|
+
}
|
|
723
|
+
}
|
|
582
724
|
let result;
|
|
583
725
|
try {
|
|
584
726
|
result = await tool.run(call.args, { cwd: this.deps.cwd });
|
|
@@ -631,10 +773,250 @@ function truncate(s, max = 500) {
|
|
|
631
773
|
return s.length > max ? `${s.slice(0, max)}\u2026` : s;
|
|
632
774
|
}
|
|
633
775
|
|
|
776
|
+
// src/ui/runTui.tsx
|
|
777
|
+
import { render } from "ink";
|
|
778
|
+
|
|
779
|
+
// src/ui/permissionController.ts
|
|
780
|
+
function createPermissionController() {
|
|
781
|
+
const sessionAllow = /* @__PURE__ */ new Set();
|
|
782
|
+
const subscribers = /* @__PURE__ */ new Set();
|
|
783
|
+
const queue = [];
|
|
784
|
+
let active = null;
|
|
785
|
+
let currentPending = null;
|
|
786
|
+
const notify = (p) => {
|
|
787
|
+
currentPending = p;
|
|
788
|
+
for (const fn of subscribers) fn(p);
|
|
789
|
+
};
|
|
790
|
+
const pump = () => {
|
|
791
|
+
if (active || queue.length === 0) return;
|
|
792
|
+
active = queue.shift();
|
|
793
|
+
notify({ req: active.req });
|
|
794
|
+
};
|
|
795
|
+
const gate = (req) => {
|
|
796
|
+
if (sessionAllow.has(req.name)) {
|
|
797
|
+
return Promise.resolve({ allow: true, reason: "allowed (remembered this session)" });
|
|
798
|
+
}
|
|
799
|
+
return new Promise((res) => {
|
|
800
|
+
queue.push({ req, settle: res });
|
|
801
|
+
pump();
|
|
802
|
+
});
|
|
803
|
+
};
|
|
804
|
+
const resolve3 = (decision) => {
|
|
805
|
+
if (!active) return;
|
|
806
|
+
const { req, settle } = active;
|
|
807
|
+
active = null;
|
|
808
|
+
if (decision === "always") sessionAllow.add(req.name);
|
|
809
|
+
settle({
|
|
810
|
+
allow: decision !== "deny",
|
|
811
|
+
reason: decision === "deny" ? `permission denied by user: ${req.name}` : `allowed (${decision})`
|
|
812
|
+
});
|
|
813
|
+
pump();
|
|
814
|
+
if (!active) notify(null);
|
|
815
|
+
};
|
|
816
|
+
const subscribe = (fn) => {
|
|
817
|
+
subscribers.add(fn);
|
|
818
|
+
return () => {
|
|
819
|
+
subscribers.delete(fn);
|
|
820
|
+
};
|
|
821
|
+
};
|
|
822
|
+
const getPending = () => currentPending;
|
|
823
|
+
return { gate, subscribe, getPending, resolve: resolve3 };
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
// src/ui/app.tsx
|
|
827
|
+
import { useEffect, useState, useSyncExternalStore, useCallback } from "react";
|
|
828
|
+
import { Box as Box4, Text as Text4, useInput as useInput2 } from "ink";
|
|
829
|
+
import TextInput from "ink-text-input";
|
|
830
|
+
|
|
831
|
+
// src/ui/components/MessageView.tsx
|
|
832
|
+
import { Box, Text } from "ink";
|
|
833
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
834
|
+
function toolIcon(status) {
|
|
835
|
+
if (status === "running") return "\u23F3";
|
|
836
|
+
return status === "ok" ? "\u2705" : "\u274C";
|
|
837
|
+
}
|
|
838
|
+
function truncate2(s, max = 300) {
|
|
839
|
+
return s.length > max ? `${s.slice(0, max)}\u2026` : s;
|
|
840
|
+
}
|
|
841
|
+
function MessageView({ items }) {
|
|
842
|
+
return /* @__PURE__ */ jsx(Box, { flexDirection: "column", children: items.map((it, i) => {
|
|
843
|
+
if (it.kind === "user") {
|
|
844
|
+
return /* @__PURE__ */ jsxs(Text, { children: [
|
|
845
|
+
/* @__PURE__ */ jsxs(Text, { color: "cyan", bold: true, children: [
|
|
846
|
+
"You",
|
|
847
|
+
" "
|
|
848
|
+
] }),
|
|
849
|
+
it.text
|
|
850
|
+
] }, i);
|
|
851
|
+
}
|
|
852
|
+
if (it.kind === "assistant") {
|
|
853
|
+
return /* @__PURE__ */ jsxs(Text, { children: [
|
|
854
|
+
/* @__PURE__ */ jsxs(Text, { color: "green", bold: true, children: [
|
|
855
|
+
"Axon",
|
|
856
|
+
" "
|
|
857
|
+
] }),
|
|
858
|
+
it.text
|
|
859
|
+
] }, i);
|
|
860
|
+
}
|
|
861
|
+
if (it.kind === "error") {
|
|
862
|
+
return /* @__PURE__ */ jsxs(Text, { color: "red", children: [
|
|
863
|
+
"\u{1F4A5} ",
|
|
864
|
+
it.text
|
|
865
|
+
] }, i);
|
|
866
|
+
}
|
|
867
|
+
if (it.kind === "tool") {
|
|
868
|
+
return /* @__PURE__ */ jsxs(Text, { color: "gray", children: [
|
|
869
|
+
toolIcon(it.status),
|
|
870
|
+
" ",
|
|
871
|
+
it.name,
|
|
872
|
+
"(",
|
|
873
|
+
truncate2(JSON.stringify(it.args ?? {}), 60),
|
|
874
|
+
")",
|
|
875
|
+
it.output ? ` \u2014 ${truncate2(it.output)}` : ""
|
|
876
|
+
] }, i);
|
|
877
|
+
}
|
|
878
|
+
return null;
|
|
879
|
+
}) });
|
|
880
|
+
}
|
|
881
|
+
|
|
882
|
+
// src/ui/components/PermissionPrompt.tsx
|
|
883
|
+
import { Box as Box2, Text as Text2, useInput } from "ink";
|
|
884
|
+
import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
|
|
885
|
+
function summarize(args) {
|
|
886
|
+
const s = JSON.stringify(args ?? {});
|
|
887
|
+
return s.length > 80 ? `${s.slice(0, 80)}\u2026` : s;
|
|
888
|
+
}
|
|
889
|
+
function decisionForKey(input) {
|
|
890
|
+
if (input === "a") return "once";
|
|
891
|
+
if (input === "A") return "always";
|
|
892
|
+
if (input === "d") return "deny";
|
|
893
|
+
return null;
|
|
894
|
+
}
|
|
895
|
+
function PermissionPrompt({ req, onDecide }) {
|
|
896
|
+
useInput((input) => {
|
|
897
|
+
const d = decisionForKey(input);
|
|
898
|
+
if (d) onDecide(d);
|
|
899
|
+
});
|
|
900
|
+
return /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", children: [
|
|
901
|
+
/* @__PURE__ */ jsxs2(Text2, { color: "yellow", children: [
|
|
902
|
+
"\u{1F512} ",
|
|
903
|
+
req.name,
|
|
904
|
+
"(",
|
|
905
|
+
summarize(req.args),
|
|
906
|
+
")"
|
|
907
|
+
] }),
|
|
908
|
+
/* @__PURE__ */ jsx2(Text2, { color: "yellow", children: "[a] allow once / [A] always this session / [d] deny" })
|
|
909
|
+
] });
|
|
910
|
+
}
|
|
911
|
+
|
|
912
|
+
// src/ui/components/StatusBar.tsx
|
|
913
|
+
import { Box as Box3, Text as Text3 } from "ink";
|
|
914
|
+
import { jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
|
|
915
|
+
function StatusBar({ provider, model, running, yolo }) {
|
|
916
|
+
return /* @__PURE__ */ jsx3(Box3, { children: /* @__PURE__ */ jsxs3(Text3, { color: "gray", children: [
|
|
917
|
+
"[axon: ",
|
|
918
|
+
provider,
|
|
919
|
+
"/",
|
|
920
|
+
model,
|
|
921
|
+
yolo ? " \xB7 yolo" : "",
|
|
922
|
+
" \xB7 ",
|
|
923
|
+
running ? "working\u2026" : "ready",
|
|
924
|
+
" \xB7 ^C quit]"
|
|
925
|
+
] }) });
|
|
926
|
+
}
|
|
927
|
+
|
|
928
|
+
// src/ui/app.tsx
|
|
929
|
+
import { jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
|
|
930
|
+
function reduceEvent(items, e) {
|
|
931
|
+
switch (e.type) {
|
|
932
|
+
case "text_delta": {
|
|
933
|
+
const last = items[items.length - 1];
|
|
934
|
+
if (last && last.kind === "assistant") {
|
|
935
|
+
return [...items.slice(0, -1), { kind: "assistant", text: last.text + e.text }];
|
|
936
|
+
}
|
|
937
|
+
return [...items, { kind: "assistant", text: e.text }];
|
|
938
|
+
}
|
|
939
|
+
case "tool_start":
|
|
940
|
+
return [...items, { kind: "tool", id: e.id, name: e.name, args: e.args, status: "running" }];
|
|
941
|
+
case "tool_end":
|
|
942
|
+
return items.map(
|
|
943
|
+
(it) => it.kind === "tool" && it.id === e.id ? { ...it, status: e.ok ? "ok" : "fail", output: e.output } : it
|
|
944
|
+
);
|
|
945
|
+
default:
|
|
946
|
+
return items;
|
|
947
|
+
}
|
|
948
|
+
}
|
|
949
|
+
function usePendingPermission(controller) {
|
|
950
|
+
const subscribe = useCallback((onChange) => controller.subscribe(() => onChange()), [controller]);
|
|
951
|
+
const getSnapshot = useCallback(() => controller.getPending(), [controller]);
|
|
952
|
+
const pending = useSyncExternalStore(subscribe, getSnapshot, getSnapshot);
|
|
953
|
+
return pending ? pending.req : null;
|
|
954
|
+
}
|
|
955
|
+
function App({ engine, controller, provider, model, yolo }) {
|
|
956
|
+
const [items, setItems] = useState([]);
|
|
957
|
+
const [input, setInput] = useState("");
|
|
958
|
+
const [running, setRunning] = useState(false);
|
|
959
|
+
const pending = usePendingPermission(controller);
|
|
960
|
+
useEffect(() => {
|
|
961
|
+
engine.on((e) => setItems((prev) => reduceEvent(prev, e)));
|
|
962
|
+
}, [engine]);
|
|
963
|
+
useInput2(
|
|
964
|
+
(_input, key) => {
|
|
965
|
+
if (key.escape && !running) setInput("");
|
|
966
|
+
},
|
|
967
|
+
{ isActive: !pending }
|
|
968
|
+
);
|
|
969
|
+
const handleSubmit = (text) => {
|
|
970
|
+
if (!text.trim() || running) return;
|
|
971
|
+
setItems((prev) => [...prev, { kind: "user", text }]);
|
|
972
|
+
setInput("");
|
|
973
|
+
setRunning(true);
|
|
974
|
+
engine.submit(text).catch((err) => {
|
|
975
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
976
|
+
setItems((prev) => [...prev, { kind: "error", text: msg }]);
|
|
977
|
+
}).finally(() => setRunning(false));
|
|
978
|
+
};
|
|
979
|
+
return /* @__PURE__ */ jsxs4(Box4, { flexDirection: "column", children: [
|
|
980
|
+
/* @__PURE__ */ jsx4(MessageView, { items }),
|
|
981
|
+
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: [
|
|
982
|
+
/* @__PURE__ */ jsx4(Text4, { color: "cyan", children: "\u203A " }),
|
|
983
|
+
/* @__PURE__ */ jsx4(TextInput, { value: input, onChange: setInput, onSubmit: handleSubmit })
|
|
984
|
+
] }),
|
|
985
|
+
/* @__PURE__ */ jsx4(StatusBar, { provider, model, running, yolo })
|
|
986
|
+
] });
|
|
987
|
+
}
|
|
988
|
+
|
|
989
|
+
// src/ui/runTui.tsx
|
|
990
|
+
import { jsx as jsx5 } from "react/jsx-runtime";
|
|
991
|
+
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.`;
|
|
992
|
+
function runTui(opts) {
|
|
993
|
+
const cfg = loadConfig();
|
|
994
|
+
if (opts.provider) cfg.provider = opts.provider;
|
|
995
|
+
if (opts.model) cfg.model = opts.model;
|
|
996
|
+
const provider = createProvider(cfg);
|
|
997
|
+
const tools = buildAllTools();
|
|
998
|
+
const controller = createPermissionController();
|
|
999
|
+
const gate = opts.yolo ? allowAllGate : controller.gate;
|
|
1000
|
+
const engine = new Engine({ provider, tools, system: TUI_SYSTEM, cwd: process.cwd(), gate });
|
|
1001
|
+
render(
|
|
1002
|
+
/* @__PURE__ */ jsx5(
|
|
1003
|
+
App,
|
|
1004
|
+
{
|
|
1005
|
+
engine,
|
|
1006
|
+
controller,
|
|
1007
|
+
provider: cfg.provider,
|
|
1008
|
+
model: resolveModel(cfg),
|
|
1009
|
+
yolo: Boolean(opts.yolo)
|
|
1010
|
+
}
|
|
1011
|
+
)
|
|
1012
|
+
);
|
|
1013
|
+
}
|
|
1014
|
+
|
|
634
1015
|
// src/cli.ts
|
|
635
|
-
var
|
|
1016
|
+
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.`;
|
|
1017
|
+
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.`;
|
|
636
1018
|
var program = new Command();
|
|
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");
|
|
1019
|
+
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)");
|
|
638
1020
|
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
1021
|
if (action === "get") {
|
|
640
1022
|
process.stdout.write(JSON.stringify(readConfigFile(), null, 2) + "\n");
|
|
@@ -660,17 +1042,19 @@ program.action(() => {
|
|
|
660
1042
|
program.parse();
|
|
661
1043
|
async function main(opts) {
|
|
662
1044
|
if (!opts.print) {
|
|
663
|
-
|
|
1045
|
+
runTui({ provider: opts.provider, model: opts.model, yolo: opts.yolo });
|
|
664
1046
|
return;
|
|
665
1047
|
}
|
|
666
1048
|
const cfg = loadConfig();
|
|
667
1049
|
if (opts.provider) cfg.provider = opts.provider;
|
|
668
1050
|
if (opts.model) cfg.model = opts.model;
|
|
669
1051
|
const provider = createProvider(cfg);
|
|
670
|
-
const tools = buildReadOnlyTools();
|
|
671
|
-
const
|
|
1052
|
+
const tools = opts.yolo ? buildAllTools() : buildReadOnlyTools();
|
|
1053
|
+
const gate = opts.yolo ? allowAllGate : denyGate;
|
|
1054
|
+
const system = opts.yolo ? YOLO_SYSTEM : READONLY_SYSTEM;
|
|
1055
|
+
const engine = new Engine({ provider, tools, system, cwd: process.cwd(), gate });
|
|
672
1056
|
printRunner(engine, (s) => process.stdout.write(s));
|
|
673
|
-
process.stderr.write(`[axon: ${cfg.provider} / ${resolveModel(cfg)}]
|
|
1057
|
+
process.stderr.write(`[axon: ${cfg.provider} / ${resolveModel(cfg)}${opts.yolo ? " / yolo" : ""}]
|
|
674
1058
|
`);
|
|
675
1059
|
await engine.submit(opts.print);
|
|
676
1060
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@wanghuimvp/axon",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
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
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -38,7 +38,7 @@
|
|
|
38
38
|
"scripts": {
|
|
39
39
|
"dev": "tsx src/cli.ts",
|
|
40
40
|
"test": "vitest run",
|
|
41
|
-
"build": "esbuild src/cli.ts --bundle --platform=node --format=esm --outfile=dist/cli.js --packages=external",
|
|
41
|
+
"build": "esbuild src/cli.ts --bundle --platform=node --format=esm --outfile=dist/cli.js --packages=external --jsx=automatic",
|
|
42
42
|
"prepublishOnly": "npm test && npm run build"
|
|
43
43
|
},
|
|
44
44
|
"dependencies": {
|
|
@@ -46,11 +46,16 @@
|
|
|
46
46
|
"@google/genai": "^1.52.0",
|
|
47
47
|
"commander": "^12.0.0",
|
|
48
48
|
"fast-glob": "^3.3.0",
|
|
49
|
-
"
|
|
49
|
+
"ink": "^5.2.1",
|
|
50
|
+
"ink-text-input": "^6.0.0",
|
|
51
|
+
"openai": "^4.104.0",
|
|
52
|
+
"react": "^18.3.1"
|
|
50
53
|
},
|
|
51
54
|
"devDependencies": {
|
|
52
55
|
"@types/node": "^20.0.0",
|
|
56
|
+
"@types/react": "^18.3.31",
|
|
53
57
|
"esbuild": "^0.23.0",
|
|
58
|
+
"ink-testing-library": "^4.0.0",
|
|
54
59
|
"tsx": "^4.0.0",
|
|
55
60
|
"typescript": "^5.5.0",
|
|
56
61
|
"vitest": "^2.0.0"
|