@wipcomputer/wip-ldm-os 0.4.85-alpha.2 → 0.4.85-alpha.20
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 +22 -2
- package/SKILL.md +8 -5
- package/bin/ldm.js +95 -51
- package/docs/universal-installer/SPEC.md +16 -3
- package/docs/universal-installer/TECHNICAL.md +4 -4
- package/lib/deploy.mjs +104 -20
- package/lib/detect.mjs +35 -4
- package/package.json +12 -2
- package/scripts/test-crc-agentid-tenant-boundary.mjs +80 -0
- package/scripts/test-crc-e2ee-key-persistence.mjs +150 -0
- package/scripts/test-crc-e2ee-session-route.mjs +129 -0
- package/scripts/test-crc-pair-login-flow.mjs +40 -0
- package/scripts/test-crc-pair-relink-audit-and-rotation.mjs +164 -0
- package/scripts/test-crc-pair-status-poll-token.mjs +73 -0
- package/scripts/test-install-prompt-policy.mjs +60 -0
- package/scripts/test-installer-skill-directory.mjs +55 -0
- package/scripts/test-installer-skill-dry-run-destinations.mjs +100 -0
- package/scripts/test-installer-target-self-update.mjs +131 -0
- package/shared/templates/install-prompt.md +20 -2
- package/src/hosted-mcp/README.md +15 -0
- package/src/hosted-mcp/app/footer.js +74 -0
- package/src/hosted-mcp/app/kaleidoscope-login.html +846 -0
- package/src/hosted-mcp/app/pair.html +165 -57
- package/src/hosted-mcp/app/sprites.png +0 -0
- package/src/hosted-mcp/codex-relay-e2ee-registry.mjs +208 -0
- package/src/hosted-mcp/demo/index.html +3 -7
- package/src/hosted-mcp/demo/login.html +318 -20
- package/src/hosted-mcp/deploy.sh +307 -56
- package/src/hosted-mcp/docs/self-host.md +268 -0
- package/src/hosted-mcp/nginx/codex-relay.conf +25 -0
- package/src/hosted-mcp/nginx/conf.d/redact-logs.conf +60 -0
- package/src/hosted-mcp/nginx/mcp-oauth.conf +58 -0
- package/src/hosted-mcp/nginx/wip.computer.conf +25 -1
- package/src/hosted-mcp/scripts/audit-logs.sh +205 -0
- package/src/hosted-mcp/scripts/verify-deploy.sh +102 -0
- package/src/hosted-mcp/server.mjs +963 -146
package/README.md
CHANGED
|
@@ -22,7 +22,18 @@ Open your AI and paste this:
|
|
|
22
22
|
```
|
|
23
23
|
Read https://wip.computer/install/wip-ldm-os.txt
|
|
24
24
|
|
|
25
|
-
|
|
25
|
+
Use the install document and live local checks as the source of truth. Do not search memory or prior notes for this install.
|
|
26
|
+
|
|
27
|
+
Check if LDM OS is installed (`which ldm && ldm --version`). Branch.
|
|
28
|
+
|
|
29
|
+
If installed: run `ldm status` and show me the version table and what's new for each component. Then ask:
|
|
30
|
+
- Do you have questions?
|
|
31
|
+
- Want to see a dry run?
|
|
32
|
+
|
|
33
|
+
If yes to dry run, run `ldm install --dry-run`.
|
|
34
|
+
|
|
35
|
+
If I say install, run:
|
|
36
|
+
`npm install -g @wipcomputer/wip-ldm-os@latest && ldm install && ldm doctor`
|
|
26
37
|
|
|
27
38
|
If not, walk me through setup and explain:
|
|
28
39
|
|
|
@@ -35,7 +46,14 @@ Then ask:
|
|
|
35
46
|
- Do you have questions?
|
|
36
47
|
- Want to see a dry run?
|
|
37
48
|
|
|
38
|
-
If
|
|
49
|
+
If yes to dry run, install the CLI first:
|
|
50
|
+
`npm install -g @wipcomputer/wip-ldm-os`
|
|
51
|
+
|
|
52
|
+
Then run:
|
|
53
|
+
`ldm init --dry-run`
|
|
54
|
+
|
|
55
|
+
If I say install, run:
|
|
56
|
+
`ldm init`
|
|
39
57
|
|
|
40
58
|
Show me exactly what will change. Don't install anything until I say "install".
|
|
41
59
|
```
|
|
@@ -105,6 +123,8 @@ The OS connects your AIs. Add-ons are what they actually use. Each one is a full
|
|
|
105
123
|
## More Info
|
|
106
124
|
|
|
107
125
|
- [Architecture, principles, and technical details](TECHNICAL.md)
|
|
126
|
+
- [Hosted MCP and relay source](src/hosted-mcp/README.md)
|
|
127
|
+
- [Hosted relay self-host guide](src/hosted-mcp/docs/self-host.md)
|
|
108
128
|
|
|
109
129
|
## License
|
|
110
130
|
|
package/SKILL.md
CHANGED
|
@@ -27,6 +27,12 @@ metadata:
|
|
|
27
27
|
|
|
28
28
|
# LDM OS
|
|
29
29
|
|
|
30
|
+
## Source of truth
|
|
31
|
+
|
|
32
|
+
Use this install document and live local checks as the source of truth. Do not search memory or prior notes for this install.
|
|
33
|
+
|
|
34
|
+
Do not run GitHub release commands during the install-state flow. Do not run `gh release list` or `gh release view` unless the user explicitly asks for release notes. Install-state answers should come from local commands, `ldm status`, and npm package metadata.
|
|
35
|
+
|
|
30
36
|
## Step 1: Check if installed
|
|
31
37
|
|
|
32
38
|
```bash
|
|
@@ -62,12 +68,9 @@ Always a table. Every component with an update gets its own row.
|
|
|
62
68
|
|
|
63
69
|
**Do NOT skip this step.**
|
|
64
70
|
|
|
65
|
-
|
|
66
|
-
gh release list --repo wipcomputer/<repo-name> --limit 5 --json tagName,name --jq '.[]'
|
|
67
|
-
gh release view <tag> --repo wipcomputer/<repo-name> --json body --jq .body
|
|
68
|
-
```
|
|
71
|
+
Use the output of `ldm status`, installed package metadata, and npm metadata. Do not use GitHub release commands here.
|
|
69
72
|
|
|
70
|
-
Translate
|
|
73
|
+
Translate available update information to user language. Every bullet answers "what changed for ME?" If the status output does not include enough detail for a component, say that clearly and do not invent release notes.
|
|
71
74
|
|
|
72
75
|
Good: "Your AIs now explain what LDM OS actually does when you ask them to install it"
|
|
73
76
|
Bad: "Restored rich product content to SKILL.md"
|
package/bin/ldm.js
CHANGED
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
|
|
23
23
|
import { existsSync, readFileSync, writeFileSync, mkdirSync, readdirSync, cpSync, chmodSync, unlinkSync, readlinkSync, renameSync, statSync, lstatSync, symlinkSync } from 'node:fs';
|
|
24
24
|
import { join, basename, resolve, dirname } from 'node:path';
|
|
25
|
-
import { execSync } from 'node:child_process';
|
|
25
|
+
import { execSync, spawnSync } from 'node:child_process';
|
|
26
26
|
import { fileURLToPath } from 'node:url';
|
|
27
27
|
|
|
28
28
|
const __filename = fileURLToPath(import.meta.url);
|
|
@@ -229,6 +229,62 @@ function checkCliVersion() {
|
|
|
229
229
|
}
|
|
230
230
|
}
|
|
231
231
|
|
|
232
|
+
function selectedLdmNpmTrack() {
|
|
233
|
+
return ALPHA_FLAG ? 'alpha' : BETA_FLAG ? 'beta' : 'latest';
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
function selectedLdmTrackLabel(npmTag) {
|
|
237
|
+
return npmTag === 'latest' ? '' : ` (${npmTag} track)`;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
function latestLdmCliForSelectedTrack() {
|
|
241
|
+
const npmTag = selectedLdmNpmTrack();
|
|
242
|
+
const npmViewCmd = npmTag === 'latest'
|
|
243
|
+
? 'npm view @wipcomputer/wip-ldm-os version 2>/dev/null'
|
|
244
|
+
: `npm view @wipcomputer/wip-ldm-os dist-tags.${npmTag} 2>/dev/null`;
|
|
245
|
+
const latest = execSync(npmViewCmd, {
|
|
246
|
+
encoding: 'utf8',
|
|
247
|
+
timeout: 15000,
|
|
248
|
+
}).trim();
|
|
249
|
+
return { latest, npmTag };
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
function maybeSelfUpdateLdmCliBeforeInstall() {
|
|
253
|
+
// This shared preflight covers both bare `ldm install` and targeted
|
|
254
|
+
// `ldm install <app>`. Dry runs never update, but still disclose skew.
|
|
255
|
+
if (process.env.LDM_SELF_UPDATED) return;
|
|
256
|
+
|
|
257
|
+
try {
|
|
258
|
+
const { latest, npmTag } = latestLdmCliForSelectedTrack();
|
|
259
|
+
if (!latest || !semverNewer(latest, PKG_VERSION)) return;
|
|
260
|
+
|
|
261
|
+
const trackLabel = selectedLdmTrackLabel(npmTag);
|
|
262
|
+
|
|
263
|
+
if (DRY_RUN) {
|
|
264
|
+
console.log(` LDM OS CLI v${PKG_VERSION} -> v${latest}${trackLabel} is available.`);
|
|
265
|
+
console.log(` Dry run only: continuing with v${PKG_VERSION}.`);
|
|
266
|
+
console.log('');
|
|
267
|
+
return;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
console.log(` LDM OS CLI v${PKG_VERSION} -> v${latest}${trackLabel}. Updating first...`);
|
|
271
|
+
try {
|
|
272
|
+
execSync(`npm install -g @wipcomputer/wip-ldm-os@${latest}`, { stdio: 'inherit', timeout: 60000 });
|
|
273
|
+
console.log(` CLI updated to v${latest}. Re-running with new code...`);
|
|
274
|
+
console.log('');
|
|
275
|
+
const reArgs = process.argv.slice(2);
|
|
276
|
+
const child = spawnSync('ldm', reArgs.length > 0 ? reArgs : ['install'], {
|
|
277
|
+
stdio: 'inherit',
|
|
278
|
+
env: { ...process.env, LDM_SELF_UPDATED: '1' },
|
|
279
|
+
});
|
|
280
|
+
if (child.error) throw child.error;
|
|
281
|
+
process.exit(child.status ?? 1);
|
|
282
|
+
} catch (e) {
|
|
283
|
+
console.log(` ! Self-update failed: ${e.message}. Continuing with v${PKG_VERSION}.`);
|
|
284
|
+
}
|
|
285
|
+
} catch {}
|
|
286
|
+
}
|
|
287
|
+
|
|
232
288
|
// ── Dead backup trigger cleanup (#207) ──
|
|
233
289
|
// Three backup systems were competing. Only ai.openclaw.ldm-backup (3am) works.
|
|
234
290
|
// This removes: broken cron entry (LDMDevTools.app), old com.wipcomputer.daily-backup.
|
|
@@ -738,6 +794,24 @@ function deployDocs() {
|
|
|
738
794
|
console.log(` + ${agentDocsCount} personalized doc(s) deployed to ${agentLibraryDest.replace(HOME, '~')}/`);
|
|
739
795
|
}
|
|
740
796
|
|
|
797
|
+
// Migration-window compatibility write (added 2026-04-30).
|
|
798
|
+
// dev-guide-wipcomputerinc.md was migrated from ~/.ldm/shared/ to
|
|
799
|
+
// ~/.ldm/library/documentation/ on 2026-04-19, but agent boot files,
|
|
800
|
+
// ~/.claude/rules/, and other consumers still reference the old shared
|
|
801
|
+
// path. Without this write, the old path serves stale content and
|
|
802
|
+
// agents reading by the old path get pre-migration policy.
|
|
803
|
+
// Forward-migration (grep-update consumers, then remove this compat
|
|
804
|
+
// write) is tracked separately. See bugs/installer/ ticket
|
|
805
|
+
// 2026-04-30--cc-mini--dev-guide-split-path-migration.md.
|
|
806
|
+
const devGuideName = 'dev-guide-wipcomputerinc.md';
|
|
807
|
+
const devGuideNew = join(agentLibraryDest, devGuideName);
|
|
808
|
+
const devGuideOld = join(LDM_ROOT, 'shared', devGuideName);
|
|
809
|
+
if (existsSync(devGuideNew)) {
|
|
810
|
+
mkdirSync(dirname(devGuideOld), { recursive: true });
|
|
811
|
+
cpSync(devGuideNew, devGuideOld);
|
|
812
|
+
console.log(` + Compat write: ${devGuideName} also deployed to ${devGuideOld.replace(HOME, '~')} (migration window)`);
|
|
813
|
+
}
|
|
814
|
+
|
|
741
815
|
return docsCount + agentDocsCount;
|
|
742
816
|
}
|
|
743
817
|
|
|
@@ -1422,23 +1496,6 @@ async function showCatalogPicker() {
|
|
|
1422
1496
|
// ── ldm install ──
|
|
1423
1497
|
|
|
1424
1498
|
async function cmdInstall() {
|
|
1425
|
-
if (!DRY_RUN && !acquireInstallLock()) return;
|
|
1426
|
-
|
|
1427
|
-
// Ensure LDM is initialized
|
|
1428
|
-
if (!existsSync(VERSION_PATH)) {
|
|
1429
|
-
console.log(' LDM OS not initialized. Running init first...');
|
|
1430
|
-
console.log('');
|
|
1431
|
-
cmdInit();
|
|
1432
|
-
}
|
|
1433
|
-
|
|
1434
|
-
const { setFlags, installFromPath, installSingleTool, installToolbox, detectHarnesses } = await import('../lib/deploy.mjs');
|
|
1435
|
-
const { detectInterfacesJSON } = await import('../lib/detect.mjs');
|
|
1436
|
-
|
|
1437
|
-
// Refresh harness detection (catches newly installed harnesses)
|
|
1438
|
-
detectHarnesses();
|
|
1439
|
-
|
|
1440
|
-
setFlags({ dryRun: DRY_RUN, jsonOutput: JSON_OUTPUT, origin: 'manual' });
|
|
1441
|
-
|
|
1442
1499
|
// --help flag (#81)
|
|
1443
1500
|
if (args.includes('--help') || args.includes('-h')) {
|
|
1444
1501
|
console.log(`
|
|
@@ -1458,6 +1515,25 @@ async function cmdInstall() {
|
|
|
1458
1515
|
process.exit(0);
|
|
1459
1516
|
}
|
|
1460
1517
|
|
|
1518
|
+
maybeSelfUpdateLdmCliBeforeInstall();
|
|
1519
|
+
|
|
1520
|
+
if (!DRY_RUN && !acquireInstallLock()) return;
|
|
1521
|
+
|
|
1522
|
+
// Ensure LDM is initialized
|
|
1523
|
+
if (!existsSync(VERSION_PATH)) {
|
|
1524
|
+
console.log(' LDM OS not initialized. Running init first...');
|
|
1525
|
+
console.log('');
|
|
1526
|
+
cmdInit();
|
|
1527
|
+
}
|
|
1528
|
+
|
|
1529
|
+
const { setFlags, installFromPath, installSingleTool, installToolbox, detectHarnesses } = await import('../lib/deploy.mjs');
|
|
1530
|
+
const { detectInterfacesJSON } = await import('../lib/detect.mjs');
|
|
1531
|
+
|
|
1532
|
+
// Refresh harness detection (catches newly installed harnesses)
|
|
1533
|
+
detectHarnesses();
|
|
1534
|
+
|
|
1535
|
+
setFlags({ dryRun: DRY_RUN, jsonOutput: JSON_OUTPUT, origin: 'manual' });
|
|
1536
|
+
|
|
1461
1537
|
// Find the target (skip flags)
|
|
1462
1538
|
const target = args.slice(1).find(a => !a.startsWith('--'));
|
|
1463
1539
|
|
|
@@ -1903,38 +1979,6 @@ async function cmdInstallCatalog() {
|
|
|
1903
1979
|
// No lock here. cmdInstall() already holds it when calling this.
|
|
1904
1980
|
installLog(`ldm install started (v${PKG_VERSION}, DRY_RUN=${DRY_RUN})`);
|
|
1905
1981
|
|
|
1906
|
-
// Self-update: check if CLI itself is outdated. Update first, then re-exec.
|
|
1907
|
-
// This breaks the chicken-and-egg: new features in ldm install are always
|
|
1908
|
-
// available because the installer upgrades itself before doing anything else.
|
|
1909
|
-
// --alpha and --beta flags check the corresponding npm dist-tag instead of @latest.
|
|
1910
|
-
if (!DRY_RUN && !process.env.LDM_SELF_UPDATED) {
|
|
1911
|
-
try {
|
|
1912
|
-
const npmTag = ALPHA_FLAG ? 'alpha' : BETA_FLAG ? 'beta' : 'latest';
|
|
1913
|
-
const trackLabel = npmTag === 'latest' ? '' : ` (${npmTag} track)`;
|
|
1914
|
-
const npmViewCmd = npmTag === 'latest'
|
|
1915
|
-
? 'npm view @wipcomputer/wip-ldm-os version 2>/dev/null'
|
|
1916
|
-
: `npm view @wipcomputer/wip-ldm-os dist-tags.${npmTag} 2>/dev/null`;
|
|
1917
|
-
const latest = execSync(npmViewCmd, {
|
|
1918
|
-
encoding: 'utf8', timeout: 15000,
|
|
1919
|
-
}).trim();
|
|
1920
|
-
if (latest && semverNewer(latest, PKG_VERSION)) {
|
|
1921
|
-
console.log(` LDM OS CLI v${PKG_VERSION} -> v${latest}${trackLabel}. Updating first...`);
|
|
1922
|
-
try {
|
|
1923
|
-
execSync(`npm install -g @wipcomputer/wip-ldm-os@${latest}`, { stdio: 'inherit', timeout: 60000 });
|
|
1924
|
-
console.log(` CLI updated to v${latest}. Re-running with new code...`);
|
|
1925
|
-
console.log('');
|
|
1926
|
-
// Re-exec with the new binary. LDM_SELF_UPDATED prevents infinite loop.
|
|
1927
|
-
// process.argv.slice(2) skips 'node' and the script path, keeps just 'install' + flags
|
|
1928
|
-
const reArgs = process.argv.slice(2).join(' ') || 'install';
|
|
1929
|
-
execSync(`LDM_SELF_UPDATED=1 ldm ${reArgs}`, { stdio: 'inherit' });
|
|
1930
|
-
process.exit(0);
|
|
1931
|
-
} catch (e) {
|
|
1932
|
-
console.log(` ! Self-update failed: ${e.message}. Continuing with v${PKG_VERSION}.`);
|
|
1933
|
-
}
|
|
1934
|
-
}
|
|
1935
|
-
} catch {}
|
|
1936
|
-
}
|
|
1937
|
-
|
|
1938
1982
|
autoDetectExtensions();
|
|
1939
1983
|
|
|
1940
1984
|
// Migrate old registry entries to v2 format (#262)
|
|
@@ -3987,7 +4031,7 @@ async function main() {
|
|
|
3987
4031
|
console.log(' Module ... ESM main/exports -> importable');
|
|
3988
4032
|
console.log(' MCP Server ... mcp-server.mjs -> claude mcp add --scope user');
|
|
3989
4033
|
console.log(' OpenClaw ... openclaw.plugin.json -> ~/.ldm/extensions/ + ~/.openclaw/extensions/');
|
|
3990
|
-
console.log(' Skill ... SKILL.md
|
|
4034
|
+
console.log(' Skill ... SKILL.md or skills/<name>/SKILL.md -> agent skill paths');
|
|
3991
4035
|
console.log(' CC Hook ... guard.mjs or claudeCode.hook -> ~/.claude/settings.json');
|
|
3992
4036
|
console.log('');
|
|
3993
4037
|
console.log(` v${PKG_VERSION}`);
|
|
@@ -144,13 +144,15 @@ A plugin for OpenClaw agents. Lifecycle hooks, tool registration, settings.
|
|
|
144
144
|
|
|
145
145
|
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).
|
|
146
146
|
|
|
147
|
-
**Convention:** `SKILL.md` at the repo root. YAML frontmatter
|
|
147
|
+
**Convention:** either `SKILL.md` at the repo root, or one or more skill folders at `skills/<skill-name>/SKILL.md`. YAML frontmatter includes name and description. Optional `references/`, `agents/`, `scripts/`, and `assets/` directories live beside `SKILL.md`.
|
|
148
148
|
|
|
149
149
|
**Platform variants:** Codex CLI reads `AGENTS.md` instead of `SKILL.md`, with the same role and the same content shape. Treat `AGENTS.md` as the Codex-flavored filename for this same interface, not a separate interface. A repo may ship both (or symlink one to the other) so it works in Codex and SKILL.md-aware agents.
|
|
150
150
|
|
|
151
|
-
**Detection:** `SKILL.md` exists.
|
|
151
|
+
**Detection:** `SKILL.md` exists, or at least one `skills/<skill-name>/SKILL.md` exists.
|
|
152
152
|
|
|
153
|
-
**Install:**
|
|
153
|
+
**Install:** the skill folder is deployed to every supported local agent skill surface, including OpenClaw, Codex, Claude Code, and WIP agent compatibility paths when present. If `references/` exists, it is deployed alongside SKILL.md and to `settings/docs/skills/<name>/` in the workspace.
|
|
154
|
+
|
|
155
|
+
**Npm package shape:** a public skill package can expose `SKILL.md` at package root. For example, `@wipcomputer/wip-ai-chat-ui` is sourced from a private repo folder at `design/skills/wip-ai-chat-ui/`, but publishes a tarball with `SKILL.md`, `agents/`, and `references/` at package root so `ldm install @wipcomputer/wip-ai-chat-ui` installs the skill directly.
|
|
154
156
|
|
|
155
157
|
**Structure:**
|
|
156
158
|
```
|
|
@@ -162,6 +164,17 @@ repo/
|
|
|
162
164
|
└── ...
|
|
163
165
|
```
|
|
164
166
|
|
|
167
|
+
Multiple skills can also live in one repo:
|
|
168
|
+
|
|
169
|
+
```text
|
|
170
|
+
repo/
|
|
171
|
+
└── skills/
|
|
172
|
+
└── wip-ai-chat-ui/
|
|
173
|
+
├── SKILL.md
|
|
174
|
+
├── agents/
|
|
175
|
+
└── references/
|
|
176
|
+
```
|
|
177
|
+
|
|
165
178
|
**Key rules (from Agent Skills Spec):**
|
|
166
179
|
- SKILL.md body < 5000 tokens. Process goes in SKILL.md, context goes in references/.
|
|
167
180
|
- Imperative language: "Run this command" not "This product enables..."
|
|
@@ -148,13 +148,13 @@ A plugin for OpenClaw agents. Lifecycle hooks, tool registration, settings.
|
|
|
148
148
|
|
|
149
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).
|
|
150
150
|
|
|
151
|
-
**Convention:** `SKILL.md` at the repo root. Optional `references/`
|
|
151
|
+
**Convention:** either `SKILL.md` at the repo root, or one or more skill folders at `skills/<skill-name>/SKILL.md`. Optional `references/`, `agents/`, `scripts/`, and `assets/` directories live beside `SKILL.md`.
|
|
152
152
|
|
|
153
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
154
|
|
|
155
|
-
**Detection:** `SKILL.md` exists.
|
|
155
|
+
**Detection:** `SKILL.md` exists, or at least one `skills/<skill-name>/SKILL.md` exists.
|
|
156
156
|
|
|
157
|
-
**Install:** `ldm install` deploys
|
|
157
|
+
**Install:** `ldm install` deploys the skill folder to every supported local agent skill surface, including OpenClaw, Codex, Claude Code, and WIP agent compatibility paths when present. If `references/` exists, it is deployed alongside and also to `settings/docs/skills/<name>/` in the workspace so all agents can read them.
|
|
158
158
|
|
|
159
159
|
**Key rules:**
|
|
160
160
|
- SKILL.md body < 5000 tokens. Process in SKILL.md, context in references/.
|
|
@@ -364,7 +364,7 @@ ldm install # update all
|
|
|
364
364
|
| `mcp-server.mjs` | MCP (local stdio) | Adds `command` + `args` entry to `.mcp.json` |
|
|
365
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)).** |
|
|
366
366
|
| `openclaw.plugin.json` | OpenClaw | Copies to `~/.openclaw/extensions/` |
|
|
367
|
-
| `SKILL.md` | Skill |
|
|
367
|
+
| `SKILL.md` or `skills/<name>/SKILL.md` | Skill | Deploys skill folder to supported agent skill paths |
|
|
368
368
|
| `guard.mjs` or `claudeCode.hook` | CC Hook | Adds to `~/.claude/settings.json` |
|
|
369
369
|
| `.claude-plugin/plugin.json` | CC Plugin | Registers with Claude Code marketplace |
|
|
370
370
|
|
package/lib/deploy.mjs
CHANGED
|
@@ -177,6 +177,14 @@ export function detectHarnesses() {
|
|
|
177
177
|
skills: join(codexHome, 'skills'),
|
|
178
178
|
};
|
|
179
179
|
|
|
180
|
+
// WIP agent skill compatibility directory
|
|
181
|
+
const agentsHome = join(HOME, '.agents');
|
|
182
|
+
harnesses['wip-agents'] = {
|
|
183
|
+
detected: existsSync(agentsHome),
|
|
184
|
+
home: agentsHome,
|
|
185
|
+
skills: join(agentsHome, 'skills'),
|
|
186
|
+
};
|
|
187
|
+
|
|
180
188
|
// Cursor
|
|
181
189
|
const cursorHome = join(HOME, '.cursor');
|
|
182
190
|
harnesses['cursor'] = {
|
|
@@ -207,7 +215,7 @@ export function detectHarnesses() {
|
|
|
207
215
|
function getHarnesses() {
|
|
208
216
|
try {
|
|
209
217
|
const config = readJSON(LDM_CONFIG_PATH) || {};
|
|
210
|
-
if (config.harnesses) {
|
|
218
|
+
if (config.harnesses && config.harnesses['wip-agents']) {
|
|
211
219
|
const workspace = (config.workspace || '').replace('~', HOME);
|
|
212
220
|
return { harnesses: config.harnesses, workspace };
|
|
213
221
|
}
|
|
@@ -1236,11 +1244,77 @@ function installClaudeCodeHookEvent(repoPath, door, toolName = basename(repoPath
|
|
|
1236
1244
|
}
|
|
1237
1245
|
}
|
|
1238
1246
|
|
|
1239
|
-
function
|
|
1247
|
+
function copySkillTree(skillDir, dest, copyFullFolder = false) {
|
|
1248
|
+
mkdirSync(dest, { recursive: true });
|
|
1249
|
+
if (copyFullFolder) {
|
|
1250
|
+
cpSync(skillDir, dest, { recursive: true });
|
|
1251
|
+
return;
|
|
1252
|
+
}
|
|
1253
|
+
|
|
1254
|
+
cpSync(join(skillDir, 'SKILL.md'), join(dest, 'SKILL.md'));
|
|
1255
|
+
for (const child of ['references', 'agents', 'scripts', 'assets']) {
|
|
1256
|
+
const src = join(skillDir, child);
|
|
1257
|
+
if (existsSync(src)) cpSync(src, join(dest, child), { recursive: true });
|
|
1258
|
+
}
|
|
1259
|
+
}
|
|
1260
|
+
|
|
1261
|
+
const SKILL_COMPANION_DIRS = ['references', 'agents', 'scripts', 'assets'];
|
|
1262
|
+
|
|
1263
|
+
function listSkillCopyEntries(skillDir, copyFullFolder = false) {
|
|
1264
|
+
if (copyFullFolder) {
|
|
1265
|
+
try {
|
|
1266
|
+
return readdirSync(skillDir)
|
|
1267
|
+
.filter((entry) => entry !== '.DS_Store')
|
|
1268
|
+
.sort();
|
|
1269
|
+
} catch {
|
|
1270
|
+
return ['SKILL.md'];
|
|
1271
|
+
}
|
|
1272
|
+
}
|
|
1273
|
+
|
|
1274
|
+
const entries = ['SKILL.md'];
|
|
1275
|
+
for (const child of SKILL_COMPANION_DIRS) {
|
|
1276
|
+
if (existsSync(join(skillDir, child))) entries.push(`${child}/`);
|
|
1277
|
+
}
|
|
1278
|
+
return entries;
|
|
1279
|
+
}
|
|
1280
|
+
|
|
1281
|
+
function printSkillDryRunPlan({ sourceSkillDir, refsSrc, toolName, harnesses, workspace, entries }) {
|
|
1282
|
+
const formatTargetEntries = (base) => entries.map((entry) => join(base, entry));
|
|
1283
|
+
const harnessTargets = Object.entries(harnesses)
|
|
1284
|
+
.filter(([, h]) => h.detected && h.skills)
|
|
1285
|
+
.map(([name, h]) => ({ name, base: join(h.skills, toolName) }));
|
|
1286
|
+
|
|
1287
|
+
ok(`Skill: ${toolName}`);
|
|
1288
|
+
log(`Source: ${sourceSkillDir}`);
|
|
1289
|
+
log('Would copy:');
|
|
1290
|
+
for (const entry of entries) log(`- ${entry}`);
|
|
1291
|
+
|
|
1292
|
+
const permanentBase = join(LDM_EXTENSIONS, toolName);
|
|
1293
|
+
log('Permanent copy:');
|
|
1294
|
+
for (const target of formatTargetEntries(permanentBase)) log(`- ${target}`);
|
|
1295
|
+
|
|
1296
|
+
if (harnessTargets.length > 0) {
|
|
1297
|
+
log('Agent skill targets:');
|
|
1298
|
+
for (const target of harnessTargets) {
|
|
1299
|
+
log(`- ${target.name}: ${target.base}`);
|
|
1300
|
+
for (const entryTarget of formatTargetEntries(target.base)) log(` - ${entryTarget}`);
|
|
1301
|
+
}
|
|
1302
|
+
} else {
|
|
1303
|
+
log('Agent skill targets: no detected skill harnesses');
|
|
1304
|
+
}
|
|
1305
|
+
|
|
1306
|
+
if (existsSync(refsSrc) && workspace && existsSync(workspace)) {
|
|
1307
|
+
const homeRefsDest = join(workspace, 'settings', 'docs', 'skills', toolName);
|
|
1308
|
+
log('Workspace docs target:');
|
|
1309
|
+
log(`- ${homeRefsDest} (references/ only)`);
|
|
1310
|
+
}
|
|
1311
|
+
}
|
|
1312
|
+
|
|
1313
|
+
function installSkillFolder(skillDir, toolName, opts = {}) {
|
|
1240
1314
|
const { harnesses, workspace } = getHarnesses();
|
|
1241
1315
|
|
|
1242
|
-
// Find SKILL.md source:
|
|
1243
|
-
let skillSrc = join(
|
|
1316
|
+
// Find SKILL.md source: skill dir first, then permanent copy at ~/.ldm/extensions/
|
|
1317
|
+
let skillSrc = join(skillDir, 'SKILL.md');
|
|
1244
1318
|
const permanentSkill = join(LDM_EXTENSIONS, toolName, 'SKILL.md');
|
|
1245
1319
|
if (!existsSync(skillSrc) && existsSync(permanentSkill)) skillSrc = permanentSkill;
|
|
1246
1320
|
if (!existsSync(skillSrc)) return false;
|
|
@@ -1251,14 +1325,16 @@ function installSkill(repoPath, toolName) {
|
|
|
1251
1325
|
return false;
|
|
1252
1326
|
}
|
|
1253
1327
|
|
|
1254
|
-
|
|
1255
|
-
|
|
1328
|
+
const sourceSkillDir = dirname(skillSrc);
|
|
1329
|
+
|
|
1330
|
+
// Find references/ source: skill dir first, then permanent copy
|
|
1331
|
+
let refsSrc = join(skillDir, 'references');
|
|
1256
1332
|
const permanentRefs = join(LDM_EXTENSIONS, toolName, 'references');
|
|
1257
1333
|
if (!existsSync(refsSrc) && existsSync(permanentRefs)) refsSrc = permanentRefs;
|
|
1258
1334
|
|
|
1259
1335
|
if (DRY_RUN) {
|
|
1260
|
-
const
|
|
1261
|
-
|
|
1336
|
+
const entries = listSkillCopyEntries(sourceSkillDir, opts.copyFullFolder);
|
|
1337
|
+
printSkillDryRunPlan({ sourceSkillDir, refsSrc, toolName, harnesses, workspace, entries });
|
|
1262
1338
|
return true;
|
|
1263
1339
|
}
|
|
1264
1340
|
|
|
@@ -1267,21 +1343,13 @@ function installSkill(repoPath, toolName) {
|
|
|
1267
1343
|
|
|
1268
1344
|
// 1. Save permanent copy to ~/.ldm/extensions/<name>/ (survives tmp cleanup)
|
|
1269
1345
|
const ldmSkillDir = join(LDM_EXTENSIONS, toolName);
|
|
1270
|
-
|
|
1271
|
-
cpSync(skillSrc, join(ldmSkillDir, 'SKILL.md'));
|
|
1272
|
-
if (existsSync(refsSrc) && refsSrc !== permanentRefs) {
|
|
1273
|
-
cpSync(refsSrc, join(ldmSkillDir, 'references'), { recursive: true });
|
|
1274
|
-
}
|
|
1346
|
+
copySkillTree(sourceSkillDir, ldmSkillDir, opts.copyFullFolder);
|
|
1275
1347
|
|
|
1276
1348
|
// 2. Deploy to every detected harness that has a skills path
|
|
1277
1349
|
for (const [name, harness] of Object.entries(harnesses)) {
|
|
1278
1350
|
if (!harness.detected || !harness.skills) continue;
|
|
1279
1351
|
const dest = join(harness.skills, toolName);
|
|
1280
|
-
|
|
1281
|
-
cpSync(skillSrc, join(dest, 'SKILL.md'));
|
|
1282
|
-
if (existsSync(refsSrc)) {
|
|
1283
|
-
cpSync(refsSrc, join(dest, 'references'), { recursive: true });
|
|
1284
|
-
}
|
|
1352
|
+
copySkillTree(sourceSkillDir, dest, opts.copyFullFolder);
|
|
1285
1353
|
deployed.push(name);
|
|
1286
1354
|
}
|
|
1287
1355
|
|
|
@@ -1301,6 +1369,18 @@ function installSkill(repoPath, toolName) {
|
|
|
1301
1369
|
}
|
|
1302
1370
|
}
|
|
1303
1371
|
|
|
1372
|
+
function installSkill(repoPath, toolName, skillInfo = null) {
|
|
1373
|
+
if (Array.isArray(skillInfo?.skills) && skillInfo.skills.length > 0) {
|
|
1374
|
+
let installed = 0;
|
|
1375
|
+
for (const skill of skillInfo.skills) {
|
|
1376
|
+
if (installSkillFolder(skill.path, skill.name, { copyFullFolder: true })) installed++;
|
|
1377
|
+
}
|
|
1378
|
+
return installed > 0;
|
|
1379
|
+
}
|
|
1380
|
+
|
|
1381
|
+
return installSkillFolder(repoPath, toolName);
|
|
1382
|
+
}
|
|
1383
|
+
|
|
1304
1384
|
// ── Single tool install ──
|
|
1305
1385
|
|
|
1306
1386
|
export function installSingleTool(toolPath) {
|
|
@@ -1361,6 +1441,10 @@ export function installSingleTool(toolPath) {
|
|
|
1361
1441
|
}
|
|
1362
1442
|
}
|
|
1363
1443
|
|
|
1444
|
+
if (interfaces.skill) {
|
|
1445
|
+
installSkill(toolPath, toolName, interfaces.skill);
|
|
1446
|
+
}
|
|
1447
|
+
|
|
1364
1448
|
return ifaceNames.length;
|
|
1365
1449
|
}
|
|
1366
1450
|
|
|
@@ -1429,7 +1513,7 @@ export function installSingleTool(toolPath) {
|
|
|
1429
1513
|
|
|
1430
1514
|
if (interfaces.skill) {
|
|
1431
1515
|
// Skills always deploy. They're instruction files, not running code.
|
|
1432
|
-
if (installSkill(toolPath, toolName)) installed++;
|
|
1516
|
+
if (installSkill(toolPath, toolName, interfaces.skill)) installed++;
|
|
1433
1517
|
}
|
|
1434
1518
|
|
|
1435
1519
|
if (interfaces.module) {
|
|
@@ -1590,7 +1674,7 @@ export async function enableExtension(name) {
|
|
|
1590
1674
|
|
|
1591
1675
|
if (interfaces.mcp) registerMCP(extPath, interfaces.mcp, name);
|
|
1592
1676
|
if (interfaces.claudeCodeHook) installClaudeCodeHook(extPath, interfaces.claudeCodeHook);
|
|
1593
|
-
if (interfaces.skill) installSkill(extPath, name);
|
|
1677
|
+
if (interfaces.skill) installSkill(extPath, name, interfaces.skill);
|
|
1594
1678
|
|
|
1595
1679
|
entry.enabled = true;
|
|
1596
1680
|
entry.updatedAt = new Date().toISOString();
|
package/lib/detect.mjs
CHANGED
|
@@ -15,6 +15,23 @@ function readJSON(path) {
|
|
|
15
15
|
}
|
|
16
16
|
}
|
|
17
17
|
|
|
18
|
+
function detectSkillDirectories(repoPath) {
|
|
19
|
+
const skillsDir = join(repoPath, 'skills');
|
|
20
|
+
if (!existsSync(skillsDir)) return [];
|
|
21
|
+
|
|
22
|
+
try {
|
|
23
|
+
return readdirSync(skillsDir, { withFileTypes: true })
|
|
24
|
+
.filter(e => e.isDirectory() && existsSync(join(skillsDir, e.name, 'SKILL.md')))
|
|
25
|
+
.map(e => ({
|
|
26
|
+
name: e.name,
|
|
27
|
+
path: join(skillsDir, e.name),
|
|
28
|
+
skillPath: join(skillsDir, e.name, 'SKILL.md'),
|
|
29
|
+
}));
|
|
30
|
+
} catch {
|
|
31
|
+
return [];
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
18
35
|
/**
|
|
19
36
|
* Detect all interfaces in a repo.
|
|
20
37
|
* Returns { interfaces, pkg } where interfaces is an object keyed by interface type.
|
|
@@ -48,9 +65,18 @@ export function detectInterfaces(repoPath) {
|
|
|
48
65
|
interfaces.openclaw = { config: readJSON(ocPlugin), path: ocPlugin };
|
|
49
66
|
}
|
|
50
67
|
|
|
51
|
-
// 5. Skill: SKILL.md
|
|
52
|
-
|
|
53
|
-
|
|
68
|
+
// 5. Skill: root SKILL.md, or a skills/<name>/SKILL.md collection.
|
|
69
|
+
const rootSkillPath = join(repoPath, 'SKILL.md');
|
|
70
|
+
if (existsSync(rootSkillPath)) {
|
|
71
|
+
interfaces.skill = { path: rootSkillPath };
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const skillDirs = detectSkillDirectories(repoPath);
|
|
75
|
+
if (skillDirs.length > 0) {
|
|
76
|
+
interfaces.skill = {
|
|
77
|
+
path: join(repoPath, 'skills'),
|
|
78
|
+
skills: skillDirs,
|
|
79
|
+
};
|
|
54
80
|
}
|
|
55
81
|
|
|
56
82
|
// 6. Claude Code Hook: guard.mjs or claudeCode.hook(s) in package.json
|
|
@@ -103,7 +129,12 @@ export function describeInterfaces(interfaces) {
|
|
|
103
129
|
if (interfaces.module) lines.push(`Module: ${JSON.stringify(interfaces.module.main)}`);
|
|
104
130
|
if (interfaces.mcp) lines.push(`MCP Server: ${interfaces.mcp.file}`);
|
|
105
131
|
if (interfaces.openclaw) lines.push(`OpenClaw Plugin: ${interfaces.openclaw.config?.name || 'detected'}`);
|
|
106
|
-
if (interfaces.skill)
|
|
132
|
+
if (interfaces.skill?.skills?.length) {
|
|
133
|
+
const skills = interfaces.skill.skills.map(s => `${s.name} (${s.skillPath})`);
|
|
134
|
+
lines.push(`Skill: ${skills.join(', ')}`);
|
|
135
|
+
} else if (interfaces.skill) {
|
|
136
|
+
lines.push(`Skill: SKILL.md`);
|
|
137
|
+
}
|
|
107
138
|
if (interfaces.claudeCodeHook) {
|
|
108
139
|
const events = interfaces.claudeCodeHook.map(h => h.event || 'PreToolUse');
|
|
109
140
|
lines.push(`Claude Code Hook: ${events.join(', ')}`);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@wipcomputer/wip-ldm-os",
|
|
3
|
-
"version": "0.4.85-alpha.
|
|
3
|
+
"version": "0.4.85-alpha.20",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "LDM OS: identity, memory, and sovereignty infrastructure for AI agents",
|
|
6
6
|
"engines": {
|
|
@@ -18,14 +18,24 @@
|
|
|
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 && npm run validate:bin-manifest",
|
|
21
|
+
"prepublishOnly": "npm run build:bridge && npm run validate:bin-manifest && npm run test:install-prompt-policy",
|
|
22
22
|
"validate:bin-manifest": "node scripts/validate-bin-manifest.mjs",
|
|
23
23
|
"test:skill-frontmatter": "node scripts/test-skill-frontmatter.mjs",
|
|
24
|
+
"test:install-prompt-policy": "node scripts/test-install-prompt-policy.mjs",
|
|
24
25
|
"test:installer-update-tracks": "node scripts/test-installer-update-tracks.mjs",
|
|
25
26
|
"test:installer-hook-toolname": "node scripts/test-installer-hook-toolname.mjs",
|
|
27
|
+
"test:installer-target-self-update": "node scripts/test-installer-target-self-update.mjs",
|
|
28
|
+
"test:installer-skill-directory": "node scripts/test-installer-skill-directory.mjs",
|
|
29
|
+
"test:installer-skill-dry-run-destinations": "node scripts/test-installer-skill-dry-run-destinations.mjs",
|
|
26
30
|
"test:ldm-install-bin-shim": "node scripts/test-ldm-install-preserves-foreign-bin.mjs",
|
|
27
31
|
"test:doctor-cron-target": "node scripts/test-doctor-cron-target.mjs",
|
|
28
32
|
"test:bin-manifest": "node scripts/test-bin-manifest.mjs",
|
|
33
|
+
"test:crc-agentid-tenant-boundary": "node scripts/test-crc-agentid-tenant-boundary.mjs",
|
|
34
|
+
"test:crc-pair-login-flow": "node scripts/test-crc-pair-login-flow.mjs",
|
|
35
|
+
"test:crc-pair-status-poll-token": "node scripts/test-crc-pair-status-poll-token.mjs",
|
|
36
|
+
"test:crc-pair-relink-audit-and-rotation": "node scripts/test-crc-pair-relink-audit-and-rotation.mjs",
|
|
37
|
+
"test:crc-e2ee-key-persistence": "node scripts/test-crc-e2ee-key-persistence.mjs",
|
|
38
|
+
"test:crc-e2ee-session-route": "node scripts/test-crc-e2ee-session-route.mjs",
|
|
29
39
|
"fmt": "npx prettier --write 'src/**/*.{ts,mjs}' 'lib/**/*.mjs' 'bin/**/*.js'",
|
|
30
40
|
"fmt:check": "npx prettier --check 'src/**/*.{ts,mjs}' 'lib/**/*.mjs' 'bin/**/*.js'"
|
|
31
41
|
},
|