@rhize/skill-forge 0.17.1 → 0.19.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +1 -0
- package/README.md +20 -3
- package/dist/cli.js +1283 -778
- package/dist/cli.js.map +1 -1
- package/dist/refine-prompt.md +10 -0
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -1,15 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
-
// src/cli.ts
|
|
4
|
-
import { Command } from "commander";
|
|
5
|
-
import { createRequire } from "module";
|
|
6
|
-
|
|
7
|
-
// src/commands/add.ts
|
|
8
|
-
import { join as join24 } from "path";
|
|
9
|
-
|
|
10
|
-
// src/config.ts
|
|
11
|
-
import { chmodSync, existsSync, mkdirSync, readFileSync as readFileSync2, writeFileSync } from "fs";
|
|
12
|
-
|
|
13
3
|
// src/errors.ts
|
|
14
4
|
var UserError = class extends Error {
|
|
15
5
|
constructor(message) {
|
|
@@ -18,6 +8,13 @@ var UserError = class extends Error {
|
|
|
18
8
|
}
|
|
19
9
|
};
|
|
20
10
|
|
|
11
|
+
// src/pluginDecisions.ts
|
|
12
|
+
import { existsSync as existsSync2, readFileSync as readFileSync3, realpathSync } from "fs";
|
|
13
|
+
import { isAbsolute, join as join4, resolve } from "path";
|
|
14
|
+
|
|
15
|
+
// src/config.ts
|
|
16
|
+
import { chmodSync, existsSync, mkdirSync, readFileSync as readFileSync2, writeFileSync } from "fs";
|
|
17
|
+
|
|
21
18
|
// src/gate/scanShared.ts
|
|
22
19
|
import { readFileSync, readdirSync, statSync } from "fs";
|
|
23
20
|
import { extname, join } from "path";
|
|
@@ -476,9 +473,6 @@ function saveConfig(config) {
|
|
|
476
473
|
chmodSync(path, 384);
|
|
477
474
|
}
|
|
478
475
|
|
|
479
|
-
// src/install/resolve.ts
|
|
480
|
-
import { existsSync as existsSync2 } from "fs";
|
|
481
|
-
|
|
482
476
|
// src/util.ts
|
|
483
477
|
import {
|
|
484
478
|
accessSync,
|
|
@@ -596,11 +590,83 @@ function appendPrivateJsonLine(path, value) {
|
|
|
596
590
|
}
|
|
597
591
|
}
|
|
598
592
|
|
|
593
|
+
// src/pluginDecisions.ts
|
|
594
|
+
function projectIdentity(path) {
|
|
595
|
+
return realpathSync(resolve(path));
|
|
596
|
+
}
|
|
597
|
+
function validatePluginDecision(value) {
|
|
598
|
+
const d = value;
|
|
599
|
+
if (!d || !["claude", "codex"].includes(d.host) || typeof d.pluginId !== "string" || !/^[\w.-]+@[\w.-]+$/.test(d.pluginId) || !["essential", "project-specific", "intentionally-infrequent"].includes(d.category) || typeof d.reason !== "string" || !d.reason.trim() || d.reason.length > 2e3 || /[\x00-\x1f\x7f-\x9f\u200b-\u200f\u202a-\u202e\u2066-\u2069]/.test(d.reason) || d.project !== void 0 && (typeof d.project !== "string" || !isAbsolute(d.project)) || d.category === "project-specific" && !d.project || typeof d.updatedAt !== "string" || !Number.isFinite(Date.parse(d.updatedAt))) {
|
|
600
|
+
throw new UserError("Invalid plugin keep decision: require host, plugin@marketplace, category, reason, date, and a project for project-specific decisions.");
|
|
601
|
+
}
|
|
602
|
+
return { host: d.host, pluginId: d.pluginId, category: d.category, reason: d.reason.trim(), project: d.project, updatedAt: d.updatedAt };
|
|
603
|
+
}
|
|
604
|
+
function decisionKey(d) {
|
|
605
|
+
return JSON.stringify([d.host, d.pluginId, d.project ?? null]);
|
|
606
|
+
}
|
|
607
|
+
function readPluginDecisions() {
|
|
608
|
+
const path = join4(getHomeDir(), "plugin-decisions.json");
|
|
609
|
+
if (!existsSync2(path)) return [];
|
|
610
|
+
const store = JSON.parse(readFileSync3(path, "utf8"));
|
|
611
|
+
if (store.schemaVersion !== 1 || !Array.isArray(store.records)) throw new UserError("Invalid plugin-decisions.json schema.");
|
|
612
|
+
const records = store.records.map(validatePluginDecision);
|
|
613
|
+
if (new Set(records.map(decisionKey)).size !== records.length) throw new UserError("Duplicate plugin keep decisions.");
|
|
614
|
+
return records;
|
|
615
|
+
}
|
|
616
|
+
function writePluginDecisions(records) {
|
|
617
|
+
writeJsonAtomic(join4(getHomeDir(), "plugin-decisions.json"), { schemaVersion: 1, records: records.map(validatePluginDecision) });
|
|
618
|
+
}
|
|
619
|
+
function applyPluginDecisions(report, cwd = process.cwd()) {
|
|
620
|
+
try {
|
|
621
|
+
const records = readPluginDecisions();
|
|
622
|
+
const project = projectIdentity(cwd);
|
|
623
|
+
for (const plugin of report.plugins ?? []) {
|
|
624
|
+
const matches = records.filter((d) => d.host === (plugin.host ?? "claude") && d.pluginId === plugin.pluginId);
|
|
625
|
+
const decision = matches.find((d) => d.project === project) ?? matches.find((d) => !d.project);
|
|
626
|
+
if (!decision) continue;
|
|
627
|
+
plugin.keepDecision = decision;
|
|
628
|
+
if (plugin.recommendation !== "review") plugin.recommendation = "keep";
|
|
629
|
+
}
|
|
630
|
+
} catch (error) {
|
|
631
|
+
report.notices.push(`Plugin keep decisions unavailable; retention advice unchanged: ${error.message}`);
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
// src/commands/plugins.ts
|
|
636
|
+
function pluginsKeepCommand(pluginId, options) {
|
|
637
|
+
const decision = validatePluginDecision({ ...options, pluginId, project: options.project ? projectIdentity(options.project) : void 0, updatedAt: (/* @__PURE__ */ new Date()).toISOString() });
|
|
638
|
+
const records = readPluginDecisions().filter((d) => decisionKey(d) !== decisionKey(decision));
|
|
639
|
+
records.push(decision);
|
|
640
|
+
writePluginDecisions(records);
|
|
641
|
+
console.log(options.json ? JSON.stringify(decision, null, 2) : `Keep decision recorded for ${decision.host}:${pluginId}. Safety findings remain active.`);
|
|
642
|
+
}
|
|
643
|
+
function pluginsListCommand(options) {
|
|
644
|
+
const records = readPluginDecisions();
|
|
645
|
+
console.log(options.json ? JSON.stringify({ schemaVersion: 1, records }, null, 2) : records.map((d) => `${d.host}:${d.pluginId} \u2014 ${d.category}${d.project ? ` (${d.project})` : ""}: ${d.reason}`).join("\n") || "No explicit plugin keep decisions.");
|
|
646
|
+
}
|
|
647
|
+
function pluginsForgetCommand(pluginId, options) {
|
|
648
|
+
if (!["claude", "codex"].includes(options.host ?? "")) throw new UserError("--host claude|codex is required.");
|
|
649
|
+
const key = decisionKey({ host: options.host, pluginId, project: options.project ? projectIdentity(options.project) : void 0 });
|
|
650
|
+
const records = readPluginDecisions();
|
|
651
|
+
const remaining = records.filter((d) => decisionKey(d) !== key);
|
|
652
|
+
if (remaining.length === records.length) throw new UserError("No matching keep decision for that host, plugin and project.");
|
|
653
|
+
writePluginDecisions(remaining);
|
|
654
|
+
console.log(options.json ? JSON.stringify({ removed: true, pluginId }) : `Keep decision removed for ${options.host}:${pluginId}.`);
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
// src/cli.ts
|
|
658
|
+
import { Command } from "commander";
|
|
659
|
+
import { createRequire } from "module";
|
|
660
|
+
|
|
661
|
+
// src/commands/add.ts
|
|
662
|
+
import { join as join26 } from "path";
|
|
663
|
+
|
|
599
664
|
// src/install/resolve.ts
|
|
665
|
+
import { existsSync as existsSync3 } from "fs";
|
|
600
666
|
var GIT_URL_PATTERN = /^(git@|https?:\/\/|ssh:\/\/)/;
|
|
601
667
|
var LOCAL_PATH_PATTERN = /^(\.{1,2}\/|\/|~\/|~$)/;
|
|
602
668
|
var NPM_SCOPED_PATTERN = /^@[^/]+\/[^/]+$/;
|
|
603
|
-
function
|
|
669
|
+
function resolve2(source, artifact = "skill") {
|
|
604
670
|
const value = source.trim();
|
|
605
671
|
if (value.startsWith("-")) {
|
|
606
672
|
throw new UserError(`skill source cannot start with "-" (looks like a flag, not a source): ${value}`);
|
|
@@ -609,7 +675,7 @@ function resolve(source, artifact = "skill") {
|
|
|
609
675
|
return { type: "git", value };
|
|
610
676
|
}
|
|
611
677
|
const localCandidate = expandHome(value);
|
|
612
|
-
if (
|
|
678
|
+
if (existsSync3(localCandidate) || LOCAL_PATH_PATTERN.test(value)) {
|
|
613
679
|
return { type: "local", value: localCandidate };
|
|
614
680
|
}
|
|
615
681
|
if (artifact === "mcp" && (NPM_SCOPED_PATTERN.test(value) || !value.includes("/"))) {
|
|
@@ -621,14 +687,14 @@ function resolve(source, artifact = "skill") {
|
|
|
621
687
|
// src/install/quarantine.ts
|
|
622
688
|
import { randomUUID as randomUUID2 } from "crypto";
|
|
623
689
|
import { spawn } from "child_process";
|
|
624
|
-
import { cpSync, existsSync as
|
|
625
|
-
import { basename as basename2, join as
|
|
690
|
+
import { cpSync, existsSync as existsSync4, mkdirSync as mkdirSync3, readdirSync as readdirSync2 } from "fs";
|
|
691
|
+
import { basename as basename2, join as join5, resolve as resolvePath } from "path";
|
|
626
692
|
var SKILL_MANIFEST = "SKILL.md";
|
|
627
693
|
var SKIP_DIRS2 = /* @__PURE__ */ new Set([".git", "node_modules"]);
|
|
628
694
|
async function quarantine(source, quarantineDir, deps = {}) {
|
|
629
695
|
const exec = deps.exec ?? defaultExec;
|
|
630
696
|
const id = randomUUID2();
|
|
631
|
-
const quarantinePath =
|
|
697
|
+
const quarantinePath = join5(quarantineDir, id);
|
|
632
698
|
mkdirSync3(quarantinePath, { recursive: true });
|
|
633
699
|
let packageDir;
|
|
634
700
|
switch (source.type) {
|
|
@@ -644,7 +710,7 @@ async function quarantine(source, quarantineDir, deps = {}) {
|
|
|
644
710
|
break;
|
|
645
711
|
}
|
|
646
712
|
case "git": {
|
|
647
|
-
const clonePath =
|
|
713
|
+
const clonePath = join5(quarantinePath, deriveGitDirName(source.value));
|
|
648
714
|
const result = await exec("git", ["clone", "--depth", "1", "--", source.value, clonePath], {
|
|
649
715
|
cwd: quarantinePath
|
|
650
716
|
});
|
|
@@ -656,11 +722,11 @@ async function quarantine(source, quarantineDir, deps = {}) {
|
|
|
656
722
|
break;
|
|
657
723
|
}
|
|
658
724
|
case "local": {
|
|
659
|
-
if (!
|
|
725
|
+
if (!existsSync4(source.value)) {
|
|
660
726
|
throw new UserError(`local path not found: ${source.value}`);
|
|
661
727
|
}
|
|
662
728
|
const dirName = basename2(resolvePath(source.value));
|
|
663
|
-
cpSync(source.value,
|
|
729
|
+
cpSync(source.value, join5(quarantinePath, dirName), { recursive: true });
|
|
664
730
|
break;
|
|
665
731
|
}
|
|
666
732
|
case "npm": {
|
|
@@ -674,7 +740,7 @@ async function quarantine(source, quarantineDir, deps = {}) {
|
|
|
674
740
|
`npm pack failed for "${source.value}" (exit ${packResult.code}): ${packResult.stderr || packResult.stdout}`
|
|
675
741
|
);
|
|
676
742
|
}
|
|
677
|
-
const tarballPath =
|
|
743
|
+
const tarballPath = join5(quarantinePath, findTarball(quarantinePath, source.value));
|
|
678
744
|
const listResult = await exec("tar", ["-tzf", tarballPath], { cwd: quarantinePath });
|
|
679
745
|
if (listResult.code !== 0) {
|
|
680
746
|
throw new UserError(
|
|
@@ -690,8 +756,8 @@ async function quarantine(source, quarantineDir, deps = {}) {
|
|
|
690
756
|
`tarball extraction failed for "${source.value}" (exit ${extractResult.code}): ${extractResult.stderr || extractResult.stdout}`
|
|
691
757
|
);
|
|
692
758
|
}
|
|
693
|
-
packageDir =
|
|
694
|
-
if (!
|
|
759
|
+
packageDir = join5(quarantinePath, "package");
|
|
760
|
+
if (!existsSync4(packageDir)) {
|
|
695
761
|
throw new UserError(
|
|
696
762
|
`npm pack for "${source.value}" did not produce the expected package/ dir after extraction`
|
|
697
763
|
);
|
|
@@ -705,7 +771,7 @@ async function quarantine(source, quarantineDir, deps = {}) {
|
|
|
705
771
|
return { id, quarantinePath, skillDirs: findSkillDirs(quarantinePath), packageDir, source };
|
|
706
772
|
}
|
|
707
773
|
function listQuarantineIds(quarantineDir) {
|
|
708
|
-
if (!
|
|
774
|
+
if (!existsSync4(quarantineDir)) return [];
|
|
709
775
|
return readdirSync2(quarantineDir, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name);
|
|
710
776
|
}
|
|
711
777
|
function findSkillDirs(root) {
|
|
@@ -722,7 +788,7 @@ function findSkillDirs(root) {
|
|
|
722
788
|
}
|
|
723
789
|
for (const entry of entries) {
|
|
724
790
|
if (entry.isDirectory() && !SKIP_DIRS2.has(entry.name)) {
|
|
725
|
-
walk2(
|
|
791
|
+
walk2(join5(dir, entry.name));
|
|
726
792
|
}
|
|
727
793
|
}
|
|
728
794
|
}
|
|
@@ -765,27 +831,27 @@ function defaultExec(command, args, options) {
|
|
|
765
831
|
}
|
|
766
832
|
|
|
767
833
|
// src/install/promote.ts
|
|
768
|
-
import { cpSync as cpSync2, existsSync as
|
|
769
|
-
import { basename as basename3, join as
|
|
834
|
+
import { cpSync as cpSync2, existsSync as existsSync5, mkdirSync as mkdirSync4, renameSync as renameSync2, rmSync as rmSync2 } from "fs";
|
|
835
|
+
import { basename as basename3, join as join7 } from "path";
|
|
770
836
|
|
|
771
837
|
// src/install/fingerprint.ts
|
|
772
838
|
import { createHash } from "crypto";
|
|
773
|
-
import { lstatSync as lstatSync2, readFileSync as
|
|
774
|
-
import { isAbsolute, join as
|
|
839
|
+
import { lstatSync as lstatSync2, readFileSync as readFileSync4, readdirSync as readdirSync3, readlinkSync, realpathSync as realpathSync2 } from "fs";
|
|
840
|
+
import { isAbsolute as isAbsolute2, join as join6, relative, sep } from "path";
|
|
775
841
|
var SKIP_DIRS3 = /* @__PURE__ */ new Set([".git", "node_modules"]);
|
|
776
842
|
function isInside(child, root) {
|
|
777
843
|
const rel = relative(root, child);
|
|
778
|
-
return rel === "" || !rel.startsWith(".." + sep) && rel !== ".." && !
|
|
844
|
+
return rel === "" || !rel.startsWith(".." + sep) && rel !== ".." && !isAbsolute2(rel);
|
|
779
845
|
}
|
|
780
846
|
function fingerprintTree(root) {
|
|
781
847
|
const hash = createHash("sha256");
|
|
782
848
|
const symlinks = [];
|
|
783
849
|
const escaping = [];
|
|
784
|
-
const realRoot =
|
|
850
|
+
const realRoot = realpathSync2(root);
|
|
785
851
|
function walk2(dir) {
|
|
786
852
|
const entries = readdirSync3(dir, { withFileTypes: true }).sort((a, b) => a.name < b.name ? -1 : a.name > b.name ? 1 : 0);
|
|
787
853
|
for (const entry of entries) {
|
|
788
|
-
const full =
|
|
854
|
+
const full = join6(dir, entry.name);
|
|
789
855
|
const rel = relative(root, full);
|
|
790
856
|
if (entry.isSymbolicLink()) {
|
|
791
857
|
const target = readlinkSync(full);
|
|
@@ -793,7 +859,7 @@ function fingerprintTree(root) {
|
|
|
793
859
|
hash.update(`L ${rel} ${target}
|
|
794
860
|
`);
|
|
795
861
|
try {
|
|
796
|
-
if (!isInside(
|
|
862
|
+
if (!isInside(realpathSync2(full), realRoot)) escaping.push(rel);
|
|
797
863
|
} catch {
|
|
798
864
|
escaping.push(rel);
|
|
799
865
|
}
|
|
@@ -807,7 +873,7 @@ function fingerprintTree(root) {
|
|
|
807
873
|
continue;
|
|
808
874
|
}
|
|
809
875
|
if (entry.isFile()) {
|
|
810
|
-
const contentHash = createHash("sha256").update(
|
|
876
|
+
const contentHash = createHash("sha256").update(readFileSync4(full)).digest("hex");
|
|
811
877
|
hash.update(`F ${rel} ${contentHash}
|
|
812
878
|
`);
|
|
813
879
|
continue;
|
|
@@ -856,8 +922,8 @@ function promote(entry, targetSkillsRoot, options = {}) {
|
|
|
856
922
|
`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`
|
|
857
923
|
);
|
|
858
924
|
}
|
|
859
|
-
const dest =
|
|
860
|
-
if (
|
|
925
|
+
const dest = join7(targetSkillsRoot, basename3(skillDir));
|
|
926
|
+
if (existsSync5(dest) && !force) {
|
|
861
927
|
throw new Error(`Refusing to overwrite existing skill dir: ${dest} (pass force to override)`);
|
|
862
928
|
}
|
|
863
929
|
}
|
|
@@ -876,8 +942,8 @@ function promote(entry, targetSkillsRoot, options = {}) {
|
|
|
876
942
|
mkdirSync4(targetSkillsRoot, { recursive: true });
|
|
877
943
|
const promoted = [];
|
|
878
944
|
for (const skillDir of entry.skillDirs) {
|
|
879
|
-
const dest =
|
|
880
|
-
if (
|
|
945
|
+
const dest = join7(targetSkillsRoot, basename3(skillDir));
|
|
946
|
+
if (existsSync5(dest)) {
|
|
881
947
|
rmSync2(dest, { recursive: true, force: true });
|
|
882
948
|
}
|
|
883
949
|
moveDir(skillDir, dest);
|
|
@@ -905,7 +971,7 @@ function moveDir(src, dest) {
|
|
|
905
971
|
}
|
|
906
972
|
|
|
907
973
|
// src/install/promoteMcp.ts
|
|
908
|
-
import { existsSync as
|
|
974
|
+
import { existsSync as existsSync6, mkdirSync as mkdirSync5, readFileSync as readFileSync5, writeFileSync as writeFileSync3 } from "fs";
|
|
909
975
|
import { dirname as dirname2 } from "path";
|
|
910
976
|
function promoteMcp(_entry, profile, targetConfigPath, options = {}) {
|
|
911
977
|
const force = options.force ?? false;
|
|
@@ -913,8 +979,8 @@ function promoteMcp(_entry, profile, targetConfigPath, options = {}) {
|
|
|
913
979
|
const { command, args, envVarsToFill, autoPinned } = deriveServerEntry(profile);
|
|
914
980
|
let config = {};
|
|
915
981
|
let rawExisting;
|
|
916
|
-
if (
|
|
917
|
-
rawExisting =
|
|
982
|
+
if (existsSync6(targetConfigPath)) {
|
|
983
|
+
rawExisting = readFileSync5(targetConfigPath, "utf-8");
|
|
918
984
|
try {
|
|
919
985
|
config = JSON.parse(rawExisting);
|
|
920
986
|
} catch (err) {
|
|
@@ -998,11 +1064,11 @@ function deriveServerEntry(profile) {
|
|
|
998
1064
|
}
|
|
999
1065
|
|
|
1000
1066
|
// src/install/mcpCandidate.ts
|
|
1001
|
-
import { readFileSync as
|
|
1002
|
-
import { dirname as dirname3, join as
|
|
1067
|
+
import { readFileSync as readFileSync6 } from "fs";
|
|
1068
|
+
import { dirname as dirname3, join as join8 } from "path";
|
|
1003
1069
|
function readPackageInfo(pkgPath) {
|
|
1004
1070
|
try {
|
|
1005
|
-
const pkg2 = JSON.parse(
|
|
1071
|
+
const pkg2 = JSON.parse(readFileSync6(pkgPath, "utf-8"));
|
|
1006
1072
|
return {
|
|
1007
1073
|
name: typeof pkg2.name === "string" ? pkg2.name : void 0,
|
|
1008
1074
|
version: typeof pkg2.version === "string" ? pkg2.version : void 0
|
|
@@ -1013,7 +1079,7 @@ function readPackageInfo(pkgPath) {
|
|
|
1013
1079
|
}
|
|
1014
1080
|
function readConfigEntry(configPath, name) {
|
|
1015
1081
|
try {
|
|
1016
|
-
const cfg = JSON.parse(
|
|
1082
|
+
const cfg = JSON.parse(readFileSync6(configPath, "utf-8"));
|
|
1017
1083
|
const servers = cfg.mcpServers ?? cfg.servers ?? {};
|
|
1018
1084
|
const raw = servers[name] ?? {};
|
|
1019
1085
|
return {
|
|
@@ -1031,7 +1097,7 @@ function resolveMcpCandidateDetail(profile) {
|
|
|
1031
1097
|
return { name, version: readPackageInfo(profile.config).version };
|
|
1032
1098
|
}
|
|
1033
1099
|
const entry = readConfigEntry(profile.config, name);
|
|
1034
|
-
const sidecar = readPackageInfo(
|
|
1100
|
+
const sidecar = readPackageInfo(join8(dirname3(profile.config), "package.json"));
|
|
1035
1101
|
return {
|
|
1036
1102
|
name,
|
|
1037
1103
|
command: entry?.command,
|
|
@@ -1043,8 +1109,8 @@ function resolveMcpCandidateDetail(profile) {
|
|
|
1043
1109
|
}
|
|
1044
1110
|
|
|
1045
1111
|
// src/provenance.ts
|
|
1046
|
-
import { appendFileSync, existsSync as
|
|
1047
|
-
import { join as
|
|
1112
|
+
import { appendFileSync, existsSync as existsSync7, readFileSync as readFileSync7, statSync as statSync3, writeFileSync as writeFileSync4 } from "fs";
|
|
1113
|
+
import { join as join9 } from "path";
|
|
1048
1114
|
var FORGE_VERBS = ["DEFER", "ABSORB", "FORK", "REJECT", "WATCH"];
|
|
1049
1115
|
var LEDGER_HEADER = "# Skill Forge \u2014 Provenance Ledger\n\nOne entry per external-skill ingestion decision.\n";
|
|
1050
1116
|
function todayLocalIsoDate() {
|
|
@@ -1058,7 +1124,7 @@ function appendProvenance(entry, skillsRoot) {
|
|
|
1058
1124
|
if (!FORGE_VERBS.includes(entry.verb)) {
|
|
1059
1125
|
throw new Error(`verb must be one of ${FORGE_VERBS.join(", ")}`);
|
|
1060
1126
|
}
|
|
1061
|
-
if (!
|
|
1127
|
+
if (!existsSync7(skillsRoot) || !statSync3(skillsRoot).isDirectory()) {
|
|
1062
1128
|
throw new Error(`skills-root not a directory: ${skillsRoot}`);
|
|
1063
1129
|
}
|
|
1064
1130
|
const today = todayLocalIsoDate();
|
|
@@ -1075,8 +1141,8 @@ function appendProvenance(entry, skillsRoot) {
|
|
|
1075
1141
|
- **Drift check:** \`${driftCheck}\`
|
|
1076
1142
|
- **Notes:** ${entry.notes ?? ""}
|
|
1077
1143
|
`;
|
|
1078
|
-
const ledgerPath =
|
|
1079
|
-
if (!
|
|
1144
|
+
const ledgerPath = join9(skillsRoot, "SOURCES.md");
|
|
1145
|
+
if (!existsSync7(ledgerPath)) {
|
|
1080
1146
|
writeFileSync4(ledgerPath, LEDGER_HEADER, "utf-8");
|
|
1081
1147
|
}
|
|
1082
1148
|
appendFileSync(ledgerPath, block, "utf-8");
|
|
@@ -1123,11 +1189,11 @@ function parseProvenanceLedger(text) {
|
|
|
1123
1189
|
return { entries, warnings };
|
|
1124
1190
|
}
|
|
1125
1191
|
function readProvenanceLedger(skillsRoot) {
|
|
1126
|
-
const ledgerPath =
|
|
1127
|
-
if (!
|
|
1192
|
+
const ledgerPath = join9(skillsRoot, "SOURCES.md");
|
|
1193
|
+
if (!existsSync7(ledgerPath)) {
|
|
1128
1194
|
return { ledgerPath, exists: false, entries: [], warnings: [] };
|
|
1129
1195
|
}
|
|
1130
|
-
const text =
|
|
1196
|
+
const text = readFileSync7(ledgerPath, "utf-8");
|
|
1131
1197
|
const parsed = parseProvenanceLedger(text);
|
|
1132
1198
|
return { ledgerPath, exists: true, ...parsed };
|
|
1133
1199
|
}
|
|
@@ -1136,28 +1202,115 @@ function readProvenanceLedger(skillsRoot) {
|
|
|
1136
1202
|
import { readdirSync as readdirSync7 } from "fs";
|
|
1137
1203
|
|
|
1138
1204
|
// src/auditState.ts
|
|
1139
|
-
import { existsSync as
|
|
1140
|
-
import { basename as basename7, dirname as dirname5, join as
|
|
1205
|
+
import { existsSync as existsSync11, readFileSync as readFileSync12 } from "fs";
|
|
1206
|
+
import { basename as basename7, dirname as dirname5, join as join14 } from "path";
|
|
1207
|
+
|
|
1208
|
+
// src/auditComparison.ts
|
|
1209
|
+
import { readFileSync as readFileSync8 } from "fs";
|
|
1210
|
+
import { relative as relative2, sep as sep2 } from "path";
|
|
1211
|
+
function targetIdentity(report, target) {
|
|
1212
|
+
for (const p of report.plugins ?? []) {
|
|
1213
|
+
const rel = relative2(p.installPath, target);
|
|
1214
|
+
if (rel !== ".." && !rel.startsWith(".." + sep2) && !rel.startsWith(sep2)) return `${p.host ?? "claude"}:${p.pluginId}/${rel}`;
|
|
1215
|
+
}
|
|
1216
|
+
return target;
|
|
1217
|
+
}
|
|
1218
|
+
function scope(report) {
|
|
1219
|
+
return JSON.stringify({
|
|
1220
|
+
host: report.hostInventory?.host ?? (report.plugins ? "claude" : "configured"),
|
|
1221
|
+
roots: report.inventory.skillRoots.filter((r) => r.origin !== "plugin").map((r) => r.root).sort(),
|
|
1222
|
+
skills: report.inventory.skills.map((s) => targetIdentity(report, s.path)).sort(),
|
|
1223
|
+
mcp: report.inventory.mcpTargets.map((m) => m.path).sort(),
|
|
1224
|
+
plugins: report.plugins?.map((p) => p.pluginId).sort()
|
|
1225
|
+
});
|
|
1226
|
+
}
|
|
1227
|
+
function incomplete(report) {
|
|
1228
|
+
return report.hostInventory?.complete === false || report.hygiene.some((f) => /unreadable|missing|symlink|parse|invalid/.test(f.rule));
|
|
1229
|
+
}
|
|
1230
|
+
function compareAudits(current, previous) {
|
|
1231
|
+
const result = { baselineAt: previous?.generatedAt, comparable: false, added: [], changed: [], resolved: [], unchanged: 0 };
|
|
1232
|
+
if (!previous || previous.schemaVersion !== 1 || !previous.inventory || !Array.isArray(previous.hygiene) || !Array.isArray(previous.inventory.skills) || !Array.isArray(previous.inventory.skillRoots) || !Array.isArray(previous.inventory.mcpTargets)) {
|
|
1233
|
+
return { ...result, reason: "Baseline must be a full schema-v1 audit JSON, not a routine/audit-state summary." };
|
|
1234
|
+
}
|
|
1235
|
+
if (!Number.isFinite(Date.parse(previous.generatedAt)) || Date.parse(previous.generatedAt) > Date.parse(current.generatedAt)) return { ...result, reason: "Baseline timestamp is invalid or newer than the current audit." };
|
|
1236
|
+
if (incomplete(current) || incomplete(previous) || scope(current) !== scope(previous)) {
|
|
1237
|
+
return { ...result, reason: "Inventory scope changed or a scan is incomplete; no resolution claims can be made. Review the full findings." };
|
|
1238
|
+
}
|
|
1239
|
+
const groups = (report) => {
|
|
1240
|
+
const map = /* @__PURE__ */ new Map();
|
|
1241
|
+
for (const f of report.hygiene) {
|
|
1242
|
+
const key = JSON.stringify([targetIdentity(report, f.target), f.rule]);
|
|
1243
|
+
map.set(key, [...map.get(key) ?? [], f]);
|
|
1244
|
+
}
|
|
1245
|
+
return map;
|
|
1246
|
+
};
|
|
1247
|
+
const old = groups(previous), next = groups(current);
|
|
1248
|
+
const content = (f) => JSON.stringify([f.severity, f.context ?? f.detail, f.classification ?? null]);
|
|
1249
|
+
for (const key of /* @__PURE__ */ new Set([...old.keys(), ...next.keys()])) {
|
|
1250
|
+
const before = [...old.get(key) ?? []];
|
|
1251
|
+
const after = [...next.get(key) ?? []];
|
|
1252
|
+
for (let i = after.length - 1; i >= 0; i--) {
|
|
1253
|
+
const match = before.findIndex((f) => content(f) === content(after[i]));
|
|
1254
|
+
if (match >= 0) {
|
|
1255
|
+
before.splice(match, 1);
|
|
1256
|
+
after.splice(i, 1);
|
|
1257
|
+
result.unchanged++;
|
|
1258
|
+
}
|
|
1259
|
+
}
|
|
1260
|
+
for (let i = after.length - 1; i >= 0; i--) {
|
|
1261
|
+
const identity = after[i].context ?? after[i].detail;
|
|
1262
|
+
const matches = before.map((f, n) => (f.context ?? f.detail) === identity ? n : -1).filter((n) => n >= 0);
|
|
1263
|
+
if (matches.length === 1 && after.filter((f) => (f.context ?? f.detail) === identity).length === 1) {
|
|
1264
|
+
result.changed.push({ before: before.splice(matches[0], 1)[0], after: after.splice(i, 1)[0] });
|
|
1265
|
+
}
|
|
1266
|
+
}
|
|
1267
|
+
if (before.length === 1 && after.length === 1) result.changed.push({ before: before[0], after: after[0] });
|
|
1268
|
+
else {
|
|
1269
|
+
result.added.push(...after);
|
|
1270
|
+
result.resolved.push(...before);
|
|
1271
|
+
}
|
|
1272
|
+
}
|
|
1273
|
+
result.comparable = true;
|
|
1274
|
+
return result;
|
|
1275
|
+
}
|
|
1276
|
+
function loadAuditComparison(current, path) {
|
|
1277
|
+
try {
|
|
1278
|
+
return compareAudits(current, JSON.parse(readFileSync8(path, "utf8")));
|
|
1279
|
+
} catch (error) {
|
|
1280
|
+
return { comparable: false, reason: `Baseline unusable: ${error.message}`, added: [], changed: [], resolved: [], unchanged: 0 };
|
|
1281
|
+
}
|
|
1282
|
+
}
|
|
1283
|
+
function renderAuditComparison(comparison) {
|
|
1284
|
+
const c = comparison;
|
|
1285
|
+
return ["## Changes since prior audit", "", ...c.comparable ? [
|
|
1286
|
+
`${c.added.length} new \xB7 ${c.changed.length} changed \xB7 ${c.resolved.length} no longer active \xB7 ${c.unchanged} unchanged.`,
|
|
1287
|
+
"No longer active can include accepted findings; it does not prove a security fix.",
|
|
1288
|
+
"",
|
|
1289
|
+
...c.added.map((f) => `- NEW ${f.severity} [${sanitizeForMarkdown(f.rule)}] ${sanitizeForMarkdown(f.target)}`),
|
|
1290
|
+
...c.changed.map((f) => `- CHANGED ${f.before.severity} \u2192 ${f.after.severity} [${sanitizeForMarkdown(f.after.rule)}] ${sanitizeForMarkdown(f.after.target)}`),
|
|
1291
|
+
...c.resolved.map((f) => `- NO LONGER ACTIVE [${sanitizeForMarkdown(f.rule)}] ${sanitizeForMarkdown(f.target)}`)
|
|
1292
|
+
] : [sanitizeForMarkdown(c.reason ?? "Comparison unavailable.")], ""];
|
|
1293
|
+
}
|
|
1141
1294
|
|
|
1142
1295
|
// src/audit.ts
|
|
1143
1296
|
import {
|
|
1144
|
-
existsSync as
|
|
1297
|
+
existsSync as existsSync10,
|
|
1145
1298
|
lstatSync as lstatSync3,
|
|
1146
1299
|
mkdtempSync,
|
|
1147
|
-
readFileSync as
|
|
1300
|
+
readFileSync as readFileSync11,
|
|
1148
1301
|
readdirSync as readdirSync5,
|
|
1149
|
-
realpathSync as
|
|
1302
|
+
realpathSync as realpathSync3,
|
|
1150
1303
|
renameSync as renameSync3,
|
|
1151
1304
|
rmSync as rmSync3,
|
|
1152
1305
|
statSync as statSync6,
|
|
1153
1306
|
writeFileSync as writeFileSync5
|
|
1154
1307
|
} from "fs";
|
|
1155
|
-
import { basename as basename6, delimiter as delimiter3, extname as extname4, join as
|
|
1308
|
+
import { basename as basename6, delimiter as delimiter3, extname as extname4, join as join13, resolve as resolvePath2, sep as sep3 } from "path";
|
|
1156
1309
|
|
|
1157
1310
|
// src/gate/safety.ts
|
|
1158
1311
|
import { statSync as statSync4 } from "fs";
|
|
1159
1312
|
import { execFile } from "child_process";
|
|
1160
|
-
import { delimiter as delimiter2, extname as extname2, join as
|
|
1313
|
+
import { delimiter as delimiter2, extname as extname2, join as join10 } from "path";
|
|
1161
1314
|
import { promisify } from "util";
|
|
1162
1315
|
var execFileAsync = promisify(execFile);
|
|
1163
1316
|
var RM_RF_TARGET_RE = /\brm\s+(?:-[a-z]+\s+)+("[^"]*"|'[^']*'|[^\s;|&)"'`]+)/i;
|
|
@@ -1203,6 +1356,10 @@ function hasUnnegatedInstallCue(line) {
|
|
|
1203
1356
|
}
|
|
1204
1357
|
function refineCurlPipeShell(line, ctx) {
|
|
1205
1358
|
const base = { severity: "CRITICAL", message: "downloads a remote script and pipes it directly into a shell" };
|
|
1359
|
+
const pipelines = [...line.matchAll(/\b(?:curl|wget)\b[^\n|]*\|\s*(?:sudo\s+)?(?:bash|sh|zsh)\b([^|;\n]*)/gi)];
|
|
1360
|
+
if (!/\$\(|`/.test(line) && pipelines.length && pipelines.every((m) => /^\s+(?:\.\/)?(?:[a-zA-Z0-9_-]+\/)*[a-zA-Z0-9_-]+\.sh(?:\s|$)/.test(m[1]))) {
|
|
1361
|
+
return { ...base, message: "passes downloaded data to a named local shell script; inspect that script for evaluation of stdin before use", classification: "local-script-input" };
|
|
1362
|
+
}
|
|
1206
1363
|
if (ctx.inFence) return base;
|
|
1207
1364
|
if (!DOC_EXTENSIONS.has(extname2(ctx.file).toLowerCase())) return base;
|
|
1208
1365
|
if (!DOC_CUE_RE.test(line)) return base;
|
|
@@ -1291,7 +1448,7 @@ var DENY_RULES = [
|
|
|
1291
1448
|
message: "invokes sudo \u2014 review for unnecessary privilege escalation"
|
|
1292
1449
|
}
|
|
1293
1450
|
];
|
|
1294
|
-
function scanStaticRules(targetPath) {
|
|
1451
|
+
function scanStaticRules(targetPath, capturedFiles) {
|
|
1295
1452
|
const findings = [];
|
|
1296
1453
|
let root;
|
|
1297
1454
|
try {
|
|
@@ -1300,8 +1457,8 @@ function scanStaticRules(targetPath) {
|
|
|
1300
1457
|
return findings;
|
|
1301
1458
|
}
|
|
1302
1459
|
const files = root.isDirectory() ? Array.from(walk(targetPath)) : [targetPath];
|
|
1303
|
-
|
|
1304
|
-
|
|
1460
|
+
const sources = capturedFiles ?? files.map((file) => ({ file, content: readTextFile(file) }));
|
|
1461
|
+
for (const { file, content } of sources) {
|
|
1305
1462
|
if (content === void 0) continue;
|
|
1306
1463
|
const lines = content.split("\n");
|
|
1307
1464
|
const fenceFlags = DOC_EXTENSIONS.has(extname2(file).toLowerCase()) ? computeFenceFlags(lines) : void 0;
|
|
@@ -1330,7 +1487,7 @@ function which(cmd) {
|
|
|
1330
1487
|
const exts = process.platform === "win32" ? (process.env.PATHEXT ?? ".EXE;.CMD;.BAT").split(";") : [""];
|
|
1331
1488
|
for (const dir of dirs) {
|
|
1332
1489
|
for (const ext of exts) {
|
|
1333
|
-
const candidate =
|
|
1490
|
+
const candidate = join10(dir, cmd + ext);
|
|
1334
1491
|
try {
|
|
1335
1492
|
if (statSync4(candidate).isFile()) return candidate;
|
|
1336
1493
|
} catch {
|
|
@@ -1364,8 +1521,8 @@ async function scanWithSkillSpector(targetPath) {
|
|
|
1364
1521
|
return [];
|
|
1365
1522
|
}
|
|
1366
1523
|
}
|
|
1367
|
-
async function scanSafety(targetPath, strictness = DEFAULT_STRICTNESS) {
|
|
1368
|
-
const findings = [...scanStaticRules(targetPath), ...await scanWithSkillSpector(targetPath)];
|
|
1524
|
+
async function scanSafety(targetPath, strictness = DEFAULT_STRICTNESS, capturedFiles) {
|
|
1525
|
+
const findings = [...scanStaticRules(targetPath, capturedFiles), ...await scanWithSkillSpector(targetPath)];
|
|
1369
1526
|
return { findings, verdict: verdictFor(findings, strictness) };
|
|
1370
1527
|
}
|
|
1371
1528
|
|
|
@@ -1605,8 +1762,8 @@ async function mcpScan(targetPath, strictness = DEFAULT_STRICTNESS) {
|
|
|
1605
1762
|
}
|
|
1606
1763
|
|
|
1607
1764
|
// src/gate/overlap.ts
|
|
1608
|
-
import { existsSync as
|
|
1609
|
-
import { basename as basename5, dirname as dirname4, join as
|
|
1765
|
+
import { existsSync as existsSync8, readFileSync as readFileSync9, readdirSync as readdirSync4 } from "fs";
|
|
1766
|
+
import { basename as basename5, dirname as dirname4, join as join11, resolve as resolve3 } from "path";
|
|
1610
1767
|
var STOPWORDS = new Set(
|
|
1611
1768
|
`
|
|
1612
1769
|
a an the and or of to for in on with use used using when whether this that these those is are be
|
|
@@ -1615,20 +1772,20 @@ skill skills user claude code based comprehensive enhanced practical including i
|
|
|
1615
1772
|
`.split(/\s+/).filter(Boolean)
|
|
1616
1773
|
);
|
|
1617
1774
|
var DEFAULT_SET_THRESHOLD = 0.45;
|
|
1618
|
-
function splitLimited(text,
|
|
1775
|
+
function splitLimited(text, sep11, maxSplit) {
|
|
1619
1776
|
const parts = [];
|
|
1620
1777
|
let rest = text;
|
|
1621
1778
|
for (let i = 0; i < maxSplit; i++) {
|
|
1622
|
-
const idx = rest.indexOf(
|
|
1779
|
+
const idx = rest.indexOf(sep11);
|
|
1623
1780
|
if (idx === -1) break;
|
|
1624
1781
|
parts.push(rest.slice(0, idx));
|
|
1625
|
-
rest = rest.slice(idx +
|
|
1782
|
+
rest = rest.slice(idx + sep11.length);
|
|
1626
1783
|
}
|
|
1627
1784
|
parts.push(rest);
|
|
1628
1785
|
return parts;
|
|
1629
1786
|
}
|
|
1630
1787
|
function readFrontmatter(skillMdPath) {
|
|
1631
|
-
const text =
|
|
1788
|
+
const text = readFileSync9(skillMdPath, "utf-8");
|
|
1632
1789
|
const meta = { name: basename5(dirname4(skillMdPath)), description: "" };
|
|
1633
1790
|
if (text.startsWith("---\n")) {
|
|
1634
1791
|
const block = splitLimited(text, "---", 2)[1] ?? "";
|
|
@@ -1723,47 +1880,47 @@ function suggestVerbCode(topScore) {
|
|
|
1723
1880
|
return verbTier(topScore).verb;
|
|
1724
1881
|
}
|
|
1725
1882
|
function listSubdirs(dir) {
|
|
1726
|
-
if (!
|
|
1883
|
+
if (!existsSync8(dir)) return [];
|
|
1727
1884
|
let entries;
|
|
1728
1885
|
try {
|
|
1729
1886
|
entries = readdirSync4(dir, { withFileTypes: true });
|
|
1730
1887
|
} catch {
|
|
1731
1888
|
return [];
|
|
1732
1889
|
}
|
|
1733
|
-
return entries.filter((e) => e.isDirectory()).map((e) =>
|
|
1890
|
+
return entries.filter((e) => e.isDirectory()).map((e) => join11(dir, e.name));
|
|
1734
1891
|
}
|
|
1735
1892
|
function findSkillMdFiles(root) {
|
|
1736
1893
|
const found = /* @__PURE__ */ new Set();
|
|
1737
1894
|
for (const dir of listSubdirs(root)) {
|
|
1738
|
-
const candidate =
|
|
1739
|
-
if (
|
|
1895
|
+
const candidate = join11(dir, "SKILL.md");
|
|
1896
|
+
if (existsSync8(candidate)) found.add(resolve3(candidate));
|
|
1740
1897
|
}
|
|
1741
|
-
for (const dir of listSubdirs(
|
|
1742
|
-
const candidate =
|
|
1743
|
-
if (
|
|
1898
|
+
for (const dir of listSubdirs(join11(root, "skills"))) {
|
|
1899
|
+
const candidate = join11(dir, "SKILL.md");
|
|
1900
|
+
if (existsSync8(candidate)) found.add(resolve3(candidate));
|
|
1744
1901
|
}
|
|
1745
1902
|
for (const topDir of listSubdirs(root)) {
|
|
1746
|
-
for (const dir of listSubdirs(
|
|
1747
|
-
const candidate =
|
|
1748
|
-
if (
|
|
1903
|
+
for (const dir of listSubdirs(join11(topDir, "skills"))) {
|
|
1904
|
+
const candidate = join11(dir, "SKILL.md");
|
|
1905
|
+
if (existsSync8(candidate)) found.add(resolve3(candidate));
|
|
1749
1906
|
}
|
|
1750
1907
|
}
|
|
1751
1908
|
return Array.from(found).filter((p) => !p.includes("/.git/")).sort();
|
|
1752
1909
|
}
|
|
1753
1910
|
function scanOverlap(candidatePath, skillsRoot) {
|
|
1754
|
-
const root =
|
|
1911
|
+
const root = resolve3(expandHome(skillsRoot));
|
|
1755
1912
|
if (!isDir(root)) {
|
|
1756
1913
|
throw new Error(`skills-root not a directory: ${root}`);
|
|
1757
1914
|
}
|
|
1758
|
-
let cpath =
|
|
1915
|
+
let cpath = resolve3(expandHome(candidatePath));
|
|
1759
1916
|
if (isDir(cpath)) {
|
|
1760
|
-
cpath =
|
|
1917
|
+
cpath = join11(cpath, "SKILL.md");
|
|
1761
1918
|
}
|
|
1762
|
-
if (!
|
|
1919
|
+
if (!existsSync8(cpath)) {
|
|
1763
1920
|
throw new Error(`candidate SKILL.md not found: ${cpath}`);
|
|
1764
1921
|
}
|
|
1765
1922
|
const cand = readFrontmatter(cpath);
|
|
1766
|
-
const existing = findSkillMdFiles(root).filter((p) => p !==
|
|
1923
|
+
const existing = findSkillMdFiles(root).filter((p) => p !== resolve3(cpath));
|
|
1767
1924
|
if (existing.length === 0) {
|
|
1768
1925
|
throw new Error(`no existing skills found under ${root}`);
|
|
1769
1926
|
}
|
|
@@ -1804,8 +1961,8 @@ function scanOverlapPaths(skillMdPaths, threshold = DEFAULT_SET_THRESHOLD) {
|
|
|
1804
1961
|
|
|
1805
1962
|
// src/findings.ts
|
|
1806
1963
|
import { createHash as createHash2 } from "crypto";
|
|
1807
|
-
import { existsSync as
|
|
1808
|
-
import { join as
|
|
1964
|
+
import { existsSync as existsSync9, readFileSync as readFileSync10 } from "fs";
|
|
1965
|
+
import { join as join12 } from "path";
|
|
1809
1966
|
var FINGERPRINT_DISPLAY_LENGTH = 12;
|
|
1810
1967
|
var MIN_FINGERPRINT_PREFIX_LENGTH = 8;
|
|
1811
1968
|
var FINGERPRINT_HEX_RE = /^[0-9a-f]{64}$/;
|
|
@@ -1818,7 +1975,7 @@ function fingerprintFinding(f) {
|
|
|
1818
1975
|
return createHash2("sha256").update(`${f.severity}\0${f.rule}\0${f.target}\0${f.context}`).digest("hex");
|
|
1819
1976
|
}
|
|
1820
1977
|
function getAcceptedFindingsPath() {
|
|
1821
|
-
return
|
|
1978
|
+
return join12(getHomeDir(), "accepted-findings.json");
|
|
1822
1979
|
}
|
|
1823
1980
|
function invalidRecordReason(record) {
|
|
1824
1981
|
if (typeof record !== "object" || record === null) return "not an object";
|
|
@@ -1838,10 +1995,10 @@ function invalidRecordReason(record) {
|
|
|
1838
1995
|
}
|
|
1839
1996
|
function readAcceptedFindings() {
|
|
1840
1997
|
const path = getAcceptedFindingsPath();
|
|
1841
|
-
if (!
|
|
1998
|
+
if (!existsSync9(path)) return { records: [], notices: [] };
|
|
1842
1999
|
let parsed;
|
|
1843
2000
|
try {
|
|
1844
|
-
parsed = JSON.parse(
|
|
2001
|
+
parsed = JSON.parse(readFileSync10(path, "utf-8"));
|
|
1845
2002
|
} catch {
|
|
1846
2003
|
return { records: [], notices: ["accepted-findings store unreadable (corrupt JSON) \u2014 nothing suppressed this run"] };
|
|
1847
2004
|
}
|
|
@@ -2011,7 +2168,7 @@ function listSubdirsFollowingSymlinks(dir) {
|
|
|
2011
2168
|
}
|
|
2012
2169
|
const out = [];
|
|
2013
2170
|
for (const entry of entries) {
|
|
2014
|
-
const full =
|
|
2171
|
+
const full = join13(dir, entry.name);
|
|
2015
2172
|
if (entry.isDirectory() || entry.isSymbolicLink() && isDirFollowingSymlink(full)) {
|
|
2016
2173
|
out.push(full);
|
|
2017
2174
|
}
|
|
@@ -2025,20 +2182,20 @@ function listSubdirsNoSymlinks(dir) {
|
|
|
2025
2182
|
} catch {
|
|
2026
2183
|
return [];
|
|
2027
2184
|
}
|
|
2028
|
-
return entries.filter((e) => e.isDirectory()).map((e) =>
|
|
2185
|
+
return entries.filter((e) => e.isDirectory()).map((e) => join13(dir, e.name));
|
|
2029
2186
|
}
|
|
2030
2187
|
function findSkillDirsUnderRoot(root) {
|
|
2031
2188
|
const found = /* @__PURE__ */ new Set();
|
|
2032
2189
|
const rootSubdirs = listSubdirsFollowingSymlinks(root);
|
|
2033
2190
|
for (const dir of rootSubdirs) {
|
|
2034
|
-
if (
|
|
2191
|
+
if (existsSync10(join13(dir, "SKILL.md"))) found.add(dir);
|
|
2035
2192
|
}
|
|
2036
|
-
for (const dir of listSubdirsFollowingSymlinks(
|
|
2037
|
-
if (
|
|
2193
|
+
for (const dir of listSubdirsFollowingSymlinks(join13(root, "skills"))) {
|
|
2194
|
+
if (existsSync10(join13(dir, "SKILL.md"))) found.add(dir);
|
|
2038
2195
|
}
|
|
2039
2196
|
for (const topDir of rootSubdirs) {
|
|
2040
|
-
for (const dir of listSubdirsNoSymlinks(
|
|
2041
|
-
if (
|
|
2197
|
+
for (const dir of listSubdirsNoSymlinks(join13(topDir, "skills"))) {
|
|
2198
|
+
if (existsSync10(join13(dir, "SKILL.md"))) found.add(dir);
|
|
2042
2199
|
}
|
|
2043
2200
|
}
|
|
2044
2201
|
return Array.from(found).filter((p) => !p.includes("/.git/"));
|
|
@@ -2052,7 +2209,7 @@ function collectSkillCandidates(roots) {
|
|
|
2052
2209
|
let realPath;
|
|
2053
2210
|
if (exists) {
|
|
2054
2211
|
try {
|
|
2055
|
-
realPath =
|
|
2212
|
+
realPath = realpathSync3(resolvedRoot);
|
|
2056
2213
|
} catch {
|
|
2057
2214
|
realPath = void 0;
|
|
2058
2215
|
}
|
|
@@ -2071,7 +2228,7 @@ function dedupeSkillCandidates(candidates, findings) {
|
|
|
2071
2228
|
for (const candidate of candidates) {
|
|
2072
2229
|
let realPath;
|
|
2073
2230
|
try {
|
|
2074
|
-
realPath =
|
|
2231
|
+
realPath = realpathSync3(candidate.configuredPath);
|
|
2075
2232
|
} catch (err) {
|
|
2076
2233
|
findings.push({
|
|
2077
2234
|
severity: "MEDIUM",
|
|
@@ -2134,7 +2291,7 @@ function estimateTokens(text) {
|
|
|
2134
2291
|
return Math.ceil(Buffer.byteLength(text, "utf-8") / 4);
|
|
2135
2292
|
}
|
|
2136
2293
|
function profileAuditedSkill(group, findings, unreadableSkillMdPaths, pluginId) {
|
|
2137
|
-
const skillMdPath =
|
|
2294
|
+
const skillMdPath = join13(group.canonicalPath, "SKILL.md");
|
|
2138
2295
|
const base = {
|
|
2139
2296
|
path: group.canonicalPath,
|
|
2140
2297
|
skillMdPath,
|
|
@@ -2146,7 +2303,7 @@ function profileAuditedSkill(group, findings, unreadableSkillMdPaths, pluginId)
|
|
|
2146
2303
|
let sizeBytes;
|
|
2147
2304
|
try {
|
|
2148
2305
|
sizeBytes = statSync6(skillMdPath).size;
|
|
2149
|
-
text =
|
|
2306
|
+
text = readFileSync11(skillMdPath, "utf-8");
|
|
2150
2307
|
} catch (err) {
|
|
2151
2308
|
findings.push({
|
|
2152
2309
|
severity: "MEDIUM",
|
|
@@ -2203,7 +2360,7 @@ function auditMcpTarget(targetPath, notices) {
|
|
|
2203
2360
|
const findings = [];
|
|
2204
2361
|
const isToml = extname4(targetPath).toLowerCase() === ".toml";
|
|
2205
2362
|
const format = isToml ? "toml" : "json";
|
|
2206
|
-
if (!
|
|
2363
|
+
if (!existsSync10(targetPath)) {
|
|
2207
2364
|
findings.push({
|
|
2208
2365
|
severity: "LOW",
|
|
2209
2366
|
rule: "missing-mcp-target",
|
|
@@ -2224,7 +2381,7 @@ function auditMcpTarget(targetPath, notices) {
|
|
|
2224
2381
|
}
|
|
2225
2382
|
if (isToml) {
|
|
2226
2383
|
try {
|
|
2227
|
-
|
|
2384
|
+
readFileSync11(targetPath, "utf-8");
|
|
2228
2385
|
} catch (err) {
|
|
2229
2386
|
findings.push({
|
|
2230
2387
|
severity: "MEDIUM",
|
|
@@ -2239,7 +2396,7 @@ function auditMcpTarget(targetPath, notices) {
|
|
|
2239
2396
|
}
|
|
2240
2397
|
let raw;
|
|
2241
2398
|
try {
|
|
2242
|
-
raw =
|
|
2399
|
+
raw = readFileSync11(targetPath, "utf-8");
|
|
2243
2400
|
} catch (err) {
|
|
2244
2401
|
findings.push({
|
|
2245
2402
|
severity: "MEDIUM",
|
|
@@ -2373,18 +2530,32 @@ function buildUsageJoinIndex(usage, skills) {
|
|
|
2373
2530
|
}
|
|
2374
2531
|
const inventoryNameCounts = /* @__PURE__ */ new Map();
|
|
2375
2532
|
for (const skill of skills) inventoryNameCounts.set(skill.name, (inventoryNameCounts.get(skill.name) ?? 0) + 1);
|
|
2376
|
-
|
|
2533
|
+
const compositeCounts = /* @__PURE__ */ new Map();
|
|
2534
|
+
for (const skill of skills) {
|
|
2535
|
+
if (!skill.pluginId) continue;
|
|
2536
|
+
const key = `${skill.pluginId.split("@")[0]}:${skill.name}`;
|
|
2537
|
+
compositeCounts.set(key, (compositeCounts.get(key) ?? 0) + 1);
|
|
2538
|
+
}
|
|
2539
|
+
return { bareKeys, inventoryNameCounts, compositeCounts, noticed: /* @__PURE__ */ new Set(), uncertainPaths: /* @__PURE__ */ new Set() };
|
|
2377
2540
|
}
|
|
2378
2541
|
function matchUsage(skill, usage, index, notices) {
|
|
2379
|
-
if (usage.has(skill.name)) return usage.get(skill.name);
|
|
2380
2542
|
if (skill.pluginId) {
|
|
2381
2543
|
const composite = `${skill.pluginId.split("@")[0]}:${skill.name}`;
|
|
2382
|
-
if (usage.has(composite)
|
|
2544
|
+
if (usage.has(composite) && index.compositeCounts.get(composite) === 1) {
|
|
2545
|
+
if (usage.get(composite) === 0 && (usage.get(skill.name) ?? 0) > 0) {
|
|
2546
|
+
index.uncertainPaths.add(skill.path);
|
|
2547
|
+
notices.push(`usage snapshot: conflicting zero and positive aliases for "${skill.name}" \u2014 skipping attribution`);
|
|
2548
|
+
return void 0;
|
|
2549
|
+
}
|
|
2550
|
+
return usage.get(composite);
|
|
2551
|
+
}
|
|
2383
2552
|
}
|
|
2384
2553
|
const bareMatches = index.bareKeys.get(skill.name) ?? [];
|
|
2385
2554
|
if (bareMatches.length === 0) return void 0;
|
|
2386
2555
|
const installedWithName = index.inventoryNameCounts.get(skill.name) ?? 0;
|
|
2387
|
-
if (
|
|
2556
|
+
if (installedWithName === 1 && usage.has(skill.name)) return usage.get(skill.name);
|
|
2557
|
+
if (bareMatches.length === 1 && installedWithName === 1 && !skill.pluginId) return usage.get(bareMatches[0]);
|
|
2558
|
+
index.uncertainPaths.add(skill.path);
|
|
2388
2559
|
if (!index.noticed.has(skill.name)) {
|
|
2389
2560
|
index.noticed.add(skill.name);
|
|
2390
2561
|
notices.push(
|
|
@@ -2394,9 +2565,9 @@ function matchUsage(skill, usage, index, notices) {
|
|
|
2394
2565
|
return void 0;
|
|
2395
2566
|
}
|
|
2396
2567
|
function isUnderInstallPath(target, installPath) {
|
|
2397
|
-
return target === installPath || target.startsWith(installPath +
|
|
2568
|
+
return target === installPath || target.startsWith(installPath + sep3);
|
|
2398
2569
|
}
|
|
2399
|
-
function computePluginSummaries(pluginRoots, activeFindings, skills, usage, usageJoined) {
|
|
2570
|
+
function computePluginSummaries(pluginRoots, activeFindings, skills, usage, usageJoined, uncertainPaths, positiveJoined) {
|
|
2400
2571
|
return pluginRoots.map((pr) => {
|
|
2401
2572
|
const findingCounts = { LOW: 0, MEDIUM: 0, HIGH: 0, CRITICAL: 0 };
|
|
2402
2573
|
for (const f of activeFindings) {
|
|
@@ -2410,9 +2581,10 @@ function computePluginSummaries(pluginRoots, activeFindings, skills, usage, usag
|
|
|
2410
2581
|
let recommendation;
|
|
2411
2582
|
const reasons = [];
|
|
2412
2583
|
const longWindow = windowDays !== void 0 && windowDays >= 28;
|
|
2584
|
+
const ambiguousUsage = skills.some((s) => s.pluginId === pr.pluginId && uncertainPaths.has(s.path));
|
|
2413
2585
|
if (findingCounts.HIGH > 0 || findingCounts.CRITICAL > 0) {
|
|
2414
2586
|
recommendation = "review";
|
|
2415
|
-
} else if (exhaustive && longWindow && skillCount > 0 && observedSkillCount === 0 &&
|
|
2587
|
+
} else if (!ambiguousUsage && exhaustive && longWindow && skillCount > 0 && observedSkillCount === 0 && positiveJoined > 0) {
|
|
2416
2588
|
recommendation = "unobserved";
|
|
2417
2589
|
} else if (exhaustive && (observedSkillCount ?? 0) >= 1) {
|
|
2418
2590
|
recommendation = "keep";
|
|
@@ -2421,9 +2593,11 @@ function computePluginSummaries(pluginRoots, activeFindings, skills, usage, usag
|
|
|
2421
2593
|
if (skillCount === 0) reasons.push("no skills to observe");
|
|
2422
2594
|
if (!hasUsage) reasons.push("no usage telemetry");
|
|
2423
2595
|
else if (usage.usage.size === 0) reasons.push("usage snapshot is empty or unreadable (see notices)");
|
|
2424
|
-
else if (!exhaustive) reasons.push("usage snapshot is not exhaustive (top-50 only)");
|
|
2596
|
+
else if (!exhaustive) reasons.push("usage snapshot is not exhaustive (top-50 only or invalid counts)");
|
|
2597
|
+
else if (ambiguousUsage) reasons.push("ambiguous usage identity \u2014 absence is not zero usage");
|
|
2425
2598
|
else if (!longWindow) reasons.push(`window ${windowDays ?? "unknown"} < 28 days`);
|
|
2426
2599
|
else if ((usageJoined ?? 0) === 0) reasons.push("usage snapshot joined 0 installed skills");
|
|
2600
|
+
else if (positiveJoined === 0) reasons.push("usage snapshot joined no positive installed-skill usage");
|
|
2427
2601
|
}
|
|
2428
2602
|
return {
|
|
2429
2603
|
pluginId: pr.pluginId,
|
|
@@ -2443,7 +2617,7 @@ function defaultWhich(cmd) {
|
|
|
2443
2617
|
const exts = process.platform === "win32" ? (process.env.PATHEXT ?? ".EXE;.CMD;.BAT").split(";") : [""];
|
|
2444
2618
|
for (const dir of dirs) {
|
|
2445
2619
|
for (const ext of exts) {
|
|
2446
|
-
const candidate =
|
|
2620
|
+
const candidate = join13(dir, cmd + ext);
|
|
2447
2621
|
try {
|
|
2448
2622
|
if (statSync6(candidate).isFile()) return candidate;
|
|
2449
2623
|
} catch {
|
|
@@ -2643,18 +2817,21 @@ async function auditConfiguredSet(config, options = {}) {
|
|
|
2643
2817
|
message: "no business-foundation skill yet \u2014 `skill-forge audit --foundation` can scaffold one to ground future skills in your business profile"
|
|
2644
2818
|
};
|
|
2645
2819
|
const usage = options.usage;
|
|
2820
|
+
const uncertainPaths = /* @__PURE__ */ new Set();
|
|
2646
2821
|
if (usage) {
|
|
2647
2822
|
const index = buildUsageJoinIndex(usage.usage, skills);
|
|
2648
2823
|
for (const skill of skills) {
|
|
2649
2824
|
const matched = matchUsage(skill, usage.usage, index, notices);
|
|
2650
2825
|
if (matched !== void 0) skill.usage = matched;
|
|
2826
|
+
if (index.uncertainPaths.has(skill.path)) uncertainPaths.add(skill.path);
|
|
2651
2827
|
}
|
|
2652
2828
|
}
|
|
2829
|
+
const positiveJoined = skills.filter((s) => (s.usage ?? 0) > 0).length;
|
|
2653
2830
|
const usageJoined = usage ? skills.filter((s) => s.usage !== void 0).length : void 0;
|
|
2654
2831
|
if (usage && usageJoined === 0) {
|
|
2655
2832
|
notices.push(`usage snapshot (${usage.sourcePath}) joined 0 of ${skills.length} skill(s)`);
|
|
2656
2833
|
}
|
|
2657
|
-
const unobservedInWindow = usage && usage.exhaustive && usage.windowDays !== void 0 && usage.windowDays >= 28 &&
|
|
2834
|
+
const unobservedInWindow = usage && usage.exhaustive && usage.windowDays !== void 0 && usage.windowDays >= 28 && positiveJoined > 0 ? skills.filter((s) => (s.usage ?? 0) === 0 && !uncertainPaths.has(s.path)).map((s) => ({ name: s.name, path: s.path, pluginId: s.pluginId })) : [];
|
|
2658
2835
|
const findingCounts = { LOW: 0, MEDIUM: 0, HIGH: 0, CRITICAL: 0 };
|
|
2659
2836
|
for (const f of activeFindings) findingCounts[f.severity]++;
|
|
2660
2837
|
const summary = {
|
|
@@ -2669,7 +2846,7 @@ async function auditConfiguredSet(config, options = {}) {
|
|
|
2669
2846
|
documentationExampleCount: activeFindings.filter((f) => f.classification === "documentation-example").length,
|
|
2670
2847
|
...usage ? { usageJoined, usageExhaustive: usage.exhaustive, usageWindowDays: usage.windowDays } : {}
|
|
2671
2848
|
};
|
|
2672
|
-
const
|
|
2849
|
+
const plugins2 = options.pluginRoots ? computePluginSummaries(pluginRoots, activeFindings, skills, usage, usageJoined, uncertainPaths, positiveJoined) : void 0;
|
|
2673
2850
|
const tooling = checkTooling(options.which ?? defaultWhich, options.env ?? process.env);
|
|
2674
2851
|
return {
|
|
2675
2852
|
schemaVersion: 1,
|
|
@@ -2681,7 +2858,7 @@ async function auditConfiguredSet(config, options = {}) {
|
|
|
2681
2858
|
tooling,
|
|
2682
2859
|
notices,
|
|
2683
2860
|
summary,
|
|
2684
|
-
plugins
|
|
2861
|
+
plugins: plugins2
|
|
2685
2862
|
};
|
|
2686
2863
|
}
|
|
2687
2864
|
function bulletList(items, emptyPlaceholder) {
|
|
@@ -2816,16 +2993,17 @@ function renderUsageSection(report) {
|
|
|
2816
2993
|
}
|
|
2817
2994
|
return lines;
|
|
2818
2995
|
}
|
|
2819
|
-
function renderPluginsSection(
|
|
2996
|
+
function renderPluginsSection(plugins2) {
|
|
2820
2997
|
return [
|
|
2821
2998
|
"## Plugins",
|
|
2822
2999
|
"",
|
|
2823
3000
|
...bulletList(
|
|
2824
|
-
|
|
3001
|
+
plugins2.map((p) => {
|
|
2825
3002
|
const findingsPart = `${p.findingCounts.CRITICAL} CRITICAL, ${p.findingCounts.HIGH} HIGH, ${p.findingCounts.MEDIUM} MEDIUM, ${p.findingCounts.LOW} LOW`;
|
|
2826
3003
|
const observedPart = p.observedSkillCount !== void 0 ? `, observed ${p.observedSkillCount}` : "";
|
|
2827
|
-
const reasonsPart = p.reasons.length > 0 ? ` (${p.reasons.join("; ")})` : "";
|
|
2828
|
-
|
|
3004
|
+
const reasonsPart = p.reasons.length > 0 ? ` (${p.reasons.map(sanitizeForMarkdown).join("; ")})` : "";
|
|
3005
|
+
const action = p.recommendation === "review" ? " \u2014 next: inspect finding context, update or fix the owning source; do not edit an installed cache" : p.recommendation === "unobserved" ? " \u2014 next: check hooks, commands and occasional use; record a keep rationale or explicitly review disabling" : p.recommendation === "unknown" ? " \u2014 next: resolve the listed evidence gaps before a retention decision" : "";
|
|
3006
|
+
return `${sanitizeForMarkdown(p.pluginId)} @ ${sanitizeForMarkdown(p.version)} \u2014 ${sanitizeForMarkdown(p.installPath)} \u2014 ${p.skillCount} skill(s)${observedPart} \u2014 findings: ${findingsPart} \u2014 **${p.recommendation}**${reasonsPart}` + (p.keepDecision ? ` \u2014 explicit keep (${p.keepDecision.category}): ${sanitizeForMarkdown(p.keepDecision.reason)}` : "") + action;
|
|
2829
3007
|
}),
|
|
2830
3008
|
"(no enabled plugins)"
|
|
2831
3009
|
)
|
|
@@ -2858,6 +3036,7 @@ function renderAuditMarkdown(report) {
|
|
|
2858
3036
|
const lines = [
|
|
2859
3037
|
`# skill-forge audit \u2014 ${report.generatedAt}`,
|
|
2860
3038
|
"",
|
|
3039
|
+
...report.comparison ? renderAuditComparison(report.comparison) : [],
|
|
2861
3040
|
"## Summary",
|
|
2862
3041
|
"",
|
|
2863
3042
|
`- Skill roots: ${summary.rootCount}`,
|
|
@@ -2933,7 +3112,7 @@ async function scaffoldFoundationSkill(config, profile, targetRoot) {
|
|
|
2933
3112
|
if (!isDir(resolvedRoot)) {
|
|
2934
3113
|
throw new UserError(`skills root does not exist: ${resolvedRoot}`);
|
|
2935
3114
|
}
|
|
2936
|
-
const destDir =
|
|
3115
|
+
const destDir = join13(resolvedRoot, "business-foundation");
|
|
2937
3116
|
let destLstat;
|
|
2938
3117
|
try {
|
|
2939
3118
|
destLstat = lstatSync3(destDir);
|
|
@@ -2947,9 +3126,9 @@ async function scaffoldFoundationSkill(config, profile, targetRoot) {
|
|
|
2947
3126
|
throw new UserError(`refusing to scaffold a foundation skill at ${destDir}: already exists`);
|
|
2948
3127
|
}
|
|
2949
3128
|
const content = renderFoundationSkillMd(profile);
|
|
2950
|
-
const tempDir = mkdtempSync(
|
|
3129
|
+
const tempDir = mkdtempSync(join13(resolvedRoot, ".business-foundation-"));
|
|
2951
3130
|
try {
|
|
2952
|
-
writeFileSync5(
|
|
3131
|
+
writeFileSync5(join13(tempDir, "SKILL.md"), content, { flag: "wx", mode: 384 });
|
|
2953
3132
|
const safety = await scanSafety(tempDir, config.strictness);
|
|
2954
3133
|
if (safety.verdict === "block") {
|
|
2955
3134
|
throw new UserError(
|
|
@@ -2983,7 +3162,7 @@ async function scaffoldFoundationSkill(config, profile, targetRoot) {
|
|
|
2983
3162
|
|
|
2984
3163
|
// src/auditState.ts
|
|
2985
3164
|
function getAuditStatePath() {
|
|
2986
|
-
return
|
|
3165
|
+
return join14(getHomeDir(), "audit-state.json");
|
|
2987
3166
|
}
|
|
2988
3167
|
function emptyCounts() {
|
|
2989
3168
|
return { CRITICAL: 0, HIGH: 0, MEDIUM: 0, LOW: 0 };
|
|
@@ -3046,9 +3225,9 @@ function writeAuditState(state) {
|
|
|
3046
3225
|
}
|
|
3047
3226
|
function readAuditState() {
|
|
3048
3227
|
const path = getAuditStatePath();
|
|
3049
|
-
if (!
|
|
3228
|
+
if (!existsSync11(path)) return void 0;
|
|
3050
3229
|
try {
|
|
3051
|
-
const parsed = JSON.parse(
|
|
3230
|
+
const parsed = JSON.parse(readFileSync12(path, "utf-8"));
|
|
3052
3231
|
if (parsed.schemaVersion !== 1 || typeof parsed.lastRunAt !== "string") return void 0;
|
|
3053
3232
|
return {
|
|
3054
3233
|
schemaVersion: 1,
|
|
@@ -3110,32 +3289,32 @@ function formatAuditAdvisory(state, options = {}) {
|
|
|
3110
3289
|
}
|
|
3111
3290
|
|
|
3112
3291
|
// src/commands/gatePipeline.ts
|
|
3113
|
-
import { join as
|
|
3292
|
+
import { join as join17 } from "path";
|
|
3114
3293
|
|
|
3115
3294
|
// src/gate/profile.ts
|
|
3116
3295
|
import { execFileSync } from "child_process";
|
|
3117
3296
|
import {
|
|
3118
3297
|
closeSync as closeSync2,
|
|
3119
|
-
existsSync as
|
|
3298
|
+
existsSync as existsSync13,
|
|
3120
3299
|
mkdtempSync as mkdtempSync2,
|
|
3121
3300
|
openSync as openSync2,
|
|
3122
|
-
readFileSync as
|
|
3301
|
+
readFileSync as readFileSync14,
|
|
3123
3302
|
readdirSync as readdirSync6,
|
|
3124
3303
|
readSync,
|
|
3125
3304
|
rmSync as rmSync4,
|
|
3126
3305
|
statSync as statSync7
|
|
3127
3306
|
} from "fs";
|
|
3128
3307
|
import { tmpdir } from "os";
|
|
3129
|
-
import { basename as basename8, dirname as dirname6, extname as extname6, join as
|
|
3308
|
+
import { basename as basename8, dirname as dirname6, extname as extname6, join as join16, resolve as resolve4 } from "path";
|
|
3130
3309
|
|
|
3131
3310
|
// src/gate/mcpCapabilities.ts
|
|
3132
|
-
import { existsSync as
|
|
3133
|
-
import { extname as extname5, join as
|
|
3311
|
+
import { existsSync as existsSync12, readFileSync as readFileSync13 } from "fs";
|
|
3312
|
+
import { extname as extname5, join as join15 } from "path";
|
|
3134
3313
|
var SOURCE_EXT = /* @__PURE__ */ new Set([".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs"]);
|
|
3135
3314
|
var MAX_ITEMS_PER_CATEGORY = 200;
|
|
3136
3315
|
function readJsonSafe(file) {
|
|
3137
3316
|
try {
|
|
3138
|
-
return JSON.parse(
|
|
3317
|
+
return JSON.parse(readFileSync13(file, "utf-8"));
|
|
3139
3318
|
} catch {
|
|
3140
3319
|
return void 0;
|
|
3141
3320
|
}
|
|
@@ -3236,16 +3415,16 @@ var MCP_SDK_IMPORT_RE = /@modelcontextprotocol\/sdk/;
|
|
|
3236
3415
|
function extractMcpCapabilities(packageDir) {
|
|
3237
3416
|
const buckets = { tools: /* @__PURE__ */ new Set(), resources: /* @__PURE__ */ new Set(), prompts: /* @__PURE__ */ new Set() };
|
|
3238
3417
|
let hasPackageJsonMarker = false;
|
|
3239
|
-
const pkgPath =
|
|
3240
|
-
if (
|
|
3418
|
+
const pkgPath = join15(packageDir, "package.json");
|
|
3419
|
+
if (existsSync12(pkgPath)) {
|
|
3241
3420
|
const pkg2 = readJsonSafe(pkgPath);
|
|
3242
3421
|
if (pkg2 && typeof pkg2 === "object" && !Array.isArray(pkg2)) {
|
|
3243
3422
|
hasPackageJsonMarker = hasPackageJsonMcpMarkers(pkg2);
|
|
3244
3423
|
}
|
|
3245
3424
|
}
|
|
3246
3425
|
for (const manifestName of [".mcp.json", "mcp.json"]) {
|
|
3247
|
-
const manifestPath =
|
|
3248
|
-
if (!
|
|
3426
|
+
const manifestPath = join15(packageDir, manifestName);
|
|
3427
|
+
if (!existsSync12(manifestPath)) continue;
|
|
3249
3428
|
const parsed = readJsonSafe(manifestPath);
|
|
3250
3429
|
if (parsed !== void 0) {
|
|
3251
3430
|
collectDeclaredFromManifest(parsed, buckets);
|
|
@@ -3321,8 +3500,8 @@ var PYTHON_STDLIB = /* @__PURE__ */ new Set([
|
|
|
3321
3500
|
"__future__"
|
|
3322
3501
|
]);
|
|
3323
3502
|
function profileSkill(inputPath) {
|
|
3324
|
-
const p =
|
|
3325
|
-
if (!
|
|
3503
|
+
const p = resolve4(expandHome(inputPath));
|
|
3504
|
+
if (!existsSync13(p)) {
|
|
3326
3505
|
throw new Error(`path does not exist: ${p}`);
|
|
3327
3506
|
}
|
|
3328
3507
|
const stat = statSync7(p);
|
|
@@ -3352,7 +3531,7 @@ function asProfile(candidatePath, artifact) {
|
|
|
3352
3531
|
return asSkillProfile(profile, `at ${candidatePath}`);
|
|
3353
3532
|
}
|
|
3354
3533
|
function classifyInput(p) {
|
|
3355
|
-
if (
|
|
3534
|
+
if (existsSync13(join16(p, ".claude-plugin", "plugin.json"))) return "plugin";
|
|
3356
3535
|
if (detectArtifactType(p) === "mcp") return "mcp";
|
|
3357
3536
|
return "skill";
|
|
3358
3537
|
}
|
|
@@ -3360,10 +3539,10 @@ var MCP_SDK_DEP2 = "@modelcontextprotocol/sdk";
|
|
|
3360
3539
|
var MCP_KEYWORD2 = "mcp-server";
|
|
3361
3540
|
var MCP_BIN_PREFIX2 = /^mcp-/i;
|
|
3362
3541
|
function readPackageJsonSafe(root) {
|
|
3363
|
-
const pkgPath =
|
|
3364
|
-
if (!
|
|
3542
|
+
const pkgPath = join16(root, "package.json");
|
|
3543
|
+
if (!existsSync13(pkgPath)) return void 0;
|
|
3365
3544
|
try {
|
|
3366
|
-
return JSON.parse(
|
|
3545
|
+
return JSON.parse(readFileSync14(pkgPath, "utf-8"));
|
|
3367
3546
|
} catch {
|
|
3368
3547
|
return void 0;
|
|
3369
3548
|
}
|
|
@@ -3384,7 +3563,7 @@ function hasMcpPackageMarkers(pkg2) {
|
|
|
3384
3563
|
return false;
|
|
3385
3564
|
}
|
|
3386
3565
|
function detectArtifactType(root) {
|
|
3387
|
-
if (
|
|
3566
|
+
if (existsSync13(join16(root, ".mcp.json")) || existsSync13(join16(root, "mcp.json"))) return "mcp";
|
|
3388
3567
|
const pkg2 = readPackageJsonSafe(root);
|
|
3389
3568
|
if (pkg2 && hasMcpPackageMarkers(pkg2)) return "mcp";
|
|
3390
3569
|
return "skill";
|
|
@@ -3394,7 +3573,7 @@ function resolveSkillMd(inputPath) {
|
|
|
3394
3573
|
let tmpDir;
|
|
3395
3574
|
const initialStat = statSync7(currentPath);
|
|
3396
3575
|
if (extname6(currentPath) === ".skill" || initialStat.isFile() && isZipFile(currentPath)) {
|
|
3397
|
-
tmpDir = mkdtempSync2(
|
|
3576
|
+
tmpDir = mkdtempSync2(join16(tmpdir(), "forge_"));
|
|
3398
3577
|
extractZip(currentPath, tmpDir);
|
|
3399
3578
|
currentPath = tmpDir;
|
|
3400
3579
|
}
|
|
@@ -3403,8 +3582,8 @@ function resolveSkillMd(inputPath) {
|
|
|
3403
3582
|
return { skillMd: currentPath, root: dirname6(currentPath), tmpDir };
|
|
3404
3583
|
}
|
|
3405
3584
|
if (stat.isDirectory()) {
|
|
3406
|
-
const direct =
|
|
3407
|
-
if (
|
|
3585
|
+
const direct = join16(currentPath, "SKILL.md");
|
|
3586
|
+
if (existsSync13(direct)) {
|
|
3408
3587
|
return { skillMd: direct, root: currentPath, tmpDir };
|
|
3409
3588
|
}
|
|
3410
3589
|
const matches = findSkillMdOneLevelDown(currentPath);
|
|
@@ -3429,16 +3608,16 @@ function findSkillMdOneLevelDown(dir) {
|
|
|
3429
3608
|
const matches = /* @__PURE__ */ new Set();
|
|
3430
3609
|
for (const entry of readdirSync6(dir, { withFileTypes: true })) {
|
|
3431
3610
|
if (entry.isDirectory()) {
|
|
3432
|
-
const candidate =
|
|
3433
|
-
if (
|
|
3611
|
+
const candidate = join16(dir, entry.name, "SKILL.md");
|
|
3612
|
+
if (existsSync13(candidate)) matches.add(candidate);
|
|
3434
3613
|
}
|
|
3435
3614
|
}
|
|
3436
|
-
const skillsDir =
|
|
3615
|
+
const skillsDir = join16(dir, "skills");
|
|
3437
3616
|
if (isDir(skillsDir)) {
|
|
3438
3617
|
for (const entry of readdirSync6(skillsDir, { withFileTypes: true })) {
|
|
3439
3618
|
if (entry.isDirectory()) {
|
|
3440
|
-
const candidate =
|
|
3441
|
-
if (
|
|
3619
|
+
const candidate = join16(skillsDir, entry.name, "SKILL.md");
|
|
3620
|
+
if (existsSync13(candidate)) matches.add(candidate);
|
|
3442
3621
|
}
|
|
3443
3622
|
}
|
|
3444
3623
|
}
|
|
@@ -3549,9 +3728,9 @@ function detectLicense(root, fm) {
|
|
|
3549
3728
|
if (fm.license) return fm.license;
|
|
3550
3729
|
const names = ["LICENSE", "LICENSE.md", "LICENSE.txt", "COPYING", "license"];
|
|
3551
3730
|
for (const name of names) {
|
|
3552
|
-
const p =
|
|
3553
|
-
if (
|
|
3554
|
-
const head =
|
|
3731
|
+
const p = join16(root, name);
|
|
3732
|
+
if (existsSync13(p) && statSync7(p).isFile()) {
|
|
3733
|
+
const head = readFileSync14(p, "utf-8").slice(0, 2e3).toLowerCase();
|
|
3555
3734
|
for (const [hint, label2] of Object.entries(SPDX_HINTS)) {
|
|
3556
3735
|
if (head.includes(hint)) return label2;
|
|
3557
3736
|
}
|
|
@@ -3563,7 +3742,7 @@ function detectLicense(root, fm) {
|
|
|
3563
3742
|
function walkFiles(dir) {
|
|
3564
3743
|
const out = [];
|
|
3565
3744
|
for (const entry of readdirSync6(dir, { withFileTypes: true })) {
|
|
3566
|
-
const full =
|
|
3745
|
+
const full = join16(dir, entry.name);
|
|
3567
3746
|
if (entry.isDirectory()) {
|
|
3568
3747
|
out.push(...walkFiles(full));
|
|
3569
3748
|
} else if (entry.isFile()) {
|
|
@@ -3575,7 +3754,7 @@ function walkFiles(dir) {
|
|
|
3575
3754
|
function countResources(root) {
|
|
3576
3755
|
const resources = {};
|
|
3577
3756
|
for (const d of RESOURCE_DIRS) {
|
|
3578
|
-
const dp =
|
|
3757
|
+
const dp = join16(root, d);
|
|
3579
3758
|
if (isDir(dp)) {
|
|
3580
3759
|
resources[d] = walkFiles(dp).length;
|
|
3581
3760
|
}
|
|
@@ -3586,11 +3765,11 @@ function scanMcpAndImports(text, root) {
|
|
|
3586
3765
|
const mcpRefRe = /mcp__[a-zA-Z0-9_]+/g;
|
|
3587
3766
|
const mcpRefs = new Set(text.match(mcpRefRe) ?? []);
|
|
3588
3767
|
const imports = /* @__PURE__ */ new Set();
|
|
3589
|
-
const scriptsDir =
|
|
3768
|
+
const scriptsDir = join16(root, "scripts");
|
|
3590
3769
|
if (isDir(scriptsDir)) {
|
|
3591
3770
|
for (const f of walkFiles(scriptsDir)) {
|
|
3592
3771
|
if (!f.endsWith(".py")) continue;
|
|
3593
|
-
const ft =
|
|
3772
|
+
const ft = readFileSync14(f, "utf-8");
|
|
3594
3773
|
for (const m2 of ft.match(mcpRefRe) ?? []) mcpRefs.add(m2);
|
|
3595
3774
|
const importRe = /^\s*(?:import|from)\s+([a-zA-Z0-9_]+)/gm;
|
|
3596
3775
|
let m;
|
|
@@ -3607,7 +3786,7 @@ function scanMcpAndImports(text, root) {
|
|
|
3607
3786
|
function profileSkillDir(inputPath) {
|
|
3608
3787
|
const { skillMd, root, tmpDir } = resolveSkillMd(inputPath);
|
|
3609
3788
|
try {
|
|
3610
|
-
const text =
|
|
3789
|
+
const text = readFileSync14(skillMd, "utf-8");
|
|
3611
3790
|
const fm = parseFrontmatter(text);
|
|
3612
3791
|
const bodyLines = (text.match(/\n/g)?.length ?? 0) + 1;
|
|
3613
3792
|
const headers = Array.from(text.matchAll(/^#{1,3}\s+(.+)$/gm)).map((m) => m[1]);
|
|
@@ -3636,19 +3815,19 @@ function profileSkillDir(inputPath) {
|
|
|
3636
3815
|
}
|
|
3637
3816
|
}
|
|
3638
3817
|
function profilePlugin(p) {
|
|
3639
|
-
const manifestPath =
|
|
3818
|
+
const manifestPath = join16(p, ".claude-plugin", "plugin.json");
|
|
3640
3819
|
let manifest;
|
|
3641
3820
|
try {
|
|
3642
|
-
manifest = JSON.parse(
|
|
3821
|
+
manifest = JSON.parse(readFileSync14(manifestPath, "utf-8"));
|
|
3643
3822
|
} catch (err) {
|
|
3644
3823
|
throw new Error(`could not parse plugin.json under ${p}: ${err.message}`);
|
|
3645
3824
|
}
|
|
3646
|
-
const skillsDir =
|
|
3825
|
+
const skillsDir = join16(p, "skills");
|
|
3647
3826
|
let skills = [];
|
|
3648
3827
|
if (isDir(skillsDir)) {
|
|
3649
|
-
skills = readdirSync6(skillsDir, { withFileTypes: true }).filter((d) => d.isDirectory() &&
|
|
3828
|
+
skills = readdirSync6(skillsDir, { withFileTypes: true }).filter((d) => d.isDirectory() && existsSync13(join16(skillsDir, d.name, "SKILL.md"))).map((d) => d.name).sort();
|
|
3650
3829
|
}
|
|
3651
|
-
const commandsDir =
|
|
3830
|
+
const commandsDir = join16(p, "commands");
|
|
3652
3831
|
const commands = isDir(commandsDir) ? readdirSync6(commandsDir).filter((f) => f.endsWith(".md")).length : 0;
|
|
3653
3832
|
const mc = manifest.mcpServers ?? manifest.mcp_servers ?? {};
|
|
3654
3833
|
const mcpServers = typeof mc === "object" && mc !== null ? Object.keys(mc).sort() : [];
|
|
@@ -3666,11 +3845,11 @@ function profilePlugin(p) {
|
|
|
3666
3845
|
};
|
|
3667
3846
|
}
|
|
3668
3847
|
function profileMcp(p) {
|
|
3669
|
-
const explicitConfig =
|
|
3848
|
+
const explicitConfig = existsSync13(join16(p, ".mcp.json")) ? join16(p, ".mcp.json") : existsSync13(join16(p, "mcp.json")) ? join16(p, "mcp.json") : void 0;
|
|
3670
3849
|
if (explicitConfig) {
|
|
3671
3850
|
let cfg;
|
|
3672
3851
|
try {
|
|
3673
|
-
cfg = JSON.parse(
|
|
3852
|
+
cfg = JSON.parse(readFileSync14(explicitConfig, "utf-8"));
|
|
3674
3853
|
} catch (err) {
|
|
3675
3854
|
throw new Error(`could not parse MCP config ${explicitConfig}: ${err.message}`);
|
|
3676
3855
|
}
|
|
@@ -3686,10 +3865,10 @@ function profileMcp(p) {
|
|
|
3686
3865
|
capabilities: extractMcpCapabilities(p)
|
|
3687
3866
|
};
|
|
3688
3867
|
}
|
|
3689
|
-
const pkgPath =
|
|
3868
|
+
const pkgPath = join16(p, "package.json");
|
|
3690
3869
|
let pkg2;
|
|
3691
3870
|
try {
|
|
3692
|
-
pkg2 = JSON.parse(
|
|
3871
|
+
pkg2 = JSON.parse(readFileSync14(pkgPath, "utf-8"));
|
|
3693
3872
|
} catch (err) {
|
|
3694
3873
|
throw new Error(`could not parse package.json ${pkgPath}: ${err.message}`);
|
|
3695
3874
|
}
|
|
@@ -3706,14 +3885,14 @@ function profileMcp(p) {
|
|
|
3706
3885
|
}
|
|
3707
3886
|
|
|
3708
3887
|
// src/gate/mcpOverlap.ts
|
|
3709
|
-
import { existsSync as
|
|
3710
|
-
import { resolve as
|
|
3888
|
+
import { existsSync as existsSync14, readFileSync as readFileSync15 } from "fs";
|
|
3889
|
+
import { resolve as resolve5 } from "path";
|
|
3711
3890
|
function readTargetServers(targetPath) {
|
|
3712
|
-
const path =
|
|
3713
|
-
if (!
|
|
3891
|
+
const path = resolve5(expandHome(targetPath));
|
|
3892
|
+
if (!existsSync14(path)) return [];
|
|
3714
3893
|
let cfg;
|
|
3715
3894
|
try {
|
|
3716
|
-
cfg = JSON.parse(
|
|
3895
|
+
cfg = JSON.parse(readFileSync15(path, "utf-8"));
|
|
3717
3896
|
} catch {
|
|
3718
3897
|
return [];
|
|
3719
3898
|
}
|
|
@@ -3769,7 +3948,7 @@ function score2(candidate, target) {
|
|
|
3769
3948
|
function scanMcpOverlap(candidate, mcpTargetPaths) {
|
|
3770
3949
|
const ranking = [];
|
|
3771
3950
|
for (const targetPath of mcpTargetPaths) {
|
|
3772
|
-
const resolvedPath =
|
|
3951
|
+
const resolvedPath = resolve5(expandHome(targetPath));
|
|
3773
3952
|
for (const target of readTargetServers(targetPath)) {
|
|
3774
3953
|
ranking.push({ skill: target.name, path: resolvedPath, score: score2(candidate, target) });
|
|
3775
3954
|
}
|
|
@@ -3786,13 +3965,13 @@ function scanMcpOverlap(candidate, mcpTargetPaths) {
|
|
|
3786
3965
|
}
|
|
3787
3966
|
|
|
3788
3967
|
// src/gate/skillMap.ts
|
|
3789
|
-
import { readFileSync as
|
|
3790
|
-
import { resolve as
|
|
3968
|
+
import { readFileSync as readFileSync16 } from "fs";
|
|
3969
|
+
import { resolve as resolve6 } from "path";
|
|
3791
3970
|
function loadSkillMap(path) {
|
|
3792
|
-
const resolved =
|
|
3971
|
+
const resolved = resolve6(expandHome(path));
|
|
3793
3972
|
let text;
|
|
3794
3973
|
try {
|
|
3795
|
-
text =
|
|
3974
|
+
text = readFileSync16(resolved, "utf-8");
|
|
3796
3975
|
} catch (err) {
|
|
3797
3976
|
throw new Error(`could not read skill map at ${resolved}: ${err.message}`);
|
|
3798
3977
|
}
|
|
@@ -4092,7 +4271,7 @@ function resolveMcpCandidatePath(entry) {
|
|
|
4092
4271
|
children = readdirSync7(entry.quarantinePath, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => d.name);
|
|
4093
4272
|
} catch {
|
|
4094
4273
|
}
|
|
4095
|
-
return children.length === 1 ?
|
|
4274
|
+
return children.length === 1 ? join17(entry.quarantinePath, children[0]) : entry.quarantinePath;
|
|
4096
4275
|
}
|
|
4097
4276
|
async function runGate(candidatePath, skillsRoot, {
|
|
4098
4277
|
proFeatureLabel = "overlap analysis",
|
|
@@ -4141,17 +4320,17 @@ async function runGate(candidatePath, skillsRoot, {
|
|
|
4141
4320
|
|
|
4142
4321
|
// src/queue.ts
|
|
4143
4322
|
import { randomUUID as randomUUID3 } from "crypto";
|
|
4144
|
-
import { existsSync as
|
|
4145
|
-
import { join as
|
|
4323
|
+
import { existsSync as existsSync15, mkdirSync as mkdirSync6, readFileSync as readFileSync17, renameSync as renameSync4, rmSync as rmSync5, writeFileSync as writeFileSync6 } from "fs";
|
|
4324
|
+
import { join as join18 } from "path";
|
|
4146
4325
|
function getQueuePath() {
|
|
4147
|
-
return
|
|
4326
|
+
return join18(getHomeDir(), "queue.json");
|
|
4148
4327
|
}
|
|
4149
4328
|
function readQueue() {
|
|
4150
4329
|
const path = getQueuePath();
|
|
4151
|
-
if (!
|
|
4330
|
+
if (!existsSync15(path)) {
|
|
4152
4331
|
return { version: 1, entries: [] };
|
|
4153
4332
|
}
|
|
4154
|
-
const raw =
|
|
4333
|
+
const raw = readFileSync17(path, "utf-8");
|
|
4155
4334
|
const parsed = JSON.parse(raw);
|
|
4156
4335
|
return { version: 1, entries: parsed.entries ?? [] };
|
|
4157
4336
|
}
|
|
@@ -4159,7 +4338,7 @@ function writeQueue(queue2) {
|
|
|
4159
4338
|
const home = getHomeDir();
|
|
4160
4339
|
mkdirSync6(home, { recursive: true });
|
|
4161
4340
|
const finalPath = getQueuePath();
|
|
4162
|
-
const tempPath =
|
|
4341
|
+
const tempPath = join18(home, `.queue.json.tmp-${randomUUID3()}`);
|
|
4163
4342
|
try {
|
|
4164
4343
|
writeFileSync6(tempPath, JSON.stringify(queue2, null, 2) + "\n", "utf-8");
|
|
4165
4344
|
renameSync4(tempPath, finalPath);
|
|
@@ -4208,19 +4387,19 @@ function generateId(existingIds) {
|
|
|
4208
4387
|
|
|
4209
4388
|
// src/handoff.ts
|
|
4210
4389
|
import { spawnSync } from "child_process";
|
|
4211
|
-
import { existsSync as
|
|
4212
|
-
import { dirname as dirname7, isAbsolute as
|
|
4390
|
+
import { existsSync as existsSync16 } from "fs";
|
|
4391
|
+
import { dirname as dirname7, isAbsolute as isAbsolute3, join as join19, win32 } from "path";
|
|
4213
4392
|
import { fileURLToPath } from "url";
|
|
4214
4393
|
var KNOWN_AGENT_BINARIES = ["claude", "codex", "cursor-agent", "windsurf", "opencode", "gemini"];
|
|
4215
4394
|
var HANDOFF_INSTRUCTION = "Read {prompt} and follow its instructions for the skill installed at {path}";
|
|
4216
4395
|
function resolvePromptPath(assetName = "ingest-prompt.md") {
|
|
4217
|
-
if (assetName.length === 0 || assetName.includes("\0") ||
|
|
4396
|
+
if (assetName.length === 0 || assetName.includes("\0") || isAbsolute3(assetName) || win32.isAbsolute(assetName) || assetName.split(/[\\/]/).includes("..")) {
|
|
4218
4397
|
throw new UserError(`Invalid handoff prompt asset path: ${assetName}`);
|
|
4219
4398
|
}
|
|
4220
4399
|
const here = dirname7(fileURLToPath(import.meta.url));
|
|
4221
|
-
const builtCandidate =
|
|
4222
|
-
if (
|
|
4223
|
-
return
|
|
4400
|
+
const builtCandidate = join19(here, assetName);
|
|
4401
|
+
if (existsSync16(builtCandidate)) return builtCandidate;
|
|
4402
|
+
return join19(here, "..", "assets", assetName);
|
|
4224
4403
|
}
|
|
4225
4404
|
function defaultIsOnPath(bin) {
|
|
4226
4405
|
const finder = process.platform === "win32" ? "where" : "which";
|
|
@@ -4282,15 +4461,15 @@ function runHandoff(config, installedPath, options = {}) {
|
|
|
4282
4461
|
}
|
|
4283
4462
|
|
|
4284
4463
|
// src/commands/init.ts
|
|
4285
|
-
import { existsSync as
|
|
4464
|
+
import { existsSync as existsSync19 } from "fs";
|
|
4286
4465
|
import { homedir as homedir5 } from "os";
|
|
4287
|
-
import { join as
|
|
4466
|
+
import { join as join25 } from "path";
|
|
4288
4467
|
import { createInterface as createInterface3 } from "readline/promises";
|
|
4289
4468
|
|
|
4290
4469
|
// src/agents.ts
|
|
4291
4470
|
import { statSync as statSync8 } from "fs";
|
|
4292
4471
|
import { homedir as homedir3 } from "os";
|
|
4293
|
-
import { join as
|
|
4472
|
+
import { join as join20, resolve as resolve7 } from "path";
|
|
4294
4473
|
function isFile(p) {
|
|
4295
4474
|
try {
|
|
4296
4475
|
return statSync8(p).isFile();
|
|
@@ -4299,12 +4478,12 @@ function isFile(p) {
|
|
|
4299
4478
|
}
|
|
4300
4479
|
}
|
|
4301
4480
|
function projectPath(root, cwd) {
|
|
4302
|
-
return root ?
|
|
4481
|
+
return root ? resolve7(cwd, root) : void 0;
|
|
4303
4482
|
}
|
|
4304
4483
|
function globalPath(root, home) {
|
|
4305
4484
|
if (!root) return void 0;
|
|
4306
4485
|
if (root === "~") return home;
|
|
4307
|
-
if (root.startsWith("~/")) return
|
|
4486
|
+
if (root.startsWith("~/")) return join20(home, root.slice(2));
|
|
4308
4487
|
return root;
|
|
4309
4488
|
}
|
|
4310
4489
|
function makeAgent(spec) {
|
|
@@ -4321,30 +4500,30 @@ function makeAgent(spec) {
|
|
|
4321
4500
|
};
|
|
4322
4501
|
return agent;
|
|
4323
4502
|
}
|
|
4324
|
-
function resolveAgentRoot(agent,
|
|
4325
|
-
return
|
|
4503
|
+
function resolveAgentRoot(agent, scope2, cwd = process.cwd(), home = homedir3()) {
|
|
4504
|
+
return scope2 === "project" ? projectPath(agent.projectRoot, cwd) : globalPath(agent.globalRoot, home);
|
|
4326
4505
|
}
|
|
4327
4506
|
function agentCliPresence(agent, pathEnv = process.env.PATH) {
|
|
4328
4507
|
if (!agent.cliBinaries || agent.cliBinaries.length === 0) return "unknown";
|
|
4329
4508
|
return agent.cliBinaries.some((bin) => resolveBinaryOnPath(bin, pathEnv) !== void 0) ? "installed" : "absent";
|
|
4330
4509
|
}
|
|
4331
4510
|
function detectAgentRoot(agent, cwd = process.cwd(), home = homedir3()) {
|
|
4332
|
-
for (const
|
|
4333
|
-
const root = resolveAgentRoot(agent,
|
|
4334
|
-
if (root && isDir(root)) return { scope, root };
|
|
4511
|
+
for (const scope2 of ["project", "global"]) {
|
|
4512
|
+
const root = resolveAgentRoot(agent, scope2, cwd, home);
|
|
4513
|
+
if (root && isDir(root)) return { scope: scope2, root };
|
|
4335
4514
|
}
|
|
4336
4515
|
return void 0;
|
|
4337
4516
|
}
|
|
4338
|
-
function resolveMcpConfigPath(agent,
|
|
4517
|
+
function resolveMcpConfigPath(agent, scope2, cwd = process.cwd(), home = homedir3()) {
|
|
4339
4518
|
if (!agent.mcpConfig) return void 0;
|
|
4340
|
-
return
|
|
4519
|
+
return scope2 === "project" ? projectPath(agent.mcpConfig.project, cwd) : globalPath(agent.mcpConfig.global, home);
|
|
4341
4520
|
}
|
|
4342
4521
|
function detectMcpConfigFiles(agent, cwd = process.cwd(), home = homedir3()) {
|
|
4343
4522
|
if (!agent.mcpConfig) return [];
|
|
4344
4523
|
const found = [];
|
|
4345
|
-
for (const
|
|
4346
|
-
const path = resolveMcpConfigPath(agent,
|
|
4347
|
-
if (path && isFile(path)) found.push({ scope, path });
|
|
4524
|
+
for (const scope2 of ["project", "global"]) {
|
|
4525
|
+
const path = resolveMcpConfigPath(agent, scope2, cwd, home);
|
|
4526
|
+
if (path && isFile(path)) found.push({ scope: scope2, path });
|
|
4348
4527
|
}
|
|
4349
4528
|
return found;
|
|
4350
4529
|
}
|
|
@@ -4534,12 +4713,12 @@ var AGENTS = [
|
|
|
4534
4713
|
].map(makeAgent);
|
|
4535
4714
|
|
|
4536
4715
|
// src/gitPreflight.ts
|
|
4537
|
-
import { relative as
|
|
4716
|
+
import { relative as relative3, sep as sep4 } from "path";
|
|
4538
4717
|
function splitNulTerminated(output) {
|
|
4539
4718
|
return output.split("\0").filter((s) => s.length > 0);
|
|
4540
4719
|
}
|
|
4541
4720
|
function toPosixPath(p) {
|
|
4542
|
-
return p.split(
|
|
4721
|
+
return p.split(sep4).join("/");
|
|
4543
4722
|
}
|
|
4544
4723
|
var missingResult = (root) => ({
|
|
4545
4724
|
root,
|
|
@@ -4569,7 +4748,7 @@ async function evaluateGitPreflight(root, deps = {}) {
|
|
|
4569
4748
|
if (toplevel.code !== 0) return notInRepoResult(root);
|
|
4570
4749
|
const repoRoot = toplevel.stdout.trim();
|
|
4571
4750
|
if (!repoRoot) return notInRepoResult(root);
|
|
4572
|
-
const rel = toPosixPath(
|
|
4751
|
+
const rel = toPosixPath(relative3(repoRoot, root));
|
|
4573
4752
|
const pathspec = rel === "" ? "." : rel;
|
|
4574
4753
|
const tracked = await exec("git", ["--literal-pathspecs", "-C", repoRoot, "ls-files", "-z", "--", pathspec], { cwd: repoRoot });
|
|
4575
4754
|
const untracked = await exec(
|
|
@@ -4640,7 +4819,7 @@ function untrackedAddCommitCommands(root, repoRoot) {
|
|
|
4640
4819
|
];
|
|
4641
4820
|
}
|
|
4642
4821
|
function ignoredUnignoreSuggestions(root, repoRoot) {
|
|
4643
|
-
const rel = toPosixPath(
|
|
4822
|
+
const rel = toPosixPath(relative3(repoRoot, root));
|
|
4644
4823
|
return [`!${rel}/`, `!${rel}/**`];
|
|
4645
4824
|
}
|
|
4646
4825
|
async function runGitCommands(commands, deps = {}) {
|
|
@@ -4663,9 +4842,52 @@ function describeGitPreflight(result) {
|
|
|
4663
4842
|
return parts.join(", ");
|
|
4664
4843
|
}
|
|
4665
4844
|
|
|
4845
|
+
// src/pluginInventory.ts
|
|
4846
|
+
import { readFileSync as readFileSync18, realpathSync as realpathSync4, statSync as statSync9 } from "fs";
|
|
4847
|
+
import { isAbsolute as isAbsolute4, join as join21, relative as relative4, sep as sep5 } from "path";
|
|
4848
|
+
function readPluginInventory(path) {
|
|
4849
|
+
const data = JSON.parse(readFileSync18(path, "utf8"));
|
|
4850
|
+
if (data.schemaVersion !== 1 || data.kind !== "skill-forge-host-inventory" || !["claude", "codex"].includes(data.host) || !Array.isArray(data.plugins) || typeof data.complete !== "boolean" || typeof data.generatedAt !== "string" || !Number.isFinite(Date.parse(data.generatedAt))) throw new UserError("Invalid host inventory export.");
|
|
4851
|
+
const roots = [];
|
|
4852
|
+
const notices = ["Imported host inventory is static evidence; runtime discovery/invocation is unverified."];
|
|
4853
|
+
if (Array.isArray(data.notices)) notices.push(...data.notices.filter((n) => typeof n === "string"));
|
|
4854
|
+
const ids = /* @__PURE__ */ new Set();
|
|
4855
|
+
let complete = data.complete;
|
|
4856
|
+
for (const row of data.plugins) {
|
|
4857
|
+
if (!row || row.host !== data.host || typeof row.pluginId !== "string" || !/^[\w.-]+@[\w.-]+$/.test(row.pluginId) || ids.has(row.pluginId)) {
|
|
4858
|
+
throw new UserError("Invalid or duplicate host inventory plugin identity.");
|
|
4859
|
+
}
|
|
4860
|
+
ids.add(row.pluginId);
|
|
4861
|
+
if (row.enabled === false && row.status === "disabled") continue;
|
|
4862
|
+
if (row.enabled !== true || row.status !== "installed") {
|
|
4863
|
+
complete = false;
|
|
4864
|
+
notices.push(`${row.pluginId}: installed skills unavailable (${typeof row.reason === "string" ? row.reason : "unknown state"}).`);
|
|
4865
|
+
continue;
|
|
4866
|
+
}
|
|
4867
|
+
if (typeof row.installPath !== "string" || !isAbsolute4(row.installPath) || typeof row.version !== "string" || !Array.isArray(row.skillDirs) || row.skillDirs.some((p) => typeof p !== "string" || !isAbsolute4(p))) {
|
|
4868
|
+
throw new UserError("Invalid host inventory install paths/version.");
|
|
4869
|
+
}
|
|
4870
|
+
const installPath = realpathSync4(row.installPath);
|
|
4871
|
+
const skillDirs = [...new Set(row.skillDirs.map((dir) => {
|
|
4872
|
+
const real = realpathSync4(dir);
|
|
4873
|
+
for (const target of [real, realpathSync4(join21(real, "SKILL.md"))]) {
|
|
4874
|
+
const rel = relative4(installPath, target);
|
|
4875
|
+
if (rel === ".." || rel.startsWith(".." + sep5) || isAbsolute4(rel)) throw new UserError("Imported skill escapes its plugin install.");
|
|
4876
|
+
}
|
|
4877
|
+
if (!statSync9(join21(real, "SKILL.md")).isFile()) throw new UserError("Imported SKILL.md is not a file.");
|
|
4878
|
+
return real;
|
|
4879
|
+
}))].sort();
|
|
4880
|
+
const [plugin, marketplace] = row.pluginId.split("@");
|
|
4881
|
+
roots.push({ pluginId: row.pluginId, plugin, marketplace, version: row.version, installPath, skillDirs });
|
|
4882
|
+
}
|
|
4883
|
+
if (!complete) notices.push("Host inventory coverage is incomplete; missing plugins are not evidence of removal.");
|
|
4884
|
+
if (Date.now() - Date.parse(data.generatedAt) > 24 * 60 * 60 * 1e3) notices.push("Host inventory export is older than 24 hours; refresh it before making retention decisions.");
|
|
4885
|
+
return { host: data.host, roots, notices, complete, generatedAt: data.generatedAt };
|
|
4886
|
+
}
|
|
4887
|
+
|
|
4666
4888
|
// src/commands/audit.ts
|
|
4667
|
-
import { existsSync as
|
|
4668
|
-
import { join as
|
|
4889
|
+
import { existsSync as existsSync18 } from "fs";
|
|
4890
|
+
import { join as join24, resolve as resolvePath5 } from "path";
|
|
4669
4891
|
import { createInterface as createInterface2 } from "readline/promises";
|
|
4670
4892
|
|
|
4671
4893
|
// src/decision.ts
|
|
@@ -4698,13 +4920,13 @@ ${prompt}`);
|
|
|
4698
4920
|
}
|
|
4699
4921
|
|
|
4700
4922
|
// src/pluginRoots.ts
|
|
4701
|
-
import { existsSync as
|
|
4923
|
+
import { existsSync as existsSync17, readFileSync as readFileSync19, readdirSync as readdirSync8, realpathSync as realpathSync5 } from "fs";
|
|
4702
4924
|
import { homedir as homedir4 } from "os";
|
|
4703
|
-
import { isAbsolute as
|
|
4925
|
+
import { isAbsolute as isAbsolute5, join as join22, relative as relative5, resolve as resolvePath3, sep as sep6 } from "path";
|
|
4704
4926
|
function readJsonFileSafe(path) {
|
|
4705
4927
|
let raw;
|
|
4706
4928
|
try {
|
|
4707
|
-
raw =
|
|
4929
|
+
raw = readFileSync19(path, "utf-8");
|
|
4708
4930
|
} catch (err) {
|
|
4709
4931
|
return { problem: err.code === "ENOENT" ? "missing" : "unreadable" };
|
|
4710
4932
|
}
|
|
@@ -4740,12 +4962,12 @@ function extractRegistry(data) {
|
|
|
4740
4962
|
const entries = [];
|
|
4741
4963
|
for (const raw of entriesRaw) {
|
|
4742
4964
|
const rec = typeof raw === "object" && raw !== null ? raw : {};
|
|
4743
|
-
const
|
|
4965
|
+
const scope2 = rec.scope;
|
|
4744
4966
|
const installPath = rec.installPath;
|
|
4745
4967
|
const version = rec.version;
|
|
4746
|
-
if ((
|
|
4968
|
+
if ((scope2 === "user" || scope2 === "project" || scope2 === "local") && typeof installPath === "string" && typeof version === "string") {
|
|
4747
4969
|
entries.push({
|
|
4748
|
-
scope,
|
|
4970
|
+
scope: scope2,
|
|
4749
4971
|
projectPath: typeof rec.projectPath === "string" ? rec.projectPath : void 0,
|
|
4750
4972
|
installPath,
|
|
4751
4973
|
version
|
|
@@ -4765,7 +4987,7 @@ function splitPluginId(id) {
|
|
|
4765
4987
|
}
|
|
4766
4988
|
function safeRealpath(p) {
|
|
4767
4989
|
try {
|
|
4768
|
-
return
|
|
4990
|
+
return realpathSync5(p);
|
|
4769
4991
|
} catch {
|
|
4770
4992
|
return void 0;
|
|
4771
4993
|
}
|
|
@@ -4800,8 +5022,8 @@ function selectInstallEntry(entries, cwd) {
|
|
|
4800
5022
|
}
|
|
4801
5023
|
function isInside2(child, root) {
|
|
4802
5024
|
if (child === root) return true;
|
|
4803
|
-
const rel =
|
|
4804
|
-
return rel !== "" && !rel.startsWith(".." +
|
|
5025
|
+
const rel = relative5(root, child);
|
|
5026
|
+
return rel !== "" && !rel.startsWith(".." + sep6) && rel !== ".." && !isAbsolute5(rel);
|
|
4805
5027
|
}
|
|
4806
5028
|
var DEFAULT_SKILL_LAYOUT = { paths: ["./skills/"], explicit: false };
|
|
4807
5029
|
function normalizeDeclaredSkillsField(manifest, pluginId, notices) {
|
|
@@ -4817,7 +5039,7 @@ function resolveDeclaredPath(installPath, realInstall, declaredPath, explicit, p
|
|
|
4817
5039
|
const joined = resolvePath3(installPath, declaredPath);
|
|
4818
5040
|
let real;
|
|
4819
5041
|
try {
|
|
4820
|
-
real =
|
|
5042
|
+
real = realpathSync5(joined);
|
|
4821
5043
|
} catch {
|
|
4822
5044
|
if (explicit) notices.push(`plugin ${pluginId}: declared skill path does not exist, skipping: ${declaredPath}`);
|
|
4823
5045
|
return [];
|
|
@@ -4826,7 +5048,7 @@ function resolveDeclaredPath(installPath, realInstall, declaredPath, explicit, p
|
|
|
4826
5048
|
notices.push(`plugin ${pluginId}: declared skill path escapes the install root, skipping: ${declaredPath}`);
|
|
4827
5049
|
return [];
|
|
4828
5050
|
}
|
|
4829
|
-
if (
|
|
5051
|
+
if (existsSync17(join22(real, "SKILL.md"))) return [real];
|
|
4830
5052
|
let entries;
|
|
4831
5053
|
try {
|
|
4832
5054
|
entries = readdirSync8(real, { withFileTypes: true });
|
|
@@ -4835,24 +5057,24 @@ function resolveDeclaredPath(installPath, realInstall, declaredPath, explicit, p
|
|
|
4835
5057
|
}
|
|
4836
5058
|
const found = [];
|
|
4837
5059
|
for (const entry of entries) {
|
|
4838
|
-
let childPath =
|
|
5060
|
+
let childPath = join22(real, entry.name);
|
|
4839
5061
|
if (entry.isSymbolicLink()) {
|
|
4840
5062
|
const target = safeRealpath(childPath);
|
|
4841
5063
|
if (!target || !isInside2(target, realInstall)) {
|
|
4842
|
-
notices.push(`plugin ${pluginId}: skill dir symlink escapes the install root, skipping: ${
|
|
5064
|
+
notices.push(`plugin ${pluginId}: skill dir symlink escapes the install root, skipping: ${join22(declaredPath, entry.name)}`);
|
|
4843
5065
|
continue;
|
|
4844
5066
|
}
|
|
4845
5067
|
childPath = target;
|
|
4846
5068
|
} else if (!entry.isDirectory()) {
|
|
4847
5069
|
continue;
|
|
4848
5070
|
}
|
|
4849
|
-
if (
|
|
5071
|
+
if (existsSync17(join22(childPath, "SKILL.md"))) found.push(childPath);
|
|
4850
5072
|
}
|
|
4851
5073
|
return found;
|
|
4852
5074
|
}
|
|
4853
5075
|
function resolveSkillDirs(installPath, pluginId) {
|
|
4854
5076
|
const notices = [];
|
|
4855
|
-
const manifestPath =
|
|
5077
|
+
const manifestPath = join22(installPath, ".claude-plugin", "plugin.json");
|
|
4856
5078
|
const manifestResult = readJsonFileSafe(manifestPath);
|
|
4857
5079
|
if (manifestResult.problem) {
|
|
4858
5080
|
notices.push(`plugin ${pluginId}: ${manifestResult.problem} manifest at ${manifestPath} \u2014 using default skill layout`);
|
|
@@ -4870,9 +5092,9 @@ function resolveSkillDirs(installPath, pluginId) {
|
|
|
4870
5092
|
function discoverClaudePluginRoots(options = {}) {
|
|
4871
5093
|
const homeDir = options.homeDir ?? homedir4();
|
|
4872
5094
|
const cwd = options.cwd ?? process.cwd();
|
|
4873
|
-
const settingsPath = options.settingsPath ??
|
|
4874
|
-
const localSettingsPath = options.localSettingsPath ??
|
|
4875
|
-
const installedPluginsPath = options.installedPluginsPath ??
|
|
5095
|
+
const settingsPath = options.settingsPath ?? join22(homeDir, ".claude", "settings.json");
|
|
5096
|
+
const localSettingsPath = options.localSettingsPath ?? join22(cwd, ".claude", "settings.local.json");
|
|
5097
|
+
const installedPluginsPath = options.installedPluginsPath ?? join22(homeDir, ".claude", "plugins", "installed_plugins.json");
|
|
4876
5098
|
const notices = [];
|
|
4877
5099
|
const settingsResult = readJsonFileSafe(settingsPath);
|
|
4878
5100
|
if (settingsResult.problem) {
|
|
@@ -4886,7 +5108,7 @@ function discoverClaudePluginRoots(options = {}) {
|
|
|
4886
5108
|
}
|
|
4887
5109
|
const globalEnabled = extractEnabledMap(settingsResult.data);
|
|
4888
5110
|
let localEnabled = {};
|
|
4889
|
-
if (
|
|
5111
|
+
if (existsSync17(localSettingsPath)) {
|
|
4890
5112
|
const localResult = readJsonFileSafe(localSettingsPath);
|
|
4891
5113
|
if (localResult.problem) {
|
|
4892
5114
|
notices.push(`local settings ${describeProblem(localResult.problem, localSettingsPath)} \u2014 ignoring local plugin overrides`);
|
|
@@ -4919,10 +5141,10 @@ function discoverClaudePluginRoots(options = {}) {
|
|
|
4919
5141
|
}
|
|
4920
5142
|
|
|
4921
5143
|
// src/usageSnapshot.ts
|
|
4922
|
-
import { readFileSync as
|
|
4923
|
-
import { extname as extname7, join as
|
|
5144
|
+
import { readFileSync as readFileSync20, readdirSync as readdirSync9 } from "fs";
|
|
5145
|
+
import { extname as extname7, join as join23, resolve as resolvePath4 } from "path";
|
|
4924
5146
|
function isValidCount(value) {
|
|
4925
|
-
return typeof value === "number" && Number.
|
|
5147
|
+
return typeof value === "number" && Number.isSafeInteger(value) && value >= 0;
|
|
4926
5148
|
}
|
|
4927
5149
|
function toSnapshot(parsed, sourcePath) {
|
|
4928
5150
|
return {
|
|
@@ -4949,24 +5171,28 @@ function parseSnapshotFile(raw) {
|
|
|
4949
5171
|
if (!report) return { error: 'usage snapshot has no "report" object' };
|
|
4950
5172
|
const generatedAt = typeof report.generated_at === "string" ? report.generated_at : void 0;
|
|
4951
5173
|
const windowDays = typeof report.window_days === "number" ? report.window_days : void 0;
|
|
4952
|
-
const uniqueSkillsUsed =
|
|
5174
|
+
const uniqueSkillsUsed = report.unique_skills_used;
|
|
4953
5175
|
const skillTotalsRaw = report.skill_totals;
|
|
4954
5176
|
const skillTotals = skillTotalsRaw && typeof skillTotalsRaw === "object" && !Array.isArray(skillTotalsRaw) ? skillTotalsRaw : void 0;
|
|
4955
|
-
|
|
5177
|
+
let warning = skillTotalsRaw !== void 0 && skillTotals === void 0 ? 'usage snapshot: "skill_totals" is not an object \u2014 ignoring it' : void 0;
|
|
4956
5178
|
const topSkills = Array.isArray(report.top_skills) ? report.top_skills : void 0;
|
|
4957
5179
|
const usage = /* @__PURE__ */ new Map();
|
|
4958
|
-
|
|
4959
|
-
|
|
4960
|
-
|
|
4961
|
-
|
|
4962
|
-
|
|
4963
|
-
|
|
4964
|
-
if (Array.isArray(row) && row.length === 2 && typeof row[0] === "string" && isValidCount(row[1])) {
|
|
4965
|
-
usage.set(row[0], row[1]);
|
|
4966
|
-
}
|
|
5180
|
+
let invalidRows = false;
|
|
5181
|
+
const rows = skillTotals ? Object.entries(skillTotals) : topSkills ?? [];
|
|
5182
|
+
for (const row of rows) {
|
|
5183
|
+
if (!Array.isArray(row) || row.length !== 2 || typeof row[0] !== "string" || !row[0].trim() || !isValidCount(row[1]) || usage.has(row[0])) {
|
|
5184
|
+
invalidRows = true;
|
|
5185
|
+
continue;
|
|
4967
5186
|
}
|
|
5187
|
+
usage.set(row[0], row[1]);
|
|
5188
|
+
}
|
|
5189
|
+
const observed = [...usage.values()].filter((count) => count > 0).length;
|
|
5190
|
+
const validUniqueCount = isValidCount(uniqueSkillsUsed);
|
|
5191
|
+
const inconsistentTotal = uniqueSkillsUsed !== void 0 && (!validUniqueCount || (skillTotals ? observed !== uniqueSkillsUsed : observed > uniqueSkillsUsed));
|
|
5192
|
+
const exhaustive = !invalidRows && !inconsistentTotal && (Boolean(skillTotals) || topSkills !== void 0 && validUniqueCount && observed === uniqueSkillsUsed);
|
|
5193
|
+
if (invalidRows || inconsistentTotal) {
|
|
5194
|
+
warning = [warning, "usage snapshot contains invalid, duplicate, or inconsistent counts \u2014 coverage is not exhaustive"].filter(Boolean).join("; ");
|
|
4968
5195
|
}
|
|
4969
|
-
const exhaustive = Boolean(skillTotals) || topSkills !== void 0 && uniqueSkillsUsed !== void 0 && topSkills.length === uniqueSkillsUsed;
|
|
4970
5196
|
return { usage, exhaustive, windowDays, generatedAt, warning };
|
|
4971
5197
|
}
|
|
4972
5198
|
function pickLatest(candidates) {
|
|
@@ -4995,10 +5221,10 @@ function loadUsageSnapshot(pathOrDir, opts = {}) {
|
|
|
4995
5221
|
}
|
|
4996
5222
|
const candidates = [];
|
|
4997
5223
|
for (const name of entries) {
|
|
4998
|
-
const filePath =
|
|
5224
|
+
const filePath = join23(resolved, name);
|
|
4999
5225
|
let raw2;
|
|
5000
5226
|
try {
|
|
5001
|
-
raw2 =
|
|
5227
|
+
raw2 = readFileSync20(filePath, "utf-8");
|
|
5002
5228
|
} catch {
|
|
5003
5229
|
continue;
|
|
5004
5230
|
}
|
|
@@ -5018,7 +5244,7 @@ function loadUsageSnapshot(pathOrDir, opts = {}) {
|
|
|
5018
5244
|
}
|
|
5019
5245
|
let raw;
|
|
5020
5246
|
try {
|
|
5021
|
-
raw =
|
|
5247
|
+
raw = readFileSync20(resolved, "utf-8");
|
|
5022
5248
|
} catch (err) {
|
|
5023
5249
|
return emptySnapshot(resolved, `could not read usage snapshot (${err.message})`);
|
|
5024
5250
|
}
|
|
@@ -5033,18 +5259,21 @@ function resolvePluginAndUsageContext(config, options, deps = {}) {
|
|
|
5033
5259
|
if (options.usageWindow !== void 0 && (!Number.isInteger(options.usageWindow) || options.usageWindow < 1)) {
|
|
5034
5260
|
throw new UserError(`--usage-window must be a positive integer (got "${options.usageWindow}")`);
|
|
5035
5261
|
}
|
|
5036
|
-
|
|
5037
|
-
|
|
5038
|
-
const
|
|
5262
|
+
if (options.pluginInventory && options.claudePlugins === true) throw new UserError("--plugin-inventory and --claude-plugins cannot be combined. Audit each host separately.");
|
|
5263
|
+
const importedInventory = options.pluginInventory ? readPluginInventory(options.pluginInventory) : void 0;
|
|
5264
|
+
const wantPlugins = !importedInventory && (options.claudePlugins ?? config.claudePlugins ?? false);
|
|
5265
|
+
let pluginRoots = importedInventory?.roots;
|
|
5266
|
+
const notices = [...importedInventory?.notices ?? []];
|
|
5039
5267
|
if (wantPlugins) {
|
|
5040
5268
|
const discovered = discoverClaudePluginRoots(deps);
|
|
5041
5269
|
pluginRoots = discovered.roots;
|
|
5042
5270
|
notices.push(...discovered.notices);
|
|
5043
5271
|
}
|
|
5044
|
-
|
|
5272
|
+
if (importedInventory?.host === "codex" && options.usageSnapshot) throw new UserError("Claude skill-monitor snapshots cannot be joined to Codex inventory.");
|
|
5273
|
+
const usageSnapshotPath = importedInventory?.host === "codex" ? void 0 : options.usageSnapshot ?? config.usageSnapshotPath;
|
|
5045
5274
|
const usage = usageSnapshotPath ? loadUsageSnapshot(usageSnapshotPath, { windowDays: options.usageWindow }) : void 0;
|
|
5046
5275
|
if (usage?.warning) notices.push(usage.warning);
|
|
5047
|
-
return { pluginRoots, usage, notices };
|
|
5276
|
+
return { pluginRoots, usage, notices, importedInventory };
|
|
5048
5277
|
}
|
|
5049
5278
|
function splitCsv(s) {
|
|
5050
5279
|
return s.split(",").map((x) => x.trim()).filter(Boolean);
|
|
@@ -5080,12 +5309,12 @@ async function captureBusinessProfile(rl, existing) {
|
|
|
5080
5309
|
function resolveReportPath(reportOption) {
|
|
5081
5310
|
if (reportOption) return resolvePath5(reportOption);
|
|
5082
5311
|
const iso = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
|
|
5083
|
-
return
|
|
5312
|
+
return join24(getHomeDir(), "reports", `audit-${iso}.md`);
|
|
5084
5313
|
}
|
|
5085
|
-
function summarizePluginRecommendations(
|
|
5314
|
+
function summarizePluginRecommendations(plugins2) {
|
|
5086
5315
|
const counts = { keep: 0, review: 0, unobserved: 0, unknown: 0 };
|
|
5087
|
-
for (const p of
|
|
5088
|
-
return `plugins: ${
|
|
5316
|
+
for (const p of plugins2) counts[p.recommendation]++;
|
|
5317
|
+
return `plugins: ${plugins2.length} (review ${counts.review}, unobserved ${counts.unobserved}, unknown ${counts.unknown})`;
|
|
5089
5318
|
}
|
|
5090
5319
|
function printTerminalSummary(report, reportPath, usage) {
|
|
5091
5320
|
const { summary } = report;
|
|
@@ -5141,7 +5370,7 @@ async function maybeHandoff(config, reportPath, options, rl) {
|
|
|
5141
5370
|
}
|
|
5142
5371
|
async function auditCommand(options = {}, deps = {}) {
|
|
5143
5372
|
await maybeOfferInit({ json: options.json, yes: options.yes, suppressAuditOffer: true });
|
|
5144
|
-
if (!
|
|
5373
|
+
if (!existsSync18(getConfigPath())) {
|
|
5145
5374
|
throw new UserError(
|
|
5146
5375
|
"no skill-forge config found \u2014 run `skill-forge init` first (audit requires a configured skill/MCP set; it never audits an invented default)."
|
|
5147
5376
|
);
|
|
@@ -5157,11 +5386,17 @@ async function auditCommand(options = {}, deps = {}) {
|
|
|
5157
5386
|
saveConfig(config);
|
|
5158
5387
|
}
|
|
5159
5388
|
}
|
|
5160
|
-
const { pluginRoots, usage, notices: pluginUsageNotices } = resolvePluginAndUsageContext(config, options, deps);
|
|
5389
|
+
const { pluginRoots, usage, importedInventory, notices: pluginUsageNotices } = resolvePluginAndUsageContext(config, options, deps);
|
|
5161
5390
|
const { records: accepted, notices: storeNotices } = readAcceptedFindings();
|
|
5162
5391
|
const report = await auditConfiguredSet(config, { accepted, pluginRoots, usage });
|
|
5392
|
+
if (importedInventory) {
|
|
5393
|
+
report.hostInventory = { host: importedInventory.host, complete: importedInventory.complete, generatedAt: importedInventory.generatedAt };
|
|
5394
|
+
for (const plugin of report.plugins ?? []) plugin.host = importedInventory.host;
|
|
5395
|
+
}
|
|
5396
|
+
applyPluginDecisions(report, deps.cwd);
|
|
5163
5397
|
const extraNotices = [...storeNotices, ...pluginUsageNotices];
|
|
5164
5398
|
if (extraNotices.length > 0) report.notices = [...report.notices, ...extraNotices];
|
|
5399
|
+
if (options.compare) report.comparison = loadAuditComparison(report, options.compare);
|
|
5165
5400
|
const reportPath = resolveReportPath(options.report);
|
|
5166
5401
|
writeNewFile(reportPath, renderAuditMarkdown(report), "--report");
|
|
5167
5402
|
try {
|
|
@@ -5212,19 +5447,19 @@ function hasInstalledMember(group) {
|
|
|
5212
5447
|
return group.members.some((m) => m.presence === "installed");
|
|
5213
5448
|
}
|
|
5214
5449
|
function describeGroup(members, scopes) {
|
|
5215
|
-
const
|
|
5450
|
+
const scope2 = [...scopes].join("+");
|
|
5216
5451
|
const installed = members.filter((m) => m.presence === "installed").map((m) => m.agent.displayName);
|
|
5217
5452
|
const others = members.length - installed.length;
|
|
5218
5453
|
if (installed.length > 0) {
|
|
5219
5454
|
const share = others > 0 ? ` (+${others} other agent${others === 1 ? "" : "s"} share this path)` : "";
|
|
5220
|
-
return `on PATH: ${installed.join(", ")} (${
|
|
5455
|
+
return `on PATH: ${installed.join(", ")} (${scope2})${share}`;
|
|
5221
5456
|
}
|
|
5222
5457
|
const names = members.map((m) => m.agent.displayName);
|
|
5223
5458
|
const shown = names.slice(0, 3).join(", ");
|
|
5224
5459
|
const more = names.length > 3 ? `, +${names.length - 3} more` : "";
|
|
5225
5460
|
const suffix = members.every((m) => m.presence === "absent") ? "CLI not on PATH" : "no verified CLI name to check";
|
|
5226
5461
|
const who = names.length === 1 ? names[0] : `${names.length} agents: ${shown}${more}`;
|
|
5227
|
-
return `${who} (${
|
|
5462
|
+
return `${who} (${scope2}) \u2014 ${suffix}`;
|
|
5228
5463
|
}
|
|
5229
5464
|
function detectAgents(cwd, home, pathEnv) {
|
|
5230
5465
|
const found = [];
|
|
@@ -5490,8 +5725,8 @@ async function init(options = {}, deps = {}) {
|
|
|
5490
5725
|
"[skill-forge] No detected agent CLI was found on PATH \u2014 keeping every detected target selected."
|
|
5491
5726
|
);
|
|
5492
5727
|
}
|
|
5493
|
-
const installedPluginsPath = options.installedPluginsPath ??
|
|
5494
|
-
const claudePluginsRegistryExists =
|
|
5728
|
+
const installedPluginsPath = options.installedPluginsPath ?? join25(home, ".claude", "plugins", "installed_plugins.json");
|
|
5729
|
+
const claudePluginsRegistryExists = existsSync19(installedPluginsPath);
|
|
5495
5730
|
if (options.defaults) {
|
|
5496
5731
|
const selectedRoots = narrow ? rootGroups.filter(hasInstalledMember) : rootGroups;
|
|
5497
5732
|
const selectedMcpTargets = narrow ? mcpGroups.filter(hasInstalledMember) : mcpGroups;
|
|
@@ -5550,7 +5785,7 @@ async function init(options = {}, deps = {}) {
|
|
|
5550
5785
|
}
|
|
5551
5786
|
async function maybeOfferInit(options = {}) {
|
|
5552
5787
|
if (options.json || options.yes || !process.stdin.isTTY) return;
|
|
5553
|
-
if (
|
|
5788
|
+
if (existsSync19(getConfigPath())) return;
|
|
5554
5789
|
console.log(
|
|
5555
5790
|
"[skill-forge] No config found \u2014 run `skill-forge init` to detect your agents and set gate targets (init finishes with a `skill-forge audit` health check over what it just configured)."
|
|
5556
5791
|
);
|
|
@@ -5581,7 +5816,7 @@ async function add(source, options = {}) {
|
|
|
5581
5816
|
if (!targetRoot) {
|
|
5582
5817
|
throw new Error("no target skills root configured; pass --target <dir> or set skillsRoots in the config");
|
|
5583
5818
|
}
|
|
5584
|
-
const resolved =
|
|
5819
|
+
const resolved = resolve2(source, artifact);
|
|
5585
5820
|
const entry = await quarantine(resolved, config.quarantineDir);
|
|
5586
5821
|
const gatedFingerprint = fingerprintDirs(entry.skillDirs).digest;
|
|
5587
5822
|
if (entry.skillDirs.length === 0) {
|
|
@@ -5665,7 +5900,7 @@ async function add(source, options = {}) {
|
|
|
5665
5900
|
}
|
|
5666
5901
|
}
|
|
5667
5902
|
async function addMcp(source, config, options) {
|
|
5668
|
-
const resolved =
|
|
5903
|
+
const resolved = resolve2(source, "mcp");
|
|
5669
5904
|
const entry = await quarantine(resolved, config.quarantineDir);
|
|
5670
5905
|
const candidatePath = resolveMcpCandidatePath(entry);
|
|
5671
5906
|
const { safety, gateResult, proEnabled } = await runGate(candidatePath, "", {
|
|
@@ -5701,7 +5936,7 @@ async function addMcp(source, config, options) {
|
|
|
5701
5936
|
envVars: detail.env ? Object.keys(detail.env) : void 0,
|
|
5702
5937
|
candidateEntry: detail.command ? { command: detail.command, args: detail.args, env: detail.env } : void 0
|
|
5703
5938
|
};
|
|
5704
|
-
const mcpTargetPath = options.mcpTarget ?? config.mcpTargets?.[0] ??
|
|
5939
|
+
const mcpTargetPath = options.mcpTarget ?? config.mcpTargets?.[0] ?? join26(process.cwd(), ".mcp.json");
|
|
5705
5940
|
const result = promoteMcp(entry, serverProfile, mcpTargetPath, { force: options.force });
|
|
5706
5941
|
reject(entry);
|
|
5707
5942
|
logOutcome(`[skill-forge] promoted -> ${result.target} (server "${result.serverName}")`, options.json);
|
|
@@ -5756,7 +5991,7 @@ async function scan(source, options = {}) {
|
|
|
5756
5991
|
if (artifact === "skill" && !skillsRoot) {
|
|
5757
5992
|
throw new Error("no skills root configured; set skillsRoots in the config");
|
|
5758
5993
|
}
|
|
5759
|
-
const resolved =
|
|
5994
|
+
const resolved = resolve2(source, artifact);
|
|
5760
5995
|
const entry = await quarantine(resolved, config.quarantineDir);
|
|
5761
5996
|
try {
|
|
5762
5997
|
let candidatePath;
|
|
@@ -5797,7 +6032,7 @@ async function scan(source, options = {}) {
|
|
|
5797
6032
|
}
|
|
5798
6033
|
|
|
5799
6034
|
// src/commands/list.ts
|
|
5800
|
-
import { join as
|
|
6035
|
+
import { join as join27 } from "path";
|
|
5801
6036
|
async function list() {
|
|
5802
6037
|
await maybeOfferInit();
|
|
5803
6038
|
const config = loadConfig();
|
|
@@ -5810,14 +6045,14 @@ async function list() {
|
|
|
5810
6045
|
}
|
|
5811
6046
|
console.log(`[skill-forge] ${ids.length} held in quarantine (${quarantineDir}):`);
|
|
5812
6047
|
for (const id of ids) {
|
|
5813
|
-
const skillDirs = findSkillDirs(
|
|
6048
|
+
const skillDirs = findSkillDirs(join27(quarantineDir, id));
|
|
5814
6049
|
if (skillDirs.length === 0) {
|
|
5815
6050
|
console.log(` ${id} (no SKILL.md found)`);
|
|
5816
6051
|
continue;
|
|
5817
6052
|
}
|
|
5818
6053
|
const names = skillDirs.map((dir) => {
|
|
5819
6054
|
try {
|
|
5820
|
-
return readFrontmatter(
|
|
6055
|
+
return readFrontmatter(join27(dir, "SKILL.md")).name;
|
|
5821
6056
|
} catch {
|
|
5822
6057
|
return dir;
|
|
5823
6058
|
}
|
|
@@ -5827,23 +6062,23 @@ async function list() {
|
|
|
5827
6062
|
}
|
|
5828
6063
|
|
|
5829
6064
|
// src/commands/guide.ts
|
|
5830
|
-
import { existsSync as
|
|
6065
|
+
import { existsSync as existsSync21 } from "fs";
|
|
5831
6066
|
|
|
5832
6067
|
// src/commands/held.ts
|
|
5833
|
-
import { existsSync as
|
|
5834
|
-
import { basename as basename9, join as
|
|
6068
|
+
import { existsSync as existsSync20, realpathSync as realpathSync6 } from "fs";
|
|
6069
|
+
import { basename as basename9, join as join28, resolve as resolvePath6, sep as sep7 } from "path";
|
|
5835
6070
|
var PRO_FEATURE_LABEL2 = "overlap analysis, the provenance ledger, and the pending-ingestion queue";
|
|
5836
6071
|
function resolveHeldPath(quarantineDir, id) {
|
|
5837
6072
|
if (id === "" || id !== basename9(id) || id === "." || id === "..") {
|
|
5838
6073
|
throw new UserError(`invalid quarantine id "${id}" \u2014 pass a single id exactly as printed by \`skill-forge list\``);
|
|
5839
6074
|
}
|
|
5840
|
-
const candidate =
|
|
5841
|
-
if (!
|
|
6075
|
+
const candidate = join28(quarantineDir, id);
|
|
6076
|
+
if (!existsSync20(candidate)) {
|
|
5842
6077
|
throw new UserError(`no held entry with id "${id}" \u2014 run \`skill-forge list\` to see what is in quarantine`);
|
|
5843
6078
|
}
|
|
5844
|
-
const realRoot =
|
|
5845
|
-
const realCandidate =
|
|
5846
|
-
if (!realCandidate.startsWith(realRoot +
|
|
6079
|
+
const realRoot = realpathSync6(resolvePath6(quarantineDir));
|
|
6080
|
+
const realCandidate = realpathSync6(candidate);
|
|
6081
|
+
if (!realCandidate.startsWith(realRoot + sep7)) {
|
|
5847
6082
|
throw new UserError(`refusing to act on "${id}": it resolves outside the quarantine dir (${realRoot})`);
|
|
5848
6083
|
}
|
|
5849
6084
|
return realCandidate;
|
|
@@ -5866,7 +6101,7 @@ function resolveTargetRoot(config, target) {
|
|
|
5866
6101
|
function heldSkillNames(skillDirs) {
|
|
5867
6102
|
return skillDirs.map((dir) => {
|
|
5868
6103
|
try {
|
|
5869
|
-
return readFrontmatter(
|
|
6104
|
+
return readFrontmatter(join28(dir, "SKILL.md")).name;
|
|
5870
6105
|
} catch {
|
|
5871
6106
|
return basename9(dir);
|
|
5872
6107
|
}
|
|
@@ -5874,7 +6109,7 @@ function heldSkillNames(skillDirs) {
|
|
|
5874
6109
|
}
|
|
5875
6110
|
function looksLikeMcpSandbox(quarantinePath) {
|
|
5876
6111
|
const candidate = resolveMcpCandidatePath({ quarantinePath });
|
|
5877
|
-
return [".mcp.json", "mcp.json", "package.json"].some((f) =>
|
|
6112
|
+
return [".mcp.json", "mcp.json", "package.json"].some((f) => existsSync20(join28(candidate, f)));
|
|
5878
6113
|
}
|
|
5879
6114
|
async function promoteHeldMcp(id, entry, config, options) {
|
|
5880
6115
|
const candidatePath = resolveMcpCandidatePath(entry);
|
|
@@ -5908,7 +6143,7 @@ async function promoteHeldMcp(id, entry, config, options) {
|
|
|
5908
6143
|
envVars: detail.env ? Object.keys(detail.env) : void 0,
|
|
5909
6144
|
candidateEntry: detail.command ? { command: detail.command, args: detail.args, env: detail.env } : void 0
|
|
5910
6145
|
};
|
|
5911
|
-
const mcpTargetPath = options.mcpTarget ?? config.mcpTargets?.[0] ??
|
|
6146
|
+
const mcpTargetPath = options.mcpTarget ?? config.mcpTargets?.[0] ?? join28(process.cwd(), ".mcp.json");
|
|
5912
6147
|
const result = promoteMcp(entry, serverProfile, mcpTargetPath, { force: options.force });
|
|
5913
6148
|
reject(entry);
|
|
5914
6149
|
logOutcome(`[skill-forge] promoted -> ${result.target} (server "${result.serverName}")`, options.json);
|
|
@@ -6153,7 +6388,7 @@ var TOPICS = [
|
|
|
6153
6388
|
];
|
|
6154
6389
|
function readState() {
|
|
6155
6390
|
const configPath = getConfigPath();
|
|
6156
|
-
const configured =
|
|
6391
|
+
const configured = existsSync21(configPath);
|
|
6157
6392
|
const base = {
|
|
6158
6393
|
configured,
|
|
6159
6394
|
configPath,
|
|
@@ -6387,17 +6622,17 @@ async function status() {
|
|
|
6387
6622
|
// src/evolve.ts
|
|
6388
6623
|
import { createInterface as createInterface5 } from "readline/promises";
|
|
6389
6624
|
import { createHash as createHash3 } from "crypto";
|
|
6390
|
-
import { cpSync as cpSync3, existsSync as
|
|
6625
|
+
import { cpSync as cpSync3, existsSync as existsSync22, lstatSync as lstatSync4, mkdtempSync as mkdtempSync3, readFileSync as readFileSync21, rmSync as rmSync6 } from "fs";
|
|
6391
6626
|
import { tmpdir as tmpdir2 } from "os";
|
|
6392
|
-
import { basename as basename10, dirname as dirname9, join as
|
|
6627
|
+
import { basename as basename10, dirname as dirname9, join as join29, resolve as resolvePath7, sep as sep8 } from "path";
|
|
6393
6628
|
var SKILLOPT_BIN = "skillopt-sleep";
|
|
6394
6629
|
var PRO_FEATURE_LABEL3 = "self-evolution via SkillOpt-Sleep (evolve) \u2014 re-gating, provenance, and the pending-ingestion queue";
|
|
6395
6630
|
async function evolve(targetSkillDir, options = {}, deps = {}) {
|
|
6396
6631
|
const exec = deps.exec ?? defaultExec;
|
|
6397
6632
|
const json = options.json;
|
|
6398
6633
|
const resolvedTargetDir = resolvePath7(targetSkillDir);
|
|
6399
|
-
const skillMdPath =
|
|
6400
|
-
if (!
|
|
6634
|
+
const skillMdPath = join29(resolvedTargetDir, "SKILL.md");
|
|
6635
|
+
if (!existsSync22(skillMdPath)) {
|
|
6401
6636
|
throw new UserError(`no SKILL.md found at ${skillMdPath} \u2014 evolve requires an existing, installed skill directory`);
|
|
6402
6637
|
}
|
|
6403
6638
|
const proStatus = proFeatureStatus(PRO_FEATURE_LABEL3);
|
|
@@ -6455,33 +6690,33 @@ async function evolve(targetSkillDir, options = {}, deps = {}) {
|
|
|
6455
6690
|
}
|
|
6456
6691
|
return;
|
|
6457
6692
|
}
|
|
6458
|
-
const stagingRoot =
|
|
6693
|
+
const stagingRoot = join29(project, ".skillopt-sleep", "staging");
|
|
6459
6694
|
const stagingDir = resolvePath7(parsed.staging_dir);
|
|
6460
|
-
if (stagingDir !== stagingRoot && !stagingDir.startsWith(stagingRoot +
|
|
6695
|
+
if (stagingDir !== stagingRoot && !stagingDir.startsWith(stagingRoot + sep8)) {
|
|
6461
6696
|
throw new UserError(
|
|
6462
6697
|
`skillopt-sleep reported a staging_dir outside the expected sandbox (${stagingRoot}): ${stagingDir} \u2014 refusing to use it`
|
|
6463
6698
|
);
|
|
6464
6699
|
}
|
|
6465
|
-
const proposedSkillMd =
|
|
6466
|
-
const proposedClaudeMd =
|
|
6467
|
-
const reportMdPath =
|
|
6468
|
-
if (!
|
|
6700
|
+
const proposedSkillMd = join29(stagingDir, "proposed_SKILL.md");
|
|
6701
|
+
const proposedClaudeMd = join29(stagingDir, "proposed_CLAUDE.md");
|
|
6702
|
+
const reportMdPath = join29(stagingDir, "report.md");
|
|
6703
|
+
if (!existsSync22(proposedSkillMd)) {
|
|
6469
6704
|
throw new UserError(`skillopt-sleep reported an accepted proposal but ${proposedSkillMd} is missing`);
|
|
6470
6705
|
}
|
|
6471
|
-
if (
|
|
6706
|
+
if (existsSync22(reportMdPath)) {
|
|
6472
6707
|
notices.push(`SkillOpt-Sleep's full report: ${reportMdPath}`);
|
|
6473
6708
|
}
|
|
6474
6709
|
assertNotSymlink(proposedSkillMd, "proposed_SKILL.md");
|
|
6475
6710
|
assertNotSymlink(proposedClaudeMd, "proposed_CLAUDE.md");
|
|
6476
6711
|
const scannedSkillHash = sha256File(proposedSkillMd);
|
|
6477
6712
|
const scannedClaudeHash = sha256File(proposedClaudeMd);
|
|
6478
|
-
const tempDir = mkdtempSync3(
|
|
6713
|
+
const tempDir = mkdtempSync3(join29(tmpdir2(), "skill-forge-evolve-"));
|
|
6479
6714
|
let profile;
|
|
6480
6715
|
let safety;
|
|
6481
6716
|
try {
|
|
6482
|
-
cpSync3(proposedSkillMd,
|
|
6483
|
-
if (
|
|
6484
|
-
cpSync3(proposedClaudeMd,
|
|
6717
|
+
cpSync3(proposedSkillMd, join29(tempDir, "SKILL.md"));
|
|
6718
|
+
if (existsSync22(proposedClaudeMd)) {
|
|
6719
|
+
cpSync3(proposedClaudeMd, join29(tempDir, "CLAUDE.md"));
|
|
6485
6720
|
}
|
|
6486
6721
|
profile = asSkillProfile(profileSkill(tempDir), "for the evolved proposal");
|
|
6487
6722
|
const strictness = loadConfig().strictness;
|
|
@@ -6580,7 +6815,7 @@ async function evolve(targetSkillDir, options = {}, deps = {}) {
|
|
|
6580
6815
|
if (safetyVerdict === "block") process.exitCode = 1;
|
|
6581
6816
|
} else {
|
|
6582
6817
|
logOutcome(`[skill-forge] held \u2014 staging dir left in place -> ${stagingDir}`, json);
|
|
6583
|
-
if (
|
|
6818
|
+
if (existsSync22(reportMdPath)) {
|
|
6584
6819
|
logOutcome(`[skill-forge] see ${reportMdPath} for the full evidence report`, json);
|
|
6585
6820
|
}
|
|
6586
6821
|
}
|
|
@@ -6622,10 +6857,10 @@ function validateRunOutput(raw) {
|
|
|
6622
6857
|
return { accepted: Boolean(obj.accepted), staging_dir: stagingDirRaw ?? null };
|
|
6623
6858
|
}
|
|
6624
6859
|
function readManifest(stagingDir) {
|
|
6625
|
-
const manifestPath =
|
|
6860
|
+
const manifestPath = join29(stagingDir, "manifest.json");
|
|
6626
6861
|
let raw;
|
|
6627
6862
|
try {
|
|
6628
|
-
raw =
|
|
6863
|
+
raw = readFileSync21(manifestPath, "utf-8");
|
|
6629
6864
|
} catch (err) {
|
|
6630
6865
|
throw new UserError(`could not read staging manifest ${manifestPath}: ${err.message}`);
|
|
6631
6866
|
}
|
|
@@ -6659,18 +6894,18 @@ function validateManifest(raw, manifestPath) {
|
|
|
6659
6894
|
};
|
|
6660
6895
|
}
|
|
6661
6896
|
function filesIdentical(a, b) {
|
|
6662
|
-
if (!
|
|
6663
|
-
return
|
|
6897
|
+
if (!existsSync22(a) || !existsSync22(b)) return false;
|
|
6898
|
+
return readFileSync21(a).equals(readFileSync21(b));
|
|
6664
6899
|
}
|
|
6665
6900
|
function isAlreadyAdopted(stagingDir, manifest) {
|
|
6666
|
-
const proposedSkillMd =
|
|
6667
|
-
const proposedClaudeMd =
|
|
6901
|
+
const proposedSkillMd = join29(stagingDir, "proposed_SKILL.md");
|
|
6902
|
+
const proposedClaudeMd = join29(stagingDir, "proposed_CLAUDE.md");
|
|
6668
6903
|
let sawComparison = false;
|
|
6669
|
-
if (
|
|
6904
|
+
if (existsSync22(proposedSkillMd)) {
|
|
6670
6905
|
sawComparison = true;
|
|
6671
6906
|
if (!filesIdentical(proposedSkillMd, manifest.live_skill_path)) return false;
|
|
6672
6907
|
}
|
|
6673
|
-
if (
|
|
6908
|
+
if (existsSync22(proposedClaudeMd) && manifest.live_memory_path) {
|
|
6674
6909
|
sawComparison = true;
|
|
6675
6910
|
if (!filesIdentical(proposedClaudeMd, manifest.live_memory_path)) return false;
|
|
6676
6911
|
}
|
|
@@ -6688,8 +6923,8 @@ function assertNotSymlink(path, label2) {
|
|
|
6688
6923
|
}
|
|
6689
6924
|
}
|
|
6690
6925
|
function sha256File(path) {
|
|
6691
|
-
if (!
|
|
6692
|
-
return createHash3("sha256").update(
|
|
6926
|
+
if (!existsSync22(path)) return null;
|
|
6927
|
+
return createHash3("sha256").update(readFileSync21(path)).digest("hex");
|
|
6693
6928
|
}
|
|
6694
6929
|
|
|
6695
6930
|
// src/commands/evolve.ts
|
|
@@ -6698,9 +6933,9 @@ async function evolveCommand(targetSkillDir, options = {}) {
|
|
|
6698
6933
|
}
|
|
6699
6934
|
|
|
6700
6935
|
// src/commands/finding.ts
|
|
6701
|
-
import { existsSync as
|
|
6936
|
+
import { existsSync as existsSync23 } from "fs";
|
|
6702
6937
|
function requireSavedConfig() {
|
|
6703
|
-
if (!
|
|
6938
|
+
if (!existsSync23(getConfigPath())) {
|
|
6704
6939
|
throw new UserError(
|
|
6705
6940
|
"no skill-forge config found \u2014 run `skill-forge init` first (`finding accept` re-runs the audit engine to resolve a live finding, and audit never runs against an invented default)."
|
|
6706
6941
|
);
|
|
@@ -6786,7 +7021,7 @@ async function findingRevokeCommand(prefix, options = {}) {
|
|
|
6786
7021
|
async function findingListCommand(options = {}) {
|
|
6787
7022
|
const { records, notices } = readAcceptedFindings();
|
|
6788
7023
|
for (const notice of notices) console.error(`[skill-forge] ${notice}`);
|
|
6789
|
-
const rows = options.stale ? records.filter((r) => !
|
|
7024
|
+
const rows = options.stale ? records.filter((r) => !existsSync23(r.target)) : records;
|
|
6790
7025
|
if (options.json) {
|
|
6791
7026
|
console.log(JSON.stringify(rows, null, 2));
|
|
6792
7027
|
return;
|
|
@@ -6807,8 +7042,8 @@ async function findingListCommand(options = {}) {
|
|
|
6807
7042
|
import { resolve as resolvePath9 } from "path";
|
|
6808
7043
|
|
|
6809
7044
|
// src/organize.ts
|
|
6810
|
-
import { existsSync as
|
|
6811
|
-
import { basename as basename11, join as
|
|
7045
|
+
import { existsSync as existsSync24, readFileSync as readFileSync22, readdirSync as readdirSync10, realpathSync as realpathSync7, statSync as statSync10 } from "fs";
|
|
7046
|
+
import { basename as basename11, join as join30, resolve as resolvePath8 } from "path";
|
|
6812
7047
|
var PRO_FEATURE_LABEL4 = "set-level organizer reports";
|
|
6813
7048
|
var TIERS = /* @__PURE__ */ new Set(["resource", "custom"]);
|
|
6814
7049
|
var MATURITIES = /* @__PURE__ */ new Set(["seedling", "stable", "deprecated", "experimental"]);
|
|
@@ -6821,14 +7056,14 @@ function listSubdirs2(dir) {
|
|
|
6821
7056
|
}
|
|
6822
7057
|
const out = [];
|
|
6823
7058
|
for (const entry of entries) {
|
|
6824
|
-
const full =
|
|
7059
|
+
const full = join30(dir, entry.name);
|
|
6825
7060
|
if (entry.isDirectory()) {
|
|
6826
7061
|
out.push(full);
|
|
6827
7062
|
continue;
|
|
6828
7063
|
}
|
|
6829
7064
|
if (entry.isSymbolicLink()) {
|
|
6830
7065
|
try {
|
|
6831
|
-
if (
|
|
7066
|
+
if (statSync10(full).isDirectory()) out.push(full);
|
|
6832
7067
|
} catch {
|
|
6833
7068
|
}
|
|
6834
7069
|
}
|
|
@@ -6839,14 +7074,14 @@ function findSkillDirsUnderRoot2(root) {
|
|
|
6839
7074
|
const found = /* @__PURE__ */ new Set();
|
|
6840
7075
|
const topDirs = listSubdirs2(root);
|
|
6841
7076
|
for (const dir of topDirs) {
|
|
6842
|
-
if (
|
|
7077
|
+
if (existsSync24(join30(dir, "SKILL.md"))) found.add(dir);
|
|
6843
7078
|
}
|
|
6844
|
-
for (const dir of listSubdirs2(
|
|
6845
|
-
if (
|
|
7079
|
+
for (const dir of listSubdirs2(join30(root, "skills"))) {
|
|
7080
|
+
if (existsSync24(join30(dir, "SKILL.md"))) found.add(dir);
|
|
6846
7081
|
}
|
|
6847
7082
|
for (const topDir of topDirs) {
|
|
6848
|
-
for (const dir of listSubdirs2(
|
|
6849
|
-
if (
|
|
7083
|
+
for (const dir of listSubdirs2(join30(topDir, "skills"))) {
|
|
7084
|
+
if (existsSync24(join30(dir, "SKILL.md"))) found.add(dir);
|
|
6850
7085
|
}
|
|
6851
7086
|
}
|
|
6852
7087
|
return Array.from(found).filter((p) => !p.includes("/.git/"));
|
|
@@ -7042,17 +7277,17 @@ function buildRegistryEntries(candidates, warnings) {
|
|
|
7042
7277
|
for (const candidate of candidates) {
|
|
7043
7278
|
let realPath;
|
|
7044
7279
|
try {
|
|
7045
|
-
realPath =
|
|
7280
|
+
realPath = realpathSync7(candidate.dirPath);
|
|
7046
7281
|
} catch (err) {
|
|
7047
7282
|
warnings.push(`could not resolve skill directory ${candidate.dirPath}: ${err.message}`);
|
|
7048
7283
|
continue;
|
|
7049
7284
|
}
|
|
7050
7285
|
if (seenRealPaths.has(realPath)) continue;
|
|
7051
7286
|
seenRealPaths.add(realPath);
|
|
7052
|
-
const skillMdPath =
|
|
7287
|
+
const skillMdPath = join30(realPath, "SKILL.md");
|
|
7053
7288
|
let text;
|
|
7054
7289
|
try {
|
|
7055
|
-
text =
|
|
7290
|
+
text = readFileSync22(skillMdPath, "utf-8");
|
|
7056
7291
|
} catch (err) {
|
|
7057
7292
|
warnings.push(`could not read ${skillMdPath}: ${err.message}`);
|
|
7058
7293
|
continue;
|
|
@@ -7458,13 +7693,13 @@ async function find(query, options = {}, deps = {}) {
|
|
|
7458
7693
|
}
|
|
7459
7694
|
|
|
7460
7695
|
// src/commands/watch.ts
|
|
7461
|
-
import { realpathSync as
|
|
7462
|
-
import { join as
|
|
7696
|
+
import { realpathSync as realpathSync8 } from "fs";
|
|
7697
|
+
import { join as join31, resolve as resolvePath10 } from "path";
|
|
7463
7698
|
|
|
7464
7699
|
// src/gate/skillMapDrift.ts
|
|
7465
7700
|
import { createHash as createHash4 } from "crypto";
|
|
7466
|
-
import { readFileSync as
|
|
7467
|
-
import { dirname as dirname10, isAbsolute as
|
|
7701
|
+
import { readFileSync as readFileSync23, existsSync as existsSync25 } from "fs";
|
|
7702
|
+
import { dirname as dirname10, isAbsolute as isAbsolute6, resolve as resolve8 } from "path";
|
|
7468
7703
|
function sha256(text) {
|
|
7469
7704
|
return createHash4("sha256").update(text, "utf-8").digest("hex");
|
|
7470
7705
|
}
|
|
@@ -7497,22 +7732,22 @@ function threeWayVerdict(localNormalized, upstreamHash, baselineHash) {
|
|
|
7497
7732
|
}
|
|
7498
7733
|
function resolveLocalPath(path, mapDir) {
|
|
7499
7734
|
const expanded = expandHome(path);
|
|
7500
|
-
if (
|
|
7501
|
-
const candidates = [
|
|
7735
|
+
if (isAbsolute6(expanded)) return resolve8(expanded);
|
|
7736
|
+
const candidates = [resolve8(expanded)];
|
|
7502
7737
|
if (mapDir) {
|
|
7503
|
-
candidates.push(
|
|
7504
|
-
candidates.push(
|
|
7738
|
+
candidates.push(resolve8(mapDir, expanded));
|
|
7739
|
+
candidates.push(resolve8(mapDir, "..", expanded));
|
|
7505
7740
|
}
|
|
7506
7741
|
for (const candidate of candidates) {
|
|
7507
|
-
if (
|
|
7742
|
+
if (existsSync25(candidate)) return candidate;
|
|
7508
7743
|
}
|
|
7509
7744
|
return candidates[0];
|
|
7510
7745
|
}
|
|
7511
7746
|
function defaultReadLocalFile(path, mapDir) {
|
|
7512
7747
|
const resolved = resolveLocalPath(path, mapDir);
|
|
7513
|
-
if (!
|
|
7748
|
+
if (!existsSync25(resolved)) return void 0;
|
|
7514
7749
|
try {
|
|
7515
|
-
return
|
|
7750
|
+
return readFileSync23(resolved, "utf-8");
|
|
7516
7751
|
} catch {
|
|
7517
7752
|
return void 0;
|
|
7518
7753
|
}
|
|
@@ -7533,7 +7768,7 @@ function upstreamLabel(node, edge) {
|
|
|
7533
7768
|
return node?.url ?? node?.path ?? node?.name ?? node?.id ?? edge.to;
|
|
7534
7769
|
}
|
|
7535
7770
|
async function computeMapDrift(map, deps = {}) {
|
|
7536
|
-
const mapDir = deps.mapPath ? dirname10(
|
|
7771
|
+
const mapDir = deps.mapPath ? dirname10(resolve8(expandHome(deps.mapPath))) : void 0;
|
|
7537
7772
|
const readLocalFile = deps.readLocalFile ?? ((path) => defaultReadLocalFile(path, mapDir));
|
|
7538
7773
|
const fetchUpstream = deps.fetchUpstream ?? defaultFetchUpstream;
|
|
7539
7774
|
const rows = [];
|
|
@@ -7553,7 +7788,7 @@ async function computeMapDrift(map, deps = {}) {
|
|
|
7553
7788
|
continue;
|
|
7554
7789
|
}
|
|
7555
7790
|
const localResolved = resolveLocalPath(localNode.path, mapDir);
|
|
7556
|
-
if (!
|
|
7791
|
+
if (!existsSync25(localResolved)) {
|
|
7557
7792
|
rows.push({
|
|
7558
7793
|
skill: skillLabel,
|
|
7559
7794
|
upstream,
|
|
@@ -7664,7 +7899,7 @@ async function runWatch(config, options = {}, deps = {}) {
|
|
|
7664
7899
|
let realPath = resolvedRoot;
|
|
7665
7900
|
if (exists) {
|
|
7666
7901
|
try {
|
|
7667
|
-
realPath =
|
|
7902
|
+
realPath = realpathSync8(resolvedRoot);
|
|
7668
7903
|
} catch {
|
|
7669
7904
|
realPath = resolvedRoot;
|
|
7670
7905
|
}
|
|
@@ -7676,7 +7911,7 @@ async function runWatch(config, options = {}, deps = {}) {
|
|
|
7676
7911
|
ledger = readProvenanceLedger(resolvedRoot);
|
|
7677
7912
|
} catch (err) {
|
|
7678
7913
|
const message = err instanceof Error ? err.message : String(err);
|
|
7679
|
-
const ledgerPath =
|
|
7914
|
+
const ledgerPath = join31(resolvedRoot, "SOURCES.md");
|
|
7680
7915
|
roots.push({ root, realPath, exists, ledgerPath, ledgerExists: false });
|
|
7681
7916
|
warnings.push(`${ledgerPath}: could not read ledger: ${message}`);
|
|
7682
7917
|
continue;
|
|
@@ -7786,8 +8021,8 @@ async function watchCommand(options = {}) {
|
|
|
7786
8021
|
}
|
|
7787
8022
|
|
|
7788
8023
|
// src/commands/routine.ts
|
|
7789
|
-
import { existsSync as
|
|
7790
|
-
import { join as
|
|
8024
|
+
import { existsSync as existsSync26, rmSync as rmSync7, statSync as statSync11 } from "fs";
|
|
8025
|
+
import { join as join32 } from "path";
|
|
7791
8026
|
var PRO_FEATURE_LABEL6 = "the scheduled maintenance routine";
|
|
7792
8027
|
var SEVERITY_ORDER = ["LOW", "MEDIUM", "HIGH", "CRITICAL"];
|
|
7793
8028
|
function parseFailOnFloor(failOn) {
|
|
@@ -7795,20 +8030,20 @@ function parseFailOnFloor(failOn) {
|
|
|
7795
8030
|
if (failOn === "any") return "LOW";
|
|
7796
8031
|
return failOn.toUpperCase();
|
|
7797
8032
|
}
|
|
7798
|
-
function exceedsFloor(
|
|
8033
|
+
function exceedsFloor(findings, failOn) {
|
|
7799
8034
|
const floor = parseFailOnFloor(failOn);
|
|
7800
8035
|
if (!floor) return void 0;
|
|
7801
8036
|
const floorIndex = SEVERITY_ORDER.indexOf(floor);
|
|
7802
8037
|
for (let i = SEVERITY_ORDER.length - 1; i >= floorIndex; i--) {
|
|
7803
8038
|
const severity = SEVERITY_ORDER[i];
|
|
7804
|
-
const count =
|
|
8039
|
+
const count = findings[severity] ?? 0;
|
|
7805
8040
|
if (count > 0) return `${count} ${severity} finding(s) at or above --fail-on ${failOn}`;
|
|
7806
8041
|
}
|
|
7807
8042
|
return void 0;
|
|
7808
8043
|
}
|
|
7809
8044
|
function resolveReportPath2(override, generatedAt) {
|
|
7810
8045
|
if (override) return override;
|
|
7811
|
-
return
|
|
8046
|
+
return join32(getHomeDir(), "reports", `routine-${generatedAt.replace(/[:.]/g, "-")}.md`);
|
|
7812
8047
|
}
|
|
7813
8048
|
function runHousekeeping(config, options) {
|
|
7814
8049
|
const actions = [];
|
|
@@ -7817,10 +8052,10 @@ function runHousekeeping(config, options) {
|
|
|
7817
8052
|
const pruneDays = options.pruneDays ?? 30;
|
|
7818
8053
|
const cutoff = Date.now() - pruneDays * 864e5;
|
|
7819
8054
|
for (const id of listQuarantineIds(config.quarantineDir)) {
|
|
7820
|
-
const path =
|
|
8055
|
+
const path = join32(config.quarantineDir, id);
|
|
7821
8056
|
let mtime;
|
|
7822
8057
|
try {
|
|
7823
|
-
mtime =
|
|
8058
|
+
mtime = statSync11(path).mtimeMs;
|
|
7824
8059
|
} catch {
|
|
7825
8060
|
continue;
|
|
7826
8061
|
}
|
|
@@ -7855,7 +8090,7 @@ function runHousekeeping(config, options) {
|
|
|
7855
8090
|
}
|
|
7856
8091
|
for (const entry of entries) {
|
|
7857
8092
|
if (entry.status !== "pending") continue;
|
|
7858
|
-
if (!entry.installedPath ||
|
|
8093
|
+
if (!entry.installedPath || existsSync26(entry.installedPath)) continue;
|
|
7859
8094
|
if (apply) {
|
|
7860
8095
|
try {
|
|
7861
8096
|
updateQueueEntry(entry.id, { status: "dismissed" });
|
|
@@ -7888,6 +8123,7 @@ function renderRoutineMarkdown(result, audit2, watch, organize) {
|
|
|
7888
8123
|
"",
|
|
7889
8124
|
`- Skills: ${result.audit.skillCount} across ${audit2.summary.rootCount} configured root(s)`,
|
|
7890
8125
|
`- Findings: ${audit2.summary.findingCounts.CRITICAL ?? 0} critical, ${audit2.summary.findingCounts.HIGH ?? 0} high, ${audit2.summary.findingCounts.MEDIUM ?? 0} medium, ${audit2.summary.findingCounts.LOW ?? 0} low`,
|
|
8126
|
+
`- Failure scope: ${result.failScope} (configured excludes plugin-origin findings; all includes them)`,
|
|
7891
8127
|
`- Accepted: ${audit2.summary.acceptedCount} (suppressed from the counts above; see Accepted findings)`,
|
|
7892
8128
|
`- Overlap clusters: ${result.audit.overlapClusters.length}${result.audit.overlapLocked ? ' (overlap analysis was locked \u2014 not "none found")' : ""}`,
|
|
7893
8129
|
`- Pending queue entries: ${result.pending.queue}`,
|
|
@@ -7957,15 +8193,23 @@ async function routineCommand(options = {}, deps = {}) {
|
|
|
7957
8193
|
if (options.dryRun && !options.housekeeping) {
|
|
7958
8194
|
throw new UserError("--dry-run only applies to --housekeeping; without it `routine` already writes nothing but its own report");
|
|
7959
8195
|
}
|
|
8196
|
+
const failScope = options.failScope ?? "configured";
|
|
8197
|
+
if (failScope !== "configured" && failScope !== "all") throw new UserError("--fail-scope must be configured or all");
|
|
7960
8198
|
const config = loadConfig();
|
|
7961
8199
|
const generatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
7962
|
-
const { pluginRoots, usage, notices: pluginUsageNotices } = resolvePluginAndUsageContext(config, options, deps);
|
|
8200
|
+
const { pluginRoots, usage, importedInventory, notices: pluginUsageNotices } = resolvePluginAndUsageContext(config, options, deps);
|
|
7963
8201
|
const { records: accepted, notices: storeNotices } = readAcceptedFindings();
|
|
7964
8202
|
const notices = [...storeNotices, ...pluginUsageNotices];
|
|
7965
8203
|
for (const notice of notices) log(`[skill-forge] ${notice}`);
|
|
7966
8204
|
const audit2 = await auditConfiguredSet(config, { accepted, pluginRoots, usage });
|
|
8205
|
+
if (importedInventory) {
|
|
8206
|
+
audit2.hostInventory = { host: importedInventory.host, complete: importedInventory.complete, generatedAt: importedInventory.generatedAt };
|
|
8207
|
+
for (const plugin of audit2.plugins ?? []) plugin.host = importedInventory.host;
|
|
8208
|
+
}
|
|
8209
|
+
applyPluginDecisions(audit2, deps.cwd);
|
|
8210
|
+
notices.push(...audit2.notices);
|
|
7967
8211
|
const auditState = summarizeAudit(audit2);
|
|
7968
|
-
if (audit2.plugins) {
|
|
8212
|
+
if (audit2.plugins && failScope === "configured") {
|
|
7969
8213
|
const critical = audit2.plugins.reduce((n, p) => n + p.findingCounts.CRITICAL, 0);
|
|
7970
8214
|
const high = audit2.plugins.reduce((n, p) => n + p.findingCounts.HIGH, 0);
|
|
7971
8215
|
if (critical + high > 0) {
|
|
@@ -8001,7 +8245,11 @@ async function routineCommand(options = {}, deps = {}) {
|
|
|
8001
8245
|
}
|
|
8002
8246
|
const quarantine2 = heldCount(config.quarantineDir);
|
|
8003
8247
|
if (!quarantine2.readable) degraded.push("quarantine dir could not be listed \u2014 held count is not trustworthy");
|
|
8248
|
+
const failureCounts = failScope === "all" ? audit2.summary.findingCounts : auditState.findings;
|
|
8004
8249
|
const result = {
|
|
8250
|
+
schemaVersion: 1,
|
|
8251
|
+
failScope,
|
|
8252
|
+
failureCounts,
|
|
8005
8253
|
generatedAt,
|
|
8006
8254
|
reportPath: resolveReportPath2(options.report, generatedAt),
|
|
8007
8255
|
audit: auditState,
|
|
@@ -8019,7 +8267,7 @@ async function routineCommand(options = {}, deps = {}) {
|
|
|
8019
8267
|
notices,
|
|
8020
8268
|
degraded
|
|
8021
8269
|
};
|
|
8022
|
-
const failure = degraded.length > 0 ? `${degraded.length} step(s) did not complete: ${degraded.join("; ")}` : exceedsFloor(
|
|
8270
|
+
const failure = degraded.length > 0 ? `${degraded.length} step(s) did not complete: ${degraded.join("; ")}` : exceedsFloor(failureCounts, options.failOn ?? "none");
|
|
8023
8271
|
if (failure) result.failure = failure;
|
|
8024
8272
|
writeNewFile(result.reportPath, renderRoutineMarkdown(result, audit2, watch, organize), "--report");
|
|
8025
8273
|
try {
|
|
@@ -8031,6 +8279,7 @@ async function routineCommand(options = {}, deps = {}) {
|
|
|
8031
8279
|
console.log(JSON.stringify(result, null, 2));
|
|
8032
8280
|
} else {
|
|
8033
8281
|
console.log("[skill-forge] routine");
|
|
8282
|
+
console.log(` failure scope: ${failScope}`);
|
|
8034
8283
|
console.log(` skills: ${auditState.skillCount}`);
|
|
8035
8284
|
console.log(
|
|
8036
8285
|
` findings: ${auditState.findings.CRITICAL ?? 0} critical / ${auditState.findings.HIGH ?? 0} high / ${auditState.findings.MEDIUM ?? 0} medium / ${auditState.findings.LOW ?? 0} low`
|
|
@@ -8060,8 +8309,8 @@ async function routineCommand(options = {}, deps = {}) {
|
|
|
8060
8309
|
}
|
|
8061
8310
|
|
|
8062
8311
|
// src/commands/ingest.ts
|
|
8063
|
-
import { realpathSync as
|
|
8064
|
-
import { dirname as dirname12, isAbsolute as
|
|
8312
|
+
import { realpathSync as realpathSync9 } from "fs";
|
|
8313
|
+
import { dirname as dirname12, isAbsolute as isAbsolute7, relative as relative6, resolve as resolvePath11 } from "path";
|
|
8065
8314
|
var PRO_FEATURE_LABEL7 = "the pending-ingestion queue";
|
|
8066
8315
|
var VALID_STATUSES = ["ingested", "dismissed"];
|
|
8067
8316
|
function requireProFeature() {
|
|
@@ -8102,14 +8351,14 @@ function printPendingList(pending, json) {
|
|
|
8102
8351
|
}
|
|
8103
8352
|
function realpathOrNull(p) {
|
|
8104
8353
|
try {
|
|
8105
|
-
return
|
|
8354
|
+
return realpathSync9(p);
|
|
8106
8355
|
} catch {
|
|
8107
8356
|
return null;
|
|
8108
8357
|
}
|
|
8109
8358
|
}
|
|
8110
8359
|
function isUnderRoot(candidate, root) {
|
|
8111
|
-
const rel =
|
|
8112
|
-
return rel === "" || !rel.startsWith("..") && !
|
|
8360
|
+
const rel = relative6(root, candidate);
|
|
8361
|
+
return rel === "" || !rel.startsWith("..") && !isAbsolute7(rel);
|
|
8113
8362
|
}
|
|
8114
8363
|
function validatePendingEntries(entries, config) {
|
|
8115
8364
|
const quarantineRoot = realpathOrNull(resolvePath11(config.quarantineDir));
|
|
@@ -8191,15 +8440,15 @@ import {
|
|
|
8191
8440
|
chmodSync as chmodSync3,
|
|
8192
8441
|
lstatSync as lstatSync6,
|
|
8193
8442
|
mkdirSync as mkdirSync9,
|
|
8194
|
-
readFileSync as
|
|
8443
|
+
readFileSync as readFileSync27,
|
|
8195
8444
|
readSync as readSync3,
|
|
8196
8445
|
writeFileSync as writeFileSync8
|
|
8197
8446
|
} from "fs";
|
|
8198
|
-
import { dirname as dirname13, isAbsolute as
|
|
8447
|
+
import { dirname as dirname13, isAbsolute as isAbsolute9, join as join37, relative as relative8, resolve as resolvePath12 } from "path";
|
|
8199
8448
|
|
|
8200
8449
|
// src/insights/inventory.ts
|
|
8201
|
-
import { readFileSync as
|
|
8202
|
-
import { join as
|
|
8450
|
+
import { readFileSync as readFileSync24 } from "fs";
|
|
8451
|
+
import { join as join33 } from "path";
|
|
8203
8452
|
|
|
8204
8453
|
// src/insights/models.ts
|
|
8205
8454
|
import { createHash as createHash5 } from "crypto";
|
|
@@ -8313,7 +8562,7 @@ function declaredMapCoverage(path, claim) {
|
|
|
8313
8562
|
}
|
|
8314
8563
|
function fingerprintInstalledSkill(path) {
|
|
8315
8564
|
try {
|
|
8316
|
-
return { fingerprint: stableHash(
|
|
8565
|
+
return { fingerprint: stableHash(readFileSync24(join33(path, "SKILL.md"), "utf-8")) };
|
|
8317
8566
|
} catch (error) {
|
|
8318
8567
|
return { reason: `could not fingerprint SKILL.md: ${error.message}` };
|
|
8319
8568
|
}
|
|
@@ -8463,12 +8712,12 @@ function buildInsightInventory(config, options = {}) {
|
|
|
8463
8712
|
}
|
|
8464
8713
|
|
|
8465
8714
|
// src/insights/jira.ts
|
|
8466
|
-
import { existsSync as
|
|
8467
|
-
import { join as
|
|
8715
|
+
import { existsSync as existsSync28, readFileSync as readFileSync26 } from "fs";
|
|
8716
|
+
import { join as join36 } from "path";
|
|
8468
8717
|
|
|
8469
8718
|
// src/insights/store.ts
|
|
8470
|
-
import { chmodSync as chmodSync2, existsSync as
|
|
8471
|
-
import { join as
|
|
8719
|
+
import { chmodSync as chmodSync2, existsSync as existsSync27, mkdirSync as mkdirSync8, readFileSync as readFileSync25 } from "fs";
|
|
8720
|
+
import { join as join35 } from "path";
|
|
8472
8721
|
|
|
8473
8722
|
// src/insights/sources.ts
|
|
8474
8723
|
import { createHash as createHash6 } from "crypto";
|
|
@@ -8480,10 +8729,10 @@ import {
|
|
|
8480
8729
|
openSync as openSync3,
|
|
8481
8730
|
readSync as readSync2,
|
|
8482
8731
|
readdirSync as readdirSync11,
|
|
8483
|
-
realpathSync as
|
|
8484
|
-
statSync as
|
|
8732
|
+
realpathSync as realpathSync10,
|
|
8733
|
+
statSync as statSync12
|
|
8485
8734
|
} from "fs";
|
|
8486
|
-
import { basename as basename12, extname as extname8, isAbsolute as
|
|
8735
|
+
import { basename as basename12, extname as extname8, isAbsolute as isAbsolute8, join as join34, relative as relative7, resolve as resolve9, sep as sep9 } from "path";
|
|
8487
8736
|
var DEFAULT_MAX_SOURCE_FILE_BYTES = 5 * 1024 * 1024;
|
|
8488
8737
|
var DEFAULT_MAX_SOURCE_TOTAL_BYTES = 25 * 1024 * 1024;
|
|
8489
8738
|
var SKIP_DIRS4 = /* @__PURE__ */ new Set([".git", "node_modules", "dist", "build", "coverage", ".next", "out", "__pycache__", ".venv", "venv"]);
|
|
@@ -8554,8 +8803,8 @@ function canonicalPublicUrl(raw, label2) {
|
|
|
8554
8803
|
return url.toString();
|
|
8555
8804
|
}
|
|
8556
8805
|
function isInside3(child, root) {
|
|
8557
|
-
const rel =
|
|
8558
|
-
return rel === "" || !rel.startsWith(`..${
|
|
8806
|
+
const rel = relative7(root, child);
|
|
8807
|
+
return rel === "" || !rel.startsWith(`..${sep9}`) && rel !== ".." && !isAbsolute8(rel);
|
|
8559
8808
|
}
|
|
8560
8809
|
function isHiddenName(name) {
|
|
8561
8810
|
return name.startsWith(".") && name !== "." && name !== "..";
|
|
@@ -8619,8 +8868,8 @@ function observeFile(path, relativePath, declaredMediaType, maxFileBytes, remain
|
|
|
8619
8868
|
if (!bytes || !descriptorStat) throw new UserError(`source file could not be observed: ${relativePath}`);
|
|
8620
8869
|
let realPath;
|
|
8621
8870
|
try {
|
|
8622
|
-
realPath =
|
|
8623
|
-
const pathStat =
|
|
8871
|
+
realPath = realpathSync10(path);
|
|
8872
|
+
const pathStat = statSync12(realPath);
|
|
8624
8873
|
if (pathStat.dev !== descriptorStat.dev || pathStat.ino !== descriptorStat.ino) {
|
|
8625
8874
|
throw new Error("path identity changed");
|
|
8626
8875
|
}
|
|
@@ -8741,7 +8990,7 @@ function normalizeStdin(input, retrievedAt, maxFileBytes, remainingBytes) {
|
|
|
8741
8990
|
};
|
|
8742
8991
|
}
|
|
8743
8992
|
function normalizeFile(input, retrievedAt, maxFileBytes, remainingBytes) {
|
|
8744
|
-
const resolved =
|
|
8993
|
+
const resolved = resolve9(input.path);
|
|
8745
8994
|
let lexicalStat;
|
|
8746
8995
|
try {
|
|
8747
8996
|
lexicalStat = lstatSync5(resolved);
|
|
@@ -8792,7 +9041,7 @@ function normalizeFile(input, retrievedAt, maxFileBytes, remainingBytes) {
|
|
|
8792
9041
|
};
|
|
8793
9042
|
}
|
|
8794
9043
|
function normalizeDirectory(input, retrievedAt, maxFileBytes, remainingBytes) {
|
|
8795
|
-
const resolved =
|
|
9044
|
+
const resolved = resolve9(input.path);
|
|
8796
9045
|
let lexicalStat;
|
|
8797
9046
|
try {
|
|
8798
9047
|
lexicalStat = lstatSync5(resolved);
|
|
@@ -8801,7 +9050,7 @@ function normalizeDirectory(input, retrievedAt, maxFileBytes, remainingBytes) {
|
|
|
8801
9050
|
}
|
|
8802
9051
|
if (lexicalStat.isSymbolicLink()) throw new UserError(`source directory must not be a symlink: ${resolved}`);
|
|
8803
9052
|
if (!lexicalStat.isDirectory()) throw new UserError(`source directory is not a directory: ${resolved}`);
|
|
8804
|
-
const realRoot =
|
|
9053
|
+
const realRoot = realpathSync10(resolved);
|
|
8805
9054
|
const observations = [];
|
|
8806
9055
|
const skipped = [];
|
|
8807
9056
|
const seenFiles = /* @__PURE__ */ new Set();
|
|
@@ -8815,7 +9064,7 @@ function normalizeDirectory(input, retrievedAt, maxFileBytes, remainingBytes) {
|
|
|
8815
9064
|
throw new UserError(`unable to enumerate source directory ${dir}: ${err.message}`);
|
|
8816
9065
|
}
|
|
8817
9066
|
for (const entry of entries) {
|
|
8818
|
-
const full =
|
|
9067
|
+
const full = join34(dir, entry.name);
|
|
8819
9068
|
const logicalPath = logicalDir ? `${logicalDir}/${entry.name}` : entry.name;
|
|
8820
9069
|
if (entry.isDirectory() && SKIP_DIRS4.has(entry.name)) {
|
|
8821
9070
|
skipped.push({ path: logicalPath, reason: "ignored-directory" });
|
|
@@ -8832,14 +9081,14 @@ function normalizeDirectory(input, retrievedAt, maxFileBytes, remainingBytes) {
|
|
|
8832
9081
|
if (entry.isSymbolicLink()) {
|
|
8833
9082
|
let realTarget;
|
|
8834
9083
|
try {
|
|
8835
|
-
realTarget =
|
|
9084
|
+
realTarget = realpathSync10(full);
|
|
8836
9085
|
} catch {
|
|
8837
9086
|
throw new UserError(`source directory contains an unreadable symlink: ${logicalPath}`);
|
|
8838
9087
|
}
|
|
8839
9088
|
if (!isInside3(realTarget, realRoot)) {
|
|
8840
9089
|
throw new UserError(`source directory symlink escapes its root: ${logicalPath}`);
|
|
8841
9090
|
}
|
|
8842
|
-
const targetStat =
|
|
9091
|
+
const targetStat = statSync12(realTarget);
|
|
8843
9092
|
if (targetStat.isDirectory()) {
|
|
8844
9093
|
if (seenDirs.has(realTarget)) continue;
|
|
8845
9094
|
seenDirs.add(realTarget);
|
|
@@ -8851,7 +9100,7 @@ function normalizeDirectory(input, retrievedAt, maxFileBytes, remainingBytes) {
|
|
|
8851
9100
|
continue;
|
|
8852
9101
|
}
|
|
8853
9102
|
} else if (entry.isDirectory()) {
|
|
8854
|
-
const realDir =
|
|
9103
|
+
const realDir = realpathSync10(full);
|
|
8855
9104
|
if (seenDirs.has(realDir)) continue;
|
|
8856
9105
|
seenDirs.add(realDir);
|
|
8857
9106
|
walk2(full, logicalPath);
|
|
@@ -8860,7 +9109,7 @@ function normalizeDirectory(input, retrievedAt, maxFileBytes, remainingBytes) {
|
|
|
8860
9109
|
skipped.push({ path: logicalPath, reason: "unsupported-media-type" });
|
|
8861
9110
|
continue;
|
|
8862
9111
|
}
|
|
8863
|
-
const realFile =
|
|
9112
|
+
const realFile = realpathSync10(full);
|
|
8864
9113
|
if (!isInside3(realFile, realRoot)) {
|
|
8865
9114
|
throw new UserError(`source directory entry escaped its root while being observed: ${logicalPath}`);
|
|
8866
9115
|
}
|
|
@@ -8967,17 +9216,17 @@ function normalizeSources(inputs, options = {}) {
|
|
|
8967
9216
|
// src/insights/store.ts
|
|
8968
9217
|
var STUDY_ID = /^STUDY-[0-9a-f]{16}$/;
|
|
8969
9218
|
function getInsightsDir() {
|
|
8970
|
-
return
|
|
9219
|
+
return join35(getHomeDir(), "insights");
|
|
8971
9220
|
}
|
|
8972
9221
|
function getStudyDir(studyId) {
|
|
8973
9222
|
assertStudyId(studyId);
|
|
8974
|
-
return
|
|
9223
|
+
return join35(getInsightsDir(), studyId);
|
|
8975
9224
|
}
|
|
8976
9225
|
function getStudyPath(studyId) {
|
|
8977
|
-
return
|
|
9226
|
+
return join35(getStudyDir(studyId), "study.json");
|
|
8978
9227
|
}
|
|
8979
9228
|
function getStudyEventsPath(studyId) {
|
|
8980
|
-
return
|
|
9229
|
+
return join35(getStudyDir(studyId), "events.jsonl");
|
|
8981
9230
|
}
|
|
8982
9231
|
function createStudyId(sourceInventoryHash, createdAt) {
|
|
8983
9232
|
return stableId("STUDY", { sourceInventoryHash, createdAt });
|
|
@@ -9108,7 +9357,7 @@ function validateStudyShape(value, path) {
|
|
|
9108
9357
|
function parseStudy(path) {
|
|
9109
9358
|
let raw;
|
|
9110
9359
|
try {
|
|
9111
|
-
raw =
|
|
9360
|
+
raw = readFileSync25(path, "utf-8");
|
|
9112
9361
|
} catch (err) {
|
|
9113
9362
|
throw new UserError(`unable to read insight study at ${path}: ${err.message}`);
|
|
9114
9363
|
}
|
|
@@ -9142,7 +9391,7 @@ function createInsightStudy(input, options = {}) {
|
|
|
9142
9391
|
const studyId = options.studyId ?? createStudyId(input.sourceInventoryHash, createdAt);
|
|
9143
9392
|
assertStudyId(studyId);
|
|
9144
9393
|
const path = getStudyPath(studyId);
|
|
9145
|
-
if (
|
|
9394
|
+
if (existsSync27(path)) throw new UserError(`insight study already exists: ${studyId}`);
|
|
9146
9395
|
const study = {
|
|
9147
9396
|
schemaVersion: INSIGHT_SCHEMA_VERSION,
|
|
9148
9397
|
studyId,
|
|
@@ -9163,7 +9412,7 @@ function createInsightStudy(input, options = {}) {
|
|
|
9163
9412
|
}
|
|
9164
9413
|
function readInsightStudy(studyId) {
|
|
9165
9414
|
const path = getStudyPath(studyId);
|
|
9166
|
-
if (!
|
|
9415
|
+
if (!existsSync27(path)) throw new UserError(`no insight study with id: ${studyId}`);
|
|
9167
9416
|
const study = parseStudy(path);
|
|
9168
9417
|
if (study.studyId !== studyId) {
|
|
9169
9418
|
throw new UserError(`malformed insight study at ${path}: stored studyId does not match its directory`);
|
|
@@ -9173,7 +9422,7 @@ function readInsightStudy(studyId) {
|
|
|
9173
9422
|
function writeInsightStudy(study, now = /* @__PURE__ */ new Date()) {
|
|
9174
9423
|
assertStudyId(study.studyId);
|
|
9175
9424
|
const path = getStudyPath(study.studyId);
|
|
9176
|
-
if (!
|
|
9425
|
+
if (!existsSync27(path)) throw new UserError(`no insight study with id: ${study.studyId}`);
|
|
9177
9426
|
const existing = parseStudy(path);
|
|
9178
9427
|
let next = { ...study, updatedAt: now.toISOString() };
|
|
9179
9428
|
if (next.status === "dismissed" || next.status === "archived") {
|
|
@@ -9910,7 +10159,7 @@ function buildJiraManifest(study, projectKey, options = {}) {
|
|
|
9910
10159
|
return manifest;
|
|
9911
10160
|
}
|
|
9912
10161
|
function getJiraReceiptsPath(studyId) {
|
|
9913
|
-
return
|
|
10162
|
+
return join36(getStudyDir(studyId), "jira-receipts.jsonl");
|
|
9914
10163
|
}
|
|
9915
10164
|
function assertCurrentJiraManifest(study, value) {
|
|
9916
10165
|
if (study.status !== "jira-manifested" || !study.jira) {
|
|
@@ -10042,10 +10291,10 @@ function appendJiraReceipt(studyId, manifest, input) {
|
|
|
10042
10291
|
function readJiraReceipts(studyId, manifest) {
|
|
10043
10292
|
const currentManifest = assertCurrentJiraManifest(readInsightStudy(studyId), manifest);
|
|
10044
10293
|
const path = getJiraReceiptsPath(studyId);
|
|
10045
|
-
if (!
|
|
10294
|
+
if (!existsSync28(path)) return [];
|
|
10046
10295
|
let text;
|
|
10047
10296
|
try {
|
|
10048
|
-
text =
|
|
10297
|
+
text = readFileSync26(path, "utf-8");
|
|
10049
10298
|
} catch (error) {
|
|
10050
10299
|
throw new UserError(`unable to read Jira receipts at ${path}: ${error.message}`);
|
|
10051
10300
|
}
|
|
@@ -10187,7 +10436,7 @@ function parseJsonFile(path, label2) {
|
|
|
10187
10436
|
const resolved = resolvePath12(path);
|
|
10188
10437
|
let raw;
|
|
10189
10438
|
try {
|
|
10190
|
-
raw =
|
|
10439
|
+
raw = readFileSync27(resolved, "utf-8");
|
|
10191
10440
|
} catch (error) {
|
|
10192
10441
|
throw new UserError(`unable to read ${label2} at ${resolved}: ${error.message}`);
|
|
10193
10442
|
}
|
|
@@ -10242,7 +10491,7 @@ function assertCommonDescriptorFields(value, label2) {
|
|
|
10242
10491
|
}
|
|
10243
10492
|
function pathRelativeTo(baseDir, raw, label2) {
|
|
10244
10493
|
const path = nonEmptyString(raw, label2);
|
|
10245
|
-
return
|
|
10494
|
+
return isAbsolute9(path) ? path : resolvePath12(baseDir, path);
|
|
10246
10495
|
}
|
|
10247
10496
|
function parseManifestDescriptor(value, index, baseDir) {
|
|
10248
10497
|
const label2 = `source manifest entry ${index + 1}`;
|
|
@@ -10332,10 +10581,10 @@ function collectSourceInputs(sourceArgs, options) {
|
|
|
10332
10581
|
return inputs;
|
|
10333
10582
|
}
|
|
10334
10583
|
function contextPath(studyId) {
|
|
10335
|
-
return
|
|
10584
|
+
return join37(getStudyDir(studyId), CONTEXT_FILENAME);
|
|
10336
10585
|
}
|
|
10337
10586
|
function inventoryPath(studyId) {
|
|
10338
|
-
return
|
|
10587
|
+
return join37(getStudyDir(studyId), INVENTORY_FILENAME);
|
|
10339
10588
|
}
|
|
10340
10589
|
function writeCommandContext(studyId, skillMapPath, skillMapCoverage, inventory) {
|
|
10341
10590
|
const context = {
|
|
@@ -10349,7 +10598,7 @@ function writeCommandContext(studyId, skillMapPath, skillMapCoverage, inventory)
|
|
|
10349
10598
|
function readCommandContext(studyId) {
|
|
10350
10599
|
const path = contextPath(studyId);
|
|
10351
10600
|
const parsed = parseJsonFile(path, "insight command context");
|
|
10352
|
-
if (!isRecord5(parsed) || parsed.schemaVersion !== 1 || parsed.skillMapPath !== void 0 && (typeof parsed.skillMapPath !== "string" || !
|
|
10601
|
+
if (!isRecord5(parsed) || parsed.schemaVersion !== 1 || parsed.skillMapPath !== void 0 && (typeof parsed.skillMapPath !== "string" || !isAbsolute9(parsed.skillMapPath)) || parsed.skillMapCoverage !== void 0 && parsed.skillMapCoverage !== "static" && parsed.skillMapCoverage !== "resolved" || parsed.skillMapCoverage !== void 0 && parsed.skillMapPath === void 0 || Object.keys(parsed).some((key) => !["schemaVersion", "skillMapPath", "skillMapCoverage"].includes(key))) {
|
|
10353
10602
|
throw new UserError(`malformed insight command context at ${path}`);
|
|
10354
10603
|
}
|
|
10355
10604
|
return parsed;
|
|
@@ -10373,18 +10622,18 @@ function readCurrentStudy(studyId) {
|
|
|
10373
10622
|
}
|
|
10374
10623
|
function retainedPath(root, relativePath) {
|
|
10375
10624
|
const candidate = resolvePath12(root, relativePath);
|
|
10376
|
-
const rel =
|
|
10377
|
-
if (rel.startsWith("..") ||
|
|
10625
|
+
const rel = relative8(root, candidate);
|
|
10626
|
+
if (rel.startsWith("..") || isAbsolute9(rel)) throw new UserError(`refusing retained source path outside the study`);
|
|
10378
10627
|
return candidate;
|
|
10379
10628
|
}
|
|
10380
10629
|
function retainObservedSources(study, observedFiles) {
|
|
10381
10630
|
if (observedFiles.length === 0) return study;
|
|
10382
|
-
const root =
|
|
10631
|
+
const root = join37(getStudyDir(study.studyId), "retained-sources");
|
|
10383
10632
|
mkdirSync9(root, { recursive: true, mode: 448 });
|
|
10384
10633
|
chmodSync3(root, 448);
|
|
10385
10634
|
const retainedSourceIds = /* @__PURE__ */ new Set();
|
|
10386
10635
|
for (const file of observedFiles) {
|
|
10387
|
-
const sourceRoot =
|
|
10636
|
+
const sourceRoot = join37(root, file.sourceId);
|
|
10388
10637
|
mkdirSync9(sourceRoot, { recursive: true, mode: 448 });
|
|
10389
10638
|
chmodSync3(sourceRoot, 448);
|
|
10390
10639
|
const destination = retainedPath(sourceRoot, file.relativePath);
|
|
@@ -10622,7 +10871,7 @@ async function insightJira(studyId, options) {
|
|
|
10622
10871
|
throw new UserError(`study ${studyId} cannot generate a Jira manifest: ${validation.blockers.map((issue) => issue.message).join("; ")}`);
|
|
10623
10872
|
}
|
|
10624
10873
|
if (localMode) {
|
|
10625
|
-
const privatePath2 =
|
|
10874
|
+
const privatePath2 = join37(getStudyDir(studyId), JIRA_MANIFEST_FILENAME);
|
|
10626
10875
|
const manifest2 = assertCurrentJiraManifest(study, parseJsonFile(privatePath2, "stored Jira manifest"));
|
|
10627
10876
|
const receipt = options.receipt === void 0 ? void 0 : appendJiraReceipt(studyId, manifest2, parseJiraReceiptInput(parseJsonFile(options.receipt, "Jira receipt import")));
|
|
10628
10877
|
const resume = buildJiraResumeState(manifest2, readJiraReceipts(studyId, manifest2));
|
|
@@ -10634,7 +10883,7 @@ async function insightJira(studyId, options) {
|
|
|
10634
10883
|
return;
|
|
10635
10884
|
}
|
|
10636
10885
|
const manifest = buildJiraManifest(study, projectKey);
|
|
10637
|
-
const privatePath =
|
|
10886
|
+
const privatePath = join37(getStudyDir(studyId), JIRA_MANIFEST_FILENAME);
|
|
10638
10887
|
writeJsonAtomic(privatePath, manifest);
|
|
10639
10888
|
if (options.out) {
|
|
10640
10889
|
writeNewFile(resolvePath12(options.out), `${JSON.stringify(manifest, null, 2)}
|
|
@@ -10705,14 +10954,11 @@ async function insightClose(studyId, options) {
|
|
|
10705
10954
|
}
|
|
10706
10955
|
|
|
10707
10956
|
// src/commands/refine.ts
|
|
10708
|
-
import {
|
|
10709
|
-
import { chmodSync as chmodSync4, existsSync as existsSync30, lstatSync as lstatSync7, mkdirSync as mkdirSync11, readFileSync as readFileSync26, realpathSync as realpathSync11, renameSync as renameSync6, rmSync as rmSync9, writeFileSync as writeFileSync10 } from "fs";
|
|
10710
|
-
import { dirname as dirname15, isAbsolute as isAbsolute9, join as join38, relative as relative8, resolve as resolvePath15, sep as sep8 } from "path";
|
|
10711
|
-
import { createInterface as createInterface7 } from "readline/promises";
|
|
10957
|
+
import { tmpdir as tmpdir3 } from "os";
|
|
10712
10958
|
|
|
10713
|
-
// src/refine/
|
|
10714
|
-
import {
|
|
10715
|
-
import {
|
|
10959
|
+
// src/refine/activation.ts
|
|
10960
|
+
import { lstatSync as lstatSync7, readFileSync as readFileSync28, readdirSync as readdirSync12, realpathSync as realpathSync11 } from "fs";
|
|
10961
|
+
import { join as join38 } from "path";
|
|
10716
10962
|
|
|
10717
10963
|
// src/refine/models.ts
|
|
10718
10964
|
import { randomBytes } from "crypto";
|
|
@@ -10780,88 +11026,449 @@ function contentSimilarity(a, b) {
|
|
|
10780
11026
|
return unionSize3 === 0 ? 0 : intersectionSize / unionSize3;
|
|
10781
11027
|
}
|
|
10782
11028
|
|
|
10783
|
-
// src/refine/
|
|
10784
|
-
|
|
10785
|
-
|
|
10786
|
-
|
|
10787
|
-
var
|
|
10788
|
-
var
|
|
10789
|
-
function
|
|
10790
|
-
|
|
10791
|
-
|
|
10792
|
-
|
|
10793
|
-
|
|
10794
|
-
|
|
10795
|
-
|
|
10796
|
-
|
|
10797
|
-
|
|
10798
|
-
|
|
10799
|
-
|
|
10800
|
-
|
|
10801
|
-
|
|
10802
|
-
|
|
10803
|
-
|
|
10804
|
-
|
|
10805
|
-
|
|
10806
|
-
|
|
11029
|
+
// src/refine/patch.ts
|
|
11030
|
+
var FENCE_RE = /^\s*(```|~~~)/;
|
|
11031
|
+
var HEADING_RE = /^(#{1,6})\s+(.*)$/;
|
|
11032
|
+
var ACTION_RE = /^<!--\s*ACTION:\s*([a-zA-Z-]+)(?:\s+"([^"]*)")?\s*-->\s*$/;
|
|
11033
|
+
var TARGET_RE = /^<!--\s*TARGET:\s*(.+?)\s*-->\s*$/;
|
|
11034
|
+
var ACTIONS_REQUIRING_MARKER = /* @__PURE__ */ new Set(["replace-section", "insert-after", "insert-before", "delete-section"]);
|
|
11035
|
+
function parsePatchFile(source) {
|
|
11036
|
+
const lines = source.split("\n");
|
|
11037
|
+
const blocks = [];
|
|
11038
|
+
let inFence = false;
|
|
11039
|
+
let current = null;
|
|
11040
|
+
const finalizeCurrent = () => {
|
|
11041
|
+
if (!current) return;
|
|
11042
|
+
const contentLines = current.contentLines;
|
|
11043
|
+
while (contentLines.length > 0 && contentLines[0].trim() === "") contentLines.shift();
|
|
11044
|
+
while (contentLines.length > 0 && contentLines[contentLines.length - 1].trim() === "") contentLines.pop();
|
|
11045
|
+
blocks.push({
|
|
11046
|
+
action: current.action,
|
|
11047
|
+
marker: current.marker,
|
|
11048
|
+
target: current.target,
|
|
11049
|
+
content: contentLines.join("\n")
|
|
11050
|
+
});
|
|
11051
|
+
current = null;
|
|
11052
|
+
};
|
|
11053
|
+
let i = 0;
|
|
11054
|
+
while (i < lines.length) {
|
|
11055
|
+
const line = lines[i];
|
|
11056
|
+
if (FENCE_RE.test(line)) {
|
|
11057
|
+
inFence = !inFence;
|
|
11058
|
+
if (current) current.contentLines.push(line);
|
|
11059
|
+
i++;
|
|
11060
|
+
continue;
|
|
11061
|
+
}
|
|
11062
|
+
if (!inFence) {
|
|
11063
|
+
const trimmed = line.trim();
|
|
11064
|
+
const actionMatch = ACTION_RE.exec(trimmed);
|
|
11065
|
+
if (actionMatch) {
|
|
11066
|
+
finalizeCurrent();
|
|
11067
|
+
const action = actionMatch[1];
|
|
11068
|
+
if (!isPatchAction(action)) {
|
|
11069
|
+
throw new UserError(`Unknown patch action "${action}" (valid: append, prepend, replace-section, insert-after, insert-before, delete-section)`);
|
|
11070
|
+
}
|
|
11071
|
+
const marker = actionMatch[2];
|
|
11072
|
+
if (ACTIONS_REQUIRING_MARKER.has(action) && (!marker || marker.trim().length === 0)) {
|
|
11073
|
+
throw new UserError(`Action "${action}" requires a quoted marker argument, e.g. <!-- ACTION: ${action} "marker text" -->`);
|
|
11074
|
+
}
|
|
11075
|
+
current = { action, marker: marker || void 0, contentLines: [] };
|
|
11076
|
+
i++;
|
|
11077
|
+
let peek = i;
|
|
11078
|
+
while (peek < lines.length && lines[peek].trim() === "") peek++;
|
|
11079
|
+
if (peek < lines.length) {
|
|
11080
|
+
const targetMatch = TARGET_RE.exec(lines[peek].trim());
|
|
11081
|
+
if (targetMatch) {
|
|
11082
|
+
current.target = targetMatch[1];
|
|
11083
|
+
i = peek + 1;
|
|
11084
|
+
}
|
|
11085
|
+
}
|
|
11086
|
+
continue;
|
|
11087
|
+
}
|
|
11088
|
+
}
|
|
11089
|
+
if (current) current.contentLines.push(line);
|
|
11090
|
+
i++;
|
|
10807
11091
|
}
|
|
10808
|
-
|
|
10809
|
-
|
|
10810
|
-
|
|
10811
|
-
throw new UserError(`malformed ${label2} at ${path}: not valid JSON`);
|
|
11092
|
+
finalizeCurrent();
|
|
11093
|
+
if (blocks.length === 0) {
|
|
11094
|
+
throw new UserError("No ACTION blocks found in patch source");
|
|
10812
11095
|
}
|
|
11096
|
+
return blocks;
|
|
10813
11097
|
}
|
|
10814
|
-
function
|
|
10815
|
-
if (
|
|
10816
|
-
|
|
11098
|
+
function frontmatterOffset(lines) {
|
|
11099
|
+
if (lines[0] !== "---") return 0;
|
|
11100
|
+
for (let i = 1; i < lines.length; i++) {
|
|
11101
|
+
if (lines[i] === "---") return i + 1;
|
|
10817
11102
|
}
|
|
10818
|
-
|
|
10819
|
-
|
|
10820
|
-
|
|
11103
|
+
return 0;
|
|
11104
|
+
}
|
|
11105
|
+
function buildHeadingTree(lines, bodyOffset) {
|
|
11106
|
+
const flat = [];
|
|
11107
|
+
let inFence = false;
|
|
11108
|
+
for (let i = bodyOffset; i < lines.length; i++) {
|
|
11109
|
+
const line = lines[i];
|
|
11110
|
+
if (FENCE_RE.test(line)) {
|
|
11111
|
+
inFence = !inFence;
|
|
11112
|
+
continue;
|
|
11113
|
+
}
|
|
11114
|
+
if (inFence) continue;
|
|
11115
|
+
const m = HEADING_RE.exec(line);
|
|
11116
|
+
if (m) flat.push({ level: m[1].length, text: m[2].trim(), line: i });
|
|
10821
11117
|
}
|
|
10822
|
-
const
|
|
10823
|
-
|
|
10824
|
-
|
|
11118
|
+
const root = { level: 0, heading: "", headingLine: bodyOffset - 1, bodyStart: bodyOffset, bodyEnd: lines.length, children: [] };
|
|
11119
|
+
const stack = [root];
|
|
11120
|
+
for (const h of flat) {
|
|
11121
|
+
while (stack.length > 1 && stack[stack.length - 1].level >= h.level) {
|
|
11122
|
+
const done = stack.pop();
|
|
11123
|
+
if (done) done.bodyEnd = h.line;
|
|
11124
|
+
}
|
|
11125
|
+
const node = { level: h.level, heading: h.text, headingLine: h.line, bodyStart: h.line + 1, bodyEnd: lines.length, children: [] };
|
|
11126
|
+
stack[stack.length - 1].children.push(node);
|
|
11127
|
+
stack.push(node);
|
|
10825
11128
|
}
|
|
10826
|
-
return
|
|
11129
|
+
return root;
|
|
10827
11130
|
}
|
|
10828
|
-
function
|
|
10829
|
-
|
|
10830
|
-
|
|
11131
|
+
function normalizeText(s) {
|
|
11132
|
+
return s.trim().replace(/\s+/g, " ");
|
|
11133
|
+
}
|
|
11134
|
+
function splitHeadingPath(target) {
|
|
11135
|
+
return target.split(">").map((segment) => segment.trim()).filter((segment) => segment.length > 0);
|
|
11136
|
+
}
|
|
11137
|
+
function collectDescendants(node, acc) {
|
|
11138
|
+
acc.push(node);
|
|
11139
|
+
for (const child of node.children) collectDescendants(child, acc);
|
|
11140
|
+
}
|
|
11141
|
+
function resolveHeadingPath(root, segments) {
|
|
11142
|
+
if (segments.length === 0) return root;
|
|
11143
|
+
const allNodes = [];
|
|
11144
|
+
collectDescendants(root, allNodes);
|
|
11145
|
+
const headingNodes = allNodes.filter((n) => n.level > 0);
|
|
11146
|
+
const firstNorm = normalizeText(segments[0]);
|
|
11147
|
+
let candidates = headingNodes.filter((n) => normalizeText(n.heading) === firstNorm);
|
|
11148
|
+
if (candidates.length === 0) {
|
|
11149
|
+
const available = headingNodes.map((n) => n.heading);
|
|
11150
|
+
throw new UserError(`No section matching "${segments[0]}" found (available headings: ${available.length > 0 ? available.join(", ") : "none"})`);
|
|
10831
11151
|
}
|
|
10832
|
-
|
|
10833
|
-
|
|
10834
|
-
|
|
11152
|
+
for (let i = 1; i < segments.length; i++) {
|
|
11153
|
+
const segment = segments[i];
|
|
11154
|
+
const norm = normalizeText(segment);
|
|
11155
|
+
const next = [];
|
|
11156
|
+
for (const candidate of candidates) {
|
|
11157
|
+
for (const child of candidate.children) {
|
|
11158
|
+
if (normalizeText(child.heading) === norm) next.push(child);
|
|
11159
|
+
}
|
|
11160
|
+
}
|
|
11161
|
+
if (next.length === 0) {
|
|
11162
|
+
const available = [...new Set(candidates.flatMap((c) => c.children.map((child) => child.heading)))];
|
|
11163
|
+
throw new UserError(`No section matching "${segment}" found (available headings: ${available.length > 0 ? available.join(", ") : "none"})`);
|
|
11164
|
+
}
|
|
11165
|
+
candidates = next;
|
|
10835
11166
|
}
|
|
10836
|
-
if (
|
|
10837
|
-
throw new UserError(`
|
|
11167
|
+
if (candidates.length > 1) {
|
|
11168
|
+
throw new UserError(`Target selector "${segments.join(" > ")}" matched ${candidates.length} sections; be more specific`);
|
|
10838
11169
|
}
|
|
10839
|
-
|
|
10840
|
-
|
|
11170
|
+
return candidates[0];
|
|
11171
|
+
}
|
|
11172
|
+
function findMarkerLine(lines, start, end, marker) {
|
|
11173
|
+
const norm = normalizeText(marker);
|
|
11174
|
+
const matches = [];
|
|
11175
|
+
for (let i = start; i < end; i++) {
|
|
11176
|
+
if (normalizeText(lines[i]).includes(norm)) matches.push(i);
|
|
10841
11177
|
}
|
|
10842
|
-
if (
|
|
10843
|
-
throw new UserError(`
|
|
11178
|
+
if (matches.length === 0) {
|
|
11179
|
+
throw new UserError(`No line matching marker "${marker}" found in target section`);
|
|
10844
11180
|
}
|
|
10845
|
-
|
|
10846
|
-
|
|
10847
|
-
|
|
10848
|
-
throw new UserError(`malformed refinements.json at ${path}: record "${r.id}" has overrideType "patch" but is missing "patchAction"`);
|
|
10849
|
-
}
|
|
10850
|
-
if (patchActionsRequiringMarker.has(r.patchAction) && (typeof r.patchMarker !== "string" || r.patchMarker.length === 0)) {
|
|
10851
|
-
throw new UserError(
|
|
10852
|
-
`malformed refinements.json at ${path}: record "${r.id}" has patchAction "${r.patchAction}" but is missing "patchMarker"`
|
|
10853
|
-
);
|
|
10854
|
-
}
|
|
10855
|
-
if (typeof r.patchContent !== "string" || r.patchContent.length === 0) {
|
|
10856
|
-
throw new UserError(`malformed refinements.json at ${path}: record "${r.id}" has overrideType "patch" but is missing "patchContent"`);
|
|
10857
|
-
}
|
|
10858
|
-
} else if (r.patchAction !== void 0) {
|
|
10859
|
-
throw new UserError(
|
|
10860
|
-
`malformed refinements.json at ${path}: record "${r.id}" has overrideType "${String(r.overrideType)}" but carries "patchAction" \u2014 only valid when overrideType is "patch"`
|
|
10861
|
-
);
|
|
11181
|
+
if (matches.length > 1) {
|
|
11182
|
+
const preview = matches.map((i) => `line ${i + 1}: "${lines[i].trim()}"`).join("; ");
|
|
11183
|
+
throw new UserError(`Marker "${marker}" matched ${matches.length} lines, expected exactly 1 (${preview})`);
|
|
10862
11184
|
}
|
|
11185
|
+
return matches[0];
|
|
10863
11186
|
}
|
|
10864
|
-
function
|
|
11187
|
+
function findChildByMarker(target, marker) {
|
|
11188
|
+
const norm = normalizeText(marker);
|
|
11189
|
+
const matches = target.children.filter((c) => normalizeText(c.heading).includes(norm));
|
|
11190
|
+
if (matches.length === 0) {
|
|
11191
|
+
const available = target.children.map((c) => c.heading).join(", ") || "none";
|
|
11192
|
+
throw new UserError(`No subsection matching "${marker}" found under target section (available: ${available})`);
|
|
11193
|
+
}
|
|
11194
|
+
if (matches.length > 1) {
|
|
11195
|
+
throw new UserError(`Marker "${marker}" matched ${matches.length} subsections: ${matches.map((c) => c.heading).join(", ")}`);
|
|
11196
|
+
}
|
|
11197
|
+
return matches[0];
|
|
11198
|
+
}
|
|
11199
|
+
function requireMarker(block) {
|
|
11200
|
+
if (!block.marker || block.marker.trim().length === 0) {
|
|
11201
|
+
throw new UserError(`Action "${block.action}" requires a marker`);
|
|
11202
|
+
}
|
|
11203
|
+
return block.marker;
|
|
11204
|
+
}
|
|
11205
|
+
function applyOneAction(lines, target, block) {
|
|
11206
|
+
const contentLines = block.content.length === 0 ? [""] : block.content.split("\n");
|
|
11207
|
+
switch (block.action) {
|
|
11208
|
+
case "append": {
|
|
11209
|
+
lines.splice(target.bodyEnd, 0, ...contentLines);
|
|
11210
|
+
return;
|
|
11211
|
+
}
|
|
11212
|
+
case "prepend": {
|
|
11213
|
+
lines.splice(target.bodyStart, 0, ...contentLines);
|
|
11214
|
+
return;
|
|
11215
|
+
}
|
|
11216
|
+
case "insert-after":
|
|
11217
|
+
case "insert-before": {
|
|
11218
|
+
const idx = findMarkerLine(lines, target.bodyStart, target.bodyEnd, requireMarker(block));
|
|
11219
|
+
lines.splice(block.action === "insert-after" ? idx + 1 : idx, 0, ...contentLines);
|
|
11220
|
+
return;
|
|
11221
|
+
}
|
|
11222
|
+
case "replace-section":
|
|
11223
|
+
case "delete-section": {
|
|
11224
|
+
const child = findChildByMarker(target, requireMarker(block));
|
|
11225
|
+
const replacement = block.action === "replace-section" ? contentLines : [];
|
|
11226
|
+
lines.splice(child.headingLine, child.bodyEnd - child.headingLine, ...replacement);
|
|
11227
|
+
return;
|
|
11228
|
+
}
|
|
11229
|
+
default: {
|
|
11230
|
+
const exhaustive = block.action;
|
|
11231
|
+
throw new UserError(`Unknown patch action "${exhaustive}"`);
|
|
11232
|
+
}
|
|
11233
|
+
}
|
|
11234
|
+
}
|
|
11235
|
+
function applyPatchBlocks(skillMd, blocks) {
|
|
11236
|
+
const lines = skillMd.split("\n");
|
|
11237
|
+
for (const block of blocks) {
|
|
11238
|
+
const bodyOffset = frontmatterOffset(lines);
|
|
11239
|
+
const root = buildHeadingTree(lines, bodyOffset);
|
|
11240
|
+
const segments = block.target ? splitHeadingPath(block.target) : [];
|
|
11241
|
+
const target = resolveHeadingPath(root, segments);
|
|
11242
|
+
applyOneAction(lines, target, block);
|
|
11243
|
+
}
|
|
11244
|
+
return lines.join("\n");
|
|
11245
|
+
}
|
|
11246
|
+
function renderExtendFile(meta) {
|
|
11247
|
+
return [
|
|
11248
|
+
"# SKILL.extend.md",
|
|
11249
|
+
`# Extensions for: ${meta.skillName}`,
|
|
11250
|
+
`# Generated: ${meta.generatedAt}`,
|
|
11251
|
+
`# Refinement: ${meta.refinementId}`,
|
|
11252
|
+
"",
|
|
11253
|
+
`## EXTEND: ${meta.targetSection}`,
|
|
11254
|
+
"<!-- ACTION: append -->",
|
|
11255
|
+
meta.content,
|
|
11256
|
+
""
|
|
11257
|
+
].join("\n");
|
|
11258
|
+
}
|
|
11259
|
+
function renderConfigOverride(meta) {
|
|
11260
|
+
const payload = {
|
|
11261
|
+
skill: meta.skillName,
|
|
11262
|
+
version: "1.0.0",
|
|
11263
|
+
generated: meta.generatedAt,
|
|
11264
|
+
config: meta.config
|
|
11265
|
+
};
|
|
11266
|
+
return `${JSON.stringify(payload, null, 2)}
|
|
11267
|
+
`;
|
|
11268
|
+
}
|
|
11269
|
+
function computeLcsDiff(a, b) {
|
|
11270
|
+
const n = a.length;
|
|
11271
|
+
const m = b.length;
|
|
11272
|
+
const dp = Array.from({ length: n + 1 }, () => new Array(m + 1).fill(0));
|
|
11273
|
+
for (let i2 = n - 1; i2 >= 0; i2--) {
|
|
11274
|
+
for (let j2 = m - 1; j2 >= 0; j2--) {
|
|
11275
|
+
dp[i2][j2] = a[i2] === b[j2] ? dp[i2 + 1][j2 + 1] + 1 : Math.max(dp[i2 + 1][j2], dp[i2][j2 + 1]);
|
|
11276
|
+
}
|
|
11277
|
+
}
|
|
11278
|
+
const ops = [];
|
|
11279
|
+
let i = 0;
|
|
11280
|
+
let j = 0;
|
|
11281
|
+
while (i < n && j < m) {
|
|
11282
|
+
if (a[i] === b[j]) {
|
|
11283
|
+
ops.push({ type: "context", line: a[i] });
|
|
11284
|
+
i++;
|
|
11285
|
+
j++;
|
|
11286
|
+
} else if (dp[i + 1][j] >= dp[i][j + 1]) {
|
|
11287
|
+
ops.push({ type: "remove", line: a[i] });
|
|
11288
|
+
i++;
|
|
11289
|
+
} else {
|
|
11290
|
+
ops.push({ type: "add", line: b[j] });
|
|
11291
|
+
j++;
|
|
11292
|
+
}
|
|
11293
|
+
}
|
|
11294
|
+
while (i < n) {
|
|
11295
|
+
ops.push({ type: "remove", line: a[i] });
|
|
11296
|
+
i++;
|
|
11297
|
+
}
|
|
11298
|
+
while (j < m) {
|
|
11299
|
+
ops.push({ type: "add", line: b[j] });
|
|
11300
|
+
j++;
|
|
11301
|
+
}
|
|
11302
|
+
return ops;
|
|
11303
|
+
}
|
|
11304
|
+
function previewDiff(before, after, path) {
|
|
11305
|
+
const a = before.split("\n");
|
|
11306
|
+
const b = after.split("\n");
|
|
11307
|
+
const ops = computeLcsDiff(a, b);
|
|
11308
|
+
const lines = [`--- ${path}`, `+++ ${path}`];
|
|
11309
|
+
for (const op of ops) {
|
|
11310
|
+
const prefix = op.type === "add" ? "+" : op.type === "remove" ? "-" : " ";
|
|
11311
|
+
lines.push(`${prefix}${op.line}`);
|
|
11312
|
+
}
|
|
11313
|
+
return lines.join("\n");
|
|
11314
|
+
}
|
|
11315
|
+
|
|
11316
|
+
// src/refine/activation.ts
|
|
11317
|
+
var OUTSIDE_RESOURCE = /\$\{?CLAUDE_PLUGIN_ROOT\b|(?:\]\(|["'\s])(?:\.\.\/)/;
|
|
11318
|
+
function activationFiles(baseDir, overrideDir, name) {
|
|
11319
|
+
const files = [];
|
|
11320
|
+
let total = 0;
|
|
11321
|
+
function walk2(dir, prefix = "") {
|
|
11322
|
+
for (const entry of readdirSync12(dir).sort()) {
|
|
11323
|
+
if (entry === ".git" || entry === "node_modules") continue;
|
|
11324
|
+
const path = join38(dir, entry), relativePath = prefix ? `${prefix}/${entry}` : entry;
|
|
11325
|
+
const stat = lstatSync7(path);
|
|
11326
|
+
if (stat.isSymbolicLink()) throw new UserError(`Activation refuses symlinks: ${path}`);
|
|
11327
|
+
if (stat.isDirectory()) {
|
|
11328
|
+
walk2(path, relativePath);
|
|
11329
|
+
continue;
|
|
11330
|
+
}
|
|
11331
|
+
if (!stat.isFile() || stat.size > 2 * 1024 * 1024 || files.length >= 1e3 || (total += stat.size) > 16 * 1024 * 1024) {
|
|
11332
|
+
throw new UserError("Activation source exceeds the bounded file limits (1000 files, 2 MiB/file, 16 MiB total) or contains a special file.");
|
|
11333
|
+
}
|
|
11334
|
+
const bytes = readFileSync28(path);
|
|
11335
|
+
if (OUTSIDE_RESOURCE.test(bytes.toString("utf8"))) {
|
|
11336
|
+
throw new UserError(`Activation needs a self-contained skill; ${relativePath} references its parent/plugin tree. Vendor the required resources first.`);
|
|
11337
|
+
}
|
|
11338
|
+
files.push({ relativePath, bytes, mode: stat.mode & 511 });
|
|
11339
|
+
}
|
|
11340
|
+
}
|
|
11341
|
+
walk2(realpathSync11(baseDir));
|
|
11342
|
+
const skill = files.find((f) => f.relativePath === "SKILL.md");
|
|
11343
|
+
if (!skill) throw new UserError("The explicit base directory must contain SKILL.md.");
|
|
11344
|
+
const artifacts = readdirSync12(overrideDir).sort();
|
|
11345
|
+
if (!artifacts.length || artifacts.some((p) => !["SKILL.patch.md", "SKILL.extend.md"].includes(p))) {
|
|
11346
|
+
throw new UserError("Activation supports a capture directory containing only SKILL.patch.md and/or SKILL.extend.md. Full/config/hook/script overrides require separate review.");
|
|
11347
|
+
}
|
|
11348
|
+
let content = skill.bytes.toString("utf8");
|
|
11349
|
+
for (const artifact of artifacts) {
|
|
11350
|
+
const path = join38(overrideDir, artifact);
|
|
11351
|
+
const stat = lstatSync7(path);
|
|
11352
|
+
if (!stat.isFile() || stat.isSymbolicLink()) throw new UserError("Override artifact must be a regular file, not a symlink.");
|
|
11353
|
+
if (stat.size > 2 * 1024 * 1024) throw new UserError("Override artifact exceeds 2 MiB.");
|
|
11354
|
+
const text = readFileSync28(path, "utf8");
|
|
11355
|
+
if (artifact === "SKILL.patch.md") content = applyPatchBlocks(content, parsePatchFile(text));
|
|
11356
|
+
else {
|
|
11357
|
+
const chunks = text.split(/^# SKILL\.extend\.md\r?$/m).filter((c) => c.trim());
|
|
11358
|
+
for (const chunk of chunks) {
|
|
11359
|
+
const match = /^\n# Extensions for: [^\n]+\n# Generated: [^\n]+\n# Refinement: [^\n]+\n\n## EXTEND: ([^\n]+)\n<!-- ACTION: append -->\n([\s\S]*)$/.exec(chunk);
|
|
11360
|
+
if (!match) throw new UserError("Unrecognized extend artifact; re-capture it or use an explicit patch.");
|
|
11361
|
+
content = applyPatchBlocks(content, [{ action: "append", target: match[1], content: match[2].trim() }]);
|
|
11362
|
+
}
|
|
11363
|
+
}
|
|
11364
|
+
}
|
|
11365
|
+
const frontmatter = /^---\r?\n([\s\S]*?)\r?\n---(?:\r?\n|$)/.exec(content);
|
|
11366
|
+
if (!frontmatter) throw new UserError("Host activation requires SKILL.md YAML frontmatter.");
|
|
11367
|
+
const header = frontmatter[1].replace(/^name:.*(?:\r?\n|$)/m, "");
|
|
11368
|
+
content = `---
|
|
11369
|
+
name: ${name}
|
|
11370
|
+
${header}
|
|
11371
|
+
---
|
|
11372
|
+
${content.slice(frontmatter[0].length)}`;
|
|
11373
|
+
if (OUTSIDE_RESOURCE.test(content)) {
|
|
11374
|
+
throw new UserError("Activation needs a self-contained skill; captured content references its parent/plugin tree.");
|
|
11375
|
+
}
|
|
11376
|
+
skill.bytes = Buffer.from(content);
|
|
11377
|
+
return files;
|
|
11378
|
+
}
|
|
11379
|
+
|
|
11380
|
+
// src/commands/refine.ts
|
|
11381
|
+
import { createHash as createHash8, randomUUID as randomUUID5 } from "crypto";
|
|
11382
|
+
import { chmodSync as chmodSync4, existsSync as existsSync31, lstatSync as lstatSync8, mkdirSync as mkdirSync11, mkdtempSync as mkdtempSync4, rmdirSync, readFileSync as readFileSync30, realpathSync as realpathSync14, renameSync as renameSync6, rmSync as rmSync9, writeFileSync as writeFileSync10 } from "fs";
|
|
11383
|
+
import { dirname as dirname15, isAbsolute as isAbsolute11, join as join41, relative as relative10, resolve as resolvePath15, sep as sep10 } from "path";
|
|
11384
|
+
import { createInterface as createInterface7 } from "readline/promises";
|
|
11385
|
+
|
|
11386
|
+
// src/refine/context.ts
|
|
11387
|
+
import { existsSync as existsSync30, realpathSync as realpathSync13, statSync as statSync14 } from "fs";
|
|
11388
|
+
import { isAbsolute as isAbsolute10, join as join40, relative as relative9, resolve as resolvePath14 } from "path";
|
|
11389
|
+
|
|
11390
|
+
// src/refine/store.ts
|
|
11391
|
+
import { createHash as createHash7 } from "crypto";
|
|
11392
|
+
import { existsSync as existsSync29, readFileSync as readFileSync29, statSync as statSync13, realpathSync as realpathSync12 } from "fs";
|
|
11393
|
+
import { join as join39, resolve as resolvePath13 } from "path";
|
|
11394
|
+
var SIMILARITY_JOIN_THRESHOLD = 0.7;
|
|
11395
|
+
var READY_THRESHOLD = 2;
|
|
11396
|
+
function getRefinementsDir() {
|
|
11397
|
+
return join39(getHomeDir(), "refinements");
|
|
11398
|
+
}
|
|
11399
|
+
function getRefinementsPath() {
|
|
11400
|
+
return join39(getRefinementsDir(), "refinements.json");
|
|
11401
|
+
}
|
|
11402
|
+
function getPatternsPath() {
|
|
11403
|
+
return join39(getRefinementsDir(), "patterns.json");
|
|
11404
|
+
}
|
|
11405
|
+
function getBackupsDir() {
|
|
11406
|
+
return join39(getRefinementsDir(), "backups");
|
|
11407
|
+
}
|
|
11408
|
+
function readJsonFile(path, label2) {
|
|
11409
|
+
let raw;
|
|
11410
|
+
try {
|
|
11411
|
+
raw = readFileSync29(path, "utf-8");
|
|
11412
|
+
} catch (err) {
|
|
11413
|
+
throw new UserError(`unable to read ${label2} at ${path}: ${err.message}`);
|
|
11414
|
+
}
|
|
11415
|
+
try {
|
|
11416
|
+
return JSON.parse(raw);
|
|
11417
|
+
} catch {
|
|
11418
|
+
throw new UserError(`malformed ${label2} at ${path}: not valid JSON`);
|
|
11419
|
+
}
|
|
11420
|
+
}
|
|
11421
|
+
function readStore(path, label2, validateRecord) {
|
|
11422
|
+
if (!existsSync29(path)) {
|
|
11423
|
+
return { schemaVersion: 1, records: [] };
|
|
11424
|
+
}
|
|
11425
|
+
const parsed = readJsonFile(path, label2);
|
|
11426
|
+
if (typeof parsed !== "object" || parsed === null || !Array.isArray(parsed.records)) {
|
|
11427
|
+
throw new UserError(`malformed ${label2} at ${path}: expected an object with a "records" array`);
|
|
11428
|
+
}
|
|
11429
|
+
const records = parsed.records;
|
|
11430
|
+
if (validateRecord) {
|
|
11431
|
+
records.forEach((record, index) => validateRecord(record, index));
|
|
11432
|
+
}
|
|
11433
|
+
return { schemaVersion: 1, records };
|
|
11434
|
+
}
|
|
11435
|
+
function validateRefinementRecordShape(record, index, path) {
|
|
11436
|
+
if (typeof record !== "object" || record === null) {
|
|
11437
|
+
throw new UserError(`malformed refinements.json at ${path}: record[${index}] is not an object`);
|
|
11438
|
+
}
|
|
11439
|
+
const r = record;
|
|
11440
|
+
if (typeof r.id !== "string" || r.id.length === 0) {
|
|
11441
|
+
throw new UserError(`malformed refinements.json at ${path}: record[${index}] is missing a valid "id"`);
|
|
11442
|
+
}
|
|
11443
|
+
if (typeof r.createdAt !== "string" || r.createdAt.length === 0) {
|
|
11444
|
+
throw new UserError(`malformed refinements.json at ${path}: record "${r.id}" is missing a valid "createdAt"`);
|
|
11445
|
+
}
|
|
11446
|
+
if (typeof r.status !== "string" || r.status.length === 0) {
|
|
11447
|
+
throw new UserError(`malformed refinements.json at ${path}: record "${r.id}" is missing a valid "status"`);
|
|
11448
|
+
}
|
|
11449
|
+
if (typeof r.skill !== "string" || r.skill.length === 0) {
|
|
11450
|
+
throw new UserError(`malformed refinements.json at ${path}: record "${r.id}" is missing a valid "skill"`);
|
|
11451
|
+
}
|
|
11452
|
+
const patchActionsRequiringMarker = /* @__PURE__ */ new Set(["insert-after", "insert-before", "replace-section", "delete-section"]);
|
|
11453
|
+
if (r.overrideType === "patch") {
|
|
11454
|
+
if (typeof r.patchAction !== "string" || r.patchAction.length === 0) {
|
|
11455
|
+
throw new UserError(`malformed refinements.json at ${path}: record "${r.id}" has overrideType "patch" but is missing "patchAction"`);
|
|
11456
|
+
}
|
|
11457
|
+
if (patchActionsRequiringMarker.has(r.patchAction) && (typeof r.patchMarker !== "string" || r.patchMarker.length === 0)) {
|
|
11458
|
+
throw new UserError(
|
|
11459
|
+
`malformed refinements.json at ${path}: record "${r.id}" has patchAction "${r.patchAction}" but is missing "patchMarker"`
|
|
11460
|
+
);
|
|
11461
|
+
}
|
|
11462
|
+
if (typeof r.patchContent !== "string" || r.patchContent.length === 0) {
|
|
11463
|
+
throw new UserError(`malformed refinements.json at ${path}: record "${r.id}" has overrideType "patch" but is missing "patchContent"`);
|
|
11464
|
+
}
|
|
11465
|
+
} else if (r.patchAction !== void 0) {
|
|
11466
|
+
throw new UserError(
|
|
11467
|
+
`malformed refinements.json at ${path}: record "${r.id}" has overrideType "${String(r.overrideType)}" but carries "patchAction" \u2014 only valid when overrideType is "patch"`
|
|
11468
|
+
);
|
|
11469
|
+
}
|
|
11470
|
+
}
|
|
11471
|
+
function validatePatternRecordShape(record, index, path) {
|
|
10865
11472
|
if (typeof record !== "object" || record === null) {
|
|
10866
11473
|
throw new UserError(`malformed patterns.json at ${path}: record[${index}] is not an object`);
|
|
10867
11474
|
}
|
|
@@ -10904,7 +11511,7 @@ function writePatternsStore(store) {
|
|
|
10904
11511
|
function resolveProjectRealpath(projectRoot) {
|
|
10905
11512
|
const resolved = resolvePath13(projectRoot);
|
|
10906
11513
|
try {
|
|
10907
|
-
return
|
|
11514
|
+
return realpathSync12(resolved);
|
|
10908
11515
|
} catch {
|
|
10909
11516
|
return resolved;
|
|
10910
11517
|
}
|
|
@@ -11036,16 +11643,16 @@ function createBackup(filePaths, deps = {}) {
|
|
|
11036
11643
|
const randomHex = deps.randomHex ?? defaultRandomHex;
|
|
11037
11644
|
const id = `${formatBackupTimestamp(now)}-${randomHex()}`;
|
|
11038
11645
|
const files = filePaths.map((path) => {
|
|
11039
|
-
if (!
|
|
11646
|
+
if (!existsSync29(path)) {
|
|
11040
11647
|
return { path, existed: false, sha256: null, mode: null, contentBase64: null };
|
|
11041
11648
|
}
|
|
11042
|
-
const bytes =
|
|
11043
|
-
const mode =
|
|
11649
|
+
const bytes = readFileSync29(path);
|
|
11650
|
+
const mode = statSync13(path).mode & 511;
|
|
11044
11651
|
const sha2562 = createHash7("sha256").update(bytes).digest("hex");
|
|
11045
11652
|
return { path, existed: true, sha256: sha2562, mode, contentBase64: bytes.toString("base64") };
|
|
11046
11653
|
});
|
|
11047
11654
|
const manifest = { id, createdAt: now.toISOString(), files };
|
|
11048
|
-
writeJsonAtomic(
|
|
11655
|
+
writeJsonAtomic(join39(getBackupsDir(), id, "manifest.json"), manifest);
|
|
11049
11656
|
return manifest;
|
|
11050
11657
|
}
|
|
11051
11658
|
function validateBackupManifestShape(value, path) {
|
|
@@ -11094,8 +11701,8 @@ function readBackupManifest(id) {
|
|
|
11094
11701
|
if (!isValidBackupId(id)) {
|
|
11095
11702
|
throw new UserError(`invalid backup id "${id}" \u2014 expected the format produced by \`skill-forge refine promote\` (e.g. 20260720T142305Z-a1b2)`);
|
|
11096
11703
|
}
|
|
11097
|
-
const manifestPath =
|
|
11098
|
-
if (!
|
|
11704
|
+
const manifestPath = join39(getBackupsDir(), id, "manifest.json");
|
|
11705
|
+
if (!existsSync29(manifestPath)) {
|
|
11099
11706
|
throw new UserError(`no backup with id: ${id}`);
|
|
11100
11707
|
}
|
|
11101
11708
|
const parsed = readJsonFile(manifestPath, "backup manifest");
|
|
@@ -11117,32 +11724,32 @@ function assertPlainSkillName(name) {
|
|
|
11117
11724
|
}
|
|
11118
11725
|
function realpathOrNull2(p) {
|
|
11119
11726
|
try {
|
|
11120
|
-
return
|
|
11727
|
+
return realpathSync13(p);
|
|
11121
11728
|
} catch {
|
|
11122
11729
|
return null;
|
|
11123
11730
|
}
|
|
11124
11731
|
}
|
|
11125
11732
|
function isUnderRoot2(candidate, root) {
|
|
11126
|
-
const rel =
|
|
11127
|
-
return rel === "" || !rel.startsWith("..") && !
|
|
11733
|
+
const rel = relative9(root, candidate);
|
|
11734
|
+
return rel === "" || !rel.startsWith("..") && !isAbsolute10(rel);
|
|
11128
11735
|
}
|
|
11129
11736
|
function resolveScopes(config, cwd, skill, userRootFlag) {
|
|
11130
11737
|
assertPlainSkillName(skill);
|
|
11131
11738
|
const resolvedCwd = resolvePath14(cwd);
|
|
11132
|
-
const localDir =
|
|
11133
|
-
const sharedDir =
|
|
11134
|
-
const local = { scope: "local", dir: localDir, exists:
|
|
11135
|
-
const shared = { scope: "shared", dir: sharedDir, exists:
|
|
11739
|
+
const localDir = join40(resolvedCwd, ".claude", "skills", skill);
|
|
11740
|
+
const sharedDir = join40(resolvedCwd, "skills", skill);
|
|
11741
|
+
const local = { scope: "local", dir: localDir, exists: existsSync30(localDir) };
|
|
11742
|
+
const shared = { scope: "shared", dir: sharedDir, exists: existsSync30(sharedDir) };
|
|
11136
11743
|
const user = resolveUserScope(config, resolvedCwd, skill, userRootFlag);
|
|
11137
11744
|
return { local, shared, user };
|
|
11138
11745
|
}
|
|
11139
11746
|
function resolveUserScope(config, resolvedCwd, skill, userRootFlag) {
|
|
11140
11747
|
if (userRootFlag) {
|
|
11141
11748
|
const root = resolvePath14(userRootFlag);
|
|
11142
|
-
return { resolved: true, dir:
|
|
11749
|
+
return { resolved: true, dir: join40(root, skill), root: userRootFlag, reason: "flag" };
|
|
11143
11750
|
}
|
|
11144
11751
|
const cwdReal = realpathOrNull2(resolvedCwd) ?? resolvedCwd;
|
|
11145
|
-
const candidates = config.skillsRoots.map((root) => ({ root, dir:
|
|
11752
|
+
const candidates = config.skillsRoots.map((root) => ({ root, dir: join40(resolveConfiguredRoot2(root), skill) }));
|
|
11146
11753
|
for (const candidate of candidates) {
|
|
11147
11754
|
const skillReal = realpathOrNull2(candidate.dir);
|
|
11148
11755
|
if (skillReal) {
|
|
@@ -11160,29 +11767,29 @@ function resolveUserScope(config, resolvedCwd, skill, userRootFlag) {
|
|
|
11160
11767
|
}
|
|
11161
11768
|
function isFile2(p) {
|
|
11162
11769
|
try {
|
|
11163
|
-
return
|
|
11770
|
+
return statSync14(p).isFile();
|
|
11164
11771
|
} catch {
|
|
11165
11772
|
return false;
|
|
11166
11773
|
}
|
|
11167
11774
|
}
|
|
11168
11775
|
function isDirectory(p) {
|
|
11169
11776
|
try {
|
|
11170
|
-
return
|
|
11777
|
+
return statSync14(p).isDirectory();
|
|
11171
11778
|
} catch {
|
|
11172
11779
|
return false;
|
|
11173
11780
|
}
|
|
11174
11781
|
}
|
|
11175
|
-
function inventoryScopeDir(
|
|
11782
|
+
function inventoryScopeDir(scope2, dir) {
|
|
11176
11783
|
return {
|
|
11177
|
-
scope,
|
|
11784
|
+
scope: scope2,
|
|
11178
11785
|
dir,
|
|
11179
11786
|
dirExists: isDirectory(dir),
|
|
11180
|
-
skillMd: isFile2(
|
|
11181
|
-
patchMd: isFile2(
|
|
11182
|
-
extendMd: isFile2(
|
|
11183
|
-
configJson: isFile2(
|
|
11184
|
-
hooksDir: isDirectory(
|
|
11185
|
-
scriptsDir: isDirectory(
|
|
11787
|
+
skillMd: isFile2(join40(dir, "SKILL.md")),
|
|
11788
|
+
patchMd: isFile2(join40(dir, "SKILL.patch.md")),
|
|
11789
|
+
extendMd: isFile2(join40(dir, "SKILL.extend.md")),
|
|
11790
|
+
configJson: isFile2(join40(dir, "skill-config.json")),
|
|
11791
|
+
hooksDir: isDirectory(join40(dir, "hooks")),
|
|
11792
|
+
scriptsDir: isDirectory(join40(dir, "scripts"))
|
|
11186
11793
|
};
|
|
11187
11794
|
}
|
|
11188
11795
|
function inventoryAllScopes(resolution) {
|
|
@@ -11193,230 +11800,10 @@ function inventoryAllScopes(resolution) {
|
|
|
11193
11800
|
return rows;
|
|
11194
11801
|
}
|
|
11195
11802
|
|
|
11196
|
-
// src/refine/patch.ts
|
|
11197
|
-
var FENCE_RE = /^\s*(```|~~~)/;
|
|
11198
|
-
var HEADING_RE = /^(#{1,6})\s+(.*)$/;
|
|
11199
|
-
function frontmatterOffset(lines) {
|
|
11200
|
-
if (lines[0] !== "---") return 0;
|
|
11201
|
-
for (let i = 1; i < lines.length; i++) {
|
|
11202
|
-
if (lines[i] === "---") return i + 1;
|
|
11203
|
-
}
|
|
11204
|
-
return 0;
|
|
11205
|
-
}
|
|
11206
|
-
function buildHeadingTree(lines, bodyOffset) {
|
|
11207
|
-
const flat = [];
|
|
11208
|
-
let inFence = false;
|
|
11209
|
-
for (let i = bodyOffset; i < lines.length; i++) {
|
|
11210
|
-
const line = lines[i];
|
|
11211
|
-
if (FENCE_RE.test(line)) {
|
|
11212
|
-
inFence = !inFence;
|
|
11213
|
-
continue;
|
|
11214
|
-
}
|
|
11215
|
-
if (inFence) continue;
|
|
11216
|
-
const m = HEADING_RE.exec(line);
|
|
11217
|
-
if (m) flat.push({ level: m[1].length, text: m[2].trim(), line: i });
|
|
11218
|
-
}
|
|
11219
|
-
const root = { level: 0, heading: "", headingLine: bodyOffset - 1, bodyStart: bodyOffset, bodyEnd: lines.length, children: [] };
|
|
11220
|
-
const stack = [root];
|
|
11221
|
-
for (const h of flat) {
|
|
11222
|
-
while (stack.length > 1 && stack[stack.length - 1].level >= h.level) {
|
|
11223
|
-
const done = stack.pop();
|
|
11224
|
-
if (done) done.bodyEnd = h.line;
|
|
11225
|
-
}
|
|
11226
|
-
const node = { level: h.level, heading: h.text, headingLine: h.line, bodyStart: h.line + 1, bodyEnd: lines.length, children: [] };
|
|
11227
|
-
stack[stack.length - 1].children.push(node);
|
|
11228
|
-
stack.push(node);
|
|
11229
|
-
}
|
|
11230
|
-
return root;
|
|
11231
|
-
}
|
|
11232
|
-
function normalizeText(s) {
|
|
11233
|
-
return s.trim().replace(/\s+/g, " ");
|
|
11234
|
-
}
|
|
11235
|
-
function splitHeadingPath(target) {
|
|
11236
|
-
return target.split(">").map((segment) => segment.trim()).filter((segment) => segment.length > 0);
|
|
11237
|
-
}
|
|
11238
|
-
function collectDescendants(node, acc) {
|
|
11239
|
-
acc.push(node);
|
|
11240
|
-
for (const child of node.children) collectDescendants(child, acc);
|
|
11241
|
-
}
|
|
11242
|
-
function resolveHeadingPath(root, segments) {
|
|
11243
|
-
if (segments.length === 0) return root;
|
|
11244
|
-
const allNodes = [];
|
|
11245
|
-
collectDescendants(root, allNodes);
|
|
11246
|
-
const headingNodes = allNodes.filter((n) => n.level > 0);
|
|
11247
|
-
const firstNorm = normalizeText(segments[0]);
|
|
11248
|
-
let candidates = headingNodes.filter((n) => normalizeText(n.heading) === firstNorm);
|
|
11249
|
-
if (candidates.length === 0) {
|
|
11250
|
-
const available = headingNodes.map((n) => n.heading);
|
|
11251
|
-
throw new UserError(`No section matching "${segments[0]}" found (available headings: ${available.length > 0 ? available.join(", ") : "none"})`);
|
|
11252
|
-
}
|
|
11253
|
-
for (let i = 1; i < segments.length; i++) {
|
|
11254
|
-
const segment = segments[i];
|
|
11255
|
-
const norm = normalizeText(segment);
|
|
11256
|
-
const next = [];
|
|
11257
|
-
for (const candidate of candidates) {
|
|
11258
|
-
for (const child of candidate.children) {
|
|
11259
|
-
if (normalizeText(child.heading) === norm) next.push(child);
|
|
11260
|
-
}
|
|
11261
|
-
}
|
|
11262
|
-
if (next.length === 0) {
|
|
11263
|
-
const available = [...new Set(candidates.flatMap((c) => c.children.map((child) => child.heading)))];
|
|
11264
|
-
throw new UserError(`No section matching "${segment}" found (available headings: ${available.length > 0 ? available.join(", ") : "none"})`);
|
|
11265
|
-
}
|
|
11266
|
-
candidates = next;
|
|
11267
|
-
}
|
|
11268
|
-
if (candidates.length > 1) {
|
|
11269
|
-
throw new UserError(`Target selector "${segments.join(" > ")}" matched ${candidates.length} sections; be more specific`);
|
|
11270
|
-
}
|
|
11271
|
-
return candidates[0];
|
|
11272
|
-
}
|
|
11273
|
-
function findMarkerLine(lines, start, end, marker) {
|
|
11274
|
-
const norm = normalizeText(marker);
|
|
11275
|
-
const matches = [];
|
|
11276
|
-
for (let i = start; i < end; i++) {
|
|
11277
|
-
if (normalizeText(lines[i]).includes(norm)) matches.push(i);
|
|
11278
|
-
}
|
|
11279
|
-
if (matches.length === 0) {
|
|
11280
|
-
throw new UserError(`No line matching marker "${marker}" found in target section`);
|
|
11281
|
-
}
|
|
11282
|
-
if (matches.length > 1) {
|
|
11283
|
-
const preview = matches.map((i) => `line ${i + 1}: "${lines[i].trim()}"`).join("; ");
|
|
11284
|
-
throw new UserError(`Marker "${marker}" matched ${matches.length} lines, expected exactly 1 (${preview})`);
|
|
11285
|
-
}
|
|
11286
|
-
return matches[0];
|
|
11287
|
-
}
|
|
11288
|
-
function findChildByMarker(target, marker) {
|
|
11289
|
-
const norm = normalizeText(marker);
|
|
11290
|
-
const matches = target.children.filter((c) => normalizeText(c.heading).includes(norm));
|
|
11291
|
-
if (matches.length === 0) {
|
|
11292
|
-
const available = target.children.map((c) => c.heading).join(", ") || "none";
|
|
11293
|
-
throw new UserError(`No subsection matching "${marker}" found under target section (available: ${available})`);
|
|
11294
|
-
}
|
|
11295
|
-
if (matches.length > 1) {
|
|
11296
|
-
throw new UserError(`Marker "${marker}" matched ${matches.length} subsections: ${matches.map((c) => c.heading).join(", ")}`);
|
|
11297
|
-
}
|
|
11298
|
-
return matches[0];
|
|
11299
|
-
}
|
|
11300
|
-
function requireMarker(block) {
|
|
11301
|
-
if (!block.marker || block.marker.trim().length === 0) {
|
|
11302
|
-
throw new UserError(`Action "${block.action}" requires a marker`);
|
|
11303
|
-
}
|
|
11304
|
-
return block.marker;
|
|
11305
|
-
}
|
|
11306
|
-
function applyOneAction(lines, target, block) {
|
|
11307
|
-
const contentLines = block.content.length === 0 ? [""] : block.content.split("\n");
|
|
11308
|
-
switch (block.action) {
|
|
11309
|
-
case "append": {
|
|
11310
|
-
lines.splice(target.bodyEnd, 0, ...contentLines);
|
|
11311
|
-
return;
|
|
11312
|
-
}
|
|
11313
|
-
case "prepend": {
|
|
11314
|
-
lines.splice(target.bodyStart, 0, ...contentLines);
|
|
11315
|
-
return;
|
|
11316
|
-
}
|
|
11317
|
-
case "insert-after":
|
|
11318
|
-
case "insert-before": {
|
|
11319
|
-
const idx = findMarkerLine(lines, target.bodyStart, target.bodyEnd, requireMarker(block));
|
|
11320
|
-
lines.splice(block.action === "insert-after" ? idx + 1 : idx, 0, ...contentLines);
|
|
11321
|
-
return;
|
|
11322
|
-
}
|
|
11323
|
-
case "replace-section":
|
|
11324
|
-
case "delete-section": {
|
|
11325
|
-
const child = findChildByMarker(target, requireMarker(block));
|
|
11326
|
-
const replacement = block.action === "replace-section" ? contentLines : [];
|
|
11327
|
-
lines.splice(child.headingLine, child.bodyEnd - child.headingLine, ...replacement);
|
|
11328
|
-
return;
|
|
11329
|
-
}
|
|
11330
|
-
default: {
|
|
11331
|
-
const exhaustive = block.action;
|
|
11332
|
-
throw new UserError(`Unknown patch action "${exhaustive}"`);
|
|
11333
|
-
}
|
|
11334
|
-
}
|
|
11335
|
-
}
|
|
11336
|
-
function applyPatchBlocks(skillMd, blocks) {
|
|
11337
|
-
const lines = skillMd.split("\n");
|
|
11338
|
-
for (const block of blocks) {
|
|
11339
|
-
const bodyOffset = frontmatterOffset(lines);
|
|
11340
|
-
const root = buildHeadingTree(lines, bodyOffset);
|
|
11341
|
-
const segments = block.target ? splitHeadingPath(block.target) : [];
|
|
11342
|
-
const target = resolveHeadingPath(root, segments);
|
|
11343
|
-
applyOneAction(lines, target, block);
|
|
11344
|
-
}
|
|
11345
|
-
return lines.join("\n");
|
|
11346
|
-
}
|
|
11347
|
-
function renderExtendFile(meta) {
|
|
11348
|
-
return [
|
|
11349
|
-
"# SKILL.extend.md",
|
|
11350
|
-
`# Extensions for: ${meta.skillName}`,
|
|
11351
|
-
`# Generated: ${meta.generatedAt}`,
|
|
11352
|
-
`# Refinement: ${meta.refinementId}`,
|
|
11353
|
-
"",
|
|
11354
|
-
`## EXTEND: ${meta.targetSection}`,
|
|
11355
|
-
"<!-- ACTION: append -->",
|
|
11356
|
-
meta.content,
|
|
11357
|
-
""
|
|
11358
|
-
].join("\n");
|
|
11359
|
-
}
|
|
11360
|
-
function renderConfigOverride(meta) {
|
|
11361
|
-
const payload = {
|
|
11362
|
-
skill: meta.skillName,
|
|
11363
|
-
version: "1.0.0",
|
|
11364
|
-
generated: meta.generatedAt,
|
|
11365
|
-
config: meta.config
|
|
11366
|
-
};
|
|
11367
|
-
return `${JSON.stringify(payload, null, 2)}
|
|
11368
|
-
`;
|
|
11369
|
-
}
|
|
11370
|
-
function computeLcsDiff(a, b) {
|
|
11371
|
-
const n = a.length;
|
|
11372
|
-
const m = b.length;
|
|
11373
|
-
const dp = Array.from({ length: n + 1 }, () => new Array(m + 1).fill(0));
|
|
11374
|
-
for (let i2 = n - 1; i2 >= 0; i2--) {
|
|
11375
|
-
for (let j2 = m - 1; j2 >= 0; j2--) {
|
|
11376
|
-
dp[i2][j2] = a[i2] === b[j2] ? dp[i2 + 1][j2 + 1] + 1 : Math.max(dp[i2 + 1][j2], dp[i2][j2 + 1]);
|
|
11377
|
-
}
|
|
11378
|
-
}
|
|
11379
|
-
const ops = [];
|
|
11380
|
-
let i = 0;
|
|
11381
|
-
let j = 0;
|
|
11382
|
-
while (i < n && j < m) {
|
|
11383
|
-
if (a[i] === b[j]) {
|
|
11384
|
-
ops.push({ type: "context", line: a[i] });
|
|
11385
|
-
i++;
|
|
11386
|
-
j++;
|
|
11387
|
-
} else if (dp[i + 1][j] >= dp[i][j + 1]) {
|
|
11388
|
-
ops.push({ type: "remove", line: a[i] });
|
|
11389
|
-
i++;
|
|
11390
|
-
} else {
|
|
11391
|
-
ops.push({ type: "add", line: b[j] });
|
|
11392
|
-
j++;
|
|
11393
|
-
}
|
|
11394
|
-
}
|
|
11395
|
-
while (i < n) {
|
|
11396
|
-
ops.push({ type: "remove", line: a[i] });
|
|
11397
|
-
i++;
|
|
11398
|
-
}
|
|
11399
|
-
while (j < m) {
|
|
11400
|
-
ops.push({ type: "add", line: b[j] });
|
|
11401
|
-
j++;
|
|
11402
|
-
}
|
|
11403
|
-
return ops;
|
|
11404
|
-
}
|
|
11405
|
-
function previewDiff(before, after, path) {
|
|
11406
|
-
const a = before.split("\n");
|
|
11407
|
-
const b = after.split("\n");
|
|
11408
|
-
const ops = computeLcsDiff(a, b);
|
|
11409
|
-
const lines = [`--- ${path}`, `+++ ${path}`];
|
|
11410
|
-
for (const op of ops) {
|
|
11411
|
-
const prefix = op.type === "add" ? "+" : op.type === "remove" ? "-" : " ";
|
|
11412
|
-
lines.push(`${prefix}${op.line}`);
|
|
11413
|
-
}
|
|
11414
|
-
return lines.join("\n");
|
|
11415
|
-
}
|
|
11416
|
-
|
|
11417
11803
|
// src/commands/refine.ts
|
|
11418
11804
|
var PRO_FEATURE_LABEL9 = "skill refinement";
|
|
11419
11805
|
var REFINE_PROMPT_ASSET = "refine-prompt.md";
|
|
11806
|
+
var PROJECT_SKILL_ROOTS = { claude: ".claude/skills", codex: ".agents/skills" };
|
|
11420
11807
|
function requireProFeature3() {
|
|
11421
11808
|
const status2 = proFeatureStatus(PRO_FEATURE_LABEL9);
|
|
11422
11809
|
if (!status2.enabled) {
|
|
@@ -11454,15 +11841,15 @@ function resolveConfiguredRoot3(root) {
|
|
|
11454
11841
|
return resolvePath15(expandHome(root));
|
|
11455
11842
|
}
|
|
11456
11843
|
function isUnderRoot3(candidate, root) {
|
|
11457
|
-
const rel =
|
|
11458
|
-
return rel === "" || !rel.startsWith("..") && !
|
|
11844
|
+
const rel = relative10(root, candidate);
|
|
11845
|
+
return rel === "" || !rel.startsWith("..") && !isAbsolute11(rel);
|
|
11459
11846
|
}
|
|
11460
11847
|
function sanitizeDiffForConsole(diff) {
|
|
11461
11848
|
return diff.split("\n").map((line) => sanitizeForMarkdown(line)).join("\n");
|
|
11462
11849
|
}
|
|
11463
11850
|
function resolveContentOption(options) {
|
|
11464
11851
|
if (options.content !== void 0) return options.content;
|
|
11465
|
-
if (options.contentFile) return
|
|
11852
|
+
if (options.contentFile) return readFileSync30(resolvePath15(options.contentFile), "utf-8");
|
|
11466
11853
|
return void 0;
|
|
11467
11854
|
}
|
|
11468
11855
|
function validateCaptureOptions(options) {
|
|
@@ -11588,7 +11975,7 @@ ${rendered}` : rendered;
|
|
|
11588
11975
|
function ensureScopeDirSafe(scopeDir, resolvedCwd) {
|
|
11589
11976
|
let lst;
|
|
11590
11977
|
try {
|
|
11591
|
-
lst =
|
|
11978
|
+
lst = lstatSync8(scopeDir);
|
|
11592
11979
|
} catch {
|
|
11593
11980
|
lst = void 0;
|
|
11594
11981
|
}
|
|
@@ -11600,7 +11987,7 @@ function ensureScopeDirSafe(scopeDir, resolvedCwd) {
|
|
|
11600
11987
|
const lexical = resolvePath15(scopeDir);
|
|
11601
11988
|
let realResolved;
|
|
11602
11989
|
try {
|
|
11603
|
-
realResolved =
|
|
11990
|
+
realResolved = realpathSync14(scopeDir);
|
|
11604
11991
|
} catch {
|
|
11605
11992
|
realResolved = real;
|
|
11606
11993
|
}
|
|
@@ -11614,16 +12001,16 @@ function ensureScopeDirSafe(scopeDir, resolvedCwd) {
|
|
|
11614
12001
|
}
|
|
11615
12002
|
function writeOverrideFile(scopeDir, relPath, content, resolvedCwd) {
|
|
11616
12003
|
const realScopeDir = ensureScopeDirSafe(scopeDir, resolvedCwd);
|
|
11617
|
-
const destPath =
|
|
12004
|
+
const destPath = join41(scopeDir, relPath);
|
|
11618
12005
|
const destDir = dirname15(destPath);
|
|
11619
12006
|
mkdirSync11(destDir, { recursive: true });
|
|
11620
|
-
const realDestDir =
|
|
12007
|
+
const realDestDir = realpathSync14(destDir);
|
|
11621
12008
|
if (!isUnderRoot3(realDestDir, realScopeDir)) {
|
|
11622
12009
|
throw new UserError(`refusing to write ${destPath}: resolves outside scope directory ${scopeDir}`);
|
|
11623
12010
|
}
|
|
11624
12011
|
let destLst;
|
|
11625
12012
|
try {
|
|
11626
|
-
destLst =
|
|
12013
|
+
destLst = lstatSync8(destPath);
|
|
11627
12014
|
} catch {
|
|
11628
12015
|
destLst = void 0;
|
|
11629
12016
|
}
|
|
@@ -11710,15 +12097,105 @@ Write ${preview.destPath}? [y/N] `);
|
|
|
11710
12097
|
}
|
|
11711
12098
|
function renderCaptureArtifactPreview(opts, content, refinementId, generatedAt, scopeDir) {
|
|
11712
12099
|
const probeRelPath = opts.overrideType === "patch" ? "SKILL.patch.md" : opts.overrideType === "extend" || opts.overrideType === "new" ? "SKILL.extend.md" : void 0;
|
|
11713
|
-
const probePath = probeRelPath ?
|
|
11714
|
-
const existingContent = probePath &&
|
|
12100
|
+
const probePath = probeRelPath ? join41(scopeDir, probeRelPath) : void 0;
|
|
12101
|
+
const existingContent = probePath && existsSync31(probePath) ? readFileSync30(probePath, "utf-8") : "";
|
|
11715
12102
|
const rendered = renderCaptureArtifact(opts, content, refinementId, generatedAt, existingContent);
|
|
11716
12103
|
const relPath = rendered.relPath;
|
|
11717
|
-
const destPath =
|
|
11718
|
-
const before =
|
|
12104
|
+
const destPath = join41(scopeDir, relPath);
|
|
12105
|
+
const before = existsSync31(destPath) ? readFileSync30(destPath, "utf-8") : "";
|
|
11719
12106
|
const diff = previewDiff(before, rendered.content, destPath);
|
|
11720
12107
|
return { relPath, content: rendered.content, destPath, diff };
|
|
11721
12108
|
}
|
|
12109
|
+
async function refineActivateCommand(options) {
|
|
12110
|
+
requireProFeature3();
|
|
12111
|
+
if (!options.skill || !options.base || !options.as || !["claude", "codex"].includes(options.host ?? "")) {
|
|
12112
|
+
throw new UserError("activate requires --skill, --base, --as and --host claude|codex.");
|
|
12113
|
+
}
|
|
12114
|
+
if (!/^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(options.as)) throw new UserError("--as must be a lowercase hyphenated skill name.");
|
|
12115
|
+
if (options.scope && !["local", "shared"].includes(options.scope)) throw new UserError("--scope must be local|shared.");
|
|
12116
|
+
const cwd = realpathSync14(process.cwd());
|
|
12117
|
+
const scopes = resolveScopes(loadConfig(), cwd, options.skill);
|
|
12118
|
+
const overrideDir = options.scope === "shared" ? scopes.shared.dir : scopes.local.dir;
|
|
12119
|
+
if (realpathSync14(overrideDir) !== overrideDir) throw new UserError("Activation refuses a symlinked override directory.");
|
|
12120
|
+
const hostRoot = join41(cwd, PROJECT_SKILL_ROOTS[options.host]);
|
|
12121
|
+
const destination = join41(hostRoot, options.as);
|
|
12122
|
+
if (existsSync31(destination) || resolveTargetRealPath(destination) !== destination) throw new UserError("Activation destination already exists or contains a symlink; choose a new --as name.");
|
|
12123
|
+
const files = activationFiles(options.base, overrideDir, options.as);
|
|
12124
|
+
const receiptName = ".skill-forge-activation.json";
|
|
12125
|
+
if (files.some((f) => f.relativePath === receiptName)) throw new UserError("Base is already an activation; use its original source and capture.");
|
|
12126
|
+
const provenance = {
|
|
12127
|
+
schemaVersion: 1,
|
|
12128
|
+
host: options.host,
|
|
12129
|
+
skill: options.as,
|
|
12130
|
+
capture: options.skill,
|
|
12131
|
+
source: realpathSync14(options.base),
|
|
12132
|
+
sourceSha256: createHash8("sha256").update(readFileSync30(join41(options.base, "SKILL.md"))).digest("hex"),
|
|
12133
|
+
generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
12134
|
+
runtimeVerified: false
|
|
12135
|
+
};
|
|
12136
|
+
files.push({ relativePath: receiptName, mode: 384, bytes: Buffer.from(JSON.stringify(provenance, null, 2) + "\n") });
|
|
12137
|
+
if (files.length > 1e3 || files.some((f) => f.bytes.length > 2 * 1024 * 1024) || files.reduce((n, f) => n + f.bytes.length, 0) > 16 * 1024 * 1024) {
|
|
12138
|
+
throw new UserError("Composed activation exceeds file or total size limits.");
|
|
12139
|
+
}
|
|
12140
|
+
const stage = mkdtempSync4(join41(tmpdir3(), "skill-forge-activate-"));
|
|
12141
|
+
try {
|
|
12142
|
+
for (const file of files) {
|
|
12143
|
+
const path = join41(stage, file.relativePath);
|
|
12144
|
+
mkdirSync11(dirname15(path), { recursive: true });
|
|
12145
|
+
writeFileSync10(path, file.bytes, { mode: file.mode });
|
|
12146
|
+
}
|
|
12147
|
+
const safety = await scanSafety(stage, "block-high", files.map((f) => ({ file: join41(stage, f.relativePath), content: f.bytes.toString("utf8") })));
|
|
12148
|
+
safety.findings = safety.findings.map((f) => ({ ...f, file: join41(destination, relative10(stage, f.file)) }));
|
|
12149
|
+
const preview = {
|
|
12150
|
+
destination,
|
|
12151
|
+
provenance,
|
|
12152
|
+
host: options.host,
|
|
12153
|
+
invocation: options.host === "claude" ? `/${options.as}` : `$${options.as}`,
|
|
12154
|
+
source: realpathSync14(options.base),
|
|
12155
|
+
files: files.map((f) => f.relativePath),
|
|
12156
|
+
safety,
|
|
12157
|
+
diff: previewDiff(readFileSync30(join41(options.base, "SKILL.md"), "utf8"), files.find((f) => f.relativePath === "SKILL.md").bytes.toString("utf8"), join41(destination, "SKILL.md"))
|
|
12158
|
+
};
|
|
12159
|
+
if (options.dryRun) {
|
|
12160
|
+
emit2(options.json, { ...preview, dryRun: true, activated: false }, () => console.log(JSON.stringify(preview, null, 2)));
|
|
12161
|
+
return;
|
|
12162
|
+
}
|
|
12163
|
+
if (safety.verdict === "block") throw new UserError(`Activation safety gate blocked the staged skill. Inspect --dry-run --json findings; nothing activated.`);
|
|
12164
|
+
if (!options.json) console.log(sanitizeDiffForConsole(preview.diff));
|
|
12165
|
+
if (!await confirmWrite(options.yes, options.json, `Activate ${destination}? [y/N] `)) {
|
|
12166
|
+
emit2(options.json, { activated: false, cancelled: true }, () => console.log("Activation cancelled."));
|
|
12167
|
+
return;
|
|
12168
|
+
}
|
|
12169
|
+
if (resolveTargetRealPath(destination) !== destination) throw new UserError("Activation destination became symlinked.");
|
|
12170
|
+
ensureScopeDirSafe(hostRoot, cwd);
|
|
12171
|
+
mkdirSync11(destination);
|
|
12172
|
+
const backup = createBackup(files.map((f) => join41(destination, f.relativePath)));
|
|
12173
|
+
const hashes = files.map((f) => ({ path: join41(destination, f.relativePath), sha256: createHash8("sha256").update(f.bytes).digest("hex") }));
|
|
12174
|
+
writePostWriteSidecar(backup.id, hashes);
|
|
12175
|
+
const projectConfig = { ...loadConfig(), skillsRoots: [hostRoot] };
|
|
12176
|
+
try {
|
|
12177
|
+
for (const file of files) safeRestoreWrite(join41(destination, file.relativePath), file.bytes, file.mode, projectConfig);
|
|
12178
|
+
for (const file of hashes) {
|
|
12179
|
+
if (createHash8("sha256").update(readFileSync30(file.path)).digest("hex") !== file.sha256) throw new UserError("Activation read-back hash mismatch.");
|
|
12180
|
+
}
|
|
12181
|
+
} catch (error) {
|
|
12182
|
+
throw new UserError(`Activation write failed; backup ${backup.id} records recovery paths. Inspect before using rollback --project --force: ${error.message}`);
|
|
12183
|
+
}
|
|
12184
|
+
emit2(options.json, {
|
|
12185
|
+
...preview,
|
|
12186
|
+
activated: true,
|
|
12187
|
+
backupId: backup.id,
|
|
12188
|
+
runtimeVerified: false,
|
|
12189
|
+
verification: `Open a fresh ${options.host} task in this project, invoke ${preview.invocation}, and verify it reads ${join41(destination, "SKILL.md")}.`,
|
|
12190
|
+
rollback: `skill-forge refine rollback ${backup.id} --project --yes`
|
|
12191
|
+
}, () => {
|
|
12192
|
+
console.log(`Activated ${destination}. Invoke ${preview.invocation} in a fresh ${options.host} task and verify the loaded file; runtime use is not yet observed.`);
|
|
12193
|
+
console.log(`Rollback from this project: skill-forge refine rollback ${backup.id} --project --yes`);
|
|
12194
|
+
});
|
|
12195
|
+
} finally {
|
|
12196
|
+
rmSync9(stage, { recursive: true, force: true });
|
|
12197
|
+
}
|
|
12198
|
+
}
|
|
11722
12199
|
async function refineListCommand(options = {}) {
|
|
11723
12200
|
requireProFeature3();
|
|
11724
12201
|
const { records } = readRefinementsStore();
|
|
@@ -11812,16 +12289,16 @@ async function refineWhichCommand(skill, options = {}) {
|
|
|
11812
12289
|
}
|
|
11813
12290
|
function writePostWriteSidecar(backupId, files) {
|
|
11814
12291
|
const sidecar = { schemaVersion: 1, files };
|
|
11815
|
-
const dir =
|
|
12292
|
+
const dir = join41(getBackupsDir(), backupId);
|
|
11816
12293
|
mkdirSync11(dir, { recursive: true });
|
|
11817
|
-
writeFileSync10(
|
|
12294
|
+
writeFileSync10(join41(dir, "post.json"), JSON.stringify(sidecar, null, 2) + "\n", { mode: 384 });
|
|
11818
12295
|
}
|
|
11819
12296
|
function readPostWriteSidecar(backupId) {
|
|
11820
|
-
const path =
|
|
11821
|
-
if (!
|
|
12297
|
+
const path = join41(getBackupsDir(), backupId, "post.json");
|
|
12298
|
+
if (!existsSync31(path)) return { hashes: /* @__PURE__ */ new Map(), status: "missing" };
|
|
11822
12299
|
let raw;
|
|
11823
12300
|
try {
|
|
11824
|
-
raw =
|
|
12301
|
+
raw = readFileSync30(path, "utf-8");
|
|
11825
12302
|
} catch {
|
|
11826
12303
|
return { hashes: /* @__PURE__ */ new Map(), status: "unreadable" };
|
|
11827
12304
|
}
|
|
@@ -11866,21 +12343,21 @@ async function refinePromoteCommand(patternId, options = {}) {
|
|
|
11866
12343
|
const resolvedUserRoot = resolveConfiguredRoot3(scopes.user.root);
|
|
11867
12344
|
let realUserRoot;
|
|
11868
12345
|
try {
|
|
11869
|
-
realUserRoot =
|
|
12346
|
+
realUserRoot = realpathSync14(resolvedUserRoot);
|
|
11870
12347
|
} catch {
|
|
11871
12348
|
realUserRoot = resolvedUserRoot;
|
|
11872
12349
|
}
|
|
11873
12350
|
let realSkillDir;
|
|
11874
12351
|
try {
|
|
11875
|
-
realSkillDir =
|
|
12352
|
+
realSkillDir = realpathSync14(skillDir);
|
|
11876
12353
|
} catch {
|
|
11877
12354
|
realSkillDir = resolvePath15(skillDir);
|
|
11878
12355
|
}
|
|
11879
12356
|
if (!isUnderRoot3(realSkillDir, realUserRoot)) {
|
|
11880
12357
|
throw new UserError(`refusing to promote: resolved skill directory ${skillDir} is outside the configured user scope root ${scopes.user.root}`);
|
|
11881
12358
|
}
|
|
11882
|
-
const skillMdPath =
|
|
11883
|
-
if (!
|
|
12359
|
+
const skillMdPath = join41(skillDir, "SKILL.md");
|
|
12360
|
+
if (!existsSync31(skillMdPath)) {
|
|
11884
12361
|
throw new UserError(`user-scope SKILL.md not found at ${skillMdPath} \u2014 refusing to promote against a missing base skill.`);
|
|
11885
12362
|
}
|
|
11886
12363
|
const refinementsStore = readRefinementsStore();
|
|
@@ -11897,7 +12374,7 @@ async function refinePromoteCommand(patternId, options = {}) {
|
|
|
11897
12374
|
`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.`
|
|
11898
12375
|
);
|
|
11899
12376
|
}
|
|
11900
|
-
const before =
|
|
12377
|
+
const before = readFileSync30(skillMdPath, "utf-8");
|
|
11901
12378
|
const after = applyPatchBlocks(before, [block]);
|
|
11902
12379
|
const diff = previewDiff(before, after, skillMdPath);
|
|
11903
12380
|
if (options.dryRun) {
|
|
@@ -11929,7 +12406,7 @@ Promote pattern ${patternId} into ${skillMdPath}? [y/N] `);
|
|
|
11929
12406
|
throw err;
|
|
11930
12407
|
}
|
|
11931
12408
|
try {
|
|
11932
|
-
const postSha256 = createHash8("sha256").update(
|
|
12409
|
+
const postSha256 = createHash8("sha256").update(readFileSync30(skillMdPath)).digest("hex");
|
|
11933
12410
|
writePostWriteSidecar(backup.id, [{ path: skillMdPath, sha256: postSha256 }]);
|
|
11934
12411
|
markGeneralized(patternId, backup.id);
|
|
11935
12412
|
const skillsRoot = resolveConfiguredRoot3(scopes.user.root);
|
|
@@ -11955,11 +12432,11 @@ Promote pattern ${patternId} into ${skillMdPath}? [y/N] `);
|
|
|
11955
12432
|
}
|
|
11956
12433
|
function assertBackupDirContained(backupId) {
|
|
11957
12434
|
const backupsDir = getBackupsDir();
|
|
11958
|
-
if (!
|
|
11959
|
-
const realBackupsDir =
|
|
11960
|
-
const backupDir =
|
|
11961
|
-
if (!
|
|
11962
|
-
const realBackupDir =
|
|
12435
|
+
if (!existsSync31(backupsDir)) return;
|
|
12436
|
+
const realBackupsDir = realpathSync14(backupsDir);
|
|
12437
|
+
const backupDir = join41(backupsDir, backupId);
|
|
12438
|
+
if (!existsSync31(backupDir)) return;
|
|
12439
|
+
const realBackupDir = realpathSync14(backupDir);
|
|
11963
12440
|
if (!isUnderRoot3(realBackupDir, realBackupsDir)) {
|
|
11964
12441
|
throw new UserError(`refusing rollback: backup id "${backupId}" resolves outside the backups directory`);
|
|
11965
12442
|
}
|
|
@@ -11968,9 +12445,9 @@ function resolveTargetRealPath(path) {
|
|
|
11968
12445
|
let lexicalAncestor = resolvePath15(path);
|
|
11969
12446
|
while (true) {
|
|
11970
12447
|
try {
|
|
11971
|
-
const real =
|
|
11972
|
-
const remainder =
|
|
11973
|
-
return remainder === "" ? real :
|
|
12448
|
+
const real = realpathSync14(lexicalAncestor);
|
|
12449
|
+
const remainder = relative10(lexicalAncestor, resolvePath15(path));
|
|
12450
|
+
return remainder === "" ? real : join41(real, remainder);
|
|
11974
12451
|
} catch {
|
|
11975
12452
|
const parent = dirname15(lexicalAncestor);
|
|
11976
12453
|
if (parent === lexicalAncestor) return resolvePath15(path);
|
|
@@ -11985,7 +12462,7 @@ function assertManifestPathsWithinRoots(manifest, config) {
|
|
|
11985
12462
|
const roots = config.skillsRoots.map((root) => {
|
|
11986
12463
|
const resolved = resolveConfiguredRoot3(root);
|
|
11987
12464
|
try {
|
|
11988
|
-
return
|
|
12465
|
+
return realpathSync14(resolved);
|
|
11989
12466
|
} catch {
|
|
11990
12467
|
return resolved;
|
|
11991
12468
|
}
|
|
@@ -12002,7 +12479,7 @@ function assertNoSymlinkLeaves(manifest) {
|
|
|
12002
12479
|
for (const file of manifest.files) {
|
|
12003
12480
|
let lst;
|
|
12004
12481
|
try {
|
|
12005
|
-
lst =
|
|
12482
|
+
lst = lstatSync8(file.path);
|
|
12006
12483
|
} catch {
|
|
12007
12484
|
lst = void 0;
|
|
12008
12485
|
}
|
|
@@ -12015,11 +12492,11 @@ function findLexicalRoot(filePath, config) {
|
|
|
12015
12492
|
const lexicalTarget = resolvePath15(filePath);
|
|
12016
12493
|
for (const root of config.skillsRoots) {
|
|
12017
12494
|
const rootLexical = resolveConfiguredRoot3(root);
|
|
12018
|
-
const rel =
|
|
12019
|
-
if (rel !== "" && (rel === ".." || rel.startsWith(`..${
|
|
12495
|
+
const rel = relative10(rootLexical, lexicalTarget);
|
|
12496
|
+
if (rel !== "" && (rel === ".." || rel.startsWith(`..${sep10}`) || isAbsolute11(rel))) continue;
|
|
12020
12497
|
let rootReal;
|
|
12021
12498
|
try {
|
|
12022
|
-
rootReal =
|
|
12499
|
+
rootReal = realpathSync14(rootLexical);
|
|
12023
12500
|
} catch {
|
|
12024
12501
|
rootReal = rootLexical;
|
|
12025
12502
|
}
|
|
@@ -12029,8 +12506,8 @@ function findLexicalRoot(filePath, config) {
|
|
|
12029
12506
|
}
|
|
12030
12507
|
function walkRealComponents(filePath, config, createMissing) {
|
|
12031
12508
|
const { rootLexical, rootReal } = findLexicalRoot(filePath, config);
|
|
12032
|
-
const relPath =
|
|
12033
|
-
const components = relPath.split(
|
|
12509
|
+
const relPath = relative10(rootLexical, resolvePath15(filePath));
|
|
12510
|
+
const components = relPath.split(sep10).filter((c) => c.length > 0);
|
|
12034
12511
|
if (components.length === 0) {
|
|
12035
12512
|
throw new UserError(`refusing to operate on ${filePath}: resolves to the skills root itself`);
|
|
12036
12513
|
}
|
|
@@ -12039,10 +12516,10 @@ function walkRealComponents(filePath, config, createMissing) {
|
|
|
12039
12516
|
let current = rootReal;
|
|
12040
12517
|
for (const component of dirComponents) {
|
|
12041
12518
|
if (current === void 0) break;
|
|
12042
|
-
const next =
|
|
12519
|
+
const next = join41(current, component);
|
|
12043
12520
|
let lst;
|
|
12044
12521
|
try {
|
|
12045
|
-
lst =
|
|
12522
|
+
lst = lstatSync8(next);
|
|
12046
12523
|
} catch {
|
|
12047
12524
|
lst = void 0;
|
|
12048
12525
|
}
|
|
@@ -12070,13 +12547,13 @@ function safeRestoreWrite(filePath, contentBuffer, mode, config) {
|
|
|
12070
12547
|
if (parentReal === void 0) {
|
|
12071
12548
|
throw new UserError(`refusing to restore ${filePath}: could not establish a real parent directory`);
|
|
12072
12549
|
}
|
|
12073
|
-
const finalPath =
|
|
12550
|
+
const finalPath = join41(parentReal, leafComponent);
|
|
12074
12551
|
if (!isUnderRoot3(finalPath, rootReal)) {
|
|
12075
12552
|
throw new UserError(`refusing to restore ${filePath}: resolves outside root ${rootReal}`);
|
|
12076
12553
|
}
|
|
12077
12554
|
let leafLst;
|
|
12078
12555
|
try {
|
|
12079
|
-
leafLst =
|
|
12556
|
+
leafLst = lstatSync8(finalPath);
|
|
12080
12557
|
} catch {
|
|
12081
12558
|
leafLst = void 0;
|
|
12082
12559
|
}
|
|
@@ -12096,13 +12573,13 @@ function safeRestoreWrite(filePath, contentBuffer, mode, config) {
|
|
|
12096
12573
|
function safeDeleteTombstone(filePath, config) {
|
|
12097
12574
|
const { parentReal, leafComponent, rootReal } = walkRealComponents(filePath, config, false);
|
|
12098
12575
|
if (parentReal === void 0) return;
|
|
12099
|
-
const finalPath =
|
|
12576
|
+
const finalPath = join41(parentReal, leafComponent);
|
|
12100
12577
|
if (!isUnderRoot3(finalPath, rootReal)) {
|
|
12101
12578
|
throw new UserError(`refusing to delete ${filePath}: resolves outside root ${rootReal}`);
|
|
12102
12579
|
}
|
|
12103
12580
|
let leafLst;
|
|
12104
12581
|
try {
|
|
12105
|
-
leafLst =
|
|
12582
|
+
leafLst = lstatSync8(finalPath);
|
|
12106
12583
|
} catch {
|
|
12107
12584
|
leafLst = void 0;
|
|
12108
12585
|
}
|
|
@@ -12121,6 +12598,13 @@ async function refineRollbackCommand(backupId, options = {}) {
|
|
|
12121
12598
|
assertBackupDirContained(backupId);
|
|
12122
12599
|
const manifest = readBackupManifest(backupId);
|
|
12123
12600
|
const config = loadConfig();
|
|
12601
|
+
if (options.project) config.skillsRoots = Object.values(PROJECT_SKILL_ROOTS).map((p) => join41(realpathSync14(process.cwd()), p));
|
|
12602
|
+
const assertProjectRoots = () => {
|
|
12603
|
+
if (options.project && config.skillsRoots.some((root) => resolveTargetRealPath(root) !== root)) {
|
|
12604
|
+
throw new UserError("refusing project rollback: a project skill root or its ancestor is symlinked.");
|
|
12605
|
+
}
|
|
12606
|
+
};
|
|
12607
|
+
assertProjectRoots();
|
|
12124
12608
|
assertManifestPathsWithinRoots(manifest, config);
|
|
12125
12609
|
assertNoSymlinkLeaves(manifest);
|
|
12126
12610
|
const warnings = [];
|
|
@@ -12134,11 +12618,14 @@ async function refineRollbackCommand(backupId, options = {}) {
|
|
|
12134
12618
|
}
|
|
12135
12619
|
warnings.push(`post-write verification data for backup ${backupId} is ${reason} \u2014 restoring without hash verification (--force).`);
|
|
12136
12620
|
}
|
|
12621
|
+
if (sidecar.status === "ok" && manifest.files.some((f) => !sidecar.hashes.has(f.path)) && !options.force) {
|
|
12622
|
+
throw new UserError("refusing rollback: post-write verification is incomplete for the backup manifest.");
|
|
12623
|
+
}
|
|
12137
12624
|
const mismatches = [];
|
|
12138
12625
|
for (const file of manifest.files) {
|
|
12139
12626
|
const expected = sidecar.hashes.get(file.path);
|
|
12140
12627
|
if (expected === void 0) continue;
|
|
12141
|
-
const currentSha256 =
|
|
12628
|
+
const currentSha256 = existsSync31(file.path) ? createHash8("sha256").update(readFileSync30(file.path)).digest("hex") : null;
|
|
12142
12629
|
if (currentSha256 !== expected) mismatches.push(file.path);
|
|
12143
12630
|
}
|
|
12144
12631
|
if (mismatches.length > 0 && !options.force) {
|
|
@@ -12165,6 +12652,7 @@ ${fileList}
|
|
|
12165
12652
|
const restored = [];
|
|
12166
12653
|
for (const file of manifest.files) {
|
|
12167
12654
|
try {
|
|
12655
|
+
assertProjectRoots();
|
|
12168
12656
|
if (!file.existed) {
|
|
12169
12657
|
safeDeleteTombstone(file.path, config);
|
|
12170
12658
|
} else {
|
|
@@ -12178,6 +12666,18 @@ ${fileList}
|
|
|
12178
12666
|
);
|
|
12179
12667
|
}
|
|
12180
12668
|
}
|
|
12669
|
+
if (options.project) {
|
|
12670
|
+
const roots = new Set(config.skillsRoots);
|
|
12671
|
+
for (const file of manifest.files) {
|
|
12672
|
+
for (let dir = dirname15(file.path); !roots.has(dir) && dirname15(dir) !== dir; dir = dirname15(dir)) {
|
|
12673
|
+
try {
|
|
12674
|
+
rmdirSync(dir);
|
|
12675
|
+
} catch {
|
|
12676
|
+
break;
|
|
12677
|
+
}
|
|
12678
|
+
}
|
|
12679
|
+
}
|
|
12680
|
+
}
|
|
12181
12681
|
const patternsStore = readPatternsStore();
|
|
12182
12682
|
const pattern = patternsStore.records.find((p) => p.generalizedBackupId === backupId);
|
|
12183
12683
|
if (pattern) {
|
|
@@ -12283,7 +12783,7 @@ configCmd.command("propose <key> <value>").description("Queue a preference for r
|
|
|
12283
12783
|
configCmd.command("review").description("Review pending agent-proposed preferences one at a time").option("--accept-all", "accept every pending proposal without prompting").option("--reject-all", "reject every pending proposal without prompting").action(configReview);
|
|
12284
12784
|
program.command("guide [topic]").description("What this tool does, what state you are in, and the next command to run").action(guide);
|
|
12285
12785
|
program.command("evolve <target-skill-dir>").description("Self-evolve a skill via SkillOpt-Sleep, re-gate the proposal, then promote/hold/reject (Pro)").option("--project <dir>", "project dir passed to SkillOpt-Sleep as --project (defaults to cwd)").option("--dry-run", "SkillOpt-Sleep dry-run: report only, nothing staged").option("--backend <name>", "SkillOpt-Sleep backend (default: mock, offline \u2014 a real backend requires --yes or interactive confirmation)").option("--lookback-hours <n>", "hours of session history for SkillOpt-Sleep to harvest", parseLookbackHours).option("-y, --yes", "skip interactive prompts; honor the re-gate verdict automatically").option("--json", "print the re-gate result as JSON instead of the terminal report").option("--force", "allow re-adopting a staging dir whose proposed content already matches the live skill").action(evolveCommand);
|
|
12286
|
-
program.command("audit").alias("doctor").description("Doctor-style health check over the configured skill/MCP set: inventory, hygiene findings, and consolidation/refinement opportunities").option("--json", "print the audit report as JSON instead of the terminal summary (non-interactive: skips the business-profile prompt)").option("-y, --yes", "skip interactive prompts (business profile, foundation, handoff); never implies --foundation or --handoff").option("--report <file>", "write the report to this path instead of the default ~/.skill-forge/reports/audit-<timestamp>.md (refuses an existing path)").option("--foundation", "authorizes exactly one skills-root write: scaffold a business-foundation skill from the captured business profile").option("--handoff", "authorizes launching the configured agent with the bundled curation prompt against the written report").option("--claude-plugins", "include enabled Claude Code plugins' declared skills in this audit (default: config.claudePlugins, else off)").option("--no-claude-plugins", "exclude enabled Claude Code plugins from this audit even when config.claudePlugins is on").option("--usage-snapshot <file>", "join per-skill usage counts from a skill-monitor snapshot JSON file or a directory of them (default: config.usageSnapshotPath)").option("--usage-window <days>", "restrict directory-input usage-snapshot selection to a snapshot with this window_days (integer)", parseUsageWindow).action(auditCommand);
|
|
12786
|
+
program.command("audit").option("--compare <file>", "compare with a prior full audit JSON; preserve all current findings").alias("doctor").description("Doctor-style health check over the configured skill/MCP set: inventory, hygiene findings, and consolidation/refinement opportunities").option("--json", "print the audit report as JSON instead of the terminal summary (non-interactive: skips the business-profile prompt)").option("-y, --yes", "skip interactive prompts (business profile, foundation, handoff); never implies --foundation or --handoff").option("--report <file>", "write the report to this path instead of the default ~/.skill-forge/reports/audit-<timestamp>.md (refuses an existing path)").option("--foundation", "authorizes exactly one skills-root write: scaffold a business-foundation skill from the captured business profile").option("--handoff", "authorizes launching the configured agent with the bundled curation prompt against the written report").option("--plugin-inventory <file>", "import a static host inventory JSON; audit one host at a time").option("--claude-plugins", "include enabled Claude Code plugins' declared skills in this audit (default: config.claudePlugins, else off)").option("--no-claude-plugins", "exclude enabled Claude Code plugins from this audit even when config.claudePlugins is on").option("--usage-snapshot <file>", "join per-skill usage counts from a skill-monitor snapshot JSON file or a directory of them (default: config.usageSnapshotPath)").option("--usage-window <days>", "restrict directory-input usage-snapshot selection to a snapshot with this window_days (integer)", parseUsageWindow).action(auditCommand);
|
|
12287
12787
|
var finding = program.command("finding").description("Review and acknowledge audit findings \u2014 human-only, never a gate input (see docs/accepted-findings-schema.md)");
|
|
12288
12788
|
finding.command("accept <fingerprint-prefix>").description("Accept a currently-active LOW/MEDIUM finding (no override for HIGH/CRITICAL); re-runs the audit engine to resolve it").requiredOption("--reason <text>", "why this finding is accepted (mandatory, 1-200 chars, no control characters)").option("--json", "print the accepted record as JSON").action(findingAcceptCommand);
|
|
12289
12789
|
finding.command("revoke <fingerprint-prefix>").description("Remove a stored acceptance so the finding reports again").option("--json", "print the removed record as JSON").action(findingRevokeCommand);
|
|
@@ -12294,7 +12794,7 @@ program.command("watch").description("Drift check across every SOURCES.md proven
|
|
|
12294
12794
|
"--skill-map <path>",
|
|
12295
12795
|
"also drift-check every fork-of edge in a generated rhize-plugins skill map (contentHash compare against upstream)"
|
|
12296
12796
|
).action(watchCommand);
|
|
12297
|
-
program.command("routine").description("One scheduled maintenance pass: audit + drift + registry, one report, cron-friendly exit codes (Pro)").option("--json", "print the routine result as JSON instead of the terminal summary").option("--offline", "skip every network check (drift entries are reported unchecked)").option("--skip-watch", "skip the drift check").option("--skip-organize", "skip the capability registry").option("--fail-on <level>", "exit 1 when a finding at or above this severity exists: none|low|medium|high|critical|any (default none)", parseFailOn).option("--housekeeping", "opt in to bounded writes: prune expired quarantine sandboxes, close orphaned queue entries").option("--prune-days <n>", "quarantine age threshold in days for --housekeeping (default 30)", parsePruneDays).option("--dry-run", "with --housekeeping: report what would change, change nothing").option("--handoff", "hand the written report to the configured agent (curation prompt)").option("--report <file>", "write the report here instead of ~/.skill-forge/reports/routine-<timestamp>.md (refuses an existing path)").option("--claude-plugins", "include enabled Claude Code plugins' declared skills in the audit step (default: config.claudePlugins, else off)").option("--no-claude-plugins", "exclude enabled Claude Code plugins from the audit step even when config.claudePlugins is on").option("--usage-snapshot <file>", "join per-skill usage counts from a skill-monitor snapshot JSON file or a directory of them (default: config.usageSnapshotPath)").option("--usage-window <days>", "restrict directory-input usage-snapshot selection to a snapshot with this window_days (integer)", parseUsageWindow).action(routineCommand);
|
|
12797
|
+
program.command("routine").description("One scheduled maintenance pass: audit + drift + registry, one report, cron-friendly exit codes (Pro)").option("--json", "print the routine result as JSON instead of the terminal summary").option("--offline", "skip every network check (drift entries are reported unchecked)").option("--skip-watch", "skip the drift check").option("--skip-organize", "skip the capability registry").option("--fail-on <level>", "exit 1 when a finding at or above this severity exists: none|low|medium|high|critical|any (default none)", parseFailOn).option("--fail-scope <scope>", "findings used by --fail-on: configured|all (default configured; all includes plugin findings)").option("--housekeeping", "opt in to bounded writes: prune expired quarantine sandboxes, close orphaned queue entries").option("--prune-days <n>", "quarantine age threshold in days for --housekeeping (default 30)", parsePruneDays).option("--dry-run", "with --housekeeping: report what would change, change nothing").option("--handoff", "hand the written report to the configured agent (curation prompt)").option("--report <file>", "write the report here instead of ~/.skill-forge/reports/routine-<timestamp>.md (refuses an existing path)").option("--plugin-inventory <file>", "import a static host inventory JSON; audit one host at a time").option("--claude-plugins", "include enabled Claude Code plugins' declared skills in the audit step (default: config.claudePlugins, else off)").option("--no-claude-plugins", "exclude enabled Claude Code plugins from the audit step even when config.claudePlugins is on").option("--usage-snapshot <file>", "join per-skill usage counts from a skill-monitor snapshot JSON file or a directory of them (default: config.usageSnapshotPath)").option("--usage-window <days>", "restrict directory-input usage-snapshot selection to a snapshot with this window_days (integer)", parseUsageWindow).action(routineCommand);
|
|
12298
12798
|
program.command("ingest").description(
|
|
12299
12799
|
"Hand the pending-ingestion queue to the configured coding agent for the decide/absorb pass (Pro). For a single new source, use `skill-forge add <source> --ingest` instead."
|
|
12300
12800
|
).option("--list", "print pending queue entries and exit \u2014 no handoff").option("--json", "with --list: print pending entries as JSON").action(ingest);
|
|
@@ -12308,13 +12808,18 @@ insight.command("approve <study-id>").description("Human-only TTY approval of th
|
|
|
12308
12808
|
insight.command("jira <study-id>").description("Generate a Jira manifest or manage its private local receipts; never calls Jira").option("--project <key>", "destination Jira project key recorded in a new manifest").option("--out <file>", "write an additional export-safe manifest here (refuses an existing file)").option("--handoff", "launch the configured agent to present and confirm the backlog before Jira writes").option("--receipt <file>", "append one validated local receipt for the current manifest").option("--resume", "show local resume state for the current manifest").option("--json", "print the selected mode result as one JSON document").action(insightJira);
|
|
12309
12809
|
insight.command("status <study-id>").description("Show private study lifecycle and evidence counts; read-only").option("--json", "print the status payload as one JSON document").action(insightStatus);
|
|
12310
12810
|
insight.command("close <study-id>").description("Close a study while preserving its append-only receipts").requiredOption("--status <status>", "dismissed|archived", parseInsightCloseStatus).action(insightClose);
|
|
12811
|
+
var plugins = program.command("plugins").description("Explicit host-specific plugin retention decisions; never suppresses safety findings");
|
|
12812
|
+
plugins.command("keep <plugin-id>").requiredOption("--host <host>", "claude|codex").requiredOption("--category <category>", "essential|project-specific|intentionally-infrequent").requiredOption("--reason <text>", "why this capability should be retained").option("--project <path>", "bind the decision to an existing project").option("--json", "print JSON").action(pluginsKeepCommand);
|
|
12813
|
+
plugins.command("list").option("--json", "print JSON").action(pluginsListCommand);
|
|
12814
|
+
plugins.command("forget <plugin-id>").requiredOption("--host <host>", "claude|codex").option("--project <path>", "remove the decision for this project only").option("--json", "print JSON").action(pluginsForgetCommand);
|
|
12311
12815
|
var refine = program.command("refine").description("Capture, apply, and generalize improvements to installed skills from usage feedback (Pro)");
|
|
12312
12816
|
refine.command("capture", { isDefault: true }).description("Record a refinement and write a project-scope override artifact (never touches the base skill)").option("--skill <name>", "target skill name").option("--category <cat>", "trigger|content|hook|tool|pattern|config|new").option("--target <section>", 'heading path or file the refinement targets, e.g. "Hooks > duplicate-check"').option("--override-type <type>", "patch|extend|config|full|hook|script|new").option("--action <action>", "patch action: append|prepend|replace-section|insert-after|insert-before|delete-section").option("--marker <text>", "marker text for insert-* / replace-section / delete-section (filename for full|hook|script)").option("--content <text>", "override content inline").option("--content-file <file>", "read override content from a file").option("--expected <text>", "expected behavior").option("--actual <text>", "actual observed behavior").option("--example <text>", "concrete example of the gap").option("--outcome <text>", "desired outcome").option("--root-cause <text>", "root cause per the gap analysis").option("--pattern-id <id>", "join an existing pattern (PAT-xxxx) instead of fingerprint matching").option("--scope <scope>", 'where to write the artifact: "local" (.claude/skills, default) or "shared" (./skills)').option("--dry-run", "preview the artifact diff; write nothing").option("--json", "print the result as JSON").option("-y, --yes", "skip the interactive confirm; never implies --handoff").option("--handoff", "authorizes launching the configured agent with the bundled refine prompt").action(refineCaptureCommand);
|
|
12313
12817
|
refine.command("list").description("Show refinement history from the user-scope store").option("--status <status>", "filter by refinement status").option("--skill <name>", "filter by skill").option("--project <name>", "filter by project").option("--json", "print records as JSON").action(refineListCommand);
|
|
12314
12818
|
refine.command("patterns").description("Review tracked patterns and the generalization queue").option("--status <status>", "filter: tracking|ready|generalized|dismissed").option("--skill <name>", "filter by affected skill").option("--project <name>", "filter by project").option("--json", "print patterns as JSON").action(refinePatternsCommand);
|
|
12819
|
+
refine.command("activate").description("Materialize a captured patch as a new project skill; never edits a plugin cache").requiredOption("--skill <name>", "captured skill directory name").requiredOption("--base <dir>", "explicit base skill directory, read-only").requiredOption("--as <name>", "unique project skill name").requiredOption("--host <host>", "claude|codex").option("--scope <scope>", "capture source local|shared (default local)").option("--dry-run", "compose and safety-scan without activating").option("--json", "print one JSON result").option("-y, --yes", "confirm activation").action(refineActivateCommand);
|
|
12315
12820
|
refine.command("which <skill>").description("Print override-scope resolution order and which override files exist at each scope").option("--user-root <dir>", "explicit user-scope skills root").option("--json", "print the resolution as JSON").action(refineWhichCommand);
|
|
12316
12821
|
refine.command("promote <pattern-id>").description("Materialize a READY pattern into the user-scope skill default (backup + manifest first)").option("--force", "promote a pattern that is not READY").option("--dry-run", "preview the user-scope diff; write nothing").option("--user-root <dir>", "explicit user-scope skills root").option("--json", "print the result as JSON").option("-y, --yes", "skip the interactive confirm").action(refinePromoteCommand);
|
|
12317
|
-
refine.command("rollback <backup-id>").description("Restore a promotion backup (refuses if files changed since promotion unless --force)").option("--force", "restore even if current files differ from the post-promotion state").option("--json", "print the result as JSON").option("-y, --yes", "skip the interactive confirm").action(refineRollbackCommand);
|
|
12822
|
+
refine.command("rollback <backup-id>").option("--project", "restrict rollback to this project\u2019s .claude/skills and .agents/skills roots").description("Restore a promotion backup (refuses if files changed since promotion unless --force)").option("--force", "restore even if current files differ from the post-promotion state").option("--json", "print the result as JSON").option("-y, --yes", "skip the interactive confirm").action(refineRollbackCommand);
|
|
12318
12823
|
program.command("init").description("Detect installed agents and set gate targets, default promotion target, and handoff agent").option("--defaults", "non-interactive: use all detected agents as gate targets, the first as the default target").option("--list", "print detected agent skill roots and exit \u2014 no writes").option("--all-agents", "pre-select every detected root, not just those of agents whose CLI is on PATH").action(init);
|
|
12319
12824
|
try {
|
|
12320
12825
|
await program.parseAsync();
|