@totalreclaw/totalreclaw 3.3.8-rc.1 → 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 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 totalreclaw_remember / totalreclaw_recall. Trigger on 'install TotalReclaw', 'set up TotalReclaw', 'restore my recovery phrase', or any remember / recall request."
4
- version: 3.3.8-rc.1
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–6 short lines TOTAL** (lines 1, 2, 5, 6 always; lines 3 and 4 only if the corresponding condition fires). 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.
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 + auto-reload + `totalreclaw_pair` is bound)
40
- > ✓ Installed (totalreclaw &lt;version&gt;)
41
- 3. (optional only if auto-reload didn't fire and you need to issue `/totalreclaw-restart`)
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 &lt;version&gt;, hybrid mode)
65
+ 3. (account-setup URL + PIN, IMMEDIATELY after `tr pair --json` returns no consent gate)
46
66
  > Open &lt;url&gt; in your browser. Enter PIN &lt;pin&gt;. Generate or paste a 12-word recovery phrase. Reply `done` once it's sealed.
47
- 6. (done + free-tier confirmation)
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. Plugin-first lets the plugin land cleanly, then the skill's reload picks it up on the next gateway loop. If you accidentally do skill-first and the plugin install dies mid-flight, just retry `openclaw plugins install @totalreclaw/totalreclaw` once — it's idempotent.
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. **Wait for auto-reloadthen issue `/totalreclaw-restart` autonomously if needed (silent — emit user-visible line 2 on success, line 3 only if you had to restart).** OpenClaw's default config (`gateway.reload.mode = "hybrid"`) auto-restarts the gateway via a file-watcher within 1-3 seconds. Wait ~5s, then run `openclaw plugins list | grep totalreclaw` and confirm `totalreclaw_pair` is in your toolset. When bound, emit user-visible line 2 and proceed.
67
-
68
- **If `totalreclaw_pair` is NOT visible after 10s — restart imperative (read carefully):**
69
-
70
- > Your IMMEDIATE next message must be the literal slash command `/totalreclaw-restart` nothing else. Do not narrate. Do not ask permission. Do not describe what `/totalreclaw-restart` does. Do not propose alternatives. Just send `/totalreclaw-restart`. You may emit user-visible line 3 (`Restarting OpenClaw to load the plugin…`) ONCE in the same turn, then issue `/totalreclaw-restart`. Nothing else.
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
- `/totalreclaw-restart` is in-process it drains active runs cleanly; your shell is NOT shelling out to `docker restart` or `openclaw gateway restart` (those would suicide the host). After `/totalreclaw-restart`, wait ~3s and re-run `openclaw plugins list`. (3.3.7-rc.2 renamed this from `/restart` because OpenClaw reserves `/restart` for its built-in the plugin-side fallback can only attach to a unique name. The 5-tier auth resolver is unchanged; only the command name moved.)
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
- **If `/totalreclaw-restart` returns "You are not authorized to use this command":** surface this verbatim and stop. Do NOT loop `/totalreclaw-restart` calls. Do NOT propose alternatives.
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
- > Your Telegram channel needs `allowFrom` configured. Run on your host: `jq '.channels.<channel>.allowFrom = ["<your-id>"]' ~/.openclaw/openclaw.json | sponge ~/.openclaw/openclaw.json && docker restart <your-container-name>`. Substitute `<channel>` (e.g. `telegram`, `slack`), `<your-id>` (Telegram: `@username` or numeric `chat_id`; Slack: workspace member ID), and `<your-container-name>` (`docker ps` shows it). For non-Docker installs substitute `openclaw gateway restart` for the `docker restart` portion. On a managed service, ask the service's support to enable slash commands. Reply `done` after.
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
- Then wait for `done`. Do NOT retry the install. Do NOT keep restarting. Do NOT issue `/new` it wipes the chat context, the agent forgets it was mid-install, and the user's next message is treated as a fresh install request which retries from scratch and re-trips the scanner block (Pedro QA, 3.3.5-rc.1).
100
+ ## `tr` CLI reference (hybrid-primary commands)
79
101
 
80
- **Forbidden vocabulary** (these are anti-patterns they show up to the user even if your transport edits them later, and they violate the silence rule):
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
- Substitute all of these with silence — the next user-visible line in the set, or nothing.
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
- 3. **Verify `totalreclaw_pair` is bound.** If still missing after `/totalreclaw-restart` (or after the user fixed allowFrom and said `done`), tell them the restart didn't take effect and ask them to retry. Do not loop the install.
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
- 4. **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.
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
- 5. **Set up the user's TotalReclaw account (emit user-visible line 5 once, after `totalreclaw_pair` returns).** Call `totalreclaw_pair` silently (the account-setup tool — name kept for backward compatibility). It returns `{url, pin, qr_ascii, qr_png_b64, qr_unicode, expires_at_ms}` (see "Rendering the QR" below). Then emit user-visible line 5 with `<url>` and `<pin>` substituted. Do not pre-narrate ("I'll walk you through…", "Setting up your account now…") — line 5 itself is the only thing the user needs to see. The URL + PIN itself is the real consent moment: surfacing them verbatim is what gives the user the chance to opt out by simply not opening the page.
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
- 6. **Verify and confirm (silent until line 6).** After the user says `done`, re-run `test -s ~/.totalreclaw/credentials.json`. If missing, the PIN expired call `totalreclaw_pair` again silently and resend line 5 once. If present, emit user-visible line 6 and stop.
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,44 +133,52 @@ 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; (2) if the tool is missing from your tool list a known gap for some container-based agents, issue #87 — 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}`, issue #95 — same account-setup flow, `pair_url` key naming). 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, call `totalreclaw_pair` with `mode=generate` for a fresh wallet.
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" → `totalreclaw_remember` (don't ask permission).
117
- - First-person factual query ("do I / what's my / where do I …") → `totalreclaw_recall` FIRST, then answer from returned facts. If 0 results, say so.
118
- - "forget / pin / unpin / retype / move to <scope>" X → `totalreclaw_recall` first for `factId`, then `totalreclaw_forget` / `_pin` / `_unpin` / `_retype` / `_set_scope`.
119
- - "status" → `totalreclaw_status`. "upgrade" → `totalreclaw_upgrade` (paste Stripe URL verbatim). Post-upgrade testnet → mainnet: `totalreclaw_migrate` with `dry_run` first.
120
- - "import / export / consolidate" `totalreclaw_import_from` (`dry_run=true` first; `_import_batch` for >50 chunks) · `totalreclaw_export` · `totalreclaw_consolidate` (`dry_run` first).
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
- ## OpenClaw 2026.5.2 hybrid mode (issue #223 workaround — plugin 3.3.8-rc.1+)
123
-
124
- OpenClaw 2026.5.2 has a tool-policy-pipeline bug (issue #223) that strips non-bundled plugin tools before they reach the agent toolset. Plugin 3.3.8-rc.1 ships a hybrid workaround:
125
-
126
- - **Hooks still fire**: `before_agent_start`, `agent_end`, `message_received`, `before_reset` use a separate code path not affected by issue #223. Auto-extraction, digest injection, preloading, and billing checks all work normally.
127
- - **Tools replaced by `tr` CLI**: instead of `totalreclaw_remember`, `totalreclaw_recall`, etc., use the `tr` binary installed with the plugin. The agent runs it via shell tool.
128
-
129
- **Agent usage under hybrid mode** (check `tr status` output for `hybrid-mode: yes`):
130
-
131
- ```
132
- # remember a fact
133
- node ~/.openclaw/extensions/totalreclaw/dist/tr-cli.js remember "I work at Graph Foundation"
134
- # → outputs {"ok":true,"id":"...","text":"..."}
135
-
136
- # recall facts
137
- node ~/.openclaw/extensions/totalreclaw/dist/tr-cli.js recall "where does the user work"
138
- # → outputs {"ok":true,"count":1,"query":"...","memories":[...]}
165
+ ## Diagnostics
139
166
 
140
- # status check
141
- node ~/.openclaw/extensions/totalreclaw/dist/tr-cli.js status
142
- ```
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.
171
+ - `onboarding required` → credentials missing; redo from the account-setup step.
172
+ - `quota exceeded` → `tr status --json` or `totalreclaw_status`, then offer `totalreclaw_upgrade`.
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`.
143
174
 
144
- Note: the `tr` bin name conflicts with the system `tr` utility; always invoke via full path `node ~/.openclaw/extensions/totalreclaw/dist/tr-cli.js <cmd>` or the gateway host's equivalent path.
175
+ ## Required post-install config (OpenClaw 2026.5.2+)
145
176
 
146
- **Required post-install config** (add to `~/.openclaw/openclaw.json`):
177
+ Add to `~/.openclaw/openclaw.json` after installing:
147
178
  ```json
148
179
  {
149
180
  "plugins": {
181
+ "allow": ["totalreclaw"],
150
182
  "entries": {
151
183
  "totalreclaw": {
152
184
  "hooks": { "allowConversationAccess": true }
@@ -156,16 +188,9 @@ Note: the `tr` bin name conflicts with the system `tr` utility; always invoke vi
156
188
  }
157
189
  ```
158
190
 
159
- This unlocks typed hooks (`agent_end`, etc.) for non-bundled plugins. Without it, auto-extraction still works (extraction runs in `message_received`) but the `agent_end` hook that writes the digest is silently blocked.
160
-
161
- ## Diagnostics
162
-
163
- - `totalreclaw_*` tools not visible → plugin not installed or auto-reload didn't fire. Verify via `openclaw plugins list | grep totalreclaw` (or, on a managed service that doesn't expose the CLI, ask the user to confirm via their service's plugins UI). If `totalreclaw` is listed but tools are still absent, the gateway probably hasn't completed its auto-restart yet (give it 5-10s) OR `gateway.reload.mode = off` is set — instruct manual restart as fallback: `openclaw gateway restart` (native), `docker restart <your-container-name>` (Docker self-host), or the managed service's restart control. If the plugin is listed and the gateway has been restarted but tools are still missing (container-agent gap, issue #87 / OC 2026.5.2 issue #223), switch to hybrid mode: use `node ~/.openclaw/extensions/totalreclaw/dist/tr-cli.js <cmd>` for remember/recall/pair/status. If your shell doesn't reach the gateway host (managed service), tell the user to install/enable the plugin via their service's plugins UI and restart the gateway via the service's restart control. Do NOT suggest the bare `openclaw totalreclaw onboard` (leaks phrase on stdout — deprecated in rc.18, removed next RC).
164
- - User says done but `credentials.json` missing → PIN expired or entered wrong phrase; call `totalreclaw_pair` again.
165
- - `onboarding required` → credentials missing; redo from the account-setup step.
166
- - `quota exceeded` → `totalreclaw_status`, then offer `totalreclaw_upgrade`.
167
- - `No LLM available for auto-extraction` at startup → provider key unreachable; check `~/.openclaw/agents/<agent>/agent/auth-profiles.json` or plugin config `extraction.llm`.
191
+ `plugins.allow` suppresses the "plugins.allow is empty" warning. `hooks.allowConversationAccess` unlocks typed hooks (`agent_end`, etc.) for auto-extraction.
168
192
 
169
193
  ## Tool surface
170
194
 
171
- `totalreclaw_pair` (ONLY account-setup path) · `_remember` · `_recall` · `_forget` · `_pin` · `_unpin` · `_retype` · `_set_scope` · `_export` · `_status` · `_upgrade` · `_migrate` · `_import_from` · `_import_batch` · `_consolidate` · `_onboarding_start` (pointer to local-terminal wizard, for users explicitly rejecting the browser flow) · `_report_qa_bug` (RC only).
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/tr-cli.js CHANGED
@@ -1,24 +1,28 @@
1
1
  #!/usr/bin/env node
2
2
  /**
3
- * tr — TotalReclaw hybrid CLI (3.3.8-rc.1 workaround for OpenClaw 2026.5.2 issue #223)
3
+ * tr — TotalReclaw hybrid CLI (3.3.9-rc.1 primary architecture)
4
4
  *
5
- * OpenClaw 2026.5.2 has a tool-policy-pipeline bug that strips non-bundled plugin tools
6
- * before they reach the agent toolset. This CLI bypasses the broken tool-registration
7
- * path entirely. The agent runs `tr <cmd>` from shell; the plugin keeps its hooks
8
- * (before_agent_start, agent_end, message_received) via the unbroken hook code path.
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.
9
10
  *
10
11
  * Phrase-safety: this CLI reads credentials.json (mnemonic at rest) but NEVER
11
12
  * prints the mnemonic to stdout, stderr, or any log. Phrase only enters via QR-pair
12
13
  * browser tier (pair-cli.ts / pair-cli-relay.ts — unchanged).
13
14
  *
14
15
  * Commands:
15
- * tr status — print onboarding + credentials state
16
- * tr pair [--json] — start a relay pairing session, print URL+PIN+QR
17
- * tr remember <text> — store a memory in the encrypted vault
18
- * tr recall <query> — search the encrypted vault, print results as JSON
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.
19
23
  *
20
24
  * Install: wired via package.json `bin.tr` → dist/tr-cli.js
21
- * Usage from container: `docker exec tr-openclaw tr status`
25
+ * Usage from container: `docker exec tr-openclaw node ~/.openclaw/extensions/totalreclaw/dist/tr-cli.js status --json`
22
26
  */
23
27
  import path from 'node:path';
24
28
  import os from 'node:os';
@@ -34,6 +38,7 @@ import { createApiClient } from './api-client.js';
34
38
  const CREDENTIALS_PATH = CONFIG.credentialsPath;
35
39
  const SERVER_URL = CONFIG.serverUrl;
36
40
  const STATE_PATH = CONFIG.onboardingStatePath;
41
+ const PLUGIN_VERSION = '3.3.9-rc.1';
37
42
  function die(msg, code = 1) {
38
43
  process.stderr.write(`tr: ${msg}\n`);
39
44
  process.exit(code);
@@ -41,16 +46,32 @@ function die(msg, code = 1) {
41
46
  function log(msg) {
42
47
  process.stdout.write(msg + '\n');
43
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
+ }
44
65
  async function buildContext() {
45
66
  const creds = loadCredentialsJson(CREDENTIALS_PATH);
46
67
  if (!creds) {
47
- die('TotalReclaw is not set up. Run: openclaw totalreclaw onboard\n(or: tr pair)');
68
+ die('TotalReclaw is not set up. Run: node ~/.openclaw/extensions/totalreclaw/dist/tr-cli.js pair --json');
48
69
  }
49
70
  const mnemonic = (typeof creds.mnemonic === 'string' && creds.mnemonic.trim()) ||
50
71
  (typeof creds.recovery_phrase === 'string' && creds.recovery_phrase.trim()) ||
51
72
  '';
52
73
  if (!mnemonic) {
53
- die('No recovery phrase in credentials.json. Run: openclaw totalreclaw onboard');
74
+ die('No recovery phrase in credentials.json. Run: tr pair --json');
54
75
  }
55
76
  // Parse existing salt/userId from credentials.json
56
77
  let existingSalt;
@@ -101,40 +122,59 @@ async function buildContext() {
101
122
  // ---------------------------------------------------------------------------
102
123
  // Command: status
103
124
  // ---------------------------------------------------------------------------
104
- async function cmdStatus() {
105
- // Print onboarding + credentials state (never prints mnemonic — same as
106
- // the `openclaw totalreclaw status` subcommand surface).
107
- printStatus(CREDENTIALS_PATH, STATE_PATH, process.stdout);
108
- // Additional: loaded.json check to confirm plugin hooks are active.
109
- // Reads manifest written by register() in index.ts.
110
- // Probe both install paths: extensions/ (local tgz installs) and npm/ (registry installs).
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;
111
132
  try {
112
133
  const fs = await import('node:fs');
113
134
  const candidatePaths = [
114
- // extensions-path (local tgz / --force install) — .loaded.json sits at root, not dist/
115
135
  path.join(os.homedir(), '.openclaw', 'extensions', 'totalreclaw', '.loaded.json'),
116
- // npm-path (registry install) — .loaded.json inside dist/
117
136
  path.join(os.homedir(), '.openclaw', 'npm', 'node_modules', '@totalreclaw', 'totalreclaw', 'dist', '.loaded.json'),
118
137
  ];
119
138
  const resolvedPath = candidatePaths.find((p) => fs.existsSync(p));
120
139
  if (resolvedPath) {
121
140
  const raw = fs.readFileSync(resolvedPath, 'utf-8');
122
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;
123
146
  const ageMs = Date.now() - (manifest.loadedAt ?? 0);
124
- const ageSec = Math.round(ageMs / 1000);
125
- process.stdout.write(`\n plugin: loaded (version=${manifest.version ?? '?'} bootCount=${manifest.bootCount ?? '?'} loaded=${ageSec}s ago)\n` +
126
- ` hybrid-mode: ${manifest.hybridMode ? 'yes (use tr <cmd>)' : 'no'}\n` +
127
- ` hooks: before_agent_start, agent_end, message_received, before_reset\n` +
128
- ` note: tools from .loaded.json are STRIPPED by OC 2026.5.2 issue #223;\n` +
129
- ` use \`tr <cmd>\` from shell instead\n`);
130
- }
131
- else {
132
- process.stdout.write('\n plugin: .loaded.json not found — plugin may not be loaded\n');
147
+ loadedAgeSec = Math.round(ageMs / 1000);
133
148
  }
134
149
  }
135
150
  catch {
136
151
  // Best-effort
137
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
+ }
138
178
  }
139
179
  // ---------------------------------------------------------------------------
140
180
  // Command: pair
@@ -156,7 +196,7 @@ async function cmdPair(args) {
156
196
  warn: (m) => process.stderr.write(`[warn] ${m}\n`),
157
197
  error: (m) => process.stderr.write(`[error] ${m}\n`),
158
198
  },
159
- pluginVersion: '3.3.8-rc.1',
199
+ pluginVersion: PLUGIN_VERSION,
160
200
  deriveScopeAddress: undefined,
161
201
  renderQr: defaultRenderQr,
162
202
  io,
@@ -172,10 +212,11 @@ async function cmdPair(args) {
172
212
  // ---------------------------------------------------------------------------
173
213
  // Command: remember
174
214
  // ---------------------------------------------------------------------------
175
- async function cmdRemember(args) {
215
+ async function cmdRemember(rawArgs) {
216
+ const [jsonMode, args] = popFlag(rawArgs, '--json');
176
217
  const text = args.join(' ').trim();
177
218
  if (!text) {
178
- die('Usage: tr remember <text>');
219
+ die('Usage: tr remember [--json] <text>');
179
220
  }
180
221
  const ctx = await buildContext();
181
222
  // Build a minimal MemoryTaxonomy v1 claim blob (same format as storeExtractedFacts)
@@ -211,7 +252,14 @@ async function cmdRemember(args) {
211
252
  };
212
253
  try {
213
254
  await ctx.apiClient.store(ctx.userId, [payload], ctx.authKeyHex);
214
- log(JSON.stringify({ ok: true, id: factId, text }));
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
+ }
215
263
  }
216
264
  catch (err) {
217
265
  const msg = err instanceof Error ? err.message : String(err);
@@ -221,31 +269,36 @@ async function cmdRemember(args) {
221
269
  // ---------------------------------------------------------------------------
222
270
  // Command: recall
223
271
  // ---------------------------------------------------------------------------
224
- async function cmdRecall(args) {
225
- const query = args.join(' ').trim();
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();
226
276
  if (!query) {
227
- die('Usage: tr recall <query>');
277
+ die('Usage: tr recall [--json] [--limit N] <query>');
228
278
  }
229
279
  const ctx = await buildContext();
230
280
  // Generate word trapdoors for blind search
231
281
  const trapdoors = generateBlindIndices(query);
232
282
  if (trapdoors.length === 0) {
233
- log(JSON.stringify({ ok: true, count: 0, memories: [] }));
283
+ if (jsonMode) {
284
+ log(JSON.stringify({ results: [] }));
285
+ }
286
+ else {
287
+ log('No results (0 searchable terms in query).');
288
+ }
234
289
  return;
235
290
  }
236
291
  try {
237
- const candidates = await ctx.apiClient.search(ctx.userId, trapdoors, 12, ctx.authKeyHex);
238
- const memories = [];
292
+ const candidates = await ctx.apiClient.search(ctx.userId, trapdoors, Math.min(limit * 2, 20), ctx.authKeyHex);
293
+ const results = [];
239
294
  for (const c of candidates) {
240
295
  try {
241
296
  const raw = decrypt(c.encrypted_blob, ctx.encryptionKey);
242
297
  const parsed = JSON.parse(raw);
243
298
  if (parsed.text) {
244
- memories.push({
245
- id: c.fact_id,
299
+ results.push({
246
300
  text: parsed.text,
247
301
  score: c.decay_score,
248
- timestamp: new Date(c.timestamp).toISOString(),
249
302
  });
250
303
  }
251
304
  }
@@ -253,9 +306,19 @@ async function cmdRecall(args) {
253
306
  // Skip undecryptable
254
307
  }
255
308
  }
256
- // Simple relevance sort by decay_score (descending)
257
- memories.sort((a, b) => b.score - a.score);
258
- log(JSON.stringify({ ok: true, count: memories.length, query, memories }));
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
+ }
259
322
  }
260
323
  catch (err) {
261
324
  const msg = err instanceof Error ? err.message : String(err);
@@ -269,9 +332,11 @@ async function main() {
269
332
  const args = process.argv.slice(2);
270
333
  const cmd = args[0];
271
334
  switch (cmd) {
272
- case 'status':
273
- await cmdStatus();
335
+ case 'status': {
336
+ const [jsonMode] = popFlag(args.slice(1), '--json');
337
+ await cmdStatus(jsonMode);
274
338
  break;
339
+ }
275
340
  case 'pair':
276
341
  await cmdPair(args.slice(1));
277
342
  break;
@@ -284,15 +349,23 @@ async function main() {
284
349
  case undefined:
285
350
  case '--help':
286
351
  case '-h':
287
- process.stdout.write('TotalReclaw hybrid CLI (OpenClaw 2026.5.2 issue #223 workaround)\n\n' +
352
+ process.stdout.write(`TotalReclaw hybrid CLI v${PLUGIN_VERSION} (primary mode — OpenClaw 2026.5.2+)\n\n` +
288
353
  'Usage:\n' +
289
- ' tr status — onboarding + plugin load state\n' +
290
- ' tr pair [--json] — start a relay pairing session\n' +
291
- ' tr remember <text> — store a memory\n' +
292
- ' tr recall <query> — search memories (outputs JSON)\n\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' +
293
366
  'Environment:\n' +
294
- ' TOTALRECLAW_SERVER_URL — relay URL (default: api-staging.totalreclaw.xyz)\n' +
295
- ' TOTALRECLAW_CREDENTIALS_PATH — override credentials.json path\n');
367
+ ' TOTALRECLAW_SERVER_URL — relay URL (default: api-staging.totalreclaw.xyz)\n' +
368
+ ' TOTALRECLAW_CREDENTIALS_PATH — override credentials.json path\n');
296
369
  break;
297
370
  default:
298
371
  die(`Unknown command: ${cmd}. Run \`tr --help\` for usage.`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@totalreclaw/totalreclaw",
3
- "version": "3.3.8-rc.1",
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": [
@@ -67,7 +67,7 @@
67
67
  "scripts": {
68
68
  "build": "rm -rf dist && tsc -p tsconfig.json --noCheck",
69
69
  "verify-tarball": "node ../scripts/verify-tarball.mjs",
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",
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",
71
71
  "smoke:dist": "npx tsx dist-esm-smoke.test.ts",
72
72
  "check-scanner": "node ../scripts/check-scanner.mjs",
73
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.8-rc.1",
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 CHANGED
@@ -1,24 +1,28 @@
1
1
  #!/usr/bin/env node
2
2
  /**
3
- * tr — TotalReclaw hybrid CLI (3.3.8-rc.1 workaround for OpenClaw 2026.5.2 issue #223)
3
+ * tr — TotalReclaw hybrid CLI (3.3.9-rc.1 primary architecture)
4
4
  *
5
- * OpenClaw 2026.5.2 has a tool-policy-pipeline bug that strips non-bundled plugin tools
6
- * before they reach the agent toolset. This CLI bypasses the broken tool-registration
7
- * path entirely. The agent runs `tr <cmd>` from shell; the plugin keeps its hooks
8
- * (before_agent_start, agent_end, message_received) via the unbroken hook code path.
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.
9
10
  *
10
11
  * Phrase-safety: this CLI reads credentials.json (mnemonic at rest) but NEVER
11
12
  * prints the mnemonic to stdout, stderr, or any log. Phrase only enters via QR-pair
12
13
  * browser tier (pair-cli.ts / pair-cli-relay.ts — unchanged).
13
14
  *
14
15
  * Commands:
15
- * tr status — print onboarding + credentials state
16
- * tr pair [--json] — start a relay pairing session, print URL+PIN+QR
17
- * tr remember <text> — store a memory in the encrypted vault
18
- * tr recall <query> — search the encrypted vault, print results as JSON
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.
19
23
  *
20
24
  * Install: wired via package.json `bin.tr` → dist/tr-cli.js
21
- * Usage from container: `docker exec tr-openclaw tr status`
25
+ * Usage from container: `docker exec tr-openclaw node ~/.openclaw/extensions/totalreclaw/dist/tr-cli.js status --json`
22
26
  */
23
27
 
24
28
  import path from 'node:path';
@@ -45,6 +49,7 @@ import { createApiClient } from './api-client.js';
45
49
  const CREDENTIALS_PATH = CONFIG.credentialsPath;
46
50
  const SERVER_URL = CONFIG.serverUrl;
47
51
  const STATE_PATH = CONFIG.onboardingStatePath;
52
+ const PLUGIN_VERSION = '3.3.9-rc.1';
48
53
 
49
54
  function die(msg: string, code = 1): never {
50
55
  process.stderr.write(`tr: ${msg}\n`);
@@ -55,6 +60,22 @@ function log(msg: string): void {
55
60
  process.stdout.write(msg + '\n');
56
61
  }
57
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
+
58
79
  // ---------------------------------------------------------------------------
59
80
  // Core init — minimal version of index.ts initialize()
60
81
  // ---------------------------------------------------------------------------
@@ -70,7 +91,7 @@ interface CliContext {
70
91
  async function buildContext(): Promise<CliContext> {
71
92
  const creds = loadCredentialsJson(CREDENTIALS_PATH);
72
93
  if (!creds) {
73
- die('TotalReclaw is not set up. Run: openclaw totalreclaw onboard\n(or: tr pair)');
94
+ die('TotalReclaw is not set up. Run: node ~/.openclaw/extensions/totalreclaw/dist/tr-cli.js pair --json');
74
95
  }
75
96
 
76
97
  const mnemonic =
@@ -79,7 +100,7 @@ async function buildContext(): Promise<CliContext> {
79
100
  '';
80
101
 
81
102
  if (!mnemonic) {
82
- die('No recovery phrase in credentials.json. Run: openclaw totalreclaw onboard');
103
+ die('No recovery phrase in credentials.json. Run: tr pair --json');
83
104
  }
84
105
 
85
106
  // Parse existing salt/userId from credentials.json
@@ -134,20 +155,18 @@ async function buildContext(): Promise<CliContext> {
134
155
  // Command: status
135
156
  // ---------------------------------------------------------------------------
136
157
 
137
- async function cmdStatus(): Promise<void> {
138
- // Print onboarding + credentials state (never prints mnemonic — same as
139
- // the `openclaw totalreclaw status` subcommand surface).
140
- printStatus(CREDENTIALS_PATH, STATE_PATH, process.stdout);
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;
141
165
 
142
- // Additional: loaded.json check to confirm plugin hooks are active.
143
- // Reads manifest written by register() in index.ts.
144
- // Probe both install paths: extensions/ (local tgz installs) and npm/ (registry installs).
145
166
  try {
146
167
  const fs = await import('node:fs');
147
168
  const candidatePaths = [
148
- // extensions-path (local tgz / --force install) — .loaded.json sits at root, not dist/
149
169
  path.join(os.homedir(), '.openclaw', 'extensions', 'totalreclaw', '.loaded.json'),
150
- // npm-path (registry install) — .loaded.json inside dist/
151
170
  path.join(os.homedir(), '.openclaw', 'npm', 'node_modules', '@totalreclaw', 'totalreclaw', 'dist', '.loaded.json'),
152
171
  ];
153
172
  const resolvedPath = candidatePaths.find((p) => fs.existsSync(p));
@@ -160,21 +179,42 @@ async function cmdStatus(): Promise<void> {
160
179
  hybridMode?: boolean;
161
180
  tools?: string[];
162
181
  };
182
+ pluginVersion = manifest.version ?? PLUGIN_VERSION;
183
+ bootCount = manifest.bootCount;
184
+ hybridMode = manifest.hybridMode !== false; // default true
185
+ toolCount = manifest.tools?.length;
163
186
  const ageMs = Date.now() - (manifest.loadedAt ?? 0);
164
- const ageSec = Math.round(ageMs / 1000);
165
- process.stdout.write(
166
- `\n plugin: loaded (version=${manifest.version ?? '?'} bootCount=${manifest.bootCount ?? '?'} loaded=${ageSec}s ago)\n` +
167
- ` hybrid-mode: ${manifest.hybridMode ? 'yes (use tr <cmd>)' : 'no'}\n` +
168
- ` hooks: before_agent_start, agent_end, message_received, before_reset\n` +
169
- ` note: tools from .loaded.json are STRIPPED by OC 2026.5.2 issue #223;\n` +
170
- ` use \`tr <cmd>\` from shell instead\n`,
171
- );
172
- } else {
173
- process.stdout.write('\n plugin: .loaded.json not found — plugin may not be loaded\n');
187
+ loadedAgeSec = Math.round(ageMs / 1000);
174
188
  }
175
189
  } catch {
176
190
  // Best-effort
177
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
+ }
178
218
  }
179
219
 
180
220
  // ---------------------------------------------------------------------------
@@ -200,7 +240,7 @@ async function cmdPair(args: string[]): Promise<void> {
200
240
  warn: (m: string) => process.stderr.write(`[warn] ${m}\n`),
201
241
  error: (m: string) => process.stderr.write(`[error] ${m}\n`),
202
242
  },
203
- pluginVersion: '3.3.8-rc.1',
243
+ pluginVersion: PLUGIN_VERSION,
204
244
  deriveScopeAddress: undefined,
205
245
  renderQr: defaultRenderQr,
206
246
  io,
@@ -219,10 +259,11 @@ async function cmdPair(args: string[]): Promise<void> {
219
259
  // Command: remember
220
260
  // ---------------------------------------------------------------------------
221
261
 
222
- async function cmdRemember(args: string[]): Promise<void> {
262
+ async function cmdRemember(rawArgs: string[]): Promise<void> {
263
+ const [jsonMode, args] = popFlag(rawArgs, '--json');
223
264
  const text = args.join(' ').trim();
224
265
  if (!text) {
225
- die('Usage: tr remember <text>');
266
+ die('Usage: tr remember [--json] <text>');
226
267
  }
227
268
 
228
269
  const ctx = await buildContext();
@@ -263,7 +304,13 @@ async function cmdRemember(args: string[]): Promise<void> {
263
304
 
264
305
  try {
265
306
  await ctx.apiClient.store(ctx.userId, [payload], ctx.authKeyHex);
266
- log(JSON.stringify({ ok: true, id: factId, text }));
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
+ }
267
314
  } catch (err) {
268
315
  const msg = err instanceof Error ? err.message : String(err);
269
316
  die(`remember failed: ${msg}`);
@@ -274,10 +321,12 @@ async function cmdRemember(args: string[]): Promise<void> {
274
321
  // Command: recall
275
322
  // ---------------------------------------------------------------------------
276
323
 
277
- async function cmdRecall(args: string[]): Promise<void> {
278
- const query = args.join(' ').trim();
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();
279
328
  if (!query) {
280
- die('Usage: tr recall <query>');
329
+ die('Usage: tr recall [--json] [--limit N] <query>');
281
330
  }
282
331
 
283
332
  const ctx = await buildContext();
@@ -286,25 +335,27 @@ async function cmdRecall(args: string[]): Promise<void> {
286
335
  const trapdoors = generateBlindIndices(query);
287
336
 
288
337
  if (trapdoors.length === 0) {
289
- log(JSON.stringify({ ok: true, count: 0, memories: [] }));
338
+ if (jsonMode) {
339
+ log(JSON.stringify({ results: [] }));
340
+ } else {
341
+ log('No results (0 searchable terms in query).');
342
+ }
290
343
  return;
291
344
  }
292
345
 
293
346
  try {
294
- const candidates = await ctx.apiClient.search(ctx.userId, trapdoors, 12, ctx.authKeyHex);
347
+ const candidates = await ctx.apiClient.search(ctx.userId, trapdoors, Math.min(limit * 2, 20), ctx.authKeyHex);
295
348
 
296
- const memories: Array<{ id: string; text: string; score: number; timestamp: string }> = [];
349
+ const results: Array<{ text: string; score: number }> = [];
297
350
 
298
351
  for (const c of candidates) {
299
352
  try {
300
353
  const raw = decrypt(c.encrypted_blob, ctx.encryptionKey);
301
354
  const parsed = JSON.parse(raw) as { text?: string };
302
355
  if (parsed.text) {
303
- memories.push({
304
- id: c.fact_id,
356
+ results.push({
305
357
  text: parsed.text,
306
358
  score: c.decay_score,
307
- timestamp: new Date(c.timestamp).toISOString(),
308
359
  });
309
360
  }
310
361
  } catch {
@@ -312,10 +363,19 @@ async function cmdRecall(args: string[]): Promise<void> {
312
363
  }
313
364
  }
314
365
 
315
- // Simple relevance sort by decay_score (descending)
316
- memories.sort((a, b) => b.score - a.score);
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);
317
369
 
318
- log(JSON.stringify({ ok: true, count: memories.length, query, memories }));
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
+ }
319
379
  } catch (err) {
320
380
  const msg = err instanceof Error ? err.message : String(err);
321
381
  die(`recall failed: ${msg}`);
@@ -331,9 +391,11 @@ async function main(): Promise<void> {
331
391
  const cmd = args[0];
332
392
 
333
393
  switch (cmd) {
334
- case 'status':
335
- await cmdStatus();
394
+ case 'status': {
395
+ const [jsonMode] = popFlag(args.slice(1), '--json');
396
+ await cmdStatus(jsonMode);
336
397
  break;
398
+ }
337
399
 
338
400
  case 'pair':
339
401
  await cmdPair(args.slice(1));
@@ -351,15 +413,23 @@ async function main(): Promise<void> {
351
413
  case '--help':
352
414
  case '-h':
353
415
  process.stdout.write(
354
- 'TotalReclaw hybrid CLI (OpenClaw 2026.5.2 issue #223 workaround)\n\n' +
416
+ `TotalReclaw hybrid CLI v${PLUGIN_VERSION} (primary mode — OpenClaw 2026.5.2+)\n\n` +
355
417
  'Usage:\n' +
356
- ' tr status — onboarding + plugin load state\n' +
357
- ' tr pair [--json] — start a relay pairing session\n' +
358
- ' tr remember <text> — store a memory\n' +
359
- ' tr recall <query> — search memories (outputs JSON)\n\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' +
360
430
  'Environment:\n' +
361
- ' TOTALRECLAW_SERVER_URL — relay URL (default: api-staging.totalreclaw.xyz)\n' +
362
- ' TOTALRECLAW_CREDENTIALS_PATH — override credentials.json path\n',
431
+ ' TOTALRECLAW_SERVER_URL — relay URL (default: api-staging.totalreclaw.xyz)\n' +
432
+ ' TOTALRECLAW_CREDENTIALS_PATH — override credentials.json path\n',
363
433
  );
364
434
  break;
365
435