@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.
- package/LICENSE +21 -0
- package/README.md +116 -0
- package/dist/EngramClient.d.ts +204 -0
- package/dist/EngramClient.js +1168 -0
- package/dist/EngramClient.js.map +1 -0
- package/dist/ProfileManager.d.ts +61 -0
- package/dist/ProfileManager.js +116 -0
- package/dist/ProfileManager.js.map +1 -0
- package/dist/SessionManager.d.ts +46 -0
- package/dist/SessionManager.js +114 -0
- package/dist/SessionManager.js.map +1 -0
- package/dist/agent-skill.d.ts +150 -0
- package/dist/agent-skill.js +152 -0
- package/dist/agent-skill.js.map +1 -0
- package/dist/auth.d.ts +17 -0
- package/dist/auth.js +65 -0
- package/dist/auth.js.map +1 -0
- package/dist/bootstrap.d.ts +21 -0
- package/dist/bootstrap.js +71 -0
- package/dist/bootstrap.js.map +1 -0
- package/dist/cache.d.ts +11 -0
- package/dist/cache.js +41 -0
- package/dist/cache.js.map +1 -0
- package/dist/claude-setup.d.ts +29 -0
- package/dist/claude-setup.js +57 -0
- package/dist/claude-setup.js.map +1 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +1078 -0
- package/dist/cli.js.map +1 -0
- package/dist/encryption.d.ts +44 -0
- package/dist/encryption.js +108 -0
- package/dist/encryption.js.map +1 -0
- package/dist/index.d.ts +36 -0
- package/dist/index.js +23 -0
- package/dist/index.js.map +1 -0
- package/dist/migrate.d.ts +106 -0
- package/dist/migrate.js +882 -0
- package/dist/migrate.js.map +1 -0
- package/dist/offline.d.ts +21 -0
- package/dist/offline.js +90 -0
- package/dist/offline.js.map +1 -0
- package/dist/openclaw-setup.d.ts +24 -0
- package/dist/openclaw-setup.js +249 -0
- package/dist/openclaw-setup.js.map +1 -0
- package/dist/pair.d.ts +20 -0
- package/dist/pair.js +68 -0
- package/dist/pair.js.map +1 -0
- package/dist/path-utils.d.ts +16 -0
- package/dist/path-utils.js +29 -0
- package/dist/path-utils.js.map +1 -0
- package/dist/schema.d.ts +54 -0
- package/dist/schema.js +101 -0
- package/dist/schema.js.map +1 -0
- package/dist/types.d.ts +452 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- 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
|