@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/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 readFileSync6,
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 readFileSync5 } from "fs";
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(readFileSync5(file, "utf-8"));
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(readFileSync6(pkgPath, "utf-8"));
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 = readFileSync6(p, "utf-8").slice(0, 2e3).toLowerCase();
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 = readFileSync6(f, "utf-8");
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 = readFileSync6(skillMd, "utf-8");
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(readFileSync6(manifestPath, "utf-8"));
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(readFileSync6(explicitConfig, "utf-8"));
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(readFileSync6(pkgPath, "utf-8"));
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 readFileSync7, readdirSync as readdirSync4 } from "fs";
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 = readFileSync7(skillMdPath, "utf-8");
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 readFileSync8 } from "fs";
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(readFileSync8(path, "utf-8"));
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 readFileSync9, writeFileSync as writeFileSync4 } from "fs";
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 = readFileSync9(path, "utf-8");
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(queue) {
2038
- mkdirSync5(getHomeDir(), { recursive: true });
2039
- writeFileSync4(getQueuePath(), JSON.stringify(queue, null, 2) + "\n", "utf-8");
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 queue = readQueue();
2043
- const existingIds = new Set(queue.entries.map((e) => e.id));
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
- queue.entries.push(full);
2050
- writeQueue(queue);
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 readFileSync10,
2448
+ readFileSync as readFileSync11,
2380
2449
  readdirSync as readdirSync6,
2381
2450
  realpathSync,
2382
- renameSync as renameSync2,
2383
- rmSync as rmSync3,
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 = readFileSync10(skillMdPath, "utf-8");
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
- readFileSync10(targetPath, "utf-8");
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 = readFileSync10(targetPath, "utf-8");
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 find(x) {
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 = find(a);
2689
- const rb = find(b);
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 = find(pair.aPath);
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
- renameSync2(tempDir, destDir);
3161
+ renameSync3(tempDir, destDir);
3028
3162
  } catch (err) {
3029
- rmSync3(tempDir, { recursive: true, force: true });
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 readFileSync11, rmSync as rmSync4 } from "fs";
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
- rmSync4(tempDir, { recursive: true, force: true });
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
- rmSync4(stagingDir, { recursive: true, force: true });
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 = readFileSync11(manifestPath, "utf-8");
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 readFileSync11(a).equals(readFileSync11(b));
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(readFileSync11(path)).digest("hex");
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();