@tokmizer/plugin 0.9.57 → 0.9.58
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/.claude-plugin/plugin.json +1 -1
- package/README.md +165 -6
- package/agent-plugins/amp.js +38 -0
- package/agent-plugins/hermes/__init__.py +39 -0
- package/agent-plugins/hermes/plugin.yaml +5 -0
- package/agent-plugins/kilo.js +40 -0
- package/agent-plugins/omp.js +32 -0
- package/agent-plugins/openclaw/index.js +39 -0
- package/agent-plugins/openclaw/openclaw.plugin.json +12 -0
- package/agent-plugins/openclaw/package.json +11 -0
- package/agent-plugins/opencode.js +40 -0
- package/agent-plugins/pi.js +30 -0
- package/codex-plugin/plugin.json +1 -1
- package/command-templates/tkr-gain.md +7 -2
- package/command-templates/tkr-status.md +5 -1
- package/command-templates/tkr-telemetry.md +3 -3
- package/command-templates/tkr-unlink.md +2 -2
- package/dist/bin/tkr-agent-hook.js +11 -0
- package/dist/bin/tkr-codex-hook.js +131 -40
- package/dist/bin/tkr-copilot-hook.js +11 -0
- package/dist/bin/tkr-crush-hook.js +11 -0
- package/dist/bin/tkr-cursor-hook.js +11 -0
- package/dist/bin/tkr-daemon.js +161 -55
- package/dist/bin/tkr-droid-hook.js +11 -0
- package/dist/bin/tkr-exec +135 -42
- package/dist/bin/tkr-exec.js +135 -42
- package/dist/bin/tkr-gemini-hook.js +11 -0
- package/dist/bin/tkr-grok-hook.js +11 -0
- package/dist/bin/tkr-junie-hook.js +11 -0
- package/dist/bin/tkr-processor.js +162 -0
- package/dist/bin/tkr-qwen-hook.js +11 -0
- package/dist/bin/tkr-shim.js +131 -38
- package/dist/bin/tkr-trae-hook.js +11 -0
- package/dist/bin/tkr-zcode-hook.js +11 -0
- package/dist/bin/tkr.js +356 -151
- package/dist/hooks-host/post-tool-use.js +144 -48
- package/dist/hooks-host/pre-tool-use.js +8 -7
- package/dist/hooks-host/reducer-prompt-submit.js +1 -1
- package/dist/hooks-host/reducer-session-start.js +1 -1
- package/dist/hooks-host/reducer-stop.js +25 -16
- package/dist/hooks-host/session-recap.js +4 -3
- package/dist/index.js +125 -39
- package/dist/native/manifest.json +18 -8
- package/dist/native/single.json +38 -0
- package/dist/native/tkr-exec-darwin-arm64 +0 -0
- package/dist/native/tkr-exec-darwin-x64 +0 -0
- package/dist/native/tkr-exec-linux-arm64 +0 -0
- package/dist/native/tkr-exec-linux-x64 +0 -0
- package/dist/native/tkr-exec-win32-arm64.exe +0 -0
- package/dist/native/tkr-exec-win32-x64.exe +0 -0
- package/hooks/on-session-start.d.mts +1 -0
- package/hooks/on-session-start.mjs +25 -8
- package/package.json +24 -2
- package/scripts/README-INSTALL.md +13 -5
- package/scripts/install-mac-linux.sh +72 -6
- package/scripts/install-native.mjs +164 -25
|
@@ -1,22 +1,32 @@
|
|
|
1
1
|
{
|
|
2
2
|
"darwin-arm64": {
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.58",
|
|
4
4
|
"file": "tkr-exec-darwin-arm64",
|
|
5
|
-
"sha256": "
|
|
5
|
+
"sha256": "e50af875107c01b7a8bd892a00b7debc305d025d116d84a1fd778642eb039fec"
|
|
6
6
|
},
|
|
7
7
|
"darwin-x64": {
|
|
8
|
-
"version": "0.9.
|
|
8
|
+
"version": "0.9.58",
|
|
9
9
|
"file": "tkr-exec-darwin-x64",
|
|
10
|
-
"sha256": "
|
|
10
|
+
"sha256": "14333667d09b4b2721d783a6294dd5c5b7a6170fd89596dbbf22588cac2d91dc"
|
|
11
11
|
},
|
|
12
12
|
"linux-arm64": {
|
|
13
|
-
"version": "0.9.
|
|
13
|
+
"version": "0.9.58",
|
|
14
14
|
"file": "tkr-exec-linux-arm64",
|
|
15
|
-
"sha256": "
|
|
15
|
+
"sha256": "2178af8eece9a1a3f8bb3668339cf093276db8defa8e6afb170d5d0097f30248"
|
|
16
16
|
},
|
|
17
17
|
"linux-x64": {
|
|
18
|
-
"version": "0.9.
|
|
18
|
+
"version": "0.9.58",
|
|
19
19
|
"file": "tkr-exec-linux-x64",
|
|
20
|
-
"sha256": "
|
|
20
|
+
"sha256": "8dbbe896fa8df2e2dfdce1ea429a5b4d140116785241b5e15c433811144701be"
|
|
21
|
+
},
|
|
22
|
+
"win32-arm64": {
|
|
23
|
+
"version": "0.9.58",
|
|
24
|
+
"file": "tkr-exec-win32-arm64.exe",
|
|
25
|
+
"sha256": "91fa46e8e512c8f212152c96547815648429bbfc382b4c625ef3eda6bd6c4737"
|
|
26
|
+
},
|
|
27
|
+
"win32-x64": {
|
|
28
|
+
"version": "0.9.58",
|
|
29
|
+
"file": "tkr-exec-win32-x64.exe",
|
|
30
|
+
"sha256": "e77522c9abebc8113a1ace6290d72d23590044932e19c7e5a119e0b635d240e8"
|
|
21
31
|
}
|
|
22
32
|
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
{
|
|
2
|
+
"darwin-arm64": {
|
|
3
|
+
"version": "0.9.58",
|
|
4
|
+
"package": "@tokmizer/plugin-darwin-arm64",
|
|
5
|
+
"file": "tkr",
|
|
6
|
+
"sha256": "17caa69f27d337f4eb6592f1ddc1a3272fa4c54703f474c3005fcce631e0c0f7"
|
|
7
|
+
},
|
|
8
|
+
"darwin-x64": {
|
|
9
|
+
"version": "0.9.58",
|
|
10
|
+
"package": "@tokmizer/plugin-darwin-x64",
|
|
11
|
+
"file": "tkr",
|
|
12
|
+
"sha256": "325caeb23f6bdb17a9bdddb3f15302aa150a0da26df7d985d8f1360deabfb926"
|
|
13
|
+
},
|
|
14
|
+
"linux-arm64": {
|
|
15
|
+
"version": "0.9.58",
|
|
16
|
+
"package": "@tokmizer/plugin-linux-arm64",
|
|
17
|
+
"file": "tkr",
|
|
18
|
+
"sha256": "b27429d17ef185819646c35b008e5bca6b61b33dd37412443ada4edf4b04ce93"
|
|
19
|
+
},
|
|
20
|
+
"linux-x64": {
|
|
21
|
+
"version": "0.9.58",
|
|
22
|
+
"package": "@tokmizer/plugin-linux-x64",
|
|
23
|
+
"file": "tkr",
|
|
24
|
+
"sha256": "f33976feeaa39bfc9b073fcf81d056e7a329d5ec9ceab49ff637cedada11b5bc"
|
|
25
|
+
},
|
|
26
|
+
"win32-arm64": {
|
|
27
|
+
"version": "0.9.58",
|
|
28
|
+
"package": "@tokmizer/plugin-win32-arm64",
|
|
29
|
+
"file": "tkr.exe",
|
|
30
|
+
"sha256": "b5e6ed96373bb546568cf50a16e47c0560e7af2a583b6580f25aae963a5b0987"
|
|
31
|
+
},
|
|
32
|
+
"win32-x64": {
|
|
33
|
+
"version": "0.9.58",
|
|
34
|
+
"package": "@tokmizer/plugin-win32-x64",
|
|
35
|
+
"file": "tkr.exe",
|
|
36
|
+
"sha256": "55668f0ad9f3916cb4b3cd486dd48637f879b4ae24b90d440d3e165834bda0d7"
|
|
37
|
+
}
|
|
38
|
+
}
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -5,9 +5,13 @@ import { readFileSync, writeFileSync, existsSync, mkdirSync, readdirSync, rename
|
|
|
5
5
|
import { homedir } from "node:os";
|
|
6
6
|
|
|
7
7
|
const home = process.env.TOKMIZER_HOME || join(homedir(), ".tokmizer");
|
|
8
|
+
const single = process.env.TOKMIZER_SINGLE_BINARY === "1";
|
|
9
|
+
const pluginRoot = process.env.CLAUDE_PLUGIN_ROOT || (single ? join(dirname(process.execPath), "..", "..") : join(dirname(fileURLToPath(import.meta.url)), ".."));
|
|
10
|
+
const networkOff =
|
|
11
|
+
["0", "off", "false", "no"].includes(String(process.env.TOKMIZER_NETWORK ?? "").trim().toLowerCase()) ||
|
|
12
|
+
existsSync(join(home, "network-off"));
|
|
8
13
|
|
|
9
14
|
try {
|
|
10
|
-
const pluginRoot = process.env.CLAUDE_PLUGIN_ROOT || join(dirname(fileURLToPath(import.meta.url)), "..");
|
|
11
15
|
const srcDir = join(pluginRoot, "command-templates");
|
|
12
16
|
const destDir = process.env.TOKMIZER_CLAUDE_COMMANDS_DIR || join(homedir(), ".claude", "commands");
|
|
13
17
|
const MARKER = "managed-by: tokmizer";
|
|
@@ -30,7 +34,6 @@ try {
|
|
|
30
34
|
} catch {}
|
|
31
35
|
|
|
32
36
|
try {
|
|
33
|
-
const pluginRoot = process.env.CLAUDE_PLUGIN_ROOT || join(dirname(fileURLToPath(import.meta.url)), "..");
|
|
34
37
|
const SEMVER = /^\d+\.\d+\.\d+$/;
|
|
35
38
|
const self = basename(pluginRoot);
|
|
36
39
|
if (SEMVER.test(self)) {
|
|
@@ -68,6 +71,22 @@ try {
|
|
|
68
71
|
}
|
|
69
72
|
} catch {}
|
|
70
73
|
|
|
74
|
+
try {
|
|
75
|
+
const noticeFile = join(home, "network-notice-shown");
|
|
76
|
+
if (!networkOff && !existsSync(noticeFile)) {
|
|
77
|
+
console.log(
|
|
78
|
+
[
|
|
79
|
+
"Tokmizer contacts tokmizer.com to keep its rules up to date and to improve the product. No account or email is needed.",
|
|
80
|
+
"It sends a random install ID, your computer name, OS and CPU model, a hash of your network card address and of your user name, and the plugin version. Command contents and output are never sent.",
|
|
81
|
+
"Usage stats (command names and token counts) are sent only while they are on. Check with: tkr telemetry status.",
|
|
82
|
+
"Turn all network access off: tkr network off. Usage stats only: tkr telemetry off."
|
|
83
|
+
].join("\n")
|
|
84
|
+
);
|
|
85
|
+
mkdirSync(home, { recursive: true });
|
|
86
|
+
writeFileSync(noticeFile, String(Date.now()));
|
|
87
|
+
}
|
|
88
|
+
} catch {}
|
|
89
|
+
|
|
71
90
|
try {
|
|
72
91
|
const onboardFile = join(home, "onboarded");
|
|
73
92
|
const shimNudge = join(home, "shim-nudge");
|
|
@@ -126,8 +145,7 @@ try {
|
|
|
126
145
|
} catch {}
|
|
127
146
|
|
|
128
147
|
try {
|
|
129
|
-
const
|
|
130
|
-
const manifest = JSON.parse(readFileSync(join(here, "..", ".claude-plugin", "plugin.json"), "utf8"));
|
|
148
|
+
const manifest = JSON.parse(readFileSync(join(pluginRoot, ".claude-plugin", "plugin.json"), "utf8"));
|
|
131
149
|
const state = JSON.parse(readFileSync(join(home, "state.json"), "utf8"));
|
|
132
150
|
const latest = state.latestPluginVersion;
|
|
133
151
|
if (latest && manifest.version && latest !== manifest.version) {
|
|
@@ -149,13 +167,12 @@ try {
|
|
|
149
167
|
last = Number(readFileSync(throttleFile, "utf8")) || 0;
|
|
150
168
|
} catch {}
|
|
151
169
|
|
|
152
|
-
if (existsSync(stateFile) && Date.now() - last >= THROTTLE_MS) {
|
|
170
|
+
if (!networkOff && existsSync(stateFile) && Date.now() - last >= THROTTLE_MS) {
|
|
153
171
|
try {
|
|
154
172
|
writeFileSync(throttleFile, String(Date.now()));
|
|
155
173
|
} catch {}
|
|
156
|
-
const
|
|
157
|
-
const
|
|
158
|
-
const child = spawn(process.execPath, [tkr, "heartbeat"], {
|
|
174
|
+
const args = single ? ["heartbeat"] : [join(dirname(fileURLToPath(import.meta.url)), "..", "dist", "bin", "tkr.js"), "heartbeat"];
|
|
175
|
+
const child = spawn(process.execPath, args, {
|
|
159
176
|
detached: true,
|
|
160
177
|
stdio: "ignore"
|
|
161
178
|
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tokmizer/plugin",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.58",
|
|
4
4
|
"description": "Tokmizer reduces token consumption for AI coding tools (Claude Code, Codex, Cursor, Aider, Gemini CLI, Cline, Windsurf, Copilot). Cross-OS.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"engines": {
|
|
@@ -12,6 +12,17 @@
|
|
|
12
12
|
"tkr-exec": "dist/bin/tkr-exec",
|
|
13
13
|
"tkr-shim": "dist/bin/tkr-shim.js",
|
|
14
14
|
"tkr-codex-hook": "dist/bin/tkr-codex-hook.js",
|
|
15
|
+
"tkr-cursor-hook": "dist/bin/tkr-cursor-hook.js",
|
|
16
|
+
"tkr-gemini-hook": "dist/bin/tkr-gemini-hook.js",
|
|
17
|
+
"tkr-agent-hook": "dist/bin/tkr-agent-hook.js",
|
|
18
|
+
"tkr-copilot-hook": "dist/bin/tkr-copilot-hook.js",
|
|
19
|
+
"tkr-droid-hook": "dist/bin/tkr-droid-hook.js",
|
|
20
|
+
"tkr-trae-hook": "dist/bin/tkr-trae-hook.js",
|
|
21
|
+
"tkr-qwen-hook": "dist/bin/tkr-qwen-hook.js",
|
|
22
|
+
"tkr-crush-hook": "dist/bin/tkr-crush-hook.js",
|
|
23
|
+
"tkr-junie-hook": "dist/bin/tkr-junie-hook.js",
|
|
24
|
+
"tkr-zcode-hook": "dist/bin/tkr-zcode-hook.js",
|
|
25
|
+
"tkr-grok-hook": "dist/bin/tkr-grok-hook.js",
|
|
15
26
|
"tkr-daemon": "dist/bin/tkr-daemon.js"
|
|
16
27
|
},
|
|
17
28
|
"files": [
|
|
@@ -20,6 +31,7 @@
|
|
|
20
31
|
".claude-plugin",
|
|
21
32
|
"hooks",
|
|
22
33
|
"command-templates",
|
|
34
|
+
"agent-plugins",
|
|
23
35
|
"scripts/install-mac-linux.sh",
|
|
24
36
|
"scripts/install-windows.ps1",
|
|
25
37
|
"scripts/install-native.mjs",
|
|
@@ -31,7 +43,9 @@
|
|
|
31
43
|
"build": "tsc -p tsconfig.json --noEmit && node scripts/bundle.mjs",
|
|
32
44
|
"build:native": "node scripts/build-native.mjs",
|
|
33
45
|
"postinstall": "node scripts/install-native.mjs",
|
|
34
|
-
"prepublishOnly": "npm run build && node scripts/check-native-artifacts.mjs && ( cd ../.. && npx vitest run packages/plugin ) && node scripts/check-prod-key.mjs && node scripts/check-version.mjs && node scripts/check-dist-clean.mjs && node scripts/check-pack-clean.mjs && node
|
|
46
|
+
"prepublishOnly": "npm run build && node scripts/check-native-artifacts.mjs && ( cd ../.. && npx vitest run packages/plugin ) && node scripts/check-prod-key.mjs && node scripts/check-version.mjs && node scripts/check-dist-clean.mjs && node scripts/check-pack-clean.mjs && node scripts/smoke-dist.mjs",
|
|
47
|
+
"build:single": "node scripts/build-single.mjs",
|
|
48
|
+
"pack:single": "node scripts/pack-single.mjs"
|
|
35
49
|
},
|
|
36
50
|
"devDependencies": {
|
|
37
51
|
"@types/cross-spawn": "^6.0.6",
|
|
@@ -41,6 +55,14 @@
|
|
|
41
55
|
"smol-toml": "^1.9.0",
|
|
42
56
|
"zod": "^3.23.8"
|
|
43
57
|
},
|
|
58
|
+
"optionalDependencies": {
|
|
59
|
+
"@tokmizer/plugin-darwin-arm64": "0.9.58",
|
|
60
|
+
"@tokmizer/plugin-darwin-x64": "0.9.58",
|
|
61
|
+
"@tokmizer/plugin-linux-arm64": "0.9.58",
|
|
62
|
+
"@tokmizer/plugin-linux-x64": "0.9.58",
|
|
63
|
+
"@tokmizer/plugin-win32-arm64": "0.9.58",
|
|
64
|
+
"@tokmizer/plugin-win32-x64": "0.9.58"
|
|
65
|
+
},
|
|
44
66
|
"publishConfig": {
|
|
45
67
|
"access": "public"
|
|
46
68
|
},
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
Tokmizer works with **Claude Code, OpenAI Codex, Cursor, Cline, Aider, Gemini CLI, Windsurf, and GitHub Copilot** — on **macOS, Linux, and Windows**.
|
|
4
4
|
|
|
5
|
-
Claude Code and
|
|
5
|
+
Claude Code, Codex, Cursor, Gemini CLI, GitHub Copilot, Factory Droid, Trae, Qwen Code, Crush, Junie CLI, ZCode, Grok Build and Kilo Code use native hooks or plugins (`tkr setup-<tool>` installs them; see the table below). Other tools can use command wrappers configured by `tkr shim install`. Node.js 20.11 or newer is required for npm installation. Tokmizer is free and requires no account.
|
|
6
6
|
|
|
7
7
|
## macOS / Linux
|
|
8
8
|
|
|
@@ -35,14 +35,22 @@ brew install norwelltw/tokmizer/tokmizer && tkr shim install
|
|
|
35
35
|
|
|
36
36
|
| Tool | Adapter | Setup |
|
|
37
37
|
|---------------|-----------------|----------------------------------------------------------------|
|
|
38
|
-
| Claude Code | Native hooks | `/plugin marketplace add norwelltw/Tokmizer` then `/plugin install tokmizer@tokmizer` |
|
|
38
|
+
| Claude Code | Native hooks | `/plugin marketplace add norwelltw/Tokmizer` then `/plugin install tokmizer@tokmizer`. Without Node.js, `install.sh` runs `tkr setup-claude` for you (`tkr setup-claude --remove` undoes it) |
|
|
39
39
|
| Codex CLI | Native hook | Install tkr (npm or brew), run `tkr setup-codex`, restart Codex, then approve in `/hooks` |
|
|
40
|
-
| Cursor |
|
|
40
|
+
| Cursor | Native hook | Install tkr (npm or brew), run `tkr setup-cursor`, then restart Cursor. The shell-shim keeps working without the hook. |
|
|
41
41
|
| Cline | Shell-shim | Open a new terminal after install. |
|
|
42
42
|
| Aider | Shell-shim | Open a new terminal after install. |
|
|
43
|
-
| Gemini CLI |
|
|
43
|
+
| Gemini CLI | Native hook | Install tkr (npm or brew), run `tkr setup-gemini`, then restart Gemini CLI. The shell-shim keeps working without the hook. |
|
|
44
44
|
| Windsurf | Shell-shim | Open a new terminal after install. |
|
|
45
|
-
| GitHub Copilot|
|
|
45
|
+
| GitHub Copilot| Native hook | Install tkr (npm or brew), run `tkr setup-copilot`, then restart VS Code or Copilot CLI. The shell-shim keeps working without the hook. |
|
|
46
|
+
| Factory Droid | Native hook | Run `tkr setup-droid`, then restart Droid. |
|
|
47
|
+
| Trae | Native hook | Run `tkr setup-trae`, then restart Trae. |
|
|
48
|
+
| Qwen Code | Native hook | Run `tkr setup-qwen`, then restart Qwen Code. |
|
|
49
|
+
| Crush | Native hook | Run `tkr setup-crush`, then restart Crush. |
|
|
50
|
+
| Junie CLI | Native hook | Run `tkr setup-junie`, then restart Junie. |
|
|
51
|
+
| ZCode | Native hook | Run `tkr setup-zcode`, then restart ZCode. |
|
|
52
|
+
| Grok Build | Native hook | Run `tkr setup-grok`, then restart Grok Build (1.0.4 or newer). |
|
|
53
|
+
| Kilo Code | Plugin | Run `tkr setup-kilo`, then restart Kilo Code. |
|
|
46
54
|
| Plain shell | Shell-shim | Works out of the box. |
|
|
47
55
|
|
|
48
56
|
Open a new terminal after installation and run `tkr status`. If activation is pending because the service could not be reached, rerun `tkr status` when online. Connecting an account is optional: `tkr link <your-key>`.
|
|
@@ -4,15 +4,81 @@ set -euo pipefail
|
|
|
4
4
|
err() { printf 'tkr install: %s\n' "$*" >&2; exit 1; }
|
|
5
5
|
log() { printf 'tkr: %s\n' "$*"; }
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
command -v
|
|
9
|
-
node -e 'const [major, minor] = process.versions.node.split(".").map(Number); process.exit(major > 20 || major === 20 && minor >= 11 ? 0 : 1)'
|
|
7
|
+
node_ready() {
|
|
8
|
+
command -v node >/dev/null 2>&1 && command -v npm >/dev/null 2>&1 &&
|
|
9
|
+
node -e 'const [major, minor] = process.versions.node.split(".").map(Number); process.exit(major > 20 || major === 20 && minor >= 11 ? 0 : 1)' 2>/dev/null
|
|
10
|
+
}
|
|
10
11
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
12
|
+
work=""
|
|
13
|
+
|
|
14
|
+
sha256_of() {
|
|
15
|
+
if command -v sha256sum >/dev/null 2>&1; then sha256sum "$1" | cut -d' ' -f1
|
|
16
|
+
else shasum -a 256 "$1" | cut -d' ' -f1
|
|
17
|
+
fi
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
install_standalone() {
|
|
21
|
+
local os arch key base line name sum root bindir unavailable
|
|
22
|
+
unavailable="Install Node.js 20.11+ and retry."
|
|
23
|
+
case "$(uname -s)" in
|
|
24
|
+
Darwin) os=darwin ;;
|
|
25
|
+
Linux) os=linux ;;
|
|
26
|
+
*) err "No standalone build for $(uname -s). $unavailable" ;;
|
|
27
|
+
esac
|
|
28
|
+
case "$(uname -m)" in
|
|
29
|
+
arm64|aarch64) arch=arm64 ;;
|
|
30
|
+
x86_64|amd64) arch=x64 ;;
|
|
31
|
+
*) err "No standalone build for $(uname -m). $unavailable" ;;
|
|
32
|
+
esac
|
|
33
|
+
key="$os-$arch"
|
|
34
|
+
base="${TOKMIZER_DOWNLOAD_BASE:-https://github.com/norwelltw/Tokmizer/releases/latest/download}"
|
|
35
|
+
root="${TOKMIZER_INSTALL_DIR:-$HOME/.local/share/tokmizer}"
|
|
36
|
+
bindir="${TOKMIZER_BIN_DIR:-$HOME/.local/bin}"
|
|
37
|
+
command -v curl >/dev/null 2>&1 || err "curl is required."
|
|
38
|
+
command -v tar >/dev/null 2>&1 || err "tar is required."
|
|
39
|
+
work="$(mktemp -d)"
|
|
40
|
+
trap 'rm -rf "${work:-}"' EXIT
|
|
41
|
+
curl -fsSL "$base/SHA256SUMS" -o "$work/SHA256SUMS" || err "Could not download the standalone build. $unavailable"
|
|
42
|
+
line="$(grep -E "^[0-9a-f]{64} tokmizer-[0-9][^ /]*-$key\.tar\.gz$" "$work/SHA256SUMS" | head -n 1)" || true
|
|
43
|
+
[ -n "$line" ] || err "No standalone build for $key. $unavailable"
|
|
44
|
+
sum="${line%% *}"
|
|
45
|
+
name="${line#* }"
|
|
46
|
+
log "Downloading $name..."
|
|
47
|
+
curl -fsSL "$base/$name" -o "$work/$name" || err "Could not download $name."
|
|
48
|
+
[ "$(sha256_of "$work/$name")" = "$sum" ] || err "Checksum mismatch for $name."
|
|
49
|
+
tar -xzf "$work/$name" -C "$work"
|
|
50
|
+
"$work/${name%.tar.gz}/dist/bin/tkr" --single-check >/dev/null 2>&1 || err "The downloaded executable does not run on this system. $unavailable"
|
|
51
|
+
mkdir -p "$root" "$bindir"
|
|
52
|
+
rm -rf "${root:?}/${name%.tar.gz}"
|
|
53
|
+
mv "$work/${name%.tar.gz}" "$root/${name%.tar.gz}"
|
|
54
|
+
ln -sfn "${name%.tar.gz}" "$root/current"
|
|
55
|
+
ln -sf "$root/current/dist/bin/tkr" "$bindir/tkr"
|
|
56
|
+
ln -sf "$root/current/dist/bin/tkr-exec" "$bindir/tkr-exec"
|
|
57
|
+
case ":$PATH:" in
|
|
58
|
+
*":$bindir:"*) ;;
|
|
59
|
+
*) PATH="$bindir:$PATH"; log "Add $bindir to your PATH." ;;
|
|
60
|
+
esac
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
claude_code_present() {
|
|
64
|
+
[ -d "${CLAUDE_CONFIG_DIR:-$HOME/.claude}" ] || command -v claude >/dev/null 2>&1
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
standalone=0
|
|
68
|
+
if [ "${TOKMIZER_STANDALONE:-}" = "1" ] || ! node_ready; then
|
|
69
|
+
log "Installing the standalone tkr executable (Node.js not needed)..."
|
|
70
|
+
install_standalone
|
|
71
|
+
standalone=1
|
|
72
|
+
else
|
|
73
|
+
log "Installing @tokmizer/plugin from npm..."
|
|
74
|
+
npm install -g @tokmizer/plugin
|
|
75
|
+
fi
|
|
76
|
+
command -v tkr >/dev/null 2>&1 || err "tkr is not on PATH. Add its directory to PATH and run tkr shim install."
|
|
14
77
|
|
|
15
78
|
tkr shim install </dev/null
|
|
79
|
+
if [ "$standalone" = "1" ] && claude_code_present; then
|
|
80
|
+
tkr setup-claude </dev/null || log "Could not add the Claude Code hooks. Run: tkr setup-claude"
|
|
81
|
+
fi
|
|
16
82
|
log "Done. Open a new terminal. Tokmizer is free and does not require an account."
|
|
17
83
|
log "For Codex, also run: tkr setup-codex"
|
|
18
84
|
log "Check the installation with: tkr status"
|
|
@@ -1,26 +1,88 @@
|
|
|
1
1
|
import { createHash, randomUUID } from "node:crypto";
|
|
2
2
|
import { execFileSync } from "node:child_process";
|
|
3
|
-
import { chmodSync, readFileSync, renameSync, rmSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { chmodSync, existsSync, linkSync, readFileSync, renameSync, rmSync, writeFileSync } from "node:fs";
|
|
4
|
+
import { createRequire } from "node:module";
|
|
4
5
|
import { dirname, join, resolve } from "node:path";
|
|
5
6
|
import { fileURLToPath } from "node:url";
|
|
6
7
|
|
|
7
8
|
const packageDirectory = resolve(dirname(fileURLToPath(import.meta.url)), "..");
|
|
9
|
+
const sessionStartCommand = '"${CLAUDE_PLUGIN_ROOT}/dist/bin/tkr-exec" --session-start';
|
|
10
|
+
const windowsCommand = "${CLAUDE_PLUGIN_ROOT}/dist/bin/tkr-exec.exe";
|
|
8
11
|
|
|
9
|
-
function
|
|
12
|
+
function sessionStartHook(hook) {
|
|
13
|
+
return hook.command === sessionStartCommand || (hook.command === windowsCommand && Array.isArray(hook.args) && hook.args.length === 1 && hook.args[0] === "--session-start");
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function configureSessionStart(manifest, mode) {
|
|
17
|
+
const groups = manifest.hooks?.SessionStart;
|
|
18
|
+
if (!Array.isArray(groups) || groups.length === 0) return false;
|
|
19
|
+
const wanted = mode === "windows" ? { type: "command", command: windowsCommand, args: ["--session-start"], timeout: 5 } : mode === "unix" ? { type: "command", command: sessionStartCommand, timeout: 5 } : null;
|
|
20
|
+
const present = groups.flatMap((group) => (group.hooks ?? []).filter(sessionStartHook));
|
|
21
|
+
if (wanted && present.length === 1 && present[0].command === wanted.command) return false;
|
|
22
|
+
if (!wanted && present.length === 0) return false;
|
|
23
|
+
for (const group of groups) if (group.hooks) group.hooks = group.hooks.filter((hook) => !sessionStartHook(hook));
|
|
24
|
+
if (wanted) (groups[0].hooks ??= []).unshift(wanted);
|
|
25
|
+
return true;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const root = "${CLAUDE_PLUGIN_ROOT}";
|
|
29
|
+
const knownHooks = [
|
|
30
|
+
{ event: "PreToolUse", node: `node "${root}/dist/hooks-host/pre-tool-use.js"`, words: ["hook", "pre-tool-use"], flag: "--pre-tool-use" },
|
|
31
|
+
{ event: "PostToolUse", node: `node "${root}/dist/hooks-host/post-tool-use.js"`, words: ["hook", "post-tool-use"], flag: "--post-tool-use" },
|
|
32
|
+
{ event: "SessionStart", node: `node "${root}/hooks/on-session-start.mjs"`, words: ["hook", "session-start"] },
|
|
33
|
+
{ event: "SessionStart", node: `node "${root}/dist/hooks-host/reducer-session-start.js"`, words: ["hook", "reducer-session-start"] },
|
|
34
|
+
{ event: "SessionStart", node: `node "${root}/dist/hooks-host/session-recap.js"`, words: ["hook", "session-recap"] },
|
|
35
|
+
{ event: "UserPromptSubmit", node: `node "${root}/dist/hooks-host/reducer-prompt-submit.js"`, words: ["hook", "reducer-prompt-submit"] },
|
|
36
|
+
{ event: "Stop", node: `node "${root}/dist/hooks-host/reducer-stop.js"`, words: ["hook", "reducer-stop"] }
|
|
37
|
+
];
|
|
38
|
+
|
|
39
|
+
function hookForms(spec) {
|
|
40
|
+
const forms = {
|
|
41
|
+
node: { command: spec.node },
|
|
42
|
+
singleUnix: { command: `"${root}/dist/bin/tkr" ${spec.words.join(" ")}` },
|
|
43
|
+
singleWindows: { command: `${root}/dist/bin/tkr.exe`, args: spec.words }
|
|
44
|
+
};
|
|
45
|
+
if (spec.flag) {
|
|
46
|
+
forms.nativeUnix = { command: `"${root}/dist/bin/tkr-exec" ${spec.flag}` };
|
|
47
|
+
forms.nativeWindows = { command: windowsCommand, args: [spec.flag] };
|
|
48
|
+
}
|
|
49
|
+
return forms;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function sameArgs(left, right) {
|
|
53
|
+
return Array.isArray(left) && left.length === right.length && left.every((value, index) => value === right[index]);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function matchesForm(hook, form) {
|
|
57
|
+
return hook.command === form.command && (!form.args || sameArgs(hook.args, form.args));
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function wantedForm(forms, mode, single, windows) {
|
|
61
|
+
if (forms.nativeUnix && mode === "unix") return forms.nativeUnix;
|
|
62
|
+
if (forms.nativeWindows && mode === "windows") return forms.nativeWindows;
|
|
63
|
+
if (single) return windows ? forms.singleWindows : forms.singleUnix;
|
|
64
|
+
return forms.node;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function configureHook(directory, mode, single = false, windows = mode === "windows") {
|
|
10
68
|
const path = join(directory, "hooks", "hooks.json");
|
|
11
69
|
let manifest;
|
|
12
70
|
try { manifest = JSON.parse(readFileSync(path, "utf8")); } catch { return; }
|
|
13
|
-
const
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
const
|
|
20
|
-
|
|
71
|
+
const before = JSON.stringify(manifest);
|
|
72
|
+
configureSessionStart(manifest, mode);
|
|
73
|
+
for (const spec of knownHooks) {
|
|
74
|
+
const forms = hookForms(spec);
|
|
75
|
+
const wanted = wantedForm(forms, mode, single, windows);
|
|
76
|
+
for (const group of manifest.hooks?.[spec.event] ?? []) {
|
|
77
|
+
for (const hook of group.hooks ?? []) {
|
|
78
|
+
if (!Object.values(forms).some((form) => matchesForm(hook, form))) continue;
|
|
79
|
+
hook.command = wanted.command;
|
|
80
|
+
if (wanted.args) hook.args = [...wanted.args];
|
|
81
|
+
else delete hook.args;
|
|
82
|
+
}
|
|
21
83
|
}
|
|
22
84
|
}
|
|
23
|
-
if (
|
|
85
|
+
if (JSON.stringify(manifest) === before) return;
|
|
24
86
|
const temporary = `${path}.${randomUUID()}.tmp`;
|
|
25
87
|
try {
|
|
26
88
|
writeFileSync(temporary, JSON.stringify(manifest, null, 2) + "\n", { flag: "wx", mode: 0o644 });
|
|
@@ -30,37 +92,114 @@ function configureHook(directory, native) {
|
|
|
30
92
|
}
|
|
31
93
|
}
|
|
32
94
|
|
|
33
|
-
|
|
34
|
-
|
|
95
|
+
function singlePath(directory, platform) {
|
|
96
|
+
return join(directory, "dist", "bin", platform === "win32" ? "tkr.exe" : "tkr");
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function probeSingle(path, version) {
|
|
100
|
+
try {
|
|
101
|
+
return execFileSync(path, ["--single-check"], { encoding: "utf8", timeout: 5000, maxBuffer: 1024, stdio: ["ignore", "pipe", "ignore"], windowsHide: true }).trim() === `tokmizer-single ${version}`;
|
|
102
|
+
} catch {
|
|
103
|
+
return false;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function packageVersion(directory) {
|
|
108
|
+
return JSON.parse(readFileSync(join(directory, "package.json"), "utf8")).version;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export function singleInstalled(directory = packageDirectory, platform = process.platform) {
|
|
112
|
+
if (process.env.TOKMIZER_SINGLE_EXEC === "0") return false;
|
|
113
|
+
const path = singlePath(directory, platform);
|
|
114
|
+
try {
|
|
115
|
+
return existsSync(path) && probeSingle(path, packageVersion(directory));
|
|
116
|
+
} catch {
|
|
117
|
+
return false;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export function installSingle(directory = packageDirectory, platform = process.platform, arch = process.arch) {
|
|
122
|
+
const target = singlePath(directory, platform);
|
|
35
123
|
const temporary = `${target}.${randomUUID()}.tmp`;
|
|
36
|
-
const
|
|
37
|
-
|
|
124
|
+
const discard = () => {
|
|
125
|
+
try { rmSync(target, { force: true }); } catch {}
|
|
126
|
+
return false;
|
|
127
|
+
};
|
|
128
|
+
try {
|
|
129
|
+
if (process.env.TOKMIZER_SINGLE_EXEC === "0" || !["darwin", "linux", "win32"].includes(platform) || !["arm64", "x64"].includes(arch)) return discard();
|
|
130
|
+
const version = packageVersion(directory);
|
|
131
|
+
const key = `${platform}-${arch}`;
|
|
132
|
+
const entry = JSON.parse(readFileSync(join(directory, "dist", "native", "single.json"), "utf8"))[key];
|
|
133
|
+
if (!entry || entry.version !== version || entry.package !== `@tokmizer/plugin-${key}` || entry.file !== (platform === "win32" ? "tkr.exe" : "tkr") || !/^[0-9a-f]{64}$/.test(entry.sha256)) return discard();
|
|
134
|
+
const source = join(dirname(createRequire(join(directory, "package.json")).resolve(`${entry.package}/package.json`)), entry.file);
|
|
135
|
+
const bytes = readFileSync(source);
|
|
136
|
+
if (createHash("sha256").update(bytes).digest("hex") !== entry.sha256) return discard();
|
|
137
|
+
try { chmodSync(source, 0o755); } catch {}
|
|
138
|
+
if (!probeSingle(source, version)) return discard();
|
|
139
|
+
try {
|
|
140
|
+
linkSync(source, temporary);
|
|
141
|
+
} catch {
|
|
142
|
+
writeFileSync(temporary, bytes, { flag: "wx", mode: 0o755 });
|
|
143
|
+
}
|
|
38
144
|
chmodSync(temporary, 0o755);
|
|
145
|
+
if (createHash("sha256").update(readFileSync(temporary)).digest("hex") !== entry.sha256) return discard();
|
|
39
146
|
renameSync(temporary, target);
|
|
147
|
+
return probeSingle(target, version) || discard();
|
|
148
|
+
} catch {
|
|
149
|
+
return discard();
|
|
150
|
+
} finally {
|
|
151
|
+
try { rmSync(temporary, { force: true }); } catch {}
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
export function installNative(directory = packageDirectory, platform = process.platform, arch = process.arch, single = singleInstalled(directory, platform)) {
|
|
156
|
+
const windows = platform === "win32";
|
|
157
|
+
const launcher = join(directory, "dist", "bin", "tkr-exec");
|
|
158
|
+
const target = windows ? `${launcher}.exe` : launcher;
|
|
159
|
+
const temporaries = [];
|
|
160
|
+
const replace = (destination, bytes) => {
|
|
161
|
+
const temporary = `${destination}.${randomUUID()}.tmp`;
|
|
162
|
+
temporaries.push(temporary);
|
|
163
|
+
writeFileSync(temporary, bytes, { flag: "wx", mode: 0o755 });
|
|
164
|
+
chmodSync(temporary, 0o755);
|
|
165
|
+
renameSync(temporary, destination);
|
|
40
166
|
};
|
|
41
167
|
try {
|
|
42
|
-
configureHook(directory,
|
|
43
|
-
replace(readFileSync(join(directory, "dist", "bin", "tkr-exec.js")));
|
|
44
|
-
if (
|
|
168
|
+
configureHook(directory, "node", single, windows);
|
|
169
|
+
replace(launcher, readFileSync(join(directory, "dist", "bin", "tkr-exec.js")));
|
|
170
|
+
if (windows) rmSync(target, { force: true });
|
|
171
|
+
if (process.env.TOKMIZER_NATIVE_EXEC === "0" || !["darwin", "linux", "win32"].includes(platform) || !["arm64", "x64"].includes(arch)) return false;
|
|
45
172
|
const { version } = JSON.parse(readFileSync(join(directory, "package.json"), "utf8"));
|
|
46
173
|
const manifest = JSON.parse(readFileSync(join(directory, "dist", "native", "manifest.json"), "utf8"));
|
|
47
174
|
const key = `${platform}-${arch}`;
|
|
48
175
|
const entry = manifest[key];
|
|
49
|
-
if (!entry || entry.version !== version || entry.file !== `tkr-exec-${key}` || !/^[0-9a-f]{64}$/.test(entry.sha256)) return false;
|
|
176
|
+
if (!entry || entry.version !== version || entry.file !== `tkr-exec-${key}${windows ? ".exe" : ""}` || !/^[0-9a-f]{64}$/.test(entry.sha256)) return false;
|
|
50
177
|
const source = join(directory, "dist", "native", entry.file);
|
|
51
178
|
const bytes = readFileSync(source);
|
|
52
179
|
if (createHash("sha256").update(bytes).digest("hex") !== entry.sha256) return false;
|
|
53
180
|
chmodSync(source, 0o755);
|
|
54
|
-
const probe = execFileSync(
|
|
55
|
-
if (probe
|
|
56
|
-
replace(bytes);
|
|
57
|
-
|
|
181
|
+
const probe = (path) => execFileSync(path, ["--native-check"], { encoding: "utf8", timeout: 2000, maxBuffer: 1024, stdio: ["ignore", "pipe", "ignore"], windowsHide: true }).trim() === `tokmizer-native ${version} protocol=2`;
|
|
182
|
+
if (!probe(source)) return false;
|
|
183
|
+
replace(target, bytes);
|
|
184
|
+
let installed = false;
|
|
185
|
+
try { installed = probe(target); } catch {}
|
|
186
|
+
if (!installed) {
|
|
187
|
+
if (windows) rmSync(target, { force: true });
|
|
188
|
+
else replace(launcher, readFileSync(join(directory, "dist", "bin", "tkr-exec.js")));
|
|
189
|
+
return false;
|
|
190
|
+
}
|
|
191
|
+
configureHook(directory, windows ? "windows" : "unix", single, windows);
|
|
58
192
|
return true;
|
|
59
193
|
} catch {
|
|
60
194
|
return false;
|
|
61
195
|
} finally {
|
|
62
|
-
|
|
196
|
+
for (const temporary of temporaries) {
|
|
197
|
+
try { rmSync(temporary, { force: true }); } catch {}
|
|
198
|
+
}
|
|
63
199
|
}
|
|
64
200
|
}
|
|
65
201
|
|
|
66
|
-
if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.url))
|
|
202
|
+
if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
|
|
203
|
+
installSingle();
|
|
204
|
+
installNative();
|
|
205
|
+
}
|