@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/README.md CHANGED
@@ -96,7 +96,36 @@ openclaw plugins install @totalreclaw/totalreclaw
96
96
 
97
97
  ### 2. Configure
98
98
 
99
- Set one environment variable:
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 deployments:
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 BIP-39 mnemonic.\n' +
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 sub = (ctx.args || '').trim().split(/\s+/)[0]?.toLowerCase() || 'help';
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, open a terminal on this machine and run:\n\n' +
2787
+ 'To set up TotalReclaw on a local machine, run:\n\n' +
2591
2788
  ' openclaw totalreclaw onboard\n\n' +
2592
- 'Why a terminal? Your recovery phrase must never pass through the ' +
2593
- 'LLM provider. This chat message is visible to the LLM, so we do ' +
2594
- 'not show the phrase here. The wizard runs entirely on your local ' +
2595
- 'machine the phrase is generated, displayed, and stored on disk ' +
2596
- '(mode 0600) without ever touching the network.',
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` in a terminal to complete setup.'),
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 BIP-39 phrase (12 words required, checksum must match).\n' +
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: mnemonic generator returned an invalid phrase.\n');
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",
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
  ],