@wanghuimvp/axon 0.1.0 → 0.2.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 +12 -1
  2. package/dist/cli.js +153 -8
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  An agentic coding CLI. Axon streams from Anthropic and runs a multi-step tool loop over your codebase — it reads, searches, and reasons across your files to answer a prompt.
4
4
 
5
- > Foundation release (`0.0.x`). Non-interactive `axon -p` mode with read-only tools. Interactive TUI and write/edit/shell tools are on the roadmap.
5
+ > Foundation release (`0.0.x`). Non-interactive `axon -p` mode with read-only tools (always available) and write/edit/shell tools (enabled with `--yolo`). Interactive TUI is on the roadmap.
6
6
 
7
7
  ## Install
8
8
 
@@ -73,8 +73,19 @@ Axon will stream the model's reasoning, run read-only tools as needed (`read_fil
73
73
  - `-p, --print <prompt>` — run one prompt non-interactively and stream the result.
74
74
  - `--provider <name>` — select a provider (anthropic, openai, gemini) for this run.
75
75
  - `--model <name>` — select a model for this run.
76
+ - `--yolo` — enable the `write_file`/`edit_file`/`shell` tools without prompting (non-interactive). Off by default: these tools are not even offered to the model, so it sticks to read-only inspection.
76
77
  - `--version` — print the version.
77
78
 
79
+ ### Changing files
80
+
81
+ Axon can modify your workspace with `write_file`, `edit_file`, and run commands with `shell`. In non-interactive (`-p`) mode these tools are **only available with `--yolo`** — without the flag they are not offered to the model at all (and a permission gate denies them as defense-in-depth).
82
+
83
+ ```bash
84
+ axon --yolo -p "add a CHANGELOG.md with an initial entry, then run the tests"
85
+ ```
86
+
87
+ Read/search tools (`read_file`, `list_dir`, `glob`, `grep`) always run without a prompt. `write_file` and `edit_file` are confined to the project root (path-escape and symlink-escape protected). `shell` is **not** sandboxed — under `--yolo` it runs arbitrary commands and can affect anything your shell can. Only pass `--yolo` to runs you trust.
88
+
78
89
  ## Configuration
79
90
 
80
91
  Optional config file at `~/.axon/config.json`:
package/dist/cli.js CHANGED
@@ -383,7 +383,8 @@ import { join as join3, resolve as resolve2 } from "node:path";
383
383
  import fg from "fast-glob";
384
384
 
385
385
  // src/tools/paths.ts
386
- import { resolve, relative, isAbsolute, sep } from "node:path";
386
+ import { resolve, relative, isAbsolute, sep, dirname as dirname2 } from "node:path";
387
+ import { realpathSync, existsSync } from "node:fs";
387
388
  function resolveInside(cwd, p) {
388
389
  const root = resolve(cwd);
389
390
  const full = isAbsolute(p) ? resolve(p) : resolve(root, p);
@@ -399,6 +400,20 @@ function assertSafeGlob(pattern) {
399
400
  throw new Error(`glob pattern escapes project root: ${pattern}`);
400
401
  }
401
402
  }
403
+ function assertRealInside(cwd, full) {
404
+ const root = realpathSync(resolve(cwd));
405
+ let p = full;
406
+ while (!existsSync(p)) {
407
+ const parent = dirname2(p);
408
+ if (parent === p) throw new Error(`cannot resolve path: ${full}`);
409
+ p = parent;
410
+ }
411
+ const real = realpathSync(p);
412
+ const rel = relative(root, real);
413
+ if (rel === ".." || rel.startsWith(".." + sep) || isAbsolute(rel)) {
414
+ throw new Error(`path escapes project root via symlink: ${full}`);
415
+ }
416
+ }
402
417
 
403
418
  // src/tools/fs.ts
404
419
  function fail(err) {
@@ -496,11 +511,129 @@ var grepTool = {
496
511
  }
497
512
  };
498
513
 
514
+ // src/tools/edit.ts
515
+ import { readFile as readFile2, writeFile, mkdir } from "node:fs/promises";
516
+ import { dirname as dirname3 } from "node:path";
517
+ function fail2(err) {
518
+ return { ok: false, output: err instanceof Error ? err.message : String(err) };
519
+ }
520
+ var writeFileTool = {
521
+ name: "write_file",
522
+ dangerous: true,
523
+ schema: {
524
+ name: "write_file",
525
+ description: "Create or overwrite a file (relative to the project root). Creates parent directories as needed.",
526
+ parameters: {
527
+ type: "object",
528
+ properties: { path: { type: "string" }, content: { type: "string" } },
529
+ required: ["path", "content"]
530
+ }
531
+ },
532
+ async run(args, ctx) {
533
+ try {
534
+ const { path, content } = args;
535
+ const full = resolveInside(ctx.cwd, path);
536
+ assertRealInside(ctx.cwd, full);
537
+ await mkdir(dirname3(full), { recursive: true });
538
+ await writeFile(full, content, "utf8");
539
+ return { ok: true, output: `wrote ${Buffer.byteLength(content, "utf8")} bytes to ${path}` };
540
+ } catch (err) {
541
+ return fail2(err);
542
+ }
543
+ }
544
+ };
545
+ var editFileTool = {
546
+ name: "edit_file",
547
+ dangerous: true,
548
+ schema: {
549
+ name: "edit_file",
550
+ description: "Replace an exact, unique occurrence of old_string with new_string in a file.",
551
+ parameters: {
552
+ type: "object",
553
+ properties: { path: { type: "string" }, old_string: { type: "string" }, new_string: { type: "string" } },
554
+ required: ["path", "old_string", "new_string"]
555
+ }
556
+ },
557
+ async run(args, ctx) {
558
+ try {
559
+ const { path, old_string, new_string } = args;
560
+ const full = resolveInside(ctx.cwd, path);
561
+ assertRealInside(ctx.cwd, full);
562
+ if (old_string === "") return { ok: false, output: "old_string must not be empty" };
563
+ const text = await readFile2(full, "utf8");
564
+ const count = text.split(old_string).length - 1;
565
+ if (count === 0) return { ok: false, output: `old_string not found in ${path}` };
566
+ if (count > 1) return { ok: false, output: `old_string matches ${count} occurrences in ${path}; make it unique` };
567
+ const updated = text.split(old_string).join(new_string);
568
+ await writeFile(full, updated, "utf8");
569
+ return { ok: true, output: `edited ${path}` };
570
+ } catch (err) {
571
+ return fail2(err);
572
+ }
573
+ }
574
+ };
575
+
576
+ // src/tools/shell.ts
577
+ import { exec } from "node:child_process";
578
+ import { promisify } from "node:util";
579
+ var pexec = promisify(exec);
580
+ function combine(stdout, stderr) {
581
+ return [stdout, stderr].filter((s) => s.trim()).join("\n").trim();
582
+ }
583
+ var shellTool = {
584
+ name: "shell",
585
+ dangerous: true,
586
+ schema: {
587
+ name: "shell",
588
+ description: "Run a shell command in the project root. Returns combined stdout+stderr; ok is false on a nonzero exit or timeout.",
589
+ parameters: {
590
+ type: "object",
591
+ properties: { command: { type: "string" } },
592
+ required: ["command"]
593
+ }
594
+ },
595
+ async run(args, ctx) {
596
+ const { command } = args;
597
+ try {
598
+ const { stdout, stderr } = await pexec(command, {
599
+ cwd: ctx.cwd,
600
+ timeout: 12e4,
601
+ maxBuffer: 10 * 1024 * 1024,
602
+ windowsHide: true
603
+ });
604
+ return { ok: true, output: combine(stdout, stderr) || "(no output)" };
605
+ } catch (err) {
606
+ const e = err;
607
+ const body = combine(e.stdout ?? "", e.stderr ?? "");
608
+ const status = e.killed && e.code == null ? "timed out" : e.killed ? "killed by signal" : `exit ${e.code ?? "?"}`;
609
+ return { ok: false, output: `[${status}] ${body || e.message}`.trim() };
610
+ }
611
+ }
612
+ };
613
+
499
614
  // src/tools/registry.ts
500
- function buildReadOnlyTools() {
501
- const tools = [readFileTool, listDirTool, globTool, grepTool];
615
+ function toMap(tools) {
502
616
  return new Map(tools.map((t) => [t.name, t]));
503
617
  }
618
+ function buildReadOnlyTools() {
619
+ return toMap([readFileTool, listDirTool, globTool, grepTool]);
620
+ }
621
+ function buildMutatingTools() {
622
+ return toMap([writeFileTool, editFileTool, shellTool]);
623
+ }
624
+ function buildAllTools() {
625
+ return toMap([...buildReadOnlyTools().values(), ...buildMutatingTools().values()]);
626
+ }
627
+
628
+ // src/permission/gate.ts
629
+ var denyGate = async (req) => ({
630
+ allow: false,
631
+ reason: `permission denied: "${req.name}" is a write/exec action and non-interactive mode blocks it. Re-run with --yolo to allow.`
632
+ });
633
+ var allowAllGate = async () => ({
634
+ allow: true,
635
+ reason: "allowed (--yolo)"
636
+ });
504
637
 
505
638
  // src/core/conversation.ts
506
639
  var Conversation = class {
@@ -579,6 +712,15 @@ var Engine = class {
579
712
  this.convo.pushToolResult(call.id, `unknown tool: ${call.name}`);
580
713
  continue;
581
714
  }
715
+ if (tool.dangerous) {
716
+ const gate = this.deps.gate ?? denyGate;
717
+ const verdict = await gate({ id: call.id, name: call.name, args: call.args });
718
+ if (!verdict.allow) {
719
+ this.emit({ type: "tool_end", id: call.id, ok: false, output: verdict.reason });
720
+ this.convo.pushToolResult(call.id, verdict.reason);
721
+ continue;
722
+ }
723
+ }
582
724
  let result;
583
725
  try {
584
726
  result = await tool.run(call.args, { cwd: this.deps.cwd });
@@ -632,9 +774,10 @@ function truncate(s, max = 500) {
632
774
  }
633
775
 
634
776
  // src/cli.ts
635
- var SYSTEM = `You are Axon, an agentic coding assistant. Use the provided tools to inspect the project and answer precisely. When done, stop calling tools.`;
777
+ 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
+ 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.`;
636
779
  var program = new Command();
637
- 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");
780
+ 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)");
638
781
  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) => {
639
782
  if (action === "get") {
640
783
  process.stdout.write(JSON.stringify(readConfigFile(), null, 2) + "\n");
@@ -667,10 +810,12 @@ async function main(opts) {
667
810
  if (opts.provider) cfg.provider = opts.provider;
668
811
  if (opts.model) cfg.model = opts.model;
669
812
  const provider = createProvider(cfg);
670
- const tools = buildReadOnlyTools();
671
- const engine = new Engine({ provider, tools, system: SYSTEM, cwd: process.cwd() });
813
+ const tools = opts.yolo ? buildAllTools() : buildReadOnlyTools();
814
+ const gate = opts.yolo ? allowAllGate : denyGate;
815
+ const system = opts.yolo ? YOLO_SYSTEM : READONLY_SYSTEM;
816
+ const engine = new Engine({ provider, tools, system, cwd: process.cwd(), gate });
672
817
  printRunner(engine, (s) => process.stdout.write(s));
673
- process.stderr.write(`[axon: ${cfg.provider} / ${resolveModel(cfg)}]
818
+ process.stderr.write(`[axon: ${cfg.provider} / ${resolveModel(cfg)}${opts.yolo ? " / yolo" : ""}]
674
819
  `);
675
820
  await engine.submit(opts.print);
676
821
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wanghuimvp/axon",
3
- "version": "0.1.0",
3
+ "version": "0.2.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": {