@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.
Files changed (56) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/README.md +165 -6
  3. package/agent-plugins/amp.js +38 -0
  4. package/agent-plugins/hermes/__init__.py +39 -0
  5. package/agent-plugins/hermes/plugin.yaml +5 -0
  6. package/agent-plugins/kilo.js +40 -0
  7. package/agent-plugins/omp.js +32 -0
  8. package/agent-plugins/openclaw/index.js +39 -0
  9. package/agent-plugins/openclaw/openclaw.plugin.json +12 -0
  10. package/agent-plugins/openclaw/package.json +11 -0
  11. package/agent-plugins/opencode.js +40 -0
  12. package/agent-plugins/pi.js +30 -0
  13. package/codex-plugin/plugin.json +1 -1
  14. package/command-templates/tkr-gain.md +7 -2
  15. package/command-templates/tkr-status.md +5 -1
  16. package/command-templates/tkr-telemetry.md +3 -3
  17. package/command-templates/tkr-unlink.md +2 -2
  18. package/dist/bin/tkr-agent-hook.js +11 -0
  19. package/dist/bin/tkr-codex-hook.js +131 -40
  20. package/dist/bin/tkr-copilot-hook.js +11 -0
  21. package/dist/bin/tkr-crush-hook.js +11 -0
  22. package/dist/bin/tkr-cursor-hook.js +11 -0
  23. package/dist/bin/tkr-daemon.js +161 -55
  24. package/dist/bin/tkr-droid-hook.js +11 -0
  25. package/dist/bin/tkr-exec +135 -42
  26. package/dist/bin/tkr-exec.js +135 -42
  27. package/dist/bin/tkr-gemini-hook.js +11 -0
  28. package/dist/bin/tkr-grok-hook.js +11 -0
  29. package/dist/bin/tkr-junie-hook.js +11 -0
  30. package/dist/bin/tkr-processor.js +162 -0
  31. package/dist/bin/tkr-qwen-hook.js +11 -0
  32. package/dist/bin/tkr-shim.js +131 -38
  33. package/dist/bin/tkr-trae-hook.js +11 -0
  34. package/dist/bin/tkr-zcode-hook.js +11 -0
  35. package/dist/bin/tkr.js +356 -151
  36. package/dist/hooks-host/post-tool-use.js +144 -48
  37. package/dist/hooks-host/pre-tool-use.js +8 -7
  38. package/dist/hooks-host/reducer-prompt-submit.js +1 -1
  39. package/dist/hooks-host/reducer-session-start.js +1 -1
  40. package/dist/hooks-host/reducer-stop.js +25 -16
  41. package/dist/hooks-host/session-recap.js +4 -3
  42. package/dist/index.js +125 -39
  43. package/dist/native/manifest.json +18 -8
  44. package/dist/native/single.json +38 -0
  45. package/dist/native/tkr-exec-darwin-arm64 +0 -0
  46. package/dist/native/tkr-exec-darwin-x64 +0 -0
  47. package/dist/native/tkr-exec-linux-arm64 +0 -0
  48. package/dist/native/tkr-exec-linux-x64 +0 -0
  49. package/dist/native/tkr-exec-win32-arm64.exe +0 -0
  50. package/dist/native/tkr-exec-win32-x64.exe +0 -0
  51. package/hooks/on-session-start.d.mts +1 -0
  52. package/hooks/on-session-start.mjs +25 -8
  53. package/package.json +24 -2
  54. package/scripts/README-INSTALL.md +13 -5
  55. package/scripts/install-mac-linux.sh +72 -6
  56. package/scripts/install-native.mjs +164 -25
@@ -1,22 +1,32 @@
1
1
  {
2
2
  "darwin-arm64": {
3
- "version": "0.9.57",
3
+ "version": "0.9.58",
4
4
  "file": "tkr-exec-darwin-arm64",
5
- "sha256": "c3979c74dc1faea94b6c5e3f974c3dd34df9615ca6dd3448a28a356c51f1948f"
5
+ "sha256": "e50af875107c01b7a8bd892a00b7debc305d025d116d84a1fd778642eb039fec"
6
6
  },
7
7
  "darwin-x64": {
8
- "version": "0.9.57",
8
+ "version": "0.9.58",
9
9
  "file": "tkr-exec-darwin-x64",
10
- "sha256": "645abd7bf9bf7723db091de56d7aebefa5eeb4189303a1843739ab76529e6e1f"
10
+ "sha256": "14333667d09b4b2721d783a6294dd5c5b7a6170fd89596dbbf22588cac2d91dc"
11
11
  },
12
12
  "linux-arm64": {
13
- "version": "0.9.57",
13
+ "version": "0.9.58",
14
14
  "file": "tkr-exec-linux-arm64",
15
- "sha256": "06f615141483965cc840c9eeec3da63149daf2a09603b9390471a712124ab160"
15
+ "sha256": "2178af8eece9a1a3f8bb3668339cf093276db8defa8e6afb170d5d0097f30248"
16
16
  },
17
17
  "linux-x64": {
18
- "version": "0.9.57",
18
+ "version": "0.9.58",
19
19
  "file": "tkr-exec-linux-x64",
20
- "sha256": "1a8ff65bdfb5f31444c3b2df2b68cd556ca2ebfdcc21c2e89d976f15f53e5dd2"
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
@@ -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 here = dirname(fileURLToPath(import.meta.url));
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 here = dirname(fileURLToPath(import.meta.url));
157
- const tkr = join(here, "..", "dist", "bin", "tkr.js");
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.57",
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 dist/bin/tkr-exec.js echo dist-smoke-ok && node dist/bin/tkr.js help > /dev/null"
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 Codex use native hooks. 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.
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 | Shell-shim | Open a new terminal after install. |
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 | Shell-shim | Open a new terminal after install. |
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| Shell-shim | Open a new terminal after install. |
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
- command -v node >/dev/null 2>&1 || err "Node.js 20.11+ is required."
8
- command -v npm >/dev/null 2>&1 || err "npm is required."
9
- node -e 'const [major, minor] = process.versions.node.split(".").map(Number); process.exit(major > 20 || major === 20 && minor >= 11 ? 0 : 1)' || err "Node.js 20.11+ is required."
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
- log "Installing @tokmizer/plugin from npm..."
12
- npm install -g @tokmizer/plugin
13
- command -v tkr >/dev/null 2>&1 || err "tkr is not on PATH. Add the npm global bin directory to PATH and run tkr shim install."
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 configureHook(directory, native) {
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 nodeCommand = 'node "${CLAUDE_PLUGIN_ROOT}/dist/hooks-host/pre-tool-use.js"';
14
- const nativeCommand = '"${CLAUDE_PLUGIN_ROOT}/dist/bin/tkr-exec" --pre-tool-use';
15
- let changed = false;
16
- for (const group of manifest.hooks?.PreToolUse ?? []) {
17
- for (const hook of group.hooks ?? []) {
18
- if (hook.command !== nodeCommand && hook.command !== nativeCommand) continue;
19
- const command = native ? nativeCommand : nodeCommand;
20
- if (hook.command !== command) { hook.command = command; changed = true; }
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 (!changed) return;
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
- export function installNative(directory = packageDirectory, platform = process.platform, arch = process.arch) {
34
- const target = join(directory, "dist", "bin", "tkr-exec");
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 replace = (bytes) => {
37
- writeFileSync(temporary, bytes, { flag: "wx", mode: 0o755 });
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, false);
43
- replace(readFileSync(join(directory, "dist", "bin", "tkr-exec.js")));
44
- if (process.env.TOKMIZER_NATIVE_EXEC === "0" || !["darwin", "linux"].includes(platform) || !["arm64", "x64"].includes(arch)) return false;
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(source, ["--native-check"], { encoding: "utf8", timeout: 2000, maxBuffer: 1024, stdio: ["ignore", "pipe", "ignore"] });
55
- if (probe.trim() !== `tokmizer-native ${version} protocol=2`) return false;
56
- replace(bytes);
57
- configureHook(directory, true);
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
- try { rmSync(temporary, { force: true }); } catch {}
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)) installNative();
202
+ if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
203
+ installSingle();
204
+ installNative();
205
+ }