@wipcomputer/wip-ldm-os 0.4.81 → 0.4.83
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/README.md +2 -0
- package/SKILL.md +1 -1
- package/bin/ldm.js +152 -14
- package/docs/skills/README.md +2 -0
- package/lib/bin-manifest.mjs +257 -0
- package/package.json +36 -2
- package/scripts/test-bin-manifest.mjs +282 -0
- package/scripts/test-doctor-cron-target.mjs +172 -0
- package/scripts/test-installer-update-tracks.mjs +39 -0
- package/scripts/test-ldm-install-preserves-foreign-bin.mjs +112 -0
- package/scripts/validate-bin-manifest.mjs +41 -0
- package/shared/docs/dev-guide-wipcomputerinc.md.tmpl +3 -1
- package/shared/docs/how-install-works.md.tmpl +2 -0
- package/shared/docs/how-releases-work.md.tmpl +9 -3
- package/shared/rules/release-pipeline.md +4 -2
- package/src/hosted-mcp/app/codex-remote-control/index.html +254 -0
- package/src/hosted-mcp/app/login.html +176 -0
- package/src/hosted-mcp/app/pair.html +118 -0
- package/src/hosted-mcp/package-lock.json +22 -0
- package/src/hosted-mcp/package.json +1 -0
- package/src/hosted-mcp/server.mjs +418 -10
package/README.md
CHANGED
|
@@ -96,6 +96,8 @@ The OS connects your AIs. Add-ons are what they actually use. Each one is a full
|
|
|
96
96
|
|
|
97
97
|
**OpenClaw**
|
|
98
98
|
- Open-source agent runtime. Run AI agents 24/7 with identity, memory, and tool access. The existence proof for LDM OS.
|
|
99
|
+
- WIP contributions accepted upstream: `before_message_write` plugin hook (#18197), Codex app-server final chat events (#70815 -> maintainer PR #71293), memory-core seed cache streaming/yield (#73067 -> maintainer PR #73118), and fallback vector top-K streaming (#73069 -> maintainer PR #73100).
|
|
100
|
+
- Submitted / superseded: symlink plugin discovery fix (#45744; bug confirmed, superseded by #69971, not landed as submitted).
|
|
99
101
|
- [Read more about OpenClaw](https://github.com/openclaw/openclaw)
|
|
100
102
|
|
|
101
103
|
[See all skills](docs/skills/README.md)
|
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.83"
|
|
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
|
@@ -1935,6 +1935,36 @@ async function cmdInstallCatalog() {
|
|
|
1935
1935
|
console.log(` + Migrated ${migrated} registry entries to v2 format (source info added)`);
|
|
1936
1936
|
}
|
|
1937
1937
|
|
|
1938
|
+
// Aggregate the bin ownership manifest BEFORE seedLocalCatalog,
|
|
1939
|
+
// deployBridge, deployScripts, and the heal walk run. If two
|
|
1940
|
+
// declarers claim the same file in ~/.ldm/bin/ we cannot safely
|
|
1941
|
+
// decide ownership, so install must abort before catalog.json,
|
|
1942
|
+
// bridge files, or any ~/.ldm/bin/ shim is written. See
|
|
1943
|
+
// ai/product/plans-prds/current/2026-04-28--cc-mini--ldm-bin-ownership-manifest-design.md
|
|
1944
|
+
const { aggregateBinManifest, healBinManifest } = await import('../lib/bin-manifest.mjs');
|
|
1945
|
+
const ldmCliRoot = join(__dirname, '..');
|
|
1946
|
+
const binManifestRegistry = readJSON(REGISTRY_PATH) || { extensions: {} };
|
|
1947
|
+
const manifest = aggregateBinManifest({
|
|
1948
|
+
ldmCliRoot,
|
|
1949
|
+
extensionsRoot: LDM_EXTENSIONS,
|
|
1950
|
+
binDir: join(LDM_ROOT, 'bin'),
|
|
1951
|
+
registry: binManifestRegistry,
|
|
1952
|
+
});
|
|
1953
|
+
if (manifest.conflicts.length > 0) {
|
|
1954
|
+
console.log('');
|
|
1955
|
+
console.log(' x bin manifest conflict ... aborting before seedLocalCatalog/deployBridge/deployScripts run:');
|
|
1956
|
+
for (const c of manifest.conflicts) {
|
|
1957
|
+
console.log(` "${c.name}" claimed by ${c.declarers.length} declarers:`);
|
|
1958
|
+
for (const d of c.declarers) {
|
|
1959
|
+
console.log(` ${d.declarer}: ${d.sourcePath.replace(HOME, '~')}`);
|
|
1960
|
+
}
|
|
1961
|
+
}
|
|
1962
|
+
console.log('');
|
|
1963
|
+
console.log(' Resolve ownership in the package manifests, then re-run.');
|
|
1964
|
+
installLog(`ldm install aborted: ${manifest.conflicts.length} bin manifest conflict(s)`);
|
|
1965
|
+
process.exit(1);
|
|
1966
|
+
}
|
|
1967
|
+
|
|
1938
1968
|
// Seed local catalog if missing (#262)
|
|
1939
1969
|
if (seedLocalCatalog()) {
|
|
1940
1970
|
console.log(` + catalog.json seeded to ~/.ldm/catalog.json`);
|
|
@@ -1950,6 +1980,24 @@ async function cmdInstallCatalog() {
|
|
|
1950
1980
|
deployDocs();
|
|
1951
1981
|
deployRules();
|
|
1952
1982
|
|
|
1983
|
+
// Manifest-driven self-heal. deployScripts() above wrote LDM CLI's own
|
|
1984
|
+
// *.sh files; this pass covers the rest of the manifest (LDM CLI files
|
|
1985
|
+
// deployed elsewhere, e.g. process-monitor.sh, plus extension-owned
|
|
1986
|
+
// shims like crystal-capture.sh). Read-only entries that already match
|
|
1987
|
+
// the manifest are no-ops.
|
|
1988
|
+
if (manifest.entries.length > 0) {
|
|
1989
|
+
const heal = healBinManifest(manifest.entries, { heal: !DRY_RUN });
|
|
1990
|
+
for (const e of heal.healed) {
|
|
1991
|
+
console.log(` + Restored ${e.name} from ${e.sourcePath.replace(HOME, '~')} (declarer: ${e.declarer})`);
|
|
1992
|
+
}
|
|
1993
|
+
for (const f of heal.failed) {
|
|
1994
|
+
console.log(` ! ${f.entry.name}: ${f.reason} (declarer: ${f.entry.declarer})`);
|
|
1995
|
+
}
|
|
1996
|
+
if (heal.ok.length > 0 && heal.healed.length === 0 && heal.failed.length === 0) {
|
|
1997
|
+
console.log(` + bin manifest: ${heal.ok.length} entr${heal.ok.length === 1 ? 'y' : 'ies'} verified`);
|
|
1998
|
+
}
|
|
1999
|
+
}
|
|
2000
|
+
|
|
1953
2001
|
// Check backup configuration
|
|
1954
2002
|
checkBackupHealth();
|
|
1955
2003
|
|
|
@@ -2334,7 +2382,11 @@ async function cmdInstallCatalog() {
|
|
|
2334
2382
|
const currentVersion = matchEntry?.installed?.version || matchEntry?.version || '?';
|
|
2335
2383
|
|
|
2336
2384
|
try {
|
|
2337
|
-
const
|
|
2385
|
+
const npmTag = ALPHA_FLAG ? 'alpha' : BETA_FLAG ? 'beta' : 'latest';
|
|
2386
|
+
const npmViewCmd = npmTag === 'latest'
|
|
2387
|
+
? `npm view ${comp.npm} version 2>/dev/null`
|
|
2388
|
+
: `npm view ${comp.npm} dist-tags.${npmTag} 2>/dev/null`;
|
|
2389
|
+
const latest = execSync(npmViewCmd, {
|
|
2338
2390
|
encoding: 'utf8', timeout: 10000,
|
|
2339
2391
|
}).trim();
|
|
2340
2392
|
if (latest && semverNewer(latest, currentVersion)) {
|
|
@@ -2608,24 +2660,18 @@ async function cmdInstallCatalog() {
|
|
|
2608
2660
|
execSync(`ldm install ${installSource}`, { stdio: 'inherit' });
|
|
2609
2661
|
updated++;
|
|
2610
2662
|
|
|
2611
|
-
// For parent packages,
|
|
2663
|
+
// For parent packages, installFromPath already refreshes each sub-tool
|
|
2664
|
+
// registry entry with that sub-tool's own package version. Do not stamp
|
|
2665
|
+
// the parent package version onto sub-tools; their versions intentionally
|
|
2666
|
+
// differ from the toolbox aggregate version.
|
|
2612
2667
|
if (entry.isParent && entry.registryMatches) {
|
|
2613
2668
|
const registry = readJSON(REGISTRY_PATH);
|
|
2614
2669
|
if (registry?.extensions) {
|
|
2615
|
-
|
|
2670
|
+
let refreshed = 0;
|
|
2616
2671
|
for (const subTool of entry.registryMatches) {
|
|
2617
|
-
if (registry.extensions[subTool])
|
|
2618
|
-
registry.extensions[subTool].version = entry.latestVersion;
|
|
2619
|
-
registry.extensions[subTool].updatedAt = now;
|
|
2620
|
-
// Also update v2 installed block
|
|
2621
|
-
if (registry.extensions[subTool].installed) {
|
|
2622
|
-
registry.extensions[subTool].installed.version = entry.latestVersion;
|
|
2623
|
-
registry.extensions[subTool].installed.updatedAt = now;
|
|
2624
|
-
}
|
|
2625
|
-
}
|
|
2672
|
+
if (registry.extensions[subTool]) refreshed++;
|
|
2626
2673
|
}
|
|
2627
|
-
|
|
2628
|
-
console.log(` + Updated registry for ${entry.registryMatches.length} sub-tools`);
|
|
2674
|
+
console.log(` + Refreshed registry for ${refreshed}/${entry.registryMatches.length} sub-tools`);
|
|
2629
2675
|
}
|
|
2630
2676
|
}
|
|
2631
2677
|
} catch (e) {
|
|
@@ -2949,6 +2995,98 @@ async function cmdDoctor() {
|
|
|
2949
2995
|
}
|
|
2950
2996
|
}
|
|
2951
2997
|
|
|
2998
|
+
// 3c. Cron target health (parallel to crystal doctor's checkCaptureShim)
|
|
2999
|
+
//
|
|
3000
|
+
// When an extension wires a shim into ~/.ldm/bin/ via cron, the cron
|
|
3001
|
+
// line is sticky: it keeps firing even if the shim file goes missing
|
|
3002
|
+
// for any reason. crystal doctor catches this for crystal-capture.sh,
|
|
3003
|
+
// but any cron line referencing ~/.ldm/bin/<file> is at the same risk
|
|
3004
|
+
// class. Walk the crontab and surface broken bin-targeted entries
|
|
3005
|
+
// explicitly. Read-only by default; --fix restores known shims from
|
|
3006
|
+
// their extension dist when the canonical source is present on disk.
|
|
3007
|
+
{
|
|
3008
|
+
const ldmBinPrefix = join(LDM_ROOT, 'bin') + '/';
|
|
3009
|
+
let crontab = '';
|
|
3010
|
+
try {
|
|
3011
|
+
crontab = execSync('crontab -l 2>/dev/null', { encoding: 'utf-8' });
|
|
3012
|
+
} catch {}
|
|
3013
|
+
function expandTilde(p) {
|
|
3014
|
+
return p.startsWith('~') ? join(HOME, p.slice(1)) : p;
|
|
3015
|
+
}
|
|
3016
|
+
function extractCronTarget(line) {
|
|
3017
|
+
const trimmed = line.trim();
|
|
3018
|
+
if (!trimmed.startsWith('*')) return null;
|
|
3019
|
+
const tokens = trimmed.split(/\s+/);
|
|
3020
|
+
if (tokens.length < 6) return null;
|
|
3021
|
+
const expanded = expandTilde(tokens[5]);
|
|
3022
|
+
return expanded.startsWith(ldmBinPrefix) ? expanded : null;
|
|
3023
|
+
}
|
|
3024
|
+
const cronTargets = new Map(); // path → ['missing'|'not executable']
|
|
3025
|
+
const seenTargets = new Set(); // every bin-targeted path we found
|
|
3026
|
+
for (const line of crontab.split('\n')) {
|
|
3027
|
+
const expanded = extractCronTarget(line);
|
|
3028
|
+
if (!expanded) continue;
|
|
3029
|
+
seenTargets.add(expanded);
|
|
3030
|
+
if (cronTargets.has(expanded)) continue;
|
|
3031
|
+
const problems = [];
|
|
3032
|
+
if (!existsSync(expanded)) {
|
|
3033
|
+
problems.push('missing');
|
|
3034
|
+
} else if ((statSync(expanded).mode & 0o111) === 0) {
|
|
3035
|
+
problems.push('not executable');
|
|
3036
|
+
}
|
|
3037
|
+
if (problems.length > 0) cronTargets.set(expanded, problems);
|
|
3038
|
+
}
|
|
3039
|
+
if (cronTargets.size > 0) {
|
|
3040
|
+
// Manifest-driven lookup. Aggregates LDM CLI's wipLdmOs.binFiles
|
|
3041
|
+
// and every registered extension's openclaw.plugin.json#binFiles.
|
|
3042
|
+
// If aggregation reports a conflict, the lookup stays empty and
|
|
3043
|
+
// each broken target becomes "owner unknown" until the conflict
|
|
3044
|
+
// is resolved at the package-manifest layer.
|
|
3045
|
+
const { aggregateBinManifest } = await import('../lib/bin-manifest.mjs');
|
|
3046
|
+
const drManifest = aggregateBinManifest({
|
|
3047
|
+
ldmCliRoot: join(__dirname, '..'),
|
|
3048
|
+
extensionsRoot: LDM_EXTENSIONS,
|
|
3049
|
+
binDir: join(LDM_ROOT, 'bin'),
|
|
3050
|
+
registry: readJSON(REGISTRY_PATH) || { extensions: {} },
|
|
3051
|
+
});
|
|
3052
|
+
const knownSources = {};
|
|
3053
|
+
const declarers = {};
|
|
3054
|
+
if (drManifest.conflicts.length === 0) {
|
|
3055
|
+
for (const e of drManifest.entries) {
|
|
3056
|
+
knownSources[e.name] = e.sourcePath;
|
|
3057
|
+
declarers[e.name] = e.declarer;
|
|
3058
|
+
}
|
|
3059
|
+
} else {
|
|
3060
|
+
console.log(` ! bin manifest conflict: ${drManifest.conflicts.length}; restore disabled until resolved`);
|
|
3061
|
+
}
|
|
3062
|
+
let healed = 0;
|
|
3063
|
+
for (const [path, problems] of cronTargets) {
|
|
3064
|
+
const detail = problems.join(', ');
|
|
3065
|
+
console.log(` ! cron target ${detail}: ${path}`);
|
|
3066
|
+
const base = basename(path);
|
|
3067
|
+
const src = knownSources[base];
|
|
3068
|
+
if (FIX_FLAG && src && existsSync(src)) {
|
|
3069
|
+
try {
|
|
3070
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
3071
|
+
cpSync(src, path);
|
|
3072
|
+
chmodSync(path, 0o755);
|
|
3073
|
+
console.log(` + Restored ${base} from ${src.replace(HOME, '~')}`);
|
|
3074
|
+
healed++;
|
|
3075
|
+
} catch (e) {
|
|
3076
|
+
console.log(` Restore failed: ${e.message}`);
|
|
3077
|
+
}
|
|
3078
|
+
} else if (src) {
|
|
3079
|
+
console.log(` Run: ldm doctor --fix to restore from ${src.replace(HOME, '~')}`);
|
|
3080
|
+
} else {
|
|
3081
|
+
console.log(` Owner unknown; consult the extension that registered this cron entry`);
|
|
3082
|
+
}
|
|
3083
|
+
}
|
|
3084
|
+
issues += cronTargets.size - healed;
|
|
3085
|
+
} else if (seenTargets.size > 0) {
|
|
3086
|
+
console.log(` + Cron targets under ~/.ldm/bin/: ${seenTargets.size} entr${seenTargets.size === 1 ? 'y' : 'ies'}, all exist and executable`);
|
|
3087
|
+
}
|
|
3088
|
+
}
|
|
3089
|
+
|
|
2952
3090
|
// 4. Check sacred locations
|
|
2953
3091
|
const sacred = [
|
|
2954
3092
|
{ path: join(LDM_ROOT, 'memory'), label: 'memory/' },
|
package/docs/skills/README.md
CHANGED
|
@@ -31,6 +31,8 @@ Your AIs are only as powerful as what you give them. Here's everything available
|
|
|
31
31
|
|
|
32
32
|
**OpenClaw**
|
|
33
33
|
- Open-source agent runtime. Run AI agents 24/7 with identity, memory, and tool access. The existence proof for LDM OS.
|
|
34
|
+
- WIP contributions accepted upstream: `before_message_write` plugin hook (#18197), Codex app-server final chat events (#70815 -> maintainer PR #71293), memory-core seed cache streaming/yield (#73067 -> maintainer PR #73118), and fallback vector top-K streaming (#73069 -> maintainer PR #73100).
|
|
35
|
+
- Submitted / superseded: symlink plugin discovery fix (#45744; bug confirmed, superseded by #69971, not landed as submitted).
|
|
34
36
|
- [Read more about OpenClaw](https://github.com/openclaw/openclaw)
|
|
35
37
|
|
|
36
38
|
## Identity
|
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
// lib/bin-manifest.mjs — bin ownership manifest aggregator + heal helpers
|
|
2
|
+
//
|
|
3
|
+
// Implements the design at
|
|
4
|
+
// ai/product/plans-prds/current/2026-04-28--cc-mini--ldm-bin-ownership-manifest-design.md
|
|
5
|
+
//
|
|
6
|
+
// Two declarers contribute entries:
|
|
7
|
+
// - LDM CLI: package.json `wipLdmOs.binFiles`
|
|
8
|
+
// - Extensions: ~/.ldm/extensions/<name>/openclaw.plugin.json `binFiles`
|
|
9
|
+
//
|
|
10
|
+
// Aggregation produces { entries, conflicts }. Conflicts are hard
|
|
11
|
+
// failures: callers MUST check `conflicts.length === 0` before any
|
|
12
|
+
// write. heal() never runs if conflicts exist.
|
|
13
|
+
|
|
14
|
+
import { existsSync, readFileSync, statSync, mkdirSync, copyFileSync, chmodSync } from 'node:fs';
|
|
15
|
+
import { join, dirname, basename } from 'node:path';
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* @typedef {Object} BinDeclaration
|
|
19
|
+
* @property {string} name - basename written to <binDir>/<name>
|
|
20
|
+
* @property {string} source - relative to declarer's installed root
|
|
21
|
+
* @property {boolean} [executable] - default true; chmod 0755 after copy
|
|
22
|
+
* @property {string} [purpose] - free-form, surfaces in verbose doctor
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* @typedef {Object} BinEntry
|
|
27
|
+
* @property {string} name
|
|
28
|
+
* @property {string} destPath - resolved absolute path under binDir
|
|
29
|
+
* @property {string} sourcePath - resolved absolute path
|
|
30
|
+
* @property {boolean} executable
|
|
31
|
+
* @property {string} declarer - 'wip-ldm-os' or extension name
|
|
32
|
+
* @property {string} [purpose]
|
|
33
|
+
*/
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* @typedef {Object} Conflict
|
|
37
|
+
* @property {string} name
|
|
38
|
+
* @property {{declarer: string, sourcePath: string}[]} declarers
|
|
39
|
+
*/
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Validate a single declaration shape. Returns array of error strings (empty = ok).
|
|
43
|
+
* @param {BinDeclaration} decl
|
|
44
|
+
* @returns {string[]}
|
|
45
|
+
*/
|
|
46
|
+
export function validateDeclaration(decl) {
|
|
47
|
+
const errors = [];
|
|
48
|
+
if (!decl || typeof decl !== 'object') {
|
|
49
|
+
errors.push('declaration must be an object');
|
|
50
|
+
return errors;
|
|
51
|
+
}
|
|
52
|
+
if (typeof decl.name !== 'string' || !decl.name) {
|
|
53
|
+
errors.push('"name" must be a non-empty string');
|
|
54
|
+
} else if (decl.name !== basename(decl.name) || decl.name.includes('/') || decl.name.includes('\\')) {
|
|
55
|
+
errors.push(`"name" must be a basename, got "${decl.name}"`);
|
|
56
|
+
} else if (decl.name.includes('..')) {
|
|
57
|
+
errors.push(`"name" must not contain "..", got "${decl.name}"`);
|
|
58
|
+
}
|
|
59
|
+
if (typeof decl.source !== 'string' || !decl.source) {
|
|
60
|
+
errors.push('"source" must be a non-empty string');
|
|
61
|
+
} else if (decl.source.includes('..')) {
|
|
62
|
+
errors.push(`"source" must not contain "..", got "${decl.source}"`);
|
|
63
|
+
}
|
|
64
|
+
if (decl.executable !== undefined && typeof decl.executable !== 'boolean') {
|
|
65
|
+
errors.push('"executable" must be a boolean if provided');
|
|
66
|
+
}
|
|
67
|
+
return errors;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Validate all declarations from one declarer (e.g. the LDM CLI's own
|
|
72
|
+
* `wipLdmOs.binFiles`, or one extension's `binFiles`). Used by both
|
|
73
|
+
* runtime aggregation and prepublish CI gate.
|
|
74
|
+
*
|
|
75
|
+
* Checks: shape per entry, no internal duplicate `name`, `source` exists
|
|
76
|
+
* on disk under `packageRoot`.
|
|
77
|
+
*
|
|
78
|
+
* @param {string} declarer
|
|
79
|
+
* @param {string} packageRoot - the absolute path to resolve `source` against
|
|
80
|
+
* @param {BinDeclaration[]} decls
|
|
81
|
+
* @returns {{valid: boolean, errors: string[]}}
|
|
82
|
+
*/
|
|
83
|
+
export function validateDeclarations(declarer, packageRoot, decls) {
|
|
84
|
+
const errors = [];
|
|
85
|
+
if (!Array.isArray(decls)) {
|
|
86
|
+
return { valid: false, errors: [`${declarer}: binFiles must be an array`] };
|
|
87
|
+
}
|
|
88
|
+
const seen = new Set();
|
|
89
|
+
for (let i = 0; i < decls.length; i++) {
|
|
90
|
+
const d = decls[i];
|
|
91
|
+
const ctx = `${declarer}[${i}]${d?.name ? ` ${d.name}` : ''}`;
|
|
92
|
+
for (const e of validateDeclaration(d)) errors.push(`${ctx}: ${e}`);
|
|
93
|
+
if (d && typeof d.name === 'string') {
|
|
94
|
+
if (seen.has(d.name)) errors.push(`${declarer}: duplicate name within declarer: ${d.name}`);
|
|
95
|
+
seen.add(d.name);
|
|
96
|
+
}
|
|
97
|
+
if (d && typeof d.source === 'string') {
|
|
98
|
+
const src = join(packageRoot, d.source);
|
|
99
|
+
if (!existsSync(src)) errors.push(`${ctx}: source not found at ${src}`);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
return { valid: errors.length === 0, errors };
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Read LDM CLI's own bin declarations from its package.json.
|
|
107
|
+
* @param {string} ldmCliRoot
|
|
108
|
+
* @returns {BinDeclaration[]}
|
|
109
|
+
*/
|
|
110
|
+
function readLdmCliDeclarations(ldmCliRoot) {
|
|
111
|
+
const pkgPath = join(ldmCliRoot, 'package.json');
|
|
112
|
+
if (!existsSync(pkgPath)) return [];
|
|
113
|
+
try {
|
|
114
|
+
const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8'));
|
|
115
|
+
return Array.isArray(pkg?.wipLdmOs?.binFiles) ? pkg.wipLdmOs.binFiles : [];
|
|
116
|
+
} catch {
|
|
117
|
+
return [];
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Read one extension's bin declarations from its openclaw.plugin.json.
|
|
123
|
+
* @param {string} extDir - ~/.ldm/extensions/<name>
|
|
124
|
+
* @returns {BinDeclaration[]}
|
|
125
|
+
*/
|
|
126
|
+
function readExtensionDeclarations(extDir) {
|
|
127
|
+
const manifestPath = join(extDir, 'openclaw.plugin.json');
|
|
128
|
+
if (!existsSync(manifestPath)) return [];
|
|
129
|
+
try {
|
|
130
|
+
const m = JSON.parse(readFileSync(manifestPath, 'utf-8'));
|
|
131
|
+
return Array.isArray(m?.binFiles) ? m.binFiles : [];
|
|
132
|
+
} catch {
|
|
133
|
+
return [];
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Aggregate all bin entries across LDM CLI + registered extensions.
|
|
139
|
+
* Returns conflicts for any name claimed by 2+ declarers.
|
|
140
|
+
*
|
|
141
|
+
* IMPORTANT: callers MUST check `conflicts.length === 0` before doing any
|
|
142
|
+
* writes. Conflict means we cannot safely decide who owns the file.
|
|
143
|
+
*
|
|
144
|
+
* @param {Object} opts
|
|
145
|
+
* @param {string} opts.ldmCliRoot - absolute path to LDM CLI package root
|
|
146
|
+
* @param {string} opts.extensionsRoot - absolute path, typically ~/.ldm/extensions
|
|
147
|
+
* @param {string} opts.binDir - absolute path, typically ~/.ldm/bin
|
|
148
|
+
* @param {Object} [opts.registry] - optional ~/.ldm/extensions/registry.json contents
|
|
149
|
+
* @returns {{entries: BinEntry[], conflicts: Conflict[]}}
|
|
150
|
+
*/
|
|
151
|
+
export function aggregateBinManifest({ ldmCliRoot, extensionsRoot, binDir, registry } = {}) {
|
|
152
|
+
/** @type {BinEntry[]} */
|
|
153
|
+
const entries = [];
|
|
154
|
+
/** @type {Map<string, {declarer: string, sourcePath: string}[]>} */
|
|
155
|
+
const claims = new Map();
|
|
156
|
+
|
|
157
|
+
function record(declarer, declarerRoot, decls) {
|
|
158
|
+
for (const d of decls) {
|
|
159
|
+
if (!d || typeof d.name !== 'string' || typeof d.source !== 'string') continue;
|
|
160
|
+
const sourcePath = join(declarerRoot, d.source);
|
|
161
|
+
const list = claims.get(d.name) || [];
|
|
162
|
+
list.push({ declarer, sourcePath });
|
|
163
|
+
claims.set(d.name, list);
|
|
164
|
+
entries.push({
|
|
165
|
+
name: d.name,
|
|
166
|
+
destPath: join(binDir, d.name),
|
|
167
|
+
sourcePath,
|
|
168
|
+
executable: d.executable !== false,
|
|
169
|
+
declarer,
|
|
170
|
+
purpose: d.purpose,
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// 1. LDM CLI
|
|
176
|
+
record('wip-ldm-os', ldmCliRoot, readLdmCliDeclarations(ldmCliRoot));
|
|
177
|
+
|
|
178
|
+
// 2. Registered extensions
|
|
179
|
+
const extNames = registry?.extensions ? Object.keys(registry.extensions) : [];
|
|
180
|
+
for (const name of extNames) {
|
|
181
|
+
const extDir = join(extensionsRoot, name);
|
|
182
|
+
record(name, extDir, readExtensionDeclarations(extDir));
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/** @type {Conflict[]} */
|
|
186
|
+
const conflicts = [];
|
|
187
|
+
for (const [name, declarers] of claims.entries()) {
|
|
188
|
+
if (declarers.length > 1) conflicts.push({ name, declarers });
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
return { entries, conflicts };
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Verify and (optionally) heal each entry. heal=false is read-only and
|
|
196
|
+
* just classifies. heal=true restores missing/unexecutable files from
|
|
197
|
+
* `sourcePath`. Returns a per-entry result so callers can build their
|
|
198
|
+
* own output.
|
|
199
|
+
*
|
|
200
|
+
* NEVER call this if aggregateBinManifest reported conflicts. The caller
|
|
201
|
+
* must abort instead.
|
|
202
|
+
*
|
|
203
|
+
* @param {BinEntry[]} entries
|
|
204
|
+
* @param {Object} [opts]
|
|
205
|
+
* @param {boolean} [opts.heal] - default false
|
|
206
|
+
* @returns {{
|
|
207
|
+
* ok: BinEntry[],
|
|
208
|
+
* missing: BinEntry[],
|
|
209
|
+
* notExecutable: BinEntry[],
|
|
210
|
+
* sourceMissing: BinEntry[],
|
|
211
|
+
* healed: BinEntry[],
|
|
212
|
+
* failed: {entry: BinEntry, reason: string}[]
|
|
213
|
+
* }}
|
|
214
|
+
*/
|
|
215
|
+
export function healBinManifest(entries, opts = {}) {
|
|
216
|
+
const heal = opts.heal === true;
|
|
217
|
+
const ok = [];
|
|
218
|
+
const missing = [];
|
|
219
|
+
const notExecutable = [];
|
|
220
|
+
const sourceMissing = [];
|
|
221
|
+
const healed = [];
|
|
222
|
+
const failed = [];
|
|
223
|
+
|
|
224
|
+
for (const e of entries) {
|
|
225
|
+
const destExists = existsSync(e.destPath);
|
|
226
|
+
const destExecutable = destExists && (statSync(e.destPath).mode & 0o111) !== 0;
|
|
227
|
+
const expectedExec = e.executable !== false;
|
|
228
|
+
const destOk = destExists && (!expectedExec || destExecutable);
|
|
229
|
+
|
|
230
|
+
if (destOk) {
|
|
231
|
+
ok.push(e);
|
|
232
|
+
continue;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
if (!destExists) missing.push(e);
|
|
236
|
+
else if (expectedExec && !destExecutable) notExecutable.push(e);
|
|
237
|
+
|
|
238
|
+
if (!heal) continue;
|
|
239
|
+
|
|
240
|
+
if (!existsSync(e.sourcePath)) {
|
|
241
|
+
sourceMissing.push(e);
|
|
242
|
+
failed.push({ entry: e, reason: `source missing at ${e.sourcePath}` });
|
|
243
|
+
continue;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
try {
|
|
247
|
+
mkdirSync(dirname(e.destPath), { recursive: true });
|
|
248
|
+
copyFileSync(e.sourcePath, e.destPath);
|
|
249
|
+
if (expectedExec) chmodSync(e.destPath, 0o755);
|
|
250
|
+
healed.push(e);
|
|
251
|
+
} catch (err) {
|
|
252
|
+
failed.push({ entry: e, reason: err.message });
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
return { ok, missing, notExecutable, sourceMissing, healed, failed };
|
|
257
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@wipcomputer/wip-ldm-os",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.83",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "LDM OS: identity, memory, and sovereignty infrastructure for AI agents",
|
|
6
6
|
"engines": {
|
|
@@ -18,8 +18,13 @@
|
|
|
18
18
|
"scripts": {
|
|
19
19
|
"build:bridge": "cd src/bridge && npm install && npx tsup core.ts mcp-server.ts cli.ts openclaw.ts --format esm --dts --clean --outDir ../../dist/bridge",
|
|
20
20
|
"build": "npm run build:bridge",
|
|
21
|
-
"prepublishOnly": "npm run build:bridge",
|
|
21
|
+
"prepublishOnly": "npm run build:bridge && npm run validate:bin-manifest",
|
|
22
|
+
"validate:bin-manifest": "node scripts/validate-bin-manifest.mjs",
|
|
22
23
|
"test:skill-frontmatter": "node scripts/test-skill-frontmatter.mjs",
|
|
24
|
+
"test:installer-update-tracks": "node scripts/test-installer-update-tracks.mjs",
|
|
25
|
+
"test:ldm-install-bin-shim": "node scripts/test-ldm-install-preserves-foreign-bin.mjs",
|
|
26
|
+
"test:doctor-cron-target": "node scripts/test-doctor-cron-target.mjs",
|
|
27
|
+
"test:bin-manifest": "node scripts/test-bin-manifest.mjs",
|
|
23
28
|
"fmt": "npx prettier --write 'src/**/*.{ts,mjs}' 'lib/**/*.mjs' 'bin/**/*.js'",
|
|
24
29
|
"fmt:check": "npx prettier --check 'src/**/*.{ts,mjs}' 'lib/**/*.mjs' 'bin/**/*.js'"
|
|
25
30
|
},
|
|
@@ -31,6 +36,35 @@
|
|
|
31
36
|
"timeout": 15
|
|
32
37
|
}
|
|
33
38
|
},
|
|
39
|
+
"wipLdmOs": {
|
|
40
|
+
"binFiles": [
|
|
41
|
+
{
|
|
42
|
+
"name": "process-monitor.sh",
|
|
43
|
+
"source": "bin/process-monitor.sh",
|
|
44
|
+
"purpose": "kills zombie processes; cron every 3 min"
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
"name": "ldm-backup.sh",
|
|
48
|
+
"source": "scripts/ldm-backup.sh",
|
|
49
|
+
"purpose": "daily backup; LaunchAgent at 03:00"
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
"name": "ldm-restore.sh",
|
|
53
|
+
"source": "scripts/ldm-restore.sh",
|
|
54
|
+
"purpose": "restore from backup; operator-invoked"
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
"name": "ldm-summary.sh",
|
|
58
|
+
"source": "scripts/ldm-summary.sh",
|
|
59
|
+
"purpose": "generate session/daily summaries"
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
"name": "backfill-summaries.sh",
|
|
63
|
+
"source": "scripts/backfill-summaries.sh",
|
|
64
|
+
"purpose": "backfill summary cadences"
|
|
65
|
+
}
|
|
66
|
+
]
|
|
67
|
+
},
|
|
34
68
|
"files": [
|
|
35
69
|
"src/",
|
|
36
70
|
"lib/",
|