diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b2c7824..d7172a2 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -17,13 +17,30 @@ jobs: - name: Check out repository uses: actions/checkout@v4 - - name: Set up Node.js + - name: Set up minimum supported Node.js uses: actions/setup-node@v4 with: - node-version: 22.12.0 + node-version: 22.13.0 + + - name: Verify required Node.js capabilities + run: node --no-warnings -e 'import("node:sqlite")' - name: Install dependencies run: npm ci + - name: Verify minimum supported runtime + run: >- + node --test + test/claude-runtime-selection.test.mjs + test/claude-overlay-bridge.test.mjs + test/codex-overlay-bridge.test.mjs + test/codex-snapshot-runtime.test.mjs + test/codex-thread-state.test.mjs + + - name: Set up packaged Node.js + uses: actions/setup-node@v4 + with: + node-version-file: .node-version + - name: Run checks and tests run: npm run ci diff --git a/.node-version b/.node-version new file mode 100644 index 0000000..85e5027 --- /dev/null +++ b/.node-version @@ -0,0 +1 @@ +22.22.0 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 78b38d6..873f09b 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -11,7 +11,7 @@ Thank you for helping make agent token usage easier to understand. ## Development setup -Token Widget development requires Git and Node.js 22.12 or newer. Building the native Claude companion also requires macOS 13 or newer and Xcode Command Line Tools with Swift. +Token Widget development requires Git and Node.js 22.13 or newer. Building the native Claude companion also requires macOS 13 or newer and Xcode Command Line Tools with Swift. ```bash git clone https://github.com/SergioChan/token-meter.git diff --git a/INSTALL_WITH_AGENT.md b/INSTALL_WITH_AGENT.md index ad7aacb..08f0da3 100644 --- a/INSTALL_WITH_AGENT.md +++ b/INSTALL_WITH_AGENT.md @@ -28,7 +28,7 @@ Finish only when each selected host is installed and its real runtime status has 3. Confirm the operating system is macOS and record the installed host paths: - Codex: `/Applications/ChatGPT.app` - Claude: `/Applications/Claude.app` -4. Run `./scripts/doctor-claude-meter-macos.sh` before a Claude installation. It checks Git, Node.js 22.12+, Xcode Command Line Tools, Swift, and code-signing tools. If a prerequisite is missing, ask before changing package-manager or Xcode state. Do not silently install or upgrade dependencies. +4. Run `./scripts/doctor-claude-meter-macos.sh` before a Claude installation. It checks Git, Node.js 22.13+ with `node:sqlite`, Xcode Command Line Tools, Swift, and code-signing tools. If a prerequisite is missing, ask before changing package-manager or Xcode state. Do not silently install or upgrade dependencies. 5. Run the repository verification suite: ```bash diff --git a/README.md b/README.md index c19efbd..e139d1c 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,7 @@ CI MIT License macOS - Node.js 22.12+ + Node.js 22.13+

@@ -205,7 +205,7 @@ Requirements: - macOS 13 or newer; - Git; -- Node.js 22.12 or newer; +- Node.js 22.13 or newer; - Xcode Command Line Tools and Swift only when building the Claude native companion; - the official Codex Desktop and/or Claude Desktop application. diff --git a/docs/install-claude-desktop.md b/docs/install-claude-desktop.md index 31b44b1..a36bd86 100644 --- a/docs/install-claude-desktop.md +++ b/docs/install-claude-desktop.md @@ -12,7 +12,7 @@ The signed and Apple-notarized DMG is the normal installation path. Source build - The official signed Claude Desktop application at `/Applications/Claude.app`. - Permission to enable **Token Widget** in System Settings > Privacy & Security > Accessibility. -The DMG contains its own compatible Node.js runtime. Building from source additionally requires Git, Node.js 22.12 or newer, and Xcode Command Line Tools with Swift. +The DMG contains its own compatible Node.js runtime. Building from source additionally requires Git, Node.js 22.13 or newer with `node:sqlite`, and Xcode Command Line Tools with Swift. ## Install the release DMG diff --git a/integrations/claude-desktop/scripts/doctor.sh b/integrations/claude-desktop/scripts/doctor.sh index 25c63ce..171e308 100755 --- a/integrations/claude-desktop/scripts/doctor.sh +++ b/integrations/claude-desktop/scripts/doctor.sh @@ -82,7 +82,7 @@ else printf 'Swift compiler: %s\n' "$SWIFT_COMPILER" printf 'Code-signing tools: %s\n' "$CODE_SIGNING_TOOLS" printf 'Git: %s\n' "$GIT_AVAILABLE" - printf 'Node.js 22.12 or newer: %s' "$NODE_COMPATIBLE" + printf 'Node.js 22.13 or newer with node:sqlite: %s' "$NODE_COMPATIBLE" [ -n "$RESOLVED_NODE" ] && printf ' (%s)' "$RESOLVED_NODE" printf '\nReady for source install: %s\n' "$READY" fi diff --git a/integrations/claude-desktop/scripts/install.sh b/integrations/claude-desktop/scripts/install.sh index 0a78116..876d341 100755 --- a/integrations/claude-desktop/scripts/install.sh +++ b/integrations/claude-desktop/scripts/install.sh @@ -33,7 +33,7 @@ Install the native Token Meter companion for Claude Code inside Claude Desktop. This installer does not quit, relaunch, modify, patch, or re-sign Claude.app. Options: - --node PATH Absolute Node.js 22.12+ executable path + --node PATH Absolute Node.js 22.13+ executable path --claude-app PATH Claude.app path (default: /Applications/Claude.app) --no-load Install files and LaunchAgent without loading it --no-prompt Do not request macOS Accessibility permission diff --git a/integrations/claude-desktop/scripts/runtime-selection.sh b/integrations/claude-desktop/scripts/runtime-selection.sh index 810de8e..d40842e 100755 --- a/integrations/claude-desktop/scripts/runtime-selection.sh +++ b/integrations/claude-desktop/scripts/runtime-selection.sh @@ -1,14 +1,31 @@ #!/bin/bash +token_meter_node_version_is_compatible() { + if [ "$#" -ne 1 ]; then return 2; fi + local version="${1#v}" + local major="${version%%.*}" + local remainder="${version#*.}" + [ "$remainder" != "$version" ] || return 1 + local minor="${remainder%%.*}" + case "$major" in ''|*[!0-9]*) return 1 ;; esac + case "$minor" in ''|*[!0-9]*) return 1 ;; esac + [ "$major" -gt 22 ] || { [ "$major" -eq 22 ] && [ "$minor" -ge 13 ]; } +} + token_meter_node_is_compatible() { if [ "$#" -ne 1 ]; then return 2; fi local candidate="$1" [ -n "$candidate" ] || return 1 [ "${candidate#/}" != "$candidate" ] || return 1 [ -x "$candidate" ] || return 1 + local version + version="$("$candidate" --version 2>/dev/null)" || return 1 + token_meter_node_version_is_compatible "$version" || return 1 "$candidate" -e ' - const [major, minor] = process.versions.node.split(".").map(Number); - process.exit(major > 22 || (major === 22 && minor >= 12) ? 0 : 1); + import("node:sqlite").then( + ({ DatabaseSync }) => process.exit(typeof DatabaseSync === "function" ? 0 : 1), + () => process.exit(1), + ); ' >/dev/null 2>&1 } @@ -53,14 +70,14 @@ token_meter_resolve_node() { printf '%s\n' "$explicit" return 0 fi - printf 'Node.js 22.12 or newer is required: %s\n' "$explicit" >&2 + printf 'Node.js 22.13 or newer with node:sqlite is required: %s\n' "$explicit" >&2 return 1 fi local selected selected="$(token_meter_find_compatible_node || true)" if [ -z "$selected" ]; then - printf 'Node.js 22.12 or newer was not found. Run the Claude source-install doctor for details.\n' >&2 + printf 'Node.js 22.13 or newer with node:sqlite was not found. Run the Claude source-install doctor for details.\n' >&2 return 1 fi printf '%s\n' "$selected" diff --git a/integrations/claude-desktop/src/overlay-bridge.mjs b/integrations/claude-desktop/src/overlay-bridge.mjs index db26114..90f3653 100644 --- a/integrations/claude-desktop/src/overlay-bridge.mjs +++ b/integrations/claude-desktop/src/overlay-bridge.mjs @@ -5,12 +5,13 @@ import readline from "node:readline"; import { once } from "node:events"; import { fileURLToPath } from "node:url"; import { ClaudeSnapshotRuntime } from "./snapshot-runtime.mjs"; -import { CodexSnapshotRuntime } from "../../codex-desktop/src/snapshot-runtime.mjs"; // The Codex host path reads state_5.sqlite through node:sqlite, whose // experimental warning would otherwise repeat into the LaunchAgent log on -// every process start. Replace Node's default warning printer with one that -// drops only that warning and prints everything else unchanged. +// every process start. Install the filter before dynamically loading that +// runtime; static dependencies are evaluated before this module body. Replace +// Node's default warning printer with one that drops only that warning and +// prints everything else unchanged. process.removeAllListeners("warning"); process.on("warning", (warning) => { if (warning.name === "ExperimentalWarning" && /SQLite/i.test(warning.message)) { @@ -18,6 +19,9 @@ process.on("warning", (warning) => { } process.stderr.write(`${warning.name}: ${warning.message}\n`); }); +const { CodexSnapshotRuntime } = await import( + "../../codex-desktop/src/snapshot-runtime.mjs", +); import { runCommunitySyncWorker } from "../../../src/core/community-sync.mjs"; import { loadOrCreateIdentity, diff --git a/integrations/codex-desktop/src/cdp-client.mjs b/integrations/codex-desktop/src/cdp-client.mjs index f627d0a..600f8f0 100644 --- a/integrations/codex-desktop/src/cdp-client.mjs +++ b/integrations/codex-desktop/src/cdp-client.mjs @@ -34,7 +34,7 @@ export class CdpClient { throw new Error("Refusing a non-loopback CDP WebSocket URL"); } if (typeof WebSocket !== "function") { - throw new Error("Token Meter requires Node.js 22.12 or newer for WebSocket support"); + throw new Error("Token Meter requires Node.js 22.13 or newer for WebSocket support"); } const socket = new WebSocket(webSocketUrl); diff --git a/package-lock.json b/package-lock.json index 707b33f..f8e7777 100644 --- a/package-lock.json +++ b/package-lock.json @@ -18,7 +18,7 @@ "pg-mem": "^3.0.5" }, "engines": { - "node": ">=22.12.0" + "node": ">=22.13.0" } }, "node_modules/call-bind": { diff --git a/package.json b/package.json index 22a9717..8858923 100644 --- a/package.json +++ b/package.json @@ -27,7 +27,7 @@ "darwin" ], "engines": { - "node": ">=22.12.0" + "node": ">=22.13.0" }, "dependencies": { "pg": "^8.16.3" diff --git a/scripts/build-v2-app.sh b/scripts/build-v2-app.sh index ca2afe4..da54fa9 100755 --- a/scripts/build-v2-app.sh +++ b/scripts/build-v2-app.sh @@ -9,7 +9,7 @@ set -euo pipefail ROOT="$(cd "$(dirname "$0")/.." && pwd -P)" VERSION="$(node -p "require('$ROOT/package.json').version")" DIST="$ROOT/dist" -NODE_VERSION="v22.22.0" +NODE_VERSION="v$(/bin/cat "$ROOT/.node-version")" NODE_TAR="$DIST/node-$NODE_VERSION-darwin-arm64.tar.gz" IDENTITY="${TOKEN_METER_CODESIGN_IDENTITY:?set TOKEN_METER_CODESIGN_IDENTITY}" diff --git a/scripts/package-macos.sh b/scripts/package-macos.sh index 933c1e1..3777c8c 100755 --- a/scripts/package-macos.sh +++ b/scripts/package-macos.sh @@ -38,10 +38,12 @@ xcode-select -p >/dev/null 2>&1 \ || fail "Xcode Command Line Tools are required." "Run: xcode-select --install then re-run this installer." if ! command -v node >/dev/null 2>&1; then - fail "Node.js 22.12+ is required." "Install with: brew install node (or https://nodejs.org) then re-run." + fail "Node.js 22.13+ is required." "Install with: brew install node (or https://nodejs.org) then re-run." fi -node -e 'const [maj, min] = process.versions.node.split(".").map(Number); process.exit(maj > 22 || (maj === 22 && min >= 12) ? 0 : 1)' \ - || fail "Node.js 22.12+ is required (found $(node --version))." "Upgrade with: brew upgrade node or via nvm." +node -e 'const [maj, min] = process.versions.node.split(".").map(Number); process.exit(maj > 22 || (maj === 22 && min >= 13) ? 0 : 1)' \ + || fail "Node.js 22.13+ is required (found $(node --version))." "Upgrade with: brew upgrade node or via nvm." +node -e 'import("node:sqlite")' >/dev/null 2>&1 \ + || fail "This Node.js build does not provide node:sqlite." "Install an official Node.js 22.13+ release, then re-run." echo "Prerequisites OK (Node $(node --version)). Building — this takes about a minute..." echo diff --git a/test/claude-runtime-selection.test.mjs b/test/claude-runtime-selection.test.mjs index ddcdd39..5e4f665 100644 --- a/test/claude-runtime-selection.test.mjs +++ b/test/claude-runtime-selection.test.mjs @@ -10,17 +10,54 @@ import test from "node:test"; const execFileAsync = promisify(execFile); const helper = "integrations/claude-desktop/scripts/runtime-selection.sh"; -async function fakeNode(file, compatible) { - await writeFile(file, `#!/bin/bash\nexit ${compatible ? 0 : 1}\n`); +async function fakeNode(file, version, sqliteAvailable = true) { + await writeFile( + file, + `#!/bin/bash +if [ "$1" = "--version" ]; then + printf '%s\\n' '${version}' + exit 0 +fi +if [ "$1" = "-e" ]; then + exit ${sqliteAvailable ? 0 : 1} +fi +exit 1 +`, + ); await chmod(file, 0o755); } +async function versionIsCompatible(version) { + try { + await execFileAsync("/bin/bash", [ + "-c", + 'source "$1"; token_meter_node_version_is_compatible "$2"', + "bash", + helper, + version, + ]); + return true; + } catch { + return false; + } +} + +test("Claude runtime policy starts at unflagged node:sqlite support", async () => { + assert.equal(await versionIsCompatible("v22.12.9"), false); + assert.equal(await versionIsCompatible("v22.13.0"), true); + assert.equal(await versionIsCompatible("v23.0.0"), true); + assert.equal(await versionIsCompatible("not-a-version"), false); +}); + test("Claude runtime selection skips an older Node and chooses a compatible candidate", async (context) => { const directory = await mkdtemp(path.join(os.tmpdir(), "token-meter-node-select-")); context.after(() => rm(directory, { recursive: true, force: true })); const oldNode = path.join(directory, "node-old"); const currentNode = path.join(directory, "node-current"); - await Promise.all([fakeNode(oldNode, false), fakeNode(currentNode, true)]); + await Promise.all([ + fakeNode(oldNode, "v22.12.9"), + fakeNode(currentNode, "v22.13.0"), + ]); const { stdout } = await execFileAsync( "/bin/bash", @@ -41,7 +78,10 @@ test("Claude runtime selection fails an explicitly selected incompatible Node", context.after(() => rm(directory, { recursive: true, force: true })); const oldNode = path.join(directory, "node-old"); const currentNode = path.join(directory, "node-current"); - await Promise.all([fakeNode(oldNode, false), fakeNode(currentNode, true)]); + await Promise.all([ + fakeNode(oldNode, "v22.12.9"), + fakeNode(currentNode, "v22.13.0"), + ]); await assert.rejects( execFileAsync( @@ -55,7 +95,25 @@ test("Claude runtime selection fails an explicitly selected incompatible Node", currentNode, ], ), - /Node\.js 22\.12 or newer is required/i, + /Node\.js 22\.13 or newer with node:sqlite is required/i, + ); +}); + +test("Claude runtime selection rejects a Node build without node:sqlite", async (context) => { + const directory = await mkdtemp(path.join(os.tmpdir(), "token-meter-node-sqlite-")); + context.after(() => rm(directory, { recursive: true, force: true })); + const nodeWithoutSqlite = path.join(directory, "node-without-sqlite"); + await fakeNode(nodeWithoutSqlite, "v22.13.0", false); + + await assert.rejects( + execFileAsync("/bin/bash", [ + "-c", + 'source "$1"; token_meter_resolve_node "$2"', + "bash", + helper, + nodeWithoutSqlite, + ]), + /node:sqlite is required/i, ); }); diff --git a/web/install.sh b/web/install.sh index bf76ee7..51586bb 100644 --- a/web/install.sh +++ b/web/install.sh @@ -15,11 +15,13 @@ sw_vers -productVersion | awk -F. '{ exit ($1 < 13) }' \ xcode-select -p >/dev/null 2>&1 \ || fail "Xcode Command Line Tools are required." "Run: xcode-select --install then re-run this installer." command -v node >/dev/null 2>&1 \ - || fail "Node.js 22.12+ is required." "Install with: brew install node (or https://nodejs.org) then re-run." + || fail "Node.js 22.13+ is required." "Install with: brew install node (or https://nodejs.org) then re-run." command -v git >/dev/null 2>&1 \ || fail "Git is required." "Install the Xcode Command Line Tools, then re-run this installer." -node -e 'const [maj, min] = process.versions.node.split(".").map(Number); process.exit(maj > 22 || (maj === 22 && min >= 12) ? 0 : 1)' \ - || fail "Node.js 22.12+ is required (found $(node --version))." "Upgrade with: brew upgrade node or via nvm." +node -e 'const [maj, min] = process.versions.node.split(".").map(Number); process.exit(maj > 22 || (maj === 22 && min >= 13) ? 0 : 1)' \ + || fail "Node.js 22.13+ is required (found $(node --version))." "Upgrade with: brew upgrade node or via nvm." +node -e 'import("node:sqlite")' >/dev/null 2>&1 \ + || fail "This Node.js build does not provide node:sqlite." "Install an official Node.js 22.13+ release, then re-run." TMP="$(mktemp -d)" trap 'rm -rf "$TMP"' EXIT