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,225 +1,109 @@
1
1
  #!/usr/bin/env python3
2
- """Tokenless command rewriting hook via rtk.
2
+ """Tokenless command rewriting hook via Protocol v2 PreTool.
3
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.
4
+ Reads a PreToolUse JSON from stdin, forwards shell arguments to
5
+ ``tokenless compress``, and translates the Core result into the host's
6
+ HookOutput envelope.
7
7
 
8
8
  Hook point: **PreToolUse** — matcher: shell-family tool names
9
9
  (``Bash``, ``run_shell_command``, ``terminal``, ``Shell``, ``shell``,
10
- ``exec``, ``process``). The lowercase ``shell`` alternative covers
11
- cosh-ng, whose built-in shell tool is named ``shell`` on the wire.
10
+ ``exec``, ``process``).
12
11
 
13
- The agent ID is read from the TOKENLESS_AGENT_ID environment variable
14
- (set by the install action script). Fallback paths follow the ANOLISA
15
- FHS spec: /usr/libexec/anolisa/tokenless/rtk.
12
+ The agent ID is resolved from the host runtime, ``--agent-id`` argument, or
13
+ TOKENLESS_AGENT_ID environment variable. Fallback paths follow the ANOLISA
14
+ FHS spec: /usr/bin/tokenless.
16
15
  """
17
16
 
18
17
  import json
19
18
  import os
20
- import shlex
21
- import subprocess
22
19
  import sys
23
20
 
24
21
  sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
25
22
 
26
23
  from hook_utils import (
27
- _RTK_FALLBACK,
28
- _RTK_LOCAL_LIB,
29
- _RTK_LOCAL_SHARE,
30
24
  _TOKENLESS_FALLBACK,
31
25
  _TOKENLESS_LOCAL_LIB,
32
26
  _TOKENLESS_LOCAL_SHARE,
33
- forward_stderr,
34
- parse_version,
27
+ build_pre_tool_request,
28
+ mark_rtk_optimized,
35
29
  resolve_agent_id,
36
30
  resolve_binary,
37
31
  resolve_tool_call_id,
32
+ run_compress,
38
33
  skip,
39
34
  warn,
40
- write_context,
41
35
  )
42
36
 
43
- # -- constants ---------------------------------------------------------------
44
-
45
- _MIN_RTK_VERSION = (0, 35, 0)
37
+ _PRE_TOOL_TIMEOUT = 8
46
38
  _AGENT_ID = resolve_agent_id()
47
39
 
48
- # Shell connectives that terminate a command-list / pipeline segment.
49
- # A bare `rtk` wrapper can appear at command start or right after one.
50
- _SEGMENT_OPS = frozenset({"&&", "||", ";", "|", "&"})
51
-
52
-
53
- def _is_env_assignment(token: str) -> bool:
54
- """Return True for a leading shell variable assignment (NAME=value)."""
55
- name, sep, _ = token.partition("=")
56
- if not sep or not name:
57
- return False
58
- if not (name[0].isalpha() or name[0] == "_"):
59
- return False
60
- return all(c.isalnum() or c == "_" for c in name)
61
-
62
-
63
- def _anchor_rtk_prefix(rewritten: str, rtk_bin: str) -> str:
64
- """Replace bare `rtk` wrapper tokens with the resolved binary path.
65
-
66
- rtk prints rewrites with a literal `rtk` prefix, which only resolves
67
- when the shell executing the tool call has the rtk location on its
68
- PATH. Agent runtimes with a trimmed PATH (e.g. IDE tool environments
69
- without ~/.local/bin) would fail every rewritten command with exit
70
- 127 even though the hook resolved rtk successfully. Anchoring makes
71
- the rewritten command self-contained.
72
-
73
- The pass swaps the first unquoted `rtk` token of each segment — at
74
- command start or right after `&&` / `||` / `;` / `|` / `&`,
75
- optionally behind leading environment assignments or wrappers such
76
- as `sudo`. It lexes with `posix=False` and no punctuation splitting,
77
- so every token keeps its original surface: quoted patterns like
78
- `grep -E 'foo|rtk bar'` are never modified, unquoted globs like
79
- `*.txt` are not re-quoted into literals, fd redirections (`2>&1`,
80
- `2>/dev/null`) and command substitutions (`$(date)`) stay intact,
81
- and comment stripping is disabled so a `#` argument cannot truncate
82
- the command. Connectives are recognized as whitespace-delimited
83
- tokens; an unspaced `cmd1;cmd2` is not split, which only leaves
84
- that segment's `rtk` unanchored (conservative, never corrupts).
85
- Unparseable input is returned untouched.
86
-
87
- Known limitation: a bare `rtk` token that is another command's
88
- argument (e.g. `echo rtk done`) is indistinguishable from a wrapper
89
- position inside the rtk-rewrite output domain and gets anchored.
90
- Real rtk rewrites do not produce that shape, so this is accepted.
91
- """
92
- lexer = shlex.shlex(rewritten, posix=False)
93
- lexer.whitespace_split = True
94
- lexer.commenters = ""
95
- try:
96
- tokens = list(lexer)
97
- except ValueError:
98
- return rewritten
99
-
100
- quoted = shlex.quote(rtk_bin)
101
- result = list(tokens)
102
- wrapped = False
103
- for i, token in enumerate(tokens):
104
- if token in _SEGMENT_OPS:
105
- wrapped = False
106
- continue
107
- if _is_env_assignment(token):
108
- continue
109
- if not wrapped and token == "rtk":
110
- result[i] = quoted
111
- wrapped = True
112
-
113
- return " ".join(result)
114
-
115
-
116
- # -- main --------------------------------------------------------------------
117
-
118
40
 
119
41
  def main() -> None:
120
- # 1. Resolve rtk binary
121
- rtk_bin = resolve_binary(
122
- "rtk", _RTK_FALLBACK, _RTK_LOCAL_SHARE, _RTK_LOCAL_LIB
123
- )
124
- if not rtk_bin:
125
- warn("rtk is not installed or not in PATH. Hook disabled.")
126
- skip()
127
-
128
- # 2. Version guard
129
- try:
130
- result = subprocess.run(
131
- [rtk_bin, "--version"],
132
- capture_output=True,
133
- text=True,
134
- timeout=3,
135
- )
136
- ver = parse_version(result.stdout)
137
- if ver and ver < _MIN_RTK_VERSION:
138
- warn(f"rtk {result.stdout.strip()} is too old (need >= 0.35.0).")
139
- skip()
140
- except Exception as e:
141
- warn(f"rtk version check failed: {e}")
142
-
143
- # 3. Check tokenless binary (for stats)
144
- if not resolve_binary(
42
+ tokenless_bin = resolve_binary(
145
43
  "tokenless",
146
44
  _TOKENLESS_FALLBACK,
147
45
  _TOKENLESS_LOCAL_SHARE,
148
46
  _TOKENLESS_LOCAL_LIB,
149
- ):
47
+ )
48
+ if not tokenless_bin:
150
49
  warn("tokenless is not installed. Hook disabled.")
151
50
  skip()
152
51
 
153
- # 4. Read stdin JSON
154
52
  try:
155
53
  input_data = json.load(sys.stdin)
156
54
  except (json.JSONDecodeError, EOFError, ValueError):
157
55
  skip()
158
56
 
159
- # 5. Extract command
160
57
  tool_input = input_data.get("tool_input", {})
161
- cmd = tool_input.get("command", "")
162
- if not cmd:
58
+ if not isinstance(tool_input, dict):
59
+ skip()
60
+ command = tool_input.get("command", "")
61
+ if not isinstance(command, str) or not command:
163
62
  skip()
164
63
 
165
- # 6. Rewrite via rtk
166
- env = os.environ.copy()
167
- env["TOKENLESS_AGENT_ID"] = _AGENT_ID
168
64
  session_id = input_data.get("session_id", "")
169
65
  tool_use_id = resolve_tool_call_id(_AGENT_ID, input_data)
170
- if session_id:
171
- env["TOKENLESS_SESSION_ID"] = session_id
172
- if tool_use_id:
173
- env["TOKENLESS_TOOL_USE_ID"] = tool_use_id
174
-
175
- write_context(_AGENT_ID, session_id, tool_use_id)
66
+ if not tool_use_id:
67
+ skip()
176
68
 
177
- try:
178
- proc = subprocess.run(
179
- [rtk_bin, "rewrite", cmd],
180
- capture_output=True,
181
- text=True,
182
- timeout=5,
183
- env=env,
184
- )
185
- except Exception as e:
186
- warn(f"rtk rewrite subprocess failed: {e}")
69
+ request = build_pre_tool_request(
70
+ tool_input,
71
+ _AGENT_ID,
72
+ input_data.get("tool_name", ""),
73
+ "command",
74
+ session_id=session_id,
75
+ tool_use_id=tool_use_id,
76
+ )
77
+ response = run_compress(tokenless_bin, request, _PRE_TOOL_TIMEOUT, "pre_tool")
78
+ if response is None or response.get("action") != "replace_arguments":
79
+ skip()
80
+ if response.get("output_optimization") != "rtk":
187
81
  skip()
188
82
 
189
- # Exit code protocol (from rtk rewrite_cmd.rs):
190
- # 0 = rewrite available, Allow verdict (auto-allow by permission rule)
191
- # 1 = no RTK equivalent (passthrough)
192
- # 2 = deny rule matched (let hook handle)
193
- # 3 = Ask/Default verdict (rewrite available but permission model requires
194
- # user confirmation; in non-interactive hook context, treat as valid
195
- # rewrite since the intent is token optimization, not permission gating)
196
- if proc.returncode not in (0, 1, 2, 3):
197
- forward_stderr(proc)
198
- warn(f"rtk rewrite exited with unexpected code {proc.returncode}")
83
+ updated_input = response.get("arguments")
84
+ if not isinstance(updated_input, dict):
199
85
  skip()
200
- if proc.returncode in (1, 2):
86
+ rewritten = updated_input.get("command")
87
+ if not isinstance(rewritten, str) or not rewritten or rewritten == command:
201
88
  skip()
202
- rewritten = proc.stdout.strip()
203
- if not rewritten or rewritten == cmd:
89
+
90
+ try:
91
+ mark_rtk_optimized(_AGENT_ID, session_id, tool_use_id)
92
+ except OSError as error:
93
+ warn(f"failed to persist PreTool optimization state: {error}")
204
94
  skip()
205
95
 
206
- rewritten = _anchor_rtk_prefix(rewritten, rtk_bin)
207
-
208
- # 7. Build response
209
- # Emit both formats for runtime compatibility:
210
- # - ``tool_input``: Cosh-NG partial patch (merges with original params)
211
- # - ``updatedInput``: copilot-shell full replacement (legacy)
212
- updated_input = dict(tool_input)
213
- updated_input["command"] = rewritten
214
-
215
- output = {
216
- "hookSpecificOutput": {
217
- "hookEventName": "PreToolUse",
218
- "tool_input": {"command": rewritten},
219
- "updatedInput": updated_input,
220
- },
221
- }
222
- print(json.dumps(output))
96
+ print(
97
+ json.dumps(
98
+ {
99
+ "hookSpecificOutput": {
100
+ "hookEventName": "PreToolUse",
101
+ "tool_input": {"command": rewritten},
102
+ "updatedInput": updated_input,
103
+ },
104
+ }
105
+ )
106
+ )
223
107
 
224
108
 
225
109
  if __name__ == "__main__":