@npeercy/skills 0.1.4 → 0.1.6
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/skills.js +2 -2
- package/lib/skills.js +148 -11
- package/package.json +1 -1
package/bin/skills.js
CHANGED
|
@@ -23,7 +23,7 @@ Setup:
|
|
|
23
23
|
Discovery:
|
|
24
24
|
search [query] Search skills (no query = browse all)
|
|
25
25
|
info <skill> Skill details + versions
|
|
26
|
-
list [--verify] [--outdated] [--json] Show all local skills (managed + unmanaged)
|
|
26
|
+
list [--verify] [--outdated] [--json] Show all local skills (managed + unmanaged + bundled)
|
|
27
27
|
|
|
28
28
|
Install:
|
|
29
29
|
install <skill>[@ver] [--agent <a>] [--dry-run]
|
|
@@ -99,7 +99,7 @@ async function maybeWarnOutdated() {
|
|
|
99
99
|
async function main() {
|
|
100
100
|
const args = process.argv.slice(2);
|
|
101
101
|
|
|
102
|
-
// `skills` by itself -> list all local skills (managed + unmanaged)
|
|
102
|
+
// `skills` by itself -> list all local skills (managed + unmanaged + bundled)
|
|
103
103
|
if (args.length === 0) {
|
|
104
104
|
await maybeWarnOutdated();
|
|
105
105
|
await cmdList({ verify: false, outdated: false, json: false });
|
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,22 +209,24 @@ 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);
|
|
219
|
+
const skillMd = skillMdPathFromEntry(full, entry);
|
|
220
|
+
if (!skillMd) continue;
|
|
132
221
|
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
const key = entry.name;
|
|
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
|
|
|
@@ -167,11 +259,24 @@ async function installBuiltins(agents) {
|
|
|
167
259
|
const st = loadState();
|
|
168
260
|
|
|
169
261
|
const installed = [];
|
|
262
|
+
const skipped = [];
|
|
263
|
+
const nameUsage = discoverSkillNameUsage(agents);
|
|
264
|
+
|
|
170
265
|
for (const b of bundledBuiltins()) {
|
|
171
266
|
const srcDir = b.srcDir;
|
|
172
267
|
const id = b.id;
|
|
173
268
|
const version = 'v1';
|
|
174
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
|
+
|
|
175
280
|
const files = collectFiles(srcDir);
|
|
176
281
|
|
|
177
282
|
// Write to managed store
|
|
@@ -184,8 +289,7 @@ async function installBuiltins(agents) {
|
|
|
184
289
|
}
|
|
185
290
|
|
|
186
291
|
// Symlink into all detected agents
|
|
187
|
-
const
|
|
188
|
-
for (const agentPath of Object.values(agents)) {
|
|
292
|
+
for (const [agentName, agentPath] of Object.entries(agents)) {
|
|
189
293
|
mkdirSync(agentPath, { recursive: true });
|
|
190
294
|
const link = join(agentPath, linkName);
|
|
191
295
|
try {
|
|
@@ -196,6 +300,14 @@ async function installBuiltins(agents) {
|
|
|
196
300
|
// doesn't exist
|
|
197
301
|
}
|
|
198
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
|
+
});
|
|
199
311
|
}
|
|
200
312
|
|
|
201
313
|
st.installed[id] = {
|
|
@@ -211,12 +323,22 @@ async function installBuiltins(agents) {
|
|
|
211
323
|
}
|
|
212
324
|
|
|
213
325
|
saveState(st);
|
|
326
|
+
|
|
214
327
|
if (installed.length > 0) {
|
|
215
328
|
console.log('\nInstalled built-in skills:');
|
|
216
329
|
for (const name of installed) {
|
|
217
330
|
console.log(` ✓ ${name} → ${Object.keys(agents).join(', ') || '(no agents detected)'}`);
|
|
218
331
|
}
|
|
219
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
|
+
}
|
|
220
342
|
}
|
|
221
343
|
|
|
222
344
|
// --- Commands ---
|
|
@@ -510,6 +632,15 @@ export async function cmdList(args = {}) {
|
|
|
510
632
|
if (unmanagedCount > 0) {
|
|
511
633
|
console.log(`\nFound ${unmanagedCount} unmanaged skill(s). Run: skills import`);
|
|
512
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
|
+
}
|
|
513
644
|
}
|
|
514
645
|
|
|
515
646
|
function verifyInstall(id, rec, agents) {
|
|
@@ -788,6 +919,12 @@ export async function cmdDoctor(args) {
|
|
|
788
919
|
}
|
|
789
920
|
}
|
|
790
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
|
+
|
|
791
928
|
if (issues === 0) console.log('All good.');
|
|
792
929
|
else console.log(`\n${issues} issue(s) found.`);
|
|
793
930
|
}
|