@wipcomputer/wip-ldm-os 0.4.62 → 0.4.64
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 +136 -4
- package/catalog.json +2 -0
- package/dist/bridge/{chunk-I5FNBIR2.js → chunk-QZ4DNVJM.js} +1 -1
- package/dist/bridge/cli.js +1 -1
- package/dist/bridge/core.js +1 -1
- package/dist/bridge/mcp-server.js +1 -1
- package/package.json +1 -1
- package/scripts/ldm-backup.sh +18 -9
- package/src/bridge/core.ts +1 -1
package/SKILL.md
CHANGED
|
@@ -9,7 +9,7 @@ license: MIT
|
|
|
9
9
|
compatibility: Requires git, npm, node. Node.js 18+.
|
|
10
10
|
metadata:
|
|
11
11
|
display-name: "LDM OS"
|
|
12
|
-
version: "0.4.
|
|
12
|
+
version: "0.4.64"
|
|
13
13
|
homepage: "https://github.com/wipcomputer/wip-ldm-os"
|
|
14
14
|
author: "Parker Todd Brooks"
|
|
15
15
|
category: infrastructure
|
package/bin/ldm.js
CHANGED
|
@@ -226,6 +226,22 @@ function cleanDeadBackupTriggers() {
|
|
|
226
226
|
cleaned++;
|
|
227
227
|
}
|
|
228
228
|
|
|
229
|
+
// 3. Unload and disable com.wipcomputer.cc-watcher LaunchAgent
|
|
230
|
+
// Broken since Mar 24 migration (old iCloud path, wrong node path).
|
|
231
|
+
// The agent communication channel needs redesign, not screen automation.
|
|
232
|
+
const ccWatcherPlist = join(HOME, 'Library', 'LaunchAgents', 'com.wipcomputer.cc-watcher.plist');
|
|
233
|
+
const ccWatcherDisabled = ccWatcherPlist + '.disabled';
|
|
234
|
+
if (existsSync(ccWatcherPlist)) {
|
|
235
|
+
try { execSync(`launchctl unload "${ccWatcherPlist}" 2>/dev/null`, { stdio: 'pipe' }); } catch {}
|
|
236
|
+
try {
|
|
237
|
+
renameSync(ccWatcherPlist, ccWatcherDisabled);
|
|
238
|
+
} catch {
|
|
239
|
+
try { unlinkSync(ccWatcherPlist); } catch {}
|
|
240
|
+
}
|
|
241
|
+
console.log(' + Disabled dead LaunchAgent: com.wipcomputer.cc-watcher');
|
|
242
|
+
cleaned++;
|
|
243
|
+
}
|
|
244
|
+
|
|
229
245
|
return cleaned;
|
|
230
246
|
}
|
|
231
247
|
|
|
@@ -375,6 +391,86 @@ async function installCatalogComponent(c) {
|
|
|
375
391
|
console.log(` ✓ Installed ${c.name}`);
|
|
376
392
|
}
|
|
377
393
|
|
|
394
|
+
// ── Bridge deploy (#245) ──
|
|
395
|
+
// The bridge (src/bridge/) builds to dist/bridge/ and ships in the npm package.
|
|
396
|
+
// After `npm install -g`, the updated files live at the npm package location but
|
|
397
|
+
// never get copied to ~/.ldm/extensions/lesa-bridge/dist/. This function fixes that.
|
|
398
|
+
|
|
399
|
+
function deployBridge() {
|
|
400
|
+
const bridgeDest = join(LDM_EXTENSIONS, 'lesa-bridge', 'dist');
|
|
401
|
+
|
|
402
|
+
// Only deploy if the extension is installed
|
|
403
|
+
if (!existsSync(join(LDM_EXTENSIONS, 'lesa-bridge'))) return 0;
|
|
404
|
+
|
|
405
|
+
// Find the npm package bridge files. Try require.resolve first, fall back to known path.
|
|
406
|
+
let bridgeSrc = '';
|
|
407
|
+
try {
|
|
408
|
+
const pkgJson = join(__dirname, '..', 'dist', 'bridge');
|
|
409
|
+
if (existsSync(pkgJson)) {
|
|
410
|
+
bridgeSrc = pkgJson;
|
|
411
|
+
}
|
|
412
|
+
} catch {}
|
|
413
|
+
|
|
414
|
+
if (!bridgeSrc) {
|
|
415
|
+
// Fallback: check common global npm locations
|
|
416
|
+
const candidates = [
|
|
417
|
+
'/opt/homebrew/lib/node_modules/@wipcomputer/wip-ldm-os/dist/bridge',
|
|
418
|
+
join(HOME, '.npm-global/lib/node_modules/@wipcomputer/wip-ldm-os/dist/bridge'),
|
|
419
|
+
];
|
|
420
|
+
for (const c of candidates) {
|
|
421
|
+
if (existsSync(c)) {
|
|
422
|
+
bridgeSrc = c;
|
|
423
|
+
break;
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
if (!bridgeSrc || !existsSync(bridgeSrc)) return 0;
|
|
429
|
+
|
|
430
|
+
// Check if files differ before copying
|
|
431
|
+
let changed = false;
|
|
432
|
+
try {
|
|
433
|
+
const srcFiles = readdirSync(bridgeSrc).filter(f => f.endsWith('.js') || f.endsWith('.d.ts'));
|
|
434
|
+
for (const file of srcFiles) {
|
|
435
|
+
const srcPath = join(bridgeSrc, file);
|
|
436
|
+
const destPath = join(bridgeDest, file);
|
|
437
|
+
if (!existsSync(destPath)) {
|
|
438
|
+
changed = true;
|
|
439
|
+
break;
|
|
440
|
+
}
|
|
441
|
+
const srcContent = readFileSync(srcPath);
|
|
442
|
+
const destContent = readFileSync(destPath);
|
|
443
|
+
if (!srcContent.equals(destContent)) {
|
|
444
|
+
changed = true;
|
|
445
|
+
break;
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
} catch {
|
|
449
|
+
changed = true; // if comparison fails, copy anyway
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
if (!changed) return 0;
|
|
453
|
+
|
|
454
|
+
if (DRY_RUN) {
|
|
455
|
+
console.log(` + would deploy bridge files to ~/.ldm/extensions/lesa-bridge/dist/`);
|
|
456
|
+
return 0;
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
try {
|
|
460
|
+
mkdirSync(bridgeDest, { recursive: true });
|
|
461
|
+
const files = readdirSync(bridgeSrc).filter(f => f.endsWith('.js') || f.endsWith('.d.ts'));
|
|
462
|
+
for (const file of files) {
|
|
463
|
+
cpSync(join(bridgeSrc, file), join(bridgeDest, file));
|
|
464
|
+
}
|
|
465
|
+
console.log(` + bridge deployed to ~/.ldm/extensions/lesa-bridge/dist/ (${files.length} files)`);
|
|
466
|
+
installLog(`Bridge deployed: ${files.length} files to ~/.ldm/extensions/lesa-bridge/dist/`);
|
|
467
|
+
return files.length;
|
|
468
|
+
} catch (e) {
|
|
469
|
+
console.log(` ! bridge deploy failed: ${e.message}`);
|
|
470
|
+
return 0;
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
|
|
378
474
|
// ── ldm init ──
|
|
379
475
|
|
|
380
476
|
async function cmdInit() {
|
|
@@ -396,6 +492,32 @@ async function cmdInit() {
|
|
|
396
492
|
join(LDM_ROOT, 'hooks'),
|
|
397
493
|
];
|
|
398
494
|
|
|
495
|
+
// Migrate config-from-home.json into config.json (one-time merge)
|
|
496
|
+
// config-from-home.json held org identity (coAuthors, paths, agents, backup, etc.)
|
|
497
|
+
// config.json held runtime/harness info. Now they are one file.
|
|
498
|
+
const configFromHomePath = join(LDM_ROOT, 'config-from-home.json');
|
|
499
|
+
if (existsSync(configFromHomePath) && existsSync(join(LDM_ROOT, 'config.json'))) {
|
|
500
|
+
try {
|
|
501
|
+
const existing = JSON.parse(readFileSync(join(LDM_ROOT, 'config.json'), 'utf8'));
|
|
502
|
+
const fromHome = JSON.parse(readFileSync(configFromHomePath, 'utf8'));
|
|
503
|
+
// Merge: config-from-home.json wins where keys overlap (richer data)
|
|
504
|
+
const merged = { ...existing, ...fromHome };
|
|
505
|
+
// Preserve harnesses from existing config (not in config-from-home.json)
|
|
506
|
+
if (existing.harnesses) merged.harnesses = existing.harnesses;
|
|
507
|
+
// Preserve version and created from existing config
|
|
508
|
+
if (existing.version) merged.version = existing.version;
|
|
509
|
+
if (existing.created) merged.created = existing.created;
|
|
510
|
+
// Update timestamp
|
|
511
|
+
merged.updatedAt = new Date().toISOString();
|
|
512
|
+
writeFileSync(join(LDM_ROOT, 'config.json'), JSON.stringify(merged, null, 2) + '\n');
|
|
513
|
+
renameSync(configFromHomePath, configFromHomePath + '.migrated');
|
|
514
|
+
console.log(` + config-from-home.json merged into config.json`);
|
|
515
|
+
console.log(` + config-from-home.json renamed to config-from-home.json.migrated`);
|
|
516
|
+
} catch (e) {
|
|
517
|
+
console.log(` ! config-from-home.json migration failed: ${e.message}`);
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
|
|
399
521
|
// Scaffold per-agent memory dirs
|
|
400
522
|
try {
|
|
401
523
|
const config = JSON.parse(readFileSync(join(LDM_ROOT, 'config.json'), 'utf8'));
|
|
@@ -674,10 +796,9 @@ async function cmdInit() {
|
|
|
674
796
|
mkdirSync(docsDest, { recursive: true });
|
|
675
797
|
let docsCount = 0;
|
|
676
798
|
|
|
677
|
-
// Build template values from
|
|
678
|
-
//
|
|
679
|
-
const
|
|
680
|
-
const sc = settingsConfig;
|
|
799
|
+
// Build template values from ~/.ldm/config.json (unified config)
|
|
800
|
+
// Legacy: settings/config.json was a separate file, now merged into config.json
|
|
801
|
+
const sc = ldmConfig;
|
|
681
802
|
const lc = ldmConfig;
|
|
682
803
|
|
|
683
804
|
// Agents from settings config (rich objects with harness/machine/prefix)
|
|
@@ -771,6 +892,9 @@ async function cmdInit() {
|
|
|
771
892
|
}
|
|
772
893
|
}
|
|
773
894
|
|
|
895
|
+
// Deploy bridge files to ~/.ldm/extensions/lesa-bridge/dist/ (#245)
|
|
896
|
+
deployBridge();
|
|
897
|
+
|
|
774
898
|
// Clean up dead backup triggers (#207)
|
|
775
899
|
// Bug: three backup systems were competing. Only ai.openclaw.ldm-backup (3am) works.
|
|
776
900
|
// The old cron entry (LDMDevTools.app) and com.wipcomputer.daily-backup are dead.
|
|
@@ -1142,6 +1266,11 @@ async function cmdInstallCatalog() {
|
|
|
1142
1266
|
|
|
1143
1267
|
autoDetectExtensions();
|
|
1144
1268
|
|
|
1269
|
+
// Deploy bridge files after self-update or on every catalog install (#245)
|
|
1270
|
+
// After npm install -g, the new bridge files are in the npm package but not
|
|
1271
|
+
// in ~/.ldm/extensions/lesa-bridge/dist/. This copies them.
|
|
1272
|
+
deployBridge();
|
|
1273
|
+
|
|
1145
1274
|
const { detectSystemState, reconcileState, formatReconciliation } = await import('../lib/state.mjs');
|
|
1146
1275
|
const state = detectSystemState();
|
|
1147
1276
|
const reconciled = reconcileState(state);
|
|
@@ -1384,6 +1513,9 @@ async function cmdInstallCatalog() {
|
|
|
1384
1513
|
return matches.includes(name) || c.id === name;
|
|
1385
1514
|
});
|
|
1386
1515
|
|
|
1516
|
+
// Skip pinned components (e.g. OpenClaw). Upgrades must be explicit.
|
|
1517
|
+
if (catalogEntry?.pinned) continue;
|
|
1518
|
+
|
|
1387
1519
|
// Fallback: use repository.url from extension's package.json (#82)
|
|
1388
1520
|
let repoUrl = catalogEntry?.repo || null;
|
|
1389
1521
|
if (!repoUrl && extPkg?.repository) {
|
package/catalog.json
CHANGED
|
@@ -255,6 +255,8 @@
|
|
|
255
255
|
],
|
|
256
256
|
"recommended": false,
|
|
257
257
|
"status": "stable",
|
|
258
|
+
"pinned": true,
|
|
259
|
+
"pinnedReason": "OpenClaw is the runtime. Upgrades overwrite dist patches and can change API behavior. Use: ldm upgrade openclaw",
|
|
258
260
|
"postInstall": null,
|
|
259
261
|
"installs": {
|
|
260
262
|
"cli": [
|
package/dist/bridge/cli.js
CHANGED
package/dist/bridge/core.js
CHANGED
package/package.json
CHANGED
package/scripts/ldm-backup.sh
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/bin/bash
|
|
2
2
|
# ldm-backup.sh — Unified backup for LDM OS
|
|
3
|
-
# Backs up: ~/.ldm/, ~/.openclaw/, ~/.claude/,
|
|
3
|
+
# Backs up: ~/.ldm/, ~/.openclaw/, ~/.claude/, $WORKSPACE/
|
|
4
4
|
# Handles SQLite safely (sqlite3 .backup). Tars to iCloud for offsite.
|
|
5
5
|
#
|
|
6
6
|
# Source of truth: wip-ldm-os-private/scripts/ldm-backup.sh
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
# ldm-backup.sh --keep 14 # keep last 14 backups (default: 7)
|
|
13
13
|
# ldm-backup.sh --include-secrets # include ~/.ldm/secrets/
|
|
14
14
|
#
|
|
15
|
-
# Config: ~/.ldm/config.json (workspace path
|
|
15
|
+
# Config: ~/.ldm/config.json (workspace path, backup settings, iCloud path)
|
|
16
16
|
|
|
17
17
|
set -euo pipefail
|
|
18
18
|
|
|
@@ -45,20 +45,29 @@ if [ -z "$WORKSPACE" ]; then
|
|
|
45
45
|
echo "WARNING: No workspace in ~/.ldm/config.json. Skipping workspace backup."
|
|
46
46
|
fi
|
|
47
47
|
|
|
48
|
-
# Read
|
|
48
|
+
# Read org name from config (used for tar filename)
|
|
49
|
+
ORG=""
|
|
50
|
+
if [ -f "$LDM_HOME/config.json" ]; then
|
|
51
|
+
ORG=$(python3 -c "import json; print(json.load(open('$LDM_HOME/config.json')).get('org',''))" 2>/dev/null || true)
|
|
52
|
+
fi
|
|
53
|
+
if [ -z "$ORG" ]; then
|
|
54
|
+
ORG="workspace"
|
|
55
|
+
fi
|
|
56
|
+
|
|
57
|
+
# Read iCloud backup path from ~/.ldm/config.json
|
|
49
58
|
ICLOUD_BACKUP=""
|
|
50
|
-
if [ -
|
|
59
|
+
if [ -f "$LDM_HOME/config.json" ]; then
|
|
51
60
|
ICLOUD_BACKUP=$(python3 -c "
|
|
52
61
|
import json, os
|
|
53
|
-
c = json.load(open('$
|
|
62
|
+
c = json.load(open('$LDM_HOME/config.json'))
|
|
54
63
|
p = c.get('paths',{}).get('icloudBackup','')
|
|
55
64
|
print(os.path.expanduser(p))
|
|
56
65
|
" 2>/dev/null || true)
|
|
57
66
|
fi
|
|
58
67
|
|
|
59
|
-
# Read keep from
|
|
60
|
-
if [ -
|
|
61
|
-
CONFIG_KEEP=$(python3 -c "import json; print(json.load(open('$
|
|
68
|
+
# Read keep from ~/.ldm/config.json (override if set there)
|
|
69
|
+
if [ -f "$LDM_HOME/config.json" ]; then
|
|
70
|
+
CONFIG_KEEP=$(python3 -c "import json; print(json.load(open('$LDM_HOME/config.json')).get('backup',{}).get('keep',0))" 2>/dev/null || true)
|
|
62
71
|
if [ -n "$CONFIG_KEEP" ] && [ "$CONFIG_KEEP" -gt 0 ] 2>/dev/null; then
|
|
63
72
|
KEEP="$CONFIG_KEEP"
|
|
64
73
|
fi
|
|
@@ -211,7 +220,7 @@ if [ -n "$WORKSPACE" ] && [ -d "$WORKSPACE" ]; then
|
|
|
211
220
|
echo " ERROR: Workspace estimated at ${ESTIMATED_KB}KB (>10GB). Aborting tar to prevent disk fill."
|
|
212
221
|
echo " Check for large directories: du -sh $WORKSPACE/*/"
|
|
213
222
|
else
|
|
214
|
-
tar -cf "$DEST
|
|
223
|
+
tar -cf "$DEST/$ORG.tar" \
|
|
215
224
|
--exclude "node_modules" \
|
|
216
225
|
--exclude ".git/objects" \
|
|
217
226
|
--exclude ".DS_Store" \
|