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