From 85e92fe6ebc75c55ddb9cde686a947dbccd53dfa Mon Sep 17 00:00:00 2001 From: swapnil <78632212+swapnilpaliwal-sd@users.noreply.github.com> Date: Thu, 8 Oct 2026 10:59:23 -0700 Subject: [PATCH 1/4] mcp: a bare AXIOMCODE_BASH is a program on PATH, not a missing file (#1357) On macOS and Linux `axiomcode mcp` always warned "AXIOMCODE_BASH is set to bash, which does not exist. The server starts, but every tool will say it cannot run the CLI", while every tool worked. The command exports the bash it chose, the bare name `bash` on POSIX, and the MCP launcher it starts checked that value as a file path. A bare name is now looked up on PATH (absolute directories only, as which.js does); a path is still checked as written, and either one missing is still an error, exit 127. Co-authored-by: axiomcode-bot[bot] <334110751+axiomcode-bot[bot]@users.noreply.github.com> --- plugins/axiomcode/mcp/find-bash.js | 19 ++++++++++++++++++- tests/mcp.py | 14 ++++++++++++++ 2 files changed, 32 insertions(+), 1 deletion(-) diff --git a/plugins/axiomcode/mcp/find-bash.js b/plugins/axiomcode/mcp/find-bash.js index 2d58c4bf..3bf2b0f8 100644 --- a/plugins/axiomcode/mcp/find-bash.js +++ b/plugins/axiomcode/mcp/find-bash.js @@ -33,12 +33,29 @@ function gitBash() { return candidates.find((p) => fs.existsSync(p)); } +// AXIOMCODE_BASH as a program would be started: a path is taken as written, a bare name is looked up on PATH. The +// command exports the bash it chose to everything it starts, and on POSIX that is the bare `bash`, so `axiomcode mcp` +// reached the MCP launcher with AXIOMCODE_BASH=bash; read as a path, it "did not exist", and the launcher warned that +// every tool would fail while every tool worked (#1357). The PATH search skips relative directories, as which.js does. +function onPath(name) { + if (path.isAbsolute(name) || /[\\/]/.test(name)) return fs.existsSync(name) ? name : null; + if (process.platform === 'win32') return which(name); + for (const d of (process.env.PATH || '').split(path.delimiter)) { + if (!d || !path.isAbsolute(d)) continue; + const p = path.join(d, name); + try { fs.accessSync(p, fs.constants.X_OK); if (fs.statSync(p).isFile()) return p; } catch { /* not here */ } + } + return null; +} + // { bash } or { error }: callers decide how to fail, because the MCP launcher must say it on stderr and // still leave the client a reason, while the command exits 127 as a shell would for a missing program. function findBash() { const override = process.env.AXIOMCODE_BASH; if (override) { - return fs.existsSync(override) ? { bash: override } : { error: `AXIOMCODE_BASH is set to ${override}, which does not exist.` }; + const found = onPath(override); + if (found) return { bash: found }; + return { error: `AXIOMCODE_BASH is set to ${override}, which ${path.isAbsolute(override) || /[\\/]/.test(override) ? 'does not exist' : 'is not on PATH'}.` }; } if (process.platform !== 'win32') return { bash: 'bash' }; const found = gitBash(); diff --git a/tests/mcp.py b/tests/mcp.py index 2870385e..b6e43791 100644 --- a/tests/mcp.py +++ b/tests/mcp.py @@ -377,6 +377,20 @@ def main(): env=dict(os.environ, AXIOMCODE_BASH=os.path.join(work, 'no-bash'))) if r.returncode != 127 or 'AXIOMCODE_BASH' not in r.stderr: bad.append(f"axiomcode with a missing AXIOMCODE_BASH: exit {r.returncode}, stderr {r.stderr.strip()[:200]!r}") + # A bare name is a program on PATH, not a file in the working directory: the command exports AXIOMCODE_BASH=bash + # on POSIX, and the MCP launcher it starts warned that every tool would fail while they all worked (#1357). + # Named but not on PATH is still the error above, said as such. + if os.name != 'nt': + find = os.path.join(ROOT, 'plugins', 'axiomcode', 'mcp', 'find-bash.js') + probe = f"const r = require({json.dumps(find)}).findBash(); process.stdout.write(JSON.stringify(r))" + for name, ok in (('bash', True), ('no-such-bash-1357', False)): + p = subprocess.run(['node', '-e', probe], capture_output=True, text=True, cwd=work, + env=dict(os.environ, AXIOMCODE_BASH=name)) + got = json.loads(p.stdout or '{}') + if ok and not (got.get('bash') and os.path.isabs(got['bash']) and not got.get('error')): + bad.append(f"AXIOMCODE_BASH={name}, a program on PATH: {got}") + if not ok and 'is not on PATH' not in (got.get('error') or ''): + bad.append(f"AXIOMCODE_BASH={name}, on no PATH: {got}") for b in bad: print('FAIL', b) print('ok' if not bad else f'{len(bad)} failure(s)') From 981fdf23feda246f0f955171ae10dbc65c16a447 Mon Sep 17 00:00:00 2001 From: swapnil <78632212+swapnilpaliwal-sd@users.noreply.github.com> Date: Thu, 8 Oct 2026 11:15:42 -0700 Subject: [PATCH 2/4] mcp: on Windows a bare AXIOMCODE_BASH stays an error, never WSL's bash The PATH lookup for a bare AXIOMCODE_BASH went through which.js on Windows too, where a bare `bash` resolves to WSL's (System32 or the WindowsApps alias), the bash this file exists to avoid. The command always exports a full path there, so a bare name can only be one a user set: it stays an error, now saying to give Git Bash's full path. Co-authored-by: axiomcode-bot[bot] <334110751+axiomcode-bot[bot]@users.noreply.github.com> --- plugins/axiomcode/mcp/find-bash.js | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/plugins/axiomcode/mcp/find-bash.js b/plugins/axiomcode/mcp/find-bash.js index 3bf2b0f8..3414048d 100644 --- a/plugins/axiomcode/mcp/find-bash.js +++ b/plugins/axiomcode/mcp/find-bash.js @@ -37,9 +37,10 @@ function gitBash() { // command exports the bash it chose to everything it starts, and on POSIX that is the bare `bash`, so `axiomcode mcp` // reached the MCP launcher with AXIOMCODE_BASH=bash; read as a path, it "did not exist", and the launcher warned that // every tool would fail while every tool worked (#1357). The PATH search skips relative directories, as which.js does. +// Not on Windows: the command always exports a full path there, and a bare `bash` on that PATH is WSL's (see above). function onPath(name) { if (path.isAbsolute(name) || /[\\/]/.test(name)) return fs.existsSync(name) ? name : null; - if (process.platform === 'win32') return which(name); + if (process.platform === 'win32') return null; for (const d of (process.env.PATH || '').split(path.delimiter)) { if (!d || !path.isAbsolute(d)) continue; const p = path.join(d, name); @@ -55,7 +56,9 @@ function findBash() { if (override) { const found = onPath(override); if (found) return { bash: found }; - return { error: `AXIOMCODE_BASH is set to ${override}, which ${path.isAbsolute(override) || /[\\/]/.test(override) ? 'does not exist' : 'is not on PATH'}.` }; + const named = !(path.isAbsolute(override) || /[\\/]/.test(override)); + return { error: `AXIOMCODE_BASH is set to ${override}, which ` + (!named ? 'does not exist.' + : process.platform === 'win32' ? 'is not a full path; on Windows set it to the full path of Git Bash\'s bash.exe.' : 'is not on PATH.') }; } if (process.platform !== 'win32') return { bash: 'bash' }; const found = gitBash(); From 2893531a33f161e2c2716301ec25a51f2f5593c3 Mon Sep 17 00:00:00 2001 From: swapnil <78632212+swapnilpaliwal-sd@users.noreply.github.com> Date: Thu, 8 Oct 2026 11:35:51 -0700 Subject: [PATCH 3/4] node: the floor is 22.13, said before anything runs (#1865) The graph is written with node:sqlite, which Node ships without a flag only from 22.13 (and 23.4). #1866 set engines to >=22.5, but on 22.5-22.12 the module needs --experimental-sqlite: on Node 22.11 `axiomcode index` got past the bundle's version check and failed with "No such built-in module: node:sqlite". - bin/axiomcode.js stops at once on an older Node: "axiomcode needs Node 22.13 or newer (it writes the graph with node:sqlite); this is Node X". Asked by version, since loading the module prints Node's experimental-feature warning on every command. - sqliteAvailable() asks the module, not the version. - engines.node >=22.13.0 in both package.json files; CI and the C# suite README say 22.13. Checked on Node 22.11 and 23.3 (refused, module absent) and 22.13 and 23.4 (run, module present). Co-authored-by: axiomcode-bot[bot] <334110751+axiomcode-bot[bot]@users.noreply.github.com> --- .github/workflows/ci.yml | 2 +- bin/axiomcode.js | 8 ++++++++ graph/bundle/cli.ts | 2 +- graph/bundle/node-sqlite.d.ts | 2 +- graph/bundle/write.ts | 7 ++++--- graph/test/csharp/README.md | 2 +- package.json | 2 +- parser/package.json | 2 +- 8 files changed, 18 insertions(+), 9 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index cb6710f5..dbb5196f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -52,7 +52,7 @@ permissions: contents: read env: - # bin/axiomcode and the parser need Node ≥ 22.5. + # bin/axiomcode and the parser need Node ≥ 22.13 (node:sqlite without a flag). NODE_VERSION: '22' SOUFFLE_VERSION: '2.5' SOUFFLE_SHA512: '6b86e554f6aa5abf8a8b55d8312ae37c0957c5bd6c9edeea89246db9406f645ec5e600b84fe6636b1c163da556f0da6c3d2dad46c1083413f2fcf4f95b9ac62c' diff --git a/bin/axiomcode.js b/bin/axiomcode.js index 72a1fc1a..3f54b1e5 100755 --- a/bin/axiomcode.js +++ b/bin/axiomcode.js @@ -37,6 +37,14 @@ if (args.length === 1 && args[0] === '--version') { process.exit(0); } +// THE NODE FLOOR, SAID BEFORE ANYTHING RUNS. The graph is written with node:sqlite, which Node ships without a flag from +// 22.13 (and 23.4); on 22.5-22.12 the index failed minutes in with "No such built-in module: node:sqlite". Asked by +// version, not by loading the module, which would print Node's experimental-feature warning on every command. +const [nodeMaj, nodeMin] = process.versions.node.split('.').map(Number); +if (!(nodeMaj > 23 || (nodeMaj === 23 && nodeMin >= 4) || (nodeMaj === 22 && nodeMin >= 13))) { + fail(`axiomcode needs Node 22.13 or newer (it writes the graph with node:sqlite); this is Node ${process.versions.node}.`); +} + const { bash, error } = findBash(); if (error) fail(error); const py = findPython(); diff --git a/graph/bundle/cli.ts b/graph/bundle/cli.ts index adb2061c..ad975fad 100644 --- a/graph/bundle/cli.ts +++ b/graph/bundle/cli.ts @@ -95,7 +95,7 @@ export async function main(argv = process.argv.slice(2)): Promise { log(`▶ wrote ${dbPath}${a.debug ? ' (+ csv/*.csv, --debug)' : ''}`); } else { fs.rmSync(dbPath, { force: true }); - console.error(` ! node ${process.versions.node} has no node:sqlite (needs ≥ 22.5) — graph.sqlite NOT written; csv/*.csv is complete`); + console.error(` ! node ${process.versions.node} has no node:sqlite (needs ≥ 22.13) — graph.sqlite NOT written; csv/*.csv is complete`); } log(`▶ bundle complete in ${((Date.now() - t0) / 1000).toFixed(1)}s`); } diff --git a/graph/bundle/node-sqlite.d.ts b/graph/bundle/node-sqlite.d.ts index a2d35d8c..618db642 100644 --- a/graph/bundle/node-sqlite.d.ts +++ b/graph/bundle/node-sqlite.d.ts @@ -1,5 +1,5 @@ /** - * The slice of `node:sqlite` this package uses. Node ships the module from 22.5; the + * The slice of `node:sqlite` this package uses. Node ships the module unflagged from 22.13; the * installed @types/node predates it, so the four members used here are declared locally * rather than pulling a newer type package for one import. */ diff --git a/graph/bundle/write.ts b/graph/bundle/write.ts index fcc4a347..d00899fe 100644 --- a/graph/bundle/write.ts +++ b/graph/bundle/write.ts @@ -20,10 +20,11 @@ export function writeCoreCsv(graphDir: string, core: CoreTables, log: (s: string } } -/** Node ≥ 22.5 ships `node:sqlite`. */ +/** Whether `node:sqlite` loads here. Asked of the module, not the version: Node 22.5–22.12 and 23.0–23.3 ship it only + * behind --experimental-sqlite, and a version test passed them, so the import that followed failed with "No such + * built-in module: node:sqlite" on Node 22.11. */ export function sqliteAvailable(): boolean { - const [maj, min] = process.versions.node.split('.').map(Number); - return maj! > 22 || (maj === 22 && min! >= 5); + try { require('node:sqlite'); return true; } catch { return false; } } function createSql(t: TableSpec, prefix = ''): string { diff --git a/graph/test/csharp/README.md b/graph/test/csharp/README.md index 56247142..1663ccb8 100644 --- a/graph/test/csharp/README.md +++ b/graph/test/csharp/README.md @@ -306,7 +306,7 @@ dotnet build -c Release graph/test/csharp/ground-truth/AxiomCsOracle dotnet build -c Release graph/test/csharp/runtime-oracle/AxiomCsInstrument ``` -`node` must be 22.5 or newer (what the repository itself requires), and the SAME version across a baseline and a fix run: a +`node` must be 22.13 or newer (what the repository itself requires), and the SAME version across a baseline and a fix run: a number measured on one version and compared against another is not a measurement. Every script exits 77 when a tool it needs is missing, so a machine without the toolchain skips rather than reporting a false failure. diff --git a/package.json b/package.json index 2a28f9fe..448fe575 100644 --- a/package.json +++ b/package.json @@ -19,7 +19,7 @@ "schema-doc": "tsx graph/bundle/cli.ts --print-schema > graph/bundle/SCHEMA.md" }, "engines": { - "node": ">=22.5.0" + "node": ">=22.13.0" }, "devDependencies": { "@types/node": "^20.10.0", diff --git a/parser/package.json b/parser/package.json index a8ac8e3b..46a15929 100644 --- a/parser/package.json +++ b/parser/package.json @@ -13,7 +13,7 @@ "test": "vitest" }, "engines": { - "node": ">=22.5.0" + "node": ">=22.13.0" }, "dependencies": { "entities": "6.0.1", From e87b491b13db237d32ff6b2450886e8f3dbbfbc1 Mon Sep 17 00:00:00 2001 From: swapnil <78632212+swapnilpaliwal-sd@users.noreply.github.com> Date: Thu, 8 Oct 2026 11:38:08 -0700 Subject: [PATCH 4/4] README: Node 22.13 is the floor, and a query answers mid-edit at once MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Node ≥ 22.13 in the badge and Requirements (#1865). The refresh paragraph still said a query waits up to AXIOMCODE_FRESH_WAIT (default 10) seconds; since #1864 it answers from the previous graph at once, with the rows in edited files marked, and --fresh waits. Committed with --no-verify (approved for this PR): the term check matches content already in README.md, none of it in these lines. Co-authored-by: axiomcode-bot[bot] <334110751+axiomcode-bot[bot]@users.noreply.github.com> --- README.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 2ff85549..c9153fa3 100644 --- a/README.md +++ b/README.md @@ -43,7 +43,7 @@ npm Nightly (dev) License: FSL-1.1-Apache-2.0 - Node ≥ 22.5 + Node ≥ 22.13

@@ -125,7 +125,7 @@ AxiomCode Graph is two parts. The **engine** (`@axiomcode/code-graph` on npm) pa graph; it also provides the `axiomcode` command and an MCP server. The **plugin** (`plugins/axiomcode/`) is the agent-facing frontend: a skill, four MCP tools, and hooks. Install the engine first. -Requirements: **Node ≥ 22.5** and **Python 3** (`python3`, or `python` / `py` on Windows). On Windows, also +Requirements: **Node ≥ 22.13** and **Python 3** (`python3`, or `python` / `py` on Windows). On Windows, also [Git for Windows](https://git-scm.com/download/win): the CLI runs under its bash. The engine ships as a prebuilt binary for macOS (Apple Silicon and Intel), Linux (x64 and arm64) and Windows x64, and `npm install` takes the one for your platform. No Soufflé and no compiler are needed, with one exception: @@ -334,9 +334,9 @@ command's usage. The graph stays current on its own. Every file the parser reads is recorded with its hash at build time; after an edit, a shell command, a finished turn, at session start, and before a query, anything that differs starts one background rebuild per repository, with the language, `--src` and `--library` of the graph it replaces. Every -command keeps reading the previous graph until the new one is indexed and swapped in. A query waits up to -`AXIOMCODE_FRESH_WAIT` seconds (default 10) for it, then answers from the previous graph with a `graph refresh:` line -naming the files it predates. The MCP server also checks every repository it has answered for once 15 minutes have +command keeps reading the previous graph until the new one is indexed and swapped in. A query answers from the +previous graph at once, with a `graph refresh:` line naming the files it predates and the rows in them marked; +`--fresh` waits for the rebuild instead, and `AXIOMCODE_FRESH_WAIT` (seconds, default 0) lets every query wait that long. The MCP server also checks every repository it has answered for once 15 minutes have passed since its last update (`AXIOMCODE_REFRESH_INTERVAL`, seconds; 0 turns it off), which catches edits made while a session sits idle. The graph records when and why it was built in `index_meta` (`refreshed_at`, `refresh_reason`). `AXIOMCODE_NO_REFRESH=1` turns the rebuilds off, not the check: an answer from a graph older than an edit still