@wipcomputer/wip-ldm-os 0.4.73-alpha.2 → 0.4.73-alpha.20
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/bin/ldm.js +80 -29
- package/dist/bridge/{chunk-LF7EMFBY.js → chunk-24DJYS7Z.js} +95 -48
- package/dist/bridge/cli.js +1 -1
- package/dist/bridge/core.d.ts +1 -0
- package/dist/bridge/core.js +1 -1
- package/dist/bridge/mcp-server.js +18 -4
- package/dist/bridge/openclaw.d.ts +5 -0
- package/dist/bridge/openclaw.js +9 -0
- package/docs/doc-pipeline/README.md +74 -0
- package/docs/doc-pipeline/TECHNICAL.md +79 -0
- package/lib/deploy.mjs +66 -10
- package/lib/detect.mjs +20 -6
- package/package.json +2 -2
- package/shared/docs/how-install-works.md.tmpl +22 -2
- package/shared/docs/how-releases-work.md.tmpl +57 -43
- package/shared/rules/git-conventions.md +3 -3
- package/shared/rules/release-pipeline.md +1 -1
- package/shared/rules/security.md +1 -1
- package/shared/rules/workspace-boundaries.md +1 -1
- package/shared/rules/writing-style.md +1 -1
- package/src/bridge/core.ts +113 -53
- package/src/bridge/mcp-server.ts +35 -4
- package/src/bridge/openclaw.ts +14 -0
- package/src/hooks/inbox-check-hook.mjs +176 -0
- package/src/hosted-mcp/demo/agent.html +300 -0
- package/src/hosted-mcp/demo/agent.txt +84 -0
- package/src/hosted-mcp/demo/fallback.jpg +0 -0
- package/src/hosted-mcp/demo/footer.js +16 -0
- package/src/hosted-mcp/demo/index.html +1291 -0
- package/src/hosted-mcp/demo/privacy.html +230 -0
- package/src/hosted-mcp/demo/sprites.jpg +0 -0
- package/src/hosted-mcp/demo/sprites.png +0 -0
- package/src/hosted-mcp/demo/tos.html +205 -0
- package/src/hosted-mcp/deploy.sh +70 -0
- package/src/hosted-mcp/inbox.mjs +64 -0
- package/src/hosted-mcp/package.json +21 -0
- package/src/hosted-mcp/server.mjs +1625 -0
- package/src/hosted-mcp/tools.mjs +73 -0
package/bin/ldm.js
CHANGED
|
@@ -544,20 +544,14 @@ function deployDocs() {
|
|
|
544
544
|
return count;
|
|
545
545
|
}
|
|
546
546
|
|
|
547
|
-
// Deploy to
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
if (docsCount > 0) {
|
|
551
|
-
console.log(` + ${docsCount} personalized doc(s) deployed to ${docsDest.replace(HOME, '~')}/`);
|
|
552
|
-
}
|
|
553
|
-
|
|
554
|
-
// Deploy to library/documentation/ (human-readable library copy)
|
|
547
|
+
// Deploy to library/documentation/ (the canonical doc path since Mar 28 rename).
|
|
548
|
+
// Previously also deployed to settings/docs/ which Parker renamed to library/documentation/.
|
|
549
|
+
// That created a ghost folder on every install. Removed 2026-04-05 per INST-1.
|
|
555
550
|
const libraryDest = join(workspacePath, 'library', 'documentation');
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
}
|
|
551
|
+
mkdirSync(libraryDest, { recursive: true });
|
|
552
|
+
const docsCount = renderTemplates(libraryDest);
|
|
553
|
+
if (docsCount > 0) {
|
|
554
|
+
console.log(` + ${docsCount} personalized doc(s) deployed to ${libraryDest.replace(HOME, '~')}/`);
|
|
561
555
|
}
|
|
562
556
|
|
|
563
557
|
return docsCount;
|
|
@@ -820,10 +814,16 @@ async function cmdInit() {
|
|
|
820
814
|
// Scaffold workspace output dirs if workspace is configured
|
|
821
815
|
const workspace = config.workspace;
|
|
822
816
|
if (workspace && existsSync(workspace)) {
|
|
823
|
-
// Per-agent workspace dirs
|
|
824
|
-
|
|
817
|
+
// Per-agent workspace dirs.
|
|
818
|
+
// Resolve the team folder name from config.json agents[id].teamFolder
|
|
819
|
+
// so agents with unicode names or custom folder names don't get ghost
|
|
820
|
+
// folders created from their agent ID. Falls back to agent ID if no
|
|
821
|
+
// override is configured. Fixed 2026-04-05 per INST-1: previously
|
|
822
|
+
// hardcoded a map that only knew three agents and created ghost folders
|
|
823
|
+
// for any others.
|
|
825
824
|
for (const agentId of agentList) {
|
|
826
|
-
const
|
|
825
|
+
const agentObj = typeof agentsObj[agentId] === 'object' ? agentsObj[agentId] : {};
|
|
826
|
+
const teamName = agentObj.teamFolder || agentObj.name || agentId;
|
|
827
827
|
for (const sub of ['journals', 'automated/memory/summaries/daily', 'automated/memory/summaries/weekly', 'automated/memory/summaries/monthly', 'automated/memory/summaries/quarterly']) {
|
|
828
828
|
dirs.push(join(workspace, 'team', teamName, sub));
|
|
829
829
|
}
|
|
@@ -1321,23 +1321,31 @@ async function cmdInstall() {
|
|
|
1321
1321
|
|
|
1322
1322
|
// Check if target looks like an npm package (starts with @ or is a plain name without /)
|
|
1323
1323
|
if (resolvedTarget.startsWith('@') || (!resolvedTarget.includes('/') && !existsSync(resolve(resolvedTarget)))) {
|
|
1324
|
-
// Try npm
|
|
1324
|
+
// Try npm pack + tar extract to temp dir
|
|
1325
|
+
// npm install --prefix silently fails for scoped packages in temp directories...
|
|
1326
|
+
// it creates the lock file but doesn't extract files. npm pack is reliable.
|
|
1325
1327
|
const npmName = resolvedTarget;
|
|
1326
1328
|
const tempDir = join(LDM_TMP, `npm-${Date.now()}`);
|
|
1327
1329
|
console.log('');
|
|
1328
1330
|
console.log(` Installing ${npmName} from npm...`);
|
|
1329
1331
|
try {
|
|
1330
1332
|
mkdirSync(tempDir, { recursive: true });
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1333
|
+
// Use npm pack + tar instead of npm install --prefix
|
|
1334
|
+
const tarball = execSync(`npm pack ${npmName} --pack-destination "${tempDir}" 2>/dev/null`, {
|
|
1335
|
+
encoding: 'utf8', timeout: 60000, cwd: tempDir,
|
|
1336
|
+
}).trim();
|
|
1337
|
+
const tarPath = join(tempDir, tarball);
|
|
1338
|
+
if (existsSync(tarPath)) {
|
|
1339
|
+
execSync(`tar xzf "${tarPath}" -C "${tempDir}"`, { stdio: 'pipe' });
|
|
1340
|
+
const extracted = join(tempDir, 'package');
|
|
1341
|
+
if (existsSync(extracted)) {
|
|
1342
|
+
console.log(` + Installed from npm`);
|
|
1343
|
+
repoPath = extracted;
|
|
1344
|
+
} else {
|
|
1345
|
+
console.error(` x npm pack succeeded but extraction failed`);
|
|
1346
|
+
}
|
|
1338
1347
|
} else {
|
|
1339
|
-
console.error(` x
|
|
1340
|
-
process.exit(1);
|
|
1348
|
+
console.error(` x npm pack failed: tarball not found`);
|
|
1341
1349
|
}
|
|
1342
1350
|
} catch (e) {
|
|
1343
1351
|
// npm failed, fall through to git clone or path resolution
|
|
@@ -2206,13 +2214,56 @@ async function cmdInstallCatalog() {
|
|
|
2206
2214
|
continue;
|
|
2207
2215
|
}
|
|
2208
2216
|
|
|
2209
|
-
if (!entry.catalogRepo) {
|
|
2217
|
+
if (!entry.catalogRepo && !entry.catalogNpm) {
|
|
2210
2218
|
console.log(` Skipping ${entry.name}: no catalog repo (install manually with ldm install <org/repo>)`);
|
|
2211
2219
|
continue;
|
|
2212
2220
|
}
|
|
2213
|
-
|
|
2221
|
+
|
|
2222
|
+
// Source resolution chain (#264):
|
|
2223
|
+
// 1. npm (when --alpha/--beta or npm package available) - works online, any machine
|
|
2224
|
+
// 2. Local private repo (offline, developer machine) - works without internet
|
|
2225
|
+
// 3. GitHub clone (fallback) - works online, any machine
|
|
2226
|
+
let installSource = null;
|
|
2227
|
+
const npmTag = ALPHA_FLAG ? 'alpha' : BETA_FLAG ? 'beta' : null;
|
|
2228
|
+
|
|
2229
|
+
// Try npm first when using alpha/beta tracks or when npm is available
|
|
2230
|
+
if (entry.catalogNpm && (npmTag || !entry.catalogRepo)) {
|
|
2231
|
+
const ver = npmTag ? `${entry.catalogNpm}@${npmTag}` : `${entry.catalogNpm}@${entry.latestVersion}`;
|
|
2232
|
+
installSource = ver;
|
|
2233
|
+
console.log(` Updating ${entry.name} v${entry.currentVersion} -> v${entry.latestVersion} (from npm ${npmTag || 'latest'})...`);
|
|
2234
|
+
}
|
|
2235
|
+
|
|
2236
|
+
// Try local private repo (for offline/developer installs)
|
|
2237
|
+
if (!installSource && entry.catalogRepo) {
|
|
2238
|
+
const repoName = basename(entry.catalogRepo);
|
|
2239
|
+
const privateRepoName = repoName + '-private';
|
|
2240
|
+
const WORKSPACE = join(HOME, 'wipcomputerinc');
|
|
2241
|
+
// Search known repo locations
|
|
2242
|
+
const searchDirs = ['repos/ldm-os/devops', 'repos/ldm-os/components', 'repos/ldm-os/utilities', 'repos/ldm-os/apps', 'repos/ldm-os/apis', 'repos/ldm-os/identity'];
|
|
2243
|
+
for (const dir of searchDirs) {
|
|
2244
|
+
const localPrivate = join(WORKSPACE, dir, privateRepoName);
|
|
2245
|
+
const localPublic = join(WORKSPACE, dir, repoName);
|
|
2246
|
+
if (existsSync(localPrivate)) {
|
|
2247
|
+
installSource = localPrivate;
|
|
2248
|
+
console.log(` Updating ${entry.name} v${entry.currentVersion} -> v${entry.latestVersion} (from local ${privateRepoName})...`);
|
|
2249
|
+
break;
|
|
2250
|
+
}
|
|
2251
|
+
if (existsSync(localPublic)) {
|
|
2252
|
+
installSource = localPublic;
|
|
2253
|
+
console.log(` Updating ${entry.name} v${entry.currentVersion} -> v${entry.latestVersion} (from local ${repoName})...`);
|
|
2254
|
+
break;
|
|
2255
|
+
}
|
|
2256
|
+
}
|
|
2257
|
+
}
|
|
2258
|
+
|
|
2259
|
+
// Fallback: GitHub clone
|
|
2260
|
+
if (!installSource) {
|
|
2261
|
+
installSource = entry.catalogRepo;
|
|
2262
|
+
console.log(` Updating ${entry.name} v${entry.currentVersion} -> v${entry.latestVersion} (from ${entry.catalogRepo})...`);
|
|
2263
|
+
}
|
|
2264
|
+
|
|
2214
2265
|
try {
|
|
2215
|
-
execSync(`ldm install ${
|
|
2266
|
+
execSync(`ldm install ${installSource}`, { stdio: 'inherit' });
|
|
2216
2267
|
updated++;
|
|
2217
2268
|
|
|
2218
2269
|
// For parent packages, update registry version for all sub-tools (#139, #262)
|
|
@@ -6,6 +6,27 @@ import { homedir } from "os";
|
|
|
6
6
|
import { promisify } from "util";
|
|
7
7
|
import { randomUUID } from "crypto";
|
|
8
8
|
var execAsync = promisify(exec);
|
|
9
|
+
var GATEWAY_HOST = "127.0.0.1";
|
|
10
|
+
var DEFAULT_GATEWAY_PORT = 18789;
|
|
11
|
+
var DEFAULT_INBOX_PORT = 18790;
|
|
12
|
+
var GATEWAY_TIMEOUT_MS = 12e4;
|
|
13
|
+
var OP_CLI_TIMEOUT_MS = 1e4;
|
|
14
|
+
var EMBEDDING_API_URL = "https://api.openai.com/v1/embeddings";
|
|
15
|
+
var DEFAULT_EMBEDDING_MODEL = "text-embedding-3-small";
|
|
16
|
+
var DEFAULT_EMBEDDING_DIMS = 1536;
|
|
17
|
+
var VECTOR_SEARCH_ROW_LIMIT = 1e3;
|
|
18
|
+
var RECENCY_DECAY_RATE = 0.01;
|
|
19
|
+
var RECENCY_FLOOR = 0.5;
|
|
20
|
+
var FRESHNESS_FRESH_DAYS = 3;
|
|
21
|
+
var FRESHNESS_RECENT_DAYS = 7;
|
|
22
|
+
var FRESHNESS_AGING_DAYS = 14;
|
|
23
|
+
var DEFAULT_SEARCH_LIMIT = 5;
|
|
24
|
+
var WORKSPACE_MAX_DEPTH = 4;
|
|
25
|
+
var WORKSPACE_MAX_EXCERPTS = 5;
|
|
26
|
+
var WORKSPACE_MAX_RESULTS = 10;
|
|
27
|
+
var SKILL_EXEC_TIMEOUT_MS = 12e4;
|
|
28
|
+
var SKILL_EXEC_MAX_BUFFER = 10 * 1024 * 1024;
|
|
29
|
+
var MS_PER_DAY = 1e3 * 60 * 60 * 24;
|
|
9
30
|
var HOME = process.env.HOME || homedir();
|
|
10
31
|
var LDM_ROOT = process.env.LDM_ROOT || join(HOME, ".ldm");
|
|
11
32
|
function resolveConfig(overrides) {
|
|
@@ -14,9 +35,9 @@ function resolveConfig(overrides) {
|
|
|
14
35
|
openclawDir,
|
|
15
36
|
workspaceDir: overrides?.workspaceDir || join(openclawDir, "workspace"),
|
|
16
37
|
dbPath: overrides?.dbPath || join(openclawDir, "memory", "context-embeddings.sqlite"),
|
|
17
|
-
inboxPort: overrides?.inboxPort || parseInt(process.env.LESA_BRIDGE_INBOX_PORT ||
|
|
18
|
-
embeddingModel: overrides?.embeddingModel ||
|
|
19
|
-
embeddingDimensions: overrides?.embeddingDimensions ||
|
|
38
|
+
inboxPort: overrides?.inboxPort || parseInt(process.env.LESA_BRIDGE_INBOX_PORT || String(DEFAULT_INBOX_PORT), 10),
|
|
39
|
+
embeddingModel: overrides?.embeddingModel || DEFAULT_EMBEDDING_MODEL,
|
|
40
|
+
embeddingDimensions: overrides?.embeddingDimensions || DEFAULT_EMBEDDING_DIMS
|
|
20
41
|
};
|
|
21
42
|
}
|
|
22
43
|
function resolveConfigMulti(overrides) {
|
|
@@ -29,9 +50,9 @@ function resolveConfigMulti(overrides) {
|
|
|
29
50
|
openclawDir,
|
|
30
51
|
workspaceDir: raw.workspaceDir || overrides?.workspaceDir || join(openclawDir, "workspace"),
|
|
31
52
|
dbPath: raw.dbPath || overrides?.dbPath || join(openclawDir, "memory", "context-embeddings.sqlite"),
|
|
32
|
-
inboxPort: raw.inboxPort || overrides?.inboxPort || parseInt(process.env.LESA_BRIDGE_INBOX_PORT ||
|
|
33
|
-
embeddingModel: raw.embeddingModel || overrides?.embeddingModel ||
|
|
34
|
-
embeddingDimensions: raw.embeddingDimensions || overrides?.embeddingDimensions ||
|
|
53
|
+
inboxPort: raw.inboxPort || overrides?.inboxPort || parseInt(process.env.LESA_BRIDGE_INBOX_PORT || String(DEFAULT_INBOX_PORT), 10),
|
|
54
|
+
embeddingModel: raw.embeddingModel || overrides?.embeddingModel || DEFAULT_EMBEDDING_MODEL,
|
|
55
|
+
embeddingDimensions: raw.embeddingDimensions || overrides?.embeddingDimensions || DEFAULT_EMBEDDING_DIMS
|
|
35
56
|
};
|
|
36
57
|
} catch {
|
|
37
58
|
}
|
|
@@ -53,7 +74,7 @@ function resolveApiKey(openclawDir) {
|
|
|
53
74
|
`op read "op://Agent Secrets/OpenAI API/api key" 2>/dev/null`,
|
|
54
75
|
{
|
|
55
76
|
env: { ...process.env, OP_SERVICE_ACCOUNT_TOKEN: saToken },
|
|
56
|
-
timeout:
|
|
77
|
+
timeout: OP_CLI_TIMEOUT_MS,
|
|
57
78
|
encoding: "utf-8"
|
|
58
79
|
}
|
|
59
80
|
).trim();
|
|
@@ -76,7 +97,7 @@ function resolveGatewayConfig(openclawDir) {
|
|
|
76
97
|
}
|
|
77
98
|
const config = JSON.parse(readFileSync(configPath, "utf-8"));
|
|
78
99
|
const token = config?.gateway?.auth?.token;
|
|
79
|
-
const port = config?.gateway?.port ||
|
|
100
|
+
const port = config?.gateway?.port || DEFAULT_GATEWAY_PORT;
|
|
80
101
|
if (!token) {
|
|
81
102
|
throw new Error("No gateway.auth.token found in openclaw.json");
|
|
82
103
|
}
|
|
@@ -262,37 +283,64 @@ async function sendMessage(openclawDir, message, options) {
|
|
|
262
283
|
const { token, port } = resolveGatewayConfig(openclawDir);
|
|
263
284
|
const agentId = options?.agentId || "main";
|
|
264
285
|
const senderLabel = options?.senderLabel || "Claude Code";
|
|
265
|
-
const
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
model: `openclaw/${agentId}`,
|
|
275
|
-
messages: [
|
|
276
|
-
{
|
|
277
|
-
role: "user",
|
|
278
|
-
content: `[${senderLabel}]: ${message}`
|
|
279
|
-
}
|
|
280
|
-
]
|
|
281
|
-
})
|
|
286
|
+
const fireAndForget = options?.fireAndForget ?? false;
|
|
287
|
+
const requestBody = JSON.stringify({
|
|
288
|
+
model: `openclaw/${agentId}`,
|
|
289
|
+
messages: [
|
|
290
|
+
{
|
|
291
|
+
role: "user",
|
|
292
|
+
content: `[${senderLabel}]: ${message}`
|
|
293
|
+
}
|
|
294
|
+
]
|
|
282
295
|
});
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
296
|
+
const requestHeaders = {
|
|
297
|
+
Authorization: `Bearer ${token}`,
|
|
298
|
+
"Content-Type": "application/json",
|
|
299
|
+
"x-openclaw-scopes": "operator.read,operator.write",
|
|
300
|
+
"x-openclaw-session-key": `agent:${agentId}:main`
|
|
301
|
+
};
|
|
302
|
+
const url = `http://${GATEWAY_HOST}:${port}/v1/chat/completions`;
|
|
303
|
+
if (fireAndForget) {
|
|
304
|
+
fetch(url, {
|
|
305
|
+
method: "POST",
|
|
306
|
+
headers: requestHeaders,
|
|
307
|
+
body: requestBody
|
|
308
|
+
}).catch(() => {
|
|
309
|
+
});
|
|
310
|
+
return "Message sent (queued). Response will arrive in the TUI.";
|
|
286
311
|
}
|
|
287
|
-
const
|
|
288
|
-
const
|
|
289
|
-
|
|
290
|
-
|
|
312
|
+
const controller = new AbortController();
|
|
313
|
+
const timeoutId = setTimeout(() => controller.abort(), GATEWAY_TIMEOUT_MS);
|
|
314
|
+
try {
|
|
315
|
+
const response = await fetch(url, {
|
|
316
|
+
method: "POST",
|
|
317
|
+
headers: requestHeaders,
|
|
318
|
+
body: requestBody,
|
|
319
|
+
signal: controller.signal
|
|
320
|
+
});
|
|
321
|
+
clearTimeout(timeoutId);
|
|
322
|
+
if (!response.ok) {
|
|
323
|
+
const body = await response.text();
|
|
324
|
+
throw new Error(`Gateway returned ${response.status}: ${body}`);
|
|
325
|
+
}
|
|
326
|
+
const data = await response.json();
|
|
327
|
+
const reply = data.choices?.[0]?.message?.content;
|
|
328
|
+
if (!reply) {
|
|
329
|
+
throw new Error("No response content from gateway");
|
|
330
|
+
}
|
|
331
|
+
return reply;
|
|
332
|
+
} catch (err) {
|
|
333
|
+
clearTimeout(timeoutId);
|
|
334
|
+
if (err.name === "AbortError") {
|
|
335
|
+
throw new Error(
|
|
336
|
+
"Gateway timeout: Lesa may be busy or the gateway is processing another request. Try again in a moment."
|
|
337
|
+
);
|
|
338
|
+
}
|
|
339
|
+
throw err;
|
|
291
340
|
}
|
|
292
|
-
return reply;
|
|
293
341
|
}
|
|
294
|
-
async function getQueryEmbedding(text, apiKey, model =
|
|
295
|
-
const response = await fetch(
|
|
342
|
+
async function getQueryEmbedding(text, apiKey, model = DEFAULT_EMBEDDING_MODEL, dimensions = DEFAULT_EMBEDDING_DIMS) {
|
|
343
|
+
const response = await fetch(EMBEDDING_API_URL, {
|
|
296
344
|
method: "POST",
|
|
297
345
|
headers: {
|
|
298
346
|
Authorization: `Bearer ${apiKey}`,
|
|
@@ -331,15 +379,15 @@ function cosineSimilarity(a, b) {
|
|
|
331
379
|
return denom === 0 ? 0 : dot / denom;
|
|
332
380
|
}
|
|
333
381
|
function recencyWeight(ageDays) {
|
|
334
|
-
return Math.max(
|
|
382
|
+
return Math.max(RECENCY_FLOOR, 1 - ageDays * RECENCY_DECAY_RATE);
|
|
335
383
|
}
|
|
336
384
|
function freshnessLabel(ageDays) {
|
|
337
|
-
if (ageDays <
|
|
338
|
-
if (ageDays <
|
|
339
|
-
if (ageDays <
|
|
385
|
+
if (ageDays < FRESHNESS_FRESH_DAYS) return "fresh";
|
|
386
|
+
if (ageDays < FRESHNESS_RECENT_DAYS) return "recent";
|
|
387
|
+
if (ageDays < FRESHNESS_AGING_DAYS) return "aging";
|
|
340
388
|
return "stale";
|
|
341
389
|
}
|
|
342
|
-
async function searchConversations(config, query, limit =
|
|
390
|
+
async function searchConversations(config, query, limit = DEFAULT_SEARCH_LIMIT) {
|
|
343
391
|
const Database = (await import("better-sqlite3")).default;
|
|
344
392
|
if (!existsSync(config.dbPath)) {
|
|
345
393
|
throw new Error(`Database not found: ${config.dbPath}`);
|
|
@@ -360,12 +408,12 @@ async function searchConversations(config, query, limit = 5) {
|
|
|
360
408
|
FROM conversation_chunks
|
|
361
409
|
WHERE embedding IS NOT NULL
|
|
362
410
|
ORDER BY timestamp DESC
|
|
363
|
-
LIMIT
|
|
411
|
+
LIMIT ${VECTOR_SEARCH_ROW_LIMIT}`
|
|
364
412
|
).all();
|
|
365
413
|
const now = Date.now();
|
|
366
414
|
return rows.map((row) => {
|
|
367
415
|
const cosine = cosineSimilarity(queryEmbedding, blobToEmbedding(row.embedding));
|
|
368
|
-
const ageDays = (now - row.timestamp) /
|
|
416
|
+
const ageDays = (now - row.timestamp) / MS_PER_DAY;
|
|
369
417
|
const weight = recencyWeight(ageDays);
|
|
370
418
|
return {
|
|
371
419
|
text: row.chunk_text,
|
|
@@ -396,7 +444,7 @@ async function searchConversations(config, query, limit = 5) {
|
|
|
396
444
|
db.close();
|
|
397
445
|
}
|
|
398
446
|
}
|
|
399
|
-
function findMarkdownFiles(dir, maxDepth =
|
|
447
|
+
function findMarkdownFiles(dir, maxDepth = WORKSPACE_MAX_DEPTH, depth = 0) {
|
|
400
448
|
if (depth > maxDepth || !existsSync(dir)) return [];
|
|
401
449
|
const files = [];
|
|
402
450
|
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
@@ -426,7 +474,7 @@ function searchWorkspace(workspaceDir, query) {
|
|
|
426
474
|
if (score === 0) continue;
|
|
427
475
|
const lines = content.split("\n");
|
|
428
476
|
const excerpts = [];
|
|
429
|
-
for (let i = 0; i < lines.length && excerpts.length <
|
|
477
|
+
for (let i = 0; i < lines.length && excerpts.length < WORKSPACE_MAX_EXCERPTS; i++) {
|
|
430
478
|
const lineLower = lines[i].toLowerCase();
|
|
431
479
|
if (words.some((w) => lineLower.includes(w))) {
|
|
432
480
|
const start = Math.max(0, i - 1);
|
|
@@ -438,7 +486,7 @@ function searchWorkspace(workspaceDir, query) {
|
|
|
438
486
|
} catch {
|
|
439
487
|
}
|
|
440
488
|
}
|
|
441
|
-
return results.sort((a, b) => b.score - a.score).slice(0,
|
|
489
|
+
return results.sort((a, b) => b.score - a.score).slice(0, WORKSPACE_MAX_RESULTS);
|
|
442
490
|
}
|
|
443
491
|
function parseSkillFrontmatter(content) {
|
|
444
492
|
const match = content.match(/^---\n([\s\S]*?)\n---/);
|
|
@@ -532,9 +580,8 @@ async function executeSkillScript(skillDir, scripts, scriptName, args) {
|
|
|
532
580
|
`${interpreter} "${scriptPath}" ${args}`,
|
|
533
581
|
{
|
|
534
582
|
env: { ...process.env },
|
|
535
|
-
timeout:
|
|
536
|
-
maxBuffer:
|
|
537
|
-
// 10MB
|
|
583
|
+
timeout: SKILL_EXEC_TIMEOUT_MS,
|
|
584
|
+
maxBuffer: SKILL_EXEC_MAX_BUFFER
|
|
538
585
|
}
|
|
539
586
|
);
|
|
540
587
|
return stdout || stderr || "(no output)";
|
package/dist/bridge/cli.js
CHANGED
package/dist/bridge/core.d.ts
CHANGED
|
@@ -107,6 +107,7 @@ declare function sendMessage(openclawDir: string, message: string, options?: {
|
|
|
107
107
|
agentId?: string;
|
|
108
108
|
user?: string;
|
|
109
109
|
senderLabel?: string;
|
|
110
|
+
fireAndForget?: boolean;
|
|
110
111
|
}): Promise<string>;
|
|
111
112
|
declare function getQueryEmbedding(text: string, apiKey: string, model?: string, dimensions?: number): Promise<number[]>;
|
|
112
113
|
declare function blobToEmbedding(blob: Buffer): number[];
|
package/dist/bridge/core.js
CHANGED
|
@@ -15,7 +15,7 @@ import {
|
|
|
15
15
|
sendLdmMessage,
|
|
16
16
|
sendMessage,
|
|
17
17
|
setSessionIdentity
|
|
18
|
-
} from "./chunk-
|
|
18
|
+
} from "./chunk-24DJYS7Z.js";
|
|
19
19
|
|
|
20
20
|
// mcp-server.ts
|
|
21
21
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
@@ -191,15 +191,29 @@ ${result.content}` }] };
|
|
|
191
191
|
server.registerTool(
|
|
192
192
|
"lesa_send_message",
|
|
193
193
|
{
|
|
194
|
-
description: "Send a message to the OpenClaw agent through the gateway. Routes through the agent's full pipeline: memory, tools, personality, workspace. Use this for direct communication: asking questions, sharing findings, coordinating work, or having a discussion. Messages are prefixed with [Claude Code] so the agent knows the source.",
|
|
194
|
+
description: "Send a message to the OpenClaw agent through the gateway. Routes through the agent's full pipeline: memory, tools, personality, workspace. Use this for direct communication: asking questions, sharing findings, coordinating work, or having a discussion. Messages are prefixed with [Claude Code] so the agent knows the source.\n\nThis is async: returns immediately after sending. The agent's reply will arrive in your inbox (check via lesa_check_inbox or it appears automatically on your next turn).",
|
|
195
195
|
inputSchema: {
|
|
196
196
|
message: z.string().describe("Message to send to the OpenClaw agent")
|
|
197
197
|
}
|
|
198
198
|
},
|
|
199
199
|
async ({ message }) => {
|
|
200
200
|
try {
|
|
201
|
-
|
|
202
|
-
|
|
201
|
+
await sendMessage(config.openclawDir, message, { fireAndForget: true });
|
|
202
|
+
const { agentId, sessionName } = getSessionIdentity();
|
|
203
|
+
sendLdmMessage({
|
|
204
|
+
from: `${agentId}:${sessionName}`,
|
|
205
|
+
to: "lesa",
|
|
206
|
+
body: message,
|
|
207
|
+
type: "chat"
|
|
208
|
+
});
|
|
209
|
+
return {
|
|
210
|
+
content: [{
|
|
211
|
+
type: "text",
|
|
212
|
+
text: `Sent to L\u0113sa: "${message}"
|
|
213
|
+
|
|
214
|
+
Message delivered to the gateway (fire-and-forget). L\u0113sa will process it through her full pipeline. Her reply will arrive in your inbox. Use lesa_check_inbox to check, or it will appear automatically on your next turn.`
|
|
215
|
+
}]
|
|
216
|
+
};
|
|
203
217
|
} catch (err) {
|
|
204
218
|
return { content: [{ type: "text", text: `Error sending message: ${err.message}` }], isError: true };
|
|
205
219
|
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
# Documentation Pipeline
|
|
2
|
+
|
|
3
|
+
Documentation lives in three places. They stay in sync through the installer. This is not optional.
|
|
4
|
+
|
|
5
|
+
## The Three Levels
|
|
6
|
+
|
|
7
|
+
### 1. Repo Docs (source of truth)
|
|
8
|
+
|
|
9
|
+
Every repo has documentation at its root and in `docs/` for features:
|
|
10
|
+
|
|
11
|
+
```
|
|
12
|
+
repo/
|
|
13
|
+
├── README.md What this repo is
|
|
14
|
+
├── TECHNICAL.md How it works
|
|
15
|
+
├── SKILL.md Agent instructions
|
|
16
|
+
├── CLAUDE.md Agent context for Claude Code
|
|
17
|
+
├── docs/
|
|
18
|
+
│ └── <feature>/
|
|
19
|
+
│ ├── README.md What this feature is
|
|
20
|
+
│ └── TECHNICAL.md How this feature works
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
When a feature gets absorbed into a repo, its README and TECHNICAL move into `docs/<feature>/`.
|
|
24
|
+
|
|
25
|
+
Repo docs are the source of truth. Everything else is derived from them.
|
|
26
|
+
|
|
27
|
+
### 2. Home Docs (human readable, personalized)
|
|
28
|
+
|
|
29
|
+
Location: `~/wipcomputerinc/library/documentation/`
|
|
30
|
+
|
|
31
|
+
These are personalized for YOUR system. "Here's how releases work on YOUR machine." Generated by the installer from repo doc templates + your `~/.ldm/config.json`.
|
|
32
|
+
|
|
33
|
+
The human reads these. They describe how the system is set up on this specific machine, with this specific configuration.
|
|
34
|
+
|
|
35
|
+
### 3. Agent Docs (OS reference)
|
|
36
|
+
|
|
37
|
+
Location: `~/.ldm/shared/`
|
|
38
|
+
|
|
39
|
+
```
|
|
40
|
+
~/.ldm/shared/
|
|
41
|
+
├── rules/ Thin rules deployed to ~/.claude/rules/
|
|
42
|
+
├── dev-guide-*.md Org-specific dev conventions
|
|
43
|
+
├── boot/ Boot sequence config
|
|
44
|
+
└── prompts/ Cron prompts
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
These are what agents reference. Rules, dev guide, boot config. The installer deploys them so agents always have current instructions.
|
|
48
|
+
|
|
49
|
+
### 4. ai/ (development process)
|
|
50
|
+
|
|
51
|
+
Location: `<repo>/ai/`
|
|
52
|
+
|
|
53
|
+
Plans, bugs, research, dev updates. Private repo only. Never ships to public. Updated by the dev team (humans + AI agents) during development.
|
|
54
|
+
|
|
55
|
+
## The Update Flow
|
|
56
|
+
|
|
57
|
+
### On merge to private main
|
|
58
|
+
|
|
59
|
+
1. **Repo docs** updated. README, TECHNICAL, docs/<feature>/, SKILL.md, CLAUDE.md. Part of the PR. Code and docs ship together.
|
|
60
|
+
2. **ai/** updated. Plan archived, bugs closed, dev update written. Notes the version is on alpha.
|
|
61
|
+
|
|
62
|
+
### On `ldm install`
|
|
63
|
+
|
|
64
|
+
3. **Home docs** regenerated. Installer reads repo doc templates + config.json, generates personalized `library/documentation/` files.
|
|
65
|
+
4. **Agent docs** deployed. Installer copies rules, dev guide, boot config from the installed package to `~/.ldm/shared/` and `~/.claude/rules/`.
|
|
66
|
+
|
|
67
|
+
### On deploy to public
|
|
68
|
+
|
|
69
|
+
5. **Public repo** updated. `deploy-public.sh` syncs everything except `ai/`.
|
|
70
|
+
6. **ai/** dev update notes the version moved from alpha to release.
|
|
71
|
+
|
|
72
|
+
## The Rule
|
|
73
|
+
|
|
74
|
+
Three places, one update, never out of sync. The installer is the bridge between "code landed" and "docs are current everywhere." Developers write repo docs. The installer propagates them. Nobody manually updates home docs or agent docs.
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
# Documentation Pipeline: Technical Details
|
|
2
|
+
|
|
3
|
+
## File Paths
|
|
4
|
+
|
|
5
|
+
### Repo doc templates (in the LDM OS repo)
|
|
6
|
+
|
|
7
|
+
```
|
|
8
|
+
shared/docs/*.md.tmpl Templates for home docs
|
|
9
|
+
shared/rules/*.md Source for agent rules
|
|
10
|
+
shared/boot/ Boot sequence config
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
Templates use placeholders from `~/.ldm/config.json` (workspace path, agent names, org name, etc.).
|
|
14
|
+
|
|
15
|
+
### Installed locations
|
|
16
|
+
|
|
17
|
+
```
|
|
18
|
+
~/.ldm/config.json Org config (agents, paths, co-authors)
|
|
19
|
+
~/.ldm/shared/rules/*.md Agent rules (source for ~/.claude/rules/)
|
|
20
|
+
~/.ldm/shared/dev-guide-*.md Org dev guide
|
|
21
|
+
~/.ldm/shared/boot/ Boot sequence config
|
|
22
|
+
~/.ldm/shared/prompts/ Cron prompts
|
|
23
|
+
~/.ldm/templates/ CLAUDE.md templates, install prompt, etc.
|
|
24
|
+
~/wipcomputerinc/library/documentation/ Personalized human docs (from templates)
|
|
25
|
+
~/.claude/rules/ Deployed rules (copied from ~/.ldm/shared/rules/)
|
|
26
|
+
~/.claude/CLAUDE.md Level 1 global (generated from template + config)
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
### What ldm install deploys
|
|
30
|
+
|
|
31
|
+
| Source | Destination | When |
|
|
32
|
+
|--------|------------|------|
|
|
33
|
+
| `shared/rules/*.md` | `~/.ldm/shared/rules/` then `~/.claude/rules/` | Every install |
|
|
34
|
+
| `shared/docs/*.md.tmpl` | `~/wipcomputerinc/library/documentation/` | Every install |
|
|
35
|
+
| `shared/boot/` | `~/.ldm/shared/boot/` | Every install |
|
|
36
|
+
| `shared/prompts/` | `~/.ldm/shared/prompts/` | Every install |
|
|
37
|
+
| `shared/templates/` | `~/.ldm/templates/` | Every install |
|
|
38
|
+
|
|
39
|
+
**Known bug (April 2026):** The installer currently deploys shared rules on `ldm init` (first install) but not on every `ldm install`. This means rule updates in new versions don't propagate until the user re-initializes. This needs to be fixed so rules deploy on every install.
|
|
40
|
+
|
|
41
|
+
**Known bug (April 2026):** The installer previously deployed home docs to `settings/docs/`. This path was renamed to `library/documentation/` on March 28, 2026. The installer must deploy to the correct path.
|
|
42
|
+
|
|
43
|
+
## How templates work
|
|
44
|
+
|
|
45
|
+
Home doc templates at `shared/docs/*.md.tmpl` contain placeholders:
|
|
46
|
+
|
|
47
|
+
```
|
|
48
|
+
Workspace: {{workspace}}
|
|
49
|
+
Agents: {{agents}}
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
The installer reads `~/.ldm/config.json`, substitutes the placeholders, and writes the personalized docs to `~/wipcomputerinc/library/documentation/`.
|
|
53
|
+
|
|
54
|
+
## CLAUDE.md cascade
|
|
55
|
+
|
|
56
|
+
Three levels. Claude Code reads all of them, walking up from CWD:
|
|
57
|
+
|
|
58
|
+
```
|
|
59
|
+
Level 1: ~/.claude/CLAUDE.md Global. ~30 lines. Universal rules.
|
|
60
|
+
Level 2: ~/wipcomputerinc/CLAUDE.md Workspace. ~150 lines. Org context.
|
|
61
|
+
Level 3: <repo>/CLAUDE.md Per-repo. ~50-86 lines. Repo-specific.
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
After context compaction, CWD may shift to a repo. Without Level 3, all context is lost. Every repo must have a CLAUDE.md.
|
|
65
|
+
|
|
66
|
+
## Config paths (correct as of April 2026)
|
|
67
|
+
|
|
68
|
+
References in CLAUDE.md and rules must use absolute paths:
|
|
69
|
+
|
|
70
|
+
| Reference | Correct path |
|
|
71
|
+
|-----------|-------------|
|
|
72
|
+
| Org config | `~/.ldm/config.json` |
|
|
73
|
+
| Dev guide | `~/.ldm/shared/dev-guide-wipcomputerinc.md` |
|
|
74
|
+
| Human docs | `~/wipcomputerinc/library/documentation/` |
|
|
75
|
+
| Agent rules | `~/.ldm/shared/rules/` (source) or `~/.claude/rules/` (deployed) |
|
|
76
|
+
| Workspace CLAUDE.md | `~/wipcomputerinc/CLAUDE.md` |
|
|
77
|
+
| Global CLAUDE.md | `~/.claude/CLAUDE.md` |
|
|
78
|
+
|
|
79
|
+
NOT `settings/config.json`. NOT `settings/docs/`. Those paths are from before the March 28 rename.
|