anolisa-tokenless 0.7.7

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 (70) hide show
  1. package/LICENSE +190 -0
  2. package/README.md +770 -0
  3. package/adapters/tokenless/claude-code/.claude-plugin/marketplace.json +15 -0
  4. package/adapters/tokenless/claude-code/.claude-plugin/plugin.json +9 -0
  5. package/adapters/tokenless/claude-code/hooks/hooks.json +38 -0
  6. package/adapters/tokenless/claude-code/scripts/detect.sh +193 -0
  7. package/adapters/tokenless/claude-code/scripts/install.sh +92 -0
  8. package/adapters/tokenless/claude-code/scripts/uninstall.sh +50 -0
  9. package/adapters/tokenless/codex/.codex-plugin/plugin.json +20 -0
  10. package/adapters/tokenless/codex/README.md +160 -0
  11. package/adapters/tokenless/codex/hooks/hooks.json +52 -0
  12. package/adapters/tokenless/codex/scripts/_common.sh +25 -0
  13. package/adapters/tokenless/codex/scripts/check-tokenless +94 -0
  14. package/adapters/tokenless/codex/scripts/compress-response +441 -0
  15. package/adapters/tokenless/codex/scripts/detect.sh +61 -0
  16. package/adapters/tokenless/codex/scripts/install.sh +151 -0
  17. package/adapters/tokenless/codex/scripts/rewrite-hook +282 -0
  18. package/adapters/tokenless/codex/scripts/tool-ready +303 -0
  19. package/adapters/tokenless/codex/scripts/uninstall.sh +78 -0
  20. package/adapters/tokenless/common/commands/tokenless-stats.toml +2 -0
  21. package/adapters/tokenless/common/cosh-extension.json +65 -0
  22. package/adapters/tokenless/common/hooks/compress_response_hook.py +487 -0
  23. package/adapters/tokenless/common/hooks/compress_schema_hook.py +144 -0
  24. package/adapters/tokenless/common/hooks/compress_toon_hook.py +160 -0
  25. package/adapters/tokenless/common/hooks/hook_utils.py +584 -0
  26. package/adapters/tokenless/common/hooks/rewrite_hook.py +223 -0
  27. package/adapters/tokenless/common/hooks/run-hook.sh +62 -0
  28. package/adapters/tokenless/common/hooks/tool_categories.json +99 -0
  29. package/adapters/tokenless/common/hooks/tool_ready_hook.sh +571 -0
  30. package/adapters/tokenless/common/tokenless-env-fix.sh +730 -0
  31. package/adapters/tokenless/common/tool-ready-spec.json +113 -0
  32. package/adapters/tokenless/dsh/cordis.patch.yml +8 -0
  33. package/adapters/tokenless/dsh/dist/index.js +399 -0
  34. package/adapters/tokenless/dsh/package.json +26 -0
  35. package/adapters/tokenless/hermes/__init__.py +572 -0
  36. package/adapters/tokenless/hermes/plugin.yaml +9 -0
  37. package/adapters/tokenless/hermes/scripts/detect.sh +80 -0
  38. package/adapters/tokenless/hermes/scripts/install.sh +60 -0
  39. package/adapters/tokenless/hermes/scripts/uninstall.sh +45 -0
  40. package/adapters/tokenless/manifest.json +147 -0
  41. package/adapters/tokenless/openclaw/dist/index.d.ts +27 -0
  42. package/adapters/tokenless/openclaw/dist/index.js +598 -0
  43. package/adapters/tokenless/openclaw/dist/tool_categories.json +99 -0
  44. package/adapters/tokenless/openclaw/index.ts +720 -0
  45. package/adapters/tokenless/openclaw/openclaw.plugin.json +40 -0
  46. package/adapters/tokenless/openclaw/package.json +24 -0
  47. package/adapters/tokenless/openclaw/scripts/detect.sh +102 -0
  48. package/adapters/tokenless/openclaw/scripts/install.sh +75 -0
  49. package/adapters/tokenless/openclaw/scripts/uninstall.sh +46 -0
  50. package/adapters/tokenless/openclaw/tsconfig.json +13 -0
  51. package/adapters/tokenless/opencode/plugin.js +246 -0
  52. package/adapters/tokenless/opencode/scripts/detect.sh +38 -0
  53. package/adapters/tokenless/opencode/scripts/install.sh +56 -0
  54. package/adapters/tokenless/opencode/scripts/uninstall.sh +29 -0
  55. package/adapters/tokenless/qoder/.qoder-plugin/plugin.json +11 -0
  56. package/adapters/tokenless/qoder/commands/tokenless-stats.md +8 -0
  57. package/adapters/tokenless/qoder/hooks/hooks.json +36 -0
  58. package/adapters/tokenless/qoder/hooks/run-hook.sh +51 -0
  59. package/adapters/tokenless/qoder/scripts/detect.sh +35 -0
  60. package/adapters/tokenless/qoder/scripts/install.sh +136 -0
  61. package/adapters/tokenless/qoder/scripts/uninstall.sh +106 -0
  62. package/adapters/tokenless/qwencode/qwen-extension.json +69 -0
  63. package/adapters/tokenless/qwencode/scripts/detect.sh +125 -0
  64. package/adapters/tokenless/qwencode/scripts/install.sh +128 -0
  65. package/adapters/tokenless/qwencode/scripts/uninstall.sh +62 -0
  66. package/bin/rtk +6 -0
  67. package/bin/tokenless +6 -0
  68. package/bin/toon +6 -0
  69. package/package.json +57 -0
  70. package/scripts/postinstall.js +208 -0
@@ -0,0 +1,151 @@
1
+ #!/usr/bin/env bash
2
+ # Install the Tokenless plugin for Codex.
3
+ #
4
+ # Two-phase installation:
5
+ # Phase 1 — Binary: verify tokenless CLI is reachable (RPM or dev build).
6
+ # Phase 2 — Plugin: create a local codex marketplace, register it, and
7
+ # install the tokenless plugin via `codex plugin add`.
8
+ #
9
+ # Codex marketplace layout:
10
+ #
11
+ # codex-marketplace/ ← marketplace root
12
+ # ├── .agents/
13
+ # │ └── plugins/
14
+ # │ └── marketplace.json ← manifest
15
+ # └── tokenless/ ← symlink to the actual plugin dir
16
+ # ├── plugin.json
17
+ # ├── hooks/
18
+ # └── scripts/
19
+ #
20
+ # The plugin path in marketplace.json ("./tokenless") is resolved relative
21
+ # to the marketplace root, not the manifest file.
22
+ #
23
+ # Usage:
24
+ # ./install.sh # Interactive
25
+ # ./install.sh --non-interactive # CI / automated
26
+ #
27
+ # Environment variables:
28
+ # TOKENLESS_INSTALL_PREFIX Installation prefix (default: ~/.local)
29
+ # TOKENLESS_SOURCE_DIR Path to anolisa/src/tokenless (dev-only override)
30
+ # TOKENLESS_SKIP_BUILD Set to "1" to skip cargo build
31
+ # CODEX_BIN Path to codex CLI (default: codex)
32
+
33
+ set -euo pipefail
34
+
35
+ PREFIX="${TOKENLESS_INSTALL_PREFIX:-$HOME/.local}"
36
+ BINDIR="$PREFIX/bin"
37
+ CODEX_BIN="${CODEX_BIN:-}"
38
+ MARKETPLACE_NAME="anolisa-tokenless"
39
+
40
+ # Resolve codex binary via shared helper (see _common.sh).
41
+
42
+ # Resolve the codex plugin source directory.
43
+ SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
44
+ # shellcheck source=./_common.sh
45
+ source "$SCRIPT_DIR/_common.sh"
46
+ PLUGIN_SRC="$(cd "$SCRIPT_DIR/.." && pwd)" # codex/ directory
47
+
48
+ # ---------------------------------------------------------------------------
49
+ # Phase 1 — Ensure tokenless binary is available
50
+ # ---------------------------------------------------------------------------
51
+
52
+ TOKENLESS_BIN=""
53
+ if command -v tokenless >/dev/null 2>&1; then
54
+ TOKENLESS_BIN="$(command -v tokenless)"
55
+ elif [[ -x "$BINDIR/tokenless" ]]; then
56
+ TOKENLESS_BIN="$BINDIR/tokenless"
57
+ elif [[ -x /usr/local/bin/tokenless ]]; then
58
+ TOKENLESS_BIN="/usr/local/bin/tokenless"
59
+ elif [[ -x /usr/bin/tokenless ]]; then
60
+ TOKENLESS_BIN="/usr/bin/tokenless"
61
+ fi
62
+
63
+ if [[ -z "$TOKENLESS_BIN" ]]; then
64
+ # No binary — try building from source (dev scenario)
65
+ if [[ -n "${TOKENLESS_SOURCE_DIR:-}" ]]; then
66
+ SRCDIR="$TOKENLESS_SOURCE_DIR"
67
+ else
68
+ SRCDIR="$(cd "$SCRIPT_DIR/../../../.." && pwd)"
69
+ fi
70
+
71
+ if [[ ! -f "$SRCDIR/Cargo.toml" ]]; then
72
+ echo "[tokenless] ERROR: tokenless binary not found and no source tree at $SRCDIR" >&2
73
+ echo "[tokenless] Install the tokenless RPM package first:" >&2
74
+ echo "[tokenless] rpm -Uvh tokenless-*.rpm" >&2
75
+ exit 1
76
+ fi
77
+
78
+ echo "[tokenless] Building tokenless from source: $SRCDIR"
79
+ cd "$SRCDIR"
80
+ cargo build --release -p tokenless-cli 2>&1 | tail -3
81
+ mkdir -p "$BINDIR"
82
+ cp "$SRCDIR/target/release/tokenless" "$BINDIR/tokenless"
83
+ chmod 755 "$BINDIR/tokenless"
84
+ echo "[tokenless] Built and installed to $BINDIR/tokenless"
85
+ else
86
+ echo "[tokenless] Binary: $TOKENLESS_BIN ($("$TOKENLESS_BIN" --version))"
87
+ fi
88
+
89
+ # ---------------------------------------------------------------------------
90
+ # Phase 2 — Register plugin with codex
91
+ # ---------------------------------------------------------------------------
92
+
93
+ # 2a. Resolve codex and check availability
94
+ CODEX_BIN="$(resolve_codex)"
95
+ if [[ -z "$CODEX_BIN" ]]; then
96
+ echo "[tokenless] codex CLI not found — skipping plugin registration."
97
+ echo "[tokenless] Install codex first, then run: codex plugin add tokenless@${MARKETPLACE_NAME}"
98
+ exit 0
99
+ fi
100
+
101
+ # 2a. Create marketplace directory layout in a user-local location so
102
+ # non-root users can run this script even when the plugin source tree
103
+ # is installed under a root-owned prefix (e.g. /usr/share/anolisa).
104
+ MARKETPLACE_DIR="${XDG_DATA_HOME:-$HOME/.local/share}/anolisa/codex-marketplace"
105
+ AGENTS_DIR="$MARKETPLACE_DIR/.agents/plugins"
106
+ mkdir -p "$AGENTS_DIR"
107
+
108
+ # Symlink plugin at marketplace root (codex resolves plugin paths relative to
109
+ # the marketplace root, not the manifest file).
110
+ PLUGIN_LINK="$MARKETPLACE_DIR/tokenless"
111
+ if [[ ! -L "$PLUGIN_LINK" ]] || [[ "$(readlink "$PLUGIN_LINK")" != "$PLUGIN_SRC" ]]; then
112
+ rm -f "$PLUGIN_LINK"
113
+ ln -sfn "$PLUGIN_SRC" "$PLUGIN_LINK"
114
+ fi
115
+
116
+ # 2b. Write marketplace.json at .agents/plugins/marketplace.json
117
+ cat > "$AGENTS_DIR/marketplace.json" <<JSON
118
+ {
119
+ "name": "${MARKETPLACE_NAME}",
120
+ "interface": {
121
+ "displayName": "ANOLISA Tokenless"
122
+ },
123
+ "plugins": [
124
+ {
125
+ "name": "tokenless",
126
+ "source": {
127
+ "source": "local",
128
+ "path": "./tokenless"
129
+ },
130
+ "policy": {
131
+ "installation": "AVAILABLE"
132
+ },
133
+ "category": "developer-tools"
134
+ }
135
+ ]
136
+ }
137
+ JSON
138
+
139
+ # 2c. Register marketplace (idempotent — remove first if exists)
140
+ if "$CODEX_BIN" plugin marketplace list 2>/dev/null | grep -q "^${MARKETPLACE_NAME}[[:space:]]"; then
141
+ "$CODEX_BIN" plugin marketplace remove "$MARKETPLACE_NAME" 2>/dev/null || true
142
+ fi
143
+ echo "[tokenless] Registering marketplace '${MARKETPLACE_NAME}'..."
144
+ "$CODEX_BIN" plugin marketplace add "$MARKETPLACE_DIR" 2>&1
145
+
146
+ # 2d. Install the plugin from marketplace
147
+ echo "[tokenless] Installing plugin 'tokenless@${MARKETPLACE_NAME}'..."
148
+ "$CODEX_BIN" plugin add "tokenless@${MARKETPLACE_NAME}" 2>&1
149
+
150
+ echo "[tokenless] Codex plugin installation complete."
151
+ echo "[tokenless] Verify: codex plugin list"
@@ -0,0 +1,282 @@
1
+ #!/usr/bin/env python3
2
+ """Codex PreToolUse RTK command rewriting hook.
3
+
4
+ Reads a PreToolUse hook request from stdin, extracts the shell command,
5
+ invokes ``rtk rewrite`` for token optimization, and returns the rewritten
6
+ command via ``updatedInput``.
7
+
8
+ Protocol:
9
+ stdin ← codex PreToolUse JSON (tool_name, tool_input.command, ...)
10
+ stdout → {"hookSpecificOutput": {"hookEventName": "PreToolUse",
11
+ "permissionDecision": "allow",
12
+ "updatedInput": {...}}}
13
+
14
+ rtk exit code protocol:
15
+ 0 = rewrite available → apply
16
+ 1 = no RTK equivalent → skip (passthrough)
17
+ 2 = deny rule matched → skip
18
+ 3 = Ask/Default → apply (non-interactive context)
19
+
20
+ Fail-open: all error paths exit 0 with empty stdout (passthrough).
21
+ """
22
+
23
+ import json
24
+ import os
25
+ import shutil
26
+ import subprocess
27
+ import sys
28
+ from typing import Optional
29
+
30
+ # Resolve shared hook utilities (common/hooks/) with FHS fallback paths.
31
+ # Primary: relative path (works for source-tree and FHS/RPM install).
32
+ # Fallbacks: system and user FHS paths (works when Codex caches only the
33
+ # plugin subdirectory and the relative path resolves outside the cache).
34
+ #
35
+ # Trust model (aligned with bash is_trusted_file / Rust is_trusted_path):
36
+ # - System FHS paths (/usr/share, /usr/local/share) are unconditional.
37
+ # - User-relative paths use passwd DB home (NOT $HOME — env-controllable),
38
+ # with ownership and parent-dir permission checks.
39
+ # - Relative ../../common/hooks is trusted only if the resolved target
40
+ # exists and is under a system prefix or a passwd-validated home path.
41
+ _HERE = os.path.dirname(os.path.abspath(__file__))
42
+
43
+
44
+ def _is_trusted_dir(path: str) -> bool:
45
+ """Check whether a directory is trusted for Python module imports.
46
+
47
+ Mirrors the trust criteria in bash tool_ready_hook.sh (is_trusted_file)
48
+ and Rust env_check.rs (is_trusted_path): system paths unconditional,
49
+ user paths validated via passwd + ownership + parent permission checks.
50
+
51
+ NOTE: Callers should pass realpath-resolved paths (not just abspath)
52
+ so that symlink targets are validated before the trust check — a
53
+ symlinked /usr/share/anolisa → /tmp/ would bypass the prefix check
54
+ if only abspath is used.
55
+ """
56
+ # System FHS prefixes are always trusted
57
+ for prefix in ("/usr/share/", "/usr/local/share/", "/usr/libexec/", "/usr/lib/anolisa/"):
58
+ if path.startswith(prefix):
59
+ return True
60
+ # For paths outside system prefixes (e.g. source-tree cloned to
61
+ # /opt/anolisa/), trust them if owned by current uid or root AND
62
+ # parent directory is not world-writable. This covers developer
63
+ # worktrees without requiring the path to be under passwd home.
64
+ try:
65
+ st = os.stat(path)
66
+ except OSError:
67
+ return False
68
+ if st.st_uid != os.getuid() and st.st_uid != 0:
69
+ # Not owned by us or root — refuse. Home paths must be owned by
70
+ # us or root; source-tree paths outside system prefixes are
71
+ # already covered by the ownership check above (current uid/root).
72
+ return False
73
+ # Ownership OK — check parent directory is not world-writable
74
+ parent = os.path.dirname(path)
75
+ try:
76
+ pst = os.stat(parent)
77
+ except OSError:
78
+ return False
79
+ if pst.st_uid != os.getuid() and pst.st_uid != 0:
80
+ return False
81
+ if pst.st_mode & 0o002: # world-writable
82
+ return False
83
+ return True
84
+
85
+
86
+ # Resolve real home from passwd DB for user-install fallback path.
87
+ # Falls back to "" (empty) when unavailable — candidate is then skipped.
88
+ try:
89
+ import pwd as _pwd
90
+ _REAL_HOME = _pwd.getpwuid(os.getuid()).pw_dir
91
+ except (ImportError, KeyError):
92
+ _REAL_HOME = ""
93
+ if not os.path.isabs(_REAL_HOME):
94
+ _REAL_HOME = ""
95
+
96
+
97
+ def _user_path(*parts: str) -> str:
98
+ return os.path.join(_REAL_HOME, *parts) if _REAL_HOME else ""
99
+
100
+
101
+ _HOOK_UTILS_CANDIDATES = [
102
+ os.path.join(_HERE, "..", "..", "common", "hooks"), # source-tree / FHS
103
+ "/usr/share/anolisa/adapters/tokenless/common/hooks", # RPM system
104
+ "/usr/local/share/anolisa/adapters/tokenless/common/hooks", # manual system
105
+ os.path.join(_REAL_HOME, ".local", "share",
106
+ "anolisa", "adapters", "tokenless", "common", "hooks") if _REAL_HOME else "",
107
+ ]
108
+ _HOOK_UTILS_RESOLVED = ""
109
+ for _C in _HOOK_UTILS_CANDIDATES:
110
+ if _C and _is_trusted_dir(os.path.realpath(_C)) and os.path.isdir(_C):
111
+ _HOOK_UTILS_RESOLVED = os.path.realpath(_C)
112
+ sys.path.insert(0, _C)
113
+ break
114
+
115
+ if _HOOK_UTILS_RESOLVED:
116
+ from hook_utils import parse_version as _parse_version, skip_silent as _skip, write_context as _write_context # noqa: E402
117
+ else:
118
+ # Fail-open: no trusted hook_utils found → provide inline fallbacks
119
+ # so the hook script exits 0 (passthrough) rather than ImportError → exit 1.
120
+ import re as _re
121
+
122
+ def _skip() -> None: # type: ignore[assignment,misc]
123
+ sys.exit(0)
124
+
125
+ def _parse_version(version_str: str) -> tuple | None: # type: ignore[assignment,misc]
126
+ m = _re.search(r"(\d+)\.(\d+)\.(\d+)", version_str)
127
+ if m:
128
+ return (int(m.group(1)), int(m.group(2)), int(m.group(3)))
129
+ return None
130
+
131
+ _WRITE_CONTEXT_WARNED = False
132
+
133
+ def _write_context(agent_id: str, session_id: str, tool_use_id: str) -> None: # type: ignore[assignment,misc]
134
+ global _WRITE_CONTEXT_WARNED
135
+ if not _WRITE_CONTEXT_WARNED:
136
+ print("[tokenless] WARNING: rtk session tracking unavailable (hook_utils not loaded, rewrite still works)", file=sys.stderr)
137
+ _WRITE_CONTEXT_WARNED = True
138
+
139
+ # ---------------------------------------------------------------------------
140
+ # constants
141
+ # ---------------------------------------------------------------------------
142
+
143
+ AGENT_ID = os.environ.get("TOKENLESS_AGENT_ID", "codex")
144
+ _MIN_RTK_VERSION = (0, 35, 0)
145
+
146
+ # RTK binary search paths. Codex can cache this script without common/hooks;
147
+ # keep this standalone list aligned with hook_utils.py::_known_binary_paths.
148
+ _RTK_FALLBACK = "/usr/libexec/anolisa/tokenless/rtk"
149
+ _RTK_FALLBACK_DEBIAN = "/usr/lib/anolisa/tokenless/rtk"
150
+ _RTK_SYSTEM_BIN = "/usr/local/bin/rtk"
151
+ _RTK_SYSTEM_LIBEXEC = "/usr/local/libexec/anolisa/tokenless/rtk"
152
+ _RTK_RPM_BIN = "/usr/bin/rtk"
153
+ _RTK_LOCAL_BIN = _user_path(".local", "bin", "rtk")
154
+ _RTK_ANOLISA_USER = _user_path(
155
+ ".local",
156
+ "lib",
157
+ "anolisa",
158
+ "libexec",
159
+ "tokenless",
160
+ "rtk",
161
+ )
162
+ _RTK_MAKE_USER = _user_path(".local", "libexec", "anolisa", "tokenless", "rtk")
163
+ _RTK_LOCAL_SHARE = _user_path(".local", "share", "anolisa", "tokenless", "rtk")
164
+ _RTK_LOCAL_LIB = _user_path(".local", "lib", "anolisa", "tokenless", "rtk")
165
+
166
+ # ---------------------------------------------------------------------------
167
+ # helpers
168
+ # ---------------------------------------------------------------------------
169
+
170
+
171
+ def _find_rtk() -> Optional[str]:
172
+ """Locate the rtk binary."""
173
+ path = shutil.which("rtk")
174
+ if path:
175
+ return path
176
+ for fp in (
177
+ _RTK_LOCAL_BIN,
178
+ _RTK_ANOLISA_USER,
179
+ _RTK_MAKE_USER,
180
+ _RTK_SYSTEM_BIN,
181
+ _RTK_SYSTEM_LIBEXEC,
182
+ _RTK_RPM_BIN,
183
+ _RTK_FALLBACK,
184
+ _RTK_FALLBACK_DEBIAN,
185
+ _RTK_LOCAL_SHARE,
186
+ _RTK_LOCAL_LIB,
187
+ ):
188
+ if os.path.isfile(fp) and os.access(fp, os.X_OK):
189
+ return fp
190
+ return None
191
+
192
+
193
+ # ---------------------------------------------------------------------------
194
+ # main
195
+ # ---------------------------------------------------------------------------
196
+
197
+
198
+ def main() -> None:
199
+ # 1. Resolve rtk binary
200
+ rtk_bin = _find_rtk()
201
+ if not rtk_bin:
202
+ _skip()
203
+
204
+ # 2. Version guard
205
+ try:
206
+ result = subprocess.run(
207
+ [rtk_bin, "--version"],
208
+ capture_output=True, text=True, timeout=3,
209
+ )
210
+ ver = _parse_version(result.stdout)
211
+ if ver and ver < _MIN_RTK_VERSION:
212
+ _skip()
213
+ except Exception:
214
+ _skip()
215
+
216
+ # 3. Read stdin JSON
217
+ try:
218
+ raw = sys.stdin.read()
219
+ if not raw.strip():
220
+ _skip()
221
+ input_data = json.loads(raw)
222
+ except (json.JSONDecodeError, EOFError, ValueError):
223
+ _skip()
224
+
225
+ # 4. Extract command from tool_input
226
+ tool_name = input_data.get("tool_name", "")
227
+ tool_input = input_data.get("tool_input", {})
228
+ if isinstance(tool_input, str):
229
+ try:
230
+ tool_input = json.loads(tool_input)
231
+ except (json.JSONDecodeError, ValueError):
232
+ _skip()
233
+ cmd = tool_input.get("command", "")
234
+ if not cmd:
235
+ _skip()
236
+
237
+ # 5. Set up RTK env context for stats
238
+ session_id = input_data.get("session_id", "")
239
+ tool_use_id = input_data.get("tool_use_id", "")
240
+
241
+ env = os.environ.copy()
242
+ env["TOKENLESS_AGENT_ID"] = AGENT_ID
243
+ if session_id:
244
+ env["TOKENLESS_SESSION_ID"] = session_id
245
+ if tool_use_id:
246
+ env["TOKENLESS_TOOL_USE_ID"] = tool_use_id
247
+
248
+ _write_context(AGENT_ID, session_id, tool_use_id)
249
+
250
+ # 6. Rewrite via rtk
251
+ try:
252
+ proc = subprocess.run(
253
+ [rtk_bin, "rewrite", cmd],
254
+ capture_output=True, text=True, timeout=5, env=env,
255
+ )
256
+ except Exception:
257
+ _skip()
258
+
259
+ # 7. Interpret rtk exit code
260
+ if proc.returncode in (1, 2):
261
+ _skip()
262
+
263
+ rewritten = proc.stdout.strip()
264
+ if not rewritten or rewritten == cmd:
265
+ _skip()
266
+
267
+ # 8. Build codex PreToolUse response with updatedInput
268
+ updated_input = dict(tool_input)
269
+ updated_input["command"] = rewritten
270
+
271
+ response = {
272
+ "hookSpecificOutput": {
273
+ "hookEventName": "PreToolUse",
274
+ "permissionDecision": "allow",
275
+ "updatedInput": updated_input,
276
+ }
277
+ }
278
+ print(json.dumps(response, ensure_ascii=False))
279
+
280
+
281
+ if __name__ == "__main__":
282
+ main()