@rhize/skill-forge 0.2.0 → 0.4.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,13 +5,13 @@ 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";
13
12
  import { homedir } from "os";
14
13
  import { join } from "path";
14
+ var DEFAULT_STRICTNESS = "block-high";
15
15
  function getHomeDir() {
16
16
  return process.env.SKILL_FORGE_HOME ?? join(homedir(), ".skill-forge");
17
17
  }
@@ -23,7 +23,7 @@ function defaultConfig() {
23
23
  return {
24
24
  skillsRoots: [join(process.cwd(), ".claude", "skills")],
25
25
  quarantineDir: join(home, "quarantine"),
26
- strictness: "block-high"
26
+ strictness: DEFAULT_STRICTNESS
27
27
  };
28
28
  }
29
29
  function loadConfig() {
@@ -38,20 +38,65 @@ function loadConfig() {
38
38
  skillsRoots: parsed.skillsRoots ?? defaults.skillsRoots,
39
39
  quarantineDir: parsed.quarantineDir ?? defaults.quarantineDir,
40
40
  strictness: parsed.strictness ?? defaults.strictness,
41
- licenseKey: parsed.licenseKey ?? defaults.licenseKey
41
+ licenseKey: parsed.licenseKey ?? defaults.licenseKey,
42
+ defaultTarget: parsed.defaultTarget ?? defaults.defaultTarget,
43
+ agents: parsed.agents ?? defaults.agents,
44
+ handoffCommand: parsed.handoffCommand ?? defaults.handoffCommand
42
45
  };
43
46
  }
47
+ function saveConfig(config) {
48
+ const home = getHomeDir();
49
+ if (!existsSync(home)) {
50
+ mkdirSync(home, { recursive: true });
51
+ }
52
+ writeFileSync(getConfigPath(), JSON.stringify(config, null, 2) + "\n", "utf-8");
53
+ }
44
54
 
45
55
  // src/install/resolve.ts
46
56
  import { existsSync as existsSync2 } from "fs";
57
+
58
+ // src/errors.ts
59
+ var UserError = class extends Error {
60
+ constructor(message) {
61
+ super(message);
62
+ this.name = "UserError";
63
+ }
64
+ };
65
+
66
+ // src/util.ts
67
+ import { statSync } from "fs";
68
+ import { homedir as homedir2 } from "os";
69
+ import { join as join2 } from "path";
70
+ function expandHome(p) {
71
+ if (p === "~") return homedir2();
72
+ if (p.startsWith("~/")) return join2(homedir2(), p.slice(2));
73
+ return p;
74
+ }
75
+ function stripQuotes(s) {
76
+ return s.replace(/^['"]+/, "").replace(/['"]+$/, "");
77
+ }
78
+ function isDir(p) {
79
+ try {
80
+ return statSync(p).isDirectory();
81
+ } catch {
82
+ return false;
83
+ }
84
+ }
85
+
86
+ // src/install/resolve.ts
47
87
  var GIT_URL_PATTERN = /^(git@|https?:\/\/|ssh:\/\/)/;
88
+ var LOCAL_PATH_PATTERN = /^(\.{1,2}\/|\/|~\/|~$)/;
48
89
  function resolve(source) {
49
90
  const value = source.trim();
91
+ if (value.startsWith("-")) {
92
+ throw new UserError(`skill source cannot start with "-" (looks like a flag, not a source): ${value}`);
93
+ }
50
94
  if (GIT_URL_PATTERN.test(value) || value.endsWith(".git")) {
51
95
  return { type: "git", value };
52
96
  }
53
- if (existsSync2(value)) {
54
- return { type: "local", value };
97
+ const localCandidate = expandHome(value);
98
+ if (existsSync2(localCandidate) || LOCAL_PATH_PATTERN.test(value)) {
99
+ return { type: "local", value: localCandidate };
55
100
  }
56
101
  return { type: "skills.sh", value };
57
102
  }
@@ -60,13 +105,13 @@ function resolve(source) {
60
105
  import { randomUUID } from "crypto";
61
106
  import { spawn } from "child_process";
62
107
  import { cpSync, existsSync as existsSync3, mkdirSync as mkdirSync2, readdirSync } from "fs";
63
- import { basename, join as join2, resolve as resolvePath } from "path";
108
+ import { basename, join as join3, resolve as resolvePath } from "path";
64
109
  var SKILL_MANIFEST = "SKILL.md";
65
110
  var SKIP_DIRS = /* @__PURE__ */ new Set([".git", "node_modules"]);
66
111
  async function quarantine(source, quarantineDir, deps = {}) {
67
112
  const exec = deps.exec ?? defaultExec;
68
113
  const id = randomUUID();
69
- const quarantinePath = join2(quarantineDir, id);
114
+ const quarantinePath = join3(quarantineDir, id);
70
115
  mkdirSync2(quarantinePath, { recursive: true });
71
116
  switch (source.type) {
72
117
  case "skills.sh": {
@@ -81,22 +126,28 @@ async function quarantine(source, quarantineDir, deps = {}) {
81
126
  break;
82
127
  }
83
128
  case "git": {
84
- const clonePath = join2(quarantinePath, deriveGitDirName(source.value));
85
- const result = await exec("git", ["clone", "--depth", "1", source.value, clonePath], {
129
+ const clonePath = join3(quarantinePath, deriveGitDirName(source.value));
130
+ const result = await exec("git", ["clone", "--depth", "1", "--", source.value, clonePath], {
86
131
  cwd: quarantinePath
87
132
  });
88
133
  if (result.code !== 0) {
89
- throw new Error(
134
+ throw new UserError(
90
135
  `git clone failed for "${source.value}" (exit ${result.code}): ${result.stderr || result.stdout}`
91
136
  );
92
137
  }
93
138
  break;
94
139
  }
95
140
  case "local": {
141
+ if (!existsSync3(source.value)) {
142
+ throw new UserError(`local path not found: ${source.value}`);
143
+ }
96
144
  const dirName = basename(resolvePath(source.value));
97
- cpSync(source.value, join2(quarantinePath, dirName), { recursive: true });
145
+ cpSync(source.value, join3(quarantinePath, dirName), { recursive: true });
98
146
  break;
99
147
  }
148
+ default: {
149
+ throw new UserError(`unknown source type: ${source.type}`);
150
+ }
100
151
  }
101
152
  return { id, quarantinePath, skillDirs: findSkillDirs(quarantinePath), source };
102
153
  }
@@ -118,7 +169,7 @@ function findSkillDirs(root) {
118
169
  }
119
170
  for (const entry of entries) {
120
171
  if (entry.isDirectory() && !SKIP_DIRS.has(entry.name)) {
121
- walk2(join2(dir, entry.name));
172
+ walk2(join3(dir, entry.name));
122
173
  }
123
174
  }
124
175
  }
@@ -144,11 +195,11 @@ function defaultExec(command, args, options) {
144
195
 
145
196
  // src/install/promote.ts
146
197
  import { cpSync as cpSync2, existsSync as existsSync4, mkdirSync as mkdirSync3, renameSync, rmSync } from "fs";
147
- import { basename as basename2, join as join3 } from "path";
198
+ import { basename as basename2, join as join4 } from "path";
148
199
  function promote(entry, targetSkillsRoot, options = {}) {
149
200
  const force = options.force ?? false;
150
201
  for (const skillDir of entry.skillDirs) {
151
- const dest = join3(targetSkillsRoot, basename2(skillDir));
202
+ const dest = join4(targetSkillsRoot, basename2(skillDir));
152
203
  if (existsSync4(dest) && !force) {
153
204
  throw new Error(`Refusing to overwrite existing skill dir: ${dest} (pass force to override)`);
154
205
  }
@@ -156,7 +207,7 @@ function promote(entry, targetSkillsRoot, options = {}) {
156
207
  mkdirSync3(targetSkillsRoot, { recursive: true });
157
208
  const promoted = [];
158
209
  for (const skillDir of entry.skillDirs) {
159
- const dest = join3(targetSkillsRoot, basename2(skillDir));
210
+ const dest = join4(targetSkillsRoot, basename2(skillDir));
160
211
  if (existsSync4(dest)) {
161
212
  rmSync(dest, { recursive: true, force: true });
162
213
  }
@@ -184,115 +235,11 @@ function moveDir(src, dest) {
184
235
  }
185
236
  }
186
237
 
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
238
  // src/provenance.ts
292
- import { appendFileSync, existsSync as existsSync5, statSync, writeFileSync as writeFileSync2 } from "fs";
293
- import { join as join4 } from "path";
239
+ import { appendFileSync, existsSync as existsSync5, statSync as statSync2, writeFileSync as writeFileSync2 } from "fs";
240
+ import { join as join5 } from "path";
294
241
  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";
242
+ var LEDGER_HEADER = "# Skill Forge \u2014 Provenance Ledger\n\nOne entry per external-skill ingestion decision.\n";
296
243
  function todayLocalIsoDate() {
297
244
  const d = /* @__PURE__ */ new Date();
298
245
  const year = d.getFullYear();
@@ -304,7 +251,7 @@ function appendProvenance(entry, skillsRoot) {
304
251
  if (!FORGE_VERBS.includes(entry.verb)) {
305
252
  throw new Error(`verb must be one of ${FORGE_VERBS.join(", ")}`);
306
253
  }
307
- if (!existsSync5(skillsRoot) || !statSync(skillsRoot).isDirectory()) {
254
+ if (!existsSync5(skillsRoot) || !statSync2(skillsRoot).isDirectory()) {
308
255
  throw new Error(`skills-root not a directory: ${skillsRoot}`);
309
256
  }
310
257
  const today = todayLocalIsoDate();
@@ -321,7 +268,7 @@ function appendProvenance(entry, skillsRoot) {
321
268
  - **Drift check:** \`${driftCheck}\`
322
269
  - **Notes:** ${entry.notes ?? ""}
323
270
  `;
324
- const ledgerPath = join4(skillsRoot, "SOURCES.md");
271
+ const ledgerPath = join5(skillsRoot, "SOURCES.md");
325
272
  if (!existsSync5(ledgerPath)) {
326
273
  writeFileSync2(ledgerPath, LEDGER_HEADER, "utf-8");
327
274
  }
@@ -344,28 +291,6 @@ import {
344
291
  } from "fs";
345
292
  import { tmpdir } from "os";
346
293
  import { basename as basename3, dirname, extname, join as join6, resolve as resolve2 } from "path";
347
-
348
- // src/util.ts
349
- import { statSync as statSync2 } from "fs";
350
- import { homedir as homedir2 } from "os";
351
- import { join as join5 } from "path";
352
- function expandHome(p) {
353
- if (p === "~") return homedir2();
354
- if (p.startsWith("~/")) return join5(homedir2(), p.slice(2));
355
- return p;
356
- }
357
- function stripQuotes(s) {
358
- return s.replace(/^['"]+/, "").replace(/['"]+$/, "");
359
- }
360
- function isDir(p) {
361
- try {
362
- return statSync2(p).isDirectory();
363
- } catch {
364
- return false;
365
- }
366
- }
367
-
368
- // src/gate/profile.ts
369
294
  var SPDX_HINTS = {
370
295
  mit: "MIT (permissive)",
371
296
  apache: "Apache-2.0 (permissive)",
@@ -695,7 +620,11 @@ import { execFile } from "child_process";
695
620
  import { delimiter, extname as extname2, join as join7 } from "path";
696
621
  import { promisify } from "util";
697
622
  var execFileAsync = promisify(execFile);
698
- var BLOCK_SEVERITIES = /* @__PURE__ */ new Set(["HIGH", "CRITICAL"]);
623
+ var BLOCK_SEVERITIES_BY_STRICTNESS = {
624
+ "block-high": /* @__PURE__ */ new Set(["HIGH", "CRITICAL"]),
625
+ "block-medium": /* @__PURE__ */ new Set(["MEDIUM", "HIGH", "CRITICAL"]),
626
+ "warn-only": /* @__PURE__ */ new Set()
627
+ };
699
628
  var DENY_RULES = [
700
629
  {
701
630
  rule: "curl-pipe-shell",
@@ -877,14 +806,15 @@ async function scanWithSkillSpector(targetPath) {
877
806
  return [];
878
807
  }
879
808
  }
880
- function verdictFor(findings) {
881
- if (findings.some((f) => BLOCK_SEVERITIES.has(f.severity))) return "block";
809
+ function verdictFor(findings, strictness) {
810
+ const blockSeverities = BLOCK_SEVERITIES_BY_STRICTNESS[strictness];
811
+ if (findings.some((f) => blockSeverities.has(f.severity))) return "block";
882
812
  if (findings.length > 0) return "warn";
883
813
  return "pass";
884
814
  }
885
- async function scanSafety(targetPath) {
815
+ async function scanSafety(targetPath, strictness = DEFAULT_STRICTNESS) {
886
816
  const findings = [...scanStaticRules(targetPath), ...await scanWithSkillSpector(targetPath)];
887
- return { findings, verdict: verdictFor(findings) };
817
+ return { findings, verdict: verdictFor(findings, strictness) };
888
818
  }
889
819
 
890
820
  // src/gate/overlap.ts
@@ -1064,12 +994,159 @@ function scanOverlap(candidatePath, skillsRoot) {
1064
994
  };
1065
995
  }
1066
996
 
997
+ // src/license.ts
998
+ import { verify as cryptoVerify } from "crypto";
999
+ var LICENSE_PREFIX = "skf1";
1000
+ var BETA_FREE = true;
1001
+ var PUBLIC_KEY_PEM = `-----BEGIN PUBLIC KEY-----
1002
+ MCowBQYDK2VwAyEAWSVbcX9u70IsN1O5zS5z+AYtAUVz7ExT7h+1gT9x+wQ=
1003
+ -----END PUBLIC KEY-----
1004
+ `;
1005
+ function readLicenseKey() {
1006
+ const fromEnv = process.env.SKILL_FORGE_LICENSE?.trim();
1007
+ if (fromEnv) return fromEnv;
1008
+ return loadConfig().licenseKey?.trim() || void 0;
1009
+ }
1010
+ function isLicensePayload(value) {
1011
+ if (typeof value !== "object" || value === null) return false;
1012
+ const p = value;
1013
+ return typeof p.name === "string" && typeof p.email === "string" && typeof p.seats === "number" && typeof p.exp === "string";
1014
+ }
1015
+ function checkLicense(rawKey = readLicenseKey()) {
1016
+ if (!rawKey) {
1017
+ return { valid: false, reason: "missing" };
1018
+ }
1019
+ const parts = rawKey.trim().split(".");
1020
+ if (parts.length !== 3 || parts[0] !== LICENSE_PREFIX) {
1021
+ return { valid: false, reason: "malformed" };
1022
+ }
1023
+ const [, payloadPart, signaturePart] = parts;
1024
+ const payloadBuffer = Buffer.from(payloadPart, "base64url");
1025
+ const signature = Buffer.from(signaturePart, "base64url");
1026
+ let payload;
1027
+ try {
1028
+ payload = JSON.parse(payloadBuffer.toString("utf-8"));
1029
+ } catch {
1030
+ return { valid: false, reason: "malformed" };
1031
+ }
1032
+ if (!isLicensePayload(payload)) {
1033
+ return { valid: false, reason: "malformed" };
1034
+ }
1035
+ let signatureValid;
1036
+ try {
1037
+ signatureValid = cryptoVerify(null, payloadBuffer, PUBLIC_KEY_PEM, signature);
1038
+ } catch {
1039
+ return { valid: false, reason: "malformed" };
1040
+ }
1041
+ if (!signatureValid) {
1042
+ return { valid: false, reason: "signature" };
1043
+ }
1044
+ const expMs = new Date(payload.exp).getTime();
1045
+ if (Number.isNaN(expMs) || expMs < Date.now()) {
1046
+ return { valid: false, reason: "expired" };
1047
+ }
1048
+ return { valid: true, payload };
1049
+ }
1050
+ function upgradeNotice(feature) {
1051
+ return `[skill-forge] Pro feature (${feature}) \u2014 set SKILL_FORGE_LICENSE or config.licenseKey to a valid license to unlock.`;
1052
+ }
1053
+ function invalidLicenseReasonText(reason) {
1054
+ switch (reason) {
1055
+ case "malformed":
1056
+ return "is malformed";
1057
+ case "signature":
1058
+ return "has an invalid signature";
1059
+ case "expired":
1060
+ return "has expired";
1061
+ default:
1062
+ return "is invalid";
1063
+ }
1064
+ }
1065
+ function proFeatureStatus(feature, betaFree = BETA_FREE) {
1066
+ const status2 = checkLicense();
1067
+ if (status2.valid) {
1068
+ return { enabled: true };
1069
+ }
1070
+ if (!betaFree) {
1071
+ return { enabled: false, notice: upgradeNotice(feature) };
1072
+ }
1073
+ const betaNotice = `Pro feature (${feature}) \u2014 free during the 0.x beta; will require a license at 1.0.`;
1074
+ if (status2.reason === "missing") {
1075
+ return { enabled: true, notice: betaNotice };
1076
+ }
1077
+ return {
1078
+ enabled: true,
1079
+ notice: `${betaNotice} (Note: your license key ${invalidLicenseReasonText(status2.reason)} \u2014 ignored while Pro is free.)`
1080
+ };
1081
+ }
1082
+
1083
+ // src/report/render.ts
1084
+ var BORDER = "\u2501".repeat(55);
1085
+ var LABEL_WIDTH = 20;
1086
+ function padLabel(text) {
1087
+ return text.padEnd(LABEL_WIDTH);
1088
+ }
1089
+ function renderListField(labelText, items, placeholder) {
1090
+ const head = `${padLabel(labelText)}: `;
1091
+ if (!items || items.length === 0) {
1092
+ return `${head}${placeholder}`;
1093
+ }
1094
+ const bullets = items.map((item) => ` - ${item}`).join("\n");
1095
+ return `${head.trimEnd()}
1096
+ ${bullets}`;
1097
+ }
1098
+ function renderSafetyLine(safety) {
1099
+ const detail = [safety.severity, safety.score !== void 0 ? `score ${safety.score}` : void 0].filter((v) => Boolean(v)).join(", ");
1100
+ const withDetail = detail ? `${safety.verdict} (${detail})` : safety.verdict;
1101
+ return safety.recommendation ? `${withDetail} \u2014 ${safety.recommendation}` : withDetail;
1102
+ }
1103
+ function renderFindingLine(finding) {
1104
+ const severity = finding.severity ?? "UNKNOWN";
1105
+ const rule = finding.rule ?? "unknown-rule";
1106
+ return ` - [${severity}] ${rule}${finding.message ? `: ${finding.message}` : ""}`;
1107
+ }
1108
+ function renderFindingsLines(findings) {
1109
+ if (!findings?.length) return [];
1110
+ return [`${padLabel("Findings")}:`, ...findings.map(renderFindingLine)];
1111
+ }
1112
+ function renderReport(result) {
1113
+ const { profile, overlap, overlapLocked, safety, worthTaking, leaveBehind } = result;
1114
+ const name = profile.name ?? "unknown";
1115
+ const version = profile.version ?? "unknown";
1116
+ const license = profile.license ?? "UNKNOWN";
1117
+ const overlapLines = overlapLocked ? [`${padLabel("Overlap analysis")}: ${upgradeNotice("overlap analysis")}`] : overlap ? [
1118
+ `${padLabel("Nearest existing skill")}: ${overlap.nearest} (overlap ${overlap.nearestScore})`,
1119
+ `${padLabel("Recommended verb")}: ${overlap.suggestedVerb}`
1120
+ ] : [`${padLabel("Nearest existing skill")}: n/a \u2014 no skills root configured`, `${padLabel("Recommended verb")}: n/a`];
1121
+ const lines = [
1122
+ BORDER,
1123
+ `\u{1F528} Forge: ${name} (v${version}, ${license})`,
1124
+ BORDER,
1125
+ ...overlapLines,
1126
+ renderListField("What's worth taking", worthTaking, "(pending \u2014 not yet determined)"),
1127
+ renderListField("What to leave behind", leaveBehind, "(pending \u2014 not yet determined)"),
1128
+ `${padLabel("Safety verdict")}: ${renderSafetyLine(safety)}`,
1129
+ ...renderFindingsLines(safety.findings),
1130
+ BORDER
1131
+ ];
1132
+ return lines.join("\n");
1133
+ }
1134
+ function renderJson(result) {
1135
+ return JSON.stringify(result, null, 2);
1136
+ }
1137
+
1067
1138
  // src/commands/gatePipeline.ts
1068
1139
  var SAFETY_VERDICT_MAP = {
1069
1140
  pass: "ALLOW",
1070
1141
  warn: "CAUTION",
1071
1142
  block: "BLOCK"
1072
1143
  };
1144
+ function printGateOutput(gateResult, json) {
1145
+ if (!json) {
1146
+ for (const notice of gateResult.notices ?? []) console.log(`[skill-forge] ${notice}`);
1147
+ }
1148
+ console.log(json ? renderJson(gateResult) : renderReport(gateResult));
1149
+ }
1073
1150
  function asSkillProfile(candidatePath) {
1074
1151
  const profile = profileSkill(candidatePath);
1075
1152
  if (profile.kind !== "skill") {
@@ -1085,15 +1162,19 @@ function tryOverlap(candidatePath, skillsRoot) {
1085
1162
  return void 0;
1086
1163
  }
1087
1164
  }
1088
- async function runGate(candidatePath, skillsRoot) {
1165
+ async function runGate(candidatePath, skillsRoot, {
1166
+ proFeatureLabel = "overlap analysis",
1167
+ strictness = DEFAULT_STRICTNESS
1168
+ } = {}) {
1089
1169
  const profile = asSkillProfile(candidatePath);
1090
- const safety = await scanSafety(candidatePath);
1091
- const pro = hasProLicense();
1092
- const overlap = pro ? tryOverlap(candidatePath, skillsRoot) : void 0;
1170
+ const safety = await scanSafety(candidatePath, strictness);
1171
+ const proStatus = proFeatureStatus(proFeatureLabel);
1172
+ const overlap = proStatus.enabled ? tryOverlap(candidatePath, skillsRoot) : void 0;
1093
1173
  const gateResult = {
1094
1174
  profile,
1095
1175
  overlap,
1096
- overlapLocked: !pro,
1176
+ overlapLocked: !proStatus.enabled,
1177
+ notices: proStatus.notice ? [proStatus.notice] : void 0,
1097
1178
  safety: {
1098
1179
  verdict: SAFETY_VERDICT_MAP[safety.verdict],
1099
1180
  findings: safety.findings.map((f) => ({
@@ -1103,7 +1184,7 @@ async function runGate(candidatePath, skillsRoot) {
1103
1184
  }))
1104
1185
  }
1105
1186
  };
1106
- return { safety, gateResult, pro };
1187
+ return { safety, gateResult, proEnabled: proStatus.enabled };
1107
1188
  }
1108
1189
 
1109
1190
  // src/queue.ts
@@ -1146,10 +1227,400 @@ function generateId(existingIds) {
1146
1227
  return id;
1147
1228
  }
1148
1229
 
1230
+ // src/handoff.ts
1231
+ import { spawnSync } from "child_process";
1232
+ import { existsSync as existsSync9 } from "fs";
1233
+ import { dirname as dirname3, join as join10 } from "path";
1234
+ import { fileURLToPath } from "url";
1235
+ var KNOWN_AGENT_BINARIES = ["claude", "codex", "cursor-agent", "windsurf", "opencode", "gemini"];
1236
+ var HANDOFF_INSTRUCTION = "Read {prompt} and follow its instructions for the skill installed at {path}";
1237
+ function resolvePromptPath() {
1238
+ const here = dirname3(fileURLToPath(import.meta.url));
1239
+ const builtCandidate = join10(here, "ingest-prompt.md");
1240
+ if (existsSync9(builtCandidate)) return builtCandidate;
1241
+ return join10(here, "..", "assets", "ingest-prompt.md");
1242
+ }
1243
+ function defaultIsOnPath(bin) {
1244
+ const finder = process.platform === "win32" ? "where" : "which";
1245
+ const result = spawnSync(finder, [bin], { stdio: "ignore" });
1246
+ return result.status === 0;
1247
+ }
1248
+ function substitute(template, vars) {
1249
+ return template.map((token) => token.replace(/\{(\w+)\}/g, (match, key) => vars[key] ?? match));
1250
+ }
1251
+ function buildHandoffCommand(config, installedPath, deps = {}) {
1252
+ const isOnPath = deps.isOnPath ?? defaultIsOnPath;
1253
+ const promptPath = resolvePromptPath();
1254
+ const vars = { path: installedPath, prompt: promptPath };
1255
+ if (config.handoffCommand && config.handoffCommand.length > 0) {
1256
+ return { argv: substitute(config.handoffCommand, vars), source: "template" };
1257
+ }
1258
+ for (const bin of KNOWN_AGENT_BINARIES) {
1259
+ if (isOnPath(bin)) {
1260
+ return { argv: substitute([bin, "-p", HANDOFF_INSTRUCTION], vars), source: "detected" };
1261
+ }
1262
+ }
1263
+ return { argv: [], source: "none" };
1264
+ }
1265
+ function printOnlyMessage(installedPath) {
1266
+ const promptPath = resolvePromptPath();
1267
+ return `[skill-forge] no agent handoff available \u2014 run one yourself:
1268
+ Prompt: ${promptPath}
1269
+ Skill path: ${installedPath}`;
1270
+ }
1271
+ function runHandoff(config, installedPath, options = {}) {
1272
+ const command = buildHandoffCommand(config, installedPath, options);
1273
+ const spawn2 = options.spawn ?? spawnSync;
1274
+ if (command.source === "none" || command.argv.length === 0) {
1275
+ const printed = printOnlyMessage(installedPath);
1276
+ console.log(printed);
1277
+ return { ran: false, command, printed };
1278
+ }
1279
+ const [bin, ...args] = command.argv;
1280
+ const result = spawn2(bin, args, { stdio: options.stdio ?? "inherit" });
1281
+ const error = result.error;
1282
+ if (error?.code === "ENOENT") {
1283
+ const printed = printOnlyMessage(installedPath);
1284
+ console.log(`[skill-forge] ${bin} not found on PATH \u2014 run this yourself:`);
1285
+ console.log(printed);
1286
+ return { ran: false, command, printed, error: error.message };
1287
+ }
1288
+ if (error) {
1289
+ console.error(`[skill-forge] failed to launch handoff: ${error.message}`);
1290
+ return { ran: false, command, error: error.message };
1291
+ }
1292
+ return { ran: true, command };
1293
+ }
1294
+
1295
+ // src/commands/init.ts
1296
+ import { existsSync as existsSync10 } from "fs";
1297
+ import { homedir as homedir4 } from "os";
1298
+ import { createInterface } from "readline/promises";
1299
+
1300
+ // src/agents.ts
1301
+ import { homedir as homedir3 } from "os";
1302
+ import { join as join11, resolve as resolve4 } from "path";
1303
+ function projectPath(root, cwd) {
1304
+ return root ? resolve4(cwd, root) : void 0;
1305
+ }
1306
+ function globalPath(root, home) {
1307
+ if (!root) return void 0;
1308
+ if (root === "~") return home;
1309
+ if (root.startsWith("~/")) return join11(home, root.slice(2));
1310
+ return root;
1311
+ }
1312
+ function makeAgent(spec) {
1313
+ const agent = {
1314
+ id: spec.id,
1315
+ displayName: spec.displayName,
1316
+ projectRoot: spec.projectRoot,
1317
+ globalRoot: spec.globalRoot,
1318
+ verified: spec.verified ?? true,
1319
+ defaultHandoffTemplate: spec.defaultHandoffTemplate,
1320
+ detect: (cwd, home) => detectAgentRoot(agent, cwd, home) !== void 0
1321
+ };
1322
+ return agent;
1323
+ }
1324
+ function resolveAgentRoot(agent, scope, cwd = process.cwd(), home = homedir3()) {
1325
+ return scope === "project" ? projectPath(agent.projectRoot, cwd) : globalPath(agent.globalRoot, home);
1326
+ }
1327
+ function detectAgentRoot(agent, cwd = process.cwd(), home = homedir3()) {
1328
+ for (const scope of ["project", "global"]) {
1329
+ const root = resolveAgentRoot(agent, scope, cwd, home);
1330
+ if (root && isDir(root)) return { scope, root };
1331
+ }
1332
+ return void 0;
1333
+ }
1334
+ var AGENTS = [
1335
+ { id: "aider-desk", displayName: "AiderDesk", projectRoot: ".aider-desk/skills", globalRoot: "~/.aider-desk/skills" },
1336
+ { id: "amp", displayName: "Amp", projectRoot: ".agents/skills", globalRoot: "~/.config/agents/skills" },
1337
+ { id: "replit", displayName: "Replit", projectRoot: ".agents/skills", globalRoot: "~/.config/agents/skills" },
1338
+ { id: "universal", displayName: "Universal", projectRoot: ".agents/skills", globalRoot: "~/.config/agents/skills" },
1339
+ { id: "antigravity", displayName: "Antigravity", projectRoot: ".agents/skills", globalRoot: "~/.gemini/antigravity/skills" },
1340
+ {
1341
+ id: "antigravity-cli",
1342
+ displayName: "Antigravity CLI",
1343
+ projectRoot: ".agents/skills",
1344
+ globalRoot: "~/.gemini/antigravity-cli/skills"
1345
+ },
1346
+ { id: "astrbot", displayName: "AstrBot", projectRoot: "data/skills", globalRoot: "~/.astrbot/data/skills" },
1347
+ { id: "autohand-code", displayName: "Autohand Code CLI", projectRoot: ".autohand/skills", globalRoot: "~/.autohand/skills" },
1348
+ { id: "augment", displayName: "Augment", projectRoot: ".augment/skills", globalRoot: "~/.augment/skills" },
1349
+ { id: "bob", displayName: "IBM Bob", projectRoot: ".bob/skills", globalRoot: "~/.bob/skills" },
1350
+ {
1351
+ id: "claude-code",
1352
+ displayName: "Claude Code",
1353
+ projectRoot: ".claude/skills",
1354
+ globalRoot: "~/.claude/skills",
1355
+ // `claude -p "<prompt>"` runs one non-interactive turn and exits. Verified 2026-07-11:
1356
+ // https://code.claude.com/docs/en/headless
1357
+ defaultHandoffTemplate: ["claude", "-p", HANDOFF_INSTRUCTION]
1358
+ },
1359
+ { id: "openclaw", displayName: "OpenClaw", projectRoot: "skills", globalRoot: "~/.openclaw/skills" },
1360
+ { id: "cline", displayName: "Cline", projectRoot: ".agents/skills", globalRoot: "~/.agents/skills" },
1361
+ { id: "dexto", displayName: "Dexto", projectRoot: ".agents/skills", globalRoot: "~/.agents/skills" },
1362
+ { id: "kimi-code-cli", displayName: "Kimi Code CLI", projectRoot: ".agents/skills", globalRoot: "~/.agents/skills" },
1363
+ { id: "loaf", displayName: "Loaf", projectRoot: ".agents/skills", globalRoot: "~/.agents/skills" },
1364
+ { id: "warp", displayName: "Warp", projectRoot: ".agents/skills", globalRoot: "~/.agents/skills" },
1365
+ { id: "zed", displayName: "Zed", projectRoot: ".agents/skills", globalRoot: "~/.agents/skills" },
1366
+ {
1367
+ id: "codearts-agent",
1368
+ displayName: "CodeArts Agent",
1369
+ projectRoot: ".codeartsdoer/skills",
1370
+ globalRoot: "~/.codeartsdoer/skills"
1371
+ },
1372
+ { id: "codebuddy", displayName: "CodeBuddy", projectRoot: ".codebuddy/skills", globalRoot: "~/.codebuddy/skills" },
1373
+ { id: "codemaker", displayName: "Codemaker", projectRoot: ".codemaker/skills", globalRoot: "~/.codemaker/skills" },
1374
+ { id: "codestudio", displayName: "Code Studio", projectRoot: ".codestudio/skills", globalRoot: "~/.codestudio/skills" },
1375
+ {
1376
+ id: "codex",
1377
+ displayName: "Codex",
1378
+ projectRoot: ".agents/skills",
1379
+ globalRoot: "~/.codex/skills",
1380
+ // `codex exec "<prompt>"` runs one non-interactive task and prints the final message.
1381
+ // Verified 2026-07-11: https://developers.openai.com/codex/noninteractive
1382
+ defaultHandoffTemplate: ["codex", "exec", HANDOFF_INSTRUCTION]
1383
+ },
1384
+ {
1385
+ id: "command-code",
1386
+ displayName: "Command Code",
1387
+ projectRoot: ".commandcode/skills",
1388
+ globalRoot: "~/.commandcode/skills"
1389
+ },
1390
+ { id: "continue", displayName: "Continue", projectRoot: ".continue/skills", globalRoot: "~/.continue/skills" },
1391
+ { id: "cortex", displayName: "Cortex Code", projectRoot: ".cortex/skills", globalRoot: "~/.snowflake/cortex/skills" },
1392
+ { id: "crush", displayName: "Crush", projectRoot: ".crush/skills", globalRoot: "~/.config/crush/skills" },
1393
+ { id: "cursor", displayName: "Cursor", projectRoot: ".agents/skills", globalRoot: "~/.cursor/skills" },
1394
+ {
1395
+ id: "deepagents",
1396
+ displayName: "Deep Agents",
1397
+ projectRoot: ".agents/skills",
1398
+ globalRoot: "~/.deepagents/agent/skills"
1399
+ },
1400
+ { id: "devin", displayName: "Devin for Terminal", projectRoot: ".devin/skills", globalRoot: "~/.config/devin/skills" },
1401
+ { id: "droid", displayName: "Droid", projectRoot: ".factory/skills", globalRoot: "~/.factory/skills" },
1402
+ { id: "eve", displayName: "Eve", projectRoot: "agent/skills" },
1403
+ { id: "firebender", displayName: "Firebender", projectRoot: ".agents/skills", globalRoot: "~/.firebender/skills" },
1404
+ { id: "forgecode", displayName: "ForgeCode", projectRoot: ".forge/skills", globalRoot: "~/.forge/skills" },
1405
+ {
1406
+ id: "gemini-cli",
1407
+ displayName: "Gemini CLI",
1408
+ projectRoot: ".agents/skills",
1409
+ globalRoot: "~/.gemini/skills",
1410
+ // `gemini -p "<prompt>"` executes a single turn headlessly and exits. Verified 2026-07-11:
1411
+ // https://google-gemini.github.io/gemini-cli/docs/cli/headless.html
1412
+ defaultHandoffTemplate: ["gemini", "-p", HANDOFF_INSTRUCTION]
1413
+ },
1414
+ {
1415
+ id: "github-copilot",
1416
+ displayName: "GitHub Copilot",
1417
+ projectRoot: ".agents/skills",
1418
+ globalRoot: "~/.copilot/skills"
1419
+ },
1420
+ { id: "goose", displayName: "Goose", projectRoot: ".goose/skills", globalRoot: "~/.config/goose/skills" },
1421
+ { id: "hermes-agent", displayName: "Hermes Agent", projectRoot: ".hermes/skills", globalRoot: "~/.hermes/skills" },
1422
+ {
1423
+ id: "inference-sh",
1424
+ displayName: "inference.sh",
1425
+ projectRoot: ".inferencesh/skills",
1426
+ globalRoot: "~/.inferencesh/skills"
1427
+ },
1428
+ { id: "jazz", displayName: "Jazz", projectRoot: ".jazz/skills", globalRoot: "~/.jazz/skills" },
1429
+ { id: "junie", displayName: "Junie", projectRoot: ".junie/skills", globalRoot: "~/.junie/skills" },
1430
+ { id: "iflow-cli", displayName: "iFlow CLI", projectRoot: ".iflow/skills", globalRoot: "~/.iflow/skills" },
1431
+ { id: "kilo", displayName: "Kilo Code", projectRoot: ".kilocode/skills", globalRoot: "~/.kilocode/skills" },
1432
+ { id: "kiro-cli", displayName: "Kiro CLI", projectRoot: ".kiro/skills", globalRoot: "~/.kiro/skills" },
1433
+ { id: "kode", displayName: "Kode", projectRoot: ".kode/skills", globalRoot: "~/.kode/skills" },
1434
+ { id: "lingma", displayName: "Lingma", projectRoot: ".lingma/skills", globalRoot: "~/.lingma/skills" },
1435
+ { id: "mcpjam", displayName: "MCPJam", projectRoot: ".mcpjam/skills", globalRoot: "~/.mcpjam/skills" },
1436
+ { id: "mistral-vibe", displayName: "Mistral Vibe", projectRoot: ".vibe/skills", globalRoot: "~/.vibe/skills" },
1437
+ { id: "moxby", displayName: "Moxby", projectRoot: ".moxby/skills", globalRoot: "~/.moxby/skills" },
1438
+ { id: "mux", displayName: "Mux", projectRoot: ".mux/skills", globalRoot: "~/.mux/skills" },
1439
+ {
1440
+ id: "opencode",
1441
+ displayName: "OpenCode",
1442
+ projectRoot: ".agents/skills",
1443
+ globalRoot: "~/.config/opencode/skills",
1444
+ // `opencode run "<prompt>"` runs one non-interactive prompt outside the TUI.
1445
+ // Verified 2026-07-11: https://opencode.ai/docs/cli/
1446
+ defaultHandoffTemplate: ["opencode", "run", HANDOFF_INSTRUCTION]
1447
+ },
1448
+ { id: "openhands", displayName: "OpenHands", projectRoot: ".openhands/skills", globalRoot: "~/.openhands/skills" },
1449
+ { id: "ona", displayName: "Ona", projectRoot: ".ona/skills", globalRoot: "~/.ona/skills" },
1450
+ { id: "pi", displayName: "Pi", projectRoot: ".pi/skills", globalRoot: "~/.pi/agent/skills" },
1451
+ { id: "qoder", displayName: "Qoder", projectRoot: ".qoder/skills", globalRoot: "~/.qoder/skills" },
1452
+ { id: "qoder-cn", displayName: "Qoder CN", projectRoot: ".qoder/skills", globalRoot: "~/.qoder-cn/skills" },
1453
+ { id: "qwen-code", displayName: "Qwen Code", projectRoot: ".qwen/skills", globalRoot: "~/.qwen/skills" },
1454
+ { id: "reasonix", displayName: "Reasonix", projectRoot: ".reasonix/skills", globalRoot: "~/.reasonix/skills" },
1455
+ { id: "rovodev", displayName: "Rovo Dev", projectRoot: ".rovodev/skills", globalRoot: "~/.rovodev/skills" },
1456
+ { id: "roo", displayName: "Roo Code", projectRoot: ".roo/skills", globalRoot: "~/.roo/skills" },
1457
+ {
1458
+ id: "tabnine-cli",
1459
+ displayName: "Tabnine CLI",
1460
+ projectRoot: ".tabnine/agent/skills",
1461
+ globalRoot: "~/.tabnine/agent/skills"
1462
+ },
1463
+ { id: "terramind", displayName: "Terramind", projectRoot: ".terramind/skills", globalRoot: "~/.terramind/skills" },
1464
+ { id: "tinycloud", displayName: "Tinycloud", projectRoot: ".tinycloud/skills", globalRoot: "~/.tinycloud/skills" },
1465
+ { id: "trae", displayName: "Trae", projectRoot: ".trae/skills", globalRoot: "~/.trae/skills" },
1466
+ { id: "trae-cn", displayName: "Trae CN", projectRoot: ".trae/skills", globalRoot: "~/.trae-cn/skills" },
1467
+ { id: "windsurf", displayName: "Windsurf", projectRoot: ".windsurf/skills", globalRoot: "~/.codeium/windsurf/skills" },
1468
+ { id: "zcode", displayName: "ZCode", projectRoot: ".zcode/skills", globalRoot: "~/.zcode/skills" },
1469
+ { id: "zencoder", displayName: "Zencoder", projectRoot: ".zencoder/skills", globalRoot: "~/.zencoder/skills" },
1470
+ { id: "zenflow", displayName: "Zenflow", projectRoot: ".zencoder/skills", globalRoot: "~/.zencoder/skills" },
1471
+ { id: "neovate", displayName: "Neovate", projectRoot: ".neovate/skills", globalRoot: "~/.neovate/skills" },
1472
+ { id: "pochi", displayName: "Pochi", projectRoot: ".pochi/skills", globalRoot: "~/.pochi/skills" },
1473
+ { id: "promptscript", displayName: "PromptScript", projectRoot: ".agents/skills" },
1474
+ { id: "adal", displayName: "AdaL", projectRoot: ".adal/skills", globalRoot: "~/.adal/skills" }
1475
+ ].map(makeAgent);
1476
+
1477
+ // src/commands/init.ts
1478
+ function detectAgents(cwd, home) {
1479
+ const found = [];
1480
+ for (const agent of AGENTS) {
1481
+ const hit = detectAgentRoot(agent, cwd, home);
1482
+ if (hit) found.push({ agent, ...hit });
1483
+ }
1484
+ return found;
1485
+ }
1486
+ function printDetectionResults(detected) {
1487
+ console.log(`[skill-forge] init \u2014 agent detection (${AGENTS.length} known agents)`);
1488
+ if (detected.length === 0) {
1489
+ console.log(" none detected");
1490
+ return;
1491
+ }
1492
+ for (const d of detected) {
1493
+ console.log(` ${d.agent.displayName} (${d.agent.id}) \u2014 ${d.scope}: ${d.root}`);
1494
+ }
1495
+ }
1496
+ function handoffTemplateFor(agent) {
1497
+ return agent.defaultHandoffTemplate ?? [agent.id, "-p", HANDOFF_INSTRUCTION];
1498
+ }
1499
+ function buildConfig(selected, defaultTargetEntry, handoffAgent) {
1500
+ const existing = loadConfig();
1501
+ return {
1502
+ ...existing,
1503
+ skillsRoots: selected.map((s) => s.root),
1504
+ agents: selected.map((s) => ({ id: s.agent.id, skillsRoot: s.root })),
1505
+ defaultTarget: defaultTargetEntry?.root ?? existing.defaultTarget,
1506
+ handoffCommand: handoffAgent ? handoffTemplateFor(handoffAgent) : existing.handoffCommand
1507
+ };
1508
+ }
1509
+ function printSummary(config) {
1510
+ console.log(`
1511
+ [skill-forge] wrote config to ${getConfigPath()}`);
1512
+ console.log(` gate targets: ${config.skillsRoots.join(", ")}`);
1513
+ if (config.defaultTarget) console.log(` default target: ${config.defaultTarget}`);
1514
+ if (config.handoffCommand?.length) console.log(` handoff command: ${config.handoffCommand.join(" ")}`);
1515
+ }
1516
+ function parseChoice(answer, max) {
1517
+ const n = Number(answer);
1518
+ return Number.isInteger(n) && n >= 1 && n <= max ? n - 1 : void 0;
1519
+ }
1520
+ async function promptMultiSelect(rl, detected) {
1521
+ const chosen = new Set(detected.map((_, i) => i));
1522
+ for (; ; ) {
1523
+ console.log("\nGate targets (all detected roots pre-selected \u2014 type a number to toggle, Enter to confirm):");
1524
+ detected.forEach((d, i) => {
1525
+ const mark = chosen.has(i) ? "[x]" : "[ ]";
1526
+ console.log(` ${mark} ${i + 1}. ${d.agent.displayName} (${d.scope}) \u2014 ${d.root}`);
1527
+ });
1528
+ const answer = (await rl.question("> ")).trim();
1529
+ if (answer === "") break;
1530
+ const idx = parseChoice(answer, detected.length);
1531
+ if (idx !== void 0) {
1532
+ if (chosen.has(idx)) chosen.delete(idx);
1533
+ else chosen.add(idx);
1534
+ } else {
1535
+ console.log(" (invalid selection \u2014 enter a listed number, or press Enter to confirm)");
1536
+ }
1537
+ }
1538
+ return detected.filter((_, i) => chosen.has(i));
1539
+ }
1540
+ async function promptDefaultTarget(rl, selected) {
1541
+ console.log("\nDefault promotion target:");
1542
+ selected.forEach((d, i) => console.log(` ${i + 1}. ${d.agent.displayName} \u2014 ${d.root}`));
1543
+ const answer = (await rl.question(`Choose [1-${selected.length}] (default 1): `)).trim();
1544
+ const idx = answer === "" ? 0 : parseChoice(answer, selected.length) ?? 0;
1545
+ return selected[idx];
1546
+ }
1547
+ async function promptHandoffAgent(rl, detected) {
1548
+ console.log("\nHandoff agent (used to hand off follow-up prompts after promotion; optional):");
1549
+ detected.forEach((d, i) => {
1550
+ const note = d.agent.defaultHandoffTemplate ? "" : " (no confirmed CLI \u2014 best-effort)";
1551
+ console.log(` ${i + 1}. ${d.agent.displayName}${note}`);
1552
+ });
1553
+ const answer = (await rl.question(`Choose [1-${detected.length}] or Enter to skip: `)).trim();
1554
+ if (answer === "") return void 0;
1555
+ const idx = parseChoice(answer, detected.length);
1556
+ return idx === void 0 ? void 0 : detected[idx].agent;
1557
+ }
1558
+ async function init(options = {}) {
1559
+ const cwd = options.cwd ?? process.cwd();
1560
+ const home = options.home ?? homedir4();
1561
+ const detected = detectAgents(cwd, home);
1562
+ if (options.list) {
1563
+ printDetectionResults(detected);
1564
+ return;
1565
+ }
1566
+ if (detected.length === 0) {
1567
+ console.log("[skill-forge] No known agent skill directories were found.");
1568
+ console.log(" Install a coding agent (Claude Code, Codex, Cursor, ...) or create a skills root, then re-run `skill-forge init`.");
1569
+ return;
1570
+ }
1571
+ if (options.defaults) {
1572
+ const config = buildConfig(detected, detected[0], void 0);
1573
+ saveConfig(config);
1574
+ printSummary(config);
1575
+ return;
1576
+ }
1577
+ if (!process.stdin.isTTY) {
1578
+ console.log("[skill-forge] Not an interactive terminal \u2014 run with --defaults for non-interactive setup.");
1579
+ return;
1580
+ }
1581
+ printDetectionResults(detected);
1582
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
1583
+ try {
1584
+ const selected = await promptMultiSelect(rl, detected);
1585
+ if (selected.length === 0) {
1586
+ console.log("[skill-forge] No targets selected \u2014 aborting, nothing written.");
1587
+ return;
1588
+ }
1589
+ const target = await promptDefaultTarget(rl, selected);
1590
+ const handoffAgent = await promptHandoffAgent(rl, detected);
1591
+ const config = buildConfig(selected, target, handoffAgent);
1592
+ saveConfig(config);
1593
+ printSummary(config);
1594
+ } finally {
1595
+ rl.close();
1596
+ }
1597
+ }
1598
+ async function maybeOfferInit(options = {}) {
1599
+ if (options.json || options.yes || !process.stdin.isTTY) return;
1600
+ if (existsSync10(getConfigPath())) return;
1601
+ console.log("[skill-forge] No config found \u2014 run `skill-forge init` to detect your agents and set gate targets.");
1602
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
1603
+ let answer = "";
1604
+ try {
1605
+ answer = (await rl.question("Run `skill-forge init` now? [y/N] ")).trim().toLowerCase();
1606
+ } finally {
1607
+ rl.close();
1608
+ }
1609
+ if (answer === "y" || answer === "yes") {
1610
+ await init();
1611
+ }
1612
+ }
1613
+
1149
1614
  // src/commands/add.ts
1615
+ var PRO_FEATURE_LABEL = "overlap analysis, the provenance ledger, and the pending-ingestion queue";
1616
+ function logOutcome(message, json) {
1617
+ if (json) console.error(message);
1618
+ else console.log(message);
1619
+ }
1150
1620
  async function add(source, options = {}) {
1621
+ await maybeOfferInit({ json: options.json, yes: options.yes });
1151
1622
  const config = loadConfig();
1152
- const targetRoot = options.target ?? config.skillsRoots[0];
1623
+ const targetRoot = options.target ?? config.defaultTarget ?? config.skillsRoots[0];
1153
1624
  if (!targetRoot) {
1154
1625
  throw new Error("no target skills root configured; pass --target <dir> or set skillsRoots in the config");
1155
1626
  }
@@ -1167,15 +1638,18 @@ async function add(source, options = {}) {
1167
1638
  ` + entry.skillDirs.map((d) => ` - ${d}`).join("\n")
1168
1639
  );
1169
1640
  }
1170
- const { safety, gateResult, pro } = await runGate(entry.skillDirs[0], targetRoot);
1641
+ const { safety, gateResult, proEnabled } = await runGate(entry.skillDirs[0], targetRoot, {
1642
+ proFeatureLabel: PRO_FEATURE_LABEL,
1643
+ strictness: config.strictness
1644
+ });
1171
1645
  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);
1646
+ printGateOutput(gateResult, options.json);
1647
+ const decision = options.yes || options.json ? yesDecision(safety.verdict) : await promptDecision(safety.verdict);
1174
1648
  let installedPath = null;
1175
1649
  if (decision === "promote") {
1176
1650
  const result = promote(entry, targetRoot);
1177
- console.log(`[skill-forge] promoted -> ${result.promoted.join(", ")}`);
1178
- if (pro) {
1651
+ logOutcome(`[skill-forge] promoted -> ${result.promoted.join(", ")}`, options.json);
1652
+ if (proEnabled) {
1179
1653
  const ledgerPath = appendProvenance(
1180
1654
  {
1181
1655
  name: profile.name ?? entry.id,
@@ -1189,19 +1663,19 @@ async function add(source, options = {}) {
1189
1663
  },
1190
1664
  targetRoot
1191
1665
  );
1192
- console.log(`[skill-forge] provenance recorded -> ${ledgerPath}`);
1666
+ logOutcome(`[skill-forge] provenance recorded -> ${ledgerPath}`, options.json);
1193
1667
  }
1194
1668
  installedPath = result.promoted[0] ?? null;
1195
1669
  } else if (decision === "reject") {
1196
1670
  reject(entry);
1197
- console.log("[skill-forge] rejected \u2014 quarantine sandbox removed");
1671
+ logOutcome("[skill-forge] rejected \u2014 quarantine sandbox removed", options.json);
1198
1672
  if (safety.verdict === "block") process.exitCode = 1;
1199
1673
  return;
1200
1674
  } else {
1201
1675
  hold(entry);
1202
- console.log(`[skill-forge] held in quarantine -> ${entry.quarantinePath}`);
1676
+ logOutcome(`[skill-forge] held in quarantine -> ${entry.quarantinePath}`, options.json);
1203
1677
  }
1204
- if (pro) {
1678
+ if (proEnabled) {
1205
1679
  appendQueueEntry({
1206
1680
  source,
1207
1681
  sourceType: resolved.type,
@@ -1217,28 +1691,15 @@ async function add(source, options = {}) {
1217
1691
  status: "pending"
1218
1692
  });
1219
1693
  if (options.ingest && installedPath) {
1220
- handoffToClaude(installedPath);
1694
+ runHandoff(config, installedPath);
1221
1695
  }
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
1696
  }
1236
1697
  }
1237
1698
  function yesDecision(verdict) {
1238
1699
  return verdict === "block" ? "reject" : "promote";
1239
1700
  }
1240
1701
  async function promptDecision(verdict) {
1241
- const rl = createInterface({ input: process.stdin, output: process.stdout });
1702
+ const rl = createInterface2({ input: process.stdin, output: process.stdout });
1242
1703
  const defaultChoice = verdict === "block" ? "reject" : verdict === "warn" ? "hold" : "promote";
1243
1704
  const prompt = `Promote, hold, or reject this skill? [promote/hold/reject] (default: ${defaultChoice}) `;
1244
1705
  try {
@@ -1258,9 +1719,10 @@ ${prompt}`);
1258
1719
  }
1259
1720
 
1260
1721
  // src/commands/scan.ts
1261
- async function scan(source) {
1722
+ async function scan(source, options = {}) {
1723
+ await maybeOfferInit({ json: options.json });
1262
1724
  const config = loadConfig();
1263
- const skillsRoot = config.skillsRoots[0];
1725
+ const skillsRoot = config.defaultTarget ?? config.skillsRoots[0];
1264
1726
  if (!skillsRoot) {
1265
1727
  throw new Error("no skills root configured; set skillsRoots in the config");
1266
1728
  }
@@ -1278,8 +1740,8 @@ async function scan(source) {
1278
1740
  ` + entry.skillDirs.map((d) => ` - ${d}`).join("\n")
1279
1741
  );
1280
1742
  }
1281
- const { safety, gateResult } = await runGate(entry.skillDirs[0], skillsRoot);
1282
- console.log(renderReport(gateResult));
1743
+ const { safety, gateResult } = await runGate(entry.skillDirs[0], skillsRoot, { strictness: config.strictness });
1744
+ printGateOutput(gateResult, options.json);
1283
1745
  if (safety.verdict === "block") {
1284
1746
  process.exitCode = 1;
1285
1747
  }
@@ -1289,8 +1751,9 @@ async function scan(source) {
1289
1751
  }
1290
1752
 
1291
1753
  // src/commands/list.ts
1292
- import { join as join10 } from "path";
1754
+ import { join as join12 } from "path";
1293
1755
  async function list() {
1756
+ await maybeOfferInit();
1294
1757
  const config = loadConfig();
1295
1758
  const { quarantineDir } = config;
1296
1759
  const ids = listQuarantineIds(quarantineDir);
@@ -1301,14 +1764,14 @@ async function list() {
1301
1764
  }
1302
1765
  console.log(`[skill-forge] ${ids.length} held in quarantine (${quarantineDir}):`);
1303
1766
  for (const id of ids) {
1304
- const skillDirs = findSkillDirs(join10(quarantineDir, id));
1767
+ const skillDirs = findSkillDirs(join12(quarantineDir, id));
1305
1768
  if (skillDirs.length === 0) {
1306
1769
  console.log(` ${id} (no SKILL.md found)`);
1307
1770
  continue;
1308
1771
  }
1309
1772
  const names = skillDirs.map((dir) => {
1310
1773
  try {
1311
- return readFrontmatter(join10(dir, "SKILL.md")).name;
1774
+ return readFrontmatter(join12(dir, "SKILL.md")).name;
1312
1775
  } catch {
1313
1776
  return dir;
1314
1777
  }
@@ -1319,6 +1782,7 @@ async function list() {
1319
1782
 
1320
1783
  // src/commands/status.ts
1321
1784
  async function status() {
1785
+ await maybeOfferInit();
1322
1786
  const config = loadConfig();
1323
1787
  const heldCount = listQuarantineIds(config.quarantineDir).length;
1324
1788
  console.log("[skill-forge] status");
@@ -1333,9 +1797,25 @@ var require2 = createRequire(import.meta.url);
1333
1797
  var pkg = require2("../package.json");
1334
1798
  var program = new Command();
1335
1799
  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);
1800
+ 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 defaultTarget, then 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);
1801
+ 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
1802
  program.command("list").description("List skills currently in quarantine").action(list);
1339
1803
  program.command("status").description("Show skill-forge configuration and quarantine status").action(status);
1340
- await program.parseAsync();
1804
+ 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);
1805
+ try {
1806
+ await program.parseAsync();
1807
+ } catch (err) {
1808
+ if (err instanceof UserError) {
1809
+ console.error(`skill-forge: ${err.message}`);
1810
+ process.exit(1);
1811
+ }
1812
+ if (process.env.DEBUG === "skill-forge") {
1813
+ console.error(err);
1814
+ } else {
1815
+ const message = err instanceof Error ? err.message : String(err);
1816
+ console.error(`skill-forge: unexpected error: ${message}`);
1817
+ console.error("Re-run with DEBUG=skill-forge to see the full stack trace.");
1818
+ }
1819
+ process.exit(1);
1820
+ }
1341
1821
  //# sourceMappingURL=cli.js.map