@wipcomputer/memory-crystal 0.7.34-alpha.1 → 0.7.34-alpha.3

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/dist/file-sync.js CHANGED
@@ -1,13 +1,411 @@
1
+ // src/crypto.ts
2
+ import { readFileSync as readFileSync2, existsSync as existsSync2 } from "fs";
3
+ import { createCipheriv, createDecipheriv, createHmac, randomBytes, hkdfSync } from "crypto";
4
+
5
+ // src/ldm.ts
6
+ import { existsSync, mkdirSync, readFileSync, writeFileSync, copyFileSync, chmodSync, readdirSync } from "fs";
7
+ import { join, dirname } from "path";
8
+ import { execSync } from "child_process";
9
+ import { fileURLToPath } from "url";
10
+ var HOME = process.env.HOME || "";
11
+ var LDM_ROOT = join(HOME, ".ldm");
12
+ function loadAgentConfig(id) {
13
+ const cfgPath = join(LDM_ROOT, "agents", id, "config.json");
14
+ try {
15
+ if (existsSync(cfgPath)) return JSON.parse(readFileSync(cfgPath, "utf-8"));
16
+ } catch {
17
+ }
18
+ return null;
19
+ }
20
+ function getAgentId(harnessHint) {
21
+ if (process.env.CRYSTAL_AGENT_ID) return process.env.CRYSTAL_AGENT_ID;
22
+ const agentsDir = join(LDM_ROOT, "agents");
23
+ if (existsSync(agentsDir)) {
24
+ try {
25
+ for (const d of readdirSync(agentsDir)) {
26
+ const cfg = loadAgentConfig(d);
27
+ if (!cfg || !cfg.agentId) continue;
28
+ if (!harnessHint) return cfg.agentId;
29
+ if (harnessHint === "claude-code" && cfg.harness === "claude-code-cli") return cfg.agentId;
30
+ if (harnessHint === "openclaw" && cfg.harness === "openclaw") return cfg.agentId;
31
+ }
32
+ } catch {
33
+ }
34
+ }
35
+ return harnessHint === "openclaw" ? "oc-lesa-mini" : "cc-mini";
36
+ }
37
+ function ldmPaths(agentId) {
38
+ const id = agentId || getAgentId();
39
+ const agentRoot = join(LDM_ROOT, "agents", id);
40
+ return {
41
+ root: LDM_ROOT,
42
+ bin: join(LDM_ROOT, "bin"),
43
+ secrets: join(LDM_ROOT, "secrets"),
44
+ state: join(LDM_ROOT, "state"),
45
+ config: join(LDM_ROOT, "config.json"),
46
+ crystalDb: join(LDM_ROOT, "memory", "crystal.db"),
47
+ crystalLance: join(LDM_ROOT, "memory", "lance"),
48
+ agentRoot,
49
+ transcripts: join(agentRoot, "memory", "transcripts"),
50
+ sessions: join(agentRoot, "memory", "sessions"),
51
+ daily: join(agentRoot, "memory", "daily"),
52
+ journals: join(agentRoot, "memory", "journals"),
53
+ workspace: join(agentRoot, "memory", "workspace")
54
+ };
55
+ }
56
+ var LEGACY_OC_DIR = join(HOME, ".openclaw");
57
+ function resolveStatePath(filename) {
58
+ const paths = ldmPaths();
59
+ const ldmPath = join(paths.state, filename);
60
+ if (existsSync(ldmPath)) return ldmPath;
61
+ const legacyPath = join(LEGACY_OC_DIR, "memory", filename);
62
+ if (existsSync(legacyPath)) return legacyPath;
63
+ return ldmPath;
64
+ }
65
+ function stateWritePath(filename) {
66
+ const paths = ldmPaths();
67
+ const dir = paths.state;
68
+ if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
69
+ return join(dir, filename);
70
+ }
71
+ function resolveSecretPath(filename) {
72
+ const paths = ldmPaths();
73
+ const ldmPath = join(paths.secrets, filename);
74
+ if (existsSync(ldmPath)) return ldmPath;
75
+ const legacyPath = join(LEGACY_OC_DIR, "secrets", filename);
76
+ if (existsSync(legacyPath)) return legacyPath;
77
+ return ldmPath;
78
+ }
79
+
80
+ // src/crypto.ts
81
+ import { createHash } from "crypto";
82
+ var KEY_PATH = process.env.CRYSTAL_RELAY_KEY_PATH || resolveSecretPath("crystal-relay-key");
83
+ function loadRelayKey() {
84
+ if (!existsSync2(KEY_PATH)) {
85
+ throw new Error(
86
+ `Relay key not found at ${KEY_PATH}
87
+ Generate one: mkdir -p ~/.ldm/secrets && openssl rand -base64 32 > ~/.ldm/secrets/crystal-relay-key && chmod 600 ~/.ldm/secrets/crystal-relay-key
88
+ Or run: crystal pair`
89
+ );
90
+ }
91
+ const raw = readFileSync2(KEY_PATH, "utf-8").trim();
92
+ const key = Buffer.from(raw, "base64");
93
+ if (key.length !== 32) {
94
+ throw new Error(`Relay key must be 32 bytes (256 bits). Got ${key.length} bytes. Regenerate with: openssl rand -base64 32`);
95
+ }
96
+ return key;
97
+ }
98
+ function deriveSigningKey(masterKey) {
99
+ return Buffer.from(hkdfSync("sha256", masterKey, "", "crystal-relay-sign", 32));
100
+ }
101
+ function encrypt(plaintext, masterKey) {
102
+ const nonce = randomBytes(12);
103
+ const cipher = createCipheriv("aes-256-gcm", masterKey, nonce);
104
+ const ciphertext = Buffer.concat([cipher.update(plaintext), cipher.final()]);
105
+ const tag = cipher.getAuthTag();
106
+ const signingKey = deriveSigningKey(masterKey);
107
+ const hmacData = Buffer.concat([nonce, ciphertext, tag]);
108
+ const hmac = createHmac("sha256", signingKey).update(hmacData).digest("hex");
109
+ return {
110
+ v: 1,
111
+ nonce: nonce.toString("base64"),
112
+ ciphertext: ciphertext.toString("base64"),
113
+ tag: tag.toString("base64"),
114
+ hmac
115
+ };
116
+ }
117
+ function decrypt(payload, masterKey) {
118
+ if (payload.v !== 1) {
119
+ throw new Error(`Unknown payload version: ${payload.v}`);
120
+ }
121
+ const nonce = Buffer.from(payload.nonce, "base64");
122
+ const ciphertext = Buffer.from(payload.ciphertext, "base64");
123
+ const tag = Buffer.from(payload.tag, "base64");
124
+ const signingKey = deriveSigningKey(masterKey);
125
+ const hmacData = Buffer.concat([nonce, ciphertext, tag]);
126
+ const expectedHmac = createHmac("sha256", signingKey).update(hmacData).digest("hex");
127
+ if (payload.hmac !== expectedHmac) {
128
+ throw new Error("HMAC verification failed \u2014 blob rejected (tampered or wrong key)");
129
+ }
130
+ const decipher = createDecipheriv("aes-256-gcm", masterKey, nonce);
131
+ decipher.setAuthTag(tag);
132
+ return Buffer.concat([decipher.update(ciphertext), decipher.final()]);
133
+ }
134
+ function encryptJSON(data, masterKey) {
135
+ const plaintext = Buffer.from(JSON.stringify(data), "utf-8");
136
+ return encrypt(plaintext, masterKey);
137
+ }
138
+ function decryptJSON(payload, masterKey) {
139
+ const plaintext = decrypt(payload, masterKey);
140
+ return JSON.parse(plaintext.toString("utf-8"));
141
+ }
142
+
143
+ // src/file-sync.ts
144
+ import { createHash as createHash2 } from "crypto";
1
145
  import {
2
- compareManifest,
3
- generateManifest,
4
- loadFileSyncState,
5
- pullFileSync,
6
- pushFileSync,
7
- saveFileSyncState
8
- } from "./chunk-CGIDSAJB.js";
9
- import "./chunk-D3MACYZ4.js";
10
- import "./chunk-DFQ72B7M.js";
146
+ existsSync as existsSync3,
147
+ mkdirSync as mkdirSync2,
148
+ readFileSync as readFileSync3,
149
+ writeFileSync as writeFileSync2,
150
+ unlinkSync,
151
+ readdirSync as readdirSync2,
152
+ statSync
153
+ } from "fs";
154
+ import { join as join2, relative, dirname as dirname2 } from "path";
155
+ var RELAY_URL = process.env.CRYSTAL_RELAY_URL || "";
156
+ var RELAY_TOKEN = process.env.CRYSTAL_RELAY_TOKEN || "";
157
+ var EXCLUDE_PATTERNS = [
158
+ "memory/crystal.db",
159
+ // DB syncs via delta chunks, not file copy
160
+ "memory/crystal.db-wal",
161
+ "memory/crystal.db-shm",
162
+ "memory/crystal.db.bak",
163
+ "memory/crystal.db.tmp",
164
+ "memory/lance/",
165
+ // LanceDB (deprecated, not synced)
166
+ "state/",
167
+ // Local state files (watermarks, etc.)
168
+ "secrets/",
169
+ // Encryption keys, tokens
170
+ "staging/",
171
+ // Staging pipeline (Core-only)
172
+ "bin/",
173
+ // Local scripts
174
+ ".DS_Store"
175
+ ];
176
+ function shouldExclude(relativePath) {
177
+ for (const pattern of EXCLUDE_PATTERNS) {
178
+ if (relativePath === pattern || relativePath.startsWith(pattern)) return true;
179
+ }
180
+ const parts = relativePath.split("/");
181
+ for (const part of parts) {
182
+ if (part.startsWith(".") && part !== ".ldm") return true;
183
+ }
184
+ return false;
185
+ }
186
+ function scanDir(baseDir, currentDir, entries) {
187
+ if (!existsSync3(currentDir)) return;
188
+ const items = readdirSync2(currentDir);
189
+ for (const item of items) {
190
+ const fullPath = join2(currentDir, item);
191
+ const relPath = relative(baseDir, fullPath);
192
+ if (shouldExclude(relPath)) continue;
193
+ let stat;
194
+ try {
195
+ stat = statSync(fullPath);
196
+ } catch {
197
+ continue;
198
+ }
199
+ if (stat.isDirectory()) {
200
+ scanDir(baseDir, fullPath, entries);
201
+ } else if (stat.isFile()) {
202
+ if (stat.size > 50 * 1024 * 1024) continue;
203
+ const content = readFileSync3(fullPath);
204
+ const sha256 = createHash2("sha256").update(content).digest("hex");
205
+ entries.push({ path: relPath, sha256, size: stat.size });
206
+ }
207
+ }
208
+ }
209
+ function generateManifest() {
210
+ const paths = ldmPaths();
211
+ const entries = [];
212
+ scanDir(paths.root, paths.root, entries);
213
+ return {
214
+ version: 1,
215
+ generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
216
+ fileCount: entries.length,
217
+ entries
218
+ };
219
+ }
220
+ function compareManifest(coreManifest) {
221
+ const paths = ldmPaths();
222
+ const upsert = [];
223
+ const toDelete = [];
224
+ const localManifest = generateManifest();
225
+ const localMap = new Map(localManifest.entries.map((e) => [e.path, e]));
226
+ const coreMap = new Map(coreManifest.entries.map((e) => [e.path, e]));
227
+ for (const entry of coreManifest.entries) {
228
+ const local = localMap.get(entry.path);
229
+ if (!local || local.sha256 !== entry.sha256) {
230
+ upsert.push({ path: entry.path, sha256: entry.sha256 });
231
+ }
232
+ }
233
+ for (const entry of localManifest.entries) {
234
+ if (!coreMap.has(entry.path)) {
235
+ toDelete.push(entry.path);
236
+ }
237
+ }
238
+ return { upsert, delete: toDelete };
239
+ }
240
+ function loadFileSyncState() {
241
+ const statePath = resolveStatePath("file-sync-state.json");
242
+ try {
243
+ if (existsSync3(statePath)) {
244
+ return JSON.parse(readFileSync3(statePath, "utf-8"));
245
+ }
246
+ } catch {
247
+ }
248
+ return { lastSync: null, lastManifestHash: null, filesTransferred: 0, filesDeleted: 0 };
249
+ }
250
+ function saveFileSyncState(state) {
251
+ const writePath = stateWritePath("file-sync-state.json");
252
+ writeFileSync2(writePath, JSON.stringify(state, null, 2));
253
+ }
254
+ async function pushFileSync() {
255
+ if (!RELAY_URL || !RELAY_TOKEN) {
256
+ throw new Error("CRYSTAL_RELAY_URL and CRYSTAL_RELAY_TOKEN must be set");
257
+ }
258
+ const relayKey = loadRelayKey();
259
+ const paths = ldmPaths();
260
+ const manifest = generateManifest();
261
+ const manifestJson = JSON.stringify(manifest.entries.map((e) => `${e.path}:${e.sha256}`));
262
+ const manifestHash = createHash2("sha256").update(manifestJson).digest("hex");
263
+ const state = loadFileSyncState();
264
+ if (state.lastManifestHash === manifestHash) {
265
+ return { manifest: 0, files: 0 };
266
+ }
267
+ const encryptedManifest = encryptJSON(manifest, relayKey);
268
+ const manifestResp = await fetch(`${RELAY_URL}/drop/files`, {
269
+ method: "POST",
270
+ headers: {
271
+ "Authorization": `Bearer ${RELAY_TOKEN}`,
272
+ "Content-Type": "application/octet-stream",
273
+ "X-File-Type": "manifest"
274
+ },
275
+ body: JSON.stringify(encryptedManifest)
276
+ });
277
+ if (!manifestResp.ok) {
278
+ throw new Error(`Manifest push failed: ${manifestResp.status} ${await manifestResp.text()}`);
279
+ }
280
+ let filesPushed = 0;
281
+ for (const entry of manifest.entries) {
282
+ const fullPath = join2(paths.root, entry.path);
283
+ if (!existsSync3(fullPath)) continue;
284
+ const content = readFileSync3(fullPath);
285
+ const encrypted = encrypt(content, relayKey);
286
+ const filePayload = JSON.stringify({
287
+ path: entry.path,
288
+ sha256: entry.sha256,
289
+ size: entry.size,
290
+ data: encrypted
291
+ });
292
+ const fileResp = await fetch(`${RELAY_URL}/drop/files`, {
293
+ method: "POST",
294
+ headers: {
295
+ "Authorization": `Bearer ${RELAY_TOKEN}`,
296
+ "Content-Type": "application/octet-stream",
297
+ "X-File-Type": "file"
298
+ },
299
+ body: filePayload
300
+ });
301
+ if (fileResp.ok) filesPushed++;
302
+ }
303
+ state.lastSync = (/* @__PURE__ */ new Date()).toISOString();
304
+ state.lastManifestHash = manifestHash;
305
+ state.filesTransferred += filesPushed;
306
+ saveFileSyncState(state);
307
+ return { manifest: manifest.fileCount, files: filesPushed };
308
+ }
309
+ async function pullFileSync() {
310
+ if (!RELAY_URL || !RELAY_TOKEN) {
311
+ throw new Error("CRYSTAL_RELAY_URL and CRYSTAL_RELAY_TOKEN must be set");
312
+ }
313
+ const relayKey = loadRelayKey();
314
+ const paths = ldmPaths();
315
+ const listResp = await fetch(`${RELAY_URL}/pickup/files`, {
316
+ headers: { "Authorization": `Bearer ${RELAY_TOKEN}` }
317
+ });
318
+ if (!listResp.ok) {
319
+ throw new Error(`File sync list failed: ${listResp.status} ${await listResp.text()}`);
320
+ }
321
+ const listData = await listResp.json();
322
+ if (listData.count === 0) {
323
+ return { imported: 0, deleted: 0 };
324
+ }
325
+ let coreManifest = null;
326
+ const fileBlobs = [];
327
+ for (const blob of listData.blobs) {
328
+ try {
329
+ const blobResp = await fetch(`${RELAY_URL}/pickup/files/${blob.id}`, {
330
+ headers: { "Authorization": `Bearer ${RELAY_TOKEN}` }
331
+ });
332
+ if (!blobResp.ok) continue;
333
+ const text = await blobResp.text();
334
+ const parsed = JSON.parse(text);
335
+ if (parsed.v !== void 0 && parsed.nonce !== void 0) {
336
+ try {
337
+ const manifest = decryptJSON(parsed, relayKey);
338
+ if (manifest.version && manifest.entries) {
339
+ coreManifest = manifest;
340
+ }
341
+ } catch {
342
+ }
343
+ } else if (parsed.path && parsed.data) {
344
+ fileBlobs.push({
345
+ id: blob.id,
346
+ path: parsed.path,
347
+ sha256: parsed.sha256,
348
+ data: parsed.data
349
+ });
350
+ }
351
+ await fetch(`${RELAY_URL}/confirm/files/${blob.id}`, {
352
+ method: "DELETE",
353
+ headers: { "Authorization": `Bearer ${RELAY_TOKEN}` }
354
+ });
355
+ } catch (err) {
356
+ process.stderr.write(`[file-sync] error processing blob ${blob.id}: ${err.message}
357
+ `);
358
+ }
359
+ }
360
+ if (!coreManifest) {
361
+ process.stderr.write("[file-sync] no manifest found in relay\n");
362
+ return { imported: 0, deleted: 0 };
363
+ }
364
+ const delta = compareManifest(coreManifest);
365
+ const fileBlobMap = new Map(fileBlobs.map((f) => [f.path, f]));
366
+ let imported = 0;
367
+ for (const entry of delta.upsert) {
368
+ const fileBlob = fileBlobMap.get(entry.path);
369
+ if (!fileBlob) {
370
+ continue;
371
+ }
372
+ try {
373
+ const content = decrypt(fileBlob.data, relayKey);
374
+ const actualHash = createHash2("sha256").update(content).digest("hex");
375
+ if (actualHash !== entry.sha256) {
376
+ process.stderr.write(`[file-sync] hash mismatch for ${entry.path}, skipping
377
+ `);
378
+ continue;
379
+ }
380
+ const destPath = join2(paths.root, entry.path);
381
+ mkdirSync2(dirname2(destPath), { recursive: true });
382
+ writeFileSync2(destPath, content);
383
+ imported++;
384
+ } catch (err) {
385
+ process.stderr.write(`[file-sync] failed to write ${entry.path}: ${err.message}
386
+ `);
387
+ }
388
+ }
389
+ let deleted = 0;
390
+ for (const path of delta.delete) {
391
+ const destPath = join2(paths.root, path);
392
+ if (existsSync3(destPath)) {
393
+ try {
394
+ unlinkSync(destPath);
395
+ deleted++;
396
+ } catch (err) {
397
+ process.stderr.write(`[file-sync] failed to delete ${path}: ${err.message}
398
+ `);
399
+ }
400
+ }
401
+ }
402
+ const state = loadFileSyncState();
403
+ state.lastSync = (/* @__PURE__ */ new Date()).toISOString();
404
+ state.filesTransferred += imported;
405
+ state.filesDeleted += deleted;
406
+ saveFileSyncState(state);
407
+ return { imported, deleted };
408
+ }
11
409
  export {
12
410
  compareManifest,
13
411
  generateManifest,