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,40 @@
1
+ {
2
+ "id": "tokenless",
3
+ "name": "Tokenless",
4
+ "version": "0.7.7",
5
+ "description": "Unified RTK command rewriting + response/TOON compression + registered but hard-disabled Tool Ready. Wraps tokenless and rtk system binaries via child_process — this is expected and not malicious.",
6
+ "activation": {
7
+ "onCapabilities": ["hook"]
8
+ },
9
+ "configSchema": {
10
+ "type": "object",
11
+ "properties": {
12
+ "rtk_enabled": { "type": "boolean", "default": true },
13
+ "tool_ready_enabled": { "type": "boolean", "default": true },
14
+ "response_compression_enabled": { "type": "boolean", "default": true },
15
+ "skip_tools": {
16
+ "type": "array",
17
+ "items": { "type": "string" },
18
+ "default": [],
19
+ "description": "Layer 1: Tools that skip all compression (preserve integrity). Loaded from tool_categories.json if not specified."
20
+ },
21
+ "shell_tools": {
22
+ "type": "array",
23
+ "items": { "type": "string" },
24
+ "default": [],
25
+ "description": "Layer 2: Shell/exec tools with moderate truncation. Loaded from tool_categories.json if not specified."
26
+ },
27
+ "toon_compression_enabled": { "type": "boolean", "default": false },
28
+ "verbose": { "type": "boolean", "default": false }
29
+ }
30
+ },
31
+ "uiHints": {
32
+ "rtk_enabled": { "label": "Enable RTK command rewriting" },
33
+ "tool_ready_enabled": { "label": "Register Tool Ready hook (hard-disabled)" },
34
+ "response_compression_enabled": { "label": "Enable response compression" },
35
+ "skip_tools": { "label": "Tool names whose responses should not be compressed" },
36
+ "shell_tools": { "label": "Tool names treated as shell/exec (moderate truncation)" },
37
+ "toon_compression_enabled": { "label": "Enable TOON format compression" },
38
+ "verbose": { "label": "Verbose logging" }
39
+ }
40
+ }
@@ -0,0 +1,24 @@
1
+ {
2
+ "name": "@tokenless/openclaw-plugin",
3
+ "version": "0.7.7",
4
+ "description": "Unified OpenClaw plugin — RTK command rewriting + tokenless schema/response compression for 60-90% LLM token savings",
5
+ "type": "module",
6
+ "main": "dist/index.js",
7
+ "openclaw": {
8
+ "extensions": ["./dist/index.js"]
9
+ },
10
+ "scripts": {
11
+ "clean": "node -e \"require('node:fs').rmSync('dist', { recursive: true, force: true })\"",
12
+ "build": "npm run clean && tsc --project tsconfig.json && cp ../common/hooks/tool_categories.json dist/"
13
+ },
14
+ "peerDependencies": {
15
+ "rtk": ">=0.35.0",
16
+ "tokenless": ">=0.1.0"
17
+ },
18
+ "devDependencies": {
19
+ "@types/node": ">=22",
20
+ "typescript": "^5.8.0"
21
+ },
22
+ "files": ["dist/", "openclaw.plugin.json", "scripts/"],
23
+ "license": "Apache-2.0"
24
+ }
@@ -0,0 +1,102 @@
1
+ #!/usr/bin/env bash
2
+ # detect.sh — Inspect tokenless OpenClaw integration. Read-only.
3
+ #
4
+ # Reports OpenClaw CLI, tokenless plugin install state, runtime
5
+ # artifact (dist/index.js), and adapter resource. Exits 0 when the OpenClaw
6
+ # CLI and the tokenless plugin are both present; non-zero otherwise.
7
+ set -euo pipefail
8
+
9
+ COMPONENT="${ANOLISA_COMPONENT:-tokenless}"
10
+ AGENT="${ANOLISA_TARGET:-openclaw}"
11
+ ADAPTER_DIR="${ANOLISA_ADAPTER_DIR:-$(cd "$(dirname "$0")/../.." && pwd)}"
12
+ OPENCLAW_HOME="${OPENCLAW_HOME:-$HOME/.openclaw}"
13
+ OPENCLAW_STATE_DIR="${OPENCLAW_STATE_DIR:-$OPENCLAW_HOME}"
14
+ OPENCLAW_STATE_DIR="${OPENCLAW_STATE_DIR%/}"
15
+ OPENCLAW_HOME="${OPENCLAW_HOME%/}"
16
+ OPENCLAW_BIN="${OPENCLAW_BIN:-}"
17
+ export PATH="$HOME/.local/bin:${OPENCLAW_STATE_DIR%/}/bin:/usr/local/bin:$PATH"
18
+
19
+ PLUGIN_ID="tokenless"
20
+ PLUGIN_SRC="$ADAPTER_DIR/openclaw"
21
+
22
+ line() { printf '[%s] %s\n' "$COMPONENT" "$*"; }
23
+ field() { printf '[%s] %-26s %s\n' "$COMPONENT" "$1" "$2"; }
24
+
25
+ PREREQ_MISSING=()
26
+ INSTALL_MISSING=()
27
+ note_prereq_missing() { PREREQ_MISSING+=("$1"); }
28
+ note_install_missing() { INSTALL_MISSING+=("$1"); }
29
+
30
+ if [ -z "$OPENCLAW_BIN" ]; then
31
+ OPENCLAW_BIN="$(command -v openclaw 2>/dev/null || true)"
32
+ fi
33
+
34
+ line "${AGENT} detect"
35
+ if [ -n "$OPENCLAW_BIN" ] && [ -x "$OPENCLAW_BIN" ]; then
36
+ field "openclaw CLI" "present (${OPENCLAW_BIN})"
37
+ else
38
+ field "openclaw CLI" "missing"
39
+ note_prereq_missing "openclaw CLI"
40
+ fi
41
+
42
+ if [ -d "$OPENCLAW_STATE_DIR" ]; then
43
+ field "openclaw home" "present (${OPENCLAW_STATE_DIR})"
44
+ else
45
+ field "openclaw home" "not installed (${OPENCLAW_STATE_DIR})"
46
+ note_install_missing "openclaw home"
47
+ fi
48
+
49
+ plugin_state="missing"
50
+ plugin_detail="$PLUGIN_ID"
51
+ if [ -n "$OPENCLAW_BIN" ] && [ -x "$OPENCLAW_BIN" ]; then
52
+ plugins_json="$(env -u OPENCLAW_HOME OPENCLAW_STATE_DIR="$OPENCLAW_STATE_DIR" "$OPENCLAW_BIN" plugins list --json 2>/dev/null || true)"
53
+ plugins_txt="$(env -u OPENCLAW_HOME OPENCLAW_STATE_DIR="$OPENCLAW_STATE_DIR" "$OPENCLAW_BIN" plugins list 2>/dev/null || true)"
54
+ if grep -qE "\"id\"[[:space:]]*:[[:space:]]*\"${PLUGIN_ID}\"" <<<"$plugins_json" \
55
+ || grep -qE "(^|[[:space:]])${PLUGIN_ID}([[:space:]]|$)" <<<"$plugins_txt"; then
56
+ plugin_state="listed"
57
+ plugin_detail="$PLUGIN_ID (openclaw plugins list)"
58
+ fi
59
+ fi
60
+ if [ "$plugin_state" = "missing" ] && [ -d "${OPENCLAW_STATE_DIR%/}/extensions/${PLUGIN_ID}" ]; then
61
+ plugin_state="installed"
62
+ plugin_detail="${OPENCLAW_STATE_DIR%/}/extensions/${PLUGIN_ID}"
63
+ fi
64
+ if [ "$plugin_state" != "missing" ]; then
65
+ field "${PLUGIN_ID} plugin" "${plugin_state} (${plugin_detail})"
66
+ else
67
+ field "${PLUGIN_ID} plugin" "missing"
68
+ note_install_missing "${PLUGIN_ID} plugin"
69
+ fi
70
+
71
+ runtime_bin="$(command -v tokenless 2>/dev/null || true)"
72
+ if [ -n "$runtime_bin" ]; then
73
+ field "tokenless binary" "present (${runtime_bin})"
74
+ else
75
+ field "tokenless binary" "missing"
76
+ note_prereq_missing "tokenless binary"
77
+ fi
78
+
79
+ if [ -d "$PLUGIN_SRC" ]; then
80
+ field "adapter resource" "present (${PLUGIN_SRC})"
81
+ else
82
+ field "adapter resource" "missing (${PLUGIN_SRC})"
83
+ note_prereq_missing "adapter resource"
84
+ fi
85
+
86
+ if [ -f "$PLUGIN_SRC/dist/index.js" ]; then
87
+ field "plugin build artifact" "present"
88
+ else
89
+ field "plugin build artifact" "missing (${PLUGIN_SRC}/dist/index.js)"
90
+ note_prereq_missing "plugin build artifact"
91
+ fi
92
+
93
+ if [ ${#PREREQ_MISSING[@]} -gt 0 ]; then
94
+ line "${AGENT}: missing prerequisites (${PREREQ_MISSING[*]})"
95
+ exit 2
96
+ fi
97
+ if [ ${#INSTALL_MISSING[@]} -gt 0 ]; then
98
+ line "${AGENT}: not installed (ready to install)"
99
+ exit 1
100
+ fi
101
+ line "${AGENT}: ready"
102
+ exit 0
@@ -0,0 +1,75 @@
1
+ #!/usr/bin/env bash
2
+ # install.sh — Deploy the tokenless OpenClaw plugin via the openclaw CLI.
3
+ #
4
+ # Responsibility boundary (mirrors sec-core/openclaw-plugin/scripts/deploy.sh):
5
+ # - This script ONLY deploys an already-built plugin.
6
+ # - Compilation (index.ts -> dist/index.js) is the Makefile's job:
7
+ # make -C src/tokenless build-openclaw-plugin
8
+ # which `make install` runs automatically before `install-adapter-resources`
9
+ # copies the result into $SHARE_DIR/openclaw.
10
+ # - If dist/index.js is missing, exit with a clear error pointing at the
11
+ # Makefile target. Do NOT compile here — adapters shouldn't invoke npm at
12
+ # deploy time.
13
+ set -euo pipefail
14
+
15
+ AGENT="${ANOLISA_TARGET:-openclaw}"
16
+ COMPONENT="${ANOLISA_COMPONENT:-tokenless}"
17
+ ADAPTER_DIR="${ANOLISA_ADAPTER_DIR:-$(cd "$(dirname "$0")/../.." && pwd)}"
18
+
19
+ # Allow the orchestrator (or a packaging script) to inject a specific openclaw
20
+ # binary. Defaults to whatever `openclaw` resolves to on PATH.
21
+ OPENCLAW_BIN="${OPENCLAW_BIN:-openclaw}"
22
+ OPENCLAW_HOME="${OPENCLAW_HOME:-$HOME/.openclaw}"
23
+ OPENCLAW_STATE_DIR="${OPENCLAW_STATE_DIR:-$OPENCLAW_HOME}"
24
+ OPENCLAW_STATE_DIR="${OPENCLAW_STATE_DIR%/}"
25
+ OPENCLAW_HOME="${OPENCLAW_HOME%/}"
26
+ DRY_RUN="${ANOLISA_DRY_RUN:-0}"
27
+ export PATH="$HOME/.local/bin:${OPENCLAW_STATE_DIR%/}/bin:/usr/local/bin:$PATH"
28
+
29
+ PLUGIN_SRC="$ADAPTER_DIR/openclaw"
30
+
31
+ echo "[${COMPONENT}] Installing ${AGENT} plugin..."
32
+
33
+ if [ ! -d "$PLUGIN_SRC" ]; then
34
+ echo "[${COMPONENT}] Plugin source not found: $PLUGIN_SRC" >&2
35
+ exit 1
36
+ fi
37
+
38
+ if [ ! -f "$PLUGIN_SRC/dist/index.js" ]; then
39
+ echo "[${COMPONENT}] ERROR: $PLUGIN_SRC/dist/index.js is missing." >&2
40
+ echo "[${COMPONENT}] Build the plugin first:" >&2
41
+ echo "[${COMPONENT}] make -C src/tokenless build-openclaw-plugin" >&2
42
+ echo "[${COMPONENT}] (run by 'make install' automatically; only an issue when" >&2
43
+ echo "[${COMPONENT}] deploying a hand-assembled adapter directory)." >&2
44
+ exit 1
45
+ fi
46
+
47
+ if [ "$DRY_RUN" = "1" ]; then
48
+ echo "DRY-RUN: env -u OPENCLAW_HOME OPENCLAW_STATE_DIR=$OPENCLAW_STATE_DIR $OPENCLAW_BIN plugins install $PLUGIN_SRC --force --dangerously-force-unsafe-install"
49
+ exit 0
50
+ fi
51
+
52
+ if ! command -v "$OPENCLAW_BIN" &>/dev/null; then
53
+ echo "[${COMPONENT}] openclaw CLI not found (OPENCLAW_BIN=${OPENCLAW_BIN}) — skipping plugin installation."
54
+ echo "[${COMPONENT}] Install OpenClaw first, then run this script again."
55
+ exit 0
56
+ fi
57
+
58
+ # OpenClaw's built-in security scanner flags child_process imports as "dangerous
59
+ # code patterns" and requires --dangerously-force-unsafe-install to proceed.
60
+ # This is expected for the tokenless plugin: it delegates to tokenless and rtk
61
+ # system binaries via execFileSync/spawnSync with fixed paths and timeouts.
62
+ # No shell injection vector exists — all subprocess arguments are hardcoded or
63
+ # come from resolveBinaryPath(), never from user input.
64
+ echo "[${COMPONENT}] Note: --dangerously-force-unsafe-install is required because"
65
+ echo "[${COMPONENT}] this plugin wraps tokenless/rtk system binaries via child_process."
66
+ echo "[${COMPONENT}] See https://github.com/alibaba/anolisa for source."
67
+
68
+ env -u OPENCLAW_HOME OPENCLAW_STATE_DIR="$OPENCLAW_STATE_DIR" "$OPENCLAW_BIN" plugins install "$PLUGIN_SRC" \
69
+ --force --dangerously-force-unsafe-install || {
70
+ echo "[${COMPONENT}] openclaw CLI install failed — check OpenClaw version >= 5.0.0" >&2
71
+ exit 1
72
+ }
73
+
74
+ echo "[${COMPONENT}] ${AGENT} plugin installed via openclaw CLI."
75
+ echo "[${COMPONENT}] Run '${OPENCLAW_BIN} gateway restart' to activate."
@@ -0,0 +1,46 @@
1
+ #!/usr/bin/env bash
2
+ # uninstall.sh — Remove tokenless plugin via OpenClaw official CLI.
3
+ #
4
+ # TODO(adapter-manifest): keep this explicit script while adapter actions are
5
+ # invoked by component Makefile/build-all instead of a shared manifest runner.
6
+ set -euo pipefail
7
+
8
+ AGENT="${ANOLISA_TARGET:-openclaw}"
9
+ COMPONENT="${ANOLISA_COMPONENT:-tokenless}"
10
+ OPENCLAW_BIN="${OPENCLAW_BIN:-}"
11
+ OPENCLAW_HOME="${OPENCLAW_HOME:-$HOME/.openclaw}"
12
+ OPENCLAW_STATE_DIR="${OPENCLAW_STATE_DIR:-$OPENCLAW_HOME}"
13
+ OPENCLAW_STATE_DIR="${OPENCLAW_STATE_DIR%/}"
14
+ OPENCLAW_HOME="${OPENCLAW_HOME%/}"
15
+ DRY_RUN="${ANOLISA_DRY_RUN:-0}"
16
+ export PATH="$HOME/.local/bin:${OPENCLAW_STATE_DIR%/}/bin:/usr/local/bin:$PATH"
17
+
18
+ if [ -z "$OPENCLAW_BIN" ]; then
19
+ OPENCLAW_BIN="$(command -v openclaw 2>/dev/null || true)"
20
+ fi
21
+
22
+ echo "[${COMPONENT}] Removing ${AGENT} plugin..."
23
+
24
+ if [ "$DRY_RUN" = "1" ]; then
25
+ if [ -n "$OPENCLAW_BIN" ]; then
26
+ echo "DRY-RUN: env -u OPENCLAW_HOME OPENCLAW_STATE_DIR=$OPENCLAW_STATE_DIR $OPENCLAW_BIN plugins uninstall tokenless --force"
27
+ else
28
+ echo "DRY-RUN: openclaw CLI not found; remove plugin files manually"
29
+ fi
30
+ echo "DRY-RUN: rm -rf ${OPENCLAW_STATE_DIR%/}/plugins/tokenless"
31
+ echo "DRY-RUN: rm -rf ${OPENCLAW_STATE_DIR%/}/extensions/tokenless"
32
+ exit 0
33
+ fi
34
+
35
+ if [ -z "$OPENCLAW_BIN" ]; then
36
+ echo "[${COMPONENT}] openclaw CLI not found — removing plugin files manually."
37
+ rm -rf "${OPENCLAW_STATE_DIR%/}/plugins/tokenless" 2>/dev/null || true
38
+ rm -rf "${OPENCLAW_STATE_DIR%/}/extensions/tokenless" 2>/dev/null || true
39
+ echo "[${COMPONENT}] Plugin files removed. Manually clean up openclaw.json if needed."
40
+ exit 0
41
+ fi
42
+
43
+ # Use openclaw CLI for proper removal (handles file cleanup + config update)
44
+ env -u OPENCLAW_HOME OPENCLAW_STATE_DIR="$OPENCLAW_STATE_DIR" "$OPENCLAW_BIN" plugins uninstall tokenless --force || true
45
+
46
+ echo "[${COMPONENT}] ${AGENT} plugin removed via openclaw CLI."
@@ -0,0 +1,13 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2023",
4
+ "module": "Node16",
5
+ "moduleResolution": "Node16",
6
+ "outDir": "dist",
7
+ "declaration": true,
8
+ "strict": false,
9
+ "esModuleInterop": true,
10
+ "skipLibCheck": true
11
+ },
12
+ "include": ["index.ts"]
13
+ }
@@ -0,0 +1,246 @@
1
+ /**
2
+ * Tokenless integration for OpenCode's local plugin API.
3
+ *
4
+ * The adapter translates OpenCode's in-process hooks to the shared Tokenless
5
+ * JSON hook protocol, keeping compression and readiness behavior consistent
6
+ * with the other agent integrations.
7
+ */
8
+
9
+ import { spawn } from "node:child_process";
10
+ import { existsSync, realpathSync, statSync } from "node:fs";
11
+ import { homedir } from "node:os";
12
+ import { dirname, isAbsolute, join } from "node:path";
13
+ import { fileURLToPath } from "node:url";
14
+
15
+ const AGENT_ID = "opencode";
16
+ const HOOK_TIMEOUT_MS = 15_000;
17
+ const MAX_HOOK_OUTPUT_BYTES = 1024 * 1024;
18
+ const MAX_CACHE_ENTRIES = 256;
19
+ const PLUGIN_FILE = fileURLToPath(import.meta.url);
20
+ const PLUGIN_DIR = dirname(realpathSync(PLUGIN_FILE));
21
+
22
+ function isFile(path) {
23
+ try {
24
+ return existsSync(path) && statSync(path).isFile();
25
+ } catch {
26
+ // Hook discovery is fail-open so unreadable paths never block OpenCode startup.
27
+ return false;
28
+ }
29
+ }
30
+
31
+ function findHookRunner() {
32
+ const adapterDir = process.env.ANOLISA_ADAPTER_DIR;
33
+ const userHome = process.env.HOME && isAbsolute(process.env.HOME)
34
+ ? process.env.HOME
35
+ : homedir();
36
+ const candidates = [
37
+ process.env.TOKENLESS_HOOK_RUNNER,
38
+ adapterDir ? join(adapterDir, "common", "hooks", "run-hook.sh") : "",
39
+ // The installed plugin remains beside the shared adapter tree after its
40
+ // global registration symlink is resolved through PLUGIN_FILE.
41
+ join(PLUGIN_DIR, "..", "common", "hooks", "run-hook.sh"),
42
+ join(
43
+ userHome,
44
+ ".local",
45
+ "share",
46
+ "anolisa",
47
+ "adapters",
48
+ "tokenless",
49
+ "common",
50
+ "hooks",
51
+ "run-hook.sh",
52
+ ),
53
+ "/usr/local/share/anolisa/adapters/tokenless/common/hooks/run-hook.sh",
54
+ "/usr/share/anolisa/adapters/tokenless/common/hooks/run-hook.sh",
55
+ ];
56
+ return candidates.find((candidate) => candidate && isFile(candidate)) ?? null;
57
+ }
58
+
59
+ function parseHookOutput(raw) {
60
+ if (!raw.trim()) return null;
61
+ try {
62
+ const parsed = JSON.parse(raw);
63
+ return parsed && typeof parsed === "object" ? parsed : null;
64
+ } catch {
65
+ return null;
66
+ }
67
+ }
68
+
69
+ function runHook(runner, hookName, payload) {
70
+ if (!runner) return Promise.resolve(null);
71
+ let serialized;
72
+ try {
73
+ serialized = JSON.stringify(payload);
74
+ } catch {
75
+ return Promise.resolve(null);
76
+ }
77
+
78
+ return new Promise((resolve) => {
79
+ let stdout = "";
80
+ let settled = false;
81
+ let child;
82
+ let timer;
83
+ try {
84
+ child = spawn("bash", [runner, hookName], {
85
+ env: {
86
+ ...process.env,
87
+ TOKENLESS_AGENT_ID: AGENT_ID,
88
+ },
89
+ stdio: ["pipe", "pipe", "ignore"],
90
+ });
91
+ } catch {
92
+ resolve(null);
93
+ return;
94
+ }
95
+
96
+ const finish = (value) => {
97
+ if (settled) return;
98
+ settled = true;
99
+ clearTimeout(timer);
100
+ resolve(value);
101
+ };
102
+ timer = setTimeout(() => {
103
+ child.kill();
104
+ finish(null);
105
+ }, HOOK_TIMEOUT_MS);
106
+
107
+ child.on("error", () => finish(null));
108
+ child.stdout.setEncoding("utf8");
109
+ child.stdout.on("data", (chunk) => {
110
+ if (stdout.length < MAX_HOOK_OUTPUT_BYTES) {
111
+ stdout += chunk.slice(0, MAX_HOOK_OUTPUT_BYTES - stdout.length);
112
+ }
113
+ });
114
+ child.on("close", (code) => {
115
+ finish(code === 0 ? parseHookOutput(stdout) : null);
116
+ });
117
+
118
+ child.stdin.on("error", () => {
119
+ child.kill();
120
+ finish(null);
121
+ });
122
+ child.stdin.end(serialized);
123
+ });
124
+ }
125
+
126
+ function toolPayload(input, args, toolResponse) {
127
+ const payload = {
128
+ session_id: input.sessionID,
129
+ tool_use_id: input.callID,
130
+ tool_call_id: input.callID,
131
+ tool_name: input.tool,
132
+ tool_input: args ?? {},
133
+ };
134
+ if (toolResponse !== undefined) payload.tool_response = toolResponse;
135
+ return payload;
136
+ }
137
+
138
+ function hookSpecificOutput(result) {
139
+ const output = result?.hookSpecificOutput;
140
+ return output && typeof output === "object" ? output : null;
141
+ }
142
+
143
+ function prependContext(context, content) {
144
+ if (typeof context !== "string" || !context) return content;
145
+ return content ? `${context}\n${content}` : context;
146
+ }
147
+
148
+ export const TokenlessPlugin = async () => {
149
+ const runner = findHookRunner();
150
+ const readinessContext = new Map();
151
+ const schemaCache = new Map();
152
+
153
+ const cacheValue = (cache, key, value) => {
154
+ if (cache.has(key)) {
155
+ cache.delete(key);
156
+ } else if (cache.size >= MAX_CACHE_ENTRIES) {
157
+ cache.delete(cache.keys().next().value);
158
+ }
159
+ cache.set(key, value);
160
+ };
161
+
162
+ const cachedValue = (cache, key) => {
163
+ if (!cache.has(key)) return undefined;
164
+ const value = cache.get(key);
165
+ cache.delete(key);
166
+ cache.set(key, value);
167
+ return value;
168
+ };
169
+
170
+ return {
171
+ "tool.execute.before": async (input, output) => {
172
+ const payload = toolPayload(input, output.args);
173
+ const readyResult = await runHook(runner, "tool_ready_hook.sh", payload);
174
+ if (readyResult?.decision === "block") {
175
+ throw new Error(
176
+ readyResult.reason || "Tokenless reports that the tool is not ready",
177
+ );
178
+ }
179
+
180
+ const readyOutput = hookSpecificOutput(readyResult);
181
+ if (typeof readyOutput?.additionalContext === "string") {
182
+ cacheValue(
183
+ readinessContext,
184
+ `${input.sessionID}:${input.callID}`,
185
+ readyOutput.additionalContext,
186
+ );
187
+ }
188
+
189
+ if (input.tool !== "bash" || typeof output.args?.command !== "string") return;
190
+ const rewriteResult = await runHook(runner, "rewrite_hook.py", payload);
191
+ const rewritten = hookSpecificOutput(rewriteResult)?.updatedInput?.command;
192
+ if (typeof rewritten === "string" && rewritten) output.args.command = rewritten;
193
+ },
194
+
195
+ "tool.execute.after": async (input, output) => {
196
+ const key = `${input.sessionID}:${input.callID}`;
197
+ const readyContext = readinessContext.get(key);
198
+ readinessContext.delete(key);
199
+
200
+ const result = await runHook(
201
+ runner,
202
+ "compress_response_hook.py",
203
+ toolPayload(input, input.args, output.output),
204
+ );
205
+ const hookOutput = hookSpecificOutput(result);
206
+ const compressedOutput = hookOutput?.updatedToolOutput;
207
+ if (typeof compressedOutput === "string") {
208
+ output.output = compressedOutput;
209
+ }
210
+ const context = [readyContext, hookOutput?.additionalContext]
211
+ .filter((value) => typeof value === "string" && value)
212
+ .join("\n");
213
+ output.output = prependContext(context, output.output);
214
+ },
215
+
216
+ "tool.definition": async (input, output) => {
217
+ const declaration = {
218
+ name: input.toolID,
219
+ description: output.description,
220
+ parameters: output.parameters,
221
+ };
222
+ let cacheKey;
223
+ try {
224
+ cacheKey = JSON.stringify(declaration);
225
+ } catch {
226
+ return;
227
+ }
228
+ let tools = cachedValue(schemaCache, cacheKey);
229
+ if (!tools) {
230
+ const result = await runHook(runner, "compress_schema_hook.py", {
231
+ llm_request: { config: { tools: [declaration] } },
232
+ });
233
+ tools = hookSpecificOutput(result)?.llm_request?.config?.tools;
234
+ if (Array.isArray(tools)) cacheValue(schemaCache, cacheKey, tools);
235
+ }
236
+ if (!Array.isArray(tools) || tools.length !== 1 || tools[0]?.name !== input.toolID) {
237
+ return;
238
+ }
239
+
240
+ if (typeof tools[0].description === "string") output.description = tools[0].description;
241
+ if (tools[0].parameters && typeof tools[0].parameters === "object") {
242
+ output.parameters = tools[0].parameters;
243
+ }
244
+ },
245
+ };
246
+ };
@@ -0,0 +1,38 @@
1
+ #!/usr/bin/env bash
2
+ # detect.sh — Check whether OpenCode can load the Tokenless local plugin.
3
+ set -euo pipefail
4
+
5
+ AGENT="${ANOLISA_TARGET:-opencode}"
6
+ COMPONENT="${ANOLISA_COMPONENT:-tokenless}"
7
+ ADAPTER_DIR="${ANOLISA_ADAPTER_DIR:-$(cd "$(dirname "$0")/../.." && pwd)}"
8
+ PLUGIN_SOURCE="$ADAPTER_DIR/opencode/plugin.js"
9
+
10
+ OPENCODE_BIN="${OPENCODE_BIN:-}"
11
+ if [ -z "$OPENCODE_BIN" ]; then
12
+ OPENCODE_BIN="$(command -v opencode 2>/dev/null || true)"
13
+ fi
14
+
15
+ if [ -z "$OPENCODE_BIN" ] || { [ ! -x "$OPENCODE_BIN" ] && ! command -v "$OPENCODE_BIN" >/dev/null 2>&1; }; then
16
+ echo "[${COMPONENT}] ${AGENT}: opencode CLI not found" >&2
17
+ exit 2
18
+ fi
19
+ if [ ! -f "$PLUGIN_SOURCE" ]; then
20
+ echo "[${COMPONENT}] ${AGENT}: plugin source missing: ${PLUGIN_SOURCE}" >&2
21
+ exit 2
22
+ fi
23
+
24
+ VERSION="$($OPENCODE_BIN --version 2>/dev/null || true)"
25
+ if [ -z "$VERSION" ]; then
26
+ echo "[${COMPONENT}] ${AGENT}: opencode CLI did not report a version" >&2
27
+ exit 2
28
+ fi
29
+
30
+ CONFIG_HOME="${TOKENLESS_OPENCODE_CONFIG_DIR:-${OPENCODE_CONFIG_DIR:-${XDG_CONFIG_HOME:-$HOME/.config}/opencode}}"
31
+ PLUGIN_LINK="$CONFIG_HOME/plugins/tokenless.js"
32
+ if [ -L "$PLUGIN_LINK" ] && [ "$PLUGIN_LINK" -ef "$PLUGIN_SOURCE" ]; then
33
+ echo "[${COMPONENT}] ${AGENT}: ready (${VERSION})"
34
+ exit 0
35
+ fi
36
+
37
+ echo "[${COMPONENT}] ${AGENT}: detected (${VERSION}), plugin not installed"
38
+ exit 1
@@ -0,0 +1,56 @@
1
+ #!/usr/bin/env bash
2
+ # install.sh — Register Tokenless in OpenCode's global local-plugin directory.
3
+ set -euo pipefail
4
+
5
+ AGENT="${ANOLISA_TARGET:-opencode}"
6
+ COMPONENT="${ANOLISA_COMPONENT:-tokenless}"
7
+ ADAPTER_DIR="${ANOLISA_ADAPTER_DIR:-$(cd "$(dirname "$0")/../.." && pwd)}"
8
+ PLUGIN_SOURCE="$ADAPTER_DIR/opencode/plugin.js"
9
+
10
+ OPENCODE_BIN="${OPENCODE_BIN:-}"
11
+ if [ -z "$OPENCODE_BIN" ]; then
12
+ OPENCODE_BIN="$(command -v opencode 2>/dev/null || true)"
13
+ fi
14
+ if [ -z "$OPENCODE_BIN" ] || { [ ! -x "$OPENCODE_BIN" ] && ! command -v "$OPENCODE_BIN" >/dev/null 2>&1; }; then
15
+ echo "[${COMPONENT}] opencode CLI not found — skipping ${AGENT} plugin installation."
16
+ exit 0
17
+ fi
18
+ if [ ! -f "$PLUGIN_SOURCE" ]; then
19
+ echo "[${COMPONENT}] ERROR: OpenCode plugin source not found: ${PLUGIN_SOURCE}" >&2
20
+ exit 1
21
+ fi
22
+
23
+ CONFIG_HOME="${TOKENLESS_OPENCODE_CONFIG_DIR:-${OPENCODE_CONFIG_DIR:-${XDG_CONFIG_HOME:-$HOME/.config}/opencode}}"
24
+ PLUGIN_DIR="$CONFIG_HOME/plugins"
25
+ PLUGIN_LINK="$PLUGIN_DIR/tokenless.js"
26
+
27
+ is_managed_link() {
28
+ [ -L "$PLUGIN_LINK" ] && {
29
+ [ "$PLUGIN_LINK" -ef "$PLUGIN_SOURCE" ] ||
30
+ [ "$(readlink "$PLUGIN_LINK")" = "$PLUGIN_SOURCE" ]
31
+ }
32
+ }
33
+
34
+ if [ -e "$PLUGIN_LINK" ] || [ -L "$PLUGIN_LINK" ]; then
35
+ if is_managed_link; then
36
+ echo "[${COMPONENT}] ${AGENT} plugin already installed at ${PLUGIN_LINK}."
37
+ exit 0
38
+ fi
39
+ echo "[${COMPONENT}] ERROR: refusing to replace unmanaged path: ${PLUGIN_LINK}" >&2
40
+ exit 1
41
+ fi
42
+
43
+ if [ "${ANOLISA_DRY_RUN:-0}" = "1" ]; then
44
+ echo "DRY-RUN: ln -s ${PLUGIN_SOURCE} ${PLUGIN_LINK}"
45
+ exit 0
46
+ fi
47
+
48
+ mkdir -p "$PLUGIN_DIR"
49
+ ln -s "$PLUGIN_SOURCE" "$PLUGIN_LINK"
50
+ if ! is_managed_link; then
51
+ echo "[${COMPONENT}] ERROR: failed to verify OpenCode plugin link: ${PLUGIN_LINK}" >&2
52
+ exit 1
53
+ fi
54
+
55
+ echo "[${COMPONENT}] ${AGENT} plugin installed at ${PLUGIN_LINK}."
56
+ echo "[${COMPONENT}] Restart OpenCode to load the plugin."
@@ -0,0 +1,29 @@
1
+ #!/usr/bin/env bash
2
+ # uninstall.sh — Remove only the OpenCode plugin link managed by Tokenless.
3
+ set -euo pipefail
4
+
5
+ AGENT="${ANOLISA_TARGET:-opencode}"
6
+ COMPONENT="${ANOLISA_COMPONENT:-tokenless}"
7
+ ADAPTER_DIR="${ANOLISA_ADAPTER_DIR:-$(cd "$(dirname "$0")/../.." && pwd)}"
8
+ PLUGIN_SOURCE="$ADAPTER_DIR/opencode/plugin.js"
9
+ CONFIG_HOME="${TOKENLESS_OPENCODE_CONFIG_DIR:-${OPENCODE_CONFIG_DIR:-${XDG_CONFIG_HOME:-$HOME/.config}/opencode}}"
10
+ PLUGIN_LINK="$CONFIG_HOME/plugins/tokenless.js"
11
+
12
+ is_managed_link() {
13
+ [ -L "$PLUGIN_LINK" ] && {
14
+ [ "$PLUGIN_LINK" -ef "$PLUGIN_SOURCE" ] ||
15
+ [ "$(readlink "$PLUGIN_LINK")" = "$PLUGIN_SOURCE" ]
16
+ }
17
+ }
18
+
19
+ if [ ! -e "$PLUGIN_LINK" ] && [ ! -L "$PLUGIN_LINK" ]; then
20
+ echo "[${COMPONENT}] ${AGENT} plugin is already absent."
21
+ exit 0
22
+ fi
23
+ if ! is_managed_link; then
24
+ echo "[${COMPONENT}] WARNING: leaving unmanaged path untouched: ${PLUGIN_LINK}" >&2
25
+ exit 0
26
+ fi
27
+
28
+ rm "$PLUGIN_LINK"
29
+ echo "[${COMPONENT}] ${AGENT} plugin removed."