@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.
Files changed (3) hide show
  1. package/README.md +21 -2
  2. package/dist/cli.js +240 -1
  3. package/package.json +8 -3
package/README.md CHANGED
@@ -1,8 +1,8 @@
1
1
  # Axon
2
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.
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
- > Foundation release (`0.0.x`). Non-interactive `axon -p` mode with read-only tools (always available) and write/edit/shell tools (enabled with `--yolo`). Interactive TUI is on the roadmap.
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
- process.stdout.write('Interactive TUI not built yet \u2014 use: axon -p "your prompt"\n');
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.2.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
- "openai": "^4.104.0"
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"