@totalreclaw/totalreclaw 3.2.3 → 3.3.0-rc.2
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 +917 -0
- package/README.md +31 -2
- package/config.ts +5 -0
- package/first-run.ts +131 -0
- package/index.ts +256 -9
- package/onboarding-cli.ts +9 -2
- package/package.json +4 -2
- package/pair-cli.ts +351 -0
- package/pair-crypto.ts +474 -0
- package/pair-http.ts +527 -0
- package/pair-page.ts +841 -0
- package/pair-session-store.ts +764 -0
- package/subgraph-store.ts +2 -2
package/README.md
CHANGED
|
@@ -96,7 +96,36 @@ openclaw plugins install @totalreclaw/totalreclaw
|
|
|
96
96
|
|
|
97
97
|
### 2. Configure
|
|
98
98
|
|
|
99
|
-
|
|
99
|
+
You have three ways to set up TotalReclaw, depending on where your OpenClaw gateway runs.
|
|
100
|
+
|
|
101
|
+
**Local gateway (laptop / workstation):** run the CLI wizard on the same machine:
|
|
102
|
+
|
|
103
|
+
```bash
|
|
104
|
+
openclaw totalreclaw onboard
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
The wizard generates or accepts a 12-word BIP-39 TotalReclaw account key directly on your terminal. The phrase never touches the LLM, the chat transcript, or the network -- it's written straight to `~/.totalreclaw/credentials.json` (mode 0600).
|
|
108
|
+
|
|
109
|
+
**Remote gateway (VPS, home server, shared / team):** use QR-pairing (new in v3.3.0).
|
|
110
|
+
|
|
111
|
+
On the gateway host:
|
|
112
|
+
|
|
113
|
+
```bash
|
|
114
|
+
openclaw totalreclaw pair # generate a new account key
|
|
115
|
+
openclaw totalreclaw pair import # import an existing TotalReclaw key
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
You'll see a QR code, a 6-digit secondary code, and a URL. Scan the QR with your phone's camera or open the URL on any modern browser. The browser page:
|
|
119
|
+
|
|
120
|
+
1. Asks you to enter the 6-digit code (prevents a bystander from hijacking the session).
|
|
121
|
+
2. Generates or accepts your 12-word account key in-page.
|
|
122
|
+
3. Encrypts it end-to-end (x25519 + ChaCha20-Poly1305, key derived from a DH shared secret the relay never sees) and delivers it to your gateway.
|
|
123
|
+
|
|
124
|
+
The phrase never enters the LLM, the chat transcript, or the relay server in plaintext. The pairing URL embeds the gateway's ephemeral public key in the URL fragment -- this is TLS-MITM resistant and invisible to any server on the path. See `CHANGELOG.md` §3.3.0 for the full threat model.
|
|
125
|
+
|
|
126
|
+
Browser support: Safari 17+, Chrome 123+, Firefox 130+ (these ship WebCrypto x25519 + ChaCha20-Poly1305).
|
|
127
|
+
|
|
128
|
+
**Legacy / self-hosted:** set the env var directly (useful for containers / CI):
|
|
100
129
|
|
|
101
130
|
```bash
|
|
102
131
|
export TOTALRECLAW_RECOVERY_PHRASE="your twelve word recovery phrase here"
|
|
@@ -104,7 +133,7 @@ export TOTALRECLAW_RECOVERY_PHRASE="your twelve word recovery phrase here"
|
|
|
104
133
|
|
|
105
134
|
**That's it.** v1 is the default extraction and write path. Extraction cadence, importance floor, candidate pool size, and dedup thresholds are all server-tuned via the relay's billing response -- no client env vars to set. See [env vars reference](../../docs/guides/env-vars-reference.md).
|
|
106
135
|
|
|
107
|
-
For self-hosted
|
|
136
|
+
For self-hosted relays:
|
|
108
137
|
|
|
109
138
|
```bash
|
|
110
139
|
export TOTALRECLAW_SERVER_URL="http://your-totalreclaw-server:8080"
|
package/config.ts
CHANGED
|
@@ -99,6 +99,11 @@ export const CONFIG = {
|
|
|
99
99
|
// never contains secrets. Loaded on every plugin init + on every
|
|
100
100
|
// before_tool_call gate check.
|
|
101
101
|
onboardingStatePath: process.env.TOTALRECLAW_STATE_PATH || path.join(home, '.totalreclaw', 'state.json'),
|
|
102
|
+
// 3.3.0 QR-pairing session store. Separate file from both credentials.json
|
|
103
|
+
// and state.json so the session-store module does not have to touch either
|
|
104
|
+
// (keeps scanner surface isolated). Contains ephemeral x25519 secret keys
|
|
105
|
+
// for 15-min TTL windows; 0600 mode.
|
|
106
|
+
pairSessionsPath: process.env.TOTALRECLAW_PAIR_SESSIONS_PATH || path.join(home, '.totalreclaw', 'pair-sessions.json'),
|
|
102
107
|
|
|
103
108
|
// Chain — chainId is no longer user-configurable. It is auto-detected from
|
|
104
109
|
// the relay billing response (free = Base Sepolia / 84532, Pro = Gnosis /
|
package/first-run.ts
ADDED
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* first-run — detect a fresh machine and return the welcome/branch-question
|
|
3
|
+
* copy that the `before_agent_start` hook prepends to the first agent prompt
|
|
4
|
+
* after install.
|
|
5
|
+
*
|
|
6
|
+
* Shipped 2026-04-20 as part of the 3.3.0-rc.2 UX polish. Paired with the
|
|
7
|
+
* scanner false-positive fix that unblocked rc.1 install.
|
|
8
|
+
*
|
|
9
|
+
* Scope and scanner surface
|
|
10
|
+
* -------------------------
|
|
11
|
+
* - This module reads credentials.json via `loadCredentialsJson` from
|
|
12
|
+
* `fs-helpers.ts` (the one file in the plugin that is allowed to touch
|
|
13
|
+
* disk) — we do NOT import `node:fs` directly. That preserves the
|
|
14
|
+
* file-level isolation pattern introduced in 3.0.8 (see `fs-helpers.ts`
|
|
15
|
+
* header) and ensures the expanded `check-scanner.mjs` rules cannot
|
|
16
|
+
* flag this file even incidentally.
|
|
17
|
+
* - No network. No env-var reads. No dynamic code execution.
|
|
18
|
+
* - All user-facing copy is exported as `COPY` so tests can assert on
|
|
19
|
+
* exact strings and a future localisation pass has a single seam.
|
|
20
|
+
*
|
|
21
|
+
* Design notes
|
|
22
|
+
* ------------
|
|
23
|
+
* - `detectFirstRun` is deliberately lax: missing file, empty file,
|
|
24
|
+
* JSON-parse-error, or a file that parses but carries no usable
|
|
25
|
+
* mnemonic (neither `mnemonic` nor the `recovery_phrase` alias) all
|
|
26
|
+
* count as first-run. Anything looser would risk double-welcoming a
|
|
27
|
+
* returning user whose credentials.json has been hand-edited.
|
|
28
|
+
* - `buildWelcomePrepend` branches on `'local'` vs `'remote'` gateway
|
|
29
|
+
* mode. The caller in `index.ts` resolves the mode from
|
|
30
|
+
* `api.config.gateway.remote.url` the same way `buildPairingUrl`
|
|
31
|
+
* already does.
|
|
32
|
+
* - Terminology: "recovery phrase" everywhere in user-facing copy. The
|
|
33
|
+
* prior mix of "account key", "mnemonic", "seed phrase", and "recovery
|
|
34
|
+
* phrase" across the plugin was confusing users; 3.3.0-rc.2
|
|
35
|
+
* standardises on "recovery phrase". Internal variable names
|
|
36
|
+
* (`mnemonic`, etc.) are intentionally kept so we do not churn the
|
|
37
|
+
* crypto code for a copy change.
|
|
38
|
+
*/
|
|
39
|
+
|
|
40
|
+
import { loadCredentialsJson, extractBootstrapMnemonic } from './fs-helpers.js';
|
|
41
|
+
|
|
42
|
+
// ---------------------------------------------------------------------------
|
|
43
|
+
// Canonical copy — single source of truth for the welcome-on-first-run UX.
|
|
44
|
+
// Tests import these constants and assert on exact-match substrings; the
|
|
45
|
+
// `index.ts` before_agent_start hook consumes them via `buildWelcomePrepend`.
|
|
46
|
+
// ---------------------------------------------------------------------------
|
|
47
|
+
|
|
48
|
+
export const WELCOME =
|
|
49
|
+
'Welcome to TotalReclaw — encrypted, agent-portable memory.\n\n' +
|
|
50
|
+
'Your memories are stored end-to-end encrypted and on-chain. You can restore them on any agent — OpenClaw, Hermes, or NanoClaw — with a single recovery phrase.';
|
|
51
|
+
|
|
52
|
+
export const BRANCH_QUESTION =
|
|
53
|
+
"Let's set up your account. Do you already have a recovery phrase, or should we generate a new one?";
|
|
54
|
+
|
|
55
|
+
export const LOCAL_MODE_INSTRUCTIONS =
|
|
56
|
+
'If you have one, run: openclaw plugin totalreclaw onboard restore\n' +
|
|
57
|
+
'If you need a new one, run: openclaw plugin totalreclaw onboard generate';
|
|
58
|
+
|
|
59
|
+
export const REMOTE_MODE_INSTRUCTIONS =
|
|
60
|
+
'Run: openclaw plugin totalreclaw pair start\n' +
|
|
61
|
+
'This opens a browser page with a QR code. Scan it (or open the URL) to complete setup securely — your recovery phrase never passes through the chat.';
|
|
62
|
+
|
|
63
|
+
export const STORAGE_GUIDANCE =
|
|
64
|
+
'Your recovery phrase is 12 words. Store it somewhere safe — a password manager works well. Use it only for TotalReclaw. Don\'t reuse it anywhere else. Don\'t put funds on it.';
|
|
65
|
+
|
|
66
|
+
export const RESTORE_PROMPT =
|
|
67
|
+
'Enter your 12-word recovery phrase to restore your account.';
|
|
68
|
+
|
|
69
|
+
export const GENERATED_CONFIRMATION =
|
|
70
|
+
'A new recovery phrase has been generated. Write it down now, somewhere safe. This is the only way to restore your account later.';
|
|
71
|
+
|
|
72
|
+
export const COPY = {
|
|
73
|
+
WELCOME,
|
|
74
|
+
BRANCH_QUESTION,
|
|
75
|
+
LOCAL_MODE_INSTRUCTIONS,
|
|
76
|
+
REMOTE_MODE_INSTRUCTIONS,
|
|
77
|
+
STORAGE_GUIDANCE,
|
|
78
|
+
RESTORE_PROMPT,
|
|
79
|
+
GENERATED_CONFIRMATION,
|
|
80
|
+
} as const;
|
|
81
|
+
|
|
82
|
+
// ---------------------------------------------------------------------------
|
|
83
|
+
// Public API
|
|
84
|
+
// ---------------------------------------------------------------------------
|
|
85
|
+
|
|
86
|
+
export type GatewayMode = 'local' | 'remote';
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Returns `true` when the machine at `credentialsPath` has never been
|
|
90
|
+
* onboarded. Specifically: the file is missing, unreadable, invalid JSON,
|
|
91
|
+
* or parses but carries neither `mnemonic` nor `recovery_phrase`.
|
|
92
|
+
*
|
|
93
|
+
* All failure modes collapse to "first run" so the welcome can always
|
|
94
|
+
* recover from a broken install. The caller is responsible for deciding
|
|
95
|
+
* whether to ALSO preserve the broken file for recovery (the onboarding
|
|
96
|
+
* wizard already handles that via `autoBootstrapCredentials`).
|
|
97
|
+
*/
|
|
98
|
+
export async function detectFirstRun(credentialsPath: string): Promise<boolean> {
|
|
99
|
+
const creds = loadCredentialsJson(credentialsPath);
|
|
100
|
+
if (!creds) return true;
|
|
101
|
+
const mnemonic = extractBootstrapMnemonic(creds);
|
|
102
|
+
return mnemonic === null || mnemonic.length === 0;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Build the exact text to feed `prependContext` on first run. The text is
|
|
107
|
+
* structured as a markdown block with a visible heading so the agent and
|
|
108
|
+
* user can both tell at a glance that this is the one-shot first-run
|
|
109
|
+
* banner, not arbitrary injected context.
|
|
110
|
+
*
|
|
111
|
+
* The mode-specific instructions branch on whether the gateway is running
|
|
112
|
+
* locally (user has shell access → CLI onboard wizard) or remotely (user
|
|
113
|
+
* needs QR-pairing). The caller resolves the mode from
|
|
114
|
+
* `api.config.gateway.remote.url` — same resolution `buildPairingUrl`
|
|
115
|
+
* uses.
|
|
116
|
+
*/
|
|
117
|
+
export function buildWelcomePrepend(mode: GatewayMode): string {
|
|
118
|
+
const instructions =
|
|
119
|
+
mode === 'local' ? LOCAL_MODE_INSTRUCTIONS : REMOTE_MODE_INSTRUCTIONS;
|
|
120
|
+
|
|
121
|
+
return (
|
|
122
|
+
'## Welcome to TotalReclaw\n\n' +
|
|
123
|
+
WELCOME +
|
|
124
|
+
'\n\n' +
|
|
125
|
+
BRANCH_QUESTION +
|
|
126
|
+
'\n\n' +
|
|
127
|
+
instructions +
|
|
128
|
+
'\n\n' +
|
|
129
|
+
STORAGE_GUIDANCE
|
|
130
|
+
);
|
|
131
|
+
}
|
package/index.ts
CHANGED
|
@@ -136,6 +136,7 @@ import {
|
|
|
136
136
|
type OnboardingState,
|
|
137
137
|
} from './fs-helpers.js';
|
|
138
138
|
import { decideToolGate, isGatedToolName } from './tool-gating.js';
|
|
139
|
+
import { detectFirstRun, buildWelcomePrepend, type GatewayMode } from './first-run.js';
|
|
139
140
|
import crypto from 'node:crypto';
|
|
140
141
|
|
|
141
142
|
// ---------------------------------------------------------------------------
|
|
@@ -204,6 +205,16 @@ interface OpenClawPluginApi {
|
|
|
204
205
|
config?: unknown;
|
|
205
206
|
}) => { text: string } | Promise<{ text: string }>;
|
|
206
207
|
}): void;
|
|
208
|
+
/**
|
|
209
|
+
* 3.3.0 — register an HTTP route on the gateway's HTTP server.
|
|
210
|
+
* Used by the QR-pairing flow to serve the pairing page + the
|
|
211
|
+
* encrypted-payload respond endpoint. Path is exact-match against
|
|
212
|
+
* `new URL(req.url, ...).pathname`; no params supported.
|
|
213
|
+
*/
|
|
214
|
+
registerHttpRoute?(params: {
|
|
215
|
+
path: string;
|
|
216
|
+
handler: (req: import('node:http').IncomingMessage, res: import('node:http').ServerResponse) => Promise<void> | void;
|
|
217
|
+
}): void;
|
|
207
218
|
}
|
|
208
219
|
|
|
209
220
|
// ---------------------------------------------------------------------------
|
|
@@ -245,6 +256,110 @@ function humanizeError(rawMessage: string): string {
|
|
|
245
256
|
/** Path where we persist userId + salt across restarts. */
|
|
246
257
|
const CREDENTIALS_PATH = CONFIG.credentialsPath;
|
|
247
258
|
|
|
259
|
+
// ---------------------------------------------------------------------------
|
|
260
|
+
// 3.3.0 — pairing URL resolution
|
|
261
|
+
// ---------------------------------------------------------------------------
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* Build the full pairing URL (including `#pk=` fragment) for a fresh
|
|
265
|
+
* pairing session. Pulls gateway config from `api.config.gateway`.
|
|
266
|
+
*
|
|
267
|
+
* Resolution order (mirrors the device-pair extension):
|
|
268
|
+
* 1. pluginConfig.publicUrl (if the operator set it explicitly)
|
|
269
|
+
* 2. gateway.remote.url (if the gateway is marked remote)
|
|
270
|
+
* 3. gateway.bind=custom + customBindHost + port
|
|
271
|
+
* 4. gateway.bind=tailnet/lan is acknowledged but we do NOT probe
|
|
272
|
+
* the host here (network calls); we fall back to localhost with
|
|
273
|
+
* a warning log.
|
|
274
|
+
* 5. gateway.port default = 18789 + localhost.
|
|
275
|
+
*
|
|
276
|
+
* Always returns a working URL string; never throws. The caller can
|
|
277
|
+
* log a warning if the URL is localhost and the gateway is remote,
|
|
278
|
+
* but the CLI always prints whatever we give it.
|
|
279
|
+
*/
|
|
280
|
+
function buildPairingUrl(
|
|
281
|
+
api: Pick<OpenClawPluginApi, 'config' | 'pluginConfig' | 'logger'>,
|
|
282
|
+
session: { sid: string; pkGatewayB64: string },
|
|
283
|
+
): string {
|
|
284
|
+
const cfg = api.config as {
|
|
285
|
+
gateway?: {
|
|
286
|
+
port?: number;
|
|
287
|
+
bind?: string;
|
|
288
|
+
customBindHost?: string;
|
|
289
|
+
tls?: { enabled?: boolean };
|
|
290
|
+
remote?: { url?: string };
|
|
291
|
+
};
|
|
292
|
+
} | undefined;
|
|
293
|
+
const pluginCfg = (api.pluginConfig ?? {}) as { publicUrl?: string };
|
|
294
|
+
|
|
295
|
+
const tlsEnabled = cfg?.gateway?.tls?.enabled === true;
|
|
296
|
+
const scheme = tlsEnabled ? 'https' : 'http';
|
|
297
|
+
const port = cfg?.gateway?.port ?? 18789;
|
|
298
|
+
|
|
299
|
+
let base: string;
|
|
300
|
+
if (typeof pluginCfg.publicUrl === 'string' && pluginCfg.publicUrl.trim()) {
|
|
301
|
+
base = pluginCfg.publicUrl.replace(/\/+$/, '');
|
|
302
|
+
// If the user gave us a ws:// URL, rewrite to http(s)://
|
|
303
|
+
base = base.replace(/^wss:\/\//i, 'https://').replace(/^ws:\/\//i, 'http://');
|
|
304
|
+
} else if (typeof cfg?.gateway?.remote?.url === 'string' && cfg.gateway.remote.url.trim()) {
|
|
305
|
+
base = cfg.gateway.remote.url.trim().replace(/\/+$/, '');
|
|
306
|
+
base = base.replace(/^wss:\/\//i, 'https://').replace(/^ws:\/\//i, 'http://');
|
|
307
|
+
} else if (cfg?.gateway?.bind === 'custom' && cfg.gateway.customBindHost) {
|
|
308
|
+
base = `${scheme}://${cfg.gateway.customBindHost}:${port}`;
|
|
309
|
+
} else {
|
|
310
|
+
const bind = cfg?.gateway?.bind;
|
|
311
|
+
if (bind === 'lan' || bind === 'tailnet') {
|
|
312
|
+
api.logger.warn(
|
|
313
|
+
`TotalReclaw: pairing URL is falling back to localhost because gateway.bind=${bind} without explicit host probe. ` +
|
|
314
|
+
'Set plugins.entries.totalreclaw.config.publicUrl to override.',
|
|
315
|
+
);
|
|
316
|
+
}
|
|
317
|
+
base = `${scheme}://localhost:${port}`;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
return `${base}/plugin/totalreclaw/pair/finish?sid=${encodeURIComponent(session.sid)}#pk=${encodeURIComponent(session.pkGatewayB64)}`;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
/**
|
|
324
|
+
* Resolve whether this plugin is running on a `local` or `remote` gateway.
|
|
325
|
+
*
|
|
326
|
+
* Follows the same config surface `buildPairingUrl` uses:
|
|
327
|
+
* - `pluginConfig.publicUrl` set + non-localhost → remote
|
|
328
|
+
* - `gateway.remote.url` set + non-localhost → remote
|
|
329
|
+
* - `gateway.bind === 'lan' | 'tailnet' | 'custom'` → remote
|
|
330
|
+
* - anything else → local
|
|
331
|
+
*
|
|
332
|
+
* We treat a `publicUrl` or `remote.url` that points at `localhost` /
|
|
333
|
+
* `127.*` as local because that is what a dev-loopback override looks like;
|
|
334
|
+
* no one publishes a remote QR pairing for localhost.
|
|
335
|
+
*/
|
|
336
|
+
function resolveGatewayMode(
|
|
337
|
+
api: Pick<OpenClawPluginApi, 'config' | 'pluginConfig'>,
|
|
338
|
+
): GatewayMode {
|
|
339
|
+
const cfg = api.config as
|
|
340
|
+
| { gateway?: { bind?: string; remote?: { url?: string } } }
|
|
341
|
+
| undefined;
|
|
342
|
+
const pluginCfg = (api.pluginConfig ?? {}) as { publicUrl?: string };
|
|
343
|
+
const looksLocal = (url: string | undefined): boolean => {
|
|
344
|
+
if (!url) return true;
|
|
345
|
+
const u = url.trim().toLowerCase();
|
|
346
|
+
if (u === '') return true;
|
|
347
|
+
return /^(?:wss?:\/\/|https?:\/\/)?(?:localhost|127\.|0\.0\.0\.0)/.test(u);
|
|
348
|
+
};
|
|
349
|
+
if (typeof pluginCfg.publicUrl === 'string' && !looksLocal(pluginCfg.publicUrl)) {
|
|
350
|
+
return 'remote';
|
|
351
|
+
}
|
|
352
|
+
const remoteUrl = cfg?.gateway?.remote?.url;
|
|
353
|
+
if (typeof remoteUrl === 'string' && !looksLocal(remoteUrl)) {
|
|
354
|
+
return 'remote';
|
|
355
|
+
}
|
|
356
|
+
const bind = cfg?.gateway?.bind;
|
|
357
|
+
if (bind === 'lan' || bind === 'tailnet' || bind === 'custom') {
|
|
358
|
+
return 'remote';
|
|
359
|
+
}
|
|
360
|
+
return 'local';
|
|
361
|
+
}
|
|
362
|
+
|
|
248
363
|
// ---------------------------------------------------------------------------
|
|
249
364
|
// Cosine similarity threshold — skip injection when top result is below this
|
|
250
365
|
// ---------------------------------------------------------------------------
|
|
@@ -458,6 +573,15 @@ let needsSetup = false;
|
|
|
458
573
|
/** True on first before_agent_start after successful init — show welcome message once. */
|
|
459
574
|
let firstRunAfterInit = true;
|
|
460
575
|
|
|
576
|
+
/**
|
|
577
|
+
* Once-per-gateway-session flag for the 3.3.0-rc.2 first-run welcome banner.
|
|
578
|
+
* The banner fires on the first `before_agent_start` after install when
|
|
579
|
+
* credentials.json is absent/empty — exactly once per gateway process.
|
|
580
|
+
* A second before_agent_start in the same session finds this flipped and
|
|
581
|
+
* skips. A fresh gateway restart resets it back to `false`.
|
|
582
|
+
*/
|
|
583
|
+
let firstRunWelcomeShown = false;
|
|
584
|
+
|
|
461
585
|
/**
|
|
462
586
|
* Derive keys from the recovery phrase, load credentials, and register with
|
|
463
587
|
* the server if this is the first run.
|
|
@@ -659,7 +783,7 @@ function buildSetupErrorMsg(): string {
|
|
|
659
783
|
function buildSetupErrorMsgLegacy(): string {
|
|
660
784
|
const base =
|
|
661
785
|
'TotalReclaw setup required:\n' +
|
|
662
|
-
'1. Set TOTALRECLAW_RECOVERY_PHRASE — ask the user if they have an existing recovery phrase or generate a new 12-word
|
|
786
|
+
'1. Set TOTALRECLAW_RECOVERY_PHRASE — ask the user if they have an existing recovery phrase or generate a new 12-word recovery phrase.\n' +
|
|
663
787
|
'2. Restart the gateway to apply changes.\n' +
|
|
664
788
|
' (Optional: set TOTALRECLAW_SELF_HOSTED=true if using your own server instead of the managed service.)\n\n';
|
|
665
789
|
|
|
@@ -2557,6 +2681,14 @@ const plugin = {
|
|
|
2557
2681
|
statePath: CONFIG.onboardingStatePath,
|
|
2558
2682
|
logger: api.logger,
|
|
2559
2683
|
});
|
|
2684
|
+
// 3.3.0 — `openclaw totalreclaw pair [generate|import]` attaches
|
|
2685
|
+
// alongside the existing `onboard` + `status` subcommands.
|
|
2686
|
+
const { registerPairCli } = await import('./pair-cli.js');
|
|
2687
|
+
registerPairCli(program as import('commander').Command, {
|
|
2688
|
+
sessionsPath: CONFIG.pairSessionsPath,
|
|
2689
|
+
renderPairingUrl: (session) => buildPairingUrl(api, session),
|
|
2690
|
+
logger: api.logger,
|
|
2691
|
+
});
|
|
2560
2692
|
},
|
|
2561
2693
|
{ commands: ['totalreclaw'] },
|
|
2562
2694
|
);
|
|
@@ -2567,6 +2699,69 @@ const plugin = {
|
|
|
2567
2699
|
);
|
|
2568
2700
|
}
|
|
2569
2701
|
|
|
2702
|
+
// ---------------------------------------------------------------
|
|
2703
|
+
// 3.3.0 — HTTP routes for QR-pairing (pair-http)
|
|
2704
|
+
// ---------------------------------------------------------------
|
|
2705
|
+
//
|
|
2706
|
+
// Four endpoints under /plugin/totalreclaw/pair/ are registered on
|
|
2707
|
+
// the gateway's HTTP server. Collectively they serve the browser
|
|
2708
|
+
// pairing page, verify the 6-digit secondary code, accept the
|
|
2709
|
+
// encrypted mnemonic payload, and expose a status polled by the
|
|
2710
|
+
// CLI. See pair-http.ts and the 2026-04-20 design doc.
|
|
2711
|
+
if (typeof api.registerHttpRoute === 'function') {
|
|
2712
|
+
(async () => {
|
|
2713
|
+
try {
|
|
2714
|
+
const { buildPairRoutes } = await import('./pair-http.js');
|
|
2715
|
+
const { validateMnemonic } = await import('@scure/bip39');
|
|
2716
|
+
const { wordlist } = await import('@scure/bip39/wordlists/english.js');
|
|
2717
|
+
const bundle = buildPairRoutes({
|
|
2718
|
+
sessionsPath: CONFIG.pairSessionsPath,
|
|
2719
|
+
apiBase: '/plugin/totalreclaw/pair',
|
|
2720
|
+
logger: api.logger,
|
|
2721
|
+
validateMnemonic: (p) => validateMnemonic(p, wordlist),
|
|
2722
|
+
completePairing: async ({ mnemonic }) => {
|
|
2723
|
+
// Write credentials.json + flip state to 'active' via
|
|
2724
|
+
// fs-helpers. This centralizes disk I/O off the
|
|
2725
|
+
// pair-http surface (scanner isolation).
|
|
2726
|
+
const creds = loadCredentialsJson(CREDENTIALS_PATH) ?? {};
|
|
2727
|
+
const next = { ...creds, mnemonic };
|
|
2728
|
+
if (!writeCredentialsJson(CREDENTIALS_PATH, next)) {
|
|
2729
|
+
return { state: 'error', error: 'credentials_write_failed' };
|
|
2730
|
+
}
|
|
2731
|
+
// Hot-reload: update the runtime override so existing
|
|
2732
|
+
// in-memory state picks up the new phrase without a
|
|
2733
|
+
// process restart.
|
|
2734
|
+
setRecoveryPhraseOverride(mnemonic);
|
|
2735
|
+
// Flip onboarding state. writeOnboardingState is in
|
|
2736
|
+
// fs-helpers; dynamic import to keep it out of any
|
|
2737
|
+
// potential scanner collision surface in this file.
|
|
2738
|
+
const { writeOnboardingState } = await import('./fs-helpers.js');
|
|
2739
|
+
writeOnboardingState(CONFIG.onboardingStatePath, {
|
|
2740
|
+
onboardingState: 'active',
|
|
2741
|
+
createdBy: 'generate',
|
|
2742
|
+
credentialsCreatedAt: new Date().toISOString(),
|
|
2743
|
+
version: '3.3.0',
|
|
2744
|
+
});
|
|
2745
|
+
return { state: 'active' };
|
|
2746
|
+
},
|
|
2747
|
+
});
|
|
2748
|
+
api.registerHttpRoute!({ path: bundle.finishPath, handler: bundle.handlers.finish });
|
|
2749
|
+
api.registerHttpRoute!({ path: bundle.startPath, handler: bundle.handlers.start });
|
|
2750
|
+
api.registerHttpRoute!({ path: bundle.respondPath, handler: bundle.handlers.respond });
|
|
2751
|
+
api.registerHttpRoute!({ path: bundle.statusPath, handler: bundle.handlers.status });
|
|
2752
|
+
api.logger.info('TotalReclaw: registered 4 QR-pairing HTTP routes');
|
|
2753
|
+
} catch (err) {
|
|
2754
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
2755
|
+
api.logger.error(`TotalReclaw: failed to register pairing HTTP routes: ${msg}`);
|
|
2756
|
+
}
|
|
2757
|
+
})();
|
|
2758
|
+
} else {
|
|
2759
|
+
api.logger.warn(
|
|
2760
|
+
'api.registerHttpRoute is unavailable on this OpenClaw version — /totalreclaw pair will not work. ' +
|
|
2761
|
+
'Use `openclaw totalreclaw onboard` on the gateway host instead.',
|
|
2762
|
+
);
|
|
2763
|
+
}
|
|
2764
|
+
|
|
2570
2765
|
// ---------------------------------------------------------------
|
|
2571
2766
|
// 3.2.0 — slash command `/totalreclaw {onboard,status}` (in-chat bridge)
|
|
2572
2767
|
// ---------------------------------------------------------------
|
|
@@ -2583,17 +2778,48 @@ const plugin = {
|
|
|
2583
2778
|
acceptsArgs: true,
|
|
2584
2779
|
requireAuth: false,
|
|
2585
2780
|
handler: async (ctx) => {
|
|
2586
|
-
const
|
|
2781
|
+
const args = (ctx.args || '').trim();
|
|
2782
|
+
const parts = args.split(/\s+/).filter(Boolean);
|
|
2783
|
+
const sub = (parts[0] || 'help').toLowerCase();
|
|
2587
2784
|
if (sub === 'onboard' || sub === 'setup' || sub === 'init') {
|
|
2588
2785
|
return {
|
|
2589
2786
|
text:
|
|
2590
|
-
'To set up TotalReclaw
|
|
2787
|
+
'To set up TotalReclaw on a local machine, run:\n\n' +
|
|
2591
2788
|
' openclaw totalreclaw onboard\n\n' +
|
|
2592
|
-
'
|
|
2593
|
-
'
|
|
2594
|
-
'not
|
|
2595
|
-
'
|
|
2596
|
-
'
|
|
2789
|
+
'For a REMOTE gateway (VPS, home server, etc.) use QR-pairing:\n\n' +
|
|
2790
|
+
' /totalreclaw pair\n\n' +
|
|
2791
|
+
'Why not paste the phrase here? Chat messages are visible to the ' +
|
|
2792
|
+
'LLM. Both flows keep your recovery phrase off the LLM transcript: ' +
|
|
2793
|
+
'the CLI wizard runs on your terminal, and the QR-pair flow ' +
|
|
2794
|
+
'encrypts the phrase in your browser before upload.',
|
|
2795
|
+
};
|
|
2796
|
+
}
|
|
2797
|
+
if (sub === 'pair') {
|
|
2798
|
+
// 3.3.0 — remote QR pairing. The slash command is a non-secret
|
|
2799
|
+
// pointer: it tells the operator to run the CLI on the gateway
|
|
2800
|
+
// host (which emits the QR + URL + code). Running the full
|
|
2801
|
+
// pairing protocol directly from this handler would require
|
|
2802
|
+
// sending the URL + code through the chat transcript, which
|
|
2803
|
+
// the LLM would then see — acceptable for the URL + code (both
|
|
2804
|
+
// are non-secret, because the gateway ephemeral pk lives in
|
|
2805
|
+
// the URL fragment and the 6-digit code is one-shot), but
|
|
2806
|
+
// requires the gateway to actually be reachable AND the user
|
|
2807
|
+
// to type a code from chat into a browser on a different
|
|
2808
|
+
// device. Design doc section 4a recommends the CLI path as
|
|
2809
|
+
// primary. Chat-delivery is a future 3.4.0 enhancement.
|
|
2810
|
+
return {
|
|
2811
|
+
text:
|
|
2812
|
+
'Remote pairing (QR):\n\n' +
|
|
2813
|
+
' On the gateway host, run:\n\n' +
|
|
2814
|
+
' openclaw totalreclaw pair # generate new account\n' +
|
|
2815
|
+
' openclaw totalreclaw pair import # import existing\n\n' +
|
|
2816
|
+
'It will print a QR code + a 6-digit secondary code + a URL. ' +
|
|
2817
|
+
'Scan the QR with your phone (or open the URL on any browser). ' +
|
|
2818
|
+
'Enter the 6-digit code in the browser, write down (or paste) ' +
|
|
2819
|
+
'your recovery phrase, and the gateway will activate.\n\n' +
|
|
2820
|
+
'The phrase is generated (or pasted) in your BROWSER and ' +
|
|
2821
|
+
'encrypted end-to-end before upload. It never touches the ' +
|
|
2822
|
+
'LLM, this chat, or the relay server in plaintext.',
|
|
2597
2823
|
};
|
|
2598
2824
|
}
|
|
2599
2825
|
if (sub === 'status') {
|
|
@@ -2610,13 +2836,14 @@ const plugin = {
|
|
|
2610
2836
|
`TotalReclaw onboarding state: ${stateLabel}.\n` +
|
|
2611
2837
|
(stateLabel === 'active'
|
|
2612
2838
|
? 'Memory tools are active on this machine.'
|
|
2613
|
-
: 'Memory tools are gated. Run `openclaw totalreclaw onboard`
|
|
2839
|
+
: 'Memory tools are gated. Run `openclaw totalreclaw onboard` (local) or `openclaw totalreclaw pair` (remote) to complete setup.'),
|
|
2614
2840
|
};
|
|
2615
2841
|
}
|
|
2616
2842
|
return {
|
|
2617
2843
|
text:
|
|
2618
2844
|
'TotalReclaw slash commands:\n' +
|
|
2619
2845
|
' /totalreclaw onboard — how to set up TotalReclaw securely\n' +
|
|
2846
|
+
' /totalreclaw pair — remote-gateway QR-pairing (3.3.0)\n' +
|
|
2620
2847
|
' /totalreclaw status — current onboarding state',
|
|
2621
2848
|
};
|
|
2622
2849
|
},
|
|
@@ -4364,9 +4591,29 @@ const plugin = {
|
|
|
4364
4591
|
// This contains ZERO secret material — the phrase never enters an
|
|
4365
4592
|
// LLM request. The CLI wizard (`openclaw totalreclaw onboard`) is
|
|
4366
4593
|
// the only surface that generates / reveals the recovery phrase.
|
|
4594
|
+
//
|
|
4595
|
+
// 3.3.0-rc.2: the FIRST time a fresh machine hits this branch we
|
|
4596
|
+
// also include the welcome+branch-question banner (copy in
|
|
4597
|
+
// `first-run.ts`). The flag is session-scoped so the welcome never
|
|
4598
|
+
// fires twice in the same gateway process.
|
|
4367
4599
|
if (needsSetup) {
|
|
4600
|
+
let welcomeBlock = '';
|
|
4601
|
+
try {
|
|
4602
|
+
if (!firstRunWelcomeShown && (await detectFirstRun(CREDENTIALS_PATH))) {
|
|
4603
|
+
const mode = resolveGatewayMode(api);
|
|
4604
|
+
welcomeBlock = buildWelcomePrepend(mode) + '\n\n';
|
|
4605
|
+
firstRunWelcomeShown = true;
|
|
4606
|
+
api.logger.info(`TotalReclaw first-run welcome emitted (mode=${mode})`);
|
|
4607
|
+
}
|
|
4608
|
+
} catch (err) {
|
|
4609
|
+
// Never block session start on the welcome — treat any failure
|
|
4610
|
+
// as "skip the welcome, still emit the setup-pending banner".
|
|
4611
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
4612
|
+
api.logger.warn(`First-run welcome check failed: ${msg}`);
|
|
4613
|
+
}
|
|
4368
4614
|
return {
|
|
4369
4615
|
prependContext:
|
|
4616
|
+
welcomeBlock +
|
|
4370
4617
|
'## TotalReclaw setup pending\n\n' +
|
|
4371
4618
|
'TotalReclaw encrypted memory is installed but not yet set up on this machine. ' +
|
|
4372
4619
|
'If the user asks about memory features or wants to configure TotalReclaw, ' +
|
package/onboarding-cli.ts
CHANGED
|
@@ -124,7 +124,7 @@ export const COPY = {
|
|
|
124
124
|
'\nWord mismatch. Please write the phrase down carefully and run this\n' +
|
|
125
125
|
'wizard again. No credentials have been written.\n',
|
|
126
126
|
importInvalid:
|
|
127
|
-
'\nInvalid
|
|
127
|
+
'\nInvalid recovery phrase (12 words required, checksum must match).\n' +
|
|
128
128
|
'No credentials have been written. Run the wizard again with the\n' +
|
|
129
129
|
'correct phrase.\n',
|
|
130
130
|
existingPhraseHint:
|
|
@@ -407,12 +407,19 @@ export async function runOnboardingWizard(deps: WizardDeps): Promise<WizardResul
|
|
|
407
407
|
io.stdout.write(COPY.importRemoteLimitation);
|
|
408
408
|
const mnemonic = genMnemonic();
|
|
409
409
|
if (typeof mnemonic !== 'string' || mnemonic.trim().split(/\s+/).length !== 12) {
|
|
410
|
-
io.stderr.write('\nInternal error:
|
|
410
|
+
io.stderr.write('\nInternal error: recovery phrase generator returned an invalid phrase.\n');
|
|
411
411
|
return { choice, error: 'generator-invalid' };
|
|
412
412
|
}
|
|
413
413
|
|
|
414
414
|
io.stdout.write('\nYour recovery phrase (WRITE THIS DOWN):\n\n');
|
|
415
415
|
printMnemonicGrid(mnemonic, io.stdout);
|
|
416
|
+
// 3.3.0-rc.2: storage guidance canonical copy — emitted verbatim so
|
|
417
|
+
// the CLI, the browser page, and any future surface share identical
|
|
418
|
+
// wording. See first-run.ts COPY.STORAGE_GUIDANCE.
|
|
419
|
+
io.stdout.write(
|
|
420
|
+
'\n' +
|
|
421
|
+
'Your recovery phrase is 12 words. Store it somewhere safe — a password manager works well. Use it only for TotalReclaw. Don\'t reuse it anywhere else. Don\'t put funds on it.\n',
|
|
422
|
+
);
|
|
416
423
|
io.stdout.write(COPY.clipboardHint);
|
|
417
424
|
io.stdout.write('\n');
|
|
418
425
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@totalreclaw/totalreclaw",
|
|
3
|
-
"version": "3.2
|
|
3
|
+
"version": "3.3.0-rc.2",
|
|
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": [
|
|
@@ -34,7 +34,8 @@
|
|
|
34
34
|
"@totalreclaw/client": "^1.2.0",
|
|
35
35
|
"@totalreclaw/core": "^2.1.1",
|
|
36
36
|
"@huggingface/transformers": "^4.0.1",
|
|
37
|
-
"onnxruntime-node": "^1.24.0"
|
|
37
|
+
"onnxruntime-node": "^1.24.0",
|
|
38
|
+
"qrcode-terminal": "^0.12.0"
|
|
38
39
|
},
|
|
39
40
|
"files": [
|
|
40
41
|
"*.ts",
|
|
@@ -44,6 +45,7 @@
|
|
|
44
45
|
"openclaw.plugin.json",
|
|
45
46
|
"SKILL.md",
|
|
46
47
|
"README.md",
|
|
48
|
+
"CHANGELOG.md",
|
|
47
49
|
"CLAWHUB.md",
|
|
48
50
|
"skill.json"
|
|
49
51
|
],
|