@wanghuimvp/axon 0.3.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 +4 -0
- package/dist/cli.js +181 -22
- 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
|
@@ -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";
|
|
@@ -774,7 +786,39 @@ function truncate(s, max = 500) {
|
|
|
774
786
|
}
|
|
775
787
|
|
|
776
788
|
// src/ui/runTui.tsx
|
|
777
|
-
import {
|
|
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
|
+
}
|
|
778
822
|
|
|
779
823
|
// src/ui/permissionController.ts
|
|
780
824
|
function createPermissionController() {
|
|
@@ -823,6 +867,20 @@ function createPermissionController() {
|
|
|
823
867
|
return { gate, subscribe, getPending, resolve: resolve3 };
|
|
824
868
|
}
|
|
825
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
|
+
|
|
826
884
|
// src/ui/app.tsx
|
|
827
885
|
import { useEffect, useState, useSyncExternalStore, useCallback } from "react";
|
|
828
886
|
import { Box as Box4, Text as Text4, useInput as useInput2 } from "ink";
|
|
@@ -835,7 +893,7 @@ function toolIcon(status) {
|
|
|
835
893
|
if (status === "running") return "\u23F3";
|
|
836
894
|
return status === "ok" ? "\u2705" : "\u274C";
|
|
837
895
|
}
|
|
838
|
-
function
|
|
896
|
+
function truncate3(s, max = 300) {
|
|
839
897
|
return s.length > max ? `${s.slice(0, max)}\u2026` : s;
|
|
840
898
|
}
|
|
841
899
|
function MessageView({ items }) {
|
|
@@ -870,9 +928,9 @@ function MessageView({ items }) {
|
|
|
870
928
|
" ",
|
|
871
929
|
it.name,
|
|
872
930
|
"(",
|
|
873
|
-
|
|
931
|
+
truncate3(JSON.stringify(it.args ?? {}), 60),
|
|
874
932
|
")",
|
|
875
|
-
it.output ? ` \u2014 ${
|
|
933
|
+
it.output ? ` \u2014 ${truncate3(it.output)}` : ""
|
|
876
934
|
] }, i);
|
|
877
935
|
}
|
|
878
936
|
return null;
|
|
@@ -986,40 +1044,136 @@ function App({ engine, controller, provider, model, yolo }) {
|
|
|
986
1044
|
] });
|
|
987
1045
|
}
|
|
988
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
|
+
|
|
989
1088
|
// src/ui/runTui.tsx
|
|
990
|
-
import { jsx as
|
|
1089
|
+
import { jsx as jsx6, jsxs as jsxs6 } from "react/jsx-runtime";
|
|
991
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.`;
|
|
992
|
-
function
|
|
993
|
-
const cfg =
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
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(
|
|
1003
1114
|
App,
|
|
1004
1115
|
{
|
|
1005
|
-
engine,
|
|
1116
|
+
engine: engineRef.current,
|
|
1006
1117
|
controller,
|
|
1007
1118
|
provider: cfg.provider,
|
|
1008
1119
|
model: resolveModel(cfg),
|
|
1009
|
-
yolo
|
|
1120
|
+
yolo
|
|
1010
1121
|
}
|
|
1011
1122
|
)
|
|
1012
|
-
);
|
|
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 }));
|
|
1013
1158
|
}
|
|
1014
1159
|
|
|
1015
1160
|
// src/cli.ts
|
|
1016
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.`;
|
|
1017
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
|
+
}
|
|
1018
1172
|
var program = new Command();
|
|
1019
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)");
|
|
1020
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) => {
|
|
1021
1175
|
if (action === "get") {
|
|
1022
|
-
process.stdout.write(JSON.stringify(readConfigFile(), null, 2) + "\n");
|
|
1176
|
+
process.stdout.write(JSON.stringify(redactKeys(readConfigFile()), null, 2) + "\n");
|
|
1023
1177
|
return;
|
|
1024
1178
|
}
|
|
1025
1179
|
if (action === "set") {
|
|
@@ -1051,7 +1205,12 @@ async function main(opts) {
|
|
|
1051
1205
|
const provider = createProvider(cfg);
|
|
1052
1206
|
const tools = opts.yolo ? buildAllTools() : buildReadOnlyTools();
|
|
1053
1207
|
const gate = opts.yolo ? allowAllGate : denyGate;
|
|
1054
|
-
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}` : "");
|
|
1055
1214
|
const engine = new Engine({ provider, tools, system, cwd: process.cwd(), gate });
|
|
1056
1215
|
printRunner(engine, (s) => process.stdout.write(s));
|
|
1057
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": {
|