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