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
@@ -0,0 +1,148 @@
1
+ // src/summarize.ts
2
+ import { writeFileSync, existsSync, mkdirSync } from "fs";
3
+ import { join } from "path";
4
+ import https from "https";
5
+ import http from "http";
6
+ var SUMMARY_MODE = process.env.CRYSTAL_SUMMARY_MODE || "simple";
7
+ var SUMMARY_PROVIDER = process.env.CRYSTAL_SUMMARY_PROVIDER || "openai";
8
+ var SUMMARY_MODEL = process.env.CRYSTAL_SUMMARY_MODEL || "gpt-4o-mini";
9
+ function generateSimpleSummary(messages) {
10
+ const firstUser = messages.find((m) => m.role === "user");
11
+ const title = firstUser ? firstUser.text.slice(0, 80).replace(/\n/g, " ").trim() : "Untitled Session";
12
+ const slug = title.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "").slice(0, 50);
13
+ const preview = messages.slice(0, 10).map((m) => {
14
+ const roleLabel = m.role === "user" ? "User" : "Assistant";
15
+ const snippet = m.text.slice(0, 200).replace(/\n/g, " ").trim();
16
+ return `**${roleLabel}:** ${snippet}${m.text.length > 200 ? "..." : ""}`;
17
+ }).join("\n\n");
18
+ const date = messages[0]?.timestamp?.slice(0, 10) || (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
19
+ return {
20
+ title,
21
+ slug,
22
+ summary: preview,
23
+ topics: [],
24
+ messageCount: messages.length,
25
+ date
26
+ };
27
+ }
28
+ async function generateLlmSummary(messages) {
29
+ const condensed = messages.slice(0, 30).map((m) => {
30
+ const roleLabel = m.role === "user" ? "User" : "Assistant";
31
+ const text = m.text.slice(0, 500);
32
+ return `${roleLabel}: ${text}`;
33
+ }).join("\n\n");
34
+ const prompt = `Summarize this conversation. Return JSON only, no markdown fences.
35
+
36
+ Format:
37
+ {"title": "short title", "slug": "url-safe-slug", "summary": "2-4 sentences", "topics": ["topic1", "topic2"]}
38
+
39
+ Conversation:
40
+ ${condensed}`;
41
+ const apiKey = process.env.OPENAI_API_KEY;
42
+ if (!apiKey) {
43
+ return generateSimpleSummary(messages);
44
+ }
45
+ try {
46
+ const body = JSON.stringify({
47
+ model: SUMMARY_MODEL,
48
+ messages: [{ role: "user", content: prompt }],
49
+ temperature: 0.3,
50
+ max_tokens: 300
51
+ });
52
+ const result = await httpPost("https://api.openai.com/v1/chat/completions", body, {
53
+ "Authorization": `Bearer ${apiKey}`,
54
+ "Content-Type": "application/json"
55
+ });
56
+ const parsed = JSON.parse(result);
57
+ const content = parsed.choices?.[0]?.message?.content || "";
58
+ const jsonStr = content.replace(/```json?\n?/g, "").replace(/```/g, "").trim();
59
+ const data = JSON.parse(jsonStr);
60
+ const date = messages[0]?.timestamp?.slice(0, 10) || (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
61
+ return {
62
+ title: data.title || "Untitled",
63
+ slug: (data.slug || "untitled").slice(0, 50),
64
+ summary: data.summary || "",
65
+ topics: data.topics || [],
66
+ messageCount: messages.length,
67
+ date
68
+ };
69
+ } catch {
70
+ return generateSimpleSummary(messages);
71
+ }
72
+ }
73
+ function httpPost(url, body, headers) {
74
+ return new Promise((resolve, reject) => {
75
+ const parsed = new URL(url);
76
+ const client = parsed.protocol === "https:" ? https : http;
77
+ const req = client.request({
78
+ hostname: parsed.hostname,
79
+ port: parsed.port,
80
+ path: parsed.pathname + parsed.search,
81
+ method: "POST",
82
+ headers: { ...headers, "Content-Length": Buffer.byteLength(body) },
83
+ timeout: 3e4
84
+ }, (res) => {
85
+ let data = "";
86
+ res.on("data", (chunk) => {
87
+ data += chunk;
88
+ });
89
+ res.on("end", () => {
90
+ if (res.statusCode && res.statusCode >= 200 && res.statusCode < 300) {
91
+ resolve(data);
92
+ } else {
93
+ reject(new Error(`HTTP ${res.statusCode}: ${data.slice(0, 200)}`));
94
+ }
95
+ });
96
+ });
97
+ req.on("error", reject);
98
+ req.on("timeout", () => {
99
+ req.destroy();
100
+ reject(new Error("Request timeout"));
101
+ });
102
+ req.write(body);
103
+ req.end();
104
+ });
105
+ }
106
+ async function generateSessionSummary(messages) {
107
+ if (SUMMARY_MODE === "llm") {
108
+ return generateLlmSummary(messages);
109
+ }
110
+ return generateSimpleSummary(messages);
111
+ }
112
+ function formatSummaryMarkdown(summary, sessionId) {
113
+ const lines = [];
114
+ lines.push(`# ${summary.title}`);
115
+ lines.push("");
116
+ lines.push(`**Session:** ${sessionId} **Date:** ${summary.date} **Messages:** ${summary.messageCount}`);
117
+ lines.push("");
118
+ lines.push("## Summary");
119
+ lines.push("");
120
+ lines.push(summary.summary);
121
+ if (summary.topics.length > 0) {
122
+ lines.push("");
123
+ lines.push("## Key Topics");
124
+ lines.push("");
125
+ for (const topic of summary.topics) {
126
+ lines.push(`- ${topic}`);
127
+ }
128
+ }
129
+ lines.push("");
130
+ return lines.join("\n");
131
+ }
132
+ function writeSummaryFile(sessionsDir, summary, agentId, sessionId) {
133
+ if (!existsSync(sessionsDir)) mkdirSync(sessionsDir, { recursive: true });
134
+ const now = /* @__PURE__ */ new Date();
135
+ const dateStr = now.toISOString().slice(0, 10);
136
+ const timeStr = now.toISOString().slice(11, 19).replace(/:/g, "-");
137
+ const filename = `${dateStr}--${timeStr}--${agentId}--${summary.slug}.md`;
138
+ const filepath = join(sessionsDir, filename);
139
+ const content = formatSummaryMarkdown(summary, sessionId);
140
+ writeFileSync(filepath, content);
141
+ return filepath;
142
+ }
143
+
144
+ export {
145
+ generateSessionSummary,
146
+ formatSummaryMarkdown,
147
+ writeSummaryFile
148
+ };
package/dist/cli.d.ts ADDED
@@ -0,0 +1 @@
1
+ #!/usr/bin/env node
package/dist/cli.js ADDED
@@ -0,0 +1,325 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ Crystal,
4
+ resolveConfig
5
+ } from "./chunk-52QE3YI3.js";
6
+ import {
7
+ ensureLdm,
8
+ getAgentId,
9
+ scaffoldLdm
10
+ } from "./chunk-PJ6FFKEX.js";
11
+
12
+ // src/cli.ts
13
+ import { existsSync, copyFileSync, symlinkSync, lstatSync, unlinkSync } from "fs";
14
+ import { join } from "path";
15
+ var USAGE = `
16
+ crystal \u2014 Sovereign memory system
17
+
18
+ Commands:
19
+ crystal search <query> [-n limit] [--agent <id>] [--provider <openai|ollama|google>]
20
+ crystal remember <text> [--category fact|preference|event|opinion|skill]
21
+ crystal forget <id>
22
+ crystal status [--provider <openai|ollama|google>]
23
+
24
+ crystal sources add <path> --name <name> Add a directory for source indexing
25
+ crystal sources sync <name> [--dry-run] Sync (re-index changed files)
26
+ crystal sources status Show all indexed collections
27
+ crystal sources remove <name> Remove a collection
28
+
29
+ crystal init [--agent <id>] Scaffold ~/.ldm/ directory tree
30
+ crystal migrate-db Move crystal.db to ~/.ldm/memory/
31
+
32
+ Environment:
33
+ CRYSTAL_EMBEDDING_PROVIDER openai | ollama | google (default: openai)
34
+ CRYSTAL_OLLAMA_HOST Ollama URL (default: http://localhost:11434)
35
+ CRYSTAL_REMOTE_URL Worker URL for cloud mirror mode
36
+ CRYSTAL_REMOTE_TOKEN Auth token for cloud mirror
37
+ CRYSTAL_AGENT_ID Agent identifier (default: cc-mini)
38
+ `.trim();
39
+ async function main() {
40
+ const args = process.argv.slice(2);
41
+ if (args.length === 0 || args[0] === "--help" || args[0] === "-h") {
42
+ console.log(USAGE);
43
+ process.exit(0);
44
+ }
45
+ const command = args[0];
46
+ const flags = {};
47
+ let positional = [];
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
+ if (command === "init" || command === "migrate-db") {
59
+ await handleLdmCommand(command, flags);
60
+ return;
61
+ }
62
+ const overrides = {};
63
+ if (flags.provider) overrides.embeddingProvider = flags.provider;
64
+ const config = resolveConfig(overrides);
65
+ const crystal = new Crystal(config);
66
+ await crystal.init();
67
+ try {
68
+ switch (command) {
69
+ case "search": {
70
+ const query = positional.join(" ");
71
+ if (!query) {
72
+ console.error("Usage: crystal search <query>");
73
+ process.exit(1);
74
+ }
75
+ const limit = parseInt(flags.n || "5", 10);
76
+ const filter = {};
77
+ if (flags.agent) filter.agent_id = flags.agent;
78
+ const results = await crystal.search(query, limit, filter);
79
+ if (results.length === 0) {
80
+ console.log("No results found.");
81
+ } else {
82
+ for (const r of results) {
83
+ const score = (r.score * 100).toFixed(1);
84
+ const date = r.created_at?.slice(0, 10) || "unknown";
85
+ console.log(`[${score}%] [${r.agent_id}] [${date}] [${r.role}]`);
86
+ console.log(r.text.slice(0, 300) + (r.text.length > 300 ? "..." : ""));
87
+ console.log("---");
88
+ }
89
+ }
90
+ break;
91
+ }
92
+ case "remember": {
93
+ const text = positional.join(" ");
94
+ if (!text) {
95
+ console.error("Usage: crystal remember <text>");
96
+ process.exit(1);
97
+ }
98
+ const category = flags.category || "fact";
99
+ const id = await crystal.remember(text, category);
100
+ console.log(`Remembered (id: ${id}, category: ${category}): ${text}`);
101
+ break;
102
+ }
103
+ case "forget": {
104
+ const id = parseInt(positional[0], 10);
105
+ if (isNaN(id)) {
106
+ console.error("Usage: crystal forget <id>");
107
+ process.exit(1);
108
+ }
109
+ const ok = crystal.forget(id);
110
+ console.log(ok ? `Forgot memory ${id}` : `Memory ${id} not found or already deprecated`);
111
+ break;
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
+ case "sources": {
125
+ const subCommand = positional[0];
126
+ if (!subCommand) {
127
+ console.error("Usage: crystal sources <add|sync|status|remove> ...");
128
+ process.exit(1);
129
+ }
130
+ switch (subCommand) {
131
+ case "add": {
132
+ const path = positional[1];
133
+ const name = flags.name;
134
+ if (!path || !name) {
135
+ console.error("Usage: crystal sources add <path> --name <name>");
136
+ process.exit(1);
137
+ }
138
+ const col = await crystal.sourcesAdd(path, name);
139
+ console.log(`Added collection "${col.name}" at ${col.root_path}`);
140
+ console.log(`Run "crystal sources sync ${name}" to index files.`);
141
+ break;
142
+ }
143
+ case "sync": {
144
+ const name = positional[1];
145
+ if (!name) {
146
+ console.error("Usage: crystal sources sync <name> [--dry-run]");
147
+ process.exit(1);
148
+ }
149
+ const dryRun = "dry-run" in flags;
150
+ if (dryRun) {
151
+ console.log(`Dry run for "${name}"...`);
152
+ } else {
153
+ console.log(`Syncing "${name}"...`);
154
+ }
155
+ const result = await crystal.sourcesSync(name, { dryRun });
156
+ console.log(` Added: ${result.added} files`);
157
+ console.log(` Updated: ${result.updated} files`);
158
+ console.log(` Removed: ${result.removed} files`);
159
+ console.log(` Chunks: ${result.chunks_added} embedded`);
160
+ console.log(` Time: ${(result.duration_ms / 1e3).toFixed(1)}s`);
161
+ break;
162
+ }
163
+ case "status": {
164
+ const status = crystal.sourcesStatus();
165
+ if (status.collections.length === 0) {
166
+ console.log('No source collections. Use "crystal sources add <path> --name <name>" to add one.');
167
+ } else {
168
+ console.log("Source Collections:");
169
+ for (const col of status.collections) {
170
+ const syncAgo = col.last_sync_at ? `${Math.round((Date.now() - new Date(col.last_sync_at).getTime()) / 6e4)}m ago` : "never";
171
+ console.log(` ${col.name}: ${col.file_count.toLocaleString()} files, ${col.chunk_count.toLocaleString()} chunks, last sync ${syncAgo}`);
172
+ }
173
+ console.log(` Total: ${status.total_files.toLocaleString()} files, ${status.total_chunks.toLocaleString()} chunks`);
174
+ }
175
+ break;
176
+ }
177
+ case "remove": {
178
+ const name = positional[1];
179
+ if (!name) {
180
+ console.error("Usage: crystal sources remove <name>");
181
+ process.exit(1);
182
+ }
183
+ const ok = crystal.sourcesRemove(name);
184
+ console.log(ok ? `Removed collection "${name}"` : `Collection "${name}" not found`);
185
+ break;
186
+ }
187
+ default:
188
+ console.error(`Unknown sources subcommand: ${subCommand}`);
189
+ process.exit(1);
190
+ }
191
+ break;
192
+ }
193
+ case "init": {
194
+ const agentId = flags.agent || getAgentId();
195
+ const paths = scaffoldLdm(agentId);
196
+ console.log(`LDM scaffolded for agent "${agentId}"`);
197
+ console.log(` Root: ${paths.root}`);
198
+ console.log(` Crystal DB: ${paths.crystalDb}`);
199
+ console.log(` Transcripts: ${paths.transcripts}`);
200
+ console.log(` Sessions: ${paths.sessions}`);
201
+ console.log(` Daily: ${paths.daily}`);
202
+ console.log(` Journals: ${paths.journals}`);
203
+ break;
204
+ }
205
+ case "migrate-db": {
206
+ const paths = ensureLdm();
207
+ const HOME = process.env.HOME || "";
208
+ const legacyDir = join(HOME, ".openclaw", "memory-crystal");
209
+ const legacyDb = join(legacyDir, "crystal.db");
210
+ const destDb = paths.crystalDb;
211
+ if (!existsSync(legacyDb)) {
212
+ console.error(`Source not found: ${legacyDb}`);
213
+ process.exit(1);
214
+ }
215
+ if (existsSync(destDb)) {
216
+ try {
217
+ const stat = lstatSync(destDb);
218
+ if (!stat.isSymbolicLink()) {
219
+ console.error(`Destination already exists (not a symlink): ${destDb}`);
220
+ console.error("If this is from a previous migration, remove it first.");
221
+ process.exit(1);
222
+ }
223
+ } catch {
224
+ }
225
+ }
226
+ console.log(`Copying ${legacyDb} -> ${destDb}`);
227
+ copyFileSync(legacyDb, destDb);
228
+ const Database = (await import("better-sqlite3")).default;
229
+ const db = new Database(destDb, { readonly: true });
230
+ const row = db.prepare("SELECT COUNT(*) as count FROM chunks").get();
231
+ db.close();
232
+ console.log(`Verified: ${row.count.toLocaleString()} chunks in destination DB`);
233
+ if (existsSync(legacyDb)) {
234
+ try {
235
+ const stat = lstatSync(legacyDb);
236
+ if (!stat.isSymbolicLink()) {
237
+ unlinkSync(legacyDb);
238
+ symlinkSync(destDb, legacyDb);
239
+ console.log(`Symlinked ${legacyDb} -> ${destDb}`);
240
+ }
241
+ } catch (err) {
242
+ console.error(`Symlink failed (non-fatal): ${err.message}`);
243
+ }
244
+ }
245
+ const legacyLance = join(legacyDir, "lance");
246
+ if (existsSync(legacyLance)) {
247
+ try {
248
+ const stat = lstatSync(legacyLance);
249
+ if (!stat.isSymbolicLink()) {
250
+ console.log(`Note: lance/ at ${legacyLance} left in place (LanceDB will use new path on next write)`);
251
+ }
252
+ } catch {
253
+ }
254
+ }
255
+ console.log("Migration complete. Restart gateway to use new path.");
256
+ break;
257
+ }
258
+ default:
259
+ console.error(`Unknown command: ${command}`);
260
+ console.log(USAGE);
261
+ process.exit(1);
262
+ }
263
+ } finally {
264
+ crystal.close();
265
+ }
266
+ }
267
+ async function handleLdmCommand(command, flags) {
268
+ if (command === "init") {
269
+ const agentId = flags.agent || getAgentId();
270
+ const paths = scaffoldLdm(agentId);
271
+ console.log(`LDM scaffolded for agent "${agentId}"`);
272
+ console.log(` Root: ${paths.root}`);
273
+ console.log(` Crystal DB: ${paths.crystalDb}`);
274
+ console.log(` Transcripts: ${paths.transcripts}`);
275
+ console.log(` Sessions: ${paths.sessions}`);
276
+ console.log(` Daily: ${paths.daily}`);
277
+ console.log(` Journals: ${paths.journals}`);
278
+ return;
279
+ }
280
+ if (command === "migrate-db") {
281
+ const paths = ensureLdm();
282
+ const HOME = process.env.HOME || "";
283
+ const legacyDir = join(HOME, ".openclaw", "memory-crystal");
284
+ const legacyDb = join(legacyDir, "crystal.db");
285
+ const destDb = paths.crystalDb;
286
+ if (!existsSync(legacyDb)) {
287
+ console.error(`Source not found: ${legacyDb}`);
288
+ process.exit(1);
289
+ }
290
+ if (existsSync(destDb)) {
291
+ try {
292
+ const stat = lstatSync(destDb);
293
+ if (!stat.isSymbolicLink()) {
294
+ console.error(`Destination already exists (not a symlink): ${destDb}`);
295
+ console.error("If this is from a previous migration, remove it first.");
296
+ process.exit(1);
297
+ }
298
+ } catch {
299
+ }
300
+ }
301
+ console.log(`Copying ${legacyDb} -> ${destDb}`);
302
+ copyFileSync(legacyDb, destDb);
303
+ const Database = (await import("better-sqlite3")).default;
304
+ const db = new Database(destDb, { readonly: true });
305
+ const row = db.prepare("SELECT COUNT(*) as count FROM chunks").get();
306
+ db.close();
307
+ console.log(`Verified: ${row.count.toLocaleString()} chunks in destination DB`);
308
+ try {
309
+ const stat = lstatSync(legacyDb);
310
+ if (!stat.isSymbolicLink()) {
311
+ unlinkSync(legacyDb);
312
+ symlinkSync(destDb, legacyDb);
313
+ console.log(`Symlinked ${legacyDb} -> ${destDb}`);
314
+ }
315
+ } catch (err) {
316
+ console.error(`Symlink failed (non-fatal): ${err.message}`);
317
+ }
318
+ console.log("Migration complete. Restart gateway to use new path.");
319
+ return;
320
+ }
321
+ }
322
+ main().catch((err) => {
323
+ console.error(`Error: ${err.message}`);
324
+ process.exit(1);
325
+ });
package/dist/core.d.ts ADDED
@@ -0,0 +1,188 @@
1
+ interface CrystalConfig {
2
+ /** Root directory for all crystal data */
3
+ dataDir: string;
4
+ /** Embedding provider: 'openai' | 'ollama' | 'google' */
5
+ embeddingProvider: 'openai' | 'ollama' | 'google';
6
+ /** OpenAI API key (required if provider is 'openai') */
7
+ openaiApiKey?: string;
8
+ /** OpenAI embedding model (default: text-embedding-3-small) */
9
+ openaiModel?: string;
10
+ /** Ollama host (default: http://localhost:11434) */
11
+ ollamaHost?: string;
12
+ /** Ollama model (default: nomic-embed-text) */
13
+ ollamaModel?: string;
14
+ /** Google API key (required if provider is 'google') */
15
+ googleApiKey?: string;
16
+ /** Google embedding model (default: text-embedding-004) */
17
+ googleModel?: string;
18
+ /** Remote Worker URL for cloud mirror mode */
19
+ remoteUrl?: string;
20
+ /** Remote auth token */
21
+ remoteToken?: string;
22
+ }
23
+ interface Chunk {
24
+ id?: number;
25
+ text: string;
26
+ embedding?: number[];
27
+ role: 'user' | 'assistant' | 'system';
28
+ source_type: string;
29
+ source_id: string;
30
+ agent_id: string;
31
+ token_count: number;
32
+ created_at: string;
33
+ }
34
+ interface Memory {
35
+ id?: number;
36
+ text: string;
37
+ embedding?: number[];
38
+ category: 'fact' | 'preference' | 'event' | 'opinion' | 'skill';
39
+ confidence: number;
40
+ source_ids: string;
41
+ status: 'active' | 'deprecated' | 'deleted';
42
+ created_at: string;
43
+ updated_at: string;
44
+ }
45
+ interface SearchResult {
46
+ text: string;
47
+ role: string;
48
+ score: number;
49
+ source_type: string;
50
+ source_id: string;
51
+ agent_id: string;
52
+ created_at: string;
53
+ freshness?: "fresh" | "recent" | "aging" | "stale";
54
+ }
55
+ interface CrystalStatus {
56
+ chunks: number;
57
+ memories: number;
58
+ sources: number;
59
+ agents: string[];
60
+ oldestChunk: string | null;
61
+ newestChunk: string | null;
62
+ embeddingProvider: string;
63
+ dataDir: string;
64
+ capturedSessions: number;
65
+ latestCapture: string | null;
66
+ }
67
+ interface SourceCollection {
68
+ id?: number;
69
+ name: string;
70
+ root_path: string;
71
+ glob_patterns: string;
72
+ ignore_patterns: string;
73
+ file_count: number;
74
+ chunk_count: number;
75
+ last_sync_at: string | null;
76
+ created_at: string;
77
+ }
78
+ interface SourceFile {
79
+ id?: number;
80
+ collection_id: number;
81
+ file_path: string;
82
+ file_hash: string;
83
+ file_size: number;
84
+ chunk_count: number;
85
+ last_indexed_at: string;
86
+ }
87
+ interface SourcesStatus {
88
+ collections: Array<{
89
+ name: string;
90
+ root_path: string;
91
+ file_count: number;
92
+ chunk_count: number;
93
+ last_sync_at: string | null;
94
+ }>;
95
+ total_files: number;
96
+ total_chunks: number;
97
+ }
98
+ interface SyncResult {
99
+ collection: string;
100
+ added: number;
101
+ updated: number;
102
+ removed: number;
103
+ chunks_added: number;
104
+ duration_ms: number;
105
+ }
106
+ declare class Crystal {
107
+ private config;
108
+ private lanceDb;
109
+ private sqliteDb;
110
+ private chunksTable;
111
+ private vecDimensions;
112
+ constructor(config: CrystalConfig);
113
+ init(): Promise<void>;
114
+ private initSqliteTables;
115
+ private initChunksTables;
116
+ private ensureVecTable;
117
+ private initLanceTables;
118
+ embed(texts: string[]): Promise<number[][]>;
119
+ chunkText(text: string, targetTokens?: number, overlapTokens?: number): string[];
120
+ ingest(chunks: Chunk[]): Promise<number>;
121
+ private recencyWeight;
122
+ private freshnessLabel;
123
+ search(query: string, limit?: number, filter?: {
124
+ agent_id?: string;
125
+ source_type?: string;
126
+ }): Promise<SearchResult[]>;
127
+ /** Vector search via sqlite-vec. Two-step pattern: MATCH first, then JOIN. */
128
+ private searchVec;
129
+ /** Full-text search via FTS5 with BM25 scoring. */
130
+ private searchFTS;
131
+ /** Build a safe FTS5 query from user input. */
132
+ private buildFTS5Query;
133
+ /**
134
+ * Reciprocal Rank Fusion. Ported from QMD (MIT License, Tobi Lutke, 2024-2026).
135
+ * Fuses multiple ranked result lists into one using RRF scoring.
136
+ * Uses text content as dedup key (instead of QMD's file path).
137
+ */
138
+ private reciprocalRankFusion;
139
+ /** LanceDB fallback for search (used when sqlite-vec tables are empty, pre-migration). */
140
+ private searchLanceFallback;
141
+ remember(text: string, category?: Memory['category']): Promise<number>;
142
+ forget(memoryId: number): boolean;
143
+ status(): Promise<CrystalStatus>;
144
+ getCaptureState(agentId: string, sourceId: string): {
145
+ lastMessageCount: number;
146
+ captureCount: number;
147
+ };
148
+ setCaptureState(agentId: string, sourceId: string, messageCount: number, captureCount: number): void;
149
+ private static readonly DEFAULT_INCLUDE;
150
+ private static readonly DEFAULT_IGNORE;
151
+ /** Add a directory as a source collection for indexing. */
152
+ sourcesAdd(rootPath: string, name: string, options?: {
153
+ include?: string[];
154
+ ignore?: string[];
155
+ }): Promise<SourceCollection>;
156
+ /** Remove a source collection and its file records. Chunks remain in LanceDB. */
157
+ sourcesRemove(name: string): boolean;
158
+ /** Sync a collection: scan files, detect changes, re-index what changed. */
159
+ sourcesSync(name: string, options?: {
160
+ dryRun?: boolean;
161
+ batchSize?: number;
162
+ }): Promise<SyncResult>;
163
+ /** Get status of all source collections. */
164
+ sourcesStatus(): SourcesStatus;
165
+ /** Scan a directory recursively, matching include/ignore patterns. */
166
+ private scanDirectory;
167
+ close(): void;
168
+ }
169
+ declare function resolveConfig(overrides?: Partial<CrystalConfig>): CrystalConfig;
170
+ declare class RemoteCrystal {
171
+ private url;
172
+ private token;
173
+ constructor(url: string, token: string);
174
+ init(): Promise<void>;
175
+ private request;
176
+ search(query: string, limit?: number, filter?: {
177
+ agent_id?: string;
178
+ }): Promise<SearchResult[]>;
179
+ ingest(chunks: Chunk[]): Promise<number>;
180
+ remember(text: string, category?: Memory['category']): Promise<number>;
181
+ forget(memoryId: number): Promise<boolean>;
182
+ status(): Promise<CrystalStatus>;
183
+ chunkText(text: string): string[];
184
+ }
185
+ /** Create the appropriate Crystal instance based on config. */
186
+ declare function createCrystal(config: CrystalConfig): Crystal | RemoteCrystal;
187
+
188
+ export { type Chunk, Crystal, type CrystalConfig, type CrystalStatus, type Memory, RemoteCrystal, type SearchResult, type SourceCollection, type SourceFile, type SourcesStatus, type SyncResult, createCrystal, resolveConfig };
package/dist/core.js ADDED
@@ -0,0 +1,12 @@
1
+ import {
2
+ Crystal,
3
+ RemoteCrystal,
4
+ createCrystal,
5
+ resolveConfig
6
+ } from "./chunk-52QE3YI3.js";
7
+ export {
8
+ Crystal,
9
+ RemoteCrystal,
10
+ createCrystal,
11
+ resolveConfig
12
+ };
@@ -0,0 +1,16 @@
1
+ declare function loadRelayKey(): Buffer;
2
+ interface EncryptedPayload {
3
+ v: 1;
4
+ nonce: string;
5
+ ciphertext: string;
6
+ tag: string;
7
+ hmac: string;
8
+ }
9
+ declare function encrypt(plaintext: Buffer, masterKey: Buffer): EncryptedPayload;
10
+ declare function decrypt(payload: EncryptedPayload, masterKey: Buffer): Buffer;
11
+ declare function encryptJSON(data: unknown, masterKey: Buffer): EncryptedPayload;
12
+ declare function decryptJSON<T = unknown>(payload: EncryptedPayload, masterKey: Buffer): T;
13
+ declare function encryptFile(filePath: string, masterKey: Buffer): EncryptedPayload;
14
+ declare function hashBuffer(data: Buffer): string;
15
+
16
+ export { type EncryptedPayload, decrypt, decryptJSON, encrypt, encryptFile, encryptJSON, hashBuffer, loadRelayKey };