@rhize/skill-forge 0.17.0 → 0.17.1
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 +1 -1
- package/dist/cli.js +138 -66
- package/dist/cli.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
**The supply-chain gate for agent skills.**
|
|
4
4
|
|
|
5
|
-
Status: published — `@rhize/skill-forge@0.17.
|
|
5
|
+
Status: published — `@rhize/skill-forge@0.17.1` on npm (released 2026-09-05),
|
|
6
6
|
0.x beta (Pro features free until 1.0)
|
|
7
7
|
|
|
8
8
|
> This project is unrelated to the [`skillforge`](https://www.npmjs.com/package/skillforge)
|
package/dist/cli.js
CHANGED
|
@@ -1193,12 +1193,20 @@ function refineRmRf(line, _ctx) {
|
|
|
1193
1193
|
}
|
|
1194
1194
|
var DOC_CUE_RE = /\b(never|not|don't|do not|must not|reject|refuse|attack|dangerous|unsafe|avoid|warning)\b/i;
|
|
1195
1195
|
var DOC_EXTENSIONS = /* @__PURE__ */ new Set([".md", ".mdx", ".markdown"]);
|
|
1196
|
-
var
|
|
1196
|
+
var INSTALL_CUE_RE = /\b(install|run|paste|set ?up)\b/gi;
|
|
1197
|
+
var NEGATION_BEFORE_CUE_RE = /\b(never|not|don't|avoid|shouldn't|mustn't)(\s+\w+)?\s*$/i;
|
|
1198
|
+
function hasUnnegatedInstallCue(line) {
|
|
1199
|
+
for (const m of line.matchAll(INSTALL_CUE_RE)) {
|
|
1200
|
+
if (!NEGATION_BEFORE_CUE_RE.test(line.slice(0, m.index))) return true;
|
|
1201
|
+
}
|
|
1202
|
+
return false;
|
|
1203
|
+
}
|
|
1197
1204
|
function refineCurlPipeShell(line, ctx) {
|
|
1198
1205
|
const base = { severity: "CRITICAL", message: "downloads a remote script and pipes it directly into a shell" };
|
|
1199
1206
|
if (ctx.inFence) return base;
|
|
1200
1207
|
if (!DOC_EXTENSIONS.has(extname2(ctx.file).toLowerCase())) return base;
|
|
1201
1208
|
if (!DOC_CUE_RE.test(line)) return base;
|
|
1209
|
+
if (hasUnnegatedInstallCue(line)) return base;
|
|
1202
1210
|
return { ...base, classification: "documentation-example" };
|
|
1203
1211
|
}
|
|
1204
1212
|
function computeFenceFlags(lines) {
|
|
@@ -1296,11 +1304,11 @@ function scanStaticRules(targetPath) {
|
|
|
1296
1304
|
const content = readTextFile(file);
|
|
1297
1305
|
if (content === void 0) continue;
|
|
1298
1306
|
const lines = content.split("\n");
|
|
1299
|
-
const fenceFlags = DOC_EXTENSIONS.has(extname2(file).toLowerCase()) ? computeFenceFlags(lines) :
|
|
1307
|
+
const fenceFlags = DOC_EXTENSIONS.has(extname2(file).toLowerCase()) ? computeFenceFlags(lines) : void 0;
|
|
1300
1308
|
for (const rule of DENY_RULES) {
|
|
1301
1309
|
for (let i = 0; i < lines.length; i++) {
|
|
1302
1310
|
if (rule.pattern.test(lines[i])) {
|
|
1303
|
-
const refined = rule.refine ? rule.refine(lines[i], { inFence: fenceFlags[i], file }) : { severity: rule.severity, message: rule.message };
|
|
1311
|
+
const refined = rule.refine ? rule.refine(lines[i], { inFence: fenceFlags?.[i] ?? false, file }) : { severity: rule.severity, message: rule.message };
|
|
1304
1312
|
if (!refined) continue;
|
|
1305
1313
|
findings.push({
|
|
1306
1314
|
severity: refined.severity,
|
|
@@ -2304,15 +2312,7 @@ function toClusterMembers(members) {
|
|
|
2304
2312
|
function annotateSiblingClusters(clusters, nameByPath) {
|
|
2305
2313
|
return clusters.map((cluster) => {
|
|
2306
2314
|
const names = cluster.members.map((p) => nameByPath.get(p) ?? basename6(p));
|
|
2307
|
-
|
|
2308
|
-
outer: for (let i = 0; i < names.length && allSiblings; i++) {
|
|
2309
|
-
for (let j = i + 1; j < names.length; j++) {
|
|
2310
|
-
if (!isStackSiblingPair(names[i], names[j])) {
|
|
2311
|
-
allSiblings = false;
|
|
2312
|
-
break outer;
|
|
2313
|
-
}
|
|
2314
|
-
}
|
|
2315
|
-
}
|
|
2315
|
+
const allSiblings = names.every((a, i) => names.slice(i + 1).every((b) => isStackSiblingPair(a, b)));
|
|
2316
2316
|
return allSiblings ? { ...cluster, siblingVariants: true, suggestedVerb: "DEFER" } : cluster;
|
|
2317
2317
|
});
|
|
2318
2318
|
}
|
|
@@ -2351,7 +2351,7 @@ function collapseIdenticalContent(skills, configuredRootOrder) {
|
|
|
2351
2351
|
const ai = configuredRootOrder.indexOf(a.root);
|
|
2352
2352
|
const bi = configuredRootOrder.indexOf(b.root);
|
|
2353
2353
|
if (ai !== bi) return ai - bi;
|
|
2354
|
-
return a.path
|
|
2354
|
+
return comparePaths(a.path, b.path);
|
|
2355
2355
|
});
|
|
2356
2356
|
const canonical = sorted[0];
|
|
2357
2357
|
for (const duplicate of sorted.slice(1)) {
|
|
@@ -2362,23 +2362,41 @@ function collapseIdenticalContent(skills, configuredRootOrder) {
|
|
|
2362
2362
|
}
|
|
2363
2363
|
return { skills: skills.filter((s) => !toRemove.has(s)), collapsedCount };
|
|
2364
2364
|
}
|
|
2365
|
-
function
|
|
2365
|
+
function comparePaths(a, b) {
|
|
2366
|
+
return a < b ? -1 : a > b ? 1 : 0;
|
|
2367
|
+
}
|
|
2368
|
+
function buildUsageJoinIndex(usage, skills) {
|
|
2369
|
+
const bareKeys = /* @__PURE__ */ new Map();
|
|
2370
|
+
for (const key of usage.keys()) {
|
|
2371
|
+
const bare = key.includes(":") ? key.slice(key.lastIndexOf(":") + 1) : key;
|
|
2372
|
+
bareKeys.set(bare, [...bareKeys.get(bare) ?? [], key]);
|
|
2373
|
+
}
|
|
2374
|
+
const inventoryNameCounts = /* @__PURE__ */ new Map();
|
|
2375
|
+
for (const skill of skills) inventoryNameCounts.set(skill.name, (inventoryNameCounts.get(skill.name) ?? 0) + 1);
|
|
2376
|
+
return { bareKeys, inventoryNameCounts, noticed: /* @__PURE__ */ new Set() };
|
|
2377
|
+
}
|
|
2378
|
+
function matchUsage(skill, usage, index, notices) {
|
|
2366
2379
|
if (usage.has(skill.name)) return usage.get(skill.name);
|
|
2367
2380
|
if (skill.pluginId) {
|
|
2368
2381
|
const composite = `${skill.pluginId.split("@")[0]}:${skill.name}`;
|
|
2369
2382
|
if (usage.has(composite)) return usage.get(composite);
|
|
2370
2383
|
}
|
|
2371
|
-
const bareMatches =
|
|
2372
|
-
if (bareMatches.length ===
|
|
2373
|
-
|
|
2374
|
-
|
|
2384
|
+
const bareMatches = index.bareKeys.get(skill.name) ?? [];
|
|
2385
|
+
if (bareMatches.length === 0) return void 0;
|
|
2386
|
+
const installedWithName = index.inventoryNameCounts.get(skill.name) ?? 0;
|
|
2387
|
+
if (bareMatches.length === 1 && installedWithName === 1) return usage.get(bareMatches[0]);
|
|
2388
|
+
if (!index.noticed.has(skill.name)) {
|
|
2389
|
+
index.noticed.add(skill.name);
|
|
2390
|
+
notices.push(
|
|
2391
|
+
bareMatches.length > 1 ? `usage snapshot: ambiguous bare name "${skill.name}" matches ${bareMatches.length} entries \u2014 skipping attribution` : `usage snapshot: bare name "${skill.name}" matches ${installedWithName} installed skills \u2014 skipping attribution`
|
|
2392
|
+
);
|
|
2375
2393
|
}
|
|
2376
2394
|
return void 0;
|
|
2377
2395
|
}
|
|
2378
2396
|
function isUnderInstallPath(target, installPath) {
|
|
2379
2397
|
return target === installPath || target.startsWith(installPath + sep2);
|
|
2380
2398
|
}
|
|
2381
|
-
function computePluginSummaries(pluginRoots, activeFindings, skills, usage) {
|
|
2399
|
+
function computePluginSummaries(pluginRoots, activeFindings, skills, usage, usageJoined) {
|
|
2382
2400
|
return pluginRoots.map((pr) => {
|
|
2383
2401
|
const findingCounts = { LOW: 0, MEDIUM: 0, HIGH: 0, CRITICAL: 0 };
|
|
2384
2402
|
for (const f of activeFindings) {
|
|
@@ -2391,9 +2409,10 @@ function computePluginSummaries(pluginRoots, activeFindings, skills, usage) {
|
|
|
2391
2409
|
const observedSkillCount = hasUsage ? skills.filter((s) => s.pluginId === pr.pluginId && (s.usage ?? 0) > 0).length : void 0;
|
|
2392
2410
|
let recommendation;
|
|
2393
2411
|
const reasons = [];
|
|
2412
|
+
const longWindow = windowDays !== void 0 && windowDays >= 28;
|
|
2394
2413
|
if (findingCounts.HIGH > 0 || findingCounts.CRITICAL > 0) {
|
|
2395
2414
|
recommendation = "review";
|
|
2396
|
-
} else if (exhaustive &&
|
|
2415
|
+
} else if (exhaustive && longWindow && skillCount > 0 && observedSkillCount === 0 && (usageJoined ?? 0) > 0) {
|
|
2397
2416
|
recommendation = "unobserved";
|
|
2398
2417
|
} else if (exhaustive && (observedSkillCount ?? 0) >= 1) {
|
|
2399
2418
|
recommendation = "keep";
|
|
@@ -2401,9 +2420,10 @@ function computePluginSummaries(pluginRoots, activeFindings, skills, usage) {
|
|
|
2401
2420
|
recommendation = "unknown";
|
|
2402
2421
|
if (skillCount === 0) reasons.push("no skills to observe");
|
|
2403
2422
|
if (!hasUsage) reasons.push("no usage telemetry");
|
|
2423
|
+
else if (usage.usage.size === 0) reasons.push("usage snapshot is empty or unreadable (see notices)");
|
|
2404
2424
|
else if (!exhaustive) reasons.push("usage snapshot is not exhaustive (top-50 only)");
|
|
2405
|
-
else if (
|
|
2406
|
-
if (
|
|
2425
|
+
else if (!longWindow) reasons.push(`window ${windowDays ?? "unknown"} < 28 days`);
|
|
2426
|
+
else if ((usageJoined ?? 0) === 0) reasons.push("usage snapshot joined 0 installed skills");
|
|
2407
2427
|
}
|
|
2408
2428
|
return {
|
|
2409
2429
|
pluginId: pr.pluginId,
|
|
@@ -2413,9 +2433,9 @@ function computePluginSummaries(pluginRoots, activeFindings, skills, usage) {
|
|
|
2413
2433
|
findingCounts,
|
|
2414
2434
|
observedSkillCount,
|
|
2415
2435
|
recommendation,
|
|
2416
|
-
reasons:
|
|
2436
|
+
reasons: reasons.sort()
|
|
2417
2437
|
};
|
|
2418
|
-
}).sort((a, b) => a.pluginId
|
|
2438
|
+
}).sort((a, b) => comparePaths(a.pluginId, b.pluginId));
|
|
2419
2439
|
}
|
|
2420
2440
|
function defaultWhich(cmd) {
|
|
2421
2441
|
const pathEnv = process.env.PATH ?? "";
|
|
@@ -2624,8 +2644,9 @@ async function auditConfiguredSet(config, options = {}) {
|
|
|
2624
2644
|
};
|
|
2625
2645
|
const usage = options.usage;
|
|
2626
2646
|
if (usage) {
|
|
2647
|
+
const index = buildUsageJoinIndex(usage.usage, skills);
|
|
2627
2648
|
for (const skill of skills) {
|
|
2628
|
-
const matched = matchUsage(skill, usage.usage, notices);
|
|
2649
|
+
const matched = matchUsage(skill, usage.usage, index, notices);
|
|
2629
2650
|
if (matched !== void 0) skill.usage = matched;
|
|
2630
2651
|
}
|
|
2631
2652
|
}
|
|
@@ -2633,7 +2654,7 @@ async function auditConfiguredSet(config, options = {}) {
|
|
|
2633
2654
|
if (usage && usageJoined === 0) {
|
|
2634
2655
|
notices.push(`usage snapshot (${usage.sourcePath}) joined 0 of ${skills.length} skill(s)`);
|
|
2635
2656
|
}
|
|
2636
|
-
const unobservedInWindow = usage && usage.exhaustive ? skills.filter((s) => (s.usage ?? 0) === 0).map((s) => ({ name: s.name, path: s.path, pluginId: s.pluginId })) : [];
|
|
2657
|
+
const unobservedInWindow = usage && usage.exhaustive && usage.windowDays !== void 0 && usage.windowDays >= 28 && (usageJoined ?? 0) > 0 ? skills.filter((s) => (s.usage ?? 0) === 0).map((s) => ({ name: s.name, path: s.path, pluginId: s.pluginId })) : [];
|
|
2637
2658
|
const findingCounts = { LOW: 0, MEDIUM: 0, HIGH: 0, CRITICAL: 0 };
|
|
2638
2659
|
for (const f of activeFindings) findingCounts[f.severity]++;
|
|
2639
2660
|
const summary = {
|
|
@@ -2648,7 +2669,7 @@ async function auditConfiguredSet(config, options = {}) {
|
|
|
2648
2669
|
documentationExampleCount: activeFindings.filter((f) => f.classification === "documentation-example").length,
|
|
2649
2670
|
...usage ? { usageJoined, usageExhaustive: usage.exhaustive, usageWindowDays: usage.windowDays } : {}
|
|
2650
2671
|
};
|
|
2651
|
-
const plugins = options.pluginRoots ? computePluginSummaries(pluginRoots, activeFindings, skills, usage) : void 0;
|
|
2672
|
+
const plugins = options.pluginRoots ? computePluginSummaries(pluginRoots, activeFindings, skills, usage, usageJoined) : void 0;
|
|
2652
2673
|
const tooling = checkTooling(options.which ?? defaultWhich, options.env ?? process.env);
|
|
2653
2674
|
return {
|
|
2654
2675
|
schemaVersion: 1,
|
|
@@ -2731,7 +2752,7 @@ function renderFindingsSection(findings) {
|
|
|
2731
2752
|
const docExamples = findings.filter((f) => f.classification === "documentation-example");
|
|
2732
2753
|
const lines = ["## Hygiene findings", "", ...bulletList(live.map(renderFindingLine), "(none \u2014 clean)")];
|
|
2733
2754
|
if (docExamples.length > 0) {
|
|
2734
|
-
lines.push("", "###
|
|
2755
|
+
lines.push("", "### Heuristically classified as documentation examples (still CRITICAL \u2014 review each; the heuristic can be gamed)", "", ...bulletList(docExamples.map(renderFindingLine), ""));
|
|
2735
2756
|
}
|
|
2736
2757
|
return lines;
|
|
2737
2758
|
}
|
|
@@ -2778,7 +2799,9 @@ function renderUsageSection(report) {
|
|
|
2778
2799
|
const lines = ["## Usage", ""];
|
|
2779
2800
|
lines.push(`- Matched: ${summary.usageJoined ?? 0} of ${summary.skillCount} skill(s)`);
|
|
2780
2801
|
lines.push(`- Window: ${summary.usageWindowDays !== void 0 ? `${summary.usageWindowDays} day(s)` : "unknown"}`);
|
|
2781
|
-
if (summary.
|
|
2802
|
+
if ((summary.usageJoined ?? 0) === 0) {
|
|
2803
|
+
lines.push("- Coverage: nothing joined (see Notices)");
|
|
2804
|
+
} else if (summary.usageExhaustive) {
|
|
2782
2805
|
lines.push("- Coverage: exhaustive (every skill counted this window)");
|
|
2783
2806
|
} else {
|
|
2784
2807
|
lines.push("- Coverage: top-50 only; absence is not zero usage");
|
|
@@ -2788,6 +2811,8 @@ function renderUsageSection(report) {
|
|
|
2788
2811
|
for (const s of report.opportunities.unobservedInWindow) {
|
|
2789
2812
|
lines.push(`- ${sanitizeForMarkdown(s.name)} \u2014 ${s.path}${s.pluginId ? ` (plugin ${sanitizeForMarkdown(s.pluginId)})` : ""}`);
|
|
2790
2813
|
}
|
|
2814
|
+
} else if (summary.usageExhaustive && (summary.usageJoined ?? 0) > 0 && (summary.usageWindowDays ?? 0) < 28) {
|
|
2815
|
+
lines.push("- Unobserved list: suppressed \u2014 window shorter than 28 days proves nothing about monthly-use skills");
|
|
2791
2816
|
}
|
|
2792
2817
|
return lines;
|
|
2793
2818
|
}
|
|
@@ -2843,6 +2868,7 @@ function renderAuditMarkdown(report) {
|
|
|
2843
2868
|
`- Overlap clusters: ${summary.overlapClusterCount}`,
|
|
2844
2869
|
`- Evolve-eligible skills: ${summary.evolveEligibleCount}`,
|
|
2845
2870
|
`- Collapsed duplicates: ${summary.collapsedDuplicateCount}`,
|
|
2871
|
+
`- Documentation-example classified (heuristic; counted above as CRITICAL): ${summary.documentationExampleCount}`,
|
|
2846
2872
|
"",
|
|
2847
2873
|
"## Inventory",
|
|
2848
2874
|
"",
|
|
@@ -2973,7 +2999,9 @@ function summarizeAudit(report, reportPath) {
|
|
|
2973
2999
|
for (const skill of report.inventory.skills) {
|
|
2974
3000
|
rootOfSkillPath.set(skill.path, skill.root);
|
|
2975
3001
|
}
|
|
3002
|
+
const pluginRootPaths = new Set(report.inventory.skillRoots.filter((r) => r.origin === "plugin").map((r) => r.root));
|
|
2976
3003
|
const byRoot = {};
|
|
3004
|
+
const pluginCounts = emptyCounts();
|
|
2977
3005
|
for (const finding2 of report.hygiene) {
|
|
2978
3006
|
let root;
|
|
2979
3007
|
for (let dir = finding2.target; ; dir = dirname5(dir)) {
|
|
@@ -2981,19 +3009,24 @@ function summarizeAudit(report, reportPath) {
|
|
|
2981
3009
|
if (root || dirname5(dir) === dir) break;
|
|
2982
3010
|
}
|
|
2983
3011
|
if (!root) continue;
|
|
3012
|
+
if (pluginRootPaths.has(root)) {
|
|
3013
|
+
bump(pluginCounts, finding2.severity);
|
|
3014
|
+
continue;
|
|
3015
|
+
}
|
|
2984
3016
|
byRoot[root] ??= emptyCounts();
|
|
2985
3017
|
bump(byRoot[root], finding2.severity);
|
|
2986
3018
|
}
|
|
3019
|
+
const findings = emptyCounts();
|
|
3020
|
+
for (const severity of Object.keys(findings)) {
|
|
3021
|
+
findings[severity] = report.summary.findingCounts[severity] - pluginCounts[severity];
|
|
3022
|
+
}
|
|
2987
3023
|
return {
|
|
2988
3024
|
schemaVersion: 1,
|
|
2989
3025
|
lastRunAt: report.generatedAt,
|
|
2990
3026
|
reportPath,
|
|
2991
3027
|
skillCount: report.summary.skillCount,
|
|
2992
3028
|
mcpTargetCount: report.summary.mcpTargetCount,
|
|
2993
|
-
|
|
2994
|
-
// array, and a second tally is how the terminal summary and the markdown report end up
|
|
2995
|
-
// disagreeing about the same run.
|
|
2996
|
-
findings: { ...report.summary.findingCounts },
|
|
3029
|
+
findings,
|
|
2997
3030
|
byRoot,
|
|
2998
3031
|
overlapClusters: report.opportunities.overlapClusters.map((c) => ({
|
|
2999
3032
|
// Cluster members are SKILL.md paths; the skill name is the parent directory name. Stripped
|
|
@@ -4696,16 +4729,17 @@ function extractEnabledMap(data) {
|
|
|
4696
4729
|
return out;
|
|
4697
4730
|
}
|
|
4698
4731
|
function extractRegistry(data) {
|
|
4699
|
-
|
|
4732
|
+
const empty = { entries: {}, malformed: {} };
|
|
4733
|
+
if (typeof data !== "object" || data === null) return empty;
|
|
4700
4734
|
const pluginsRaw = data.plugins;
|
|
4701
|
-
if (typeof pluginsRaw !== "object" || pluginsRaw === null) return
|
|
4735
|
+
if (typeof pluginsRaw !== "object" || pluginsRaw === null) return empty;
|
|
4702
4736
|
const out = {};
|
|
4737
|
+
const malformed = {};
|
|
4703
4738
|
for (const [id, entriesRaw] of Object.entries(pluginsRaw)) {
|
|
4704
4739
|
if (!Array.isArray(entriesRaw)) continue;
|
|
4705
4740
|
const entries = [];
|
|
4706
4741
|
for (const raw of entriesRaw) {
|
|
4707
|
-
|
|
4708
|
-
const rec = raw;
|
|
4742
|
+
const rec = typeof raw === "object" && raw !== null ? raw : {};
|
|
4709
4743
|
const scope = rec.scope;
|
|
4710
4744
|
const installPath = rec.installPath;
|
|
4711
4745
|
const version = rec.version;
|
|
@@ -4716,11 +4750,13 @@ function extractRegistry(data) {
|
|
|
4716
4750
|
installPath,
|
|
4717
4751
|
version
|
|
4718
4752
|
});
|
|
4753
|
+
} else {
|
|
4754
|
+
malformed[id] = (malformed[id] ?? 0) + 1;
|
|
4719
4755
|
}
|
|
4720
4756
|
}
|
|
4721
4757
|
if (entries.length > 0) out[id] = entries;
|
|
4722
4758
|
}
|
|
4723
|
-
return out;
|
|
4759
|
+
return { entries: out, malformed };
|
|
4724
4760
|
}
|
|
4725
4761
|
function splitPluginId(id) {
|
|
4726
4762
|
const idx = id.lastIndexOf("@");
|
|
@@ -4799,8 +4835,17 @@ function resolveDeclaredPath(installPath, realInstall, declaredPath, explicit, p
|
|
|
4799
4835
|
}
|
|
4800
4836
|
const found = [];
|
|
4801
4837
|
for (const entry of entries) {
|
|
4802
|
-
|
|
4803
|
-
|
|
4838
|
+
let childPath = join20(real, entry.name);
|
|
4839
|
+
if (entry.isSymbolicLink()) {
|
|
4840
|
+
const target = safeRealpath(childPath);
|
|
4841
|
+
if (!target || !isInside2(target, realInstall)) {
|
|
4842
|
+
notices.push(`plugin ${pluginId}: skill dir symlink escapes the install root, skipping: ${join20(declaredPath, entry.name)}`);
|
|
4843
|
+
continue;
|
|
4844
|
+
}
|
|
4845
|
+
childPath = target;
|
|
4846
|
+
} else if (!entry.isDirectory()) {
|
|
4847
|
+
continue;
|
|
4848
|
+
}
|
|
4804
4849
|
if (existsSync16(join20(childPath, "SKILL.md"))) found.push(childPath);
|
|
4805
4850
|
}
|
|
4806
4851
|
return found;
|
|
@@ -4820,7 +4865,7 @@ function resolveSkillDirs(installPath, pluginId) {
|
|
|
4820
4865
|
skillDirs.add(dir);
|
|
4821
4866
|
}
|
|
4822
4867
|
}
|
|
4823
|
-
return { skillDirs: Array.from(skillDirs).sort(), notices };
|
|
4868
|
+
return { skillDirs: Array.from(skillDirs).sort(), notices, realInstall };
|
|
4824
4869
|
}
|
|
4825
4870
|
function discoverClaudePluginRoots(options = {}) {
|
|
4826
4871
|
const homeDir = options.homeDir ?? homedir4();
|
|
@@ -4851,27 +4896,44 @@ function discoverClaudePluginRoots(options = {}) {
|
|
|
4851
4896
|
}
|
|
4852
4897
|
const merged = { ...globalEnabled, ...localEnabled };
|
|
4853
4898
|
const enabledIds = Object.keys(merged).filter((id) => merged[id] === true).sort();
|
|
4854
|
-
const registry = extractRegistry(registryResult.data);
|
|
4899
|
+
const { entries: registry, malformed } = extractRegistry(registryResult.data);
|
|
4855
4900
|
const roots = [];
|
|
4856
4901
|
for (const id of enabledIds) {
|
|
4857
4902
|
const entries = registry[id];
|
|
4903
|
+
const dropped = malformed[id] ?? 0;
|
|
4904
|
+
if (dropped > 0) {
|
|
4905
|
+
notices.push(`plugin ${id}: ignored ${dropped} malformed installed_plugins.json entr${dropped === 1 ? "y" : "ies"} (missing installPath/version or unknown scope)`);
|
|
4906
|
+
}
|
|
4858
4907
|
if (!entries || entries.length === 0) {
|
|
4859
|
-
notices.push(`plugin ${id} is enabled but has no installed_plugins.json entry \u2014 skipping`);
|
|
4908
|
+
notices.push(`plugin ${id} is enabled but has no ${dropped > 0 ? "usable " : ""}installed_plugins.json entry \u2014 skipping`);
|
|
4860
4909
|
continue;
|
|
4861
4910
|
}
|
|
4862
4911
|
const entry = selectInstallEntry(entries, cwd);
|
|
4863
4912
|
const { plugin, marketplace } = splitPluginId(id);
|
|
4864
|
-
const { skillDirs, notices: manifestNotices } = resolveSkillDirs(entry.installPath, id);
|
|
4913
|
+
const { skillDirs, notices: manifestNotices, realInstall } = resolveSkillDirs(entry.installPath, id);
|
|
4865
4914
|
notices.push(...manifestNotices);
|
|
4866
|
-
roots.push({ pluginId: id, marketplace, plugin, version: entry.version, installPath: entry.installPath, skillDirs });
|
|
4915
|
+
roots.push({ pluginId: id, marketplace, plugin, version: entry.version, installPath: realInstall ?? entry.installPath, skillDirs });
|
|
4867
4916
|
}
|
|
4868
|
-
roots.sort((a, b) => a.pluginId.
|
|
4917
|
+
roots.sort((a, b) => a.pluginId < b.pluginId ? -1 : a.pluginId > b.pluginId ? 1 : 0);
|
|
4869
4918
|
return { roots, notices };
|
|
4870
4919
|
}
|
|
4871
4920
|
|
|
4872
4921
|
// src/usageSnapshot.ts
|
|
4873
4922
|
import { readFileSync as readFileSync17, readdirSync as readdirSync9 } from "fs";
|
|
4874
4923
|
import { extname as extname7, join as join21, resolve as resolvePath4 } from "path";
|
|
4924
|
+
function isValidCount(value) {
|
|
4925
|
+
return typeof value === "number" && Number.isFinite(value) && value >= 0;
|
|
4926
|
+
}
|
|
4927
|
+
function toSnapshot(parsed, sourcePath) {
|
|
4928
|
+
return {
|
|
4929
|
+
usage: parsed.usage,
|
|
4930
|
+
exhaustive: parsed.exhaustive,
|
|
4931
|
+
windowDays: parsed.windowDays,
|
|
4932
|
+
generatedAt: parsed.generatedAt,
|
|
4933
|
+
sourcePath,
|
|
4934
|
+
...parsed.warning ? { warning: parsed.warning } : {}
|
|
4935
|
+
};
|
|
4936
|
+
}
|
|
4875
4937
|
function emptySnapshot(sourcePath, warning) {
|
|
4876
4938
|
return { usage: /* @__PURE__ */ new Map(), exhaustive: false, sourcePath, warning };
|
|
4877
4939
|
}
|
|
@@ -4888,31 +4950,35 @@ function parseSnapshotFile(raw) {
|
|
|
4888
4950
|
const generatedAt = typeof report.generated_at === "string" ? report.generated_at : void 0;
|
|
4889
4951
|
const windowDays = typeof report.window_days === "number" ? report.window_days : void 0;
|
|
4890
4952
|
const uniqueSkillsUsed = typeof report.unique_skills_used === "number" ? report.unique_skills_used : void 0;
|
|
4891
|
-
const
|
|
4953
|
+
const skillTotalsRaw = report.skill_totals;
|
|
4954
|
+
const skillTotals = skillTotalsRaw && typeof skillTotalsRaw === "object" && !Array.isArray(skillTotalsRaw) ? skillTotalsRaw : void 0;
|
|
4955
|
+
const warning = skillTotalsRaw !== void 0 && skillTotals === void 0 ? 'usage snapshot: "skill_totals" is not an object \u2014 ignoring it' : void 0;
|
|
4892
4956
|
const topSkills = Array.isArray(report.top_skills) ? report.top_skills : void 0;
|
|
4893
4957
|
const usage = /* @__PURE__ */ new Map();
|
|
4894
4958
|
if (skillTotals) {
|
|
4895
4959
|
for (const [name, count] of Object.entries(skillTotals)) {
|
|
4896
|
-
if (
|
|
4960
|
+
if (isValidCount(count)) usage.set(name, count);
|
|
4897
4961
|
}
|
|
4898
4962
|
} else if (topSkills) {
|
|
4899
4963
|
for (const row of topSkills) {
|
|
4900
|
-
if (Array.isArray(row) && row.length === 2 && typeof row[0] === "string" &&
|
|
4964
|
+
if (Array.isArray(row) && row.length === 2 && typeof row[0] === "string" && isValidCount(row[1])) {
|
|
4901
4965
|
usage.set(row[0], row[1]);
|
|
4902
4966
|
}
|
|
4903
4967
|
}
|
|
4904
4968
|
}
|
|
4905
4969
|
const exhaustive = Boolean(skillTotals) || topSkills !== void 0 && uniqueSkillsUsed !== void 0 && topSkills.length === uniqueSkillsUsed;
|
|
4906
|
-
return { usage, exhaustive, windowDays, generatedAt };
|
|
4970
|
+
return { usage, exhaustive, windowDays, generatedAt, warning };
|
|
4907
4971
|
}
|
|
4908
4972
|
function pickLatest(candidates) {
|
|
4909
4973
|
return candidates.reduce((best, cur) => {
|
|
4910
4974
|
if (!best) return cur;
|
|
4911
|
-
|
|
4912
|
-
|
|
4913
|
-
if (
|
|
4914
|
-
if (
|
|
4915
|
-
|
|
4975
|
+
const curAt = cur.parsed.generatedAt;
|
|
4976
|
+
const bestAt = best.parsed.generatedAt;
|
|
4977
|
+
if (curAt === void 0 && bestAt === void 0) return cur.path > best.path ? cur : best;
|
|
4978
|
+
if (curAt === void 0) return best;
|
|
4979
|
+
if (bestAt === void 0) return cur;
|
|
4980
|
+
if (curAt === bestAt) return cur.path > best.path ? cur : best;
|
|
4981
|
+
return curAt > bestAt ? cur : best;
|
|
4916
4982
|
}, void 0);
|
|
4917
4983
|
}
|
|
4918
4984
|
function loadUsageSnapshot(pathOrDir, opts = {}) {
|
|
@@ -4930,16 +4996,16 @@ function loadUsageSnapshot(pathOrDir, opts = {}) {
|
|
|
4930
4996
|
const candidates = [];
|
|
4931
4997
|
for (const name of entries) {
|
|
4932
4998
|
const filePath = join21(resolved, name);
|
|
4933
|
-
let
|
|
4999
|
+
let raw2;
|
|
4934
5000
|
try {
|
|
4935
|
-
|
|
5001
|
+
raw2 = readFileSync17(filePath, "utf-8");
|
|
4936
5002
|
} catch {
|
|
4937
5003
|
continue;
|
|
4938
5004
|
}
|
|
4939
|
-
const
|
|
4940
|
-
if ("error" in
|
|
4941
|
-
if (opts.windowDays !== void 0 &&
|
|
4942
|
-
candidates.push({ path: filePath,
|
|
5005
|
+
const parsed2 = parseSnapshotFile(raw2);
|
|
5006
|
+
if ("error" in parsed2) continue;
|
|
5007
|
+
if (opts.windowDays !== void 0 && parsed2.windowDays !== opts.windowDays) continue;
|
|
5008
|
+
candidates.push({ path: filePath, parsed: parsed2 });
|
|
4943
5009
|
}
|
|
4944
5010
|
const picked = pickLatest(candidates);
|
|
4945
5011
|
if (!picked) {
|
|
@@ -4948,10 +5014,7 @@ function loadUsageSnapshot(pathOrDir, opts = {}) {
|
|
|
4948
5014
|
opts.windowDays !== void 0 ? `no usage snapshot in ${resolved} matches --usage-window ${opts.windowDays}` : `no readable usage snapshot found in ${resolved}`
|
|
4949
5015
|
);
|
|
4950
5016
|
}
|
|
4951
|
-
|
|
4952
|
-
const parsed2 = parseSnapshotFile(raw2);
|
|
4953
|
-
if ("error" in parsed2) return emptySnapshot(picked.path, parsed2.error);
|
|
4954
|
-
return { usage: parsed2.usage, exhaustive: parsed2.exhaustive, windowDays: parsed2.windowDays, generatedAt: parsed2.generatedAt, sourcePath: picked.path };
|
|
5017
|
+
return toSnapshot(picked.parsed, picked.path);
|
|
4955
5018
|
}
|
|
4956
5019
|
let raw;
|
|
4957
5020
|
try {
|
|
@@ -4961,7 +5024,7 @@ function loadUsageSnapshot(pathOrDir, opts = {}) {
|
|
|
4961
5024
|
}
|
|
4962
5025
|
const parsed = parseSnapshotFile(raw);
|
|
4963
5026
|
if ("error" in parsed) return emptySnapshot(resolved, parsed.error);
|
|
4964
|
-
return
|
|
5027
|
+
return toSnapshot(parsed, resolved);
|
|
4965
5028
|
}
|
|
4966
5029
|
|
|
4967
5030
|
// src/commands/audit.ts
|
|
@@ -7902,6 +7965,15 @@ async function routineCommand(options = {}, deps = {}) {
|
|
|
7902
7965
|
for (const notice of notices) log(`[skill-forge] ${notice}`);
|
|
7903
7966
|
const audit2 = await auditConfiguredSet(config, { accepted, pluginRoots, usage });
|
|
7904
7967
|
const auditState = summarizeAudit(audit2);
|
|
7968
|
+
if (audit2.plugins) {
|
|
7969
|
+
const critical = audit2.plugins.reduce((n, p) => n + p.findingCounts.CRITICAL, 0);
|
|
7970
|
+
const high = audit2.plugins.reduce((n, p) => n + p.findingCounts.HIGH, 0);
|
|
7971
|
+
if (critical + high > 0) {
|
|
7972
|
+
const notice = `plugin-origin findings (${critical} CRITICAL, ${high} HIGH) are excluded from --fail-on and the add/scan advisory \u2014 see the Plugins section for per-plugin review recommendations`;
|
|
7973
|
+
notices.push(notice);
|
|
7974
|
+
log(`[skill-forge] ${notice}`);
|
|
7975
|
+
}
|
|
7976
|
+
}
|
|
7905
7977
|
const degraded = [];
|
|
7906
7978
|
let watch;
|
|
7907
7979
|
if (!options.skipWatch) {
|