@wipcomputer/wip-ldm-os 0.4.11 → 0.4.14
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 +89 -0
- package/bin/process-monitor.sh +65 -0
- package/bin/wip-install.js +17 -1
- package/catalog.json +53 -11
- package/lib/deploy.mjs +12 -2
- package/package.json +1 -1
package/SKILL.md
CHANGED
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/bin/wip-install.js
CHANGED
|
@@ -4,5 +4,21 @@
|
|
|
4
4
|
// If not, installs LDM OS from npm, then delegates.
|
|
5
5
|
// Replaces the standalone 700-line install.js from wip-ai-devops-toolbox.
|
|
6
6
|
|
|
7
|
+
// Handle --version directly to prevent recursive spawn loop (#70)
|
|
8
|
+
// detectCLIBinaries() calls wip-install --version which would trigger
|
|
9
|
+
// ldm install which triggers npm checks which spawn more processes.
|
|
10
|
+
const args = process.argv.slice(2);
|
|
11
|
+
if (args.includes('--version') || args.includes('-v')) {
|
|
12
|
+
const { readFileSync } = await import('node:fs');
|
|
13
|
+
const { join, dirname } = await import('node:path');
|
|
14
|
+
const { fileURLToPath } = await import('node:url');
|
|
15
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
16
|
+
try {
|
|
17
|
+
const pkg = JSON.parse(readFileSync(join(__dirname, '..', 'package.json'), 'utf8'));
|
|
18
|
+
console.log(pkg.version);
|
|
19
|
+
} catch { console.log('unknown'); }
|
|
20
|
+
process.exit(0);
|
|
21
|
+
}
|
|
22
|
+
|
|
7
23
|
import { run } from '../lib/bootstrap.mjs';
|
|
8
|
-
run(
|
|
24
|
+
run(args);
|
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": "
|
|
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
|
}
|
package/lib/deploy.mjs
CHANGED
|
@@ -137,7 +137,7 @@ function findExistingInstalls(toolName, pkg, ocPluginConfig) {
|
|
|
137
137
|
|
|
138
138
|
function runBuildIfNeeded(repoPath) {
|
|
139
139
|
const pkg = readJSON(join(repoPath, 'package.json'));
|
|
140
|
-
if (!pkg) return;
|
|
140
|
+
if (!pkg) return true;
|
|
141
141
|
|
|
142
142
|
const hasBuildScript = !!pkg.scripts?.build;
|
|
143
143
|
const hasTsConfig = existsSync(join(repoPath, 'tsconfig.json'));
|
|
@@ -155,8 +155,10 @@ function runBuildIfNeeded(repoPath) {
|
|
|
155
155
|
ok(`Build complete`);
|
|
156
156
|
} catch (e) {
|
|
157
157
|
fail(`Build failed: ${e.stderr?.toString()?.slice(0, 200) || e.message}`);
|
|
158
|
+
return false;
|
|
158
159
|
}
|
|
159
160
|
}
|
|
161
|
+
return true;
|
|
160
162
|
}
|
|
161
163
|
|
|
162
164
|
// ── Version comparison (fix #7) ──
|
|
@@ -229,13 +231,21 @@ function safeDeployDir(repoPath, destDir, name) {
|
|
|
229
231
|
} catch {}
|
|
230
232
|
}
|
|
231
233
|
|
|
232
|
-
// 3. Verify
|
|
234
|
+
// 3. Verify staged copy is valid
|
|
233
235
|
if (!existsSync(join(tempPath, 'package.json'))) {
|
|
234
236
|
fail(`Deploy verification failed: no package.json in staged copy`);
|
|
235
237
|
rmSync(tempPath, { recursive: true, force: true });
|
|
236
238
|
return false;
|
|
237
239
|
}
|
|
238
240
|
|
|
241
|
+
// 3b. If source has a build script, verify dist/ exists (#69)
|
|
242
|
+
const stagePkg = readJSON(join(tempPath, 'package.json'));
|
|
243
|
+
if (stagePkg?.scripts?.build && !existsSync(join(tempPath, 'dist'))) {
|
|
244
|
+
fail(`Deploy aborted: ${name} has a build script but no dist/. Build failed or was skipped.`);
|
|
245
|
+
rmSync(tempPath, { recursive: true, force: true });
|
|
246
|
+
return false;
|
|
247
|
+
}
|
|
248
|
+
|
|
239
249
|
// 4. Swap: old -> backup, temp -> final
|
|
240
250
|
mkdirSync(destDir, { recursive: true });
|
|
241
251
|
if (existsSync(finalPath)) {
|