@rhize/skill-forge 0.17.0 → 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 +1374 -797
- 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;
|
|
@@ -1193,12 +1346,24 @@ function refineRmRf(line, _ctx) {
|
|
|
1193
1346
|
}
|
|
1194
1347
|
var DOC_CUE_RE = /\b(never|not|don't|do not|must not|reject|refuse|attack|dangerous|unsafe|avoid|warning)\b/i;
|
|
1195
1348
|
var DOC_EXTENSIONS = /* @__PURE__ */ new Set([".md", ".mdx", ".markdown"]);
|
|
1196
|
-
var
|
|
1349
|
+
var INSTALL_CUE_RE = /\b(install|run|paste|set ?up)\b/gi;
|
|
1350
|
+
var NEGATION_BEFORE_CUE_RE = /\b(never|not|don't|avoid|shouldn't|mustn't)(\s+\w+)?\s*$/i;
|
|
1351
|
+
function hasUnnegatedInstallCue(line) {
|
|
1352
|
+
for (const m of line.matchAll(INSTALL_CUE_RE)) {
|
|
1353
|
+
if (!NEGATION_BEFORE_CUE_RE.test(line.slice(0, m.index))) return true;
|
|
1354
|
+
}
|
|
1355
|
+
return false;
|
|
1356
|
+
}
|
|
1197
1357
|
function refineCurlPipeShell(line, ctx) {
|
|
1198
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
|
+
}
|
|
1199
1363
|
if (ctx.inFence) return base;
|
|
1200
1364
|
if (!DOC_EXTENSIONS.has(extname2(ctx.file).toLowerCase())) return base;
|
|
1201
1365
|
if (!DOC_CUE_RE.test(line)) return base;
|
|
1366
|
+
if (hasUnnegatedInstallCue(line)) return base;
|
|
1202
1367
|
return { ...base, classification: "documentation-example" };
|
|
1203
1368
|
}
|
|
1204
1369
|
function computeFenceFlags(lines) {
|
|
@@ -1283,7 +1448,7 @@ var DENY_RULES = [
|
|
|
1283
1448
|
message: "invokes sudo \u2014 review for unnecessary privilege escalation"
|
|
1284
1449
|
}
|
|
1285
1450
|
];
|
|
1286
|
-
function scanStaticRules(targetPath) {
|
|
1451
|
+
function scanStaticRules(targetPath, capturedFiles) {
|
|
1287
1452
|
const findings = [];
|
|
1288
1453
|
let root;
|
|
1289
1454
|
try {
|
|
@@ -1292,15 +1457,15 @@ function scanStaticRules(targetPath) {
|
|
|
1292
1457
|
return findings;
|
|
1293
1458
|
}
|
|
1294
1459
|
const files = root.isDirectory() ? Array.from(walk(targetPath)) : [targetPath];
|
|
1295
|
-
|
|
1296
|
-
|
|
1460
|
+
const sources = capturedFiles ?? files.map((file) => ({ file, content: readTextFile(file) }));
|
|
1461
|
+
for (const { file, content } of sources) {
|
|
1297
1462
|
if (content === void 0) continue;
|
|
1298
1463
|
const lines = content.split("\n");
|
|
1299
|
-
const fenceFlags = DOC_EXTENSIONS.has(extname2(file).toLowerCase()) ? computeFenceFlags(lines) :
|
|
1464
|
+
const fenceFlags = DOC_EXTENSIONS.has(extname2(file).toLowerCase()) ? computeFenceFlags(lines) : void 0;
|
|
1300
1465
|
for (const rule of DENY_RULES) {
|
|
1301
1466
|
for (let i = 0; i < lines.length; i++) {
|
|
1302
1467
|
if (rule.pattern.test(lines[i])) {
|
|
1303
|
-
const refined = rule.refine ? rule.refine(lines[i], { inFence: fenceFlags[i], file }) : { severity: rule.severity, message: rule.message };
|
|
1468
|
+
const refined = rule.refine ? rule.refine(lines[i], { inFence: fenceFlags?.[i] ?? false, file }) : { severity: rule.severity, message: rule.message };
|
|
1304
1469
|
if (!refined) continue;
|
|
1305
1470
|
findings.push({
|
|
1306
1471
|
severity: refined.severity,
|
|
@@ -1322,7 +1487,7 @@ function which(cmd) {
|
|
|
1322
1487
|
const exts = process.platform === "win32" ? (process.env.PATHEXT ?? ".EXE;.CMD;.BAT").split(";") : [""];
|
|
1323
1488
|
for (const dir of dirs) {
|
|
1324
1489
|
for (const ext of exts) {
|
|
1325
|
-
const candidate =
|
|
1490
|
+
const candidate = join10(dir, cmd + ext);
|
|
1326
1491
|
try {
|
|
1327
1492
|
if (statSync4(candidate).isFile()) return candidate;
|
|
1328
1493
|
} catch {
|
|
@@ -1356,8 +1521,8 @@ async function scanWithSkillSpector(targetPath) {
|
|
|
1356
1521
|
return [];
|
|
1357
1522
|
}
|
|
1358
1523
|
}
|
|
1359
|
-
async function scanSafety(targetPath, strictness = DEFAULT_STRICTNESS) {
|
|
1360
|
-
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)];
|
|
1361
1526
|
return { findings, verdict: verdictFor(findings, strictness) };
|
|
1362
1527
|
}
|
|
1363
1528
|
|
|
@@ -1597,8 +1762,8 @@ async function mcpScan(targetPath, strictness = DEFAULT_STRICTNESS) {
|
|
|
1597
1762
|
}
|
|
1598
1763
|
|
|
1599
1764
|
// src/gate/overlap.ts
|
|
1600
|
-
import { existsSync as
|
|
1601
|
-
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";
|
|
1602
1767
|
var STOPWORDS = new Set(
|
|
1603
1768
|
`
|
|
1604
1769
|
a an the and or of to for in on with use used using when whether this that these those is are be
|
|
@@ -1607,20 +1772,20 @@ skill skills user claude code based comprehensive enhanced practical including i
|
|
|
1607
1772
|
`.split(/\s+/).filter(Boolean)
|
|
1608
1773
|
);
|
|
1609
1774
|
var DEFAULT_SET_THRESHOLD = 0.45;
|
|
1610
|
-
function splitLimited(text,
|
|
1775
|
+
function splitLimited(text, sep11, maxSplit) {
|
|
1611
1776
|
const parts = [];
|
|
1612
1777
|
let rest = text;
|
|
1613
1778
|
for (let i = 0; i < maxSplit; i++) {
|
|
1614
|
-
const idx = rest.indexOf(
|
|
1779
|
+
const idx = rest.indexOf(sep11);
|
|
1615
1780
|
if (idx === -1) break;
|
|
1616
1781
|
parts.push(rest.slice(0, idx));
|
|
1617
|
-
rest = rest.slice(idx +
|
|
1782
|
+
rest = rest.slice(idx + sep11.length);
|
|
1618
1783
|
}
|
|
1619
1784
|
parts.push(rest);
|
|
1620
1785
|
return parts;
|
|
1621
1786
|
}
|
|
1622
1787
|
function readFrontmatter(skillMdPath) {
|
|
1623
|
-
const text =
|
|
1788
|
+
const text = readFileSync9(skillMdPath, "utf-8");
|
|
1624
1789
|
const meta = { name: basename5(dirname4(skillMdPath)), description: "" };
|
|
1625
1790
|
if (text.startsWith("---\n")) {
|
|
1626
1791
|
const block = splitLimited(text, "---", 2)[1] ?? "";
|
|
@@ -1715,47 +1880,47 @@ function suggestVerbCode(topScore) {
|
|
|
1715
1880
|
return verbTier(topScore).verb;
|
|
1716
1881
|
}
|
|
1717
1882
|
function listSubdirs(dir) {
|
|
1718
|
-
if (!
|
|
1883
|
+
if (!existsSync8(dir)) return [];
|
|
1719
1884
|
let entries;
|
|
1720
1885
|
try {
|
|
1721
1886
|
entries = readdirSync4(dir, { withFileTypes: true });
|
|
1722
1887
|
} catch {
|
|
1723
1888
|
return [];
|
|
1724
1889
|
}
|
|
1725
|
-
return entries.filter((e) => e.isDirectory()).map((e) =>
|
|
1890
|
+
return entries.filter((e) => e.isDirectory()).map((e) => join11(dir, e.name));
|
|
1726
1891
|
}
|
|
1727
1892
|
function findSkillMdFiles(root) {
|
|
1728
1893
|
const found = /* @__PURE__ */ new Set();
|
|
1729
1894
|
for (const dir of listSubdirs(root)) {
|
|
1730
|
-
const candidate =
|
|
1731
|
-
if (
|
|
1895
|
+
const candidate = join11(dir, "SKILL.md");
|
|
1896
|
+
if (existsSync8(candidate)) found.add(resolve3(candidate));
|
|
1732
1897
|
}
|
|
1733
|
-
for (const dir of listSubdirs(
|
|
1734
|
-
const candidate =
|
|
1735
|
-
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));
|
|
1736
1901
|
}
|
|
1737
1902
|
for (const topDir of listSubdirs(root)) {
|
|
1738
|
-
for (const dir of listSubdirs(
|
|
1739
|
-
const candidate =
|
|
1740
|
-
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));
|
|
1741
1906
|
}
|
|
1742
1907
|
}
|
|
1743
1908
|
return Array.from(found).filter((p) => !p.includes("/.git/")).sort();
|
|
1744
1909
|
}
|
|
1745
1910
|
function scanOverlap(candidatePath, skillsRoot) {
|
|
1746
|
-
const root =
|
|
1911
|
+
const root = resolve3(expandHome(skillsRoot));
|
|
1747
1912
|
if (!isDir(root)) {
|
|
1748
1913
|
throw new Error(`skills-root not a directory: ${root}`);
|
|
1749
1914
|
}
|
|
1750
|
-
let cpath =
|
|
1915
|
+
let cpath = resolve3(expandHome(candidatePath));
|
|
1751
1916
|
if (isDir(cpath)) {
|
|
1752
|
-
cpath =
|
|
1917
|
+
cpath = join11(cpath, "SKILL.md");
|
|
1753
1918
|
}
|
|
1754
|
-
if (!
|
|
1919
|
+
if (!existsSync8(cpath)) {
|
|
1755
1920
|
throw new Error(`candidate SKILL.md not found: ${cpath}`);
|
|
1756
1921
|
}
|
|
1757
1922
|
const cand = readFrontmatter(cpath);
|
|
1758
|
-
const existing = findSkillMdFiles(root).filter((p) => p !==
|
|
1923
|
+
const existing = findSkillMdFiles(root).filter((p) => p !== resolve3(cpath));
|
|
1759
1924
|
if (existing.length === 0) {
|
|
1760
1925
|
throw new Error(`no existing skills found under ${root}`);
|
|
1761
1926
|
}
|
|
@@ -1796,8 +1961,8 @@ function scanOverlapPaths(skillMdPaths, threshold = DEFAULT_SET_THRESHOLD) {
|
|
|
1796
1961
|
|
|
1797
1962
|
// src/findings.ts
|
|
1798
1963
|
import { createHash as createHash2 } from "crypto";
|
|
1799
|
-
import { existsSync as
|
|
1800
|
-
import { join as
|
|
1964
|
+
import { existsSync as existsSync9, readFileSync as readFileSync10 } from "fs";
|
|
1965
|
+
import { join as join12 } from "path";
|
|
1801
1966
|
var FINGERPRINT_DISPLAY_LENGTH = 12;
|
|
1802
1967
|
var MIN_FINGERPRINT_PREFIX_LENGTH = 8;
|
|
1803
1968
|
var FINGERPRINT_HEX_RE = /^[0-9a-f]{64}$/;
|
|
@@ -1810,7 +1975,7 @@ function fingerprintFinding(f) {
|
|
|
1810
1975
|
return createHash2("sha256").update(`${f.severity}\0${f.rule}\0${f.target}\0${f.context}`).digest("hex");
|
|
1811
1976
|
}
|
|
1812
1977
|
function getAcceptedFindingsPath() {
|
|
1813
|
-
return
|
|
1978
|
+
return join12(getHomeDir(), "accepted-findings.json");
|
|
1814
1979
|
}
|
|
1815
1980
|
function invalidRecordReason(record) {
|
|
1816
1981
|
if (typeof record !== "object" || record === null) return "not an object";
|
|
@@ -1830,10 +1995,10 @@ function invalidRecordReason(record) {
|
|
|
1830
1995
|
}
|
|
1831
1996
|
function readAcceptedFindings() {
|
|
1832
1997
|
const path = getAcceptedFindingsPath();
|
|
1833
|
-
if (!
|
|
1998
|
+
if (!existsSync9(path)) return { records: [], notices: [] };
|
|
1834
1999
|
let parsed;
|
|
1835
2000
|
try {
|
|
1836
|
-
parsed = JSON.parse(
|
|
2001
|
+
parsed = JSON.parse(readFileSync10(path, "utf-8"));
|
|
1837
2002
|
} catch {
|
|
1838
2003
|
return { records: [], notices: ["accepted-findings store unreadable (corrupt JSON) \u2014 nothing suppressed this run"] };
|
|
1839
2004
|
}
|
|
@@ -2003,7 +2168,7 @@ function listSubdirsFollowingSymlinks(dir) {
|
|
|
2003
2168
|
}
|
|
2004
2169
|
const out = [];
|
|
2005
2170
|
for (const entry of entries) {
|
|
2006
|
-
const full =
|
|
2171
|
+
const full = join13(dir, entry.name);
|
|
2007
2172
|
if (entry.isDirectory() || entry.isSymbolicLink() && isDirFollowingSymlink(full)) {
|
|
2008
2173
|
out.push(full);
|
|
2009
2174
|
}
|
|
@@ -2017,20 +2182,20 @@ function listSubdirsNoSymlinks(dir) {
|
|
|
2017
2182
|
} catch {
|
|
2018
2183
|
return [];
|
|
2019
2184
|
}
|
|
2020
|
-
return entries.filter((e) => e.isDirectory()).map((e) =>
|
|
2185
|
+
return entries.filter((e) => e.isDirectory()).map((e) => join13(dir, e.name));
|
|
2021
2186
|
}
|
|
2022
2187
|
function findSkillDirsUnderRoot(root) {
|
|
2023
2188
|
const found = /* @__PURE__ */ new Set();
|
|
2024
2189
|
const rootSubdirs = listSubdirsFollowingSymlinks(root);
|
|
2025
2190
|
for (const dir of rootSubdirs) {
|
|
2026
|
-
if (
|
|
2191
|
+
if (existsSync10(join13(dir, "SKILL.md"))) found.add(dir);
|
|
2027
2192
|
}
|
|
2028
|
-
for (const dir of listSubdirsFollowingSymlinks(
|
|
2029
|
-
if (
|
|
2193
|
+
for (const dir of listSubdirsFollowingSymlinks(join13(root, "skills"))) {
|
|
2194
|
+
if (existsSync10(join13(dir, "SKILL.md"))) found.add(dir);
|
|
2030
2195
|
}
|
|
2031
2196
|
for (const topDir of rootSubdirs) {
|
|
2032
|
-
for (const dir of listSubdirsNoSymlinks(
|
|
2033
|
-
if (
|
|
2197
|
+
for (const dir of listSubdirsNoSymlinks(join13(topDir, "skills"))) {
|
|
2198
|
+
if (existsSync10(join13(dir, "SKILL.md"))) found.add(dir);
|
|
2034
2199
|
}
|
|
2035
2200
|
}
|
|
2036
2201
|
return Array.from(found).filter((p) => !p.includes("/.git/"));
|
|
@@ -2044,7 +2209,7 @@ function collectSkillCandidates(roots) {
|
|
|
2044
2209
|
let realPath;
|
|
2045
2210
|
if (exists) {
|
|
2046
2211
|
try {
|
|
2047
|
-
realPath =
|
|
2212
|
+
realPath = realpathSync3(resolvedRoot);
|
|
2048
2213
|
} catch {
|
|
2049
2214
|
realPath = void 0;
|
|
2050
2215
|
}
|
|
@@ -2063,7 +2228,7 @@ function dedupeSkillCandidates(candidates, findings) {
|
|
|
2063
2228
|
for (const candidate of candidates) {
|
|
2064
2229
|
let realPath;
|
|
2065
2230
|
try {
|
|
2066
|
-
realPath =
|
|
2231
|
+
realPath = realpathSync3(candidate.configuredPath);
|
|
2067
2232
|
} catch (err) {
|
|
2068
2233
|
findings.push({
|
|
2069
2234
|
severity: "MEDIUM",
|
|
@@ -2126,7 +2291,7 @@ function estimateTokens(text) {
|
|
|
2126
2291
|
return Math.ceil(Buffer.byteLength(text, "utf-8") / 4);
|
|
2127
2292
|
}
|
|
2128
2293
|
function profileAuditedSkill(group, findings, unreadableSkillMdPaths, pluginId) {
|
|
2129
|
-
const skillMdPath =
|
|
2294
|
+
const skillMdPath = join13(group.canonicalPath, "SKILL.md");
|
|
2130
2295
|
const base = {
|
|
2131
2296
|
path: group.canonicalPath,
|
|
2132
2297
|
skillMdPath,
|
|
@@ -2138,7 +2303,7 @@ function profileAuditedSkill(group, findings, unreadableSkillMdPaths, pluginId)
|
|
|
2138
2303
|
let sizeBytes;
|
|
2139
2304
|
try {
|
|
2140
2305
|
sizeBytes = statSync6(skillMdPath).size;
|
|
2141
|
-
text =
|
|
2306
|
+
text = readFileSync11(skillMdPath, "utf-8");
|
|
2142
2307
|
} catch (err) {
|
|
2143
2308
|
findings.push({
|
|
2144
2309
|
severity: "MEDIUM",
|
|
@@ -2195,7 +2360,7 @@ function auditMcpTarget(targetPath, notices) {
|
|
|
2195
2360
|
const findings = [];
|
|
2196
2361
|
const isToml = extname4(targetPath).toLowerCase() === ".toml";
|
|
2197
2362
|
const format = isToml ? "toml" : "json";
|
|
2198
|
-
if (!
|
|
2363
|
+
if (!existsSync10(targetPath)) {
|
|
2199
2364
|
findings.push({
|
|
2200
2365
|
severity: "LOW",
|
|
2201
2366
|
rule: "missing-mcp-target",
|
|
@@ -2216,7 +2381,7 @@ function auditMcpTarget(targetPath, notices) {
|
|
|
2216
2381
|
}
|
|
2217
2382
|
if (isToml) {
|
|
2218
2383
|
try {
|
|
2219
|
-
|
|
2384
|
+
readFileSync11(targetPath, "utf-8");
|
|
2220
2385
|
} catch (err) {
|
|
2221
2386
|
findings.push({
|
|
2222
2387
|
severity: "MEDIUM",
|
|
@@ -2231,7 +2396,7 @@ function auditMcpTarget(targetPath, notices) {
|
|
|
2231
2396
|
}
|
|
2232
2397
|
let raw;
|
|
2233
2398
|
try {
|
|
2234
|
-
raw =
|
|
2399
|
+
raw = readFileSync11(targetPath, "utf-8");
|
|
2235
2400
|
} catch (err) {
|
|
2236
2401
|
findings.push({
|
|
2237
2402
|
severity: "MEDIUM",
|
|
@@ -2304,15 +2469,7 @@ function toClusterMembers(members) {
|
|
|
2304
2469
|
function annotateSiblingClusters(clusters, nameByPath) {
|
|
2305
2470
|
return clusters.map((cluster) => {
|
|
2306
2471
|
const names = cluster.members.map((p) => nameByPath.get(p) ?? basename6(p));
|
|
2307
|
-
|
|
2308
|
-
outer: for (let i = 0; i < names.length && allSiblings; i++) {
|
|
2309
|
-
for (let j = i + 1; j < names.length; j++) {
|
|
2310
|
-
if (!isStackSiblingPair(names[i], names[j])) {
|
|
2311
|
-
allSiblings = false;
|
|
2312
|
-
break outer;
|
|
2313
|
-
}
|
|
2314
|
-
}
|
|
2315
|
-
}
|
|
2472
|
+
const allSiblings = names.every((a, i) => names.slice(i + 1).every((b) => isStackSiblingPair(a, b)));
|
|
2316
2473
|
return allSiblings ? { ...cluster, siblingVariants: true, suggestedVerb: "DEFER" } : cluster;
|
|
2317
2474
|
});
|
|
2318
2475
|
}
|
|
@@ -2351,7 +2508,7 @@ function collapseIdenticalContent(skills, configuredRootOrder) {
|
|
|
2351
2508
|
const ai = configuredRootOrder.indexOf(a.root);
|
|
2352
2509
|
const bi = configuredRootOrder.indexOf(b.root);
|
|
2353
2510
|
if (ai !== bi) return ai - bi;
|
|
2354
|
-
return a.path
|
|
2511
|
+
return comparePaths(a.path, b.path);
|
|
2355
2512
|
});
|
|
2356
2513
|
const canonical = sorted[0];
|
|
2357
2514
|
for (const duplicate of sorted.slice(1)) {
|
|
@@ -2362,23 +2519,55 @@ function collapseIdenticalContent(skills, configuredRootOrder) {
|
|
|
2362
2519
|
}
|
|
2363
2520
|
return { skills: skills.filter((s) => !toRemove.has(s)), collapsedCount };
|
|
2364
2521
|
}
|
|
2365
|
-
function
|
|
2366
|
-
|
|
2522
|
+
function comparePaths(a, b) {
|
|
2523
|
+
return a < b ? -1 : a > b ? 1 : 0;
|
|
2524
|
+
}
|
|
2525
|
+
function buildUsageJoinIndex(usage, skills) {
|
|
2526
|
+
const bareKeys = /* @__PURE__ */ new Map();
|
|
2527
|
+
for (const key of usage.keys()) {
|
|
2528
|
+
const bare = key.includes(":") ? key.slice(key.lastIndexOf(":") + 1) : key;
|
|
2529
|
+
bareKeys.set(bare, [...bareKeys.get(bare) ?? [], key]);
|
|
2530
|
+
}
|
|
2531
|
+
const inventoryNameCounts = /* @__PURE__ */ new Map();
|
|
2532
|
+
for (const skill of skills) inventoryNameCounts.set(skill.name, (inventoryNameCounts.get(skill.name) ?? 0) + 1);
|
|
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() };
|
|
2540
|
+
}
|
|
2541
|
+
function matchUsage(skill, usage, index, notices) {
|
|
2367
2542
|
if (skill.pluginId) {
|
|
2368
2543
|
const composite = `${skill.pluginId.split("@")[0]}:${skill.name}`;
|
|
2369
|
-
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
|
+
}
|
|
2370
2552
|
}
|
|
2371
|
-
const bareMatches =
|
|
2372
|
-
if (bareMatches.length ===
|
|
2373
|
-
|
|
2374
|
-
|
|
2553
|
+
const bareMatches = index.bareKeys.get(skill.name) ?? [];
|
|
2554
|
+
if (bareMatches.length === 0) return void 0;
|
|
2555
|
+
const installedWithName = index.inventoryNameCounts.get(skill.name) ?? 0;
|
|
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);
|
|
2559
|
+
if (!index.noticed.has(skill.name)) {
|
|
2560
|
+
index.noticed.add(skill.name);
|
|
2561
|
+
notices.push(
|
|
2562
|
+
bareMatches.length > 1 ? `usage snapshot: ambiguous bare name "${skill.name}" matches ${bareMatches.length} entries \u2014 skipping attribution` : `usage snapshot: bare name "${skill.name}" matches ${installedWithName} installed skills \u2014 skipping attribution`
|
|
2563
|
+
);
|
|
2375
2564
|
}
|
|
2376
2565
|
return void 0;
|
|
2377
2566
|
}
|
|
2378
2567
|
function isUnderInstallPath(target, installPath) {
|
|
2379
|
-
return target === installPath || target.startsWith(installPath +
|
|
2568
|
+
return target === installPath || target.startsWith(installPath + sep3);
|
|
2380
2569
|
}
|
|
2381
|
-
function computePluginSummaries(pluginRoots, activeFindings, skills, usage) {
|
|
2570
|
+
function computePluginSummaries(pluginRoots, activeFindings, skills, usage, usageJoined, uncertainPaths, positiveJoined) {
|
|
2382
2571
|
return pluginRoots.map((pr) => {
|
|
2383
2572
|
const findingCounts = { LOW: 0, MEDIUM: 0, HIGH: 0, CRITICAL: 0 };
|
|
2384
2573
|
for (const f of activeFindings) {
|
|
@@ -2391,9 +2580,11 @@ function computePluginSummaries(pluginRoots, activeFindings, skills, usage) {
|
|
|
2391
2580
|
const observedSkillCount = hasUsage ? skills.filter((s) => s.pluginId === pr.pluginId && (s.usage ?? 0) > 0).length : void 0;
|
|
2392
2581
|
let recommendation;
|
|
2393
2582
|
const reasons = [];
|
|
2583
|
+
const longWindow = windowDays !== void 0 && windowDays >= 28;
|
|
2584
|
+
const ambiguousUsage = skills.some((s) => s.pluginId === pr.pluginId && uncertainPaths.has(s.path));
|
|
2394
2585
|
if (findingCounts.HIGH > 0 || findingCounts.CRITICAL > 0) {
|
|
2395
2586
|
recommendation = "review";
|
|
2396
|
-
} else if (
|
|
2587
|
+
} else if (!ambiguousUsage && exhaustive && longWindow && skillCount > 0 && observedSkillCount === 0 && positiveJoined > 0) {
|
|
2397
2588
|
recommendation = "unobserved";
|
|
2398
2589
|
} else if (exhaustive && (observedSkillCount ?? 0) >= 1) {
|
|
2399
2590
|
recommendation = "keep";
|
|
@@ -2401,9 +2592,12 @@ function computePluginSummaries(pluginRoots, activeFindings, skills, usage) {
|
|
|
2401
2592
|
recommendation = "unknown";
|
|
2402
2593
|
if (skillCount === 0) reasons.push("no skills to observe");
|
|
2403
2594
|
if (!hasUsage) reasons.push("no usage telemetry");
|
|
2404
|
-
else if (
|
|
2405
|
-
else if (
|
|
2406
|
-
if (
|
|
2595
|
+
else if (usage.usage.size === 0) reasons.push("usage snapshot is empty or unreadable (see notices)");
|
|
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");
|
|
2598
|
+
else if (!longWindow) reasons.push(`window ${windowDays ?? "unknown"} < 28 days`);
|
|
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");
|
|
2407
2601
|
}
|
|
2408
2602
|
return {
|
|
2409
2603
|
pluginId: pr.pluginId,
|
|
@@ -2413,9 +2607,9 @@ function computePluginSummaries(pluginRoots, activeFindings, skills, usage) {
|
|
|
2413
2607
|
findingCounts,
|
|
2414
2608
|
observedSkillCount,
|
|
2415
2609
|
recommendation,
|
|
2416
|
-
reasons:
|
|
2610
|
+
reasons: reasons.sort()
|
|
2417
2611
|
};
|
|
2418
|
-
}).sort((a, b) => a.pluginId
|
|
2612
|
+
}).sort((a, b) => comparePaths(a.pluginId, b.pluginId));
|
|
2419
2613
|
}
|
|
2420
2614
|
function defaultWhich(cmd) {
|
|
2421
2615
|
const pathEnv = process.env.PATH ?? "";
|
|
@@ -2423,7 +2617,7 @@ function defaultWhich(cmd) {
|
|
|
2423
2617
|
const exts = process.platform === "win32" ? (process.env.PATHEXT ?? ".EXE;.CMD;.BAT").split(";") : [""];
|
|
2424
2618
|
for (const dir of dirs) {
|
|
2425
2619
|
for (const ext of exts) {
|
|
2426
|
-
const candidate =
|
|
2620
|
+
const candidate = join13(dir, cmd + ext);
|
|
2427
2621
|
try {
|
|
2428
2622
|
if (statSync6(candidate).isFile()) return candidate;
|
|
2429
2623
|
} catch {
|
|
@@ -2623,17 +2817,21 @@ async function auditConfiguredSet(config, options = {}) {
|
|
|
2623
2817
|
message: "no business-foundation skill yet \u2014 `skill-forge audit --foundation` can scaffold one to ground future skills in your business profile"
|
|
2624
2818
|
};
|
|
2625
2819
|
const usage = options.usage;
|
|
2820
|
+
const uncertainPaths = /* @__PURE__ */ new Set();
|
|
2626
2821
|
if (usage) {
|
|
2822
|
+
const index = buildUsageJoinIndex(usage.usage, skills);
|
|
2627
2823
|
for (const skill of skills) {
|
|
2628
|
-
const matched = matchUsage(skill, usage.usage, notices);
|
|
2824
|
+
const matched = matchUsage(skill, usage.usage, index, notices);
|
|
2629
2825
|
if (matched !== void 0) skill.usage = matched;
|
|
2826
|
+
if (index.uncertainPaths.has(skill.path)) uncertainPaths.add(skill.path);
|
|
2630
2827
|
}
|
|
2631
2828
|
}
|
|
2829
|
+
const positiveJoined = skills.filter((s) => (s.usage ?? 0) > 0).length;
|
|
2632
2830
|
const usageJoined = usage ? skills.filter((s) => s.usage !== void 0).length : void 0;
|
|
2633
2831
|
if (usage && usageJoined === 0) {
|
|
2634
2832
|
notices.push(`usage snapshot (${usage.sourcePath}) joined 0 of ${skills.length} skill(s)`);
|
|
2635
2833
|
}
|
|
2636
|
-
const unobservedInWindow = usage && usage.exhaustive ? skills.filter((s) => (s.usage ?? 0) === 0).map((s) => ({ name: s.name, path: s.path, pluginId: s.pluginId })) : [];
|
|
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 })) : [];
|
|
2637
2835
|
const findingCounts = { LOW: 0, MEDIUM: 0, HIGH: 0, CRITICAL: 0 };
|
|
2638
2836
|
for (const f of activeFindings) findingCounts[f.severity]++;
|
|
2639
2837
|
const summary = {
|
|
@@ -2648,7 +2846,7 @@ async function auditConfiguredSet(config, options = {}) {
|
|
|
2648
2846
|
documentationExampleCount: activeFindings.filter((f) => f.classification === "documentation-example").length,
|
|
2649
2847
|
...usage ? { usageJoined, usageExhaustive: usage.exhaustive, usageWindowDays: usage.windowDays } : {}
|
|
2650
2848
|
};
|
|
2651
|
-
const
|
|
2849
|
+
const plugins2 = options.pluginRoots ? computePluginSummaries(pluginRoots, activeFindings, skills, usage, usageJoined, uncertainPaths, positiveJoined) : void 0;
|
|
2652
2850
|
const tooling = checkTooling(options.which ?? defaultWhich, options.env ?? process.env);
|
|
2653
2851
|
return {
|
|
2654
2852
|
schemaVersion: 1,
|
|
@@ -2660,7 +2858,7 @@ async function auditConfiguredSet(config, options = {}) {
|
|
|
2660
2858
|
tooling,
|
|
2661
2859
|
notices,
|
|
2662
2860
|
summary,
|
|
2663
|
-
plugins
|
|
2861
|
+
plugins: plugins2
|
|
2664
2862
|
};
|
|
2665
2863
|
}
|
|
2666
2864
|
function bulletList(items, emptyPlaceholder) {
|
|
@@ -2731,7 +2929,7 @@ function renderFindingsSection(findings) {
|
|
|
2731
2929
|
const docExamples = findings.filter((f) => f.classification === "documentation-example");
|
|
2732
2930
|
const lines = ["## Hygiene findings", "", ...bulletList(live.map(renderFindingLine), "(none \u2014 clean)")];
|
|
2733
2931
|
if (docExamples.length > 0) {
|
|
2734
|
-
lines.push("", "###
|
|
2932
|
+
lines.push("", "### Heuristically classified as documentation examples (still CRITICAL \u2014 review each; the heuristic can be gamed)", "", ...bulletList(docExamples.map(renderFindingLine), ""));
|
|
2735
2933
|
}
|
|
2736
2934
|
return lines;
|
|
2737
2935
|
}
|
|
@@ -2778,7 +2976,9 @@ function renderUsageSection(report) {
|
|
|
2778
2976
|
const lines = ["## Usage", ""];
|
|
2779
2977
|
lines.push(`- Matched: ${summary.usageJoined ?? 0} of ${summary.skillCount} skill(s)`);
|
|
2780
2978
|
lines.push(`- Window: ${summary.usageWindowDays !== void 0 ? `${summary.usageWindowDays} day(s)` : "unknown"}`);
|
|
2781
|
-
if (summary.
|
|
2979
|
+
if ((summary.usageJoined ?? 0) === 0) {
|
|
2980
|
+
lines.push("- Coverage: nothing joined (see Notices)");
|
|
2981
|
+
} else if (summary.usageExhaustive) {
|
|
2782
2982
|
lines.push("- Coverage: exhaustive (every skill counted this window)");
|
|
2783
2983
|
} else {
|
|
2784
2984
|
lines.push("- Coverage: top-50 only; absence is not zero usage");
|
|
@@ -2788,19 +2988,22 @@ function renderUsageSection(report) {
|
|
|
2788
2988
|
for (const s of report.opportunities.unobservedInWindow) {
|
|
2789
2989
|
lines.push(`- ${sanitizeForMarkdown(s.name)} \u2014 ${s.path}${s.pluginId ? ` (plugin ${sanitizeForMarkdown(s.pluginId)})` : ""}`);
|
|
2790
2990
|
}
|
|
2991
|
+
} else if (summary.usageExhaustive && (summary.usageJoined ?? 0) > 0 && (summary.usageWindowDays ?? 0) < 28) {
|
|
2992
|
+
lines.push("- Unobserved list: suppressed \u2014 window shorter than 28 days proves nothing about monthly-use skills");
|
|
2791
2993
|
}
|
|
2792
2994
|
return lines;
|
|
2793
2995
|
}
|
|
2794
|
-
function renderPluginsSection(
|
|
2996
|
+
function renderPluginsSection(plugins2) {
|
|
2795
2997
|
return [
|
|
2796
2998
|
"## Plugins",
|
|
2797
2999
|
"",
|
|
2798
3000
|
...bulletList(
|
|
2799
|
-
|
|
3001
|
+
plugins2.map((p) => {
|
|
2800
3002
|
const findingsPart = `${p.findingCounts.CRITICAL} CRITICAL, ${p.findingCounts.HIGH} HIGH, ${p.findingCounts.MEDIUM} MEDIUM, ${p.findingCounts.LOW} LOW`;
|
|
2801
3003
|
const observedPart = p.observedSkillCount !== void 0 ? `, observed ${p.observedSkillCount}` : "";
|
|
2802
|
-
const reasonsPart = p.reasons.length > 0 ? ` (${p.reasons.join("; ")})` : "";
|
|
2803
|
-
|
|
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;
|
|
2804
3007
|
}),
|
|
2805
3008
|
"(no enabled plugins)"
|
|
2806
3009
|
)
|
|
@@ -2833,6 +3036,7 @@ function renderAuditMarkdown(report) {
|
|
|
2833
3036
|
const lines = [
|
|
2834
3037
|
`# skill-forge audit \u2014 ${report.generatedAt}`,
|
|
2835
3038
|
"",
|
|
3039
|
+
...report.comparison ? renderAuditComparison(report.comparison) : [],
|
|
2836
3040
|
"## Summary",
|
|
2837
3041
|
"",
|
|
2838
3042
|
`- Skill roots: ${summary.rootCount}`,
|
|
@@ -2843,6 +3047,7 @@ function renderAuditMarkdown(report) {
|
|
|
2843
3047
|
`- Overlap clusters: ${summary.overlapClusterCount}`,
|
|
2844
3048
|
`- Evolve-eligible skills: ${summary.evolveEligibleCount}`,
|
|
2845
3049
|
`- Collapsed duplicates: ${summary.collapsedDuplicateCount}`,
|
|
3050
|
+
`- Documentation-example classified (heuristic; counted above as CRITICAL): ${summary.documentationExampleCount}`,
|
|
2846
3051
|
"",
|
|
2847
3052
|
"## Inventory",
|
|
2848
3053
|
"",
|
|
@@ -2907,7 +3112,7 @@ async function scaffoldFoundationSkill(config, profile, targetRoot) {
|
|
|
2907
3112
|
if (!isDir(resolvedRoot)) {
|
|
2908
3113
|
throw new UserError(`skills root does not exist: ${resolvedRoot}`);
|
|
2909
3114
|
}
|
|
2910
|
-
const destDir =
|
|
3115
|
+
const destDir = join13(resolvedRoot, "business-foundation");
|
|
2911
3116
|
let destLstat;
|
|
2912
3117
|
try {
|
|
2913
3118
|
destLstat = lstatSync3(destDir);
|
|
@@ -2921,9 +3126,9 @@ async function scaffoldFoundationSkill(config, profile, targetRoot) {
|
|
|
2921
3126
|
throw new UserError(`refusing to scaffold a foundation skill at ${destDir}: already exists`);
|
|
2922
3127
|
}
|
|
2923
3128
|
const content = renderFoundationSkillMd(profile);
|
|
2924
|
-
const tempDir = mkdtempSync(
|
|
3129
|
+
const tempDir = mkdtempSync(join13(resolvedRoot, ".business-foundation-"));
|
|
2925
3130
|
try {
|
|
2926
|
-
writeFileSync5(
|
|
3131
|
+
writeFileSync5(join13(tempDir, "SKILL.md"), content, { flag: "wx", mode: 384 });
|
|
2927
3132
|
const safety = await scanSafety(tempDir, config.strictness);
|
|
2928
3133
|
if (safety.verdict === "block") {
|
|
2929
3134
|
throw new UserError(
|
|
@@ -2957,7 +3162,7 @@ async function scaffoldFoundationSkill(config, profile, targetRoot) {
|
|
|
2957
3162
|
|
|
2958
3163
|
// src/auditState.ts
|
|
2959
3164
|
function getAuditStatePath() {
|
|
2960
|
-
return
|
|
3165
|
+
return join14(getHomeDir(), "audit-state.json");
|
|
2961
3166
|
}
|
|
2962
3167
|
function emptyCounts() {
|
|
2963
3168
|
return { CRITICAL: 0, HIGH: 0, MEDIUM: 0, LOW: 0 };
|
|
@@ -2973,7 +3178,9 @@ function summarizeAudit(report, reportPath) {
|
|
|
2973
3178
|
for (const skill of report.inventory.skills) {
|
|
2974
3179
|
rootOfSkillPath.set(skill.path, skill.root);
|
|
2975
3180
|
}
|
|
3181
|
+
const pluginRootPaths = new Set(report.inventory.skillRoots.filter((r) => r.origin === "plugin").map((r) => r.root));
|
|
2976
3182
|
const byRoot = {};
|
|
3183
|
+
const pluginCounts = emptyCounts();
|
|
2977
3184
|
for (const finding2 of report.hygiene) {
|
|
2978
3185
|
let root;
|
|
2979
3186
|
for (let dir = finding2.target; ; dir = dirname5(dir)) {
|
|
@@ -2981,19 +3188,24 @@ function summarizeAudit(report, reportPath) {
|
|
|
2981
3188
|
if (root || dirname5(dir) === dir) break;
|
|
2982
3189
|
}
|
|
2983
3190
|
if (!root) continue;
|
|
3191
|
+
if (pluginRootPaths.has(root)) {
|
|
3192
|
+
bump(pluginCounts, finding2.severity);
|
|
3193
|
+
continue;
|
|
3194
|
+
}
|
|
2984
3195
|
byRoot[root] ??= emptyCounts();
|
|
2985
3196
|
bump(byRoot[root], finding2.severity);
|
|
2986
3197
|
}
|
|
3198
|
+
const findings = emptyCounts();
|
|
3199
|
+
for (const severity of Object.keys(findings)) {
|
|
3200
|
+
findings[severity] = report.summary.findingCounts[severity] - pluginCounts[severity];
|
|
3201
|
+
}
|
|
2987
3202
|
return {
|
|
2988
3203
|
schemaVersion: 1,
|
|
2989
3204
|
lastRunAt: report.generatedAt,
|
|
2990
3205
|
reportPath,
|
|
2991
3206
|
skillCount: report.summary.skillCount,
|
|
2992
3207
|
mcpTargetCount: report.summary.mcpTargetCount,
|
|
2993
|
-
|
|
2994
|
-
// array, and a second tally is how the terminal summary and the markdown report end up
|
|
2995
|
-
// disagreeing about the same run.
|
|
2996
|
-
findings: { ...report.summary.findingCounts },
|
|
3208
|
+
findings,
|
|
2997
3209
|
byRoot,
|
|
2998
3210
|
overlapClusters: report.opportunities.overlapClusters.map((c) => ({
|
|
2999
3211
|
// Cluster members are SKILL.md paths; the skill name is the parent directory name. Stripped
|
|
@@ -3013,9 +3225,9 @@ function writeAuditState(state) {
|
|
|
3013
3225
|
}
|
|
3014
3226
|
function readAuditState() {
|
|
3015
3227
|
const path = getAuditStatePath();
|
|
3016
|
-
if (!
|
|
3228
|
+
if (!existsSync11(path)) return void 0;
|
|
3017
3229
|
try {
|
|
3018
|
-
const parsed = JSON.parse(
|
|
3230
|
+
const parsed = JSON.parse(readFileSync12(path, "utf-8"));
|
|
3019
3231
|
if (parsed.schemaVersion !== 1 || typeof parsed.lastRunAt !== "string") return void 0;
|
|
3020
3232
|
return {
|
|
3021
3233
|
schemaVersion: 1,
|
|
@@ -3077,32 +3289,32 @@ function formatAuditAdvisory(state, options = {}) {
|
|
|
3077
3289
|
}
|
|
3078
3290
|
|
|
3079
3291
|
// src/commands/gatePipeline.ts
|
|
3080
|
-
import { join as
|
|
3292
|
+
import { join as join17 } from "path";
|
|
3081
3293
|
|
|
3082
3294
|
// src/gate/profile.ts
|
|
3083
3295
|
import { execFileSync } from "child_process";
|
|
3084
3296
|
import {
|
|
3085
3297
|
closeSync as closeSync2,
|
|
3086
|
-
existsSync as
|
|
3298
|
+
existsSync as existsSync13,
|
|
3087
3299
|
mkdtempSync as mkdtempSync2,
|
|
3088
3300
|
openSync as openSync2,
|
|
3089
|
-
readFileSync as
|
|
3301
|
+
readFileSync as readFileSync14,
|
|
3090
3302
|
readdirSync as readdirSync6,
|
|
3091
3303
|
readSync,
|
|
3092
3304
|
rmSync as rmSync4,
|
|
3093
3305
|
statSync as statSync7
|
|
3094
3306
|
} from "fs";
|
|
3095
3307
|
import { tmpdir } from "os";
|
|
3096
|
-
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";
|
|
3097
3309
|
|
|
3098
3310
|
// src/gate/mcpCapabilities.ts
|
|
3099
|
-
import { existsSync as
|
|
3100
|
-
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";
|
|
3101
3313
|
var SOURCE_EXT = /* @__PURE__ */ new Set([".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs"]);
|
|
3102
3314
|
var MAX_ITEMS_PER_CATEGORY = 200;
|
|
3103
3315
|
function readJsonSafe(file) {
|
|
3104
3316
|
try {
|
|
3105
|
-
return JSON.parse(
|
|
3317
|
+
return JSON.parse(readFileSync13(file, "utf-8"));
|
|
3106
3318
|
} catch {
|
|
3107
3319
|
return void 0;
|
|
3108
3320
|
}
|
|
@@ -3203,16 +3415,16 @@ var MCP_SDK_IMPORT_RE = /@modelcontextprotocol\/sdk/;
|
|
|
3203
3415
|
function extractMcpCapabilities(packageDir) {
|
|
3204
3416
|
const buckets = { tools: /* @__PURE__ */ new Set(), resources: /* @__PURE__ */ new Set(), prompts: /* @__PURE__ */ new Set() };
|
|
3205
3417
|
let hasPackageJsonMarker = false;
|
|
3206
|
-
const pkgPath =
|
|
3207
|
-
if (
|
|
3418
|
+
const pkgPath = join15(packageDir, "package.json");
|
|
3419
|
+
if (existsSync12(pkgPath)) {
|
|
3208
3420
|
const pkg2 = readJsonSafe(pkgPath);
|
|
3209
3421
|
if (pkg2 && typeof pkg2 === "object" && !Array.isArray(pkg2)) {
|
|
3210
3422
|
hasPackageJsonMarker = hasPackageJsonMcpMarkers(pkg2);
|
|
3211
3423
|
}
|
|
3212
3424
|
}
|
|
3213
3425
|
for (const manifestName of [".mcp.json", "mcp.json"]) {
|
|
3214
|
-
const manifestPath =
|
|
3215
|
-
if (!
|
|
3426
|
+
const manifestPath = join15(packageDir, manifestName);
|
|
3427
|
+
if (!existsSync12(manifestPath)) continue;
|
|
3216
3428
|
const parsed = readJsonSafe(manifestPath);
|
|
3217
3429
|
if (parsed !== void 0) {
|
|
3218
3430
|
collectDeclaredFromManifest(parsed, buckets);
|
|
@@ -3288,8 +3500,8 @@ var PYTHON_STDLIB = /* @__PURE__ */ new Set([
|
|
|
3288
3500
|
"__future__"
|
|
3289
3501
|
]);
|
|
3290
3502
|
function profileSkill(inputPath) {
|
|
3291
|
-
const p =
|
|
3292
|
-
if (!
|
|
3503
|
+
const p = resolve4(expandHome(inputPath));
|
|
3504
|
+
if (!existsSync13(p)) {
|
|
3293
3505
|
throw new Error(`path does not exist: ${p}`);
|
|
3294
3506
|
}
|
|
3295
3507
|
const stat = statSync7(p);
|
|
@@ -3319,7 +3531,7 @@ function asProfile(candidatePath, artifact) {
|
|
|
3319
3531
|
return asSkillProfile(profile, `at ${candidatePath}`);
|
|
3320
3532
|
}
|
|
3321
3533
|
function classifyInput(p) {
|
|
3322
|
-
if (
|
|
3534
|
+
if (existsSync13(join16(p, ".claude-plugin", "plugin.json"))) return "plugin";
|
|
3323
3535
|
if (detectArtifactType(p) === "mcp") return "mcp";
|
|
3324
3536
|
return "skill";
|
|
3325
3537
|
}
|
|
@@ -3327,10 +3539,10 @@ var MCP_SDK_DEP2 = "@modelcontextprotocol/sdk";
|
|
|
3327
3539
|
var MCP_KEYWORD2 = "mcp-server";
|
|
3328
3540
|
var MCP_BIN_PREFIX2 = /^mcp-/i;
|
|
3329
3541
|
function readPackageJsonSafe(root) {
|
|
3330
|
-
const pkgPath =
|
|
3331
|
-
if (!
|
|
3542
|
+
const pkgPath = join16(root, "package.json");
|
|
3543
|
+
if (!existsSync13(pkgPath)) return void 0;
|
|
3332
3544
|
try {
|
|
3333
|
-
return JSON.parse(
|
|
3545
|
+
return JSON.parse(readFileSync14(pkgPath, "utf-8"));
|
|
3334
3546
|
} catch {
|
|
3335
3547
|
return void 0;
|
|
3336
3548
|
}
|
|
@@ -3351,7 +3563,7 @@ function hasMcpPackageMarkers(pkg2) {
|
|
|
3351
3563
|
return false;
|
|
3352
3564
|
}
|
|
3353
3565
|
function detectArtifactType(root) {
|
|
3354
|
-
if (
|
|
3566
|
+
if (existsSync13(join16(root, ".mcp.json")) || existsSync13(join16(root, "mcp.json"))) return "mcp";
|
|
3355
3567
|
const pkg2 = readPackageJsonSafe(root);
|
|
3356
3568
|
if (pkg2 && hasMcpPackageMarkers(pkg2)) return "mcp";
|
|
3357
3569
|
return "skill";
|
|
@@ -3361,7 +3573,7 @@ function resolveSkillMd(inputPath) {
|
|
|
3361
3573
|
let tmpDir;
|
|
3362
3574
|
const initialStat = statSync7(currentPath);
|
|
3363
3575
|
if (extname6(currentPath) === ".skill" || initialStat.isFile() && isZipFile(currentPath)) {
|
|
3364
|
-
tmpDir = mkdtempSync2(
|
|
3576
|
+
tmpDir = mkdtempSync2(join16(tmpdir(), "forge_"));
|
|
3365
3577
|
extractZip(currentPath, tmpDir);
|
|
3366
3578
|
currentPath = tmpDir;
|
|
3367
3579
|
}
|
|
@@ -3370,8 +3582,8 @@ function resolveSkillMd(inputPath) {
|
|
|
3370
3582
|
return { skillMd: currentPath, root: dirname6(currentPath), tmpDir };
|
|
3371
3583
|
}
|
|
3372
3584
|
if (stat.isDirectory()) {
|
|
3373
|
-
const direct =
|
|
3374
|
-
if (
|
|
3585
|
+
const direct = join16(currentPath, "SKILL.md");
|
|
3586
|
+
if (existsSync13(direct)) {
|
|
3375
3587
|
return { skillMd: direct, root: currentPath, tmpDir };
|
|
3376
3588
|
}
|
|
3377
3589
|
const matches = findSkillMdOneLevelDown(currentPath);
|
|
@@ -3396,16 +3608,16 @@ function findSkillMdOneLevelDown(dir) {
|
|
|
3396
3608
|
const matches = /* @__PURE__ */ new Set();
|
|
3397
3609
|
for (const entry of readdirSync6(dir, { withFileTypes: true })) {
|
|
3398
3610
|
if (entry.isDirectory()) {
|
|
3399
|
-
const candidate =
|
|
3400
|
-
if (
|
|
3611
|
+
const candidate = join16(dir, entry.name, "SKILL.md");
|
|
3612
|
+
if (existsSync13(candidate)) matches.add(candidate);
|
|
3401
3613
|
}
|
|
3402
3614
|
}
|
|
3403
|
-
const skillsDir =
|
|
3615
|
+
const skillsDir = join16(dir, "skills");
|
|
3404
3616
|
if (isDir(skillsDir)) {
|
|
3405
3617
|
for (const entry of readdirSync6(skillsDir, { withFileTypes: true })) {
|
|
3406
3618
|
if (entry.isDirectory()) {
|
|
3407
|
-
const candidate =
|
|
3408
|
-
if (
|
|
3619
|
+
const candidate = join16(skillsDir, entry.name, "SKILL.md");
|
|
3620
|
+
if (existsSync13(candidate)) matches.add(candidate);
|
|
3409
3621
|
}
|
|
3410
3622
|
}
|
|
3411
3623
|
}
|
|
@@ -3516,9 +3728,9 @@ function detectLicense(root, fm) {
|
|
|
3516
3728
|
if (fm.license) return fm.license;
|
|
3517
3729
|
const names = ["LICENSE", "LICENSE.md", "LICENSE.txt", "COPYING", "license"];
|
|
3518
3730
|
for (const name of names) {
|
|
3519
|
-
const p =
|
|
3520
|
-
if (
|
|
3521
|
-
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();
|
|
3522
3734
|
for (const [hint, label2] of Object.entries(SPDX_HINTS)) {
|
|
3523
3735
|
if (head.includes(hint)) return label2;
|
|
3524
3736
|
}
|
|
@@ -3530,7 +3742,7 @@ function detectLicense(root, fm) {
|
|
|
3530
3742
|
function walkFiles(dir) {
|
|
3531
3743
|
const out = [];
|
|
3532
3744
|
for (const entry of readdirSync6(dir, { withFileTypes: true })) {
|
|
3533
|
-
const full =
|
|
3745
|
+
const full = join16(dir, entry.name);
|
|
3534
3746
|
if (entry.isDirectory()) {
|
|
3535
3747
|
out.push(...walkFiles(full));
|
|
3536
3748
|
} else if (entry.isFile()) {
|
|
@@ -3542,7 +3754,7 @@ function walkFiles(dir) {
|
|
|
3542
3754
|
function countResources(root) {
|
|
3543
3755
|
const resources = {};
|
|
3544
3756
|
for (const d of RESOURCE_DIRS) {
|
|
3545
|
-
const dp =
|
|
3757
|
+
const dp = join16(root, d);
|
|
3546
3758
|
if (isDir(dp)) {
|
|
3547
3759
|
resources[d] = walkFiles(dp).length;
|
|
3548
3760
|
}
|
|
@@ -3553,11 +3765,11 @@ function scanMcpAndImports(text, root) {
|
|
|
3553
3765
|
const mcpRefRe = /mcp__[a-zA-Z0-9_]+/g;
|
|
3554
3766
|
const mcpRefs = new Set(text.match(mcpRefRe) ?? []);
|
|
3555
3767
|
const imports = /* @__PURE__ */ new Set();
|
|
3556
|
-
const scriptsDir =
|
|
3768
|
+
const scriptsDir = join16(root, "scripts");
|
|
3557
3769
|
if (isDir(scriptsDir)) {
|
|
3558
3770
|
for (const f of walkFiles(scriptsDir)) {
|
|
3559
3771
|
if (!f.endsWith(".py")) continue;
|
|
3560
|
-
const ft =
|
|
3772
|
+
const ft = readFileSync14(f, "utf-8");
|
|
3561
3773
|
for (const m2 of ft.match(mcpRefRe) ?? []) mcpRefs.add(m2);
|
|
3562
3774
|
const importRe = /^\s*(?:import|from)\s+([a-zA-Z0-9_]+)/gm;
|
|
3563
3775
|
let m;
|
|
@@ -3574,7 +3786,7 @@ function scanMcpAndImports(text, root) {
|
|
|
3574
3786
|
function profileSkillDir(inputPath) {
|
|
3575
3787
|
const { skillMd, root, tmpDir } = resolveSkillMd(inputPath);
|
|
3576
3788
|
try {
|
|
3577
|
-
const text =
|
|
3789
|
+
const text = readFileSync14(skillMd, "utf-8");
|
|
3578
3790
|
const fm = parseFrontmatter(text);
|
|
3579
3791
|
const bodyLines = (text.match(/\n/g)?.length ?? 0) + 1;
|
|
3580
3792
|
const headers = Array.from(text.matchAll(/^#{1,3}\s+(.+)$/gm)).map((m) => m[1]);
|
|
@@ -3603,19 +3815,19 @@ function profileSkillDir(inputPath) {
|
|
|
3603
3815
|
}
|
|
3604
3816
|
}
|
|
3605
3817
|
function profilePlugin(p) {
|
|
3606
|
-
const manifestPath =
|
|
3818
|
+
const manifestPath = join16(p, ".claude-plugin", "plugin.json");
|
|
3607
3819
|
let manifest;
|
|
3608
3820
|
try {
|
|
3609
|
-
manifest = JSON.parse(
|
|
3821
|
+
manifest = JSON.parse(readFileSync14(manifestPath, "utf-8"));
|
|
3610
3822
|
} catch (err) {
|
|
3611
3823
|
throw new Error(`could not parse plugin.json under ${p}: ${err.message}`);
|
|
3612
3824
|
}
|
|
3613
|
-
const skillsDir =
|
|
3825
|
+
const skillsDir = join16(p, "skills");
|
|
3614
3826
|
let skills = [];
|
|
3615
3827
|
if (isDir(skillsDir)) {
|
|
3616
|
-
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();
|
|
3617
3829
|
}
|
|
3618
|
-
const commandsDir =
|
|
3830
|
+
const commandsDir = join16(p, "commands");
|
|
3619
3831
|
const commands = isDir(commandsDir) ? readdirSync6(commandsDir).filter((f) => f.endsWith(".md")).length : 0;
|
|
3620
3832
|
const mc = manifest.mcpServers ?? manifest.mcp_servers ?? {};
|
|
3621
3833
|
const mcpServers = typeof mc === "object" && mc !== null ? Object.keys(mc).sort() : [];
|
|
@@ -3633,11 +3845,11 @@ function profilePlugin(p) {
|
|
|
3633
3845
|
};
|
|
3634
3846
|
}
|
|
3635
3847
|
function profileMcp(p) {
|
|
3636
|
-
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;
|
|
3637
3849
|
if (explicitConfig) {
|
|
3638
3850
|
let cfg;
|
|
3639
3851
|
try {
|
|
3640
|
-
cfg = JSON.parse(
|
|
3852
|
+
cfg = JSON.parse(readFileSync14(explicitConfig, "utf-8"));
|
|
3641
3853
|
} catch (err) {
|
|
3642
3854
|
throw new Error(`could not parse MCP config ${explicitConfig}: ${err.message}`);
|
|
3643
3855
|
}
|
|
@@ -3653,10 +3865,10 @@ function profileMcp(p) {
|
|
|
3653
3865
|
capabilities: extractMcpCapabilities(p)
|
|
3654
3866
|
};
|
|
3655
3867
|
}
|
|
3656
|
-
const pkgPath =
|
|
3868
|
+
const pkgPath = join16(p, "package.json");
|
|
3657
3869
|
let pkg2;
|
|
3658
3870
|
try {
|
|
3659
|
-
pkg2 = JSON.parse(
|
|
3871
|
+
pkg2 = JSON.parse(readFileSync14(pkgPath, "utf-8"));
|
|
3660
3872
|
} catch (err) {
|
|
3661
3873
|
throw new Error(`could not parse package.json ${pkgPath}: ${err.message}`);
|
|
3662
3874
|
}
|
|
@@ -3673,14 +3885,14 @@ function profileMcp(p) {
|
|
|
3673
3885
|
}
|
|
3674
3886
|
|
|
3675
3887
|
// src/gate/mcpOverlap.ts
|
|
3676
|
-
import { existsSync as
|
|
3677
|
-
import { resolve as
|
|
3888
|
+
import { existsSync as existsSync14, readFileSync as readFileSync15 } from "fs";
|
|
3889
|
+
import { resolve as resolve5 } from "path";
|
|
3678
3890
|
function readTargetServers(targetPath) {
|
|
3679
|
-
const path =
|
|
3680
|
-
if (!
|
|
3891
|
+
const path = resolve5(expandHome(targetPath));
|
|
3892
|
+
if (!existsSync14(path)) return [];
|
|
3681
3893
|
let cfg;
|
|
3682
3894
|
try {
|
|
3683
|
-
cfg = JSON.parse(
|
|
3895
|
+
cfg = JSON.parse(readFileSync15(path, "utf-8"));
|
|
3684
3896
|
} catch {
|
|
3685
3897
|
return [];
|
|
3686
3898
|
}
|
|
@@ -3736,7 +3948,7 @@ function score2(candidate, target) {
|
|
|
3736
3948
|
function scanMcpOverlap(candidate, mcpTargetPaths) {
|
|
3737
3949
|
const ranking = [];
|
|
3738
3950
|
for (const targetPath of mcpTargetPaths) {
|
|
3739
|
-
const resolvedPath =
|
|
3951
|
+
const resolvedPath = resolve5(expandHome(targetPath));
|
|
3740
3952
|
for (const target of readTargetServers(targetPath)) {
|
|
3741
3953
|
ranking.push({ skill: target.name, path: resolvedPath, score: score2(candidate, target) });
|
|
3742
3954
|
}
|
|
@@ -3753,13 +3965,13 @@ function scanMcpOverlap(candidate, mcpTargetPaths) {
|
|
|
3753
3965
|
}
|
|
3754
3966
|
|
|
3755
3967
|
// src/gate/skillMap.ts
|
|
3756
|
-
import { readFileSync as
|
|
3757
|
-
import { resolve as
|
|
3968
|
+
import { readFileSync as readFileSync16 } from "fs";
|
|
3969
|
+
import { resolve as resolve6 } from "path";
|
|
3758
3970
|
function loadSkillMap(path) {
|
|
3759
|
-
const resolved =
|
|
3971
|
+
const resolved = resolve6(expandHome(path));
|
|
3760
3972
|
let text;
|
|
3761
3973
|
try {
|
|
3762
|
-
text =
|
|
3974
|
+
text = readFileSync16(resolved, "utf-8");
|
|
3763
3975
|
} catch (err) {
|
|
3764
3976
|
throw new Error(`could not read skill map at ${resolved}: ${err.message}`);
|
|
3765
3977
|
}
|
|
@@ -4059,7 +4271,7 @@ function resolveMcpCandidatePath(entry) {
|
|
|
4059
4271
|
children = readdirSync7(entry.quarantinePath, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => d.name);
|
|
4060
4272
|
} catch {
|
|
4061
4273
|
}
|
|
4062
|
-
return children.length === 1 ?
|
|
4274
|
+
return children.length === 1 ? join17(entry.quarantinePath, children[0]) : entry.quarantinePath;
|
|
4063
4275
|
}
|
|
4064
4276
|
async function runGate(candidatePath, skillsRoot, {
|
|
4065
4277
|
proFeatureLabel = "overlap analysis",
|
|
@@ -4108,17 +4320,17 @@ async function runGate(candidatePath, skillsRoot, {
|
|
|
4108
4320
|
|
|
4109
4321
|
// src/queue.ts
|
|
4110
4322
|
import { randomUUID as randomUUID3 } from "crypto";
|
|
4111
|
-
import { existsSync as
|
|
4112
|
-
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";
|
|
4113
4325
|
function getQueuePath() {
|
|
4114
|
-
return
|
|
4326
|
+
return join18(getHomeDir(), "queue.json");
|
|
4115
4327
|
}
|
|
4116
4328
|
function readQueue() {
|
|
4117
4329
|
const path = getQueuePath();
|
|
4118
|
-
if (!
|
|
4330
|
+
if (!existsSync15(path)) {
|
|
4119
4331
|
return { version: 1, entries: [] };
|
|
4120
4332
|
}
|
|
4121
|
-
const raw =
|
|
4333
|
+
const raw = readFileSync17(path, "utf-8");
|
|
4122
4334
|
const parsed = JSON.parse(raw);
|
|
4123
4335
|
return { version: 1, entries: parsed.entries ?? [] };
|
|
4124
4336
|
}
|
|
@@ -4126,7 +4338,7 @@ function writeQueue(queue2) {
|
|
|
4126
4338
|
const home = getHomeDir();
|
|
4127
4339
|
mkdirSync6(home, { recursive: true });
|
|
4128
4340
|
const finalPath = getQueuePath();
|
|
4129
|
-
const tempPath =
|
|
4341
|
+
const tempPath = join18(home, `.queue.json.tmp-${randomUUID3()}`);
|
|
4130
4342
|
try {
|
|
4131
4343
|
writeFileSync6(tempPath, JSON.stringify(queue2, null, 2) + "\n", "utf-8");
|
|
4132
4344
|
renameSync4(tempPath, finalPath);
|
|
@@ -4175,19 +4387,19 @@ function generateId(existingIds) {
|
|
|
4175
4387
|
|
|
4176
4388
|
// src/handoff.ts
|
|
4177
4389
|
import { spawnSync } from "child_process";
|
|
4178
|
-
import { existsSync as
|
|
4179
|
-
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";
|
|
4180
4392
|
import { fileURLToPath } from "url";
|
|
4181
4393
|
var KNOWN_AGENT_BINARIES = ["claude", "codex", "cursor-agent", "windsurf", "opencode", "gemini"];
|
|
4182
4394
|
var HANDOFF_INSTRUCTION = "Read {prompt} and follow its instructions for the skill installed at {path}";
|
|
4183
4395
|
function resolvePromptPath(assetName = "ingest-prompt.md") {
|
|
4184
|
-
if (assetName.length === 0 || assetName.includes("\0") ||
|
|
4396
|
+
if (assetName.length === 0 || assetName.includes("\0") || isAbsolute3(assetName) || win32.isAbsolute(assetName) || assetName.split(/[\\/]/).includes("..")) {
|
|
4185
4397
|
throw new UserError(`Invalid handoff prompt asset path: ${assetName}`);
|
|
4186
4398
|
}
|
|
4187
4399
|
const here = dirname7(fileURLToPath(import.meta.url));
|
|
4188
|
-
const builtCandidate =
|
|
4189
|
-
if (
|
|
4190
|
-
return
|
|
4400
|
+
const builtCandidate = join19(here, assetName);
|
|
4401
|
+
if (existsSync16(builtCandidate)) return builtCandidate;
|
|
4402
|
+
return join19(here, "..", "assets", assetName);
|
|
4191
4403
|
}
|
|
4192
4404
|
function defaultIsOnPath(bin) {
|
|
4193
4405
|
const finder = process.platform === "win32" ? "where" : "which";
|
|
@@ -4249,15 +4461,15 @@ function runHandoff(config, installedPath, options = {}) {
|
|
|
4249
4461
|
}
|
|
4250
4462
|
|
|
4251
4463
|
// src/commands/init.ts
|
|
4252
|
-
import { existsSync as
|
|
4464
|
+
import { existsSync as existsSync19 } from "fs";
|
|
4253
4465
|
import { homedir as homedir5 } from "os";
|
|
4254
|
-
import { join as
|
|
4466
|
+
import { join as join25 } from "path";
|
|
4255
4467
|
import { createInterface as createInterface3 } from "readline/promises";
|
|
4256
4468
|
|
|
4257
4469
|
// src/agents.ts
|
|
4258
4470
|
import { statSync as statSync8 } from "fs";
|
|
4259
4471
|
import { homedir as homedir3 } from "os";
|
|
4260
|
-
import { join as
|
|
4472
|
+
import { join as join20, resolve as resolve7 } from "path";
|
|
4261
4473
|
function isFile(p) {
|
|
4262
4474
|
try {
|
|
4263
4475
|
return statSync8(p).isFile();
|
|
@@ -4266,12 +4478,12 @@ function isFile(p) {
|
|
|
4266
4478
|
}
|
|
4267
4479
|
}
|
|
4268
4480
|
function projectPath(root, cwd) {
|
|
4269
|
-
return root ?
|
|
4481
|
+
return root ? resolve7(cwd, root) : void 0;
|
|
4270
4482
|
}
|
|
4271
4483
|
function globalPath(root, home) {
|
|
4272
4484
|
if (!root) return void 0;
|
|
4273
4485
|
if (root === "~") return home;
|
|
4274
|
-
if (root.startsWith("~/")) return
|
|
4486
|
+
if (root.startsWith("~/")) return join20(home, root.slice(2));
|
|
4275
4487
|
return root;
|
|
4276
4488
|
}
|
|
4277
4489
|
function makeAgent(spec) {
|
|
@@ -4288,30 +4500,30 @@ function makeAgent(spec) {
|
|
|
4288
4500
|
};
|
|
4289
4501
|
return agent;
|
|
4290
4502
|
}
|
|
4291
|
-
function resolveAgentRoot(agent,
|
|
4292
|
-
return
|
|
4503
|
+
function resolveAgentRoot(agent, scope2, cwd = process.cwd(), home = homedir3()) {
|
|
4504
|
+
return scope2 === "project" ? projectPath(agent.projectRoot, cwd) : globalPath(agent.globalRoot, home);
|
|
4293
4505
|
}
|
|
4294
4506
|
function agentCliPresence(agent, pathEnv = process.env.PATH) {
|
|
4295
4507
|
if (!agent.cliBinaries || agent.cliBinaries.length === 0) return "unknown";
|
|
4296
4508
|
return agent.cliBinaries.some((bin) => resolveBinaryOnPath(bin, pathEnv) !== void 0) ? "installed" : "absent";
|
|
4297
4509
|
}
|
|
4298
4510
|
function detectAgentRoot(agent, cwd = process.cwd(), home = homedir3()) {
|
|
4299
|
-
for (const
|
|
4300
|
-
const root = resolveAgentRoot(agent,
|
|
4301
|
-
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 };
|
|
4302
4514
|
}
|
|
4303
4515
|
return void 0;
|
|
4304
4516
|
}
|
|
4305
|
-
function resolveMcpConfigPath(agent,
|
|
4517
|
+
function resolveMcpConfigPath(agent, scope2, cwd = process.cwd(), home = homedir3()) {
|
|
4306
4518
|
if (!agent.mcpConfig) return void 0;
|
|
4307
|
-
return
|
|
4519
|
+
return scope2 === "project" ? projectPath(agent.mcpConfig.project, cwd) : globalPath(agent.mcpConfig.global, home);
|
|
4308
4520
|
}
|
|
4309
4521
|
function detectMcpConfigFiles(agent, cwd = process.cwd(), home = homedir3()) {
|
|
4310
4522
|
if (!agent.mcpConfig) return [];
|
|
4311
4523
|
const found = [];
|
|
4312
|
-
for (const
|
|
4313
|
-
const path = resolveMcpConfigPath(agent,
|
|
4314
|
-
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 });
|
|
4315
4527
|
}
|
|
4316
4528
|
return found;
|
|
4317
4529
|
}
|
|
@@ -4501,12 +4713,12 @@ var AGENTS = [
|
|
|
4501
4713
|
].map(makeAgent);
|
|
4502
4714
|
|
|
4503
4715
|
// src/gitPreflight.ts
|
|
4504
|
-
import { relative as
|
|
4716
|
+
import { relative as relative3, sep as sep4 } from "path";
|
|
4505
4717
|
function splitNulTerminated(output) {
|
|
4506
4718
|
return output.split("\0").filter((s) => s.length > 0);
|
|
4507
4719
|
}
|
|
4508
4720
|
function toPosixPath(p) {
|
|
4509
|
-
return p.split(
|
|
4721
|
+
return p.split(sep4).join("/");
|
|
4510
4722
|
}
|
|
4511
4723
|
var missingResult = (root) => ({
|
|
4512
4724
|
root,
|
|
@@ -4536,7 +4748,7 @@ async function evaluateGitPreflight(root, deps = {}) {
|
|
|
4536
4748
|
if (toplevel.code !== 0) return notInRepoResult(root);
|
|
4537
4749
|
const repoRoot = toplevel.stdout.trim();
|
|
4538
4750
|
if (!repoRoot) return notInRepoResult(root);
|
|
4539
|
-
const rel = toPosixPath(
|
|
4751
|
+
const rel = toPosixPath(relative3(repoRoot, root));
|
|
4540
4752
|
const pathspec = rel === "" ? "." : rel;
|
|
4541
4753
|
const tracked = await exec("git", ["--literal-pathspecs", "-C", repoRoot, "ls-files", "-z", "--", pathspec], { cwd: repoRoot });
|
|
4542
4754
|
const untracked = await exec(
|
|
@@ -4607,7 +4819,7 @@ function untrackedAddCommitCommands(root, repoRoot) {
|
|
|
4607
4819
|
];
|
|
4608
4820
|
}
|
|
4609
4821
|
function ignoredUnignoreSuggestions(root, repoRoot) {
|
|
4610
|
-
const rel = toPosixPath(
|
|
4822
|
+
const rel = toPosixPath(relative3(repoRoot, root));
|
|
4611
4823
|
return [`!${rel}/`, `!${rel}/**`];
|
|
4612
4824
|
}
|
|
4613
4825
|
async function runGitCommands(commands, deps = {}) {
|
|
@@ -4630,9 +4842,52 @@ function describeGitPreflight(result) {
|
|
|
4630
4842
|
return parts.join(", ");
|
|
4631
4843
|
}
|
|
4632
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
|
+
|
|
4633
4888
|
// src/commands/audit.ts
|
|
4634
|
-
import { existsSync as
|
|
4635
|
-
import { join as
|
|
4889
|
+
import { existsSync as existsSync18 } from "fs";
|
|
4890
|
+
import { join as join24, resolve as resolvePath5 } from "path";
|
|
4636
4891
|
import { createInterface as createInterface2 } from "readline/promises";
|
|
4637
4892
|
|
|
4638
4893
|
// src/decision.ts
|
|
@@ -4665,13 +4920,13 @@ ${prompt}`);
|
|
|
4665
4920
|
}
|
|
4666
4921
|
|
|
4667
4922
|
// src/pluginRoots.ts
|
|
4668
|
-
import { existsSync as
|
|
4923
|
+
import { existsSync as existsSync17, readFileSync as readFileSync19, readdirSync as readdirSync8, realpathSync as realpathSync5 } from "fs";
|
|
4669
4924
|
import { homedir as homedir4 } from "os";
|
|
4670
|
-
import { isAbsolute as
|
|
4925
|
+
import { isAbsolute as isAbsolute5, join as join22, relative as relative5, resolve as resolvePath3, sep as sep6 } from "path";
|
|
4671
4926
|
function readJsonFileSafe(path) {
|
|
4672
4927
|
let raw;
|
|
4673
4928
|
try {
|
|
4674
|
-
raw =
|
|
4929
|
+
raw = readFileSync19(path, "utf-8");
|
|
4675
4930
|
} catch (err) {
|
|
4676
4931
|
return { problem: err.code === "ENOENT" ? "missing" : "unreadable" };
|
|
4677
4932
|
}
|
|
@@ -4696,31 +4951,34 @@ function extractEnabledMap(data) {
|
|
|
4696
4951
|
return out;
|
|
4697
4952
|
}
|
|
4698
4953
|
function extractRegistry(data) {
|
|
4699
|
-
|
|
4954
|
+
const empty = { entries: {}, malformed: {} };
|
|
4955
|
+
if (typeof data !== "object" || data === null) return empty;
|
|
4700
4956
|
const pluginsRaw = data.plugins;
|
|
4701
|
-
if (typeof pluginsRaw !== "object" || pluginsRaw === null) return
|
|
4957
|
+
if (typeof pluginsRaw !== "object" || pluginsRaw === null) return empty;
|
|
4702
4958
|
const out = {};
|
|
4959
|
+
const malformed = {};
|
|
4703
4960
|
for (const [id, entriesRaw] of Object.entries(pluginsRaw)) {
|
|
4704
4961
|
if (!Array.isArray(entriesRaw)) continue;
|
|
4705
4962
|
const entries = [];
|
|
4706
4963
|
for (const raw of entriesRaw) {
|
|
4707
|
-
|
|
4708
|
-
const
|
|
4709
|
-
const scope = rec.scope;
|
|
4964
|
+
const rec = typeof raw === "object" && raw !== null ? raw : {};
|
|
4965
|
+
const scope2 = rec.scope;
|
|
4710
4966
|
const installPath = rec.installPath;
|
|
4711
4967
|
const version = rec.version;
|
|
4712
|
-
if ((
|
|
4968
|
+
if ((scope2 === "user" || scope2 === "project" || scope2 === "local") && typeof installPath === "string" && typeof version === "string") {
|
|
4713
4969
|
entries.push({
|
|
4714
|
-
scope,
|
|
4970
|
+
scope: scope2,
|
|
4715
4971
|
projectPath: typeof rec.projectPath === "string" ? rec.projectPath : void 0,
|
|
4716
4972
|
installPath,
|
|
4717
4973
|
version
|
|
4718
4974
|
});
|
|
4975
|
+
} else {
|
|
4976
|
+
malformed[id] = (malformed[id] ?? 0) + 1;
|
|
4719
4977
|
}
|
|
4720
4978
|
}
|
|
4721
4979
|
if (entries.length > 0) out[id] = entries;
|
|
4722
4980
|
}
|
|
4723
|
-
return out;
|
|
4981
|
+
return { entries: out, malformed };
|
|
4724
4982
|
}
|
|
4725
4983
|
function splitPluginId(id) {
|
|
4726
4984
|
const idx = id.lastIndexOf("@");
|
|
@@ -4729,7 +4987,7 @@ function splitPluginId(id) {
|
|
|
4729
4987
|
}
|
|
4730
4988
|
function safeRealpath(p) {
|
|
4731
4989
|
try {
|
|
4732
|
-
return
|
|
4990
|
+
return realpathSync5(p);
|
|
4733
4991
|
} catch {
|
|
4734
4992
|
return void 0;
|
|
4735
4993
|
}
|
|
@@ -4764,8 +5022,8 @@ function selectInstallEntry(entries, cwd) {
|
|
|
4764
5022
|
}
|
|
4765
5023
|
function isInside2(child, root) {
|
|
4766
5024
|
if (child === root) return true;
|
|
4767
|
-
const rel =
|
|
4768
|
-
return rel !== "" && !rel.startsWith(".." +
|
|
5025
|
+
const rel = relative5(root, child);
|
|
5026
|
+
return rel !== "" && !rel.startsWith(".." + sep6) && rel !== ".." && !isAbsolute5(rel);
|
|
4769
5027
|
}
|
|
4770
5028
|
var DEFAULT_SKILL_LAYOUT = { paths: ["./skills/"], explicit: false };
|
|
4771
5029
|
function normalizeDeclaredSkillsField(manifest, pluginId, notices) {
|
|
@@ -4781,7 +5039,7 @@ function resolveDeclaredPath(installPath, realInstall, declaredPath, explicit, p
|
|
|
4781
5039
|
const joined = resolvePath3(installPath, declaredPath);
|
|
4782
5040
|
let real;
|
|
4783
5041
|
try {
|
|
4784
|
-
real =
|
|
5042
|
+
real = realpathSync5(joined);
|
|
4785
5043
|
} catch {
|
|
4786
5044
|
if (explicit) notices.push(`plugin ${pluginId}: declared skill path does not exist, skipping: ${declaredPath}`);
|
|
4787
5045
|
return [];
|
|
@@ -4790,7 +5048,7 @@ function resolveDeclaredPath(installPath, realInstall, declaredPath, explicit, p
|
|
|
4790
5048
|
notices.push(`plugin ${pluginId}: declared skill path escapes the install root, skipping: ${declaredPath}`);
|
|
4791
5049
|
return [];
|
|
4792
5050
|
}
|
|
4793
|
-
if (
|
|
5051
|
+
if (existsSync17(join22(real, "SKILL.md"))) return [real];
|
|
4794
5052
|
let entries;
|
|
4795
5053
|
try {
|
|
4796
5054
|
entries = readdirSync8(real, { withFileTypes: true });
|
|
@@ -4799,15 +5057,24 @@ function resolveDeclaredPath(installPath, realInstall, declaredPath, explicit, p
|
|
|
4799
5057
|
}
|
|
4800
5058
|
const found = [];
|
|
4801
5059
|
for (const entry of entries) {
|
|
4802
|
-
|
|
4803
|
-
|
|
4804
|
-
|
|
5060
|
+
let childPath = join22(real, entry.name);
|
|
5061
|
+
if (entry.isSymbolicLink()) {
|
|
5062
|
+
const target = safeRealpath(childPath);
|
|
5063
|
+
if (!target || !isInside2(target, realInstall)) {
|
|
5064
|
+
notices.push(`plugin ${pluginId}: skill dir symlink escapes the install root, skipping: ${join22(declaredPath, entry.name)}`);
|
|
5065
|
+
continue;
|
|
5066
|
+
}
|
|
5067
|
+
childPath = target;
|
|
5068
|
+
} else if (!entry.isDirectory()) {
|
|
5069
|
+
continue;
|
|
5070
|
+
}
|
|
5071
|
+
if (existsSync17(join22(childPath, "SKILL.md"))) found.push(childPath);
|
|
4805
5072
|
}
|
|
4806
5073
|
return found;
|
|
4807
5074
|
}
|
|
4808
5075
|
function resolveSkillDirs(installPath, pluginId) {
|
|
4809
5076
|
const notices = [];
|
|
4810
|
-
const manifestPath =
|
|
5077
|
+
const manifestPath = join22(installPath, ".claude-plugin", "plugin.json");
|
|
4811
5078
|
const manifestResult = readJsonFileSafe(manifestPath);
|
|
4812
5079
|
if (manifestResult.problem) {
|
|
4813
5080
|
notices.push(`plugin ${pluginId}: ${manifestResult.problem} manifest at ${manifestPath} \u2014 using default skill layout`);
|
|
@@ -4820,14 +5087,14 @@ function resolveSkillDirs(installPath, pluginId) {
|
|
|
4820
5087
|
skillDirs.add(dir);
|
|
4821
5088
|
}
|
|
4822
5089
|
}
|
|
4823
|
-
return { skillDirs: Array.from(skillDirs).sort(), notices };
|
|
5090
|
+
return { skillDirs: Array.from(skillDirs).sort(), notices, realInstall };
|
|
4824
5091
|
}
|
|
4825
5092
|
function discoverClaudePluginRoots(options = {}) {
|
|
4826
5093
|
const homeDir = options.homeDir ?? homedir4();
|
|
4827
5094
|
const cwd = options.cwd ?? process.cwd();
|
|
4828
|
-
const settingsPath = options.settingsPath ??
|
|
4829
|
-
const localSettingsPath = options.localSettingsPath ??
|
|
4830
|
-
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");
|
|
4831
5098
|
const notices = [];
|
|
4832
5099
|
const settingsResult = readJsonFileSafe(settingsPath);
|
|
4833
5100
|
if (settingsResult.problem) {
|
|
@@ -4841,7 +5108,7 @@ function discoverClaudePluginRoots(options = {}) {
|
|
|
4841
5108
|
}
|
|
4842
5109
|
const globalEnabled = extractEnabledMap(settingsResult.data);
|
|
4843
5110
|
let localEnabled = {};
|
|
4844
|
-
if (
|
|
5111
|
+
if (existsSync17(localSettingsPath)) {
|
|
4845
5112
|
const localResult = readJsonFileSafe(localSettingsPath);
|
|
4846
5113
|
if (localResult.problem) {
|
|
4847
5114
|
notices.push(`local settings ${describeProblem(localResult.problem, localSettingsPath)} \u2014 ignoring local plugin overrides`);
|
|
@@ -4851,27 +5118,44 @@ function discoverClaudePluginRoots(options = {}) {
|
|
|
4851
5118
|
}
|
|
4852
5119
|
const merged = { ...globalEnabled, ...localEnabled };
|
|
4853
5120
|
const enabledIds = Object.keys(merged).filter((id) => merged[id] === true).sort();
|
|
4854
|
-
const registry = extractRegistry(registryResult.data);
|
|
5121
|
+
const { entries: registry, malformed } = extractRegistry(registryResult.data);
|
|
4855
5122
|
const roots = [];
|
|
4856
5123
|
for (const id of enabledIds) {
|
|
4857
5124
|
const entries = registry[id];
|
|
5125
|
+
const dropped = malformed[id] ?? 0;
|
|
5126
|
+
if (dropped > 0) {
|
|
5127
|
+
notices.push(`plugin ${id}: ignored ${dropped} malformed installed_plugins.json entr${dropped === 1 ? "y" : "ies"} (missing installPath/version or unknown scope)`);
|
|
5128
|
+
}
|
|
4858
5129
|
if (!entries || entries.length === 0) {
|
|
4859
|
-
notices.push(`plugin ${id} is enabled but has no installed_plugins.json entry \u2014 skipping`);
|
|
5130
|
+
notices.push(`plugin ${id} is enabled but has no ${dropped > 0 ? "usable " : ""}installed_plugins.json entry \u2014 skipping`);
|
|
4860
5131
|
continue;
|
|
4861
5132
|
}
|
|
4862
5133
|
const entry = selectInstallEntry(entries, cwd);
|
|
4863
5134
|
const { plugin, marketplace } = splitPluginId(id);
|
|
4864
|
-
const { skillDirs, notices: manifestNotices } = resolveSkillDirs(entry.installPath, id);
|
|
5135
|
+
const { skillDirs, notices: manifestNotices, realInstall } = resolveSkillDirs(entry.installPath, id);
|
|
4865
5136
|
notices.push(...manifestNotices);
|
|
4866
|
-
roots.push({ pluginId: id, marketplace, plugin, version: entry.version, installPath: entry.installPath, skillDirs });
|
|
5137
|
+
roots.push({ pluginId: id, marketplace, plugin, version: entry.version, installPath: realInstall ?? entry.installPath, skillDirs });
|
|
4867
5138
|
}
|
|
4868
|
-
roots.sort((a, b) => a.pluginId.
|
|
5139
|
+
roots.sort((a, b) => a.pluginId < b.pluginId ? -1 : a.pluginId > b.pluginId ? 1 : 0);
|
|
4869
5140
|
return { roots, notices };
|
|
4870
5141
|
}
|
|
4871
5142
|
|
|
4872
5143
|
// src/usageSnapshot.ts
|
|
4873
|
-
import { readFileSync as
|
|
4874
|
-
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";
|
|
5146
|
+
function isValidCount(value) {
|
|
5147
|
+
return typeof value === "number" && Number.isSafeInteger(value) && value >= 0;
|
|
5148
|
+
}
|
|
5149
|
+
function toSnapshot(parsed, sourcePath) {
|
|
5150
|
+
return {
|
|
5151
|
+
usage: parsed.usage,
|
|
5152
|
+
exhaustive: parsed.exhaustive,
|
|
5153
|
+
windowDays: parsed.windowDays,
|
|
5154
|
+
generatedAt: parsed.generatedAt,
|
|
5155
|
+
sourcePath,
|
|
5156
|
+
...parsed.warning ? { warning: parsed.warning } : {}
|
|
5157
|
+
};
|
|
5158
|
+
}
|
|
4875
5159
|
function emptySnapshot(sourcePath, warning) {
|
|
4876
5160
|
return { usage: /* @__PURE__ */ new Map(), exhaustive: false, sourcePath, warning };
|
|
4877
5161
|
}
|
|
@@ -4887,32 +5171,40 @@ function parseSnapshotFile(raw) {
|
|
|
4887
5171
|
if (!report) return { error: 'usage snapshot has no "report" object' };
|
|
4888
5172
|
const generatedAt = typeof report.generated_at === "string" ? report.generated_at : void 0;
|
|
4889
5173
|
const windowDays = typeof report.window_days === "number" ? report.window_days : void 0;
|
|
4890
|
-
const uniqueSkillsUsed =
|
|
4891
|
-
const
|
|
5174
|
+
const uniqueSkillsUsed = report.unique_skills_used;
|
|
5175
|
+
const skillTotalsRaw = report.skill_totals;
|
|
5176
|
+
const skillTotals = skillTotalsRaw && typeof skillTotalsRaw === "object" && !Array.isArray(skillTotalsRaw) ? skillTotalsRaw : void 0;
|
|
5177
|
+
let warning = skillTotalsRaw !== void 0 && skillTotals === void 0 ? 'usage snapshot: "skill_totals" is not an object \u2014 ignoring it' : void 0;
|
|
4892
5178
|
const topSkills = Array.isArray(report.top_skills) ? report.top_skills : void 0;
|
|
4893
5179
|
const usage = /* @__PURE__ */ new Map();
|
|
4894
|
-
|
|
4895
|
-
|
|
4896
|
-
|
|
4897
|
-
|
|
4898
|
-
|
|
4899
|
-
|
|
4900
|
-
if (Array.isArray(row) && row.length === 2 && typeof row[0] === "string" && typeof row[1] === "number") {
|
|
4901
|
-
usage.set(row[0], row[1]);
|
|
4902
|
-
}
|
|
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;
|
|
4903
5186
|
}
|
|
5187
|
+
usage.set(row[0], row[1]);
|
|
4904
5188
|
}
|
|
4905
|
-
const
|
|
4906
|
-
|
|
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("; ");
|
|
5195
|
+
}
|
|
5196
|
+
return { usage, exhaustive, windowDays, generatedAt, warning };
|
|
4907
5197
|
}
|
|
4908
5198
|
function pickLatest(candidates) {
|
|
4909
5199
|
return candidates.reduce((best, cur) => {
|
|
4910
5200
|
if (!best) return cur;
|
|
4911
|
-
|
|
4912
|
-
|
|
4913
|
-
if (
|
|
4914
|
-
if (
|
|
4915
|
-
|
|
5201
|
+
const curAt = cur.parsed.generatedAt;
|
|
5202
|
+
const bestAt = best.parsed.generatedAt;
|
|
5203
|
+
if (curAt === void 0 && bestAt === void 0) return cur.path > best.path ? cur : best;
|
|
5204
|
+
if (curAt === void 0) return best;
|
|
5205
|
+
if (bestAt === void 0) return cur;
|
|
5206
|
+
if (curAt === bestAt) return cur.path > best.path ? cur : best;
|
|
5207
|
+
return curAt > bestAt ? cur : best;
|
|
4916
5208
|
}, void 0);
|
|
4917
5209
|
}
|
|
4918
5210
|
function loadUsageSnapshot(pathOrDir, opts = {}) {
|
|
@@ -4929,17 +5221,17 @@ function loadUsageSnapshot(pathOrDir, opts = {}) {
|
|
|
4929
5221
|
}
|
|
4930
5222
|
const candidates = [];
|
|
4931
5223
|
for (const name of entries) {
|
|
4932
|
-
const filePath =
|
|
4933
|
-
let
|
|
5224
|
+
const filePath = join23(resolved, name);
|
|
5225
|
+
let raw2;
|
|
4934
5226
|
try {
|
|
4935
|
-
|
|
5227
|
+
raw2 = readFileSync20(filePath, "utf-8");
|
|
4936
5228
|
} catch {
|
|
4937
5229
|
continue;
|
|
4938
5230
|
}
|
|
4939
|
-
const
|
|
4940
|
-
if ("error" in
|
|
4941
|
-
if (opts.windowDays !== void 0 &&
|
|
4942
|
-
candidates.push({ path: filePath,
|
|
5231
|
+
const parsed2 = parseSnapshotFile(raw2);
|
|
5232
|
+
if ("error" in parsed2) continue;
|
|
5233
|
+
if (opts.windowDays !== void 0 && parsed2.windowDays !== opts.windowDays) continue;
|
|
5234
|
+
candidates.push({ path: filePath, parsed: parsed2 });
|
|
4943
5235
|
}
|
|
4944
5236
|
const picked = pickLatest(candidates);
|
|
4945
5237
|
if (!picked) {
|
|
@@ -4948,20 +5240,17 @@ function loadUsageSnapshot(pathOrDir, opts = {}) {
|
|
|
4948
5240
|
opts.windowDays !== void 0 ? `no usage snapshot in ${resolved} matches --usage-window ${opts.windowDays}` : `no readable usage snapshot found in ${resolved}`
|
|
4949
5241
|
);
|
|
4950
5242
|
}
|
|
4951
|
-
|
|
4952
|
-
const parsed2 = parseSnapshotFile(raw2);
|
|
4953
|
-
if ("error" in parsed2) return emptySnapshot(picked.path, parsed2.error);
|
|
4954
|
-
return { usage: parsed2.usage, exhaustive: parsed2.exhaustive, windowDays: parsed2.windowDays, generatedAt: parsed2.generatedAt, sourcePath: picked.path };
|
|
5243
|
+
return toSnapshot(picked.parsed, picked.path);
|
|
4955
5244
|
}
|
|
4956
5245
|
let raw;
|
|
4957
5246
|
try {
|
|
4958
|
-
raw =
|
|
5247
|
+
raw = readFileSync20(resolved, "utf-8");
|
|
4959
5248
|
} catch (err) {
|
|
4960
5249
|
return emptySnapshot(resolved, `could not read usage snapshot (${err.message})`);
|
|
4961
5250
|
}
|
|
4962
5251
|
const parsed = parseSnapshotFile(raw);
|
|
4963
5252
|
if ("error" in parsed) return emptySnapshot(resolved, parsed.error);
|
|
4964
|
-
return
|
|
5253
|
+
return toSnapshot(parsed, resolved);
|
|
4965
5254
|
}
|
|
4966
5255
|
|
|
4967
5256
|
// src/commands/audit.ts
|
|
@@ -4970,18 +5259,21 @@ function resolvePluginAndUsageContext(config, options, deps = {}) {
|
|
|
4970
5259
|
if (options.usageWindow !== void 0 && (!Number.isInteger(options.usageWindow) || options.usageWindow < 1)) {
|
|
4971
5260
|
throw new UserError(`--usage-window must be a positive integer (got "${options.usageWindow}")`);
|
|
4972
5261
|
}
|
|
4973
|
-
|
|
4974
|
-
|
|
4975
|
-
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 ?? []];
|
|
4976
5267
|
if (wantPlugins) {
|
|
4977
5268
|
const discovered = discoverClaudePluginRoots(deps);
|
|
4978
5269
|
pluginRoots = discovered.roots;
|
|
4979
5270
|
notices.push(...discovered.notices);
|
|
4980
5271
|
}
|
|
4981
|
-
|
|
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;
|
|
4982
5274
|
const usage = usageSnapshotPath ? loadUsageSnapshot(usageSnapshotPath, { windowDays: options.usageWindow }) : void 0;
|
|
4983
5275
|
if (usage?.warning) notices.push(usage.warning);
|
|
4984
|
-
return { pluginRoots, usage, notices };
|
|
5276
|
+
return { pluginRoots, usage, notices, importedInventory };
|
|
4985
5277
|
}
|
|
4986
5278
|
function splitCsv(s) {
|
|
4987
5279
|
return s.split(",").map((x) => x.trim()).filter(Boolean);
|
|
@@ -5017,12 +5309,12 @@ async function captureBusinessProfile(rl, existing) {
|
|
|
5017
5309
|
function resolveReportPath(reportOption) {
|
|
5018
5310
|
if (reportOption) return resolvePath5(reportOption);
|
|
5019
5311
|
const iso = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
|
|
5020
|
-
return
|
|
5312
|
+
return join24(getHomeDir(), "reports", `audit-${iso}.md`);
|
|
5021
5313
|
}
|
|
5022
|
-
function summarizePluginRecommendations(
|
|
5314
|
+
function summarizePluginRecommendations(plugins2) {
|
|
5023
5315
|
const counts = { keep: 0, review: 0, unobserved: 0, unknown: 0 };
|
|
5024
|
-
for (const p of
|
|
5025
|
-
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})`;
|
|
5026
5318
|
}
|
|
5027
5319
|
function printTerminalSummary(report, reportPath, usage) {
|
|
5028
5320
|
const { summary } = report;
|
|
@@ -5078,7 +5370,7 @@ async function maybeHandoff(config, reportPath, options, rl) {
|
|
|
5078
5370
|
}
|
|
5079
5371
|
async function auditCommand(options = {}, deps = {}) {
|
|
5080
5372
|
await maybeOfferInit({ json: options.json, yes: options.yes, suppressAuditOffer: true });
|
|
5081
|
-
if (!
|
|
5373
|
+
if (!existsSync18(getConfigPath())) {
|
|
5082
5374
|
throw new UserError(
|
|
5083
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)."
|
|
5084
5376
|
);
|
|
@@ -5094,11 +5386,17 @@ async function auditCommand(options = {}, deps = {}) {
|
|
|
5094
5386
|
saveConfig(config);
|
|
5095
5387
|
}
|
|
5096
5388
|
}
|
|
5097
|
-
const { pluginRoots, usage, notices: pluginUsageNotices } = resolvePluginAndUsageContext(config, options, deps);
|
|
5389
|
+
const { pluginRoots, usage, importedInventory, notices: pluginUsageNotices } = resolvePluginAndUsageContext(config, options, deps);
|
|
5098
5390
|
const { records: accepted, notices: storeNotices } = readAcceptedFindings();
|
|
5099
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);
|
|
5100
5397
|
const extraNotices = [...storeNotices, ...pluginUsageNotices];
|
|
5101
5398
|
if (extraNotices.length > 0) report.notices = [...report.notices, ...extraNotices];
|
|
5399
|
+
if (options.compare) report.comparison = loadAuditComparison(report, options.compare);
|
|
5102
5400
|
const reportPath = resolveReportPath(options.report);
|
|
5103
5401
|
writeNewFile(reportPath, renderAuditMarkdown(report), "--report");
|
|
5104
5402
|
try {
|
|
@@ -5149,19 +5447,19 @@ function hasInstalledMember(group) {
|
|
|
5149
5447
|
return group.members.some((m) => m.presence === "installed");
|
|
5150
5448
|
}
|
|
5151
5449
|
function describeGroup(members, scopes) {
|
|
5152
|
-
const
|
|
5450
|
+
const scope2 = [...scopes].join("+");
|
|
5153
5451
|
const installed = members.filter((m) => m.presence === "installed").map((m) => m.agent.displayName);
|
|
5154
5452
|
const others = members.length - installed.length;
|
|
5155
5453
|
if (installed.length > 0) {
|
|
5156
5454
|
const share = others > 0 ? ` (+${others} other agent${others === 1 ? "" : "s"} share this path)` : "";
|
|
5157
|
-
return `on PATH: ${installed.join(", ")} (${
|
|
5455
|
+
return `on PATH: ${installed.join(", ")} (${scope2})${share}`;
|
|
5158
5456
|
}
|
|
5159
5457
|
const names = members.map((m) => m.agent.displayName);
|
|
5160
5458
|
const shown = names.slice(0, 3).join(", ");
|
|
5161
5459
|
const more = names.length > 3 ? `, +${names.length - 3} more` : "";
|
|
5162
5460
|
const suffix = members.every((m) => m.presence === "absent") ? "CLI not on PATH" : "no verified CLI name to check";
|
|
5163
5461
|
const who = names.length === 1 ? names[0] : `${names.length} agents: ${shown}${more}`;
|
|
5164
|
-
return `${who} (${
|
|
5462
|
+
return `${who} (${scope2}) \u2014 ${suffix}`;
|
|
5165
5463
|
}
|
|
5166
5464
|
function detectAgents(cwd, home, pathEnv) {
|
|
5167
5465
|
const found = [];
|
|
@@ -5427,8 +5725,8 @@ async function init(options = {}, deps = {}) {
|
|
|
5427
5725
|
"[skill-forge] No detected agent CLI was found on PATH \u2014 keeping every detected target selected."
|
|
5428
5726
|
);
|
|
5429
5727
|
}
|
|
5430
|
-
const installedPluginsPath = options.installedPluginsPath ??
|
|
5431
|
-
const claudePluginsRegistryExists =
|
|
5728
|
+
const installedPluginsPath = options.installedPluginsPath ?? join25(home, ".claude", "plugins", "installed_plugins.json");
|
|
5729
|
+
const claudePluginsRegistryExists = existsSync19(installedPluginsPath);
|
|
5432
5730
|
if (options.defaults) {
|
|
5433
5731
|
const selectedRoots = narrow ? rootGroups.filter(hasInstalledMember) : rootGroups;
|
|
5434
5732
|
const selectedMcpTargets = narrow ? mcpGroups.filter(hasInstalledMember) : mcpGroups;
|
|
@@ -5487,7 +5785,7 @@ async function init(options = {}, deps = {}) {
|
|
|
5487
5785
|
}
|
|
5488
5786
|
async function maybeOfferInit(options = {}) {
|
|
5489
5787
|
if (options.json || options.yes || !process.stdin.isTTY) return;
|
|
5490
|
-
if (
|
|
5788
|
+
if (existsSync19(getConfigPath())) return;
|
|
5491
5789
|
console.log(
|
|
5492
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)."
|
|
5493
5791
|
);
|
|
@@ -5518,7 +5816,7 @@ async function add(source, options = {}) {
|
|
|
5518
5816
|
if (!targetRoot) {
|
|
5519
5817
|
throw new Error("no target skills root configured; pass --target <dir> or set skillsRoots in the config");
|
|
5520
5818
|
}
|
|
5521
|
-
const resolved =
|
|
5819
|
+
const resolved = resolve2(source, artifact);
|
|
5522
5820
|
const entry = await quarantine(resolved, config.quarantineDir);
|
|
5523
5821
|
const gatedFingerprint = fingerprintDirs(entry.skillDirs).digest;
|
|
5524
5822
|
if (entry.skillDirs.length === 0) {
|
|
@@ -5602,7 +5900,7 @@ async function add(source, options = {}) {
|
|
|
5602
5900
|
}
|
|
5603
5901
|
}
|
|
5604
5902
|
async function addMcp(source, config, options) {
|
|
5605
|
-
const resolved =
|
|
5903
|
+
const resolved = resolve2(source, "mcp");
|
|
5606
5904
|
const entry = await quarantine(resolved, config.quarantineDir);
|
|
5607
5905
|
const candidatePath = resolveMcpCandidatePath(entry);
|
|
5608
5906
|
const { safety, gateResult, proEnabled } = await runGate(candidatePath, "", {
|
|
@@ -5638,7 +5936,7 @@ async function addMcp(source, config, options) {
|
|
|
5638
5936
|
envVars: detail.env ? Object.keys(detail.env) : void 0,
|
|
5639
5937
|
candidateEntry: detail.command ? { command: detail.command, args: detail.args, env: detail.env } : void 0
|
|
5640
5938
|
};
|
|
5641
|
-
const mcpTargetPath = options.mcpTarget ?? config.mcpTargets?.[0] ??
|
|
5939
|
+
const mcpTargetPath = options.mcpTarget ?? config.mcpTargets?.[0] ?? join26(process.cwd(), ".mcp.json");
|
|
5642
5940
|
const result = promoteMcp(entry, serverProfile, mcpTargetPath, { force: options.force });
|
|
5643
5941
|
reject(entry);
|
|
5644
5942
|
logOutcome(`[skill-forge] promoted -> ${result.target} (server "${result.serverName}")`, options.json);
|
|
@@ -5693,7 +5991,7 @@ async function scan(source, options = {}) {
|
|
|
5693
5991
|
if (artifact === "skill" && !skillsRoot) {
|
|
5694
5992
|
throw new Error("no skills root configured; set skillsRoots in the config");
|
|
5695
5993
|
}
|
|
5696
|
-
const resolved =
|
|
5994
|
+
const resolved = resolve2(source, artifact);
|
|
5697
5995
|
const entry = await quarantine(resolved, config.quarantineDir);
|
|
5698
5996
|
try {
|
|
5699
5997
|
let candidatePath;
|
|
@@ -5734,7 +6032,7 @@ async function scan(source, options = {}) {
|
|
|
5734
6032
|
}
|
|
5735
6033
|
|
|
5736
6034
|
// src/commands/list.ts
|
|
5737
|
-
import { join as
|
|
6035
|
+
import { join as join27 } from "path";
|
|
5738
6036
|
async function list() {
|
|
5739
6037
|
await maybeOfferInit();
|
|
5740
6038
|
const config = loadConfig();
|
|
@@ -5747,14 +6045,14 @@ async function list() {
|
|
|
5747
6045
|
}
|
|
5748
6046
|
console.log(`[skill-forge] ${ids.length} held in quarantine (${quarantineDir}):`);
|
|
5749
6047
|
for (const id of ids) {
|
|
5750
|
-
const skillDirs = findSkillDirs(
|
|
6048
|
+
const skillDirs = findSkillDirs(join27(quarantineDir, id));
|
|
5751
6049
|
if (skillDirs.length === 0) {
|
|
5752
6050
|
console.log(` ${id} (no SKILL.md found)`);
|
|
5753
6051
|
continue;
|
|
5754
6052
|
}
|
|
5755
6053
|
const names = skillDirs.map((dir) => {
|
|
5756
6054
|
try {
|
|
5757
|
-
return readFrontmatter(
|
|
6055
|
+
return readFrontmatter(join27(dir, "SKILL.md")).name;
|
|
5758
6056
|
} catch {
|
|
5759
6057
|
return dir;
|
|
5760
6058
|
}
|
|
@@ -5764,23 +6062,23 @@ async function list() {
|
|
|
5764
6062
|
}
|
|
5765
6063
|
|
|
5766
6064
|
// src/commands/guide.ts
|
|
5767
|
-
import { existsSync as
|
|
6065
|
+
import { existsSync as existsSync21 } from "fs";
|
|
5768
6066
|
|
|
5769
6067
|
// src/commands/held.ts
|
|
5770
|
-
import { existsSync as
|
|
5771
|
-
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";
|
|
5772
6070
|
var PRO_FEATURE_LABEL2 = "overlap analysis, the provenance ledger, and the pending-ingestion queue";
|
|
5773
6071
|
function resolveHeldPath(quarantineDir, id) {
|
|
5774
6072
|
if (id === "" || id !== basename9(id) || id === "." || id === "..") {
|
|
5775
6073
|
throw new UserError(`invalid quarantine id "${id}" \u2014 pass a single id exactly as printed by \`skill-forge list\``);
|
|
5776
6074
|
}
|
|
5777
|
-
const candidate =
|
|
5778
|
-
if (!
|
|
6075
|
+
const candidate = join28(quarantineDir, id);
|
|
6076
|
+
if (!existsSync20(candidate)) {
|
|
5779
6077
|
throw new UserError(`no held entry with id "${id}" \u2014 run \`skill-forge list\` to see what is in quarantine`);
|
|
5780
6078
|
}
|
|
5781
|
-
const realRoot =
|
|
5782
|
-
const realCandidate =
|
|
5783
|
-
if (!realCandidate.startsWith(realRoot +
|
|
6079
|
+
const realRoot = realpathSync6(resolvePath6(quarantineDir));
|
|
6080
|
+
const realCandidate = realpathSync6(candidate);
|
|
6081
|
+
if (!realCandidate.startsWith(realRoot + sep7)) {
|
|
5784
6082
|
throw new UserError(`refusing to act on "${id}": it resolves outside the quarantine dir (${realRoot})`);
|
|
5785
6083
|
}
|
|
5786
6084
|
return realCandidate;
|
|
@@ -5803,7 +6101,7 @@ function resolveTargetRoot(config, target) {
|
|
|
5803
6101
|
function heldSkillNames(skillDirs) {
|
|
5804
6102
|
return skillDirs.map((dir) => {
|
|
5805
6103
|
try {
|
|
5806
|
-
return readFrontmatter(
|
|
6104
|
+
return readFrontmatter(join28(dir, "SKILL.md")).name;
|
|
5807
6105
|
} catch {
|
|
5808
6106
|
return basename9(dir);
|
|
5809
6107
|
}
|
|
@@ -5811,7 +6109,7 @@ function heldSkillNames(skillDirs) {
|
|
|
5811
6109
|
}
|
|
5812
6110
|
function looksLikeMcpSandbox(quarantinePath) {
|
|
5813
6111
|
const candidate = resolveMcpCandidatePath({ quarantinePath });
|
|
5814
|
-
return [".mcp.json", "mcp.json", "package.json"].some((f) =>
|
|
6112
|
+
return [".mcp.json", "mcp.json", "package.json"].some((f) => existsSync20(join28(candidate, f)));
|
|
5815
6113
|
}
|
|
5816
6114
|
async function promoteHeldMcp(id, entry, config, options) {
|
|
5817
6115
|
const candidatePath = resolveMcpCandidatePath(entry);
|
|
@@ -5845,7 +6143,7 @@ async function promoteHeldMcp(id, entry, config, options) {
|
|
|
5845
6143
|
envVars: detail.env ? Object.keys(detail.env) : void 0,
|
|
5846
6144
|
candidateEntry: detail.command ? { command: detail.command, args: detail.args, env: detail.env } : void 0
|
|
5847
6145
|
};
|
|
5848
|
-
const mcpTargetPath = options.mcpTarget ?? config.mcpTargets?.[0] ??
|
|
6146
|
+
const mcpTargetPath = options.mcpTarget ?? config.mcpTargets?.[0] ?? join28(process.cwd(), ".mcp.json");
|
|
5849
6147
|
const result = promoteMcp(entry, serverProfile, mcpTargetPath, { force: options.force });
|
|
5850
6148
|
reject(entry);
|
|
5851
6149
|
logOutcome(`[skill-forge] promoted -> ${result.target} (server "${result.serverName}")`, options.json);
|
|
@@ -6090,7 +6388,7 @@ var TOPICS = [
|
|
|
6090
6388
|
];
|
|
6091
6389
|
function readState() {
|
|
6092
6390
|
const configPath = getConfigPath();
|
|
6093
|
-
const configured =
|
|
6391
|
+
const configured = existsSync21(configPath);
|
|
6094
6392
|
const base = {
|
|
6095
6393
|
configured,
|
|
6096
6394
|
configPath,
|
|
@@ -6324,17 +6622,17 @@ async function status() {
|
|
|
6324
6622
|
// src/evolve.ts
|
|
6325
6623
|
import { createInterface as createInterface5 } from "readline/promises";
|
|
6326
6624
|
import { createHash as createHash3 } from "crypto";
|
|
6327
|
-
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";
|
|
6328
6626
|
import { tmpdir as tmpdir2 } from "os";
|
|
6329
|
-
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";
|
|
6330
6628
|
var SKILLOPT_BIN = "skillopt-sleep";
|
|
6331
6629
|
var PRO_FEATURE_LABEL3 = "self-evolution via SkillOpt-Sleep (evolve) \u2014 re-gating, provenance, and the pending-ingestion queue";
|
|
6332
6630
|
async function evolve(targetSkillDir, options = {}, deps = {}) {
|
|
6333
6631
|
const exec = deps.exec ?? defaultExec;
|
|
6334
6632
|
const json = options.json;
|
|
6335
6633
|
const resolvedTargetDir = resolvePath7(targetSkillDir);
|
|
6336
|
-
const skillMdPath =
|
|
6337
|
-
if (!
|
|
6634
|
+
const skillMdPath = join29(resolvedTargetDir, "SKILL.md");
|
|
6635
|
+
if (!existsSync22(skillMdPath)) {
|
|
6338
6636
|
throw new UserError(`no SKILL.md found at ${skillMdPath} \u2014 evolve requires an existing, installed skill directory`);
|
|
6339
6637
|
}
|
|
6340
6638
|
const proStatus = proFeatureStatus(PRO_FEATURE_LABEL3);
|
|
@@ -6392,33 +6690,33 @@ async function evolve(targetSkillDir, options = {}, deps = {}) {
|
|
|
6392
6690
|
}
|
|
6393
6691
|
return;
|
|
6394
6692
|
}
|
|
6395
|
-
const stagingRoot =
|
|
6693
|
+
const stagingRoot = join29(project, ".skillopt-sleep", "staging");
|
|
6396
6694
|
const stagingDir = resolvePath7(parsed.staging_dir);
|
|
6397
|
-
if (stagingDir !== stagingRoot && !stagingDir.startsWith(stagingRoot +
|
|
6695
|
+
if (stagingDir !== stagingRoot && !stagingDir.startsWith(stagingRoot + sep8)) {
|
|
6398
6696
|
throw new UserError(
|
|
6399
6697
|
`skillopt-sleep reported a staging_dir outside the expected sandbox (${stagingRoot}): ${stagingDir} \u2014 refusing to use it`
|
|
6400
6698
|
);
|
|
6401
6699
|
}
|
|
6402
|
-
const proposedSkillMd =
|
|
6403
|
-
const proposedClaudeMd =
|
|
6404
|
-
const reportMdPath =
|
|
6405
|
-
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)) {
|
|
6406
6704
|
throw new UserError(`skillopt-sleep reported an accepted proposal but ${proposedSkillMd} is missing`);
|
|
6407
6705
|
}
|
|
6408
|
-
if (
|
|
6706
|
+
if (existsSync22(reportMdPath)) {
|
|
6409
6707
|
notices.push(`SkillOpt-Sleep's full report: ${reportMdPath}`);
|
|
6410
6708
|
}
|
|
6411
6709
|
assertNotSymlink(proposedSkillMd, "proposed_SKILL.md");
|
|
6412
6710
|
assertNotSymlink(proposedClaudeMd, "proposed_CLAUDE.md");
|
|
6413
6711
|
const scannedSkillHash = sha256File(proposedSkillMd);
|
|
6414
6712
|
const scannedClaudeHash = sha256File(proposedClaudeMd);
|
|
6415
|
-
const tempDir = mkdtempSync3(
|
|
6713
|
+
const tempDir = mkdtempSync3(join29(tmpdir2(), "skill-forge-evolve-"));
|
|
6416
6714
|
let profile;
|
|
6417
6715
|
let safety;
|
|
6418
6716
|
try {
|
|
6419
|
-
cpSync3(proposedSkillMd,
|
|
6420
|
-
if (
|
|
6421
|
-
cpSync3(proposedClaudeMd,
|
|
6717
|
+
cpSync3(proposedSkillMd, join29(tempDir, "SKILL.md"));
|
|
6718
|
+
if (existsSync22(proposedClaudeMd)) {
|
|
6719
|
+
cpSync3(proposedClaudeMd, join29(tempDir, "CLAUDE.md"));
|
|
6422
6720
|
}
|
|
6423
6721
|
profile = asSkillProfile(profileSkill(tempDir), "for the evolved proposal");
|
|
6424
6722
|
const strictness = loadConfig().strictness;
|
|
@@ -6517,7 +6815,7 @@ async function evolve(targetSkillDir, options = {}, deps = {}) {
|
|
|
6517
6815
|
if (safetyVerdict === "block") process.exitCode = 1;
|
|
6518
6816
|
} else {
|
|
6519
6817
|
logOutcome(`[skill-forge] held \u2014 staging dir left in place -> ${stagingDir}`, json);
|
|
6520
|
-
if (
|
|
6818
|
+
if (existsSync22(reportMdPath)) {
|
|
6521
6819
|
logOutcome(`[skill-forge] see ${reportMdPath} for the full evidence report`, json);
|
|
6522
6820
|
}
|
|
6523
6821
|
}
|
|
@@ -6559,10 +6857,10 @@ function validateRunOutput(raw) {
|
|
|
6559
6857
|
return { accepted: Boolean(obj.accepted), staging_dir: stagingDirRaw ?? null };
|
|
6560
6858
|
}
|
|
6561
6859
|
function readManifest(stagingDir) {
|
|
6562
|
-
const manifestPath =
|
|
6860
|
+
const manifestPath = join29(stagingDir, "manifest.json");
|
|
6563
6861
|
let raw;
|
|
6564
6862
|
try {
|
|
6565
|
-
raw =
|
|
6863
|
+
raw = readFileSync21(manifestPath, "utf-8");
|
|
6566
6864
|
} catch (err) {
|
|
6567
6865
|
throw new UserError(`could not read staging manifest ${manifestPath}: ${err.message}`);
|
|
6568
6866
|
}
|
|
@@ -6596,18 +6894,18 @@ function validateManifest(raw, manifestPath) {
|
|
|
6596
6894
|
};
|
|
6597
6895
|
}
|
|
6598
6896
|
function filesIdentical(a, b) {
|
|
6599
|
-
if (!
|
|
6600
|
-
return
|
|
6897
|
+
if (!existsSync22(a) || !existsSync22(b)) return false;
|
|
6898
|
+
return readFileSync21(a).equals(readFileSync21(b));
|
|
6601
6899
|
}
|
|
6602
6900
|
function isAlreadyAdopted(stagingDir, manifest) {
|
|
6603
|
-
const proposedSkillMd =
|
|
6604
|
-
const proposedClaudeMd =
|
|
6901
|
+
const proposedSkillMd = join29(stagingDir, "proposed_SKILL.md");
|
|
6902
|
+
const proposedClaudeMd = join29(stagingDir, "proposed_CLAUDE.md");
|
|
6605
6903
|
let sawComparison = false;
|
|
6606
|
-
if (
|
|
6904
|
+
if (existsSync22(proposedSkillMd)) {
|
|
6607
6905
|
sawComparison = true;
|
|
6608
6906
|
if (!filesIdentical(proposedSkillMd, manifest.live_skill_path)) return false;
|
|
6609
6907
|
}
|
|
6610
|
-
if (
|
|
6908
|
+
if (existsSync22(proposedClaudeMd) && manifest.live_memory_path) {
|
|
6611
6909
|
sawComparison = true;
|
|
6612
6910
|
if (!filesIdentical(proposedClaudeMd, manifest.live_memory_path)) return false;
|
|
6613
6911
|
}
|
|
@@ -6625,8 +6923,8 @@ function assertNotSymlink(path, label2) {
|
|
|
6625
6923
|
}
|
|
6626
6924
|
}
|
|
6627
6925
|
function sha256File(path) {
|
|
6628
|
-
if (!
|
|
6629
|
-
return createHash3("sha256").update(
|
|
6926
|
+
if (!existsSync22(path)) return null;
|
|
6927
|
+
return createHash3("sha256").update(readFileSync21(path)).digest("hex");
|
|
6630
6928
|
}
|
|
6631
6929
|
|
|
6632
6930
|
// src/commands/evolve.ts
|
|
@@ -6635,9 +6933,9 @@ async function evolveCommand(targetSkillDir, options = {}) {
|
|
|
6635
6933
|
}
|
|
6636
6934
|
|
|
6637
6935
|
// src/commands/finding.ts
|
|
6638
|
-
import { existsSync as
|
|
6936
|
+
import { existsSync as existsSync23 } from "fs";
|
|
6639
6937
|
function requireSavedConfig() {
|
|
6640
|
-
if (!
|
|
6938
|
+
if (!existsSync23(getConfigPath())) {
|
|
6641
6939
|
throw new UserError(
|
|
6642
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)."
|
|
6643
6941
|
);
|
|
@@ -6723,7 +7021,7 @@ async function findingRevokeCommand(prefix, options = {}) {
|
|
|
6723
7021
|
async function findingListCommand(options = {}) {
|
|
6724
7022
|
const { records, notices } = readAcceptedFindings();
|
|
6725
7023
|
for (const notice of notices) console.error(`[skill-forge] ${notice}`);
|
|
6726
|
-
const rows = options.stale ? records.filter((r) => !
|
|
7024
|
+
const rows = options.stale ? records.filter((r) => !existsSync23(r.target)) : records;
|
|
6727
7025
|
if (options.json) {
|
|
6728
7026
|
console.log(JSON.stringify(rows, null, 2));
|
|
6729
7027
|
return;
|
|
@@ -6744,8 +7042,8 @@ async function findingListCommand(options = {}) {
|
|
|
6744
7042
|
import { resolve as resolvePath9 } from "path";
|
|
6745
7043
|
|
|
6746
7044
|
// src/organize.ts
|
|
6747
|
-
import { existsSync as
|
|
6748
|
-
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";
|
|
6749
7047
|
var PRO_FEATURE_LABEL4 = "set-level organizer reports";
|
|
6750
7048
|
var TIERS = /* @__PURE__ */ new Set(["resource", "custom"]);
|
|
6751
7049
|
var MATURITIES = /* @__PURE__ */ new Set(["seedling", "stable", "deprecated", "experimental"]);
|
|
@@ -6758,14 +7056,14 @@ function listSubdirs2(dir) {
|
|
|
6758
7056
|
}
|
|
6759
7057
|
const out = [];
|
|
6760
7058
|
for (const entry of entries) {
|
|
6761
|
-
const full =
|
|
7059
|
+
const full = join30(dir, entry.name);
|
|
6762
7060
|
if (entry.isDirectory()) {
|
|
6763
7061
|
out.push(full);
|
|
6764
7062
|
continue;
|
|
6765
7063
|
}
|
|
6766
7064
|
if (entry.isSymbolicLink()) {
|
|
6767
7065
|
try {
|
|
6768
|
-
if (
|
|
7066
|
+
if (statSync10(full).isDirectory()) out.push(full);
|
|
6769
7067
|
} catch {
|
|
6770
7068
|
}
|
|
6771
7069
|
}
|
|
@@ -6776,14 +7074,14 @@ function findSkillDirsUnderRoot2(root) {
|
|
|
6776
7074
|
const found = /* @__PURE__ */ new Set();
|
|
6777
7075
|
const topDirs = listSubdirs2(root);
|
|
6778
7076
|
for (const dir of topDirs) {
|
|
6779
|
-
if (
|
|
7077
|
+
if (existsSync24(join30(dir, "SKILL.md"))) found.add(dir);
|
|
6780
7078
|
}
|
|
6781
|
-
for (const dir of listSubdirs2(
|
|
6782
|
-
if (
|
|
7079
|
+
for (const dir of listSubdirs2(join30(root, "skills"))) {
|
|
7080
|
+
if (existsSync24(join30(dir, "SKILL.md"))) found.add(dir);
|
|
6783
7081
|
}
|
|
6784
7082
|
for (const topDir of topDirs) {
|
|
6785
|
-
for (const dir of listSubdirs2(
|
|
6786
|
-
if (
|
|
7083
|
+
for (const dir of listSubdirs2(join30(topDir, "skills"))) {
|
|
7084
|
+
if (existsSync24(join30(dir, "SKILL.md"))) found.add(dir);
|
|
6787
7085
|
}
|
|
6788
7086
|
}
|
|
6789
7087
|
return Array.from(found).filter((p) => !p.includes("/.git/"));
|
|
@@ -6979,17 +7277,17 @@ function buildRegistryEntries(candidates, warnings) {
|
|
|
6979
7277
|
for (const candidate of candidates) {
|
|
6980
7278
|
let realPath;
|
|
6981
7279
|
try {
|
|
6982
|
-
realPath =
|
|
7280
|
+
realPath = realpathSync7(candidate.dirPath);
|
|
6983
7281
|
} catch (err) {
|
|
6984
7282
|
warnings.push(`could not resolve skill directory ${candidate.dirPath}: ${err.message}`);
|
|
6985
7283
|
continue;
|
|
6986
7284
|
}
|
|
6987
7285
|
if (seenRealPaths.has(realPath)) continue;
|
|
6988
7286
|
seenRealPaths.add(realPath);
|
|
6989
|
-
const skillMdPath =
|
|
7287
|
+
const skillMdPath = join30(realPath, "SKILL.md");
|
|
6990
7288
|
let text;
|
|
6991
7289
|
try {
|
|
6992
|
-
text =
|
|
7290
|
+
text = readFileSync22(skillMdPath, "utf-8");
|
|
6993
7291
|
} catch (err) {
|
|
6994
7292
|
warnings.push(`could not read ${skillMdPath}: ${err.message}`);
|
|
6995
7293
|
continue;
|
|
@@ -7395,13 +7693,13 @@ async function find(query, options = {}, deps = {}) {
|
|
|
7395
7693
|
}
|
|
7396
7694
|
|
|
7397
7695
|
// src/commands/watch.ts
|
|
7398
|
-
import { realpathSync as
|
|
7399
|
-
import { join as
|
|
7696
|
+
import { realpathSync as realpathSync8 } from "fs";
|
|
7697
|
+
import { join as join31, resolve as resolvePath10 } from "path";
|
|
7400
7698
|
|
|
7401
7699
|
// src/gate/skillMapDrift.ts
|
|
7402
7700
|
import { createHash as createHash4 } from "crypto";
|
|
7403
|
-
import { readFileSync as
|
|
7404
|
-
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";
|
|
7405
7703
|
function sha256(text) {
|
|
7406
7704
|
return createHash4("sha256").update(text, "utf-8").digest("hex");
|
|
7407
7705
|
}
|
|
@@ -7434,22 +7732,22 @@ function threeWayVerdict(localNormalized, upstreamHash, baselineHash) {
|
|
|
7434
7732
|
}
|
|
7435
7733
|
function resolveLocalPath(path, mapDir) {
|
|
7436
7734
|
const expanded = expandHome(path);
|
|
7437
|
-
if (
|
|
7438
|
-
const candidates = [
|
|
7735
|
+
if (isAbsolute6(expanded)) return resolve8(expanded);
|
|
7736
|
+
const candidates = [resolve8(expanded)];
|
|
7439
7737
|
if (mapDir) {
|
|
7440
|
-
candidates.push(
|
|
7441
|
-
candidates.push(
|
|
7738
|
+
candidates.push(resolve8(mapDir, expanded));
|
|
7739
|
+
candidates.push(resolve8(mapDir, "..", expanded));
|
|
7442
7740
|
}
|
|
7443
7741
|
for (const candidate of candidates) {
|
|
7444
|
-
if (
|
|
7742
|
+
if (existsSync25(candidate)) return candidate;
|
|
7445
7743
|
}
|
|
7446
7744
|
return candidates[0];
|
|
7447
7745
|
}
|
|
7448
7746
|
function defaultReadLocalFile(path, mapDir) {
|
|
7449
7747
|
const resolved = resolveLocalPath(path, mapDir);
|
|
7450
|
-
if (!
|
|
7748
|
+
if (!existsSync25(resolved)) return void 0;
|
|
7451
7749
|
try {
|
|
7452
|
-
return
|
|
7750
|
+
return readFileSync23(resolved, "utf-8");
|
|
7453
7751
|
} catch {
|
|
7454
7752
|
return void 0;
|
|
7455
7753
|
}
|
|
@@ -7470,7 +7768,7 @@ function upstreamLabel(node, edge) {
|
|
|
7470
7768
|
return node?.url ?? node?.path ?? node?.name ?? node?.id ?? edge.to;
|
|
7471
7769
|
}
|
|
7472
7770
|
async function computeMapDrift(map, deps = {}) {
|
|
7473
|
-
const mapDir = deps.mapPath ? dirname10(
|
|
7771
|
+
const mapDir = deps.mapPath ? dirname10(resolve8(expandHome(deps.mapPath))) : void 0;
|
|
7474
7772
|
const readLocalFile = deps.readLocalFile ?? ((path) => defaultReadLocalFile(path, mapDir));
|
|
7475
7773
|
const fetchUpstream = deps.fetchUpstream ?? defaultFetchUpstream;
|
|
7476
7774
|
const rows = [];
|
|
@@ -7490,7 +7788,7 @@ async function computeMapDrift(map, deps = {}) {
|
|
|
7490
7788
|
continue;
|
|
7491
7789
|
}
|
|
7492
7790
|
const localResolved = resolveLocalPath(localNode.path, mapDir);
|
|
7493
|
-
if (!
|
|
7791
|
+
if (!existsSync25(localResolved)) {
|
|
7494
7792
|
rows.push({
|
|
7495
7793
|
skill: skillLabel,
|
|
7496
7794
|
upstream,
|
|
@@ -7601,7 +7899,7 @@ async function runWatch(config, options = {}, deps = {}) {
|
|
|
7601
7899
|
let realPath = resolvedRoot;
|
|
7602
7900
|
if (exists) {
|
|
7603
7901
|
try {
|
|
7604
|
-
realPath =
|
|
7902
|
+
realPath = realpathSync8(resolvedRoot);
|
|
7605
7903
|
} catch {
|
|
7606
7904
|
realPath = resolvedRoot;
|
|
7607
7905
|
}
|
|
@@ -7613,7 +7911,7 @@ async function runWatch(config, options = {}, deps = {}) {
|
|
|
7613
7911
|
ledger = readProvenanceLedger(resolvedRoot);
|
|
7614
7912
|
} catch (err) {
|
|
7615
7913
|
const message = err instanceof Error ? err.message : String(err);
|
|
7616
|
-
const ledgerPath =
|
|
7914
|
+
const ledgerPath = join31(resolvedRoot, "SOURCES.md");
|
|
7617
7915
|
roots.push({ root, realPath, exists, ledgerPath, ledgerExists: false });
|
|
7618
7916
|
warnings.push(`${ledgerPath}: could not read ledger: ${message}`);
|
|
7619
7917
|
continue;
|
|
@@ -7723,8 +8021,8 @@ async function watchCommand(options = {}) {
|
|
|
7723
8021
|
}
|
|
7724
8022
|
|
|
7725
8023
|
// src/commands/routine.ts
|
|
7726
|
-
import { existsSync as
|
|
7727
|
-
import { join as
|
|
8024
|
+
import { existsSync as existsSync26, rmSync as rmSync7, statSync as statSync11 } from "fs";
|
|
8025
|
+
import { join as join32 } from "path";
|
|
7728
8026
|
var PRO_FEATURE_LABEL6 = "the scheduled maintenance routine";
|
|
7729
8027
|
var SEVERITY_ORDER = ["LOW", "MEDIUM", "HIGH", "CRITICAL"];
|
|
7730
8028
|
function parseFailOnFloor(failOn) {
|
|
@@ -7732,20 +8030,20 @@ function parseFailOnFloor(failOn) {
|
|
|
7732
8030
|
if (failOn === "any") return "LOW";
|
|
7733
8031
|
return failOn.toUpperCase();
|
|
7734
8032
|
}
|
|
7735
|
-
function exceedsFloor(
|
|
8033
|
+
function exceedsFloor(findings, failOn) {
|
|
7736
8034
|
const floor = parseFailOnFloor(failOn);
|
|
7737
8035
|
if (!floor) return void 0;
|
|
7738
8036
|
const floorIndex = SEVERITY_ORDER.indexOf(floor);
|
|
7739
8037
|
for (let i = SEVERITY_ORDER.length - 1; i >= floorIndex; i--) {
|
|
7740
8038
|
const severity = SEVERITY_ORDER[i];
|
|
7741
|
-
const count =
|
|
8039
|
+
const count = findings[severity] ?? 0;
|
|
7742
8040
|
if (count > 0) return `${count} ${severity} finding(s) at or above --fail-on ${failOn}`;
|
|
7743
8041
|
}
|
|
7744
8042
|
return void 0;
|
|
7745
8043
|
}
|
|
7746
8044
|
function resolveReportPath2(override, generatedAt) {
|
|
7747
8045
|
if (override) return override;
|
|
7748
|
-
return
|
|
8046
|
+
return join32(getHomeDir(), "reports", `routine-${generatedAt.replace(/[:.]/g, "-")}.md`);
|
|
7749
8047
|
}
|
|
7750
8048
|
function runHousekeeping(config, options) {
|
|
7751
8049
|
const actions = [];
|
|
@@ -7754,10 +8052,10 @@ function runHousekeeping(config, options) {
|
|
|
7754
8052
|
const pruneDays = options.pruneDays ?? 30;
|
|
7755
8053
|
const cutoff = Date.now() - pruneDays * 864e5;
|
|
7756
8054
|
for (const id of listQuarantineIds(config.quarantineDir)) {
|
|
7757
|
-
const path =
|
|
8055
|
+
const path = join32(config.quarantineDir, id);
|
|
7758
8056
|
let mtime;
|
|
7759
8057
|
try {
|
|
7760
|
-
mtime =
|
|
8058
|
+
mtime = statSync11(path).mtimeMs;
|
|
7761
8059
|
} catch {
|
|
7762
8060
|
continue;
|
|
7763
8061
|
}
|
|
@@ -7792,7 +8090,7 @@ function runHousekeeping(config, options) {
|
|
|
7792
8090
|
}
|
|
7793
8091
|
for (const entry of entries) {
|
|
7794
8092
|
if (entry.status !== "pending") continue;
|
|
7795
|
-
if (!entry.installedPath ||
|
|
8093
|
+
if (!entry.installedPath || existsSync26(entry.installedPath)) continue;
|
|
7796
8094
|
if (apply) {
|
|
7797
8095
|
try {
|
|
7798
8096
|
updateQueueEntry(entry.id, { status: "dismissed" });
|
|
@@ -7825,6 +8123,7 @@ function renderRoutineMarkdown(result, audit2, watch, organize) {
|
|
|
7825
8123
|
"",
|
|
7826
8124
|
`- Skills: ${result.audit.skillCount} across ${audit2.summary.rootCount} configured root(s)`,
|
|
7827
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)`,
|
|
7828
8127
|
`- Accepted: ${audit2.summary.acceptedCount} (suppressed from the counts above; see Accepted findings)`,
|
|
7829
8128
|
`- Overlap clusters: ${result.audit.overlapClusters.length}${result.audit.overlapLocked ? ' (overlap analysis was locked \u2014 not "none found")' : ""}`,
|
|
7830
8129
|
`- Pending queue entries: ${result.pending.queue}`,
|
|
@@ -7894,14 +8193,31 @@ async function routineCommand(options = {}, deps = {}) {
|
|
|
7894
8193
|
if (options.dryRun && !options.housekeeping) {
|
|
7895
8194
|
throw new UserError("--dry-run only applies to --housekeeping; without it `routine` already writes nothing but its own report");
|
|
7896
8195
|
}
|
|
8196
|
+
const failScope = options.failScope ?? "configured";
|
|
8197
|
+
if (failScope !== "configured" && failScope !== "all") throw new UserError("--fail-scope must be configured or all");
|
|
7897
8198
|
const config = loadConfig();
|
|
7898
8199
|
const generatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
7899
|
-
const { pluginRoots, usage, notices: pluginUsageNotices } = resolvePluginAndUsageContext(config, options, deps);
|
|
8200
|
+
const { pluginRoots, usage, importedInventory, notices: pluginUsageNotices } = resolvePluginAndUsageContext(config, options, deps);
|
|
7900
8201
|
const { records: accepted, notices: storeNotices } = readAcceptedFindings();
|
|
7901
8202
|
const notices = [...storeNotices, ...pluginUsageNotices];
|
|
7902
8203
|
for (const notice of notices) log(`[skill-forge] ${notice}`);
|
|
7903
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);
|
|
7904
8211
|
const auditState = summarizeAudit(audit2);
|
|
8212
|
+
if (audit2.plugins && failScope === "configured") {
|
|
8213
|
+
const critical = audit2.plugins.reduce((n, p) => n + p.findingCounts.CRITICAL, 0);
|
|
8214
|
+
const high = audit2.plugins.reduce((n, p) => n + p.findingCounts.HIGH, 0);
|
|
8215
|
+
if (critical + high > 0) {
|
|
8216
|
+
const notice = `plugin-origin findings (${critical} CRITICAL, ${high} HIGH) are excluded from --fail-on and the add/scan advisory \u2014 see the Plugins section for per-plugin review recommendations`;
|
|
8217
|
+
notices.push(notice);
|
|
8218
|
+
log(`[skill-forge] ${notice}`);
|
|
8219
|
+
}
|
|
8220
|
+
}
|
|
7905
8221
|
const degraded = [];
|
|
7906
8222
|
let watch;
|
|
7907
8223
|
if (!options.skipWatch) {
|
|
@@ -7929,7 +8245,11 @@ async function routineCommand(options = {}, deps = {}) {
|
|
|
7929
8245
|
}
|
|
7930
8246
|
const quarantine2 = heldCount(config.quarantineDir);
|
|
7931
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;
|
|
7932
8249
|
const result = {
|
|
8250
|
+
schemaVersion: 1,
|
|
8251
|
+
failScope,
|
|
8252
|
+
failureCounts,
|
|
7933
8253
|
generatedAt,
|
|
7934
8254
|
reportPath: resolveReportPath2(options.report, generatedAt),
|
|
7935
8255
|
audit: auditState,
|
|
@@ -7947,7 +8267,7 @@ async function routineCommand(options = {}, deps = {}) {
|
|
|
7947
8267
|
notices,
|
|
7948
8268
|
degraded
|
|
7949
8269
|
};
|
|
7950
|
-
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");
|
|
7951
8271
|
if (failure) result.failure = failure;
|
|
7952
8272
|
writeNewFile(result.reportPath, renderRoutineMarkdown(result, audit2, watch, organize), "--report");
|
|
7953
8273
|
try {
|
|
@@ -7959,6 +8279,7 @@ async function routineCommand(options = {}, deps = {}) {
|
|
|
7959
8279
|
console.log(JSON.stringify(result, null, 2));
|
|
7960
8280
|
} else {
|
|
7961
8281
|
console.log("[skill-forge] routine");
|
|
8282
|
+
console.log(` failure scope: ${failScope}`);
|
|
7962
8283
|
console.log(` skills: ${auditState.skillCount}`);
|
|
7963
8284
|
console.log(
|
|
7964
8285
|
` findings: ${auditState.findings.CRITICAL ?? 0} critical / ${auditState.findings.HIGH ?? 0} high / ${auditState.findings.MEDIUM ?? 0} medium / ${auditState.findings.LOW ?? 0} low`
|
|
@@ -7988,8 +8309,8 @@ async function routineCommand(options = {}, deps = {}) {
|
|
|
7988
8309
|
}
|
|
7989
8310
|
|
|
7990
8311
|
// src/commands/ingest.ts
|
|
7991
|
-
import { realpathSync as
|
|
7992
|
-
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";
|
|
7993
8314
|
var PRO_FEATURE_LABEL7 = "the pending-ingestion queue";
|
|
7994
8315
|
var VALID_STATUSES = ["ingested", "dismissed"];
|
|
7995
8316
|
function requireProFeature() {
|
|
@@ -8030,14 +8351,14 @@ function printPendingList(pending, json) {
|
|
|
8030
8351
|
}
|
|
8031
8352
|
function realpathOrNull(p) {
|
|
8032
8353
|
try {
|
|
8033
|
-
return
|
|
8354
|
+
return realpathSync9(p);
|
|
8034
8355
|
} catch {
|
|
8035
8356
|
return null;
|
|
8036
8357
|
}
|
|
8037
8358
|
}
|
|
8038
8359
|
function isUnderRoot(candidate, root) {
|
|
8039
|
-
const rel =
|
|
8040
|
-
return rel === "" || !rel.startsWith("..") && !
|
|
8360
|
+
const rel = relative6(root, candidate);
|
|
8361
|
+
return rel === "" || !rel.startsWith("..") && !isAbsolute7(rel);
|
|
8041
8362
|
}
|
|
8042
8363
|
function validatePendingEntries(entries, config) {
|
|
8043
8364
|
const quarantineRoot = realpathOrNull(resolvePath11(config.quarantineDir));
|
|
@@ -8119,15 +8440,15 @@ import {
|
|
|
8119
8440
|
chmodSync as chmodSync3,
|
|
8120
8441
|
lstatSync as lstatSync6,
|
|
8121
8442
|
mkdirSync as mkdirSync9,
|
|
8122
|
-
readFileSync as
|
|
8443
|
+
readFileSync as readFileSync27,
|
|
8123
8444
|
readSync as readSync3,
|
|
8124
8445
|
writeFileSync as writeFileSync8
|
|
8125
8446
|
} from "fs";
|
|
8126
|
-
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";
|
|
8127
8448
|
|
|
8128
8449
|
// src/insights/inventory.ts
|
|
8129
|
-
import { readFileSync as
|
|
8130
|
-
import { join as
|
|
8450
|
+
import { readFileSync as readFileSync24 } from "fs";
|
|
8451
|
+
import { join as join33 } from "path";
|
|
8131
8452
|
|
|
8132
8453
|
// src/insights/models.ts
|
|
8133
8454
|
import { createHash as createHash5 } from "crypto";
|
|
@@ -8241,7 +8562,7 @@ function declaredMapCoverage(path, claim) {
|
|
|
8241
8562
|
}
|
|
8242
8563
|
function fingerprintInstalledSkill(path) {
|
|
8243
8564
|
try {
|
|
8244
|
-
return { fingerprint: stableHash(
|
|
8565
|
+
return { fingerprint: stableHash(readFileSync24(join33(path, "SKILL.md"), "utf-8")) };
|
|
8245
8566
|
} catch (error) {
|
|
8246
8567
|
return { reason: `could not fingerprint SKILL.md: ${error.message}` };
|
|
8247
8568
|
}
|
|
@@ -8391,12 +8712,12 @@ function buildInsightInventory(config, options = {}) {
|
|
|
8391
8712
|
}
|
|
8392
8713
|
|
|
8393
8714
|
// src/insights/jira.ts
|
|
8394
|
-
import { existsSync as
|
|
8395
|
-
import { join as
|
|
8715
|
+
import { existsSync as existsSync28, readFileSync as readFileSync26 } from "fs";
|
|
8716
|
+
import { join as join36 } from "path";
|
|
8396
8717
|
|
|
8397
8718
|
// src/insights/store.ts
|
|
8398
|
-
import { chmodSync as chmodSync2, existsSync as
|
|
8399
|
-
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";
|
|
8400
8721
|
|
|
8401
8722
|
// src/insights/sources.ts
|
|
8402
8723
|
import { createHash as createHash6 } from "crypto";
|
|
@@ -8408,10 +8729,10 @@ import {
|
|
|
8408
8729
|
openSync as openSync3,
|
|
8409
8730
|
readSync as readSync2,
|
|
8410
8731
|
readdirSync as readdirSync11,
|
|
8411
|
-
realpathSync as
|
|
8412
|
-
statSync as
|
|
8732
|
+
realpathSync as realpathSync10,
|
|
8733
|
+
statSync as statSync12
|
|
8413
8734
|
} from "fs";
|
|
8414
|
-
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";
|
|
8415
8736
|
var DEFAULT_MAX_SOURCE_FILE_BYTES = 5 * 1024 * 1024;
|
|
8416
8737
|
var DEFAULT_MAX_SOURCE_TOTAL_BYTES = 25 * 1024 * 1024;
|
|
8417
8738
|
var SKIP_DIRS4 = /* @__PURE__ */ new Set([".git", "node_modules", "dist", "build", "coverage", ".next", "out", "__pycache__", ".venv", "venv"]);
|
|
@@ -8482,8 +8803,8 @@ function canonicalPublicUrl(raw, label2) {
|
|
|
8482
8803
|
return url.toString();
|
|
8483
8804
|
}
|
|
8484
8805
|
function isInside3(child, root) {
|
|
8485
|
-
const rel =
|
|
8486
|
-
return rel === "" || !rel.startsWith(`..${
|
|
8806
|
+
const rel = relative7(root, child);
|
|
8807
|
+
return rel === "" || !rel.startsWith(`..${sep9}`) && rel !== ".." && !isAbsolute8(rel);
|
|
8487
8808
|
}
|
|
8488
8809
|
function isHiddenName(name) {
|
|
8489
8810
|
return name.startsWith(".") && name !== "." && name !== "..";
|
|
@@ -8547,8 +8868,8 @@ function observeFile(path, relativePath, declaredMediaType, maxFileBytes, remain
|
|
|
8547
8868
|
if (!bytes || !descriptorStat) throw new UserError(`source file could not be observed: ${relativePath}`);
|
|
8548
8869
|
let realPath;
|
|
8549
8870
|
try {
|
|
8550
|
-
realPath =
|
|
8551
|
-
const pathStat =
|
|
8871
|
+
realPath = realpathSync10(path);
|
|
8872
|
+
const pathStat = statSync12(realPath);
|
|
8552
8873
|
if (pathStat.dev !== descriptorStat.dev || pathStat.ino !== descriptorStat.ino) {
|
|
8553
8874
|
throw new Error("path identity changed");
|
|
8554
8875
|
}
|
|
@@ -8669,7 +8990,7 @@ function normalizeStdin(input, retrievedAt, maxFileBytes, remainingBytes) {
|
|
|
8669
8990
|
};
|
|
8670
8991
|
}
|
|
8671
8992
|
function normalizeFile(input, retrievedAt, maxFileBytes, remainingBytes) {
|
|
8672
|
-
const resolved =
|
|
8993
|
+
const resolved = resolve9(input.path);
|
|
8673
8994
|
let lexicalStat;
|
|
8674
8995
|
try {
|
|
8675
8996
|
lexicalStat = lstatSync5(resolved);
|
|
@@ -8720,7 +9041,7 @@ function normalizeFile(input, retrievedAt, maxFileBytes, remainingBytes) {
|
|
|
8720
9041
|
};
|
|
8721
9042
|
}
|
|
8722
9043
|
function normalizeDirectory(input, retrievedAt, maxFileBytes, remainingBytes) {
|
|
8723
|
-
const resolved =
|
|
9044
|
+
const resolved = resolve9(input.path);
|
|
8724
9045
|
let lexicalStat;
|
|
8725
9046
|
try {
|
|
8726
9047
|
lexicalStat = lstatSync5(resolved);
|
|
@@ -8729,7 +9050,7 @@ function normalizeDirectory(input, retrievedAt, maxFileBytes, remainingBytes) {
|
|
|
8729
9050
|
}
|
|
8730
9051
|
if (lexicalStat.isSymbolicLink()) throw new UserError(`source directory must not be a symlink: ${resolved}`);
|
|
8731
9052
|
if (!lexicalStat.isDirectory()) throw new UserError(`source directory is not a directory: ${resolved}`);
|
|
8732
|
-
const realRoot =
|
|
9053
|
+
const realRoot = realpathSync10(resolved);
|
|
8733
9054
|
const observations = [];
|
|
8734
9055
|
const skipped = [];
|
|
8735
9056
|
const seenFiles = /* @__PURE__ */ new Set();
|
|
@@ -8743,7 +9064,7 @@ function normalizeDirectory(input, retrievedAt, maxFileBytes, remainingBytes) {
|
|
|
8743
9064
|
throw new UserError(`unable to enumerate source directory ${dir}: ${err.message}`);
|
|
8744
9065
|
}
|
|
8745
9066
|
for (const entry of entries) {
|
|
8746
|
-
const full =
|
|
9067
|
+
const full = join34(dir, entry.name);
|
|
8747
9068
|
const logicalPath = logicalDir ? `${logicalDir}/${entry.name}` : entry.name;
|
|
8748
9069
|
if (entry.isDirectory() && SKIP_DIRS4.has(entry.name)) {
|
|
8749
9070
|
skipped.push({ path: logicalPath, reason: "ignored-directory" });
|
|
@@ -8760,14 +9081,14 @@ function normalizeDirectory(input, retrievedAt, maxFileBytes, remainingBytes) {
|
|
|
8760
9081
|
if (entry.isSymbolicLink()) {
|
|
8761
9082
|
let realTarget;
|
|
8762
9083
|
try {
|
|
8763
|
-
realTarget =
|
|
9084
|
+
realTarget = realpathSync10(full);
|
|
8764
9085
|
} catch {
|
|
8765
9086
|
throw new UserError(`source directory contains an unreadable symlink: ${logicalPath}`);
|
|
8766
9087
|
}
|
|
8767
9088
|
if (!isInside3(realTarget, realRoot)) {
|
|
8768
9089
|
throw new UserError(`source directory symlink escapes its root: ${logicalPath}`);
|
|
8769
9090
|
}
|
|
8770
|
-
const targetStat =
|
|
9091
|
+
const targetStat = statSync12(realTarget);
|
|
8771
9092
|
if (targetStat.isDirectory()) {
|
|
8772
9093
|
if (seenDirs.has(realTarget)) continue;
|
|
8773
9094
|
seenDirs.add(realTarget);
|
|
@@ -8779,7 +9100,7 @@ function normalizeDirectory(input, retrievedAt, maxFileBytes, remainingBytes) {
|
|
|
8779
9100
|
continue;
|
|
8780
9101
|
}
|
|
8781
9102
|
} else if (entry.isDirectory()) {
|
|
8782
|
-
const realDir =
|
|
9103
|
+
const realDir = realpathSync10(full);
|
|
8783
9104
|
if (seenDirs.has(realDir)) continue;
|
|
8784
9105
|
seenDirs.add(realDir);
|
|
8785
9106
|
walk2(full, logicalPath);
|
|
@@ -8788,7 +9109,7 @@ function normalizeDirectory(input, retrievedAt, maxFileBytes, remainingBytes) {
|
|
|
8788
9109
|
skipped.push({ path: logicalPath, reason: "unsupported-media-type" });
|
|
8789
9110
|
continue;
|
|
8790
9111
|
}
|
|
8791
|
-
const realFile =
|
|
9112
|
+
const realFile = realpathSync10(full);
|
|
8792
9113
|
if (!isInside3(realFile, realRoot)) {
|
|
8793
9114
|
throw new UserError(`source directory entry escaped its root while being observed: ${logicalPath}`);
|
|
8794
9115
|
}
|
|
@@ -8895,17 +9216,17 @@ function normalizeSources(inputs, options = {}) {
|
|
|
8895
9216
|
// src/insights/store.ts
|
|
8896
9217
|
var STUDY_ID = /^STUDY-[0-9a-f]{16}$/;
|
|
8897
9218
|
function getInsightsDir() {
|
|
8898
|
-
return
|
|
9219
|
+
return join35(getHomeDir(), "insights");
|
|
8899
9220
|
}
|
|
8900
9221
|
function getStudyDir(studyId) {
|
|
8901
9222
|
assertStudyId(studyId);
|
|
8902
|
-
return
|
|
9223
|
+
return join35(getInsightsDir(), studyId);
|
|
8903
9224
|
}
|
|
8904
9225
|
function getStudyPath(studyId) {
|
|
8905
|
-
return
|
|
9226
|
+
return join35(getStudyDir(studyId), "study.json");
|
|
8906
9227
|
}
|
|
8907
9228
|
function getStudyEventsPath(studyId) {
|
|
8908
|
-
return
|
|
9229
|
+
return join35(getStudyDir(studyId), "events.jsonl");
|
|
8909
9230
|
}
|
|
8910
9231
|
function createStudyId(sourceInventoryHash, createdAt) {
|
|
8911
9232
|
return stableId("STUDY", { sourceInventoryHash, createdAt });
|
|
@@ -9036,7 +9357,7 @@ function validateStudyShape(value, path) {
|
|
|
9036
9357
|
function parseStudy(path) {
|
|
9037
9358
|
let raw;
|
|
9038
9359
|
try {
|
|
9039
|
-
raw =
|
|
9360
|
+
raw = readFileSync25(path, "utf-8");
|
|
9040
9361
|
} catch (err) {
|
|
9041
9362
|
throw new UserError(`unable to read insight study at ${path}: ${err.message}`);
|
|
9042
9363
|
}
|
|
@@ -9070,7 +9391,7 @@ function createInsightStudy(input, options = {}) {
|
|
|
9070
9391
|
const studyId = options.studyId ?? createStudyId(input.sourceInventoryHash, createdAt);
|
|
9071
9392
|
assertStudyId(studyId);
|
|
9072
9393
|
const path = getStudyPath(studyId);
|
|
9073
|
-
if (
|
|
9394
|
+
if (existsSync27(path)) throw new UserError(`insight study already exists: ${studyId}`);
|
|
9074
9395
|
const study = {
|
|
9075
9396
|
schemaVersion: INSIGHT_SCHEMA_VERSION,
|
|
9076
9397
|
studyId,
|
|
@@ -9091,7 +9412,7 @@ function createInsightStudy(input, options = {}) {
|
|
|
9091
9412
|
}
|
|
9092
9413
|
function readInsightStudy(studyId) {
|
|
9093
9414
|
const path = getStudyPath(studyId);
|
|
9094
|
-
if (!
|
|
9415
|
+
if (!existsSync27(path)) throw new UserError(`no insight study with id: ${studyId}`);
|
|
9095
9416
|
const study = parseStudy(path);
|
|
9096
9417
|
if (study.studyId !== studyId) {
|
|
9097
9418
|
throw new UserError(`malformed insight study at ${path}: stored studyId does not match its directory`);
|
|
@@ -9101,7 +9422,7 @@ function readInsightStudy(studyId) {
|
|
|
9101
9422
|
function writeInsightStudy(study, now = /* @__PURE__ */ new Date()) {
|
|
9102
9423
|
assertStudyId(study.studyId);
|
|
9103
9424
|
const path = getStudyPath(study.studyId);
|
|
9104
|
-
if (!
|
|
9425
|
+
if (!existsSync27(path)) throw new UserError(`no insight study with id: ${study.studyId}`);
|
|
9105
9426
|
const existing = parseStudy(path);
|
|
9106
9427
|
let next = { ...study, updatedAt: now.toISOString() };
|
|
9107
9428
|
if (next.status === "dismissed" || next.status === "archived") {
|
|
@@ -9838,7 +10159,7 @@ function buildJiraManifest(study, projectKey, options = {}) {
|
|
|
9838
10159
|
return manifest;
|
|
9839
10160
|
}
|
|
9840
10161
|
function getJiraReceiptsPath(studyId) {
|
|
9841
|
-
return
|
|
10162
|
+
return join36(getStudyDir(studyId), "jira-receipts.jsonl");
|
|
9842
10163
|
}
|
|
9843
10164
|
function assertCurrentJiraManifest(study, value) {
|
|
9844
10165
|
if (study.status !== "jira-manifested" || !study.jira) {
|
|
@@ -9970,10 +10291,10 @@ function appendJiraReceipt(studyId, manifest, input) {
|
|
|
9970
10291
|
function readJiraReceipts(studyId, manifest) {
|
|
9971
10292
|
const currentManifest = assertCurrentJiraManifest(readInsightStudy(studyId), manifest);
|
|
9972
10293
|
const path = getJiraReceiptsPath(studyId);
|
|
9973
|
-
if (!
|
|
10294
|
+
if (!existsSync28(path)) return [];
|
|
9974
10295
|
let text;
|
|
9975
10296
|
try {
|
|
9976
|
-
text =
|
|
10297
|
+
text = readFileSync26(path, "utf-8");
|
|
9977
10298
|
} catch (error) {
|
|
9978
10299
|
throw new UserError(`unable to read Jira receipts at ${path}: ${error.message}`);
|
|
9979
10300
|
}
|
|
@@ -10115,7 +10436,7 @@ function parseJsonFile(path, label2) {
|
|
|
10115
10436
|
const resolved = resolvePath12(path);
|
|
10116
10437
|
let raw;
|
|
10117
10438
|
try {
|
|
10118
|
-
raw =
|
|
10439
|
+
raw = readFileSync27(resolved, "utf-8");
|
|
10119
10440
|
} catch (error) {
|
|
10120
10441
|
throw new UserError(`unable to read ${label2} at ${resolved}: ${error.message}`);
|
|
10121
10442
|
}
|
|
@@ -10170,7 +10491,7 @@ function assertCommonDescriptorFields(value, label2) {
|
|
|
10170
10491
|
}
|
|
10171
10492
|
function pathRelativeTo(baseDir, raw, label2) {
|
|
10172
10493
|
const path = nonEmptyString(raw, label2);
|
|
10173
|
-
return
|
|
10494
|
+
return isAbsolute9(path) ? path : resolvePath12(baseDir, path);
|
|
10174
10495
|
}
|
|
10175
10496
|
function parseManifestDescriptor(value, index, baseDir) {
|
|
10176
10497
|
const label2 = `source manifest entry ${index + 1}`;
|
|
@@ -10260,10 +10581,10 @@ function collectSourceInputs(sourceArgs, options) {
|
|
|
10260
10581
|
return inputs;
|
|
10261
10582
|
}
|
|
10262
10583
|
function contextPath(studyId) {
|
|
10263
|
-
return
|
|
10584
|
+
return join37(getStudyDir(studyId), CONTEXT_FILENAME);
|
|
10264
10585
|
}
|
|
10265
10586
|
function inventoryPath(studyId) {
|
|
10266
|
-
return
|
|
10587
|
+
return join37(getStudyDir(studyId), INVENTORY_FILENAME);
|
|
10267
10588
|
}
|
|
10268
10589
|
function writeCommandContext(studyId, skillMapPath, skillMapCoverage, inventory) {
|
|
10269
10590
|
const context = {
|
|
@@ -10277,7 +10598,7 @@ function writeCommandContext(studyId, skillMapPath, skillMapCoverage, inventory)
|
|
|
10277
10598
|
function readCommandContext(studyId) {
|
|
10278
10599
|
const path = contextPath(studyId);
|
|
10279
10600
|
const parsed = parseJsonFile(path, "insight command context");
|
|
10280
|
-
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))) {
|
|
10281
10602
|
throw new UserError(`malformed insight command context at ${path}`);
|
|
10282
10603
|
}
|
|
10283
10604
|
return parsed;
|
|
@@ -10301,18 +10622,18 @@ function readCurrentStudy(studyId) {
|
|
|
10301
10622
|
}
|
|
10302
10623
|
function retainedPath(root, relativePath) {
|
|
10303
10624
|
const candidate = resolvePath12(root, relativePath);
|
|
10304
|
-
const rel =
|
|
10305
|
-
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`);
|
|
10306
10627
|
return candidate;
|
|
10307
10628
|
}
|
|
10308
10629
|
function retainObservedSources(study, observedFiles) {
|
|
10309
10630
|
if (observedFiles.length === 0) return study;
|
|
10310
|
-
const root =
|
|
10631
|
+
const root = join37(getStudyDir(study.studyId), "retained-sources");
|
|
10311
10632
|
mkdirSync9(root, { recursive: true, mode: 448 });
|
|
10312
10633
|
chmodSync3(root, 448);
|
|
10313
10634
|
const retainedSourceIds = /* @__PURE__ */ new Set();
|
|
10314
10635
|
for (const file of observedFiles) {
|
|
10315
|
-
const sourceRoot =
|
|
10636
|
+
const sourceRoot = join37(root, file.sourceId);
|
|
10316
10637
|
mkdirSync9(sourceRoot, { recursive: true, mode: 448 });
|
|
10317
10638
|
chmodSync3(sourceRoot, 448);
|
|
10318
10639
|
const destination = retainedPath(sourceRoot, file.relativePath);
|
|
@@ -10550,7 +10871,7 @@ async function insightJira(studyId, options) {
|
|
|
10550
10871
|
throw new UserError(`study ${studyId} cannot generate a Jira manifest: ${validation.blockers.map((issue) => issue.message).join("; ")}`);
|
|
10551
10872
|
}
|
|
10552
10873
|
if (localMode) {
|
|
10553
|
-
const privatePath2 =
|
|
10874
|
+
const privatePath2 = join37(getStudyDir(studyId), JIRA_MANIFEST_FILENAME);
|
|
10554
10875
|
const manifest2 = assertCurrentJiraManifest(study, parseJsonFile(privatePath2, "stored Jira manifest"));
|
|
10555
10876
|
const receipt = options.receipt === void 0 ? void 0 : appendJiraReceipt(studyId, manifest2, parseJiraReceiptInput(parseJsonFile(options.receipt, "Jira receipt import")));
|
|
10556
10877
|
const resume = buildJiraResumeState(manifest2, readJiraReceipts(studyId, manifest2));
|
|
@@ -10562,7 +10883,7 @@ async function insightJira(studyId, options) {
|
|
|
10562
10883
|
return;
|
|
10563
10884
|
}
|
|
10564
10885
|
const manifest = buildJiraManifest(study, projectKey);
|
|
10565
|
-
const privatePath =
|
|
10886
|
+
const privatePath = join37(getStudyDir(studyId), JIRA_MANIFEST_FILENAME);
|
|
10566
10887
|
writeJsonAtomic(privatePath, manifest);
|
|
10567
10888
|
if (options.out) {
|
|
10568
10889
|
writeNewFile(resolvePath12(options.out), `${JSON.stringify(manifest, null, 2)}
|
|
@@ -10633,14 +10954,11 @@ async function insightClose(studyId, options) {
|
|
|
10633
10954
|
}
|
|
10634
10955
|
|
|
10635
10956
|
// src/commands/refine.ts
|
|
10636
|
-
import {
|
|
10637
|
-
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";
|
|
10638
|
-
import { dirname as dirname15, isAbsolute as isAbsolute9, join as join38, relative as relative8, resolve as resolvePath15, sep as sep8 } from "path";
|
|
10639
|
-
import { createInterface as createInterface7 } from "readline/promises";
|
|
10957
|
+
import { tmpdir as tmpdir3 } from "os";
|
|
10640
10958
|
|
|
10641
|
-
// src/refine/
|
|
10642
|
-
import {
|
|
10643
|
-
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";
|
|
10644
10962
|
|
|
10645
10963
|
// src/refine/models.ts
|
|
10646
10964
|
import { randomBytes } from "crypto";
|
|
@@ -10708,52 +11026,413 @@ function contentSimilarity(a, b) {
|
|
|
10708
11026
|
return unionSize3 === 0 ? 0 : intersectionSize / unionSize3;
|
|
10709
11027
|
}
|
|
10710
11028
|
|
|
10711
|
-
// src/refine/
|
|
10712
|
-
|
|
10713
|
-
|
|
10714
|
-
|
|
10715
|
-
var
|
|
10716
|
-
var
|
|
10717
|
-
function
|
|
10718
|
-
|
|
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++;
|
|
11091
|
+
}
|
|
11092
|
+
finalizeCurrent();
|
|
11093
|
+
if (blocks.length === 0) {
|
|
11094
|
+
throw new UserError("No ACTION blocks found in patch source");
|
|
11095
|
+
}
|
|
11096
|
+
return blocks;
|
|
10719
11097
|
}
|
|
10720
|
-
function
|
|
10721
|
-
|
|
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;
|
|
11102
|
+
}
|
|
11103
|
+
return 0;
|
|
10722
11104
|
}
|
|
10723
|
-
function
|
|
10724
|
-
|
|
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 });
|
|
11117
|
+
}
|
|
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);
|
|
11128
|
+
}
|
|
11129
|
+
return root;
|
|
10725
11130
|
}
|
|
10726
|
-
function
|
|
10727
|
-
return
|
|
11131
|
+
function normalizeText(s) {
|
|
11132
|
+
return s.trim().replace(/\s+/g, " ");
|
|
10728
11133
|
}
|
|
10729
|
-
function
|
|
10730
|
-
|
|
10731
|
-
|
|
10732
|
-
|
|
10733
|
-
|
|
10734
|
-
|
|
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"})`);
|
|
10735
11151
|
}
|
|
10736
|
-
|
|
10737
|
-
|
|
10738
|
-
|
|
10739
|
-
|
|
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;
|
|
11166
|
+
}
|
|
11167
|
+
if (candidates.length > 1) {
|
|
11168
|
+
throw new UserError(`Target selector "${segments.join(" > ")}" matched ${candidates.length} sections; be more specific`);
|
|
10740
11169
|
}
|
|
11170
|
+
return candidates[0];
|
|
10741
11171
|
}
|
|
10742
|
-
function
|
|
10743
|
-
|
|
10744
|
-
|
|
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);
|
|
10745
11177
|
}
|
|
10746
|
-
|
|
10747
|
-
|
|
10748
|
-
throw new UserError(`malformed ${label2} at ${path}: expected an object with a "records" array`);
|
|
11178
|
+
if (matches.length === 0) {
|
|
11179
|
+
throw new UserError(`No line matching marker "${marker}" found in target section`);
|
|
10749
11180
|
}
|
|
10750
|
-
|
|
10751
|
-
|
|
10752
|
-
|
|
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})`);
|
|
10753
11184
|
}
|
|
10754
|
-
return
|
|
11185
|
+
return matches[0];
|
|
10755
11186
|
}
|
|
10756
|
-
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) {
|
|
10757
11436
|
if (typeof record !== "object" || record === null) {
|
|
10758
11437
|
throw new UserError(`malformed refinements.json at ${path}: record[${index}] is not an object`);
|
|
10759
11438
|
}
|
|
@@ -10832,7 +11511,7 @@ function writePatternsStore(store) {
|
|
|
10832
11511
|
function resolveProjectRealpath(projectRoot) {
|
|
10833
11512
|
const resolved = resolvePath13(projectRoot);
|
|
10834
11513
|
try {
|
|
10835
|
-
return
|
|
11514
|
+
return realpathSync12(resolved);
|
|
10836
11515
|
} catch {
|
|
10837
11516
|
return resolved;
|
|
10838
11517
|
}
|
|
@@ -10964,16 +11643,16 @@ function createBackup(filePaths, deps = {}) {
|
|
|
10964
11643
|
const randomHex = deps.randomHex ?? defaultRandomHex;
|
|
10965
11644
|
const id = `${formatBackupTimestamp(now)}-${randomHex()}`;
|
|
10966
11645
|
const files = filePaths.map((path) => {
|
|
10967
|
-
if (!
|
|
11646
|
+
if (!existsSync29(path)) {
|
|
10968
11647
|
return { path, existed: false, sha256: null, mode: null, contentBase64: null };
|
|
10969
11648
|
}
|
|
10970
|
-
const bytes =
|
|
10971
|
-
const mode =
|
|
11649
|
+
const bytes = readFileSync29(path);
|
|
11650
|
+
const mode = statSync13(path).mode & 511;
|
|
10972
11651
|
const sha2562 = createHash7("sha256").update(bytes).digest("hex");
|
|
10973
11652
|
return { path, existed: true, sha256: sha2562, mode, contentBase64: bytes.toString("base64") };
|
|
10974
11653
|
});
|
|
10975
11654
|
const manifest = { id, createdAt: now.toISOString(), files };
|
|
10976
|
-
writeJsonAtomic(
|
|
11655
|
+
writeJsonAtomic(join39(getBackupsDir(), id, "manifest.json"), manifest);
|
|
10977
11656
|
return manifest;
|
|
10978
11657
|
}
|
|
10979
11658
|
function validateBackupManifestShape(value, path) {
|
|
@@ -11022,8 +11701,8 @@ function readBackupManifest(id) {
|
|
|
11022
11701
|
if (!isValidBackupId(id)) {
|
|
11023
11702
|
throw new UserError(`invalid backup id "${id}" \u2014 expected the format produced by \`skill-forge refine promote\` (e.g. 20260720T142305Z-a1b2)`);
|
|
11024
11703
|
}
|
|
11025
|
-
const manifestPath =
|
|
11026
|
-
if (!
|
|
11704
|
+
const manifestPath = join39(getBackupsDir(), id, "manifest.json");
|
|
11705
|
+
if (!existsSync29(manifestPath)) {
|
|
11027
11706
|
throw new UserError(`no backup with id: ${id}`);
|
|
11028
11707
|
}
|
|
11029
11708
|
const parsed = readJsonFile(manifestPath, "backup manifest");
|
|
@@ -11045,32 +11724,32 @@ function assertPlainSkillName(name) {
|
|
|
11045
11724
|
}
|
|
11046
11725
|
function realpathOrNull2(p) {
|
|
11047
11726
|
try {
|
|
11048
|
-
return
|
|
11727
|
+
return realpathSync13(p);
|
|
11049
11728
|
} catch {
|
|
11050
11729
|
return null;
|
|
11051
11730
|
}
|
|
11052
11731
|
}
|
|
11053
11732
|
function isUnderRoot2(candidate, root) {
|
|
11054
|
-
const rel =
|
|
11055
|
-
return rel === "" || !rel.startsWith("..") && !
|
|
11733
|
+
const rel = relative9(root, candidate);
|
|
11734
|
+
return rel === "" || !rel.startsWith("..") && !isAbsolute10(rel);
|
|
11056
11735
|
}
|
|
11057
11736
|
function resolveScopes(config, cwd, skill, userRootFlag) {
|
|
11058
11737
|
assertPlainSkillName(skill);
|
|
11059
11738
|
const resolvedCwd = resolvePath14(cwd);
|
|
11060
|
-
const localDir =
|
|
11061
|
-
const sharedDir =
|
|
11062
|
-
const local = { scope: "local", dir: localDir, exists:
|
|
11063
|
-
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) };
|
|
11064
11743
|
const user = resolveUserScope(config, resolvedCwd, skill, userRootFlag);
|
|
11065
11744
|
return { local, shared, user };
|
|
11066
11745
|
}
|
|
11067
11746
|
function resolveUserScope(config, resolvedCwd, skill, userRootFlag) {
|
|
11068
11747
|
if (userRootFlag) {
|
|
11069
11748
|
const root = resolvePath14(userRootFlag);
|
|
11070
|
-
return { resolved: true, dir:
|
|
11749
|
+
return { resolved: true, dir: join40(root, skill), root: userRootFlag, reason: "flag" };
|
|
11071
11750
|
}
|
|
11072
11751
|
const cwdReal = realpathOrNull2(resolvedCwd) ?? resolvedCwd;
|
|
11073
|
-
const candidates = config.skillsRoots.map((root) => ({ root, dir:
|
|
11752
|
+
const candidates = config.skillsRoots.map((root) => ({ root, dir: join40(resolveConfiguredRoot2(root), skill) }));
|
|
11074
11753
|
for (const candidate of candidates) {
|
|
11075
11754
|
const skillReal = realpathOrNull2(candidate.dir);
|
|
11076
11755
|
if (skillReal) {
|
|
@@ -11088,29 +11767,29 @@ function resolveUserScope(config, resolvedCwd, skill, userRootFlag) {
|
|
|
11088
11767
|
}
|
|
11089
11768
|
function isFile2(p) {
|
|
11090
11769
|
try {
|
|
11091
|
-
return
|
|
11770
|
+
return statSync14(p).isFile();
|
|
11092
11771
|
} catch {
|
|
11093
11772
|
return false;
|
|
11094
11773
|
}
|
|
11095
11774
|
}
|
|
11096
11775
|
function isDirectory(p) {
|
|
11097
11776
|
try {
|
|
11098
|
-
return
|
|
11777
|
+
return statSync14(p).isDirectory();
|
|
11099
11778
|
} catch {
|
|
11100
11779
|
return false;
|
|
11101
11780
|
}
|
|
11102
11781
|
}
|
|
11103
|
-
function inventoryScopeDir(
|
|
11782
|
+
function inventoryScopeDir(scope2, dir) {
|
|
11104
11783
|
return {
|
|
11105
|
-
scope,
|
|
11784
|
+
scope: scope2,
|
|
11106
11785
|
dir,
|
|
11107
11786
|
dirExists: isDirectory(dir),
|
|
11108
|
-
skillMd: isFile2(
|
|
11109
|
-
patchMd: isFile2(
|
|
11110
|
-
extendMd: isFile2(
|
|
11111
|
-
configJson: isFile2(
|
|
11112
|
-
hooksDir: isDirectory(
|
|
11113
|
-
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"))
|
|
11114
11793
|
};
|
|
11115
11794
|
}
|
|
11116
11795
|
function inventoryAllScopes(resolution) {
|
|
@@ -11121,230 +11800,10 @@ function inventoryAllScopes(resolution) {
|
|
|
11121
11800
|
return rows;
|
|
11122
11801
|
}
|
|
11123
11802
|
|
|
11124
|
-
// src/refine/patch.ts
|
|
11125
|
-
var FENCE_RE = /^\s*(```|~~~)/;
|
|
11126
|
-
var HEADING_RE = /^(#{1,6})\s+(.*)$/;
|
|
11127
|
-
function frontmatterOffset(lines) {
|
|
11128
|
-
if (lines[0] !== "---") return 0;
|
|
11129
|
-
for (let i = 1; i < lines.length; i++) {
|
|
11130
|
-
if (lines[i] === "---") return i + 1;
|
|
11131
|
-
}
|
|
11132
|
-
return 0;
|
|
11133
|
-
}
|
|
11134
|
-
function buildHeadingTree(lines, bodyOffset) {
|
|
11135
|
-
const flat = [];
|
|
11136
|
-
let inFence = false;
|
|
11137
|
-
for (let i = bodyOffset; i < lines.length; i++) {
|
|
11138
|
-
const line = lines[i];
|
|
11139
|
-
if (FENCE_RE.test(line)) {
|
|
11140
|
-
inFence = !inFence;
|
|
11141
|
-
continue;
|
|
11142
|
-
}
|
|
11143
|
-
if (inFence) continue;
|
|
11144
|
-
const m = HEADING_RE.exec(line);
|
|
11145
|
-
if (m) flat.push({ level: m[1].length, text: m[2].trim(), line: i });
|
|
11146
|
-
}
|
|
11147
|
-
const root = { level: 0, heading: "", headingLine: bodyOffset - 1, bodyStart: bodyOffset, bodyEnd: lines.length, children: [] };
|
|
11148
|
-
const stack = [root];
|
|
11149
|
-
for (const h of flat) {
|
|
11150
|
-
while (stack.length > 1 && stack[stack.length - 1].level >= h.level) {
|
|
11151
|
-
const done = stack.pop();
|
|
11152
|
-
if (done) done.bodyEnd = h.line;
|
|
11153
|
-
}
|
|
11154
|
-
const node = { level: h.level, heading: h.text, headingLine: h.line, bodyStart: h.line + 1, bodyEnd: lines.length, children: [] };
|
|
11155
|
-
stack[stack.length - 1].children.push(node);
|
|
11156
|
-
stack.push(node);
|
|
11157
|
-
}
|
|
11158
|
-
return root;
|
|
11159
|
-
}
|
|
11160
|
-
function normalizeText(s) {
|
|
11161
|
-
return s.trim().replace(/\s+/g, " ");
|
|
11162
|
-
}
|
|
11163
|
-
function splitHeadingPath(target) {
|
|
11164
|
-
return target.split(">").map((segment) => segment.trim()).filter((segment) => segment.length > 0);
|
|
11165
|
-
}
|
|
11166
|
-
function collectDescendants(node, acc) {
|
|
11167
|
-
acc.push(node);
|
|
11168
|
-
for (const child of node.children) collectDescendants(child, acc);
|
|
11169
|
-
}
|
|
11170
|
-
function resolveHeadingPath(root, segments) {
|
|
11171
|
-
if (segments.length === 0) return root;
|
|
11172
|
-
const allNodes = [];
|
|
11173
|
-
collectDescendants(root, allNodes);
|
|
11174
|
-
const headingNodes = allNodes.filter((n) => n.level > 0);
|
|
11175
|
-
const firstNorm = normalizeText(segments[0]);
|
|
11176
|
-
let candidates = headingNodes.filter((n) => normalizeText(n.heading) === firstNorm);
|
|
11177
|
-
if (candidates.length === 0) {
|
|
11178
|
-
const available = headingNodes.map((n) => n.heading);
|
|
11179
|
-
throw new UserError(`No section matching "${segments[0]}" found (available headings: ${available.length > 0 ? available.join(", ") : "none"})`);
|
|
11180
|
-
}
|
|
11181
|
-
for (let i = 1; i < segments.length; i++) {
|
|
11182
|
-
const segment = segments[i];
|
|
11183
|
-
const norm = normalizeText(segment);
|
|
11184
|
-
const next = [];
|
|
11185
|
-
for (const candidate of candidates) {
|
|
11186
|
-
for (const child of candidate.children) {
|
|
11187
|
-
if (normalizeText(child.heading) === norm) next.push(child);
|
|
11188
|
-
}
|
|
11189
|
-
}
|
|
11190
|
-
if (next.length === 0) {
|
|
11191
|
-
const available = [...new Set(candidates.flatMap((c) => c.children.map((child) => child.heading)))];
|
|
11192
|
-
throw new UserError(`No section matching "${segment}" found (available headings: ${available.length > 0 ? available.join(", ") : "none"})`);
|
|
11193
|
-
}
|
|
11194
|
-
candidates = next;
|
|
11195
|
-
}
|
|
11196
|
-
if (candidates.length > 1) {
|
|
11197
|
-
throw new UserError(`Target selector "${segments.join(" > ")}" matched ${candidates.length} sections; be more specific`);
|
|
11198
|
-
}
|
|
11199
|
-
return candidates[0];
|
|
11200
|
-
}
|
|
11201
|
-
function findMarkerLine(lines, start, end, marker) {
|
|
11202
|
-
const norm = normalizeText(marker);
|
|
11203
|
-
const matches = [];
|
|
11204
|
-
for (let i = start; i < end; i++) {
|
|
11205
|
-
if (normalizeText(lines[i]).includes(norm)) matches.push(i);
|
|
11206
|
-
}
|
|
11207
|
-
if (matches.length === 0) {
|
|
11208
|
-
throw new UserError(`No line matching marker "${marker}" found in target section`);
|
|
11209
|
-
}
|
|
11210
|
-
if (matches.length > 1) {
|
|
11211
|
-
const preview = matches.map((i) => `line ${i + 1}: "${lines[i].trim()}"`).join("; ");
|
|
11212
|
-
throw new UserError(`Marker "${marker}" matched ${matches.length} lines, expected exactly 1 (${preview})`);
|
|
11213
|
-
}
|
|
11214
|
-
return matches[0];
|
|
11215
|
-
}
|
|
11216
|
-
function findChildByMarker(target, marker) {
|
|
11217
|
-
const norm = normalizeText(marker);
|
|
11218
|
-
const matches = target.children.filter((c) => normalizeText(c.heading).includes(norm));
|
|
11219
|
-
if (matches.length === 0) {
|
|
11220
|
-
const available = target.children.map((c) => c.heading).join(", ") || "none";
|
|
11221
|
-
throw new UserError(`No subsection matching "${marker}" found under target section (available: ${available})`);
|
|
11222
|
-
}
|
|
11223
|
-
if (matches.length > 1) {
|
|
11224
|
-
throw new UserError(`Marker "${marker}" matched ${matches.length} subsections: ${matches.map((c) => c.heading).join(", ")}`);
|
|
11225
|
-
}
|
|
11226
|
-
return matches[0];
|
|
11227
|
-
}
|
|
11228
|
-
function requireMarker(block) {
|
|
11229
|
-
if (!block.marker || block.marker.trim().length === 0) {
|
|
11230
|
-
throw new UserError(`Action "${block.action}" requires a marker`);
|
|
11231
|
-
}
|
|
11232
|
-
return block.marker;
|
|
11233
|
-
}
|
|
11234
|
-
function applyOneAction(lines, target, block) {
|
|
11235
|
-
const contentLines = block.content.length === 0 ? [""] : block.content.split("\n");
|
|
11236
|
-
switch (block.action) {
|
|
11237
|
-
case "append": {
|
|
11238
|
-
lines.splice(target.bodyEnd, 0, ...contentLines);
|
|
11239
|
-
return;
|
|
11240
|
-
}
|
|
11241
|
-
case "prepend": {
|
|
11242
|
-
lines.splice(target.bodyStart, 0, ...contentLines);
|
|
11243
|
-
return;
|
|
11244
|
-
}
|
|
11245
|
-
case "insert-after":
|
|
11246
|
-
case "insert-before": {
|
|
11247
|
-
const idx = findMarkerLine(lines, target.bodyStart, target.bodyEnd, requireMarker(block));
|
|
11248
|
-
lines.splice(block.action === "insert-after" ? idx + 1 : idx, 0, ...contentLines);
|
|
11249
|
-
return;
|
|
11250
|
-
}
|
|
11251
|
-
case "replace-section":
|
|
11252
|
-
case "delete-section": {
|
|
11253
|
-
const child = findChildByMarker(target, requireMarker(block));
|
|
11254
|
-
const replacement = block.action === "replace-section" ? contentLines : [];
|
|
11255
|
-
lines.splice(child.headingLine, child.bodyEnd - child.headingLine, ...replacement);
|
|
11256
|
-
return;
|
|
11257
|
-
}
|
|
11258
|
-
default: {
|
|
11259
|
-
const exhaustive = block.action;
|
|
11260
|
-
throw new UserError(`Unknown patch action "${exhaustive}"`);
|
|
11261
|
-
}
|
|
11262
|
-
}
|
|
11263
|
-
}
|
|
11264
|
-
function applyPatchBlocks(skillMd, blocks) {
|
|
11265
|
-
const lines = skillMd.split("\n");
|
|
11266
|
-
for (const block of blocks) {
|
|
11267
|
-
const bodyOffset = frontmatterOffset(lines);
|
|
11268
|
-
const root = buildHeadingTree(lines, bodyOffset);
|
|
11269
|
-
const segments = block.target ? splitHeadingPath(block.target) : [];
|
|
11270
|
-
const target = resolveHeadingPath(root, segments);
|
|
11271
|
-
applyOneAction(lines, target, block);
|
|
11272
|
-
}
|
|
11273
|
-
return lines.join("\n");
|
|
11274
|
-
}
|
|
11275
|
-
function renderExtendFile(meta) {
|
|
11276
|
-
return [
|
|
11277
|
-
"# SKILL.extend.md",
|
|
11278
|
-
`# Extensions for: ${meta.skillName}`,
|
|
11279
|
-
`# Generated: ${meta.generatedAt}`,
|
|
11280
|
-
`# Refinement: ${meta.refinementId}`,
|
|
11281
|
-
"",
|
|
11282
|
-
`## EXTEND: ${meta.targetSection}`,
|
|
11283
|
-
"<!-- ACTION: append -->",
|
|
11284
|
-
meta.content,
|
|
11285
|
-
""
|
|
11286
|
-
].join("\n");
|
|
11287
|
-
}
|
|
11288
|
-
function renderConfigOverride(meta) {
|
|
11289
|
-
const payload = {
|
|
11290
|
-
skill: meta.skillName,
|
|
11291
|
-
version: "1.0.0",
|
|
11292
|
-
generated: meta.generatedAt,
|
|
11293
|
-
config: meta.config
|
|
11294
|
-
};
|
|
11295
|
-
return `${JSON.stringify(payload, null, 2)}
|
|
11296
|
-
`;
|
|
11297
|
-
}
|
|
11298
|
-
function computeLcsDiff(a, b) {
|
|
11299
|
-
const n = a.length;
|
|
11300
|
-
const m = b.length;
|
|
11301
|
-
const dp = Array.from({ length: n + 1 }, () => new Array(m + 1).fill(0));
|
|
11302
|
-
for (let i2 = n - 1; i2 >= 0; i2--) {
|
|
11303
|
-
for (let j2 = m - 1; j2 >= 0; j2--) {
|
|
11304
|
-
dp[i2][j2] = a[i2] === b[j2] ? dp[i2 + 1][j2 + 1] + 1 : Math.max(dp[i2 + 1][j2], dp[i2][j2 + 1]);
|
|
11305
|
-
}
|
|
11306
|
-
}
|
|
11307
|
-
const ops = [];
|
|
11308
|
-
let i = 0;
|
|
11309
|
-
let j = 0;
|
|
11310
|
-
while (i < n && j < m) {
|
|
11311
|
-
if (a[i] === b[j]) {
|
|
11312
|
-
ops.push({ type: "context", line: a[i] });
|
|
11313
|
-
i++;
|
|
11314
|
-
j++;
|
|
11315
|
-
} else if (dp[i + 1][j] >= dp[i][j + 1]) {
|
|
11316
|
-
ops.push({ type: "remove", line: a[i] });
|
|
11317
|
-
i++;
|
|
11318
|
-
} else {
|
|
11319
|
-
ops.push({ type: "add", line: b[j] });
|
|
11320
|
-
j++;
|
|
11321
|
-
}
|
|
11322
|
-
}
|
|
11323
|
-
while (i < n) {
|
|
11324
|
-
ops.push({ type: "remove", line: a[i] });
|
|
11325
|
-
i++;
|
|
11326
|
-
}
|
|
11327
|
-
while (j < m) {
|
|
11328
|
-
ops.push({ type: "add", line: b[j] });
|
|
11329
|
-
j++;
|
|
11330
|
-
}
|
|
11331
|
-
return ops;
|
|
11332
|
-
}
|
|
11333
|
-
function previewDiff(before, after, path) {
|
|
11334
|
-
const a = before.split("\n");
|
|
11335
|
-
const b = after.split("\n");
|
|
11336
|
-
const ops = computeLcsDiff(a, b);
|
|
11337
|
-
const lines = [`--- ${path}`, `+++ ${path}`];
|
|
11338
|
-
for (const op of ops) {
|
|
11339
|
-
const prefix = op.type === "add" ? "+" : op.type === "remove" ? "-" : " ";
|
|
11340
|
-
lines.push(`${prefix}${op.line}`);
|
|
11341
|
-
}
|
|
11342
|
-
return lines.join("\n");
|
|
11343
|
-
}
|
|
11344
|
-
|
|
11345
11803
|
// src/commands/refine.ts
|
|
11346
11804
|
var PRO_FEATURE_LABEL9 = "skill refinement";
|
|
11347
11805
|
var REFINE_PROMPT_ASSET = "refine-prompt.md";
|
|
11806
|
+
var PROJECT_SKILL_ROOTS = { claude: ".claude/skills", codex: ".agents/skills" };
|
|
11348
11807
|
function requireProFeature3() {
|
|
11349
11808
|
const status2 = proFeatureStatus(PRO_FEATURE_LABEL9);
|
|
11350
11809
|
if (!status2.enabled) {
|
|
@@ -11382,15 +11841,15 @@ function resolveConfiguredRoot3(root) {
|
|
|
11382
11841
|
return resolvePath15(expandHome(root));
|
|
11383
11842
|
}
|
|
11384
11843
|
function isUnderRoot3(candidate, root) {
|
|
11385
|
-
const rel =
|
|
11386
|
-
return rel === "" || !rel.startsWith("..") && !
|
|
11844
|
+
const rel = relative10(root, candidate);
|
|
11845
|
+
return rel === "" || !rel.startsWith("..") && !isAbsolute11(rel);
|
|
11387
11846
|
}
|
|
11388
11847
|
function sanitizeDiffForConsole(diff) {
|
|
11389
11848
|
return diff.split("\n").map((line) => sanitizeForMarkdown(line)).join("\n");
|
|
11390
11849
|
}
|
|
11391
11850
|
function resolveContentOption(options) {
|
|
11392
11851
|
if (options.content !== void 0) return options.content;
|
|
11393
|
-
if (options.contentFile) return
|
|
11852
|
+
if (options.contentFile) return readFileSync30(resolvePath15(options.contentFile), "utf-8");
|
|
11394
11853
|
return void 0;
|
|
11395
11854
|
}
|
|
11396
11855
|
function validateCaptureOptions(options) {
|
|
@@ -11516,7 +11975,7 @@ ${rendered}` : rendered;
|
|
|
11516
11975
|
function ensureScopeDirSafe(scopeDir, resolvedCwd) {
|
|
11517
11976
|
let lst;
|
|
11518
11977
|
try {
|
|
11519
|
-
lst =
|
|
11978
|
+
lst = lstatSync8(scopeDir);
|
|
11520
11979
|
} catch {
|
|
11521
11980
|
lst = void 0;
|
|
11522
11981
|
}
|
|
@@ -11528,7 +11987,7 @@ function ensureScopeDirSafe(scopeDir, resolvedCwd) {
|
|
|
11528
11987
|
const lexical = resolvePath15(scopeDir);
|
|
11529
11988
|
let realResolved;
|
|
11530
11989
|
try {
|
|
11531
|
-
realResolved =
|
|
11990
|
+
realResolved = realpathSync14(scopeDir);
|
|
11532
11991
|
} catch {
|
|
11533
11992
|
realResolved = real;
|
|
11534
11993
|
}
|
|
@@ -11542,16 +12001,16 @@ function ensureScopeDirSafe(scopeDir, resolvedCwd) {
|
|
|
11542
12001
|
}
|
|
11543
12002
|
function writeOverrideFile(scopeDir, relPath, content, resolvedCwd) {
|
|
11544
12003
|
const realScopeDir = ensureScopeDirSafe(scopeDir, resolvedCwd);
|
|
11545
|
-
const destPath =
|
|
12004
|
+
const destPath = join41(scopeDir, relPath);
|
|
11546
12005
|
const destDir = dirname15(destPath);
|
|
11547
12006
|
mkdirSync11(destDir, { recursive: true });
|
|
11548
|
-
const realDestDir =
|
|
12007
|
+
const realDestDir = realpathSync14(destDir);
|
|
11549
12008
|
if (!isUnderRoot3(realDestDir, realScopeDir)) {
|
|
11550
12009
|
throw new UserError(`refusing to write ${destPath}: resolves outside scope directory ${scopeDir}`);
|
|
11551
12010
|
}
|
|
11552
12011
|
let destLst;
|
|
11553
12012
|
try {
|
|
11554
|
-
destLst =
|
|
12013
|
+
destLst = lstatSync8(destPath);
|
|
11555
12014
|
} catch {
|
|
11556
12015
|
destLst = void 0;
|
|
11557
12016
|
}
|
|
@@ -11638,15 +12097,105 @@ Write ${preview.destPath}? [y/N] `);
|
|
|
11638
12097
|
}
|
|
11639
12098
|
function renderCaptureArtifactPreview(opts, content, refinementId, generatedAt, scopeDir) {
|
|
11640
12099
|
const probeRelPath = opts.overrideType === "patch" ? "SKILL.patch.md" : opts.overrideType === "extend" || opts.overrideType === "new" ? "SKILL.extend.md" : void 0;
|
|
11641
|
-
const probePath = probeRelPath ?
|
|
11642
|
-
const existingContent = probePath &&
|
|
12100
|
+
const probePath = probeRelPath ? join41(scopeDir, probeRelPath) : void 0;
|
|
12101
|
+
const existingContent = probePath && existsSync31(probePath) ? readFileSync30(probePath, "utf-8") : "";
|
|
11643
12102
|
const rendered = renderCaptureArtifact(opts, content, refinementId, generatedAt, existingContent);
|
|
11644
12103
|
const relPath = rendered.relPath;
|
|
11645
|
-
const destPath =
|
|
11646
|
-
const before =
|
|
12104
|
+
const destPath = join41(scopeDir, relPath);
|
|
12105
|
+
const before = existsSync31(destPath) ? readFileSync30(destPath, "utf-8") : "";
|
|
11647
12106
|
const diff = previewDiff(before, rendered.content, destPath);
|
|
11648
12107
|
return { relPath, content: rendered.content, destPath, diff };
|
|
11649
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
|
+
}
|
|
11650
12199
|
async function refineListCommand(options = {}) {
|
|
11651
12200
|
requireProFeature3();
|
|
11652
12201
|
const { records } = readRefinementsStore();
|
|
@@ -11740,16 +12289,16 @@ async function refineWhichCommand(skill, options = {}) {
|
|
|
11740
12289
|
}
|
|
11741
12290
|
function writePostWriteSidecar(backupId, files) {
|
|
11742
12291
|
const sidecar = { schemaVersion: 1, files };
|
|
11743
|
-
const dir =
|
|
12292
|
+
const dir = join41(getBackupsDir(), backupId);
|
|
11744
12293
|
mkdirSync11(dir, { recursive: true });
|
|
11745
|
-
writeFileSync10(
|
|
12294
|
+
writeFileSync10(join41(dir, "post.json"), JSON.stringify(sidecar, null, 2) + "\n", { mode: 384 });
|
|
11746
12295
|
}
|
|
11747
12296
|
function readPostWriteSidecar(backupId) {
|
|
11748
|
-
const path =
|
|
11749
|
-
if (!
|
|
12297
|
+
const path = join41(getBackupsDir(), backupId, "post.json");
|
|
12298
|
+
if (!existsSync31(path)) return { hashes: /* @__PURE__ */ new Map(), status: "missing" };
|
|
11750
12299
|
let raw;
|
|
11751
12300
|
try {
|
|
11752
|
-
raw =
|
|
12301
|
+
raw = readFileSync30(path, "utf-8");
|
|
11753
12302
|
} catch {
|
|
11754
12303
|
return { hashes: /* @__PURE__ */ new Map(), status: "unreadable" };
|
|
11755
12304
|
}
|
|
@@ -11794,21 +12343,21 @@ async function refinePromoteCommand(patternId, options = {}) {
|
|
|
11794
12343
|
const resolvedUserRoot = resolveConfiguredRoot3(scopes.user.root);
|
|
11795
12344
|
let realUserRoot;
|
|
11796
12345
|
try {
|
|
11797
|
-
realUserRoot =
|
|
12346
|
+
realUserRoot = realpathSync14(resolvedUserRoot);
|
|
11798
12347
|
} catch {
|
|
11799
12348
|
realUserRoot = resolvedUserRoot;
|
|
11800
12349
|
}
|
|
11801
12350
|
let realSkillDir;
|
|
11802
12351
|
try {
|
|
11803
|
-
realSkillDir =
|
|
12352
|
+
realSkillDir = realpathSync14(skillDir);
|
|
11804
12353
|
} catch {
|
|
11805
12354
|
realSkillDir = resolvePath15(skillDir);
|
|
11806
12355
|
}
|
|
11807
12356
|
if (!isUnderRoot3(realSkillDir, realUserRoot)) {
|
|
11808
12357
|
throw new UserError(`refusing to promote: resolved skill directory ${skillDir} is outside the configured user scope root ${scopes.user.root}`);
|
|
11809
12358
|
}
|
|
11810
|
-
const skillMdPath =
|
|
11811
|
-
if (!
|
|
12359
|
+
const skillMdPath = join41(skillDir, "SKILL.md");
|
|
12360
|
+
if (!existsSync31(skillMdPath)) {
|
|
11812
12361
|
throw new UserError(`user-scope SKILL.md not found at ${skillMdPath} \u2014 refusing to promote against a missing base skill.`);
|
|
11813
12362
|
}
|
|
11814
12363
|
const refinementsStore = readRefinementsStore();
|
|
@@ -11825,7 +12374,7 @@ async function refinePromoteCommand(patternId, options = {}) {
|
|
|
11825
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.`
|
|
11826
12375
|
);
|
|
11827
12376
|
}
|
|
11828
|
-
const before =
|
|
12377
|
+
const before = readFileSync30(skillMdPath, "utf-8");
|
|
11829
12378
|
const after = applyPatchBlocks(before, [block]);
|
|
11830
12379
|
const diff = previewDiff(before, after, skillMdPath);
|
|
11831
12380
|
if (options.dryRun) {
|
|
@@ -11857,7 +12406,7 @@ Promote pattern ${patternId} into ${skillMdPath}? [y/N] `);
|
|
|
11857
12406
|
throw err;
|
|
11858
12407
|
}
|
|
11859
12408
|
try {
|
|
11860
|
-
const postSha256 = createHash8("sha256").update(
|
|
12409
|
+
const postSha256 = createHash8("sha256").update(readFileSync30(skillMdPath)).digest("hex");
|
|
11861
12410
|
writePostWriteSidecar(backup.id, [{ path: skillMdPath, sha256: postSha256 }]);
|
|
11862
12411
|
markGeneralized(patternId, backup.id);
|
|
11863
12412
|
const skillsRoot = resolveConfiguredRoot3(scopes.user.root);
|
|
@@ -11883,11 +12432,11 @@ Promote pattern ${patternId} into ${skillMdPath}? [y/N] `);
|
|
|
11883
12432
|
}
|
|
11884
12433
|
function assertBackupDirContained(backupId) {
|
|
11885
12434
|
const backupsDir = getBackupsDir();
|
|
11886
|
-
if (!
|
|
11887
|
-
const realBackupsDir =
|
|
11888
|
-
const backupDir =
|
|
11889
|
-
if (!
|
|
11890
|
-
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);
|
|
11891
12440
|
if (!isUnderRoot3(realBackupDir, realBackupsDir)) {
|
|
11892
12441
|
throw new UserError(`refusing rollback: backup id "${backupId}" resolves outside the backups directory`);
|
|
11893
12442
|
}
|
|
@@ -11896,9 +12445,9 @@ function resolveTargetRealPath(path) {
|
|
|
11896
12445
|
let lexicalAncestor = resolvePath15(path);
|
|
11897
12446
|
while (true) {
|
|
11898
12447
|
try {
|
|
11899
|
-
const real =
|
|
11900
|
-
const remainder =
|
|
11901
|
-
return remainder === "" ? real :
|
|
12448
|
+
const real = realpathSync14(lexicalAncestor);
|
|
12449
|
+
const remainder = relative10(lexicalAncestor, resolvePath15(path));
|
|
12450
|
+
return remainder === "" ? real : join41(real, remainder);
|
|
11902
12451
|
} catch {
|
|
11903
12452
|
const parent = dirname15(lexicalAncestor);
|
|
11904
12453
|
if (parent === lexicalAncestor) return resolvePath15(path);
|
|
@@ -11913,7 +12462,7 @@ function assertManifestPathsWithinRoots(manifest, config) {
|
|
|
11913
12462
|
const roots = config.skillsRoots.map((root) => {
|
|
11914
12463
|
const resolved = resolveConfiguredRoot3(root);
|
|
11915
12464
|
try {
|
|
11916
|
-
return
|
|
12465
|
+
return realpathSync14(resolved);
|
|
11917
12466
|
} catch {
|
|
11918
12467
|
return resolved;
|
|
11919
12468
|
}
|
|
@@ -11930,7 +12479,7 @@ function assertNoSymlinkLeaves(manifest) {
|
|
|
11930
12479
|
for (const file of manifest.files) {
|
|
11931
12480
|
let lst;
|
|
11932
12481
|
try {
|
|
11933
|
-
lst =
|
|
12482
|
+
lst = lstatSync8(file.path);
|
|
11934
12483
|
} catch {
|
|
11935
12484
|
lst = void 0;
|
|
11936
12485
|
}
|
|
@@ -11943,11 +12492,11 @@ function findLexicalRoot(filePath, config) {
|
|
|
11943
12492
|
const lexicalTarget = resolvePath15(filePath);
|
|
11944
12493
|
for (const root of config.skillsRoots) {
|
|
11945
12494
|
const rootLexical = resolveConfiguredRoot3(root);
|
|
11946
|
-
const rel =
|
|
11947
|
-
if (rel !== "" && (rel === ".." || rel.startsWith(`..${
|
|
12495
|
+
const rel = relative10(rootLexical, lexicalTarget);
|
|
12496
|
+
if (rel !== "" && (rel === ".." || rel.startsWith(`..${sep10}`) || isAbsolute11(rel))) continue;
|
|
11948
12497
|
let rootReal;
|
|
11949
12498
|
try {
|
|
11950
|
-
rootReal =
|
|
12499
|
+
rootReal = realpathSync14(rootLexical);
|
|
11951
12500
|
} catch {
|
|
11952
12501
|
rootReal = rootLexical;
|
|
11953
12502
|
}
|
|
@@ -11957,8 +12506,8 @@ function findLexicalRoot(filePath, config) {
|
|
|
11957
12506
|
}
|
|
11958
12507
|
function walkRealComponents(filePath, config, createMissing) {
|
|
11959
12508
|
const { rootLexical, rootReal } = findLexicalRoot(filePath, config);
|
|
11960
|
-
const relPath =
|
|
11961
|
-
const components = relPath.split(
|
|
12509
|
+
const relPath = relative10(rootLexical, resolvePath15(filePath));
|
|
12510
|
+
const components = relPath.split(sep10).filter((c) => c.length > 0);
|
|
11962
12511
|
if (components.length === 0) {
|
|
11963
12512
|
throw new UserError(`refusing to operate on ${filePath}: resolves to the skills root itself`);
|
|
11964
12513
|
}
|
|
@@ -11967,10 +12516,10 @@ function walkRealComponents(filePath, config, createMissing) {
|
|
|
11967
12516
|
let current = rootReal;
|
|
11968
12517
|
for (const component of dirComponents) {
|
|
11969
12518
|
if (current === void 0) break;
|
|
11970
|
-
const next =
|
|
12519
|
+
const next = join41(current, component);
|
|
11971
12520
|
let lst;
|
|
11972
12521
|
try {
|
|
11973
|
-
lst =
|
|
12522
|
+
lst = lstatSync8(next);
|
|
11974
12523
|
} catch {
|
|
11975
12524
|
lst = void 0;
|
|
11976
12525
|
}
|
|
@@ -11998,13 +12547,13 @@ function safeRestoreWrite(filePath, contentBuffer, mode, config) {
|
|
|
11998
12547
|
if (parentReal === void 0) {
|
|
11999
12548
|
throw new UserError(`refusing to restore ${filePath}: could not establish a real parent directory`);
|
|
12000
12549
|
}
|
|
12001
|
-
const finalPath =
|
|
12550
|
+
const finalPath = join41(parentReal, leafComponent);
|
|
12002
12551
|
if (!isUnderRoot3(finalPath, rootReal)) {
|
|
12003
12552
|
throw new UserError(`refusing to restore ${filePath}: resolves outside root ${rootReal}`);
|
|
12004
12553
|
}
|
|
12005
12554
|
let leafLst;
|
|
12006
12555
|
try {
|
|
12007
|
-
leafLst =
|
|
12556
|
+
leafLst = lstatSync8(finalPath);
|
|
12008
12557
|
} catch {
|
|
12009
12558
|
leafLst = void 0;
|
|
12010
12559
|
}
|
|
@@ -12024,13 +12573,13 @@ function safeRestoreWrite(filePath, contentBuffer, mode, config) {
|
|
|
12024
12573
|
function safeDeleteTombstone(filePath, config) {
|
|
12025
12574
|
const { parentReal, leafComponent, rootReal } = walkRealComponents(filePath, config, false);
|
|
12026
12575
|
if (parentReal === void 0) return;
|
|
12027
|
-
const finalPath =
|
|
12576
|
+
const finalPath = join41(parentReal, leafComponent);
|
|
12028
12577
|
if (!isUnderRoot3(finalPath, rootReal)) {
|
|
12029
12578
|
throw new UserError(`refusing to delete ${filePath}: resolves outside root ${rootReal}`);
|
|
12030
12579
|
}
|
|
12031
12580
|
let leafLst;
|
|
12032
12581
|
try {
|
|
12033
|
-
leafLst =
|
|
12582
|
+
leafLst = lstatSync8(finalPath);
|
|
12034
12583
|
} catch {
|
|
12035
12584
|
leafLst = void 0;
|
|
12036
12585
|
}
|
|
@@ -12049,6 +12598,13 @@ async function refineRollbackCommand(backupId, options = {}) {
|
|
|
12049
12598
|
assertBackupDirContained(backupId);
|
|
12050
12599
|
const manifest = readBackupManifest(backupId);
|
|
12051
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();
|
|
12052
12608
|
assertManifestPathsWithinRoots(manifest, config);
|
|
12053
12609
|
assertNoSymlinkLeaves(manifest);
|
|
12054
12610
|
const warnings = [];
|
|
@@ -12062,11 +12618,14 @@ async function refineRollbackCommand(backupId, options = {}) {
|
|
|
12062
12618
|
}
|
|
12063
12619
|
warnings.push(`post-write verification data for backup ${backupId} is ${reason} \u2014 restoring without hash verification (--force).`);
|
|
12064
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
|
+
}
|
|
12065
12624
|
const mismatches = [];
|
|
12066
12625
|
for (const file of manifest.files) {
|
|
12067
12626
|
const expected = sidecar.hashes.get(file.path);
|
|
12068
12627
|
if (expected === void 0) continue;
|
|
12069
|
-
const currentSha256 =
|
|
12628
|
+
const currentSha256 = existsSync31(file.path) ? createHash8("sha256").update(readFileSync30(file.path)).digest("hex") : null;
|
|
12070
12629
|
if (currentSha256 !== expected) mismatches.push(file.path);
|
|
12071
12630
|
}
|
|
12072
12631
|
if (mismatches.length > 0 && !options.force) {
|
|
@@ -12093,6 +12652,7 @@ ${fileList}
|
|
|
12093
12652
|
const restored = [];
|
|
12094
12653
|
for (const file of manifest.files) {
|
|
12095
12654
|
try {
|
|
12655
|
+
assertProjectRoots();
|
|
12096
12656
|
if (!file.existed) {
|
|
12097
12657
|
safeDeleteTombstone(file.path, config);
|
|
12098
12658
|
} else {
|
|
@@ -12106,6 +12666,18 @@ ${fileList}
|
|
|
12106
12666
|
);
|
|
12107
12667
|
}
|
|
12108
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
|
+
}
|
|
12109
12681
|
const patternsStore = readPatternsStore();
|
|
12110
12682
|
const pattern = patternsStore.records.find((p) => p.generalizedBackupId === backupId);
|
|
12111
12683
|
if (pattern) {
|
|
@@ -12211,7 +12783,7 @@ configCmd.command("propose <key> <value>").description("Queue a preference for r
|
|
|
12211
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);
|
|
12212
12784
|
program.command("guide [topic]").description("What this tool does, what state you are in, and the next command to run").action(guide);
|
|
12213
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);
|
|
12214
|
-
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);
|
|
12215
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)");
|
|
12216
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);
|
|
12217
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);
|
|
@@ -12222,7 +12794,7 @@ program.command("watch").description("Drift check across every SOURCES.md proven
|
|
|
12222
12794
|
"--skill-map <path>",
|
|
12223
12795
|
"also drift-check every fork-of edge in a generated rhize-plugins skill map (contentHash compare against upstream)"
|
|
12224
12796
|
).action(watchCommand);
|
|
12225
|
-
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);
|
|
12226
12798
|
program.command("ingest").description(
|
|
12227
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."
|
|
12228
12800
|
).option("--list", "print pending queue entries and exit \u2014 no handoff").option("--json", "with --list: print pending entries as JSON").action(ingest);
|
|
@@ -12236,13 +12808,18 @@ insight.command("approve <study-id>").description("Human-only TTY approval of th
|
|
|
12236
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);
|
|
12237
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);
|
|
12238
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);
|
|
12239
12815
|
var refine = program.command("refine").description("Capture, apply, and generalize improvements to installed skills from usage feedback (Pro)");
|
|
12240
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);
|
|
12241
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);
|
|
12242
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);
|
|
12243
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);
|
|
12244
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);
|
|
12245
|
-
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);
|
|
12246
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);
|
|
12247
12824
|
try {
|
|
12248
12825
|
await program.parseAsync();
|