diff --git a/.github/dependabot.yml b/.github/dependabot.yml
index 3ad56855a2..7015888496 100644
--- a/.github/dependabot.yml
+++ b/.github/dependabot.yml
@@ -14,6 +14,13 @@ updates:
open-pull-requests-limit: 20
labels:
- "dependencies"
+ ignore:
+ # @types/node describes the APIs available in the supported Node runtime.
+ # Runtime major upgrades are deliberate compatibility changes, so keep
+ # Dependabot on the current major until the runtime baseline moves too.
+ - dependency-name: "@types/node"
+ update-types:
+ - "version-update:semver-major"
- package-ecosystem: github-actions
directory: "/"
diff --git a/.github/workflows/commitperclip-review.yml b/.github/workflows/commitperclip-review.yml
index 485e269eef..4078244d79 100644
--- a/.github/workflows/commitperclip-review.yml
+++ b/.github/workflows/commitperclip-review.yml
@@ -31,7 +31,7 @@ jobs:
- name: Set up Node
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
- node-version: '20'
+ node-version: '24'
- name: Generate commitperclip token
id: token
diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml
index 2e41dd7657..549595dbe3 100644
--- a/.github/workflows/docker.yml
+++ b/.github/workflows/docker.yml
@@ -82,7 +82,7 @@ jobs:
- name: Setup Node.js
uses: actions/setup-node@v7
with:
- node-version: 20
+ node-version: 24
- name: Refresh lockfile for Docker build context
run: |
@@ -257,7 +257,7 @@ jobs:
- name: Setup Node.js
uses: actions/setup-node@v7
with:
- node-version: 20
+ node-version: 24
- name: Refresh lockfile for Docker build context
run: |
diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml
index 68042c0496..1b5159a072 100644
--- a/.github/workflows/e2e.yml
+++ b/.github/workflows/e2e.yml
@@ -24,7 +24,7 @@ jobs:
- uses: actions/setup-node@v7
with:
- node-version: 20
+ node-version: 24
cache: pnpm
- run: pnpm install --frozen-lockfile
diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml
index 7fe5fca58e..591caf04ed 100644
--- a/.github/workflows/pr.yml
+++ b/.github/workflows/pr.yml
@@ -49,6 +49,9 @@ jobs:
- name: Validate Dockerfile deps stage
run: node ./scripts/check-docker-deps-stage.mjs
+ - name: Validate Node version policy
+ run: pnpm check:node-version
+
- name: Reject git push in adapter/runtime code
run: node ./scripts/check-no-git-push.mjs
diff --git a/.github/workflows/refresh-lockfile.yml b/.github/workflows/refresh-lockfile.yml
index 06546bbb73..49ac2176e7 100644
--- a/.github/workflows/refresh-lockfile.yml
+++ b/.github/workflows/refresh-lockfile.yml
@@ -31,7 +31,7 @@ jobs:
- name: Setup Node.js
uses: actions/setup-node@v7
with:
- node-version: 20
+ node-version: 24
cache: pnpm
- name: Refresh pnpm lockfile
diff --git a/.nvmrc b/.nvmrc
new file mode 100644
index 0000000000..a45fd52cc5
--- /dev/null
+++ b/.nvmrc
@@ -0,0 +1 @@
+24
diff --git a/Dockerfile b/Dockerfile
index 4e3a271529..c41520df32 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -1,5 +1,5 @@
# syntax=docker/dockerfile:1.20
-FROM node:lts-trixie-slim AS base
+FROM node:24-trixie-slim AS base
ARG USER_UID=1000
ARG USER_GID=1000
RUN apt-get update \
diff --git a/README.md b/README.md
index 53a7291c46..5f19d11df6 100644
--- a/README.md
+++ b/README.md
@@ -311,7 +311,7 @@ fi
bash install.sh
```
-The installer ensures Node.js 20 or newer is available, installs a managed
+The installer ensures Node.js 24.11 or newer is available, installs a managed
Paperclip CLI under `~/.paperclip/cli`, and starts interactive onboarding. It
can also install Paperclip as a background service on supported Linux and
macOS systems. The checksum detects transfer or publishing mistakes, but it is
@@ -376,7 +376,7 @@ pnpm dev
This starts the API server at `http://localhost:3100`. An embedded PostgreSQL database is created automatically — no setup required.
-> **Requirements:** Node.js 20+, pnpm 9.15+
+> **Requirements:** Node.js 24.11+, pnpm 9.15+
diff --git a/cli/README.md b/cli/README.md
index a32a6c5611..5817e6b6fd 100644
--- a/cli/README.md
+++ b/cli/README.md
@@ -305,7 +305,7 @@ pnpm dev
This starts the API server at `http://localhost:3100`. An embedded PostgreSQL database is created automatically — no setup required.
-> **Requirements:** Node.js 20+, pnpm 9.15+
+> **Requirements:** Node.js 24.11+, pnpm 9.15+
diff --git a/cli/esbuild.config.mjs b/cli/esbuild.config.mjs
index 3432ef5bf4..fee12f51fb 100644
--- a/cli/esbuild.config.mjs
+++ b/cli/esbuild.config.mjs
@@ -70,7 +70,7 @@ export default {
entryPoints: ["src/index.ts"],
bundle: true,
platform: "node",
- target: "node20",
+ target: "node24",
format: "esm",
outfile: "dist/index.js",
banner: { js: "#!/usr/bin/env node" },
diff --git a/cli/package.json b/cli/package.json
index 77ce5f6dbc..4113ed4d45 100644
--- a/cli/package.json
+++ b/cli/package.json
@@ -59,8 +59,11 @@
"picocolors": "^1.1.1"
},
"devDependencies": {
- "@types/node": "^22.20.1",
+ "@types/node": "^24.0.0",
"tsx": "^4.23.12",
"typescript": "^5.7.3"
+ },
+ "engines": {
+ "node": ">=24.11.0"
}
}
diff --git a/cli/src/checks/managed-install-check.ts b/cli/src/checks/managed-install-check.ts
index 025f1bf33f..3f1447a4e7 100644
--- a/cli/src/checks/managed-install-check.ts
+++ b/cli/src/checks/managed-install-check.ts
@@ -7,6 +7,7 @@ import {
type InstallStorePaths,
} from "../install-store.js";
import type { CheckResult } from "./index.js";
+import { isSupportedNodeVersion, MINIMUM_NODE_VERSION } from "@paperclipai/shared/node-version";
function pathContains(directory: string): boolean {
const normalized = path.resolve(directory);
@@ -33,14 +34,13 @@ function hasManagedArtifacts(paths: InstallStorePaths): boolean {
}
export function nodeRuntimeCheck(): CheckResult {
- const major = Number.parseInt(process.versions.node.split(".")[0] ?? "0", 10);
- return major >= 20
+ return isSupportedNodeVersion(process.versions.node)
? { name: "Node.js runtime", status: "pass", message: `Node.js ${process.versions.node}` }
: {
name: "Node.js runtime",
status: "fail",
message: `Node.js ${process.versions.node} is unsupported`,
- repairHint: "Install Node.js 20 or newer before installing or running Paperclip",
+ repairHint: `Install Node.js ${MINIMUM_NODE_VERSION} or newer before installing or running Paperclip`,
};
}
diff --git a/cli/src/commands/install.ts b/cli/src/commands/install.ts
index 1667fe3b3b..8559d83cb8 100644
--- a/cli/src/commands/install.ts
+++ b/cli/src/commands/install.ts
@@ -5,6 +5,7 @@ import { execFile } from "node:child_process";
import { promisify } from "node:util";
import * as p from "@clack/prompts";
import pc from "picocolors";
+import { isSupportedNodeVersion, MINIMUM_NODE_VERSION } from "@paperclipai/shared/node-version";
import {
addManagedPathBlock,
assertManagedShimWritable,
@@ -84,9 +85,8 @@ export function resolveGitInstallWorkspacePackages(checkoutPath: string): Releas
}
function assertSupportedNodeVersion(): void {
- const major = Number(process.versions.node.split(".")[0]);
- if (!Number.isFinite(major) || major < 20) {
- throw new Error(`Managed installs require Node.js 20 or newer (found ${process.version}).`);
+ if (!isSupportedNodeVersion(process.versions.node)) {
+ throw new Error(`Managed installs require Node.js ${MINIMUM_NODE_VERSION} or newer (found ${process.version}).`);
}
}
diff --git a/cli/src/index.ts b/cli/src/index.ts
index b26b5cbe18..db9c8413f3 100644
--- a/cli/src/index.ts
+++ b/cli/src/index.ts
@@ -1,4 +1,5 @@
import { Command } from "commander";
+import { warnIfUnsupportedNodeVersion } from "@paperclipai/shared/node-version";
import { onboard } from "./commands/onboard.js";
import { doctor } from "./commands/doctor.js";
import { envCommand } from "./commands/env.js";
@@ -250,6 +251,8 @@ auth
registerClientAuthCommands(auth);
async function main(): Promise {
+ warnIfUnsupportedNodeVersion(process.versions.node, (message) => console.warn(message));
+
let failed = false;
try {
await program.parseAsync();
diff --git a/cli/src/node-version.test.ts b/cli/src/node-version.test.ts
new file mode 100644
index 0000000000..ef1cde7359
--- /dev/null
+++ b/cli/src/node-version.test.ts
@@ -0,0 +1,44 @@
+import { describe, expect, it } from "vitest";
+import {
+ formatNodeVersionWarning,
+ isSupportedNodeVersion,
+ MINIMUM_NODE_VERSION,
+ NODE_VERSION_INSTALL_GUIDE_URL,
+ warnIfUnsupportedNodeVersion,
+} from "@paperclipai/shared/node-version";
+
+describe("isSupportedNodeVersion", () => {
+ it("accepts the Node 24 LTS floor and newer releases", () => {
+ expect(MINIMUM_NODE_VERSION).toBe("24.11.0");
+ expect(isSupportedNodeVersion("v24.11.0")).toBe(true);
+ expect(isSupportedNodeVersion("24.19.0")).toBe(true);
+ expect(isSupportedNodeVersion("v26.0.0")).toBe(true);
+ });
+
+ it("rejects pre-LTS Node 24 and older major releases", () => {
+ expect(isSupportedNodeVersion("v24.10.0")).toBe(false);
+ expect(isSupportedNodeVersion("v22.23.0")).toBe(false);
+ expect(isSupportedNodeVersion("unknown")).toBe(false);
+ });
+
+ it("provides non-blocking remediation text for unsupported runtimes", () => {
+ expect(formatNodeVersionWarning("v24.11.0")).toBeNull();
+ const warning = formatNodeVersionWarning("v22.23.0");
+ expect(warning).toContain("Node.js v22.23.0 is unsupported");
+ expect(warning).toContain("requires Node.js 24.11.0 or newer");
+ expect(warning).toContain(NODE_VERSION_INSTALL_GUIDE_URL);
+ expect(warning).toContain("piped install.sh form cannot upgrade");
+ expect(warning).toContain("Restart Paperclip after upgrading");
+ });
+
+ it("emits at most one warning when CLI and server boot in the same process", () => {
+ const warnings: string[] = [];
+ expect(
+ warnIfUnsupportedNodeVersion("22.23.0", (message) => warnings.push(message)),
+ ).toBe(true);
+ expect(
+ warnIfUnsupportedNodeVersion("22.23.0", (message) => warnings.push(message)),
+ ).toBe(false);
+ expect(warnings).toHaveLength(1);
+ });
+});
diff --git a/doc/DEVELOPING.md b/doc/DEVELOPING.md
index 3a5e153175..995a2d9e93 100644
--- a/doc/DEVELOPING.md
+++ b/doc/DEVELOPING.md
@@ -12,7 +12,7 @@ Current implementation status:
## Prerequisites
-- Node.js 20+
+- Node.js 24.11+
- pnpm 9+
## Dependency Lockfile Policy
diff --git a/doc/INSTALLING.md b/doc/INSTALLING.md
index 60db823616..c817645e2b 100644
--- a/doc/INSTALLING.md
+++ b/doc/INSTALLING.md
@@ -23,7 +23,7 @@ bash install.sh
The bootstrap script:
1. verifies that the platform is supported;
-2. ensures Node.js 20 or newer is available;
+2. ensures Node.js 24.11 or newer is available;
3. delegates installation to `paperclipai install`;
4. starts interactive onboarding when stdin and stdout are terminals.
@@ -241,6 +241,12 @@ paperclipai service status
presence and drift, running state, configured port ownership, and the running
server version.
+The CLI and server also print a non-blocking startup warning when Node.js is
+below the supported minimum. Upgrade Node.js with a version manager or follow
+the downloaded `install.sh` workflow under **Recommended Install**. Do not use
+the piped form for this repair because it requires a supported Node.js runtime
+before it starts.
+
## Uninstall
Remove the background service and managed CLI payloads:
diff --git a/doc/SPEC-implementation.md b/doc/SPEC-implementation.md
index 5bba24c9b9..bb672aca60 100644
--- a/doc/SPEC-implementation.md
+++ b/doc/SPEC-implementation.md
@@ -1313,7 +1313,7 @@ Required UX behaviors:
## 15.1 Environment
-- Node 20+
+- Node 24.11+
- `DATABASE_URL` optional
- if unset, auto-use embedded PostgreSQL under `~/.paperclip/instances/default/db`
diff --git a/doc/plugins/LOCAL_PLUGIN_DEVELOPMENT.md b/doc/plugins/LOCAL_PLUGIN_DEVELOPMENT.md
index 124ba3c0ac..b3a489815d 100644
--- a/doc/plugins/LOCAL_PLUGIN_DEVELOPMENT.md
+++ b/doc/plugins/LOCAL_PLUGIN_DEVELOPMENT.md
@@ -11,7 +11,7 @@ pause controls, and consistent audits instead of hidden daemon behavior.
## Prerequisites
-- Node.js 22+ and `pnpm`.
+- Node.js 24.11+ and `pnpm`.
- A local Paperclip checkout you can run from source. Local plugin installs read source from disk, so the running server must be able to see the path you give it.
## The five steps
diff --git a/docker/Dockerfile.onboard-smoke b/docker/Dockerfile.onboard-smoke
index 63689c6c8a..3823bf3f09 100644
--- a/docker/Dockerfile.onboard-smoke
+++ b/docker/Dockerfile.onboard-smoke
@@ -1,6 +1,6 @@
FROM ubuntu:24.04
-ARG NODE_MAJOR=20
+ARG NODE_MAJOR=24
ARG PAPERCLIPAI_VERSION=latest
ARG HOST_UID=10001
diff --git a/docker/agent-runtime/Dockerfile.base b/docker/agent-runtime/Dockerfile.base
index 775a248313..335edca31e 100644
--- a/docker/agent-runtime/Dockerfile.base
+++ b/docker/agent-runtime/Dockerfile.base
@@ -1,5 +1,5 @@
# syntax=docker/dockerfile:1.6
-ARG NODE_VERSION=22
+ARG NODE_VERSION=24
ARG TARGETARCH
# ---------- Stage 1: build agent-shim ----------
diff --git a/docker/agent-runtime/README.md b/docker/agent-runtime/README.md
index 9783495173..2d9c2c3f57 100644
--- a/docker/agent-runtime/README.md
+++ b/docker/agent-runtime/README.md
@@ -4,7 +4,7 @@ Container images for running coding-agent harnesses in sandboxed environments (f
## Image Lineup
-- **`agent-runtime-base`**: Foundation. Ubuntu 22.04 + Node 22 + git + tini + non-root user (uid 1000) + the agent shim.
+- **`agent-runtime-base`**: Foundation. Ubuntu 22.04 + Node 24 + git + tini + non-root user (uid 1000) + the agent shim.
- **`agent-runtime-opencode`**: Extends base with `opencode-ai` globally installed.
- **`agent-runtime-pi`**: Extends base with `@mariozechner/pi-coding-agent`.
- **`agent-runtime-codex`**: Extends base with `@openai/codex`.
@@ -16,7 +16,7 @@ Container images for running coding-agent harnesses in sandboxed environments (f
**OS & Runtime:**
- Ubuntu 22.04
-- Node.js 22 (via NodeSource APT repo)
+- Node.js 24 (via NodeSource APT repo)
- git
- tini (PID-1 init, ensures signal propagation)
- Non-root user `paperclip` (uid/gid 1000)
diff --git a/docker/hermes-gateway-smoke/Dockerfile b/docker/hermes-gateway-smoke/Dockerfile
index 5939c7df6e..5349441e4f 100644
--- a/docker/hermes-gateway-smoke/Dockerfile
+++ b/docker/hermes-gateway-smoke/Dockerfile
@@ -1,6 +1,6 @@
# syntax=docker/dockerfile:1.6
ARG HERMES_VERSION=0.17.0
-FROM node:22-bookworm-slim
+FROM node:24-bookworm-slim
ARG HERMES_VERSION
diff --git a/docker/openclaw-smoke/Dockerfile b/docker/openclaw-smoke/Dockerfile
index abdbaf1476..b644bd5f6a 100644
--- a/docker/openclaw-smoke/Dockerfile
+++ b/docker/openclaw-smoke/Dockerfile
@@ -1,4 +1,4 @@
-FROM node:22-alpine
+FROM node:24-alpine
WORKDIR /app
COPY server.mjs /app/server.mjs
diff --git a/docker/untrusted-review/Dockerfile b/docker/untrusted-review/Dockerfile
index c8b1f4323a..b40e59a96b 100644
--- a/docker/untrusted-review/Dockerfile
+++ b/docker/untrusted-review/Dockerfile
@@ -1,4 +1,4 @@
-FROM node:lts-trixie-slim
+FROM node:24-trixie-slim
RUN apt-get update \
&& apt-get install -y --no-install-recommends \
diff --git a/docs/adapters/external-adapters.md b/docs/adapters/external-adapters.md
index 3a6b4ee6c7..5c2f6c31c7 100644
--- a/docs/adapters/external-adapters.md
+++ b/docs/adapters/external-adapters.md
@@ -74,7 +74,7 @@ my-adapter/
"picocolors": "^1.1.0"
},
"devDependencies": {
- "@types/node": "^22.0.0",
+ "@types/node": "^24.0.0",
"typescript": "^5.7.0"
}
}
diff --git a/docs/deploy/local-development.md b/docs/deploy/local-development.md
index 65b2cb59e6..ac93c069ff 100644
--- a/docs/deploy/local-development.md
+++ b/docs/deploy/local-development.md
@@ -7,7 +7,7 @@ Run Paperclip locally with zero external dependencies.
## Prerequisites
-- Node.js 20+
+- Node.js 24.11+
- pnpm 9+
## Start Dev Server
diff --git a/docs/guides/openclaw-docker-setup.md b/docs/guides/openclaw-docker-setup.md
index d3eb170431..27262d263c 100644
--- a/docs/guides/openclaw-docker-setup.md
+++ b/docs/guides/openclaw-docker-setup.md
@@ -316,7 +316,7 @@ This issue does not affect the Docker Sandbox approach.
### Node version mismatch in community template images
-Some community-built sandbox templates (e.g. `olegselajev241/openclaw-dmr:latest`) ship Node 20, but OpenClaw requires Node >=22.12.0. Use our locally built `openclaw:local` image as the sandbox template instead, which includes Node 22.
+Some community-built sandbox templates (e.g. `olegselajev241/openclaw-dmr:latest`) ship Node 20, but OpenClaw requires Node >=22.12.0. Use our locally built `openclaw:local` image as the sandbox template instead, which includes Node 24.
### Gateway takes ~15 seconds to respond after start
diff --git a/docs/start/architecture.md b/docs/start/architecture.md
index dd8839a51b..11bd009ddc 100644
--- a/docs/start/architecture.md
+++ b/docs/start/architecture.md
@@ -29,7 +29,7 @@ Paperclip is a monorepo with four main layers.
| Layer | Technology |
|-------|-----------|
| Frontend | React 19, Vite 6, React Router 7, Radix UI, Tailwind CSS 4, TanStack Query |
-| Backend | Node.js 20+, Express.js 5, TypeScript |
+| Backend | Node.js 24.11+, Express.js 5, TypeScript |
| Database | PostgreSQL 17 (or embedded PGlite), Drizzle ORM |
| Auth | Better Auth (sessions + API keys) |
| Adapters | Claude Code CLI, Codex CLI, shell process, HTTP webhook |
diff --git a/docs/start/quickstart.md b/docs/start/quickstart.md
index 2abe538b0e..73dbc9b79e 100644
--- a/docs/start/quickstart.md
+++ b/docs/start/quickstart.md
@@ -25,7 +25,7 @@ npx paperclipai run
## Local Development
-For contributors working on Paperclip itself. Prerequisites: Node.js 20+ and pnpm 9+.
+For contributors working on Paperclip itself. Prerequisites: Node.js 24.11+ and pnpm 9+.
Clone the repository, then:
diff --git a/package.json b/package.json
index 6bb72c3f63..16132aeaae 100644
--- a/package.json
+++ b/package.json
@@ -40,6 +40,7 @@
"release:bootstrap-package": "node scripts/bootstrap-npm-package.mjs",
"check:tokens": "node scripts/check-forbidden-tokens.mjs",
"check:token-gates": "node scripts/check-token-gates.mjs",
+ "check:node-version": "node scripts/check-node-version-policy.mjs",
"check:no-git-push": "node scripts/check-no-git-push.mjs",
"test:check-no-git-push": "node --test scripts/check-no-git-push.test.mjs",
"test:install-sh-docker": "./scripts/test-install-sh-docker.sh",
@@ -76,7 +77,7 @@
"vitest": "^4.1.10"
},
"engines": {
- "node": ">=20"
+ "node": ">=24.11.0"
},
"packageManager": "pnpm@9.15.4",
"pnpm": {
diff --git a/packages/adapter-utils/package.json b/packages/adapter-utils/package.json
index 5f8525a384..7e5d745171 100644
--- a/packages/adapter-utils/package.json
+++ b/packages/adapter-utils/package.json
@@ -47,7 +47,10 @@
"picocolors": "^1.1.1"
},
"devDependencies": {
- "@types/node": "^22.20.1",
+ "@types/node": "^24.0.0",
"typescript": "^5.7.3"
+ },
+ "engines": {
+ "node": ">=24.11.0"
}
}
diff --git a/packages/adapter-utils/src/sandbox-install-command.test.ts b/packages/adapter-utils/src/sandbox-install-command.test.ts
index 01c32936e3..8826341376 100644
--- a/packages/adapter-utils/src/sandbox-install-command.test.ts
+++ b/packages/adapter-utils/src/sandbox-install-command.test.ts
@@ -9,9 +9,11 @@ describe("buildSandboxNpmInstallCommand", () => {
expect(command).toContain("npm install -g --prefix \"$HOME/.local\" '@google/gemini-cli'");
});
- it("bootstraps npm from a portable Node tarball when missing", () => {
+ it("bootstraps npm from a portable Node tarball when missing or unsupported", () => {
const command = buildSandboxNpmInstallCommand("@google/gemini-cli");
- expect(command).toContain("if ! command -v npm >/dev/null 2>&1; then");
+ expect(command).toContain("if ! command -v npm >/dev/null 2>&1 || ! command -v node >/dev/null 2>&1 ||");
+ expect(command).toContain("process.versions.node");
+ expect(command).toContain("v[0]>24||(v[0]===24&&v[1]>=11)");
expect(command).toContain("https://nodejs.org/dist/");
expect(command).toContain('export PATH="$HOME/.local/bin:$PATH"');
});
diff --git a/packages/adapter-utils/src/sandbox-install-command.ts b/packages/adapter-utils/src/sandbox-install-command.ts
index d8ef0767d2..c3d1468e5d 100644
--- a/packages/adapter-utils/src/sandbox-install-command.ts
+++ b/packages/adapter-utils/src/sandbox-install-command.ts
@@ -13,13 +13,16 @@ function shellSingleQuote(value: string): string {
// $HOME/.local/bin.
const ENSURE_NPM_PREAMBLE =
"PAPERCLIP_NPM_BOOTSTRAPPED=; " +
- 'if ! command -v npm >/dev/null 2>&1; then ' +
+ "if ! command -v npm >/dev/null 2>&1 || " +
+ "! command -v node >/dev/null 2>&1 || " +
+ "! node -e 'const v=process.versions.node.split(\".\").map(Number);" +
+ "process.exit(v[0]>24||(v[0]===24&&v[1]>=11)?0:1)' >/dev/null 2>&1; then " +
'NODE_ARCH="$(uname -m)"; ' +
'case "$NODE_ARCH" in ' +
"x86_64) NODE_ARCH=x64 ;; " +
"aarch64|arm64) NODE_ARCH=arm64 ;; " +
"esac; " +
- 'NODE_VERSION="v22.11.0"; ' +
+ 'NODE_VERSION="v24.11.0"; ' +
'NODE_TARBALL="node-${NODE_VERSION}-linux-${NODE_ARCH}.tar.xz"; ' +
'mkdir -p "$HOME/.local"; ' +
'curl -fsSL "https://nodejs.org/dist/${NODE_VERSION}/${NODE_TARBALL}" -o "/tmp/${NODE_TARBALL}" && ' +
diff --git a/packages/adapters/claude-local/package.json b/packages/adapters/claude-local/package.json
index f2343d7772..e03130bc2e 100644
--- a/packages/adapters/claude-local/package.json
+++ b/packages/adapters/claude-local/package.json
@@ -59,7 +59,10 @@
"picocolors": "^1.1.1"
},
"devDependencies": {
- "@types/node": "^22.20.1",
+ "@types/node": "^24.0.0",
"typescript": "^5.7.3"
+ },
+ "engines": {
+ "node": ">=24.11.0"
}
}
diff --git a/packages/adapters/claude-local/src/index.ts b/packages/adapters/claude-local/src/index.ts
index 88fb662612..4d54d86ff4 100644
--- a/packages/adapters/claude-local/src/index.ts
+++ b/packages/adapters/claude-local/src/index.ts
@@ -69,7 +69,7 @@ Operational fields:
Notes:
- filesystemScope and networkScope are spawn-level confinement and are orthogonal to Claude permission flags. Both require Bubblewrap on the host and select the CLI engine in auto mode; engine="acp" is rejected because ACP confinement is not yet supported. networkScope="allowlist" injects HTTP_PROXY/HTTPS_PROXY for the CLI while its private network namespace blocks direct sockets, so every required provider/API hostname must be listed explicitly.
-- The Claude ACP lane requires Node >=22.12.0 and @agentclientprotocol/claude-agent-acp to be installed with this adapter package. Auto engine selection falls back to CLI when those prerequisites are unavailable; explicit engine="acp" fails loudly.
+- The Claude ACP lane requires Node >=24.11.0 and @agentclientprotocol/claude-agent-acp to be installed with this adapter package. Auto engine selection falls back to CLI when those prerequisites are unavailable; explicit engine="acp" fails loudly.
- For ACP runs, model selection is passed through ANTHROPIC_MODEL at ACP server startup; Paperclip-managed Claude permissions and ephemeral skill materialization are handled by the shared ACP engine.
- When Paperclip realizes a workspace/runtime for a run, it injects PAPERCLIP_WORKSPACE_* and PAPERCLIP_RUNTIME_* env vars for agent-side tooling.
`;
diff --git a/packages/adapters/claude-local/src/server/acp.test.ts b/packages/adapters/claude-local/src/server/acp.test.ts
index 4c54b1ac7f..ddf1737fa7 100644
--- a/packages/adapters/claude-local/src/server/acp.test.ts
+++ b/packages/adapters/claude-local/src/server/acp.test.ts
@@ -259,9 +259,9 @@ describe("claude_local ACP lane", () => {
});
it("checks the Node version required by the Claude ACP runtime", () => {
- setNodeVersion("v22.11.0");
+ setNodeVersion("v24.10.0");
expect(nodeVersionMeetsClaudeAcpMinimum()).toBe(false);
- setNodeVersion("v22.12.0");
+ setNodeVersion("v24.11.0");
expect(nodeVersionMeetsClaudeAcpMinimum()).toBe(true);
});
@@ -270,7 +270,7 @@ describe("claude_local ACP lane", () => {
const commandPath = path.join(root, "bin", "claude-agent-acp");
await fs.mkdir(path.dirname(commandPath), { recursive: true });
await fs.writeFile(commandPath, "#!/usr/bin/env sh\n", "utf8");
- setNodeVersion("v22.12.0");
+ setNodeVersion("v24.11.0");
expect(resolveClaudeExecutionEngine({})).toEqual({ engine: "acp", explicit: false });
await expect(
@@ -286,7 +286,7 @@ describe("claude_local ACP lane", () => {
}),
).resolves.toEqual({ engine: "cli", explicit: true });
- setNodeVersion("v22.11.0");
+ setNodeVersion("v24.10.0");
await expect(
resolveClaudeExecutionEngineForRun({
config: { agentCommand: commandPath },
@@ -341,7 +341,7 @@ describe("claude_local ACP lane", () => {
});
it("uses ACP for bridged sandbox auto runs when the ACP command is configured as a shell command", async () => {
- setNodeVersion("v22.12.0");
+ setNodeVersion("v24.11.0");
await expect(
resolveClaudeExecutionEngineForRun({
config: { agentCommand: "claude-agent-acp" },
@@ -367,7 +367,7 @@ describe("claude_local ACP lane", () => {
});
it("falls back to the CLI lane for one-shot sandbox auto runs", async () => {
- setNodeVersion("v22.12.0");
+ setNodeVersion("v24.11.0");
await expect(
resolveClaudeExecutionEngineForRun({
config: {},
@@ -386,7 +386,7 @@ describe("claude_local ACP lane", () => {
});
it("falls back to the CLI lane for non-sandbox remote auto runs", async () => {
- setNodeVersion("v22.12.0");
+ setNodeVersion("v24.11.0");
await expect(
resolveClaudeExecutionEngineForRun({
config: {},
@@ -418,7 +418,7 @@ describe("claude_local ACP lane", () => {
const commandPath = path.join(root, "bin", "claude-agent-acp");
await fs.mkdir(path.dirname(commandPath), { recursive: true });
await fs.writeFile(commandPath, "#!/usr/bin/env sh\n", "utf8");
- setNodeVersion("v22.12.0");
+ setNodeVersion("v24.11.0");
const result = await testClaudeAcpEnvironment({
adapterType: "claude_local",
@@ -833,7 +833,7 @@ describe("claude_local ACP lane", () => {
});
it("falls back to the CLI lane for a runner-less sandbox even when the ACP command is set", async () => {
- setNodeVersion("v22.13.0");
+ setNodeVersion("v24.11.0");
await expect(
resolveClaudeExecutionEngineForRun({
config: { agentCommand: "claude-agent-acp" },
diff --git a/packages/adapters/claude-local/src/server/acp.ts b/packages/adapters/claude-local/src/server/acp.ts
index cb18a87eb2..eb8cea89de 100644
--- a/packages/adapters/claude-local/src/server/acp.ts
+++ b/packages/adapters/claude-local/src/server/acp.ts
@@ -53,7 +53,7 @@ import { SANDBOX_INSTALL_COMMAND } from "../index.js";
const moduleDir = path.dirname(fileURLToPath(import.meta.url));
const packageRootDir = path.resolve(moduleDir, "../..");
-const MIN_ACP_NODE_VERSION = "22.12.0";
+const MIN_ACP_NODE_VERSION = "24.11.0";
export type ClaudeExecutionEngine = "cli" | "acp";
diff --git a/packages/adapters/codex-local/package.json b/packages/adapters/codex-local/package.json
index 5f51cdba2a..4de01144b4 100644
--- a/packages/adapters/codex-local/package.json
+++ b/packages/adapters/codex-local/package.json
@@ -58,7 +58,10 @@
"picocolors": "^1.1.1"
},
"devDependencies": {
- "@types/node": "^22.20.1",
+ "@types/node": "^24.0.0",
"typescript": "^5.7.3"
+ },
+ "engines": {
+ "node": ">=24.11.0"
}
}
diff --git a/packages/adapters/codex-local/src/index.ts b/packages/adapters/codex-local/src/index.ts
index c29388a457..9870a34657 100644
--- a/packages/adapters/codex-local/src/index.ts
+++ b/packages/adapters/codex-local/src/index.ts
@@ -129,5 +129,5 @@ Notes:
- Some model/tool combinations reject certain effort levels (for example minimal with web search enabled).
- Fast mode is supported on GPT-5.6 (sol/terra/luna), GPT-5.5, GPT-5.4 and manual model IDs. When enabled for those models, Paperclip applies \`service_tier="fast"\` and \`features.fast_mode=true\`.
- When Paperclip realizes a workspace/runtime for a run, it injects PAPERCLIP_WORKSPACE_* and PAPERCLIP_RUNTIME_* env vars for agent-side tooling.
-- Codex ACP is the preferred auto lane when Node >=22.13.0 and the Codex ACP server are available. It reuses shared ACP prompt/runtime guidance, selected skill materialization into CODEX_HOME/skills, model/reasoning/fast-mode session config, and existing quota-window reporting. Auto selection falls back to CLI when ACP prerequisites are unavailable; explicit engine="acp" fails loudly.
+- Codex ACP is the preferred auto lane when Node >=24.11.0 and the Codex ACP server are available. It reuses shared ACP prompt/runtime guidance, selected skill materialization into CODEX_HOME/skills, model/reasoning/fast-mode session config, and existing quota-window reporting. Auto selection falls back to CLI when ACP prerequisites are unavailable; explicit engine="acp" fails loudly.
`;
diff --git a/packages/adapters/codex-local/src/server/acp.test.ts b/packages/adapters/codex-local/src/server/acp.test.ts
index 4829b14f08..37f547324f 100644
--- a/packages/adapters/codex-local/src/server/acp.test.ts
+++ b/packages/adapters/codex-local/src/server/acp.test.ts
@@ -276,7 +276,7 @@ describe("codex_local ACP lane", () => {
const commandPath = path.join(root, "bin", "codex-acp");
await fs.mkdir(path.dirname(commandPath), { recursive: true });
await fs.writeFile(commandPath, "#!/usr/bin/env sh\n", "utf8");
- setNodeVersion("v22.13.0");
+ setNodeVersion("v24.11.0");
expect(resolveCodexExecutionEngine({})).toEqual({ engine: "acp", explicit: false });
await expect(
@@ -296,7 +296,7 @@ describe("codex_local ACP lane", () => {
explicit: true,
});
- setNodeVersion("v22.12.0");
+ setNodeVersion("v24.10.0");
await expect(
resolveCodexExecutionEngineForRun({
config: { agentCommand: commandPath },
@@ -375,7 +375,7 @@ describe("codex_local ACP lane", () => {
});
it("uses ACP for bridged sandbox auto runs when the ACP command is configured as a shell command", async () => {
- setNodeVersion("v22.13.0");
+ setNodeVersion("v24.11.0");
await expect(
resolveCodexExecutionEngineForRun({
config: { agentCommand: "codex-acp" },
@@ -401,7 +401,7 @@ describe("codex_local ACP lane", () => {
});
it("falls back to the CLI lane for one-shot sandbox auto runs", async () => {
- setNodeVersion("v22.13.0");
+ setNodeVersion("v24.11.0");
await expect(
resolveCodexExecutionEngineForRun({
config: {},
@@ -420,7 +420,7 @@ describe("codex_local ACP lane", () => {
});
it("falls back to the CLI lane for non-sandbox remote auto runs", async () => {
- setNodeVersion("v22.13.0");
+ setNodeVersion("v24.11.0");
await expect(
resolveCodexExecutionEngineForRun({
config: {},
@@ -479,9 +479,9 @@ describe("codex_local ACP lane", () => {
});
it("checks the Node version required by the ACPX runtime", () => {
- setNodeVersion("v22.12.0");
+ setNodeVersion("v24.10.0");
expect(nodeVersionMeetsCodexAcpMinimum()).toBe(false);
- setNodeVersion("v22.13.0");
+ setNodeVersion("v24.11.0");
expect(nodeVersionMeetsCodexAcpMinimum()).toBe(true);
});
@@ -490,7 +490,7 @@ describe("codex_local ACP lane", () => {
const commandPath = path.join(root, "bin", "codex-acp");
await fs.mkdir(path.dirname(commandPath), { recursive: true });
await fs.writeFile(commandPath, "#!/usr/bin/env sh\n", "utf8");
- setNodeVersion("v22.13.0");
+ setNodeVersion("v24.11.0");
const result = await testCodexAcpEnvironment({
adapterType: "codex_local",
@@ -543,7 +543,7 @@ describe("codex_local ACP lane", () => {
await fs.writeFile(commandPath, "#!/usr/bin/env sh\n", "utf8");
await fs.mkdir(sharedCodexHome, { recursive: true });
await fs.writeFile(path.join(sharedCodexHome, "auth.json"), '{"OPENAI_API_KEY":"sk-shared"}', "utf8");
- setNodeVersion("v22.13.0");
+ setNodeVersion("v24.11.0");
process.env.CODEX_HOME = sharedCodexHome;
delete process.env.OPENAI_API_KEY;
@@ -591,7 +591,7 @@ describe("codex_local ACP lane", () => {
await fs.mkdir(path.dirname(commandPath), { recursive: true });
await fs.writeFile(commandPath, "#!/usr/bin/env sh\n", "utf8");
await fs.mkdir(sharedCodexHome, { recursive: true });
- setNodeVersion("v22.13.0");
+ setNodeVersion("v24.11.0");
process.env.CODEX_HOME = sharedCodexHome;
delete process.env.OPENAI_API_KEY;
@@ -632,7 +632,7 @@ describe("codex_local ACP lane", () => {
"codex-home",
);
await fs.mkdir(sharedCodexHome, { recursive: true });
- setNodeVersion("v22.13.0");
+ setNodeVersion("v24.11.0");
process.env.CODEX_HOME = sharedCodexHome;
delete process.env.OPENAI_API_KEY;
@@ -1136,7 +1136,7 @@ describe("codex_local ACP lane", () => {
});
it("falls back to the CLI lane for a runner-less sandbox even when the ACP command is set", async () => {
- setNodeVersion("v22.13.0");
+ setNodeVersion("v24.11.0");
// Isolate the missing bidirectional runner as the sole fallback cause:
// provide a valid ACP command and Node version so the only difference from
// the runner-backed ACP case is the absent `runner`.
diff --git a/packages/adapters/codex-local/src/server/acp.ts b/packages/adapters/codex-local/src/server/acp.ts
index be08fe9a12..30b265ee65 100644
--- a/packages/adapters/codex-local/src/server/acp.ts
+++ b/packages/adapters/codex-local/src/server/acp.ts
@@ -48,7 +48,7 @@ import { ADAPTER_AUTH_MISSING_CHECK_CODE } from "./auth-check.js";
const moduleDir = path.dirname(fileURLToPath(import.meta.url));
const packageRootDir = path.resolve(moduleDir, "../..");
-const MIN_ACP_NODE_VERSION = "22.13.0";
+const MIN_ACP_NODE_VERSION = "24.11.0";
export type CodexExecutionEngine = "cli" | "acp";
diff --git a/packages/adapters/cursor-cloud/package.json b/packages/adapters/cursor-cloud/package.json
index 6082595064..24339e59f4 100644
--- a/packages/adapters/cursor-cloud/package.json
+++ b/packages/adapters/cursor-cloud/package.json
@@ -55,7 +55,10 @@
"picocolors": "^1.1.1"
},
"devDependencies": {
- "@types/node": "^22.20.1",
+ "@types/node": "^24.0.0",
"typescript": "^5.7.3"
+ },
+ "engines": {
+ "node": ">=24.11.0"
}
}
diff --git a/packages/adapters/cursor-local/package.json b/packages/adapters/cursor-local/package.json
index b54dd563c0..45f7cd846f 100644
--- a/packages/adapters/cursor-local/package.json
+++ b/packages/adapters/cursor-local/package.json
@@ -55,7 +55,10 @@
"picocolors": "^1.1.1"
},
"devDependencies": {
- "@types/node": "^22.20.1",
+ "@types/node": "^24.0.0",
"typescript": "^5.7.3"
+ },
+ "engines": {
+ "node": ">=24.11.0"
}
}
diff --git a/packages/adapters/gemini-local/package.json b/packages/adapters/gemini-local/package.json
index 2a4c1211b6..787e5ba00c 100644
--- a/packages/adapters/gemini-local/package.json
+++ b/packages/adapters/gemini-local/package.json
@@ -55,7 +55,10 @@
"picocolors": "^1.1.1"
},
"devDependencies": {
- "@types/node": "^22.20.1",
+ "@types/node": "^24.0.0",
"typescript": "^5.7.3"
+ },
+ "engines": {
+ "node": ">=24.11.0"
}
}
diff --git a/packages/adapters/gemini-local/src/index.ts b/packages/adapters/gemini-local/src/index.ts
index d4f99331be..e895c66102 100644
--- a/packages/adapters/gemini-local/src/index.ts
+++ b/packages/adapters/gemini-local/src/index.ts
@@ -68,7 +68,7 @@ Operational fields:
- graceSec (number, optional): SIGTERM grace period in seconds
Notes:
-- Gemini ACP is the preferred auto lane when Node >=20 and the local Gemini CLI command is available. It runs Gemini CLI's native \`gemini --acp\` server through Paperclip's shared ACP engine, including selected skill links, Paperclip runtime prompt/env guidance, model config, and persistent ACP session state. Auto selection falls back to the CLI lane when ACP prerequisites are unavailable; explicit engine="acp" fails loudly.
+- Gemini ACP is the preferred auto lane when Node >=24.11.0 and the local Gemini CLI command is available. It runs Gemini CLI's native \`gemini --acp\` server through Paperclip's shared ACP engine, including selected skill links, Paperclip runtime prompt/env guidance, model config, and persistent ACP session state. Auto selection falls back to the CLI lane when ACP prerequisites are unavailable; explicit engine="acp" fails loudly.
- Runs use --prompt for non-interactive execution, not stdin.
- The adapter sets a headless-safe terminal/browser environment for Gemini CLI child processes so unattended runs do not wait on browser auth or 256-color terminal prompts.
- Sessions resume with --resume when stored session cwd matches the current cwd.
diff --git a/packages/adapters/gemini-local/src/server/acp.test.ts b/packages/adapters/gemini-local/src/server/acp.test.ts
index ae6ff3780b..27ce706f82 100644
--- a/packages/adapters/gemini-local/src/server/acp.test.ts
+++ b/packages/adapters/gemini-local/src/server/acp.test.ts
@@ -244,9 +244,9 @@ describe("gemini_local ACP lane", () => {
});
it("checks the Node version required by the Gemini ACP runtime", () => {
- setNodeVersion("v19.9.0");
+ setNodeVersion("v24.10.0");
expect(nodeVersionMeetsGeminiAcpMinimum()).toBe(false);
- setNodeVersion("v20.0.0");
+ setNodeVersion("v24.11.0");
expect(nodeVersionMeetsGeminiAcpMinimum()).toBe(true);
});
@@ -255,7 +255,7 @@ describe("gemini_local ACP lane", () => {
const commandPath = path.join(root, "bin", "gemini");
await fs.mkdir(path.dirname(commandPath), { recursive: true });
await fs.writeFile(commandPath, "#!/usr/bin/env sh\n", "utf8");
- setNodeVersion("v20.0.0");
+ setNodeVersion("v24.11.0");
expect(resolveGeminiExecutionEngine({})).toEqual({ engine: "acp", explicit: false });
await expect(
@@ -275,7 +275,7 @@ describe("gemini_local ACP lane", () => {
explicit: true,
});
- setNodeVersion("v19.9.0");
+ setNodeVersion("v24.10.0");
await expect(
resolveGeminiExecutionEngineForRun({
config: { command: commandPath },
@@ -295,7 +295,7 @@ describe("gemini_local ACP lane", () => {
});
it("falls back to the CLI lane for non-sandbox remote auto runs", async () => {
- setNodeVersion("v20.0.0");
+ setNodeVersion("v24.11.0");
await expect(
resolveGeminiExecutionEngineForRun({
config: { agentCommand: "gemini --acp" },
@@ -323,7 +323,7 @@ describe("gemini_local ACP lane", () => {
});
it("falls back to the CLI lane for one-shot sandbox auto runs", async () => {
- setNodeVersion("v20.0.0");
+ setNodeVersion("v24.11.0");
await expect(
resolveGeminiExecutionEngineForRun({
config: {},
@@ -342,7 +342,7 @@ describe("gemini_local ACP lane", () => {
});
it("uses ACP for bridged sandbox auto runs when the ACP command is configured as a shell command", async () => {
- setNodeVersion("v20.0.0");
+ setNodeVersion("v24.11.0");
await expect(
resolveGeminiExecutionEngineForRun({
config: { agentCommand: "gemini --acp" },
@@ -681,7 +681,7 @@ describe("gemini_local ACP lane", () => {
});
it("falls back to the CLI lane for a runner-less sandbox even when the ACP command is set", async () => {
- setNodeVersion("v22.13.0");
+ setNodeVersion("v24.11.0");
await expect(
resolveGeminiExecutionEngineForRun({
config: { agentCommand: "gemini --acp" },
@@ -706,7 +706,7 @@ describe("gemini_local ACP lane", () => {
await fs.writeFile(path.join(bin, "gemini"), "#!/usr/bin/env sh\n", "utf8");
process.env.PATH = `${bin}${path.delimiter}${process.env.PATH ?? ""}`;
process.env.GEMINI_API_KEY = "test-key";
- setNodeVersion("v20.0.0");
+ setNodeVersion("v24.11.0");
const result = await testGeminiAcpEnvironment({
adapterType: "gemini_local",
diff --git a/packages/adapters/gemini-local/src/server/acp.ts b/packages/adapters/gemini-local/src/server/acp.ts
index ca573626cd..f169b45801 100644
--- a/packages/adapters/gemini-local/src/server/acp.ts
+++ b/packages/adapters/gemini-local/src/server/acp.ts
@@ -35,7 +35,7 @@ import { DEFAULT_GEMINI_LOCAL_MODEL } from "../index.js";
const moduleDir = path.dirname(fileURLToPath(import.meta.url));
const packageRootDir = path.resolve(moduleDir, "../..");
-const MIN_ACP_NODE_VERSION = "20.0.0";
+const MIN_ACP_NODE_VERSION = "24.11.0";
export type GeminiExecutionEngine = "cli" | "acp";
diff --git a/packages/adapters/grok-local/package.json b/packages/adapters/grok-local/package.json
index fc3d6f4a71..70043ee0a2 100644
--- a/packages/adapters/grok-local/package.json
+++ b/packages/adapters/grok-local/package.json
@@ -54,7 +54,10 @@
"picocolors": "^1.1.1"
},
"devDependencies": {
- "@types/node": "^22.20.1",
+ "@types/node": "^24.0.0",
"typescript": "^5.7.3"
+ },
+ "engines": {
+ "node": ">=24.11.0"
}
}
diff --git a/packages/adapters/hermes-gateway/package.json b/packages/adapters/hermes-gateway/package.json
index 2353ec2d72..02c49f0e8b 100644
--- a/packages/adapters/hermes-gateway/package.json
+++ b/packages/adapters/hermes-gateway/package.json
@@ -71,11 +71,11 @@
"@paperclipai/hermes-paperclip-adapter": "workspace:*"
},
"devDependencies": {
- "@types/node": "^22.20.1",
+ "@types/node": "^24.0.0",
"typescript": "^5.7.3",
"vitest": "^4.1.10"
},
"engines": {
- "node": ">=20.0.0"
+ "node": ">=24.11.0"
}
}
diff --git a/packages/adapters/hermes/package.json b/packages/adapters/hermes/package.json
index cf3b322d56..e43f9e97e1 100644
--- a/packages/adapters/hermes/package.json
+++ b/packages/adapters/hermes/package.json
@@ -103,11 +103,11 @@
"picocolors": "^1.1.1"
},
"devDependencies": {
- "@types/node": "^22.20.1",
+ "@types/node": "^24.0.0",
"typescript": "^5.7.3",
"vitest": "^4.1.10"
},
"engines": {
- "node": ">=20.0.0"
+ "node": ">=24.11.0"
}
}
diff --git a/packages/adapters/openclaw-gateway/package.json b/packages/adapters/openclaw-gateway/package.json
index 9a34601ef4..e8d110c52a 100644
--- a/packages/adapters/openclaw-gateway/package.json
+++ b/packages/adapters/openclaw-gateway/package.json
@@ -55,8 +55,11 @@
"ws": "^8.21.3"
},
"devDependencies": {
- "@types/node": "^22.20.1",
+ "@types/node": "^24.0.0",
"@types/ws": "^8.18.1",
"typescript": "^5.7.3"
+ },
+ "engines": {
+ "node": ">=24.11.0"
}
}
diff --git a/packages/adapters/opencode-local/package.json b/packages/adapters/opencode-local/package.json
index cd956b0e4a..42a4687880 100644
--- a/packages/adapters/opencode-local/package.json
+++ b/packages/adapters/opencode-local/package.json
@@ -55,7 +55,10 @@
"picocolors": "^1.1.1"
},
"devDependencies": {
- "@types/node": "^22.20.1",
+ "@types/node": "^24.0.0",
"typescript": "^5.7.3"
+ },
+ "engines": {
+ "node": ">=24.11.0"
}
}
diff --git a/packages/adapters/pi-local/package.json b/packages/adapters/pi-local/package.json
index 24ef2e78c0..b9d258bf45 100644
--- a/packages/adapters/pi-local/package.json
+++ b/packages/adapters/pi-local/package.json
@@ -54,7 +54,10 @@
"picocolors": "^1.1.1"
},
"devDependencies": {
- "@types/node": "^22.20.1",
+ "@types/node": "^24.0.0",
"typescript": "^5.7.3"
+ },
+ "engines": {
+ "node": ">=24.11.0"
}
}
diff --git a/packages/db/package.json b/packages/db/package.json
index 1b2bbdb200..50e6d11a8b 100644
--- a/packages/db/package.json
+++ b/packages/db/package.json
@@ -53,10 +53,13 @@
"postgres": "^3.4.9"
},
"devDependencies": {
- "@types/node": "^22.20.1",
+ "@types/node": "^24.0.0",
"drizzle-kit": "^0.31.10",
"tsx": "^4.23.12",
"typescript": "^5.7.3",
"vitest": "^4.1.10"
+ },
+ "engines": {
+ "node": ">=24.11.0"
}
}
diff --git a/packages/google-sheets-mcp-server/package.json b/packages/google-sheets-mcp-server/package.json
index a1747d88de..366b147178 100644
--- a/packages/google-sheets-mcp-server/package.json
+++ b/packages/google-sheets-mcp-server/package.json
@@ -50,8 +50,11 @@
"zod": "^4.4.3"
},
"devDependencies": {
- "@types/node": "^22.20.1",
+ "@types/node": "^24.0.0",
"typescript": "^5.7.3",
"vitest": "^4.1.10"
+ },
+ "engines": {
+ "node": ">=24.11.0"
}
}
diff --git a/packages/kv-demo-mcp-server/README.md b/packages/kv-demo-mcp-server/README.md
index c9f59217ea..7934bd25b4 100644
--- a/packages/kv-demo-mcp-server/README.md
+++ b/packages/kv-demo-mcp-server/README.md
@@ -40,7 +40,7 @@ Or run the source directly during development:
```sh
cd packages/kv-demo-mcp-server
-node --experimental-strip-types src/main.ts # Node 22+/24
+node --experimental-strip-types src/main.ts # Node 24+
```
By default it listens on `http://127.0.0.1:8848` and prints three URLs to
diff --git a/packages/kv-demo-mcp-server/package.json b/packages/kv-demo-mcp-server/package.json
index f7491de8f2..bc2701878d 100644
--- a/packages/kv-demo-mcp-server/package.json
+++ b/packages/kv-demo-mcp-server/package.json
@@ -48,8 +48,11 @@
"zod": "^4.4.3"
},
"devDependencies": {
- "@types/node": "^22.20.1",
+ "@types/node": "^24.0.0",
"typescript": "^5.7.3",
"vitest": "^4.1.10"
+ },
+ "engines": {
+ "node": ">=24.11.0"
}
}
diff --git a/packages/mcp-server/package.json b/packages/mcp-server/package.json
index 03d8b88d61..371ac8e5e5 100644
--- a/packages/mcp-server/package.json
+++ b/packages/mcp-server/package.json
@@ -48,8 +48,11 @@
"zod": "^4.4.3"
},
"devDependencies": {
- "@types/node": "^22.20.1",
+ "@types/node": "^24.0.0",
"typescript": "^5.7.3",
"vitest": "^4.1.10"
+ },
+ "engines": {
+ "node": ">=24.11.0"
}
}
diff --git a/packages/plugins/create-paperclip-plugin/package.json b/packages/plugins/create-paperclip-plugin/package.json
index 321b5d68f6..d83b8cd4da 100644
--- a/packages/plugins/create-paperclip-plugin/package.json
+++ b/packages/plugins/create-paperclip-plugin/package.json
@@ -45,7 +45,10 @@
"@paperclipai/plugin-sdk": "workspace:*"
},
"devDependencies": {
- "@types/node": "^22.20.1",
+ "@types/node": "^24.0.0",
"typescript": "^5.7.3"
+ },
+ "engines": {
+ "node": ">=24.11.0"
}
}
diff --git a/packages/plugins/create-paperclip-plugin/src/index.ts b/packages/plugins/create-paperclip-plugin/src/index.ts
index 365bf71d82..2598e828e8 100644
--- a/packages/plugins/create-paperclip-plugin/src/index.ts
+++ b/packages/plugins/create-paperclip-plugin/src/index.ts
@@ -196,7 +196,7 @@ export function scaffoldPluginProject(options: ScaffoldPluginOptions): string {
"@paperclipai/plugin-sdk": sdkDependency,
"@rollup/plugin-node-resolve": "^16.0.1",
"@rollup/plugin-typescript": "^12.1.2",
- "@types/node": "^24.6.0",
+ "@types/node": "^24.0.0",
"@types/react": "^19.0.8",
esbuild: "^0.27.3",
rollup: "^4.38.0",
diff --git a/packages/plugins/examples/plugin-authoring-smoke-example/package.json b/packages/plugins/examples/plugin-authoring-smoke-example/package.json
index 1bded50d71..0813d363cf 100644
--- a/packages/plugins/examples/plugin-authoring-smoke-example/package.json
+++ b/packages/plugins/examples/plugin-authoring-smoke-example/package.json
@@ -31,7 +31,7 @@
"devDependencies": {
"@rollup/plugin-node-resolve": "^16.0.1",
"@rollup/plugin-typescript": "^12.1.2",
- "@types/node": "^22.20.1",
+ "@types/node": "^24.0.0",
"@types/react": "^19.2.18",
"esbuild": "^0.28.1",
"rollup": "^4.62.4",
@@ -41,5 +41,8 @@
},
"peerDependencies": {
"react": ">=18"
+ },
+ "engines": {
+ "node": ">=24.11.0"
}
}
diff --git a/packages/plugins/examples/plugin-file-browser-example/package.json b/packages/plugins/examples/plugin-file-browser-example/package.json
index 3ae6ee0314..82da74ca01 100644
--- a/packages/plugins/examples/plugin-file-browser-example/package.json
+++ b/packages/plugins/examples/plugin-file-browser-example/package.json
@@ -28,7 +28,7 @@
"codemirror": "^6.0.1"
},
"devDependencies": {
- "@types/node": "^22.20.1",
+ "@types/node": "^24.0.0",
"@types/react": "^19.2.18",
"@types/react-dom": "^19.2.4",
"esbuild": "^0.28.1",
@@ -38,5 +38,8 @@
},
"peerDependencies": {
"react": ">=18"
+ },
+ "engines": {
+ "node": ">=24.11.0"
}
}
diff --git a/packages/plugins/examples/plugin-hello-world-example/package.json b/packages/plugins/examples/plugin-hello-world-example/package.json
index b16b2f4a42..b2b8a43686 100644
--- a/packages/plugins/examples/plugin-hello-world-example/package.json
+++ b/packages/plugins/examples/plugin-hello-world-example/package.json
@@ -22,7 +22,7 @@
"@paperclipai/plugin-sdk": "workspace:*"
},
"devDependencies": {
- "@types/node": "^22.20.1",
+ "@types/node": "^24.0.0",
"@types/react": "^19.2.18",
"@types/react-dom": "^19.2.4",
"react": "^19.2.8",
@@ -31,5 +31,8 @@
},
"peerDependencies": {
"react": ">=18"
+ },
+ "engines": {
+ "node": ">=24.11.0"
}
}
diff --git a/packages/plugins/examples/plugin-kitchen-sink-example/package.json b/packages/plugins/examples/plugin-kitchen-sink-example/package.json
index 76a8774346..da4c65acc6 100644
--- a/packages/plugins/examples/plugin-kitchen-sink-example/package.json
+++ b/packages/plugins/examples/plugin-kitchen-sink-example/package.json
@@ -24,7 +24,7 @@
},
"devDependencies": {
"esbuild": "^0.28.1",
- "@types/node": "^22.20.1",
+ "@types/node": "^24.0.0",
"@types/react": "^19.2.18",
"@types/react-dom": "^19.2.4",
"react": "^19.2.8",
@@ -33,5 +33,8 @@
},
"peerDependencies": {
"react": ">=18"
+ },
+ "engines": {
+ "node": ">=24.11.0"
}
}
diff --git a/packages/plugins/examples/plugin-orchestration-smoke-example/package.json b/packages/plugins/examples/plugin-orchestration-smoke-example/package.json
index a9dbcbc629..b8f161725c 100644
--- a/packages/plugins/examples/plugin-orchestration-smoke-example/package.json
+++ b/packages/plugins/examples/plugin-orchestration-smoke-example/package.json
@@ -32,7 +32,7 @@
"@paperclipai/shared": "workspace:*",
"@rollup/plugin-node-resolve": "^16.0.1",
"@rollup/plugin-typescript": "^12.1.2",
- "@types/node": "^22.20.1",
+ "@types/node": "^24.0.0",
"@types/react": "^19.2.18",
"esbuild": "^0.28.1",
"rollup": "^4.62.4",
@@ -42,5 +42,8 @@
},
"peerDependencies": {
"react": ">=18"
+ },
+ "engines": {
+ "node": ">=24.11.0"
}
}
diff --git a/packages/plugins/paperclip-plugin-fake-sandbox/package.json b/packages/plugins/paperclip-plugin-fake-sandbox/package.json
index 82dbf39ab5..be4d6ea04c 100644
--- a/packages/plugins/paperclip-plugin-fake-sandbox/package.json
+++ b/packages/plugins/paperclip-plugin-fake-sandbox/package.json
@@ -22,8 +22,11 @@
"@paperclipai/plugin-sdk": "workspace:*"
},
"devDependencies": {
- "@types/node": "^22.20.1",
+ "@types/node": "^24.0.0",
"typescript": "^5.7.3",
"vitest": "^4.1.10"
+ },
+ "engines": {
+ "node": ">=24.11.0"
}
}
diff --git a/packages/plugins/plugin-llm-wiki/package.json b/packages/plugins/plugin-llm-wiki/package.json
index 0f917b6075..1ab7c00999 100644
--- a/packages/plugins/plugin-llm-wiki/package.json
+++ b/packages/plugins/plugin-llm-wiki/package.json
@@ -39,7 +39,7 @@
"@paperclipai/plugin-sdk": "workspace:*",
"@rollup/plugin-node-resolve": "^16.0.1",
"@rollup/plugin-typescript": "^12.1.2",
- "@types/node": "^22.20.1",
+ "@types/node": "^24.0.0",
"@types/react": "^19.2.18",
"@types/react-dom": "^19.2.4",
"esbuild": "^0.28.1",
@@ -51,5 +51,8 @@
},
"peerDependencies": {
"react": ">=18"
+ },
+ "engines": {
+ "node": ">=24.11.0"
}
}
diff --git a/packages/plugins/plugin-workspace-diff/package.json b/packages/plugins/plugin-workspace-diff/package.json
index 85ca2d162a..2631eba12a 100644
--- a/packages/plugins/plugin-workspace-diff/package.json
+++ b/packages/plugins/plugin-workspace-diff/package.json
@@ -55,7 +55,7 @@
"@pierre/diffs": "^1.2.11"
},
"devDependencies": {
- "@types/node": "^22.20.1",
+ "@types/node": "^24.0.0",
"@types/react": "^19.2.18",
"@types/react-dom": "^19.2.4",
"esbuild": "^0.28.1",
@@ -67,5 +67,8 @@
"peerDependencies": {
"react": ">=18",
"react-dom": ">=18"
+ },
+ "engines": {
+ "node": ">=24.11.0"
}
}
diff --git a/packages/plugins/sandbox-providers/cloudflare/bridge-template/package.json b/packages/plugins/sandbox-providers/cloudflare/bridge-template/package.json
index ca3d38e609..b51ee6ea68 100644
--- a/packages/plugins/sandbox-providers/cloudflare/bridge-template/package.json
+++ b/packages/plugins/sandbox-providers/cloudflare/bridge-template/package.json
@@ -17,5 +17,8 @@
"typescript": "^5.7.3",
"vitest": "^4.1.8",
"wrangler": "^4.15.0"
+ },
+ "engines": {
+ "node": ">=24.11.0"
}
}
diff --git a/packages/plugins/sandbox-providers/cloudflare/package.json b/packages/plugins/sandbox-providers/cloudflare/package.json
index d959d6ff49..4956e4c302 100644
--- a/packages/plugins/sandbox-providers/cloudflare/package.json
+++ b/packages/plugins/sandbox-providers/cloudflare/package.json
@@ -51,8 +51,11 @@
"postpack": "if [ -f package.dev.json ]; then mv package.dev.json package.json; fi"
},
"devDependencies": {
- "@types/node": "^24.6.0",
+ "@types/node": "^24.0.0",
"typescript": "^5.7.3",
"vitest": "^4.1.8"
+ },
+ "engines": {
+ "node": ">=24.11.0"
}
}
diff --git a/packages/plugins/sandbox-providers/daytona/package.json b/packages/plugins/sandbox-providers/daytona/package.json
index 25c45046e9..ddc5acd934 100644
--- a/packages/plugins/sandbox-providers/daytona/package.json
+++ b/packages/plugins/sandbox-providers/daytona/package.json
@@ -53,8 +53,11 @@
"@daytonaio/sdk": "0.203.0"
},
"devDependencies": {
- "@types/node": "^24.6.0",
+ "@types/node": "^24.0.0",
"typescript": "^5.7.3",
"vitest": "^4.1.8"
+ },
+ "engines": {
+ "node": ">=24.11.0"
}
}
diff --git a/packages/plugins/sandbox-providers/e2b/package.json b/packages/plugins/sandbox-providers/e2b/package.json
index 0e180ae7bb..a99ff86204 100644
--- a/packages/plugins/sandbox-providers/e2b/package.json
+++ b/packages/plugins/sandbox-providers/e2b/package.json
@@ -53,8 +53,11 @@
"e2b": "^2.19.0"
},
"devDependencies": {
- "@types/node": "^24.6.0",
+ "@types/node": "^24.0.0",
"typescript": "^5.7.3",
"vitest": "^4.1.8"
+ },
+ "engines": {
+ "node": ">=24.11.0"
}
}
diff --git a/packages/plugins/sandbox-providers/exe-dev/README.md b/packages/plugins/sandbox-providers/exe-dev/README.md
index 9e4a6b119b..52d420c25d 100644
--- a/packages/plugins/sandbox-providers/exe-dev/README.md
+++ b/packages/plugins/sandbox-providers/exe-dev/README.md
@@ -37,7 +37,7 @@ Operational notes:
- Reusable leases keep the VM alive between runs. exe.dev does not expose a documented "stop and later resume" command in the public CLI docs, so `reuseLease: true` means "retain the VM" rather than "suspend it."
- The provisioning path uses `https://exe.dev/exec`, which exe.dev documents as a command-style HTTPS API with a 30-second request timeout. Typical `new` calls are expected to fit inside that limit; command execution itself does not use `/exec`.
- Probes still create and delete a real exe.dev VM through `/exec`, and so do the `new`/`rm` calls inside the normal acquire/release lifecycle. Treat all of those as real provisioning cost, not just probes.
-- exe.dev runs `--setup-script` as the unprivileged `exedev` user, not as root. That user has passwordless `sudo`, so any system-level steps in a custom `setupScript` must invoke `sudo` explicitly (for example `sudo apt-get install -y …`). When you omit `setupScript`, the plugin supplies a default that installs Node 20 via the official nodesource script — Paperclip's sandbox callback bridge is a Node program, so the VM needs `node` on `PATH` before the bridge can launch.
+- exe.dev runs `--setup-script` as the unprivileged `exedev` user, not as root. That user has passwordless `sudo`, so any system-level steps in a custom `setupScript` must invoke `sudo` explicitly (for example `sudo apt-get install -y …`). When you omit `setupScript`, the plugin supplies a default that installs Node 24 via the official nodesource script — Paperclip's sandbox callback bridge is a Node program, so the VM needs `node` on `PATH` before the bridge can launch.
## Local development
diff --git a/packages/plugins/sandbox-providers/exe-dev/package.json b/packages/plugins/sandbox-providers/exe-dev/package.json
index 23dfd89f82..e62831f4d7 100644
--- a/packages/plugins/sandbox-providers/exe-dev/package.json
+++ b/packages/plugins/sandbox-providers/exe-dev/package.json
@@ -50,8 +50,11 @@
"postpack": "if [ -f package.dev.json ]; then mv package.dev.json package.json; fi"
},
"devDependencies": {
- "@types/node": "^24.6.0",
+ "@types/node": "^24.0.0",
"typescript": "^5.7.3",
"vitest": "^4.1.8"
+ },
+ "engines": {
+ "node": ">=24.11.0"
}
}
diff --git a/packages/plugins/sandbox-providers/exe-dev/src/plugin.test.ts b/packages/plugins/sandbox-providers/exe-dev/src/plugin.test.ts
index 0432bfecf8..16618bf507 100644
--- a/packages/plugins/sandbox-providers/exe-dev/src/plugin.test.ts
+++ b/packages/plugins/sandbox-providers/exe-dev/src/plugin.test.ts
@@ -372,7 +372,9 @@ describe("exe.dev sandbox provider plugin", () => {
const body = String(fetchMock.mock.calls[0]?.[1]?.body ?? "");
expect(body).toContain("--setup-script=");
- expect(body).toContain("nodesource.com/setup_20.x");
+ expect(body).toContain("process.versions.node");
+ expect(body).toContain("v[0]>24||(v[0]===24&&v[1]>=11)");
+ expect(body).toContain("nodesource.com/setup_24.x");
expect(body).toContain("sudo apt-get install -y nodejs");
});
@@ -425,7 +427,7 @@ describe("exe.dev sandbox provider plugin", () => {
await acquirePromise?.catch((error: Error) => {
// Operator did not supply a setupScript, so the visible default install
// is not a secret and stays in the error for debuggability.
- expect(error.message).toContain("nodesource.com/setup_20.x");
+ expect(error.message).toContain("nodesource.com/setup_24.x");
expect(error.message).not.toContain("[REDACTED]");
});
});
diff --git a/packages/plugins/sandbox-providers/exe-dev/src/plugin.ts b/packages/plugins/sandbox-providers/exe-dev/src/plugin.ts
index 22a8e3a317..a586b665f4 100644
--- a/packages/plugins/sandbox-providers/exe-dev/src/plugin.ts
+++ b/packages/plugins/sandbox-providers/exe-dev/src/plugin.ts
@@ -74,11 +74,13 @@ const UUID_SECRET_REF_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-
// exe.dev's `--setup-script` runs at VM init as the unprivileged `exedev` user, which
// has passwordless sudo. The Paperclip sandbox callback bridge is a Node script, so
// every Paperclip workload on this provider needs node on PATH before the bridge can
-// start. When the operator hasn't supplied their own setup script, install Node 20 via
+// start. When the operator hasn't supplied their own setup script, install Node 24 via
// nodesource so the VM comes up ready for Paperclip out of the box.
const DEFAULT_SETUP_SCRIPT =
- "command -v node >/dev/null 2>&1 || " +
- "(curl -fsSL https://deb.nodesource.com/setup_20.x | sudo -E bash - && " +
+ "(command -v node >/dev/null 2>&1 && " +
+ "node -e 'const v=process.versions.node.split(\".\").map(Number);" +
+ "process.exit(v[0]>24||(v[0]===24&&v[1]>=11)?0:1)' >/dev/null 2>&1) || " +
+ "(curl -fsSL https://deb.nodesource.com/setup_24.x | sudo -E bash - && " +
"sudo apt-get install -y nodejs)";
class ExeDevApiError extends Error {
diff --git a/packages/plugins/sandbox-providers/kubernetes/package.json b/packages/plugins/sandbox-providers/kubernetes/package.json
index 74b2f1f5e4..d1238aa707 100644
--- a/packages/plugins/sandbox-providers/kubernetes/package.json
+++ b/packages/plugins/sandbox-providers/kubernetes/package.json
@@ -52,8 +52,11 @@
"zod": "^3.24.2"
},
"devDependencies": {
- "@types/node": "^24.6.0",
+ "@types/node": "^24.0.0",
"typescript": "^5.7.3",
"vitest": "^3.2.4"
+ },
+ "engines": {
+ "node": ">=24.11.0"
}
}
diff --git a/packages/plugins/sandbox-providers/modal/README.md b/packages/plugins/sandbox-providers/modal/README.md
index 054b5d7137..073c80dcb7 100644
--- a/packages/plugins/sandbox-providers/modal/README.md
+++ b/packages/plugins/sandbox-providers/modal/README.md
@@ -16,9 +16,7 @@ The host plugin installer runs `npm install` into the managed plugin directory,
## Runtime support note
-Modal's official JS SDK README pins support to **Node 22 or later**. Paperclip's repo baseline is currently `node >= 20`; empirically `modal@0.7.4` imports and operates against the Modal API under Node 20, so the plugin runs there today, but the vendor support contract is Node 22+. The plugin logs a startup warning when it detects Node `< 22`. Operators who can pin their Paperclip runtime to Node 22+ should do so; treat Node-20 usage as best-effort until the host bumps its baseline.
-
-The empirical Node 20 compatibility check is recorded in [PAPA-352](/PAPA/issues/PAPA-352).
+Paperclip and this plugin require **Node 24 or later**. The plugin logs a startup warning when it detects an older host runtime.
## Configuration
@@ -27,7 +25,7 @@ Configure Modal from `Instance Settings -> Environments`, not from the plugin's
| Field | Required | Description |
| --- | --- | --- |
| `appName` | yes | Modal App name. The plugin calls `modal.apps.fromName(appName, { createIfMissing: true })`, so the App is created on first acquire if it does not already exist. |
-| `image` | yes | Container image passed to `modal.images.fromRegistry()`, e.g. `python:3.13` or `node:20`. |
+| `image` | yes | Container image passed to `modal.images.fromRegistry()`, e.g. `python:3.13` or `node:24`. |
| `tokenId` / `tokenSecret` | yes | Modal auth tokens. Both must be provided together. Paperclip stores pasted values as company secrets. The plugin worker runs in a child process that does not inherit host env vars, so `MODAL_TOKEN_ID` / `MODAL_TOKEN_SECRET` set on the Paperclip server are **not** read by the plugin — provide the tokens in this form. |
| `environment` | no | Optional Modal environment name. Falls back to the SDK profile default. |
| `workdir` | no | Remote working directory inside the sandbox. Defaults to `/workspace/paperclip`. |
diff --git a/packages/plugins/sandbox-providers/modal/package.json b/packages/plugins/sandbox-providers/modal/package.json
index 8d6b0bf92e..6e0376e7df 100644
--- a/packages/plugins/sandbox-providers/modal/package.json
+++ b/packages/plugins/sandbox-providers/modal/package.json
@@ -53,8 +53,11 @@
"modal": "^0.7.4"
},
"devDependencies": {
- "@types/node": "^24.6.0",
+ "@types/node": "^24.0.0",
"typescript": "^5.7.3",
"vitest": "^4.1.8"
+ },
+ "engines": {
+ "node": ">=24.11.0"
}
}
diff --git a/packages/plugins/sandbox-providers/modal/src/manifest.ts b/packages/plugins/sandbox-providers/modal/src/manifest.ts
index 9a0ef1bc60..9f13ea7abf 100644
--- a/packages/plugins/sandbox-providers/modal/src/manifest.ts
+++ b/packages/plugins/sandbox-providers/modal/src/manifest.ts
@@ -36,8 +36,8 @@ const manifest: PaperclipPluginManifestV1 = {
image: {
type: "string",
description:
- "Container image reference passed to `modal.images.fromRegistry()`, e.g. `python:3.13` or `node:22`. The default `node:22` satisfies the sandbox runtime contract (node, sh, and tar on PATH).",
- default: "node:22",
+ "Container image reference passed to `modal.images.fromRegistry()`, e.g. `python:3.13` or `node:24`. The default `node:24` satisfies the sandbox runtime contract (node, sh, and tar on PATH).",
+ default: "node:24",
},
tokenId: {
type: "string",
diff --git a/packages/plugins/sandbox-providers/modal/src/plugin.test.ts b/packages/plugins/sandbox-providers/modal/src/plugin.test.ts
index 7fabac055e..a6739b4dde 100644
--- a/packages/plugins/sandbox-providers/modal/src/plugin.test.ts
+++ b/packages/plugins/sandbox-providers/modal/src/plugin.test.ts
@@ -717,7 +717,7 @@ describe("modal manifest form defaults", () => {
it("pre-fills the required app name and image so the form works out of the box", () => {
expect(properties.appName?.default).toBe("paperclip");
- expect(properties.image?.default).toBe("node:22");
+ expect(properties.image?.default).toBe("node:24");
});
it("declares no default on secret-ref fields, which would be persisted as a company secret", () => {
diff --git a/packages/plugins/sandbox-providers/modal/src/plugin.ts b/packages/plugins/sandbox-providers/modal/src/plugin.ts
index 6e45d9581c..29147dc35a 100644
--- a/packages/plugins/sandbox-providers/modal/src/plugin.ts
+++ b/packages/plugins/sandbox-providers/modal/src/plugin.ts
@@ -318,8 +318,8 @@ async function getSandboxOrNull(
function warnIfUnsupportedNode(logger: { warn: (msg: string) => void } | undefined): void {
const major = Number.parseInt(process.versions.node.split(".")[0] ?? "0", 10);
- if (Number.isFinite(major) && major < 22) {
- const message = `Modal sandbox provider is running on Node ${process.versions.node}; Modal officially supports Node 22+. The plugin will attempt to operate but vendor support is not guaranteed below Node 22.`;
+ if (Number.isFinite(major) && major < 24) {
+ const message = `Modal sandbox provider is running on Node ${process.versions.node}; Paperclip requires Node 24+. Upgrade the host runtime before using this plugin.`;
logger?.warn(message);
}
}
diff --git a/packages/plugins/sandbox-providers/novita/package.json b/packages/plugins/sandbox-providers/novita/package.json
index 1bb6ad9d1b..b09d1b6529 100644
--- a/packages/plugins/sandbox-providers/novita/package.json
+++ b/packages/plugins/sandbox-providers/novita/package.json
@@ -54,8 +54,11 @@
"novita-sandbox": "^1.0.3-b2"
},
"devDependencies": {
- "@types/node": "^24.6.0",
+ "@types/node": "^24.0.0",
"typescript": "^5.7.3",
"vitest": "^4.1.8"
+ },
+ "engines": {
+ "node": ">=24.11.0"
}
}
diff --git a/packages/plugins/sdk/package.json b/packages/plugins/sdk/package.json
index 60824d27f0..3b216adea1 100644
--- a/packages/plugins/sdk/package.json
+++ b/packages/plugins/sdk/package.json
@@ -112,7 +112,7 @@
"zod": "^4.4.3"
},
"devDependencies": {
- "@types/node": "^22.20.1",
+ "@types/node": "^24.0.0",
"@types/react": "^19.2.18",
"typescript": "^5.7.3"
},
@@ -123,5 +123,8 @@
"react": {
"optional": true
}
+ },
+ "engines": {
+ "node": ">=24.11.0"
}
}
diff --git a/packages/plugins/sdk/src/bundlers.ts b/packages/plugins/sdk/src/bundlers.ts
index e761489c09..883bf3d9f2 100644
--- a/packages/plugins/sdk/src/bundlers.ts
+++ b/packages/plugins/sdk/src/bundlers.ts
@@ -80,7 +80,7 @@ export function createPluginBundlerPresets(input: PluginBundlerPresetInput = {})
bundle: true,
format: "esm",
platform: "node",
- target: "node20",
+ target: "node24",
sourcemap,
minify,
external: ["react", "react-dom"],
@@ -92,7 +92,7 @@ export function createPluginBundlerPresets(input: PluginBundlerPresetInput = {})
bundle: true,
format: "esm",
platform: "node",
- target: "node20",
+ target: "node24",
sourcemap,
external: ["@paperclipai/plugin-sdk"],
};
diff --git a/packages/shared/package.json b/packages/shared/package.json
index 715aa35b7b..aeb8fe1235 100644
--- a/packages/shared/package.json
+++ b/packages/shared/package.json
@@ -48,7 +48,10 @@
"zod": "^4.4.3"
},
"devDependencies": {
- "@types/node": "^22.20.1",
+ "@types/node": "^24.0.0",
"typescript": "^5.7.3"
+ },
+ "engines": {
+ "node": ">=24.11.0"
}
}
diff --git a/packages/shared/src/node-version.ts b/packages/shared/src/node-version.ts
new file mode 100644
index 0000000000..0cd7ed5bbe
--- /dev/null
+++ b/packages/shared/src/node-version.ts
@@ -0,0 +1,49 @@
+export const MINIMUM_NODE_VERSION = "24.11.0";
+export const NODE_VERSION_INSTALL_GUIDE_URL =
+ "https://github.com/paperclipai/paperclip/blob/master/doc/INSTALLING.md#recommended-install";
+const NODE_VERSION_WARNING_EMITTED = Symbol.for("@paperclipai/node-version-warning-emitted");
+
+function parseVersion(version: string): [number, number, number] | null {
+ const match = /^v?(\d+)\.(\d+)\.(\d+)/.exec(version.trim());
+ if (!match) return null;
+ return [Number(match[1]), Number(match[2]), Number(match[3])];
+}
+
+export function isSupportedNodeVersion(version: string): boolean {
+ const current = parseVersion(version);
+ const minimum = parseVersion(MINIMUM_NODE_VERSION);
+ if (!current || !minimum) return false;
+
+ for (let index = 0; index < current.length; index += 1) {
+ if (current[index] > minimum[index]) return true;
+ if (current[index] < minimum[index]) return false;
+ }
+ return true;
+}
+
+export function formatNodeVersionWarning(version: string): string | null {
+ if (isSupportedNodeVersion(version)) return null;
+ const currentVersion = version.trim() || "unknown";
+ return [
+ `[paperclip] warning: Node.js ${currentVersion} is unsupported. Paperclip requires Node.js ${MINIMUM_NODE_VERSION} or newer.`,
+ "Upgrade Node.js with your version manager, or follow the recommended downloaded install.sh workflow:",
+ ` ${NODE_VERSION_INSTALL_GUIDE_URL}`,
+ "The piped install.sh form cannot upgrade an unsupported Node.js runtime.",
+ "Restart Paperclip after upgrading.",
+ ].join("\n");
+}
+
+export function warnIfUnsupportedNodeVersion(
+ version: string,
+ warn: (message: string) => void,
+): boolean {
+ const warning = formatNodeVersionWarning(version);
+ if (!warning) return false;
+
+ const warningState = globalThis as unknown as Record;
+ if (warningState[NODE_VERSION_WARNING_EMITTED]) return false;
+
+ warningState[NODE_VERSION_WARNING_EMITTED] = true;
+ warn(warning);
+ return true;
+}
diff --git a/packages/skills-catalog/package.json b/packages/skills-catalog/package.json
index 1e84534fa9..af5ad7c28a 100644
--- a/packages/skills-catalog/package.json
+++ b/packages/skills-catalog/package.json
@@ -52,6 +52,9 @@
"@paperclipai/shared": "workspace:*"
},
"devDependencies": {
- "@types/node": "^22.20.1"
+ "@types/node": "^24.0.0"
+ },
+ "engines": {
+ "node": ">=24.11.0"
}
}
diff --git a/packages/tailscale-https-broker/package.json b/packages/tailscale-https-broker/package.json
index 715a7c6fa9..c20893b5d2 100644
--- a/packages/tailscale-https-broker/package.json
+++ b/packages/tailscale-https-broker/package.json
@@ -18,8 +18,11 @@
"test": "node scripts/build-native.mjs && vitest run"
},
"devDependencies": {
- "@types/node": "^22.20.1",
+ "@types/node": "^24.0.0",
"typescript": "^5.7.3",
"vitest": "^4.1.10"
+ },
+ "engines": {
+ "node": ">=24.11.0"
}
}
diff --git a/packages/teams-catalog/package.json b/packages/teams-catalog/package.json
index 1b9a7d936b..7933556b09 100644
--- a/packages/teams-catalog/package.json
+++ b/packages/teams-catalog/package.json
@@ -47,6 +47,9 @@
"validate": "node ../../cli/node_modules/tsx/dist/cli.mjs scripts/validate-catalog.ts"
},
"devDependencies": {
- "@types/node": "^22.20.1"
+ "@types/node": "^24.0.0"
+ },
+ "engines": {
+ "node": ">=24.11.0"
}
}
diff --git a/scripts/check-node-version-policy.mjs b/scripts/check-node-version-policy.mjs
new file mode 100644
index 0000000000..cceb9c7519
--- /dev/null
+++ b/scripts/check-node-version-policy.mjs
@@ -0,0 +1,85 @@
+import fs from "node:fs";
+import path from "node:path";
+import { fileURLToPath } from "node:url";
+
+const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
+const expectedEngine = ">=24.11.0";
+const expectedTypes = "^24.0.0";
+const failures = [];
+const skippedDirectories = new Set([".git", ".paperclip", "coverage", "data", "dist", "node_modules"]);
+
+function relative(filePath) {
+ return path.relative(repoRoot, filePath) || ".";
+}
+
+function walk(directory, visit) {
+ for (const entry of fs.readdirSync(directory, { withFileTypes: true })) {
+ if (entry.isDirectory() && skippedDirectories.has(entry.name)) continue;
+ const entryPath = path.join(directory, entry.name);
+ if (entry.isDirectory()) walk(entryPath, visit);
+ else if (entry.isFile()) visit(entryPath);
+ }
+}
+
+walk(repoRoot, (filePath) => {
+ if (path.basename(filePath) !== "package.json") return;
+ const manifest = JSON.parse(fs.readFileSync(filePath, "utf8"));
+ for (const section of ["dependencies", "devDependencies", "optionalDependencies", "peerDependencies"]) {
+ const specifier = manifest[section]?.["@types/node"];
+ if (specifier && specifier !== expectedTypes) {
+ failures.push(`${relative(filePath)}: ${section}.@types/node must be ${expectedTypes}, found ${specifier}`);
+ }
+ }
+ if (manifest.engines?.node !== expectedEngine) {
+ failures.push(`${relative(filePath)}: engines.node must be ${expectedEngine}, found ${manifest.engines?.node ?? "missing"}`);
+ }
+});
+
+const workflowRoot = path.join(repoRoot, ".github", "workflows");
+walk(workflowRoot, (filePath) => {
+ if (!/\.ya?ml$/.test(filePath)) return;
+ const source = fs.readFileSync(filePath, "utf8");
+ for (const match of source.matchAll(/node-version:\s*["']?([^\s"'#]+)/g)) {
+ if (match[1] !== "24") failures.push(`${relative(filePath)}: node-version must be 24, found ${match[1]}`);
+ }
+});
+
+walk(repoRoot, (filePath) => {
+ if (!path.basename(filePath).startsWith("Dockerfile")) return;
+ const source = fs.readFileSync(filePath, "utf8");
+ for (const match of source.matchAll(/^\s*FROM\s+node:([^\s]+)/gm)) {
+ if (!match[1].startsWith("24-")) failures.push(`${relative(filePath)}: Node base image must use the Node 24 major, found node:${match[1]}`);
+ }
+ for (const match of source.matchAll(/^\s*ARG\s+NODE_(?:MAJOR|VERSION)=([^\s]+)/gm)) {
+ if (match[1] !== "24") failures.push(`${relative(filePath)}: Node build argument must be 24, found ${match[1]}`);
+ }
+});
+
+const requiredSourceFragments = [
+ [".nvmrc", "24"],
+ ["packages/shared/src/node-version.ts", 'MINIMUM_NODE_VERSION = "24.11.0"'],
+ ["scripts/install.sh", "MIN_NODE_VERSION=\"${MIN_NODE_MAJOR}.${MIN_NODE_MINOR}.${MIN_NODE_PATCH}\""],
+ ["scripts/install.sh", "MIN_NODE_MAJOR=24"],
+ ["scripts/install.sh", "MIN_NODE_MINOR=11"],
+ ["scripts/install.sh", "DEFAULT_NODE_MAJOR=24"],
+ ["docker/agent-runtime/Dockerfile.base", "ARG NODE_VERSION=24"],
+ ["packages/adapter-utils/src/sandbox-install-command.ts", "NODE_VERSION=\"v24.11.0\""],
+ ["packages/plugins/sandbox-providers/exe-dev/src/plugin.ts", "nodesource.com/setup_24.x"],
+ ["packages/plugins/sandbox-providers/modal/src/manifest.ts", 'default: "node:24"'],
+ ["cli/esbuild.config.mjs", 'target: "node24"'],
+ ["packages/plugins/sdk/src/bundlers.ts", 'target: "node24"'],
+ ["scripts/generate-npm-package-json.mjs", `engines: { node: "${expectedEngine}" }`],
+];
+
+for (const [filePath, fragment] of requiredSourceFragments) {
+ const source = fs.readFileSync(path.join(repoRoot, filePath), "utf8");
+ if (!source.includes(fragment)) failures.push(`${filePath}: missing Node 24 policy fragment ${JSON.stringify(fragment)}`);
+}
+
+if (failures.length > 0) {
+ console.error("Node version policy check failed:");
+ for (const failure of failures) console.error(`- ${failure}`);
+ process.exit(1);
+}
+
+console.log("Node version policy check passed (Node >=24.11.0, @types/node 24.x).");
diff --git a/scripts/e2e-install-lifecycle.sh b/scripts/e2e-install-lifecycle.sh
index 7e1719cc99..653b4cbc9d 100755
--- a/scripts/e2e-install-lifecycle.sh
+++ b/scripts/e2e-install-lifecycle.sh
@@ -6,7 +6,7 @@
# -> update --check -> update --rollback -> reinstall (payload reuse)
# -> bad-ref failure hygiene -> service lifecycle -> uninstall (data preserved)
#
-# Machine requirements: bash, curl, tar, node >= 20 (with corepack), npm.
+# Machine requirements: bash, curl, tar, node >= 24.11 (with corepack), npm.
# The machine's $HOME must not already contain a managed install.
#
# Env knobs:
diff --git a/scripts/generate-npm-package-json.mjs b/scripts/generate-npm-package-json.mjs
index 310d6e1277..a972dc3e94 100644
--- a/scripts/generate-npm-package-json.mjs
+++ b/scripts/generate-npm-package-json.mjs
@@ -110,7 +110,7 @@ const publishPkg = {
homepage: cliPkg.homepage,
bugs: cliPkg.bugs,
files: cliPkg.files,
- engines: { node: ">=20" },
+ engines: { node: ">=24.11.0" },
dependencies: sortedDeps,
};
diff --git a/scripts/install.sh b/scripts/install.sh
index 572f2f73c8..7e6ea05fb4 100755
--- a/scripts/install.sh
+++ b/scripts/install.sh
@@ -1,15 +1,18 @@
#!/usr/bin/env bash
set -euo pipefail
-MIN_NODE_MAJOR=20
-DEFAULT_NODE_MAJOR=22
+MIN_NODE_MAJOR=24
+MIN_NODE_MINOR=11
+MIN_NODE_PATCH=0
+MIN_NODE_VERSION="${MIN_NODE_MAJOR}.${MIN_NODE_MINOR}.${MIN_NODE_PATCH}"
+DEFAULT_NODE_MAJOR=24
PAPERCLIP_PACKAGE="paperclipai"
PUBLIC_NPM_REGISTRY="https://registry.npmjs.org"
HOMEBREW_INSTALL_COMMIT="99e13e96cbbdc1ac1ac09c0a40b450bf219ef3aa"
HOMEBREW_INSTALL_SHA256="99287f194a8b3c9e6b0203a11a5fa54518be57209343e6bb954dec4635796d9d"
NODESOURCE_DISTRIBUTIONS_COMMIT="9b431d8ae0f10df272598585855c6eca6c0e1bd2"
-NODESOURCE_DEB_SHA256="575583bbac2fccc0b5edd0dbc03e222d9f9dc8d724da996d22754d6411104fd1"
-NODESOURCE_RPM_SHA256="b0ed2b9b66002e7ee802e8777cf3a92b25f1ecc0129812dc6f59a43a536810cc"
+NODESOURCE_DEB_SHA256="6e3d580f5bd7ccf2aa1e8df8d35c60d78e873c3ff8beb282c9bebd914904ad72"
+NODESOURCE_RPM_SHA256="5550ad302050f887377a0451e720b800466573ebc83392fea80924393dba642b"
CANARY=0
VERSION=""
@@ -190,19 +193,19 @@ esac
log "Detected $OS_NAME/$ARCH_NAME"
-node_major() {
- local version
+has_supported_node() {
+ local version major minor patch
+ command -v node >/dev/null 2>&1 || return 1
version="$(node --version 2>/dev/null || true)"
version="${version#v}"
- printf '%s' "${version%%.*}"
-}
-
-has_supported_node() {
- local major
- command -v node >/dev/null 2>&1 || return 1
- major="$(node_major)"
- [[ "$major" =~ ^[0-9]+$ ]] || return 1
- [ "$major" -ge "$MIN_NODE_MAJOR" ] || return 1
+ IFS=. read -r major minor patch <<< "$version"
+ patch="${patch%%-*}"
+ [[ "$major" =~ ^[0-9]+$ && "$minor" =~ ^[0-9]+$ && "$patch" =~ ^[0-9]+$ ]] || return 1
+ if [ "$major" -lt "$MIN_NODE_MAJOR" ] ||
+ { [ "$major" -eq "$MIN_NODE_MAJOR" ] && [ "$minor" -lt "$MIN_NODE_MINOR" ]; } ||
+ { [ "$major" -eq "$MIN_NODE_MAJOR" ] && [ "$minor" -eq "$MIN_NODE_MINOR" ] && [ "$patch" -lt "$MIN_NODE_PATCH" ]; }; then
+ return 1
+ fi
command -v npm >/dev/null 2>&1 || return 1
command -v npx >/dev/null 2>&1 || return 1
}
@@ -339,7 +342,7 @@ if has_supported_node; then
log "Using Node.js $(node --version)"
else
if command -v node >/dev/null 2>&1; then
- log "Node.js $(node --version 2>/dev/null || printf unknown) is too old; Node.js >= $MIN_NODE_MAJOR is required"
+ log "Node.js $(node --version 2>/dev/null || printf unknown) is too old; Node.js >= $MIN_NODE_VERSION is required"
else
log "Node.js was not found"
fi
@@ -353,7 +356,7 @@ else
else
install_node_linux
fi
- has_supported_node || fail "Node.js installation finished, but Node.js >= $MIN_NODE_MAJOR with npm/npx is not available"
+ has_supported_node || fail "Node.js installation finished, but Node.js >= $MIN_NODE_VERSION with npm/npx is not available"
log "Installed Node.js $(node --version)"
fi
diff --git a/scripts/test-install-sh-docker.sh b/scripts/test-install-sh-docker.sh
index 741c138cf9..c1aaef2952 100755
--- a/scripts/test-install-sh-docker.sh
+++ b/scripts/test-install-sh-docker.sh
@@ -36,7 +36,7 @@ run_with_node() {
-v "$RESULTS_DIR:/results" \
-e "PAPERCLIP_INSTALL_TEST_LOG=/results/$name.args" \
-e PATH="/paperclip-scripts/install-sh-fixtures:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin" \
- node:22-bookworm-slim \
+ node:24-bookworm-slim \
"$@"
}
@@ -84,7 +84,7 @@ docker run --rm \
-e npm_config_registry=http://attacker-registry.invalid \
-e PAPERCLIP_INSTALL_TEST_LOG=/results/hostile.args \
-e PATH="/paperclip-scripts/install-sh-fixtures:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin" \
- node:22-bookworm-slim \
+ node:24-bookworm-slim \
bash /paperclip-scripts/install.sh --no-prompt --no-onboard
assert_line "$RESULTS_DIR/hostile.args" "--registry=https://registry.npmjs.org"
assert_line "$RESULTS_DIR/hostile.args" "NPM_CONFIG_REGISTRY=https://registry.npmjs.org"
@@ -140,7 +140,7 @@ docker run --rm \
-e PAPERCLIP_INSTALL_NO_ONBOARD=1 \
-e PAPERCLIP_INSTALL_NO_PROMPT=1 \
-e PATH="/paperclip-scripts/install-sh-fixtures:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin" \
- node:22-bookworm-slim \
+ node:24-bookworm-slim \
bash /paperclip-scripts/install.sh
assert_line "$RESULTS_DIR/env.args" "paperclipai@2026.722.0"
assert_line "$RESULTS_DIR/env.args" "--version"
@@ -161,8 +161,8 @@ assert_line "$RESULTS_DIR/no-node.args" "paperclipai@latest"
node_version="$(cat "$RESULTS_DIR/no-node.args.node")"
node_major="${node_version#v}"
node_major="${node_major%%.*}"
-[ "$node_major" -ge 20 ] || {
- printf 'Expected Node >= 20, got %s\n' "$node_version" >&2
+[ "$node_major" -ge 24 ] || {
+ printf 'Expected Node >= 24, got %s\n' "$node_version" >&2
exit 1
}
diff --git a/server/package.json b/server/package.json
index a3fc20d0e8..7e52e3a1b6 100644
--- a/server/package.json
+++ b/server/package.json
@@ -87,7 +87,7 @@
"@types/express-serve-static-core": "^5.1.3",
"@types/jsdom": "^30.0.0",
"@types/multer": "^2.2.0",
- "@types/node": "^22.20.1",
+ "@types/node": "^24.0.0",
"@types/sharp": "^0.32.0",
"@types/supertest": "^6.0.2",
"@types/ws": "^8.18.1",
@@ -97,5 +97,8 @@
"typescript": "^5.7.3",
"vite": "^6.4.3",
"vitest": "^4.1.10"
+ },
+ "engines": {
+ "node": ">=24.11.0"
}
}
diff --git a/server/src/__tests__/adapter-routes.test.ts b/server/src/__tests__/adapter-routes.test.ts
index 7765859679..92b0322161 100644
--- a/server/src/__tests__/adapter-routes.test.ts
+++ b/server/src/__tests__/adapter-routes.test.ts
@@ -167,7 +167,7 @@ describe("adapter routes", () => {
agentId: "codex",
skillsMode: "ephemeral",
prerequisites: {
- nodeRange: ">=22.13.0",
+ nodeRange: ">=24.11.0",
packages: ["@agentclientprotocol/codex-acp"],
},
});
@@ -203,7 +203,7 @@ describe("adapter routes", () => {
agentId: "gemini",
skillsMode: "ephemeral",
prerequisites: {
- nodeRange: ">=20.0.0",
+ nodeRange: ">=24.11.0",
packages: ["@google/gemini-cli"],
},
});
diff --git a/server/src/adapters/registry.ts b/server/src/adapters/registry.ts
index f71352dedb..88fccf066d 100644
--- a/server/src/adapters/registry.ts
+++ b/server/src/adapters/registry.ts
@@ -243,7 +243,7 @@ const claudeLocalAdapter: ServerAdapterModule = {
agentId: "claude",
skillsMode: "ephemeral",
prerequisites: {
- nodeRange: ">=22.12.0",
+ nodeRange: ">=24.11.0",
packages: ["@agentclientprotocol/claude-agent-acp"],
},
},
@@ -317,7 +317,7 @@ const codexLocalAdapter: ServerAdapterModule = {
agentId: "codex",
skillsMode: "ephemeral",
prerequisites: {
- nodeRange: ">=22.13.0",
+ nodeRange: ">=24.11.0",
packages: ["@agentclientprotocol/codex-acp"],
},
},
@@ -382,7 +382,7 @@ const geminiLocalAdapter: ServerAdapterModule = {
agentId: "gemini",
skillsMode: "ephemeral",
prerequisites: {
- nodeRange: ">=20.0.0",
+ nodeRange: ">=24.11.0",
packages: ["@google/gemini-cli"],
},
},
diff --git a/server/src/index.ts b/server/src/index.ts
index cd4a49194f..6786407a65 100644
--- a/server/src/index.ts
+++ b/server/src/index.ts
@@ -11,6 +11,7 @@ import { createInterface } from "node:readline/promises";
import { stdin, stdout } from "node:process";
import { pathToFileURL } from "node:url";
import type { Request as ExpressRequest, RequestHandler } from "express";
+import { warnIfUnsupportedNodeVersion } from "@paperclipai/shared/node-version";
import { and, eq } from "drizzle-orm";
import {
createDb,
@@ -143,6 +144,8 @@ export interface StartedServer {
}
export async function startServer(): Promise {
+ warnIfUnsupportedNodeVersion(process.versions.node, (message) => logger.warn(message));
+
// Tracing must be active (or have failed and logged) before the first DB
// connection or the HTTP server exists — see instrumentation.ts.
await instrumentationReady;
diff --git a/ui/package.json b/ui/package.json
index f47efad0bc..6b626670ef 100644
--- a/ui/package.json
+++ b/ui/package.json
@@ -75,7 +75,7 @@
"@storybook/addon-docs": "10.5.8",
"@storybook/react-vite": "10.5.8",
"@tailwindcss/vite": "^4.3.3",
- "@types/node": "^22.20.1",
+ "@types/node": "^24.0.0",
"@types/react": "^19.2.18",
"@types/react-dom": "^19.2.4",
"@vitejs/plugin-react": "^5.2.0",
@@ -84,5 +84,8 @@
"typescript": "^5.7.3",
"vite": "^6.4.3",
"vitest": "^4.1.10"
+ },
+ "engines": {
+ "node": ">=24.11.0"
}
}