@rhize/skill-forge 0.11.0 → 0.11.2
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 +6 -0
- package/dist/cli.js +405 -253
- 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 join20 } from "path";
|
|
9
9
|
|
|
10
10
|
// src/config.ts
|
|
11
11
|
import { existsSync, mkdirSync, readFileSync as readFileSync2, writeFileSync } from "fs";
|
|
@@ -131,7 +131,32 @@ function collectNpxPackageSpecs(args) {
|
|
|
131
131
|
}
|
|
132
132
|
return { packageFlagSpecs, positionalSpec };
|
|
133
133
|
}
|
|
134
|
+
var ISO_TIMESTAMP_RE = /^\d{4}-\d{2}-\d{2}(?:[T ]\d{2}:\d{2}(?::\d{2}(?:\.\d+)?)?(?:Z|[+-]\d{2}:?\d{2})?)?$/;
|
|
135
|
+
var PATH_SEGMENT_RE = /^[A-Za-z0-9._-]+$/;
|
|
136
|
+
function looksLikeFsPath(v) {
|
|
137
|
+
if (!/^(?:\/|~\/|\.\.?\/)/.test(v)) return false;
|
|
138
|
+
const segments = v.replace(/^~/, "").split("/").filter(Boolean);
|
|
139
|
+
return segments.length >= 2 && segments.every((seg) => PATH_SEGMENT_RE.test(seg));
|
|
140
|
+
}
|
|
141
|
+
function looksLikeBareUrl(v) {
|
|
142
|
+
let url;
|
|
143
|
+
try {
|
|
144
|
+
url = new URL(v);
|
|
145
|
+
} catch {
|
|
146
|
+
return false;
|
|
147
|
+
}
|
|
148
|
+
if (!["http:", "https:", "ws:", "wss:", "file:"].includes(url.protocol)) return false;
|
|
149
|
+
if (url.username !== "" || url.password !== "") return false;
|
|
150
|
+
if (url.search !== "") return false;
|
|
151
|
+
return true;
|
|
152
|
+
}
|
|
153
|
+
function looksStructuredNonSecret(value) {
|
|
154
|
+
const v = value.trim().replace(/^[[\s'"]+/, "").replace(/[\]\s'",]+$/, "");
|
|
155
|
+
if (v === "") return false;
|
|
156
|
+
return ISO_TIMESTAMP_RE.test(v) || looksLikeBareUrl(v) || looksLikeFsPath(v);
|
|
157
|
+
}
|
|
134
158
|
function looksHighEntropy(value) {
|
|
159
|
+
if (looksStructuredNonSecret(value)) return false;
|
|
135
160
|
if (/\s/.test(value)) return false;
|
|
136
161
|
if (value.length < 20) return false;
|
|
137
162
|
const hasLower = /[a-z]/.test(value);
|
|
@@ -143,6 +168,9 @@ function looksHighEntropy(value) {
|
|
|
143
168
|
}
|
|
144
169
|
|
|
145
170
|
// src/preferences.ts
|
|
171
|
+
function markAccepted(value) {
|
|
172
|
+
return value;
|
|
173
|
+
}
|
|
146
174
|
var PREFERENCE_KEY_PATTERN = /^[a-z0-9][a-z0-9._-]{0,63}$/;
|
|
147
175
|
var MAX_PREFERENCE_VALUE_LENGTH = 500;
|
|
148
176
|
var MAX_PENDING_PROPOSALS = 50;
|
|
@@ -208,7 +236,7 @@ function setPreference(config, key, value, options = {}) {
|
|
|
208
236
|
validateProvenanceText("origin", options.origin);
|
|
209
237
|
validateProvenanceText("note", options.note);
|
|
210
238
|
const entry = {
|
|
211
|
-
value,
|
|
239
|
+
value: markAccepted(value),
|
|
212
240
|
source: options.source ?? "user",
|
|
213
241
|
updatedAt: (options.now ?? /* @__PURE__ */ new Date()).toISOString(),
|
|
214
242
|
origin: options.origin,
|
|
@@ -285,7 +313,9 @@ function normalizePreferences(raw) {
|
|
|
285
313
|
continue;
|
|
286
314
|
}
|
|
287
315
|
out[key] = {
|
|
288
|
-
|
|
316
|
+
// Re-minted only after `validatePreference` passed above: a hand-edited config.json cannot
|
|
317
|
+
// smuggle in a value that would fail today's rules.
|
|
318
|
+
value: markAccepted(entry.value),
|
|
289
319
|
source: entry.source === "agent" ? "agent" : "user",
|
|
290
320
|
updatedAt: typeof entry.updatedAt === "string" ? entry.updatedAt : (/* @__PURE__ */ new Date(0)).toISOString(),
|
|
291
321
|
origin,
|
|
@@ -625,19 +655,117 @@ function defaultExec(command, args, options) {
|
|
|
625
655
|
|
|
626
656
|
// src/install/promote.ts
|
|
627
657
|
import { cpSync as cpSync2, existsSync as existsSync4, mkdirSync as mkdirSync4, renameSync as renameSync2, rmSync as rmSync2 } from "fs";
|
|
628
|
-
import { basename as basename3, join as
|
|
658
|
+
import { basename as basename3, join as join6 } from "path";
|
|
659
|
+
|
|
660
|
+
// src/install/fingerprint.ts
|
|
661
|
+
import { createHash } from "crypto";
|
|
662
|
+
import { lstatSync, readFileSync as readFileSync3, readdirSync as readdirSync3, readlinkSync, realpathSync } from "fs";
|
|
663
|
+
import { isAbsolute, join as join5, relative, sep } from "path";
|
|
664
|
+
var SKIP_DIRS3 = /* @__PURE__ */ new Set([".git", "node_modules"]);
|
|
665
|
+
function isInside(child, root) {
|
|
666
|
+
const rel = relative(root, child);
|
|
667
|
+
return rel === "" || !rel.startsWith(".." + sep) && rel !== ".." && !isAbsolute(rel);
|
|
668
|
+
}
|
|
669
|
+
function fingerprintTree(root) {
|
|
670
|
+
const hash = createHash("sha256");
|
|
671
|
+
const symlinks = [];
|
|
672
|
+
const escaping = [];
|
|
673
|
+
const realRoot = realpathSync(root);
|
|
674
|
+
function walk2(dir) {
|
|
675
|
+
const entries = readdirSync3(dir, { withFileTypes: true }).sort((a, b) => a.name < b.name ? -1 : a.name > b.name ? 1 : 0);
|
|
676
|
+
for (const entry of entries) {
|
|
677
|
+
const full = join5(dir, entry.name);
|
|
678
|
+
const rel = relative(root, full);
|
|
679
|
+
if (entry.isSymbolicLink()) {
|
|
680
|
+
const target = readlinkSync(full);
|
|
681
|
+
symlinks.push(`${rel} -> ${target}`);
|
|
682
|
+
hash.update(`L ${rel} ${target}
|
|
683
|
+
`);
|
|
684
|
+
try {
|
|
685
|
+
if (!isInside(realpathSync(full), realRoot)) escaping.push(rel);
|
|
686
|
+
} catch {
|
|
687
|
+
escaping.push(rel);
|
|
688
|
+
}
|
|
689
|
+
continue;
|
|
690
|
+
}
|
|
691
|
+
if (entry.isDirectory()) {
|
|
692
|
+
if (SKIP_DIRS3.has(entry.name)) continue;
|
|
693
|
+
hash.update(`D ${rel}
|
|
694
|
+
`);
|
|
695
|
+
walk2(full);
|
|
696
|
+
continue;
|
|
697
|
+
}
|
|
698
|
+
if (entry.isFile()) {
|
|
699
|
+
const contentHash = createHash("sha256").update(readFileSync3(full)).digest("hex");
|
|
700
|
+
hash.update(`F ${rel} ${contentHash}
|
|
701
|
+
`);
|
|
702
|
+
continue;
|
|
703
|
+
}
|
|
704
|
+
hash.update(`O ${rel}
|
|
705
|
+
`);
|
|
706
|
+
}
|
|
707
|
+
}
|
|
708
|
+
walk2(root);
|
|
709
|
+
return { digest: hash.digest("hex"), symlinks, escaping };
|
|
710
|
+
}
|
|
711
|
+
function fingerprintDirs(dirs) {
|
|
712
|
+
const hash = createHash("sha256");
|
|
713
|
+
const symlinks = [];
|
|
714
|
+
const escaping = [];
|
|
715
|
+
for (const dir of [...dirs].sort()) {
|
|
716
|
+
let result;
|
|
717
|
+
try {
|
|
718
|
+
result = fingerprintTree(dir);
|
|
719
|
+
} catch (err) {
|
|
720
|
+
hash.update(`MISSING ${dir} ${err.message}
|
|
721
|
+
`);
|
|
722
|
+
continue;
|
|
723
|
+
}
|
|
724
|
+
hash.update(`DIR ${dir} ${result.digest}
|
|
725
|
+
`);
|
|
726
|
+
symlinks.push(...result.symlinks.map((s) => `${dir}: ${s}`));
|
|
727
|
+
escaping.push(...result.escaping.map((s) => `${dir}: ${s}`));
|
|
728
|
+
}
|
|
729
|
+
return { digest: hash.digest("hex"), symlinks, escaping };
|
|
730
|
+
}
|
|
731
|
+
function isRealDirectory(path) {
|
|
732
|
+
try {
|
|
733
|
+
return lstatSync(path).isDirectory();
|
|
734
|
+
} catch {
|
|
735
|
+
return false;
|
|
736
|
+
}
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
// src/install/promote.ts
|
|
629
740
|
function promote(entry, targetSkillsRoot, options = {}) {
|
|
630
741
|
const force = options.force ?? false;
|
|
631
742
|
for (const skillDir of entry.skillDirs) {
|
|
632
|
-
|
|
743
|
+
if (!isRealDirectory(skillDir)) {
|
|
744
|
+
throw new UserError(
|
|
745
|
+
`refusing to promote ${skillDir}: it is no longer a real directory (a symlink or a deleted path) \u2014 re-run the gate on this candidate`
|
|
746
|
+
);
|
|
747
|
+
}
|
|
748
|
+
const dest = join6(targetSkillsRoot, basename3(skillDir));
|
|
633
749
|
if (existsSync4(dest) && !force) {
|
|
634
750
|
throw new Error(`Refusing to overwrite existing skill dir: ${dest} (pass force to override)`);
|
|
635
751
|
}
|
|
636
752
|
}
|
|
753
|
+
const fingerprint = fingerprintDirs(entry.skillDirs);
|
|
754
|
+
if (fingerprint.escaping.length > 0) {
|
|
755
|
+
throw new UserError(
|
|
756
|
+
`refusing to promote ${entry.id}: ${fingerprint.escaping.length} symlink(s) inside the candidate resolve outside the quarantine sandbox, and would land in your skills root still pointing there:
|
|
757
|
+
` + fingerprint.escaping.join("\n ")
|
|
758
|
+
);
|
|
759
|
+
}
|
|
760
|
+
if (options.expectedFingerprint !== void 0 && options.expectedFingerprint !== fingerprint.digest) {
|
|
761
|
+
throw new UserError(
|
|
762
|
+
`refusing to promote ${entry.id}: the candidate's contents changed after it was gated \u2014 what was scanned is not what would be installed. Re-run \`skill-forge scan\` on the source.`
|
|
763
|
+
);
|
|
764
|
+
}
|
|
637
765
|
mkdirSync4(targetSkillsRoot, { recursive: true });
|
|
638
766
|
const promoted = [];
|
|
639
767
|
for (const skillDir of entry.skillDirs) {
|
|
640
|
-
const dest =
|
|
768
|
+
const dest = join6(targetSkillsRoot, basename3(skillDir));
|
|
641
769
|
if (existsSync4(dest)) {
|
|
642
770
|
rmSync2(dest, { recursive: true, force: true });
|
|
643
771
|
}
|
|
@@ -666,7 +794,7 @@ function moveDir(src, dest) {
|
|
|
666
794
|
}
|
|
667
795
|
|
|
668
796
|
// src/install/promoteMcp.ts
|
|
669
|
-
import { existsSync as existsSync5, mkdirSync as mkdirSync5, readFileSync as
|
|
797
|
+
import { existsSync as existsSync5, mkdirSync as mkdirSync5, readFileSync as readFileSync4, writeFileSync as writeFileSync3 } from "fs";
|
|
670
798
|
import { dirname as dirname2 } from "path";
|
|
671
799
|
function promoteMcp(_entry, profile, targetConfigPath, options = {}) {
|
|
672
800
|
const force = options.force ?? false;
|
|
@@ -675,7 +803,7 @@ function promoteMcp(_entry, profile, targetConfigPath, options = {}) {
|
|
|
675
803
|
let config = {};
|
|
676
804
|
let rawExisting;
|
|
677
805
|
if (existsSync5(targetConfigPath)) {
|
|
678
|
-
rawExisting =
|
|
806
|
+
rawExisting = readFileSync4(targetConfigPath, "utf-8");
|
|
679
807
|
try {
|
|
680
808
|
config = JSON.parse(rawExisting);
|
|
681
809
|
} catch (err) {
|
|
@@ -759,11 +887,11 @@ function deriveServerEntry(profile) {
|
|
|
759
887
|
}
|
|
760
888
|
|
|
761
889
|
// src/install/mcpCandidate.ts
|
|
762
|
-
import { readFileSync as
|
|
763
|
-
import { dirname as dirname3, join as
|
|
890
|
+
import { readFileSync as readFileSync5 } from "fs";
|
|
891
|
+
import { dirname as dirname3, join as join7 } from "path";
|
|
764
892
|
function readPackageInfo(pkgPath) {
|
|
765
893
|
try {
|
|
766
|
-
const pkg2 = JSON.parse(
|
|
894
|
+
const pkg2 = JSON.parse(readFileSync5(pkgPath, "utf-8"));
|
|
767
895
|
return {
|
|
768
896
|
name: typeof pkg2.name === "string" ? pkg2.name : void 0,
|
|
769
897
|
version: typeof pkg2.version === "string" ? pkg2.version : void 0
|
|
@@ -774,7 +902,7 @@ function readPackageInfo(pkgPath) {
|
|
|
774
902
|
}
|
|
775
903
|
function readConfigEntry(configPath, name) {
|
|
776
904
|
try {
|
|
777
|
-
const cfg = JSON.parse(
|
|
905
|
+
const cfg = JSON.parse(readFileSync5(configPath, "utf-8"));
|
|
778
906
|
const servers = cfg.mcpServers ?? cfg.servers ?? {};
|
|
779
907
|
const raw = servers[name] ?? {};
|
|
780
908
|
return {
|
|
@@ -792,7 +920,7 @@ function resolveMcpCandidateDetail(profile) {
|
|
|
792
920
|
return { name, version: readPackageInfo(profile.config).version };
|
|
793
921
|
}
|
|
794
922
|
const entry = readConfigEntry(profile.config, name);
|
|
795
|
-
const sidecar = readPackageInfo(
|
|
923
|
+
const sidecar = readPackageInfo(join7(dirname3(profile.config), "package.json"));
|
|
796
924
|
return {
|
|
797
925
|
name,
|
|
798
926
|
command: entry?.command,
|
|
@@ -804,8 +932,8 @@ function resolveMcpCandidateDetail(profile) {
|
|
|
804
932
|
}
|
|
805
933
|
|
|
806
934
|
// src/provenance.ts
|
|
807
|
-
import { appendFileSync, existsSync as existsSync6, readFileSync as
|
|
808
|
-
import { join as
|
|
935
|
+
import { appendFileSync, existsSync as existsSync6, readFileSync as readFileSync6, statSync as statSync3, writeFileSync as writeFileSync4 } from "fs";
|
|
936
|
+
import { join as join8 } from "path";
|
|
809
937
|
var FORGE_VERBS = ["DEFER", "ABSORB", "FORK", "REJECT", "WATCH"];
|
|
810
938
|
var LEDGER_HEADER = "# Skill Forge \u2014 Provenance Ledger\n\nOne entry per external-skill ingestion decision.\n";
|
|
811
939
|
function todayLocalIsoDate() {
|
|
@@ -836,7 +964,7 @@ function appendProvenance(entry, skillsRoot) {
|
|
|
836
964
|
- **Drift check:** \`${driftCheck}\`
|
|
837
965
|
- **Notes:** ${entry.notes ?? ""}
|
|
838
966
|
`;
|
|
839
|
-
const ledgerPath =
|
|
967
|
+
const ledgerPath = join8(skillsRoot, "SOURCES.md");
|
|
840
968
|
if (!existsSync6(ledgerPath)) {
|
|
841
969
|
writeFileSync4(ledgerPath, LEDGER_HEADER, "utf-8");
|
|
842
970
|
}
|
|
@@ -884,41 +1012,41 @@ function parseProvenanceLedger(text) {
|
|
|
884
1012
|
return { entries, warnings };
|
|
885
1013
|
}
|
|
886
1014
|
function readProvenanceLedger(skillsRoot) {
|
|
887
|
-
const ledgerPath =
|
|
1015
|
+
const ledgerPath = join8(skillsRoot, "SOURCES.md");
|
|
888
1016
|
if (!existsSync6(ledgerPath)) {
|
|
889
1017
|
return { ledgerPath, exists: false, entries: [], warnings: [] };
|
|
890
1018
|
}
|
|
891
|
-
const text =
|
|
1019
|
+
const text = readFileSync6(ledgerPath, "utf-8");
|
|
892
1020
|
const parsed = parseProvenanceLedger(text);
|
|
893
1021
|
return { ledgerPath, exists: true, ...parsed };
|
|
894
1022
|
}
|
|
895
1023
|
|
|
896
1024
|
// src/commands/gatePipeline.ts
|
|
897
|
-
import { readdirSync as
|
|
1025
|
+
import { readdirSync as readdirSync7 } from "fs";
|
|
898
1026
|
|
|
899
1027
|
// src/auditState.ts
|
|
900
|
-
import { existsSync as existsSync9, readFileSync as
|
|
901
|
-
import { basename as basename7, dirname as dirname5, join as
|
|
1028
|
+
import { existsSync as existsSync9, readFileSync as readFileSync9 } from "fs";
|
|
1029
|
+
import { basename as basename7, dirname as dirname5, join as join12 } from "path";
|
|
902
1030
|
|
|
903
1031
|
// src/audit.ts
|
|
904
1032
|
import {
|
|
905
1033
|
existsSync as existsSync8,
|
|
906
|
-
lstatSync,
|
|
1034
|
+
lstatSync as lstatSync2,
|
|
907
1035
|
mkdtempSync,
|
|
908
|
-
readFileSync as
|
|
909
|
-
readdirSync as
|
|
910
|
-
realpathSync,
|
|
1036
|
+
readFileSync as readFileSync8,
|
|
1037
|
+
readdirSync as readdirSync5,
|
|
1038
|
+
realpathSync as realpathSync2,
|
|
911
1039
|
renameSync as renameSync3,
|
|
912
1040
|
rmSync as rmSync3,
|
|
913
1041
|
statSync as statSync6,
|
|
914
1042
|
writeFileSync as writeFileSync5
|
|
915
1043
|
} from "fs";
|
|
916
|
-
import { basename as basename6, delimiter as delimiter3, extname as extname3, join as
|
|
1044
|
+
import { basename as basename6, delimiter as delimiter3, extname as extname3, join as join11, resolve as resolvePath2 } from "path";
|
|
917
1045
|
|
|
918
1046
|
// src/gate/safety.ts
|
|
919
1047
|
import { statSync as statSync4 } from "fs";
|
|
920
1048
|
import { execFile } from "child_process";
|
|
921
|
-
import { delimiter as delimiter2, join as
|
|
1049
|
+
import { delimiter as delimiter2, join as join9 } from "path";
|
|
922
1050
|
import { promisify } from "util";
|
|
923
1051
|
var execFileAsync = promisify(execFile);
|
|
924
1052
|
var DENY_RULES = [
|
|
@@ -1017,7 +1145,7 @@ function which(cmd) {
|
|
|
1017
1145
|
const exts = process.platform === "win32" ? (process.env.PATHEXT ?? ".EXE;.CMD;.BAT").split(";") : [""];
|
|
1018
1146
|
for (const dir of dirs) {
|
|
1019
1147
|
for (const ext of exts) {
|
|
1020
|
-
const candidate =
|
|
1148
|
+
const candidate = join9(dir, cmd + ext);
|
|
1021
1149
|
try {
|
|
1022
1150
|
if (statSync4(candidate).isFile()) return candidate;
|
|
1023
1151
|
} catch {
|
|
@@ -1140,14 +1268,22 @@ function scanJsonConfig(file, parsed) {
|
|
|
1140
1268
|
}
|
|
1141
1269
|
if (node.env && typeof node.env === "object" && !Array.isArray(node.env)) {
|
|
1142
1270
|
for (const [key, value] of Object.entries(node.env)) {
|
|
1143
|
-
if (typeof value !== "string"
|
|
1144
|
-
|
|
1271
|
+
if (typeof value !== "string") continue;
|
|
1272
|
+
const kind = classifyCredentialAssignment(key, value);
|
|
1273
|
+
if (kind === "inline-secret") {
|
|
1145
1274
|
findings.push({
|
|
1146
1275
|
severity: "HIGH",
|
|
1147
1276
|
rule: "mcp-inline-credential-value",
|
|
1148
1277
|
file,
|
|
1149
1278
|
detail: `env.${key} has an inline credential value \u2014 emit an empty string and document that the user must fill it in`
|
|
1150
1279
|
});
|
|
1280
|
+
} else if (kind === "file-path") {
|
|
1281
|
+
findings.push({
|
|
1282
|
+
severity: "LOW",
|
|
1283
|
+
rule: "mcp-credential-file-path",
|
|
1284
|
+
file,
|
|
1285
|
+
detail: `env.${key} points at a credential file rather than an inline secret \u2014 the value leaks nothing, but keep that file out of version control`
|
|
1286
|
+
});
|
|
1151
1287
|
}
|
|
1152
1288
|
}
|
|
1153
1289
|
}
|
|
@@ -1159,15 +1295,29 @@ var DANGEROUS_FLAG_RE = /--dangerously-[\w-]+|--no-sandbox\b/g;
|
|
|
1159
1295
|
var CREDENTIAL_ASSIGNMENT_RE = /["']?([A-Za-z][A-Za-z0-9_]*)["']?\s*[:=]\s*["']([^"']{6,})["']/g;
|
|
1160
1296
|
var CREDENTIAL_ASSIGNMENT_UNQUOTED_RE = /^\s*(?:export\s+|-\s+)?([A-Za-z][A-Za-z0-9_]*)\s*[:=]\s*(\S+)/;
|
|
1161
1297
|
var LEADING_QUOTE_RE = /^["']/;
|
|
1298
|
+
function classifyCredentialAssignment(key, value) {
|
|
1299
|
+
if (isPlaceholderValue(value)) return "none";
|
|
1300
|
+
const credentialKey = looksCredentialKey(key);
|
|
1301
|
+
if (credentialKey && looksStructuredNonSecret(value) && !/^[a-z]+:\/\//i.test(value.trim())) return "file-path";
|
|
1302
|
+
if (credentialKey || looksHighEntropy(value)) return "inline-secret";
|
|
1303
|
+
return "none";
|
|
1304
|
+
}
|
|
1162
1305
|
function pushCredentialFinding(findings, file, lineNo, varName, value) {
|
|
1163
|
-
|
|
1164
|
-
if (
|
|
1306
|
+
const kind = classifyCredentialAssignment(varName, value);
|
|
1307
|
+
if (kind === "inline-secret") {
|
|
1165
1308
|
findings.push({
|
|
1166
1309
|
severity: "HIGH",
|
|
1167
1310
|
rule: "mcp-inline-credential-value",
|
|
1168
1311
|
file,
|
|
1169
1312
|
detail: `line ${lineNo}: inline credential value assigned to ${varName}`
|
|
1170
1313
|
});
|
|
1314
|
+
} else if (kind === "file-path") {
|
|
1315
|
+
findings.push({
|
|
1316
|
+
severity: "LOW",
|
|
1317
|
+
rule: "mcp-credential-file-path",
|
|
1318
|
+
file,
|
|
1319
|
+
detail: `line ${lineNo}: ${varName} points at a credential file rather than an inline secret \u2014 the value leaks nothing, but keep that file out of version control`
|
|
1320
|
+
});
|
|
1171
1321
|
}
|
|
1172
1322
|
}
|
|
1173
1323
|
function scanTextFile(file, content, allowCredentialCheck) {
|
|
@@ -1243,8 +1393,8 @@ async function mcpScan(targetPath, strictness = DEFAULT_STRICTNESS) {
|
|
|
1243
1393
|
}
|
|
1244
1394
|
|
|
1245
1395
|
// src/gate/overlap.ts
|
|
1246
|
-
import { existsSync as existsSync7, readFileSync as
|
|
1247
|
-
import { basename as basename5, dirname as dirname4, join as
|
|
1396
|
+
import { existsSync as existsSync7, readFileSync as readFileSync7, readdirSync as readdirSync4 } from "fs";
|
|
1397
|
+
import { basename as basename5, dirname as dirname4, join as join10, resolve as resolve2 } from "path";
|
|
1248
1398
|
var STOPWORDS = new Set(
|
|
1249
1399
|
`
|
|
1250
1400
|
a an the and or of to for in on with use used using when whether this that these those is are be
|
|
@@ -1253,20 +1403,20 @@ skill skills user claude code based comprehensive enhanced practical including i
|
|
|
1253
1403
|
`.split(/\s+/).filter(Boolean)
|
|
1254
1404
|
);
|
|
1255
1405
|
var DEFAULT_SET_THRESHOLD = 0.45;
|
|
1256
|
-
function splitLimited(text,
|
|
1406
|
+
function splitLimited(text, sep5, maxSplit) {
|
|
1257
1407
|
const parts = [];
|
|
1258
1408
|
let rest = text;
|
|
1259
1409
|
for (let i = 0; i < maxSplit; i++) {
|
|
1260
|
-
const idx = rest.indexOf(
|
|
1410
|
+
const idx = rest.indexOf(sep5);
|
|
1261
1411
|
if (idx === -1) break;
|
|
1262
1412
|
parts.push(rest.slice(0, idx));
|
|
1263
|
-
rest = rest.slice(idx +
|
|
1413
|
+
rest = rest.slice(idx + sep5.length);
|
|
1264
1414
|
}
|
|
1265
1415
|
parts.push(rest);
|
|
1266
1416
|
return parts;
|
|
1267
1417
|
}
|
|
1268
1418
|
function readFrontmatter(skillMdPath) {
|
|
1269
|
-
const text =
|
|
1419
|
+
const text = readFileSync7(skillMdPath, "utf-8");
|
|
1270
1420
|
const meta = { name: basename5(dirname4(skillMdPath)), description: "" };
|
|
1271
1421
|
if (text.startsWith("---\n")) {
|
|
1272
1422
|
const block = splitLimited(text, "---", 2)[1] ?? "";
|
|
@@ -1364,25 +1514,25 @@ function listSubdirs(dir) {
|
|
|
1364
1514
|
if (!existsSync7(dir)) return [];
|
|
1365
1515
|
let entries;
|
|
1366
1516
|
try {
|
|
1367
|
-
entries =
|
|
1517
|
+
entries = readdirSync4(dir, { withFileTypes: true });
|
|
1368
1518
|
} catch {
|
|
1369
1519
|
return [];
|
|
1370
1520
|
}
|
|
1371
|
-
return entries.filter((e) => e.isDirectory()).map((e) =>
|
|
1521
|
+
return entries.filter((e) => e.isDirectory()).map((e) => join10(dir, e.name));
|
|
1372
1522
|
}
|
|
1373
1523
|
function findSkillMdFiles(root) {
|
|
1374
1524
|
const found = /* @__PURE__ */ new Set();
|
|
1375
1525
|
for (const dir of listSubdirs(root)) {
|
|
1376
|
-
const candidate =
|
|
1526
|
+
const candidate = join10(dir, "SKILL.md");
|
|
1377
1527
|
if (existsSync7(candidate)) found.add(resolve2(candidate));
|
|
1378
1528
|
}
|
|
1379
|
-
for (const dir of listSubdirs(
|
|
1380
|
-
const candidate =
|
|
1529
|
+
for (const dir of listSubdirs(join10(root, "skills"))) {
|
|
1530
|
+
const candidate = join10(dir, "SKILL.md");
|
|
1381
1531
|
if (existsSync7(candidate)) found.add(resolve2(candidate));
|
|
1382
1532
|
}
|
|
1383
1533
|
for (const topDir of listSubdirs(root)) {
|
|
1384
|
-
for (const dir of listSubdirs(
|
|
1385
|
-
const candidate =
|
|
1534
|
+
for (const dir of listSubdirs(join10(topDir, "skills"))) {
|
|
1535
|
+
const candidate = join10(dir, "SKILL.md");
|
|
1386
1536
|
if (existsSync7(candidate)) found.add(resolve2(candidate));
|
|
1387
1537
|
}
|
|
1388
1538
|
}
|
|
@@ -1395,7 +1545,7 @@ function scanOverlap(candidatePath, skillsRoot) {
|
|
|
1395
1545
|
}
|
|
1396
1546
|
let cpath = resolve2(expandHome(candidatePath));
|
|
1397
1547
|
if (isDir(cpath)) {
|
|
1398
|
-
cpath =
|
|
1548
|
+
cpath = join10(cpath, "SKILL.md");
|
|
1399
1549
|
}
|
|
1400
1550
|
if (!existsSync7(cpath)) {
|
|
1401
1551
|
throw new Error(`candidate SKILL.md not found: ${cpath}`);
|
|
@@ -1547,13 +1697,13 @@ function isDirFollowingSymlink(p) {
|
|
|
1547
1697
|
function listSubdirsFollowingSymlinks(dir) {
|
|
1548
1698
|
let entries;
|
|
1549
1699
|
try {
|
|
1550
|
-
entries =
|
|
1700
|
+
entries = readdirSync5(dir, { withFileTypes: true });
|
|
1551
1701
|
} catch {
|
|
1552
1702
|
return [];
|
|
1553
1703
|
}
|
|
1554
1704
|
const out = [];
|
|
1555
1705
|
for (const entry of entries) {
|
|
1556
|
-
const full =
|
|
1706
|
+
const full = join11(dir, entry.name);
|
|
1557
1707
|
if (entry.isDirectory() || entry.isSymbolicLink() && isDirFollowingSymlink(full)) {
|
|
1558
1708
|
out.push(full);
|
|
1559
1709
|
}
|
|
@@ -1563,24 +1713,24 @@ function listSubdirsFollowingSymlinks(dir) {
|
|
|
1563
1713
|
function listSubdirsNoSymlinks(dir) {
|
|
1564
1714
|
let entries;
|
|
1565
1715
|
try {
|
|
1566
|
-
entries =
|
|
1716
|
+
entries = readdirSync5(dir, { withFileTypes: true });
|
|
1567
1717
|
} catch {
|
|
1568
1718
|
return [];
|
|
1569
1719
|
}
|
|
1570
|
-
return entries.filter((e) => e.isDirectory()).map((e) =>
|
|
1720
|
+
return entries.filter((e) => e.isDirectory()).map((e) => join11(dir, e.name));
|
|
1571
1721
|
}
|
|
1572
1722
|
function findSkillDirsUnderRoot(root) {
|
|
1573
1723
|
const found = /* @__PURE__ */ new Set();
|
|
1574
1724
|
const rootSubdirs = listSubdirsFollowingSymlinks(root);
|
|
1575
1725
|
for (const dir of rootSubdirs) {
|
|
1576
|
-
if (existsSync8(
|
|
1726
|
+
if (existsSync8(join11(dir, "SKILL.md"))) found.add(dir);
|
|
1577
1727
|
}
|
|
1578
|
-
for (const dir of listSubdirsFollowingSymlinks(
|
|
1579
|
-
if (existsSync8(
|
|
1728
|
+
for (const dir of listSubdirsFollowingSymlinks(join11(root, "skills"))) {
|
|
1729
|
+
if (existsSync8(join11(dir, "SKILL.md"))) found.add(dir);
|
|
1580
1730
|
}
|
|
1581
1731
|
for (const topDir of rootSubdirs) {
|
|
1582
|
-
for (const dir of listSubdirsNoSymlinks(
|
|
1583
|
-
if (existsSync8(
|
|
1732
|
+
for (const dir of listSubdirsNoSymlinks(join11(topDir, "skills"))) {
|
|
1733
|
+
if (existsSync8(join11(dir, "SKILL.md"))) found.add(dir);
|
|
1584
1734
|
}
|
|
1585
1735
|
}
|
|
1586
1736
|
return Array.from(found).filter((p) => !p.includes("/.git/"));
|
|
@@ -1594,7 +1744,7 @@ function collectSkillCandidates(roots) {
|
|
|
1594
1744
|
let realPath;
|
|
1595
1745
|
if (exists) {
|
|
1596
1746
|
try {
|
|
1597
|
-
realPath =
|
|
1747
|
+
realPath = realpathSync2(resolvedRoot);
|
|
1598
1748
|
} catch {
|
|
1599
1749
|
realPath = void 0;
|
|
1600
1750
|
}
|
|
@@ -1613,7 +1763,7 @@ function dedupeSkillCandidates(candidates, findings) {
|
|
|
1613
1763
|
for (const candidate of candidates) {
|
|
1614
1764
|
let realPath;
|
|
1615
1765
|
try {
|
|
1616
|
-
realPath =
|
|
1766
|
+
realPath = realpathSync2(candidate.configuredPath);
|
|
1617
1767
|
} catch (err) {
|
|
1618
1768
|
findings.push({
|
|
1619
1769
|
severity: "MEDIUM",
|
|
@@ -1676,7 +1826,7 @@ function estimateTokens(text) {
|
|
|
1676
1826
|
return Math.ceil(Buffer.byteLength(text, "utf-8") / 4);
|
|
1677
1827
|
}
|
|
1678
1828
|
function profileAuditedSkill(group, findings, unreadableSkillMdPaths) {
|
|
1679
|
-
const skillMdPath =
|
|
1829
|
+
const skillMdPath = join11(group.canonicalPath, "SKILL.md");
|
|
1680
1830
|
const base = {
|
|
1681
1831
|
path: group.canonicalPath,
|
|
1682
1832
|
skillMdPath,
|
|
@@ -1687,7 +1837,7 @@ function profileAuditedSkill(group, findings, unreadableSkillMdPaths) {
|
|
|
1687
1837
|
let sizeBytes;
|
|
1688
1838
|
try {
|
|
1689
1839
|
sizeBytes = statSync6(skillMdPath).size;
|
|
1690
|
-
text =
|
|
1840
|
+
text = readFileSync8(skillMdPath, "utf-8");
|
|
1691
1841
|
} catch (err) {
|
|
1692
1842
|
findings.push({
|
|
1693
1843
|
severity: "MEDIUM",
|
|
@@ -1765,7 +1915,7 @@ function auditMcpTarget(targetPath, notices) {
|
|
|
1765
1915
|
}
|
|
1766
1916
|
if (isToml) {
|
|
1767
1917
|
try {
|
|
1768
|
-
|
|
1918
|
+
readFileSync8(targetPath, "utf-8");
|
|
1769
1919
|
} catch (err) {
|
|
1770
1920
|
findings.push({
|
|
1771
1921
|
severity: "MEDIUM",
|
|
@@ -1780,7 +1930,7 @@ function auditMcpTarget(targetPath, notices) {
|
|
|
1780
1930
|
}
|
|
1781
1931
|
let raw;
|
|
1782
1932
|
try {
|
|
1783
|
-
raw =
|
|
1933
|
+
raw = readFileSync8(targetPath, "utf-8");
|
|
1784
1934
|
} catch (err) {
|
|
1785
1935
|
findings.push({
|
|
1786
1936
|
severity: "MEDIUM",
|
|
@@ -1856,7 +2006,7 @@ function defaultWhich(cmd) {
|
|
|
1856
2006
|
const exts = process.platform === "win32" ? (process.env.PATHEXT ?? ".EXE;.CMD;.BAT").split(";") : [""];
|
|
1857
2007
|
for (const dir of dirs) {
|
|
1858
2008
|
for (const ext of exts) {
|
|
1859
|
-
const candidate =
|
|
2009
|
+
const candidate = join11(dir, cmd + ext);
|
|
1860
2010
|
try {
|
|
1861
2011
|
if (statSync6(candidate).isFile()) return candidate;
|
|
1862
2012
|
} catch {
|
|
@@ -2208,10 +2358,10 @@ async function scaffoldFoundationSkill(config, profile, targetRoot) {
|
|
|
2208
2358
|
if (!isDir(resolvedRoot)) {
|
|
2209
2359
|
throw new UserError(`skills root does not exist: ${resolvedRoot}`);
|
|
2210
2360
|
}
|
|
2211
|
-
const destDir =
|
|
2361
|
+
const destDir = join11(resolvedRoot, "business-foundation");
|
|
2212
2362
|
let destLstat;
|
|
2213
2363
|
try {
|
|
2214
|
-
destLstat =
|
|
2364
|
+
destLstat = lstatSync2(destDir);
|
|
2215
2365
|
} catch {
|
|
2216
2366
|
destLstat = void 0;
|
|
2217
2367
|
}
|
|
@@ -2222,9 +2372,9 @@ async function scaffoldFoundationSkill(config, profile, targetRoot) {
|
|
|
2222
2372
|
throw new UserError(`refusing to scaffold a foundation skill at ${destDir}: already exists`);
|
|
2223
2373
|
}
|
|
2224
2374
|
const content = renderFoundationSkillMd(profile);
|
|
2225
|
-
const tempDir = mkdtempSync(
|
|
2375
|
+
const tempDir = mkdtempSync(join11(resolvedRoot, ".business-foundation-"));
|
|
2226
2376
|
try {
|
|
2227
|
-
writeFileSync5(
|
|
2377
|
+
writeFileSync5(join11(tempDir, "SKILL.md"), content, { flag: "wx", mode: 384 });
|
|
2228
2378
|
const safety = await scanSafety(tempDir, config.strictness);
|
|
2229
2379
|
if (safety.verdict === "block") {
|
|
2230
2380
|
throw new UserError(
|
|
@@ -2258,7 +2408,7 @@ async function scaffoldFoundationSkill(config, profile, targetRoot) {
|
|
|
2258
2408
|
|
|
2259
2409
|
// src/auditState.ts
|
|
2260
2410
|
function getAuditStatePath() {
|
|
2261
|
-
return
|
|
2411
|
+
return join12(getHomeDir(), "audit-state.json");
|
|
2262
2412
|
}
|
|
2263
2413
|
function emptyCounts() {
|
|
2264
2414
|
return { CRITICAL: 0, HIGH: 0, MEDIUM: 0, LOW: 0 };
|
|
@@ -2315,7 +2465,7 @@ function readAuditState() {
|
|
|
2315
2465
|
const path = getAuditStatePath();
|
|
2316
2466
|
if (!existsSync9(path)) return void 0;
|
|
2317
2467
|
try {
|
|
2318
|
-
const parsed = JSON.parse(
|
|
2468
|
+
const parsed = JSON.parse(readFileSync9(path, "utf-8"));
|
|
2319
2469
|
if (parsed.schemaVersion !== 1 || typeof parsed.lastRunAt !== "string") return void 0;
|
|
2320
2470
|
return {
|
|
2321
2471
|
schemaVersion: 1,
|
|
@@ -2376,7 +2526,7 @@ function formatAuditAdvisory(state, options = {}) {
|
|
|
2376
2526
|
}
|
|
2377
2527
|
|
|
2378
2528
|
// src/commands/gatePipeline.ts
|
|
2379
|
-
import { join as
|
|
2529
|
+
import { join as join15 } from "path";
|
|
2380
2530
|
|
|
2381
2531
|
// src/gate/profile.ts
|
|
2382
2532
|
import { execFileSync } from "child_process";
|
|
@@ -2385,23 +2535,23 @@ import {
|
|
|
2385
2535
|
existsSync as existsSync11,
|
|
2386
2536
|
mkdtempSync as mkdtempSync2,
|
|
2387
2537
|
openSync,
|
|
2388
|
-
readFileSync as
|
|
2389
|
-
readdirSync as
|
|
2538
|
+
readFileSync as readFileSync11,
|
|
2539
|
+
readdirSync as readdirSync6,
|
|
2390
2540
|
readSync,
|
|
2391
2541
|
rmSync as rmSync4,
|
|
2392
2542
|
statSync as statSync7
|
|
2393
2543
|
} from "fs";
|
|
2394
2544
|
import { tmpdir } from "os";
|
|
2395
|
-
import { basename as basename8, dirname as dirname6, extname as extname5, join as
|
|
2545
|
+
import { basename as basename8, dirname as dirname6, extname as extname5, join as join14, resolve as resolve3 } from "path";
|
|
2396
2546
|
|
|
2397
2547
|
// src/gate/mcpCapabilities.ts
|
|
2398
|
-
import { existsSync as existsSync10, readFileSync as
|
|
2399
|
-
import { extname as extname4, join as
|
|
2548
|
+
import { existsSync as existsSync10, readFileSync as readFileSync10 } from "fs";
|
|
2549
|
+
import { extname as extname4, join as join13 } from "path";
|
|
2400
2550
|
var SOURCE_EXT = /* @__PURE__ */ new Set([".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs"]);
|
|
2401
2551
|
var MAX_ITEMS_PER_CATEGORY = 200;
|
|
2402
2552
|
function readJsonSafe(file) {
|
|
2403
2553
|
try {
|
|
2404
|
-
return JSON.parse(
|
|
2554
|
+
return JSON.parse(readFileSync10(file, "utf-8"));
|
|
2405
2555
|
} catch {
|
|
2406
2556
|
return void 0;
|
|
2407
2557
|
}
|
|
@@ -2502,7 +2652,7 @@ var MCP_SDK_IMPORT_RE = /@modelcontextprotocol\/sdk/;
|
|
|
2502
2652
|
function extractMcpCapabilities(packageDir) {
|
|
2503
2653
|
const buckets = { tools: /* @__PURE__ */ new Set(), resources: /* @__PURE__ */ new Set(), prompts: /* @__PURE__ */ new Set() };
|
|
2504
2654
|
let hasPackageJsonMarker = false;
|
|
2505
|
-
const pkgPath =
|
|
2655
|
+
const pkgPath = join13(packageDir, "package.json");
|
|
2506
2656
|
if (existsSync10(pkgPath)) {
|
|
2507
2657
|
const pkg2 = readJsonSafe(pkgPath);
|
|
2508
2658
|
if (pkg2 && typeof pkg2 === "object" && !Array.isArray(pkg2)) {
|
|
@@ -2510,7 +2660,7 @@ function extractMcpCapabilities(packageDir) {
|
|
|
2510
2660
|
}
|
|
2511
2661
|
}
|
|
2512
2662
|
for (const manifestName of [".mcp.json", "mcp.json"]) {
|
|
2513
|
-
const manifestPath =
|
|
2663
|
+
const manifestPath = join13(packageDir, manifestName);
|
|
2514
2664
|
if (!existsSync10(manifestPath)) continue;
|
|
2515
2665
|
const parsed = readJsonSafe(manifestPath);
|
|
2516
2666
|
if (parsed !== void 0) {
|
|
@@ -2618,7 +2768,7 @@ function asProfile(candidatePath, artifact) {
|
|
|
2618
2768
|
return asSkillProfile(profile, `at ${candidatePath}`);
|
|
2619
2769
|
}
|
|
2620
2770
|
function classifyInput(p) {
|
|
2621
|
-
if (existsSync11(
|
|
2771
|
+
if (existsSync11(join14(p, ".claude-plugin", "plugin.json"))) return "plugin";
|
|
2622
2772
|
if (detectArtifactType(p) === "mcp") return "mcp";
|
|
2623
2773
|
return "skill";
|
|
2624
2774
|
}
|
|
@@ -2626,10 +2776,10 @@ var MCP_SDK_DEP2 = "@modelcontextprotocol/sdk";
|
|
|
2626
2776
|
var MCP_KEYWORD2 = "mcp-server";
|
|
2627
2777
|
var MCP_BIN_PREFIX2 = /^mcp-/i;
|
|
2628
2778
|
function readPackageJsonSafe(root) {
|
|
2629
|
-
const pkgPath =
|
|
2779
|
+
const pkgPath = join14(root, "package.json");
|
|
2630
2780
|
if (!existsSync11(pkgPath)) return void 0;
|
|
2631
2781
|
try {
|
|
2632
|
-
return JSON.parse(
|
|
2782
|
+
return JSON.parse(readFileSync11(pkgPath, "utf-8"));
|
|
2633
2783
|
} catch {
|
|
2634
2784
|
return void 0;
|
|
2635
2785
|
}
|
|
@@ -2650,7 +2800,7 @@ function hasMcpPackageMarkers(pkg2) {
|
|
|
2650
2800
|
return false;
|
|
2651
2801
|
}
|
|
2652
2802
|
function detectArtifactType(root) {
|
|
2653
|
-
if (existsSync11(
|
|
2803
|
+
if (existsSync11(join14(root, ".mcp.json")) || existsSync11(join14(root, "mcp.json"))) return "mcp";
|
|
2654
2804
|
const pkg2 = readPackageJsonSafe(root);
|
|
2655
2805
|
if (pkg2 && hasMcpPackageMarkers(pkg2)) return "mcp";
|
|
2656
2806
|
return "skill";
|
|
@@ -2660,7 +2810,7 @@ function resolveSkillMd(inputPath) {
|
|
|
2660
2810
|
let tmpDir;
|
|
2661
2811
|
const initialStat = statSync7(currentPath);
|
|
2662
2812
|
if (extname5(currentPath) === ".skill" || initialStat.isFile() && isZipFile(currentPath)) {
|
|
2663
|
-
tmpDir = mkdtempSync2(
|
|
2813
|
+
tmpDir = mkdtempSync2(join14(tmpdir(), "forge_"));
|
|
2664
2814
|
extractZip(currentPath, tmpDir);
|
|
2665
2815
|
currentPath = tmpDir;
|
|
2666
2816
|
}
|
|
@@ -2669,7 +2819,7 @@ function resolveSkillMd(inputPath) {
|
|
|
2669
2819
|
return { skillMd: currentPath, root: dirname6(currentPath), tmpDir };
|
|
2670
2820
|
}
|
|
2671
2821
|
if (stat.isDirectory()) {
|
|
2672
|
-
const direct =
|
|
2822
|
+
const direct = join14(currentPath, "SKILL.md");
|
|
2673
2823
|
if (existsSync11(direct)) {
|
|
2674
2824
|
return { skillMd: direct, root: currentPath, tmpDir };
|
|
2675
2825
|
}
|
|
@@ -2693,17 +2843,17 @@ function cleanupTmp(tmpDir) {
|
|
|
2693
2843
|
}
|
|
2694
2844
|
function findSkillMdOneLevelDown(dir) {
|
|
2695
2845
|
const matches = /* @__PURE__ */ new Set();
|
|
2696
|
-
for (const entry of
|
|
2846
|
+
for (const entry of readdirSync6(dir, { withFileTypes: true })) {
|
|
2697
2847
|
if (entry.isDirectory()) {
|
|
2698
|
-
const candidate =
|
|
2848
|
+
const candidate = join14(dir, entry.name, "SKILL.md");
|
|
2699
2849
|
if (existsSync11(candidate)) matches.add(candidate);
|
|
2700
2850
|
}
|
|
2701
2851
|
}
|
|
2702
|
-
const skillsDir =
|
|
2852
|
+
const skillsDir = join14(dir, "skills");
|
|
2703
2853
|
if (isDir(skillsDir)) {
|
|
2704
|
-
for (const entry of
|
|
2854
|
+
for (const entry of readdirSync6(skillsDir, { withFileTypes: true })) {
|
|
2705
2855
|
if (entry.isDirectory()) {
|
|
2706
|
-
const candidate =
|
|
2856
|
+
const candidate = join14(skillsDir, entry.name, "SKILL.md");
|
|
2707
2857
|
if (existsSync11(candidate)) matches.add(candidate);
|
|
2708
2858
|
}
|
|
2709
2859
|
}
|
|
@@ -2787,9 +2937,9 @@ function detectLicense(root, fm) {
|
|
|
2787
2937
|
if (fm.license) return fm.license;
|
|
2788
2938
|
const names = ["LICENSE", "LICENSE.md", "LICENSE.txt", "COPYING", "license"];
|
|
2789
2939
|
for (const name of names) {
|
|
2790
|
-
const p =
|
|
2940
|
+
const p = join14(root, name);
|
|
2791
2941
|
if (existsSync11(p) && statSync7(p).isFile()) {
|
|
2792
|
-
const head =
|
|
2942
|
+
const head = readFileSync11(p, "utf-8").slice(0, 2e3).toLowerCase();
|
|
2793
2943
|
for (const [hint, label] of Object.entries(SPDX_HINTS)) {
|
|
2794
2944
|
if (head.includes(hint)) return label;
|
|
2795
2945
|
}
|
|
@@ -2800,8 +2950,8 @@ function detectLicense(root, fm) {
|
|
|
2800
2950
|
}
|
|
2801
2951
|
function walkFiles(dir) {
|
|
2802
2952
|
const out = [];
|
|
2803
|
-
for (const entry of
|
|
2804
|
-
const full =
|
|
2953
|
+
for (const entry of readdirSync6(dir, { withFileTypes: true })) {
|
|
2954
|
+
const full = join14(dir, entry.name);
|
|
2805
2955
|
if (entry.isDirectory()) {
|
|
2806
2956
|
out.push(...walkFiles(full));
|
|
2807
2957
|
} else if (entry.isFile()) {
|
|
@@ -2813,7 +2963,7 @@ function walkFiles(dir) {
|
|
|
2813
2963
|
function countResources(root) {
|
|
2814
2964
|
const resources = {};
|
|
2815
2965
|
for (const d of RESOURCE_DIRS) {
|
|
2816
|
-
const dp =
|
|
2966
|
+
const dp = join14(root, d);
|
|
2817
2967
|
if (isDir(dp)) {
|
|
2818
2968
|
resources[d] = walkFiles(dp).length;
|
|
2819
2969
|
}
|
|
@@ -2824,11 +2974,11 @@ function scanMcpAndImports(text, root) {
|
|
|
2824
2974
|
const mcpRefRe = /mcp__[a-zA-Z0-9_]+/g;
|
|
2825
2975
|
const mcpRefs = new Set(text.match(mcpRefRe) ?? []);
|
|
2826
2976
|
const imports = /* @__PURE__ */ new Set();
|
|
2827
|
-
const scriptsDir =
|
|
2977
|
+
const scriptsDir = join14(root, "scripts");
|
|
2828
2978
|
if (isDir(scriptsDir)) {
|
|
2829
2979
|
for (const f of walkFiles(scriptsDir)) {
|
|
2830
2980
|
if (!f.endsWith(".py")) continue;
|
|
2831
|
-
const ft =
|
|
2981
|
+
const ft = readFileSync11(f, "utf-8");
|
|
2832
2982
|
for (const m2 of ft.match(mcpRefRe) ?? []) mcpRefs.add(m2);
|
|
2833
2983
|
const importRe = /^\s*(?:import|from)\s+([a-zA-Z0-9_]+)/gm;
|
|
2834
2984
|
let m;
|
|
@@ -2845,7 +2995,7 @@ function scanMcpAndImports(text, root) {
|
|
|
2845
2995
|
function profileSkillDir(inputPath) {
|
|
2846
2996
|
const { skillMd, root, tmpDir } = resolveSkillMd(inputPath);
|
|
2847
2997
|
try {
|
|
2848
|
-
const text =
|
|
2998
|
+
const text = readFileSync11(skillMd, "utf-8");
|
|
2849
2999
|
const fm = parseFrontmatter(text);
|
|
2850
3000
|
const bodyLines = (text.match(/\n/g)?.length ?? 0) + 1;
|
|
2851
3001
|
const headers = Array.from(text.matchAll(/^#{1,3}\s+(.+)$/gm)).map((m) => m[1]);
|
|
@@ -2873,20 +3023,20 @@ function profileSkillDir(inputPath) {
|
|
|
2873
3023
|
}
|
|
2874
3024
|
}
|
|
2875
3025
|
function profilePlugin(p) {
|
|
2876
|
-
const manifestPath =
|
|
3026
|
+
const manifestPath = join14(p, ".claude-plugin", "plugin.json");
|
|
2877
3027
|
let manifest;
|
|
2878
3028
|
try {
|
|
2879
|
-
manifest = JSON.parse(
|
|
3029
|
+
manifest = JSON.parse(readFileSync11(manifestPath, "utf-8"));
|
|
2880
3030
|
} catch (err) {
|
|
2881
3031
|
throw new Error(`could not parse plugin.json under ${p}: ${err.message}`);
|
|
2882
3032
|
}
|
|
2883
|
-
const skillsDir =
|
|
3033
|
+
const skillsDir = join14(p, "skills");
|
|
2884
3034
|
let skills = [];
|
|
2885
3035
|
if (isDir(skillsDir)) {
|
|
2886
|
-
skills =
|
|
3036
|
+
skills = readdirSync6(skillsDir, { withFileTypes: true }).filter((d) => d.isDirectory() && existsSync11(join14(skillsDir, d.name, "SKILL.md"))).map((d) => d.name).sort();
|
|
2887
3037
|
}
|
|
2888
|
-
const commandsDir =
|
|
2889
|
-
const commands = isDir(commandsDir) ?
|
|
3038
|
+
const commandsDir = join14(p, "commands");
|
|
3039
|
+
const commands = isDir(commandsDir) ? readdirSync6(commandsDir).filter((f) => f.endsWith(".md")).length : 0;
|
|
2890
3040
|
const mc = manifest.mcpServers ?? manifest.mcp_servers ?? {};
|
|
2891
3041
|
const mcpServers = typeof mc === "object" && mc !== null ? Object.keys(mc).sort() : [];
|
|
2892
3042
|
return {
|
|
@@ -2903,11 +3053,11 @@ function profilePlugin(p) {
|
|
|
2903
3053
|
};
|
|
2904
3054
|
}
|
|
2905
3055
|
function profileMcp(p) {
|
|
2906
|
-
const explicitConfig = existsSync11(
|
|
3056
|
+
const explicitConfig = existsSync11(join14(p, ".mcp.json")) ? join14(p, ".mcp.json") : existsSync11(join14(p, "mcp.json")) ? join14(p, "mcp.json") : void 0;
|
|
2907
3057
|
if (explicitConfig) {
|
|
2908
3058
|
let cfg;
|
|
2909
3059
|
try {
|
|
2910
|
-
cfg = JSON.parse(
|
|
3060
|
+
cfg = JSON.parse(readFileSync11(explicitConfig, "utf-8"));
|
|
2911
3061
|
} catch (err) {
|
|
2912
3062
|
throw new Error(`could not parse MCP config ${explicitConfig}: ${err.message}`);
|
|
2913
3063
|
}
|
|
@@ -2923,10 +3073,10 @@ function profileMcp(p) {
|
|
|
2923
3073
|
capabilities: extractMcpCapabilities(p)
|
|
2924
3074
|
};
|
|
2925
3075
|
}
|
|
2926
|
-
const pkgPath =
|
|
3076
|
+
const pkgPath = join14(p, "package.json");
|
|
2927
3077
|
let pkg2;
|
|
2928
3078
|
try {
|
|
2929
|
-
pkg2 = JSON.parse(
|
|
3079
|
+
pkg2 = JSON.parse(readFileSync11(pkgPath, "utf-8"));
|
|
2930
3080
|
} catch (err) {
|
|
2931
3081
|
throw new Error(`could not parse package.json ${pkgPath}: ${err.message}`);
|
|
2932
3082
|
}
|
|
@@ -2943,14 +3093,14 @@ function profileMcp(p) {
|
|
|
2943
3093
|
}
|
|
2944
3094
|
|
|
2945
3095
|
// src/gate/mcpOverlap.ts
|
|
2946
|
-
import { existsSync as existsSync12, readFileSync as
|
|
3096
|
+
import { existsSync as existsSync12, readFileSync as readFileSync12 } from "fs";
|
|
2947
3097
|
import { resolve as resolve4 } from "path";
|
|
2948
3098
|
function readTargetServers(targetPath) {
|
|
2949
3099
|
const path = resolve4(expandHome(targetPath));
|
|
2950
3100
|
if (!existsSync12(path)) return [];
|
|
2951
3101
|
let cfg;
|
|
2952
3102
|
try {
|
|
2953
|
-
cfg = JSON.parse(
|
|
3103
|
+
cfg = JSON.parse(readFileSync12(path, "utf-8"));
|
|
2954
3104
|
} catch {
|
|
2955
3105
|
return [];
|
|
2956
3106
|
}
|
|
@@ -3151,10 +3301,10 @@ function resolveMcpCandidatePath(entry) {
|
|
|
3151
3301
|
if (entry.packageDir) return entry.packageDir;
|
|
3152
3302
|
let children = [];
|
|
3153
3303
|
try {
|
|
3154
|
-
children =
|
|
3304
|
+
children = readdirSync7(entry.quarantinePath, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => d.name);
|
|
3155
3305
|
} catch {
|
|
3156
3306
|
}
|
|
3157
|
-
return children.length === 1 ?
|
|
3307
|
+
return children.length === 1 ? join15(entry.quarantinePath, children[0]) : entry.quarantinePath;
|
|
3158
3308
|
}
|
|
3159
3309
|
async function runGate(candidatePath, skillsRoot, {
|
|
3160
3310
|
proFeatureLabel = "overlap analysis",
|
|
@@ -3186,17 +3336,17 @@ async function runGate(candidatePath, skillsRoot, {
|
|
|
3186
3336
|
|
|
3187
3337
|
// src/queue.ts
|
|
3188
3338
|
import { randomUUID as randomUUID3 } from "crypto";
|
|
3189
|
-
import { existsSync as existsSync13, mkdirSync as mkdirSync6, readFileSync as
|
|
3190
|
-
import { join as
|
|
3339
|
+
import { existsSync as existsSync13, mkdirSync as mkdirSync6, readFileSync as readFileSync13, renameSync as renameSync4, rmSync as rmSync5, writeFileSync as writeFileSync6 } from "fs";
|
|
3340
|
+
import { join as join16 } from "path";
|
|
3191
3341
|
function getQueuePath() {
|
|
3192
|
-
return
|
|
3342
|
+
return join16(getHomeDir(), "queue.json");
|
|
3193
3343
|
}
|
|
3194
3344
|
function readQueue() {
|
|
3195
3345
|
const path = getQueuePath();
|
|
3196
3346
|
if (!existsSync13(path)) {
|
|
3197
3347
|
return { version: 1, entries: [] };
|
|
3198
3348
|
}
|
|
3199
|
-
const raw =
|
|
3349
|
+
const raw = readFileSync13(path, "utf-8");
|
|
3200
3350
|
const parsed = JSON.parse(raw);
|
|
3201
3351
|
return { version: 1, entries: parsed.entries ?? [] };
|
|
3202
3352
|
}
|
|
@@ -3204,7 +3354,7 @@ function writeQueue(queue2) {
|
|
|
3204
3354
|
const home = getHomeDir();
|
|
3205
3355
|
mkdirSync6(home, { recursive: true });
|
|
3206
3356
|
const finalPath = getQueuePath();
|
|
3207
|
-
const tempPath =
|
|
3357
|
+
const tempPath = join16(home, `.queue.json.tmp-${randomUUID3()}`);
|
|
3208
3358
|
try {
|
|
3209
3359
|
writeFileSync6(tempPath, JSON.stringify(queue2, null, 2) + "\n", "utf-8");
|
|
3210
3360
|
renameSync4(tempPath, finalPath);
|
|
@@ -3254,15 +3404,15 @@ function generateId(existingIds) {
|
|
|
3254
3404
|
// src/handoff.ts
|
|
3255
3405
|
import { spawnSync } from "child_process";
|
|
3256
3406
|
import { existsSync as existsSync14 } from "fs";
|
|
3257
|
-
import { dirname as dirname7, join as
|
|
3407
|
+
import { dirname as dirname7, join as join17 } from "path";
|
|
3258
3408
|
import { fileURLToPath } from "url";
|
|
3259
3409
|
var KNOWN_AGENT_BINARIES = ["claude", "codex", "cursor-agent", "windsurf", "opencode", "gemini"];
|
|
3260
3410
|
var HANDOFF_INSTRUCTION = "Read {prompt} and follow its instructions for the skill installed at {path}";
|
|
3261
3411
|
function resolvePromptPath(assetName = "ingest-prompt.md") {
|
|
3262
3412
|
const here = dirname7(fileURLToPath(import.meta.url));
|
|
3263
|
-
const builtCandidate =
|
|
3413
|
+
const builtCandidate = join17(here, assetName);
|
|
3264
3414
|
if (existsSync14(builtCandidate)) return builtCandidate;
|
|
3265
|
-
return
|
|
3415
|
+
return join17(here, "..", "assets", assetName);
|
|
3266
3416
|
}
|
|
3267
3417
|
function defaultIsOnPath(bin) {
|
|
3268
3418
|
const finder = process.platform === "win32" ? "where" : "which";
|
|
@@ -3325,7 +3475,7 @@ import { createInterface as createInterface3 } from "readline/promises";
|
|
|
3325
3475
|
// src/agents.ts
|
|
3326
3476
|
import { statSync as statSync8 } from "fs";
|
|
3327
3477
|
import { homedir as homedir3 } from "os";
|
|
3328
|
-
import { join as
|
|
3478
|
+
import { join as join18, resolve as resolve5 } from "path";
|
|
3329
3479
|
function isFile(p) {
|
|
3330
3480
|
try {
|
|
3331
3481
|
return statSync8(p).isFile();
|
|
@@ -3339,7 +3489,7 @@ function projectPath(root, cwd) {
|
|
|
3339
3489
|
function globalPath(root, home) {
|
|
3340
3490
|
if (!root) return void 0;
|
|
3341
3491
|
if (root === "~") return home;
|
|
3342
|
-
if (root.startsWith("~/")) return
|
|
3492
|
+
if (root.startsWith("~/")) return join18(home, root.slice(2));
|
|
3343
3493
|
return root;
|
|
3344
3494
|
}
|
|
3345
3495
|
function makeAgent(spec) {
|
|
@@ -3570,7 +3720,7 @@ var AGENTS = [
|
|
|
3570
3720
|
|
|
3571
3721
|
// src/commands/audit.ts
|
|
3572
3722
|
import { existsSync as existsSync15 } from "fs";
|
|
3573
|
-
import { join as
|
|
3723
|
+
import { join as join19, resolve as resolvePath3 } from "path";
|
|
3574
3724
|
import { createInterface as createInterface2 } from "readline/promises";
|
|
3575
3725
|
|
|
3576
3726
|
// src/decision.ts
|
|
@@ -3638,7 +3788,7 @@ async function captureBusinessProfile(rl, existing) {
|
|
|
3638
3788
|
function resolveReportPath(reportOption) {
|
|
3639
3789
|
if (reportOption) return resolvePath3(reportOption);
|
|
3640
3790
|
const iso = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
|
|
3641
|
-
return
|
|
3791
|
+
return join19(getHomeDir(), "reports", `audit-${iso}.md`);
|
|
3642
3792
|
}
|
|
3643
3793
|
function printTerminalSummary(report, reportPath) {
|
|
3644
3794
|
const { summary } = report;
|
|
@@ -4063,6 +4213,7 @@ async function add(source, options = {}) {
|
|
|
4063
4213
|
}
|
|
4064
4214
|
const resolved = resolve(source, artifact);
|
|
4065
4215
|
const entry = await quarantine(resolved, config.quarantineDir);
|
|
4216
|
+
const gatedFingerprint = fingerprintDirs(entry.skillDirs).digest;
|
|
4066
4217
|
if (entry.skillDirs.length === 0) {
|
|
4067
4218
|
console.error(`[skill-forge] no SKILL.md found in installed source: ${source}`);
|
|
4068
4219
|
reject(entry);
|
|
@@ -4088,7 +4239,7 @@ async function add(source, options = {}) {
|
|
|
4088
4239
|
const decision = options.yes || options.json ? yesDecision(safety.verdict) : await promptDecision(safety.verdict, "this skill");
|
|
4089
4240
|
let installedPath = null;
|
|
4090
4241
|
if (decision === "promote") {
|
|
4091
|
-
const result = promote(entry, targetRoot);
|
|
4242
|
+
const result = promote(entry, targetRoot, { expectedFingerprint: gatedFingerprint });
|
|
4092
4243
|
logOutcome(`[skill-forge] promoted -> ${result.promoted.join(", ")}`, options.json);
|
|
4093
4244
|
if (proEnabled) {
|
|
4094
4245
|
try {
|
|
@@ -4179,7 +4330,7 @@ async function addMcp(source, config, options) {
|
|
|
4179
4330
|
envVars: detail.env ? Object.keys(detail.env) : void 0,
|
|
4180
4331
|
candidateEntry: detail.command ? { command: detail.command, args: detail.args, env: detail.env } : void 0
|
|
4181
4332
|
};
|
|
4182
|
-
const mcpTargetPath = options.mcpTarget ?? config.mcpTargets?.[0] ??
|
|
4333
|
+
const mcpTargetPath = options.mcpTarget ?? config.mcpTargets?.[0] ?? join20(process.cwd(), ".mcp.json");
|
|
4183
4334
|
const result = promoteMcp(entry, serverProfile, mcpTargetPath, { force: options.force });
|
|
4184
4335
|
reject(entry);
|
|
4185
4336
|
logOutcome(`[skill-forge] promoted -> ${result.target} (server "${result.serverName}")`, options.json);
|
|
@@ -4274,7 +4425,7 @@ async function scan(source, options = {}) {
|
|
|
4274
4425
|
}
|
|
4275
4426
|
|
|
4276
4427
|
// src/commands/list.ts
|
|
4277
|
-
import { join as
|
|
4428
|
+
import { join as join21 } from "path";
|
|
4278
4429
|
async function list() {
|
|
4279
4430
|
await maybeOfferInit();
|
|
4280
4431
|
const config = loadConfig();
|
|
@@ -4287,14 +4438,14 @@ async function list() {
|
|
|
4287
4438
|
}
|
|
4288
4439
|
console.log(`[skill-forge] ${ids.length} held in quarantine (${quarantineDir}):`);
|
|
4289
4440
|
for (const id of ids) {
|
|
4290
|
-
const skillDirs = findSkillDirs(
|
|
4441
|
+
const skillDirs = findSkillDirs(join21(quarantineDir, id));
|
|
4291
4442
|
if (skillDirs.length === 0) {
|
|
4292
4443
|
console.log(` ${id} (no SKILL.md found)`);
|
|
4293
4444
|
continue;
|
|
4294
4445
|
}
|
|
4295
4446
|
const names = skillDirs.map((dir) => {
|
|
4296
4447
|
try {
|
|
4297
|
-
return readFrontmatter(
|
|
4448
|
+
return readFrontmatter(join21(dir, "SKILL.md")).name;
|
|
4298
4449
|
} catch {
|
|
4299
4450
|
return dir;
|
|
4300
4451
|
}
|
|
@@ -4307,20 +4458,20 @@ async function list() {
|
|
|
4307
4458
|
import { existsSync as existsSync18 } from "fs";
|
|
4308
4459
|
|
|
4309
4460
|
// src/commands/held.ts
|
|
4310
|
-
import { existsSync as existsSync17, realpathSync as
|
|
4311
|
-
import { basename as basename9, join as
|
|
4461
|
+
import { existsSync as existsSync17, realpathSync as realpathSync3 } from "fs";
|
|
4462
|
+
import { basename as basename9, join as join22, resolve as resolvePath4, sep as sep2 } from "path";
|
|
4312
4463
|
var PRO_FEATURE_LABEL2 = "overlap analysis, the provenance ledger, and the pending-ingestion queue";
|
|
4313
4464
|
function resolveHeldPath(quarantineDir, id) {
|
|
4314
4465
|
if (id === "" || id !== basename9(id) || id === "." || id === "..") {
|
|
4315
4466
|
throw new UserError(`invalid quarantine id "${id}" \u2014 pass a single id exactly as printed by \`skill-forge list\``);
|
|
4316
4467
|
}
|
|
4317
|
-
const candidate =
|
|
4468
|
+
const candidate = join22(quarantineDir, id);
|
|
4318
4469
|
if (!existsSync17(candidate)) {
|
|
4319
4470
|
throw new UserError(`no held entry with id "${id}" \u2014 run \`skill-forge list\` to see what is in quarantine`);
|
|
4320
4471
|
}
|
|
4321
|
-
const realRoot =
|
|
4322
|
-
const realCandidate =
|
|
4323
|
-
if (!realCandidate.startsWith(realRoot +
|
|
4472
|
+
const realRoot = realpathSync3(resolvePath4(quarantineDir));
|
|
4473
|
+
const realCandidate = realpathSync3(candidate);
|
|
4474
|
+
if (!realCandidate.startsWith(realRoot + sep2)) {
|
|
4324
4475
|
throw new UserError(`refusing to act on "${id}": it resolves outside the quarantine dir (${realRoot})`);
|
|
4325
4476
|
}
|
|
4326
4477
|
return realCandidate;
|
|
@@ -4343,7 +4494,7 @@ function resolveTargetRoot(config, target) {
|
|
|
4343
4494
|
function heldSkillNames(skillDirs) {
|
|
4344
4495
|
return skillDirs.map((dir) => {
|
|
4345
4496
|
try {
|
|
4346
|
-
return readFrontmatter(
|
|
4497
|
+
return readFrontmatter(join22(dir, "SKILL.md")).name;
|
|
4347
4498
|
} catch {
|
|
4348
4499
|
return basename9(dir);
|
|
4349
4500
|
}
|
|
@@ -4351,7 +4502,7 @@ function heldSkillNames(skillDirs) {
|
|
|
4351
4502
|
}
|
|
4352
4503
|
function looksLikeMcpSandbox(quarantinePath) {
|
|
4353
4504
|
const candidate = resolveMcpCandidatePath({ quarantinePath });
|
|
4354
|
-
return [".mcp.json", "mcp.json", "package.json"].some((f) => existsSync17(
|
|
4505
|
+
return [".mcp.json", "mcp.json", "package.json"].some((f) => existsSync17(join22(candidate, f)));
|
|
4355
4506
|
}
|
|
4356
4507
|
async function promoteHeldMcp(id, entry, config, options) {
|
|
4357
4508
|
const candidatePath = resolveMcpCandidatePath(entry);
|
|
@@ -4385,7 +4536,7 @@ async function promoteHeldMcp(id, entry, config, options) {
|
|
|
4385
4536
|
envVars: detail.env ? Object.keys(detail.env) : void 0,
|
|
4386
4537
|
candidateEntry: detail.command ? { command: detail.command, args: detail.args, env: detail.env } : void 0
|
|
4387
4538
|
};
|
|
4388
|
-
const mcpTargetPath = options.mcpTarget ?? config.mcpTargets?.[0] ??
|
|
4539
|
+
const mcpTargetPath = options.mcpTarget ?? config.mcpTargets?.[0] ?? join22(process.cwd(), ".mcp.json");
|
|
4389
4540
|
const result = promoteMcp(entry, serverProfile, mcpTargetPath, { force: options.force });
|
|
4390
4541
|
reject(entry);
|
|
4391
4542
|
logOutcome(`[skill-forge] promoted -> ${result.target} (server "${result.serverName}")`, options.json);
|
|
@@ -4430,6 +4581,7 @@ async function promoteHeld(id, options = {}) {
|
|
|
4430
4581
|
}
|
|
4431
4582
|
const targetRoot = resolveTargetRoot(config, options.target);
|
|
4432
4583
|
const candidatePath = entry.skillDirs[0];
|
|
4584
|
+
const gatedFingerprint = fingerprintDirs(entry.skillDirs).digest;
|
|
4433
4585
|
logOutcome(`[skill-forge] re-gating held entry ${id} (${heldSkillNames(entry.skillDirs).join(", ")})`, options.json);
|
|
4434
4586
|
const { safety, gateResult, proEnabled } = await runGate(candidatePath, targetRoot, {
|
|
4435
4587
|
proFeatureLabel: PRO_FEATURE_LABEL2,
|
|
@@ -4450,7 +4602,7 @@ async function promoteHeld(id, options = {}) {
|
|
|
4450
4602
|
logOutcome(`[skill-forge] rejected \u2014 removed ${quarantinePath}`, options.json);
|
|
4451
4603
|
return;
|
|
4452
4604
|
}
|
|
4453
|
-
const result = promote(entry, targetRoot, { force: options.force });
|
|
4605
|
+
const result = promote(entry, targetRoot, { force: options.force, expectedFingerprint: gatedFingerprint });
|
|
4454
4606
|
logOutcome(`[skill-forge] promoted ${result.promoted.length} skill dir(s):`, options.json);
|
|
4455
4607
|
for (const promotedPath of result.promoted) logOutcome(` ${promotedPath}`, options.json);
|
|
4456
4608
|
if (!proEnabled) return;
|
|
@@ -4858,17 +5010,17 @@ async function status() {
|
|
|
4858
5010
|
|
|
4859
5011
|
// src/evolve.ts
|
|
4860
5012
|
import { createInterface as createInterface5 } from "readline/promises";
|
|
4861
|
-
import { createHash } from "crypto";
|
|
4862
|
-
import { cpSync as cpSync3, existsSync as existsSync19, lstatSync as
|
|
5013
|
+
import { createHash as createHash2 } from "crypto";
|
|
5014
|
+
import { cpSync as cpSync3, existsSync as existsSync19, lstatSync as lstatSync3, mkdtempSync as mkdtempSync3, readFileSync as readFileSync14, rmSync as rmSync6 } from "fs";
|
|
4863
5015
|
import { tmpdir as tmpdir2 } from "os";
|
|
4864
|
-
import { basename as basename10, dirname as dirname9, join as
|
|
5016
|
+
import { basename as basename10, dirname as dirname9, join as join23, resolve as resolvePath5, sep as sep3 } from "path";
|
|
4865
5017
|
var SKILLOPT_BIN = "skillopt-sleep";
|
|
4866
5018
|
var PRO_FEATURE_LABEL3 = "self-evolution via SkillOpt-Sleep (evolve) \u2014 re-gating, provenance, and the pending-ingestion queue";
|
|
4867
5019
|
async function evolve(targetSkillDir, options = {}, deps = {}) {
|
|
4868
5020
|
const exec = deps.exec ?? defaultExec;
|
|
4869
5021
|
const json = options.json;
|
|
4870
5022
|
const resolvedTargetDir = resolvePath5(targetSkillDir);
|
|
4871
|
-
const skillMdPath =
|
|
5023
|
+
const skillMdPath = join23(resolvedTargetDir, "SKILL.md");
|
|
4872
5024
|
if (!existsSync19(skillMdPath)) {
|
|
4873
5025
|
throw new UserError(`no SKILL.md found at ${skillMdPath} \u2014 evolve requires an existing, installed skill directory`);
|
|
4874
5026
|
}
|
|
@@ -4927,16 +5079,16 @@ async function evolve(targetSkillDir, options = {}, deps = {}) {
|
|
|
4927
5079
|
}
|
|
4928
5080
|
return;
|
|
4929
5081
|
}
|
|
4930
|
-
const stagingRoot =
|
|
5082
|
+
const stagingRoot = join23(project, ".skillopt-sleep", "staging");
|
|
4931
5083
|
const stagingDir = resolvePath5(parsed.staging_dir);
|
|
4932
|
-
if (stagingDir !== stagingRoot && !stagingDir.startsWith(stagingRoot +
|
|
5084
|
+
if (stagingDir !== stagingRoot && !stagingDir.startsWith(stagingRoot + sep3)) {
|
|
4933
5085
|
throw new UserError(
|
|
4934
5086
|
`skillopt-sleep reported a staging_dir outside the expected sandbox (${stagingRoot}): ${stagingDir} \u2014 refusing to use it`
|
|
4935
5087
|
);
|
|
4936
5088
|
}
|
|
4937
|
-
const proposedSkillMd =
|
|
4938
|
-
const proposedClaudeMd =
|
|
4939
|
-
const reportMdPath =
|
|
5089
|
+
const proposedSkillMd = join23(stagingDir, "proposed_SKILL.md");
|
|
5090
|
+
const proposedClaudeMd = join23(stagingDir, "proposed_CLAUDE.md");
|
|
5091
|
+
const reportMdPath = join23(stagingDir, "report.md");
|
|
4940
5092
|
if (!existsSync19(proposedSkillMd)) {
|
|
4941
5093
|
throw new UserError(`skillopt-sleep reported an accepted proposal but ${proposedSkillMd} is missing`);
|
|
4942
5094
|
}
|
|
@@ -4947,13 +5099,13 @@ async function evolve(targetSkillDir, options = {}, deps = {}) {
|
|
|
4947
5099
|
assertNotSymlink(proposedClaudeMd, "proposed_CLAUDE.md");
|
|
4948
5100
|
const scannedSkillHash = sha256File(proposedSkillMd);
|
|
4949
5101
|
const scannedClaudeHash = sha256File(proposedClaudeMd);
|
|
4950
|
-
const tempDir = mkdtempSync3(
|
|
5102
|
+
const tempDir = mkdtempSync3(join23(tmpdir2(), "skill-forge-evolve-"));
|
|
4951
5103
|
let profile;
|
|
4952
5104
|
let safety;
|
|
4953
5105
|
try {
|
|
4954
|
-
cpSync3(proposedSkillMd,
|
|
5106
|
+
cpSync3(proposedSkillMd, join23(tempDir, "SKILL.md"));
|
|
4955
5107
|
if (existsSync19(proposedClaudeMd)) {
|
|
4956
|
-
cpSync3(proposedClaudeMd,
|
|
5108
|
+
cpSync3(proposedClaudeMd, join23(tempDir, "CLAUDE.md"));
|
|
4957
5109
|
}
|
|
4958
5110
|
profile = asSkillProfile(profileSkill(tempDir), "for the evolved proposal");
|
|
4959
5111
|
const strictness = loadConfig().strictness;
|
|
@@ -5094,10 +5246,10 @@ function validateRunOutput(raw) {
|
|
|
5094
5246
|
return { accepted: Boolean(obj.accepted), staging_dir: stagingDirRaw ?? null };
|
|
5095
5247
|
}
|
|
5096
5248
|
function readManifest(stagingDir) {
|
|
5097
|
-
const manifestPath =
|
|
5249
|
+
const manifestPath = join23(stagingDir, "manifest.json");
|
|
5098
5250
|
let raw;
|
|
5099
5251
|
try {
|
|
5100
|
-
raw =
|
|
5252
|
+
raw = readFileSync14(manifestPath, "utf-8");
|
|
5101
5253
|
} catch (err) {
|
|
5102
5254
|
throw new UserError(`could not read staging manifest ${manifestPath}: ${err.message}`);
|
|
5103
5255
|
}
|
|
@@ -5132,11 +5284,11 @@ function validateManifest(raw, manifestPath) {
|
|
|
5132
5284
|
}
|
|
5133
5285
|
function filesIdentical(a, b) {
|
|
5134
5286
|
if (!existsSync19(a) || !existsSync19(b)) return false;
|
|
5135
|
-
return
|
|
5287
|
+
return readFileSync14(a).equals(readFileSync14(b));
|
|
5136
5288
|
}
|
|
5137
5289
|
function isAlreadyAdopted(stagingDir, manifest) {
|
|
5138
|
-
const proposedSkillMd =
|
|
5139
|
-
const proposedClaudeMd =
|
|
5290
|
+
const proposedSkillMd = join23(stagingDir, "proposed_SKILL.md");
|
|
5291
|
+
const proposedClaudeMd = join23(stagingDir, "proposed_CLAUDE.md");
|
|
5140
5292
|
let sawComparison = false;
|
|
5141
5293
|
if (existsSync19(proposedSkillMd)) {
|
|
5142
5294
|
sawComparison = true;
|
|
@@ -5151,7 +5303,7 @@ function isAlreadyAdopted(stagingDir, manifest) {
|
|
|
5151
5303
|
function assertNotSymlink(path, label) {
|
|
5152
5304
|
let stat;
|
|
5153
5305
|
try {
|
|
5154
|
-
stat =
|
|
5306
|
+
stat = lstatSync3(path);
|
|
5155
5307
|
} catch {
|
|
5156
5308
|
return;
|
|
5157
5309
|
}
|
|
@@ -5161,7 +5313,7 @@ function assertNotSymlink(path, label) {
|
|
|
5161
5313
|
}
|
|
5162
5314
|
function sha256File(path) {
|
|
5163
5315
|
if (!existsSync19(path)) return null;
|
|
5164
|
-
return
|
|
5316
|
+
return createHash2("sha256").update(readFileSync14(path)).digest("hex");
|
|
5165
5317
|
}
|
|
5166
5318
|
|
|
5167
5319
|
// src/commands/evolve.ts
|
|
@@ -5173,21 +5325,21 @@ async function evolveCommand(targetSkillDir, options = {}) {
|
|
|
5173
5325
|
import { resolve as resolvePath7 } from "path";
|
|
5174
5326
|
|
|
5175
5327
|
// src/organize.ts
|
|
5176
|
-
import { existsSync as existsSync20, readFileSync as
|
|
5177
|
-
import { basename as basename11, join as
|
|
5328
|
+
import { existsSync as existsSync20, readFileSync as readFileSync15, readdirSync as readdirSync8, realpathSync as realpathSync4, statSync as statSync9 } from "fs";
|
|
5329
|
+
import { basename as basename11, join as join24, resolve as resolvePath6 } from "path";
|
|
5178
5330
|
var PRO_FEATURE_LABEL4 = "set-level organizer reports";
|
|
5179
5331
|
var TIERS = /* @__PURE__ */ new Set(["resource", "custom"]);
|
|
5180
5332
|
var MATURITIES = /* @__PURE__ */ new Set(["seedling", "stable", "deprecated"]);
|
|
5181
5333
|
function listSubdirs2(dir) {
|
|
5182
5334
|
let entries;
|
|
5183
5335
|
try {
|
|
5184
|
-
entries =
|
|
5336
|
+
entries = readdirSync8(dir, { withFileTypes: true });
|
|
5185
5337
|
} catch {
|
|
5186
5338
|
return [];
|
|
5187
5339
|
}
|
|
5188
5340
|
const out = [];
|
|
5189
5341
|
for (const entry of entries) {
|
|
5190
|
-
const full =
|
|
5342
|
+
const full = join24(dir, entry.name);
|
|
5191
5343
|
if (entry.isDirectory()) {
|
|
5192
5344
|
out.push(full);
|
|
5193
5345
|
continue;
|
|
@@ -5205,14 +5357,14 @@ function findSkillDirsUnderRoot2(root) {
|
|
|
5205
5357
|
const found = /* @__PURE__ */ new Set();
|
|
5206
5358
|
const topDirs = listSubdirs2(root);
|
|
5207
5359
|
for (const dir of topDirs) {
|
|
5208
|
-
if (existsSync20(
|
|
5360
|
+
if (existsSync20(join24(dir, "SKILL.md"))) found.add(dir);
|
|
5209
5361
|
}
|
|
5210
|
-
for (const dir of listSubdirs2(
|
|
5211
|
-
if (existsSync20(
|
|
5362
|
+
for (const dir of listSubdirs2(join24(root, "skills"))) {
|
|
5363
|
+
if (existsSync20(join24(dir, "SKILL.md"))) found.add(dir);
|
|
5212
5364
|
}
|
|
5213
5365
|
for (const topDir of topDirs) {
|
|
5214
|
-
for (const dir of listSubdirs2(
|
|
5215
|
-
if (existsSync20(
|
|
5366
|
+
for (const dir of listSubdirs2(join24(topDir, "skills"))) {
|
|
5367
|
+
if (existsSync20(join24(dir, "SKILL.md"))) found.add(dir);
|
|
5216
5368
|
}
|
|
5217
5369
|
}
|
|
5218
5370
|
return Array.from(found).filter((p) => !p.includes("/.git/"));
|
|
@@ -5281,7 +5433,7 @@ function loadUsageSnapshot(snapshotPath) {
|
|
|
5281
5433
|
const resolvedPath = resolvePath6(expandHome(snapshotPath));
|
|
5282
5434
|
let raw;
|
|
5283
5435
|
try {
|
|
5284
|
-
raw =
|
|
5436
|
+
raw = readFileSync15(resolvedPath, "utf-8");
|
|
5285
5437
|
} catch (err) {
|
|
5286
5438
|
return { usage: /* @__PURE__ */ new Map(), warning: `could not read usage snapshot (${err.message}); proceeding without usage` };
|
|
5287
5439
|
}
|
|
@@ -5380,17 +5532,17 @@ function buildRegistryEntries(candidates, warnings) {
|
|
|
5380
5532
|
for (const candidate of candidates) {
|
|
5381
5533
|
let realPath;
|
|
5382
5534
|
try {
|
|
5383
|
-
realPath =
|
|
5535
|
+
realPath = realpathSync4(candidate.dirPath);
|
|
5384
5536
|
} catch (err) {
|
|
5385
5537
|
warnings.push(`could not resolve skill directory ${candidate.dirPath}: ${err.message}`);
|
|
5386
5538
|
continue;
|
|
5387
5539
|
}
|
|
5388
5540
|
if (seenRealPaths.has(realPath)) continue;
|
|
5389
5541
|
seenRealPaths.add(realPath);
|
|
5390
|
-
const skillMdPath =
|
|
5542
|
+
const skillMdPath = join24(realPath, "SKILL.md");
|
|
5391
5543
|
let text;
|
|
5392
5544
|
try {
|
|
5393
|
-
text =
|
|
5545
|
+
text = readFileSync15(skillMdPath, "utf-8");
|
|
5394
5546
|
} catch (err) {
|
|
5395
5547
|
warnings.push(`could not read ${skillMdPath}: ${err.message}`);
|
|
5396
5548
|
continue;
|
|
@@ -5795,8 +5947,8 @@ async function find(query, options = {}, deps = {}) {
|
|
|
5795
5947
|
}
|
|
5796
5948
|
|
|
5797
5949
|
// src/commands/watch.ts
|
|
5798
|
-
import { realpathSync as
|
|
5799
|
-
import { join as
|
|
5950
|
+
import { realpathSync as realpathSync5 } from "fs";
|
|
5951
|
+
import { join as join25, resolve as resolvePath8 } from "path";
|
|
5800
5952
|
var PRO_FEATURE_LABEL5 = "provenance drift watch (watch)";
|
|
5801
5953
|
function isGitUrl(source) {
|
|
5802
5954
|
const s = source.trim();
|
|
@@ -5846,7 +5998,7 @@ async function runWatch(config, options = {}, deps = {}) {
|
|
|
5846
5998
|
let realPath = resolvedRoot;
|
|
5847
5999
|
if (exists) {
|
|
5848
6000
|
try {
|
|
5849
|
-
realPath =
|
|
6001
|
+
realPath = realpathSync5(resolvedRoot);
|
|
5850
6002
|
} catch {
|
|
5851
6003
|
realPath = resolvedRoot;
|
|
5852
6004
|
}
|
|
@@ -5858,7 +6010,7 @@ async function runWatch(config, options = {}, deps = {}) {
|
|
|
5858
6010
|
ledger = readProvenanceLedger(resolvedRoot);
|
|
5859
6011
|
} catch (err) {
|
|
5860
6012
|
const message = err instanceof Error ? err.message : String(err);
|
|
5861
|
-
const ledgerPath =
|
|
6013
|
+
const ledgerPath = join25(resolvedRoot, "SOURCES.md");
|
|
5862
6014
|
roots.push({ root, realPath, exists, ledgerPath, ledgerExists: false });
|
|
5863
6015
|
warnings.push(`${ledgerPath}: could not read ledger: ${message}`);
|
|
5864
6016
|
continue;
|
|
@@ -5950,7 +6102,7 @@ async function watchCommand(options = {}) {
|
|
|
5950
6102
|
|
|
5951
6103
|
// src/commands/routine.ts
|
|
5952
6104
|
import { existsSync as existsSync21, rmSync as rmSync7, statSync as statSync10 } from "fs";
|
|
5953
|
-
import { join as
|
|
6105
|
+
import { join as join26 } from "path";
|
|
5954
6106
|
var PRO_FEATURE_LABEL6 = "the scheduled maintenance routine";
|
|
5955
6107
|
var SEVERITY_ORDER = ["LOW", "MEDIUM", "HIGH", "CRITICAL"];
|
|
5956
6108
|
function parseFailOnFloor(failOn) {
|
|
@@ -5971,7 +6123,7 @@ function exceedsFloor(audit2, failOn) {
|
|
|
5971
6123
|
}
|
|
5972
6124
|
function resolveReportPath2(override, generatedAt) {
|
|
5973
6125
|
if (override) return override;
|
|
5974
|
-
return
|
|
6126
|
+
return join26(getHomeDir(), "reports", `routine-${generatedAt.replace(/[:.]/g, "-")}.md`);
|
|
5975
6127
|
}
|
|
5976
6128
|
function runHousekeeping(config, options) {
|
|
5977
6129
|
const actions = [];
|
|
@@ -5980,7 +6132,7 @@ function runHousekeeping(config, options) {
|
|
|
5980
6132
|
const pruneDays = options.pruneDays ?? 30;
|
|
5981
6133
|
const cutoff = Date.now() - pruneDays * 864e5;
|
|
5982
6134
|
for (const id of listQuarantineIds(config.quarantineDir)) {
|
|
5983
|
-
const path =
|
|
6135
|
+
const path = join26(config.quarantineDir, id);
|
|
5984
6136
|
let mtime;
|
|
5985
6137
|
try {
|
|
5986
6138
|
mtime = statSync10(path).mtimeMs;
|
|
@@ -6192,8 +6344,8 @@ async function routineCommand(options = {}) {
|
|
|
6192
6344
|
}
|
|
6193
6345
|
|
|
6194
6346
|
// src/commands/ingest.ts
|
|
6195
|
-
import { realpathSync as
|
|
6196
|
-
import { dirname as dirname11, isAbsolute, relative, resolve as resolvePath9 } from "path";
|
|
6347
|
+
import { realpathSync as realpathSync6 } from "fs";
|
|
6348
|
+
import { dirname as dirname11, isAbsolute as isAbsolute2, relative as relative2, resolve as resolvePath9 } from "path";
|
|
6197
6349
|
var PRO_FEATURE_LABEL7 = "the pending-ingestion queue";
|
|
6198
6350
|
var VALID_STATUSES = ["ingested", "dismissed"];
|
|
6199
6351
|
function requireProFeature() {
|
|
@@ -6234,14 +6386,14 @@ function printPendingList(pending, json) {
|
|
|
6234
6386
|
}
|
|
6235
6387
|
function realpathOrNull(p) {
|
|
6236
6388
|
try {
|
|
6237
|
-
return
|
|
6389
|
+
return realpathSync6(p);
|
|
6238
6390
|
} catch {
|
|
6239
6391
|
return null;
|
|
6240
6392
|
}
|
|
6241
6393
|
}
|
|
6242
6394
|
function isUnderRoot(candidate, root) {
|
|
6243
|
-
const rel =
|
|
6244
|
-
return rel === "" || !rel.startsWith("..") && !
|
|
6395
|
+
const rel = relative2(root, candidate);
|
|
6396
|
+
return rel === "" || !rel.startsWith("..") && !isAbsolute2(rel);
|
|
6245
6397
|
}
|
|
6246
6398
|
function validatePendingEntries(entries, config) {
|
|
6247
6399
|
const quarantineRoot = realpathOrNull(resolvePath9(config.quarantineDir));
|
|
@@ -6318,14 +6470,14 @@ async function queueClose(id, options = {}) {
|
|
|
6318
6470
|
}
|
|
6319
6471
|
|
|
6320
6472
|
// src/commands/refine.ts
|
|
6321
|
-
import { createHash as
|
|
6322
|
-
import { chmodSync, existsSync as existsSync24, lstatSync as
|
|
6323
|
-
import { dirname as dirname13, isAbsolute as
|
|
6473
|
+
import { createHash as createHash4, randomUUID as randomUUID5 } from "crypto";
|
|
6474
|
+
import { chmodSync, existsSync as existsSync24, lstatSync as lstatSync4, mkdirSync as mkdirSync9, readFileSync as readFileSync17, realpathSync as realpathSync9, renameSync as renameSync6, rmSync as rmSync9, writeFileSync as writeFileSync9 } from "fs";
|
|
6475
|
+
import { dirname as dirname13, isAbsolute as isAbsolute4, join as join29, relative as relative4, resolve as resolvePath12, sep as sep4 } from "path";
|
|
6324
6476
|
import { createInterface as createInterface6 } from "readline/promises";
|
|
6325
6477
|
|
|
6326
6478
|
// src/refine/context.ts
|
|
6327
|
-
import { existsSync as existsSync23, realpathSync as
|
|
6328
|
-
import { isAbsolute as
|
|
6479
|
+
import { existsSync as existsSync23, realpathSync as realpathSync8, statSync as statSync12 } from "fs";
|
|
6480
|
+
import { isAbsolute as isAbsolute3, join as join28, relative as relative3, resolve as resolvePath11 } from "path";
|
|
6329
6481
|
|
|
6330
6482
|
// src/refine/models.ts
|
|
6331
6483
|
import { randomBytes } from "crypto";
|
|
@@ -6394,27 +6546,27 @@ function contentSimilarity(a, b) {
|
|
|
6394
6546
|
}
|
|
6395
6547
|
|
|
6396
6548
|
// src/refine/store.ts
|
|
6397
|
-
import { createHash as
|
|
6398
|
-
import { existsSync as existsSync22, readFileSync as
|
|
6399
|
-
import { join as
|
|
6549
|
+
import { createHash as createHash3 } from "crypto";
|
|
6550
|
+
import { existsSync as existsSync22, readFileSync as readFileSync16, statSync as statSync11, realpathSync as realpathSync7 } from "fs";
|
|
6551
|
+
import { join as join27, resolve as resolvePath10 } from "path";
|
|
6400
6552
|
var SIMILARITY_JOIN_THRESHOLD = 0.7;
|
|
6401
6553
|
var READY_THRESHOLD = 2;
|
|
6402
6554
|
function getRefinementsDir() {
|
|
6403
|
-
return
|
|
6555
|
+
return join27(getHomeDir(), "refinements");
|
|
6404
6556
|
}
|
|
6405
6557
|
function getRefinementsPath() {
|
|
6406
|
-
return
|
|
6558
|
+
return join27(getRefinementsDir(), "refinements.json");
|
|
6407
6559
|
}
|
|
6408
6560
|
function getPatternsPath() {
|
|
6409
|
-
return
|
|
6561
|
+
return join27(getRefinementsDir(), "patterns.json");
|
|
6410
6562
|
}
|
|
6411
6563
|
function getBackupsDir() {
|
|
6412
|
-
return
|
|
6564
|
+
return join27(getRefinementsDir(), "backups");
|
|
6413
6565
|
}
|
|
6414
6566
|
function readJsonFile(path, label) {
|
|
6415
6567
|
let raw;
|
|
6416
6568
|
try {
|
|
6417
|
-
raw =
|
|
6569
|
+
raw = readFileSync16(path, "utf-8");
|
|
6418
6570
|
} catch (err) {
|
|
6419
6571
|
throw new UserError(`unable to read ${label} at ${path}: ${err.message}`);
|
|
6420
6572
|
}
|
|
@@ -6517,7 +6669,7 @@ function writePatternsStore(store) {
|
|
|
6517
6669
|
function resolveProjectRealpath(projectRoot) {
|
|
6518
6670
|
const resolved = resolvePath10(projectRoot);
|
|
6519
6671
|
try {
|
|
6520
|
-
return
|
|
6672
|
+
return realpathSync7(resolved);
|
|
6521
6673
|
} catch {
|
|
6522
6674
|
return resolved;
|
|
6523
6675
|
}
|
|
@@ -6652,13 +6804,13 @@ function createBackup(filePaths, deps = {}) {
|
|
|
6652
6804
|
if (!existsSync22(path)) {
|
|
6653
6805
|
return { path, existed: false, sha256: null, mode: null, contentBase64: null };
|
|
6654
6806
|
}
|
|
6655
|
-
const bytes =
|
|
6807
|
+
const bytes = readFileSync16(path);
|
|
6656
6808
|
const mode = statSync11(path).mode & 511;
|
|
6657
|
-
const sha256 =
|
|
6809
|
+
const sha256 = createHash3("sha256").update(bytes).digest("hex");
|
|
6658
6810
|
return { path, existed: true, sha256, mode, contentBase64: bytes.toString("base64") };
|
|
6659
6811
|
});
|
|
6660
6812
|
const manifest = { id, createdAt: now.toISOString(), files };
|
|
6661
|
-
writeJsonAtomic(
|
|
6813
|
+
writeJsonAtomic(join27(getBackupsDir(), id, "manifest.json"), manifest);
|
|
6662
6814
|
return manifest;
|
|
6663
6815
|
}
|
|
6664
6816
|
function validateBackupManifestShape(value, path) {
|
|
@@ -6707,7 +6859,7 @@ function readBackupManifest(id) {
|
|
|
6707
6859
|
if (!isValidBackupId(id)) {
|
|
6708
6860
|
throw new UserError(`invalid backup id "${id}" \u2014 expected the format produced by \`skill-forge refine promote\` (e.g. 20260720T142305Z-a1b2)`);
|
|
6709
6861
|
}
|
|
6710
|
-
const manifestPath =
|
|
6862
|
+
const manifestPath = join27(getBackupsDir(), id, "manifest.json");
|
|
6711
6863
|
if (!existsSync22(manifestPath)) {
|
|
6712
6864
|
throw new UserError(`no backup with id: ${id}`);
|
|
6713
6865
|
}
|
|
@@ -6730,20 +6882,20 @@ function assertPlainSkillName(name) {
|
|
|
6730
6882
|
}
|
|
6731
6883
|
function realpathOrNull2(p) {
|
|
6732
6884
|
try {
|
|
6733
|
-
return
|
|
6885
|
+
return realpathSync8(p);
|
|
6734
6886
|
} catch {
|
|
6735
6887
|
return null;
|
|
6736
6888
|
}
|
|
6737
6889
|
}
|
|
6738
6890
|
function isUnderRoot2(candidate, root) {
|
|
6739
|
-
const rel =
|
|
6740
|
-
return rel === "" || !rel.startsWith("..") && !
|
|
6891
|
+
const rel = relative3(root, candidate);
|
|
6892
|
+
return rel === "" || !rel.startsWith("..") && !isAbsolute3(rel);
|
|
6741
6893
|
}
|
|
6742
6894
|
function resolveScopes(config, cwd, skill, userRootFlag) {
|
|
6743
6895
|
assertPlainSkillName(skill);
|
|
6744
6896
|
const resolvedCwd = resolvePath11(cwd);
|
|
6745
|
-
const localDir =
|
|
6746
|
-
const sharedDir =
|
|
6897
|
+
const localDir = join28(resolvedCwd, ".claude", "skills", skill);
|
|
6898
|
+
const sharedDir = join28(resolvedCwd, "skills", skill);
|
|
6747
6899
|
const local = { scope: "local", dir: localDir, exists: existsSync23(localDir) };
|
|
6748
6900
|
const shared = { scope: "shared", dir: sharedDir, exists: existsSync23(sharedDir) };
|
|
6749
6901
|
const user = resolveUserScope(config, resolvedCwd, skill, userRootFlag);
|
|
@@ -6752,10 +6904,10 @@ function resolveScopes(config, cwd, skill, userRootFlag) {
|
|
|
6752
6904
|
function resolveUserScope(config, resolvedCwd, skill, userRootFlag) {
|
|
6753
6905
|
if (userRootFlag) {
|
|
6754
6906
|
const root = resolvePath11(userRootFlag);
|
|
6755
|
-
return { resolved: true, dir:
|
|
6907
|
+
return { resolved: true, dir: join28(root, skill), root: userRootFlag, reason: "flag" };
|
|
6756
6908
|
}
|
|
6757
6909
|
const cwdReal = realpathOrNull2(resolvedCwd) ?? resolvedCwd;
|
|
6758
|
-
const candidates = config.skillsRoots.map((root) => ({ root, dir:
|
|
6910
|
+
const candidates = config.skillsRoots.map((root) => ({ root, dir: join28(resolveConfiguredRoot2(root), skill) }));
|
|
6759
6911
|
for (const candidate of candidates) {
|
|
6760
6912
|
const skillReal = realpathOrNull2(candidate.dir);
|
|
6761
6913
|
if (skillReal) {
|
|
@@ -6790,12 +6942,12 @@ function inventoryScopeDir(scope, dir) {
|
|
|
6790
6942
|
scope,
|
|
6791
6943
|
dir,
|
|
6792
6944
|
dirExists: isDirectory(dir),
|
|
6793
|
-
skillMd: isFile2(
|
|
6794
|
-
patchMd: isFile2(
|
|
6795
|
-
extendMd: isFile2(
|
|
6796
|
-
configJson: isFile2(
|
|
6797
|
-
hooksDir: isDirectory(
|
|
6798
|
-
scriptsDir: isDirectory(
|
|
6945
|
+
skillMd: isFile2(join28(dir, "SKILL.md")),
|
|
6946
|
+
patchMd: isFile2(join28(dir, "SKILL.patch.md")),
|
|
6947
|
+
extendMd: isFile2(join28(dir, "SKILL.extend.md")),
|
|
6948
|
+
configJson: isFile2(join28(dir, "skill-config.json")),
|
|
6949
|
+
hooksDir: isDirectory(join28(dir, "hooks")),
|
|
6950
|
+
scriptsDir: isDirectory(join28(dir, "scripts"))
|
|
6799
6951
|
};
|
|
6800
6952
|
}
|
|
6801
6953
|
function inventoryAllScopes(resolution) {
|
|
@@ -7067,15 +7219,15 @@ function resolveConfiguredRoot3(root) {
|
|
|
7067
7219
|
return resolvePath12(expandHome(root));
|
|
7068
7220
|
}
|
|
7069
7221
|
function isUnderRoot3(candidate, root) {
|
|
7070
|
-
const rel =
|
|
7071
|
-
return rel === "" || !rel.startsWith("..") && !
|
|
7222
|
+
const rel = relative4(root, candidate);
|
|
7223
|
+
return rel === "" || !rel.startsWith("..") && !isAbsolute4(rel);
|
|
7072
7224
|
}
|
|
7073
7225
|
function sanitizeDiffForConsole(diff) {
|
|
7074
7226
|
return diff.split("\n").map((line) => sanitizeForMarkdown(line)).join("\n");
|
|
7075
7227
|
}
|
|
7076
7228
|
function resolveContentOption(options) {
|
|
7077
7229
|
if (options.content !== void 0) return options.content;
|
|
7078
|
-
if (options.contentFile) return
|
|
7230
|
+
if (options.contentFile) return readFileSync17(resolvePath12(options.contentFile), "utf-8");
|
|
7079
7231
|
return void 0;
|
|
7080
7232
|
}
|
|
7081
7233
|
function validateCaptureOptions(options) {
|
|
@@ -7201,7 +7353,7 @@ ${rendered}` : rendered;
|
|
|
7201
7353
|
function ensureScopeDirSafe(scopeDir, resolvedCwd) {
|
|
7202
7354
|
let lst;
|
|
7203
7355
|
try {
|
|
7204
|
-
lst =
|
|
7356
|
+
lst = lstatSync4(scopeDir);
|
|
7205
7357
|
} catch {
|
|
7206
7358
|
lst = void 0;
|
|
7207
7359
|
}
|
|
@@ -7213,7 +7365,7 @@ function ensureScopeDirSafe(scopeDir, resolvedCwd) {
|
|
|
7213
7365
|
const lexical = resolvePath12(scopeDir);
|
|
7214
7366
|
let realResolved;
|
|
7215
7367
|
try {
|
|
7216
|
-
realResolved =
|
|
7368
|
+
realResolved = realpathSync9(scopeDir);
|
|
7217
7369
|
} catch {
|
|
7218
7370
|
realResolved = real;
|
|
7219
7371
|
}
|
|
@@ -7227,16 +7379,16 @@ function ensureScopeDirSafe(scopeDir, resolvedCwd) {
|
|
|
7227
7379
|
}
|
|
7228
7380
|
function writeOverrideFile(scopeDir, relPath, content, resolvedCwd) {
|
|
7229
7381
|
const realScopeDir = ensureScopeDirSafe(scopeDir, resolvedCwd);
|
|
7230
|
-
const destPath =
|
|
7382
|
+
const destPath = join29(scopeDir, relPath);
|
|
7231
7383
|
const destDir = dirname13(destPath);
|
|
7232
7384
|
mkdirSync9(destDir, { recursive: true });
|
|
7233
|
-
const realDestDir =
|
|
7385
|
+
const realDestDir = realpathSync9(destDir);
|
|
7234
7386
|
if (!isUnderRoot3(realDestDir, realScopeDir)) {
|
|
7235
7387
|
throw new UserError(`refusing to write ${destPath}: resolves outside scope directory ${scopeDir}`);
|
|
7236
7388
|
}
|
|
7237
7389
|
let destLst;
|
|
7238
7390
|
try {
|
|
7239
|
-
destLst =
|
|
7391
|
+
destLst = lstatSync4(destPath);
|
|
7240
7392
|
} catch {
|
|
7241
7393
|
destLst = void 0;
|
|
7242
7394
|
}
|
|
@@ -7323,12 +7475,12 @@ Write ${preview.destPath}? [y/N] `);
|
|
|
7323
7475
|
}
|
|
7324
7476
|
function renderCaptureArtifactPreview(opts, content, refinementId, generatedAt, scopeDir) {
|
|
7325
7477
|
const probeRelPath = opts.overrideType === "patch" ? "SKILL.patch.md" : opts.overrideType === "extend" || opts.overrideType === "new" ? "SKILL.extend.md" : void 0;
|
|
7326
|
-
const probePath = probeRelPath ?
|
|
7327
|
-
const existingContent = probePath && existsSync24(probePath) ?
|
|
7478
|
+
const probePath = probeRelPath ? join29(scopeDir, probeRelPath) : void 0;
|
|
7479
|
+
const existingContent = probePath && existsSync24(probePath) ? readFileSync17(probePath, "utf-8") : "";
|
|
7328
7480
|
const rendered = renderCaptureArtifact(opts, content, refinementId, generatedAt, existingContent);
|
|
7329
7481
|
const relPath = rendered.relPath;
|
|
7330
|
-
const destPath =
|
|
7331
|
-
const before = existsSync24(destPath) ?
|
|
7482
|
+
const destPath = join29(scopeDir, relPath);
|
|
7483
|
+
const before = existsSync24(destPath) ? readFileSync17(destPath, "utf-8") : "";
|
|
7332
7484
|
const diff = previewDiff(before, rendered.content, destPath);
|
|
7333
7485
|
return { relPath, content: rendered.content, destPath, diff };
|
|
7334
7486
|
}
|
|
@@ -7425,16 +7577,16 @@ async function refineWhichCommand(skill, options = {}) {
|
|
|
7425
7577
|
}
|
|
7426
7578
|
function writePostWriteSidecar(backupId, files) {
|
|
7427
7579
|
const sidecar = { schemaVersion: 1, files };
|
|
7428
|
-
const dir =
|
|
7580
|
+
const dir = join29(getBackupsDir(), backupId);
|
|
7429
7581
|
mkdirSync9(dir, { recursive: true });
|
|
7430
|
-
writeFileSync9(
|
|
7582
|
+
writeFileSync9(join29(dir, "post.json"), JSON.stringify(sidecar, null, 2) + "\n", { mode: 384 });
|
|
7431
7583
|
}
|
|
7432
7584
|
function readPostWriteSidecar(backupId) {
|
|
7433
|
-
const path =
|
|
7585
|
+
const path = join29(getBackupsDir(), backupId, "post.json");
|
|
7434
7586
|
if (!existsSync24(path)) return { hashes: /* @__PURE__ */ new Map(), status: "missing" };
|
|
7435
7587
|
let raw;
|
|
7436
7588
|
try {
|
|
7437
|
-
raw =
|
|
7589
|
+
raw = readFileSync17(path, "utf-8");
|
|
7438
7590
|
} catch {
|
|
7439
7591
|
return { hashes: /* @__PURE__ */ new Map(), status: "unreadable" };
|
|
7440
7592
|
}
|
|
@@ -7479,20 +7631,20 @@ async function refinePromoteCommand(patternId, options = {}) {
|
|
|
7479
7631
|
const resolvedUserRoot = resolveConfiguredRoot3(scopes.user.root);
|
|
7480
7632
|
let realUserRoot;
|
|
7481
7633
|
try {
|
|
7482
|
-
realUserRoot =
|
|
7634
|
+
realUserRoot = realpathSync9(resolvedUserRoot);
|
|
7483
7635
|
} catch {
|
|
7484
7636
|
realUserRoot = resolvedUserRoot;
|
|
7485
7637
|
}
|
|
7486
7638
|
let realSkillDir;
|
|
7487
7639
|
try {
|
|
7488
|
-
realSkillDir =
|
|
7640
|
+
realSkillDir = realpathSync9(skillDir);
|
|
7489
7641
|
} catch {
|
|
7490
7642
|
realSkillDir = resolvePath12(skillDir);
|
|
7491
7643
|
}
|
|
7492
7644
|
if (!isUnderRoot3(realSkillDir, realUserRoot)) {
|
|
7493
7645
|
throw new UserError(`refusing to promote: resolved skill directory ${skillDir} is outside the configured user scope root ${scopes.user.root}`);
|
|
7494
7646
|
}
|
|
7495
|
-
const skillMdPath =
|
|
7647
|
+
const skillMdPath = join29(skillDir, "SKILL.md");
|
|
7496
7648
|
if (!existsSync24(skillMdPath)) {
|
|
7497
7649
|
throw new UserError(`user-scope SKILL.md not found at ${skillMdPath} \u2014 refusing to promote against a missing base skill.`);
|
|
7498
7650
|
}
|
|
@@ -7510,7 +7662,7 @@ async function refinePromoteCommand(patternId, options = {}) {
|
|
|
7510
7662
|
`pattern ${patternId}'s representative refinement is a '${representative.overrideType}' override \u2014 promote currently only materializes patch/extend/new-type patterns against the user-scope SKILL.md; other override types must be merged manually.`
|
|
7511
7663
|
);
|
|
7512
7664
|
}
|
|
7513
|
-
const before =
|
|
7665
|
+
const before = readFileSync17(skillMdPath, "utf-8");
|
|
7514
7666
|
const after = applyPatchBlocks(before, [block]);
|
|
7515
7667
|
const diff = previewDiff(before, after, skillMdPath);
|
|
7516
7668
|
if (options.dryRun) {
|
|
@@ -7542,7 +7694,7 @@ Promote pattern ${patternId} into ${skillMdPath}? [y/N] `);
|
|
|
7542
7694
|
throw err;
|
|
7543
7695
|
}
|
|
7544
7696
|
try {
|
|
7545
|
-
const postSha256 =
|
|
7697
|
+
const postSha256 = createHash4("sha256").update(readFileSync17(skillMdPath)).digest("hex");
|
|
7546
7698
|
writePostWriteSidecar(backup.id, [{ path: skillMdPath, sha256: postSha256 }]);
|
|
7547
7699
|
markGeneralized(patternId, backup.id);
|
|
7548
7700
|
const skillsRoot = resolveConfiguredRoot3(scopes.user.root);
|
|
@@ -7569,10 +7721,10 @@ Promote pattern ${patternId} into ${skillMdPath}? [y/N] `);
|
|
|
7569
7721
|
function assertBackupDirContained(backupId) {
|
|
7570
7722
|
const backupsDir = getBackupsDir();
|
|
7571
7723
|
if (!existsSync24(backupsDir)) return;
|
|
7572
|
-
const realBackupsDir =
|
|
7573
|
-
const backupDir =
|
|
7724
|
+
const realBackupsDir = realpathSync9(backupsDir);
|
|
7725
|
+
const backupDir = join29(backupsDir, backupId);
|
|
7574
7726
|
if (!existsSync24(backupDir)) return;
|
|
7575
|
-
const realBackupDir =
|
|
7727
|
+
const realBackupDir = realpathSync9(backupDir);
|
|
7576
7728
|
if (!isUnderRoot3(realBackupDir, realBackupsDir)) {
|
|
7577
7729
|
throw new UserError(`refusing rollback: backup id "${backupId}" resolves outside the backups directory`);
|
|
7578
7730
|
}
|
|
@@ -7581,9 +7733,9 @@ function resolveTargetRealPath(path) {
|
|
|
7581
7733
|
let lexicalAncestor = resolvePath12(path);
|
|
7582
7734
|
while (true) {
|
|
7583
7735
|
try {
|
|
7584
|
-
const real =
|
|
7585
|
-
const remainder =
|
|
7586
|
-
return remainder === "" ? real :
|
|
7736
|
+
const real = realpathSync9(lexicalAncestor);
|
|
7737
|
+
const remainder = relative4(lexicalAncestor, resolvePath12(path));
|
|
7738
|
+
return remainder === "" ? real : join29(real, remainder);
|
|
7587
7739
|
} catch {
|
|
7588
7740
|
const parent = dirname13(lexicalAncestor);
|
|
7589
7741
|
if (parent === lexicalAncestor) return resolvePath12(path);
|
|
@@ -7598,7 +7750,7 @@ function assertManifestPathsWithinRoots(manifest, config) {
|
|
|
7598
7750
|
const roots = config.skillsRoots.map((root) => {
|
|
7599
7751
|
const resolved = resolveConfiguredRoot3(root);
|
|
7600
7752
|
try {
|
|
7601
|
-
return
|
|
7753
|
+
return realpathSync9(resolved);
|
|
7602
7754
|
} catch {
|
|
7603
7755
|
return resolved;
|
|
7604
7756
|
}
|
|
@@ -7615,7 +7767,7 @@ function assertNoSymlinkLeaves(manifest) {
|
|
|
7615
7767
|
for (const file of manifest.files) {
|
|
7616
7768
|
let lst;
|
|
7617
7769
|
try {
|
|
7618
|
-
lst =
|
|
7770
|
+
lst = lstatSync4(file.path);
|
|
7619
7771
|
} catch {
|
|
7620
7772
|
lst = void 0;
|
|
7621
7773
|
}
|
|
@@ -7628,11 +7780,11 @@ function findLexicalRoot(filePath, config) {
|
|
|
7628
7780
|
const lexicalTarget = resolvePath12(filePath);
|
|
7629
7781
|
for (const root of config.skillsRoots) {
|
|
7630
7782
|
const rootLexical = resolveConfiguredRoot3(root);
|
|
7631
|
-
const rel =
|
|
7632
|
-
if (rel !== "" && (rel === ".." || rel.startsWith(`..${
|
|
7783
|
+
const rel = relative4(rootLexical, lexicalTarget);
|
|
7784
|
+
if (rel !== "" && (rel === ".." || rel.startsWith(`..${sep4}`) || isAbsolute4(rel))) continue;
|
|
7633
7785
|
let rootReal;
|
|
7634
7786
|
try {
|
|
7635
|
-
rootReal =
|
|
7787
|
+
rootReal = realpathSync9(rootLexical);
|
|
7636
7788
|
} catch {
|
|
7637
7789
|
rootReal = rootLexical;
|
|
7638
7790
|
}
|
|
@@ -7642,8 +7794,8 @@ function findLexicalRoot(filePath, config) {
|
|
|
7642
7794
|
}
|
|
7643
7795
|
function walkRealComponents(filePath, config, createMissing) {
|
|
7644
7796
|
const { rootLexical, rootReal } = findLexicalRoot(filePath, config);
|
|
7645
|
-
const relPath =
|
|
7646
|
-
const components = relPath.split(
|
|
7797
|
+
const relPath = relative4(rootLexical, resolvePath12(filePath));
|
|
7798
|
+
const components = relPath.split(sep4).filter((c) => c.length > 0);
|
|
7647
7799
|
if (components.length === 0) {
|
|
7648
7800
|
throw new UserError(`refusing to operate on ${filePath}: resolves to the skills root itself`);
|
|
7649
7801
|
}
|
|
@@ -7652,10 +7804,10 @@ function walkRealComponents(filePath, config, createMissing) {
|
|
|
7652
7804
|
let current = rootReal;
|
|
7653
7805
|
for (const component of dirComponents) {
|
|
7654
7806
|
if (current === void 0) break;
|
|
7655
|
-
const next =
|
|
7807
|
+
const next = join29(current, component);
|
|
7656
7808
|
let lst;
|
|
7657
7809
|
try {
|
|
7658
|
-
lst =
|
|
7810
|
+
lst = lstatSync4(next);
|
|
7659
7811
|
} catch {
|
|
7660
7812
|
lst = void 0;
|
|
7661
7813
|
}
|
|
@@ -7683,13 +7835,13 @@ function safeRestoreWrite(filePath, contentBuffer, mode, config) {
|
|
|
7683
7835
|
if (parentReal === void 0) {
|
|
7684
7836
|
throw new UserError(`refusing to restore ${filePath}: could not establish a real parent directory`);
|
|
7685
7837
|
}
|
|
7686
|
-
const finalPath =
|
|
7838
|
+
const finalPath = join29(parentReal, leafComponent);
|
|
7687
7839
|
if (!isUnderRoot3(finalPath, rootReal)) {
|
|
7688
7840
|
throw new UserError(`refusing to restore ${filePath}: resolves outside root ${rootReal}`);
|
|
7689
7841
|
}
|
|
7690
7842
|
let leafLst;
|
|
7691
7843
|
try {
|
|
7692
|
-
leafLst =
|
|
7844
|
+
leafLst = lstatSync4(finalPath);
|
|
7693
7845
|
} catch {
|
|
7694
7846
|
leafLst = void 0;
|
|
7695
7847
|
}
|
|
@@ -7709,13 +7861,13 @@ function safeRestoreWrite(filePath, contentBuffer, mode, config) {
|
|
|
7709
7861
|
function safeDeleteTombstone(filePath, config) {
|
|
7710
7862
|
const { parentReal, leafComponent, rootReal } = walkRealComponents(filePath, config, false);
|
|
7711
7863
|
if (parentReal === void 0) return;
|
|
7712
|
-
const finalPath =
|
|
7864
|
+
const finalPath = join29(parentReal, leafComponent);
|
|
7713
7865
|
if (!isUnderRoot3(finalPath, rootReal)) {
|
|
7714
7866
|
throw new UserError(`refusing to delete ${filePath}: resolves outside root ${rootReal}`);
|
|
7715
7867
|
}
|
|
7716
7868
|
let leafLst;
|
|
7717
7869
|
try {
|
|
7718
|
-
leafLst =
|
|
7870
|
+
leafLst = lstatSync4(finalPath);
|
|
7719
7871
|
} catch {
|
|
7720
7872
|
leafLst = void 0;
|
|
7721
7873
|
}
|
|
@@ -7751,7 +7903,7 @@ async function refineRollbackCommand(backupId, options = {}) {
|
|
|
7751
7903
|
for (const file of manifest.files) {
|
|
7752
7904
|
const expected = sidecar.hashes.get(file.path);
|
|
7753
7905
|
if (expected === void 0) continue;
|
|
7754
|
-
const currentSha256 = existsSync24(file.path) ?
|
|
7906
|
+
const currentSha256 = existsSync24(file.path) ? createHash4("sha256").update(readFileSync17(file.path)).digest("hex") : null;
|
|
7755
7907
|
if (currentSha256 !== expected) mismatches.push(file.path);
|
|
7756
7908
|
}
|
|
7757
7909
|
if (mismatches.length > 0 && !options.force) {
|