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.
- package/.env.example +20 -0
- package/CHANGELOG.md +6 -0
- package/LETTERS.md +22 -0
- package/LICENSE +21 -0
- package/README-ENTERPRISE.md +162 -0
- package/README-old.md +275 -0
- package/README.md +91 -0
- package/RELAY.md +88 -0
- package/TECHNICAL.md +379 -0
- package/ai/dev-updates/2026-02-25--cc-air--phase2-architecture-pivot.md +70 -0
- package/ai/dev-updates/2026-02-25--cc-air--phase2-worker-build.md +72 -0
- package/ai/dev-updates/2026-02-26--10-25-16--cc-mini--phase2-implementation.md +49 -0
- package/ai/dev-updates/2026-02-27--20-30-00--cc-mini--readme-overhaul-and-public-deploy.md +69 -0
- package/ai/notes/2026-02-26--cc-air--notes.md +412 -0
- package/ai/notes/2026-02-27--cc-mini--grok-feedback.md +44 -0
- package/ai/notes/2026-02-27--cc-mini--lesa-feedback.md +45 -0
- package/ai/notes/RESEARCH.md +1185 -0
- package/ai/notes/salience-research/README.md +29 -0
- package/ai/notes/salience-research/eurosla-salience-review.md +64 -0
- package/ai/notes/salience-research/full-research-summary.md +269 -0
- package/ai/notes/salience-research/salience-levels-diagram.png +0 -0
- package/ai/plan/2026-02-27--cc-mini--qr-pairing-spec.md +203 -0
- package/ai/plan/_archive/PLAN.md +194 -0
- package/ai/plan/_archive/PRD.md +1014 -0
- package/ai/plan/cc-plans-duplicates-from-dot-claude/2026-02-26--cc-mini--phase2-implementation-plan.md +245 -0
- package/ai/plan/dev-conventions-note.md +70 -0
- package/ai/plan/ldm-os-install-and-boot-architecture.md +285 -0
- package/ai/plan/memory-crystal-phase2-plan.md +192 -0
- package/ai/plan/memory-system-lay-of-the-land.md +214 -0
- package/ai/plan/phase2-ephemeral-relay.md +238 -0
- package/ai/plan/readme-first.md +68 -0
- package/ai/plan/roadmap.md +159 -0
- package/ai/todos/PUNCHLIST.md +44 -0
- package/ai/todos/README.md +31 -0
- package/ai/todos/inboxes/cc-air/2026-02-26--cc-air--post-relay-todos.md +85 -0
- package/ai/todos/inboxes/cc-mini/2026-02-26--cc-mini--phase2-status.md +100 -0
- package/ai/todos/inboxes/cc-mini/_archive/TODO.md +25 -0
- package/ai/todos/inboxes/parker/2026-02-25--cc-air--setup-checklist.md +139 -0
- package/ai/todos/inboxes/parker/2026-02-26--cc-mini--phase2-your-moves.md +72 -0
- package/dist/cc-hook.d.ts +1 -0
- package/dist/cc-hook.js +349 -0
- package/dist/chunk-3VFIJYS4.js +818 -0
- package/dist/chunk-52QE3YI3.js +1169 -0
- package/dist/chunk-AA3OPP4Z.js +432 -0
- package/dist/chunk-D3I3ZSE2.js +411 -0
- package/dist/chunk-EKSACBTJ.js +1070 -0
- package/dist/chunk-F3Y7EL7K.js +83 -0
- package/dist/chunk-JWZXYVET.js +1068 -0
- package/dist/chunk-KYVWO6ZM.js +1069 -0
- package/dist/chunk-L3VHARQH.js +413 -0
- package/dist/chunk-LOVAHSQV.js +411 -0
- package/dist/chunk-LQOYCAGG.js +446 -0
- package/dist/chunk-MK42FMEG.js +147 -0
- package/dist/chunk-NIJCVN3O.js +147 -0
- package/dist/chunk-O2UITJGH.js +465 -0
- package/dist/chunk-PEK6JH65.js +432 -0
- package/dist/chunk-PJ6FFKEX.js +77 -0
- package/dist/chunk-PLUBBZYR.js +800 -0
- package/dist/chunk-SGL6ISBJ.js +1061 -0
- package/dist/chunk-UNHVZB5G.js +411 -0
- package/dist/chunk-VAFTWSTE.js +1061 -0
- package/dist/chunk-XZ3S56RQ.js +1061 -0
- package/dist/chunk-Y72C7F6O.js +148 -0
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +325 -0
- package/dist/core.d.ts +188 -0
- package/dist/core.js +12 -0
- package/dist/crypto.d.ts +16 -0
- package/dist/crypto.js +18 -0
- package/dist/dev-update-SZ2Z4WCQ.js +6 -0
- package/dist/ldm.d.ts +17 -0
- package/dist/ldm.js +12 -0
- package/dist/mcp-server.d.ts +1 -0
- package/dist/mcp-server.js +250 -0
- package/dist/migrate.d.ts +1 -0
- package/dist/migrate.js +89 -0
- package/dist/mirror-sync.d.ts +1 -0
- package/dist/mirror-sync.js +130 -0
- package/dist/openclaw.d.ts +5 -0
- package/dist/openclaw.js +349 -0
- package/dist/poller.d.ts +1 -0
- package/dist/poller.js +272 -0
- package/dist/summarize.d.ts +19 -0
- package/dist/summarize.js +10 -0
- package/dist/worker.js +137 -0
- package/openclaw.plugin.json +11 -0
- package/package.json +40 -0
- package/scripts/migrate-lance-to-sqlite.mjs +217 -0
- package/skills/memory/SKILL.md +61 -0
- package/src/cc-hook.ts +447 -0
- package/src/cli.ts +356 -0
- package/src/core.ts +1472 -0
- package/src/crypto.ts +113 -0
- package/src/dev-update.ts +178 -0
- package/src/ldm.ts +117 -0
- package/src/mcp-server.ts +274 -0
- package/src/migrate.ts +104 -0
- package/src/mirror-sync.ts +175 -0
- package/src/openclaw.ts +250 -0
- package/src/poller.ts +345 -0
- package/src/summarize.ts +210 -0
- package/src/worker.ts +208 -0
- package/tsconfig.json +18 -0
- 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
|
+
});
|