@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/README.md +89 -36
- package/dist/cli.js +673 -193
- package/dist/cli.js.map +1 -1
- package/dist/ingest-prompt.md +211 -0
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -5,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:
|
|
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
|
-
|
|
54
|
-
|
|
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
|
|
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 =
|
|
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 =
|
|
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
|
|
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,
|
|
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(
|
|
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
|
|
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 =
|
|
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 =
|
|
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
|
|
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 = "#
|
|
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) || !
|
|
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 =
|
|
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
|
|
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
|
-
|
|
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
|
|
1092
|
-
const overlap =
|
|
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: !
|
|
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,
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
1178
|
-
if (
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1676
|
+
logOutcome(`[skill-forge] held in quarantine -> ${entry.quarantinePath}`, options.json);
|
|
1203
1677
|
}
|
|
1204
|
-
if (
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
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
|
|
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(
|
|
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(
|
|
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
|
|
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
|
-
|
|
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
|