@npeercy/skills 0.1.3 → 0.1.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/lib/skills.js +182 -19
- package/package.json +1 -1
package/lib/skills.js
CHANGED
|
@@ -104,13 +104,103 @@ async function promptYesNo(question, defaultYes = true) {
|
|
|
104
104
|
return ['y', 'yes'].includes(ans);
|
|
105
105
|
}
|
|
106
106
|
|
|
107
|
+
function resolveSymlinkTarget(linkPath) {
|
|
108
|
+
const target = readlinkSync(linkPath);
|
|
109
|
+
return target.startsWith('/') ? target : resolve(dirname(linkPath), target);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function skillMdPathFromEntry(fullPath, entry) {
|
|
113
|
+
try {
|
|
114
|
+
if (entry.isDirectory()) {
|
|
115
|
+
const md = join(fullPath, 'SKILL.md');
|
|
116
|
+
return existsSync(md) ? md : null;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const st = lstatSync(fullPath);
|
|
120
|
+
if (!st.isSymbolicLink()) return null;
|
|
121
|
+
const target = resolveSymlinkTarget(fullPath);
|
|
122
|
+
const md = join(target, 'SKILL.md');
|
|
123
|
+
return existsSync(md) ? md : null;
|
|
124
|
+
} catch {
|
|
125
|
+
return null;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function readSkillMeta(skillMdPath) {
|
|
130
|
+
try {
|
|
131
|
+
return parseFrontmatter(readFileSync(skillMdPath, 'utf8'));
|
|
132
|
+
} catch {
|
|
133
|
+
return null;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function discoverSkillNameUsage(agents) {
|
|
138
|
+
const usage = new Map(); // skill-name -> [{agent, entryName, path, skillMdPath}]
|
|
139
|
+
|
|
140
|
+
for (const [agentName, agentPath] of Object.entries(agents)) {
|
|
141
|
+
if (!existsSync(agentPath)) continue;
|
|
142
|
+
|
|
143
|
+
for (const entry of readdirSync(agentPath, { withFileTypes: true })) {
|
|
144
|
+
if (entry.name.startsWith('.')) continue;
|
|
145
|
+
const full = join(agentPath, entry.name);
|
|
146
|
+
const skillMd = skillMdPathFromEntry(full, entry);
|
|
147
|
+
if (!skillMd) continue;
|
|
148
|
+
|
|
149
|
+
const meta = readSkillMeta(skillMd);
|
|
150
|
+
if (!meta?.name) continue;
|
|
151
|
+
|
|
152
|
+
if (!usage.has(meta.name)) usage.set(meta.name, []);
|
|
153
|
+
usage.get(meta.name).push({
|
|
154
|
+
agent: agentName,
|
|
155
|
+
entryName: entry.name,
|
|
156
|
+
path: full,
|
|
157
|
+
skillMdPath: skillMd,
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
return usage;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function discoverAgentNameConflicts(agents) {
|
|
166
|
+
const conflicts = []; // [{agent,name,entries}]
|
|
167
|
+
|
|
168
|
+
for (const [agentName, agentPath] of Object.entries(agents)) {
|
|
169
|
+
if (!existsSync(agentPath)) continue;
|
|
170
|
+
const byName = new Map();
|
|
171
|
+
|
|
172
|
+
for (const entry of readdirSync(agentPath, { withFileTypes: true })) {
|
|
173
|
+
if (entry.name.startsWith('.')) continue;
|
|
174
|
+
const full = join(agentPath, entry.name);
|
|
175
|
+
const skillMd = skillMdPathFromEntry(full, entry);
|
|
176
|
+
if (!skillMd) continue;
|
|
177
|
+
|
|
178
|
+
const meta = readSkillMeta(skillMd);
|
|
179
|
+
if (!meta?.name) continue;
|
|
180
|
+
|
|
181
|
+
if (!byName.has(meta.name)) byName.set(meta.name, []);
|
|
182
|
+
byName.get(meta.name).push(entry.name);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
for (const [name, entries] of byName.entries()) {
|
|
186
|
+
if (entries.length > 1) {
|
|
187
|
+
conflicts.push({ agent: agentName, name, entries: entries.sort() });
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
return conflicts;
|
|
193
|
+
}
|
|
194
|
+
|
|
107
195
|
function discoverUnmanaged(agents, st) {
|
|
108
|
-
const unmanaged = new Map(); // name -> { sources: [{agent,path,skillMdPath}] }
|
|
196
|
+
const unmanaged = new Map(); // skill-name -> { sources: [{agent,path,skillMdPath}] }
|
|
109
197
|
const managedLinks = new Set(Object.keys(st.installed).map(linkNameForSkill));
|
|
110
198
|
|
|
111
199
|
for (const [agentName, agentPath] of Object.entries(agents)) {
|
|
112
200
|
if (!existsSync(agentPath)) continue;
|
|
201
|
+
|
|
113
202
|
for (const entry of readdirSync(agentPath, { withFileTypes: true })) {
|
|
203
|
+
if (entry.name.startsWith('.')) continue;
|
|
114
204
|
const full = join(agentPath, entry.name);
|
|
115
205
|
|
|
116
206
|
// Skip managed link names
|
|
@@ -119,47 +209,74 @@ function discoverUnmanaged(agents, st) {
|
|
|
119
209
|
// Skip if symlink points to managed store
|
|
120
210
|
try {
|
|
121
211
|
if (lstatSync(full).isSymbolicLink()) {
|
|
122
|
-
const target =
|
|
212
|
+
const target = resolveSymlinkTarget(full);
|
|
123
213
|
if (target.includes('skill-sharer')) continue;
|
|
124
214
|
}
|
|
125
215
|
} catch {
|
|
126
216
|
// ignore stat errors
|
|
127
217
|
}
|
|
128
218
|
|
|
129
|
-
const skillMd = entry
|
|
130
|
-
|
|
131
|
-
: (lstatSync(full).isSymbolicLink() ? join(resolve(readlinkSync(full)), 'SKILL.md') : null);
|
|
132
|
-
|
|
133
|
-
if (!skillMd || !existsSync(skillMd)) continue;
|
|
219
|
+
const skillMd = skillMdPathFromEntry(full, entry);
|
|
220
|
+
if (!skillMd) continue;
|
|
134
221
|
|
|
135
|
-
const
|
|
222
|
+
const meta = readSkillMeta(skillMd);
|
|
223
|
+
const key = meta?.name || entry.name;
|
|
136
224
|
if (!unmanaged.has(key)) unmanaged.set(key, { sources: [] });
|
|
137
|
-
unmanaged.get(key).sources.push({
|
|
225
|
+
unmanaged.get(key).sources.push({
|
|
226
|
+
agent: agentName,
|
|
227
|
+
path: full,
|
|
228
|
+
skillMdPath: skillMd,
|
|
229
|
+
});
|
|
138
230
|
}
|
|
139
231
|
}
|
|
140
232
|
|
|
141
233
|
return unmanaged;
|
|
142
234
|
}
|
|
143
235
|
|
|
144
|
-
|
|
145
|
-
const
|
|
236
|
+
function bundledBuiltins() {
|
|
237
|
+
const out = [];
|
|
146
238
|
const builtinsRoot = resolve(dirname(fileURLToPath(import.meta.url)), '..', 'builtin-skills');
|
|
147
|
-
if (!existsSync(builtinsRoot)) return;
|
|
239
|
+
if (!existsSync(builtinsRoot)) return out;
|
|
148
240
|
|
|
149
|
-
const installed = [];
|
|
150
241
|
for (const entry of readdirSync(builtinsRoot, { withFileTypes: true })) {
|
|
151
242
|
if (!entry.isDirectory()) continue;
|
|
152
|
-
|
|
153
243
|
const srcDir = join(builtinsRoot, entry.name);
|
|
154
244
|
const skillMd = join(srcDir, 'SKILL.md');
|
|
155
245
|
if (!existsSync(skillMd)) continue;
|
|
156
|
-
|
|
157
246
|
const meta = parseFrontmatter(readFileSync(skillMd, 'utf8'));
|
|
158
247
|
if (!meta) continue;
|
|
248
|
+
out.push({
|
|
249
|
+
id: `__builtin__/local/${meta.name}`,
|
|
250
|
+
name: meta.name,
|
|
251
|
+
srcDir,
|
|
252
|
+
});
|
|
253
|
+
}
|
|
159
254
|
|
|
160
|
-
|
|
255
|
+
return out;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
async function installBuiltins(agents) {
|
|
259
|
+
const st = loadState();
|
|
260
|
+
|
|
261
|
+
const installed = [];
|
|
262
|
+
const skipped = [];
|
|
263
|
+
const nameUsage = discoverSkillNameUsage(agents);
|
|
264
|
+
|
|
265
|
+
for (const b of bundledBuiltins()) {
|
|
266
|
+
const srcDir = b.srcDir;
|
|
267
|
+
const id = b.id;
|
|
161
268
|
const version = 'v1';
|
|
162
269
|
const dest = managedPath(id, version);
|
|
270
|
+
const linkName = linkNameForSkill(id);
|
|
271
|
+
|
|
272
|
+
const conflictingEntries = (nameUsage.get(b.name) || [])
|
|
273
|
+
.filter(s => s.entryName !== linkName);
|
|
274
|
+
|
|
275
|
+
if (conflictingEntries.length > 0) {
|
|
276
|
+
skipped.push({ name: b.name, conflicts: conflictingEntries });
|
|
277
|
+
continue;
|
|
278
|
+
}
|
|
279
|
+
|
|
163
280
|
const files = collectFiles(srcDir);
|
|
164
281
|
|
|
165
282
|
// Write to managed store
|
|
@@ -172,8 +289,7 @@ async function installBuiltins(agents) {
|
|
|
172
289
|
}
|
|
173
290
|
|
|
174
291
|
// Symlink into all detected agents
|
|
175
|
-
const
|
|
176
|
-
for (const agentPath of Object.values(agents)) {
|
|
292
|
+
for (const [agentName, agentPath] of Object.entries(agents)) {
|
|
177
293
|
mkdirSync(agentPath, { recursive: true });
|
|
178
294
|
const link = join(agentPath, linkName);
|
|
179
295
|
try {
|
|
@@ -184,6 +300,14 @@ async function installBuiltins(agents) {
|
|
|
184
300
|
// doesn't exist
|
|
185
301
|
}
|
|
186
302
|
symlinkSync(dest, link);
|
|
303
|
+
|
|
304
|
+
if (!nameUsage.has(b.name)) nameUsage.set(b.name, []);
|
|
305
|
+
nameUsage.get(b.name).push({
|
|
306
|
+
agent: agentName,
|
|
307
|
+
entryName: linkName,
|
|
308
|
+
path: link,
|
|
309
|
+
skillMdPath: join(dest, 'SKILL.md'),
|
|
310
|
+
});
|
|
187
311
|
}
|
|
188
312
|
|
|
189
313
|
st.installed[id] = {
|
|
@@ -195,16 +319,26 @@ async function installBuiltins(agents) {
|
|
|
195
319
|
builtin: true,
|
|
196
320
|
};
|
|
197
321
|
|
|
198
|
-
installed.push(
|
|
322
|
+
installed.push(b.name);
|
|
199
323
|
}
|
|
200
324
|
|
|
201
325
|
saveState(st);
|
|
326
|
+
|
|
202
327
|
if (installed.length > 0) {
|
|
203
328
|
console.log('\nInstalled built-in skills:');
|
|
204
329
|
for (const name of installed) {
|
|
205
330
|
console.log(` ✓ ${name} → ${Object.keys(agents).join(', ') || '(no agents detected)'}`);
|
|
206
331
|
}
|
|
207
332
|
}
|
|
333
|
+
|
|
334
|
+
if (skipped.length > 0) {
|
|
335
|
+
console.log('\nSkipped built-ins to avoid skill-name conflicts in agents:');
|
|
336
|
+
for (const s of skipped) {
|
|
337
|
+
const where = s.conflicts.map(c => `${c.agent}/${c.entryName}`).join(', ');
|
|
338
|
+
console.log(` ! ${s.name} (already present at: ${where})`);
|
|
339
|
+
}
|
|
340
|
+
console.log(' Tip: remove/rename duplicates if you want bundled docs linked automatically.');
|
|
341
|
+
}
|
|
208
342
|
}
|
|
209
343
|
|
|
210
344
|
// --- Commands ---
|
|
@@ -449,6 +583,20 @@ export async function cmdList(args = {}) {
|
|
|
449
583
|
});
|
|
450
584
|
}
|
|
451
585
|
|
|
586
|
+
// Bundled built-ins (show even if not installed yet)
|
|
587
|
+
const installedIds = new Set(Object.keys(st.installed));
|
|
588
|
+
for (const b of bundledBuiltins()) {
|
|
589
|
+
if (installedIds.has(b.id)) continue;
|
|
590
|
+
rows.push({
|
|
591
|
+
id: b.id,
|
|
592
|
+
version: '-',
|
|
593
|
+
latest: 'v1',
|
|
594
|
+
agents: '-',
|
|
595
|
+
status: 'bundled',
|
|
596
|
+
managed: false,
|
|
597
|
+
});
|
|
598
|
+
}
|
|
599
|
+
|
|
452
600
|
// Unmanaged installs (present in agent dirs but not in skill-sharer state)
|
|
453
601
|
const unmanaged = discoverUnmanaged(agents, st);
|
|
454
602
|
for (const [name, info] of unmanaged.entries()) {
|
|
@@ -484,6 +632,15 @@ export async function cmdList(args = {}) {
|
|
|
484
632
|
if (unmanagedCount > 0) {
|
|
485
633
|
console.log(`\nFound ${unmanagedCount} unmanaged skill(s). Run: skills import`);
|
|
486
634
|
}
|
|
635
|
+
|
|
636
|
+
const conflicts = discoverAgentNameConflicts(agents);
|
|
637
|
+
if (conflicts.length > 0) {
|
|
638
|
+
console.log('\nPotential skill-name conflicts detected (can break skill selection in agents):');
|
|
639
|
+
for (const c of conflicts) {
|
|
640
|
+
console.log(` ${c.agent}: "${c.name}" appears in ${c.entries.join(', ')}`);
|
|
641
|
+
}
|
|
642
|
+
console.log('Resolve duplicates by removing/renaming one copy.');
|
|
643
|
+
}
|
|
487
644
|
}
|
|
488
645
|
|
|
489
646
|
function verifyInstall(id, rec, agents) {
|
|
@@ -762,6 +919,12 @@ export async function cmdDoctor(args) {
|
|
|
762
919
|
}
|
|
763
920
|
}
|
|
764
921
|
|
|
922
|
+
const conflicts = discoverAgentNameConflicts(agents);
|
|
923
|
+
for (const c of conflicts) {
|
|
924
|
+
console.log(`Conflict: ${c.agent} has duplicate skill name "${c.name}" in ${c.entries.join(', ')}`);
|
|
925
|
+
issues++;
|
|
926
|
+
}
|
|
927
|
+
|
|
765
928
|
if (issues === 0) console.log('All good.');
|
|
766
929
|
else console.log(`\n${issues} issue(s) found.`);
|
|
767
930
|
}
|