@rhize/skill-forge 0.5.0 → 0.6.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +36 -5
- package/dist/cli.js +413 -256
- 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";
|
|
@@ -153,15 +153,24 @@ async function quarantine(source, quarantineDir, deps = {}) {
|
|
|
153
153
|
break;
|
|
154
154
|
}
|
|
155
155
|
case "npm": {
|
|
156
|
-
const packResult = await exec(
|
|
157
|
-
|
|
158
|
-
|
|
156
|
+
const packResult = await exec(
|
|
157
|
+
"npm",
|
|
158
|
+
["pack", source.value, "--ignore-scripts", "--pack-destination", quarantinePath],
|
|
159
|
+
{ cwd: quarantinePath }
|
|
160
|
+
);
|
|
159
161
|
if (packResult.code !== 0) {
|
|
160
162
|
throw new UserError(
|
|
161
163
|
`npm pack failed for "${source.value}" (exit ${packResult.code}): ${packResult.stderr || packResult.stdout}`
|
|
162
164
|
);
|
|
163
165
|
}
|
|
164
166
|
const tarballPath = join3(quarantinePath, findTarball(quarantinePath, source.value));
|
|
167
|
+
const listResult = await exec("tar", ["-tzf", tarballPath], { cwd: quarantinePath });
|
|
168
|
+
if (listResult.code !== 0) {
|
|
169
|
+
throw new UserError(
|
|
170
|
+
`could not list tarball contents for "${source.value}" (exit ${listResult.code}): ${listResult.stderr || listResult.stdout}`
|
|
171
|
+
);
|
|
172
|
+
}
|
|
173
|
+
assertSafeTarMembers(listResult.stdout, source.value);
|
|
165
174
|
const extractResult = await exec("tar", ["-xzf", tarballPath, "-C", quarantinePath], {
|
|
166
175
|
cwd: quarantinePath
|
|
167
176
|
});
|
|
@@ -190,7 +199,7 @@ function listQuarantineIds(quarantineDir) {
|
|
|
190
199
|
}
|
|
191
200
|
function findSkillDirs(root) {
|
|
192
201
|
const found = [];
|
|
193
|
-
function
|
|
202
|
+
function walk2(dir) {
|
|
194
203
|
let entries;
|
|
195
204
|
try {
|
|
196
205
|
entries = readdirSync(dir, { withFileTypes: true });
|
|
@@ -202,13 +211,24 @@ function findSkillDirs(root) {
|
|
|
202
211
|
}
|
|
203
212
|
for (const entry of entries) {
|
|
204
213
|
if (entry.isDirectory() && !SKIP_DIRS.has(entry.name)) {
|
|
205
|
-
|
|
214
|
+
walk2(join3(dir, entry.name));
|
|
206
215
|
}
|
|
207
216
|
}
|
|
208
217
|
}
|
|
209
|
-
|
|
218
|
+
walk2(root);
|
|
210
219
|
return found;
|
|
211
220
|
}
|
|
221
|
+
function assertSafeTarMembers(tarListOutput, sourceValue) {
|
|
222
|
+
for (const raw of tarListOutput.split("\n")) {
|
|
223
|
+
const member = raw.trim();
|
|
224
|
+
if (!member) continue;
|
|
225
|
+
if (member.startsWith("/") || member.split("/").includes("..")) {
|
|
226
|
+
throw new UserError(
|
|
227
|
+
`refusing to extract tarball for "${sourceValue}": unsafe member path "${member}" (absolute path or ".." segment \u2014 possible path traversal)`
|
|
228
|
+
);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
}
|
|
212
232
|
function findTarball(dir, sourceValue) {
|
|
213
233
|
const tarball = readdirSync(dir).find((name) => name.endsWith(".tgz"));
|
|
214
234
|
if (!tarball) {
|
|
@@ -281,6 +301,7 @@ import { dirname } from "path";
|
|
|
281
301
|
function promoteMcp(_entry, profile, targetConfigPath, options = {}) {
|
|
282
302
|
const force = options.force ?? false;
|
|
283
303
|
const serverName = profile.name;
|
|
304
|
+
const { command, args, envVarsToFill } = deriveServerEntry(profile);
|
|
284
305
|
let config = {};
|
|
285
306
|
let rawExisting;
|
|
286
307
|
if (existsSync5(targetConfigPath)) {
|
|
@@ -302,7 +323,6 @@ function promoteMcp(_entry, profile, targetConfigPath, options = {}) {
|
|
|
302
323
|
backupPath = `${targetConfigPath}.bak-${Date.now()}`;
|
|
303
324
|
writeFileSync2(backupPath, rawExisting);
|
|
304
325
|
}
|
|
305
|
-
const { command, args, envVarsToFill } = deriveServerEntry(profile);
|
|
306
326
|
const env = {};
|
|
307
327
|
for (const name of envVarsToFill) env[name] = "";
|
|
308
328
|
servers[serverName] = { command, args, env };
|
|
@@ -324,6 +344,11 @@ function deriveServerEntry(profile) {
|
|
|
324
344
|
envVarsToFill: Array.from(envNames)
|
|
325
345
|
};
|
|
326
346
|
}
|
|
347
|
+
if (!profile.version) {
|
|
348
|
+
throw new UserError(
|
|
349
|
+
`cannot promote MCP server "${profile.name}": no version was found in its package.json, and there is no documented launch command to fall back to \u2014 refusing to write an unpinnable "npx ${profile.name}@unknown" command`
|
|
350
|
+
);
|
|
351
|
+
}
|
|
327
352
|
return {
|
|
328
353
|
command: "npx",
|
|
329
354
|
args: ["-y", `${profile.name}@${profile.version}`],
|
|
@@ -406,24 +431,246 @@ function appendProvenance(entry, skillsRoot) {
|
|
|
406
431
|
}
|
|
407
432
|
|
|
408
433
|
// src/commands/gatePipeline.ts
|
|
409
|
-
import { readdirSync as
|
|
410
|
-
import { join as
|
|
434
|
+
import { readdirSync as readdirSync5 } from "fs";
|
|
435
|
+
import { join as join11 } from "path";
|
|
411
436
|
|
|
412
437
|
// src/gate/profile.ts
|
|
413
438
|
import { execFileSync } from "child_process";
|
|
414
439
|
import {
|
|
415
440
|
closeSync,
|
|
416
|
-
existsSync as
|
|
441
|
+
existsSync as existsSync8,
|
|
417
442
|
mkdtempSync,
|
|
418
443
|
openSync,
|
|
419
|
-
readFileSync as
|
|
420
|
-
readdirSync as
|
|
444
|
+
readFileSync as readFileSync6,
|
|
445
|
+
readdirSync as readdirSync3,
|
|
421
446
|
readSync,
|
|
422
447
|
rmSync as rmSync2,
|
|
423
|
-
statSync as
|
|
448
|
+
statSync as statSync4
|
|
424
449
|
} from "fs";
|
|
425
450
|
import { tmpdir } from "os";
|
|
426
|
-
import { basename as basename3, dirname as dirname2, extname, join as
|
|
451
|
+
import { basename as basename3, dirname as dirname2, extname as extname3, join as join8, resolve as resolve2 } from "path";
|
|
452
|
+
|
|
453
|
+
// src/gate/mcpCapabilities.ts
|
|
454
|
+
import { existsSync as existsSync7, readFileSync as readFileSync5 } from "fs";
|
|
455
|
+
import { extname as extname2, join as join7 } from "path";
|
|
456
|
+
|
|
457
|
+
// src/gate/scanShared.ts
|
|
458
|
+
import { readFileSync as readFileSync4, readdirSync as readdirSync2, statSync as statSync3 } from "fs";
|
|
459
|
+
import { extname, join as join6 } from "path";
|
|
460
|
+
var SKIP_DIRS2 = /* @__PURE__ */ new Set([".git", "node_modules", "dist", "build", "__pycache__", ".venv", "venv"]);
|
|
461
|
+
var BINARY_EXT = /* @__PURE__ */ new Set([
|
|
462
|
+
".png",
|
|
463
|
+
".jpg",
|
|
464
|
+
".jpeg",
|
|
465
|
+
".gif",
|
|
466
|
+
".webp",
|
|
467
|
+
".ico",
|
|
468
|
+
".pdf",
|
|
469
|
+
".zip",
|
|
470
|
+
".tar",
|
|
471
|
+
".gz",
|
|
472
|
+
".woff",
|
|
473
|
+
".woff2",
|
|
474
|
+
".ttf",
|
|
475
|
+
".eot",
|
|
476
|
+
".mp4",
|
|
477
|
+
".mp3",
|
|
478
|
+
".wasm",
|
|
479
|
+
".bin",
|
|
480
|
+
".exe"
|
|
481
|
+
]);
|
|
482
|
+
var MAX_FILE_BYTES = 2 * 1024 * 1024;
|
|
483
|
+
function* walk(dir) {
|
|
484
|
+
let entries;
|
|
485
|
+
try {
|
|
486
|
+
entries = readdirSync2(dir, { withFileTypes: true });
|
|
487
|
+
} catch {
|
|
488
|
+
return;
|
|
489
|
+
}
|
|
490
|
+
for (const entry of entries) {
|
|
491
|
+
const full = join6(dir, entry.name);
|
|
492
|
+
if (entry.isDirectory()) {
|
|
493
|
+
if (SKIP_DIRS2.has(entry.name)) continue;
|
|
494
|
+
yield* walk(full);
|
|
495
|
+
} else if (entry.isFile()) {
|
|
496
|
+
yield full;
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
function readTextFile(file) {
|
|
501
|
+
if (BINARY_EXT.has(extname(file).toLowerCase())) return void 0;
|
|
502
|
+
try {
|
|
503
|
+
if (statSync3(file).size > MAX_FILE_BYTES) return void 0;
|
|
504
|
+
const content = readFileSync4(file, "utf-8");
|
|
505
|
+
if (content.slice(0, 512).includes("\0")) return void 0;
|
|
506
|
+
return content;
|
|
507
|
+
} catch {
|
|
508
|
+
return void 0;
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
var BLOCK_SEVERITIES_BY_STRICTNESS = {
|
|
512
|
+
"block-high": /* @__PURE__ */ new Set(["HIGH", "CRITICAL"]),
|
|
513
|
+
"block-medium": /* @__PURE__ */ new Set(["MEDIUM", "HIGH", "CRITICAL"]),
|
|
514
|
+
"warn-only": /* @__PURE__ */ new Set()
|
|
515
|
+
};
|
|
516
|
+
function verdictFor(findings, strictness) {
|
|
517
|
+
const blockSeverities = BLOCK_SEVERITIES_BY_STRICTNESS[strictness];
|
|
518
|
+
if (findings.some((f) => blockSeverities.has(f.severity))) return "block";
|
|
519
|
+
if (findings.length > 0) return "warn";
|
|
520
|
+
return "pass";
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
// src/gate/mcpCapabilities.ts
|
|
524
|
+
var SOURCE_EXT = /* @__PURE__ */ new Set([".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs"]);
|
|
525
|
+
var MAX_ITEMS_PER_CATEGORY = 200;
|
|
526
|
+
function readJsonSafe(file) {
|
|
527
|
+
try {
|
|
528
|
+
return JSON.parse(readFileSync5(file, "utf-8"));
|
|
529
|
+
} catch {
|
|
530
|
+
return void 0;
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
var MCP_SDK_DEP = "@modelcontextprotocol/sdk";
|
|
534
|
+
var MCP_KEYWORD = "mcp-server";
|
|
535
|
+
var MCP_BIN_PREFIX = /^mcp-/i;
|
|
536
|
+
function hasPackageJsonMcpMarkers(pkg2) {
|
|
537
|
+
for (const section of ["dependencies", "devDependencies", "peerDependencies"]) {
|
|
538
|
+
const deps = pkg2[section];
|
|
539
|
+
if (deps && typeof deps === "object" && MCP_SDK_DEP in deps) return true;
|
|
540
|
+
}
|
|
541
|
+
const keywords = pkg2.keywords;
|
|
542
|
+
if (Array.isArray(keywords) && keywords.some((k) => k === MCP_KEYWORD)) return true;
|
|
543
|
+
const bin = pkg2.bin;
|
|
544
|
+
if (typeof bin === "string") {
|
|
545
|
+
if (MCP_BIN_PREFIX.test(bin.split("/").pop() ?? bin)) return true;
|
|
546
|
+
} else if (bin && typeof bin === "object") {
|
|
547
|
+
if (Object.keys(bin).some((k) => MCP_BIN_PREFIX.test(k))) return true;
|
|
548
|
+
}
|
|
549
|
+
if (pkg2.mcp && typeof pkg2.mcp === "object") return true;
|
|
550
|
+
return false;
|
|
551
|
+
}
|
|
552
|
+
function collectDeclaredFromManifest(node, buckets) {
|
|
553
|
+
if (Array.isArray(node)) {
|
|
554
|
+
for (const item of node) collectDeclaredFromManifest(item, buckets);
|
|
555
|
+
return;
|
|
556
|
+
}
|
|
557
|
+
if (!node || typeof node !== "object") return;
|
|
558
|
+
const obj = node;
|
|
559
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
560
|
+
if ((key === "tools" || key === "resources" || key === "prompts") && Array.isArray(value)) {
|
|
561
|
+
const target = buckets[key];
|
|
562
|
+
for (const item of value) {
|
|
563
|
+
if (typeof item === "string") {
|
|
564
|
+
target.add(item);
|
|
565
|
+
} else if (item && typeof item === "object") {
|
|
566
|
+
const name = item.name ?? item.uri;
|
|
567
|
+
if (typeof name === "string") target.add(name);
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
} else {
|
|
571
|
+
collectDeclaredFromManifest(value, buckets);
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
var REGISTRATION_CALL_RE = /\bserver\.(tool|registerTool|resource|prompt)\(\s*['"]([^'"]+)['"]/g;
|
|
576
|
+
var REGISTRATION_METHOD_CATEGORY = {
|
|
577
|
+
tool: "tools",
|
|
578
|
+
registerTool: "tools",
|
|
579
|
+
resource: "resources",
|
|
580
|
+
prompt: "prompts"
|
|
581
|
+
};
|
|
582
|
+
function captureRegistrations(text, buckets) {
|
|
583
|
+
REGISTRATION_CALL_RE.lastIndex = 0;
|
|
584
|
+
let m;
|
|
585
|
+
while ((m = REGISTRATION_CALL_RE.exec(text)) !== null) {
|
|
586
|
+
buckets[REGISTRATION_METHOD_CATEGORY[m[1]]].add(m[2]);
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
var LIST_HANDLER_SCHEMAS = {
|
|
590
|
+
ListToolsRequestSchema: "tools",
|
|
591
|
+
ListResourcesRequestSchema: "resources",
|
|
592
|
+
ListPromptsRequestSchema: "prompts"
|
|
593
|
+
};
|
|
594
|
+
var SET_REQUEST_HANDLER_RE = /setRequestHandler\(\s*(ListToolsRequestSchema|ListResourcesRequestSchema|ListPromptsRequestSchema)\s*,/g;
|
|
595
|
+
var NAME_OR_URI_FIELD_RE = /\b(?:name|uri)\s*:\s*['"]([^'"]+)['"]/g;
|
|
596
|
+
function extractBalancedCall(text, openParenIndex) {
|
|
597
|
+
let depth = 0;
|
|
598
|
+
for (let i = openParenIndex; i < text.length; i++) {
|
|
599
|
+
const ch = text[i];
|
|
600
|
+
if (ch === "(") depth++;
|
|
601
|
+
else if (ch === ")") {
|
|
602
|
+
depth--;
|
|
603
|
+
if (depth === 0) return text.slice(openParenIndex, i + 1);
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
return text.slice(openParenIndex, Math.min(text.length, openParenIndex + 2e4));
|
|
607
|
+
}
|
|
608
|
+
function extractFromListHandlers(text, buckets) {
|
|
609
|
+
SET_REQUEST_HANDLER_RE.lastIndex = 0;
|
|
610
|
+
let m;
|
|
611
|
+
while ((m = SET_REQUEST_HANDLER_RE.exec(text)) !== null) {
|
|
612
|
+
const openParenIdx = text.indexOf("(", m.index);
|
|
613
|
+
if (openParenIdx === -1) continue;
|
|
614
|
+
const body = extractBalancedCall(text, openParenIdx);
|
|
615
|
+
const target = buckets[LIST_HANDLER_SCHEMAS[m[1]]];
|
|
616
|
+
NAME_OR_URI_FIELD_RE.lastIndex = 0;
|
|
617
|
+
let fieldMatch;
|
|
618
|
+
while ((fieldMatch = NAME_OR_URI_FIELD_RE.exec(body)) !== null) {
|
|
619
|
+
target.add(fieldMatch[1]);
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
var STDIO_TRANSPORT_RE = /StdioServerTransport/;
|
|
624
|
+
var HTTP_TRANSPORT_RE = /StreamableHTTPServerTransport|SSEServerTransport|WebSocketServerTransport/;
|
|
625
|
+
var MCP_SDK_IMPORT_RE = /@modelcontextprotocol\/sdk/;
|
|
626
|
+
function extractMcpCapabilities(packageDir) {
|
|
627
|
+
const buckets = { tools: /* @__PURE__ */ new Set(), resources: /* @__PURE__ */ new Set(), prompts: /* @__PURE__ */ new Set() };
|
|
628
|
+
let hasPackageJsonMarker = false;
|
|
629
|
+
const pkgPath = join7(packageDir, "package.json");
|
|
630
|
+
if (existsSync7(pkgPath)) {
|
|
631
|
+
const pkg2 = readJsonSafe(pkgPath);
|
|
632
|
+
if (pkg2 && typeof pkg2 === "object" && !Array.isArray(pkg2)) {
|
|
633
|
+
hasPackageJsonMarker = hasPackageJsonMcpMarkers(pkg2);
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
for (const manifestName of [".mcp.json", "mcp.json"]) {
|
|
637
|
+
const manifestPath = join7(packageDir, manifestName);
|
|
638
|
+
if (!existsSync7(manifestPath)) continue;
|
|
639
|
+
const parsed = readJsonSafe(manifestPath);
|
|
640
|
+
if (parsed !== void 0) {
|
|
641
|
+
collectDeclaredFromManifest(parsed, buckets);
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
let sawStdioTransport = false;
|
|
645
|
+
let sawHttpTransport = false;
|
|
646
|
+
let sawMcpSdkImport = false;
|
|
647
|
+
for (const file of walk(packageDir)) {
|
|
648
|
+
if (!SOURCE_EXT.has(extname2(file))) continue;
|
|
649
|
+
const text = readTextFile(file);
|
|
650
|
+
if (text === void 0) continue;
|
|
651
|
+
captureRegistrations(text, buckets);
|
|
652
|
+
extractFromListHandlers(text, buckets);
|
|
653
|
+
if (STDIO_TRANSPORT_RE.test(text)) sawStdioTransport = true;
|
|
654
|
+
if (HTTP_TRANSPORT_RE.test(text)) sawHttpTransport = true;
|
|
655
|
+
if (MCP_SDK_IMPORT_RE.test(text)) sawMcpSdkImport = true;
|
|
656
|
+
}
|
|
657
|
+
const transport = sawStdioTransport ? "stdio" : sawHttpTransport ? "http" : sawMcpSdkImport ? "unknown" : void 0;
|
|
658
|
+
const sortCap = (set) => Array.from(set).sort().slice(0, MAX_ITEMS_PER_CATEGORY);
|
|
659
|
+
const tools = sortCap(buckets.tools);
|
|
660
|
+
const resources = sortCap(buckets.resources);
|
|
661
|
+
const prompts = sortCap(buckets.prompts);
|
|
662
|
+
const hasExplicitSignal = tools.length > 0 || resources.length > 0 || prompts.length > 0;
|
|
663
|
+
const declaredConfidence = hasExplicitSignal ? "high" : hasPackageJsonMarker ? "partial" : "none";
|
|
664
|
+
return {
|
|
665
|
+
tools,
|
|
666
|
+
resources,
|
|
667
|
+
prompts,
|
|
668
|
+
transport,
|
|
669
|
+
declaredConfidence
|
|
670
|
+
};
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
// src/gate/profile.ts
|
|
427
674
|
var SPDX_HINTS = {
|
|
428
675
|
mit: "MIT (permissive)",
|
|
429
676
|
apache: "Apache-2.0 (permissive)",
|
|
@@ -465,10 +712,10 @@ var PYTHON_STDLIB = /* @__PURE__ */ new Set([
|
|
|
465
712
|
]);
|
|
466
713
|
function profileSkill(inputPath) {
|
|
467
714
|
const p = resolve2(expandHome(inputPath));
|
|
468
|
-
if (!
|
|
715
|
+
if (!existsSync8(p)) {
|
|
469
716
|
throw new Error(`path does not exist: ${p}`);
|
|
470
717
|
}
|
|
471
|
-
const stat =
|
|
718
|
+
const stat = statSync4(p);
|
|
472
719
|
const kind = stat.isDirectory() ? classifyInput(p) : "skill";
|
|
473
720
|
if (kind === "plugin") {
|
|
474
721
|
return profilePlugin(p);
|
|
@@ -479,18 +726,18 @@ function profileSkill(inputPath) {
|
|
|
479
726
|
return profileSkillDir(p);
|
|
480
727
|
}
|
|
481
728
|
function classifyInput(p) {
|
|
482
|
-
if (
|
|
729
|
+
if (existsSync8(join8(p, ".claude-plugin", "plugin.json"))) return "plugin";
|
|
483
730
|
if (detectArtifactType(p) === "mcp") return "mcp";
|
|
484
731
|
return "skill";
|
|
485
732
|
}
|
|
486
|
-
var
|
|
487
|
-
var
|
|
488
|
-
var
|
|
733
|
+
var MCP_SDK_DEP2 = "@modelcontextprotocol/sdk";
|
|
734
|
+
var MCP_KEYWORD2 = "mcp-server";
|
|
735
|
+
var MCP_BIN_PREFIX2 = /^mcp-/i;
|
|
489
736
|
function readPackageJsonSafe(root) {
|
|
490
|
-
const pkgPath =
|
|
491
|
-
if (!
|
|
737
|
+
const pkgPath = join8(root, "package.json");
|
|
738
|
+
if (!existsSync8(pkgPath)) return void 0;
|
|
492
739
|
try {
|
|
493
|
-
return JSON.parse(
|
|
740
|
+
return JSON.parse(readFileSync6(pkgPath, "utf-8"));
|
|
494
741
|
} catch {
|
|
495
742
|
return void 0;
|
|
496
743
|
}
|
|
@@ -498,20 +745,20 @@ function readPackageJsonSafe(root) {
|
|
|
498
745
|
function hasMcpPackageMarkers(pkg2) {
|
|
499
746
|
for (const section of ["dependencies", "devDependencies", "peerDependencies"]) {
|
|
500
747
|
const deps = pkg2[section];
|
|
501
|
-
if (deps && typeof deps === "object" &&
|
|
748
|
+
if (deps && typeof deps === "object" && MCP_SDK_DEP2 in deps) return true;
|
|
502
749
|
}
|
|
503
750
|
const keywords = pkg2.keywords;
|
|
504
|
-
if (Array.isArray(keywords) && keywords.some((k) => k ===
|
|
751
|
+
if (Array.isArray(keywords) && keywords.some((k) => k === MCP_KEYWORD2)) return true;
|
|
505
752
|
const bin = pkg2.bin;
|
|
506
753
|
if (typeof bin === "string") {
|
|
507
|
-
if (
|
|
754
|
+
if (MCP_BIN_PREFIX2.test(basename3(bin))) return true;
|
|
508
755
|
} else if (bin && typeof bin === "object") {
|
|
509
|
-
if (Object.keys(bin).some((k) =>
|
|
756
|
+
if (Object.keys(bin).some((k) => MCP_BIN_PREFIX2.test(k))) return true;
|
|
510
757
|
}
|
|
511
758
|
return false;
|
|
512
759
|
}
|
|
513
760
|
function detectArtifactType(root) {
|
|
514
|
-
if (
|
|
761
|
+
if (existsSync8(join8(root, ".mcp.json")) || existsSync8(join8(root, "mcp.json"))) return "mcp";
|
|
515
762
|
const pkg2 = readPackageJsonSafe(root);
|
|
516
763
|
if (pkg2 && hasMcpPackageMarkers(pkg2)) return "mcp";
|
|
517
764
|
return "skill";
|
|
@@ -519,19 +766,19 @@ function detectArtifactType(root) {
|
|
|
519
766
|
function resolveSkillMd(inputPath) {
|
|
520
767
|
let currentPath = inputPath;
|
|
521
768
|
let tmpDir;
|
|
522
|
-
const initialStat =
|
|
523
|
-
if (
|
|
524
|
-
tmpDir = mkdtempSync(
|
|
769
|
+
const initialStat = statSync4(currentPath);
|
|
770
|
+
if (extname3(currentPath) === ".skill" || initialStat.isFile() && isZipFile(currentPath)) {
|
|
771
|
+
tmpDir = mkdtempSync(join8(tmpdir(), "forge_"));
|
|
525
772
|
extractZip(currentPath, tmpDir);
|
|
526
773
|
currentPath = tmpDir;
|
|
527
774
|
}
|
|
528
|
-
const stat =
|
|
775
|
+
const stat = statSync4(currentPath);
|
|
529
776
|
if (stat.isFile() && basename3(currentPath) === "SKILL.md") {
|
|
530
777
|
return { skillMd: currentPath, root: dirname2(currentPath), tmpDir };
|
|
531
778
|
}
|
|
532
779
|
if (stat.isDirectory()) {
|
|
533
|
-
const direct =
|
|
534
|
-
if (
|
|
780
|
+
const direct = join8(currentPath, "SKILL.md");
|
|
781
|
+
if (existsSync8(direct)) {
|
|
535
782
|
return { skillMd: direct, root: currentPath, tmpDir };
|
|
536
783
|
}
|
|
537
784
|
const matches = findSkillMdOneLevelDown(currentPath);
|
|
@@ -554,18 +801,18 @@ function cleanupTmp(tmpDir) {
|
|
|
554
801
|
}
|
|
555
802
|
function findSkillMdOneLevelDown(dir) {
|
|
556
803
|
const matches = /* @__PURE__ */ new Set();
|
|
557
|
-
for (const entry of
|
|
804
|
+
for (const entry of readdirSync3(dir, { withFileTypes: true })) {
|
|
558
805
|
if (entry.isDirectory()) {
|
|
559
|
-
const candidate =
|
|
560
|
-
if (
|
|
806
|
+
const candidate = join8(dir, entry.name, "SKILL.md");
|
|
807
|
+
if (existsSync8(candidate)) matches.add(candidate);
|
|
561
808
|
}
|
|
562
809
|
}
|
|
563
|
-
const skillsDir =
|
|
810
|
+
const skillsDir = join8(dir, "skills");
|
|
564
811
|
if (isDir(skillsDir)) {
|
|
565
|
-
for (const entry of
|
|
812
|
+
for (const entry of readdirSync3(skillsDir, { withFileTypes: true })) {
|
|
566
813
|
if (entry.isDirectory()) {
|
|
567
|
-
const candidate =
|
|
568
|
-
if (
|
|
814
|
+
const candidate = join8(skillsDir, entry.name, "SKILL.md");
|
|
815
|
+
if (existsSync8(candidate)) matches.add(candidate);
|
|
569
816
|
}
|
|
570
817
|
}
|
|
571
818
|
}
|
|
@@ -648,9 +895,9 @@ function detectLicense(root, fm) {
|
|
|
648
895
|
if (fm.license) return fm.license;
|
|
649
896
|
const names = ["LICENSE", "LICENSE.md", "LICENSE.txt", "COPYING", "license"];
|
|
650
897
|
for (const name of names) {
|
|
651
|
-
const p =
|
|
652
|
-
if (
|
|
653
|
-
const head =
|
|
898
|
+
const p = join8(root, name);
|
|
899
|
+
if (existsSync8(p) && statSync4(p).isFile()) {
|
|
900
|
+
const head = readFileSync6(p, "utf-8").slice(0, 2e3).toLowerCase();
|
|
654
901
|
for (const [hint, label] of Object.entries(SPDX_HINTS)) {
|
|
655
902
|
if (head.includes(hint)) return label;
|
|
656
903
|
}
|
|
@@ -661,8 +908,8 @@ function detectLicense(root, fm) {
|
|
|
661
908
|
}
|
|
662
909
|
function walkFiles(dir) {
|
|
663
910
|
const out = [];
|
|
664
|
-
for (const entry of
|
|
665
|
-
const full =
|
|
911
|
+
for (const entry of readdirSync3(dir, { withFileTypes: true })) {
|
|
912
|
+
const full = join8(dir, entry.name);
|
|
666
913
|
if (entry.isDirectory()) {
|
|
667
914
|
out.push(...walkFiles(full));
|
|
668
915
|
} else if (entry.isFile()) {
|
|
@@ -674,7 +921,7 @@ function walkFiles(dir) {
|
|
|
674
921
|
function countResources(root) {
|
|
675
922
|
const resources = {};
|
|
676
923
|
for (const d of RESOURCE_DIRS) {
|
|
677
|
-
const dp =
|
|
924
|
+
const dp = join8(root, d);
|
|
678
925
|
if (isDir(dp)) {
|
|
679
926
|
resources[d] = walkFiles(dp).length;
|
|
680
927
|
}
|
|
@@ -685,11 +932,11 @@ function scanMcpAndImports(text, root) {
|
|
|
685
932
|
const mcpRefRe = /mcp__[a-zA-Z0-9_]+/g;
|
|
686
933
|
const mcpRefs = new Set(text.match(mcpRefRe) ?? []);
|
|
687
934
|
const imports = /* @__PURE__ */ new Set();
|
|
688
|
-
const scriptsDir =
|
|
935
|
+
const scriptsDir = join8(root, "scripts");
|
|
689
936
|
if (isDir(scriptsDir)) {
|
|
690
937
|
for (const f of walkFiles(scriptsDir)) {
|
|
691
938
|
if (!f.endsWith(".py")) continue;
|
|
692
|
-
const ft =
|
|
939
|
+
const ft = readFileSync6(f, "utf-8");
|
|
693
940
|
for (const m2 of ft.match(mcpRefRe) ?? []) mcpRefs.add(m2);
|
|
694
941
|
const importRe = /^\s*(?:import|from)\s+([a-zA-Z0-9_]+)/gm;
|
|
695
942
|
let m;
|
|
@@ -706,7 +953,7 @@ function scanMcpAndImports(text, root) {
|
|
|
706
953
|
function profileSkillDir(inputPath) {
|
|
707
954
|
const { skillMd, root, tmpDir } = resolveSkillMd(inputPath);
|
|
708
955
|
try {
|
|
709
|
-
const text =
|
|
956
|
+
const text = readFileSync6(skillMd, "utf-8");
|
|
710
957
|
const fm = parseFrontmatter(text);
|
|
711
958
|
const bodyLines = (text.match(/\n/g)?.length ?? 0) + 1;
|
|
712
959
|
const headers = Array.from(text.matchAll(/^#{1,3}\s+(.+)$/gm)).map((m) => m[1]);
|
|
@@ -734,20 +981,20 @@ function profileSkillDir(inputPath) {
|
|
|
734
981
|
}
|
|
735
982
|
}
|
|
736
983
|
function profilePlugin(p) {
|
|
737
|
-
const manifestPath =
|
|
984
|
+
const manifestPath = join8(p, ".claude-plugin", "plugin.json");
|
|
738
985
|
let manifest;
|
|
739
986
|
try {
|
|
740
|
-
manifest = JSON.parse(
|
|
987
|
+
manifest = JSON.parse(readFileSync6(manifestPath, "utf-8"));
|
|
741
988
|
} catch (err) {
|
|
742
989
|
throw new Error(`could not parse plugin.json under ${p}: ${err.message}`);
|
|
743
990
|
}
|
|
744
|
-
const skillsDir =
|
|
991
|
+
const skillsDir = join8(p, "skills");
|
|
745
992
|
let skills = [];
|
|
746
993
|
if (isDir(skillsDir)) {
|
|
747
|
-
skills =
|
|
994
|
+
skills = readdirSync3(skillsDir, { withFileTypes: true }).filter((d) => d.isDirectory() && existsSync8(join8(skillsDir, d.name, "SKILL.md"))).map((d) => d.name).sort();
|
|
748
995
|
}
|
|
749
|
-
const commandsDir =
|
|
750
|
-
const commands = isDir(commandsDir) ?
|
|
996
|
+
const commandsDir = join8(p, "commands");
|
|
997
|
+
const commands = isDir(commandsDir) ? readdirSync3(commandsDir).filter((f) => f.endsWith(".md")).length : 0;
|
|
751
998
|
const mc = manifest.mcpServers ?? manifest.mcp_servers ?? {};
|
|
752
999
|
const mcpServers = typeof mc === "object" && mc !== null ? Object.keys(mc).sort() : [];
|
|
753
1000
|
return {
|
|
@@ -764,11 +1011,11 @@ function profilePlugin(p) {
|
|
|
764
1011
|
};
|
|
765
1012
|
}
|
|
766
1013
|
function profileMcp(p) {
|
|
767
|
-
const explicitConfig =
|
|
1014
|
+
const explicitConfig = existsSync8(join8(p, ".mcp.json")) ? join8(p, ".mcp.json") : existsSync8(join8(p, "mcp.json")) ? join8(p, "mcp.json") : void 0;
|
|
768
1015
|
if (explicitConfig) {
|
|
769
1016
|
let cfg;
|
|
770
1017
|
try {
|
|
771
|
-
cfg = JSON.parse(
|
|
1018
|
+
cfg = JSON.parse(readFileSync6(explicitConfig, "utf-8"));
|
|
772
1019
|
} catch (err) {
|
|
773
1020
|
throw new Error(`could not parse MCP config ${explicitConfig}: ${err.message}`);
|
|
774
1021
|
}
|
|
@@ -780,13 +1027,14 @@ function profileMcp(p) {
|
|
|
780
1027
|
config: explicitConfig,
|
|
781
1028
|
servers: names,
|
|
782
1029
|
serverCount: names.length,
|
|
783
|
-
artifactType: "mcp"
|
|
1030
|
+
artifactType: "mcp",
|
|
1031
|
+
capabilities: extractMcpCapabilities(p)
|
|
784
1032
|
};
|
|
785
1033
|
}
|
|
786
|
-
const pkgPath =
|
|
1034
|
+
const pkgPath = join8(p, "package.json");
|
|
787
1035
|
let pkg2;
|
|
788
1036
|
try {
|
|
789
|
-
pkg2 = JSON.parse(
|
|
1037
|
+
pkg2 = JSON.parse(readFileSync6(pkgPath, "utf-8"));
|
|
790
1038
|
} catch (err) {
|
|
791
1039
|
throw new Error(`could not parse package.json ${pkgPath}: ${err.message}`);
|
|
792
1040
|
}
|
|
@@ -797,21 +1045,17 @@ function profileMcp(p) {
|
|
|
797
1045
|
config: pkgPath,
|
|
798
1046
|
servers: [name],
|
|
799
1047
|
serverCount: 1,
|
|
800
|
-
artifactType: "mcp"
|
|
1048
|
+
artifactType: "mcp",
|
|
1049
|
+
capabilities: extractMcpCapabilities(p)
|
|
801
1050
|
};
|
|
802
1051
|
}
|
|
803
1052
|
|
|
804
1053
|
// src/gate/safety.ts
|
|
805
|
-
import {
|
|
1054
|
+
import { statSync as statSync5 } from "fs";
|
|
806
1055
|
import { execFile } from "child_process";
|
|
807
|
-
import { delimiter,
|
|
1056
|
+
import { delimiter, join as join9 } from "path";
|
|
808
1057
|
import { promisify } from "util";
|
|
809
1058
|
var execFileAsync = promisify(execFile);
|
|
810
|
-
var BLOCK_SEVERITIES_BY_STRICTNESS = {
|
|
811
|
-
"block-high": /* @__PURE__ */ new Set(["HIGH", "CRITICAL"]),
|
|
812
|
-
"block-medium": /* @__PURE__ */ new Set(["MEDIUM", "HIGH", "CRITICAL"]),
|
|
813
|
-
"warn-only": /* @__PURE__ */ new Set()
|
|
814
|
-
};
|
|
815
1059
|
var DENY_RULES = [
|
|
816
1060
|
{
|
|
817
1061
|
rule: "curl-pipe-shell",
|
|
@@ -874,62 +1118,11 @@ var DENY_RULES = [
|
|
|
874
1118
|
message: "invokes sudo \u2014 review for unnecessary privilege escalation"
|
|
875
1119
|
}
|
|
876
1120
|
];
|
|
877
|
-
var SKIP_DIRS2 = /* @__PURE__ */ new Set([".git", "node_modules", "dist", "build", "__pycache__", ".venv", "venv"]);
|
|
878
|
-
var BINARY_EXT = /* @__PURE__ */ new Set([
|
|
879
|
-
".png",
|
|
880
|
-
".jpg",
|
|
881
|
-
".jpeg",
|
|
882
|
-
".gif",
|
|
883
|
-
".webp",
|
|
884
|
-
".ico",
|
|
885
|
-
".pdf",
|
|
886
|
-
".zip",
|
|
887
|
-
".tar",
|
|
888
|
-
".gz",
|
|
889
|
-
".woff",
|
|
890
|
-
".woff2",
|
|
891
|
-
".ttf",
|
|
892
|
-
".eot",
|
|
893
|
-
".mp4",
|
|
894
|
-
".mp3",
|
|
895
|
-
".wasm",
|
|
896
|
-
".bin",
|
|
897
|
-
".exe"
|
|
898
|
-
]);
|
|
899
|
-
var MAX_FILE_BYTES = 2 * 1024 * 1024;
|
|
900
|
-
function* walk(dir) {
|
|
901
|
-
let entries;
|
|
902
|
-
try {
|
|
903
|
-
entries = readdirSync3(dir, { withFileTypes: true });
|
|
904
|
-
} catch {
|
|
905
|
-
return;
|
|
906
|
-
}
|
|
907
|
-
for (const entry of entries) {
|
|
908
|
-
const full = join7(dir, entry.name);
|
|
909
|
-
if (entry.isDirectory()) {
|
|
910
|
-
if (SKIP_DIRS2.has(entry.name)) continue;
|
|
911
|
-
yield* walk(full);
|
|
912
|
-
} else if (entry.isFile()) {
|
|
913
|
-
yield full;
|
|
914
|
-
}
|
|
915
|
-
}
|
|
916
|
-
}
|
|
917
|
-
function readTextFile(file) {
|
|
918
|
-
if (BINARY_EXT.has(extname2(file).toLowerCase())) return void 0;
|
|
919
|
-
try {
|
|
920
|
-
if (statSync4(file).size > MAX_FILE_BYTES) return void 0;
|
|
921
|
-
const content = readFileSync5(file, "utf-8");
|
|
922
|
-
if (content.slice(0, 512).includes("\0")) return void 0;
|
|
923
|
-
return content;
|
|
924
|
-
} catch {
|
|
925
|
-
return void 0;
|
|
926
|
-
}
|
|
927
|
-
}
|
|
928
1121
|
function scanStaticRules(targetPath) {
|
|
929
1122
|
const findings = [];
|
|
930
1123
|
let root;
|
|
931
1124
|
try {
|
|
932
|
-
root =
|
|
1125
|
+
root = statSync5(targetPath);
|
|
933
1126
|
} catch {
|
|
934
1127
|
return findings;
|
|
935
1128
|
}
|
|
@@ -959,9 +1152,9 @@ function which(cmd) {
|
|
|
959
1152
|
const exts = process.platform === "win32" ? (process.env.PATHEXT ?? ".EXE;.CMD;.BAT").split(";") : [""];
|
|
960
1153
|
for (const dir of dirs) {
|
|
961
1154
|
for (const ext of exts) {
|
|
962
|
-
const candidate =
|
|
1155
|
+
const candidate = join9(dir, cmd + ext);
|
|
963
1156
|
try {
|
|
964
|
-
if (
|
|
1157
|
+
if (statSync5(candidate).isFile()) return candidate;
|
|
965
1158
|
} catch {
|
|
966
1159
|
}
|
|
967
1160
|
}
|
|
@@ -993,20 +1186,14 @@ async function scanWithSkillSpector(targetPath) {
|
|
|
993
1186
|
return [];
|
|
994
1187
|
}
|
|
995
1188
|
}
|
|
996
|
-
function verdictFor(findings, strictness) {
|
|
997
|
-
const blockSeverities = BLOCK_SEVERITIES_BY_STRICTNESS[strictness];
|
|
998
|
-
if (findings.some((f) => blockSeverities.has(f.severity))) return "block";
|
|
999
|
-
if (findings.length > 0) return "warn";
|
|
1000
|
-
return "pass";
|
|
1001
|
-
}
|
|
1002
1189
|
async function scanSafety(targetPath, strictness = DEFAULT_STRICTNESS) {
|
|
1003
1190
|
const findings = [...scanStaticRules(targetPath), ...await scanWithSkillSpector(targetPath)];
|
|
1004
1191
|
return { findings, verdict: verdictFor(findings, strictness) };
|
|
1005
1192
|
}
|
|
1006
1193
|
|
|
1007
1194
|
// src/gate/overlap.ts
|
|
1008
|
-
import { existsSync as
|
|
1009
|
-
import { basename as basename4, dirname as dirname3, join as
|
|
1195
|
+
import { existsSync as existsSync9, readFileSync as readFileSync7, readdirSync as readdirSync4 } from "fs";
|
|
1196
|
+
import { basename as basename4, dirname as dirname3, join as join10, resolve as resolve3 } from "path";
|
|
1010
1197
|
var STOPWORDS = new Set(
|
|
1011
1198
|
`
|
|
1012
1199
|
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 +1214,7 @@ function splitLimited(text, sep, maxSplit) {
|
|
|
1027
1214
|
return parts;
|
|
1028
1215
|
}
|
|
1029
1216
|
function readFrontmatter(skillMdPath) {
|
|
1030
|
-
const text =
|
|
1217
|
+
const text = readFileSync7(skillMdPath, "utf-8");
|
|
1031
1218
|
const meta = { name: basename4(dirname3(skillMdPath)), description: "" };
|
|
1032
1219
|
if (text.startsWith("---\n")) {
|
|
1033
1220
|
const block = splitLimited(text, "---", 2)[1] ?? "";
|
|
@@ -1122,29 +1309,29 @@ function suggestVerbCode(topScore) {
|
|
|
1122
1309
|
return verbTier(topScore).verb;
|
|
1123
1310
|
}
|
|
1124
1311
|
function listSubdirs(dir) {
|
|
1125
|
-
if (!
|
|
1312
|
+
if (!existsSync9(dir)) return [];
|
|
1126
1313
|
let entries;
|
|
1127
1314
|
try {
|
|
1128
1315
|
entries = readdirSync4(dir, { withFileTypes: true });
|
|
1129
1316
|
} catch {
|
|
1130
1317
|
return [];
|
|
1131
1318
|
}
|
|
1132
|
-
return entries.filter((e) => e.isDirectory()).map((e) =>
|
|
1319
|
+
return entries.filter((e) => e.isDirectory()).map((e) => join10(dir, e.name));
|
|
1133
1320
|
}
|
|
1134
1321
|
function findSkillMdFiles(root) {
|
|
1135
1322
|
const found = /* @__PURE__ */ new Set();
|
|
1136
1323
|
for (const dir of listSubdirs(root)) {
|
|
1137
|
-
const candidate =
|
|
1138
|
-
if (
|
|
1324
|
+
const candidate = join10(dir, "SKILL.md");
|
|
1325
|
+
if (existsSync9(candidate)) found.add(resolve3(candidate));
|
|
1139
1326
|
}
|
|
1140
|
-
for (const dir of listSubdirs(
|
|
1141
|
-
const candidate =
|
|
1142
|
-
if (
|
|
1327
|
+
for (const dir of listSubdirs(join10(root, "skills"))) {
|
|
1328
|
+
const candidate = join10(dir, "SKILL.md");
|
|
1329
|
+
if (existsSync9(candidate)) found.add(resolve3(candidate));
|
|
1143
1330
|
}
|
|
1144
1331
|
for (const topDir of listSubdirs(root)) {
|
|
1145
|
-
for (const dir of listSubdirs(
|
|
1146
|
-
const candidate =
|
|
1147
|
-
if (
|
|
1332
|
+
for (const dir of listSubdirs(join10(topDir, "skills"))) {
|
|
1333
|
+
const candidate = join10(dir, "SKILL.md");
|
|
1334
|
+
if (existsSync9(candidate)) found.add(resolve3(candidate));
|
|
1148
1335
|
}
|
|
1149
1336
|
}
|
|
1150
1337
|
return Array.from(found).filter((p) => !p.includes("/.git/")).sort();
|
|
@@ -1156,9 +1343,9 @@ function scanOverlap(candidatePath, skillsRoot) {
|
|
|
1156
1343
|
}
|
|
1157
1344
|
let cpath = resolve3(expandHome(candidatePath));
|
|
1158
1345
|
if (isDir(cpath)) {
|
|
1159
|
-
cpath =
|
|
1346
|
+
cpath = join10(cpath, "SKILL.md");
|
|
1160
1347
|
}
|
|
1161
|
-
if (!
|
|
1348
|
+
if (!existsSync9(cpath)) {
|
|
1162
1349
|
throw new Error(`candidate SKILL.md not found: ${cpath}`);
|
|
1163
1350
|
}
|
|
1164
1351
|
const cand = readFrontmatter(cpath);
|
|
@@ -1182,59 +1369,8 @@ function scanOverlap(candidatePath, skillsRoot) {
|
|
|
1182
1369
|
}
|
|
1183
1370
|
|
|
1184
1371
|
// src/gate/mcpSafety.ts
|
|
1185
|
-
import {
|
|
1186
|
-
import { basename as basename5, extname as
|
|
1187
|
-
var SKIP_DIRS3 = /* @__PURE__ */ new Set([".git", "node_modules", "dist", "build", "__pycache__", ".venv", "venv"]);
|
|
1188
|
-
var BINARY_EXT2 = /* @__PURE__ */ new Set([
|
|
1189
|
-
".png",
|
|
1190
|
-
".jpg",
|
|
1191
|
-
".jpeg",
|
|
1192
|
-
".gif",
|
|
1193
|
-
".webp",
|
|
1194
|
-
".ico",
|
|
1195
|
-
".pdf",
|
|
1196
|
-
".zip",
|
|
1197
|
-
".tar",
|
|
1198
|
-
".gz",
|
|
1199
|
-
".woff",
|
|
1200
|
-
".woff2",
|
|
1201
|
-
".ttf",
|
|
1202
|
-
".eot",
|
|
1203
|
-
".mp4",
|
|
1204
|
-
".mp3",
|
|
1205
|
-
".wasm",
|
|
1206
|
-
".bin",
|
|
1207
|
-
".exe"
|
|
1208
|
-
]);
|
|
1209
|
-
var MAX_FILE_BYTES2 = 2 * 1024 * 1024;
|
|
1210
|
-
function* walk2(dir) {
|
|
1211
|
-
let entries;
|
|
1212
|
-
try {
|
|
1213
|
-
entries = readdirSync5(dir, { withFileTypes: true });
|
|
1214
|
-
} catch {
|
|
1215
|
-
return;
|
|
1216
|
-
}
|
|
1217
|
-
for (const entry of entries) {
|
|
1218
|
-
const full = join9(dir, entry.name);
|
|
1219
|
-
if (entry.isDirectory()) {
|
|
1220
|
-
if (SKIP_DIRS3.has(entry.name)) continue;
|
|
1221
|
-
yield* walk2(full);
|
|
1222
|
-
} else if (entry.isFile()) {
|
|
1223
|
-
yield full;
|
|
1224
|
-
}
|
|
1225
|
-
}
|
|
1226
|
-
}
|
|
1227
|
-
function readTextFile2(file) {
|
|
1228
|
-
if (BINARY_EXT2.has(extname3(file).toLowerCase())) return void 0;
|
|
1229
|
-
try {
|
|
1230
|
-
if (statSync5(file).size > MAX_FILE_BYTES2) return void 0;
|
|
1231
|
-
const content = readFileSync7(file, "utf-8");
|
|
1232
|
-
if (content.slice(0, 512).includes("\0")) return void 0;
|
|
1233
|
-
return content;
|
|
1234
|
-
} catch {
|
|
1235
|
-
return void 0;
|
|
1236
|
-
}
|
|
1237
|
-
}
|
|
1372
|
+
import { statSync as statSync6 } from "fs";
|
|
1373
|
+
import { basename as basename5, extname as extname4 } from "path";
|
|
1238
1374
|
function isConfigOrExampleFile(file) {
|
|
1239
1375
|
const base = basename5(file).toLowerCase();
|
|
1240
1376
|
if (base.startsWith(".env")) return true;
|
|
@@ -1264,7 +1400,18 @@ function looksHighEntropy(value) {
|
|
|
1264
1400
|
}
|
|
1265
1401
|
function isPinnedPackageSpec(spec) {
|
|
1266
1402
|
const withoutScope = spec.startsWith("@") ? spec.slice(spec.indexOf("/") + 1) : spec;
|
|
1267
|
-
|
|
1403
|
+
const atIdx = withoutScope.lastIndexOf("@");
|
|
1404
|
+
if (atIdx <= 0) return false;
|
|
1405
|
+
return /^\d/.test(withoutScope.slice(atIdx + 1));
|
|
1406
|
+
}
|
|
1407
|
+
function isNpxCommand(command) {
|
|
1408
|
+
const base = command.split(/[\\/]/).pop() ?? command;
|
|
1409
|
+
return base.replace(/\.(cmd|exe)$/i, "").toLowerCase() === "npx";
|
|
1410
|
+
}
|
|
1411
|
+
function packageSpecBase(spec) {
|
|
1412
|
+
const scopeEnd = spec.startsWith("@") ? spec.indexOf("/") + 1 : 0;
|
|
1413
|
+
const atIdx = spec.lastIndexOf("@");
|
|
1414
|
+
return atIdx > scopeEnd ? spec.slice(0, atIdx) : spec;
|
|
1268
1415
|
}
|
|
1269
1416
|
function collectServerNodes(node, out) {
|
|
1270
1417
|
if (Array.isArray(node)) {
|
|
@@ -1293,7 +1440,7 @@ function scanJsonConfig(file, parsed) {
|
|
|
1293
1440
|
for (const node of nodes) {
|
|
1294
1441
|
const command = typeof node.command === "string" ? node.command : void 0;
|
|
1295
1442
|
const args = Array.isArray(node.args) ? node.args.filter((a) => typeof a === "string") : [];
|
|
1296
|
-
if (command
|
|
1443
|
+
if (command && isNpxCommand(command)) {
|
|
1297
1444
|
const flagIdx = args.findIndex((a) => a === "-y" || a === "--yes");
|
|
1298
1445
|
if (flagIdx !== -1) {
|
|
1299
1446
|
const pkgSpec = args.slice(flagIdx + 1).find((a) => !a.startsWith("-"));
|
|
@@ -1302,7 +1449,7 @@ function scanJsonConfig(file, parsed) {
|
|
|
1302
1449
|
severity: "MEDIUM",
|
|
1303
1450
|
rule: "mcp-unpinned-npx",
|
|
1304
1451
|
file,
|
|
1305
|
-
detail: `npx -y ${pkgSpec} is unpinned \u2014 pin an exact version (${pkgSpec}@<version>) so the resolved server code can't change silently`
|
|
1452
|
+
detail: `npx -y ${pkgSpec} is unpinned \u2014 pin an exact version (${packageSpecBase(pkgSpec)}@<version>) so the resolved server code can't change silently`
|
|
1306
1453
|
});
|
|
1307
1454
|
}
|
|
1308
1455
|
}
|
|
@@ -1341,9 +1488,22 @@ function scanJsonConfig(file, parsed) {
|
|
|
1341
1488
|
}
|
|
1342
1489
|
return findings;
|
|
1343
1490
|
}
|
|
1344
|
-
var NPX_SHELL_RE = /\bnpx
|
|
1491
|
+
var NPX_SHELL_RE = /\bnpx(?:\.cmd|\.exe)?\s+(?:-y|--yes)\s+((?:@[\w.-]+\/)?[\w.-]+)(@[\w.-]+)?/gi;
|
|
1345
1492
|
var DANGEROUS_FLAG_RE = /--dangerously-[\w-]+|--no-sandbox\b/g;
|
|
1346
1493
|
var CREDENTIAL_ASSIGNMENT_RE = /["']?([A-Za-z][A-Za-z0-9_]*)["']?\s*[:=]\s*["']([^"']{6,})["']/g;
|
|
1494
|
+
var CREDENTIAL_ASSIGNMENT_UNQUOTED_RE = /^\s*(?:export\s+|-\s+)?([A-Za-z][A-Za-z0-9_]*)\s*[:=]\s*(\S+)/;
|
|
1495
|
+
var LEADING_QUOTE_RE = /^["']/;
|
|
1496
|
+
function pushCredentialFinding(findings, file, lineNo, varName, value) {
|
|
1497
|
+
if (isPlaceholderValue(value)) return;
|
|
1498
|
+
if (looksCredentialKey(varName) || looksHighEntropy(value)) {
|
|
1499
|
+
findings.push({
|
|
1500
|
+
severity: "HIGH",
|
|
1501
|
+
rule: "mcp-inline-credential-value",
|
|
1502
|
+
file,
|
|
1503
|
+
detail: `line ${lineNo}: inline credential value assigned to ${varName}`
|
|
1504
|
+
});
|
|
1505
|
+
}
|
|
1506
|
+
}
|
|
1347
1507
|
function scanTextFile(file, content, allowCredentialCheck) {
|
|
1348
1508
|
const findings = [];
|
|
1349
1509
|
const lines = content.split("\n");
|
|
@@ -1354,8 +1514,8 @@ function scanTextFile(file, content, allowCredentialCheck) {
|
|
|
1354
1514
|
let m;
|
|
1355
1515
|
while ((m = NPX_SHELL_RE.exec(line)) !== null) {
|
|
1356
1516
|
const pkgSpec = m[1];
|
|
1357
|
-
const
|
|
1358
|
-
if (!
|
|
1517
|
+
const versionSuffix = m[2] ?? "";
|
|
1518
|
+
if (!isPinnedPackageSpec(pkgSpec + versionSuffix)) {
|
|
1359
1519
|
findings.push({
|
|
1360
1520
|
severity: "MEDIUM",
|
|
1361
1521
|
rule: "mcp-unpinned-npx",
|
|
@@ -1376,26 +1536,20 @@ function scanTextFile(file, content, allowCredentialCheck) {
|
|
|
1376
1536
|
if (allowCredentialCheck) {
|
|
1377
1537
|
CREDENTIAL_ASSIGNMENT_RE.lastIndex = 0;
|
|
1378
1538
|
while ((m = CREDENTIAL_ASSIGNMENT_RE.exec(line)) !== null) {
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
severity: "HIGH",
|
|
1385
|
-
rule: "mcp-inline-credential-value",
|
|
1386
|
-
file,
|
|
1387
|
-
detail: `line ${lineNo}: inline credential value assigned to ${varName}`
|
|
1388
|
-
});
|
|
1389
|
-
}
|
|
1539
|
+
pushCredentialFinding(findings, file, lineNo, m[1], m[2]);
|
|
1540
|
+
}
|
|
1541
|
+
const unquoted = CREDENTIAL_ASSIGNMENT_UNQUOTED_RE.exec(line);
|
|
1542
|
+
if (unquoted && !LEADING_QUOTE_RE.test(unquoted[2])) {
|
|
1543
|
+
pushCredentialFinding(findings, file, lineNo, unquoted[1], unquoted[2]);
|
|
1390
1544
|
}
|
|
1391
1545
|
}
|
|
1392
1546
|
}
|
|
1393
1547
|
return findings;
|
|
1394
1548
|
}
|
|
1395
1549
|
function scanFileForMcpFindings(file) {
|
|
1396
|
-
const content =
|
|
1550
|
+
const content = readTextFile(file);
|
|
1397
1551
|
if (content === void 0) return [];
|
|
1398
|
-
if (
|
|
1552
|
+
if (extname4(file).toLowerCase() === ".json") {
|
|
1399
1553
|
const parsed = tryParseJson(content);
|
|
1400
1554
|
if (parsed !== void 0) return scanJsonConfig(file, parsed);
|
|
1401
1555
|
}
|
|
@@ -1404,41 +1558,30 @@ function scanFileForMcpFindings(file) {
|
|
|
1404
1558
|
function scanMcpFindings(targetPath) {
|
|
1405
1559
|
let root;
|
|
1406
1560
|
try {
|
|
1407
|
-
root =
|
|
1561
|
+
root = statSync6(targetPath);
|
|
1408
1562
|
} catch {
|
|
1409
1563
|
return [];
|
|
1410
1564
|
}
|
|
1411
|
-
const files = root.isDirectory() ? Array.from(
|
|
1565
|
+
const files = root.isDirectory() ? Array.from(walk(targetPath)) : [targetPath];
|
|
1412
1566
|
const findings = [];
|
|
1413
1567
|
for (const file of files) {
|
|
1414
1568
|
findings.push(...scanFileForMcpFindings(file));
|
|
1415
1569
|
}
|
|
1416
1570
|
return findings;
|
|
1417
1571
|
}
|
|
1418
|
-
var BLOCK_SEVERITIES_BY_STRICTNESS2 = {
|
|
1419
|
-
"block-high": /* @__PURE__ */ new Set(["HIGH", "CRITICAL"]),
|
|
1420
|
-
"block-medium": /* @__PURE__ */ new Set(["MEDIUM", "HIGH", "CRITICAL"]),
|
|
1421
|
-
"warn-only": /* @__PURE__ */ new Set()
|
|
1422
|
-
};
|
|
1423
|
-
function verdictFor2(findings, strictness) {
|
|
1424
|
-
const blockSeverities = BLOCK_SEVERITIES_BY_STRICTNESS2[strictness];
|
|
1425
|
-
if (findings.some((f) => blockSeverities.has(f.severity))) return "block";
|
|
1426
|
-
if (findings.length > 0) return "warn";
|
|
1427
|
-
return "pass";
|
|
1428
|
-
}
|
|
1429
1572
|
async function mcpScan(targetPath, strictness = DEFAULT_STRICTNESS) {
|
|
1430
1573
|
const base = await scanSafety(targetPath, strictness);
|
|
1431
1574
|
const mcpFindings = scanMcpFindings(targetPath);
|
|
1432
1575
|
const findings = [...base.findings, ...mcpFindings];
|
|
1433
|
-
return { findings, verdict:
|
|
1576
|
+
return { findings, verdict: verdictFor(findings, strictness) };
|
|
1434
1577
|
}
|
|
1435
1578
|
|
|
1436
1579
|
// src/gate/mcpOverlap.ts
|
|
1437
|
-
import { existsSync as
|
|
1580
|
+
import { existsSync as existsSync10, readFileSync as readFileSync8 } from "fs";
|
|
1438
1581
|
import { resolve as resolve4 } from "path";
|
|
1439
1582
|
function readTargetServers(targetPath) {
|
|
1440
1583
|
const path = resolve4(expandHome(targetPath));
|
|
1441
|
-
if (!
|
|
1584
|
+
if (!existsSync10(path)) return [];
|
|
1442
1585
|
let cfg;
|
|
1443
1586
|
try {
|
|
1444
1587
|
cfg = JSON.parse(readFileSync8(path, "utf-8"));
|
|
@@ -1639,6 +1782,16 @@ function profileHeaderFields(profile) {
|
|
|
1639
1782
|
license: profile.kind === "skill" ? profile.license : "UNKNOWN"
|
|
1640
1783
|
};
|
|
1641
1784
|
}
|
|
1785
|
+
function pluralize(count, noun) {
|
|
1786
|
+
return `${count} ${noun}${count === 1 ? "" : "s"}`;
|
|
1787
|
+
}
|
|
1788
|
+
function renderCapabilitiesLine(profile) {
|
|
1789
|
+
const { tools, resources, prompts, declaredConfidence } = profile.capabilities;
|
|
1790
|
+
const counts = [pluralize(tools.length, "tool"), pluralize(resources.length, "resource"), pluralize(prompts.length, "prompt")].join(
|
|
1791
|
+
", "
|
|
1792
|
+
);
|
|
1793
|
+
return `${padLabel("Capabilities")}: ${counts} (declaredConfidence: ${declaredConfidence})`;
|
|
1794
|
+
}
|
|
1642
1795
|
function renderReport(result) {
|
|
1643
1796
|
const { profile, overlap, overlapLocked, safety, worthTaking, leaveBehind } = result;
|
|
1644
1797
|
const { name, version, license } = profileHeaderFields(profile);
|
|
@@ -1651,6 +1804,7 @@ function renderReport(result) {
|
|
|
1651
1804
|
`\u{1F528} Forge: ${name} (v${version}, ${license})`,
|
|
1652
1805
|
BORDER,
|
|
1653
1806
|
`${padLabel("Artifact type")}: ${profile.artifactType}`,
|
|
1807
|
+
...profile.kind === "mcp" ? [renderCapabilitiesLine(profile)] : [],
|
|
1654
1808
|
...overlapLines,
|
|
1655
1809
|
renderListField("What's worth taking", worthTaking, "(pending \u2014 not yet determined)"),
|
|
1656
1810
|
renderListField("What to leave behind", leaveBehind, "(pending \u2014 not yet determined)"),
|
|
@@ -1715,10 +1869,10 @@ function resolveMcpCandidatePath(entry) {
|
|
|
1715
1869
|
if (entry.packageDir) return entry.packageDir;
|
|
1716
1870
|
let children = [];
|
|
1717
1871
|
try {
|
|
1718
|
-
children =
|
|
1872
|
+
children = readdirSync5(entry.quarantinePath, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => d.name);
|
|
1719
1873
|
} catch {
|
|
1720
1874
|
}
|
|
1721
|
-
return children.length === 1 ?
|
|
1875
|
+
return children.length === 1 ? join11(entry.quarantinePath, children[0]) : entry.quarantinePath;
|
|
1722
1876
|
}
|
|
1723
1877
|
async function runGate(candidatePath, skillsRoot, {
|
|
1724
1878
|
proFeatureLabel = "overlap analysis",
|
|
@@ -1749,14 +1903,14 @@ async function runGate(candidatePath, skillsRoot, {
|
|
|
1749
1903
|
|
|
1750
1904
|
// src/queue.ts
|
|
1751
1905
|
import { randomUUID as randomUUID2 } from "crypto";
|
|
1752
|
-
import { existsSync as
|
|
1753
|
-
import { join as
|
|
1906
|
+
import { existsSync as existsSync11, mkdirSync as mkdirSync5, readFileSync as readFileSync9, writeFileSync as writeFileSync4 } from "fs";
|
|
1907
|
+
import { join as join12 } from "path";
|
|
1754
1908
|
function getQueuePath() {
|
|
1755
|
-
return
|
|
1909
|
+
return join12(getHomeDir(), "queue.json");
|
|
1756
1910
|
}
|
|
1757
1911
|
function readQueue() {
|
|
1758
1912
|
const path = getQueuePath();
|
|
1759
|
-
if (!
|
|
1913
|
+
if (!existsSync11(path)) {
|
|
1760
1914
|
return { version: 1, entries: [] };
|
|
1761
1915
|
}
|
|
1762
1916
|
const raw = readFileSync9(path, "utf-8");
|
|
@@ -1789,16 +1943,16 @@ function generateId(existingIds) {
|
|
|
1789
1943
|
|
|
1790
1944
|
// src/handoff.ts
|
|
1791
1945
|
import { spawnSync } from "child_process";
|
|
1792
|
-
import { existsSync as
|
|
1793
|
-
import { dirname as dirname4, join as
|
|
1946
|
+
import { existsSync as existsSync12 } from "fs";
|
|
1947
|
+
import { dirname as dirname4, join as join13 } from "path";
|
|
1794
1948
|
import { fileURLToPath } from "url";
|
|
1795
1949
|
var KNOWN_AGENT_BINARIES = ["claude", "codex", "cursor-agent", "windsurf", "opencode", "gemini"];
|
|
1796
1950
|
var HANDOFF_INSTRUCTION = "Read {prompt} and follow its instructions for the skill installed at {path}";
|
|
1797
1951
|
function resolvePromptPath() {
|
|
1798
1952
|
const here = dirname4(fileURLToPath(import.meta.url));
|
|
1799
|
-
const builtCandidate =
|
|
1800
|
-
if (
|
|
1801
|
-
return
|
|
1953
|
+
const builtCandidate = join13(here, "ingest-prompt.md");
|
|
1954
|
+
if (existsSync12(builtCandidate)) return builtCandidate;
|
|
1955
|
+
return join13(here, "..", "assets", "ingest-prompt.md");
|
|
1802
1956
|
}
|
|
1803
1957
|
function defaultIsOnPath(bin) {
|
|
1804
1958
|
const finder = process.platform === "win32" ? "where" : "which";
|
|
@@ -1853,17 +2007,17 @@ function runHandoff(config, installedPath, options = {}) {
|
|
|
1853
2007
|
}
|
|
1854
2008
|
|
|
1855
2009
|
// src/commands/init.ts
|
|
1856
|
-
import { existsSync as
|
|
2010
|
+
import { existsSync as existsSync13 } from "fs";
|
|
1857
2011
|
import { homedir as homedir4 } from "os";
|
|
1858
2012
|
import { createInterface } from "readline/promises";
|
|
1859
2013
|
|
|
1860
2014
|
// src/agents.ts
|
|
1861
|
-
import { statSync as
|
|
2015
|
+
import { statSync as statSync7 } from "fs";
|
|
1862
2016
|
import { homedir as homedir3 } from "os";
|
|
1863
|
-
import { join as
|
|
2017
|
+
import { join as join14, resolve as resolve5 } from "path";
|
|
1864
2018
|
function isFile(p) {
|
|
1865
2019
|
try {
|
|
1866
|
-
return
|
|
2020
|
+
return statSync7(p).isFile();
|
|
1867
2021
|
} catch {
|
|
1868
2022
|
return false;
|
|
1869
2023
|
}
|
|
@@ -1874,7 +2028,7 @@ function projectPath(root, cwd) {
|
|
|
1874
2028
|
function globalPath(root, home) {
|
|
1875
2029
|
if (!root) return void 0;
|
|
1876
2030
|
if (root === "~") return home;
|
|
1877
|
-
if (root.startsWith("~/")) return
|
|
2031
|
+
if (root.startsWith("~/")) return join14(home, root.slice(2));
|
|
1878
2032
|
return root;
|
|
1879
2033
|
}
|
|
1880
2034
|
function makeAgent(spec) {
|
|
@@ -2261,7 +2415,7 @@ async function init(options = {}) {
|
|
|
2261
2415
|
}
|
|
2262
2416
|
async function maybeOfferInit(options = {}) {
|
|
2263
2417
|
if (options.json || options.yes || !process.stdin.isTTY) return;
|
|
2264
|
-
if (
|
|
2418
|
+
if (existsSync13(getConfigPath())) return;
|
|
2265
2419
|
console.log("[skill-forge] No config found \u2014 run `skill-forge init` to detect your agents and set gate targets.");
|
|
2266
2420
|
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
2267
2421
|
let answer = "";
|
|
@@ -2396,11 +2550,13 @@ async function addMcp(source, config, options) {
|
|
|
2396
2550
|
const detail = resolveMcpCandidateDetail(profile);
|
|
2397
2551
|
const serverProfile = {
|
|
2398
2552
|
name: detail.name,
|
|
2399
|
-
|
|
2553
|
+
// version may be absent — promoteMcp refuses the unpinnable npx fallback
|
|
2554
|
+
// rather than writing "npx pkg@unknown"; see deriveServerEntry in promoteMcp.ts.
|
|
2555
|
+
version: detail.version,
|
|
2400
2556
|
envVars: detail.env ? Object.keys(detail.env) : void 0,
|
|
2401
2557
|
candidateEntry: detail.command ? { command: detail.command, args: detail.args, env: detail.env } : void 0
|
|
2402
2558
|
};
|
|
2403
|
-
const mcpTargetPath = options.mcpTarget ?? config.mcpTargets?.[0] ??
|
|
2559
|
+
const mcpTargetPath = options.mcpTarget ?? config.mcpTargets?.[0] ?? join15(process.cwd(), ".mcp.json");
|
|
2404
2560
|
const result = promoteMcp(entry, serverProfile, mcpTargetPath, { force: options.force });
|
|
2405
2561
|
reject(entry);
|
|
2406
2562
|
logOutcome(`[skill-forge] promoted -> ${result.target} (server "${result.serverName}")`, options.json);
|
|
@@ -2434,7 +2590,8 @@ async function addMcp(source, config, options) {
|
|
|
2434
2590
|
suggestedVerb: overlap ? suggestVerbCode(overlap.nearestScore) : null
|
|
2435
2591
|
},
|
|
2436
2592
|
status: "pending",
|
|
2437
|
-
artifactType: "mcp"
|
|
2593
|
+
artifactType: "mcp",
|
|
2594
|
+
capabilities: profile.capabilities
|
|
2438
2595
|
});
|
|
2439
2596
|
if (options.ingest && installedPath) {
|
|
2440
2597
|
runHandoff(config, installedPath);
|
|
@@ -2513,7 +2670,7 @@ async function scan(source, options = {}) {
|
|
|
2513
2670
|
}
|
|
2514
2671
|
|
|
2515
2672
|
// src/commands/list.ts
|
|
2516
|
-
import { join as
|
|
2673
|
+
import { join as join16 } from "path";
|
|
2517
2674
|
async function list() {
|
|
2518
2675
|
await maybeOfferInit();
|
|
2519
2676
|
const config = loadConfig();
|
|
@@ -2526,14 +2683,14 @@ async function list() {
|
|
|
2526
2683
|
}
|
|
2527
2684
|
console.log(`[skill-forge] ${ids.length} held in quarantine (${quarantineDir}):`);
|
|
2528
2685
|
for (const id of ids) {
|
|
2529
|
-
const skillDirs = findSkillDirs(
|
|
2686
|
+
const skillDirs = findSkillDirs(join16(quarantineDir, id));
|
|
2530
2687
|
if (skillDirs.length === 0) {
|
|
2531
2688
|
console.log(` ${id} (no SKILL.md found)`);
|
|
2532
2689
|
continue;
|
|
2533
2690
|
}
|
|
2534
2691
|
const names = skillDirs.map((dir) => {
|
|
2535
2692
|
try {
|
|
2536
|
-
return readFrontmatter(
|
|
2693
|
+
return readFrontmatter(join16(dir, "SKILL.md")).name;
|
|
2537
2694
|
} catch {
|
|
2538
2695
|
return dir;
|
|
2539
2696
|
}
|