@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.
Files changed (3) hide show
  1. package/README.md +4 -0
  2. package/dist/cli.js +219 -23
  3. 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 ?? DEFAULTS.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 { render } from "ink";
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 truncate2(s, max = 300) {
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
- truncate2(JSON.stringify(it.args ?? {}), 60),
945
+ truncate3(JSON.stringify(it.args ?? {}), 60),
874
946
  ")",
875
- it.output ? ` \u2014 ${truncate2(it.output)}` : ""
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 jsx5 } from "react/jsx-runtime";
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 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(
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: Boolean(opts.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 system = opts.yolo ? YOLO_SYSTEM : READONLY_SYSTEM;
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.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": {