@wanghuimvp/axon 0.2.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 +21 -2
- package/dist/cli.js +240 -1
- 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
|
package/dist/cli.js
CHANGED
|
@@ -773,6 +773,245 @@ function truncate(s, max = 500) {
|
|
|
773
773
|
return s.length > max ? `${s.slice(0, max)}\u2026` : s;
|
|
774
774
|
}
|
|
775
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
|
+
|
|
776
1015
|
// src/cli.ts
|
|
777
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.`;
|
|
778
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.`;
|
|
@@ -803,7 +1042,7 @@ program.action(() => {
|
|
|
803
1042
|
program.parse();
|
|
804
1043
|
async function main(opts) {
|
|
805
1044
|
if (!opts.print) {
|
|
806
|
-
|
|
1045
|
+
runTui({ provider: opts.provider, model: opts.model, yolo: opts.yolo });
|
|
807
1046
|
return;
|
|
808
1047
|
}
|
|
809
1048
|
const cfg = loadConfig();
|
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"
|