anolisa-tokenless 0.7.13 → 0.8.0

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 (34) hide show
  1. package/README.md +219 -87
  2. package/adapters/tokenless/claude-code/.claude-plugin/plugin.json +1 -1
  3. package/adapters/tokenless/claude-code/hooks/run-hook.sh +62 -0
  4. package/adapters/tokenless/codex/.codex-plugin/plugin.json +5 -4
  5. package/adapters/tokenless/codex/README.md +22 -32
  6. package/adapters/tokenless/codex/hooks/hooks.json +3 -3
  7. package/adapters/tokenless/codex/scripts/response-diagnostics +170 -0
  8. package/adapters/tokenless/common/cosh-extension.json +4 -4
  9. package/adapters/tokenless/common/hooks/compress_response_hook.py +250 -307
  10. package/adapters/tokenless/common/hooks/compress_schema_hook.py +34 -42
  11. package/adapters/tokenless/common/hooks/hook_utils.py +281 -44
  12. package/adapters/tokenless/common/hooks/rewrite_hook.py +53 -169
  13. package/adapters/tokenless/dsh/dist/index.js +353 -264
  14. package/adapters/tokenless/dsh/package.json +2 -2
  15. package/adapters/tokenless/hermes/__init__.py +191 -355
  16. package/adapters/tokenless/hermes/plugin.yaml +2 -2
  17. package/adapters/tokenless/manifest.json +18 -3
  18. package/adapters/tokenless/openclaw/dist/index.d.ts +4 -16
  19. package/adapters/tokenless/openclaw/dist/index.js +291 -507
  20. package/adapters/tokenless/openclaw/index.ts +408 -628
  21. package/adapters/tokenless/openclaw/openclaw.plugin.json +4 -20
  22. package/adapters/tokenless/openclaw/package.json +6 -4
  23. package/adapters/tokenless/qoder/.qoder-plugin/plugin.json +1 -1
  24. package/adapters/tokenless/qwencode/hooks/run-hook.sh +62 -0
  25. package/adapters/tokenless/qwencode/qwen-extension.json +4 -4
  26. package/adapters/tokenless/qwenpaw/plugin.json +17 -0
  27. package/adapters/tokenless/qwenpaw/plugin.py +390 -0
  28. package/adapters/tokenless/qwenpaw/requirements.txt +6 -0
  29. package/adapters/tokenless/qwenpaw/scripts/detect.sh +131 -0
  30. package/adapters/tokenless/qwenpaw/scripts/install.sh +98 -0
  31. package/adapters/tokenless/qwenpaw/scripts/uninstall.sh +61 -0
  32. package/package.json +5 -5
  33. package/adapters/tokenless/codex/scripts/compress-response +0 -445
  34. package/adapters/tokenless/common/hooks/compress_toon_hook.py +0 -171
@@ -1,445 +0,0 @@
1
- #!/usr/bin/env python3
2
- """Codex PostToolUse compression hook for the Tokenless plugin.
3
-
4
- Reads a PostToolUse hook request from stdin, compresses the tool response
5
- via ``tokenless compress-response`` and optional TOON encoding, then
6
- injects a compressed summary and environment error classification as
7
- ``additionalContext``.
8
-
9
- Protocol:
10
- stdin ← codex PostToolUse JSON (tool_name, tool_response, ...)
11
- stdout → {"hookSpecificOutput": {"hookEventName": "PostToolUse",
12
- "additionalContext": "..."}}
13
-
14
- Design notes for Codex:
15
- - PostToolUse CANNOT suppress the original tool output (``suppressOutput``
16
- is rejected by the codex hook engine for this event).
17
- - ``additionalContext`` is additive — the model sees both the original
18
- output AND the injected context.
19
- - Therefore we inject a compressed *summary* rather than duplicating the
20
- full content, plus environment error classification with fix hints.
21
- - For very large JSON responses (> 4 KB), we include the full compressed
22
- content so the model has a compact alternative to the raw output.
23
-
24
- Fail-open: every error path silently passes through (exit 0 with empty
25
- stdout) so the agent session is never blocked by a compression failure.
26
- """
27
-
28
- import json
29
- import os
30
- import shutil
31
- import subprocess
32
- import sys
33
- from typing import Optional
34
-
35
- # Resolve shared hook utilities (common/hooks/) with FHS fallback paths.
36
- # Primary: relative path (works for source-tree and FHS/RPM install).
37
- # Fallbacks: system and user FHS paths (works when Codex caches only the
38
- # plugin subdirectory and the relative path resolves outside the cache).
39
- #
40
- # Trust model (aligned with bash is_trusted_file / Rust is_trusted_path):
41
- # - System FHS paths (/usr/share, /usr/local/share) are unconditional.
42
- # - User-relative paths use passwd DB home (NOT $HOME — env-controllable),
43
- # with ownership and parent-dir permission checks.
44
- # - Relative ../../common/hooks is trusted only if the resolved target
45
- # exists and is under a system prefix or a passwd-validated home path.
46
- _HERE = os.path.dirname(os.path.abspath(__file__))
47
-
48
-
49
- def _is_trusted_dir(path: str) -> bool:
50
- """Check whether a directory is trusted for Python module imports.
51
-
52
- Mirrors the trust criteria in bash tool_ready_hook.sh (is_trusted_file)
53
- and Rust env_check.rs (is_trusted_path): system paths unconditional,
54
- user paths validated via passwd + ownership + parent permission checks.
55
-
56
- NOTE: Callers should pass realpath-resolved paths (not just abspath)
57
- so that symlink targets are validated before the trust check — a
58
- symlinked /usr/share/anolisa → /tmp/ would bypass the prefix check
59
- if only abspath is used.
60
- """
61
- # System FHS prefixes are always trusted
62
- for prefix in ("/usr/share/", "/usr/local/share/", "/usr/libexec/", "/usr/lib/anolisa/"):
63
- if path.startswith(prefix):
64
- return True
65
- # For paths outside system prefixes (e.g. source-tree cloned to
66
- # /opt/anolisa/), trust them if owned by current uid or root AND
67
- # parent directory is not world-writable. This covers developer
68
- # worktrees without requiring the path to be under passwd home.
69
- try:
70
- st = os.stat(path)
71
- except OSError:
72
- return False
73
- if st.st_uid != os.getuid() and st.st_uid != 0:
74
- # Not owned by us or root — refuse. Home paths must be owned by
75
- # us or root; source-tree paths outside system prefixes are
76
- # already covered by the ownership check above (current uid/root).
77
- return False
78
- # Ownership OK — check parent directory is not world-writable
79
- parent = os.path.dirname(path)
80
- try:
81
- pst = os.stat(parent)
82
- except OSError:
83
- return False
84
- if pst.st_uid != os.getuid() and pst.st_uid != 0:
85
- return False
86
- if pst.st_mode & 0o002: # world-writable
87
- return False
88
- return True
89
-
90
-
91
- # Resolve real home from passwd DB for user-install fallback path.
92
- # Falls back to "" (empty) when unavailable — candidate is then skipped.
93
- try:
94
- import pwd as _pwd
95
- _REAL_HOME = _pwd.getpwuid(os.getuid()).pw_dir
96
- except (ImportError, KeyError):
97
- _REAL_HOME = ""
98
- if not os.path.isabs(_REAL_HOME):
99
- _REAL_HOME = ""
100
-
101
-
102
- def _user_path(*parts: str) -> str:
103
- return os.path.join(_REAL_HOME, *parts) if _REAL_HOME else ""
104
-
105
-
106
- _HOOK_UTILS_CANDIDATES = [
107
- os.path.join(_HERE, "..", "..", "common", "hooks"), # source-tree / FHS
108
- "/usr/share/anolisa/adapters/tokenless/common/hooks", # RPM system
109
- "/usr/local/share/anolisa/adapters/tokenless/common/hooks", # manual system
110
- os.path.join(_REAL_HOME, ".local", "share",
111
- "anolisa", "adapters", "tokenless", "common", "hooks") if _REAL_HOME else "",
112
- ]
113
- _HOOK_UTILS_RESOLVED = ""
114
- for _C in _HOOK_UTILS_CANDIDATES:
115
- if _C and _is_trusted_dir(os.path.realpath(_C)) and os.path.isdir(_C):
116
- _HOOK_UTILS_RESOLVED = os.path.realpath(_C)
117
- sys.path.insert(0, _C)
118
- break
119
-
120
- if _HOOK_UTILS_RESOLVED:
121
- from hook_utils import ( # noqa: E402
122
- skip_silent as _skip,
123
- try_parse_json as _try_parse_json,
124
- warn as _warn,
125
- classify_env_error as _classify_env_error,
126
- get_thresholds as _get_thresholds,
127
- SKIP_TOOLS as _SKIP_TOOLS_SHARED,
128
- )
129
- else:
130
- # Fail-open: no trusted hook_utils found → provide inline fallbacks
131
- # so the hook script exits 0 (passthrough) rather than ImportError → exit 1.
132
- def _skip() -> None: # type: ignore[assignment,misc]
133
- sys.exit(0)
134
-
135
- def _try_parse_json(data: str) -> object | None: # type: ignore[assignment,misc]
136
- try:
137
- return json.loads(data)
138
- except (json.JSONDecodeError, ValueError):
139
- return None
140
-
141
- def _warn(msg: str) -> None: # type: ignore[assignment,misc]
142
- print(f"[tokenless] WARNING: {msg}", file=sys.stderr)
143
-
144
- def _classify_env_error(tool_response) -> tuple[str | None, str | None]: # type: ignore[assignment,misc]
145
- # Classification unavailable without hook_utils — return no-op.
146
- # Caller handles (None, None) by skipping env error injection.
147
- return None, None
148
-
149
- def _get_thresholds(tool_name: str) -> tuple[int, int, int]: # type: ignore[assignment,misc]
150
- # Hardcoded defaults mirroring hook_utils.get_thresholds().
151
- # Keep in sync with common/hooks/hook_utils.py thresholds.
152
- return (65_536, 128, 8) if tool_name in {"Bash", "bash", "Shell", "shell", "exec", "terminal"} else (1_048_576, 65_536, 32)
153
-
154
- _SKIP_TOOLS_SHARED: set[str] = { # type: ignore[assignment,misc]
155
- "Read", "read", "read_file", "read_many_files",
156
- "Glob", "glob", "list_directory",
157
- "Grep", "grep", "grep_search", "search_files",
158
- "Lsp", "lsp",
159
- "NotebookRead", "notebook_read", "notebookread",
160
- }
161
-
162
- # ---------------------------------------------------------------------------
163
- # constants
164
- # ---------------------------------------------------------------------------
165
-
166
- AGENT_ID = os.environ.get("TOKENLESS_AGENT_ID", "codex")
167
- MIN_RESPONSE_CHARS: int = 500
168
- LARGE_RESPONSE_CHARS: int = 4000
169
- # TOON on small JSON saves only a few characters (observed ~0.3% below
170
- # ~500 chars) while the per-event encode cost stays the same, so payloads
171
- # under this threshold keep the compressed form and skip the TOON pass.
172
- MIN_TOON_CHARS: int = 500
173
-
174
- # 3-layer compression strategy:
175
- # Layer 1: Content retrieval + task management → skip all compression
176
- # Layer 2: Shell/exec → moderate truncation (64K/128/8)
177
- # Layer 3: API/structured → zero-truncation (1M/64K/32)
178
-
179
- # Layer 1: content retrieval (shared) + codex-specific task management
180
- SKIP_TOOLS: set[str] = _SKIP_TOOLS_SHARED | {
181
- "TodoWrite", "Task", "TaskStatus",
182
- }
183
-
184
- # Cap for compressed content included in additionalContext (large responses only).
185
- # Layer 2 thresholds (64K strings) preserve 95% of real shell output; Layer 3
186
- # zero-truncation (1M strings) can still produce large payloads for API tools.
187
- COMPRESSED_CONTENT_CAP: int = 32_768
188
-
189
- # -- tokenless binary search paths (ANOLISA FHS spec) ------------------------
190
- # Codex can cache this script without common/hooks. Keep this standalone list
191
- # aligned with hook_utils.py::_known_binary_paths.
192
-
193
- _TOKENLESS_FALLBACK = "/usr/bin/tokenless"
194
- _TOKENLESS_SYSTEM_FALLBACK = "/usr/local/bin/tokenless"
195
- _TOKENLESS_LOCAL_BIN = _user_path(".local", "bin", "tokenless")
196
- _TOKENLESS_LOCAL_SHARE = _user_path(
197
- ".local", "share", "anolisa", "tokenless", "tokenless"
198
- )
199
- _TOKENLESS_LOCAL_LIB = _user_path(
200
- ".local", "lib", "anolisa", "tokenless", "tokenless"
201
- )
202
-
203
- # ---------------------------------------------------------------------------
204
- # helpers
205
- # ---------------------------------------------------------------------------
206
-
207
-
208
- def _find_tokenless() -> Optional[str]:
209
- """Locate the tokenless CLI binary."""
210
- explicit = os.environ.get("TOKENLESS_BIN", "")
211
- if explicit and os.path.isfile(explicit) and os.access(explicit, os.X_OK):
212
- return explicit
213
-
214
- path = shutil.which("tokenless")
215
- if path:
216
- return path
217
-
218
- for fp in (
219
- _TOKENLESS_LOCAL_BIN,
220
- _TOKENLESS_SYSTEM_FALLBACK,
221
- _TOKENLESS_FALLBACK,
222
- _TOKENLESS_LOCAL_SHARE,
223
- _TOKENLESS_LOCAL_LIB,
224
- ):
225
- if os.path.isfile(fp) and os.access(fp, os.X_OK):
226
- return fp
227
- return None
228
-
229
-
230
- def _is_json_serializable(obj) -> bool:
231
- """Check if a value is a JSON object or array (compressible)."""
232
- return isinstance(obj, (dict, list))
233
-
234
-
235
- def _extract_text_for_compression(tool_response) -> Optional[str]:
236
- """Extract a JSON string from tool_response for compression."""
237
- if isinstance(tool_response, str):
238
- return tool_response
239
- if isinstance(tool_response, (dict, list)):
240
- return json.dumps(tool_response, separators=(",", ":"), ensure_ascii=False)
241
- return None
242
-
243
-
244
- def _run_tokenless(tokenless_bin: str, subcommand: str, input_data: str,
245
- session_id: str = "", tool_use_id: str = "",
246
- timeout: int = 10,
247
- extra_args: Optional[list[str]] = None) -> Optional[str]:
248
- """Run a tokenless subcommand with input, return stdout or None on failure."""
249
- cmd = [tokenless_bin, subcommand, "--agent-id", AGENT_ID]
250
- if extra_args:
251
- cmd.extend(extra_args)
252
- if session_id:
253
- cmd.extend(["--session-id", session_id])
254
- if tool_use_id:
255
- cmd.extend(["--tool-use-id", tool_use_id])
256
-
257
- try:
258
- proc = subprocess.run(
259
- cmd,
260
- input=input_data,
261
- capture_output=True,
262
- text=True,
263
- timeout=timeout,
264
- )
265
- if proc.returncode == 0 and proc.stdout.strip():
266
- return proc.stdout.strip()
267
- if proc.returncode != 0 and proc.stderr:
268
- _warn(f"{subcommand}: {proc.stderr.strip()[:200]}")
269
- except subprocess.TimeoutExpired:
270
- _warn(f"{subcommand} timed out after {timeout}s")
271
- except OSError as e:
272
- _warn(f"{subcommand} OS error: {e}")
273
- return None
274
-
275
-
276
- # ---------------------------------------------------------------------------
277
- # main
278
- # ---------------------------------------------------------------------------
279
-
280
-
281
- def main() -> None:
282
- # 1. Locate tokenless binary (fail-open: skip if not installed)
283
- tokenless_bin = _find_tokenless()
284
- if not tokenless_bin:
285
- _skip()
286
-
287
- # 2. Read and parse stdin JSON from codex
288
- try:
289
- raw = sys.stdin.read()
290
- if not raw.strip():
291
- _skip()
292
- input_data = json.loads(raw)
293
- except (json.JSONDecodeError, EOFError, ValueError):
294
- _skip()
295
-
296
- # 3. Extract fields
297
- tool_name = input_data.get("tool_name", "")
298
- tool_response = input_data.get("tool_response")
299
- session_id = input_data.get("session_id", "")
300
- tool_use_id = input_data.get("tool_use_id", "")
301
-
302
- # 4. Layer 1: Content retrieval + task management → skip entirely
303
- if tool_name in SKIP_TOOLS:
304
- _skip()
305
-
306
- # 5. Skip empty or trivial responses
307
- if tool_response is None:
308
- _skip()
309
-
310
- raw_text = _extract_text_for_compression(tool_response)
311
- if not raw_text or raw_text in ("{}", "[]", '""'):
312
- _skip()
313
-
314
- # 6. Detect environment errors (always, regardless of size)
315
- env_category, env_hint = _classify_env_error(tool_response)
316
-
317
- # 7. Skip compression for small responses, but still report env errors
318
- if len(raw_text) < MIN_RESPONSE_CHARS:
319
- if env_category:
320
- response = {
321
- "hookSpecificOutput": {
322
- "hookEventName": "PostToolUse",
323
- "additionalContext": (
324
- f"[tokenless:env:{env_category}] {env_hint} "
325
- f"Do NOT retry the same command — fix the environment first."
326
- ),
327
- }
328
- }
329
- print(json.dumps(response, ensure_ascii=False))
330
- _skip()
331
-
332
- # 8. Determine JSON status (needed for compression pipeline)
333
- parsed = _try_parse_json(raw_text)
334
- is_json = parsed is not None and _is_json_serializable(parsed)
335
-
336
- # 9. Compression pipeline: 3-layer dispatch thresholds
337
- # Layer 1 (content retrieval): already skipped above
338
- # Layer 2 (shell/exec): moderate truncation (64K/128/8)
339
- # Layer 3 (API/structured): zero-truncation (1M/64K/32)
340
- compressed_text: Optional[str] = None
341
- toon_text: Optional[str] = None
342
-
343
- # -- Build compression input --
344
- # If the response is plain text (not JSON), wrap it as a structured
345
- # dict so the compression pipeline can still run and record stats.
346
- # Otherwise bare strings like git log output would be silently skipped
347
- # and never appear in tokenless stats, unlike openclaw/hermes where the
348
- # agent framework natively wraps Bash output in {"stdout":...,...}.
349
- if is_json:
350
- compression_input = raw_text
351
- else:
352
- compression_input = json.dumps(
353
- {"stdout": raw_text}, separators=(",", ":"), ensure_ascii=False
354
- )
355
-
356
- # -- Determine thresholds based on tool category --
357
- thresholds = _get_thresholds(tool_name)
358
-
359
- # -- Response compression --
360
- compressed_text = _run_tokenless(
361
- tokenless_bin, "compress-response",
362
- compression_input, session_id, tool_use_id, timeout=10,
363
- extra_args=[
364
- "--truncate-strings-at", str(thresholds[0]),
365
- "--truncate-arrays-at", str(thresholds[1]),
366
- "--max-depth", str(thresholds[2]),
367
- ],
368
- )
369
- if compressed_text and len(compressed_text) >= len(compression_input):
370
- compressed_text = None # no savings → skip
371
-
372
- # -- TOON encoding (if still valid JSON after compression) --
373
- if compressed_text:
374
- toon_input = compressed_text
375
- else:
376
- toon_input = compression_input
377
-
378
- toon_parsed = _try_parse_json(toon_input)
379
- if toon_parsed is not None and len(toon_input) >= MIN_TOON_CHARS:
380
- toon_text = _run_tokenless(
381
- tokenless_bin, "compress-toon",
382
- toon_input, session_id, tool_use_id, timeout=10,
383
- )
384
- if toon_text and len(toon_text) >= len(toon_input):
385
- toon_text = None # no savings → skip
386
-
387
- # 10. Build additionalContext
388
- parts: list[str] = []
389
-
390
- original_chars = len(raw_text)
391
- final_text = toon_text or compressed_text
392
- is_large = len(raw_text) >= LARGE_RESPONSE_CHARS
393
-
394
- if final_text:
395
- final_chars = len(final_text)
396
- savings_pct = round((1 - final_chars / max(original_chars, 1)) * 100)
397
- parts.append(
398
- f"[tokenless:compressed] {tool_name}: "
399
- f"{original_chars:,} → {final_chars:,} chars ({savings_pct}% reduction)"
400
- )
401
- else:
402
- # No compression savings
403
- if is_large:
404
- parts.append(
405
- f"[tokenless] {tool_name}: {original_chars:,} chars, "
406
- f"no compression savings achieved"
407
- )
408
-
409
- # Environment error hints
410
- if env_category:
411
- parts.append(
412
- f"[tokenless:env:{env_category}] {env_hint} "
413
- f"Do NOT retry the same command — fix the environment first."
414
- )
415
-
416
- # For large responses, include the compressed content so the model has a
417
- # compact alternative to the raw output.
418
- if final_text and is_large:
419
- parts.append("--- compressed content ---")
420
- if len(final_text) > COMPRESSED_CONTENT_CAP:
421
- parts.append(final_text[:COMPRESSED_CONTENT_CAP])
422
- parts.append(
423
- f"... (compressed content truncated at {COMPRESSED_CONTENT_CAP:,} chars)"
424
- )
425
- else:
426
- parts.append(final_text)
427
- parts.append("--- end compressed content ---")
428
-
429
- if not parts:
430
- _skip()
431
-
432
- additional_context = "\n".join(parts)
433
-
434
- # 11. Output codex hook response
435
- response = {
436
- "hookSpecificOutput": {
437
- "hookEventName": "PostToolUse",
438
- "additionalContext": additional_context,
439
- }
440
- }
441
- print(json.dumps(response, ensure_ascii=False))
442
-
443
-
444
- if __name__ == "__main__":
445
- main()
@@ -1,171 +0,0 @@
1
- #!/usr/bin/env python3
2
- """Tokenless standalone TOON encoding hook.
3
-
4
- Reads a PostToolUse JSON from stdin, encodes the tool response
5
- to TOON format via ``tokenless compress-toon``, and writes a
6
- HookOutput JSON to stdout.
7
-
8
- This is a standalone TOON-only hook for users who want pure TOON
9
- encoding without response compression. The combined pipeline
10
- (response compression + TOON) is in compress_response_hook.py.
11
-
12
- Hook point: **PostToolUse**
13
-
14
- The agent ID is read from the TOKENLESS_AGENT_ID environment variable
15
- (set by the install action script).
16
- """
17
-
18
- import json
19
- import os
20
- import subprocess
21
- import sys
22
-
23
- sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
24
-
25
- from hook_utils import (
26
- _TOKENLESS_FALLBACK,
27
- _TOKENLESS_LOCAL_LIB,
28
- _TOKENLESS_LOCAL_SHARE,
29
- CONTENT_RETRIEVAL_TOOLS,
30
- is_skill_file,
31
- resolve_agent_id,
32
- resolve_binary,
33
- skip,
34
- try_parse_json,
35
- unwrap_string_json,
36
- warn,
37
- )
38
-
39
- # -- constants ---------------------------------------------------------------
40
-
41
- _AGENT_ID = resolve_agent_id()
42
-
43
- # Minimum payload size for TOON encoding. TOON on small JSON saves only a
44
- # few characters (observed ~0.3% below ~500 chars) while the per-event
45
- # encode cost stays the same, so smaller responses pass through untouched.
46
- _MIN_TOON_CHARS = 500
47
-
48
-
49
- # -- main --------------------------------------------------------------------
50
-
51
-
52
- def main() -> None:
53
- # 1. Resolve binaries
54
- tokenless_bin = resolve_binary(
55
- "tokenless",
56
- _TOKENLESS_FALLBACK,
57
- _TOKENLESS_LOCAL_SHARE,
58
- _TOKENLESS_LOCAL_LIB,
59
- )
60
- if not tokenless_bin:
61
- warn("tokenless is not installed. TOON compression hook disabled.")
62
- skip()
63
-
64
- # 2. Read stdin JSON
65
- try:
66
- input_data = json.load(sys.stdin)
67
- except (json.JSONDecodeError, EOFError, ValueError):
68
- warn("failed to read PostToolUse payload. Passing through unchanged.")
69
- skip()
70
-
71
- # 3. Skip content-retrieval tools (preserve integrity)
72
- tool_name = input_data.get("tool_name", "unknown")
73
- if tool_name in CONTENT_RETRIEVAL_TOOLS:
74
- skip()
75
-
76
- # 4. Extract tool_response
77
- tool_response_raw = input_data.get("tool_response", "")
78
- if not tool_response_raw or tool_response_raw == "{}":
79
- skip()
80
-
81
- # 5. Skip skill files (YAML frontmatter)
82
- if isinstance(tool_response_raw, str) and is_skill_file(tool_response_raw):
83
- skip()
84
-
85
- # 6. Normalize: unwrap string-wrapped JSON
86
- if isinstance(tool_response_raw, str):
87
- tool_response = unwrap_string_json(tool_response_raw)
88
- if tool_response is None:
89
- skip() # Plain text, not JSON
90
- elif isinstance(tool_response_raw, (dict, list)):
91
- # ensure_ascii=False: the threshold below counts Unicode
92
- # characters (code points), not \uXXXX escape sequences, so
93
- # structured payloads are measured the same way as JSON string
94
- # inputs and the OpenClaw adapter.
95
- tool_response = json.dumps(
96
- tool_response_raw, separators=(",", ":"), ensure_ascii=False
97
- )
98
- else:
99
- skip()
100
-
101
- if not tool_response:
102
- skip()
103
-
104
- # 7. Skip payloads below the TOON minimum threshold (character count,
105
- # not byte length): TOON savings on small JSON are near-zero
106
- if len(tool_response) < _MIN_TOON_CHARS:
107
- skip()
108
-
109
- # 8. Validate it's JSON
110
- parsed = try_parse_json(tool_response)
111
- if parsed is None:
112
- skip()
113
-
114
- # 9. Extract caller context
115
- session_id = input_data.get("session_id", "")
116
- tool_use_id = input_data.get("tool_use_id") or input_data.get(
117
- "toolCallId", ""
118
- )
119
-
120
- # 10. Encode to TOON via tokenless compress-toon
121
- cmd = [tokenless_bin, "compress-toon", "--agent-id", _AGENT_ID]
122
- if session_id:
123
- cmd.extend(["--session-id", session_id])
124
- if tool_use_id:
125
- cmd.extend(["--tool-use-id", tool_use_id])
126
-
127
- try:
128
- proc = subprocess.run(
129
- cmd,
130
- input=tool_response,
131
- capture_output=True,
132
- text=True,
133
- timeout=10,
134
- )
135
- except Exception as e:
136
- warn(f"TOON encoding failed: {e}. Passing through unchanged.")
137
- skip()
138
-
139
- if proc.returncode != 0:
140
- detail = (proc.stderr or "").strip()[:200]
141
- warn(
142
- f"TOON encoding exited with code {proc.returncode}: {detail}"
143
- if detail
144
- else f"TOON encoding exited with code {proc.returncode}. Passing through unchanged."
145
- )
146
- skip()
147
-
148
- toon_output = proc.stdout.strip()
149
- if not toon_output:
150
- warn("TOON encoding returned empty output. Passing through unchanged.")
151
- skip()
152
-
153
- # 11. Size guard — skip if TOON output is not smaller
154
- before_chars = len(tool_response)
155
- after_chars = len(toon_output)
156
- if after_chars >= before_chars:
157
- skip()
158
-
159
- # 12. Build response
160
- output = {
161
- "suppressOutput": True,
162
- "hookSpecificOutput": {
163
- "hookEventName": "PostToolUse",
164
- "additionalContext": toon_output,
165
- },
166
- }
167
- print(json.dumps(output, ensure_ascii=False))
168
-
169
-
170
- if __name__ == "__main__":
171
- main()