@wipcomputer/wip-ldm-os 0.4.85-alpha.4 → 0.4.85-alpha.5

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 CHANGED
@@ -4005,7 +4005,7 @@ async function main() {
4005
4005
  console.log(' Module ... ESM main/exports -> importable');
4006
4006
  console.log(' MCP Server ... mcp-server.mjs -> claude mcp add --scope user');
4007
4007
  console.log(' OpenClaw ... openclaw.plugin.json -> ~/.ldm/extensions/ + ~/.openclaw/extensions/');
4008
- console.log(' Skill ... SKILL.md -> ~/.openclaw/skills/<tool>/');
4008
+ console.log(' Skill ... SKILL.md or skills/<name>/SKILL.md -> agent skill paths');
4009
4009
  console.log(' CC Hook ... guard.mjs or claudeCode.hook -> ~/.claude/settings.json');
4010
4010
  console.log('');
4011
4011
  console.log(` v${PKG_VERSION}`);
@@ -144,13 +144,13 @@ 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 with name, description. Optional `references/` directory for context files.
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:** `SKILL.md` deployed to `~/.openclaw/skills/<name>/`. If `references/` exists, deployed alongside SKILL.md and to `settings/docs/skills/<name>/` in the workspace.
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
154
 
155
155
  **Structure:**
156
156
  ```
@@ -162,6 +162,17 @@ repo/
162
162
  └── ...
163
163
  ```
164
164
 
165
+ Multiple skills can also live in one repo:
166
+
167
+ ```text
168
+ repo/
169
+ └── skills/
170
+ └── wip-ai-chat-ui/
171
+ ├── SKILL.md
172
+ ├── agents/
173
+ └── references/
174
+ ```
175
+
165
176
  **Key rules (from Agent Skills Spec):**
166
177
  - SKILL.md body < 5000 tokens. Process goes in SKILL.md, context goes in references/.
167
178
  - 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/` directory for context files.
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 `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).
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 | Reports path |
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,25 @@ function installClaudeCodeHookEvent(repoPath, door, toolName = basename(repoPath
1236
1244
  }
1237
1245
  }
1238
1246
 
1239
- function installSkill(repoPath, toolName) {
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
+ function installSkillFolder(skillDir, toolName, opts = {}) {
1240
1262
  const { harnesses, workspace } = getHarnesses();
1241
1263
 
1242
- // Find SKILL.md source: repo path first, then permanent copy at ~/.ldm/extensions/
1243
- let skillSrc = join(repoPath, 'SKILL.md');
1264
+ // Find SKILL.md source: skill dir first, then permanent copy at ~/.ldm/extensions/
1265
+ let skillSrc = join(skillDir, 'SKILL.md');
1244
1266
  const permanentSkill = join(LDM_EXTENSIONS, toolName, 'SKILL.md');
1245
1267
  if (!existsSync(skillSrc) && existsSync(permanentSkill)) skillSrc = permanentSkill;
1246
1268
  if (!existsSync(skillSrc)) return false;
@@ -1251,14 +1273,25 @@ function installSkill(repoPath, toolName) {
1251
1273
  return false;
1252
1274
  }
1253
1275
 
1254
- // Find references/ source: repo path first, then permanent copy
1255
- let refsSrc = join(repoPath, 'references');
1276
+ const sourceSkillDir = dirname(skillSrc);
1277
+
1278
+ // Find references/ source: skill dir first, then permanent copy
1279
+ let refsSrc = join(skillDir, 'references');
1256
1280
  const permanentRefs = join(LDM_EXTENSIONS, toolName, 'references');
1257
1281
  if (!existsSync(refsSrc) && existsSync(permanentRefs)) refsSrc = permanentRefs;
1258
1282
 
1259
1283
  if (DRY_RUN) {
1260
- const targets = Object.entries(harnesses).filter(([,h]) => h.detected && h.skills).map(([name]) => name);
1261
- ok(`Skill: would deploy ${toolName} to ${targets.join(', ')} (dry run)`);
1284
+ const targets = Object.entries(harnesses)
1285
+ .filter(([,h]) => h.detected && h.skills)
1286
+ .map(([,h]) => join(h.skills, toolName));
1287
+ ok(`Skill: ${toolName}`);
1288
+ log(`Source: ${sourceSkillDir}`);
1289
+ if (targets.length > 0) {
1290
+ log(`Targets:`);
1291
+ for (const target of targets) log(`- ${target}`);
1292
+ } else {
1293
+ log(`Targets: no detected skill harnesses`);
1294
+ }
1262
1295
  return true;
1263
1296
  }
1264
1297
 
@@ -1267,21 +1300,13 @@ function installSkill(repoPath, toolName) {
1267
1300
 
1268
1301
  // 1. Save permanent copy to ~/.ldm/extensions/<name>/ (survives tmp cleanup)
1269
1302
  const ldmSkillDir = join(LDM_EXTENSIONS, toolName);
1270
- mkdirSync(ldmSkillDir, { recursive: true });
1271
- cpSync(skillSrc, join(ldmSkillDir, 'SKILL.md'));
1272
- if (existsSync(refsSrc) && refsSrc !== permanentRefs) {
1273
- cpSync(refsSrc, join(ldmSkillDir, 'references'), { recursive: true });
1274
- }
1303
+ copySkillTree(sourceSkillDir, ldmSkillDir, opts.copyFullFolder);
1275
1304
 
1276
1305
  // 2. Deploy to every detected harness that has a skills path
1277
1306
  for (const [name, harness] of Object.entries(harnesses)) {
1278
1307
  if (!harness.detected || !harness.skills) continue;
1279
1308
  const dest = join(harness.skills, toolName);
1280
- mkdirSync(dest, { recursive: true });
1281
- cpSync(skillSrc, join(dest, 'SKILL.md'));
1282
- if (existsSync(refsSrc)) {
1283
- cpSync(refsSrc, join(dest, 'references'), { recursive: true });
1284
- }
1309
+ copySkillTree(sourceSkillDir, dest, opts.copyFullFolder);
1285
1310
  deployed.push(name);
1286
1311
  }
1287
1312
 
@@ -1301,6 +1326,18 @@ function installSkill(repoPath, toolName) {
1301
1326
  }
1302
1327
  }
1303
1328
 
1329
+ function installSkill(repoPath, toolName, skillInfo = null) {
1330
+ if (Array.isArray(skillInfo?.skills) && skillInfo.skills.length > 0) {
1331
+ let installed = 0;
1332
+ for (const skill of skillInfo.skills) {
1333
+ if (installSkillFolder(skill.path, skill.name, { copyFullFolder: true })) installed++;
1334
+ }
1335
+ return installed > 0;
1336
+ }
1337
+
1338
+ return installSkillFolder(repoPath, toolName);
1339
+ }
1340
+
1304
1341
  // ── Single tool install ──
1305
1342
 
1306
1343
  export function installSingleTool(toolPath) {
@@ -1361,6 +1398,10 @@ export function installSingleTool(toolPath) {
1361
1398
  }
1362
1399
  }
1363
1400
 
1401
+ if (interfaces.skill) {
1402
+ installSkill(toolPath, toolName, interfaces.skill);
1403
+ }
1404
+
1364
1405
  return ifaceNames.length;
1365
1406
  }
1366
1407
 
@@ -1429,7 +1470,7 @@ export function installSingleTool(toolPath) {
1429
1470
 
1430
1471
  if (interfaces.skill) {
1431
1472
  // Skills always deploy. They're instruction files, not running code.
1432
- if (installSkill(toolPath, toolName)) installed++;
1473
+ if (installSkill(toolPath, toolName, interfaces.skill)) installed++;
1433
1474
  }
1434
1475
 
1435
1476
  if (interfaces.module) {
@@ -1590,7 +1631,7 @@ export async function enableExtension(name) {
1590
1631
 
1591
1632
  if (interfaces.mcp) registerMCP(extPath, interfaces.mcp, name);
1592
1633
  if (interfaces.claudeCodeHook) installClaudeCodeHook(extPath, interfaces.claudeCodeHook);
1593
- if (interfaces.skill) installSkill(extPath, name);
1634
+ if (interfaces.skill) installSkill(extPath, name, interfaces.skill);
1594
1635
 
1595
1636
  entry.enabled = true;
1596
1637
  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 exists
52
- if (existsSync(join(repoPath, 'SKILL.md'))) {
53
- interfaces.skill = { path: join(repoPath, 'SKILL.md') };
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) lines.push(`Skill: SKILL.md`);
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.4",
3
+ "version": "0.4.85-alpha.5",
4
4
  "type": "module",
5
5
  "description": "LDM OS: identity, memory, and sovereignty infrastructure for AI agents",
6
6
  "engines": {
@@ -23,6 +23,7 @@
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
25
  "test:installer-hook-toolname": "node scripts/test-installer-hook-toolname.mjs",
26
+ "test:installer-skill-directory": "node scripts/test-installer-skill-directory.mjs",
26
27
  "test:ldm-install-bin-shim": "node scripts/test-ldm-install-preserves-foreign-bin.mjs",
27
28
  "test:doctor-cron-target": "node scripts/test-doctor-cron-target.mjs",
28
29
  "test:bin-manifest": "node scripts/test-bin-manifest.mjs",
@@ -0,0 +1,55 @@
1
+ #!/usr/bin/env node
2
+ import { existsSync, lstatSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
3
+ import { join } from 'node:path';
4
+ import { tmpdir } from 'node:os';
5
+
6
+ const home = mkdtempSync(join(tmpdir(), 'ldm-skill-dir-home-'));
7
+ const source = mkdtempSync(join(tmpdir(), 'ldm-skill-dir-source-'));
8
+
9
+ function assert(condition, message) {
10
+ if (!condition) throw new Error(message);
11
+ }
12
+
13
+ try {
14
+ process.env.HOME = home;
15
+
16
+ for (const dir of ['.claude', '.openclaw', '.codex', '.agents']) {
17
+ mkdirSync(join(home, dir), { recursive: true });
18
+ }
19
+
20
+ const skillDir = join(source, 'skills', 'wip-ai-chat-ui');
21
+ mkdirSync(join(skillDir, 'references'), { recursive: true });
22
+ mkdirSync(join(skillDir, 'agents'), { recursive: true });
23
+ writeFileSync(join(skillDir, 'SKILL.md'), '---\nname: wip-ai-chat-ui\ndescription: "test skill"\n---\n\n# Test Skill\n');
24
+ writeFileSync(join(skillDir, 'references', 'stack.md'), '# Stack\n');
25
+ writeFileSync(join(skillDir, 'agents', 'openai.yaml'), 'display_name: "WIP AI Chat UI"\n');
26
+
27
+ const { detectInterfacesJSON } = await import('../lib/detect.mjs');
28
+ const detected = detectInterfacesJSON(source);
29
+ assert(detected.interfaceCount === 1, 'skill directory repo should expose one interface');
30
+ assert(detected.interfaces.skill?.skills?.[0]?.name === 'wip-ai-chat-ui', 'skill directory name should be detected');
31
+
32
+ const { installFromPath } = await import('../lib/deploy.mjs');
33
+ const result = await installFromPath(source);
34
+ assert(result.interfaces === 1, 'skill directory install should process one interface');
35
+
36
+ for (const target of [
37
+ join(home, '.claude', 'skills', 'wip-ai-chat-ui'),
38
+ join(home, '.openclaw', 'skills', 'wip-ai-chat-ui'),
39
+ join(home, '.codex', 'skills', 'wip-ai-chat-ui'),
40
+ join(home, '.agents', 'skills', 'wip-ai-chat-ui'),
41
+ ]) {
42
+ assert(existsSync(join(target, 'SKILL.md')), `${target} should include SKILL.md`);
43
+ assert(existsSync(join(target, 'references', 'stack.md')), `${target} should include references`);
44
+ assert(existsSync(join(target, 'agents', 'openai.yaml')), `${target} should include agents metadata`);
45
+ assert(!lstatSync(target).isSymbolicLink(), `${target} should be a deployed directory, not a symlink`);
46
+ }
47
+
48
+ const codexSkill = readFileSync(join(home, '.codex', 'skills', 'wip-ai-chat-ui', 'SKILL.md'), 'utf8');
49
+ assert(codexSkill.includes('name: wip-ai-chat-ui'), 'Codex target should contain the expected skill');
50
+
51
+ console.log('installer skill directory regression passed');
52
+ } finally {
53
+ rmSync(home, { recursive: true, force: true });
54
+ rmSync(source, { recursive: true, force: true });
55
+ }