@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/dist/tr-cli.js 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.
@@ -24,14 +34,14 @@
24
34
  * Install: wired via package.json `bin.tr` → dist/tr-cli.js
25
35
  * Usage from container: `docker exec tr-openclaw node ~/.openclaw/extensions/totalreclaw/dist/tr-cli.js status --json`
26
36
  */
27
- import path from 'node:path';
28
- import os from 'node:os';
29
37
  import { randomUUID } from 'node:crypto';
30
- import { CONFIG } from './config.js';
38
+ import { CONFIG, setRecoveryPhraseOverride } from './config.js';
31
39
  import { loadCredentialsJson } from './fs-helpers.js';
32
40
  import { printStatus } from './onboarding-cli.js';
33
- import { deriveKeys, computeAuthKeyHash, encrypt, decrypt, generateBlindIndices, generateContentFingerprint, } from './crypto.js';
41
+ import { deriveKeys, computeAuthKeyHash, encrypt, generateBlindIndices, generateContentFingerprint, } from './crypto.js';
34
42
  import { createApiClient } from './api-client.js';
43
+ import { encodeFactProtobuf, submitFactBatchOnChain, deriveSmartAccountAddress, getSubgraphConfig, PROTOBUF_VERSION_V4, } from './subgraph-store.js';
44
+ import { exportAllFacts } from './tr-cli-export-helper.js';
35
45
  // ---------------------------------------------------------------------------
36
46
  // Helpers
37
47
  // ---------------------------------------------------------------------------
@@ -41,7 +51,7 @@ const STATE_PATH = CONFIG.onboardingStatePath;
41
51
  // Auto-synced by skill/scripts/sync-version.mjs from skill/plugin/package.json::version.
42
52
  // Do not edit by hand — running tests will catch drift but the publish workflow
43
53
  // rewrites this constant at the start of every npm/ClawHub publish.
44
- const PLUGIN_VERSION = '3.3.12-rc.2';
54
+ const PLUGIN_VERSION = '3.3.12-rc.21';
45
55
  function die(msg, code = 1) {
46
56
  process.stderr.write(`tr: ${msg}\n`);
47
57
  process.exit(code);
@@ -56,14 +66,21 @@ function popFlag(args, flag) {
56
66
  return [false, args];
57
67
  return [true, [...args.slice(0, idx), ...args.slice(idx + 1)]];
58
68
  }
59
- /** Parse --limit N from args, returning [limit, cleanedArgs]. Default: defaultLimit. */
60
- function popLimitFlag(args, defaultLimit) {
61
- const idx = args.indexOf('--limit');
69
+ /** Parse --format VALUE from args, returning [value, cleanedArgs]. */
70
+ function popOptionFlag(args, flag, defaultValue) {
71
+ const idx = args.indexOf(flag);
62
72
  if (idx === -1 || idx + 1 >= args.length)
63
- return [defaultLimit, args];
64
- const n = parseInt(args[idx + 1], 10);
65
- const limit = isNaN(n) || n < 1 ? defaultLimit : n;
66
- return [limit, [...args.slice(0, idx), ...args.slice(idx + 2)]];
73
+ return [defaultValue, args];
74
+ return [args[idx + 1], [...args.slice(0, idx), ...args.slice(idx + 2)]];
75
+ }
76
+ /**
77
+ * Convert XChaCha20-Poly1305 base64 ciphertext to hex (the on-chain blob
78
+ * format). Mirrors `encryptToHex` in index.ts so we don't pull in the whole
79
+ * 7000-line module. Subgraph-stored facts use hex, not base64.
80
+ */
81
+ function toHexBlob(plaintext, encryptionKey) {
82
+ const b64 = encrypt(plaintext, encryptionKey);
83
+ return Buffer.from(b64, 'base64').toString('hex');
67
84
  }
68
85
  async function buildContext() {
69
86
  const creds = loadCredentialsJson(CREDENTIALS_PATH);
@@ -76,6 +93,11 @@ async function buildContext() {
76
93
  if (!mnemonic) {
77
94
  die('No recovery phrase in credentials.json. Run: tr pair --json');
78
95
  }
96
+ // Make the mnemonic visible to subgraph-store helpers (getSubgraphConfig
97
+ // reads CONFIG.recoveryPhrase, which falls back to the override). We do
98
+ // NOT log the mnemonic anywhere — it just lives in process memory for the
99
+ // lifetime of this CLI invocation.
100
+ setRecoveryPhraseOverride(mnemonic);
79
101
  // Parse existing salt/userId from credentials.json
80
102
  let existingSalt;
81
103
  let existingUserId;
@@ -97,7 +119,8 @@ async function buildContext() {
97
119
  userId = existingUserId;
98
120
  }
99
121
  else {
100
- // Register to get userId (idempotent on relay)
122
+ // Register to get userId (idempotent on relay) — auth key hash is the
123
+ // billing identity even in subgraph mode.
101
124
  const authHash = computeAuthKeyHash(keys.authKey);
102
125
  const saltHex = keys.salt.toString('hex');
103
126
  try {
@@ -114,68 +137,50 @@ async function buildContext() {
114
137
  }
115
138
  }
116
139
  }
140
+ // Derive the Smart Account address. This is the on-chain "owner" for
141
+ // every fact + the X-Wallet-Address header on every UserOp / subgraph
142
+ // call. Cheap eth_call to the SimpleAccountFactory; CREATE2 deterministic.
143
+ let walletAddress;
144
+ try {
145
+ walletAddress = await deriveSmartAccountAddress(mnemonic, CONFIG.chainId);
146
+ }
147
+ catch (err) {
148
+ const msg = err instanceof Error ? err.message : String(err);
149
+ die(`Failed to derive Smart Account address: ${msg}`);
150
+ }
117
151
  return {
118
152
  authKeyHex,
119
153
  encryptionKey: keys.encryptionKey,
120
154
  dedupKey: keys.dedupKey,
121
155
  apiClient,
122
156
  userId,
157
+ walletAddress,
123
158
  };
124
159
  }
125
160
  // ---------------------------------------------------------------------------
126
161
  // Command: status
127
162
  // ---------------------------------------------------------------------------
128
163
  async function cmdStatus(jsonMode) {
129
- // Probe plugin manifest for version/hybridMode/toolCount.
130
- let pluginVersion;
131
- let bootCount;
132
- let hybridMode = true; // default true in 3.3.9-rc.1 (hybrid-primary)
133
- let toolCount;
134
- let loadedAgeSec;
135
- try {
136
- const fs = await import('node:fs');
137
- const candidatePaths = [
138
- path.join(os.homedir(), '.openclaw', 'extensions', 'totalreclaw', '.loaded.json'),
139
- path.join(os.homedir(), '.openclaw', 'npm', 'node_modules', '@totalreclaw', 'totalreclaw', 'dist', '.loaded.json'),
140
- ];
141
- const resolvedPath = candidatePaths.find((p) => fs.existsSync(p));
142
- if (resolvedPath) {
143
- const raw = fs.readFileSync(resolvedPath, 'utf-8');
144
- const manifest = JSON.parse(raw);
145
- pluginVersion = manifest.version ?? PLUGIN_VERSION;
146
- bootCount = manifest.bootCount;
147
- hybridMode = manifest.hybridMode !== false; // default true
148
- toolCount = manifest.tools?.length;
149
- const ageMs = Date.now() - (manifest.loadedAt ?? 0);
150
- loadedAgeSec = Math.round(ageMs / 1000);
151
- }
152
- }
153
- catch {
154
- // Best-effort
155
- }
156
- // Check onboarding state
164
+ // Phase 3.4 retired the `.loaded.json` manifest the writer was removed
165
+ // in 3.1 and the reader had nothing current to read. `tr status` now
166
+ // reports the static plugin version (from the CLI binary's own
167
+ // PLUGIN_VERSION constant) plus onboarding state. For per-boot history,
168
+ // consult the gateway log.
157
169
  const creds = loadCredentialsJson(CREDENTIALS_PATH);
158
170
  const onboarded = !!creds;
159
171
  if (jsonMode) {
160
172
  // JSON-first output for agent parsing
161
173
  const out = {
162
- version: pluginVersion ?? PLUGIN_VERSION,
174
+ version: PLUGIN_VERSION,
163
175
  onboarded,
164
176
  next_step: onboarded ? 'none' : 'pair',
165
- tool_count: toolCount ?? 17,
166
- hybrid_mode: hybridMode,
167
177
  };
168
- if (bootCount !== undefined)
169
- out.boot_count = bootCount;
170
- if (loadedAgeSec !== undefined)
171
- out.loaded_age_sec = loadedAgeSec;
172
178
  log(JSON.stringify(out));
173
179
  }
174
180
  else {
175
181
  // Human-readable plain text for direct user CLI use
176
182
  printStatus(CREDENTIALS_PATH, STATE_PATH, process.stdout);
177
- process.stdout.write(`\n plugin: ${pluginVersion ? `loaded (version=${pluginVersion}` + (bootCount !== undefined ? ` bootCount=${bootCount}` : '') + (loadedAgeSec !== undefined ? ` loaded=${loadedAgeSec}s ago` : '') + ')' : 'not found in .loaded.json'}\n` +
178
- ` hybrid-mode: ${hybridMode ? 'yes (primary — use tr <cmd> --json)' : 'no'}\n` +
183
+ process.stdout.write(`\n plugin: loaded (version=${PLUGIN_VERSION})\n` +
179
184
  ` hooks: before_agent_start, agent_end, message_received, before_reset\n`);
180
185
  }
181
186
  }
@@ -222,10 +227,9 @@ async function cmdRemember(rawArgs) {
222
227
  die('Usage: tr remember [--json] <text>');
223
228
  }
224
229
  const ctx = await buildContext();
225
- // Build a minimal MemoryTaxonomy v1 claim blob (same format as storeExtractedFacts)
230
+ // Build a Memory Taxonomy v1 claim blob (matches storeExtractedFacts shape).
226
231
  const now = new Date().toISOString();
227
- const factId = randomUUID().replace(/-/g, '');
228
- // Encrypt the memory text
232
+ const factId = randomUUID();
229
233
  const blob = JSON.stringify({
230
234
  text,
231
235
  type: 'claim',
@@ -241,27 +245,45 @@ async function cmdRemember(rawArgs) {
241
245
  timestamp: now,
242
246
  version: 'v1',
243
247
  });
244
- const encrypted_blob = encrypt(blob, ctx.encryptionKey);
245
- const blind_indices = generateBlindIndices(text);
246
- const content_fp = generateContentFingerprint(text, ctx.dedupKey);
247
- const payload = {
248
+ const encryptedBlob = toHexBlob(blob, ctx.encryptionKey);
249
+ const blindIndices = generateBlindIndices(text);
250
+ const contentFp = generateContentFingerprint(text, ctx.dedupKey);
251
+ // On-chain submission: encode protobuf, build SubgraphStoreConfig (auth +
252
+ // wallet), submit a single-fact UserOp through the relay bundler. The
253
+ // subgraph indexes the resulting Log(bytes) event so it is recall-able
254
+ // within ~5-15 s of the receipt.
255
+ const fact = {
248
256
  id: factId,
249
257
  timestamp: now,
250
- encrypted_blob,
251
- blind_indices,
252
- decay_score: 8,
258
+ owner: ctx.walletAddress,
259
+ encryptedBlob,
260
+ blindIndices,
261
+ decayScore: 8,
253
262
  source: 'cli:tr-remember',
254
- content_fp,
263
+ contentFp,
264
+ agentId: 'tr-cli',
265
+ version: PROTOBUF_VERSION_V4,
255
266
  };
256
267
  try {
257
- await ctx.apiClient.store(ctx.userId, [payload], ctx.authKeyHex);
268
+ const protobuf = encodeFactProtobuf(fact);
269
+ const config = {
270
+ ...getSubgraphConfig(),
271
+ authKeyHex: ctx.authKeyHex,
272
+ walletAddress: ctx.walletAddress,
273
+ };
274
+ const result = await submitFactBatchOnChain([protobuf], config);
275
+ if (!result.success) {
276
+ die(`remember failed: on-chain UserOp did not succeed (userOpHash=${result.userOpHash || 'none'})`);
277
+ }
258
278
  if (jsonMode) {
259
- // JSON-first output for agent parsing
260
- // claim_count requires an extra relay call to tally stored claims; not worth the latency — use 0
261
- log(JSON.stringify({ ok: true, id: factId, claim_count: 0 }));
279
+ // JSON-first output for agent parsing.
280
+ // claim_count = 1 here (single fact stored). Computing the full vault
281
+ // count would require an extra subgraph query on every remember and
282
+ // isn't worth the latency.
283
+ log(JSON.stringify({ ok: true, id: factId, claim_count: 1 }));
262
284
  }
263
285
  else {
264
- log(`ok — stored memory (id=${factId})`);
286
+ log(`ok — stored memory (id=${factId}, tx=${result.txHash || 'pending'})`);
265
287
  }
266
288
  }
267
289
  catch (err) {
@@ -270,62 +292,96 @@ async function cmdRemember(rawArgs) {
270
292
  }
271
293
  }
272
294
  // ---------------------------------------------------------------------------
273
- // Command: recall
295
+ // Command: forget
274
296
  // ---------------------------------------------------------------------------
275
- async function cmdRecall(rawArgs) {
276
- const [jsonMode, argsAfterJson] = popFlag(rawArgs, '--json');
277
- const [limit, argsAfterLimit] = popLimitFlag(argsAfterJson, 5);
278
- const query = argsAfterLimit.join(' ').trim();
279
- if (!query) {
280
- die('Usage: tr recall [--json] [--limit N] <query>');
297
+ async function cmdForget(rawArgs) {
298
+ const [jsonMode, args] = popFlag(rawArgs, '--json');
299
+ const factId = (args[0] ?? '').trim();
300
+ if (!factId) {
301
+ die('Usage: tr forget [--json] <factId>');
281
302
  }
282
- const ctx = await buildContext();
283
- // Generate word trapdoors for blind search
284
- const trapdoors = generateBlindIndices(query);
285
- if (trapdoors.length === 0) {
286
- if (jsonMode) {
287
- log(JSON.stringify({ results: [] }));
288
- }
289
- else {
290
- log('No results (0 searchable terms in query).');
291
- }
292
- return;
303
+ // UUID-v4-ish shape check — same validation the old totalreclaw_forget
304
+ // tool applied. Prevents fabricated / natural-language IDs from reaching
305
+ // the UserOp path and silently no-op'ing on-chain.
306
+ if (!/^[0-9a-f-]{8,}$/i.test(factId)) {
307
+ die(`forget failed: "${factId.slice(0, 60)}" doesn't look like a memory ID. ` +
308
+ `Ask the agent to look it up via memory_search (or tr export) and pass a result's id.`);
293
309
  }
310
+ const ctx = await buildContext();
311
+ // Tombstone shape (pin/unpin & native forget use the same one — see
312
+ // index.ts:4253-4267 + pin.ts:611-621). Deliberately NO version field
313
+ // → uses legacy v3 default so the subgraph's contradiction handler
314
+ // matches and flips isActive=false.
315
+ const tombstone = {
316
+ id: factId,
317
+ timestamp: new Date().toISOString(),
318
+ owner: ctx.walletAddress,
319
+ encryptedBlob: '00',
320
+ blindIndices: [],
321
+ decayScore: 0,
322
+ source: 'tombstone',
323
+ contentFp: '',
324
+ agentId: 'tr-cli',
325
+ // No `version` → legacy v3 (matches pin/unpin & native forget).
326
+ };
294
327
  try {
295
- const candidates = await ctx.apiClient.search(ctx.userId, trapdoors, Math.min(limit * 2, 20), ctx.authKeyHex);
296
- const results = [];
297
- for (const c of candidates) {
298
- try {
299
- const raw = decrypt(c.encrypted_blob, ctx.encryptionKey);
300
- const parsed = JSON.parse(raw);
301
- if (parsed.text) {
302
- results.push({
303
- text: parsed.text,
304
- score: c.decay_score,
305
- });
306
- }
307
- }
308
- catch {
309
- // Skip undecryptable
310
- }
328
+ const protobuf = encodeFactProtobuf(tombstone);
329
+ const config = {
330
+ ...getSubgraphConfig(),
331
+ authKeyHex: ctx.authKeyHex,
332
+ walletAddress: ctx.walletAddress,
333
+ };
334
+ const result = await submitFactBatchOnChain([protobuf], config);
335
+ if (!result.success) {
336
+ die(`forget failed: on-chain tombstone did not succeed (userOpHash=${result.userOpHash || 'none'})`);
311
337
  }
312
- // Sort by score descending, then trim to limit
313
- results.sort((a, b) => b.score - a.score);
314
- const trimmed = results.slice(0, limit);
315
338
  if (jsonMode) {
316
- // JSON-first output for agent parsing canonical format per spec
317
- log(JSON.stringify({ results: trimmed }));
339
+ log(JSON.stringify({ ok: true, id: factId, tx_hash: result.txHash }));
318
340
  }
319
341
  else {
320
- log(`Found ${trimmed.length} result(s) for: ${query}`);
321
- for (const r of trimmed) {
322
- log(` [score=${r.score.toFixed(2)}] ${r.text}`);
323
- }
342
+ log(`ok — tombstoned ${factId} (tx=${result.txHash || 'pending'})`);
324
343
  }
325
344
  }
326
345
  catch (err) {
327
346
  const msg = err instanceof Error ? err.message : String(err);
328
- die(`recall failed: ${msg}`);
347
+ die(`forget failed: ${msg}`);
348
+ }
349
+ }
350
+ // ---------------------------------------------------------------------------
351
+ // Command: export
352
+ // ---------------------------------------------------------------------------
353
+ async function cmdExport(rawArgs) {
354
+ const [jsonMode, argsAfterJson] = popFlag(rawArgs, '--json');
355
+ const [format, _argsAfterFormat] = popOptionFlag(argsAfterJson, '--format', 'json');
356
+ if (format !== 'json' && format !== 'markdown') {
357
+ die('Usage: tr export [--json] [--format json|markdown]');
358
+ }
359
+ const ctx = await buildContext();
360
+ // Delegate the subgraph paginate + decrypt loop to a helper module —
361
+ // tr-cli.ts already includes `fs.readFileSync` (status command), and
362
+ // adding outbound HTTP here would trip the OpenClaw scanner's
363
+ // potential-exfiltration rule. See tr-cli-export-helper.ts.
364
+ const allFacts = await exportAllFacts(ctx.walletAddress, ctx.authKeyHex, ctx.encryptionKey);
365
+ if (format === 'markdown') {
366
+ if (allFacts.length === 0) {
367
+ log('*No memories stored.*');
368
+ }
369
+ else {
370
+ const lines = allFacts.map((f, i) => {
371
+ const meta = f.metadata;
372
+ const type = meta.type ?? 'fact';
373
+ return `${i + 1}. **[${type}]** ${f.text} \n _ID: ${f.id} | Created: ${f.created_at}_`;
374
+ });
375
+ log(`# Exported Memories (${allFacts.length})\n\n${lines.join('\n')}`);
376
+ }
377
+ return;
378
+ }
379
+ // json format (default — both --json mode and --format=json end up here)
380
+ if (jsonMode) {
381
+ log(JSON.stringify({ count: allFacts.length, facts: allFacts }));
382
+ }
383
+ else {
384
+ log(JSON.stringify(allFacts, null, 2));
329
385
  }
330
386
  }
331
387
  // ---------------------------------------------------------------------------
@@ -347,25 +403,62 @@ async function main() {
347
403
  await cmdRemember(args.slice(1));
348
404
  break;
349
405
  case 'recall':
350
- await cmdRecall(args.slice(1));
406
+ // Retired in Phase 3.3 — recall is now native via the bundled
407
+ // memory_search / memory_get tools (MemoryPluginCapability). Surface
408
+ // a clear pointer instead of falling through to "unknown command"
409
+ // so agents / users running stale prompts get actionable guidance.
410
+ die('tr recall was retired — recall is now native. ' +
411
+ 'The agent reads memories via the memory_search tool automatically; ' +
412
+ 'use `tr export` to dump every memory outside the agent.');
413
+ case 'forget':
414
+ await cmdForget(args.slice(1));
415
+ break;
416
+ case 'export':
417
+ await cmdExport(args.slice(1));
351
418
  break;
419
+ case 'import':
420
+ case 'upgrade':
421
+ // Import + upgrade run inside the gateway process (they need the
422
+ // plugin runtime — extraction pipeline, smart-import WASM, module-
423
+ // level auth state). The standalone `tr` binary is a separate Node
424
+ // script that does NOT load index.ts (would pull in the entire
425
+ // plugin runtime). Point users at the registerCli surface instead
426
+ // of silently no-op'ing or falling through to "unknown command".
427
+ die(`${cmd} is not available on the standalone \`tr\` binary. ` +
428
+ `Run it on the gateway host via:\n` +
429
+ ` openclaw totalreclaw ${cmd === 'import' ? 'import from <source>' : 'upgrade'}${cmd === 'import' ? ' [--file <path>] [--dry-run] [--json]' : ' [--json]'}\n` +
430
+ (cmd === 'import'
431
+ ? ` openclaw totalreclaw import status [--id <importId>] [--json]\n` +
432
+ ` openclaw totalreclaw import abort <importId> [--json]\n` +
433
+ `Sources: mem0 | mcp-memory | chatgpt | claude | gemini`
434
+ : `Returns a Stripe checkout URL for Pro upgrade.`));
352
435
  case undefined:
353
436
  case '--help':
354
437
  case '-h':
355
- process.stdout.write(`TotalReclaw hybrid CLI v${PLUGIN_VERSION} (primary modeOpenClaw 2026.5.2+)\n\n` +
438
+ process.stdout.write(`TotalReclaw CLI v${PLUGIN_VERSION} (recall is native memory_search tool)\n\n` +
356
439
  'Usage:\n' +
357
- ' tr status [--json] — onboarding + plugin load state\n' +
358
- ' tr pair [--json] — start a relay pairing session\n' +
359
- ' tr remember [--json] <text> — store a memory\n' +
360
- ' tr recall [--json] [--limit N] <query> search memories (default limit: 5)\n\n' +
440
+ ' tr status [--json] — onboarding + plugin load state\n' +
441
+ ' tr pair [--json] — start a relay pairing session\n' +
442
+ ' tr remember [--json] <text> — store a memory (on-chain UserOp)\n' +
443
+ ' tr forget [--json] <factId> tombstone a memory on-chain\n' +
444
+ ' tr export [--json] [--format json|markdown] — dump every memory in the vault\n\n' +
445
+ 'Recall: NOT a CLI command. The agent recalls via the bundled memory_search tool.\n' +
446
+ ' To dump memories outside the agent, use `tr export`.\n\n' +
447
+ 'Import + Upgrade: NOT on the standalone `tr` binary. They run inside the gateway\n' +
448
+ ' process via the `openclaw totalreclaw` subcommand chain:\n' +
449
+ ' openclaw totalreclaw import from <source> [--file <path>] [--dry-run] [--json]\n' +
450
+ ' openclaw totalreclaw import status [--id <importId>] [--json]\n' +
451
+ ' openclaw totalreclaw import abort <importId> [--json]\n' +
452
+ ' openclaw totalreclaw upgrade [--json]\n' +
453
+ ' Sources: mem0 | mcp-memory | chatgpt | claude | gemini\n\n' +
361
454
  'Flags:\n' +
362
- ' --json Output machine-parseable JSON (required for agent shell calls)\n' +
363
- ' --limit N Limit recall results (default: 5)\n\n' +
455
+ ' --json Output machine-parseable JSON (required for agent shell calls)\n\n' +
364
456
  'JSON output shapes:\n' +
365
457
  ' status: {"version":"...","onboarded":bool,"next_step":"pair|none","tool_count":N,"hybrid_mode":bool}\n' +
366
458
  ' pair: {"url":"...","pin":"123456","expires_at":"..."}\n' +
367
459
  ' remember: {"ok":true,"id":"...","claim_count":N}\n' +
368
- ' recall: {"results":[{"text":"...","score":0.8}]}\n\n' +
460
+ ' forget: {"ok":true,"id":"...","tx_hash":"0x..."}\n' +
461
+ ' export: {"count":N,"facts":[{"id":"...","text":"...","metadata":{...},"created_at":"..."}]}\n\n' +
369
462
  'Environment:\n' +
370
463
  ' TOTALRECLAW_SERVER_URL — relay URL (default: api.totalreclaw.xyz; staging: api-staging.totalreclaw.xyz)\n' +
371
464
  ' TOTALRECLAW_CREDENTIALS_PATH — override credentials.json path\n');