@wipcomputer/memory-crystal 0.7.34-alpha.1 → 0.7.34-alpha.3
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/LICENSE +1 -1
- package/dist/bridge.js +64 -7
- package/dist/bulk-copy.js +67 -16
- package/dist/cc-hook.js +2291 -47
- package/dist/cc-poller.js +1967 -70
- package/dist/cli.js +4538 -139
- package/dist/core.js +1789 -6
- package/dist/crypto.js +153 -14
- package/dist/crystal-serve.js +64 -12
- package/dist/doctor.js +517 -52
- package/dist/dream-weaver.js +1755 -7
- package/dist/file-sync.js +407 -9
- package/dist/installer.js +840 -145
- package/dist/ldm.js +231 -16
- package/dist/mcp-server.js +1882 -17
- package/dist/migrate.js +1707 -11
- package/dist/mirror-sync.js +2052 -34
- package/dist/openclaw.js +1970 -69
- package/dist/pair.js +112 -16
- package/dist/poller.js +2275 -80
- package/dist/role.js +159 -7
- package/dist/staging.js +235 -10
- package/dist/summarize.js +142 -5
- package/package.json +3 -3
package/dist/installer.js
CHANGED
|
@@ -1,29 +1,724 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
} from "./chunk-DFQ72B7M.js";
|
|
1
|
+
var __defProp = Object.defineProperty;
|
|
2
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
3
|
+
var __esm = (fn, res) => function __init() {
|
|
4
|
+
return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
|
|
5
|
+
};
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
11
10
|
|
|
12
|
-
// src/
|
|
13
|
-
import { existsSync, readFileSync, writeFileSync,
|
|
11
|
+
// src/ldm.ts
|
|
12
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync, copyFileSync, chmodSync, readdirSync } from "fs";
|
|
14
13
|
import { join, dirname } from "path";
|
|
15
14
|
import { execSync } from "child_process";
|
|
16
15
|
import { fileURLToPath } from "url";
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
16
|
+
function loadAgentConfig(id) {
|
|
17
|
+
const cfgPath = join(LDM_ROOT, "agents", id, "config.json");
|
|
18
|
+
try {
|
|
19
|
+
if (existsSync(cfgPath)) return JSON.parse(readFileSync(cfgPath, "utf-8"));
|
|
20
|
+
} catch {
|
|
21
|
+
}
|
|
22
|
+
return null;
|
|
23
|
+
}
|
|
24
|
+
function saveAgentConfig(id, config) {
|
|
25
|
+
const dir = join(LDM_ROOT, "agents", id);
|
|
26
|
+
mkdirSync(dir, { recursive: true });
|
|
27
|
+
writeFileSync(join(dir, "config.json"), JSON.stringify(config, null, 2) + "\n");
|
|
28
|
+
}
|
|
29
|
+
function getAgentId(harnessHint) {
|
|
30
|
+
if (process.env.CRYSTAL_AGENT_ID) return process.env.CRYSTAL_AGENT_ID;
|
|
31
|
+
const agentsDir = join(LDM_ROOT, "agents");
|
|
32
|
+
if (existsSync(agentsDir)) {
|
|
33
|
+
try {
|
|
34
|
+
for (const d of readdirSync(agentsDir)) {
|
|
35
|
+
const cfg = loadAgentConfig(d);
|
|
36
|
+
if (!cfg || !cfg.agentId) continue;
|
|
37
|
+
if (!harnessHint) return cfg.agentId;
|
|
38
|
+
if (harnessHint === "claude-code" && cfg.harness === "claude-code-cli") return cfg.agentId;
|
|
39
|
+
if (harnessHint === "openclaw" && cfg.harness === "openclaw") return cfg.agentId;
|
|
40
|
+
}
|
|
41
|
+
} catch {
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
return harnessHint === "openclaw" ? "oc-lesa-mini" : "cc-mini";
|
|
45
|
+
}
|
|
46
|
+
function ldmPaths(agentId) {
|
|
47
|
+
const id = agentId || getAgentId();
|
|
48
|
+
const agentRoot = join(LDM_ROOT, "agents", id);
|
|
49
|
+
return {
|
|
50
|
+
root: LDM_ROOT,
|
|
51
|
+
bin: join(LDM_ROOT, "bin"),
|
|
52
|
+
secrets: join(LDM_ROOT, "secrets"),
|
|
53
|
+
state: join(LDM_ROOT, "state"),
|
|
54
|
+
config: join(LDM_ROOT, "config.json"),
|
|
55
|
+
crystalDb: join(LDM_ROOT, "memory", "crystal.db"),
|
|
56
|
+
crystalLance: join(LDM_ROOT, "memory", "lance"),
|
|
57
|
+
agentRoot,
|
|
58
|
+
transcripts: join(agentRoot, "memory", "transcripts"),
|
|
59
|
+
sessions: join(agentRoot, "memory", "sessions"),
|
|
60
|
+
daily: join(agentRoot, "memory", "daily"),
|
|
61
|
+
journals: join(agentRoot, "memory", "journals"),
|
|
62
|
+
workspace: join(agentRoot, "memory", "workspace")
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
function loadConfig() {
|
|
66
|
+
const configPath = join(LDM_ROOT, "config.json");
|
|
67
|
+
try {
|
|
68
|
+
if (existsSync(configPath)) {
|
|
69
|
+
return JSON.parse(readFileSync(configPath, "utf-8"));
|
|
70
|
+
}
|
|
71
|
+
} catch {
|
|
72
|
+
}
|
|
73
|
+
return null;
|
|
74
|
+
}
|
|
75
|
+
function saveConfig(config) {
|
|
76
|
+
const configPath = join(LDM_ROOT, "config.json");
|
|
77
|
+
writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n");
|
|
78
|
+
}
|
|
79
|
+
function scaffoldLdm(agentId) {
|
|
80
|
+
const paths = ldmPaths(agentId);
|
|
81
|
+
mkdirSync(join(paths.root, "memory"), { recursive: true });
|
|
82
|
+
mkdirSync(paths.crystalLance, { recursive: true });
|
|
83
|
+
mkdirSync(paths.bin, { recursive: true });
|
|
84
|
+
mkdirSync(paths.secrets, { recursive: true, mode: 448 });
|
|
85
|
+
mkdirSync(paths.state, { recursive: true });
|
|
86
|
+
mkdirSync(paths.transcripts, { recursive: true });
|
|
87
|
+
mkdirSync(paths.sessions, { recursive: true });
|
|
88
|
+
mkdirSync(paths.daily, { recursive: true });
|
|
89
|
+
mkdirSync(paths.journals, { recursive: true });
|
|
90
|
+
mkdirSync(paths.workspace, { recursive: true });
|
|
91
|
+
const id = agentId || getAgentId();
|
|
92
|
+
let config = loadConfig();
|
|
93
|
+
if (!config) {
|
|
94
|
+
config = {
|
|
95
|
+
version: "1.0.0",
|
|
96
|
+
agents: [id],
|
|
97
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
98
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
99
|
+
};
|
|
100
|
+
} else {
|
|
101
|
+
if (!config.agents.includes(id)) {
|
|
102
|
+
config.agents.push(id);
|
|
103
|
+
}
|
|
104
|
+
config.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
105
|
+
}
|
|
106
|
+
saveConfig(config);
|
|
107
|
+
return paths;
|
|
108
|
+
}
|
|
109
|
+
function deployCaptureScript() {
|
|
110
|
+
const paths = ldmPaths();
|
|
111
|
+
mkdirSync(paths.bin, { recursive: true });
|
|
112
|
+
const thisDir = dirname(fileURLToPath(import.meta.url));
|
|
113
|
+
let scriptSrc = join(thisDir, "crystal-capture.sh");
|
|
114
|
+
if (!existsSync(scriptSrc)) {
|
|
115
|
+
scriptSrc = join(thisDir, "..", "scripts", "crystal-capture.sh");
|
|
116
|
+
}
|
|
117
|
+
const scriptDest = join(paths.bin, "crystal-capture.sh");
|
|
118
|
+
if (!existsSync(scriptSrc)) {
|
|
119
|
+
throw new Error(`crystal-capture.sh not found at ${scriptSrc}`);
|
|
120
|
+
}
|
|
121
|
+
copyFileSync(scriptSrc, scriptDest);
|
|
122
|
+
chmodSync(scriptDest, 493);
|
|
123
|
+
return scriptDest;
|
|
124
|
+
}
|
|
125
|
+
function isCrystalCaptureLine(line) {
|
|
126
|
+
return line === CRON_TAG || line.includes("crystal-capture.sh") && line.startsWith("*");
|
|
127
|
+
}
|
|
128
|
+
function installCron() {
|
|
129
|
+
mkdirSync(join(HOME, ".ldm", "logs"), { recursive: true });
|
|
130
|
+
let existing = "";
|
|
131
|
+
try {
|
|
132
|
+
existing = execSync("crontab -l 2>/dev/null", { encoding: "utf8" });
|
|
133
|
+
} catch {
|
|
134
|
+
}
|
|
135
|
+
const lines = existing.split("\n").filter((line) => !isCrystalCaptureLine(line));
|
|
136
|
+
lines.push(CRON_TAG);
|
|
137
|
+
lines.push(CRON_ENTRY);
|
|
138
|
+
const newCrontab = lines.filter((l, i, arr) => !(l === "" && i === arr.length - 1)).join("\n") + "\n";
|
|
139
|
+
execSync("crontab -", { input: newCrontab, encoding: "utf8" });
|
|
140
|
+
}
|
|
141
|
+
function deployBackupScript() {
|
|
142
|
+
const paths = ldmPaths();
|
|
143
|
+
mkdirSync(paths.bin, { recursive: true });
|
|
144
|
+
const thisDir = dirname(fileURLToPath(import.meta.url));
|
|
145
|
+
let scriptSrc = join(thisDir, "ldm-backup.sh");
|
|
146
|
+
if (!existsSync(scriptSrc)) {
|
|
147
|
+
scriptSrc = join(thisDir, "..", "scripts", "ldm-backup.sh");
|
|
148
|
+
}
|
|
149
|
+
const scriptDest = join(paths.bin, "ldm-backup.sh");
|
|
150
|
+
if (!existsSync(scriptSrc)) {
|
|
151
|
+
throw new Error(`ldm-backup.sh not found at ${scriptSrc}`);
|
|
152
|
+
}
|
|
153
|
+
copyFileSync(scriptSrc, scriptDest);
|
|
154
|
+
chmodSync(scriptDest, 493);
|
|
155
|
+
return scriptDest;
|
|
156
|
+
}
|
|
157
|
+
function resolveStatePath(filename) {
|
|
158
|
+
const paths = ldmPaths();
|
|
159
|
+
const ldmPath = join(paths.state, filename);
|
|
160
|
+
if (existsSync(ldmPath)) return ldmPath;
|
|
161
|
+
const legacyPath = join(LEGACY_OC_DIR, "memory", filename);
|
|
162
|
+
if (existsSync(legacyPath)) return legacyPath;
|
|
163
|
+
return ldmPath;
|
|
164
|
+
}
|
|
165
|
+
function stateWritePath(filename) {
|
|
166
|
+
const paths = ldmPaths();
|
|
167
|
+
const dir = paths.state;
|
|
168
|
+
if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
|
|
169
|
+
return join(dir, filename);
|
|
170
|
+
}
|
|
171
|
+
function resolveSecretPath(filename) {
|
|
172
|
+
const paths = ldmPaths();
|
|
173
|
+
const ldmPath = join(paths.secrets, filename);
|
|
174
|
+
if (existsSync(ldmPath)) return ldmPath;
|
|
175
|
+
const legacyPath = join(LEGACY_OC_DIR, "secrets", filename);
|
|
176
|
+
if (existsSync(legacyPath)) return legacyPath;
|
|
177
|
+
return ldmPath;
|
|
178
|
+
}
|
|
179
|
+
var HOME, LDM_ROOT, CRON_TAG, CRON_ENTRY, LEGACY_OC_DIR;
|
|
180
|
+
var init_ldm = __esm({
|
|
181
|
+
"src/ldm.ts"() {
|
|
182
|
+
"use strict";
|
|
183
|
+
HOME = process.env.HOME || "";
|
|
184
|
+
LDM_ROOT = join(HOME, ".ldm");
|
|
185
|
+
CRON_TAG = "# crystal-capture";
|
|
186
|
+
CRON_ENTRY = "* * * * * ~/.ldm/bin/crystal-capture.sh >> ~/.ldm/logs/crystal-capture.log 2>&1";
|
|
187
|
+
LEGACY_OC_DIR = join(HOME, ".openclaw");
|
|
188
|
+
}
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
// src/mlx-setup.ts
|
|
192
|
+
var mlx_setup_exports = {};
|
|
193
|
+
__export(mlx_setup_exports, {
|
|
194
|
+
MLX_CONFIG: () => MLX_CONFIG,
|
|
195
|
+
canRunMlx: () => canRunMlx,
|
|
196
|
+
createLaunchAgent: () => createLaunchAgent,
|
|
197
|
+
detectPlatform: () => detectPlatform,
|
|
198
|
+
doctorCheck: () => doctorCheck,
|
|
199
|
+
installMlxLm: () => installMlxLm,
|
|
200
|
+
isMlxLmInstalled: () => isMlxLmInstalled,
|
|
201
|
+
isServerRunning: () => isServerRunning,
|
|
202
|
+
setupMlx: () => setupMlx,
|
|
203
|
+
startServer: () => startServer,
|
|
204
|
+
stopServer: () => stopServer,
|
|
205
|
+
verifyServer: () => verifyServer
|
|
206
|
+
});
|
|
207
|
+
import { existsSync as existsSync2, readFileSync as readFileSync2, writeFileSync as writeFileSync2, mkdirSync as mkdirSync2 } from "fs";
|
|
208
|
+
import { execSync as execSync2 } from "child_process";
|
|
209
|
+
import { join as join2 } from "path";
|
|
210
|
+
import { homedir } from "os";
|
|
211
|
+
function detectPlatform() {
|
|
212
|
+
const platform = process.platform;
|
|
213
|
+
const arch = process.arch;
|
|
214
|
+
if (platform === "darwin") {
|
|
215
|
+
return arch === "arm64" ? "apple-silicon" : "intel-mac";
|
|
216
|
+
}
|
|
217
|
+
if (platform === "linux") return "linux";
|
|
218
|
+
return "other";
|
|
219
|
+
}
|
|
220
|
+
function canRunMlx() {
|
|
221
|
+
return detectPlatform() === "apple-silicon";
|
|
222
|
+
}
|
|
223
|
+
function findPython() {
|
|
224
|
+
const candidates = ["python3", "/opt/homebrew/bin/python3", "/usr/local/bin/python3"];
|
|
225
|
+
for (const cmd of candidates) {
|
|
226
|
+
try {
|
|
227
|
+
const version = execSync2(`${cmd} --version 2>&1`, { encoding: "utf-8", timeout: 5e3 }).trim();
|
|
228
|
+
const match = version.match(/Python (\d+)\.(\d+)/);
|
|
229
|
+
if (match && parseInt(match[1]) >= 3 && parseInt(match[2]) >= 10) {
|
|
230
|
+
const realPath = execSync2(`which ${cmd} 2>/dev/null`, { encoding: "utf-8", timeout: 3e3 }).trim();
|
|
231
|
+
return realPath || cmd;
|
|
232
|
+
}
|
|
233
|
+
} catch {
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
return null;
|
|
237
|
+
}
|
|
238
|
+
function findInstaller() {
|
|
239
|
+
try {
|
|
240
|
+
execSync2("uv --version 2>/dev/null", { encoding: "utf-8", timeout: 3e3 });
|
|
241
|
+
return "uv";
|
|
242
|
+
} catch {
|
|
243
|
+
}
|
|
244
|
+
try {
|
|
245
|
+
execSync2("pip3 --version 2>/dev/null", { encoding: "utf-8", timeout: 3e3 });
|
|
246
|
+
return "pip3";
|
|
247
|
+
} catch {
|
|
248
|
+
}
|
|
249
|
+
return null;
|
|
250
|
+
}
|
|
251
|
+
function isMlxLmInstalled() {
|
|
252
|
+
try {
|
|
253
|
+
execSync2('python3 -c "import mlx_lm" 2>/dev/null', { timeout: 5e3 });
|
|
254
|
+
return true;
|
|
255
|
+
} catch {
|
|
256
|
+
return false;
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
function installMlxLm(steps) {
|
|
260
|
+
const installer = findInstaller();
|
|
261
|
+
if (!installer) {
|
|
262
|
+
steps.push("No pip3 or uv found. Cannot install mlx-lm.");
|
|
263
|
+
return false;
|
|
264
|
+
}
|
|
265
|
+
const cmd = installer === "uv" ? "uv pip install mlx-lm" : "pip3 install mlx-lm";
|
|
266
|
+
steps.push(`Installing mlx-lm via ${installer}...`);
|
|
267
|
+
try {
|
|
268
|
+
execSync2(cmd, { encoding: "utf-8", timeout: 12e4, stdio: "pipe" });
|
|
269
|
+
steps.push("mlx-lm installed successfully.");
|
|
270
|
+
return true;
|
|
271
|
+
} catch (err) {
|
|
272
|
+
if (installer === "pip3") {
|
|
273
|
+
try {
|
|
274
|
+
execSync2("pip3 install --user mlx-lm", { encoding: "utf-8", timeout: 12e4, stdio: "pipe" });
|
|
275
|
+
steps.push("mlx-lm installed (--user) successfully.");
|
|
276
|
+
return true;
|
|
277
|
+
} catch {
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
steps.push(`mlx-lm install failed: ${err.message.slice(0, 200)}`);
|
|
281
|
+
return false;
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
function isServerRunning() {
|
|
285
|
+
try {
|
|
286
|
+
const state = loadState();
|
|
287
|
+
const port = state?.port || MLX_PORT;
|
|
288
|
+
execSync2(`curl -s -o /dev/null -w "%{http_code}" http://localhost:${port}/v1/models`, {
|
|
289
|
+
encoding: "utf-8",
|
|
290
|
+
timeout: 3e3
|
|
291
|
+
});
|
|
292
|
+
return true;
|
|
293
|
+
} catch {
|
|
294
|
+
return false;
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
function createLaunchAgent(pythonPath, steps) {
|
|
298
|
+
const plist = `<?xml version="1.0" encoding="UTF-8"?>
|
|
299
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
300
|
+
<plist version="1.0">
|
|
301
|
+
<dict>
|
|
302
|
+
<key>Label</key>
|
|
303
|
+
<string>${MLX_PLIST_LABEL}</string>
|
|
304
|
+
<key>ProgramArguments</key>
|
|
305
|
+
<array>
|
|
306
|
+
<string>${pythonPath}</string>
|
|
307
|
+
<string>-m</string>
|
|
308
|
+
<string>mlx_lm.server</string>
|
|
309
|
+
<string>--model</string>
|
|
310
|
+
<string>${MLX_MODEL}</string>
|
|
311
|
+
<string>--port</string>
|
|
312
|
+
<string>${MLX_PORT}</string>
|
|
313
|
+
</array>
|
|
314
|
+
<key>RunAtLoad</key>
|
|
315
|
+
<true/>
|
|
316
|
+
<key>KeepAlive</key>
|
|
317
|
+
<true/>
|
|
318
|
+
<key>StandardOutPath</key>
|
|
319
|
+
<string>${MLX_LOG_PATH}</string>
|
|
320
|
+
<key>StandardErrorPath</key>
|
|
321
|
+
<string>${MLX_LOG_PATH}</string>
|
|
322
|
+
</dict>
|
|
323
|
+
</plist>`;
|
|
324
|
+
try {
|
|
325
|
+
writeFileSync2(MLX_PLIST_PATH, plist);
|
|
326
|
+
execSync2(`launchctl load "${MLX_PLIST_PATH}" 2>/dev/null`, { timeout: 5e3 });
|
|
327
|
+
steps.push(`LaunchAgent installed at ${MLX_PLIST_PATH}`);
|
|
328
|
+
steps.push(`MLX server will start on port ${MLX_PORT}`);
|
|
329
|
+
return true;
|
|
330
|
+
} catch (err) {
|
|
331
|
+
steps.push(`LaunchAgent install failed: ${err.message}`);
|
|
332
|
+
return false;
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
function startServer(steps) {
|
|
336
|
+
try {
|
|
337
|
+
execSync2(`launchctl kickstart -kp gui/$(id -u)/${MLX_PLIST_LABEL} 2>/dev/null`, { timeout: 1e4 });
|
|
338
|
+
steps.push("MLX server started.");
|
|
339
|
+
return true;
|
|
340
|
+
} catch {
|
|
341
|
+
steps.push("MLX server start failed. Check /tmp/mlx-server.log");
|
|
342
|
+
return false;
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
function stopServer() {
|
|
346
|
+
try {
|
|
347
|
+
execSync2(`launchctl unload "${MLX_PLIST_PATH}" 2>/dev/null`, { timeout: 5e3 });
|
|
348
|
+
return true;
|
|
349
|
+
} catch {
|
|
350
|
+
return false;
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
function loadState() {
|
|
354
|
+
try {
|
|
355
|
+
if (existsSync2(MLX_STATE_FILE)) {
|
|
356
|
+
return JSON.parse(readFileSync2(MLX_STATE_FILE, "utf-8"));
|
|
357
|
+
}
|
|
358
|
+
} catch {
|
|
359
|
+
}
|
|
360
|
+
return null;
|
|
361
|
+
}
|
|
362
|
+
function saveState(state) {
|
|
363
|
+
const dir = join2(HOME2, ".ldm", "state");
|
|
364
|
+
if (!existsSync2(dir)) mkdirSync2(dir, { recursive: true });
|
|
365
|
+
writeFileSync2(MLX_STATE_FILE, JSON.stringify(state, null, 2) + "\n");
|
|
366
|
+
}
|
|
367
|
+
async function verifyServer(steps) {
|
|
368
|
+
const state = loadState();
|
|
369
|
+
const port = state?.port || MLX_PORT;
|
|
370
|
+
for (let i = 0; i < 15; i++) {
|
|
371
|
+
try {
|
|
372
|
+
const resp = await fetch(`http://localhost:${port}/v1/models`, {
|
|
373
|
+
signal: AbortSignal.timeout(2e3)
|
|
374
|
+
});
|
|
375
|
+
if (resp.ok) {
|
|
376
|
+
const data = await resp.json();
|
|
377
|
+
const model = data?.data?.[0]?.id || "unknown";
|
|
378
|
+
steps.push(`MLX server verified: ${model} on port ${port}`);
|
|
379
|
+
return true;
|
|
380
|
+
}
|
|
381
|
+
} catch {
|
|
382
|
+
}
|
|
383
|
+
await new Promise((r) => setTimeout(r, 2e3));
|
|
384
|
+
}
|
|
385
|
+
steps.push("MLX server did not respond within 30 seconds. Check /tmp/mlx-server.log");
|
|
386
|
+
return false;
|
|
387
|
+
}
|
|
388
|
+
async function setupMlx(options) {
|
|
389
|
+
const steps = [];
|
|
390
|
+
const platform = detectPlatform();
|
|
391
|
+
if (platform !== "apple-silicon") {
|
|
392
|
+
steps.push(`Platform: ${platform}. MLX requires Apple Silicon. Skipping.`);
|
|
393
|
+
return { ok: false, steps };
|
|
394
|
+
}
|
|
395
|
+
steps.push("Platform: Apple Silicon detected.");
|
|
396
|
+
const pythonPath = findPython();
|
|
397
|
+
if (!pythonPath) {
|
|
398
|
+
steps.push("Python 3.10+ not found. Install via: brew install python3");
|
|
399
|
+
return { ok: false, steps };
|
|
400
|
+
}
|
|
401
|
+
steps.push(`Python: ${pythonPath}`);
|
|
402
|
+
if (!isMlxLmInstalled()) {
|
|
403
|
+
if (!options?.yes) {
|
|
404
|
+
steps.push("mlx-lm not installed. Run with --yes to auto-install, or: pip3 install mlx-lm");
|
|
405
|
+
return { ok: false, steps };
|
|
406
|
+
}
|
|
407
|
+
const installed = installMlxLm(steps);
|
|
408
|
+
if (!installed) return { ok: false, steps };
|
|
409
|
+
} else {
|
|
410
|
+
steps.push("mlx-lm: already installed.");
|
|
411
|
+
}
|
|
412
|
+
if (!existsSync2(MLX_PLIST_PATH)) {
|
|
413
|
+
const created = createLaunchAgent(pythonPath, steps);
|
|
414
|
+
if (!created) return { ok: false, steps };
|
|
415
|
+
} else {
|
|
416
|
+
steps.push("LaunchAgent: already installed.");
|
|
417
|
+
}
|
|
418
|
+
saveState({
|
|
419
|
+
installed: true,
|
|
420
|
+
port: MLX_PORT,
|
|
421
|
+
model: MLX_MODEL,
|
|
422
|
+
pythonPath,
|
|
423
|
+
installedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
424
|
+
});
|
|
425
|
+
if (!isServerRunning()) {
|
|
426
|
+
startServer(steps);
|
|
427
|
+
steps.push(`Waiting for model to load (~1.5 GB on first run)...`);
|
|
428
|
+
const verified = await verifyServer(steps);
|
|
429
|
+
if (!verified) {
|
|
430
|
+
steps.push("Server started but not yet responding. It may still be downloading the model.");
|
|
431
|
+
steps.push(`Check: tail -f ${MLX_LOG_PATH}`);
|
|
432
|
+
}
|
|
433
|
+
} else {
|
|
434
|
+
steps.push(`MLX server: already running on port ${MLX_PORT}`);
|
|
435
|
+
}
|
|
436
|
+
return { ok: true, steps };
|
|
437
|
+
}
|
|
438
|
+
function doctorCheck() {
|
|
439
|
+
const platform = detectPlatform();
|
|
440
|
+
if (platform !== "apple-silicon") {
|
|
441
|
+
return { status: "skip", detail: `${platform} (MLX requires Apple Silicon)` };
|
|
442
|
+
}
|
|
443
|
+
const state = loadState();
|
|
444
|
+
if (!state || !state.installed) {
|
|
445
|
+
return {
|
|
446
|
+
status: "warn",
|
|
447
|
+
detail: "not installed",
|
|
448
|
+
fix: "crystal init (will offer MLX setup)"
|
|
449
|
+
};
|
|
450
|
+
}
|
|
451
|
+
if (isServerRunning()) {
|
|
452
|
+
return { status: "ok", detail: `running on port ${state.port} (${state.model})` };
|
|
453
|
+
}
|
|
454
|
+
if (existsSync2(MLX_PLIST_PATH)) {
|
|
455
|
+
return {
|
|
456
|
+
status: "warn",
|
|
457
|
+
detail: "installed but not running",
|
|
458
|
+
fix: `launchctl kickstart -kp gui/$(id -u)/${MLX_PLIST_LABEL}`
|
|
459
|
+
};
|
|
460
|
+
}
|
|
461
|
+
return {
|
|
462
|
+
status: "warn",
|
|
463
|
+
detail: "installed but LaunchAgent missing",
|
|
464
|
+
fix: "crystal init (will recreate LaunchAgent)"
|
|
465
|
+
};
|
|
466
|
+
}
|
|
467
|
+
var HOME2, MLX_PORT, MLX_MODEL, MLX_STATE_FILE, MLX_PLIST_LABEL, MLX_PLIST_PATH, MLX_LOG_PATH, MLX_CONFIG;
|
|
468
|
+
var init_mlx_setup = __esm({
|
|
469
|
+
"src/mlx-setup.ts"() {
|
|
470
|
+
"use strict";
|
|
471
|
+
HOME2 = homedir();
|
|
472
|
+
MLX_PORT = 18791;
|
|
473
|
+
MLX_MODEL = "mlx-community/Qwen2.5-3B-Instruct-4bit";
|
|
474
|
+
MLX_STATE_FILE = join2(HOME2, ".ldm", "state", "mlx-server.json");
|
|
475
|
+
MLX_PLIST_LABEL = "ai.ldm.mlx-server";
|
|
476
|
+
MLX_PLIST_PATH = join2(HOME2, "Library", "LaunchAgents", `${MLX_PLIST_LABEL}.plist`);
|
|
477
|
+
MLX_LOG_PATH = "/tmp/mlx-server.log";
|
|
478
|
+
MLX_CONFIG = {
|
|
479
|
+
port: MLX_PORT,
|
|
480
|
+
model: MLX_MODEL,
|
|
481
|
+
plistPath: MLX_PLIST_PATH,
|
|
482
|
+
logPath: MLX_LOG_PATH,
|
|
483
|
+
stateFile: MLX_STATE_FILE
|
|
484
|
+
};
|
|
485
|
+
}
|
|
486
|
+
});
|
|
487
|
+
|
|
488
|
+
// src/role.ts
|
|
489
|
+
var role_exports = {};
|
|
490
|
+
__export(role_exports, {
|
|
491
|
+
demoteToNode: () => demoteToNode,
|
|
492
|
+
detectRole: () => detectRole,
|
|
493
|
+
loadRoleState: () => loadRoleState,
|
|
494
|
+
promoteToCore: () => promoteToCore
|
|
495
|
+
});
|
|
496
|
+
import { existsSync as existsSync3, readFileSync as readFileSync3, writeFileSync as writeFileSync3 } from "fs";
|
|
497
|
+
function loadRoleState() {
|
|
498
|
+
try {
|
|
499
|
+
const path = resolveStatePath(STATE_FILE);
|
|
500
|
+
if (existsSync3(path)) {
|
|
501
|
+
return JSON.parse(readFileSync3(path, "utf-8"));
|
|
502
|
+
}
|
|
503
|
+
} catch {
|
|
504
|
+
}
|
|
505
|
+
return null;
|
|
506
|
+
}
|
|
507
|
+
function saveRoleState(state) {
|
|
508
|
+
const writePath = stateWritePath(STATE_FILE);
|
|
509
|
+
writeFileSync3(writePath, JSON.stringify(state, null, 2) + "\n");
|
|
510
|
+
}
|
|
511
|
+
function hasLocalEmbeddingProvider() {
|
|
512
|
+
if (process.env.OPENAI_API_KEY) return true;
|
|
513
|
+
if (process.env.GOOGLE_API_KEY && process.env.CRYSTAL_EMBEDDING_PROVIDER === "google") return true;
|
|
514
|
+
if (process.env.CRYSTAL_EMBEDDING_PROVIDER === "ollama") return true;
|
|
515
|
+
return false;
|
|
516
|
+
}
|
|
517
|
+
function hasRelayKey() {
|
|
518
|
+
const keyPath = resolveSecretPath("crystal-relay-key");
|
|
519
|
+
return existsSync3(keyPath);
|
|
520
|
+
}
|
|
521
|
+
function detectRole() {
|
|
522
|
+
const agentId = getAgentId();
|
|
523
|
+
const paths = ldmPaths(agentId);
|
|
524
|
+
const relayUrl = process.env.CRYSTAL_RELAY_URL || null;
|
|
525
|
+
const relayToken = !!process.env.CRYSTAL_RELAY_TOKEN;
|
|
526
|
+
const relayKeyExists = hasRelayKey();
|
|
527
|
+
const localEmbeddings = hasLocalEmbeddingProvider();
|
|
528
|
+
const localDb = existsSync3(paths.crystalDb);
|
|
529
|
+
const state = loadRoleState();
|
|
530
|
+
if (state && state.override) {
|
|
531
|
+
return {
|
|
532
|
+
role: state.role,
|
|
533
|
+
source: "state-file",
|
|
534
|
+
relayUrl,
|
|
535
|
+
relayToken,
|
|
536
|
+
relayKeyExists,
|
|
537
|
+
agentId,
|
|
538
|
+
hasLocalEmbeddings: localEmbeddings,
|
|
539
|
+
hasLocalDb: localDb
|
|
540
|
+
};
|
|
541
|
+
}
|
|
542
|
+
let role = "core";
|
|
543
|
+
if ((relayUrl || relayKeyExists) && !localEmbeddings) {
|
|
544
|
+
role = "node";
|
|
545
|
+
} else if ((relayUrl || relayKeyExists) && localEmbeddings) {
|
|
546
|
+
role = "core";
|
|
547
|
+
}
|
|
548
|
+
return {
|
|
549
|
+
role,
|
|
550
|
+
source: "auto-detected",
|
|
551
|
+
relayUrl,
|
|
552
|
+
relayToken,
|
|
553
|
+
relayKeyExists,
|
|
554
|
+
agentId,
|
|
555
|
+
hasLocalEmbeddings: localEmbeddings,
|
|
556
|
+
hasLocalDb: localDb
|
|
557
|
+
};
|
|
558
|
+
}
|
|
559
|
+
function promoteToCore() {
|
|
560
|
+
saveRoleState({
|
|
561
|
+
role: "core",
|
|
562
|
+
override: true,
|
|
563
|
+
agentId: getAgentId(),
|
|
564
|
+
setAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
565
|
+
});
|
|
566
|
+
}
|
|
567
|
+
function demoteToNode(relayUrl) {
|
|
568
|
+
saveRoleState({
|
|
569
|
+
role: "node",
|
|
570
|
+
override: true,
|
|
571
|
+
relayUrl,
|
|
572
|
+
agentId: getAgentId(),
|
|
573
|
+
setAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
574
|
+
});
|
|
575
|
+
}
|
|
576
|
+
var STATE_FILE;
|
|
577
|
+
var init_role = __esm({
|
|
578
|
+
"src/role.ts"() {
|
|
579
|
+
"use strict";
|
|
580
|
+
init_ldm();
|
|
581
|
+
STATE_FILE = "crystal-role.json";
|
|
582
|
+
}
|
|
583
|
+
});
|
|
584
|
+
|
|
585
|
+
// src/crypto.ts
|
|
586
|
+
import { readFileSync as readFileSync4, existsSync as existsSync4 } from "fs";
|
|
587
|
+
import { createCipheriv, createDecipheriv, createHmac, randomBytes, hkdfSync } from "crypto";
|
|
588
|
+
import { createHash } from "crypto";
|
|
589
|
+
function loadRelayKey() {
|
|
590
|
+
if (!existsSync4(KEY_PATH)) {
|
|
591
|
+
throw new Error(
|
|
592
|
+
`Relay key not found at ${KEY_PATH}
|
|
593
|
+
Generate one: mkdir -p ~/.ldm/secrets && openssl rand -base64 32 > ~/.ldm/secrets/crystal-relay-key && chmod 600 ~/.ldm/secrets/crystal-relay-key
|
|
594
|
+
Or run: crystal pair`
|
|
595
|
+
);
|
|
596
|
+
}
|
|
597
|
+
const raw = readFileSync4(KEY_PATH, "utf-8").trim();
|
|
598
|
+
const key = Buffer.from(raw, "base64");
|
|
599
|
+
if (key.length !== 32) {
|
|
600
|
+
throw new Error(`Relay key must be 32 bytes (256 bits). Got ${key.length} bytes. Regenerate with: openssl rand -base64 32`);
|
|
601
|
+
}
|
|
602
|
+
return key;
|
|
603
|
+
}
|
|
604
|
+
function generateRelayKey() {
|
|
605
|
+
return randomBytes(32);
|
|
606
|
+
}
|
|
607
|
+
function encodePairingString(key) {
|
|
608
|
+
if (key.length !== 32) throw new Error("Key must be 32 bytes");
|
|
609
|
+
return `mc1:${key.toString("base64")}`;
|
|
610
|
+
}
|
|
611
|
+
function decodePairingString(str) {
|
|
612
|
+
const trimmed = str.trim();
|
|
613
|
+
if (!trimmed.startsWith("mc1:")) {
|
|
614
|
+
throw new Error("Invalid pairing string (expected mc1: prefix)");
|
|
615
|
+
}
|
|
616
|
+
const key = Buffer.from(trimmed.slice(4), "base64");
|
|
617
|
+
if (key.length !== 32) {
|
|
618
|
+
throw new Error(`Invalid key length: expected 32 bytes, got ${key.length}`);
|
|
619
|
+
}
|
|
620
|
+
return key;
|
|
621
|
+
}
|
|
622
|
+
var KEY_PATH, RELAY_KEY_PATH;
|
|
623
|
+
var init_crypto = __esm({
|
|
624
|
+
"src/crypto.ts"() {
|
|
625
|
+
"use strict";
|
|
626
|
+
init_ldm();
|
|
627
|
+
KEY_PATH = process.env.CRYSTAL_RELAY_KEY_PATH || resolveSecretPath("crystal-relay-key");
|
|
628
|
+
RELAY_KEY_PATH = KEY_PATH;
|
|
629
|
+
}
|
|
630
|
+
});
|
|
631
|
+
|
|
632
|
+
// src/pair.ts
|
|
633
|
+
var pair_exports = {};
|
|
634
|
+
__export(pair_exports, {
|
|
635
|
+
pairReceive: () => pairReceive,
|
|
636
|
+
pairShow: () => pairShow
|
|
637
|
+
});
|
|
638
|
+
import { existsSync as existsSync5, writeFileSync as writeFileSync4, mkdirSync as mkdirSync4, chmodSync as chmodSync2 } from "fs";
|
|
639
|
+
import { dirname as dirname2 } from "path";
|
|
640
|
+
import qrcode from "qrcode-terminal";
|
|
641
|
+
function generateQR(text) {
|
|
642
|
+
return new Promise((resolve) => {
|
|
643
|
+
qrcode.generate(text, { small: true }, (code) => {
|
|
644
|
+
resolve(code);
|
|
645
|
+
});
|
|
646
|
+
});
|
|
647
|
+
}
|
|
648
|
+
function saveKey(key) {
|
|
649
|
+
const dir = dirname2(RELAY_KEY_PATH);
|
|
650
|
+
if (!existsSync5(dir)) {
|
|
651
|
+
mkdirSync4(dir, { recursive: true });
|
|
652
|
+
}
|
|
653
|
+
writeFileSync4(RELAY_KEY_PATH, key.toString("base64") + "\n", { mode: 384 });
|
|
654
|
+
try {
|
|
655
|
+
chmodSync2(RELAY_KEY_PATH, 384);
|
|
656
|
+
} catch {
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
async function pairShow() {
|
|
660
|
+
let key;
|
|
661
|
+
try {
|
|
662
|
+
key = loadRelayKey();
|
|
663
|
+
console.log("Relay key found.\n");
|
|
664
|
+
} catch {
|
|
665
|
+
console.log("No relay key found. Generating one...");
|
|
666
|
+
key = generateRelayKey();
|
|
667
|
+
saveKey(key);
|
|
668
|
+
console.log(`Key saved to ${RELAY_KEY_PATH}
|
|
669
|
+
`);
|
|
670
|
+
}
|
|
671
|
+
const pairingString = encodePairingString(key);
|
|
672
|
+
console.log("Scan this QR code from your other device:\n");
|
|
673
|
+
const qr = await generateQR(pairingString);
|
|
674
|
+
console.log(qr);
|
|
675
|
+
console.log("Or copy this pairing code:\n");
|
|
676
|
+
console.log(` ${pairingString}
|
|
677
|
+
`);
|
|
678
|
+
console.log("On the other device, run:");
|
|
679
|
+
console.log(` crystal pair --code ${pairingString}
|
|
680
|
+
`);
|
|
681
|
+
}
|
|
682
|
+
function pairReceive(code) {
|
|
683
|
+
const key = decodePairingString(code);
|
|
684
|
+
if (existsSync5(RELAY_KEY_PATH)) {
|
|
685
|
+
try {
|
|
686
|
+
const existing = loadRelayKey();
|
|
687
|
+
if (existing.equals(key)) {
|
|
688
|
+
console.log("This device already has the same key. Nothing to do.");
|
|
689
|
+
return;
|
|
690
|
+
}
|
|
691
|
+
console.log("Replacing existing relay key with new key from pairing code.");
|
|
692
|
+
} catch {
|
|
693
|
+
}
|
|
694
|
+
}
|
|
695
|
+
saveKey(key);
|
|
696
|
+
console.log(`Key received and saved to ${RELAY_KEY_PATH}`);
|
|
697
|
+
console.log("Relay encryption is now active on this device.");
|
|
698
|
+
}
|
|
699
|
+
var init_pair = __esm({
|
|
700
|
+
"src/pair.ts"() {
|
|
701
|
+
"use strict";
|
|
702
|
+
init_crypto();
|
|
703
|
+
}
|
|
704
|
+
});
|
|
705
|
+
|
|
706
|
+
// src/installer.ts
|
|
707
|
+
init_ldm();
|
|
708
|
+
import { existsSync as existsSync6, readFileSync as readFileSync5, writeFileSync as writeFileSync5, mkdirSync as mkdirSync5, cpSync, copyFileSync as copyFileSync2, readdirSync as readdirSync2, statSync } from "fs";
|
|
709
|
+
import { join as join3, dirname as dirname3 } from "path";
|
|
710
|
+
import { execSync as execSync3 } from "child_process";
|
|
711
|
+
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
712
|
+
var HOME3 = process.env.HOME || "";
|
|
713
|
+
var LDM_ROOT2 = join3(HOME3, ".ldm");
|
|
714
|
+
var OC_ROOT = join3(HOME3, ".openclaw");
|
|
715
|
+
var CC_SETTINGS = join3(HOME3, ".claude", "settings.json");
|
|
716
|
+
var CC_MCP = join3(HOME3, ".claude", ".mcp.json");
|
|
717
|
+
var OC_MCP = join3(OC_ROOT, ".mcp.json");
|
|
23
718
|
function readVersion(pkgPath) {
|
|
24
719
|
try {
|
|
25
|
-
if (
|
|
26
|
-
const pkg = JSON.parse(
|
|
720
|
+
if (existsSync6(pkgPath)) {
|
|
721
|
+
const pkg = JSON.parse(readFileSync5(pkgPath, "utf-8"));
|
|
27
722
|
return pkg.version || null;
|
|
28
723
|
}
|
|
29
724
|
} catch {
|
|
@@ -31,20 +726,20 @@ function readVersion(pkgPath) {
|
|
|
31
726
|
return null;
|
|
32
727
|
}
|
|
33
728
|
function getRepoRoot() {
|
|
34
|
-
const thisDir =
|
|
729
|
+
const thisDir = dirname3(fileURLToPath2(import.meta.url));
|
|
35
730
|
let dir = thisDir;
|
|
36
731
|
for (let i = 0; i < 5; i++) {
|
|
37
|
-
const pkgPath =
|
|
38
|
-
if (
|
|
732
|
+
const pkgPath = join3(dir, "package.json");
|
|
733
|
+
if (existsSync6(pkgPath)) {
|
|
39
734
|
try {
|
|
40
|
-
const pkg = JSON.parse(
|
|
735
|
+
const pkg = JSON.parse(readFileSync5(pkgPath, "utf-8"));
|
|
41
736
|
if (pkg.name === "@wipcomputer/memory-crystal") return dir;
|
|
42
737
|
} catch {
|
|
43
738
|
}
|
|
44
739
|
}
|
|
45
|
-
dir =
|
|
740
|
+
dir = dirname3(dir);
|
|
46
741
|
}
|
|
47
|
-
return
|
|
742
|
+
return dirname3(thisDir);
|
|
48
743
|
}
|
|
49
744
|
function semverCompare(a, b) {
|
|
50
745
|
const pa = a.split(".").map(Number);
|
|
@@ -59,7 +754,7 @@ function getLatestNpmVersion() {
|
|
|
59
754
|
const names = ["@wipcomputer/memory-crystal", "memory-crystal"];
|
|
60
755
|
for (const name of names) {
|
|
61
756
|
try {
|
|
62
|
-
const v =
|
|
757
|
+
const v = execSync3(`npm view ${name} version 2>/dev/null`, { encoding: "utf-8", timeout: 1e4 }).trim();
|
|
63
758
|
if (v) return v;
|
|
64
759
|
} catch {
|
|
65
760
|
}
|
|
@@ -67,19 +762,19 @@ function getLatestNpmVersion() {
|
|
|
67
762
|
return null;
|
|
68
763
|
}
|
|
69
764
|
function detectInstallState() {
|
|
70
|
-
const ldmExtDir =
|
|
71
|
-
const ocExtDir =
|
|
765
|
+
const ldmExtDir = join3(LDM_ROOT2, "extensions", "memory-crystal");
|
|
766
|
+
const ocExtDir = join3(OC_ROOT, "extensions", "memory-crystal");
|
|
72
767
|
const paths = ldmPaths();
|
|
73
|
-
const installedVersion = readVersion(
|
|
768
|
+
const installedVersion = readVersion(join3(ldmExtDir, "package.json"));
|
|
74
769
|
const repoRoot = getRepoRoot();
|
|
75
|
-
let repoVersion = readVersion(
|
|
770
|
+
let repoVersion = readVersion(join3(repoRoot, "package.json")) || "0.0.0";
|
|
76
771
|
const npmVersion = getLatestNpmVersion();
|
|
77
772
|
if (npmVersion && semverCompare(npmVersion, repoVersion) > 0) repoVersion = npmVersion;
|
|
78
|
-
const ccHookDeployed =
|
|
773
|
+
const ccHookDeployed = existsSync6(join3(ldmExtDir, "dist", "cc-hook.js"));
|
|
79
774
|
let ccHookConfigured = false;
|
|
80
775
|
try {
|
|
81
|
-
if (
|
|
82
|
-
const settings = JSON.parse(
|
|
776
|
+
if (existsSync6(CC_SETTINGS)) {
|
|
777
|
+
const settings = JSON.parse(readFileSync5(CC_SETTINGS, "utf-8"));
|
|
83
778
|
const stopHooks = settings?.hooks?.Stop;
|
|
84
779
|
if (Array.isArray(stopHooks)) {
|
|
85
780
|
ccHookConfigured = stopHooks.some((entry) => {
|
|
@@ -92,10 +787,10 @@ function detectInstallState() {
|
|
|
92
787
|
} catch {
|
|
93
788
|
}
|
|
94
789
|
let mcpRegistered = false;
|
|
95
|
-
for (const mcpPath of [CC_MCP, OC_MCP,
|
|
790
|
+
for (const mcpPath of [CC_MCP, OC_MCP, join3(process.cwd(), ".mcp.json")]) {
|
|
96
791
|
try {
|
|
97
|
-
if (
|
|
98
|
-
const config = JSON.parse(
|
|
792
|
+
if (existsSync6(mcpPath)) {
|
|
793
|
+
const config = JSON.parse(readFileSync5(mcpPath, "utf-8"));
|
|
99
794
|
if (config?.mcpServers?.["memory-crystal"]) {
|
|
100
795
|
mcpRegistered = true;
|
|
101
796
|
break;
|
|
@@ -106,24 +801,24 @@ function detectInstallState() {
|
|
|
106
801
|
}
|
|
107
802
|
if (!mcpRegistered) {
|
|
108
803
|
try {
|
|
109
|
-
|
|
804
|
+
execSync3("claude mcp get memory-crystal 2>/dev/null", { encoding: "utf-8", timeout: 5e3, stdio: "pipe" });
|
|
110
805
|
mcpRegistered = true;
|
|
111
806
|
} catch {
|
|
112
807
|
}
|
|
113
808
|
}
|
|
114
|
-
const ocDetected =
|
|
115
|
-
const ocPluginDeployed =
|
|
809
|
+
const ocDetected = existsSync6(join3(OC_ROOT, "openclaw.json"));
|
|
810
|
+
const ocPluginDeployed = existsSync6(join3(ocExtDir, "dist", "openclaw.js"));
|
|
116
811
|
let cronInstalled = false;
|
|
117
812
|
try {
|
|
118
|
-
const crontab =
|
|
813
|
+
const crontab = execSync3("crontab -l 2>/dev/null", { encoding: "utf-8" });
|
|
119
814
|
cronInstalled = crontab.includes("crystal-capture");
|
|
120
815
|
} catch {
|
|
121
816
|
}
|
|
122
817
|
const role = "core";
|
|
123
|
-
const relayKeyExists =
|
|
818
|
+
const relayKeyExists = existsSync6(join3(LDM_ROOT2, "secrets", "crystal-relay-key"));
|
|
124
819
|
return {
|
|
125
|
-
ldmExists:
|
|
126
|
-
crystalDbExists:
|
|
820
|
+
ldmExists: existsSync6(LDM_ROOT2),
|
|
821
|
+
crystalDbExists: existsSync6(paths.crystalDb),
|
|
127
822
|
ccHookDeployed,
|
|
128
823
|
ccHookConfigured,
|
|
129
824
|
mcpRegistered,
|
|
@@ -139,42 +834,42 @@ function detectInstallState() {
|
|
|
139
834
|
}
|
|
140
835
|
function deployToLdm() {
|
|
141
836
|
const repoRoot = getRepoRoot();
|
|
142
|
-
const sourceDir =
|
|
143
|
-
const extDir =
|
|
144
|
-
const destDist =
|
|
145
|
-
if (!
|
|
837
|
+
const sourceDir = join3(repoRoot, "dist");
|
|
838
|
+
const extDir = join3(LDM_ROOT2, "extensions", "memory-crystal");
|
|
839
|
+
const destDist = join3(extDir, "dist");
|
|
840
|
+
if (!existsSync6(sourceDir)) {
|
|
146
841
|
throw new Error(`dist/ not found at ${sourceDir}. Run "npm run build" first.`);
|
|
147
842
|
}
|
|
148
|
-
|
|
149
|
-
const distFiles =
|
|
843
|
+
mkdirSync5(destDist, { recursive: true });
|
|
844
|
+
const distFiles = readdirSync2(sourceDir);
|
|
150
845
|
for (const file of distFiles) {
|
|
151
|
-
const srcPath =
|
|
152
|
-
const destPath =
|
|
846
|
+
const srcPath = join3(sourceDir, file);
|
|
847
|
+
const destPath = join3(destDist, file);
|
|
153
848
|
const stat = statSync(srcPath);
|
|
154
849
|
if (stat.isFile()) {
|
|
155
|
-
|
|
850
|
+
copyFileSync2(srcPath, destPath);
|
|
156
851
|
} else if (stat.isDirectory()) {
|
|
157
852
|
cpSync(srcPath, destPath, { recursive: true });
|
|
158
853
|
}
|
|
159
854
|
}
|
|
160
|
-
|
|
161
|
-
const pluginJson =
|
|
162
|
-
if (
|
|
163
|
-
|
|
855
|
+
copyFileSync2(join3(repoRoot, "package.json"), join3(extDir, "package.json"));
|
|
856
|
+
const pluginJson = join3(repoRoot, "openclaw.plugin.json");
|
|
857
|
+
if (existsSync6(pluginJson)) {
|
|
858
|
+
copyFileSync2(pluginJson, join3(extDir, "openclaw.plugin.json"));
|
|
164
859
|
}
|
|
165
|
-
const skillsDir =
|
|
166
|
-
if (
|
|
167
|
-
cpSync(skillsDir,
|
|
860
|
+
const skillsDir = join3(repoRoot, "skills");
|
|
861
|
+
if (existsSync6(skillsDir)) {
|
|
862
|
+
cpSync(skillsDir, join3(extDir, "skills"), { recursive: true });
|
|
168
863
|
}
|
|
169
|
-
const version = readVersion(
|
|
864
|
+
const version = readVersion(join3(extDir, "package.json")) || "unknown";
|
|
170
865
|
return { extensionDir: extDir, version };
|
|
171
866
|
}
|
|
172
867
|
function installLdmDeps() {
|
|
173
|
-
const extDir =
|
|
174
|
-
if (!
|
|
868
|
+
const extDir = join3(LDM_ROOT2, "extensions", "memory-crystal");
|
|
869
|
+
if (!existsSync6(join3(extDir, "package.json"))) {
|
|
175
870
|
throw new Error("package.json not found in LDM extension dir. Deploy first.");
|
|
176
871
|
}
|
|
177
|
-
|
|
872
|
+
execSync3("npm install --omit=dev", {
|
|
178
873
|
cwd: extDir,
|
|
179
874
|
encoding: "utf-8",
|
|
180
875
|
stdio: "pipe",
|
|
@@ -183,42 +878,42 @@ function installLdmDeps() {
|
|
|
183
878
|
}
|
|
184
879
|
function deployToOpenClaw() {
|
|
185
880
|
const repoRoot = getRepoRoot();
|
|
186
|
-
const sourceDir =
|
|
187
|
-
const extDir =
|
|
188
|
-
const destDist =
|
|
189
|
-
if (!
|
|
881
|
+
const sourceDir = join3(repoRoot, "dist");
|
|
882
|
+
const extDir = join3(OC_ROOT, "extensions", "memory-crystal");
|
|
883
|
+
const destDist = join3(extDir, "dist");
|
|
884
|
+
if (!existsSync6(sourceDir)) {
|
|
190
885
|
throw new Error(`dist/ not found at ${sourceDir}. Run "npm run build" first.`);
|
|
191
886
|
}
|
|
192
|
-
|
|
193
|
-
const distFiles =
|
|
887
|
+
mkdirSync5(destDist, { recursive: true });
|
|
888
|
+
const distFiles = readdirSync2(sourceDir);
|
|
194
889
|
for (const file of distFiles) {
|
|
195
|
-
const srcPath =
|
|
196
|
-
const destPath =
|
|
890
|
+
const srcPath = join3(sourceDir, file);
|
|
891
|
+
const destPath = join3(destDist, file);
|
|
197
892
|
const stat = statSync(srcPath);
|
|
198
893
|
if (stat.isFile()) {
|
|
199
|
-
|
|
894
|
+
copyFileSync2(srcPath, destPath);
|
|
200
895
|
} else if (stat.isDirectory()) {
|
|
201
896
|
cpSync(srcPath, destPath, { recursive: true });
|
|
202
897
|
}
|
|
203
898
|
}
|
|
204
|
-
|
|
205
|
-
const pluginJson =
|
|
206
|
-
if (
|
|
207
|
-
|
|
899
|
+
copyFileSync2(join3(repoRoot, "package.json"), join3(extDir, "package.json"));
|
|
900
|
+
const pluginJson = join3(repoRoot, "openclaw.plugin.json");
|
|
901
|
+
if (existsSync6(pluginJson)) {
|
|
902
|
+
copyFileSync2(pluginJson, join3(extDir, "openclaw.plugin.json"));
|
|
208
903
|
}
|
|
209
|
-
const skillsDir =
|
|
210
|
-
if (
|
|
211
|
-
cpSync(skillsDir,
|
|
904
|
+
const skillsDir = join3(repoRoot, "skills");
|
|
905
|
+
if (existsSync6(skillsDir)) {
|
|
906
|
+
cpSync(skillsDir, join3(extDir, "skills"), { recursive: true });
|
|
212
907
|
}
|
|
213
|
-
const version = readVersion(
|
|
908
|
+
const version = readVersion(join3(extDir, "package.json")) || "unknown";
|
|
214
909
|
return { extensionDir: extDir, version };
|
|
215
910
|
}
|
|
216
911
|
function installOcDeps() {
|
|
217
|
-
const extDir =
|
|
218
|
-
if (!
|
|
912
|
+
const extDir = join3(OC_ROOT, "extensions", "memory-crystal");
|
|
913
|
+
if (!existsSync6(join3(extDir, "package.json"))) {
|
|
219
914
|
throw new Error("package.json not found in OC extension dir. Deploy first.");
|
|
220
915
|
}
|
|
221
|
-
|
|
916
|
+
execSync3("npm install --omit=dev", {
|
|
222
917
|
cwd: extDir,
|
|
223
918
|
encoding: "utf-8",
|
|
224
919
|
stdio: "pipe",
|
|
@@ -226,11 +921,11 @@ function installOcDeps() {
|
|
|
226
921
|
});
|
|
227
922
|
}
|
|
228
923
|
function configureCCHook() {
|
|
229
|
-
const hookCommand = `node ${
|
|
924
|
+
const hookCommand = `node ${join3(LDM_ROOT2, "extensions", "memory-crystal", "dist", "cc-hook.js")}`;
|
|
230
925
|
let settings = {};
|
|
231
|
-
if (
|
|
926
|
+
if (existsSync6(CC_SETTINGS)) {
|
|
232
927
|
try {
|
|
233
|
-
settings = JSON.parse(
|
|
928
|
+
settings = JSON.parse(readFileSync5(CC_SETTINGS, "utf-8"));
|
|
234
929
|
} catch {
|
|
235
930
|
throw new Error(`~/.claude/settings.json exists but is not valid JSON. Fix it manually before proceeding.`);
|
|
236
931
|
}
|
|
@@ -254,14 +949,14 @@ function configureCCHook() {
|
|
|
254
949
|
} else {
|
|
255
950
|
settings.hooks.Stop.push(hookEntry);
|
|
256
951
|
}
|
|
257
|
-
|
|
258
|
-
|
|
952
|
+
mkdirSync5(join3(HOME3, ".claude"), { recursive: true });
|
|
953
|
+
writeFileSync5(CC_SETTINGS, JSON.stringify(settings, null, 2) + "\n");
|
|
259
954
|
}
|
|
260
955
|
function registerMCPServer() {
|
|
261
|
-
const mcpServerPath =
|
|
956
|
+
const mcpServerPath = join3(LDM_ROOT2, "extensions", "memory-crystal", "dist", "mcp-server.js");
|
|
262
957
|
const addCmd = `claude mcp add --scope user -e OPENCLAW_HOME=${OC_ROOT} memory-crystal -- node "${mcpServerPath}"`;
|
|
263
958
|
try {
|
|
264
|
-
|
|
959
|
+
execSync3(addCmd, {
|
|
265
960
|
encoding: "utf-8",
|
|
266
961
|
stdio: "pipe",
|
|
267
962
|
timeout: 15e3
|
|
@@ -271,17 +966,17 @@ function registerMCPServer() {
|
|
|
271
966
|
const output = (err.stderr || "") + (err.stdout || "");
|
|
272
967
|
if (output.includes("already exists")) {
|
|
273
968
|
try {
|
|
274
|
-
|
|
275
|
-
|
|
969
|
+
execSync3("claude mcp remove memory-crystal --scope user", { encoding: "utf-8", stdio: "pipe", timeout: 1e4 });
|
|
970
|
+
execSync3(addCmd, { encoding: "utf-8", stdio: "pipe", timeout: 15e3 });
|
|
276
971
|
} catch {
|
|
277
972
|
}
|
|
278
973
|
return;
|
|
279
974
|
}
|
|
280
975
|
}
|
|
281
976
|
let config = {};
|
|
282
|
-
if (
|
|
977
|
+
if (existsSync6(CC_MCP)) {
|
|
283
978
|
try {
|
|
284
|
-
config = JSON.parse(
|
|
979
|
+
config = JSON.parse(readFileSync5(CC_MCP, "utf-8"));
|
|
285
980
|
} catch {
|
|
286
981
|
}
|
|
287
982
|
}
|
|
@@ -291,15 +986,15 @@ function registerMCPServer() {
|
|
|
291
986
|
args: [mcpServerPath],
|
|
292
987
|
env: { OPENCLAW_HOME: OC_ROOT }
|
|
293
988
|
};
|
|
294
|
-
|
|
295
|
-
|
|
989
|
+
mkdirSync5(join3(HOME3, ".claude"), { recursive: true });
|
|
990
|
+
writeFileSync5(CC_MCP, JSON.stringify(config, null, 2) + "\n");
|
|
296
991
|
}
|
|
297
992
|
function registerOcMCPServer() {
|
|
298
|
-
const mcpServerPath =
|
|
993
|
+
const mcpServerPath = join3(OC_ROOT, "extensions", "memory-crystal", "dist", "mcp-server.js");
|
|
299
994
|
let config = {};
|
|
300
|
-
if (
|
|
995
|
+
if (existsSync6(OC_MCP)) {
|
|
301
996
|
try {
|
|
302
|
-
config = JSON.parse(
|
|
997
|
+
config = JSON.parse(readFileSync5(OC_MCP, "utf-8"));
|
|
303
998
|
} catch {
|
|
304
999
|
}
|
|
305
1000
|
}
|
|
@@ -309,21 +1004,21 @@ function registerOcMCPServer() {
|
|
|
309
1004
|
args: [mcpServerPath],
|
|
310
1005
|
env: { OPENCLAW_HOME: OC_ROOT }
|
|
311
1006
|
};
|
|
312
|
-
|
|
1007
|
+
writeFileSync5(OC_MCP, JSON.stringify(config, null, 2) + "\n");
|
|
313
1008
|
}
|
|
314
1009
|
function backupCrystalDb() {
|
|
315
1010
|
const paths = ldmPaths();
|
|
316
1011
|
const dbPath = paths.crystalDb;
|
|
317
|
-
if (!
|
|
1012
|
+
if (!existsSync6(dbPath)) {
|
|
318
1013
|
throw new Error(`crystal.db not found at ${dbPath}`);
|
|
319
1014
|
}
|
|
320
1015
|
const timestamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-").slice(0, 19);
|
|
321
1016
|
const backupPath = `${dbPath}.pre-update-${timestamp}`;
|
|
322
|
-
|
|
1017
|
+
copyFileSync2(dbPath, backupPath);
|
|
323
1018
|
const walPath = dbPath + "-wal";
|
|
324
1019
|
const shmPath = dbPath + "-shm";
|
|
325
|
-
if (
|
|
326
|
-
if (
|
|
1020
|
+
if (existsSync6(walPath)) copyFileSync2(walPath, backupPath + "-wal");
|
|
1021
|
+
if (existsSync6(shmPath)) copyFileSync2(shmPath, backupPath + "-shm");
|
|
327
1022
|
const origSize = statSync(dbPath).size;
|
|
328
1023
|
const backupSize = statSync(backupPath).size;
|
|
329
1024
|
if (backupSize !== origSize) {
|
|
@@ -334,7 +1029,7 @@ function backupCrystalDb() {
|
|
|
334
1029
|
async function verifyCrystalDbReadable() {
|
|
335
1030
|
const paths = ldmPaths();
|
|
336
1031
|
const dbPath = paths.crystalDb;
|
|
337
|
-
if (!
|
|
1032
|
+
if (!existsSync6(dbPath)) return;
|
|
338
1033
|
const { default: Database } = await import("better-sqlite3");
|
|
339
1034
|
const db = new Database(dbPath, { readonly: true });
|
|
340
1035
|
try {
|
|
@@ -371,7 +1066,7 @@ function formatUpdateSummary(oldVersion, newVersion) {
|
|
|
371
1066
|
}
|
|
372
1067
|
function ldmCliAvailable() {
|
|
373
1068
|
try {
|
|
374
|
-
|
|
1069
|
+
execSync3("ldm --version", { stdio: "pipe", timeout: 5e3 });
|
|
375
1070
|
return true;
|
|
376
1071
|
} catch {
|
|
377
1072
|
return false;
|
|
@@ -380,8 +1075,8 @@ function ldmCliAvailable() {
|
|
|
380
1075
|
function bootstrapLdmOs(steps) {
|
|
381
1076
|
try {
|
|
382
1077
|
steps.push("Installing LDM OS infrastructure...");
|
|
383
|
-
|
|
384
|
-
|
|
1078
|
+
execSync3("npm install -g @wipcomputer/wip-ldm-os", { stdio: "pipe", timeout: 12e4 });
|
|
1079
|
+
execSync3("ldm --version", { stdio: "pipe", timeout: 5e3 });
|
|
385
1080
|
steps.push("LDM OS installed.");
|
|
386
1081
|
return true;
|
|
387
1082
|
} catch {
|
|
@@ -392,7 +1087,7 @@ function bootstrapLdmOs(steps) {
|
|
|
392
1087
|
function runLdmInstall(repoDir) {
|
|
393
1088
|
const steps = [];
|
|
394
1089
|
try {
|
|
395
|
-
|
|
1090
|
+
execSync3("ldm init --yes --none", { stdio: "pipe", timeout: 3e4 });
|
|
396
1091
|
steps.push("LDM initialized via ldm CLI");
|
|
397
1092
|
} catch (err) {
|
|
398
1093
|
const msg = (err.stderr || err.message || "").toString().trim();
|
|
@@ -401,7 +1096,7 @@ function runLdmInstall(repoDir) {
|
|
|
401
1096
|
}
|
|
402
1097
|
}
|
|
403
1098
|
try {
|
|
404
|
-
|
|
1099
|
+
execSync3(`ldm install "${repoDir}"`, { stdio: "pipe", timeout: 6e4 });
|
|
405
1100
|
steps.push("Generic deployment handled by ldm install (extensions, MCP, hooks)");
|
|
406
1101
|
return { ok: true, steps };
|
|
407
1102
|
} catch (err) {
|
|
@@ -432,7 +1127,7 @@ async function runInstallOrUpdate(options) {
|
|
|
432
1127
|
if (npmV && semverCompare(npmV, state.installedVersion) > 0) {
|
|
433
1128
|
steps.push(`Upgrading v${state.installedVersion} -> v${npmV} via npm...`);
|
|
434
1129
|
try {
|
|
435
|
-
|
|
1130
|
+
execSync3("npm install -g @wipcomputer/memory-crystal 2>&1", { encoding: "utf-8", timeout: 6e4, stdio: "pipe" });
|
|
436
1131
|
steps.push(`Installed @wipcomputer/memory-crystal@${npmV}`);
|
|
437
1132
|
steps.push("Continuing with updated code...");
|
|
438
1133
|
} catch (err) {
|
|
@@ -452,10 +1147,10 @@ async function runInstallOrUpdate(options) {
|
|
|
452
1147
|
steps.push(...delegateResult.steps);
|
|
453
1148
|
if (delegateResult.ok) {
|
|
454
1149
|
ldmDelegated = true;
|
|
455
|
-
const ldmExtDir =
|
|
456
|
-
if (
|
|
457
|
-
const ocExtDir =
|
|
458
|
-
if (
|
|
1150
|
+
const ldmExtDir = join3(LDM_ROOT2, "extensions", "memory-crystal");
|
|
1151
|
+
if (existsSync6(ldmExtDir)) deployedTo.push(ldmExtDir);
|
|
1152
|
+
const ocExtDir = join3(OC_ROOT, "extensions", "memory-crystal");
|
|
1153
|
+
if (existsSync6(ocExtDir)) deployedTo.push(ocExtDir);
|
|
459
1154
|
}
|
|
460
1155
|
}
|
|
461
1156
|
if (ldmDelegated) {
|
|
@@ -520,13 +1215,13 @@ async function runInstallOrUpdate(options) {
|
|
|
520
1215
|
}
|
|
521
1216
|
} else if (options.importDb) {
|
|
522
1217
|
const importPath = options.importDb;
|
|
523
|
-
if (!
|
|
1218
|
+
if (!existsSync6(importPath)) {
|
|
524
1219
|
steps.push(`Import path not found: ${importPath}`);
|
|
525
1220
|
} else {
|
|
526
1221
|
try {
|
|
527
1222
|
const paths = ldmPaths();
|
|
528
|
-
|
|
529
|
-
|
|
1223
|
+
mkdirSync5(join3(paths.root, "memory"), { recursive: true });
|
|
1224
|
+
copyFileSync2(importPath, paths.crystalDb);
|
|
530
1225
|
const { default: Database } = await import("better-sqlite3");
|
|
531
1226
|
const db = new Database(paths.crystalDb, { readonly: true });
|
|
532
1227
|
const row = db.prepare("SELECT COUNT(*) as count FROM chunks").get();
|
|
@@ -544,11 +1239,11 @@ async function runInstallOrUpdate(options) {
|
|
|
544
1239
|
}
|
|
545
1240
|
if (ldmDelegated) {
|
|
546
1241
|
const repoRoot = getRepoRoot();
|
|
547
|
-
const ldmExtDir =
|
|
548
|
-
if (
|
|
549
|
-
const ocExtDir =
|
|
550
|
-
if (
|
|
551
|
-
steps.push(`Version synced to v${readVersion(
|
|
1242
|
+
const ldmExtDir = join3(LDM_ROOT2, "extensions", "memory-crystal");
|
|
1243
|
+
if (existsSync6(ldmExtDir)) copyFileSync2(join3(repoRoot, "package.json"), join3(ldmExtDir, "package.json"));
|
|
1244
|
+
const ocExtDir = join3(OC_ROOT, "extensions", "memory-crystal");
|
|
1245
|
+
if (existsSync6(ocExtDir)) copyFileSync2(join3(repoRoot, "package.json"), join3(ocExtDir, "package.json"));
|
|
1246
|
+
steps.push(`Version synced to v${readVersion(join3(repoRoot, "package.json")) || "unknown"}`);
|
|
552
1247
|
}
|
|
553
1248
|
if (!ldmDelegated) {
|
|
554
1249
|
const ldmResult = deployToLdm();
|
|
@@ -600,11 +1295,11 @@ async function runInstallOrUpdate(options) {
|
|
|
600
1295
|
steps.push(`Backup script failed: ${err.message}`);
|
|
601
1296
|
}
|
|
602
1297
|
try {
|
|
603
|
-
const { canRunMlx, isMlxLmInstalled, isServerRunning } = await
|
|
604
|
-
if (
|
|
605
|
-
if (
|
|
1298
|
+
const { canRunMlx: canRunMlx2, isMlxLmInstalled: isMlxLmInstalled2, isServerRunning: isServerRunning2 } = await Promise.resolve().then(() => (init_mlx_setup(), mlx_setup_exports));
|
|
1299
|
+
if (canRunMlx2()) {
|
|
1300
|
+
if (isServerRunning2()) {
|
|
606
1301
|
steps.push("MLX LLM: already running");
|
|
607
|
-
} else if (
|
|
1302
|
+
} else if (isMlxLmInstalled2()) {
|
|
608
1303
|
steps.push("MLX LLM: installed but not running. Start with: launchctl kickstart -kp gui/$(id -u)/ai.ldm.mlx-server");
|
|
609
1304
|
} else {
|
|
610
1305
|
steps.push('MLX LLM: Apple Silicon detected. Run "crystal mlx setup" to install local LLM for free, fast, offline search quality.');
|
|
@@ -653,16 +1348,16 @@ async function runInstallOrUpdate(options) {
|
|
|
653
1348
|
}
|
|
654
1349
|
if (options.role === "core") {
|
|
655
1350
|
try {
|
|
656
|
-
const { promoteToCore } = await
|
|
657
|
-
|
|
1351
|
+
const { promoteToCore: promoteToCore2 } = await Promise.resolve().then(() => (init_role(), role_exports));
|
|
1352
|
+
promoteToCore2();
|
|
658
1353
|
steps.push("Role set to Core");
|
|
659
1354
|
} catch (err) {
|
|
660
1355
|
steps.push(`Role setup failed: ${err.message}`);
|
|
661
1356
|
}
|
|
662
1357
|
} else if (options.role === "node") {
|
|
663
1358
|
try {
|
|
664
|
-
const { demoteToNode } = await
|
|
665
|
-
|
|
1359
|
+
const { demoteToNode: demoteToNode2 } = await Promise.resolve().then(() => (init_role(), role_exports));
|
|
1360
|
+
demoteToNode2();
|
|
666
1361
|
steps.push("Role set to Node");
|
|
667
1362
|
} catch (err) {
|
|
668
1363
|
steps.push(`Role setup failed: ${err.message}`);
|
|
@@ -670,24 +1365,24 @@ async function runInstallOrUpdate(options) {
|
|
|
670
1365
|
}
|
|
671
1366
|
if (options.pairCode) {
|
|
672
1367
|
try {
|
|
673
|
-
const { pairReceive } = await
|
|
674
|
-
|
|
1368
|
+
const { pairReceive: pairReceive2 } = await Promise.resolve().then(() => (init_pair(), pair_exports));
|
|
1369
|
+
pairReceive2(options.pairCode);
|
|
675
1370
|
steps.push("Pairing code accepted");
|
|
676
1371
|
} catch (err) {
|
|
677
1372
|
steps.push(`Pairing failed: ${err.message}`);
|
|
678
1373
|
}
|
|
679
1374
|
}
|
|
680
1375
|
if (options.role === "node" || process.env.CRYSTAL_RELAY_URL) {
|
|
681
|
-
const secretsDir =
|
|
682
|
-
const envPath =
|
|
683
|
-
if (!
|
|
1376
|
+
const secretsDir = join3(LDM_ROOT2, "secrets");
|
|
1377
|
+
const envPath = join3(secretsDir, "crystal-relay.env");
|
|
1378
|
+
if (!existsSync6(envPath)) {
|
|
684
1379
|
const relayUrl = "https://memory-crystal-relay.wipcomputer.workers.dev";
|
|
685
1380
|
let token = "";
|
|
686
1381
|
try {
|
|
687
|
-
const saTokenPath =
|
|
688
|
-
if (
|
|
689
|
-
const saToken =
|
|
690
|
-
token =
|
|
1382
|
+
const saTokenPath = join3(OC_ROOT, "secrets", "op-sa-token");
|
|
1383
|
+
if (existsSync6(saTokenPath)) {
|
|
1384
|
+
const saToken = readFileSync5(saTokenPath, "utf8").trim();
|
|
1385
|
+
token = execSync3(
|
|
691
1386
|
`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
1387
|
{ encoding: "utf8", timeout: 15e3 }
|
|
693
1388
|
).trim();
|
|
@@ -704,19 +1399,19 @@ async function runInstallOrUpdate(options) {
|
|
|
704
1399
|
token = token.trim();
|
|
705
1400
|
}
|
|
706
1401
|
if (token) {
|
|
707
|
-
|
|
708
|
-
|
|
1402
|
+
mkdirSync5(secretsDir, { recursive: true });
|
|
1403
|
+
writeFileSync5(envPath, `export CRYSTAL_RELAY_URL=${relayUrl}
|
|
709
1404
|
export CRYSTAL_RELAY_TOKEN=${token}
|
|
710
1405
|
export CRYSTAL_AGENT_ID=${agentId}
|
|
711
1406
|
`);
|
|
712
1407
|
process.env.CRYSTAL_RELAY_URL = relayUrl;
|
|
713
1408
|
process.env.CRYSTAL_RELAY_TOKEN = token;
|
|
714
1409
|
steps.push("Relay config written to ~/.ldm/secrets/crystal-relay.env");
|
|
715
|
-
const shellProfile =
|
|
1410
|
+
const shellProfile = join3(HOME3, ".zshrc");
|
|
716
1411
|
const sourceLine = `source ${envPath}`;
|
|
717
1412
|
let alreadySourced = false;
|
|
718
1413
|
try {
|
|
719
|
-
alreadySourced =
|
|
1414
|
+
alreadySourced = readFileSync5(shellProfile, "utf8").includes(sourceLine);
|
|
720
1415
|
} catch {
|
|
721
1416
|
}
|
|
722
1417
|
if (!alreadySourced && process.stdin.isTTY) {
|