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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 19 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
1 change: 1 addition & 0 deletions .node-version
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
22.22.0
2 changes: 1 addition & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion INSTALL_WITH_AGENT.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
<a href="https://github.com/SergioChan/token-meter/actions/workflows/ci.yml"><img alt="CI" src="https://github.com/SergioChan/token-meter/actions/workflows/ci.yml/badge.svg"></a>
<a href="LICENSE"><img alt="MIT License" src="https://img.shields.io/badge/license-MIT-2f6f52"></a>
<img alt="macOS" src="https://img.shields.io/badge/platform-macOS-555">
<img alt="Node.js 22.12+" src="https://img.shields.io/badge/node-%3E%3D22.12-43853d">
<img alt="Node.js 22.13+" src="https://img.shields.io/badge/node-%3E%3D22.13-43853d">
</p>

<p align="center">
Expand Down Expand Up @@ -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.

Expand Down
2 changes: 1 addition & 1 deletion docs/install-claude-desktop.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
2 changes: 1 addition & 1 deletion integrations/claude-desktop/scripts/doctor.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion integrations/claude-desktop/scripts/install.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
25 changes: 21 additions & 4 deletions integrations/claude-desktop/scripts/runtime-selection.sh
Original file line number Diff line number Diff line change
@@ -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
}

Expand Down Expand Up @@ -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"
Expand Down
10 changes: 7 additions & 3 deletions integrations/claude-desktop/src/overlay-bridge.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -5,19 +5,23 @@ 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)) {
return;
}
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,
Expand Down
2 changes: 1 addition & 1 deletion integrations/codex-desktop/src/cdp-client.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
2 changes: 1 addition & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@
"darwin"
],
"engines": {
"node": ">=22.12.0"
"node": ">=22.13.0"
},
"dependencies": {
"pg": "^8.16.3"
Expand Down
2 changes: 1 addition & 1 deletion scripts/build-v2-app.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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}"

Expand Down
8 changes: 5 additions & 3 deletions scripts/package-macos.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
68 changes: 63 additions & 5 deletions test/claude-runtime-selection.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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(
Expand All @@ -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,
);
});

Expand Down
8 changes: 5 additions & 3 deletions web/install.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading