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,223 @@
1
+ #!/usr/bin/env python3
2
+ """Tokenless command rewriting hook via rtk.
3
+
4
+ Reads a PreToolUse JSON from stdin, extracts the shell command,
5
+ invokes ``rtk rewrite`` via subprocess, and writes a HookOutput
6
+ JSON to stdout.
7
+
8
+ Hook point: **PreToolUse** — matcher: ``Shell``
9
+
10
+ The agent ID is read from the TOKENLESS_AGENT_ID environment variable
11
+ (set by the install action script). Fallback paths follow the ANOLISA
12
+ FHS spec: /usr/libexec/anolisa/tokenless/rtk.
13
+ """
14
+
15
+ import json
16
+ import os
17
+ import shlex
18
+ import subprocess
19
+ import sys
20
+
21
+ sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
22
+
23
+ from hook_utils import (
24
+ _RTK_FALLBACK,
25
+ _RTK_LOCAL_LIB,
26
+ _RTK_LOCAL_SHARE,
27
+ _TOKENLESS_FALLBACK,
28
+ _TOKENLESS_LOCAL_LIB,
29
+ _TOKENLESS_LOCAL_SHARE,
30
+ forward_stderr,
31
+ parse_version,
32
+ resolve_agent_id,
33
+ resolve_binary,
34
+ resolve_tool_call_id,
35
+ skip,
36
+ warn,
37
+ write_context,
38
+ )
39
+
40
+ # -- constants ---------------------------------------------------------------
41
+
42
+ _MIN_RTK_VERSION = (0, 35, 0)
43
+ _AGENT_ID = resolve_agent_id()
44
+
45
+ # Shell connectives that terminate a command-list / pipeline segment.
46
+ # A bare `rtk` wrapper can appear at command start or right after one.
47
+ _SEGMENT_OPS = frozenset({"&&", "||", ";", "|", "&"})
48
+
49
+
50
+ def _is_env_assignment(token: str) -> bool:
51
+ """Return True for a leading shell variable assignment (NAME=value)."""
52
+ name, sep, _ = token.partition("=")
53
+ if not sep or not name:
54
+ return False
55
+ if not (name[0].isalpha() or name[0] == "_"):
56
+ return False
57
+ return all(c.isalnum() or c == "_" for c in name)
58
+
59
+
60
+ def _anchor_rtk_prefix(rewritten: str, rtk_bin: str) -> str:
61
+ """Replace bare `rtk` wrapper tokens with the resolved binary path.
62
+
63
+ rtk prints rewrites with a literal `rtk` prefix, which only resolves
64
+ when the shell executing the tool call has the rtk location on its
65
+ PATH. Agent runtimes with a trimmed PATH (e.g. IDE tool environments
66
+ without ~/.local/bin) would fail every rewritten command with exit
67
+ 127 even though the hook resolved rtk successfully. Anchoring makes
68
+ the rewritten command self-contained.
69
+
70
+ The pass swaps the first unquoted `rtk` token of each segment — at
71
+ command start or right after `&&` / `||` / `;` / `|` / `&`,
72
+ optionally behind leading environment assignments or wrappers such
73
+ as `sudo`. It lexes with `posix=False` and no punctuation splitting,
74
+ so every token keeps its original surface: quoted patterns like
75
+ `grep -E 'foo|rtk bar'` are never modified, unquoted globs like
76
+ `*.txt` are not re-quoted into literals, fd redirections (`2>&1`,
77
+ `2>/dev/null`) and command substitutions (`$(date)`) stay intact,
78
+ and comment stripping is disabled so a `#` argument cannot truncate
79
+ the command. Connectives are recognized as whitespace-delimited
80
+ tokens; an unspaced `cmd1;cmd2` is not split, which only leaves
81
+ that segment's `rtk` unanchored (conservative, never corrupts).
82
+ Unparseable input is returned untouched.
83
+
84
+ Known limitation: a bare `rtk` token that is another command's
85
+ argument (e.g. `echo rtk done`) is indistinguishable from a wrapper
86
+ position inside the rtk-rewrite output domain and gets anchored.
87
+ Real rtk rewrites do not produce that shape, so this is accepted.
88
+ """
89
+ lexer = shlex.shlex(rewritten, posix=False)
90
+ lexer.whitespace_split = True
91
+ lexer.commenters = ""
92
+ try:
93
+ tokens = list(lexer)
94
+ except ValueError:
95
+ return rewritten
96
+
97
+ quoted = shlex.quote(rtk_bin)
98
+ result = list(tokens)
99
+ wrapped = False
100
+ for i, token in enumerate(tokens):
101
+ if token in _SEGMENT_OPS:
102
+ wrapped = False
103
+ continue
104
+ if _is_env_assignment(token):
105
+ continue
106
+ if not wrapped and token == "rtk":
107
+ result[i] = quoted
108
+ wrapped = True
109
+
110
+ return " ".join(result)
111
+
112
+
113
+ # -- main --------------------------------------------------------------------
114
+
115
+
116
+ def main() -> None:
117
+ # 1. Resolve rtk binary
118
+ rtk_bin = resolve_binary(
119
+ "rtk", _RTK_FALLBACK, _RTK_LOCAL_SHARE, _RTK_LOCAL_LIB
120
+ )
121
+ if not rtk_bin:
122
+ warn("rtk is not installed or not in PATH. Hook disabled.")
123
+ skip()
124
+
125
+ # 2. Version guard
126
+ try:
127
+ result = subprocess.run(
128
+ [rtk_bin, "--version"],
129
+ capture_output=True,
130
+ text=True,
131
+ timeout=3,
132
+ )
133
+ ver = parse_version(result.stdout)
134
+ if ver and ver < _MIN_RTK_VERSION:
135
+ warn(f"rtk {result.stdout.strip()} is too old (need >= 0.35.0).")
136
+ skip()
137
+ except Exception as e:
138
+ warn(f"rtk version check failed: {e}")
139
+
140
+ # 3. Check tokenless binary (for stats)
141
+ if not resolve_binary(
142
+ "tokenless",
143
+ _TOKENLESS_FALLBACK,
144
+ _TOKENLESS_LOCAL_SHARE,
145
+ _TOKENLESS_LOCAL_LIB,
146
+ ):
147
+ warn("tokenless is not installed. Hook disabled.")
148
+ skip()
149
+
150
+ # 4. Read stdin JSON
151
+ try:
152
+ input_data = json.load(sys.stdin)
153
+ except (json.JSONDecodeError, EOFError, ValueError):
154
+ skip()
155
+
156
+ # 5. Extract command
157
+ tool_input = input_data.get("tool_input", {})
158
+ cmd = tool_input.get("command", "")
159
+ if not cmd:
160
+ skip()
161
+
162
+ # 6. Rewrite via rtk
163
+ env = os.environ.copy()
164
+ env["TOKENLESS_AGENT_ID"] = _AGENT_ID
165
+ session_id = input_data.get("session_id", "")
166
+ tool_use_id = resolve_tool_call_id(_AGENT_ID, input_data)
167
+ if session_id:
168
+ env["TOKENLESS_SESSION_ID"] = session_id
169
+ if tool_use_id:
170
+ env["TOKENLESS_TOOL_USE_ID"] = tool_use_id
171
+
172
+ write_context(_AGENT_ID, session_id, tool_use_id)
173
+
174
+ try:
175
+ proc = subprocess.run(
176
+ [rtk_bin, "rewrite", cmd],
177
+ capture_output=True,
178
+ text=True,
179
+ timeout=5,
180
+ env=env,
181
+ )
182
+ except Exception as e:
183
+ warn(f"rtk rewrite subprocess failed: {e}")
184
+ skip()
185
+
186
+ # Exit code protocol (from rtk rewrite_cmd.rs):
187
+ # 0 = rewrite available, Allow verdict (auto-allow by permission rule)
188
+ # 1 = no RTK equivalent (passthrough)
189
+ # 2 = deny rule matched (let hook handle)
190
+ # 3 = Ask/Default verdict (rewrite available but permission model requires
191
+ # user confirmation; in non-interactive hook context, treat as valid
192
+ # rewrite since the intent is token optimization, not permission gating)
193
+ if proc.returncode not in (0, 1, 2, 3):
194
+ forward_stderr(proc)
195
+ warn(f"rtk rewrite exited with unexpected code {proc.returncode}")
196
+ skip()
197
+ if proc.returncode in (1, 2):
198
+ skip()
199
+ rewritten = proc.stdout.strip()
200
+ if not rewritten or rewritten == cmd:
201
+ skip()
202
+
203
+ rewritten = _anchor_rtk_prefix(rewritten, rtk_bin)
204
+
205
+ # 7. Build response
206
+ # Emit both formats for runtime compatibility:
207
+ # - ``tool_input``: Cosh-NG partial patch (merges with original params)
208
+ # - ``updatedInput``: copilot-shell full replacement (legacy)
209
+ updated_input = dict(tool_input)
210
+ updated_input["command"] = rewritten
211
+
212
+ output = {
213
+ "hookSpecificOutput": {
214
+ "hookEventName": "PreToolUse",
215
+ "tool_input": {"command": rewritten},
216
+ "updatedInput": updated_input,
217
+ },
218
+ }
219
+ print(json.dumps(output))
220
+
221
+
222
+ if __name__ == "__main__":
223
+ main()
@@ -0,0 +1,62 @@
1
+ #!/usr/bin/env bash
2
+ # run-hook.sh — Locate and exec a shared tokenless hook script.
3
+ #
4
+ # Prefer hooks from the wrapper's own adapter tree so concurrent RPM, Makefile,
5
+ # and raw installations cannot cross-load another version. Hosts that copy the
6
+ # wrapper into a detached cache still use the historical FHS fallbacks.
7
+ #
8
+ # Usage: run-hook.sh <hook-script-basename> [args...]
9
+ # Examples: run-hook.sh rewrite_hook.py
10
+ # run-hook.sh compress_response_hook.py
11
+ # run-hook.sh tool_ready_hook.sh
12
+ #
13
+ # Fail-open contract: any not-found / missing-interpreter condition emits
14
+ # an empty JSON object on stdout and exits 0, so the host never blocks
15
+ # on us.
16
+ #
17
+ # PreToolUse matcher overlap is by design: hooks.json registers both a
18
+ # Bash-specific entry (rewrite) and a catch-all entry (tool-ready). Bash
19
+ # tool calls therefore fire both — the host evaluates each matching
20
+ # matcher independently, so this is the documented way to attach a
21
+ # tool-specific hook alongside a global one. The timeout values are
22
+ # upper bounds; observed runtimes are well under them.
23
+ set -euo pipefail
24
+
25
+ SCRIPT_DIR="$(CDPATH='' cd "$(dirname "${BASH_SOURCE[0]}")" && pwd -P)"
26
+ SCRIPT="${1:?usage: run-hook.sh <hook-script-basename> [args...]}"
27
+ shift
28
+
29
+ fail_open() { echo "{}"; exit 0; }
30
+
31
+ # Reject anything but a bare basename. Practical risk is low — hooks.json is
32
+ # RPM-installed root:root 0644 and unwritable at runtime — but a defense-in-
33
+ # depth check costs one line and stops any future caller from smuggling a
34
+ # traversal segment through this argument.
35
+ case "$SCRIPT" in
36
+ */*|"") fail_open ;;
37
+ esac
38
+
39
+ CANDIDATES=(
40
+ "${SCRIPT_DIR}/../../common/hooks/${SCRIPT}"
41
+ "/usr/local/share/anolisa/adapters/tokenless/common/hooks/${SCRIPT}"
42
+ "/usr/share/anolisa/adapters/tokenless/common/hooks/${SCRIPT}"
43
+ "${HOME}/.local/share/anolisa/adapters/tokenless/common/hooks/${SCRIPT}"
44
+ )
45
+
46
+ for candidate in "${CANDIDATES[@]}"; do
47
+ [ -f "$candidate" ] || continue
48
+ case "$candidate" in
49
+ *.py)
50
+ command -v python3 >/dev/null 2>&1 || fail_open
51
+ exec python3 "$candidate" "$@"
52
+ ;;
53
+ *.sh)
54
+ exec bash "$candidate" "$@"
55
+ ;;
56
+ *)
57
+ fail_open
58
+ ;;
59
+ esac
60
+ done
61
+
62
+ fail_open
@@ -0,0 +1,99 @@
1
+ {
2
+ "_meta": {
3
+ "version": "1.1",
4
+ "description": "Unified tool categorization for both tool-ready and compression strategies",
5
+ "purpose": "Single source of truth for tool classification and compression thresholds across all adapters",
6
+ "_aliases_note": "The '_aliases' section below is a reference mapping only — not consumed by code. It documents canonical tool name normalization for human readers."
7
+ },
8
+
9
+ "layer_1_skip": {
10
+ "_description": "Layer 1: Skip all compression (preserve integrity for content retrieval)",
11
+ "_tool_ready_category": "Read",
12
+ "_rationale": "These tools return file content or search results that should not be truncated",
13
+ "tools": [
14
+ "Read",
15
+ "read",
16
+ "read_file",
17
+ "read_many_files",
18
+ "Glob",
19
+ "glob",
20
+ "search_file",
21
+ "list_directory",
22
+ "list_dir",
23
+ "Grep",
24
+ "grep",
25
+ "grep_code",
26
+ "grep_search",
27
+ "search_files",
28
+ "Lsp",
29
+ "lsp",
30
+ "NotebookRead",
31
+ "notebook_read",
32
+ "notebookread"
33
+ ]
34
+ },
35
+
36
+ "layer_2_shell": {
37
+ "_description": "Layer 2: Moderate truncation for shell/exec tools",
38
+ "_tool_ready_category": "Shell",
39
+ "_rationale": "Shell commands produce text output that can be safely truncated if too long",
40
+ "tools": [
41
+ "Bash",
42
+ "bash",
43
+ "Shell",
44
+ "shell",
45
+ "exec",
46
+ "terminal",
47
+ "run_shell_command",
48
+ "run_in_terminal",
49
+ "get_terminal_output",
50
+ "execute_command",
51
+ "process"
52
+ ],
53
+ "thresholds": {
54
+ "truncate_strings_at": 65536,
55
+ "truncate_arrays_at": 128,
56
+ "max_depth": 8
57
+ }
58
+ },
59
+
60
+ "layer_3_api": {
61
+ "_description": "Layer 3: Zero-truncation for API/structured tools (default for tools not in layer_1 or layer_2)",
62
+ "_tool_ready_categories": ["WebFetch", "Write"],
63
+ "_rationale": "API responses and structured data should not be truncated; tools in this layer are implicit — any tool not in layer_1_skip or layer_2_shell falls here",
64
+ "thresholds": {
65
+ "truncate_strings_at": 1048576,
66
+ "truncate_arrays_at": 65536,
67
+ "max_depth": 32
68
+ }
69
+ },
70
+
71
+ "_aliases": {
72
+ "_description": "Canonical name mappings (reference only — not consumed by code)",
73
+ "bash": "Bash",
74
+ "shell": "Shell",
75
+ "grep": "Grep",
76
+ "grep_code": "Grep",
77
+ "grep_search": "Grep",
78
+ "search_files": "Grep",
79
+ "glob": "Glob",
80
+ "search_file": "Glob",
81
+ "list_dir": "Read",
82
+ "read": "Read",
83
+ "read_file": "Read",
84
+ "web_fetch": "WebFetch",
85
+ "fetch_content": "WebFetch",
86
+ "web_search": "WebSearch",
87
+ "search_web": "WebSearch",
88
+ "write": "Write",
89
+ "write_file": "Write",
90
+ "create_file": "Write",
91
+ "edit": "Edit",
92
+ "edit_file": "Edit",
93
+ "search_replace": "Edit",
94
+ "delete_file": "Write",
95
+ "run_in_terminal": "Bash",
96
+ "get_terminal_output": "Bash",
97
+ "notebook_read": "NotebookRead"
98
+ }
99
+ }