@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/README.md +26 -2
- package/dist/cli.js +317 -123
- package/dist/cli.js.map +1 -1
- package/dist/ingest-prompt.md +103 -10
- package/package.json +1 -1
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
|
|
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
|
|
410
|
-
import { join as
|
|
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
|
|
416
|
+
existsSync as existsSync8,
|
|
417
417
|
mkdtempSync,
|
|
418
418
|
openSync,
|
|
419
|
-
readFileSync as
|
|
420
|
-
readdirSync as
|
|
419
|
+
readFileSync as readFileSync5,
|
|
420
|
+
readdirSync as readdirSync3,
|
|
421
421
|
readSync,
|
|
422
422
|
rmSync as rmSync2,
|
|
423
|
-
statSync as
|
|
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
|
|
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 (!
|
|
648
|
+
if (!existsSync8(p)) {
|
|
469
649
|
throw new Error(`path does not exist: ${p}`);
|
|
470
650
|
}
|
|
471
|
-
const stat =
|
|
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 (
|
|
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
|
|
487
|
-
var
|
|
488
|
-
var
|
|
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 =
|
|
491
|
-
if (!
|
|
670
|
+
const pkgPath = join7(root, "package.json");
|
|
671
|
+
if (!existsSync8(pkgPath)) return void 0;
|
|
492
672
|
try {
|
|
493
|
-
return JSON.parse(
|
|
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" &&
|
|
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 ===
|
|
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 (
|
|
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) =>
|
|
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 (
|
|
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 =
|
|
523
|
-
if (
|
|
524
|
-
tmpDir = mkdtempSync(
|
|
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 =
|
|
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 =
|
|
534
|
-
if (
|
|
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
|
|
737
|
+
for (const entry of readdirSync3(dir, { withFileTypes: true })) {
|
|
558
738
|
if (entry.isDirectory()) {
|
|
559
|
-
const candidate =
|
|
560
|
-
if (
|
|
739
|
+
const candidate = join7(dir, entry.name, "SKILL.md");
|
|
740
|
+
if (existsSync8(candidate)) matches.add(candidate);
|
|
561
741
|
}
|
|
562
742
|
}
|
|
563
|
-
const skillsDir =
|
|
743
|
+
const skillsDir = join7(dir, "skills");
|
|
564
744
|
if (isDir(skillsDir)) {
|
|
565
|
-
for (const entry of
|
|
745
|
+
for (const entry of readdirSync3(skillsDir, { withFileTypes: true })) {
|
|
566
746
|
if (entry.isDirectory()) {
|
|
567
|
-
const candidate =
|
|
568
|
-
if (
|
|
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 =
|
|
652
|
-
if (
|
|
653
|
-
const head =
|
|
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
|
|
665
|
-
const full =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
917
|
+
const manifestPath = join7(p, ".claude-plugin", "plugin.json");
|
|
738
918
|
let manifest;
|
|
739
919
|
try {
|
|
740
|
-
manifest = JSON.parse(
|
|
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 =
|
|
924
|
+
const skillsDir = join7(p, "skills");
|
|
745
925
|
let skills = [];
|
|
746
926
|
if (isDir(skillsDir)) {
|
|
747
|
-
skills =
|
|
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 =
|
|
750
|
-
const commands = isDir(commandsDir) ?
|
|
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 =
|
|
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(
|
|
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 =
|
|
967
|
+
const pkgPath = join7(p, "package.json");
|
|
787
968
|
let pkg2;
|
|
788
969
|
try {
|
|
789
|
-
pkg2 = JSON.parse(
|
|
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
|
|
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
|
|
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
|
|
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
|
|
1081
|
+
var MAX_FILE_BYTES2 = 2 * 1024 * 1024;
|
|
900
1082
|
function* walk(dir) {
|
|
901
1083
|
let entries;
|
|
902
1084
|
try {
|
|
903
|
-
entries =
|
|
1085
|
+
entries = readdirSync4(dir, { withFileTypes: true });
|
|
904
1086
|
} catch {
|
|
905
1087
|
return;
|
|
906
1088
|
}
|
|
907
1089
|
for (const entry of entries) {
|
|
908
|
-
const full =
|
|
1090
|
+
const full = join8(dir, entry.name);
|
|
909
1091
|
if (entry.isDirectory()) {
|
|
910
|
-
if (
|
|
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(
|
|
1100
|
+
if (BINARY_EXT.has(extname3(file).toLowerCase())) return void 0;
|
|
919
1101
|
try {
|
|
920
|
-
if (
|
|
921
|
-
const content =
|
|
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 =
|
|
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 =
|
|
1144
|
+
const candidate = join8(dir, cmd + ext);
|
|
963
1145
|
try {
|
|
964
|
-
if (
|
|
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
|
|
1009
|
-
import { basename as basename4, dirname as dirname3, join as
|
|
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 =
|
|
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 (!
|
|
1307
|
+
if (!existsSync9(dir)) return [];
|
|
1126
1308
|
let entries;
|
|
1127
1309
|
try {
|
|
1128
|
-
entries =
|
|
1310
|
+
entries = readdirSync5(dir, { withFileTypes: true });
|
|
1129
1311
|
} catch {
|
|
1130
1312
|
return [];
|
|
1131
1313
|
}
|
|
1132
|
-
return entries.filter((e) => e.isDirectory()).map((e) =>
|
|
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 =
|
|
1138
|
-
if (
|
|
1319
|
+
const candidate = join9(dir, "SKILL.md");
|
|
1320
|
+
if (existsSync9(candidate)) found.add(resolve3(candidate));
|
|
1139
1321
|
}
|
|
1140
|
-
for (const dir of listSubdirs(
|
|
1141
|
-
const candidate =
|
|
1142
|
-
if (
|
|
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(
|
|
1146
|
-
const candidate =
|
|
1147
|
-
if (
|
|
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 =
|
|
1341
|
+
cpath = join9(cpath, "SKILL.md");
|
|
1160
1342
|
}
|
|
1161
|
-
if (!
|
|
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
|
|
1186
|
-
import { basename as basename5, extname as
|
|
1187
|
-
var
|
|
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
|
|
1391
|
+
var MAX_FILE_BYTES3 = 2 * 1024 * 1024;
|
|
1210
1392
|
function* walk2(dir) {
|
|
1211
1393
|
let entries;
|
|
1212
1394
|
try {
|
|
1213
|
-
entries =
|
|
1395
|
+
entries = readdirSync6(dir, { withFileTypes: true });
|
|
1214
1396
|
} catch {
|
|
1215
1397
|
return;
|
|
1216
1398
|
}
|
|
1217
1399
|
for (const entry of entries) {
|
|
1218
|
-
const full =
|
|
1400
|
+
const full = join10(dir, entry.name);
|
|
1219
1401
|
if (entry.isDirectory()) {
|
|
1220
|
-
if (
|
|
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(
|
|
1410
|
+
if (BINARY_EXT2.has(extname4(file).toLowerCase())) return void 0;
|
|
1229
1411
|
try {
|
|
1230
|
-
if (
|
|
1231
|
-
const content =
|
|
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 (
|
|
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 =
|
|
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
|
|
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 (!
|
|
1623
|
+
if (!existsSync10(path)) return [];
|
|
1442
1624
|
let cfg;
|
|
1443
1625
|
try {
|
|
1444
|
-
cfg = JSON.parse(
|
|
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 =
|
|
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 ?
|
|
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
|
|
1753
|
-
import { join as
|
|
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
|
|
1948
|
+
return join12(getHomeDir(), "queue.json");
|
|
1756
1949
|
}
|
|
1757
1950
|
function readQueue() {
|
|
1758
1951
|
const path = getQueuePath();
|
|
1759
|
-
if (!
|
|
1952
|
+
if (!existsSync11(path)) {
|
|
1760
1953
|
return { version: 1, entries: [] };
|
|
1761
1954
|
}
|
|
1762
|
-
const raw =
|
|
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
|
|
1793
|
-
import { dirname as dirname4, join as
|
|
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 =
|
|
1800
|
-
if (
|
|
1801
|
-
return
|
|
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
|
|
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
|
|
2054
|
+
import { statSync as statSync7 } from "fs";
|
|
1862
2055
|
import { homedir as homedir3 } from "os";
|
|
1863
|
-
import { join as
|
|
2056
|
+
import { join as join14, resolve as resolve5 } from "path";
|
|
1864
2057
|
function isFile(p) {
|
|
1865
2058
|
try {
|
|
1866
|
-
return
|
|
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
|
|
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 (
|
|
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] ??
|
|
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
|
|
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(
|
|
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(
|
|
2730
|
+
return readFrontmatter(join16(dir, "SKILL.md")).name;
|
|
2537
2731
|
} catch {
|
|
2538
2732
|
return dir;
|
|
2539
2733
|
}
|