@totalreclaw/totalreclaw 3.3.12-rc.2 → 3.3.12-rc.21

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.
Files changed (87) hide show
  1. package/CHANGELOG.md +26 -0
  2. package/SKILL.md +34 -249
  3. package/api-client.ts +17 -9
  4. package/batch-gate.ts +42 -0
  5. package/billing-cache.ts +25 -20
  6. package/claims-helper.ts +7 -1
  7. package/config.ts +54 -12
  8. package/consolidation.ts +6 -13
  9. package/contradiction-sync.ts +19 -14
  10. package/credential-provider.ts +184 -0
  11. package/crypto.ts +27 -160
  12. package/dist/api-client.js +17 -9
  13. package/dist/batch-gate.js +40 -0
  14. package/dist/billing-cache.js +19 -21
  15. package/dist/claims-helper.js +7 -1
  16. package/dist/config.js +54 -12
  17. package/dist/consolidation.js +6 -15
  18. package/dist/contradiction-sync.js +15 -15
  19. package/dist/credential-provider.js +145 -0
  20. package/dist/crypto.js +17 -137
  21. package/dist/download-ux.js +11 -7
  22. package/dist/embedder-loader.js +266 -0
  23. package/dist/embedding.js +36 -3
  24. package/dist/entry.js +123 -0
  25. package/dist/extractor.js +134 -0
  26. package/dist/fs-helpers.js +116 -241
  27. package/dist/import-adapters/chatgpt-adapter.js +14 -0
  28. package/dist/import-adapters/claude-adapter.js +14 -0
  29. package/dist/import-adapters/gemini-adapter.js +43 -159
  30. package/dist/import-adapters/mcp-memory-adapter.js +14 -0
  31. package/dist/import-state-manager.js +100 -0
  32. package/dist/index.js +1130 -2520
  33. package/dist/llm-client.js +69 -1
  34. package/dist/memory-runtime.js +459 -0
  35. package/dist/native-memory.js +123 -0
  36. package/dist/onboarding-cli.js +3 -2
  37. package/dist/pair-cli.js +1 -1
  38. package/dist/pair-crypto.js +16 -358
  39. package/dist/pair-http.js +147 -4
  40. package/dist/relay.js +140 -0
  41. package/dist/reranker.js +13 -8
  42. package/dist/semantic-dedup.js +5 -7
  43. package/dist/skill-register.js +97 -0
  44. package/dist/subgraph-search.js +3 -1
  45. package/dist/subgraph-store.js +315 -282
  46. package/dist/tool-gating.js +39 -26
  47. package/dist/tools.js +367 -0
  48. package/dist/tr-cli-export-helper.js +103 -0
  49. package/dist/tr-cli.js +220 -127
  50. package/dist/trajectory-poller.js +155 -9
  51. package/dist/vault-crypto.js +551 -0
  52. package/download-ux.ts +12 -6
  53. package/embedder-loader.ts +293 -1
  54. package/embedding.ts +43 -3
  55. package/entry.ts +132 -0
  56. package/extractor.ts +167 -0
  57. package/fs-helpers.ts +166 -292
  58. package/import-adapters/chatgpt-adapter.ts +18 -0
  59. package/import-adapters/claude-adapter.ts +18 -0
  60. package/import-adapters/gemini-adapter.ts +56 -183
  61. package/import-adapters/mcp-memory-adapter.ts +18 -0
  62. package/import-adapters/types.ts +1 -1
  63. package/import-state-manager.ts +139 -0
  64. package/index.ts +1432 -3002
  65. package/llm-client.ts +74 -1
  66. package/memory-runtime.ts +723 -0
  67. package/native-memory.ts +196 -0
  68. package/onboarding-cli.ts +3 -2
  69. package/openclaw.plugin.json +5 -17
  70. package/package.json +7 -4
  71. package/pair-cli.ts +1 -1
  72. package/pair-crypto.ts +41 -483
  73. package/pair-http.ts +194 -5
  74. package/postinstall.mjs +138 -0
  75. package/relay.ts +172 -0
  76. package/reranker.ts +13 -8
  77. package/semantic-dedup.ts +5 -6
  78. package/skill-register.ts +146 -0
  79. package/skill.json +1 -1
  80. package/subgraph-search.ts +3 -1
  81. package/subgraph-store.ts +334 -299
  82. package/tool-gating.ts +39 -26
  83. package/tools.ts +499 -0
  84. package/tr-cli-export-helper.ts +138 -0
  85. package/tr-cli.ts +263 -133
  86. package/trajectory-poller.ts +162 -10
  87. package/vault-crypto.ts +705 -0
package/tr-cli.ts CHANGED
@@ -1,12 +1,17 @@
1
1
  #!/usr/bin/env node
2
2
  /**
3
- * tr — TotalReclaw hybrid CLI (3.3.9-rc.1 primary architecture)
3
+ * tr — TotalReclaw CLI (explicit-write + curation surface)
4
4
  *
5
- * OpenClaw 2026.5.2 has a tool-policy-pipeline bug (issue #223) that strips non-bundled plugin
6
- * tools before they reach the agent toolset. In 3.3.9-rc.1, this CLI is the PRIMARY path for
7
- * all agent memory operations (not a fallback). The agent runs `tr <cmd> --json` from shell;
8
- * hooks (before_agent_start, agent_end, message_received, before_reset) continue via the
9
- * unbroken hook code path.
5
+ * Scope (Phase 3.3 OpenClaw native integration): recall is now NATIVE.
6
+ * The agent reads memories via OpenClaw's bundled `memory_search` /
7
+ * `memory_get` tools (backed by the MemoryPluginCapability + TrMemorySearchManager
8
+ * adapter registered in `index.ts`). This CLI no longer ships a recall path.
9
+ *
10
+ * What's still CLI-only (no native agent-facing surface):
11
+ * - explicit write (`tr remember`) — the conventional memory contract has no
12
+ * agent-facing write tool; auto-extraction stores facts via hooks.
13
+ * - curation / lifecycle (`tr forget`, `tr export`).
14
+ * - onboarding + pairing (`tr status`, `tr pair`).
10
15
  *
11
16
  * Phrase-safety: this CLI reads credentials.json (mnemonic at rest) but NEVER
12
17
  * prints the mnemonic to stdout, stderr, or any log. Phrase only enters via QR-pair
@@ -15,8 +20,13 @@
15
20
  * Commands:
16
21
  * tr status [--json] — print onboarding + credentials state
17
22
  * tr pair [--json] — start a relay pairing session, print URL+PIN+QR
18
- * tr remember [--json] <text> — store a memory in the encrypted vault
19
- * tr recall [--json] [--limit N] <query> — search the encrypted vault
23
+ * tr remember [--json] <text> — store a memory in the encrypted vault (on-chain)
24
+ * tr forget [--json] <factId> — tombstone a memory on-chain (find the id via memory_search)
25
+ * tr export [--json] [--format json|markdown] — dump all memories from the subgraph
26
+ *
27
+ * 3.3.12-rc.4 — switched remember/forget/export from `/v1/store` and
28
+ * `/v1/search` (those endpoints were removed during the on-chain pivot —
29
+ * relay returns 404) to the on-chain UserOp + subgraph paths.
20
30
  *
21
31
  * --json flag: all agent-facing CLI calls MUST use --json for clean machine-parseable output.
22
32
  * Plain text mode is for direct user CLI use only.
@@ -25,22 +35,28 @@
25
35
  * Usage from container: `docker exec tr-openclaw node ~/.openclaw/extensions/totalreclaw/dist/tr-cli.js status --json`
26
36
  */
27
37
 
28
- import path from 'node:path';
29
- import os from 'node:os';
30
38
  import { randomUUID } from 'node:crypto';
31
39
 
32
- import { CONFIG } from './config.js';
40
+ import { CONFIG, setRecoveryPhraseOverride } from './config.js';
33
41
  import { loadCredentialsJson } from './fs-helpers.js';
34
42
  import { printStatus } from './onboarding-cli.js';
35
43
  import {
36
44
  deriveKeys,
37
45
  computeAuthKeyHash,
38
46
  encrypt,
39
- decrypt,
40
47
  generateBlindIndices,
41
48
  generateContentFingerprint,
42
49
  } from './crypto.js';
43
50
  import { createApiClient } from './api-client.js';
51
+ import {
52
+ encodeFactProtobuf,
53
+ submitFactBatchOnChain,
54
+ deriveSmartAccountAddress,
55
+ getSubgraphConfig,
56
+ PROTOBUF_VERSION_V4,
57
+ type FactPayload,
58
+ } from './subgraph-store.js';
59
+ import { exportAllFacts } from './tr-cli-export-helper.js';
44
60
 
45
61
  // ---------------------------------------------------------------------------
46
62
  // Helpers
@@ -52,7 +68,7 @@ const STATE_PATH = CONFIG.onboardingStatePath;
52
68
  // Auto-synced by skill/scripts/sync-version.mjs from skill/plugin/package.json::version.
53
69
  // Do not edit by hand — running tests will catch drift but the publish workflow
54
70
  // rewrites this constant at the start of every npm/ClawHub publish.
55
- const PLUGIN_VERSION = '3.3.12-rc.2';
71
+ const PLUGIN_VERSION = '3.3.12-rc.21';
56
72
 
57
73
  function die(msg: string, code = 1): never {
58
74
  process.stderr.write(`tr: ${msg}\n`);
@@ -70,13 +86,25 @@ function popFlag(args: string[], flag: string): [boolean, string[]] {
70
86
  return [true, [...args.slice(0, idx), ...args.slice(idx + 1)]];
71
87
  }
72
88
 
73
- /** Parse --limit N from args, returning [limit, cleanedArgs]. Default: defaultLimit. */
74
- function popLimitFlag(args: string[], defaultLimit: number): [number, string[]] {
75
- const idx = args.indexOf('--limit');
76
- if (idx === -1 || idx + 1 >= args.length) return [defaultLimit, args];
77
- const n = parseInt(args[idx + 1], 10);
78
- const limit = isNaN(n) || n < 1 ? defaultLimit : n;
79
- return [limit, [...args.slice(0, idx), ...args.slice(idx + 2)]];
89
+ /** Parse --format VALUE from args, returning [value, cleanedArgs]. */
90
+ function popOptionFlag(
91
+ args: string[],
92
+ flag: string,
93
+ defaultValue: string,
94
+ ): [string, string[]] {
95
+ const idx = args.indexOf(flag);
96
+ if (idx === -1 || idx + 1 >= args.length) return [defaultValue, args];
97
+ return [args[idx + 1], [...args.slice(0, idx), ...args.slice(idx + 2)]];
98
+ }
99
+
100
+ /**
101
+ * Convert XChaCha20-Poly1305 base64 ciphertext to hex (the on-chain blob
102
+ * format). Mirrors `encryptToHex` in index.ts so we don't pull in the whole
103
+ * 7000-line module. Subgraph-stored facts use hex, not base64.
104
+ */
105
+ function toHexBlob(plaintext: string, encryptionKey: Buffer): string {
106
+ const b64 = encrypt(plaintext, encryptionKey);
107
+ return Buffer.from(b64, 'base64').toString('hex');
80
108
  }
81
109
 
82
110
  // ---------------------------------------------------------------------------
@@ -89,6 +117,8 @@ interface CliContext {
89
117
  dedupKey: Buffer;
90
118
  apiClient: ReturnType<typeof createApiClient>;
91
119
  userId: string;
120
+ /** Smart Account address derived from the mnemonic (subgraph owner key). */
121
+ walletAddress: string;
92
122
  }
93
123
 
94
124
  async function buildContext(): Promise<CliContext> {
@@ -106,6 +136,12 @@ async function buildContext(): Promise<CliContext> {
106
136
  die('No recovery phrase in credentials.json. Run: tr pair --json');
107
137
  }
108
138
 
139
+ // Make the mnemonic visible to subgraph-store helpers (getSubgraphConfig
140
+ // reads CONFIG.recoveryPhrase, which falls back to the override). We do
141
+ // NOT log the mnemonic anywhere — it just lives in process memory for the
142
+ // lifetime of this CLI invocation.
143
+ setRecoveryPhraseOverride(mnemonic);
144
+
109
145
  // Parse existing salt/userId from credentials.json
110
146
  let existingSalt: Buffer | undefined;
111
147
  let existingUserId: string | undefined;
@@ -129,7 +165,8 @@ async function buildContext(): Promise<CliContext> {
129
165
  if (existingUserId) {
130
166
  userId = existingUserId;
131
167
  } else {
132
- // Register to get userId (idempotent on relay)
168
+ // Register to get userId (idempotent on relay) — auth key hash is the
169
+ // billing identity even in subgraph mode.
133
170
  const authHash = computeAuthKeyHash(keys.authKey);
134
171
  const saltHex = keys.salt.toString('hex');
135
172
  try {
@@ -145,12 +182,24 @@ async function buildContext(): Promise<CliContext> {
145
182
  }
146
183
  }
147
184
 
185
+ // Derive the Smart Account address. This is the on-chain "owner" for
186
+ // every fact + the X-Wallet-Address header on every UserOp / subgraph
187
+ // call. Cheap eth_call to the SimpleAccountFactory; CREATE2 deterministic.
188
+ let walletAddress: string;
189
+ try {
190
+ walletAddress = await deriveSmartAccountAddress(mnemonic, CONFIG.chainId);
191
+ } catch (err) {
192
+ const msg = err instanceof Error ? err.message : String(err);
193
+ die(`Failed to derive Smart Account address: ${msg}`);
194
+ }
195
+
148
196
  return {
149
197
  authKeyHex,
150
198
  encryptionKey: keys.encryptionKey,
151
199
  dedupKey: keys.dedupKey,
152
200
  apiClient,
153
201
  userId,
202
+ walletAddress,
154
203
  };
155
204
  }
156
205
 
@@ -159,62 +208,27 @@ async function buildContext(): Promise<CliContext> {
159
208
  // ---------------------------------------------------------------------------
160
209
 
161
210
  async function cmdStatus(jsonMode: boolean): Promise<void> {
162
- // Probe plugin manifest for version/hybridMode/toolCount.
163
- let pluginVersion: string | undefined;
164
- let bootCount: number | undefined;
165
- let hybridMode = true; // default true in 3.3.9-rc.1 (hybrid-primary)
166
- let toolCount: number | undefined;
167
- let loadedAgeSec: number | undefined;
168
-
169
- try {
170
- const fs = await import('node:fs');
171
- const candidatePaths = [
172
- path.join(os.homedir(), '.openclaw', 'extensions', 'totalreclaw', '.loaded.json'),
173
- path.join(os.homedir(), '.openclaw', 'npm', 'node_modules', '@totalreclaw', 'totalreclaw', 'dist', '.loaded.json'),
174
- ];
175
- const resolvedPath = candidatePaths.find((p) => fs.existsSync(p));
176
- if (resolvedPath) {
177
- const raw = fs.readFileSync(resolvedPath, 'utf-8');
178
- const manifest = JSON.parse(raw) as {
179
- version?: string;
180
- bootCount?: number;
181
- loadedAt?: number;
182
- hybridMode?: boolean;
183
- tools?: string[];
184
- };
185
- pluginVersion = manifest.version ?? PLUGIN_VERSION;
186
- bootCount = manifest.bootCount;
187
- hybridMode = manifest.hybridMode !== false; // default true
188
- toolCount = manifest.tools?.length;
189
- const ageMs = Date.now() - (manifest.loadedAt ?? 0);
190
- loadedAgeSec = Math.round(ageMs / 1000);
191
- }
192
- } catch {
193
- // Best-effort
194
- }
195
-
196
- // Check onboarding state
211
+ // Phase 3.4 retired the `.loaded.json` manifest the writer was removed
212
+ // in 3.1 and the reader had nothing current to read. `tr status` now
213
+ // reports the static plugin version (from the CLI binary's own
214
+ // PLUGIN_VERSION constant) plus onboarding state. For per-boot history,
215
+ // consult the gateway log.
197
216
  const creds = loadCredentialsJson(CREDENTIALS_PATH);
198
217
  const onboarded = !!creds;
199
218
 
200
219
  if (jsonMode) {
201
220
  // JSON-first output for agent parsing
202
221
  const out: Record<string, unknown> = {
203
- version: pluginVersion ?? PLUGIN_VERSION,
222
+ version: PLUGIN_VERSION,
204
223
  onboarded,
205
224
  next_step: onboarded ? 'none' : 'pair',
206
- tool_count: toolCount ?? 17,
207
- hybrid_mode: hybridMode,
208
225
  };
209
- if (bootCount !== undefined) out.boot_count = bootCount;
210
- if (loadedAgeSec !== undefined) out.loaded_age_sec = loadedAgeSec;
211
226
  log(JSON.stringify(out));
212
227
  } else {
213
228
  // Human-readable plain text for direct user CLI use
214
229
  printStatus(CREDENTIALS_PATH, STATE_PATH, process.stdout);
215
230
  process.stdout.write(
216
- `\n plugin: ${pluginVersion ? `loaded (version=${pluginVersion}` + (bootCount !== undefined ? ` bootCount=${bootCount}` : '') + (loadedAgeSec !== undefined ? ` loaded=${loadedAgeSec}s ago` : '') + ')' : 'not found in .loaded.json'}\n` +
217
- ` hybrid-mode: ${hybridMode ? 'yes (primary — use tr <cmd> --json)' : 'no'}\n` +
231
+ `\n plugin: loaded (version=${PLUGIN_VERSION})\n` +
218
232
  ` hooks: before_agent_start, agent_end, message_received, before_reset\n`,
219
233
  );
220
234
  }
@@ -271,11 +285,10 @@ async function cmdRemember(rawArgs: string[]): Promise<void> {
271
285
 
272
286
  const ctx = await buildContext();
273
287
 
274
- // Build a minimal MemoryTaxonomy v1 claim blob (same format as storeExtractedFacts)
288
+ // Build a Memory Taxonomy v1 claim blob (matches storeExtractedFacts shape).
275
289
  const now = new Date().toISOString();
276
- const factId = randomUUID().replace(/-/g, '');
290
+ const factId = randomUUID();
277
291
 
278
- // Encrypt the memory text
279
292
  const blob = JSON.stringify({
280
293
  text,
281
294
  type: 'claim',
@@ -291,28 +304,53 @@ async function cmdRemember(rawArgs: string[]): Promise<void> {
291
304
  timestamp: now,
292
305
  version: 'v1',
293
306
  });
294
- const encrypted_blob = encrypt(blob, ctx.encryptionKey);
295
- const blind_indices = generateBlindIndices(text);
296
- const content_fp = generateContentFingerprint(text, ctx.dedupKey);
297
307
 
298
- const payload = {
308
+ const encryptedBlob = toHexBlob(blob, ctx.encryptionKey);
309
+ const blindIndices = generateBlindIndices(text);
310
+ const contentFp = generateContentFingerprint(text, ctx.dedupKey);
311
+
312
+ // On-chain submission: encode protobuf, build SubgraphStoreConfig (auth +
313
+ // wallet), submit a single-fact UserOp through the relay bundler. The
314
+ // subgraph indexes the resulting Log(bytes) event so it is recall-able
315
+ // within ~5-15 s of the receipt.
316
+ const fact: FactPayload = {
299
317
  id: factId,
300
318
  timestamp: now,
301
- encrypted_blob,
302
- blind_indices,
303
- decay_score: 8,
319
+ owner: ctx.walletAddress,
320
+ encryptedBlob,
321
+ blindIndices,
322
+ decayScore: 8,
304
323
  source: 'cli:tr-remember',
305
- content_fp,
324
+ contentFp,
325
+ agentId: 'tr-cli',
326
+ version: PROTOBUF_VERSION_V4,
306
327
  };
307
328
 
308
329
  try {
309
- await ctx.apiClient.store(ctx.userId, [payload], ctx.authKeyHex);
330
+ const protobuf = encodeFactProtobuf(fact);
331
+ const config = {
332
+ ...getSubgraphConfig(),
333
+ authKeyHex: ctx.authKeyHex,
334
+ walletAddress: ctx.walletAddress,
335
+ };
336
+ const result = await submitFactBatchOnChain([protobuf], config);
337
+
338
+ if (!result.success) {
339
+ die(
340
+ `remember failed: on-chain UserOp did not succeed (userOpHash=${
341
+ result.userOpHash || 'none'
342
+ })`,
343
+ );
344
+ }
345
+
310
346
  if (jsonMode) {
311
- // JSON-first output for agent parsing
312
- // claim_count requires an extra relay call to tally stored claims; not worth the latency — use 0
313
- log(JSON.stringify({ ok: true, id: factId, claim_count: 0 }));
347
+ // JSON-first output for agent parsing.
348
+ // claim_count = 1 here (single fact stored). Computing the full vault
349
+ // count would require an extra subgraph query on every remember and
350
+ // isn't worth the latency.
351
+ log(JSON.stringify({ ok: true, id: factId, claim_count: 1 }));
314
352
  } else {
315
- log(`ok — stored memory (id=${factId})`);
353
+ log(`ok — stored memory (id=${factId}, tx=${result.txHash || 'pending'})`);
316
354
  }
317
355
  } catch (err) {
318
356
  const msg = err instanceof Error ? err.message : String(err);
@@ -321,67 +359,115 @@ async function cmdRemember(rawArgs: string[]): Promise<void> {
321
359
  }
322
360
 
323
361
  // ---------------------------------------------------------------------------
324
- // Command: recall
362
+ // Command: forget
325
363
  // ---------------------------------------------------------------------------
326
364
 
327
- async function cmdRecall(rawArgs: string[]): Promise<void> {
328
- const [jsonMode, argsAfterJson] = popFlag(rawArgs, '--json');
329
- const [limit, argsAfterLimit] = popLimitFlag(argsAfterJson, 5);
330
- const query = argsAfterLimit.join(' ').trim();
331
- if (!query) {
332
- die('Usage: tr recall [--json] [--limit N] <query>');
365
+ async function cmdForget(rawArgs: string[]): Promise<void> {
366
+ const [jsonMode, args] = popFlag(rawArgs, '--json');
367
+ const factId = (args[0] ?? '').trim();
368
+ if (!factId) {
369
+ die('Usage: tr forget [--json] <factId>');
370
+ }
371
+ // UUID-v4-ish shape check — same validation the old totalreclaw_forget
372
+ // tool applied. Prevents fabricated / natural-language IDs from reaching
373
+ // the UserOp path and silently no-op'ing on-chain.
374
+ if (!/^[0-9a-f-]{8,}$/i.test(factId)) {
375
+ die(
376
+ `forget failed: "${factId.slice(0, 60)}" doesn't look like a memory ID. ` +
377
+ `Ask the agent to look it up via memory_search (or tr export) and pass a result's id.`,
378
+ );
333
379
  }
334
380
 
335
381
  const ctx = await buildContext();
336
382
 
337
- // Generate word trapdoors for blind search
338
- const trapdoors = generateBlindIndices(query);
383
+ // Tombstone shape (pin/unpin & native forget use the same one — see
384
+ // index.ts:4253-4267 + pin.ts:611-621). Deliberately NO version field
385
+ // → uses legacy v3 default so the subgraph's contradiction handler
386
+ // matches and flips isActive=false.
387
+ const tombstone: FactPayload = {
388
+ id: factId,
389
+ timestamp: new Date().toISOString(),
390
+ owner: ctx.walletAddress,
391
+ encryptedBlob: '00',
392
+ blindIndices: [],
393
+ decayScore: 0,
394
+ source: 'tombstone',
395
+ contentFp: '',
396
+ agentId: 'tr-cli',
397
+ // No `version` → legacy v3 (matches pin/unpin & native forget).
398
+ };
399
+
400
+ try {
401
+ const protobuf = encodeFactProtobuf(tombstone);
402
+ const config = {
403
+ ...getSubgraphConfig(),
404
+ authKeyHex: ctx.authKeyHex,
405
+ walletAddress: ctx.walletAddress,
406
+ };
407
+ const result = await submitFactBatchOnChain([protobuf], config);
408
+
409
+ if (!result.success) {
410
+ die(
411
+ `forget failed: on-chain tombstone did not succeed (userOpHash=${
412
+ result.userOpHash || 'none'
413
+ })`,
414
+ );
415
+ }
339
416
 
340
- if (trapdoors.length === 0) {
341
417
  if (jsonMode) {
342
- log(JSON.stringify({ results: [] }));
418
+ log(JSON.stringify({ ok: true, id: factId, tx_hash: result.txHash }));
343
419
  } else {
344
- log('No results (0 searchable terms in query).');
420
+ log(`ok tombstoned ${factId} (tx=${result.txHash || 'pending'})`);
345
421
  }
346
- return;
422
+ } catch (err) {
423
+ const msg = err instanceof Error ? err.message : String(err);
424
+ die(`forget failed: ${msg}`);
347
425
  }
426
+ }
348
427
 
349
- try {
350
- const candidates = await ctx.apiClient.search(ctx.userId, trapdoors, Math.min(limit * 2, 20), ctx.authKeyHex);
351
-
352
- const results: Array<{ text: string; score: number }> = [];
353
-
354
- for (const c of candidates) {
355
- try {
356
- const raw = decrypt(c.encrypted_blob, ctx.encryptionKey);
357
- const parsed = JSON.parse(raw) as { text?: string };
358
- if (parsed.text) {
359
- results.push({
360
- text: parsed.text,
361
- score: c.decay_score,
362
- });
363
- }
364
- } catch {
365
- // Skip undecryptable
366
- }
367
- }
428
+ // ---------------------------------------------------------------------------
429
+ // Command: export
430
+ // ---------------------------------------------------------------------------
368
431
 
369
- // Sort by score descending, then trim to limit
370
- results.sort((a, b) => b.score - a.score);
371
- const trimmed = results.slice(0, limit);
432
+ async function cmdExport(rawArgs: string[]): Promise<void> {
433
+ const [jsonMode, argsAfterJson] = popFlag(rawArgs, '--json');
434
+ const [format, _argsAfterFormat] = popOptionFlag(argsAfterJson, '--format', 'json');
372
435
 
373
- if (jsonMode) {
374
- // JSON-first output for agent parsing — canonical format per spec
375
- log(JSON.stringify({ results: trimmed }));
436
+ if (format !== 'json' && format !== 'markdown') {
437
+ die('Usage: tr export [--json] [--format json|markdown]');
438
+ }
439
+
440
+ const ctx = await buildContext();
441
+
442
+ // Delegate the subgraph paginate + decrypt loop to a helper module —
443
+ // tr-cli.ts already includes `fs.readFileSync` (status command), and
444
+ // adding outbound HTTP here would trip the OpenClaw scanner's
445
+ // potential-exfiltration rule. See tr-cli-export-helper.ts.
446
+ const allFacts = await exportAllFacts(
447
+ ctx.walletAddress,
448
+ ctx.authKeyHex,
449
+ ctx.encryptionKey,
450
+ );
451
+
452
+ if (format === 'markdown') {
453
+ if (allFacts.length === 0) {
454
+ log('*No memories stored.*');
376
455
  } else {
377
- log(`Found ${trimmed.length} result(s) for: ${query}`);
378
- for (const r of trimmed) {
379
- log(` [score=${r.score.toFixed(2)}] ${r.text}`);
380
- }
456
+ const lines = allFacts.map((f, i) => {
457
+ const meta = f.metadata;
458
+ const type = (meta.type as string) ?? 'fact';
459
+ return `${i + 1}. **[${type}]** ${f.text} \n _ID: ${f.id} | Created: ${f.created_at}_`;
460
+ });
461
+ log(`# Exported Memories (${allFacts.length})\n\n${lines.join('\n')}`);
381
462
  }
382
- } catch (err) {
383
- const msg = err instanceof Error ? err.message : String(err);
384
- die(`recall failed: ${msg}`);
463
+ return;
464
+ }
465
+
466
+ // json format (default — both --json mode and --format=json end up here)
467
+ if (jsonMode) {
468
+ log(JSON.stringify({ count: allFacts.length, facts: allFacts }));
469
+ } else {
470
+ log(JSON.stringify(allFacts, null, 2));
385
471
  }
386
472
  }
387
473
 
@@ -409,27 +495,71 @@ async function main(): Promise<void> {
409
495
  break;
410
496
 
411
497
  case 'recall':
412
- await cmdRecall(args.slice(1));
498
+ // Retired in Phase 3.3 — recall is now native via the bundled
499
+ // memory_search / memory_get tools (MemoryPluginCapability). Surface
500
+ // a clear pointer instead of falling through to "unknown command"
501
+ // so agents / users running stale prompts get actionable guidance.
502
+ die(
503
+ 'tr recall was retired — recall is now native. ' +
504
+ 'The agent reads memories via the memory_search tool automatically; ' +
505
+ 'use `tr export` to dump every memory outside the agent.',
506
+ );
507
+
508
+ case 'forget':
509
+ await cmdForget(args.slice(1));
413
510
  break;
414
511
 
512
+ case 'export':
513
+ await cmdExport(args.slice(1));
514
+ break;
515
+
516
+ case 'import':
517
+ case 'upgrade':
518
+ // Import + upgrade run inside the gateway process (they need the
519
+ // plugin runtime — extraction pipeline, smart-import WASM, module-
520
+ // level auth state). The standalone `tr` binary is a separate Node
521
+ // script that does NOT load index.ts (would pull in the entire
522
+ // plugin runtime). Point users at the registerCli surface instead
523
+ // of silently no-op'ing or falling through to "unknown command".
524
+ die(
525
+ `${cmd} is not available on the standalone \`tr\` binary. ` +
526
+ `Run it on the gateway host via:\n` +
527
+ ` openclaw totalreclaw ${cmd === 'import' ? 'import from <source>' : 'upgrade'}${cmd === 'import' ? ' [--file <path>] [--dry-run] [--json]' : ' [--json]'}\n` +
528
+ (cmd === 'import'
529
+ ? ` openclaw totalreclaw import status [--id <importId>] [--json]\n` +
530
+ ` openclaw totalreclaw import abort <importId> [--json]\n` +
531
+ `Sources: mem0 | mcp-memory | chatgpt | claude | gemini`
532
+ : `Returns a Stripe checkout URL for Pro upgrade.`),
533
+ );
534
+
415
535
  case undefined:
416
536
  case '--help':
417
537
  case '-h':
418
538
  process.stdout.write(
419
- `TotalReclaw hybrid CLI v${PLUGIN_VERSION} (primary modeOpenClaw 2026.5.2+)\n\n` +
539
+ `TotalReclaw CLI v${PLUGIN_VERSION} (recall is native memory_search tool)\n\n` +
420
540
  'Usage:\n' +
421
- ' tr status [--json] — onboarding + plugin load state\n' +
422
- ' tr pair [--json] — start a relay pairing session\n' +
423
- ' tr remember [--json] <text> — store a memory\n' +
424
- ' tr recall [--json] [--limit N] <query> search memories (default limit: 5)\n\n' +
541
+ ' tr status [--json] — onboarding + plugin load state\n' +
542
+ ' tr pair [--json] — start a relay pairing session\n' +
543
+ ' tr remember [--json] <text> — store a memory (on-chain UserOp)\n' +
544
+ ' tr forget [--json] <factId> tombstone a memory on-chain\n' +
545
+ ' tr export [--json] [--format json|markdown] — dump every memory in the vault\n\n' +
546
+ 'Recall: NOT a CLI command. The agent recalls via the bundled memory_search tool.\n' +
547
+ ' To dump memories outside the agent, use `tr export`.\n\n' +
548
+ 'Import + Upgrade: NOT on the standalone `tr` binary. They run inside the gateway\n' +
549
+ ' process via the `openclaw totalreclaw` subcommand chain:\n' +
550
+ ' openclaw totalreclaw import from <source> [--file <path>] [--dry-run] [--json]\n' +
551
+ ' openclaw totalreclaw import status [--id <importId>] [--json]\n' +
552
+ ' openclaw totalreclaw import abort <importId> [--json]\n' +
553
+ ' openclaw totalreclaw upgrade [--json]\n' +
554
+ ' Sources: mem0 | mcp-memory | chatgpt | claude | gemini\n\n' +
425
555
  'Flags:\n' +
426
- ' --json Output machine-parseable JSON (required for agent shell calls)\n' +
427
- ' --limit N Limit recall results (default: 5)\n\n' +
556
+ ' --json Output machine-parseable JSON (required for agent shell calls)\n\n' +
428
557
  'JSON output shapes:\n' +
429
558
  ' status: {"version":"...","onboarded":bool,"next_step":"pair|none","tool_count":N,"hybrid_mode":bool}\n' +
430
559
  ' pair: {"url":"...","pin":"123456","expires_at":"..."}\n' +
431
560
  ' remember: {"ok":true,"id":"...","claim_count":N}\n' +
432
- ' recall: {"results":[{"text":"...","score":0.8}]}\n\n' +
561
+ ' forget: {"ok":true,"id":"...","tx_hash":"0x..."}\n' +
562
+ ' export: {"count":N,"facts":[{"id":"...","text":"...","metadata":{...},"created_at":"..."}]}\n\n' +
433
563
  'Environment:\n' +
434
564
  ' TOTALRECLAW_SERVER_URL — relay URL (default: api.totalreclaw.xyz; staging: api-staging.totalreclaw.xyz)\n' +
435
565
  ' TOTALRECLAW_CREDENTIALS_PATH — override credentials.json path\n',