@wipcomputer/wip-ldm-os 0.2.14 → 0.3.2
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/bin/ldm.js +339 -0
- package/dist/bridge/chunk-KWGJCDGS.js +424 -0
- package/dist/bridge/cli.d.ts +1 -0
- package/dist/bridge/cli.js +215 -0
- package/dist/bridge/core.d.ts +74 -0
- package/dist/bridge/core.js +40 -0
- package/dist/bridge/mcp-server.d.ts +2 -0
- package/dist/bridge/mcp-server.js +284 -0
- package/docs/TECHNICAL.md +290 -0
- package/docs/acp-compatibility.md +30 -0
- package/docs/optional-skills.md +77 -0
- package/docs/recall.md +29 -0
- package/docs/shared-workspace.md +37 -0
- package/docs/system-pulse.md +26 -0
- package/docs/universal-installer.md +84 -0
- package/lib/messages.mjs +195 -0
- package/lib/sessions.mjs +145 -0
- package/lib/updates.mjs +173 -0
- package/package.json +9 -2
- package/src/boot/boot-hook.mjs +36 -1
- package/src/bridge/cli.ts +245 -0
- package/src/bridge/core.ts +622 -0
- package/src/bridge/mcp-server.ts +371 -0
- package/src/bridge/package.json +18 -0
- package/src/bridge/tsconfig.json +19 -0
- package/src/cron/update-check.mjs +28 -0
- package/src/hooks/stop-hook.mjs +24 -0
|
@@ -0,0 +1,424 @@
|
|
|
1
|
+
// core.ts
|
|
2
|
+
import { execSync, exec } from "child_process";
|
|
3
|
+
import { readdirSync, readFileSync, existsSync, statSync } from "fs";
|
|
4
|
+
import { join, relative, resolve } from "path";
|
|
5
|
+
import { promisify } from "util";
|
|
6
|
+
var execAsync = promisify(exec);
|
|
7
|
+
var HOME = process.env.HOME || "/Users/lesa";
|
|
8
|
+
var LDM_ROOT = process.env.LDM_ROOT || join(HOME, ".ldm");
|
|
9
|
+
function resolveConfig(overrides) {
|
|
10
|
+
const openclawDir = overrides?.openclawDir || process.env.OPENCLAW_DIR || join(process.env.HOME || "~", ".openclaw");
|
|
11
|
+
return {
|
|
12
|
+
openclawDir,
|
|
13
|
+
workspaceDir: overrides?.workspaceDir || join(openclawDir, "workspace"),
|
|
14
|
+
dbPath: overrides?.dbPath || join(openclawDir, "memory", "context-embeddings.sqlite"),
|
|
15
|
+
inboxPort: overrides?.inboxPort || parseInt(process.env.LESA_BRIDGE_INBOX_PORT || "18790", 10),
|
|
16
|
+
embeddingModel: overrides?.embeddingModel || "text-embedding-3-small",
|
|
17
|
+
embeddingDimensions: overrides?.embeddingDimensions || 1536
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
function resolveConfigMulti(overrides) {
|
|
21
|
+
const ldmConfig = join(LDM_ROOT, "config.json");
|
|
22
|
+
if (existsSync(ldmConfig)) {
|
|
23
|
+
try {
|
|
24
|
+
const raw = JSON.parse(readFileSync(ldmConfig, "utf-8"));
|
|
25
|
+
const openclawDir = raw.openclawDir || process.env.OPENCLAW_DIR || join(HOME, ".openclaw");
|
|
26
|
+
return {
|
|
27
|
+
openclawDir,
|
|
28
|
+
workspaceDir: raw.workspaceDir || overrides?.workspaceDir || join(openclawDir, "workspace"),
|
|
29
|
+
dbPath: raw.dbPath || overrides?.dbPath || join(openclawDir, "memory", "context-embeddings.sqlite"),
|
|
30
|
+
inboxPort: raw.inboxPort || overrides?.inboxPort || parseInt(process.env.LESA_BRIDGE_INBOX_PORT || "18790", 10),
|
|
31
|
+
embeddingModel: raw.embeddingModel || overrides?.embeddingModel || "text-embedding-3-small",
|
|
32
|
+
embeddingDimensions: raw.embeddingDimensions || overrides?.embeddingDimensions || 1536
|
|
33
|
+
};
|
|
34
|
+
} catch {
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
return resolveConfig(overrides);
|
|
38
|
+
}
|
|
39
|
+
var cachedApiKey = void 0;
|
|
40
|
+
function resolveApiKey(openclawDir) {
|
|
41
|
+
if (cachedApiKey !== void 0) return cachedApiKey;
|
|
42
|
+
if (process.env.OPENAI_API_KEY) {
|
|
43
|
+
cachedApiKey = process.env.OPENAI_API_KEY;
|
|
44
|
+
return cachedApiKey;
|
|
45
|
+
}
|
|
46
|
+
const tokenPath = join(openclawDir, "secrets", "op-sa-token");
|
|
47
|
+
if (existsSync(tokenPath)) {
|
|
48
|
+
try {
|
|
49
|
+
const saToken = readFileSync(tokenPath, "utf-8").trim();
|
|
50
|
+
const key = execSync(
|
|
51
|
+
`op read "op://Agent Secrets/OpenAI API/api key" 2>/dev/null`,
|
|
52
|
+
{
|
|
53
|
+
env: { ...process.env, OP_SERVICE_ACCOUNT_TOKEN: saToken },
|
|
54
|
+
timeout: 1e4,
|
|
55
|
+
encoding: "utf-8"
|
|
56
|
+
}
|
|
57
|
+
).trim();
|
|
58
|
+
if (key && key.startsWith("sk-")) {
|
|
59
|
+
cachedApiKey = key;
|
|
60
|
+
return cachedApiKey;
|
|
61
|
+
}
|
|
62
|
+
} catch {
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
cachedApiKey = null;
|
|
66
|
+
return null;
|
|
67
|
+
}
|
|
68
|
+
var cachedGatewayConfig = null;
|
|
69
|
+
function resolveGatewayConfig(openclawDir) {
|
|
70
|
+
if (cachedGatewayConfig) return cachedGatewayConfig;
|
|
71
|
+
const configPath = join(openclawDir, "openclaw.json");
|
|
72
|
+
if (!existsSync(configPath)) {
|
|
73
|
+
throw new Error(`OpenClaw config not found: ${configPath}`);
|
|
74
|
+
}
|
|
75
|
+
const config = JSON.parse(readFileSync(configPath, "utf-8"));
|
|
76
|
+
const token = config?.gateway?.auth?.token;
|
|
77
|
+
const port = config?.gateway?.port || 18789;
|
|
78
|
+
if (!token) {
|
|
79
|
+
throw new Error("No gateway.auth.token found in openclaw.json");
|
|
80
|
+
}
|
|
81
|
+
cachedGatewayConfig = { token, port };
|
|
82
|
+
return cachedGatewayConfig;
|
|
83
|
+
}
|
|
84
|
+
var inboxQueue = [];
|
|
85
|
+
function pushInbox(msg) {
|
|
86
|
+
inboxQueue.push(msg);
|
|
87
|
+
return inboxQueue.length;
|
|
88
|
+
}
|
|
89
|
+
function drainInbox() {
|
|
90
|
+
const messages = [...inboxQueue];
|
|
91
|
+
inboxQueue.length = 0;
|
|
92
|
+
return messages;
|
|
93
|
+
}
|
|
94
|
+
function inboxCount() {
|
|
95
|
+
return inboxQueue.length;
|
|
96
|
+
}
|
|
97
|
+
async function sendMessage(openclawDir, message, options) {
|
|
98
|
+
const { token, port } = resolveGatewayConfig(openclawDir);
|
|
99
|
+
const agentId = options?.agentId || "main";
|
|
100
|
+
const user = options?.user || "claude-code";
|
|
101
|
+
const senderLabel = options?.senderLabel || "Claude Code";
|
|
102
|
+
const response = await fetch(`http://127.0.0.1:${port}/v1/chat/completions`, {
|
|
103
|
+
method: "POST",
|
|
104
|
+
headers: {
|
|
105
|
+
Authorization: `Bearer ${token}`,
|
|
106
|
+
"Content-Type": "application/json"
|
|
107
|
+
},
|
|
108
|
+
body: JSON.stringify({
|
|
109
|
+
model: agentId,
|
|
110
|
+
user,
|
|
111
|
+
messages: [
|
|
112
|
+
{
|
|
113
|
+
role: "user",
|
|
114
|
+
content: `[${senderLabel}]: ${message}`
|
|
115
|
+
}
|
|
116
|
+
]
|
|
117
|
+
})
|
|
118
|
+
});
|
|
119
|
+
if (!response.ok) {
|
|
120
|
+
const body = await response.text();
|
|
121
|
+
throw new Error(`Gateway returned ${response.status}: ${body}`);
|
|
122
|
+
}
|
|
123
|
+
const data = await response.json();
|
|
124
|
+
const reply = data.choices?.[0]?.message?.content;
|
|
125
|
+
if (!reply) {
|
|
126
|
+
throw new Error("No response content from gateway");
|
|
127
|
+
}
|
|
128
|
+
return reply;
|
|
129
|
+
}
|
|
130
|
+
async function getQueryEmbedding(text, apiKey, model = "text-embedding-3-small", dimensions = 1536) {
|
|
131
|
+
const response = await fetch("https://api.openai.com/v1/embeddings", {
|
|
132
|
+
method: "POST",
|
|
133
|
+
headers: {
|
|
134
|
+
Authorization: `Bearer ${apiKey}`,
|
|
135
|
+
"Content-Type": "application/json"
|
|
136
|
+
},
|
|
137
|
+
body: JSON.stringify({
|
|
138
|
+
input: [text],
|
|
139
|
+
model,
|
|
140
|
+
dimensions
|
|
141
|
+
})
|
|
142
|
+
});
|
|
143
|
+
if (!response.ok) {
|
|
144
|
+
const body = await response.text();
|
|
145
|
+
throw new Error(`OpenAI embeddings failed (${response.status}): ${body}`);
|
|
146
|
+
}
|
|
147
|
+
const data = await response.json();
|
|
148
|
+
return data.data[0].embedding;
|
|
149
|
+
}
|
|
150
|
+
function blobToEmbedding(blob) {
|
|
151
|
+
const floats = [];
|
|
152
|
+
for (let i = 0; i < blob.length; i += 4) {
|
|
153
|
+
floats.push(blob.readFloatLE(i));
|
|
154
|
+
}
|
|
155
|
+
return floats;
|
|
156
|
+
}
|
|
157
|
+
function cosineSimilarity(a, b) {
|
|
158
|
+
let dot = 0;
|
|
159
|
+
let normA = 0;
|
|
160
|
+
let normB = 0;
|
|
161
|
+
for (let i = 0; i < a.length; i++) {
|
|
162
|
+
dot += a[i] * b[i];
|
|
163
|
+
normA += a[i] * a[i];
|
|
164
|
+
normB += b[i] * b[i];
|
|
165
|
+
}
|
|
166
|
+
const denom = Math.sqrt(normA) * Math.sqrt(normB);
|
|
167
|
+
return denom === 0 ? 0 : dot / denom;
|
|
168
|
+
}
|
|
169
|
+
function recencyWeight(ageDays) {
|
|
170
|
+
return Math.max(0.5, 1 - ageDays * 0.01);
|
|
171
|
+
}
|
|
172
|
+
function freshnessLabel(ageDays) {
|
|
173
|
+
if (ageDays < 3) return "fresh";
|
|
174
|
+
if (ageDays < 7) return "recent";
|
|
175
|
+
if (ageDays < 14) return "aging";
|
|
176
|
+
return "stale";
|
|
177
|
+
}
|
|
178
|
+
async function searchConversations(config, query, limit = 5) {
|
|
179
|
+
const Database = (await import("better-sqlite3")).default;
|
|
180
|
+
if (!existsSync(config.dbPath)) {
|
|
181
|
+
throw new Error(`Database not found: ${config.dbPath}`);
|
|
182
|
+
}
|
|
183
|
+
const db = new Database(config.dbPath, { readonly: true });
|
|
184
|
+
db.pragma("journal_mode = WAL");
|
|
185
|
+
try {
|
|
186
|
+
const apiKey = resolveApiKey(config.openclawDir);
|
|
187
|
+
if (apiKey) {
|
|
188
|
+
const queryEmbedding = await getQueryEmbedding(
|
|
189
|
+
query,
|
|
190
|
+
apiKey,
|
|
191
|
+
config.embeddingModel,
|
|
192
|
+
config.embeddingDimensions
|
|
193
|
+
);
|
|
194
|
+
const rows = db.prepare(
|
|
195
|
+
`SELECT chunk_text, role, session_key, timestamp, embedding
|
|
196
|
+
FROM conversation_chunks
|
|
197
|
+
WHERE embedding IS NOT NULL
|
|
198
|
+
ORDER BY timestamp DESC
|
|
199
|
+
LIMIT 1000`
|
|
200
|
+
).all();
|
|
201
|
+
const now = Date.now();
|
|
202
|
+
return rows.map((row) => {
|
|
203
|
+
const cosine = cosineSimilarity(queryEmbedding, blobToEmbedding(row.embedding));
|
|
204
|
+
const ageDays = (now - row.timestamp) / (1e3 * 60 * 60 * 24);
|
|
205
|
+
const weight = recencyWeight(ageDays);
|
|
206
|
+
return {
|
|
207
|
+
text: row.chunk_text,
|
|
208
|
+
role: row.role,
|
|
209
|
+
sessionKey: row.session_key,
|
|
210
|
+
date: new Date(row.timestamp).toISOString().split("T")[0],
|
|
211
|
+
similarity: cosine * weight,
|
|
212
|
+
recencyScore: weight,
|
|
213
|
+
freshness: freshnessLabel(ageDays)
|
|
214
|
+
};
|
|
215
|
+
}).sort((a, b) => (b.similarity || 0) - (a.similarity || 0)).slice(0, limit);
|
|
216
|
+
} else {
|
|
217
|
+
const rows = db.prepare(
|
|
218
|
+
`SELECT chunk_text, role, session_key, timestamp
|
|
219
|
+
FROM conversation_chunks
|
|
220
|
+
WHERE chunk_text LIKE ?
|
|
221
|
+
ORDER BY timestamp DESC
|
|
222
|
+
LIMIT ?`
|
|
223
|
+
).all(`%${query}%`, limit);
|
|
224
|
+
return rows.map((row) => ({
|
|
225
|
+
text: row.chunk_text,
|
|
226
|
+
role: row.role,
|
|
227
|
+
sessionKey: row.session_key,
|
|
228
|
+
date: new Date(row.timestamp).toISOString().split("T")[0]
|
|
229
|
+
}));
|
|
230
|
+
}
|
|
231
|
+
} finally {
|
|
232
|
+
db.close();
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
function findMarkdownFiles(dir, maxDepth = 4, depth = 0) {
|
|
236
|
+
if (depth > maxDepth || !existsSync(dir)) return [];
|
|
237
|
+
const files = [];
|
|
238
|
+
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
239
|
+
if (entry.name.startsWith(".") || entry.name === "node_modules") continue;
|
|
240
|
+
const fullPath = join(dir, entry.name);
|
|
241
|
+
if (entry.isDirectory()) {
|
|
242
|
+
files.push(...findMarkdownFiles(fullPath, maxDepth, depth + 1));
|
|
243
|
+
} else if (entry.name.endsWith(".md")) {
|
|
244
|
+
files.push(fullPath);
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
return files;
|
|
248
|
+
}
|
|
249
|
+
function searchWorkspace(workspaceDir, query) {
|
|
250
|
+
const files = findMarkdownFiles(workspaceDir);
|
|
251
|
+
const queryLower = query.toLowerCase();
|
|
252
|
+
const words = queryLower.split(/\s+/).filter((w) => w.length > 2);
|
|
253
|
+
const results = [];
|
|
254
|
+
for (const filePath of files) {
|
|
255
|
+
try {
|
|
256
|
+
const content = readFileSync(filePath, "utf-8");
|
|
257
|
+
const contentLower = content.toLowerCase();
|
|
258
|
+
let score = 0;
|
|
259
|
+
for (const word of words) {
|
|
260
|
+
if (contentLower.indexOf(word) !== -1) score++;
|
|
261
|
+
}
|
|
262
|
+
if (score === 0) continue;
|
|
263
|
+
const lines = content.split("\n");
|
|
264
|
+
const excerpts = [];
|
|
265
|
+
for (let i = 0; i < lines.length && excerpts.length < 5; i++) {
|
|
266
|
+
const lineLower = lines[i].toLowerCase();
|
|
267
|
+
if (words.some((w) => lineLower.includes(w))) {
|
|
268
|
+
const start = Math.max(0, i - 1);
|
|
269
|
+
const end = Math.min(lines.length, i + 2);
|
|
270
|
+
excerpts.push(lines.slice(start, end).join("\n"));
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
results.push({ path: relative(workspaceDir, filePath), excerpts, score });
|
|
274
|
+
} catch {
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
return results.sort((a, b) => b.score - a.score).slice(0, 10);
|
|
278
|
+
}
|
|
279
|
+
function parseSkillFrontmatter(content) {
|
|
280
|
+
const match = content.match(/^---\n([\s\S]*?)\n---/);
|
|
281
|
+
if (!match) return {};
|
|
282
|
+
const yaml = match[1];
|
|
283
|
+
const name = yaml.match(/^name:\s*(.+)$/m)?.[1]?.trim();
|
|
284
|
+
const description = yaml.match(/^description:\s*(.+)$/m)?.[1]?.trim();
|
|
285
|
+
let emoji;
|
|
286
|
+
const emojiMatch = yaml.match(/"emoji":\s*"([^"]+)"/);
|
|
287
|
+
if (emojiMatch) emoji = emojiMatch[1];
|
|
288
|
+
let requires;
|
|
289
|
+
const requiresMatch = yaml.match(/"requires":\s*\{([^}]+)\}/);
|
|
290
|
+
if (requiresMatch) {
|
|
291
|
+
requires = {};
|
|
292
|
+
const pairs = requiresMatch[1].matchAll(/"(\w+)":\s*\[([^\]]*)\]/g);
|
|
293
|
+
for (const pair of pairs) {
|
|
294
|
+
const values = pair[2].match(/"([^"]+)"/g)?.map((v) => v.replace(/"/g, "")) || [];
|
|
295
|
+
requires[pair[1]] = values;
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
return { name, description, emoji, requires };
|
|
299
|
+
}
|
|
300
|
+
function discoverSkills(openclawDir) {
|
|
301
|
+
const skills = [];
|
|
302
|
+
const seen = /* @__PURE__ */ new Set();
|
|
303
|
+
const extensionsDir = join(openclawDir, "extensions");
|
|
304
|
+
if (!existsSync(extensionsDir)) return skills;
|
|
305
|
+
for (const ext of readdirSync(extensionsDir, { withFileTypes: true })) {
|
|
306
|
+
if (!ext.isDirectory() || ext.name.startsWith(".")) continue;
|
|
307
|
+
const extDir = join(extensionsDir, ext.name);
|
|
308
|
+
const searchDirs = [
|
|
309
|
+
{ dir: join(extDir, "node_modules", "openclaw", "skills"), source: "builtin" },
|
|
310
|
+
{ dir: join(extDir, "skills"), source: "custom" }
|
|
311
|
+
];
|
|
312
|
+
for (const { dir, source } of searchDirs) {
|
|
313
|
+
if (!existsSync(dir)) continue;
|
|
314
|
+
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
315
|
+
if (!entry.isDirectory() || entry.name.startsWith(".")) continue;
|
|
316
|
+
const skillDir = join(dir, entry.name);
|
|
317
|
+
const skillMd = join(skillDir, "SKILL.md");
|
|
318
|
+
if (!existsSync(skillMd)) continue;
|
|
319
|
+
if (seen.has(entry.name)) continue;
|
|
320
|
+
seen.add(entry.name);
|
|
321
|
+
try {
|
|
322
|
+
const content = readFileSync(skillMd, "utf-8");
|
|
323
|
+
const frontmatter = parseSkillFrontmatter(content);
|
|
324
|
+
const scriptsDir = join(skillDir, "scripts");
|
|
325
|
+
let scripts = [];
|
|
326
|
+
let hasScripts = false;
|
|
327
|
+
if (existsSync(scriptsDir) && statSync(scriptsDir).isDirectory()) {
|
|
328
|
+
scripts = readdirSync(scriptsDir).filter(
|
|
329
|
+
(f) => f.endsWith(".sh") || f.endsWith(".py")
|
|
330
|
+
);
|
|
331
|
+
hasScripts = scripts.length > 0;
|
|
332
|
+
}
|
|
333
|
+
skills.push({
|
|
334
|
+
name: frontmatter.name || entry.name,
|
|
335
|
+
description: frontmatter.description || `OpenClaw skill: ${entry.name}`,
|
|
336
|
+
skillDir,
|
|
337
|
+
hasScripts,
|
|
338
|
+
scripts,
|
|
339
|
+
source,
|
|
340
|
+
emoji: frontmatter.emoji,
|
|
341
|
+
requires: frontmatter.requires
|
|
342
|
+
});
|
|
343
|
+
} catch {
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
return skills.sort((a, b) => a.name.localeCompare(b.name));
|
|
349
|
+
}
|
|
350
|
+
async function executeSkillScript(skillDir, scripts, scriptName, args) {
|
|
351
|
+
const scriptsDir = join(skillDir, "scripts");
|
|
352
|
+
let script;
|
|
353
|
+
if (scriptName) {
|
|
354
|
+
if (!scripts.includes(scriptName)) {
|
|
355
|
+
throw new Error(`Script "${scriptName}" not found. Available: ${scripts.join(", ")}`);
|
|
356
|
+
}
|
|
357
|
+
script = scriptName;
|
|
358
|
+
} else if (scripts.length === 1) {
|
|
359
|
+
script = scripts[0];
|
|
360
|
+
} else {
|
|
361
|
+
const sh = scripts.find((s) => s.endsWith(".sh"));
|
|
362
|
+
script = sh || scripts[0];
|
|
363
|
+
}
|
|
364
|
+
const scriptPath = join(scriptsDir, script);
|
|
365
|
+
const interpreter = script.endsWith(".py") ? "python3" : "bash";
|
|
366
|
+
try {
|
|
367
|
+
const { stdout, stderr } = await execAsync(
|
|
368
|
+
`${interpreter} "${scriptPath}" ${args}`,
|
|
369
|
+
{
|
|
370
|
+
env: { ...process.env },
|
|
371
|
+
timeout: 12e4,
|
|
372
|
+
maxBuffer: 10 * 1024 * 1024
|
|
373
|
+
// 10MB
|
|
374
|
+
}
|
|
375
|
+
);
|
|
376
|
+
return stdout || stderr || "(no output)";
|
|
377
|
+
} catch (err) {
|
|
378
|
+
const output = err.stdout || err.stderr || err.message;
|
|
379
|
+
throw new Error(`Script failed (exit ${err.code || "?"}): ${output}`);
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
function readWorkspaceFile(workspaceDir, filePath) {
|
|
383
|
+
const resolved = resolve(workspaceDir, filePath);
|
|
384
|
+
if (!resolved.startsWith(resolve(workspaceDir))) {
|
|
385
|
+
throw new Error("Path must be within workspace/");
|
|
386
|
+
}
|
|
387
|
+
if (!existsSync(resolved)) {
|
|
388
|
+
const dir = resolved.endsWith(".md") ? join(resolved, "..") : resolved;
|
|
389
|
+
if (existsSync(dir) && statSync(dir).isDirectory()) {
|
|
390
|
+
const files = findMarkdownFiles(dir, 1);
|
|
391
|
+
const listing = files.map((f) => relative(workspaceDir, f)).join("\n");
|
|
392
|
+
throw new Error(`File not found: ${filePath}
|
|
393
|
+
|
|
394
|
+
Available files:
|
|
395
|
+
${listing}`);
|
|
396
|
+
}
|
|
397
|
+
throw new Error(`File not found: ${filePath}`);
|
|
398
|
+
}
|
|
399
|
+
return {
|
|
400
|
+
content: readFileSync(resolved, "utf-8"),
|
|
401
|
+
relativePath: relative(workspaceDir, resolved)
|
|
402
|
+
};
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
export {
|
|
406
|
+
LDM_ROOT,
|
|
407
|
+
resolveConfig,
|
|
408
|
+
resolveConfigMulti,
|
|
409
|
+
resolveApiKey,
|
|
410
|
+
resolveGatewayConfig,
|
|
411
|
+
pushInbox,
|
|
412
|
+
drainInbox,
|
|
413
|
+
inboxCount,
|
|
414
|
+
sendMessage,
|
|
415
|
+
getQueryEmbedding,
|
|
416
|
+
blobToEmbedding,
|
|
417
|
+
cosineSimilarity,
|
|
418
|
+
searchConversations,
|
|
419
|
+
findMarkdownFiles,
|
|
420
|
+
searchWorkspace,
|
|
421
|
+
discoverSkills,
|
|
422
|
+
executeSkillScript,
|
|
423
|
+
readWorkspaceFile
|
|
424
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
discoverSkills,
|
|
4
|
+
readWorkspaceFile,
|
|
5
|
+
resolveApiKey,
|
|
6
|
+
resolveConfig,
|
|
7
|
+
resolveGatewayConfig,
|
|
8
|
+
searchConversations,
|
|
9
|
+
searchWorkspace,
|
|
10
|
+
sendMessage
|
|
11
|
+
} from "./chunk-KWGJCDGS.js";
|
|
12
|
+
|
|
13
|
+
// cli.ts
|
|
14
|
+
import { existsSync, statSync } from "fs";
|
|
15
|
+
var config = resolveConfig();
|
|
16
|
+
function usage() {
|
|
17
|
+
console.log(`lesa-bridge: Claude Code CLI \u2194 OpenClaw TUI agent bridge
|
|
18
|
+
|
|
19
|
+
Usage:
|
|
20
|
+
lesa send <message> Send a message to the OpenClaw agent
|
|
21
|
+
lesa search <query> Semantic search over conversation history
|
|
22
|
+
lesa memory <query> Keyword search across workspace files
|
|
23
|
+
lesa read <path> Read a workspace file (relative to workspace/)
|
|
24
|
+
lesa status Show bridge configuration
|
|
25
|
+
lesa diagnose Check gateway, inbox, DB, skills health
|
|
26
|
+
lesa help Show this help
|
|
27
|
+
|
|
28
|
+
Examples:
|
|
29
|
+
lesa send "What are you working on?"
|
|
30
|
+
lesa search "API key resolution"
|
|
31
|
+
lesa memory "compaction"
|
|
32
|
+
lesa read MEMORY.md
|
|
33
|
+
lesa read memory/2026-02-10.md`);
|
|
34
|
+
}
|
|
35
|
+
async function main() {
|
|
36
|
+
const args = process.argv.slice(2);
|
|
37
|
+
const command = args[0];
|
|
38
|
+
if (!command || command === "help" || command === "--help" || command === "-h") {
|
|
39
|
+
usage();
|
|
40
|
+
process.exit(0);
|
|
41
|
+
}
|
|
42
|
+
const arg = args.slice(1).join(" ");
|
|
43
|
+
switch (command) {
|
|
44
|
+
case "send": {
|
|
45
|
+
if (!arg) {
|
|
46
|
+
console.error("Error: message required. Usage: lesa send <message>");
|
|
47
|
+
process.exit(1);
|
|
48
|
+
}
|
|
49
|
+
try {
|
|
50
|
+
const reply = await sendMessage(config.openclawDir, arg);
|
|
51
|
+
console.log(reply);
|
|
52
|
+
} catch (err) {
|
|
53
|
+
console.error(`Error: ${err.message}`);
|
|
54
|
+
process.exit(1);
|
|
55
|
+
}
|
|
56
|
+
break;
|
|
57
|
+
}
|
|
58
|
+
case "search": {
|
|
59
|
+
if (!arg) {
|
|
60
|
+
console.error("Error: query required. Usage: lesa search <query>");
|
|
61
|
+
process.exit(1);
|
|
62
|
+
}
|
|
63
|
+
try {
|
|
64
|
+
const results = await searchConversations(config, arg);
|
|
65
|
+
if (results.length === 0) {
|
|
66
|
+
console.log("No results found.");
|
|
67
|
+
} else {
|
|
68
|
+
const icon = { fresh: "\u{1F7E2}", recent: "\u{1F7E1}", aging: "\u{1F7E0}", stale: "\u{1F534}" };
|
|
69
|
+
for (const [i, r] of results.entries()) {
|
|
70
|
+
const sim = r.similarity !== void 0 ? ` (${(r.similarity * 100).toFixed(1)}%)` : "";
|
|
71
|
+
const fresh = r.freshness ? ` ${icon[r.freshness]} ${r.freshness}` : "";
|
|
72
|
+
console.log(`[${i + 1}]${sim}${fresh} ${r.sessionKey} ${r.date}`);
|
|
73
|
+
console.log(r.text);
|
|
74
|
+
if (i < results.length - 1) console.log("\n---\n");
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
} catch (err) {
|
|
78
|
+
console.error(`Error: ${err.message}`);
|
|
79
|
+
process.exit(1);
|
|
80
|
+
}
|
|
81
|
+
break;
|
|
82
|
+
}
|
|
83
|
+
case "memory": {
|
|
84
|
+
if (!arg) {
|
|
85
|
+
console.error("Error: query required. Usage: lesa memory <query>");
|
|
86
|
+
process.exit(1);
|
|
87
|
+
}
|
|
88
|
+
try {
|
|
89
|
+
const results = searchWorkspace(config.workspaceDir, arg);
|
|
90
|
+
if (results.length === 0) {
|
|
91
|
+
console.log(`No workspace files matched "${arg}".`);
|
|
92
|
+
} else {
|
|
93
|
+
for (const r of results) {
|
|
94
|
+
console.log(`### ${r.path}`);
|
|
95
|
+
for (const excerpt of r.excerpts) {
|
|
96
|
+
console.log(` ${excerpt.replace(/\n/g, "\n ")}`);
|
|
97
|
+
}
|
|
98
|
+
console.log();
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
} catch (err) {
|
|
102
|
+
console.error(`Error: ${err.message}`);
|
|
103
|
+
process.exit(1);
|
|
104
|
+
}
|
|
105
|
+
break;
|
|
106
|
+
}
|
|
107
|
+
case "read": {
|
|
108
|
+
if (!arg) {
|
|
109
|
+
console.error("Error: path required. Usage: lesa read <path>");
|
|
110
|
+
process.exit(1);
|
|
111
|
+
}
|
|
112
|
+
try {
|
|
113
|
+
const result = readWorkspaceFile(config.workspaceDir, arg);
|
|
114
|
+
console.log(result.content);
|
|
115
|
+
} catch (err) {
|
|
116
|
+
console.error(err.message);
|
|
117
|
+
process.exit(1);
|
|
118
|
+
}
|
|
119
|
+
break;
|
|
120
|
+
}
|
|
121
|
+
case "status": {
|
|
122
|
+
console.log(`lesa-bridge status`);
|
|
123
|
+
console.log(` OpenClaw dir: ${config.openclawDir}`);
|
|
124
|
+
console.log(` Workspace: ${config.workspaceDir}`);
|
|
125
|
+
console.log(` Database: ${config.dbPath}`);
|
|
126
|
+
console.log(` Inbox port: ${config.inboxPort}`);
|
|
127
|
+
console.log(` Embedding: ${config.embeddingModel} (${config.embeddingDimensions}d)`);
|
|
128
|
+
break;
|
|
129
|
+
}
|
|
130
|
+
case "diagnose": {
|
|
131
|
+
console.log("lesa-bridge diagnose\n");
|
|
132
|
+
let issues = 0;
|
|
133
|
+
if (existsSync(config.openclawDir)) {
|
|
134
|
+
console.log(` \u2713 OpenClaw dir exists: ${config.openclawDir}`);
|
|
135
|
+
} else {
|
|
136
|
+
console.log(` \u2717 OpenClaw dir missing: ${config.openclawDir}`);
|
|
137
|
+
issues++;
|
|
138
|
+
}
|
|
139
|
+
try {
|
|
140
|
+
const gw = resolveGatewayConfig(config.openclawDir);
|
|
141
|
+
console.log(` \u2713 Gateway config found (port ${gw.port}, token present)`);
|
|
142
|
+
try {
|
|
143
|
+
const resp = await fetch(`http://127.0.0.1:${gw.port}/health`, { signal: AbortSignal.timeout(3e3) });
|
|
144
|
+
if (resp.ok) {
|
|
145
|
+
console.log(` \u2713 Gateway responding on port ${gw.port}`);
|
|
146
|
+
} else {
|
|
147
|
+
console.log(` \u2717 Gateway returned ${resp.status}`);
|
|
148
|
+
issues++;
|
|
149
|
+
}
|
|
150
|
+
} catch {
|
|
151
|
+
console.log(` \u2717 Gateway not reachable on port ${gw.port}`);
|
|
152
|
+
issues++;
|
|
153
|
+
}
|
|
154
|
+
} catch (err) {
|
|
155
|
+
console.log(` \u2717 Gateway config: ${err.message}`);
|
|
156
|
+
issues++;
|
|
157
|
+
}
|
|
158
|
+
try {
|
|
159
|
+
const resp = await fetch(`http://127.0.0.1:${config.inboxPort}/status`, { signal: AbortSignal.timeout(3e3) });
|
|
160
|
+
const data = await resp.json();
|
|
161
|
+
if (data.ok) {
|
|
162
|
+
console.log(` \u2713 Inbox endpoint responding (${data.pending} pending)`);
|
|
163
|
+
} else {
|
|
164
|
+
console.log(` \u2717 Inbox endpoint returned unexpected response`);
|
|
165
|
+
issues++;
|
|
166
|
+
}
|
|
167
|
+
} catch {
|
|
168
|
+
console.log(` - Inbox not running (normal if MCP server isn't started)`);
|
|
169
|
+
}
|
|
170
|
+
if (existsSync(config.dbPath)) {
|
|
171
|
+
const stats = statSync(config.dbPath);
|
|
172
|
+
const sizeMB = (stats.size / 1024 / 1024).toFixed(1);
|
|
173
|
+
console.log(` \u2713 Embeddings DB exists (${sizeMB} MB)`);
|
|
174
|
+
} else {
|
|
175
|
+
console.log(` \u2717 Embeddings DB missing: ${config.dbPath}`);
|
|
176
|
+
issues++;
|
|
177
|
+
}
|
|
178
|
+
const apiKey = resolveApiKey(config.openclawDir);
|
|
179
|
+
if (apiKey) {
|
|
180
|
+
console.log(` \u2713 OpenAI API key found (semantic search enabled)`);
|
|
181
|
+
} else {
|
|
182
|
+
console.log(` - No OpenAI API key (text search fallback)`);
|
|
183
|
+
}
|
|
184
|
+
try {
|
|
185
|
+
const skills = discoverSkills(config.openclawDir);
|
|
186
|
+
const executable = skills.filter((s) => s.hasScripts).length;
|
|
187
|
+
console.log(` \u2713 Skills discovered: ${skills.length} total, ${executable} executable`);
|
|
188
|
+
} catch (err) {
|
|
189
|
+
console.log(` \u2717 Skill discovery failed: ${err.message}`);
|
|
190
|
+
issues++;
|
|
191
|
+
}
|
|
192
|
+
if (existsSync(config.workspaceDir)) {
|
|
193
|
+
console.log(` \u2713 Workspace dir exists`);
|
|
194
|
+
} else {
|
|
195
|
+
console.log(` \u2717 Workspace dir missing: ${config.workspaceDir}`);
|
|
196
|
+
issues++;
|
|
197
|
+
}
|
|
198
|
+
console.log();
|
|
199
|
+
if (issues === 0) {
|
|
200
|
+
console.log(" All checks passed.");
|
|
201
|
+
} else {
|
|
202
|
+
console.log(` ${issues} issue(s) found.`);
|
|
203
|
+
}
|
|
204
|
+
break;
|
|
205
|
+
}
|
|
206
|
+
default:
|
|
207
|
+
console.error(`Unknown command: ${command}`);
|
|
208
|
+
usage();
|
|
209
|
+
process.exit(1);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
main().catch((err) => {
|
|
213
|
+
console.error(`Fatal: ${err.message}`);
|
|
214
|
+
process.exit(1);
|
|
215
|
+
});
|