@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.
- package/README.md +69 -8
- package/dist/EngramClient.d.ts +60 -16
- package/dist/EngramClient.js +139 -70
- package/dist/EngramClient.js.map +1 -1
- package/dist/MarketClient.js.map +1 -1
- package/dist/ProfileManager.d.ts +3 -19
- package/dist/ProfileManager.js +1 -58
- package/dist/ProfileManager.js.map +1 -1
- package/dist/SessionManager.d.ts +1 -1
- package/dist/SessionManager.js.map +1 -1
- package/dist/actionReceipt.d.ts +8 -0
- package/dist/actionReceipt.js +8 -0
- package/dist/actionReceipt.js.map +1 -1
- package/dist/birthChain.d.ts +87 -0
- package/dist/birthChain.js +233 -0
- package/dist/birthChain.js.map +1 -0
- package/dist/cache.js.map +1 -1
- package/dist/certifiedAudit.d.ts +31 -0
- package/dist/certifiedAudit.js +124 -0
- package/dist/certifiedAudit.js.map +1 -1
- package/dist/claude-setup.d.ts +15 -6
- package/dist/claude-setup.js +30 -17
- package/dist/claude-setup.js.map +1 -1
- package/dist/cli.d.ts +7 -1
- package/dist/cli.js +212 -187
- package/dist/cli.js.map +1 -1
- package/dist/connect-config.d.ts +109 -0
- package/dist/connect-config.js +251 -0
- package/dist/connect-config.js.map +1 -0
- package/dist/encryption.d.ts +4 -0
- package/dist/encryption.js +7 -1
- package/dist/encryption.js.map +1 -1
- package/dist/evidence.d.ts +94 -0
- package/dist/evidence.js +352 -0
- package/dist/evidence.js.map +1 -0
- package/dist/idl.d.ts +21 -3
- package/dist/idl.js +34 -4
- package/dist/idl.js.map +1 -1
- package/dist/index.d.ts +13 -6
- package/dist/index.js +22 -5
- package/dist/index.js.map +1 -1
- package/dist/ledgerfsHash.d.ts +0 -2
- package/dist/ledgerfsHash.js +0 -2
- package/dist/ledgerfsHash.js.map +1 -1
- package/dist/lineage.d.ts +7 -1
- package/dist/lineage.js +7 -1
- package/dist/lineage.js.map +1 -1
- package/dist/memoryArchive.d.ts +77 -98
- package/dist/memoryArchive.js +58 -31
- package/dist/memoryArchive.js.map +1 -1
- package/dist/migrate.d.ts +18 -30
- package/dist/migrate.js +42 -136
- package/dist/migrate.js.map +1 -1
- package/dist/openEngram.d.ts +7 -0
- package/dist/openEngram.js +27 -3
- package/dist/openEngram.js.map +1 -1
- package/dist/openclaw-local.d.ts +86 -0
- package/dist/openclaw-local.js +332 -0
- package/dist/openclaw-local.js.map +1 -0
- package/dist/openclaw-setup.d.ts +82 -3
- package/dist/openclaw-setup.js +162 -50
- package/dist/openclaw-setup.js.map +1 -1
- package/dist/pair.js.map +1 -1
- package/dist/path-utils.d.ts +0 -16
- package/dist/path-utils.js +4 -27
- package/dist/path-utils.js.map +1 -1
- package/dist/publicRecord.d.ts +5 -0
- package/dist/publicRecord.js +52 -0
- package/dist/publicRecord.js.map +1 -0
- package/dist/recordChain.d.ts +69 -0
- package/dist/recordChain.js +145 -0
- package/dist/recordChain.js.map +1 -0
- package/dist/schema.d.ts +11 -22
- package/dist/schema.js +6 -28
- package/dist/schema.js.map +1 -1
- package/dist/types.d.ts +67 -451
- package/dist/verify.d.ts +8 -0
- package/dist/verify.js +19 -0
- package/dist/verify.js.map +1 -0
- package/dist/version.d.ts +22 -0
- package/dist/version.js +22 -0
- package/dist/version.js.map +1 -1
- package/package.json +27 -12
- package/dist/generated/engram.idl.d.ts +0 -6
- package/dist/generated/engram.idl.js +0 -1366
- package/dist/generated/engram.idl.js.map +0 -1
- package/dist/generated/market.idl.d.ts +0 -6
- package/dist/generated/market.idl.js +0 -557
- package/dist/generated/market.idl.js.map +0 -1
- package/dist/generated/registry.idl.d.ts +0 -6
- package/dist/generated/registry.idl.js +0 -510
- package/dist/generated/registry.idl.js.map +0 -1
- package/dist/offline.d.ts +0 -21
- package/dist/offline.js +0 -104
- 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 {
|
|
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(
|
|
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-
|
|
272
|
-
${NPX_CLIENT}
|
|
273
|
-
${NPX_CLIENT}
|
|
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-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
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
|
-
--
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
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(
|
|
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
|
-
|
|
386
|
-
|
|
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(
|
|
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(
|
|
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.
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
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(
|
|
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(
|
|
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
|
-
|
|
736
|
-
|
|
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(
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
1106
|
+
info(`Tip: Add git versioning with ${NPX_CLIENT} git-setup <url>`);
|
|
1103
1107
|
}
|
|
1104
1108
|
}
|
|
1105
1109
|
else {
|
|
1106
|
-
info(`Run '
|
|
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
|
-
|
|
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
|
});
|