@rhize/skill-forge 0.8.0 → 0.10.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 +7 -0
- package/README.md +252 -7
- package/dist/cli.js +2627 -57
- package/dist/cli.js.map +1 -1
- package/dist/hooks/refinement-detector.sh +115 -0
- package/dist/hooks/session-end.sh +122 -0
- package/dist/ingest-prompt.md +107 -15
- package/dist/refine-prompt.md +367 -0
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -561,7 +561,7 @@ function resolveMcpCandidateDetail(profile) {
|
|
|
561
561
|
}
|
|
562
562
|
|
|
563
563
|
// src/provenance.ts
|
|
564
|
-
import { appendFileSync, existsSync as existsSync6, statSync as statSync3, writeFileSync as writeFileSync3 } from "fs";
|
|
564
|
+
import { appendFileSync, existsSync as existsSync6, readFileSync as readFileSync5, statSync as statSync3, writeFileSync as writeFileSync3 } from "fs";
|
|
565
565
|
import { join as join7 } from "path";
|
|
566
566
|
var FORGE_VERBS = ["DEFER", "ABSORB", "FORK", "REJECT", "WATCH"];
|
|
567
567
|
var LEDGER_HEADER = "# Skill Forge \u2014 Provenance Ledger\n\nOne entry per external-skill ingestion decision.\n";
|
|
@@ -600,6 +600,55 @@ function appendProvenance(entry, skillsRoot) {
|
|
|
600
600
|
appendFileSync(ledgerPath, block, "utf-8");
|
|
601
601
|
return ledgerPath;
|
|
602
602
|
}
|
|
603
|
+
function extractField(block, label) {
|
|
604
|
+
const escaped = label.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
605
|
+
const match = block.match(new RegExp(`\\*\\*${escaped}:\\*\\*\\s*(.*)`));
|
|
606
|
+
return match ? match[1].trim() : void 0;
|
|
607
|
+
}
|
|
608
|
+
function parseProvenanceLedger(text) {
|
|
609
|
+
const entries = [];
|
|
610
|
+
const warnings = [];
|
|
611
|
+
const blocks = text.split(/\n## /);
|
|
612
|
+
for (const block of blocks.slice(1)) {
|
|
613
|
+
const headerLine = block.split("\n", 1)[0] ?? "";
|
|
614
|
+
const [namePart, ...dateParts] = headerLine.split(" \u2014 ");
|
|
615
|
+
const name = namePart?.trim();
|
|
616
|
+
if (!name) {
|
|
617
|
+
warnings.push(`skipped malformed ledger block: missing "## <name> \u2014 <date>" header (saw "${headerLine.slice(0, 80)}")`);
|
|
618
|
+
continue;
|
|
619
|
+
}
|
|
620
|
+
const date = dateParts.join(" \u2014 ").trim();
|
|
621
|
+
const verbRaw = extractField(block, "Verb");
|
|
622
|
+
if (!verbRaw || !FORGE_VERBS.includes(verbRaw)) {
|
|
623
|
+
warnings.push(`skipped malformed ledger block for "${name}": missing or invalid Verb (saw "${verbRaw ?? ""}")`);
|
|
624
|
+
continue;
|
|
625
|
+
}
|
|
626
|
+
const driftCheckMatch = block.match(/\*\*Drift check:\*\*\s*`([^`]*)`/);
|
|
627
|
+
entries.push({
|
|
628
|
+
name,
|
|
629
|
+
date,
|
|
630
|
+
source: extractField(block, "Source") ?? "",
|
|
631
|
+
upstreamRef: extractField(block, "Upstream ref") ?? "",
|
|
632
|
+
license: extractField(block, "License") ?? "",
|
|
633
|
+
verb: verbRaw,
|
|
634
|
+
target: extractField(block, "Target") ?? "",
|
|
635
|
+
took: extractField(block, "Took") ?? "",
|
|
636
|
+
verified: extractField(block, "Verified") ?? "",
|
|
637
|
+
driftCheck: driftCheckMatch ? driftCheckMatch[1] : "",
|
|
638
|
+
notes: extractField(block, "Notes") ?? ""
|
|
639
|
+
});
|
|
640
|
+
}
|
|
641
|
+
return { entries, warnings };
|
|
642
|
+
}
|
|
643
|
+
function readProvenanceLedger(skillsRoot) {
|
|
644
|
+
const ledgerPath = join7(skillsRoot, "SOURCES.md");
|
|
645
|
+
if (!existsSync6(ledgerPath)) {
|
|
646
|
+
return { ledgerPath, exists: false, entries: [], warnings: [] };
|
|
647
|
+
}
|
|
648
|
+
const text = readFileSync5(ledgerPath, "utf-8");
|
|
649
|
+
const parsed = parseProvenanceLedger(text);
|
|
650
|
+
return { ledgerPath, exists: true, ...parsed };
|
|
651
|
+
}
|
|
603
652
|
|
|
604
653
|
// src/commands/gatePipeline.ts
|
|
605
654
|
import { readdirSync as readdirSync5 } from "fs";
|
|
@@ -612,7 +661,7 @@ import {
|
|
|
612
661
|
existsSync as existsSync8,
|
|
613
662
|
mkdtempSync,
|
|
614
663
|
openSync,
|
|
615
|
-
readFileSync as
|
|
664
|
+
readFileSync as readFileSync7,
|
|
616
665
|
readdirSync as readdirSync3,
|
|
617
666
|
readSync,
|
|
618
667
|
rmSync as rmSync2,
|
|
@@ -622,13 +671,13 @@ import { tmpdir } from "os";
|
|
|
622
671
|
import { basename as basename3, dirname as dirname3, extname as extname3, join as join9, resolve as resolve2 } from "path";
|
|
623
672
|
|
|
624
673
|
// src/gate/mcpCapabilities.ts
|
|
625
|
-
import { existsSync as existsSync7, readFileSync as
|
|
674
|
+
import { existsSync as existsSync7, readFileSync as readFileSync6 } from "fs";
|
|
626
675
|
import { extname as extname2, join as join8 } from "path";
|
|
627
676
|
var SOURCE_EXT = /* @__PURE__ */ new Set([".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs"]);
|
|
628
677
|
var MAX_ITEMS_PER_CATEGORY = 200;
|
|
629
678
|
function readJsonSafe(file) {
|
|
630
679
|
try {
|
|
631
|
-
return JSON.parse(
|
|
680
|
+
return JSON.parse(readFileSync6(file, "utf-8"));
|
|
632
681
|
} catch {
|
|
633
682
|
return void 0;
|
|
634
683
|
}
|
|
@@ -856,7 +905,7 @@ function readPackageJsonSafe(root) {
|
|
|
856
905
|
const pkgPath = join9(root, "package.json");
|
|
857
906
|
if (!existsSync8(pkgPath)) return void 0;
|
|
858
907
|
try {
|
|
859
|
-
return JSON.parse(
|
|
908
|
+
return JSON.parse(readFileSync7(pkgPath, "utf-8"));
|
|
860
909
|
} catch {
|
|
861
910
|
return void 0;
|
|
862
911
|
}
|
|
@@ -1016,7 +1065,7 @@ function detectLicense(root, fm) {
|
|
|
1016
1065
|
for (const name of names) {
|
|
1017
1066
|
const p = join9(root, name);
|
|
1018
1067
|
if (existsSync8(p) && statSync4(p).isFile()) {
|
|
1019
|
-
const head =
|
|
1068
|
+
const head = readFileSync7(p, "utf-8").slice(0, 2e3).toLowerCase();
|
|
1020
1069
|
for (const [hint, label] of Object.entries(SPDX_HINTS)) {
|
|
1021
1070
|
if (head.includes(hint)) return label;
|
|
1022
1071
|
}
|
|
@@ -1055,7 +1104,7 @@ function scanMcpAndImports(text, root) {
|
|
|
1055
1104
|
if (isDir(scriptsDir)) {
|
|
1056
1105
|
for (const f of walkFiles(scriptsDir)) {
|
|
1057
1106
|
if (!f.endsWith(".py")) continue;
|
|
1058
|
-
const ft =
|
|
1107
|
+
const ft = readFileSync7(f, "utf-8");
|
|
1059
1108
|
for (const m2 of ft.match(mcpRefRe) ?? []) mcpRefs.add(m2);
|
|
1060
1109
|
const importRe = /^\s*(?:import|from)\s+([a-zA-Z0-9_]+)/gm;
|
|
1061
1110
|
let m;
|
|
@@ -1072,7 +1121,7 @@ function scanMcpAndImports(text, root) {
|
|
|
1072
1121
|
function profileSkillDir(inputPath) {
|
|
1073
1122
|
const { skillMd, root, tmpDir } = resolveSkillMd(inputPath);
|
|
1074
1123
|
try {
|
|
1075
|
-
const text =
|
|
1124
|
+
const text = readFileSync7(skillMd, "utf-8");
|
|
1076
1125
|
const fm = parseFrontmatter(text);
|
|
1077
1126
|
const bodyLines = (text.match(/\n/g)?.length ?? 0) + 1;
|
|
1078
1127
|
const headers = Array.from(text.matchAll(/^#{1,3}\s+(.+)$/gm)).map((m) => m[1]);
|
|
@@ -1103,7 +1152,7 @@ function profilePlugin(p) {
|
|
|
1103
1152
|
const manifestPath = join9(p, ".claude-plugin", "plugin.json");
|
|
1104
1153
|
let manifest;
|
|
1105
1154
|
try {
|
|
1106
|
-
manifest = JSON.parse(
|
|
1155
|
+
manifest = JSON.parse(readFileSync7(manifestPath, "utf-8"));
|
|
1107
1156
|
} catch (err) {
|
|
1108
1157
|
throw new Error(`could not parse plugin.json under ${p}: ${err.message}`);
|
|
1109
1158
|
}
|
|
@@ -1134,7 +1183,7 @@ function profileMcp(p) {
|
|
|
1134
1183
|
if (explicitConfig) {
|
|
1135
1184
|
let cfg;
|
|
1136
1185
|
try {
|
|
1137
|
-
cfg = JSON.parse(
|
|
1186
|
+
cfg = JSON.parse(readFileSync7(explicitConfig, "utf-8"));
|
|
1138
1187
|
} catch (err) {
|
|
1139
1188
|
throw new Error(`could not parse MCP config ${explicitConfig}: ${err.message}`);
|
|
1140
1189
|
}
|
|
@@ -1153,7 +1202,7 @@ function profileMcp(p) {
|
|
|
1153
1202
|
const pkgPath = join9(p, "package.json");
|
|
1154
1203
|
let pkg2;
|
|
1155
1204
|
try {
|
|
1156
|
-
pkg2 = JSON.parse(
|
|
1205
|
+
pkg2 = JSON.parse(readFileSync7(pkgPath, "utf-8"));
|
|
1157
1206
|
} catch (err) {
|
|
1158
1207
|
throw new Error(`could not parse package.json ${pkgPath}: ${err.message}`);
|
|
1159
1208
|
}
|
|
@@ -1311,7 +1360,7 @@ async function scanSafety(targetPath, strictness = DEFAULT_STRICTNESS) {
|
|
|
1311
1360
|
}
|
|
1312
1361
|
|
|
1313
1362
|
// src/gate/overlap.ts
|
|
1314
|
-
import { existsSync as existsSync9, readFileSync as
|
|
1363
|
+
import { existsSync as existsSync9, readFileSync as readFileSync8, readdirSync as readdirSync4 } from "fs";
|
|
1315
1364
|
import { basename as basename4, dirname as dirname4, join as join11, resolve as resolve3 } from "path";
|
|
1316
1365
|
var STOPWORDS = new Set(
|
|
1317
1366
|
`
|
|
@@ -1321,20 +1370,20 @@ skill skills user claude code based comprehensive enhanced practical including i
|
|
|
1321
1370
|
`.split(/\s+/).filter(Boolean)
|
|
1322
1371
|
);
|
|
1323
1372
|
var DEFAULT_SET_THRESHOLD = 0.45;
|
|
1324
|
-
function splitLimited(text,
|
|
1373
|
+
function splitLimited(text, sep3, maxSplit) {
|
|
1325
1374
|
const parts = [];
|
|
1326
1375
|
let rest = text;
|
|
1327
1376
|
for (let i = 0; i < maxSplit; i++) {
|
|
1328
|
-
const idx = rest.indexOf(
|
|
1377
|
+
const idx = rest.indexOf(sep3);
|
|
1329
1378
|
if (idx === -1) break;
|
|
1330
1379
|
parts.push(rest.slice(0, idx));
|
|
1331
|
-
rest = rest.slice(idx +
|
|
1380
|
+
rest = rest.slice(idx + sep3.length);
|
|
1332
1381
|
}
|
|
1333
1382
|
parts.push(rest);
|
|
1334
1383
|
return parts;
|
|
1335
1384
|
}
|
|
1336
1385
|
function readFrontmatter(skillMdPath) {
|
|
1337
|
-
const text =
|
|
1386
|
+
const text = readFileSync8(skillMdPath, "utf-8");
|
|
1338
1387
|
const meta = { name: basename4(dirname4(skillMdPath)), description: "" };
|
|
1339
1388
|
if (text.startsWith("---\n")) {
|
|
1340
1389
|
const block = splitLimited(text, "---", 2)[1] ?? "";
|
|
@@ -1705,14 +1754,14 @@ async function mcpScan(targetPath, strictness = DEFAULT_STRICTNESS) {
|
|
|
1705
1754
|
}
|
|
1706
1755
|
|
|
1707
1756
|
// src/gate/mcpOverlap.ts
|
|
1708
|
-
import { existsSync as existsSync10, readFileSync as
|
|
1757
|
+
import { existsSync as existsSync10, readFileSync as readFileSync9 } from "fs";
|
|
1709
1758
|
import { resolve as resolve4 } from "path";
|
|
1710
1759
|
function readTargetServers(targetPath) {
|
|
1711
1760
|
const path = resolve4(expandHome(targetPath));
|
|
1712
1761
|
if (!existsSync10(path)) return [];
|
|
1713
1762
|
let cfg;
|
|
1714
1763
|
try {
|
|
1715
|
-
cfg = JSON.parse(
|
|
1764
|
+
cfg = JSON.parse(readFileSync9(path, "utf-8"));
|
|
1716
1765
|
} catch {
|
|
1717
1766
|
return [];
|
|
1718
1767
|
}
|
|
@@ -2020,7 +2069,7 @@ async function runGate(candidatePath, skillsRoot, {
|
|
|
2020
2069
|
|
|
2021
2070
|
// src/queue.ts
|
|
2022
2071
|
import { randomUUID as randomUUID2 } from "crypto";
|
|
2023
|
-
import { existsSync as existsSync11, mkdirSync as mkdirSync5, readFileSync as
|
|
2072
|
+
import { existsSync as existsSync11, mkdirSync as mkdirSync5, readFileSync as readFileSync10, renameSync as renameSync2, rmSync as rmSync3, writeFileSync as writeFileSync4 } from "fs";
|
|
2024
2073
|
import { join as join13 } from "path";
|
|
2025
2074
|
function getQueuePath() {
|
|
2026
2075
|
return join13(getHomeDir(), "queue.json");
|
|
@@ -2030,26 +2079,46 @@ function readQueue() {
|
|
|
2030
2079
|
if (!existsSync11(path)) {
|
|
2031
2080
|
return { version: 1, entries: [] };
|
|
2032
2081
|
}
|
|
2033
|
-
const raw =
|
|
2082
|
+
const raw = readFileSync10(path, "utf-8");
|
|
2034
2083
|
const parsed = JSON.parse(raw);
|
|
2035
2084
|
return { version: 1, entries: parsed.entries ?? [] };
|
|
2036
2085
|
}
|
|
2037
|
-
function writeQueue(
|
|
2038
|
-
|
|
2039
|
-
|
|
2086
|
+
function writeQueue(queue2) {
|
|
2087
|
+
const home = getHomeDir();
|
|
2088
|
+
mkdirSync5(home, { recursive: true });
|
|
2089
|
+
const finalPath = getQueuePath();
|
|
2090
|
+
const tempPath = join13(home, `.queue.json.tmp-${randomUUID2()}`);
|
|
2091
|
+
try {
|
|
2092
|
+
writeFileSync4(tempPath, JSON.stringify(queue2, null, 2) + "\n", "utf-8");
|
|
2093
|
+
renameSync2(tempPath, finalPath);
|
|
2094
|
+
} catch (err) {
|
|
2095
|
+
rmSync3(tempPath, { force: true });
|
|
2096
|
+
throw err;
|
|
2097
|
+
}
|
|
2040
2098
|
}
|
|
2041
2099
|
function appendQueueEntry(entry) {
|
|
2042
|
-
const
|
|
2043
|
-
const existingIds = new Set(
|
|
2100
|
+
const queue2 = readQueue();
|
|
2101
|
+
const existingIds = new Set(queue2.entries.map((e) => e.id));
|
|
2044
2102
|
const full = {
|
|
2045
2103
|
...entry,
|
|
2046
2104
|
id: generateId(existingIds),
|
|
2047
2105
|
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
2048
2106
|
};
|
|
2049
|
-
|
|
2050
|
-
writeQueue(
|
|
2107
|
+
queue2.entries.push(full);
|
|
2108
|
+
writeQueue(queue2);
|
|
2051
2109
|
return full;
|
|
2052
2110
|
}
|
|
2111
|
+
function updateQueueEntry(id, patch) {
|
|
2112
|
+
const queue2 = readQueue();
|
|
2113
|
+
const index = queue2.entries.findIndex((e) => e.id === id);
|
|
2114
|
+
if (index === -1) {
|
|
2115
|
+
throw new Error(`no queue entry with id: ${id}`);
|
|
2116
|
+
}
|
|
2117
|
+
const updated = { ...queue2.entries[index], ...patch };
|
|
2118
|
+
queue2.entries[index] = updated;
|
|
2119
|
+
writeQueue(queue2);
|
|
2120
|
+
return updated;
|
|
2121
|
+
}
|
|
2053
2122
|
function generateId(existingIds) {
|
|
2054
2123
|
let id;
|
|
2055
2124
|
do {
|
|
@@ -2376,15 +2445,15 @@ import {
|
|
|
2376
2445
|
existsSync as existsSync13,
|
|
2377
2446
|
lstatSync,
|
|
2378
2447
|
mkdtempSync as mkdtempSync2,
|
|
2379
|
-
readFileSync as
|
|
2448
|
+
readFileSync as readFileSync11,
|
|
2380
2449
|
readdirSync as readdirSync6,
|
|
2381
2450
|
realpathSync,
|
|
2382
|
-
renameSync as
|
|
2383
|
-
rmSync as
|
|
2451
|
+
renameSync as renameSync3,
|
|
2452
|
+
rmSync as rmSync4,
|
|
2384
2453
|
statSync as statSync8,
|
|
2385
2454
|
writeFileSync as writeFileSync5
|
|
2386
2455
|
} from "fs";
|
|
2387
|
-
import { basename as basename6, extname as extname5, join as join16, resolve as resolvePath2 } from "path";
|
|
2456
|
+
import { basename as basename6, delimiter as delimiter2, extname as extname5, join as join16, resolve as resolvePath2 } from "path";
|
|
2388
2457
|
var OVERSIZED_SKILL_EST_TOKENS = 5e3;
|
|
2389
2458
|
function toAuditFindings(source) {
|
|
2390
2459
|
return source.map((f) => ({ severity: f.severity, rule: f.rule, target: f.file, detail: f.detail }));
|
|
@@ -2545,7 +2614,7 @@ function profileAuditedSkill(group, findings, unreadableSkillMdPaths) {
|
|
|
2545
2614
|
let sizeBytes;
|
|
2546
2615
|
try {
|
|
2547
2616
|
sizeBytes = statSync8(skillMdPath).size;
|
|
2548
|
-
text =
|
|
2617
|
+
text = readFileSync11(skillMdPath, "utf-8");
|
|
2549
2618
|
} catch (err) {
|
|
2550
2619
|
findings.push({
|
|
2551
2620
|
severity: "MEDIUM",
|
|
@@ -2623,7 +2692,7 @@ function auditMcpTarget(targetPath, notices) {
|
|
|
2623
2692
|
}
|
|
2624
2693
|
if (isToml) {
|
|
2625
2694
|
try {
|
|
2626
|
-
|
|
2695
|
+
readFileSync11(targetPath, "utf-8");
|
|
2627
2696
|
} catch (err) {
|
|
2628
2697
|
findings.push({
|
|
2629
2698
|
severity: "MEDIUM",
|
|
@@ -2638,7 +2707,7 @@ function auditMcpTarget(targetPath, notices) {
|
|
|
2638
2707
|
}
|
|
2639
2708
|
let raw;
|
|
2640
2709
|
try {
|
|
2641
|
-
raw =
|
|
2710
|
+
raw = readFileSync11(targetPath, "utf-8");
|
|
2642
2711
|
} catch (err) {
|
|
2643
2712
|
findings.push({
|
|
2644
2713
|
severity: "MEDIUM",
|
|
@@ -2672,7 +2741,7 @@ function auditMcpTarget(targetPath, notices) {
|
|
|
2672
2741
|
}
|
|
2673
2742
|
function clusterOverlapPairs(pairs) {
|
|
2674
2743
|
const parent = /* @__PURE__ */ new Map();
|
|
2675
|
-
function
|
|
2744
|
+
function find2(x) {
|
|
2676
2745
|
if (!parent.has(x)) parent.set(x, x);
|
|
2677
2746
|
let root = x;
|
|
2678
2747
|
while (parent.get(root) !== root) root = parent.get(root);
|
|
@@ -2685,14 +2754,14 @@ function clusterOverlapPairs(pairs) {
|
|
|
2685
2754
|
return root;
|
|
2686
2755
|
}
|
|
2687
2756
|
function union(a, b) {
|
|
2688
|
-
const ra =
|
|
2689
|
-
const rb =
|
|
2757
|
+
const ra = find2(a);
|
|
2758
|
+
const rb = find2(b);
|
|
2690
2759
|
if (ra !== rb) parent.set(ra, rb);
|
|
2691
2760
|
}
|
|
2692
2761
|
for (const pair of pairs) union(pair.aPath, pair.bPath);
|
|
2693
2762
|
const groups = /* @__PURE__ */ new Map();
|
|
2694
2763
|
for (const pair of pairs) {
|
|
2695
|
-
const root =
|
|
2764
|
+
const root = find2(pair.aPath);
|
|
2696
2765
|
const group = groups.get(root) ?? { members: /* @__PURE__ */ new Set(), topScore: 0 };
|
|
2697
2766
|
group.members.add(pair.aPath);
|
|
2698
2767
|
group.members.add(pair.bPath);
|
|
@@ -2708,6 +2777,45 @@ function toClusterMembers(members) {
|
|
|
2708
2777
|
}
|
|
2709
2778
|
return sorted;
|
|
2710
2779
|
}
|
|
2780
|
+
function defaultWhich(cmd) {
|
|
2781
|
+
const pathEnv = process.env.PATH ?? "";
|
|
2782
|
+
const dirs = pathEnv.split(delimiter2).filter(Boolean);
|
|
2783
|
+
const exts = process.platform === "win32" ? (process.env.PATHEXT ?? ".EXE;.CMD;.BAT").split(";") : [""];
|
|
2784
|
+
for (const dir of dirs) {
|
|
2785
|
+
for (const ext of exts) {
|
|
2786
|
+
const candidate = join16(dir, cmd + ext);
|
|
2787
|
+
try {
|
|
2788
|
+
if (statSync8(candidate).isFile()) return candidate;
|
|
2789
|
+
} catch {
|
|
2790
|
+
}
|
|
2791
|
+
}
|
|
2792
|
+
}
|
|
2793
|
+
return void 0;
|
|
2794
|
+
}
|
|
2795
|
+
var SKILLSPECTOR_PROVIDER_KEYMAP = {
|
|
2796
|
+
openai: "OPENAI_API_KEY",
|
|
2797
|
+
anthropic: "ANTHROPIC_API_KEY",
|
|
2798
|
+
nv_build: "NVIDIA_INFERENCE_KEY"
|
|
2799
|
+
};
|
|
2800
|
+
function checkTooling(which2, env) {
|
|
2801
|
+
const skillspectorPath = which2("skillspector");
|
|
2802
|
+
const skillspector = skillspectorPath ? { installed: true } : {
|
|
2803
|
+
installed: false,
|
|
2804
|
+
hint: "NOT installed \u2014 github.com/NVIDIA/skillspector (git clone + make install; Python 3.12+)"
|
|
2805
|
+
};
|
|
2806
|
+
const tokenPresent = Boolean(env.VERCEL_OIDC_TOKEN);
|
|
2807
|
+
const skillsShToken = tokenPresent ? { present: true } : {
|
|
2808
|
+
present: false,
|
|
2809
|
+
hint: "missing \u2014 enable OIDC Federation on your Vercel project, then `vercel link && vercel env pull`"
|
|
2810
|
+
};
|
|
2811
|
+
const provider = env.SKILLSPECTOR_PROVIDER;
|
|
2812
|
+
const providerKeyEnv = provider ? SKILLSPECTOR_PROVIDER_KEYMAP[provider] : void 0;
|
|
2813
|
+
const skillspectorLlm = {
|
|
2814
|
+
configured: Boolean(provider && providerKeyEnv && env[providerKeyEnv]),
|
|
2815
|
+
provider: provider || void 0
|
|
2816
|
+
};
|
|
2817
|
+
return { skillspector, skillsShToken, skillspectorLlm };
|
|
2818
|
+
}
|
|
2711
2819
|
async function auditConfiguredSet(config, options = {}) {
|
|
2712
2820
|
const now = options.now ?? (() => /* @__PURE__ */ new Date());
|
|
2713
2821
|
const oversizedThreshold = options.oversizedSkillTokens ?? OVERSIZED_SKILL_EST_TOKENS;
|
|
@@ -2828,12 +2936,14 @@ async function auditConfiguredSet(config, options = {}) {
|
|
|
2828
2936
|
overlapClusterCount: overlapClusters.length,
|
|
2829
2937
|
evolveEligibleCount: evolveEligible.length
|
|
2830
2938
|
};
|
|
2939
|
+
const tooling = checkTooling(options.which ?? defaultWhich, options.env ?? process.env);
|
|
2831
2940
|
return {
|
|
2832
2941
|
schemaVersion: 1,
|
|
2833
2942
|
generatedAt: now().toISOString(),
|
|
2834
2943
|
inventory: { skillRoots, skills, mcpTargets },
|
|
2835
2944
|
hygiene: findings,
|
|
2836
2945
|
opportunities: { overlapClusters, evolveEligible, foundation, overlapLocked: !overlapStatus.enabled },
|
|
2946
|
+
tooling,
|
|
2837
2947
|
notices,
|
|
2838
2948
|
summary
|
|
2839
2949
|
};
|
|
@@ -2927,6 +3037,28 @@ function renderOpportunitiesSection(report) {
|
|
|
2927
3037
|
`- ${foundation.message}`
|
|
2928
3038
|
];
|
|
2929
3039
|
}
|
|
3040
|
+
function renderToolingSection(tooling) {
|
|
3041
|
+
const lines = ["## Tooling", ""];
|
|
3042
|
+
if (!tooling) {
|
|
3043
|
+
lines.push("- (not available in this report)");
|
|
3044
|
+
return lines;
|
|
3045
|
+
}
|
|
3046
|
+
lines.push(`- SkillSpector CLI: ${tooling.skillspector.installed ? "installed" : "NOT installed"}`);
|
|
3047
|
+
if (tooling.skillspector.hint) lines.push(` - ${tooling.skillspector.hint}`);
|
|
3048
|
+
lines.push(`- skills.sh auth (VERCEL_OIDC_TOKEN): ${tooling.skillsShToken.present ? "present" : "missing"}`);
|
|
3049
|
+
if (tooling.skillsShToken.hint) lines.push(` - ${tooling.skillsShToken.hint}`);
|
|
3050
|
+
const llm = tooling.skillspectorLlm;
|
|
3051
|
+
if (llm.configured) {
|
|
3052
|
+
lines.push(`- SkillSpector LLM stage (optional): configured (provider: ${sanitizeForMarkdown(llm.provider ?? "")})`);
|
|
3053
|
+
} else if (llm.provider) {
|
|
3054
|
+
lines.push(`- SkillSpector LLM stage (optional): not configured \u2014 SKILLSPECTOR_PROVIDER is "${sanitizeForMarkdown(llm.provider)}" but its API key env var is not set`);
|
|
3055
|
+
} else {
|
|
3056
|
+
lines.push(
|
|
3057
|
+
"- SkillSpector LLM stage (optional): not set \u2014 static scan works without it (use --no-llm); set SKILLSPECTOR_PROVIDER + its API key for semantic analysis"
|
|
3058
|
+
);
|
|
3059
|
+
}
|
|
3060
|
+
return lines;
|
|
3061
|
+
}
|
|
2930
3062
|
function renderAuditMarkdown(report) {
|
|
2931
3063
|
const { summary } = report;
|
|
2932
3064
|
const lines = [
|
|
@@ -2953,6 +3085,8 @@ function renderAuditMarkdown(report) {
|
|
|
2953
3085
|
"",
|
|
2954
3086
|
...renderOpportunitiesSection(report),
|
|
2955
3087
|
"",
|
|
3088
|
+
...renderToolingSection(report.tooling),
|
|
3089
|
+
"",
|
|
2956
3090
|
"## Notices",
|
|
2957
3091
|
"",
|
|
2958
3092
|
...bulletList(report.notices, "(none)")
|
|
@@ -3024,9 +3158,9 @@ async function scaffoldFoundationSkill(config, profile, targetRoot) {
|
|
|
3024
3158
|
`refusing to scaffold a foundation skill: the generated SKILL.md was flagged by the safety scan (${safety.findings.map(formatSafetyFinding).join("; ")})`
|
|
3025
3159
|
);
|
|
3026
3160
|
}
|
|
3027
|
-
|
|
3161
|
+
renameSync3(tempDir, destDir);
|
|
3028
3162
|
} catch (err) {
|
|
3029
|
-
|
|
3163
|
+
rmSync4(tempDir, { recursive: true, force: true });
|
|
3030
3164
|
throw err;
|
|
3031
3165
|
}
|
|
3032
3166
|
try {
|
|
@@ -3664,7 +3798,7 @@ async function status() {
|
|
|
3664
3798
|
// src/evolve.ts
|
|
3665
3799
|
import { createInterface as createInterface4 } from "readline/promises";
|
|
3666
3800
|
import { createHash } from "crypto";
|
|
3667
|
-
import { cpSync as cpSync3, existsSync as existsSync16, lstatSync as lstatSync2, mkdtempSync as mkdtempSync3, readFileSync as
|
|
3801
|
+
import { cpSync as cpSync3, existsSync as existsSync16, lstatSync as lstatSync2, mkdtempSync as mkdtempSync3, readFileSync as readFileSync12, rmSync as rmSync5 } from "fs";
|
|
3668
3802
|
import { tmpdir as tmpdir2 } from "os";
|
|
3669
3803
|
import { basename as basename7, dirname as dirname7, join as join20, resolve as resolvePath4, sep } from "path";
|
|
3670
3804
|
var SKILLOPT_BIN = "skillopt-sleep";
|
|
@@ -3765,7 +3899,7 @@ async function evolve(targetSkillDir, options = {}, deps = {}) {
|
|
|
3765
3899
|
safety = await scanSafety(tempDir, strictness);
|
|
3766
3900
|
} finally {
|
|
3767
3901
|
try {
|
|
3768
|
-
|
|
3902
|
+
rmSync5(tempDir, { recursive: true, force: true });
|
|
3769
3903
|
} catch (err) {
|
|
3770
3904
|
console.error(`[skill-forge] failed to clean up temp re-gate dir ${tempDir}: ${err.message}`);
|
|
3771
3905
|
}
|
|
@@ -3847,7 +3981,7 @@ async function evolve(targetSkillDir, options = {}, deps = {}) {
|
|
|
3847
3981
|
logOutcome("[skill-forge] queued for ingestion review", json);
|
|
3848
3982
|
} else if (decision === "reject") {
|
|
3849
3983
|
try {
|
|
3850
|
-
|
|
3984
|
+
rmSync5(stagingDir, { recursive: true, force: true });
|
|
3851
3985
|
} catch (err) {
|
|
3852
3986
|
throw new UserError(
|
|
3853
3987
|
`rejected the proposal, but removing the staging dir ${stagingDir} failed: ${err.message} \u2014 remove it manually.`
|
|
@@ -3902,7 +4036,7 @@ function readManifest(stagingDir) {
|
|
|
3902
4036
|
const manifestPath = join20(stagingDir, "manifest.json");
|
|
3903
4037
|
let raw;
|
|
3904
4038
|
try {
|
|
3905
|
-
raw =
|
|
4039
|
+
raw = readFileSync12(manifestPath, "utf-8");
|
|
3906
4040
|
} catch (err) {
|
|
3907
4041
|
throw new UserError(`could not read staging manifest ${manifestPath}: ${err.message}`);
|
|
3908
4042
|
}
|
|
@@ -3937,7 +4071,7 @@ function validateManifest(raw, manifestPath) {
|
|
|
3937
4071
|
}
|
|
3938
4072
|
function filesIdentical(a, b) {
|
|
3939
4073
|
if (!existsSync16(a) || !existsSync16(b)) return false;
|
|
3940
|
-
return
|
|
4074
|
+
return readFileSync12(a).equals(readFileSync12(b));
|
|
3941
4075
|
}
|
|
3942
4076
|
function isAlreadyAdopted(stagingDir, manifest) {
|
|
3943
4077
|
const proposedSkillMd = join20(stagingDir, "proposed_SKILL.md");
|
|
@@ -3966,7 +4100,7 @@ function assertNotSymlink(path, label) {
|
|
|
3966
4100
|
}
|
|
3967
4101
|
function sha256File(path) {
|
|
3968
4102
|
if (!existsSync16(path)) return null;
|
|
3969
|
-
return createHash("sha256").update(
|
|
4103
|
+
return createHash("sha256").update(readFileSync12(path)).digest("hex");
|
|
3970
4104
|
}
|
|
3971
4105
|
|
|
3972
4106
|
// src/commands/evolve.ts
|
|
@@ -3974,19 +4108,2440 @@ async function evolveCommand(targetSkillDir, options = {}) {
|
|
|
3974
4108
|
await evolve(targetSkillDir, options);
|
|
3975
4109
|
}
|
|
3976
4110
|
|
|
3977
|
-
// src/
|
|
3978
|
-
|
|
3979
|
-
|
|
3980
|
-
|
|
3981
|
-
|
|
3982
|
-
|
|
4111
|
+
// src/commands/organize.ts
|
|
4112
|
+
import { mkdirSync as mkdirSync7, writeFileSync as writeFileSync7 } from "fs";
|
|
4113
|
+
import { dirname as dirname8, resolve as resolvePath6 } from "path";
|
|
4114
|
+
|
|
4115
|
+
// src/organize.ts
|
|
4116
|
+
import { existsSync as existsSync17, readFileSync as readFileSync13, readdirSync as readdirSync7, realpathSync as realpathSync2, statSync as statSync9 } from "fs";
|
|
4117
|
+
import { basename as basename8, join as join21, resolve as resolvePath5 } from "path";
|
|
4118
|
+
var PRO_FEATURE_LABEL3 = "set-level organizer reports";
|
|
4119
|
+
var TIERS = /* @__PURE__ */ new Set(["resource", "custom"]);
|
|
4120
|
+
var MATURITIES = /* @__PURE__ */ new Set(["seedling", "stable", "deprecated"]);
|
|
4121
|
+
function listSubdirs2(dir) {
|
|
4122
|
+
let entries;
|
|
4123
|
+
try {
|
|
4124
|
+
entries = readdirSync7(dir, { withFileTypes: true });
|
|
4125
|
+
} catch {
|
|
4126
|
+
return [];
|
|
3983
4127
|
}
|
|
3984
|
-
|
|
4128
|
+
const out = [];
|
|
4129
|
+
for (const entry of entries) {
|
|
4130
|
+
const full = join21(dir, entry.name);
|
|
4131
|
+
if (entry.isDirectory()) {
|
|
4132
|
+
out.push(full);
|
|
4133
|
+
continue;
|
|
4134
|
+
}
|
|
4135
|
+
if (entry.isSymbolicLink()) {
|
|
4136
|
+
try {
|
|
4137
|
+
if (statSync9(full).isDirectory()) out.push(full);
|
|
4138
|
+
} catch {
|
|
4139
|
+
}
|
|
4140
|
+
}
|
|
4141
|
+
}
|
|
4142
|
+
return out;
|
|
3985
4143
|
}
|
|
3986
|
-
function
|
|
3987
|
-
const
|
|
3988
|
-
|
|
3989
|
-
|
|
4144
|
+
function findSkillDirsUnderRoot2(root) {
|
|
4145
|
+
const found = /* @__PURE__ */ new Set();
|
|
4146
|
+
const topDirs = listSubdirs2(root);
|
|
4147
|
+
for (const dir of topDirs) {
|
|
4148
|
+
if (existsSync17(join21(dir, "SKILL.md"))) found.add(dir);
|
|
4149
|
+
}
|
|
4150
|
+
for (const dir of listSubdirs2(join21(root, "skills"))) {
|
|
4151
|
+
if (existsSync17(join21(dir, "SKILL.md"))) found.add(dir);
|
|
4152
|
+
}
|
|
4153
|
+
for (const topDir of topDirs) {
|
|
4154
|
+
for (const dir of listSubdirs2(join21(topDir, "skills"))) {
|
|
4155
|
+
if (existsSync17(join21(dir, "SKILL.md"))) found.add(dir);
|
|
4156
|
+
}
|
|
4157
|
+
}
|
|
4158
|
+
return Array.from(found).filter((p) => !p.includes("/.git/"));
|
|
4159
|
+
}
|
|
4160
|
+
function collectCandidates(roots) {
|
|
4161
|
+
const out = [];
|
|
4162
|
+
for (const root of roots) {
|
|
4163
|
+
const resolvedRoot = resolvePath5(expandHome(root));
|
|
4164
|
+
if (!isDir(resolvedRoot)) continue;
|
|
4165
|
+
for (const dirPath of findSkillDirsUnderRoot2(resolvedRoot)) {
|
|
4166
|
+
out.push({ dirPath, root });
|
|
4167
|
+
}
|
|
4168
|
+
}
|
|
4169
|
+
return out;
|
|
4170
|
+
}
|
|
4171
|
+
function parseYamlList(block, key) {
|
|
4172
|
+
const inlineMatch = block.match(new RegExp(`^${key}:\\s*\\[(.*?)\\]\\s*$`, "m"));
|
|
4173
|
+
if (inlineMatch) {
|
|
4174
|
+
return inlineMatch[1].split(",").map((x) => stripQuotes(x.trim())).filter(Boolean);
|
|
4175
|
+
}
|
|
4176
|
+
if (!new RegExp(`^${key}:\\s*$`, "m").test(block)) return [];
|
|
4177
|
+
const lines = block.split(/\r\n|\r|\n/);
|
|
4178
|
+
const startIdx = lines.findIndex((l) => new RegExp(`^${key}:\\s*$`).test(l));
|
|
4179
|
+
if (startIdx === -1) return [];
|
|
4180
|
+
const out = [];
|
|
4181
|
+
for (const line of lines.slice(startIdx + 1)) {
|
|
4182
|
+
const itemMatch = line.match(/^\s*-\s*(.+)$/);
|
|
4183
|
+
if (itemMatch) {
|
|
4184
|
+
out.push(stripQuotes(itemMatch[1].trim()));
|
|
4185
|
+
} else if (line.trim() === "") {
|
|
4186
|
+
continue;
|
|
4187
|
+
} else {
|
|
4188
|
+
break;
|
|
4189
|
+
}
|
|
4190
|
+
}
|
|
4191
|
+
return out;
|
|
4192
|
+
}
|
|
4193
|
+
function extractFrontmatterBlock(text) {
|
|
4194
|
+
if (!text.startsWith("---\n")) return null;
|
|
4195
|
+
const afterOpen = text.slice(4);
|
|
4196
|
+
const closeIdx = afterOpen.indexOf("\n---");
|
|
4197
|
+
if (closeIdx === -1) return null;
|
|
4198
|
+
return afterOpen.slice(0, closeIdx);
|
|
4199
|
+
}
|
|
4200
|
+
function parseCapabilityFrontmatter(text, fallbackName) {
|
|
4201
|
+
const block = extractFrontmatterBlock(text);
|
|
4202
|
+
if (block === null) {
|
|
4203
|
+
return { name: fallbackName, consumes: [], validFrontmatter: false };
|
|
4204
|
+
}
|
|
4205
|
+
const fields = {};
|
|
4206
|
+
for (const key of ["name", "tier", "domain", "provenance", "maturity"]) {
|
|
4207
|
+
const m = block.match(new RegExp(`^${key}:\\s*(.+)$`, "m"));
|
|
4208
|
+
if (m) fields[key] = stripQuotes(m[1].trim());
|
|
4209
|
+
}
|
|
4210
|
+
return {
|
|
4211
|
+
name: fields.name ?? fallbackName,
|
|
4212
|
+
tier: fields.tier,
|
|
4213
|
+
domain: fields.domain,
|
|
4214
|
+
provenance: fields.provenance,
|
|
4215
|
+
maturity: fields.maturity,
|
|
4216
|
+
consumes: parseYamlList(block, "consumes"),
|
|
4217
|
+
validFrontmatter: true
|
|
4218
|
+
};
|
|
4219
|
+
}
|
|
4220
|
+
function loadUsageSnapshot(snapshotPath) {
|
|
4221
|
+
const resolvedPath = resolvePath5(expandHome(snapshotPath));
|
|
4222
|
+
let raw;
|
|
4223
|
+
try {
|
|
4224
|
+
raw = readFileSync13(resolvedPath, "utf-8");
|
|
4225
|
+
} catch (err) {
|
|
4226
|
+
return { usage: /* @__PURE__ */ new Map(), warning: `could not read usage snapshot (${err.message}); proceeding without usage` };
|
|
4227
|
+
}
|
|
4228
|
+
let parsed;
|
|
4229
|
+
try {
|
|
4230
|
+
parsed = JSON.parse(raw);
|
|
4231
|
+
} catch (err) {
|
|
4232
|
+
return { usage: /* @__PURE__ */ new Map(), warning: `could not parse usage snapshot (${err.message}); proceeding without usage` };
|
|
4233
|
+
}
|
|
4234
|
+
const usage = /* @__PURE__ */ new Map();
|
|
4235
|
+
const outer = parsed;
|
|
4236
|
+
const report = outer && typeof outer === "object" ? outer.report : void 0;
|
|
4237
|
+
const topSkills = report && typeof report === "object" ? report.top_skills : void 0;
|
|
4238
|
+
if (Array.isArray(topSkills)) {
|
|
4239
|
+
for (const row of topSkills) {
|
|
4240
|
+
if (Array.isArray(row) && row.length === 2 && typeof row[0] === "string" && typeof row[1] === "number") {
|
|
4241
|
+
const [name, count] = row;
|
|
4242
|
+
usage.set(name, count);
|
|
4243
|
+
if (name.includes(":")) {
|
|
4244
|
+
const bare = name.split(":").pop();
|
|
4245
|
+
if (!usage.has(bare)) usage.set(bare, count);
|
|
4246
|
+
}
|
|
4247
|
+
}
|
|
4248
|
+
}
|
|
4249
|
+
}
|
|
4250
|
+
return { usage };
|
|
4251
|
+
}
|
|
4252
|
+
var WHITE = 0;
|
|
4253
|
+
var GRAY = 1;
|
|
4254
|
+
var BLACK = 2;
|
|
4255
|
+
function findCycles(adjacency) {
|
|
4256
|
+
const color = /* @__PURE__ */ new Map();
|
|
4257
|
+
for (const id of adjacency.keys()) color.set(id, WHITE);
|
|
4258
|
+
const stack = [];
|
|
4259
|
+
const cycles = [];
|
|
4260
|
+
function dfs(u) {
|
|
4261
|
+
color.set(u, GRAY);
|
|
4262
|
+
stack.push(u);
|
|
4263
|
+
for (const v of adjacency.get(u) ?? []) {
|
|
4264
|
+
if (!color.has(v)) continue;
|
|
4265
|
+
if (color.get(v) === GRAY && stack.includes(v)) {
|
|
4266
|
+
const idx = stack.indexOf(v);
|
|
4267
|
+
cycles.push([...stack.slice(idx), v]);
|
|
4268
|
+
} else if (color.get(v) === WHITE) {
|
|
4269
|
+
dfs(v);
|
|
4270
|
+
}
|
|
4271
|
+
}
|
|
4272
|
+
color.set(u, BLACK);
|
|
4273
|
+
stack.pop();
|
|
4274
|
+
}
|
|
4275
|
+
for (const id of adjacency.keys()) {
|
|
4276
|
+
if (color.get(id) === WHITE) dfs(id);
|
|
4277
|
+
}
|
|
4278
|
+
return cycles;
|
|
4279
|
+
}
|
|
4280
|
+
function buildGraph(entries, nameToIds) {
|
|
4281
|
+
const nodes = entries.map((e) => ({ id: e.id, name: e.name, tier: e.tier, domain: e.domain }));
|
|
4282
|
+
const edges = [];
|
|
4283
|
+
const dangling = [];
|
|
4284
|
+
const ambiguous = [];
|
|
4285
|
+
const incoming = new Map(entries.map((e) => [e.id, 0]));
|
|
4286
|
+
const adjacency = new Map(entries.map((e) => [e.id, []]));
|
|
4287
|
+
for (const e of entries) {
|
|
4288
|
+
for (const target of e.consumes) {
|
|
4289
|
+
const matches = nameToIds.get(target) ?? [];
|
|
4290
|
+
edges.push({ from: e.id, to: target, resolvedIds: matches });
|
|
4291
|
+
if (matches.length === 0) {
|
|
4292
|
+
dangling.push({ from: e.id, to: target });
|
|
4293
|
+
} else if (matches.length === 1) {
|
|
4294
|
+
incoming.set(matches[0], (incoming.get(matches[0]) ?? 0) + 1);
|
|
4295
|
+
adjacency.get(e.id).push(matches[0]);
|
|
4296
|
+
} else {
|
|
4297
|
+
ambiguous.push({ from: e.id, to: target, matches });
|
|
4298
|
+
}
|
|
4299
|
+
}
|
|
4300
|
+
}
|
|
4301
|
+
const orphans = entries.filter((e) => e.tier === "custom" && e.consumes.length === 0).map((e) => e.id);
|
|
4302
|
+
const byId = new Map(entries.map((e) => [e.id, e]));
|
|
4303
|
+
const hotResources = Array.from(incoming.entries()).filter(([, count]) => count > 0).map(([id, count]) => ({ id, name: byId.get(id).name, consumedBy: count })).sort((a, b) => b.consumedBy - a.consumedBy);
|
|
4304
|
+
const cycles = findCycles(adjacency);
|
|
4305
|
+
return {
|
|
4306
|
+
nodeCount: nodes.length,
|
|
4307
|
+
edgeCount: edges.length,
|
|
4308
|
+
nodes,
|
|
4309
|
+
edges,
|
|
4310
|
+
orphans,
|
|
4311
|
+
hotResources,
|
|
4312
|
+
danglingEdges: dangling,
|
|
4313
|
+
ambiguousConsumesTargets: ambiguous,
|
|
4314
|
+
cycles
|
|
4315
|
+
};
|
|
4316
|
+
}
|
|
4317
|
+
function buildRegistryEntries(candidates, warnings) {
|
|
4318
|
+
const entries = [];
|
|
4319
|
+
const seenRealPaths = /* @__PURE__ */ new Set();
|
|
4320
|
+
for (const candidate of candidates) {
|
|
4321
|
+
let realPath;
|
|
4322
|
+
try {
|
|
4323
|
+
realPath = realpathSync2(candidate.dirPath);
|
|
4324
|
+
} catch (err) {
|
|
4325
|
+
warnings.push(`could not resolve skill directory ${candidate.dirPath}: ${err.message}`);
|
|
4326
|
+
continue;
|
|
4327
|
+
}
|
|
4328
|
+
if (seenRealPaths.has(realPath)) continue;
|
|
4329
|
+
seenRealPaths.add(realPath);
|
|
4330
|
+
const skillMdPath = join21(realPath, "SKILL.md");
|
|
4331
|
+
let text;
|
|
4332
|
+
try {
|
|
4333
|
+
text = readFileSync13(skillMdPath, "utf-8");
|
|
4334
|
+
} catch (err) {
|
|
4335
|
+
warnings.push(`could not read ${skillMdPath}: ${err.message}`);
|
|
4336
|
+
continue;
|
|
4337
|
+
}
|
|
4338
|
+
const fm = parseCapabilityFrontmatter(text, basename8(realPath));
|
|
4339
|
+
const name = fm.name ?? basename8(realPath);
|
|
4340
|
+
if (fm.tier && !TIERS.has(fm.tier)) {
|
|
4341
|
+
warnings.push(`${name}: tier '${fm.tier}' not in ${JSON.stringify(Array.from(TIERS).sort())}`);
|
|
4342
|
+
}
|
|
4343
|
+
if (fm.maturity && !MATURITIES.has(fm.maturity)) {
|
|
4344
|
+
warnings.push(`${name}: maturity '${fm.maturity}' not in ${JSON.stringify(Array.from(MATURITIES).sort())}`);
|
|
4345
|
+
}
|
|
4346
|
+
entries.push({
|
|
4347
|
+
id: realPath,
|
|
4348
|
+
name,
|
|
4349
|
+
path: realPath,
|
|
4350
|
+
root: candidate.root,
|
|
4351
|
+
tier: fm.tier,
|
|
4352
|
+
domain: fm.domain,
|
|
4353
|
+
consumes: fm.consumes,
|
|
4354
|
+
provenance: fm.provenance,
|
|
4355
|
+
maturity: fm.maturity,
|
|
4356
|
+
validFrontmatter: fm.validFrontmatter
|
|
4357
|
+
});
|
|
4358
|
+
}
|
|
4359
|
+
return entries;
|
|
4360
|
+
}
|
|
4361
|
+
function joinUsageSnapshot(entries, snapshotPath, warnings) {
|
|
4362
|
+
const { usage, warning } = loadUsageSnapshot(snapshotPath);
|
|
4363
|
+
if (warning) warnings.push(warning);
|
|
4364
|
+
if (usage.size === 0) return false;
|
|
4365
|
+
for (const entry of entries) {
|
|
4366
|
+
const count = usage.get(entry.name);
|
|
4367
|
+
if (count !== void 0) entry.usage = count;
|
|
4368
|
+
}
|
|
4369
|
+
if (!entries.some((e) => e.usage !== void 0)) {
|
|
4370
|
+
warnings.push(
|
|
4371
|
+
"usage snapshot loaded but 0 skills matched by name \u2014 registry `name:` vs telemetry key mismatch; the usage column will be empty"
|
|
4372
|
+
);
|
|
4373
|
+
}
|
|
4374
|
+
return true;
|
|
4375
|
+
}
|
|
4376
|
+
function buildNameToIds(entries, warnings) {
|
|
4377
|
+
const nameToIds = /* @__PURE__ */ new Map();
|
|
4378
|
+
for (const e of entries) {
|
|
4379
|
+
const ids = nameToIds.get(e.name) ?? [];
|
|
4380
|
+
ids.push(e.id);
|
|
4381
|
+
nameToIds.set(e.name, ids);
|
|
4382
|
+
}
|
|
4383
|
+
for (const [name, ids] of nameToIds) {
|
|
4384
|
+
if (ids.length > 1) {
|
|
4385
|
+
warnings.push(`duplicate skill name "${name}" across ${ids.length} locations: ${ids.join(", ")}`);
|
|
4386
|
+
}
|
|
4387
|
+
}
|
|
4388
|
+
return nameToIds;
|
|
4389
|
+
}
|
|
4390
|
+
function findUntagged(entries) {
|
|
4391
|
+
return entries.filter((e) => !e.tier || !e.domain).map((e) => ({
|
|
4392
|
+
id: e.id,
|
|
4393
|
+
name: e.name,
|
|
4394
|
+
missing: [!e.tier ? "tier" : void 0, !e.domain ? "domain" : void 0].filter((v) => Boolean(v))
|
|
4395
|
+
}));
|
|
4396
|
+
}
|
|
4397
|
+
function countByTierAndDomain(entries) {
|
|
4398
|
+
const byTier = {};
|
|
4399
|
+
const byDomain = {};
|
|
4400
|
+
for (const e of entries) {
|
|
4401
|
+
byTier[e.tier ?? "untagged"] = (byTier[e.tier ?? "untagged"] ?? 0) + 1;
|
|
4402
|
+
byDomain[e.domain ?? "untagged"] = (byDomain[e.domain ?? "untagged"] ?? 0) + 1;
|
|
4403
|
+
}
|
|
4404
|
+
return { byTier, byDomain };
|
|
4405
|
+
}
|
|
4406
|
+
function buildOrganizeReport(config, options = {}) {
|
|
4407
|
+
const proStatus = proFeatureStatus(PRO_FEATURE_LABEL3);
|
|
4408
|
+
if (!proStatus.enabled) {
|
|
4409
|
+
throw new UserError(proStatus.notice ?? upgradeNotice(PRO_FEATURE_LABEL3));
|
|
4410
|
+
}
|
|
4411
|
+
const notices = proStatus.notice ? [proStatus.notice] : [];
|
|
4412
|
+
const warnings = [];
|
|
4413
|
+
const candidates = collectCandidates(config.skillsRoots);
|
|
4414
|
+
if (candidates.length === 0) {
|
|
4415
|
+
throw new UserError(
|
|
4416
|
+
`no SKILL.md found under any configured skills root (${config.skillsRoots.join(", ") || "(none configured)"})`
|
|
4417
|
+
);
|
|
4418
|
+
}
|
|
4419
|
+
const entries = buildRegistryEntries(candidates, warnings);
|
|
4420
|
+
const usageJoined = options.usageSnapshot ? joinUsageSnapshot(entries, options.usageSnapshot, warnings) : false;
|
|
4421
|
+
const nameToIds = buildNameToIds(entries, warnings);
|
|
4422
|
+
const untagged = findUntagged(entries);
|
|
4423
|
+
const { byTier, byDomain } = countByTierAndDomain(entries);
|
|
4424
|
+
const registry = {
|
|
4425
|
+
skillsRoots: [...config.skillsRoots],
|
|
4426
|
+
count: entries.length,
|
|
4427
|
+
byTier,
|
|
4428
|
+
byDomain,
|
|
4429
|
+
untagged,
|
|
4430
|
+
warnings,
|
|
4431
|
+
usageJoined,
|
|
4432
|
+
skills: entries
|
|
4433
|
+
};
|
|
4434
|
+
const graph = buildGraph(entries, nameToIds);
|
|
4435
|
+
return {
|
|
4436
|
+
generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
4437
|
+
registry,
|
|
4438
|
+
graph,
|
|
4439
|
+
notices
|
|
4440
|
+
};
|
|
4441
|
+
}
|
|
4442
|
+
|
|
4443
|
+
// src/commands/organize.ts
|
|
4444
|
+
function writeReport2(outPath, contents) {
|
|
4445
|
+
const resolved = resolvePath6(outPath);
|
|
4446
|
+
mkdirSync7(dirname8(resolved), { recursive: true });
|
|
4447
|
+
try {
|
|
4448
|
+
writeFileSync7(resolved, contents, { flag: "wx", mode: 384 });
|
|
4449
|
+
} catch (err) {
|
|
4450
|
+
if (err.code === "EEXIST") {
|
|
4451
|
+
throw new UserError(`output file already exists: ${resolved} \u2014 pass a different --out path`);
|
|
4452
|
+
}
|
|
4453
|
+
throw err;
|
|
4454
|
+
}
|
|
4455
|
+
return resolved;
|
|
4456
|
+
}
|
|
4457
|
+
function renderSummary(report) {
|
|
4458
|
+
const { registry, graph } = report;
|
|
4459
|
+
const lines = [];
|
|
4460
|
+
lines.push(`[skill-forge] organize: ${registry.count} skill(s) across ${registry.skillsRoots.length} root(s)`);
|
|
4461
|
+
lines.push(` by tier: ${JSON.stringify(registry.byTier)}`);
|
|
4462
|
+
lines.push(` by domain: ${JSON.stringify(registry.byDomain)}`);
|
|
4463
|
+
lines.push(` usage joined: ${registry.usageJoined}`);
|
|
4464
|
+
lines.push(` untagged (missing tier/domain): ${registry.untagged.length}`);
|
|
4465
|
+
for (const u of registry.untagged.slice(0, 30)) {
|
|
4466
|
+
lines.push(` - ${sanitizeForMarkdown(u.name)}: missing ${u.missing.join(", ")}`);
|
|
4467
|
+
}
|
|
4468
|
+
if (registry.warnings.length > 0) {
|
|
4469
|
+
lines.push(" warnings:");
|
|
4470
|
+
for (const w of registry.warnings) lines.push(` ! ${sanitizeForMarkdown(w)}`);
|
|
4471
|
+
}
|
|
4472
|
+
lines.push("");
|
|
4473
|
+
lines.push(` graph: ${graph.nodeCount} nodes, ${graph.edgeCount} edges`);
|
|
4474
|
+
if (graph.hotResources.length > 0) {
|
|
4475
|
+
lines.push(" hot resources (consumed by N customs):");
|
|
4476
|
+
for (const h of graph.hotResources) lines.push(` ${h.consumedBy}x ${sanitizeForMarkdown(h.name)}`);
|
|
4477
|
+
}
|
|
4478
|
+
if (graph.danglingEdges.length > 0) {
|
|
4479
|
+
lines.push(" dangling edges (consumes a target not in the set):");
|
|
4480
|
+
for (const d of graph.danglingEdges) lines.push(` ${sanitizeForMarkdown(d.from)} -> ${sanitizeForMarkdown(d.to)} (missing)`);
|
|
4481
|
+
}
|
|
4482
|
+
if (graph.ambiguousConsumesTargets.length > 0) {
|
|
4483
|
+
lines.push(" ambiguous consumes targets (name matched multiple skills):");
|
|
4484
|
+
for (const a of graph.ambiguousConsumesTargets) {
|
|
4485
|
+
lines.push(
|
|
4486
|
+
` ${sanitizeForMarkdown(a.from)} -> ${sanitizeForMarkdown(a.to)} (matches: ${a.matches.map(sanitizeForMarkdown).join(", ")})`
|
|
4487
|
+
);
|
|
4488
|
+
}
|
|
4489
|
+
}
|
|
4490
|
+
if (graph.cycles.length > 0) {
|
|
4491
|
+
lines.push(" cycles (consumes should be acyclic):");
|
|
4492
|
+
for (const c of graph.cycles) lines.push(` ${c.map(sanitizeForMarkdown).join(" -> ")}`);
|
|
4493
|
+
}
|
|
4494
|
+
if (graph.orphans.length > 0) {
|
|
4495
|
+
lines.push(` custom skills with no declared consumes (${graph.orphans.length}):`);
|
|
4496
|
+
for (const o of graph.orphans.slice(0, 30)) lines.push(` - ${sanitizeForMarkdown(o)}`);
|
|
4497
|
+
}
|
|
4498
|
+
for (const n of report.notices) lines.push(` [notice] ${sanitizeForMarkdown(n)}`);
|
|
4499
|
+
return lines;
|
|
4500
|
+
}
|
|
4501
|
+
async function organizeCommand(options = {}) {
|
|
4502
|
+
const config = loadConfig();
|
|
4503
|
+
const report = buildOrganizeReport(config, { usageSnapshot: options.usageSnapshot });
|
|
4504
|
+
let outPath;
|
|
4505
|
+
if (options.out) {
|
|
4506
|
+
outPath = writeReport2(options.out, JSON.stringify(report, null, 2) + "\n");
|
|
4507
|
+
}
|
|
4508
|
+
if (options.json) {
|
|
4509
|
+
for (const n of report.notices) console.error(`[skill-forge] ${n}`);
|
|
4510
|
+
console.log(JSON.stringify(report, null, 2));
|
|
4511
|
+
return;
|
|
4512
|
+
}
|
|
4513
|
+
for (const line of renderSummary(report)) console.log(line);
|
|
4514
|
+
if (outPath) console.log(` report -> ${outPath}`);
|
|
4515
|
+
}
|
|
4516
|
+
|
|
4517
|
+
// src/gate/skillsShApi.ts
|
|
4518
|
+
var SKILLS_SH_ORIGIN = "https://skills.sh";
|
|
4519
|
+
var SKILLS_SH_API_BASE = "/api/v1";
|
|
4520
|
+
var MAX_RESPONSE_BYTES = 2 * 1024 * 1024;
|
|
4521
|
+
var REQUEST_TIMEOUT_MS = 15e3;
|
|
4522
|
+
var ID_PATTERN = /^[A-Za-z0-9._/-]+$/;
|
|
4523
|
+
var TOKEN_HINT = "No VERCEL_OIDC_TOKEN found. skills.sh authenticates with a short-lived Vercel OIDC token:\n 1. Vercel dashboard -> your project -> Settings -> OIDC Federation -> enable.\n 2. npm i -g vercel && vercel link && vercel env pull (writes VERCEL_OIDC_TOKEN to .env.local)\n 3. export VERCEL_OIDC_TOKEN from .env.local into your shell before running this command\n (or set it directly: export VERCEL_OIDC_TOKEN=...) - docs: https://skills.sh/docs/api";
|
|
4524
|
+
var SkillsShError = class extends Error {
|
|
4525
|
+
exitCode;
|
|
4526
|
+
constructor(message, exitCode) {
|
|
4527
|
+
super(message);
|
|
4528
|
+
this.name = "SkillsShError";
|
|
4529
|
+
this.exitCode = exitCode;
|
|
4530
|
+
}
|
|
4531
|
+
};
|
|
4532
|
+
var RISK_ORDER = { NONE: 0, LOW: 1, MEDIUM: 2, HIGH: 3, CRITICAL: 4 };
|
|
4533
|
+
function getToken(env = process.env) {
|
|
4534
|
+
const tok = env.VERCEL_OIDC_TOKEN;
|
|
4535
|
+
return tok ? tok : void 0;
|
|
4536
|
+
}
|
|
4537
|
+
function assertValidId(id) {
|
|
4538
|
+
if (id.length === 0 || !ID_PATTERN.test(id)) {
|
|
4539
|
+
throw new SkillsShError(`invalid skills.sh id: ${JSON.stringify(id)}`, 1);
|
|
4540
|
+
}
|
|
4541
|
+
}
|
|
4542
|
+
function validatedIdSegments(id) {
|
|
4543
|
+
assertValidId(id);
|
|
4544
|
+
const segments = id.split("/");
|
|
4545
|
+
if (segments.some((s) => s === "" || s === "." || s === "..")) {
|
|
4546
|
+
throw new SkillsShError(`invalid skills.sh id: ${JSON.stringify(id)}`, 1);
|
|
4547
|
+
}
|
|
4548
|
+
return segments;
|
|
4549
|
+
}
|
|
4550
|
+
function buildUrl(pathSegments, params) {
|
|
4551
|
+
const url = new URL(SKILLS_SH_API_BASE + "/" + pathSegments.map((s) => encodeURIComponent(s)).join("/"), SKILLS_SH_ORIGIN);
|
|
4552
|
+
if (params) {
|
|
4553
|
+
const search2 = new URLSearchParams();
|
|
4554
|
+
for (const [k, v] of Object.entries(params)) search2.set(k, String(v));
|
|
4555
|
+
url.search = search2.toString();
|
|
4556
|
+
}
|
|
4557
|
+
return url;
|
|
4558
|
+
}
|
|
4559
|
+
async function readCappedText(res) {
|
|
4560
|
+
const text = await res.text();
|
|
4561
|
+
if (new TextEncoder().encode(text).length > MAX_RESPONSE_BYTES) {
|
|
4562
|
+
throw new SkillsShError("skills.sh response exceeded the 2 MB size cap", 1);
|
|
4563
|
+
}
|
|
4564
|
+
return text;
|
|
4565
|
+
}
|
|
4566
|
+
async function call(pathSegments, params, options = {}) {
|
|
4567
|
+
const env = options.env ?? process.env;
|
|
4568
|
+
const token = getToken(env);
|
|
4569
|
+
if (!token) {
|
|
4570
|
+
throw new SkillsShError(TOKEN_HINT, 3);
|
|
4571
|
+
}
|
|
4572
|
+
const url = buildUrl(pathSegments, params);
|
|
4573
|
+
const fetchImpl = options.fetchImpl ?? fetch;
|
|
4574
|
+
let res;
|
|
4575
|
+
try {
|
|
4576
|
+
res = await fetchImpl(url, {
|
|
4577
|
+
headers: { Authorization: `Bearer ${token}` },
|
|
4578
|
+
redirect: "error",
|
|
4579
|
+
signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS)
|
|
4580
|
+
});
|
|
4581
|
+
} catch (err) {
|
|
4582
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
4583
|
+
throw new SkillsShError(`skills.sh request failed: ${message}`, 1);
|
|
4584
|
+
}
|
|
4585
|
+
if (res.status === 404) {
|
|
4586
|
+
return void 0;
|
|
4587
|
+
}
|
|
4588
|
+
if (!res.ok) {
|
|
4589
|
+
if (res.status === 401) {
|
|
4590
|
+
throw new SkillsShError("skills.sh HTTP 401: unauthorized \u2014 the VERCEL_OIDC_TOKEN was rejected (expired or invalid)", 3);
|
|
4591
|
+
}
|
|
4592
|
+
let snippet = "";
|
|
4593
|
+
try {
|
|
4594
|
+
snippet = (await readCappedText(res)).slice(0, 300);
|
|
4595
|
+
} catch {
|
|
4596
|
+
}
|
|
4597
|
+
throw new SkillsShError(`skills.sh HTTP ${res.status}${snippet ? `: ${snippet}` : ""}`, 1);
|
|
4598
|
+
}
|
|
4599
|
+
const text = await readCappedText(res);
|
|
4600
|
+
let parsed;
|
|
4601
|
+
try {
|
|
4602
|
+
parsed = JSON.parse(text);
|
|
4603
|
+
} catch {
|
|
4604
|
+
throw new SkillsShError("skills.sh returned malformed JSON", 1);
|
|
4605
|
+
}
|
|
4606
|
+
return parsed;
|
|
4607
|
+
}
|
|
4608
|
+
function isRecord(v) {
|
|
4609
|
+
return typeof v === "object" && v !== null && !Array.isArray(v);
|
|
4610
|
+
}
|
|
4611
|
+
async function search(query, limit, options = {}) {
|
|
4612
|
+
const data = await call(["skills", "search"], { q: query, limit }, options);
|
|
4613
|
+
if (!isRecord(data) || !Array.isArray(data.data)) {
|
|
4614
|
+
throw new SkillsShError("skills.sh search response had an unexpected shape", 1);
|
|
4615
|
+
}
|
|
4616
|
+
return data;
|
|
4617
|
+
}
|
|
4618
|
+
async function audit(id, options = {}) {
|
|
4619
|
+
const data = await call(["skills", "audit", ...validatedIdSegments(id)], void 0, options);
|
|
4620
|
+
if (data === void 0) return void 0;
|
|
4621
|
+
if (!isRecord(data) || !Array.isArray(data.audits)) {
|
|
4622
|
+
throw new SkillsShError("skills.sh audit response had an unexpected shape", 1);
|
|
4623
|
+
}
|
|
4624
|
+
return data;
|
|
4625
|
+
}
|
|
4626
|
+
async function get(id, options = {}) {
|
|
4627
|
+
return call(["skills", ...validatedIdSegments(id)], void 0, options);
|
|
4628
|
+
}
|
|
4629
|
+
async function curated(options = {}) {
|
|
4630
|
+
return call(["skills", "curated"], void 0, options);
|
|
4631
|
+
}
|
|
4632
|
+
function auditVerdict(data) {
|
|
4633
|
+
const audits = data.audits ?? [];
|
|
4634
|
+
const statuses = new Set(audits.map((a) => a.status));
|
|
4635
|
+
let worst = "NONE";
|
|
4636
|
+
let worstRank = 0;
|
|
4637
|
+
for (const a of audits) {
|
|
4638
|
+
const rl = a.riskLevel ?? "NONE";
|
|
4639
|
+
const rank = RISK_ORDER[rl] ?? 0;
|
|
4640
|
+
if (rank > worstRank) {
|
|
4641
|
+
worstRank = rank;
|
|
4642
|
+
worst = rl;
|
|
4643
|
+
}
|
|
4644
|
+
}
|
|
4645
|
+
const verdict = statuses.has("fail") ? "fail" : statuses.has("warn") ? "warn" : audits.length > 0 ? "pass" : "none";
|
|
4646
|
+
return { verdict, risk: worst, audits };
|
|
4647
|
+
}
|
|
4648
|
+
|
|
4649
|
+
// src/commands/find.ts
|
|
4650
|
+
var INSTALL_HINT_NOTE = "Results are unvetted \u2014 `skill-forge add <id>` runs the full quarantine + safety gate; `skill-forge find --audit <id>` shows partner audit verdicts.";
|
|
4651
|
+
function isRecordWithDataArray(v) {
|
|
4652
|
+
return typeof v === "object" && v !== null && Array.isArray(v.data);
|
|
4653
|
+
}
|
|
4654
|
+
function printJson(data) {
|
|
4655
|
+
console.log(JSON.stringify(data, null, 2));
|
|
4656
|
+
}
|
|
4657
|
+
function printSearchResults(data, query) {
|
|
4658
|
+
const items = data.data;
|
|
4659
|
+
const count = data.count ?? items.length;
|
|
4660
|
+
const searchType = data.searchType ? ` (${sanitizeForMarkdown(data.searchType)})` : "";
|
|
4661
|
+
console.log(`skills.sh \u2014 ${count} matches for ${JSON.stringify(query)}${searchType}:`);
|
|
4662
|
+
for (const item of items) {
|
|
4663
|
+
const name = sanitizeForMarkdown(String(item.name ?? item.id));
|
|
4664
|
+
const id = sanitizeForMarkdown(String(item.id));
|
|
4665
|
+
const installs = item.installs ?? "?";
|
|
4666
|
+
console.log(` - ${name} [${id}] ${installs} installs [unvetted]`);
|
|
4667
|
+
console.log(` install: skill-forge add ${id}`);
|
|
4668
|
+
}
|
|
4669
|
+
console.log("");
|
|
4670
|
+
console.log(INSTALL_HINT_NOTE);
|
|
4671
|
+
}
|
|
4672
|
+
async function find(query, options = {}, deps = {}) {
|
|
4673
|
+
const modes = [query !== void 0, options.audit !== void 0, options.get !== void 0, Boolean(options.curated)];
|
|
4674
|
+
const activeCount = modes.filter(Boolean).length;
|
|
4675
|
+
if (activeCount === 0) {
|
|
4676
|
+
throw new UserError("skill-forge find: provide a search query, or exactly one of --audit <id>, --get <id>, --curated");
|
|
4677
|
+
}
|
|
4678
|
+
if (activeCount > 1) {
|
|
4679
|
+
throw new UserError("skill-forge find: exactly one mode per invocation \u2014 a search query, or exactly one of --audit <id>, --get <id>, --curated");
|
|
4680
|
+
}
|
|
4681
|
+
const callOptions = { fetchImpl: deps.fetchImpl, env: deps.env };
|
|
4682
|
+
try {
|
|
4683
|
+
if (query !== void 0) {
|
|
4684
|
+
if (query.trim().length < 2) {
|
|
4685
|
+
throw new UserError("skill-forge find: query must be at least 2 characters");
|
|
4686
|
+
}
|
|
4687
|
+
const limit = options.limit ?? 10;
|
|
4688
|
+
const data2 = await search(query, limit, callOptions);
|
|
4689
|
+
if (options.json) {
|
|
4690
|
+
printJson(data2);
|
|
4691
|
+
return;
|
|
4692
|
+
}
|
|
4693
|
+
printSearchResults(data2, query);
|
|
4694
|
+
return;
|
|
4695
|
+
}
|
|
4696
|
+
if (options.audit !== void 0) {
|
|
4697
|
+
const id = options.audit;
|
|
4698
|
+
const data2 = await audit(id, callOptions);
|
|
4699
|
+
if (!data2) {
|
|
4700
|
+
console.log(`no partner audits yet for ${sanitizeForMarkdown(id)} (generated after first install).`);
|
|
4701
|
+
return;
|
|
4702
|
+
}
|
|
4703
|
+
if (options.json) {
|
|
4704
|
+
printJson(data2);
|
|
4705
|
+
return;
|
|
4706
|
+
}
|
|
4707
|
+
const { verdict, risk, audits } = auditVerdict(data2);
|
|
4708
|
+
console.log(
|
|
4709
|
+
`skills.sh audit for ${sanitizeForMarkdown(id)}: ${verdict.toUpperCase()} (max risk ${sanitizeForMarkdown(risk)}) \u2014 ${audits.length} partner(s)`
|
|
4710
|
+
);
|
|
4711
|
+
for (const entry of audits) {
|
|
4712
|
+
const provider = sanitizeForMarkdown(String(entry.provider ?? "unknown"));
|
|
4713
|
+
const status2 = sanitizeForMarkdown(String(entry.status ?? "unknown"));
|
|
4714
|
+
const riskLevel = entry.riskLevel ? ` \xB7 ${sanitizeForMarkdown(String(entry.riskLevel))}` : "";
|
|
4715
|
+
const summary = entry.summary ? ` \u2014 ${sanitizeForMarkdown(String(entry.summary))}` : "";
|
|
4716
|
+
console.log(` - ${provider}: ${status2}${riskLevel}${summary}`);
|
|
4717
|
+
}
|
|
4718
|
+
return;
|
|
4719
|
+
}
|
|
4720
|
+
if (options.get !== void 0) {
|
|
4721
|
+
const data2 = await get(options.get, callOptions);
|
|
4722
|
+
printJson(data2);
|
|
4723
|
+
return;
|
|
4724
|
+
}
|
|
4725
|
+
const data = await curated(callOptions);
|
|
4726
|
+
if (options.json) {
|
|
4727
|
+
printJson(data);
|
|
4728
|
+
return;
|
|
4729
|
+
}
|
|
4730
|
+
if (isRecordWithDataArray(data)) {
|
|
4731
|
+
printSearchResults(data, "(curated)");
|
|
4732
|
+
return;
|
|
4733
|
+
}
|
|
4734
|
+
printJson(data);
|
|
4735
|
+
} catch (err) {
|
|
4736
|
+
if (err instanceof SkillsShError) {
|
|
4737
|
+
console.error(err.message);
|
|
4738
|
+
process.exitCode = err.exitCode;
|
|
4739
|
+
return;
|
|
4740
|
+
}
|
|
4741
|
+
throw err;
|
|
4742
|
+
}
|
|
4743
|
+
}
|
|
4744
|
+
|
|
4745
|
+
// src/commands/watch.ts
|
|
4746
|
+
import { realpathSync as realpathSync3 } from "fs";
|
|
4747
|
+
import { join as join22, resolve as resolvePath7 } from "path";
|
|
4748
|
+
var PRO_FEATURE_LABEL4 = "provenance drift watch (watch)";
|
|
4749
|
+
function isGitUrl(source) {
|
|
4750
|
+
const s = source.trim();
|
|
4751
|
+
return /^https?:\/\/\S+$/i.test(s) || /^git@[\w.-]+:\S+$/i.test(s);
|
|
4752
|
+
}
|
|
4753
|
+
function looksLikeGitRef(ref) {
|
|
4754
|
+
const r = ref.trim();
|
|
4755
|
+
return /^[0-9a-fA-F]{7,40}$/.test(r);
|
|
4756
|
+
}
|
|
4757
|
+
function firstNonEmptyLine(text) {
|
|
4758
|
+
return text.split("\n").find((l) => l.trim().length > 0)?.trim() ?? "(no output)";
|
|
4759
|
+
}
|
|
4760
|
+
async function checkGitDrift(exec, url, ref) {
|
|
4761
|
+
let result;
|
|
4762
|
+
try {
|
|
4763
|
+
result = await exec("git", ["ls-remote", "--", url, ref], { cwd: process.cwd() });
|
|
4764
|
+
} catch (err) {
|
|
4765
|
+
return { status: "unreachable", detail: `git ls-remote failed to run: ${err.message}` };
|
|
4766
|
+
}
|
|
4767
|
+
if (result.code !== 0) {
|
|
4768
|
+
return { status: "unreachable", detail: `git ls-remote exited ${result.code}: ${firstNonEmptyLine(result.stderr || result.stdout)}` };
|
|
4769
|
+
}
|
|
4770
|
+
const shas = result.stdout.split("\n").map((l) => l.trim()).filter((l) => l.length > 0).map((l) => l.split(/\s+/)[0] ?? "").filter((sha) => sha.length > 0);
|
|
4771
|
+
if (shas.length === 0) {
|
|
4772
|
+
return { status: "unreachable", detail: `no ref matching "${ref}" found on remote` };
|
|
4773
|
+
}
|
|
4774
|
+
const recorded = ref.trim().toLowerCase();
|
|
4775
|
+
if (shas.some((sha) => sha.toLowerCase() === recorded)) {
|
|
4776
|
+
return { status: "unchanged", detail: `current: ${recorded}` };
|
|
4777
|
+
}
|
|
4778
|
+
return { status: "moved", detail: `recorded ${ref}, remote now resolves to ${shas[0]}` };
|
|
4779
|
+
}
|
|
4780
|
+
async function runWatch(config, options = {}, deps = {}) {
|
|
4781
|
+
const proStatus = proFeatureStatus(PRO_FEATURE_LABEL4);
|
|
4782
|
+
if (!proStatus.enabled) {
|
|
4783
|
+
return { roots: [], rows: [], warnings: [], notices: [proStatus.notice ?? "Pro feature (watch) locked."] };
|
|
4784
|
+
}
|
|
4785
|
+
const notices = proStatus.notice ? [proStatus.notice] : [];
|
|
4786
|
+
const exec = deps.exec ?? defaultExec;
|
|
4787
|
+
const roots = [];
|
|
4788
|
+
const rows = [];
|
|
4789
|
+
const warnings = [];
|
|
4790
|
+
const seenRealPaths = /* @__PURE__ */ new Set();
|
|
4791
|
+
for (const root of config.skillsRoots) {
|
|
4792
|
+
const resolvedRoot = resolvePath7(expandHome(root));
|
|
4793
|
+
const exists = isDir(resolvedRoot);
|
|
4794
|
+
let realPath = resolvedRoot;
|
|
4795
|
+
if (exists) {
|
|
4796
|
+
try {
|
|
4797
|
+
realPath = realpathSync3(resolvedRoot);
|
|
4798
|
+
} catch {
|
|
4799
|
+
realPath = resolvedRoot;
|
|
4800
|
+
}
|
|
4801
|
+
}
|
|
4802
|
+
if (seenRealPaths.has(realPath)) continue;
|
|
4803
|
+
seenRealPaths.add(realPath);
|
|
4804
|
+
let ledger;
|
|
4805
|
+
try {
|
|
4806
|
+
ledger = readProvenanceLedger(resolvedRoot);
|
|
4807
|
+
} catch (err) {
|
|
4808
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
4809
|
+
const ledgerPath = join22(resolvedRoot, "SOURCES.md");
|
|
4810
|
+
roots.push({ root, realPath, exists, ledgerPath, ledgerExists: false });
|
|
4811
|
+
warnings.push(`${ledgerPath}: could not read ledger: ${message}`);
|
|
4812
|
+
continue;
|
|
4813
|
+
}
|
|
4814
|
+
roots.push({ root, realPath, exists, ledgerPath: ledger.ledgerPath, ledgerExists: ledger.exists });
|
|
4815
|
+
warnings.push(...ledger.warnings.map((w) => `${ledger.ledgerPath}: ${w}`));
|
|
4816
|
+
for (const entry of ledger.entries) {
|
|
4817
|
+
if (entry.verb === "REJECT") continue;
|
|
4818
|
+
const rowBase = {
|
|
4819
|
+
root,
|
|
4820
|
+
ledgerPath: ledger.ledgerPath,
|
|
4821
|
+
name: entry.name,
|
|
4822
|
+
verb: entry.verb,
|
|
4823
|
+
source: entry.source,
|
|
4824
|
+
upstreamRef: entry.upstreamRef,
|
|
4825
|
+
driftCheckCommand: entry.driftCheck
|
|
4826
|
+
};
|
|
4827
|
+
if (!(isGitUrl(entry.source) && looksLikeGitRef(entry.upstreamRef))) {
|
|
4828
|
+
rows.push({
|
|
4829
|
+
...rowBase,
|
|
4830
|
+
checkMethod: "manual",
|
|
4831
|
+
driftStatus: "not-checked",
|
|
4832
|
+
detail: "source is not a recognized git URL + ref pin \u2014 check manually"
|
|
4833
|
+
});
|
|
4834
|
+
continue;
|
|
4835
|
+
}
|
|
4836
|
+
if (options.offline) {
|
|
4837
|
+
rows.push({
|
|
4838
|
+
...rowBase,
|
|
4839
|
+
checkMethod: "git-ls-remote",
|
|
4840
|
+
driftStatus: "not-checked",
|
|
4841
|
+
detail: "--offline: network check skipped"
|
|
4842
|
+
});
|
|
4843
|
+
continue;
|
|
4844
|
+
}
|
|
4845
|
+
const { status: status2, detail } = await checkGitDrift(exec, entry.source, entry.upstreamRef);
|
|
4846
|
+
rows.push({ ...rowBase, checkMethod: "git-ls-remote", driftStatus: status2, detail });
|
|
4847
|
+
}
|
|
4848
|
+
}
|
|
4849
|
+
return { roots, rows, warnings, notices };
|
|
4850
|
+
}
|
|
4851
|
+
function printWatchTerminal(report) {
|
|
4852
|
+
console.log("[skill-forge] watch \u2014 provenance drift check");
|
|
4853
|
+
for (const notice of report.notices) console.log(`[notice] ${notice}`);
|
|
4854
|
+
if (report.roots.length === 0) {
|
|
4855
|
+
console.log("\nNo configured skills roots.");
|
|
4856
|
+
}
|
|
4857
|
+
for (const root of report.roots) {
|
|
4858
|
+
console.log(`
|
|
4859
|
+
${sanitizeForMarkdown(root.root)} (${sanitizeForMarkdown(root.realPath)})`);
|
|
4860
|
+
if (!root.exists) {
|
|
4861
|
+
console.log(" root does not exist on disk");
|
|
4862
|
+
continue;
|
|
4863
|
+
}
|
|
4864
|
+
if (!root.ledgerExists) {
|
|
4865
|
+
console.log(` no ledger at ${sanitizeForMarkdown(root.ledgerPath)}`);
|
|
4866
|
+
continue;
|
|
4867
|
+
}
|
|
4868
|
+
console.log(` ledger: ${sanitizeForMarkdown(root.ledgerPath)}`);
|
|
4869
|
+
const rowsForRoot = report.rows.filter((r) => r.root === root.root);
|
|
4870
|
+
if (rowsForRoot.length === 0) {
|
|
4871
|
+
console.log(" (no tracked, non-REJECT sources)");
|
|
4872
|
+
continue;
|
|
4873
|
+
}
|
|
4874
|
+
for (const row of rowsForRoot) {
|
|
4875
|
+
console.log(` \u2022 ${sanitizeForMarkdown(row.name)} [${sanitizeForMarkdown(row.verb)}] ref=${sanitizeForMarkdown(row.upstreamRef)}`);
|
|
4876
|
+
if (row.checkMethod === "git-ls-remote") {
|
|
4877
|
+
console.log(` drift: ${row.driftStatus} \u2014 ${sanitizeForMarkdown(row.detail)}`);
|
|
4878
|
+
} else {
|
|
4879
|
+
console.log(` manual check \u2014 stored drift-check command (data, NOT executed): ${sanitizeForMarkdown(row.driftCheckCommand)}`);
|
|
4880
|
+
}
|
|
4881
|
+
}
|
|
4882
|
+
}
|
|
4883
|
+
if (report.warnings.length > 0) {
|
|
4884
|
+
console.log("\nWarnings:");
|
|
4885
|
+
for (const w of report.warnings) console.log(` - ${sanitizeForMarkdown(w)}`);
|
|
4886
|
+
}
|
|
4887
|
+
console.log("\nCompare each stored ref to current upstream; re-run ingestion on any that moved.");
|
|
4888
|
+
}
|
|
4889
|
+
async function watchCommand(options = {}) {
|
|
4890
|
+
const report = await runWatch(loadConfig(), options);
|
|
4891
|
+
if (options.json) {
|
|
4892
|
+
console.log(JSON.stringify(report, null, 2));
|
|
4893
|
+
for (const notice of report.notices) console.error(`[skill-forge] ${notice}`);
|
|
4894
|
+
return;
|
|
4895
|
+
}
|
|
4896
|
+
printWatchTerminal(report);
|
|
4897
|
+
}
|
|
4898
|
+
|
|
4899
|
+
// src/commands/ingest.ts
|
|
4900
|
+
import { realpathSync as realpathSync4 } from "fs";
|
|
4901
|
+
import { dirname as dirname9, isAbsolute, relative, resolve as resolvePath8 } from "path";
|
|
4902
|
+
var PRO_FEATURE_LABEL5 = "the pending-ingestion queue";
|
|
4903
|
+
var VALID_STATUSES = ["ingested", "dismissed"];
|
|
4904
|
+
function requireProFeature() {
|
|
4905
|
+
const status2 = proFeatureStatus(PRO_FEATURE_LABEL5);
|
|
4906
|
+
if (!status2.enabled) {
|
|
4907
|
+
throw new UserError(
|
|
4908
|
+
status2.notice ?? "ingest is a Pro feature \u2014 set SKILL_FORGE_LICENSE or config.licenseKey to a valid license to unlock."
|
|
4909
|
+
);
|
|
4910
|
+
}
|
|
4911
|
+
if (status2.notice) console.error(`[skill-forge] ${status2.notice}`);
|
|
4912
|
+
}
|
|
4913
|
+
function pluralizeEntries(count) {
|
|
4914
|
+
return `entr${count === 1 ? "y" : "ies"}`;
|
|
4915
|
+
}
|
|
4916
|
+
function toListRow(entry) {
|
|
4917
|
+
return {
|
|
4918
|
+
id: entry.id,
|
|
4919
|
+
source: entry.source,
|
|
4920
|
+
artifactType: entry.artifactType ?? "skill",
|
|
4921
|
+
safetyVerdict: entry.gate.safetyVerdict,
|
|
4922
|
+
createdAt: entry.createdAt
|
|
4923
|
+
};
|
|
4924
|
+
}
|
|
4925
|
+
function printPendingList(pending, json) {
|
|
4926
|
+
if (json) {
|
|
4927
|
+
console.log(JSON.stringify({ entries: pending.map(toListRow) }, null, 2));
|
|
4928
|
+
return;
|
|
4929
|
+
}
|
|
4930
|
+
if (pending.length === 0) {
|
|
4931
|
+
console.log("[skill-forge] no pending queue entries.");
|
|
4932
|
+
return;
|
|
4933
|
+
}
|
|
4934
|
+
console.log(`[skill-forge] ${pending.length} pending ${pluralizeEntries(pending.length)}:`);
|
|
4935
|
+
for (const entry of pending) {
|
|
4936
|
+
const row = toListRow(entry);
|
|
4937
|
+
console.log(` ${row.id} ${row.source} (${row.artifactType}) verdict=${row.safetyVerdict} created=${row.createdAt}`);
|
|
4938
|
+
}
|
|
4939
|
+
}
|
|
4940
|
+
function realpathOrNull(p) {
|
|
4941
|
+
try {
|
|
4942
|
+
return realpathSync4(p);
|
|
4943
|
+
} catch {
|
|
4944
|
+
return null;
|
|
4945
|
+
}
|
|
4946
|
+
}
|
|
4947
|
+
function isUnderRoot(candidate, root) {
|
|
4948
|
+
const rel = relative(root, candidate);
|
|
4949
|
+
return rel === "" || !rel.startsWith("..") && !isAbsolute(rel);
|
|
4950
|
+
}
|
|
4951
|
+
function validatePendingEntries(entries, config) {
|
|
4952
|
+
const quarantineRoot = realpathOrNull(resolvePath8(config.quarantineDir));
|
|
4953
|
+
const skillsRoots = config.skillsRoots.map((r) => realpathOrNull(resolvePath8(r))).filter((r) => r !== null);
|
|
4954
|
+
const mcpTargetDirs = (config.mcpTargets ?? []).map((f) => realpathOrNull(dirname9(resolvePath8(f)))).filter((r) => r !== null);
|
|
4955
|
+
const allowedRoots = [quarantineRoot, ...skillsRoots, ...mcpTargetDirs].filter((r) => r !== null);
|
|
4956
|
+
const valid = [];
|
|
4957
|
+
const excluded = [];
|
|
4958
|
+
for (const entry of entries) {
|
|
4959
|
+
const effectivePath = entry.installedPath ?? entry.quarantinePath;
|
|
4960
|
+
if (!effectivePath) {
|
|
4961
|
+
excluded.push({ entry, reason: "no installedPath or quarantinePath recorded on the entry" });
|
|
4962
|
+
continue;
|
|
4963
|
+
}
|
|
4964
|
+
const real = realpathOrNull(resolvePath8(effectivePath));
|
|
4965
|
+
if (!real) {
|
|
4966
|
+
excluded.push({ entry, reason: `path does not exist or could not be resolved: ${effectivePath}` });
|
|
4967
|
+
continue;
|
|
4968
|
+
}
|
|
4969
|
+
if (allowedRoots.length === 0 || !allowedRoots.some((root) => isUnderRoot(real, root))) {
|
|
4970
|
+
excluded.push({ entry, reason: `path escapes every configured root (quarantineDir/skillsRoots/mcpTargets): ${effectivePath}` });
|
|
4971
|
+
continue;
|
|
4972
|
+
}
|
|
4973
|
+
valid.push(entry);
|
|
4974
|
+
}
|
|
4975
|
+
return { valid, excluded };
|
|
4976
|
+
}
|
|
4977
|
+
async function ingest(options = {}) {
|
|
4978
|
+
requireProFeature();
|
|
4979
|
+
const queue2 = readQueue();
|
|
4980
|
+
const pending = queue2.entries.filter((e) => e.status === "pending");
|
|
4981
|
+
if (options.list) {
|
|
4982
|
+
printPendingList(pending, options.json);
|
|
4983
|
+
return;
|
|
4984
|
+
}
|
|
4985
|
+
if (pending.length === 0) {
|
|
4986
|
+
console.log("[skill-forge] no pending queue entries to ingest.");
|
|
4987
|
+
return;
|
|
4988
|
+
}
|
|
4989
|
+
const config = loadConfig();
|
|
4990
|
+
const { valid, excluded } = validatePendingEntries(pending, config);
|
|
4991
|
+
if (excluded.length > 0) {
|
|
4992
|
+
console.error(`[skill-forge] excluded ${excluded.length} pending ${pluralizeEntries(excluded.length)} that failed path validation:`);
|
|
4993
|
+
for (const { entry, reason } of excluded) {
|
|
4994
|
+
console.error(` - ${entry.id} (${entry.source}): ${reason}`);
|
|
4995
|
+
}
|
|
4996
|
+
console.error(
|
|
4997
|
+
"[skill-forge] excluded entries stay in the queue \u2014 review each, then dismiss with: skill-forge queue close <id> --status dismissed"
|
|
4998
|
+
);
|
|
4999
|
+
}
|
|
5000
|
+
if (valid.length === 0) {
|
|
5001
|
+
console.log("[skill-forge] no pending entries passed path validation \u2014 nothing to hand off.");
|
|
5002
|
+
return;
|
|
5003
|
+
}
|
|
5004
|
+
console.log(`[skill-forge] ${valid.length} pending ${pluralizeEntries(valid.length)} ready for ingest:`);
|
|
5005
|
+
for (const entry of valid) {
|
|
5006
|
+
console.log(` - ${entry.id} ${entry.source} (${entry.artifactType ?? "skill"})`);
|
|
5007
|
+
}
|
|
5008
|
+
runHandoff(config, getQueuePath(), { promptAsset: "ingest-prompt.md" });
|
|
5009
|
+
}
|
|
5010
|
+
async function queueClose(id, options = {}) {
|
|
5011
|
+
requireProFeature();
|
|
5012
|
+
const status2 = options.status;
|
|
5013
|
+
if (!status2 || !VALID_STATUSES.includes(status2)) {
|
|
5014
|
+
throw new UserError(`--status must be one of: ${VALID_STATUSES.join(", ")} (got ${status2 ? JSON.stringify(status2) : "nothing"})`);
|
|
5015
|
+
}
|
|
5016
|
+
let updated;
|
|
5017
|
+
try {
|
|
5018
|
+
updated = updateQueueEntry(id, { status: status2 });
|
|
5019
|
+
} catch (err) {
|
|
5020
|
+
throw new UserError(err instanceof Error ? err.message : String(err));
|
|
5021
|
+
}
|
|
5022
|
+
console.log(`[skill-forge] queue entry ${updated.id} closed -> status: ${updated.status}`);
|
|
5023
|
+
}
|
|
5024
|
+
|
|
5025
|
+
// src/commands/refine.ts
|
|
5026
|
+
import { createHash as createHash3, randomUUID as randomUUID4 } from "crypto";
|
|
5027
|
+
import { chmodSync, existsSync as existsSync20, lstatSync as lstatSync3, mkdirSync as mkdirSync9, readFileSync as readFileSync15, realpathSync as realpathSync7, renameSync as renameSync5, rmSync as rmSync7, writeFileSync as writeFileSync9 } from "fs";
|
|
5028
|
+
import { dirname as dirname11, isAbsolute as isAbsolute3, join as join25, relative as relative3, resolve as resolvePath11, sep as sep2 } from "path";
|
|
5029
|
+
import { createInterface as createInterface5 } from "readline/promises";
|
|
5030
|
+
|
|
5031
|
+
// src/refine/context.ts
|
|
5032
|
+
import { existsSync as existsSync19, realpathSync as realpathSync6, statSync as statSync11 } from "fs";
|
|
5033
|
+
import { isAbsolute as isAbsolute2, join as join24, relative as relative2, resolve as resolvePath10 } from "path";
|
|
5034
|
+
|
|
5035
|
+
// src/refine/models.ts
|
|
5036
|
+
import { randomBytes } from "crypto";
|
|
5037
|
+
var REFINEMENT_CATEGORIES = ["trigger", "content", "hook", "tool", "pattern", "config", "new"];
|
|
5038
|
+
var OVERRIDE_TYPES = ["patch", "extend", "config", "full", "hook", "script", "new"];
|
|
5039
|
+
var PATCH_ACTIONS = ["append", "prepend", "replace-section", "insert-after", "insert-before", "delete-section"];
|
|
5040
|
+
var PATTERN_STATUSES = ["tracking", "ready", "generalized", "dismissed"];
|
|
5041
|
+
function isRefinementCategory(value) {
|
|
5042
|
+
return REFINEMENT_CATEGORIES.includes(value);
|
|
5043
|
+
}
|
|
5044
|
+
function isOverrideType(value) {
|
|
5045
|
+
return OVERRIDE_TYPES.includes(value);
|
|
5046
|
+
}
|
|
5047
|
+
function isPatchAction(value) {
|
|
5048
|
+
return PATCH_ACTIONS.includes(value);
|
|
5049
|
+
}
|
|
5050
|
+
function isPatternStatus(value) {
|
|
5051
|
+
return PATTERN_STATUSES.includes(value);
|
|
5052
|
+
}
|
|
5053
|
+
var REFINEMENT_STATUSES = ["pending", "applied", "generalized"];
|
|
5054
|
+
function isRefinementStatus(value) {
|
|
5055
|
+
return REFINEMENT_STATUSES.includes(value);
|
|
5056
|
+
}
|
|
5057
|
+
function patternCount(pattern) {
|
|
5058
|
+
return new Set(pattern.occurrences.map((o) => o.projectRealpath)).size;
|
|
5059
|
+
}
|
|
5060
|
+
function defaultRandomHex() {
|
|
5061
|
+
return randomBytes(2).toString("hex");
|
|
5062
|
+
}
|
|
5063
|
+
function formatYYYYMMDD(date) {
|
|
5064
|
+
const yyyy = date.getUTCFullYear().toString().padStart(4, "0");
|
|
5065
|
+
const mm = (date.getUTCMonth() + 1).toString().padStart(2, "0");
|
|
5066
|
+
const dd = date.getUTCDate().toString().padStart(2, "0");
|
|
5067
|
+
return `${yyyy}${mm}${dd}`;
|
|
5068
|
+
}
|
|
5069
|
+
function generateRefinementId(existingIds = /* @__PURE__ */ new Set(), now = /* @__PURE__ */ new Date(), randomHex = defaultRandomHex) {
|
|
5070
|
+
const datePart = formatYYYYMMDD(now);
|
|
5071
|
+
let id;
|
|
5072
|
+
do {
|
|
5073
|
+
id = `REF-${datePart}-${randomHex()}`;
|
|
5074
|
+
} while (existingIds.has(id));
|
|
5075
|
+
return id;
|
|
5076
|
+
}
|
|
5077
|
+
function generatePatternId(existingIds = /* @__PURE__ */ new Set(), randomHex = defaultRandomHex) {
|
|
5078
|
+
let id;
|
|
5079
|
+
do {
|
|
5080
|
+
id = `PAT-${randomHex()}`;
|
|
5081
|
+
} while (existingIds.has(id));
|
|
5082
|
+
return id;
|
|
5083
|
+
}
|
|
5084
|
+
function fingerprintKey(skill, target, overrideType, action) {
|
|
5085
|
+
const normalize = (s) => s.trim().toLowerCase().replace(/\s+/g, " ");
|
|
5086
|
+
return [normalize(skill), normalize(target), overrideType, action ?? ""].join("::");
|
|
5087
|
+
}
|
|
5088
|
+
function contentSimilarity(a, b) {
|
|
5089
|
+
const tokenize2 = (s) => new Set(s.toLowerCase().split(/\s+/).filter(Boolean));
|
|
5090
|
+
const tokensA = tokenize2(a);
|
|
5091
|
+
const tokensB = tokenize2(b);
|
|
5092
|
+
if (tokensA.size === 0 || tokensB.size === 0) return 0;
|
|
5093
|
+
let intersectionSize = 0;
|
|
5094
|
+
for (const token of tokensA) {
|
|
5095
|
+
if (tokensB.has(token)) intersectionSize++;
|
|
5096
|
+
}
|
|
5097
|
+
const unionSize2 = tokensA.size + tokensB.size - intersectionSize;
|
|
5098
|
+
return unionSize2 === 0 ? 0 : intersectionSize / unionSize2;
|
|
5099
|
+
}
|
|
5100
|
+
|
|
5101
|
+
// src/refine/store.ts
|
|
5102
|
+
import { createHash as createHash2, randomUUID as randomUUID3 } from "crypto";
|
|
5103
|
+
import { existsSync as existsSync18, mkdirSync as mkdirSync8, readFileSync as readFileSync14, renameSync as renameSync4, rmSync as rmSync6, statSync as statSync10, writeFileSync as writeFileSync8, realpathSync as realpathSync5 } from "fs";
|
|
5104
|
+
import { basename as basename9, dirname as dirname10, join as join23, resolve as resolvePath9 } from "path";
|
|
5105
|
+
var SIMILARITY_JOIN_THRESHOLD = 0.7;
|
|
5106
|
+
var READY_THRESHOLD = 2;
|
|
5107
|
+
function getRefinementsDir() {
|
|
5108
|
+
return join23(getHomeDir(), "refinements");
|
|
5109
|
+
}
|
|
5110
|
+
function getRefinementsPath() {
|
|
5111
|
+
return join23(getRefinementsDir(), "refinements.json");
|
|
5112
|
+
}
|
|
5113
|
+
function getPatternsPath() {
|
|
5114
|
+
return join23(getRefinementsDir(), "patterns.json");
|
|
5115
|
+
}
|
|
5116
|
+
function getBackupsDir() {
|
|
5117
|
+
return join23(getRefinementsDir(), "backups");
|
|
5118
|
+
}
|
|
5119
|
+
function writeJsonAtomic(path, data) {
|
|
5120
|
+
const dir = dirname10(path);
|
|
5121
|
+
mkdirSync8(dir, { recursive: true });
|
|
5122
|
+
const tempPath = join23(dir, `.${basename9(path)}.tmp-${randomUUID3()}`);
|
|
5123
|
+
try {
|
|
5124
|
+
writeFileSync8(tempPath, JSON.stringify(data, null, 2) + "\n", { encoding: "utf-8", mode: 384 });
|
|
5125
|
+
renameSync4(tempPath, path);
|
|
5126
|
+
} catch (err) {
|
|
5127
|
+
rmSync6(tempPath, { force: true });
|
|
5128
|
+
throw err;
|
|
5129
|
+
}
|
|
5130
|
+
}
|
|
5131
|
+
function readJsonFile(path, label) {
|
|
5132
|
+
let raw;
|
|
5133
|
+
try {
|
|
5134
|
+
raw = readFileSync14(path, "utf-8");
|
|
5135
|
+
} catch (err) {
|
|
5136
|
+
throw new UserError(`unable to read ${label} at ${path}: ${err.message}`);
|
|
5137
|
+
}
|
|
5138
|
+
try {
|
|
5139
|
+
return JSON.parse(raw);
|
|
5140
|
+
} catch {
|
|
5141
|
+
throw new UserError(`malformed ${label} at ${path}: not valid JSON`);
|
|
5142
|
+
}
|
|
5143
|
+
}
|
|
5144
|
+
function readStore(path, label, validateRecord) {
|
|
5145
|
+
if (!existsSync18(path)) {
|
|
5146
|
+
return { schemaVersion: 1, records: [] };
|
|
5147
|
+
}
|
|
5148
|
+
const parsed = readJsonFile(path, label);
|
|
5149
|
+
if (typeof parsed !== "object" || parsed === null || !Array.isArray(parsed.records)) {
|
|
5150
|
+
throw new UserError(`malformed ${label} at ${path}: expected an object with a "records" array`);
|
|
5151
|
+
}
|
|
5152
|
+
const records = parsed.records;
|
|
5153
|
+
if (validateRecord) {
|
|
5154
|
+
records.forEach((record, index) => validateRecord(record, index));
|
|
5155
|
+
}
|
|
5156
|
+
return { schemaVersion: 1, records };
|
|
5157
|
+
}
|
|
5158
|
+
function validateRefinementRecordShape(record, index, path) {
|
|
5159
|
+
if (typeof record !== "object" || record === null) {
|
|
5160
|
+
throw new UserError(`malformed refinements.json at ${path}: record[${index}] is not an object`);
|
|
5161
|
+
}
|
|
5162
|
+
const r = record;
|
|
5163
|
+
if (typeof r.id !== "string" || r.id.length === 0) {
|
|
5164
|
+
throw new UserError(`malformed refinements.json at ${path}: record[${index}] is missing a valid "id"`);
|
|
5165
|
+
}
|
|
5166
|
+
if (typeof r.createdAt !== "string" || r.createdAt.length === 0) {
|
|
5167
|
+
throw new UserError(`malformed refinements.json at ${path}: record "${r.id}" is missing a valid "createdAt"`);
|
|
5168
|
+
}
|
|
5169
|
+
if (typeof r.status !== "string" || r.status.length === 0) {
|
|
5170
|
+
throw new UserError(`malformed refinements.json at ${path}: record "${r.id}" is missing a valid "status"`);
|
|
5171
|
+
}
|
|
5172
|
+
if (typeof r.skill !== "string" || r.skill.length === 0) {
|
|
5173
|
+
throw new UserError(`malformed refinements.json at ${path}: record "${r.id}" is missing a valid "skill"`);
|
|
5174
|
+
}
|
|
5175
|
+
const patchActionsRequiringMarker = /* @__PURE__ */ new Set(["insert-after", "insert-before", "replace-section", "delete-section"]);
|
|
5176
|
+
if (r.overrideType === "patch") {
|
|
5177
|
+
if (typeof r.patchAction !== "string" || r.patchAction.length === 0) {
|
|
5178
|
+
throw new UserError(`malformed refinements.json at ${path}: record "${r.id}" has overrideType "patch" but is missing "patchAction"`);
|
|
5179
|
+
}
|
|
5180
|
+
if (patchActionsRequiringMarker.has(r.patchAction) && (typeof r.patchMarker !== "string" || r.patchMarker.length === 0)) {
|
|
5181
|
+
throw new UserError(
|
|
5182
|
+
`malformed refinements.json at ${path}: record "${r.id}" has patchAction "${r.patchAction}" but is missing "patchMarker"`
|
|
5183
|
+
);
|
|
5184
|
+
}
|
|
5185
|
+
if (typeof r.patchContent !== "string" || r.patchContent.length === 0) {
|
|
5186
|
+
throw new UserError(`malformed refinements.json at ${path}: record "${r.id}" has overrideType "patch" but is missing "patchContent"`);
|
|
5187
|
+
}
|
|
5188
|
+
} else if (r.patchAction !== void 0) {
|
|
5189
|
+
throw new UserError(
|
|
5190
|
+
`malformed refinements.json at ${path}: record "${r.id}" has overrideType "${String(r.overrideType)}" but carries "patchAction" \u2014 only valid when overrideType is "patch"`
|
|
5191
|
+
);
|
|
5192
|
+
}
|
|
5193
|
+
}
|
|
5194
|
+
function validatePatternRecordShape(record, index, path) {
|
|
5195
|
+
if (typeof record !== "object" || record === null) {
|
|
5196
|
+
throw new UserError(`malformed patterns.json at ${path}: record[${index}] is not an object`);
|
|
5197
|
+
}
|
|
5198
|
+
const p = record;
|
|
5199
|
+
if (typeof p.id !== "string" || p.id.length === 0) {
|
|
5200
|
+
throw new UserError(`malformed patterns.json at ${path}: record[${index}] is missing a valid "id"`);
|
|
5201
|
+
}
|
|
5202
|
+
if (typeof p.status !== "string" || p.status.length === 0) {
|
|
5203
|
+
throw new UserError(`malformed patterns.json at ${path}: record "${p.id}" is missing a valid "status"`);
|
|
5204
|
+
}
|
|
5205
|
+
if (!Array.isArray(p.occurrences)) {
|
|
5206
|
+
throw new UserError(`malformed patterns.json at ${path}: record "${p.id}" is missing a valid "occurrences" array`);
|
|
5207
|
+
}
|
|
5208
|
+
if (p.status === "dismissed" && (typeof p.dismissedReason !== "string" || p.dismissedReason.length === 0)) {
|
|
5209
|
+
throw new UserError(`malformed patterns.json at ${path}: record "${p.id}" has status "dismissed" but is missing "dismissedReason"`);
|
|
5210
|
+
}
|
|
5211
|
+
if (p.status === "generalized") {
|
|
5212
|
+
if (typeof p.generalizedAt !== "string" || p.generalizedAt.length === 0) {
|
|
5213
|
+
throw new UserError(`malformed patterns.json at ${path}: record "${p.id}" has status "generalized" but is missing "generalizedAt"`);
|
|
5214
|
+
}
|
|
5215
|
+
if (typeof p.generalizedBackupId !== "string" || p.generalizedBackupId.length === 0) {
|
|
5216
|
+
throw new UserError(`malformed patterns.json at ${path}: record "${p.id}" has status "generalized" but is missing "generalizedBackupId"`);
|
|
5217
|
+
}
|
|
5218
|
+
}
|
|
5219
|
+
}
|
|
5220
|
+
function readRefinementsStore() {
|
|
5221
|
+
const path = getRefinementsPath();
|
|
5222
|
+
return readStore(path, "refinements.json", (record, index) => validateRefinementRecordShape(record, index, path));
|
|
5223
|
+
}
|
|
5224
|
+
function writeRefinementsStore(store) {
|
|
5225
|
+
writeJsonAtomic(getRefinementsPath(), store);
|
|
5226
|
+
}
|
|
5227
|
+
function readPatternsStore() {
|
|
5228
|
+
const path = getPatternsPath();
|
|
5229
|
+
return readStore(path, "patterns.json", (record, index) => validatePatternRecordShape(record, index, path));
|
|
5230
|
+
}
|
|
5231
|
+
function writePatternsStore(store) {
|
|
5232
|
+
writeJsonAtomic(getPatternsPath(), store);
|
|
5233
|
+
}
|
|
5234
|
+
function resolveProjectRealpath(projectRoot) {
|
|
5235
|
+
const resolved = resolvePath9(projectRoot);
|
|
5236
|
+
try {
|
|
5237
|
+
return realpathSync5(resolved);
|
|
5238
|
+
} catch {
|
|
5239
|
+
return resolved;
|
|
5240
|
+
}
|
|
5241
|
+
}
|
|
5242
|
+
function generatePatternName(refinement) {
|
|
5243
|
+
const parts = refinement.targetSection.replace(/\//g, " ").replace(/-/g, " ").split(/\s+/).filter(Boolean);
|
|
5244
|
+
const lastThree = parts.slice(-3);
|
|
5245
|
+
if (lastThree.length === 0) return refinement.targetSection || `pattern-${refinement.skill}`;
|
|
5246
|
+
return lastThree.map((word) => word.charAt(0).toUpperCase() + word.slice(1)).join(" ");
|
|
5247
|
+
}
|
|
5248
|
+
function generatePatternDescription(refinement) {
|
|
5249
|
+
const category = refinement.category.charAt(0).toUpperCase() + refinement.category.slice(1);
|
|
5250
|
+
const cause = refinement.rootCause || refinement.expectedBehavior;
|
|
5251
|
+
return `${category} modification for ${refinement.skill}: ${cause}`;
|
|
5252
|
+
}
|
|
5253
|
+
function upsertPattern(records, refinement, projectRealpath, nowIso, randomHex, explicitPatternId) {
|
|
5254
|
+
let pattern;
|
|
5255
|
+
if (explicitPatternId) {
|
|
5256
|
+
pattern = records.find((p) => p.id === explicitPatternId);
|
|
5257
|
+
if (!pattern) {
|
|
5258
|
+
throw new UserError(`no pattern with id: ${explicitPatternId}`);
|
|
5259
|
+
}
|
|
5260
|
+
if (pattern.status === "dismissed" || pattern.status === "generalized") {
|
|
5261
|
+
throw new UserError(`cannot join pattern ${explicitPatternId}: it is ${pattern.status} and no longer accepts new occurrences`);
|
|
5262
|
+
}
|
|
5263
|
+
} else {
|
|
5264
|
+
const joinable = records.filter((p) => p.status !== "generalized" && p.status !== "dismissed");
|
|
5265
|
+
const fingerprint = fingerprintKey(refinement.skill, refinement.targetSection, refinement.overrideType, refinement.patchAction);
|
|
5266
|
+
pattern = joinable.find((p) => p.fingerprint === fingerprint);
|
|
5267
|
+
if (!pattern && refinement.patchContent) {
|
|
5268
|
+
pattern = joinable.find((p) => {
|
|
5269
|
+
const sample = p.proposedChanges.content;
|
|
5270
|
+
return sample ? contentSimilarity(refinement.patchContent, sample) > SIMILARITY_JOIN_THRESHOLD : false;
|
|
5271
|
+
});
|
|
5272
|
+
}
|
|
5273
|
+
}
|
|
5274
|
+
const wasTracking = pattern?.status === "tracking";
|
|
5275
|
+
if (!pattern) {
|
|
5276
|
+
const created = {
|
|
5277
|
+
id: generatePatternId(new Set(records.map((p) => p.id)), randomHex),
|
|
5278
|
+
name: generatePatternName(refinement),
|
|
5279
|
+
description: generatePatternDescription(refinement),
|
|
5280
|
+
affectedSkills: [refinement.skill],
|
|
5281
|
+
refinementIds: [refinement.id],
|
|
5282
|
+
occurrences: [{ refinementId: refinement.id, projectRealpath, timestamp: nowIso }],
|
|
5283
|
+
status: "tracking",
|
|
5284
|
+
proposedChanges: refinement.patchContent ? { content: refinement.patchContent } : {},
|
|
5285
|
+
fingerprint: fingerprintKey(refinement.skill, refinement.targetSection, refinement.overrideType, refinement.patchAction),
|
|
5286
|
+
firstSeen: nowIso,
|
|
5287
|
+
lastSeen: nowIso
|
|
5288
|
+
};
|
|
5289
|
+
records.push(created);
|
|
5290
|
+
return { pattern: created, becameReady: false };
|
|
5291
|
+
}
|
|
5292
|
+
if (!pattern.affectedSkills.includes(refinement.skill)) pattern.affectedSkills.push(refinement.skill);
|
|
5293
|
+
if (!pattern.refinementIds.includes(refinement.id)) pattern.refinementIds.push(refinement.id);
|
|
5294
|
+
pattern.occurrences.push({ refinementId: refinement.id, projectRealpath, timestamp: nowIso });
|
|
5295
|
+
pattern.lastSeen = nowIso;
|
|
5296
|
+
const becameReady = wasTracking && patternCount(pattern) >= READY_THRESHOLD;
|
|
5297
|
+
if (becameReady) pattern.status = "ready";
|
|
5298
|
+
return { pattern, becameReady };
|
|
5299
|
+
}
|
|
5300
|
+
function recordRefinement(input, deps = {}) {
|
|
5301
|
+
const now = deps.now ?? /* @__PURE__ */ new Date();
|
|
5302
|
+
const nowIso = now.toISOString();
|
|
5303
|
+
const refinementsStore = readRefinementsStore();
|
|
5304
|
+
const patternsStore = readPatternsStore();
|
|
5305
|
+
const existingRefIds = new Set(refinementsStore.records.map((r) => r.id));
|
|
5306
|
+
const refinementId = deps.id ?? generateRefinementId(existingRefIds, now, deps.randomHex);
|
|
5307
|
+
const projectRealpath = resolveProjectRealpath(input.projectRoot);
|
|
5308
|
+
const refinement = {
|
|
5309
|
+
id: refinementId,
|
|
5310
|
+
skill: input.skill,
|
|
5311
|
+
category: input.category,
|
|
5312
|
+
overrideType: input.overrideType,
|
|
5313
|
+
targetSection: input.targetSection,
|
|
5314
|
+
expectedBehavior: input.expectedBehavior,
|
|
5315
|
+
actualBehavior: input.actualBehavior,
|
|
5316
|
+
example: input.example,
|
|
5317
|
+
desiredOutcome: input.desiredOutcome,
|
|
5318
|
+
rootCause: input.rootCause,
|
|
5319
|
+
confidence: input.confidence,
|
|
5320
|
+
project: input.project,
|
|
5321
|
+
projectRoot: projectRealpath,
|
|
5322
|
+
patchAction: input.patchAction,
|
|
5323
|
+
patchMarker: input.patchMarker,
|
|
5324
|
+
patchContent: input.patchContent,
|
|
5325
|
+
generalizationPotential: input.generalizationPotential,
|
|
5326
|
+
scope: input.scope,
|
|
5327
|
+
status: input.status ?? "applied",
|
|
5328
|
+
createdAt: nowIso
|
|
5329
|
+
};
|
|
5330
|
+
const { pattern, becameReady } = upsertPattern(patternsStore.records, refinement, projectRealpath, nowIso, deps.randomHex, input.patternId);
|
|
5331
|
+
refinement.patternId = pattern.id;
|
|
5332
|
+
refinementsStore.records.push(refinement);
|
|
5333
|
+
writeRefinementsStore(refinementsStore);
|
|
5334
|
+
try {
|
|
5335
|
+
writePatternsStore(patternsStore);
|
|
5336
|
+
} catch (err) {
|
|
5337
|
+
throw new UserError(
|
|
5338
|
+
`refinements.json was updated with refinement ${refinement.id}, but patterns.json failed to update: ${err.message}`
|
|
5339
|
+
);
|
|
5340
|
+
}
|
|
5341
|
+
return { refinement, pattern, patternBecameReady: becameReady };
|
|
5342
|
+
}
|
|
5343
|
+
function markGeneralized(id, backupId) {
|
|
5344
|
+
const store = readPatternsStore();
|
|
5345
|
+
const pattern = store.records.find((p) => p.id === id);
|
|
5346
|
+
if (!pattern) {
|
|
5347
|
+
throw new UserError(`no pattern with id: ${id}`);
|
|
5348
|
+
}
|
|
5349
|
+
const nowIso = (/* @__PURE__ */ new Date()).toISOString();
|
|
5350
|
+
pattern.status = "generalized";
|
|
5351
|
+
pattern.generalizedAt = nowIso;
|
|
5352
|
+
pattern.generalizedBackupId = backupId;
|
|
5353
|
+
pattern.lastSeen = nowIso;
|
|
5354
|
+
writePatternsStore(store);
|
|
5355
|
+
return pattern;
|
|
5356
|
+
}
|
|
5357
|
+
function formatBackupTimestamp(date) {
|
|
5358
|
+
return date.toISOString().replace(/[-:]/g, "").replace(/\.\d{3}Z$/, "Z");
|
|
5359
|
+
}
|
|
5360
|
+
var BACKUP_ID_PATTERN = /^\d{8}T\d{6}Z-[0-9a-f]{4}$/;
|
|
5361
|
+
function isValidBackupId(id) {
|
|
5362
|
+
return BACKUP_ID_PATTERN.test(id);
|
|
5363
|
+
}
|
|
5364
|
+
function createBackup(filePaths, deps = {}) {
|
|
5365
|
+
const now = deps.now ?? /* @__PURE__ */ new Date();
|
|
5366
|
+
const randomHex = deps.randomHex ?? defaultRandomHex;
|
|
5367
|
+
const id = `${formatBackupTimestamp(now)}-${randomHex()}`;
|
|
5368
|
+
const files = filePaths.map((path) => {
|
|
5369
|
+
if (!existsSync18(path)) {
|
|
5370
|
+
return { path, existed: false, sha256: null, mode: null, contentBase64: null };
|
|
5371
|
+
}
|
|
5372
|
+
const bytes = readFileSync14(path);
|
|
5373
|
+
const mode = statSync10(path).mode & 511;
|
|
5374
|
+
const sha256 = createHash2("sha256").update(bytes).digest("hex");
|
|
5375
|
+
return { path, existed: true, sha256, mode, contentBase64: bytes.toString("base64") };
|
|
5376
|
+
});
|
|
5377
|
+
const manifest = { id, createdAt: now.toISOString(), files };
|
|
5378
|
+
writeJsonAtomic(join23(getBackupsDir(), id, "manifest.json"), manifest);
|
|
5379
|
+
return manifest;
|
|
5380
|
+
}
|
|
5381
|
+
function validateBackupManifestShape(value, path) {
|
|
5382
|
+
if (typeof value !== "object" || value === null) {
|
|
5383
|
+
throw new UserError(`malformed backup manifest at ${path}: not an object`);
|
|
5384
|
+
}
|
|
5385
|
+
const m = value;
|
|
5386
|
+
if (typeof m.id !== "string" || m.id.length === 0) {
|
|
5387
|
+
throw new UserError(`malformed backup manifest at ${path}: missing a valid "id"`);
|
|
5388
|
+
}
|
|
5389
|
+
if (typeof m.createdAt !== "string" || m.createdAt.length === 0) {
|
|
5390
|
+
throw new UserError(`malformed backup manifest at ${path}: missing a valid "createdAt"`);
|
|
5391
|
+
}
|
|
5392
|
+
if (!Array.isArray(m.files)) {
|
|
5393
|
+
throw new UserError(`malformed backup manifest at ${path}: missing a valid "files" array`);
|
|
5394
|
+
}
|
|
5395
|
+
m.files.forEach((entry, index) => {
|
|
5396
|
+
if (typeof entry !== "object" || entry === null) {
|
|
5397
|
+
throw new UserError(`malformed backup manifest at ${path}: files[${index}] is not an object`);
|
|
5398
|
+
}
|
|
5399
|
+
const f = entry;
|
|
5400
|
+
if (typeof f.path !== "string" || f.path.length === 0) {
|
|
5401
|
+
throw new UserError(`malformed backup manifest at ${path}: files[${index}] is missing a valid "path"`);
|
|
5402
|
+
}
|
|
5403
|
+
if (typeof f.existed !== "boolean") {
|
|
5404
|
+
throw new UserError(`malformed backup manifest at ${path}: files[${index}] ("${f.path}") is missing a valid "existed" (must be boolean)`);
|
|
5405
|
+
}
|
|
5406
|
+
if (f.existed) {
|
|
5407
|
+
if (typeof f.sha256 !== "string" || f.sha256.length === 0) {
|
|
5408
|
+
throw new UserError(`malformed backup manifest at ${path}: files[${index}] ("${f.path}") has existed:true but is missing "sha256"`);
|
|
5409
|
+
}
|
|
5410
|
+
if (typeof f.mode !== "number") {
|
|
5411
|
+
throw new UserError(`malformed backup manifest at ${path}: files[${index}] ("${f.path}") has existed:true but is missing a numeric "mode"`);
|
|
5412
|
+
}
|
|
5413
|
+
if (typeof f.contentBase64 !== "string") {
|
|
5414
|
+
throw new UserError(`malformed backup manifest at ${path}: files[${index}] ("${f.path}") has existed:true but is missing "contentBase64"`);
|
|
5415
|
+
}
|
|
5416
|
+
} else {
|
|
5417
|
+
if (f.sha256 !== null || f.mode !== null || f.contentBase64 !== null) {
|
|
5418
|
+
throw new UserError(`malformed backup manifest at ${path}: files[${index}] ("${f.path}") has existed:false (a tombstone) but sha256/mode/contentBase64 are not all null`);
|
|
5419
|
+
}
|
|
5420
|
+
}
|
|
5421
|
+
});
|
|
5422
|
+
}
|
|
5423
|
+
function readBackupManifest(id) {
|
|
5424
|
+
if (!isValidBackupId(id)) {
|
|
5425
|
+
throw new UserError(`invalid backup id "${id}" \u2014 expected the format produced by \`skill-forge refine promote\` (e.g. 20260720T142305Z-a1b2)`);
|
|
5426
|
+
}
|
|
5427
|
+
const manifestPath = join23(getBackupsDir(), id, "manifest.json");
|
|
5428
|
+
if (!existsSync18(manifestPath)) {
|
|
5429
|
+
throw new UserError(`no backup with id: ${id}`);
|
|
5430
|
+
}
|
|
5431
|
+
const parsed = readJsonFile(manifestPath, "backup manifest");
|
|
5432
|
+
validateBackupManifestShape(parsed, manifestPath);
|
|
5433
|
+
return parsed;
|
|
5434
|
+
}
|
|
5435
|
+
|
|
5436
|
+
// src/refine/context.ts
|
|
5437
|
+
function resolveConfiguredRoot2(root) {
|
|
5438
|
+
return resolvePath10(expandHome(root));
|
|
5439
|
+
}
|
|
5440
|
+
function assertPlainSkillName(name) {
|
|
5441
|
+
if (!name || name.trim().length === 0) {
|
|
5442
|
+
throw new UserError("skill name must not be empty");
|
|
5443
|
+
}
|
|
5444
|
+
if (name.includes("/") || name.includes("\\") || name === "." || name === "..") {
|
|
5445
|
+
throw new UserError(`invalid skill name "${name}" \u2014 must be a plain directory name, not a path`);
|
|
5446
|
+
}
|
|
5447
|
+
}
|
|
5448
|
+
function realpathOrNull2(p) {
|
|
5449
|
+
try {
|
|
5450
|
+
return realpathSync6(p);
|
|
5451
|
+
} catch {
|
|
5452
|
+
return null;
|
|
5453
|
+
}
|
|
5454
|
+
}
|
|
5455
|
+
function isUnderRoot2(candidate, root) {
|
|
5456
|
+
const rel = relative2(root, candidate);
|
|
5457
|
+
return rel === "" || !rel.startsWith("..") && !isAbsolute2(rel);
|
|
5458
|
+
}
|
|
5459
|
+
function resolveScopes(config, cwd, skill, userRootFlag) {
|
|
5460
|
+
assertPlainSkillName(skill);
|
|
5461
|
+
const resolvedCwd = resolvePath10(cwd);
|
|
5462
|
+
const localDir = join24(resolvedCwd, ".claude", "skills", skill);
|
|
5463
|
+
const sharedDir = join24(resolvedCwd, "skills", skill);
|
|
5464
|
+
const local = { scope: "local", dir: localDir, exists: existsSync19(localDir) };
|
|
5465
|
+
const shared = { scope: "shared", dir: sharedDir, exists: existsSync19(sharedDir) };
|
|
5466
|
+
const user = resolveUserScope(config, resolvedCwd, skill, userRootFlag);
|
|
5467
|
+
return { local, shared, user };
|
|
5468
|
+
}
|
|
5469
|
+
function resolveUserScope(config, resolvedCwd, skill, userRootFlag) {
|
|
5470
|
+
if (userRootFlag) {
|
|
5471
|
+
const root = resolvePath10(userRootFlag);
|
|
5472
|
+
return { resolved: true, dir: join24(root, skill), root: userRootFlag, reason: "flag" };
|
|
5473
|
+
}
|
|
5474
|
+
const cwdReal = realpathOrNull2(resolvedCwd) ?? resolvedCwd;
|
|
5475
|
+
const candidates = config.skillsRoots.map((root) => ({ root, dir: join24(resolveConfiguredRoot2(root), skill) }));
|
|
5476
|
+
for (const candidate of candidates) {
|
|
5477
|
+
const skillReal = realpathOrNull2(candidate.dir);
|
|
5478
|
+
if (skillReal) {
|
|
5479
|
+
return { resolved: true, dir: candidate.dir, root: candidate.root, reason: "contains-skill" };
|
|
5480
|
+
}
|
|
5481
|
+
}
|
|
5482
|
+
const externalCandidates = candidates.filter((candidate) => {
|
|
5483
|
+
const rootReal = realpathOrNull2(resolveConfiguredRoot2(candidate.root)) ?? resolveConfiguredRoot2(candidate.root);
|
|
5484
|
+
return !isUnderRoot2(rootReal, cwdReal) && !isUnderRoot2(cwdReal, rootReal);
|
|
5485
|
+
});
|
|
5486
|
+
if (externalCandidates.length === 1) {
|
|
5487
|
+
return { resolved: true, dir: externalCandidates[0].dir, root: externalCandidates[0].root, reason: "sole-external-root" };
|
|
5488
|
+
}
|
|
5489
|
+
return { resolved: false, candidates: externalCandidates.length > 0 ? externalCandidates : candidates };
|
|
5490
|
+
}
|
|
5491
|
+
function isFile2(p) {
|
|
5492
|
+
try {
|
|
5493
|
+
return statSync11(p).isFile();
|
|
5494
|
+
} catch {
|
|
5495
|
+
return false;
|
|
5496
|
+
}
|
|
5497
|
+
}
|
|
5498
|
+
function isDirectory(p) {
|
|
5499
|
+
try {
|
|
5500
|
+
return statSync11(p).isDirectory();
|
|
5501
|
+
} catch {
|
|
5502
|
+
return false;
|
|
5503
|
+
}
|
|
5504
|
+
}
|
|
5505
|
+
function inventoryScopeDir(scope, dir) {
|
|
5506
|
+
return {
|
|
5507
|
+
scope,
|
|
5508
|
+
dir,
|
|
5509
|
+
dirExists: isDirectory(dir),
|
|
5510
|
+
skillMd: isFile2(join24(dir, "SKILL.md")),
|
|
5511
|
+
patchMd: isFile2(join24(dir, "SKILL.patch.md")),
|
|
5512
|
+
extendMd: isFile2(join24(dir, "SKILL.extend.md")),
|
|
5513
|
+
configJson: isFile2(join24(dir, "skill-config.json")),
|
|
5514
|
+
hooksDir: isDirectory(join24(dir, "hooks")),
|
|
5515
|
+
scriptsDir: isDirectory(join24(dir, "scripts"))
|
|
5516
|
+
};
|
|
5517
|
+
}
|
|
5518
|
+
function inventoryAllScopes(resolution) {
|
|
5519
|
+
const rows = [inventoryScopeDir("local", resolution.local.dir), inventoryScopeDir("shared", resolution.shared.dir)];
|
|
5520
|
+
if (resolution.user.resolved) {
|
|
5521
|
+
rows.push(inventoryScopeDir("user", resolution.user.dir));
|
|
5522
|
+
}
|
|
5523
|
+
return rows;
|
|
5524
|
+
}
|
|
5525
|
+
|
|
5526
|
+
// src/refine/patch.ts
|
|
5527
|
+
var FENCE_RE = /^\s*(```|~~~)/;
|
|
5528
|
+
var HEADING_RE = /^(#{1,6})\s+(.*)$/;
|
|
5529
|
+
function frontmatterOffset(lines) {
|
|
5530
|
+
if (lines[0] !== "---") return 0;
|
|
5531
|
+
for (let i = 1; i < lines.length; i++) {
|
|
5532
|
+
if (lines[i] === "---") return i + 1;
|
|
5533
|
+
}
|
|
5534
|
+
return 0;
|
|
5535
|
+
}
|
|
5536
|
+
function buildHeadingTree(lines, bodyOffset) {
|
|
5537
|
+
const flat = [];
|
|
5538
|
+
let inFence = false;
|
|
5539
|
+
for (let i = bodyOffset; i < lines.length; i++) {
|
|
5540
|
+
const line = lines[i];
|
|
5541
|
+
if (FENCE_RE.test(line)) {
|
|
5542
|
+
inFence = !inFence;
|
|
5543
|
+
continue;
|
|
5544
|
+
}
|
|
5545
|
+
if (inFence) continue;
|
|
5546
|
+
const m = HEADING_RE.exec(line);
|
|
5547
|
+
if (m) flat.push({ level: m[1].length, text: m[2].trim(), line: i });
|
|
5548
|
+
}
|
|
5549
|
+
const root = { level: 0, heading: "", headingLine: bodyOffset - 1, bodyStart: bodyOffset, bodyEnd: lines.length, children: [] };
|
|
5550
|
+
const stack = [root];
|
|
5551
|
+
for (const h of flat) {
|
|
5552
|
+
while (stack.length > 1 && stack[stack.length - 1].level >= h.level) {
|
|
5553
|
+
const done = stack.pop();
|
|
5554
|
+
if (done) done.bodyEnd = h.line;
|
|
5555
|
+
}
|
|
5556
|
+
const node = { level: h.level, heading: h.text, headingLine: h.line, bodyStart: h.line + 1, bodyEnd: lines.length, children: [] };
|
|
5557
|
+
stack[stack.length - 1].children.push(node);
|
|
5558
|
+
stack.push(node);
|
|
5559
|
+
}
|
|
5560
|
+
return root;
|
|
5561
|
+
}
|
|
5562
|
+
function normalizeText(s) {
|
|
5563
|
+
return s.trim().replace(/\s+/g, " ");
|
|
5564
|
+
}
|
|
5565
|
+
function splitHeadingPath(target) {
|
|
5566
|
+
return target.split(">").map((segment) => segment.trim()).filter((segment) => segment.length > 0);
|
|
5567
|
+
}
|
|
5568
|
+
function collectDescendants(node, acc) {
|
|
5569
|
+
acc.push(node);
|
|
5570
|
+
for (const child of node.children) collectDescendants(child, acc);
|
|
5571
|
+
}
|
|
5572
|
+
function resolveHeadingPath(root, segments) {
|
|
5573
|
+
if (segments.length === 0) return root;
|
|
5574
|
+
const allNodes = [];
|
|
5575
|
+
collectDescendants(root, allNodes);
|
|
5576
|
+
const headingNodes = allNodes.filter((n) => n.level > 0);
|
|
5577
|
+
const firstNorm = normalizeText(segments[0]);
|
|
5578
|
+
let candidates = headingNodes.filter((n) => normalizeText(n.heading) === firstNorm);
|
|
5579
|
+
if (candidates.length === 0) {
|
|
5580
|
+
const available = headingNodes.map((n) => n.heading);
|
|
5581
|
+
throw new UserError(`No section matching "${segments[0]}" found (available headings: ${available.length > 0 ? available.join(", ") : "none"})`);
|
|
5582
|
+
}
|
|
5583
|
+
for (let i = 1; i < segments.length; i++) {
|
|
5584
|
+
const segment = segments[i];
|
|
5585
|
+
const norm = normalizeText(segment);
|
|
5586
|
+
const next = [];
|
|
5587
|
+
for (const candidate of candidates) {
|
|
5588
|
+
for (const child of candidate.children) {
|
|
5589
|
+
if (normalizeText(child.heading) === norm) next.push(child);
|
|
5590
|
+
}
|
|
5591
|
+
}
|
|
5592
|
+
if (next.length === 0) {
|
|
5593
|
+
const available = [...new Set(candidates.flatMap((c) => c.children.map((child) => child.heading)))];
|
|
5594
|
+
throw new UserError(`No section matching "${segment}" found (available headings: ${available.length > 0 ? available.join(", ") : "none"})`);
|
|
5595
|
+
}
|
|
5596
|
+
candidates = next;
|
|
5597
|
+
}
|
|
5598
|
+
if (candidates.length > 1) {
|
|
5599
|
+
throw new UserError(`Target selector "${segments.join(" > ")}" matched ${candidates.length} sections; be more specific`);
|
|
5600
|
+
}
|
|
5601
|
+
return candidates[0];
|
|
5602
|
+
}
|
|
5603
|
+
function findMarkerLine(lines, start, end, marker) {
|
|
5604
|
+
const norm = normalizeText(marker);
|
|
5605
|
+
const matches = [];
|
|
5606
|
+
for (let i = start; i < end; i++) {
|
|
5607
|
+
if (normalizeText(lines[i]).includes(norm)) matches.push(i);
|
|
5608
|
+
}
|
|
5609
|
+
if (matches.length === 0) {
|
|
5610
|
+
throw new UserError(`No line matching marker "${marker}" found in target section`);
|
|
5611
|
+
}
|
|
5612
|
+
if (matches.length > 1) {
|
|
5613
|
+
const preview = matches.map((i) => `line ${i + 1}: "${lines[i].trim()}"`).join("; ");
|
|
5614
|
+
throw new UserError(`Marker "${marker}" matched ${matches.length} lines, expected exactly 1 (${preview})`);
|
|
5615
|
+
}
|
|
5616
|
+
return matches[0];
|
|
5617
|
+
}
|
|
5618
|
+
function findChildByMarker(target, marker) {
|
|
5619
|
+
const norm = normalizeText(marker);
|
|
5620
|
+
const matches = target.children.filter((c) => normalizeText(c.heading).includes(norm));
|
|
5621
|
+
if (matches.length === 0) {
|
|
5622
|
+
const available = target.children.map((c) => c.heading).join(", ") || "none";
|
|
5623
|
+
throw new UserError(`No subsection matching "${marker}" found under target section (available: ${available})`);
|
|
5624
|
+
}
|
|
5625
|
+
if (matches.length > 1) {
|
|
5626
|
+
throw new UserError(`Marker "${marker}" matched ${matches.length} subsections: ${matches.map((c) => c.heading).join(", ")}`);
|
|
5627
|
+
}
|
|
5628
|
+
return matches[0];
|
|
5629
|
+
}
|
|
5630
|
+
function requireMarker(block) {
|
|
5631
|
+
if (!block.marker || block.marker.trim().length === 0) {
|
|
5632
|
+
throw new UserError(`Action "${block.action}" requires a marker`);
|
|
5633
|
+
}
|
|
5634
|
+
return block.marker;
|
|
5635
|
+
}
|
|
5636
|
+
function applyOneAction(lines, target, block) {
|
|
5637
|
+
const contentLines = block.content.length === 0 ? [""] : block.content.split("\n");
|
|
5638
|
+
switch (block.action) {
|
|
5639
|
+
case "append": {
|
|
5640
|
+
lines.splice(target.bodyEnd, 0, ...contentLines);
|
|
5641
|
+
return;
|
|
5642
|
+
}
|
|
5643
|
+
case "prepend": {
|
|
5644
|
+
lines.splice(target.bodyStart, 0, ...contentLines);
|
|
5645
|
+
return;
|
|
5646
|
+
}
|
|
5647
|
+
case "insert-after":
|
|
5648
|
+
case "insert-before": {
|
|
5649
|
+
const idx = findMarkerLine(lines, target.bodyStart, target.bodyEnd, requireMarker(block));
|
|
5650
|
+
lines.splice(block.action === "insert-after" ? idx + 1 : idx, 0, ...contentLines);
|
|
5651
|
+
return;
|
|
5652
|
+
}
|
|
5653
|
+
case "replace-section":
|
|
5654
|
+
case "delete-section": {
|
|
5655
|
+
const child = findChildByMarker(target, requireMarker(block));
|
|
5656
|
+
const replacement = block.action === "replace-section" ? contentLines : [];
|
|
5657
|
+
lines.splice(child.headingLine, child.bodyEnd - child.headingLine, ...replacement);
|
|
5658
|
+
return;
|
|
5659
|
+
}
|
|
5660
|
+
default: {
|
|
5661
|
+
const exhaustive = block.action;
|
|
5662
|
+
throw new UserError(`Unknown patch action "${exhaustive}"`);
|
|
5663
|
+
}
|
|
5664
|
+
}
|
|
5665
|
+
}
|
|
5666
|
+
function applyPatchBlocks(skillMd, blocks) {
|
|
5667
|
+
const lines = skillMd.split("\n");
|
|
5668
|
+
for (const block of blocks) {
|
|
5669
|
+
const bodyOffset = frontmatterOffset(lines);
|
|
5670
|
+
const root = buildHeadingTree(lines, bodyOffset);
|
|
5671
|
+
const segments = block.target ? splitHeadingPath(block.target) : [];
|
|
5672
|
+
const target = resolveHeadingPath(root, segments);
|
|
5673
|
+
applyOneAction(lines, target, block);
|
|
5674
|
+
}
|
|
5675
|
+
return lines.join("\n");
|
|
5676
|
+
}
|
|
5677
|
+
function renderExtendFile(meta) {
|
|
5678
|
+
return [
|
|
5679
|
+
"# SKILL.extend.md",
|
|
5680
|
+
`# Extensions for: ${meta.skillName}`,
|
|
5681
|
+
`# Generated: ${meta.generatedAt}`,
|
|
5682
|
+
`# Refinement: ${meta.refinementId}`,
|
|
5683
|
+
"",
|
|
5684
|
+
`## EXTEND: ${meta.targetSection}`,
|
|
5685
|
+
"<!-- ACTION: append -->",
|
|
5686
|
+
meta.content,
|
|
5687
|
+
""
|
|
5688
|
+
].join("\n");
|
|
5689
|
+
}
|
|
5690
|
+
function renderConfigOverride(meta) {
|
|
5691
|
+
const payload = {
|
|
5692
|
+
skill: meta.skillName,
|
|
5693
|
+
version: "1.0.0",
|
|
5694
|
+
generated: meta.generatedAt,
|
|
5695
|
+
config: meta.config
|
|
5696
|
+
};
|
|
5697
|
+
return `${JSON.stringify(payload, null, 2)}
|
|
5698
|
+
`;
|
|
5699
|
+
}
|
|
5700
|
+
function computeLcsDiff(a, b) {
|
|
5701
|
+
const n = a.length;
|
|
5702
|
+
const m = b.length;
|
|
5703
|
+
const dp = Array.from({ length: n + 1 }, () => new Array(m + 1).fill(0));
|
|
5704
|
+
for (let i2 = n - 1; i2 >= 0; i2--) {
|
|
5705
|
+
for (let j2 = m - 1; j2 >= 0; j2--) {
|
|
5706
|
+
dp[i2][j2] = a[i2] === b[j2] ? dp[i2 + 1][j2 + 1] + 1 : Math.max(dp[i2 + 1][j2], dp[i2][j2 + 1]);
|
|
5707
|
+
}
|
|
5708
|
+
}
|
|
5709
|
+
const ops = [];
|
|
5710
|
+
let i = 0;
|
|
5711
|
+
let j = 0;
|
|
5712
|
+
while (i < n && j < m) {
|
|
5713
|
+
if (a[i] === b[j]) {
|
|
5714
|
+
ops.push({ type: "context", line: a[i] });
|
|
5715
|
+
i++;
|
|
5716
|
+
j++;
|
|
5717
|
+
} else if (dp[i + 1][j] >= dp[i][j + 1]) {
|
|
5718
|
+
ops.push({ type: "remove", line: a[i] });
|
|
5719
|
+
i++;
|
|
5720
|
+
} else {
|
|
5721
|
+
ops.push({ type: "add", line: b[j] });
|
|
5722
|
+
j++;
|
|
5723
|
+
}
|
|
5724
|
+
}
|
|
5725
|
+
while (i < n) {
|
|
5726
|
+
ops.push({ type: "remove", line: a[i] });
|
|
5727
|
+
i++;
|
|
5728
|
+
}
|
|
5729
|
+
while (j < m) {
|
|
5730
|
+
ops.push({ type: "add", line: b[j] });
|
|
5731
|
+
j++;
|
|
5732
|
+
}
|
|
5733
|
+
return ops;
|
|
5734
|
+
}
|
|
5735
|
+
function previewDiff(before, after, path) {
|
|
5736
|
+
const a = before.split("\n");
|
|
5737
|
+
const b = after.split("\n");
|
|
5738
|
+
const ops = computeLcsDiff(a, b);
|
|
5739
|
+
const lines = [`--- ${path}`, `+++ ${path}`];
|
|
5740
|
+
for (const op of ops) {
|
|
5741
|
+
const prefix = op.type === "add" ? "+" : op.type === "remove" ? "-" : " ";
|
|
5742
|
+
lines.push(`${prefix}${op.line}`);
|
|
5743
|
+
}
|
|
5744
|
+
return lines.join("\n");
|
|
5745
|
+
}
|
|
5746
|
+
|
|
5747
|
+
// src/commands/refine.ts
|
|
5748
|
+
var PRO_FEATURE_LABEL6 = "skill refinement";
|
|
5749
|
+
var REFINE_PROMPT_ASSET = "refine-prompt.md";
|
|
5750
|
+
function requireProFeature2() {
|
|
5751
|
+
const status2 = proFeatureStatus(PRO_FEATURE_LABEL6);
|
|
5752
|
+
if (!status2.enabled) {
|
|
5753
|
+
throw new UserError(status2.notice ?? "refine is a Pro feature \u2014 set SKILL_FORGE_LICENSE or config.licenseKey to a valid license to unlock.");
|
|
5754
|
+
}
|
|
5755
|
+
if (status2.notice) console.error(`[skill-forge] ${status2.notice}`);
|
|
5756
|
+
}
|
|
5757
|
+
function isInteractive(json) {
|
|
5758
|
+
return !json && Boolean(process.stdin.isTTY) && Boolean(process.stdout.isTTY);
|
|
5759
|
+
}
|
|
5760
|
+
function emit(json, payload, renderText) {
|
|
5761
|
+
if (json) {
|
|
5762
|
+
console.log(JSON.stringify(payload, null, 2));
|
|
5763
|
+
} else {
|
|
5764
|
+
renderText();
|
|
5765
|
+
}
|
|
5766
|
+
}
|
|
5767
|
+
async function confirmYesNo(rl, question) {
|
|
5768
|
+
const answer = (await rl.question(question)).trim().toLowerCase();
|
|
5769
|
+
return answer === "y" || answer === "yes";
|
|
5770
|
+
}
|
|
5771
|
+
async function confirmWrite(yes, json, prompt) {
|
|
5772
|
+
if (yes) return true;
|
|
5773
|
+
if (!isInteractive(json)) {
|
|
5774
|
+
throw new UserError("refusing to write without confirmation \u2014 pass --yes, or run interactively.");
|
|
5775
|
+
}
|
|
5776
|
+
const rl = createInterface5({ input: process.stdin, output: process.stdout });
|
|
5777
|
+
try {
|
|
5778
|
+
return await confirmYesNo(rl, prompt);
|
|
5779
|
+
} finally {
|
|
5780
|
+
rl.close();
|
|
5781
|
+
}
|
|
5782
|
+
}
|
|
5783
|
+
function resolveConfiguredRoot3(root) {
|
|
5784
|
+
return resolvePath11(expandHome(root));
|
|
5785
|
+
}
|
|
5786
|
+
function isUnderRoot3(candidate, root) {
|
|
5787
|
+
const rel = relative3(root, candidate);
|
|
5788
|
+
return rel === "" || !rel.startsWith("..") && !isAbsolute3(rel);
|
|
5789
|
+
}
|
|
5790
|
+
function sanitizeDiffForConsole(diff) {
|
|
5791
|
+
return diff.split("\n").map((line) => sanitizeForMarkdown(line)).join("\n");
|
|
5792
|
+
}
|
|
5793
|
+
function resolveContentOption(options) {
|
|
5794
|
+
if (options.content !== void 0) return options.content;
|
|
5795
|
+
if (options.contentFile) return readFileSync15(resolvePath11(options.contentFile), "utf-8");
|
|
5796
|
+
return void 0;
|
|
5797
|
+
}
|
|
5798
|
+
function validateCaptureOptions(options) {
|
|
5799
|
+
if (!options.skill) throw new UserError("--skill is required");
|
|
5800
|
+
assertPlainSkillName(options.skill);
|
|
5801
|
+
if (!options.category || !isRefinementCategory(options.category)) {
|
|
5802
|
+
throw new UserError(`--category is required and must be one of: trigger, content, hook, tool, pattern, config, new`);
|
|
5803
|
+
}
|
|
5804
|
+
if (!options.target) throw new UserError("--target is required (heading-path selector or a short description for whole-file override types)");
|
|
5805
|
+
if (!options.overrideType || !isOverrideType(options.overrideType)) {
|
|
5806
|
+
throw new UserError("--override-type is required and must be one of: patch, extend, config, full, hook, script, new");
|
|
5807
|
+
}
|
|
5808
|
+
if (!options.expected) throw new UserError("--expected is required");
|
|
5809
|
+
if (!options.actual) throw new UserError("--actual is required");
|
|
5810
|
+
const scopeRaw = options.scope ?? "local";
|
|
5811
|
+
if (scopeRaw !== "local" && scopeRaw !== "shared") {
|
|
5812
|
+
throw new UserError('--scope must be "local" or "shared"');
|
|
5813
|
+
}
|
|
5814
|
+
let action;
|
|
5815
|
+
if (options.overrideType === "patch") {
|
|
5816
|
+
if (!options.action || !isPatchAction(options.action)) {
|
|
5817
|
+
throw new UserError(
|
|
5818
|
+
"--action is required for --override-type patch (one of: append, prepend, replace-section, insert-after, insert-before, delete-section)"
|
|
5819
|
+
);
|
|
5820
|
+
}
|
|
5821
|
+
action = options.action;
|
|
5822
|
+
}
|
|
5823
|
+
return {
|
|
5824
|
+
skill: options.skill,
|
|
5825
|
+
category: options.category,
|
|
5826
|
+
target: options.target,
|
|
5827
|
+
overrideType: options.overrideType,
|
|
5828
|
+
action,
|
|
5829
|
+
marker: options.marker,
|
|
5830
|
+
expected: options.expected,
|
|
5831
|
+
actual: options.actual,
|
|
5832
|
+
example: options.example,
|
|
5833
|
+
outcome: options.outcome,
|
|
5834
|
+
rootCause: options.rootCause,
|
|
5835
|
+
patternId: options.patternId,
|
|
5836
|
+
scope: scopeRaw,
|
|
5837
|
+
dryRun: options.dryRun ?? false,
|
|
5838
|
+
json: options.json ?? false,
|
|
5839
|
+
yes: options.yes ?? false,
|
|
5840
|
+
handoff: options.handoff ?? false
|
|
5841
|
+
};
|
|
5842
|
+
}
|
|
5843
|
+
function renderPatchActionBlock(params) {
|
|
5844
|
+
const lines = [];
|
|
5845
|
+
lines.push(params.marker ? `<!-- ACTION: ${params.action} "${params.marker}" -->` : `<!-- ACTION: ${params.action} -->`);
|
|
5846
|
+
lines.push(`<!-- TARGET: ${params.target} -->`);
|
|
5847
|
+
lines.push(params.content);
|
|
5848
|
+
return lines.join("\n");
|
|
5849
|
+
}
|
|
5850
|
+
function defaultOverrideFilename(overrideType) {
|
|
5851
|
+
switch (overrideType) {
|
|
5852
|
+
case "full":
|
|
5853
|
+
return "SKILL.md";
|
|
5854
|
+
case "hook":
|
|
5855
|
+
return "hooks/refinement.sh";
|
|
5856
|
+
case "script":
|
|
5857
|
+
return "scripts/refinement.sh";
|
|
5858
|
+
default: {
|
|
5859
|
+
const exhaustive = overrideType;
|
|
5860
|
+
throw new Error(`internal error: defaultOverrideFilename called for non-whole-file override type "${exhaustive}"`);
|
|
5861
|
+
}
|
|
5862
|
+
}
|
|
5863
|
+
}
|
|
5864
|
+
function renderCaptureArtifact(opts, content, refinementId, generatedAt, existingContent) {
|
|
5865
|
+
switch (opts.overrideType) {
|
|
5866
|
+
case "patch": {
|
|
5867
|
+
if (!content) throw new UserError("--content or --content-file is required for --override-type patch");
|
|
5868
|
+
const block = renderPatchActionBlock({ action: opts.action, marker: opts.marker, target: opts.target, content });
|
|
5869
|
+
const preamble = `# SKILL.patch.md
|
|
5870
|
+
# Patches for: ${opts.skill}
|
|
5871
|
+
`;
|
|
5872
|
+
const base = existingContent.trim().length > 0 ? existingContent.trimEnd() : preamble.trimEnd();
|
|
5873
|
+
return { relPath: "SKILL.patch.md", content: `${base}
|
|
5874
|
+
|
|
5875
|
+
${block}
|
|
5876
|
+
` };
|
|
5877
|
+
}
|
|
5878
|
+
case "extend":
|
|
5879
|
+
case "new": {
|
|
5880
|
+
if (!content) throw new UserError(`--content or --content-file is required for --override-type ${opts.overrideType}`);
|
|
5881
|
+
const rendered = renderExtendFile({ skillName: opts.skill, refinementId, generatedAt, targetSection: opts.target, content });
|
|
5882
|
+
const combined = existingContent.trim().length > 0 ? `${existingContent.trimEnd()}
|
|
5883
|
+
|
|
5884
|
+
${rendered}` : rendered;
|
|
5885
|
+
return { relPath: "SKILL.extend.md", content: combined };
|
|
5886
|
+
}
|
|
5887
|
+
case "config": {
|
|
5888
|
+
if (!content) throw new UserError("--content or --content-file (JSON object) is required for --override-type config");
|
|
5889
|
+
let parsed;
|
|
5890
|
+
try {
|
|
5891
|
+
parsed = JSON.parse(content);
|
|
5892
|
+
} catch (err) {
|
|
5893
|
+
throw new UserError(`--content for --override-type config must be valid JSON: ${err.message}`);
|
|
5894
|
+
}
|
|
5895
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
|
|
5896
|
+
throw new UserError("--content for --override-type config must be a JSON object");
|
|
5897
|
+
}
|
|
5898
|
+
const rendered = renderConfigOverride({
|
|
5899
|
+
skillName: opts.skill,
|
|
5900
|
+
refinementId,
|
|
5901
|
+
generatedAt,
|
|
5902
|
+
config: parsed
|
|
5903
|
+
});
|
|
5904
|
+
return { relPath: "skill-config.json", content: rendered };
|
|
5905
|
+
}
|
|
5906
|
+
case "full":
|
|
5907
|
+
case "hook":
|
|
5908
|
+
case "script": {
|
|
5909
|
+
if (!content) throw new UserError(`--content or --content-file is required for --override-type ${opts.overrideType}`);
|
|
5910
|
+
return { relPath: opts.marker && opts.marker.trim().length > 0 ? opts.marker.trim() : defaultOverrideFilename(opts.overrideType), content };
|
|
5911
|
+
}
|
|
5912
|
+
default: {
|
|
5913
|
+
const exhaustive = opts.overrideType;
|
|
5914
|
+
throw new UserError(`unknown override type "${exhaustive}"`);
|
|
5915
|
+
}
|
|
5916
|
+
}
|
|
5917
|
+
}
|
|
5918
|
+
function ensureScopeDirSafe(scopeDir, resolvedCwd) {
|
|
5919
|
+
let lst;
|
|
5920
|
+
try {
|
|
5921
|
+
lst = lstatSync3(scopeDir);
|
|
5922
|
+
} catch {
|
|
5923
|
+
lst = void 0;
|
|
5924
|
+
}
|
|
5925
|
+
if (lst?.isSymbolicLink()) {
|
|
5926
|
+
throw new UserError(`refusing to write under ${scopeDir}: scope directory is a symlink`);
|
|
5927
|
+
}
|
|
5928
|
+
mkdirSync9(scopeDir, { recursive: true });
|
|
5929
|
+
const real = resolveConfiguredRoot3(scopeDir);
|
|
5930
|
+
const lexical = resolvePath11(scopeDir);
|
|
5931
|
+
let realResolved;
|
|
5932
|
+
try {
|
|
5933
|
+
realResolved = realpathSync7(scopeDir);
|
|
5934
|
+
} catch {
|
|
5935
|
+
realResolved = real;
|
|
5936
|
+
}
|
|
5937
|
+
if (realResolved !== lexical) {
|
|
5938
|
+
throw new UserError(`refusing to write under ${scopeDir}: path contains a symlink`);
|
|
5939
|
+
}
|
|
5940
|
+
if (!isUnderRoot3(realResolved, resolveConfiguredRoot3(resolvedCwd))) {
|
|
5941
|
+
throw new UserError(`refusing to write under ${scopeDir}: resolves outside the project root ${resolvedCwd}`);
|
|
5942
|
+
}
|
|
5943
|
+
return realResolved;
|
|
5944
|
+
}
|
|
5945
|
+
function writeOverrideFile(scopeDir, relPath, content, resolvedCwd) {
|
|
5946
|
+
const realScopeDir = ensureScopeDirSafe(scopeDir, resolvedCwd);
|
|
5947
|
+
const destPath = join25(scopeDir, relPath);
|
|
5948
|
+
const destDir = dirname11(destPath);
|
|
5949
|
+
mkdirSync9(destDir, { recursive: true });
|
|
5950
|
+
const realDestDir = realpathSync7(destDir);
|
|
5951
|
+
if (!isUnderRoot3(realDestDir, realScopeDir)) {
|
|
5952
|
+
throw new UserError(`refusing to write ${destPath}: resolves outside scope directory ${scopeDir}`);
|
|
5953
|
+
}
|
|
5954
|
+
let destLst;
|
|
5955
|
+
try {
|
|
5956
|
+
destLst = lstatSync3(destPath);
|
|
5957
|
+
} catch {
|
|
5958
|
+
destLst = void 0;
|
|
5959
|
+
}
|
|
5960
|
+
if (destLst?.isSymbolicLink()) {
|
|
5961
|
+
throw new UserError(`refusing to write ${destPath}: destination is a symlink`);
|
|
5962
|
+
}
|
|
5963
|
+
writeFileSync9(destPath, content, { mode: 384 });
|
|
5964
|
+
chmodSync(destPath, 384);
|
|
5965
|
+
return destPath;
|
|
5966
|
+
}
|
|
5967
|
+
async function refineCaptureCommand(rawOptions = {}) {
|
|
5968
|
+
requireProFeature2();
|
|
5969
|
+
const opts = validateCaptureOptions(rawOptions);
|
|
5970
|
+
const config = loadConfig();
|
|
5971
|
+
const cwd = process.cwd();
|
|
5972
|
+
const scopes = resolveScopes(config, cwd, opts.skill);
|
|
5973
|
+
const scopeDir = opts.scope === "local" ? scopes.local.dir : scopes.shared.dir;
|
|
5974
|
+
const content = resolveContentOption(rawOptions);
|
|
5975
|
+
const now = /* @__PURE__ */ new Date();
|
|
5976
|
+
const existingIds = new Set(readRefinementsStore().records.map((r) => r.id));
|
|
5977
|
+
const refinementId = generateRefinementId(existingIds, now, defaultRandomHex);
|
|
5978
|
+
const preview = renderCaptureArtifactPreview(opts, content, refinementId, now.toISOString(), scopeDir);
|
|
5979
|
+
if (opts.dryRun) {
|
|
5980
|
+
emit(opts.json, { dryRun: true, path: preview.destPath, diff: preview.diff }, () => {
|
|
5981
|
+
console.log(`[skill-forge] refine capture \u2014 ${opts.overrideType} override for "${sanitizeForMarkdown(opts.skill)}" at ${opts.scope} scope`);
|
|
5982
|
+
console.log(sanitizeDiffForConsole(preview.diff));
|
|
5983
|
+
});
|
|
5984
|
+
return;
|
|
5985
|
+
}
|
|
5986
|
+
if (!opts.json) {
|
|
5987
|
+
console.log(`[skill-forge] refine capture \u2014 ${opts.overrideType} override for "${sanitizeForMarkdown(opts.skill)}" at ${opts.scope} scope`);
|
|
5988
|
+
console.log(sanitizeDiffForConsole(preview.diff));
|
|
5989
|
+
}
|
|
5990
|
+
const proceed = await confirmWrite(opts.yes, opts.json, `
|
|
5991
|
+
Write ${preview.destPath}? [y/N] `);
|
|
5992
|
+
if (!proceed) {
|
|
5993
|
+
emit(opts.json, { written: false, cancelled: true, path: preview.destPath }, () => {
|
|
5994
|
+
console.log("[skill-forge] capture cancelled \u2014 nothing written.");
|
|
5995
|
+
});
|
|
5996
|
+
return;
|
|
5997
|
+
}
|
|
5998
|
+
const writtenPath = writeOverrideFile(scopeDir, preview.relPath, preview.content, cwd);
|
|
5999
|
+
let result;
|
|
6000
|
+
try {
|
|
6001
|
+
result = recordRefinement(
|
|
6002
|
+
{
|
|
6003
|
+
skill: opts.skill,
|
|
6004
|
+
category: opts.category,
|
|
6005
|
+
overrideType: opts.overrideType,
|
|
6006
|
+
targetSection: opts.target,
|
|
6007
|
+
expectedBehavior: opts.expected,
|
|
6008
|
+
actualBehavior: opts.actual,
|
|
6009
|
+
example: opts.example,
|
|
6010
|
+
desiredOutcome: opts.outcome,
|
|
6011
|
+
rootCause: opts.rootCause,
|
|
6012
|
+
project: cwd,
|
|
6013
|
+
projectRoot: cwd,
|
|
6014
|
+
patchAction: opts.action,
|
|
6015
|
+
patchMarker: opts.marker,
|
|
6016
|
+
patchContent: content,
|
|
6017
|
+
status: "applied",
|
|
6018
|
+
scope: opts.scope,
|
|
6019
|
+
patternId: opts.patternId
|
|
6020
|
+
},
|
|
6021
|
+
{ now, id: refinementId }
|
|
6022
|
+
);
|
|
6023
|
+
} catch (err) {
|
|
6024
|
+
throw new UserError(`wrote the override artifact at ${writtenPath}, but recording the refinement in the store failed: ${err.message}`);
|
|
6025
|
+
}
|
|
6026
|
+
emit(
|
|
6027
|
+
opts.json,
|
|
6028
|
+
{ written: true, path: writtenPath, refinementId: result.refinement.id, patternId: result.pattern.id, patternBecameReady: result.patternBecameReady },
|
|
6029
|
+
() => {
|
|
6030
|
+
console.log(`[skill-forge] wrote ${writtenPath}`);
|
|
6031
|
+
console.log(`[skill-forge] recorded refinement ${result.refinement.id} -> pattern ${result.pattern.id} (status: ${result.pattern.status})`);
|
|
6032
|
+
if (result.patternBecameReady) {
|
|
6033
|
+
console.log(`[skill-forge] pattern ${result.pattern.id} is now READY (seen in a second project) \u2014 consider \`skill-forge refine promote ${result.pattern.id}\``);
|
|
6034
|
+
}
|
|
6035
|
+
}
|
|
6036
|
+
);
|
|
6037
|
+
if (opts.handoff) {
|
|
6038
|
+
runHandoff(config, writtenPath, { promptAsset: REFINE_PROMPT_ASSET, log: opts.json ? console.error : console.log });
|
|
6039
|
+
}
|
|
6040
|
+
}
|
|
6041
|
+
function renderCaptureArtifactPreview(opts, content, refinementId, generatedAt, scopeDir) {
|
|
6042
|
+
const probeRelPath = opts.overrideType === "patch" ? "SKILL.patch.md" : opts.overrideType === "extend" || opts.overrideType === "new" ? "SKILL.extend.md" : void 0;
|
|
6043
|
+
const probePath = probeRelPath ? join25(scopeDir, probeRelPath) : void 0;
|
|
6044
|
+
const existingContent = probePath && existsSync20(probePath) ? readFileSync15(probePath, "utf-8") : "";
|
|
6045
|
+
const rendered = renderCaptureArtifact(opts, content, refinementId, generatedAt, existingContent);
|
|
6046
|
+
const relPath = rendered.relPath;
|
|
6047
|
+
const destPath = join25(scopeDir, relPath);
|
|
6048
|
+
const before = existsSync20(destPath) ? readFileSync15(destPath, "utf-8") : "";
|
|
6049
|
+
const diff = previewDiff(before, rendered.content, destPath);
|
|
6050
|
+
return { relPath, content: rendered.content, destPath, diff };
|
|
6051
|
+
}
|
|
6052
|
+
async function refineListCommand(options = {}) {
|
|
6053
|
+
requireProFeature2();
|
|
6054
|
+
const { records } = readRefinementsStore();
|
|
6055
|
+
let rows = records;
|
|
6056
|
+
if (options.skill) rows = rows.filter((r) => r.skill === options.skill);
|
|
6057
|
+
if (options.project) rows = rows.filter((r) => r.project === options.project || r.projectRoot === resolvePath11(options.project));
|
|
6058
|
+
if (options.status) {
|
|
6059
|
+
if (!isRefinementStatus(options.status)) {
|
|
6060
|
+
throw new UserError("--status must be one of: pending, applied, generalized");
|
|
6061
|
+
}
|
|
6062
|
+
rows = rows.filter((r) => r.status === options.status);
|
|
6063
|
+
}
|
|
6064
|
+
const filtered = Boolean(options.skill || options.project || options.status);
|
|
6065
|
+
emit(options.json, { records: rows }, () => {
|
|
6066
|
+
if (rows.length === 0) {
|
|
6067
|
+
if (filtered && records.length > 0) {
|
|
6068
|
+
console.log(`[skill-forge] no refinements match the given filter(s) (${records.length} total recorded).`);
|
|
6069
|
+
} else {
|
|
6070
|
+
console.log("[skill-forge] no refinements recorded.");
|
|
6071
|
+
}
|
|
6072
|
+
return;
|
|
6073
|
+
}
|
|
6074
|
+
console.log(`[skill-forge] ${rows.length} refinement(s):`);
|
|
6075
|
+
for (const r of rows) {
|
|
6076
|
+
console.log(
|
|
6077
|
+
` ${r.id} ${sanitizeForMarkdown(r.skill)} [${r.category}/${r.overrideType}] status=${r.status} pattern=${r.patternId ?? "(none)"} ${r.createdAt}`
|
|
6078
|
+
);
|
|
6079
|
+
}
|
|
6080
|
+
});
|
|
6081
|
+
}
|
|
6082
|
+
async function refinePatternsCommand(options = {}) {
|
|
6083
|
+
requireProFeature2();
|
|
6084
|
+
const { records } = readPatternsStore();
|
|
6085
|
+
let rows = records;
|
|
6086
|
+
if (options.status) {
|
|
6087
|
+
if (!isPatternStatus(options.status)) {
|
|
6088
|
+
throw new UserError("--status must be one of: tracking, ready, generalized, dismissed");
|
|
6089
|
+
}
|
|
6090
|
+
rows = rows.filter((p) => p.status === options.status);
|
|
6091
|
+
}
|
|
6092
|
+
if (options.skill) rows = rows.filter((p) => p.affectedSkills.includes(options.skill));
|
|
6093
|
+
if (options.project) {
|
|
6094
|
+
const resolvedProject = resolvePath11(options.project);
|
|
6095
|
+
rows = rows.filter((p) => p.occurrences.some((o) => o.projectRealpath === resolvedProject));
|
|
6096
|
+
}
|
|
6097
|
+
const filtered = Boolean(options.skill || options.project || options.status);
|
|
6098
|
+
emit(options.json, { records: rows.map((p) => ({ ...p, count: patternCount(p) })) }, () => {
|
|
6099
|
+
if (rows.length === 0) {
|
|
6100
|
+
if (filtered && records.length > 0) {
|
|
6101
|
+
console.log(`[skill-forge] no patterns match the given filter(s) (${records.length} total recorded).`);
|
|
6102
|
+
} else {
|
|
6103
|
+
console.log("[skill-forge] no patterns recorded.");
|
|
6104
|
+
}
|
|
6105
|
+
return;
|
|
6106
|
+
}
|
|
6107
|
+
console.log(`[skill-forge] ${rows.length} pattern(s):`);
|
|
6108
|
+
for (const p of rows) {
|
|
6109
|
+
console.log(
|
|
6110
|
+
` ${p.id} ${sanitizeForMarkdown(p.name)} status=${p.status} count=${patternCount(p)} skills=${p.affectedSkills.map(sanitizeForMarkdown).join(", ")}`
|
|
6111
|
+
);
|
|
6112
|
+
}
|
|
6113
|
+
});
|
|
6114
|
+
}
|
|
6115
|
+
function renderInventoryLine(row) {
|
|
6116
|
+
const present = [
|
|
6117
|
+
row.skillMd ? "SKILL.md" : void 0,
|
|
6118
|
+
row.patchMd ? "SKILL.patch.md" : void 0,
|
|
6119
|
+
row.extendMd ? "SKILL.extend.md" : void 0,
|
|
6120
|
+
row.configJson ? "skill-config.json" : void 0,
|
|
6121
|
+
row.hooksDir ? "hooks/" : void 0,
|
|
6122
|
+
row.scriptsDir ? "scripts/" : void 0
|
|
6123
|
+
].filter((v) => Boolean(v));
|
|
6124
|
+
return ` [${row.scope}] ${row.dir}${row.dirExists ? "" : " (missing)"} \u2014 ${present.length > 0 ? present.join(", ") : "(no override files)"}`;
|
|
6125
|
+
}
|
|
6126
|
+
async function refineWhichCommand(skill, options = {}) {
|
|
6127
|
+
requireProFeature2();
|
|
6128
|
+
if (!skill) throw new UserError("a skill name is required: skill-forge refine which <skill>");
|
|
6129
|
+
const config = loadConfig();
|
|
6130
|
+
const cwd = process.cwd();
|
|
6131
|
+
const scopes = resolveScopes(config, cwd, skill, options.userRoot);
|
|
6132
|
+
const inventory = inventoryAllScopes(scopes);
|
|
6133
|
+
emit(options.json, { scopes, inventory }, () => {
|
|
6134
|
+
console.log(`[skill-forge] refine which "${sanitizeForMarkdown(skill)}" \u2014 resolution order (highest precedence first):`);
|
|
6135
|
+
for (const row of inventory) console.log(renderInventoryLine(row));
|
|
6136
|
+
if (!scopes.user.resolved) {
|
|
6137
|
+
console.log(
|
|
6138
|
+
` [user] ambiguous \u2014 candidates: ${scopes.user.candidates.map((c) => c.root).join(", ") || "(none configured)"} (pass --user-root to disambiguate)`
|
|
6139
|
+
);
|
|
6140
|
+
}
|
|
6141
|
+
});
|
|
6142
|
+
}
|
|
6143
|
+
function writePostWriteSidecar(backupId, files) {
|
|
6144
|
+
const sidecar = { schemaVersion: 1, files };
|
|
6145
|
+
const dir = join25(getBackupsDir(), backupId);
|
|
6146
|
+
mkdirSync9(dir, { recursive: true });
|
|
6147
|
+
writeFileSync9(join25(dir, "post.json"), JSON.stringify(sidecar, null, 2) + "\n", { mode: 384 });
|
|
6148
|
+
}
|
|
6149
|
+
function readPostWriteSidecar(backupId) {
|
|
6150
|
+
const path = join25(getBackupsDir(), backupId, "post.json");
|
|
6151
|
+
if (!existsSync20(path)) return { hashes: /* @__PURE__ */ new Map(), status: "missing" };
|
|
6152
|
+
let raw;
|
|
6153
|
+
try {
|
|
6154
|
+
raw = readFileSync15(path, "utf-8");
|
|
6155
|
+
} catch {
|
|
6156
|
+
return { hashes: /* @__PURE__ */ new Map(), status: "unreadable" };
|
|
6157
|
+
}
|
|
6158
|
+
const hashes = /* @__PURE__ */ new Map();
|
|
6159
|
+
try {
|
|
6160
|
+
const parsed = JSON.parse(raw);
|
|
6161
|
+
for (const f of parsed.files ?? []) {
|
|
6162
|
+
if (typeof f !== "object" || f === null || typeof f.path !== "string" || typeof f.sha256 !== "string") {
|
|
6163
|
+
throw new Error("malformed post.json files entry");
|
|
6164
|
+
}
|
|
6165
|
+
hashes.set(f.path, f.sha256);
|
|
6166
|
+
}
|
|
6167
|
+
} catch {
|
|
6168
|
+
return { hashes: /* @__PURE__ */ new Map(), status: "corrupt" };
|
|
6169
|
+
}
|
|
6170
|
+
return { hashes, status: "ok" };
|
|
6171
|
+
}
|
|
6172
|
+
async function refinePromoteCommand(patternId, options = {}) {
|
|
6173
|
+
requireProFeature2();
|
|
6174
|
+
if (!patternId) throw new UserError("a pattern id is required: skill-forge refine promote <pattern-id>");
|
|
6175
|
+
const patternsStore = readPatternsStore();
|
|
6176
|
+
const pattern = patternsStore.records.find((p) => p.id === patternId);
|
|
6177
|
+
if (!pattern) throw new UserError(`no pattern with id: ${patternId}`);
|
|
6178
|
+
if (pattern.status === "generalized" && !options.force) {
|
|
6179
|
+
throw new UserError(`pattern ${patternId} is already generalized (backup ${pattern.generalizedBackupId ?? "(unknown)"}) \u2014 pass --force to re-promote.`);
|
|
6180
|
+
}
|
|
6181
|
+
if (pattern.status !== "ready" && pattern.status !== "generalized" && !options.force) {
|
|
6182
|
+
throw new UserError(`pattern ${patternId} is not ready (status: ${pattern.status}) \u2014 pass --force to promote anyway.`);
|
|
6183
|
+
}
|
|
6184
|
+
const skill = pattern.affectedSkills[0];
|
|
6185
|
+
if (!skill) throw new UserError(`pattern ${patternId} has no affected skills recorded`);
|
|
6186
|
+
assertPlainSkillName(skill);
|
|
6187
|
+
const config = loadConfig();
|
|
6188
|
+
const cwd = process.cwd();
|
|
6189
|
+
const scopes = resolveScopes(config, cwd, skill, options.userRoot);
|
|
6190
|
+
if (!scopes.user.resolved) {
|
|
6191
|
+
throw new UserError(
|
|
6192
|
+
`cannot resolve a single user scope for skill "${skill}" \u2014 candidates: ${scopes.user.candidates.map((c) => c.root).join(", ") || "(none configured)"}. Pass --user-root to disambiguate.`
|
|
6193
|
+
);
|
|
6194
|
+
}
|
|
6195
|
+
const skillDir = scopes.user.dir;
|
|
6196
|
+
const resolvedUserRoot = resolveConfiguredRoot3(scopes.user.root);
|
|
6197
|
+
let realUserRoot;
|
|
6198
|
+
try {
|
|
6199
|
+
realUserRoot = realpathSync7(resolvedUserRoot);
|
|
6200
|
+
} catch {
|
|
6201
|
+
realUserRoot = resolvedUserRoot;
|
|
6202
|
+
}
|
|
6203
|
+
let realSkillDir;
|
|
6204
|
+
try {
|
|
6205
|
+
realSkillDir = realpathSync7(skillDir);
|
|
6206
|
+
} catch {
|
|
6207
|
+
realSkillDir = resolvePath11(skillDir);
|
|
6208
|
+
}
|
|
6209
|
+
if (!isUnderRoot3(realSkillDir, realUserRoot)) {
|
|
6210
|
+
throw new UserError(`refusing to promote: resolved skill directory ${skillDir} is outside the configured user scope root ${scopes.user.root}`);
|
|
6211
|
+
}
|
|
6212
|
+
const skillMdPath = join25(skillDir, "SKILL.md");
|
|
6213
|
+
if (!existsSync20(skillMdPath)) {
|
|
6214
|
+
throw new UserError(`user-scope SKILL.md not found at ${skillMdPath} \u2014 refusing to promote against a missing base skill.`);
|
|
6215
|
+
}
|
|
6216
|
+
const refinementsStore = readRefinementsStore();
|
|
6217
|
+
const patternRefinements = refinementsStore.records.filter((r) => pattern.refinementIds.includes(r.id));
|
|
6218
|
+
const representative = patternRefinements[patternRefinements.length - 1];
|
|
6219
|
+
if (!representative) throw new UserError(`pattern ${patternId} has no recorded refinements to promote`);
|
|
6220
|
+
let block;
|
|
6221
|
+
if (representative.overrideType === "patch" && representative.patchAction && representative.patchContent) {
|
|
6222
|
+
block = { action: representative.patchAction, marker: representative.patchMarker, target: representative.targetSection, content: representative.patchContent };
|
|
6223
|
+
} else if ((representative.overrideType === "extend" || representative.overrideType === "new") && representative.patchContent) {
|
|
6224
|
+
block = { action: "append", target: representative.targetSection, content: representative.patchContent };
|
|
6225
|
+
} else {
|
|
6226
|
+
throw new UserError(
|
|
6227
|
+
`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.`
|
|
6228
|
+
);
|
|
6229
|
+
}
|
|
6230
|
+
const before = readFileSync15(skillMdPath, "utf-8");
|
|
6231
|
+
const after = applyPatchBlocks(before, [block]);
|
|
6232
|
+
const diff = previewDiff(before, after, skillMdPath);
|
|
6233
|
+
if (options.dryRun) {
|
|
6234
|
+
emit(options.json, { dryRun: true, path: skillMdPath, diff }, () => {
|
|
6235
|
+
console.log(`[skill-forge] refine promote ${patternId} -> ${skillMdPath}`);
|
|
6236
|
+
console.log(sanitizeDiffForConsole(diff));
|
|
6237
|
+
});
|
|
6238
|
+
return;
|
|
6239
|
+
}
|
|
6240
|
+
if (!options.json) {
|
|
6241
|
+
console.log(`[skill-forge] refine promote ${patternId} -> ${skillMdPath}`);
|
|
6242
|
+
console.log(sanitizeDiffForConsole(diff));
|
|
6243
|
+
}
|
|
6244
|
+
const proceed = await confirmWrite(options.yes, options.json, `
|
|
6245
|
+
Promote pattern ${patternId} into ${skillMdPath}? [y/N] `);
|
|
6246
|
+
if (!proceed) {
|
|
6247
|
+
emit(options.json, { promoted: false, cancelled: true }, () => {
|
|
6248
|
+
console.log("[skill-forge] promote cancelled \u2014 nothing written.");
|
|
6249
|
+
});
|
|
6250
|
+
return;
|
|
6251
|
+
}
|
|
6252
|
+
const backup = createBackup([skillMdPath]);
|
|
6253
|
+
const tempPath = `${skillMdPath}.tmp-${randomUUID4()}`;
|
|
6254
|
+
try {
|
|
6255
|
+
writeFileSync9(tempPath, after, "utf-8");
|
|
6256
|
+
renameSync5(tempPath, skillMdPath);
|
|
6257
|
+
} catch (err) {
|
|
6258
|
+
rmSync7(tempPath, { force: true });
|
|
6259
|
+
throw err;
|
|
6260
|
+
}
|
|
6261
|
+
try {
|
|
6262
|
+
const postSha256 = createHash3("sha256").update(readFileSync15(skillMdPath)).digest("hex");
|
|
6263
|
+
writePostWriteSidecar(backup.id, [{ path: skillMdPath, sha256: postSha256 }]);
|
|
6264
|
+
markGeneralized(patternId, backup.id);
|
|
6265
|
+
const skillsRoot = resolveConfiguredRoot3(scopes.user.root);
|
|
6266
|
+
appendProvenance(
|
|
6267
|
+
{
|
|
6268
|
+
name: skill,
|
|
6269
|
+
source: `skill-forge refine (${patternId})`,
|
|
6270
|
+
verb: "DEFER",
|
|
6271
|
+
target: skillMdPath,
|
|
6272
|
+
took: `applied a ${block.action} patch materializing pattern ${patternId}`,
|
|
6273
|
+
notes: `generalized from ${patternId} via skill-forge refine`
|
|
6274
|
+
},
|
|
6275
|
+
skillsRoot
|
|
6276
|
+
);
|
|
6277
|
+
} catch (err) {
|
|
6278
|
+
throw new UserError(
|
|
6279
|
+
`the user-scope skill was updated at ${skillMdPath} (backup ${backup.id}), but recording post-write verification and/or pattern/provenance state failed: ${err.message}`
|
|
6280
|
+
);
|
|
6281
|
+
}
|
|
6282
|
+
emit(options.json, { promoted: true, path: skillMdPath, backupId: backup.id }, () => {
|
|
6283
|
+
console.log(`[skill-forge] promoted pattern ${patternId} -> ${skillMdPath} (backup: ${backup.id})`);
|
|
6284
|
+
});
|
|
6285
|
+
}
|
|
6286
|
+
function assertBackupDirContained(backupId) {
|
|
6287
|
+
const backupsDir = getBackupsDir();
|
|
6288
|
+
if (!existsSync20(backupsDir)) return;
|
|
6289
|
+
const realBackupsDir = realpathSync7(backupsDir);
|
|
6290
|
+
const backupDir = join25(backupsDir, backupId);
|
|
6291
|
+
if (!existsSync20(backupDir)) return;
|
|
6292
|
+
const realBackupDir = realpathSync7(backupDir);
|
|
6293
|
+
if (!isUnderRoot3(realBackupDir, realBackupsDir)) {
|
|
6294
|
+
throw new UserError(`refusing rollback: backup id "${backupId}" resolves outside the backups directory`);
|
|
6295
|
+
}
|
|
6296
|
+
}
|
|
6297
|
+
function resolveTargetRealPath(path) {
|
|
6298
|
+
let lexicalAncestor = resolvePath11(path);
|
|
6299
|
+
while (true) {
|
|
6300
|
+
try {
|
|
6301
|
+
const real = realpathSync7(lexicalAncestor);
|
|
6302
|
+
const remainder = relative3(lexicalAncestor, resolvePath11(path));
|
|
6303
|
+
return remainder === "" ? real : join25(real, remainder);
|
|
6304
|
+
} catch {
|
|
6305
|
+
const parent = dirname11(lexicalAncestor);
|
|
6306
|
+
if (parent === lexicalAncestor) return resolvePath11(path);
|
|
6307
|
+
lexicalAncestor = parent;
|
|
6308
|
+
}
|
|
6309
|
+
}
|
|
6310
|
+
}
|
|
6311
|
+
function assertManifestPathsWithinRoots(manifest, config) {
|
|
6312
|
+
if (config.skillsRoots.length === 0) {
|
|
6313
|
+
throw new UserError(`refusing rollback ${manifest.id}: no skillsRoots are configured \u2014 cannot verify the backup's file paths are within a trusted skills root.`);
|
|
6314
|
+
}
|
|
6315
|
+
const roots = config.skillsRoots.map((root) => {
|
|
6316
|
+
const resolved = resolveConfiguredRoot3(root);
|
|
6317
|
+
try {
|
|
6318
|
+
return realpathSync7(resolved);
|
|
6319
|
+
} catch {
|
|
6320
|
+
return resolved;
|
|
6321
|
+
}
|
|
6322
|
+
});
|
|
6323
|
+
for (const file of manifest.files) {
|
|
6324
|
+
const resolvedTarget = resolveTargetRealPath(file.path);
|
|
6325
|
+
const withinAnyRoot = roots.some((root) => isUnderRoot3(resolvedTarget, root));
|
|
6326
|
+
if (!withinAnyRoot) {
|
|
6327
|
+
throw new UserError(`refusing rollback ${manifest.id}: backup manifest references a path outside configured skills roots \u2014 ${file.path}`);
|
|
6328
|
+
}
|
|
6329
|
+
}
|
|
6330
|
+
}
|
|
6331
|
+
function assertNoSymlinkLeaves(manifest) {
|
|
6332
|
+
for (const file of manifest.files) {
|
|
6333
|
+
let lst;
|
|
6334
|
+
try {
|
|
6335
|
+
lst = lstatSync3(file.path);
|
|
6336
|
+
} catch {
|
|
6337
|
+
lst = void 0;
|
|
6338
|
+
}
|
|
6339
|
+
if (lst?.isSymbolicLink()) {
|
|
6340
|
+
throw new UserError(`refusing rollback ${manifest.id}: backup manifest path is currently a symlink \u2014 refusing to follow or overwrite it: ${file.path}`);
|
|
6341
|
+
}
|
|
6342
|
+
}
|
|
6343
|
+
}
|
|
6344
|
+
function findLexicalRoot(filePath, config) {
|
|
6345
|
+
const lexicalTarget = resolvePath11(filePath);
|
|
6346
|
+
for (const root of config.skillsRoots) {
|
|
6347
|
+
const rootLexical = resolveConfiguredRoot3(root);
|
|
6348
|
+
const rel = relative3(rootLexical, lexicalTarget);
|
|
6349
|
+
if (rel !== "" && (rel === ".." || rel.startsWith(`..${sep2}`) || isAbsolute3(rel))) continue;
|
|
6350
|
+
let rootReal;
|
|
6351
|
+
try {
|
|
6352
|
+
rootReal = realpathSync7(rootLexical);
|
|
6353
|
+
} catch {
|
|
6354
|
+
rootReal = rootLexical;
|
|
6355
|
+
}
|
|
6356
|
+
return { rootLexical, rootReal };
|
|
6357
|
+
}
|
|
6358
|
+
throw new UserError(`refusing to operate on ${filePath}: does not resolve lexically under any configured skills root`);
|
|
6359
|
+
}
|
|
6360
|
+
function walkRealComponents(filePath, config, createMissing) {
|
|
6361
|
+
const { rootLexical, rootReal } = findLexicalRoot(filePath, config);
|
|
6362
|
+
const relPath = relative3(rootLexical, resolvePath11(filePath));
|
|
6363
|
+
const components = relPath.split(sep2).filter((c) => c.length > 0);
|
|
6364
|
+
if (components.length === 0) {
|
|
6365
|
+
throw new UserError(`refusing to operate on ${filePath}: resolves to the skills root itself`);
|
|
6366
|
+
}
|
|
6367
|
+
const leafComponent = components[components.length - 1];
|
|
6368
|
+
const dirComponents = components.slice(0, -1);
|
|
6369
|
+
let current = rootReal;
|
|
6370
|
+
for (const component of dirComponents) {
|
|
6371
|
+
if (current === void 0) break;
|
|
6372
|
+
const next = join25(current, component);
|
|
6373
|
+
let lst;
|
|
6374
|
+
try {
|
|
6375
|
+
lst = lstatSync3(next);
|
|
6376
|
+
} catch {
|
|
6377
|
+
lst = void 0;
|
|
6378
|
+
}
|
|
6379
|
+
if (lst === void 0) {
|
|
6380
|
+
if (!createMissing) {
|
|
6381
|
+
current = void 0;
|
|
6382
|
+
continue;
|
|
6383
|
+
}
|
|
6384
|
+
mkdirSync9(next);
|
|
6385
|
+
current = next;
|
|
6386
|
+
continue;
|
|
6387
|
+
}
|
|
6388
|
+
if (lst.isSymbolicLink()) {
|
|
6389
|
+
throw new UserError(`refusing to operate on ${filePath}: intermediate path component is a symlink \u2014 ${next}`);
|
|
6390
|
+
}
|
|
6391
|
+
if (!lst.isDirectory()) {
|
|
6392
|
+
throw new UserError(`refusing to operate on ${filePath}: intermediate path component is not a directory \u2014 ${next}`);
|
|
6393
|
+
}
|
|
6394
|
+
current = next;
|
|
6395
|
+
}
|
|
6396
|
+
return { parentReal: current, leafComponent, rootReal };
|
|
6397
|
+
}
|
|
6398
|
+
function safeRestoreWrite(filePath, contentBuffer, mode, config) {
|
|
6399
|
+
const { parentReal, leafComponent, rootReal } = walkRealComponents(filePath, config, true);
|
|
6400
|
+
if (parentReal === void 0) {
|
|
6401
|
+
throw new UserError(`refusing to restore ${filePath}: could not establish a real parent directory`);
|
|
6402
|
+
}
|
|
6403
|
+
const finalPath = join25(parentReal, leafComponent);
|
|
6404
|
+
if (!isUnderRoot3(finalPath, rootReal)) {
|
|
6405
|
+
throw new UserError(`refusing to restore ${filePath}: resolves outside root ${rootReal}`);
|
|
6406
|
+
}
|
|
6407
|
+
let leafLst;
|
|
6408
|
+
try {
|
|
6409
|
+
leafLst = lstatSync3(finalPath);
|
|
6410
|
+
} catch {
|
|
6411
|
+
leafLst = void 0;
|
|
6412
|
+
}
|
|
6413
|
+
if (leafLst?.isSymbolicLink()) {
|
|
6414
|
+
throw new UserError(`refusing to restore ${filePath}: it is now a symlink \u2014 refusing to follow or overwrite it.`);
|
|
6415
|
+
}
|
|
6416
|
+
const tempPath = `${finalPath}.tmp-${randomUUID4()}`;
|
|
6417
|
+
try {
|
|
6418
|
+
writeFileSync9(tempPath, contentBuffer);
|
|
6419
|
+
chmodSync(tempPath, mode);
|
|
6420
|
+
renameSync5(tempPath, finalPath);
|
|
6421
|
+
} catch (err) {
|
|
6422
|
+
rmSync7(tempPath, { force: true });
|
|
6423
|
+
throw err;
|
|
6424
|
+
}
|
|
6425
|
+
}
|
|
6426
|
+
function safeDeleteTombstone(filePath, config) {
|
|
6427
|
+
const { parentReal, leafComponent, rootReal } = walkRealComponents(filePath, config, false);
|
|
6428
|
+
if (parentReal === void 0) return;
|
|
6429
|
+
const finalPath = join25(parentReal, leafComponent);
|
|
6430
|
+
if (!isUnderRoot3(finalPath, rootReal)) {
|
|
6431
|
+
throw new UserError(`refusing to delete ${filePath}: resolves outside root ${rootReal}`);
|
|
6432
|
+
}
|
|
6433
|
+
let leafLst;
|
|
6434
|
+
try {
|
|
6435
|
+
leafLst = lstatSync3(finalPath);
|
|
6436
|
+
} catch {
|
|
6437
|
+
leafLst = void 0;
|
|
6438
|
+
}
|
|
6439
|
+
if (leafLst === void 0) return;
|
|
6440
|
+
if (leafLst.isSymbolicLink()) {
|
|
6441
|
+
throw new UserError(`refusing to delete ${filePath}: it is now a symlink \u2014 refusing to follow or overwrite it.`);
|
|
6442
|
+
}
|
|
6443
|
+
rmSync7(finalPath, { force: true });
|
|
6444
|
+
}
|
|
6445
|
+
async function refineRollbackCommand(backupId, options = {}) {
|
|
6446
|
+
requireProFeature2();
|
|
6447
|
+
if (!backupId) throw new UserError("a backup id is required: skill-forge refine rollback <backup-id>");
|
|
6448
|
+
if (!isValidBackupId(backupId)) {
|
|
6449
|
+
throw new UserError(`invalid backup id "${backupId}" \u2014 expected the format produced by \`skill-forge refine promote\` (e.g. 20260720T142305Z-a1b2)`);
|
|
6450
|
+
}
|
|
6451
|
+
assertBackupDirContained(backupId);
|
|
6452
|
+
const manifest = readBackupManifest(backupId);
|
|
6453
|
+
const config = loadConfig();
|
|
6454
|
+
assertManifestPathsWithinRoots(manifest, config);
|
|
6455
|
+
assertNoSymlinkLeaves(manifest);
|
|
6456
|
+
const warnings = [];
|
|
6457
|
+
const sidecar = readPostWriteSidecar(backupId);
|
|
6458
|
+
if (sidecar.status !== "ok") {
|
|
6459
|
+
const reason = sidecar.status;
|
|
6460
|
+
if (!options.force) {
|
|
6461
|
+
throw new UserError(
|
|
6462
|
+
`refusing rollback ${backupId}: post-write verification data (post.json) is ${reason} \u2014 cannot confirm the current file state matches what was promoted. Pass --force to override.`
|
|
6463
|
+
);
|
|
6464
|
+
}
|
|
6465
|
+
warnings.push(`post-write verification data for backup ${backupId} is ${reason} \u2014 restoring without hash verification (--force).`);
|
|
6466
|
+
}
|
|
6467
|
+
const mismatches = [];
|
|
6468
|
+
for (const file of manifest.files) {
|
|
6469
|
+
const expected = sidecar.hashes.get(file.path);
|
|
6470
|
+
if (expected === void 0) continue;
|
|
6471
|
+
const currentSha256 = existsSync20(file.path) ? createHash3("sha256").update(readFileSync15(file.path)).digest("hex") : null;
|
|
6472
|
+
if (currentSha256 !== expected) mismatches.push(file.path);
|
|
6473
|
+
}
|
|
6474
|
+
if (mismatches.length > 0 && !options.force) {
|
|
6475
|
+
throw new UserError(
|
|
6476
|
+
`refusing rollback ${backupId}: current file(s) differ from the state recorded right after promote \u2014 ${mismatches.join(", ")} (pass --force to override)`
|
|
6477
|
+
);
|
|
6478
|
+
}
|
|
6479
|
+
const fileList = manifest.files.map((file) => ` ${file.existed ? "restore" : "delete"} ${file.path}`).join("\n");
|
|
6480
|
+
if (!options.json) {
|
|
6481
|
+
console.log(`[skill-forge] refine rollback ${backupId} \u2014 restoring ${manifest.files.length} file(s):`);
|
|
6482
|
+
console.log(fileList);
|
|
6483
|
+
for (const w of warnings) console.log(`[skill-forge] warning: ${w}`);
|
|
6484
|
+
}
|
|
6485
|
+
const proceed = await confirmWrite(options.yes, options.json, `
|
|
6486
|
+
Roll back backup ${backupId}? Affected files:
|
|
6487
|
+
${fileList}
|
|
6488
|
+
[y/N] `);
|
|
6489
|
+
if (!proceed) {
|
|
6490
|
+
emit(options.json, { rolledBack: false, cancelled: true, backupId }, () => {
|
|
6491
|
+
console.log("[skill-forge] rollback cancelled \u2014 nothing restored.");
|
|
6492
|
+
});
|
|
6493
|
+
return;
|
|
6494
|
+
}
|
|
6495
|
+
const restored = [];
|
|
6496
|
+
for (const file of manifest.files) {
|
|
6497
|
+
try {
|
|
6498
|
+
if (!file.existed) {
|
|
6499
|
+
safeDeleteTombstone(file.path, config);
|
|
6500
|
+
} else {
|
|
6501
|
+
safeRestoreWrite(file.path, Buffer.from(file.contentBase64 ?? "", "base64"), file.mode ?? 420, config);
|
|
6502
|
+
}
|
|
6503
|
+
restored.push(file.path);
|
|
6504
|
+
} catch (err) {
|
|
6505
|
+
const remaining = manifest.files.slice(restored.length + 1).map((f) => f.path);
|
|
6506
|
+
throw new UserError(
|
|
6507
|
+
`rollback ${backupId} failed partway through: restored ${restored.length} of ${manifest.files.length} file(s) (${restored.join(", ") || "(none)"}), then failed on ${file.path}: ${err.message}. NOT restored: ${[file.path, ...remaining].join(", ")}.`
|
|
6508
|
+
);
|
|
6509
|
+
}
|
|
6510
|
+
}
|
|
6511
|
+
const patternsStore = readPatternsStore();
|
|
6512
|
+
const pattern = patternsStore.records.find((p) => p.generalizedBackupId === backupId);
|
|
6513
|
+
if (pattern) {
|
|
6514
|
+
pattern.status = "ready";
|
|
6515
|
+
pattern.generalizedAt = void 0;
|
|
6516
|
+
pattern.generalizedBackupId = void 0;
|
|
6517
|
+
writePatternsStore(patternsStore);
|
|
6518
|
+
}
|
|
6519
|
+
emit(options.json, { rolledBack: true, backupId, patternReset: pattern?.id, warnings }, () => {
|
|
6520
|
+
console.log(`[skill-forge] rollback complete for ${backupId}${pattern ? ` (pattern ${pattern.id} reset to ready)` : ""}`);
|
|
6521
|
+
for (const w of warnings) console.log(`[skill-forge] warning: ${w}`);
|
|
6522
|
+
});
|
|
6523
|
+
}
|
|
6524
|
+
|
|
6525
|
+
// src/cli.ts
|
|
6526
|
+
var require2 = createRequire(import.meta.url);
|
|
6527
|
+
var pkg = require2("../package.json");
|
|
6528
|
+
function parseArtifact(value) {
|
|
6529
|
+
if (value !== "skill" && value !== "mcp") {
|
|
6530
|
+
throw new UserError(`--artifact must be "skill" or "mcp" (got "${value}")`);
|
|
6531
|
+
}
|
|
6532
|
+
return value;
|
|
6533
|
+
}
|
|
6534
|
+
function parseLookbackHours(value) {
|
|
6535
|
+
const n = Number(value);
|
|
6536
|
+
if (value.trim() === "" || !Number.isFinite(n) || n < 0) {
|
|
6537
|
+
throw new UserError(`--lookback-hours must be a non-negative number (got "${value}")`);
|
|
6538
|
+
}
|
|
6539
|
+
return n;
|
|
6540
|
+
}
|
|
6541
|
+
function parseLimit(value) {
|
|
6542
|
+
const n = Number(value);
|
|
6543
|
+
if (value.trim() === "" || !Number.isInteger(n) || n < 1 || n > 50) {
|
|
6544
|
+
throw new UserError(`--limit must be an integer between 1 and 50 (got "${value}")`);
|
|
3990
6545
|
}
|
|
3991
6546
|
return n;
|
|
3992
6547
|
}
|
|
@@ -4001,6 +6556,21 @@ program.command("list").description("List skills currently in quarantine").actio
|
|
|
4001
6556
|
program.command("status").description("Show skill-forge configuration and quarantine status").action(status);
|
|
4002
6557
|
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);
|
|
4003
6558
|
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").action(auditCommand);
|
|
6559
|
+
program.command("organize").description("Build the set-level capability registry + dependency graph across configured skills roots (Pro)").option("--json", "print the registry+graph report as JSON instead of the terminal summary").option("--out <file>", "write the full report JSON to this path (refuses an existing path)").option("--usage-snapshot <file>", "join per-skill usage counts from a skill-monitor snapshot JSON").action(organizeCommand);
|
|
6560
|
+
program.command("find [query]").description("Discover skills via skills.sh and check partner security audits (requires VERCEL_OIDC_TOKEN)").option("--limit <n>", "max search results (default 10)", parseLimit).option("--audit <id>", "show partner audit verdicts (Socket/Snyk/\u2026) for a skills.sh id").option("--get <id>", "show detail + file tree for a skills.sh id").option("--curated", "list skills.sh curated skills").option("--json", "print the raw API payload as JSON").action(find);
|
|
6561
|
+
program.command("watch").description("Drift check across every SOURCES.md provenance ledger: which ingested upstreams have moved (Pro)").option("--offline", "skip all network checks; list entries for manual comparison only").option("--json", "print the drift report as JSON instead of the terminal summary").action(watchCommand);
|
|
6562
|
+
program.command("ingest").description(
|
|
6563
|
+
"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."
|
|
6564
|
+
).option("--list", "print pending queue entries and exit \u2014 no handoff").option("--json", "with --list: print pending entries as JSON").action(ingest);
|
|
6565
|
+
var queue = program.command("queue").description("Manage the pending-ingestion queue (Pro)");
|
|
6566
|
+
queue.command("close <id>").description("Close a queue entry after the decide pass \u2014 agents use this instead of hand-editing queue.json").requiredOption("--status <status>", 'final status: "ingested" or "dismissed"').action(queueClose);
|
|
6567
|
+
var refine = program.command("refine").description("Capture, apply, and generalize improvements to installed skills from usage feedback (Pro)");
|
|
6568
|
+
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);
|
|
6569
|
+
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);
|
|
6570
|
+
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);
|
|
6571
|
+
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);
|
|
6572
|
+
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);
|
|
6573
|
+
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);
|
|
4004
6574
|
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").action(init);
|
|
4005
6575
|
try {
|
|
4006
6576
|
await program.parseAsync();
|