@rhize/skill-forge 0.7.0 → 0.7.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 +8 -0
- package/dist/cli.js +259 -175
- package/dist/cli.js.map +1 -1
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -5,7 +5,7 @@ import { Command } from "commander";
|
|
|
5
5
|
import { createRequire } from "module";
|
|
6
6
|
|
|
7
7
|
// src/commands/add.ts
|
|
8
|
-
import { join as
|
|
8
|
+
import { join as join16 } from "path";
|
|
9
9
|
|
|
10
10
|
// src/config.ts
|
|
11
11
|
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
|
|
@@ -295,16 +295,132 @@ function moveDir(src, dest) {
|
|
|
295
295
|
}
|
|
296
296
|
|
|
297
297
|
// src/install/promoteMcp.ts
|
|
298
|
-
import { existsSync as existsSync5, mkdirSync as mkdirSync4, readFileSync as
|
|
298
|
+
import { existsSync as existsSync5, mkdirSync as mkdirSync4, readFileSync as readFileSync3, writeFileSync as writeFileSync2 } from "fs";
|
|
299
299
|
import { dirname } from "path";
|
|
300
|
+
|
|
301
|
+
// src/gate/scanShared.ts
|
|
302
|
+
import { readFileSync as readFileSync2, readdirSync as readdirSync2, statSync as statSync2 } from "fs";
|
|
303
|
+
import { extname, join as join5 } from "path";
|
|
304
|
+
var SKIP_DIRS2 = /* @__PURE__ */ new Set([".git", "node_modules", "dist", "build", "__pycache__", ".venv", "venv"]);
|
|
305
|
+
var BINARY_EXT = /* @__PURE__ */ new Set([
|
|
306
|
+
".png",
|
|
307
|
+
".jpg",
|
|
308
|
+
".jpeg",
|
|
309
|
+
".gif",
|
|
310
|
+
".webp",
|
|
311
|
+
".ico",
|
|
312
|
+
".pdf",
|
|
313
|
+
".zip",
|
|
314
|
+
".tar",
|
|
315
|
+
".gz",
|
|
316
|
+
".woff",
|
|
317
|
+
".woff2",
|
|
318
|
+
".ttf",
|
|
319
|
+
".eot",
|
|
320
|
+
".mp4",
|
|
321
|
+
".mp3",
|
|
322
|
+
".wasm",
|
|
323
|
+
".bin",
|
|
324
|
+
".exe"
|
|
325
|
+
]);
|
|
326
|
+
var MAX_FILE_BYTES = 2 * 1024 * 1024;
|
|
327
|
+
function* walk(dir) {
|
|
328
|
+
let entries;
|
|
329
|
+
try {
|
|
330
|
+
entries = readdirSync2(dir, { withFileTypes: true });
|
|
331
|
+
} catch {
|
|
332
|
+
return;
|
|
333
|
+
}
|
|
334
|
+
for (const entry of entries) {
|
|
335
|
+
const full = join5(dir, entry.name);
|
|
336
|
+
if (entry.isDirectory()) {
|
|
337
|
+
if (SKIP_DIRS2.has(entry.name)) continue;
|
|
338
|
+
yield* walk(full);
|
|
339
|
+
} else if (entry.isFile()) {
|
|
340
|
+
yield full;
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
function readTextFile(file) {
|
|
345
|
+
if (BINARY_EXT.has(extname(file).toLowerCase())) return void 0;
|
|
346
|
+
try {
|
|
347
|
+
if (statSync2(file).size > MAX_FILE_BYTES) return void 0;
|
|
348
|
+
const content = readFileSync2(file, "utf-8");
|
|
349
|
+
if (content.slice(0, 512).includes("\0")) return void 0;
|
|
350
|
+
return content;
|
|
351
|
+
} catch {
|
|
352
|
+
return void 0;
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
var BLOCK_SEVERITIES_BY_STRICTNESS = {
|
|
356
|
+
"block-high": /* @__PURE__ */ new Set(["HIGH", "CRITICAL"]),
|
|
357
|
+
"block-medium": /* @__PURE__ */ new Set(["MEDIUM", "HIGH", "CRITICAL"]),
|
|
358
|
+
"warn-only": /* @__PURE__ */ new Set()
|
|
359
|
+
};
|
|
360
|
+
function verdictFor(findings, strictness) {
|
|
361
|
+
const blockSeverities = BLOCK_SEVERITIES_BY_STRICTNESS[strictness];
|
|
362
|
+
if (findings.some((f) => blockSeverities.has(f.severity))) return "block";
|
|
363
|
+
if (findings.length > 0) return "warn";
|
|
364
|
+
return "pass";
|
|
365
|
+
}
|
|
366
|
+
function formatSafetyFinding(f) {
|
|
367
|
+
return `${f.severity} [${f.rule}] ${f.file}: ${f.detail}`;
|
|
368
|
+
}
|
|
369
|
+
function isPinnedPackageSpec(spec) {
|
|
370
|
+
const withoutScope = spec.startsWith("@") ? spec.slice(spec.indexOf("/") + 1) : spec;
|
|
371
|
+
const atIdx = withoutScope.lastIndexOf("@");
|
|
372
|
+
if (atIdx <= 0) return false;
|
|
373
|
+
return /^\d/.test(withoutScope.slice(atIdx + 1));
|
|
374
|
+
}
|
|
375
|
+
function isNpxCommand(command) {
|
|
376
|
+
const base = command.split(/[\\/]/).pop() ?? command;
|
|
377
|
+
return base.replace(/\.(cmd|exe)$/i, "").toLowerCase() === "npx";
|
|
378
|
+
}
|
|
379
|
+
function packageSpecBase(spec) {
|
|
380
|
+
const scopeEnd = spec.startsWith("@") ? spec.indexOf("/") + 1 : 0;
|
|
381
|
+
const atIdx = spec.lastIndexOf("@");
|
|
382
|
+
return atIdx > scopeEnd ? spec.slice(0, atIdx) : spec;
|
|
383
|
+
}
|
|
384
|
+
var NPX_VALUE_FLAGS = /* @__PURE__ */ new Set(["p", "package", "c", "call", "cache", "userconfig", "shell", "n", "node-arg", "npm", "registry"]);
|
|
385
|
+
var NPX_PACKAGE_SPEC_FLAGS = /* @__PURE__ */ new Set(["p", "package"]);
|
|
386
|
+
function collectNpxPackageSpecs(args) {
|
|
387
|
+
const packageFlagSpecs = [];
|
|
388
|
+
let positionalSpec;
|
|
389
|
+
for (let i = 0; i < args.length; i++) {
|
|
390
|
+
const arg = args[i];
|
|
391
|
+
if (arg === "--") break;
|
|
392
|
+
if (arg.startsWith("-")) {
|
|
393
|
+
const eqIdx = arg.indexOf("=");
|
|
394
|
+
const key = (eqIdx === -1 ? arg : arg.slice(0, eqIdx)).replace(/^-+/, "");
|
|
395
|
+
const attached = eqIdx === -1 ? void 0 : arg.slice(eqIdx + 1);
|
|
396
|
+
if (NPX_VALUE_FLAGS.has(key)) {
|
|
397
|
+
let value = attached;
|
|
398
|
+
if (value === void 0) {
|
|
399
|
+
value = args[i + 1];
|
|
400
|
+
i++;
|
|
401
|
+
}
|
|
402
|
+
if (NPX_PACKAGE_SPEC_FLAGS.has(key) && value !== void 0) {
|
|
403
|
+
packageFlagSpecs.push(value);
|
|
404
|
+
}
|
|
405
|
+
continue;
|
|
406
|
+
}
|
|
407
|
+
continue;
|
|
408
|
+
}
|
|
409
|
+
positionalSpec = arg;
|
|
410
|
+
break;
|
|
411
|
+
}
|
|
412
|
+
return { packageFlagSpecs, positionalSpec };
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
// src/install/promoteMcp.ts
|
|
300
416
|
function promoteMcp(_entry, profile, targetConfigPath, options = {}) {
|
|
301
417
|
const force = options.force ?? false;
|
|
302
418
|
const serverName = profile.name;
|
|
303
|
-
const { command, args, envVarsToFill } = deriveServerEntry(profile);
|
|
419
|
+
const { command, args, envVarsToFill, autoPinned } = deriveServerEntry(profile);
|
|
304
420
|
let config = {};
|
|
305
421
|
let rawExisting;
|
|
306
422
|
if (existsSync5(targetConfigPath)) {
|
|
307
|
-
rawExisting =
|
|
423
|
+
rawExisting = readFileSync3(targetConfigPath, "utf-8");
|
|
308
424
|
try {
|
|
309
425
|
config = JSON.parse(rawExisting);
|
|
310
426
|
} catch (err) {
|
|
@@ -329,7 +445,7 @@ function promoteMcp(_entry, profile, targetConfigPath, options = {}) {
|
|
|
329
445
|
mkdirSync4(dirname(targetConfigPath), { recursive: true });
|
|
330
446
|
writeFileSync2(targetConfigPath, `${JSON.stringify(config, null, 2)}
|
|
331
447
|
`);
|
|
332
|
-
return { target: targetConfigPath, serverName, envVarsToFill, backupPath };
|
|
448
|
+
return { target: targetConfigPath, serverName, envVarsToFill, backupPath, autoPinned };
|
|
333
449
|
}
|
|
334
450
|
function deriveServerEntry(profile) {
|
|
335
451
|
const envNames = new Set(profile.envVars ?? []);
|
|
@@ -337,9 +453,41 @@ function deriveServerEntry(profile) {
|
|
|
337
453
|
for (const key of Object.keys(profile.candidateEntry.env)) envNames.add(key);
|
|
338
454
|
}
|
|
339
455
|
if (profile.candidateEntry) {
|
|
456
|
+
const { command, args = [] } = profile.candidateEntry;
|
|
457
|
+
if (isNpxCommand(command)) {
|
|
458
|
+
const { packageFlagSpecs, positionalSpec } = collectNpxPackageSpecs(args);
|
|
459
|
+
const allSpecs = positionalSpec ? [...packageFlagSpecs, positionalSpec] : packageFlagSpecs;
|
|
460
|
+
const unpinned = allSpecs.filter((spec) => !isPinnedPackageSpec(spec));
|
|
461
|
+
if (unpinned.length > 0) {
|
|
462
|
+
const isSoleUnpinnedPositional = packageFlagSpecs.length === 0 && allSpecs.length === 1 && positionalSpec === unpinned[0];
|
|
463
|
+
if (isSoleUnpinnedPositional) {
|
|
464
|
+
const spec = unpinned[0];
|
|
465
|
+
const specPackage = packageSpecBase(spec);
|
|
466
|
+
if (profile.packageName && specPackage === profile.packageName && profile.version) {
|
|
467
|
+
const to = `${profile.packageName}@${profile.version}`;
|
|
468
|
+
const specIdx = args.indexOf(spec);
|
|
469
|
+
const pinnedArgs = [...args];
|
|
470
|
+
pinnedArgs[specIdx] = to;
|
|
471
|
+
return {
|
|
472
|
+
command,
|
|
473
|
+
args: pinnedArgs,
|
|
474
|
+
envVarsToFill: Array.from(envNames),
|
|
475
|
+
autoPinned: { from: spec, to }
|
|
476
|
+
};
|
|
477
|
+
}
|
|
478
|
+
const reason = profile.packageName && specPackage !== profile.packageName ? `the launch spec names package "${specPackage}", but the candidate's own package.json is "${profile.packageName}" \u2014 pinning it to a version scanned for a different package would be unsafe` : "no version was found in the candidate's own package.json to pin it to";
|
|
479
|
+
throw new UserError(
|
|
480
|
+
`cannot promote MCP server "${profile.name}": its documented launch command "npx ${spec}" is unpinned and can't be safely auto-pinned (${reason}). Pin the spec to an exact version in the candidate's config (e.g. \`${specPackage}@<version>\`) and try again \u2014 there is no override for this.`
|
|
481
|
+
);
|
|
482
|
+
}
|
|
483
|
+
throw new UserError(
|
|
484
|
+
`cannot promote MCP server "${profile.name}": its documented launch command "npx" has unpinned package spec(s) \u2014 ${unpinned.map((s) => `"${s}"`).join(", ")} \u2014 and this shape (a -p/--package dependency, or more than one spec) can't be safely auto-pinned. Pin every spec to an exact version in the candidate's config and try again \u2014 there is no override for this.`
|
|
485
|
+
);
|
|
486
|
+
}
|
|
487
|
+
}
|
|
340
488
|
return {
|
|
341
|
-
command
|
|
342
|
-
args
|
|
489
|
+
command,
|
|
490
|
+
args,
|
|
343
491
|
envVarsToFill: Array.from(envNames)
|
|
344
492
|
};
|
|
345
493
|
}
|
|
@@ -356,18 +504,22 @@ function deriveServerEntry(profile) {
|
|
|
356
504
|
}
|
|
357
505
|
|
|
358
506
|
// src/install/mcpCandidate.ts
|
|
359
|
-
import { readFileSync as
|
|
360
|
-
|
|
507
|
+
import { readFileSync as readFileSync4 } from "fs";
|
|
508
|
+
import { dirname as dirname2, join as join6 } from "path";
|
|
509
|
+
function readPackageInfo(pkgPath) {
|
|
361
510
|
try {
|
|
362
|
-
const pkg2 = JSON.parse(
|
|
363
|
-
return
|
|
511
|
+
const pkg2 = JSON.parse(readFileSync4(pkgPath, "utf-8"));
|
|
512
|
+
return {
|
|
513
|
+
name: typeof pkg2.name === "string" ? pkg2.name : void 0,
|
|
514
|
+
version: typeof pkg2.version === "string" ? pkg2.version : void 0
|
|
515
|
+
};
|
|
364
516
|
} catch {
|
|
365
|
-
return
|
|
517
|
+
return {};
|
|
366
518
|
}
|
|
367
519
|
}
|
|
368
520
|
function readConfigEntry(configPath, name) {
|
|
369
521
|
try {
|
|
370
|
-
const cfg = JSON.parse(
|
|
522
|
+
const cfg = JSON.parse(readFileSync4(configPath, "utf-8"));
|
|
371
523
|
const servers = cfg.mcpServers ?? cfg.servers ?? {};
|
|
372
524
|
const raw = servers[name] ?? {};
|
|
373
525
|
return {
|
|
@@ -382,15 +534,23 @@ function readConfigEntry(configPath, name) {
|
|
|
382
534
|
function resolveMcpCandidateDetail(profile) {
|
|
383
535
|
const name = profile.servers[0];
|
|
384
536
|
if (profile.config.endsWith("package.json")) {
|
|
385
|
-
return { name, version:
|
|
537
|
+
return { name, version: readPackageInfo(profile.config).version };
|
|
386
538
|
}
|
|
387
539
|
const entry = readConfigEntry(profile.config, name);
|
|
388
|
-
|
|
540
|
+
const sidecar = readPackageInfo(join6(dirname2(profile.config), "package.json"));
|
|
541
|
+
return {
|
|
542
|
+
name,
|
|
543
|
+
command: entry?.command,
|
|
544
|
+
args: entry?.args,
|
|
545
|
+
env: entry?.env,
|
|
546
|
+
version: sidecar.version,
|
|
547
|
+
packageName: sidecar.name
|
|
548
|
+
};
|
|
389
549
|
}
|
|
390
550
|
|
|
391
551
|
// src/provenance.ts
|
|
392
|
-
import { appendFileSync, existsSync as existsSync6, statSync as
|
|
393
|
-
import { join as
|
|
552
|
+
import { appendFileSync, existsSync as existsSync6, statSync as statSync3, writeFileSync as writeFileSync3 } from "fs";
|
|
553
|
+
import { join as join7 } from "path";
|
|
394
554
|
var FORGE_VERBS = ["DEFER", "ABSORB", "FORK", "REJECT", "WATCH"];
|
|
395
555
|
var LEDGER_HEADER = "# Skill Forge \u2014 Provenance Ledger\n\nOne entry per external-skill ingestion decision.\n";
|
|
396
556
|
function todayLocalIsoDate() {
|
|
@@ -404,7 +564,7 @@ function appendProvenance(entry, skillsRoot) {
|
|
|
404
564
|
if (!FORGE_VERBS.includes(entry.verb)) {
|
|
405
565
|
throw new Error(`verb must be one of ${FORGE_VERBS.join(", ")}`);
|
|
406
566
|
}
|
|
407
|
-
if (!existsSync6(skillsRoot) || !
|
|
567
|
+
if (!existsSync6(skillsRoot) || !statSync3(skillsRoot).isDirectory()) {
|
|
408
568
|
throw new Error(`skills-root not a directory: ${skillsRoot}`);
|
|
409
569
|
}
|
|
410
570
|
const today = todayLocalIsoDate();
|
|
@@ -421,7 +581,7 @@ function appendProvenance(entry, skillsRoot) {
|
|
|
421
581
|
- **Drift check:** \`${driftCheck}\`
|
|
422
582
|
- **Notes:** ${entry.notes ?? ""}
|
|
423
583
|
`;
|
|
424
|
-
const ledgerPath =
|
|
584
|
+
const ledgerPath = join7(skillsRoot, "SOURCES.md");
|
|
425
585
|
if (!existsSync6(ledgerPath)) {
|
|
426
586
|
writeFileSync3(ledgerPath, LEDGER_HEADER, "utf-8");
|
|
427
587
|
}
|
|
@@ -431,7 +591,7 @@ function appendProvenance(entry, skillsRoot) {
|
|
|
431
591
|
|
|
432
592
|
// src/commands/gatePipeline.ts
|
|
433
593
|
import { readdirSync as readdirSync5 } from "fs";
|
|
434
|
-
import { join as
|
|
594
|
+
import { join as join12 } from "path";
|
|
435
595
|
|
|
436
596
|
// src/gate/profile.ts
|
|
437
597
|
import { execFileSync } from "child_process";
|
|
@@ -447,82 +607,11 @@ import {
|
|
|
447
607
|
statSync as statSync4
|
|
448
608
|
} from "fs";
|
|
449
609
|
import { tmpdir } from "os";
|
|
450
|
-
import { basename as basename3, dirname as
|
|
610
|
+
import { basename as basename3, dirname as dirname3, extname as extname3, join as join9, resolve as resolve2 } from "path";
|
|
451
611
|
|
|
452
612
|
// src/gate/mcpCapabilities.ts
|
|
453
613
|
import { existsSync as existsSync7, readFileSync as readFileSync5 } from "fs";
|
|
454
|
-
import { extname as extname2, join as
|
|
455
|
-
|
|
456
|
-
// src/gate/scanShared.ts
|
|
457
|
-
import { readFileSync as readFileSync4, readdirSync as readdirSync2, statSync as statSync3 } from "fs";
|
|
458
|
-
import { extname, join as join6 } from "path";
|
|
459
|
-
var SKIP_DIRS2 = /* @__PURE__ */ new Set([".git", "node_modules", "dist", "build", "__pycache__", ".venv", "venv"]);
|
|
460
|
-
var BINARY_EXT = /* @__PURE__ */ new Set([
|
|
461
|
-
".png",
|
|
462
|
-
".jpg",
|
|
463
|
-
".jpeg",
|
|
464
|
-
".gif",
|
|
465
|
-
".webp",
|
|
466
|
-
".ico",
|
|
467
|
-
".pdf",
|
|
468
|
-
".zip",
|
|
469
|
-
".tar",
|
|
470
|
-
".gz",
|
|
471
|
-
".woff",
|
|
472
|
-
".woff2",
|
|
473
|
-
".ttf",
|
|
474
|
-
".eot",
|
|
475
|
-
".mp4",
|
|
476
|
-
".mp3",
|
|
477
|
-
".wasm",
|
|
478
|
-
".bin",
|
|
479
|
-
".exe"
|
|
480
|
-
]);
|
|
481
|
-
var MAX_FILE_BYTES = 2 * 1024 * 1024;
|
|
482
|
-
function* walk(dir) {
|
|
483
|
-
let entries;
|
|
484
|
-
try {
|
|
485
|
-
entries = readdirSync2(dir, { withFileTypes: true });
|
|
486
|
-
} catch {
|
|
487
|
-
return;
|
|
488
|
-
}
|
|
489
|
-
for (const entry of entries) {
|
|
490
|
-
const full = join6(dir, entry.name);
|
|
491
|
-
if (entry.isDirectory()) {
|
|
492
|
-
if (SKIP_DIRS2.has(entry.name)) continue;
|
|
493
|
-
yield* walk(full);
|
|
494
|
-
} else if (entry.isFile()) {
|
|
495
|
-
yield full;
|
|
496
|
-
}
|
|
497
|
-
}
|
|
498
|
-
}
|
|
499
|
-
function readTextFile(file) {
|
|
500
|
-
if (BINARY_EXT.has(extname(file).toLowerCase())) return void 0;
|
|
501
|
-
try {
|
|
502
|
-
if (statSync3(file).size > MAX_FILE_BYTES) return void 0;
|
|
503
|
-
const content = readFileSync4(file, "utf-8");
|
|
504
|
-
if (content.slice(0, 512).includes("\0")) return void 0;
|
|
505
|
-
return content;
|
|
506
|
-
} catch {
|
|
507
|
-
return void 0;
|
|
508
|
-
}
|
|
509
|
-
}
|
|
510
|
-
var BLOCK_SEVERITIES_BY_STRICTNESS = {
|
|
511
|
-
"block-high": /* @__PURE__ */ new Set(["HIGH", "CRITICAL"]),
|
|
512
|
-
"block-medium": /* @__PURE__ */ new Set(["MEDIUM", "HIGH", "CRITICAL"]),
|
|
513
|
-
"warn-only": /* @__PURE__ */ new Set()
|
|
514
|
-
};
|
|
515
|
-
function verdictFor(findings, strictness) {
|
|
516
|
-
const blockSeverities = BLOCK_SEVERITIES_BY_STRICTNESS[strictness];
|
|
517
|
-
if (findings.some((f) => blockSeverities.has(f.severity))) return "block";
|
|
518
|
-
if (findings.length > 0) return "warn";
|
|
519
|
-
return "pass";
|
|
520
|
-
}
|
|
521
|
-
function formatSafetyFinding(f) {
|
|
522
|
-
return `${f.severity} [${f.rule}] ${f.file}: ${f.detail}`;
|
|
523
|
-
}
|
|
524
|
-
|
|
525
|
-
// src/gate/mcpCapabilities.ts
|
|
614
|
+
import { extname as extname2, join as join8 } from "path";
|
|
526
615
|
var SOURCE_EXT = /* @__PURE__ */ new Set([".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs"]);
|
|
527
616
|
var MAX_ITEMS_PER_CATEGORY = 200;
|
|
528
617
|
function readJsonSafe(file) {
|
|
@@ -628,7 +717,7 @@ var MCP_SDK_IMPORT_RE = /@modelcontextprotocol\/sdk/;
|
|
|
628
717
|
function extractMcpCapabilities(packageDir) {
|
|
629
718
|
const buckets = { tools: /* @__PURE__ */ new Set(), resources: /* @__PURE__ */ new Set(), prompts: /* @__PURE__ */ new Set() };
|
|
630
719
|
let hasPackageJsonMarker = false;
|
|
631
|
-
const pkgPath =
|
|
720
|
+
const pkgPath = join8(packageDir, "package.json");
|
|
632
721
|
if (existsSync7(pkgPath)) {
|
|
633
722
|
const pkg2 = readJsonSafe(pkgPath);
|
|
634
723
|
if (pkg2 && typeof pkg2 === "object" && !Array.isArray(pkg2)) {
|
|
@@ -636,7 +725,7 @@ function extractMcpCapabilities(packageDir) {
|
|
|
636
725
|
}
|
|
637
726
|
}
|
|
638
727
|
for (const manifestName of [".mcp.json", "mcp.json"]) {
|
|
639
|
-
const manifestPath =
|
|
728
|
+
const manifestPath = join8(packageDir, manifestName);
|
|
640
729
|
if (!existsSync7(manifestPath)) continue;
|
|
641
730
|
const parsed = readJsonSafe(manifestPath);
|
|
642
731
|
if (parsed !== void 0) {
|
|
@@ -744,7 +833,7 @@ function asProfile(candidatePath, artifact) {
|
|
|
744
833
|
return asSkillProfile(profile, `at ${candidatePath}`);
|
|
745
834
|
}
|
|
746
835
|
function classifyInput(p) {
|
|
747
|
-
if (existsSync8(
|
|
836
|
+
if (existsSync8(join9(p, ".claude-plugin", "plugin.json"))) return "plugin";
|
|
748
837
|
if (detectArtifactType(p) === "mcp") return "mcp";
|
|
749
838
|
return "skill";
|
|
750
839
|
}
|
|
@@ -752,7 +841,7 @@ var MCP_SDK_DEP2 = "@modelcontextprotocol/sdk";
|
|
|
752
841
|
var MCP_KEYWORD2 = "mcp-server";
|
|
753
842
|
var MCP_BIN_PREFIX2 = /^mcp-/i;
|
|
754
843
|
function readPackageJsonSafe(root) {
|
|
755
|
-
const pkgPath =
|
|
844
|
+
const pkgPath = join9(root, "package.json");
|
|
756
845
|
if (!existsSync8(pkgPath)) return void 0;
|
|
757
846
|
try {
|
|
758
847
|
return JSON.parse(readFileSync6(pkgPath, "utf-8"));
|
|
@@ -776,7 +865,7 @@ function hasMcpPackageMarkers(pkg2) {
|
|
|
776
865
|
return false;
|
|
777
866
|
}
|
|
778
867
|
function detectArtifactType(root) {
|
|
779
|
-
if (existsSync8(
|
|
868
|
+
if (existsSync8(join9(root, ".mcp.json")) || existsSync8(join9(root, "mcp.json"))) return "mcp";
|
|
780
869
|
const pkg2 = readPackageJsonSafe(root);
|
|
781
870
|
if (pkg2 && hasMcpPackageMarkers(pkg2)) return "mcp";
|
|
782
871
|
return "skill";
|
|
@@ -786,22 +875,22 @@ function resolveSkillMd(inputPath) {
|
|
|
786
875
|
let tmpDir;
|
|
787
876
|
const initialStat = statSync4(currentPath);
|
|
788
877
|
if (extname3(currentPath) === ".skill" || initialStat.isFile() && isZipFile(currentPath)) {
|
|
789
|
-
tmpDir = mkdtempSync(
|
|
878
|
+
tmpDir = mkdtempSync(join9(tmpdir(), "forge_"));
|
|
790
879
|
extractZip(currentPath, tmpDir);
|
|
791
880
|
currentPath = tmpDir;
|
|
792
881
|
}
|
|
793
882
|
const stat = statSync4(currentPath);
|
|
794
883
|
if (stat.isFile() && basename3(currentPath) === "SKILL.md") {
|
|
795
|
-
return { skillMd: currentPath, root:
|
|
884
|
+
return { skillMd: currentPath, root: dirname3(currentPath), tmpDir };
|
|
796
885
|
}
|
|
797
886
|
if (stat.isDirectory()) {
|
|
798
|
-
const direct =
|
|
887
|
+
const direct = join9(currentPath, "SKILL.md");
|
|
799
888
|
if (existsSync8(direct)) {
|
|
800
889
|
return { skillMd: direct, root: currentPath, tmpDir };
|
|
801
890
|
}
|
|
802
891
|
const matches = findSkillMdOneLevelDown(currentPath);
|
|
803
892
|
if (matches.length === 1) {
|
|
804
|
-
return { skillMd: matches[0], root:
|
|
893
|
+
return { skillMd: matches[0], root: dirname3(matches[0]), tmpDir };
|
|
805
894
|
}
|
|
806
895
|
if (matches.length > 1) {
|
|
807
896
|
cleanupTmp(tmpDir);
|
|
@@ -821,15 +910,15 @@ function findSkillMdOneLevelDown(dir) {
|
|
|
821
910
|
const matches = /* @__PURE__ */ new Set();
|
|
822
911
|
for (const entry of readdirSync3(dir, { withFileTypes: true })) {
|
|
823
912
|
if (entry.isDirectory()) {
|
|
824
|
-
const candidate =
|
|
913
|
+
const candidate = join9(dir, entry.name, "SKILL.md");
|
|
825
914
|
if (existsSync8(candidate)) matches.add(candidate);
|
|
826
915
|
}
|
|
827
916
|
}
|
|
828
|
-
const skillsDir =
|
|
917
|
+
const skillsDir = join9(dir, "skills");
|
|
829
918
|
if (isDir(skillsDir)) {
|
|
830
919
|
for (const entry of readdirSync3(skillsDir, { withFileTypes: true })) {
|
|
831
920
|
if (entry.isDirectory()) {
|
|
832
|
-
const candidate =
|
|
921
|
+
const candidate = join9(skillsDir, entry.name, "SKILL.md");
|
|
833
922
|
if (existsSync8(candidate)) matches.add(candidate);
|
|
834
923
|
}
|
|
835
924
|
}
|
|
@@ -913,7 +1002,7 @@ function detectLicense(root, fm) {
|
|
|
913
1002
|
if (fm.license) return fm.license;
|
|
914
1003
|
const names = ["LICENSE", "LICENSE.md", "LICENSE.txt", "COPYING", "license"];
|
|
915
1004
|
for (const name of names) {
|
|
916
|
-
const p =
|
|
1005
|
+
const p = join9(root, name);
|
|
917
1006
|
if (existsSync8(p) && statSync4(p).isFile()) {
|
|
918
1007
|
const head = readFileSync6(p, "utf-8").slice(0, 2e3).toLowerCase();
|
|
919
1008
|
for (const [hint, label] of Object.entries(SPDX_HINTS)) {
|
|
@@ -927,7 +1016,7 @@ function detectLicense(root, fm) {
|
|
|
927
1016
|
function walkFiles(dir) {
|
|
928
1017
|
const out = [];
|
|
929
1018
|
for (const entry of readdirSync3(dir, { withFileTypes: true })) {
|
|
930
|
-
const full =
|
|
1019
|
+
const full = join9(dir, entry.name);
|
|
931
1020
|
if (entry.isDirectory()) {
|
|
932
1021
|
out.push(...walkFiles(full));
|
|
933
1022
|
} else if (entry.isFile()) {
|
|
@@ -939,7 +1028,7 @@ function walkFiles(dir) {
|
|
|
939
1028
|
function countResources(root) {
|
|
940
1029
|
const resources = {};
|
|
941
1030
|
for (const d of RESOURCE_DIRS) {
|
|
942
|
-
const dp =
|
|
1031
|
+
const dp = join9(root, d);
|
|
943
1032
|
if (isDir(dp)) {
|
|
944
1033
|
resources[d] = walkFiles(dp).length;
|
|
945
1034
|
}
|
|
@@ -950,7 +1039,7 @@ function scanMcpAndImports(text, root) {
|
|
|
950
1039
|
const mcpRefRe = /mcp__[a-zA-Z0-9_]+/g;
|
|
951
1040
|
const mcpRefs = new Set(text.match(mcpRefRe) ?? []);
|
|
952
1041
|
const imports = /* @__PURE__ */ new Set();
|
|
953
|
-
const scriptsDir =
|
|
1042
|
+
const scriptsDir = join9(root, "scripts");
|
|
954
1043
|
if (isDir(scriptsDir)) {
|
|
955
1044
|
for (const f of walkFiles(scriptsDir)) {
|
|
956
1045
|
if (!f.endsWith(".py")) continue;
|
|
@@ -999,19 +1088,19 @@ function profileSkillDir(inputPath) {
|
|
|
999
1088
|
}
|
|
1000
1089
|
}
|
|
1001
1090
|
function profilePlugin(p) {
|
|
1002
|
-
const manifestPath =
|
|
1091
|
+
const manifestPath = join9(p, ".claude-plugin", "plugin.json");
|
|
1003
1092
|
let manifest;
|
|
1004
1093
|
try {
|
|
1005
1094
|
manifest = JSON.parse(readFileSync6(manifestPath, "utf-8"));
|
|
1006
1095
|
} catch (err) {
|
|
1007
1096
|
throw new Error(`could not parse plugin.json under ${p}: ${err.message}`);
|
|
1008
1097
|
}
|
|
1009
|
-
const skillsDir =
|
|
1098
|
+
const skillsDir = join9(p, "skills");
|
|
1010
1099
|
let skills = [];
|
|
1011
1100
|
if (isDir(skillsDir)) {
|
|
1012
|
-
skills = readdirSync3(skillsDir, { withFileTypes: true }).filter((d) => d.isDirectory() && existsSync8(
|
|
1101
|
+
skills = readdirSync3(skillsDir, { withFileTypes: true }).filter((d) => d.isDirectory() && existsSync8(join9(skillsDir, d.name, "SKILL.md"))).map((d) => d.name).sort();
|
|
1013
1102
|
}
|
|
1014
|
-
const commandsDir =
|
|
1103
|
+
const commandsDir = join9(p, "commands");
|
|
1015
1104
|
const commands = isDir(commandsDir) ? readdirSync3(commandsDir).filter((f) => f.endsWith(".md")).length : 0;
|
|
1016
1105
|
const mc = manifest.mcpServers ?? manifest.mcp_servers ?? {};
|
|
1017
1106
|
const mcpServers = typeof mc === "object" && mc !== null ? Object.keys(mc).sort() : [];
|
|
@@ -1029,7 +1118,7 @@ function profilePlugin(p) {
|
|
|
1029
1118
|
};
|
|
1030
1119
|
}
|
|
1031
1120
|
function profileMcp(p) {
|
|
1032
|
-
const explicitConfig = existsSync8(
|
|
1121
|
+
const explicitConfig = existsSync8(join9(p, ".mcp.json")) ? join9(p, ".mcp.json") : existsSync8(join9(p, "mcp.json")) ? join9(p, "mcp.json") : void 0;
|
|
1033
1122
|
if (explicitConfig) {
|
|
1034
1123
|
let cfg;
|
|
1035
1124
|
try {
|
|
@@ -1049,7 +1138,7 @@ function profileMcp(p) {
|
|
|
1049
1138
|
capabilities: extractMcpCapabilities(p)
|
|
1050
1139
|
};
|
|
1051
1140
|
}
|
|
1052
|
-
const pkgPath =
|
|
1141
|
+
const pkgPath = join9(p, "package.json");
|
|
1053
1142
|
let pkg2;
|
|
1054
1143
|
try {
|
|
1055
1144
|
pkg2 = JSON.parse(readFileSync6(pkgPath, "utf-8"));
|
|
@@ -1071,7 +1160,7 @@ function profileMcp(p) {
|
|
|
1071
1160
|
// src/gate/safety.ts
|
|
1072
1161
|
import { statSync as statSync5 } from "fs";
|
|
1073
1162
|
import { execFile } from "child_process";
|
|
1074
|
-
import { delimiter, join as
|
|
1163
|
+
import { delimiter, join as join10 } from "path";
|
|
1075
1164
|
import { promisify } from "util";
|
|
1076
1165
|
var execFileAsync = promisify(execFile);
|
|
1077
1166
|
var DENY_RULES = [
|
|
@@ -1170,7 +1259,7 @@ function which(cmd) {
|
|
|
1170
1259
|
const exts = process.platform === "win32" ? (process.env.PATHEXT ?? ".EXE;.CMD;.BAT").split(";") : [""];
|
|
1171
1260
|
for (const dir of dirs) {
|
|
1172
1261
|
for (const ext of exts) {
|
|
1173
|
-
const candidate =
|
|
1262
|
+
const candidate = join10(dir, cmd + ext);
|
|
1174
1263
|
try {
|
|
1175
1264
|
if (statSync5(candidate).isFile()) return candidate;
|
|
1176
1265
|
} catch {
|
|
@@ -1211,7 +1300,7 @@ async function scanSafety(targetPath, strictness = DEFAULT_STRICTNESS) {
|
|
|
1211
1300
|
|
|
1212
1301
|
// src/gate/overlap.ts
|
|
1213
1302
|
import { existsSync as existsSync9, readFileSync as readFileSync7, readdirSync as readdirSync4 } from "fs";
|
|
1214
|
-
import { basename as basename4, dirname as
|
|
1303
|
+
import { basename as basename4, dirname as dirname4, join as join11, resolve as resolve3 } from "path";
|
|
1215
1304
|
var STOPWORDS = new Set(
|
|
1216
1305
|
`
|
|
1217
1306
|
a an the and or of to for in on with use used using when whether this that these those is are be
|
|
@@ -1233,7 +1322,7 @@ function splitLimited(text, sep2, maxSplit) {
|
|
|
1233
1322
|
}
|
|
1234
1323
|
function readFrontmatter(skillMdPath) {
|
|
1235
1324
|
const text = readFileSync7(skillMdPath, "utf-8");
|
|
1236
|
-
const meta = { name: basename4(
|
|
1325
|
+
const meta = { name: basename4(dirname4(skillMdPath)), description: "" };
|
|
1237
1326
|
if (text.startsWith("---\n")) {
|
|
1238
1327
|
const block = splitLimited(text, "---", 2)[1] ?? "";
|
|
1239
1328
|
const nameMatch = block.match(/^name:\s*(.+)$/m);
|
|
@@ -1334,21 +1423,21 @@ function listSubdirs(dir) {
|
|
|
1334
1423
|
} catch {
|
|
1335
1424
|
return [];
|
|
1336
1425
|
}
|
|
1337
|
-
return entries.filter((e) => e.isDirectory()).map((e) =>
|
|
1426
|
+
return entries.filter((e) => e.isDirectory()).map((e) => join11(dir, e.name));
|
|
1338
1427
|
}
|
|
1339
1428
|
function findSkillMdFiles(root) {
|
|
1340
1429
|
const found = /* @__PURE__ */ new Set();
|
|
1341
1430
|
for (const dir of listSubdirs(root)) {
|
|
1342
|
-
const candidate =
|
|
1431
|
+
const candidate = join11(dir, "SKILL.md");
|
|
1343
1432
|
if (existsSync9(candidate)) found.add(resolve3(candidate));
|
|
1344
1433
|
}
|
|
1345
|
-
for (const dir of listSubdirs(
|
|
1346
|
-
const candidate =
|
|
1434
|
+
for (const dir of listSubdirs(join11(root, "skills"))) {
|
|
1435
|
+
const candidate = join11(dir, "SKILL.md");
|
|
1347
1436
|
if (existsSync9(candidate)) found.add(resolve3(candidate));
|
|
1348
1437
|
}
|
|
1349
1438
|
for (const topDir of listSubdirs(root)) {
|
|
1350
|
-
for (const dir of listSubdirs(
|
|
1351
|
-
const candidate =
|
|
1439
|
+
for (const dir of listSubdirs(join11(topDir, "skills"))) {
|
|
1440
|
+
const candidate = join11(dir, "SKILL.md");
|
|
1352
1441
|
if (existsSync9(candidate)) found.add(resolve3(candidate));
|
|
1353
1442
|
}
|
|
1354
1443
|
}
|
|
@@ -1361,7 +1450,7 @@ function scanOverlap(candidatePath, skillsRoot) {
|
|
|
1361
1450
|
}
|
|
1362
1451
|
let cpath = resolve3(expandHome(candidatePath));
|
|
1363
1452
|
if (isDir(cpath)) {
|
|
1364
|
-
cpath =
|
|
1453
|
+
cpath = join11(cpath, "SKILL.md");
|
|
1365
1454
|
}
|
|
1366
1455
|
if (!existsSync9(cpath)) {
|
|
1367
1456
|
throw new Error(`candidate SKILL.md not found: ${cpath}`);
|
|
@@ -1416,21 +1505,6 @@ function looksHighEntropy(value) {
|
|
|
1416
1505
|
const uniqueChars = new Set(value).size;
|
|
1417
1506
|
return varietyCount >= 2 && uniqueChars / value.length > 0.4;
|
|
1418
1507
|
}
|
|
1419
|
-
function isPinnedPackageSpec(spec) {
|
|
1420
|
-
const withoutScope = spec.startsWith("@") ? spec.slice(spec.indexOf("/") + 1) : spec;
|
|
1421
|
-
const atIdx = withoutScope.lastIndexOf("@");
|
|
1422
|
-
if (atIdx <= 0) return false;
|
|
1423
|
-
return /^\d/.test(withoutScope.slice(atIdx + 1));
|
|
1424
|
-
}
|
|
1425
|
-
function isNpxCommand(command) {
|
|
1426
|
-
const base = command.split(/[\\/]/).pop() ?? command;
|
|
1427
|
-
return base.replace(/\.(cmd|exe)$/i, "").toLowerCase() === "npx";
|
|
1428
|
-
}
|
|
1429
|
-
function packageSpecBase(spec) {
|
|
1430
|
-
const scopeEnd = spec.startsWith("@") ? spec.indexOf("/") + 1 : 0;
|
|
1431
|
-
const atIdx = spec.lastIndexOf("@");
|
|
1432
|
-
return atIdx > scopeEnd ? spec.slice(0, atIdx) : spec;
|
|
1433
|
-
}
|
|
1434
1508
|
function collectServerNodes(node, out) {
|
|
1435
1509
|
if (Array.isArray(node)) {
|
|
1436
1510
|
for (const item of node) collectServerNodes(item, out);
|
|
@@ -1461,14 +1535,17 @@ function scanJsonConfig(file, parsed) {
|
|
|
1461
1535
|
if (command && isNpxCommand(command)) {
|
|
1462
1536
|
const flagIdx = args.findIndex((a) => a === "-y" || a === "--yes");
|
|
1463
1537
|
if (flagIdx !== -1) {
|
|
1464
|
-
const
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1538
|
+
const { packageFlagSpecs, positionalSpec } = collectNpxPackageSpecs(args);
|
|
1539
|
+
const allSpecs = positionalSpec ? [...packageFlagSpecs, positionalSpec] : packageFlagSpecs;
|
|
1540
|
+
for (const pkgSpec of allSpecs) {
|
|
1541
|
+
if (!isPinnedPackageSpec(pkgSpec)) {
|
|
1542
|
+
findings.push({
|
|
1543
|
+
severity: "MEDIUM",
|
|
1544
|
+
rule: "mcp-unpinned-npx",
|
|
1545
|
+
file,
|
|
1546
|
+
detail: `npx -y ${pkgSpec} is unpinned \u2014 pin an exact version (${packageSpecBase(pkgSpec)}@<version>) so the resolved server code can't change silently`
|
|
1547
|
+
});
|
|
1548
|
+
}
|
|
1472
1549
|
}
|
|
1473
1550
|
}
|
|
1474
1551
|
}
|
|
@@ -1883,7 +1960,7 @@ function resolveMcpCandidatePath(entry) {
|
|
|
1883
1960
|
children = readdirSync5(entry.quarantinePath, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => d.name);
|
|
1884
1961
|
} catch {
|
|
1885
1962
|
}
|
|
1886
|
-
return children.length === 1 ?
|
|
1963
|
+
return children.length === 1 ? join12(entry.quarantinePath, children[0]) : entry.quarantinePath;
|
|
1887
1964
|
}
|
|
1888
1965
|
async function runGate(candidatePath, skillsRoot, {
|
|
1889
1966
|
proFeatureLabel = "overlap analysis",
|
|
@@ -1911,9 +1988,9 @@ async function runGate(candidatePath, skillsRoot, {
|
|
|
1911
1988
|
// src/queue.ts
|
|
1912
1989
|
import { randomUUID as randomUUID2 } from "crypto";
|
|
1913
1990
|
import { existsSync as existsSync11, mkdirSync as mkdirSync5, readFileSync as readFileSync9, writeFileSync as writeFileSync4 } from "fs";
|
|
1914
|
-
import { join as
|
|
1991
|
+
import { join as join13 } from "path";
|
|
1915
1992
|
function getQueuePath() {
|
|
1916
|
-
return
|
|
1993
|
+
return join13(getHomeDir(), "queue.json");
|
|
1917
1994
|
}
|
|
1918
1995
|
function readQueue() {
|
|
1919
1996
|
const path = getQueuePath();
|
|
@@ -1951,15 +2028,15 @@ function generateId(existingIds) {
|
|
|
1951
2028
|
// src/handoff.ts
|
|
1952
2029
|
import { spawnSync } from "child_process";
|
|
1953
2030
|
import { existsSync as existsSync12 } from "fs";
|
|
1954
|
-
import { dirname as
|
|
2031
|
+
import { dirname as dirname5, join as join14 } from "path";
|
|
1955
2032
|
import { fileURLToPath } from "url";
|
|
1956
2033
|
var KNOWN_AGENT_BINARIES = ["claude", "codex", "cursor-agent", "windsurf", "opencode", "gemini"];
|
|
1957
2034
|
var HANDOFF_INSTRUCTION = "Read {prompt} and follow its instructions for the skill installed at {path}";
|
|
1958
2035
|
function resolvePromptPath() {
|
|
1959
|
-
const here =
|
|
1960
|
-
const builtCandidate =
|
|
2036
|
+
const here = dirname5(fileURLToPath(import.meta.url));
|
|
2037
|
+
const builtCandidate = join14(here, "ingest-prompt.md");
|
|
1961
2038
|
if (existsSync12(builtCandidate)) return builtCandidate;
|
|
1962
|
-
return
|
|
2039
|
+
return join14(here, "..", "assets", "ingest-prompt.md");
|
|
1963
2040
|
}
|
|
1964
2041
|
function defaultIsOnPath(bin) {
|
|
1965
2042
|
const finder = process.platform === "win32" ? "where" : "which";
|
|
@@ -2021,7 +2098,7 @@ import { createInterface } from "readline/promises";
|
|
|
2021
2098
|
// src/agents.ts
|
|
2022
2099
|
import { statSync as statSync7 } from "fs";
|
|
2023
2100
|
import { homedir as homedir3 } from "os";
|
|
2024
|
-
import { join as
|
|
2101
|
+
import { join as join15, resolve as resolve5 } from "path";
|
|
2025
2102
|
function isFile(p) {
|
|
2026
2103
|
try {
|
|
2027
2104
|
return statSync7(p).isFile();
|
|
@@ -2035,7 +2112,7 @@ function projectPath(root, cwd) {
|
|
|
2035
2112
|
function globalPath(root, home) {
|
|
2036
2113
|
if (!root) return void 0;
|
|
2037
2114
|
if (root === "~") return home;
|
|
2038
|
-
if (root.startsWith("~/")) return
|
|
2115
|
+
if (root.startsWith("~/")) return join15(home, root.slice(2));
|
|
2039
2116
|
return root;
|
|
2040
2117
|
}
|
|
2041
2118
|
function makeAgent(spec) {
|
|
@@ -2585,13 +2662,20 @@ async function addMcp(source, config, options) {
|
|
|
2585
2662
|
// version may be absent — promoteMcp refuses the unpinnable npx fallback
|
|
2586
2663
|
// rather than writing "npx pkg@unknown"; see deriveServerEntry in promoteMcp.ts.
|
|
2587
2664
|
version: detail.version,
|
|
2665
|
+
// packageName (v0.7) — the candidate's own scanned package.json name, so
|
|
2666
|
+
// deriveServerEntry can auto-pin an unpinned npx spec in candidateEntry
|
|
2667
|
+
// only when it provably matches the package that was actually scanned.
|
|
2668
|
+
packageName: detail.packageName,
|
|
2588
2669
|
envVars: detail.env ? Object.keys(detail.env) : void 0,
|
|
2589
2670
|
candidateEntry: detail.command ? { command: detail.command, args: detail.args, env: detail.env } : void 0
|
|
2590
2671
|
};
|
|
2591
|
-
const mcpTargetPath = options.mcpTarget ?? config.mcpTargets?.[0] ??
|
|
2672
|
+
const mcpTargetPath = options.mcpTarget ?? config.mcpTargets?.[0] ?? join16(process.cwd(), ".mcp.json");
|
|
2592
2673
|
const result = promoteMcp(entry, serverProfile, mcpTargetPath, { force: options.force });
|
|
2593
2674
|
reject(entry);
|
|
2594
2675
|
logOutcome(`[skill-forge] promoted -> ${result.target} (server "${result.serverName}")`, options.json);
|
|
2676
|
+
if (result.autoPinned) {
|
|
2677
|
+
logOutcome(`[skill-forge] pinned launch spec ${result.autoPinned.from} -> ${result.autoPinned.to}`, options.json);
|
|
2678
|
+
}
|
|
2595
2679
|
if (result.backupPath) {
|
|
2596
2680
|
logOutcome(`[skill-forge] backed up existing config -> ${result.backupPath}`, options.json);
|
|
2597
2681
|
}
|
|
@@ -2680,7 +2764,7 @@ async function scan(source, options = {}) {
|
|
|
2680
2764
|
}
|
|
2681
2765
|
|
|
2682
2766
|
// src/commands/list.ts
|
|
2683
|
-
import { join as
|
|
2767
|
+
import { join as join17 } from "path";
|
|
2684
2768
|
async function list() {
|
|
2685
2769
|
await maybeOfferInit();
|
|
2686
2770
|
const config = loadConfig();
|
|
@@ -2693,14 +2777,14 @@ async function list() {
|
|
|
2693
2777
|
}
|
|
2694
2778
|
console.log(`[skill-forge] ${ids.length} held in quarantine (${quarantineDir}):`);
|
|
2695
2779
|
for (const id of ids) {
|
|
2696
|
-
const skillDirs = findSkillDirs(
|
|
2780
|
+
const skillDirs = findSkillDirs(join17(quarantineDir, id));
|
|
2697
2781
|
if (skillDirs.length === 0) {
|
|
2698
2782
|
console.log(` ${id} (no SKILL.md found)`);
|
|
2699
2783
|
continue;
|
|
2700
2784
|
}
|
|
2701
2785
|
const names = skillDirs.map((dir) => {
|
|
2702
2786
|
try {
|
|
2703
|
-
return readFrontmatter(
|
|
2787
|
+
return readFrontmatter(join17(dir, "SKILL.md")).name;
|
|
2704
2788
|
} catch {
|
|
2705
2789
|
return dir;
|
|
2706
2790
|
}
|
|
@@ -2726,14 +2810,14 @@ import { createInterface as createInterface3 } from "readline/promises";
|
|
|
2726
2810
|
import { createHash } from "crypto";
|
|
2727
2811
|
import { cpSync as cpSync3, existsSync as existsSync14, lstatSync, mkdtempSync as mkdtempSync2, readFileSync as readFileSync10, rmSync as rmSync3 } from "fs";
|
|
2728
2812
|
import { tmpdir as tmpdir2 } from "os";
|
|
2729
|
-
import { basename as basename6, dirname as
|
|
2813
|
+
import { basename as basename6, dirname as dirname6, join as join18, resolve as resolvePath2, sep } from "path";
|
|
2730
2814
|
var SKILLOPT_BIN = "skillopt-sleep";
|
|
2731
2815
|
var PRO_FEATURE_LABEL2 = "self-evolution via SkillOpt-Sleep (evolve) \u2014 re-gating, provenance, and the pending-ingestion queue";
|
|
2732
2816
|
async function evolve(targetSkillDir, options = {}, deps = {}) {
|
|
2733
2817
|
const exec = deps.exec ?? defaultExec;
|
|
2734
2818
|
const json = options.json;
|
|
2735
2819
|
const resolvedTargetDir = resolvePath2(targetSkillDir);
|
|
2736
|
-
const skillMdPath =
|
|
2820
|
+
const skillMdPath = join18(resolvedTargetDir, "SKILL.md");
|
|
2737
2821
|
if (!existsSync14(skillMdPath)) {
|
|
2738
2822
|
throw new UserError(`no SKILL.md found at ${skillMdPath} \u2014 evolve requires an existing, installed skill directory`);
|
|
2739
2823
|
}
|
|
@@ -2792,16 +2876,16 @@ async function evolve(targetSkillDir, options = {}, deps = {}) {
|
|
|
2792
2876
|
}
|
|
2793
2877
|
return;
|
|
2794
2878
|
}
|
|
2795
|
-
const stagingRoot =
|
|
2879
|
+
const stagingRoot = join18(project, ".skillopt-sleep", "staging");
|
|
2796
2880
|
const stagingDir = resolvePath2(parsed.staging_dir);
|
|
2797
2881
|
if (stagingDir !== stagingRoot && !stagingDir.startsWith(stagingRoot + sep)) {
|
|
2798
2882
|
throw new UserError(
|
|
2799
2883
|
`skillopt-sleep reported a staging_dir outside the expected sandbox (${stagingRoot}): ${stagingDir} \u2014 refusing to use it`
|
|
2800
2884
|
);
|
|
2801
2885
|
}
|
|
2802
|
-
const proposedSkillMd =
|
|
2803
|
-
const proposedClaudeMd =
|
|
2804
|
-
const reportMdPath =
|
|
2886
|
+
const proposedSkillMd = join18(stagingDir, "proposed_SKILL.md");
|
|
2887
|
+
const proposedClaudeMd = join18(stagingDir, "proposed_CLAUDE.md");
|
|
2888
|
+
const reportMdPath = join18(stagingDir, "report.md");
|
|
2805
2889
|
if (!existsSync14(proposedSkillMd)) {
|
|
2806
2890
|
throw new UserError(`skillopt-sleep reported an accepted proposal but ${proposedSkillMd} is missing`);
|
|
2807
2891
|
}
|
|
@@ -2812,13 +2896,13 @@ async function evolve(targetSkillDir, options = {}, deps = {}) {
|
|
|
2812
2896
|
assertNotSymlink(proposedClaudeMd, "proposed_CLAUDE.md");
|
|
2813
2897
|
const scannedSkillHash = sha256File(proposedSkillMd);
|
|
2814
2898
|
const scannedClaudeHash = sha256File(proposedClaudeMd);
|
|
2815
|
-
const tempDir = mkdtempSync2(
|
|
2899
|
+
const tempDir = mkdtempSync2(join18(tmpdir2(), "skill-forge-evolve-"));
|
|
2816
2900
|
let profile;
|
|
2817
2901
|
let safety;
|
|
2818
2902
|
try {
|
|
2819
|
-
cpSync3(proposedSkillMd,
|
|
2903
|
+
cpSync3(proposedSkillMd, join18(tempDir, "SKILL.md"));
|
|
2820
2904
|
if (existsSync14(proposedClaudeMd)) {
|
|
2821
|
-
cpSync3(proposedClaudeMd,
|
|
2905
|
+
cpSync3(proposedClaudeMd, join18(tempDir, "CLAUDE.md"));
|
|
2822
2906
|
}
|
|
2823
2907
|
profile = asSkillProfile(profileSkill(tempDir), "for the evolved proposal");
|
|
2824
2908
|
const strictness = loadConfig().strictness;
|
|
@@ -2860,7 +2944,7 @@ async function evolve(targetSkillDir, options = {}, deps = {}) {
|
|
|
2860
2944
|
throw new UserError(`skillopt-sleep adopt failed (exit ${adoptResult.code}): ${tail(adoptResult.stderr || adoptResult.stdout)}`);
|
|
2861
2945
|
}
|
|
2862
2946
|
logOutcome(`[skill-forge] adopted -> ${resolvedTargetDir}`, json);
|
|
2863
|
-
const skillsRootForProvenance =
|
|
2947
|
+
const skillsRootForProvenance = dirname6(resolvedTargetDir);
|
|
2864
2948
|
let ledgerPath;
|
|
2865
2949
|
try {
|
|
2866
2950
|
ledgerPath = appendProvenance(
|
|
@@ -2959,7 +3043,7 @@ function validateRunOutput(raw) {
|
|
|
2959
3043
|
return { accepted: Boolean(obj.accepted), staging_dir: stagingDirRaw ?? null };
|
|
2960
3044
|
}
|
|
2961
3045
|
function readManifest(stagingDir) {
|
|
2962
|
-
const manifestPath =
|
|
3046
|
+
const manifestPath = join18(stagingDir, "manifest.json");
|
|
2963
3047
|
let raw;
|
|
2964
3048
|
try {
|
|
2965
3049
|
raw = readFileSync10(manifestPath, "utf-8");
|
|
@@ -3000,8 +3084,8 @@ function filesIdentical(a, b) {
|
|
|
3000
3084
|
return readFileSync10(a).equals(readFileSync10(b));
|
|
3001
3085
|
}
|
|
3002
3086
|
function isAlreadyAdopted(stagingDir, manifest) {
|
|
3003
|
-
const proposedSkillMd =
|
|
3004
|
-
const proposedClaudeMd =
|
|
3087
|
+
const proposedSkillMd = join18(stagingDir, "proposed_SKILL.md");
|
|
3088
|
+
const proposedClaudeMd = join18(stagingDir, "proposed_CLAUDE.md");
|
|
3005
3089
|
let sawComparison = false;
|
|
3006
3090
|
if (existsSync14(proposedSkillMd)) {
|
|
3007
3091
|
sawComparison = true;
|