@wanghuimvp/axon 0.2.0 → 0.4.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 +25 -2
- package/dist/cli.js +402 -4
- 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,29 @@ 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
|
+
On first run without an API key, `axon` opens a short setup screen — paste a key for the active provider and it is saved to `~/.axon/config.json` (with restricted file permissions) so later runs start straight in the chat. You can also set the provider's environment variable instead (e.g. `ANTHROPIC_API_KEY`), which always takes precedence.
|
|
22
|
+
|
|
23
|
+
Axon reads your project on startup — `package.json` and the first of `AGENTS.md` / `CLAUDE.md` / `README.md` — and gives the model that context.
|
|
24
|
+
|
|
25
|
+
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:
|
|
26
|
+
|
|
27
|
+
```
|
|
28
|
+
🔒 write_file({"path":"NOTES.md", …})
|
|
29
|
+
[a] allow once / [A] always this session / [d] deny
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
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.
|
|
33
|
+
|
|
34
|
+
For one-shot, non-interactive use, see [Usage](#usage) (`axon -p`).
|
|
35
|
+
|
|
13
36
|
## Providers
|
|
14
37
|
|
|
15
38
|
Axon speaks to three backends. The OpenAI-compatible provider also drives any
|
package/dist/cli.js
CHANGED
|
@@ -54,7 +54,7 @@ function loadConfig() {
|
|
|
54
54
|
}
|
|
55
55
|
|
|
56
56
|
// src/config/configFile.ts
|
|
57
|
-
import { readFileSync as readFileSync2, writeFileSync, mkdirSync } from "node:fs";
|
|
57
|
+
import { readFileSync as readFileSync2, writeFileSync, mkdirSync, chmodSync } from "node:fs";
|
|
58
58
|
import { homedir as homedir2 } from "node:os";
|
|
59
59
|
import { join as join2, dirname } from "node:path";
|
|
60
60
|
function configPath() {
|
|
@@ -86,6 +86,18 @@ function setConfigValue(key, value) {
|
|
|
86
86
|
mkdirSync(dirname(path), { recursive: true });
|
|
87
87
|
writeFileSync(path, JSON.stringify(cfg, null, 2) + "\n");
|
|
88
88
|
}
|
|
89
|
+
function setApiKey(provider, key) {
|
|
90
|
+
const cfg = readConfigFile();
|
|
91
|
+
const providers = cfg.providers ??= {};
|
|
92
|
+
(providers[provider] ??= {}).apiKey = key;
|
|
93
|
+
const path = configPath();
|
|
94
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
95
|
+
writeFileSync(path, JSON.stringify(cfg, null, 2) + "\n", { mode: 384 });
|
|
96
|
+
try {
|
|
97
|
+
chmodSync(path, 384);
|
|
98
|
+
} catch {
|
|
99
|
+
}
|
|
100
|
+
}
|
|
89
101
|
|
|
90
102
|
// src/providers/registry.ts
|
|
91
103
|
import Anthropic from "@anthropic-ai/sdk";
|
|
@@ -773,14 +785,395 @@ function truncate(s, max = 500) {
|
|
|
773
785
|
return s.length > max ? `${s.slice(0, max)}\u2026` : s;
|
|
774
786
|
}
|
|
775
787
|
|
|
788
|
+
// src/ui/runTui.tsx
|
|
789
|
+
import { useState as useState3, useRef } from "react";
|
|
790
|
+
import { Box as Box6, Text as Text6, render } from "ink";
|
|
791
|
+
|
|
792
|
+
// src/core/projectContext.ts
|
|
793
|
+
import { readFileSync as readFileSync3 } from "node:fs";
|
|
794
|
+
import { join as join4 } from "node:path";
|
|
795
|
+
function truncate2(s, max) {
|
|
796
|
+
return s.length > max ? `${s.slice(0, max)}\u2026` : s;
|
|
797
|
+
}
|
|
798
|
+
function loadProjectContext(cwd) {
|
|
799
|
+
const parts = [];
|
|
800
|
+
try {
|
|
801
|
+
const pkg = JSON.parse(readFileSync3(join4(cwd, "package.json"), "utf8"));
|
|
802
|
+
const bits = [];
|
|
803
|
+
if (pkg.name) bits.push(`name: ${pkg.name}`);
|
|
804
|
+
if (pkg.description) bits.push(`description: ${pkg.description}`);
|
|
805
|
+
if (pkg.scripts && typeof pkg.scripts === "object") bits.push(`scripts: ${Object.keys(pkg.scripts).join(", ")}`);
|
|
806
|
+
if (bits.length) parts.push(`package.json \u2014 ${bits.join("; ")}`);
|
|
807
|
+
} catch {
|
|
808
|
+
}
|
|
809
|
+
for (const name of ["AGENTS.md", "CLAUDE.md", "README.md"]) {
|
|
810
|
+
try {
|
|
811
|
+
const text = readFileSync3(join4(cwd, name), "utf8").trim();
|
|
812
|
+
if (text) {
|
|
813
|
+
parts.push(`${name}:
|
|
814
|
+
${truncate2(text, 2e3)}`);
|
|
815
|
+
break;
|
|
816
|
+
}
|
|
817
|
+
} catch {
|
|
818
|
+
}
|
|
819
|
+
}
|
|
820
|
+
return parts.join("\n\n");
|
|
821
|
+
}
|
|
822
|
+
|
|
823
|
+
// src/ui/permissionController.ts
|
|
824
|
+
function createPermissionController() {
|
|
825
|
+
const sessionAllow = /* @__PURE__ */ new Set();
|
|
826
|
+
const subscribers = /* @__PURE__ */ new Set();
|
|
827
|
+
const queue = [];
|
|
828
|
+
let active = null;
|
|
829
|
+
let currentPending = null;
|
|
830
|
+
const notify = (p) => {
|
|
831
|
+
currentPending = p;
|
|
832
|
+
for (const fn of subscribers) fn(p);
|
|
833
|
+
};
|
|
834
|
+
const pump = () => {
|
|
835
|
+
if (active || queue.length === 0) return;
|
|
836
|
+
active = queue.shift();
|
|
837
|
+
notify({ req: active.req });
|
|
838
|
+
};
|
|
839
|
+
const gate = (req) => {
|
|
840
|
+
if (sessionAllow.has(req.name)) {
|
|
841
|
+
return Promise.resolve({ allow: true, reason: "allowed (remembered this session)" });
|
|
842
|
+
}
|
|
843
|
+
return new Promise((res) => {
|
|
844
|
+
queue.push({ req, settle: res });
|
|
845
|
+
pump();
|
|
846
|
+
});
|
|
847
|
+
};
|
|
848
|
+
const resolve3 = (decision) => {
|
|
849
|
+
if (!active) return;
|
|
850
|
+
const { req, settle } = active;
|
|
851
|
+
active = null;
|
|
852
|
+
if (decision === "always") sessionAllow.add(req.name);
|
|
853
|
+
settle({
|
|
854
|
+
allow: decision !== "deny",
|
|
855
|
+
reason: decision === "deny" ? `permission denied by user: ${req.name}` : `allowed (${decision})`
|
|
856
|
+
});
|
|
857
|
+
pump();
|
|
858
|
+
if (!active) notify(null);
|
|
859
|
+
};
|
|
860
|
+
const subscribe = (fn) => {
|
|
861
|
+
subscribers.add(fn);
|
|
862
|
+
return () => {
|
|
863
|
+
subscribers.delete(fn);
|
|
864
|
+
};
|
|
865
|
+
};
|
|
866
|
+
const getPending = () => currentPending;
|
|
867
|
+
return { gate, subscribe, getPending, resolve: resolve3 };
|
|
868
|
+
}
|
|
869
|
+
|
|
870
|
+
// src/config/credentials.ts
|
|
871
|
+
function hasUsableKey(cfg) {
|
|
872
|
+
const k = cfg.providers[cfg.provider]?.apiKey;
|
|
873
|
+
return typeof k === "string" && k.trim().length > 0;
|
|
874
|
+
}
|
|
875
|
+
var INFO = {
|
|
876
|
+
anthropic: { envVar: "ANTHROPIC_API_KEY", url: "https://console.anthropic.com/settings/keys" },
|
|
877
|
+
openai: { envVar: "OPENAI_API_KEY", url: "https://platform.openai.com/api-keys" },
|
|
878
|
+
gemini: { envVar: "GEMINI_API_KEY", url: "https://aistudio.google.com/apikey" }
|
|
879
|
+
};
|
|
880
|
+
function keyProviderInfo(provider) {
|
|
881
|
+
return INFO[provider] ?? { envVar: `${provider.toUpperCase()}_API_KEY`, url: "" };
|
|
882
|
+
}
|
|
883
|
+
|
|
884
|
+
// src/ui/app.tsx
|
|
885
|
+
import { useEffect, useState, useSyncExternalStore, useCallback } from "react";
|
|
886
|
+
import { Box as Box4, Text as Text4, useInput as useInput2 } from "ink";
|
|
887
|
+
import TextInput from "ink-text-input";
|
|
888
|
+
|
|
889
|
+
// src/ui/components/MessageView.tsx
|
|
890
|
+
import { Box, Text } from "ink";
|
|
891
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
892
|
+
function toolIcon(status) {
|
|
893
|
+
if (status === "running") return "\u23F3";
|
|
894
|
+
return status === "ok" ? "\u2705" : "\u274C";
|
|
895
|
+
}
|
|
896
|
+
function truncate3(s, max = 300) {
|
|
897
|
+
return s.length > max ? `${s.slice(0, max)}\u2026` : s;
|
|
898
|
+
}
|
|
899
|
+
function MessageView({ items }) {
|
|
900
|
+
return /* @__PURE__ */ jsx(Box, { flexDirection: "column", children: items.map((it, i) => {
|
|
901
|
+
if (it.kind === "user") {
|
|
902
|
+
return /* @__PURE__ */ jsxs(Text, { children: [
|
|
903
|
+
/* @__PURE__ */ jsxs(Text, { color: "cyan", bold: true, children: [
|
|
904
|
+
"You",
|
|
905
|
+
" "
|
|
906
|
+
] }),
|
|
907
|
+
it.text
|
|
908
|
+
] }, i);
|
|
909
|
+
}
|
|
910
|
+
if (it.kind === "assistant") {
|
|
911
|
+
return /* @__PURE__ */ jsxs(Text, { children: [
|
|
912
|
+
/* @__PURE__ */ jsxs(Text, { color: "green", bold: true, children: [
|
|
913
|
+
"Axon",
|
|
914
|
+
" "
|
|
915
|
+
] }),
|
|
916
|
+
it.text
|
|
917
|
+
] }, i);
|
|
918
|
+
}
|
|
919
|
+
if (it.kind === "error") {
|
|
920
|
+
return /* @__PURE__ */ jsxs(Text, { color: "red", children: [
|
|
921
|
+
"\u{1F4A5} ",
|
|
922
|
+
it.text
|
|
923
|
+
] }, i);
|
|
924
|
+
}
|
|
925
|
+
if (it.kind === "tool") {
|
|
926
|
+
return /* @__PURE__ */ jsxs(Text, { color: "gray", children: [
|
|
927
|
+
toolIcon(it.status),
|
|
928
|
+
" ",
|
|
929
|
+
it.name,
|
|
930
|
+
"(",
|
|
931
|
+
truncate3(JSON.stringify(it.args ?? {}), 60),
|
|
932
|
+
")",
|
|
933
|
+
it.output ? ` \u2014 ${truncate3(it.output)}` : ""
|
|
934
|
+
] }, i);
|
|
935
|
+
}
|
|
936
|
+
return null;
|
|
937
|
+
}) });
|
|
938
|
+
}
|
|
939
|
+
|
|
940
|
+
// src/ui/components/PermissionPrompt.tsx
|
|
941
|
+
import { Box as Box2, Text as Text2, useInput } from "ink";
|
|
942
|
+
import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
|
|
943
|
+
function summarize(args) {
|
|
944
|
+
const s = JSON.stringify(args ?? {});
|
|
945
|
+
return s.length > 80 ? `${s.slice(0, 80)}\u2026` : s;
|
|
946
|
+
}
|
|
947
|
+
function decisionForKey(input) {
|
|
948
|
+
if (input === "a") return "once";
|
|
949
|
+
if (input === "A") return "always";
|
|
950
|
+
if (input === "d") return "deny";
|
|
951
|
+
return null;
|
|
952
|
+
}
|
|
953
|
+
function PermissionPrompt({ req, onDecide }) {
|
|
954
|
+
useInput((input) => {
|
|
955
|
+
const d = decisionForKey(input);
|
|
956
|
+
if (d) onDecide(d);
|
|
957
|
+
});
|
|
958
|
+
return /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", children: [
|
|
959
|
+
/* @__PURE__ */ jsxs2(Text2, { color: "yellow", children: [
|
|
960
|
+
"\u{1F512} ",
|
|
961
|
+
req.name,
|
|
962
|
+
"(",
|
|
963
|
+
summarize(req.args),
|
|
964
|
+
")"
|
|
965
|
+
] }),
|
|
966
|
+
/* @__PURE__ */ jsx2(Text2, { color: "yellow", children: "[a] allow once / [A] always this session / [d] deny" })
|
|
967
|
+
] });
|
|
968
|
+
}
|
|
969
|
+
|
|
970
|
+
// src/ui/components/StatusBar.tsx
|
|
971
|
+
import { Box as Box3, Text as Text3 } from "ink";
|
|
972
|
+
import { jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
|
|
973
|
+
function StatusBar({ provider, model, running, yolo }) {
|
|
974
|
+
return /* @__PURE__ */ jsx3(Box3, { children: /* @__PURE__ */ jsxs3(Text3, { color: "gray", children: [
|
|
975
|
+
"[axon: ",
|
|
976
|
+
provider,
|
|
977
|
+
"/",
|
|
978
|
+
model,
|
|
979
|
+
yolo ? " \xB7 yolo" : "",
|
|
980
|
+
" \xB7 ",
|
|
981
|
+
running ? "working\u2026" : "ready",
|
|
982
|
+
" \xB7 ^C quit]"
|
|
983
|
+
] }) });
|
|
984
|
+
}
|
|
985
|
+
|
|
986
|
+
// src/ui/app.tsx
|
|
987
|
+
import { jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
|
|
988
|
+
function reduceEvent(items, e) {
|
|
989
|
+
switch (e.type) {
|
|
990
|
+
case "text_delta": {
|
|
991
|
+
const last = items[items.length - 1];
|
|
992
|
+
if (last && last.kind === "assistant") {
|
|
993
|
+
return [...items.slice(0, -1), { kind: "assistant", text: last.text + e.text }];
|
|
994
|
+
}
|
|
995
|
+
return [...items, { kind: "assistant", text: e.text }];
|
|
996
|
+
}
|
|
997
|
+
case "tool_start":
|
|
998
|
+
return [...items, { kind: "tool", id: e.id, name: e.name, args: e.args, status: "running" }];
|
|
999
|
+
case "tool_end":
|
|
1000
|
+
return items.map(
|
|
1001
|
+
(it) => it.kind === "tool" && it.id === e.id ? { ...it, status: e.ok ? "ok" : "fail", output: e.output } : it
|
|
1002
|
+
);
|
|
1003
|
+
default:
|
|
1004
|
+
return items;
|
|
1005
|
+
}
|
|
1006
|
+
}
|
|
1007
|
+
function usePendingPermission(controller) {
|
|
1008
|
+
const subscribe = useCallback((onChange) => controller.subscribe(() => onChange()), [controller]);
|
|
1009
|
+
const getSnapshot = useCallback(() => controller.getPending(), [controller]);
|
|
1010
|
+
const pending = useSyncExternalStore(subscribe, getSnapshot, getSnapshot);
|
|
1011
|
+
return pending ? pending.req : null;
|
|
1012
|
+
}
|
|
1013
|
+
function App({ engine, controller, provider, model, yolo }) {
|
|
1014
|
+
const [items, setItems] = useState([]);
|
|
1015
|
+
const [input, setInput] = useState("");
|
|
1016
|
+
const [running, setRunning] = useState(false);
|
|
1017
|
+
const pending = usePendingPermission(controller);
|
|
1018
|
+
useEffect(() => {
|
|
1019
|
+
engine.on((e) => setItems((prev) => reduceEvent(prev, e)));
|
|
1020
|
+
}, [engine]);
|
|
1021
|
+
useInput2(
|
|
1022
|
+
(_input, key) => {
|
|
1023
|
+
if (key.escape && !running) setInput("");
|
|
1024
|
+
},
|
|
1025
|
+
{ isActive: !pending }
|
|
1026
|
+
);
|
|
1027
|
+
const handleSubmit = (text) => {
|
|
1028
|
+
if (!text.trim() || running) return;
|
|
1029
|
+
setItems((prev) => [...prev, { kind: "user", text }]);
|
|
1030
|
+
setInput("");
|
|
1031
|
+
setRunning(true);
|
|
1032
|
+
engine.submit(text).catch((err) => {
|
|
1033
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
1034
|
+
setItems((prev) => [...prev, { kind: "error", text: msg }]);
|
|
1035
|
+
}).finally(() => setRunning(false));
|
|
1036
|
+
};
|
|
1037
|
+
return /* @__PURE__ */ jsxs4(Box4, { flexDirection: "column", children: [
|
|
1038
|
+
/* @__PURE__ */ jsx4(MessageView, { items }),
|
|
1039
|
+
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: [
|
|
1040
|
+
/* @__PURE__ */ jsx4(Text4, { color: "cyan", children: "\u203A " }),
|
|
1041
|
+
/* @__PURE__ */ jsx4(TextInput, { value: input, onChange: setInput, onSubmit: handleSubmit })
|
|
1042
|
+
] }),
|
|
1043
|
+
/* @__PURE__ */ jsx4(StatusBar, { provider, model, running, yolo })
|
|
1044
|
+
] });
|
|
1045
|
+
}
|
|
1046
|
+
|
|
1047
|
+
// src/ui/Setup.tsx
|
|
1048
|
+
import { useState as useState2 } from "react";
|
|
1049
|
+
import { Box as Box5, Text as Text5 } from "ink";
|
|
1050
|
+
import TextInput2 from "ink-text-input";
|
|
1051
|
+
import { jsx as jsx5, jsxs as jsxs5 } from "react/jsx-runtime";
|
|
1052
|
+
function trimmedKey(v) {
|
|
1053
|
+
const t = v.trim();
|
|
1054
|
+
return t ? t : null;
|
|
1055
|
+
}
|
|
1056
|
+
function Setup({
|
|
1057
|
+
provider,
|
|
1058
|
+
info,
|
|
1059
|
+
onSubmit
|
|
1060
|
+
}) {
|
|
1061
|
+
const [value, setValue] = useState2("");
|
|
1062
|
+
const submit = (v) => {
|
|
1063
|
+
const k = trimmedKey(v);
|
|
1064
|
+
if (k) onSubmit(k);
|
|
1065
|
+
};
|
|
1066
|
+
return /* @__PURE__ */ jsxs5(Box5, { flexDirection: "column", children: [
|
|
1067
|
+
/* @__PURE__ */ jsxs5(Text5, { color: "yellow", children: [
|
|
1068
|
+
'No API key found for "',
|
|
1069
|
+
provider,
|
|
1070
|
+
'".'
|
|
1071
|
+
] }),
|
|
1072
|
+
/* @__PURE__ */ jsxs5(Text5, { children: [
|
|
1073
|
+
"Set ",
|
|
1074
|
+
/* @__PURE__ */ jsx5(Text5, { bold: true, children: info.envVar }),
|
|
1075
|
+
" in your environment, or paste a key below to save it to ~/.axon/config.json."
|
|
1076
|
+
] }),
|
|
1077
|
+
info.url ? /* @__PURE__ */ jsxs5(Text5, { color: "gray", children: [
|
|
1078
|
+
"Get a key at: ",
|
|
1079
|
+
info.url
|
|
1080
|
+
] }) : null,
|
|
1081
|
+
/* @__PURE__ */ jsxs5(Box5, { children: [
|
|
1082
|
+
/* @__PURE__ */ jsx5(Text5, { color: "cyan", children: "key \u203A " }),
|
|
1083
|
+
/* @__PURE__ */ jsx5(TextInput2, { value, onChange: setValue, onSubmit: submit, mask: "*" })
|
|
1084
|
+
] })
|
|
1085
|
+
] });
|
|
1086
|
+
}
|
|
1087
|
+
|
|
1088
|
+
// src/ui/runTui.tsx
|
|
1089
|
+
import { jsx as jsx6, jsxs as jsxs6 } from "react/jsx-runtime";
|
|
1090
|
+
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.`;
|
|
1091
|
+
function Root({ deps }) {
|
|
1092
|
+
const { cfg, controller, yolo, welcome, buildEngine, persistKey } = deps;
|
|
1093
|
+
const [ready, setReady] = useState3(hasUsableKey(cfg));
|
|
1094
|
+
const engineRef = useRef(null);
|
|
1095
|
+
if (ready && !engineRef.current) engineRef.current = buildEngine();
|
|
1096
|
+
if (!ready || !engineRef.current) {
|
|
1097
|
+
const info = keyProviderInfo(cfg.provider);
|
|
1098
|
+
return /* @__PURE__ */ jsx6(
|
|
1099
|
+
Setup,
|
|
1100
|
+
{
|
|
1101
|
+
provider: cfg.provider,
|
|
1102
|
+
info,
|
|
1103
|
+
onSubmit: (key) => {
|
|
1104
|
+
persistKey(cfg.provider, key);
|
|
1105
|
+
cfg.providers[cfg.provider] = { ...cfg.providers[cfg.provider] ?? {}, apiKey: key };
|
|
1106
|
+
setReady(true);
|
|
1107
|
+
}
|
|
1108
|
+
}
|
|
1109
|
+
);
|
|
1110
|
+
}
|
|
1111
|
+
return /* @__PURE__ */ jsxs6(Box6, { flexDirection: "column", children: [
|
|
1112
|
+
/* @__PURE__ */ jsx6(Text6, { color: "green", children: welcome }),
|
|
1113
|
+
/* @__PURE__ */ jsx6(
|
|
1114
|
+
App,
|
|
1115
|
+
{
|
|
1116
|
+
engine: engineRef.current,
|
|
1117
|
+
controller,
|
|
1118
|
+
provider: cfg.provider,
|
|
1119
|
+
model: resolveModel(cfg),
|
|
1120
|
+
yolo
|
|
1121
|
+
}
|
|
1122
|
+
)
|
|
1123
|
+
] });
|
|
1124
|
+
}
|
|
1125
|
+
function runTui(opts) {
|
|
1126
|
+
const cfg = loadConfig();
|
|
1127
|
+
if (opts.provider) cfg.provider = opts.provider;
|
|
1128
|
+
if (opts.model) cfg.model = opts.model;
|
|
1129
|
+
const controller = createPermissionController();
|
|
1130
|
+
const deps = {
|
|
1131
|
+
cfg,
|
|
1132
|
+
controller,
|
|
1133
|
+
yolo: Boolean(opts.yolo),
|
|
1134
|
+
welcome: `Axon \xB7 ${cfg.provider}/${resolveModel(cfg)} \xB7 type a request, Ctrl+C to quit`,
|
|
1135
|
+
persistKey: (provider, key) => {
|
|
1136
|
+
try {
|
|
1137
|
+
setApiKey(provider, key);
|
|
1138
|
+
} catch {
|
|
1139
|
+
}
|
|
1140
|
+
},
|
|
1141
|
+
buildEngine: () => {
|
|
1142
|
+
const provider = createProvider(cfg);
|
|
1143
|
+
const tools = buildAllTools();
|
|
1144
|
+
const gate = opts.yolo ? allowAllGate : controller.gate;
|
|
1145
|
+
const context = loadProjectContext(process.cwd());
|
|
1146
|
+
const system = TUI_SYSTEM + (context ? `
|
|
1147
|
+
|
|
1148
|
+
Project context:
|
|
1149
|
+
${context}` : "");
|
|
1150
|
+
return new Engine({ provider, tools, system, cwd: process.cwd(), gate });
|
|
1151
|
+
}
|
|
1152
|
+
};
|
|
1153
|
+
if (!process.stdin.isTTY) {
|
|
1154
|
+
process.stderr.write('axon: the interactive chat needs a terminal. For non-interactive use, run: axon -p "your prompt"\n');
|
|
1155
|
+
process.exit(1);
|
|
1156
|
+
}
|
|
1157
|
+
render(/* @__PURE__ */ jsx6(Root, { deps }));
|
|
1158
|
+
}
|
|
1159
|
+
|
|
776
1160
|
// src/cli.ts
|
|
777
1161
|
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
1162
|
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.`;
|
|
1163
|
+
function redactKeys(cfg) {
|
|
1164
|
+
const providers = cfg.providers;
|
|
1165
|
+
if (!providers || typeof providers !== "object") return cfg;
|
|
1166
|
+
const masked = {};
|
|
1167
|
+
for (const [name, p] of Object.entries(providers)) {
|
|
1168
|
+
masked[name] = p && typeof p === "object" && "apiKey" in p && p.apiKey ? { ...p, apiKey: "***redacted***" } : p;
|
|
1169
|
+
}
|
|
1170
|
+
return { ...cfg, providers: masked };
|
|
1171
|
+
}
|
|
779
1172
|
var program = new Command();
|
|
780
1173
|
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)");
|
|
781
1174
|
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) => {
|
|
782
1175
|
if (action === "get") {
|
|
783
|
-
process.stdout.write(JSON.stringify(readConfigFile(), null, 2) + "\n");
|
|
1176
|
+
process.stdout.write(JSON.stringify(redactKeys(readConfigFile()), null, 2) + "\n");
|
|
784
1177
|
return;
|
|
785
1178
|
}
|
|
786
1179
|
if (action === "set") {
|
|
@@ -803,7 +1196,7 @@ program.action(() => {
|
|
|
803
1196
|
program.parse();
|
|
804
1197
|
async function main(opts) {
|
|
805
1198
|
if (!opts.print) {
|
|
806
|
-
|
|
1199
|
+
runTui({ provider: opts.provider, model: opts.model, yolo: opts.yolo });
|
|
807
1200
|
return;
|
|
808
1201
|
}
|
|
809
1202
|
const cfg = loadConfig();
|
|
@@ -812,7 +1205,12 @@ async function main(opts) {
|
|
|
812
1205
|
const provider = createProvider(cfg);
|
|
813
1206
|
const tools = opts.yolo ? buildAllTools() : buildReadOnlyTools();
|
|
814
1207
|
const gate = opts.yolo ? allowAllGate : denyGate;
|
|
815
|
-
const
|
|
1208
|
+
const baseSystem = opts.yolo ? YOLO_SYSTEM : READONLY_SYSTEM;
|
|
1209
|
+
const context = loadProjectContext(process.cwd());
|
|
1210
|
+
const system = baseSystem + (context ? `
|
|
1211
|
+
|
|
1212
|
+
Project context:
|
|
1213
|
+
${context}` : "");
|
|
816
1214
|
const engine = new Engine({ provider, tools, system, cwd: process.cwd(), gate });
|
|
817
1215
|
printRunner(engine, (s) => process.stdout.write(s));
|
|
818
1216
|
process.stderr.write(`[axon: ${cfg.provider} / ${resolveModel(cfg)}${opts.yolo ? " / yolo" : ""}]
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@wanghuimvp/axon",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.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"
|