From c50606901ff4fda6db3b01b1846369c71a121d54 Mon Sep 17 00:00:00 2001 From: Kevin Thomas Date: Fri, 26 Jun 2026 23:49:07 +0000 Subject: [PATCH 1/3] guide users on unsupported node instead of crashing on launch --- package-lock.json | 4 +- package.json | 2 +- packages/coding-agent/package.json | 2 +- packages/coding-agent/src/cli-main.ts | 29 +++++++++++ packages/coding-agent/src/cli.ts | 50 +++++-------------- .../src/cli/node-version-check.ts | 35 +++++++++++++ .../test/node-version-check.test.ts | 46 +++++++++++++++++ 7 files changed, 127 insertions(+), 41 deletions(-) create mode 100644 packages/coding-agent/src/cli-main.ts create mode 100644 packages/coding-agent/src/cli/node-version-check.ts create mode 100644 packages/coding-agent/test/node-version-check.test.ts diff --git a/package-lock.json b/package-lock.json index 896bbfd529..6a3c62d4a4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -31,7 +31,7 @@ "typescript": "^5.9.2" }, "engines": { - "node": ">=20.0.0" + "node": ">=22.0.0" } }, "node_modules/@anthropic-ai/sandbox-runtime": { @@ -6039,7 +6039,7 @@ "vitest": "^4.1.8" }, "engines": { - "node": ">=20.6.0" + "node": ">=22.0.0" }, "optionalDependencies": { "@mariozechner/clipboard": "^0.3.5" diff --git a/package.json b/package.json index baed2c604d..16fce9c7b2 100644 --- a/package.json +++ b/package.json @@ -46,7 +46,7 @@ "typescript": "^5.9.2" }, "engines": { - "node": ">=20.0.0" + "node": ">=22.0.0" }, "version": "0.2.2", "dependencies": { diff --git a/packages/coding-agent/package.json b/packages/coding-agent/package.json index e26ef60196..c9f1d94207 100644 --- a/packages/coding-agent/package.json +++ b/packages/coding-agent/package.json @@ -102,6 +102,6 @@ "directory": "packages/coding-agent" }, "engines": { - "node": ">=20.6.0" + "node": ">=22.0.0" } } diff --git a/packages/coding-agent/src/cli-main.ts b/packages/coding-agent/src/cli-main.ts new file mode 100644 index 0000000000..8e23a3ec64 --- /dev/null +++ b/packages/coding-agent/src/cli-main.ts @@ -0,0 +1,29 @@ +import { enableCompileCache } from "node:module"; +import { maybeStartInteractiveDaemonEarly } from "./cli/daemon-launch.js"; +import { APP_NAME } from "./config.js"; + +export async function runCli(): Promise { + try { + enableCompileCache?.(); + } catch { + // Read-only cache dir; startup just skips the cache. + } + + process.title = APP_NAME; + process.env.PI_CODING_AGENT = "true"; + process.emitWarning = (() => {}) as typeof process.emitWarning; + + // Boot a cold daemon concurrently with this process's heavy imports. + maybeStartInteractiveDaemonEarly(process.argv.slice(2)); + + const [{ EnvHttpProxyAgent, setGlobalDispatcher }, { main }] = await Promise.all([ + import("undici"), + import("./main.js"), + ]); + + // undici's 300s body/headers timeouts abort long local-LLM SSE stalls; provider + // SDKs enforce their own deadlines via retry.provider.timeoutMs. + setGlobalDispatcher(new EnvHttpProxyAgent({ bodyTimeout: 0, headersTimeout: 0 })); + + await main(process.argv.slice(2)); +} diff --git a/packages/coding-agent/src/cli.ts b/packages/coding-agent/src/cli.ts index 2a8b49d3e7..07d1b9b3d0 100644 --- a/packages/coding-agent/src/cli.ts +++ b/packages/coding-agent/src/cli.ts @@ -1,39 +1,15 @@ #!/usr/bin/env node -/** - * CLI entry point for the refactored coding agent. - * Uses main.ts with AgentSession and new mode modules. - * - * Test with: npx tsx src/cli-new.ts [args...] - */ -import { enableCompileCache } from "node:module"; -import { maybeStartInteractiveDaemonEarly } from "./cli/daemon-launch.js"; -import { APP_NAME } from "./config.js"; - -// Persist V8 compile caches across runs (~10-15% off module-graph load time). -try { - enableCompileCache?.(); -} catch { - // Unsupported Node version or read-only cache dir; startup just skips the cache. +// The Node 22+ module graph fails at link time on older Node, so it must load +// behind the dynamic import, after the dependency-free guard runs. +import { assertNodeVersion } from "./cli/node-version-check.js"; + +const supported = assertNodeVersion({ + version: process.versions.node, + log: console.error, + exit: (code) => process.exit(code), +}); + +if (supported) { + const { runCli } = await import("./cli-main.js"); + await runCli(); } - -process.title = APP_NAME; -process.env.PI_CODING_AGENT = "true"; -process.emitWarning = (() => {}) as typeof process.emitWarning; - -// Kick off the interactive daemon spawn/probe before importing the heavy main -// module graph (~1.5s), so a cold daemon boots concurrently with this -// process's own imports instead of serially after them. -maybeStartInteractiveDaemonEarly(process.argv.slice(2)); - -const [{ EnvHttpProxyAgent, setGlobalDispatcher }, { main }] = await Promise.all([ - import("undici"), - import("./main.js"), -]); - -// bodyTimeout/headersTimeout default to 300s in undici; long local-LLM stalls -// (e.g. vLLM buffering a large tool call) exceed that and abort the SSE stream -// with UND_ERR_BODY_TIMEOUT. Disable both — provider SDKs enforce their own -// AbortController-based deadlines via retry.provider.timeoutMs. -setGlobalDispatcher(new EnvHttpProxyAgent({ bodyTimeout: 0, headersTimeout: 0 })); - -await main(process.argv.slice(2)); diff --git a/packages/coding-agent/src/cli/node-version-check.ts b/packages/coding-agent/src/cli/node-version-check.ts new file mode 100644 index 0000000000..f2bff2ec2a --- /dev/null +++ b/packages/coding-agent/src/cli/node-version-check.ts @@ -0,0 +1,35 @@ +// Dependency-free and Node-20-safe so it can never crash on the versions it rejects. + +export const MIN_NODE_MAJOR = 22; + +export interface NodeVersionGuardIO { + version: string; + log: (message: string) => void; + exit: (code: number) => void; +} + +function parseMajor(version: string): number { + return parseInt(version.split(".")[0]!.replace(/^v/, ""), 10); +} + +export function assertNodeVersion(io: NodeVersionGuardIO): boolean { + // Bun ships its own runtime; its node-compat version is unrelated to the user's Node. + if (process.versions.bun) { + return true; + } + + const major = parseMajor(io.version); + if (Number.isNaN(major) || major >= MIN_NODE_MAJOR) { + return true; + } + + io.log(`prime-agent requires Node ${MIN_NODE_MAJOR} or newer, but the active Node is v${io.version}.`); + io.log(""); + io.log("Upgrade Node, then reinstall so the command resolves to the new version:"); + io.log(""); + io.log(" nvm install 22 && nvm use 22 # or: install Node 22+ from https://nodejs.org"); + io.log(" npm install -g prime-agent"); + io.log(""); + io.exit(1); + return false; +} diff --git a/packages/coding-agent/test/node-version-check.test.ts b/packages/coding-agent/test/node-version-check.test.ts new file mode 100644 index 0000000000..fc9a766f97 --- /dev/null +++ b/packages/coding-agent/test/node-version-check.test.ts @@ -0,0 +1,46 @@ +import { describe, expect, test } from "vitest"; +import { assertNodeVersion, MIN_NODE_MAJOR } from "../src/cli/node-version-check.js"; + +function run(version: string) { + const logs: string[] = []; + let exitCode: number | null = null; + const ok = assertNodeVersion({ + version, + log: (m) => logs.push(m), + exit: (code) => { + exitCode = code; + }, + }); + return { ok, logs, exitCode }; +} + +describe("assertNodeVersion", () => { + test("passes on the minimum supported major", () => { + const { ok, logs, exitCode } = run(`${MIN_NODE_MAJOR}.0.0`); + expect(ok).toBe(true); + expect(exitCode).toBeNull(); + expect(logs).toHaveLength(0); + }); + + test("passes on a newer major", () => { + const { ok, exitCode } = run("25.9.0"); + expect(ok).toBe(true); + expect(exitCode).toBeNull(); + }); + + test("rejects an outdated major with guidance and exit 1", () => { + const { ok, logs, exitCode } = run("20.18.1"); + expect(ok).toBe(false); + expect(exitCode).toBe(1); + const text = logs.join("\n"); + expect(text).toContain(`Node ${MIN_NODE_MAJOR}`); + expect(text).toContain("20.18.1"); + expect(text).toContain("npm install -g prime-agent"); + }); + + test("lets an unparseable version through rather than blocking", () => { + const { ok, exitCode } = run("not-a-version"); + expect(ok).toBe(true); + expect(exitCode).toBeNull(); + }); +}); From 0457145b5b15a519e587b618aff69c5636a9b4bb Mon Sep 17 00:00:00 2001 From: Kevin Thomas Date: Sat, 27 Jun 2026 01:24:34 +0000 Subject: [PATCH 2/3] point node-version guidance at the releases page instead of a wrong npm name --- packages/coding-agent/src/cli/node-version-check.ts | 8 +++----- packages/coding-agent/test/node-version-check.test.ts | 2 +- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/packages/coding-agent/src/cli/node-version-check.ts b/packages/coding-agent/src/cli/node-version-check.ts index f2bff2ec2a..61d4d59ab7 100644 --- a/packages/coding-agent/src/cli/node-version-check.ts +++ b/packages/coding-agent/src/cli/node-version-check.ts @@ -25,11 +25,9 @@ export function assertNodeVersion(io: NodeVersionGuardIO): boolean { io.log(`prime-agent requires Node ${MIN_NODE_MAJOR} or newer, but the active Node is v${io.version}.`); io.log(""); - io.log("Upgrade Node, then reinstall so the command resolves to the new version:"); - io.log(""); - io.log(" nvm install 22 && nvm use 22 # or: install Node 22+ from https://nodejs.org"); - io.log(" npm install -g prime-agent"); - io.log(""); + io.log(` 1. Install Node ${MIN_NODE_MAJOR}+ (e.g. "nvm install 22 && nvm use 22", or from https://nodejs.org)`); + io.log(" 2. Reinstall prime-agent under that Node so the command resolves to it:"); + io.log(" https://github.com/PrimeIntellect-ai/prime-agent/releases/latest"); io.exit(1); return false; } diff --git a/packages/coding-agent/test/node-version-check.test.ts b/packages/coding-agent/test/node-version-check.test.ts index fc9a766f97..3003bd2066 100644 --- a/packages/coding-agent/test/node-version-check.test.ts +++ b/packages/coding-agent/test/node-version-check.test.ts @@ -35,7 +35,7 @@ describe("assertNodeVersion", () => { const text = logs.join("\n"); expect(text).toContain(`Node ${MIN_NODE_MAJOR}`); expect(text).toContain("20.18.1"); - expect(text).toContain("npm install -g prime-agent"); + expect(text).toContain("github.com/PrimeIntellect-ai/prime-agent/releases/latest"); }); test("lets an unparseable version through rather than blocking", () => { From fd0a37153ba1d45eb0a2a2be73050a2bc981f9ec Mon Sep 17 00:00:00 2001 From: Kevin Thomas Date: Thu, 16 Jul 2026 11:47:28 -0700 Subject: [PATCH 3/3] fix node version preflight --- package-lock.json | 4 +- package.json | 2 +- packages/coding-agent/CHANGELOG.md | 1 + packages/coding-agent/package.json | 2 +- .../src/cli/node-version-check.ts | 39 +++++++++++++--- .../test/node-version-check.test.ts | 45 +++++++++++++++++-- 6 files changed, 78 insertions(+), 15 deletions(-) diff --git a/package-lock.json b/package-lock.json index a7a418d52a..820d73ceda 100644 --- a/package-lock.json +++ b/package-lock.json @@ -31,7 +31,7 @@ "typescript": "^5.9.2" }, "engines": { - "node": ">=22.0.0" + "node": ">=22.8.0" } }, "node_modules/@anthropic-ai/sandbox-runtime": { @@ -5852,7 +5852,7 @@ "vitest": "^4.1.9" }, "engines": { - "node": ">=22.0.0" + "node": ">=22.8.0" }, "optionalDependencies": { "@mariozechner/clipboard": "^0.3.9" diff --git a/package.json b/package.json index 1a5981a807..40bc2147b9 100644 --- a/package.json +++ b/package.json @@ -46,7 +46,7 @@ "typescript": "^5.9.2" }, "engines": { - "node": ">=22.0.0" + "node": ">=22.8.0" }, "version": "0.3.0", "dependencies": { diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index 23545c2701..0f713958ef 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -2,6 +2,7 @@ ## [Unreleased] +- Fixed unsupported Node versions crashing before startup by requiring Node 22.8.0 or newer and showing upgrade guidance before loading the CLI ([ENG-4260](https://linear.app/primeintellect/issue/ENG-4260/incorrect-node-version-breaks-first-launch)). - Added confirmation when fullscreen text selection copies to the clipboard ([ENG-4644](https://linear.app/primeintellect/issue/ENG-4644/copy-issues)). - Added compact file change stats to collapsed tool calls and an agent-run edit total above the recap. - Changed tool expansion hints to appear only on the latest tool row instead of every tool call ([ENG-4583](https://linear.app/primeintellect/issue/ENG-4583/too-many-ctrlo-alerts)). diff --git a/packages/coding-agent/package.json b/packages/coding-agent/package.json index f49c98988a..6a4a1211a0 100644 --- a/packages/coding-agent/package.json +++ b/packages/coding-agent/package.json @@ -102,6 +102,6 @@ "directory": "packages/coding-agent" }, "engines": { - "node": ">=22.0.0" + "node": ">=22.8.0" } } diff --git a/packages/coding-agent/src/cli/node-version-check.ts b/packages/coding-agent/src/cli/node-version-check.ts index 61d4d59ab7..24eef4eb8d 100644 --- a/packages/coding-agent/src/cli/node-version-check.ts +++ b/packages/coding-agent/src/cli/node-version-check.ts @@ -1,6 +1,7 @@ // Dependency-free and Node-20-safe so it can never crash on the versions it rejects. -export const MIN_NODE_MAJOR = 22; +const MIN_NODE_VERSION_PARTS = [22, 8, 0] as const; +export const MIN_NODE_VERSION = MIN_NODE_VERSION_PARTS.join("."); export interface NodeVersionGuardIO { version: string; @@ -8,8 +9,32 @@ export interface NodeVersionGuardIO { exit: (code: number) => void; } -function parseMajor(version: string): number { - return parseInt(version.split(".")[0]!.replace(/^v/, ""), 10); +interface ParsedNodeVersion { + parts: readonly [number, number, number]; + prerelease: boolean; +} + +function parseVersion(version: string): ParsedNodeVersion | undefined { + const match = /^v?(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?(?:\+[0-9A-Za-z.-]+)?$/.exec(version); + if (!match) { + return undefined; + } + + return { + parts: [Number(match[1]), Number(match[2]), Number(match[3])], + prerelease: match[4] !== undefined, + }; +} + +function isSupportedNodeVersion(version: ParsedNodeVersion): boolean { + for (let index = 0; index < MIN_NODE_VERSION_PARTS.length; index++) { + const part = version.parts[index]!; + const minimumPart = MIN_NODE_VERSION_PARTS[index]!; + if (part !== minimumPart) { + return part > minimumPart; + } + } + return !version.prerelease; } export function assertNodeVersion(io: NodeVersionGuardIO): boolean { @@ -18,14 +43,14 @@ export function assertNodeVersion(io: NodeVersionGuardIO): boolean { return true; } - const major = parseMajor(io.version); - if (Number.isNaN(major) || major >= MIN_NODE_MAJOR) { + const version = parseVersion(io.version); + if (!version || isSupportedNodeVersion(version)) { return true; } - io.log(`prime-agent requires Node ${MIN_NODE_MAJOR} or newer, but the active Node is v${io.version}.`); + io.log(`prime-agent requires Node ${MIN_NODE_VERSION} or newer, but the active Node is v${io.version}.`); io.log(""); - io.log(` 1. Install Node ${MIN_NODE_MAJOR}+ (e.g. "nvm install 22 && nvm use 22", or from https://nodejs.org)`); + io.log(` 1. Install Node ${MIN_NODE_VERSION}+ (e.g. "nvm install 22 && nvm use 22", or from https://nodejs.org)`); io.log(" 2. Reinstall prime-agent under that Node so the command resolves to it:"); io.log(" https://github.com/PrimeIntellect-ai/prime-agent/releases/latest"); io.exit(1); diff --git a/packages/coding-agent/test/node-version-check.test.ts b/packages/coding-agent/test/node-version-check.test.ts index 3003bd2066..903b677395 100644 --- a/packages/coding-agent/test/node-version-check.test.ts +++ b/packages/coding-agent/test/node-version-check.test.ts @@ -1,5 +1,5 @@ import { describe, expect, test } from "vitest"; -import { assertNodeVersion, MIN_NODE_MAJOR } from "../src/cli/node-version-check.js"; +import { assertNodeVersion, MIN_NODE_VERSION } from "../src/cli/node-version-check.js"; function run(version: string) { const logs: string[] = []; @@ -15,29 +15,66 @@ function run(version: string) { } describe("assertNodeVersion", () => { - test("passes on the minimum supported major", () => { - const { ok, logs, exitCode } = run(`${MIN_NODE_MAJOR}.0.0`); + test("passes on the minimum supported version", () => { + const { ok, logs, exitCode } = run(MIN_NODE_VERSION); expect(ok).toBe(true); expect(exitCode).toBeNull(); expect(logs).toHaveLength(0); }); + test("passes on a newer minor", () => { + const { ok, exitCode } = run("22.9.0"); + expect(ok).toBe(true); + expect(exitCode).toBeNull(); + }); + + test("passes on a newer patch", () => { + const { ok, exitCode } = run("22.8.1"); + expect(ok).toBe(true); + expect(exitCode).toBeNull(); + }); + test("passes on a newer major", () => { const { ok, exitCode } = run("25.9.0"); expect(ok).toBe(true); expect(exitCode).toBeNull(); }); + test("rejects a Node 22 release below the minimum", () => { + const { ok, logs, exitCode } = run("22.7.0"); + expect(ok).toBe(false); + expect(exitCode).toBe(1); + expect(logs.join("\n")).toContain(`Node ${MIN_NODE_VERSION}`); + }); + test("rejects an outdated major with guidance and exit 1", () => { const { ok, logs, exitCode } = run("20.18.1"); expect(ok).toBe(false); expect(exitCode).toBe(1); const text = logs.join("\n"); - expect(text).toContain(`Node ${MIN_NODE_MAJOR}`); + expect(text).toContain(`Node ${MIN_NODE_VERSION}`); expect(text).toContain("20.18.1"); expect(text).toContain("github.com/PrimeIntellect-ai/prime-agent/releases/latest"); }); + test("accepts the v prefix used by process.version", () => { + const { ok, exitCode } = run(`v${MIN_NODE_VERSION}`); + expect(ok).toBe(true); + expect(exitCode).toBeNull(); + }); + + test("accepts build metadata at the minimum version", () => { + const { ok, exitCode } = run(`${MIN_NODE_VERSION}+build.1`); + expect(ok).toBe(true); + expect(exitCode).toBeNull(); + }); + + test("rejects a prerelease of the minimum version", () => { + const { ok, exitCode } = run(`${MIN_NODE_VERSION}-rc.1`); + expect(ok).toBe(false); + expect(exitCode).toBe(1); + }); + test("lets an unparseable version through rather than blocking", () => { const { ok, exitCode } = run("not-a-version"); expect(ok).toBe(true);