wittyhub 0.0.3 → 0.0.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.mjs CHANGED
@@ -11,14 +11,18 @@ import { existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync } from
11
11
  import { basename, dirname, extname, isAbsolute, join, normalize, relative, resolve, sep } from "path";
12
12
  import { fileURLToPath } from "url";
13
13
  import { createHash } from "node:crypto";
14
- import { stripVTControlCharacters } from "node:util";
15
- import { homedir, platform, tmpdir } from "os";
14
+ import { execFile } from "node:child_process";
15
+ import { promisify, stripVTControlCharacters } from "node:util";
16
+ import { mkdtemp, rm, writeFile } from "node:fs/promises";
17
+ import { homedir, tmpdir } from "node:os";
18
+ import { dirname as dirname$1, join as join$1, sep as sep$1 } from "node:path";
19
+ import { homedir as homedir$1, platform, tmpdir as tmpdir$1 } from "os";
20
+ import { parse } from "yaml";
16
21
  import * as readline from "readline";
17
22
  import { Writable } from "stream";
18
- import { promisify } from "util";
19
- import { execFile, execSync, spawn, spawnSync } from "child_process";
20
- import { access, cp, lstat, mkdir, mkdtemp, readFile, readdir, readlink, realpath, rm, stat, symlink, writeFile } from "fs/promises";
21
- import { parse } from "yaml";
23
+ import { promisify as promisify$1 } from "util";
24
+ import { execFile as execFile$1, execSync, spawn, spawnSync } from "child_process";
25
+ import { access, cp, lstat, mkdir, mkdtemp as mkdtemp$1, readFile, readdir, readlink, realpath, rm as rm$1, stat, symlink, writeFile as writeFile$1 } from "fs/promises";
22
26
  import { createHash as createHash$1 } from "crypto";
23
27
  import { gunzipSync, inflateRawSync } from "node:zlib";
24
28
  var import_picocolors = /* @__PURE__ */ __toESM(require_picocolors(), 1);
@@ -251,6 +255,30 @@ function isWellKnownUrl(input) {
251
255
  return false;
252
256
  }
253
257
  }
258
+ const CLI_CONFIG_PATH = join(homedir$1(), ".config", "wittyhub", "cli.yaml");
259
+ function loadCliConfig() {
260
+ if (!existsSync(CLI_CONFIG_PATH)) return {};
261
+ try {
262
+ const parsed = parse(readFileSync(CLI_CONFIG_PATH, "utf-8"));
263
+ if (!parsed || typeof parsed !== "object") return {};
264
+ return parsed;
265
+ } catch {
266
+ return {};
267
+ }
268
+ }
269
+ const DEFAULT_API_URL = "https://skillhub.openeuler.org/api/v1";
270
+ const DEFAULT_SEARCH_URL = "https://skillhub.openeuler.org/api/v1/index/search";
271
+ const DEFAULT_TELEMETRY_URL = "https://skillhub.openeuler.org/api/v1/skills/telemetry";
272
+ const DEFAULT_AUDIT_URL = "https://skillhub.openeuler.org/api/v1/skills/{skill_id}/audit";
273
+ const DEFAULT_GET_URL = "https://skillhub.openeuler.org/api/v1/skills/{skill_id}";
274
+ const DEFAULT_LIST_URL = "https://skillhub.openeuler.org/api/v1/skills/";
275
+ const cliConfig = loadCliConfig();
276
+ const DOWNLOAD_URL = `${typeof cliConfig.api_url === "string" && cliConfig.api_url.trim() ? cliConfig.api_url.trim() : process.env.SKILLS_API_URL ? `${process.env.SKILLS_API_URL.replace(/\/$/, "")}/api/v1` : DEFAULT_API_URL}/skills/{skill_id}/download`;
277
+ const SEARCH_URL = typeof cliConfig.search_url === "string" && cliConfig.search_url.trim() ? cliConfig.search_url.trim() : process.env.SKILLS_API_URL ? `${process.env.SKILLS_API_URL.replace(/\/$/, "")}/api/v1/index/search` : DEFAULT_SEARCH_URL;
278
+ const TELEMETRY_URL = typeof cliConfig.telemetry_url === "string" && cliConfig.telemetry_url.trim() ? cliConfig.telemetry_url.trim() : DEFAULT_TELEMETRY_URL;
279
+ const AUDIT_URL = typeof cliConfig.audit_url === "string" && cliConfig.audit_url.trim() ? cliConfig.audit_url.trim() : DEFAULT_AUDIT_URL;
280
+ const GET_URL = typeof cliConfig.get_url === "string" && cliConfig.get_url.trim() ? cliConfig.get_url.trim() : DEFAULT_GET_URL;
281
+ const LIST_URL = typeof cliConfig.list_url === "string" && cliConfig.list_url.trim() ? cliConfig.list_url.trim() : GET_URL.replace(/\{skill_id\}[^/]*$/, "") || DEFAULT_LIST_URL;
254
282
  const CSI_RE = /\x1b\[[\x30-\x3f]*[\x20-\x2f]*[\x40-\x7e]/g;
255
283
  const OSC_RE = /\x1b\][\s\S]*?(?:\x07|\x1b\\)/g;
256
284
  const DCS_PM_APC_RE = /\x1b[P^_][\s\S]*?(?:\x1b\\)/g;
@@ -453,7 +481,7 @@ const CLONE_TIMEOUT_MS = (() => {
453
481
  const parsed = Number.parseInt(raw, 10);
454
482
  return Number.isFinite(parsed) && parsed > 0 ? parsed : DEFAULT_CLONE_TIMEOUT_MS;
455
483
  })();
456
- const execFileAsync = promisify(execFile);
484
+ const execFileAsync$1 = promisify$1(execFile$1);
457
485
  var GitCloneError = class extends Error {
458
486
  url;
459
487
  isTimeout;
@@ -524,11 +552,12 @@ function createGitClient(extraEnv) {
524
552
  "filter.lfs.smudge=",
525
553
  "filter.lfs.clean=",
526
554
  "filter.lfs.process="
527
- ]
555
+ ],
556
+ unsafe: { allowUnsafeFilter: true }
528
557
  });
529
558
  }
530
559
  async function resetTempDir(dir) {
531
- await rm(dir, {
560
+ await rm$1(dir, {
532
561
  recursive: true,
533
562
  force: true
534
563
  }).catch(() => {});
@@ -537,7 +566,7 @@ async function resetTempDir(dir) {
537
566
  async function tryGhClone(repo, tempDir, ref) {
538
567
  let cloneTarget = repo.slug;
539
568
  try {
540
- const { stdout, stderr } = await execFileAsync("gh", [
569
+ const { stdout, stderr } = await execFileAsync$1("gh", [
541
570
  "auth",
542
571
  "status",
543
572
  "-h",
@@ -554,7 +583,7 @@ async function tryGhClone(repo, tempDir, ref) {
554
583
  } catch {
555
584
  return false;
556
585
  }
557
- await execFileAsync("gh", [
586
+ await execFileAsync$1("gh", [
558
587
  "repo",
559
588
  "clone",
560
589
  cloneTarget,
@@ -580,7 +609,7 @@ function buildGitHubAuthError(url, repo, message) {
580
609
  return `Authentication failed for ${url}.\n - For private repos, ensure you have access\n - For SSH: Check your keys with 'ssh -T git@github.com'\n - For HTTPS: Run 'gh auth login' or configure git credentials`;
581
610
  }
582
611
  async function cloneRepo(url, ref) {
583
- const tempDir = await mkdtemp(join(tmpdir(), "skills-"));
612
+ const tempDir = await mkdtemp$1(join(tmpdir$1(), "skills-"));
584
613
  const cloneOptions = ref ? [
585
614
  "--depth",
586
615
  "1",
@@ -596,7 +625,7 @@ async function cloneRepo(url, ref) {
596
625
  const isTimeout = errorMessage.includes("block timeout") || errorMessage.includes("timed out");
597
626
  const isAuthError = isAuthFailure(errorMessage);
598
627
  if (isTimeout) {
599
- await rm(tempDir, {
628
+ await rm$1(tempDir, {
600
629
  recursive: true,
601
630
  force: true
602
631
  }).catch(() => {});
@@ -613,7 +642,7 @@ async function cloneRepo(url, ref) {
613
642
  return tempDir;
614
643
  } catch {}
615
644
  }
616
- await rm(tempDir, {
645
+ await rm$1(tempDir, {
617
646
  recursive: true,
618
647
  force: true
619
648
  }).catch(() => {});
@@ -623,9 +652,9 @@ async function cloneRepo(url, ref) {
623
652
  }
624
653
  async function cleanupTempDir(dir) {
625
654
  const normalizedDir = normalize(resolve(dir));
626
- const normalizedTmpDir = normalize(resolve(tmpdir()));
655
+ const normalizedTmpDir = normalize(resolve(tmpdir$1()));
627
656
  if (!normalizedDir.startsWith(normalizedTmpDir + sep) && normalizedDir !== normalizedTmpDir) throw new Error("Attempted to clean up directory outside of temp directory");
628
- await rm(dir, {
657
+ await rm$1(dir, {
629
658
  recursive: true,
630
659
  force: true
631
660
  });
@@ -731,7 +760,7 @@ async function writeLocalLock(lock, cwd) {
731
760
  version: lock.version,
732
761
  skills: sortedSkills
733
762
  };
734
- await writeFile(lockPath, JSON.stringify(sorted, null, 2) + "\n", "utf-8");
763
+ await writeFile$1(lockPath, JSON.stringify(sorted, null, 2) + "\n", "utf-8");
735
764
  }
736
765
  async function computeSkillFolderHash(skillDir) {
737
766
  const files = [];
@@ -950,7 +979,7 @@ function filterSkills(skills, inputNames) {
950
979
  return normalizedInputs.some((input) => input === name || input === displayName);
951
980
  });
952
981
  }
953
- const home = homedir();
982
+ const home = homedir$1();
954
983
  const configHome = xdgConfig ?? join(home, ".config");
955
984
  const codexHome = process.env.CODEX_HOME?.trim() || join(home, ".codex");
956
985
  const claudeHome = process.env.CLAUDE_CONFIG_DIR?.trim() || join(home, ".claude");
@@ -1679,7 +1708,7 @@ async function isDirEntryOrSymlinkToDir(entry, entryPath) {
1679
1708
  }
1680
1709
  }
1681
1710
  function getCanonicalSkillsDir(global, cwd) {
1682
- return join(global ? homedir() : cwd || process.cwd(), AGENTS_DIR$1, SKILLS_SUBDIR);
1711
+ return join(global ? homedir$1() : cwd || process.cwd(), AGENTS_DIR$1, SKILLS_SUBDIR);
1683
1712
  }
1684
1713
  function getEveSubagentSkillsDir(subagent, cwd) {
1685
1714
  return join(cwd || process.cwd(), EVE_SUBAGENTS_DIR, sanitizeName(subagent), "skills");
@@ -1688,7 +1717,7 @@ function getAgentBaseDir(agentType, global, cwd, eveSubagent) {
1688
1717
  if (isUniversalAgent(agentType)) return getCanonicalSkillsDir(global, cwd);
1689
1718
  if (agentType === "eve" && eveSubagent) return getEveSubagentSkillsDir(eveSubagent, cwd);
1690
1719
  const agent = agents[agentType];
1691
- const baseDir = global ? homedir() : cwd || process.cwd();
1720
+ const baseDir = global ? homedir$1() : cwd || process.cwd();
1692
1721
  if (global) {
1693
1722
  if (agent.globalSkillsDir === void 0) return join(baseDir, agent.skillsDir);
1694
1723
  return agent.globalSkillsDir;
@@ -1700,7 +1729,7 @@ function resolveSymlinkTarget(linkPath, linkTarget) {
1700
1729
  }
1701
1730
  async function cleanAndCreateDirectory(path) {
1702
1731
  try {
1703
- await rm(path, {
1732
+ await rm$1(path, {
1704
1733
  recursive: true,
1705
1734
  force: true
1706
1735
  });
@@ -1727,11 +1756,11 @@ async function createSymlink(target, linkPath) {
1727
1756
  try {
1728
1757
  if ((await lstat(linkPath)).isSymbolicLink()) {
1729
1758
  if (resolveSymlinkTarget(linkPath, await readlink(linkPath)) === resolvedTarget) return true;
1730
- await rm(linkPath);
1731
- } else await rm(linkPath, { recursive: true });
1759
+ await rm$1(linkPath);
1760
+ } else await rm$1(linkPath, { recursive: true });
1732
1761
  } catch (err) {
1733
1762
  if (err && typeof err === "object" && "code" in err && err.code === "ELOOP") try {
1734
- await rm(linkPath, { force: true });
1763
+ await rm$1(linkPath, { force: true });
1735
1764
  } catch {}
1736
1765
  }
1737
1766
  const linkDir = dirname(linkPath);
@@ -1872,7 +1901,7 @@ async function copyDirectory(src, dest, agentType) {
1872
1901
  if (entry.isDirectory()) await copyDirectory(srcPath, destPath, agentType);
1873
1902
  else try {
1874
1903
  if (agentType === "eve" && entry.name.toLowerCase() === "skill.md") {
1875
- await writeFile(destPath, stripIgnoredEveFrontmatter(await readFile(srcPath, "utf-8")));
1904
+ await writeFile$1(destPath, stripIgnoredEveFrontmatter(await readFile(srcPath, "utf-8")));
1876
1905
  return;
1877
1906
  }
1878
1907
  await cp(srcPath, destPath, {
@@ -1927,88 +1956,6 @@ function getCanonicalPath(skillName, options = {}) {
1927
1956
  if (!isPathSafe$1(canonicalBase, canonicalPath)) throw new Error("Invalid skill name: potential path traversal detected");
1928
1957
  return canonicalPath;
1929
1958
  }
1930
- async function installWellKnownSkillForAgent(skill, agentType, options = {}) {
1931
- const agent = agents[agentType];
1932
- const isGlobal = options.global ?? false;
1933
- const cwd = options.cwd || process.cwd();
1934
- const installMode = options.mode ?? "symlink";
1935
- const eveSubagent = options.eveSubagent;
1936
- if (isGlobal && agent.globalSkillsDir === void 0) return {
1937
- success: false,
1938
- path: "",
1939
- mode: installMode,
1940
- error: `${agent.displayName} does not support global skill installation`
1941
- };
1942
- const skillName = sanitizeName(skill.installName);
1943
- const canonicalBase = agentType === "eve" && installMode === "symlink" ? getAgentBaseDir(agentType, isGlobal, cwd, eveSubagent) : getCanonicalSkillsDir(isGlobal, cwd);
1944
- const canonicalDir = join(canonicalBase, skillName);
1945
- const agentBase = getAgentBaseDir(agentType, isGlobal, cwd, eveSubagent);
1946
- const agentDir = join(agentBase, skillName);
1947
- if (!isPathSafe$1(canonicalBase, canonicalDir)) return {
1948
- success: false,
1949
- path: agentDir,
1950
- mode: installMode,
1951
- error: "Invalid skill name: potential path traversal detected"
1952
- };
1953
- if (!isPathSafe$1(agentBase, agentDir)) return {
1954
- success: false,
1955
- path: agentDir,
1956
- mode: installMode,
1957
- error: "Invalid skill name: potential path traversal detected"
1958
- };
1959
- async function writeSkillFiles(targetDir) {
1960
- for (const [filePath, content] of skill.files) {
1961
- const fullPath = join(targetDir, filePath);
1962
- if (!isPathSafe$1(targetDir, fullPath)) continue;
1963
- const parentDir = dirname(fullPath);
1964
- if (parentDir !== targetDir) await mkdir(parentDir, { recursive: true });
1965
- await writeFile(fullPath, agentType === "eve" && basename(filePath).toLowerCase() === "skill.md" && typeof content === "string" ? stripIgnoredEveFrontmatter(content) : content);
1966
- }
1967
- }
1968
- try {
1969
- if (installMode === "copy") {
1970
- await cleanAndCreateDirectory(agentDir);
1971
- await writeSkillFiles(agentDir);
1972
- return {
1973
- success: true,
1974
- path: agentDir,
1975
- mode: "copy"
1976
- };
1977
- }
1978
- await cleanAndCreateDirectory(canonicalDir);
1979
- await writeSkillFiles(canonicalDir);
1980
- if (isGlobal && isUniversalAgent(agentType)) return {
1981
- success: true,
1982
- path: canonicalDir,
1983
- canonicalPath: canonicalDir,
1984
- mode: "symlink"
1985
- };
1986
- if (!await createSymlink(canonicalDir, agentDir)) {
1987
- await cleanAndCreateDirectory(agentDir);
1988
- await writeSkillFiles(agentDir);
1989
- return {
1990
- success: true,
1991
- path: agentDir,
1992
- canonicalPath: canonicalDir,
1993
- mode: "symlink",
1994
- symlinkFailed: true
1995
- };
1996
- }
1997
- return {
1998
- success: true,
1999
- path: agentDir,
2000
- canonicalPath: canonicalDir,
2001
- mode: "symlink"
2002
- };
2003
- } catch (error) {
2004
- return {
2005
- success: false,
2006
- path: agentDir,
2007
- mode: installMode,
2008
- error: error instanceof Error ? error.message : "Unknown error"
2009
- };
2010
- }
2011
- }
2012
1959
  async function installBlobSkillForAgent(skill, agentType, options = {}) {
2013
1960
  const agent = agents[agentType];
2014
1961
  const isGlobal = options.global ?? false;
@@ -2033,11 +1980,11 @@ async function installBlobSkillForAgent(skill, agentType, options = {}) {
2033
1980
  };
2034
1981
  try {
2035
1982
  await mkdir(agentBase, { recursive: true });
2036
- await rm(flatSkillPath, {
1983
+ await rm$1(flatSkillPath, {
2037
1984
  recursive: true,
2038
1985
  force: true
2039
1986
  });
2040
- await writeFile(flatSkillPath, getEveFlatSkillMarkdown(skill.files), "utf-8");
1987
+ await writeFile$1(flatSkillPath, getEveFlatSkillMarkdown(skill.files), "utf-8");
2041
1988
  return {
2042
1989
  success: true,
2043
1990
  path: flatSkillPath,
@@ -2073,7 +2020,7 @@ async function installBlobSkillForAgent(skill, agentType, options = {}) {
2073
2020
  if (!isPathSafe$1(targetDir, fullPath)) continue;
2074
2021
  const parentDir = dirname(fullPath);
2075
2022
  if (parentDir !== targetDir) await mkdir(parentDir, { recursive: true });
2076
- await writeFile(fullPath, agentType === "eve" && basename(file.path).toLowerCase() === "skill.md" ? stripIgnoredEveFrontmatter(file.contents) : file.contents, "utf-8");
2023
+ await writeFile$1(fullPath, agentType === "eve" && basename(file.path).toLowerCase() === "skill.md" ? stripIgnoredEveFrontmatter(file.contents) : file.contents, "utf-8");
2077
2024
  }
2078
2025
  }
2079
2026
  try {
@@ -2260,21 +2207,6 @@ async function listInstalledSkills(options = {}) {
2260
2207
  } catch {}
2261
2208
  return Array.from(skillsMap.values());
2262
2209
  }
2263
- const CLI_CONFIG_PATH = join(homedir(), ".config", "wittyhub", "cli.yaml");
2264
- function loadCliConfig() {
2265
- if (!existsSync(CLI_CONFIG_PATH)) return {};
2266
- try {
2267
- const parsed = parse(readFileSync(CLI_CONFIG_PATH, "utf-8"));
2268
- if (!parsed || typeof parsed !== "object") return {};
2269
- return parsed;
2270
- } catch {
2271
- return {};
2272
- }
2273
- }
2274
- const DEFAULT_TELEMETRY_URL = "http://localhost:8080/api/v1/skills/telemetry";
2275
- const cliConfig = loadCliConfig();
2276
- const TELEMETRY_URL = typeof cliConfig.telemetry_url === "string" && cliConfig.telemetry_url.trim() ? cliConfig.telemetry_url.trim() : DEFAULT_TELEMETRY_URL;
2277
- typeof cliConfig.audit_url === "string" && cliConfig.audit_url.trim() && cliConfig.audit_url.trim();
2278
2210
  let cliVersion = null;
2279
2211
  let detectedAgentName = null;
2280
2212
  function setDetectedAgent(agentName) {
@@ -2289,6 +2221,67 @@ function isEnabled() {
2289
2221
  function setVersion(version) {
2290
2222
  cliVersion = version;
2291
2223
  }
2224
+ function slugifyTelemetryValue(value) {
2225
+ const lowered = value.trim().toLowerCase();
2226
+ if (!lowered) return "";
2227
+ let normalized = lowered.replace(/[^a-z0-9._-]+/g, "-");
2228
+ normalized = normalized.replace(/-{2,}/g, "-");
2229
+ return normalized.replace(/^-|-$/g, "");
2230
+ }
2231
+ function buildSkillId(sourceType, ownerRepo, skillName, skillFiles) {
2232
+ if (![
2233
+ "github",
2234
+ "gitcode",
2235
+ "gitlab",
2236
+ "gitee"
2237
+ ].includes(sourceType)) return null;
2238
+ if (!ownerRepo) return null;
2239
+ const slugifiedOwnerRepo = ownerRepo.split("/").map(slugifyTelemetryValue).join("/");
2240
+ if (skillFiles) {
2241
+ const relativePath = skillFiles[skillName];
2242
+ if (relativePath) {
2243
+ const normalizedPath = relativePath.trim().replace(/\\/g, "/").replace(/\/+$/, "");
2244
+ if (normalizedPath === "SKILL.md") return `${sourceType}/${slugifiedOwnerRepo}/${slugifiedOwnerRepo.split("/").pop()}`;
2245
+ if (normalizedPath.endsWith("/SKILL.md")) return `${sourceType}/${slugifiedOwnerRepo}/${normalizedPath.slice(0, -9)}`;
2246
+ }
2247
+ }
2248
+ const skillPath = slugifyTelemetryValue(skillName);
2249
+ if (!skillPath) return null;
2250
+ return `${sourceType}/${slugifiedOwnerRepo}/${skillPath}`;
2251
+ }
2252
+ async function fetchAuditData(source, skillSlugs, sourceType = "github", skillFiles, timeoutMs = 15e3) {
2253
+ if (skillSlugs.length === 0) return null;
2254
+ const controller = new AbortController();
2255
+ const timeout = setTimeout(() => controller.abort(), timeoutMs);
2256
+ try {
2257
+ const results = {};
2258
+ const promises = skillSlugs.map(async (skillName) => {
2259
+ const skillId = buildSkillId(sourceType, source, skillName, skillFiles);
2260
+ if (!skillId) return;
2261
+ try {
2262
+ const response = await fetch(AUDIT_URL.replace("{skill_id}", skillId.split("/").map(encodeURIComponent).join("/")), { signal: controller.signal });
2263
+ if (response.ok) {
2264
+ const data = await response.json();
2265
+ if (!data.error) {
2266
+ const auditData = data.data || data;
2267
+ results[skillName] = {
2268
+ risk_level: auditData.risk_level ?? "unknown",
2269
+ risk_score: auditData.risk_score ?? null,
2270
+ risk_signals: auditData.risk_signals ?? [],
2271
+ audited_at: auditData.audited_at ?? null
2272
+ };
2273
+ }
2274
+ }
2275
+ } catch {}
2276
+ });
2277
+ await Promise.all(promises);
2278
+ return Object.keys(results).length > 0 ? results : null;
2279
+ } catch {
2280
+ return null;
2281
+ } finally {
2282
+ clearTimeout(timeout);
2283
+ }
2284
+ }
2292
2285
  const pendingTelemetry = [];
2293
2286
  function track(data) {
2294
2287
  if (!isEnabled()) return;
@@ -2806,7 +2799,7 @@ const CURRENT_VERSION = 3;
2806
2799
  function getSkillLockPath() {
2807
2800
  const xdgStateHome = process.env.XDG_STATE_HOME;
2808
2801
  if (xdgStateHome) return join(xdgStateHome, "skills", LOCK_FILE);
2809
- return join(homedir(), AGENTS_DIR, LOCK_FILE);
2802
+ return join(homedir$1(), AGENTS_DIR, LOCK_FILE);
2810
2803
  }
2811
2804
  async function readSkillLock() {
2812
2805
  const lockPath = getSkillLockPath();
@@ -2823,7 +2816,7 @@ async function readSkillLock() {
2823
2816
  async function writeSkillLock(lock) {
2824
2817
  const lockPath = getSkillLockPath();
2825
2818
  await mkdir(dirname(lockPath), { recursive: true });
2826
- await writeFile(lockPath, JSON.stringify(lock, null, 2), "utf-8");
2819
+ await writeFile$1(lockPath, JSON.stringify(lock, null, 2), "utf-8");
2827
2820
  }
2828
2821
  let _ghWarningShown = false;
2829
2822
  function getGitHubToken() {
@@ -3160,65 +3153,126 @@ async function tryBlobInstall(ownerRepo, options = {}) {
3160
3153
  tree
3161
3154
  };
3162
3155
  }
3163
- var version$1 = "0.0.3";
3156
+ var version$1 = "0.0.4";
3164
3157
  const isCancelled$1 = (value) => typeof value === "symbol";
3165
- async function isSourcePrivate(source) {
3166
- const ownerRepo = parseOwnerRepo(source);
3167
- if (!ownerRepo) return false;
3168
- return isRepoPrivate(ownerRepo.owner, ownerRepo.repo);
3169
- }
3170
3158
  function getLockSource(parsedUrl, normalizedSource) {
3171
3159
  return parsedUrl.startsWith("git@") || parsedUrl.startsWith("ssh://") ? parsedUrl : normalizedSource;
3172
3160
  }
3173
3161
  function initTelemetry(version) {
3174
3162
  setVersion(version);
3175
3163
  }
3176
- function riskLabel(risk) {
3164
+ const execFileAsync = promisify(execFile);
3165
+ function normalizeRepoRef(input) {
3166
+ let s = input.trim().toLowerCase();
3167
+ s = s.replace(/^(https?:\/\/|git:\/\/|ssh:\/\/)/, "");
3168
+ s = s.replace(/^git@([^:]+):/, "$1/");
3169
+ s = s.replace(/\.git$/, "");
3170
+ s = s.replace(/\/+$/, "");
3171
+ return s;
3172
+ }
3173
+ function looksLikeRepoSource(source) {
3174
+ const s = source.trim();
3175
+ if (s.startsWith("http://") || s.startsWith("https://")) return true;
3176
+ if (s.startsWith("git@") || s.startsWith("ssh://") || s.startsWith("git://")) return true;
3177
+ return (s.match(/\//g) || []).length <= 1 && !s.includes(":") && !/\s/.test(s);
3178
+ }
3179
+ async function resolveSkillId$1(source, skillName) {
3180
+ if (!looksLikeRepoSource(source)) return source;
3181
+ if (!skillName || !skillName.trim()) throw new Error(`源 "${source}" 看起来是仓库地址,需要同时传入 --skill <skill_name> 才能定位具体技能。`);
3182
+ const searchUrl = `${SEARCH_URL}?${new URLSearchParams({
3183
+ q: skillName.trim(),
3184
+ limit: "20",
3185
+ mode: "text",
3186
+ scope: "summary"
3187
+ }).toString()}`;
3188
+ let res;
3189
+ try {
3190
+ res = await fetch(searchUrl);
3191
+ } catch {
3192
+ throw new Error(`无法连接搜索服务: ${SEARCH_URL}`);
3193
+ }
3194
+ if (!res.ok) throw new Error(`搜索服务返回 HTTP ${res.status}`);
3195
+ const payload = await res.json();
3196
+ const results = payload.data?.results ?? payload.results ?? [];
3197
+ if (results.length === 0) throw new Error(`未找到名称匹配 "${skillName}" 的技能。`);
3198
+ const wantedRef = normalizeRepoRef(source);
3199
+ const match = results.find((r) => {
3200
+ const ref = normalizeRepoRef(r.source_url);
3201
+ return ref === wantedRef || ref.startsWith(`${wantedRef}/`);
3202
+ }) ?? results[0];
3203
+ if (!match) throw new Error(`未找到名称匹配 "${skillName}" 的技能。`);
3204
+ return match.skill_id;
3205
+ }
3206
+ async function downloadAndExtractSkill(skillId) {
3207
+ const downloadUrl = DOWNLOAD_URL.replace("{skill_id}", encodeURIComponent(skillId));
3208
+ const response = await fetch(downloadUrl);
3209
+ if (!response.ok) throw new Error(`HTTP ${response.status} ${response.statusText}`);
3210
+ const tempDir = await mkdtemp(join$1(tmpdir(), "skills-"));
3211
+ const zipPath = join$1(tempDir, ".skill.zip");
3212
+ try {
3213
+ await writeFile(zipPath, Buffer.from(await response.arrayBuffer()));
3214
+ try {
3215
+ await execFileAsync("unzip", [
3216
+ "-q",
3217
+ zipPath,
3218
+ "-d",
3219
+ tempDir
3220
+ ]);
3221
+ } catch {
3222
+ throw new Error("Failed to extract skill archive. Ensure the 'unzip' command is available on PATH.");
3223
+ }
3224
+ } finally {
3225
+ await rm(zipPath, { force: true });
3226
+ }
3227
+ return tempDir;
3228
+ }
3229
+ function riskLabel$1(risk) {
3177
3230
  switch (risk) {
3178
- case "critical": return import_picocolors.default.red(import_picocolors.default.bold("Critical Risk"));
3179
- case "high": return import_picocolors.default.red("High Risk");
3180
- case "medium": return import_picocolors.default.yellow("Med Risk");
3181
- case "low": return import_picocolors.default.green("Low Risk");
3231
+ case "critical": return import_picocolors.default.red(import_picocolors.default.bold("Critical"));
3232
+ case "high": return import_picocolors.default.red("High");
3233
+ case "medium": return import_picocolors.default.yellow("Med");
3234
+ case "low": return import_picocolors.default.green("Low");
3182
3235
  case "safe": return import_picocolors.default.green("Safe");
3183
- default: return import_picocolors.default.dim("--");
3236
+ default: return import_picocolors.default.dim("Unknown");
3184
3237
  }
3185
3238
  }
3186
- function socketLabel(audit) {
3187
- if (!audit) return import_picocolors.default.dim("--");
3188
- const count = audit.alerts ?? 0;
3189
- return count > 0 ? import_picocolors.default.red(`${count} alert${count !== 1 ? "s" : ""}`) : import_picocolors.default.green("0 alerts");
3190
- }
3191
3239
  function padEnd(str, width) {
3192
3240
  const visible = stripTerminalEscapes(str);
3193
3241
  const pad = Math.max(0, width - visible.length);
3194
3242
  return str + " ".repeat(pad);
3195
3243
  }
3196
- function buildSecurityLines(auditData, skills, source) {
3244
+ function buildSecurityLines(auditData, skills) {
3197
3245
  if (!auditData) return [];
3198
- if (!skills.some((s) => {
3199
- const data = auditData[s.slug];
3200
- return data && Object.keys(data).length > 0;
3201
- })) return [];
3202
- const nameWidth = Math.min(Math.max(...skills.map((s) => s.displayName.length)), 36);
3246
+ const rows = skills.filter((s) => auditData[s.slug]);
3247
+ if (rows.length === 0) return [];
3248
+ const nameWidth = Math.min(Math.max(...rows.map((s) => s.displayName.length)), 36);
3203
3249
  const lines = [];
3204
- const header = padEnd("", nameWidth + 2) + padEnd(import_picocolors.default.dim("Gen"), 18) + padEnd(import_picocolors.default.dim("Socket"), 18) + import_picocolors.default.dim("Snyk");
3250
+ const header = padEnd("", nameWidth + 2) + padEnd(import_picocolors.default.dim("Risk"), 18) + padEnd(import_picocolors.default.dim("Score"), 8) + import_picocolors.default.dim("Signals");
3205
3251
  lines.push(header);
3206
- for (const skill of skills) {
3252
+ for (const skill of rows) {
3207
3253
  const data = auditData[skill.slug];
3208
3254
  const name = skill.displayName.length > nameWidth ? skill.displayName.slice(0, nameWidth - 1) + "…" : skill.displayName;
3209
- const ath = data?.ath ? riskLabel(data.ath.risk) : import_picocolors.default.dim("--");
3210
- const socket = data?.socket ? socketLabel(data.socket) : import_picocolors.default.dim("--");
3211
- const snyk = data?.snyk ? riskLabel(data.snyk.risk) : import_picocolors.default.dim("--");
3212
- lines.push(padEnd(import_picocolors.default.cyan(name), nameWidth + 2) + padEnd(ath, 18) + padEnd(socket, 18) + snyk);
3255
+ const risk = riskLabel$1(data.risk_level);
3256
+ const score = data.risk_score != null ? import_picocolors.default.dim(String(data.risk_score)) : import_picocolors.default.dim("--");
3257
+ const signals = data.risk_signals.length > 0 ? import_picocolors.default.yellow(String(data.risk_signals.length)) : import_picocolors.default.dim("0");
3258
+ lines.push(padEnd(import_picocolors.default.cyan(name), nameWidth + 2) + padEnd(risk, 18) + padEnd(score, 8) + signals);
3213
3259
  }
3214
- lines.push("");
3215
- lines.push(`${import_picocolors.default.dim("Details:")} ${import_picocolors.default.dim(`https://skills.sh/${source}`)}`);
3216
3260
  return lines;
3217
3261
  }
3262
+ function getHighRiskSkills(auditData, skills) {
3263
+ if (!auditData) return [];
3264
+ return skills.filter((s) => {
3265
+ const data = auditData[s.slug];
3266
+ return data && (data.risk_level === "high" || data.risk_level === "critical");
3267
+ }).map((s) => ({
3268
+ name: s.displayName,
3269
+ riskLevel: auditData[s.slug].risk_level
3270
+ }));
3271
+ }
3218
3272
  function shortenPath$2(fullPath, cwd) {
3219
3273
  const home = homedir();
3220
- if (fullPath === home || fullPath.startsWith(home + sep)) return "~" + fullPath.slice(home.length);
3221
- if (fullPath === cwd || fullPath.startsWith(cwd + sep)) return "." + fullPath.slice(cwd.length);
3274
+ if (fullPath === home || fullPath.startsWith(home + sep$1)) return "~" + fullPath.slice(home.length);
3275
+ if (fullPath === cwd || fullPath.startsWith(cwd + sep$1)) return "." + fullPath.slice(cwd.length);
3222
3276
  return fullPath;
3223
3277
  }
3224
3278
  function computeSingleFileSkillHash(contents) {
@@ -3233,6 +3287,13 @@ function formatList$1(items, maxShow = 5) {
3233
3287
  const remaining = items.length - maxShow;
3234
3288
  return `${shown.join(", ")} +${remaining} more`;
3235
3289
  }
3290
+ function buildSkillFiles(selectedSkills, tempDir, blobResult) {
3291
+ const skillFiles = {};
3292
+ for (const skill of selectedSkills) if (blobResult && "repoPath" in skill) skillFiles[skill.name] = skill.repoPath;
3293
+ else if (tempDir && skill.path === tempDir) skillFiles[skill.name] = "SKILL.md";
3294
+ else if (tempDir && skill.path.startsWith(tempDir + sep$1)) skillFiles[skill.name] = skill.path.slice(tempDir.length + 1).split(sep$1).join("/") + "/SKILL.md";
3295
+ return skillFiles;
3296
+ }
3236
3297
  function splitAgentsByType(agentTypes) {
3237
3298
  const universal = [];
3238
3299
  const symlinked = [];
@@ -3243,18 +3304,6 @@ function splitAgentsByType(agentTypes) {
3243
3304
  symlinked
3244
3305
  };
3245
3306
  }
3246
- function buildAgentSummaryLines(targetAgents, installMode) {
3247
- const lines = [];
3248
- const { universal, symlinked } = splitAgentsByType(targetAgents);
3249
- if (installMode === "symlink") {
3250
- if (universal.length > 0) lines.push(` ${import_picocolors.default.green("universal:")} ${formatList$1(universal)}`);
3251
- if (symlinked.length > 0) lines.push(` ${import_picocolors.default.dim("symlink →")} ${formatList$1(symlinked)}`);
3252
- } else {
3253
- const allNames = targetAgents.map((a) => agents[a].displayName);
3254
- lines.push(` ${import_picocolors.default.dim("copy →")} ${formatList$1(allNames)}`);
3255
- }
3256
- return lines;
3257
- }
3258
3307
  function targetDisplayName(target) {
3259
3308
  const base = agents[target.agent].displayName;
3260
3309
  return target.subagent ? `${base} (${target.subagent})` : base;
@@ -3382,419 +3431,95 @@ async function selectAgentsInteractive(options) {
3382
3431
  return selected;
3383
3432
  }
3384
3433
  setVersion(version$1);
3385
- async function handleWellKnownSkills(source, url, options, spinner) {
3386
- spinner.start("Discovering skills from well-known endpoint...");
3387
- const skills = await wellKnownProvider.fetchAllSkills(url);
3388
- if (skills.length === 0) {
3389
- spinner.stop(import_picocolors.default.red("No skills found"));
3390
- Se(import_picocolors.default.red("No skills found at this URL. Make sure the server has a /.well-known/agent-skills/index.json or /.well-known/skills/index.json file."));
3434
+ async function runAdd(args, options = {}) {
3435
+ const source = args[0];
3436
+ let installTipShown = false;
3437
+ const showInstallTip = () => {
3438
+ if (installTipShown) return;
3439
+ M.message(import_picocolors.default.dim("Tip: use the --yes (-y) and --global (-g) flags to install without prompts."));
3440
+ installTipShown = true;
3441
+ };
3442
+ if (!source) {
3443
+ console.log();
3444
+ console.log(import_picocolors.default.bgRed(import_picocolors.default.white(import_picocolors.default.bold(" ERROR "))) + " " + import_picocolors.default.red("Missing required argument: source"));
3445
+ console.log();
3446
+ console.log(import_picocolors.default.dim(" Usage:"));
3447
+ console.log(` ${import_picocolors.default.cyan("npx wittyhub add")} ${import_picocolors.default.yellow("<source>")} ${import_picocolors.default.dim("[options]")}`);
3448
+ console.log();
3449
+ console.log(import_picocolors.default.dim(" Example:"));
3450
+ console.log(` ${import_picocolors.default.cyan("npx wittyhub add")} ${import_picocolors.default.yellow("vercel-labs/agent-skills")}`);
3451
+ console.log();
3391
3452
  process.exit(1);
3392
3453
  }
3393
- spinner.stop(`Found ${import_picocolors.default.green(skills.length)} skill${skills.length > 1 ? "s" : ""}`);
3394
- for (const skill of skills) {
3395
- M.info(`Skill: ${import_picocolors.default.cyan(skill.installName)}`);
3396
- M.message(import_picocolors.default.dim(skill.description));
3397
- if (skill.files.size > 1) M.message(import_picocolors.default.dim(` Files: ${Array.from(skill.files.keys()).join(", ")}`));
3454
+ if (options.all) {
3455
+ options.skill = ["*"];
3456
+ options.agent = ["*"];
3457
+ options.yes = true;
3398
3458
  }
3399
- if (options.list) {
3400
- console.log();
3401
- M.step(import_picocolors.default.bold("Available Skills"));
3402
- for (const skill of skills) {
3403
- M.message(` ${import_picocolors.default.cyan(skill.installName)}`);
3404
- M.message(` ${import_picocolors.default.dim(skill.description)}`);
3405
- if (skill.files.size > 1) M.message(` ${import_picocolors.default.dim(`Files: ${skill.files.size}`)}`);
3459
+ const agentResult = await detectAgent();
3460
+ if (agentResult.isAgent) {
3461
+ options.yes = true;
3462
+ if (!options.agent || options.agent.length === 0) {
3463
+ const mappedAgent = getAgentType(agentResult.agent.name);
3464
+ if (mappedAgent) options.agent = ensureUniversalAgents([mappedAgent]);
3406
3465
  }
3407
- console.log();
3408
- Se("Run without --list to install");
3409
- process.exit(0);
3410
3466
  }
3411
- let selectedSkills;
3412
- if (options.skill?.includes("*")) {
3413
- selectedSkills = skills;
3414
- M.info(`Installing all ${skills.length} skills`);
3415
- } else if (options.skill && options.skill.length > 0) {
3416
- selectedSkills = skills.filter((s) => options.skill.some((name) => s.installName.toLowerCase() === name.toLowerCase() || s.name.toLowerCase() === name.toLowerCase()));
3417
- if (selectedSkills.length === 0) {
3418
- M.error(`No matching skills found for: ${options.skill.join(", ")}`);
3419
- M.info("Available skills:");
3420
- for (const s of skills) M.message(` - ${s.installName}`);
3467
+ console.log();
3468
+ if (!agentResult.isAgent) Ie(import_picocolors.default.bgCyan(import_picocolors.default.black(" skills ")));
3469
+ if (agentResult.isAgent) M.info(import_picocolors.default.bgCyan(import_picocolors.default.black(import_picocolors.default.bold(` ${agentResult.agent.name} `))) + " Agent detected — installing non-interactively");
3470
+ else if (!process.stdin.isTTY) showInstallTip();
3471
+ let tempDir = null;
3472
+ try {
3473
+ const spinner = Y();
3474
+ const skillNameForLookup = options.skill?.[0];
3475
+ let skillId;
3476
+ try {
3477
+ skillId = await resolveSkillId$1(source, skillNameForLookup);
3478
+ } catch (e) {
3479
+ const message = e instanceof Error ? e.message : String(e);
3480
+ Se(import_picocolors.default.red(message));
3421
3481
  process.exit(1);
3422
3482
  }
3423
- } else if (skills.length === 1) {
3424
- selectedSkills = skills;
3425
- const firstSkill = skills[0];
3426
- M.info(`Skill: ${import_picocolors.default.cyan(firstSkill.installName)}`);
3427
- } else if (options.yes) {
3428
- selectedSkills = skills;
3429
- M.info(`Installing all ${skills.length} skills`);
3430
- } else {
3431
- const selected = await multiselect({
3432
- message: "Select skills to install",
3433
- options: skills.map((s) => ({
3434
- value: s,
3435
- label: s.installName,
3436
- hint: s.description.length > 60 ? s.description.slice(0, 57) + "..." : s.description
3437
- })),
3438
- required: true
3439
- });
3440
- if (pD(selected)) {
3441
- xe("Installation cancelled");
3442
- process.exit(0);
3443
- }
3444
- selectedSkills = selected;
3445
- }
3446
- let targetAgents;
3447
- const validAgents = Object.keys(agents);
3448
- if (options.agent?.includes("*")) {
3449
- targetAgents = validAgents;
3450
- M.info(`Installing to all ${targetAgents.length} agents`);
3451
- } else if (options.agent && options.agent.length > 0) {
3452
- const invalidAgents = options.agent.filter((a) => !validAgents.includes(a));
3453
- if (invalidAgents.length > 0) {
3454
- M.error(`Invalid agents: ${invalidAgents.join(", ")}`);
3455
- M.info(`Valid agents: ${validAgents.join(", ")}`);
3483
+ spinner.start(`Downloading skill ${import_picocolors.default.cyan(skillId)} from API...`);
3484
+ try {
3485
+ tempDir = await downloadAndExtractSkill(skillId);
3486
+ } catch (e) {
3487
+ spinner.stop(import_picocolors.default.red("Download failed"));
3488
+ const message = e instanceof Error ? e.message : String(e);
3489
+ Se(import_picocolors.default.red(`Failed to download skill "${skillId}": ${message}`));
3456
3490
  process.exit(1);
3457
3491
  }
3458
- targetAgents = options.agent;
3459
- } else {
3460
- spinner.start("Loading agents...");
3461
- const installedAgents = await detectInstalledAgents();
3462
- const totalAgents = Object.keys(agents).length;
3463
- spinner.stop(`${totalAgents} agents`);
3464
- if (installedAgents.length === 0) if (options.yes) {
3465
- targetAgents = validAgents;
3466
- M.info("Installing to all agents");
3467
- } else {
3468
- M.info("Select agents to install skills to");
3469
- const selected = await promptForAgents("Which agents do you want to install to?", Object.entries(agents).map(([key, config]) => ({
3470
- value: key,
3471
- label: config.displayName
3472
- })));
3473
- if (pD(selected)) {
3474
- xe("Installation cancelled");
3475
- process.exit(0);
3476
- }
3477
- targetAgents = selected;
3478
- }
3479
- else if (installedAgents.length === 1 || options.yes) {
3480
- targetAgents = ensureUniversalAgents(installedAgents);
3481
- if (installedAgents.length === 1) {
3482
- const firstAgent = installedAgents[0];
3483
- M.info(`Installing to: ${import_picocolors.default.cyan(agents[firstAgent].displayName)}`);
3484
- } else M.info(`Installing to: ${installedAgents.map((a) => import_picocolors.default.cyan(agents[a].displayName)).join(", ")}`);
3485
- } else {
3486
- const selected = await selectAgentsInteractive({ global: options.global });
3487
- if (pD(selected)) {
3488
- xe("Installation cancelled");
3489
- process.exit(0);
3490
- }
3491
- targetAgents = selected;
3492
- }
3493
- }
3494
- let installGlobally = options.global ?? false;
3495
- const supportsGlobal = targetAgents.some((a) => agents[a].globalSkillsDir !== void 0);
3496
- if (options.global === void 0 && !options.yes && supportsGlobal) {
3497
- const scope = await ve({
3498
- message: "Installation scope",
3499
- options: [{
3500
- value: false,
3501
- label: "Project",
3502
- hint: "Install in current directory (committed with your project)"
3503
- }, {
3504
- value: true,
3505
- label: "Global",
3506
- hint: "Install in home directory (available across all projects)"
3507
- }]
3508
- });
3509
- if (pD(scope)) {
3510
- xe("Installation cancelled");
3511
- process.exit(0);
3492
+ spinner.stop(`Skill archive downloaded from ${import_picocolors.default.cyan("API")}`);
3493
+ const parsed = {
3494
+ type: "git",
3495
+ url: skillId
3496
+ };
3497
+ const repoPrivacyPromise = Promise.resolve(null);
3498
+ if (source.split("/")[0]?.toLowerCase() === "openclaw" && !options.dangerouslyAcceptOpenclawRisks) {
3499
+ console.log();
3500
+ M.warn(import_picocolors.default.yellow(import_picocolors.default.bold("⚠ OpenClaw skills are unverified community submissions.")));
3501
+ M.message(import_picocolors.default.yellow("This source contains user-submitted skills that have not been reviewed for safety or quality."));
3502
+ M.message(import_picocolors.default.yellow("Skills run with full agent permissions and could be malicious."));
3503
+ console.log();
3504
+ M.message(`If you understand the risks, re-run with:\n\n ${import_picocolors.default.cyan(`npx wittyhub add ${source} --dangerously-accept-openclaw-risks`)}\n`);
3505
+ Se(import_picocolors.default.red("Installation blocked"));
3506
+ process.exit(1);
3512
3507
  }
3513
- installGlobally = scope;
3514
- }
3515
- let installMode = options.copy ? "copy" : "symlink";
3516
- const uniqueDirs = new Set(targetAgents.map((a) => agents[a].skillsDir));
3517
- if (!options.copy && !options.yes && uniqueDirs.size > 1) {
3518
- const modeChoice = await ve({
3519
- message: "Installation method",
3520
- options: [{
3521
- value: "symlink",
3522
- label: "Symlink (Recommended)",
3523
- hint: "Single source of truth, easy updates"
3524
- }, {
3525
- value: "copy",
3526
- label: "Copy to all agents",
3527
- hint: "Independent copies for each agent"
3528
- }]
3508
+ const includeInternal = !!(options.skill && options.skill.length > 0);
3509
+ let skills;
3510
+ let blobResult = null;
3511
+ spinner.start("Discovering skills...");
3512
+ skills = await discoverSkills(tempDir, void 0, {
3513
+ includeInternal,
3514
+ fullDepth: options.fullDepth
3529
3515
  });
3530
- if (pD(modeChoice)) {
3531
- xe("Installation cancelled");
3532
- process.exit(0);
3516
+ if (skills.length === 0) {
3517
+ spinner.stop(import_picocolors.default.red("No skills found"));
3518
+ Se(import_picocolors.default.red("No valid skills found. Skills require a SKILL.md with name and description."));
3519
+ await cleanup(tempDir);
3520
+ process.exit(1);
3533
3521
  }
3534
- installMode = modeChoice;
3535
- } else if (uniqueDirs.size <= 1) installMode = "copy";
3536
- const cwd = process.cwd();
3537
- const summaryLines = [];
3538
- targetAgents.map((a) => agents[a].displayName);
3539
- const overwriteChecks = await Promise.all(selectedSkills.flatMap((skill) => targetAgents.map(async (agent) => ({
3540
- skillName: skill.installName,
3541
- agent,
3542
- installed: await isSkillInstalled(skill.installName, agent, { global: installGlobally })
3543
- }))));
3544
- const overwriteStatus = /* @__PURE__ */ new Map();
3545
- for (const { skillName, agent, installed } of overwriteChecks) {
3546
- if (!overwriteStatus.has(skillName)) overwriteStatus.set(skillName, /* @__PURE__ */ new Map());
3547
- overwriteStatus.get(skillName).set(agent, installed);
3548
- }
3549
- for (const skill of selectedSkills) {
3550
- if (summaryLines.length > 0) summaryLines.push("");
3551
- const shortCanonical = shortenPath$2(getCanonicalPath(skill.installName, { global: installGlobally }), cwd);
3552
- summaryLines.push(`${import_picocolors.default.cyan(shortCanonical)}`);
3553
- summaryLines.push(...buildAgentSummaryLines(targetAgents, installMode));
3554
- if (skill.files.size > 1) summaryLines.push(` ${import_picocolors.default.dim("files:")} ${skill.files.size}`);
3555
- const skillOverwrites = overwriteStatus.get(skill.installName);
3556
- const overwriteAgents = targetAgents.filter((a) => skillOverwrites?.get(a)).map((a) => agents[a].displayName);
3557
- if (overwriteAgents.length > 0) summaryLines.push(` ${import_picocolors.default.yellow("overwrites:")} ${formatList$1(overwriteAgents)}`);
3558
- }
3559
- console.log();
3560
- Me(summaryLines.join("\n"), "Installation Summary");
3561
- if (!options.yes) {
3562
- const confirmed = await ye({ message: "Proceed with installation?" });
3563
- if (pD(confirmed) || !confirmed) {
3564
- xe("Installation cancelled");
3565
- process.exit(0);
3566
- }
3567
- }
3568
- const sourceIdentifier = wellKnownProvider.getSourceIdentifier(url);
3569
- const wellKnownPrivacyPromise = isSourcePrivate(sourceIdentifier).catch(() => null);
3570
- spinner.start("Installing skills...");
3571
- const results = [];
3572
- for (const skill of selectedSkills) for (const agent of targetAgents) {
3573
- const result = await installWellKnownSkillForAgent(skill, agent, {
3574
- global: installGlobally,
3575
- mode: installMode
3576
- });
3577
- results.push({
3578
- skill: skill.installName,
3579
- agent: agents[agent].displayName,
3580
- ...result
3581
- });
3582
- }
3583
- spinner.stop("Installation complete");
3584
- console.log();
3585
- const successful = results.filter((r) => r.success);
3586
- const failed = results.filter((r) => !r.success);
3587
- const skillFiles = {};
3588
- for (const skill of selectedSkills) skillFiles[skill.installName] = skill.sourceUrl;
3589
- if (await wellKnownPrivacyPromise !== true) track({
3590
- event: "install",
3591
- source: sourceIdentifier,
3592
- skills: selectedSkills.map((s) => s.installName).join(","),
3593
- agents: targetAgents.join(","),
3594
- ...installGlobally && { global: "1" },
3595
- skillFiles: JSON.stringify(skillFiles),
3596
- sourceType: "well-known"
3597
- });
3598
- if (successful.length > 0 && installGlobally) {
3599
- const successfulSkillNames = new Set(successful.map((r) => r.skill));
3600
- for (const skill of selectedSkills) if (successfulSkillNames.has(skill.installName)) try {
3601
- await addSkillToLock(skill.installName, {
3602
- source: sourceIdentifier,
3603
- sourceType: "well-known",
3604
- sourceUrl: skill.sourceUrl,
3605
- skillFolderHash: ""
3606
- });
3607
- } catch {}
3608
- }
3609
- if (successful.length > 0 && !installGlobally) {
3610
- const successfulSkillNames = new Set(successful.map((r) => r.skill));
3611
- for (const skill of selectedSkills) if (successfulSkillNames.has(skill.installName)) try {
3612
- const matchingResult = successful.find((r) => r.skill === skill.installName);
3613
- const installDir = matchingResult?.canonicalPath || matchingResult?.path;
3614
- if (installDir) {
3615
- const computedHash = await computeSkillFolderHash(installDir);
3616
- await addSkillToLocalLock(skill.installName, {
3617
- source: sourceIdentifier,
3618
- sourceType: "well-known",
3619
- computedHash
3620
- }, cwd);
3621
- }
3622
- } catch {}
3623
- }
3624
- if (successful.length > 0) {
3625
- const bySkill = /* @__PURE__ */ new Map();
3626
- for (const r of successful) {
3627
- const skillResults = bySkill.get(r.skill) || [];
3628
- skillResults.push(r);
3629
- bySkill.set(r.skill, skillResults);
3630
- }
3631
- const skillCount = bySkill.size;
3632
- const symlinkFailures = successful.filter((r) => r.mode === "symlink" && r.symlinkFailed);
3633
- const copiedAgents = symlinkFailures.map((r) => r.agent);
3634
- const resultLines = [];
3635
- for (const [skillName, skillResults] of bySkill) {
3636
- const firstResult = skillResults[0];
3637
- if (firstResult.mode === "copy") {
3638
- resultLines.push(`${import_picocolors.default.green("✓")} ${skillName} ${import_picocolors.default.dim("(copied)")}`);
3639
- resultLines.push(...buildCopyResultLines(skillResults, cwd));
3640
- } else {
3641
- if (firstResult.canonicalPath) {
3642
- const shortPath = shortenPath$2(firstResult.canonicalPath, cwd);
3643
- resultLines.push(`${import_picocolors.default.green("✓")} ${shortPath}`);
3644
- } else resultLines.push(`${import_picocolors.default.green("✓")} ${skillName}`);
3645
- resultLines.push(...buildResultLines(skillResults, targetAgents));
3646
- }
3647
- }
3648
- const title = import_picocolors.default.green(`Installed ${skillCount} skill${skillCount !== 1 ? "s" : ""}`);
3649
- Me(resultLines.join("\n"), title);
3650
- if (symlinkFailures.length > 0) {
3651
- M.warn(import_picocolors.default.yellow(`Symlinks failed for: ${formatList$1(copiedAgents)}`));
3652
- M.message(import_picocolors.default.dim(" Files were copied instead. On Windows, enable Developer Mode for symlink support."));
3653
- }
3654
- }
3655
- if (failed.length > 0) {
3656
- console.log();
3657
- M.error(import_picocolors.default.red(`Failed to install ${failed.length}`));
3658
- for (const r of failed) M.message(` ${import_picocolors.default.red("✗")} ${r.skill} → ${r.agent}: ${import_picocolors.default.dim(r.error)}`);
3659
- }
3660
- console.log();
3661
- Se(import_picocolors.default.green("Done!") + import_picocolors.default.dim(" Review skills before use; they run with full agent permissions."));
3662
- await promptForFindSkills(options, targetAgents);
3663
- }
3664
- async function runAdd(args, options = {}) {
3665
- const source = args[0];
3666
- let installTipShown = false;
3667
- const showInstallTip = () => {
3668
- if (installTipShown) return;
3669
- M.message(import_picocolors.default.dim("Tip: use the --yes (-y) and --global (-g) flags to install without prompts."));
3670
- installTipShown = true;
3671
- };
3672
- if (!source) {
3673
- console.log();
3674
- console.log(import_picocolors.default.bgRed(import_picocolors.default.white(import_picocolors.default.bold(" ERROR "))) + " " + import_picocolors.default.red("Missing required argument: source"));
3675
- console.log();
3676
- console.log(import_picocolors.default.dim(" Usage:"));
3677
- console.log(` ${import_picocolors.default.cyan("npx wittyhub add")} ${import_picocolors.default.yellow("<source>")} ${import_picocolors.default.dim("[options]")}`);
3678
- console.log();
3679
- console.log(import_picocolors.default.dim(" Example:"));
3680
- console.log(` ${import_picocolors.default.cyan("npx wittyhub add")} ${import_picocolors.default.yellow("vercel-labs/agent-skills")}`);
3681
- console.log();
3682
- process.exit(1);
3683
- }
3684
- if (options.all) {
3685
- options.skill = ["*"];
3686
- options.agent = ["*"];
3687
- options.yes = true;
3688
- }
3689
- const agentResult = await detectAgent();
3690
- if (agentResult.isAgent) {
3691
- options.yes = true;
3692
- if (!options.agent || options.agent.length === 0) {
3693
- const mappedAgent = getAgentType(agentResult.agent.name);
3694
- if (mappedAgent) options.agent = ensureUniversalAgents([mappedAgent]);
3695
- }
3696
- }
3697
- console.log();
3698
- if (!agentResult.isAgent) Ie(import_picocolors.default.bgCyan(import_picocolors.default.black(" skills ")));
3699
- if (agentResult.isAgent) M.info(import_picocolors.default.bgCyan(import_picocolors.default.black(import_picocolors.default.bold(` ${agentResult.agent.name} `))) + " Agent detected — installing non-interactively");
3700
- else if (!process.stdin.isTTY) showInstallTip();
3701
- let tempDir = null;
3702
- try {
3703
- const spinner = Y();
3704
- spinner.start("Parsing source...");
3705
- const parsed = parseSource(source);
3706
- spinner.stop(`Source: ${parsed.type === "local" ? parsed.localPath : parsed.url}${parsed.ref ? ` @ ${import_picocolors.default.yellow(parsed.ref)}` : ""}${parsed.subpath ? ` (${parsed.subpath})` : ""}${parsed.skillFilter ? ` ${import_picocolors.default.dim("@")}${import_picocolors.default.cyan(parsed.skillFilter)}` : ""}`);
3707
- const ownerRepoRaw = getOwnerRepo(parsed);
3708
- const repoPrivacyPromise = (() => {
3709
- if (!ownerRepoRaw || parsed.type !== "github") return Promise.resolve(null);
3710
- const ownerRepo = parseOwnerRepo(ownerRepoRaw);
3711
- if (!ownerRepo) return Promise.resolve(null);
3712
- return isRepoPrivate(ownerRepo.owner, ownerRepo.repo).catch(() => null);
3713
- })();
3714
- if (ownerRepoRaw?.split("/")[0]?.toLowerCase() === "openclaw" && !options.dangerouslyAcceptOpenclawRisks) {
3715
- console.log();
3716
- M.warn(import_picocolors.default.yellow(import_picocolors.default.bold("⚠ OpenClaw skills are unverified community submissions.")));
3717
- M.message(import_picocolors.default.yellow("This source contains user-submitted skills that have not been reviewed for safety or quality."));
3718
- M.message(import_picocolors.default.yellow("Skills run with full agent permissions and could be malicious."));
3719
- console.log();
3720
- M.message(`If you understand the risks, re-run with:\n\n ${import_picocolors.default.cyan(`npx wittyhub add ${source} --dangerously-accept-openclaw-risks`)}\n`);
3721
- Se(import_picocolors.default.red("Installation blocked"));
3722
- process.exit(1);
3723
- }
3724
- if (parsed.type === "well-known") {
3725
- await handleWellKnownSkills(source, parsed.url, options, spinner);
3726
- return;
3727
- }
3728
- if (parsed.skillFilter) {
3729
- options.skill = options.skill || [];
3730
- if (!options.skill.includes(parsed.skillFilter)) options.skill.push(parsed.skillFilter);
3731
- }
3732
- const includeInternal = !!(options.skill && options.skill.length > 0);
3733
- let skills;
3734
- let blobResult = null;
3735
- if (parsed.type === "local") {
3736
- spinner.start("Validating local path...");
3737
- if (!existsSync(parsed.localPath)) {
3738
- spinner.stop(import_picocolors.default.red("Path not found"));
3739
- Se(import_picocolors.default.red(`Local path does not exist: ${parsed.localPath}`));
3740
- process.exit(1);
3741
- }
3742
- spinner.stop("Local path validated");
3743
- spinner.start("Discovering skills...");
3744
- skills = await discoverSkills(parsed.localPath, parsed.subpath, {
3745
- includeInternal,
3746
- fullDepth: options.fullDepth
3747
- });
3748
- } else if (parsed.type === "github" && !options.fullDepth) {
3749
- const BLOB_ALLOWED_OWNERS = [
3750
- "vercel",
3751
- "vercel-labs",
3752
- "heygen-com"
3753
- ];
3754
- const ownerRepo = getOwnerRepo(parsed);
3755
- const owner = ownerRepo?.split("/")[0]?.toLowerCase();
3756
- const isSelfHostedRepo = !!ownerRepo && Object.hasOwn(BLOB_ALLOWED_REPOS, ownerRepo.toLowerCase());
3757
- if (ownerRepo && owner && (isSelfHostedRepo || BLOB_ALLOWED_OWNERS.includes(owner))) {
3758
- spinner.start("Fetching skills...");
3759
- blobResult = await tryBlobInstall(ownerRepo, {
3760
- subpath: parsed.subpath,
3761
- skillFilter: parsed.skillFilter,
3762
- ref: parsed.ref,
3763
- getToken: getGitHubToken,
3764
- includeInternal
3765
- });
3766
- if (!blobResult) spinner.stop(import_picocolors.default.dim("Falling back to clone..."));
3767
- }
3768
- if (blobResult) {
3769
- skills = blobResult.skills;
3770
- spinner.stop(`Found ${import_picocolors.default.green(skills.length)} skill${skills.length > 1 ? "s" : ""}`);
3771
- } else {
3772
- spinner.start("Cloning repository...");
3773
- tempDir = await cloneRepo(parsed.url, parsed.ref);
3774
- spinner.stop("Repository cloned");
3775
- spinner.start("Discovering skills...");
3776
- skills = await discoverSkills(tempDir, parsed.subpath, {
3777
- includeInternal,
3778
- fullDepth: options.fullDepth
3779
- });
3780
- }
3781
- } else {
3782
- spinner.start("Cloning repository...");
3783
- tempDir = await cloneRepo(parsed.url, parsed.ref);
3784
- spinner.stop("Repository cloned");
3785
- spinner.start("Discovering skills...");
3786
- skills = await discoverSkills(tempDir, parsed.subpath, {
3787
- includeInternal,
3788
- fullDepth: options.fullDepth
3789
- });
3790
- }
3791
- if (skills.length === 0) {
3792
- spinner.stop(import_picocolors.default.red("No skills found"));
3793
- Se(import_picocolors.default.red("No valid skills found. Skills require a SKILL.md with name and description."));
3794
- await cleanup(tempDir);
3795
- process.exit(1);
3796
- }
3797
- if (!blobResult) spinner.stop(`Found ${import_picocolors.default.green(skills.length)} skill${skills.length > 1 ? "s" : ""}`);
3522
+ spinner.stop(`Found ${import_picocolors.default.green(skills.length)} skill${skills.length > 1 ? "s" : ""}`);
3798
3523
  if (options.list) {
3799
3524
  console.log();
3800
3525
  M.step(import_picocolors.default.bold("Available Skills"));
@@ -3892,7 +3617,8 @@ async function runAdd(args, options = {}) {
3892
3617
  selectedSkills = selected;
3893
3618
  }
3894
3619
  const ownerRepoForAudit = getOwnerRepo(parsed);
3895
- const auditPromise = Promise.resolve(null);
3620
+ const auditSkillFiles = buildSkillFiles(selectedSkills, tempDir, blobResult);
3621
+ const auditPromise = ownerRepoForAudit ? fetchAuditData(ownerRepoForAudit, selectedSkills.map((s) => s.name), parsed.type, auditSkillFiles) : Promise.resolve(null);
3896
3622
  let targetAgents;
3897
3623
  const validAgents = Object.keys(agents);
3898
3624
  if (options.agent?.includes("*")) {
@@ -4093,18 +3819,28 @@ async function runAdd(args, options = {}) {
4093
3819
  }
4094
3820
  console.log();
4095
3821
  Me(summaryLines.join("\n"), "Installation Summary");
3822
+ const auditSkills = selectedSkills.map((s) => ({
3823
+ slug: getSkillDisplayName(s),
3824
+ displayName: getSkillDisplayName(s)
3825
+ }));
3826
+ let auditData = null;
4096
3827
  try {
4097
- const auditData = await auditPromise;
3828
+ auditData = await auditPromise;
4098
3829
  if (auditData && ownerRepoForAudit) {
4099
- const securityLines = buildSecurityLines(auditData, selectedSkills.map((s) => ({
4100
- slug: getSkillDisplayName(s),
4101
- displayName: getSkillDisplayName(s)
4102
- })), ownerRepoForAudit);
3830
+ const securityLines = buildSecurityLines(auditData, auditSkills);
4103
3831
  if (securityLines.length > 0) Me(securityLines.join("\n"), "Security Risk Assessments");
4104
3832
  }
4105
3833
  } catch {}
3834
+ const highRiskSkills = getHighRiskSkills(auditData, auditSkills);
3835
+ if (highRiskSkills.length > 0) {
3836
+ const list = highRiskSkills.map((s) => `${s.name} 经安全审计标记为 ${s.riskLevel} 风险`).join(",");
3837
+ M.warn(`${import_picocolors.default.red(import_picocolors.default.bold("⚠ 高风险警告:"))} ${list},请谨慎安装。`);
3838
+ }
4106
3839
  if (!options.yes) {
4107
- const confirmed = await ye({ message: "Proceed with installation?" });
3840
+ const confirmed = await ye({
3841
+ message: highRiskSkills.length > 0 ? `检测到以下技能存在安全风险:${highRiskSkills.map((s) => `${s.name} (${s.riskLevel})`).join(", ")}。仍要继续安装?` : "Proceed with installation?",
3842
+ initialValue: highRiskSkills.length === 0
3843
+ });
4108
3844
  if (pD(confirmed) || !confirmed) {
4109
3845
  xe("Installation cancelled");
4110
3846
  await cleanup(tempDir);
@@ -4116,17 +3852,7 @@ async function runAdd(args, options = {}) {
4116
3852
  for (const skill of selectedSkills) for (const target of installTargets) {
4117
3853
  const { agent, subagent } = target;
4118
3854
  let result;
4119
- if (blobResult && "files" in skill) {
4120
- const blobSkill = skill;
4121
- result = await installBlobSkillForAgent({
4122
- installName: blobSkill.name,
4123
- files: blobSkill.files
4124
- }, agent, {
4125
- global: installGlobally,
4126
- mode: installMode,
4127
- eveSubagent: subagent
4128
- });
4129
- } else if (tempDir && skill.path === tempDir && skill.rawContent) result = await installBlobSkillForAgent({
3855
+ if (tempDir && skill.path === tempDir && skill.rawContent) result = await installBlobSkillForAgent({
4130
3856
  installName: skill.name,
4131
3857
  files: [{
4132
3858
  path: "SKILL.md",
@@ -4152,16 +3878,37 @@ async function runAdd(args, options = {}) {
4152
3878
  console.log();
4153
3879
  const successful = results.filter((r) => r.success);
4154
3880
  const failed = results.filter((r) => !r.success);
4155
- const skillFiles = {};
4156
- for (const skill of selectedSkills) if (blobResult && "repoPath" in skill) skillFiles[skill.name] = skill.repoPath;
4157
- else if (tempDir && skill.path === tempDir) skillFiles[skill.name] = "SKILL.md";
4158
- else if (tempDir && skill.path.startsWith(tempDir + sep)) skillFiles[skill.name] = skill.path.slice(tempDir.length + 1).split(sep).join("/") + "/SKILL.md";
4159
- else continue;
3881
+ const skillFiles = buildSkillFiles(selectedSkills, tempDir, blobResult);
4160
3882
  const normalizedSource = getOwnerRepo(parsed);
4161
3883
  const lockSource = getLockSource(parsed.url, normalizedSource);
4162
3884
  if (normalizedSource) if (parseOwnerRepo(normalizedSource)) {
4163
3885
  const isPrivate = await repoPrivacyPromise;
4164
- if (isPrivate === false || isPrivate === null && parsed.type !== "github") track({
3886
+ if (isPrivate === false || isPrivate === null && parsed.type !== "github") {
3887
+ console.log("[telemetry-debug-1]", JSON.stringify({
3888
+ isPrivate,
3889
+ parsedType: parsed.type,
3890
+ normalizedSource,
3891
+ sourceType: parsed.type,
3892
+ skillFiles: JSON.stringify(skillFiles)
3893
+ }));
3894
+ track({
3895
+ event: "install",
3896
+ source: normalizedSource,
3897
+ skills: selectedSkills.map((s) => s.name).join(","),
3898
+ agents: targetAgents.join(","),
3899
+ ...installGlobally && { global: "1" },
3900
+ skillFiles: JSON.stringify(skillFiles),
3901
+ sourceType: parsed.type
3902
+ });
3903
+ }
3904
+ } else {
3905
+ console.log("[telemetry-debug-2]", JSON.stringify({
3906
+ parsedType: parsed.type,
3907
+ normalizedSource,
3908
+ sourceType: parsed.type,
3909
+ skillFiles: JSON.stringify(skillFiles)
3910
+ }));
3911
+ track({
4165
3912
  event: "install",
4166
3913
  source: normalizedSource,
4167
3914
  skills: selectedSkills.map((s) => s.name).join(","),
@@ -4170,32 +3917,21 @@ async function runAdd(args, options = {}) {
4170
3917
  skillFiles: JSON.stringify(skillFiles),
4171
3918
  sourceType: parsed.type
4172
3919
  });
4173
- } else track({
4174
- event: "install",
4175
- source: normalizedSource,
4176
- skills: selectedSkills.map((s) => s.name).join(","),
4177
- agents: targetAgents.join(","),
4178
- ...installGlobally && { global: "1" },
4179
- skillFiles: JSON.stringify(skillFiles),
4180
- sourceType: parsed.type
4181
- });
3920
+ }
4182
3921
  if (successful.length > 0 && installGlobally && normalizedSource) {
4183
3922
  const successfulSkillNames = new Set(successful.map((r) => r.skill));
4184
3923
  let cachedTree;
4185
- if (parsed.type === "github" && !blobResult) cachedTree = await fetchRepoTree(normalizedSource, parsed.ref, getGitHubToken);
3924
+ if (parsed.type === "github" && true) cachedTree = await fetchRepoTree(normalizedSource, parsed.ref, getGitHubToken);
4186
3925
  for (const skill of selectedSkills) {
4187
3926
  const skillDisplayName = getSkillDisplayName(skill);
4188
3927
  if (successfulSkillNames.has(skillDisplayName)) try {
4189
3928
  let skillFolderHash = "";
4190
3929
  const skillPathValue = skillFiles[skill.name];
4191
- if (blobResult && skillPathValue) {
4192
- const hash = getSkillFolderHashFromTree(blobResult.tree, skillPathValue);
4193
- if (hash) skillFolderHash = hash;
4194
- } else if (parsed.type === "github" && skillPathValue && cachedTree) {
3930
+ if (parsed.type === "github" && skillPathValue && cachedTree) {
4195
3931
  const hash = getSkillFolderHashFromTree(cachedTree, skillPathValue);
4196
3932
  if (hash) skillFolderHash = hash;
4197
3933
  } else if (skillPathValue && tempDir) {
4198
- const hash = await computeSkillFolderHash(join(tempDir, dirname(skillPathValue)));
3934
+ const hash = await computeSkillFolderHash(join$1(tempDir, dirname$1(skillPathValue)));
4199
3935
  if (hash) skillFolderHash = hash;
4200
3936
  }
4201
3937
  await addSkillToLock(skill.name, {
@@ -4217,7 +3953,7 @@ async function runAdd(args, options = {}) {
4217
3953
  for (const skill of selectedSkills) {
4218
3954
  const skillDisplayName = getSkillDisplayName(skill);
4219
3955
  if (successfulSkillNames.has(skillDisplayName)) try {
4220
- const computedHash = blobResult && "snapshotHash" in skill ? skill.snapshotHash : tempDir && skill.path === tempDir && skill.rawContent ? computeSingleFileSkillHash(skill.rawContent) : await computeSkillFolderHash(skill.path);
3956
+ const computedHash = tempDir && skill.path === tempDir && skill.rawContent ? computeSingleFileSkillHash(skill.rawContent) : await computeSkillFolderHash(skill.path);
4221
3957
  const skillPathValue = skillFiles[skill.name];
4222
3958
  await addSkillToLocalLock(skill.name, {
4223
3959
  source: lockSource || parsed.url,
@@ -4398,18 +4134,300 @@ function parseAddOptions(args) {
4398
4134
  options
4399
4135
  };
4400
4136
  }
4137
+ const SOURCE_TYPE_PREFIXES = [
4138
+ "github",
4139
+ "gitcode",
4140
+ "gitlab",
4141
+ "gitee"
4142
+ ];
4143
+ function normalizeSkillId(input) {
4144
+ const trimmed = input.trim();
4145
+ const first = trimmed.split("/")[0];
4146
+ if (first && SOURCE_TYPE_PREFIXES.includes(first.toLowerCase())) return trimmed;
4147
+ return `github/${trimmed}`;
4148
+ }
4149
+ function buildSkillIdFromSource(source, skillName) {
4150
+ const parsed = parseSource(source);
4151
+ if (!SOURCE_TYPE_PREFIXES.includes(parsed.type)) return null;
4152
+ const ownerRepo = getOwnerRepo(parsed);
4153
+ if (!ownerRepo) return null;
4154
+ const slugifiedOwnerRepo = ownerRepo.split("/").map(slugifyTelemetryValue).join("/");
4155
+ const skillPath = slugifyTelemetryValue(skillName);
4156
+ if (!skillPath) return null;
4157
+ return `${parsed.type}/${slugifiedOwnerRepo}/${skillPath}`;
4158
+ }
4159
+ async function listSkillsByRepo(source) {
4160
+ const parsed = parseSource(source);
4161
+ if (!SOURCE_TYPE_PREFIXES.includes(parsed.type)) return [];
4162
+ const ownerRepo = getOwnerRepo(parsed);
4163
+ if (!ownerRepo) return [];
4164
+ const slugifiedOwnerRepo = ownerRepo.split("/").map(slugifyTelemetryValue).join("/");
4165
+ if (!slugifiedOwnerRepo || slugifiedOwnerRepo.split("/").length < 2) return [];
4166
+ const url = `${LIST_URL}?${new URLSearchParams({
4167
+ source_type: parsed.type,
4168
+ repo: slugifiedOwnerRepo,
4169
+ limit: "100"
4170
+ }).toString()}`;
4171
+ try {
4172
+ const res = await fetch(url);
4173
+ if (!res.ok) return [];
4174
+ const raw = await res.json();
4175
+ return ((raw.data || raw).skills ?? []).filter((skill) => skill && typeof skill.skill_id === "string" && skill.skill_id).map((skill) => ({
4176
+ skill_id: skill.skill_id,
4177
+ name: skill.name ?? "",
4178
+ source_url: skill.source_url ?? ""
4179
+ }));
4180
+ } catch {
4181
+ return [];
4182
+ }
4183
+ }
4184
+ async function resolveBySearch(source, skillName) {
4185
+ let results = [];
4186
+ try {
4187
+ const params = new URLSearchParams({
4188
+ q: skillName,
4189
+ limit: "10",
4190
+ mode: "text"
4191
+ });
4192
+ const res = await fetch(`${SEARCH_URL}?${params.toString()}`);
4193
+ if (res.ok) {
4194
+ const raw = await res.json();
4195
+ results = (raw.data || raw).results ?? [];
4196
+ }
4197
+ } catch {
4198
+ return null;
4199
+ }
4200
+ const sourceLower = source.trim().toLowerCase();
4201
+ const matches = results.filter((skill) => {
4202
+ const url = (skill.source_url || "").toLowerCase();
4203
+ if (!url) return false;
4204
+ if (url.startsWith(sourceLower)) return true;
4205
+ if (!sourceLower.includes("://") && sourceLower.split("/").length === 2) return url.includes(`/${sourceLower}/`);
4206
+ return false;
4207
+ });
4208
+ const exact = matches.find((skill) => skill.name.toLowerCase() === skillName.trim().toLowerCase());
4209
+ return exact ? exact.skill_id : matches.length > 0 ? matches[0].skill_id : null;
4210
+ }
4211
+ async function resolveSkillIdBySourceAndName(source, skillName) {
4212
+ const repoSkills = await listSkillsByRepo(source);
4213
+ if (repoSkills.length > 0) {
4214
+ const nameLower = skillName.trim().toLowerCase();
4215
+ const exact = repoSkills.find((skill) => skill.name.toLowerCase() === nameLower);
4216
+ if (exact) return exact.skill_id;
4217
+ const slug = slugifyTelemetryValue(skillName);
4218
+ if (slug) {
4219
+ const bySlug = repoSkills.find((skill) => {
4220
+ return (skill.skill_id.split("/").pop() ?? "").toLowerCase() === slug;
4221
+ });
4222
+ if (bySlug) return bySlug.skill_id;
4223
+ }
4224
+ return null;
4225
+ }
4226
+ return resolveBySearch(source, skillName);
4227
+ }
4228
+ async function resolveSkillId(source, skillName) {
4229
+ if (!skillName) return {
4230
+ skillId: normalizeSkillId(source),
4231
+ derived: false
4232
+ };
4233
+ const resolved = await resolveSkillIdBySourceAndName(source, skillName);
4234
+ if (resolved) return {
4235
+ skillId: resolved,
4236
+ derived: false
4237
+ };
4238
+ const derived = buildSkillIdFromSource(source, skillName);
4239
+ if (derived) return {
4240
+ skillId: derived,
4241
+ derived: true
4242
+ };
4243
+ return {
4244
+ skillId: null,
4245
+ derived: false
4246
+ };
4247
+ }
4248
+ function parseAuditOptions(args) {
4249
+ let source = "";
4250
+ let skill = "";
4251
+ const errors = [];
4252
+ for (let i = 0; i < args.length; i++) {
4253
+ const arg = args[i];
4254
+ if (arg === "-s" || arg === "--skill") {
4255
+ i++;
4256
+ skill = args[i]?.trim() ?? "";
4257
+ } else if (arg && !arg.startsWith("-")) source = arg.trim();
4258
+ }
4259
+ if (!source) errors.push("Missing source or skill id");
4260
+ return {
4261
+ source,
4262
+ skill,
4263
+ errors
4264
+ };
4265
+ }
4266
+ async function fetchSkillAudit(skillId, timeoutMs = 15e3) {
4267
+ const controller = new AbortController();
4268
+ const timeout = setTimeout(() => controller.abort(), timeoutMs);
4269
+ try {
4270
+ const url = AUDIT_URL.replace("{skill_id}", skillId.split("/").map(encodeURIComponent).join("/"));
4271
+ let response;
4272
+ try {
4273
+ response = await fetch(url, { signal: controller.signal });
4274
+ } catch {
4275
+ return {
4276
+ status: "error",
4277
+ message: `无法连接审计服务: ${AUDIT_URL}`
4278
+ };
4279
+ }
4280
+ if (response.status === 404) return { status: "not_found" };
4281
+ if (!response.ok) return {
4282
+ status: "error",
4283
+ message: `审计服务返回 HTTP ${response.status}`
4284
+ };
4285
+ let data;
4286
+ try {
4287
+ data = await response.json();
4288
+ } catch {
4289
+ return {
4290
+ status: "error",
4291
+ message: "审计服务响应格式无效"
4292
+ };
4293
+ }
4294
+ if (data.error) return {
4295
+ status: "no_audit",
4296
+ message: String(data.error)
4297
+ };
4298
+ const auditData = data.data || data;
4299
+ return {
4300
+ status: "ok",
4301
+ data: {
4302
+ risk_level: auditData.risk_level ?? "unknown",
4303
+ risk_score: auditData.risk_score ?? null,
4304
+ risk_signals: auditData.risk_signals ?? [],
4305
+ audited_at: auditData.audited_at ?? null,
4306
+ audit_type: auditData.audit_type ?? null,
4307
+ details: auditData.details ?? null
4308
+ }
4309
+ };
4310
+ } finally {
4311
+ clearTimeout(timeout);
4312
+ }
4313
+ }
4314
+ function riskColor(level) {
4315
+ switch (level) {
4316
+ case "critical":
4317
+ case "high": return import_picocolors.default.red;
4318
+ case "medium": return import_picocolors.default.yellow;
4319
+ case "low":
4320
+ case "safe": return import_picocolors.default.green;
4321
+ default: return import_picocolors.default.dim;
4322
+ }
4323
+ }
4324
+ const RISK_LABELS = {
4325
+ critical: "Critical",
4326
+ high: "High",
4327
+ medium: "Medium",
4328
+ low: "Low",
4329
+ safe: "Safe"
4330
+ };
4331
+ function riskLabel(level) {
4332
+ const color = riskColor(level);
4333
+ const label = RISK_LABELS[level] || level || "Unknown";
4334
+ return level === "critical" ? color(import_picocolors.default.bold(label)) : color(label);
4335
+ }
4336
+ function severityColor(severity) {
4337
+ const s = severity.toLowerCase();
4338
+ if (s === "critical" || s === "high") return import_picocolors.default.red(severity);
4339
+ if (s === "medium") return import_picocolors.default.yellow(severity);
4340
+ if (s === "low") return import_picocolors.default.green(severity);
4341
+ return import_picocolors.default.dim(severity);
4342
+ }
4343
+ function buildAuditOutput(skillId, data) {
4344
+ const lines = [];
4345
+ lines.push(`${import_picocolors.default.bold("Skill:")} ${import_picocolors.default.cyan(skillId)}`);
4346
+ const color = riskColor(data.risk_level);
4347
+ const score = data.risk_score != null ? color(`(score ${data.risk_score})`) : color("(score --)");
4348
+ lines.push(`${import_picocolors.default.bold("Risk:")} ${riskLabel(data.risk_level)} ${score}`);
4349
+ if (data.audited_at) lines.push(`${import_picocolors.default.bold("Audited at:")} ${import_picocolors.default.dim(data.audited_at)}`);
4350
+ lines.push("");
4351
+ if (data.risk_signals.length === 0) {
4352
+ lines.push(import_picocolors.default.green("No risk signals detected."));
4353
+ return lines;
4354
+ }
4355
+ lines.push(import_picocolors.default.bold("Risk signals:"));
4356
+ for (const signal of data.risk_signals) {
4357
+ const severity = severityColor(signal.severity || "unknown");
4358
+ lines.push(` ${import_picocolors.default.bold(signal.name)} ${import_picocolors.default.dim(`[${severity}]`)}`);
4359
+ if (signal.description) lines.push(` ${import_picocolors.default.dim(signal.description)}`);
4360
+ }
4361
+ return lines;
4362
+ }
4363
+ async function runAudit(args) {
4364
+ const { source, skill, errors } = parseAuditOptions(args);
4365
+ if (errors.length > 0) {
4366
+ for (const error of errors) console.error(import_picocolors.default.red(error));
4367
+ console.error("Usage: wittyhub audit <source> --skill <skill>");
4368
+ console.error(" wittyhub audit <skill_id>");
4369
+ return;
4370
+ }
4371
+ const { skillId, derived } = await resolveSkillId(source, skill || void 0);
4372
+ if (!skillId) {
4373
+ console.error(import_picocolors.default.red(`Unable to resolve skill id from source: ${source}`));
4374
+ console.error("Usage: wittyhub audit <source> --skill <skill>");
4375
+ return;
4376
+ }
4377
+ if (derived) console.error(import_picocolors.default.yellow(`Could not locate skill in repo; trying derived id: ${skillId}`));
4378
+ const result = await fetchSkillAudit(skillId);
4379
+ track({
4380
+ event: "audit",
4381
+ skillId,
4382
+ found: result.status === "ok" ? "1" : "0"
4383
+ });
4384
+ switch (result.status) {
4385
+ case "ok":
4386
+ for (const line of buildAuditOutput(skillId, result.data)) console.log(line);
4387
+ break;
4388
+ case "no_audit":
4389
+ console.log(`${import_picocolors.default.yellow("No audit result yet.")} ${import_picocolors.default.dim(result.message)}`);
4390
+ break;
4391
+ case "not_found":
4392
+ console.error(import_picocolors.default.red(`Skill not found: ${skillId}`));
4393
+ break;
4394
+ case "error":
4395
+ console.error(import_picocolors.default.red(result.message));
4396
+ break;
4397
+ }
4398
+ }
4401
4399
  const RESET$3 = "\x1B[0m";
4402
4400
  const BOLD$3 = "\x1B[1m";
4403
4401
  const DIM$3 = "\x1B[38;5;102m";
4404
4402
  const TEXT$2 = "\x1B[38;5;145m";
4405
4403
  const CYAN$1 = "\x1B[36m";
4406
- const SEARCH_API_BASE = process.env.SKILLS_API_URL || "https://skills.sh";
4407
- function formatInstalls(count) {
4408
- if (!count || count <= 0) return "";
4404
+ const MAGENTA = "\x1B[35m";
4405
+ const YELLOW$1 = "\x1B[33m";
4406
+ const RED = "\x1B[31m";
4407
+ const GREEN = "\x1B[32m";
4408
+ function formatInstalls$1(count) {
4409
+ if (count === void 0 || count === null || Number.isNaN(count)) return "";
4410
+ if (count <= 0) return "0 installs";
4409
4411
  if (count >= 1e6) return `${(count / 1e6).toFixed(1).replace(/\.0$/, "")}M installs`;
4410
4412
  if (count >= 1e3) return `${(count / 1e3).toFixed(1).replace(/\.0$/, "")}K installs`;
4411
4413
  return `${count} install${count === 1 ? "" : "s"}`;
4412
4414
  }
4415
+ function formatRiskLevel$1(riskScore) {
4416
+ if (riskScore === void 0 || riskScore === null) return "";
4417
+ if (riskScore <= 20) return `${GREEN}safe risk${RESET$3}`;
4418
+ if (riskScore <= 50) return `${GREEN}low risk${RESET$3}`;
4419
+ if (riskScore <= 80) return `${YELLOW$1}medium risk${RESET$3}`;
4420
+ return `${RED}high risk${RESET$3}`;
4421
+ }
4422
+ function escapeRegExp(s) {
4423
+ return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
4424
+ }
4425
+ function highlightMatches(text, query) {
4426
+ if (!text || !query) return text;
4427
+ const terms = query.trim().split(/\s+/).filter(Boolean).map(escapeRegExp);
4428
+ if (terms.length === 0) return text;
4429
+ return text.replace(new RegExp(`(${terms.join("|")})`, "gi"), `${MAGENTA}$1${RESET$3}`);
4430
+ }
4413
4431
  const GITHUB_OWNER_PATTERN = /^[a-z0-9](?:[a-z0-9-]{0,38})$/i;
4414
4432
  function parseFindOptions(args) {
4415
4433
  const queryParts = [];
@@ -4422,7 +4440,7 @@ function parseFindOptions(args) {
4422
4440
  if (arg === "--owner") {
4423
4441
  const value = args[i + 1];
4424
4442
  if (!value || value.startsWith("-")) {
4425
- errors.push("--owner requires a GitHub owner");
4443
+ errors.push("--owner requires an owner");
4426
4444
  continue;
4427
4445
  }
4428
4446
  ownerValue = value;
@@ -4430,7 +4448,7 @@ function parseFindOptions(args) {
4430
4448
  } else if (arg.startsWith("--owner=")) {
4431
4449
  ownerValue = arg.slice(8);
4432
4450
  if (!ownerValue) {
4433
- errors.push("--owner requires a GitHub owner");
4451
+ errors.push("--owner requires an owner");
4434
4452
  continue;
4435
4453
  }
4436
4454
  } else {
@@ -4439,7 +4457,7 @@ function parseFindOptions(args) {
4439
4457
  }
4440
4458
  const owner = ownerValue.trim().toLowerCase();
4441
4459
  if (!GITHUB_OWNER_PATTERN.test(owner)) {
4442
- errors.push("--owner must be a valid GitHub owner");
4460
+ errors.push("--owner must be a valid owner");
4443
4461
  continue;
4444
4462
  }
4445
4463
  options.owner = owner;
@@ -4451,24 +4469,52 @@ function parseFindOptions(args) {
4451
4469
  };
4452
4470
  }
4453
4471
  async function searchSkillsAPI(query, owner) {
4472
+ const url = `${SEARCH_URL}?${new URLSearchParams({
4473
+ q: query,
4474
+ limit: "10",
4475
+ mode: "text"
4476
+ }).toString()}`;
4477
+ let res;
4454
4478
  try {
4455
- const params = new URLSearchParams({
4456
- q: query,
4457
- limit: "10"
4458
- });
4459
- if (owner) params.set("owner", owner);
4460
- const url = `${SEARCH_API_BASE}/api/search?${params.toString()}`;
4461
- const res = await fetch(url);
4462
- if (!res.ok) return [];
4463
- return (await res.json()).skills.map((skill) => ({
4464
- name: sanitizeMetadata(skill.name),
4465
- slug: sanitizeMetadata(skill.id),
4466
- source: sanitizeMetadata(skill.source || ""),
4467
- installs: skill.installs
4468
- })).sort((a, b) => (b.installs || 0) - (a.installs || 0));
4479
+ res = await fetch(url);
4469
4480
  } catch {
4470
- return [];
4481
+ return {
4482
+ skills: [],
4483
+ error: `无法连接搜索服务: ${SEARCH_URL}`
4484
+ };
4485
+ }
4486
+ if (!res.ok) return {
4487
+ skills: [],
4488
+ error: `搜索服务返回 HTTP ${res.status}`
4489
+ };
4490
+ let data;
4491
+ try {
4492
+ data = await res.json();
4493
+ } catch {
4494
+ return {
4495
+ skills: [],
4496
+ error: "搜索服务响应格式无效"
4497
+ };
4498
+ }
4499
+ let skills = (data.data?.results ?? data.results ?? []).map((skill) => ({
4500
+ name: sanitizeMetadata(skill.name),
4501
+ slug: sanitizeMetadata(skill.skill_id),
4502
+ source: sanitizeMetadata(skill.source || ""),
4503
+ installs: skill.download_count || 0,
4504
+ description: skill.description ? sanitizeMetadata(skill.description) : void 0,
4505
+ riskScore: skill.risk_score ?? void 0,
4506
+ sourceUrl: skill.source_url ? sanitizeMetadata(skill.source_url) : void 0
4507
+ }));
4508
+ if (owner) {
4509
+ const ownerLower = owner.toLowerCase();
4510
+ skills = skills.filter((skill) => {
4511
+ if (skill.sourceUrl) try {
4512
+ if (new URL(skill.sourceUrl).pathname.split("/").filter(Boolean)[0]?.toLowerCase() === ownerLower) return true;
4513
+ } catch {}
4514
+ return skill.slug.toLowerCase().includes(`/${ownerLower}/`);
4515
+ });
4471
4516
  }
4517
+ return { skills: skills.sort((a, b) => (b.installs || 0) - (a.installs || 0)) };
4472
4518
  }
4473
4519
  const HIDE_CURSOR = "\x1B[?25l";
4474
4520
  const SHOW_CURSOR = "\x1B[?25h";
@@ -4480,6 +4526,7 @@ async function runSearchPrompt(initialQuery = "", owner) {
4480
4526
  let selectedIndex = 0;
4481
4527
  let query = initialQuery;
4482
4528
  let loading = false;
4529
+ let searchError = null;
4483
4530
  let debounceTimer = null;
4484
4531
  let lastRenderedLines = 0;
4485
4532
  if (process.stdin.isTTY) process.stdin.setRawMode(true);
@@ -4495,6 +4542,7 @@ async function runSearchPrompt(initialQuery = "", owner) {
4495
4542
  lines.push("");
4496
4543
  if (!query || query.length < 2) lines.push(`${DIM$3}Start typing to search (min 2 chars)${RESET$3}`);
4497
4544
  else if (results.length === 0 && loading) lines.push(`${DIM$3}Searching...${RESET$3}`);
4545
+ else if (results.length === 0 && searchError) lines.push(`${RED}${searchError}${RESET$3}`);
4498
4546
  else if (results.length === 0) lines.push(`${DIM$3}No skills found${RESET$3}`);
4499
4547
  else {
4500
4548
  const visible = results.slice(0, 8);
@@ -4502,12 +4550,23 @@ async function runSearchPrompt(initialQuery = "", owner) {
4502
4550
  const skill = visible[i];
4503
4551
  const isSelected = i === selectedIndex;
4504
4552
  const arrow = isSelected ? `${BOLD$3}>${RESET$3}` : " ";
4505
- const name = isSelected ? `${BOLD$3}${skill.name}${RESET$3}` : `${TEXT$2}${skill.name}${RESET$3}`;
4553
+ const hlName = highlightMatches(skill.name, query);
4554
+ const name = isSelected ? `${BOLD$3}${hlName}${RESET$3}` : `${TEXT$2}${hlName}${RESET$3}`;
4506
4555
  const source = skill.source ? ` ${DIM$3}${skill.source}${RESET$3}` : "";
4507
- const installs = formatInstalls(skill.installs);
4556
+ const installs = formatInstalls$1(skill.installs);
4508
4557
  const installsBadge = installs ? ` ${CYAN$1}${installs}${RESET$3}` : "";
4558
+ const risk = formatRiskLevel$1(skill.riskScore);
4559
+ const riskBadge = risk ? ` ${risk}` : "";
4509
4560
  const loadingIndicator = loading && i === 0 ? ` ${DIM$3}...${RESET$3}` : "";
4510
- lines.push(` ${arrow} ${name}${source}${installsBadge}${loadingIndicator}`);
4561
+ lines.push(` ${arrow} ${name}${source}${loadingIndicator}`);
4562
+ if (installsBadge || riskBadge) {
4563
+ const meta = [installsBadge.trimStart(), riskBadge.trimStart()].filter(Boolean).join(" ");
4564
+ lines.push(` ${meta}`);
4565
+ }
4566
+ if (skill.description) {
4567
+ const descPlain = skill.description.length > 80 ? `${skill.description.slice(0, 77)}...` : skill.description;
4568
+ lines.push(` ${DIM$3}${highlightMatches(descPlain, query)}${RESET$3}`);
4569
+ }
4511
4570
  }
4512
4571
  }
4513
4572
  lines.push("");
@@ -4523,6 +4582,7 @@ async function runSearchPrompt(initialQuery = "", owner) {
4523
4582
  loading = false;
4524
4583
  if (!q || q.length < 2) {
4525
4584
  results = [];
4585
+ searchError = null;
4526
4586
  selectedIndex = 0;
4527
4587
  render();
4528
4588
  return;
@@ -4532,10 +4592,13 @@ async function runSearchPrompt(initialQuery = "", owner) {
4532
4592
  const debounceMs = Math.max(150, 350 - q.length * 50);
4533
4593
  debounceTimer = setTimeout(async () => {
4534
4594
  try {
4535
- results = await searchSkillsAPI(q, owner);
4595
+ const result = await searchSkillsAPI(q, owner);
4596
+ results = result.skills;
4597
+ searchError = result.error ?? null;
4536
4598
  selectedIndex = 0;
4537
4599
  } catch {
4538
4600
  results = [];
4601
+ searchError = null;
4539
4602
  } finally {
4540
4603
  loading = false;
4541
4604
  debounceTimer = null;
@@ -4594,10 +4657,26 @@ async function runSearchPrompt(initialQuery = "", owner) {
4594
4657
  }
4595
4658
  function getOwnerRepoFromString(pkg) {
4596
4659
  const atIndex = pkg.lastIndexOf("@");
4597
- const match = (atIndex > 0 ? pkg.slice(0, atIndex) : pkg).match(/^([^/]+)\/([^/]+)$/);
4598
- if (match) return {
4599
- owner: match[1],
4600
- repo: match[2]
4660
+ const repoPath = atIndex > 0 ? pkg.slice(0, atIndex) : pkg;
4661
+ const urlMatch = repoPath.match(/^https?:\/\/[^/]+\/([^/]+)\/([^/]+?)(?:\.git)?\/?$/);
4662
+ if (urlMatch) return {
4663
+ owner: urlMatch[1],
4664
+ repo: urlMatch[2]
4665
+ };
4666
+ const blobMatch = repoPath.match(/^https?:\/\/[^/]+\/([^/]+)\/([^/]+?)\/blob\//);
4667
+ if (blobMatch) return {
4668
+ owner: blobMatch[1],
4669
+ repo: blobMatch[2]
4670
+ };
4671
+ const shorthandMatch = repoPath.match(/^([^/]+)\/([^/]+)$/);
4672
+ if (shorthandMatch) return {
4673
+ owner: shorthandMatch[1],
4674
+ repo: shorthandMatch[2]
4675
+ };
4676
+ const slugMatch = repoPath.match(/^[^/]+\/([^/]+)\/([^/]+)/);
4677
+ if (slugMatch) return {
4678
+ owner: slugMatch[1],
4679
+ repo: slugMatch[2]
4601
4680
  };
4602
4681
  return null;
4603
4682
  }
@@ -4617,24 +4696,41 @@ ${DIM$3} 2) npx wittyhub add <owner/repo@skill>${RESET$3}`;
4617
4696
  return;
4618
4697
  }
4619
4698
  if (query) {
4620
- const results = await searchSkillsAPI(query, owner);
4699
+ const { skills: results, error: searchError } = await searchSkillsAPI(query, owner);
4621
4700
  track({
4622
4701
  event: "find",
4623
4702
  query,
4624
4703
  resultCount: String(results.length)
4625
4704
  });
4705
+ if (searchError) {
4706
+ console.error(`${RED}${searchError}${RESET$3}`);
4707
+ return;
4708
+ }
4626
4709
  if (results.length === 0) {
4627
4710
  const ownerSuffix = owner ? ` from owner "${owner}"` : "";
4628
4711
  console.log(`${DIM$3}No skills found for "${query}"${ownerSuffix}${RESET$3}`);
4629
4712
  return;
4630
4713
  }
4631
- console.log(`${DIM$3}Install with${RESET$3} npx wittyhub add <owner/repo@skill>`);
4714
+ console.log(`${DIM$3}Install with${RESET$3} npx wittyhub add <repo-url> --skill <skill>`);
4632
4715
  console.log();
4633
4716
  for (const skill of results.slice(0, 6)) {
4634
- const pkg = skill.source || skill.slug;
4635
- const installs = formatInstalls(skill.installs);
4636
- console.log(`${TEXT$2}${pkg}@${skill.name}${RESET$3}${installs ? ` ${CYAN$1}${installs}${RESET$3}` : ""}`);
4637
- console.log(`${DIM$3}└ https://skills.sh/${skill.slug}${RESET$3}`);
4717
+ const raw = skill.sourceUrl || skill.source || skill.slug;
4718
+ const hlName = highlightMatches(skill.name, query);
4719
+ const installTarget = skill.sourceUrl ? `${skill.sourceUrl} --skill ${hlName}` : `${raw}@${hlName}`;
4720
+ const installs = formatInstalls$1(skill.installs);
4721
+ const installsBadge = installs ? ` ${CYAN$1}${installs}${RESET$3}` : "";
4722
+ const risk = formatRiskLevel$1(skill.riskScore);
4723
+ const riskBadge = risk ? ` ${risk}` : "";
4724
+ console.log(`${TEXT$2}${installTarget}${RESET$3}`);
4725
+ if (installsBadge || riskBadge) {
4726
+ const meta = [installsBadge.trimStart(), riskBadge.trimStart()].filter(Boolean).join(" ");
4727
+ console.log(` ${meta}`);
4728
+ }
4729
+ if (skill.description) {
4730
+ const descPlain = skill.description.length > 100 ? `${skill.description.slice(0, 97)}...` : skill.description;
4731
+ console.log(`${DIM$3} ${highlightMatches(descPlain, query)}${RESET$3}`);
4732
+ }
4733
+ console.log(`${DIM$3}└ ${skill.sourceUrl || `https://skills.sh/${skill.slug}`}${RESET$3}`);
4638
4734
  console.log();
4639
4735
  }
4640
4736
  return;
@@ -4657,7 +4753,7 @@ ${DIM$3} 2) npx wittyhub add <owner/repo@skill>${RESET$3}`;
4657
4753
  console.log();
4658
4754
  return;
4659
4755
  }
4660
- const pkg = selected.source || selected.slug;
4756
+ const pkg = selected.sourceUrl || selected.source || selected.slug;
4661
4757
  const skillName = selected.name;
4662
4758
  console.log();
4663
4759
  console.log(`${TEXT$2}Installing ${BOLD$3}${skillName}${RESET$3} from ${DIM$3}${pkg}${RESET$3}...`);
@@ -4674,9 +4770,156 @@ ${DIM$3} 2) npx wittyhub add <owner/repo@skill>${RESET$3}`;
4674
4770
  else console.log(`${DIM$3}Discover more skills at${RESET$3} ${TEXT$2}https://skills.sh${RESET$3}`);
4675
4771
  console.log();
4676
4772
  }
4773
+ function parseGetOptions(args) {
4774
+ let source = "";
4775
+ let skill = "";
4776
+ const errors = [];
4777
+ for (let i = 0; i < args.length; i++) {
4778
+ const arg = args[i];
4779
+ if (arg === "-s" || arg === "--skill") {
4780
+ i++;
4781
+ skill = args[i]?.trim() ?? "";
4782
+ } else if (arg && !arg.startsWith("-")) source = arg.trim();
4783
+ }
4784
+ if (!source) errors.push("Missing source or skill id");
4785
+ return {
4786
+ source,
4787
+ skill,
4788
+ errors
4789
+ };
4790
+ }
4791
+ async function fetchSkillDetail(skillId, timeoutMs = 15e3) {
4792
+ const controller = new AbortController();
4793
+ const timeout = setTimeout(() => controller.abort(), timeoutMs);
4794
+ try {
4795
+ const url = GET_URL.replace("{skill_id}", skillId.split("/").map(encodeURIComponent).join("/"));
4796
+ let response;
4797
+ try {
4798
+ response = await fetch(url, { signal: controller.signal });
4799
+ } catch {
4800
+ return {
4801
+ status: "error",
4802
+ message: `无法连接详情服务: ${GET_URL}`
4803
+ };
4804
+ }
4805
+ if (response.status === 404) return { status: "not_found" };
4806
+ if (!response.ok) return {
4807
+ status: "error",
4808
+ message: `详情服务返回 HTTP ${response.status}`
4809
+ };
4810
+ let data;
4811
+ try {
4812
+ data = await response.json();
4813
+ } catch {
4814
+ return {
4815
+ status: "error",
4816
+ message: "详情服务响应格式无效"
4817
+ };
4818
+ }
4819
+ if (data.error) return {
4820
+ status: "error",
4821
+ message: String(data.error)
4822
+ };
4823
+ const skillData = data.data || data;
4824
+ return {
4825
+ status: "ok",
4826
+ data: {
4827
+ skill_id: skillData.skill_id ?? skillId,
4828
+ name: skillData.name ?? "",
4829
+ description: skillData.description ?? null,
4830
+ version: skillData.version ?? null,
4831
+ commit_id: skillData.commit_id ?? null,
4832
+ author: skillData.author ?? null,
4833
+ source: skillData.source ?? "",
4834
+ source_url: skillData.source_url ?? "",
4835
+ repo_url: skillData.repo_url ?? null,
4836
+ category: skillData.category ?? null,
4837
+ tags: Array.isArray(skillData.tags) ? skillData.tags : null,
4838
+ platform: skillData.platform ?? null,
4839
+ metadata: skillData.metadata ?? null,
4840
+ risk_score: skillData.risk_score ?? null,
4841
+ download_count: skillData.download_count ?? 0,
4842
+ period_downloads: skillData.period_downloads ?? null,
4843
+ rating: skillData.rating ?? null,
4844
+ created_at: skillData.created_at ?? null,
4845
+ updated_at: skillData.updated_at ?? null,
4846
+ last_indexed_at: skillData.last_indexed_at ?? null
4847
+ }
4848
+ };
4849
+ } finally {
4850
+ clearTimeout(timeout);
4851
+ }
4852
+ }
4853
+ function formatInstalls(count) {
4854
+ if (count === void 0 || count === null || Number.isNaN(count)) return "0 installs";
4855
+ if (count <= 0) return "0 installs";
4856
+ if (count >= 1e6) return `${(count / 1e6).toFixed(1).replace(/\.0$/, "")}M installs`;
4857
+ if (count >= 1e3) return `${(count / 1e3).toFixed(1).replace(/\.0$/, "")}K installs`;
4858
+ return `${count} install${count === 1 ? "" : "s"}`;
4859
+ }
4860
+ function formatRiskLevel(riskScore) {
4861
+ if (riskScore === void 0 || riskScore === null) return import_picocolors.default.dim("unknown risk");
4862
+ if (riskScore <= 20) return import_picocolors.default.green("safe risk");
4863
+ if (riskScore <= 50) return import_picocolors.default.green("low risk");
4864
+ if (riskScore <= 80) return import_picocolors.default.yellow("medium risk");
4865
+ return import_picocolors.default.red("high risk");
4866
+ }
4867
+ function buildGetOutput(data) {
4868
+ const lines = [];
4869
+ lines.push(`${import_picocolors.default.bold("Skill:")} ${import_picocolors.default.cyan(data.skill_id)}`);
4870
+ if (data.name) lines.push(`${import_picocolors.default.bold("Name:")} ${data.name}`);
4871
+ if (data.description) lines.push(`${import_picocolors.default.bold("Description:")} ${data.description}`);
4872
+ if (data.author) lines.push(`${import_picocolors.default.bold("Author:")} ${data.author}`);
4873
+ if (data.category) lines.push(`${import_picocolors.default.bold("Category:")} ${data.category}`);
4874
+ if (data.version) lines.push(`${import_picocolors.default.bold("Version:")} ${data.version}`);
4875
+ if (data.tags && data.tags.length > 0) lines.push(`${import_picocolors.default.bold("Tags:")} ${data.tags.join(", ")}`);
4876
+ if (data.platform) lines.push(`${import_picocolors.default.bold("Platform:")} ${data.platform}`);
4877
+ if (data.source) lines.push(`${import_picocolors.default.bold("Source:")} ${data.source}`);
4878
+ if (data.source_url) lines.push(`${import_picocolors.default.bold("Source URL:")} ${import_picocolors.default.dim(data.source_url)}`);
4879
+ if (data.repo_url) lines.push(`${import_picocolors.default.bold("Repo URL:")} ${import_picocolors.default.dim(data.repo_url)}`);
4880
+ lines.push(`${import_picocolors.default.bold("Installs:")} ${import_picocolors.default.cyan(formatInstalls(data.download_count))}`);
4881
+ const riskLabel = formatRiskLevel(data.risk_score);
4882
+ const score = data.risk_score === void 0 || data.risk_score === null ? "" : import_picocolors.default.dim(` (score ${data.risk_score})`);
4883
+ lines.push(`${import_picocolors.default.bold("Risk:")} ${riskLabel}${score}`);
4884
+ if (data.updated_at) lines.push(`${import_picocolors.default.bold("Updated at:")} ${import_picocolors.default.dim(data.updated_at)}`);
4885
+ return lines;
4886
+ }
4887
+ async function runGet(args) {
4888
+ const { source, skill, errors } = parseGetOptions(args);
4889
+ if (errors.length > 0) {
4890
+ for (const error of errors) console.error(import_picocolors.default.red(error));
4891
+ console.error("Usage: wittyhub get <source> --skill <skill>");
4892
+ console.error(" wittyhub get <skill_id>");
4893
+ return;
4894
+ }
4895
+ const { skillId, derived } = await resolveSkillId(source, skill || void 0);
4896
+ if (!skillId) {
4897
+ console.error(import_picocolors.default.red(`Unable to resolve skill id from source: ${source}`));
4898
+ console.error("Usage: wittyhub get <source> --skill <skill>");
4899
+ return;
4900
+ }
4901
+ if (derived) console.error(import_picocolors.default.yellow(`Could not locate skill in repo; trying derived id: ${skillId}`));
4902
+ const result = await fetchSkillDetail(skillId);
4903
+ track({
4904
+ event: "get",
4905
+ skillId,
4906
+ found: result.status === "ok" ? "1" : "0"
4907
+ });
4908
+ switch (result.status) {
4909
+ case "ok":
4910
+ for (const line of buildGetOutput(result.data)) console.log(line);
4911
+ break;
4912
+ case "not_found":
4913
+ console.error(import_picocolors.default.red(`Skill not found: ${skillId}`));
4914
+ break;
4915
+ case "error":
4916
+ console.error(import_picocolors.default.red(result.message));
4917
+ break;
4918
+ }
4919
+ }
4677
4920
  const isCancelled = (value) => typeof value === "symbol";
4678
4921
  function shortenPath$1(fullPath, cwd) {
4679
- const home = homedir();
4922
+ const home = homedir$1();
4680
4923
  if (fullPath === home || fullPath.startsWith(home + sep)) return "~" + fullPath.slice(home.length);
4681
4924
  if (fullPath === cwd || fullPath.startsWith(cwd + sep)) return "." + fullPath.slice(cwd.length);
4682
4925
  return fullPath;
@@ -5031,7 +5274,7 @@ const DIM$2 = "\x1B[38;5;102m";
5031
5274
  const CYAN = "\x1B[36m";
5032
5275
  const YELLOW = "\x1B[33m";
5033
5276
  function shortenPath(fullPath, cwd) {
5034
- const home = homedir();
5277
+ const home = homedir$1();
5035
5278
  if (fullPath.startsWith(home)) return fullPath.replace(home, "~");
5036
5279
  if (fullPath.startsWith(cwd)) return "." + fullPath.slice(cwd.length);
5037
5280
  return fullPath;
@@ -5268,7 +5511,7 @@ async function removeCommand(skillNames, options) {
5268
5511
  for (const pathToCleanup of pathsToCleanup) {
5269
5512
  if (pathToCleanup === canonicalPath) continue;
5270
5513
  try {
5271
- if (await lstat(pathToCleanup).catch(() => null)) await rm(pathToCleanup, {
5514
+ if (await lstat(pathToCleanup).catch(() => null)) await rm$1(pathToCleanup, {
5272
5515
  recursive: true,
5273
5516
  force: true
5274
5517
  });
@@ -5286,7 +5529,7 @@ async function removeCommand(skillNames, options) {
5286
5529
  isStillUsed = true;
5287
5530
  break;
5288
5531
  }
5289
- if (!isStillUsed) await rm(canonicalPath, {
5532
+ if (!isStillUsed) await rm$1(canonicalPath, {
5290
5533
  recursive: true,
5291
5534
  force: true
5292
5535
  });
@@ -5934,7 +6177,7 @@ function buildUsePrompt(input) {
5934
6177
  return sections.join("\n\n") + "\n";
5935
6178
  }
5936
6179
  async function materializeUseSkill(skill) {
5937
- const tempRoot = await mkdtemp(join(tmpdir(), "skills-use-"));
6180
+ const tempRoot = await mkdtemp$1(join(tmpdir$1(), "skills-use-"));
5938
6181
  const skillDir = join(tempRoot, sanitizeName(skill.directoryName || skill.name));
5939
6182
  if (!isPathSafe(tempRoot, skillDir)) throw new Error("Invalid skill name: potential path traversal detected");
5940
6183
  await mkdir(skillDir, { recursive: true });
@@ -6161,8 +6404,8 @@ async function writeSafeFile(targetDir, filePath, contents) {
6161
6404
  const fullPath = join(targetDir, filePath);
6162
6405
  if (!isPathSafe(targetDir, fullPath)) return;
6163
6406
  await mkdir(dirname(fullPath), { recursive: true });
6164
- if (typeof contents === "string") await writeFile(fullPath, contents, "utf-8");
6165
- else await writeFile(fullPath, contents);
6407
+ if (typeof contents === "string") await writeFile$1(fullPath, contents, "utf-8");
6408
+ else await writeFile$1(fullPath, contents);
6166
6409
  }
6167
6410
  async function copySkillDirectory(src, dest) {
6168
6411
  await mkdir(dest, { recursive: true });
@@ -6222,6 +6465,308 @@ function fail(message) {
6222
6465
  process.exit(1);
6223
6466
  }
6224
6467
  var UseCommandError = class extends Error {};
6468
+ const CANONICAL_CATEGORIES = new Set([
6469
+ "Research and Design",
6470
+ "Development and Build",
6471
+ "Engineering and Compilation",
6472
+ "Quality and Validation",
6473
+ "Release and Deployment",
6474
+ "Monitoring and Operations",
6475
+ "Performance Optimization",
6476
+ "Security Hardening",
6477
+ "others"
6478
+ ]);
6479
+ const NAME_PATTERN = /^[a-z][a-z0-9-]*$/;
6480
+ const SEMVER_PATTERN = /^\d+\.\d+\.\d+(?:-[\w.]+)?(?:\+[\w.]+)?$/;
6481
+ function parseValidateOptions(args) {
6482
+ let path = "";
6483
+ const options = {};
6484
+ const errors = [];
6485
+ for (let i = 0; i < args.length; i++) {
6486
+ const arg = args[i];
6487
+ if (arg === "--json") options.json = true;
6488
+ else if (arg === "-h" || arg === "--help") {} else if (arg && !arg.startsWith("-")) if (!path) path = arg.trim();
6489
+ else errors.push(`Unexpected argument: ${arg}`);
6490
+ else if (arg) errors.push(`Unknown option: ${arg}`);
6491
+ }
6492
+ if (!path) errors.push("Missing required argument: <path>");
6493
+ return {
6494
+ path,
6495
+ options,
6496
+ errors
6497
+ };
6498
+ }
6499
+ async function runValidate(args) {
6500
+ const { path, options, errors } = parseValidateOptions(args);
6501
+ if (errors.length > 0) {
6502
+ for (const e of errors) console.error(import_picocolors.default.red(`error: ${e}`));
6503
+ console.error(`\nRun ${import_picocolors.default.cyan("wittyhub validate --help")} for usage.\n`);
6504
+ return null;
6505
+ }
6506
+ const result = await validateSkill(resolve(path));
6507
+ if (options.json) console.log(JSON.stringify(result, null, 2));
6508
+ else printResult(result);
6509
+ return result;
6510
+ }
6511
+ async function validateSkill(absPath) {
6512
+ const errors = [];
6513
+ const warnings = [];
6514
+ let skillDir = absPath;
6515
+ let skillMdPath = null;
6516
+ let stats;
6517
+ try {
6518
+ stats = await stat(absPath);
6519
+ } catch {
6520
+ errors.push({
6521
+ id: "path-not-found",
6522
+ severity: "error",
6523
+ message: `Path does not exist: ${absPath}`,
6524
+ hint: "Pass the skill directory (e.g. ./my-skill) or the SKILL.md file path."
6525
+ });
6526
+ return {
6527
+ path: absPath,
6528
+ errors,
6529
+ warnings,
6530
+ ok: false
6531
+ };
6532
+ }
6533
+ if (stats.isFile()) {
6534
+ const filename = basename(absPath);
6535
+ if (filename !== "SKILL.md") {
6536
+ errors.push({
6537
+ id: "filename-must-be-skill-md",
6538
+ severity: "error",
6539
+ message: `File must be named "SKILL.md" (got "${filename}")`,
6540
+ hint: "Rename the file to SKILL.md (uppercase). The agent skill spec requires this exact filename."
6541
+ });
6542
+ return {
6543
+ path: absPath,
6544
+ errors,
6545
+ warnings,
6546
+ ok: false
6547
+ };
6548
+ }
6549
+ skillMdPath = absPath;
6550
+ skillDir = dirname(absPath);
6551
+ } else if (stats.isDirectory()) {
6552
+ const expected = join(absPath, "SKILL.md");
6553
+ try {
6554
+ await stat(expected);
6555
+ skillMdPath = expected;
6556
+ } catch {
6557
+ errors.push({
6558
+ id: "missing-skill-md",
6559
+ severity: "error",
6560
+ message: `No SKILL.md found in directory: ${absPath}`,
6561
+ hint: "Create one with `wittyhub init <name>` or rename an existing markdown file to SKILL.md (uppercase)."
6562
+ });
6563
+ return {
6564
+ path: absPath,
6565
+ errors,
6566
+ warnings,
6567
+ ok: false
6568
+ };
6569
+ }
6570
+ } else {
6571
+ errors.push({
6572
+ id: "path-invalid-type",
6573
+ severity: "error",
6574
+ message: `Path is neither a file nor a directory: ${absPath}`
6575
+ });
6576
+ return {
6577
+ path: absPath,
6578
+ errors,
6579
+ warnings,
6580
+ ok: false
6581
+ };
6582
+ }
6583
+ try {
6584
+ const lowerMdFile = (await readdir(skillDir)).find((e) => e.toLowerCase() === "skill.md" && e !== "SKILL.md");
6585
+ if (lowerMdFile) warnings.push({
6586
+ id: "case-variant-found",
6587
+ severity: "warning",
6588
+ message: `Found a case-variant file "${lowerMdFile}" alongside SKILL.md`,
6589
+ hint: "Only \"SKILL.md\" (all uppercase) is recognized by the agent spec. Remove the variant to avoid confusion."
6590
+ });
6591
+ } catch {}
6592
+ const content = await readFile(skillMdPath, "utf-8");
6593
+ const match = content.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n?([\s\S]*)$/);
6594
+ if (!match) {
6595
+ errors.push({
6596
+ id: "frontmatter-missing",
6597
+ severity: "error",
6598
+ message: "SKILL.md must start with a YAML frontmatter block delimited by \"---\"",
6599
+ hint: "Add a frontmatter block at the top:\n\n ---\n name: my-skill\n description: what it does\n ---"
6600
+ });
6601
+ return {
6602
+ path: skillMdPath,
6603
+ errors,
6604
+ warnings,
6605
+ ok: false
6606
+ };
6607
+ }
6608
+ match[1];
6609
+ const body = match[2] ?? "";
6610
+ const { data: fm } = parseFrontmatter(content);
6611
+ if (!fm || typeof fm !== "object" || Array.isArray(fm)) {
6612
+ errors.push({
6613
+ id: "frontmatter-not-object",
6614
+ severity: "error",
6615
+ message: "Frontmatter must be a YAML mapping (key-value pairs), not a scalar or list."
6616
+ });
6617
+ return {
6618
+ path: skillMdPath,
6619
+ errors,
6620
+ warnings,
6621
+ ok: false
6622
+ };
6623
+ }
6624
+ if (fm.name === void 0 || fm.name === null || fm.name === "") errors.push({
6625
+ id: "name-required",
6626
+ severity: "error",
6627
+ message: "Missing required frontmatter field: name",
6628
+ hint: "Add `name: <skill-name>` to the frontmatter."
6629
+ });
6630
+ else if (typeof fm.name !== "string") errors.push({
6631
+ id: "name-not-string",
6632
+ severity: "error",
6633
+ message: `Frontmatter field "name" must be a string (got ${typeof fm.name})`,
6634
+ hint: "Wrap the value in quotes if it contains special characters."
6635
+ });
6636
+ else {
6637
+ if (!NAME_PATTERN.test(fm.name)) warnings.push({
6638
+ id: "name-not-kebab",
6639
+ severity: "warning",
6640
+ message: `Skill name "${fm.name}" is not in kebab-case (lowercase letters, digits, hyphens)`,
6641
+ hint: "Use a name like \"add-or-fix-type-checking\" for best compatibility."
6642
+ });
6643
+ if (basename(skillDir) !== fm.name) warnings.push({
6644
+ id: "name-dirname-mismatch",
6645
+ severity: "warning",
6646
+ message: `Skill name "${fm.name}" does not match directory name "${basename(skillDir)}"`,
6647
+ hint: "For consistency, the directory name and `name` field should match."
6648
+ });
6649
+ }
6650
+ if (fm.description === void 0 || fm.description === null || fm.description === "") errors.push({
6651
+ id: "description-required",
6652
+ severity: "error",
6653
+ message: "Missing required frontmatter field: description",
6654
+ hint: "Add `description: <one-line summary>` to the frontmatter."
6655
+ });
6656
+ else if (typeof fm.description !== "string") errors.push({
6657
+ id: "description-not-string",
6658
+ severity: "error",
6659
+ message: `Frontmatter field "description" must be a string (got ${typeof fm.description})`
6660
+ });
6661
+ else {
6662
+ const desc = fm.description.trim();
6663
+ if (desc.length < 10) warnings.push({
6664
+ id: "description-too-short",
6665
+ severity: "warning",
6666
+ message: `Description is very short (${desc.length} chars): "${desc}"`,
6667
+ hint: "Aim for at least 10-20 characters so users can quickly understand the skill."
6668
+ });
6669
+ if (desc.length > 200) warnings.push({
6670
+ id: "description-too-long",
6671
+ severity: "warning",
6672
+ message: `Description is very long (${desc.length} chars)`,
6673
+ hint: "Keep it under 200 characters for readability in listings."
6674
+ });
6675
+ }
6676
+ if (fm.category !== void 0 && fm.category !== null) {
6677
+ if (typeof fm.category !== "string") warnings.push({
6678
+ id: "category-not-string",
6679
+ severity: "warning",
6680
+ message: `Frontmatter field "category" should be a string (got ${typeof fm.category})`
6681
+ });
6682
+ else if (!CANONICAL_CATEGORIES.has(fm.category)) warnings.push({
6683
+ id: "category-not-canonical",
6684
+ severity: "warning",
6685
+ message: `Category "${fm.category}" is not in the canonical list`,
6686
+ hint: `Valid values: ${[...CANONICAL_CATEGORIES].join(", ")}. See https://gitcode.com/openeuler/openEuler-skills.`
6687
+ });
6688
+ }
6689
+ if (fm.version !== void 0 && fm.version !== null && fm.version !== "") {
6690
+ if (typeof fm.version !== "string") warnings.push({
6691
+ id: "version-not-string",
6692
+ severity: "warning",
6693
+ message: `Frontmatter field "version" should be a string (got ${typeof fm.version})`,
6694
+ hint: "Quote the value, e.g. `version: \"1.0.0\"`."
6695
+ });
6696
+ else if (!SEMVER_PATTERN.test(fm.version.trim())) warnings.push({
6697
+ id: "version-not-semver",
6698
+ severity: "warning",
6699
+ message: `Version "${fm.version}" is not a valid semver`,
6700
+ hint: "Use the form X.Y.Z (e.g. 1.0.0, 0.2.1-beta)."
6701
+ });
6702
+ }
6703
+ if (fm.metadata !== void 0 && (typeof fm.metadata !== "object" || Array.isArray(fm.metadata))) warnings.push({
6704
+ id: "metadata-not-object",
6705
+ severity: "warning",
6706
+ message: `Frontmatter field "metadata" should be a mapping (got ${Array.isArray(fm.metadata) ? "array" : typeof fm.metadata})`
6707
+ });
6708
+ if (body.trim().length === 0) warnings.push({
6709
+ id: "body-empty",
6710
+ severity: "warning",
6711
+ message: "SKILL.md has no body content after the frontmatter",
6712
+ hint: "Add at least a heading and a few instructions for the agent to follow."
6713
+ });
6714
+ else if (!/^#{1,6}\s+\S/m.test(body)) warnings.push({
6715
+ id: "body-no-heading",
6716
+ severity: "warning",
6717
+ message: "Body content has no Markdown headings",
6718
+ hint: "Structure the instructions with headings like `## When to use`, `## Instructions`."
6719
+ });
6720
+ const knownKeys = new Set([
6721
+ "name",
6722
+ "description",
6723
+ "version",
6724
+ "category",
6725
+ "metadata"
6726
+ ]);
6727
+ const unknownKeys = Object.keys(fm).filter((k) => !knownKeys.has(k));
6728
+ if (unknownKeys.length > 0) warnings.push({
6729
+ id: "unknown-frontmatter-keys",
6730
+ severity: "warning",
6731
+ message: `Unknown frontmatter keys: ${unknownKeys.join(", ")}`,
6732
+ hint: `Recognized keys: ${[...knownKeys].join(", ")}.`
6733
+ });
6734
+ return {
6735
+ path: skillMdPath,
6736
+ errors,
6737
+ warnings,
6738
+ ok: errors.length === 0
6739
+ };
6740
+ }
6741
+ function printResult(result) {
6742
+ const { path, errors, warnings, ok } = result;
6743
+ console.log(`${import_picocolors.default.cyan("Path:")} ${path}`);
6744
+ console.log();
6745
+ if (errors.length === 0 && warnings.length === 0) {
6746
+ console.log(import_picocolors.default.green(" All checks passed."));
6747
+ console.log();
6748
+ return;
6749
+ }
6750
+ if (errors.length > 0) {
6751
+ console.log(import_picocolors.default.red(`${errors.length} error(s):`));
6752
+ for (const rule of errors) {
6753
+ console.log(` ${import_picocolors.default.red("✗")} [${rule.id}] ${rule.message}`);
6754
+ if (rule.hint) console.log(` ${import_picocolors.default.dim(rule.hint)}`);
6755
+ }
6756
+ console.log();
6757
+ }
6758
+ if (warnings.length > 0) {
6759
+ console.log(import_picocolors.default.yellow(`${warnings.length} warning(s):`));
6760
+ for (const rule of warnings) {
6761
+ console.log(` ${import_picocolors.default.yellow("!")} [${rule.id}] ${rule.message}`);
6762
+ if (rule.hint) console.log(` ${import_picocolors.default.dim(rule.hint)}`);
6763
+ }
6764
+ console.log();
6765
+ }
6766
+ if (ok) console.log(import_picocolors.default.green(" Skill is valid.") + import_picocolors.default.yellow(warnings.length > 0 ? ` (with ${warnings.length} warning${warnings.length > 1 ? "s" : ""})` : ""));
6767
+ else console.log(import_picocolors.default.red(" Skill is NOT valid. Fix the errors above."));
6768
+ console.log();
6769
+ }
6225
6770
  const __dirname = dirname(fileURLToPath(import.meta.url));
6226
6771
  function getVersion() {
6227
6772
  try {
@@ -6274,11 +6819,12 @@ function showBanner() {
6274
6819
  console.log();
6275
6820
  console.log(` ${DIM}$${RESET} ${TEXT}npx wittyhub experimental_install${RESET} ${DIM}Restore from skills-lock.json${RESET}`);
6276
6821
  console.log(` ${DIM}$${RESET} ${TEXT}npx wittyhub init ${DIM}[name]${RESET} ${DIM}Create a new skill${RESET}`);
6822
+ console.log(` ${DIM}$${RESET} ${TEXT}npx wittyhub validate ${DIM}<path>${RESET} ${DIM}Validate a local skill${RESET}`);
6277
6823
  console.log(` ${DIM}$${RESET} ${TEXT}npx wittyhub experimental_sync${RESET} ${DIM}Sync skills from node_modules${RESET}`);
6278
6824
  console.log();
6279
6825
  console.log(`${DIM}try:${RESET} npx wittyhub add vercel-labs/agent-skills`);
6280
6826
  console.log();
6281
- console.log(`Discover more skills at ${TEXT}https://skills.sh/${RESET}`);
6827
+ console.log(`Discover more skills at ${TEXT}https://skillhub.openeuler.org/${RESET}`);
6282
6828
  console.log();
6283
6829
  }
6284
6830
  function showHelp() {
@@ -6294,6 +6840,14 @@ ${BOLD}Manage Skills:${RESET}
6294
6840
  remove [skills] Remove installed skills
6295
6841
  list, ls List installed skills
6296
6842
  find [query] Search for skills interactively
6843
+ audit <source> --skill <skill>
6844
+ Show security audit result (risk level + risk signals)
6845
+ e.g. audit https://github.com/huggingface/transformers --skill add-or-fix-type-checking
6846
+ get <source> --skill <skill>
6847
+ View skill details (author/category/version/description/tags)
6848
+ e.g. get https://github.com/huggingface/transformers --skill add-or-fix-type-checking
6849
+ validate <path> Validate a local skill for spec compliance (SKILL.md filename,
6850
+ frontmatter, required fields name/description, etc.)
6297
6851
 
6298
6852
  ${BOLD}Find Options:${RESET}
6299
6853
  --owner <owner> Search only repositories from a GitHub owner
@@ -6308,7 +6862,7 @@ ${BOLD}Update Options:${RESET}
6308
6862
 
6309
6863
  ${BOLD}Project:${RESET}
6310
6864
  experimental_install Restore skills from skills-lock.json
6311
- init [name] Initialize a skill (creates <name>/SKILL.md or ./SKILL.md)
6865
+ init [name] Initialize a skill (creates <name>/SKILL.md; defaults to my-skill/)
6312
6866
  experimental_sync Sync skills from node_modules into agent directories
6313
6867
 
6314
6868
  ${BOLD}Add Options:${RESET}
@@ -6350,12 +6904,9 @@ ${BOLD}Options:${RESET}
6350
6904
  --version, -v Show version number
6351
6905
 
6352
6906
  ${BOLD}Examples:${RESET}
6353
- ${DIM}$${RESET} wittyhub add vercel-labs/agent-skills
6354
- ${DIM}$${RESET} wittyhub use vercel-labs/agent-skills@vercel-optimize | claude
6355
- ${DIM}$${RESET} wittyhub use vercel-labs/agent-skills --skill vercel-optimize --agent claude-code
6356
- ${DIM}$${RESET} wittyhub add vercel-labs/agent-skills -g
6357
- ${DIM}$${RESET} wittyhub add vercel-labs/agent-skills --agent claude-code cursor
6358
- ${DIM}$${RESET} wittyhub add vercel-labs/agent-skills --skill pr-review commit
6907
+ ${DIM}$${RESET} wittyhub add https://gitcode.com/openeuler/witty-diagnosis-agent --skill system-resource-diagnosis
6908
+ ${DIM}$${RESET} wittyhub add https://gitcode.com/openeuler/witty-diagnosis-agent --skill system-resource-diagnosis -g
6909
+ ${DIM}$${RESET} wittyhub add https://gitcode.com/openeuler/witty-diagnosis-agent --skill system-resource-diagnosis --agent claude-code cursor
6359
6910
  ${DIM}$${RESET} wittyhub remove ${DIM}# interactive remove${RESET}
6360
6911
  ${DIM}$${RESET} wittyhub remove web-design ${DIM}# remove by name${RESET}
6361
6912
  ${DIM}$${RESET} wittyhub rm --global frontend-design
@@ -6374,7 +6925,45 @@ ${BOLD}Examples:${RESET}
6374
6925
  ${DIM}$${RESET} wittyhub experimental_sync ${DIM}# sync from node_modules${RESET}
6375
6926
  ${DIM}$${RESET} wittyhub experimental_sync -y ${DIM}# sync without prompts${RESET}
6376
6927
 
6377
- Discover more skills at ${TEXT}https://skills.sh/${RESET}
6928
+ Discover more skills at ${TEXT}https://skillhub.openeuler.org/${RESET}
6929
+ `);
6930
+ }
6931
+ function showInitHelp() {
6932
+ console.log(`
6933
+ ${BOLD}Usage:${RESET} wittyhub init [name]
6934
+
6935
+ ${BOLD}Description:${RESET}
6936
+ Initialize a new skill by creating a SKILL.md template.
6937
+
6938
+ ${BOLD}Arguments:${RESET}
6939
+ name Optional skill name. Creates <name>/SKILL.md.
6940
+ If omitted, defaults to my-skill/ (never the cwd itself).
6941
+
6942
+ ${BOLD}Examples:${RESET}
6943
+ ${DIM}$${RESET} wittyhub init ${DIM}# creates my-skill/SKILL.md${RESET}
6944
+ ${DIM}$${RESET} wittyhub init my-skill ${DIM}# creates my-skill/SKILL.md${RESET}
6945
+ ${DIM}$${RESET} wittyhub init code-reviewer ${DIM}# creates code-reviewer/SKILL.md${RESET}
6946
+ `);
6947
+ }
6948
+ function showValidateHelp() {
6949
+ console.log(`
6950
+ ${BOLD}Usage:${RESET} wittyhub validate <path> [options]
6951
+
6952
+ ${BOLD}Description:${RESET}
6953
+ Validate a local skill for spec compliance (SKILL.md filename, frontmatter,
6954
+ required fields, etc.). Reports errors (must-fix) and warnings (recommended).
6955
+
6956
+ ${BOLD}Arguments:${RESET}
6957
+ path Path to the skill directory or SKILL.md file.
6958
+
6959
+ ${BOLD}Options:${RESET}
6960
+ --json Output result as JSON (no ANSI colors).
6961
+ -h, --help Show this help message.
6962
+
6963
+ ${BOLD}Examples:${RESET}
6964
+ ${DIM}$${RESET} wittyhub validate ./my-skill
6965
+ ${DIM}$${RESET} wittyhub validate ./my-skill/SKILL.md
6966
+ ${DIM}$${RESET} wittyhub validate ./my-skill --json
6378
6967
  `);
6379
6968
  }
6380
6969
  function showRemoveHelp() {
@@ -6404,24 +6993,25 @@ ${BOLD}Examples:${RESET}
6404
6993
  ${DIM}$${RESET} wittyhub remove --all ${DIM}# remove all skills${RESET}
6405
6994
  ${DIM}$${RESET} wittyhub remove --skill '*' -a cursor ${DIM}# remove all skills from cursor${RESET}
6406
6995
 
6407
- Discover more skills at ${TEXT}https://skills.sh/${RESET}
6996
+ Discover more skills at ${TEXT}https://skillhub.openeuler.org/${RESET}
6408
6997
  `);
6409
6998
  }
6410
6999
  function runInit(args) {
6411
7000
  const cwd = process.cwd();
6412
- const skillName = args[0] || basename(cwd);
6413
- const hasName = args[0] !== void 0;
6414
- const skillDir = hasName ? join(cwd, skillName) : cwd;
7001
+ const skillName = args[0] ?? "my-skill";
7002
+ const skillDir = join(cwd, skillName);
6415
7003
  const skillFile = join(skillDir, "SKILL.md");
6416
- const displayPath = hasName ? `${skillName}/SKILL.md` : "SKILL.md";
7004
+ const displayPath = `${skillName}/SKILL.md`;
6417
7005
  if (existsSync(skillFile)) {
6418
7006
  console.log(`${TEXT}Skill already exists at ${DIM}${displayPath}${RESET}`);
6419
7007
  return;
6420
7008
  }
6421
- if (hasName) mkdirSync(skillDir, { recursive: true });
7009
+ mkdirSync(skillDir, { recursive: true });
6422
7010
  writeFileSync(skillFile, `---
6423
7011
  name: ${skillName}
6424
7012
  description: A brief description of what this skill does
7013
+ version: 0.1.0 # optional; increment as needed
7014
+ category: others # optional;change if needed — full list at https://gitcode.com/openeuler/openEuler-skills
6425
7015
  ---
6426
7016
 
6427
7017
  # ${skillName}
@@ -6446,12 +7036,12 @@ Describe when this skill should be used.
6446
7036
  console.log(`${DIM}Next steps:${RESET}`);
6447
7037
  console.log(` 1. Edit ${TEXT}${displayPath}${RESET} to define your skill instructions`);
6448
7038
  console.log(` 2. Update the ${TEXT}name${RESET} and ${TEXT}description${RESET} in the frontmatter`);
7039
+ console.log(` 3. Optionally set ${TEXT}version${RESET} and ${TEXT}category${RESET} (see the comment in ${TEXT}${displayPath}${RESET} for the full list of categories)`);
6449
7040
  console.log();
6450
7041
  console.log(`${DIM}Publishing:${RESET}`);
6451
- console.log(` ${DIM}GitHub:${RESET} Push to a repo, then ${TEXT}npx wittyhub add <owner>/<repo>${RESET}`);
6452
- console.log(` ${DIM}URL:${RESET} Host the file, then ${TEXT}npx wittyhub add https://example.com/${displayPath}${RESET}`);
7042
+ console.log(` Submit your skill to ${TEXT}https://gitcode.com/openeuler/openEuler-skills${RESET}`);
6453
7043
  console.log();
6454
- console.log(`Browse existing skills for inspiration at ${TEXT}https://skills.sh/${RESET}`);
7044
+ console.log(`Browse existing skills for inspiration at ${TEXT}https://skillhub.openeuler.org/${RESET}`);
6455
7045
  console.log();
6456
7046
  }
6457
7047
  async function main() {
@@ -6473,6 +7063,10 @@ async function main() {
6473
7063
  await runFind(restArgs);
6474
7064
  break;
6475
7065
  case "init":
7066
+ if (restArgs.includes("--help") || restArgs.includes("-h")) {
7067
+ showInitHelp();
7068
+ break;
7069
+ }
6476
7070
  if (!inAgent) showLogo();
6477
7071
  console.log();
6478
7072
  runInit(restArgs);
@@ -6515,6 +7109,24 @@ async function main() {
6515
7109
  case "ls":
6516
7110
  await runList(restArgs);
6517
7111
  break;
7112
+ case "audit":
7113
+ if (!inAgent) showLogo();
7114
+ await runAudit(restArgs);
7115
+ break;
7116
+ case "get":
7117
+ if (!inAgent) showLogo();
7118
+ await runGet(restArgs);
7119
+ break;
7120
+ case "validate":
7121
+ case "check-skill":
7122
+ if (restArgs.includes("--help") || restArgs.includes("-h")) {
7123
+ showValidateHelp();
7124
+ break;
7125
+ }
7126
+ if (!inAgent) showLogo();
7127
+ console.log();
7128
+ await runValidate(restArgs);
7129
+ break;
6518
7130
  case "check":
6519
7131
  case "update":
6520
7132
  case "upgrade":