anolisa-tokenless 0.7.14 → 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 (30) hide show
  1. package/README.md +206 -79
  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 +1 -1
  5. package/adapters/tokenless/common/cosh-extension.json +4 -4
  6. package/adapters/tokenless/common/hooks/compress_response_hook.py +180 -96
  7. package/adapters/tokenless/common/hooks/compress_schema_hook.py +19 -31
  8. package/adapters/tokenless/common/hooks/hook_utils.py +240 -70
  9. package/adapters/tokenless/common/hooks/rewrite_hook.py +53 -169
  10. package/adapters/tokenless/dsh/dist/index.js +353 -264
  11. package/adapters/tokenless/dsh/package.json +2 -2
  12. package/adapters/tokenless/hermes/__init__.py +191 -357
  13. package/adapters/tokenless/hermes/plugin.yaml +2 -2
  14. package/adapters/tokenless/manifest.json +17 -1
  15. package/adapters/tokenless/openclaw/dist/index.d.ts +4 -16
  16. package/adapters/tokenless/openclaw/dist/index.js +291 -507
  17. package/adapters/tokenless/openclaw/index.ts +408 -628
  18. package/adapters/tokenless/openclaw/openclaw.plugin.json +4 -20
  19. package/adapters/tokenless/openclaw/package.json +6 -4
  20. package/adapters/tokenless/qoder/.qoder-plugin/plugin.json +1 -1
  21. package/adapters/tokenless/qwencode/hooks/run-hook.sh +62 -0
  22. package/adapters/tokenless/qwencode/qwen-extension.json +4 -4
  23. package/adapters/tokenless/qwenpaw/plugin.json +17 -0
  24. package/adapters/tokenless/qwenpaw/plugin.py +390 -0
  25. package/adapters/tokenless/qwenpaw/requirements.txt +6 -0
  26. package/adapters/tokenless/qwenpaw/scripts/detect.sh +131 -0
  27. package/adapters/tokenless/qwenpaw/scripts/install.sh +98 -0
  28. package/adapters/tokenless/qwenpaw/scripts/uninstall.sh +61 -0
  29. package/package.json +5 -5
  30. package/adapters/tokenless/common/hooks/compress_toon_hook.py +0 -174
@@ -2,12 +2,14 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
+ import hashlib
5
6
  import json
6
7
  import os
7
8
  import re
8
9
  import shutil
9
10
  import subprocess
10
11
  import sys
12
+ import time
11
13
 
12
14
  # -- Binary fallback paths ----------------------------------------------------
13
15
  #
@@ -35,16 +37,10 @@ def _user_path(*parts: str) -> str:
35
37
 
36
38
 
37
39
  _TOKENLESS_FALLBACK = "/usr/bin/tokenless"
38
- _TOKENLESS_LOCAL_SHARE = _user_path(
39
- ".local", "share", "anolisa", "tokenless", "tokenless"
40
- )
41
- _TOKENLESS_LOCAL_LIB = _user_path(
42
- ".local", "lib", "anolisa", "tokenless", "tokenless"
43
- )
40
+ _TOKENLESS_LOCAL_SHARE = _user_path(".local", "share", "anolisa", "tokenless", "tokenless")
41
+ _TOKENLESS_LOCAL_LIB = _user_path(".local", "lib", "anolisa", "tokenless", "tokenless")
44
42
  _RTK_FALLBACK = "/usr/libexec/anolisa/tokenless/rtk"
45
- _RTK_LOCAL_SHARE = _user_path(
46
- ".local", "share", "anolisa", "tokenless", "rtk"
47
- )
43
+ _RTK_LOCAL_SHARE = _user_path(".local", "share", "anolisa", "tokenless", "rtk")
48
44
  _RTK_LOCAL_LIB = _user_path(".local", "lib", "anolisa", "tokenless", "rtk")
49
45
 
50
46
  _TOKENLESS_HELPER_BINARIES = frozenset({"rtk"})
@@ -87,9 +83,7 @@ def _known_binary_paths(name: str, home: str | None = None) -> tuple[str, ...]:
87
83
  paths.append(os.path.join("/usr/local/bin", name))
88
84
  if name in _TOKENLESS_HELPER_BINARIES:
89
85
  # Anolisa CLI system mode.
90
- paths.append(
91
- os.path.join("/usr/local/libexec/anolisa/tokenless", name)
92
- )
86
+ paths.append(os.path.join("/usr/local/libexec/anolisa/tokenless", name))
93
87
  paths.append(os.path.join("/usr/bin", name))
94
88
  if name in _TOKENLESS_HELPER_BINARIES:
95
89
  paths.extend(
@@ -103,16 +97,13 @@ def _known_binary_paths(name: str, home: str | None = None) -> tuple[str, ...]:
103
97
  if user_home:
104
98
  paths.extend(
105
99
  [
106
- os.path.join(
107
- user_home, ".local", "share", "anolisa", "tokenless", name
108
- ),
109
- os.path.join(
110
- user_home, ".local", "lib", "anolisa", "tokenless", name
111
- ),
100
+ os.path.join(user_home, ".local", "share", "anolisa", "tokenless", name),
101
+ os.path.join(user_home, ".local", "lib", "anolisa", "tokenless", name),
112
102
  ]
113
103
  )
114
104
  return tuple(paths)
115
105
 
106
+
116
107
  # -- Unified tool categorization ----------------------------------------------
117
108
 
118
109
  # Tool categories are loaded from tool_categories.json, which serves as the
@@ -126,16 +117,38 @@ _TOOL_CATEGORIES_PATH = os.path.join(os.path.dirname(__file__), "tool_categories
126
117
  # invalid. Matches the minimum safe classification from before the JSON was
127
118
  # introduced, ensuring content-retrieval tools are never accidentally compressed.
128
119
  _FALLBACK_SKIP_TOOLS = [
129
- "Read", "read", "read_file", "read_many_files",
130
- "Glob", "glob", "search_file", "list_directory", "list_dir",
131
- "Grep", "grep", "grep_code", "grep_search", "search_files",
132
- "Lsp", "lsp",
133
- "NotebookRead", "notebook_read", "notebookread",
120
+ "Read",
121
+ "read",
122
+ "read_file",
123
+ "read_many_files",
124
+ "Glob",
125
+ "glob",
126
+ "search_file",
127
+ "list_directory",
128
+ "list_dir",
129
+ "Grep",
130
+ "grep",
131
+ "grep_code",
132
+ "grep_search",
133
+ "search_files",
134
+ "Lsp",
135
+ "lsp",
136
+ "NotebookRead",
137
+ "notebook_read",
138
+ "notebookread",
134
139
  ]
135
140
  _FALLBACK_SHELL_TOOLS = [
136
- "Bash", "bash", "Shell", "shell", "exec", "terminal",
137
- "run_shell_command", "run_in_terminal", "get_terminal_output",
138
- "execute_command", "process",
141
+ "Bash",
142
+ "bash",
143
+ "Shell",
144
+ "shell",
145
+ "exec",
146
+ "terminal",
147
+ "run_shell_command",
148
+ "run_in_terminal",
149
+ "get_terminal_output",
150
+ "execute_command",
151
+ "process",
139
152
  ]
140
153
 
141
154
 
@@ -187,6 +200,29 @@ SKIP_TOOLS: set[str] = set(_tool_categories.get("layer_1_skip", {}).get("tools",
187
200
  # These tools produce text output that can be safely truncated if too long.
188
201
  SHELL_TOOLS: set[str] = set(_tool_categories.get("layer_2_shell", {}).get("tools", []))
189
202
 
203
+ _TOKENLESS_RETRIEVE_COMMAND_RE = re.compile(
204
+ r"^[ \t]*(?:\"tokenless\"|'tokenless'|tokenless)[ \t]+retrieve[ \t]+"
205
+ r"(?:\"(?:[0-9a-f]{24}|<<tokenless:[0-9a-f]{24}>>)\"|"
206
+ r"'(?:[0-9a-f]{24}|<<tokenless:[0-9a-f]{24}>>)'|[0-9a-f]{24})[ \t]*$",
207
+ re.IGNORECASE,
208
+ )
209
+
210
+
211
+ def tokenless_retrieve_command_available() -> bool:
212
+ """Return whether a Marker command can invoke bare ``tokenless``."""
213
+ return shutil.which("tokenless") is not None
214
+
215
+
216
+ def is_tokenless_retrieve_command(tool_name: str, arguments: object) -> bool:
217
+ """Recognize the exact local recovery command emitted by Tokenless markers."""
218
+ if tool_name not in SHELL_TOOLS or not isinstance(arguments, dict):
219
+ return False
220
+ command = arguments.get("command")
221
+ if not isinstance(command, str):
222
+ return False
223
+ return _TOKENLESS_RETRIEVE_COMMAND_RE.fullmatch(command) is not None
224
+
225
+
190
226
  # Layer 3: API tools (zero-truncation).
191
227
  # These tools return structured data or API responses that should not be truncated.
192
228
  # No explicit set needed; tools not in SKIP_TOOLS or SHELL_TOOLS are Layer 3.
@@ -196,7 +232,7 @@ SHELL_TOOLS: set[str] = set(_tool_categories.get("layer_2_shell", {}).get("tools
196
232
  # is missing or the file failed to load.
197
233
 
198
234
  # Layer 2 thresholds: moderate truncation for shell/exec output.
199
- # Restores old ResponseCompressor defaults for shell commands (git log, ls,
235
+ # Restores the old JSON compression defaults for shell commands (git log, ls,
200
236
  # cat, etc.) where truncation is acceptable.
201
237
  # 64K strings: 95% of real shell output (git diff ~63K, git log ~34K) preserved.
202
238
  # 128 arrays: 95% of result sets (test results, audit reports) preserved.
@@ -211,11 +247,6 @@ _TRUNCATE_STRINGS_AT = _layer3_thr.get("truncate_strings_at", 1_048_576)
211
247
  _TRUNCATE_ARRAYS_AT = _layer3_thr.get("truncate_arrays_at", 65_536)
212
248
  _MAX_DEPTH = _layer3_thr.get("max_depth", 32)
213
249
 
214
- # Backward-compatible alias — direct reference (not a copy) so consumers see
215
- # the same set as SKIP_TOOLS. Used by compress_toon_hook.py for the standalone
216
- # TOON-only path where "content retrieval" is the more descriptive name.
217
- CONTENT_RETRIEVAL_TOOLS = SKIP_TOOLS
218
-
219
250
 
220
251
  def get_thresholds(tool_name: str) -> tuple[int, int, int]:
221
252
  """Return (truncate_strings_at, truncate_arrays_at, max_depth) for a tool.
@@ -346,6 +377,9 @@ def classify_env_error(tool_response) -> tuple[str | None, str | None]:
346
377
 
347
378
  _CONTEXT_DIR = os.path.join(os.path.expanduser("~"), ".tokenless")
348
379
  _CONTEXT_FILE = os.path.join(_CONTEXT_DIR, ".rewrite-context")
380
+ _OPTIMIZATION_STATE_DIR = os.path.join(_CONTEXT_DIR, "hook-state")
381
+ _OPTIMIZATION_STATE_TTL_SECONDS = 24 * 60 * 60
382
+ _OPTIMIZATION_STATE_MAX_FILES = 1024
349
383
 
350
384
  # -- Binary resolution (cached) -----------------------------------------------
351
385
 
@@ -412,9 +446,7 @@ def unwrap_string_json(raw: str) -> str | None:
412
446
  # characters (code points), not \uXXXX escape sequences, so
413
447
  # string-wrapped payloads are measured the same way as the
414
448
  # dict/list branch and the OpenClaw adapter.
415
- return json.dumps(
416
- inner_obj, separators=(",", ":"), ensure_ascii=False
417
- )
449
+ return json.dumps(inner_obj, separators=(",", ":"), ensure_ascii=False)
418
450
  return None
419
451
  return raw
420
452
 
@@ -474,6 +506,77 @@ def write_context(agent_id: str, session_id: str, tool_use_id: str) -> None:
474
506
  secure_write_text(_CONTEXT_FILE, f"{agent_id}\n{session_id}\n{tool_use_id}\n")
475
507
 
476
508
 
509
+ def _optimization_state_path(agent_id: str, session_id: str, tool_use_id: str) -> str:
510
+ identity = "\0".join((agent_id, session_id, tool_use_id)).encode()
511
+ digest = hashlib.sha256(identity).hexdigest()
512
+ return os.path.join(_OPTIMIZATION_STATE_DIR, digest)
513
+
514
+
515
+ def _prune_optimization_states() -> None:
516
+ """Bound abandoned per-call state when a host omits PostToolUse."""
517
+ try:
518
+ entries = os.scandir(_OPTIMIZATION_STATE_DIR)
519
+ except FileNotFoundError:
520
+ return
521
+
522
+ cutoff = time.time() - _OPTIMIZATION_STATE_TTL_SECONDS
523
+ live = []
524
+ with entries:
525
+ for entry in entries:
526
+ try:
527
+ if not entry.is_file(follow_symlinks=False):
528
+ continue
529
+ modified = entry.stat(follow_symlinks=False).st_mtime
530
+ if ".consuming." in entry.name and modified > cutoff:
531
+ continue
532
+ if modified <= cutoff:
533
+ os.unlink(entry.path)
534
+ else:
535
+ live.append((modified, entry.path))
536
+ except FileNotFoundError:
537
+ continue
538
+
539
+ excess = len(live) - _OPTIMIZATION_STATE_MAX_FILES + 1
540
+ if excess > 0:
541
+ for _, path in sorted(live)[:excess]:
542
+ try:
543
+ os.unlink(path)
544
+ except FileNotFoundError:
545
+ pass
546
+
547
+
548
+ def mark_rtk_optimized(agent_id: str, session_id: str, tool_use_id: str) -> None:
549
+ """Persist RTK ownership for one tool call before applying its rewrite."""
550
+ _prune_optimization_states()
551
+ secure_write_text(_optimization_state_path(agent_id, session_id, tool_use_id), "rtk\n")
552
+
553
+
554
+ def consume_output_optimization(agent_id: str, session_id: str, tool_use_id: str) -> str:
555
+ """Consume one tool call's optimization state for its final result."""
556
+ if not tool_use_id:
557
+ return "none"
558
+ path = _optimization_state_path(agent_id, session_id, tool_use_id)
559
+ consuming_path = f"{path}.consuming.{os.getpid()}"
560
+ try:
561
+ os.rename(path, consuming_path)
562
+ except FileNotFoundError:
563
+ return "none"
564
+
565
+ try:
566
+ flags = os.O_RDONLY
567
+ if hasattr(os, "O_NOFOLLOW"):
568
+ flags |= os.O_NOFOLLOW
569
+ fd = os.open(consuming_path, flags)
570
+ with os.fdopen(fd) as state_file:
571
+ state = state_file.read()
572
+ finally:
573
+ try:
574
+ os.unlink(consuming_path)
575
+ except FileNotFoundError:
576
+ pass
577
+ return "rtk" if state == "rtk\n" else "none"
578
+
579
+
477
580
  def forward_stderr(proc: subprocess.CompletedProcess) -> None:
478
581
  """Forward subprocess stderr on failure (non-zero exit) via warn()."""
479
582
  if proc.returncode != 0 and proc.stderr:
@@ -496,45 +599,109 @@ def run(args: list[str], input_data: str, timeout: int = 3) -> subprocess.Comple
496
599
  return None
497
600
 
498
601
 
499
- def build_compression_request(
602
+ def _attribution(
603
+ agent_id: str,
604
+ session_id: str = "",
605
+ tool_use_id: str = "",
606
+ ) -> dict:
607
+ """Build the shared Protocol v2 attribution object."""
608
+ value = {"agent_id": agent_id}
609
+ if session_id:
610
+ value["session_id"] = session_id
611
+ if tool_use_id:
612
+ value["tool_use_id"] = tool_use_id
613
+ return value
614
+
615
+
616
+ def build_before_model_request(
617
+ tools: list,
618
+ visible_context: object,
619
+ agent_id: str,
620
+ session_id: str = "",
621
+ tool_use_id: str = "",
622
+ ) -> dict:
623
+ """Build a Protocol v2 BeforeModel transport request."""
624
+ return {
625
+ "protocol_version": 2,
626
+ "operation": "before_model",
627
+ "attribution": _attribution(agent_id, session_id, tool_use_id),
628
+ "input": {
629
+ "tools": tools,
630
+ "visible_context": visible_context,
631
+ "capabilities": {
632
+ "replace_tools": True,
633
+ # A local CLI is not marker-scoped Agent authorization.
634
+ "recovery": {"kind": "none"},
635
+ },
636
+ },
637
+ }
638
+
639
+
640
+ def build_pre_tool_request(
641
+ arguments: dict,
642
+ agent_id: str,
643
+ tool_name: str,
644
+ command_field: str,
645
+ session_id: str = "",
646
+ tool_use_id: str = "",
647
+ replace_arguments: bool = True,
648
+ block_and_suggest: bool = False,
649
+ ) -> dict:
650
+ """Build a Protocol v2 PreTool transport request."""
651
+ return {
652
+ "protocol_version": 2,
653
+ "operation": "pre_tool",
654
+ "attribution": _attribution(agent_id, session_id, tool_use_id),
655
+ "input": {
656
+ "tool_name": tool_name,
657
+ "arguments": arguments,
658
+ "command_field": command_field,
659
+ "capabilities": {
660
+ "replace_arguments": replace_arguments,
661
+ "block_and_suggest": block_and_suggest,
662
+ },
663
+ },
664
+ }
665
+
666
+
667
+ def build_post_tool_request(
500
668
  content: str,
501
669
  agent_id: str,
502
- seam: str,
670
+ tool_name: str,
671
+ status: str,
672
+ content_origin: str,
673
+ output_optimization: str,
674
+ *,
675
+ result_kind: str,
676
+ recovery: dict[str, str],
503
677
  session_id: str = "",
504
678
  tool_use_id: str = "",
505
- tool_name: str = "",
506
679
  replace_output: bool = False,
507
- publish_retrieve_tool: bool = False,
508
680
  replace_with_text: bool = False,
509
681
  ) -> dict:
510
- """Build a protocol-v1 CompressionRequest for ``tokenless compress``.
511
-
512
- The adapter only copies the model-visible value and declares what its
513
- host can do with the result (roadmap §4.5); all compression decisions
514
- live behind the entry point.
515
- """
516
- request = {
517
- "protocol_version": 1,
518
- "content": content,
519
- "agent_id": agent_id,
520
- "seam": seam,
521
- "capabilities": {
522
- "replace_output": replace_output,
523
- "publish_retrieve_tool": publish_retrieve_tool,
524
- "replace_with_text": replace_with_text,
682
+ """Build a Protocol v2 PostTool transport request."""
683
+ return {
684
+ "protocol_version": 2,
685
+ "operation": "post_tool",
686
+ "attribution": _attribution(agent_id, session_id, tool_use_id),
687
+ "input": {
688
+ "result_kind": result_kind,
689
+ "tool_name": tool_name,
690
+ "content": content,
691
+ "status": status,
692
+ "content_origin": content_origin,
693
+ "output_optimization": output_optimization,
694
+ "capabilities": {
695
+ "replace_output": replace_output,
696
+ "recovery": recovery,
697
+ "replace_with_text": replace_with_text,
698
+ },
525
699
  },
526
700
  }
527
- if session_id:
528
- request["session_id"] = session_id
529
- if tool_use_id:
530
- request["tool_use_id"] = tool_use_id
531
- if tool_name:
532
- request["tool_name"] = tool_name
533
- return request
534
701
 
535
702
 
536
703
  def run_compress(
537
- tokenless_bin: str, request: dict, timeout: int
704
+ tokenless_bin: str, request: dict, timeout: int, expected_operation: str
538
705
  ) -> dict | None:
539
706
  """Run ``tokenless compress`` on one request; None on any failure.
540
707
 
@@ -555,12 +722,19 @@ def run_compress(
555
722
  if not isinstance(response, dict):
556
723
  warn("tokenless compress returned malformed output")
557
724
  return None
558
- if response.get("protocol_version") != 1:
725
+ if response.get("protocol_version") != 2:
559
726
  # Version-skewed binary: never trust a response whose contract
560
727
  # this adapter does not speak.
561
728
  warn("tokenless compress returned an unsupported protocol version")
562
729
  return None
563
- return response
730
+ if response.get("operation") != expected_operation:
731
+ warn("tokenless compress returned a mismatched operation")
732
+ return None
733
+ result = response.get("result")
734
+ if not isinstance(result, dict):
735
+ warn("tokenless compress returned a malformed operation result")
736
+ return None
737
+ return result
564
738
 
565
739
 
566
740
  def detect_cosh_ng_runtime() -> tuple | None:
@@ -614,7 +788,7 @@ def _agent_id_from_argv(argv: list[str] | None) -> str | None:
614
788
  following = args[index + 1] if index + 1 < len(args) else ""
615
789
  return following or None
616
790
  if arg.startswith("--agent-id="):
617
- return arg[len("--agent-id="):] or None
791
+ return arg[len("--agent-id=") :] or None
618
792
  return None
619
793
 
620
794
 
@@ -642,11 +816,7 @@ def resolve_agent_id(default: str = "unknown", argv: list[str] | None = None) ->
642
816
  """
643
817
  if detect_cosh_ng_runtime() is not None:
644
818
  return "cosh-ng"
645
- return (
646
- _agent_id_from_argv(argv)
647
- or os.environ.get("TOKENLESS_AGENT_ID")
648
- or default
649
- )
819
+ return _agent_id_from_argv(argv) or os.environ.get("TOKENLESS_AGENT_ID") or default
650
820
 
651
821
 
652
822
  def parse_version(version_str: str) -> tuple | None: