@wipcomputer/wip-ldm-os 0.2.13 → 0.3.1

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.
@@ -0,0 +1,173 @@
1
+ /**
2
+ * lib/updates.mjs
3
+ * NPM update checker with cached manifest.
4
+ * Reads the extension registry, checks npm for newer versions,
5
+ * and writes results to ~/.ldm/state/available-updates.json.
6
+ * Zero external dependencies.
7
+ */
8
+
9
+ import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'node:fs';
10
+ import { join } from 'node:path';
11
+ import { execSync } from 'node:child_process';
12
+
13
+ const HOME = process.env.HOME || '';
14
+ const LDM_ROOT = join(HOME, '.ldm');
15
+ const LDM_EXTENSIONS = join(LDM_ROOT, 'extensions');
16
+ const STATE_DIR = join(LDM_ROOT, 'state');
17
+ const UPDATES_PATH = join(STATE_DIR, 'available-updates.json');
18
+ const REGISTRY_PATH = join(LDM_EXTENSIONS, 'registry.json');
19
+
20
+ // ── Helpers ──
21
+
22
+ function readJSON(path) {
23
+ try {
24
+ return JSON.parse(readFileSync(path, 'utf8'));
25
+ } catch {
26
+ return null;
27
+ }
28
+ }
29
+
30
+ function writeJSON(path, data) {
31
+ mkdirSync(join(path, '..'), { recursive: true });
32
+ writeFileSync(path, JSON.stringify(data, null, 2) + '\n');
33
+ }
34
+
35
+ // ── Semver comparison ──
36
+ // Copied from deploy.mjs to keep this module self-contained.
37
+
38
+ /**
39
+ * Compare two semver strings. Returns 1 if a > b, -1 if a < b, 0 if equal.
40
+ */
41
+ export function compareSemver(a, b) {
42
+ if (!a || !b) return 0;
43
+ const pa = a.split('.').map(Number);
44
+ const pb = b.split('.').map(Number);
45
+ for (let i = 0; i < 3; i++) {
46
+ const na = pa[i] || 0;
47
+ const nb = pb[i] || 0;
48
+ if (na > nb) return 1;
49
+ if (na < nb) return -1;
50
+ }
51
+ return 0;
52
+ }
53
+
54
+ // ── Catalog lookup ──
55
+
56
+ let _catalog = null;
57
+
58
+ function loadCatalog() {
59
+ if (_catalog) return _catalog;
60
+ try {
61
+ // Try the installed location first, then the repo-relative location
62
+ const paths = [
63
+ join(LDM_EXTENSIONS, 'wip-ldm-os', 'catalog.json'),
64
+ join(LDM_ROOT, 'catalog.json'),
65
+ ];
66
+ for (const p of paths) {
67
+ const data = readJSON(p);
68
+ if (data?.components) {
69
+ _catalog = data;
70
+ return data;
71
+ }
72
+ }
73
+ } catch {}
74
+ return { components: [] };
75
+ }
76
+
77
+ /**
78
+ * Resolve the npm package name for a registry entry.
79
+ * Checks the catalog for an npm field, or infers from the name.
80
+ * @param {string} name - Extension name from registry
81
+ * @param {Object} info - Registry entry info
82
+ * @returns {string|null} npm package name or null
83
+ */
84
+ export function resolveNpmName(name, info) {
85
+ // Check catalog for npm field
86
+ const catalog = loadCatalog();
87
+ for (const c of catalog.components || []) {
88
+ // Match by ID or by registryMatches
89
+ if (c.id === name || (c.registryMatches || []).includes(name)) {
90
+ if (c.npm) return c.npm;
91
+ }
92
+ }
93
+
94
+ // Check if the registry entry itself has a packageName
95
+ if (info?.packageName) return info.packageName;
96
+
97
+ // Check deployed package.json for npm name
98
+ if (info?.ldmPath) {
99
+ const pkg = readJSON(join(info.ldmPath, 'package.json'));
100
+ if (pkg?.name) return pkg.name;
101
+ }
102
+
103
+ return null;
104
+ }
105
+
106
+ // ── Update checking ──
107
+
108
+ /**
109
+ * Check for available updates by querying npm registry.
110
+ * Reads ~/.ldm/extensions/registry.json, checks each entry against npm,
111
+ * and writes results to ~/.ldm/state/available-updates.json.
112
+ * @returns {{ checkedAt: string, checked: number, updatesAvailable: number, updates: Array }}
113
+ */
114
+ export function checkForUpdates() {
115
+ const registry = readJSON(REGISTRY_PATH);
116
+ if (!registry?.extensions) {
117
+ return { checkedAt: new Date().toISOString(), checked: 0, updatesAvailable: 0, updates: [] };
118
+ }
119
+
120
+ const updates = [];
121
+ let checked = 0;
122
+
123
+ for (const [name, info] of Object.entries(registry.extensions)) {
124
+ const npmName = resolveNpmName(name, info);
125
+ if (!npmName) continue;
126
+
127
+ const currentVersion = info.version;
128
+ if (!currentVersion || currentVersion === 'unknown' || currentVersion === '?') continue;
129
+
130
+ try {
131
+ const result = execSync(`npm view ${npmName} version 2>/dev/null`, {
132
+ encoding: 'utf8',
133
+ timeout: 10000,
134
+ }).trim();
135
+
136
+ checked++;
137
+
138
+ if (result && compareSemver(result, currentVersion) > 0) {
139
+ updates.push({
140
+ name,
141
+ packageName: npmName,
142
+ currentVersion,
143
+ latestVersion: result,
144
+ checkedAt: new Date().toISOString(),
145
+ });
146
+ }
147
+ } catch {
148
+ // Skip on failure (network error, package not found, timeout, etc.)
149
+ }
150
+ }
151
+
152
+ const manifest = {
153
+ checkedAt: new Date().toISOString(),
154
+ checked,
155
+ updatesAvailable: updates.length,
156
+ updates,
157
+ };
158
+
159
+ // Write to cache
160
+ try {
161
+ writeJSON(UPDATES_PATH, manifest);
162
+ } catch {}
163
+
164
+ return manifest;
165
+ }
166
+
167
+ /**
168
+ * Read the cached update manifest without re-checking npm.
169
+ * @returns {Object|null} The cached manifest or null if not found
170
+ */
171
+ export function readUpdateManifest() {
172
+ return readJSON(UPDATES_PATH);
173
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wipcomputer/wip-ldm-os",
3
- "version": "0.2.13",
3
+ "version": "0.3.1",
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",
@@ -8,7 +8,12 @@
8
8
  "ldm": "bin/ldm.js",
9
9
  "wip-ldm-os": "bin/ldm.js",
10
10
  "ldm-scaffold": "bin/scaffold.sh",
11
- "ldm-boot-install": "src/boot/install-cli.js"
11
+ "ldm-boot-install": "src/boot/install-cli.js",
12
+ "lesa": "dist/bridge/cli.js"
13
+ },
14
+ "scripts": {
15
+ "build:bridge": "cd src/bridge && npm install && npx tsup core.ts mcp-server.ts cli.ts --format esm --dts --clean --outDir ../../dist/bridge",
16
+ "build": "npm run build:bridge"
12
17
  },
13
18
  "claudeCode": {
14
19
  "hook": {
@@ -22,7 +27,9 @@
22
27
  "src/",
23
28
  "lib/",
24
29
  "bin/",
30
+ "dist/bridge/",
25
31
  "templates/",
32
+ "docs/",
26
33
  "catalog.json",
27
34
  "SKILL.md"
28
35
  ],
@@ -5,7 +5,7 @@
5
5
  // Follows guard.mjs pattern: stdin JSON in, stdout JSON out, exit 0 always.
6
6
 
7
7
  import { readFileSync, readdirSync, existsSync } from 'node:fs';
8
- import { join, dirname, resolve } from 'node:path';
8
+ import { join, dirname, resolve, basename } from 'node:path';
9
9
  import { homedir } from 'node:os';
10
10
  import { fileURLToPath } from 'node:url';
11
11
 
@@ -212,6 +212,41 @@ async function main() {
212
212
  }
213
213
  }
214
214
 
215
+ // ── Register session (fire-and-forget) ──
216
+ try {
217
+ const { registerSession } = await import('../../lib/sessions.mjs');
218
+ const name = process.env.CLAUDE_SESSION_NAME || basename(input?.cwd || process.cwd()) || `session-${process.pid}`;
219
+ registerSession({
220
+ name,
221
+ agentId: config?.agentId || 'unknown',
222
+ pid: process.ppid || process.pid,
223
+ meta: { cwd: input?.cwd },
224
+ });
225
+ } catch {}
226
+
227
+ // ── Check pending messages ──
228
+ try {
229
+ const { readMessages } = await import('../../lib/messages.mjs');
230
+ const sessionName = process.env.CLAUDE_SESSION_NAME || basename(input?.cwd || process.cwd()) || 'unknown';
231
+ const pending = readMessages(sessionName, { markRead: false });
232
+ if (pending.length > 0) {
233
+ const msgLines = pending.map(m => ` [${m.type}] ${m.from}: ${m.body}`).join('\n');
234
+ sections.push(`== Pending Messages (${pending.length}) ==\n${msgLines}`);
235
+ }
236
+ } catch {}
237
+
238
+ // ── Check for updates ──
239
+ try {
240
+ const { readUpdateManifest } = await import('../../lib/updates.mjs');
241
+ const manifest = readUpdateManifest();
242
+ if (manifest?.updatesAvailable > 0) {
243
+ const updateLines = manifest.updates
244
+ .map(u => ` ${u.name}: ${u.currentVersion} -> ${u.latestVersion}`)
245
+ .join('\n');
246
+ sections.push(`== Updates Available (${manifest.updatesAvailable}) ==\n${updateLines}\nRun: ldm install`);
247
+ }
248
+ } catch {}
249
+
215
250
  const elapsed = Date.now() - startTime;
216
251
  const footer = `== Boot complete. Loaded ${loaded.length}/9 files in ${elapsed}ms. ==`;
217
252
  if (skipped.length > 0) {
@@ -0,0 +1,245 @@
1
+ #!/usr/bin/env node
2
+ // lesa-bridge/cli.ts: CLI interface.
3
+ // lesa send "message", lesa inbox, lesa search "query", lesa read <file>
4
+
5
+ import { existsSync, statSync } from "node:fs";
6
+ import {
7
+ resolveConfig,
8
+ resolveGatewayConfig,
9
+ resolveApiKey,
10
+ sendMessage,
11
+ searchConversations,
12
+ searchWorkspace,
13
+ readWorkspaceFile,
14
+ discoverSkills,
15
+ } from "./core.js";
16
+
17
+ const config = resolveConfig();
18
+
19
+ function usage(): void {
20
+ console.log(`lesa-bridge: Claude Code CLI ↔ OpenClaw TUI agent bridge
21
+
22
+ Usage:
23
+ lesa send <message> Send a message to the OpenClaw agent
24
+ lesa search <query> Semantic search over conversation history
25
+ lesa memory <query> Keyword search across workspace files
26
+ lesa read <path> Read a workspace file (relative to workspace/)
27
+ lesa status Show bridge configuration
28
+ lesa diagnose Check gateway, inbox, DB, skills health
29
+ lesa help Show this help
30
+
31
+ Examples:
32
+ lesa send "What are you working on?"
33
+ lesa search "API key resolution"
34
+ lesa memory "compaction"
35
+ lesa read MEMORY.md
36
+ lesa read memory/2026-02-10.md`);
37
+ }
38
+
39
+ async function main(): Promise<void> {
40
+ const args = process.argv.slice(2);
41
+ const command = args[0];
42
+
43
+ if (!command || command === "help" || command === "--help" || command === "-h") {
44
+ usage();
45
+ process.exit(0);
46
+ }
47
+
48
+ const arg = args.slice(1).join(" ");
49
+
50
+ switch (command) {
51
+ case "send": {
52
+ if (!arg) {
53
+ console.error("Error: message required. Usage: lesa send <message>");
54
+ process.exit(1);
55
+ }
56
+ try {
57
+ const reply = await sendMessage(config.openclawDir, arg);
58
+ console.log(reply);
59
+ } catch (err: any) {
60
+ console.error(`Error: ${err.message}`);
61
+ process.exit(1);
62
+ }
63
+ break;
64
+ }
65
+
66
+ case "search": {
67
+ if (!arg) {
68
+ console.error("Error: query required. Usage: lesa search <query>");
69
+ process.exit(1);
70
+ }
71
+ try {
72
+ const results = await searchConversations(config, arg);
73
+ if (results.length === 0) {
74
+ console.log("No results found.");
75
+ } else {
76
+ const icon: Record<string, string> = { fresh: "🟢", recent: "🟡", aging: "🟠", stale: "🔴" };
77
+ for (const [i, r] of results.entries()) {
78
+ const sim = r.similarity !== undefined ? ` (${(r.similarity * 100).toFixed(1)}%)` : "";
79
+ const fresh = r.freshness ? ` ${icon[r.freshness]} ${r.freshness}` : "";
80
+ console.log(`[${i + 1}]${sim}${fresh} ${r.sessionKey} ${r.date}`);
81
+ console.log(r.text);
82
+ if (i < results.length - 1) console.log("\n---\n");
83
+ }
84
+ }
85
+ } catch (err: any) {
86
+ console.error(`Error: ${err.message}`);
87
+ process.exit(1);
88
+ }
89
+ break;
90
+ }
91
+
92
+ case "memory": {
93
+ if (!arg) {
94
+ console.error("Error: query required. Usage: lesa memory <query>");
95
+ process.exit(1);
96
+ }
97
+ try {
98
+ const results = searchWorkspace(config.workspaceDir, arg);
99
+ if (results.length === 0) {
100
+ console.log(`No workspace files matched "${arg}".`);
101
+ } else {
102
+ for (const r of results) {
103
+ console.log(`### ${r.path}`);
104
+ for (const excerpt of r.excerpts) {
105
+ console.log(` ${excerpt.replace(/\n/g, "\n ")}`);
106
+ }
107
+ console.log();
108
+ }
109
+ }
110
+ } catch (err: any) {
111
+ console.error(`Error: ${err.message}`);
112
+ process.exit(1);
113
+ }
114
+ break;
115
+ }
116
+
117
+ case "read": {
118
+ if (!arg) {
119
+ console.error("Error: path required. Usage: lesa read <path>");
120
+ process.exit(1);
121
+ }
122
+ try {
123
+ const result = readWorkspaceFile(config.workspaceDir, arg);
124
+ console.log(result.content);
125
+ } catch (err: any) {
126
+ console.error(err.message);
127
+ process.exit(1);
128
+ }
129
+ break;
130
+ }
131
+
132
+ case "status": {
133
+ console.log(`lesa-bridge status`);
134
+ console.log(` OpenClaw dir: ${config.openclawDir}`);
135
+ console.log(` Workspace: ${config.workspaceDir}`);
136
+ console.log(` Database: ${config.dbPath}`);
137
+ console.log(` Inbox port: ${config.inboxPort}`);
138
+ console.log(` Embedding: ${config.embeddingModel} (${config.embeddingDimensions}d)`);
139
+ break;
140
+ }
141
+
142
+ case "diagnose": {
143
+ console.log("lesa-bridge diagnose\n");
144
+ let issues = 0;
145
+
146
+ // 1. OpenClaw dir
147
+ if (existsSync(config.openclawDir)) {
148
+ console.log(` ✓ OpenClaw dir exists: ${config.openclawDir}`);
149
+ } else {
150
+ console.log(` ✗ OpenClaw dir missing: ${config.openclawDir}`);
151
+ issues++;
152
+ }
153
+
154
+ // 2. Gateway config + connectivity
155
+ try {
156
+ const gw = resolveGatewayConfig(config.openclawDir);
157
+ console.log(` ✓ Gateway config found (port ${gw.port}, token present)`);
158
+
159
+ try {
160
+ const resp = await fetch(`http://127.0.0.1:${gw.port}/health`, { signal: AbortSignal.timeout(3000) });
161
+ if (resp.ok) {
162
+ console.log(` ✓ Gateway responding on port ${gw.port}`);
163
+ } else {
164
+ console.log(` ✗ Gateway returned ${resp.status}`);
165
+ issues++;
166
+ }
167
+ } catch {
168
+ console.log(` ✗ Gateway not reachable on port ${gw.port}`);
169
+ issues++;
170
+ }
171
+ } catch (err: any) {
172
+ console.log(` ✗ Gateway config: ${err.message}`);
173
+ issues++;
174
+ }
175
+
176
+ // 3. Inbox endpoint
177
+ try {
178
+ const resp = await fetch(`http://127.0.0.1:${config.inboxPort}/status`, { signal: AbortSignal.timeout(3000) });
179
+ const data = await resp.json() as { ok: boolean; pending: number };
180
+ if (data.ok) {
181
+ console.log(` ✓ Inbox endpoint responding (${data.pending} pending)`);
182
+ } else {
183
+ console.log(` ✗ Inbox endpoint returned unexpected response`);
184
+ issues++;
185
+ }
186
+ } catch {
187
+ console.log(` - Inbox not running (normal if MCP server isn't started)`);
188
+ }
189
+
190
+ // 4. Embeddings DB
191
+ if (existsSync(config.dbPath)) {
192
+ const stats = statSync(config.dbPath);
193
+ const sizeMB = (stats.size / 1024 / 1024).toFixed(1);
194
+ console.log(` ✓ Embeddings DB exists (${sizeMB} MB)`);
195
+ } else {
196
+ console.log(` ✗ Embeddings DB missing: ${config.dbPath}`);
197
+ issues++;
198
+ }
199
+
200
+ // 5. API key
201
+ const apiKey = resolveApiKey(config.openclawDir);
202
+ if (apiKey) {
203
+ console.log(` ✓ OpenAI API key found (semantic search enabled)`);
204
+ } else {
205
+ console.log(` - No OpenAI API key (text search fallback)`);
206
+ }
207
+
208
+ // 6. Skills
209
+ try {
210
+ const skills = discoverSkills(config.openclawDir);
211
+ const executable = skills.filter(s => s.hasScripts).length;
212
+ console.log(` ✓ Skills discovered: ${skills.length} total, ${executable} executable`);
213
+ } catch (err: any) {
214
+ console.log(` ✗ Skill discovery failed: ${err.message}`);
215
+ issues++;
216
+ }
217
+
218
+ // 7. Workspace
219
+ if (existsSync(config.workspaceDir)) {
220
+ console.log(` ✓ Workspace dir exists`);
221
+ } else {
222
+ console.log(` ✗ Workspace dir missing: ${config.workspaceDir}`);
223
+ issues++;
224
+ }
225
+
226
+ console.log();
227
+ if (issues === 0) {
228
+ console.log(" All checks passed.");
229
+ } else {
230
+ console.log(` ${issues} issue(s) found.`);
231
+ }
232
+ break;
233
+ }
234
+
235
+ default:
236
+ console.error(`Unknown command: ${command}`);
237
+ usage();
238
+ process.exit(1);
239
+ }
240
+ }
241
+
242
+ main().catch((err) => {
243
+ console.error(`Fatal: ${err.message}`);
244
+ process.exit(1);
245
+ });