guardskills 1.1.0 → 1.2.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.
Files changed (4) hide show
  1. package/README.md +24 -5
  2. package/dist/cli.cjs +524 -109
  3. package/dist/cli.js +524 -109
  4. package/package.json +2 -1
package/dist/cli.js CHANGED
@@ -176,9 +176,13 @@ function enforceSourcePolicy(repoInput, policy) {
176
176
 
177
177
  // src/install/skills.ts
178
178
  import { execa } from "execa";
179
- async function runSkillsInstall(repo, skill) {
179
+ async function runProviderInstall(provider, repo, skill) {
180
+ if (provider === "playbooks" && !skill) {
181
+ return 30;
182
+ }
183
+ const args = provider === "skills" ? skill ? ["skills", "add", repo, "--skill", skill] : ["skills", "add", repo] : provider === "playbooks" ? ["playbooks", "add", "skill", repo, "--skill", skill] : provider === "skillkit" ? skill ? ["skillkit", "install", repo, "--skill", skill] : ["skillkit", "install", repo] : skill ? ["openskills", "install", repo, skill] : ["openskills", "install", repo];
180
184
  try {
181
- await execa("npx", ["skills", "add", repo, "--skill", skill], {
185
+ await execa("npx", args, {
182
186
  stdio: "inherit"
183
187
  });
184
188
  return 0;
@@ -1043,6 +1047,22 @@ var cliAddOptionsSchema = z2.object({
1043
1047
  maxAuxFiles: z2.coerce.number().int().min(1).max(200).optional(),
1044
1048
  maxTotalFiles: z2.coerce.number().int().min(1).max(400).optional()
1045
1049
  });
1050
+ var cliBulkAddOptionsSchema = z2.object({
1051
+ config: z2.string().optional(),
1052
+ strict: z2.boolean().optional(),
1053
+ ci: z2.boolean().optional(),
1054
+ json: z2.boolean().optional(),
1055
+ yes: z2.boolean().optional(),
1056
+ dryRun: z2.boolean().optional(),
1057
+ force: z2.boolean().optional(),
1058
+ allowUnverifiable: z2.boolean().optional(),
1059
+ githubTimeoutMs: z2.coerce.number().int().min(1e3).max(12e4).optional(),
1060
+ githubRetries: z2.coerce.number().int().min(0).max(6).optional(),
1061
+ githubRetryBaseMs: z2.coerce.number().int().min(50).max(5e3).optional(),
1062
+ maxFileBytes: z2.coerce.number().int().min(4096).max(5e6).optional(),
1063
+ maxAuxFiles: z2.coerce.number().int().min(1).max(200).optional(),
1064
+ maxTotalFiles: z2.coerce.number().int().min(1).max(400).optional()
1065
+ });
1046
1066
  var effectiveAddOptionsSchema = z2.object({
1047
1067
  skill: z2.string().min(1),
1048
1068
  strict: z2.boolean(),
@@ -1162,9 +1182,11 @@ function evaluateGate(level, options) {
1162
1182
  gateNote: level === "WARNING" ? "WARNING accepted via --yes." : "SAFE to proceed."
1163
1183
  };
1164
1184
  }
1165
- async function runAddCommand(repo, rawOptions) {
1185
+ async function runAddCommand(repo, rawOptions, context = {}) {
1166
1186
  const cliOptions = cliAddOptionsSchema.parse(rawOptions);
1167
1187
  const loadedConfig = loadGuardSkillsConfig(cliOptions.config);
1188
+ const provider = context.provider ?? "skills";
1189
+ const commandName = context.commandName ?? "guardskills add";
1168
1190
  const options = resolveEffectiveAddOptions(cliOptions, loadedConfig.config);
1169
1191
  enforceSourcePolicy(repo, loadedConfig.config.policy);
1170
1192
  enforceOptionPolicy(options, loadedConfig.config);
@@ -1185,7 +1207,7 @@ async function runAddCommand(repo, rawOptions) {
1185
1207
  const gate = evaluateGate(decision.level, options);
1186
1208
  const configNote = loadedConfig.path ? ` Config: ${loadedConfig.path}` : "";
1187
1209
  const report = {
1188
- command: "guardskills add",
1210
+ command: commandName,
1189
1211
  repo,
1190
1212
  skill: options.skill,
1191
1213
  strict: options.strict,
@@ -1210,14 +1232,353 @@ async function runAddCommand(repo, rawOptions) {
1210
1232
  if (options.dryRun || options.ci) {
1211
1233
  return 0;
1212
1234
  }
1213
- return runSkillsInstall(repo, options.skill);
1235
+ return runProviderInstall(provider, repo, options.skill);
1214
1236
  }
1215
1237
 
1216
- // src/commands/scan-clawhub.ts
1238
+ // src/commands/openskills-install.ts
1239
+ import fs2 from "fs";
1240
+ import os from "os";
1241
+ import path4 from "path";
1242
+ import { execa as execa2 } from "execa";
1217
1243
  import { z as z3 } from "zod";
1244
+ var ALLOWED_TEXT_EXTENSIONS2 = /* @__PURE__ */ new Set([
1245
+ ".md",
1246
+ ".txt",
1247
+ ".sh",
1248
+ ".bash",
1249
+ ".zsh",
1250
+ ".ps1",
1251
+ ".py",
1252
+ ".js",
1253
+ ".ts",
1254
+ ".mjs",
1255
+ ".cjs",
1256
+ ".json",
1257
+ ".yaml",
1258
+ ".yml",
1259
+ ".toml",
1260
+ ".ini",
1261
+ ".cfg"
1262
+ ]);
1263
+ var SKIP_DIRS = /* @__PURE__ */ new Set([".git", "node_modules", "dist", "build", ".next", ".turbo"]);
1264
+ var cliOpenSkillsOptionsSchema = z3.object({
1265
+ config: z3.string().optional(),
1266
+ strict: z3.boolean().optional(),
1267
+ ci: z3.boolean().optional(),
1268
+ json: z3.boolean().optional(),
1269
+ yes: z3.boolean().optional(),
1270
+ dryRun: z3.boolean().optional(),
1271
+ force: z3.boolean().optional(),
1272
+ allowUnverifiable: z3.boolean().optional(),
1273
+ githubTimeoutMs: z3.coerce.number().int().min(1e3).max(12e4).optional(),
1274
+ githubRetries: z3.coerce.number().int().min(0).max(6).optional(),
1275
+ githubRetryBaseMs: z3.coerce.number().int().min(50).max(5e3).optional(),
1276
+ maxFileBytes: z3.coerce.number().int().min(4096).max(5e6).optional(),
1277
+ maxAuxFiles: z3.coerce.number().int().min(1).max(200).optional(),
1278
+ maxTotalFiles: z3.coerce.number().int().min(1).max(400).optional()
1279
+ });
1280
+ var DEFAULT_OPTIONS2 = {
1281
+ strict: false,
1282
+ ci: false,
1283
+ json: false,
1284
+ yes: false,
1285
+ dryRun: false,
1286
+ force: false,
1287
+ allowUnverifiable: false,
1288
+ githubTimeoutMs: 15e3,
1289
+ githubRetries: 2,
1290
+ githubRetryBaseMs: 300,
1291
+ maxFileBytes: 25e4,
1292
+ maxAuxFiles: 40,
1293
+ maxTotalFiles: 120
1294
+ };
1295
+ function resolveEffectiveOptions(cliOptions, config) {
1296
+ const defaults = config.defaults ?? {};
1297
+ const resolver = config.resolver ?? {};
1298
+ return {
1299
+ strict: cliOptions.strict ?? defaults.strict ?? DEFAULT_OPTIONS2.strict,
1300
+ ci: cliOptions.ci ?? defaults.ci ?? DEFAULT_OPTIONS2.ci,
1301
+ json: cliOptions.json ?? defaults.json ?? DEFAULT_OPTIONS2.json,
1302
+ yes: cliOptions.yes ?? defaults.yes ?? DEFAULT_OPTIONS2.yes,
1303
+ dryRun: cliOptions.dryRun ?? defaults.dryRun ?? DEFAULT_OPTIONS2.dryRun,
1304
+ force: cliOptions.force ?? defaults.force ?? DEFAULT_OPTIONS2.force,
1305
+ allowUnverifiable: cliOptions.allowUnverifiable ?? defaults.allowUnverifiable ?? DEFAULT_OPTIONS2.allowUnverifiable,
1306
+ githubTimeoutMs: cliOptions.githubTimeoutMs ?? resolver.githubTimeoutMs ?? DEFAULT_OPTIONS2.githubTimeoutMs,
1307
+ githubRetries: cliOptions.githubRetries ?? resolver.githubRetries ?? DEFAULT_OPTIONS2.githubRetries,
1308
+ githubRetryBaseMs: cliOptions.githubRetryBaseMs ?? resolver.githubRetryBaseMs ?? DEFAULT_OPTIONS2.githubRetryBaseMs,
1309
+ maxFileBytes: cliOptions.maxFileBytes ?? resolver.maxFileBytes ?? DEFAULT_OPTIONS2.maxFileBytes,
1310
+ maxAuxFiles: cliOptions.maxAuxFiles ?? resolver.maxAuxFiles ?? DEFAULT_OPTIONS2.maxAuxFiles,
1311
+ maxTotalFiles: cliOptions.maxTotalFiles ?? resolver.maxTotalFiles ?? DEFAULT_OPTIONS2.maxTotalFiles
1312
+ };
1313
+ }
1314
+ function enforceOptionPolicy2(options, config) {
1315
+ const policy = config.policy;
1316
+ if (!policy) {
1317
+ return;
1318
+ }
1319
+ if (options.force && policy.allowForce === false) {
1320
+ throw new GuardSkillsError(
1321
+ "POLICY_VIOLATION",
1322
+ "Policy blocks --force overrides (allowForce=false)."
1323
+ );
1324
+ }
1325
+ if (options.allowUnverifiable && policy.allowUnverifiableOverride === false) {
1326
+ throw new GuardSkillsError(
1327
+ "POLICY_VIOLATION",
1328
+ "Policy blocks --allow-unverifiable overrides (allowUnverifiableOverride=false)."
1329
+ );
1330
+ }
1331
+ }
1332
+ function findSkillDirs(rootDir) {
1333
+ const found = /* @__PURE__ */ new Set();
1334
+ const stack = [{ dir: rootDir, depth: 0 }];
1335
+ while (stack.length > 0) {
1336
+ const current = stack.pop();
1337
+ if (!current) {
1338
+ continue;
1339
+ }
1340
+ const skillFile = path4.join(current.dir, "SKILL.md");
1341
+ if (fs2.existsSync(skillFile) && fs2.statSync(skillFile).isFile()) {
1342
+ found.add(current.dir);
1343
+ continue;
1344
+ }
1345
+ if (current.depth >= 8) {
1346
+ continue;
1347
+ }
1348
+ let entries;
1349
+ try {
1350
+ entries = fs2.readdirSync(current.dir, { withFileTypes: true });
1351
+ } catch {
1352
+ continue;
1353
+ }
1354
+ for (const entry of entries) {
1355
+ if (!entry.isDirectory()) {
1356
+ continue;
1357
+ }
1358
+ if (SKIP_DIRS.has(entry.name)) {
1359
+ continue;
1360
+ }
1361
+ stack.push({ dir: path4.join(current.dir, entry.name), depth: current.depth + 1 });
1362
+ }
1363
+ }
1364
+ return [...found].sort();
1365
+ }
1366
+ function collectLocalFiles(skillDir, options) {
1367
+ const files = [];
1368
+ const unverifiableReasons = [];
1369
+ const stack = [skillDir];
1370
+ while (stack.length > 0) {
1371
+ const currentDir = stack.pop();
1372
+ if (!currentDir) {
1373
+ continue;
1374
+ }
1375
+ let entries;
1376
+ try {
1377
+ entries = fs2.readdirSync(currentDir, { withFileTypes: true });
1378
+ } catch {
1379
+ unverifiableReasons.push(`Cannot read directory: ${currentDir}`);
1380
+ continue;
1381
+ }
1382
+ for (const entry of entries) {
1383
+ const fullPath = path4.join(currentDir, entry.name);
1384
+ if (entry.isDirectory()) {
1385
+ if (!SKIP_DIRS.has(entry.name)) {
1386
+ stack.push(fullPath);
1387
+ }
1388
+ continue;
1389
+ }
1390
+ if (!entry.isFile()) {
1391
+ continue;
1392
+ }
1393
+ const relativePath = path4.relative(skillDir, fullPath).replace(/\\/g, "/");
1394
+ const ext = path4.extname(relativePath).toLowerCase();
1395
+ const isSkillFile2 = path4.basename(relativePath).toLowerCase() === "skill.md";
1396
+ if (!isSkillFile2 && !ALLOWED_TEXT_EXTENSIONS2.has(ext)) {
1397
+ continue;
1398
+ }
1399
+ if (files.length >= options.maxTotalFiles) {
1400
+ unverifiableReasons.push(
1401
+ `Reached maxTotalFiles=${options.maxTotalFiles}. Remaining files were not scanned.`
1402
+ );
1403
+ return { files, unverifiableReasons };
1404
+ }
1405
+ let sizeBytes = 0;
1406
+ try {
1407
+ sizeBytes = fs2.statSync(fullPath).size;
1408
+ } catch {
1409
+ unverifiableReasons.push(`Cannot stat file: ${relativePath}`);
1410
+ continue;
1411
+ }
1412
+ if (sizeBytes > options.maxFileBytes) {
1413
+ unverifiableReasons.push(
1414
+ `Skipped oversized file (${sizeBytes} bytes > ${options.maxFileBytes}): ${relativePath}`
1415
+ );
1416
+ continue;
1417
+ }
1418
+ try {
1419
+ files.push({
1420
+ path: relativePath,
1421
+ content: fs2.readFileSync(fullPath, "utf8")
1422
+ });
1423
+ } catch {
1424
+ unverifiableReasons.push(`Cannot read text content: ${relativePath}`);
1425
+ }
1426
+ }
1427
+ }
1428
+ return { files, unverifiableReasons };
1429
+ }
1430
+ function resolveSource(source) {
1431
+ const maybePath = path4.resolve(source);
1432
+ if (fs2.existsSync(maybePath)) {
1433
+ return { kind: "local", path: maybePath };
1434
+ }
1435
+ const shorthand = source.match(/^([A-Za-z0-9_.-]+)\/([A-Za-z0-9_.-]+)$/);
1436
+ if (shorthand) {
1437
+ return { kind: "git", cloneUrl: `https://github.com/${shorthand[1]}/${shorthand[2]}.git` };
1438
+ }
1439
+ try {
1440
+ const parsed = new URL(source);
1441
+ if (parsed.hostname === "github.com" || parsed.hostname === "www.github.com") {
1442
+ return { kind: "git", cloneUrl: source.endsWith(".git") ? source : `${source}.git` };
1443
+ }
1444
+ } catch {
1445
+ }
1446
+ return { kind: "git", cloneUrl: source };
1447
+ }
1448
+ function aggregateLevel(levels) {
1449
+ if (levels.includes("UNVERIFIABLE")) {
1450
+ return "UNVERIFIABLE";
1451
+ }
1452
+ if (levels.includes("CRITICAL")) {
1453
+ return "CRITICAL";
1454
+ }
1455
+ if (levels.includes("UNSAFE")) {
1456
+ return "UNSAFE";
1457
+ }
1458
+ if (levels.includes("WARNING")) {
1459
+ return "WARNING";
1460
+ }
1461
+ return "SAFE";
1462
+ }
1463
+ async function runInteractiveInstallCommand(provider, source, skillName, rawOptions) {
1464
+ if (provider !== "openskills" && provider !== "skills" && provider !== "skillkit") {
1465
+ throw new GuardSkillsError(
1466
+ "INVALID_OPTIONS",
1467
+ `Interactive install flow is not supported for provider '${provider}'.`
1468
+ );
1469
+ }
1470
+ const cliOptions = cliOpenSkillsOptionsSchema.parse(rawOptions);
1471
+ const loadedConfig = loadGuardSkillsConfig(cliOptions.config);
1472
+ const options = resolveEffectiveOptions(cliOptions, loadedConfig.config);
1473
+ enforceSourcePolicy(source, loadedConfig.config.policy);
1474
+ enforceOptionPolicy2(options, loadedConfig.config);
1475
+ const resolvedSource = resolveSource(source);
1476
+ const tempDir = resolvedSource.kind === "git" ? fs2.mkdtempSync(path4.join(os.tmpdir(), `guardskills-${provider}-`)) : null;
1477
+ const scanRoot = resolvedSource.kind === "git" ? tempDir ?? "" : resolvedSource.path;
1478
+ try {
1479
+ if (resolvedSource.kind === "git") {
1480
+ try {
1481
+ await execa2("git", ["clone", "--depth", "1", resolvedSource.cloneUrl, scanRoot], {
1482
+ timeout: options.githubTimeoutMs
1483
+ });
1484
+ } catch (error) {
1485
+ const message = error instanceof Error ? error.message : String(error);
1486
+ throw new GuardSkillsError(
1487
+ "GITHUB_UNKNOWN",
1488
+ `Failed to clone source '${source}' for ${provider} scan: ${message}`,
1489
+ { cause: error }
1490
+ );
1491
+ }
1492
+ }
1493
+ const discovered = findSkillDirs(scanRoot);
1494
+ if (discovered.length === 0) {
1495
+ throw new GuardSkillsError("SKILL_NOT_FOUND", "No SKILL.md files were found in the source.");
1496
+ }
1497
+ const selectedDirs = skillName ? discovered.filter((dir) => path4.basename(dir).toLowerCase() === skillName.toLowerCase()) : discovered;
1498
+ if (selectedDirs.length === 0) {
1499
+ throw new GuardSkillsError(
1500
+ "SKILL_NOT_FOUND",
1501
+ `Skill '${skillName}' was not found. Available: ${discovered.map((dir) => path4.basename(dir)).join(", ")}`
1502
+ );
1503
+ }
1504
+ const levels = [];
1505
+ const summaries = [];
1506
+ for (const skillDir of selectedDirs) {
1507
+ const skill = path4.basename(skillDir);
1508
+ const { files, unverifiableReasons } = collectLocalFiles(skillDir, options);
1509
+ const resolvedSkill = {
1510
+ source: `local:${skillDir}`,
1511
+ owner: "local",
1512
+ repo: "local",
1513
+ defaultBranch: "local",
1514
+ commitSha: "local",
1515
+ skillName: skill,
1516
+ skillDir: skillDir.replace(/\\/g, "/"),
1517
+ skillFilePath: "SKILL.md",
1518
+ files,
1519
+ unverifiableReasons
1520
+ };
1521
+ const scan = scanResolvedSkill(resolvedSkill);
1522
+ const decision = calculateRiskScore(scan.findings, {
1523
+ strict: options.strict,
1524
+ trustCredits: 0,
1525
+ hasUnverifiableContent: scan.hasUnverifiableContent
1526
+ });
1527
+ levels.push(decision.level);
1528
+ summaries.push({ skill, level: decision.level, riskScore: decision.riskScore });
1529
+ }
1530
+ const overall = aggregateLevel(levels);
1531
+ const gate = evaluateGate(overall, { ...options, skill: skillName ?? "ALL_SKILLS" });
1532
+ const label = provider === "openskills" ? "OpenSkills" : provider === "skillkit" ? "skillkit" : "skills.sh";
1533
+ const note = `${gate.gateNote} ${label} flow: ${skillName ? "single skill" : "interactive skill selection"}.`;
1534
+ if (options.json) {
1535
+ console.log(
1536
+ JSON.stringify(
1537
+ {
1538
+ command: `guardskills ${provider} ${provider === "skills" ? "add" : "install"}`,
1539
+ source,
1540
+ skill: skillName ?? null,
1541
+ scannedSkills: summaries.length,
1542
+ overallLevel: overall,
1543
+ skills: summaries,
1544
+ note
1545
+ },
1546
+ null,
1547
+ 2
1548
+ )
1549
+ );
1550
+ } else {
1551
+ console.log(`Command: guardskills ${provider} ${provider === "skills" ? "add" : "install"}`);
1552
+ console.log(`Source: ${source}`);
1553
+ console.log(`Selection: ${skillName ?? "interactive (all scanned)"}`);
1554
+ console.log(`Scanned Skills: ${summaries.length}`);
1555
+ console.log(`Decision: ${overall}`);
1556
+ console.log("Per-skill results:");
1557
+ for (const summary of summaries) {
1558
+ const score = summary.riskScore === null ? "n/a" : summary.riskScore.toFixed(1);
1559
+ console.log(`- ${summary.skill}: ${summary.level} (risk ${score})`);
1560
+ }
1561
+ console.log(`Note: ${note}`);
1562
+ }
1563
+ if (!gate.canInstall) {
1564
+ return gate.exitCode;
1565
+ }
1566
+ if (options.dryRun || options.ci) {
1567
+ return 0;
1568
+ }
1569
+ return runProviderInstall(provider, source, skillName);
1570
+ } finally {
1571
+ if (tempDir) {
1572
+ fs2.rmSync(tempDir, { recursive: true, force: true });
1573
+ }
1574
+ }
1575
+ }
1576
+
1577
+ // src/commands/scan-clawhub.ts
1578
+ import { z as z4 } from "zod";
1218
1579
 
1219
1580
  // src/resolver/clawhub.ts
1220
- import path4 from "path";
1581
+ import path5 from "path";
1221
1582
  import JSZip from "jszip";
1222
1583
  var DEFAULT_REGISTRY_BASE_URL = "https://clawhub.ai";
1223
1584
  var DEFAULT_ARCHIVE_BASE_URL = "https://auth.clawdhub.com";
@@ -1225,7 +1586,7 @@ var DEFAULT_REQUEST_TIMEOUT_MS2 = 15e3;
1225
1586
  var DEFAULT_MAX_FILE_SIZE_BYTES = 25e4;
1226
1587
  var DEFAULT_MAX_TOTAL_FILES = 120;
1227
1588
  var RETRYABLE_STATUS2 = /* @__PURE__ */ new Set([429, 500, 502, 503, 504]);
1228
- var ALLOWED_TEXT_EXTENSIONS2 = /* @__PURE__ */ new Set([
1589
+ var ALLOWED_TEXT_EXTENSIONS3 = /* @__PURE__ */ new Set([
1229
1590
  ".md",
1230
1591
  ".txt",
1231
1592
  ".sh",
@@ -1467,8 +1828,8 @@ function isLikelyTextFile2(filePath) {
1467
1828
  if (lower.endsWith("/skill.md") || lower === "skill.md") {
1468
1829
  return true;
1469
1830
  }
1470
- const ext = path4.posix.extname(lower);
1471
- return ALLOWED_TEXT_EXTENSIONS2.has(ext);
1831
+ const ext = path5.posix.extname(lower);
1832
+ return ALLOWED_TEXT_EXTENSIONS3.has(ext);
1472
1833
  }
1473
1834
  function isBinaryContent2(content) {
1474
1835
  return content.includes("\0");
@@ -1611,7 +1972,7 @@ async function resolveSkillFromArchive(metadata, identifier, options) {
1611
1972
  defaultBranch: "archive",
1612
1973
  commitSha: archiveMeta.version ?? "archive",
1613
1974
  skillName: options.skillNameOverride ?? archiveMeta.slug,
1614
- skillDir: path4.posix.dirname(skillFilePath),
1975
+ skillDir: path5.posix.dirname(skillFilePath),
1615
1976
  skillFilePath,
1616
1977
  files,
1617
1978
  unverifiableReasons: [...unverifiableReasons],
@@ -1759,34 +2120,34 @@ async function resolveSkillFromClawHub(identifier, options = {}) {
1759
2120
  }
1760
2121
 
1761
2122
  // src/commands/scan-clawhub.ts
1762
- var cliScanClawHubOptionsSchema = z3.object({
1763
- config: z3.string().optional(),
1764
- strict: z3.boolean().optional(),
1765
- json: z3.boolean().optional(),
1766
- skill: z3.string().min(1).optional(),
1767
- version: z3.string().min(1).optional(),
1768
- clawhubRegistry: z3.string().min(1).optional(),
1769
- githubTimeoutMs: z3.coerce.number().int().min(1e3).max(12e4).optional(),
1770
- githubRetries: z3.coerce.number().int().min(0).max(6).optional(),
1771
- githubRetryBaseMs: z3.coerce.number().int().min(50).max(5e3).optional(),
1772
- maxFileBytes: z3.coerce.number().int().min(4096).max(5e6).optional(),
1773
- maxAuxFiles: z3.coerce.number().int().min(1).max(200).optional(),
1774
- maxTotalFiles: z3.coerce.number().int().min(1).max(400).optional()
2123
+ var cliScanClawHubOptionsSchema = z4.object({
2124
+ config: z4.string().optional(),
2125
+ strict: z4.boolean().optional(),
2126
+ json: z4.boolean().optional(),
2127
+ skill: z4.string().min(1).optional(),
2128
+ version: z4.string().min(1).optional(),
2129
+ clawhubRegistry: z4.string().min(1).optional(),
2130
+ githubTimeoutMs: z4.coerce.number().int().min(1e3).max(12e4).optional(),
2131
+ githubRetries: z4.coerce.number().int().min(0).max(6).optional(),
2132
+ githubRetryBaseMs: z4.coerce.number().int().min(50).max(5e3).optional(),
2133
+ maxFileBytes: z4.coerce.number().int().min(4096).max(5e6).optional(),
2134
+ maxAuxFiles: z4.coerce.number().int().min(1).max(200).optional(),
2135
+ maxTotalFiles: z4.coerce.number().int().min(1).max(400).optional()
1775
2136
  });
1776
- var effectiveScanClawHubOptionsSchema = z3.object({
1777
- strict: z3.boolean(),
1778
- json: z3.boolean(),
1779
- skill: z3.string().min(1).optional(),
1780
- version: z3.string().min(1).optional(),
1781
- clawhubRegistry: z3.string().min(1),
1782
- githubTimeoutMs: z3.number().int().min(1e3).max(12e4),
1783
- githubRetries: z3.number().int().min(0).max(6),
1784
- githubRetryBaseMs: z3.number().int().min(50).max(5e3),
1785
- maxFileBytes: z3.number().int().min(4096).max(5e6),
1786
- maxAuxFiles: z3.number().int().min(1).max(200),
1787
- maxTotalFiles: z3.number().int().min(1).max(400)
2137
+ var effectiveScanClawHubOptionsSchema = z4.object({
2138
+ strict: z4.boolean(),
2139
+ json: z4.boolean(),
2140
+ skill: z4.string().min(1).optional(),
2141
+ version: z4.string().min(1).optional(),
2142
+ clawhubRegistry: z4.string().min(1),
2143
+ githubTimeoutMs: z4.number().int().min(1e3).max(12e4),
2144
+ githubRetries: z4.number().int().min(0).max(6),
2145
+ githubRetryBaseMs: z4.number().int().min(50).max(5e3),
2146
+ maxFileBytes: z4.number().int().min(4096).max(5e6),
2147
+ maxAuxFiles: z4.number().int().min(1).max(200),
2148
+ maxTotalFiles: z4.number().int().min(1).max(400)
1788
2149
  });
1789
- var DEFAULT_OPTIONS2 = {
2150
+ var DEFAULT_OPTIONS3 = {
1790
2151
  strict: false,
1791
2152
  json: false,
1792
2153
  skill: void 0,
@@ -1834,17 +2195,17 @@ function resolveEffectiveScanClawHubOptions(cliOptions, config) {
1834
2195
  const defaults = config.defaults ?? {};
1835
2196
  const resolver = config.resolver ?? {};
1836
2197
  return effectiveScanClawHubOptionsSchema.parse({
1837
- strict: cliOptions.strict ?? defaults.strict ?? DEFAULT_OPTIONS2.strict,
1838
- json: cliOptions.json ?? defaults.json ?? DEFAULT_OPTIONS2.json,
1839
- skill: cliOptions.skill ?? DEFAULT_OPTIONS2.skill,
1840
- version: cliOptions.version ?? DEFAULT_OPTIONS2.version,
1841
- clawhubRegistry: cliOptions.clawhubRegistry ?? DEFAULT_OPTIONS2.clawhubRegistry,
1842
- githubTimeoutMs: cliOptions.githubTimeoutMs ?? resolver.githubTimeoutMs ?? DEFAULT_OPTIONS2.githubTimeoutMs,
1843
- githubRetries: cliOptions.githubRetries ?? resolver.githubRetries ?? DEFAULT_OPTIONS2.githubRetries,
1844
- githubRetryBaseMs: cliOptions.githubRetryBaseMs ?? resolver.githubRetryBaseMs ?? DEFAULT_OPTIONS2.githubRetryBaseMs,
1845
- maxFileBytes: cliOptions.maxFileBytes ?? resolver.maxFileBytes ?? DEFAULT_OPTIONS2.maxFileBytes,
1846
- maxAuxFiles: cliOptions.maxAuxFiles ?? resolver.maxAuxFiles ?? DEFAULT_OPTIONS2.maxAuxFiles,
1847
- maxTotalFiles: cliOptions.maxTotalFiles ?? resolver.maxTotalFiles ?? DEFAULT_OPTIONS2.maxTotalFiles
2198
+ strict: cliOptions.strict ?? defaults.strict ?? DEFAULT_OPTIONS3.strict,
2199
+ json: cliOptions.json ?? defaults.json ?? DEFAULT_OPTIONS3.json,
2200
+ skill: cliOptions.skill ?? DEFAULT_OPTIONS3.skill,
2201
+ version: cliOptions.version ?? DEFAULT_OPTIONS3.version,
2202
+ clawhubRegistry: cliOptions.clawhubRegistry ?? DEFAULT_OPTIONS3.clawhubRegistry,
2203
+ githubTimeoutMs: cliOptions.githubTimeoutMs ?? resolver.githubTimeoutMs ?? DEFAULT_OPTIONS3.githubTimeoutMs,
2204
+ githubRetries: cliOptions.githubRetries ?? resolver.githubRetries ?? DEFAULT_OPTIONS3.githubRetries,
2205
+ githubRetryBaseMs: cliOptions.githubRetryBaseMs ?? resolver.githubRetryBaseMs ?? DEFAULT_OPTIONS3.githubRetryBaseMs,
2206
+ maxFileBytes: cliOptions.maxFileBytes ?? resolver.maxFileBytes ?? DEFAULT_OPTIONS3.maxFileBytes,
2207
+ maxAuxFiles: cliOptions.maxAuxFiles ?? resolver.maxAuxFiles ?? DEFAULT_OPTIONS3.maxAuxFiles,
2208
+ maxTotalFiles: cliOptions.maxTotalFiles ?? resolver.maxTotalFiles ?? DEFAULT_OPTIONS3.maxTotalFiles
1848
2209
  });
1849
2210
  }
1850
2211
  async function runScanClawHubCommand(identifier, rawOptions) {
@@ -1906,10 +2267,10 @@ async function runScanClawHubCommand(identifier, rawOptions) {
1906
2267
  }
1907
2268
 
1908
2269
  // src/commands/scan-local.ts
1909
- import fs2 from "fs";
1910
- import path5 from "path";
1911
- import { z as z4 } from "zod";
1912
- var ALLOWED_TEXT_EXTENSIONS3 = /* @__PURE__ */ new Set([
2270
+ import fs3 from "fs";
2271
+ import path6 from "path";
2272
+ import { z as z5 } from "zod";
2273
+ var ALLOWED_TEXT_EXTENSIONS4 = /* @__PURE__ */ new Set([
1913
2274
  ".md",
1914
2275
  ".txt",
1915
2276
  ".sh",
@@ -1928,23 +2289,23 @@ var ALLOWED_TEXT_EXTENSIONS3 = /* @__PURE__ */ new Set([
1928
2289
  ".ini",
1929
2290
  ".cfg"
1930
2291
  ]);
1931
- var SKIP_DIRS = /* @__PURE__ */ new Set([".git", "node_modules", "dist", "build", ".next", ".turbo"]);
1932
- var cliScanLocalOptionsSchema = z4.object({
1933
- config: z4.string().optional(),
1934
- strict: z4.boolean().optional(),
1935
- json: z4.boolean().optional(),
1936
- skill: z4.string().min(1).optional(),
1937
- maxFileBytes: z4.coerce.number().int().min(4096).max(5e6).optional(),
1938
- maxTotalFiles: z4.coerce.number().int().min(1).max(400).optional()
2292
+ var SKIP_DIRS2 = /* @__PURE__ */ new Set([".git", "node_modules", "dist", "build", ".next", ".turbo"]);
2293
+ var cliScanLocalOptionsSchema = z5.object({
2294
+ config: z5.string().optional(),
2295
+ strict: z5.boolean().optional(),
2296
+ json: z5.boolean().optional(),
2297
+ skill: z5.string().min(1).optional(),
2298
+ maxFileBytes: z5.coerce.number().int().min(4096).max(5e6).optional(),
2299
+ maxTotalFiles: z5.coerce.number().int().min(1).max(400).optional()
1939
2300
  });
1940
- var effectiveScanLocalOptionsSchema = z4.object({
1941
- strict: z4.boolean(),
1942
- json: z4.boolean(),
1943
- skill: z4.string().min(1).optional(),
1944
- maxFileBytes: z4.number().int().min(4096).max(5e6),
1945
- maxTotalFiles: z4.number().int().min(1).max(400)
2301
+ var effectiveScanLocalOptionsSchema = z5.object({
2302
+ strict: z5.boolean(),
2303
+ json: z5.boolean(),
2304
+ skill: z5.string().min(1).optional(),
2305
+ maxFileBytes: z5.number().int().min(4096).max(5e6),
2306
+ maxTotalFiles: z5.number().int().min(1).max(400)
1946
2307
  });
1947
- var DEFAULT_OPTIONS3 = {
2308
+ var DEFAULT_OPTIONS4 = {
1948
2309
  strict: false,
1949
2310
  json: false,
1950
2311
  skill: void 0,
@@ -1955,30 +2316,30 @@ function toPosixPath(filePath) {
1955
2316
  return filePath.replace(/\\/g, "/");
1956
2317
  }
1957
2318
  function getNearbyPathSuggestions(targetPath) {
1958
- const parent = path5.dirname(targetPath);
1959
- if (!fs2.existsSync(parent)) {
2319
+ const parent = path6.dirname(targetPath);
2320
+ if (!fs3.existsSync(parent)) {
1960
2321
  return [];
1961
2322
  }
1962
- const needle = path5.basename(targetPath).toLowerCase();
2323
+ const needle = path6.basename(targetPath).toLowerCase();
1963
2324
  const suggestions = [];
1964
- for (const entry of fs2.readdirSync(parent, { withFileTypes: true })) {
2325
+ for (const entry of fs3.readdirSync(parent, { withFileTypes: true })) {
1965
2326
  if (entry.name.toLowerCase().includes(needle)) {
1966
- suggestions.push(path5.join(parent, entry.name));
2327
+ suggestions.push(path6.join(parent, entry.name));
1967
2328
  }
1968
2329
  }
1969
2330
  return suggestions.slice(0, 5);
1970
2331
  }
1971
2332
  function isSkillFile(filePath) {
1972
- return path5.basename(filePath).toLowerCase() === "skill.md";
2333
+ return path6.basename(filePath).toLowerCase() === "skill.md";
1973
2334
  }
1974
2335
  function isScannableTextFile(filePath) {
1975
2336
  if (isSkillFile(filePath)) {
1976
2337
  return true;
1977
2338
  }
1978
- const ext = path5.extname(filePath).toLowerCase();
1979
- return ALLOWED_TEXT_EXTENSIONS3.has(ext);
2339
+ const ext = path6.extname(filePath).toLowerCase();
2340
+ return ALLOWED_TEXT_EXTENSIONS4.has(ext);
1980
2341
  }
1981
- function findSkillDirs(rootDir) {
2342
+ function findSkillDirs2(rootDir) {
1982
2343
  const found = /* @__PURE__ */ new Set();
1983
2344
  const stack = [{ dir: rootDir, depth: 0 }];
1984
2345
  let seen = 0;
@@ -1991,8 +2352,8 @@ function findSkillDirs(rootDir) {
1991
2352
  if (seen > 5e3) {
1992
2353
  break;
1993
2354
  }
1994
- const skillFile = path5.join(current.dir, "SKILL.md");
1995
- if (fs2.existsSync(skillFile) && fs2.statSync(skillFile).isFile()) {
2355
+ const skillFile = path6.join(current.dir, "SKILL.md");
2356
+ if (fs3.existsSync(skillFile) && fs3.statSync(skillFile).isFile()) {
1996
2357
  found.add(current.dir);
1997
2358
  continue;
1998
2359
  }
@@ -2001,7 +2362,7 @@ function findSkillDirs(rootDir) {
2001
2362
  }
2002
2363
  let entries;
2003
2364
  try {
2004
- entries = fs2.readdirSync(current.dir, { withFileTypes: true });
2365
+ entries = fs3.readdirSync(current.dir, { withFileTypes: true });
2005
2366
  } catch {
2006
2367
  continue;
2007
2368
  }
@@ -2009,10 +2370,10 @@ function findSkillDirs(rootDir) {
2009
2370
  if (!entry.isDirectory()) {
2010
2371
  continue;
2011
2372
  }
2012
- if (SKIP_DIRS.has(entry.name)) {
2373
+ if (SKIP_DIRS2.has(entry.name)) {
2013
2374
  continue;
2014
2375
  }
2015
- stack.push({ dir: path5.join(current.dir, entry.name), depth: current.depth + 1 });
2376
+ stack.push({ dir: path6.join(current.dir, entry.name), depth: current.depth + 1 });
2016
2377
  }
2017
2378
  }
2018
2379
  return [...found].sort();
@@ -2021,8 +2382,8 @@ function formatCandidates(candidates) {
2021
2382
  return candidates.map((candidate) => `- ${toPosixPath(candidate)}`).join("\n");
2022
2383
  }
2023
2384
  function resolveSkillDirectory(inputPath, preferredSkillName) {
2024
- const absoluteInput = path5.resolve(inputPath);
2025
- if (!fs2.existsSync(absoluteInput)) {
2385
+ const absoluteInput = path6.resolve(inputPath);
2386
+ if (!fs3.existsSync(absoluteInput)) {
2026
2387
  const suggestions = getNearbyPathSuggestions(absoluteInput);
2027
2388
  const suggestionText = suggestions.length > 0 ? `
2028
2389
  Nearby paths:
@@ -2032,7 +2393,7 @@ ${formatCandidates(suggestions)}` : "";
2032
2393
  `Local path not found: ${toPosixPath(absoluteInput)}${suggestionText}`
2033
2394
  );
2034
2395
  }
2035
- const stat = fs2.statSync(absoluteInput);
2396
+ const stat = fs3.statSync(absoluteInput);
2036
2397
  if (stat.isFile()) {
2037
2398
  if (!isSkillFile(absoluteInput)) {
2038
2399
  throw new GuardSkillsError(
@@ -2041,15 +2402,15 @@ ${formatCandidates(suggestions)}` : "";
2041
2402
  );
2042
2403
  }
2043
2404
  return {
2044
- skillDir: path5.dirname(absoluteInput),
2405
+ skillDir: path6.dirname(absoluteInput),
2045
2406
  note: "Using parent directory of provided SKILL.md file."
2046
2407
  };
2047
2408
  }
2048
- const directSkillFile = path5.join(absoluteInput, "SKILL.md");
2049
- if (fs2.existsSync(directSkillFile) && fs2.statSync(directSkillFile).isFile()) {
2409
+ const directSkillFile = path6.join(absoluteInput, "SKILL.md");
2410
+ if (fs3.existsSync(directSkillFile) && fs3.statSync(directSkillFile).isFile()) {
2050
2411
  return { skillDir: absoluteInput };
2051
2412
  }
2052
- const discovered = findSkillDirs(absoluteInput);
2413
+ const discovered = findSkillDirs2(absoluteInput);
2053
2414
  if (discovered.length === 0) {
2054
2415
  throw new GuardSkillsError(
2055
2416
  "INVALID_LOCAL_PATH",
@@ -2058,7 +2419,7 @@ ${formatCandidates(suggestions)}` : "";
2058
2419
  }
2059
2420
  if (preferredSkillName) {
2060
2421
  const matches = discovered.filter(
2061
- (directory) => path5.basename(directory).toLowerCase() === preferredSkillName.toLowerCase()
2422
+ (directory) => path6.basename(directory).toLowerCase() === preferredSkillName.toLowerCase()
2062
2423
  );
2063
2424
  if (matches.length === 1) {
2064
2425
  const selected = matches[0];
@@ -2070,7 +2431,7 @@ ${formatCandidates(suggestions)}` : "";
2070
2431
  note: `Auto-selected skill '${preferredSkillName}' under the provided path.`
2071
2432
  };
2072
2433
  }
2073
- const available = discovered.map((directory) => path5.basename(directory));
2434
+ const available = discovered.map((directory) => path6.basename(directory));
2074
2435
  throw new GuardSkillsError(
2075
2436
  "INVALID_LOCAL_PATH",
2076
2437
  `Requested --skill '${preferredSkillName}' was not found.
@@ -2093,7 +2454,7 @@ Available skills: ${available.join(", ")}`
2093
2454
  ${formatCandidates(discovered)}`
2094
2455
  );
2095
2456
  }
2096
- function collectLocalFiles(skillDir, options) {
2457
+ function collectLocalFiles2(skillDir, options) {
2097
2458
  const files = [];
2098
2459
  const unverifiableReasons = [];
2099
2460
  const stack = [skillDir];
@@ -2104,15 +2465,15 @@ function collectLocalFiles(skillDir, options) {
2104
2465
  }
2105
2466
  let entries;
2106
2467
  try {
2107
- entries = fs2.readdirSync(currentDir, { withFileTypes: true });
2468
+ entries = fs3.readdirSync(currentDir, { withFileTypes: true });
2108
2469
  } catch {
2109
2470
  unverifiableReasons.push(`Cannot read directory: ${toPosixPath(currentDir)}`);
2110
2471
  continue;
2111
2472
  }
2112
2473
  for (const entry of entries) {
2113
- const fullPath = path5.join(currentDir, entry.name);
2474
+ const fullPath = path6.join(currentDir, entry.name);
2114
2475
  if (entry.isDirectory()) {
2115
- if (!SKIP_DIRS.has(entry.name)) {
2476
+ if (!SKIP_DIRS2.has(entry.name)) {
2116
2477
  stack.push(fullPath);
2117
2478
  }
2118
2479
  continue;
@@ -2120,7 +2481,7 @@ function collectLocalFiles(skillDir, options) {
2120
2481
  if (!entry.isFile()) {
2121
2482
  continue;
2122
2483
  }
2123
- const relativePath = toPosixPath(path5.relative(skillDir, fullPath));
2484
+ const relativePath = toPosixPath(path6.relative(skillDir, fullPath));
2124
2485
  if (!isScannableTextFile(relativePath)) {
2125
2486
  continue;
2126
2487
  }
@@ -2132,7 +2493,7 @@ function collectLocalFiles(skillDir, options) {
2132
2493
  }
2133
2494
  let sizeBytes = 0;
2134
2495
  try {
2135
- sizeBytes = fs2.statSync(fullPath).size;
2496
+ sizeBytes = fs3.statSync(fullPath).size;
2136
2497
  } catch {
2137
2498
  unverifiableReasons.push(`Cannot stat file: ${relativePath}`);
2138
2499
  continue;
@@ -2146,7 +2507,7 @@ function collectLocalFiles(skillDir, options) {
2146
2507
  try {
2147
2508
  files.push({
2148
2509
  path: relativePath,
2149
- content: fs2.readFileSync(fullPath, "utf8")
2510
+ content: fs3.readFileSync(fullPath, "utf8")
2150
2511
  });
2151
2512
  } catch {
2152
2513
  unverifiableReasons.push(`Cannot read text content: ${relativePath}`);
@@ -2159,11 +2520,11 @@ function resolveEffectiveScanLocalOptions(cliOptions, config) {
2159
2520
  const defaults = config.defaults ?? {};
2160
2521
  const resolver = config.resolver ?? {};
2161
2522
  return effectiveScanLocalOptionsSchema.parse({
2162
- strict: cliOptions.strict ?? defaults.strict ?? DEFAULT_OPTIONS3.strict,
2163
- json: cliOptions.json ?? defaults.json ?? DEFAULT_OPTIONS3.json,
2164
- skill: cliOptions.skill ?? DEFAULT_OPTIONS3.skill,
2165
- maxFileBytes: cliOptions.maxFileBytes ?? resolver.maxFileBytes ?? DEFAULT_OPTIONS3.maxFileBytes,
2166
- maxTotalFiles: cliOptions.maxTotalFiles ?? resolver.maxTotalFiles ?? DEFAULT_OPTIONS3.maxTotalFiles
2523
+ strict: cliOptions.strict ?? defaults.strict ?? DEFAULT_OPTIONS4.strict,
2524
+ json: cliOptions.json ?? defaults.json ?? DEFAULT_OPTIONS4.json,
2525
+ skill: cliOptions.skill ?? DEFAULT_OPTIONS4.skill,
2526
+ maxFileBytes: cliOptions.maxFileBytes ?? resolver.maxFileBytes ?? DEFAULT_OPTIONS4.maxFileBytes,
2527
+ maxTotalFiles: cliOptions.maxTotalFiles ?? resolver.maxTotalFiles ?? DEFAULT_OPTIONS4.maxTotalFiles
2167
2528
  });
2168
2529
  }
2169
2530
  async function runScanLocalCommand(inputPath, rawOptions) {
@@ -2171,7 +2532,7 @@ async function runScanLocalCommand(inputPath, rawOptions) {
2171
2532
  const loadedConfig = loadGuardSkillsConfig(cliOptions.config);
2172
2533
  const options = resolveEffectiveScanLocalOptions(cliOptions, loadedConfig.config);
2173
2534
  const target = resolveSkillDirectory(inputPath, options.skill);
2174
- const { files, unverifiableReasons } = collectLocalFiles(target.skillDir, options);
2535
+ const { files, unverifiableReasons } = collectLocalFiles2(target.skillDir, options);
2175
2536
  if (files.length === 0) {
2176
2537
  throw new GuardSkillsError(
2177
2538
  "INVALID_LOCAL_PATH",
@@ -2184,7 +2545,7 @@ async function runScanLocalCommand(inputPath, rawOptions) {
2184
2545
  repo: "local",
2185
2546
  defaultBranch: "local",
2186
2547
  commitSha: "local",
2187
- skillName: options.skill ?? path5.basename(target.skillDir),
2548
+ skillName: options.skill ?? path6.basename(target.skillDir),
2188
2549
  skillDir: toPosixPath(target.skillDir),
2189
2550
  skillFilePath: "SKILL.md",
2190
2551
  files,
@@ -2226,15 +2587,69 @@ async function runScanLocalCommand(inputPath, rawOptions) {
2226
2587
  }
2227
2588
 
2228
2589
  // src/cli.ts
2590
+ function withCommonAddOptions(command, requireSkill) {
2591
+ const configured = command.option("--config <path>", "Path to guardskills.config.json").option("--strict", "Use stricter risk thresholds").option("--ci", "Deterministic CI mode: scan + gate only, no install handoff").option("--json", "Output machine-readable JSON").option("--yes", "Auto-confirm warnings").option("--dry-run", "Scan only, do not install").option("--force", "Override UNSAFE outcome").option("--allow-unverifiable", "Override UNVERIFIABLE outcome").option("--github-timeout-ms <ms>", "GitHub API request timeout in milliseconds").option("--github-retries <count>", "Retry count for retryable GitHub errors").option("--github-retry-base-ms <ms>", "Base backoff delay for GitHub retries").option("--max-file-bytes <bytes>", "Max file size to scan").option("--max-aux-files <count>", "Max auxiliary files from scripts/src folders").option("--max-total-files <count>", "Max total resolved files to scan");
2592
+ return requireSkill ? configured.requiredOption("--skill <name>", "Skill name to install") : configured.option("--skill <name>", "Skill name to install");
2593
+ }
2229
2594
  async function main() {
2230
2595
  const program = new Command();
2231
- program.name("guardskills").description("Security wrapper around skills add").version("1.1.0");
2232
- program.command("add").description("Scan a skill source and conditionally install it via skills CLI").argument("<repo>", "GitHub repository URL or owner/repo").requiredOption("--skill <name>", "Skill name to install").option("--config <path>", "Path to guardskills.config.json").option("--strict", "Use stricter risk thresholds").option("--ci", "Deterministic CI mode: scan + gate only, no install handoff").option("--json", "Output machine-readable JSON").option("--yes", "Auto-confirm warnings").option("--dry-run", "Scan only, do not install").option("--force", "Override UNSAFE outcome").option("--allow-unverifiable", "Override UNVERIFIABLE outcome").option("--github-timeout-ms <ms>", "GitHub API request timeout in milliseconds").option("--github-retries <count>", "Retry count for retryable GitHub errors").option("--github-retry-base-ms <ms>", "Base backoff delay for GitHub retries").option("--max-file-bytes <bytes>", "Max file size to scan").option("--max-aux-files <count>", "Max auxiliary files from scripts/src folders").option("--max-total-files <count>", "Max total resolved files to scan").action(async (repo, options) => {
2233
- const code = await runAddCommand(repo, options);
2596
+ program.name("guardskills").description("Security wrapper around skill installation CLIs").version("1.1.0");
2597
+ const legacyAdd = program.command("add").description("Scan a skill source and conditionally install it via skills CLI").argument("<repo>", "GitHub repository URL or owner/repo");
2598
+ withCommonAddOptions(legacyAdd, true).action(
2599
+ async (repo, options) => {
2600
+ const code = await runAddCommand(repo, options);
2601
+ process.exitCode = code;
2602
+ }
2603
+ );
2604
+ const skills = program.command("skills").description("Guarded wrapper for skills.sh install commands");
2605
+ withCommonAddOptions(
2606
+ skills.command("add").description("Scan a skill source and conditionally install it via skills CLI").argument("<repo>", "GitHub repository URL or owner/repo"),
2607
+ false
2608
+ ).action(async (repo, options) => {
2609
+ const resolvedSkill = typeof options.skill === "string" ? options.skill : void 0;
2610
+ const code = resolvedSkill ? await runAddCommand(repo, options, {
2611
+ provider: "skills",
2612
+ commandName: "guardskills skills add"
2613
+ }) : await runInteractiveInstallCommand("skills", repo, void 0, options);
2614
+ process.exitCode = code;
2615
+ });
2616
+ const playbooks = program.command("playbooks").description("Guarded wrapper for Playbooks install commands");
2617
+ withCommonAddOptions(
2618
+ playbooks.command("add").description("Scan a skill source and conditionally install it via Playbooks CLI").argument("<resource>", "Playbooks resource type (must be 'skill')").argument("<repo>", "GitHub repository URL or owner/repo"),
2619
+ true
2620
+ ).action(async (resource, repo, options) => {
2621
+ if (resource !== "skill") {
2622
+ throw new GuardSkillsError(
2623
+ "INVALID_OPTIONS",
2624
+ `Unsupported playbooks resource '${resource}'. Use: guardskills playbooks add skill <repo> --skill <name>`
2625
+ );
2626
+ }
2627
+ const code = await runAddCommand(repo, options, {
2628
+ provider: "playbooks",
2629
+ commandName: "guardskills playbooks add skill"
2630
+ });
2631
+ process.exitCode = code;
2632
+ });
2633
+ const openskills = program.command("openskills").description("Guarded wrapper for OpenSkills install commands");
2634
+ withCommonAddOptions(
2635
+ openskills.command("install").description("Scan a skill source and conditionally install it via OpenSkills CLI").argument("<repo>", "GitHub repository URL or owner/repo").argument("[skill]", "Skill name to install"),
2636
+ false
2637
+ ).action(async (repo, skill, options) => {
2638
+ const resolvedSkill = skill ?? (typeof options.skill === "string" ? options.skill : void 0);
2639
+ const code = await runInteractiveInstallCommand("openskills", repo, resolvedSkill, options);
2640
+ process.exitCode = code;
2641
+ });
2642
+ const skillkit = program.command("skillkit").description("Guarded wrapper for skillkit install commands");
2643
+ withCommonAddOptions(
2644
+ skillkit.command("install").description("Scan a skill source and conditionally install it via skillkit CLI").argument("<repo>", "GitHub repository URL or owner/repo").argument("[skill]", "Skill name to install"),
2645
+ false
2646
+ ).action(async (repo, skill, options) => {
2647
+ const resolvedSkill = skill ?? (typeof options.skill === "string" ? options.skill : void 0);
2648
+ const code = await runInteractiveInstallCommand("skillkit", repo, resolvedSkill, options);
2234
2649
  process.exitCode = code;
2235
2650
  });
2236
- program.command("scan-local").description("Scan a local skill folder and print a risk decision").argument("<path>", "Local folder path (or SKILL.md file path)").option("--skill <name>", "Skill directory name when path contains multiple skills").option("--config <path>", "Path to guardskills.config.json").option("--strict", "Use stricter risk thresholds").option("--json", "Output machine-readable JSON").option("--max-file-bytes <bytes>", "Max file size to scan").option("--max-total-files <count>", "Max total files to scan").action(async (inputPath, options) => {
2237
- const code = await runScanLocalCommand(inputPath, options);
2651
+ program.command("scan-local").description("Scan a local skill folder and print a risk decision").argument("<path>", "Local folder path (or SKILL.md file path)").option("--skill <name>", "Skill directory name when path contains multiple skills").option("--config <path>", "Path to guardskills.config.json").option("--strict", "Use stricter risk thresholds").option("--json", "Output machine-readable JSON").option("--max-file-bytes <bytes>", "Max file size to scan").option("--max-total-files <count>", "Max total files to scan").action(async (repo, options) => {
2652
+ const code = await runScanLocalCommand(repo, options);
2238
2653
  process.exitCode = code;
2239
2654
  });
2240
2655
  program.command("scan-clawhub").description("Scan a ClawHub skill package and print a risk decision").argument("<identifier>", "ClawHub package identifier").option("--skill <name>", "Override skill folder name to resolve in source repository").option("--version <version>", "Preferred package version/tag").option("--clawhub-registry <url>", "ClawHub registry base URL").option("--config <path>", "Path to guardskills.config.json").option("--strict", "Use stricter risk thresholds").option("--json", "Output machine-readable JSON").option("--github-timeout-ms <ms>", "Upstream resolver request timeout in milliseconds").option("--github-retries <count>", "Retry count for retryable upstream errors").option("--github-retry-base-ms <ms>", "Base backoff delay for upstream retries").option("--max-file-bytes <bytes>", "Max file size to scan").option("--max-aux-files <count>", "Max auxiliary files from scripts/src folders").option("--max-total-files <count>", "Max total resolved files to scan").action(async (identifier, options) => {