@totalreclaw/totalreclaw 3.3.7-rc.3 → 3.3.9-rc.1
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/CHANGELOG.md +48 -0
- package/SKILL.md +113 -49
- package/dist/index.js +29 -1
- package/dist/tr-cli.js +378 -0
- package/fs-helpers.ts +12 -0
- package/index.ts +29 -1
- package/openclaw.plugin.json +1 -0
- package/package.json +5 -2
- package/skill.json +1 -1
- package/tr-cli.ts +445 -0
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,54 @@ All notable changes to `@totalreclaw/totalreclaw` (the OpenClaw plugin) are docu
|
|
|
4
4
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
6
6
|
|
|
7
|
+
## [3.3.9-rc.1] — 2026-05-02
|
|
8
|
+
|
|
9
|
+
Pedro's QA on 3.3.8-rc.1 (Telegram → canonical OpenClaw 2026.5.2) revealed five issues that all stem from the same architectural problem — TotalReclaw's tools registered via `api.registerTool()` are blocked by OpenClaw 2026.5.2's tool-policy strip race (issue #223, filed upstream). Each release we ship adds a fix for one gate, hits another. 3.3.9-rc.1 pivots to hybrid-primary: the `tr` CLI is now the PRIMARY path. Native tools are kept for back-compat only.
|
|
10
|
+
|
|
11
|
+
### Five issues from Pedro's 3.3.8-rc.1 QA — root causes and fixes:
|
|
12
|
+
|
|
13
|
+
1. **Agent hallucinated "without an account, memories are local only"** — FALSE. TR is relay-based, no local storage. Fix: SKILL.md now has a "CRITICAL: Relay-based architecture" section at the top with an explicit forbidden-vocabulary denylist (`local`, `local-only`, `stored locally`, `on disk`, `without an account`, `local memory`, `local storage`). Both `skill/plugin/SKILL.md` and `skill/SKILL.md` updated.
|
|
14
|
+
|
|
15
|
+
2. **Agent did not autonomously emit account-setup URL+PIN after install** — asked user instead. Fix: SKILL.md now makes step 4 (running `tr pair --json`) UNCONDITIONAL — no "Want me to set up an account?" gate. The URL+PIN is the consent moment.
|
|
16
|
+
|
|
17
|
+
3. **Pair tool reply did NOT include the URL** ("open in your browser, enter PIN" with no link). Fix: `tr pair --json` output documented in SKILL.md with explicit JSON shape `{"url":"...","pin":"...","expires_at":"..."}`. Agent is instructed to parse and emit URL verbatim.
|
|
18
|
+
|
|
19
|
+
4. **`totalreclaw_pair` not bound after install, `/totalreclaw-restart` did not restore binding** — root cause is issue #223 (tool-policy strip race). Fix: SKILL.md drops the "wait for tool-bind / /totalreclaw-restart / /new" cycle from the user-facing path. `tr status --json` replaces the tool-binding check. Agent no longer depends on `totalreclaw_*` binding.
|
|
20
|
+
|
|
21
|
+
5. **Pair-page POST `/respond` returned 502 then 400** — separate relay bug, out of scope for this RC.
|
|
22
|
+
|
|
23
|
+
### Changed — SKILL.md (both `skill/plugin/SKILL.md` and `skill/SKILL.md`):
|
|
24
|
+
|
|
25
|
+
- Added "CRITICAL: Relay-based architecture" section at top with positive assertion and forbidden-vocabulary denylist.
|
|
26
|
+
- Rewritten setup flow: Step 1 install → Step 2 verify CLI (`tr status --json`) → Step 3 credentials check → Step 4 emit URL+PIN via `tr pair --json` (unconditional) → Step 5 confirm.
|
|
27
|
+
- Dropped the entire "wait for tool-bind, restart with /totalreclaw-restart, fall back to /new" cycle.
|
|
28
|
+
- Added "tr CLI reference" section documenting `--json` flag shapes for all commands.
|
|
29
|
+
- Autonomous account setup: removed consent gate, agent runs `tr pair --json` immediately.
|
|
30
|
+
- User-visible line set updated: line 2 now confirms "hybrid mode"; line 3 emitted immediately from `tr pair --json` output.
|
|
31
|
+
|
|
32
|
+
### Changed — `tr-cli.ts` → CLI JSON-first output:
|
|
33
|
+
|
|
34
|
+
- `tr status --json` → `{"version":"3.3.9-rc.1","onboarded":bool,"next_step":"pair|none","tool_count":N,"hybrid_mode":bool}`
|
|
35
|
+
- `tr pair --json` → `{"url":"...","pin":"...","expires_at":"..."}` (delegated to pair-cli-relay.ts, already JSON-capable)
|
|
36
|
+
- `tr remember --json '<fact>'` → `{"ok":true,"id":"...","claim_count":0}`
|
|
37
|
+
- `tr recall --json '<query>' --limit 5` → `{"results":[{"text":"...","score":0.8}]}`
|
|
38
|
+
- Plain text mode kept for direct user CLI use; `--json` flag required for agent shell calls.
|
|
39
|
+
- Updated plugin version reference to `3.3.9-rc.1` in pair delegation.
|
|
40
|
+
|
|
41
|
+
### New tests:
|
|
42
|
+
|
|
43
|
+
- `skill-md-hybrid-primary.test.ts` — asserts SKILL.md contains hybrid-primary instructions, forbidden-vocabulary denylist, relay-based architecture assertion, and autonomous pair call.
|
|
44
|
+
- `tr-cli-json-output.test.ts` — asserts each `tr <cmd> --json` code path returns parseable JSON with expected keys (via static source analysis).
|
|
45
|
+
|
|
46
|
+
### Not changed:
|
|
47
|
+
|
|
48
|
+
- Hooks (`before_agent_start`, `agent_end`, `message_received`, `before_reset`) — not affected by issue #223, unchanged.
|
|
49
|
+
- HTTP routes (4 QR-pairing routes) — unchanged.
|
|
50
|
+
- `/totalreclaw-restart` slash command — kept for environments where users want a manual reload.
|
|
51
|
+
- `api.registerTool()` calls — kept for back-compat (older OpenClaw versions where tools work).
|
|
52
|
+
- Relay code — 502/400 pair page bugs are out of scope.
|
|
53
|
+
- Hermes Python code — separate runtime.
|
|
54
|
+
|
|
7
55
|
## [3.3.7-rc.3] — 2026-05-04
|
|
8
56
|
|
|
9
57
|
Root cause of tool-binding gap on OpenClaw 2026.5.2: the plugin manifest (`openclaw.plugin.json`) was missing the `contracts.tools` declaration that OpenClaw 2026.5.2 now requires before any non-bundled plugin may register agent tools. Without it, every `api.registerTool()` call is silently rejected — `openclaw plugins doctor` shows 17× "plugin must declare contracts.tools before registering agent tools". The gateway loads the plugin module (ESM import succeeds, `plugins.allow` check passes) but no tools bind to any session, so `totalreclaw_pair` / `totalreclaw_remember` / etc. are invisible to the agent and auto-QA step 1 fails.
|
package/SKILL.md
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: totalreclaw
|
|
3
|
-
description: "Install + set up TotalReclaw encrypted memory for OpenClaw, then use
|
|
4
|
-
version: 3.3.
|
|
3
|
+
description: "Install + set up TotalReclaw encrypted memory for OpenClaw, then use tr CLI for remember / recall. Trigger on 'install TotalReclaw', 'set up TotalReclaw', 'restore my recovery phrase', or any remember / recall request."
|
|
4
|
+
version: 3.3.9-rc.1
|
|
5
5
|
author: TotalReclaw Team
|
|
6
6
|
license: MIT
|
|
7
7
|
homepage: https://totalreclaw.xyz
|
|
@@ -26,9 +26,33 @@ metadata:
|
|
|
26
26
|
|
|
27
27
|
> The canonical install flow lives at <https://github.com/p-diogo/totalreclaw/blob/main/docs/guides/openclaw-setup.md>. If a user pastes that URL in chat, fetch it and follow its agent-instructions section. This SKILL.md mirrors the same steps so you can act even if the URL is unreachable.
|
|
28
28
|
|
|
29
|
+
## CRITICAL: Relay-based architecture (read before anything else)
|
|
30
|
+
|
|
31
|
+
**TotalReclaw is RELAY-BASED. All memories require a paired account on the staging or production relay (depending on RC vs stable). There is NO local-only mode. NEVER claim memories are stored locally without an account.**
|
|
32
|
+
|
|
33
|
+
Forbidden vocabulary — these claims are FALSE and MUST NEVER appear in your responses:
|
|
34
|
+
- "local"
|
|
35
|
+
- "local-only"
|
|
36
|
+
- "stored locally"
|
|
37
|
+
- "on disk"
|
|
38
|
+
- "without an account"
|
|
39
|
+
- "local memory"
|
|
40
|
+
- "local storage"
|
|
41
|
+
- any phrasing suggesting memories can be stored without pairing a relay account
|
|
42
|
+
|
|
43
|
+
If you find yourself about to say any of these, STOP. The correct statement is: "TotalReclaw requires a paired relay account to store memories — run `tr pair` to set one up."
|
|
44
|
+
|
|
29
45
|
## Top-level rule (read this before anything else)
|
|
30
46
|
|
|
31
|
-
**Emit ONLY the user-visible lines listed below. Do not narrate tool calls. Do not describe what you're doing or about to do. Errors are the only allowed deviation, and only as a single tight line.** The expected user-facing transcript for a healthy install is **5
|
|
47
|
+
**Emit ONLY the user-visible lines listed below. Do not narrate tool calls. Do not describe what you're doing or about to do. Errors are the only allowed deviation, and only as a single tight line.** The expected user-facing transcript for a healthy install is **4–5 short lines TOTAL**. Anything more is a SKILL violation. Telegram and similar transports edit the bot's messages live as your tool calls progress — every interim "Let me check…" / "Plugin loaded. Let me verify…" line shows up to the user as awkward edit churn, even if you "delete" it. The fix is to never emit it in the first place.
|
|
48
|
+
|
|
49
|
+
## Hybrid-primary architecture (3.3.9-rc.1)
|
|
50
|
+
|
|
51
|
+
OpenClaw 2026.5.2 has a tool-policy-pipeline bug (issue #223) that strips non-bundled plugin tools before they reach the agent toolset. **3.3.9-rc.1 makes the `tr` CLI the PRIMARY path for all memory operations.** Native tools (`totalreclaw_*`) are kept for back-compat with older OpenClaw versions where they work, but the agent MUST NOT depend on them. Use `tr` from your shell tool for everything.
|
|
52
|
+
|
|
53
|
+
**The `tr` binary is installed at:** `~/.openclaw/extensions/totalreclaw/dist/tr-cli.js`
|
|
54
|
+
**Always invoke via:** `node ~/.openclaw/extensions/totalreclaw/dist/tr-cli.js <cmd> [--json]`
|
|
55
|
+
**Never rely on `totalreclaw_*` tool binding** — it may not be available due to issue #223.
|
|
32
56
|
|
|
33
57
|
## User-visible line set (THIS IS ALL YOU SAY)
|
|
34
58
|
|
|
@@ -36,70 +60,70 @@ Speak only these lines, in order. Substitute the bracketed placeholders with the
|
|
|
36
60
|
|
|
37
61
|
1. (start of install)
|
|
38
62
|
> Installing TotalReclaw…
|
|
39
|
-
2. (after install +
|
|
40
|
-
> ✓ Installed (totalreclaw <version>
|
|
41
|
-
3. (
|
|
42
|
-
> Restarting OpenClaw to load the plugin…
|
|
43
|
-
4. (optional — only if you successfully disabled the built-in memory tool to prevent dual-write)
|
|
44
|
-
> Disabled built-in memory tool.
|
|
45
|
-
5. (account-setup URL + PIN, immediately after `totalreclaw_pair` returns)
|
|
63
|
+
2. (after install + CLI verify — `tr status` works)
|
|
64
|
+
> ✓ Installed (totalreclaw <version>, hybrid mode)
|
|
65
|
+
3. (account-setup URL + PIN, IMMEDIATELY after `tr pair --json` returns — no consent gate)
|
|
46
66
|
> Open <url> in your browser. Enter PIN <pin>. Generate or paste a 12-word recovery phrase. Reply `done` once it's sealed.
|
|
47
|
-
|
|
67
|
+
4. (done + free-tier confirmation)
|
|
48
68
|
> ✓ You're set up on the TotalReclaw free tier — 500 memories/month, unlimited reads. https://totalreclaw.xyz/pricing. Ask me to remember anything.
|
|
49
69
|
|
|
50
70
|
If existing credentials are detected (step "Check for existing credentials" below), emit ONLY this line and stop:
|
|
51
71
|
> ✓ TotalReclaw is already set up.
|
|
52
72
|
|
|
53
|
-
## Setup flow
|
|
54
|
-
|
|
55
|
-
If `totalreclaw_*` tools are already bound to your toolset, SKIP install and jump to step 4. Otherwise run all steps in order.
|
|
73
|
+
## Setup flow (hybrid-primary)
|
|
56
74
|
|
|
57
75
|
1. **Install plugin first, skill second (silent — emit user-visible line 1 once at the start, then no prose until line 2).** Run via your shell tool, in this exact order:
|
|
58
76
|
```
|
|
59
77
|
openclaw plugins install @totalreclaw/totalreclaw
|
|
60
78
|
openclaw skills install totalreclaw
|
|
61
79
|
```
|
|
62
|
-
For RC: replace the first command with `openclaw plugins install @totalreclaw/totalreclaw@rc`. **Plugin BEFORE skill is deliberate** (3.3.4-rc.1 — issue from Pedro's QA on rc.3): the skill install triggers a config-driven SIGUSR1 gateway reload that can race a concurrent plugin install.
|
|
80
|
+
For RC: replace the first command with `openclaw plugins install @totalreclaw/totalreclaw@rc`. **Plugin BEFORE skill is deliberate** (3.3.4-rc.1 — issue from Pedro's QA on rc.3): the skill install triggers a config-driven SIGUSR1 gateway reload that can race a concurrent plugin install.
|
|
63
81
|
|
|
64
82
|
If either shell call fails with `command not found` / ENOENT / "not authorized" (managed-service or shell-restricted environment), tell the user verbatim *"Your environment doesn't expose the OpenClaw CLI to me. Please install `totalreclaw` from your service's plugins / skills UI, then reply `done`."* Wait for `done` and skip to step 3.
|
|
65
83
|
|
|
66
|
-
2. **
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
84
|
+
2. **Verify CLI works — emit user-visible line 2 when successful.** Run:
|
|
85
|
+
```
|
|
86
|
+
node ~/.openclaw/extensions/totalreclaw/dist/tr-cli.js status --json
|
|
87
|
+
```
|
|
88
|
+
Parse the JSON. When you get `{"version": "...", "hybrid_mode": true, ...}`, emit user-visible line 2 with the version substituted. **Do NOT wait for tool binding. Do NOT issue `/totalreclaw-restart`. Do NOT check whether `totalreclaw_pair` is in your toolset.** The CLI works independently of tool binding. If `tr status` exits non-zero or the file doesn't exist, wait 5s and retry once — it may still be installing.
|
|
71
89
|
|
|
72
|
-
|
|
90
|
+
3. **Check for existing credentials (silent unless found).** Run `test -s ~/.totalreclaw/credentials.json`. If it exists and is non-empty, emit *"✓ TotalReclaw is already set up."* and stop.
|
|
73
91
|
|
|
74
|
-
|
|
92
|
+
4. **Set up the user's TotalReclaw account IMMEDIATELY — no consent gate.** As soon as `tr status --json` returns and credentials are not present, run:
|
|
93
|
+
```
|
|
94
|
+
node ~/.openclaw/extensions/totalreclaw/dist/tr-cli.js pair --json
|
|
95
|
+
```
|
|
96
|
+
This is UNCONDITIONAL. Do NOT ask "Would you like to set up an account?" or "Want me to pair now?". The user installed TotalReclaw — setup is the expected next step. The URL + PIN itself is the consent moment (the user can simply not open the page to opt out). Parse the JSON response `{"url": "...", "pin": "...", "expires_at": "..."}` and emit user-visible line 3. If `tr pair --json` is not available (old install), fall back to `totalreclaw_pair` tool or `openclaw totalreclaw pair generate --url-pin-only`.
|
|
75
97
|
|
|
76
|
-
|
|
98
|
+
5. **Verify and confirm (silent until line 4).** After the user says `done`, re-run `test -s ~/.totalreclaw/credentials.json`. If missing, the PIN expired — run `tr pair --json` again and resend line 3 once. If present, emit user-visible line 4 and stop.
|
|
77
99
|
|
|
78
|
-
|
|
100
|
+
## `tr` CLI reference (hybrid-primary commands)
|
|
79
101
|
|
|
80
|
-
|
|
81
|
-
- "Let me check that…"
|
|
82
|
-
- "Plugin loaded. Let me verify…"
|
|
83
|
-
- "I'll now…"
|
|
84
|
-
- "Quick fix…"
|
|
85
|
-
- "However, I don't see…"
|
|
86
|
-
- "I need to use…"
|
|
87
|
-
- "Should I /totalreclaw-restart" / "Do you have a public URL" / "Want me to restart" / "Let me check if the tool is bound" / "I need permission to restart"
|
|
88
|
-
- "Now installing…" / "Got it" / "In parallel…" / "Step 1a/1b/1c"
|
|
102
|
+
Always use `--json` flag when running from shell for machine-parseable output. Plain text mode is for direct user invocation only.
|
|
89
103
|
|
|
90
|
-
|
|
104
|
+
```bash
|
|
105
|
+
# Status check (parse JSON to confirm install + onboarding state)
|
|
106
|
+
node ~/.openclaw/extensions/totalreclaw/dist/tr-cli.js status --json
|
|
107
|
+
# Returns: {"version":"3.3.9-rc.1","onboarded":false,"next_step":"pair","tool_count":17,"hybrid_mode":true}
|
|
91
108
|
|
|
92
|
-
|
|
109
|
+
# Pair / account setup
|
|
110
|
+
node ~/.openclaw/extensions/totalreclaw/dist/tr-cli.js pair --json
|
|
111
|
+
# Returns: {"url":"https://...","pin":"123456","expires_at":"2026-05-05T..."}
|
|
93
112
|
|
|
94
|
-
|
|
113
|
+
# Remember a fact
|
|
114
|
+
node ~/.openclaw/extensions/totalreclaw/dist/tr-cli.js remember --json "I work at Graph Foundation"
|
|
115
|
+
# Returns: {"ok":true,"id":"...","claim_count":N}
|
|
95
116
|
|
|
96
|
-
|
|
117
|
+
# Recall memories
|
|
118
|
+
node ~/.openclaw/extensions/totalreclaw/dist/tr-cli.js recall --json "where does the user work" --limit 5
|
|
119
|
+
# Returns: {"results":[{"text":"...","score":0.8},...]}
|
|
120
|
+
```
|
|
97
121
|
|
|
98
|
-
|
|
122
|
+
**Note on tool names**: `totalreclaw_remember`, `totalreclaw_recall`, etc. may or may not appear in your toolset depending on OpenClaw version. Under hybrid-primary, you use the `tr` CLI instead. If native tools ARE available (older OpenClaw or future OC fix), they work too — but always prefer `tr` for reliability.
|
|
99
123
|
|
|
100
124
|
## Rendering the QR on your transport (rc.5+)
|
|
101
125
|
|
|
102
|
-
When you receive the `totalreclaw_pair` payload, render the QR based on your chat transport:
|
|
126
|
+
When you receive the `totalreclaw_pair` payload (from tool OR `tr pair --json`), render the QR based on your chat transport:
|
|
103
127
|
|
|
104
128
|
- **Transport supports image attachments** (Telegram, Slack, web chat, any transport with file/image upload): decode `qr_png_b64` from base64 to bytes, save to a tempfile with a `.png` suffix, and attach the tempfile as an image. Also show the URL + PIN as text so users who prefer clicking can still do so.
|
|
105
129
|
- **Terminal-only transport** (OpenClaw native CLI chat, bare SSH shell): emit `qr_unicode` inline in your reply. It renders as block characters in most terminal fonts; the user scans from their phone.
|
|
@@ -109,24 +133,64 @@ The QR encodes ONLY the account-setup URL. The 6-digit PIN is a separate out-of-
|
|
|
109
133
|
|
|
110
134
|
## Phrase safety (HARD — never break)
|
|
111
135
|
|
|
112
|
-
NEVER echo / generate / ask the user to paste a recovery phrase in chat. NEVER invoke `openclaw totalreclaw onboard` (bare interactive form), `totalreclaw setup`, or any phrase-touching CLI via your shell tool (stdout enters LLM context). The ONLY agent-facilitated account-setup paths are (in order of preference): (1) the `totalreclaw_pair` account-setup tool; (
|
|
136
|
+
NEVER echo / generate / ask the user to paste a recovery phrase in chat. NEVER invoke `openclaw totalreclaw onboard` (bare interactive form), `totalreclaw setup`, or any phrase-touching CLI via your shell tool (stdout enters LLM context). The ONLY agent-facilitated account-setup paths are (in order of preference): (1) `tr pair --json`; (2) the `totalreclaw_pair` account-setup tool; (3) if both are missing — shell out to EITHER `openclaw totalreclaw pair generate --url-pin-only` (single-line JSON `{v,url,pin,expires_at_ms}`) OR `openclaw totalreclaw onboard --pair-only` (single-line JSON `{v,pair_url,pin,expires_at_ms}`). Both have zero phrase material on stdout (account-setup crypto is x25519-only, not BIP-39). If the user pastes a phrase anyway: tell them it is compromised, re-run `tr pair --json` for a fresh wallet.
|
|
113
137
|
|
|
114
138
|
## Usage (post-setup)
|
|
115
139
|
|
|
116
|
-
- Stable user facts / preferences / identity / "remember X"
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
-
|
|
140
|
+
- Stable user facts / preferences / identity / "remember X":
|
|
141
|
+
```
|
|
142
|
+
node ~/.openclaw/extensions/totalreclaw/dist/tr-cli.js remember --json "<fact>"
|
|
143
|
+
```
|
|
144
|
+
- First-person factual query ("do I / what's my / where do I…"):
|
|
145
|
+
```
|
|
146
|
+
node ~/.openclaw/extensions/totalreclaw/dist/tr-cli.js recall --json "<query>" --limit 5
|
|
147
|
+
```
|
|
148
|
+
Recall FIRST, then answer from returned facts. If 0 results, say so.
|
|
149
|
+
- For forget / pin / unpin / retype / set_scope — use native tools if available, or ask user to run `openclaw totalreclaw` subcommand.
|
|
150
|
+
- "status" → `tr status --json` (parse version + onboarded + next_step).
|
|
151
|
+
- "upgrade" → `totalreclaw_upgrade` native tool (paste Stripe URL verbatim). Post-upgrade testnet → mainnet: `totalreclaw_migrate` with `dry_run` first.
|
|
152
|
+
- "import / export / consolidate" → `totalreclaw_import_from` · `totalreclaw_export` · `totalreclaw_consolidate` native tools.
|
|
153
|
+
|
|
154
|
+
**Forbidden vocabulary** (these are anti-patterns that violate the silence rule):
|
|
155
|
+
- "Let me check that…"
|
|
156
|
+
- "Plugin loaded. Let me verify…"
|
|
157
|
+
- "I'll now…"
|
|
158
|
+
- "Quick fix…"
|
|
159
|
+
- "However, I don't see…"
|
|
160
|
+
- "I need to use…"
|
|
161
|
+
- "Should I /totalreclaw-restart" / "Do you have a public URL" / "Want me to restart" / "Let me check if the tool is bound" / "I need permission to restart"
|
|
162
|
+
- "Now installing…" / "Got it" / "In parallel…" / "Step 1a/1b/1c"
|
|
163
|
+
- "local" / "local-only" / "stored locally" / "on disk" / "without an account" / "local memory" / "local storage"
|
|
121
164
|
|
|
122
165
|
## Diagnostics
|
|
123
166
|
|
|
124
|
-
- `
|
|
125
|
-
-
|
|
167
|
+
- `tr` CLI not found → plugin not installed or install still in progress. Wait 5s, retry. If still missing, retry `openclaw plugins install @totalreclaw/totalreclaw@rc` once.
|
|
168
|
+
- `totalreclaw_*` tools not visible → EXPECTED under OpenClaw 2026.5.2 (issue #223). Use `tr` CLI instead. Do not restart, do not issue `/totalreclaw-restart`, do not issue `/new`.
|
|
169
|
+
- If `/totalreclaw-restart` is needed for another reason, issue the literal slash command `/totalreclaw-restart` — nothing else. Do not narrate.
|
|
170
|
+
- User says done but `credentials.json` missing → PIN expired or entered wrong phrase; re-run `tr pair --json` and resend line 3 once.
|
|
126
171
|
- `onboarding required` → credentials missing; redo from the account-setup step.
|
|
127
|
-
- `quota exceeded` → `totalreclaw_status`, then offer `totalreclaw_upgrade`.
|
|
172
|
+
- `quota exceeded` → `tr status --json` or `totalreclaw_status`, then offer `totalreclaw_upgrade`.
|
|
128
173
|
- `No LLM available for auto-extraction` at startup → provider key unreachable; check `~/.openclaw/agents/<agent>/agent/auth-profiles.json` or plugin config `extraction.llm`.
|
|
129
174
|
|
|
175
|
+
## Required post-install config (OpenClaw 2026.5.2+)
|
|
176
|
+
|
|
177
|
+
Add to `~/.openclaw/openclaw.json` after installing:
|
|
178
|
+
```json
|
|
179
|
+
{
|
|
180
|
+
"plugins": {
|
|
181
|
+
"allow": ["totalreclaw"],
|
|
182
|
+
"entries": {
|
|
183
|
+
"totalreclaw": {
|
|
184
|
+
"hooks": { "allowConversationAccess": true }
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
`plugins.allow` suppresses the "plugins.allow is empty" warning. `hooks.allowConversationAccess` unlocks typed hooks (`agent_end`, etc.) for auto-extraction.
|
|
192
|
+
|
|
130
193
|
## Tool surface
|
|
131
194
|
|
|
132
|
-
|
|
195
|
+
Hybrid-primary: `tr remember` · `tr recall` · `tr pair` · `tr status` (primary path for all agent ops)
|
|
196
|
+
Native fallback (when available): `totalreclaw_pair` · `_remember` · `_recall` · `_forget` · `_pin` · `_unpin` · `_retype` · `_set_scope` · `_export` · `_status` · `_upgrade` · `_migrate` · `_import_from` · `_import_batch` · `_consolidate` · `_onboarding_start` · `_report_qa_bug` (RC only)
|
package/dist/index.js
CHANGED
|
@@ -2358,6 +2358,29 @@ const plugin = {
|
|
|
2358
2358
|
// write would race that freeze.
|
|
2359
2359
|
const _registeredToolNames = [];
|
|
2360
2360
|
const _originalRegisterTool = api.registerTool.bind(api);
|
|
2361
|
+
// 3.3.8-rc.1 HYBRID MODE (OpenClaw 2026.5.2 issue #223 workaround):
|
|
2362
|
+
// The tool-policy-pipeline in OC 2026.5.2 strips non-bundled plugin tools
|
|
2363
|
+
// before they reach the agent's session toolset. registerTool() calls
|
|
2364
|
+
// succeed and tools are declared in contracts.tools, so the PLUGIN LOADS.
|
|
2365
|
+
// But tool calls never reach execute() — the pipeline discards them before
|
|
2366
|
+
// the agent's toolset is built.
|
|
2367
|
+
//
|
|
2368
|
+
// Strategy: keep all registerTool() calls intact so the plugin loader can
|
|
2369
|
+
// verify the contracts.tools declaration and load the plugin (hooks fire).
|
|
2370
|
+
// The `tr` CLI binary (dist/tr-cli.js) provides the alternative execution
|
|
2371
|
+
// path. Agent runs `tr remember|recall|status|pair` from shell; tool calls
|
|
2372
|
+
// are dead-letter but hooks (before_agent_start, agent_end, message_received,
|
|
2373
|
+
// before_reset) still fire via the unbroken hook code path.
|
|
2374
|
+
//
|
|
2375
|
+
// NOTE: do NOT no-op registerTool here — OC 2026.5.2 validates the
|
|
2376
|
+
// contracts.tools declaration against registered tools at load time and
|
|
2377
|
+
// drops the plugin (unloads it) if no tools match. Confirmed empirically:
|
|
2378
|
+
// no-op'ing registerTool causes the gateway to log "4 plugins" instead of
|
|
2379
|
+
// "5 plugins" after restart (plugin excluded from active set).
|
|
2380
|
+
//
|
|
2381
|
+
// TODO: when OC ships a fix for issue #223, restore tool-call routing
|
|
2382
|
+
// and remove the tr-cli.ts CLI layer. The bin/tr field in package.json
|
|
2383
|
+
// can stay as a convenience CLI regardless.
|
|
2361
2384
|
api.registerTool = (tool, opts) => {
|
|
2362
2385
|
try {
|
|
2363
2386
|
const t = tool;
|
|
@@ -5932,9 +5955,14 @@ const plugin = {
|
|
|
5932
5955
|
loadedAt: Date.now(),
|
|
5933
5956
|
tools: _registeredToolNames.slice(),
|
|
5934
5957
|
version: pluginVersion ?? 'unknown',
|
|
5958
|
+
// 3.3.8-rc.1 hybrid mode annotation: tools ARE registered with the
|
|
5959
|
+
// SDK (required for plugin loader validation), but tool calls are
|
|
5960
|
+
// dead-letter on OC 2026.5.2 due to issue #223. Use `tr <cmd>` CLI.
|
|
5961
|
+
hybridMode: true,
|
|
5962
|
+
hybridCliTools: ['tr status', 'tr pair', 'tr remember', 'tr recall'],
|
|
5935
5963
|
});
|
|
5936
5964
|
if (ok) {
|
|
5937
|
-
api.logger.info(`TotalReclaw: wrote .loaded.json manifest (${_registeredToolNames.length} tools, version=${pluginVersion ?? 'unknown'})`);
|
|
5965
|
+
api.logger.info(`TotalReclaw: wrote .loaded.json manifest (${_registeredToolNames.length} tools + hybridCli=tr, version=${pluginVersion ?? 'unknown'})`);
|
|
5938
5966
|
}
|
|
5939
5967
|
}
|
|
5940
5968
|
catch {
|
package/dist/tr-cli.js
ADDED
|
@@ -0,0 +1,378 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* tr — TotalReclaw hybrid CLI (3.3.9-rc.1 primary architecture)
|
|
4
|
+
*
|
|
5
|
+
* OpenClaw 2026.5.2 has a tool-policy-pipeline bug (issue #223) that strips non-bundled plugin
|
|
6
|
+
* tools before they reach the agent toolset. In 3.3.9-rc.1, this CLI is the PRIMARY path for
|
|
7
|
+
* all agent memory operations (not a fallback). The agent runs `tr <cmd> --json` from shell;
|
|
8
|
+
* hooks (before_agent_start, agent_end, message_received, before_reset) continue via the
|
|
9
|
+
* unbroken hook code path.
|
|
10
|
+
*
|
|
11
|
+
* Phrase-safety: this CLI reads credentials.json (mnemonic at rest) but NEVER
|
|
12
|
+
* prints the mnemonic to stdout, stderr, or any log. Phrase only enters via QR-pair
|
|
13
|
+
* browser tier (pair-cli.ts / pair-cli-relay.ts — unchanged).
|
|
14
|
+
*
|
|
15
|
+
* Commands:
|
|
16
|
+
* tr status [--json] — print onboarding + credentials state
|
|
17
|
+
* tr pair [--json] — start a relay pairing session, print URL+PIN+QR
|
|
18
|
+
* tr remember [--json] <text> — store a memory in the encrypted vault
|
|
19
|
+
* tr recall [--json] [--limit N] <query> — search the encrypted vault
|
|
20
|
+
*
|
|
21
|
+
* --json flag: all agent-facing CLI calls MUST use --json for clean machine-parseable output.
|
|
22
|
+
* Plain text mode is for direct user CLI use only.
|
|
23
|
+
*
|
|
24
|
+
* Install: wired via package.json `bin.tr` → dist/tr-cli.js
|
|
25
|
+
* Usage from container: `docker exec tr-openclaw node ~/.openclaw/extensions/totalreclaw/dist/tr-cli.js status --json`
|
|
26
|
+
*/
|
|
27
|
+
import path from 'node:path';
|
|
28
|
+
import os from 'node:os';
|
|
29
|
+
import { randomUUID } from 'node:crypto';
|
|
30
|
+
import { CONFIG } from './config.js';
|
|
31
|
+
import { loadCredentialsJson } from './fs-helpers.js';
|
|
32
|
+
import { printStatus } from './onboarding-cli.js';
|
|
33
|
+
import { deriveKeys, computeAuthKeyHash, encrypt, decrypt, generateBlindIndices, generateContentFingerprint, } from './crypto.js';
|
|
34
|
+
import { createApiClient } from './api-client.js';
|
|
35
|
+
// ---------------------------------------------------------------------------
|
|
36
|
+
// Helpers
|
|
37
|
+
// ---------------------------------------------------------------------------
|
|
38
|
+
const CREDENTIALS_PATH = CONFIG.credentialsPath;
|
|
39
|
+
const SERVER_URL = CONFIG.serverUrl;
|
|
40
|
+
const STATE_PATH = CONFIG.onboardingStatePath;
|
|
41
|
+
const PLUGIN_VERSION = '3.3.9-rc.1';
|
|
42
|
+
function die(msg, code = 1) {
|
|
43
|
+
process.stderr.write(`tr: ${msg}\n`);
|
|
44
|
+
process.exit(code);
|
|
45
|
+
}
|
|
46
|
+
function log(msg) {
|
|
47
|
+
process.stdout.write(msg + '\n');
|
|
48
|
+
}
|
|
49
|
+
/** Parse --flag from args array, returning the cleaned args without the flag. */
|
|
50
|
+
function popFlag(args, flag) {
|
|
51
|
+
const idx = args.indexOf(flag);
|
|
52
|
+
if (idx === -1)
|
|
53
|
+
return [false, args];
|
|
54
|
+
return [true, [...args.slice(0, idx), ...args.slice(idx + 1)]];
|
|
55
|
+
}
|
|
56
|
+
/** Parse --limit N from args, returning [limit, cleanedArgs]. Default: defaultLimit. */
|
|
57
|
+
function popLimitFlag(args, defaultLimit) {
|
|
58
|
+
const idx = args.indexOf('--limit');
|
|
59
|
+
if (idx === -1 || idx + 1 >= args.length)
|
|
60
|
+
return [defaultLimit, args];
|
|
61
|
+
const n = parseInt(args[idx + 1], 10);
|
|
62
|
+
const limit = isNaN(n) || n < 1 ? defaultLimit : n;
|
|
63
|
+
return [limit, [...args.slice(0, idx), ...args.slice(idx + 2)]];
|
|
64
|
+
}
|
|
65
|
+
async function buildContext() {
|
|
66
|
+
const creds = loadCredentialsJson(CREDENTIALS_PATH);
|
|
67
|
+
if (!creds) {
|
|
68
|
+
die('TotalReclaw is not set up. Run: node ~/.openclaw/extensions/totalreclaw/dist/tr-cli.js pair --json');
|
|
69
|
+
}
|
|
70
|
+
const mnemonic = (typeof creds.mnemonic === 'string' && creds.mnemonic.trim()) ||
|
|
71
|
+
(typeof creds.recovery_phrase === 'string' && creds.recovery_phrase.trim()) ||
|
|
72
|
+
'';
|
|
73
|
+
if (!mnemonic) {
|
|
74
|
+
die('No recovery phrase in credentials.json. Run: tr pair --json');
|
|
75
|
+
}
|
|
76
|
+
// Parse existing salt/userId from credentials.json
|
|
77
|
+
let existingSalt;
|
|
78
|
+
let existingUserId;
|
|
79
|
+
const saltStr = typeof creds.salt === 'string' ? creds.salt : undefined;
|
|
80
|
+
if (saltStr) {
|
|
81
|
+
if (/^[0-9a-f]{64}$/i.test(saltStr)) {
|
|
82
|
+
existingSalt = Buffer.from(saltStr, 'hex');
|
|
83
|
+
}
|
|
84
|
+
else {
|
|
85
|
+
existingSalt = Buffer.from(saltStr, 'base64');
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
existingUserId = typeof creds.userId === 'string' ? creds.userId : undefined;
|
|
89
|
+
const keys = deriveKeys(mnemonic, existingSalt);
|
|
90
|
+
const authKeyHex = keys.authKey.toString('hex');
|
|
91
|
+
const apiClient = createApiClient(SERVER_URL);
|
|
92
|
+
let userId;
|
|
93
|
+
if (existingUserId) {
|
|
94
|
+
userId = existingUserId;
|
|
95
|
+
}
|
|
96
|
+
else {
|
|
97
|
+
// Register to get userId (idempotent on relay)
|
|
98
|
+
const authHash = computeAuthKeyHash(keys.authKey);
|
|
99
|
+
const saltHex = keys.salt.toString('hex');
|
|
100
|
+
try {
|
|
101
|
+
const result = await apiClient.register(authHash, saltHex);
|
|
102
|
+
userId = result.user_id;
|
|
103
|
+
}
|
|
104
|
+
catch (err) {
|
|
105
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
106
|
+
if (msg.includes('USER_EXISTS')) {
|
|
107
|
+
userId = authHash.slice(0, 32);
|
|
108
|
+
}
|
|
109
|
+
else {
|
|
110
|
+
die(`Relay registration failed: ${msg}`);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
return {
|
|
115
|
+
authKeyHex,
|
|
116
|
+
encryptionKey: keys.encryptionKey,
|
|
117
|
+
dedupKey: keys.dedupKey,
|
|
118
|
+
apiClient,
|
|
119
|
+
userId,
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
// ---------------------------------------------------------------------------
|
|
123
|
+
// Command: status
|
|
124
|
+
// ---------------------------------------------------------------------------
|
|
125
|
+
async function cmdStatus(jsonMode) {
|
|
126
|
+
// Probe plugin manifest for version/hybridMode/toolCount.
|
|
127
|
+
let pluginVersion;
|
|
128
|
+
let bootCount;
|
|
129
|
+
let hybridMode = true; // default true in 3.3.9-rc.1 (hybrid-primary)
|
|
130
|
+
let toolCount;
|
|
131
|
+
let loadedAgeSec;
|
|
132
|
+
try {
|
|
133
|
+
const fs = await import('node:fs');
|
|
134
|
+
const candidatePaths = [
|
|
135
|
+
path.join(os.homedir(), '.openclaw', 'extensions', 'totalreclaw', '.loaded.json'),
|
|
136
|
+
path.join(os.homedir(), '.openclaw', 'npm', 'node_modules', '@totalreclaw', 'totalreclaw', 'dist', '.loaded.json'),
|
|
137
|
+
];
|
|
138
|
+
const resolvedPath = candidatePaths.find((p) => fs.existsSync(p));
|
|
139
|
+
if (resolvedPath) {
|
|
140
|
+
const raw = fs.readFileSync(resolvedPath, 'utf-8');
|
|
141
|
+
const manifest = JSON.parse(raw);
|
|
142
|
+
pluginVersion = manifest.version ?? PLUGIN_VERSION;
|
|
143
|
+
bootCount = manifest.bootCount;
|
|
144
|
+
hybridMode = manifest.hybridMode !== false; // default true
|
|
145
|
+
toolCount = manifest.tools?.length;
|
|
146
|
+
const ageMs = Date.now() - (manifest.loadedAt ?? 0);
|
|
147
|
+
loadedAgeSec = Math.round(ageMs / 1000);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
catch {
|
|
151
|
+
// Best-effort
|
|
152
|
+
}
|
|
153
|
+
// Check onboarding state
|
|
154
|
+
const creds = loadCredentialsJson(CREDENTIALS_PATH);
|
|
155
|
+
const onboarded = !!creds;
|
|
156
|
+
if (jsonMode) {
|
|
157
|
+
// JSON-first output for agent parsing
|
|
158
|
+
const out = {
|
|
159
|
+
version: pluginVersion ?? PLUGIN_VERSION,
|
|
160
|
+
onboarded,
|
|
161
|
+
next_step: onboarded ? 'none' : 'pair',
|
|
162
|
+
tool_count: toolCount ?? 17,
|
|
163
|
+
hybrid_mode: hybridMode,
|
|
164
|
+
};
|
|
165
|
+
if (bootCount !== undefined)
|
|
166
|
+
out.boot_count = bootCount;
|
|
167
|
+
if (loadedAgeSec !== undefined)
|
|
168
|
+
out.loaded_age_sec = loadedAgeSec;
|
|
169
|
+
log(JSON.stringify(out));
|
|
170
|
+
}
|
|
171
|
+
else {
|
|
172
|
+
// Human-readable plain text for direct user CLI use
|
|
173
|
+
printStatus(CREDENTIALS_PATH, STATE_PATH, process.stdout);
|
|
174
|
+
process.stdout.write(`\n plugin: ${pluginVersion ? `loaded (version=${pluginVersion}` + (bootCount !== undefined ? ` bootCount=${bootCount}` : '') + (loadedAgeSec !== undefined ? ` loaded=${loadedAgeSec}s ago` : '') + ')' : 'not found in .loaded.json'}\n` +
|
|
175
|
+
` hybrid-mode: ${hybridMode ? 'yes (primary — use tr <cmd> --json)' : 'no'}\n` +
|
|
176
|
+
` hooks: before_agent_start, agent_end, message_received, before_reset\n`);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
// ---------------------------------------------------------------------------
|
|
180
|
+
// Command: pair
|
|
181
|
+
// ---------------------------------------------------------------------------
|
|
182
|
+
async function cmdPair(args) {
|
|
183
|
+
// Delegate to the existing pair-cli-relay.ts via a thin wrapper.
|
|
184
|
+
// The pair flow is relay-brokered (works through Docker NAT).
|
|
185
|
+
// Phrase-safety: pair-cli-relay.ts is x25519-only; mnemonic never appears.
|
|
186
|
+
const outputMode = args.includes('--json') ? 'json' : args.includes('--url-pin') ? 'url-pin' : 'human';
|
|
187
|
+
const { runRelayPairCli } = await import('./pair-cli-relay.js');
|
|
188
|
+
const { defaultRenderQr, buildDefaultPairCliIo } = await import('./pair-cli.js');
|
|
189
|
+
const io = buildDefaultPairCliIo();
|
|
190
|
+
const outcome = await runRelayPairCli('generate', {
|
|
191
|
+
relayBaseUrl: CONFIG.pairRelayUrl,
|
|
192
|
+
credentialsPath: CREDENTIALS_PATH,
|
|
193
|
+
onboardingStatePath: STATE_PATH,
|
|
194
|
+
logger: {
|
|
195
|
+
info: (m) => process.stderr.write(`[info] ${m}\n`),
|
|
196
|
+
warn: (m) => process.stderr.write(`[warn] ${m}\n`),
|
|
197
|
+
error: (m) => process.stderr.write(`[error] ${m}\n`),
|
|
198
|
+
},
|
|
199
|
+
pluginVersion: PLUGIN_VERSION,
|
|
200
|
+
deriveScopeAddress: undefined,
|
|
201
|
+
renderQr: defaultRenderQr,
|
|
202
|
+
io,
|
|
203
|
+
outputMode: outputMode,
|
|
204
|
+
});
|
|
205
|
+
if (outcome.status !== 'completed' && outcome.status !== 'canceled') {
|
|
206
|
+
die(`Pairing ${outcome.status}`, 1);
|
|
207
|
+
}
|
|
208
|
+
if (outcome.status === 'canceled') {
|
|
209
|
+
process.exit(130);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
// ---------------------------------------------------------------------------
|
|
213
|
+
// Command: remember
|
|
214
|
+
// ---------------------------------------------------------------------------
|
|
215
|
+
async function cmdRemember(rawArgs) {
|
|
216
|
+
const [jsonMode, args] = popFlag(rawArgs, '--json');
|
|
217
|
+
const text = args.join(' ').trim();
|
|
218
|
+
if (!text) {
|
|
219
|
+
die('Usage: tr remember [--json] <text>');
|
|
220
|
+
}
|
|
221
|
+
const ctx = await buildContext();
|
|
222
|
+
// Build a minimal MemoryTaxonomy v1 claim blob (same format as storeExtractedFacts)
|
|
223
|
+
const now = new Date().toISOString();
|
|
224
|
+
const factId = randomUUID().replace(/-/g, '');
|
|
225
|
+
// Encrypt the memory text
|
|
226
|
+
const blob = JSON.stringify({
|
|
227
|
+
text,
|
|
228
|
+
type: 'claim',
|
|
229
|
+
source: 'user',
|
|
230
|
+
scope: 'unspecified',
|
|
231
|
+
importance: 8,
|
|
232
|
+
metadata: {
|
|
233
|
+
type: 'claim',
|
|
234
|
+
source: 'user',
|
|
235
|
+
scope: 'unspecified',
|
|
236
|
+
importance: 8,
|
|
237
|
+
},
|
|
238
|
+
timestamp: now,
|
|
239
|
+
version: 'v1',
|
|
240
|
+
});
|
|
241
|
+
const encrypted_blob = encrypt(blob, ctx.encryptionKey);
|
|
242
|
+
const blind_indices = generateBlindIndices(text);
|
|
243
|
+
const content_fp = generateContentFingerprint(text, ctx.dedupKey);
|
|
244
|
+
const payload = {
|
|
245
|
+
id: factId,
|
|
246
|
+
timestamp: now,
|
|
247
|
+
encrypted_blob,
|
|
248
|
+
blind_indices,
|
|
249
|
+
decay_score: 8,
|
|
250
|
+
source: 'cli:tr-remember',
|
|
251
|
+
content_fp,
|
|
252
|
+
};
|
|
253
|
+
try {
|
|
254
|
+
await ctx.apiClient.store(ctx.userId, [payload], ctx.authKeyHex);
|
|
255
|
+
if (jsonMode) {
|
|
256
|
+
// JSON-first output for agent parsing
|
|
257
|
+
// claim_count requires an extra relay call to tally stored claims; not worth the latency — use 0
|
|
258
|
+
log(JSON.stringify({ ok: true, id: factId, claim_count: 0 }));
|
|
259
|
+
}
|
|
260
|
+
else {
|
|
261
|
+
log(`ok — stored memory (id=${factId})`);
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
catch (err) {
|
|
265
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
266
|
+
die(`remember failed: ${msg}`);
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
// ---------------------------------------------------------------------------
|
|
270
|
+
// Command: recall
|
|
271
|
+
// ---------------------------------------------------------------------------
|
|
272
|
+
async function cmdRecall(rawArgs) {
|
|
273
|
+
const [jsonMode, argsAfterJson] = popFlag(rawArgs, '--json');
|
|
274
|
+
const [limit, argsAfterLimit] = popLimitFlag(argsAfterJson, 5);
|
|
275
|
+
const query = argsAfterLimit.join(' ').trim();
|
|
276
|
+
if (!query) {
|
|
277
|
+
die('Usage: tr recall [--json] [--limit N] <query>');
|
|
278
|
+
}
|
|
279
|
+
const ctx = await buildContext();
|
|
280
|
+
// Generate word trapdoors for blind search
|
|
281
|
+
const trapdoors = generateBlindIndices(query);
|
|
282
|
+
if (trapdoors.length === 0) {
|
|
283
|
+
if (jsonMode) {
|
|
284
|
+
log(JSON.stringify({ results: [] }));
|
|
285
|
+
}
|
|
286
|
+
else {
|
|
287
|
+
log('No results (0 searchable terms in query).');
|
|
288
|
+
}
|
|
289
|
+
return;
|
|
290
|
+
}
|
|
291
|
+
try {
|
|
292
|
+
const candidates = await ctx.apiClient.search(ctx.userId, trapdoors, Math.min(limit * 2, 20), ctx.authKeyHex);
|
|
293
|
+
const results = [];
|
|
294
|
+
for (const c of candidates) {
|
|
295
|
+
try {
|
|
296
|
+
const raw = decrypt(c.encrypted_blob, ctx.encryptionKey);
|
|
297
|
+
const parsed = JSON.parse(raw);
|
|
298
|
+
if (parsed.text) {
|
|
299
|
+
results.push({
|
|
300
|
+
text: parsed.text,
|
|
301
|
+
score: c.decay_score,
|
|
302
|
+
});
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
catch {
|
|
306
|
+
// Skip undecryptable
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
// Sort by score descending, then trim to limit
|
|
310
|
+
results.sort((a, b) => b.score - a.score);
|
|
311
|
+
const trimmed = results.slice(0, limit);
|
|
312
|
+
if (jsonMode) {
|
|
313
|
+
// JSON-first output for agent parsing — canonical format per spec
|
|
314
|
+
log(JSON.stringify({ results: trimmed }));
|
|
315
|
+
}
|
|
316
|
+
else {
|
|
317
|
+
log(`Found ${trimmed.length} result(s) for: ${query}`);
|
|
318
|
+
for (const r of trimmed) {
|
|
319
|
+
log(` [score=${r.score.toFixed(2)}] ${r.text}`);
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
catch (err) {
|
|
324
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
325
|
+
die(`recall failed: ${msg}`);
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
// ---------------------------------------------------------------------------
|
|
329
|
+
// Dispatch
|
|
330
|
+
// ---------------------------------------------------------------------------
|
|
331
|
+
async function main() {
|
|
332
|
+
const args = process.argv.slice(2);
|
|
333
|
+
const cmd = args[0];
|
|
334
|
+
switch (cmd) {
|
|
335
|
+
case 'status': {
|
|
336
|
+
const [jsonMode] = popFlag(args.slice(1), '--json');
|
|
337
|
+
await cmdStatus(jsonMode);
|
|
338
|
+
break;
|
|
339
|
+
}
|
|
340
|
+
case 'pair':
|
|
341
|
+
await cmdPair(args.slice(1));
|
|
342
|
+
break;
|
|
343
|
+
case 'remember':
|
|
344
|
+
await cmdRemember(args.slice(1));
|
|
345
|
+
break;
|
|
346
|
+
case 'recall':
|
|
347
|
+
await cmdRecall(args.slice(1));
|
|
348
|
+
break;
|
|
349
|
+
case undefined:
|
|
350
|
+
case '--help':
|
|
351
|
+
case '-h':
|
|
352
|
+
process.stdout.write(`TotalReclaw hybrid CLI v${PLUGIN_VERSION} (primary mode — OpenClaw 2026.5.2+)\n\n` +
|
|
353
|
+
'Usage:\n' +
|
|
354
|
+
' tr status [--json] — onboarding + plugin load state\n' +
|
|
355
|
+
' tr pair [--json] — start a relay pairing session\n' +
|
|
356
|
+
' tr remember [--json] <text> — store a memory\n' +
|
|
357
|
+
' tr recall [--json] [--limit N] <query> — search memories (default limit: 5)\n\n' +
|
|
358
|
+
'Flags:\n' +
|
|
359
|
+
' --json Output machine-parseable JSON (required for agent shell calls)\n' +
|
|
360
|
+
' --limit N Limit recall results (default: 5)\n\n' +
|
|
361
|
+
'JSON output shapes:\n' +
|
|
362
|
+
' status: {"version":"...","onboarded":bool,"next_step":"pair|none","tool_count":N,"hybrid_mode":bool}\n' +
|
|
363
|
+
' pair: {"url":"...","pin":"123456","expires_at":"..."}\n' +
|
|
364
|
+
' remember: {"ok":true,"id":"...","claim_count":N}\n' +
|
|
365
|
+
' recall: {"results":[{"text":"...","score":0.8}]}\n\n' +
|
|
366
|
+
'Environment:\n' +
|
|
367
|
+
' TOTALRECLAW_SERVER_URL — relay URL (default: api-staging.totalreclaw.xyz)\n' +
|
|
368
|
+
' TOTALRECLAW_CREDENTIALS_PATH — override credentials.json path\n');
|
|
369
|
+
break;
|
|
370
|
+
default:
|
|
371
|
+
die(`Unknown command: ${cmd}. Run \`tr --help\` for usage.`);
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
main().catch((err) => {
|
|
375
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
376
|
+
process.stderr.write(`tr: fatal: ${msg}\n`);
|
|
377
|
+
process.exit(2);
|
|
378
|
+
});
|
package/fs-helpers.ts
CHANGED
|
@@ -594,6 +594,18 @@ export interface PluginLoadManifest {
|
|
|
594
594
|
* verify the manifest is from the currently-running container vs a
|
|
595
595
|
* stale-mounted copy. */
|
|
596
596
|
pid?: number;
|
|
597
|
+
/**
|
|
598
|
+
* 3.3.8-rc.1 — true when registerTool() calls are no-op'd due to the
|
|
599
|
+
* OC 2026.5.2 issue #223 hybrid workaround. Tools in `tools[]` are
|
|
600
|
+
* exposed via the `tr` CLI binary instead of via the plugin API.
|
|
601
|
+
*/
|
|
602
|
+
hybridMode?: boolean;
|
|
603
|
+
/**
|
|
604
|
+
* 3.3.8-rc.1 — CLI commands that replace the tool registrations
|
|
605
|
+
* when hybridMode=true. Agent runs these from shell instead of using
|
|
606
|
+
* tool calls.
|
|
607
|
+
*/
|
|
608
|
+
hybridCliTools?: string[];
|
|
597
609
|
}
|
|
598
610
|
|
|
599
611
|
/** Schema written to `.error.json` when register() throws. */
|
package/index.ts
CHANGED
|
@@ -2926,6 +2926,29 @@ const plugin = {
|
|
|
2926
2926
|
// write would race that freeze.
|
|
2927
2927
|
const _registeredToolNames: string[] = [];
|
|
2928
2928
|
const _originalRegisterTool = api.registerTool.bind(api);
|
|
2929
|
+
// 3.3.8-rc.1 HYBRID MODE (OpenClaw 2026.5.2 issue #223 workaround):
|
|
2930
|
+
// The tool-policy-pipeline in OC 2026.5.2 strips non-bundled plugin tools
|
|
2931
|
+
// before they reach the agent's session toolset. registerTool() calls
|
|
2932
|
+
// succeed and tools are declared in contracts.tools, so the PLUGIN LOADS.
|
|
2933
|
+
// But tool calls never reach execute() — the pipeline discards them before
|
|
2934
|
+
// the agent's toolset is built.
|
|
2935
|
+
//
|
|
2936
|
+
// Strategy: keep all registerTool() calls intact so the plugin loader can
|
|
2937
|
+
// verify the contracts.tools declaration and load the plugin (hooks fire).
|
|
2938
|
+
// The `tr` CLI binary (dist/tr-cli.js) provides the alternative execution
|
|
2939
|
+
// path. Agent runs `tr remember|recall|status|pair` from shell; tool calls
|
|
2940
|
+
// are dead-letter but hooks (before_agent_start, agent_end, message_received,
|
|
2941
|
+
// before_reset) still fire via the unbroken hook code path.
|
|
2942
|
+
//
|
|
2943
|
+
// NOTE: do NOT no-op registerTool here — OC 2026.5.2 validates the
|
|
2944
|
+
// contracts.tools declaration against registered tools at load time and
|
|
2945
|
+
// drops the plugin (unloads it) if no tools match. Confirmed empirically:
|
|
2946
|
+
// no-op'ing registerTool causes the gateway to log "4 plugins" instead of
|
|
2947
|
+
// "5 plugins" after restart (plugin excluded from active set).
|
|
2948
|
+
//
|
|
2949
|
+
// TODO: when OC ships a fix for issue #223, restore tool-call routing
|
|
2950
|
+
// and remove the tr-cli.ts CLI layer. The bin/tr field in package.json
|
|
2951
|
+
// can stay as a convenience CLI regardless.
|
|
2929
2952
|
api.registerTool = (tool: unknown, opts?: { name?: string; names?: string[] }) => {
|
|
2930
2953
|
try {
|
|
2931
2954
|
const t = tool as { name?: unknown } | null | undefined;
|
|
@@ -6917,10 +6940,15 @@ const plugin = {
|
|
|
6917
6940
|
loadedAt: Date.now(),
|
|
6918
6941
|
tools: _registeredToolNames.slice(),
|
|
6919
6942
|
version: pluginVersion ?? 'unknown',
|
|
6943
|
+
// 3.3.8-rc.1 hybrid mode annotation: tools ARE registered with the
|
|
6944
|
+
// SDK (required for plugin loader validation), but tool calls are
|
|
6945
|
+
// dead-letter on OC 2026.5.2 due to issue #223. Use `tr <cmd>` CLI.
|
|
6946
|
+
hybridMode: true,
|
|
6947
|
+
hybridCliTools: ['tr status', 'tr pair', 'tr remember', 'tr recall'],
|
|
6920
6948
|
});
|
|
6921
6949
|
if (ok) {
|
|
6922
6950
|
api.logger.info(
|
|
6923
|
-
`TotalReclaw: wrote .loaded.json manifest (${_registeredToolNames.length} tools, version=${pluginVersion ?? 'unknown'})`,
|
|
6951
|
+
`TotalReclaw: wrote .loaded.json manifest (${_registeredToolNames.length} tools + hybridCli=tr, version=${pluginVersion ?? 'unknown'})`,
|
|
6924
6952
|
);
|
|
6925
6953
|
}
|
|
6926
6954
|
} catch {
|
package/openclaw.plugin.json
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@totalreclaw/totalreclaw",
|
|
3
|
-
"version": "3.3.
|
|
3
|
+
"version": "3.3.9-rc.1",
|
|
4
4
|
"description": "End-to-end encrypted, agent-portable memory for OpenClaw and any LLM-agent runtime. XChaCha20-Poly1305 with protobuf v4 + on-chain Memory Taxonomy v1 (claim / preference / directive / commitment / episode / summary).",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"keywords": [
|
|
@@ -48,6 +48,9 @@
|
|
|
48
48
|
},
|
|
49
49
|
"main": "./dist/index.js",
|
|
50
50
|
"types": "./dist/index.d.ts",
|
|
51
|
+
"bin": {
|
|
52
|
+
"tr": "./dist/tr-cli.js"
|
|
53
|
+
},
|
|
51
54
|
"files": [
|
|
52
55
|
"dist/",
|
|
53
56
|
"*.ts",
|
|
@@ -64,7 +67,7 @@
|
|
|
64
67
|
"scripts": {
|
|
65
68
|
"build": "rm -rf dist && tsc -p tsconfig.json --noCheck",
|
|
66
69
|
"verify-tarball": "node ../scripts/verify-tarball.mjs",
|
|
67
|
-
"test": "npx tsx manifest-shape.test.ts && npx tsx config-schema.test.ts && npx tsx config.test.ts && npx tsx relay-headers.test.ts && npx tsx scope-address-visible.test.ts && npx tsx llm-profile-reader.test.ts && npx tsx llm-client.test.ts && npx tsx llm-client-retry.test.ts && npx tsx gateway-url.test.ts && npx tsx retype-setscope.test.ts && npx tsx tool-gating.test.ts && npx tsx onboarding-noninteractive.test.ts && npx tsx pair-cli-json.test.ts && npx tsx pair-qr.test.ts && npx tsx pair-remote-client.test.ts && npx tsx qa-bug-report.test.ts && npx tsx nonce-serialization.test.ts && npx tsx phrase-safety-registry.test.ts && npx tsx test_issue_92_onnx_download_ux.test.ts && npx tsx onboard-pair-only.test.ts && npx tsx import-time-smoke.test.ts && npx tsx install-staging-cleanup.test.ts && npx tsx partial-install-detection.test.ts && npx tsx install-reload-idempotency.test.ts && npx tsx json-stdout-cleanliness.test.ts && npx tsx load-manifest.test.ts && npx tsx url-binding.test.ts && npx tsx fs-helpers.test.ts && npx tsx pair-cli-default-mode.test.ts && npx tsx embedding-fallback-tag.test.ts && npx tsx staging-banner-gate.test.ts && npx tsx restart-auth.test.ts && npx tsx inbound-user-tracker.test.ts && npx tsx register-command-name.test.ts",
|
|
70
|
+
"test": "npx tsx manifest-shape.test.ts && npx tsx config-schema.test.ts && npx tsx config.test.ts && npx tsx relay-headers.test.ts && npx tsx scope-address-visible.test.ts && npx tsx llm-profile-reader.test.ts && npx tsx llm-client.test.ts && npx tsx llm-client-retry.test.ts && npx tsx gateway-url.test.ts && npx tsx retype-setscope.test.ts && npx tsx tool-gating.test.ts && npx tsx onboarding-noninteractive.test.ts && npx tsx pair-cli-json.test.ts && npx tsx pair-qr.test.ts && npx tsx pair-remote-client.test.ts && npx tsx qa-bug-report.test.ts && npx tsx nonce-serialization.test.ts && npx tsx phrase-safety-registry.test.ts && npx tsx test_issue_92_onnx_download_ux.test.ts && npx tsx onboard-pair-only.test.ts && npx tsx import-time-smoke.test.ts && npx tsx install-staging-cleanup.test.ts && npx tsx partial-install-detection.test.ts && npx tsx install-reload-idempotency.test.ts && npx tsx json-stdout-cleanliness.test.ts && npx tsx load-manifest.test.ts && npx tsx url-binding.test.ts && npx tsx fs-helpers.test.ts && npx tsx pair-cli-default-mode.test.ts && npx tsx embedding-fallback-tag.test.ts && npx tsx staging-banner-gate.test.ts && npx tsx restart-auth.test.ts && npx tsx inbound-user-tracker.test.ts && npx tsx register-command-name.test.ts && npx tsx skill-md-hybrid-primary.test.ts && npx tsx tr-cli-json-output.test.ts",
|
|
68
71
|
"smoke:dist": "npx tsx dist-esm-smoke.test.ts",
|
|
69
72
|
"check-scanner": "node ../scripts/check-scanner.mjs",
|
|
70
73
|
"check-version-drift": "node ../scripts/check-version-drift.mjs",
|
package/skill.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "totalreclaw",
|
|
3
|
-
"version": "3.3.
|
|
3
|
+
"version": "3.3.9-rc.1",
|
|
4
4
|
"description": "End-to-end encrypted memory for AI agents — portable, yours forever. XChaCha20-Poly1305 E2EE: server never sees plaintext.",
|
|
5
5
|
"author": "TotalReclaw Team",
|
|
6
6
|
"license": "MIT",
|
package/tr-cli.ts
ADDED
|
@@ -0,0 +1,445 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* tr — TotalReclaw hybrid CLI (3.3.9-rc.1 primary architecture)
|
|
4
|
+
*
|
|
5
|
+
* OpenClaw 2026.5.2 has a tool-policy-pipeline bug (issue #223) that strips non-bundled plugin
|
|
6
|
+
* tools before they reach the agent toolset. In 3.3.9-rc.1, this CLI is the PRIMARY path for
|
|
7
|
+
* all agent memory operations (not a fallback). The agent runs `tr <cmd> --json` from shell;
|
|
8
|
+
* hooks (before_agent_start, agent_end, message_received, before_reset) continue via the
|
|
9
|
+
* unbroken hook code path.
|
|
10
|
+
*
|
|
11
|
+
* Phrase-safety: this CLI reads credentials.json (mnemonic at rest) but NEVER
|
|
12
|
+
* prints the mnemonic to stdout, stderr, or any log. Phrase only enters via QR-pair
|
|
13
|
+
* browser tier (pair-cli.ts / pair-cli-relay.ts — unchanged).
|
|
14
|
+
*
|
|
15
|
+
* Commands:
|
|
16
|
+
* tr status [--json] — print onboarding + credentials state
|
|
17
|
+
* tr pair [--json] — start a relay pairing session, print URL+PIN+QR
|
|
18
|
+
* tr remember [--json] <text> — store a memory in the encrypted vault
|
|
19
|
+
* tr recall [--json] [--limit N] <query> — search the encrypted vault
|
|
20
|
+
*
|
|
21
|
+
* --json flag: all agent-facing CLI calls MUST use --json for clean machine-parseable output.
|
|
22
|
+
* Plain text mode is for direct user CLI use only.
|
|
23
|
+
*
|
|
24
|
+
* Install: wired via package.json `bin.tr` → dist/tr-cli.js
|
|
25
|
+
* Usage from container: `docker exec tr-openclaw node ~/.openclaw/extensions/totalreclaw/dist/tr-cli.js status --json`
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
import path from 'node:path';
|
|
29
|
+
import os from 'node:os';
|
|
30
|
+
import { randomUUID } from 'node:crypto';
|
|
31
|
+
|
|
32
|
+
import { CONFIG } from './config.js';
|
|
33
|
+
import { loadCredentialsJson } from './fs-helpers.js';
|
|
34
|
+
import { printStatus } from './onboarding-cli.js';
|
|
35
|
+
import {
|
|
36
|
+
deriveKeys,
|
|
37
|
+
computeAuthKeyHash,
|
|
38
|
+
encrypt,
|
|
39
|
+
decrypt,
|
|
40
|
+
generateBlindIndices,
|
|
41
|
+
generateContentFingerprint,
|
|
42
|
+
} from './crypto.js';
|
|
43
|
+
import { createApiClient } from './api-client.js';
|
|
44
|
+
|
|
45
|
+
// ---------------------------------------------------------------------------
|
|
46
|
+
// Helpers
|
|
47
|
+
// ---------------------------------------------------------------------------
|
|
48
|
+
|
|
49
|
+
const CREDENTIALS_PATH = CONFIG.credentialsPath;
|
|
50
|
+
const SERVER_URL = CONFIG.serverUrl;
|
|
51
|
+
const STATE_PATH = CONFIG.onboardingStatePath;
|
|
52
|
+
const PLUGIN_VERSION = '3.3.9-rc.1';
|
|
53
|
+
|
|
54
|
+
function die(msg: string, code = 1): never {
|
|
55
|
+
process.stderr.write(`tr: ${msg}\n`);
|
|
56
|
+
process.exit(code);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function log(msg: string): void {
|
|
60
|
+
process.stdout.write(msg + '\n');
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** Parse --flag from args array, returning the cleaned args without the flag. */
|
|
64
|
+
function popFlag(args: string[], flag: string): [boolean, string[]] {
|
|
65
|
+
const idx = args.indexOf(flag);
|
|
66
|
+
if (idx === -1) return [false, args];
|
|
67
|
+
return [true, [...args.slice(0, idx), ...args.slice(idx + 1)]];
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** Parse --limit N from args, returning [limit, cleanedArgs]. Default: defaultLimit. */
|
|
71
|
+
function popLimitFlag(args: string[], defaultLimit: number): [number, string[]] {
|
|
72
|
+
const idx = args.indexOf('--limit');
|
|
73
|
+
if (idx === -1 || idx + 1 >= args.length) return [defaultLimit, args];
|
|
74
|
+
const n = parseInt(args[idx + 1], 10);
|
|
75
|
+
const limit = isNaN(n) || n < 1 ? defaultLimit : n;
|
|
76
|
+
return [limit, [...args.slice(0, idx), ...args.slice(idx + 2)]];
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// ---------------------------------------------------------------------------
|
|
80
|
+
// Core init — minimal version of index.ts initialize()
|
|
81
|
+
// ---------------------------------------------------------------------------
|
|
82
|
+
|
|
83
|
+
interface CliContext {
|
|
84
|
+
authKeyHex: string;
|
|
85
|
+
encryptionKey: Buffer;
|
|
86
|
+
dedupKey: Buffer;
|
|
87
|
+
apiClient: ReturnType<typeof createApiClient>;
|
|
88
|
+
userId: string;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
async function buildContext(): Promise<CliContext> {
|
|
92
|
+
const creds = loadCredentialsJson(CREDENTIALS_PATH);
|
|
93
|
+
if (!creds) {
|
|
94
|
+
die('TotalReclaw is not set up. Run: node ~/.openclaw/extensions/totalreclaw/dist/tr-cli.js pair --json');
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const mnemonic =
|
|
98
|
+
(typeof creds.mnemonic === 'string' && creds.mnemonic.trim()) ||
|
|
99
|
+
(typeof creds.recovery_phrase === 'string' && creds.recovery_phrase.trim()) ||
|
|
100
|
+
'';
|
|
101
|
+
|
|
102
|
+
if (!mnemonic) {
|
|
103
|
+
die('No recovery phrase in credentials.json. Run: tr pair --json');
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Parse existing salt/userId from credentials.json
|
|
107
|
+
let existingSalt: Buffer | undefined;
|
|
108
|
+
let existingUserId: string | undefined;
|
|
109
|
+
|
|
110
|
+
const saltStr = typeof creds.salt === 'string' ? creds.salt : undefined;
|
|
111
|
+
if (saltStr) {
|
|
112
|
+
if (/^[0-9a-f]{64}$/i.test(saltStr)) {
|
|
113
|
+
existingSalt = Buffer.from(saltStr, 'hex');
|
|
114
|
+
} else {
|
|
115
|
+
existingSalt = Buffer.from(saltStr, 'base64');
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
existingUserId = typeof creds.userId === 'string' ? creds.userId : undefined;
|
|
119
|
+
|
|
120
|
+
const keys = deriveKeys(mnemonic, existingSalt);
|
|
121
|
+
const authKeyHex = keys.authKey.toString('hex');
|
|
122
|
+
|
|
123
|
+
const apiClient = createApiClient(SERVER_URL);
|
|
124
|
+
|
|
125
|
+
let userId: string;
|
|
126
|
+
if (existingUserId) {
|
|
127
|
+
userId = existingUserId;
|
|
128
|
+
} else {
|
|
129
|
+
// Register to get userId (idempotent on relay)
|
|
130
|
+
const authHash = computeAuthKeyHash(keys.authKey);
|
|
131
|
+
const saltHex = keys.salt.toString('hex');
|
|
132
|
+
try {
|
|
133
|
+
const result = await apiClient.register(authHash, saltHex);
|
|
134
|
+
userId = result.user_id;
|
|
135
|
+
} catch (err) {
|
|
136
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
137
|
+
if (msg.includes('USER_EXISTS')) {
|
|
138
|
+
userId = authHash.slice(0, 32);
|
|
139
|
+
} else {
|
|
140
|
+
die(`Relay registration failed: ${msg}`);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
return {
|
|
146
|
+
authKeyHex,
|
|
147
|
+
encryptionKey: keys.encryptionKey,
|
|
148
|
+
dedupKey: keys.dedupKey,
|
|
149
|
+
apiClient,
|
|
150
|
+
userId,
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// ---------------------------------------------------------------------------
|
|
155
|
+
// Command: status
|
|
156
|
+
// ---------------------------------------------------------------------------
|
|
157
|
+
|
|
158
|
+
async function cmdStatus(jsonMode: boolean): Promise<void> {
|
|
159
|
+
// Probe plugin manifest for version/hybridMode/toolCount.
|
|
160
|
+
let pluginVersion: string | undefined;
|
|
161
|
+
let bootCount: number | undefined;
|
|
162
|
+
let hybridMode = true; // default true in 3.3.9-rc.1 (hybrid-primary)
|
|
163
|
+
let toolCount: number | undefined;
|
|
164
|
+
let loadedAgeSec: number | undefined;
|
|
165
|
+
|
|
166
|
+
try {
|
|
167
|
+
const fs = await import('node:fs');
|
|
168
|
+
const candidatePaths = [
|
|
169
|
+
path.join(os.homedir(), '.openclaw', 'extensions', 'totalreclaw', '.loaded.json'),
|
|
170
|
+
path.join(os.homedir(), '.openclaw', 'npm', 'node_modules', '@totalreclaw', 'totalreclaw', 'dist', '.loaded.json'),
|
|
171
|
+
];
|
|
172
|
+
const resolvedPath = candidatePaths.find((p) => fs.existsSync(p));
|
|
173
|
+
if (resolvedPath) {
|
|
174
|
+
const raw = fs.readFileSync(resolvedPath, 'utf-8');
|
|
175
|
+
const manifest = JSON.parse(raw) as {
|
|
176
|
+
version?: string;
|
|
177
|
+
bootCount?: number;
|
|
178
|
+
loadedAt?: number;
|
|
179
|
+
hybridMode?: boolean;
|
|
180
|
+
tools?: string[];
|
|
181
|
+
};
|
|
182
|
+
pluginVersion = manifest.version ?? PLUGIN_VERSION;
|
|
183
|
+
bootCount = manifest.bootCount;
|
|
184
|
+
hybridMode = manifest.hybridMode !== false; // default true
|
|
185
|
+
toolCount = manifest.tools?.length;
|
|
186
|
+
const ageMs = Date.now() - (manifest.loadedAt ?? 0);
|
|
187
|
+
loadedAgeSec = Math.round(ageMs / 1000);
|
|
188
|
+
}
|
|
189
|
+
} catch {
|
|
190
|
+
// Best-effort
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// Check onboarding state
|
|
194
|
+
const creds = loadCredentialsJson(CREDENTIALS_PATH);
|
|
195
|
+
const onboarded = !!creds;
|
|
196
|
+
|
|
197
|
+
if (jsonMode) {
|
|
198
|
+
// JSON-first output for agent parsing
|
|
199
|
+
const out: Record<string, unknown> = {
|
|
200
|
+
version: pluginVersion ?? PLUGIN_VERSION,
|
|
201
|
+
onboarded,
|
|
202
|
+
next_step: onboarded ? 'none' : 'pair',
|
|
203
|
+
tool_count: toolCount ?? 17,
|
|
204
|
+
hybrid_mode: hybridMode,
|
|
205
|
+
};
|
|
206
|
+
if (bootCount !== undefined) out.boot_count = bootCount;
|
|
207
|
+
if (loadedAgeSec !== undefined) out.loaded_age_sec = loadedAgeSec;
|
|
208
|
+
log(JSON.stringify(out));
|
|
209
|
+
} else {
|
|
210
|
+
// Human-readable plain text for direct user CLI use
|
|
211
|
+
printStatus(CREDENTIALS_PATH, STATE_PATH, process.stdout);
|
|
212
|
+
process.stdout.write(
|
|
213
|
+
`\n plugin: ${pluginVersion ? `loaded (version=${pluginVersion}` + (bootCount !== undefined ? ` bootCount=${bootCount}` : '') + (loadedAgeSec !== undefined ? ` loaded=${loadedAgeSec}s ago` : '') + ')' : 'not found in .loaded.json'}\n` +
|
|
214
|
+
` hybrid-mode: ${hybridMode ? 'yes (primary — use tr <cmd> --json)' : 'no'}\n` +
|
|
215
|
+
` hooks: before_agent_start, agent_end, message_received, before_reset\n`,
|
|
216
|
+
);
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// ---------------------------------------------------------------------------
|
|
221
|
+
// Command: pair
|
|
222
|
+
// ---------------------------------------------------------------------------
|
|
223
|
+
|
|
224
|
+
async function cmdPair(args: string[]): Promise<void> {
|
|
225
|
+
// Delegate to the existing pair-cli-relay.ts via a thin wrapper.
|
|
226
|
+
// The pair flow is relay-brokered (works through Docker NAT).
|
|
227
|
+
// Phrase-safety: pair-cli-relay.ts is x25519-only; mnemonic never appears.
|
|
228
|
+
const outputMode = args.includes('--json') ? 'json' : args.includes('--url-pin') ? 'url-pin' : 'human';
|
|
229
|
+
|
|
230
|
+
const { runRelayPairCli } = await import('./pair-cli-relay.js');
|
|
231
|
+
const { defaultRenderQr, buildDefaultPairCliIo } = await import('./pair-cli.js');
|
|
232
|
+
|
|
233
|
+
const io = buildDefaultPairCliIo();
|
|
234
|
+
const outcome = await runRelayPairCli('generate', {
|
|
235
|
+
relayBaseUrl: CONFIG.pairRelayUrl,
|
|
236
|
+
credentialsPath: CREDENTIALS_PATH,
|
|
237
|
+
onboardingStatePath: STATE_PATH,
|
|
238
|
+
logger: {
|
|
239
|
+
info: (m: string) => process.stderr.write(`[info] ${m}\n`),
|
|
240
|
+
warn: (m: string) => process.stderr.write(`[warn] ${m}\n`),
|
|
241
|
+
error: (m: string) => process.stderr.write(`[error] ${m}\n`),
|
|
242
|
+
},
|
|
243
|
+
pluginVersion: PLUGIN_VERSION,
|
|
244
|
+
deriveScopeAddress: undefined,
|
|
245
|
+
renderQr: defaultRenderQr,
|
|
246
|
+
io,
|
|
247
|
+
outputMode: outputMode as import('./pair-cli.js').PairCliOutputMode,
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
if (outcome.status !== 'completed' && outcome.status !== 'canceled') {
|
|
251
|
+
die(`Pairing ${outcome.status}`, 1);
|
|
252
|
+
}
|
|
253
|
+
if (outcome.status === 'canceled') {
|
|
254
|
+
process.exit(130);
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// ---------------------------------------------------------------------------
|
|
259
|
+
// Command: remember
|
|
260
|
+
// ---------------------------------------------------------------------------
|
|
261
|
+
|
|
262
|
+
async function cmdRemember(rawArgs: string[]): Promise<void> {
|
|
263
|
+
const [jsonMode, args] = popFlag(rawArgs, '--json');
|
|
264
|
+
const text = args.join(' ').trim();
|
|
265
|
+
if (!text) {
|
|
266
|
+
die('Usage: tr remember [--json] <text>');
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
const ctx = await buildContext();
|
|
270
|
+
|
|
271
|
+
// Build a minimal MemoryTaxonomy v1 claim blob (same format as storeExtractedFacts)
|
|
272
|
+
const now = new Date().toISOString();
|
|
273
|
+
const factId = randomUUID().replace(/-/g, '');
|
|
274
|
+
|
|
275
|
+
// Encrypt the memory text
|
|
276
|
+
const blob = JSON.stringify({
|
|
277
|
+
text,
|
|
278
|
+
type: 'claim',
|
|
279
|
+
source: 'user',
|
|
280
|
+
scope: 'unspecified',
|
|
281
|
+
importance: 8,
|
|
282
|
+
metadata: {
|
|
283
|
+
type: 'claim',
|
|
284
|
+
source: 'user',
|
|
285
|
+
scope: 'unspecified',
|
|
286
|
+
importance: 8,
|
|
287
|
+
},
|
|
288
|
+
timestamp: now,
|
|
289
|
+
version: 'v1',
|
|
290
|
+
});
|
|
291
|
+
const encrypted_blob = encrypt(blob, ctx.encryptionKey);
|
|
292
|
+
const blind_indices = generateBlindIndices(text);
|
|
293
|
+
const content_fp = generateContentFingerprint(text, ctx.dedupKey);
|
|
294
|
+
|
|
295
|
+
const payload = {
|
|
296
|
+
id: factId,
|
|
297
|
+
timestamp: now,
|
|
298
|
+
encrypted_blob,
|
|
299
|
+
blind_indices,
|
|
300
|
+
decay_score: 8,
|
|
301
|
+
source: 'cli:tr-remember',
|
|
302
|
+
content_fp,
|
|
303
|
+
};
|
|
304
|
+
|
|
305
|
+
try {
|
|
306
|
+
await ctx.apiClient.store(ctx.userId, [payload], ctx.authKeyHex);
|
|
307
|
+
if (jsonMode) {
|
|
308
|
+
// JSON-first output for agent parsing
|
|
309
|
+
// claim_count requires an extra relay call to tally stored claims; not worth the latency — use 0
|
|
310
|
+
log(JSON.stringify({ ok: true, id: factId, claim_count: 0 }));
|
|
311
|
+
} else {
|
|
312
|
+
log(`ok — stored memory (id=${factId})`);
|
|
313
|
+
}
|
|
314
|
+
} catch (err) {
|
|
315
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
316
|
+
die(`remember failed: ${msg}`);
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
// ---------------------------------------------------------------------------
|
|
321
|
+
// Command: recall
|
|
322
|
+
// ---------------------------------------------------------------------------
|
|
323
|
+
|
|
324
|
+
async function cmdRecall(rawArgs: string[]): Promise<void> {
|
|
325
|
+
const [jsonMode, argsAfterJson] = popFlag(rawArgs, '--json');
|
|
326
|
+
const [limit, argsAfterLimit] = popLimitFlag(argsAfterJson, 5);
|
|
327
|
+
const query = argsAfterLimit.join(' ').trim();
|
|
328
|
+
if (!query) {
|
|
329
|
+
die('Usage: tr recall [--json] [--limit N] <query>');
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
const ctx = await buildContext();
|
|
333
|
+
|
|
334
|
+
// Generate word trapdoors for blind search
|
|
335
|
+
const trapdoors = generateBlindIndices(query);
|
|
336
|
+
|
|
337
|
+
if (trapdoors.length === 0) {
|
|
338
|
+
if (jsonMode) {
|
|
339
|
+
log(JSON.stringify({ results: [] }));
|
|
340
|
+
} else {
|
|
341
|
+
log('No results (0 searchable terms in query).');
|
|
342
|
+
}
|
|
343
|
+
return;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
try {
|
|
347
|
+
const candidates = await ctx.apiClient.search(ctx.userId, trapdoors, Math.min(limit * 2, 20), ctx.authKeyHex);
|
|
348
|
+
|
|
349
|
+
const results: Array<{ text: string; score: number }> = [];
|
|
350
|
+
|
|
351
|
+
for (const c of candidates) {
|
|
352
|
+
try {
|
|
353
|
+
const raw = decrypt(c.encrypted_blob, ctx.encryptionKey);
|
|
354
|
+
const parsed = JSON.parse(raw) as { text?: string };
|
|
355
|
+
if (parsed.text) {
|
|
356
|
+
results.push({
|
|
357
|
+
text: parsed.text,
|
|
358
|
+
score: c.decay_score,
|
|
359
|
+
});
|
|
360
|
+
}
|
|
361
|
+
} catch {
|
|
362
|
+
// Skip undecryptable
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
// Sort by score descending, then trim to limit
|
|
367
|
+
results.sort((a, b) => b.score - a.score);
|
|
368
|
+
const trimmed = results.slice(0, limit);
|
|
369
|
+
|
|
370
|
+
if (jsonMode) {
|
|
371
|
+
// JSON-first output for agent parsing — canonical format per spec
|
|
372
|
+
log(JSON.stringify({ results: trimmed }));
|
|
373
|
+
} else {
|
|
374
|
+
log(`Found ${trimmed.length} result(s) for: ${query}`);
|
|
375
|
+
for (const r of trimmed) {
|
|
376
|
+
log(` [score=${r.score.toFixed(2)}] ${r.text}`);
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
} catch (err) {
|
|
380
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
381
|
+
die(`recall failed: ${msg}`);
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
// ---------------------------------------------------------------------------
|
|
386
|
+
// Dispatch
|
|
387
|
+
// ---------------------------------------------------------------------------
|
|
388
|
+
|
|
389
|
+
async function main(): Promise<void> {
|
|
390
|
+
const args = process.argv.slice(2);
|
|
391
|
+
const cmd = args[0];
|
|
392
|
+
|
|
393
|
+
switch (cmd) {
|
|
394
|
+
case 'status': {
|
|
395
|
+
const [jsonMode] = popFlag(args.slice(1), '--json');
|
|
396
|
+
await cmdStatus(jsonMode);
|
|
397
|
+
break;
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
case 'pair':
|
|
401
|
+
await cmdPair(args.slice(1));
|
|
402
|
+
break;
|
|
403
|
+
|
|
404
|
+
case 'remember':
|
|
405
|
+
await cmdRemember(args.slice(1));
|
|
406
|
+
break;
|
|
407
|
+
|
|
408
|
+
case 'recall':
|
|
409
|
+
await cmdRecall(args.slice(1));
|
|
410
|
+
break;
|
|
411
|
+
|
|
412
|
+
case undefined:
|
|
413
|
+
case '--help':
|
|
414
|
+
case '-h':
|
|
415
|
+
process.stdout.write(
|
|
416
|
+
`TotalReclaw hybrid CLI v${PLUGIN_VERSION} (primary mode — OpenClaw 2026.5.2+)\n\n` +
|
|
417
|
+
'Usage:\n' +
|
|
418
|
+
' tr status [--json] — onboarding + plugin load state\n' +
|
|
419
|
+
' tr pair [--json] — start a relay pairing session\n' +
|
|
420
|
+
' tr remember [--json] <text> — store a memory\n' +
|
|
421
|
+
' tr recall [--json] [--limit N] <query> — search memories (default limit: 5)\n\n' +
|
|
422
|
+
'Flags:\n' +
|
|
423
|
+
' --json Output machine-parseable JSON (required for agent shell calls)\n' +
|
|
424
|
+
' --limit N Limit recall results (default: 5)\n\n' +
|
|
425
|
+
'JSON output shapes:\n' +
|
|
426
|
+
' status: {"version":"...","onboarded":bool,"next_step":"pair|none","tool_count":N,"hybrid_mode":bool}\n' +
|
|
427
|
+
' pair: {"url":"...","pin":"123456","expires_at":"..."}\n' +
|
|
428
|
+
' remember: {"ok":true,"id":"...","claim_count":N}\n' +
|
|
429
|
+
' recall: {"results":[{"text":"...","score":0.8}]}\n\n' +
|
|
430
|
+
'Environment:\n' +
|
|
431
|
+
' TOTALRECLAW_SERVER_URL — relay URL (default: api-staging.totalreclaw.xyz)\n' +
|
|
432
|
+
' TOTALRECLAW_CREDENTIALS_PATH — override credentials.json path\n',
|
|
433
|
+
);
|
|
434
|
+
break;
|
|
435
|
+
|
|
436
|
+
default:
|
|
437
|
+
die(`Unknown command: ${cmd}. Run \`tr --help\` for usage.`);
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
main().catch((err) => {
|
|
442
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
443
|
+
process.stderr.write(`tr: fatal: ${msg}\n`);
|
|
444
|
+
process.exit(2);
|
|
445
|
+
});
|