@wipcomputer/wip-ldm-os 0.4.12 → 0.4.15

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 CHANGED
@@ -5,7 +5,7 @@ license: MIT
5
5
  interface: [cli, skill]
6
6
  metadata:
7
7
  display-name: "LDM OS"
8
- version: "0.4.12"
8
+ version: "0.4.15"
9
9
  homepage: "https://github.com/wipcomputer/wip-ldm-os"
10
10
  author: "Parker Todd Brooks"
11
11
  category: infrastructure
package/bin/ldm.js CHANGED
@@ -314,6 +314,28 @@ async function cmdInit() {
314
314
  }
315
315
  }
316
316
 
317
+ // Deploy process monitor (#75)
318
+ const monitorSrc = join(__dirname, '..', 'bin', 'process-monitor.sh');
319
+ const monitorDest = join(LDM_ROOT, 'bin', 'process-monitor.sh');
320
+ if (existsSync(monitorSrc)) {
321
+ mkdirSync(join(LDM_ROOT, 'bin'), { recursive: true });
322
+ cpSync(monitorSrc, monitorDest);
323
+ chmodSync(monitorDest, 0o755);
324
+ // Add cron entry if not already there
325
+ try {
326
+ const crontab = execSync('crontab -l 2>/dev/null', { encoding: 'utf8' });
327
+ if (!crontab.includes('process-monitor')) {
328
+ execSync(`(crontab -l 2>/dev/null; echo "*/3 * * * * ${monitorDest}") | crontab -`);
329
+ console.log(` + process monitor installed (every 3 min, kills zombie processes)`);
330
+ }
331
+ } catch {
332
+ try {
333
+ execSync(`echo "*/3 * * * * ${monitorDest}" | crontab -`);
334
+ console.log(` + process monitor installed (every 3 min)`);
335
+ } catch {}
336
+ }
337
+ }
338
+
317
339
  console.log('');
318
340
  console.log(` LDM OS v${PKG_VERSION} initialized at ${LDM_ROOT}`);
319
341
  console.log('');
@@ -782,6 +804,10 @@ async function cmdInstallCatalog() {
782
804
 
783
805
  // Update from npm via catalog repos (#55)
784
806
  for (const entry of npmUpdates) {
807
+ if (!entry.catalogRepo) {
808
+ console.log(` Skipping ${entry.name}: no catalog repo (install manually with ldm install <org/repo>)`);
809
+ continue;
810
+ }
785
811
  console.log(` Updating ${entry.name} v${entry.currentVersion} -> v${entry.latestVersion} (from ${entry.catalogRepo})...`);
786
812
  try {
787
813
  execSync(`ldm install ${entry.catalogRepo}`, { stdio: 'inherit' });
@@ -1529,6 +1555,66 @@ async function cmdStackInstall() {
1529
1555
  console.log('');
1530
1556
  }
1531
1557
 
1558
+ // ── ldm catalog show ──
1559
+
1560
+ function cmdCatalogShow() {
1561
+ const subcommand = args[1];
1562
+ const target = args[2];
1563
+
1564
+ if (subcommand === 'show' && target) {
1565
+ const entry = loadCatalog().find(c => c.id === target || c.name.toLowerCase() === target.toLowerCase());
1566
+ if (!entry) {
1567
+ console.error(` Unknown component: "${target}"`);
1568
+ console.error(' Run: ldm catalog');
1569
+ process.exit(1);
1570
+ }
1571
+
1572
+ console.log('');
1573
+ console.log(` ${entry.name}`);
1574
+ console.log(' ────────────────────────────────────');
1575
+ console.log(` ${entry.description}`);
1576
+ console.log('');
1577
+ console.log(` Status: ${entry.status}`);
1578
+ if (entry.repo) console.log(` Repo: github.com/${entry.repo}`);
1579
+ if (entry.npm) console.log(` npm: ${entry.npm}`);
1580
+
1581
+ const inst = entry.installs;
1582
+ if (inst) {
1583
+ console.log('');
1584
+ console.log(' What gets installed:');
1585
+ if (inst.cli) console.log(` CLI: ${Array.isArray(inst.cli) ? inst.cli.join(', ') : inst.cli}`);
1586
+ if (inst.mcp) console.log(` MCP tools: ${Array.isArray(inst.mcp) ? inst.mcp.join(', ') : inst.mcp}`);
1587
+ if (inst.ocPlugin) console.log(` OpenClaw plugin: ${inst.ocPlugin}`);
1588
+ if (inst.ccHook) console.log(` CC hooks: ${inst.ccHook}`);
1589
+ if (inst.cron) console.log(` Cron: ${inst.cron}`);
1590
+ if (inst.data) console.log(` Data: ${inst.data}`);
1591
+ if (inst.tools) console.log(` Tools: ${inst.tools}`);
1592
+ if (inst.web) console.log(` Web: ${inst.web}`);
1593
+ if (inst.runtime) console.log(` Runtime: ${inst.runtime}`);
1594
+ if (inst.plugins) console.log(` Plugins: ${inst.plugins}`);
1595
+ if (inst.skill) console.log(` Skill: ${inst.skill}`);
1596
+ if (inst.docs) console.log(` Docs: ${inst.docs}`);
1597
+ if (inst.note) console.log(` Note: ${inst.note}`);
1598
+ }
1599
+
1600
+ console.log('');
1601
+ return;
1602
+ }
1603
+
1604
+ // Default: list all catalog items
1605
+ const components = loadCatalog();
1606
+ console.log('');
1607
+ console.log(' Catalog');
1608
+ console.log(' ────────────────────────────────────');
1609
+ for (const c of components) {
1610
+ console.log(` ${c.id}: ${c.name} (${c.status})`);
1611
+ console.log(` ${c.description}`);
1612
+ console.log('');
1613
+ }
1614
+ console.log(' Show details: ldm catalog show <name>');
1615
+ console.log('');
1616
+ }
1617
+
1532
1618
  // ── Main ──
1533
1619
 
1534
1620
  async function main() {
@@ -1603,6 +1689,9 @@ async function main() {
1603
1689
  case 'stack':
1604
1690
  await cmdStack();
1605
1691
  break;
1692
+ case 'catalog':
1693
+ cmdCatalogShow();
1694
+ break;
1606
1695
  case 'updates':
1607
1696
  await cmdUpdates();
1608
1697
  break;
@@ -0,0 +1,65 @@
1
+ #!/bin/bash
2
+ # LDM OS Process Monitor
3
+ # Kills zombie npm/ldm processes, cleans stale locks.
4
+ # Run via healthcheck (every 3 min) or standalone cron.
5
+
6
+ LOG="/tmp/ldm-process-monitor.log"
7
+ KILLED=0
8
+
9
+ log() { echo "[$(date '+%H:%M:%S')] $1" >> "$LOG"; }
10
+
11
+ # 1. Kill npm view/list processes older than 30s
12
+ for pid in $(ps -eo pid,etime,args | grep -E "npm (view|list)" | grep -v grep | awk '{
13
+ split($2, t, /[:-]/);
14
+ if (length(t) >= 3) secs = t[1]*3600 + t[2]*60 + t[3];
15
+ else if (length(t) == 2) secs = t[1]*60 + t[2];
16
+ else secs = t[1];
17
+ if (secs > 30) print $1
18
+ }'); do
19
+ kill -9 "$pid" 2>/dev/null && KILLED=$((KILLED + 1))
20
+ done
21
+
22
+ # 2. Kill orphaned ldm install (parent is init/launchd)
23
+ for pid in $(pgrep -f "ldm install" 2>/dev/null); do
24
+ ppid=$(ps -p "$pid" -o ppid= 2>/dev/null | tr -d ' ')
25
+ if [ "$ppid" = "1" ]; then
26
+ kill -9 "$pid" 2>/dev/null && KILLED=$((KILLED + 1))
27
+ fi
28
+ done
29
+
30
+ # 3. Kill npm install null (should never exist)
31
+ for pid in $(pgrep -f "npm install null" 2>/dev/null); do
32
+ kill -9 "$pid" 2>/dev/null && KILLED=$((KILLED + 1))
33
+ done
34
+
35
+ # 4. Kill ldm install --version zombies older than 10s
36
+ for pid in $(ps -eo pid,etime,args | grep "ldm install --version" | grep -v grep | awk '{
37
+ split($2, t, /[:-]/);
38
+ if (length(t) >= 2) secs = t[1]*60 + t[2];
39
+ else secs = t[1];
40
+ if (secs > 10) print $1
41
+ }'); do
42
+ kill -9 "$pid" 2>/dev/null && KILLED=$((KILLED + 1))
43
+ done
44
+
45
+ # 5. Clean stale lockfile
46
+ LOCK="$HOME/.ldm/state/.ldm-install.lock"
47
+ if [ -f "$LOCK" ]; then
48
+ lock_pid=$(python3 -c "import json; print(json.load(open('$LOCK'))['pid'])" 2>/dev/null)
49
+ if [ -n "$lock_pid" ] && ! kill -0 "$lock_pid" 2>/dev/null; then
50
+ rm -f "$LOCK"
51
+ log "Cleaned stale lockfile (PID $lock_pid dead)"
52
+ KILLED=$((KILLED + 1))
53
+ fi
54
+ fi
55
+
56
+ # Log if we killed anything
57
+ if [ "$KILLED" -gt 0 ]; then
58
+ log "Killed $KILLED zombie process(es)"
59
+ fi
60
+
61
+ # Alert if node count is high
62
+ NODE_COUNT=$(pgrep -fl node 2>/dev/null | wc -l | tr -d ' ')
63
+ if [ "$NODE_COUNT" -gt 150 ]; then
64
+ log "WARNING: $NODE_COUNT node processes running"
65
+ fi
package/catalog.json CHANGED
@@ -37,7 +37,15 @@
37
37
  "cliMatches": ["crystal"],
38
38
  "recommended": true,
39
39
  "status": "stable",
40
- "postInstall": "crystal doctor"
40
+ "postInstall": "crystal doctor",
41
+ "installs": {
42
+ "cli": ["crystal"],
43
+ "mcp": ["crystal_search", "crystal_remember", "crystal_forget"],
44
+ "ocPlugin": "agent_end hook (conversation capture every turn)",
45
+ "ccHook": "Stop hook (crystal capture + daily log)",
46
+ "cron": "crystal-capture.sh (every 1 min, backup capture)",
47
+ "data": "~/.ldm/memory/crystal.db (shared vector DB)"
48
+ }
41
49
  },
42
50
  {
43
51
  "id": "wip-ai-devops-toolbox",
@@ -49,7 +57,13 @@
49
57
  "cliMatches": ["wip-release", "wip-repos", "wip-file-guard", "wip-install"],
50
58
  "recommended": false,
51
59
  "status": "stable",
52
- "postInstall": null
60
+ "postInstall": null,
61
+ "installs": {
62
+ "cli": ["wip-release", "wip-repos", "wip-file-guard", "wip-install"],
63
+ "mcp": ["wip-release", "wip-repos", "wip-license-hook", "wip-repo-permissions-hook"],
64
+ "ccHook": "branch-guard (blocks writes on main), file-guard (protects identity files), repo-permissions (workspace boundaries)",
65
+ "tools": "12 sub-tools: release, repos, file-guard, license-hook, license-guard, repo-permissions-hook, deploy-public, post-merge-rename, repo-init, readme-format, branch-guard, universal-installer"
66
+ }
53
67
  },
54
68
  {
55
69
  "id": "wip-1password",
@@ -61,7 +75,11 @@
61
75
  "cliMatches": [],
62
76
  "recommended": false,
63
77
  "status": "stable",
64
- "postInstall": null
78
+ "postInstall": null,
79
+ "installs": {
80
+ "mcp": ["op_list_items", "op_read_secret", "op_test"],
81
+ "ocPlugin": "op-secrets (headless 1Password via service account)"
82
+ }
65
83
  },
66
84
  {
67
85
  "id": "wip-markdown-viewer",
@@ -73,7 +91,11 @@
73
91
  "cliMatches": ["mdview"],
74
92
  "recommended": false,
75
93
  "status": "stable",
76
- "postInstall": null
94
+ "postInstall": null,
95
+ "installs": {
96
+ "cli": ["mdview"],
97
+ "web": "localhost:3000 (live reload markdown renderer)"
98
+ }
77
99
  },
78
100
  {
79
101
  "id": "wip-xai-grok",
@@ -85,7 +107,10 @@
85
107
  "cliMatches": [],
86
108
  "recommended": false,
87
109
  "status": "stable",
88
- "postInstall": null
110
+ "postInstall": null,
111
+ "installs": {
112
+ "mcp": ["grok_search_web", "grok_search_x", "grok_imagine", "grok_edit_image", "grok_generate_video", "grok_poll_video"]
113
+ }
89
114
  },
90
115
  {
91
116
  "id": "wip-xai-x",
@@ -97,7 +122,10 @@
97
122
  "cliMatches": [],
98
123
  "recommended": false,
99
124
  "status": "stable",
100
- "postInstall": null
125
+ "postInstall": null,
126
+ "installs": {
127
+ "mcp": ["x_fetch_post", "x_search_recent", "x_get_bookmarks", "x_get_user", "x_post_tweet", "x_delete_tweet", "x_upload_media"]
128
+ }
101
129
  },
102
130
  {
103
131
  "id": "openclaw",
@@ -109,7 +137,12 @@
109
137
  "cliMatches": ["openclaw"],
110
138
  "recommended": false,
111
139
  "status": "stable",
112
- "postInstall": null
140
+ "postInstall": null,
141
+ "installs": {
142
+ "cli": ["openclaw"],
143
+ "runtime": "24/7 agent gateway on localhost:18789",
144
+ "plugins": "Extensions system at ~/.openclaw/extensions/"
145
+ }
113
146
  },
114
147
  {
115
148
  "id": "dream-weaver-protocol",
@@ -121,19 +154,28 @@
121
154
  "cliMatches": [],
122
155
  "recommended": false,
123
156
  "status": "stable",
124
- "postInstall": null
157
+ "postInstall": null,
158
+ "installs": {
159
+ "skill": "SKILL.md (protocol documentation for agents)",
160
+ "docs": "19-page paper on memory consolidation. No runtime components."
161
+ }
125
162
  },
126
163
  {
127
164
  "id": "wip-bridge",
128
165
  "name": "Bridge",
129
166
  "description": "Cross-platform agent bridge. Enables Claude Code CLI to talk to OpenClaw CLI without a human in the middle.",
130
167
  "npm": null,
131
- "repo": "wipcomputer/wip-bridge",
168
+ "repo": "wipcomputer/wip-bridge-deprecated",
132
169
  "registryMatches": ["wip-bridge", "lesa-bridge"],
133
170
  "cliMatches": [],
134
171
  "recommended": false,
135
- "status": "stable",
136
- "postInstall": null
172
+ "status": "included",
173
+ "postInstall": null,
174
+ "installs": {
175
+ "note": "Included with LDM OS v0.3.0+. No separate install needed.",
176
+ "mcp": ["lesa_send_message", "lesa_check_inbox", "lesa_conversation_search", "lesa_memory_search", "lesa_read_workspace", "oc_skills_list"],
177
+ "cli": ["lesa"]
178
+ }
137
179
  }
138
180
  ]
139
181
  }
@@ -0,0 +1,423 @@
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 senderLabel = options?.senderLabel || "Claude Code";
101
+ const response = await fetch(`http://127.0.0.1:${port}/v1/chat/completions`, {
102
+ method: "POST",
103
+ headers: {
104
+ Authorization: `Bearer ${token}`,
105
+ "Content-Type": "application/json"
106
+ },
107
+ body: JSON.stringify({
108
+ model: agentId,
109
+ user: "main",
110
+ messages: [
111
+ {
112
+ role: "user",
113
+ content: `[${senderLabel}]: ${message}`
114
+ }
115
+ ]
116
+ })
117
+ });
118
+ if (!response.ok) {
119
+ const body = await response.text();
120
+ throw new Error(`Gateway returned ${response.status}: ${body}`);
121
+ }
122
+ const data = await response.json();
123
+ const reply = data.choices?.[0]?.message?.content;
124
+ if (!reply) {
125
+ throw new Error("No response content from gateway");
126
+ }
127
+ return reply;
128
+ }
129
+ async function getQueryEmbedding(text, apiKey, model = "text-embedding-3-small", dimensions = 1536) {
130
+ const response = await fetch("https://api.openai.com/v1/embeddings", {
131
+ method: "POST",
132
+ headers: {
133
+ Authorization: `Bearer ${apiKey}`,
134
+ "Content-Type": "application/json"
135
+ },
136
+ body: JSON.stringify({
137
+ input: [text],
138
+ model,
139
+ dimensions
140
+ })
141
+ });
142
+ if (!response.ok) {
143
+ const body = await response.text();
144
+ throw new Error(`OpenAI embeddings failed (${response.status}): ${body}`);
145
+ }
146
+ const data = await response.json();
147
+ return data.data[0].embedding;
148
+ }
149
+ function blobToEmbedding(blob) {
150
+ const floats = [];
151
+ for (let i = 0; i < blob.length; i += 4) {
152
+ floats.push(blob.readFloatLE(i));
153
+ }
154
+ return floats;
155
+ }
156
+ function cosineSimilarity(a, b) {
157
+ let dot = 0;
158
+ let normA = 0;
159
+ let normB = 0;
160
+ for (let i = 0; i < a.length; i++) {
161
+ dot += a[i] * b[i];
162
+ normA += a[i] * a[i];
163
+ normB += b[i] * b[i];
164
+ }
165
+ const denom = Math.sqrt(normA) * Math.sqrt(normB);
166
+ return denom === 0 ? 0 : dot / denom;
167
+ }
168
+ function recencyWeight(ageDays) {
169
+ return Math.max(0.5, 1 - ageDays * 0.01);
170
+ }
171
+ function freshnessLabel(ageDays) {
172
+ if (ageDays < 3) return "fresh";
173
+ if (ageDays < 7) return "recent";
174
+ if (ageDays < 14) return "aging";
175
+ return "stale";
176
+ }
177
+ async function searchConversations(config, query, limit = 5) {
178
+ const Database = (await import("better-sqlite3")).default;
179
+ if (!existsSync(config.dbPath)) {
180
+ throw new Error(`Database not found: ${config.dbPath}`);
181
+ }
182
+ const db = new Database(config.dbPath, { readonly: true });
183
+ db.pragma("journal_mode = WAL");
184
+ try {
185
+ const apiKey = resolveApiKey(config.openclawDir);
186
+ if (apiKey) {
187
+ const queryEmbedding = await getQueryEmbedding(
188
+ query,
189
+ apiKey,
190
+ config.embeddingModel,
191
+ config.embeddingDimensions
192
+ );
193
+ const rows = db.prepare(
194
+ `SELECT chunk_text, role, session_key, timestamp, embedding
195
+ FROM conversation_chunks
196
+ WHERE embedding IS NOT NULL
197
+ ORDER BY timestamp DESC
198
+ LIMIT 1000`
199
+ ).all();
200
+ const now = Date.now();
201
+ return rows.map((row) => {
202
+ const cosine = cosineSimilarity(queryEmbedding, blobToEmbedding(row.embedding));
203
+ const ageDays = (now - row.timestamp) / (1e3 * 60 * 60 * 24);
204
+ const weight = recencyWeight(ageDays);
205
+ return {
206
+ text: row.chunk_text,
207
+ role: row.role,
208
+ sessionKey: row.session_key,
209
+ date: new Date(row.timestamp).toISOString().split("T")[0],
210
+ similarity: cosine * weight,
211
+ recencyScore: weight,
212
+ freshness: freshnessLabel(ageDays)
213
+ };
214
+ }).sort((a, b) => (b.similarity || 0) - (a.similarity || 0)).slice(0, limit);
215
+ } else {
216
+ const rows = db.prepare(
217
+ `SELECT chunk_text, role, session_key, timestamp
218
+ FROM conversation_chunks
219
+ WHERE chunk_text LIKE ?
220
+ ORDER BY timestamp DESC
221
+ LIMIT ?`
222
+ ).all(`%${query}%`, limit);
223
+ return rows.map((row) => ({
224
+ text: row.chunk_text,
225
+ role: row.role,
226
+ sessionKey: row.session_key,
227
+ date: new Date(row.timestamp).toISOString().split("T")[0]
228
+ }));
229
+ }
230
+ } finally {
231
+ db.close();
232
+ }
233
+ }
234
+ function findMarkdownFiles(dir, maxDepth = 4, depth = 0) {
235
+ if (depth > maxDepth || !existsSync(dir)) return [];
236
+ const files = [];
237
+ for (const entry of readdirSync(dir, { withFileTypes: true })) {
238
+ if (entry.name.startsWith(".") || entry.name === "node_modules") continue;
239
+ const fullPath = join(dir, entry.name);
240
+ if (entry.isDirectory()) {
241
+ files.push(...findMarkdownFiles(fullPath, maxDepth, depth + 1));
242
+ } else if (entry.name.endsWith(".md")) {
243
+ files.push(fullPath);
244
+ }
245
+ }
246
+ return files;
247
+ }
248
+ function searchWorkspace(workspaceDir, query) {
249
+ const files = findMarkdownFiles(workspaceDir);
250
+ const queryLower = query.toLowerCase();
251
+ const words = queryLower.split(/\s+/).filter((w) => w.length > 2);
252
+ const results = [];
253
+ for (const filePath of files) {
254
+ try {
255
+ const content = readFileSync(filePath, "utf-8");
256
+ const contentLower = content.toLowerCase();
257
+ let score = 0;
258
+ for (const word of words) {
259
+ if (contentLower.indexOf(word) !== -1) score++;
260
+ }
261
+ if (score === 0) continue;
262
+ const lines = content.split("\n");
263
+ const excerpts = [];
264
+ for (let i = 0; i < lines.length && excerpts.length < 5; i++) {
265
+ const lineLower = lines[i].toLowerCase();
266
+ if (words.some((w) => lineLower.includes(w))) {
267
+ const start = Math.max(0, i - 1);
268
+ const end = Math.min(lines.length, i + 2);
269
+ excerpts.push(lines.slice(start, end).join("\n"));
270
+ }
271
+ }
272
+ results.push({ path: relative(workspaceDir, filePath), excerpts, score });
273
+ } catch {
274
+ }
275
+ }
276
+ return results.sort((a, b) => b.score - a.score).slice(0, 10);
277
+ }
278
+ function parseSkillFrontmatter(content) {
279
+ const match = content.match(/^---\n([\s\S]*?)\n---/);
280
+ if (!match) return {};
281
+ const yaml = match[1];
282
+ const name = yaml.match(/^name:\s*(.+)$/m)?.[1]?.trim();
283
+ const description = yaml.match(/^description:\s*(.+)$/m)?.[1]?.trim();
284
+ let emoji;
285
+ const emojiMatch = yaml.match(/"emoji":\s*"([^"]+)"/);
286
+ if (emojiMatch) emoji = emojiMatch[1];
287
+ let requires;
288
+ const requiresMatch = yaml.match(/"requires":\s*\{([^}]+)\}/);
289
+ if (requiresMatch) {
290
+ requires = {};
291
+ const pairs = requiresMatch[1].matchAll(/"(\w+)":\s*\[([^\]]*)\]/g);
292
+ for (const pair of pairs) {
293
+ const values = pair[2].match(/"([^"]+)"/g)?.map((v) => v.replace(/"/g, "")) || [];
294
+ requires[pair[1]] = values;
295
+ }
296
+ }
297
+ return { name, description, emoji, requires };
298
+ }
299
+ function discoverSkills(openclawDir) {
300
+ const skills = [];
301
+ const seen = /* @__PURE__ */ new Set();
302
+ const extensionsDir = join(openclawDir, "extensions");
303
+ if (!existsSync(extensionsDir)) return skills;
304
+ for (const ext of readdirSync(extensionsDir, { withFileTypes: true })) {
305
+ if (!ext.isDirectory() || ext.name.startsWith(".")) continue;
306
+ const extDir = join(extensionsDir, ext.name);
307
+ const searchDirs = [
308
+ { dir: join(extDir, "node_modules", "openclaw", "skills"), source: "builtin" },
309
+ { dir: join(extDir, "skills"), source: "custom" }
310
+ ];
311
+ for (const { dir, source } of searchDirs) {
312
+ if (!existsSync(dir)) continue;
313
+ for (const entry of readdirSync(dir, { withFileTypes: true })) {
314
+ if (!entry.isDirectory() || entry.name.startsWith(".")) continue;
315
+ const skillDir = join(dir, entry.name);
316
+ const skillMd = join(skillDir, "SKILL.md");
317
+ if (!existsSync(skillMd)) continue;
318
+ if (seen.has(entry.name)) continue;
319
+ seen.add(entry.name);
320
+ try {
321
+ const content = readFileSync(skillMd, "utf-8");
322
+ const frontmatter = parseSkillFrontmatter(content);
323
+ const scriptsDir = join(skillDir, "scripts");
324
+ let scripts = [];
325
+ let hasScripts = false;
326
+ if (existsSync(scriptsDir) && statSync(scriptsDir).isDirectory()) {
327
+ scripts = readdirSync(scriptsDir).filter(
328
+ (f) => f.endsWith(".sh") || f.endsWith(".py")
329
+ );
330
+ hasScripts = scripts.length > 0;
331
+ }
332
+ skills.push({
333
+ name: frontmatter.name || entry.name,
334
+ description: frontmatter.description || `OpenClaw skill: ${entry.name}`,
335
+ skillDir,
336
+ hasScripts,
337
+ scripts,
338
+ source,
339
+ emoji: frontmatter.emoji,
340
+ requires: frontmatter.requires
341
+ });
342
+ } catch {
343
+ }
344
+ }
345
+ }
346
+ }
347
+ return skills.sort((a, b) => a.name.localeCompare(b.name));
348
+ }
349
+ async function executeSkillScript(skillDir, scripts, scriptName, args) {
350
+ const scriptsDir = join(skillDir, "scripts");
351
+ let script;
352
+ if (scriptName) {
353
+ if (!scripts.includes(scriptName)) {
354
+ throw new Error(`Script "${scriptName}" not found. Available: ${scripts.join(", ")}`);
355
+ }
356
+ script = scriptName;
357
+ } else if (scripts.length === 1) {
358
+ script = scripts[0];
359
+ } else {
360
+ const sh = scripts.find((s) => s.endsWith(".sh"));
361
+ script = sh || scripts[0];
362
+ }
363
+ const scriptPath = join(scriptsDir, script);
364
+ const interpreter = script.endsWith(".py") ? "python3" : "bash";
365
+ try {
366
+ const { stdout, stderr } = await execAsync(
367
+ `${interpreter} "${scriptPath}" ${args}`,
368
+ {
369
+ env: { ...process.env },
370
+ timeout: 12e4,
371
+ maxBuffer: 10 * 1024 * 1024
372
+ // 10MB
373
+ }
374
+ );
375
+ return stdout || stderr || "(no output)";
376
+ } catch (err) {
377
+ const output = err.stdout || err.stderr || err.message;
378
+ throw new Error(`Script failed (exit ${err.code || "?"}): ${output}`);
379
+ }
380
+ }
381
+ function readWorkspaceFile(workspaceDir, filePath) {
382
+ const resolved = resolve(workspaceDir, filePath);
383
+ if (!resolved.startsWith(resolve(workspaceDir))) {
384
+ throw new Error("Path must be within workspace/");
385
+ }
386
+ if (!existsSync(resolved)) {
387
+ const dir = resolved.endsWith(".md") ? join(resolved, "..") : resolved;
388
+ if (existsSync(dir) && statSync(dir).isDirectory()) {
389
+ const files = findMarkdownFiles(dir, 1);
390
+ const listing = files.map((f) => relative(workspaceDir, f)).join("\n");
391
+ throw new Error(`File not found: ${filePath}
392
+
393
+ Available files:
394
+ ${listing}`);
395
+ }
396
+ throw new Error(`File not found: ${filePath}`);
397
+ }
398
+ return {
399
+ content: readFileSync(resolved, "utf-8"),
400
+ relativePath: relative(workspaceDir, resolved)
401
+ };
402
+ }
403
+
404
+ export {
405
+ LDM_ROOT,
406
+ resolveConfig,
407
+ resolveConfigMulti,
408
+ resolveApiKey,
409
+ resolveGatewayConfig,
410
+ pushInbox,
411
+ drainInbox,
412
+ inboxCount,
413
+ sendMessage,
414
+ getQueryEmbedding,
415
+ blobToEmbedding,
416
+ cosineSimilarity,
417
+ searchConversations,
418
+ findMarkdownFiles,
419
+ searchWorkspace,
420
+ discoverSkills,
421
+ executeSkillScript,
422
+ readWorkspaceFile
423
+ };
@@ -8,13 +8,13 @@ import {
8
8
  searchConversations,
9
9
  searchWorkspace,
10
10
  sendMessage
11
- } from "./chunk-KWGJCDGS.js";
11
+ } from "./chunk-LT4KM3AD.js";
12
12
 
13
13
  // cli.ts
14
14
  import { existsSync, statSync } from "fs";
15
15
  var config = resolveConfig();
16
16
  function usage() {
17
- console.log(`lesa-bridge: Claude Code CLI \u2194 OpenClaw TUI agent bridge
17
+ console.log(`wip-bridge: Claude Code CLI \u2194 OpenClaw TUI agent bridge
18
18
 
19
19
  Usage:
20
20
  lesa send <message> Send a message to the OpenClaw agent
@@ -119,7 +119,7 @@ async function main() {
119
119
  break;
120
120
  }
121
121
  case "status": {
122
- console.log(`lesa-bridge status`);
122
+ console.log(`wip-bridge status`);
123
123
  console.log(` OpenClaw dir: ${config.openclawDir}`);
124
124
  console.log(` Workspace: ${config.workspaceDir}`);
125
125
  console.log(` Database: ${config.dbPath}`);
@@ -128,7 +128,7 @@ async function main() {
128
128
  break;
129
129
  }
130
130
  case "diagnose": {
131
- console.log("lesa-bridge diagnose\n");
131
+ console.log("wip-bridge diagnose\n");
132
132
  let issues = 0;
133
133
  if (existsSync(config.openclawDir)) {
134
134
  console.log(` \u2713 OpenClaw dir exists: ${config.openclawDir}`);
@@ -17,7 +17,7 @@ import {
17
17
  searchConversations,
18
18
  searchWorkspace,
19
19
  sendMessage
20
- } from "./chunk-KWGJCDGS.js";
20
+ } from "./chunk-LT4KM3AD.js";
21
21
  export {
22
22
  LDM_ROOT,
23
23
  blobToEmbedding,
@@ -9,7 +9,7 @@ import {
9
9
  searchConversations,
10
10
  searchWorkspace,
11
11
  sendMessage
12
- } from "./chunk-KWGJCDGS.js";
12
+ } from "./chunk-LT4KM3AD.js";
13
13
 
14
14
  // mcp-server.ts
15
15
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
@@ -59,11 +59,11 @@ function startInboxServer(cfg) {
59
59
  timestamp: (/* @__PURE__ */ new Date()).toISOString()
60
60
  };
61
61
  const queued = pushInbox(msg);
62
- console.error(`lesa-bridge inbox: message from ${msg.from}`);
62
+ console.error(`wip-bridge inbox: message from ${msg.from}`);
63
63
  try {
64
64
  server.sendLoggingMessage({
65
65
  level: "info",
66
- logger: "lesa-bridge",
66
+ logger: "wip-bridge",
67
67
  data: `[OpenClaw \u2192 Claude Code] ${msg.from}: ${msg.message}`
68
68
  });
69
69
  } catch {
@@ -85,14 +85,14 @@ function startInboxServer(cfg) {
85
85
  res.end(JSON.stringify({ error: "not found" }));
86
86
  });
87
87
  httpServer.listen(cfg.inboxPort, "127.0.0.1", () => {
88
- console.error(`lesa-bridge inbox listening on 127.0.0.1:${cfg.inboxPort}`);
88
+ console.error(`wip-bridge inbox listening on 127.0.0.1:${cfg.inboxPort}`);
89
89
  });
90
90
  httpServer.on("error", (err) => {
91
- console.error(`lesa-bridge inbox server error: ${err.message}`);
91
+ console.error(`wip-bridge inbox server error: ${err.message}`);
92
92
  });
93
93
  }
94
94
  var server = new McpServer({
95
- name: "lesa-bridge",
95
+ name: "wip-bridge",
96
96
  version: "0.3.0"
97
97
  });
98
98
  server.registerTool(
@@ -264,7 +264,7 @@ function registerSkillTools(skills) {
264
264
  ${lines.join("\n")}` }] };
265
265
  }
266
266
  );
267
- console.error(`lesa-bridge: registered ${executableSkills.length} skill tools + oc_skills_list (${skills.length} total skills)`);
267
+ console.error(`wip-bridge: registered ${executableSkills.length} skill tools + oc_skills_list (${skills.length} total skills)`);
268
268
  }
269
269
  async function main() {
270
270
  startInboxServer(config);
@@ -272,11 +272,11 @@ async function main() {
272
272
  const skills = discoverSkills(config.openclawDir);
273
273
  registerSkillTools(skills);
274
274
  } catch (err) {
275
- console.error(`lesa-bridge: skill discovery failed: ${err.message}`);
275
+ console.error(`wip-bridge: skill discovery failed: ${err.message}`);
276
276
  }
277
277
  const transport = new StdioServerTransport();
278
278
  await server.connect(transport);
279
- console.error(`lesa-bridge MCP server running (openclaw: ${config.openclawDir})`);
279
+ console.error(`wip-bridge MCP server running (openclaw: ${config.openclawDir})`);
280
280
  }
281
281
  main().catch((error) => {
282
282
  console.error("Fatal error:", error);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wipcomputer/wip-ldm-os",
3
- "version": "0.4.12",
3
+ "version": "0.4.15",
4
4
  "type": "module",
5
5
  "description": "LDM OS: identity, memory, and sovereignty infrastructure for AI agents",
6
6
  "main": "src/boot/boot-hook.mjs",
@@ -185,9 +185,11 @@ export async function sendMessage(
185
185
  ): Promise<string> {
186
186
  const { token, port } = resolveGatewayConfig(openclawDir);
187
187
  const agentId = options?.agentId || "main";
188
- const user = options?.user || "claude-code";
189
188
  const senderLabel = options?.senderLabel || "Claude Code";
190
189
 
190
+ // Send user: "main" to route to the main session (agent:main:main).
191
+ // This ensures Parker sees CC's messages in the same stream as iMessage.
192
+ // The OpenClaw gateway treats user: "main" as "use the default session."
191
193
  const response = await fetch(`http://127.0.0.1:${port}/v1/chat/completions`, {
192
194
  method: "POST",
193
195
  headers: {
@@ -196,7 +198,7 @@ export async function sendMessage(
196
198
  },
197
199
  body: JSON.stringify({
198
200
  model: agentId,
199
- user,
201
+ user: "main",
200
202
  messages: [
201
203
  {
202
204
  role: "user",