@rhize/skill-forge 0.1.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/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 Error(
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 = "# Rhize Skill Forge \u2014 Provenance Ledger\n\nOne entry per external-skill ingestion decision.\n";
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 pro = hasProLicense();
1092
- const overlap = pro ? tryOverlap(candidatePath, skillsRoot) : void 0;
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: !pro,
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, pro };
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, pro } = await runGate(entry.skillDirs[0], targetRoot);
1622
+ const { safety, gateResult, proEnabled } = await runGate(entry.skillDirs[0], targetRoot, PRO_FEATURE_LABEL);
1171
1623
  const { profile, overlap } = gateResult;
1172
- console.log(options.json ? renderJson(gateResult) : renderReport(gateResult));
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
- console.log(`[skill-forge] promoted -> ${result.promoted.join(", ")}`);
1178
- if (pro) {
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
- console.log(`[skill-forge] provenance recorded -> ${ledgerPath}`);
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
- console.log("[skill-forge] rejected \u2014 quarantine sandbox removed");
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
- console.log(`[skill-forge] held in quarantine -> ${entry.quarantinePath}`);
1654
+ logOutcome(`[skill-forge] held in quarantine -> ${entry.quarantinePath}`, options.json);
1203
1655
  }
1204
- if (pro) {
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
- handoffToClaude(installedPath);
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 = createInterface({ input: process.stdin, output: process.stdout });
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
- console.log(renderReport(gateResult));
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 join10 } from "path";
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(join10(quarantineDir, id));
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(join10(dir, "SKILL.md")).name;
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 Claude via /rhize-meta:forge-ingest").action(add);
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
- await program.parseAsync();
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