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" } }