@wanghuimvp/axon 0.3.0 → 0.4.1
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 +4 -0
- package/dist/cli.js +219 -23
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -18,6 +18,10 @@ Run `axon` with no arguments to open the interactive chat:
|
|
|
18
18
|
axon
|
|
19
19
|
```
|
|
20
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
|
+
|
|
21
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:
|
|
22
26
|
|
|
23
27
|
```
|
package/dist/cli.js
CHANGED
|
@@ -23,6 +23,20 @@ var DEFAULTS = {
|
|
|
23
23
|
gemini: { apiKey: "env:GEMINI_API_KEY" }
|
|
24
24
|
}
|
|
25
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
|
+
}
|
|
26
40
|
function resolveModel(cfg) {
|
|
27
41
|
const model = cfg.model ?? cfg.providers[cfg.provider]?.model ?? DEFAULT_MODELS[cfg.provider];
|
|
28
42
|
if (!model) {
|
|
@@ -46,7 +60,7 @@ function loadConfig() {
|
|
|
46
60
|
} catch {
|
|
47
61
|
}
|
|
48
62
|
const merged = {
|
|
49
|
-
provider: fileCfg.provider ??
|
|
63
|
+
provider: fileCfg.provider ?? detectProvider(fileCfg),
|
|
50
64
|
model: fileCfg.model,
|
|
51
65
|
providers: { ...DEFAULTS.providers, ...fileCfg.providers ?? {} }
|
|
52
66
|
};
|
|
@@ -54,7 +68,7 @@ function loadConfig() {
|
|
|
54
68
|
}
|
|
55
69
|
|
|
56
70
|
// src/config/configFile.ts
|
|
57
|
-
import { readFileSync as readFileSync2, writeFileSync, mkdirSync } from "node:fs";
|
|
71
|
+
import { readFileSync as readFileSync2, writeFileSync, mkdirSync, chmodSync } from "node:fs";
|
|
58
72
|
import { homedir as homedir2 } from "node:os";
|
|
59
73
|
import { join as join2, dirname } from "node:path";
|
|
60
74
|
function configPath() {
|
|
@@ -86,6 +100,18 @@ function setConfigValue(key, value) {
|
|
|
86
100
|
mkdirSync(dirname(path), { recursive: true });
|
|
87
101
|
writeFileSync(path, JSON.stringify(cfg, null, 2) + "\n");
|
|
88
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
|
+
}
|
|
89
115
|
|
|
90
116
|
// src/providers/registry.ts
|
|
91
117
|
import Anthropic from "@anthropic-ai/sdk";
|
|
@@ -774,7 +800,39 @@ function truncate(s, max = 500) {
|
|
|
774
800
|
}
|
|
775
801
|
|
|
776
802
|
// src/ui/runTui.tsx
|
|
777
|
-
import {
|
|
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
|
+
}
|
|
778
836
|
|
|
779
837
|
// src/ui/permissionController.ts
|
|
780
838
|
function createPermissionController() {
|
|
@@ -823,6 +881,20 @@ function createPermissionController() {
|
|
|
823
881
|
return { gate, subscribe, getPending, resolve: resolve3 };
|
|
824
882
|
}
|
|
825
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
|
+
|
|
826
898
|
// src/ui/app.tsx
|
|
827
899
|
import { useEffect, useState, useSyncExternalStore, useCallback } from "react";
|
|
828
900
|
import { Box as Box4, Text as Text4, useInput as useInput2 } from "ink";
|
|
@@ -835,7 +907,7 @@ function toolIcon(status) {
|
|
|
835
907
|
if (status === "running") return "\u23F3";
|
|
836
908
|
return status === "ok" ? "\u2705" : "\u274C";
|
|
837
909
|
}
|
|
838
|
-
function
|
|
910
|
+
function truncate3(s, max = 300) {
|
|
839
911
|
return s.length > max ? `${s.slice(0, max)}\u2026` : s;
|
|
840
912
|
}
|
|
841
913
|
function MessageView({ items }) {
|
|
@@ -870,9 +942,9 @@ function MessageView({ items }) {
|
|
|
870
942
|
" ",
|
|
871
943
|
it.name,
|
|
872
944
|
"(",
|
|
873
|
-
|
|
945
|
+
truncate3(JSON.stringify(it.args ?? {}), 60),
|
|
874
946
|
")",
|
|
875
|
-
it.output ? ` \u2014 ${
|
|
947
|
+
it.output ? ` \u2014 ${truncate3(it.output)}` : ""
|
|
876
948
|
] }, i);
|
|
877
949
|
}
|
|
878
950
|
return null;
|
|
@@ -986,40 +1058,159 @@ function App({ engine, controller, provider, model, yolo }) {
|
|
|
986
1058
|
] });
|
|
987
1059
|
}
|
|
988
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
|
+
|
|
989
1120
|
// src/ui/runTui.tsx
|
|
990
|
-
import { jsx as
|
|
1121
|
+
import { jsx as jsx6, jsxs as jsxs6 } from "react/jsx-runtime";
|
|
991
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.`;
|
|
992
|
-
function
|
|
993
|
-
const cfg =
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
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(
|
|
1003
1151
|
App,
|
|
1004
1152
|
{
|
|
1005
|
-
engine,
|
|
1153
|
+
engine: engineRef.current,
|
|
1006
1154
|
controller,
|
|
1007
1155
|
provider: cfg.provider,
|
|
1008
1156
|
model: resolveModel(cfg),
|
|
1009
|
-
yolo
|
|
1157
|
+
yolo
|
|
1010
1158
|
}
|
|
1011
1159
|
)
|
|
1012
|
-
);
|
|
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 }));
|
|
1013
1195
|
}
|
|
1014
1196
|
|
|
1015
1197
|
// src/cli.ts
|
|
1016
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.`;
|
|
1017
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
|
+
}
|
|
1018
1209
|
var program = new Command();
|
|
1019
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)");
|
|
1020
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) => {
|
|
1021
1212
|
if (action === "get") {
|
|
1022
|
-
process.stdout.write(JSON.stringify(readConfigFile(), null, 2) + "\n");
|
|
1213
|
+
process.stdout.write(JSON.stringify(redactKeys(readConfigFile()), null, 2) + "\n");
|
|
1023
1214
|
return;
|
|
1024
1215
|
}
|
|
1025
1216
|
if (action === "set") {
|
|
@@ -1051,7 +1242,12 @@ async function main(opts) {
|
|
|
1051
1242
|
const provider = createProvider(cfg);
|
|
1052
1243
|
const tools = opts.yolo ? buildAllTools() : buildReadOnlyTools();
|
|
1053
1244
|
const gate = opts.yolo ? allowAllGate : denyGate;
|
|
1054
|
-
const
|
|
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}` : "");
|
|
1055
1251
|
const engine = new Engine({ provider, tools, system, cwd: process.cwd(), gate });
|
|
1056
1252
|
printRunner(engine, (s) => process.stdout.write(s));
|
|
1057
1253
|
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.1",
|
|
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": {
|