@engramx/client 0.1.0

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 (57) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +116 -0
  3. package/dist/EngramClient.d.ts +204 -0
  4. package/dist/EngramClient.js +1168 -0
  5. package/dist/EngramClient.js.map +1 -0
  6. package/dist/ProfileManager.d.ts +61 -0
  7. package/dist/ProfileManager.js +116 -0
  8. package/dist/ProfileManager.js.map +1 -0
  9. package/dist/SessionManager.d.ts +46 -0
  10. package/dist/SessionManager.js +114 -0
  11. package/dist/SessionManager.js.map +1 -0
  12. package/dist/agent-skill.d.ts +150 -0
  13. package/dist/agent-skill.js +152 -0
  14. package/dist/agent-skill.js.map +1 -0
  15. package/dist/auth.d.ts +17 -0
  16. package/dist/auth.js +65 -0
  17. package/dist/auth.js.map +1 -0
  18. package/dist/bootstrap.d.ts +21 -0
  19. package/dist/bootstrap.js +71 -0
  20. package/dist/bootstrap.js.map +1 -0
  21. package/dist/cache.d.ts +11 -0
  22. package/dist/cache.js +41 -0
  23. package/dist/cache.js.map +1 -0
  24. package/dist/claude-setup.d.ts +29 -0
  25. package/dist/claude-setup.js +57 -0
  26. package/dist/claude-setup.js.map +1 -0
  27. package/dist/cli.d.ts +2 -0
  28. package/dist/cli.js +1078 -0
  29. package/dist/cli.js.map +1 -0
  30. package/dist/encryption.d.ts +44 -0
  31. package/dist/encryption.js +108 -0
  32. package/dist/encryption.js.map +1 -0
  33. package/dist/index.d.ts +36 -0
  34. package/dist/index.js +23 -0
  35. package/dist/index.js.map +1 -0
  36. package/dist/migrate.d.ts +106 -0
  37. package/dist/migrate.js +882 -0
  38. package/dist/migrate.js.map +1 -0
  39. package/dist/offline.d.ts +21 -0
  40. package/dist/offline.js +90 -0
  41. package/dist/offline.js.map +1 -0
  42. package/dist/openclaw-setup.d.ts +24 -0
  43. package/dist/openclaw-setup.js +249 -0
  44. package/dist/openclaw-setup.js.map +1 -0
  45. package/dist/pair.d.ts +20 -0
  46. package/dist/pair.js +68 -0
  47. package/dist/pair.js.map +1 -0
  48. package/dist/path-utils.d.ts +16 -0
  49. package/dist/path-utils.js +29 -0
  50. package/dist/path-utils.js.map +1 -0
  51. package/dist/schema.d.ts +54 -0
  52. package/dist/schema.js +101 -0
  53. package/dist/schema.js.map +1 -0
  54. package/dist/types.d.ts +452 -0
  55. package/dist/types.js +2 -0
  56. package/dist/types.js.map +1 -0
  57. package/package.json +67 -0
package/dist/cli.js ADDED
@@ -0,0 +1,1078 @@
1
+ #!/usr/bin/env node
2
+ import { parseArgs } from 'node:util';
3
+ import * as readline from 'readline';
4
+ import * as fs from 'fs';
5
+ import { pair, PairError } from './pair';
6
+ import { execFileSync } from 'node:child_process';
7
+ import * as path from 'path';
8
+ import { setupOpenClaw, setupGitVersioning, OpenClawNotFoundError, getOpenClawDir, getMemoryDir, isGitRepo, } from './openclaw-setup';
9
+ import { generateMcpConfig, claudeConfigPaths, installMcpConfig } from './claude-setup';
10
+ import { migrateOpenClawMemory, recoverOpenClaw, rollbackCheckpoint, listCheckpoints, hasValidPairing, findIncompleteCheckpoint, resumeMigration, } from './migrate';
11
+ import { EngramClient } from './EngramClient';
12
+ import { Principal } from '@icp-sdk/core/principal';
13
+ // Simple colored output helpers
14
+ const green = (s) => `\x1b[32m${s}\x1b[0m`;
15
+ const red = (s) => `\x1b[31m${s}\x1b[0m`;
16
+ const yellow = (s) => `\x1b[33m${s}\x1b[0m`;
17
+ const dim = (s) => `\x1b[2m${s}\x1b[0m`;
18
+ const bold = (s) => `\x1b[1m${s}\x1b[0m`;
19
+ function ok(msg) {
20
+ console.log(`${green('\u2713')} ${msg}`);
21
+ }
22
+ function fail(msg) {
23
+ console.error(`${red('\u2717')} ${msg}`);
24
+ }
25
+ function warn(msg) {
26
+ console.log(`${yellow('\u26A0')} ${msg}`);
27
+ }
28
+ function info(msg) {
29
+ console.log(` ${dim(msg)}`);
30
+ }
31
+ function step(msg) {
32
+ console.log(`\n${bold('\u25B6')} ${msg}`);
33
+ }
34
+ let autoConfirm = false;
35
+ /** Detect which agent frameworks are installed on this machine. */
36
+ function detectTargets() {
37
+ const detected = [];
38
+ const openclawDir = process.env['OPENCLAW_CONFIG_DIR'] || path.join(process.env['HOME'] || '', '.openclaw');
39
+ if (fs.existsSync(openclawDir)) {
40
+ detected.push('openclaw');
41
+ }
42
+ const claudeDir = path.join(process.env['HOME'] || '', '.claude');
43
+ if (fs.existsSync(claudeDir)) {
44
+ detected.push('claude');
45
+ }
46
+ return detected;
47
+ }
48
+ /** Prompt user to choose a target when multiple are detected. */
49
+ async function promptTarget(detected) {
50
+ if (detected.length === 0) {
51
+ // Nothing detected — ask
52
+ console.log(`\n${bold('No agent framework detected.')}`);
53
+ info('Which framework are you pairing with?');
54
+ }
55
+ else {
56
+ console.log(`\n${bold('Multiple agent frameworks detected:')}`);
57
+ }
58
+ for (let i = 0; i < detected.length; i++) {
59
+ console.log(` ${bold(`[${i + 1}]`)} ${detected[i]}`);
60
+ }
61
+ if (detected.length === 0) {
62
+ console.log(` ${bold('[1]')} openclaw`);
63
+ console.log(` ${bold('[2]')} claude`);
64
+ }
65
+ const choices = detected.length > 0 ? detected : ['openclaw', 'claude'];
66
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
67
+ return new Promise((resolve) => {
68
+ rl.question(`\n Select ${dim(`[1]`)}: `, (answer) => {
69
+ rl.close();
70
+ const trimmed = answer.trim();
71
+ if (trimmed === '') {
72
+ resolve(choices[0]);
73
+ return;
74
+ }
75
+ const num = parseInt(trimmed, 10);
76
+ if (isNaN(num) || num < 1 || num > choices.length) {
77
+ fail(`Invalid selection: ${trimmed}`);
78
+ process.exit(1);
79
+ }
80
+ resolve(choices[num - 1]);
81
+ });
82
+ });
83
+ }
84
+ /** Resolve the target: from --target flag, auto-detection, or prompt. */
85
+ async function resolveTarget(explicitTarget) {
86
+ if (explicitTarget) {
87
+ if (explicitTarget !== 'openclaw' && explicitTarget !== 'claude') {
88
+ fail(`Invalid --target: ${explicitTarget}. Must be 'openclaw' or 'claude'.`);
89
+ process.exit(1);
90
+ }
91
+ return explicitTarget;
92
+ }
93
+ const detected = detectTargets();
94
+ if (detected.length === 1) {
95
+ const t = detected[0];
96
+ if (autoConfirm) {
97
+ info(`Auto-detected: ${t}`);
98
+ return t;
99
+ }
100
+ const confirmed = await confirm(` Detected ${bold(t)}. Use it?`);
101
+ if (confirmed)
102
+ return t;
103
+ // User said no — prompt for choice
104
+ return promptTarget([]);
105
+ }
106
+ if (detected.length > 1) {
107
+ if (autoConfirm) {
108
+ fail('Multiple agent frameworks detected. Use --target to specify one.');
109
+ process.exit(1);
110
+ }
111
+ return promptTarget(detected);
112
+ }
113
+ // Nothing detected
114
+ if (autoConfirm) {
115
+ fail('No agent framework detected. Use --target to specify one.');
116
+ process.exit(1);
117
+ }
118
+ return promptTarget([]);
119
+ }
120
+ async function confirm(message) {
121
+ if (autoConfirm) {
122
+ console.log(`${message} ${dim('[y/N]')} y ${dim('(--yes)')}`);
123
+ return true;
124
+ }
125
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
126
+ return new Promise((resolve) => {
127
+ rl.question(`${message} ${dim('[y/N]')} `, (answer) => {
128
+ rl.close();
129
+ resolve(answer.trim().toLowerCase() === 'y');
130
+ });
131
+ });
132
+ }
133
+ async function promptGitRemote() {
134
+ if (autoConfirm) {
135
+ console.log(`\nEnable git versioning for memory? ${dim('[y/N]')} n ${dim('(--yes: skipped, use --git-remote to set)')}`);
136
+ return null;
137
+ }
138
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
139
+ return new Promise((resolve) => {
140
+ rl.question(`\nEnable git versioning for memory? ${dim('[y/N]')} `, (answer) => {
141
+ rl.close();
142
+ if (answer.trim().toLowerCase() !== 'y') {
143
+ resolve(null);
144
+ return;
145
+ }
146
+ const rl2 = readline.createInterface({ input: process.stdin, output: process.stdout });
147
+ rl2.question(`Git remote URL (e.g. git@github.com:user/memory.git): `, (url) => {
148
+ rl2.close();
149
+ const trimmed = url.trim();
150
+ resolve(trimmed || null);
151
+ });
152
+ });
153
+ });
154
+ }
155
+ function formatBytes(bytes) {
156
+ if (bytes < 1024)
157
+ return `${bytes} B`;
158
+ if (bytes < 1024 * 1024)
159
+ return `${(bytes / 1024).toFixed(1)} KiB`;
160
+ return `${(bytes / (1024 * 1024)).toFixed(1)} MiB`;
161
+ }
162
+ function formatTimestamp(iso) {
163
+ const d = new Date(iso);
164
+ return d.toLocaleString();
165
+ }
166
+ function promptCheckpointSelection(checkpoints) {
167
+ return new Promise((resolve) => {
168
+ console.log(`\n${bold('Available checkpoints:')}`);
169
+ for (let i = 0; i < checkpoints.length; i++) {
170
+ const c = checkpoints[i];
171
+ const label = i === 0 ? ' (latest)' : '';
172
+ console.log(` ${bold(`[${i + 1}]`)} ${formatTimestamp(c.timestamp)} \u2014 ${c.mode}${label}`);
173
+ }
174
+ console.log();
175
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
176
+ rl.question(`Select checkpoint to roll back ${dim(`[1]`)}: `, (answer) => {
177
+ rl.close();
178
+ const trimmed = answer.trim();
179
+ if (trimmed === '') {
180
+ resolve(checkpoints[0].id);
181
+ return;
182
+ }
183
+ const num = parseInt(trimmed, 10);
184
+ if (isNaN(num) || num < 1 || num > checkpoints.length) {
185
+ fail(`Invalid selection: ${trimmed}`);
186
+ process.exit(1);
187
+ }
188
+ resolve(checkpoints[num - 1].id);
189
+ });
190
+ });
191
+ }
192
+ // ── Migration progress display helpers ──────────────────────────
193
+ let fileCounter = 0;
194
+ let totalFileCount = 0;
195
+ function displayFileEvent(event) {
196
+ if (event.type === 'scan') {
197
+ totalFileCount = event.totalFiles ?? 0;
198
+ fileCounter = 0;
199
+ return;
200
+ }
201
+ if (event.type === 'retry') {
202
+ console.log(` ${yellow('\u21BB')} Retry ${event.attempt}/${event.maxAttempts}...`);
203
+ return;
204
+ }
205
+ fileCounter++;
206
+ console.log(` [${fileCounter}/${totalFileCount}] ${event.relativePath}`);
207
+ if (event.type === 'migrated') {
208
+ ok(' Uploaded \u2192 checkpointed \u2192 removed from disk');
209
+ }
210
+ else if (event.type === 'skipped') {
211
+ const reason = event.reason ?? 'empty';
212
+ info(` Skipped (${reason})`);
213
+ }
214
+ else if (event.type === 'error') {
215
+ fail(` Upload failed: ${event.error}`);
216
+ }
217
+ }
218
+ function displayMigrateSummary(result) {
219
+ const totalFiles = result.migrated + result.skipped + result.errors.length;
220
+ if (result.errors.length > 0) {
221
+ fail(`Migrated ${result.migrated} of ${totalFiles} file${totalFiles === 1 ? '' : 's'} (${result.errors.length} failed)`);
222
+ }
223
+ else if (result.migrated > 0) {
224
+ ok(`Migrated ${result.migrated} file${result.migrated === 1 ? '' : 's'} to engram`);
225
+ }
226
+ else {
227
+ info('No local memory files found to migrate');
228
+ }
229
+ if (result.skipped > 0) {
230
+ info(`Skipped ${result.skipped} file${result.skipped === 1 ? '' : 's'}`);
231
+ }
232
+ if (result.checkpointDir) {
233
+ info(`Checkpoint: ${result.checkpointDir}`);
234
+ }
235
+ }
236
+ async function runResumeMigration(clientOptions) {
237
+ step('Resuming incomplete migration...');
238
+ try {
239
+ const result = await resumeMigration(clientOptions, displayFileEvent);
240
+ console.log();
241
+ displayMigrateSummary(result);
242
+ }
243
+ catch (err) {
244
+ fail(`Resume failed: ${err.message}`);
245
+ process.exit(1);
246
+ }
247
+ console.log(`\nTo undo: ${bold('npx @engramx/client rollback')}`);
248
+ }
249
+ // ── Help text ───────────────────────────────────────────────────
250
+ function printUsage() {
251
+ console.log(`
252
+ ${bold('Usage:')}
253
+ npx @engramx/client pair <code> --engram <id> [options]
254
+ npx @engramx/client migrate --engram <id> [options]
255
+ npx @engramx/client rollback [checkpoint-id]
256
+ npx @engramx/client git-setup <remote-url>
257
+ npx @engramx/client git-log [file]
258
+ npx @engramx/client git-diff [ref] [--file <path>]
259
+ npx @engramx/client git-revert <ref> --engram <id> [--file <path>]
260
+ npx @engramx/client config <push|pull> --engram <id>
261
+ npx @engramx/client read <path> --engram <id> [--raw]
262
+
263
+ ${bold('Commands:')}
264
+ pair Generate session key and register as operator
265
+ migrate Migrate local memory to engram (or recover with --recover)
266
+ rollback Restore files from a checkpoint
267
+ git-setup Initialize git versioning for the memory directory
268
+ git-log Show commit history for the memory repo
269
+ git-diff Show changes since a commit (or working tree diff)
270
+ git-revert Revert memory to a previous commit and sync to canister
271
+ config Push/pull OpenClaw config to/from engram
272
+ read Read a memory file (use --raw to see raw stored bytes)
273
+
274
+ ${bold('Options (pair):')}
275
+ --engram <id> Engram canister ID (required)
276
+ --target <openclaw|claude> Agent framework (auto-detected if omitted)
277
+ --install Write Claude MCP config to settings file
278
+ --host <url> ICP host (default: https://ic0.app)
279
+ --key-path <path> Session key path (default: ~/.engramx/session.key)
280
+ --git-remote <url> Enable git versioning and set remote
281
+ --force Overwrite existing session key
282
+ --yes, -y Auto-confirm all prompts (non-interactive mode)
283
+
284
+ ${bold('Options (migrate):')}
285
+ --engram <id> Engram canister ID (required)
286
+ --recover Recovery mode: download memory from engram to local
287
+ --resume Resume an incomplete migration
288
+ --dry-run Show what would be migrated without uploading
289
+ --host <url> ICP host (default: https://ic0.app)
290
+ --key-path <path> Session key path (default: ~/.engramx/session.key)
291
+
292
+ ${bold('Options (git-revert):')}
293
+ --engram <id> Engram canister ID (required)
294
+ --file <path> Revert a single file instead of the entire memory dir
295
+ --host <url> ICP host (default: https://ic0.app)
296
+ --key-path <path> Session key path (default: ~/.engramx/session.key)
297
+
298
+ ${bold('Options (read):')}
299
+ --raw Show raw stored bytes as hex instead of decoded text
300
+
301
+ --help Show this help message
302
+ `);
303
+ }
304
+ async function main() {
305
+ const { values, positionals } = parseArgs({
306
+ allowPositionals: true,
307
+ options: {
308
+ engram: { type: 'string' },
309
+ target: { type: 'string' },
310
+ install: { type: 'boolean', default: false },
311
+ recover: { type: 'boolean', default: false },
312
+ resume: { type: 'boolean', default: false },
313
+ 'dry-run': { type: 'boolean', default: false },
314
+ host: { type: 'string' },
315
+ 'key-path': { type: 'string' },
316
+ 'git-remote': { type: 'string' },
317
+ force: { type: 'boolean', default: false },
318
+ yes: { type: 'boolean', short: 'y', default: false },
319
+ file: { type: 'string' },
320
+ raw: { type: 'boolean', default: false },
321
+ help: { type: 'boolean', default: false },
322
+ },
323
+ });
324
+ if (values.help || positionals.length === 0) {
325
+ printUsage();
326
+ process.exit(values.help ? 0 : 1);
327
+ }
328
+ const command = positionals[0];
329
+ autoConfirm = values.yes;
330
+ if (values.engram) {
331
+ try {
332
+ Principal.fromText(values.engram);
333
+ }
334
+ catch {
335
+ fail(`Invalid canister ID: ${values.engram}`);
336
+ process.exit(1);
337
+ }
338
+ }
339
+ // ── config command ──────────────────────────────────────────
340
+ if (command === 'config') {
341
+ const subcommand = positionals[1];
342
+ if (!subcommand || (subcommand !== 'push' && subcommand !== 'pull')) {
343
+ fail('Usage: npx @engramx/client config <push|pull> --engram <id>');
344
+ process.exit(1);
345
+ }
346
+ if (!values.engram) {
347
+ fail('Missing --engram <canister-id>');
348
+ process.exit(1);
349
+ }
350
+ const keyPath = values['key-path'] || '~/.engramx/session.key';
351
+ const host = values.host || 'https://ic0.app';
352
+ const client = new EngramClient({ canisterId: values.engram, sessionKeyPath: keyPath, host });
353
+ if (subcommand === 'push') {
354
+ const configPath = positionals[2] || '~/.openclaw/openclaw.json';
355
+ const resolvedPath = configPath.replace(/^~/, process.env['HOME'] || '');
356
+ if (!fs.existsSync(resolvedPath)) {
357
+ fail(`Config file not found: ${resolvedPath}`);
358
+ process.exit(1);
359
+ }
360
+ const content = fs.readFileSync(resolvedPath, 'utf-8');
361
+ step(`Pushing config to engram...`);
362
+ try {
363
+ // Owner-only: writeMemory + config/* path require owner credentials.
364
+ // Use raw actor since the SDK doesn't expose writeMemory.
365
+ const ciphertext = new TextEncoder().encode(content);
366
+ const result = await client.actor.writeMemory('config/openclaw.json', ciphertext);
367
+ if ('Err' in result)
368
+ throw new Error(result.Err);
369
+ ok(`Config pushed (version ${result.Ok})`);
370
+ }
371
+ catch (err) {
372
+ fail(`Push failed: ${err.message}`);
373
+ process.exit(1);
374
+ }
375
+ }
376
+ else {
377
+ step('Pulling config from engram...');
378
+ try {
379
+ const file = await client.readMemory('config/openclaw.json');
380
+ console.log(file.content);
381
+ }
382
+ catch (err) {
383
+ fail(`Pull failed: ${err.message}`);
384
+ process.exit(1);
385
+ }
386
+ }
387
+ return;
388
+ }
389
+ // ── read command (debug) ─────────────────────────────────────
390
+ if (command === 'read') {
391
+ const filePath = positionals[1];
392
+ if (!filePath) {
393
+ fail('Usage: npx @engramx/client read <path> --engram <id> [--raw]');
394
+ process.exit(1);
395
+ }
396
+ if (!values.engram) {
397
+ fail('Missing --engram <canister-id>');
398
+ process.exit(1);
399
+ }
400
+ const keyPath = values['key-path'] || '~/.engramx/session.key';
401
+ const host = values.host || 'https://ic0.app';
402
+ const client = new EngramClient({ canisterId: values.engram, sessionKeyPath: keyPath, host });
403
+ try {
404
+ if (values.raw) {
405
+ console.warn('WARNING: --raw shows the raw stored bytes (content is stored as plaintext).');
406
+ // Show raw stored bytes as hex (no decode)
407
+ const result = await client.actor.readMemory(filePath);
408
+ if ('Err' in result) {
409
+ fail(result.Err);
410
+ process.exit(1);
411
+ }
412
+ const raw = result.Ok;
413
+ const bytes = new Uint8Array(raw.content);
414
+ console.log(`Path: ${raw.path}`);
415
+ console.log(`Version: ${raw.version}`);
416
+ console.log(`Size: ${bytes.length} bytes (raw stored, plaintext)`);
417
+ console.log(`Hex: ${Buffer.from(bytes).toString('hex').slice(0, 200)}...`);
418
+ }
419
+ else {
420
+ const file = await client.readMemory(filePath);
421
+ console.log(file.content);
422
+ }
423
+ }
424
+ catch (err) {
425
+ fail(`Read failed: ${err.message}`);
426
+ process.exit(1);
427
+ }
428
+ return;
429
+ }
430
+ // ── rollback command ──────────────────────────────────────────
431
+ if (command === 'rollback') {
432
+ const checkpoints = listCheckpoints();
433
+ if (checkpoints.length === 0) {
434
+ fail('No checkpoints to roll back. Run "npx @engramx/client pair" first to create a checkpoint.');
435
+ process.exit(1);
436
+ }
437
+ let targetId;
438
+ const explicitId = positionals[1];
439
+ if (explicitId) {
440
+ // Direct checkpoint ID passed as argument
441
+ if (!checkpoints.find((c) => c.id === explicitId)) {
442
+ fail(`Checkpoint not found: ${explicitId}`);
443
+ process.exit(1);
444
+ }
445
+ targetId = explicitId;
446
+ }
447
+ else if (checkpoints.length === 1) {
448
+ targetId = checkpoints[0].id;
449
+ }
450
+ else {
451
+ // Interactive selector
452
+ targetId = await promptCheckpointSelection(checkpoints);
453
+ }
454
+ try {
455
+ const result = rollbackCheckpoint(targetId);
456
+ // Show per-file restore results
457
+ for (const file of result.restoredFiles) {
458
+ const basename = file.path.split('/').pop() || file.path;
459
+ if (file.status === 'restored') {
460
+ ok(`Restored ${basename} ${dim(`\u2192 ${file.path}`)}`);
461
+ }
462
+ else if (file.status === 'skipped') {
463
+ info(`Skipped ${basename} (not found in checkpoint)`);
464
+ }
465
+ else {
466
+ fail(`Failed to restore ${basename}: ${file.error}`);
467
+ }
468
+ }
469
+ const restoredCount = result.restoredFiles.filter((f) => f.status === 'restored').length;
470
+ if (result.partial) {
471
+ const failedCount = result.restoredFiles.filter((f) => f.status !== 'restored').length;
472
+ fail(`Partial rollback: ${failedCount} file${failedCount === 1 ? '' : 's'} could not be restored (see errors above)`);
473
+ info('Checkpoint preserved for retry.');
474
+ }
475
+ else {
476
+ const dbList = result.restoredDatabases.length > 0
477
+ ? ` + databases: ${result.restoredDatabases.join(', ')}`
478
+ : '';
479
+ ok(`Rolled back ${restoredCount} file${restoredCount === 1 ? '' : 's'}${dbList}`);
480
+ info(`(from checkpoint ${formatTimestamp(result.checkpointTimestamp)})`);
481
+ }
482
+ const remaining = listCheckpoints();
483
+ if (remaining.length > 0) {
484
+ console.log(dim(`${remaining.length} older checkpoint${remaining.length === 1 ? '' : 's'} still available.`));
485
+ }
486
+ }
487
+ catch (err) {
488
+ fail(err.message);
489
+ process.exit(1);
490
+ }
491
+ return;
492
+ }
493
+ // ── git-log command ──────────────────────────────────────────
494
+ if (command === 'git-log') {
495
+ const memoryDir = getMemoryDir();
496
+ if (!isGitRepo(memoryDir)) {
497
+ fail('Git versioning not set up. Run: npx @engramx/client git-setup <url>');
498
+ process.exit(1);
499
+ }
500
+ const filePath = positionals[1] || values.file;
501
+ const args = ['log', '--oneline', '--no-decorate', '-30'];
502
+ if (filePath)
503
+ args.push('--', filePath);
504
+ try {
505
+ const output = execFileSync('git', args, { cwd: memoryDir, stdio: 'pipe' }).toString().trim();
506
+ if (output) {
507
+ console.log(output);
508
+ }
509
+ else {
510
+ info('No commits yet.');
511
+ }
512
+ }
513
+ catch {
514
+ info('No commits yet.');
515
+ }
516
+ return;
517
+ }
518
+ // ── git-diff command ────────────────────────────────────────
519
+ if (command === 'git-diff') {
520
+ const memoryDir = getMemoryDir();
521
+ if (!isGitRepo(memoryDir)) {
522
+ fail('Git versioning not set up. Run: npx @engramx/client git-setup <url>');
523
+ process.exit(1);
524
+ }
525
+ const ref = positionals[1];
526
+ const filePath = values.file;
527
+ const args = ['diff'];
528
+ if (ref)
529
+ args.push(ref);
530
+ if (filePath)
531
+ args.push('--', filePath);
532
+ try {
533
+ const output = execFileSync('git', args, { cwd: memoryDir, stdio: 'pipe' }).toString().trim();
534
+ if (output) {
535
+ console.log(output);
536
+ }
537
+ else {
538
+ info('No changes.');
539
+ }
540
+ }
541
+ catch (err) {
542
+ fail(`git diff failed: ${err.message}`);
543
+ process.exit(1);
544
+ }
545
+ return;
546
+ }
547
+ // ── git-revert command ──────────────────────────────────────
548
+ if (command === 'git-revert') {
549
+ const ref = positionals[1];
550
+ if (!ref) {
551
+ fail('Usage: npx @engramx/client git-revert <commit-or-ref> --engram <id> [--file <path>]');
552
+ process.exit(1);
553
+ }
554
+ if (!values.engram) {
555
+ fail('Missing --engram <canister-id> (needed to sync reverted files to canister)');
556
+ process.exit(1);
557
+ }
558
+ const memoryDir = getMemoryDir();
559
+ if (!isGitRepo(memoryDir)) {
560
+ fail('Git versioning not set up. Run: npx @engramx/client git-setup <url>');
561
+ process.exit(1);
562
+ }
563
+ const filePath = values.file;
564
+ const keyPath = values['key-path'] || '~/.engramx/session.key';
565
+ const host = values.host || 'https://ic0.app';
566
+ // 1. Git checkout to revert files
567
+ step(`Reverting to ${ref}${filePath ? ` (file: ${filePath})` : ''}...`);
568
+ try {
569
+ const checkoutArgs = ['checkout', ref, '--'];
570
+ if (filePath) {
571
+ checkoutArgs.push(filePath);
572
+ }
573
+ else {
574
+ checkoutArgs.push('.');
575
+ }
576
+ execFileSync('git', checkoutArgs, { cwd: memoryDir, stdio: 'pipe' });
577
+ }
578
+ catch (err) {
579
+ fail(`git checkout failed: ${err.message}`);
580
+ process.exit(1);
581
+ }
582
+ // 2. Commit the revert
583
+ try {
584
+ execFileSync('git', ['add', '-A'], { cwd: memoryDir, stdio: 'pipe' });
585
+ const status = execFileSync('git', ['diff', '--cached', '--stat'], {
586
+ cwd: memoryDir,
587
+ stdio: 'pipe',
588
+ })
589
+ .toString()
590
+ .trim();
591
+ if (status) {
592
+ const msg = filePath
593
+ ? `engramx: revert ${filePath} to ${ref}`
594
+ : `engramx: revert to ${ref}`;
595
+ execFileSync('git', ['commit', '-m', msg], { cwd: memoryDir, stdio: 'pipe' });
596
+ ok('Committed revert to git');
597
+ }
598
+ else {
599
+ info('No changes to commit (already at target state)');
600
+ }
601
+ }
602
+ catch (err) {
603
+ const msg = err instanceof Error ? err.message : String(err);
604
+ if (!msg.includes('nothing to commit')) {
605
+ warn(`Git commit failed: ${msg}`);
606
+ }
607
+ }
608
+ // 3. Sync reverted files to canister
609
+ step('Syncing reverted files to engram canister...');
610
+ const client = new EngramClient({ canisterId: values.engram, sessionKeyPath: keyPath, host });
611
+ let syncCount = 0;
612
+ let syncErrors = 0;
613
+ async function syncDir(dir) {
614
+ if (!fs.existsSync(dir))
615
+ return;
616
+ const entries = fs.readdirSync(dir, { withFileTypes: true });
617
+ for (const entry of entries) {
618
+ const fullPath = `${dir}/${entry.name}`;
619
+ if (entry.isDirectory()) {
620
+ if (entry.name === '.git')
621
+ continue;
622
+ await syncDir(fullPath);
623
+ }
624
+ else if (entry.isFile() && entry.name.endsWith('.md')) {
625
+ const relativePath = path.relative(memoryDir, fullPath);
626
+ if (relativePath.startsWith('..'))
627
+ continue; // skip files outside memoryDir
628
+ const content = fs.readFileSync(fullPath, 'utf-8');
629
+ try {
630
+ // Operators can append (creates files on first sync, accumulates
631
+ // content on subsequent runs — preserves the append-only invariant).
632
+ await client.appendMemory(relativePath, content);
633
+ ok(` ${relativePath}`);
634
+ syncCount++;
635
+ }
636
+ catch (err) {
637
+ fail(` ${relativePath}: ${err.message}`);
638
+ syncErrors++;
639
+ }
640
+ }
641
+ }
642
+ }
643
+ await syncDir(memoryDir);
644
+ if (syncErrors > 0) {
645
+ fail(`Synced ${syncCount} file(s), ${syncErrors} failed`);
646
+ }
647
+ else if (syncCount > 0) {
648
+ ok(`Synced ${syncCount} file${syncCount === 1 ? '' : 's'} to engram`);
649
+ }
650
+ else {
651
+ info('No .md files to sync');
652
+ }
653
+ // 4. Push to remote if configured
654
+ try {
655
+ execFileSync('git', ['push'], { cwd: memoryDir, stdio: 'pipe' });
656
+ ok('Pushed to remote');
657
+ }
658
+ catch (err) {
659
+ const msg = err instanceof Error ? err.message : String(err);
660
+ warn(`Git push skipped: ${msg.split('\n')[0]}`);
661
+ }
662
+ return;
663
+ }
664
+ // ── git-setup command ────────────────────────────────────────
665
+ if (command === 'git-setup') {
666
+ const remoteUrl = positionals[1];
667
+ if (!remoteUrl) {
668
+ fail('Missing remote URL. Usage: npx @engramx/client git-setup <remote-url>');
669
+ process.exit(1);
670
+ }
671
+ step('Setting up git versioning...');
672
+ try {
673
+ const gitResult = setupGitVersioning(remoteUrl);
674
+ ok(`Git repo initialized ${dim(`→ ${gitResult.memoryDir}`)}`);
675
+ ok(`Remote "origin" set to ${dim(remoteUrl)}`);
676
+ if (gitResult.existingRepo) {
677
+ info('Using existing git repo');
678
+ }
679
+ }
680
+ catch (err) {
681
+ fail(`Git setup failed: ${err.message}`);
682
+ process.exit(1);
683
+ }
684
+ return;
685
+ }
686
+ // ── migrate command ─────────────────────────────────────────
687
+ if (command === 'migrate') {
688
+ if (!values.engram) {
689
+ fail('Missing --engram <canister-id>');
690
+ process.exit(1);
691
+ }
692
+ const keyPath = values['key-path'] || '~/.engramx/session.key';
693
+ const host = values.host || 'https://ic0.app';
694
+ // Pre-flight: require valid pairing
695
+ const clientOptions = {
696
+ canisterId: values.engram,
697
+ sessionKeyPath: keyPath,
698
+ host,
699
+ dryRun: values['dry-run'],
700
+ };
701
+ if (!values['dry-run']) {
702
+ const paired = await hasValidPairing(clientOptions);
703
+ if (!paired) {
704
+ fail("No valid pairing found. Run 'npx @engramx/client pair' first.");
705
+ process.exit(1);
706
+ }
707
+ }
708
+ // Flag validation
709
+ if (values.resume && values['dry-run']) {
710
+ fail('Cannot combine --resume with --dry-run');
711
+ process.exit(1);
712
+ }
713
+ if (values.recover) {
714
+ // ── Recovery mode ───────────────────────────────────────
715
+ step('Recovering from engram...');
716
+ info('This will checkpoint local state and download memory from engram.');
717
+ if (!(await confirm(' Continue?'))) {
718
+ info('Recovery cancelled.');
719
+ process.exit(0);
720
+ }
721
+ info('Checkpointing local state before recovery...');
722
+ try {
723
+ const result = await recoverOpenClaw(clientOptions);
724
+ ok(`Checkpointed local state ${dim(`\u2192 ${result.checkpointDir}`)}`);
725
+ if (result.restoredBackups.length === 0) {
726
+ info('No database backups found on engram');
727
+ }
728
+ for (const backup of result.restoredBackups) {
729
+ ok(`Restored database: ${backup.dbType} ${dim(`(${backup.backupId}, ${formatBytes(backup.size)})`)}`);
730
+ }
731
+ for (const error of result.errors) {
732
+ fail(`Recovery: ${error}`);
733
+ }
734
+ }
735
+ catch (err) {
736
+ fail(`Recovery failed: ${err.message}`);
737
+ process.exit(1);
738
+ }
739
+ console.log(`\nTo undo: ${bold('npx @engramx/client rollback')}`);
740
+ console.log(`Restart OpenClaw \u2014 memory files will sync automatically.`);
741
+ return;
742
+ }
743
+ if (values.resume) {
744
+ // ── Resume mode ─────────────────────────────────────────
745
+ await runResumeMigration(clientOptions);
746
+ return;
747
+ }
748
+ // ── Default: check for incomplete checkpoint ────────────
749
+ const incomplete = findIncompleteCheckpoint();
750
+ if (incomplete && !values['dry-run']) {
751
+ info(`Found incomplete migration: ${incomplete.migratedCount} migrated, ${incomplete.remainingCount} remaining.`);
752
+ info(`Checkpoint: ${incomplete.checkpoint.id}`);
753
+ const shouldResume = await confirm(' Resume incomplete migration?');
754
+ if (shouldResume) {
755
+ await runResumeMigration(clientOptions);
756
+ return;
757
+ }
758
+ // User chose not to resume — start fresh
759
+ }
760
+ // ── Migration (fresh or dry-run) ────────────────────────
761
+ if (values['dry-run']) {
762
+ step('Dry run: scanning files that would be migrated...');
763
+ }
764
+ else {
765
+ step('Migrating local memory to engram...');
766
+ info('This will checkpoint local state, upload each file, and remove migrated originals.');
767
+ if (!(await confirm(' Continue?'))) {
768
+ info('Migration cancelled.');
769
+ process.exit(0);
770
+ }
771
+ }
772
+ try {
773
+ const result = await migrateOpenClawMemory(clientOptions, displayFileEvent);
774
+ console.log();
775
+ displayMigrateSummary(result);
776
+ }
777
+ catch (err) {
778
+ fail(`Migration failed: ${err.message}`);
779
+ process.exit(1);
780
+ }
781
+ if (!values['dry-run']) {
782
+ console.log(`\nTo undo: ${bold('npx @engramx/client rollback')}`);
783
+ }
784
+ return;
785
+ }
786
+ // ── pair command ──────────────────────────────────────────────
787
+ if (command !== 'pair') {
788
+ fail(`Unknown command: ${command}`);
789
+ printUsage();
790
+ process.exit(1);
791
+ }
792
+ // Error if --recover is passed to pair (moved to migrate)
793
+ if (values.recover) {
794
+ fail('The --recover flag has moved to the migrate command. Use: npx @engramx/client migrate --engram <id> --recover');
795
+ process.exit(1);
796
+ }
797
+ const inviteCode = positionals[1];
798
+ if (!inviteCode) {
799
+ fail('Missing invite code. Usage: npx @engramx/client pair <code> --engram <id>');
800
+ process.exit(1);
801
+ }
802
+ if (!values.engram) {
803
+ fail('Missing --engram <canister-id>');
804
+ process.exit(1);
805
+ }
806
+ const keyPath = values['key-path'] || '~/.engramx/session.key';
807
+ const host = values.host || 'https://ic0.app';
808
+ // Resolve target framework
809
+ const target = await resolveTarget(values.target);
810
+ console.log(`\n${bold('EngramX Pairing')}`);
811
+ info(`Engram: ${values.engram}`);
812
+ info(`Host: ${host}`);
813
+ info(`Key path: ${keyPath}`);
814
+ info(`Target: ${target}`);
815
+ // Step 1: Pair (generate key + register operator)
816
+ step('Generating session key and registering operator...');
817
+ info(`This will write: ${keyPath}`);
818
+ if (!(await confirm(' Continue?'))) {
819
+ console.log('\nPairing cancelled.');
820
+ process.exit(0);
821
+ }
822
+ let pairResult;
823
+ try {
824
+ pairResult = await pair({
825
+ inviteCode,
826
+ canisterId: values.engram,
827
+ host: values.host,
828
+ keyPath,
829
+ force: values.force,
830
+ });
831
+ ok(`Generated session key ${dim(`\u2192 ${pairResult.keyPath}`)}`);
832
+ ok(`Registered as operator ${dim(`(principal: ${pairResult.principal})`)}`);
833
+ }
834
+ catch (err) {
835
+ if (err instanceof PairError) {
836
+ fail(err.message);
837
+ }
838
+ else {
839
+ fail(`Unexpected error: ${err.message}`);
840
+ }
841
+ process.exit(1);
842
+ }
843
+ // Step 2: OpenClaw setup
844
+ if (target === 'openclaw') {
845
+ step('Installing OpenClaw plugin...');
846
+ info('This will modify: ~/.openclaw/openclaw.json');
847
+ let skipOpenClaw = false;
848
+ if (!(await confirm(' Continue?'))) {
849
+ info('Skipping OpenClaw plugin setup.');
850
+ skipOpenClaw = true;
851
+ }
852
+ if (!skipOpenClaw)
853
+ try {
854
+ const setupResult = await setupOpenClaw({
855
+ canisterId: values.engram,
856
+ sessionKeyPath: keyPath,
857
+ host: values.host,
858
+ });
859
+ ok(`Installed EngramX plugin for OpenClaw`);
860
+ ok(`Updated ${dim(setupResult.configPath)}`);
861
+ }
862
+ catch (err) {
863
+ if (err instanceof OpenClawNotFoundError) {
864
+ fail(err.message);
865
+ console.log(`\n${dim('Manual setup:')}`);
866
+ console.log(dim(' 1. npm install @engramx/openclaw'));
867
+ console.log(dim(' 2. Add to openclaw.json: { "plugins": { "slots": { "memory": "memory-engramx" } } }'));
868
+ }
869
+ else {
870
+ fail(`OpenClaw setup failed: ${err.message}`);
871
+ }
872
+ process.exit(1);
873
+ }
874
+ }
875
+ // Step 2b: Claude setup
876
+ if (target === 'claude') {
877
+ step('Configuring Claude MCP integration...');
878
+ const mcpConfig = generateMcpConfig({
879
+ canisterId: values.engram,
880
+ sessionKeyPath: keyPath,
881
+ host: values.host,
882
+ });
883
+ if (values.install) {
884
+ const paths = claudeConfigPaths();
885
+ // Install to Claude Code settings
886
+ try {
887
+ installMcpConfig(paths.claudeCode, {
888
+ canisterId: values.engram,
889
+ sessionKeyPath: keyPath,
890
+ host: values.host,
891
+ });
892
+ ok(`Updated ${dim(paths.claudeCode)}`);
893
+ }
894
+ catch (err) {
895
+ warn(`Could not write to ${paths.claudeCode}: ${err.message}`);
896
+ }
897
+ // Install to Claude Desktop settings
898
+ try {
899
+ installMcpConfig(paths.claudeDesktop, {
900
+ canisterId: values.engram,
901
+ sessionKeyPath: keyPath,
902
+ host: values.host,
903
+ });
904
+ ok(`Updated ${dim(paths.claudeDesktop)}`);
905
+ }
906
+ catch (err) {
907
+ warn(`Could not write to ${paths.claudeDesktop}: ${err.message}`);
908
+ }
909
+ info('Restart Claude Code / Cowork for the changes to take effect.');
910
+ }
911
+ else {
912
+ ok('MCP config generated. Add this to your Claude config:\n');
913
+ const paths = claudeConfigPaths();
914
+ info(`Claude Code: ${paths.claudeCode}`);
915
+ info(`Claude Cowork: ${paths.claudeDesktop}`);
916
+ console.log();
917
+ console.log(JSON.stringify(mcpConfig, null, 2));
918
+ console.log();
919
+ info('Or re-run with --install to write it automatically.');
920
+ }
921
+ }
922
+ // Step 3: Git versioning
923
+ let gitRemoteUrl = values['git-remote'] || null;
924
+ if (gitRemoteUrl) {
925
+ step('Setting up git versioning...');
926
+ info(`Remote: ${gitRemoteUrl}`);
927
+ if (!(await confirm(' Continue?'))) {
928
+ info('Skipping git versioning setup.');
929
+ gitRemoteUrl = null;
930
+ }
931
+ }
932
+ else {
933
+ gitRemoteUrl = await promptGitRemote();
934
+ }
935
+ if (gitRemoteUrl) {
936
+ if (!values['git-remote'])
937
+ step('Setting up git versioning...');
938
+ try {
939
+ const gitResult = setupGitVersioning(gitRemoteUrl);
940
+ ok(`Git repo initialized ${dim(`→ ${gitResult.memoryDir}`)}`);
941
+ ok(`Remote "origin" set to ${dim(gitRemoteUrl)}`);
942
+ if (gitResult.existingRepo) {
943
+ info('Using existing git repo');
944
+ }
945
+ // Stage local memory files into the git-tracked memory dir so the
946
+ // baseline commit captures actual content (not just .gitignore).
947
+ try {
948
+ if (target === 'openclaw') {
949
+ const openclawDir = getOpenClawDir();
950
+ const ocConfig = (() => {
951
+ const p = path.join(openclawDir, 'openclaw.json');
952
+ try {
953
+ return JSON.parse(fs.readFileSync(p, 'utf-8'));
954
+ }
955
+ catch {
956
+ return {};
957
+ }
958
+ })();
959
+ const agentsCfg = ocConfig['agents'];
960
+ const defaultsCfg = agentsCfg?.['defaults'];
961
+ const workspaceCfg = defaultsCfg?.['workspace'];
962
+ const workspace = workspaceCfg
963
+ ? path.isAbsolute(workspaceCfg)
964
+ ? workspaceCfg
965
+ : path.join(openclawDir, workspaceCfg)
966
+ : path.join(openclawDir, 'workspace');
967
+ // Copy workspace .md files into memoryDir (matching canister paths)
968
+ if (fs.existsSync(workspace)) {
969
+ for (const entry of fs.readdirSync(workspace, { withFileTypes: true })) {
970
+ if (!entry.isFile() || !entry.name.endsWith('.md'))
971
+ continue;
972
+ if (entry.name === 'BOOTSTRAP.md')
973
+ continue;
974
+ const src = path.join(workspace, entry.name);
975
+ const dest = path.join(gitResult.memoryDir, entry.name);
976
+ fs.copyFileSync(src, dest);
977
+ }
978
+ }
979
+ // Also copy top-level MEMORY.md if it exists
980
+ const topMemory = path.join(openclawDir, 'MEMORY.md');
981
+ if (fs.existsSync(topMemory)) {
982
+ fs.copyFileSync(topMemory, path.join(gitResult.memoryDir, 'MEMORY.md'));
983
+ }
984
+ }
985
+ execFileSync('git', ['add', '-A'], { cwd: gitResult.memoryDir, stdio: 'pipe' });
986
+ const status = execFileSync('git', ['diff', '--cached', '--stat'], {
987
+ cwd: gitResult.memoryDir,
988
+ stdio: 'pipe',
989
+ })
990
+ .toString()
991
+ .trim();
992
+ if (status) {
993
+ execFileSync('git', ['commit', '-m', 'engramx: pre-migration baseline'], {
994
+ cwd: gitResult.memoryDir,
995
+ stdio: 'pipe',
996
+ });
997
+ ok('Committed pre-migration baseline to git');
998
+ try {
999
+ const branch = execFileSync('git', ['branch', '--show-current'], {
1000
+ cwd: gitResult.memoryDir,
1001
+ stdio: 'pipe',
1002
+ })
1003
+ .toString()
1004
+ .trim() || 'main';
1005
+ try {
1006
+ execFileSync('git', ['pull', '--rebase', 'origin', branch], {
1007
+ cwd: gitResult.memoryDir,
1008
+ stdio: 'pipe',
1009
+ });
1010
+ }
1011
+ catch {
1012
+ // pull may fail if remote branch doesn't exist yet — that's fine
1013
+ }
1014
+ execFileSync('git', ['push', '-u', 'origin', branch], {
1015
+ cwd: gitResult.memoryDir,
1016
+ stdio: 'pipe',
1017
+ });
1018
+ ok('Pushed baseline to remote');
1019
+ }
1020
+ catch (pushErr) {
1021
+ const stderr = pushErr?.stderr?.toString() ?? '';
1022
+ const reason = (stderr
1023
+ .split('\n')
1024
+ .find((l) => l.includes('[rejected]') || l.includes('error:')) ??
1025
+ stderr.split('\n').slice(0, 3).join(' ').trim()) ||
1026
+ pushErr?.message ||
1027
+ 'unknown error';
1028
+ warn(`Git push failed (will retry on next sync): ${reason}`);
1029
+ }
1030
+ }
1031
+ }
1032
+ catch (gitErr) {
1033
+ // git commit is best-effort — don't block pairing
1034
+ warn(`Git baseline commit skipped: ${gitErr?.message || gitErr}`);
1035
+ }
1036
+ }
1037
+ catch (err) {
1038
+ fail(`Git setup failed: ${err.message}`);
1039
+ info('You can set this up manually later. See: npx @engramx/client git-setup <url>');
1040
+ }
1041
+ }
1042
+ // Step 4: Migration
1043
+ {
1044
+ const clientOptions = {
1045
+ canisterId: values.engram,
1046
+ sessionKeyPath: keyPath,
1047
+ host: values.host,
1048
+ };
1049
+ step('Migrating local memory to engram...');
1050
+ info('This will checkpoint local state, upload each file, and remove migrated originals.');
1051
+ if (await confirm(' Continue?')) {
1052
+ try {
1053
+ const migrateResult = await migrateOpenClawMemory(clientOptions, displayFileEvent);
1054
+ console.log();
1055
+ displayMigrateSummary(migrateResult);
1056
+ }
1057
+ catch (err) {
1058
+ fail(`Migration failed: ${err.message}`);
1059
+ process.exit(1);
1060
+ }
1061
+ console.log(`\nTo undo: ${bold('npx @engramx/client rollback')}`);
1062
+ if (gitRemoteUrl) {
1063
+ console.log(`Memory changes will be auto-committed and pushed to GitHub.`);
1064
+ }
1065
+ else {
1066
+ info('Tip: Add git versioning with npx @engramx/client git-setup <url>');
1067
+ }
1068
+ }
1069
+ else {
1070
+ info(`Run 'npx @engramx/client migrate --engram ${values.engram}' to migrate later.`);
1071
+ }
1072
+ }
1073
+ }
1074
+ main().catch((err) => {
1075
+ fail(`Fatal: ${err.message}`);
1076
+ process.exit(1);
1077
+ });
1078
+ //# sourceMappingURL=cli.js.map