@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/doctor.js
CHANGED
|
@@ -1,20 +1,485 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
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
|
+
};
|
|
10
|
+
|
|
11
|
+
// src/mlx-setup.ts
|
|
12
|
+
var mlx_setup_exports = {};
|
|
13
|
+
__export(mlx_setup_exports, {
|
|
14
|
+
MLX_CONFIG: () => MLX_CONFIG,
|
|
15
|
+
canRunMlx: () => canRunMlx,
|
|
16
|
+
createLaunchAgent: () => createLaunchAgent,
|
|
17
|
+
detectPlatform: () => detectPlatform,
|
|
18
|
+
doctorCheck: () => doctorCheck,
|
|
19
|
+
installMlxLm: () => installMlxLm,
|
|
20
|
+
isMlxLmInstalled: () => isMlxLmInstalled,
|
|
21
|
+
isServerRunning: () => isServerRunning,
|
|
22
|
+
setupMlx: () => setupMlx,
|
|
23
|
+
startServer: () => startServer,
|
|
24
|
+
stopServer: () => stopServer,
|
|
25
|
+
verifyServer: () => verifyServer
|
|
26
|
+
});
|
|
27
|
+
import { existsSync as existsSync4, readFileSync as readFileSync4, writeFileSync as writeFileSync4, mkdirSync as mkdirSync3 } from "fs";
|
|
28
|
+
import { execSync as execSync3 } from "child_process";
|
|
29
|
+
import { join as join3 } from "path";
|
|
30
|
+
import { homedir } from "os";
|
|
31
|
+
function detectPlatform() {
|
|
32
|
+
const platform = process.platform;
|
|
33
|
+
const arch = process.arch;
|
|
34
|
+
if (platform === "darwin") {
|
|
35
|
+
return arch === "arm64" ? "apple-silicon" : "intel-mac";
|
|
36
|
+
}
|
|
37
|
+
if (platform === "linux") return "linux";
|
|
38
|
+
return "other";
|
|
39
|
+
}
|
|
40
|
+
function canRunMlx() {
|
|
41
|
+
return detectPlatform() === "apple-silicon";
|
|
42
|
+
}
|
|
43
|
+
function findPython() {
|
|
44
|
+
const candidates = ["python3", "/opt/homebrew/bin/python3", "/usr/local/bin/python3"];
|
|
45
|
+
for (const cmd of candidates) {
|
|
46
|
+
try {
|
|
47
|
+
const version = execSync3(`${cmd} --version 2>&1`, { encoding: "utf-8", timeout: 5e3 }).trim();
|
|
48
|
+
const match = version.match(/Python (\d+)\.(\d+)/);
|
|
49
|
+
if (match && parseInt(match[1]) >= 3 && parseInt(match[2]) >= 10) {
|
|
50
|
+
const realPath = execSync3(`which ${cmd} 2>/dev/null`, { encoding: "utf-8", timeout: 3e3 }).trim();
|
|
51
|
+
return realPath || cmd;
|
|
52
|
+
}
|
|
53
|
+
} catch {
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
58
|
+
function findInstaller() {
|
|
59
|
+
try {
|
|
60
|
+
execSync3("uv --version 2>/dev/null", { encoding: "utf-8", timeout: 3e3 });
|
|
61
|
+
return "uv";
|
|
62
|
+
} catch {
|
|
63
|
+
}
|
|
64
|
+
try {
|
|
65
|
+
execSync3("pip3 --version 2>/dev/null", { encoding: "utf-8", timeout: 3e3 });
|
|
66
|
+
return "pip3";
|
|
67
|
+
} catch {
|
|
68
|
+
}
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
function isMlxLmInstalled() {
|
|
72
|
+
try {
|
|
73
|
+
execSync3('python3 -c "import mlx_lm" 2>/dev/null', { timeout: 5e3 });
|
|
74
|
+
return true;
|
|
75
|
+
} catch {
|
|
76
|
+
return false;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
function installMlxLm(steps) {
|
|
80
|
+
const installer = findInstaller();
|
|
81
|
+
if (!installer) {
|
|
82
|
+
steps.push("No pip3 or uv found. Cannot install mlx-lm.");
|
|
83
|
+
return false;
|
|
84
|
+
}
|
|
85
|
+
const cmd = installer === "uv" ? "uv pip install mlx-lm" : "pip3 install mlx-lm";
|
|
86
|
+
steps.push(`Installing mlx-lm via ${installer}...`);
|
|
87
|
+
try {
|
|
88
|
+
execSync3(cmd, { encoding: "utf-8", timeout: 12e4, stdio: "pipe" });
|
|
89
|
+
steps.push("mlx-lm installed successfully.");
|
|
90
|
+
return true;
|
|
91
|
+
} catch (err) {
|
|
92
|
+
if (installer === "pip3") {
|
|
93
|
+
try {
|
|
94
|
+
execSync3("pip3 install --user mlx-lm", { encoding: "utf-8", timeout: 12e4, stdio: "pipe" });
|
|
95
|
+
steps.push("mlx-lm installed (--user) successfully.");
|
|
96
|
+
return true;
|
|
97
|
+
} catch {
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
steps.push(`mlx-lm install failed: ${err.message.slice(0, 200)}`);
|
|
101
|
+
return false;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
function isServerRunning() {
|
|
105
|
+
try {
|
|
106
|
+
const state = loadState();
|
|
107
|
+
const port = state?.port || MLX_PORT;
|
|
108
|
+
execSync3(`curl -s -o /dev/null -w "%{http_code}" http://localhost:${port}/v1/models`, {
|
|
109
|
+
encoding: "utf-8",
|
|
110
|
+
timeout: 3e3
|
|
111
|
+
});
|
|
112
|
+
return true;
|
|
113
|
+
} catch {
|
|
114
|
+
return false;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
function createLaunchAgent(pythonPath, steps) {
|
|
118
|
+
const plist = `<?xml version="1.0" encoding="UTF-8"?>
|
|
119
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
120
|
+
<plist version="1.0">
|
|
121
|
+
<dict>
|
|
122
|
+
<key>Label</key>
|
|
123
|
+
<string>${MLX_PLIST_LABEL}</string>
|
|
124
|
+
<key>ProgramArguments</key>
|
|
125
|
+
<array>
|
|
126
|
+
<string>${pythonPath}</string>
|
|
127
|
+
<string>-m</string>
|
|
128
|
+
<string>mlx_lm.server</string>
|
|
129
|
+
<string>--model</string>
|
|
130
|
+
<string>${MLX_MODEL}</string>
|
|
131
|
+
<string>--port</string>
|
|
132
|
+
<string>${MLX_PORT}</string>
|
|
133
|
+
</array>
|
|
134
|
+
<key>RunAtLoad</key>
|
|
135
|
+
<true/>
|
|
136
|
+
<key>KeepAlive</key>
|
|
137
|
+
<true/>
|
|
138
|
+
<key>StandardOutPath</key>
|
|
139
|
+
<string>${MLX_LOG_PATH}</string>
|
|
140
|
+
<key>StandardErrorPath</key>
|
|
141
|
+
<string>${MLX_LOG_PATH}</string>
|
|
142
|
+
</dict>
|
|
143
|
+
</plist>`;
|
|
144
|
+
try {
|
|
145
|
+
writeFileSync4(MLX_PLIST_PATH, plist);
|
|
146
|
+
execSync3(`launchctl load "${MLX_PLIST_PATH}" 2>/dev/null`, { timeout: 5e3 });
|
|
147
|
+
steps.push(`LaunchAgent installed at ${MLX_PLIST_PATH}`);
|
|
148
|
+
steps.push(`MLX server will start on port ${MLX_PORT}`);
|
|
149
|
+
return true;
|
|
150
|
+
} catch (err) {
|
|
151
|
+
steps.push(`LaunchAgent install failed: ${err.message}`);
|
|
152
|
+
return false;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
function startServer(steps) {
|
|
156
|
+
try {
|
|
157
|
+
execSync3(`launchctl kickstart -kp gui/$(id -u)/${MLX_PLIST_LABEL} 2>/dev/null`, { timeout: 1e4 });
|
|
158
|
+
steps.push("MLX server started.");
|
|
159
|
+
return true;
|
|
160
|
+
} catch {
|
|
161
|
+
steps.push("MLX server start failed. Check /tmp/mlx-server.log");
|
|
162
|
+
return false;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
function stopServer() {
|
|
166
|
+
try {
|
|
167
|
+
execSync3(`launchctl unload "${MLX_PLIST_PATH}" 2>/dev/null`, { timeout: 5e3 });
|
|
168
|
+
return true;
|
|
169
|
+
} catch {
|
|
170
|
+
return false;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
function loadState() {
|
|
174
|
+
try {
|
|
175
|
+
if (existsSync4(MLX_STATE_FILE)) {
|
|
176
|
+
return JSON.parse(readFileSync4(MLX_STATE_FILE, "utf-8"));
|
|
177
|
+
}
|
|
178
|
+
} catch {
|
|
179
|
+
}
|
|
180
|
+
return null;
|
|
181
|
+
}
|
|
182
|
+
function saveState(state) {
|
|
183
|
+
const dir = join3(HOME3, ".ldm", "state");
|
|
184
|
+
if (!existsSync4(dir)) mkdirSync3(dir, { recursive: true });
|
|
185
|
+
writeFileSync4(MLX_STATE_FILE, JSON.stringify(state, null, 2) + "\n");
|
|
186
|
+
}
|
|
187
|
+
async function verifyServer(steps) {
|
|
188
|
+
const state = loadState();
|
|
189
|
+
const port = state?.port || MLX_PORT;
|
|
190
|
+
for (let i = 0; i < 15; i++) {
|
|
191
|
+
try {
|
|
192
|
+
const resp = await fetch(`http://localhost:${port}/v1/models`, {
|
|
193
|
+
signal: AbortSignal.timeout(2e3)
|
|
194
|
+
});
|
|
195
|
+
if (resp.ok) {
|
|
196
|
+
const data = await resp.json();
|
|
197
|
+
const model = data?.data?.[0]?.id || "unknown";
|
|
198
|
+
steps.push(`MLX server verified: ${model} on port ${port}`);
|
|
199
|
+
return true;
|
|
200
|
+
}
|
|
201
|
+
} catch {
|
|
202
|
+
}
|
|
203
|
+
await new Promise((r) => setTimeout(r, 2e3));
|
|
204
|
+
}
|
|
205
|
+
steps.push("MLX server did not respond within 30 seconds. Check /tmp/mlx-server.log");
|
|
206
|
+
return false;
|
|
207
|
+
}
|
|
208
|
+
async function setupMlx(options) {
|
|
209
|
+
const steps = [];
|
|
210
|
+
const platform = detectPlatform();
|
|
211
|
+
if (platform !== "apple-silicon") {
|
|
212
|
+
steps.push(`Platform: ${platform}. MLX requires Apple Silicon. Skipping.`);
|
|
213
|
+
return { ok: false, steps };
|
|
214
|
+
}
|
|
215
|
+
steps.push("Platform: Apple Silicon detected.");
|
|
216
|
+
const pythonPath = findPython();
|
|
217
|
+
if (!pythonPath) {
|
|
218
|
+
steps.push("Python 3.10+ not found. Install via: brew install python3");
|
|
219
|
+
return { ok: false, steps };
|
|
220
|
+
}
|
|
221
|
+
steps.push(`Python: ${pythonPath}`);
|
|
222
|
+
if (!isMlxLmInstalled()) {
|
|
223
|
+
if (!options?.yes) {
|
|
224
|
+
steps.push("mlx-lm not installed. Run with --yes to auto-install, or: pip3 install mlx-lm");
|
|
225
|
+
return { ok: false, steps };
|
|
226
|
+
}
|
|
227
|
+
const installed = installMlxLm(steps);
|
|
228
|
+
if (!installed) return { ok: false, steps };
|
|
229
|
+
} else {
|
|
230
|
+
steps.push("mlx-lm: already installed.");
|
|
231
|
+
}
|
|
232
|
+
if (!existsSync4(MLX_PLIST_PATH)) {
|
|
233
|
+
const created = createLaunchAgent(pythonPath, steps);
|
|
234
|
+
if (!created) return { ok: false, steps };
|
|
235
|
+
} else {
|
|
236
|
+
steps.push("LaunchAgent: already installed.");
|
|
237
|
+
}
|
|
238
|
+
saveState({
|
|
239
|
+
installed: true,
|
|
240
|
+
port: MLX_PORT,
|
|
241
|
+
model: MLX_MODEL,
|
|
242
|
+
pythonPath,
|
|
243
|
+
installedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
244
|
+
});
|
|
245
|
+
if (!isServerRunning()) {
|
|
246
|
+
startServer(steps);
|
|
247
|
+
steps.push(`Waiting for model to load (~1.5 GB on first run)...`);
|
|
248
|
+
const verified = await verifyServer(steps);
|
|
249
|
+
if (!verified) {
|
|
250
|
+
steps.push("Server started but not yet responding. It may still be downloading the model.");
|
|
251
|
+
steps.push(`Check: tail -f ${MLX_LOG_PATH}`);
|
|
252
|
+
}
|
|
253
|
+
} else {
|
|
254
|
+
steps.push(`MLX server: already running on port ${MLX_PORT}`);
|
|
255
|
+
}
|
|
256
|
+
return { ok: true, steps };
|
|
257
|
+
}
|
|
258
|
+
function doctorCheck() {
|
|
259
|
+
const platform = detectPlatform();
|
|
260
|
+
if (platform !== "apple-silicon") {
|
|
261
|
+
return { status: "skip", detail: `${platform} (MLX requires Apple Silicon)` };
|
|
262
|
+
}
|
|
263
|
+
const state = loadState();
|
|
264
|
+
if (!state || !state.installed) {
|
|
265
|
+
return {
|
|
266
|
+
status: "warn",
|
|
267
|
+
detail: "not installed",
|
|
268
|
+
fix: "crystal init (will offer MLX setup)"
|
|
269
|
+
};
|
|
270
|
+
}
|
|
271
|
+
if (isServerRunning()) {
|
|
272
|
+
return { status: "ok", detail: `running on port ${state.port} (${state.model})` };
|
|
273
|
+
}
|
|
274
|
+
if (existsSync4(MLX_PLIST_PATH)) {
|
|
275
|
+
return {
|
|
276
|
+
status: "warn",
|
|
277
|
+
detail: "installed but not running",
|
|
278
|
+
fix: `launchctl kickstart -kp gui/$(id -u)/${MLX_PLIST_LABEL}`
|
|
279
|
+
};
|
|
280
|
+
}
|
|
281
|
+
return {
|
|
282
|
+
status: "warn",
|
|
283
|
+
detail: "installed but LaunchAgent missing",
|
|
284
|
+
fix: "crystal init (will recreate LaunchAgent)"
|
|
285
|
+
};
|
|
286
|
+
}
|
|
287
|
+
var HOME3, MLX_PORT, MLX_MODEL, MLX_STATE_FILE, MLX_PLIST_LABEL, MLX_PLIST_PATH, MLX_LOG_PATH, MLX_CONFIG;
|
|
288
|
+
var init_mlx_setup = __esm({
|
|
289
|
+
"src/mlx-setup.ts"() {
|
|
290
|
+
"use strict";
|
|
291
|
+
HOME3 = homedir();
|
|
292
|
+
MLX_PORT = 18791;
|
|
293
|
+
MLX_MODEL = "mlx-community/Qwen2.5-3B-Instruct-4bit";
|
|
294
|
+
MLX_STATE_FILE = join3(HOME3, ".ldm", "state", "mlx-server.json");
|
|
295
|
+
MLX_PLIST_LABEL = "ai.ldm.mlx-server";
|
|
296
|
+
MLX_PLIST_PATH = join3(HOME3, "Library", "LaunchAgents", `${MLX_PLIST_LABEL}.plist`);
|
|
297
|
+
MLX_LOG_PATH = "/tmp/mlx-server.log";
|
|
298
|
+
MLX_CONFIG = {
|
|
299
|
+
port: MLX_PORT,
|
|
300
|
+
model: MLX_MODEL,
|
|
301
|
+
plistPath: MLX_PLIST_PATH,
|
|
302
|
+
logPath: MLX_LOG_PATH,
|
|
303
|
+
stateFile: MLX_STATE_FILE
|
|
304
|
+
};
|
|
305
|
+
}
|
|
306
|
+
});
|
|
12
307
|
|
|
13
308
|
// src/doctor.ts
|
|
14
|
-
import { existsSync, readFileSync, readdirSync } from "fs";
|
|
309
|
+
import { existsSync as existsSync5, readFileSync as readFileSync5, readdirSync as readdirSync2 } from "fs";
|
|
310
|
+
import { execSync as execSync4 } from "child_process";
|
|
311
|
+
import { join as join4 } from "path";
|
|
312
|
+
|
|
313
|
+
// src/role.ts
|
|
314
|
+
import { existsSync as existsSync2, readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "fs";
|
|
315
|
+
|
|
316
|
+
// src/ldm.ts
|
|
317
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync, copyFileSync, chmodSync, readdirSync } from "fs";
|
|
318
|
+
import { join, dirname } from "path";
|
|
15
319
|
import { execSync } from "child_process";
|
|
16
|
-
import {
|
|
320
|
+
import { fileURLToPath } from "url";
|
|
17
321
|
var HOME = process.env.HOME || "";
|
|
322
|
+
var LDM_ROOT = join(HOME, ".ldm");
|
|
323
|
+
function loadAgentConfig(id) {
|
|
324
|
+
const cfgPath = join(LDM_ROOT, "agents", id, "config.json");
|
|
325
|
+
try {
|
|
326
|
+
if (existsSync(cfgPath)) return JSON.parse(readFileSync(cfgPath, "utf-8"));
|
|
327
|
+
} catch {
|
|
328
|
+
}
|
|
329
|
+
return null;
|
|
330
|
+
}
|
|
331
|
+
function getAgentId(harnessHint) {
|
|
332
|
+
if (process.env.CRYSTAL_AGENT_ID) return process.env.CRYSTAL_AGENT_ID;
|
|
333
|
+
const agentsDir = join(LDM_ROOT, "agents");
|
|
334
|
+
if (existsSync(agentsDir)) {
|
|
335
|
+
try {
|
|
336
|
+
for (const d of readdirSync(agentsDir)) {
|
|
337
|
+
const cfg = loadAgentConfig(d);
|
|
338
|
+
if (!cfg || !cfg.agentId) continue;
|
|
339
|
+
if (!harnessHint) return cfg.agentId;
|
|
340
|
+
if (harnessHint === "claude-code" && cfg.harness === "claude-code-cli") return cfg.agentId;
|
|
341
|
+
if (harnessHint === "openclaw" && cfg.harness === "openclaw") return cfg.agentId;
|
|
342
|
+
}
|
|
343
|
+
} catch {
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
return harnessHint === "openclaw" ? "oc-lesa-mini" : "cc-mini";
|
|
347
|
+
}
|
|
348
|
+
function ldmPaths(agentId) {
|
|
349
|
+
const id = agentId || getAgentId();
|
|
350
|
+
const agentRoot = join(LDM_ROOT, "agents", id);
|
|
351
|
+
return {
|
|
352
|
+
root: LDM_ROOT,
|
|
353
|
+
bin: join(LDM_ROOT, "bin"),
|
|
354
|
+
secrets: join(LDM_ROOT, "secrets"),
|
|
355
|
+
state: join(LDM_ROOT, "state"),
|
|
356
|
+
config: join(LDM_ROOT, "config.json"),
|
|
357
|
+
crystalDb: join(LDM_ROOT, "memory", "crystal.db"),
|
|
358
|
+
crystalLance: join(LDM_ROOT, "memory", "lance"),
|
|
359
|
+
agentRoot,
|
|
360
|
+
transcripts: join(agentRoot, "memory", "transcripts"),
|
|
361
|
+
sessions: join(agentRoot, "memory", "sessions"),
|
|
362
|
+
daily: join(agentRoot, "memory", "daily"),
|
|
363
|
+
journals: join(agentRoot, "memory", "journals"),
|
|
364
|
+
workspace: join(agentRoot, "memory", "workspace")
|
|
365
|
+
};
|
|
366
|
+
}
|
|
367
|
+
var LEGACY_OC_DIR = join(HOME, ".openclaw");
|
|
368
|
+
function resolveStatePath(filename) {
|
|
369
|
+
const paths = ldmPaths();
|
|
370
|
+
const ldmPath = join(paths.state, filename);
|
|
371
|
+
if (existsSync(ldmPath)) return ldmPath;
|
|
372
|
+
const legacyPath = join(LEGACY_OC_DIR, "memory", filename);
|
|
373
|
+
if (existsSync(legacyPath)) return legacyPath;
|
|
374
|
+
return ldmPath;
|
|
375
|
+
}
|
|
376
|
+
function resolveSecretPath(filename) {
|
|
377
|
+
const paths = ldmPaths();
|
|
378
|
+
const ldmPath = join(paths.secrets, filename);
|
|
379
|
+
if (existsSync(ldmPath)) return ldmPath;
|
|
380
|
+
const legacyPath = join(LEGACY_OC_DIR, "secrets", filename);
|
|
381
|
+
if (existsSync(legacyPath)) return legacyPath;
|
|
382
|
+
return ldmPath;
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
// src/role.ts
|
|
386
|
+
var STATE_FILE = "crystal-role.json";
|
|
387
|
+
function loadRoleState() {
|
|
388
|
+
try {
|
|
389
|
+
const path = resolveStatePath(STATE_FILE);
|
|
390
|
+
if (existsSync2(path)) {
|
|
391
|
+
return JSON.parse(readFileSync2(path, "utf-8"));
|
|
392
|
+
}
|
|
393
|
+
} catch {
|
|
394
|
+
}
|
|
395
|
+
return null;
|
|
396
|
+
}
|
|
397
|
+
function hasLocalEmbeddingProvider() {
|
|
398
|
+
if (process.env.OPENAI_API_KEY) return true;
|
|
399
|
+
if (process.env.GOOGLE_API_KEY && process.env.CRYSTAL_EMBEDDING_PROVIDER === "google") return true;
|
|
400
|
+
if (process.env.CRYSTAL_EMBEDDING_PROVIDER === "ollama") return true;
|
|
401
|
+
return false;
|
|
402
|
+
}
|
|
403
|
+
function hasRelayKey() {
|
|
404
|
+
const keyPath = resolveSecretPath("crystal-relay-key");
|
|
405
|
+
return existsSync2(keyPath);
|
|
406
|
+
}
|
|
407
|
+
function detectRole() {
|
|
408
|
+
const agentId = getAgentId();
|
|
409
|
+
const paths = ldmPaths(agentId);
|
|
410
|
+
const relayUrl = process.env.CRYSTAL_RELAY_URL || null;
|
|
411
|
+
const relayToken = !!process.env.CRYSTAL_RELAY_TOKEN;
|
|
412
|
+
const relayKeyExists = hasRelayKey();
|
|
413
|
+
const localEmbeddings = hasLocalEmbeddingProvider();
|
|
414
|
+
const localDb = existsSync2(paths.crystalDb);
|
|
415
|
+
const state = loadRoleState();
|
|
416
|
+
if (state && state.override) {
|
|
417
|
+
return {
|
|
418
|
+
role: state.role,
|
|
419
|
+
source: "state-file",
|
|
420
|
+
relayUrl,
|
|
421
|
+
relayToken,
|
|
422
|
+
relayKeyExists,
|
|
423
|
+
agentId,
|
|
424
|
+
hasLocalEmbeddings: localEmbeddings,
|
|
425
|
+
hasLocalDb: localDb
|
|
426
|
+
};
|
|
427
|
+
}
|
|
428
|
+
let role = "core";
|
|
429
|
+
if ((relayUrl || relayKeyExists) && !localEmbeddings) {
|
|
430
|
+
role = "node";
|
|
431
|
+
} else if ((relayUrl || relayKeyExists) && localEmbeddings) {
|
|
432
|
+
role = "core";
|
|
433
|
+
}
|
|
434
|
+
return {
|
|
435
|
+
role,
|
|
436
|
+
source: "auto-detected",
|
|
437
|
+
relayUrl,
|
|
438
|
+
relayToken,
|
|
439
|
+
relayKeyExists,
|
|
440
|
+
agentId,
|
|
441
|
+
hasLocalEmbeddings: localEmbeddings,
|
|
442
|
+
hasLocalDb: localDb
|
|
443
|
+
};
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
// src/bridge.ts
|
|
447
|
+
import { existsSync as existsSync3, readFileSync as readFileSync3, writeFileSync as writeFileSync3 } from "fs";
|
|
448
|
+
import { execSync as execSync2 } from "child_process";
|
|
449
|
+
import { join as join2 } from "path";
|
|
450
|
+
var HOME2 = process.env.HOME || "";
|
|
451
|
+
function _checkLocalBridge() {
|
|
452
|
+
if (existsSync3(join2(HOME2, ".openclaw", "extensions", "lesa-bridge", "dist", "index.js"))) return true;
|
|
453
|
+
if (existsSync3(join2(HOME2, ".ldm", "extensions", "lesa-bridge", "dist", "index.js"))) return true;
|
|
454
|
+
return false;
|
|
455
|
+
}
|
|
456
|
+
function isBridgeInstalled() {
|
|
457
|
+
try {
|
|
458
|
+
execSync2("which lesa 2>/dev/null", { encoding: "utf-8" });
|
|
459
|
+
return true;
|
|
460
|
+
} catch {
|
|
461
|
+
return _checkLocalBridge();
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
function isBridgeRegistered() {
|
|
465
|
+
const mcpPath = join2(HOME2, ".claude", ".mcp.json");
|
|
466
|
+
try {
|
|
467
|
+
if (existsSync3(mcpPath)) {
|
|
468
|
+
const config = JSON.parse(readFileSync3(mcpPath, "utf-8"));
|
|
469
|
+
if (config.mcpServers && config.mcpServers["lesa-bridge"]) return true;
|
|
470
|
+
}
|
|
471
|
+
} catch {
|
|
472
|
+
}
|
|
473
|
+
try {
|
|
474
|
+
const r = execSync2("claude mcp get lesa-bridge 2>&1", { encoding: "utf-8", timeout: 5e3 });
|
|
475
|
+
if (!r.includes("not found") && !r.includes("error")) return true;
|
|
476
|
+
} catch {
|
|
477
|
+
}
|
|
478
|
+
return false;
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
// src/doctor.ts
|
|
482
|
+
var HOME4 = process.env.HOME || "";
|
|
18
483
|
async function runDoctor() {
|
|
19
484
|
const checks = [];
|
|
20
485
|
const role = detectRole();
|
|
@@ -36,8 +501,8 @@ async function runDoctor() {
|
|
|
36
501
|
checks.push(checkLdmDirectory(paths));
|
|
37
502
|
checks.push(checkPrivateMode());
|
|
38
503
|
try {
|
|
39
|
-
const { doctorCheck } = await
|
|
40
|
-
const mlx =
|
|
504
|
+
const { doctorCheck: doctorCheck2 } = await Promise.resolve().then(() => (init_mlx_setup(), mlx_setup_exports));
|
|
505
|
+
const mlx = doctorCheck2();
|
|
41
506
|
if (mlx.status !== "skip") {
|
|
42
507
|
checks.push({ name: "MLX LLM", status: mlx.status, detail: mlx.detail, fix: mlx.fix });
|
|
43
508
|
}
|
|
@@ -46,13 +511,13 @@ async function runDoctor() {
|
|
|
46
511
|
return checks;
|
|
47
512
|
}
|
|
48
513
|
function checkOpEmbeddings() {
|
|
49
|
-
const saTokenLdm =
|
|
50
|
-
const saTokenOc =
|
|
51
|
-
if (!
|
|
52
|
-
const saTokenPath =
|
|
514
|
+
const saTokenLdm = join4(HOME4, ".ldm", "secrets", "op-sa-token");
|
|
515
|
+
const saTokenOc = join4(HOME4, ".openclaw", "secrets", "op-sa-token");
|
|
516
|
+
if (!existsSync5(saTokenLdm) && !existsSync5(saTokenOc)) return null;
|
|
517
|
+
const saTokenPath = existsSync5(saTokenLdm) ? saTokenLdm : saTokenOc;
|
|
53
518
|
try {
|
|
54
|
-
const saToken =
|
|
55
|
-
const result =
|
|
519
|
+
const saToken = readFileSync5(saTokenPath, "utf-8").trim();
|
|
520
|
+
const result = execSync4('op read "op://Agent Secrets/OpenAI API/api key" 2>/dev/null', {
|
|
56
521
|
encoding: "utf-8",
|
|
57
522
|
env: { ...process.env, OP_SERVICE_ACCOUNT_TOKEN: saToken },
|
|
58
523
|
timeout: 1e4
|
|
@@ -71,12 +536,12 @@ function checkOpEmbeddings() {
|
|
|
71
536
|
return null;
|
|
72
537
|
}
|
|
73
538
|
function checkVersion() {
|
|
74
|
-
const ldmExtPkg =
|
|
75
|
-
const ocExtPkg =
|
|
539
|
+
const ldmExtPkg = join4(HOME4, ".ldm", "extensions", "memory-crystal", "package.json");
|
|
540
|
+
const ocExtPkg = join4(HOME4, ".openclaw", "extensions", "memory-crystal", "package.json");
|
|
76
541
|
let installedVersion = null;
|
|
77
542
|
try {
|
|
78
|
-
if (
|
|
79
|
-
const pkg = JSON.parse(
|
|
543
|
+
if (existsSync5(ldmExtPkg)) {
|
|
544
|
+
const pkg = JSON.parse(readFileSync5(ldmExtPkg, "utf-8"));
|
|
80
545
|
installedVersion = pkg.version;
|
|
81
546
|
}
|
|
82
547
|
} catch {
|
|
@@ -91,8 +556,8 @@ function checkVersion() {
|
|
|
91
556
|
}
|
|
92
557
|
let ocVersion = null;
|
|
93
558
|
try {
|
|
94
|
-
if (
|
|
95
|
-
const pkg = JSON.parse(
|
|
559
|
+
if (existsSync5(ocExtPkg)) {
|
|
560
|
+
const pkg = JSON.parse(readFileSync5(ocExtPkg, "utf-8"));
|
|
96
561
|
ocVersion = pkg.version;
|
|
97
562
|
}
|
|
98
563
|
} catch {
|
|
@@ -114,10 +579,10 @@ function checkVersion() {
|
|
|
114
579
|
};
|
|
115
580
|
}
|
|
116
581
|
function checkCCHook() {
|
|
117
|
-
const settingsPath =
|
|
582
|
+
const settingsPath = join4(HOME4, ".claude", "settings.json");
|
|
118
583
|
try {
|
|
119
|
-
if (
|
|
120
|
-
const settings = JSON.parse(
|
|
584
|
+
if (existsSync5(settingsPath)) {
|
|
585
|
+
const settings = JSON.parse(readFileSync5(settingsPath, "utf-8"));
|
|
121
586
|
const stopHooks = settings?.hooks?.Stop;
|
|
122
587
|
if (Array.isArray(stopHooks)) {
|
|
123
588
|
const found = stopHooks.some((entry) => {
|
|
@@ -140,7 +605,7 @@ function checkCCHook() {
|
|
|
140
605
|
};
|
|
141
606
|
}
|
|
142
607
|
async function checkDatabase(dbPath) {
|
|
143
|
-
if (!
|
|
608
|
+
if (!existsSync5(dbPath)) {
|
|
144
609
|
return {
|
|
145
610
|
name: "Database",
|
|
146
611
|
status: "fail",
|
|
@@ -196,7 +661,7 @@ function checkEmbeddingProvider(role) {
|
|
|
196
661
|
}
|
|
197
662
|
function checkCaptureCron() {
|
|
198
663
|
try {
|
|
199
|
-
const crontab =
|
|
664
|
+
const crontab = execSync4("crontab -l 2>/dev/null", { encoding: "utf-8" });
|
|
200
665
|
if (crontab.includes("crystal-capture")) {
|
|
201
666
|
return { name: "Capture", status: "ok", detail: "cron installed" };
|
|
202
667
|
}
|
|
@@ -232,26 +697,26 @@ function checkRelayConfig(role) {
|
|
|
232
697
|
}
|
|
233
698
|
function checkMcpServer() {
|
|
234
699
|
const candidates = [
|
|
235
|
-
|
|
700
|
+
join4(HOME4, ".claude", ".mcp.json"),
|
|
236
701
|
// user-level (legacy)
|
|
237
|
-
|
|
702
|
+
join4(HOME4, ".openclaw", ".mcp.json"),
|
|
238
703
|
// OpenClaw project-level
|
|
239
|
-
|
|
704
|
+
join4(process.cwd(), ".mcp.json")
|
|
240
705
|
// current project
|
|
241
706
|
];
|
|
242
707
|
for (const mcpPath of candidates) {
|
|
243
708
|
try {
|
|
244
|
-
if (
|
|
245
|
-
const config = JSON.parse(
|
|
709
|
+
if (existsSync5(mcpPath)) {
|
|
710
|
+
const config = JSON.parse(readFileSync5(mcpPath, "utf-8"));
|
|
246
711
|
if (config.mcpServers && config.mcpServers["memory-crystal"]) {
|
|
247
|
-
return { name: "MCP Server", status: "ok", detail: `memory-crystal registered (${mcpPath.replace(
|
|
712
|
+
return { name: "MCP Server", status: "ok", detail: `memory-crystal registered (${mcpPath.replace(HOME4, "~")})` };
|
|
248
713
|
}
|
|
249
714
|
}
|
|
250
715
|
} catch {
|
|
251
716
|
}
|
|
252
717
|
}
|
|
253
718
|
try {
|
|
254
|
-
const result =
|
|
719
|
+
const result = execSync4("claude mcp get memory-crystal 2>&1", { encoding: "utf-8", timeout: 5e3 });
|
|
255
720
|
if (!result.includes("not found") && !result.includes("error")) {
|
|
256
721
|
return { name: "MCP Server", status: "ok", detail: "memory-crystal registered (user scope)" };
|
|
257
722
|
}
|
|
@@ -265,21 +730,21 @@ function checkMcpServer() {
|
|
|
265
730
|
};
|
|
266
731
|
}
|
|
267
732
|
function checkBackup() {
|
|
268
|
-
const plistPath =
|
|
269
|
-
if (
|
|
733
|
+
const plistPath = join4(HOME4, "Library", "LaunchAgents", "ai.openclaw.ldm-backup.plist");
|
|
734
|
+
if (existsSync5(plistPath)) {
|
|
270
735
|
return { name: "Backup", status: "ok", detail: "LaunchAgent installed" };
|
|
271
736
|
}
|
|
272
737
|
try {
|
|
273
|
-
const crontab =
|
|
738
|
+
const crontab = execSync4("crontab -l 2>/dev/null", { encoding: "utf-8" });
|
|
274
739
|
if (crontab.includes("ldm-backup") || crontab.includes("LDMDevTools") && crontab.includes("backup")) {
|
|
275
740
|
return { name: "Backup", status: "ok", detail: "cron installed" };
|
|
276
741
|
}
|
|
277
742
|
} catch {
|
|
278
743
|
}
|
|
279
|
-
const backupsDir =
|
|
280
|
-
if (
|
|
744
|
+
const backupsDir = join4(HOME4, ".ldm", "backups");
|
|
745
|
+
if (existsSync5(backupsDir)) {
|
|
281
746
|
try {
|
|
282
|
-
const entries =
|
|
747
|
+
const entries = readdirSync2(backupsDir).filter((e) => !e.startsWith("."));
|
|
283
748
|
if (entries.length > 0) return { name: "Backup", status: "ok", detail: `${entries.length} backup(s) in ~/.ldm/backups/` };
|
|
284
749
|
} catch {
|
|
285
750
|
}
|
|
@@ -309,11 +774,11 @@ function checkBridge() {
|
|
|
309
774
|
}
|
|
310
775
|
function checkLdmDirectory(paths) {
|
|
311
776
|
const missing = [];
|
|
312
|
-
if (!
|
|
313
|
-
if (!
|
|
314
|
-
if (!
|
|
315
|
-
if (!
|
|
316
|
-
if (!
|
|
777
|
+
if (!existsSync5(paths.root)) missing.push("~/.ldm");
|
|
778
|
+
if (!existsSync5(join4(paths.root, "memory"))) missing.push("memory/");
|
|
779
|
+
if (!existsSync5(paths.state)) missing.push("state/");
|
|
780
|
+
if (!existsSync5(paths.bin)) missing.push("bin/");
|
|
781
|
+
if (!existsSync5(paths.transcripts)) missing.push("transcripts/");
|
|
317
782
|
if (missing.length === 0) {
|
|
318
783
|
return { name: "LDM Directory", status: "ok", detail: "intact" };
|
|
319
784
|
}
|
|
@@ -327,8 +792,8 @@ function checkLdmDirectory(paths) {
|
|
|
327
792
|
function checkPrivateMode() {
|
|
328
793
|
const statePath = resolveStatePath("memory-capture-state.json");
|
|
329
794
|
try {
|
|
330
|
-
if (
|
|
331
|
-
const state = JSON.parse(
|
|
795
|
+
if (existsSync5(statePath)) {
|
|
796
|
+
const state = JSON.parse(readFileSync5(statePath, "utf-8"));
|
|
332
797
|
if (state.enabled === false) {
|
|
333
798
|
return { name: "Private Mode", status: "warn", detail: "capture disabled (private mode ON)" };
|
|
334
799
|
}
|