@wipcomputer/memory-crystal 0.7.32 → 0.7.33
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/SKILL.md +1 -1
- package/cloud/wrangler.toml +30 -0
- package/dist/bridge.d.ts +7 -0
- package/dist/bridge.js +14 -0
- package/dist/bulk-copy.d.ts +17 -0
- package/dist/bulk-copy.js +90 -0
- package/dist/cc-hook.d.ts +8 -0
- package/dist/cc-hook.js +368 -0
- package/dist/cc-poller.d.ts +1 -0
- package/dist/cc-poller.js +550 -0
- package/dist/chunk-25LXQJ4Z.js +110 -0
- package/dist/chunk-2DRXIRQW.js +97 -0
- package/dist/chunk-2GBYLMEF.js +1385 -0
- package/dist/chunk-2ZNH5F6E.js +1281 -0
- package/dist/chunk-3G3SFYYI.js +288 -0
- package/dist/chunk-3RG5ZIWI.js +10 -0
- package/dist/chunk-3S6TI23B.js +97 -0
- package/dist/chunk-3VFIJYS4.js +818 -0
- package/dist/chunk-437F27T6.js +97 -0
- package/dist/chunk-52QE3YI3.js +1169 -0
- package/dist/chunk-57RP3DIN.js +1205 -0
- package/dist/chunk-5HSZ4W2P.js +62 -0
- package/dist/chunk-5I7GMRDN.js +146 -0
- package/dist/chunk-645IPXW3.js +290 -0
- package/dist/chunk-7A7ELD4C.js +1205 -0
- package/dist/chunk-7FYY4GZM.js +1205 -0
- package/dist/chunk-7IUE7ODU.js +254 -0
- package/dist/chunk-7RMLKZIS.js +108 -0
- package/dist/chunk-AA3OPP4Z.js +432 -0
- package/dist/chunk-AEWLSYPH.js +72 -0
- package/dist/chunk-ASSZDR6I.js +108 -0
- package/dist/chunk-AYRJVWUC.js +1205 -0
- package/dist/chunk-CCYI5O3D.js +148 -0
- package/dist/chunk-CGIDSAJB.js +288 -0
- package/dist/chunk-D3I3ZSE2.js +411 -0
- package/dist/chunk-D3MACYZ4.js +108 -0
- package/dist/chunk-DACSKLY6.js +219 -0
- package/dist/chunk-DFQ72B7M.js +248 -0
- package/dist/chunk-DW5B4BL7.js +108 -0
- package/dist/chunk-EKSACBTJ.js +1070 -0
- package/dist/chunk-EXEZZADG.js +248 -0
- package/dist/chunk-F3Y7EL7K.js +83 -0
- package/dist/chunk-FBQWSDPC.js +1328 -0
- package/dist/chunk-FHRZNOMW.js +1205 -0
- package/dist/chunk-IM7N24MT.js +129 -0
- package/dist/chunk-IPNYIXFK.js +1178 -0
- package/dist/chunk-J7MRSZIO.js +167 -0
- package/dist/chunk-JITKI2OI.js +106 -0
- package/dist/chunk-JWZXYVET.js +1068 -0
- package/dist/chunk-KCQUXVYT.js +108 -0
- package/dist/chunk-KOQ43OX6.js +1281 -0
- package/dist/chunk-KYVWO6ZM.js +1069 -0
- package/dist/chunk-L3VHARQH.js +413 -0
- package/dist/chunk-LBWDS6BE.js +288 -0
- package/dist/chunk-LOVAHSQV.js +411 -0
- package/dist/chunk-LQOYCAGG.js +446 -0
- package/dist/chunk-LWAIPJ2W.js +146 -0
- package/dist/chunk-M5DHKW7M.js +127 -0
- package/dist/chunk-MBKCIJHM.js +1328 -0
- package/dist/chunk-MK42FMEG.js +147 -0
- package/dist/chunk-MOBMYHKL.js +1205 -0
- package/dist/chunk-MPLTNMRG.js +67 -0
- package/dist/chunk-NIJCVN3O.js +147 -0
- package/dist/chunk-NX647OM3.js +310 -0
- package/dist/chunk-NZCFSZQ7.js +1205 -0
- package/dist/chunk-O2UITJGH.js +465 -0
- package/dist/chunk-OCRA44AZ.js +108 -0
- package/dist/chunk-P3KJR66H.js +117 -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-PNKVD2UK.js +26 -0
- package/dist/chunk-PSQZURHO.js +229 -0
- package/dist/chunk-SGL6ISBJ.js +1061 -0
- package/dist/chunk-SJABZZT5.js +97 -0
- package/dist/chunk-TD3P3K32.js +1199 -0
- package/dist/chunk-TMDZJJKV.js +288 -0
- package/dist/chunk-UNHVZB5G.js +411 -0
- package/dist/chunk-VAFTWSTE.js +1061 -0
- package/dist/chunk-VNFXFQBB.js +217 -0
- package/dist/chunk-X3GVFKSJ.js +1205 -0
- package/dist/chunk-XZ3S56RQ.js +1061 -0
- package/dist/chunk-Y72C7F6O.js +148 -0
- package/dist/chunk-YLICP577.js +1205 -0
- package/dist/chunk-YX6AXLVK.js +159 -0
- package/dist/chunk-ZCQYHTNU.js +146 -0
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +1160 -0
- package/dist/cloud-crystal.js +6 -0
- package/dist/core.d.ts +252 -0
- package/dist/core.js +12 -0
- package/dist/crypto.d.ts +20 -0
- package/dist/crypto.js +27 -0
- package/dist/crystal-capture.sh +29 -0
- package/dist/crystal-serve.d.ts +4 -0
- package/dist/crystal-serve.js +252 -0
- package/dist/dev-update-SZ2Z4WCQ.js +6 -0
- package/dist/discover.d.ts +30 -0
- package/dist/discover.js +177 -0
- package/dist/doctor.d.ts +9 -0
- package/dist/doctor.js +342 -0
- package/dist/dream-weaver.d.ts +8 -0
- package/dist/dream-weaver.js +56 -0
- package/dist/file-sync.d.ts +48 -0
- package/dist/file-sync.js +18 -0
- package/dist/installer.d.ts +61 -0
- package/dist/installer.js +772 -0
- package/dist/ldm-backup.sh +116 -0
- package/dist/ldm.d.ts +50 -0
- package/dist/ldm.js +32 -0
- package/dist/llm-XXLYPIOF.js +16 -0
- package/dist/mcp-server.d.ts +1 -0
- package/dist/mcp-server.js +277 -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 +159 -0
- package/dist/mlx-setup-XKU67WCT.js +289 -0
- package/dist/oc-backfill.d.ts +19 -0
- package/dist/oc-backfill.js +74 -0
- package/dist/openclaw.d.ts +5 -0
- package/dist/openclaw.js +434 -0
- package/dist/pair.d.ts +4 -0
- package/dist/pair.js +75 -0
- package/dist/poller.d.ts +1 -0
- package/dist/poller.js +634 -0
- package/dist/role.d.ts +24 -0
- package/dist/role.js +13 -0
- package/dist/search-pipeline-4K4OJSSS.js +255 -0
- package/dist/search-pipeline-4PRS6LI7.js +280 -0
- package/dist/search-pipeline-7UJMXPLO.js +280 -0
- package/dist/search-pipeline-CBV25NX7.js +99 -0
- package/dist/search-pipeline-DQTRLGBH.js +74 -0
- package/dist/search-pipeline-HNG37REH.js +282 -0
- package/dist/search-pipeline-IZFPLBUB.js +280 -0
- package/dist/search-pipeline-MID6F26Q.js +73 -0
- package/dist/search-pipeline-N52JZFNN.js +282 -0
- package/dist/search-pipeline-OPB2PRQQ.js +280 -0
- package/dist/search-pipeline-VXTE5HAD.js +262 -0
- package/dist/search-pipeline-XHFKADRG.js +73 -0
- package/dist/staging.d.ts +29 -0
- package/dist/staging.js +21 -0
- package/dist/summarize.d.ts +19 -0
- package/dist/summarize.js +10 -0
- package/dist/worker-demo.js +186 -0
- package/dist/worker-mcp.js +404 -0
- package/dist/worker.js +137 -0
- package/package.json +15 -1
- package/.env.example +0 -20
- package/.publish-skill.json +0 -1
- package/CHANGELOG.md +0 -1372
- package/README-ENTERPRISE.md +0 -226
- package/RELAY.md +0 -199
- package/wrangler-demo.toml +0 -8
- package/wrangler-mcp.toml +0 -24
package/dist/cli.js
ADDED
|
@@ -0,0 +1,1160 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
Crystal,
|
|
4
|
+
createCrystal,
|
|
5
|
+
resolveConfig
|
|
6
|
+
} from "./chunk-2GBYLMEF.js";
|
|
7
|
+
import {
|
|
8
|
+
deployBackupScript,
|
|
9
|
+
ensureLdm,
|
|
10
|
+
getAgentId,
|
|
11
|
+
installBackupLaunchAgent,
|
|
12
|
+
installCron,
|
|
13
|
+
ldmPaths,
|
|
14
|
+
removeCron
|
|
15
|
+
} from "./chunk-DFQ72B7M.js";
|
|
16
|
+
|
|
17
|
+
// src/cli.ts
|
|
18
|
+
import { existsSync, copyFileSync, symlinkSync, lstatSync, unlinkSync, readFileSync, readdirSync, statSync } from "fs";
|
|
19
|
+
import { join, basename } from "path";
|
|
20
|
+
import { execSync } from "child_process";
|
|
21
|
+
import { createInterface } from "readline";
|
|
22
|
+
var USAGE = `
|
|
23
|
+
crystal \u2014 Sovereign memory system
|
|
24
|
+
|
|
25
|
+
Commands:
|
|
26
|
+
crystal search <query> [-n limit] [--agent <id>] [--since <time>] [--until <date>] [--intent <context>] [--candidates <n>] [--explain] [--provider <openai|ollama|google>]
|
|
27
|
+
crystal remember <text> [--category fact|preference|event|opinion|skill]
|
|
28
|
+
crystal forget <id>
|
|
29
|
+
crystal status [--provider <openai|ollama|google>]
|
|
30
|
+
|
|
31
|
+
crystal sources add <path> --name <name> Add a directory for source indexing
|
|
32
|
+
crystal sources sync <name> [--dry-run] Sync (re-index changed files)
|
|
33
|
+
crystal sources status Show all indexed collections
|
|
34
|
+
crystal sources remove <name> Remove a collection
|
|
35
|
+
|
|
36
|
+
crystal role Show current role (Core/Node/Standalone)
|
|
37
|
+
crystal promote Promote this device to Crystal Core
|
|
38
|
+
crystal demote [--relay <url>] Demote this device to Crystal Node
|
|
39
|
+
crystal doctor Full health check with fix suggestions
|
|
40
|
+
|
|
41
|
+
crystal cleanup [--dry-run] Clean orphaned vec/FTS entries
|
|
42
|
+
crystal backup Run a backup now
|
|
43
|
+
crystal backup setup Install daily backup (LaunchAgent, 03:00)
|
|
44
|
+
crystal backup --keep <n> Keep last n backups (default: 7)
|
|
45
|
+
|
|
46
|
+
crystal mlx setup [--yes] Install MLX local LLM (Apple Silicon only)
|
|
47
|
+
crystal mlx status Show MLX server status
|
|
48
|
+
crystal mlx stop Stop MLX server
|
|
49
|
+
|
|
50
|
+
crystal bridge setup Install + register Bridge MCP server
|
|
51
|
+
crystal bridge status Show Bridge install state
|
|
52
|
+
|
|
53
|
+
crystal pair Show QR code with relay key (generate if none)
|
|
54
|
+
crystal pair --code <string> Accept a pairing code from another device
|
|
55
|
+
|
|
56
|
+
crystal serve [--port 18790] Crystal Core gateway (localhost HTTP server)
|
|
57
|
+
crystal dream-weave [--agent <id>] [--mode full|incremental] [--dry-run] [--since <datetime>]
|
|
58
|
+
crystal init [--agent <id>] [--core] [--node] [--pair <code>] [--import <path>] [--yes] [--skip-discover]
|
|
59
|
+
Install or update Memory Crystal
|
|
60
|
+
crystal update [--agent <id>] [--yes] Update existing install (alias for init --update)
|
|
61
|
+
crystal backfill [--agent <id>] [--dry-run] [--limit <n>] Embed raw sessions into crystal
|
|
62
|
+
crystal migrate-embeddings [--dry-run] Migrate context-embeddings into crystal
|
|
63
|
+
crystal migrate-db Move crystal.db to ~/.ldm/memory/
|
|
64
|
+
|
|
65
|
+
Environment:
|
|
66
|
+
CRYSTAL_EMBEDDING_PROVIDER openai | ollama | google (default: openai)
|
|
67
|
+
CRYSTAL_OLLAMA_HOST Ollama URL (default: http://localhost:11434)
|
|
68
|
+
CRYSTAL_REMOTE_URL Worker URL for cloud mirror mode
|
|
69
|
+
CRYSTAL_REMOTE_TOKEN Auth token for cloud mirror
|
|
70
|
+
CRYSTAL_AGENT_ID Agent identifier (default: cc-mini)
|
|
71
|
+
`.trim();
|
|
72
|
+
async function main() {
|
|
73
|
+
const args = process.argv.slice(2);
|
|
74
|
+
if (args.length === 0 || args[0] === "--help" || args[0] === "-h") {
|
|
75
|
+
console.log(USAGE);
|
|
76
|
+
process.exit(0);
|
|
77
|
+
}
|
|
78
|
+
if (args[0] === "--version" || args[0] === "-v") {
|
|
79
|
+
try {
|
|
80
|
+
const { readFileSync: readFileSync2 } = await import("fs");
|
|
81
|
+
const { dirname, join: join2 } = await import("path");
|
|
82
|
+
const { fileURLToPath } = await import("url");
|
|
83
|
+
const thisDir = dirname(fileURLToPath(import.meta.url));
|
|
84
|
+
const pkg = JSON.parse(readFileSync2(join2(thisDir, "..", "package.json"), "utf-8"));
|
|
85
|
+
console.log(pkg.version);
|
|
86
|
+
} catch {
|
|
87
|
+
console.log("unknown");
|
|
88
|
+
}
|
|
89
|
+
process.exit(0);
|
|
90
|
+
}
|
|
91
|
+
const command = args[0];
|
|
92
|
+
const flags = {};
|
|
93
|
+
let positional = [];
|
|
94
|
+
for (let i = 1; i < args.length; i++) {
|
|
95
|
+
if (args[i] === "--dry-run" || args[i] === "--yes" || args[i] === "-y" || args[i] === "--skip-discover" || args[i] === "--include-secrets" || args[i] === "--deep" || args[i] === "--core" || args[i] === "--node" || args[i] === "--update" || args[i] === "--explain") {
|
|
96
|
+
flags[args[i].replace(/^-+/, "")] = "true";
|
|
97
|
+
} else if (args[i].startsWith("--") || args[i] === "-n") {
|
|
98
|
+
const key = args[i].replace(/^-+/, "");
|
|
99
|
+
flags[key] = args[++i] || "";
|
|
100
|
+
} else {
|
|
101
|
+
positional.push(args[i]);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
if (command === "pair") {
|
|
105
|
+
const { pairShow, pairReceive } = await import("./pair.js");
|
|
106
|
+
if (flags.code) {
|
|
107
|
+
pairReceive(flags.code);
|
|
108
|
+
} else {
|
|
109
|
+
await pairShow();
|
|
110
|
+
}
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
if (command === "role") {
|
|
114
|
+
const { detectRole } = await import("./role.js");
|
|
115
|
+
const info = detectRole();
|
|
116
|
+
console.log(`Crystal Role`);
|
|
117
|
+
console.log(` Role: ${info.role} (${info.source})`);
|
|
118
|
+
console.log(` Agent ID: ${info.agentId}`);
|
|
119
|
+
console.log(` Local DB: ${info.hasLocalDb ? "yes" : "no"}`);
|
|
120
|
+
console.log(` Embeddings: ${info.hasLocalEmbeddings ? "yes (local)" : "no (relay only)"}`);
|
|
121
|
+
if (info.relayUrl) {
|
|
122
|
+
console.log(` Relay URL: ${info.relayUrl}`);
|
|
123
|
+
console.log(` Relay token: ${info.relayToken ? "set" : "NOT SET"}`);
|
|
124
|
+
console.log(` Relay key: ${info.relayKeyExists ? "present" : "NOT FOUND"}`);
|
|
125
|
+
}
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
if (command === "promote") {
|
|
129
|
+
const { promoteToCore, detectRole } = await import("./role.js");
|
|
130
|
+
promoteToCore();
|
|
131
|
+
const info = detectRole();
|
|
132
|
+
console.log("This device is now Crystal Core.");
|
|
133
|
+
console.log("All embeddings will be generated locally.");
|
|
134
|
+
console.log(`Database: ${info.hasLocalDb ? "found" : "will be created on next ingest"}`);
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
if (command === "demote") {
|
|
138
|
+
const { demoteToNode, detectRole } = await import("./role.js");
|
|
139
|
+
const relayUrl = flags.relay || positional[0];
|
|
140
|
+
demoteToNode(relayUrl);
|
|
141
|
+
console.log("This device is now Crystal Node.");
|
|
142
|
+
console.log("Conversations will be relayed to the Core for embedding.");
|
|
143
|
+
if (relayUrl) {
|
|
144
|
+
console.log(`Relay URL: ${relayUrl}`);
|
|
145
|
+
} else {
|
|
146
|
+
console.log("Set CRYSTAL_RELAY_URL in your shell profile to enable relay.");
|
|
147
|
+
}
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
if (command === "doctor") {
|
|
151
|
+
const { runDoctor } = await import("./doctor.js");
|
|
152
|
+
const checks = await runDoctor();
|
|
153
|
+
const icons = { ok: "OK", warn: "!!", fail: "XX" };
|
|
154
|
+
console.log("Crystal Doctor\n");
|
|
155
|
+
for (const check of checks) {
|
|
156
|
+
console.log(` [${icons[check.status]}] ${check.name}: ${check.detail}`);
|
|
157
|
+
if (check.fix && check.status !== "ok") {
|
|
158
|
+
console.log(` Fix: ${check.fix}`);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
const fails = checks.filter((c) => c.status === "fail").length;
|
|
162
|
+
const warns = checks.filter((c) => c.status === "warn").length;
|
|
163
|
+
console.log(`
|
|
164
|
+
${fails === 0 && warns === 0 ? "All checks passed." : `${fails} failures, ${warns} warnings.`}`);
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
if (command === "backup") {
|
|
168
|
+
const subCmd = positional[0];
|
|
169
|
+
if (subCmd === "setup") {
|
|
170
|
+
try {
|
|
171
|
+
deployBackupScript();
|
|
172
|
+
const plistPath = installBackupLaunchAgent();
|
|
173
|
+
console.log("Backup LaunchAgent installed.");
|
|
174
|
+
console.log(` Runs daily at 03:00`);
|
|
175
|
+
console.log(` Plist: ${plistPath}`);
|
|
176
|
+
console.log(` Log: ~/.ldm/logs/ldm-backup.log`);
|
|
177
|
+
} catch (err) {
|
|
178
|
+
console.error(`Setup failed: ${err.message}`);
|
|
179
|
+
process.exit(1);
|
|
180
|
+
}
|
|
181
|
+
} else {
|
|
182
|
+
const paths = ldmPaths();
|
|
183
|
+
const scriptPath = join(paths.bin, "ldm-backup.sh");
|
|
184
|
+
if (!existsSync(scriptPath)) {
|
|
185
|
+
console.error(`Backup script not found. Run "crystal init" first.`);
|
|
186
|
+
process.exit(1);
|
|
187
|
+
}
|
|
188
|
+
const keepFlag = flags.keep ? `--keep ${flags.keep}` : "";
|
|
189
|
+
const secretsFlag = "include-secrets" in flags ? "--include-secrets" : "";
|
|
190
|
+
try {
|
|
191
|
+
execSync(`bash ${scriptPath} ${keepFlag} ${secretsFlag}`.trim(), { stdio: "inherit" });
|
|
192
|
+
} catch (err) {
|
|
193
|
+
process.exit(1);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
if (command === "cleanup") {
|
|
199
|
+
const dryRun = "dry-run" in flags;
|
|
200
|
+
const config2 = resolveConfig();
|
|
201
|
+
const dbPath = join(config2.dataDir, "crystal.db");
|
|
202
|
+
if (!existsSync(dbPath)) {
|
|
203
|
+
console.error(`Database not found: ${dbPath}`);
|
|
204
|
+
process.exit(1);
|
|
205
|
+
}
|
|
206
|
+
const DatabaseCtor = (await import("better-sqlite3")).default;
|
|
207
|
+
const sqliteVecMod = await import("sqlite-vec");
|
|
208
|
+
const db = new DatabaseCtor(dbPath);
|
|
209
|
+
db.pragma("journal_mode = WAL");
|
|
210
|
+
sqliteVecMod.load(db);
|
|
211
|
+
const chunkCount = db.prepare("SELECT COUNT(*) as cnt FROM chunks").get().cnt;
|
|
212
|
+
console.log("Crystal Cleanup");
|
|
213
|
+
console.log(` Database: ${dbPath}`);
|
|
214
|
+
console.log(` Chunks: ${chunkCount.toLocaleString()}`);
|
|
215
|
+
const orphanedVec = db.prepare(
|
|
216
|
+
"SELECT COUNT(*) as cnt FROM chunks_vec WHERE chunk_id NOT IN (SELECT id FROM chunks)"
|
|
217
|
+
).get().cnt;
|
|
218
|
+
const orphanedFts = db.prepare(
|
|
219
|
+
"SELECT COUNT(*) as cnt FROM chunks_fts WHERE rowid NOT IN (SELECT id FROM chunks)"
|
|
220
|
+
).get().cnt;
|
|
221
|
+
console.log(`
|
|
222
|
+
Orphans found:`);
|
|
223
|
+
console.log(` Vec: ${orphanedVec.toLocaleString()} orphaned vector entries`);
|
|
224
|
+
console.log(` FTS: ${orphanedFts.toLocaleString()} orphaned full-text entries`);
|
|
225
|
+
if (orphanedVec === 0 && orphanedFts === 0) {
|
|
226
|
+
console.log(`
|
|
227
|
+
No orphans found. Database is clean.`);
|
|
228
|
+
db.close();
|
|
229
|
+
return;
|
|
230
|
+
}
|
|
231
|
+
if (dryRun) {
|
|
232
|
+
console.log(`
|
|
233
|
+
(dry run) Run "crystal cleanup" without --dry-run to remove them.`);
|
|
234
|
+
db.close();
|
|
235
|
+
return;
|
|
236
|
+
}
|
|
237
|
+
const backupPath = dbPath + `.pre-cleanup-${Date.now()}`;
|
|
238
|
+
console.log(`
|
|
239
|
+
Backing up database...`);
|
|
240
|
+
try {
|
|
241
|
+
db.pragma("wal_checkpoint(TRUNCATE)");
|
|
242
|
+
copyFileSync(dbPath, backupPath);
|
|
243
|
+
const bSize = statSync(backupPath).size;
|
|
244
|
+
console.log(` Backup: ${backupPath} (${(bSize / 1024 / 1024 / 1024).toFixed(2)} GB)`);
|
|
245
|
+
} catch (err) {
|
|
246
|
+
console.error(`Backup failed: ${err.message}`);
|
|
247
|
+
console.error("Aborting cleanup.");
|
|
248
|
+
db.close();
|
|
249
|
+
process.exit(1);
|
|
250
|
+
}
|
|
251
|
+
console.log(`
|
|
252
|
+
Pausing capture cron...`);
|
|
253
|
+
removeCron();
|
|
254
|
+
console.log(` Cron paused.`);
|
|
255
|
+
try {
|
|
256
|
+
if (orphanedVec > 0) {
|
|
257
|
+
console.log(`
|
|
258
|
+
Cleaning orphaned vectors...`);
|
|
259
|
+
const ids = db.prepare(
|
|
260
|
+
"SELECT chunk_id FROM chunks_vec WHERE chunk_id NOT IN (SELECT id FROM chunks)"
|
|
261
|
+
).all();
|
|
262
|
+
const delVec = db.prepare("DELETE FROM chunks_vec WHERE chunk_id = ?");
|
|
263
|
+
const BATCH = 1e3;
|
|
264
|
+
let cleaned = 0;
|
|
265
|
+
for (let i = 0; i < ids.length; i += BATCH) {
|
|
266
|
+
const batch = ids.slice(i, i + BATCH);
|
|
267
|
+
db.transaction(() => {
|
|
268
|
+
for (const r of batch) {
|
|
269
|
+
delVec.run(r.chunk_id);
|
|
270
|
+
cleaned++;
|
|
271
|
+
}
|
|
272
|
+
})();
|
|
273
|
+
if (cleaned % 1e4 === 0 || i + BATCH >= ids.length) {
|
|
274
|
+
process.stderr.write(` ${cleaned.toLocaleString()} / ${ids.length.toLocaleString()} vectors cleaned\r`);
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
console.log(` ${cleaned.toLocaleString()} orphaned vectors removed. `);
|
|
278
|
+
}
|
|
279
|
+
if (orphanedFts > 0) {
|
|
280
|
+
console.log(`
|
|
281
|
+
Rebuilding FTS index...`);
|
|
282
|
+
const ftsStart = Date.now();
|
|
283
|
+
db.exec("DELETE FROM chunks_fts");
|
|
284
|
+
db.exec("INSERT INTO chunks_fts(rowid, text) SELECT id, text FROM chunks");
|
|
285
|
+
const ftsElapsed = ((Date.now() - ftsStart) / 1e3).toFixed(1);
|
|
286
|
+
console.log(` FTS rebuilt from ${chunkCount.toLocaleString()} chunks in ${ftsElapsed}s.`);
|
|
287
|
+
}
|
|
288
|
+
console.log(`
|
|
289
|
+
Vacuuming database...`);
|
|
290
|
+
const preSize = statSync(dbPath).size;
|
|
291
|
+
db.exec("VACUUM");
|
|
292
|
+
const postSize = statSync(dbPath).size;
|
|
293
|
+
const savedMB = ((preSize - postSize) / 1024 / 1024).toFixed(0);
|
|
294
|
+
console.log(` Before: ${(preSize / 1024 / 1024 / 1024).toFixed(2)} GB`);
|
|
295
|
+
console.log(` After: ${(postSize / 1024 / 1024 / 1024).toFixed(2)} GB`);
|
|
296
|
+
console.log(` Saved: ${savedMB} MB`);
|
|
297
|
+
const postChunks = db.prepare("SELECT COUNT(*) as cnt FROM chunks").get().cnt;
|
|
298
|
+
const postFts = db.prepare("SELECT COUNT(*) as cnt FROM chunks_fts").get().cnt;
|
|
299
|
+
console.log(`
|
|
300
|
+
Verification:`);
|
|
301
|
+
console.log(` Chunks: ${postChunks.toLocaleString()}`);
|
|
302
|
+
console.log(` FTS entries: ${postFts.toLocaleString()}`);
|
|
303
|
+
console.log(` Match: ${postChunks === postFts ? "YES" : "NO (WARNING: mismatch!)"}`);
|
|
304
|
+
console.log(`
|
|
305
|
+
Cleanup complete. Verify search: crystal search "test query"`);
|
|
306
|
+
} finally {
|
|
307
|
+
console.log(`
|
|
308
|
+
Resuming capture cron...`);
|
|
309
|
+
installCron();
|
|
310
|
+
console.log(` Cron resumed.`);
|
|
311
|
+
db.close();
|
|
312
|
+
}
|
|
313
|
+
return;
|
|
314
|
+
}
|
|
315
|
+
if (command === "mlx") {
|
|
316
|
+
const subCmd = positional[0] || "status";
|
|
317
|
+
const { setupMlx, isServerRunning, stopServer, doctorCheck, MLX_CONFIG } = await import("./mlx-setup-XKU67WCT.js");
|
|
318
|
+
if (subCmd === "setup") {
|
|
319
|
+
const yes = "yes" in flags || "y" in flags;
|
|
320
|
+
const result = await setupMlx({ yes });
|
|
321
|
+
for (const step of result.steps) {
|
|
322
|
+
console.log(` ${result.ok ? "[OK]" : "[!!]"} ${step}`);
|
|
323
|
+
}
|
|
324
|
+
if (!result.ok) process.exit(1);
|
|
325
|
+
} else if (subCmd === "status") {
|
|
326
|
+
const check = doctorCheck();
|
|
327
|
+
const icon = check.status === "ok" ? "[OK]" : check.status === "warn" ? "[!!]" : "[XX]";
|
|
328
|
+
console.log(`MLX LLM: ${icon} ${check.detail}`);
|
|
329
|
+
if (check.fix) console.log(` Fix: ${check.fix}`);
|
|
330
|
+
console.log(` Port: ${MLX_CONFIG.port}`);
|
|
331
|
+
console.log(` Model: ${MLX_CONFIG.model}`);
|
|
332
|
+
console.log(` Log: ${MLX_CONFIG.logPath}`);
|
|
333
|
+
} else if (subCmd === "stop") {
|
|
334
|
+
if (stopServer()) {
|
|
335
|
+
console.log("MLX server stopped.");
|
|
336
|
+
} else {
|
|
337
|
+
console.log("MLX server was not running.");
|
|
338
|
+
}
|
|
339
|
+
} else {
|
|
340
|
+
console.error(`Unknown mlx subcommand: ${subCmd}. Use: setup, status, stop`);
|
|
341
|
+
process.exit(1);
|
|
342
|
+
}
|
|
343
|
+
return;
|
|
344
|
+
}
|
|
345
|
+
if (command === "bridge") {
|
|
346
|
+
const { isBridgeInstalled, isBridgeRegistered, registerBridgeMcp, registerBridgeDesktop, isBridgeDesktopRegistered } = await import("./bridge.js");
|
|
347
|
+
const subCmd = positional[0] || "status";
|
|
348
|
+
if (subCmd === "setup") {
|
|
349
|
+
if (!isBridgeInstalled()) {
|
|
350
|
+
console.log("Bridge (lesa-bridge) is not installed.");
|
|
351
|
+
console.log("Install it first: npm install -g lesa-bridge");
|
|
352
|
+
process.exit(1);
|
|
353
|
+
}
|
|
354
|
+
if (!isBridgeRegistered()) {
|
|
355
|
+
try {
|
|
356
|
+
registerBridgeMcp();
|
|
357
|
+
console.log("Bridge registered with Claude Code CLI.");
|
|
358
|
+
} catch (err) {
|
|
359
|
+
console.error(`Claude Code registration failed: ${err.message}`);
|
|
360
|
+
}
|
|
361
|
+
} else {
|
|
362
|
+
console.log("Bridge already registered with Claude Code CLI.");
|
|
363
|
+
}
|
|
364
|
+
if (!isBridgeDesktopRegistered()) {
|
|
365
|
+
const ok = registerBridgeDesktop();
|
|
366
|
+
if (ok) console.log("Bridge registered with Claude Desktop.");
|
|
367
|
+
}
|
|
368
|
+
console.log("Done. Restart Claude Code to activate.");
|
|
369
|
+
} else {
|
|
370
|
+
console.log("Bridge Status");
|
|
371
|
+
console.log(` Installed: ${isBridgeInstalled() ? "yes" : "no"}`);
|
|
372
|
+
console.log(` Claude Code: ${isBridgeRegistered() ? "registered" : "not registered"}`);
|
|
373
|
+
console.log(` Desktop: ${isBridgeDesktopRegistered() ? "registered" : "not registered"}`);
|
|
374
|
+
}
|
|
375
|
+
return;
|
|
376
|
+
}
|
|
377
|
+
if (command === "update") {
|
|
378
|
+
flags["update"] = "true";
|
|
379
|
+
await handleLdmCommand("init", flags, positional);
|
|
380
|
+
return;
|
|
381
|
+
}
|
|
382
|
+
if (command === "init" || command === "migrate-db") {
|
|
383
|
+
await handleLdmCommand(command, flags, positional);
|
|
384
|
+
return;
|
|
385
|
+
}
|
|
386
|
+
if (command === "serve") {
|
|
387
|
+
const { startServer } = await import("./crystal-serve.js");
|
|
388
|
+
const port = flags.port ? parseInt(flags.port, 10) : 18790;
|
|
389
|
+
startServer(port);
|
|
390
|
+
return;
|
|
391
|
+
}
|
|
392
|
+
if (command === "dream-weave") {
|
|
393
|
+
await handleDreamWeave(flags);
|
|
394
|
+
return;
|
|
395
|
+
}
|
|
396
|
+
if (command === "backfill") {
|
|
397
|
+
await handleBackfill(flags);
|
|
398
|
+
return;
|
|
399
|
+
}
|
|
400
|
+
if (command === "migrate-embeddings") {
|
|
401
|
+
await handleMigrateEmbeddings(flags);
|
|
402
|
+
return;
|
|
403
|
+
}
|
|
404
|
+
const overrides = {};
|
|
405
|
+
if (flags.provider) overrides.embeddingProvider = flags.provider;
|
|
406
|
+
const config = resolveConfig(overrides);
|
|
407
|
+
const crystal = new Crystal(config);
|
|
408
|
+
await crystal.init();
|
|
409
|
+
try {
|
|
410
|
+
switch (command) {
|
|
411
|
+
case "search": {
|
|
412
|
+
const query = positional.join(" ");
|
|
413
|
+
if (!query) {
|
|
414
|
+
console.error("Usage: crystal search <query>");
|
|
415
|
+
process.exit(1);
|
|
416
|
+
}
|
|
417
|
+
const limit = parseInt(flags.n || "5", 10);
|
|
418
|
+
const filter = {};
|
|
419
|
+
if (flags.agent) filter.agent_id = flags.agent;
|
|
420
|
+
if (flags.since) filter.since = flags.since;
|
|
421
|
+
if (flags.until) filter.until = flags.until;
|
|
422
|
+
const intent = flags.intent;
|
|
423
|
+
const candidateLimit = flags.candidates ? parseInt(flags.candidates, 10) : void 0;
|
|
424
|
+
const explainMode = "explain" in flags;
|
|
425
|
+
const results = await crystal.deepSearch(query, limit, filter, { intent, candidateLimit, explain: explainMode });
|
|
426
|
+
if (results.length === 0) {
|
|
427
|
+
console.log("No results found.");
|
|
428
|
+
} else {
|
|
429
|
+
const icon = { fresh: "\u{1F7E2}", recent: "\u{1F7E1}", aging: "\u{1F7E0}", stale: "\u{1F534}" };
|
|
430
|
+
console.log("(Recency-weighted. \u{1F7E2} fresh <3d, \u{1F7E1} recent <7d, \u{1F7E0} aging <14d, \u{1F534} stale 14d+)\n");
|
|
431
|
+
for (const [i, r] of results.entries()) {
|
|
432
|
+
const score = (r.score * 100).toFixed(1);
|
|
433
|
+
const date = r.created_at?.slice(0, 10) || "unknown";
|
|
434
|
+
const fresh = r.freshness ? `${icon[r.freshness]} ${r.freshness}, ` : "";
|
|
435
|
+
console.log(`[${i + 1}] (${fresh}${score}% match, ${r.agent_id}, ${date}, ${r.role})`);
|
|
436
|
+
console.log(r.text.slice(0, 300) + (r.text.length > 300 ? "..." : ""));
|
|
437
|
+
if (explainMode && r.explain) {
|
|
438
|
+
const e = r.explain;
|
|
439
|
+
console.log(` explain: fts=${e.fts_score?.toFixed(3) || "n/a"} vec=${e.vec_score?.toFixed(3) || "n/a"} rrf_rank=${e.rrf_rank} rerank=${e.rerank_score.toFixed(3)} recency=${e.recency_weight.toFixed(3)} final=${e.final_score.toFixed(4)}`);
|
|
440
|
+
}
|
|
441
|
+
console.log("---");
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
break;
|
|
445
|
+
}
|
|
446
|
+
case "remember": {
|
|
447
|
+
const text = positional.join(" ");
|
|
448
|
+
if (!text) {
|
|
449
|
+
console.error("Usage: crystal remember <text>");
|
|
450
|
+
process.exit(1);
|
|
451
|
+
}
|
|
452
|
+
const category = flags.category || "fact";
|
|
453
|
+
const id = await crystal.remember(text, category);
|
|
454
|
+
console.log(`Remembered (id: ${id}, category: ${category}): ${text}`);
|
|
455
|
+
break;
|
|
456
|
+
}
|
|
457
|
+
case "forget": {
|
|
458
|
+
const id = parseInt(positional[0], 10);
|
|
459
|
+
if (isNaN(id)) {
|
|
460
|
+
console.error("Usage: crystal forget <id>");
|
|
461
|
+
process.exit(1);
|
|
462
|
+
}
|
|
463
|
+
const ok = crystal.forget(id);
|
|
464
|
+
console.log(ok ? `Forgot memory ${id}` : `Memory ${id} not found or already deprecated`);
|
|
465
|
+
break;
|
|
466
|
+
}
|
|
467
|
+
case "status": {
|
|
468
|
+
const status = await crystal.status();
|
|
469
|
+
console.log(`Memory Crystal Status`);
|
|
470
|
+
console.log(` Data dir: ${status.dataDir}`);
|
|
471
|
+
console.log(` Provider: ${status.embeddingProvider}`);
|
|
472
|
+
console.log(` Chunks: ${status.chunks.toLocaleString()}`);
|
|
473
|
+
console.log(` Memories: ${status.memories}`);
|
|
474
|
+
console.log(` Sources: ${status.sources}`);
|
|
475
|
+
console.log(` Agents: ${status.agents.length > 0 ? status.agents.join(", ") : "none yet"}`);
|
|
476
|
+
break;
|
|
477
|
+
}
|
|
478
|
+
case "sources": {
|
|
479
|
+
const subCommand = positional[0];
|
|
480
|
+
if (!subCommand) {
|
|
481
|
+
console.error("Usage: crystal sources <add|sync|status|remove> ...");
|
|
482
|
+
process.exit(1);
|
|
483
|
+
}
|
|
484
|
+
switch (subCommand) {
|
|
485
|
+
case "add": {
|
|
486
|
+
const path = positional[1];
|
|
487
|
+
const name = flags.name;
|
|
488
|
+
if (!path || !name) {
|
|
489
|
+
console.error("Usage: crystal sources add <path> --name <name>");
|
|
490
|
+
process.exit(1);
|
|
491
|
+
}
|
|
492
|
+
const col = await crystal.sourcesAdd(path, name);
|
|
493
|
+
console.log(`Added collection "${col.name}" at ${col.root_path}`);
|
|
494
|
+
console.log(`Run "crystal sources sync ${name}" to index files.`);
|
|
495
|
+
break;
|
|
496
|
+
}
|
|
497
|
+
case "sync": {
|
|
498
|
+
const name = positional[1];
|
|
499
|
+
if (!name) {
|
|
500
|
+
console.error("Usage: crystal sources sync <name> [--dry-run]");
|
|
501
|
+
process.exit(1);
|
|
502
|
+
}
|
|
503
|
+
const dryRun = "dry-run" in flags;
|
|
504
|
+
if (dryRun) {
|
|
505
|
+
console.log(`Dry run for "${name}"...`);
|
|
506
|
+
} else {
|
|
507
|
+
console.log(`Syncing "${name}"...`);
|
|
508
|
+
}
|
|
509
|
+
const result = await crystal.sourcesSync(name, { dryRun });
|
|
510
|
+
console.log(` Added: ${result.added} files`);
|
|
511
|
+
console.log(` Updated: ${result.updated} files`);
|
|
512
|
+
console.log(` Removed: ${result.removed} files`);
|
|
513
|
+
console.log(` Chunks: ${result.chunks_added} embedded`);
|
|
514
|
+
console.log(` Time: ${(result.duration_ms / 1e3).toFixed(1)}s`);
|
|
515
|
+
break;
|
|
516
|
+
}
|
|
517
|
+
case "status": {
|
|
518
|
+
const status = crystal.sourcesStatus();
|
|
519
|
+
if (status.collections.length === 0) {
|
|
520
|
+
console.log('No source collections. Use "crystal sources add <path> --name <name>" to add one.');
|
|
521
|
+
} else {
|
|
522
|
+
console.log("Source Collections:");
|
|
523
|
+
for (const col of status.collections) {
|
|
524
|
+
const syncAgo = col.last_sync_at ? `${Math.round((Date.now() - new Date(col.last_sync_at).getTime()) / 6e4)}m ago` : "never";
|
|
525
|
+
console.log(` ${col.name}: ${col.file_count.toLocaleString()} files, ${col.chunk_count.toLocaleString()} chunks, last sync ${syncAgo}`);
|
|
526
|
+
}
|
|
527
|
+
console.log(` Total: ${status.total_files.toLocaleString()} files, ${status.total_chunks.toLocaleString()} chunks`);
|
|
528
|
+
}
|
|
529
|
+
break;
|
|
530
|
+
}
|
|
531
|
+
case "remove": {
|
|
532
|
+
const name = positional[1];
|
|
533
|
+
if (!name) {
|
|
534
|
+
console.error("Usage: crystal sources remove <name>");
|
|
535
|
+
process.exit(1);
|
|
536
|
+
}
|
|
537
|
+
const ok = crystal.sourcesRemove(name);
|
|
538
|
+
console.log(ok ? `Removed collection "${name}"` : `Collection "${name}" not found`);
|
|
539
|
+
break;
|
|
540
|
+
}
|
|
541
|
+
default:
|
|
542
|
+
console.error(`Unknown sources subcommand: ${subCommand}`);
|
|
543
|
+
process.exit(1);
|
|
544
|
+
}
|
|
545
|
+
break;
|
|
546
|
+
}
|
|
547
|
+
case "migrate-db": {
|
|
548
|
+
const paths = ensureLdm();
|
|
549
|
+
const HOME = process.env.HOME || "";
|
|
550
|
+
const legacyDir = join(HOME, ".openclaw", "memory-crystal");
|
|
551
|
+
const legacyDb = join(legacyDir, "crystal.db");
|
|
552
|
+
const destDb = paths.crystalDb;
|
|
553
|
+
if (!existsSync(legacyDb)) {
|
|
554
|
+
console.error(`Source not found: ${legacyDb}`);
|
|
555
|
+
process.exit(1);
|
|
556
|
+
}
|
|
557
|
+
if (existsSync(destDb)) {
|
|
558
|
+
try {
|
|
559
|
+
const stat = lstatSync(destDb);
|
|
560
|
+
if (!stat.isSymbolicLink()) {
|
|
561
|
+
console.error(`Destination already exists (not a symlink): ${destDb}`);
|
|
562
|
+
console.error("If this is from a previous migration, remove it first.");
|
|
563
|
+
process.exit(1);
|
|
564
|
+
}
|
|
565
|
+
} catch {
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
console.log(`Copying ${legacyDb} -> ${destDb}`);
|
|
569
|
+
copyFileSync(legacyDb, destDb);
|
|
570
|
+
const Database = (await import("better-sqlite3")).default;
|
|
571
|
+
const db = new Database(destDb, { readonly: true });
|
|
572
|
+
const row = db.prepare("SELECT COUNT(*) as count FROM chunks").get();
|
|
573
|
+
db.close();
|
|
574
|
+
console.log(`Verified: ${row.count.toLocaleString()} chunks in destination DB`);
|
|
575
|
+
if (existsSync(legacyDb)) {
|
|
576
|
+
try {
|
|
577
|
+
const stat = lstatSync(legacyDb);
|
|
578
|
+
if (!stat.isSymbolicLink()) {
|
|
579
|
+
unlinkSync(legacyDb);
|
|
580
|
+
symlinkSync(destDb, legacyDb);
|
|
581
|
+
console.log(`Symlinked ${legacyDb} -> ${destDb}`);
|
|
582
|
+
}
|
|
583
|
+
} catch (err) {
|
|
584
|
+
console.error(`Symlink failed (non-fatal): ${err.message}`);
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
const legacyLance = join(legacyDir, "lance");
|
|
588
|
+
if (existsSync(legacyLance)) {
|
|
589
|
+
try {
|
|
590
|
+
const stat = lstatSync(legacyLance);
|
|
591
|
+
if (!stat.isSymbolicLink()) {
|
|
592
|
+
console.log(`Note: lance/ at ${legacyLance} left in place (LanceDB will use new path on next write)`);
|
|
593
|
+
}
|
|
594
|
+
} catch {
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
console.log("Migration complete. Restart gateway to use new path.");
|
|
598
|
+
break;
|
|
599
|
+
}
|
|
600
|
+
default:
|
|
601
|
+
console.error(`Unknown command: ${command}`);
|
|
602
|
+
console.log(USAGE);
|
|
603
|
+
process.exit(1);
|
|
604
|
+
}
|
|
605
|
+
} finally {
|
|
606
|
+
crystal.close();
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
async function handleLdmCommand(command, flags, positional = []) {
|
|
610
|
+
if (command === "init") {
|
|
611
|
+
const { detectInstallState, runInstallOrUpdate, formatUpdateSummary } = await import("./installer.js");
|
|
612
|
+
const agentId = flags.agent || getAgentId();
|
|
613
|
+
const state = detectInstallState();
|
|
614
|
+
const isFresh = !state.ldmExists || state.installedVersion === null;
|
|
615
|
+
const isUpdate = !isFresh && state.needsUpdate;
|
|
616
|
+
if (!isFresh && !isUpdate && !("update" in flags) && !("dry-run" in flags)) {
|
|
617
|
+
console.log(`Memory Crystal v${state.repoVersion} is already installed and up to date.`);
|
|
618
|
+
console.log(`Run "crystal doctor" to check health.`);
|
|
619
|
+
return;
|
|
620
|
+
}
|
|
621
|
+
if ("dry-run" in flags) {
|
|
622
|
+
const label = isFresh ? "not installed" : isUpdate ? `v${state.installedVersion} -> v${state.repoVersion}` : `v${state.repoVersion} (up to date)`;
|
|
623
|
+
console.log(`Memory Crystal ${label}
|
|
624
|
+
`);
|
|
625
|
+
const { runDoctor } = await import("./doctor.js");
|
|
626
|
+
const checks = await runDoctor();
|
|
627
|
+
for (const c of checks) {
|
|
628
|
+
const icon = c.status === "ok" ? "[OK]" : c.status === "warn" ? "[!!]" : "[XX]";
|
|
629
|
+
console.log(` ${icon} ${c.name}: ${c.detail}`);
|
|
630
|
+
if (c.fix) console.log(` Fix: ${c.fix}`);
|
|
631
|
+
}
|
|
632
|
+
if (isFresh) console.log(`
|
|
633
|
+
Run "crystal init" to install.`);
|
|
634
|
+
else if (isUpdate) console.log(`
|
|
635
|
+
Run "crystal init" to update.`);
|
|
636
|
+
else console.log(`
|
|
637
|
+
No changes needed. Already at latest version.`);
|
|
638
|
+
return;
|
|
639
|
+
}
|
|
640
|
+
if (isUpdate && state.installedVersion) {
|
|
641
|
+
console.log(formatUpdateSummary(state.installedVersion, state.repoVersion));
|
|
642
|
+
console.log("");
|
|
643
|
+
} else if (isFresh) {
|
|
644
|
+
console.log(`Installing Memory Crystal v${state.repoVersion}...`);
|
|
645
|
+
console.log("");
|
|
646
|
+
}
|
|
647
|
+
let role;
|
|
648
|
+
if ("core" in flags) role = "core";
|
|
649
|
+
else if ("node" in flags) role = "node";
|
|
650
|
+
const result = await runInstallOrUpdate({
|
|
651
|
+
agentId,
|
|
652
|
+
role,
|
|
653
|
+
pairCode: flags.pair,
|
|
654
|
+
importDb: flags.import,
|
|
655
|
+
yes: "yes" in flags || "y" in flags,
|
|
656
|
+
skipDiscover: "skip-discover" in flags
|
|
657
|
+
});
|
|
658
|
+
if (result.action === "up-to-date") {
|
|
659
|
+
console.log(`Memory Crystal v${result.version} is already installed and up to date.`);
|
|
660
|
+
console.log(`Run "crystal doctor" to check health.`);
|
|
661
|
+
return;
|
|
662
|
+
}
|
|
663
|
+
console.log(`
|
|
664
|
+
${result.action === "installed" ? "Install" : "Update"} complete (v${result.version}):
|
|
665
|
+
`);
|
|
666
|
+
for (const step of result.steps) {
|
|
667
|
+
const isError = step.includes("failed") || step.includes("FAILED");
|
|
668
|
+
console.log(` ${isError ? "[!!]" : "[OK]"} ${step}`);
|
|
669
|
+
}
|
|
670
|
+
try {
|
|
671
|
+
const { isBridgeInstalled, isBridgeRegistered } = await import("./bridge.js");
|
|
672
|
+
if (isBridgeInstalled() && !isBridgeRegistered()) {
|
|
673
|
+
console.log(`
|
|
674
|
+
Bridge found but not registered. Run "crystal bridge setup" to connect.`);
|
|
675
|
+
} else if (!isBridgeInstalled()) {
|
|
676
|
+
console.log(`
|
|
677
|
+
Bridge not installed. Run "npm install -g lesa-bridge && crystal bridge setup" for AI-to-AI communication.`);
|
|
678
|
+
}
|
|
679
|
+
} catch {
|
|
680
|
+
}
|
|
681
|
+
if (isFresh && !("skip-discover" in flags)) {
|
|
682
|
+
try {
|
|
683
|
+
const { discoverAll, formatBytes } = await import("./discover.js");
|
|
684
|
+
const { bulkCopyToLdm } = await import("./bulk-copy.js");
|
|
685
|
+
const discovery = discoverAll();
|
|
686
|
+
if (discovery.totalFiles > 0) {
|
|
687
|
+
console.log(`
|
|
688
|
+
Discovered sessions:`);
|
|
689
|
+
for (const b of discovery.breakdown) {
|
|
690
|
+
console.log(` ${b.platform}: ${b.files} files (${formatBytes(b.sizeBytes)})`);
|
|
691
|
+
}
|
|
692
|
+
console.log(` Total: ${discovery.totalFiles} files (${formatBytes(discovery.totalSizeBytes)})`);
|
|
693
|
+
let shouldCopy = "yes" in flags || "y" in flags;
|
|
694
|
+
if (!shouldCopy && process.stdin.isTTY) {
|
|
695
|
+
shouldCopy = await askYesNo(`
|
|
696
|
+
Copy to LDM? [Y/n] `);
|
|
697
|
+
} else if (!shouldCopy) {
|
|
698
|
+
shouldCopy = true;
|
|
699
|
+
}
|
|
700
|
+
if (shouldCopy) {
|
|
701
|
+
for (const harness of discovery.harnesses) {
|
|
702
|
+
const { discoverSessionFiles } = await import("./discover.js");
|
|
703
|
+
const sessionPaths = discoverSessionFiles(harness);
|
|
704
|
+
const copyResult = bulkCopyToLdm(sessionPaths, agentId, {
|
|
705
|
+
workspace: harness.platform === "openclaw",
|
|
706
|
+
workspaceSrc: harness.workspaceDir
|
|
707
|
+
});
|
|
708
|
+
console.log(` ${harness.platform}: copied ${copyResult.filesCopied}, skipped ${copyResult.filesSkipped} (${formatBytes(copyResult.bytesWritten)} in ${copyResult.durationMs}ms)`);
|
|
709
|
+
}
|
|
710
|
+
}
|
|
711
|
+
} else {
|
|
712
|
+
console.log(`
|
|
713
|
+
No session files found on this machine.`);
|
|
714
|
+
}
|
|
715
|
+
} catch (err) {
|
|
716
|
+
console.error(`
|
|
717
|
+
Session discovery failed (non-fatal): ${err.message}`);
|
|
718
|
+
}
|
|
719
|
+
}
|
|
720
|
+
console.log(`
|
|
721
|
+
Next: Run "crystal doctor" to verify everything is working.`);
|
|
722
|
+
if (result.action === "installed") {
|
|
723
|
+
console.log(`Restart Claude Code to activate the new hooks and MCP server.`);
|
|
724
|
+
}
|
|
725
|
+
return;
|
|
726
|
+
}
|
|
727
|
+
if (command === "migrate-db") {
|
|
728
|
+
const paths = ensureLdm();
|
|
729
|
+
const HOME = process.env.HOME || "";
|
|
730
|
+
const legacyDir = join(HOME, ".openclaw", "memory-crystal");
|
|
731
|
+
const legacyDb = join(legacyDir, "crystal.db");
|
|
732
|
+
const destDb = paths.crystalDb;
|
|
733
|
+
if (!existsSync(legacyDb)) {
|
|
734
|
+
console.error(`Source not found: ${legacyDb}`);
|
|
735
|
+
process.exit(1);
|
|
736
|
+
}
|
|
737
|
+
if (existsSync(destDb)) {
|
|
738
|
+
try {
|
|
739
|
+
const stat = lstatSync(destDb);
|
|
740
|
+
if (!stat.isSymbolicLink()) {
|
|
741
|
+
console.error(`Destination already exists (not a symlink): ${destDb}`);
|
|
742
|
+
console.error("If this is from a previous migration, remove it first.");
|
|
743
|
+
process.exit(1);
|
|
744
|
+
}
|
|
745
|
+
} catch {
|
|
746
|
+
}
|
|
747
|
+
}
|
|
748
|
+
console.log(`Copying ${legacyDb} -> ${destDb}`);
|
|
749
|
+
copyFileSync(legacyDb, destDb);
|
|
750
|
+
const Database = (await import("better-sqlite3")).default;
|
|
751
|
+
const db = new Database(destDb, { readonly: true });
|
|
752
|
+
const row = db.prepare("SELECT COUNT(*) as count FROM chunks").get();
|
|
753
|
+
db.close();
|
|
754
|
+
console.log(`Verified: ${row.count.toLocaleString()} chunks in destination DB`);
|
|
755
|
+
try {
|
|
756
|
+
const stat = lstatSync(legacyDb);
|
|
757
|
+
if (!stat.isSymbolicLink()) {
|
|
758
|
+
unlinkSync(legacyDb);
|
|
759
|
+
symlinkSync(destDb, legacyDb);
|
|
760
|
+
console.log(`Symlinked ${legacyDb} -> ${destDb}`);
|
|
761
|
+
}
|
|
762
|
+
} catch (err) {
|
|
763
|
+
console.error(`Symlink failed (non-fatal): ${err.message}`);
|
|
764
|
+
}
|
|
765
|
+
console.log("Migration complete. Restart gateway to use new path.");
|
|
766
|
+
return;
|
|
767
|
+
}
|
|
768
|
+
}
|
|
769
|
+
async function handleDreamWeave(flags) {
|
|
770
|
+
const agentId = flags.agent || getAgentId();
|
|
771
|
+
const mode = flags.mode || "incremental";
|
|
772
|
+
const dryRun = "dry-run" in flags;
|
|
773
|
+
const since = flags.since;
|
|
774
|
+
const paths = ldmPaths(agentId);
|
|
775
|
+
if (!existsSync(paths.transcripts)) {
|
|
776
|
+
console.error(`No transcripts directory found at ${paths.transcripts}`);
|
|
777
|
+
console.error(`Run "crystal init --agent ${agentId}" first.`);
|
|
778
|
+
process.exit(1);
|
|
779
|
+
}
|
|
780
|
+
console.log(`Dream Weaver consolidation (${mode})`);
|
|
781
|
+
console.log(` Agent: ${agentId}`);
|
|
782
|
+
console.log(` Transcripts: ${paths.transcripts}`);
|
|
783
|
+
console.log(` Output: ${paths.agentRoot}`);
|
|
784
|
+
if (since) console.log(` Since: ${since}`);
|
|
785
|
+
if (dryRun) console.log(` Mode: dry run`);
|
|
786
|
+
const { runDreamWeaver } = await import("./dream-weaver.js");
|
|
787
|
+
try {
|
|
788
|
+
const result = await runDreamWeaver({
|
|
789
|
+
agentId,
|
|
790
|
+
mode,
|
|
791
|
+
transcriptsDir: paths.transcripts,
|
|
792
|
+
outputDir: paths.agentRoot,
|
|
793
|
+
sinceDatetime: since,
|
|
794
|
+
dryRun
|
|
795
|
+
});
|
|
796
|
+
console.log(`
|
|
797
|
+
Results:`);
|
|
798
|
+
console.log(` Sessions processed: ${result.sessionsProcessed}`);
|
|
799
|
+
console.log(` Journals written: ${result.journalsWritten.length}`);
|
|
800
|
+
if (result.journalsWritten.length > 0) {
|
|
801
|
+
for (const j of result.journalsWritten) {
|
|
802
|
+
console.log(` ${j}`);
|
|
803
|
+
}
|
|
804
|
+
}
|
|
805
|
+
console.log(` Identity created: ${result.identityCreated}`);
|
|
806
|
+
console.log(` Context updated: ${result.contextUpdated}`);
|
|
807
|
+
console.log(` Memories extracted: ${result.memoriesExtracted}`);
|
|
808
|
+
console.log(` Duration: ${(result.durationMs / 1e3).toFixed(1)}s`);
|
|
809
|
+
} catch (err) {
|
|
810
|
+
console.error(`Dream Weaver failed: ${err.message}`);
|
|
811
|
+
process.exit(1);
|
|
812
|
+
}
|
|
813
|
+
}
|
|
814
|
+
async function handleBackfill(flags) {
|
|
815
|
+
const agentId = flags.agent || getAgentId();
|
|
816
|
+
const dryRun = "dry-run" in flags;
|
|
817
|
+
const limit = flags.limit ? parseInt(flags.limit, 10) : 0;
|
|
818
|
+
const paths = ldmPaths(agentId);
|
|
819
|
+
if (!existsSync(paths.transcripts)) {
|
|
820
|
+
console.error(`No transcripts directory found at ${paths.transcripts}`);
|
|
821
|
+
console.error(`Run "crystal init --agent ${agentId}" first.`);
|
|
822
|
+
process.exit(1);
|
|
823
|
+
}
|
|
824
|
+
const jsonlFiles = [];
|
|
825
|
+
try {
|
|
826
|
+
for (const file of readdirSync(paths.transcripts)) {
|
|
827
|
+
if (file.endsWith(".jsonl") && !file.startsWith(".")) {
|
|
828
|
+
jsonlFiles.push(join(paths.transcripts, file));
|
|
829
|
+
}
|
|
830
|
+
}
|
|
831
|
+
} catch {
|
|
832
|
+
}
|
|
833
|
+
if (jsonlFiles.length === 0) {
|
|
834
|
+
console.log(`No JSONL files found in ${paths.transcripts}`);
|
|
835
|
+
console.log(`Run "crystal init" to discover and copy session files.`);
|
|
836
|
+
return;
|
|
837
|
+
}
|
|
838
|
+
const filesToProcess = limit > 0 ? jsonlFiles.slice(0, limit) : jsonlFiles;
|
|
839
|
+
const { isOpenClawJsonl, extractOpenClawMessages } = await import("./oc-backfill.js");
|
|
840
|
+
let totalTokens = 0;
|
|
841
|
+
let totalMessages = 0;
|
|
842
|
+
let ocFiles = 0;
|
|
843
|
+
let ccFiles = 0;
|
|
844
|
+
console.log(`Scanning ${filesToProcess.length} JSONL files in ${paths.transcripts}...`);
|
|
845
|
+
for (const filePath of filesToProcess) {
|
|
846
|
+
const isOC = isOpenClawJsonl(filePath);
|
|
847
|
+
if (isOC) {
|
|
848
|
+
ocFiles++;
|
|
849
|
+
const { messages } = extractOpenClawMessages(filePath, 0);
|
|
850
|
+
totalMessages += messages.length;
|
|
851
|
+
totalTokens += messages.reduce((sum, m) => sum + Math.ceil(m.text.length / 4), 0);
|
|
852
|
+
} else {
|
|
853
|
+
ccFiles++;
|
|
854
|
+
const fileSize = statSync(filePath).size;
|
|
855
|
+
const content = readFileSync(filePath, "utf-8");
|
|
856
|
+
const lines = content.split("\n").filter(Boolean);
|
|
857
|
+
for (const line of lines) {
|
|
858
|
+
try {
|
|
859
|
+
const obj = JSON.parse(line);
|
|
860
|
+
if (obj.type !== "user" && obj.type !== "assistant") continue;
|
|
861
|
+
const msg = obj.message;
|
|
862
|
+
if (!msg) continue;
|
|
863
|
+
let text = "";
|
|
864
|
+
if (typeof msg.content === "string") text = msg.content;
|
|
865
|
+
else if (Array.isArray(msg.content)) {
|
|
866
|
+
text = msg.content.map((b) => b.type === "text" ? b.text : b.type === "thinking" ? `[thinking] ${b.thinking}` : "").filter(Boolean).join("\n\n");
|
|
867
|
+
}
|
|
868
|
+
if (text.length >= 20) {
|
|
869
|
+
totalMessages++;
|
|
870
|
+
totalTokens += Math.ceil(text.length / 4);
|
|
871
|
+
}
|
|
872
|
+
} catch {
|
|
873
|
+
}
|
|
874
|
+
}
|
|
875
|
+
}
|
|
876
|
+
}
|
|
877
|
+
const estimatedCost = totalTokens / 1e6 * 0.02;
|
|
878
|
+
console.log(`
|
|
879
|
+
Backfill summary:`);
|
|
880
|
+
console.log(` Files: ${filesToProcess.length} (${ccFiles} Claude Code, ${ocFiles} OpenClaw)`);
|
|
881
|
+
console.log(` Messages: ${totalMessages.toLocaleString()}`);
|
|
882
|
+
console.log(` Est. tokens: ${totalTokens.toLocaleString()}`);
|
|
883
|
+
console.log(` Est. cost: $${estimatedCost.toFixed(2)} (text-embedding-3-small)`);
|
|
884
|
+
if (limit > 0) console.log(` Limit: ${limit} files`);
|
|
885
|
+
if (dryRun) {
|
|
886
|
+
console.log(`
|
|
887
|
+
(dry run, no embeddings created)`);
|
|
888
|
+
return;
|
|
889
|
+
}
|
|
890
|
+
let role = "core";
|
|
891
|
+
try {
|
|
892
|
+
const { detectRole } = await import("./role.js");
|
|
893
|
+
role = detectRole().role;
|
|
894
|
+
} catch {
|
|
895
|
+
}
|
|
896
|
+
if (role === "node") {
|
|
897
|
+
console.log(`
|
|
898
|
+
Node mode: would relay to Core for embedding.`);
|
|
899
|
+
console.log(`(Node backfill relay not yet implemented. Run on Core instead.)`);
|
|
900
|
+
return;
|
|
901
|
+
}
|
|
902
|
+
console.log(`
|
|
903
|
+
Embedding locally...`);
|
|
904
|
+
const config = resolveConfig();
|
|
905
|
+
const crystal = createCrystal(config);
|
|
906
|
+
await crystal.init();
|
|
907
|
+
let totalChunks = 0;
|
|
908
|
+
let filesProcessed = 0;
|
|
909
|
+
const BATCH_SIZE = 200;
|
|
910
|
+
for (const filePath of filesToProcess) {
|
|
911
|
+
const isOC = isOpenClawJsonl(filePath);
|
|
912
|
+
let messages;
|
|
913
|
+
if (isOC) {
|
|
914
|
+
messages = extractOpenClawMessages(filePath, 0).messages;
|
|
915
|
+
} else {
|
|
916
|
+
messages = extractCCMessages(filePath);
|
|
917
|
+
}
|
|
918
|
+
if (messages.length === 0) continue;
|
|
919
|
+
const maxSingleChunkChars = 2e3 * 4;
|
|
920
|
+
const chunks = [];
|
|
921
|
+
for (const msg of messages) {
|
|
922
|
+
if (msg.text.length <= maxSingleChunkChars) {
|
|
923
|
+
chunks.push({
|
|
924
|
+
text: msg.text,
|
|
925
|
+
role: msg.role,
|
|
926
|
+
source_type: "conversation",
|
|
927
|
+
source_id: isOC ? `oc:${msg.sessionId}` : `cc:${msg.sessionId}`,
|
|
928
|
+
agent_id: agentId,
|
|
929
|
+
token_count: Math.ceil(msg.text.length / 4),
|
|
930
|
+
created_at: msg.timestamp
|
|
931
|
+
});
|
|
932
|
+
} else {
|
|
933
|
+
for (const ct of crystal.chunkText(msg.text)) {
|
|
934
|
+
chunks.push({
|
|
935
|
+
text: ct,
|
|
936
|
+
role: msg.role,
|
|
937
|
+
source_type: "conversation",
|
|
938
|
+
source_id: isOC ? `oc:${msg.sessionId}` : `cc:${msg.sessionId}`,
|
|
939
|
+
agent_id: agentId,
|
|
940
|
+
token_count: Math.ceil(ct.length / 4),
|
|
941
|
+
created_at: msg.timestamp
|
|
942
|
+
});
|
|
943
|
+
}
|
|
944
|
+
}
|
|
945
|
+
}
|
|
946
|
+
let fileChunks = 0;
|
|
947
|
+
for (let i = 0; i < chunks.length; i += BATCH_SIZE) {
|
|
948
|
+
const batch = chunks.slice(i, i + BATCH_SIZE);
|
|
949
|
+
try {
|
|
950
|
+
fileChunks += await crystal.ingest(batch);
|
|
951
|
+
} catch (err) {
|
|
952
|
+
process.stderr.write(` Error on ${basename(filePath)}: ${err.message}
|
|
953
|
+
`);
|
|
954
|
+
break;
|
|
955
|
+
}
|
|
956
|
+
}
|
|
957
|
+
totalChunks += fileChunks;
|
|
958
|
+
filesProcessed++;
|
|
959
|
+
if (filesProcessed % 50 === 0) {
|
|
960
|
+
process.stderr.write(` [${filesProcessed}/${filesToProcess.length}] ${totalChunks} chunks embedded...
|
|
961
|
+
`);
|
|
962
|
+
}
|
|
963
|
+
}
|
|
964
|
+
if ("close" in crystal) crystal.close();
|
|
965
|
+
console.log(`
|
|
966
|
+
Backfill complete: ${totalChunks} chunks embedded from ${filesProcessed} files.`);
|
|
967
|
+
}
|
|
968
|
+
function extractCCMessages(filePath) {
|
|
969
|
+
const messages = [];
|
|
970
|
+
try {
|
|
971
|
+
const content = readFileSync(filePath, "utf-8");
|
|
972
|
+
for (const line of content.split("\n")) {
|
|
973
|
+
if (!line) continue;
|
|
974
|
+
try {
|
|
975
|
+
const obj = JSON.parse(line);
|
|
976
|
+
if (obj.type !== "user" && obj.type !== "assistant") continue;
|
|
977
|
+
const msg = obj.message;
|
|
978
|
+
if (!msg) continue;
|
|
979
|
+
let text = "";
|
|
980
|
+
if (typeof msg.content === "string") text = msg.content;
|
|
981
|
+
else if (Array.isArray(msg.content)) {
|
|
982
|
+
const parts = [];
|
|
983
|
+
for (const block of msg.content) {
|
|
984
|
+
if (block.type === "text" && block.text) parts.push(block.text);
|
|
985
|
+
if (block.type === "thinking" && block.thinking) parts.push(`[thinking] ${block.thinking}`);
|
|
986
|
+
}
|
|
987
|
+
text = parts.join("\n\n");
|
|
988
|
+
}
|
|
989
|
+
if (text.length < 20) continue;
|
|
990
|
+
messages.push({
|
|
991
|
+
role: msg.role || obj.type,
|
|
992
|
+
text,
|
|
993
|
+
timestamp: obj.timestamp || (/* @__PURE__ */ new Date()).toISOString(),
|
|
994
|
+
sessionId: obj.sessionId || "unknown"
|
|
995
|
+
});
|
|
996
|
+
} catch {
|
|
997
|
+
}
|
|
998
|
+
}
|
|
999
|
+
} catch {
|
|
1000
|
+
}
|
|
1001
|
+
return messages;
|
|
1002
|
+
}
|
|
1003
|
+
async function handleMigrateEmbeddings(flags) {
|
|
1004
|
+
const dryRun = "dry-run" in flags;
|
|
1005
|
+
const HOME = process.env.HOME || "";
|
|
1006
|
+
const cePath = join(HOME, ".openclaw", "memory", "context-embeddings.sqlite");
|
|
1007
|
+
if (!existsSync(cePath)) {
|
|
1008
|
+
console.error(`Context-embeddings database not found at ${cePath}`);
|
|
1009
|
+
process.exit(1);
|
|
1010
|
+
}
|
|
1011
|
+
const Database = (await import("better-sqlite3")).default;
|
|
1012
|
+
const ceDb = new Database(cePath, { readonly: true });
|
|
1013
|
+
const ceCount = ceDb.prepare("SELECT COUNT(*) as cnt FROM conversation_chunks").get().cnt;
|
|
1014
|
+
console.log(`Context-embeddings: ${ceCount.toLocaleString()} chunks`);
|
|
1015
|
+
const config = resolveConfig();
|
|
1016
|
+
const crystalDbPath = join(config.dataDir, "crystal.db");
|
|
1017
|
+
if (!existsSync(crystalDbPath)) {
|
|
1018
|
+
console.error(`Crystal database not found at ${crystalDbPath}`);
|
|
1019
|
+
ceDb.close();
|
|
1020
|
+
process.exit(1);
|
|
1021
|
+
}
|
|
1022
|
+
const crystalDb = new Database(crystalDbPath);
|
|
1023
|
+
try {
|
|
1024
|
+
const sqliteVec = await import("sqlite-vec");
|
|
1025
|
+
sqliteVec.load(crystalDb);
|
|
1026
|
+
} catch (err) {
|
|
1027
|
+
console.error(`Failed to load sqlite-vec: ${err.message}`);
|
|
1028
|
+
ceDb.close();
|
|
1029
|
+
crystalDb.close();
|
|
1030
|
+
process.exit(1);
|
|
1031
|
+
}
|
|
1032
|
+
const hasVec = crystalDb.prepare(
|
|
1033
|
+
"SELECT name FROM sqlite_master WHERE type='table' AND name='chunks_vec'"
|
|
1034
|
+
).get();
|
|
1035
|
+
if (!hasVec) {
|
|
1036
|
+
console.error("Crystal database missing chunks_vec table. Run crystal ingest first.");
|
|
1037
|
+
ceDb.close();
|
|
1038
|
+
crystalDb.close();
|
|
1039
|
+
process.exit(1);
|
|
1040
|
+
}
|
|
1041
|
+
const crystalCount = crystalDb.prepare("SELECT COUNT(*) as cnt FROM chunks").get().cnt;
|
|
1042
|
+
console.log(`Crystal: ${crystalCount.toLocaleString()} chunks`);
|
|
1043
|
+
const ceRows = ceDb.prepare(
|
|
1044
|
+
"SELECT id, agent_id, session_key, chunk_text, role, timestamp, embedding FROM conversation_chunks"
|
|
1045
|
+
).all();
|
|
1046
|
+
const { createHash } = await import("crypto");
|
|
1047
|
+
let migrated = 0;
|
|
1048
|
+
let skipped = 0;
|
|
1049
|
+
let failed = 0;
|
|
1050
|
+
const checkHash = crystalDb.prepare("SELECT id FROM chunks WHERE text_hash = ?");
|
|
1051
|
+
const insertChunk = crystalDb.prepare(
|
|
1052
|
+
`INSERT INTO chunks (text, text_hash, role, source_type, source_id, agent_id, token_count, created_at)
|
|
1053
|
+
VALUES (?, ?, ?, 'conversation', ?, ?, ?, ?)`
|
|
1054
|
+
);
|
|
1055
|
+
const insertVec = crystalDb.prepare(
|
|
1056
|
+
"INSERT INTO chunks_vec (chunk_id, embedding) VALUES (?, ?)"
|
|
1057
|
+
);
|
|
1058
|
+
if (dryRun) {
|
|
1059
|
+
for (const row of ceRows) {
|
|
1060
|
+
const hash = createHash("sha256").update(row.chunk_text).digest("hex");
|
|
1061
|
+
const existing = checkHash.get(hash);
|
|
1062
|
+
if (existing) {
|
|
1063
|
+
skipped++;
|
|
1064
|
+
} else {
|
|
1065
|
+
migrated++;
|
|
1066
|
+
}
|
|
1067
|
+
}
|
|
1068
|
+
console.log(`
|
|
1069
|
+
Dry run results:`);
|
|
1070
|
+
console.log(` Would migrate: ${migrated.toLocaleString()} unique chunks`);
|
|
1071
|
+
console.log(` Would skip: ${skipped.toLocaleString()} duplicates`);
|
|
1072
|
+
console.log(` Cost: $0.00 (embeddings copied directly, no API calls)`);
|
|
1073
|
+
ceDb.close();
|
|
1074
|
+
crystalDb.close();
|
|
1075
|
+
return;
|
|
1076
|
+
}
|
|
1077
|
+
console.log(`
|
|
1078
|
+
Backing up crystal.db...`);
|
|
1079
|
+
const backupPath = crystalDbPath + `.pre-migration-${Date.now()}`;
|
|
1080
|
+
try {
|
|
1081
|
+
crystalDb.backup(backupPath);
|
|
1082
|
+
console.log(` Backup: ${backupPath}`);
|
|
1083
|
+
} catch (err) {
|
|
1084
|
+
console.error(`Backup failed: ${err.message}`);
|
|
1085
|
+
console.error("Aborting migration. Fix backup and retry.");
|
|
1086
|
+
ceDb.close();
|
|
1087
|
+
crystalDb.close();
|
|
1088
|
+
process.exit(1);
|
|
1089
|
+
}
|
|
1090
|
+
console.log(`Migrating...`);
|
|
1091
|
+
const migrate = crystalDb.transaction(() => {
|
|
1092
|
+
for (const row of ceRows) {
|
|
1093
|
+
const hash = createHash("sha256").update(row.chunk_text).digest("hex");
|
|
1094
|
+
const existing = checkHash.get(hash);
|
|
1095
|
+
if (existing) {
|
|
1096
|
+
skipped++;
|
|
1097
|
+
continue;
|
|
1098
|
+
}
|
|
1099
|
+
if (!row.embedding || row.embedding.length !== 6144) {
|
|
1100
|
+
failed++;
|
|
1101
|
+
continue;
|
|
1102
|
+
}
|
|
1103
|
+
try {
|
|
1104
|
+
const agentId = row.agent_id === "main" ? "oc-lesa-mini" : row.agent_id;
|
|
1105
|
+
const sourceId = `ce:${row.session_key}`;
|
|
1106
|
+
const tokenCount = Math.ceil(row.chunk_text.length / 4);
|
|
1107
|
+
const createdAt = new Date(row.timestamp).toISOString();
|
|
1108
|
+
const result = insertChunk.run(
|
|
1109
|
+
row.chunk_text,
|
|
1110
|
+
hash,
|
|
1111
|
+
row.role,
|
|
1112
|
+
sourceId,
|
|
1113
|
+
agentId,
|
|
1114
|
+
tokenCount,
|
|
1115
|
+
createdAt
|
|
1116
|
+
);
|
|
1117
|
+
const chunkId = result.lastInsertRowid;
|
|
1118
|
+
insertVec.run(chunkId, row.embedding);
|
|
1119
|
+
migrated++;
|
|
1120
|
+
} catch (err) {
|
|
1121
|
+
failed++;
|
|
1122
|
+
}
|
|
1123
|
+
}
|
|
1124
|
+
});
|
|
1125
|
+
try {
|
|
1126
|
+
migrate();
|
|
1127
|
+
} catch (err) {
|
|
1128
|
+
console.error(`Migration failed: ${err.message}`);
|
|
1129
|
+
console.error(`Restore from backup: cp "${backupPath}" "${crystalDbPath}"`);
|
|
1130
|
+
ceDb.close();
|
|
1131
|
+
crystalDb.close();
|
|
1132
|
+
process.exit(1);
|
|
1133
|
+
}
|
|
1134
|
+
ceDb.close();
|
|
1135
|
+
crystalDb.close();
|
|
1136
|
+
console.log(`
|
|
1137
|
+
Migration complete:`);
|
|
1138
|
+
console.log(` Migrated: ${migrated.toLocaleString()} unique chunks`);
|
|
1139
|
+
console.log(` Skipped: ${skipped.toLocaleString()} duplicates`);
|
|
1140
|
+
console.log(` Failed: ${failed.toLocaleString()}`);
|
|
1141
|
+
console.log(` Cost: $0.00 (embeddings copied directly)`);
|
|
1142
|
+
console.log(` Backup: ${backupPath}`);
|
|
1143
|
+
console.log(`
|
|
1144
|
+
Next: Verify with "crystal doctor" and "crystal search <test query>"`);
|
|
1145
|
+
console.log(`Then: Remove context-embeddings from openclaw.json plugins to stop dual-write.`);
|
|
1146
|
+
}
|
|
1147
|
+
function askYesNo(prompt) {
|
|
1148
|
+
return new Promise((resolve) => {
|
|
1149
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
1150
|
+
rl.question(prompt, (answer) => {
|
|
1151
|
+
rl.close();
|
|
1152
|
+
const a = answer.trim().toLowerCase();
|
|
1153
|
+
resolve(a === "" || a === "y" || a === "yes");
|
|
1154
|
+
});
|
|
1155
|
+
});
|
|
1156
|
+
}
|
|
1157
|
+
main().catch((err) => {
|
|
1158
|
+
console.error(`Error: ${err.message}`);
|
|
1159
|
+
process.exit(1);
|
|
1160
|
+
});
|