@rhize/skill-forge 0.2.0 → 0.3.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/README.md +84 -36
- package/dist/cli.js +606 -148
- package/dist/cli.js.map +1 -1
- package/dist/ingest-prompt.md +211 -0
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -5,8 +5,7 @@ import { Command } from "commander";
|
|
|
5
5
|
import { createRequire } from "module";
|
|
6
6
|
|
|
7
7
|
// src/commands/add.ts
|
|
8
|
-
import { createInterface } from "readline/promises";
|
|
9
|
-
import { spawnSync } from "child_process";
|
|
8
|
+
import { createInterface as createInterface2 } from "readline/promises";
|
|
10
9
|
|
|
11
10
|
// src/config.ts
|
|
12
11
|
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
|
|
@@ -38,19 +37,30 @@ function loadConfig() {
|
|
|
38
37
|
skillsRoots: parsed.skillsRoots ?? defaults.skillsRoots,
|
|
39
38
|
quarantineDir: parsed.quarantineDir ?? defaults.quarantineDir,
|
|
40
39
|
strictness: parsed.strictness ?? defaults.strictness,
|
|
41
|
-
licenseKey: parsed.licenseKey ?? defaults.licenseKey
|
|
40
|
+
licenseKey: parsed.licenseKey ?? defaults.licenseKey,
|
|
41
|
+
defaultTarget: parsed.defaultTarget ?? defaults.defaultTarget,
|
|
42
|
+
agents: parsed.agents ?? defaults.agents,
|
|
43
|
+
handoffCommand: parsed.handoffCommand ?? defaults.handoffCommand
|
|
42
44
|
};
|
|
43
45
|
}
|
|
46
|
+
function saveConfig(config) {
|
|
47
|
+
const home = getHomeDir();
|
|
48
|
+
if (!existsSync(home)) {
|
|
49
|
+
mkdirSync(home, { recursive: true });
|
|
50
|
+
}
|
|
51
|
+
writeFileSync(getConfigPath(), JSON.stringify(config, null, 2) + "\n", "utf-8");
|
|
52
|
+
}
|
|
44
53
|
|
|
45
54
|
// src/install/resolve.ts
|
|
46
55
|
import { existsSync as existsSync2 } from "fs";
|
|
47
56
|
var GIT_URL_PATTERN = /^(git@|https?:\/\/|ssh:\/\/)/;
|
|
57
|
+
var LOCAL_PATH_PATTERN = /^(\.{1,2}\/|\/|~\/|~$)/;
|
|
48
58
|
function resolve(source) {
|
|
49
59
|
const value = source.trim();
|
|
50
60
|
if (GIT_URL_PATTERN.test(value) || value.endsWith(".git")) {
|
|
51
61
|
return { type: "git", value };
|
|
52
62
|
}
|
|
53
|
-
if (existsSync2(value)) {
|
|
63
|
+
if (existsSync2(value) || LOCAL_PATH_PATTERN.test(value)) {
|
|
54
64
|
return { type: "local", value };
|
|
55
65
|
}
|
|
56
66
|
return { type: "skills.sh", value };
|
|
@@ -61,6 +71,16 @@ import { randomUUID } from "crypto";
|
|
|
61
71
|
import { spawn } from "child_process";
|
|
62
72
|
import { cpSync, existsSync as existsSync3, mkdirSync as mkdirSync2, readdirSync } from "fs";
|
|
63
73
|
import { basename, join as join2, resolve as resolvePath } from "path";
|
|
74
|
+
|
|
75
|
+
// src/errors.ts
|
|
76
|
+
var UserError = class extends Error {
|
|
77
|
+
constructor(message) {
|
|
78
|
+
super(message);
|
|
79
|
+
this.name = "UserError";
|
|
80
|
+
}
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
// src/install/quarantine.ts
|
|
64
84
|
var SKILL_MANIFEST = "SKILL.md";
|
|
65
85
|
var SKIP_DIRS = /* @__PURE__ */ new Set([".git", "node_modules"]);
|
|
66
86
|
async function quarantine(source, quarantineDir, deps = {}) {
|
|
@@ -86,17 +106,23 @@ async function quarantine(source, quarantineDir, deps = {}) {
|
|
|
86
106
|
cwd: quarantinePath
|
|
87
107
|
});
|
|
88
108
|
if (result.code !== 0) {
|
|
89
|
-
throw new
|
|
109
|
+
throw new UserError(
|
|
90
110
|
`git clone failed for "${source.value}" (exit ${result.code}): ${result.stderr || result.stdout}`
|
|
91
111
|
);
|
|
92
112
|
}
|
|
93
113
|
break;
|
|
94
114
|
}
|
|
95
115
|
case "local": {
|
|
116
|
+
if (!existsSync3(source.value)) {
|
|
117
|
+
throw new UserError(`local path not found: ${source.value}`);
|
|
118
|
+
}
|
|
96
119
|
const dirName = basename(resolvePath(source.value));
|
|
97
120
|
cpSync(source.value, join2(quarantinePath, dirName), { recursive: true });
|
|
98
121
|
break;
|
|
99
122
|
}
|
|
123
|
+
default: {
|
|
124
|
+
throw new UserError(`unknown source type: ${source.type}`);
|
|
125
|
+
}
|
|
100
126
|
}
|
|
101
127
|
return { id, quarantinePath, skillDirs: findSkillDirs(quarantinePath), source };
|
|
102
128
|
}
|
|
@@ -184,115 +210,11 @@ function moveDir(src, dest) {
|
|
|
184
210
|
}
|
|
185
211
|
}
|
|
186
212
|
|
|
187
|
-
// src/license.ts
|
|
188
|
-
import { verify as cryptoVerify } from "crypto";
|
|
189
|
-
var LICENSE_PREFIX = "skf1";
|
|
190
|
-
var PUBLIC_KEY_PEM = `-----BEGIN PUBLIC KEY-----
|
|
191
|
-
MCowBQYDK2VwAyEAWSVbcX9u70IsN1O5zS5z+AYtAUVz7ExT7h+1gT9x+wQ=
|
|
192
|
-
-----END PUBLIC KEY-----
|
|
193
|
-
`;
|
|
194
|
-
function readLicenseKey() {
|
|
195
|
-
const fromEnv = process.env.SKILL_FORGE_LICENSE?.trim();
|
|
196
|
-
if (fromEnv) return fromEnv;
|
|
197
|
-
return loadConfig().licenseKey?.trim() || void 0;
|
|
198
|
-
}
|
|
199
|
-
function isLicensePayload(value) {
|
|
200
|
-
if (typeof value !== "object" || value === null) return false;
|
|
201
|
-
const p = value;
|
|
202
|
-
return typeof p.name === "string" && typeof p.email === "string" && typeof p.seats === "number" && typeof p.exp === "string";
|
|
203
|
-
}
|
|
204
|
-
function checkLicense(rawKey = readLicenseKey()) {
|
|
205
|
-
if (!rawKey) {
|
|
206
|
-
return { valid: false, reason: "missing" };
|
|
207
|
-
}
|
|
208
|
-
const parts = rawKey.trim().split(".");
|
|
209
|
-
if (parts.length !== 3 || parts[0] !== LICENSE_PREFIX) {
|
|
210
|
-
return { valid: false, reason: "malformed" };
|
|
211
|
-
}
|
|
212
|
-
const [, payloadPart, signaturePart] = parts;
|
|
213
|
-
const payloadBuffer = Buffer.from(payloadPart, "base64url");
|
|
214
|
-
const signature = Buffer.from(signaturePart, "base64url");
|
|
215
|
-
let payload;
|
|
216
|
-
try {
|
|
217
|
-
payload = JSON.parse(payloadBuffer.toString("utf-8"));
|
|
218
|
-
} catch {
|
|
219
|
-
return { valid: false, reason: "malformed" };
|
|
220
|
-
}
|
|
221
|
-
if (!isLicensePayload(payload)) {
|
|
222
|
-
return { valid: false, reason: "malformed" };
|
|
223
|
-
}
|
|
224
|
-
let signatureValid;
|
|
225
|
-
try {
|
|
226
|
-
signatureValid = cryptoVerify(null, payloadBuffer, PUBLIC_KEY_PEM, signature);
|
|
227
|
-
} catch {
|
|
228
|
-
return { valid: false, reason: "malformed" };
|
|
229
|
-
}
|
|
230
|
-
if (!signatureValid) {
|
|
231
|
-
return { valid: false, reason: "signature" };
|
|
232
|
-
}
|
|
233
|
-
const expMs = new Date(payload.exp).getTime();
|
|
234
|
-
if (Number.isNaN(expMs) || expMs < Date.now()) {
|
|
235
|
-
return { valid: false, reason: "expired" };
|
|
236
|
-
}
|
|
237
|
-
return { valid: true, payload };
|
|
238
|
-
}
|
|
239
|
-
function hasProLicense() {
|
|
240
|
-
return checkLicense().valid;
|
|
241
|
-
}
|
|
242
|
-
function upgradeNotice(feature) {
|
|
243
|
-
return `[skill-forge] Pro feature (${feature}) \u2014 set SKILL_FORGE_LICENSE or config.licenseKey to a valid license to unlock.`;
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
// src/report/render.ts
|
|
247
|
-
var BORDER = "\u2501".repeat(55);
|
|
248
|
-
var LABEL_WIDTH = 20;
|
|
249
|
-
function padLabel(text) {
|
|
250
|
-
return text.padEnd(LABEL_WIDTH);
|
|
251
|
-
}
|
|
252
|
-
function renderListField(labelText, items, placeholder) {
|
|
253
|
-
const head = `${padLabel(labelText)}: `;
|
|
254
|
-
if (!items || items.length === 0) {
|
|
255
|
-
return `${head}${placeholder}`;
|
|
256
|
-
}
|
|
257
|
-
const bullets = items.map((item) => ` - ${item}`).join("\n");
|
|
258
|
-
return `${head.trimEnd()}
|
|
259
|
-
${bullets}`;
|
|
260
|
-
}
|
|
261
|
-
function renderSafetyLine(safety) {
|
|
262
|
-
const detail = [safety.severity, safety.score !== void 0 ? `score ${safety.score}` : void 0].filter((v) => Boolean(v)).join(", ");
|
|
263
|
-
const withDetail = detail ? `${safety.verdict} (${detail})` : safety.verdict;
|
|
264
|
-
return safety.recommendation ? `${withDetail} \u2014 ${safety.recommendation}` : withDetail;
|
|
265
|
-
}
|
|
266
|
-
function renderReport(result) {
|
|
267
|
-
const { profile, overlap, overlapLocked, safety, worthTaking, leaveBehind } = result;
|
|
268
|
-
const name = profile.name ?? "unknown";
|
|
269
|
-
const version = profile.version ?? "unknown";
|
|
270
|
-
const license = profile.license ?? "UNKNOWN";
|
|
271
|
-
const overlapLines = overlapLocked ? [`${padLabel("Overlap analysis")}: ${upgradeNotice("overlap analysis")}`] : overlap ? [
|
|
272
|
-
`${padLabel("Nearest Rhize skill")}: ${overlap.nearest} (overlap ${overlap.nearestScore})`,
|
|
273
|
-
`${padLabel("Recommended verb")}: ${overlap.suggestedVerb}`
|
|
274
|
-
] : [`${padLabel("Nearest Rhize skill")}: n/a \u2014 no skills root configured`, `${padLabel("Recommended verb")}: n/a`];
|
|
275
|
-
const lines = [
|
|
276
|
-
BORDER,
|
|
277
|
-
`\u{1F528} Forge: ${name} (v${version}, ${license})`,
|
|
278
|
-
BORDER,
|
|
279
|
-
...overlapLines,
|
|
280
|
-
renderListField("What's worth taking", worthTaking, "(pending \u2014 not yet determined)"),
|
|
281
|
-
renderListField("What to leave behind", leaveBehind, "(pending \u2014 not yet determined)"),
|
|
282
|
-
`${padLabel("Safety verdict")}: ${renderSafetyLine(safety)}`,
|
|
283
|
-
BORDER
|
|
284
|
-
];
|
|
285
|
-
return lines.join("\n");
|
|
286
|
-
}
|
|
287
|
-
function renderJson(result) {
|
|
288
|
-
return JSON.stringify(result, null, 2);
|
|
289
|
-
}
|
|
290
|
-
|
|
291
213
|
// src/provenance.ts
|
|
292
214
|
import { appendFileSync, existsSync as existsSync5, statSync, writeFileSync as writeFileSync2 } from "fs";
|
|
293
215
|
import { join as join4 } from "path";
|
|
294
216
|
var FORGE_VERBS = ["DEFER", "ABSORB", "FORK", "REJECT", "WATCH"];
|
|
295
|
-
var LEDGER_HEADER = "#
|
|
217
|
+
var LEDGER_HEADER = "# Skill Forge \u2014 Provenance Ledger\n\nOne entry per external-skill ingestion decision.\n";
|
|
296
218
|
function todayLocalIsoDate() {
|
|
297
219
|
const d = /* @__PURE__ */ new Date();
|
|
298
220
|
const year = d.getFullYear();
|
|
@@ -1064,12 +986,159 @@ function scanOverlap(candidatePath, skillsRoot) {
|
|
|
1064
986
|
};
|
|
1065
987
|
}
|
|
1066
988
|
|
|
989
|
+
// src/license.ts
|
|
990
|
+
import { verify as cryptoVerify } from "crypto";
|
|
991
|
+
var LICENSE_PREFIX = "skf1";
|
|
992
|
+
var BETA_FREE = true;
|
|
993
|
+
var PUBLIC_KEY_PEM = `-----BEGIN PUBLIC KEY-----
|
|
994
|
+
MCowBQYDK2VwAyEAWSVbcX9u70IsN1O5zS5z+AYtAUVz7ExT7h+1gT9x+wQ=
|
|
995
|
+
-----END PUBLIC KEY-----
|
|
996
|
+
`;
|
|
997
|
+
function readLicenseKey() {
|
|
998
|
+
const fromEnv = process.env.SKILL_FORGE_LICENSE?.trim();
|
|
999
|
+
if (fromEnv) return fromEnv;
|
|
1000
|
+
return loadConfig().licenseKey?.trim() || void 0;
|
|
1001
|
+
}
|
|
1002
|
+
function isLicensePayload(value) {
|
|
1003
|
+
if (typeof value !== "object" || value === null) return false;
|
|
1004
|
+
const p = value;
|
|
1005
|
+
return typeof p.name === "string" && typeof p.email === "string" && typeof p.seats === "number" && typeof p.exp === "string";
|
|
1006
|
+
}
|
|
1007
|
+
function checkLicense(rawKey = readLicenseKey()) {
|
|
1008
|
+
if (!rawKey) {
|
|
1009
|
+
return { valid: false, reason: "missing" };
|
|
1010
|
+
}
|
|
1011
|
+
const parts = rawKey.trim().split(".");
|
|
1012
|
+
if (parts.length !== 3 || parts[0] !== LICENSE_PREFIX) {
|
|
1013
|
+
return { valid: false, reason: "malformed" };
|
|
1014
|
+
}
|
|
1015
|
+
const [, payloadPart, signaturePart] = parts;
|
|
1016
|
+
const payloadBuffer = Buffer.from(payloadPart, "base64url");
|
|
1017
|
+
const signature = Buffer.from(signaturePart, "base64url");
|
|
1018
|
+
let payload;
|
|
1019
|
+
try {
|
|
1020
|
+
payload = JSON.parse(payloadBuffer.toString("utf-8"));
|
|
1021
|
+
} catch {
|
|
1022
|
+
return { valid: false, reason: "malformed" };
|
|
1023
|
+
}
|
|
1024
|
+
if (!isLicensePayload(payload)) {
|
|
1025
|
+
return { valid: false, reason: "malformed" };
|
|
1026
|
+
}
|
|
1027
|
+
let signatureValid;
|
|
1028
|
+
try {
|
|
1029
|
+
signatureValid = cryptoVerify(null, payloadBuffer, PUBLIC_KEY_PEM, signature);
|
|
1030
|
+
} catch {
|
|
1031
|
+
return { valid: false, reason: "malformed" };
|
|
1032
|
+
}
|
|
1033
|
+
if (!signatureValid) {
|
|
1034
|
+
return { valid: false, reason: "signature" };
|
|
1035
|
+
}
|
|
1036
|
+
const expMs = new Date(payload.exp).getTime();
|
|
1037
|
+
if (Number.isNaN(expMs) || expMs < Date.now()) {
|
|
1038
|
+
return { valid: false, reason: "expired" };
|
|
1039
|
+
}
|
|
1040
|
+
return { valid: true, payload };
|
|
1041
|
+
}
|
|
1042
|
+
function upgradeNotice(feature) {
|
|
1043
|
+
return `[skill-forge] Pro feature (${feature}) \u2014 set SKILL_FORGE_LICENSE or config.licenseKey to a valid license to unlock.`;
|
|
1044
|
+
}
|
|
1045
|
+
function invalidLicenseReasonText(reason) {
|
|
1046
|
+
switch (reason) {
|
|
1047
|
+
case "malformed":
|
|
1048
|
+
return "is malformed";
|
|
1049
|
+
case "signature":
|
|
1050
|
+
return "has an invalid signature";
|
|
1051
|
+
case "expired":
|
|
1052
|
+
return "has expired";
|
|
1053
|
+
default:
|
|
1054
|
+
return "is invalid";
|
|
1055
|
+
}
|
|
1056
|
+
}
|
|
1057
|
+
function proFeatureStatus(feature, betaFree = BETA_FREE) {
|
|
1058
|
+
const status2 = checkLicense();
|
|
1059
|
+
if (status2.valid) {
|
|
1060
|
+
return { enabled: true };
|
|
1061
|
+
}
|
|
1062
|
+
if (!betaFree) {
|
|
1063
|
+
return { enabled: false, notice: upgradeNotice(feature) };
|
|
1064
|
+
}
|
|
1065
|
+
const betaNotice = `Pro feature (${feature}) \u2014 free during the 0.x beta; will require a license at 1.0.`;
|
|
1066
|
+
if (status2.reason === "missing") {
|
|
1067
|
+
return { enabled: true, notice: betaNotice };
|
|
1068
|
+
}
|
|
1069
|
+
return {
|
|
1070
|
+
enabled: true,
|
|
1071
|
+
notice: `${betaNotice} (Note: your license key ${invalidLicenseReasonText(status2.reason)} \u2014 ignored while Pro is free.)`
|
|
1072
|
+
};
|
|
1073
|
+
}
|
|
1074
|
+
|
|
1075
|
+
// src/report/render.ts
|
|
1076
|
+
var BORDER = "\u2501".repeat(55);
|
|
1077
|
+
var LABEL_WIDTH = 20;
|
|
1078
|
+
function padLabel(text) {
|
|
1079
|
+
return text.padEnd(LABEL_WIDTH);
|
|
1080
|
+
}
|
|
1081
|
+
function renderListField(labelText, items, placeholder) {
|
|
1082
|
+
const head = `${padLabel(labelText)}: `;
|
|
1083
|
+
if (!items || items.length === 0) {
|
|
1084
|
+
return `${head}${placeholder}`;
|
|
1085
|
+
}
|
|
1086
|
+
const bullets = items.map((item) => ` - ${item}`).join("\n");
|
|
1087
|
+
return `${head.trimEnd()}
|
|
1088
|
+
${bullets}`;
|
|
1089
|
+
}
|
|
1090
|
+
function renderSafetyLine(safety) {
|
|
1091
|
+
const detail = [safety.severity, safety.score !== void 0 ? `score ${safety.score}` : void 0].filter((v) => Boolean(v)).join(", ");
|
|
1092
|
+
const withDetail = detail ? `${safety.verdict} (${detail})` : safety.verdict;
|
|
1093
|
+
return safety.recommendation ? `${withDetail} \u2014 ${safety.recommendation}` : withDetail;
|
|
1094
|
+
}
|
|
1095
|
+
function renderFindingLine(finding) {
|
|
1096
|
+
const severity = finding.severity ?? "UNKNOWN";
|
|
1097
|
+
const rule = finding.rule ?? "unknown-rule";
|
|
1098
|
+
return ` - [${severity}] ${rule}${finding.message ? `: ${finding.message}` : ""}`;
|
|
1099
|
+
}
|
|
1100
|
+
function renderFindingsLines(findings) {
|
|
1101
|
+
if (!findings?.length) return [];
|
|
1102
|
+
return [`${padLabel("Findings")}:`, ...findings.map(renderFindingLine)];
|
|
1103
|
+
}
|
|
1104
|
+
function renderReport(result) {
|
|
1105
|
+
const { profile, overlap, overlapLocked, safety, worthTaking, leaveBehind } = result;
|
|
1106
|
+
const name = profile.name ?? "unknown";
|
|
1107
|
+
const version = profile.version ?? "unknown";
|
|
1108
|
+
const license = profile.license ?? "UNKNOWN";
|
|
1109
|
+
const overlapLines = overlapLocked ? [`${padLabel("Overlap analysis")}: ${upgradeNotice("overlap analysis")}`] : overlap ? [
|
|
1110
|
+
`${padLabel("Nearest existing skill")}: ${overlap.nearest} (overlap ${overlap.nearestScore})`,
|
|
1111
|
+
`${padLabel("Recommended verb")}: ${overlap.suggestedVerb}`
|
|
1112
|
+
] : [`${padLabel("Nearest existing skill")}: n/a \u2014 no skills root configured`, `${padLabel("Recommended verb")}: n/a`];
|
|
1113
|
+
const lines = [
|
|
1114
|
+
BORDER,
|
|
1115
|
+
`\u{1F528} Forge: ${name} (v${version}, ${license})`,
|
|
1116
|
+
BORDER,
|
|
1117
|
+
...overlapLines,
|
|
1118
|
+
renderListField("What's worth taking", worthTaking, "(pending \u2014 not yet determined)"),
|
|
1119
|
+
renderListField("What to leave behind", leaveBehind, "(pending \u2014 not yet determined)"),
|
|
1120
|
+
`${padLabel("Safety verdict")}: ${renderSafetyLine(safety)}`,
|
|
1121
|
+
...renderFindingsLines(safety.findings),
|
|
1122
|
+
BORDER
|
|
1123
|
+
];
|
|
1124
|
+
return lines.join("\n");
|
|
1125
|
+
}
|
|
1126
|
+
function renderJson(result) {
|
|
1127
|
+
return JSON.stringify(result, null, 2);
|
|
1128
|
+
}
|
|
1129
|
+
|
|
1067
1130
|
// src/commands/gatePipeline.ts
|
|
1068
1131
|
var SAFETY_VERDICT_MAP = {
|
|
1069
1132
|
pass: "ALLOW",
|
|
1070
1133
|
warn: "CAUTION",
|
|
1071
1134
|
block: "BLOCK"
|
|
1072
1135
|
};
|
|
1136
|
+
function printGateOutput(gateResult, json) {
|
|
1137
|
+
if (!json) {
|
|
1138
|
+
for (const notice of gateResult.notices ?? []) console.log(`[skill-forge] ${notice}`);
|
|
1139
|
+
}
|
|
1140
|
+
console.log(json ? renderJson(gateResult) : renderReport(gateResult));
|
|
1141
|
+
}
|
|
1073
1142
|
function asSkillProfile(candidatePath) {
|
|
1074
1143
|
const profile = profileSkill(candidatePath);
|
|
1075
1144
|
if (profile.kind !== "skill") {
|
|
@@ -1085,15 +1154,16 @@ function tryOverlap(candidatePath, skillsRoot) {
|
|
|
1085
1154
|
return void 0;
|
|
1086
1155
|
}
|
|
1087
1156
|
}
|
|
1088
|
-
async function runGate(candidatePath, skillsRoot) {
|
|
1157
|
+
async function runGate(candidatePath, skillsRoot, proFeatureLabel = "overlap analysis") {
|
|
1089
1158
|
const profile = asSkillProfile(candidatePath);
|
|
1090
1159
|
const safety = await scanSafety(candidatePath);
|
|
1091
|
-
const
|
|
1092
|
-
const overlap =
|
|
1160
|
+
const proStatus = proFeatureStatus(proFeatureLabel);
|
|
1161
|
+
const overlap = proStatus.enabled ? tryOverlap(candidatePath, skillsRoot) : void 0;
|
|
1093
1162
|
const gateResult = {
|
|
1094
1163
|
profile,
|
|
1095
1164
|
overlap,
|
|
1096
|
-
overlapLocked: !
|
|
1165
|
+
overlapLocked: !proStatus.enabled,
|
|
1166
|
+
notices: proStatus.notice ? [proStatus.notice] : void 0,
|
|
1097
1167
|
safety: {
|
|
1098
1168
|
verdict: SAFETY_VERDICT_MAP[safety.verdict],
|
|
1099
1169
|
findings: safety.findings.map((f) => ({
|
|
@@ -1103,7 +1173,7 @@ async function runGate(candidatePath, skillsRoot) {
|
|
|
1103
1173
|
}))
|
|
1104
1174
|
}
|
|
1105
1175
|
};
|
|
1106
|
-
return { safety, gateResult,
|
|
1176
|
+
return { safety, gateResult, proEnabled: proStatus.enabled };
|
|
1107
1177
|
}
|
|
1108
1178
|
|
|
1109
1179
|
// src/queue.ts
|
|
@@ -1146,10 +1216,392 @@ function generateId(existingIds) {
|
|
|
1146
1216
|
return id;
|
|
1147
1217
|
}
|
|
1148
1218
|
|
|
1219
|
+
// src/handoff.ts
|
|
1220
|
+
import { spawnSync } from "child_process";
|
|
1221
|
+
import { existsSync as existsSync9 } from "fs";
|
|
1222
|
+
import { dirname as dirname3, join as join10 } from "path";
|
|
1223
|
+
import { fileURLToPath } from "url";
|
|
1224
|
+
var KNOWN_AGENT_BINARIES = ["claude", "codex", "cursor-agent", "windsurf", "opencode", "gemini"];
|
|
1225
|
+
var HANDOFF_INSTRUCTION = "Read {prompt} and follow its instructions for the skill installed at {path}";
|
|
1226
|
+
function resolvePromptPath() {
|
|
1227
|
+
const here = dirname3(fileURLToPath(import.meta.url));
|
|
1228
|
+
const builtCandidate = join10(here, "ingest-prompt.md");
|
|
1229
|
+
if (existsSync9(builtCandidate)) return builtCandidate;
|
|
1230
|
+
return join10(here, "..", "assets", "ingest-prompt.md");
|
|
1231
|
+
}
|
|
1232
|
+
function defaultIsOnPath(bin) {
|
|
1233
|
+
const finder = process.platform === "win32" ? "where" : "which";
|
|
1234
|
+
const result = spawnSync(finder, [bin], { stdio: "ignore" });
|
|
1235
|
+
return result.status === 0;
|
|
1236
|
+
}
|
|
1237
|
+
function substitute(template, vars) {
|
|
1238
|
+
return template.map((token) => token.replace(/\{(\w+)\}/g, (match, key) => vars[key] ?? match));
|
|
1239
|
+
}
|
|
1240
|
+
function buildHandoffCommand(config, installedPath, deps = {}) {
|
|
1241
|
+
const isOnPath = deps.isOnPath ?? defaultIsOnPath;
|
|
1242
|
+
const promptPath = resolvePromptPath();
|
|
1243
|
+
const vars = { path: installedPath, prompt: promptPath };
|
|
1244
|
+
if (config.handoffCommand && config.handoffCommand.length > 0) {
|
|
1245
|
+
return { argv: substitute(config.handoffCommand, vars), source: "template" };
|
|
1246
|
+
}
|
|
1247
|
+
for (const bin of KNOWN_AGENT_BINARIES) {
|
|
1248
|
+
if (isOnPath(bin)) {
|
|
1249
|
+
return { argv: substitute([bin, "-p", HANDOFF_INSTRUCTION], vars), source: "detected" };
|
|
1250
|
+
}
|
|
1251
|
+
}
|
|
1252
|
+
return { argv: [], source: "none" };
|
|
1253
|
+
}
|
|
1254
|
+
function printOnlyMessage(installedPath) {
|
|
1255
|
+
const promptPath = resolvePromptPath();
|
|
1256
|
+
return `[skill-forge] no agent handoff available \u2014 run one yourself:
|
|
1257
|
+
Prompt: ${promptPath}
|
|
1258
|
+
Skill path: ${installedPath}`;
|
|
1259
|
+
}
|
|
1260
|
+
function runHandoff(config, installedPath, options = {}) {
|
|
1261
|
+
const command = buildHandoffCommand(config, installedPath, options);
|
|
1262
|
+
const spawn2 = options.spawn ?? spawnSync;
|
|
1263
|
+
if (command.source === "none" || command.argv.length === 0) {
|
|
1264
|
+
const printed = printOnlyMessage(installedPath);
|
|
1265
|
+
console.log(printed);
|
|
1266
|
+
return { ran: false, command, printed };
|
|
1267
|
+
}
|
|
1268
|
+
const [bin, ...args] = command.argv;
|
|
1269
|
+
const result = spawn2(bin, args, { stdio: options.stdio ?? "inherit" });
|
|
1270
|
+
const error = result.error;
|
|
1271
|
+
if (error?.code === "ENOENT") {
|
|
1272
|
+
const printed = printOnlyMessage(installedPath);
|
|
1273
|
+
console.log(`[skill-forge] ${bin} not found on PATH \u2014 run this yourself:`);
|
|
1274
|
+
console.log(printed);
|
|
1275
|
+
return { ran: false, command, printed, error: error.message };
|
|
1276
|
+
}
|
|
1277
|
+
if (error) {
|
|
1278
|
+
console.error(`[skill-forge] failed to launch handoff: ${error.message}`);
|
|
1279
|
+
return { ran: false, command, error: error.message };
|
|
1280
|
+
}
|
|
1281
|
+
return { ran: true, command };
|
|
1282
|
+
}
|
|
1283
|
+
|
|
1284
|
+
// src/commands/init.ts
|
|
1285
|
+
import { existsSync as existsSync10 } from "fs";
|
|
1286
|
+
import { homedir as homedir4 } from "os";
|
|
1287
|
+
import { createInterface } from "readline/promises";
|
|
1288
|
+
|
|
1289
|
+
// src/agents.ts
|
|
1290
|
+
import { homedir as homedir3 } from "os";
|
|
1291
|
+
import { join as join11, resolve as resolve4 } from "path";
|
|
1292
|
+
function projectPath(root, cwd) {
|
|
1293
|
+
return root ? resolve4(cwd, root) : void 0;
|
|
1294
|
+
}
|
|
1295
|
+
function globalPath(root, home) {
|
|
1296
|
+
if (!root) return void 0;
|
|
1297
|
+
if (root === "~") return home;
|
|
1298
|
+
if (root.startsWith("~/")) return join11(home, root.slice(2));
|
|
1299
|
+
return root;
|
|
1300
|
+
}
|
|
1301
|
+
function makeAgent(spec) {
|
|
1302
|
+
const agent = {
|
|
1303
|
+
id: spec.id,
|
|
1304
|
+
displayName: spec.displayName,
|
|
1305
|
+
projectRoot: spec.projectRoot,
|
|
1306
|
+
globalRoot: spec.globalRoot,
|
|
1307
|
+
verified: spec.verified ?? true,
|
|
1308
|
+
defaultHandoffTemplate: spec.defaultHandoffTemplate,
|
|
1309
|
+
detect: (cwd, home) => detectAgentRoot(agent, cwd, home) !== void 0
|
|
1310
|
+
};
|
|
1311
|
+
return agent;
|
|
1312
|
+
}
|
|
1313
|
+
function resolveAgentRoot(agent, scope, cwd = process.cwd(), home = homedir3()) {
|
|
1314
|
+
return scope === "project" ? projectPath(agent.projectRoot, cwd) : globalPath(agent.globalRoot, home);
|
|
1315
|
+
}
|
|
1316
|
+
function detectAgentRoot(agent, cwd = process.cwd(), home = homedir3()) {
|
|
1317
|
+
for (const scope of ["project", "global"]) {
|
|
1318
|
+
const root = resolveAgentRoot(agent, scope, cwd, home);
|
|
1319
|
+
if (root && isDir(root)) return { scope, root };
|
|
1320
|
+
}
|
|
1321
|
+
return void 0;
|
|
1322
|
+
}
|
|
1323
|
+
var AGENTS = [
|
|
1324
|
+
{ id: "aider-desk", displayName: "AiderDesk", projectRoot: ".aider-desk/skills", globalRoot: "~/.aider-desk/skills" },
|
|
1325
|
+
{ id: "amp", displayName: "Amp", projectRoot: ".agents/skills", globalRoot: "~/.config/agents/skills" },
|
|
1326
|
+
{ id: "replit", displayName: "Replit", projectRoot: ".agents/skills", globalRoot: "~/.config/agents/skills" },
|
|
1327
|
+
{ id: "universal", displayName: "Universal", projectRoot: ".agents/skills", globalRoot: "~/.config/agents/skills" },
|
|
1328
|
+
{ id: "antigravity", displayName: "Antigravity", projectRoot: ".agents/skills", globalRoot: "~/.gemini/antigravity/skills" },
|
|
1329
|
+
{
|
|
1330
|
+
id: "antigravity-cli",
|
|
1331
|
+
displayName: "Antigravity CLI",
|
|
1332
|
+
projectRoot: ".agents/skills",
|
|
1333
|
+
globalRoot: "~/.gemini/antigravity-cli/skills"
|
|
1334
|
+
},
|
|
1335
|
+
{ id: "astrbot", displayName: "AstrBot", projectRoot: "data/skills", globalRoot: "~/.astrbot/data/skills" },
|
|
1336
|
+
{ id: "autohand-code", displayName: "Autohand Code CLI", projectRoot: ".autohand/skills", globalRoot: "~/.autohand/skills" },
|
|
1337
|
+
{ id: "augment", displayName: "Augment", projectRoot: ".augment/skills", globalRoot: "~/.augment/skills" },
|
|
1338
|
+
{ id: "bob", displayName: "IBM Bob", projectRoot: ".bob/skills", globalRoot: "~/.bob/skills" },
|
|
1339
|
+
{
|
|
1340
|
+
id: "claude-code",
|
|
1341
|
+
displayName: "Claude Code",
|
|
1342
|
+
projectRoot: ".claude/skills",
|
|
1343
|
+
globalRoot: "~/.claude/skills",
|
|
1344
|
+
defaultHandoffTemplate: ["claude", "-p", HANDOFF_INSTRUCTION]
|
|
1345
|
+
},
|
|
1346
|
+
{ id: "openclaw", displayName: "OpenClaw", projectRoot: "skills", globalRoot: "~/.openclaw/skills" },
|
|
1347
|
+
{ id: "cline", displayName: "Cline", projectRoot: ".agents/skills", globalRoot: "~/.agents/skills" },
|
|
1348
|
+
{ id: "dexto", displayName: "Dexto", projectRoot: ".agents/skills", globalRoot: "~/.agents/skills" },
|
|
1349
|
+
{ id: "kimi-code-cli", displayName: "Kimi Code CLI", projectRoot: ".agents/skills", globalRoot: "~/.agents/skills" },
|
|
1350
|
+
{ id: "loaf", displayName: "Loaf", projectRoot: ".agents/skills", globalRoot: "~/.agents/skills" },
|
|
1351
|
+
{ id: "warp", displayName: "Warp", projectRoot: ".agents/skills", globalRoot: "~/.agents/skills" },
|
|
1352
|
+
{ id: "zed", displayName: "Zed", projectRoot: ".agents/skills", globalRoot: "~/.agents/skills" },
|
|
1353
|
+
{
|
|
1354
|
+
id: "codearts-agent",
|
|
1355
|
+
displayName: "CodeArts Agent",
|
|
1356
|
+
projectRoot: ".codeartsdoer/skills",
|
|
1357
|
+
globalRoot: "~/.codeartsdoer/skills"
|
|
1358
|
+
},
|
|
1359
|
+
{ id: "codebuddy", displayName: "CodeBuddy", projectRoot: ".codebuddy/skills", globalRoot: "~/.codebuddy/skills" },
|
|
1360
|
+
{ id: "codemaker", displayName: "Codemaker", projectRoot: ".codemaker/skills", globalRoot: "~/.codemaker/skills" },
|
|
1361
|
+
{ id: "codestudio", displayName: "Code Studio", projectRoot: ".codestudio/skills", globalRoot: "~/.codestudio/skills" },
|
|
1362
|
+
{
|
|
1363
|
+
id: "codex",
|
|
1364
|
+
displayName: "Codex",
|
|
1365
|
+
projectRoot: ".agents/skills",
|
|
1366
|
+
globalRoot: "~/.codex/skills",
|
|
1367
|
+
defaultHandoffTemplate: ["codex", "exec", HANDOFF_INSTRUCTION]
|
|
1368
|
+
},
|
|
1369
|
+
{
|
|
1370
|
+
id: "command-code",
|
|
1371
|
+
displayName: "Command Code",
|
|
1372
|
+
projectRoot: ".commandcode/skills",
|
|
1373
|
+
globalRoot: "~/.commandcode/skills"
|
|
1374
|
+
},
|
|
1375
|
+
{ id: "continue", displayName: "Continue", projectRoot: ".continue/skills", globalRoot: "~/.continue/skills" },
|
|
1376
|
+
{ id: "cortex", displayName: "Cortex Code", projectRoot: ".cortex/skills", globalRoot: "~/.snowflake/cortex/skills" },
|
|
1377
|
+
{ id: "crush", displayName: "Crush", projectRoot: ".crush/skills", globalRoot: "~/.config/crush/skills" },
|
|
1378
|
+
{ id: "cursor", displayName: "Cursor", projectRoot: ".agents/skills", globalRoot: "~/.cursor/skills" },
|
|
1379
|
+
{
|
|
1380
|
+
id: "deepagents",
|
|
1381
|
+
displayName: "Deep Agents",
|
|
1382
|
+
projectRoot: ".agents/skills",
|
|
1383
|
+
globalRoot: "~/.deepagents/agent/skills"
|
|
1384
|
+
},
|
|
1385
|
+
{ id: "devin", displayName: "Devin for Terminal", projectRoot: ".devin/skills", globalRoot: "~/.config/devin/skills" },
|
|
1386
|
+
{ id: "droid", displayName: "Droid", projectRoot: ".factory/skills", globalRoot: "~/.factory/skills" },
|
|
1387
|
+
{ id: "eve", displayName: "Eve", projectRoot: "agent/skills" },
|
|
1388
|
+
{ id: "firebender", displayName: "Firebender", projectRoot: ".agents/skills", globalRoot: "~/.firebender/skills" },
|
|
1389
|
+
{ id: "forgecode", displayName: "ForgeCode", projectRoot: ".forge/skills", globalRoot: "~/.forge/skills" },
|
|
1390
|
+
{
|
|
1391
|
+
id: "gemini-cli",
|
|
1392
|
+
displayName: "Gemini CLI",
|
|
1393
|
+
projectRoot: ".agents/skills",
|
|
1394
|
+
globalRoot: "~/.gemini/skills",
|
|
1395
|
+
defaultHandoffTemplate: ["gemini", "-p", HANDOFF_INSTRUCTION]
|
|
1396
|
+
},
|
|
1397
|
+
{
|
|
1398
|
+
id: "github-copilot",
|
|
1399
|
+
displayName: "GitHub Copilot",
|
|
1400
|
+
projectRoot: ".agents/skills",
|
|
1401
|
+
globalRoot: "~/.copilot/skills"
|
|
1402
|
+
},
|
|
1403
|
+
{ id: "goose", displayName: "Goose", projectRoot: ".goose/skills", globalRoot: "~/.config/goose/skills" },
|
|
1404
|
+
{ id: "hermes-agent", displayName: "Hermes Agent", projectRoot: ".hermes/skills", globalRoot: "~/.hermes/skills" },
|
|
1405
|
+
{
|
|
1406
|
+
id: "inference-sh",
|
|
1407
|
+
displayName: "inference.sh",
|
|
1408
|
+
projectRoot: ".inferencesh/skills",
|
|
1409
|
+
globalRoot: "~/.inferencesh/skills"
|
|
1410
|
+
},
|
|
1411
|
+
{ id: "jazz", displayName: "Jazz", projectRoot: ".jazz/skills", globalRoot: "~/.jazz/skills" },
|
|
1412
|
+
{ id: "junie", displayName: "Junie", projectRoot: ".junie/skills", globalRoot: "~/.junie/skills" },
|
|
1413
|
+
{ id: "iflow-cli", displayName: "iFlow CLI", projectRoot: ".iflow/skills", globalRoot: "~/.iflow/skills" },
|
|
1414
|
+
{ id: "kilo", displayName: "Kilo Code", projectRoot: ".kilocode/skills", globalRoot: "~/.kilocode/skills" },
|
|
1415
|
+
{ id: "kiro-cli", displayName: "Kiro CLI", projectRoot: ".kiro/skills", globalRoot: "~/.kiro/skills" },
|
|
1416
|
+
{ id: "kode", displayName: "Kode", projectRoot: ".kode/skills", globalRoot: "~/.kode/skills" },
|
|
1417
|
+
{ id: "lingma", displayName: "Lingma", projectRoot: ".lingma/skills", globalRoot: "~/.lingma/skills" },
|
|
1418
|
+
{ id: "mcpjam", displayName: "MCPJam", projectRoot: ".mcpjam/skills", globalRoot: "~/.mcpjam/skills" },
|
|
1419
|
+
{ id: "mistral-vibe", displayName: "Mistral Vibe", projectRoot: ".vibe/skills", globalRoot: "~/.vibe/skills" },
|
|
1420
|
+
{ id: "moxby", displayName: "Moxby", projectRoot: ".moxby/skills", globalRoot: "~/.moxby/skills" },
|
|
1421
|
+
{ id: "mux", displayName: "Mux", projectRoot: ".mux/skills", globalRoot: "~/.mux/skills" },
|
|
1422
|
+
{
|
|
1423
|
+
id: "opencode",
|
|
1424
|
+
displayName: "OpenCode",
|
|
1425
|
+
projectRoot: ".agents/skills",
|
|
1426
|
+
globalRoot: "~/.config/opencode/skills",
|
|
1427
|
+
defaultHandoffTemplate: ["opencode", "run", HANDOFF_INSTRUCTION]
|
|
1428
|
+
},
|
|
1429
|
+
{ id: "openhands", displayName: "OpenHands", projectRoot: ".openhands/skills", globalRoot: "~/.openhands/skills" },
|
|
1430
|
+
{ id: "ona", displayName: "Ona", projectRoot: ".ona/skills", globalRoot: "~/.ona/skills" },
|
|
1431
|
+
{ id: "pi", displayName: "Pi", projectRoot: ".pi/skills", globalRoot: "~/.pi/agent/skills" },
|
|
1432
|
+
{ id: "qoder", displayName: "Qoder", projectRoot: ".qoder/skills", globalRoot: "~/.qoder/skills" },
|
|
1433
|
+
{ id: "qoder-cn", displayName: "Qoder CN", projectRoot: ".qoder/skills", globalRoot: "~/.qoder-cn/skills" },
|
|
1434
|
+
{ id: "qwen-code", displayName: "Qwen Code", projectRoot: ".qwen/skills", globalRoot: "~/.qwen/skills" },
|
|
1435
|
+
{ id: "reasonix", displayName: "Reasonix", projectRoot: ".reasonix/skills", globalRoot: "~/.reasonix/skills" },
|
|
1436
|
+
{ id: "rovodev", displayName: "Rovo Dev", projectRoot: ".rovodev/skills", globalRoot: "~/.rovodev/skills" },
|
|
1437
|
+
{ id: "roo", displayName: "Roo Code", projectRoot: ".roo/skills", globalRoot: "~/.roo/skills" },
|
|
1438
|
+
{
|
|
1439
|
+
id: "tabnine-cli",
|
|
1440
|
+
displayName: "Tabnine CLI",
|
|
1441
|
+
projectRoot: ".tabnine/agent/skills",
|
|
1442
|
+
globalRoot: "~/.tabnine/agent/skills"
|
|
1443
|
+
},
|
|
1444
|
+
{ id: "terramind", displayName: "Terramind", projectRoot: ".terramind/skills", globalRoot: "~/.terramind/skills" },
|
|
1445
|
+
{ id: "tinycloud", displayName: "Tinycloud", projectRoot: ".tinycloud/skills", globalRoot: "~/.tinycloud/skills" },
|
|
1446
|
+
{ id: "trae", displayName: "Trae", projectRoot: ".trae/skills", globalRoot: "~/.trae/skills" },
|
|
1447
|
+
{ id: "trae-cn", displayName: "Trae CN", projectRoot: ".trae/skills", globalRoot: "~/.trae-cn/skills" },
|
|
1448
|
+
{ id: "windsurf", displayName: "Windsurf", projectRoot: ".windsurf/skills", globalRoot: "~/.codeium/windsurf/skills" },
|
|
1449
|
+
{ id: "zcode", displayName: "ZCode", projectRoot: ".zcode/skills", globalRoot: "~/.zcode/skills" },
|
|
1450
|
+
{ id: "zencoder", displayName: "Zencoder", projectRoot: ".zencoder/skills", globalRoot: "~/.zencoder/skills" },
|
|
1451
|
+
{ id: "zenflow", displayName: "Zenflow", projectRoot: ".zencoder/skills", globalRoot: "~/.zencoder/skills" },
|
|
1452
|
+
{ id: "neovate", displayName: "Neovate", projectRoot: ".neovate/skills", globalRoot: "~/.neovate/skills" },
|
|
1453
|
+
{ id: "pochi", displayName: "Pochi", projectRoot: ".pochi/skills", globalRoot: "~/.pochi/skills" },
|
|
1454
|
+
{ id: "promptscript", displayName: "PromptScript", projectRoot: ".agents/skills" },
|
|
1455
|
+
{ id: "adal", displayName: "AdaL", projectRoot: ".adal/skills", globalRoot: "~/.adal/skills" }
|
|
1456
|
+
].map(makeAgent);
|
|
1457
|
+
|
|
1458
|
+
// src/commands/init.ts
|
|
1459
|
+
function detectAgents(cwd, home) {
|
|
1460
|
+
const found = [];
|
|
1461
|
+
for (const agent of AGENTS) {
|
|
1462
|
+
const hit = detectAgentRoot(agent, cwd, home);
|
|
1463
|
+
if (hit) found.push({ agent, ...hit });
|
|
1464
|
+
}
|
|
1465
|
+
return found;
|
|
1466
|
+
}
|
|
1467
|
+
function printDetectionResults(detected) {
|
|
1468
|
+
console.log(`[skill-forge] init \u2014 agent detection (${AGENTS.length} known agents)`);
|
|
1469
|
+
if (detected.length === 0) {
|
|
1470
|
+
console.log(" none detected");
|
|
1471
|
+
return;
|
|
1472
|
+
}
|
|
1473
|
+
for (const d of detected) {
|
|
1474
|
+
console.log(` ${d.agent.displayName} (${d.agent.id}) \u2014 ${d.scope}: ${d.root}`);
|
|
1475
|
+
}
|
|
1476
|
+
}
|
|
1477
|
+
function handoffTemplateFor(agent) {
|
|
1478
|
+
return agent.defaultHandoffTemplate ?? [agent.id, "-p", HANDOFF_INSTRUCTION];
|
|
1479
|
+
}
|
|
1480
|
+
function buildConfig(selected, defaultTargetEntry, handoffAgent) {
|
|
1481
|
+
const existing = loadConfig();
|
|
1482
|
+
return {
|
|
1483
|
+
...existing,
|
|
1484
|
+
skillsRoots: selected.map((s) => s.root),
|
|
1485
|
+
agents: selected.map((s) => ({ id: s.agent.id, skillsRoot: s.root })),
|
|
1486
|
+
defaultTarget: defaultTargetEntry?.root ?? existing.defaultTarget,
|
|
1487
|
+
handoffCommand: handoffAgent ? handoffTemplateFor(handoffAgent) : existing.handoffCommand
|
|
1488
|
+
};
|
|
1489
|
+
}
|
|
1490
|
+
function printSummary(config) {
|
|
1491
|
+
console.log(`
|
|
1492
|
+
[skill-forge] wrote config to ${getConfigPath()}`);
|
|
1493
|
+
console.log(` gate targets: ${config.skillsRoots.join(", ")}`);
|
|
1494
|
+
if (config.defaultTarget) console.log(` default target: ${config.defaultTarget}`);
|
|
1495
|
+
if (config.handoffCommand?.length) console.log(` handoff command: ${config.handoffCommand.join(" ")}`);
|
|
1496
|
+
}
|
|
1497
|
+
function parseChoice(answer, max) {
|
|
1498
|
+
const n = Number(answer);
|
|
1499
|
+
return Number.isInteger(n) && n >= 1 && n <= max ? n - 1 : void 0;
|
|
1500
|
+
}
|
|
1501
|
+
async function promptMultiSelect(rl, detected) {
|
|
1502
|
+
const chosen = new Set(detected.map((_, i) => i));
|
|
1503
|
+
for (; ; ) {
|
|
1504
|
+
console.log("\nGate targets (all detected roots pre-selected \u2014 type a number to toggle, Enter to confirm):");
|
|
1505
|
+
detected.forEach((d, i) => {
|
|
1506
|
+
const mark = chosen.has(i) ? "[x]" : "[ ]";
|
|
1507
|
+
console.log(` ${mark} ${i + 1}. ${d.agent.displayName} (${d.scope}) \u2014 ${d.root}`);
|
|
1508
|
+
});
|
|
1509
|
+
const answer = (await rl.question("> ")).trim();
|
|
1510
|
+
if (answer === "") break;
|
|
1511
|
+
const idx = parseChoice(answer, detected.length);
|
|
1512
|
+
if (idx !== void 0) {
|
|
1513
|
+
if (chosen.has(idx)) chosen.delete(idx);
|
|
1514
|
+
else chosen.add(idx);
|
|
1515
|
+
} else {
|
|
1516
|
+
console.log(" (invalid selection \u2014 enter a listed number, or press Enter to confirm)");
|
|
1517
|
+
}
|
|
1518
|
+
}
|
|
1519
|
+
return detected.filter((_, i) => chosen.has(i));
|
|
1520
|
+
}
|
|
1521
|
+
async function promptDefaultTarget(rl, selected) {
|
|
1522
|
+
console.log("\nDefault promotion target:");
|
|
1523
|
+
selected.forEach((d, i) => console.log(` ${i + 1}. ${d.agent.displayName} \u2014 ${d.root}`));
|
|
1524
|
+
const answer = (await rl.question(`Choose [1-${selected.length}] (default 1): `)).trim();
|
|
1525
|
+
const idx = answer === "" ? 0 : parseChoice(answer, selected.length) ?? 0;
|
|
1526
|
+
return selected[idx];
|
|
1527
|
+
}
|
|
1528
|
+
async function promptHandoffAgent(rl, detected) {
|
|
1529
|
+
console.log("\nHandoff agent (used to hand off follow-up prompts after promotion; optional):");
|
|
1530
|
+
detected.forEach((d, i) => {
|
|
1531
|
+
const note = d.agent.defaultHandoffTemplate ? "" : " (no confirmed CLI \u2014 best-effort)";
|
|
1532
|
+
console.log(` ${i + 1}. ${d.agent.displayName}${note}`);
|
|
1533
|
+
});
|
|
1534
|
+
const answer = (await rl.question(`Choose [1-${detected.length}] or Enter to skip: `)).trim();
|
|
1535
|
+
if (answer === "") return void 0;
|
|
1536
|
+
const idx = parseChoice(answer, detected.length);
|
|
1537
|
+
return idx === void 0 ? void 0 : detected[idx].agent;
|
|
1538
|
+
}
|
|
1539
|
+
async function init(options = {}) {
|
|
1540
|
+
const cwd = options.cwd ?? process.cwd();
|
|
1541
|
+
const home = options.home ?? homedir4();
|
|
1542
|
+
const detected = detectAgents(cwd, home);
|
|
1543
|
+
if (options.list) {
|
|
1544
|
+
printDetectionResults(detected);
|
|
1545
|
+
return;
|
|
1546
|
+
}
|
|
1547
|
+
if (detected.length === 0) {
|
|
1548
|
+
console.log("[skill-forge] No known agent skill directories were found.");
|
|
1549
|
+
console.log(" Install a coding agent (Claude Code, Codex, Cursor, ...) or create a skills root, then re-run `skill-forge init`.");
|
|
1550
|
+
return;
|
|
1551
|
+
}
|
|
1552
|
+
if (options.defaults) {
|
|
1553
|
+
const config = buildConfig(detected, detected[0], void 0);
|
|
1554
|
+
saveConfig(config);
|
|
1555
|
+
printSummary(config);
|
|
1556
|
+
return;
|
|
1557
|
+
}
|
|
1558
|
+
if (!process.stdin.isTTY) {
|
|
1559
|
+
console.log("[skill-forge] Not an interactive terminal \u2014 run with --defaults for non-interactive setup.");
|
|
1560
|
+
return;
|
|
1561
|
+
}
|
|
1562
|
+
printDetectionResults(detected);
|
|
1563
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
1564
|
+
try {
|
|
1565
|
+
const selected = await promptMultiSelect(rl, detected);
|
|
1566
|
+
if (selected.length === 0) {
|
|
1567
|
+
console.log("[skill-forge] No targets selected \u2014 aborting, nothing written.");
|
|
1568
|
+
return;
|
|
1569
|
+
}
|
|
1570
|
+
const target = await promptDefaultTarget(rl, selected);
|
|
1571
|
+
const handoffAgent = await promptHandoffAgent(rl, detected);
|
|
1572
|
+
const config = buildConfig(selected, target, handoffAgent);
|
|
1573
|
+
saveConfig(config);
|
|
1574
|
+
printSummary(config);
|
|
1575
|
+
} finally {
|
|
1576
|
+
rl.close();
|
|
1577
|
+
}
|
|
1578
|
+
}
|
|
1579
|
+
async function maybeOfferInit(options = {}) {
|
|
1580
|
+
if (options.json || options.yes || !process.stdin.isTTY) return;
|
|
1581
|
+
if (existsSync10(getConfigPath())) return;
|
|
1582
|
+
console.log("[skill-forge] No config found \u2014 run `skill-forge init` to detect your agents and set gate targets.");
|
|
1583
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
1584
|
+
let answer = "";
|
|
1585
|
+
try {
|
|
1586
|
+
answer = (await rl.question("Run `skill-forge init` now? [y/N] ")).trim().toLowerCase();
|
|
1587
|
+
} finally {
|
|
1588
|
+
rl.close();
|
|
1589
|
+
}
|
|
1590
|
+
if (answer === "y" || answer === "yes") {
|
|
1591
|
+
await init();
|
|
1592
|
+
}
|
|
1593
|
+
}
|
|
1594
|
+
|
|
1149
1595
|
// src/commands/add.ts
|
|
1596
|
+
var PRO_FEATURE_LABEL = "overlap analysis, the provenance ledger, and the pending-ingestion queue";
|
|
1597
|
+
function logOutcome(message, json) {
|
|
1598
|
+
if (json) console.error(message);
|
|
1599
|
+
else console.log(message);
|
|
1600
|
+
}
|
|
1150
1601
|
async function add(source, options = {}) {
|
|
1602
|
+
await maybeOfferInit({ json: options.json, yes: options.yes });
|
|
1151
1603
|
const config = loadConfig();
|
|
1152
|
-
const targetRoot = options.target ?? config.skillsRoots[0];
|
|
1604
|
+
const targetRoot = options.target ?? config.defaultTarget ?? config.skillsRoots[0];
|
|
1153
1605
|
if (!targetRoot) {
|
|
1154
1606
|
throw new Error("no target skills root configured; pass --target <dir> or set skillsRoots in the config");
|
|
1155
1607
|
}
|
|
@@ -1167,15 +1619,15 @@ async function add(source, options = {}) {
|
|
|
1167
1619
|
` + entry.skillDirs.map((d) => ` - ${d}`).join("\n")
|
|
1168
1620
|
);
|
|
1169
1621
|
}
|
|
1170
|
-
const { safety, gateResult,
|
|
1622
|
+
const { safety, gateResult, proEnabled } = await runGate(entry.skillDirs[0], targetRoot, PRO_FEATURE_LABEL);
|
|
1171
1623
|
const { profile, overlap } = gateResult;
|
|
1172
|
-
|
|
1173
|
-
const decision = options.yes ? yesDecision(safety.verdict) : await promptDecision(safety.verdict);
|
|
1624
|
+
printGateOutput(gateResult, options.json);
|
|
1625
|
+
const decision = options.yes || options.json ? yesDecision(safety.verdict) : await promptDecision(safety.verdict);
|
|
1174
1626
|
let installedPath = null;
|
|
1175
1627
|
if (decision === "promote") {
|
|
1176
1628
|
const result = promote(entry, targetRoot);
|
|
1177
|
-
|
|
1178
|
-
if (
|
|
1629
|
+
logOutcome(`[skill-forge] promoted -> ${result.promoted.join(", ")}`, options.json);
|
|
1630
|
+
if (proEnabled) {
|
|
1179
1631
|
const ledgerPath = appendProvenance(
|
|
1180
1632
|
{
|
|
1181
1633
|
name: profile.name ?? entry.id,
|
|
@@ -1189,19 +1641,19 @@ async function add(source, options = {}) {
|
|
|
1189
1641
|
},
|
|
1190
1642
|
targetRoot
|
|
1191
1643
|
);
|
|
1192
|
-
|
|
1644
|
+
logOutcome(`[skill-forge] provenance recorded -> ${ledgerPath}`, options.json);
|
|
1193
1645
|
}
|
|
1194
1646
|
installedPath = result.promoted[0] ?? null;
|
|
1195
1647
|
} else if (decision === "reject") {
|
|
1196
1648
|
reject(entry);
|
|
1197
|
-
|
|
1649
|
+
logOutcome("[skill-forge] rejected \u2014 quarantine sandbox removed", options.json);
|
|
1198
1650
|
if (safety.verdict === "block") process.exitCode = 1;
|
|
1199
1651
|
return;
|
|
1200
1652
|
} else {
|
|
1201
1653
|
hold(entry);
|
|
1202
|
-
|
|
1654
|
+
logOutcome(`[skill-forge] held in quarantine -> ${entry.quarantinePath}`, options.json);
|
|
1203
1655
|
}
|
|
1204
|
-
if (
|
|
1656
|
+
if (proEnabled) {
|
|
1205
1657
|
appendQueueEntry({
|
|
1206
1658
|
source,
|
|
1207
1659
|
sourceType: resolved.type,
|
|
@@ -1217,28 +1669,15 @@ async function add(source, options = {}) {
|
|
|
1217
1669
|
status: "pending"
|
|
1218
1670
|
});
|
|
1219
1671
|
if (options.ingest && installedPath) {
|
|
1220
|
-
|
|
1672
|
+
runHandoff(config, installedPath);
|
|
1221
1673
|
}
|
|
1222
|
-
} else {
|
|
1223
|
-
console.log(upgradeNotice("provenance ledger, pending-ingestion queue, and --ingest handoff"));
|
|
1224
|
-
}
|
|
1225
|
-
}
|
|
1226
|
-
function handoffToClaude(installedPath) {
|
|
1227
|
-
const command = `/rhize-meta:forge-ingest ${installedPath}`;
|
|
1228
|
-
const result = spawnSync("claude", ["-p", command], { stdio: "inherit" });
|
|
1229
|
-
const error = result.error;
|
|
1230
|
-
if (error?.code === "ENOENT") {
|
|
1231
|
-
console.log("[skill-forge] claude not found on PATH \u2014 run this yourself:");
|
|
1232
|
-
console.log(` claude -p "${command}"`);
|
|
1233
|
-
} else if (error) {
|
|
1234
|
-
console.error(`[skill-forge] failed to launch claude: ${error.message}`);
|
|
1235
1674
|
}
|
|
1236
1675
|
}
|
|
1237
1676
|
function yesDecision(verdict) {
|
|
1238
1677
|
return verdict === "block" ? "reject" : "promote";
|
|
1239
1678
|
}
|
|
1240
1679
|
async function promptDecision(verdict) {
|
|
1241
|
-
const rl =
|
|
1680
|
+
const rl = createInterface2({ input: process.stdin, output: process.stdout });
|
|
1242
1681
|
const defaultChoice = verdict === "block" ? "reject" : verdict === "warn" ? "hold" : "promote";
|
|
1243
1682
|
const prompt = `Promote, hold, or reject this skill? [promote/hold/reject] (default: ${defaultChoice}) `;
|
|
1244
1683
|
try {
|
|
@@ -1258,7 +1697,8 @@ ${prompt}`);
|
|
|
1258
1697
|
}
|
|
1259
1698
|
|
|
1260
1699
|
// src/commands/scan.ts
|
|
1261
|
-
async function scan(source) {
|
|
1700
|
+
async function scan(source, options = {}) {
|
|
1701
|
+
await maybeOfferInit({ json: options.json });
|
|
1262
1702
|
const config = loadConfig();
|
|
1263
1703
|
const skillsRoot = config.skillsRoots[0];
|
|
1264
1704
|
if (!skillsRoot) {
|
|
@@ -1279,7 +1719,7 @@ async function scan(source) {
|
|
|
1279
1719
|
);
|
|
1280
1720
|
}
|
|
1281
1721
|
const { safety, gateResult } = await runGate(entry.skillDirs[0], skillsRoot);
|
|
1282
|
-
|
|
1722
|
+
printGateOutput(gateResult, options.json);
|
|
1283
1723
|
if (safety.verdict === "block") {
|
|
1284
1724
|
process.exitCode = 1;
|
|
1285
1725
|
}
|
|
@@ -1289,8 +1729,9 @@ async function scan(source) {
|
|
|
1289
1729
|
}
|
|
1290
1730
|
|
|
1291
1731
|
// src/commands/list.ts
|
|
1292
|
-
import { join as
|
|
1732
|
+
import { join as join12 } from "path";
|
|
1293
1733
|
async function list() {
|
|
1734
|
+
await maybeOfferInit();
|
|
1294
1735
|
const config = loadConfig();
|
|
1295
1736
|
const { quarantineDir } = config;
|
|
1296
1737
|
const ids = listQuarantineIds(quarantineDir);
|
|
@@ -1301,14 +1742,14 @@ async function list() {
|
|
|
1301
1742
|
}
|
|
1302
1743
|
console.log(`[skill-forge] ${ids.length} held in quarantine (${quarantineDir}):`);
|
|
1303
1744
|
for (const id of ids) {
|
|
1304
|
-
const skillDirs = findSkillDirs(
|
|
1745
|
+
const skillDirs = findSkillDirs(join12(quarantineDir, id));
|
|
1305
1746
|
if (skillDirs.length === 0) {
|
|
1306
1747
|
console.log(` ${id} (no SKILL.md found)`);
|
|
1307
1748
|
continue;
|
|
1308
1749
|
}
|
|
1309
1750
|
const names = skillDirs.map((dir) => {
|
|
1310
1751
|
try {
|
|
1311
|
-
return readFrontmatter(
|
|
1752
|
+
return readFrontmatter(join12(dir, "SKILL.md")).name;
|
|
1312
1753
|
} catch {
|
|
1313
1754
|
return dir;
|
|
1314
1755
|
}
|
|
@@ -1319,6 +1760,7 @@ async function list() {
|
|
|
1319
1760
|
|
|
1320
1761
|
// src/commands/status.ts
|
|
1321
1762
|
async function status() {
|
|
1763
|
+
await maybeOfferInit();
|
|
1322
1764
|
const config = loadConfig();
|
|
1323
1765
|
const heldCount = listQuarantineIds(config.quarantineDir).length;
|
|
1324
1766
|
console.log("[skill-forge] status");
|
|
@@ -1333,9 +1775,25 @@ var require2 = createRequire(import.meta.url);
|
|
|
1333
1775
|
var pkg = require2("../package.json");
|
|
1334
1776
|
var program = new Command();
|
|
1335
1777
|
program.name("skill-forge").description("The supply-chain gate for agent skills").version(pkg.version);
|
|
1336
|
-
program.command("add <source>").description("Quarantine-install a skill and run it through the safety gate").option("-y, --yes", "skip the interactive prompt; honor the gate verdict automatically").option("-t, --target <dir>", "skills root to promote into (defaults to config skillsRoots[0])").option("--json", "print the gate result as JSON instead of the terminal report").option("--ingest", "after a successful promote, hand off to
|
|
1337
|
-
program.command("scan <source>").description("Run the safety gate against a skill without installing it").action(scan);
|
|
1778
|
+
program.command("add <source>").description("Quarantine-install a skill and run it through the safety gate").option("-y, --yes", "skip the interactive prompt; honor the gate verdict automatically").option("-t, --target <dir>", "skills root to promote into (defaults to config skillsRoots[0])").option("--json", "print the gate result as JSON instead of the terminal report").option("--ingest", "after a successful promote, hand off the bundled ingest prompt to a coding agent").action(add);
|
|
1779
|
+
program.command("scan <source>").description("Run the safety gate against a skill without installing it").option("--json", "print the gate result as JSON instead of the terminal report").action(scan);
|
|
1338
1780
|
program.command("list").description("List skills currently in quarantine").action(list);
|
|
1339
1781
|
program.command("status").description("Show skill-forge configuration and quarantine status").action(status);
|
|
1340
|
-
|
|
1782
|
+
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);
|
|
1783
|
+
try {
|
|
1784
|
+
await program.parseAsync();
|
|
1785
|
+
} catch (err) {
|
|
1786
|
+
if (err instanceof UserError) {
|
|
1787
|
+
console.error(`skill-forge: ${err.message}`);
|
|
1788
|
+
process.exit(1);
|
|
1789
|
+
}
|
|
1790
|
+
if (process.env.DEBUG === "skill-forge") {
|
|
1791
|
+
console.error(err);
|
|
1792
|
+
} else {
|
|
1793
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
1794
|
+
console.error(`skill-forge: unexpected error: ${message}`);
|
|
1795
|
+
console.error("Re-run with DEBUG=skill-forge to see the full stack trace.");
|
|
1796
|
+
}
|
|
1797
|
+
process.exit(1);
|
|
1798
|
+
}
|
|
1341
1799
|
//# sourceMappingURL=cli.js.map
|