@wipcomputer/wip-ldm-os 0.4.82-alpha.1 → 0.4.84
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 +140 -0
- package/docs/skills/README.md +2 -0
- package/docs/universal-installer/README.md +1 -1
- package/docs/universal-installer/SPEC.md +189 -16
- package/docs/universal-installer/TECHNICAL.md +84 -29
- package/lib/bin-manifest.mjs +257 -0
- package/package.json +35 -2
- package/scripts/test-bin-manifest.mjs +282 -0
- package/scripts/test-doctor-cron-target.mjs +172 -0
- package/scripts/test-ldm-install-preserves-foreign-bin.mjs +112 -0
- package/scripts/validate-bin-manifest.mjs +41 -0
- 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/nginx/wip.computer.conf +88 -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
|
@@ -18,23 +18,24 @@ Every tool is a sensor, an actuator, or both. Every tool should be accessible th
|
|
|
18
18
|
- Guard a file from edits (wip-file-guard)
|
|
19
19
|
- Generate a video (wip-grok generate_video)
|
|
20
20
|
|
|
21
|
-
## The
|
|
21
|
+
## The Eight Interfaces
|
|
22
22
|
|
|
23
|
-
Agents don't all speak the same language. Some run shell commands. Some import modules. Some talk MCP. Some read markdown instructions.
|
|
23
|
+
Agents don't all speak the same language. Some run shell commands. Some import modules. Some talk MCP over stdio. Some talk MCP over HTTPS. Some read markdown instructions.
|
|
24
24
|
|
|
25
25
|
So every tool should expose multiple interfaces into the same core logic:
|
|
26
26
|
|
|
27
|
-
| Interface | What | Who uses it |
|
|
28
|
-
|
|
29
|
-
| **CLI** | Shell command | Humans, any agent with bash |
|
|
30
|
-
| **Module** | ES import | Other tools, scripts |
|
|
31
|
-
| **MCP Server** | JSON-RPC over stdio | Claude Code, Cursor,
|
|
32
|
-
| **
|
|
33
|
-
| **
|
|
34
|
-
|
|
|
35
|
-
| **Claude Code
|
|
27
|
+
| # | Interface | What | Who uses it |
|
|
28
|
+
|---|-----------|------|-------------|
|
|
29
|
+
| 1 | **CLI** | Shell command | Humans, any agent with bash |
|
|
30
|
+
| 2 | **Module** | ES import | Other tools, scripts |
|
|
31
|
+
| 3 | **MCP Server (local stdio)** | JSON-RPC over stdio | Claude Code, Cursor, OpenClaw |
|
|
32
|
+
| 4 | **Remote MCP** | JSON-RPC over HTTPS (SSE / streamable HTTP) | Claude Desktop, web, mobile |
|
|
33
|
+
| 5 | **OpenClaw Plugin** | Lifecycle hooks + tools | OpenClaw agents |
|
|
34
|
+
| 6 | **Skill** | Markdown instructions (SKILL.md) | Any agent that reads files |
|
|
35
|
+
| 7 | **Claude Code Hook** | PreToolUse/Stop events | Claude Code |
|
|
36
|
+
| 8 | **Claude Code Plugin** | Distributable package (skills, agents, hooks, MCP, LSP) | Claude Code marketplace |
|
|
36
37
|
|
|
37
|
-
Not every tool needs all
|
|
38
|
+
Not every tool needs all eight. Build what makes sense. But the more interfaces you expose, the more agents can use your tool. Local and Remote MCP are sibling transports of the same protocol; they sit next to each other so the relationship is obvious.
|
|
38
39
|
|
|
39
40
|
### 1. CLI
|
|
40
41
|
|
|
@@ -75,15 +76,15 @@ An importable ES module. The programmatic interface. Other tools compose with it
|
|
|
75
76
|
}
|
|
76
77
|
```
|
|
77
78
|
|
|
78
|
-
### 3. MCP Server
|
|
79
|
+
### 3. MCP Server (local stdio)
|
|
79
80
|
|
|
80
|
-
A JSON-RPC server implementing the Model Context Protocol.
|
|
81
|
+
A JSON-RPC server implementing the Model Context Protocol over stdio. Spawned as a child process by the agent. For the HTTP/SSE sibling, see [#4 Remote MCP](#4-remote-mcp).
|
|
81
82
|
|
|
82
83
|
**Convention:** `mcp-server.mjs` (or `.js`, `.ts`) at the repo root. Uses `@modelcontextprotocol/sdk`.
|
|
83
84
|
|
|
84
85
|
**Detection:** One of `mcp-server.mjs`, `mcp-server.js`, `mcp-server.ts`, `dist/mcp-server.js` exists.
|
|
85
86
|
|
|
86
|
-
**Install:** Add to `.mcp.json`:
|
|
87
|
+
**Install:** Add to `.mcp.json` with `command` + `args`:
|
|
87
88
|
|
|
88
89
|
```json
|
|
89
90
|
{
|
|
@@ -94,7 +95,46 @@ A JSON-RPC server implementing the Model Context Protocol. Any MCP-compatible ag
|
|
|
94
95
|
}
|
|
95
96
|
```
|
|
96
97
|
|
|
97
|
-
### 4.
|
|
98
|
+
### 4. Remote MCP
|
|
99
|
+
|
|
100
|
+
The HTTP/SSE (or streamable HTTP) sibling of #3. Hosted at an HTTPS endpoint, not spawned locally. The transport that lights up Claude Desktop connectors, web, and mobile clients.
|
|
101
|
+
|
|
102
|
+
**Contract:** Remote MCP endpoint is **declared by package/catalog metadata** and **registered by `ldm install`**. No filesystem-sniffing fallback.
|
|
103
|
+
|
|
104
|
+
**Convention:** `mcp.remote` field in `package.json`:
|
|
105
|
+
|
|
106
|
+
```json
|
|
107
|
+
{
|
|
108
|
+
"mcp": {
|
|
109
|
+
"remote": {
|
|
110
|
+
"url": "https://example.com/mcp",
|
|
111
|
+
"transport": "streamable-http",
|
|
112
|
+
"auth": "oauth"
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
`url` may be a placeholder (`"https://__DEPLOYED_URL__"`) when the repo ships the server code and the catalog supplies the URL at install time.
|
|
119
|
+
|
|
120
|
+
**Detection:** `package.json.mcp.remote.url` is a string.
|
|
121
|
+
|
|
122
|
+
**Install:** Add to `.mcp.json` as a remote entry, plus print a one-line Claude Desktop hint:
|
|
123
|
+
|
|
124
|
+
```json
|
|
125
|
+
{
|
|
126
|
+
"tool-name": {
|
|
127
|
+
"url": "https://example.com/mcp",
|
|
128
|
+
"transport": "streamable-http"
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
**Auth:** `none` writes the URL as-is. `shared-secret` prompts the user once and stores out-of-band. `oauth` is gated behind a follow-up ticket; first cut prints a TODO.
|
|
134
|
+
|
|
135
|
+
**Implementation status:** detection + install action are tracked in `ai/product/bugs/installer/` (see [Remote MCP detection](../../ai/product/bugs/installer/2026-04-28--cc-mini--installer-remote-mcp-detection.md) and [Remote MCP install](../../ai/product/bugs/installer/2026-04-28--cc-mini--installer-remote-mcp-install.md)). The spec is canonical now; the detector and installer catch up next.
|
|
136
|
+
|
|
137
|
+
### 5. OpenClaw Plugin
|
|
98
138
|
|
|
99
139
|
A plugin for OpenClaw agents. Lifecycle hooks, tool registration, settings.
|
|
100
140
|
|
|
@@ -104,12 +144,14 @@ A plugin for OpenClaw agents. Lifecycle hooks, tool registration, settings.
|
|
|
104
144
|
|
|
105
145
|
**Install:** Copy to `~/.openclaw/extensions/<name>/`, run `npm install --omit=dev`.
|
|
106
146
|
|
|
107
|
-
###
|
|
147
|
+
### 6. Skill (SKILL.md)
|
|
108
148
|
|
|
109
149
|
A markdown file that teaches agents when and how to use the tool. The instruction interface. Follows the [Agent Skills Spec](https://agentskills.io/specification).
|
|
110
150
|
|
|
111
151
|
**Convention:** `SKILL.md` at the repo root. Optional `references/` directory for context files.
|
|
112
152
|
|
|
153
|
+
**Platform variants:** Codex CLI reads `AGENTS.md` with the same role and content shape. Treat as the Codex-flavored filename for this same interface, not a separate one.
|
|
154
|
+
|
|
113
155
|
**Detection:** `SKILL.md` exists.
|
|
114
156
|
|
|
115
157
|
**Install:** `ldm install` deploys `SKILL.md` to `~/.openclaw/skills/<name>/`. If `references/` exists, it is deployed alongside and also to `settings/docs/skills/<name>/` in the workspace (so all agents can read them).
|
|
@@ -132,7 +174,7 @@ metadata:
|
|
|
132
174
|
---
|
|
133
175
|
```
|
|
134
176
|
|
|
135
|
-
###
|
|
177
|
+
### 7. Claude Code Hook
|
|
136
178
|
|
|
137
179
|
A hook that runs during Claude Code's tool lifecycle (PreToolUse, Stop, etc.).
|
|
138
180
|
|
|
@@ -157,7 +199,7 @@ A hook that runs during Claude Code's tool lifecycle (PreToolUse, Stop, etc.).
|
|
|
157
199
|
}
|
|
158
200
|
```
|
|
159
201
|
|
|
160
|
-
###
|
|
202
|
+
### 8. Claude Code Plugin
|
|
161
203
|
|
|
162
204
|
A distributable plugin for Claude Code. Bundles skills, agents, hooks, MCP servers, and LSP servers into one installable package. Shareable via marketplaces.
|
|
163
205
|
|
|
@@ -188,6 +230,14 @@ your-plugin/
|
|
|
188
230
|
}
|
|
189
231
|
```
|
|
190
232
|
|
|
233
|
+
### Out of scope by design
|
|
234
|
+
|
|
235
|
+
Disposable, agent-generated artifacts (custom dashboards, ephemeral scripts, one-off automations) are out of scope for this spec. They are products of an agent, not Universal Interface products. The eight interfaces describe what the agent has to *compose with*. Worked example: [SPEC.md ... Worked example (compact sketch)](SPEC.md#worked-example-compact-sketch).
|
|
236
|
+
|
|
237
|
+
### Future considerations
|
|
238
|
+
|
|
239
|
+
*LSP as a standalone interface (#9).* LSP servers are currently surfaced via Claude Code Plugin bundles (#8) ... `.lsp.json` is part of the plugin shape. If a product ships a standalone LSP server outside a CC Plugin, we will add it as a numbered interface. Not added today.
|
|
240
|
+
|
|
191
241
|
## How to Build It
|
|
192
242
|
|
|
193
243
|
The architecture is simple. Four files:
|
|
@@ -206,30 +256,33 @@ This means one codebase, one set of tests, multiple interfaces.
|
|
|
206
256
|
|
|
207
257
|
## Install Prompt Template
|
|
208
258
|
|
|
209
|
-
Every product gets an install prompt. Paste it into any AI. The AI reads the spec, explains
|
|
259
|
+
Every product gets an install prompt. Paste it into any AI. The AI reads the install spec, explains the product, checks what's installed, and walks you through a dry run before touching anything.
|
|
260
|
+
|
|
261
|
+
The install spec URL convention, behavior contract, track flags, and `agent.txt` distinction are defined in [SPEC.md ... Install Spec](SPEC.md#install-spec). This is the canonical paste-into-an-AI form:
|
|
210
262
|
|
|
211
263
|
```
|
|
212
|
-
Read wip.computer/install
|
|
264
|
+
Read https://wip.computer/install/<slug>.txt
|
|
213
265
|
|
|
214
|
-
|
|
215
|
-
|
|
266
|
+
Check if <product name> is already installed. If it is, run
|
|
267
|
+
`ldm install --dry-run <slug>` and show me what I have and what's new.
|
|
268
|
+
|
|
269
|
+
If not, walk me through setup and explain:
|
|
270
|
+
1. What is <product name>?
|
|
216
271
|
2. What does it install on my system?
|
|
217
272
|
3. What changes for us? (this AI)
|
|
218
273
|
4. What changes across all my AIs?
|
|
219
274
|
|
|
220
|
-
Check if {name of product} is already installed.
|
|
221
|
-
|
|
222
|
-
If it is, show me what I have and what's new.
|
|
223
|
-
|
|
224
275
|
Then ask:
|
|
225
276
|
- Do you have questions?
|
|
226
277
|
- Want to see a dry run?
|
|
227
278
|
|
|
228
|
-
If I say yes, run:
|
|
279
|
+
If I say yes, run: ldm install --dry-run <slug>
|
|
229
280
|
|
|
230
281
|
Show me exactly what will change. Don't install anything until I say "install".
|
|
231
282
|
```
|
|
232
283
|
|
|
284
|
+
Tracks: append `--alpha` or `--beta` to install a prerelease (e.g. `ldm install --beta <slug>`). The same install spec URL covers all tracks.
|
|
285
|
+
|
|
233
286
|
## The `ai/` Folder
|
|
234
287
|
|
|
235
288
|
Every repo should have an `ai/` folder. This is where agents and humans collaborate on the project ... plans, todos, dev updates, research notes, conversations.
|
|
@@ -308,7 +361,8 @@ ldm install # update all
|
|
|
308
361
|
|---------|-----------|---------------|
|
|
309
362
|
| `package.json` with `bin` | CLI | `npm install -g` |
|
|
310
363
|
| `main` or `exports` in `package.json` | Module | Reports import path |
|
|
311
|
-
| `mcp-server.mjs` | MCP |
|
|
364
|
+
| `mcp-server.mjs` | MCP (local stdio) | Adds `command` + `args` entry to `.mcp.json` |
|
|
365
|
+
| `mcp.remote.url` in `package.json` | Remote MCP | Adds `url` + `transport` entry to `.mcp.json`; prints Claude Desktop hint. **Implementation in flight ([ticket](../../ai/product/bugs/installer/2026-04-28--cc-mini--installer-remote-mcp-detection.md)).** |
|
|
312
366
|
| `openclaw.plugin.json` | OpenClaw | Copies to `~/.openclaw/extensions/` |
|
|
313
367
|
| `SKILL.md` | Skill | Reports path |
|
|
314
368
|
| `guard.mjs` or `claudeCode.hook` | CC Hook | Adds to `~/.claude/settings.json` |
|
|
@@ -323,6 +377,7 @@ ldm install # update all
|
|
|
323
377
|
| [wip-file-guard](https://github.com/wipcomputer/wip-ai-devops-toolbox/tree/main/tools/wip-file-guard) | Actuator | CLI + OpenClaw + CC Hook | Protect files from AI edits |
|
|
324
378
|
| [wip-healthcheck](https://github.com/wipcomputer/wip-healthcheck) | Sensor | CLI + Module | System health monitoring |
|
|
325
379
|
| [wip-markdown-viewer](https://github.com/wipcomputer/wip-markdown-viewer) | Actuator | CLI + Module | Live markdown viewer |
|
|
380
|
+
| [wip-codex-remote-control](https://wip.computer/install/wip-codex-remote-control.txt) | Sensor + Actuator | CLI + Module + MCP + Skill + Install Spec | Pair phone with Codex; AI-led install via published install spec. Worked example of the install-spec URL pattern. |
|
|
326
381
|
|
|
327
382
|
## Supported Tools
|
|
328
383
|
|
|
@@ -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.84",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "LDM OS: identity, memory, and sovereignty infrastructure for AI agents",
|
|
6
6
|
"engines": {
|
|
@@ -18,9 +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",
|
|
23
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",
|
|
24
28
|
"fmt": "npx prettier --write 'src/**/*.{ts,mjs}' 'lib/**/*.mjs' 'bin/**/*.js'",
|
|
25
29
|
"fmt:check": "npx prettier --check 'src/**/*.{ts,mjs}' 'lib/**/*.mjs' 'bin/**/*.js'"
|
|
26
30
|
},
|
|
@@ -32,6 +36,35 @@
|
|
|
32
36
|
"timeout": 15
|
|
33
37
|
}
|
|
34
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
|
+
},
|
|
35
68
|
"files": [
|
|
36
69
|
"src/",
|
|
37
70
|
"lib/",
|