Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down
10 changes: 5 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@
<a href="https://www.npmjs.com/package/@axiomcode/code-graph"><img alt="npm" src="https://img.shields.io/npm/v/@axiomcode/code-graph?label=npm"></a>
<a href="https://github.com/AxiomCodeAI/axiomcodegraph/actions/workflows/nightly.yml"><img alt="Nightly (dev)" src="https://github.com/AxiomCodeAI/axiomcodegraph/actions/workflows/nightly.yml/badge.svg?branch=dev"></a>
<a href="LICENSE.md"><img alt="License: FSL-1.1-Apache-2.0" src="https://img.shields.io/badge/license-FSL--1.1--Apache--2.0-blue"></a>
<img alt="Node ≥ 22.5" src="https://img.shields.io/badge/node-%E2%89%A5%2022.5-brightgreen">
<img alt="Node ≥ 22.13" src="https://img.shields.io/badge/node-%E2%89%A5%2022.13-brightgreen">
</p>

<p align="center">
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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
Expand Down
8 changes: 8 additions & 0 deletions bin/axiomcode.js
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
2 changes: 1 addition & 1 deletion graph/bundle/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@ export async function main(argv = process.argv.slice(2)): Promise<void> {
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`);
}
Expand Down
2 changes: 1 addition & 1 deletion graph/bundle/node-sqlite.d.ts
Original file line number Diff line number Diff line change
@@ -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.
*/
Expand Down
7 changes: 4 additions & 3 deletions graph/bundle/write.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
2 changes: 1 addition & 1 deletion graph/test/csharp/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
2 changes: 1 addition & 1 deletion parser/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
"test": "vitest"
},
"engines": {
"node": ">=22.5.0"
"node": ">=22.13.0"
},
"dependencies": {
"entities": "6.0.1",
Expand Down
22 changes: 21 additions & 1 deletion plugins/axiomcode/mcp/find-bash.js
Original file line number Diff line number Diff line change
Expand Up @@ -33,12 +33,32 @@ 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.
// 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 null;
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 };
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();
Expand Down
14 changes: 14 additions & 0 deletions tests/mcp.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)')
Expand Down
Loading