@wipcomputer/memory-crystal 0.7.32 → 0.7.34-alpha.1
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
|
@@ -0,0 +1,772 @@
|
|
|
1
|
+
import {
|
|
2
|
+
deployBackupScript,
|
|
3
|
+
deployCaptureScript,
|
|
4
|
+
getAgentId,
|
|
5
|
+
installCron,
|
|
6
|
+
ldmPaths,
|
|
7
|
+
loadAgentConfig,
|
|
8
|
+
saveAgentConfig,
|
|
9
|
+
scaffoldLdm
|
|
10
|
+
} from "./chunk-DFQ72B7M.js";
|
|
11
|
+
|
|
12
|
+
// src/installer.ts
|
|
13
|
+
import { existsSync, readFileSync, writeFileSync, mkdirSync, cpSync, copyFileSync, readdirSync, statSync } from "fs";
|
|
14
|
+
import { join, dirname } from "path";
|
|
15
|
+
import { execSync } from "child_process";
|
|
16
|
+
import { fileURLToPath } from "url";
|
|
17
|
+
var HOME = process.env.HOME || "";
|
|
18
|
+
var LDM_ROOT = join(HOME, ".ldm");
|
|
19
|
+
var OC_ROOT = join(HOME, ".openclaw");
|
|
20
|
+
var CC_SETTINGS = join(HOME, ".claude", "settings.json");
|
|
21
|
+
var CC_MCP = join(HOME, ".claude", ".mcp.json");
|
|
22
|
+
var OC_MCP = join(OC_ROOT, ".mcp.json");
|
|
23
|
+
function readVersion(pkgPath) {
|
|
24
|
+
try {
|
|
25
|
+
if (existsSync(pkgPath)) {
|
|
26
|
+
const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
|
|
27
|
+
return pkg.version || null;
|
|
28
|
+
}
|
|
29
|
+
} catch {
|
|
30
|
+
}
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
function getRepoRoot() {
|
|
34
|
+
const thisDir = dirname(fileURLToPath(import.meta.url));
|
|
35
|
+
let dir = thisDir;
|
|
36
|
+
for (let i = 0; i < 5; i++) {
|
|
37
|
+
const pkgPath = join(dir, "package.json");
|
|
38
|
+
if (existsSync(pkgPath)) {
|
|
39
|
+
try {
|
|
40
|
+
const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
|
|
41
|
+
if (pkg.name === "@wipcomputer/memory-crystal") return dir;
|
|
42
|
+
} catch {
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
dir = dirname(dir);
|
|
46
|
+
}
|
|
47
|
+
return dirname(thisDir);
|
|
48
|
+
}
|
|
49
|
+
function semverCompare(a, b) {
|
|
50
|
+
const pa = a.split(".").map(Number);
|
|
51
|
+
const pb = b.split(".").map(Number);
|
|
52
|
+
for (let i = 0; i < 3; i++) {
|
|
53
|
+
if ((pa[i] || 0) > (pb[i] || 0)) return 1;
|
|
54
|
+
if ((pa[i] || 0) < (pb[i] || 0)) return -1;
|
|
55
|
+
}
|
|
56
|
+
return 0;
|
|
57
|
+
}
|
|
58
|
+
function getLatestNpmVersion() {
|
|
59
|
+
const names = ["@wipcomputer/memory-crystal", "memory-crystal"];
|
|
60
|
+
for (const name of names) {
|
|
61
|
+
try {
|
|
62
|
+
const v = execSync(`npm view ${name} version 2>/dev/null`, { encoding: "utf-8", timeout: 1e4 }).trim();
|
|
63
|
+
if (v) return v;
|
|
64
|
+
} catch {
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
return null;
|
|
68
|
+
}
|
|
69
|
+
function detectInstallState() {
|
|
70
|
+
const ldmExtDir = join(LDM_ROOT, "extensions", "memory-crystal");
|
|
71
|
+
const ocExtDir = join(OC_ROOT, "extensions", "memory-crystal");
|
|
72
|
+
const paths = ldmPaths();
|
|
73
|
+
const installedVersion = readVersion(join(ldmExtDir, "package.json"));
|
|
74
|
+
const repoRoot = getRepoRoot();
|
|
75
|
+
let repoVersion = readVersion(join(repoRoot, "package.json")) || "0.0.0";
|
|
76
|
+
const npmVersion = getLatestNpmVersion();
|
|
77
|
+
if (npmVersion && semverCompare(npmVersion, repoVersion) > 0) repoVersion = npmVersion;
|
|
78
|
+
const ccHookDeployed = existsSync(join(ldmExtDir, "dist", "cc-hook.js"));
|
|
79
|
+
let ccHookConfigured = false;
|
|
80
|
+
try {
|
|
81
|
+
if (existsSync(CC_SETTINGS)) {
|
|
82
|
+
const settings = JSON.parse(readFileSync(CC_SETTINGS, "utf-8"));
|
|
83
|
+
const stopHooks = settings?.hooks?.Stop;
|
|
84
|
+
if (Array.isArray(stopHooks)) {
|
|
85
|
+
ccHookConfigured = stopHooks.some((entry) => {
|
|
86
|
+
const hooks = entry?.hooks;
|
|
87
|
+
if (!Array.isArray(hooks)) return false;
|
|
88
|
+
return hooks.some((h) => h?.command?.includes("memory-crystal") && h?.command?.includes("cc-hook"));
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
} catch {
|
|
93
|
+
}
|
|
94
|
+
let mcpRegistered = false;
|
|
95
|
+
for (const mcpPath of [CC_MCP, OC_MCP, join(process.cwd(), ".mcp.json")]) {
|
|
96
|
+
try {
|
|
97
|
+
if (existsSync(mcpPath)) {
|
|
98
|
+
const config = JSON.parse(readFileSync(mcpPath, "utf-8"));
|
|
99
|
+
if (config?.mcpServers?.["memory-crystal"]) {
|
|
100
|
+
mcpRegistered = true;
|
|
101
|
+
break;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
} catch {
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
if (!mcpRegistered) {
|
|
108
|
+
try {
|
|
109
|
+
execSync("claude mcp get memory-crystal 2>/dev/null", { encoding: "utf-8", timeout: 5e3, stdio: "pipe" });
|
|
110
|
+
mcpRegistered = true;
|
|
111
|
+
} catch {
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
const ocDetected = existsSync(join(OC_ROOT, "openclaw.json"));
|
|
115
|
+
const ocPluginDeployed = existsSync(join(ocExtDir, "dist", "openclaw.js"));
|
|
116
|
+
let cronInstalled = false;
|
|
117
|
+
try {
|
|
118
|
+
const crontab = execSync("crontab -l 2>/dev/null", { encoding: "utf-8" });
|
|
119
|
+
cronInstalled = crontab.includes("crystal-capture");
|
|
120
|
+
} catch {
|
|
121
|
+
}
|
|
122
|
+
const role = "core";
|
|
123
|
+
const relayKeyExists = existsSync(join(LDM_ROOT, "secrets", "crystal-relay-key"));
|
|
124
|
+
return {
|
|
125
|
+
ldmExists: existsSync(LDM_ROOT),
|
|
126
|
+
crystalDbExists: existsSync(paths.crystalDb),
|
|
127
|
+
ccHookDeployed,
|
|
128
|
+
ccHookConfigured,
|
|
129
|
+
mcpRegistered,
|
|
130
|
+
ocDetected,
|
|
131
|
+
ocPluginDeployed,
|
|
132
|
+
cronInstalled,
|
|
133
|
+
installedVersion,
|
|
134
|
+
repoVersion,
|
|
135
|
+
needsUpdate: installedVersion !== null && installedVersion !== repoVersion,
|
|
136
|
+
role,
|
|
137
|
+
relayKeyExists
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
function deployToLdm() {
|
|
141
|
+
const repoRoot = getRepoRoot();
|
|
142
|
+
const sourceDir = join(repoRoot, "dist");
|
|
143
|
+
const extDir = join(LDM_ROOT, "extensions", "memory-crystal");
|
|
144
|
+
const destDist = join(extDir, "dist");
|
|
145
|
+
if (!existsSync(sourceDir)) {
|
|
146
|
+
throw new Error(`dist/ not found at ${sourceDir}. Run "npm run build" first.`);
|
|
147
|
+
}
|
|
148
|
+
mkdirSync(destDist, { recursive: true });
|
|
149
|
+
const distFiles = readdirSync(sourceDir);
|
|
150
|
+
for (const file of distFiles) {
|
|
151
|
+
const srcPath = join(sourceDir, file);
|
|
152
|
+
const destPath = join(destDist, file);
|
|
153
|
+
const stat = statSync(srcPath);
|
|
154
|
+
if (stat.isFile()) {
|
|
155
|
+
copyFileSync(srcPath, destPath);
|
|
156
|
+
} else if (stat.isDirectory()) {
|
|
157
|
+
cpSync(srcPath, destPath, { recursive: true });
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
copyFileSync(join(repoRoot, "package.json"), join(extDir, "package.json"));
|
|
161
|
+
const pluginJson = join(repoRoot, "openclaw.plugin.json");
|
|
162
|
+
if (existsSync(pluginJson)) {
|
|
163
|
+
copyFileSync(pluginJson, join(extDir, "openclaw.plugin.json"));
|
|
164
|
+
}
|
|
165
|
+
const skillsDir = join(repoRoot, "skills");
|
|
166
|
+
if (existsSync(skillsDir)) {
|
|
167
|
+
cpSync(skillsDir, join(extDir, "skills"), { recursive: true });
|
|
168
|
+
}
|
|
169
|
+
const version = readVersion(join(extDir, "package.json")) || "unknown";
|
|
170
|
+
return { extensionDir: extDir, version };
|
|
171
|
+
}
|
|
172
|
+
function installLdmDeps() {
|
|
173
|
+
const extDir = join(LDM_ROOT, "extensions", "memory-crystal");
|
|
174
|
+
if (!existsSync(join(extDir, "package.json"))) {
|
|
175
|
+
throw new Error("package.json not found in LDM extension dir. Deploy first.");
|
|
176
|
+
}
|
|
177
|
+
execSync("npm install --omit=dev", {
|
|
178
|
+
cwd: extDir,
|
|
179
|
+
encoding: "utf-8",
|
|
180
|
+
stdio: "pipe",
|
|
181
|
+
timeout: 12e4
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
function deployToOpenClaw() {
|
|
185
|
+
const repoRoot = getRepoRoot();
|
|
186
|
+
const sourceDir = join(repoRoot, "dist");
|
|
187
|
+
const extDir = join(OC_ROOT, "extensions", "memory-crystal");
|
|
188
|
+
const destDist = join(extDir, "dist");
|
|
189
|
+
if (!existsSync(sourceDir)) {
|
|
190
|
+
throw new Error(`dist/ not found at ${sourceDir}. Run "npm run build" first.`);
|
|
191
|
+
}
|
|
192
|
+
mkdirSync(destDist, { recursive: true });
|
|
193
|
+
const distFiles = readdirSync(sourceDir);
|
|
194
|
+
for (const file of distFiles) {
|
|
195
|
+
const srcPath = join(sourceDir, file);
|
|
196
|
+
const destPath = join(destDist, file);
|
|
197
|
+
const stat = statSync(srcPath);
|
|
198
|
+
if (stat.isFile()) {
|
|
199
|
+
copyFileSync(srcPath, destPath);
|
|
200
|
+
} else if (stat.isDirectory()) {
|
|
201
|
+
cpSync(srcPath, destPath, { recursive: true });
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
copyFileSync(join(repoRoot, "package.json"), join(extDir, "package.json"));
|
|
205
|
+
const pluginJson = join(repoRoot, "openclaw.plugin.json");
|
|
206
|
+
if (existsSync(pluginJson)) {
|
|
207
|
+
copyFileSync(pluginJson, join(extDir, "openclaw.plugin.json"));
|
|
208
|
+
}
|
|
209
|
+
const skillsDir = join(repoRoot, "skills");
|
|
210
|
+
if (existsSync(skillsDir)) {
|
|
211
|
+
cpSync(skillsDir, join(extDir, "skills"), { recursive: true });
|
|
212
|
+
}
|
|
213
|
+
const version = readVersion(join(extDir, "package.json")) || "unknown";
|
|
214
|
+
return { extensionDir: extDir, version };
|
|
215
|
+
}
|
|
216
|
+
function installOcDeps() {
|
|
217
|
+
const extDir = join(OC_ROOT, "extensions", "memory-crystal");
|
|
218
|
+
if (!existsSync(join(extDir, "package.json"))) {
|
|
219
|
+
throw new Error("package.json not found in OC extension dir. Deploy first.");
|
|
220
|
+
}
|
|
221
|
+
execSync("npm install --omit=dev", {
|
|
222
|
+
cwd: extDir,
|
|
223
|
+
encoding: "utf-8",
|
|
224
|
+
stdio: "pipe",
|
|
225
|
+
timeout: 12e4
|
|
226
|
+
});
|
|
227
|
+
}
|
|
228
|
+
function configureCCHook() {
|
|
229
|
+
const hookCommand = `node ${join(LDM_ROOT, "extensions", "memory-crystal", "dist", "cc-hook.js")}`;
|
|
230
|
+
let settings = {};
|
|
231
|
+
if (existsSync(CC_SETTINGS)) {
|
|
232
|
+
try {
|
|
233
|
+
settings = JSON.parse(readFileSync(CC_SETTINGS, "utf-8"));
|
|
234
|
+
} catch {
|
|
235
|
+
throw new Error(`~/.claude/settings.json exists but is not valid JSON. Fix it manually before proceeding.`);
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
if (!settings.hooks) settings.hooks = {};
|
|
239
|
+
if (!Array.isArray(settings.hooks.Stop)) settings.hooks.Stop = [];
|
|
240
|
+
const existingIdx = settings.hooks.Stop.findIndex((entry) => {
|
|
241
|
+
const hooks = entry?.hooks;
|
|
242
|
+
if (!Array.isArray(hooks)) return false;
|
|
243
|
+
return hooks.some((h) => h?.command?.includes("memory-crystal") || h?.command?.includes("cc-hook"));
|
|
244
|
+
});
|
|
245
|
+
const hookEntry = {
|
|
246
|
+
hooks: [{
|
|
247
|
+
type: "command",
|
|
248
|
+
command: hookCommand,
|
|
249
|
+
timeout: 30
|
|
250
|
+
}]
|
|
251
|
+
};
|
|
252
|
+
if (existingIdx >= 0) {
|
|
253
|
+
settings.hooks.Stop[existingIdx] = hookEntry;
|
|
254
|
+
} else {
|
|
255
|
+
settings.hooks.Stop.push(hookEntry);
|
|
256
|
+
}
|
|
257
|
+
mkdirSync(join(HOME, ".claude"), { recursive: true });
|
|
258
|
+
writeFileSync(CC_SETTINGS, JSON.stringify(settings, null, 2) + "\n");
|
|
259
|
+
}
|
|
260
|
+
function registerMCPServer() {
|
|
261
|
+
const mcpServerPath = join(LDM_ROOT, "extensions", "memory-crystal", "dist", "mcp-server.js");
|
|
262
|
+
const addCmd = `claude mcp add --scope user -e OPENCLAW_HOME=${OC_ROOT} memory-crystal -- node "${mcpServerPath}"`;
|
|
263
|
+
try {
|
|
264
|
+
execSync(addCmd, {
|
|
265
|
+
encoding: "utf-8",
|
|
266
|
+
stdio: "pipe",
|
|
267
|
+
timeout: 15e3
|
|
268
|
+
});
|
|
269
|
+
return;
|
|
270
|
+
} catch (err) {
|
|
271
|
+
const output = (err.stderr || "") + (err.stdout || "");
|
|
272
|
+
if (output.includes("already exists")) {
|
|
273
|
+
try {
|
|
274
|
+
execSync("claude mcp remove memory-crystal --scope user", { encoding: "utf-8", stdio: "pipe", timeout: 1e4 });
|
|
275
|
+
execSync(addCmd, { encoding: "utf-8", stdio: "pipe", timeout: 15e3 });
|
|
276
|
+
} catch {
|
|
277
|
+
}
|
|
278
|
+
return;
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
let config = {};
|
|
282
|
+
if (existsSync(CC_MCP)) {
|
|
283
|
+
try {
|
|
284
|
+
config = JSON.parse(readFileSync(CC_MCP, "utf-8"));
|
|
285
|
+
} catch {
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
if (!config.mcpServers) config.mcpServers = {};
|
|
289
|
+
config.mcpServers["memory-crystal"] = {
|
|
290
|
+
command: "node",
|
|
291
|
+
args: [mcpServerPath],
|
|
292
|
+
env: { OPENCLAW_HOME: OC_ROOT }
|
|
293
|
+
};
|
|
294
|
+
mkdirSync(join(HOME, ".claude"), { recursive: true });
|
|
295
|
+
writeFileSync(CC_MCP, JSON.stringify(config, null, 2) + "\n");
|
|
296
|
+
}
|
|
297
|
+
function registerOcMCPServer() {
|
|
298
|
+
const mcpServerPath = join(OC_ROOT, "extensions", "memory-crystal", "dist", "mcp-server.js");
|
|
299
|
+
let config = {};
|
|
300
|
+
if (existsSync(OC_MCP)) {
|
|
301
|
+
try {
|
|
302
|
+
config = JSON.parse(readFileSync(OC_MCP, "utf-8"));
|
|
303
|
+
} catch {
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
if (!config.mcpServers) config.mcpServers = {};
|
|
307
|
+
config.mcpServers["memory-crystal"] = {
|
|
308
|
+
command: "node",
|
|
309
|
+
args: [mcpServerPath],
|
|
310
|
+
env: { OPENCLAW_HOME: OC_ROOT }
|
|
311
|
+
};
|
|
312
|
+
writeFileSync(OC_MCP, JSON.stringify(config, null, 2) + "\n");
|
|
313
|
+
}
|
|
314
|
+
function backupCrystalDb() {
|
|
315
|
+
const paths = ldmPaths();
|
|
316
|
+
const dbPath = paths.crystalDb;
|
|
317
|
+
if (!existsSync(dbPath)) {
|
|
318
|
+
throw new Error(`crystal.db not found at ${dbPath}`);
|
|
319
|
+
}
|
|
320
|
+
const timestamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-").slice(0, 19);
|
|
321
|
+
const backupPath = `${dbPath}.pre-update-${timestamp}`;
|
|
322
|
+
copyFileSync(dbPath, backupPath);
|
|
323
|
+
const walPath = dbPath + "-wal";
|
|
324
|
+
const shmPath = dbPath + "-shm";
|
|
325
|
+
if (existsSync(walPath)) copyFileSync(walPath, backupPath + "-wal");
|
|
326
|
+
if (existsSync(shmPath)) copyFileSync(shmPath, backupPath + "-shm");
|
|
327
|
+
const origSize = statSync(dbPath).size;
|
|
328
|
+
const backupSize = statSync(backupPath).size;
|
|
329
|
+
if (backupSize !== origSize) {
|
|
330
|
+
throw new Error(`Backup size mismatch: original ${origSize}, backup ${backupSize}`);
|
|
331
|
+
}
|
|
332
|
+
return backupPath;
|
|
333
|
+
}
|
|
334
|
+
async function verifyCrystalDbReadable() {
|
|
335
|
+
const paths = ldmPaths();
|
|
336
|
+
const dbPath = paths.crystalDb;
|
|
337
|
+
if (!existsSync(dbPath)) return;
|
|
338
|
+
const { default: Database } = await import("better-sqlite3");
|
|
339
|
+
const db = new Database(dbPath, { readonly: true });
|
|
340
|
+
try {
|
|
341
|
+
const row = db.prepare("SELECT COUNT(*) as count FROM chunks").get();
|
|
342
|
+
if (typeof row.count !== "number") {
|
|
343
|
+
throw new Error("chunks table returned unexpected data");
|
|
344
|
+
}
|
|
345
|
+
const tables = db.prepare(
|
|
346
|
+
"SELECT name FROM sqlite_master WHERE type='table'"
|
|
347
|
+
).all();
|
|
348
|
+
const tableNames = tables.map((t) => t.name);
|
|
349
|
+
if (!tableNames.includes("chunks")) {
|
|
350
|
+
throw new Error("chunks table missing from database");
|
|
351
|
+
}
|
|
352
|
+
} finally {
|
|
353
|
+
db.close();
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
function formatUpdateSummary(oldVersion, newVersion) {
|
|
357
|
+
const lines = [];
|
|
358
|
+
lines.push(`Updating v${oldVersion} -> v${newVersion}`);
|
|
359
|
+
lines.push("");
|
|
360
|
+
lines.push("What will be updated:");
|
|
361
|
+
lines.push(" - Code in ~/.ldm/extensions/memory-crystal/dist/");
|
|
362
|
+
lines.push(" - Skills in ~/.ldm/extensions/memory-crystal/skills/");
|
|
363
|
+
lines.push(" - package.json (version tracking)");
|
|
364
|
+
lines.push("");
|
|
365
|
+
lines.push("What will NOT be touched:");
|
|
366
|
+
lines.push(" - ~/.ldm/memory/crystal.db (your data)");
|
|
367
|
+
lines.push(" - ~/.ldm/state/* (watermarks, role)");
|
|
368
|
+
lines.push(" - ~/.ldm/secrets/* (relay key)");
|
|
369
|
+
lines.push(" - ~/.ldm/agents/* (agent data)");
|
|
370
|
+
return lines.join("\n");
|
|
371
|
+
}
|
|
372
|
+
function ldmCliAvailable() {
|
|
373
|
+
try {
|
|
374
|
+
execSync("ldm --version", { stdio: "pipe", timeout: 5e3 });
|
|
375
|
+
return true;
|
|
376
|
+
} catch {
|
|
377
|
+
return false;
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
function bootstrapLdmOs(steps) {
|
|
381
|
+
try {
|
|
382
|
+
steps.push("Installing LDM OS infrastructure...");
|
|
383
|
+
execSync("npm install -g @wipcomputer/wip-ldm-os", { stdio: "pipe", timeout: 12e4 });
|
|
384
|
+
execSync("ldm --version", { stdio: "pipe", timeout: 5e3 });
|
|
385
|
+
steps.push("LDM OS installed.");
|
|
386
|
+
return true;
|
|
387
|
+
} catch {
|
|
388
|
+
steps.push("LDM OS install skipped (npm offline or permissions issue). Using core mode.");
|
|
389
|
+
return false;
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
function runLdmInstall(repoDir) {
|
|
393
|
+
const steps = [];
|
|
394
|
+
try {
|
|
395
|
+
execSync("ldm init --yes --none", { stdio: "pipe", timeout: 3e4 });
|
|
396
|
+
steps.push("LDM initialized via ldm CLI");
|
|
397
|
+
} catch (err) {
|
|
398
|
+
const msg = (err.stderr || err.message || "").toString().trim();
|
|
399
|
+
if (!msg.includes("already")) {
|
|
400
|
+
steps.push(`ldm init warning: ${msg.slice(0, 120)}`);
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
try {
|
|
404
|
+
execSync(`ldm install "${repoDir}"`, { stdio: "pipe", timeout: 6e4 });
|
|
405
|
+
steps.push("Generic deployment handled by ldm install (extensions, MCP, hooks)");
|
|
406
|
+
return { ok: true, steps };
|
|
407
|
+
} catch (err) {
|
|
408
|
+
const msg = (err.stderr || err.message || "").toString().trim();
|
|
409
|
+
steps.push(`ldm install failed: ${msg.slice(0, 200)}`);
|
|
410
|
+
return { ok: false, steps };
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
async function runInstallOrUpdate(options) {
|
|
414
|
+
const agentId = options.agentId || getAgentId();
|
|
415
|
+
const state = detectInstallState();
|
|
416
|
+
const steps = [];
|
|
417
|
+
const deployedTo = [];
|
|
418
|
+
let dbStatus = "none";
|
|
419
|
+
let chunkCount = 0;
|
|
420
|
+
const isFresh = !state.ldmExists || state.installedVersion === null;
|
|
421
|
+
const isUpdate = !isFresh && state.needsUpdate;
|
|
422
|
+
if (!isFresh && !isUpdate) {
|
|
423
|
+
return {
|
|
424
|
+
action: "up-to-date",
|
|
425
|
+
version: state.repoVersion,
|
|
426
|
+
deployedTo: [],
|
|
427
|
+
steps: [`Already at v${state.repoVersion}. Nothing to do.`]
|
|
428
|
+
};
|
|
429
|
+
}
|
|
430
|
+
if (isUpdate && state.installedVersion) {
|
|
431
|
+
const npmV = getLatestNpmVersion();
|
|
432
|
+
if (npmV && semverCompare(npmV, state.installedVersion) > 0) {
|
|
433
|
+
steps.push(`Upgrading v${state.installedVersion} -> v${npmV} via npm...`);
|
|
434
|
+
try {
|
|
435
|
+
execSync("npm install -g @wipcomputer/memory-crystal 2>&1", { encoding: "utf-8", timeout: 6e4, stdio: "pipe" });
|
|
436
|
+
steps.push(`Installed @wipcomputer/memory-crystal@${npmV}`);
|
|
437
|
+
steps.push("Continuing with updated code...");
|
|
438
|
+
} catch (err) {
|
|
439
|
+
steps.push(`npm upgrade failed: ${err.message}. Continuing with local code.`);
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
let hasLdmCli = ldmCliAvailable();
|
|
444
|
+
if (!hasLdmCli) {
|
|
445
|
+
hasLdmCli = bootstrapLdmOs(steps);
|
|
446
|
+
}
|
|
447
|
+
let ldmDelegated = false;
|
|
448
|
+
if (hasLdmCli) {
|
|
449
|
+
steps.push("LDM OS detected. Using ldm install for deployment...");
|
|
450
|
+
const repoRoot = getRepoRoot();
|
|
451
|
+
const delegateResult = runLdmInstall(repoRoot);
|
|
452
|
+
steps.push(...delegateResult.steps);
|
|
453
|
+
if (delegateResult.ok) {
|
|
454
|
+
ldmDelegated = true;
|
|
455
|
+
const ldmExtDir = join(LDM_ROOT, "extensions", "memory-crystal");
|
|
456
|
+
if (existsSync(ldmExtDir)) deployedTo.push(ldmExtDir);
|
|
457
|
+
const ocExtDir = join(OC_ROOT, "extensions", "memory-crystal");
|
|
458
|
+
if (existsSync(ocExtDir)) deployedTo.push(ocExtDir);
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
if (ldmDelegated) {
|
|
462
|
+
steps.push("Scaffold + agent config handled by ldm CLI");
|
|
463
|
+
} else {
|
|
464
|
+
scaffoldLdm(agentId);
|
|
465
|
+
steps.push(`LDM scaffolded for agent "${agentId}"`);
|
|
466
|
+
const existingCfg = loadAgentConfig(agentId);
|
|
467
|
+
if (existingCfg && !existingCfg.agentId) {
|
|
468
|
+
existingCfg.agentId = agentId;
|
|
469
|
+
saveAgentConfig(agentId, existingCfg);
|
|
470
|
+
steps.push(`Added agentId "${agentId}" to existing config.json`);
|
|
471
|
+
} else if (!existingCfg) {
|
|
472
|
+
const harness = agentId.startsWith("oc-") ? "openclaw" : "claude-code-cli";
|
|
473
|
+
saveAgentConfig(agentId, {
|
|
474
|
+
agentId,
|
|
475
|
+
agent: agentId.startsWith("oc-") ? agentId.replace(/^oc-/, "").replace(/-[^-]+$/, "") : "cc",
|
|
476
|
+
harness,
|
|
477
|
+
created: (/* @__PURE__ */ new Date()).toISOString().slice(0, 10)
|
|
478
|
+
});
|
|
479
|
+
steps.push(`Created config.json for agent "${agentId}"`);
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
if (state.crystalDbExists) {
|
|
483
|
+
try {
|
|
484
|
+
const { default: Database } = await import("better-sqlite3");
|
|
485
|
+
const db = new Database(ldmPaths().crystalDb, { readonly: true });
|
|
486
|
+
const row = db.prepare("SELECT COUNT(*) as count FROM chunks").get();
|
|
487
|
+
chunkCount = row.count;
|
|
488
|
+
db.close();
|
|
489
|
+
dbStatus = "existing";
|
|
490
|
+
steps.push(`Existing database found: ${chunkCount.toLocaleString()} chunks in crystal.db`);
|
|
491
|
+
} catch {
|
|
492
|
+
dbStatus = "existing";
|
|
493
|
+
steps.push("Existing database found (could not read chunk count)");
|
|
494
|
+
}
|
|
495
|
+
try {
|
|
496
|
+
const backupPath = backupCrystalDb();
|
|
497
|
+
steps.push(`Database backed up to ${backupPath}`);
|
|
498
|
+
} catch (err) {
|
|
499
|
+
steps.push(`Database backup FAILED: ${err.message}`);
|
|
500
|
+
return {
|
|
501
|
+
action: "up-to-date",
|
|
502
|
+
version: state.repoVersion,
|
|
503
|
+
deployedTo: [],
|
|
504
|
+
steps: [...steps, "Aborted. Fix the backup issue before retrying."],
|
|
505
|
+
dbStatus
|
|
506
|
+
};
|
|
507
|
+
}
|
|
508
|
+
try {
|
|
509
|
+
await verifyCrystalDbReadable();
|
|
510
|
+
steps.push("Database read verification passed");
|
|
511
|
+
} catch (err) {
|
|
512
|
+
steps.push(`Database read verification FAILED: ${err.message}`);
|
|
513
|
+
return {
|
|
514
|
+
action: "up-to-date",
|
|
515
|
+
version: state.repoVersion,
|
|
516
|
+
deployedTo: [],
|
|
517
|
+
steps: [...steps, "Aborted. New code cannot read existing database."],
|
|
518
|
+
dbStatus
|
|
519
|
+
};
|
|
520
|
+
}
|
|
521
|
+
} else if (options.importDb) {
|
|
522
|
+
const importPath = options.importDb;
|
|
523
|
+
if (!existsSync(importPath)) {
|
|
524
|
+
steps.push(`Import path not found: ${importPath}`);
|
|
525
|
+
} else {
|
|
526
|
+
try {
|
|
527
|
+
const paths = ldmPaths();
|
|
528
|
+
mkdirSync(join(paths.root, "memory"), { recursive: true });
|
|
529
|
+
copyFileSync(importPath, paths.crystalDb);
|
|
530
|
+
const { default: Database } = await import("better-sqlite3");
|
|
531
|
+
const db = new Database(paths.crystalDb, { readonly: true });
|
|
532
|
+
const row = db.prepare("SELECT COUNT(*) as count FROM chunks").get();
|
|
533
|
+
chunkCount = row.count;
|
|
534
|
+
db.close();
|
|
535
|
+
dbStatus = "imported";
|
|
536
|
+
steps.push(`Database imported: ${chunkCount.toLocaleString()} chunks from ${importPath}`);
|
|
537
|
+
} catch (err) {
|
|
538
|
+
steps.push(`Database import failed: ${err.message}`);
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
} else {
|
|
542
|
+
dbStatus = "fresh";
|
|
543
|
+
steps.push("No existing database. A new one will be created on first capture.");
|
|
544
|
+
}
|
|
545
|
+
if (ldmDelegated) {
|
|
546
|
+
const repoRoot = getRepoRoot();
|
|
547
|
+
const ldmExtDir = join(LDM_ROOT, "extensions", "memory-crystal");
|
|
548
|
+
if (existsSync(ldmExtDir)) copyFileSync(join(repoRoot, "package.json"), join(ldmExtDir, "package.json"));
|
|
549
|
+
const ocExtDir = join(OC_ROOT, "extensions", "memory-crystal");
|
|
550
|
+
if (existsSync(ocExtDir)) copyFileSync(join(repoRoot, "package.json"), join(ocExtDir, "package.json"));
|
|
551
|
+
steps.push(`Version synced to v${readVersion(join(repoRoot, "package.json")) || "unknown"}`);
|
|
552
|
+
}
|
|
553
|
+
if (!ldmDelegated) {
|
|
554
|
+
const ldmResult = deployToLdm();
|
|
555
|
+
steps.push(`Code deployed to ${ldmResult.extensionDir}`);
|
|
556
|
+
deployedTo.push(ldmResult.extensionDir);
|
|
557
|
+
try {
|
|
558
|
+
installLdmDeps();
|
|
559
|
+
steps.push("Dependencies installed (LDM)");
|
|
560
|
+
} catch (err) {
|
|
561
|
+
steps.push(`Dependencies install failed (LDM): ${err.message}`);
|
|
562
|
+
}
|
|
563
|
+
try {
|
|
564
|
+
configureCCHook();
|
|
565
|
+
steps.push("CC Stop hook configured in ~/.claude/settings.json");
|
|
566
|
+
} catch (err) {
|
|
567
|
+
steps.push(`CC Stop hook config failed: ${err.message}`);
|
|
568
|
+
}
|
|
569
|
+
if (!state.mcpRegistered || isUpdate) {
|
|
570
|
+
try {
|
|
571
|
+
registerMCPServer();
|
|
572
|
+
steps.push("MCP server registered with Claude Code");
|
|
573
|
+
} catch (err) {
|
|
574
|
+
steps.push(`MCP registration failed: ${err.message}`);
|
|
575
|
+
}
|
|
576
|
+
} else {
|
|
577
|
+
steps.push("MCP server already registered");
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
try {
|
|
581
|
+
deployCaptureScript();
|
|
582
|
+
steps.push("Capture script deployed");
|
|
583
|
+
} catch (err) {
|
|
584
|
+
steps.push(`Capture script failed: ${err.message}`);
|
|
585
|
+
}
|
|
586
|
+
if (!state.cronInstalled || isFresh) {
|
|
587
|
+
try {
|
|
588
|
+
installCron();
|
|
589
|
+
steps.push("Cron job installed (every minute)");
|
|
590
|
+
} catch (err) {
|
|
591
|
+
steps.push(`Cron install failed: ${err.message}`);
|
|
592
|
+
}
|
|
593
|
+
} else {
|
|
594
|
+
steps.push("Cron job already installed");
|
|
595
|
+
}
|
|
596
|
+
try {
|
|
597
|
+
deployBackupScript();
|
|
598
|
+
steps.push("Backup script deployed");
|
|
599
|
+
} catch (err) {
|
|
600
|
+
steps.push(`Backup script failed: ${err.message}`);
|
|
601
|
+
}
|
|
602
|
+
try {
|
|
603
|
+
const { canRunMlx, isMlxLmInstalled, isServerRunning } = await import("./mlx-setup-XKU67WCT.js");
|
|
604
|
+
if (canRunMlx()) {
|
|
605
|
+
if (isServerRunning()) {
|
|
606
|
+
steps.push("MLX LLM: already running");
|
|
607
|
+
} else if (isMlxLmInstalled()) {
|
|
608
|
+
steps.push("MLX LLM: installed but not running. Start with: launchctl kickstart -kp gui/$(id -u)/ai.ldm.mlx-server");
|
|
609
|
+
} else {
|
|
610
|
+
steps.push('MLX LLM: Apple Silicon detected. Run "crystal mlx setup" to install local LLM for free, fast, offline search quality.');
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
} catch {
|
|
614
|
+
}
|
|
615
|
+
if (!ldmDelegated && state.ocDetected) {
|
|
616
|
+
try {
|
|
617
|
+
const ocResult = deployToOpenClaw();
|
|
618
|
+
steps.push(`OC plugin deployed to ${ocResult.extensionDir}`);
|
|
619
|
+
deployedTo.push(ocResult.extensionDir);
|
|
620
|
+
} catch (err) {
|
|
621
|
+
steps.push(`OC plugin deploy failed: ${err.message}`);
|
|
622
|
+
}
|
|
623
|
+
try {
|
|
624
|
+
installOcDeps();
|
|
625
|
+
steps.push("Dependencies installed (OC)");
|
|
626
|
+
} catch (err) {
|
|
627
|
+
steps.push(`Dependencies install failed (OC): ${err.message}`);
|
|
628
|
+
}
|
|
629
|
+
try {
|
|
630
|
+
registerOcMCPServer();
|
|
631
|
+
steps.push("OC MCP server config updated");
|
|
632
|
+
} catch (err) {
|
|
633
|
+
steps.push(`OC MCP config failed: ${err.message}`);
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
if (!options.role && process.stdin.isTTY) {
|
|
637
|
+
const { createInterface } = await import("readline");
|
|
638
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
639
|
+
const answer = await new Promise((resolve) => {
|
|
640
|
+
rl.question("\n Is this your primary machine (always on), or adding a device?\n [1] Primary (Crystal Core)\n [2] Adding a device (Crystal Node)\n > ", resolve);
|
|
641
|
+
});
|
|
642
|
+
rl.close();
|
|
643
|
+
if (answer.trim() === "2") {
|
|
644
|
+
options.role = "node";
|
|
645
|
+
const rl2 = createInterface({ input: process.stdin, output: process.stdout });
|
|
646
|
+
options.pairCode = await new Promise((resolve) => {
|
|
647
|
+
rl2.question(' Pairing code from Core (run "crystal pair" on Core): ', resolve);
|
|
648
|
+
});
|
|
649
|
+
rl2.close();
|
|
650
|
+
} else {
|
|
651
|
+
options.role = "core";
|
|
652
|
+
}
|
|
653
|
+
}
|
|
654
|
+
if (options.role === "core") {
|
|
655
|
+
try {
|
|
656
|
+
const { promoteToCore } = await import("./role.js");
|
|
657
|
+
promoteToCore();
|
|
658
|
+
steps.push("Role set to Core");
|
|
659
|
+
} catch (err) {
|
|
660
|
+
steps.push(`Role setup failed: ${err.message}`);
|
|
661
|
+
}
|
|
662
|
+
} else if (options.role === "node") {
|
|
663
|
+
try {
|
|
664
|
+
const { demoteToNode } = await import("./role.js");
|
|
665
|
+
demoteToNode();
|
|
666
|
+
steps.push("Role set to Node");
|
|
667
|
+
} catch (err) {
|
|
668
|
+
steps.push(`Role setup failed: ${err.message}`);
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
if (options.pairCode) {
|
|
672
|
+
try {
|
|
673
|
+
const { pairReceive } = await import("./pair.js");
|
|
674
|
+
pairReceive(options.pairCode);
|
|
675
|
+
steps.push("Pairing code accepted");
|
|
676
|
+
} catch (err) {
|
|
677
|
+
steps.push(`Pairing failed: ${err.message}`);
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
if (options.role === "node" || process.env.CRYSTAL_RELAY_URL) {
|
|
681
|
+
const secretsDir = join(LDM_ROOT, "secrets");
|
|
682
|
+
const envPath = join(secretsDir, "crystal-relay.env");
|
|
683
|
+
if (!existsSync(envPath)) {
|
|
684
|
+
const relayUrl = "https://memory-crystal-relay.wipcomputer.workers.dev";
|
|
685
|
+
let token = "";
|
|
686
|
+
try {
|
|
687
|
+
const saTokenPath = join(OC_ROOT, "secrets", "op-sa-token");
|
|
688
|
+
if (existsSync(saTokenPath)) {
|
|
689
|
+
const saToken = readFileSync(saTokenPath, "utf8").trim();
|
|
690
|
+
token = execSync(
|
|
691
|
+
`OP_SERVICE_ACCOUNT_TOKEN=${saToken} op item get "Memory Crystal Relay Auth Tokens" --vault "Agent Secrets" --fields label=${agentId}-token --reveal 2>/dev/null`,
|
|
692
|
+
{ encoding: "utf8", timeout: 15e3 }
|
|
693
|
+
).trim();
|
|
694
|
+
}
|
|
695
|
+
} catch {
|
|
696
|
+
}
|
|
697
|
+
if (!token && process.stdin.isTTY) {
|
|
698
|
+
const { createInterface } = await import("readline");
|
|
699
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
700
|
+
token = await new Promise((resolve) => {
|
|
701
|
+
rl.question(" Relay auth token: ", resolve);
|
|
702
|
+
});
|
|
703
|
+
rl.close();
|
|
704
|
+
token = token.trim();
|
|
705
|
+
}
|
|
706
|
+
if (token) {
|
|
707
|
+
mkdirSync(secretsDir, { recursive: true });
|
|
708
|
+
writeFileSync(envPath, `export CRYSTAL_RELAY_URL=${relayUrl}
|
|
709
|
+
export CRYSTAL_RELAY_TOKEN=${token}
|
|
710
|
+
export CRYSTAL_AGENT_ID=${agentId}
|
|
711
|
+
`);
|
|
712
|
+
process.env.CRYSTAL_RELAY_URL = relayUrl;
|
|
713
|
+
process.env.CRYSTAL_RELAY_TOKEN = token;
|
|
714
|
+
steps.push("Relay config written to ~/.ldm/secrets/crystal-relay.env");
|
|
715
|
+
const shellProfile = join(HOME, ".zshrc");
|
|
716
|
+
const sourceLine = `source ${envPath}`;
|
|
717
|
+
let alreadySourced = false;
|
|
718
|
+
try {
|
|
719
|
+
alreadySourced = readFileSync(shellProfile, "utf8").includes(sourceLine);
|
|
720
|
+
} catch {
|
|
721
|
+
}
|
|
722
|
+
if (!alreadySourced && process.stdin.isTTY) {
|
|
723
|
+
const { createInterface: createRL } = await import("readline");
|
|
724
|
+
const rl2 = createRL({ input: process.stdin, output: process.stdout });
|
|
725
|
+
const ans = await new Promise((resolve) => {
|
|
726
|
+
rl2.question(" Add relay config to ~/.zshrc? [Y/n] ", resolve);
|
|
727
|
+
});
|
|
728
|
+
rl2.close();
|
|
729
|
+
if (ans.trim().toLowerCase() !== "n") {
|
|
730
|
+
const { appendFileSync } = await import("fs");
|
|
731
|
+
appendFileSync(shellProfile, `
|
|
732
|
+
# Memory Crystal relay
|
|
733
|
+
${sourceLine}
|
|
734
|
+
`);
|
|
735
|
+
steps.push("Added relay source to ~/.zshrc");
|
|
736
|
+
}
|
|
737
|
+
}
|
|
738
|
+
} else {
|
|
739
|
+
steps.push("No relay token. Set CRYSTAL_RELAY_TOKEN manually.");
|
|
740
|
+
}
|
|
741
|
+
} else {
|
|
742
|
+
steps.push("Relay config exists at ~/.ldm/secrets/crystal-relay.env");
|
|
743
|
+
}
|
|
744
|
+
}
|
|
745
|
+
if (hasLdmCli) {
|
|
746
|
+
steps.push('Tip: Run "ldm install" to see more components you can add.');
|
|
747
|
+
} else if (!ldmDelegated) {
|
|
748
|
+
steps.push("Tip: Install LDM OS for more components: npm install -g @wipcomputer/wip-ldm-os");
|
|
749
|
+
}
|
|
750
|
+
return {
|
|
751
|
+
action: isFresh ? "installed" : "updated",
|
|
752
|
+
version: state.repoVersion,
|
|
753
|
+
deployedTo,
|
|
754
|
+
steps,
|
|
755
|
+
dbStatus,
|
|
756
|
+
chunkCount
|
|
757
|
+
};
|
|
758
|
+
}
|
|
759
|
+
export {
|
|
760
|
+
backupCrystalDb,
|
|
761
|
+
configureCCHook,
|
|
762
|
+
deployToLdm,
|
|
763
|
+
deployToOpenClaw,
|
|
764
|
+
detectInstallState,
|
|
765
|
+
formatUpdateSummary,
|
|
766
|
+
installLdmDeps,
|
|
767
|
+
installOcDeps,
|
|
768
|
+
registerMCPServer,
|
|
769
|
+
registerOcMCPServer,
|
|
770
|
+
runInstallOrUpdate,
|
|
771
|
+
verifyCrystalDbReadable
|
|
772
|
+
};
|