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/src/cli.ts ADDED
@@ -0,0 +1,356 @@
1
+ #!/usr/bin/env node
2
+ // memory-crystal/cli.ts — Universal CLI interface.
3
+ // crystal search "query" | crystal remember "fact" | crystal forget <id> | crystal status
4
+
5
+ import { Crystal, resolveConfig } from './core.js';
6
+ import { scaffoldLdm, ldmPaths, ensureLdm, getAgentId } from './ldm.js';
7
+ import { existsSync, copyFileSync, symlinkSync, lstatSync, unlinkSync } from 'node:fs';
8
+ import { join } from 'node:path';
9
+
10
+ const USAGE = `
11
+ crystal — Sovereign memory system
12
+
13
+ Commands:
14
+ crystal search <query> [-n limit] [--agent <id>] [--provider <openai|ollama|google>]
15
+ crystal remember <text> [--category fact|preference|event|opinion|skill]
16
+ crystal forget <id>
17
+ crystal status [--provider <openai|ollama|google>]
18
+
19
+ crystal sources add <path> --name <name> Add a directory for source indexing
20
+ crystal sources sync <name> [--dry-run] Sync (re-index changed files)
21
+ crystal sources status Show all indexed collections
22
+ crystal sources remove <name> Remove a collection
23
+
24
+ crystal init [--agent <id>] Scaffold ~/.ldm/ directory tree
25
+ crystal migrate-db Move crystal.db to ~/.ldm/memory/
26
+
27
+ Environment:
28
+ CRYSTAL_EMBEDDING_PROVIDER openai | ollama | google (default: openai)
29
+ CRYSTAL_OLLAMA_HOST Ollama URL (default: http://localhost:11434)
30
+ CRYSTAL_REMOTE_URL Worker URL for cloud mirror mode
31
+ CRYSTAL_REMOTE_TOKEN Auth token for cloud mirror
32
+ CRYSTAL_AGENT_ID Agent identifier (default: cc-mini)
33
+ `.trim();
34
+
35
+ async function main() {
36
+ const args = process.argv.slice(2);
37
+
38
+ if (args.length === 0 || args[0] === '--help' || args[0] === '-h') {
39
+ console.log(USAGE);
40
+ process.exit(0);
41
+ }
42
+
43
+ const command = args[0];
44
+
45
+ // Parse flags
46
+ const flags: Record<string, string> = {};
47
+ let positional: string[] = [];
48
+ for (let i = 1; i < args.length; i++) {
49
+ if (args[i] === '--dry-run') {
50
+ flags['dry-run'] = 'true';
51
+ } else if (args[i].startsWith('--') || args[i] === '-n') {
52
+ const key = args[i].replace(/^-+/, '');
53
+ flags[key] = args[++i] || '';
54
+ } else {
55
+ positional.push(args[i]);
56
+ }
57
+ }
58
+
59
+ // Commands that don't need Crystal: handle before init
60
+ if (command === 'init' || command === 'migrate-db') {
61
+ await handleLdmCommand(command, flags);
62
+ return;
63
+ }
64
+
65
+ const overrides: any = {};
66
+ if (flags.provider) overrides.embeddingProvider = flags.provider;
67
+
68
+ const config = resolveConfig(overrides);
69
+ const crystal = new Crystal(config);
70
+ await crystal.init();
71
+
72
+ try {
73
+ switch (command) {
74
+ case 'search': {
75
+ const query = positional.join(' ');
76
+ if (!query) { console.error('Usage: crystal search <query>'); process.exit(1); }
77
+ const limit = parseInt(flags.n || '5', 10);
78
+ const filter: any = {};
79
+ if (flags.agent) filter.agent_id = flags.agent;
80
+
81
+ const results = await crystal.search(query, limit, filter);
82
+ if (results.length === 0) {
83
+ console.log('No results found.');
84
+ } else {
85
+ for (const r of results) {
86
+ const score = (r.score * 100).toFixed(1);
87
+ const date = r.created_at?.slice(0, 10) || 'unknown';
88
+ console.log(`[${score}%] [${r.agent_id}] [${date}] [${r.role}]`);
89
+ console.log(r.text.slice(0, 300) + (r.text.length > 300 ? '...' : ''));
90
+ console.log('---');
91
+ }
92
+ }
93
+ break;
94
+ }
95
+
96
+ case 'remember': {
97
+ const text = positional.join(' ');
98
+ if (!text) { console.error('Usage: crystal remember <text>'); process.exit(1); }
99
+ const category = (flags.category || 'fact') as any;
100
+ const id = await crystal.remember(text, category);
101
+ console.log(`Remembered (id: ${id}, category: ${category}): ${text}`);
102
+ break;
103
+ }
104
+
105
+ case 'forget': {
106
+ const id = parseInt(positional[0], 10);
107
+ if (isNaN(id)) { console.error('Usage: crystal forget <id>'); process.exit(1); }
108
+ const ok = crystal.forget(id);
109
+ console.log(ok ? `Forgot memory ${id}` : `Memory ${id} not found or already deprecated`);
110
+ break;
111
+ }
112
+
113
+ case 'status': {
114
+ const status = await crystal.status();
115
+ console.log(`Memory Crystal Status`);
116
+ console.log(` Data dir: ${status.dataDir}`);
117
+ console.log(` Provider: ${status.embeddingProvider}`);
118
+ console.log(` Chunks: ${status.chunks.toLocaleString()}`);
119
+ console.log(` Memories: ${status.memories}`);
120
+ console.log(` Sources: ${status.sources}`);
121
+ console.log(` Agents: ${status.agents.length > 0 ? status.agents.join(', ') : 'none yet'}`);
122
+ break;
123
+ }
124
+
125
+ case 'sources': {
126
+ const subCommand = positional[0];
127
+ if (!subCommand) {
128
+ console.error('Usage: crystal sources <add|sync|status|remove> ...');
129
+ process.exit(1);
130
+ }
131
+
132
+ switch (subCommand) {
133
+ case 'add': {
134
+ const path = positional[1];
135
+ const name = flags.name;
136
+ if (!path || !name) {
137
+ console.error('Usage: crystal sources add <path> --name <name>');
138
+ process.exit(1);
139
+ }
140
+ const col = await crystal.sourcesAdd(path, name);
141
+ console.log(`Added collection "${col.name}" at ${col.root_path}`);
142
+ console.log(`Run "crystal sources sync ${name}" to index files.`);
143
+ break;
144
+ }
145
+
146
+ case 'sync': {
147
+ const name = positional[1];
148
+ if (!name) {
149
+ console.error('Usage: crystal sources sync <name> [--dry-run]');
150
+ process.exit(1);
151
+ }
152
+ const dryRun = 'dry-run' in flags;
153
+ if (dryRun) {
154
+ console.log(`Dry run for "${name}"...`);
155
+ } else {
156
+ console.log(`Syncing "${name}"...`);
157
+ }
158
+ const result = await crystal.sourcesSync(name, { dryRun });
159
+ console.log(` Added: ${result.added} files`);
160
+ console.log(` Updated: ${result.updated} files`);
161
+ console.log(` Removed: ${result.removed} files`);
162
+ console.log(` Chunks: ${result.chunks_added} embedded`);
163
+ console.log(` Time: ${(result.duration_ms / 1000).toFixed(1)}s`);
164
+ break;
165
+ }
166
+
167
+ case 'status': {
168
+ const status = crystal.sourcesStatus();
169
+ if (status.collections.length === 0) {
170
+ console.log('No source collections. Use "crystal sources add <path> --name <name>" to add one.');
171
+ } else {
172
+ console.log('Source Collections:');
173
+ for (const col of status.collections) {
174
+ const syncAgo = col.last_sync_at
175
+ ? `${Math.round((Date.now() - new Date(col.last_sync_at).getTime()) / 60000)}m ago`
176
+ : 'never';
177
+ console.log(` ${col.name}: ${col.file_count.toLocaleString()} files, ${col.chunk_count.toLocaleString()} chunks, last sync ${syncAgo}`);
178
+ }
179
+ console.log(` Total: ${status.total_files.toLocaleString()} files, ${status.total_chunks.toLocaleString()} chunks`);
180
+ }
181
+ break;
182
+ }
183
+
184
+ case 'remove': {
185
+ const name = positional[1];
186
+ if (!name) {
187
+ console.error('Usage: crystal sources remove <name>');
188
+ process.exit(1);
189
+ }
190
+ const ok = crystal.sourcesRemove(name);
191
+ console.log(ok ? `Removed collection "${name}"` : `Collection "${name}" not found`);
192
+ break;
193
+ }
194
+
195
+ default:
196
+ console.error(`Unknown sources subcommand: ${subCommand}`);
197
+ process.exit(1);
198
+ }
199
+ break;
200
+ }
201
+
202
+ case 'init': {
203
+ const agentId = flags.agent || getAgentId();
204
+ const paths = scaffoldLdm(agentId);
205
+ console.log(`LDM scaffolded for agent "${agentId}"`);
206
+ console.log(` Root: ${paths.root}`);
207
+ console.log(` Crystal DB: ${paths.crystalDb}`);
208
+ console.log(` Transcripts: ${paths.transcripts}`);
209
+ console.log(` Sessions: ${paths.sessions}`);
210
+ console.log(` Daily: ${paths.daily}`);
211
+ console.log(` Journals: ${paths.journals}`);
212
+ break;
213
+ }
214
+
215
+ case 'migrate-db': {
216
+ const paths = ensureLdm();
217
+ const HOME = process.env.HOME || '';
218
+ const legacyDir = join(HOME, '.openclaw', 'memory-crystal');
219
+ const legacyDb = join(legacyDir, 'crystal.db');
220
+ const destDb = paths.crystalDb;
221
+
222
+ if (!existsSync(legacyDb)) {
223
+ console.error(`Source not found: ${legacyDb}`);
224
+ process.exit(1);
225
+ }
226
+
227
+ if (existsSync(destDb)) {
228
+ try {
229
+ const stat = lstatSync(destDb);
230
+ if (!stat.isSymbolicLink()) {
231
+ console.error(`Destination already exists (not a symlink): ${destDb}`);
232
+ console.error('If this is from a previous migration, remove it first.');
233
+ process.exit(1);
234
+ }
235
+ } catch {}
236
+ }
237
+
238
+ // Copy crystal.db (never move)
239
+ console.log(`Copying ${legacyDb} -> ${destDb}`);
240
+ copyFileSync(legacyDb, destDb);
241
+
242
+ // Verify copy by opening with better-sqlite3
243
+ const Database = (await import('better-sqlite3')).default;
244
+ const db = new Database(destDb, { readonly: true });
245
+ const row = db.prepare('SELECT COUNT(*) as count FROM chunks').get() as any;
246
+ db.close();
247
+ console.log(`Verified: ${row.count.toLocaleString()} chunks in destination DB`);
248
+
249
+ // Create symlink: legacy path -> new path
250
+ if (existsSync(legacyDb)) {
251
+ try {
252
+ const stat = lstatSync(legacyDb);
253
+ if (!stat.isSymbolicLink()) {
254
+ unlinkSync(legacyDb);
255
+ symlinkSync(destDb, legacyDb);
256
+ console.log(`Symlinked ${legacyDb} -> ${destDb}`);
257
+ }
258
+ } catch (err: any) {
259
+ console.error(`Symlink failed (non-fatal): ${err.message}`);
260
+ }
261
+ }
262
+
263
+ // Handle lance/ directory if it exists
264
+ const legacyLance = join(legacyDir, 'lance');
265
+ if (existsSync(legacyLance)) {
266
+ try {
267
+ const stat = lstatSync(legacyLance);
268
+ if (!stat.isSymbolicLink()) {
269
+ // Copy lance dir handled by LanceDB on next write
270
+ console.log(`Note: lance/ at ${legacyLance} left in place (LanceDB will use new path on next write)`);
271
+ }
272
+ } catch {}
273
+ }
274
+
275
+ console.log('Migration complete. Restart gateway to use new path.');
276
+ break;
277
+ }
278
+
279
+ default:
280
+ console.error(`Unknown command: ${command}`);
281
+ console.log(USAGE);
282
+ process.exit(1);
283
+ }
284
+ } finally {
285
+ crystal.close();
286
+ }
287
+ }
288
+
289
+ async function handleLdmCommand(command: string, flags: Record<string, string>): Promise<void> {
290
+ if (command === 'init') {
291
+ const agentId = flags.agent || getAgentId();
292
+ const paths = scaffoldLdm(agentId);
293
+ console.log(`LDM scaffolded for agent "${agentId}"`);
294
+ console.log(` Root: ${paths.root}`);
295
+ console.log(` Crystal DB: ${paths.crystalDb}`);
296
+ console.log(` Transcripts: ${paths.transcripts}`);
297
+ console.log(` Sessions: ${paths.sessions}`);
298
+ console.log(` Daily: ${paths.daily}`);
299
+ console.log(` Journals: ${paths.journals}`);
300
+ return;
301
+ }
302
+
303
+ if (command === 'migrate-db') {
304
+ const paths = ensureLdm();
305
+ const HOME = process.env.HOME || '';
306
+ const legacyDir = join(HOME, '.openclaw', 'memory-crystal');
307
+ const legacyDb = join(legacyDir, 'crystal.db');
308
+ const destDb = paths.crystalDb;
309
+
310
+ if (!existsSync(legacyDb)) {
311
+ console.error(`Source not found: ${legacyDb}`);
312
+ process.exit(1);
313
+ }
314
+
315
+ if (existsSync(destDb)) {
316
+ try {
317
+ const stat = lstatSync(destDb);
318
+ if (!stat.isSymbolicLink()) {
319
+ console.error(`Destination already exists (not a symlink): ${destDb}`);
320
+ console.error('If this is from a previous migration, remove it first.');
321
+ process.exit(1);
322
+ }
323
+ } catch {}
324
+ }
325
+
326
+ console.log(`Copying ${legacyDb} -> ${destDb}`);
327
+ copyFileSync(legacyDb, destDb);
328
+
329
+ // Verify copy
330
+ const Database = (await import('better-sqlite3')).default;
331
+ const db = new Database(destDb, { readonly: true });
332
+ const row = db.prepare('SELECT COUNT(*) as count FROM chunks').get() as any;
333
+ db.close();
334
+ console.log(`Verified: ${row.count.toLocaleString()} chunks in destination DB`);
335
+
336
+ // Symlink legacy path to new location
337
+ try {
338
+ const stat = lstatSync(legacyDb);
339
+ if (!stat.isSymbolicLink()) {
340
+ unlinkSync(legacyDb);
341
+ symlinkSync(destDb, legacyDb);
342
+ console.log(`Symlinked ${legacyDb} -> ${destDb}`);
343
+ }
344
+ } catch (err: any) {
345
+ console.error(`Symlink failed (non-fatal): ${err.message}`);
346
+ }
347
+
348
+ console.log('Migration complete. Restart gateway to use new path.');
349
+ return;
350
+ }
351
+ }
352
+
353
+ main().catch(err => {
354
+ console.error(`Error: ${err.message}`);
355
+ process.exit(1);
356
+ });