@wipcomputer/wip-ldm-os 0.4.84 → 0.4.85-alpha.10
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/bin/ldm.js +197 -5
- package/docs/universal-installer/SPEC.md +16 -3
- package/docs/universal-installer/TECHNICAL.md +4 -4
- package/lib/deploy.mjs +108 -25
- package/lib/detect.mjs +35 -4
- package/package.json +9 -1
- package/scripts/test-crc-agentid-tenant-boundary.mjs +80 -0
- package/scripts/test-crc-e2ee-key-persistence.mjs +80 -0
- package/scripts/test-crc-e2ee-session-route.mjs +122 -0
- package/scripts/test-crc-pair-login-flow.mjs +40 -0
- package/scripts/test-crc-pair-status-poll-token.mjs +73 -0
- package/scripts/test-installer-hook-toolname.mjs +80 -0
- package/scripts/test-installer-skill-directory.mjs +55 -0
- package/scripts/test-installer-skill-dry-run-destinations.mjs +100 -0
- package/src/hosted-mcp/app/footer.js +74 -0
- package/src/hosted-mcp/app/kaleidoscope-login.html +843 -0
- package/src/hosted-mcp/app/pair.html +147 -57
- package/src/hosted-mcp/app/sprites.png +0 -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 +306 -56
- package/src/hosted-mcp/nginx/codex-relay.conf +134 -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 +69 -71
- 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 +886 -128
- package/src/hosted-mcp/app/codex-remote-control/index.html +0 -254
package/bin/ldm.js
CHANGED
|
@@ -738,6 +738,24 @@ function deployDocs() {
|
|
|
738
738
|
console.log(` + ${agentDocsCount} personalized doc(s) deployed to ${agentLibraryDest.replace(HOME, '~')}/`);
|
|
739
739
|
}
|
|
740
740
|
|
|
741
|
+
// Migration-window compatibility write (added 2026-04-30).
|
|
742
|
+
// dev-guide-wipcomputerinc.md was migrated from ~/.ldm/shared/ to
|
|
743
|
+
// ~/.ldm/library/documentation/ on 2026-04-19, but agent boot files,
|
|
744
|
+
// ~/.claude/rules/, and other consumers still reference the old shared
|
|
745
|
+
// path. Without this write, the old path serves stale content and
|
|
746
|
+
// agents reading by the old path get pre-migration policy.
|
|
747
|
+
// Forward-migration (grep-update consumers, then remove this compat
|
|
748
|
+
// write) is tracked separately. See bugs/installer/ ticket
|
|
749
|
+
// 2026-04-30--cc-mini--dev-guide-split-path-migration.md.
|
|
750
|
+
const devGuideName = 'dev-guide-wipcomputerinc.md';
|
|
751
|
+
const devGuideNew = join(agentLibraryDest, devGuideName);
|
|
752
|
+
const devGuideOld = join(LDM_ROOT, 'shared', devGuideName);
|
|
753
|
+
if (existsSync(devGuideNew)) {
|
|
754
|
+
mkdirSync(dirname(devGuideOld), { recursive: true });
|
|
755
|
+
cpSync(devGuideNew, devGuideOld);
|
|
756
|
+
console.log(` + Compat write: ${devGuideName} also deployed to ${devGuideOld.replace(HOME, '~')} (migration window)`);
|
|
757
|
+
}
|
|
758
|
+
|
|
741
759
|
return docsCount + agentDocsCount;
|
|
742
760
|
}
|
|
743
761
|
|
|
@@ -1537,13 +1555,21 @@ async function cmdInstall() {
|
|
|
1537
1555
|
// npm install --prefix silently fails for scoped packages in temp directories...
|
|
1538
1556
|
// it creates the lock file but doesn't extract files. npm pack is reliable.
|
|
1539
1557
|
const npmName = resolvedTarget;
|
|
1558
|
+
// --alpha and --beta select the corresponding npm dist-tag instead of @latest.
|
|
1559
|
+
// Without this, `ldm install --alpha <pkg>` was pulling the @latest version
|
|
1560
|
+
// from npm pack and an existing global install would never advance to the
|
|
1561
|
+
// current alpha. Now the tag flows through pack + the downstream
|
|
1562
|
+
// installSingleTool's `npm install -g <pkg>@<version>` step uses the
|
|
1563
|
+
// version baked into the alpha tarball.
|
|
1564
|
+
const npmTag = ALPHA_FLAG ? 'alpha' : (BETA_FLAG ? 'beta' : '');
|
|
1565
|
+
const packTarget = npmTag ? `${npmName}@${npmTag}` : npmName;
|
|
1540
1566
|
const tempDir = join(LDM_TMP, `npm-${Date.now()}`);
|
|
1541
1567
|
console.log('');
|
|
1542
|
-
console.log(` Installing ${
|
|
1568
|
+
console.log(` Installing ${packTarget} from npm...`);
|
|
1543
1569
|
try {
|
|
1544
1570
|
mkdirSync(tempDir, { recursive: true });
|
|
1545
1571
|
// Use npm pack + tar instead of npm install --prefix
|
|
1546
|
-
const tarball = execSync(`npm pack ${
|
|
1572
|
+
const tarball = execSync(`npm pack ${packTarget} --pack-destination "${tempDir}" 2>/dev/null`, {
|
|
1547
1573
|
encoding: 'utf8', timeout: 60000, cwd: tempDir,
|
|
1548
1574
|
}).trim();
|
|
1549
1575
|
const tarPath = join(tempDir, tarball);
|
|
@@ -3979,7 +4005,7 @@ async function main() {
|
|
|
3979
4005
|
console.log(' Module ... ESM main/exports -> importable');
|
|
3980
4006
|
console.log(' MCP Server ... mcp-server.mjs -> claude mcp add --scope user');
|
|
3981
4007
|
console.log(' OpenClaw ... openclaw.plugin.json -> ~/.ldm/extensions/ + ~/.openclaw/extensions/');
|
|
3982
|
-
console.log(' Skill ... SKILL.md
|
|
4008
|
+
console.log(' Skill ... SKILL.md or skills/<name>/SKILL.md -> agent skill paths');
|
|
3983
4009
|
console.log(' CC Hook ... guard.mjs or claudeCode.hook -> ~/.claude/settings.json');
|
|
3984
4010
|
console.log('');
|
|
3985
4011
|
console.log(` v${PKG_VERSION}`);
|
|
@@ -4289,6 +4315,164 @@ async function main() {
|
|
|
4289
4315
|
console.log('');
|
|
4290
4316
|
}
|
|
4291
4317
|
|
|
4318
|
+
// ── ldm uninstall <pkg> ──
|
|
4319
|
+
//
|
|
4320
|
+
// Removes a single LDM-installed package. Used to reset a single
|
|
4321
|
+
// extension between dogfood cycles without taking down the rest of
|
|
4322
|
+
// LDM OS. Pairs with the per-package cleanup hook each package may
|
|
4323
|
+
// ship (e.g. `codex-daemon uninstall --purge` for Codex Remote
|
|
4324
|
+
// Control), which the user runs first to clean up product-specific
|
|
4325
|
+
// state. This command then removes the LDM-side install record + the
|
|
4326
|
+
// global npm package + the LDM extension dir.
|
|
4327
|
+
//
|
|
4328
|
+
// Safety:
|
|
4329
|
+
// - Never touches ~/.codex/ or other unrelated user state.
|
|
4330
|
+
// - Never removes ~/.ldm/memory/ or ~/.ldm/agents/.
|
|
4331
|
+
// - Never removes other extensions.
|
|
4332
|
+
// - Idempotent: running twice exits cleanly.
|
|
4333
|
+
// - Refuses to uninstall LDM OS itself (use `ldm uninstall` for that).
|
|
4334
|
+
|
|
4335
|
+
async function cmdUninstallPackage(pkgName) {
|
|
4336
|
+
const isDryRun = args.includes('--dry-run');
|
|
4337
|
+
|
|
4338
|
+
if (pkgName === 'wip-ldm-os' || pkgName === '@wipcomputer/wip-ldm-os') {
|
|
4339
|
+
console.error(' Refusing to uninstall LDM OS itself with `ldm uninstall <pkg>`.');
|
|
4340
|
+
console.error(' To remove all of LDM OS: ldm uninstall');
|
|
4341
|
+
process.exit(1);
|
|
4342
|
+
}
|
|
4343
|
+
|
|
4344
|
+
console.log('');
|
|
4345
|
+
console.log(` ldm uninstall ${pkgName}`);
|
|
4346
|
+
console.log(' ────────────────────────────────────');
|
|
4347
|
+
|
|
4348
|
+
// 1. Look up the package in the registry.
|
|
4349
|
+
const registryPath = join(LDM_EXTENSIONS, 'registry.json');
|
|
4350
|
+
let registry = { _format: 'v1', extensions: {} };
|
|
4351
|
+
try {
|
|
4352
|
+
if (existsSync(registryPath)) {
|
|
4353
|
+
registry = JSON.parse(readFileSync(registryPath, 'utf8'));
|
|
4354
|
+
}
|
|
4355
|
+
} catch (e) {
|
|
4356
|
+
console.error(` ! could not read registry at ${registryPath}: ${e.message}`);
|
|
4357
|
+
}
|
|
4358
|
+
const entry = registry.extensions?.[pkgName] || null;
|
|
4359
|
+
|
|
4360
|
+
// 2. Resolve npm package name.
|
|
4361
|
+
// Registry entries from npm installs put the npm name in `source.npm`
|
|
4362
|
+
// or in the top-level `name` field. Fall back to the user-supplied
|
|
4363
|
+
// pkgName (works for unscoped packages).
|
|
4364
|
+
const npmPkg = entry?.source?.npm || entry?.name || pkgName;
|
|
4365
|
+
|
|
4366
|
+
// 3. Resolve LDM extension dir(s).
|
|
4367
|
+
const ldmExtPath = entry?.paths?.ldm
|
|
4368
|
+
|| entry?.ldmPath
|
|
4369
|
+
|| join(LDM_EXTENSIONS, pkgName);
|
|
4370
|
+
const ocExtPath = entry?.paths?.openclaw
|
|
4371
|
+
|| entry?.ocPath
|
|
4372
|
+
|| null;
|
|
4373
|
+
|
|
4374
|
+
// 4. Build the action plan.
|
|
4375
|
+
const actions = [];
|
|
4376
|
+
let pkgInstalledGlobally = false;
|
|
4377
|
+
try {
|
|
4378
|
+
const npmList = execSync(`npm list -g --depth=0 --json 2>/dev/null`, { encoding: 'utf8' });
|
|
4379
|
+
const deps = JSON.parse(npmList).dependencies || {};
|
|
4380
|
+
pkgInstalledGlobally = !!deps[npmPkg];
|
|
4381
|
+
} catch {}
|
|
4382
|
+
|
|
4383
|
+
if (pkgInstalledGlobally) {
|
|
4384
|
+
actions.push({ kind: 'npm-uninstall', npmPkg });
|
|
4385
|
+
} else {
|
|
4386
|
+
actions.push({ kind: 'skip', label: `npm: ${npmPkg} not installed globally` });
|
|
4387
|
+
}
|
|
4388
|
+
if (existsSync(ldmExtPath)) {
|
|
4389
|
+
actions.push({ kind: 'rm-dir', label: 'LDM extension dir', path: ldmExtPath });
|
|
4390
|
+
} else {
|
|
4391
|
+
actions.push({ kind: 'skip', label: `LDM extension dir: ${ldmExtPath} already gone` });
|
|
4392
|
+
}
|
|
4393
|
+
if (ocExtPath && existsSync(ocExtPath)) {
|
|
4394
|
+
actions.push({ kind: 'rm-dir', label: 'OpenClaw extension dir', path: ocExtPath });
|
|
4395
|
+
}
|
|
4396
|
+
if (entry) {
|
|
4397
|
+
actions.push({ kind: 'registry-remove', name: pkgName });
|
|
4398
|
+
} else {
|
|
4399
|
+
actions.push({ kind: 'skip', label: `registry: no entry for ${pkgName}` });
|
|
4400
|
+
}
|
|
4401
|
+
|
|
4402
|
+
const realActions = actions.filter(a => a.kind !== 'skip');
|
|
4403
|
+
const skips = actions.filter(a => a.kind === 'skip');
|
|
4404
|
+
|
|
4405
|
+
console.log('');
|
|
4406
|
+
if (realActions.length === 0) {
|
|
4407
|
+
for (const s of skips) console.log(` - ${s.label}`);
|
|
4408
|
+
console.log('');
|
|
4409
|
+
console.log(' Nothing to do.');
|
|
4410
|
+
console.log('');
|
|
4411
|
+
console.log(' Will NOT touch ~/.codex/ or ~/.ldm/memory/ or other extensions.');
|
|
4412
|
+
return;
|
|
4413
|
+
}
|
|
4414
|
+
console.log(' Will:');
|
|
4415
|
+
for (const a of realActions) {
|
|
4416
|
+
switch (a.kind) {
|
|
4417
|
+
case 'npm-uninstall':
|
|
4418
|
+
console.log(` - npm uninstall -g ${a.npmPkg}`);
|
|
4419
|
+
break;
|
|
4420
|
+
case 'rm-dir':
|
|
4421
|
+
console.log(` - remove ${a.label}: ${a.path}`);
|
|
4422
|
+
break;
|
|
4423
|
+
case 'registry-remove':
|
|
4424
|
+
console.log(` - remove registry entry for ${a.name}`);
|
|
4425
|
+
break;
|
|
4426
|
+
}
|
|
4427
|
+
}
|
|
4428
|
+
for (const s of skips) console.log(` - skipped: ${s.label}`);
|
|
4429
|
+
console.log('');
|
|
4430
|
+
console.log(' Will NOT touch ~/.codex/ or ~/.ldm/memory/ or other extensions.');
|
|
4431
|
+
|
|
4432
|
+
if (isDryRun) {
|
|
4433
|
+
console.log('');
|
|
4434
|
+
console.log(' Dry run. Nothing removed.');
|
|
4435
|
+
console.log('');
|
|
4436
|
+
console.log(' Re-run without --dry-run to apply.');
|
|
4437
|
+
return;
|
|
4438
|
+
}
|
|
4439
|
+
|
|
4440
|
+
console.log('');
|
|
4441
|
+
for (const a of realActions) {
|
|
4442
|
+
switch (a.kind) {
|
|
4443
|
+
case 'npm-uninstall':
|
|
4444
|
+
try {
|
|
4445
|
+
execSync(`npm uninstall -g ${a.npmPkg}`, { stdio: 'pipe', timeout: 60000 });
|
|
4446
|
+
console.log(` + npm uninstall -g ${a.npmPkg}`);
|
|
4447
|
+
} catch (e) {
|
|
4448
|
+
console.error(` ! npm uninstall -g ${a.npmPkg} failed: ${e.message}`);
|
|
4449
|
+
}
|
|
4450
|
+
break;
|
|
4451
|
+
case 'rm-dir':
|
|
4452
|
+
try {
|
|
4453
|
+
execSync(`rm -rf "${a.path}"`, { stdio: 'pipe' });
|
|
4454
|
+
console.log(` + removed ${a.label}: ${a.path}`);
|
|
4455
|
+
} catch (e) {
|
|
4456
|
+
console.error(` ! could not remove ${a.path}: ${e.message}`);
|
|
4457
|
+
}
|
|
4458
|
+
break;
|
|
4459
|
+
case 'registry-remove':
|
|
4460
|
+
try {
|
|
4461
|
+
delete registry.extensions[a.name];
|
|
4462
|
+
writeFileSync(registryPath, JSON.stringify(registry, null, 2) + '\n');
|
|
4463
|
+
console.log(` + removed registry entry for ${a.name}`);
|
|
4464
|
+
} catch (e) {
|
|
4465
|
+
console.error(` ! could not update registry: ${e.message}`);
|
|
4466
|
+
}
|
|
4467
|
+
break;
|
|
4468
|
+
}
|
|
4469
|
+
}
|
|
4470
|
+
|
|
4471
|
+
console.log('');
|
|
4472
|
+
console.log(' Uninstalled.');
|
|
4473
|
+
console.log('');
|
|
4474
|
+
}
|
|
4475
|
+
|
|
4292
4476
|
// ── ldm worktree ──
|
|
4293
4477
|
|
|
4294
4478
|
async function cmdWorktree() {
|
|
@@ -4619,9 +4803,17 @@ async function main() {
|
|
|
4619
4803
|
case 'disable':
|
|
4620
4804
|
await cmdDisable();
|
|
4621
4805
|
break;
|
|
4622
|
-
case 'uninstall':
|
|
4623
|
-
|
|
4806
|
+
case 'uninstall': {
|
|
4807
|
+
// ldm uninstall <pkg> [--dry-run] removes one package
|
|
4808
|
+
// ldm uninstall removes the whole LDM OS install
|
|
4809
|
+
const target = args.slice(1).find(a => !a.startsWith('--'));
|
|
4810
|
+
if (target) {
|
|
4811
|
+
await cmdUninstallPackage(target);
|
|
4812
|
+
} else {
|
|
4813
|
+
await cmdUninstall();
|
|
4814
|
+
}
|
|
4624
4815
|
break;
|
|
4816
|
+
}
|
|
4625
4817
|
case 'worktree':
|
|
4626
4818
|
await cmdWorktree();
|
|
4627
4819
|
break;
|
|
@@ -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
|
}
|
|
@@ -1097,18 +1105,18 @@ function registerMCP(repoPath, door, toolName) {
|
|
|
1097
1105
|
*
|
|
1098
1106
|
* Returns true if at least one door installed successfully.
|
|
1099
1107
|
*/
|
|
1100
|
-
function installClaudeCodeHook(repoPath, doorOrDoors) {
|
|
1108
|
+
function installClaudeCodeHook(repoPath, doorOrDoors, toolName = basename(repoPath)) {
|
|
1101
1109
|
const doors = Array.isArray(doorOrDoors) ? doorOrDoors : [doorOrDoors];
|
|
1102
1110
|
let anyOk = false;
|
|
1103
1111
|
for (const door of doors) {
|
|
1104
|
-
if (installClaudeCodeHookEvent(repoPath, door)) {
|
|
1112
|
+
if (installClaudeCodeHookEvent(repoPath, door, toolName)) {
|
|
1105
1113
|
anyOk = true;
|
|
1106
1114
|
}
|
|
1107
1115
|
}
|
|
1108
1116
|
return anyOk;
|
|
1109
1117
|
}
|
|
1110
1118
|
|
|
1111
|
-
function installClaudeCodeHookEvent(repoPath, door) {
|
|
1119
|
+
function installClaudeCodeHookEvent(repoPath, door, toolName = basename(repoPath)) {
|
|
1112
1120
|
const settingsPath = join(HOME, '.claude', 'settings.json');
|
|
1113
1121
|
let settings = readJSON(settingsPath);
|
|
1114
1122
|
|
|
@@ -1117,7 +1125,6 @@ function installClaudeCodeHookEvent(repoPath, door) {
|
|
|
1117
1125
|
return false;
|
|
1118
1126
|
}
|
|
1119
1127
|
|
|
1120
|
-
const toolName = basename(repoPath);
|
|
1121
1128
|
const extDir = join(LDM_EXTENSIONS, toolName);
|
|
1122
1129
|
const installedGuard = join(extDir, 'guard.mjs');
|
|
1123
1130
|
|
|
@@ -1237,11 +1244,77 @@ function installClaudeCodeHookEvent(repoPath, door) {
|
|
|
1237
1244
|
}
|
|
1238
1245
|
}
|
|
1239
1246
|
|
|
1240
|
-
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 = {}) {
|
|
1241
1314
|
const { harnesses, workspace } = getHarnesses();
|
|
1242
1315
|
|
|
1243
|
-
// Find SKILL.md source:
|
|
1244
|
-
let skillSrc = join(
|
|
1316
|
+
// Find SKILL.md source: skill dir first, then permanent copy at ~/.ldm/extensions/
|
|
1317
|
+
let skillSrc = join(skillDir, 'SKILL.md');
|
|
1245
1318
|
const permanentSkill = join(LDM_EXTENSIONS, toolName, 'SKILL.md');
|
|
1246
1319
|
if (!existsSync(skillSrc) && existsSync(permanentSkill)) skillSrc = permanentSkill;
|
|
1247
1320
|
if (!existsSync(skillSrc)) return false;
|
|
@@ -1252,14 +1325,16 @@ function installSkill(repoPath, toolName) {
|
|
|
1252
1325
|
return false;
|
|
1253
1326
|
}
|
|
1254
1327
|
|
|
1255
|
-
|
|
1256
|
-
|
|
1328
|
+
const sourceSkillDir = dirname(skillSrc);
|
|
1329
|
+
|
|
1330
|
+
// Find references/ source: skill dir first, then permanent copy
|
|
1331
|
+
let refsSrc = join(skillDir, 'references');
|
|
1257
1332
|
const permanentRefs = join(LDM_EXTENSIONS, toolName, 'references');
|
|
1258
1333
|
if (!existsSync(refsSrc) && existsSync(permanentRefs)) refsSrc = permanentRefs;
|
|
1259
1334
|
|
|
1260
1335
|
if (DRY_RUN) {
|
|
1261
|
-
const
|
|
1262
|
-
|
|
1336
|
+
const entries = listSkillCopyEntries(sourceSkillDir, opts.copyFullFolder);
|
|
1337
|
+
printSkillDryRunPlan({ sourceSkillDir, refsSrc, toolName, harnesses, workspace, entries });
|
|
1263
1338
|
return true;
|
|
1264
1339
|
}
|
|
1265
1340
|
|
|
@@ -1268,21 +1343,13 @@ function installSkill(repoPath, toolName) {
|
|
|
1268
1343
|
|
|
1269
1344
|
// 1. Save permanent copy to ~/.ldm/extensions/<name>/ (survives tmp cleanup)
|
|
1270
1345
|
const ldmSkillDir = join(LDM_EXTENSIONS, toolName);
|
|
1271
|
-
|
|
1272
|
-
cpSync(skillSrc, join(ldmSkillDir, 'SKILL.md'));
|
|
1273
|
-
if (existsSync(refsSrc) && refsSrc !== permanentRefs) {
|
|
1274
|
-
cpSync(refsSrc, join(ldmSkillDir, 'references'), { recursive: true });
|
|
1275
|
-
}
|
|
1346
|
+
copySkillTree(sourceSkillDir, ldmSkillDir, opts.copyFullFolder);
|
|
1276
1347
|
|
|
1277
1348
|
// 2. Deploy to every detected harness that has a skills path
|
|
1278
1349
|
for (const [name, harness] of Object.entries(harnesses)) {
|
|
1279
1350
|
if (!harness.detected || !harness.skills) continue;
|
|
1280
1351
|
const dest = join(harness.skills, toolName);
|
|
1281
|
-
|
|
1282
|
-
cpSync(skillSrc, join(dest, 'SKILL.md'));
|
|
1283
|
-
if (existsSync(refsSrc)) {
|
|
1284
|
-
cpSync(refsSrc, join(dest, 'references'), { recursive: true });
|
|
1285
|
-
}
|
|
1352
|
+
copySkillTree(sourceSkillDir, dest, opts.copyFullFolder);
|
|
1286
1353
|
deployed.push(name);
|
|
1287
1354
|
}
|
|
1288
1355
|
|
|
@@ -1302,6 +1369,18 @@ function installSkill(repoPath, toolName) {
|
|
|
1302
1369
|
}
|
|
1303
1370
|
}
|
|
1304
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
|
+
|
|
1305
1384
|
// ── Single tool install ──
|
|
1306
1385
|
|
|
1307
1386
|
export function installSingleTool(toolPath) {
|
|
@@ -1362,6 +1441,10 @@ export function installSingleTool(toolPath) {
|
|
|
1362
1441
|
}
|
|
1363
1442
|
}
|
|
1364
1443
|
|
|
1444
|
+
if (interfaces.skill) {
|
|
1445
|
+
installSkill(toolPath, toolName, interfaces.skill);
|
|
1446
|
+
}
|
|
1447
|
+
|
|
1365
1448
|
return ifaceNames.length;
|
|
1366
1449
|
}
|
|
1367
1450
|
|
|
@@ -1422,7 +1505,7 @@ export function installSingleTool(toolPath) {
|
|
|
1422
1505
|
|
|
1423
1506
|
if (interfaces.claudeCodeHook) {
|
|
1424
1507
|
if (isEnabled || isAlreadyDeployed) {
|
|
1425
|
-
if (installClaudeCodeHook(toolPath, interfaces.claudeCodeHook)) installed++;
|
|
1508
|
+
if (installClaudeCodeHook(toolPath, interfaces.claudeCodeHook, toolName)) installed++;
|
|
1426
1509
|
} else {
|
|
1427
1510
|
skip(`Hook: ${toolName} not enabled`);
|
|
1428
1511
|
}
|
|
@@ -1430,7 +1513,7 @@ export function installSingleTool(toolPath) {
|
|
|
1430
1513
|
|
|
1431
1514
|
if (interfaces.skill) {
|
|
1432
1515
|
// Skills always deploy. They're instruction files, not running code.
|
|
1433
|
-
if (installSkill(toolPath, toolName)) installed++;
|
|
1516
|
+
if (installSkill(toolPath, toolName, interfaces.skill)) installed++;
|
|
1434
1517
|
}
|
|
1435
1518
|
|
|
1436
1519
|
if (interfaces.module) {
|
|
@@ -1591,7 +1674,7 @@ export async function enableExtension(name) {
|
|
|
1591
1674
|
|
|
1592
1675
|
if (interfaces.mcp) registerMCP(extPath, interfaces.mcp, name);
|
|
1593
1676
|
if (interfaces.claudeCodeHook) installClaudeCodeHook(extPath, interfaces.claudeCodeHook);
|
|
1594
|
-
if (interfaces.skill) installSkill(extPath, name);
|
|
1677
|
+
if (interfaces.skill) installSkill(extPath, name, interfaces.skill);
|
|
1595
1678
|
|
|
1596
1679
|
entry.enabled = true;
|
|
1597
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.
|
|
3
|
+
"version": "0.4.85-alpha.10",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "LDM OS: identity, memory, and sovereignty infrastructure for AI agents",
|
|
6
6
|
"engines": {
|
|
@@ -22,9 +22,17 @@
|
|
|
22
22
|
"validate:bin-manifest": "node scripts/validate-bin-manifest.mjs",
|
|
23
23
|
"test:skill-frontmatter": "node scripts/test-skill-frontmatter.mjs",
|
|
24
24
|
"test:installer-update-tracks": "node scripts/test-installer-update-tracks.mjs",
|
|
25
|
+
"test:installer-hook-toolname": "node scripts/test-installer-hook-toolname.mjs",
|
|
26
|
+
"test:installer-skill-directory": "node scripts/test-installer-skill-directory.mjs",
|
|
27
|
+
"test:installer-skill-dry-run-destinations": "node scripts/test-installer-skill-dry-run-destinations.mjs",
|
|
25
28
|
"test:ldm-install-bin-shim": "node scripts/test-ldm-install-preserves-foreign-bin.mjs",
|
|
26
29
|
"test:doctor-cron-target": "node scripts/test-doctor-cron-target.mjs",
|
|
27
30
|
"test:bin-manifest": "node scripts/test-bin-manifest.mjs",
|
|
31
|
+
"test:crc-agentid-tenant-boundary": "node scripts/test-crc-agentid-tenant-boundary.mjs",
|
|
32
|
+
"test:crc-pair-login-flow": "node scripts/test-crc-pair-login-flow.mjs",
|
|
33
|
+
"test:crc-pair-status-poll-token": "node scripts/test-crc-pair-status-poll-token.mjs",
|
|
34
|
+
"test:crc-e2ee-key-persistence": "node scripts/test-crc-e2ee-key-persistence.mjs",
|
|
35
|
+
"test:crc-e2ee-session-route": "node scripts/test-crc-e2ee-session-route.mjs",
|
|
28
36
|
"fmt": "npx prettier --write 'src/**/*.{ts,mjs}' 'lib/**/*.mjs' 'bin/**/*.js'",
|
|
29
37
|
"fmt:check": "npx prettier --check 'src/**/*.{ts,mjs}' 'lib/**/*.mjs' 'bin/**/*.js'"
|
|
30
38
|
},
|