memory-crystal 0.2.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 (104) hide show
  1. package/.env.example +20 -0
  2. package/CHANGELOG.md +6 -0
  3. package/LETTERS.md +22 -0
  4. package/LICENSE +21 -0
  5. package/README-ENTERPRISE.md +162 -0
  6. package/README-old.md +275 -0
  7. package/README.md +91 -0
  8. package/RELAY.md +88 -0
  9. package/TECHNICAL.md +379 -0
  10. package/ai/dev-updates/2026-02-25--cc-air--phase2-architecture-pivot.md +70 -0
  11. package/ai/dev-updates/2026-02-25--cc-air--phase2-worker-build.md +72 -0
  12. package/ai/dev-updates/2026-02-26--10-25-16--cc-mini--phase2-implementation.md +49 -0
  13. package/ai/dev-updates/2026-02-27--20-30-00--cc-mini--readme-overhaul-and-public-deploy.md +69 -0
  14. package/ai/notes/2026-02-26--cc-air--notes.md +412 -0
  15. package/ai/notes/2026-02-27--cc-mini--grok-feedback.md +44 -0
  16. package/ai/notes/2026-02-27--cc-mini--lesa-feedback.md +45 -0
  17. package/ai/notes/RESEARCH.md +1185 -0
  18. package/ai/notes/salience-research/README.md +29 -0
  19. package/ai/notes/salience-research/eurosla-salience-review.md +64 -0
  20. package/ai/notes/salience-research/full-research-summary.md +269 -0
  21. package/ai/notes/salience-research/salience-levels-diagram.png +0 -0
  22. package/ai/plan/2026-02-27--cc-mini--qr-pairing-spec.md +203 -0
  23. package/ai/plan/_archive/PLAN.md +194 -0
  24. package/ai/plan/_archive/PRD.md +1014 -0
  25. package/ai/plan/cc-plans-duplicates-from-dot-claude/2026-02-26--cc-mini--phase2-implementation-plan.md +245 -0
  26. package/ai/plan/dev-conventions-note.md +70 -0
  27. package/ai/plan/ldm-os-install-and-boot-architecture.md +285 -0
  28. package/ai/plan/memory-crystal-phase2-plan.md +192 -0
  29. package/ai/plan/memory-system-lay-of-the-land.md +214 -0
  30. package/ai/plan/phase2-ephemeral-relay.md +238 -0
  31. package/ai/plan/readme-first.md +68 -0
  32. package/ai/plan/roadmap.md +159 -0
  33. package/ai/todos/PUNCHLIST.md +44 -0
  34. package/ai/todos/README.md +31 -0
  35. package/ai/todos/inboxes/cc-air/2026-02-26--cc-air--post-relay-todos.md +85 -0
  36. package/ai/todos/inboxes/cc-mini/2026-02-26--cc-mini--phase2-status.md +100 -0
  37. package/ai/todos/inboxes/cc-mini/_archive/TODO.md +25 -0
  38. package/ai/todos/inboxes/parker/2026-02-25--cc-air--setup-checklist.md +139 -0
  39. package/ai/todos/inboxes/parker/2026-02-26--cc-mini--phase2-your-moves.md +72 -0
  40. package/dist/cc-hook.d.ts +1 -0
  41. package/dist/cc-hook.js +349 -0
  42. package/dist/chunk-3VFIJYS4.js +818 -0
  43. package/dist/chunk-52QE3YI3.js +1169 -0
  44. package/dist/chunk-AA3OPP4Z.js +432 -0
  45. package/dist/chunk-D3I3ZSE2.js +411 -0
  46. package/dist/chunk-EKSACBTJ.js +1070 -0
  47. package/dist/chunk-F3Y7EL7K.js +83 -0
  48. package/dist/chunk-JWZXYVET.js +1068 -0
  49. package/dist/chunk-KYVWO6ZM.js +1069 -0
  50. package/dist/chunk-L3VHARQH.js +413 -0
  51. package/dist/chunk-LOVAHSQV.js +411 -0
  52. package/dist/chunk-LQOYCAGG.js +446 -0
  53. package/dist/chunk-MK42FMEG.js +147 -0
  54. package/dist/chunk-NIJCVN3O.js +147 -0
  55. package/dist/chunk-O2UITJGH.js +465 -0
  56. package/dist/chunk-PEK6JH65.js +432 -0
  57. package/dist/chunk-PJ6FFKEX.js +77 -0
  58. package/dist/chunk-PLUBBZYR.js +800 -0
  59. package/dist/chunk-SGL6ISBJ.js +1061 -0
  60. package/dist/chunk-UNHVZB5G.js +411 -0
  61. package/dist/chunk-VAFTWSTE.js +1061 -0
  62. package/dist/chunk-XZ3S56RQ.js +1061 -0
  63. package/dist/chunk-Y72C7F6O.js +148 -0
  64. package/dist/cli.d.ts +1 -0
  65. package/dist/cli.js +325 -0
  66. package/dist/core.d.ts +188 -0
  67. package/dist/core.js +12 -0
  68. package/dist/crypto.d.ts +16 -0
  69. package/dist/crypto.js +18 -0
  70. package/dist/dev-update-SZ2Z4WCQ.js +6 -0
  71. package/dist/ldm.d.ts +17 -0
  72. package/dist/ldm.js +12 -0
  73. package/dist/mcp-server.d.ts +1 -0
  74. package/dist/mcp-server.js +250 -0
  75. package/dist/migrate.d.ts +1 -0
  76. package/dist/migrate.js +89 -0
  77. package/dist/mirror-sync.d.ts +1 -0
  78. package/dist/mirror-sync.js +130 -0
  79. package/dist/openclaw.d.ts +5 -0
  80. package/dist/openclaw.js +349 -0
  81. package/dist/poller.d.ts +1 -0
  82. package/dist/poller.js +272 -0
  83. package/dist/summarize.d.ts +19 -0
  84. package/dist/summarize.js +10 -0
  85. package/dist/worker.js +137 -0
  86. package/openclaw.plugin.json +11 -0
  87. package/package.json +40 -0
  88. package/scripts/migrate-lance-to-sqlite.mjs +217 -0
  89. package/skills/memory/SKILL.md +61 -0
  90. package/src/cc-hook.ts +447 -0
  91. package/src/cli.ts +356 -0
  92. package/src/core.ts +1472 -0
  93. package/src/crypto.ts +113 -0
  94. package/src/dev-update.ts +178 -0
  95. package/src/ldm.ts +117 -0
  96. package/src/mcp-server.ts +274 -0
  97. package/src/migrate.ts +104 -0
  98. package/src/mirror-sync.ts +175 -0
  99. package/src/openclaw.ts +250 -0
  100. package/src/poller.ts +345 -0
  101. package/src/summarize.ts +210 -0
  102. package/src/worker.ts +208 -0
  103. package/tsconfig.json +18 -0
  104. package/wrangler.toml +20 -0
package/dist/crypto.js ADDED
@@ -0,0 +1,18 @@
1
+ import {
2
+ decrypt,
3
+ decryptJSON,
4
+ encrypt,
5
+ encryptFile,
6
+ encryptJSON,
7
+ hashBuffer,
8
+ loadRelayKey
9
+ } from "./chunk-F3Y7EL7K.js";
10
+ export {
11
+ decrypt,
12
+ decryptJSON,
13
+ encrypt,
14
+ encryptFile,
15
+ encryptJSON,
16
+ hashBuffer,
17
+ loadRelayKey
18
+ };
@@ -0,0 +1,6 @@
1
+ import {
2
+ runDevUpdate
3
+ } from "./chunk-MK42FMEG.js";
4
+ export {
5
+ runDevUpdate
6
+ };
package/dist/ldm.d.ts ADDED
@@ -0,0 +1,17 @@
1
+ declare function getAgentId(): string;
2
+ interface LdmPaths {
3
+ root: string;
4
+ config: string;
5
+ crystalDb: string;
6
+ crystalLance: string;
7
+ agentRoot: string;
8
+ transcripts: string;
9
+ sessions: string;
10
+ daily: string;
11
+ journals: string;
12
+ }
13
+ declare function ldmPaths(agentId?: string): LdmPaths;
14
+ declare function scaffoldLdm(agentId?: string): LdmPaths;
15
+ declare function ensureLdm(agentId?: string): LdmPaths;
16
+
17
+ export { type LdmPaths, ensureLdm, getAgentId, ldmPaths, scaffoldLdm };
package/dist/ldm.js ADDED
@@ -0,0 +1,12 @@
1
+ import {
2
+ ensureLdm,
3
+ getAgentId,
4
+ ldmPaths,
5
+ scaffoldLdm
6
+ } from "./chunk-PJ6FFKEX.js";
7
+ export {
8
+ ensureLdm,
9
+ getAgentId,
10
+ ldmPaths,
11
+ scaffoldLdm
12
+ };
@@ -0,0 +1 @@
1
+ #!/usr/bin/env node
@@ -0,0 +1,250 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ RemoteCrystal,
4
+ createCrystal,
5
+ resolveConfig
6
+ } from "./chunk-52QE3YI3.js";
7
+
8
+ // src/mcp-server.ts
9
+ import { Server } from "@modelcontextprotocol/sdk/server/index.js";
10
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
11
+ import { CallToolRequestSchema, ListToolsRequestSchema } from "@modelcontextprotocol/sdk/types.js";
12
+ import { existsSync, readFileSync, appendFileSync, mkdirSync } from "fs";
13
+ import { join } from "path";
14
+ var CONFIG_DIR = join(process.env.HOME || "", ".openclaw");
15
+ var PRIVATE_MODE_PATH = join(CONFIG_DIR, "memory", "memory-capture-state.json");
16
+ function isPrivateMode() {
17
+ try {
18
+ if (existsSync(PRIVATE_MODE_PATH)) {
19
+ const state = JSON.parse(readFileSync(PRIVATE_MODE_PATH, "utf-8"));
20
+ return state.enabled === false;
21
+ }
22
+ } catch {
23
+ }
24
+ return false;
25
+ }
26
+ var METRICS_PATH = join(CONFIG_DIR, "memory", "search-metrics.jsonl");
27
+ function logSearchMetric(tool, query, resultCount) {
28
+ try {
29
+ mkdirSync(join(CONFIG_DIR, "memory"), { recursive: true });
30
+ const entry = JSON.stringify({
31
+ ts: (/* @__PURE__ */ new Date()).toISOString(),
32
+ tool,
33
+ query,
34
+ results: resultCount
35
+ });
36
+ appendFileSync(METRICS_PATH, entry + "\n");
37
+ } catch {
38
+ }
39
+ }
40
+ var config = resolveConfig();
41
+ var crystal = createCrystal(config);
42
+ var isRemote = crystal instanceof RemoteCrystal;
43
+ if (isRemote) {
44
+ process.stderr.write("[memory-crystal] Remote mode: " + config.remoteUrl + "\n");
45
+ }
46
+ var server = new Server(
47
+ { name: "memory-crystal", version: "0.1.0" },
48
+ { capabilities: { tools: {} } }
49
+ );
50
+ server.setRequestHandler(ListToolsRequestSchema, async () => ({
51
+ tools: [
52
+ {
53
+ name: "crystal_search",
54
+ description: "Search memory crystal \u2014 semantic search across all agent conversations, files, and stored memories. Returns ranked results with similarity scores.",
55
+ inputSchema: {
56
+ type: "object",
57
+ properties: {
58
+ query: { type: "string", description: "What to search for" },
59
+ limit: { type: "number", description: "Max results (default: 5)" },
60
+ agent_id: { type: "string", description: 'Filter by agent (e.g. "main", "claude-code")' }
61
+ },
62
+ required: ["query"]
63
+ }
64
+ },
65
+ {
66
+ name: "crystal_remember",
67
+ description: "Store a fact, preference, or observation in memory crystal. Persists across sessions and compaction.",
68
+ inputSchema: {
69
+ type: "object",
70
+ properties: {
71
+ text: { type: "string", description: "The fact or observation to remember" },
72
+ category: {
73
+ type: "string",
74
+ enum: ["fact", "preference", "event", "opinion", "skill"],
75
+ description: "Category of memory (default: fact)"
76
+ }
77
+ },
78
+ required: ["text"]
79
+ }
80
+ },
81
+ {
82
+ name: "crystal_forget",
83
+ description: "Deprecate a memory by ID. Does not delete \u2014 marks as deprecated.",
84
+ inputSchema: {
85
+ type: "object",
86
+ properties: {
87
+ id: { type: "number", description: "Memory ID to deprecate" }
88
+ },
89
+ required: ["id"]
90
+ }
91
+ },
92
+ {
93
+ name: "crystal_status",
94
+ description: "Show memory crystal status \u2014 chunk count, memory count, agents, embedding provider.",
95
+ inputSchema: {
96
+ type: "object",
97
+ properties: {}
98
+ }
99
+ },
100
+ {
101
+ name: "crystal_sources_add",
102
+ description: "Add a directory for source file indexing. Files are chunked, embedded, and searchable via crystal_search. Optional feature... does not affect existing memory capture.",
103
+ inputSchema: {
104
+ type: "object",
105
+ properties: {
106
+ path: { type: "string", description: "Absolute path to the directory to index" },
107
+ name: { type: "string", description: 'Short name for this collection (e.g. "wipcomputer")' }
108
+ },
109
+ required: ["path", "name"]
110
+ }
111
+ },
112
+ {
113
+ name: "crystal_sources_sync",
114
+ description: "Sync a source collection: scan for new/changed/deleted files and re-index. Run after adding a collection or when files change.",
115
+ inputSchema: {
116
+ type: "object",
117
+ properties: {
118
+ name: { type: "string", description: "Collection name to sync" },
119
+ dry_run: { type: "boolean", description: "If true, report what would change without actually indexing" }
120
+ },
121
+ required: ["name"]
122
+ }
123
+ },
124
+ {
125
+ name: "crystal_sources_status",
126
+ description: "Show status of all source file collections: file counts, chunk counts, last sync time.",
127
+ inputSchema: {
128
+ type: "object",
129
+ properties: {}
130
+ }
131
+ }
132
+ ]
133
+ }));
134
+ server.setRequestHandler(CallToolRequestSchema, async (request) => {
135
+ const { name, arguments: args } = request.params;
136
+ try {
137
+ await crystal.init();
138
+ switch (name) {
139
+ case "crystal_search": {
140
+ const query = args?.query;
141
+ const limit = args?.limit || 5;
142
+ const filter = {};
143
+ if (args?.agent_id) filter.agent_id = args.agent_id;
144
+ const results = await crystal.search(query, limit, filter);
145
+ logSearchMetric("crystal_search", query, results.length);
146
+ if (results.length === 0) {
147
+ return { content: [{ type: "text", text: "No results found." }] };
148
+ }
149
+ const freshnessIcon = { fresh: "\u{1F7E2}", recent: "\u{1F7E1}", aging: "\u{1F7E0}", stale: "\u{1F534}" };
150
+ const formatted = results.map((r, i) => {
151
+ const score = (r.score * 100).toFixed(1);
152
+ const date = r.created_at?.slice(0, 10) || "unknown";
153
+ const fresh = r.freshness ? `${freshnessIcon[r.freshness]} ${r.freshness}, ` : "";
154
+ return `[${i + 1}] (${fresh}${score}% match, ${r.agent_id}, ${date}, ${r.role})
155
+ ${r.text}`;
156
+ }).join("\n\n---\n\n");
157
+ const header = "(Recency-weighted. \u{1F7E2} fresh <3d, \u{1F7E1} recent <7d, \u{1F7E0} aging <14d, \u{1F534} stale 14d+)\n\n";
158
+ return { content: [{ type: "text", text: header + formatted }] };
159
+ }
160
+ case "crystal_remember": {
161
+ if (isPrivateMode()) {
162
+ return { content: [{ type: "text", text: "Private mode is on. No memories are being stored. Toggle off to resume." }] };
163
+ }
164
+ const text = args?.text;
165
+ const category = args?.category || "fact";
166
+ const id = await crystal.remember(text, category);
167
+ return { content: [{ type: "text", text: `Remembered (id: ${id}, category: ${category}): ${text}` }] };
168
+ }
169
+ case "crystal_forget": {
170
+ const id = args?.id;
171
+ const ok = crystal.forget(id);
172
+ return {
173
+ content: [{ type: "text", text: ok ? `Forgot memory ${id}` : `Memory ${id} not found or already deprecated` }]
174
+ };
175
+ }
176
+ case "crystal_status": {
177
+ const status = await crystal.status();
178
+ const text = [
179
+ `Memory Crystal Status${isRemote ? " (REMOTE)" : ""}`,
180
+ ` Data dir: ${status.dataDir}`,
181
+ ` Provider: ${status.embeddingProvider}`,
182
+ ` Chunks: ${status.chunks.toLocaleString()}`,
183
+ ` Memories: ${status.memories}`,
184
+ ` Sources: ${status.sources}`,
185
+ ` Agents: ${status.agents.length > 0 ? status.agents.join(", ") : "none yet"}`,
186
+ ` Sessions: ${status.capturedSessions} captured`,
187
+ ` Latest: ${status.latestCapture || "never"}`
188
+ ].join("\n");
189
+ return { content: [{ type: "text", text }] };
190
+ }
191
+ case "crystal_sources_add": {
192
+ if (isRemote) {
193
+ return { content: [{ type: "text", text: "Source indexing not available in remote mode. Index files on the Mac Mini." }] };
194
+ }
195
+ const path = args?.path;
196
+ const collectionName = args?.name;
197
+ const col = await crystal.sourcesAdd(path, collectionName);
198
+ return {
199
+ content: [{ type: "text", text: `Added collection "${col.name}" at ${col.root_path}
200
+ Run crystal_sources_sync with name "${collectionName}" to index files.` }]
201
+ };
202
+ }
203
+ case "crystal_sources_sync": {
204
+ if (isRemote) {
205
+ return { content: [{ type: "text", text: "Source indexing not available in remote mode. Sync files on the Mac Mini." }] };
206
+ }
207
+ const collectionName = args?.name;
208
+ const dryRun = args?.dry_run;
209
+ const result = await crystal.sourcesSync(collectionName, { dryRun });
210
+ const lines = [
211
+ dryRun ? `Dry run for "${result.collection}":` : `Synced "${result.collection}":`,
212
+ ` Added: ${result.added} files`,
213
+ ` Updated: ${result.updated} files`,
214
+ ` Removed: ${result.removed} files`,
215
+ ` Chunks: ${result.chunks_added} embedded`,
216
+ ` Time: ${(result.duration_ms / 1e3).toFixed(1)}s`
217
+ ];
218
+ return { content: [{ type: "text", text: lines.join("\n") }] };
219
+ }
220
+ case "crystal_sources_status": {
221
+ if (isRemote) {
222
+ return { content: [{ type: "text", text: "Source indexing not available in remote mode." }] };
223
+ }
224
+ const sourcesStatus = crystal.sourcesStatus();
225
+ if (sourcesStatus.collections.length === 0) {
226
+ return { content: [{ type: "text", text: "No source collections. Use crystal_sources_add to add a directory." }] };
227
+ }
228
+ const lines = ["Source Collections:"];
229
+ for (const col of sourcesStatus.collections) {
230
+ const syncAgo = col.last_sync_at ? `${Math.round((Date.now() - new Date(col.last_sync_at).getTime()) / 6e4)}m ago` : "never";
231
+ lines.push(` ${col.name}: ${col.file_count.toLocaleString()} files, ${col.chunk_count.toLocaleString()} chunks, last sync ${syncAgo}`);
232
+ }
233
+ lines.push(` Total: ${sourcesStatus.total_files.toLocaleString()} files, ${sourcesStatus.total_chunks.toLocaleString()} chunks`);
234
+ return { content: [{ type: "text", text: lines.join("\n") }] };
235
+ }
236
+ default:
237
+ return { content: [{ type: "text", text: `Unknown tool: ${name}` }], isError: true };
238
+ }
239
+ } catch (err) {
240
+ return { content: [{ type: "text", text: `Error: ${err.message}` }], isError: true };
241
+ }
242
+ });
243
+ async function main() {
244
+ const transport = new StdioServerTransport();
245
+ await server.connect(transport);
246
+ }
247
+ main().catch((err) => {
248
+ console.error(`MCP server failed: ${err.message}`);
249
+ process.exit(1);
250
+ });
@@ -0,0 +1 @@
1
+ #!/usr/bin/env node
@@ -0,0 +1,89 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ Crystal,
4
+ resolveConfig
5
+ } from "./chunk-52QE3YI3.js";
6
+
7
+ // src/migrate.ts
8
+ import Database from "better-sqlite3";
9
+ import { existsSync } from "fs";
10
+ import { join } from "path";
11
+ var BATCH_SIZE = 50;
12
+ async function main() {
13
+ const args = process.argv.slice(2);
14
+ const dryRun = args.includes("--dry-run");
15
+ const providerFlag = args.find((_, i) => args[i - 1] === "--provider");
16
+ const openclawHome = process.env.OPENCLAW_HOME || join(process.env.HOME || "", ".openclaw");
17
+ const sourcePath = join(openclawHome, "memory", "context-embeddings.sqlite");
18
+ if (!existsSync(sourcePath)) {
19
+ console.error(`Source not found: ${sourcePath}`);
20
+ process.exit(1);
21
+ }
22
+ const sourceDb = new Database(sourcePath, { readonly: true });
23
+ sourceDb.pragma("journal_mode = WAL");
24
+ const total = sourceDb.prepare("SELECT COUNT(*) as count FROM conversation_chunks").get().count;
25
+ console.log(`Found ${total} chunks in context-embeddings.sqlite`);
26
+ if (dryRun) {
27
+ const samples = sourceDb.prepare("SELECT chunk_text, role, session_key, timestamp FROM conversation_chunks ORDER BY timestamp DESC LIMIT 5").all();
28
+ console.log("\nSample (5 most recent):");
29
+ for (const s of samples) {
30
+ const date = s.timestamp ? new Date(s.timestamp).toISOString().slice(0, 10) : "unknown";
31
+ console.log(` [${date}] [${s.role}] ${s.chunk_text.slice(0, 80)}...`);
32
+ }
33
+ console.log(`
34
+ Run without --dry-run to import all ${total} chunks.`);
35
+ sourceDb.close();
36
+ return;
37
+ }
38
+ const overrides = {};
39
+ if (providerFlag) overrides.embeddingProvider = providerFlag;
40
+ const config = resolveConfig(overrides);
41
+ const crystal = new Crystal(config);
42
+ await crystal.init();
43
+ console.log(`Embedding provider: ${config.embeddingProvider}`);
44
+ console.log(`Target: ${config.dataDir}`);
45
+ console.log(`Migrating ${total} chunks in batches of ${BATCH_SIZE}...`);
46
+ const rows = sourceDb.prepare(`
47
+ SELECT chunk_text, role, agent_id, session_key, timestamp, compaction_number
48
+ FROM conversation_chunks
49
+ ORDER BY timestamp ASC
50
+ `).all();
51
+ let imported = 0;
52
+ let failed = 0;
53
+ for (let i = 0; i < rows.length; i += BATCH_SIZE) {
54
+ const batch = rows.slice(i, i + BATCH_SIZE);
55
+ const chunks = batch.map((row) => ({
56
+ text: row.chunk_text,
57
+ role: row.role || "assistant",
58
+ source_type: "conversation",
59
+ source_id: row.session_key || "unknown",
60
+ agent_id: row.agent_id || "main",
61
+ token_count: Math.ceil((row.chunk_text?.length || 0) / 4),
62
+ created_at: row.timestamp ? new Date(row.timestamp).toISOString() : (/* @__PURE__ */ new Date()).toISOString()
63
+ }));
64
+ try {
65
+ const count = await crystal.ingest(chunks);
66
+ imported += count;
67
+ const pct = Math.round(imported / total * 100);
68
+ process.stdout.write(`\r ${imported}/${total} (${pct}%)`);
69
+ } catch (err) {
70
+ failed += batch.length;
71
+ console.error(`
72
+ Batch error at ${i}: ${err.message}`);
73
+ }
74
+ }
75
+ console.log(`
76
+
77
+ Migration complete:`);
78
+ console.log(` Imported: ${imported}`);
79
+ console.log(` Failed: ${failed}`);
80
+ console.log(` Provider: ${config.embeddingProvider}`);
81
+ const status = await crystal.status();
82
+ console.log(` Total chunks in crystal: ${status.chunks}`);
83
+ crystal.close();
84
+ sourceDb.close();
85
+ }
86
+ main().catch((err) => {
87
+ console.error(`Migration failed: ${err.message}`);
88
+ process.exit(1);
89
+ });
@@ -0,0 +1 @@
1
+ #!/usr/bin/env node
@@ -0,0 +1,130 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ decrypt,
4
+ decryptJSON,
5
+ hashBuffer,
6
+ loadRelayKey
7
+ } from "./chunk-F3Y7EL7K.js";
8
+ import {
9
+ ldmPaths
10
+ } from "./chunk-PJ6FFKEX.js";
11
+
12
+ // src/mirror-sync.ts
13
+ import { readFileSync, writeFileSync, existsSync, mkdirSync, renameSync } from "fs";
14
+ import { join, dirname } from "path";
15
+ var HOME = process.env.HOME || "";
16
+ var RELAY_URL = process.env.CRYSTAL_RELAY_URL || "";
17
+ var RELAY_TOKEN = process.env.CRYSTAL_RELAY_TOKEN || "";
18
+ var OC_DIR = join(HOME, ".openclaw");
19
+ var _ldmPaths = ldmPaths();
20
+ var MIRROR_DIR = join(_ldmPaths.root, "memory");
21
+ var MIRROR_DB_PATH = _ldmPaths.crystalDb;
22
+ var MIRROR_STATE_PATH = join(OC_DIR, "memory", "mirror-sync-state.json");
23
+ function loadState() {
24
+ try {
25
+ if (existsSync(MIRROR_STATE_PATH)) {
26
+ return JSON.parse(readFileSync(MIRROR_STATE_PATH, "utf-8"));
27
+ }
28
+ } catch {
29
+ }
30
+ return { lastSync: null, lastHash: null, lastSize: null };
31
+ }
32
+ function saveState(state) {
33
+ const dir = dirname(MIRROR_STATE_PATH);
34
+ if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
35
+ writeFileSync(MIRROR_STATE_PATH, JSON.stringify(state, null, 2));
36
+ }
37
+ async function pullMirror(force2) {
38
+ if (!RELAY_URL || !RELAY_TOKEN) {
39
+ throw new Error("CRYSTAL_RELAY_URL and CRYSTAL_RELAY_TOKEN must be set");
40
+ }
41
+ const relayKey = loadRelayKey();
42
+ const listResp = await fetch(`${RELAY_URL}/pickup/mirror`, {
43
+ headers: { "Authorization": `Bearer ${RELAY_TOKEN}` }
44
+ });
45
+ if (!listResp.ok) {
46
+ throw new Error(`Relay list failed: ${listResp.status} ${await listResp.text()}`);
47
+ }
48
+ const listData = await listResp.json();
49
+ if (listData.count === 0) {
50
+ process.stderr.write("[mirror-sync] no mirror available\n");
51
+ return false;
52
+ }
53
+ const latestBlob = listData.blobs[listData.blobs.length - 1];
54
+ const blobResp = await fetch(`${RELAY_URL}/pickup/mirror/${latestBlob.id}`, {
55
+ headers: { "Authorization": `Bearer ${RELAY_TOKEN}` }
56
+ });
57
+ if (!blobResp.ok) {
58
+ throw new Error(`Mirror fetch failed: ${blobResp.status}`);
59
+ }
60
+ const encryptedText = await blobResp.text();
61
+ const mirrorPayload = JSON.parse(encryptedText);
62
+ const meta = decryptJSON(mirrorPayload.meta, relayKey);
63
+ const state = loadState();
64
+ if (!force2 && state.lastHash === meta.hash) {
65
+ process.stderr.write("[mirror-sync] mirror is already up to date\n");
66
+ return false;
67
+ }
68
+ const dbData = decrypt(mirrorPayload.db, relayKey);
69
+ const actualHash = hashBuffer(dbData);
70
+ if (actualHash !== meta.hash) {
71
+ throw new Error(
72
+ `Mirror integrity check failed!
73
+ Expected: ${meta.hash}
74
+ Got: ${actualHash}
75
+ Mirror REJECTED \u2014 keeping existing local mirror.`
76
+ );
77
+ }
78
+ if (!existsSync(MIRROR_DIR)) mkdirSync(MIRROR_DIR, { recursive: true });
79
+ const tmpPath = MIRROR_DB_PATH + ".tmp";
80
+ writeFileSync(tmpPath, dbData);
81
+ if (existsSync(MIRROR_DB_PATH)) {
82
+ const backupPath = MIRROR_DB_PATH + ".bak";
83
+ try {
84
+ renameSync(MIRROR_DB_PATH, backupPath);
85
+ } catch {
86
+ }
87
+ }
88
+ renameSync(tmpPath, MIRROR_DB_PATH);
89
+ state.lastSync = (/* @__PURE__ */ new Date()).toISOString();
90
+ state.lastHash = meta.hash;
91
+ state.lastSize = dbData.length;
92
+ saveState(state);
93
+ process.stderr.write(
94
+ `[mirror-sync] updated: ${(dbData.length / 1024 / 1024).toFixed(1)}MB, hash=${meta.hash.slice(0, 12)}..., pushed=${meta.pushed_at}
95
+ `
96
+ );
97
+ for (const blob of listData.blobs) {
98
+ try {
99
+ await fetch(`${RELAY_URL}/confirm/mirror/${blob.id}`, {
100
+ method: "DELETE",
101
+ headers: { "Authorization": `Bearer ${RELAY_TOKEN}` }
102
+ });
103
+ } catch {
104
+ }
105
+ }
106
+ return true;
107
+ }
108
+ var args = process.argv.slice(2);
109
+ if (args.includes("--status")) {
110
+ const state = loadState();
111
+ const hasDb = existsSync(MIRROR_DB_PATH);
112
+ console.log("Mirror sync status:");
113
+ console.log(` Relay URL: ${RELAY_URL || "(not set)"}`);
114
+ console.log(` Local mirror: ${hasDb ? MIRROR_DB_PATH : "(none)"}`);
115
+ console.log(` Last sync: ${state.lastSync || "never"}`);
116
+ console.log(` Last hash: ${state.lastHash ? state.lastHash.slice(0, 16) + "..." : "(none)"}`);
117
+ console.log(` Last size: ${state.lastSize ? (state.lastSize / 1024 / 1024).toFixed(1) + "MB" : "(none)"}`);
118
+ process.exit(0);
119
+ }
120
+ var force = args.includes("--force");
121
+ pullMirror(force).then((updated) => {
122
+ if (updated) {
123
+ process.stderr.write("[mirror-sync] done\n");
124
+ }
125
+ process.exit(0);
126
+ }).catch((err) => {
127
+ process.stderr.write(`[mirror-sync] error: ${err.message}
128
+ `);
129
+ process.exit(1);
130
+ });
@@ -0,0 +1,5 @@
1
+ declare const _default: {
2
+ register(api: any): void;
3
+ };
4
+
5
+ export { _default as default };