guardskills 1.0.0 → 1.1.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 +101 -64
  2. package/dist/cli.cjs +791 -28
  3. package/dist/cli.js +791 -28
  4. package/package.json +2 -1
package/dist/cli.js CHANGED
@@ -284,6 +284,73 @@ function printHumanLocalReport(report) {
284
284
  function printJsonLocalReport(report) {
285
285
  console.log(JSON.stringify(report, null, 2));
286
286
  }
287
+ function printHumanClawHubReport(report) {
288
+ console.log(`Command: ${report.command}`);
289
+ console.log(`Identifier: ${report.identifier}`);
290
+ console.log(`Registry: ${report.registry}`);
291
+ console.log(`Mapped Repo: ${report.repo}`);
292
+ console.log(`Skill: ${report.skill}`);
293
+ if (report.version) {
294
+ console.log(`Version: ${report.version}`);
295
+ }
296
+ console.log(`Mode: ${report.strict ? "strict" : "standard"}`);
297
+ if (report.configPath) {
298
+ console.log(`Config: ${report.configPath}`);
299
+ }
300
+ if (report.skillDir) {
301
+ console.log(`Skill Dir: ${report.skillDir}`);
302
+ }
303
+ if (report.commitSha) {
304
+ console.log(`Commit: ${report.commitSha}`);
305
+ }
306
+ if (report.moderation) {
307
+ const parts = [];
308
+ if (report.moderation.isSuspicious) {
309
+ parts.push("suspicious");
310
+ }
311
+ if (report.moderation.isMalwareBlocked) {
312
+ parts.push("malware-blocked");
313
+ }
314
+ if (report.moderation.isRemoved) {
315
+ parts.push("removed");
316
+ }
317
+ if (parts.length > 0) {
318
+ console.log(`ClawHub Moderation: ${parts.join(", ")}`);
319
+ }
320
+ }
321
+ console.log(`Files Scanned: ${report.scanFiles.length}`);
322
+ if (report.decision.riskScore === null) {
323
+ console.log("Result: UNVERIFIABLE");
324
+ } else {
325
+ console.log(`Risk Score: ${report.decision.riskScore.toFixed(1)}/100`);
326
+ console.log(`Decision: ${report.decision.level}`);
327
+ }
328
+ if (report.unverifiableReasons && report.unverifiableReasons.length > 0) {
329
+ console.log("Unverifiable Reasons:");
330
+ for (const reason of report.unverifiableReasons) {
331
+ console.log(`- ${reason}`);
332
+ }
333
+ }
334
+ if (report.decision.chainMatches.length > 0) {
335
+ console.log("Attack Chains:");
336
+ for (const chain of report.decision.chainMatches) {
337
+ console.log(`- ${chain.id} (+${chain.bonus}): ${chain.description}`);
338
+ }
339
+ }
340
+ if (report.decision.findings.length > 0) {
341
+ console.log("Findings:");
342
+ for (const finding of report.decision.findings.slice(0, 10)) {
343
+ const fileText = finding.file ? ` (${finding.file})` : "";
344
+ console.log(`- [${finding.severity}/${finding.confidence}] ${finding.title}${fileText}`);
345
+ }
346
+ } else {
347
+ console.log("Findings: none");
348
+ }
349
+ console.log(`Note: ${report.note}`);
350
+ }
351
+ function printJsonClawHubReport(report) {
352
+ console.log(JSON.stringify(report, null, 2));
353
+ }
287
354
 
288
355
  // src/resolver/github.ts
289
356
  import path2 from "path";
@@ -1146,10 +1213,18 @@ async function runAddCommand(repo, rawOptions) {
1146
1213
  return runSkillsInstall(repo, options.skill);
1147
1214
  }
1148
1215
 
1149
- // src/commands/scan-local.ts
1150
- import fs2 from "fs";
1151
- import path4 from "path";
1216
+ // src/commands/scan-clawhub.ts
1152
1217
  import { z as z3 } from "zod";
1218
+
1219
+ // src/resolver/clawhub.ts
1220
+ import path4 from "path";
1221
+ import JSZip from "jszip";
1222
+ var DEFAULT_REGISTRY_BASE_URL = "https://clawhub.ai";
1223
+ var DEFAULT_ARCHIVE_BASE_URL = "https://auth.clawdhub.com";
1224
+ var DEFAULT_REQUEST_TIMEOUT_MS2 = 15e3;
1225
+ var DEFAULT_MAX_FILE_SIZE_BYTES = 25e4;
1226
+ var DEFAULT_MAX_TOTAL_FILES = 120;
1227
+ var RETRYABLE_STATUS2 = /* @__PURE__ */ new Set([429, 500, 502, 503, 504]);
1153
1228
  var ALLOWED_TEXT_EXTENSIONS2 = /* @__PURE__ */ new Set([
1154
1229
  ".md",
1155
1230
  ".txt",
@@ -1169,23 +1244,707 @@ var ALLOWED_TEXT_EXTENSIONS2 = /* @__PURE__ */ new Set([
1169
1244
  ".ini",
1170
1245
  ".cfg"
1171
1246
  ]);
1172
- var SKIP_DIRS = /* @__PURE__ */ new Set([".git", "node_modules", "dist", "build", ".next", ".turbo"]);
1173
- var cliScanLocalOptionsSchema = z3.object({
1247
+ function normalizeRegistryBaseUrl(input) {
1248
+ const raw = input?.trim() || DEFAULT_REGISTRY_BASE_URL;
1249
+ const withProtocol = /^https?:\/\//i.test(raw) ? raw : `https://${raw}`;
1250
+ const url = new URL(withProtocol);
1251
+ const pathname = url.pathname.endsWith("/") ? url.pathname.slice(0, -1) : url.pathname;
1252
+ return `${url.origin}${pathname}`;
1253
+ }
1254
+ function toApiCandidates(registryBaseUrl, identifier) {
1255
+ const encodedIdentifier = encodeURIComponent(identifier);
1256
+ const slugOnly = identifier.split("/").filter(Boolean).at(-1) ?? identifier;
1257
+ const encodedSlug = encodeURIComponent(slugOnly);
1258
+ return [
1259
+ `${registryBaseUrl}/api/v1/package/${encodedIdentifier}`,
1260
+ `${registryBaseUrl}/api/package/${encodedIdentifier}`,
1261
+ `${registryBaseUrl}/api/v1/skills/${encodedSlug}`
1262
+ ];
1263
+ }
1264
+ function tryParseClawHubUrl(input) {
1265
+ try {
1266
+ const parsed = new URL(input.trim());
1267
+ const host = parsed.hostname.toLowerCase();
1268
+ if (host !== "clawhub.ai" && host !== "www.clawhub.ai") {
1269
+ return null;
1270
+ }
1271
+ return parsed;
1272
+ } catch {
1273
+ return null;
1274
+ }
1275
+ }
1276
+ function extractIdentifierCandidates(input) {
1277
+ const trimmed = input.trim();
1278
+ if (!trimmed) {
1279
+ return [];
1280
+ }
1281
+ const candidates = [];
1282
+ const pushUnique = (value) => {
1283
+ if (!value) {
1284
+ return;
1285
+ }
1286
+ const normalized = value.trim();
1287
+ if (!normalized) {
1288
+ return;
1289
+ }
1290
+ if (!candidates.includes(normalized)) {
1291
+ candidates.push(normalized);
1292
+ }
1293
+ };
1294
+ try {
1295
+ const parsed = new URL(trimmed);
1296
+ const host = parsed.hostname.toLowerCase();
1297
+ if (host !== "clawhub.ai" && host !== "www.clawhub.ai") {
1298
+ return [trimmed];
1299
+ }
1300
+ const segments = parsed.pathname.split("/").filter(Boolean);
1301
+ if (segments.length >= 2 && segments[0] && segments[1]) {
1302
+ pushUnique(`${segments[0]}/${segments[1]}`);
1303
+ }
1304
+ if (segments.length >= 3 && segments[0]?.toLowerCase() === "skills" && segments[1] && segments[2]) {
1305
+ pushUnique(`${segments[1]}/${segments[2]}`);
1306
+ }
1307
+ if (segments.length > 0) {
1308
+ pushUnique(segments.join("/"));
1309
+ const last = segments.at(-1);
1310
+ pushUnique(last);
1311
+ }
1312
+ } catch {
1313
+ pushUnique(trimmed);
1314
+ }
1315
+ return candidates;
1316
+ }
1317
+ function mapClawHubError(error, operation) {
1318
+ if (error instanceof GuardSkillsError) {
1319
+ return error;
1320
+ }
1321
+ const status = typeof error === "object" && error !== null && "status" in error ? error.status : void 0;
1322
+ const message = typeof error === "object" && error !== null && "message" in error ? String(error.message) : String(error);
1323
+ const lowerMessage = message.toLowerCase();
1324
+ if (status === 401 || status === 403) {
1325
+ return new GuardSkillsError(
1326
+ "CLAWHUB_AUTH",
1327
+ `${operation} failed: authentication/authorization error from ClawHub.`,
1328
+ { status, cause: error }
1329
+ );
1330
+ }
1331
+ if (status === 404) {
1332
+ return new GuardSkillsError("CLAWHUB_NOT_FOUND", `${operation} failed: resource not found.`, {
1333
+ status,
1334
+ cause: error
1335
+ });
1336
+ }
1337
+ if (status !== void 0 && RETRYABLE_STATUS2.has(status)) {
1338
+ return new GuardSkillsError(
1339
+ status === 429 ? "CLAWHUB_RATE_LIMIT" : "CLAWHUB_TRANSIENT",
1340
+ `${operation} failed with retryable ClawHub status ${status}.`,
1341
+ { status, retryable: true, cause: error }
1342
+ );
1343
+ }
1344
+ if (lowerMessage.includes("timeout") || lowerMessage.includes("timed out") || lowerMessage.includes("abort")) {
1345
+ return new GuardSkillsError("CLAWHUB_TIMEOUT", `${operation} timed out while calling ClawHub API.`, {
1346
+ retryable: true,
1347
+ cause: error
1348
+ });
1349
+ }
1350
+ return new GuardSkillsError("CLAWHUB_UNKNOWN", `${operation} failed: ${message}`, {
1351
+ status,
1352
+ cause: error
1353
+ });
1354
+ }
1355
+ function unwrapResponsePayload(payload) {
1356
+ if (!payload || typeof payload !== "object") {
1357
+ throw new GuardSkillsError("CLAWHUB_UNKNOWN", "ClawHub returned a non-object JSON payload.");
1358
+ }
1359
+ const objectPayload = payload;
1360
+ if (objectPayload.data && typeof objectPayload.data === "object") {
1361
+ return objectPayload.data;
1362
+ }
1363
+ return objectPayload;
1364
+ }
1365
+ async function fetchJson(url, timeoutMs) {
1366
+ const controller = new AbortController();
1367
+ const timeout = setTimeout(() => controller.abort(), timeoutMs);
1368
+ try {
1369
+ const response = await fetch(url, {
1370
+ method: "GET",
1371
+ headers: { accept: "application/json" },
1372
+ signal: controller.signal
1373
+ });
1374
+ if (!response.ok) {
1375
+ throw { status: response.status, message: `${response.status} ${response.statusText}` };
1376
+ }
1377
+ const payload = await response.json();
1378
+ return unwrapResponsePayload(payload);
1379
+ } catch (error) {
1380
+ throw mapClawHubError(error, `ClawHub metadata fetch (${url})`);
1381
+ } finally {
1382
+ clearTimeout(timeout);
1383
+ }
1384
+ }
1385
+ async function fetchText(url, timeoutMs) {
1386
+ const controller = new AbortController();
1387
+ const timeout = setTimeout(() => controller.abort(), timeoutMs);
1388
+ try {
1389
+ const response = await fetch(url, {
1390
+ method: "GET",
1391
+ headers: { accept: "text/html,application/xhtml+xml" },
1392
+ signal: controller.signal
1393
+ });
1394
+ if (!response.ok) {
1395
+ throw { status: response.status, message: `${response.status} ${response.statusText}` };
1396
+ }
1397
+ return await response.text();
1398
+ } catch (error) {
1399
+ throw mapClawHubError(error, `ClawHub page fetch (${url})`);
1400
+ } finally {
1401
+ clearTimeout(timeout);
1402
+ }
1403
+ }
1404
+ async function fetchArrayBuffer(url, timeoutMs) {
1405
+ const controller = new AbortController();
1406
+ const timeout = setTimeout(() => controller.abort(), timeoutMs);
1407
+ try {
1408
+ const response = await fetch(url, {
1409
+ method: "GET",
1410
+ signal: controller.signal
1411
+ });
1412
+ if (!response.ok) {
1413
+ throw { status: response.status, message: `${response.status} ${response.statusText}` };
1414
+ }
1415
+ return await response.arrayBuffer();
1416
+ } catch (error) {
1417
+ throw mapClawHubError(error, `ClawHub archive fetch (${url})`);
1418
+ } finally {
1419
+ clearTimeout(timeout);
1420
+ }
1421
+ }
1422
+ function normalizeRepoRef(candidate) {
1423
+ const trimmed = candidate.trim();
1424
+ if (!trimmed) {
1425
+ return null;
1426
+ }
1427
+ const shorthand = trimmed.match(/^([A-Za-z0-9_.-]+)\/([A-Za-z0-9_.-]+)$/);
1428
+ if (shorthand && shorthand[1] && shorthand[2]) {
1429
+ return `${shorthand[1]}/${shorthand[2].replace(/\.git$/i, "")}`;
1430
+ }
1431
+ try {
1432
+ const parsed = new URL(trimmed);
1433
+ if (!(parsed.hostname === "github.com" || parsed.hostname === "www.github.com")) {
1434
+ return null;
1435
+ }
1436
+ const parts = parsed.pathname.split("/").filter(Boolean);
1437
+ if (parts.length < 2 || !parts[0] || !parts[1]) {
1438
+ return null;
1439
+ }
1440
+ return `${parts[0]}/${parts[1].replace(/\.git$/i, "")}`;
1441
+ } catch {
1442
+ return null;
1443
+ }
1444
+ }
1445
+ function getNestedString(obj, ...keys) {
1446
+ let cursor = obj;
1447
+ for (const key of keys) {
1448
+ if (!cursor || typeof cursor !== "object" || !(key in cursor)) {
1449
+ return void 0;
1450
+ }
1451
+ cursor = cursor[key];
1452
+ }
1453
+ return typeof cursor === "string" && cursor.trim() ? cursor.trim() : void 0;
1454
+ }
1455
+ function getNestedBoolean(obj, ...keys) {
1456
+ let cursor = obj;
1457
+ for (const key of keys) {
1458
+ if (!cursor || typeof cursor !== "object" || !(key in cursor)) {
1459
+ return void 0;
1460
+ }
1461
+ cursor = cursor[key];
1462
+ }
1463
+ return typeof cursor === "boolean" ? cursor : void 0;
1464
+ }
1465
+ function isLikelyTextFile2(filePath) {
1466
+ const lower = filePath.toLowerCase();
1467
+ if (lower.endsWith("/skill.md") || lower === "skill.md") {
1468
+ return true;
1469
+ }
1470
+ const ext = path4.posix.extname(lower);
1471
+ return ALLOWED_TEXT_EXTENSIONS2.has(ext);
1472
+ }
1473
+ function isBinaryContent2(content) {
1474
+ return content.includes("\0");
1475
+ }
1476
+ function collectGitHubRepos(value, collected, seen) {
1477
+ if (value === null || value === void 0) {
1478
+ return;
1479
+ }
1480
+ if (typeof value === "string") {
1481
+ const repo = normalizeRepoRef(value);
1482
+ if (repo) {
1483
+ collected.add(repo);
1484
+ }
1485
+ return;
1486
+ }
1487
+ if (typeof value !== "object") {
1488
+ return;
1489
+ }
1490
+ if (seen.has(value)) {
1491
+ return;
1492
+ }
1493
+ seen.add(value);
1494
+ if (Array.isArray(value)) {
1495
+ for (const item of value) {
1496
+ collectGitHubRepos(item, collected, seen);
1497
+ }
1498
+ return;
1499
+ }
1500
+ for (const nested of Object.values(value)) {
1501
+ collectGitHubRepos(nested, collected, seen);
1502
+ }
1503
+ }
1504
+ function collectPossibleSkillNames(identifier, metadata, override) {
1505
+ const names = /* @__PURE__ */ new Set();
1506
+ if (override?.trim()) {
1507
+ names.add(override.trim());
1508
+ }
1509
+ const lastSegment = identifier.split(/[/:@]/).filter(Boolean).at(-1);
1510
+ if (lastSegment) {
1511
+ names.add(lastSegment);
1512
+ }
1513
+ const maybePush = (value) => {
1514
+ if (typeof value === "string" && value.trim()) {
1515
+ names.add(value.trim());
1516
+ }
1517
+ };
1518
+ maybePush(metadata.skill);
1519
+ maybePush(metadata.slug);
1520
+ maybePush(metadata.name);
1521
+ maybePush(metadata.id);
1522
+ return [...names];
1523
+ }
1524
+ function parseArchiveMetadata(metadata, identifier, requestedVersion) {
1525
+ const ownerFromMetadata = getNestedString(metadata, "owner", "handle") ?? getNestedString(metadata, "owner") ?? getNestedString(metadata, "author") ?? getNestedString(metadata, "publisher");
1526
+ const slugFromMetadata = getNestedString(metadata, "skill", "slug") ?? getNestedString(metadata, "slug") ?? getNestedString(metadata, "name");
1527
+ const versionFromMetadata = requestedVersion ?? getNestedString(metadata, "latestVersion", "version") ?? getNestedString(metadata, "version");
1528
+ const identifierParts = identifier.split("/").filter(Boolean);
1529
+ const ownerFromIdentifier = identifierParts.length >= 2 ? identifierParts[0] : void 0;
1530
+ const slugFromIdentifier = identifierParts.length > 0 ? identifierParts.at(-1) : void 0;
1531
+ const owner = ownerFromMetadata ?? ownerFromIdentifier;
1532
+ const slug = slugFromMetadata ?? slugFromIdentifier;
1533
+ if (!owner || !slug) {
1534
+ return null;
1535
+ }
1536
+ return { owner, slug, version: versionFromMetadata };
1537
+ }
1538
+ function parseClawHubModeration(metadata) {
1539
+ const isSuspicious = getNestedBoolean(metadata, "moderation", "isSuspicious") ?? getNestedBoolean(metadata, "isSuspicious");
1540
+ const isMalwareBlocked = getNestedBoolean(metadata, "moderation", "isMalwareBlocked") ?? getNestedBoolean(metadata, "isMalwareBlocked");
1541
+ const isRemoved = getNestedBoolean(metadata, "moderation", "isRemoved") ?? getNestedBoolean(metadata, "isRemoved");
1542
+ if (isSuspicious === void 0 && isMalwareBlocked === void 0 && isRemoved === void 0) {
1543
+ return void 0;
1544
+ }
1545
+ return { isSuspicious, isMalwareBlocked, isRemoved };
1546
+ }
1547
+ async function resolveSkillFromArchive(metadata, identifier, options) {
1548
+ const archiveMeta = parseArchiveMetadata(metadata, identifier, options.version);
1549
+ if (!archiveMeta) {
1550
+ return null;
1551
+ }
1552
+ const timeoutMs = options.requestTimeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS2;
1553
+ const maxFileSizeBytes = options.maxFileSizeBytes ?? DEFAULT_MAX_FILE_SIZE_BYTES;
1554
+ const maxTotalFiles = options.maxTotalFiles ?? DEFAULT_MAX_TOTAL_FILES;
1555
+ const params = new URLSearchParams({ slug: archiveMeta.slug });
1556
+ if (archiveMeta.version) {
1557
+ params.set("version", archiveMeta.version);
1558
+ }
1559
+ const downloadUrl = `${DEFAULT_ARCHIVE_BASE_URL}/api/v1/download?${params.toString()}`;
1560
+ const archiveBuffer = await fetchArrayBuffer(downloadUrl, timeoutMs);
1561
+ const zip = await JSZip.loadAsync(Buffer.from(archiveBuffer));
1562
+ const files = new Array();
1563
+ const unverifiableReasons = [];
1564
+ let skillFilePath = null;
1565
+ const entries = Object.values(zip.files);
1566
+ for (const entry of entries) {
1567
+ if (entry.dir) {
1568
+ continue;
1569
+ }
1570
+ const normalizedPath = entry.name.replace(/\\/g, "/");
1571
+ if (!isLikelyTextFile2(normalizedPath)) {
1572
+ continue;
1573
+ }
1574
+ if (files.length >= maxTotalFiles) {
1575
+ unverifiableReasons.push(
1576
+ `Resolved archive file count exceeds maxTotalFiles=${maxTotalFiles}; scan truncated.`
1577
+ );
1578
+ break;
1579
+ }
1580
+ const contentBuffer = await entry.async("nodebuffer");
1581
+ if (contentBuffer.length > maxFileSizeBytes) {
1582
+ unverifiableReasons.push(
1583
+ `File too large to scan safely: ${normalizedPath}`
1584
+ );
1585
+ continue;
1586
+ }
1587
+ const content = contentBuffer.toString("utf8");
1588
+ if (isBinaryContent2(content)) {
1589
+ unverifiableReasons.push(`Binary content detected: ${normalizedPath}`);
1590
+ continue;
1591
+ }
1592
+ files.push({ path: normalizedPath, content });
1593
+ if (!skillFilePath && normalizedPath.toLowerCase().endsWith("/skill.md")) {
1594
+ skillFilePath = normalizedPath;
1595
+ }
1596
+ if (!skillFilePath && normalizedPath.toLowerCase() === "skill.md") {
1597
+ skillFilePath = normalizedPath;
1598
+ }
1599
+ }
1600
+ if (!skillFilePath) {
1601
+ skillFilePath = files.find((file) => file.path.toLowerCase().endsWith("skill.md"))?.path ?? null;
1602
+ }
1603
+ if (!skillFilePath || files.length === 0) {
1604
+ return null;
1605
+ }
1606
+ const moderation = parseClawHubModeration(metadata);
1607
+ return {
1608
+ source: `clawhub:${archiveMeta.owner}/${archiveMeta.slug}${archiveMeta.version ? `@${archiveMeta.version}` : ""}`,
1609
+ owner: archiveMeta.owner,
1610
+ repo: "clawhub-archive",
1611
+ defaultBranch: "archive",
1612
+ commitSha: archiveMeta.version ?? "archive",
1613
+ skillName: options.skillNameOverride ?? archiveMeta.slug,
1614
+ skillDir: path4.posix.dirname(skillFilePath),
1615
+ skillFilePath,
1616
+ files,
1617
+ unverifiableReasons: [...unverifiableReasons],
1618
+ sourceMetadata: moderation ? {
1619
+ clawhubModeration: moderation
1620
+ } : void 0
1621
+ };
1622
+ }
1623
+ function extractMetadataFromClawHubPage(html, pageUrl) {
1624
+ const metadata = {};
1625
+ const githubUrlMatch = html.match(/https?:\/\/github\.com\/([A-Za-z0-9_.-]+)\/([A-Za-z0-9_.-]+)/i);
1626
+ if (githubUrlMatch?.[1] && githubUrlMatch[2]) {
1627
+ metadata.repository = `https://github.com/${githubUrlMatch[1]}/${githubUrlMatch[2]}`;
1628
+ }
1629
+ const directRepoMatch = html.match(/"repository"\s*:\s*"([^"]+)"/i);
1630
+ if (directRepoMatch?.[1]) {
1631
+ metadata.repository = directRepoMatch[1];
1632
+ }
1633
+ const pathnameParts = pageUrl.pathname.split("/").filter(Boolean);
1634
+ if (pathnameParts.length >= 2 && pathnameParts[1]) {
1635
+ metadata.skill = pathnameParts[1];
1636
+ }
1637
+ return Object.keys(metadata).length > 0 ? metadata : null;
1638
+ }
1639
+ async function resolveFromClawHubPageUrl(input, timeoutMs) {
1640
+ const parsed = tryParseClawHubUrl(input);
1641
+ if (!parsed) {
1642
+ return null;
1643
+ }
1644
+ const html = await fetchText(parsed.toString(), timeoutMs);
1645
+ return extractMetadataFromClawHubPage(html, parsed);
1646
+ }
1647
+ async function resolveSkillFromClawHub(identifier, options = {}) {
1648
+ const identifierCandidates = extractIdentifierCandidates(identifier);
1649
+ if (identifierCandidates.length === 0) {
1650
+ throw new GuardSkillsError("CLAWHUB_UNKNOWN", "ClawHub identifier is required.");
1651
+ }
1652
+ const requestTimeoutMs = options.requestTimeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS2;
1653
+ const registryBaseUrl = normalizeRegistryBaseUrl(options.registryBaseUrl);
1654
+ let metadata = null;
1655
+ let resolvedIdentifier = null;
1656
+ let lastError = null;
1657
+ for (const identifierCandidate of identifierCandidates) {
1658
+ const candidateUrls = toApiCandidates(registryBaseUrl, identifierCandidate);
1659
+ for (const url of candidateUrls) {
1660
+ try {
1661
+ metadata = await fetchJson(url, requestTimeoutMs);
1662
+ resolvedIdentifier = identifierCandidate;
1663
+ break;
1664
+ } catch (error) {
1665
+ const mapped = mapClawHubError(error, "ClawHub package lookup");
1666
+ lastError = mapped;
1667
+ if (mapped.code !== "CLAWHUB_NOT_FOUND") {
1668
+ break;
1669
+ }
1670
+ }
1671
+ }
1672
+ if (metadata) {
1673
+ break;
1674
+ }
1675
+ }
1676
+ if (!metadata) {
1677
+ try {
1678
+ metadata = await resolveFromClawHubPageUrl(identifier, requestTimeoutMs);
1679
+ if (metadata) {
1680
+ resolvedIdentifier = identifierCandidates[0] ?? identifier;
1681
+ }
1682
+ } catch (error) {
1683
+ lastError = mapClawHubError(error, "ClawHub skill page fallback");
1684
+ }
1685
+ }
1686
+ if (!metadata) {
1687
+ if (lastError) {
1688
+ throw lastError;
1689
+ }
1690
+ throw new GuardSkillsError(
1691
+ "CLAWHUB_NOT_FOUND",
1692
+ `Unable to resolve '${identifier}' from ClawHub. Candidates tried: ${identifierCandidates.join(", ")}.`
1693
+ );
1694
+ }
1695
+ const canonicalIdentifier = resolvedIdentifier ?? identifierCandidates[0] ?? identifier;
1696
+ const repos = /* @__PURE__ */ new Set();
1697
+ collectGitHubRepos(metadata, repos, /* @__PURE__ */ new Set());
1698
+ const skillCandidates = collectPossibleSkillNames(canonicalIdentifier, metadata, options.skillNameOverride);
1699
+ if (skillCandidates.length === 0) {
1700
+ throw new GuardSkillsError(
1701
+ "CLAWHUB_UNKNOWN",
1702
+ `Could not infer skill name for ClawHub package '${canonicalIdentifier}'. Use --skill.`
1703
+ );
1704
+ }
1705
+ let resolved = null;
1706
+ let resolveError;
1707
+ const githubOptions = {
1708
+ requestTimeoutMs: options.requestTimeoutMs,
1709
+ maxFileSizeBytes: options.maxFileSizeBytes,
1710
+ maxAuxFiles: options.maxAuxFiles,
1711
+ maxTotalFiles: options.maxTotalFiles,
1712
+ retries: options.retries,
1713
+ retryBaseDelayMs: options.retryBaseDelayMs
1714
+ };
1715
+ if (repos.size > 0) {
1716
+ for (const repo of repos) {
1717
+ for (const skillName of skillCandidates) {
1718
+ try {
1719
+ resolved = await resolveSkillFromGitHub(repo, skillName, githubOptions);
1720
+ break;
1721
+ } catch (error) {
1722
+ resolveError = error;
1723
+ }
1724
+ }
1725
+ if (resolved) {
1726
+ break;
1727
+ }
1728
+ }
1729
+ }
1730
+ if (!resolved) {
1731
+ const archiveResolved = await resolveSkillFromArchive(metadata, canonicalIdentifier, options);
1732
+ if (archiveResolved) {
1733
+ return archiveResolved;
1734
+ }
1735
+ if (repos.size === 0) {
1736
+ throw new GuardSkillsError(
1737
+ "CLAWHUB_UNKNOWN",
1738
+ `ClawHub package '${canonicalIdentifier}' did not expose a resolvable GitHub source or archive payload.`
1739
+ );
1740
+ }
1741
+ throw new GuardSkillsError(
1742
+ "SKILL_NOT_FOUND",
1743
+ `Unable to map ClawHub package '${canonicalIdentifier}' to a GitHub skill. Repos tried: ${[...repos].join(", ")}. Skill names tried: ${skillCandidates.join(", ")}.`,
1744
+ { cause: resolveError }
1745
+ );
1746
+ }
1747
+ const packageVersion = options.version ?? (typeof metadata.version === "string" ? metadata.version : void 0);
1748
+ const sourceSuffix = packageVersion ? `${canonicalIdentifier}@${packageVersion}` : canonicalIdentifier;
1749
+ const moderation = parseClawHubModeration(metadata);
1750
+ return {
1751
+ ...resolved,
1752
+ source: `clawhub:${sourceSuffix}`,
1753
+ unverifiableReasons: [...resolved.unverifiableReasons],
1754
+ sourceMetadata: moderation ? {
1755
+ ...resolved.sourceMetadata ?? {},
1756
+ clawhubModeration: moderation
1757
+ } : resolved.sourceMetadata
1758
+ };
1759
+ }
1760
+
1761
+ // src/commands/scan-clawhub.ts
1762
+ var cliScanClawHubOptionsSchema = z3.object({
1174
1763
  config: z3.string().optional(),
1175
1764
  strict: z3.boolean().optional(),
1176
1765
  json: z3.boolean().optional(),
1177
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(),
1178
1772
  maxFileBytes: z3.coerce.number().int().min(4096).max(5e6).optional(),
1773
+ maxAuxFiles: z3.coerce.number().int().min(1).max(200).optional(),
1179
1774
  maxTotalFiles: z3.coerce.number().int().min(1).max(400).optional()
1180
1775
  });
1181
- var effectiveScanLocalOptionsSchema = z3.object({
1776
+ var effectiveScanClawHubOptionsSchema = z3.object({
1182
1777
  strict: z3.boolean(),
1183
1778
  json: z3.boolean(),
1184
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),
1185
1785
  maxFileBytes: z3.number().int().min(4096).max(5e6),
1786
+ maxAuxFiles: z3.number().int().min(1).max(200),
1186
1787
  maxTotalFiles: z3.number().int().min(1).max(400)
1187
1788
  });
1188
1789
  var DEFAULT_OPTIONS2 = {
1790
+ strict: false,
1791
+ json: false,
1792
+ skill: void 0,
1793
+ version: void 0,
1794
+ clawhubRegistry: "https://clawhub.ai",
1795
+ githubTimeoutMs: 15e3,
1796
+ githubRetries: 2,
1797
+ githubRetryBaseMs: 300,
1798
+ maxFileBytes: 25e4,
1799
+ maxAuxFiles: 40,
1800
+ maxTotalFiles: 120
1801
+ };
1802
+ function alignWithClawHubModeration(baseDecision, moderation) {
1803
+ if (!moderation) {
1804
+ return { decision: baseDecision };
1805
+ }
1806
+ if (moderation.isMalwareBlocked) {
1807
+ return {
1808
+ decision: {
1809
+ ...baseDecision,
1810
+ riskScore: 100,
1811
+ safetyScore: 0,
1812
+ level: "CRITICAL",
1813
+ reason: "ClawHub moderation marked this skill as malware-blocked."
1814
+ },
1815
+ moderationNote: "Aligned with ClawHub moderation: malware-blocked."
1816
+ };
1817
+ }
1818
+ if (moderation.isSuspicious && baseDecision.level === "SAFE") {
1819
+ const adjustedRisk = Math.max(baseDecision.riskScore ?? 0, 30);
1820
+ return {
1821
+ decision: {
1822
+ ...baseDecision,
1823
+ riskScore: adjustedRisk,
1824
+ safetyScore: 100 - adjustedRisk,
1825
+ level: "WARNING",
1826
+ reason: "ClawHub moderation marked this skill as suspicious."
1827
+ },
1828
+ moderationNote: "Aligned with ClawHub moderation: suspicious."
1829
+ };
1830
+ }
1831
+ return { decision: baseDecision };
1832
+ }
1833
+ function resolveEffectiveScanClawHubOptions(cliOptions, config) {
1834
+ const defaults = config.defaults ?? {};
1835
+ const resolver = config.resolver ?? {};
1836
+ 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
1848
+ });
1849
+ }
1850
+ async function runScanClawHubCommand(identifier, rawOptions) {
1851
+ const cliOptions = cliScanClawHubOptionsSchema.parse(rawOptions);
1852
+ const loadedConfig = loadGuardSkillsConfig(cliOptions.config);
1853
+ const options = resolveEffectiveScanClawHubOptions(cliOptions, loadedConfig.config);
1854
+ const resolved = await resolveSkillFromClawHub(identifier, {
1855
+ registryBaseUrl: options.clawhubRegistry,
1856
+ version: options.version,
1857
+ skillNameOverride: options.skill,
1858
+ requestTimeoutMs: options.githubTimeoutMs,
1859
+ retries: options.githubRetries,
1860
+ retryBaseDelayMs: options.githubRetryBaseMs,
1861
+ maxFileSizeBytes: options.maxFileBytes,
1862
+ maxAuxFiles: options.maxAuxFiles,
1863
+ maxTotalFiles: options.maxTotalFiles
1864
+ });
1865
+ const scan = scanResolvedSkill(resolved);
1866
+ const baseDecision = calculateRiskScore(scan.findings, {
1867
+ strict: options.strict,
1868
+ trustCredits: 0,
1869
+ hasUnverifiableContent: scan.hasUnverifiableContent
1870
+ });
1871
+ const moderation = resolved.sourceMetadata?.clawhubModeration;
1872
+ const { decision, moderationNote } = alignWithClawHubModeration(baseDecision, moderation);
1873
+ const noteParts = ["ClawHub scan complete."];
1874
+ if (moderationNote) {
1875
+ noteParts.push(moderationNote);
1876
+ }
1877
+ if (decision.level === "UNSAFE" || decision.level === "CRITICAL" || decision.level === "UNVERIFIABLE") {
1878
+ noteParts.push("Blocked-level risk detected.");
1879
+ }
1880
+ if (loadedConfig.path) {
1881
+ noteParts.push(`Config: ${loadedConfig.path}`);
1882
+ }
1883
+ const report = {
1884
+ command: "guardskills scan-clawhub",
1885
+ identifier,
1886
+ registry: options.clawhubRegistry,
1887
+ strict: options.strict,
1888
+ configPath: loadedConfig.path ?? void 0,
1889
+ decision,
1890
+ scanFiles: resolved.files.map((file) => file.path),
1891
+ skillDir: resolved.skillDir,
1892
+ repo: `${resolved.owner}/${resolved.repo}`,
1893
+ skill: resolved.skillName,
1894
+ version: options.version,
1895
+ commitSha: resolved.commitSha,
1896
+ moderation,
1897
+ unverifiableReasons: scan.unverifiableReasons,
1898
+ note: noteParts.join(" ")
1899
+ };
1900
+ if (options.json) {
1901
+ printJsonClawHubReport(report);
1902
+ } else {
1903
+ printHumanClawHubReport(report);
1904
+ }
1905
+ return decision.level === "UNSAFE" || decision.level === "CRITICAL" || decision.level === "UNVERIFIABLE" ? 20 : 0;
1906
+ }
1907
+
1908
+ // 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([
1913
+ ".md",
1914
+ ".txt",
1915
+ ".sh",
1916
+ ".bash",
1917
+ ".zsh",
1918
+ ".ps1",
1919
+ ".py",
1920
+ ".js",
1921
+ ".ts",
1922
+ ".mjs",
1923
+ ".cjs",
1924
+ ".json",
1925
+ ".yaml",
1926
+ ".yml",
1927
+ ".toml",
1928
+ ".ini",
1929
+ ".cfg"
1930
+ ]);
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()
1939
+ });
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)
1946
+ });
1947
+ var DEFAULT_OPTIONS3 = {
1189
1948
  strict: false,
1190
1949
  json: false,
1191
1950
  skill: void 0,
@@ -1196,28 +1955,28 @@ function toPosixPath(filePath) {
1196
1955
  return filePath.replace(/\\/g, "/");
1197
1956
  }
1198
1957
  function getNearbyPathSuggestions(targetPath) {
1199
- const parent = path4.dirname(targetPath);
1958
+ const parent = path5.dirname(targetPath);
1200
1959
  if (!fs2.existsSync(parent)) {
1201
1960
  return [];
1202
1961
  }
1203
- const needle = path4.basename(targetPath).toLowerCase();
1962
+ const needle = path5.basename(targetPath).toLowerCase();
1204
1963
  const suggestions = [];
1205
1964
  for (const entry of fs2.readdirSync(parent, { withFileTypes: true })) {
1206
1965
  if (entry.name.toLowerCase().includes(needle)) {
1207
- suggestions.push(path4.join(parent, entry.name));
1966
+ suggestions.push(path5.join(parent, entry.name));
1208
1967
  }
1209
1968
  }
1210
1969
  return suggestions.slice(0, 5);
1211
1970
  }
1212
1971
  function isSkillFile(filePath) {
1213
- return path4.basename(filePath).toLowerCase() === "skill.md";
1972
+ return path5.basename(filePath).toLowerCase() === "skill.md";
1214
1973
  }
1215
1974
  function isScannableTextFile(filePath) {
1216
1975
  if (isSkillFile(filePath)) {
1217
1976
  return true;
1218
1977
  }
1219
- const ext = path4.extname(filePath).toLowerCase();
1220
- return ALLOWED_TEXT_EXTENSIONS2.has(ext);
1978
+ const ext = path5.extname(filePath).toLowerCase();
1979
+ return ALLOWED_TEXT_EXTENSIONS3.has(ext);
1221
1980
  }
1222
1981
  function findSkillDirs(rootDir) {
1223
1982
  const found = /* @__PURE__ */ new Set();
@@ -1232,7 +1991,7 @@ function findSkillDirs(rootDir) {
1232
1991
  if (seen > 5e3) {
1233
1992
  break;
1234
1993
  }
1235
- const skillFile = path4.join(current.dir, "SKILL.md");
1994
+ const skillFile = path5.join(current.dir, "SKILL.md");
1236
1995
  if (fs2.existsSync(skillFile) && fs2.statSync(skillFile).isFile()) {
1237
1996
  found.add(current.dir);
1238
1997
  continue;
@@ -1253,7 +2012,7 @@ function findSkillDirs(rootDir) {
1253
2012
  if (SKIP_DIRS.has(entry.name)) {
1254
2013
  continue;
1255
2014
  }
1256
- stack.push({ dir: path4.join(current.dir, entry.name), depth: current.depth + 1 });
2015
+ stack.push({ dir: path5.join(current.dir, entry.name), depth: current.depth + 1 });
1257
2016
  }
1258
2017
  }
1259
2018
  return [...found].sort();
@@ -1262,7 +2021,7 @@ function formatCandidates(candidates) {
1262
2021
  return candidates.map((candidate) => `- ${toPosixPath(candidate)}`).join("\n");
1263
2022
  }
1264
2023
  function resolveSkillDirectory(inputPath, preferredSkillName) {
1265
- const absoluteInput = path4.resolve(inputPath);
2024
+ const absoluteInput = path5.resolve(inputPath);
1266
2025
  if (!fs2.existsSync(absoluteInput)) {
1267
2026
  const suggestions = getNearbyPathSuggestions(absoluteInput);
1268
2027
  const suggestionText = suggestions.length > 0 ? `
@@ -1282,11 +2041,11 @@ ${formatCandidates(suggestions)}` : "";
1282
2041
  );
1283
2042
  }
1284
2043
  return {
1285
- skillDir: path4.dirname(absoluteInput),
2044
+ skillDir: path5.dirname(absoluteInput),
1286
2045
  note: "Using parent directory of provided SKILL.md file."
1287
2046
  };
1288
2047
  }
1289
- const directSkillFile = path4.join(absoluteInput, "SKILL.md");
2048
+ const directSkillFile = path5.join(absoluteInput, "SKILL.md");
1290
2049
  if (fs2.existsSync(directSkillFile) && fs2.statSync(directSkillFile).isFile()) {
1291
2050
  return { skillDir: absoluteInput };
1292
2051
  }
@@ -1299,7 +2058,7 @@ ${formatCandidates(suggestions)}` : "";
1299
2058
  }
1300
2059
  if (preferredSkillName) {
1301
2060
  const matches = discovered.filter(
1302
- (directory) => path4.basename(directory).toLowerCase() === preferredSkillName.toLowerCase()
2061
+ (directory) => path5.basename(directory).toLowerCase() === preferredSkillName.toLowerCase()
1303
2062
  );
1304
2063
  if (matches.length === 1) {
1305
2064
  const selected = matches[0];
@@ -1311,7 +2070,7 @@ ${formatCandidates(suggestions)}` : "";
1311
2070
  note: `Auto-selected skill '${preferredSkillName}' under the provided path.`
1312
2071
  };
1313
2072
  }
1314
- const available = discovered.map((directory) => path4.basename(directory));
2073
+ const available = discovered.map((directory) => path5.basename(directory));
1315
2074
  throw new GuardSkillsError(
1316
2075
  "INVALID_LOCAL_PATH",
1317
2076
  `Requested --skill '${preferredSkillName}' was not found.
@@ -1351,7 +2110,7 @@ function collectLocalFiles(skillDir, options) {
1351
2110
  continue;
1352
2111
  }
1353
2112
  for (const entry of entries) {
1354
- const fullPath = path4.join(currentDir, entry.name);
2113
+ const fullPath = path5.join(currentDir, entry.name);
1355
2114
  if (entry.isDirectory()) {
1356
2115
  if (!SKIP_DIRS.has(entry.name)) {
1357
2116
  stack.push(fullPath);
@@ -1361,7 +2120,7 @@ function collectLocalFiles(skillDir, options) {
1361
2120
  if (!entry.isFile()) {
1362
2121
  continue;
1363
2122
  }
1364
- const relativePath = toPosixPath(path4.relative(skillDir, fullPath));
2123
+ const relativePath = toPosixPath(path5.relative(skillDir, fullPath));
1365
2124
  if (!isScannableTextFile(relativePath)) {
1366
2125
  continue;
1367
2126
  }
@@ -1400,11 +2159,11 @@ function resolveEffectiveScanLocalOptions(cliOptions, config) {
1400
2159
  const defaults = config.defaults ?? {};
1401
2160
  const resolver = config.resolver ?? {};
1402
2161
  return effectiveScanLocalOptionsSchema.parse({
1403
- strict: cliOptions.strict ?? defaults.strict ?? DEFAULT_OPTIONS2.strict,
1404
- json: cliOptions.json ?? defaults.json ?? DEFAULT_OPTIONS2.json,
1405
- skill: cliOptions.skill ?? DEFAULT_OPTIONS2.skill,
1406
- maxFileBytes: cliOptions.maxFileBytes ?? resolver.maxFileBytes ?? DEFAULT_OPTIONS2.maxFileBytes,
1407
- maxTotalFiles: cliOptions.maxTotalFiles ?? resolver.maxTotalFiles ?? DEFAULT_OPTIONS2.maxTotalFiles
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
1408
2167
  });
1409
2168
  }
1410
2169
  async function runScanLocalCommand(inputPath, rawOptions) {
@@ -1425,7 +2184,7 @@ async function runScanLocalCommand(inputPath, rawOptions) {
1425
2184
  repo: "local",
1426
2185
  defaultBranch: "local",
1427
2186
  commitSha: "local",
1428
- skillName: options.skill ?? path4.basename(target.skillDir),
2187
+ skillName: options.skill ?? path5.basename(target.skillDir),
1429
2188
  skillDir: toPosixPath(target.skillDir),
1430
2189
  skillFilePath: "SKILL.md",
1431
2190
  files,
@@ -1469,7 +2228,7 @@ async function runScanLocalCommand(inputPath, rawOptions) {
1469
2228
  // src/cli.ts
1470
2229
  async function main() {
1471
2230
  const program = new Command();
1472
- program.name("guardskills").description("Security wrapper around skills add").version("1.0.0");
2231
+ program.name("guardskills").description("Security wrapper around skills add").version("1.1.0");
1473
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) => {
1474
2233
  const code = await runAddCommand(repo, options);
1475
2234
  process.exitCode = code;
@@ -1478,6 +2237,10 @@ async function main() {
1478
2237
  const code = await runScanLocalCommand(inputPath, options);
1479
2238
  process.exitCode = code;
1480
2239
  });
2240
+ 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) => {
2241
+ const code = await runScanClawHubCommand(identifier, options);
2242
+ process.exitCode = code;
2243
+ });
1481
2244
  await program.parseAsync(process.argv);
1482
2245
  }
1483
2246
  main().catch((error) => {