@rhize/skill-forge 0.8.0 → 0.9.0
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/LICENSE +3 -0
- package/README.md +129 -4
- package/dist/cli.js +1106 -43
- package/dist/cli.js.map +1 -1
- package/dist/ingest-prompt.md +107 -15
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -561,7 +561,7 @@ function resolveMcpCandidateDetail(profile) {
|
|
|
561
561
|
}
|
|
562
562
|
|
|
563
563
|
// src/provenance.ts
|
|
564
|
-
import { appendFileSync, existsSync as existsSync6, statSync as statSync3, writeFileSync as writeFileSync3 } from "fs";
|
|
564
|
+
import { appendFileSync, existsSync as existsSync6, readFileSync as readFileSync5, statSync as statSync3, writeFileSync as writeFileSync3 } from "fs";
|
|
565
565
|
import { join as join7 } from "path";
|
|
566
566
|
var FORGE_VERBS = ["DEFER", "ABSORB", "FORK", "REJECT", "WATCH"];
|
|
567
567
|
var LEDGER_HEADER = "# Skill Forge \u2014 Provenance Ledger\n\nOne entry per external-skill ingestion decision.\n";
|
|
@@ -600,6 +600,55 @@ function appendProvenance(entry, skillsRoot) {
|
|
|
600
600
|
appendFileSync(ledgerPath, block, "utf-8");
|
|
601
601
|
return ledgerPath;
|
|
602
602
|
}
|
|
603
|
+
function extractField(block, label) {
|
|
604
|
+
const escaped = label.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
605
|
+
const match = block.match(new RegExp(`\\*\\*${escaped}:\\*\\*\\s*(.*)`));
|
|
606
|
+
return match ? match[1].trim() : void 0;
|
|
607
|
+
}
|
|
608
|
+
function parseProvenanceLedger(text) {
|
|
609
|
+
const entries = [];
|
|
610
|
+
const warnings = [];
|
|
611
|
+
const blocks = text.split(/\n## /);
|
|
612
|
+
for (const block of blocks.slice(1)) {
|
|
613
|
+
const headerLine = block.split("\n", 1)[0] ?? "";
|
|
614
|
+
const [namePart, ...dateParts] = headerLine.split(" \u2014 ");
|
|
615
|
+
const name = namePart?.trim();
|
|
616
|
+
if (!name) {
|
|
617
|
+
warnings.push(`skipped malformed ledger block: missing "## <name> \u2014 <date>" header (saw "${headerLine.slice(0, 80)}")`);
|
|
618
|
+
continue;
|
|
619
|
+
}
|
|
620
|
+
const date = dateParts.join(" \u2014 ").trim();
|
|
621
|
+
const verbRaw = extractField(block, "Verb");
|
|
622
|
+
if (!verbRaw || !FORGE_VERBS.includes(verbRaw)) {
|
|
623
|
+
warnings.push(`skipped malformed ledger block for "${name}": missing or invalid Verb (saw "${verbRaw ?? ""}")`);
|
|
624
|
+
continue;
|
|
625
|
+
}
|
|
626
|
+
const driftCheckMatch = block.match(/\*\*Drift check:\*\*\s*`([^`]*)`/);
|
|
627
|
+
entries.push({
|
|
628
|
+
name,
|
|
629
|
+
date,
|
|
630
|
+
source: extractField(block, "Source") ?? "",
|
|
631
|
+
upstreamRef: extractField(block, "Upstream ref") ?? "",
|
|
632
|
+
license: extractField(block, "License") ?? "",
|
|
633
|
+
verb: verbRaw,
|
|
634
|
+
target: extractField(block, "Target") ?? "",
|
|
635
|
+
took: extractField(block, "Took") ?? "",
|
|
636
|
+
verified: extractField(block, "Verified") ?? "",
|
|
637
|
+
driftCheck: driftCheckMatch ? driftCheckMatch[1] : "",
|
|
638
|
+
notes: extractField(block, "Notes") ?? ""
|
|
639
|
+
});
|
|
640
|
+
}
|
|
641
|
+
return { entries, warnings };
|
|
642
|
+
}
|
|
643
|
+
function readProvenanceLedger(skillsRoot) {
|
|
644
|
+
const ledgerPath = join7(skillsRoot, "SOURCES.md");
|
|
645
|
+
if (!existsSync6(ledgerPath)) {
|
|
646
|
+
return { ledgerPath, exists: false, entries: [], warnings: [] };
|
|
647
|
+
}
|
|
648
|
+
const text = readFileSync5(ledgerPath, "utf-8");
|
|
649
|
+
const parsed = parseProvenanceLedger(text);
|
|
650
|
+
return { ledgerPath, exists: true, ...parsed };
|
|
651
|
+
}
|
|
603
652
|
|
|
604
653
|
// src/commands/gatePipeline.ts
|
|
605
654
|
import { readdirSync as readdirSync5 } from "fs";
|
|
@@ -612,7 +661,7 @@ import {
|
|
|
612
661
|
existsSync as existsSync8,
|
|
613
662
|
mkdtempSync,
|
|
614
663
|
openSync,
|
|
615
|
-
readFileSync as
|
|
664
|
+
readFileSync as readFileSync7,
|
|
616
665
|
readdirSync as readdirSync3,
|
|
617
666
|
readSync,
|
|
618
667
|
rmSync as rmSync2,
|
|
@@ -622,13 +671,13 @@ import { tmpdir } from "os";
|
|
|
622
671
|
import { basename as basename3, dirname as dirname3, extname as extname3, join as join9, resolve as resolve2 } from "path";
|
|
623
672
|
|
|
624
673
|
// src/gate/mcpCapabilities.ts
|
|
625
|
-
import { existsSync as existsSync7, readFileSync as
|
|
674
|
+
import { existsSync as existsSync7, readFileSync as readFileSync6 } from "fs";
|
|
626
675
|
import { extname as extname2, join as join8 } from "path";
|
|
627
676
|
var SOURCE_EXT = /* @__PURE__ */ new Set([".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs"]);
|
|
628
677
|
var MAX_ITEMS_PER_CATEGORY = 200;
|
|
629
678
|
function readJsonSafe(file) {
|
|
630
679
|
try {
|
|
631
|
-
return JSON.parse(
|
|
680
|
+
return JSON.parse(readFileSync6(file, "utf-8"));
|
|
632
681
|
} catch {
|
|
633
682
|
return void 0;
|
|
634
683
|
}
|
|
@@ -856,7 +905,7 @@ function readPackageJsonSafe(root) {
|
|
|
856
905
|
const pkgPath = join9(root, "package.json");
|
|
857
906
|
if (!existsSync8(pkgPath)) return void 0;
|
|
858
907
|
try {
|
|
859
|
-
return JSON.parse(
|
|
908
|
+
return JSON.parse(readFileSync7(pkgPath, "utf-8"));
|
|
860
909
|
} catch {
|
|
861
910
|
return void 0;
|
|
862
911
|
}
|
|
@@ -1016,7 +1065,7 @@ function detectLicense(root, fm) {
|
|
|
1016
1065
|
for (const name of names) {
|
|
1017
1066
|
const p = join9(root, name);
|
|
1018
1067
|
if (existsSync8(p) && statSync4(p).isFile()) {
|
|
1019
|
-
const head =
|
|
1068
|
+
const head = readFileSync7(p, "utf-8").slice(0, 2e3).toLowerCase();
|
|
1020
1069
|
for (const [hint, label] of Object.entries(SPDX_HINTS)) {
|
|
1021
1070
|
if (head.includes(hint)) return label;
|
|
1022
1071
|
}
|
|
@@ -1055,7 +1104,7 @@ function scanMcpAndImports(text, root) {
|
|
|
1055
1104
|
if (isDir(scriptsDir)) {
|
|
1056
1105
|
for (const f of walkFiles(scriptsDir)) {
|
|
1057
1106
|
if (!f.endsWith(".py")) continue;
|
|
1058
|
-
const ft =
|
|
1107
|
+
const ft = readFileSync7(f, "utf-8");
|
|
1059
1108
|
for (const m2 of ft.match(mcpRefRe) ?? []) mcpRefs.add(m2);
|
|
1060
1109
|
const importRe = /^\s*(?:import|from)\s+([a-zA-Z0-9_]+)/gm;
|
|
1061
1110
|
let m;
|
|
@@ -1072,7 +1121,7 @@ function scanMcpAndImports(text, root) {
|
|
|
1072
1121
|
function profileSkillDir(inputPath) {
|
|
1073
1122
|
const { skillMd, root, tmpDir } = resolveSkillMd(inputPath);
|
|
1074
1123
|
try {
|
|
1075
|
-
const text =
|
|
1124
|
+
const text = readFileSync7(skillMd, "utf-8");
|
|
1076
1125
|
const fm = parseFrontmatter(text);
|
|
1077
1126
|
const bodyLines = (text.match(/\n/g)?.length ?? 0) + 1;
|
|
1078
1127
|
const headers = Array.from(text.matchAll(/^#{1,3}\s+(.+)$/gm)).map((m) => m[1]);
|
|
@@ -1103,7 +1152,7 @@ function profilePlugin(p) {
|
|
|
1103
1152
|
const manifestPath = join9(p, ".claude-plugin", "plugin.json");
|
|
1104
1153
|
let manifest;
|
|
1105
1154
|
try {
|
|
1106
|
-
manifest = JSON.parse(
|
|
1155
|
+
manifest = JSON.parse(readFileSync7(manifestPath, "utf-8"));
|
|
1107
1156
|
} catch (err) {
|
|
1108
1157
|
throw new Error(`could not parse plugin.json under ${p}: ${err.message}`);
|
|
1109
1158
|
}
|
|
@@ -1134,7 +1183,7 @@ function profileMcp(p) {
|
|
|
1134
1183
|
if (explicitConfig) {
|
|
1135
1184
|
let cfg;
|
|
1136
1185
|
try {
|
|
1137
|
-
cfg = JSON.parse(
|
|
1186
|
+
cfg = JSON.parse(readFileSync7(explicitConfig, "utf-8"));
|
|
1138
1187
|
} catch (err) {
|
|
1139
1188
|
throw new Error(`could not parse MCP config ${explicitConfig}: ${err.message}`);
|
|
1140
1189
|
}
|
|
@@ -1153,7 +1202,7 @@ function profileMcp(p) {
|
|
|
1153
1202
|
const pkgPath = join9(p, "package.json");
|
|
1154
1203
|
let pkg2;
|
|
1155
1204
|
try {
|
|
1156
|
-
pkg2 = JSON.parse(
|
|
1205
|
+
pkg2 = JSON.parse(readFileSync7(pkgPath, "utf-8"));
|
|
1157
1206
|
} catch (err) {
|
|
1158
1207
|
throw new Error(`could not parse package.json ${pkgPath}: ${err.message}`);
|
|
1159
1208
|
}
|
|
@@ -1311,7 +1360,7 @@ async function scanSafety(targetPath, strictness = DEFAULT_STRICTNESS) {
|
|
|
1311
1360
|
}
|
|
1312
1361
|
|
|
1313
1362
|
// src/gate/overlap.ts
|
|
1314
|
-
import { existsSync as existsSync9, readFileSync as
|
|
1363
|
+
import { existsSync as existsSync9, readFileSync as readFileSync8, readdirSync as readdirSync4 } from "fs";
|
|
1315
1364
|
import { basename as basename4, dirname as dirname4, join as join11, resolve as resolve3 } from "path";
|
|
1316
1365
|
var STOPWORDS = new Set(
|
|
1317
1366
|
`
|
|
@@ -1334,7 +1383,7 @@ function splitLimited(text, sep2, maxSplit) {
|
|
|
1334
1383
|
return parts;
|
|
1335
1384
|
}
|
|
1336
1385
|
function readFrontmatter(skillMdPath) {
|
|
1337
|
-
const text =
|
|
1386
|
+
const text = readFileSync8(skillMdPath, "utf-8");
|
|
1338
1387
|
const meta = { name: basename4(dirname4(skillMdPath)), description: "" };
|
|
1339
1388
|
if (text.startsWith("---\n")) {
|
|
1340
1389
|
const block = splitLimited(text, "---", 2)[1] ?? "";
|
|
@@ -1705,14 +1754,14 @@ async function mcpScan(targetPath, strictness = DEFAULT_STRICTNESS) {
|
|
|
1705
1754
|
}
|
|
1706
1755
|
|
|
1707
1756
|
// src/gate/mcpOverlap.ts
|
|
1708
|
-
import { existsSync as existsSync10, readFileSync as
|
|
1757
|
+
import { existsSync as existsSync10, readFileSync as readFileSync9 } from "fs";
|
|
1709
1758
|
import { resolve as resolve4 } from "path";
|
|
1710
1759
|
function readTargetServers(targetPath) {
|
|
1711
1760
|
const path = resolve4(expandHome(targetPath));
|
|
1712
1761
|
if (!existsSync10(path)) return [];
|
|
1713
1762
|
let cfg;
|
|
1714
1763
|
try {
|
|
1715
|
-
cfg = JSON.parse(
|
|
1764
|
+
cfg = JSON.parse(readFileSync9(path, "utf-8"));
|
|
1716
1765
|
} catch {
|
|
1717
1766
|
return [];
|
|
1718
1767
|
}
|
|
@@ -2020,7 +2069,7 @@ async function runGate(candidatePath, skillsRoot, {
|
|
|
2020
2069
|
|
|
2021
2070
|
// src/queue.ts
|
|
2022
2071
|
import { randomUUID as randomUUID2 } from "crypto";
|
|
2023
|
-
import { existsSync as existsSync11, mkdirSync as mkdirSync5, readFileSync as
|
|
2072
|
+
import { existsSync as existsSync11, mkdirSync as mkdirSync5, readFileSync as readFileSync10, renameSync as renameSync2, rmSync as rmSync3, writeFileSync as writeFileSync4 } from "fs";
|
|
2024
2073
|
import { join as join13 } from "path";
|
|
2025
2074
|
function getQueuePath() {
|
|
2026
2075
|
return join13(getHomeDir(), "queue.json");
|
|
@@ -2030,26 +2079,46 @@ function readQueue() {
|
|
|
2030
2079
|
if (!existsSync11(path)) {
|
|
2031
2080
|
return { version: 1, entries: [] };
|
|
2032
2081
|
}
|
|
2033
|
-
const raw =
|
|
2082
|
+
const raw = readFileSync10(path, "utf-8");
|
|
2034
2083
|
const parsed = JSON.parse(raw);
|
|
2035
2084
|
return { version: 1, entries: parsed.entries ?? [] };
|
|
2036
2085
|
}
|
|
2037
|
-
function writeQueue(
|
|
2038
|
-
|
|
2039
|
-
|
|
2086
|
+
function writeQueue(queue2) {
|
|
2087
|
+
const home = getHomeDir();
|
|
2088
|
+
mkdirSync5(home, { recursive: true });
|
|
2089
|
+
const finalPath = getQueuePath();
|
|
2090
|
+
const tempPath = join13(home, `.queue.json.tmp-${randomUUID2()}`);
|
|
2091
|
+
try {
|
|
2092
|
+
writeFileSync4(tempPath, JSON.stringify(queue2, null, 2) + "\n", "utf-8");
|
|
2093
|
+
renameSync2(tempPath, finalPath);
|
|
2094
|
+
} catch (err) {
|
|
2095
|
+
rmSync3(tempPath, { force: true });
|
|
2096
|
+
throw err;
|
|
2097
|
+
}
|
|
2040
2098
|
}
|
|
2041
2099
|
function appendQueueEntry(entry) {
|
|
2042
|
-
const
|
|
2043
|
-
const existingIds = new Set(
|
|
2100
|
+
const queue2 = readQueue();
|
|
2101
|
+
const existingIds = new Set(queue2.entries.map((e) => e.id));
|
|
2044
2102
|
const full = {
|
|
2045
2103
|
...entry,
|
|
2046
2104
|
id: generateId(existingIds),
|
|
2047
2105
|
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
2048
2106
|
};
|
|
2049
|
-
|
|
2050
|
-
writeQueue(
|
|
2107
|
+
queue2.entries.push(full);
|
|
2108
|
+
writeQueue(queue2);
|
|
2051
2109
|
return full;
|
|
2052
2110
|
}
|
|
2111
|
+
function updateQueueEntry(id, patch) {
|
|
2112
|
+
const queue2 = readQueue();
|
|
2113
|
+
const index = queue2.entries.findIndex((e) => e.id === id);
|
|
2114
|
+
if (index === -1) {
|
|
2115
|
+
throw new Error(`no queue entry with id: ${id}`);
|
|
2116
|
+
}
|
|
2117
|
+
const updated = { ...queue2.entries[index], ...patch };
|
|
2118
|
+
queue2.entries[index] = updated;
|
|
2119
|
+
writeQueue(queue2);
|
|
2120
|
+
return updated;
|
|
2121
|
+
}
|
|
2053
2122
|
function generateId(existingIds) {
|
|
2054
2123
|
let id;
|
|
2055
2124
|
do {
|
|
@@ -2376,15 +2445,15 @@ import {
|
|
|
2376
2445
|
existsSync as existsSync13,
|
|
2377
2446
|
lstatSync,
|
|
2378
2447
|
mkdtempSync as mkdtempSync2,
|
|
2379
|
-
readFileSync as
|
|
2448
|
+
readFileSync as readFileSync11,
|
|
2380
2449
|
readdirSync as readdirSync6,
|
|
2381
2450
|
realpathSync,
|
|
2382
|
-
renameSync as
|
|
2383
|
-
rmSync as
|
|
2451
|
+
renameSync as renameSync3,
|
|
2452
|
+
rmSync as rmSync4,
|
|
2384
2453
|
statSync as statSync8,
|
|
2385
2454
|
writeFileSync as writeFileSync5
|
|
2386
2455
|
} from "fs";
|
|
2387
|
-
import { basename as basename6, extname as extname5, join as join16, resolve as resolvePath2 } from "path";
|
|
2456
|
+
import { basename as basename6, delimiter as delimiter2, extname as extname5, join as join16, resolve as resolvePath2 } from "path";
|
|
2388
2457
|
var OVERSIZED_SKILL_EST_TOKENS = 5e3;
|
|
2389
2458
|
function toAuditFindings(source) {
|
|
2390
2459
|
return source.map((f) => ({ severity: f.severity, rule: f.rule, target: f.file, detail: f.detail }));
|
|
@@ -2545,7 +2614,7 @@ function profileAuditedSkill(group, findings, unreadableSkillMdPaths) {
|
|
|
2545
2614
|
let sizeBytes;
|
|
2546
2615
|
try {
|
|
2547
2616
|
sizeBytes = statSync8(skillMdPath).size;
|
|
2548
|
-
text =
|
|
2617
|
+
text = readFileSync11(skillMdPath, "utf-8");
|
|
2549
2618
|
} catch (err) {
|
|
2550
2619
|
findings.push({
|
|
2551
2620
|
severity: "MEDIUM",
|
|
@@ -2623,7 +2692,7 @@ function auditMcpTarget(targetPath, notices) {
|
|
|
2623
2692
|
}
|
|
2624
2693
|
if (isToml) {
|
|
2625
2694
|
try {
|
|
2626
|
-
|
|
2695
|
+
readFileSync11(targetPath, "utf-8");
|
|
2627
2696
|
} catch (err) {
|
|
2628
2697
|
findings.push({
|
|
2629
2698
|
severity: "MEDIUM",
|
|
@@ -2638,7 +2707,7 @@ function auditMcpTarget(targetPath, notices) {
|
|
|
2638
2707
|
}
|
|
2639
2708
|
let raw;
|
|
2640
2709
|
try {
|
|
2641
|
-
raw =
|
|
2710
|
+
raw = readFileSync11(targetPath, "utf-8");
|
|
2642
2711
|
} catch (err) {
|
|
2643
2712
|
findings.push({
|
|
2644
2713
|
severity: "MEDIUM",
|
|
@@ -2672,7 +2741,7 @@ function auditMcpTarget(targetPath, notices) {
|
|
|
2672
2741
|
}
|
|
2673
2742
|
function clusterOverlapPairs(pairs) {
|
|
2674
2743
|
const parent = /* @__PURE__ */ new Map();
|
|
2675
|
-
function
|
|
2744
|
+
function find2(x) {
|
|
2676
2745
|
if (!parent.has(x)) parent.set(x, x);
|
|
2677
2746
|
let root = x;
|
|
2678
2747
|
while (parent.get(root) !== root) root = parent.get(root);
|
|
@@ -2685,14 +2754,14 @@ function clusterOverlapPairs(pairs) {
|
|
|
2685
2754
|
return root;
|
|
2686
2755
|
}
|
|
2687
2756
|
function union(a, b) {
|
|
2688
|
-
const ra =
|
|
2689
|
-
const rb =
|
|
2757
|
+
const ra = find2(a);
|
|
2758
|
+
const rb = find2(b);
|
|
2690
2759
|
if (ra !== rb) parent.set(ra, rb);
|
|
2691
2760
|
}
|
|
2692
2761
|
for (const pair of pairs) union(pair.aPath, pair.bPath);
|
|
2693
2762
|
const groups = /* @__PURE__ */ new Map();
|
|
2694
2763
|
for (const pair of pairs) {
|
|
2695
|
-
const root =
|
|
2764
|
+
const root = find2(pair.aPath);
|
|
2696
2765
|
const group = groups.get(root) ?? { members: /* @__PURE__ */ new Set(), topScore: 0 };
|
|
2697
2766
|
group.members.add(pair.aPath);
|
|
2698
2767
|
group.members.add(pair.bPath);
|
|
@@ -2708,6 +2777,45 @@ function toClusterMembers(members) {
|
|
|
2708
2777
|
}
|
|
2709
2778
|
return sorted;
|
|
2710
2779
|
}
|
|
2780
|
+
function defaultWhich(cmd) {
|
|
2781
|
+
const pathEnv = process.env.PATH ?? "";
|
|
2782
|
+
const dirs = pathEnv.split(delimiter2).filter(Boolean);
|
|
2783
|
+
const exts = process.platform === "win32" ? (process.env.PATHEXT ?? ".EXE;.CMD;.BAT").split(";") : [""];
|
|
2784
|
+
for (const dir of dirs) {
|
|
2785
|
+
for (const ext of exts) {
|
|
2786
|
+
const candidate = join16(dir, cmd + ext);
|
|
2787
|
+
try {
|
|
2788
|
+
if (statSync8(candidate).isFile()) return candidate;
|
|
2789
|
+
} catch {
|
|
2790
|
+
}
|
|
2791
|
+
}
|
|
2792
|
+
}
|
|
2793
|
+
return void 0;
|
|
2794
|
+
}
|
|
2795
|
+
var SKILLSPECTOR_PROVIDER_KEYMAP = {
|
|
2796
|
+
openai: "OPENAI_API_KEY",
|
|
2797
|
+
anthropic: "ANTHROPIC_API_KEY",
|
|
2798
|
+
nv_build: "NVIDIA_INFERENCE_KEY"
|
|
2799
|
+
};
|
|
2800
|
+
function checkTooling(which2, env) {
|
|
2801
|
+
const skillspectorPath = which2("skillspector");
|
|
2802
|
+
const skillspector = skillspectorPath ? { installed: true } : {
|
|
2803
|
+
installed: false,
|
|
2804
|
+
hint: "NOT installed \u2014 github.com/NVIDIA/skillspector (git clone + make install; Python 3.12+)"
|
|
2805
|
+
};
|
|
2806
|
+
const tokenPresent = Boolean(env.VERCEL_OIDC_TOKEN);
|
|
2807
|
+
const skillsShToken = tokenPresent ? { present: true } : {
|
|
2808
|
+
present: false,
|
|
2809
|
+
hint: "missing \u2014 enable OIDC Federation on your Vercel project, then `vercel link && vercel env pull`"
|
|
2810
|
+
};
|
|
2811
|
+
const provider = env.SKILLSPECTOR_PROVIDER;
|
|
2812
|
+
const providerKeyEnv = provider ? SKILLSPECTOR_PROVIDER_KEYMAP[provider] : void 0;
|
|
2813
|
+
const skillspectorLlm = {
|
|
2814
|
+
configured: Boolean(provider && providerKeyEnv && env[providerKeyEnv]),
|
|
2815
|
+
provider: provider || void 0
|
|
2816
|
+
};
|
|
2817
|
+
return { skillspector, skillsShToken, skillspectorLlm };
|
|
2818
|
+
}
|
|
2711
2819
|
async function auditConfiguredSet(config, options = {}) {
|
|
2712
2820
|
const now = options.now ?? (() => /* @__PURE__ */ new Date());
|
|
2713
2821
|
const oversizedThreshold = options.oversizedSkillTokens ?? OVERSIZED_SKILL_EST_TOKENS;
|
|
@@ -2828,12 +2936,14 @@ async function auditConfiguredSet(config, options = {}) {
|
|
|
2828
2936
|
overlapClusterCount: overlapClusters.length,
|
|
2829
2937
|
evolveEligibleCount: evolveEligible.length
|
|
2830
2938
|
};
|
|
2939
|
+
const tooling = checkTooling(options.which ?? defaultWhich, options.env ?? process.env);
|
|
2831
2940
|
return {
|
|
2832
2941
|
schemaVersion: 1,
|
|
2833
2942
|
generatedAt: now().toISOString(),
|
|
2834
2943
|
inventory: { skillRoots, skills, mcpTargets },
|
|
2835
2944
|
hygiene: findings,
|
|
2836
2945
|
opportunities: { overlapClusters, evolveEligible, foundation, overlapLocked: !overlapStatus.enabled },
|
|
2946
|
+
tooling,
|
|
2837
2947
|
notices,
|
|
2838
2948
|
summary
|
|
2839
2949
|
};
|
|
@@ -2927,6 +3037,28 @@ function renderOpportunitiesSection(report) {
|
|
|
2927
3037
|
`- ${foundation.message}`
|
|
2928
3038
|
];
|
|
2929
3039
|
}
|
|
3040
|
+
function renderToolingSection(tooling) {
|
|
3041
|
+
const lines = ["## Tooling", ""];
|
|
3042
|
+
if (!tooling) {
|
|
3043
|
+
lines.push("- (not available in this report)");
|
|
3044
|
+
return lines;
|
|
3045
|
+
}
|
|
3046
|
+
lines.push(`- SkillSpector CLI: ${tooling.skillspector.installed ? "installed" : "NOT installed"}`);
|
|
3047
|
+
if (tooling.skillspector.hint) lines.push(` - ${tooling.skillspector.hint}`);
|
|
3048
|
+
lines.push(`- skills.sh auth (VERCEL_OIDC_TOKEN): ${tooling.skillsShToken.present ? "present" : "missing"}`);
|
|
3049
|
+
if (tooling.skillsShToken.hint) lines.push(` - ${tooling.skillsShToken.hint}`);
|
|
3050
|
+
const llm = tooling.skillspectorLlm;
|
|
3051
|
+
if (llm.configured) {
|
|
3052
|
+
lines.push(`- SkillSpector LLM stage (optional): configured (provider: ${sanitizeForMarkdown(llm.provider ?? "")})`);
|
|
3053
|
+
} else if (llm.provider) {
|
|
3054
|
+
lines.push(`- SkillSpector LLM stage (optional): not configured \u2014 SKILLSPECTOR_PROVIDER is "${sanitizeForMarkdown(llm.provider)}" but its API key env var is not set`);
|
|
3055
|
+
} else {
|
|
3056
|
+
lines.push(
|
|
3057
|
+
"- SkillSpector LLM stage (optional): not set \u2014 static scan works without it (use --no-llm); set SKILLSPECTOR_PROVIDER + its API key for semantic analysis"
|
|
3058
|
+
);
|
|
3059
|
+
}
|
|
3060
|
+
return lines;
|
|
3061
|
+
}
|
|
2930
3062
|
function renderAuditMarkdown(report) {
|
|
2931
3063
|
const { summary } = report;
|
|
2932
3064
|
const lines = [
|
|
@@ -2953,6 +3085,8 @@ function renderAuditMarkdown(report) {
|
|
|
2953
3085
|
"",
|
|
2954
3086
|
...renderOpportunitiesSection(report),
|
|
2955
3087
|
"",
|
|
3088
|
+
...renderToolingSection(report.tooling),
|
|
3089
|
+
"",
|
|
2956
3090
|
"## Notices",
|
|
2957
3091
|
"",
|
|
2958
3092
|
...bulletList(report.notices, "(none)")
|
|
@@ -3024,9 +3158,9 @@ async function scaffoldFoundationSkill(config, profile, targetRoot) {
|
|
|
3024
3158
|
`refusing to scaffold a foundation skill: the generated SKILL.md was flagged by the safety scan (${safety.findings.map(formatSafetyFinding).join("; ")})`
|
|
3025
3159
|
);
|
|
3026
3160
|
}
|
|
3027
|
-
|
|
3161
|
+
renameSync3(tempDir, destDir);
|
|
3028
3162
|
} catch (err) {
|
|
3029
|
-
|
|
3163
|
+
rmSync4(tempDir, { recursive: true, force: true });
|
|
3030
3164
|
throw err;
|
|
3031
3165
|
}
|
|
3032
3166
|
try {
|
|
@@ -3664,7 +3798,7 @@ async function status() {
|
|
|
3664
3798
|
// src/evolve.ts
|
|
3665
3799
|
import { createInterface as createInterface4 } from "readline/promises";
|
|
3666
3800
|
import { createHash } from "crypto";
|
|
3667
|
-
import { cpSync as cpSync3, existsSync as existsSync16, lstatSync as lstatSync2, mkdtempSync as mkdtempSync3, readFileSync as
|
|
3801
|
+
import { cpSync as cpSync3, existsSync as existsSync16, lstatSync as lstatSync2, mkdtempSync as mkdtempSync3, readFileSync as readFileSync12, rmSync as rmSync5 } from "fs";
|
|
3668
3802
|
import { tmpdir as tmpdir2 } from "os";
|
|
3669
3803
|
import { basename as basename7, dirname as dirname7, join as join20, resolve as resolvePath4, sep } from "path";
|
|
3670
3804
|
var SKILLOPT_BIN = "skillopt-sleep";
|
|
@@ -3765,7 +3899,7 @@ async function evolve(targetSkillDir, options = {}, deps = {}) {
|
|
|
3765
3899
|
safety = await scanSafety(tempDir, strictness);
|
|
3766
3900
|
} finally {
|
|
3767
3901
|
try {
|
|
3768
|
-
|
|
3902
|
+
rmSync5(tempDir, { recursive: true, force: true });
|
|
3769
3903
|
} catch (err) {
|
|
3770
3904
|
console.error(`[skill-forge] failed to clean up temp re-gate dir ${tempDir}: ${err.message}`);
|
|
3771
3905
|
}
|
|
@@ -3847,7 +3981,7 @@ async function evolve(targetSkillDir, options = {}, deps = {}) {
|
|
|
3847
3981
|
logOutcome("[skill-forge] queued for ingestion review", json);
|
|
3848
3982
|
} else if (decision === "reject") {
|
|
3849
3983
|
try {
|
|
3850
|
-
|
|
3984
|
+
rmSync5(stagingDir, { recursive: true, force: true });
|
|
3851
3985
|
} catch (err) {
|
|
3852
3986
|
throw new UserError(
|
|
3853
3987
|
`rejected the proposal, but removing the staging dir ${stagingDir} failed: ${err.message} \u2014 remove it manually.`
|
|
@@ -3902,7 +4036,7 @@ function readManifest(stagingDir) {
|
|
|
3902
4036
|
const manifestPath = join20(stagingDir, "manifest.json");
|
|
3903
4037
|
let raw;
|
|
3904
4038
|
try {
|
|
3905
|
-
raw =
|
|
4039
|
+
raw = readFileSync12(manifestPath, "utf-8");
|
|
3906
4040
|
} catch (err) {
|
|
3907
4041
|
throw new UserError(`could not read staging manifest ${manifestPath}: ${err.message}`);
|
|
3908
4042
|
}
|
|
@@ -3937,7 +4071,7 @@ function validateManifest(raw, manifestPath) {
|
|
|
3937
4071
|
}
|
|
3938
4072
|
function filesIdentical(a, b) {
|
|
3939
4073
|
if (!existsSync16(a) || !existsSync16(b)) return false;
|
|
3940
|
-
return
|
|
4074
|
+
return readFileSync12(a).equals(readFileSync12(b));
|
|
3941
4075
|
}
|
|
3942
4076
|
function isAlreadyAdopted(stagingDir, manifest) {
|
|
3943
4077
|
const proposedSkillMd = join20(stagingDir, "proposed_SKILL.md");
|
|
@@ -3966,7 +4100,7 @@ function assertNotSymlink(path, label) {
|
|
|
3966
4100
|
}
|
|
3967
4101
|
function sha256File(path) {
|
|
3968
4102
|
if (!existsSync16(path)) return null;
|
|
3969
|
-
return createHash("sha256").update(
|
|
4103
|
+
return createHash("sha256").update(readFileSync12(path)).digest("hex");
|
|
3970
4104
|
}
|
|
3971
4105
|
|
|
3972
4106
|
// src/commands/evolve.ts
|
|
@@ -3974,6 +4108,920 @@ async function evolveCommand(targetSkillDir, options = {}) {
|
|
|
3974
4108
|
await evolve(targetSkillDir, options);
|
|
3975
4109
|
}
|
|
3976
4110
|
|
|
4111
|
+
// src/commands/organize.ts
|
|
4112
|
+
import { mkdirSync as mkdirSync7, writeFileSync as writeFileSync7 } from "fs";
|
|
4113
|
+
import { dirname as dirname8, resolve as resolvePath6 } from "path";
|
|
4114
|
+
|
|
4115
|
+
// src/organize.ts
|
|
4116
|
+
import { existsSync as existsSync17, readFileSync as readFileSync13, readdirSync as readdirSync7, realpathSync as realpathSync2, statSync as statSync9 } from "fs";
|
|
4117
|
+
import { basename as basename8, join as join21, resolve as resolvePath5 } from "path";
|
|
4118
|
+
var PRO_FEATURE_LABEL3 = "set-level organizer reports";
|
|
4119
|
+
var TIERS = /* @__PURE__ */ new Set(["resource", "custom"]);
|
|
4120
|
+
var MATURITIES = /* @__PURE__ */ new Set(["seedling", "stable", "deprecated"]);
|
|
4121
|
+
function listSubdirs2(dir) {
|
|
4122
|
+
let entries;
|
|
4123
|
+
try {
|
|
4124
|
+
entries = readdirSync7(dir, { withFileTypes: true });
|
|
4125
|
+
} catch {
|
|
4126
|
+
return [];
|
|
4127
|
+
}
|
|
4128
|
+
const out = [];
|
|
4129
|
+
for (const entry of entries) {
|
|
4130
|
+
const full = join21(dir, entry.name);
|
|
4131
|
+
if (entry.isDirectory()) {
|
|
4132
|
+
out.push(full);
|
|
4133
|
+
continue;
|
|
4134
|
+
}
|
|
4135
|
+
if (entry.isSymbolicLink()) {
|
|
4136
|
+
try {
|
|
4137
|
+
if (statSync9(full).isDirectory()) out.push(full);
|
|
4138
|
+
} catch {
|
|
4139
|
+
}
|
|
4140
|
+
}
|
|
4141
|
+
}
|
|
4142
|
+
return out;
|
|
4143
|
+
}
|
|
4144
|
+
function findSkillDirsUnderRoot2(root) {
|
|
4145
|
+
const found = /* @__PURE__ */ new Set();
|
|
4146
|
+
const topDirs = listSubdirs2(root);
|
|
4147
|
+
for (const dir of topDirs) {
|
|
4148
|
+
if (existsSync17(join21(dir, "SKILL.md"))) found.add(dir);
|
|
4149
|
+
}
|
|
4150
|
+
for (const dir of listSubdirs2(join21(root, "skills"))) {
|
|
4151
|
+
if (existsSync17(join21(dir, "SKILL.md"))) found.add(dir);
|
|
4152
|
+
}
|
|
4153
|
+
for (const topDir of topDirs) {
|
|
4154
|
+
for (const dir of listSubdirs2(join21(topDir, "skills"))) {
|
|
4155
|
+
if (existsSync17(join21(dir, "SKILL.md"))) found.add(dir);
|
|
4156
|
+
}
|
|
4157
|
+
}
|
|
4158
|
+
return Array.from(found).filter((p) => !p.includes("/.git/"));
|
|
4159
|
+
}
|
|
4160
|
+
function collectCandidates(roots) {
|
|
4161
|
+
const out = [];
|
|
4162
|
+
for (const root of roots) {
|
|
4163
|
+
const resolvedRoot = resolvePath5(expandHome(root));
|
|
4164
|
+
if (!isDir(resolvedRoot)) continue;
|
|
4165
|
+
for (const dirPath of findSkillDirsUnderRoot2(resolvedRoot)) {
|
|
4166
|
+
out.push({ dirPath, root });
|
|
4167
|
+
}
|
|
4168
|
+
}
|
|
4169
|
+
return out;
|
|
4170
|
+
}
|
|
4171
|
+
function parseYamlList(block, key) {
|
|
4172
|
+
const inlineMatch = block.match(new RegExp(`^${key}:\\s*\\[(.*?)\\]\\s*$`, "m"));
|
|
4173
|
+
if (inlineMatch) {
|
|
4174
|
+
return inlineMatch[1].split(",").map((x) => stripQuotes(x.trim())).filter(Boolean);
|
|
4175
|
+
}
|
|
4176
|
+
if (!new RegExp(`^${key}:\\s*$`, "m").test(block)) return [];
|
|
4177
|
+
const lines = block.split(/\r\n|\r|\n/);
|
|
4178
|
+
const startIdx = lines.findIndex((l) => new RegExp(`^${key}:\\s*$`).test(l));
|
|
4179
|
+
if (startIdx === -1) return [];
|
|
4180
|
+
const out = [];
|
|
4181
|
+
for (const line of lines.slice(startIdx + 1)) {
|
|
4182
|
+
const itemMatch = line.match(/^\s*-\s*(.+)$/);
|
|
4183
|
+
if (itemMatch) {
|
|
4184
|
+
out.push(stripQuotes(itemMatch[1].trim()));
|
|
4185
|
+
} else if (line.trim() === "") {
|
|
4186
|
+
continue;
|
|
4187
|
+
} else {
|
|
4188
|
+
break;
|
|
4189
|
+
}
|
|
4190
|
+
}
|
|
4191
|
+
return out;
|
|
4192
|
+
}
|
|
4193
|
+
function extractFrontmatterBlock(text) {
|
|
4194
|
+
if (!text.startsWith("---\n")) return null;
|
|
4195
|
+
const afterOpen = text.slice(4);
|
|
4196
|
+
const closeIdx = afterOpen.indexOf("\n---");
|
|
4197
|
+
if (closeIdx === -1) return null;
|
|
4198
|
+
return afterOpen.slice(0, closeIdx);
|
|
4199
|
+
}
|
|
4200
|
+
function parseCapabilityFrontmatter(text, fallbackName) {
|
|
4201
|
+
const block = extractFrontmatterBlock(text);
|
|
4202
|
+
if (block === null) {
|
|
4203
|
+
return { name: fallbackName, consumes: [], validFrontmatter: false };
|
|
4204
|
+
}
|
|
4205
|
+
const fields = {};
|
|
4206
|
+
for (const key of ["name", "tier", "domain", "provenance", "maturity"]) {
|
|
4207
|
+
const m = block.match(new RegExp(`^${key}:\\s*(.+)$`, "m"));
|
|
4208
|
+
if (m) fields[key] = stripQuotes(m[1].trim());
|
|
4209
|
+
}
|
|
4210
|
+
return {
|
|
4211
|
+
name: fields.name ?? fallbackName,
|
|
4212
|
+
tier: fields.tier,
|
|
4213
|
+
domain: fields.domain,
|
|
4214
|
+
provenance: fields.provenance,
|
|
4215
|
+
maturity: fields.maturity,
|
|
4216
|
+
consumes: parseYamlList(block, "consumes"),
|
|
4217
|
+
validFrontmatter: true
|
|
4218
|
+
};
|
|
4219
|
+
}
|
|
4220
|
+
function loadUsageSnapshot(snapshotPath) {
|
|
4221
|
+
const resolvedPath = resolvePath5(expandHome(snapshotPath));
|
|
4222
|
+
let raw;
|
|
4223
|
+
try {
|
|
4224
|
+
raw = readFileSync13(resolvedPath, "utf-8");
|
|
4225
|
+
} catch (err) {
|
|
4226
|
+
return { usage: /* @__PURE__ */ new Map(), warning: `could not read usage snapshot (${err.message}); proceeding without usage` };
|
|
4227
|
+
}
|
|
4228
|
+
let parsed;
|
|
4229
|
+
try {
|
|
4230
|
+
parsed = JSON.parse(raw);
|
|
4231
|
+
} catch (err) {
|
|
4232
|
+
return { usage: /* @__PURE__ */ new Map(), warning: `could not parse usage snapshot (${err.message}); proceeding without usage` };
|
|
4233
|
+
}
|
|
4234
|
+
const usage = /* @__PURE__ */ new Map();
|
|
4235
|
+
const outer = parsed;
|
|
4236
|
+
const report = outer && typeof outer === "object" ? outer.report : void 0;
|
|
4237
|
+
const topSkills = report && typeof report === "object" ? report.top_skills : void 0;
|
|
4238
|
+
if (Array.isArray(topSkills)) {
|
|
4239
|
+
for (const row of topSkills) {
|
|
4240
|
+
if (Array.isArray(row) && row.length === 2 && typeof row[0] === "string" && typeof row[1] === "number") {
|
|
4241
|
+
const [name, count] = row;
|
|
4242
|
+
usage.set(name, count);
|
|
4243
|
+
if (name.includes(":")) {
|
|
4244
|
+
const bare = name.split(":").pop();
|
|
4245
|
+
if (!usage.has(bare)) usage.set(bare, count);
|
|
4246
|
+
}
|
|
4247
|
+
}
|
|
4248
|
+
}
|
|
4249
|
+
}
|
|
4250
|
+
return { usage };
|
|
4251
|
+
}
|
|
4252
|
+
var WHITE = 0;
|
|
4253
|
+
var GRAY = 1;
|
|
4254
|
+
var BLACK = 2;
|
|
4255
|
+
function findCycles(adjacency) {
|
|
4256
|
+
const color = /* @__PURE__ */ new Map();
|
|
4257
|
+
for (const id of adjacency.keys()) color.set(id, WHITE);
|
|
4258
|
+
const stack = [];
|
|
4259
|
+
const cycles = [];
|
|
4260
|
+
function dfs(u) {
|
|
4261
|
+
color.set(u, GRAY);
|
|
4262
|
+
stack.push(u);
|
|
4263
|
+
for (const v of adjacency.get(u) ?? []) {
|
|
4264
|
+
if (!color.has(v)) continue;
|
|
4265
|
+
if (color.get(v) === GRAY && stack.includes(v)) {
|
|
4266
|
+
const idx = stack.indexOf(v);
|
|
4267
|
+
cycles.push([...stack.slice(idx), v]);
|
|
4268
|
+
} else if (color.get(v) === WHITE) {
|
|
4269
|
+
dfs(v);
|
|
4270
|
+
}
|
|
4271
|
+
}
|
|
4272
|
+
color.set(u, BLACK);
|
|
4273
|
+
stack.pop();
|
|
4274
|
+
}
|
|
4275
|
+
for (const id of adjacency.keys()) {
|
|
4276
|
+
if (color.get(id) === WHITE) dfs(id);
|
|
4277
|
+
}
|
|
4278
|
+
return cycles;
|
|
4279
|
+
}
|
|
4280
|
+
function buildGraph(entries, nameToIds) {
|
|
4281
|
+
const nodes = entries.map((e) => ({ id: e.id, name: e.name, tier: e.tier, domain: e.domain }));
|
|
4282
|
+
const edges = [];
|
|
4283
|
+
const dangling = [];
|
|
4284
|
+
const ambiguous = [];
|
|
4285
|
+
const incoming = new Map(entries.map((e) => [e.id, 0]));
|
|
4286
|
+
const adjacency = new Map(entries.map((e) => [e.id, []]));
|
|
4287
|
+
for (const e of entries) {
|
|
4288
|
+
for (const target of e.consumes) {
|
|
4289
|
+
const matches = nameToIds.get(target) ?? [];
|
|
4290
|
+
edges.push({ from: e.id, to: target, resolvedIds: matches });
|
|
4291
|
+
if (matches.length === 0) {
|
|
4292
|
+
dangling.push({ from: e.id, to: target });
|
|
4293
|
+
} else if (matches.length === 1) {
|
|
4294
|
+
incoming.set(matches[0], (incoming.get(matches[0]) ?? 0) + 1);
|
|
4295
|
+
adjacency.get(e.id).push(matches[0]);
|
|
4296
|
+
} else {
|
|
4297
|
+
ambiguous.push({ from: e.id, to: target, matches });
|
|
4298
|
+
}
|
|
4299
|
+
}
|
|
4300
|
+
}
|
|
4301
|
+
const orphans = entries.filter((e) => e.tier === "custom" && e.consumes.length === 0).map((e) => e.id);
|
|
4302
|
+
const byId = new Map(entries.map((e) => [e.id, e]));
|
|
4303
|
+
const hotResources = Array.from(incoming.entries()).filter(([, count]) => count > 0).map(([id, count]) => ({ id, name: byId.get(id).name, consumedBy: count })).sort((a, b) => b.consumedBy - a.consumedBy);
|
|
4304
|
+
const cycles = findCycles(adjacency);
|
|
4305
|
+
return {
|
|
4306
|
+
nodeCount: nodes.length,
|
|
4307
|
+
edgeCount: edges.length,
|
|
4308
|
+
nodes,
|
|
4309
|
+
edges,
|
|
4310
|
+
orphans,
|
|
4311
|
+
hotResources,
|
|
4312
|
+
danglingEdges: dangling,
|
|
4313
|
+
ambiguousConsumesTargets: ambiguous,
|
|
4314
|
+
cycles
|
|
4315
|
+
};
|
|
4316
|
+
}
|
|
4317
|
+
function buildRegistryEntries(candidates, warnings) {
|
|
4318
|
+
const entries = [];
|
|
4319
|
+
const seenRealPaths = /* @__PURE__ */ new Set();
|
|
4320
|
+
for (const candidate of candidates) {
|
|
4321
|
+
let realPath;
|
|
4322
|
+
try {
|
|
4323
|
+
realPath = realpathSync2(candidate.dirPath);
|
|
4324
|
+
} catch (err) {
|
|
4325
|
+
warnings.push(`could not resolve skill directory ${candidate.dirPath}: ${err.message}`);
|
|
4326
|
+
continue;
|
|
4327
|
+
}
|
|
4328
|
+
if (seenRealPaths.has(realPath)) continue;
|
|
4329
|
+
seenRealPaths.add(realPath);
|
|
4330
|
+
const skillMdPath = join21(realPath, "SKILL.md");
|
|
4331
|
+
let text;
|
|
4332
|
+
try {
|
|
4333
|
+
text = readFileSync13(skillMdPath, "utf-8");
|
|
4334
|
+
} catch (err) {
|
|
4335
|
+
warnings.push(`could not read ${skillMdPath}: ${err.message}`);
|
|
4336
|
+
continue;
|
|
4337
|
+
}
|
|
4338
|
+
const fm = parseCapabilityFrontmatter(text, basename8(realPath));
|
|
4339
|
+
const name = fm.name ?? basename8(realPath);
|
|
4340
|
+
if (fm.tier && !TIERS.has(fm.tier)) {
|
|
4341
|
+
warnings.push(`${name}: tier '${fm.tier}' not in ${JSON.stringify(Array.from(TIERS).sort())}`);
|
|
4342
|
+
}
|
|
4343
|
+
if (fm.maturity && !MATURITIES.has(fm.maturity)) {
|
|
4344
|
+
warnings.push(`${name}: maturity '${fm.maturity}' not in ${JSON.stringify(Array.from(MATURITIES).sort())}`);
|
|
4345
|
+
}
|
|
4346
|
+
entries.push({
|
|
4347
|
+
id: realPath,
|
|
4348
|
+
name,
|
|
4349
|
+
path: realPath,
|
|
4350
|
+
root: candidate.root,
|
|
4351
|
+
tier: fm.tier,
|
|
4352
|
+
domain: fm.domain,
|
|
4353
|
+
consumes: fm.consumes,
|
|
4354
|
+
provenance: fm.provenance,
|
|
4355
|
+
maturity: fm.maturity,
|
|
4356
|
+
validFrontmatter: fm.validFrontmatter
|
|
4357
|
+
});
|
|
4358
|
+
}
|
|
4359
|
+
return entries;
|
|
4360
|
+
}
|
|
4361
|
+
function joinUsageSnapshot(entries, snapshotPath, warnings) {
|
|
4362
|
+
const { usage, warning } = loadUsageSnapshot(snapshotPath);
|
|
4363
|
+
if (warning) warnings.push(warning);
|
|
4364
|
+
if (usage.size === 0) return false;
|
|
4365
|
+
for (const entry of entries) {
|
|
4366
|
+
const count = usage.get(entry.name);
|
|
4367
|
+
if (count !== void 0) entry.usage = count;
|
|
4368
|
+
}
|
|
4369
|
+
if (!entries.some((e) => e.usage !== void 0)) {
|
|
4370
|
+
warnings.push(
|
|
4371
|
+
"usage snapshot loaded but 0 skills matched by name \u2014 registry `name:` vs telemetry key mismatch; the usage column will be empty"
|
|
4372
|
+
);
|
|
4373
|
+
}
|
|
4374
|
+
return true;
|
|
4375
|
+
}
|
|
4376
|
+
function buildNameToIds(entries, warnings) {
|
|
4377
|
+
const nameToIds = /* @__PURE__ */ new Map();
|
|
4378
|
+
for (const e of entries) {
|
|
4379
|
+
const ids = nameToIds.get(e.name) ?? [];
|
|
4380
|
+
ids.push(e.id);
|
|
4381
|
+
nameToIds.set(e.name, ids);
|
|
4382
|
+
}
|
|
4383
|
+
for (const [name, ids] of nameToIds) {
|
|
4384
|
+
if (ids.length > 1) {
|
|
4385
|
+
warnings.push(`duplicate skill name "${name}" across ${ids.length} locations: ${ids.join(", ")}`);
|
|
4386
|
+
}
|
|
4387
|
+
}
|
|
4388
|
+
return nameToIds;
|
|
4389
|
+
}
|
|
4390
|
+
function findUntagged(entries) {
|
|
4391
|
+
return entries.filter((e) => !e.tier || !e.domain).map((e) => ({
|
|
4392
|
+
id: e.id,
|
|
4393
|
+
name: e.name,
|
|
4394
|
+
missing: [!e.tier ? "tier" : void 0, !e.domain ? "domain" : void 0].filter((v) => Boolean(v))
|
|
4395
|
+
}));
|
|
4396
|
+
}
|
|
4397
|
+
function countByTierAndDomain(entries) {
|
|
4398
|
+
const byTier = {};
|
|
4399
|
+
const byDomain = {};
|
|
4400
|
+
for (const e of entries) {
|
|
4401
|
+
byTier[e.tier ?? "untagged"] = (byTier[e.tier ?? "untagged"] ?? 0) + 1;
|
|
4402
|
+
byDomain[e.domain ?? "untagged"] = (byDomain[e.domain ?? "untagged"] ?? 0) + 1;
|
|
4403
|
+
}
|
|
4404
|
+
return { byTier, byDomain };
|
|
4405
|
+
}
|
|
4406
|
+
function buildOrganizeReport(config, options = {}) {
|
|
4407
|
+
const proStatus = proFeatureStatus(PRO_FEATURE_LABEL3);
|
|
4408
|
+
if (!proStatus.enabled) {
|
|
4409
|
+
throw new UserError(proStatus.notice ?? upgradeNotice(PRO_FEATURE_LABEL3));
|
|
4410
|
+
}
|
|
4411
|
+
const notices = proStatus.notice ? [proStatus.notice] : [];
|
|
4412
|
+
const warnings = [];
|
|
4413
|
+
const candidates = collectCandidates(config.skillsRoots);
|
|
4414
|
+
if (candidates.length === 0) {
|
|
4415
|
+
throw new UserError(
|
|
4416
|
+
`no SKILL.md found under any configured skills root (${config.skillsRoots.join(", ") || "(none configured)"})`
|
|
4417
|
+
);
|
|
4418
|
+
}
|
|
4419
|
+
const entries = buildRegistryEntries(candidates, warnings);
|
|
4420
|
+
const usageJoined = options.usageSnapshot ? joinUsageSnapshot(entries, options.usageSnapshot, warnings) : false;
|
|
4421
|
+
const nameToIds = buildNameToIds(entries, warnings);
|
|
4422
|
+
const untagged = findUntagged(entries);
|
|
4423
|
+
const { byTier, byDomain } = countByTierAndDomain(entries);
|
|
4424
|
+
const registry = {
|
|
4425
|
+
skillsRoots: [...config.skillsRoots],
|
|
4426
|
+
count: entries.length,
|
|
4427
|
+
byTier,
|
|
4428
|
+
byDomain,
|
|
4429
|
+
untagged,
|
|
4430
|
+
warnings,
|
|
4431
|
+
usageJoined,
|
|
4432
|
+
skills: entries
|
|
4433
|
+
};
|
|
4434
|
+
const graph = buildGraph(entries, nameToIds);
|
|
4435
|
+
return {
|
|
4436
|
+
generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
4437
|
+
registry,
|
|
4438
|
+
graph,
|
|
4439
|
+
notices
|
|
4440
|
+
};
|
|
4441
|
+
}
|
|
4442
|
+
|
|
4443
|
+
// src/commands/organize.ts
|
|
4444
|
+
function writeReport2(outPath, contents) {
|
|
4445
|
+
const resolved = resolvePath6(outPath);
|
|
4446
|
+
mkdirSync7(dirname8(resolved), { recursive: true });
|
|
4447
|
+
try {
|
|
4448
|
+
writeFileSync7(resolved, contents, { flag: "wx", mode: 384 });
|
|
4449
|
+
} catch (err) {
|
|
4450
|
+
if (err.code === "EEXIST") {
|
|
4451
|
+
throw new UserError(`output file already exists: ${resolved} \u2014 pass a different --out path`);
|
|
4452
|
+
}
|
|
4453
|
+
throw err;
|
|
4454
|
+
}
|
|
4455
|
+
return resolved;
|
|
4456
|
+
}
|
|
4457
|
+
function renderSummary(report) {
|
|
4458
|
+
const { registry, graph } = report;
|
|
4459
|
+
const lines = [];
|
|
4460
|
+
lines.push(`[skill-forge] organize: ${registry.count} skill(s) across ${registry.skillsRoots.length} root(s)`);
|
|
4461
|
+
lines.push(` by tier: ${JSON.stringify(registry.byTier)}`);
|
|
4462
|
+
lines.push(` by domain: ${JSON.stringify(registry.byDomain)}`);
|
|
4463
|
+
lines.push(` usage joined: ${registry.usageJoined}`);
|
|
4464
|
+
lines.push(` untagged (missing tier/domain): ${registry.untagged.length}`);
|
|
4465
|
+
for (const u of registry.untagged.slice(0, 30)) {
|
|
4466
|
+
lines.push(` - ${sanitizeForMarkdown(u.name)}: missing ${u.missing.join(", ")}`);
|
|
4467
|
+
}
|
|
4468
|
+
if (registry.warnings.length > 0) {
|
|
4469
|
+
lines.push(" warnings:");
|
|
4470
|
+
for (const w of registry.warnings) lines.push(` ! ${sanitizeForMarkdown(w)}`);
|
|
4471
|
+
}
|
|
4472
|
+
lines.push("");
|
|
4473
|
+
lines.push(` graph: ${graph.nodeCount} nodes, ${graph.edgeCount} edges`);
|
|
4474
|
+
if (graph.hotResources.length > 0) {
|
|
4475
|
+
lines.push(" hot resources (consumed by N customs):");
|
|
4476
|
+
for (const h of graph.hotResources) lines.push(` ${h.consumedBy}x ${sanitizeForMarkdown(h.name)}`);
|
|
4477
|
+
}
|
|
4478
|
+
if (graph.danglingEdges.length > 0) {
|
|
4479
|
+
lines.push(" dangling edges (consumes a target not in the set):");
|
|
4480
|
+
for (const d of graph.danglingEdges) lines.push(` ${sanitizeForMarkdown(d.from)} -> ${sanitizeForMarkdown(d.to)} (missing)`);
|
|
4481
|
+
}
|
|
4482
|
+
if (graph.ambiguousConsumesTargets.length > 0) {
|
|
4483
|
+
lines.push(" ambiguous consumes targets (name matched multiple skills):");
|
|
4484
|
+
for (const a of graph.ambiguousConsumesTargets) {
|
|
4485
|
+
lines.push(
|
|
4486
|
+
` ${sanitizeForMarkdown(a.from)} -> ${sanitizeForMarkdown(a.to)} (matches: ${a.matches.map(sanitizeForMarkdown).join(", ")})`
|
|
4487
|
+
);
|
|
4488
|
+
}
|
|
4489
|
+
}
|
|
4490
|
+
if (graph.cycles.length > 0) {
|
|
4491
|
+
lines.push(" cycles (consumes should be acyclic):");
|
|
4492
|
+
for (const c of graph.cycles) lines.push(` ${c.map(sanitizeForMarkdown).join(" -> ")}`);
|
|
4493
|
+
}
|
|
4494
|
+
if (graph.orphans.length > 0) {
|
|
4495
|
+
lines.push(` custom skills with no declared consumes (${graph.orphans.length}):`);
|
|
4496
|
+
for (const o of graph.orphans.slice(0, 30)) lines.push(` - ${sanitizeForMarkdown(o)}`);
|
|
4497
|
+
}
|
|
4498
|
+
for (const n of report.notices) lines.push(` [notice] ${sanitizeForMarkdown(n)}`);
|
|
4499
|
+
return lines;
|
|
4500
|
+
}
|
|
4501
|
+
async function organizeCommand(options = {}) {
|
|
4502
|
+
const config = loadConfig();
|
|
4503
|
+
const report = buildOrganizeReport(config, { usageSnapshot: options.usageSnapshot });
|
|
4504
|
+
let outPath;
|
|
4505
|
+
if (options.out) {
|
|
4506
|
+
outPath = writeReport2(options.out, JSON.stringify(report, null, 2) + "\n");
|
|
4507
|
+
}
|
|
4508
|
+
if (options.json) {
|
|
4509
|
+
for (const n of report.notices) console.error(`[skill-forge] ${n}`);
|
|
4510
|
+
console.log(JSON.stringify(report, null, 2));
|
|
4511
|
+
return;
|
|
4512
|
+
}
|
|
4513
|
+
for (const line of renderSummary(report)) console.log(line);
|
|
4514
|
+
if (outPath) console.log(` report -> ${outPath}`);
|
|
4515
|
+
}
|
|
4516
|
+
|
|
4517
|
+
// src/gate/skillsShApi.ts
|
|
4518
|
+
var SKILLS_SH_ORIGIN = "https://skills.sh";
|
|
4519
|
+
var SKILLS_SH_API_BASE = "/api/v1";
|
|
4520
|
+
var MAX_RESPONSE_BYTES = 2 * 1024 * 1024;
|
|
4521
|
+
var REQUEST_TIMEOUT_MS = 15e3;
|
|
4522
|
+
var ID_PATTERN = /^[A-Za-z0-9._/-]+$/;
|
|
4523
|
+
var TOKEN_HINT = "No VERCEL_OIDC_TOKEN found. skills.sh authenticates with a short-lived Vercel OIDC token:\n 1. Vercel dashboard -> your project -> Settings -> OIDC Federation -> enable.\n 2. npm i -g vercel && vercel link && vercel env pull (writes VERCEL_OIDC_TOKEN to .env.local)\n 3. export VERCEL_OIDC_TOKEN from .env.local into your shell before running this command\n (or set it directly: export VERCEL_OIDC_TOKEN=...) - docs: https://skills.sh/docs/api";
|
|
4524
|
+
var SkillsShError = class extends Error {
|
|
4525
|
+
exitCode;
|
|
4526
|
+
constructor(message, exitCode) {
|
|
4527
|
+
super(message);
|
|
4528
|
+
this.name = "SkillsShError";
|
|
4529
|
+
this.exitCode = exitCode;
|
|
4530
|
+
}
|
|
4531
|
+
};
|
|
4532
|
+
var RISK_ORDER = { NONE: 0, LOW: 1, MEDIUM: 2, HIGH: 3, CRITICAL: 4 };
|
|
4533
|
+
function getToken(env = process.env) {
|
|
4534
|
+
const tok = env.VERCEL_OIDC_TOKEN;
|
|
4535
|
+
return tok ? tok : void 0;
|
|
4536
|
+
}
|
|
4537
|
+
function assertValidId(id) {
|
|
4538
|
+
if (id.length === 0 || !ID_PATTERN.test(id)) {
|
|
4539
|
+
throw new SkillsShError(`invalid skills.sh id: ${JSON.stringify(id)}`, 1);
|
|
4540
|
+
}
|
|
4541
|
+
}
|
|
4542
|
+
function validatedIdSegments(id) {
|
|
4543
|
+
assertValidId(id);
|
|
4544
|
+
const segments = id.split("/");
|
|
4545
|
+
if (segments.some((s) => s === "" || s === "." || s === "..")) {
|
|
4546
|
+
throw new SkillsShError(`invalid skills.sh id: ${JSON.stringify(id)}`, 1);
|
|
4547
|
+
}
|
|
4548
|
+
return segments;
|
|
4549
|
+
}
|
|
4550
|
+
function buildUrl(pathSegments, params) {
|
|
4551
|
+
const url = new URL(SKILLS_SH_API_BASE + "/" + pathSegments.map((s) => encodeURIComponent(s)).join("/"), SKILLS_SH_ORIGIN);
|
|
4552
|
+
if (params) {
|
|
4553
|
+
const search2 = new URLSearchParams();
|
|
4554
|
+
for (const [k, v] of Object.entries(params)) search2.set(k, String(v));
|
|
4555
|
+
url.search = search2.toString();
|
|
4556
|
+
}
|
|
4557
|
+
return url;
|
|
4558
|
+
}
|
|
4559
|
+
async function readCappedText(res) {
|
|
4560
|
+
const text = await res.text();
|
|
4561
|
+
if (new TextEncoder().encode(text).length > MAX_RESPONSE_BYTES) {
|
|
4562
|
+
throw new SkillsShError("skills.sh response exceeded the 2 MB size cap", 1);
|
|
4563
|
+
}
|
|
4564
|
+
return text;
|
|
4565
|
+
}
|
|
4566
|
+
async function call(pathSegments, params, options = {}) {
|
|
4567
|
+
const env = options.env ?? process.env;
|
|
4568
|
+
const token = getToken(env);
|
|
4569
|
+
if (!token) {
|
|
4570
|
+
throw new SkillsShError(TOKEN_HINT, 3);
|
|
4571
|
+
}
|
|
4572
|
+
const url = buildUrl(pathSegments, params);
|
|
4573
|
+
const fetchImpl = options.fetchImpl ?? fetch;
|
|
4574
|
+
let res;
|
|
4575
|
+
try {
|
|
4576
|
+
res = await fetchImpl(url, {
|
|
4577
|
+
headers: { Authorization: `Bearer ${token}` },
|
|
4578
|
+
redirect: "error",
|
|
4579
|
+
signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS)
|
|
4580
|
+
});
|
|
4581
|
+
} catch (err) {
|
|
4582
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
4583
|
+
throw new SkillsShError(`skills.sh request failed: ${message}`, 1);
|
|
4584
|
+
}
|
|
4585
|
+
if (res.status === 404) {
|
|
4586
|
+
return void 0;
|
|
4587
|
+
}
|
|
4588
|
+
if (!res.ok) {
|
|
4589
|
+
if (res.status === 401) {
|
|
4590
|
+
throw new SkillsShError("skills.sh HTTP 401: unauthorized \u2014 the VERCEL_OIDC_TOKEN was rejected (expired or invalid)", 3);
|
|
4591
|
+
}
|
|
4592
|
+
let snippet = "";
|
|
4593
|
+
try {
|
|
4594
|
+
snippet = (await readCappedText(res)).slice(0, 300);
|
|
4595
|
+
} catch {
|
|
4596
|
+
}
|
|
4597
|
+
throw new SkillsShError(`skills.sh HTTP ${res.status}${snippet ? `: ${snippet}` : ""}`, 1);
|
|
4598
|
+
}
|
|
4599
|
+
const text = await readCappedText(res);
|
|
4600
|
+
let parsed;
|
|
4601
|
+
try {
|
|
4602
|
+
parsed = JSON.parse(text);
|
|
4603
|
+
} catch {
|
|
4604
|
+
throw new SkillsShError("skills.sh returned malformed JSON", 1);
|
|
4605
|
+
}
|
|
4606
|
+
return parsed;
|
|
4607
|
+
}
|
|
4608
|
+
function isRecord(v) {
|
|
4609
|
+
return typeof v === "object" && v !== null && !Array.isArray(v);
|
|
4610
|
+
}
|
|
4611
|
+
async function search(query, limit, options = {}) {
|
|
4612
|
+
const data = await call(["skills", "search"], { q: query, limit }, options);
|
|
4613
|
+
if (!isRecord(data) || !Array.isArray(data.data)) {
|
|
4614
|
+
throw new SkillsShError("skills.sh search response had an unexpected shape", 1);
|
|
4615
|
+
}
|
|
4616
|
+
return data;
|
|
4617
|
+
}
|
|
4618
|
+
async function audit(id, options = {}) {
|
|
4619
|
+
const data = await call(["skills", "audit", ...validatedIdSegments(id)], void 0, options);
|
|
4620
|
+
if (data === void 0) return void 0;
|
|
4621
|
+
if (!isRecord(data) || !Array.isArray(data.audits)) {
|
|
4622
|
+
throw new SkillsShError("skills.sh audit response had an unexpected shape", 1);
|
|
4623
|
+
}
|
|
4624
|
+
return data;
|
|
4625
|
+
}
|
|
4626
|
+
async function get(id, options = {}) {
|
|
4627
|
+
return call(["skills", ...validatedIdSegments(id)], void 0, options);
|
|
4628
|
+
}
|
|
4629
|
+
async function curated(options = {}) {
|
|
4630
|
+
return call(["skills", "curated"], void 0, options);
|
|
4631
|
+
}
|
|
4632
|
+
function auditVerdict(data) {
|
|
4633
|
+
const audits = data.audits ?? [];
|
|
4634
|
+
const statuses = new Set(audits.map((a) => a.status));
|
|
4635
|
+
let worst = "NONE";
|
|
4636
|
+
let worstRank = 0;
|
|
4637
|
+
for (const a of audits) {
|
|
4638
|
+
const rl = a.riskLevel ?? "NONE";
|
|
4639
|
+
const rank = RISK_ORDER[rl] ?? 0;
|
|
4640
|
+
if (rank > worstRank) {
|
|
4641
|
+
worstRank = rank;
|
|
4642
|
+
worst = rl;
|
|
4643
|
+
}
|
|
4644
|
+
}
|
|
4645
|
+
const verdict = statuses.has("fail") ? "fail" : statuses.has("warn") ? "warn" : audits.length > 0 ? "pass" : "none";
|
|
4646
|
+
return { verdict, risk: worst, audits };
|
|
4647
|
+
}
|
|
4648
|
+
|
|
4649
|
+
// src/commands/find.ts
|
|
4650
|
+
var INSTALL_HINT_NOTE = "Results are unvetted \u2014 `skill-forge add <id>` runs the full quarantine + safety gate; `skill-forge find --audit <id>` shows partner audit verdicts.";
|
|
4651
|
+
function isRecordWithDataArray(v) {
|
|
4652
|
+
return typeof v === "object" && v !== null && Array.isArray(v.data);
|
|
4653
|
+
}
|
|
4654
|
+
function printJson(data) {
|
|
4655
|
+
console.log(JSON.stringify(data, null, 2));
|
|
4656
|
+
}
|
|
4657
|
+
function printSearchResults(data, query) {
|
|
4658
|
+
const items = data.data;
|
|
4659
|
+
const count = data.count ?? items.length;
|
|
4660
|
+
const searchType = data.searchType ? ` (${sanitizeForMarkdown(data.searchType)})` : "";
|
|
4661
|
+
console.log(`skills.sh \u2014 ${count} matches for ${JSON.stringify(query)}${searchType}:`);
|
|
4662
|
+
for (const item of items) {
|
|
4663
|
+
const name = sanitizeForMarkdown(String(item.name ?? item.id));
|
|
4664
|
+
const id = sanitizeForMarkdown(String(item.id));
|
|
4665
|
+
const installs = item.installs ?? "?";
|
|
4666
|
+
console.log(` - ${name} [${id}] ${installs} installs [unvetted]`);
|
|
4667
|
+
console.log(` install: skill-forge add ${id}`);
|
|
4668
|
+
}
|
|
4669
|
+
console.log("");
|
|
4670
|
+
console.log(INSTALL_HINT_NOTE);
|
|
4671
|
+
}
|
|
4672
|
+
async function find(query, options = {}, deps = {}) {
|
|
4673
|
+
const modes = [query !== void 0, options.audit !== void 0, options.get !== void 0, Boolean(options.curated)];
|
|
4674
|
+
const activeCount = modes.filter(Boolean).length;
|
|
4675
|
+
if (activeCount === 0) {
|
|
4676
|
+
throw new UserError("skill-forge find: provide a search query, or exactly one of --audit <id>, --get <id>, --curated");
|
|
4677
|
+
}
|
|
4678
|
+
if (activeCount > 1) {
|
|
4679
|
+
throw new UserError("skill-forge find: exactly one mode per invocation \u2014 a search query, or exactly one of --audit <id>, --get <id>, --curated");
|
|
4680
|
+
}
|
|
4681
|
+
const callOptions = { fetchImpl: deps.fetchImpl, env: deps.env };
|
|
4682
|
+
try {
|
|
4683
|
+
if (query !== void 0) {
|
|
4684
|
+
if (query.trim().length < 2) {
|
|
4685
|
+
throw new UserError("skill-forge find: query must be at least 2 characters");
|
|
4686
|
+
}
|
|
4687
|
+
const limit = options.limit ?? 10;
|
|
4688
|
+
const data2 = await search(query, limit, callOptions);
|
|
4689
|
+
if (options.json) {
|
|
4690
|
+
printJson(data2);
|
|
4691
|
+
return;
|
|
4692
|
+
}
|
|
4693
|
+
printSearchResults(data2, query);
|
|
4694
|
+
return;
|
|
4695
|
+
}
|
|
4696
|
+
if (options.audit !== void 0) {
|
|
4697
|
+
const id = options.audit;
|
|
4698
|
+
const data2 = await audit(id, callOptions);
|
|
4699
|
+
if (!data2) {
|
|
4700
|
+
console.log(`no partner audits yet for ${sanitizeForMarkdown(id)} (generated after first install).`);
|
|
4701
|
+
return;
|
|
4702
|
+
}
|
|
4703
|
+
if (options.json) {
|
|
4704
|
+
printJson(data2);
|
|
4705
|
+
return;
|
|
4706
|
+
}
|
|
4707
|
+
const { verdict, risk, audits } = auditVerdict(data2);
|
|
4708
|
+
console.log(
|
|
4709
|
+
`skills.sh audit for ${sanitizeForMarkdown(id)}: ${verdict.toUpperCase()} (max risk ${sanitizeForMarkdown(risk)}) \u2014 ${audits.length} partner(s)`
|
|
4710
|
+
);
|
|
4711
|
+
for (const entry of audits) {
|
|
4712
|
+
const provider = sanitizeForMarkdown(String(entry.provider ?? "unknown"));
|
|
4713
|
+
const status2 = sanitizeForMarkdown(String(entry.status ?? "unknown"));
|
|
4714
|
+
const riskLevel = entry.riskLevel ? ` \xB7 ${sanitizeForMarkdown(String(entry.riskLevel))}` : "";
|
|
4715
|
+
const summary = entry.summary ? ` \u2014 ${sanitizeForMarkdown(String(entry.summary))}` : "";
|
|
4716
|
+
console.log(` - ${provider}: ${status2}${riskLevel}${summary}`);
|
|
4717
|
+
}
|
|
4718
|
+
return;
|
|
4719
|
+
}
|
|
4720
|
+
if (options.get !== void 0) {
|
|
4721
|
+
const data2 = await get(options.get, callOptions);
|
|
4722
|
+
printJson(data2);
|
|
4723
|
+
return;
|
|
4724
|
+
}
|
|
4725
|
+
const data = await curated(callOptions);
|
|
4726
|
+
if (options.json) {
|
|
4727
|
+
printJson(data);
|
|
4728
|
+
return;
|
|
4729
|
+
}
|
|
4730
|
+
if (isRecordWithDataArray(data)) {
|
|
4731
|
+
printSearchResults(data, "(curated)");
|
|
4732
|
+
return;
|
|
4733
|
+
}
|
|
4734
|
+
printJson(data);
|
|
4735
|
+
} catch (err) {
|
|
4736
|
+
if (err instanceof SkillsShError) {
|
|
4737
|
+
console.error(err.message);
|
|
4738
|
+
process.exitCode = err.exitCode;
|
|
4739
|
+
return;
|
|
4740
|
+
}
|
|
4741
|
+
throw err;
|
|
4742
|
+
}
|
|
4743
|
+
}
|
|
4744
|
+
|
|
4745
|
+
// src/commands/watch.ts
|
|
4746
|
+
import { realpathSync as realpathSync3 } from "fs";
|
|
4747
|
+
import { join as join22, resolve as resolvePath7 } from "path";
|
|
4748
|
+
var PRO_FEATURE_LABEL4 = "provenance drift watch (watch)";
|
|
4749
|
+
function isGitUrl(source) {
|
|
4750
|
+
const s = source.trim();
|
|
4751
|
+
return /^https?:\/\/\S+$/i.test(s) || /^git@[\w.-]+:\S+$/i.test(s);
|
|
4752
|
+
}
|
|
4753
|
+
function looksLikeGitRef(ref) {
|
|
4754
|
+
const r = ref.trim();
|
|
4755
|
+
return /^[0-9a-fA-F]{7,40}$/.test(r);
|
|
4756
|
+
}
|
|
4757
|
+
function firstNonEmptyLine(text) {
|
|
4758
|
+
return text.split("\n").find((l) => l.trim().length > 0)?.trim() ?? "(no output)";
|
|
4759
|
+
}
|
|
4760
|
+
async function checkGitDrift(exec, url, ref) {
|
|
4761
|
+
let result;
|
|
4762
|
+
try {
|
|
4763
|
+
result = await exec("git", ["ls-remote", "--", url, ref], { cwd: process.cwd() });
|
|
4764
|
+
} catch (err) {
|
|
4765
|
+
return { status: "unreachable", detail: `git ls-remote failed to run: ${err.message}` };
|
|
4766
|
+
}
|
|
4767
|
+
if (result.code !== 0) {
|
|
4768
|
+
return { status: "unreachable", detail: `git ls-remote exited ${result.code}: ${firstNonEmptyLine(result.stderr || result.stdout)}` };
|
|
4769
|
+
}
|
|
4770
|
+
const shas = result.stdout.split("\n").map((l) => l.trim()).filter((l) => l.length > 0).map((l) => l.split(/\s+/)[0] ?? "").filter((sha) => sha.length > 0);
|
|
4771
|
+
if (shas.length === 0) {
|
|
4772
|
+
return { status: "unreachable", detail: `no ref matching "${ref}" found on remote` };
|
|
4773
|
+
}
|
|
4774
|
+
const recorded = ref.trim().toLowerCase();
|
|
4775
|
+
if (shas.some((sha) => sha.toLowerCase() === recorded)) {
|
|
4776
|
+
return { status: "unchanged", detail: `current: ${recorded}` };
|
|
4777
|
+
}
|
|
4778
|
+
return { status: "moved", detail: `recorded ${ref}, remote now resolves to ${shas[0]}` };
|
|
4779
|
+
}
|
|
4780
|
+
async function runWatch(config, options = {}, deps = {}) {
|
|
4781
|
+
const proStatus = proFeatureStatus(PRO_FEATURE_LABEL4);
|
|
4782
|
+
if (!proStatus.enabled) {
|
|
4783
|
+
return { roots: [], rows: [], warnings: [], notices: [proStatus.notice ?? "Pro feature (watch) locked."] };
|
|
4784
|
+
}
|
|
4785
|
+
const notices = proStatus.notice ? [proStatus.notice] : [];
|
|
4786
|
+
const exec = deps.exec ?? defaultExec;
|
|
4787
|
+
const roots = [];
|
|
4788
|
+
const rows = [];
|
|
4789
|
+
const warnings = [];
|
|
4790
|
+
const seenRealPaths = /* @__PURE__ */ new Set();
|
|
4791
|
+
for (const root of config.skillsRoots) {
|
|
4792
|
+
const resolvedRoot = resolvePath7(expandHome(root));
|
|
4793
|
+
const exists = isDir(resolvedRoot);
|
|
4794
|
+
let realPath = resolvedRoot;
|
|
4795
|
+
if (exists) {
|
|
4796
|
+
try {
|
|
4797
|
+
realPath = realpathSync3(resolvedRoot);
|
|
4798
|
+
} catch {
|
|
4799
|
+
realPath = resolvedRoot;
|
|
4800
|
+
}
|
|
4801
|
+
}
|
|
4802
|
+
if (seenRealPaths.has(realPath)) continue;
|
|
4803
|
+
seenRealPaths.add(realPath);
|
|
4804
|
+
let ledger;
|
|
4805
|
+
try {
|
|
4806
|
+
ledger = readProvenanceLedger(resolvedRoot);
|
|
4807
|
+
} catch (err) {
|
|
4808
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
4809
|
+
const ledgerPath = join22(resolvedRoot, "SOURCES.md");
|
|
4810
|
+
roots.push({ root, realPath, exists, ledgerPath, ledgerExists: false });
|
|
4811
|
+
warnings.push(`${ledgerPath}: could not read ledger: ${message}`);
|
|
4812
|
+
continue;
|
|
4813
|
+
}
|
|
4814
|
+
roots.push({ root, realPath, exists, ledgerPath: ledger.ledgerPath, ledgerExists: ledger.exists });
|
|
4815
|
+
warnings.push(...ledger.warnings.map((w) => `${ledger.ledgerPath}: ${w}`));
|
|
4816
|
+
for (const entry of ledger.entries) {
|
|
4817
|
+
if (entry.verb === "REJECT") continue;
|
|
4818
|
+
const rowBase = {
|
|
4819
|
+
root,
|
|
4820
|
+
ledgerPath: ledger.ledgerPath,
|
|
4821
|
+
name: entry.name,
|
|
4822
|
+
verb: entry.verb,
|
|
4823
|
+
source: entry.source,
|
|
4824
|
+
upstreamRef: entry.upstreamRef,
|
|
4825
|
+
driftCheckCommand: entry.driftCheck
|
|
4826
|
+
};
|
|
4827
|
+
if (!(isGitUrl(entry.source) && looksLikeGitRef(entry.upstreamRef))) {
|
|
4828
|
+
rows.push({
|
|
4829
|
+
...rowBase,
|
|
4830
|
+
checkMethod: "manual",
|
|
4831
|
+
driftStatus: "not-checked",
|
|
4832
|
+
detail: "source is not a recognized git URL + ref pin \u2014 check manually"
|
|
4833
|
+
});
|
|
4834
|
+
continue;
|
|
4835
|
+
}
|
|
4836
|
+
if (options.offline) {
|
|
4837
|
+
rows.push({
|
|
4838
|
+
...rowBase,
|
|
4839
|
+
checkMethod: "git-ls-remote",
|
|
4840
|
+
driftStatus: "not-checked",
|
|
4841
|
+
detail: "--offline: network check skipped"
|
|
4842
|
+
});
|
|
4843
|
+
continue;
|
|
4844
|
+
}
|
|
4845
|
+
const { status: status2, detail } = await checkGitDrift(exec, entry.source, entry.upstreamRef);
|
|
4846
|
+
rows.push({ ...rowBase, checkMethod: "git-ls-remote", driftStatus: status2, detail });
|
|
4847
|
+
}
|
|
4848
|
+
}
|
|
4849
|
+
return { roots, rows, warnings, notices };
|
|
4850
|
+
}
|
|
4851
|
+
function printWatchTerminal(report) {
|
|
4852
|
+
console.log("[skill-forge] watch \u2014 provenance drift check");
|
|
4853
|
+
for (const notice of report.notices) console.log(`[notice] ${notice}`);
|
|
4854
|
+
if (report.roots.length === 0) {
|
|
4855
|
+
console.log("\nNo configured skills roots.");
|
|
4856
|
+
}
|
|
4857
|
+
for (const root of report.roots) {
|
|
4858
|
+
console.log(`
|
|
4859
|
+
${sanitizeForMarkdown(root.root)} (${sanitizeForMarkdown(root.realPath)})`);
|
|
4860
|
+
if (!root.exists) {
|
|
4861
|
+
console.log(" root does not exist on disk");
|
|
4862
|
+
continue;
|
|
4863
|
+
}
|
|
4864
|
+
if (!root.ledgerExists) {
|
|
4865
|
+
console.log(` no ledger at ${sanitizeForMarkdown(root.ledgerPath)}`);
|
|
4866
|
+
continue;
|
|
4867
|
+
}
|
|
4868
|
+
console.log(` ledger: ${sanitizeForMarkdown(root.ledgerPath)}`);
|
|
4869
|
+
const rowsForRoot = report.rows.filter((r) => r.root === root.root);
|
|
4870
|
+
if (rowsForRoot.length === 0) {
|
|
4871
|
+
console.log(" (no tracked, non-REJECT sources)");
|
|
4872
|
+
continue;
|
|
4873
|
+
}
|
|
4874
|
+
for (const row of rowsForRoot) {
|
|
4875
|
+
console.log(` \u2022 ${sanitizeForMarkdown(row.name)} [${sanitizeForMarkdown(row.verb)}] ref=${sanitizeForMarkdown(row.upstreamRef)}`);
|
|
4876
|
+
if (row.checkMethod === "git-ls-remote") {
|
|
4877
|
+
console.log(` drift: ${row.driftStatus} \u2014 ${sanitizeForMarkdown(row.detail)}`);
|
|
4878
|
+
} else {
|
|
4879
|
+
console.log(` manual check \u2014 stored drift-check command (data, NOT executed): ${sanitizeForMarkdown(row.driftCheckCommand)}`);
|
|
4880
|
+
}
|
|
4881
|
+
}
|
|
4882
|
+
}
|
|
4883
|
+
if (report.warnings.length > 0) {
|
|
4884
|
+
console.log("\nWarnings:");
|
|
4885
|
+
for (const w of report.warnings) console.log(` - ${sanitizeForMarkdown(w)}`);
|
|
4886
|
+
}
|
|
4887
|
+
console.log("\nCompare each stored ref to current upstream; re-run ingestion on any that moved.");
|
|
4888
|
+
}
|
|
4889
|
+
async function watchCommand(options = {}) {
|
|
4890
|
+
const report = await runWatch(loadConfig(), options);
|
|
4891
|
+
if (options.json) {
|
|
4892
|
+
console.log(JSON.stringify(report, null, 2));
|
|
4893
|
+
for (const notice of report.notices) console.error(`[skill-forge] ${notice}`);
|
|
4894
|
+
return;
|
|
4895
|
+
}
|
|
4896
|
+
printWatchTerminal(report);
|
|
4897
|
+
}
|
|
4898
|
+
|
|
4899
|
+
// src/commands/ingest.ts
|
|
4900
|
+
import { realpathSync as realpathSync4 } from "fs";
|
|
4901
|
+
import { dirname as dirname9, isAbsolute, relative, resolve as resolvePath8 } from "path";
|
|
4902
|
+
var PRO_FEATURE_LABEL5 = "the pending-ingestion queue";
|
|
4903
|
+
var VALID_STATUSES = ["ingested", "dismissed"];
|
|
4904
|
+
function requireProFeature() {
|
|
4905
|
+
const status2 = proFeatureStatus(PRO_FEATURE_LABEL5);
|
|
4906
|
+
if (!status2.enabled) {
|
|
4907
|
+
throw new UserError(
|
|
4908
|
+
status2.notice ?? "ingest is a Pro feature \u2014 set SKILL_FORGE_LICENSE or config.licenseKey to a valid license to unlock."
|
|
4909
|
+
);
|
|
4910
|
+
}
|
|
4911
|
+
if (status2.notice) console.error(`[skill-forge] ${status2.notice}`);
|
|
4912
|
+
}
|
|
4913
|
+
function pluralizeEntries(count) {
|
|
4914
|
+
return `entr${count === 1 ? "y" : "ies"}`;
|
|
4915
|
+
}
|
|
4916
|
+
function toListRow(entry) {
|
|
4917
|
+
return {
|
|
4918
|
+
id: entry.id,
|
|
4919
|
+
source: entry.source,
|
|
4920
|
+
artifactType: entry.artifactType ?? "skill",
|
|
4921
|
+
safetyVerdict: entry.gate.safetyVerdict,
|
|
4922
|
+
createdAt: entry.createdAt
|
|
4923
|
+
};
|
|
4924
|
+
}
|
|
4925
|
+
function printPendingList(pending, json) {
|
|
4926
|
+
if (json) {
|
|
4927
|
+
console.log(JSON.stringify({ entries: pending.map(toListRow) }, null, 2));
|
|
4928
|
+
return;
|
|
4929
|
+
}
|
|
4930
|
+
if (pending.length === 0) {
|
|
4931
|
+
console.log("[skill-forge] no pending queue entries.");
|
|
4932
|
+
return;
|
|
4933
|
+
}
|
|
4934
|
+
console.log(`[skill-forge] ${pending.length} pending ${pluralizeEntries(pending.length)}:`);
|
|
4935
|
+
for (const entry of pending) {
|
|
4936
|
+
const row = toListRow(entry);
|
|
4937
|
+
console.log(` ${row.id} ${row.source} (${row.artifactType}) verdict=${row.safetyVerdict} created=${row.createdAt}`);
|
|
4938
|
+
}
|
|
4939
|
+
}
|
|
4940
|
+
function realpathOrNull(p) {
|
|
4941
|
+
try {
|
|
4942
|
+
return realpathSync4(p);
|
|
4943
|
+
} catch {
|
|
4944
|
+
return null;
|
|
4945
|
+
}
|
|
4946
|
+
}
|
|
4947
|
+
function isUnderRoot(candidate, root) {
|
|
4948
|
+
const rel = relative(root, candidate);
|
|
4949
|
+
return rel === "" || !rel.startsWith("..") && !isAbsolute(rel);
|
|
4950
|
+
}
|
|
4951
|
+
function validatePendingEntries(entries, config) {
|
|
4952
|
+
const quarantineRoot = realpathOrNull(resolvePath8(config.quarantineDir));
|
|
4953
|
+
const skillsRoots = config.skillsRoots.map((r) => realpathOrNull(resolvePath8(r))).filter((r) => r !== null);
|
|
4954
|
+
const mcpTargetDirs = (config.mcpTargets ?? []).map((f) => realpathOrNull(dirname9(resolvePath8(f)))).filter((r) => r !== null);
|
|
4955
|
+
const allowedRoots = [quarantineRoot, ...skillsRoots, ...mcpTargetDirs].filter((r) => r !== null);
|
|
4956
|
+
const valid = [];
|
|
4957
|
+
const excluded = [];
|
|
4958
|
+
for (const entry of entries) {
|
|
4959
|
+
const effectivePath = entry.installedPath ?? entry.quarantinePath;
|
|
4960
|
+
if (!effectivePath) {
|
|
4961
|
+
excluded.push({ entry, reason: "no installedPath or quarantinePath recorded on the entry" });
|
|
4962
|
+
continue;
|
|
4963
|
+
}
|
|
4964
|
+
const real = realpathOrNull(resolvePath8(effectivePath));
|
|
4965
|
+
if (!real) {
|
|
4966
|
+
excluded.push({ entry, reason: `path does not exist or could not be resolved: ${effectivePath}` });
|
|
4967
|
+
continue;
|
|
4968
|
+
}
|
|
4969
|
+
if (allowedRoots.length === 0 || !allowedRoots.some((root) => isUnderRoot(real, root))) {
|
|
4970
|
+
excluded.push({ entry, reason: `path escapes every configured root (quarantineDir/skillsRoots/mcpTargets): ${effectivePath}` });
|
|
4971
|
+
continue;
|
|
4972
|
+
}
|
|
4973
|
+
valid.push(entry);
|
|
4974
|
+
}
|
|
4975
|
+
return { valid, excluded };
|
|
4976
|
+
}
|
|
4977
|
+
async function ingest(options = {}) {
|
|
4978
|
+
requireProFeature();
|
|
4979
|
+
const queue2 = readQueue();
|
|
4980
|
+
const pending = queue2.entries.filter((e) => e.status === "pending");
|
|
4981
|
+
if (options.list) {
|
|
4982
|
+
printPendingList(pending, options.json);
|
|
4983
|
+
return;
|
|
4984
|
+
}
|
|
4985
|
+
if (pending.length === 0) {
|
|
4986
|
+
console.log("[skill-forge] no pending queue entries to ingest.");
|
|
4987
|
+
return;
|
|
4988
|
+
}
|
|
4989
|
+
const config = loadConfig();
|
|
4990
|
+
const { valid, excluded } = validatePendingEntries(pending, config);
|
|
4991
|
+
if (excluded.length > 0) {
|
|
4992
|
+
console.error(`[skill-forge] excluded ${excluded.length} pending ${pluralizeEntries(excluded.length)} that failed path validation:`);
|
|
4993
|
+
for (const { entry, reason } of excluded) {
|
|
4994
|
+
console.error(` - ${entry.id} (${entry.source}): ${reason}`);
|
|
4995
|
+
}
|
|
4996
|
+
console.error(
|
|
4997
|
+
"[skill-forge] excluded entries stay in the queue \u2014 review each, then dismiss with: skill-forge queue close <id> --status dismissed"
|
|
4998
|
+
);
|
|
4999
|
+
}
|
|
5000
|
+
if (valid.length === 0) {
|
|
5001
|
+
console.log("[skill-forge] no pending entries passed path validation \u2014 nothing to hand off.");
|
|
5002
|
+
return;
|
|
5003
|
+
}
|
|
5004
|
+
console.log(`[skill-forge] ${valid.length} pending ${pluralizeEntries(valid.length)} ready for ingest:`);
|
|
5005
|
+
for (const entry of valid) {
|
|
5006
|
+
console.log(` - ${entry.id} ${entry.source} (${entry.artifactType ?? "skill"})`);
|
|
5007
|
+
}
|
|
5008
|
+
runHandoff(config, getQueuePath(), { promptAsset: "ingest-prompt.md" });
|
|
5009
|
+
}
|
|
5010
|
+
async function queueClose(id, options = {}) {
|
|
5011
|
+
requireProFeature();
|
|
5012
|
+
const status2 = options.status;
|
|
5013
|
+
if (!status2 || !VALID_STATUSES.includes(status2)) {
|
|
5014
|
+
throw new UserError(`--status must be one of: ${VALID_STATUSES.join(", ")} (got ${status2 ? JSON.stringify(status2) : "nothing"})`);
|
|
5015
|
+
}
|
|
5016
|
+
let updated;
|
|
5017
|
+
try {
|
|
5018
|
+
updated = updateQueueEntry(id, { status: status2 });
|
|
5019
|
+
} catch (err) {
|
|
5020
|
+
throw new UserError(err instanceof Error ? err.message : String(err));
|
|
5021
|
+
}
|
|
5022
|
+
console.log(`[skill-forge] queue entry ${updated.id} closed -> status: ${updated.status}`);
|
|
5023
|
+
}
|
|
5024
|
+
|
|
3977
5025
|
// src/cli.ts
|
|
3978
5026
|
var require2 = createRequire(import.meta.url);
|
|
3979
5027
|
var pkg = require2("../package.json");
|
|
@@ -3990,6 +5038,13 @@ function parseLookbackHours(value) {
|
|
|
3990
5038
|
}
|
|
3991
5039
|
return n;
|
|
3992
5040
|
}
|
|
5041
|
+
function parseLimit(value) {
|
|
5042
|
+
const n = Number(value);
|
|
5043
|
+
if (value.trim() === "" || !Number.isInteger(n) || n < 1 || n > 50) {
|
|
5044
|
+
throw new UserError(`--limit must be an integer between 1 and 50 (got "${value}")`);
|
|
5045
|
+
}
|
|
5046
|
+
return n;
|
|
5047
|
+
}
|
|
3993
5048
|
var program = new Command();
|
|
3994
5049
|
program.name("skill-forge").description("The supply-chain gate for agent skills").version(pkg.version);
|
|
3995
5050
|
program.command("add <source>").description("Quarantine-install a skill and run it through the safety gate").option("-y, --yes", "skip the interactive prompt; honor the gate verdict automatically").option("-t, --target <dir>", "skills root to promote into (defaults to config defaultTarget, then skillsRoots[0])").option("--json", "print the gate result as JSON instead of the terminal report").option("--ingest", "after a successful promote, hand off the bundled ingest prompt to a coding agent").option("--artifact <type>", 'gate as a "skill" or an "mcp" server (default: skill)', parseArtifact).option(
|
|
@@ -4001,6 +5056,14 @@ program.command("list").description("List skills currently in quarantine").actio
|
|
|
4001
5056
|
program.command("status").description("Show skill-forge configuration and quarantine status").action(status);
|
|
4002
5057
|
program.command("evolve <target-skill-dir>").description("Self-evolve a skill via SkillOpt-Sleep, re-gate the proposal, then promote/hold/reject (Pro)").option("--project <dir>", "project dir passed to SkillOpt-Sleep as --project (defaults to cwd)").option("--dry-run", "SkillOpt-Sleep dry-run: report only, nothing staged").option("--backend <name>", "SkillOpt-Sleep backend (default: mock, offline \u2014 a real backend requires --yes or interactive confirmation)").option("--lookback-hours <n>", "hours of session history for SkillOpt-Sleep to harvest", parseLookbackHours).option("-y, --yes", "skip interactive prompts; honor the re-gate verdict automatically").option("--json", "print the re-gate result as JSON instead of the terminal report").option("--force", "allow re-adopting a staging dir whose proposed content already matches the live skill").action(evolveCommand);
|
|
4003
5058
|
program.command("audit").alias("doctor").description("Doctor-style health check over the configured skill/MCP set: inventory, hygiene findings, and consolidation/refinement opportunities").option("--json", "print the audit report as JSON instead of the terminal summary (non-interactive: skips the business-profile prompt)").option("-y, --yes", "skip interactive prompts (business profile, foundation, handoff); never implies --foundation or --handoff").option("--report <file>", "write the report to this path instead of the default ~/.skill-forge/reports/audit-<timestamp>.md (refuses an existing path)").option("--foundation", "authorizes exactly one skills-root write: scaffold a business-foundation skill from the captured business profile").option("--handoff", "authorizes launching the configured agent with the bundled curation prompt against the written report").action(auditCommand);
|
|
5059
|
+
program.command("organize").description("Build the set-level capability registry + dependency graph across configured skills roots (Pro)").option("--json", "print the registry+graph report as JSON instead of the terminal summary").option("--out <file>", "write the full report JSON to this path (refuses an existing path)").option("--usage-snapshot <file>", "join per-skill usage counts from a skill-monitor snapshot JSON").action(organizeCommand);
|
|
5060
|
+
program.command("find [query]").description("Discover skills via skills.sh and check partner security audits (requires VERCEL_OIDC_TOKEN)").option("--limit <n>", "max search results (default 10)", parseLimit).option("--audit <id>", "show partner audit verdicts (Socket/Snyk/\u2026) for a skills.sh id").option("--get <id>", "show detail + file tree for a skills.sh id").option("--curated", "list skills.sh curated skills").option("--json", "print the raw API payload as JSON").action(find);
|
|
5061
|
+
program.command("watch").description("Drift check across every SOURCES.md provenance ledger: which ingested upstreams have moved (Pro)").option("--offline", "skip all network checks; list entries for manual comparison only").option("--json", "print the drift report as JSON instead of the terminal summary").action(watchCommand);
|
|
5062
|
+
program.command("ingest").description(
|
|
5063
|
+
"Hand the pending-ingestion queue to the configured coding agent for the decide/absorb pass (Pro). For a single new source, use `skill-forge add <source> --ingest` instead."
|
|
5064
|
+
).option("--list", "print pending queue entries and exit \u2014 no handoff").option("--json", "with --list: print pending entries as JSON").action(ingest);
|
|
5065
|
+
var queue = program.command("queue").description("Manage the pending-ingestion queue (Pro)");
|
|
5066
|
+
queue.command("close <id>").description("Close a queue entry after the decide pass \u2014 agents use this instead of hand-editing queue.json").requiredOption("--status <status>", 'final status: "ingested" or "dismissed"').action(queueClose);
|
|
4004
5067
|
program.command("init").description("Detect installed agents and set gate targets, default promotion target, and handoff agent").option("--defaults", "non-interactive: use all detected agents as gate targets, the first as the default target").option("--list", "print detected agent skill roots and exit \u2014 no writes").action(init);
|
|
4005
5068
|
try {
|
|
4006
5069
|
await program.parseAsync();
|