@rhize/skill-forge 0.5.0 → 0.6.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
@@ -6,7 +6,7 @@ import { createRequire } from "module";
6
6
 
7
7
  // src/commands/add.ts
8
8
  import { createInterface as createInterface2 } from "readline/promises";
9
- import { join as join14 } from "path";
9
+ import { join as join15 } from "path";
10
10
 
11
11
  // src/config.ts
12
12
  import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
@@ -406,24 +406,204 @@ function appendProvenance(entry, skillsRoot) {
406
406
  }
407
407
 
408
408
  // src/commands/gatePipeline.ts
409
- import { readdirSync as readdirSync6 } from "fs";
410
- import { join as join10 } from "path";
409
+ import { readdirSync as readdirSync7 } from "fs";
410
+ import { join as join11 } from "path";
411
411
 
412
412
  // src/gate/profile.ts
413
413
  import { execFileSync } from "child_process";
414
414
  import {
415
415
  closeSync,
416
- existsSync as existsSync7,
416
+ existsSync as existsSync8,
417
417
  mkdtempSync,
418
418
  openSync,
419
- readFileSync as readFileSync4,
420
- readdirSync as readdirSync2,
419
+ readFileSync as readFileSync5,
420
+ readdirSync as readdirSync3,
421
421
  readSync,
422
422
  rmSync as rmSync2,
423
- statSync as statSync3
423
+ statSync as statSync4
424
424
  } from "fs";
425
425
  import { tmpdir } from "os";
426
- import { basename as basename3, dirname as dirname2, extname, join as join6, resolve as resolve2 } from "path";
426
+ import { basename as basename3, dirname as dirname2, extname as extname2, join as join7, resolve as resolve2 } from "path";
427
+
428
+ // src/gate/mcpCapabilities.ts
429
+ import { existsSync as existsSync7, readFileSync as readFileSync4, readdirSync as readdirSync2, statSync as statSync3 } from "fs";
430
+ import { extname, join as join6 } from "path";
431
+ var SKIP_DIRS2 = /* @__PURE__ */ new Set([".git", "node_modules", "dist", "build", "__pycache__", ".venv", "venv"]);
432
+ var SOURCE_EXT = /* @__PURE__ */ new Set([".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs"]);
433
+ var MAX_FILE_BYTES = 2 * 1024 * 1024;
434
+ var MAX_ITEMS_PER_CATEGORY = 200;
435
+ function* walkSourceFiles(dir) {
436
+ let entries;
437
+ try {
438
+ entries = readdirSync2(dir, { withFileTypes: true });
439
+ } catch {
440
+ return;
441
+ }
442
+ for (const entry of entries) {
443
+ const full = join6(dir, entry.name);
444
+ if (entry.isDirectory()) {
445
+ if (SKIP_DIRS2.has(entry.name)) continue;
446
+ yield* walkSourceFiles(full);
447
+ } else if (entry.isFile() && SOURCE_EXT.has(extname(entry.name))) {
448
+ yield full;
449
+ }
450
+ }
451
+ }
452
+ function readTextFileSafe(file) {
453
+ try {
454
+ if (statSync3(file).size > MAX_FILE_BYTES) return void 0;
455
+ return readFileSync4(file, "utf-8");
456
+ } catch {
457
+ return void 0;
458
+ }
459
+ }
460
+ function readJsonSafe(file) {
461
+ try {
462
+ return JSON.parse(readFileSync4(file, "utf-8"));
463
+ } catch {
464
+ return void 0;
465
+ }
466
+ }
467
+ var MCP_SDK_DEP = "@modelcontextprotocol/sdk";
468
+ var MCP_KEYWORD = "mcp-server";
469
+ var MCP_BIN_PREFIX = /^mcp-/i;
470
+ function hasPackageJsonMcpMarkers(pkg2) {
471
+ for (const section of ["dependencies", "devDependencies", "peerDependencies"]) {
472
+ const deps = pkg2[section];
473
+ if (deps && typeof deps === "object" && MCP_SDK_DEP in deps) return true;
474
+ }
475
+ const keywords = pkg2.keywords;
476
+ if (Array.isArray(keywords) && keywords.some((k) => k === MCP_KEYWORD)) return true;
477
+ const bin = pkg2.bin;
478
+ if (typeof bin === "string") {
479
+ if (MCP_BIN_PREFIX.test(bin.split("/").pop() ?? bin)) return true;
480
+ } else if (bin && typeof bin === "object") {
481
+ if (Object.keys(bin).some((k) => MCP_BIN_PREFIX.test(k))) return true;
482
+ }
483
+ if (pkg2.mcp && typeof pkg2.mcp === "object") return true;
484
+ return false;
485
+ }
486
+ function collectDeclaredFromManifest(node, buckets) {
487
+ if (Array.isArray(node)) {
488
+ for (const item of node) collectDeclaredFromManifest(item, buckets);
489
+ return;
490
+ }
491
+ if (!node || typeof node !== "object") return;
492
+ const obj = node;
493
+ for (const [key, value] of Object.entries(obj)) {
494
+ if ((key === "tools" || key === "resources" || key === "prompts") && Array.isArray(value)) {
495
+ const target = buckets[key];
496
+ for (const item of value) {
497
+ if (typeof item === "string") {
498
+ target.add(item);
499
+ } else if (item && typeof item === "object") {
500
+ const name = item.name ?? item.uri;
501
+ if (typeof name === "string") target.add(name);
502
+ }
503
+ }
504
+ } else {
505
+ collectDeclaredFromManifest(value, buckets);
506
+ }
507
+ }
508
+ }
509
+ var REGISTRATION_CALL_RE = /\bserver\.(tool|registerTool|resource|prompt)\(\s*['"]([^'"]+)['"]/g;
510
+ var REGISTRATION_METHOD_CATEGORY = {
511
+ tool: "tools",
512
+ registerTool: "tools",
513
+ resource: "resources",
514
+ prompt: "prompts"
515
+ };
516
+ function captureRegistrations(text, buckets) {
517
+ REGISTRATION_CALL_RE.lastIndex = 0;
518
+ let m;
519
+ while ((m = REGISTRATION_CALL_RE.exec(text)) !== null) {
520
+ buckets[REGISTRATION_METHOD_CATEGORY[m[1]]].add(m[2]);
521
+ }
522
+ }
523
+ var LIST_HANDLER_SCHEMAS = {
524
+ ListToolsRequestSchema: "tools",
525
+ ListResourcesRequestSchema: "resources",
526
+ ListPromptsRequestSchema: "prompts"
527
+ };
528
+ var SET_REQUEST_HANDLER_RE = /setRequestHandler\(\s*(ListToolsRequestSchema|ListResourcesRequestSchema|ListPromptsRequestSchema)\s*,/g;
529
+ var NAME_OR_URI_FIELD_RE = /\b(?:name|uri)\s*:\s*['"]([^'"]+)['"]/g;
530
+ function extractBalancedCall(text, openParenIndex) {
531
+ let depth = 0;
532
+ for (let i = openParenIndex; i < text.length; i++) {
533
+ const ch = text[i];
534
+ if (ch === "(") depth++;
535
+ else if (ch === ")") {
536
+ depth--;
537
+ if (depth === 0) return text.slice(openParenIndex, i + 1);
538
+ }
539
+ }
540
+ return text.slice(openParenIndex, Math.min(text.length, openParenIndex + 2e4));
541
+ }
542
+ function extractFromListHandlers(text, buckets) {
543
+ SET_REQUEST_HANDLER_RE.lastIndex = 0;
544
+ let m;
545
+ while ((m = SET_REQUEST_HANDLER_RE.exec(text)) !== null) {
546
+ const openParenIdx = text.indexOf("(", m.index);
547
+ if (openParenIdx === -1) continue;
548
+ const body = extractBalancedCall(text, openParenIdx);
549
+ const target = buckets[LIST_HANDLER_SCHEMAS[m[1]]];
550
+ NAME_OR_URI_FIELD_RE.lastIndex = 0;
551
+ let fieldMatch;
552
+ while ((fieldMatch = NAME_OR_URI_FIELD_RE.exec(body)) !== null) {
553
+ target.add(fieldMatch[1]);
554
+ }
555
+ }
556
+ }
557
+ var STDIO_TRANSPORT_RE = /StdioServerTransport/;
558
+ var HTTP_TRANSPORT_RE = /StreamableHTTPServerTransport|SSEServerTransport|WebSocketServerTransport/;
559
+ var MCP_SDK_IMPORT_RE = /@modelcontextprotocol\/sdk/;
560
+ function extractMcpCapabilities(packageDir) {
561
+ const buckets = { tools: /* @__PURE__ */ new Set(), resources: /* @__PURE__ */ new Set(), prompts: /* @__PURE__ */ new Set() };
562
+ let hasPackageJsonMarker = false;
563
+ const pkgPath = join6(packageDir, "package.json");
564
+ if (existsSync7(pkgPath)) {
565
+ const pkg2 = readJsonSafe(pkgPath);
566
+ if (pkg2 && typeof pkg2 === "object" && !Array.isArray(pkg2)) {
567
+ hasPackageJsonMarker = hasPackageJsonMcpMarkers(pkg2);
568
+ }
569
+ }
570
+ for (const manifestName of [".mcp.json", "mcp.json"]) {
571
+ const manifestPath = join6(packageDir, manifestName);
572
+ if (!existsSync7(manifestPath)) continue;
573
+ const parsed = readJsonSafe(manifestPath);
574
+ if (parsed !== void 0) {
575
+ collectDeclaredFromManifest(parsed, buckets);
576
+ }
577
+ }
578
+ let sawStdioTransport = false;
579
+ let sawHttpTransport = false;
580
+ let sawMcpSdkImport = false;
581
+ for (const file of walkSourceFiles(packageDir)) {
582
+ const text = readTextFileSafe(file);
583
+ if (text === void 0) continue;
584
+ captureRegistrations(text, buckets);
585
+ extractFromListHandlers(text, buckets);
586
+ if (STDIO_TRANSPORT_RE.test(text)) sawStdioTransport = true;
587
+ if (HTTP_TRANSPORT_RE.test(text)) sawHttpTransport = true;
588
+ if (MCP_SDK_IMPORT_RE.test(text)) sawMcpSdkImport = true;
589
+ }
590
+ const transport = sawStdioTransport ? "stdio" : sawHttpTransport ? "http" : sawMcpSdkImport ? "unknown" : void 0;
591
+ const sortCap = (set) => Array.from(set).sort().slice(0, MAX_ITEMS_PER_CATEGORY);
592
+ const tools = sortCap(buckets.tools);
593
+ const resources = sortCap(buckets.resources);
594
+ const prompts = sortCap(buckets.prompts);
595
+ const hasExplicitSignal = tools.length > 0 || resources.length > 0 || prompts.length > 0;
596
+ const declaredConfidence = hasExplicitSignal ? "high" : hasPackageJsonMarker ? "partial" : "none";
597
+ return {
598
+ tools,
599
+ resources,
600
+ prompts,
601
+ transport,
602
+ declaredConfidence
603
+ };
604
+ }
605
+
606
+ // src/gate/profile.ts
427
607
  var SPDX_HINTS = {
428
608
  mit: "MIT (permissive)",
429
609
  apache: "Apache-2.0 (permissive)",
@@ -465,10 +645,10 @@ var PYTHON_STDLIB = /* @__PURE__ */ new Set([
465
645
  ]);
466
646
  function profileSkill(inputPath) {
467
647
  const p = resolve2(expandHome(inputPath));
468
- if (!existsSync7(p)) {
648
+ if (!existsSync8(p)) {
469
649
  throw new Error(`path does not exist: ${p}`);
470
650
  }
471
- const stat = statSync3(p);
651
+ const stat = statSync4(p);
472
652
  const kind = stat.isDirectory() ? classifyInput(p) : "skill";
473
653
  if (kind === "plugin") {
474
654
  return profilePlugin(p);
@@ -479,18 +659,18 @@ function profileSkill(inputPath) {
479
659
  return profileSkillDir(p);
480
660
  }
481
661
  function classifyInput(p) {
482
- if (existsSync7(join6(p, ".claude-plugin", "plugin.json"))) return "plugin";
662
+ if (existsSync8(join7(p, ".claude-plugin", "plugin.json"))) return "plugin";
483
663
  if (detectArtifactType(p) === "mcp") return "mcp";
484
664
  return "skill";
485
665
  }
486
- var MCP_SDK_DEP = "@modelcontextprotocol/sdk";
487
- var MCP_KEYWORD = "mcp-server";
488
- var MCP_BIN_PREFIX = /^mcp-/i;
666
+ var MCP_SDK_DEP2 = "@modelcontextprotocol/sdk";
667
+ var MCP_KEYWORD2 = "mcp-server";
668
+ var MCP_BIN_PREFIX2 = /^mcp-/i;
489
669
  function readPackageJsonSafe(root) {
490
- const pkgPath = join6(root, "package.json");
491
- if (!existsSync7(pkgPath)) return void 0;
670
+ const pkgPath = join7(root, "package.json");
671
+ if (!existsSync8(pkgPath)) return void 0;
492
672
  try {
493
- return JSON.parse(readFileSync4(pkgPath, "utf-8"));
673
+ return JSON.parse(readFileSync5(pkgPath, "utf-8"));
494
674
  } catch {
495
675
  return void 0;
496
676
  }
@@ -498,20 +678,20 @@ function readPackageJsonSafe(root) {
498
678
  function hasMcpPackageMarkers(pkg2) {
499
679
  for (const section of ["dependencies", "devDependencies", "peerDependencies"]) {
500
680
  const deps = pkg2[section];
501
- if (deps && typeof deps === "object" && MCP_SDK_DEP in deps) return true;
681
+ if (deps && typeof deps === "object" && MCP_SDK_DEP2 in deps) return true;
502
682
  }
503
683
  const keywords = pkg2.keywords;
504
- if (Array.isArray(keywords) && keywords.some((k) => k === MCP_KEYWORD)) return true;
684
+ if (Array.isArray(keywords) && keywords.some((k) => k === MCP_KEYWORD2)) return true;
505
685
  const bin = pkg2.bin;
506
686
  if (typeof bin === "string") {
507
- if (MCP_BIN_PREFIX.test(basename3(bin))) return true;
687
+ if (MCP_BIN_PREFIX2.test(basename3(bin))) return true;
508
688
  } else if (bin && typeof bin === "object") {
509
- if (Object.keys(bin).some((k) => MCP_BIN_PREFIX.test(k))) return true;
689
+ if (Object.keys(bin).some((k) => MCP_BIN_PREFIX2.test(k))) return true;
510
690
  }
511
691
  return false;
512
692
  }
513
693
  function detectArtifactType(root) {
514
- if (existsSync7(join6(root, ".mcp.json")) || existsSync7(join6(root, "mcp.json"))) return "mcp";
694
+ if (existsSync8(join7(root, ".mcp.json")) || existsSync8(join7(root, "mcp.json"))) return "mcp";
515
695
  const pkg2 = readPackageJsonSafe(root);
516
696
  if (pkg2 && hasMcpPackageMarkers(pkg2)) return "mcp";
517
697
  return "skill";
@@ -519,19 +699,19 @@ function detectArtifactType(root) {
519
699
  function resolveSkillMd(inputPath) {
520
700
  let currentPath = inputPath;
521
701
  let tmpDir;
522
- const initialStat = statSync3(currentPath);
523
- if (extname(currentPath) === ".skill" || initialStat.isFile() && isZipFile(currentPath)) {
524
- tmpDir = mkdtempSync(join6(tmpdir(), "forge_"));
702
+ const initialStat = statSync4(currentPath);
703
+ if (extname2(currentPath) === ".skill" || initialStat.isFile() && isZipFile(currentPath)) {
704
+ tmpDir = mkdtempSync(join7(tmpdir(), "forge_"));
525
705
  extractZip(currentPath, tmpDir);
526
706
  currentPath = tmpDir;
527
707
  }
528
- const stat = statSync3(currentPath);
708
+ const stat = statSync4(currentPath);
529
709
  if (stat.isFile() && basename3(currentPath) === "SKILL.md") {
530
710
  return { skillMd: currentPath, root: dirname2(currentPath), tmpDir };
531
711
  }
532
712
  if (stat.isDirectory()) {
533
- const direct = join6(currentPath, "SKILL.md");
534
- if (existsSync7(direct)) {
713
+ const direct = join7(currentPath, "SKILL.md");
714
+ if (existsSync8(direct)) {
535
715
  return { skillMd: direct, root: currentPath, tmpDir };
536
716
  }
537
717
  const matches = findSkillMdOneLevelDown(currentPath);
@@ -554,18 +734,18 @@ function cleanupTmp(tmpDir) {
554
734
  }
555
735
  function findSkillMdOneLevelDown(dir) {
556
736
  const matches = /* @__PURE__ */ new Set();
557
- for (const entry of readdirSync2(dir, { withFileTypes: true })) {
737
+ for (const entry of readdirSync3(dir, { withFileTypes: true })) {
558
738
  if (entry.isDirectory()) {
559
- const candidate = join6(dir, entry.name, "SKILL.md");
560
- if (existsSync7(candidate)) matches.add(candidate);
739
+ const candidate = join7(dir, entry.name, "SKILL.md");
740
+ if (existsSync8(candidate)) matches.add(candidate);
561
741
  }
562
742
  }
563
- const skillsDir = join6(dir, "skills");
743
+ const skillsDir = join7(dir, "skills");
564
744
  if (isDir(skillsDir)) {
565
- for (const entry of readdirSync2(skillsDir, { withFileTypes: true })) {
745
+ for (const entry of readdirSync3(skillsDir, { withFileTypes: true })) {
566
746
  if (entry.isDirectory()) {
567
- const candidate = join6(skillsDir, entry.name, "SKILL.md");
568
- if (existsSync7(candidate)) matches.add(candidate);
747
+ const candidate = join7(skillsDir, entry.name, "SKILL.md");
748
+ if (existsSync8(candidate)) matches.add(candidate);
569
749
  }
570
750
  }
571
751
  }
@@ -648,9 +828,9 @@ function detectLicense(root, fm) {
648
828
  if (fm.license) return fm.license;
649
829
  const names = ["LICENSE", "LICENSE.md", "LICENSE.txt", "COPYING", "license"];
650
830
  for (const name of names) {
651
- const p = join6(root, name);
652
- if (existsSync7(p) && statSync3(p).isFile()) {
653
- const head = readFileSync4(p, "utf-8").slice(0, 2e3).toLowerCase();
831
+ const p = join7(root, name);
832
+ if (existsSync8(p) && statSync4(p).isFile()) {
833
+ const head = readFileSync5(p, "utf-8").slice(0, 2e3).toLowerCase();
654
834
  for (const [hint, label] of Object.entries(SPDX_HINTS)) {
655
835
  if (head.includes(hint)) return label;
656
836
  }
@@ -661,8 +841,8 @@ function detectLicense(root, fm) {
661
841
  }
662
842
  function walkFiles(dir) {
663
843
  const out = [];
664
- for (const entry of readdirSync2(dir, { withFileTypes: true })) {
665
- const full = join6(dir, entry.name);
844
+ for (const entry of readdirSync3(dir, { withFileTypes: true })) {
845
+ const full = join7(dir, entry.name);
666
846
  if (entry.isDirectory()) {
667
847
  out.push(...walkFiles(full));
668
848
  } else if (entry.isFile()) {
@@ -674,7 +854,7 @@ function walkFiles(dir) {
674
854
  function countResources(root) {
675
855
  const resources = {};
676
856
  for (const d of RESOURCE_DIRS) {
677
- const dp = join6(root, d);
857
+ const dp = join7(root, d);
678
858
  if (isDir(dp)) {
679
859
  resources[d] = walkFiles(dp).length;
680
860
  }
@@ -685,11 +865,11 @@ function scanMcpAndImports(text, root) {
685
865
  const mcpRefRe = /mcp__[a-zA-Z0-9_]+/g;
686
866
  const mcpRefs = new Set(text.match(mcpRefRe) ?? []);
687
867
  const imports = /* @__PURE__ */ new Set();
688
- const scriptsDir = join6(root, "scripts");
868
+ const scriptsDir = join7(root, "scripts");
689
869
  if (isDir(scriptsDir)) {
690
870
  for (const f of walkFiles(scriptsDir)) {
691
871
  if (!f.endsWith(".py")) continue;
692
- const ft = readFileSync4(f, "utf-8");
872
+ const ft = readFileSync5(f, "utf-8");
693
873
  for (const m2 of ft.match(mcpRefRe) ?? []) mcpRefs.add(m2);
694
874
  const importRe = /^\s*(?:import|from)\s+([a-zA-Z0-9_]+)/gm;
695
875
  let m;
@@ -706,7 +886,7 @@ function scanMcpAndImports(text, root) {
706
886
  function profileSkillDir(inputPath) {
707
887
  const { skillMd, root, tmpDir } = resolveSkillMd(inputPath);
708
888
  try {
709
- const text = readFileSync4(skillMd, "utf-8");
889
+ const text = readFileSync5(skillMd, "utf-8");
710
890
  const fm = parseFrontmatter(text);
711
891
  const bodyLines = (text.match(/\n/g)?.length ?? 0) + 1;
712
892
  const headers = Array.from(text.matchAll(/^#{1,3}\s+(.+)$/gm)).map((m) => m[1]);
@@ -734,20 +914,20 @@ function profileSkillDir(inputPath) {
734
914
  }
735
915
  }
736
916
  function profilePlugin(p) {
737
- const manifestPath = join6(p, ".claude-plugin", "plugin.json");
917
+ const manifestPath = join7(p, ".claude-plugin", "plugin.json");
738
918
  let manifest;
739
919
  try {
740
- manifest = JSON.parse(readFileSync4(manifestPath, "utf-8"));
920
+ manifest = JSON.parse(readFileSync5(manifestPath, "utf-8"));
741
921
  } catch (err) {
742
922
  throw new Error(`could not parse plugin.json under ${p}: ${err.message}`);
743
923
  }
744
- const skillsDir = join6(p, "skills");
924
+ const skillsDir = join7(p, "skills");
745
925
  let skills = [];
746
926
  if (isDir(skillsDir)) {
747
- skills = readdirSync2(skillsDir, { withFileTypes: true }).filter((d) => d.isDirectory() && existsSync7(join6(skillsDir, d.name, "SKILL.md"))).map((d) => d.name).sort();
927
+ skills = readdirSync3(skillsDir, { withFileTypes: true }).filter((d) => d.isDirectory() && existsSync8(join7(skillsDir, d.name, "SKILL.md"))).map((d) => d.name).sort();
748
928
  }
749
- const commandsDir = join6(p, "commands");
750
- const commands = isDir(commandsDir) ? readdirSync2(commandsDir).filter((f) => f.endsWith(".md")).length : 0;
929
+ const commandsDir = join7(p, "commands");
930
+ const commands = isDir(commandsDir) ? readdirSync3(commandsDir).filter((f) => f.endsWith(".md")).length : 0;
751
931
  const mc = manifest.mcpServers ?? manifest.mcp_servers ?? {};
752
932
  const mcpServers = typeof mc === "object" && mc !== null ? Object.keys(mc).sort() : [];
753
933
  return {
@@ -764,11 +944,11 @@ function profilePlugin(p) {
764
944
  };
765
945
  }
766
946
  function profileMcp(p) {
767
- const explicitConfig = existsSync7(join6(p, ".mcp.json")) ? join6(p, ".mcp.json") : existsSync7(join6(p, "mcp.json")) ? join6(p, "mcp.json") : void 0;
947
+ const explicitConfig = existsSync8(join7(p, ".mcp.json")) ? join7(p, ".mcp.json") : existsSync8(join7(p, "mcp.json")) ? join7(p, "mcp.json") : void 0;
768
948
  if (explicitConfig) {
769
949
  let cfg;
770
950
  try {
771
- cfg = JSON.parse(readFileSync4(explicitConfig, "utf-8"));
951
+ cfg = JSON.parse(readFileSync5(explicitConfig, "utf-8"));
772
952
  } catch (err) {
773
953
  throw new Error(`could not parse MCP config ${explicitConfig}: ${err.message}`);
774
954
  }
@@ -780,13 +960,14 @@ function profileMcp(p) {
780
960
  config: explicitConfig,
781
961
  servers: names,
782
962
  serverCount: names.length,
783
- artifactType: "mcp"
963
+ artifactType: "mcp",
964
+ capabilities: extractMcpCapabilities(p)
784
965
  };
785
966
  }
786
- const pkgPath = join6(p, "package.json");
967
+ const pkgPath = join7(p, "package.json");
787
968
  let pkg2;
788
969
  try {
789
- pkg2 = JSON.parse(readFileSync4(pkgPath, "utf-8"));
970
+ pkg2 = JSON.parse(readFileSync5(pkgPath, "utf-8"));
790
971
  } catch (err) {
791
972
  throw new Error(`could not parse package.json ${pkgPath}: ${err.message}`);
792
973
  }
@@ -797,14 +978,15 @@ function profileMcp(p) {
797
978
  config: pkgPath,
798
979
  servers: [name],
799
980
  serverCount: 1,
800
- artifactType: "mcp"
981
+ artifactType: "mcp",
982
+ capabilities: extractMcpCapabilities(p)
801
983
  };
802
984
  }
803
985
 
804
986
  // src/gate/safety.ts
805
- import { readFileSync as readFileSync5, readdirSync as readdirSync3, statSync as statSync4 } from "fs";
987
+ import { readFileSync as readFileSync6, readdirSync as readdirSync4, statSync as statSync5 } from "fs";
806
988
  import { execFile } from "child_process";
807
- import { delimiter, extname as extname2, join as join7 } from "path";
989
+ import { delimiter, extname as extname3, join as join8 } from "path";
808
990
  import { promisify } from "util";
809
991
  var execFileAsync = promisify(execFile);
810
992
  var BLOCK_SEVERITIES_BY_STRICTNESS = {
@@ -874,7 +1056,7 @@ var DENY_RULES = [
874
1056
  message: "invokes sudo \u2014 review for unnecessary privilege escalation"
875
1057
  }
876
1058
  ];
877
- var SKIP_DIRS2 = /* @__PURE__ */ new Set([".git", "node_modules", "dist", "build", "__pycache__", ".venv", "venv"]);
1059
+ var SKIP_DIRS3 = /* @__PURE__ */ new Set([".git", "node_modules", "dist", "build", "__pycache__", ".venv", "venv"]);
878
1060
  var BINARY_EXT = /* @__PURE__ */ new Set([
879
1061
  ".png",
880
1062
  ".jpg",
@@ -896,18 +1078,18 @@ var BINARY_EXT = /* @__PURE__ */ new Set([
896
1078
  ".bin",
897
1079
  ".exe"
898
1080
  ]);
899
- var MAX_FILE_BYTES = 2 * 1024 * 1024;
1081
+ var MAX_FILE_BYTES2 = 2 * 1024 * 1024;
900
1082
  function* walk(dir) {
901
1083
  let entries;
902
1084
  try {
903
- entries = readdirSync3(dir, { withFileTypes: true });
1085
+ entries = readdirSync4(dir, { withFileTypes: true });
904
1086
  } catch {
905
1087
  return;
906
1088
  }
907
1089
  for (const entry of entries) {
908
- const full = join7(dir, entry.name);
1090
+ const full = join8(dir, entry.name);
909
1091
  if (entry.isDirectory()) {
910
- if (SKIP_DIRS2.has(entry.name)) continue;
1092
+ if (SKIP_DIRS3.has(entry.name)) continue;
911
1093
  yield* walk(full);
912
1094
  } else if (entry.isFile()) {
913
1095
  yield full;
@@ -915,10 +1097,10 @@ function* walk(dir) {
915
1097
  }
916
1098
  }
917
1099
  function readTextFile(file) {
918
- if (BINARY_EXT.has(extname2(file).toLowerCase())) return void 0;
1100
+ if (BINARY_EXT.has(extname3(file).toLowerCase())) return void 0;
919
1101
  try {
920
- if (statSync4(file).size > MAX_FILE_BYTES) return void 0;
921
- const content = readFileSync5(file, "utf-8");
1102
+ if (statSync5(file).size > MAX_FILE_BYTES2) return void 0;
1103
+ const content = readFileSync6(file, "utf-8");
922
1104
  if (content.slice(0, 512).includes("\0")) return void 0;
923
1105
  return content;
924
1106
  } catch {
@@ -929,7 +1111,7 @@ function scanStaticRules(targetPath) {
929
1111
  const findings = [];
930
1112
  let root;
931
1113
  try {
932
- root = statSync4(targetPath);
1114
+ root = statSync5(targetPath);
933
1115
  } catch {
934
1116
  return findings;
935
1117
  }
@@ -959,9 +1141,9 @@ function which(cmd) {
959
1141
  const exts = process.platform === "win32" ? (process.env.PATHEXT ?? ".EXE;.CMD;.BAT").split(";") : [""];
960
1142
  for (const dir of dirs) {
961
1143
  for (const ext of exts) {
962
- const candidate = join7(dir, cmd + ext);
1144
+ const candidate = join8(dir, cmd + ext);
963
1145
  try {
964
- if (statSync4(candidate).isFile()) return candidate;
1146
+ if (statSync5(candidate).isFile()) return candidate;
965
1147
  } catch {
966
1148
  }
967
1149
  }
@@ -1005,8 +1187,8 @@ async function scanSafety(targetPath, strictness = DEFAULT_STRICTNESS) {
1005
1187
  }
1006
1188
 
1007
1189
  // src/gate/overlap.ts
1008
- import { existsSync as existsSync8, readFileSync as readFileSync6, readdirSync as readdirSync4 } from "fs";
1009
- import { basename as basename4, dirname as dirname3, join as join8, resolve as resolve3 } from "path";
1190
+ import { existsSync as existsSync9, readFileSync as readFileSync7, readdirSync as readdirSync5 } from "fs";
1191
+ import { basename as basename4, dirname as dirname3, join as join9, resolve as resolve3 } from "path";
1010
1192
  var STOPWORDS = new Set(
1011
1193
  `
1012
1194
  a an the and or of to for in on with use used using when whether this that these those is are be
@@ -1027,7 +1209,7 @@ function splitLimited(text, sep, maxSplit) {
1027
1209
  return parts;
1028
1210
  }
1029
1211
  function readFrontmatter(skillMdPath) {
1030
- const text = readFileSync6(skillMdPath, "utf-8");
1212
+ const text = readFileSync7(skillMdPath, "utf-8");
1031
1213
  const meta = { name: basename4(dirname3(skillMdPath)), description: "" };
1032
1214
  if (text.startsWith("---\n")) {
1033
1215
  const block = splitLimited(text, "---", 2)[1] ?? "";
@@ -1122,29 +1304,29 @@ function suggestVerbCode(topScore) {
1122
1304
  return verbTier(topScore).verb;
1123
1305
  }
1124
1306
  function listSubdirs(dir) {
1125
- if (!existsSync8(dir)) return [];
1307
+ if (!existsSync9(dir)) return [];
1126
1308
  let entries;
1127
1309
  try {
1128
- entries = readdirSync4(dir, { withFileTypes: true });
1310
+ entries = readdirSync5(dir, { withFileTypes: true });
1129
1311
  } catch {
1130
1312
  return [];
1131
1313
  }
1132
- return entries.filter((e) => e.isDirectory()).map((e) => join8(dir, e.name));
1314
+ return entries.filter((e) => e.isDirectory()).map((e) => join9(dir, e.name));
1133
1315
  }
1134
1316
  function findSkillMdFiles(root) {
1135
1317
  const found = /* @__PURE__ */ new Set();
1136
1318
  for (const dir of listSubdirs(root)) {
1137
- const candidate = join8(dir, "SKILL.md");
1138
- if (existsSync8(candidate)) found.add(resolve3(candidate));
1319
+ const candidate = join9(dir, "SKILL.md");
1320
+ if (existsSync9(candidate)) found.add(resolve3(candidate));
1139
1321
  }
1140
- for (const dir of listSubdirs(join8(root, "skills"))) {
1141
- const candidate = join8(dir, "SKILL.md");
1142
- if (existsSync8(candidate)) found.add(resolve3(candidate));
1322
+ for (const dir of listSubdirs(join9(root, "skills"))) {
1323
+ const candidate = join9(dir, "SKILL.md");
1324
+ if (existsSync9(candidate)) found.add(resolve3(candidate));
1143
1325
  }
1144
1326
  for (const topDir of listSubdirs(root)) {
1145
- for (const dir of listSubdirs(join8(topDir, "skills"))) {
1146
- const candidate = join8(dir, "SKILL.md");
1147
- if (existsSync8(candidate)) found.add(resolve3(candidate));
1327
+ for (const dir of listSubdirs(join9(topDir, "skills"))) {
1328
+ const candidate = join9(dir, "SKILL.md");
1329
+ if (existsSync9(candidate)) found.add(resolve3(candidate));
1148
1330
  }
1149
1331
  }
1150
1332
  return Array.from(found).filter((p) => !p.includes("/.git/")).sort();
@@ -1156,9 +1338,9 @@ function scanOverlap(candidatePath, skillsRoot) {
1156
1338
  }
1157
1339
  let cpath = resolve3(expandHome(candidatePath));
1158
1340
  if (isDir(cpath)) {
1159
- cpath = join8(cpath, "SKILL.md");
1341
+ cpath = join9(cpath, "SKILL.md");
1160
1342
  }
1161
- if (!existsSync8(cpath)) {
1343
+ if (!existsSync9(cpath)) {
1162
1344
  throw new Error(`candidate SKILL.md not found: ${cpath}`);
1163
1345
  }
1164
1346
  const cand = readFrontmatter(cpath);
@@ -1182,9 +1364,9 @@ function scanOverlap(candidatePath, skillsRoot) {
1182
1364
  }
1183
1365
 
1184
1366
  // src/gate/mcpSafety.ts
1185
- import { readFileSync as readFileSync7, readdirSync as readdirSync5, statSync as statSync5 } from "fs";
1186
- import { basename as basename5, extname as extname3, join as join9 } from "path";
1187
- var SKIP_DIRS3 = /* @__PURE__ */ new Set([".git", "node_modules", "dist", "build", "__pycache__", ".venv", "venv"]);
1367
+ import { readFileSync as readFileSync8, readdirSync as readdirSync6, statSync as statSync6 } from "fs";
1368
+ import { basename as basename5, extname as extname4, join as join10 } from "path";
1369
+ var SKIP_DIRS4 = /* @__PURE__ */ new Set([".git", "node_modules", "dist", "build", "__pycache__", ".venv", "venv"]);
1188
1370
  var BINARY_EXT2 = /* @__PURE__ */ new Set([
1189
1371
  ".png",
1190
1372
  ".jpg",
@@ -1206,18 +1388,18 @@ var BINARY_EXT2 = /* @__PURE__ */ new Set([
1206
1388
  ".bin",
1207
1389
  ".exe"
1208
1390
  ]);
1209
- var MAX_FILE_BYTES2 = 2 * 1024 * 1024;
1391
+ var MAX_FILE_BYTES3 = 2 * 1024 * 1024;
1210
1392
  function* walk2(dir) {
1211
1393
  let entries;
1212
1394
  try {
1213
- entries = readdirSync5(dir, { withFileTypes: true });
1395
+ entries = readdirSync6(dir, { withFileTypes: true });
1214
1396
  } catch {
1215
1397
  return;
1216
1398
  }
1217
1399
  for (const entry of entries) {
1218
- const full = join9(dir, entry.name);
1400
+ const full = join10(dir, entry.name);
1219
1401
  if (entry.isDirectory()) {
1220
- if (SKIP_DIRS3.has(entry.name)) continue;
1402
+ if (SKIP_DIRS4.has(entry.name)) continue;
1221
1403
  yield* walk2(full);
1222
1404
  } else if (entry.isFile()) {
1223
1405
  yield full;
@@ -1225,10 +1407,10 @@ function* walk2(dir) {
1225
1407
  }
1226
1408
  }
1227
1409
  function readTextFile2(file) {
1228
- if (BINARY_EXT2.has(extname3(file).toLowerCase())) return void 0;
1410
+ if (BINARY_EXT2.has(extname4(file).toLowerCase())) return void 0;
1229
1411
  try {
1230
- if (statSync5(file).size > MAX_FILE_BYTES2) return void 0;
1231
- const content = readFileSync7(file, "utf-8");
1412
+ if (statSync6(file).size > MAX_FILE_BYTES3) return void 0;
1413
+ const content = readFileSync8(file, "utf-8");
1232
1414
  if (content.slice(0, 512).includes("\0")) return void 0;
1233
1415
  return content;
1234
1416
  } catch {
@@ -1395,7 +1577,7 @@ function scanTextFile(file, content, allowCredentialCheck) {
1395
1577
  function scanFileForMcpFindings(file) {
1396
1578
  const content = readTextFile2(file);
1397
1579
  if (content === void 0) return [];
1398
- if (extname3(file).toLowerCase() === ".json") {
1580
+ if (extname4(file).toLowerCase() === ".json") {
1399
1581
  const parsed = tryParseJson(content);
1400
1582
  if (parsed !== void 0) return scanJsonConfig(file, parsed);
1401
1583
  }
@@ -1404,7 +1586,7 @@ function scanFileForMcpFindings(file) {
1404
1586
  function scanMcpFindings(targetPath) {
1405
1587
  let root;
1406
1588
  try {
1407
- root = statSync5(targetPath);
1589
+ root = statSync6(targetPath);
1408
1590
  } catch {
1409
1591
  return [];
1410
1592
  }
@@ -1434,14 +1616,14 @@ async function mcpScan(targetPath, strictness = DEFAULT_STRICTNESS) {
1434
1616
  }
1435
1617
 
1436
1618
  // src/gate/mcpOverlap.ts
1437
- import { existsSync as existsSync9, readFileSync as readFileSync8 } from "fs";
1619
+ import { existsSync as existsSync10, readFileSync as readFileSync9 } from "fs";
1438
1620
  import { resolve as resolve4 } from "path";
1439
1621
  function readTargetServers(targetPath) {
1440
1622
  const path = resolve4(expandHome(targetPath));
1441
- if (!existsSync9(path)) return [];
1623
+ if (!existsSync10(path)) return [];
1442
1624
  let cfg;
1443
1625
  try {
1444
- cfg = JSON.parse(readFileSync8(path, "utf-8"));
1626
+ cfg = JSON.parse(readFileSync9(path, "utf-8"));
1445
1627
  } catch {
1446
1628
  return [];
1447
1629
  }
@@ -1639,6 +1821,16 @@ function profileHeaderFields(profile) {
1639
1821
  license: profile.kind === "skill" ? profile.license : "UNKNOWN"
1640
1822
  };
1641
1823
  }
1824
+ function pluralize(count, noun) {
1825
+ return `${count} ${noun}${count === 1 ? "" : "s"}`;
1826
+ }
1827
+ function renderCapabilitiesLine(profile) {
1828
+ const { tools, resources, prompts, declaredConfidence } = profile.capabilities;
1829
+ const counts = [pluralize(tools.length, "tool"), pluralize(resources.length, "resource"), pluralize(prompts.length, "prompt")].join(
1830
+ ", "
1831
+ );
1832
+ return `${padLabel("Capabilities")}: ${counts} (declaredConfidence: ${declaredConfidence})`;
1833
+ }
1642
1834
  function renderReport(result) {
1643
1835
  const { profile, overlap, overlapLocked, safety, worthTaking, leaveBehind } = result;
1644
1836
  const { name, version, license } = profileHeaderFields(profile);
@@ -1651,6 +1843,7 @@ function renderReport(result) {
1651
1843
  `\u{1F528} Forge: ${name} (v${version}, ${license})`,
1652
1844
  BORDER,
1653
1845
  `${padLabel("Artifact type")}: ${profile.artifactType}`,
1846
+ ...profile.kind === "mcp" ? [renderCapabilitiesLine(profile)] : [],
1654
1847
  ...overlapLines,
1655
1848
  renderListField("What's worth taking", worthTaking, "(pending \u2014 not yet determined)"),
1656
1849
  renderListField("What to leave behind", leaveBehind, "(pending \u2014 not yet determined)"),
@@ -1715,10 +1908,10 @@ function resolveMcpCandidatePath(entry) {
1715
1908
  if (entry.packageDir) return entry.packageDir;
1716
1909
  let children = [];
1717
1910
  try {
1718
- children = readdirSync6(entry.quarantinePath, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => d.name);
1911
+ children = readdirSync7(entry.quarantinePath, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => d.name);
1719
1912
  } catch {
1720
1913
  }
1721
- return children.length === 1 ? join10(entry.quarantinePath, children[0]) : entry.quarantinePath;
1914
+ return children.length === 1 ? join11(entry.quarantinePath, children[0]) : entry.quarantinePath;
1722
1915
  }
1723
1916
  async function runGate(candidatePath, skillsRoot, {
1724
1917
  proFeatureLabel = "overlap analysis",
@@ -1749,17 +1942,17 @@ async function runGate(candidatePath, skillsRoot, {
1749
1942
 
1750
1943
  // src/queue.ts
1751
1944
  import { randomUUID as randomUUID2 } from "crypto";
1752
- import { existsSync as existsSync10, mkdirSync as mkdirSync5, readFileSync as readFileSync9, writeFileSync as writeFileSync4 } from "fs";
1753
- import { join as join11 } from "path";
1945
+ import { existsSync as existsSync11, mkdirSync as mkdirSync5, readFileSync as readFileSync10, writeFileSync as writeFileSync4 } from "fs";
1946
+ import { join as join12 } from "path";
1754
1947
  function getQueuePath() {
1755
- return join11(getHomeDir(), "queue.json");
1948
+ return join12(getHomeDir(), "queue.json");
1756
1949
  }
1757
1950
  function readQueue() {
1758
1951
  const path = getQueuePath();
1759
- if (!existsSync10(path)) {
1952
+ if (!existsSync11(path)) {
1760
1953
  return { version: 1, entries: [] };
1761
1954
  }
1762
- const raw = readFileSync9(path, "utf-8");
1955
+ const raw = readFileSync10(path, "utf-8");
1763
1956
  const parsed = JSON.parse(raw);
1764
1957
  return { version: 1, entries: parsed.entries ?? [] };
1765
1958
  }
@@ -1789,16 +1982,16 @@ function generateId(existingIds) {
1789
1982
 
1790
1983
  // src/handoff.ts
1791
1984
  import { spawnSync } from "child_process";
1792
- import { existsSync as existsSync11 } from "fs";
1793
- import { dirname as dirname4, join as join12 } from "path";
1985
+ import { existsSync as existsSync12 } from "fs";
1986
+ import { dirname as dirname4, join as join13 } from "path";
1794
1987
  import { fileURLToPath } from "url";
1795
1988
  var KNOWN_AGENT_BINARIES = ["claude", "codex", "cursor-agent", "windsurf", "opencode", "gemini"];
1796
1989
  var HANDOFF_INSTRUCTION = "Read {prompt} and follow its instructions for the skill installed at {path}";
1797
1990
  function resolvePromptPath() {
1798
1991
  const here = dirname4(fileURLToPath(import.meta.url));
1799
- const builtCandidate = join12(here, "ingest-prompt.md");
1800
- if (existsSync11(builtCandidate)) return builtCandidate;
1801
- return join12(here, "..", "assets", "ingest-prompt.md");
1992
+ const builtCandidate = join13(here, "ingest-prompt.md");
1993
+ if (existsSync12(builtCandidate)) return builtCandidate;
1994
+ return join13(here, "..", "assets", "ingest-prompt.md");
1802
1995
  }
1803
1996
  function defaultIsOnPath(bin) {
1804
1997
  const finder = process.platform === "win32" ? "where" : "which";
@@ -1853,17 +2046,17 @@ function runHandoff(config, installedPath, options = {}) {
1853
2046
  }
1854
2047
 
1855
2048
  // src/commands/init.ts
1856
- import { existsSync as existsSync12 } from "fs";
2049
+ import { existsSync as existsSync13 } from "fs";
1857
2050
  import { homedir as homedir4 } from "os";
1858
2051
  import { createInterface } from "readline/promises";
1859
2052
 
1860
2053
  // src/agents.ts
1861
- import { statSync as statSync6 } from "fs";
2054
+ import { statSync as statSync7 } from "fs";
1862
2055
  import { homedir as homedir3 } from "os";
1863
- import { join as join13, resolve as resolve5 } from "path";
2056
+ import { join as join14, resolve as resolve5 } from "path";
1864
2057
  function isFile(p) {
1865
2058
  try {
1866
- return statSync6(p).isFile();
2059
+ return statSync7(p).isFile();
1867
2060
  } catch {
1868
2061
  return false;
1869
2062
  }
@@ -1874,7 +2067,7 @@ function projectPath(root, cwd) {
1874
2067
  function globalPath(root, home) {
1875
2068
  if (!root) return void 0;
1876
2069
  if (root === "~") return home;
1877
- if (root.startsWith("~/")) return join13(home, root.slice(2));
2070
+ if (root.startsWith("~/")) return join14(home, root.slice(2));
1878
2071
  return root;
1879
2072
  }
1880
2073
  function makeAgent(spec) {
@@ -2261,7 +2454,7 @@ async function init(options = {}) {
2261
2454
  }
2262
2455
  async function maybeOfferInit(options = {}) {
2263
2456
  if (options.json || options.yes || !process.stdin.isTTY) return;
2264
- if (existsSync12(getConfigPath())) return;
2457
+ if (existsSync13(getConfigPath())) return;
2265
2458
  console.log("[skill-forge] No config found \u2014 run `skill-forge init` to detect your agents and set gate targets.");
2266
2459
  const rl = createInterface({ input: process.stdin, output: process.stdout });
2267
2460
  let answer = "";
@@ -2400,7 +2593,7 @@ async function addMcp(source, config, options) {
2400
2593
  envVars: detail.env ? Object.keys(detail.env) : void 0,
2401
2594
  candidateEntry: detail.command ? { command: detail.command, args: detail.args, env: detail.env } : void 0
2402
2595
  };
2403
- const mcpTargetPath = options.mcpTarget ?? config.mcpTargets?.[0] ?? join14(process.cwd(), ".mcp.json");
2596
+ const mcpTargetPath = options.mcpTarget ?? config.mcpTargets?.[0] ?? join15(process.cwd(), ".mcp.json");
2404
2597
  const result = promoteMcp(entry, serverProfile, mcpTargetPath, { force: options.force });
2405
2598
  reject(entry);
2406
2599
  logOutcome(`[skill-forge] promoted -> ${result.target} (server "${result.serverName}")`, options.json);
@@ -2434,7 +2627,8 @@ async function addMcp(source, config, options) {
2434
2627
  suggestedVerb: overlap ? suggestVerbCode(overlap.nearestScore) : null
2435
2628
  },
2436
2629
  status: "pending",
2437
- artifactType: "mcp"
2630
+ artifactType: "mcp",
2631
+ capabilities: profile.capabilities
2438
2632
  });
2439
2633
  if (options.ingest && installedPath) {
2440
2634
  runHandoff(config, installedPath);
@@ -2513,7 +2707,7 @@ async function scan(source, options = {}) {
2513
2707
  }
2514
2708
 
2515
2709
  // src/commands/list.ts
2516
- import { join as join15 } from "path";
2710
+ import { join as join16 } from "path";
2517
2711
  async function list() {
2518
2712
  await maybeOfferInit();
2519
2713
  const config = loadConfig();
@@ -2526,14 +2720,14 @@ async function list() {
2526
2720
  }
2527
2721
  console.log(`[skill-forge] ${ids.length} held in quarantine (${quarantineDir}):`);
2528
2722
  for (const id of ids) {
2529
- const skillDirs = findSkillDirs(join15(quarantineDir, id));
2723
+ const skillDirs = findSkillDirs(join16(quarantineDir, id));
2530
2724
  if (skillDirs.length === 0) {
2531
2725
  console.log(` ${id} (no SKILL.md found)`);
2532
2726
  continue;
2533
2727
  }
2534
2728
  const names = skillDirs.map((dir) => {
2535
2729
  try {
2536
- return readFrontmatter(join15(dir, "SKILL.md")).name;
2730
+ return readFrontmatter(join16(dir, "SKILL.md")).name;
2537
2731
  } catch {
2538
2732
  return dir;
2539
2733
  }