wittyhub 0.0.2 → 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,8 +2207,6 @@ async function listInstalledSkills(options = {}) {
2260
2207
  } catch {}
2261
2208
  return Array.from(skillsMap.values());
2262
2209
  }
2263
- const TELEMETRY_URL = "https://add-skill.vercel.sh/t";
2264
- const AUDIT_URL = "https://add-skill.vercel.sh/audit";
2265
2210
  let cliVersion = null;
2266
2211
  let detectedAgentName = null;
2267
2212
  function setDetectedAgent(agentName) {
@@ -2276,21 +2221,65 @@ function isEnabled() {
2276
2221
  function setVersion(version) {
2277
2222
  cliVersion = version;
2278
2223
  }
2279
- async function fetchAuditData(source, skillSlugs, timeoutMs = 3e3) {
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) {
2280
2253
  if (skillSlugs.length === 0) return null;
2254
+ const controller = new AbortController();
2255
+ const timeout = setTimeout(() => controller.abort(), timeoutMs);
2281
2256
  try {
2282
- const params = new URLSearchParams({
2283
- source,
2284
- skills: skillSlugs.join(",")
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 {}
2285
2276
  });
2286
- const controller = new AbortController();
2287
- const timeout = setTimeout(() => controller.abort(), timeoutMs);
2288
- const response = await fetch(`${AUDIT_URL}?${params.toString()}`, { signal: controller.signal });
2289
- clearTimeout(timeout);
2290
- if (!response.ok) return null;
2291
- return await response.json();
2277
+ await Promise.all(promises);
2278
+ return Object.keys(results).length > 0 ? results : null;
2292
2279
  } catch {
2293
2280
  return null;
2281
+ } finally {
2282
+ clearTimeout(timeout);
2294
2283
  }
2295
2284
  }
2296
2285
  const pendingTelemetry = [];
@@ -2810,7 +2799,7 @@ const CURRENT_VERSION = 3;
2810
2799
  function getSkillLockPath() {
2811
2800
  const xdgStateHome = process.env.XDG_STATE_HOME;
2812
2801
  if (xdgStateHome) return join(xdgStateHome, "skills", LOCK_FILE);
2813
- return join(homedir(), AGENTS_DIR, LOCK_FILE);
2802
+ return join(homedir$1(), AGENTS_DIR, LOCK_FILE);
2814
2803
  }
2815
2804
  async function readSkillLock() {
2816
2805
  const lockPath = getSkillLockPath();
@@ -2827,7 +2816,7 @@ async function readSkillLock() {
2827
2816
  async function writeSkillLock(lock) {
2828
2817
  const lockPath = getSkillLockPath();
2829
2818
  await mkdir(dirname(lockPath), { recursive: true });
2830
- await writeFile(lockPath, JSON.stringify(lock, null, 2), "utf-8");
2819
+ await writeFile$1(lockPath, JSON.stringify(lock, null, 2), "utf-8");
2831
2820
  }
2832
2821
  let _ghWarningShown = false;
2833
2822
  function getGitHubToken() {
@@ -3164,65 +3153,126 @@ async function tryBlobInstall(ownerRepo, options = {}) {
3164
3153
  tree
3165
3154
  };
3166
3155
  }
3167
- var version$1 = "0.0.2";
3156
+ var version$1 = "0.0.4";
3168
3157
  const isCancelled$1 = (value) => typeof value === "symbol";
3169
- async function isSourcePrivate(source) {
3170
- const ownerRepo = parseOwnerRepo(source);
3171
- if (!ownerRepo) return false;
3172
- return isRepoPrivate(ownerRepo.owner, ownerRepo.repo);
3173
- }
3174
3158
  function getLockSource(parsedUrl, normalizedSource) {
3175
3159
  return parsedUrl.startsWith("git@") || parsedUrl.startsWith("ssh://") ? parsedUrl : normalizedSource;
3176
3160
  }
3177
3161
  function initTelemetry(version) {
3178
3162
  setVersion(version);
3179
3163
  }
3180
- 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) {
3181
3230
  switch (risk) {
3182
- case "critical": return import_picocolors.default.red(import_picocolors.default.bold("Critical Risk"));
3183
- case "high": return import_picocolors.default.red("High Risk");
3184
- case "medium": return import_picocolors.default.yellow("Med Risk");
3185
- 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");
3186
3235
  case "safe": return import_picocolors.default.green("Safe");
3187
- default: return import_picocolors.default.dim("--");
3236
+ default: return import_picocolors.default.dim("Unknown");
3188
3237
  }
3189
3238
  }
3190
- function socketLabel(audit) {
3191
- if (!audit) return import_picocolors.default.dim("--");
3192
- const count = audit.alerts ?? 0;
3193
- return count > 0 ? import_picocolors.default.red(`${count} alert${count !== 1 ? "s" : ""}`) : import_picocolors.default.green("0 alerts");
3194
- }
3195
3239
  function padEnd(str, width) {
3196
3240
  const visible = stripTerminalEscapes(str);
3197
3241
  const pad = Math.max(0, width - visible.length);
3198
3242
  return str + " ".repeat(pad);
3199
3243
  }
3200
- function buildSecurityLines(auditData, skills, source) {
3244
+ function buildSecurityLines(auditData, skills) {
3201
3245
  if (!auditData) return [];
3202
- if (!skills.some((s) => {
3203
- const data = auditData[s.slug];
3204
- return data && Object.keys(data).length > 0;
3205
- })) return [];
3206
- 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);
3207
3249
  const lines = [];
3208
- 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");
3209
3251
  lines.push(header);
3210
- for (const skill of skills) {
3252
+ for (const skill of rows) {
3211
3253
  const data = auditData[skill.slug];
3212
3254
  const name = skill.displayName.length > nameWidth ? skill.displayName.slice(0, nameWidth - 1) + "…" : skill.displayName;
3213
- const ath = data?.ath ? riskLabel(data.ath.risk) : import_picocolors.default.dim("--");
3214
- const socket = data?.socket ? socketLabel(data.socket) : import_picocolors.default.dim("--");
3215
- const snyk = data?.snyk ? riskLabel(data.snyk.risk) : import_picocolors.default.dim("--");
3216
- 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);
3217
3259
  }
3218
- lines.push("");
3219
- lines.push(`${import_picocolors.default.dim("Details:")} ${import_picocolors.default.dim(`https://skills.sh/${source}`)}`);
3220
3260
  return lines;
3221
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
+ }
3222
3272
  function shortenPath$2(fullPath, cwd) {
3223
3273
  const home = homedir();
3224
- if (fullPath === home || fullPath.startsWith(home + sep)) return "~" + fullPath.slice(home.length);
3225
- 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);
3226
3276
  return fullPath;
3227
3277
  }
3228
3278
  function computeSingleFileSkillHash(contents) {
@@ -3237,6 +3287,13 @@ function formatList$1(items, maxShow = 5) {
3237
3287
  const remaining = items.length - maxShow;
3238
3288
  return `${shown.join(", ")} +${remaining} more`;
3239
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
+ }
3240
3297
  function splitAgentsByType(agentTypes) {
3241
3298
  const universal = [];
3242
3299
  const symlinked = [];
@@ -3247,18 +3304,6 @@ function splitAgentsByType(agentTypes) {
3247
3304
  symlinked
3248
3305
  };
3249
3306
  }
3250
- function buildAgentSummaryLines(targetAgents, installMode) {
3251
- const lines = [];
3252
- const { universal, symlinked } = splitAgentsByType(targetAgents);
3253
- if (installMode === "symlink") {
3254
- if (universal.length > 0) lines.push(` ${import_picocolors.default.green("universal:")} ${formatList$1(universal)}`);
3255
- if (symlinked.length > 0) lines.push(` ${import_picocolors.default.dim("symlink →")} ${formatList$1(symlinked)}`);
3256
- } else {
3257
- const allNames = targetAgents.map((a) => agents[a].displayName);
3258
- lines.push(` ${import_picocolors.default.dim("copy →")} ${formatList$1(allNames)}`);
3259
- }
3260
- return lines;
3261
- }
3262
3307
  function targetDisplayName(target) {
3263
3308
  const base = agents[target.agent].displayName;
3264
3309
  return target.subagent ? `${base} (${target.subagent})` : base;
@@ -3386,285 +3431,6 @@ async function selectAgentsInteractive(options) {
3386
3431
  return selected;
3387
3432
  }
3388
3433
  setVersion(version$1);
3389
- async function handleWellKnownSkills(source, url, options, spinner) {
3390
- spinner.start("Discovering skills from well-known endpoint...");
3391
- const skills = await wellKnownProvider.fetchAllSkills(url);
3392
- if (skills.length === 0) {
3393
- spinner.stop(import_picocolors.default.red("No skills found"));
3394
- 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."));
3395
- process.exit(1);
3396
- }
3397
- spinner.stop(`Found ${import_picocolors.default.green(skills.length)} skill${skills.length > 1 ? "s" : ""}`);
3398
- for (const skill of skills) {
3399
- M.info(`Skill: ${import_picocolors.default.cyan(skill.installName)}`);
3400
- M.message(import_picocolors.default.dim(skill.description));
3401
- if (skill.files.size > 1) M.message(import_picocolors.default.dim(` Files: ${Array.from(skill.files.keys()).join(", ")}`));
3402
- }
3403
- if (options.list) {
3404
- console.log();
3405
- M.step(import_picocolors.default.bold("Available Skills"));
3406
- for (const skill of skills) {
3407
- M.message(` ${import_picocolors.default.cyan(skill.installName)}`);
3408
- M.message(` ${import_picocolors.default.dim(skill.description)}`);
3409
- if (skill.files.size > 1) M.message(` ${import_picocolors.default.dim(`Files: ${skill.files.size}`)}`);
3410
- }
3411
- console.log();
3412
- Se("Run without --list to install");
3413
- process.exit(0);
3414
- }
3415
- let selectedSkills;
3416
- if (options.skill?.includes("*")) {
3417
- selectedSkills = skills;
3418
- M.info(`Installing all ${skills.length} skills`);
3419
- } else if (options.skill && options.skill.length > 0) {
3420
- selectedSkills = skills.filter((s) => options.skill.some((name) => s.installName.toLowerCase() === name.toLowerCase() || s.name.toLowerCase() === name.toLowerCase()));
3421
- if (selectedSkills.length === 0) {
3422
- M.error(`No matching skills found for: ${options.skill.join(", ")}`);
3423
- M.info("Available skills:");
3424
- for (const s of skills) M.message(` - ${s.installName}`);
3425
- process.exit(1);
3426
- }
3427
- } else if (skills.length === 1) {
3428
- selectedSkills = skills;
3429
- const firstSkill = skills[0];
3430
- M.info(`Skill: ${import_picocolors.default.cyan(firstSkill.installName)}`);
3431
- } else if (options.yes) {
3432
- selectedSkills = skills;
3433
- M.info(`Installing all ${skills.length} skills`);
3434
- } else {
3435
- const selected = await multiselect({
3436
- message: "Select skills to install",
3437
- options: skills.map((s) => ({
3438
- value: s,
3439
- label: s.installName,
3440
- hint: s.description.length > 60 ? s.description.slice(0, 57) + "..." : s.description
3441
- })),
3442
- required: true
3443
- });
3444
- if (pD(selected)) {
3445
- xe("Installation cancelled");
3446
- process.exit(0);
3447
- }
3448
- selectedSkills = selected;
3449
- }
3450
- let targetAgents;
3451
- const validAgents = Object.keys(agents);
3452
- if (options.agent?.includes("*")) {
3453
- targetAgents = validAgents;
3454
- M.info(`Installing to all ${targetAgents.length} agents`);
3455
- } else if (options.agent && options.agent.length > 0) {
3456
- const invalidAgents = options.agent.filter((a) => !validAgents.includes(a));
3457
- if (invalidAgents.length > 0) {
3458
- M.error(`Invalid agents: ${invalidAgents.join(", ")}`);
3459
- M.info(`Valid agents: ${validAgents.join(", ")}`);
3460
- process.exit(1);
3461
- }
3462
- targetAgents = options.agent;
3463
- } else {
3464
- spinner.start("Loading agents...");
3465
- const installedAgents = await detectInstalledAgents();
3466
- const totalAgents = Object.keys(agents).length;
3467
- spinner.stop(`${totalAgents} agents`);
3468
- if (installedAgents.length === 0) if (options.yes) {
3469
- targetAgents = validAgents;
3470
- M.info("Installing to all agents");
3471
- } else {
3472
- M.info("Select agents to install skills to");
3473
- const selected = await promptForAgents("Which agents do you want to install to?", Object.entries(agents).map(([key, config]) => ({
3474
- value: key,
3475
- label: config.displayName
3476
- })));
3477
- if (pD(selected)) {
3478
- xe("Installation cancelled");
3479
- process.exit(0);
3480
- }
3481
- targetAgents = selected;
3482
- }
3483
- else if (installedAgents.length === 1 || options.yes) {
3484
- targetAgents = ensureUniversalAgents(installedAgents);
3485
- if (installedAgents.length === 1) {
3486
- const firstAgent = installedAgents[0];
3487
- M.info(`Installing to: ${import_picocolors.default.cyan(agents[firstAgent].displayName)}`);
3488
- } else M.info(`Installing to: ${installedAgents.map((a) => import_picocolors.default.cyan(agents[a].displayName)).join(", ")}`);
3489
- } else {
3490
- const selected = await selectAgentsInteractive({ global: options.global });
3491
- if (pD(selected)) {
3492
- xe("Installation cancelled");
3493
- process.exit(0);
3494
- }
3495
- targetAgents = selected;
3496
- }
3497
- }
3498
- let installGlobally = options.global ?? false;
3499
- const supportsGlobal = targetAgents.some((a) => agents[a].globalSkillsDir !== void 0);
3500
- if (options.global === void 0 && !options.yes && supportsGlobal) {
3501
- const scope = await ve({
3502
- message: "Installation scope",
3503
- options: [{
3504
- value: false,
3505
- label: "Project",
3506
- hint: "Install in current directory (committed with your project)"
3507
- }, {
3508
- value: true,
3509
- label: "Global",
3510
- hint: "Install in home directory (available across all projects)"
3511
- }]
3512
- });
3513
- if (pD(scope)) {
3514
- xe("Installation cancelled");
3515
- process.exit(0);
3516
- }
3517
- installGlobally = scope;
3518
- }
3519
- let installMode = options.copy ? "copy" : "symlink";
3520
- const uniqueDirs = new Set(targetAgents.map((a) => agents[a].skillsDir));
3521
- if (!options.copy && !options.yes && uniqueDirs.size > 1) {
3522
- const modeChoice = await ve({
3523
- message: "Installation method",
3524
- options: [{
3525
- value: "symlink",
3526
- label: "Symlink (Recommended)",
3527
- hint: "Single source of truth, easy updates"
3528
- }, {
3529
- value: "copy",
3530
- label: "Copy to all agents",
3531
- hint: "Independent copies for each agent"
3532
- }]
3533
- });
3534
- if (pD(modeChoice)) {
3535
- xe("Installation cancelled");
3536
- process.exit(0);
3537
- }
3538
- installMode = modeChoice;
3539
- } else if (uniqueDirs.size <= 1) installMode = "copy";
3540
- const cwd = process.cwd();
3541
- const summaryLines = [];
3542
- targetAgents.map((a) => agents[a].displayName);
3543
- const overwriteChecks = await Promise.all(selectedSkills.flatMap((skill) => targetAgents.map(async (agent) => ({
3544
- skillName: skill.installName,
3545
- agent,
3546
- installed: await isSkillInstalled(skill.installName, agent, { global: installGlobally })
3547
- }))));
3548
- const overwriteStatus = /* @__PURE__ */ new Map();
3549
- for (const { skillName, agent, installed } of overwriteChecks) {
3550
- if (!overwriteStatus.has(skillName)) overwriteStatus.set(skillName, /* @__PURE__ */ new Map());
3551
- overwriteStatus.get(skillName).set(agent, installed);
3552
- }
3553
- for (const skill of selectedSkills) {
3554
- if (summaryLines.length > 0) summaryLines.push("");
3555
- const shortCanonical = shortenPath$2(getCanonicalPath(skill.installName, { global: installGlobally }), cwd);
3556
- summaryLines.push(`${import_picocolors.default.cyan(shortCanonical)}`);
3557
- summaryLines.push(...buildAgentSummaryLines(targetAgents, installMode));
3558
- if (skill.files.size > 1) summaryLines.push(` ${import_picocolors.default.dim("files:")} ${skill.files.size}`);
3559
- const skillOverwrites = overwriteStatus.get(skill.installName);
3560
- const overwriteAgents = targetAgents.filter((a) => skillOverwrites?.get(a)).map((a) => agents[a].displayName);
3561
- if (overwriteAgents.length > 0) summaryLines.push(` ${import_picocolors.default.yellow("overwrites:")} ${formatList$1(overwriteAgents)}`);
3562
- }
3563
- console.log();
3564
- Me(summaryLines.join("\n"), "Installation Summary");
3565
- if (!options.yes) {
3566
- const confirmed = await ye({ message: "Proceed with installation?" });
3567
- if (pD(confirmed) || !confirmed) {
3568
- xe("Installation cancelled");
3569
- process.exit(0);
3570
- }
3571
- }
3572
- const sourceIdentifier = wellKnownProvider.getSourceIdentifier(url);
3573
- const wellKnownPrivacyPromise = isSourcePrivate(sourceIdentifier).catch(() => null);
3574
- spinner.start("Installing skills...");
3575
- const results = [];
3576
- for (const skill of selectedSkills) for (const agent of targetAgents) {
3577
- const result = await installWellKnownSkillForAgent(skill, agent, {
3578
- global: installGlobally,
3579
- mode: installMode
3580
- });
3581
- results.push({
3582
- skill: skill.installName,
3583
- agent: agents[agent].displayName,
3584
- ...result
3585
- });
3586
- }
3587
- spinner.stop("Installation complete");
3588
- console.log();
3589
- const successful = results.filter((r) => r.success);
3590
- const failed = results.filter((r) => !r.success);
3591
- const skillFiles = {};
3592
- for (const skill of selectedSkills) skillFiles[skill.installName] = skill.sourceUrl;
3593
- if (await wellKnownPrivacyPromise !== true) track({
3594
- event: "install",
3595
- source: sourceIdentifier,
3596
- skills: selectedSkills.map((s) => s.installName).join(","),
3597
- agents: targetAgents.join(","),
3598
- ...installGlobally && { global: "1" },
3599
- skillFiles: JSON.stringify(skillFiles),
3600
- sourceType: "well-known"
3601
- });
3602
- if (successful.length > 0 && installGlobally) {
3603
- const successfulSkillNames = new Set(successful.map((r) => r.skill));
3604
- for (const skill of selectedSkills) if (successfulSkillNames.has(skill.installName)) try {
3605
- await addSkillToLock(skill.installName, {
3606
- source: sourceIdentifier,
3607
- sourceType: "well-known",
3608
- sourceUrl: skill.sourceUrl,
3609
- skillFolderHash: ""
3610
- });
3611
- } catch {}
3612
- }
3613
- if (successful.length > 0 && !installGlobally) {
3614
- const successfulSkillNames = new Set(successful.map((r) => r.skill));
3615
- for (const skill of selectedSkills) if (successfulSkillNames.has(skill.installName)) try {
3616
- const matchingResult = successful.find((r) => r.skill === skill.installName);
3617
- const installDir = matchingResult?.canonicalPath || matchingResult?.path;
3618
- if (installDir) {
3619
- const computedHash = await computeSkillFolderHash(installDir);
3620
- await addSkillToLocalLock(skill.installName, {
3621
- source: sourceIdentifier,
3622
- sourceType: "well-known",
3623
- computedHash
3624
- }, cwd);
3625
- }
3626
- } catch {}
3627
- }
3628
- if (successful.length > 0) {
3629
- const bySkill = /* @__PURE__ */ new Map();
3630
- for (const r of successful) {
3631
- const skillResults = bySkill.get(r.skill) || [];
3632
- skillResults.push(r);
3633
- bySkill.set(r.skill, skillResults);
3634
- }
3635
- const skillCount = bySkill.size;
3636
- const symlinkFailures = successful.filter((r) => r.mode === "symlink" && r.symlinkFailed);
3637
- const copiedAgents = symlinkFailures.map((r) => r.agent);
3638
- const resultLines = [];
3639
- for (const [skillName, skillResults] of bySkill) {
3640
- const firstResult = skillResults[0];
3641
- if (firstResult.mode === "copy") {
3642
- resultLines.push(`${import_picocolors.default.green("✓")} ${skillName} ${import_picocolors.default.dim("(copied)")}`);
3643
- resultLines.push(...buildCopyResultLines(skillResults, cwd));
3644
- } else {
3645
- if (firstResult.canonicalPath) {
3646
- const shortPath = shortenPath$2(firstResult.canonicalPath, cwd);
3647
- resultLines.push(`${import_picocolors.default.green("✓")} ${shortPath}`);
3648
- } else resultLines.push(`${import_picocolors.default.green("✓")} ${skillName}`);
3649
- resultLines.push(...buildResultLines(skillResults, targetAgents));
3650
- }
3651
- }
3652
- const title = import_picocolors.default.green(`Installed ${skillCount} skill${skillCount !== 1 ? "s" : ""}`);
3653
- Me(resultLines.join("\n"), title);
3654
- if (symlinkFailures.length > 0) {
3655
- M.warn(import_picocolors.default.yellow(`Symlinks failed for: ${formatList$1(copiedAgents)}`));
3656
- M.message(import_picocolors.default.dim(" Files were copied instead. On Windows, enable Developer Mode for symlink support."));
3657
- }
3658
- }
3659
- if (failed.length > 0) {
3660
- console.log();
3661
- M.error(import_picocolors.default.red(`Failed to install ${failed.length}`));
3662
- for (const r of failed) M.message(` ${import_picocolors.default.red("✗")} ${r.skill} → ${r.agent}: ${import_picocolors.default.dim(r.error)}`);
3663
- }
3664
- console.log();
3665
- Se(import_picocolors.default.green("Done!") + import_picocolors.default.dim(" Review skills before use; they run with full agent permissions."));
3666
- await promptForFindSkills(options, targetAgents);
3667
- }
3668
3434
  async function runAdd(args, options = {}) {
3669
3435
  const source = args[0];
3670
3436
  let installTipShown = false;
@@ -3705,17 +3471,31 @@ async function runAdd(args, options = {}) {
3705
3471
  let tempDir = null;
3706
3472
  try {
3707
3473
  const spinner = Y();
3708
- spinner.start("Parsing source...");
3709
- const parsed = parseSource(source);
3710
- 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)}` : ""}`);
3711
- const ownerRepoRaw = getOwnerRepo(parsed);
3712
- const repoPrivacyPromise = (() => {
3713
- if (!ownerRepoRaw || parsed.type !== "github") return Promise.resolve(null);
3714
- const ownerRepo = parseOwnerRepo(ownerRepoRaw);
3715
- if (!ownerRepo) return Promise.resolve(null);
3716
- return isRepoPrivate(ownerRepo.owner, ownerRepo.repo).catch(() => null);
3717
- })();
3718
- if (ownerRepoRaw?.split("/")[0]?.toLowerCase() === "openclaw" && !options.dangerouslyAcceptOpenclawRisks) {
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));
3481
+ process.exit(1);
3482
+ }
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}`));
3490
+ process.exit(1);
3491
+ }
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) {
3719
3499
  console.log();
3720
3500
  M.warn(import_picocolors.default.yellow(import_picocolors.default.bold("⚠ OpenClaw skills are unverified community submissions.")));
3721
3501
  M.message(import_picocolors.default.yellow("This source contains user-submitted skills that have not been reviewed for safety or quality."));
@@ -3725,80 +3505,21 @@ async function runAdd(args, options = {}) {
3725
3505
  Se(import_picocolors.default.red("Installation blocked"));
3726
3506
  process.exit(1);
3727
3507
  }
3728
- if (parsed.type === "well-known") {
3729
- await handleWellKnownSkills(source, parsed.url, options, spinner);
3730
- return;
3731
- }
3732
- if (parsed.skillFilter) {
3733
- options.skill = options.skill || [];
3734
- if (!options.skill.includes(parsed.skillFilter)) options.skill.push(parsed.skillFilter);
3735
- }
3736
3508
  const includeInternal = !!(options.skill && options.skill.length > 0);
3737
3509
  let skills;
3738
3510
  let blobResult = null;
3739
- if (parsed.type === "local") {
3740
- spinner.start("Validating local path...");
3741
- if (!existsSync(parsed.localPath)) {
3742
- spinner.stop(import_picocolors.default.red("Path not found"));
3743
- Se(import_picocolors.default.red(`Local path does not exist: ${parsed.localPath}`));
3744
- process.exit(1);
3745
- }
3746
- spinner.stop("Local path validated");
3747
- spinner.start("Discovering skills...");
3748
- skills = await discoverSkills(parsed.localPath, parsed.subpath, {
3749
- includeInternal,
3750
- fullDepth: options.fullDepth
3751
- });
3752
- } else if (parsed.type === "github" && !options.fullDepth) {
3753
- const BLOB_ALLOWED_OWNERS = [
3754
- "vercel",
3755
- "vercel-labs",
3756
- "heygen-com"
3757
- ];
3758
- const ownerRepo = getOwnerRepo(parsed);
3759
- const owner = ownerRepo?.split("/")[0]?.toLowerCase();
3760
- const isSelfHostedRepo = !!ownerRepo && Object.hasOwn(BLOB_ALLOWED_REPOS, ownerRepo.toLowerCase());
3761
- if (ownerRepo && owner && (isSelfHostedRepo || BLOB_ALLOWED_OWNERS.includes(owner))) {
3762
- spinner.start("Fetching skills...");
3763
- blobResult = await tryBlobInstall(ownerRepo, {
3764
- subpath: parsed.subpath,
3765
- skillFilter: parsed.skillFilter,
3766
- ref: parsed.ref,
3767
- getToken: getGitHubToken,
3768
- includeInternal
3769
- });
3770
- if (!blobResult) spinner.stop(import_picocolors.default.dim("Falling back to clone..."));
3771
- }
3772
- if (blobResult) {
3773
- skills = blobResult.skills;
3774
- spinner.stop(`Found ${import_picocolors.default.green(skills.length)} skill${skills.length > 1 ? "s" : ""}`);
3775
- } else {
3776
- spinner.start("Cloning repository...");
3777
- tempDir = await cloneRepo(parsed.url, parsed.ref);
3778
- spinner.stop("Repository cloned");
3779
- spinner.start("Discovering skills...");
3780
- skills = await discoverSkills(tempDir, parsed.subpath, {
3781
- includeInternal,
3782
- fullDepth: options.fullDepth
3783
- });
3784
- }
3785
- } else {
3786
- spinner.start("Cloning repository...");
3787
- tempDir = await cloneRepo(parsed.url, parsed.ref);
3788
- spinner.stop("Repository cloned");
3789
- spinner.start("Discovering skills...");
3790
- skills = await discoverSkills(tempDir, parsed.subpath, {
3791
- includeInternal,
3792
- fullDepth: options.fullDepth
3793
- });
3794
- }
3511
+ spinner.start("Discovering skills...");
3512
+ skills = await discoverSkills(tempDir, void 0, {
3513
+ includeInternal,
3514
+ fullDepth: options.fullDepth
3515
+ });
3795
3516
  if (skills.length === 0) {
3796
3517
  spinner.stop(import_picocolors.default.red("No skills found"));
3797
3518
  Se(import_picocolors.default.red("No valid skills found. Skills require a SKILL.md with name and description."));
3798
3519
  await cleanup(tempDir);
3799
3520
  process.exit(1);
3800
3521
  }
3801
- 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" : ""}`);
3802
3523
  if (options.list) {
3803
3524
  console.log();
3804
3525
  M.step(import_picocolors.default.bold("Available Skills"));
@@ -3896,7 +3617,8 @@ async function runAdd(args, options = {}) {
3896
3617
  selectedSkills = selected;
3897
3618
  }
3898
3619
  const ownerRepoForAudit = getOwnerRepo(parsed);
3899
- const auditPromise = ownerRepoForAudit ? fetchAuditData(ownerRepoForAudit, selectedSkills.map((s) => getSkillDisplayName(s))) : 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);
3900
3622
  let targetAgents;
3901
3623
  const validAgents = Object.keys(agents);
3902
3624
  if (options.agent?.includes("*")) {
@@ -4097,18 +3819,28 @@ async function runAdd(args, options = {}) {
4097
3819
  }
4098
3820
  console.log();
4099
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;
4100
3827
  try {
4101
- const auditData = await auditPromise;
3828
+ auditData = await auditPromise;
4102
3829
  if (auditData && ownerRepoForAudit) {
4103
- const securityLines = buildSecurityLines(auditData, selectedSkills.map((s) => ({
4104
- slug: getSkillDisplayName(s),
4105
- displayName: getSkillDisplayName(s)
4106
- })), ownerRepoForAudit);
3830
+ const securityLines = buildSecurityLines(auditData, auditSkills);
4107
3831
  if (securityLines.length > 0) Me(securityLines.join("\n"), "Security Risk Assessments");
4108
3832
  }
4109
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
+ }
4110
3839
  if (!options.yes) {
4111
- 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
+ });
4112
3844
  if (pD(confirmed) || !confirmed) {
4113
3845
  xe("Installation cancelled");
4114
3846
  await cleanup(tempDir);
@@ -4120,17 +3852,7 @@ async function runAdd(args, options = {}) {
4120
3852
  for (const skill of selectedSkills) for (const target of installTargets) {
4121
3853
  const { agent, subagent } = target;
4122
3854
  let result;
4123
- if (blobResult && "files" in skill) {
4124
- const blobSkill = skill;
4125
- result = await installBlobSkillForAgent({
4126
- installName: blobSkill.name,
4127
- files: blobSkill.files
4128
- }, agent, {
4129
- global: installGlobally,
4130
- mode: installMode,
4131
- eveSubagent: subagent
4132
- });
4133
- } else if (tempDir && skill.path === tempDir && skill.rawContent) result = await installBlobSkillForAgent({
3855
+ if (tempDir && skill.path === tempDir && skill.rawContent) result = await installBlobSkillForAgent({
4134
3856
  installName: skill.name,
4135
3857
  files: [{
4136
3858
  path: "SKILL.md",
@@ -4156,48 +3878,60 @@ async function runAdd(args, options = {}) {
4156
3878
  console.log();
4157
3879
  const successful = results.filter((r) => r.success);
4158
3880
  const failed = results.filter((r) => !r.success);
4159
- const skillFiles = {};
4160
- for (const skill of selectedSkills) if (blobResult && "repoPath" in skill) skillFiles[skill.name] = skill.repoPath;
4161
- else if (tempDir && skill.path === tempDir) skillFiles[skill.name] = "SKILL.md";
4162
- else if (tempDir && skill.path.startsWith(tempDir + sep)) skillFiles[skill.name] = skill.path.slice(tempDir.length + 1).split(sep).join("/") + "/SKILL.md";
4163
- else continue;
3881
+ const skillFiles = buildSkillFiles(selectedSkills, tempDir, blobResult);
4164
3882
  const normalizedSource = getOwnerRepo(parsed);
4165
3883
  const lockSource = getLockSource(parsed.url, normalizedSource);
4166
3884
  if (normalizedSource) if (parseOwnerRepo(normalizedSource)) {
4167
3885
  const isPrivate = await repoPrivacyPromise;
4168
- 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({
4169
3912
  event: "install",
4170
3913
  source: normalizedSource,
4171
3914
  skills: selectedSkills.map((s) => s.name).join(","),
4172
3915
  agents: targetAgents.join(","),
4173
3916
  ...installGlobally && { global: "1" },
4174
- skillFiles: JSON.stringify(skillFiles)
3917
+ skillFiles: JSON.stringify(skillFiles),
3918
+ sourceType: parsed.type
4175
3919
  });
4176
- } else track({
4177
- event: "install",
4178
- source: normalizedSource,
4179
- skills: selectedSkills.map((s) => s.name).join(","),
4180
- agents: targetAgents.join(","),
4181
- ...installGlobally && { global: "1" },
4182
- skillFiles: JSON.stringify(skillFiles)
4183
- });
3920
+ }
4184
3921
  if (successful.length > 0 && installGlobally && normalizedSource) {
4185
3922
  const successfulSkillNames = new Set(successful.map((r) => r.skill));
4186
3923
  let cachedTree;
4187
- 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);
4188
3925
  for (const skill of selectedSkills) {
4189
3926
  const skillDisplayName = getSkillDisplayName(skill);
4190
3927
  if (successfulSkillNames.has(skillDisplayName)) try {
4191
3928
  let skillFolderHash = "";
4192
3929
  const skillPathValue = skillFiles[skill.name];
4193
- if (blobResult && skillPathValue) {
4194
- const hash = getSkillFolderHashFromTree(blobResult.tree, skillPathValue);
4195
- if (hash) skillFolderHash = hash;
4196
- } else if (parsed.type === "github" && skillPathValue && cachedTree) {
3930
+ if (parsed.type === "github" && skillPathValue && cachedTree) {
4197
3931
  const hash = getSkillFolderHashFromTree(cachedTree, skillPathValue);
4198
3932
  if (hash) skillFolderHash = hash;
4199
3933
  } else if (skillPathValue && tempDir) {
4200
- const hash = await computeSkillFolderHash(join(tempDir, dirname(skillPathValue)));
3934
+ const hash = await computeSkillFolderHash(join$1(tempDir, dirname$1(skillPathValue)));
4201
3935
  if (hash) skillFolderHash = hash;
4202
3936
  }
4203
3937
  await addSkillToLock(skill.name, {
@@ -4219,7 +3953,7 @@ async function runAdd(args, options = {}) {
4219
3953
  for (const skill of selectedSkills) {
4220
3954
  const skillDisplayName = getSkillDisplayName(skill);
4221
3955
  if (successfulSkillNames.has(skillDisplayName)) try {
4222
- 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);
4223
3957
  const skillPathValue = skillFiles[skill.name];
4224
3958
  await addSkillToLocalLock(skill.name, {
4225
3959
  source: lockSource || parsed.url,
@@ -4400,18 +4134,300 @@ function parseAddOptions(args) {
4400
4134
  options
4401
4135
  };
4402
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
+ }
4403
4399
  const RESET$3 = "\x1B[0m";
4404
4400
  const BOLD$3 = "\x1B[1m";
4405
4401
  const DIM$3 = "\x1B[38;5;102m";
4406
4402
  const TEXT$2 = "\x1B[38;5;145m";
4407
4403
  const CYAN$1 = "\x1B[36m";
4408
- const SEARCH_API_BASE = process.env.SKILLS_API_URL || "https://skills.sh";
4409
- function formatInstalls(count) {
4410
- 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";
4411
4411
  if (count >= 1e6) return `${(count / 1e6).toFixed(1).replace(/\.0$/, "")}M installs`;
4412
4412
  if (count >= 1e3) return `${(count / 1e3).toFixed(1).replace(/\.0$/, "")}K installs`;
4413
4413
  return `${count} install${count === 1 ? "" : "s"}`;
4414
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
+ }
4415
4431
  const GITHUB_OWNER_PATTERN = /^[a-z0-9](?:[a-z0-9-]{0,38})$/i;
4416
4432
  function parseFindOptions(args) {
4417
4433
  const queryParts = [];
@@ -4424,7 +4440,7 @@ function parseFindOptions(args) {
4424
4440
  if (arg === "--owner") {
4425
4441
  const value = args[i + 1];
4426
4442
  if (!value || value.startsWith("-")) {
4427
- errors.push("--owner requires a GitHub owner");
4443
+ errors.push("--owner requires an owner");
4428
4444
  continue;
4429
4445
  }
4430
4446
  ownerValue = value;
@@ -4432,7 +4448,7 @@ function parseFindOptions(args) {
4432
4448
  } else if (arg.startsWith("--owner=")) {
4433
4449
  ownerValue = arg.slice(8);
4434
4450
  if (!ownerValue) {
4435
- errors.push("--owner requires a GitHub owner");
4451
+ errors.push("--owner requires an owner");
4436
4452
  continue;
4437
4453
  }
4438
4454
  } else {
@@ -4441,7 +4457,7 @@ function parseFindOptions(args) {
4441
4457
  }
4442
4458
  const owner = ownerValue.trim().toLowerCase();
4443
4459
  if (!GITHUB_OWNER_PATTERN.test(owner)) {
4444
- errors.push("--owner must be a valid GitHub owner");
4460
+ errors.push("--owner must be a valid owner");
4445
4461
  continue;
4446
4462
  }
4447
4463
  options.owner = owner;
@@ -4453,24 +4469,52 @@ function parseFindOptions(args) {
4453
4469
  };
4454
4470
  }
4455
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;
4456
4478
  try {
4457
- const params = new URLSearchParams({
4458
- q: query,
4459
- limit: "10"
4460
- });
4461
- if (owner) params.set("owner", owner);
4462
- const url = `${SEARCH_API_BASE}/api/search?${params.toString()}`;
4463
- const res = await fetch(url);
4464
- if (!res.ok) return [];
4465
- return (await res.json()).skills.map((skill) => ({
4466
- name: sanitizeMetadata(skill.name),
4467
- slug: sanitizeMetadata(skill.id),
4468
- source: sanitizeMetadata(skill.source || ""),
4469
- installs: skill.installs
4470
- })).sort((a, b) => (b.installs || 0) - (a.installs || 0));
4479
+ res = await fetch(url);
4471
4480
  } catch {
4472
- 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
+ };
4473
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
+ });
4516
+ }
4517
+ return { skills: skills.sort((a, b) => (b.installs || 0) - (a.installs || 0)) };
4474
4518
  }
4475
4519
  const HIDE_CURSOR = "\x1B[?25l";
4476
4520
  const SHOW_CURSOR = "\x1B[?25h";
@@ -4482,6 +4526,7 @@ async function runSearchPrompt(initialQuery = "", owner) {
4482
4526
  let selectedIndex = 0;
4483
4527
  let query = initialQuery;
4484
4528
  let loading = false;
4529
+ let searchError = null;
4485
4530
  let debounceTimer = null;
4486
4531
  let lastRenderedLines = 0;
4487
4532
  if (process.stdin.isTTY) process.stdin.setRawMode(true);
@@ -4497,6 +4542,7 @@ async function runSearchPrompt(initialQuery = "", owner) {
4497
4542
  lines.push("");
4498
4543
  if (!query || query.length < 2) lines.push(`${DIM$3}Start typing to search (min 2 chars)${RESET$3}`);
4499
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}`);
4500
4546
  else if (results.length === 0) lines.push(`${DIM$3}No skills found${RESET$3}`);
4501
4547
  else {
4502
4548
  const visible = results.slice(0, 8);
@@ -4504,12 +4550,23 @@ async function runSearchPrompt(initialQuery = "", owner) {
4504
4550
  const skill = visible[i];
4505
4551
  const isSelected = i === selectedIndex;
4506
4552
  const arrow = isSelected ? `${BOLD$3}>${RESET$3}` : " ";
4507
- 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}`;
4508
4555
  const source = skill.source ? ` ${DIM$3}${skill.source}${RESET$3}` : "";
4509
- const installs = formatInstalls(skill.installs);
4556
+ const installs = formatInstalls$1(skill.installs);
4510
4557
  const installsBadge = installs ? ` ${CYAN$1}${installs}${RESET$3}` : "";
4558
+ const risk = formatRiskLevel$1(skill.riskScore);
4559
+ const riskBadge = risk ? ` ${risk}` : "";
4511
4560
  const loadingIndicator = loading && i === 0 ? ` ${DIM$3}...${RESET$3}` : "";
4512
- 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
+ }
4513
4570
  }
4514
4571
  }
4515
4572
  lines.push("");
@@ -4525,6 +4582,7 @@ async function runSearchPrompt(initialQuery = "", owner) {
4525
4582
  loading = false;
4526
4583
  if (!q || q.length < 2) {
4527
4584
  results = [];
4585
+ searchError = null;
4528
4586
  selectedIndex = 0;
4529
4587
  render();
4530
4588
  return;
@@ -4534,10 +4592,13 @@ async function runSearchPrompt(initialQuery = "", owner) {
4534
4592
  const debounceMs = Math.max(150, 350 - q.length * 50);
4535
4593
  debounceTimer = setTimeout(async () => {
4536
4594
  try {
4537
- results = await searchSkillsAPI(q, owner);
4595
+ const result = await searchSkillsAPI(q, owner);
4596
+ results = result.skills;
4597
+ searchError = result.error ?? null;
4538
4598
  selectedIndex = 0;
4539
4599
  } catch {
4540
4600
  results = [];
4601
+ searchError = null;
4541
4602
  } finally {
4542
4603
  loading = false;
4543
4604
  debounceTimer = null;
@@ -4596,10 +4657,26 @@ async function runSearchPrompt(initialQuery = "", owner) {
4596
4657
  }
4597
4658
  function getOwnerRepoFromString(pkg) {
4598
4659
  const atIndex = pkg.lastIndexOf("@");
4599
- const match = (atIndex > 0 ? pkg.slice(0, atIndex) : pkg).match(/^([^/]+)\/([^/]+)$/);
4600
- if (match) return {
4601
- owner: match[1],
4602
- 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]
4603
4680
  };
4604
4681
  return null;
4605
4682
  }
@@ -4619,24 +4696,41 @@ ${DIM$3} 2) npx wittyhub add <owner/repo@skill>${RESET$3}`;
4619
4696
  return;
4620
4697
  }
4621
4698
  if (query) {
4622
- const results = await searchSkillsAPI(query, owner);
4699
+ const { skills: results, error: searchError } = await searchSkillsAPI(query, owner);
4623
4700
  track({
4624
4701
  event: "find",
4625
4702
  query,
4626
4703
  resultCount: String(results.length)
4627
4704
  });
4705
+ if (searchError) {
4706
+ console.error(`${RED}${searchError}${RESET$3}`);
4707
+ return;
4708
+ }
4628
4709
  if (results.length === 0) {
4629
4710
  const ownerSuffix = owner ? ` from owner "${owner}"` : "";
4630
4711
  console.log(`${DIM$3}No skills found for "${query}"${ownerSuffix}${RESET$3}`);
4631
4712
  return;
4632
4713
  }
4633
- 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>`);
4634
4715
  console.log();
4635
4716
  for (const skill of results.slice(0, 6)) {
4636
- const pkg = skill.source || skill.slug;
4637
- const installs = formatInstalls(skill.installs);
4638
- console.log(`${TEXT$2}${pkg}@${skill.name}${RESET$3}${installs ? ` ${CYAN$1}${installs}${RESET$3}` : ""}`);
4639
- 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}`);
4640
4734
  console.log();
4641
4735
  }
4642
4736
  return;
@@ -4659,7 +4753,7 @@ ${DIM$3} 2) npx wittyhub add <owner/repo@skill>${RESET$3}`;
4659
4753
  console.log();
4660
4754
  return;
4661
4755
  }
4662
- const pkg = selected.source || selected.slug;
4756
+ const pkg = selected.sourceUrl || selected.source || selected.slug;
4663
4757
  const skillName = selected.name;
4664
4758
  console.log();
4665
4759
  console.log(`${TEXT$2}Installing ${BOLD$3}${skillName}${RESET$3} from ${DIM$3}${pkg}${RESET$3}...`);
@@ -4676,9 +4770,156 @@ ${DIM$3} 2) npx wittyhub add <owner/repo@skill>${RESET$3}`;
4676
4770
  else console.log(`${DIM$3}Discover more skills at${RESET$3} ${TEXT$2}https://skills.sh${RESET$3}`);
4677
4771
  console.log();
4678
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
+ }
4679
4920
  const isCancelled = (value) => typeof value === "symbol";
4680
4921
  function shortenPath$1(fullPath, cwd) {
4681
- const home = homedir();
4922
+ const home = homedir$1();
4682
4923
  if (fullPath === home || fullPath.startsWith(home + sep)) return "~" + fullPath.slice(home.length);
4683
4924
  if (fullPath === cwd || fullPath.startsWith(cwd + sep)) return "." + fullPath.slice(cwd.length);
4684
4925
  return fullPath;
@@ -5033,7 +5274,7 @@ const DIM$2 = "\x1B[38;5;102m";
5033
5274
  const CYAN = "\x1B[36m";
5034
5275
  const YELLOW = "\x1B[33m";
5035
5276
  function shortenPath(fullPath, cwd) {
5036
- const home = homedir();
5277
+ const home = homedir$1();
5037
5278
  if (fullPath.startsWith(home)) return fullPath.replace(home, "~");
5038
5279
  if (fullPath.startsWith(cwd)) return "." + fullPath.slice(cwd.length);
5039
5280
  return fullPath;
@@ -5270,7 +5511,7 @@ async function removeCommand(skillNames, options) {
5270
5511
  for (const pathToCleanup of pathsToCleanup) {
5271
5512
  if (pathToCleanup === canonicalPath) continue;
5272
5513
  try {
5273
- if (await lstat(pathToCleanup).catch(() => null)) await rm(pathToCleanup, {
5514
+ if (await lstat(pathToCleanup).catch(() => null)) await rm$1(pathToCleanup, {
5274
5515
  recursive: true,
5275
5516
  force: true
5276
5517
  });
@@ -5288,7 +5529,7 @@ async function removeCommand(skillNames, options) {
5288
5529
  isStillUsed = true;
5289
5530
  break;
5290
5531
  }
5291
- if (!isStillUsed) await rm(canonicalPath, {
5532
+ if (!isStillUsed) await rm$1(canonicalPath, {
5292
5533
  recursive: true,
5293
5534
  force: true
5294
5535
  });
@@ -5936,7 +6177,7 @@ function buildUsePrompt(input) {
5936
6177
  return sections.join("\n\n") + "\n";
5937
6178
  }
5938
6179
  async function materializeUseSkill(skill) {
5939
- const tempRoot = await mkdtemp(join(tmpdir(), "skills-use-"));
6180
+ const tempRoot = await mkdtemp$1(join(tmpdir$1(), "skills-use-"));
5940
6181
  const skillDir = join(tempRoot, sanitizeName(skill.directoryName || skill.name));
5941
6182
  if (!isPathSafe(tempRoot, skillDir)) throw new Error("Invalid skill name: potential path traversal detected");
5942
6183
  await mkdir(skillDir, { recursive: true });
@@ -6163,8 +6404,8 @@ async function writeSafeFile(targetDir, filePath, contents) {
6163
6404
  const fullPath = join(targetDir, filePath);
6164
6405
  if (!isPathSafe(targetDir, fullPath)) return;
6165
6406
  await mkdir(dirname(fullPath), { recursive: true });
6166
- if (typeof contents === "string") await writeFile(fullPath, contents, "utf-8");
6167
- else await writeFile(fullPath, contents);
6407
+ if (typeof contents === "string") await writeFile$1(fullPath, contents, "utf-8");
6408
+ else await writeFile$1(fullPath, contents);
6168
6409
  }
6169
6410
  async function copySkillDirectory(src, dest) {
6170
6411
  await mkdir(dest, { recursive: true });
@@ -6224,6 +6465,308 @@ function fail(message) {
6224
6465
  process.exit(1);
6225
6466
  }
6226
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
+ }
6227
6770
  const __dirname = dirname(fileURLToPath(import.meta.url));
6228
6771
  function getVersion() {
6229
6772
  try {
@@ -6276,11 +6819,12 @@ function showBanner() {
6276
6819
  console.log();
6277
6820
  console.log(` ${DIM}$${RESET} ${TEXT}npx wittyhub experimental_install${RESET} ${DIM}Restore from skills-lock.json${RESET}`);
6278
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}`);
6279
6823
  console.log(` ${DIM}$${RESET} ${TEXT}npx wittyhub experimental_sync${RESET} ${DIM}Sync skills from node_modules${RESET}`);
6280
6824
  console.log();
6281
6825
  console.log(`${DIM}try:${RESET} npx wittyhub add vercel-labs/agent-skills`);
6282
6826
  console.log();
6283
- console.log(`Discover more skills at ${TEXT}https://skills.sh/${RESET}`);
6827
+ console.log(`Discover more skills at ${TEXT}https://skillhub.openeuler.org/${RESET}`);
6284
6828
  console.log();
6285
6829
  }
6286
6830
  function showHelp() {
@@ -6296,6 +6840,14 @@ ${BOLD}Manage Skills:${RESET}
6296
6840
  remove [skills] Remove installed skills
6297
6841
  list, ls List installed skills
6298
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.)
6299
6851
 
6300
6852
  ${BOLD}Find Options:${RESET}
6301
6853
  --owner <owner> Search only repositories from a GitHub owner
@@ -6310,7 +6862,7 @@ ${BOLD}Update Options:${RESET}
6310
6862
 
6311
6863
  ${BOLD}Project:${RESET}
6312
6864
  experimental_install Restore skills from skills-lock.json
6313
- 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/)
6314
6866
  experimental_sync Sync skills from node_modules into agent directories
6315
6867
 
6316
6868
  ${BOLD}Add Options:${RESET}
@@ -6352,12 +6904,9 @@ ${BOLD}Options:${RESET}
6352
6904
  --version, -v Show version number
6353
6905
 
6354
6906
  ${BOLD}Examples:${RESET}
6355
- ${DIM}$${RESET} wittyhub add vercel-labs/agent-skills
6356
- ${DIM}$${RESET} wittyhub use vercel-labs/agent-skills@vercel-optimize | claude
6357
- ${DIM}$${RESET} wittyhub use vercel-labs/agent-skills --skill vercel-optimize --agent claude-code
6358
- ${DIM}$${RESET} wittyhub add vercel-labs/agent-skills -g
6359
- ${DIM}$${RESET} wittyhub add vercel-labs/agent-skills --agent claude-code cursor
6360
- ${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
6361
6910
  ${DIM}$${RESET} wittyhub remove ${DIM}# interactive remove${RESET}
6362
6911
  ${DIM}$${RESET} wittyhub remove web-design ${DIM}# remove by name${RESET}
6363
6912
  ${DIM}$${RESET} wittyhub rm --global frontend-design
@@ -6376,7 +6925,45 @@ ${BOLD}Examples:${RESET}
6376
6925
  ${DIM}$${RESET} wittyhub experimental_sync ${DIM}# sync from node_modules${RESET}
6377
6926
  ${DIM}$${RESET} wittyhub experimental_sync -y ${DIM}# sync without prompts${RESET}
6378
6927
 
6379
- 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
6380
6967
  `);
6381
6968
  }
6382
6969
  function showRemoveHelp() {
@@ -6406,24 +6993,25 @@ ${BOLD}Examples:${RESET}
6406
6993
  ${DIM}$${RESET} wittyhub remove --all ${DIM}# remove all skills${RESET}
6407
6994
  ${DIM}$${RESET} wittyhub remove --skill '*' -a cursor ${DIM}# remove all skills from cursor${RESET}
6408
6995
 
6409
- Discover more skills at ${TEXT}https://skills.sh/${RESET}
6996
+ Discover more skills at ${TEXT}https://skillhub.openeuler.org/${RESET}
6410
6997
  `);
6411
6998
  }
6412
6999
  function runInit(args) {
6413
7000
  const cwd = process.cwd();
6414
- const skillName = args[0] || basename(cwd);
6415
- const hasName = args[0] !== void 0;
6416
- const skillDir = hasName ? join(cwd, skillName) : cwd;
7001
+ const skillName = args[0] ?? "my-skill";
7002
+ const skillDir = join(cwd, skillName);
6417
7003
  const skillFile = join(skillDir, "SKILL.md");
6418
- const displayPath = hasName ? `${skillName}/SKILL.md` : "SKILL.md";
7004
+ const displayPath = `${skillName}/SKILL.md`;
6419
7005
  if (existsSync(skillFile)) {
6420
7006
  console.log(`${TEXT}Skill already exists at ${DIM}${displayPath}${RESET}`);
6421
7007
  return;
6422
7008
  }
6423
- if (hasName) mkdirSync(skillDir, { recursive: true });
7009
+ mkdirSync(skillDir, { recursive: true });
6424
7010
  writeFileSync(skillFile, `---
6425
7011
  name: ${skillName}
6426
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
6427
7015
  ---
6428
7016
 
6429
7017
  # ${skillName}
@@ -6448,12 +7036,12 @@ Describe when this skill should be used.
6448
7036
  console.log(`${DIM}Next steps:${RESET}`);
6449
7037
  console.log(` 1. Edit ${TEXT}${displayPath}${RESET} to define your skill instructions`);
6450
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)`);
6451
7040
  console.log();
6452
7041
  console.log(`${DIM}Publishing:${RESET}`);
6453
- console.log(` ${DIM}GitHub:${RESET} Push to a repo, then ${TEXT}npx wittyhub add <owner>/<repo>${RESET}`);
6454
- 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}`);
6455
7043
  console.log();
6456
- 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}`);
6457
7045
  console.log();
6458
7046
  }
6459
7047
  async function main() {
@@ -6475,6 +7063,10 @@ async function main() {
6475
7063
  await runFind(restArgs);
6476
7064
  break;
6477
7065
  case "init":
7066
+ if (restArgs.includes("--help") || restArgs.includes("-h")) {
7067
+ showInitHelp();
7068
+ break;
7069
+ }
6478
7070
  if (!inAgent) showLogo();
6479
7071
  console.log();
6480
7072
  runInit(restArgs);
@@ -6517,6 +7109,24 @@ async function main() {
6517
7109
  case "ls":
6518
7110
  await runList(restArgs);
6519
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;
6520
7130
  case "check":
6521
7131
  case "update":
6522
7132
  case "upgrade":