wittyhub 0.0.3 → 0.0.5

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,19 @@ 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 { existsSync as existsSync$1, statSync } from "node:fs";
18
+ import { homedir, tmpdir } from "node:os";
19
+ import { dirname as dirname$1, isAbsolute as isAbsolute$1, join as join$1, resolve as resolve$1, sep as sep$1 } from "node:path";
20
+ import { homedir as homedir$1, platform, tmpdir as tmpdir$1 } from "os";
21
+ import { parse } from "yaml";
16
22
  import * as readline from "readline";
17
23
  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";
24
+ import { promisify as promisify$1 } from "util";
25
+ import { execFile as execFile$1, execSync, spawn, spawnSync } from "child_process";
26
+ 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
27
  import { createHash as createHash$1 } from "crypto";
23
28
  import { gunzipSync, inflateRawSync } from "node:zlib";
24
29
  var import_picocolors = /* @__PURE__ */ __toESM(require_picocolors(), 1);
@@ -55,15 +60,6 @@ function parseOwnerRepo(ownerRepo) {
55
60
  };
56
61
  return null;
57
62
  }
58
- async function isRepoPrivate(owner, repo) {
59
- try {
60
- const res = await fetch(`https://api.github.com/repos/${owner}/${repo}`);
61
- if (!res.ok) return null;
62
- return (await res.json()).private === true;
63
- } catch {
64
- return null;
65
- }
66
- }
67
63
  function sanitizeSubpath(subpath) {
68
64
  const segments = subpath.replace(/\\/g, "/").split("/");
69
65
  for (const segment of segments) if (segment === "..") throw new Error(`Unsafe subpath: "${subpath}" contains path traversal segments. Subpaths must not contain ".." components.`);
@@ -251,6 +247,156 @@ function isWellKnownUrl(input) {
251
247
  return false;
252
248
  }
253
249
  }
250
+ const CLI_CONFIG_PATH = join(homedir$1(), ".config", "wittyhub", "cli.yaml");
251
+ function loadCliConfig() {
252
+ if (!existsSync(CLI_CONFIG_PATH)) return {};
253
+ try {
254
+ const parsed = parse(readFileSync(CLI_CONFIG_PATH, "utf-8"));
255
+ if (!parsed || typeof parsed !== "object") return {};
256
+ return parsed;
257
+ } catch {
258
+ return {};
259
+ }
260
+ }
261
+ const cliConfig = loadCliConfig();
262
+ const API_URL = typeof cliConfig.api_url === "string" && cliConfig.api_url.trim() ? cliConfig.api_url.trim().replace(/\/$/, "") : process.env.SKILLS_API_URL ? `${process.env.SKILLS_API_URL.replace(/\/$/, "")}/api/v1` : "https://skillhub.openeuler.org/api/v1";
263
+ const SEARCH_URL = `${API_URL}/index/search`;
264
+ const TELEMETRY_URL = `${API_URL}/skills/telemetry`;
265
+ const AUDIT_URL = `${API_URL}/skills/{skill_id}/audit`;
266
+ const GET_URL = `${API_URL}/skills/{skill_id}`;
267
+ const LIST_URL = `${API_URL}/skills/`;
268
+ const DOWNLOAD_URL = `${API_URL}/skills/{skill_id}/download`;
269
+ let cliVersion = null;
270
+ let detectedAgentName = null;
271
+ function setDetectedAgent(agentName) {
272
+ detectedAgentName = agentName;
273
+ }
274
+ function isCI() {
275
+ return !!(process.env.CI || process.env.GITHUB_ACTIONS || process.env.GITLAB_CI || process.env.CIRCLECI || process.env.TRAVIS || process.env.BUILDKITE || process.env.JENKINS_URL || process.env.TEAMCITY_VERSION);
276
+ }
277
+ function isEnabled() {
278
+ return !process.env.DISABLE_TELEMETRY && !process.env.DO_NOT_TRACK;
279
+ }
280
+ function setVersion(version) {
281
+ cliVersion = version;
282
+ }
283
+ function slugifyTelemetryValue(value) {
284
+ const lowered = value.trim().toLowerCase();
285
+ if (!lowered) return "";
286
+ let normalized = lowered.replace(/[^a-z0-9._-]+/g, "-");
287
+ normalized = normalized.replace(/-{2,}/g, "-");
288
+ return normalized.replace(/^-|-$/g, "");
289
+ }
290
+ async function fetchAuditBySkillId(skillId, timeoutMs = 15e3) {
291
+ const controller = new AbortController();
292
+ const timeout = setTimeout(() => controller.abort(), timeoutMs);
293
+ try {
294
+ const response = await fetch(AUDIT_URL.replace("{skill_id}", skillId), { signal: controller.signal });
295
+ if (response.ok) {
296
+ const data = await response.json();
297
+ if (!data.error) {
298
+ const auditData = data.data || data;
299
+ return { [skillId.split("/").pop() || skillId]: {
300
+ risk_level: auditData.risk_level ?? "unknown",
301
+ risk_score: auditData.risk_score ?? null,
302
+ risk_signals: auditData.risk_signals ?? [],
303
+ audited_at: auditData.audited_at ?? null
304
+ } };
305
+ }
306
+ }
307
+ return null;
308
+ } catch {
309
+ return null;
310
+ } finally {
311
+ clearTimeout(timeout);
312
+ }
313
+ }
314
+ const pendingTelemetry = [];
315
+ function track(data) {
316
+ if (!isEnabled()) return;
317
+ try {
318
+ const params = new URLSearchParams();
319
+ if (cliVersion) params.set("v", cliVersion);
320
+ if (isCI()) params.set("ci", "1");
321
+ if (detectedAgentName) params.set("agent", detectedAgentName);
322
+ for (const [key, value] of Object.entries(data)) if (value !== void 0 && value !== null) params.set(key, String(value));
323
+ const p = fetch(`${TELEMETRY_URL}?${params.toString()}`).catch(() => {}).then(() => {});
324
+ pendingTelemetry.push(p);
325
+ } catch {}
326
+ }
327
+ async function flushTelemetry(timeoutMs = 5e3) {
328
+ if (pendingTelemetry.length === 0) return;
329
+ const timeout = new Promise((resolve) => setTimeout(resolve, timeoutMs));
330
+ await Promise.race([Promise.all(pendingTelemetry), timeout]);
331
+ }
332
+ const SOURCE_TYPE_PREFIXES = [
333
+ "github",
334
+ "gitcode",
335
+ "gitlab",
336
+ "gitee"
337
+ ];
338
+ function normalizeSkillId(input) {
339
+ const trimmed = input.trim();
340
+ const colonIdx = trimmed.indexOf(":");
341
+ if (colonIdx > 0) {
342
+ const prefix = trimmed.slice(0, colonIdx).toLowerCase();
343
+ if (SOURCE_TYPE_PREFIXES.includes(prefix)) return trimmed;
344
+ }
345
+ return `github:${trimmed}`;
346
+ }
347
+ function isRepoLevelRef(input) {
348
+ const trimmed = input.trim();
349
+ const colonIdx = trimmed.indexOf(":");
350
+ if (colonIdx <= 0) return false;
351
+ const prefix = trimmed.slice(0, colonIdx).toLowerCase();
352
+ if (!SOURCE_TYPE_PREFIXES.includes(prefix)) return false;
353
+ return trimmed.slice(colonIdx + 1).split("/").length === 2;
354
+ }
355
+ const HOST_TO_SOURCE_TYPE = {
356
+ "github.com": "github",
357
+ "gitcode.com": "gitcode",
358
+ "gitlab.com": "gitlab",
359
+ "gitee.com": "gitee"
360
+ };
361
+ function repoUrlToRef(input) {
362
+ const trimmed = input.trim().replace(/\.git$/, "");
363
+ if (!trimmed) return null;
364
+ const scpLike = trimmed.match(/^git@([^:]+):(.+)$/);
365
+ if (scpLike) return refFromHostAndPath(scpLike[1], scpLike[2]);
366
+ const sshLike = trimmed.match(/^ssh:\/\/[^@]+@([^/]+)\/(.+)$/);
367
+ if (sshLike) return refFromHostAndPath(sshLike[1], sshLike[2]);
368
+ const urlLike = trimmed.match(/^[a-z][a-z0-9+.-]*:\/\/([^/]+)\/(.+)$/i);
369
+ if (urlLike) return refFromHostAndPath(urlLike[1], urlLike[2]);
370
+ return null;
371
+ }
372
+ function refFromHostAndPath(host, path) {
373
+ const sourceType = HOST_TO_SOURCE_TYPE[host.toLowerCase()];
374
+ if (!sourceType) return null;
375
+ const parts = path.replace(/\/+$/, "").split("/");
376
+ if (parts.length !== 2 || parts.some((p) => !p)) return null;
377
+ return `${sourceType}:${parts.join("/")}`;
378
+ }
379
+ async function listSkillsByRepo(sourceType, ownerRepo) {
380
+ const slugifiedOwnerRepo = ownerRepo.split("/").map(slugifyTelemetryValue).join("/");
381
+ const url = `${LIST_URL}?${new URLSearchParams({
382
+ source_type: sourceType,
383
+ repo: slugifiedOwnerRepo,
384
+ limit: "100"
385
+ }).toString()}`;
386
+ try {
387
+ const res = await fetch(url);
388
+ if (!res.ok) return [];
389
+ const raw = await res.json();
390
+ return ((raw.data || raw).skills ?? []).filter((skill) => skill && typeof skill.skill_id === "string" && skill.skill_id).map((skill) => ({
391
+ skill_id: skill.skill_id,
392
+ name: skill.name ?? "",
393
+ description: skill.description ?? "",
394
+ source_url: skill.source_url ?? ""
395
+ }));
396
+ } catch {
397
+ return [];
398
+ }
399
+ }
254
400
  const CSI_RE = /\x1b\[[\x30-\x3f]*[\x20-\x2f]*[\x40-\x7e]/g;
255
401
  const OSC_RE = /\x1b\][\s\S]*?(?:\x07|\x1b\\)/g;
256
402
  const DCS_PM_APC_RE = /\x1b[P^_][\s\S]*?(?:\x1b\\)/g;
@@ -453,7 +599,7 @@ const CLONE_TIMEOUT_MS = (() => {
453
599
  const parsed = Number.parseInt(raw, 10);
454
600
  return Number.isFinite(parsed) && parsed > 0 ? parsed : DEFAULT_CLONE_TIMEOUT_MS;
455
601
  })();
456
- const execFileAsync = promisify(execFile);
602
+ const execFileAsync$1 = promisify$1(execFile$1);
457
603
  var GitCloneError = class extends Error {
458
604
  url;
459
605
  isTimeout;
@@ -513,22 +659,22 @@ function isAuthFailure(message) {
513
659
  function createGitClient(extraEnv) {
514
660
  return esm_default({
515
661
  timeout: { block: CLONE_TIMEOUT_MS },
516
- env: {
517
- ...process.env,
518
- GIT_TERMINAL_PROMPT: "0",
519
- GIT_LFS_SKIP_SMUDGE: "1",
520
- ...extraEnv
521
- },
522
662
  config: [
523
663
  "filter.lfs.required=false",
524
664
  "filter.lfs.smudge=",
525
665
  "filter.lfs.clean=",
526
666
  "filter.lfs.process="
527
- ]
667
+ ],
668
+ unsafe: { allowUnsafeFilter: true }
669
+ }).env({
670
+ ...process.env,
671
+ GIT_TERMINAL_PROMPT: "0",
672
+ GIT_LFS_SKIP_SMUDGE: "1",
673
+ ...extraEnv
528
674
  });
529
675
  }
530
676
  async function resetTempDir(dir) {
531
- await rm(dir, {
677
+ await rm$1(dir, {
532
678
  recursive: true,
533
679
  force: true
534
680
  }).catch(() => {});
@@ -537,7 +683,7 @@ async function resetTempDir(dir) {
537
683
  async function tryGhClone(repo, tempDir, ref) {
538
684
  let cloneTarget = repo.slug;
539
685
  try {
540
- const { stdout, stderr } = await execFileAsync("gh", [
686
+ const { stdout, stderr } = await execFileAsync$1("gh", [
541
687
  "auth",
542
688
  "status",
543
689
  "-h",
@@ -554,7 +700,7 @@ async function tryGhClone(repo, tempDir, ref) {
554
700
  } catch {
555
701
  return false;
556
702
  }
557
- await execFileAsync("gh", [
703
+ await execFileAsync$1("gh", [
558
704
  "repo",
559
705
  "clone",
560
706
  cloneTarget,
@@ -580,7 +726,7 @@ function buildGitHubAuthError(url, repo, message) {
580
726
  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
727
  }
582
728
  async function cloneRepo(url, ref) {
583
- const tempDir = await mkdtemp(join(tmpdir(), "skills-"));
729
+ const tempDir = await mkdtemp$1(join(tmpdir$1(), "skills-"));
584
730
  const cloneOptions = ref ? [
585
731
  "--depth",
586
732
  "1",
@@ -596,7 +742,7 @@ async function cloneRepo(url, ref) {
596
742
  const isTimeout = errorMessage.includes("block timeout") || errorMessage.includes("timed out");
597
743
  const isAuthError = isAuthFailure(errorMessage);
598
744
  if (isTimeout) {
599
- await rm(tempDir, {
745
+ await rm$1(tempDir, {
600
746
  recursive: true,
601
747
  force: true
602
748
  }).catch(() => {});
@@ -613,7 +759,7 @@ async function cloneRepo(url, ref) {
613
759
  return tempDir;
614
760
  } catch {}
615
761
  }
616
- await rm(tempDir, {
762
+ await rm$1(tempDir, {
617
763
  recursive: true,
618
764
  force: true
619
765
  }).catch(() => {});
@@ -623,9 +769,9 @@ async function cloneRepo(url, ref) {
623
769
  }
624
770
  async function cleanupTempDir(dir) {
625
771
  const normalizedDir = normalize(resolve(dir));
626
- const normalizedTmpDir = normalize(resolve(tmpdir()));
772
+ const normalizedTmpDir = normalize(resolve(tmpdir$1()));
627
773
  if (!normalizedDir.startsWith(normalizedTmpDir + sep) && normalizedDir !== normalizedTmpDir) throw new Error("Attempted to clean up directory outside of temp directory");
628
- await rm(dir, {
774
+ await rm$1(dir, {
629
775
  recursive: true,
630
776
  force: true
631
777
  });
@@ -731,7 +877,7 @@ async function writeLocalLock(lock, cwd) {
731
877
  version: lock.version,
732
878
  skills: sortedSkills
733
879
  };
734
- await writeFile(lockPath, JSON.stringify(sorted, null, 2) + "\n", "utf-8");
880
+ await writeFile$1(lockPath, JSON.stringify(sorted, null, 2) + "\n", "utf-8");
735
881
  }
736
882
  async function computeSkillFolderHash(skillDir) {
737
883
  const files = [];
@@ -827,13 +973,14 @@ async function parseSkillMd(skillMdPath, options) {
827
973
  const { data } = parseFrontmatter(content);
828
974
  if (!data.name || !data.description) return null;
829
975
  if (typeof data.name !== "string" || typeof data.description !== "string") return null;
830
- if (data.metadata?.internal === true && !shouldInstallInternalSkills() && !options?.includeInternal) return null;
976
+ const metadata = typeof data.metadata === "object" && data.metadata !== null ? data.metadata : void 0;
977
+ if (metadata?.internal === true && !shouldInstallInternalSkills() && !options?.includeInternal) return null;
831
978
  return {
832
979
  name: sanitizeMetadata(data.name),
833
980
  description: sanitizeMetadata(data.description),
834
981
  path: dirname(skillMdPath),
835
982
  rawContent: content,
836
- metadata: data.metadata
983
+ metadata
837
984
  };
838
985
  } catch {
839
986
  return null;
@@ -950,7 +1097,7 @@ function filterSkills(skills, inputNames) {
950
1097
  return normalizedInputs.some((input) => input === name || input === displayName);
951
1098
  });
952
1099
  }
953
- const home = homedir();
1100
+ const home = homedir$1();
954
1101
  const configHome = xdgConfig ?? join(home, ".config");
955
1102
  const codexHome = process.env.CODEX_HOME?.trim() || join(home, ".codex");
956
1103
  const claudeHome = process.env.CLAUDE_CONFIG_DIR?.trim() || join(home, ".claude");
@@ -1679,7 +1826,7 @@ async function isDirEntryOrSymlinkToDir(entry, entryPath) {
1679
1826
  }
1680
1827
  }
1681
1828
  function getCanonicalSkillsDir(global, cwd) {
1682
- return join(global ? homedir() : cwd || process.cwd(), AGENTS_DIR$1, SKILLS_SUBDIR);
1829
+ return join(global ? homedir$1() : cwd || process.cwd(), AGENTS_DIR$1, SKILLS_SUBDIR);
1683
1830
  }
1684
1831
  function getEveSubagentSkillsDir(subagent, cwd) {
1685
1832
  return join(cwd || process.cwd(), EVE_SUBAGENTS_DIR, sanitizeName(subagent), "skills");
@@ -1688,7 +1835,7 @@ function getAgentBaseDir(agentType, global, cwd, eveSubagent) {
1688
1835
  if (isUniversalAgent(agentType)) return getCanonicalSkillsDir(global, cwd);
1689
1836
  if (agentType === "eve" && eveSubagent) return getEveSubagentSkillsDir(eveSubagent, cwd);
1690
1837
  const agent = agents[agentType];
1691
- const baseDir = global ? homedir() : cwd || process.cwd();
1838
+ const baseDir = global ? homedir$1() : cwd || process.cwd();
1692
1839
  if (global) {
1693
1840
  if (agent.globalSkillsDir === void 0) return join(baseDir, agent.skillsDir);
1694
1841
  return agent.globalSkillsDir;
@@ -1700,7 +1847,7 @@ function resolveSymlinkTarget(linkPath, linkTarget) {
1700
1847
  }
1701
1848
  async function cleanAndCreateDirectory(path) {
1702
1849
  try {
1703
- await rm(path, {
1850
+ await rm$1(path, {
1704
1851
  recursive: true,
1705
1852
  force: true
1706
1853
  });
@@ -1727,11 +1874,11 @@ async function createSymlink(target, linkPath) {
1727
1874
  try {
1728
1875
  if ((await lstat(linkPath)).isSymbolicLink()) {
1729
1876
  if (resolveSymlinkTarget(linkPath, await readlink(linkPath)) === resolvedTarget) return true;
1730
- await rm(linkPath);
1731
- } else await rm(linkPath, { recursive: true });
1877
+ await rm$1(linkPath);
1878
+ } else await rm$1(linkPath, { recursive: true });
1732
1879
  } catch (err) {
1733
1880
  if (err && typeof err === "object" && "code" in err && err.code === "ELOOP") try {
1734
- await rm(linkPath, { force: true });
1881
+ await rm$1(linkPath, { force: true });
1735
1882
  } catch {}
1736
1883
  }
1737
1884
  const linkDir = dirname(linkPath);
@@ -1872,7 +2019,7 @@ async function copyDirectory(src, dest, agentType) {
1872
2019
  if (entry.isDirectory()) await copyDirectory(srcPath, destPath, agentType);
1873
2020
  else try {
1874
2021
  if (agentType === "eve" && entry.name.toLowerCase() === "skill.md") {
1875
- await writeFile(destPath, stripIgnoredEveFrontmatter(await readFile(srcPath, "utf-8")));
2022
+ await writeFile$1(destPath, stripIgnoredEveFrontmatter(await readFile(srcPath, "utf-8")));
1876
2023
  return;
1877
2024
  }
1878
2025
  await cp(srcPath, destPath, {
@@ -1927,88 +2074,6 @@ function getCanonicalPath(skillName, options = {}) {
1927
2074
  if (!isPathSafe$1(canonicalBase, canonicalPath)) throw new Error("Invalid skill name: potential path traversal detected");
1928
2075
  return canonicalPath;
1929
2076
  }
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
2077
  async function installBlobSkillForAgent(skill, agentType, options = {}) {
2013
2078
  const agent = agents[agentType];
2014
2079
  const isGlobal = options.global ?? false;
@@ -2033,11 +2098,11 @@ async function installBlobSkillForAgent(skill, agentType, options = {}) {
2033
2098
  };
2034
2099
  try {
2035
2100
  await mkdir(agentBase, { recursive: true });
2036
- await rm(flatSkillPath, {
2101
+ await rm$1(flatSkillPath, {
2037
2102
  recursive: true,
2038
2103
  force: true
2039
2104
  });
2040
- await writeFile(flatSkillPath, getEveFlatSkillMarkdown(skill.files), "utf-8");
2105
+ await writeFile$1(flatSkillPath, getEveFlatSkillMarkdown(skill.files), "utf-8");
2041
2106
  return {
2042
2107
  success: true,
2043
2108
  path: flatSkillPath,
@@ -2073,7 +2138,7 @@ async function installBlobSkillForAgent(skill, agentType, options = {}) {
2073
2138
  if (!isPathSafe$1(targetDir, fullPath)) continue;
2074
2139
  const parentDir = dirname(fullPath);
2075
2140
  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");
2141
+ await writeFile$1(fullPath, agentType === "eve" && basename(file.path).toLowerCase() === "skill.md" ? stripIgnoredEveFrontmatter(file.contents) : file.contents, "utf-8");
2077
2142
  }
2078
2143
  }
2079
2144
  try {
@@ -2260,53 +2325,6 @@ async function listInstalledSkills(options = {}) {
2260
2325
  } catch {}
2261
2326
  return Array.from(skillsMap.values());
2262
2327
  }
2263
- const CLI_CONFIG_PATH = join(homedir(), ".config", "wittyhub", "cli.yaml");
2264
- function loadCliConfig() {
2265
- if (!existsSync(CLI_CONFIG_PATH)) return {};
2266
- try {
2267
- const parsed = parse(readFileSync(CLI_CONFIG_PATH, "utf-8"));
2268
- if (!parsed || typeof parsed !== "object") return {};
2269
- return parsed;
2270
- } catch {
2271
- return {};
2272
- }
2273
- }
2274
- const DEFAULT_TELEMETRY_URL = "http://localhost:8080/api/v1/skills/telemetry";
2275
- const cliConfig = loadCliConfig();
2276
- const TELEMETRY_URL = typeof cliConfig.telemetry_url === "string" && cliConfig.telemetry_url.trim() ? cliConfig.telemetry_url.trim() : DEFAULT_TELEMETRY_URL;
2277
- typeof cliConfig.audit_url === "string" && cliConfig.audit_url.trim() && cliConfig.audit_url.trim();
2278
- let cliVersion = null;
2279
- let detectedAgentName = null;
2280
- function setDetectedAgent(agentName) {
2281
- detectedAgentName = agentName;
2282
- }
2283
- function isCI() {
2284
- return !!(process.env.CI || process.env.GITHUB_ACTIONS || process.env.GITLAB_CI || process.env.CIRCLECI || process.env.TRAVIS || process.env.BUILDKITE || process.env.JENKINS_URL || process.env.TEAMCITY_VERSION);
2285
- }
2286
- function isEnabled() {
2287
- return !process.env.DISABLE_TELEMETRY && !process.env.DO_NOT_TRACK;
2288
- }
2289
- function setVersion(version) {
2290
- cliVersion = version;
2291
- }
2292
- const pendingTelemetry = [];
2293
- function track(data) {
2294
- if (!isEnabled()) return;
2295
- try {
2296
- const params = new URLSearchParams();
2297
- if (cliVersion) params.set("v", cliVersion);
2298
- if (isCI()) params.set("ci", "1");
2299
- if (detectedAgentName) params.set("agent", detectedAgentName);
2300
- for (const [key, value] of Object.entries(data)) if (value !== void 0 && value !== null) params.set(key, String(value));
2301
- const p = fetch(`${TELEMETRY_URL}?${params.toString()}`).catch(() => {}).then(() => {});
2302
- pendingTelemetry.push(p);
2303
- } catch {}
2304
- }
2305
- async function flushTelemetry(timeoutMs = 5e3) {
2306
- if (pendingTelemetry.length === 0) return;
2307
- const timeout = new Promise((resolve) => setTimeout(resolve, timeoutMs));
2308
- await Promise.race([Promise.all(pendingTelemetry), timeout]);
2309
- }
2310
2328
  var import_dist = require_dist();
2311
2329
  let cachedResult = null;
2312
2330
  const agentNameToType = {
@@ -2806,7 +2824,7 @@ const CURRENT_VERSION = 3;
2806
2824
  function getSkillLockPath() {
2807
2825
  const xdgStateHome = process.env.XDG_STATE_HOME;
2808
2826
  if (xdgStateHome) return join(xdgStateHome, "skills", LOCK_FILE);
2809
- return join(homedir(), AGENTS_DIR, LOCK_FILE);
2827
+ return join(homedir$1(), AGENTS_DIR, LOCK_FILE);
2810
2828
  }
2811
2829
  async function readSkillLock() {
2812
2830
  const lockPath = getSkillLockPath();
@@ -2823,7 +2841,7 @@ async function readSkillLock() {
2823
2841
  async function writeSkillLock(lock) {
2824
2842
  const lockPath = getSkillLockPath();
2825
2843
  await mkdir(dirname(lockPath), { recursive: true });
2826
- await writeFile(lockPath, JSON.stringify(lock, null, 2), "utf-8");
2844
+ await writeFile$1(lockPath, JSON.stringify(lock, null, 2), "utf-8");
2827
2845
  }
2828
2846
  let _ghWarningShown = false;
2829
2847
  function getGitHubToken() {
@@ -3160,65 +3178,153 @@ async function tryBlobInstall(ownerRepo, options = {}) {
3160
3178
  tree
3161
3179
  };
3162
3180
  }
3163
- var version$1 = "0.0.3";
3181
+ var version$1 = "0.0.5";
3164
3182
  const isCancelled$1 = (value) => typeof value === "symbol";
3165
- async function isSourcePrivate(source) {
3166
- const ownerRepo = parseOwnerRepo(source);
3167
- if (!ownerRepo) return false;
3168
- return isRepoPrivate(ownerRepo.owner, ownerRepo.repo);
3169
- }
3170
3183
  function getLockSource(parsedUrl, normalizedSource) {
3171
3184
  return parsedUrl.startsWith("git@") || parsedUrl.startsWith("ssh://") ? parsedUrl : normalizedSource;
3172
3185
  }
3173
3186
  function initTelemetry(version) {
3174
3187
  setVersion(version);
3175
3188
  }
3176
- function riskLabel(risk) {
3189
+ const execFileAsync = promisify(execFile);
3190
+ function normalizeRepoRef(input) {
3191
+ let s = input.trim().toLowerCase();
3192
+ s = s.replace(/^(https?:\/\/|git:\/\/|ssh:\/\/)/, "");
3193
+ s = s.replace(/^git@([^:]+):/, "$1/");
3194
+ s = s.replace(/\.git$/, "");
3195
+ s = s.replace(/\/+$/, "");
3196
+ return s;
3197
+ }
3198
+ function looksLikeLocalPath(source) {
3199
+ const s = source.trim();
3200
+ if (!s) return false;
3201
+ if (isAbsolute$1(s)) return true;
3202
+ if (s.startsWith("./") || s.startsWith("../") || s === "." || s === "..") return true;
3203
+ if (s.startsWith("~/")) return true;
3204
+ if (!s.includes(":") && !s.includes("/") && existsSync$1(resolve$1(process.cwd(), s))) return statSync(resolve$1(process.cwd(), s)).isDirectory();
3205
+ return false;
3206
+ }
3207
+ function expandLocalPath(source) {
3208
+ const s = source.trim();
3209
+ if (s.startsWith("~/")) return join$1(homedir(), s.slice(2));
3210
+ return resolve$1(process.cwd(), s);
3211
+ }
3212
+ function isDirectory(path) {
3213
+ return statSync(path).isDirectory();
3214
+ }
3215
+ function looksLikeRepoSource(source) {
3216
+ const s = source.trim();
3217
+ if (s.startsWith("http://") || s.startsWith("https://")) return true;
3218
+ if (s.startsWith("git@") || s.startsWith("ssh://") || s.startsWith("git://")) return true;
3219
+ return (s.match(/\//g) || []).length <= 1 && !s.includes(":") && !/\s/.test(s);
3220
+ }
3221
+ async function resolveSkillId(source, skillName) {
3222
+ if (isRepoLevelRef(source)) {
3223
+ const [sourceType, ownerRepo] = source.split(":", 2);
3224
+ const skills = await listSkillsByRepo(sourceType, ownerRepo);
3225
+ if (skills.length === 0) throw new Error(`仓库 "${source}" 下未找到已收录的技能。`);
3226
+ return skills.map((s) => s.skill_id);
3227
+ }
3228
+ if (!looksLikeRepoSource(source)) return normalizeSkillId(source);
3229
+ if (!skillName || !skillName.trim()) throw new Error(`源 "${source}" 看起来是仓库地址,需要同时传入 --skill <skill_name> 才能定位具体技能。`);
3230
+ const searchUrl = `${SEARCH_URL}?${new URLSearchParams({
3231
+ q: skillName.trim(),
3232
+ limit: "20",
3233
+ mode: "text",
3234
+ scope: "summary"
3235
+ }).toString()}`;
3236
+ let res;
3237
+ try {
3238
+ res = await fetch(searchUrl);
3239
+ } catch {
3240
+ throw new Error(`无法连接搜索服务: ${SEARCH_URL}`);
3241
+ }
3242
+ if (!res.ok) throw new Error(`搜索服务返回 HTTP ${res.status}`);
3243
+ const payload = await res.json();
3244
+ const results = payload.data?.results ?? payload.results ?? [];
3245
+ if (results.length === 0) throw new Error(`未找到名称匹配 "${skillName}" 的技能。`);
3246
+ const wantedRef = normalizeRepoRef(source);
3247
+ const match = results.find((r) => {
3248
+ const ref = normalizeRepoRef(r.source_url);
3249
+ return ref === wantedRef || ref.startsWith(`${wantedRef}/`);
3250
+ }) ?? results[0];
3251
+ if (!match) throw new Error(`未找到名称匹配 "${skillName}" 的技能。`);
3252
+ return match.skill_id;
3253
+ }
3254
+ async function downloadAndExtractSkill(skillId, targetDir, version) {
3255
+ let downloadUrl = DOWNLOAD_URL.replace("{skill_id}", skillId);
3256
+ if (version) downloadUrl += `?version=${encodeURIComponent(version)}`;
3257
+ const response = await fetch(downloadUrl);
3258
+ if (!response.ok) {
3259
+ const detail = await response.json().then((data) => typeof data?.detail === "string" && data.detail ? `: ${data.detail}` : "").catch(() => "");
3260
+ throw new Error(`HTTP ${response.status} ${response.statusText}${detail}`);
3261
+ }
3262
+ const tempDir = targetDir ?? await mkdtemp(join$1(tmpdir(), "skills-"));
3263
+ const zipPath = join$1(tempDir, ".skill.zip");
3264
+ try {
3265
+ await writeFile(zipPath, Buffer.from(await response.arrayBuffer()));
3266
+ try {
3267
+ await execFileAsync("unzip", [
3268
+ "-q",
3269
+ zipPath,
3270
+ "-d",
3271
+ tempDir
3272
+ ]);
3273
+ } catch {
3274
+ throw new Error("Failed to extract skill archive. Ensure the 'unzip' command is available on PATH.");
3275
+ }
3276
+ } finally {
3277
+ await rm(zipPath, { force: true });
3278
+ }
3279
+ return tempDir;
3280
+ }
3281
+ function riskLabel$1(risk) {
3177
3282
  switch (risk) {
3178
- case "critical": return import_picocolors.default.red(import_picocolors.default.bold("Critical Risk"));
3179
- case "high": return import_picocolors.default.red("High Risk");
3180
- case "medium": return import_picocolors.default.yellow("Med Risk");
3181
- case "low": return import_picocolors.default.green("Low Risk");
3283
+ case "critical": return import_picocolors.default.red(import_picocolors.default.bold("Critical"));
3284
+ case "high": return import_picocolors.default.red("High");
3285
+ case "medium": return import_picocolors.default.yellow("Med");
3286
+ case "low": return import_picocolors.default.green("Low");
3182
3287
  case "safe": return import_picocolors.default.green("Safe");
3183
- default: return import_picocolors.default.dim("--");
3288
+ default: return import_picocolors.default.dim("Unknown");
3184
3289
  }
3185
3290
  }
3186
- function socketLabel(audit) {
3187
- if (!audit) return import_picocolors.default.dim("--");
3188
- const count = audit.alerts ?? 0;
3189
- return count > 0 ? import_picocolors.default.red(`${count} alert${count !== 1 ? "s" : ""}`) : import_picocolors.default.green("0 alerts");
3190
- }
3191
3291
  function padEnd(str, width) {
3192
3292
  const visible = stripTerminalEscapes(str);
3193
3293
  const pad = Math.max(0, width - visible.length);
3194
3294
  return str + " ".repeat(pad);
3195
3295
  }
3196
- function buildSecurityLines(auditData, skills, source) {
3296
+ function buildSecurityLines(auditData, skills) {
3197
3297
  if (!auditData) return [];
3198
- if (!skills.some((s) => {
3199
- const data = auditData[s.slug];
3200
- return data && Object.keys(data).length > 0;
3201
- })) return [];
3202
- const nameWidth = Math.min(Math.max(...skills.map((s) => s.displayName.length)), 36);
3298
+ const rows = skills.filter((s) => auditData[s.slug]);
3299
+ if (rows.length === 0) return [];
3300
+ const nameWidth = Math.min(Math.max(...rows.map((s) => s.displayName.length)), 36);
3203
3301
  const lines = [];
3204
- const header = padEnd("", nameWidth + 2) + padEnd(import_picocolors.default.dim("Gen"), 18) + padEnd(import_picocolors.default.dim("Socket"), 18) + import_picocolors.default.dim("Snyk");
3302
+ const header = padEnd("", nameWidth + 2) + padEnd(import_picocolors.default.dim("Risk"), 18) + padEnd(import_picocolors.default.dim("Score"), 8) + import_picocolors.default.dim("Signals");
3205
3303
  lines.push(header);
3206
- for (const skill of skills) {
3304
+ for (const skill of rows) {
3207
3305
  const data = auditData[skill.slug];
3208
3306
  const name = skill.displayName.length > nameWidth ? skill.displayName.slice(0, nameWidth - 1) + "…" : skill.displayName;
3209
- const ath = data?.ath ? riskLabel(data.ath.risk) : import_picocolors.default.dim("--");
3210
- const socket = data?.socket ? socketLabel(data.socket) : import_picocolors.default.dim("--");
3211
- const snyk = data?.snyk ? riskLabel(data.snyk.risk) : import_picocolors.default.dim("--");
3212
- lines.push(padEnd(import_picocolors.default.cyan(name), nameWidth + 2) + padEnd(ath, 18) + padEnd(socket, 18) + snyk);
3307
+ const risk = riskLabel$1(data.risk_level);
3308
+ const score = data.risk_score != null ? import_picocolors.default.dim(String(data.risk_score)) : import_picocolors.default.dim("--");
3309
+ const signals = data.risk_signals.length > 0 ? import_picocolors.default.yellow(String(data.risk_signals.length)) : import_picocolors.default.dim("0");
3310
+ lines.push(padEnd(import_picocolors.default.cyan(name), nameWidth + 2) + padEnd(risk, 18) + padEnd(score, 8) + signals);
3213
3311
  }
3214
- lines.push("");
3215
- lines.push(`${import_picocolors.default.dim("Details:")} ${import_picocolors.default.dim(`https://skills.sh/${source}`)}`);
3216
3312
  return lines;
3217
3313
  }
3314
+ function getHighRiskSkills(auditData, skills) {
3315
+ if (!auditData) return [];
3316
+ return skills.filter((s) => {
3317
+ const data = auditData[s.slug];
3318
+ return data && (data.risk_level === "high" || data.risk_level === "critical");
3319
+ }).map((s) => ({
3320
+ name: s.displayName,
3321
+ riskLevel: auditData[s.slug].risk_level
3322
+ }));
3323
+ }
3218
3324
  function shortenPath$2(fullPath, cwd) {
3219
3325
  const home = homedir();
3220
- if (fullPath === home || fullPath.startsWith(home + sep)) return "~" + fullPath.slice(home.length);
3221
- if (fullPath === cwd || fullPath.startsWith(cwd + sep)) return "." + fullPath.slice(cwd.length);
3326
+ if (fullPath === home || fullPath.startsWith(home + sep$1)) return "~" + fullPath.slice(home.length);
3327
+ if (fullPath === cwd || fullPath.startsWith(cwd + sep$1)) return "." + fullPath.slice(cwd.length);
3222
3328
  return fullPath;
3223
3329
  }
3224
3330
  function computeSingleFileSkillHash(contents) {
@@ -3233,6 +3339,13 @@ function formatList$1(items, maxShow = 5) {
3233
3339
  const remaining = items.length - maxShow;
3234
3340
  return `${shown.join(", ")} +${remaining} more`;
3235
3341
  }
3342
+ function buildSkillFiles(selectedSkills, tempDir, blobResult) {
3343
+ const skillFiles = {};
3344
+ for (const skill of selectedSkills) if (blobResult && "repoPath" in skill) skillFiles[skill.name] = skill.repoPath;
3345
+ else if (tempDir && skill.path === tempDir) skillFiles[skill.name] = "SKILL.md";
3346
+ else if (tempDir && skill.path.startsWith(tempDir + sep$1)) skillFiles[skill.name] = skill.path.slice(tempDir.length + 1).split(sep$1).join("/") + "/SKILL.md";
3347
+ return skillFiles;
3348
+ }
3236
3349
  function splitAgentsByType(agentTypes) {
3237
3350
  const universal = [];
3238
3351
  const symlinked = [];
@@ -3243,18 +3356,6 @@ function splitAgentsByType(agentTypes) {
3243
3356
  symlinked
3244
3357
  };
3245
3358
  }
3246
- function buildAgentSummaryLines(targetAgents, installMode) {
3247
- const lines = [];
3248
- const { universal, symlinked } = splitAgentsByType(targetAgents);
3249
- if (installMode === "symlink") {
3250
- if (universal.length > 0) lines.push(` ${import_picocolors.default.green("universal:")} ${formatList$1(universal)}`);
3251
- if (symlinked.length > 0) lines.push(` ${import_picocolors.default.dim("symlink →")} ${formatList$1(symlinked)}`);
3252
- } else {
3253
- const allNames = targetAgents.map((a) => agents[a].displayName);
3254
- lines.push(` ${import_picocolors.default.dim("copy →")} ${formatList$1(allNames)}`);
3255
- }
3256
- return lines;
3257
- }
3258
3359
  function targetDisplayName(target) {
3259
3360
  const base = agents[target.agent].displayName;
3260
3361
  return target.subagent ? `${base} (${target.subagent})` : base;
@@ -3316,12 +3417,28 @@ function buildCopyResultLines(results, cwd) {
3316
3417
  }
3317
3418
  return lines;
3318
3419
  }
3420
+ function withQuitOnQ(promise) {
3421
+ const handler = (_str, key) => {
3422
+ if (key?.name !== "q" || key.ctrl || key.meta) return;
3423
+ process.stdin.removeListener("keypress", handler);
3424
+ if (process.stdin.isTTY) try {
3425
+ process.stdin.setRawMode(false);
3426
+ } catch {}
3427
+ process.stdout.write("\n");
3428
+ xe("Cancelled");
3429
+ process.exit(0);
3430
+ };
3431
+ process.stdin.on("keypress", handler);
3432
+ return promise.finally(() => {
3433
+ process.stdin.removeListener("keypress", handler);
3434
+ });
3435
+ }
3319
3436
  function multiselect(opts) {
3320
- return fe({
3437
+ return withQuitOnQ(fe({
3321
3438
  ...opts,
3322
3439
  options: opts.options,
3323
- message: `${opts.message} ${import_picocolors.default.dim("(space to toggle)")}`
3324
- });
3440
+ message: `${opts.message} ${import_picocolors.default.dim("(space to toggle, q to quit)")}`
3441
+ }));
3325
3442
  }
3326
3443
  async function promptForAgents(message, choices) {
3327
3444
  let lastSelected;
@@ -3382,57 +3499,44 @@ async function selectAgentsInteractive(options) {
3382
3499
  return selected;
3383
3500
  }
3384
3501
  setVersion(version$1);
3385
- async function handleWellKnownSkills(source, url, options, spinner) {
3386
- spinner.start("Discovering skills from well-known endpoint...");
3387
- const skills = await wellKnownProvider.fetchAllSkills(url);
3388
- if (skills.length === 0) {
3389
- spinner.stop(import_picocolors.default.red("No skills found"));
3390
- Se(import_picocolors.default.red("No skills found at this URL. Make sure the server has a /.well-known/agent-skills/index.json or /.well-known/skills/index.json file."));
3391
- process.exit(1);
3392
- }
3393
- spinner.stop(`Found ${import_picocolors.default.green(skills.length)} skill${skills.length > 1 ? "s" : ""}`);
3394
- for (const skill of skills) {
3395
- M.info(`Skill: ${import_picocolors.default.cyan(skill.installName)}`);
3396
- M.message(import_picocolors.default.dim(skill.description));
3397
- if (skill.files.size > 1) M.message(import_picocolors.default.dim(` Files: ${Array.from(skill.files.keys()).join(", ")}`));
3398
- }
3502
+ async function installLocalSkills(source, localDir, skills, options, agentResult) {
3503
+ const spinner = Y();
3399
3504
  if (options.list) {
3400
3505
  console.log();
3401
3506
  M.step(import_picocolors.default.bold("Available Skills"));
3402
3507
  for (const skill of skills) {
3403
- M.message(` ${import_picocolors.default.cyan(skill.installName)}`);
3508
+ M.message(` ${import_picocolors.default.cyan(getSkillDisplayName(skill))}`);
3404
3509
  M.message(` ${import_picocolors.default.dim(skill.description)}`);
3405
- if (skill.files.size > 1) M.message(` ${import_picocolors.default.dim(`Files: ${skill.files.size}`)}`);
3406
3510
  }
3407
3511
  console.log();
3408
- Se("Run without --list to install");
3409
- process.exit(0);
3512
+ Se("Use --skill <name> to install specific skills");
3513
+ return;
3410
3514
  }
3411
3515
  let selectedSkills;
3412
3516
  if (options.skill?.includes("*")) {
3413
3517
  selectedSkills = skills;
3414
3518
  M.info(`Installing all ${skills.length} skills`);
3415
3519
  } else if (options.skill && options.skill.length > 0) {
3416
- selectedSkills = skills.filter((s) => options.skill.some((name) => s.installName.toLowerCase() === name.toLowerCase() || s.name.toLowerCase() === name.toLowerCase()));
3520
+ selectedSkills = filterSkills(skills, options.skill);
3417
3521
  if (selectedSkills.length === 0) {
3418
3522
  M.error(`No matching skills found for: ${options.skill.join(", ")}`);
3419
3523
  M.info("Available skills:");
3420
- for (const s of skills) M.message(` - ${s.installName}`);
3524
+ for (const s of skills) M.message(` - ${getSkillDisplayName(s)}`);
3421
3525
  process.exit(1);
3422
3526
  }
3423
- } else if (skills.length === 1) {
3424
- selectedSkills = skills;
3425
- const firstSkill = skills[0];
3426
- M.info(`Skill: ${import_picocolors.default.cyan(firstSkill.installName)}`);
3427
- } else if (options.yes) {
3527
+ M.info(`Selected ${selectedSkills.length} skill${selectedSkills.length !== 1 ? "s" : ""}: ${selectedSkills.map((s) => import_picocolors.default.cyan(getSkillDisplayName(s))).join(", ")}`);
3528
+ } else if (skills.length === 1 || options.yes) {
3428
3529
  selectedSkills = skills;
3429
- M.info(`Installing all ${skills.length} skills`);
3530
+ if (skills.length === 1) {
3531
+ M.info(`Skill: ${import_picocolors.default.cyan(getSkillDisplayName(skills[0]))}`);
3532
+ M.message(import_picocolors.default.dim(skills[0].description));
3533
+ } else M.info(`Installing all ${skills.length} skills`);
3430
3534
  } else {
3431
3535
  const selected = await multiselect({
3432
3536
  message: "Select skills to install",
3433
- options: skills.map((s) => ({
3537
+ options: [...skills].sort((a, b) => getSkillDisplayName(a).localeCompare(getSkillDisplayName(b))).map((s) => ({
3434
3538
  value: s,
3435
- label: s.installName,
3539
+ label: getSkillDisplayName(s),
3436
3540
  hint: s.description.length > 60 ? s.description.slice(0, 57) + "..." : s.description
3437
3541
  })),
3438
3542
  required: true
@@ -3466,7 +3570,7 @@ async function handleWellKnownSkills(source, url, options, spinner) {
3466
3570
  M.info("Installing to all agents");
3467
3571
  } else {
3468
3572
  M.info("Select agents to install skills to");
3469
- const selected = await promptForAgents("Which agents do you want to install to?", Object.entries(agents).map(([key, config]) => ({
3573
+ const selected = await promptForAgents("Which agents do you want to install to?", Object.entries(agents).filter(([key]) => key !== "eve").map(([key, config]) => ({
3470
3574
  value: key,
3471
3575
  label: config.displayName
3472
3576
  })));
@@ -3476,24 +3580,14 @@ async function handleWellKnownSkills(source, url, options, spinner) {
3476
3580
  }
3477
3581
  targetAgents = selected;
3478
3582
  }
3479
- else if (installedAgents.length === 1 || options.yes) {
3583
+ else {
3480
3584
  targetAgents = ensureUniversalAgents(installedAgents);
3481
- if (installedAgents.length === 1) {
3482
- const firstAgent = installedAgents[0];
3483
- M.info(`Installing to: ${import_picocolors.default.cyan(agents[firstAgent].displayName)}`);
3484
- } else M.info(`Installing to: ${installedAgents.map((a) => import_picocolors.default.cyan(agents[a].displayName)).join(", ")}`);
3485
- } else {
3486
- const selected = await selectAgentsInteractive({ global: options.global });
3487
- if (pD(selected)) {
3488
- xe("Installation cancelled");
3489
- process.exit(0);
3490
- }
3491
- targetAgents = selected;
3585
+ M.info(`Installing to: ${installedAgents.map((a) => import_picocolors.default.cyan(agents[a].displayName)).join(", ")}`);
3492
3586
  }
3493
3587
  }
3588
+ const installTargets = buildInstallTargets(targetAgents, [void 0]);
3494
3589
  let installGlobally = options.global ?? false;
3495
- const supportsGlobal = targetAgents.some((a) => agents[a].globalSkillsDir !== void 0);
3496
- if (options.global === void 0 && !options.yes && supportsGlobal) {
3590
+ if (options.global === void 0 && !options.yes) {
3497
3591
  const scope = await ve({
3498
3592
  message: "Installation scope",
3499
3593
  options: [{
@@ -3512,71 +3606,27 @@ async function handleWellKnownSkills(source, url, options, spinner) {
3512
3606
  }
3513
3607
  installGlobally = scope;
3514
3608
  }
3515
- let installMode = options.copy ? "copy" : "symlink";
3516
- const uniqueDirs = new Set(targetAgents.map((a) => agents[a].skillsDir));
3517
- if (!options.copy && !options.yes && uniqueDirs.size > 1) {
3518
- const modeChoice = await ve({
3519
- message: "Installation method",
3520
- options: [{
3521
- value: "symlink",
3522
- label: "Symlink (Recommended)",
3523
- hint: "Single source of truth, easy updates"
3524
- }, {
3525
- value: "copy",
3526
- label: "Copy to all agents",
3527
- hint: "Independent copies for each agent"
3528
- }]
3529
- });
3530
- if (pD(modeChoice)) {
3531
- xe("Installation cancelled");
3532
- process.exit(0);
3533
- }
3534
- installMode = modeChoice;
3535
- } else if (uniqueDirs.size <= 1) installMode = "copy";
3536
- const cwd = process.cwd();
3537
- const summaryLines = [];
3538
- targetAgents.map((a) => agents[a].displayName);
3539
- const overwriteChecks = await Promise.all(selectedSkills.flatMap((skill) => targetAgents.map(async (agent) => ({
3540
- skillName: skill.installName,
3541
- agent,
3542
- installed: await isSkillInstalled(skill.installName, agent, { global: installGlobally })
3543
- }))));
3544
- const overwriteStatus = /* @__PURE__ */ new Map();
3545
- for (const { skillName, agent, installed } of overwriteChecks) {
3546
- if (!overwriteStatus.has(skillName)) overwriteStatus.set(skillName, /* @__PURE__ */ new Map());
3547
- overwriteStatus.get(skillName).set(agent, installed);
3548
- }
3549
- for (const skill of selectedSkills) {
3550
- if (summaryLines.length > 0) summaryLines.push("");
3551
- const shortCanonical = shortenPath$2(getCanonicalPath(skill.installName, { global: installGlobally }), cwd);
3552
- summaryLines.push(`${import_picocolors.default.cyan(shortCanonical)}`);
3553
- summaryLines.push(...buildAgentSummaryLines(targetAgents, installMode));
3554
- if (skill.files.size > 1) summaryLines.push(` ${import_picocolors.default.dim("files:")} ${skill.files.size}`);
3555
- const skillOverwrites = overwriteStatus.get(skill.installName);
3556
- const overwriteAgents = targetAgents.filter((a) => skillOverwrites?.get(a)).map((a) => agents[a].displayName);
3557
- if (overwriteAgents.length > 0) summaryLines.push(` ${import_picocolors.default.yellow("overwrites:")} ${formatList$1(overwriteAgents)}`);
3558
- }
3559
- console.log();
3560
- Me(summaryLines.join("\n"), "Installation Summary");
3561
3609
  if (!options.yes) {
3562
- const confirmed = await ye({ message: "Proceed with installation?" });
3610
+ const confirmed = await ye({
3611
+ message: "Proceed with installation?",
3612
+ initialValue: true
3613
+ });
3563
3614
  if (pD(confirmed) || !confirmed) {
3564
3615
  xe("Installation cancelled");
3565
3616
  process.exit(0);
3566
3617
  }
3567
3618
  }
3568
- const sourceIdentifier = wellKnownProvider.getSourceIdentifier(url);
3569
- const wellKnownPrivacyPromise = isSourcePrivate(sourceIdentifier).catch(() => null);
3570
3619
  spinner.start("Installing skills...");
3571
3620
  const results = [];
3572
- for (const skill of selectedSkills) for (const agent of targetAgents) {
3573
- const result = await installWellKnownSkillForAgent(skill, agent, {
3621
+ for (const skill of selectedSkills) for (const target of installTargets) {
3622
+ const result = await installSkillForAgent(skill, target.agent, {
3574
3623
  global: installGlobally,
3575
- mode: installMode
3624
+ mode: "copy",
3625
+ eveSubagent: target.subagent
3576
3626
  });
3577
3627
  results.push({
3578
- skill: skill.installName,
3579
- agent: agents[agent].displayName,
3628
+ skill: getSkillDisplayName(skill),
3629
+ agent: targetDisplayName(target),
3580
3630
  ...result
3581
3631
  });
3582
3632
  }
@@ -3584,73 +3634,18 @@ async function handleWellKnownSkills(source, url, options, spinner) {
3584
3634
  console.log();
3585
3635
  const successful = results.filter((r) => r.success);
3586
3636
  const failed = results.filter((r) => !r.success);
3587
- const skillFiles = {};
3588
- for (const skill of selectedSkills) skillFiles[skill.installName] = skill.sourceUrl;
3589
- if (await wellKnownPrivacyPromise !== true) track({
3590
- event: "install",
3591
- source: sourceIdentifier,
3592
- skills: selectedSkills.map((s) => s.installName).join(","),
3593
- agents: targetAgents.join(","),
3594
- ...installGlobally && { global: "1" },
3595
- skillFiles: JSON.stringify(skillFiles),
3596
- sourceType: "well-known"
3597
- });
3598
- if (successful.length > 0 && installGlobally) {
3599
- const successfulSkillNames = new Set(successful.map((r) => r.skill));
3600
- for (const skill of selectedSkills) if (successfulSkillNames.has(skill.installName)) try {
3601
- await addSkillToLock(skill.installName, {
3602
- source: sourceIdentifier,
3603
- sourceType: "well-known",
3604
- sourceUrl: skill.sourceUrl,
3605
- skillFolderHash: ""
3606
- });
3607
- } catch {}
3608
- }
3609
- if (successful.length > 0 && !installGlobally) {
3610
- const successfulSkillNames = new Set(successful.map((r) => r.skill));
3611
- for (const skill of selectedSkills) if (successfulSkillNames.has(skill.installName)) try {
3612
- const matchingResult = successful.find((r) => r.skill === skill.installName);
3613
- const installDir = matchingResult?.canonicalPath || matchingResult?.path;
3614
- if (installDir) {
3615
- const computedHash = await computeSkillFolderHash(installDir);
3616
- await addSkillToLocalLock(skill.installName, {
3617
- source: sourceIdentifier,
3618
- sourceType: "well-known",
3619
- computedHash
3620
- }, cwd);
3621
- }
3622
- } catch {}
3623
- }
3624
3637
  if (successful.length > 0) {
3625
3638
  const bySkill = /* @__PURE__ */ new Map();
3626
3639
  for (const r of successful) {
3627
- const skillResults = bySkill.get(r.skill) || [];
3628
- skillResults.push(r);
3629
- bySkill.set(r.skill, skillResults);
3640
+ if (!bySkill.has(r.skill)) bySkill.set(r.skill, []);
3641
+ bySkill.get(r.skill).push(r);
3630
3642
  }
3631
- const skillCount = bySkill.size;
3632
- const symlinkFailures = successful.filter((r) => r.mode === "symlink" && r.symlinkFailed);
3633
- const copiedAgents = symlinkFailures.map((r) => r.agent);
3634
3643
  const resultLines = [];
3635
3644
  for (const [skillName, skillResults] of bySkill) {
3636
- const firstResult = skillResults[0];
3637
- if (firstResult.mode === "copy") {
3638
- resultLines.push(`${import_picocolors.default.green("✓")} ${skillName} ${import_picocolors.default.dim("(copied)")}`);
3639
- resultLines.push(...buildCopyResultLines(skillResults, cwd));
3640
- } else {
3641
- if (firstResult.canonicalPath) {
3642
- const shortPath = shortenPath$2(firstResult.canonicalPath, cwd);
3643
- resultLines.push(`${import_picocolors.default.green("✓")} ${shortPath}`);
3644
- } else resultLines.push(`${import_picocolors.default.green("✓")} ${skillName}`);
3645
- resultLines.push(...buildResultLines(skillResults, targetAgents));
3646
- }
3647
- }
3648
- const title = import_picocolors.default.green(`Installed ${skillCount} skill${skillCount !== 1 ? "s" : ""}`);
3649
- Me(resultLines.join("\n"), title);
3650
- if (symlinkFailures.length > 0) {
3651
- M.warn(import_picocolors.default.yellow(`Symlinks failed for: ${formatList$1(copiedAgents)}`));
3652
- M.message(import_picocolors.default.dim(" Files were copied instead. On Windows, enable Developer Mode for symlink support."));
3645
+ resultLines.push(`${import_picocolors.default.green("✓")} ${skillName} ${import_picocolors.default.dim("(copied)")}`);
3646
+ resultLines.push(...buildCopyResultLines(skillResults, process.cwd()));
3653
3647
  }
3648
+ Me(resultLines.join("\n"), import_picocolors.default.green(`Installed ${bySkill.size} skill${bySkill.size !== 1 ? "s" : ""}`));
3654
3649
  }
3655
3650
  if (failed.length > 0) {
3656
3651
  console.log();
@@ -3659,6 +3654,14 @@ async function handleWellKnownSkills(source, url, options, spinner) {
3659
3654
  }
3660
3655
  console.log();
3661
3656
  Se(import_picocolors.default.green("Done!") + import_picocolors.default.dim(" Review skills before use; they run with full agent permissions."));
3657
+ if (successful.length > 0 && !installGlobally) for (const skill of selectedSkills) try {
3658
+ const computedHash = await computeSkillFolderHash(skill.path);
3659
+ await addSkillToLocalLock(skill.name, {
3660
+ source,
3661
+ sourceType: "local",
3662
+ computedHash
3663
+ }, process.cwd());
3664
+ } catch {}
3662
3665
  await promptForFindSkills(options, targetAgents);
3663
3666
  }
3664
3667
  async function runAdd(args, options = {}) {
@@ -3699,102 +3702,139 @@ async function runAdd(args, options = {}) {
3699
3702
  if (agentResult.isAgent) M.info(import_picocolors.default.bgCyan(import_picocolors.default.black(import_picocolors.default.bold(` ${agentResult.agent.name} `))) + " Agent detected — installing non-interactively");
3700
3703
  else if (!process.stdin.isTTY) showInstallTip();
3701
3704
  let tempDir = null;
3705
+ if (source.split("/")[0]?.toLowerCase() === "openclaw" && !options.dangerouslyAcceptOpenclawRisks) {
3706
+ console.log();
3707
+ M.warn(import_picocolors.default.yellow(import_picocolors.default.bold("⚠ OpenClaw skills are unverified community submissions.")));
3708
+ M.message(import_picocolors.default.yellow("This source contains user-submitted skills that have not been reviewed for safety or quality."));
3709
+ M.message(import_picocolors.default.yellow("Skills run with full agent permissions and could be malicious."));
3710
+ console.log();
3711
+ M.message(`If you understand the risks, re-run with:\n\n ${import_picocolors.default.cyan(`npx wittyhub add ${source} --dangerously-accept-openclaw-risks`)}\n`);
3712
+ Se(import_picocolors.default.red("Installation blocked"));
3713
+ process.exit(1);
3714
+ }
3702
3715
  try {
3703
3716
  const spinner = Y();
3704
- spinner.start("Parsing source...");
3705
- const parsed = parseSource(source);
3706
- spinner.stop(`Source: ${parsed.type === "local" ? parsed.localPath : parsed.url}${parsed.ref ? ` @ ${import_picocolors.default.yellow(parsed.ref)}` : ""}${parsed.subpath ? ` (${parsed.subpath})` : ""}${parsed.skillFilter ? ` ${import_picocolors.default.dim("@")}${import_picocolors.default.cyan(parsed.skillFilter)}` : ""}`);
3707
- const ownerRepoRaw = getOwnerRepo(parsed);
3708
- const repoPrivacyPromise = (() => {
3709
- if (!ownerRepoRaw || parsed.type !== "github") return Promise.resolve(null);
3710
- const ownerRepo = parseOwnerRepo(ownerRepoRaw);
3711
- if (!ownerRepo) return Promise.resolve(null);
3712
- return isRepoPrivate(ownerRepo.owner, ownerRepo.repo).catch(() => null);
3713
- })();
3714
- if (ownerRepoRaw?.split("/")[0]?.toLowerCase() === "openclaw" && !options.dangerouslyAcceptOpenclawRisks) {
3715
- console.log();
3716
- M.warn(import_picocolors.default.yellow(import_picocolors.default.bold("⚠ OpenClaw skills are unverified community submissions.")));
3717
- M.message(import_picocolors.default.yellow("This source contains user-submitted skills that have not been reviewed for safety or quality."));
3718
- M.message(import_picocolors.default.yellow("Skills run with full agent permissions and could be malicious."));
3719
- console.log();
3720
- M.message(`If you understand the risks, re-run with:\n\n ${import_picocolors.default.cyan(`npx wittyhub add ${source} --dangerously-accept-openclaw-risks`)}\n`);
3721
- Se(import_picocolors.default.red("Installation blocked"));
3722
- process.exit(1);
3723
- }
3724
- if (parsed.type === "well-known") {
3725
- await handleWellKnownSkills(source, parsed.url, options, spinner);
3726
- return;
3727
- }
3728
- if (parsed.skillFilter) {
3729
- options.skill = options.skill || [];
3730
- if (!options.skill.includes(parsed.skillFilter)) options.skill.push(parsed.skillFilter);
3731
- }
3732
- const includeInternal = !!(options.skill && options.skill.length > 0);
3733
- let skills;
3734
- let blobResult = null;
3735
- if (parsed.type === "local") {
3736
- spinner.start("Validating local path...");
3737
- if (!existsSync(parsed.localPath)) {
3738
- spinner.stop(import_picocolors.default.red("Path not found"));
3739
- Se(import_picocolors.default.red(`Local path does not exist: ${parsed.localPath}`));
3717
+ if (looksLikeLocalPath(source)) {
3718
+ const expanded = expandLocalPath(source);
3719
+ if (!existsSync$1(expanded)) {
3720
+ Se(import_picocolors.default.red(`Local path does not exist: ${expanded}`));
3740
3721
  process.exit(1);
3741
3722
  }
3742
- spinner.stop("Local path validated");
3743
- spinner.start("Discovering skills...");
3744
- skills = await discoverSkills(parsed.localPath, parsed.subpath, {
3745
- includeInternal,
3746
- fullDepth: options.fullDepth
3747
- });
3748
- } else if (parsed.type === "github" && !options.fullDepth) {
3749
- const BLOB_ALLOWED_OWNERS = [
3750
- "vercel",
3751
- "vercel-labs",
3752
- "heygen-com"
3753
- ];
3754
- const ownerRepo = getOwnerRepo(parsed);
3755
- const owner = ownerRepo?.split("/")[0]?.toLowerCase();
3756
- const isSelfHostedRepo = !!ownerRepo && Object.hasOwn(BLOB_ALLOWED_REPOS, ownerRepo.toLowerCase());
3757
- if (ownerRepo && owner && (isSelfHostedRepo || BLOB_ALLOWED_OWNERS.includes(owner))) {
3758
- spinner.start("Fetching skills...");
3759
- blobResult = await tryBlobInstall(ownerRepo, {
3760
- subpath: parsed.subpath,
3761
- skillFilter: parsed.skillFilter,
3762
- ref: parsed.ref,
3763
- getToken: getGitHubToken,
3764
- includeInternal
3765
- });
3766
- if (!blobResult) spinner.stop(import_picocolors.default.dim("Falling back to clone..."));
3723
+ if (!isDirectory(expanded)) {
3724
+ Se(import_picocolors.default.red(`Local path is not a directory: ${expanded}`));
3725
+ process.exit(1);
3767
3726
  }
3768
- if (blobResult) {
3769
- skills = blobResult.skills;
3770
- spinner.stop(`Found ${import_picocolors.default.green(skills.length)} skill${skills.length > 1 ? "s" : ""}`);
3771
- } else {
3772
- spinner.start("Cloning repository...");
3773
- tempDir = await cloneRepo(parsed.url, parsed.ref);
3774
- spinner.stop("Repository cloned");
3775
- spinner.start("Discovering skills...");
3776
- skills = await discoverSkills(tempDir, parsed.subpath, {
3727
+ const includeInternal = !!(options.skill && options.skill.length > 0);
3728
+ spinner.start("Discovering skills...");
3729
+ let skills;
3730
+ try {
3731
+ skills = await discoverSkills(expanded, void 0, {
3777
3732
  includeInternal,
3778
3733
  fullDepth: options.fullDepth
3779
3734
  });
3735
+ } finally {
3736
+ spinner.stop("Discovery complete");
3780
3737
  }
3781
- } else {
3782
- spinner.start("Cloning repository...");
3783
- tempDir = await cloneRepo(parsed.url, parsed.ref);
3784
- spinner.stop("Repository cloned");
3785
- spinner.start("Discovering skills...");
3786
- skills = await discoverSkills(tempDir, parsed.subpath, {
3787
- includeInternal,
3788
- fullDepth: options.fullDepth
3789
- });
3738
+ if (skills.length === 0) {
3739
+ M.error(import_picocolors.default.red("No skills found"));
3740
+ Se(import_picocolors.default.red("No valid skills found. Skills require a SKILL.md with name and description."));
3741
+ process.exit(1);
3742
+ }
3743
+ await installLocalSkills(source, expanded, skills, options, agentResult);
3744
+ return;
3790
3745
  }
3746
+ const skillNameForLookup = options.skill?.[0];
3747
+ let skillIds;
3748
+ const repoRef = isRepoLevelRef(source) ? source : repoUrlToRef(source);
3749
+ const repoLevel = repoRef !== null;
3750
+ if (repoLevel) {
3751
+ const [sourceType, ownerRepo] = repoRef.split(":", 2);
3752
+ spinner.start("查询仓库下的技能...");
3753
+ let repoSkills;
3754
+ try {
3755
+ repoSkills = await listSkillsByRepo(sourceType, ownerRepo);
3756
+ } finally {
3757
+ spinner.stop("查询完成");
3758
+ }
3759
+ if (repoSkills.length === 0) {
3760
+ Se(import_picocolors.default.red(`仓库 "${source}" 下未找到已收录的技能。`));
3761
+ process.exit(1);
3762
+ }
3763
+ if (options.list) {
3764
+ console.log();
3765
+ M.step(import_picocolors.default.bold("Available Skills"));
3766
+ const sorted = [...repoSkills].sort((a, b) => a.name.localeCompare(b.name));
3767
+ for (const skill of sorted) {
3768
+ M.message(` ${import_picocolors.default.cyan(skill.name)}`);
3769
+ if (skill.description) M.message(` ${import_picocolors.default.dim(skill.description)}`);
3770
+ }
3771
+ console.log();
3772
+ Se("Use --skill <name> to install specific skills");
3773
+ process.exit(0);
3774
+ }
3775
+ if (options.skill && !options.skill.includes("*")) {
3776
+ const matched = repoSkills.filter((s) => options.skill.some((name) => s.name === name || s.skill_id.endsWith("/" + name)));
3777
+ if (matched.length === 0) {
3778
+ Se(import_picocolors.default.red(`未找到匹配 --skill 的技能: ${options.skill.join(", ")}`));
3779
+ process.exit(1);
3780
+ }
3781
+ skillIds = matched.map((s) => s.skill_id);
3782
+ } else if (options.skill?.includes("*") || options.all || options.yes) skillIds = repoSkills.map((s) => s.skill_id);
3783
+ else {
3784
+ const selected = await multiselect({
3785
+ message: "Select skills to install",
3786
+ options: [...repoSkills].sort((a, b) => a.name.localeCompare(b.name)).map((s) => ({
3787
+ value: s.skill_id,
3788
+ label: s.name,
3789
+ hint: s.description && s.description.length > 60 ? s.description.slice(0, 57) + "..." : s.description ?? ""
3790
+ })),
3791
+ required: true
3792
+ });
3793
+ if (pD(selected)) {
3794
+ xe("Installation cancelled");
3795
+ process.exit(0);
3796
+ }
3797
+ skillIds = selected;
3798
+ }
3799
+ } else try {
3800
+ const result = await resolveSkillId(source, skillNameForLookup);
3801
+ skillIds = Array.isArray(result) ? result : [result];
3802
+ } catch (e) {
3803
+ const message = e instanceof Error ? e.message : String(e);
3804
+ Se(import_picocolors.default.red(message));
3805
+ process.exit(1);
3806
+ }
3807
+ spinner.start(`Downloading ${skillIds.length > 1 ? `${skillIds.length} skills` : `skill ${import_picocolors.default.cyan(skillIds[0])}`} from API...`);
3808
+ try {
3809
+ tempDir = await mkdtemp(join$1(tmpdir(), "skills-"));
3810
+ for (const id of skillIds) await downloadAndExtractSkill(id, tempDir, options.version);
3811
+ } catch (e) {
3812
+ spinner.stop(import_picocolors.default.red("Download failed"));
3813
+ const message = e instanceof Error ? e.message : String(e);
3814
+ Se(import_picocolors.default.red(`Failed to download skill: ${message}`));
3815
+ process.exit(1);
3816
+ }
3817
+ spinner.stop(`Skill archive downloaded from ${import_picocolors.default.cyan("API")}`);
3818
+ const parsed = {
3819
+ type: "git",
3820
+ url: skillIds[0]
3821
+ };
3822
+ const repoPrivacyPromise = Promise.resolve(null);
3823
+ const includeInternal = !!(options.skill && options.skill.length > 0);
3824
+ let skills;
3825
+ let blobResult = null;
3826
+ spinner.start("Discovering skills...");
3827
+ skills = await discoverSkills(tempDir, void 0, {
3828
+ includeInternal,
3829
+ fullDepth: options.fullDepth
3830
+ });
3791
3831
  if (skills.length === 0) {
3792
3832
  spinner.stop(import_picocolors.default.red("No skills found"));
3793
3833
  Se(import_picocolors.default.red("No valid skills found. Skills require a SKILL.md with name and description."));
3794
3834
  await cleanup(tempDir);
3795
3835
  process.exit(1);
3796
3836
  }
3797
- if (!blobResult) spinner.stop(`Found ${import_picocolors.default.green(skills.length)} skill${skills.length > 1 ? "s" : ""}`);
3837
+ spinner.stop(`Found ${import_picocolors.default.green(skills.length)} skill${skills.length > 1 ? "s" : ""}`);
3798
3838
  if (options.list) {
3799
3839
  console.log();
3800
3840
  M.step(import_picocolors.default.bold("Available Skills"));
@@ -3828,7 +3868,13 @@ async function runAdd(args, options = {}) {
3828
3868
  process.exit(0);
3829
3869
  }
3830
3870
  let selectedSkills;
3831
- if (options.skill?.includes("*")) {
3871
+ if (repoLevel) {
3872
+ selectedSkills = skills;
3873
+ if (skills.length === 1) {
3874
+ M.info(`Skill: ${import_picocolors.default.cyan(getSkillDisplayName(skills[0]))}`);
3875
+ M.message(import_picocolors.default.dim(skills[0].description));
3876
+ } else M.info(`Installing ${skills.length} skills`);
3877
+ } else if (options.skill?.includes("*")) {
3832
3878
  selectedSkills = skills;
3833
3879
  M.info(`Installing all ${skills.length} skills`);
3834
3880
  } else if (options.skill && options.skill.length > 0) {
@@ -3891,8 +3937,10 @@ async function runAdd(args, options = {}) {
3891
3937
  }
3892
3938
  selectedSkills = selected;
3893
3939
  }
3894
- const ownerRepoForAudit = getOwnerRepo(parsed);
3895
- const auditPromise = Promise.resolve(null);
3940
+ const auditPromise = skillIds.length === 1 ? fetchAuditBySkillId(skillIds[0]) : Promise.all(skillIds.map((id) => fetchAuditBySkillId(id))).then((results) => results.reduce((acc, r) => ({
3941
+ ...acc,
3942
+ ...r
3943
+ }), {}));
3896
3944
  let targetAgents;
3897
3945
  const validAgents = Object.keys(agents);
3898
3946
  if (options.agent?.includes("*")) {
@@ -4093,18 +4141,28 @@ async function runAdd(args, options = {}) {
4093
4141
  }
4094
4142
  console.log();
4095
4143
  Me(summaryLines.join("\n"), "Installation Summary");
4144
+ const auditSkills = selectedSkills.map((s) => ({
4145
+ slug: getSkillDisplayName(s),
4146
+ displayName: getSkillDisplayName(s)
4147
+ }));
4148
+ let auditData = null;
4096
4149
  try {
4097
- const auditData = await auditPromise;
4098
- if (auditData && ownerRepoForAudit) {
4099
- const securityLines = buildSecurityLines(auditData, selectedSkills.map((s) => ({
4100
- slug: getSkillDisplayName(s),
4101
- displayName: getSkillDisplayName(s)
4102
- })), ownerRepoForAudit);
4150
+ auditData = await auditPromise;
4151
+ if (auditData) {
4152
+ const securityLines = buildSecurityLines(auditData, auditSkills);
4103
4153
  if (securityLines.length > 0) Me(securityLines.join("\n"), "Security Risk Assessments");
4104
4154
  }
4105
4155
  } catch {}
4156
+ const highRiskSkills = getHighRiskSkills(auditData, auditSkills);
4157
+ if (highRiskSkills.length > 0) {
4158
+ const list = highRiskSkills.map((s) => `${s.name} 经安全审计标记为 ${s.riskLevel} 风险`).join(",");
4159
+ M.warn(`${import_picocolors.default.red(import_picocolors.default.bold("⚠ 高风险警告:"))} ${list},请谨慎安装。`);
4160
+ }
4106
4161
  if (!options.yes) {
4107
- const confirmed = await ye({ message: "Proceed with installation?" });
4162
+ const confirmed = await ye({
4163
+ message: highRiskSkills.length > 0 ? `检测到以下技能存在安全风险:${highRiskSkills.map((s) => `${s.name} (${s.riskLevel})`).join(", ")}。仍要继续安装?` : "Proceed with installation?",
4164
+ initialValue: highRiskSkills.length === 0
4165
+ });
4108
4166
  if (pD(confirmed) || !confirmed) {
4109
4167
  xe("Installation cancelled");
4110
4168
  await cleanup(tempDir);
@@ -4116,17 +4174,7 @@ async function runAdd(args, options = {}) {
4116
4174
  for (const skill of selectedSkills) for (const target of installTargets) {
4117
4175
  const { agent, subagent } = target;
4118
4176
  let result;
4119
- if (blobResult && "files" in skill) {
4120
- const blobSkill = skill;
4121
- result = await installBlobSkillForAgent({
4122
- installName: blobSkill.name,
4123
- files: blobSkill.files
4124
- }, agent, {
4125
- global: installGlobally,
4126
- mode: installMode,
4127
- eveSubagent: subagent
4128
- });
4129
- } else if (tempDir && skill.path === tempDir && skill.rawContent) result = await installBlobSkillForAgent({
4177
+ if (tempDir && skill.path === tempDir && skill.rawContent) result = await installBlobSkillForAgent({
4130
4178
  installName: skill.name,
4131
4179
  files: [{
4132
4180
  path: "SKILL.md",
@@ -4152,16 +4200,37 @@ async function runAdd(args, options = {}) {
4152
4200
  console.log();
4153
4201
  const successful = results.filter((r) => r.success);
4154
4202
  const failed = results.filter((r) => !r.success);
4155
- const skillFiles = {};
4156
- for (const skill of selectedSkills) if (blobResult && "repoPath" in skill) skillFiles[skill.name] = skill.repoPath;
4157
- else if (tempDir && skill.path === tempDir) skillFiles[skill.name] = "SKILL.md";
4158
- else if (tempDir && skill.path.startsWith(tempDir + sep)) skillFiles[skill.name] = skill.path.slice(tempDir.length + 1).split(sep).join("/") + "/SKILL.md";
4159
- else continue;
4203
+ const skillFiles = buildSkillFiles(selectedSkills, tempDir, blobResult);
4160
4204
  const normalizedSource = getOwnerRepo(parsed);
4161
4205
  const lockSource = getLockSource(parsed.url, normalizedSource);
4162
4206
  if (normalizedSource) if (parseOwnerRepo(normalizedSource)) {
4163
4207
  const isPrivate = await repoPrivacyPromise;
4164
- if (isPrivate === false || isPrivate === null && parsed.type !== "github") track({
4208
+ if (isPrivate === false || isPrivate === null && parsed.type !== "github") {
4209
+ console.log("[telemetry-debug-1]", JSON.stringify({
4210
+ isPrivate,
4211
+ parsedType: parsed.type,
4212
+ normalizedSource,
4213
+ sourceType: parsed.type,
4214
+ skillFiles: JSON.stringify(skillFiles)
4215
+ }));
4216
+ track({
4217
+ event: "install",
4218
+ source: normalizedSource,
4219
+ skills: selectedSkills.map((s) => s.name).join(","),
4220
+ agents: targetAgents.join(","),
4221
+ ...installGlobally && { global: "1" },
4222
+ skillFiles: JSON.stringify(skillFiles),
4223
+ sourceType: parsed.type
4224
+ });
4225
+ }
4226
+ } else {
4227
+ console.log("[telemetry-debug-2]", JSON.stringify({
4228
+ parsedType: parsed.type,
4229
+ normalizedSource,
4230
+ sourceType: parsed.type,
4231
+ skillFiles: JSON.stringify(skillFiles)
4232
+ }));
4233
+ track({
4165
4234
  event: "install",
4166
4235
  source: normalizedSource,
4167
4236
  skills: selectedSkills.map((s) => s.name).join(","),
@@ -4170,32 +4239,21 @@ async function runAdd(args, options = {}) {
4170
4239
  skillFiles: JSON.stringify(skillFiles),
4171
4240
  sourceType: parsed.type
4172
4241
  });
4173
- } else track({
4174
- event: "install",
4175
- source: normalizedSource,
4176
- skills: selectedSkills.map((s) => s.name).join(","),
4177
- agents: targetAgents.join(","),
4178
- ...installGlobally && { global: "1" },
4179
- skillFiles: JSON.stringify(skillFiles),
4180
- sourceType: parsed.type
4181
- });
4242
+ }
4182
4243
  if (successful.length > 0 && installGlobally && normalizedSource) {
4183
4244
  const successfulSkillNames = new Set(successful.map((r) => r.skill));
4184
4245
  let cachedTree;
4185
- if (parsed.type === "github" && !blobResult) cachedTree = await fetchRepoTree(normalizedSource, parsed.ref, getGitHubToken);
4246
+ if (parsed.type === "github" && true) cachedTree = await fetchRepoTree(normalizedSource, parsed.ref, getGitHubToken);
4186
4247
  for (const skill of selectedSkills) {
4187
4248
  const skillDisplayName = getSkillDisplayName(skill);
4188
4249
  if (successfulSkillNames.has(skillDisplayName)) try {
4189
4250
  let skillFolderHash = "";
4190
4251
  const skillPathValue = skillFiles[skill.name];
4191
- if (blobResult && skillPathValue) {
4192
- const hash = getSkillFolderHashFromTree(blobResult.tree, skillPathValue);
4193
- if (hash) skillFolderHash = hash;
4194
- } else if (parsed.type === "github" && skillPathValue && cachedTree) {
4252
+ if (parsed.type === "github" && skillPathValue && cachedTree) {
4195
4253
  const hash = getSkillFolderHashFromTree(cachedTree, skillPathValue);
4196
4254
  if (hash) skillFolderHash = hash;
4197
4255
  } else if (skillPathValue && tempDir) {
4198
- const hash = await computeSkillFolderHash(join(tempDir, dirname(skillPathValue)));
4256
+ const hash = await computeSkillFolderHash(join$1(tempDir, dirname$1(skillPathValue)));
4199
4257
  if (hash) skillFolderHash = hash;
4200
4258
  }
4201
4259
  await addSkillToLock(skill.name, {
@@ -4217,7 +4275,7 @@ async function runAdd(args, options = {}) {
4217
4275
  for (const skill of selectedSkills) {
4218
4276
  const skillDisplayName = getSkillDisplayName(skill);
4219
4277
  if (successfulSkillNames.has(skillDisplayName)) try {
4220
- const computedHash = blobResult && "snapshotHash" in skill ? skill.snapshotHash : tempDir && skill.path === tempDir && skill.rawContent ? computeSingleFileSkillHash(skill.rawContent) : await computeSkillFolderHash(skill.path);
4278
+ const computedHash = tempDir && skill.path === tempDir && skill.rawContent ? computeSingleFileSkillHash(skill.rawContent) : await computeSkillFolderHash(skill.path);
4221
4279
  const skillPathValue = skillFiles[skill.name];
4222
4280
  await addSkillToLocalLock(skill.name, {
4223
4281
  source: lockSource || parsed.url,
@@ -4391,24 +4449,188 @@ function parseAddOptions(args) {
4391
4449
  }
4392
4450
  i--;
4393
4451
  } else if (arg === "--dangerously-accept-openclaw-risks") options.dangerouslyAcceptOpenclawRisks = true;
4394
- else if (arg && !arg.startsWith("-")) source.push(arg);
4452
+ else if (arg === "-v" || arg === "--version") {
4453
+ i++;
4454
+ options.version = args[i];
4455
+ } else if (arg && !arg.startsWith("-")) source.push(arg);
4395
4456
  }
4396
4457
  return {
4397
4458
  source,
4398
4459
  options
4399
4460
  };
4400
4461
  }
4462
+ function parseAuditOptions(args) {
4463
+ const errors = [];
4464
+ let skillId = "";
4465
+ for (const arg of args) if (arg && !arg.startsWith("-")) {
4466
+ skillId = arg.trim();
4467
+ break;
4468
+ }
4469
+ if (!skillId) errors.push("Missing skill id");
4470
+ return {
4471
+ skillId: normalizeSkillId(skillId),
4472
+ errors
4473
+ };
4474
+ }
4475
+ async function fetchSkillAudit(skillId, timeoutMs = 15e3) {
4476
+ const controller = new AbortController();
4477
+ const timeout = setTimeout(() => controller.abort(), timeoutMs);
4478
+ try {
4479
+ const url = AUDIT_URL.replace("{skill_id}", skillId);
4480
+ let response;
4481
+ try {
4482
+ response = await fetch(url, { signal: controller.signal });
4483
+ } catch {
4484
+ return {
4485
+ status: "error",
4486
+ message: `无法连接审计服务: ${AUDIT_URL}`
4487
+ };
4488
+ }
4489
+ if (response.status === 404) return { status: "not_found" };
4490
+ if (!response.ok) return {
4491
+ status: "error",
4492
+ message: `审计服务返回 HTTP ${response.status}`
4493
+ };
4494
+ let data;
4495
+ try {
4496
+ data = await response.json();
4497
+ } catch {
4498
+ return {
4499
+ status: "error",
4500
+ message: "审计服务响应格式无效"
4501
+ };
4502
+ }
4503
+ if (data.error) return {
4504
+ status: "no_audit",
4505
+ message: String(data.error)
4506
+ };
4507
+ const auditData = data.data || data;
4508
+ return {
4509
+ status: "ok",
4510
+ data: {
4511
+ risk_level: auditData.risk_level ?? "unknown",
4512
+ risk_score: auditData.risk_score ?? null,
4513
+ risk_signals: auditData.risk_signals ?? [],
4514
+ audited_at: auditData.audited_at ?? null,
4515
+ audit_type: auditData.audit_type ?? null,
4516
+ details: auditData.details ?? null
4517
+ }
4518
+ };
4519
+ } finally {
4520
+ clearTimeout(timeout);
4521
+ }
4522
+ }
4523
+ function riskColor(level) {
4524
+ switch (level) {
4525
+ case "critical":
4526
+ case "high": return import_picocolors.default.red;
4527
+ case "medium": return import_picocolors.default.yellow;
4528
+ case "low":
4529
+ case "safe": return import_picocolors.default.green;
4530
+ default: return import_picocolors.default.dim;
4531
+ }
4532
+ }
4533
+ const RISK_LABELS = {
4534
+ critical: "Critical",
4535
+ high: "High",
4536
+ medium: "Medium",
4537
+ low: "Low",
4538
+ safe: "Safe"
4539
+ };
4540
+ function riskLabel(level) {
4541
+ const color = riskColor(level);
4542
+ const label = RISK_LABELS[level] || level || "Unknown";
4543
+ return level === "critical" ? color(import_picocolors.default.bold(label)) : color(label);
4544
+ }
4545
+ function severityColor(severity) {
4546
+ const s = severity.toLowerCase();
4547
+ if (s === "critical" || s === "high") return import_picocolors.default.red(severity);
4548
+ if (s === "medium") return import_picocolors.default.yellow(severity);
4549
+ if (s === "low") return import_picocolors.default.green(severity);
4550
+ return import_picocolors.default.dim(severity);
4551
+ }
4552
+ function buildAuditOutput(skillId, data) {
4553
+ const lines = [];
4554
+ lines.push(`${import_picocolors.default.bold("Skill:")} ${import_picocolors.default.cyan(skillId)}`);
4555
+ const color = riskColor(data.risk_level);
4556
+ const score = data.risk_score != null ? color(`(score ${data.risk_score})`) : color("(score --)");
4557
+ lines.push(`${import_picocolors.default.bold("Risk:")} ${riskLabel(data.risk_level)} ${score}`);
4558
+ if (data.audited_at) lines.push(`${import_picocolors.default.bold("Audited at:")} ${import_picocolors.default.dim(data.audited_at)}`);
4559
+ lines.push("");
4560
+ if (data.risk_signals.length === 0) {
4561
+ lines.push(import_picocolors.default.green("No risk signals detected."));
4562
+ return lines;
4563
+ }
4564
+ lines.push(import_picocolors.default.bold("Risk signals:"));
4565
+ for (const signal of data.risk_signals) {
4566
+ const severity = severityColor(signal.severity || "unknown");
4567
+ lines.push(` ${import_picocolors.default.bold(signal.name)} ${import_picocolors.default.dim(`[${severity}]`)}`);
4568
+ if (signal.description) lines.push(` ${import_picocolors.default.dim(signal.description)}`);
4569
+ }
4570
+ return lines;
4571
+ }
4572
+ async function runAudit(args) {
4573
+ const { skillId, errors } = parseAuditOptions(args);
4574
+ if (errors.length > 0) {
4575
+ for (const error of errors) console.error(import_picocolors.default.red(error));
4576
+ console.error("Usage: wittyhub audit <skill_id>");
4577
+ return;
4578
+ }
4579
+ const result = await fetchSkillAudit(skillId);
4580
+ track({
4581
+ event: "audit",
4582
+ skillId,
4583
+ found: result.status === "ok" ? "1" : "0"
4584
+ });
4585
+ switch (result.status) {
4586
+ case "ok":
4587
+ for (const line of buildAuditOutput(skillId, result.data)) console.log(line);
4588
+ break;
4589
+ case "no_audit":
4590
+ console.log(`${import_picocolors.default.yellow("No audit result yet.")} ${import_picocolors.default.dim(result.message)}`);
4591
+ break;
4592
+ case "not_found":
4593
+ console.error(import_picocolors.default.red(`Skill not found: ${skillId}`));
4594
+ break;
4595
+ case "error":
4596
+ console.error(import_picocolors.default.red(result.message));
4597
+ break;
4598
+ }
4599
+ }
4401
4600
  const RESET$3 = "\x1B[0m";
4402
4601
  const BOLD$3 = "\x1B[1m";
4403
4602
  const DIM$3 = "\x1B[38;5;102m";
4404
4603
  const TEXT$2 = "\x1B[38;5;145m";
4405
4604
  const CYAN$1 = "\x1B[36m";
4406
- const SEARCH_API_BASE = process.env.SKILLS_API_URL || "https://skills.sh";
4407
- function formatInstalls(count) {
4408
- if (!count || count <= 0) return "";
4409
- if (count >= 1e6) return `${(count / 1e6).toFixed(1).replace(/\.0$/, "")}M installs`;
4410
- if (count >= 1e3) return `${(count / 1e3).toFixed(1).replace(/\.0$/, "")}K installs`;
4411
- return `${count} install${count === 1 ? "" : "s"}`;
4605
+ const MAGENTA = "\x1B[35m";
4606
+ const YELLOW$1 = "\x1B[33m";
4607
+ const RED = "\x1B[31m";
4608
+ const GREEN = "\x1B[32m";
4609
+ function formatInstalls$1(count) {
4610
+ if (count === void 0 || count === null || Number.isNaN(count)) return "0";
4611
+ if (count >= 1e6) return `${(count / 1e6).toFixed(1).replace(/\.0$/, "")}M`;
4612
+ if (count >= 1e3) return `${(count / 1e3).toFixed(1).replace(/\.0$/, "")}K`;
4613
+ return String(count);
4614
+ }
4615
+ function formatRiskLevel$1(riskScore) {
4616
+ if (riskScore === void 0 || riskScore === null) return "";
4617
+ if (riskScore <= 20) return `${GREEN}safe${RESET$3}`;
4618
+ if (riskScore <= 50) return `${GREEN}low${RESET$3}`;
4619
+ if (riskScore <= 80) return `${YELLOW$1}medium${RESET$3}`;
4620
+ return `${RED}high${RESET$3}`;
4621
+ }
4622
+ const KV_KEY_WIDTH = 12;
4623
+ function kvKey(key) {
4624
+ return `${DIM$3}${key.padEnd(KV_KEY_WIDTH)}${RESET$3}`;
4625
+ }
4626
+ function escapeRegExp(s) {
4627
+ return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
4628
+ }
4629
+ function highlightMatches(text, query) {
4630
+ if (!text || !query) return text;
4631
+ const terms = query.trim().split(/\s+/).filter(Boolean).map(escapeRegExp);
4632
+ if (terms.length === 0) return text;
4633
+ return text.replace(new RegExp(`(${terms.join("|")})`, "gi"), `${MAGENTA}$1${RESET$3}`);
4412
4634
  }
4413
4635
  const GITHUB_OWNER_PATTERN = /^[a-z0-9](?:[a-z0-9-]{0,38})$/i;
4414
4636
  function parseFindOptions(args) {
@@ -4422,7 +4644,7 @@ function parseFindOptions(args) {
4422
4644
  if (arg === "--owner") {
4423
4645
  const value = args[i + 1];
4424
4646
  if (!value || value.startsWith("-")) {
4425
- errors.push("--owner requires a GitHub owner");
4647
+ errors.push("--owner requires an owner");
4426
4648
  continue;
4427
4649
  }
4428
4650
  ownerValue = value;
@@ -4430,7 +4652,7 @@ function parseFindOptions(args) {
4430
4652
  } else if (arg.startsWith("--owner=")) {
4431
4653
  ownerValue = arg.slice(8);
4432
4654
  if (!ownerValue) {
4433
- errors.push("--owner requires a GitHub owner");
4655
+ errors.push("--owner requires an owner");
4434
4656
  continue;
4435
4657
  }
4436
4658
  } else {
@@ -4439,7 +4661,7 @@ function parseFindOptions(args) {
4439
4661
  }
4440
4662
  const owner = ownerValue.trim().toLowerCase();
4441
4663
  if (!GITHUB_OWNER_PATTERN.test(owner)) {
4442
- errors.push("--owner must be a valid GitHub owner");
4664
+ errors.push("--owner must be a valid owner");
4443
4665
  continue;
4444
4666
  }
4445
4667
  options.owner = owner;
@@ -4451,24 +4673,52 @@ function parseFindOptions(args) {
4451
4673
  };
4452
4674
  }
4453
4675
  async function searchSkillsAPI(query, owner) {
4676
+ const url = `${SEARCH_URL}?${new URLSearchParams({
4677
+ q: query,
4678
+ limit: "10",
4679
+ mode: "text"
4680
+ }).toString()}`;
4681
+ let res;
4454
4682
  try {
4455
- const params = new URLSearchParams({
4456
- q: query,
4457
- limit: "10"
4458
- });
4459
- if (owner) params.set("owner", owner);
4460
- const url = `${SEARCH_API_BASE}/api/search?${params.toString()}`;
4461
- const res = await fetch(url);
4462
- if (!res.ok) return [];
4463
- return (await res.json()).skills.map((skill) => ({
4464
- name: sanitizeMetadata(skill.name),
4465
- slug: sanitizeMetadata(skill.id),
4466
- source: sanitizeMetadata(skill.source || ""),
4467
- installs: skill.installs
4468
- })).sort((a, b) => (b.installs || 0) - (a.installs || 0));
4683
+ res = await fetch(url);
4469
4684
  } catch {
4470
- return [];
4685
+ return {
4686
+ skills: [],
4687
+ error: `无法连接搜索服务: ${SEARCH_URL}`
4688
+ };
4471
4689
  }
4690
+ if (!res.ok) return {
4691
+ skills: [],
4692
+ error: `搜索服务返回 HTTP ${res.status}`
4693
+ };
4694
+ let data;
4695
+ try {
4696
+ data = await res.json();
4697
+ } catch {
4698
+ return {
4699
+ skills: [],
4700
+ error: "搜索服务响应格式无效"
4701
+ };
4702
+ }
4703
+ let skills = (data.data?.results ?? data.results ?? []).map((skill) => ({
4704
+ name: sanitizeMetadata(skill.name),
4705
+ slug: sanitizeMetadata(skill.skill_id),
4706
+ source: sanitizeMetadata(skill.source || ""),
4707
+ installs: skill.download_count || 0,
4708
+ description: skill.description ? sanitizeMetadata(skill.description) : void 0,
4709
+ riskScore: skill.risk_score ?? void 0,
4710
+ sourceUrl: skill.source_url ? sanitizeMetadata(skill.source_url) : void 0
4711
+ }));
4712
+ if (owner) {
4713
+ const ownerLower = owner.toLowerCase();
4714
+ skills = skills.filter((skill) => {
4715
+ if (skill.sourceUrl) try {
4716
+ if (new URL(skill.sourceUrl).pathname.split("/").filter(Boolean)[0]?.toLowerCase() === ownerLower) return true;
4717
+ } catch {}
4718
+ return skill.slug.toLowerCase().includes(`/${ownerLower}/`);
4719
+ });
4720
+ }
4721
+ return { skills: skills.sort((a, b) => (b.installs || 0) - (a.installs || 0)) };
4472
4722
  }
4473
4723
  const HIDE_CURSOR = "\x1B[?25l";
4474
4724
  const SHOW_CURSOR = "\x1B[?25h";
@@ -4480,6 +4730,7 @@ async function runSearchPrompt(initialQuery = "", owner) {
4480
4730
  let selectedIndex = 0;
4481
4731
  let query = initialQuery;
4482
4732
  let loading = false;
4733
+ let searchError = null;
4483
4734
  let debounceTimer = null;
4484
4735
  let lastRenderedLines = 0;
4485
4736
  if (process.stdin.isTTY) process.stdin.setRawMode(true);
@@ -4495,19 +4746,26 @@ async function runSearchPrompt(initialQuery = "", owner) {
4495
4746
  lines.push("");
4496
4747
  if (!query || query.length < 2) lines.push(`${DIM$3}Start typing to search (min 2 chars)${RESET$3}`);
4497
4748
  else if (results.length === 0 && loading) lines.push(`${DIM$3}Searching...${RESET$3}`);
4749
+ else if (results.length === 0 && searchError) lines.push(`${RED}${searchError}${RESET$3}`);
4498
4750
  else if (results.length === 0) lines.push(`${DIM$3}No skills found${RESET$3}`);
4499
4751
  else {
4500
- const visible = results.slice(0, 8);
4752
+ const visible = results.slice(0, 6);
4501
4753
  for (let i = 0; i < visible.length; i++) {
4502
4754
  const skill = visible[i];
4503
4755
  const isSelected = i === selectedIndex;
4504
4756
  const arrow = isSelected ? `${BOLD$3}>${RESET$3}` : " ";
4505
- const name = isSelected ? `${BOLD$3}${skill.name}${RESET$3}` : `${TEXT$2}${skill.name}${RESET$3}`;
4506
- const source = skill.source ? ` ${DIM$3}${skill.source}${RESET$3}` : "";
4507
- const installs = formatInstalls(skill.installs);
4508
- const installsBadge = installs ? ` ${CYAN$1}${installs}${RESET$3}` : "";
4757
+ const hlName = highlightMatches(skill.name, query);
4758
+ const name = isSelected ? `${BOLD$3}${hlName}${RESET$3}` : `${TEXT$2}${hlName}${RESET$3}`;
4759
+ const risk = formatRiskLevel$1(skill.riskScore);
4509
4760
  const loadingIndicator = loading && i === 0 ? ` ${DIM$3}...${RESET$3}` : "";
4510
- lines.push(` ${arrow} ${name}${source}${installsBadge}${loadingIndicator}`);
4761
+ lines.push(` ${arrow} ${kvKey("skill_name:")} ${name}${loadingIndicator}`);
4762
+ lines.push(` ${kvKey("skill_id:")} ${CYAN$1}${skill.slug}${RESET$3}`);
4763
+ lines.push(` ${kvKey("installs:")} ${CYAN$1}${formatInstalls$1(skill.installs)}${RESET$3}`);
4764
+ if (risk) lines.push(` ${kvKey("risk:")} ${risk}`);
4765
+ if (skill.description) {
4766
+ const descPlain = skill.description.length > 80 ? `${skill.description.slice(0, 77)}...` : skill.description;
4767
+ lines.push(` ${kvKey("description:")} ${DIM$3}${highlightMatches(descPlain, query)}${RESET$3}`);
4768
+ }
4511
4769
  }
4512
4770
  }
4513
4771
  lines.push("");
@@ -4523,6 +4781,7 @@ async function runSearchPrompt(initialQuery = "", owner) {
4523
4781
  loading = false;
4524
4782
  if (!q || q.length < 2) {
4525
4783
  results = [];
4784
+ searchError = null;
4526
4785
  selectedIndex = 0;
4527
4786
  render();
4528
4787
  return;
@@ -4532,10 +4791,13 @@ async function runSearchPrompt(initialQuery = "", owner) {
4532
4791
  const debounceMs = Math.max(150, 350 - q.length * 50);
4533
4792
  debounceTimer = setTimeout(async () => {
4534
4793
  try {
4535
- results = await searchSkillsAPI(q, owner);
4794
+ const result = await searchSkillsAPI(q, owner);
4795
+ results = result.skills;
4796
+ searchError = result.error ?? null;
4536
4797
  selectedIndex = 0;
4537
4798
  } catch {
4538
4799
  results = [];
4800
+ searchError = null;
4539
4801
  } finally {
4540
4802
  loading = false;
4541
4803
  debounceTimer = null;
@@ -4592,49 +4854,47 @@ async function runSearchPrompt(initialQuery = "", owner) {
4592
4854
  process.stdin.on("keypress", handleKeypress);
4593
4855
  });
4594
4856
  }
4595
- function getOwnerRepoFromString(pkg) {
4596
- const atIndex = pkg.lastIndexOf("@");
4597
- const match = (atIndex > 0 ? pkg.slice(0, atIndex) : pkg).match(/^([^/]+)\/([^/]+)$/);
4598
- if (match) return {
4599
- owner: match[1],
4600
- repo: match[2]
4601
- };
4602
- return null;
4603
- }
4604
- async function isRepoPublic(owner, repo) {
4605
- return await isRepoPrivate(owner, repo) === false;
4606
- }
4607
4857
  async function runFind(args) {
4608
4858
  const { query, options: findOptions, errors } = parseFindOptions(args);
4609
4859
  const owner = findOptions.owner;
4610
4860
  const isNonInteractive = !process.stdin.isTTY;
4611
4861
  const agentTip = `${DIM$3}Tip: if running in a coding agent, follow these steps:${RESET$3}
4612
4862
  ${DIM$3} 1) npx wittyhub find [query] [--owner <owner>]${RESET$3}
4613
- ${DIM$3} 2) npx wittyhub add <owner/repo@skill>${RESET$3}`;
4863
+ ${DIM$3} 2) npx wittyhub add <skill_id>${RESET$3}`;
4614
4864
  if (errors.length > 0) {
4615
4865
  for (const error of errors) console.error(error);
4616
- console.error("Usage: npx skills find <query> [--owner <owner>]");
4866
+ console.error("Usage: npx wittyhub find <query> [--owner <owner>]");
4617
4867
  return;
4618
4868
  }
4619
4869
  if (query) {
4620
- const results = await searchSkillsAPI(query, owner);
4870
+ const { skills: results, error: searchError } = await searchSkillsAPI(query, owner);
4621
4871
  track({
4622
4872
  event: "find",
4623
4873
  query,
4624
4874
  resultCount: String(results.length)
4625
4875
  });
4876
+ if (searchError) {
4877
+ console.error(`${RED}${searchError}${RESET$3}`);
4878
+ return;
4879
+ }
4626
4880
  if (results.length === 0) {
4627
4881
  const ownerSuffix = owner ? ` from owner "${owner}"` : "";
4628
4882
  console.log(`${DIM$3}No skills found for "${query}"${ownerSuffix}${RESET$3}`);
4629
4883
  return;
4630
4884
  }
4631
- console.log(`${DIM$3}Install with${RESET$3} npx wittyhub add <owner/repo@skill>`);
4885
+ console.log(`${DIM$3}Install with${RESET$3} npx wittyhub add ${TEXT$2}<skill_id>${RESET$3}`);
4632
4886
  console.log();
4633
4887
  for (const skill of results.slice(0, 6)) {
4634
- const pkg = skill.source || skill.slug;
4635
- const installs = formatInstalls(skill.installs);
4636
- console.log(`${TEXT$2}${pkg}@${skill.name}${RESET$3}${installs ? ` ${CYAN$1}${installs}${RESET$3}` : ""}`);
4637
- console.log(`${DIM$3}└ https://skills.sh/${skill.slug}${RESET$3}`);
4888
+ const hlName = highlightMatches(skill.name, query);
4889
+ const risk = formatRiskLevel$1(skill.riskScore);
4890
+ console.log(`${BOLD$3}●${RESET$3} ${kvKey("skill_name:")} ${BOLD$3}${hlName}${RESET$3}`);
4891
+ console.log(` ${kvKey("skill_id:")} ${CYAN$1}${skill.slug}${RESET$3}`);
4892
+ console.log(` ${kvKey("installs:")} ${CYAN$1}${formatInstalls$1(skill.installs)}${RESET$3}`);
4893
+ if (risk) console.log(` ${kvKey("risk:")} ${risk}`);
4894
+ if (skill.description) {
4895
+ const descPlain = skill.description.length > 100 ? `${skill.description.slice(0, 97)}...` : skill.description;
4896
+ console.log(` ${kvKey("description:")} ${DIM$3}${highlightMatches(descPlain, query)}${RESET$3}`);
4897
+ }
4638
4898
  console.log();
4639
4899
  }
4640
4900
  return;
@@ -4657,26 +4917,154 @@ ${DIM$3} 2) npx wittyhub add <owner/repo@skill>${RESET$3}`;
4657
4917
  console.log();
4658
4918
  return;
4659
4919
  }
4660
- const pkg = selected.source || selected.slug;
4920
+ const skillId = selected.slug;
4661
4921
  const skillName = selected.name;
4662
4922
  console.log();
4663
- console.log(`${TEXT$2}Installing ${BOLD$3}${skillName}${RESET$3} from ${DIM$3}${pkg}${RESET$3}...`);
4923
+ console.log(`${TEXT$2}Installing ${BOLD$3}${skillName}${RESET$3} ${DIM$3}(${skillId})${RESET$3}...`);
4664
4924
  console.log();
4665
- const { source, options: addOptions } = parseAddOptions([
4666
- pkg,
4667
- "--skill",
4668
- skillName
4669
- ]);
4925
+ const { source, options: addOptions } = parseAddOptions([skillId]);
4670
4926
  await runAdd(source, addOptions);
4671
4927
  console.log();
4672
- const info = getOwnerRepoFromString(pkg);
4673
- if (info && await isRepoPublic(info.owner, info.repo)) console.log(`${DIM$3}View the skill at${RESET$3} ${TEXT$2}https://skills.sh/${selected.slug}${RESET$3}`);
4674
- else console.log(`${DIM$3}Discover more skills at${RESET$3} ${TEXT$2}https://skills.sh${RESET$3}`);
4928
+ console.log(`${DIM$3}View the skill at${RESET$3} ${TEXT$2}https://skillhub.openeuler.org/skills/${selected.slug}${RESET$3}`);
4675
4929
  console.log();
4676
4930
  }
4931
+ function parseGetOptions(args) {
4932
+ const errors = [];
4933
+ let skillId = "";
4934
+ for (const arg of args) if (arg && !arg.startsWith("-")) {
4935
+ skillId = arg.trim();
4936
+ break;
4937
+ }
4938
+ if (!skillId) errors.push("Missing skill id");
4939
+ return {
4940
+ skillId: normalizeSkillId(skillId),
4941
+ errors
4942
+ };
4943
+ }
4944
+ async function fetchSkillDetail(skillId, timeoutMs = 15e3) {
4945
+ const controller = new AbortController();
4946
+ const timeout = setTimeout(() => controller.abort(), timeoutMs);
4947
+ try {
4948
+ const url = GET_URL.replace("{skill_id}", skillId);
4949
+ let response;
4950
+ try {
4951
+ response = await fetch(url, { signal: controller.signal });
4952
+ } catch {
4953
+ return {
4954
+ status: "error",
4955
+ message: `无法连接详情服务: ${GET_URL}`
4956
+ };
4957
+ }
4958
+ if (response.status === 404) return { status: "not_found" };
4959
+ if (!response.ok) return {
4960
+ status: "error",
4961
+ message: `详情服务返回 HTTP ${response.status}`
4962
+ };
4963
+ let data;
4964
+ try {
4965
+ data = await response.json();
4966
+ } catch {
4967
+ return {
4968
+ status: "error",
4969
+ message: "详情服务响应格式无效"
4970
+ };
4971
+ }
4972
+ if (data.error) return {
4973
+ status: "error",
4974
+ message: String(data.error)
4975
+ };
4976
+ const skillData = data.data || data;
4977
+ return {
4978
+ status: "ok",
4979
+ data: {
4980
+ skill_id: skillData.skill_id ?? skillId,
4981
+ name: skillData.name ?? "",
4982
+ description: skillData.description ?? null,
4983
+ version: skillData.version ?? null,
4984
+ commit_id: skillData.commit_id ?? null,
4985
+ author: skillData.author ?? null,
4986
+ source: skillData.source ?? "",
4987
+ source_url: skillData.source_url ?? "",
4988
+ repo_url: skillData.repo_url ?? null,
4989
+ category: skillData.category ?? null,
4990
+ tags: Array.isArray(skillData.tags) ? skillData.tags : null,
4991
+ platform: skillData.platform ?? null,
4992
+ metadata: skillData.metadata ?? null,
4993
+ risk_score: skillData.risk_score ?? null,
4994
+ download_count: skillData.download_count ?? 0,
4995
+ period_downloads: skillData.period_downloads ?? null,
4996
+ rating: skillData.rating ?? null,
4997
+ created_at: skillData.created_at ?? null,
4998
+ updated_at: skillData.updated_at ?? null,
4999
+ last_indexed_at: skillData.last_indexed_at ?? null
5000
+ }
5001
+ };
5002
+ } finally {
5003
+ clearTimeout(timeout);
5004
+ }
5005
+ }
5006
+ function formatInstalls(count) {
5007
+ if (count === void 0 || count === null || Number.isNaN(count)) return "0 installs";
5008
+ if (count <= 0) return "0 installs";
5009
+ if (count >= 1e6) return `${(count / 1e6).toFixed(1).replace(/\.0$/, "")}M installs`;
5010
+ if (count >= 1e3) return `${(count / 1e3).toFixed(1).replace(/\.0$/, "")}K installs`;
5011
+ return `${count} install${count === 1 ? "" : "s"}`;
5012
+ }
5013
+ function formatRiskLevel(riskScore) {
5014
+ if (riskScore === void 0 || riskScore === null) return import_picocolors.default.dim("unknown risk");
5015
+ if (riskScore <= 20) return import_picocolors.default.green("safe risk");
5016
+ if (riskScore <= 50) return import_picocolors.default.green("low risk");
5017
+ if (riskScore <= 80) return import_picocolors.default.yellow("medium risk");
5018
+ return import_picocolors.default.red("high risk");
5019
+ }
5020
+ function buildGetOutput(data) {
5021
+ const lines = [];
5022
+ lines.push(`${import_picocolors.default.bold("Skill:")} ${import_picocolors.default.cyan(data.skill_id)}`);
5023
+ if (data.name) lines.push(`${import_picocolors.default.bold("Name:")} ${data.name}`);
5024
+ if (data.description) lines.push(`${import_picocolors.default.bold("Description:")} ${data.description}`);
5025
+ if (data.author) lines.push(`${import_picocolors.default.bold("Author:")} ${data.author}`);
5026
+ if (data.category) lines.push(`${import_picocolors.default.bold("Category:")} ${data.category}`);
5027
+ if (data.version) lines.push(`${import_picocolors.default.bold("Version:")} ${data.version}`);
5028
+ if (data.tags && data.tags.length > 0) lines.push(`${import_picocolors.default.bold("Tags:")} ${data.tags.join(", ")}`);
5029
+ if (data.platform) lines.push(`${import_picocolors.default.bold("Platform:")} ${data.platform}`);
5030
+ if (data.source) lines.push(`${import_picocolors.default.bold("Source:")} ${data.source}`);
5031
+ if (data.source_url) lines.push(`${import_picocolors.default.bold("Source URL:")} ${import_picocolors.default.dim(data.source_url)}`);
5032
+ if (data.repo_url) lines.push(`${import_picocolors.default.bold("Repo URL:")} ${import_picocolors.default.dim(data.repo_url)}`);
5033
+ lines.push(`${import_picocolors.default.bold("Installs:")} ${import_picocolors.default.cyan(formatInstalls(data.download_count))}`);
5034
+ const riskLabel = formatRiskLevel(data.risk_score);
5035
+ const score = data.risk_score === void 0 || data.risk_score === null ? "" : import_picocolors.default.dim(` (score ${data.risk_score})`);
5036
+ lines.push(`${import_picocolors.default.bold("Risk:")} ${riskLabel}${score}`);
5037
+ if (data.updated_at) lines.push(`${import_picocolors.default.bold("Updated at:")} ${import_picocolors.default.dim(data.updated_at)}`);
5038
+ return lines;
5039
+ }
5040
+ async function runGet(args) {
5041
+ const { skillId, errors } = parseGetOptions(args);
5042
+ if (errors.length > 0) {
5043
+ for (const error of errors) console.error(import_picocolors.default.red(error));
5044
+ console.error("Usage: wittyhub get <skill_id>");
5045
+ return;
5046
+ }
5047
+ const result = await fetchSkillDetail(skillId);
5048
+ track({
5049
+ event: "get",
5050
+ skillId,
5051
+ found: result.status === "ok" ? "1" : "0"
5052
+ });
5053
+ switch (result.status) {
5054
+ case "ok":
5055
+ for (const line of buildGetOutput(result.data)) console.log(line);
5056
+ break;
5057
+ case "not_found":
5058
+ console.error(import_picocolors.default.red(`Skill not found: ${skillId}`));
5059
+ break;
5060
+ case "error":
5061
+ console.error(import_picocolors.default.red(result.message));
5062
+ break;
5063
+ }
5064
+ }
4677
5065
  const isCancelled = (value) => typeof value === "symbol";
4678
5066
  function shortenPath$1(fullPath, cwd) {
4679
- const home = homedir();
5067
+ const home = homedir$1();
4680
5068
  if (fullPath === home || fullPath.startsWith(home + sep)) return "~" + fullPath.slice(home.length);
4681
5069
  if (fullPath === cwd || fullPath.startsWith(cwd + sep)) return "." + fullPath.slice(cwd.length);
4682
5070
  return fullPath;
@@ -5031,7 +5419,7 @@ const DIM$2 = "\x1B[38;5;102m";
5031
5419
  const CYAN = "\x1B[36m";
5032
5420
  const YELLOW = "\x1B[33m";
5033
5421
  function shortenPath(fullPath, cwd) {
5034
- const home = homedir();
5422
+ const home = homedir$1();
5035
5423
  if (fullPath.startsWith(home)) return fullPath.replace(home, "~");
5036
5424
  if (fullPath.startsWith(cwd)) return "." + fullPath.slice(cwd.length);
5037
5425
  return fullPath;
@@ -5162,6 +5550,26 @@ async function runList(args) {
5162
5550
  console.log();
5163
5551
  }
5164
5552
  }
5553
+ function extractSkillName(input) {
5554
+ try {
5555
+ const parsed = parseSource(input);
5556
+ if (parsed.subpath) {
5557
+ const segments = parsed.subpath.split("/");
5558
+ const last = segments[segments.length - 1];
5559
+ if (last) return last;
5560
+ }
5561
+ if (parsed.type !== "local" && parsed.url) {
5562
+ const match = parsed.url.match(/\/([^/]+)\.git$/);
5563
+ if (match?.[1]) return match[1];
5564
+ }
5565
+ } catch {}
5566
+ if (input.includes("/")) {
5567
+ const segments = input.replace(/\.git$/, "").split("/");
5568
+ const last = segments[segments.length - 1];
5569
+ if (last) return last;
5570
+ }
5571
+ return input;
5572
+ }
5165
5573
  async function removeCommand(skillNames, options) {
5166
5574
  const agentResult = await detectAgent();
5167
5575
  if (agentResult.isAgent) {
@@ -5207,7 +5615,10 @@ async function removeCommand(skillNames, options) {
5207
5615
  let selectedSkills = [];
5208
5616
  if (options.all) selectedSkills = installedSkills;
5209
5617
  else if (skillNames.length > 0) {
5210
- selectedSkills = installedSkills.filter((s) => skillNames.some((name) => name.toLowerCase() === s.toLowerCase()));
5618
+ selectedSkills = installedSkills.filter((s) => skillNames.some((name) => {
5619
+ if (name.toLowerCase() === s.toLowerCase()) return true;
5620
+ return extractSkillName(name).toLowerCase() === s.toLowerCase();
5621
+ }));
5211
5622
  if (selectedSkills.length === 0) {
5212
5623
  M.error(`No matching skills found for: ${skillNames.join(", ")}`);
5213
5624
  return;
@@ -5268,7 +5679,7 @@ async function removeCommand(skillNames, options) {
5268
5679
  for (const pathToCleanup of pathsToCleanup) {
5269
5680
  if (pathToCleanup === canonicalPath) continue;
5270
5681
  try {
5271
- if (await lstat(pathToCleanup).catch(() => null)) await rm(pathToCleanup, {
5682
+ if (await lstat(pathToCleanup).catch(() => null)) await rm$1(pathToCleanup, {
5272
5683
  recursive: true,
5273
5684
  force: true
5274
5685
  });
@@ -5286,7 +5697,7 @@ async function removeCommand(skillNames, options) {
5286
5697
  isStillUsed = true;
5287
5698
  break;
5288
5699
  }
5289
- if (!isStillUsed) await rm(canonicalPath, {
5700
+ if (!isStillUsed) await rm$1(canonicalPath, {
5290
5701
  recursive: true,
5291
5702
  force: true
5292
5703
  });
@@ -5934,7 +6345,7 @@ function buildUsePrompt(input) {
5934
6345
  return sections.join("\n\n") + "\n";
5935
6346
  }
5936
6347
  async function materializeUseSkill(skill) {
5937
- const tempRoot = await mkdtemp(join(tmpdir(), "skills-use-"));
6348
+ const tempRoot = await mkdtemp$1(join(tmpdir$1(), "skills-use-"));
5938
6349
  const skillDir = join(tempRoot, sanitizeName(skill.directoryName || skill.name));
5939
6350
  if (!isPathSafe(tempRoot, skillDir)) throw new Error("Invalid skill name: potential path traversal detected");
5940
6351
  await mkdir(skillDir, { recursive: true });
@@ -6161,8 +6572,8 @@ async function writeSafeFile(targetDir, filePath, contents) {
6161
6572
  const fullPath = join(targetDir, filePath);
6162
6573
  if (!isPathSafe(targetDir, fullPath)) return;
6163
6574
  await mkdir(dirname(fullPath), { recursive: true });
6164
- if (typeof contents === "string") await writeFile(fullPath, contents, "utf-8");
6165
- else await writeFile(fullPath, contents);
6575
+ if (typeof contents === "string") await writeFile$1(fullPath, contents, "utf-8");
6576
+ else await writeFile$1(fullPath, contents);
6166
6577
  }
6167
6578
  async function copySkillDirectory(src, dest) {
6168
6579
  await mkdir(dest, { recursive: true });
@@ -6222,6 +6633,308 @@ function fail(message) {
6222
6633
  process.exit(1);
6223
6634
  }
6224
6635
  var UseCommandError = class extends Error {};
6636
+ const CANONICAL_CATEGORIES = new Set([
6637
+ "Research and Design",
6638
+ "Development and Build",
6639
+ "Engineering and Compilation",
6640
+ "Quality and Validation",
6641
+ "Release and Deployment",
6642
+ "Monitoring and Operations",
6643
+ "Performance Optimization",
6644
+ "Security Hardening",
6645
+ "others"
6646
+ ]);
6647
+ const NAME_PATTERN = /^[a-z][a-z0-9-]*$/;
6648
+ const SEMVER_PATTERN = /^\d+\.\d+\.\d+(?:-[\w.]+)?(?:\+[\w.]+)?$/;
6649
+ function parseValidateOptions(args) {
6650
+ let path = "";
6651
+ const options = {};
6652
+ const errors = [];
6653
+ for (let i = 0; i < args.length; i++) {
6654
+ const arg = args[i];
6655
+ if (arg === "--json") options.json = true;
6656
+ else if (arg === "-h" || arg === "--help") {} else if (arg && !arg.startsWith("-")) if (!path) path = arg.trim();
6657
+ else errors.push(`Unexpected argument: ${arg}`);
6658
+ else if (arg) errors.push(`Unknown option: ${arg}`);
6659
+ }
6660
+ if (!path) errors.push("Missing required argument: <path>");
6661
+ return {
6662
+ path,
6663
+ options,
6664
+ errors
6665
+ };
6666
+ }
6667
+ async function runValidate(args) {
6668
+ const { path, options, errors } = parseValidateOptions(args);
6669
+ if (errors.length > 0) {
6670
+ for (const e of errors) console.error(import_picocolors.default.red(`error: ${e}`));
6671
+ console.error(`\nRun ${import_picocolors.default.cyan("wittyhub validate --help")} for usage.\n`);
6672
+ return null;
6673
+ }
6674
+ const result = await validateSkill(resolve(path));
6675
+ if (options.json) console.log(JSON.stringify(result, null, 2));
6676
+ else printResult(result);
6677
+ return result;
6678
+ }
6679
+ async function validateSkill(absPath) {
6680
+ const errors = [];
6681
+ const warnings = [];
6682
+ let skillDir = absPath;
6683
+ let skillMdPath = null;
6684
+ let stats;
6685
+ try {
6686
+ stats = await stat(absPath);
6687
+ } catch {
6688
+ errors.push({
6689
+ id: "path-not-found",
6690
+ severity: "error",
6691
+ message: `Path does not exist: ${absPath}`,
6692
+ hint: "Pass the skill directory (e.g. ./my-skill) or the SKILL.md file path."
6693
+ });
6694
+ return {
6695
+ path: absPath,
6696
+ errors,
6697
+ warnings,
6698
+ ok: false
6699
+ };
6700
+ }
6701
+ if (stats.isFile()) {
6702
+ const filename = basename(absPath);
6703
+ if (filename !== "SKILL.md") {
6704
+ errors.push({
6705
+ id: "filename-must-be-skill-md",
6706
+ severity: "error",
6707
+ message: `File must be named "SKILL.md" (got "${filename}")`,
6708
+ hint: "Rename the file to SKILL.md (uppercase). The agent skill spec requires this exact filename."
6709
+ });
6710
+ return {
6711
+ path: absPath,
6712
+ errors,
6713
+ warnings,
6714
+ ok: false
6715
+ };
6716
+ }
6717
+ skillMdPath = absPath;
6718
+ skillDir = dirname(absPath);
6719
+ } else if (stats.isDirectory()) {
6720
+ const expected = join(absPath, "SKILL.md");
6721
+ try {
6722
+ await stat(expected);
6723
+ skillMdPath = expected;
6724
+ } catch {
6725
+ errors.push({
6726
+ id: "missing-skill-md",
6727
+ severity: "error",
6728
+ message: `No SKILL.md found in directory: ${absPath}`,
6729
+ hint: "Create one with `wittyhub init <name>` or rename an existing markdown file to SKILL.md (uppercase)."
6730
+ });
6731
+ return {
6732
+ path: absPath,
6733
+ errors,
6734
+ warnings,
6735
+ ok: false
6736
+ };
6737
+ }
6738
+ } else {
6739
+ errors.push({
6740
+ id: "path-invalid-type",
6741
+ severity: "error",
6742
+ message: `Path is neither a file nor a directory: ${absPath}`
6743
+ });
6744
+ return {
6745
+ path: absPath,
6746
+ errors,
6747
+ warnings,
6748
+ ok: false
6749
+ };
6750
+ }
6751
+ try {
6752
+ const lowerMdFile = (await readdir(skillDir)).find((e) => e.toLowerCase() === "skill.md" && e !== "SKILL.md");
6753
+ if (lowerMdFile) warnings.push({
6754
+ id: "case-variant-found",
6755
+ severity: "warning",
6756
+ message: `Found a case-variant file "${lowerMdFile}" alongside SKILL.md`,
6757
+ hint: "Only \"SKILL.md\" (all uppercase) is recognized by the agent spec. Remove the variant to avoid confusion."
6758
+ });
6759
+ } catch {}
6760
+ const content = await readFile(skillMdPath, "utf-8");
6761
+ const match = content.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n?([\s\S]*)$/);
6762
+ if (!match) {
6763
+ errors.push({
6764
+ id: "frontmatter-missing",
6765
+ severity: "error",
6766
+ message: "SKILL.md must start with a YAML frontmatter block delimited by \"---\"",
6767
+ hint: "Add a frontmatter block at the top:\n\n ---\n name: my-skill\n description: what it does\n ---"
6768
+ });
6769
+ return {
6770
+ path: skillMdPath,
6771
+ errors,
6772
+ warnings,
6773
+ ok: false
6774
+ };
6775
+ }
6776
+ match[1];
6777
+ const body = match[2] ?? "";
6778
+ const { data: fm } = parseFrontmatter(content);
6779
+ if (!fm || typeof fm !== "object" || Array.isArray(fm)) {
6780
+ errors.push({
6781
+ id: "frontmatter-not-object",
6782
+ severity: "error",
6783
+ message: "Frontmatter must be a YAML mapping (key-value pairs), not a scalar or list."
6784
+ });
6785
+ return {
6786
+ path: skillMdPath,
6787
+ errors,
6788
+ warnings,
6789
+ ok: false
6790
+ };
6791
+ }
6792
+ if (fm.name === void 0 || fm.name === null || fm.name === "") errors.push({
6793
+ id: "name-required",
6794
+ severity: "error",
6795
+ message: "Missing required frontmatter field: name",
6796
+ hint: "Add `name: <skill-name>` to the frontmatter."
6797
+ });
6798
+ else if (typeof fm.name !== "string") errors.push({
6799
+ id: "name-not-string",
6800
+ severity: "error",
6801
+ message: `Frontmatter field "name" must be a string (got ${typeof fm.name})`,
6802
+ hint: "Wrap the value in quotes if it contains special characters."
6803
+ });
6804
+ else {
6805
+ if (!NAME_PATTERN.test(fm.name)) warnings.push({
6806
+ id: "name-not-kebab",
6807
+ severity: "warning",
6808
+ message: `Skill name "${fm.name}" is not in kebab-case (lowercase letters, digits, hyphens)`,
6809
+ hint: "Use a name like \"add-or-fix-type-checking\" for best compatibility."
6810
+ });
6811
+ if (basename(skillDir) !== fm.name) warnings.push({
6812
+ id: "name-dirname-mismatch",
6813
+ severity: "warning",
6814
+ message: `Skill name "${fm.name}" does not match directory name "${basename(skillDir)}"`,
6815
+ hint: "For consistency, the directory name and `name` field should match."
6816
+ });
6817
+ }
6818
+ if (fm.description === void 0 || fm.description === null || fm.description === "") errors.push({
6819
+ id: "description-required",
6820
+ severity: "error",
6821
+ message: "Missing required frontmatter field: description",
6822
+ hint: "Add `description: <one-line summary>` to the frontmatter."
6823
+ });
6824
+ else if (typeof fm.description !== "string") errors.push({
6825
+ id: "description-not-string",
6826
+ severity: "error",
6827
+ message: `Frontmatter field "description" must be a string (got ${typeof fm.description})`
6828
+ });
6829
+ else {
6830
+ const desc = fm.description.trim();
6831
+ if (desc.length < 10) warnings.push({
6832
+ id: "description-too-short",
6833
+ severity: "warning",
6834
+ message: `Description is very short (${desc.length} chars): "${desc}"`,
6835
+ hint: "Aim for at least 10-20 characters so users can quickly understand the skill."
6836
+ });
6837
+ if (desc.length > 200) warnings.push({
6838
+ id: "description-too-long",
6839
+ severity: "warning",
6840
+ message: `Description is very long (${desc.length} chars)`,
6841
+ hint: "Keep it under 200 characters for readability in listings."
6842
+ });
6843
+ }
6844
+ if (fm.category !== void 0 && fm.category !== null) {
6845
+ if (typeof fm.category !== "string") warnings.push({
6846
+ id: "category-not-string",
6847
+ severity: "warning",
6848
+ message: `Frontmatter field "category" should be a string (got ${typeof fm.category})`
6849
+ });
6850
+ else if (!CANONICAL_CATEGORIES.has(fm.category)) warnings.push({
6851
+ id: "category-not-canonical",
6852
+ severity: "warning",
6853
+ message: `Category "${fm.category}" is not in the canonical list`,
6854
+ hint: `Valid values: ${[...CANONICAL_CATEGORIES].join(", ")}. See https://gitcode.com/openeuler/openEuler-skills.`
6855
+ });
6856
+ }
6857
+ if (fm.version !== void 0 && fm.version !== null && fm.version !== "") {
6858
+ if (typeof fm.version !== "string") warnings.push({
6859
+ id: "version-not-string",
6860
+ severity: "warning",
6861
+ message: `Frontmatter field "version" should be a string (got ${typeof fm.version})`,
6862
+ hint: "Quote the value, e.g. `version: \"1.0.0\"`."
6863
+ });
6864
+ else if (!SEMVER_PATTERN.test(fm.version.trim())) warnings.push({
6865
+ id: "version-not-semver",
6866
+ severity: "warning",
6867
+ message: `Version "${fm.version}" is not a valid semver`,
6868
+ hint: "Use the form X.Y.Z (e.g. 1.0.0, 0.2.1-beta)."
6869
+ });
6870
+ }
6871
+ if (fm.metadata !== void 0 && (typeof fm.metadata !== "object" || Array.isArray(fm.metadata))) warnings.push({
6872
+ id: "metadata-not-object",
6873
+ severity: "warning",
6874
+ message: `Frontmatter field "metadata" should be a mapping (got ${Array.isArray(fm.metadata) ? "array" : typeof fm.metadata})`
6875
+ });
6876
+ if (body.trim().length === 0) warnings.push({
6877
+ id: "body-empty",
6878
+ severity: "warning",
6879
+ message: "SKILL.md has no body content after the frontmatter",
6880
+ hint: "Add at least a heading and a few instructions for the agent to follow."
6881
+ });
6882
+ else if (!/^#{1,6}\s+\S/m.test(body)) warnings.push({
6883
+ id: "body-no-heading",
6884
+ severity: "warning",
6885
+ message: "Body content has no Markdown headings",
6886
+ hint: "Structure the instructions with headings like `## When to use`, `## Instructions`."
6887
+ });
6888
+ const knownKeys = new Set([
6889
+ "name",
6890
+ "description",
6891
+ "version",
6892
+ "category",
6893
+ "metadata"
6894
+ ]);
6895
+ const unknownKeys = Object.keys(fm).filter((k) => !knownKeys.has(k));
6896
+ if (unknownKeys.length > 0) warnings.push({
6897
+ id: "unknown-frontmatter-keys",
6898
+ severity: "warning",
6899
+ message: `Unknown frontmatter keys: ${unknownKeys.join(", ")}`,
6900
+ hint: `Recognized keys: ${[...knownKeys].join(", ")}.`
6901
+ });
6902
+ return {
6903
+ path: skillMdPath,
6904
+ errors,
6905
+ warnings,
6906
+ ok: errors.length === 0
6907
+ };
6908
+ }
6909
+ function printResult(result) {
6910
+ const { path, errors, warnings, ok } = result;
6911
+ console.log(`${import_picocolors.default.cyan("Path:")} ${path}`);
6912
+ console.log();
6913
+ if (errors.length === 0 && warnings.length === 0) {
6914
+ console.log(import_picocolors.default.green(" All checks passed."));
6915
+ console.log();
6916
+ return;
6917
+ }
6918
+ if (errors.length > 0) {
6919
+ console.log(import_picocolors.default.red(`${errors.length} error(s):`));
6920
+ for (const rule of errors) {
6921
+ console.log(` ${import_picocolors.default.red("✗")} [${rule.id}] ${rule.message}`);
6922
+ if (rule.hint) console.log(` ${import_picocolors.default.dim(rule.hint)}`);
6923
+ }
6924
+ console.log();
6925
+ }
6926
+ if (warnings.length > 0) {
6927
+ console.log(import_picocolors.default.yellow(`${warnings.length} warning(s):`));
6928
+ for (const rule of warnings) {
6929
+ console.log(` ${import_picocolors.default.yellow("!")} [${rule.id}] ${rule.message}`);
6930
+ if (rule.hint) console.log(` ${import_picocolors.default.dim(rule.hint)}`);
6931
+ }
6932
+ console.log();
6933
+ }
6934
+ 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" : ""})` : ""));
6935
+ else console.log(import_picocolors.default.red(" Skill is NOT valid. Fix the errors above."));
6936
+ console.log();
6937
+ }
6225
6938
  const __dirname = dirname(fileURLToPath(import.meta.url));
6226
6939
  function getVersion() {
6227
6940
  try {
@@ -6274,11 +6987,12 @@ function showBanner() {
6274
6987
  console.log();
6275
6988
  console.log(` ${DIM}$${RESET} ${TEXT}npx wittyhub experimental_install${RESET} ${DIM}Restore from skills-lock.json${RESET}`);
6276
6989
  console.log(` ${DIM}$${RESET} ${TEXT}npx wittyhub init ${DIM}[name]${RESET} ${DIM}Create a new skill${RESET}`);
6990
+ console.log(` ${DIM}$${RESET} ${TEXT}npx wittyhub validate ${DIM}<path>${RESET} ${DIM}Validate a local skill${RESET}`);
6277
6991
  console.log(` ${DIM}$${RESET} ${TEXT}npx wittyhub experimental_sync${RESET} ${DIM}Sync skills from node_modules${RESET}`);
6278
6992
  console.log();
6279
- console.log(`${DIM}try:${RESET} npx wittyhub add vercel-labs/agent-skills`);
6993
+ console.log(`${DIM}try:${RESET} npx wittyhub add gitcode:openeuler/witty-diagnosis-agent`);
6280
6994
  console.log();
6281
- console.log(`Discover more skills at ${TEXT}https://skills.sh/${RESET}`);
6995
+ console.log(`Discover more skills at ${TEXT}https://skillhub.openeuler.org/${RESET}`);
6282
6996
  console.log();
6283
6997
  }
6284
6998
  function showHelp() {
@@ -6294,6 +7008,10 @@ ${BOLD}Manage Skills:${RESET}
6294
7008
  remove [skills] Remove installed skills
6295
7009
  list, ls List installed skills
6296
7010
  find [query] Search for skills interactively
7011
+ audit <skill_id> Show security audit result (risk level + risk signals)
7012
+ get <skill_id> View skill details (author/category/version/description/tags)
7013
+ validate <path> Validate a local skill for spec compliance (SKILL.md filename,
7014
+ frontmatter, required fields name/description, etc.)
6297
7015
 
6298
7016
  ${BOLD}Find Options:${RESET}
6299
7017
  --owner <owner> Search only repositories from a GitHub owner
@@ -6308,7 +7026,7 @@ ${BOLD}Update Options:${RESET}
6308
7026
 
6309
7027
  ${BOLD}Project:${RESET}
6310
7028
  experimental_install Restore skills from skills-lock.json
6311
- init [name] Initialize a skill (creates <name>/SKILL.md or ./SKILL.md)
7029
+ init [name] Initialize a skill (creates <name>/SKILL.md; defaults to my-skill/)
6312
7030
  experimental_sync Sync skills from node_modules into agent directories
6313
7031
 
6314
7032
  ${BOLD}Add Options:${RESET}
@@ -6321,6 +7039,7 @@ ${BOLD}Add Options:${RESET}
6321
7039
  --subagent <names> Install to Eve subagents (use 'root' for the root agent)
6322
7040
  --all Shorthand for --skill '*' --agent '*' -y
6323
7041
  --full-depth Search all subdirectories even when a root SKILL.md exists
7042
+ -v, --version <ver> Download a specific skill version from skill_versions table
6324
7043
 
6325
7044
  ${BOLD}Use Options:${RESET}
6326
7045
  -s, --skill <skill> Specify the skill to use
@@ -6350,12 +7069,9 @@ ${BOLD}Options:${RESET}
6350
7069
  --version, -v Show version number
6351
7070
 
6352
7071
  ${BOLD}Examples:${RESET}
6353
- ${DIM}$${RESET} wittyhub add vercel-labs/agent-skills
6354
- ${DIM}$${RESET} wittyhub use vercel-labs/agent-skills@vercel-optimize | claude
6355
- ${DIM}$${RESET} wittyhub use vercel-labs/agent-skills --skill vercel-optimize --agent claude-code
6356
- ${DIM}$${RESET} wittyhub add vercel-labs/agent-skills -g
6357
- ${DIM}$${RESET} wittyhub add vercel-labs/agent-skills --agent claude-code cursor
6358
- ${DIM}$${RESET} wittyhub add vercel-labs/agent-skills --skill pr-review commit
7072
+ ${DIM}$${RESET} wittyhub add https://gitcode.com/openeuler/witty-diagnosis-agent --skill system-resource-diagnosis
7073
+ ${DIM}$${RESET} wittyhub add https://gitcode.com/openeuler/witty-diagnosis-agent --skill system-resource-diagnosis -g
7074
+ ${DIM}$${RESET} wittyhub add https://gitcode.com/openeuler/witty-diagnosis-agent --skill system-resource-diagnosis --agent claude-code cursor
6359
7075
  ${DIM}$${RESET} wittyhub remove ${DIM}# interactive remove${RESET}
6360
7076
  ${DIM}$${RESET} wittyhub remove web-design ${DIM}# remove by name${RESET}
6361
7077
  ${DIM}$${RESET} wittyhub rm --global frontend-design
@@ -6374,7 +7090,45 @@ ${BOLD}Examples:${RESET}
6374
7090
  ${DIM}$${RESET} wittyhub experimental_sync ${DIM}# sync from node_modules${RESET}
6375
7091
  ${DIM}$${RESET} wittyhub experimental_sync -y ${DIM}# sync without prompts${RESET}
6376
7092
 
6377
- Discover more skills at ${TEXT}https://skills.sh/${RESET}
7093
+ Discover more skills at ${TEXT}https://skillhub.openeuler.org/${RESET}
7094
+ `);
7095
+ }
7096
+ function showInitHelp() {
7097
+ console.log(`
7098
+ ${BOLD}Usage:${RESET} wittyhub init [name]
7099
+
7100
+ ${BOLD}Description:${RESET}
7101
+ Initialize a new skill by creating a SKILL.md template.
7102
+
7103
+ ${BOLD}Arguments:${RESET}
7104
+ name Optional skill name. Creates <name>/SKILL.md.
7105
+ If omitted, defaults to my-skill/ (never the cwd itself).
7106
+
7107
+ ${BOLD}Examples:${RESET}
7108
+ ${DIM}$${RESET} wittyhub init ${DIM}# creates my-skill/SKILL.md${RESET}
7109
+ ${DIM}$${RESET} wittyhub init my-skill ${DIM}# creates my-skill/SKILL.md${RESET}
7110
+ ${DIM}$${RESET} wittyhub init code-reviewer ${DIM}# creates code-reviewer/SKILL.md${RESET}
7111
+ `);
7112
+ }
7113
+ function showValidateHelp() {
7114
+ console.log(`
7115
+ ${BOLD}Usage:${RESET} wittyhub validate <path> [options]
7116
+
7117
+ ${BOLD}Description:${RESET}
7118
+ Validate a local skill for spec compliance (SKILL.md filename, frontmatter,
7119
+ required fields, etc.). Reports errors (must-fix) and warnings (recommended).
7120
+
7121
+ ${BOLD}Arguments:${RESET}
7122
+ path Path to the skill directory or SKILL.md file.
7123
+
7124
+ ${BOLD}Options:${RESET}
7125
+ --json Output result as JSON (no ANSI colors).
7126
+ -h, --help Show this help message.
7127
+
7128
+ ${BOLD}Examples:${RESET}
7129
+ ${DIM}$${RESET} wittyhub validate ./my-skill
7130
+ ${DIM}$${RESET} wittyhub validate ./my-skill/SKILL.md
7131
+ ${DIM}$${RESET} wittyhub validate ./my-skill --json
6378
7132
  `);
6379
7133
  }
6380
7134
  function showRemoveHelp() {
@@ -6404,24 +7158,25 @@ ${BOLD}Examples:${RESET}
6404
7158
  ${DIM}$${RESET} wittyhub remove --all ${DIM}# remove all skills${RESET}
6405
7159
  ${DIM}$${RESET} wittyhub remove --skill '*' -a cursor ${DIM}# remove all skills from cursor${RESET}
6406
7160
 
6407
- Discover more skills at ${TEXT}https://skills.sh/${RESET}
7161
+ Discover more skills at ${TEXT}https://skillhub.openeuler.org/${RESET}
6408
7162
  `);
6409
7163
  }
6410
7164
  function runInit(args) {
6411
7165
  const cwd = process.cwd();
6412
- const skillName = args[0] || basename(cwd);
6413
- const hasName = args[0] !== void 0;
6414
- const skillDir = hasName ? join(cwd, skillName) : cwd;
7166
+ const skillName = args[0] ?? "my-skill";
7167
+ const skillDir = join(cwd, skillName);
6415
7168
  const skillFile = join(skillDir, "SKILL.md");
6416
- const displayPath = hasName ? `${skillName}/SKILL.md` : "SKILL.md";
7169
+ const displayPath = `${skillName}/SKILL.md`;
6417
7170
  if (existsSync(skillFile)) {
6418
7171
  console.log(`${TEXT}Skill already exists at ${DIM}${displayPath}${RESET}`);
6419
7172
  return;
6420
7173
  }
6421
- if (hasName) mkdirSync(skillDir, { recursive: true });
7174
+ mkdirSync(skillDir, { recursive: true });
6422
7175
  writeFileSync(skillFile, `---
6423
7176
  name: ${skillName}
6424
7177
  description: A brief description of what this skill does
7178
+ version: 0.1.0 # optional; increment as needed
7179
+ category: others # optional;change if needed — full list at https://gitcode.com/openeuler/openEuler-skills
6425
7180
  ---
6426
7181
 
6427
7182
  # ${skillName}
@@ -6446,12 +7201,12 @@ Describe when this skill should be used.
6446
7201
  console.log(`${DIM}Next steps:${RESET}`);
6447
7202
  console.log(` 1. Edit ${TEXT}${displayPath}${RESET} to define your skill instructions`);
6448
7203
  console.log(` 2. Update the ${TEXT}name${RESET} and ${TEXT}description${RESET} in the frontmatter`);
7204
+ console.log(` 3. Optionally set ${TEXT}version${RESET} and ${TEXT}category${RESET} (see the comment in ${TEXT}${displayPath}${RESET} for the full list of categories)`);
6449
7205
  console.log();
6450
7206
  console.log(`${DIM}Publishing:${RESET}`);
6451
- console.log(` ${DIM}GitHub:${RESET} Push to a repo, then ${TEXT}npx wittyhub add <owner>/<repo>${RESET}`);
6452
- console.log(` ${DIM}URL:${RESET} Host the file, then ${TEXT}npx wittyhub add https://example.com/${displayPath}${RESET}`);
7207
+ console.log(` Submit your skill to ${TEXT}https://gitcode.com/openeuler/openEuler-skills${RESET}`);
6453
7208
  console.log();
6454
- console.log(`Browse existing skills for inspiration at ${TEXT}https://skills.sh/${RESET}`);
7209
+ console.log(`Browse existing skills for inspiration at ${TEXT}https://skillhub.openeuler.org/${RESET}`);
6455
7210
  console.log();
6456
7211
  }
6457
7212
  async function main() {
@@ -6473,6 +7228,10 @@ async function main() {
6473
7228
  await runFind(restArgs);
6474
7229
  break;
6475
7230
  case "init":
7231
+ if (restArgs.includes("--help") || restArgs.includes("-h")) {
7232
+ showInitHelp();
7233
+ break;
7234
+ }
6476
7235
  if (!inAgent) showLogo();
6477
7236
  console.log();
6478
7237
  runInit(restArgs);
@@ -6515,6 +7274,24 @@ async function main() {
6515
7274
  case "ls":
6516
7275
  await runList(restArgs);
6517
7276
  break;
7277
+ case "audit":
7278
+ if (!inAgent) showLogo();
7279
+ await runAudit(restArgs);
7280
+ break;
7281
+ case "get":
7282
+ if (!inAgent) showLogo();
7283
+ await runGet(restArgs);
7284
+ break;
7285
+ case "validate":
7286
+ case "check-skill":
7287
+ if (restArgs.includes("--help") || restArgs.includes("-h")) {
7288
+ showValidateHelp();
7289
+ break;
7290
+ }
7291
+ if (!inAgent) showLogo();
7292
+ console.log();
7293
+ await runValidate(restArgs);
7294
+ break;
6518
7295
  case "check":
6519
7296
  case "update":
6520
7297
  case "upgrade":