@tokenade/cli 0.8.11 → 0.8.13

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -108,7 +108,7 @@ When reporting, please include your OS, `tokenade --version`, your coding agent,
108
108
  | Command | What it does |
109
109
  |---|---|
110
110
  | `install` | Register Tokenade with your detected agent (hooks). Flags: `--only <agent>`, `--with-hook`, `--with-claude-md`, `--with-lsp-enforcement-hook`, `--with-brevity-skill`, `--dry-run`. |
111
- | `uninstall [aliases]` | Remove the integration. `uninstall aliases` removes only the shell proxy. `--dry-run` to preview. |
111
+ | `uninstall` | Remove the integration (restores wrapped MCP servers). `--dry-run` to preview. |
112
112
  | `login` | Activate via browser device-flow — no key to type. |
113
113
  | `activate <key>` | Link this machine to a tokenade.net license (free or paid) with a key. |
114
114
  | `upgrade` | Self-update in place (aliases: `self-update`). `--on`/`--off` toggles the 24h background updater. |
@@ -266,7 +266,7 @@ $ tokenade incompatibilities
266
266
  [redundant] rtk
267
267
  Output-filtering CLI proxy (filters/compresses bash output)
268
268
  → binary at ~/.cargo/bin/rtk
269
- fix: Tokenade subsumes rtk: `tokenade proxy <cmd>` does the same AND
269
+ fix: Tokenade subsumes rtk: `tokenade wrap '<cmd>'` does the same AND
270
270
  auto-detects 48 formats — usually with better savings.
271
271
 
272
272
  Run `tokenade install` to interactively migrate & fix the [redundant] /
package/bin/tokenade.js CHANGED
@@ -3,16 +3,35 @@
3
3
  const { spawnSync } = require("node:child_process");
4
4
  const path = require("node:path");
5
5
  const fs = require("node:fs");
6
+ const { platformPackageName, binaryName } = require("../platform.js");
6
7
 
7
- const bin = path.join(
8
- __dirname,
9
- "..",
10
- "vendor",
11
- process.platform === "win32" ? "tokenade.exe" : "tokenade",
12
- );
13
- if (!fs.existsSync(bin)) {
8
+ // Resolve the binary, first hit wins (see platform.js for the two delivery
9
+ // paths): (1) the matching per-platform optionalDependency from the registry,
10
+ // (2) ./vendor, where install.js's download fallback drops it.
11
+ function resolveBinary() {
12
+ const name = binaryName();
13
+ // 1. Platform sub-package. A skipped optional dep throws MODULE_NOT_FOUND;
14
+ // resolve its package.json so we don't depend on a "main" entry.
15
+ try {
16
+ const pkgJson = require.resolve(`${platformPackageName()}/package.json`);
17
+ const bin = path.join(path.dirname(pkgJson), name);
18
+ if (fs.existsSync(bin)) return bin;
19
+ } catch {
20
+ // optional dep absent — fall through to the vendored copy.
21
+ }
22
+ // 2. Downloaded fallback.
23
+ const vendored = path.join(__dirname, "..", "vendor", name);
24
+ if (fs.existsSync(vendored)) return vendored;
25
+ return null;
26
+ }
27
+
28
+ const bin = resolveBinary();
29
+ if (!bin) {
14
30
  console.error(
15
- "tokenade: binary not found. Re-run `npm install -g @tokenade/cli` (the postinstall downloads it).",
31
+ "tokenade: binary not found. Re-run `npm install -g @tokenade/cli` " +
32
+ "(the platform binary ships as an optional dependency; a postinstall " +
33
+ "download is the fallback). If you installed with --no-optional or " +
34
+ "--ignore-scripts, reinstall without them.",
16
35
  );
17
36
  process.exit(1);
18
37
  }
package/install.js CHANGED
@@ -27,6 +27,11 @@ const http = require("node:http");
27
27
  const https = require("node:https");
28
28
  const crypto = require("node:crypto");
29
29
  const { URL } = require("node:url");
30
+ const {
31
+ rustTarget,
32
+ platformPackageName,
33
+ binaryName,
34
+ } = require("./platform.js");
30
35
 
31
36
  const DOWNLOADS_BASE =
32
37
  process.env.TOKENADE_DOWNLOADS_BASE || "https://downloads.tokenade.net";
@@ -38,52 +43,6 @@ const VENDOR = path.join(__dirname, "vendor");
38
43
  const REQUEST_TIMEOUT_MS = Number(process.env.TOKENADE_HTTP_TIMEOUT_MS) || 30000;
39
44
  const MAX_ATTEMPTS = 4;
40
45
 
41
- // Is this Linux userland musl (Alpine, Void-musl, …) rather than glibc?
42
- // A glibc node reports its runtime glibc version in the process report header;
43
- // a musl node does not. We fall back to looking for the musl loader on disk.
44
- // When genuinely unsure we return false (glibc) — that preserves the historical
45
- // x86_64-gnu mapping for the overwhelming glibc majority, so a detection miss is
46
- // never worse than the status quo.
47
- function isMuslLinux() {
48
- if (process.platform !== "linux") return false;
49
- try {
50
- const header = process.report && process.report.getReport().header;
51
- if (header && typeof header.glibcVersionRuntime === "string") return false;
52
- if (header && "glibcVersionRuntime" in header) {
53
- // key present but not a string ⇒ no glibc runtime ⇒ musl
54
- return true;
55
- }
56
- } catch {
57
- // fall through to the on-disk probe
58
- }
59
- try {
60
- return fs
61
- .readdirSync("/lib")
62
- .some((f) => f.startsWith("ld-musl-"));
63
- } catch {
64
- return false;
65
- }
66
- }
67
-
68
- function rustTarget() {
69
- const p = process.platform;
70
- const a = process.arch;
71
- if (p === "darwin")
72
- return a === "arm64" ? "aarch64-apple-darwin" : "x86_64-apple-darwin";
73
- if (p === "linux") {
74
- if (a === "arm64") return "aarch64-unknown-linux-musl";
75
- // x86_64: a fully-static musl build runs on glibc too, but musl's DNS
76
- // resolver ignores /etc/nsswitch.conf — so we only hand musl to machines
77
- // whose libc is already musl (Alpine et al.), and keep the proven glibc
78
- // build as the default everywhere else.
79
- return isMuslLinux()
80
- ? "x86_64-unknown-linux-musl"
81
- : "x86_64-unknown-linux-gnu";
82
- }
83
- if (p === "win32") return "x86_64-pc-windows-gnu";
84
- throw new Error(`unsupported platform: ${p}/${a}`);
85
- }
86
-
87
46
  // Resolve the proxy that applies to `targetUrl`, honouring the standard
88
47
  // HTTPS_PROXY / HTTP_PROXY / ALL_PROXY and NO_PROXY env vars (both cases).
89
48
  // Returns null when no proxy applies — including when NO_PROXY matches.
@@ -260,6 +219,28 @@ async function downloadTo(url, dest) {
260
219
 
261
220
  async function main() {
262
221
  const target = rustTarget();
222
+
223
+ // Fast path: the binary is already here, so the download is pure waste.
224
+ // It arrives one of two ways — the matching per-platform optionalDependency
225
+ // npm just installed from the registry, or a ./vendor copy from a previous
226
+ // run. Either way postinstall is a clean no-op. Crucially this returns
227
+ // BEFORE any network call, so an unreachable downloads.tokenade.net (blocked
228
+ // corporate proxy, offline sandbox) can never fail an install whose binary
229
+ // the registry already delivered.
230
+ try {
231
+ require.resolve(`${platformPackageName(target)}/package.json`);
232
+ console.log(
233
+ `✓ tokenade provided by ${platformPackageName(target)} (${target}) — no download needed.`,
234
+ );
235
+ return;
236
+ } catch {
237
+ // optional dep absent (skipped or no registry build) — try ./vendor.
238
+ }
239
+ if (fs.existsSync(path.join(VENDOR, binaryName()))) {
240
+ console.log(`✓ tokenade already vendored (${target}) — no download needed.`);
241
+ return;
242
+ }
243
+
263
244
  const manifest = await fetchJson(MANIFEST_URL);
264
245
  const entry = manifest.targets && manifest.targets[target];
265
246
  if (!entry || !entry.url || !entry.sha256) {
package/package.json CHANGED
@@ -1,21 +1,32 @@
1
1
  {
2
2
  "name": "@tokenade/cli",
3
- "version": "0.8.11",
4
- "description": "Tokenade \u2014 cut your AI coding agent's token bill. Installs the Tokenade CLI (a local, paid token-reduction tool; activate via your browser).",
3
+ "version": "0.8.13",
4
+ "description": "Tokenade cut your AI coding agent's token bill. Installs the Tokenade CLI (a local, paid token-reduction tool; activate via your browser).",
5
5
  "homepage": "https://tokenade.net",
6
6
  "license": "UNLICENSED",
7
7
  "bin": {
8
8
  "tokenade": "bin/tokenade.js"
9
9
  },
10
10
  "scripts": {
11
- "postinstall": "node install.js"
11
+ "postinstall": "node install.js",
12
+ "preuninstall": "node uninstall.js"
12
13
  },
13
14
  "dependencies": {
14
15
  "tar": "^7.5.16"
15
16
  },
17
+ "optionalDependencies": {
18
+ "@tokenade/cli-linux-x64": "0.8.13",
19
+ "@tokenade/cli-linux-x64-musl": "0.8.13",
20
+ "@tokenade/cli-linux-arm64-musl": "0.8.13",
21
+ "@tokenade/cli-darwin-arm64": "0.8.13",
22
+ "@tokenade/cli-darwin-x64": "0.8.13",
23
+ "@tokenade/cli-win32-x64": "0.8.13"
24
+ },
16
25
  "files": [
17
26
  "bin/",
27
+ "platform.js",
18
28
  "install.js",
29
+ "uninstall.js",
19
30
  "README.md"
20
31
  ],
21
32
  "engines": {
package/platform.js ADDED
@@ -0,0 +1,97 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * platform.js — the ONE place that maps this machine to a Rust target and to
4
+ * the matching per-platform npm package. Both bin/tokenade.js (runtime resolve)
5
+ * and install.js (download fallback) require it, so the platform detection —
6
+ * the musl probe especially — never drifts between the two.
7
+ *
8
+ * The per-platform packages (@tokenade/cli-<os>-<cpu>[-musl]) carry the actual
9
+ * binary and are listed as optionalDependencies of @tokenade/cli. npm installs
10
+ * ONLY the one whose os/cpu/libc match the host, straight from the registry —
11
+ * the esbuild/swc/biome pattern. install.js's download is a fallback for when
12
+ * those optional deps were skipped (--no-optional / --ignore-scripts) or the
13
+ * registry didn't carry a matching build.
14
+ */
15
+
16
+ "use strict";
17
+
18
+ const fs = require("node:fs");
19
+
20
+ // Is this Linux userland musl (Alpine, Void-musl, …) rather than glibc?
21
+ // A glibc node reports its runtime glibc version in the process report header;
22
+ // a musl node does not. We fall back to looking for the musl loader on disk.
23
+ // When genuinely unsure we return false (glibc) — that preserves the historical
24
+ // x86_64-gnu mapping for the overwhelming glibc majority, so a detection miss is
25
+ // never worse than the status quo.
26
+ function isMuslLinux() {
27
+ if (process.platform !== "linux") return false;
28
+ try {
29
+ const header = process.report && process.report.getReport().header;
30
+ if (header && typeof header.glibcVersionRuntime === "string") return false;
31
+ if (header && "glibcVersionRuntime" in header) {
32
+ // key present but not a string ⇒ no glibc runtime ⇒ musl
33
+ return true;
34
+ }
35
+ } catch {
36
+ // fall through to the on-disk probe
37
+ }
38
+ try {
39
+ return fs
40
+ .readdirSync("/lib")
41
+ .some((f) => f.startsWith("ld-musl-"));
42
+ } catch {
43
+ return false;
44
+ }
45
+ }
46
+
47
+ function rustTarget() {
48
+ const p = process.platform;
49
+ const a = process.arch;
50
+ if (p === "darwin")
51
+ return a === "arm64" ? "aarch64-apple-darwin" : "x86_64-apple-darwin";
52
+ if (p === "linux") {
53
+ if (a === "arm64") return "aarch64-unknown-linux-musl";
54
+ // x86_64: a fully-static musl build runs on glibc too, but musl's DNS
55
+ // resolver ignores /etc/nsswitch.conf — so we only hand musl to machines
56
+ // whose libc is already musl (Alpine et al.), and keep the proven glibc
57
+ // build as the default everywhere else.
58
+ return isMuslLinux()
59
+ ? "x86_64-unknown-linux-musl"
60
+ : "x86_64-unknown-linux-gnu";
61
+ }
62
+ if (p === "win32") return "x86_64-pc-windows-gnu";
63
+ throw new Error(`unsupported platform: ${p}/${a}`);
64
+ }
65
+
66
+ // Rust target dir (in dist/) → the npm sub-package that carries its binary.
67
+ // Kept in lockstep with the six packages the release script publishes and the
68
+ // optionalDependencies in package.json — one source of truth for the mapping.
69
+ const PACKAGE_BY_TARGET = {
70
+ "x86_64-unknown-linux-gnu": "@tokenade/cli-linux-x64",
71
+ "x86_64-unknown-linux-musl": "@tokenade/cli-linux-x64-musl",
72
+ "aarch64-unknown-linux-musl": "@tokenade/cli-linux-arm64-musl",
73
+ "aarch64-apple-darwin": "@tokenade/cli-darwin-arm64",
74
+ "x86_64-apple-darwin": "@tokenade/cli-darwin-x64",
75
+ "x86_64-pc-windows-gnu": "@tokenade/cli-win32-x64",
76
+ };
77
+
78
+ // The per-platform sub-package name for the current host, or the given target.
79
+ function platformPackageName(target) {
80
+ const t = target || rustTarget();
81
+ const pkg = PACKAGE_BY_TARGET[t];
82
+ if (!pkg) throw new Error(`no npm package mapped for target: ${t}`);
83
+ return pkg;
84
+ }
85
+
86
+ // The binary filename inside a sub-package (and in ./vendor).
87
+ function binaryName() {
88
+ return process.platform === "win32" ? "tokenade.exe" : "tokenade";
89
+ }
90
+
91
+ module.exports = {
92
+ isMuslLinux,
93
+ rustTarget,
94
+ platformPackageName,
95
+ binaryName,
96
+ PACKAGE_BY_TARGET,
97
+ };
package/uninstall.js ADDED
@@ -0,0 +1,76 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * preuninstall — run tokenade's own cleanup BEFORE npm deletes the package
4
+ * files, so that `npm uninstall -g @tokenade/cli` actually restores the
5
+ * machine (unwraps MCPs, strips hooks/CLAUDE.md/shell-proxy, and wipes
6
+ * ~/.tokenade + ~/.cache/tokenade) instead of orphaning integration that then
7
+ * spawns a missing binary on every tool call.
8
+ *
9
+ * We locate the SAME vendored binary that install.js (postinstall) dropped in
10
+ * ./vendor and invoke it as `tokenade uninstall --headless`:
11
+ * --headless → no banner, no next-step guidance, and it does NOT touch the
12
+ * binary itself (npm removes ./vendor right after this script).
13
+ *
14
+ * FAIL-OPEN by construction: every failure path (missing binary, spawn error,
15
+ * timeout, non-zero exit) is swallowed and we ALWAYS `process.exit(0)`. A
16
+ * cleanup that can't run must never block `npm uninstall` or leave the user
17
+ * unable to remove the package.
18
+ *
19
+ * npm lifecycle note: `preuninstall` runs on `npm uninstall` / `npm rm` /
20
+ * `npm remove` (local AND `-g`), and fires while the package files still
21
+ * exist. Caveats we accept: it is skipped when the user passes
22
+ * `--ignore-scripts` (or sets that config), and yarn/pnpm do not guarantee it.
23
+ * In those cases the binary is simply gone and its self-heal / a future
24
+ * `tokenade uninstall` covers the leftover integration.
25
+ */
26
+
27
+ "use strict";
28
+
29
+ const fs = require("node:fs");
30
+ const path = require("node:path");
31
+ const { spawnSync } = require("node:child_process");
32
+
33
+ // Bound the child so a wedged cleanup can never hang `npm uninstall`.
34
+ const CLEANUP_TIMEOUT_MS =
35
+ Number(process.env.TOKENADE_UNINSTALL_TIMEOUT_MS) || 20000;
36
+
37
+ function binaryPath() {
38
+ const name = process.platform === "win32" ? "tokenade.exe" : "tokenade";
39
+ return path.join(__dirname, "vendor", name);
40
+ }
41
+
42
+ function main() {
43
+ const bin = binaryPath();
44
+ // Missing binary (never installed, --ignore-scripts, half-install) → nothing
45
+ // to run; npm still removes the package. Not an error.
46
+ if (!fs.existsSync(bin)) {
47
+ return;
48
+ }
49
+ try {
50
+ const res = spawnSync(bin, ["uninstall", "--headless"], {
51
+ stdio: "inherit",
52
+ timeout: CLEANUP_TIMEOUT_MS,
53
+ // A cleanup crash must not surface as a throw here.
54
+ windowsHide: true,
55
+ });
56
+ if (res.error) {
57
+ console.error(
58
+ ` tokenade cleanup skipped (${res.error.message}); ` +
59
+ "run `tokenade uninstall` manually if any integration lingers.",
60
+ );
61
+ }
62
+ } catch (e) {
63
+ console.error(` tokenade cleanup skipped (${e && e.message});`);
64
+ }
65
+ }
66
+
67
+ try {
68
+ main();
69
+ } catch (e) {
70
+ // Absolute backstop — nothing above should throw, but never let it matter.
71
+ try {
72
+ console.error(` tokenade cleanup skipped (${e && e.message});`);
73
+ } catch {}
74
+ }
75
+ // ALWAYS succeed: npm must be free to remove the package.
76
+ process.exit(0);