@rhize/skill-forge 0.6.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 +10 -3
- package/dist/cli.js +177 -214
- package/dist/cli.js.map +1 -1
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -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,7 +431,7 @@ function appendProvenance(entry, skillsRoot) {
|
|
|
406
431
|
}
|
|
407
432
|
|
|
408
433
|
// src/commands/gatePipeline.ts
|
|
409
|
-
import { readdirSync as
|
|
434
|
+
import { readdirSync as readdirSync5 } from "fs";
|
|
410
435
|
import { join as join11 } from "path";
|
|
411
436
|
|
|
412
437
|
// src/gate/profile.ts
|
|
@@ -416,23 +441,46 @@ import {
|
|
|
416
441
|
existsSync as existsSync8,
|
|
417
442
|
mkdtempSync,
|
|
418
443
|
openSync,
|
|
419
|
-
readFileSync as
|
|
444
|
+
readFileSync as readFileSync6,
|
|
420
445
|
readdirSync as readdirSync3,
|
|
421
446
|
readSync,
|
|
422
447
|
rmSync as rmSync2,
|
|
423
448
|
statSync as statSync4
|
|
424
449
|
} from "fs";
|
|
425
450
|
import { tmpdir } from "os";
|
|
426
|
-
import { basename as basename3, dirname as dirname2, extname as
|
|
451
|
+
import { basename as basename3, dirname as dirname2, extname as extname3, join as join8, resolve as resolve2 } from "path";
|
|
427
452
|
|
|
428
453
|
// src/gate/mcpCapabilities.ts
|
|
429
|
-
import { existsSync as existsSync7, readFileSync as
|
|
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";
|
|
430
459
|
import { extname, join as join6 } from "path";
|
|
431
460
|
var SKIP_DIRS2 = /* @__PURE__ */ new Set([".git", "node_modules", "dist", "build", "__pycache__", ".venv", "venv"]);
|
|
432
|
-
var
|
|
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
|
+
]);
|
|
433
482
|
var MAX_FILE_BYTES = 2 * 1024 * 1024;
|
|
434
|
-
|
|
435
|
-
function* walkSourceFiles(dir) {
|
|
483
|
+
function* walk(dir) {
|
|
436
484
|
let entries;
|
|
437
485
|
try {
|
|
438
486
|
entries = readdirSync2(dir, { withFileTypes: true });
|
|
@@ -443,23 +491,41 @@ function* walkSourceFiles(dir) {
|
|
|
443
491
|
const full = join6(dir, entry.name);
|
|
444
492
|
if (entry.isDirectory()) {
|
|
445
493
|
if (SKIP_DIRS2.has(entry.name)) continue;
|
|
446
|
-
yield*
|
|
447
|
-
} else if (entry.isFile()
|
|
494
|
+
yield* walk(full);
|
|
495
|
+
} else if (entry.isFile()) {
|
|
448
496
|
yield full;
|
|
449
497
|
}
|
|
450
498
|
}
|
|
451
499
|
}
|
|
452
|
-
function
|
|
500
|
+
function readTextFile(file) {
|
|
501
|
+
if (BINARY_EXT.has(extname(file).toLowerCase())) return void 0;
|
|
453
502
|
try {
|
|
454
503
|
if (statSync3(file).size > MAX_FILE_BYTES) return void 0;
|
|
455
|
-
|
|
504
|
+
const content = readFileSync4(file, "utf-8");
|
|
505
|
+
if (content.slice(0, 512).includes("\0")) return void 0;
|
|
506
|
+
return content;
|
|
456
507
|
} catch {
|
|
457
508
|
return void 0;
|
|
458
509
|
}
|
|
459
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;
|
|
460
526
|
function readJsonSafe(file) {
|
|
461
527
|
try {
|
|
462
|
-
return JSON.parse(
|
|
528
|
+
return JSON.parse(readFileSync5(file, "utf-8"));
|
|
463
529
|
} catch {
|
|
464
530
|
return void 0;
|
|
465
531
|
}
|
|
@@ -560,7 +626,7 @@ var MCP_SDK_IMPORT_RE = /@modelcontextprotocol\/sdk/;
|
|
|
560
626
|
function extractMcpCapabilities(packageDir) {
|
|
561
627
|
const buckets = { tools: /* @__PURE__ */ new Set(), resources: /* @__PURE__ */ new Set(), prompts: /* @__PURE__ */ new Set() };
|
|
562
628
|
let hasPackageJsonMarker = false;
|
|
563
|
-
const pkgPath =
|
|
629
|
+
const pkgPath = join7(packageDir, "package.json");
|
|
564
630
|
if (existsSync7(pkgPath)) {
|
|
565
631
|
const pkg2 = readJsonSafe(pkgPath);
|
|
566
632
|
if (pkg2 && typeof pkg2 === "object" && !Array.isArray(pkg2)) {
|
|
@@ -568,7 +634,7 @@ function extractMcpCapabilities(packageDir) {
|
|
|
568
634
|
}
|
|
569
635
|
}
|
|
570
636
|
for (const manifestName of [".mcp.json", "mcp.json"]) {
|
|
571
|
-
const manifestPath =
|
|
637
|
+
const manifestPath = join7(packageDir, manifestName);
|
|
572
638
|
if (!existsSync7(manifestPath)) continue;
|
|
573
639
|
const parsed = readJsonSafe(manifestPath);
|
|
574
640
|
if (parsed !== void 0) {
|
|
@@ -578,8 +644,9 @@ function extractMcpCapabilities(packageDir) {
|
|
|
578
644
|
let sawStdioTransport = false;
|
|
579
645
|
let sawHttpTransport = false;
|
|
580
646
|
let sawMcpSdkImport = false;
|
|
581
|
-
for (const file of
|
|
582
|
-
|
|
647
|
+
for (const file of walk(packageDir)) {
|
|
648
|
+
if (!SOURCE_EXT.has(extname2(file))) continue;
|
|
649
|
+
const text = readTextFile(file);
|
|
583
650
|
if (text === void 0) continue;
|
|
584
651
|
captureRegistrations(text, buckets);
|
|
585
652
|
extractFromListHandlers(text, buckets);
|
|
@@ -659,7 +726,7 @@ function profileSkill(inputPath) {
|
|
|
659
726
|
return profileSkillDir(p);
|
|
660
727
|
}
|
|
661
728
|
function classifyInput(p) {
|
|
662
|
-
if (existsSync8(
|
|
729
|
+
if (existsSync8(join8(p, ".claude-plugin", "plugin.json"))) return "plugin";
|
|
663
730
|
if (detectArtifactType(p) === "mcp") return "mcp";
|
|
664
731
|
return "skill";
|
|
665
732
|
}
|
|
@@ -667,10 +734,10 @@ var MCP_SDK_DEP2 = "@modelcontextprotocol/sdk";
|
|
|
667
734
|
var MCP_KEYWORD2 = "mcp-server";
|
|
668
735
|
var MCP_BIN_PREFIX2 = /^mcp-/i;
|
|
669
736
|
function readPackageJsonSafe(root) {
|
|
670
|
-
const pkgPath =
|
|
737
|
+
const pkgPath = join8(root, "package.json");
|
|
671
738
|
if (!existsSync8(pkgPath)) return void 0;
|
|
672
739
|
try {
|
|
673
|
-
return JSON.parse(
|
|
740
|
+
return JSON.parse(readFileSync6(pkgPath, "utf-8"));
|
|
674
741
|
} catch {
|
|
675
742
|
return void 0;
|
|
676
743
|
}
|
|
@@ -691,7 +758,7 @@ function hasMcpPackageMarkers(pkg2) {
|
|
|
691
758
|
return false;
|
|
692
759
|
}
|
|
693
760
|
function detectArtifactType(root) {
|
|
694
|
-
if (existsSync8(
|
|
761
|
+
if (existsSync8(join8(root, ".mcp.json")) || existsSync8(join8(root, "mcp.json"))) return "mcp";
|
|
695
762
|
const pkg2 = readPackageJsonSafe(root);
|
|
696
763
|
if (pkg2 && hasMcpPackageMarkers(pkg2)) return "mcp";
|
|
697
764
|
return "skill";
|
|
@@ -700,8 +767,8 @@ function resolveSkillMd(inputPath) {
|
|
|
700
767
|
let currentPath = inputPath;
|
|
701
768
|
let tmpDir;
|
|
702
769
|
const initialStat = statSync4(currentPath);
|
|
703
|
-
if (
|
|
704
|
-
tmpDir = mkdtempSync(
|
|
770
|
+
if (extname3(currentPath) === ".skill" || initialStat.isFile() && isZipFile(currentPath)) {
|
|
771
|
+
tmpDir = mkdtempSync(join8(tmpdir(), "forge_"));
|
|
705
772
|
extractZip(currentPath, tmpDir);
|
|
706
773
|
currentPath = tmpDir;
|
|
707
774
|
}
|
|
@@ -710,7 +777,7 @@ function resolveSkillMd(inputPath) {
|
|
|
710
777
|
return { skillMd: currentPath, root: dirname2(currentPath), tmpDir };
|
|
711
778
|
}
|
|
712
779
|
if (stat.isDirectory()) {
|
|
713
|
-
const direct =
|
|
780
|
+
const direct = join8(currentPath, "SKILL.md");
|
|
714
781
|
if (existsSync8(direct)) {
|
|
715
782
|
return { skillMd: direct, root: currentPath, tmpDir };
|
|
716
783
|
}
|
|
@@ -736,15 +803,15 @@ function findSkillMdOneLevelDown(dir) {
|
|
|
736
803
|
const matches = /* @__PURE__ */ new Set();
|
|
737
804
|
for (const entry of readdirSync3(dir, { withFileTypes: true })) {
|
|
738
805
|
if (entry.isDirectory()) {
|
|
739
|
-
const candidate =
|
|
806
|
+
const candidate = join8(dir, entry.name, "SKILL.md");
|
|
740
807
|
if (existsSync8(candidate)) matches.add(candidate);
|
|
741
808
|
}
|
|
742
809
|
}
|
|
743
|
-
const skillsDir =
|
|
810
|
+
const skillsDir = join8(dir, "skills");
|
|
744
811
|
if (isDir(skillsDir)) {
|
|
745
812
|
for (const entry of readdirSync3(skillsDir, { withFileTypes: true })) {
|
|
746
813
|
if (entry.isDirectory()) {
|
|
747
|
-
const candidate =
|
|
814
|
+
const candidate = join8(skillsDir, entry.name, "SKILL.md");
|
|
748
815
|
if (existsSync8(candidate)) matches.add(candidate);
|
|
749
816
|
}
|
|
750
817
|
}
|
|
@@ -828,9 +895,9 @@ function detectLicense(root, fm) {
|
|
|
828
895
|
if (fm.license) return fm.license;
|
|
829
896
|
const names = ["LICENSE", "LICENSE.md", "LICENSE.txt", "COPYING", "license"];
|
|
830
897
|
for (const name of names) {
|
|
831
|
-
const p =
|
|
898
|
+
const p = join8(root, name);
|
|
832
899
|
if (existsSync8(p) && statSync4(p).isFile()) {
|
|
833
|
-
const head =
|
|
900
|
+
const head = readFileSync6(p, "utf-8").slice(0, 2e3).toLowerCase();
|
|
834
901
|
for (const [hint, label] of Object.entries(SPDX_HINTS)) {
|
|
835
902
|
if (head.includes(hint)) return label;
|
|
836
903
|
}
|
|
@@ -842,7 +909,7 @@ function detectLicense(root, fm) {
|
|
|
842
909
|
function walkFiles(dir) {
|
|
843
910
|
const out = [];
|
|
844
911
|
for (const entry of readdirSync3(dir, { withFileTypes: true })) {
|
|
845
|
-
const full =
|
|
912
|
+
const full = join8(dir, entry.name);
|
|
846
913
|
if (entry.isDirectory()) {
|
|
847
914
|
out.push(...walkFiles(full));
|
|
848
915
|
} else if (entry.isFile()) {
|
|
@@ -854,7 +921,7 @@ function walkFiles(dir) {
|
|
|
854
921
|
function countResources(root) {
|
|
855
922
|
const resources = {};
|
|
856
923
|
for (const d of RESOURCE_DIRS) {
|
|
857
|
-
const dp =
|
|
924
|
+
const dp = join8(root, d);
|
|
858
925
|
if (isDir(dp)) {
|
|
859
926
|
resources[d] = walkFiles(dp).length;
|
|
860
927
|
}
|
|
@@ -865,11 +932,11 @@ function scanMcpAndImports(text, root) {
|
|
|
865
932
|
const mcpRefRe = /mcp__[a-zA-Z0-9_]+/g;
|
|
866
933
|
const mcpRefs = new Set(text.match(mcpRefRe) ?? []);
|
|
867
934
|
const imports = /* @__PURE__ */ new Set();
|
|
868
|
-
const scriptsDir =
|
|
935
|
+
const scriptsDir = join8(root, "scripts");
|
|
869
936
|
if (isDir(scriptsDir)) {
|
|
870
937
|
for (const f of walkFiles(scriptsDir)) {
|
|
871
938
|
if (!f.endsWith(".py")) continue;
|
|
872
|
-
const ft =
|
|
939
|
+
const ft = readFileSync6(f, "utf-8");
|
|
873
940
|
for (const m2 of ft.match(mcpRefRe) ?? []) mcpRefs.add(m2);
|
|
874
941
|
const importRe = /^\s*(?:import|from)\s+([a-zA-Z0-9_]+)/gm;
|
|
875
942
|
let m;
|
|
@@ -886,7 +953,7 @@ function scanMcpAndImports(text, root) {
|
|
|
886
953
|
function profileSkillDir(inputPath) {
|
|
887
954
|
const { skillMd, root, tmpDir } = resolveSkillMd(inputPath);
|
|
888
955
|
try {
|
|
889
|
-
const text =
|
|
956
|
+
const text = readFileSync6(skillMd, "utf-8");
|
|
890
957
|
const fm = parseFrontmatter(text);
|
|
891
958
|
const bodyLines = (text.match(/\n/g)?.length ?? 0) + 1;
|
|
892
959
|
const headers = Array.from(text.matchAll(/^#{1,3}\s+(.+)$/gm)).map((m) => m[1]);
|
|
@@ -914,19 +981,19 @@ function profileSkillDir(inputPath) {
|
|
|
914
981
|
}
|
|
915
982
|
}
|
|
916
983
|
function profilePlugin(p) {
|
|
917
|
-
const manifestPath =
|
|
984
|
+
const manifestPath = join8(p, ".claude-plugin", "plugin.json");
|
|
918
985
|
let manifest;
|
|
919
986
|
try {
|
|
920
|
-
manifest = JSON.parse(
|
|
987
|
+
manifest = JSON.parse(readFileSync6(manifestPath, "utf-8"));
|
|
921
988
|
} catch (err) {
|
|
922
989
|
throw new Error(`could not parse plugin.json under ${p}: ${err.message}`);
|
|
923
990
|
}
|
|
924
|
-
const skillsDir =
|
|
991
|
+
const skillsDir = join8(p, "skills");
|
|
925
992
|
let skills = [];
|
|
926
993
|
if (isDir(skillsDir)) {
|
|
927
|
-
skills = readdirSync3(skillsDir, { withFileTypes: true }).filter((d) => d.isDirectory() && existsSync8(
|
|
994
|
+
skills = readdirSync3(skillsDir, { withFileTypes: true }).filter((d) => d.isDirectory() && existsSync8(join8(skillsDir, d.name, "SKILL.md"))).map((d) => d.name).sort();
|
|
928
995
|
}
|
|
929
|
-
const commandsDir =
|
|
996
|
+
const commandsDir = join8(p, "commands");
|
|
930
997
|
const commands = isDir(commandsDir) ? readdirSync3(commandsDir).filter((f) => f.endsWith(".md")).length : 0;
|
|
931
998
|
const mc = manifest.mcpServers ?? manifest.mcp_servers ?? {};
|
|
932
999
|
const mcpServers = typeof mc === "object" && mc !== null ? Object.keys(mc).sort() : [];
|
|
@@ -944,11 +1011,11 @@ function profilePlugin(p) {
|
|
|
944
1011
|
};
|
|
945
1012
|
}
|
|
946
1013
|
function profileMcp(p) {
|
|
947
|
-
const explicitConfig = existsSync8(
|
|
1014
|
+
const explicitConfig = existsSync8(join8(p, ".mcp.json")) ? join8(p, ".mcp.json") : existsSync8(join8(p, "mcp.json")) ? join8(p, "mcp.json") : void 0;
|
|
948
1015
|
if (explicitConfig) {
|
|
949
1016
|
let cfg;
|
|
950
1017
|
try {
|
|
951
|
-
cfg = JSON.parse(
|
|
1018
|
+
cfg = JSON.parse(readFileSync6(explicitConfig, "utf-8"));
|
|
952
1019
|
} catch (err) {
|
|
953
1020
|
throw new Error(`could not parse MCP config ${explicitConfig}: ${err.message}`);
|
|
954
1021
|
}
|
|
@@ -964,10 +1031,10 @@ function profileMcp(p) {
|
|
|
964
1031
|
capabilities: extractMcpCapabilities(p)
|
|
965
1032
|
};
|
|
966
1033
|
}
|
|
967
|
-
const pkgPath =
|
|
1034
|
+
const pkgPath = join8(p, "package.json");
|
|
968
1035
|
let pkg2;
|
|
969
1036
|
try {
|
|
970
|
-
pkg2 = JSON.parse(
|
|
1037
|
+
pkg2 = JSON.parse(readFileSync6(pkgPath, "utf-8"));
|
|
971
1038
|
} catch (err) {
|
|
972
1039
|
throw new Error(`could not parse package.json ${pkgPath}: ${err.message}`);
|
|
973
1040
|
}
|
|
@@ -984,16 +1051,11 @@ function profileMcp(p) {
|
|
|
984
1051
|
}
|
|
985
1052
|
|
|
986
1053
|
// src/gate/safety.ts
|
|
987
|
-
import {
|
|
1054
|
+
import { statSync as statSync5 } from "fs";
|
|
988
1055
|
import { execFile } from "child_process";
|
|
989
|
-
import { delimiter,
|
|
1056
|
+
import { delimiter, join as join9 } from "path";
|
|
990
1057
|
import { promisify } from "util";
|
|
991
1058
|
var execFileAsync = promisify(execFile);
|
|
992
|
-
var BLOCK_SEVERITIES_BY_STRICTNESS = {
|
|
993
|
-
"block-high": /* @__PURE__ */ new Set(["HIGH", "CRITICAL"]),
|
|
994
|
-
"block-medium": /* @__PURE__ */ new Set(["MEDIUM", "HIGH", "CRITICAL"]),
|
|
995
|
-
"warn-only": /* @__PURE__ */ new Set()
|
|
996
|
-
};
|
|
997
1059
|
var DENY_RULES = [
|
|
998
1060
|
{
|
|
999
1061
|
rule: "curl-pipe-shell",
|
|
@@ -1056,57 +1118,6 @@ var DENY_RULES = [
|
|
|
1056
1118
|
message: "invokes sudo \u2014 review for unnecessary privilege escalation"
|
|
1057
1119
|
}
|
|
1058
1120
|
];
|
|
1059
|
-
var SKIP_DIRS3 = /* @__PURE__ */ new Set([".git", "node_modules", "dist", "build", "__pycache__", ".venv", "venv"]);
|
|
1060
|
-
var BINARY_EXT = /* @__PURE__ */ new Set([
|
|
1061
|
-
".png",
|
|
1062
|
-
".jpg",
|
|
1063
|
-
".jpeg",
|
|
1064
|
-
".gif",
|
|
1065
|
-
".webp",
|
|
1066
|
-
".ico",
|
|
1067
|
-
".pdf",
|
|
1068
|
-
".zip",
|
|
1069
|
-
".tar",
|
|
1070
|
-
".gz",
|
|
1071
|
-
".woff",
|
|
1072
|
-
".woff2",
|
|
1073
|
-
".ttf",
|
|
1074
|
-
".eot",
|
|
1075
|
-
".mp4",
|
|
1076
|
-
".mp3",
|
|
1077
|
-
".wasm",
|
|
1078
|
-
".bin",
|
|
1079
|
-
".exe"
|
|
1080
|
-
]);
|
|
1081
|
-
var MAX_FILE_BYTES2 = 2 * 1024 * 1024;
|
|
1082
|
-
function* walk(dir) {
|
|
1083
|
-
let entries;
|
|
1084
|
-
try {
|
|
1085
|
-
entries = readdirSync4(dir, { withFileTypes: true });
|
|
1086
|
-
} catch {
|
|
1087
|
-
return;
|
|
1088
|
-
}
|
|
1089
|
-
for (const entry of entries) {
|
|
1090
|
-
const full = join8(dir, entry.name);
|
|
1091
|
-
if (entry.isDirectory()) {
|
|
1092
|
-
if (SKIP_DIRS3.has(entry.name)) continue;
|
|
1093
|
-
yield* walk(full);
|
|
1094
|
-
} else if (entry.isFile()) {
|
|
1095
|
-
yield full;
|
|
1096
|
-
}
|
|
1097
|
-
}
|
|
1098
|
-
}
|
|
1099
|
-
function readTextFile(file) {
|
|
1100
|
-
if (BINARY_EXT.has(extname3(file).toLowerCase())) return void 0;
|
|
1101
|
-
try {
|
|
1102
|
-
if (statSync5(file).size > MAX_FILE_BYTES2) return void 0;
|
|
1103
|
-
const content = readFileSync6(file, "utf-8");
|
|
1104
|
-
if (content.slice(0, 512).includes("\0")) return void 0;
|
|
1105
|
-
return content;
|
|
1106
|
-
} catch {
|
|
1107
|
-
return void 0;
|
|
1108
|
-
}
|
|
1109
|
-
}
|
|
1110
1121
|
function scanStaticRules(targetPath) {
|
|
1111
1122
|
const findings = [];
|
|
1112
1123
|
let root;
|
|
@@ -1141,7 +1152,7 @@ function which(cmd) {
|
|
|
1141
1152
|
const exts = process.platform === "win32" ? (process.env.PATHEXT ?? ".EXE;.CMD;.BAT").split(";") : [""];
|
|
1142
1153
|
for (const dir of dirs) {
|
|
1143
1154
|
for (const ext of exts) {
|
|
1144
|
-
const candidate =
|
|
1155
|
+
const candidate = join9(dir, cmd + ext);
|
|
1145
1156
|
try {
|
|
1146
1157
|
if (statSync5(candidate).isFile()) return candidate;
|
|
1147
1158
|
} catch {
|
|
@@ -1175,20 +1186,14 @@ async function scanWithSkillSpector(targetPath) {
|
|
|
1175
1186
|
return [];
|
|
1176
1187
|
}
|
|
1177
1188
|
}
|
|
1178
|
-
function verdictFor(findings, strictness) {
|
|
1179
|
-
const blockSeverities = BLOCK_SEVERITIES_BY_STRICTNESS[strictness];
|
|
1180
|
-
if (findings.some((f) => blockSeverities.has(f.severity))) return "block";
|
|
1181
|
-
if (findings.length > 0) return "warn";
|
|
1182
|
-
return "pass";
|
|
1183
|
-
}
|
|
1184
1189
|
async function scanSafety(targetPath, strictness = DEFAULT_STRICTNESS) {
|
|
1185
1190
|
const findings = [...scanStaticRules(targetPath), ...await scanWithSkillSpector(targetPath)];
|
|
1186
1191
|
return { findings, verdict: verdictFor(findings, strictness) };
|
|
1187
1192
|
}
|
|
1188
1193
|
|
|
1189
1194
|
// src/gate/overlap.ts
|
|
1190
|
-
import { existsSync as existsSync9, readFileSync as readFileSync7, readdirSync as
|
|
1191
|
-
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";
|
|
1192
1197
|
var STOPWORDS = new Set(
|
|
1193
1198
|
`
|
|
1194
1199
|
a an the and or of to for in on with use used using when whether this that these those is are be
|
|
@@ -1307,25 +1312,25 @@ function listSubdirs(dir) {
|
|
|
1307
1312
|
if (!existsSync9(dir)) return [];
|
|
1308
1313
|
let entries;
|
|
1309
1314
|
try {
|
|
1310
|
-
entries =
|
|
1315
|
+
entries = readdirSync4(dir, { withFileTypes: true });
|
|
1311
1316
|
} catch {
|
|
1312
1317
|
return [];
|
|
1313
1318
|
}
|
|
1314
|
-
return entries.filter((e) => e.isDirectory()).map((e) =>
|
|
1319
|
+
return entries.filter((e) => e.isDirectory()).map((e) => join10(dir, e.name));
|
|
1315
1320
|
}
|
|
1316
1321
|
function findSkillMdFiles(root) {
|
|
1317
1322
|
const found = /* @__PURE__ */ new Set();
|
|
1318
1323
|
for (const dir of listSubdirs(root)) {
|
|
1319
|
-
const candidate =
|
|
1324
|
+
const candidate = join10(dir, "SKILL.md");
|
|
1320
1325
|
if (existsSync9(candidate)) found.add(resolve3(candidate));
|
|
1321
1326
|
}
|
|
1322
|
-
for (const dir of listSubdirs(
|
|
1323
|
-
const candidate =
|
|
1327
|
+
for (const dir of listSubdirs(join10(root, "skills"))) {
|
|
1328
|
+
const candidate = join10(dir, "SKILL.md");
|
|
1324
1329
|
if (existsSync9(candidate)) found.add(resolve3(candidate));
|
|
1325
1330
|
}
|
|
1326
1331
|
for (const topDir of listSubdirs(root)) {
|
|
1327
|
-
for (const dir of listSubdirs(
|
|
1328
|
-
const candidate =
|
|
1332
|
+
for (const dir of listSubdirs(join10(topDir, "skills"))) {
|
|
1333
|
+
const candidate = join10(dir, "SKILL.md");
|
|
1329
1334
|
if (existsSync9(candidate)) found.add(resolve3(candidate));
|
|
1330
1335
|
}
|
|
1331
1336
|
}
|
|
@@ -1338,7 +1343,7 @@ function scanOverlap(candidatePath, skillsRoot) {
|
|
|
1338
1343
|
}
|
|
1339
1344
|
let cpath = resolve3(expandHome(candidatePath));
|
|
1340
1345
|
if (isDir(cpath)) {
|
|
1341
|
-
cpath =
|
|
1346
|
+
cpath = join10(cpath, "SKILL.md");
|
|
1342
1347
|
}
|
|
1343
1348
|
if (!existsSync9(cpath)) {
|
|
1344
1349
|
throw new Error(`candidate SKILL.md not found: ${cpath}`);
|
|
@@ -1364,59 +1369,8 @@ function scanOverlap(candidatePath, skillsRoot) {
|
|
|
1364
1369
|
}
|
|
1365
1370
|
|
|
1366
1371
|
// src/gate/mcpSafety.ts
|
|
1367
|
-
import {
|
|
1368
|
-
import { basename as basename5, extname as extname4
|
|
1369
|
-
var SKIP_DIRS4 = /* @__PURE__ */ new Set([".git", "node_modules", "dist", "build", "__pycache__", ".venv", "venv"]);
|
|
1370
|
-
var BINARY_EXT2 = /* @__PURE__ */ new Set([
|
|
1371
|
-
".png",
|
|
1372
|
-
".jpg",
|
|
1373
|
-
".jpeg",
|
|
1374
|
-
".gif",
|
|
1375
|
-
".webp",
|
|
1376
|
-
".ico",
|
|
1377
|
-
".pdf",
|
|
1378
|
-
".zip",
|
|
1379
|
-
".tar",
|
|
1380
|
-
".gz",
|
|
1381
|
-
".woff",
|
|
1382
|
-
".woff2",
|
|
1383
|
-
".ttf",
|
|
1384
|
-
".eot",
|
|
1385
|
-
".mp4",
|
|
1386
|
-
".mp3",
|
|
1387
|
-
".wasm",
|
|
1388
|
-
".bin",
|
|
1389
|
-
".exe"
|
|
1390
|
-
]);
|
|
1391
|
-
var MAX_FILE_BYTES3 = 2 * 1024 * 1024;
|
|
1392
|
-
function* walk2(dir) {
|
|
1393
|
-
let entries;
|
|
1394
|
-
try {
|
|
1395
|
-
entries = readdirSync6(dir, { withFileTypes: true });
|
|
1396
|
-
} catch {
|
|
1397
|
-
return;
|
|
1398
|
-
}
|
|
1399
|
-
for (const entry of entries) {
|
|
1400
|
-
const full = join10(dir, entry.name);
|
|
1401
|
-
if (entry.isDirectory()) {
|
|
1402
|
-
if (SKIP_DIRS4.has(entry.name)) continue;
|
|
1403
|
-
yield* walk2(full);
|
|
1404
|
-
} else if (entry.isFile()) {
|
|
1405
|
-
yield full;
|
|
1406
|
-
}
|
|
1407
|
-
}
|
|
1408
|
-
}
|
|
1409
|
-
function readTextFile2(file) {
|
|
1410
|
-
if (BINARY_EXT2.has(extname4(file).toLowerCase())) return void 0;
|
|
1411
|
-
try {
|
|
1412
|
-
if (statSync6(file).size > MAX_FILE_BYTES3) return void 0;
|
|
1413
|
-
const content = readFileSync8(file, "utf-8");
|
|
1414
|
-
if (content.slice(0, 512).includes("\0")) return void 0;
|
|
1415
|
-
return content;
|
|
1416
|
-
} catch {
|
|
1417
|
-
return void 0;
|
|
1418
|
-
}
|
|
1419
|
-
}
|
|
1372
|
+
import { statSync as statSync6 } from "fs";
|
|
1373
|
+
import { basename as basename5, extname as extname4 } from "path";
|
|
1420
1374
|
function isConfigOrExampleFile(file) {
|
|
1421
1375
|
const base = basename5(file).toLowerCase();
|
|
1422
1376
|
if (base.startsWith(".env")) return true;
|
|
@@ -1446,7 +1400,18 @@ function looksHighEntropy(value) {
|
|
|
1446
1400
|
}
|
|
1447
1401
|
function isPinnedPackageSpec(spec) {
|
|
1448
1402
|
const withoutScope = spec.startsWith("@") ? spec.slice(spec.indexOf("/") + 1) : spec;
|
|
1449
|
-
|
|
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;
|
|
1450
1415
|
}
|
|
1451
1416
|
function collectServerNodes(node, out) {
|
|
1452
1417
|
if (Array.isArray(node)) {
|
|
@@ -1475,7 +1440,7 @@ function scanJsonConfig(file, parsed) {
|
|
|
1475
1440
|
for (const node of nodes) {
|
|
1476
1441
|
const command = typeof node.command === "string" ? node.command : void 0;
|
|
1477
1442
|
const args = Array.isArray(node.args) ? node.args.filter((a) => typeof a === "string") : [];
|
|
1478
|
-
if (command
|
|
1443
|
+
if (command && isNpxCommand(command)) {
|
|
1479
1444
|
const flagIdx = args.findIndex((a) => a === "-y" || a === "--yes");
|
|
1480
1445
|
if (flagIdx !== -1) {
|
|
1481
1446
|
const pkgSpec = args.slice(flagIdx + 1).find((a) => !a.startsWith("-"));
|
|
@@ -1484,7 +1449,7 @@ function scanJsonConfig(file, parsed) {
|
|
|
1484
1449
|
severity: "MEDIUM",
|
|
1485
1450
|
rule: "mcp-unpinned-npx",
|
|
1486
1451
|
file,
|
|
1487
|
-
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`
|
|
1488
1453
|
});
|
|
1489
1454
|
}
|
|
1490
1455
|
}
|
|
@@ -1523,9 +1488,22 @@ function scanJsonConfig(file, parsed) {
|
|
|
1523
1488
|
}
|
|
1524
1489
|
return findings;
|
|
1525
1490
|
}
|
|
1526
|
-
var NPX_SHELL_RE = /\bnpx
|
|
1491
|
+
var NPX_SHELL_RE = /\bnpx(?:\.cmd|\.exe)?\s+(?:-y|--yes)\s+((?:@[\w.-]+\/)?[\w.-]+)(@[\w.-]+)?/gi;
|
|
1527
1492
|
var DANGEROUS_FLAG_RE = /--dangerously-[\w-]+|--no-sandbox\b/g;
|
|
1528
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
|
+
}
|
|
1529
1507
|
function scanTextFile(file, content, allowCredentialCheck) {
|
|
1530
1508
|
const findings = [];
|
|
1531
1509
|
const lines = content.split("\n");
|
|
@@ -1536,8 +1514,8 @@ function scanTextFile(file, content, allowCredentialCheck) {
|
|
|
1536
1514
|
let m;
|
|
1537
1515
|
while ((m = NPX_SHELL_RE.exec(line)) !== null) {
|
|
1538
1516
|
const pkgSpec = m[1];
|
|
1539
|
-
const
|
|
1540
|
-
if (!
|
|
1517
|
+
const versionSuffix = m[2] ?? "";
|
|
1518
|
+
if (!isPinnedPackageSpec(pkgSpec + versionSuffix)) {
|
|
1541
1519
|
findings.push({
|
|
1542
1520
|
severity: "MEDIUM",
|
|
1543
1521
|
rule: "mcp-unpinned-npx",
|
|
@@ -1558,24 +1536,18 @@ function scanTextFile(file, content, allowCredentialCheck) {
|
|
|
1558
1536
|
if (allowCredentialCheck) {
|
|
1559
1537
|
CREDENTIAL_ASSIGNMENT_RE.lastIndex = 0;
|
|
1560
1538
|
while ((m = CREDENTIAL_ASSIGNMENT_RE.exec(line)) !== null) {
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
severity: "HIGH",
|
|
1567
|
-
rule: "mcp-inline-credential-value",
|
|
1568
|
-
file,
|
|
1569
|
-
detail: `line ${lineNo}: inline credential value assigned to ${varName}`
|
|
1570
|
-
});
|
|
1571
|
-
}
|
|
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]);
|
|
1572
1544
|
}
|
|
1573
1545
|
}
|
|
1574
1546
|
}
|
|
1575
1547
|
return findings;
|
|
1576
1548
|
}
|
|
1577
1549
|
function scanFileForMcpFindings(file) {
|
|
1578
|
-
const content =
|
|
1550
|
+
const content = readTextFile(file);
|
|
1579
1551
|
if (content === void 0) return [];
|
|
1580
1552
|
if (extname4(file).toLowerCase() === ".json") {
|
|
1581
1553
|
const parsed = tryParseJson(content);
|
|
@@ -1590,40 +1562,29 @@ function scanMcpFindings(targetPath) {
|
|
|
1590
1562
|
} catch {
|
|
1591
1563
|
return [];
|
|
1592
1564
|
}
|
|
1593
|
-
const files = root.isDirectory() ? Array.from(
|
|
1565
|
+
const files = root.isDirectory() ? Array.from(walk(targetPath)) : [targetPath];
|
|
1594
1566
|
const findings = [];
|
|
1595
1567
|
for (const file of files) {
|
|
1596
1568
|
findings.push(...scanFileForMcpFindings(file));
|
|
1597
1569
|
}
|
|
1598
1570
|
return findings;
|
|
1599
1571
|
}
|
|
1600
|
-
var BLOCK_SEVERITIES_BY_STRICTNESS2 = {
|
|
1601
|
-
"block-high": /* @__PURE__ */ new Set(["HIGH", "CRITICAL"]),
|
|
1602
|
-
"block-medium": /* @__PURE__ */ new Set(["MEDIUM", "HIGH", "CRITICAL"]),
|
|
1603
|
-
"warn-only": /* @__PURE__ */ new Set()
|
|
1604
|
-
};
|
|
1605
|
-
function verdictFor2(findings, strictness) {
|
|
1606
|
-
const blockSeverities = BLOCK_SEVERITIES_BY_STRICTNESS2[strictness];
|
|
1607
|
-
if (findings.some((f) => blockSeverities.has(f.severity))) return "block";
|
|
1608
|
-
if (findings.length > 0) return "warn";
|
|
1609
|
-
return "pass";
|
|
1610
|
-
}
|
|
1611
1572
|
async function mcpScan(targetPath, strictness = DEFAULT_STRICTNESS) {
|
|
1612
1573
|
const base = await scanSafety(targetPath, strictness);
|
|
1613
1574
|
const mcpFindings = scanMcpFindings(targetPath);
|
|
1614
1575
|
const findings = [...base.findings, ...mcpFindings];
|
|
1615
|
-
return { findings, verdict:
|
|
1576
|
+
return { findings, verdict: verdictFor(findings, strictness) };
|
|
1616
1577
|
}
|
|
1617
1578
|
|
|
1618
1579
|
// src/gate/mcpOverlap.ts
|
|
1619
|
-
import { existsSync as existsSync10, readFileSync as
|
|
1580
|
+
import { existsSync as existsSync10, readFileSync as readFileSync8 } from "fs";
|
|
1620
1581
|
import { resolve as resolve4 } from "path";
|
|
1621
1582
|
function readTargetServers(targetPath) {
|
|
1622
1583
|
const path = resolve4(expandHome(targetPath));
|
|
1623
1584
|
if (!existsSync10(path)) return [];
|
|
1624
1585
|
let cfg;
|
|
1625
1586
|
try {
|
|
1626
|
-
cfg = JSON.parse(
|
|
1587
|
+
cfg = JSON.parse(readFileSync8(path, "utf-8"));
|
|
1627
1588
|
} catch {
|
|
1628
1589
|
return [];
|
|
1629
1590
|
}
|
|
@@ -1908,7 +1869,7 @@ function resolveMcpCandidatePath(entry) {
|
|
|
1908
1869
|
if (entry.packageDir) return entry.packageDir;
|
|
1909
1870
|
let children = [];
|
|
1910
1871
|
try {
|
|
1911
|
-
children =
|
|
1872
|
+
children = readdirSync5(entry.quarantinePath, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => d.name);
|
|
1912
1873
|
} catch {
|
|
1913
1874
|
}
|
|
1914
1875
|
return children.length === 1 ? join11(entry.quarantinePath, children[0]) : entry.quarantinePath;
|
|
@@ -1942,7 +1903,7 @@ async function runGate(candidatePath, skillsRoot, {
|
|
|
1942
1903
|
|
|
1943
1904
|
// src/queue.ts
|
|
1944
1905
|
import { randomUUID as randomUUID2 } from "crypto";
|
|
1945
|
-
import { existsSync as existsSync11, mkdirSync as mkdirSync5, readFileSync as
|
|
1906
|
+
import { existsSync as existsSync11, mkdirSync as mkdirSync5, readFileSync as readFileSync9, writeFileSync as writeFileSync4 } from "fs";
|
|
1946
1907
|
import { join as join12 } from "path";
|
|
1947
1908
|
function getQueuePath() {
|
|
1948
1909
|
return join12(getHomeDir(), "queue.json");
|
|
@@ -1952,7 +1913,7 @@ function readQueue() {
|
|
|
1952
1913
|
if (!existsSync11(path)) {
|
|
1953
1914
|
return { version: 1, entries: [] };
|
|
1954
1915
|
}
|
|
1955
|
-
const raw =
|
|
1916
|
+
const raw = readFileSync9(path, "utf-8");
|
|
1956
1917
|
const parsed = JSON.parse(raw);
|
|
1957
1918
|
return { version: 1, entries: parsed.entries ?? [] };
|
|
1958
1919
|
}
|
|
@@ -2589,7 +2550,9 @@ async function addMcp(source, config, options) {
|
|
|
2589
2550
|
const detail = resolveMcpCandidateDetail(profile);
|
|
2590
2551
|
const serverProfile = {
|
|
2591
2552
|
name: detail.name,
|
|
2592
|
-
|
|
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,
|
|
2593
2556
|
envVars: detail.env ? Object.keys(detail.env) : void 0,
|
|
2594
2557
|
candidateEntry: detail.command ? { command: detail.command, args: detail.args, env: detail.env } : void 0
|
|
2595
2558
|
};
|