@totalreclaw/totalreclaw 3.3.7-rc.2 → 3.3.8-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 +15 -0
- package/SKILL.md +41 -2
- package/dist/index.js +29 -1
- package/dist/tr-cli.js +305 -0
- package/fs-helpers.ts +12 -0
- package/index.ts +29 -1
- package/openclaw.plugin.json +22 -0
- package/package.json +4 -1
- package/skill.json +1 -1
- package/tr-cli.ts +375 -0
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,21 @@ 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.7-rc.3] — 2026-05-04
|
|
8
|
+
|
|
9
|
+
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.
|
|
10
|
+
|
|
11
|
+
The `plugins.allow` warning ("plugins.allow is empty; discovered non-bundled plugins may auto-load: totalreclaw") is a separate UX issue logged by the discoveryLoader when `plugins.allow` is absent — it is informational and does NOT prevent loading; the actual block is `contracts.tools`. Setting `plugins.allow=["totalreclaw"]` via `openclaw config set` is still recommended to suppress the noisy warning for users who follow the install guide, but it is not the root fix. See NOTE below.
|
|
12
|
+
|
|
13
|
+
### Fixed — add `contracts.tools` to plugin manifest (issue #87 / FR #72097 tool-binding race)
|
|
14
|
+
|
|
15
|
+
**Root cause:** OpenClaw 2026.5.2 (image `openclaw-qa:2026.5.2`) enforced a new security gate: non-bundled plugins must declare `contracts.tools` — an explicit allowlist of tool names — in their `openclaw.plugin.json` before registering any agent tool. Plugins that omit the field have every `registerTool` call silently dropped. The gate did not exist in ≤2026.4.24 (the version the plugin was built against), so the gap only surfaced when the QA container was upgraded.
|
|
16
|
+
|
|
17
|
+
**Fix:** added `contracts.tools` to `skill/plugin/openclaw.plugin.json` listing all 17 tool names the plugin registers:
|
|
18
|
+
`totalreclaw_remember`, `totalreclaw_recall`, `totalreclaw_forget`, `totalreclaw_export`, `totalreclaw_status`, `totalreclaw_preload_embedder`, `totalreclaw_consolidate`, `totalreclaw_pin`, `totalreclaw_unpin`, `totalreclaw_retype`, `totalreclaw_set_scope`, `totalreclaw_import_from`, `totalreclaw_import_batch`, `totalreclaw_upgrade`, `totalreclaw_migrate`, `totalreclaw_pair`, `totalreclaw_report_qa_bug`.
|
|
19
|
+
|
|
20
|
+
**NOTE — `plugins.allow` side-effect:** OpenClaw 2026.5.2 also emits `plugins.allow is empty; discovered non-bundled plugins may auto-load: totalreclaw` as an INFO log whenever a session starts and `plugins.allow` is unset. The plugin install step (`openclaw plugins install @totalreclaw/totalreclaw@<ver>`) does NOT automatically add the plugin to `plugins.allow`. As a consequence, users who follow the public install guide hit this noisy warning every session. The `contracts.tools` fix resolves tool binding; the `plugins.allow` warning requires either (a) an upstream OpenClaw change to auto-populate `plugins.allow` on `plugins install`, or (b) adding `openclaw config set plugins.allow '["totalreclaw"]'` to the install guide as a one-time post-install step. Tracked as FR upstream; the setup guide has been updated to include the config set step.
|
|
21
|
+
|
|
7
22
|
## [3.3.7-rc.2] — 2026-05-04
|
|
8
23
|
|
|
9
24
|
Follow-up to 3.3.7-rc.1, caught in Pedro's manual integration testing on the OpenClaw side 2026-05-03. The 5-tier auth fix shipped DEAD-CODE in rc.1: gateway logs surfaced `[gateway] [plugins] command registration failed: Command name "restart" is reserved by a built-in command (plugin=totalreclaw, source=/home/pdiogo/.openclaw/extensions/totalreclaw/dist/index.js)`. The plugin's `/restart` registration was rejected because OpenClaw's plugin registry hard-rejects `restart` (and the rest of `RESERVED_COMMANDS` — see `node_modules/openclaw/dist/registry-*.js` — `help`, `commands`, `status`, `whoami`, `context`, `stop`, `restart`, `reset`, `new`, `compact`, `config`, `debug`, `allowlist`, `activation`, `skill`, `subagents`, `kill`, `steer`, `tell`, `model`, `models`, `queue`, `send`, `bash`, `exec`, `think`, `verbose`, `reasoning`, `elevated`, `usage`). Built-in `/restart` retains its allow-from-only semantics, which is what gave Pedro the original "You are not authorized" — and our 5-tier fallback never ran because plugin registration never succeeded.
|
package/SKILL.md
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: totalreclaw
|
|
3
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.
|
|
4
|
+
version: 3.3.8-rc.1
|
|
5
5
|
author: TotalReclaw Team
|
|
6
6
|
license: MIT
|
|
7
7
|
homepage: https://totalreclaw.xyz
|
|
@@ -119,9 +119,48 @@ NEVER echo / generate / ask the user to paste a recovery phrase in chat. NEVER i
|
|
|
119
119
|
- "status" → `totalreclaw_status`. "upgrade" → `totalreclaw_upgrade` (paste Stripe URL verbatim). Post-upgrade testnet → mainnet: `totalreclaw_migrate` with `dry_run` first.
|
|
120
120
|
- "import / export / consolidate" → `totalreclaw_import_from` (`dry_run=true` first; `_import_batch` for >50 chunks) · `totalreclaw_export` · `totalreclaw_consolidate` (`dry_run` first).
|
|
121
121
|
|
|
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":[...]}
|
|
139
|
+
|
|
140
|
+
# status check
|
|
141
|
+
node ~/.openclaw/extensions/totalreclaw/dist/tr-cli.js status
|
|
142
|
+
```
|
|
143
|
+
|
|
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.
|
|
145
|
+
|
|
146
|
+
**Required post-install config** (add to `~/.openclaw/openclaw.json`):
|
|
147
|
+
```json
|
|
148
|
+
{
|
|
149
|
+
"plugins": {
|
|
150
|
+
"entries": {
|
|
151
|
+
"totalreclaw": {
|
|
152
|
+
"hooks": { "allowConversationAccess": true }
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
```
|
|
158
|
+
|
|
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
|
+
|
|
122
161
|
## Diagnostics
|
|
123
162
|
|
|
124
|
-
- `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
|
|
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).
|
|
125
164
|
- User says done but `credentials.json` missing → PIN expired or entered wrong phrase; call `totalreclaw_pair` again.
|
|
126
165
|
- `onboarding required` → credentials missing; redo from the account-setup step.
|
|
127
166
|
- `quota exceeded` → `totalreclaw_status`, then offer `totalreclaw_upgrade`.
|
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,305 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* tr — TotalReclaw hybrid CLI (3.3.8-rc.1 workaround for OpenClaw 2026.5.2 issue #223)
|
|
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.
|
|
9
|
+
*
|
|
10
|
+
* Phrase-safety: this CLI reads credentials.json (mnemonic at rest) but NEVER
|
|
11
|
+
* prints the mnemonic to stdout, stderr, or any log. Phrase only enters via QR-pair
|
|
12
|
+
* browser tier (pair-cli.ts / pair-cli-relay.ts — unchanged).
|
|
13
|
+
*
|
|
14
|
+
* 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
|
|
19
|
+
*
|
|
20
|
+
* Install: wired via package.json `bin.tr` → dist/tr-cli.js
|
|
21
|
+
* Usage from container: `docker exec tr-openclaw tr status`
|
|
22
|
+
*/
|
|
23
|
+
import path from 'node:path';
|
|
24
|
+
import os from 'node:os';
|
|
25
|
+
import { randomUUID } from 'node:crypto';
|
|
26
|
+
import { CONFIG } from './config.js';
|
|
27
|
+
import { loadCredentialsJson } from './fs-helpers.js';
|
|
28
|
+
import { printStatus } from './onboarding-cli.js';
|
|
29
|
+
import { deriveKeys, computeAuthKeyHash, encrypt, decrypt, generateBlindIndices, generateContentFingerprint, } from './crypto.js';
|
|
30
|
+
import { createApiClient } from './api-client.js';
|
|
31
|
+
// ---------------------------------------------------------------------------
|
|
32
|
+
// Helpers
|
|
33
|
+
// ---------------------------------------------------------------------------
|
|
34
|
+
const CREDENTIALS_PATH = CONFIG.credentialsPath;
|
|
35
|
+
const SERVER_URL = CONFIG.serverUrl;
|
|
36
|
+
const STATE_PATH = CONFIG.onboardingStatePath;
|
|
37
|
+
function die(msg, code = 1) {
|
|
38
|
+
process.stderr.write(`tr: ${msg}\n`);
|
|
39
|
+
process.exit(code);
|
|
40
|
+
}
|
|
41
|
+
function log(msg) {
|
|
42
|
+
process.stdout.write(msg + '\n');
|
|
43
|
+
}
|
|
44
|
+
async function buildContext() {
|
|
45
|
+
const creds = loadCredentialsJson(CREDENTIALS_PATH);
|
|
46
|
+
if (!creds) {
|
|
47
|
+
die('TotalReclaw is not set up. Run: openclaw totalreclaw onboard\n(or: tr pair)');
|
|
48
|
+
}
|
|
49
|
+
const mnemonic = (typeof creds.mnemonic === 'string' && creds.mnemonic.trim()) ||
|
|
50
|
+
(typeof creds.recovery_phrase === 'string' && creds.recovery_phrase.trim()) ||
|
|
51
|
+
'';
|
|
52
|
+
if (!mnemonic) {
|
|
53
|
+
die('No recovery phrase in credentials.json. Run: openclaw totalreclaw onboard');
|
|
54
|
+
}
|
|
55
|
+
// Parse existing salt/userId from credentials.json
|
|
56
|
+
let existingSalt;
|
|
57
|
+
let existingUserId;
|
|
58
|
+
const saltStr = typeof creds.salt === 'string' ? creds.salt : undefined;
|
|
59
|
+
if (saltStr) {
|
|
60
|
+
if (/^[0-9a-f]{64}$/i.test(saltStr)) {
|
|
61
|
+
existingSalt = Buffer.from(saltStr, 'hex');
|
|
62
|
+
}
|
|
63
|
+
else {
|
|
64
|
+
existingSalt = Buffer.from(saltStr, 'base64');
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
existingUserId = typeof creds.userId === 'string' ? creds.userId : undefined;
|
|
68
|
+
const keys = deriveKeys(mnemonic, existingSalt);
|
|
69
|
+
const authKeyHex = keys.authKey.toString('hex');
|
|
70
|
+
const apiClient = createApiClient(SERVER_URL);
|
|
71
|
+
let userId;
|
|
72
|
+
if (existingUserId) {
|
|
73
|
+
userId = existingUserId;
|
|
74
|
+
}
|
|
75
|
+
else {
|
|
76
|
+
// Register to get userId (idempotent on relay)
|
|
77
|
+
const authHash = computeAuthKeyHash(keys.authKey);
|
|
78
|
+
const saltHex = keys.salt.toString('hex');
|
|
79
|
+
try {
|
|
80
|
+
const result = await apiClient.register(authHash, saltHex);
|
|
81
|
+
userId = result.user_id;
|
|
82
|
+
}
|
|
83
|
+
catch (err) {
|
|
84
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
85
|
+
if (msg.includes('USER_EXISTS')) {
|
|
86
|
+
userId = authHash.slice(0, 32);
|
|
87
|
+
}
|
|
88
|
+
else {
|
|
89
|
+
die(`Relay registration failed: ${msg}`);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
return {
|
|
94
|
+
authKeyHex,
|
|
95
|
+
encryptionKey: keys.encryptionKey,
|
|
96
|
+
dedupKey: keys.dedupKey,
|
|
97
|
+
apiClient,
|
|
98
|
+
userId,
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
// ---------------------------------------------------------------------------
|
|
102
|
+
// Command: status
|
|
103
|
+
// ---------------------------------------------------------------------------
|
|
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).
|
|
111
|
+
try {
|
|
112
|
+
const fs = await import('node:fs');
|
|
113
|
+
const candidatePaths = [
|
|
114
|
+
// extensions-path (local tgz / --force install) — .loaded.json sits at root, not dist/
|
|
115
|
+
path.join(os.homedir(), '.openclaw', 'extensions', 'totalreclaw', '.loaded.json'),
|
|
116
|
+
// npm-path (registry install) — .loaded.json inside dist/
|
|
117
|
+
path.join(os.homedir(), '.openclaw', 'npm', 'node_modules', '@totalreclaw', 'totalreclaw', 'dist', '.loaded.json'),
|
|
118
|
+
];
|
|
119
|
+
const resolvedPath = candidatePaths.find((p) => fs.existsSync(p));
|
|
120
|
+
if (resolvedPath) {
|
|
121
|
+
const raw = fs.readFileSync(resolvedPath, 'utf-8');
|
|
122
|
+
const manifest = JSON.parse(raw);
|
|
123
|
+
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');
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
catch {
|
|
136
|
+
// Best-effort
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
// ---------------------------------------------------------------------------
|
|
140
|
+
// Command: pair
|
|
141
|
+
// ---------------------------------------------------------------------------
|
|
142
|
+
async function cmdPair(args) {
|
|
143
|
+
// Delegate to the existing pair-cli-relay.ts via a thin wrapper.
|
|
144
|
+
// The pair flow is relay-brokered (works through Docker NAT).
|
|
145
|
+
// Phrase-safety: pair-cli-relay.ts is x25519-only; mnemonic never appears.
|
|
146
|
+
const outputMode = args.includes('--json') ? 'json' : args.includes('--url-pin') ? 'url-pin' : 'human';
|
|
147
|
+
const { runRelayPairCli } = await import('./pair-cli-relay.js');
|
|
148
|
+
const { defaultRenderQr, buildDefaultPairCliIo } = await import('./pair-cli.js');
|
|
149
|
+
const io = buildDefaultPairCliIo();
|
|
150
|
+
const outcome = await runRelayPairCli('generate', {
|
|
151
|
+
relayBaseUrl: CONFIG.pairRelayUrl,
|
|
152
|
+
credentialsPath: CREDENTIALS_PATH,
|
|
153
|
+
onboardingStatePath: STATE_PATH,
|
|
154
|
+
logger: {
|
|
155
|
+
info: (m) => process.stderr.write(`[info] ${m}\n`),
|
|
156
|
+
warn: (m) => process.stderr.write(`[warn] ${m}\n`),
|
|
157
|
+
error: (m) => process.stderr.write(`[error] ${m}\n`),
|
|
158
|
+
},
|
|
159
|
+
pluginVersion: '3.3.8-rc.1',
|
|
160
|
+
deriveScopeAddress: undefined,
|
|
161
|
+
renderQr: defaultRenderQr,
|
|
162
|
+
io,
|
|
163
|
+
outputMode: outputMode,
|
|
164
|
+
});
|
|
165
|
+
if (outcome.status !== 'completed' && outcome.status !== 'canceled') {
|
|
166
|
+
die(`Pairing ${outcome.status}`, 1);
|
|
167
|
+
}
|
|
168
|
+
if (outcome.status === 'canceled') {
|
|
169
|
+
process.exit(130);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
// ---------------------------------------------------------------------------
|
|
173
|
+
// Command: remember
|
|
174
|
+
// ---------------------------------------------------------------------------
|
|
175
|
+
async function cmdRemember(args) {
|
|
176
|
+
const text = args.join(' ').trim();
|
|
177
|
+
if (!text) {
|
|
178
|
+
die('Usage: tr remember <text>');
|
|
179
|
+
}
|
|
180
|
+
const ctx = await buildContext();
|
|
181
|
+
// Build a minimal MemoryTaxonomy v1 claim blob (same format as storeExtractedFacts)
|
|
182
|
+
const now = new Date().toISOString();
|
|
183
|
+
const factId = randomUUID().replace(/-/g, '');
|
|
184
|
+
// Encrypt the memory text
|
|
185
|
+
const blob = JSON.stringify({
|
|
186
|
+
text,
|
|
187
|
+
type: 'claim',
|
|
188
|
+
source: 'user',
|
|
189
|
+
scope: 'unspecified',
|
|
190
|
+
importance: 8,
|
|
191
|
+
metadata: {
|
|
192
|
+
type: 'claim',
|
|
193
|
+
source: 'user',
|
|
194
|
+
scope: 'unspecified',
|
|
195
|
+
importance: 8,
|
|
196
|
+
},
|
|
197
|
+
timestamp: now,
|
|
198
|
+
version: 'v1',
|
|
199
|
+
});
|
|
200
|
+
const encrypted_blob = encrypt(blob, ctx.encryptionKey);
|
|
201
|
+
const blind_indices = generateBlindIndices(text);
|
|
202
|
+
const content_fp = generateContentFingerprint(text, ctx.dedupKey);
|
|
203
|
+
const payload = {
|
|
204
|
+
id: factId,
|
|
205
|
+
timestamp: now,
|
|
206
|
+
encrypted_blob,
|
|
207
|
+
blind_indices,
|
|
208
|
+
decay_score: 8,
|
|
209
|
+
source: 'cli:tr-remember',
|
|
210
|
+
content_fp,
|
|
211
|
+
};
|
|
212
|
+
try {
|
|
213
|
+
await ctx.apiClient.store(ctx.userId, [payload], ctx.authKeyHex);
|
|
214
|
+
log(JSON.stringify({ ok: true, id: factId, text }));
|
|
215
|
+
}
|
|
216
|
+
catch (err) {
|
|
217
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
218
|
+
die(`remember failed: ${msg}`);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
// ---------------------------------------------------------------------------
|
|
222
|
+
// Command: recall
|
|
223
|
+
// ---------------------------------------------------------------------------
|
|
224
|
+
async function cmdRecall(args) {
|
|
225
|
+
const query = args.join(' ').trim();
|
|
226
|
+
if (!query) {
|
|
227
|
+
die('Usage: tr recall <query>');
|
|
228
|
+
}
|
|
229
|
+
const ctx = await buildContext();
|
|
230
|
+
// Generate word trapdoors for blind search
|
|
231
|
+
const trapdoors = generateBlindIndices(query);
|
|
232
|
+
if (trapdoors.length === 0) {
|
|
233
|
+
log(JSON.stringify({ ok: true, count: 0, memories: [] }));
|
|
234
|
+
return;
|
|
235
|
+
}
|
|
236
|
+
try {
|
|
237
|
+
const candidates = await ctx.apiClient.search(ctx.userId, trapdoors, 12, ctx.authKeyHex);
|
|
238
|
+
const memories = [];
|
|
239
|
+
for (const c of candidates) {
|
|
240
|
+
try {
|
|
241
|
+
const raw = decrypt(c.encrypted_blob, ctx.encryptionKey);
|
|
242
|
+
const parsed = JSON.parse(raw);
|
|
243
|
+
if (parsed.text) {
|
|
244
|
+
memories.push({
|
|
245
|
+
id: c.fact_id,
|
|
246
|
+
text: parsed.text,
|
|
247
|
+
score: c.decay_score,
|
|
248
|
+
timestamp: new Date(c.timestamp).toISOString(),
|
|
249
|
+
});
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
catch {
|
|
253
|
+
// Skip undecryptable
|
|
254
|
+
}
|
|
255
|
+
}
|
|
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 }));
|
|
259
|
+
}
|
|
260
|
+
catch (err) {
|
|
261
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
262
|
+
die(`recall failed: ${msg}`);
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
// ---------------------------------------------------------------------------
|
|
266
|
+
// Dispatch
|
|
267
|
+
// ---------------------------------------------------------------------------
|
|
268
|
+
async function main() {
|
|
269
|
+
const args = process.argv.slice(2);
|
|
270
|
+
const cmd = args[0];
|
|
271
|
+
switch (cmd) {
|
|
272
|
+
case 'status':
|
|
273
|
+
await cmdStatus();
|
|
274
|
+
break;
|
|
275
|
+
case 'pair':
|
|
276
|
+
await cmdPair(args.slice(1));
|
|
277
|
+
break;
|
|
278
|
+
case 'remember':
|
|
279
|
+
await cmdRemember(args.slice(1));
|
|
280
|
+
break;
|
|
281
|
+
case 'recall':
|
|
282
|
+
await cmdRecall(args.slice(1));
|
|
283
|
+
break;
|
|
284
|
+
case undefined:
|
|
285
|
+
case '--help':
|
|
286
|
+
case '-h':
|
|
287
|
+
process.stdout.write('TotalReclaw hybrid CLI (OpenClaw 2026.5.2 issue #223 workaround)\n\n' +
|
|
288
|
+
'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' +
|
|
293
|
+
'Environment:\n' +
|
|
294
|
+
' TOTALRECLAW_SERVER_URL — relay URL (default: api-staging.totalreclaw.xyz)\n' +
|
|
295
|
+
' TOTALRECLAW_CREDENTIALS_PATH — override credentials.json path\n');
|
|
296
|
+
break;
|
|
297
|
+
default:
|
|
298
|
+
die(`Unknown command: ${cmd}. Run \`tr --help\` for usage.`);
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
main().catch((err) => {
|
|
302
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
303
|
+
process.stderr.write(`tr: fatal: ${msg}\n`);
|
|
304
|
+
process.exit(2);
|
|
305
|
+
});
|
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
|
@@ -2,6 +2,28 @@
|
|
|
2
2
|
"id": "totalreclaw",
|
|
3
3
|
"name": "TotalReclaw",
|
|
4
4
|
"description": "End-to-end encrypted memory vault for AI agents",
|
|
5
|
+
"kind": "memory",
|
|
6
|
+
"contracts": {
|
|
7
|
+
"tools": [
|
|
8
|
+
"totalreclaw_remember",
|
|
9
|
+
"totalreclaw_recall",
|
|
10
|
+
"totalreclaw_forget",
|
|
11
|
+
"totalreclaw_export",
|
|
12
|
+
"totalreclaw_status",
|
|
13
|
+
"totalreclaw_preload_embedder",
|
|
14
|
+
"totalreclaw_consolidate",
|
|
15
|
+
"totalreclaw_pin",
|
|
16
|
+
"totalreclaw_unpin",
|
|
17
|
+
"totalreclaw_retype",
|
|
18
|
+
"totalreclaw_set_scope",
|
|
19
|
+
"totalreclaw_import_from",
|
|
20
|
+
"totalreclaw_import_batch",
|
|
21
|
+
"totalreclaw_upgrade",
|
|
22
|
+
"totalreclaw_migrate",
|
|
23
|
+
"totalreclaw_pair",
|
|
24
|
+
"totalreclaw_report_qa_bug"
|
|
25
|
+
]
|
|
26
|
+
},
|
|
5
27
|
"configSchema": {
|
|
6
28
|
"type": "object",
|
|
7
29
|
"additionalProperties": false,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@totalreclaw/totalreclaw",
|
|
3
|
-
"version": "3.3.
|
|
3
|
+
"version": "3.3.8-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",
|
package/skill.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "totalreclaw",
|
|
3
|
-
"version": "3.3.
|
|
3
|
+
"version": "3.3.8-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,375 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* tr — TotalReclaw hybrid CLI (3.3.8-rc.1 workaround for OpenClaw 2026.5.2 issue #223)
|
|
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.
|
|
9
|
+
*
|
|
10
|
+
* Phrase-safety: this CLI reads credentials.json (mnemonic at rest) but NEVER
|
|
11
|
+
* prints the mnemonic to stdout, stderr, or any log. Phrase only enters via QR-pair
|
|
12
|
+
* browser tier (pair-cli.ts / pair-cli-relay.ts — unchanged).
|
|
13
|
+
*
|
|
14
|
+
* 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
|
|
19
|
+
*
|
|
20
|
+
* Install: wired via package.json `bin.tr` → dist/tr-cli.js
|
|
21
|
+
* Usage from container: `docker exec tr-openclaw tr status`
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
import path from 'node:path';
|
|
25
|
+
import os from 'node:os';
|
|
26
|
+
import { randomUUID } from 'node:crypto';
|
|
27
|
+
|
|
28
|
+
import { CONFIG } from './config.js';
|
|
29
|
+
import { loadCredentialsJson } from './fs-helpers.js';
|
|
30
|
+
import { printStatus } from './onboarding-cli.js';
|
|
31
|
+
import {
|
|
32
|
+
deriveKeys,
|
|
33
|
+
computeAuthKeyHash,
|
|
34
|
+
encrypt,
|
|
35
|
+
decrypt,
|
|
36
|
+
generateBlindIndices,
|
|
37
|
+
generateContentFingerprint,
|
|
38
|
+
} from './crypto.js';
|
|
39
|
+
import { createApiClient } from './api-client.js';
|
|
40
|
+
|
|
41
|
+
// ---------------------------------------------------------------------------
|
|
42
|
+
// Helpers
|
|
43
|
+
// ---------------------------------------------------------------------------
|
|
44
|
+
|
|
45
|
+
const CREDENTIALS_PATH = CONFIG.credentialsPath;
|
|
46
|
+
const SERVER_URL = CONFIG.serverUrl;
|
|
47
|
+
const STATE_PATH = CONFIG.onboardingStatePath;
|
|
48
|
+
|
|
49
|
+
function die(msg: string, code = 1): never {
|
|
50
|
+
process.stderr.write(`tr: ${msg}\n`);
|
|
51
|
+
process.exit(code);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function log(msg: string): void {
|
|
55
|
+
process.stdout.write(msg + '\n');
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// ---------------------------------------------------------------------------
|
|
59
|
+
// Core init — minimal version of index.ts initialize()
|
|
60
|
+
// ---------------------------------------------------------------------------
|
|
61
|
+
|
|
62
|
+
interface CliContext {
|
|
63
|
+
authKeyHex: string;
|
|
64
|
+
encryptionKey: Buffer;
|
|
65
|
+
dedupKey: Buffer;
|
|
66
|
+
apiClient: ReturnType<typeof createApiClient>;
|
|
67
|
+
userId: string;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
async function buildContext(): Promise<CliContext> {
|
|
71
|
+
const creds = loadCredentialsJson(CREDENTIALS_PATH);
|
|
72
|
+
if (!creds) {
|
|
73
|
+
die('TotalReclaw is not set up. Run: openclaw totalreclaw onboard\n(or: tr pair)');
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const mnemonic =
|
|
77
|
+
(typeof creds.mnemonic === 'string' && creds.mnemonic.trim()) ||
|
|
78
|
+
(typeof creds.recovery_phrase === 'string' && creds.recovery_phrase.trim()) ||
|
|
79
|
+
'';
|
|
80
|
+
|
|
81
|
+
if (!mnemonic) {
|
|
82
|
+
die('No recovery phrase in credentials.json. Run: openclaw totalreclaw onboard');
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Parse existing salt/userId from credentials.json
|
|
86
|
+
let existingSalt: Buffer | undefined;
|
|
87
|
+
let existingUserId: string | undefined;
|
|
88
|
+
|
|
89
|
+
const saltStr = typeof creds.salt === 'string' ? creds.salt : undefined;
|
|
90
|
+
if (saltStr) {
|
|
91
|
+
if (/^[0-9a-f]{64}$/i.test(saltStr)) {
|
|
92
|
+
existingSalt = Buffer.from(saltStr, 'hex');
|
|
93
|
+
} else {
|
|
94
|
+
existingSalt = Buffer.from(saltStr, 'base64');
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
existingUserId = typeof creds.userId === 'string' ? creds.userId : undefined;
|
|
98
|
+
|
|
99
|
+
const keys = deriveKeys(mnemonic, existingSalt);
|
|
100
|
+
const authKeyHex = keys.authKey.toString('hex');
|
|
101
|
+
|
|
102
|
+
const apiClient = createApiClient(SERVER_URL);
|
|
103
|
+
|
|
104
|
+
let userId: string;
|
|
105
|
+
if (existingUserId) {
|
|
106
|
+
userId = existingUserId;
|
|
107
|
+
} else {
|
|
108
|
+
// Register to get userId (idempotent on relay)
|
|
109
|
+
const authHash = computeAuthKeyHash(keys.authKey);
|
|
110
|
+
const saltHex = keys.salt.toString('hex');
|
|
111
|
+
try {
|
|
112
|
+
const result = await apiClient.register(authHash, saltHex);
|
|
113
|
+
userId = result.user_id;
|
|
114
|
+
} catch (err) {
|
|
115
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
116
|
+
if (msg.includes('USER_EXISTS')) {
|
|
117
|
+
userId = authHash.slice(0, 32);
|
|
118
|
+
} else {
|
|
119
|
+
die(`Relay registration failed: ${msg}`);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
return {
|
|
125
|
+
authKeyHex,
|
|
126
|
+
encryptionKey: keys.encryptionKey,
|
|
127
|
+
dedupKey: keys.dedupKey,
|
|
128
|
+
apiClient,
|
|
129
|
+
userId,
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// ---------------------------------------------------------------------------
|
|
134
|
+
// Command: status
|
|
135
|
+
// ---------------------------------------------------------------------------
|
|
136
|
+
|
|
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);
|
|
141
|
+
|
|
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
|
+
try {
|
|
146
|
+
const fs = await import('node:fs');
|
|
147
|
+
const candidatePaths = [
|
|
148
|
+
// extensions-path (local tgz / --force install) — .loaded.json sits at root, not dist/
|
|
149
|
+
path.join(os.homedir(), '.openclaw', 'extensions', 'totalreclaw', '.loaded.json'),
|
|
150
|
+
// npm-path (registry install) — .loaded.json inside dist/
|
|
151
|
+
path.join(os.homedir(), '.openclaw', 'npm', 'node_modules', '@totalreclaw', 'totalreclaw', 'dist', '.loaded.json'),
|
|
152
|
+
];
|
|
153
|
+
const resolvedPath = candidatePaths.find((p) => fs.existsSync(p));
|
|
154
|
+
if (resolvedPath) {
|
|
155
|
+
const raw = fs.readFileSync(resolvedPath, 'utf-8');
|
|
156
|
+
const manifest = JSON.parse(raw) as {
|
|
157
|
+
version?: string;
|
|
158
|
+
bootCount?: number;
|
|
159
|
+
loadedAt?: number;
|
|
160
|
+
hybridMode?: boolean;
|
|
161
|
+
tools?: string[];
|
|
162
|
+
};
|
|
163
|
+
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');
|
|
174
|
+
}
|
|
175
|
+
} catch {
|
|
176
|
+
// Best-effort
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// ---------------------------------------------------------------------------
|
|
181
|
+
// Command: pair
|
|
182
|
+
// ---------------------------------------------------------------------------
|
|
183
|
+
|
|
184
|
+
async function cmdPair(args: string[]): Promise<void> {
|
|
185
|
+
// Delegate to the existing pair-cli-relay.ts via a thin wrapper.
|
|
186
|
+
// The pair flow is relay-brokered (works through Docker NAT).
|
|
187
|
+
// Phrase-safety: pair-cli-relay.ts is x25519-only; mnemonic never appears.
|
|
188
|
+
const outputMode = args.includes('--json') ? 'json' : args.includes('--url-pin') ? 'url-pin' : 'human';
|
|
189
|
+
|
|
190
|
+
const { runRelayPairCli } = await import('./pair-cli-relay.js');
|
|
191
|
+
const { defaultRenderQr, buildDefaultPairCliIo } = await import('./pair-cli.js');
|
|
192
|
+
|
|
193
|
+
const io = buildDefaultPairCliIo();
|
|
194
|
+
const outcome = await runRelayPairCli('generate', {
|
|
195
|
+
relayBaseUrl: CONFIG.pairRelayUrl,
|
|
196
|
+
credentialsPath: CREDENTIALS_PATH,
|
|
197
|
+
onboardingStatePath: STATE_PATH,
|
|
198
|
+
logger: {
|
|
199
|
+
info: (m: string) => process.stderr.write(`[info] ${m}\n`),
|
|
200
|
+
warn: (m: string) => process.stderr.write(`[warn] ${m}\n`),
|
|
201
|
+
error: (m: string) => process.stderr.write(`[error] ${m}\n`),
|
|
202
|
+
},
|
|
203
|
+
pluginVersion: '3.3.8-rc.1',
|
|
204
|
+
deriveScopeAddress: undefined,
|
|
205
|
+
renderQr: defaultRenderQr,
|
|
206
|
+
io,
|
|
207
|
+
outputMode: outputMode as import('./pair-cli.js').PairCliOutputMode,
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
if (outcome.status !== 'completed' && outcome.status !== 'canceled') {
|
|
211
|
+
die(`Pairing ${outcome.status}`, 1);
|
|
212
|
+
}
|
|
213
|
+
if (outcome.status === 'canceled') {
|
|
214
|
+
process.exit(130);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// ---------------------------------------------------------------------------
|
|
219
|
+
// Command: remember
|
|
220
|
+
// ---------------------------------------------------------------------------
|
|
221
|
+
|
|
222
|
+
async function cmdRemember(args: string[]): Promise<void> {
|
|
223
|
+
const text = args.join(' ').trim();
|
|
224
|
+
if (!text) {
|
|
225
|
+
die('Usage: tr remember <text>');
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
const ctx = await buildContext();
|
|
229
|
+
|
|
230
|
+
// Build a minimal MemoryTaxonomy v1 claim blob (same format as storeExtractedFacts)
|
|
231
|
+
const now = new Date().toISOString();
|
|
232
|
+
const factId = randomUUID().replace(/-/g, '');
|
|
233
|
+
|
|
234
|
+
// Encrypt the memory text
|
|
235
|
+
const blob = JSON.stringify({
|
|
236
|
+
text,
|
|
237
|
+
type: 'claim',
|
|
238
|
+
source: 'user',
|
|
239
|
+
scope: 'unspecified',
|
|
240
|
+
importance: 8,
|
|
241
|
+
metadata: {
|
|
242
|
+
type: 'claim',
|
|
243
|
+
source: 'user',
|
|
244
|
+
scope: 'unspecified',
|
|
245
|
+
importance: 8,
|
|
246
|
+
},
|
|
247
|
+
timestamp: now,
|
|
248
|
+
version: 'v1',
|
|
249
|
+
});
|
|
250
|
+
const encrypted_blob = encrypt(blob, ctx.encryptionKey);
|
|
251
|
+
const blind_indices = generateBlindIndices(text);
|
|
252
|
+
const content_fp = generateContentFingerprint(text, ctx.dedupKey);
|
|
253
|
+
|
|
254
|
+
const payload = {
|
|
255
|
+
id: factId,
|
|
256
|
+
timestamp: now,
|
|
257
|
+
encrypted_blob,
|
|
258
|
+
blind_indices,
|
|
259
|
+
decay_score: 8,
|
|
260
|
+
source: 'cli:tr-remember',
|
|
261
|
+
content_fp,
|
|
262
|
+
};
|
|
263
|
+
|
|
264
|
+
try {
|
|
265
|
+
await ctx.apiClient.store(ctx.userId, [payload], ctx.authKeyHex);
|
|
266
|
+
log(JSON.stringify({ ok: true, id: factId, text }));
|
|
267
|
+
} catch (err) {
|
|
268
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
269
|
+
die(`remember failed: ${msg}`);
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// ---------------------------------------------------------------------------
|
|
274
|
+
// Command: recall
|
|
275
|
+
// ---------------------------------------------------------------------------
|
|
276
|
+
|
|
277
|
+
async function cmdRecall(args: string[]): Promise<void> {
|
|
278
|
+
const query = args.join(' ').trim();
|
|
279
|
+
if (!query) {
|
|
280
|
+
die('Usage: tr recall <query>');
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
const ctx = await buildContext();
|
|
284
|
+
|
|
285
|
+
// Generate word trapdoors for blind search
|
|
286
|
+
const trapdoors = generateBlindIndices(query);
|
|
287
|
+
|
|
288
|
+
if (trapdoors.length === 0) {
|
|
289
|
+
log(JSON.stringify({ ok: true, count: 0, memories: [] }));
|
|
290
|
+
return;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
try {
|
|
294
|
+
const candidates = await ctx.apiClient.search(ctx.userId, trapdoors, 12, ctx.authKeyHex);
|
|
295
|
+
|
|
296
|
+
const memories: Array<{ id: string; text: string; score: number; timestamp: string }> = [];
|
|
297
|
+
|
|
298
|
+
for (const c of candidates) {
|
|
299
|
+
try {
|
|
300
|
+
const raw = decrypt(c.encrypted_blob, ctx.encryptionKey);
|
|
301
|
+
const parsed = JSON.parse(raw) as { text?: string };
|
|
302
|
+
if (parsed.text) {
|
|
303
|
+
memories.push({
|
|
304
|
+
id: c.fact_id,
|
|
305
|
+
text: parsed.text,
|
|
306
|
+
score: c.decay_score,
|
|
307
|
+
timestamp: new Date(c.timestamp).toISOString(),
|
|
308
|
+
});
|
|
309
|
+
}
|
|
310
|
+
} catch {
|
|
311
|
+
// Skip undecryptable
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
// Simple relevance sort by decay_score (descending)
|
|
316
|
+
memories.sort((a, b) => b.score - a.score);
|
|
317
|
+
|
|
318
|
+
log(JSON.stringify({ ok: true, count: memories.length, query, memories }));
|
|
319
|
+
} catch (err) {
|
|
320
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
321
|
+
die(`recall failed: ${msg}`);
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
// ---------------------------------------------------------------------------
|
|
326
|
+
// Dispatch
|
|
327
|
+
// ---------------------------------------------------------------------------
|
|
328
|
+
|
|
329
|
+
async function main(): Promise<void> {
|
|
330
|
+
const args = process.argv.slice(2);
|
|
331
|
+
const cmd = args[0];
|
|
332
|
+
|
|
333
|
+
switch (cmd) {
|
|
334
|
+
case 'status':
|
|
335
|
+
await cmdStatus();
|
|
336
|
+
break;
|
|
337
|
+
|
|
338
|
+
case 'pair':
|
|
339
|
+
await cmdPair(args.slice(1));
|
|
340
|
+
break;
|
|
341
|
+
|
|
342
|
+
case 'remember':
|
|
343
|
+
await cmdRemember(args.slice(1));
|
|
344
|
+
break;
|
|
345
|
+
|
|
346
|
+
case 'recall':
|
|
347
|
+
await cmdRecall(args.slice(1));
|
|
348
|
+
break;
|
|
349
|
+
|
|
350
|
+
case undefined:
|
|
351
|
+
case '--help':
|
|
352
|
+
case '-h':
|
|
353
|
+
process.stdout.write(
|
|
354
|
+
'TotalReclaw hybrid CLI (OpenClaw 2026.5.2 issue #223 workaround)\n\n' +
|
|
355
|
+
'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' +
|
|
360
|
+
'Environment:\n' +
|
|
361
|
+
' TOTALRECLAW_SERVER_URL — relay URL (default: api-staging.totalreclaw.xyz)\n' +
|
|
362
|
+
' TOTALRECLAW_CREDENTIALS_PATH — override credentials.json path\n',
|
|
363
|
+
);
|
|
364
|
+
break;
|
|
365
|
+
|
|
366
|
+
default:
|
|
367
|
+
die(`Unknown command: ${cmd}. Run \`tr --help\` for usage.`);
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
main().catch((err) => {
|
|
372
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
373
|
+
process.stderr.write(`tr: fatal: ${msg}\n`);
|
|
374
|
+
process.exit(2);
|
|
375
|
+
});
|