@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 +2 -2
- package/bin/tokenade.js +27 -8
- package/install.js +27 -46
- package/package.json +14 -3
- package/platform.js +97 -0
- package/uninstall.js +76 -0
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
|
|
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
|
|
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
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
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`
|
|
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.
|
|
4
|
-
"description": "Tokenade
|
|
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);
|