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.
- package/LICENSE +190 -0
- package/README.md +770 -0
- package/adapters/tokenless/claude-code/.claude-plugin/marketplace.json +15 -0
- package/adapters/tokenless/claude-code/.claude-plugin/plugin.json +9 -0
- package/adapters/tokenless/claude-code/hooks/hooks.json +38 -0
- package/adapters/tokenless/claude-code/scripts/detect.sh +193 -0
- package/adapters/tokenless/claude-code/scripts/install.sh +92 -0
- package/adapters/tokenless/claude-code/scripts/uninstall.sh +50 -0
- package/adapters/tokenless/codex/.codex-plugin/plugin.json +20 -0
- package/adapters/tokenless/codex/README.md +160 -0
- package/adapters/tokenless/codex/hooks/hooks.json +52 -0
- package/adapters/tokenless/codex/scripts/_common.sh +25 -0
- package/adapters/tokenless/codex/scripts/check-tokenless +94 -0
- package/adapters/tokenless/codex/scripts/compress-response +441 -0
- package/adapters/tokenless/codex/scripts/detect.sh +61 -0
- package/adapters/tokenless/codex/scripts/install.sh +151 -0
- package/adapters/tokenless/codex/scripts/rewrite-hook +282 -0
- package/adapters/tokenless/codex/scripts/tool-ready +303 -0
- package/adapters/tokenless/codex/scripts/uninstall.sh +78 -0
- package/adapters/tokenless/common/commands/tokenless-stats.toml +2 -0
- package/adapters/tokenless/common/cosh-extension.json +65 -0
- package/adapters/tokenless/common/hooks/compress_response_hook.py +487 -0
- package/adapters/tokenless/common/hooks/compress_schema_hook.py +144 -0
- package/adapters/tokenless/common/hooks/compress_toon_hook.py +160 -0
- package/adapters/tokenless/common/hooks/hook_utils.py +584 -0
- package/adapters/tokenless/common/hooks/rewrite_hook.py +223 -0
- package/adapters/tokenless/common/hooks/run-hook.sh +62 -0
- package/adapters/tokenless/common/hooks/tool_categories.json +99 -0
- package/adapters/tokenless/common/hooks/tool_ready_hook.sh +571 -0
- package/adapters/tokenless/common/tokenless-env-fix.sh +730 -0
- package/adapters/tokenless/common/tool-ready-spec.json +113 -0
- package/adapters/tokenless/dsh/cordis.patch.yml +8 -0
- package/adapters/tokenless/dsh/dist/index.js +399 -0
- package/adapters/tokenless/dsh/package.json +26 -0
- package/adapters/tokenless/hermes/__init__.py +572 -0
- package/adapters/tokenless/hermes/plugin.yaml +9 -0
- package/adapters/tokenless/hermes/scripts/detect.sh +80 -0
- package/adapters/tokenless/hermes/scripts/install.sh +60 -0
- package/adapters/tokenless/hermes/scripts/uninstall.sh +45 -0
- package/adapters/tokenless/manifest.json +147 -0
- package/adapters/tokenless/openclaw/dist/index.d.ts +27 -0
- package/adapters/tokenless/openclaw/dist/index.js +598 -0
- package/adapters/tokenless/openclaw/dist/tool_categories.json +99 -0
- package/adapters/tokenless/openclaw/index.ts +720 -0
- package/adapters/tokenless/openclaw/openclaw.plugin.json +40 -0
- package/adapters/tokenless/openclaw/package.json +24 -0
- package/adapters/tokenless/openclaw/scripts/detect.sh +102 -0
- package/adapters/tokenless/openclaw/scripts/install.sh +75 -0
- package/adapters/tokenless/openclaw/scripts/uninstall.sh +46 -0
- package/adapters/tokenless/openclaw/tsconfig.json +13 -0
- package/adapters/tokenless/opencode/plugin.js +246 -0
- package/adapters/tokenless/opencode/scripts/detect.sh +38 -0
- package/adapters/tokenless/opencode/scripts/install.sh +56 -0
- package/adapters/tokenless/opencode/scripts/uninstall.sh +29 -0
- package/adapters/tokenless/qoder/.qoder-plugin/plugin.json +11 -0
- package/adapters/tokenless/qoder/commands/tokenless-stats.md +8 -0
- package/adapters/tokenless/qoder/hooks/hooks.json +36 -0
- package/adapters/tokenless/qoder/hooks/run-hook.sh +51 -0
- package/adapters/tokenless/qoder/scripts/detect.sh +35 -0
- package/adapters/tokenless/qoder/scripts/install.sh +136 -0
- package/adapters/tokenless/qoder/scripts/uninstall.sh +106 -0
- package/adapters/tokenless/qwencode/qwen-extension.json +69 -0
- package/adapters/tokenless/qwencode/scripts/detect.sh +125 -0
- package/adapters/tokenless/qwencode/scripts/install.sh +128 -0
- package/adapters/tokenless/qwencode/scripts/uninstall.sh +62 -0
- package/bin/rtk +6 -0
- package/bin/tokenless +6 -0
- package/bin/toon +6 -0
- package/package.json +57 -0
- 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,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."
|