@engramx/client 0.16.0-rc.2 → 0.16.0-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 (95) hide show
  1. package/README.md +69 -8
  2. package/dist/EngramClient.d.ts +60 -16
  3. package/dist/EngramClient.js +139 -70
  4. package/dist/EngramClient.js.map +1 -1
  5. package/dist/MarketClient.js.map +1 -1
  6. package/dist/ProfileManager.d.ts +3 -19
  7. package/dist/ProfileManager.js +1 -58
  8. package/dist/ProfileManager.js.map +1 -1
  9. package/dist/SessionManager.d.ts +1 -1
  10. package/dist/SessionManager.js.map +1 -1
  11. package/dist/actionReceipt.d.ts +8 -0
  12. package/dist/actionReceipt.js +8 -0
  13. package/dist/actionReceipt.js.map +1 -1
  14. package/dist/birthChain.d.ts +87 -0
  15. package/dist/birthChain.js +233 -0
  16. package/dist/birthChain.js.map +1 -0
  17. package/dist/cache.js.map +1 -1
  18. package/dist/certifiedAudit.d.ts +31 -0
  19. package/dist/certifiedAudit.js +124 -0
  20. package/dist/certifiedAudit.js.map +1 -1
  21. package/dist/claude-setup.d.ts +15 -6
  22. package/dist/claude-setup.js +30 -17
  23. package/dist/claude-setup.js.map +1 -1
  24. package/dist/cli.d.ts +7 -1
  25. package/dist/cli.js +212 -187
  26. package/dist/cli.js.map +1 -1
  27. package/dist/connect-config.d.ts +109 -0
  28. package/dist/connect-config.js +251 -0
  29. package/dist/connect-config.js.map +1 -0
  30. package/dist/encryption.d.ts +4 -0
  31. package/dist/encryption.js +7 -1
  32. package/dist/encryption.js.map +1 -1
  33. package/dist/evidence.d.ts +94 -0
  34. package/dist/evidence.js +352 -0
  35. package/dist/evidence.js.map +1 -0
  36. package/dist/idl.d.ts +21 -3
  37. package/dist/idl.js +34 -4
  38. package/dist/idl.js.map +1 -1
  39. package/dist/index.d.ts +13 -6
  40. package/dist/index.js +22 -5
  41. package/dist/index.js.map +1 -1
  42. package/dist/ledgerfsHash.d.ts +0 -2
  43. package/dist/ledgerfsHash.js +0 -2
  44. package/dist/ledgerfsHash.js.map +1 -1
  45. package/dist/lineage.d.ts +7 -1
  46. package/dist/lineage.js +7 -1
  47. package/dist/lineage.js.map +1 -1
  48. package/dist/memoryArchive.d.ts +77 -98
  49. package/dist/memoryArchive.js +58 -31
  50. package/dist/memoryArchive.js.map +1 -1
  51. package/dist/migrate.d.ts +18 -30
  52. package/dist/migrate.js +42 -136
  53. package/dist/migrate.js.map +1 -1
  54. package/dist/openEngram.d.ts +7 -0
  55. package/dist/openEngram.js +27 -3
  56. package/dist/openEngram.js.map +1 -1
  57. package/dist/openclaw-local.d.ts +86 -0
  58. package/dist/openclaw-local.js +332 -0
  59. package/dist/openclaw-local.js.map +1 -0
  60. package/dist/openclaw-setup.d.ts +82 -3
  61. package/dist/openclaw-setup.js +162 -50
  62. package/dist/openclaw-setup.js.map +1 -1
  63. package/dist/pair.js.map +1 -1
  64. package/dist/path-utils.d.ts +0 -16
  65. package/dist/path-utils.js +4 -27
  66. package/dist/path-utils.js.map +1 -1
  67. package/dist/publicRecord.d.ts +5 -0
  68. package/dist/publicRecord.js +52 -0
  69. package/dist/publicRecord.js.map +1 -0
  70. package/dist/recordChain.d.ts +69 -0
  71. package/dist/recordChain.js +145 -0
  72. package/dist/recordChain.js.map +1 -0
  73. package/dist/schema.d.ts +11 -22
  74. package/dist/schema.js +6 -28
  75. package/dist/schema.js.map +1 -1
  76. package/dist/types.d.ts +67 -451
  77. package/dist/verify.d.ts +8 -0
  78. package/dist/verify.js +19 -0
  79. package/dist/verify.js.map +1 -0
  80. package/dist/version.d.ts +22 -0
  81. package/dist/version.js +22 -0
  82. package/dist/version.js.map +1 -1
  83. package/package.json +27 -12
  84. package/dist/generated/engram.idl.d.ts +0 -6
  85. package/dist/generated/engram.idl.js +0 -1366
  86. package/dist/generated/engram.idl.js.map +0 -1
  87. package/dist/generated/market.idl.d.ts +0 -6
  88. package/dist/generated/market.idl.js +0 -557
  89. package/dist/generated/market.idl.js.map +0 -1
  90. package/dist/generated/registry.idl.d.ts +0 -6
  91. package/dist/generated/registry.idl.js +0 -510
  92. package/dist/generated/registry.idl.js.map +0 -1
  93. package/dist/offline.d.ts +0 -21
  94. package/dist/offline.js +0 -104
  95. package/dist/offline.js.map +0 -1
package/dist/cli.js CHANGED
@@ -3,14 +3,17 @@ import { parseArgs } from 'node:util';
3
3
  import * as readline from 'readline';
4
4
  import * as fs from 'fs';
5
5
  import { pair, PairError } from './pair.js';
6
- import { NPX_CLIENT, pairCommand } from './version.js';
6
+ import { NPX_CLIENT, VERSION, pairCommand } from './version.js';
7
7
  import { execFileSync } from 'node:child_process';
8
8
  import * as path from 'path';
9
- import { setupOpenClaw, setupGitVersioning, OpenClawNotFoundError, getOpenClawDir, getMemoryDir, isGitRepo, } from './openclaw-setup.js';
9
+ import { setupOpenClaw, setupGitVersioning, OpenClawNotFoundError, OpenClawPluginOutdatedError, OPENCLAW_PLUGIN, outdatedOpenClawPlugins, getOpenClawDir, getMemoryDir, isGitRepo, } from './openclaw-setup.js';
10
10
  import { generateMcpConfig, claudeConfigPaths, installMcpConfig } from './claude-setup.js';
11
- import { migrateOpenClawMemory, recoverOpenClaw, rollbackCheckpoint, listCheckpoints, hasValidPairing, findIncompleteCheckpoint, resumeMigration, } from './migrate.js';
11
+ import { migrateOpenClawMemory, findOpenClawMemoryFiles, recoverOpenClaw, rollbackCheckpoint, listCheckpoints, hasValidPairing, findIncompleteCheckpoint, resumeMigration, } from './migrate.js';
12
12
  import { EngramClient } from './EngramClient.js';
13
- import { openEngram } from './openEngram.js';
13
+ import { checkEvidence } from './evidence.js';
14
+ import { IC_MAINNET_ROOT_KEY_HEX } from './certifiedAudit.js';
15
+ import { fetchSealedRows } from './publicRecord.js';
16
+ import { openEngram, shellWord } from './openEngram.js';
14
17
  import { Principal } from '@icp-sdk/core/principal';
15
18
  // Simple colored output helpers
16
19
  const green = (s) => `\x1b[32m${s}\x1b[0m`;
@@ -254,12 +257,15 @@ async function runResumeMigration(clientOptions) {
254
257
  fail(`Resume failed: ${err.message}`);
255
258
  process.exit(1);
256
259
  }
257
- console.log(`\nTo undo: ${bold('npx @engramx/client rollback')}`);
260
+ console.log(`\nTo undo: ${bold(`${NPX_CLIENT} rollback`)}`);
258
261
  }
259
262
  // ── Help text ───────────────────────────────────────────────────
260
263
  // ⟨2026-10-03⟩ Every usage line names the version this help was built with (version.ts): the
261
264
  // owner's ruling of that date pins the pairing command to the build's version, and the lines below it
262
265
  // run the same package, so they carry the same prefix.
266
+ // ⟨2026-10-06⟩ The owner kit's line under open-engram is pinned the same way: the packages share one
267
+ // version, and a bare `npx @engramx/owner` during a release candidate's soak installs 0.15.0, whose
268
+ // `--connect` still spawns a first engram for a key that owns nothing.
263
269
  function printUsage() {
264
270
  console.log(`
265
271
  ${bold('Usage:')}
@@ -268,10 +274,9 @@ ${bold('Usage:')}
268
274
  ${NPX_CLIENT} migrate --engram <id> [options]
269
275
  ${NPX_CLIENT} rollback [checkpoint-id]
270
276
  ${NPX_CLIENT} git-setup <remote-url>
271
- ${NPX_CLIENT} git-log [file]
272
- ${NPX_CLIENT} git-diff [ref] [--file <path>]
273
- ${NPX_CLIENT} git-revert <ref> --engram <id> [--file <path>]
274
- ${NPX_CLIENT} read <path> --engram <id> [--raw]
277
+ ${NPX_CLIENT} git-revert <ref> [--file <path>]
278
+ ${NPX_CLIENT} read <path> --engram <id>
279
+ ${NPX_CLIENT} verify <bundle.json> [--offline] [--host <url>] [--ic-root-key <hex>]
275
280
 
276
281
  ${bold('Commands:')}
277
282
  pair Pair this agent with an engram: a fresh key for it, enrolled under your lasting key
@@ -280,10 +285,12 @@ ${bold('Commands:')}
280
285
  migrate Migrate local memory to engram (or recover with --recover)
281
286
  rollback Restore files from a checkpoint
282
287
  git-setup Initialize git versioning for the memory directory
283
- git-log Show commit history for the memory repo
284
- git-diff Show changes since a commit (or working tree diff)
285
- git-revert Revert memory to a previous commit and sync to canister
286
- read Read a memory file (use --raw to see raw stored bytes)
288
+ git-revert Revert the local memory repo to a previous commit; prints the owner kit's command
289
+ that puts the reverted files back on the engram (an owner's act)
290
+ read Read one memory file and print its text
291
+ verify Check an evidence bundle against the network's signature: the bytes, the record
292
+ entry, the walk to a certified head, and the time the subnet signed it. Needs no
293
+ key, no account and no owner
287
294
 
288
295
  ${bold('Options (pair):')}
289
296
  --engram <id> Engram canister ID (required)
@@ -296,7 +303,10 @@ ${bold('Options (pair):')}
296
303
  (default: ~/.engramx/root.key; created on first use, never overwritten)
297
304
  --git-remote <url> Enable git versioning and set remote
298
305
  --force Overwrite an existing key for this engram (never the lasting key)
299
- --yes, -y Auto-confirm all prompts (non-interactive mode)
306
+ --migrate Also move this machine's OpenClaw memory files into the engram: each
307
+ is uploaded, then deleted here (undo with rollback). Without it, pair
308
+ moves nothing and says what it found
309
+ --yes, -y Auto-confirm all prompts (non-interactive mode); never adds --migrate
300
310
 
301
311
  ${bold('Options (open-engram):')}
302
312
  --engram <id> The engram this agent works in, which opens the new one (required)
@@ -308,25 +318,28 @@ ${bold('Options (open-engram):')}
308
318
 
309
319
  What is attached to open it is the agent's from the moment it exists (approving is giving).
310
320
  Afterwards the owner kit acts as its owner with the same file:
311
- npx @engramx/owner --connect --key-file <the lasting key's file> --registry <id>
321
+ npx @engramx/owner@${VERSION} --connect --key-file <the lasting key's file> --registry <id>
312
322
  (open-engram prints that line with both filled in.)
313
323
 
314
324
  ${bold('Options (migrate):')}
315
325
  --engram <id> Engram canister ID (required)
316
326
  --recover Recovery mode: download memory from engram to local
317
- --resume Resume an incomplete migration
318
327
  --dry-run Show what would be migrated without uploading
319
328
  --host <url> ICP host (default: https://ic0.app)
320
329
  --key-path <path> The key for this engram (default: ~/.engramx/session.key)
321
330
 
331
+ ${bold('Options (verify):')}
332
+ --offline Use only the rows in the bundle; never fetch from the network
333
+ --host <url> Where to fetch missing hash-only rows from (default: https://icp-api.io)
334
+ --ic-root-key <hex> Check against this root key instead of the pinned IC mainnet key (a local
335
+ replica's, for testing only; the result says so)
336
+
337
+ A bundle is what EngramClient.captureEvidence() writes after a write (format engramx-evidence/1).
338
+ Exit status 0: every link held. 1: a link broke, and the line marked ✗ names it. 2: no bundle.
339
+
322
340
  ${bold('Options (git-revert):')}
323
- --engram <id> Engram canister ID (required)
324
341
  --file <path> Revert a single file instead of the entire memory dir
325
- --host <url> ICP host (default: https://ic0.app)
326
- --key-path <path> The key for this engram (default: ~/.engramx/session.key)
327
-
328
- ${bold('Options (read):')}
329
- --raw Show raw stored bytes as hex instead of decoded text
342
+ (An incomplete migration is offered for resumption by a plain \`migrate\`.)
330
343
 
331
344
  --help Show this help message
332
345
  `);
@@ -339,16 +352,17 @@ async function main() {
339
352
  target: { type: 'string' },
340
353
  install: { type: 'boolean', default: false },
341
354
  recover: { type: 'boolean', default: false },
342
- resume: { type: 'boolean', default: false },
343
355
  'dry-run': { type: 'boolean', default: false },
344
356
  host: { type: 'string' },
345
357
  'key-path': { type: 'string' },
346
358
  'root-key': { type: 'string' },
347
359
  'git-remote': { type: 'string' },
348
360
  force: { type: 'boolean', default: false },
361
+ migrate: { type: 'boolean', default: false },
349
362
  yes: { type: 'boolean', short: 'y', default: false },
350
363
  file: { type: 'string' },
351
- raw: { type: 'boolean', default: false },
364
+ offline: { type: 'boolean', default: false },
365
+ 'ic-root-key': { type: 'string' },
352
366
  help: { type: 'boolean', default: false },
353
367
  },
354
368
  });
@@ -367,11 +381,23 @@ async function main() {
367
381
  process.exit(1);
368
382
  }
369
383
  }
370
- // ── read command (debug) ─────────────────────────────────────
384
+ // ⟨2026-10-13, the bloat audit⟩ `read <path> [--raw]`, `git-log` and `git-diff` were removed: no doc
385
+ // described them and no test ran them, in this repo or the platform. `read --raw` also printed
386
+ // "[object Object]" for every refusal (`fail(result.Err)` on the six-arm error variant) and a
387
+ // "stored as plaintext" line that was false on an encrypted engram. `git log` and `git diff` in
388
+ // the memory directory do what the other two did.
389
+ // ── read command ──────────────────────────────────────────────
390
+ // ⟨RESTORED 2026-10-14⟩ engramx f1907dc3 removed `read` with git-log and git-diff as "undocumented,
391
+ // untested" (the bloat audit, the owner's answers of 2026-10-13). The premise was wrong for `read`:
392
+ // printUsage listed it, downstream end-to-end checks of the packaged CLI read through it, and the
393
+ // staging test plan sent to a design partner tells their agents to read back with it — those checks
394
+ // failed on exactly the four read cases. `read` comes back; its `--raw` option does not (it printed the raw stored bytes through the
395
+ // private actor, was in no doc and no test — the dead part the audit meant). git-log and git-diff stay
396
+ // removed. WHAT WOULD MAKE THIS WRONG: a later ruling that agents read only through the SDK or MCP.
371
397
  if (command === 'read') {
372
398
  const filePath = positionals[1];
373
399
  if (!filePath) {
374
- fail('Usage: npx @engramx/client read <path> --engram <id> [--raw]');
400
+ fail(`Usage: ${NPX_CLIENT} read <path> --engram <id>`);
375
401
  process.exit(1);
376
402
  }
377
403
  if (!values.engram) {
@@ -382,25 +408,8 @@ async function main() {
382
408
  const host = values.host || 'https://ic0.app';
383
409
  const client = new EngramClient({ canisterId: values.engram, sessionKeyPath: keyPath, host });
384
410
  try {
385
- if (values.raw) {
386
- console.warn('WARNING: --raw shows the raw stored bytes (content is stored as plaintext).');
387
- // Show raw stored bytes as hex (no decode)
388
- const result = await client.actor.readMemory(filePath);
389
- if ('Err' in result) {
390
- fail(result.Err);
391
- process.exit(1);
392
- }
393
- const raw = result.Ok;
394
- const bytes = new Uint8Array(raw.content);
395
- console.log(`Path: ${raw.path}`);
396
- console.log(`Version: ${raw.version}`);
397
- console.log(`Size: ${bytes.length} bytes (raw stored, plaintext)`);
398
- console.log(`Hex: ${Buffer.from(bytes).toString('hex').slice(0, 200)}...`);
399
- }
400
- else {
401
- const file = await client.readMemory(filePath);
402
- console.log(file.content);
403
- }
411
+ const file = await client.readMemory(filePath);
412
+ console.log(file.content);
404
413
  }
405
414
  catch (err) {
406
415
  fail(`Read failed: ${err.message}`);
@@ -408,6 +417,58 @@ async function main() {
408
417
  }
409
418
  return;
410
419
  }
420
+ // ── verify command ────────────────────────────────────────────
421
+ // ⟨2026-10-15, the owner's ruling 6B of 2026-10-14⟩ The public check. It runs on a machine with no
422
+ // identity: the bundle carries the bytes, the entry, the certificate and (usually) the hash-only
423
+ // rows; any rows it lacks are fetched anonymously from the canister's public record read. The
424
+ // certificate is checked at any age against the PINNED mainnet root key — never one fetched from
425
+ // the network it is checking. evidence.ts names each link; this prints them.
426
+ if (command === 'verify') {
427
+ const file = positionals[1];
428
+ if (!file) {
429
+ fail(`Usage: ${NPX_CLIENT} verify <bundle.json> [--offline] [--host <url>]`);
430
+ process.exit(2);
431
+ }
432
+ let bundle;
433
+ try {
434
+ bundle = JSON.parse(fs.readFileSync(file, 'utf8'));
435
+ }
436
+ catch (err) {
437
+ fail(`Cannot read the bundle ${file}: ${err.message}`);
438
+ process.exit(2);
439
+ }
440
+ let rootKey;
441
+ if (values['ic-root-key']) {
442
+ const h = values['ic-root-key'].trim().toLowerCase();
443
+ if (!/^([0-9a-f]{2})+$/.test(h)) {
444
+ fail('--ic-root-key must be hex');
445
+ process.exit(2);
446
+ }
447
+ rootKey = Uint8Array.from(h.match(/../g), (b) => parseInt(b, 16));
448
+ if (h !== IC_MAINNET_ROOT_KEY_HEX) {
449
+ warn('Checking against a root key that is NOT the IC mainnet key — nothing below is a proof to anyone else.');
450
+ }
451
+ }
452
+ const host = values.host || 'https://icp-api.io';
453
+ const result = await checkEvidence(bundle, {
454
+ rootKey,
455
+ fetchRows: values.offline
456
+ ? undefined
457
+ : (fromId) => fetchSealedRows(host, bundle.canisterId, fromId, rootKey),
458
+ });
459
+ for (const l of result.links)
460
+ (l.ok ? ok : fail)(`${l.link}: ${l.detail}`);
461
+ if (!result.ok) {
462
+ fail(`NOT VERIFIED — ${result.why}`);
463
+ process.exit(1);
464
+ }
465
+ console.log();
466
+ ok(bold('VERIFIED'));
467
+ info(`entry ${bundle.entry.id} was in ${bundle.canisterId}'s record when the subnet signed at`);
468
+ console.log(` ${bold(new Date(Number(result.signedTimeNs / 1000000n)).toISOString())} (${result.signedTimeNs} ns)`);
469
+ info(`distance walked: ${result.distance} entr${result.distance === 1n ? 'y' : 'ies'}; the certified head covers ${result.entriesCovered} entries`);
470
+ return;
471
+ }
411
472
  // ── rollback command ──────────────────────────────────────────
412
473
  if (command === 'rollback') {
413
474
  const checkpoints = listCheckpoints();
@@ -471,79 +532,19 @@ async function main() {
471
532
  }
472
533
  return;
473
534
  }
474
- // ── git-log command ──────────────────────────────────────────
475
- if (command === 'git-log') {
476
- const memoryDir = getMemoryDir();
477
- if (!isGitRepo(memoryDir)) {
478
- fail('Git versioning not set up. Run: npx @engramx/client git-setup <url>');
479
- process.exit(1);
480
- }
481
- const filePath = positionals[1] || values.file;
482
- const args = ['log', '--oneline', '--no-decorate', '-30'];
483
- if (filePath)
484
- args.push('--', filePath);
485
- try {
486
- const output = execFileSync('git', args, { cwd: memoryDir, stdio: 'pipe' }).toString().trim();
487
- if (output) {
488
- console.log(output);
489
- }
490
- else {
491
- info('No commits yet.');
492
- }
493
- }
494
- catch {
495
- info('No commits yet.');
496
- }
497
- return;
498
- }
499
- // ── git-diff command ────────────────────────────────────────
500
- if (command === 'git-diff') {
501
- const memoryDir = getMemoryDir();
502
- if (!isGitRepo(memoryDir)) {
503
- fail('Git versioning not set up. Run: npx @engramx/client git-setup <url>');
504
- process.exit(1);
505
- }
506
- const ref = positionals[1];
507
- const filePath = values.file;
508
- const args = ['diff'];
509
- if (ref)
510
- args.push(ref);
511
- if (filePath)
512
- args.push('--', filePath);
513
- try {
514
- const output = execFileSync('git', args, { cwd: memoryDir, stdio: 'pipe' }).toString().trim();
515
- if (output) {
516
- console.log(output);
517
- }
518
- else {
519
- info('No changes.');
520
- }
521
- }
522
- catch (err) {
523
- fail(`git diff failed: ${err.message}`);
524
- process.exit(1);
525
- }
526
- return;
527
- }
528
535
  // ── git-revert command ──────────────────────────────────────
529
536
  if (command === 'git-revert') {
530
537
  const ref = positionals[1];
531
538
  if (!ref) {
532
- fail('Usage: npx @engramx/client git-revert <commit-or-ref> --engram <id> [--file <path>]');
533
- process.exit(1);
534
- }
535
- if (!values.engram) {
536
- fail('Missing --engram <canister-id> (needed to sync reverted files to canister)');
539
+ fail(`Usage: ${NPX_CLIENT} git-revert <commit-or-ref> [--file <path>]`);
537
540
  process.exit(1);
538
541
  }
539
542
  const memoryDir = getMemoryDir();
540
543
  if (!isGitRepo(memoryDir)) {
541
- fail('Git versioning not set up. Run: npx @engramx/client git-setup <url>');
544
+ fail(`Git versioning not set up. Run: ${NPX_CLIENT} git-setup <url>`);
542
545
  process.exit(1);
543
546
  }
544
547
  const filePath = values.file;
545
- const keyPath = values['key-path'] || '~/.engramx/session.key';
546
- const host = values.host || 'https://ic0.app';
547
548
  // 1. Git checkout to revert files
548
549
  step(`Reverting to ${ref}${filePath ? ` (file: ${filePath})` : ''}...`);
549
550
  try {
@@ -586,62 +587,13 @@ async function main() {
586
587
  warn(`Git commit failed: ${msg}`);
587
588
  }
588
589
  }
589
- // 3. Restore reverted files on the canister
590
- step('Restoring reverted files on engram canister...');
591
- const client = new EngramClient({ canisterId: values.engram, sessionKeyPath: keyPath, host });
592
- let syncCount = 0;
593
- let syncErrors = 0;
594
- async function syncDir(dir) {
595
- if (!fs.existsSync(dir))
596
- return;
597
- const entries = fs.readdirSync(dir, { withFileTypes: true });
598
- for (const entry of entries) {
599
- const fullPath = `${dir}/${entry.name}`;
600
- if (entry.isDirectory()) {
601
- if (entry.name === '.git')
602
- continue;
603
- await syncDir(fullPath);
604
- }
605
- else if (entry.isFile() && entry.name.endsWith('.md')) {
606
- const relativePath = path.relative(memoryDir, fullPath);
607
- if (relativePath.startsWith('..'))
608
- continue; // skip files outside memoryDir
609
- const content = fs.readFileSync(fullPath, 'utf-8');
610
- try {
611
- // A revert must RESTORE the canister file to the reverted content —
612
- // appendMemory would stack the old content on top of the current, leaving
613
- // a doubled, self-contradictory memory. writeMemory overwrites; it is
614
- // OWNER-gated on the canister, so it respects the append-only invariant for
615
- // agents — a non-owner key is rejected here (a loud, honest failure)
616
- // rather than silently corrupting.
617
- // NOTE: writes plaintext bytes; a memory stored in framed-encrypted (Private)
618
- // mode would need an encryption-aware owner restore in the SDK core (the
619
- // deeper fix — out of scope for the CLI).
620
- const ciphertext = new TextEncoder().encode(content);
621
- const result = await client.actor.writeMemory(relativePath, ciphertext);
622
- if ('Err' in result)
623
- throw new Error(result.Err);
624
- ok(` ${relativePath}`);
625
- syncCount++;
626
- }
627
- catch (err) {
628
- fail(` ${relativePath}: ${err.message}`);
629
- syncErrors++;
630
- }
631
- }
632
- }
633
- }
634
- await syncDir(memoryDir);
635
- if (syncErrors > 0) {
636
- fail(`Restored ${syncCount} file(s) on engram, ${syncErrors} failed`);
637
- info("Restore requires owner credentials — an agent's key cannot overwrite canister memory.");
638
- }
639
- else if (syncCount > 0) {
640
- ok(`Restored ${syncCount} file${syncCount === 1 ? '' : 's'} on engram`);
641
- }
642
- else {
643
- info('No .md files to restore');
644
- }
590
+ // 3. ⟨2026-10-13, the owner's 5A⟩ The canister step MOVED to the owner kit. It wrote each file
591
+ // back with writeMemory, which REPLACES a file and is owner-only on the engram — so from an
592
+ // agent's key it always failed (and printed "[object Object]" for the refusal), and it wrote
593
+ // plaintext into an engram that encrypts memory. An owner's act belongs in the owner kit:
594
+ // `@engramx/owner --restore` (its owned.ts · restoreMemory says what changed on the way).
595
+ info('The files are reverted here. To put them back on the engram, its owner runs:');
596
+ console.log(` ${bold(`npx @engramx/owner@${VERSION} --restore ${shellWord(memoryDir)}${filePath ? ` --file ${shellWord(filePath)}` : ''} --key-file <the owner's key file>`)}`);
645
597
  // 4. Push to remote if configured
646
598
  try {
647
599
  execFileSync('git', ['push'], { cwd: memoryDir, stdio: 'pipe' });
@@ -657,7 +609,7 @@ async function main() {
657
609
  if (command === 'git-setup') {
658
610
  const remoteUrl = positionals[1];
659
611
  if (!remoteUrl) {
660
- fail('Missing remote URL. Usage: npx @engramx/client git-setup <remote-url>');
612
+ fail(`Missing remote URL. Usage: ${NPX_CLIENT} git-setup <remote-url>`);
661
613
  process.exit(1);
662
614
  }
663
615
  step('Setting up git versioning...');
@@ -697,11 +649,6 @@ async function main() {
697
649
  process.exit(1);
698
650
  }
699
651
  }
700
- // Flag validation
701
- if (values.resume && values['dry-run']) {
702
- fail('Cannot combine --resume with --dry-run');
703
- process.exit(1);
704
- }
705
652
  if (values.recover) {
706
653
  // ── Recovery mode ───────────────────────────────────────
707
654
  step('Recovering from engram...');
@@ -728,15 +675,12 @@ async function main() {
728
675
  fail(`Recovery failed: ${err.message}`);
729
676
  process.exit(1);
730
677
  }
731
- console.log(`\nTo undo: ${bold('npx @engramx/client rollback')}`);
678
+ console.log(`\nTo undo: ${bold(`${NPX_CLIENT} rollback`)}`);
732
679
  console.log(`Restart OpenClaw \u2014 memory files will sync automatically.`);
733
680
  return;
734
681
  }
735
- if (values.resume) {
736
- // ── Resume mode ─────────────────────────────────────────
737
- await runResumeMigration(clientOptions);
738
- return;
739
- }
682
+ // ⟨2026-10-13, the bloat audit⟩ The `--resume` flag went: it was a second door to what a plain
683
+ // `migrate` already does just below — find the incomplete checkpoint and offer to resume it.
740
684
  // ── Default: check for incomplete checkpoint ────────────
741
685
  const incomplete = findIncompleteCheckpoint();
742
686
  if (incomplete && !values['dry-run']) {
@@ -771,7 +715,7 @@ async function main() {
771
715
  process.exit(1);
772
716
  }
773
717
  if (!values['dry-run']) {
774
- console.log(`\nTo undo: ${bold('npx @engramx/client rollback')}`);
718
+ console.log(`\nTo undo: ${bold(`${NPX_CLIENT} rollback`)}`);
775
719
  }
776
720
  return;
777
721
  }
@@ -780,7 +724,9 @@ async function main() {
780
724
  // drive it with a stub engram; this block only parses and passes the terminal in.
781
725
  if (command === 'open-engram') {
782
726
  if (!values.engram) {
783
- fail('Missing --engram <canister-id>. Usage: npx @engramx/client open-engram --engram <id>');
727
+ // ⟨2026-10-06⟩ Pinned like the help's usage lines: a bare npx during a release candidate's
728
+ // soak installs 0.15.0, which has no open-engram.
729
+ fail(`Missing --engram <canister-id>. Usage: ${NPX_CLIENT} open-engram --engram <id>`);
784
730
  process.exit(1);
785
731
  }
786
732
  const result = await openEngram({
@@ -809,7 +755,7 @@ async function main() {
809
755
  }
810
756
  // Error if --recover is passed to pair (moved to migrate)
811
757
  if (values.recover) {
812
- fail('The --recover flag has moved to the migrate command. Use: npx @engramx/client migrate --engram <id> --recover');
758
+ fail(`The --recover flag has moved to the migrate command. Use: ${NPX_CLIENT} migrate --engram <id> --recover`);
813
759
  process.exit(1);
814
760
  }
815
761
  const inviteCode = positionals[1];
@@ -832,6 +778,23 @@ async function main() {
832
778
  info(`Key path: ${keyPath}`);
833
779
  info(`Lasting key: ${rootKeyPath}`);
834
780
  info(`Target: ${target}`);
781
+ // Before anything changes: an EngramX plugin for OpenClaw already on this machine must be this
782
+ // release's, or pair stops here.
783
+ // ⟨ADDED 2026-10-13, the second review of the owner's "fix the pairing migration"⟩ WHY. Step 2 left
784
+ // an older plugin in place and printed "Installed EngramX plugin for OpenClaw" over it, and one from
785
+ // 0.16.0-rc.11 or earlier deletes the local memory files the engram does not list when OpenClaw
786
+ // next starts — the files Step 4 now leaves where they are. Stopping HERE, before the invite is
787
+ // used, is what lets the same pair command run again once the plugin is upgraded; refused at Step
788
+ // 2 the pairing would already be done and the invite spent. openclaw-setup.ts ·
789
+ // outdatedOpenClawPlugins says the rest. Held by test/pair-migrate.test.ts.
790
+ if (target === 'openclaw') {
791
+ const outdated = outdatedOpenClawPlugins();
792
+ if (outdated.length > 0) {
793
+ fail(new OpenClawPluginOutdatedError(outdated).message);
794
+ info('Nothing was changed, and the invite is not used yet: upgrade the plugin, then run this pair command again.');
795
+ process.exit(1);
796
+ }
797
+ }
835
798
  // Step 1: Pair (keep the lasting key, generate a fresh key for this engram, enrol it, pairAgent)
836
799
  step('Generating a key for this engram, enrolling it under your lasting key, pairing...');
837
800
  info(`This will write: ${keyPath} (and ${rootKeyPath} if you have no lasting key yet)`);
@@ -892,14 +855,31 @@ async function main() {
892
855
  sessionKeyPath: keyPath,
893
856
  host: values.host,
894
857
  });
895
- ok(`Installed EngramX plugin for OpenClaw`);
858
+ // ⟨CHANGED 2026-10-13, the second review⟩ This printed "Installed EngramX plugin for OpenClaw"
859
+ // whatever setupOpenClaw reported — over a plugin already there at any version, and over no
860
+ // install at all when OpenClaw's gateway was not running. It now says what happened.
861
+ const where = setupResult.pluginNote ?? '';
862
+ switch (setupResult.pluginMethod) {
863
+ case 'cli':
864
+ ok(`Installed ${OPENCLAW_PLUGIN} through OpenClaw's plugin installer`);
865
+ break;
866
+ case 'manual-npm':
867
+ ok(`Installed ${OPENCLAW_PLUGIN} with npm ${dim(`\u2192 ${where}`)}`);
868
+ break;
869
+ case 'current':
870
+ ok(`The EngramX plugin is already ${OPENCLAW_PLUGIN} ${dim(`\u2192 ${where}`)}`);
871
+ break;
872
+ default:
873
+ warn(`Did not install the EngramX plugin for OpenClaw: ${where}.`);
874
+ info(`Install it with: openclaw plugins install ${OPENCLAW_PLUGIN}`);
875
+ }
896
876
  ok(`Updated ${dim(setupResult.configPath)}`);
897
877
  }
898
878
  catch (err) {
899
879
  if (err instanceof OpenClawNotFoundError) {
900
880
  fail(err.message);
901
881
  console.log(`\n${dim('Manual setup:')}`);
902
- console.log(dim(' 1. npm install @engramx/openclaw'));
882
+ console.log(dim(` 1. npm install @engramx/openclaw@${VERSION}`));
903
883
  console.log(dim(' 2. Add to openclaw.json: { "plugins": { "slots": { "memory": "memory-engramx" } } }'));
904
884
  }
905
885
  else {
@@ -1072,11 +1052,35 @@ async function main() {
1072
1052
  }
1073
1053
  catch (err) {
1074
1054
  fail(`Git setup failed: ${err.message}`);
1075
- info('You can set this up manually later. See: npx @engramx/client git-setup <url>');
1055
+ info(`You can set this up manually later. See: ${NPX_CLIENT} git-setup <url>`);
1076
1056
  }
1077
1057
  }
1078
- // Step 4: Migration
1079
- {
1058
+ // Step 4: Migration — only when asked (--migrate).
1059
+ // ⟨CHANGED 2026-10-13, the owner's ruling of that day: "fix the pairing migration"⟩ WHY. This step
1060
+ // ran on every pair, for every --target (claude too), and under --yes it confirmed itself: it moved
1061
+ // every OpenClaw memory file under OPENCLAW_CONFIG_DIR or ~/.openclaw into the engram and deleted
1062
+ // the local originals (migrate.ts · migrateFiles → fs.unlinkSync, after a checkpoint copy). Pairing
1063
+ // an agent for Claude on a machine that had once run OpenClaw moved that machine's OpenClaw memory
1064
+ // into an engram nobody asked to fill, and an environment variable pointed elsewhere was the only
1065
+ // guard. Now pairing migrates only on the explicit --migrate; --yes answers the prompts of what was
1066
+ // asked and never asks for the migration itself. Without --migrate, pairing reads the file list the
1067
+ // migration would start from, moves nothing, and prints one line: what it found and the command
1068
+ // that moves it. The `migrate` command is unchanged. Held by test/pair-migrate.test.ts.
1069
+ // WHAT WOULD MAKE THIS WRONG: a caller that relies on pair to move the files. None in this
1070
+ // repository does: integrations/openclaw never runs pair, and its README gives `migrate` as the
1071
+ // second of two commands.
1072
+ // ⟨CORRECTED 2026-10-13, later, on review⟩ One did, without calling it: the OpenClaw plugin's sync
1073
+ // deleted every local memory file the engram did not list, before pushing it, so the files in its
1074
+ // memory folder that this step no longer moves were lost at OpenClaw's next start, unless more than
1075
+ // five, and more than half the folder, would go. The plugin now deletes only what the engram had
1076
+ // (integrations/openclaw/index.ts · knownPathsFile).
1077
+ // ⟨CORRECTED 2026-10-13, the second review⟩ "The plugin now deletes only what the engram had" held
1078
+ // for the plugin released with this kit, not for the one already installed: Step 2 left an older
1079
+ // plugin in place and said it had installed one, so on such a machine the files this step leaves
1080
+ // were still lost at OpenClaw's next start. pair now stops before pairing when the installed plugin
1081
+ // is a different version (the check above Step 1), and the plugin's delete step reads the engram's
1082
+ // whole listing (index.ts · readEngramListing).
1083
+ if (values.migrate) {
1080
1084
  const clientOptions = {
1081
1085
  canisterId: values.engram,
1082
1086
  sessionKeyPath: keyPath,
@@ -1094,20 +1098,41 @@ async function main() {
1094
1098
  fail(`Migration failed: ${err.message}`);
1095
1099
  process.exit(1);
1096
1100
  }
1097
- console.log(`\nTo undo: ${bold('npx @engramx/client rollback')}`);
1101
+ console.log(`\nTo undo: ${bold(`${NPX_CLIENT} rollback`)}`);
1098
1102
  if (gitRemoteUrl) {
1099
1103
  console.log(`Memory changes will be auto-committed and pushed to GitHub.`);
1100
1104
  }
1101
1105
  else {
1102
- info('Tip: Add git versioning with npx @engramx/client git-setup <url>');
1106
+ info(`Tip: Add git versioning with ${NPX_CLIENT} git-setup <url>`);
1103
1107
  }
1104
1108
  }
1105
1109
  else {
1106
- info(`Run 'npx @engramx/client migrate --engram ${values.engram}' to migrate later.`);
1110
+ info(`Run '${NPX_CLIENT} migrate --engram ${values.engram}' to migrate later.`);
1111
+ }
1112
+ }
1113
+ else {
1114
+ let found;
1115
+ try {
1116
+ found = findOpenClawMemoryFiles();
1117
+ }
1118
+ catch (err) {
1119
+ warn(`Could not look for OpenClaw memory files (none were moved): ${err.message}`);
1120
+ }
1121
+ const n = found?.files.length ?? 0;
1122
+ if (found && n > 0) {
1123
+ info(`Found ${n} OpenClaw memory file${n === 1 ? '' : 's'} in ${found.openclawDir} and left ${n === 1 ? 'it' : 'them'} there. ` +
1124
+ `To move ${n === 1 ? 'it' : 'them'} into this engram (each is uploaded, then deleted here): ` +
1125
+ `${NPX_CLIENT} migrate --engram ${values.engram}`);
1107
1126
  }
1108
1127
  }
1109
1128
  }
1110
- main().catch((err) => {
1129
+ /**
1130
+ * The run this module starts when it is loaded — the command line's whole life.
1131
+ * ⟨2026-10-13⟩ Exported so a test can load the CLI with stubbed network modules and wait for the
1132
+ * command to END (test/pair-migrate.test.ts); before, a test could only wait one tick, which is
1133
+ * enough for --help and not for pair.
1134
+ */
1135
+ export const finished = main().catch((err) => {
1111
1136
  fail(`Fatal: ${err.message}`);
1112
1137
  process.exit(1);
1113
1138
  });