wittyhub 0.0.4 → 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.
Files changed (3) hide show
  1. package/README.md +149 -97
  2. package/dist/cli.mjs +557 -392
  3. package/package.json +1 -1
package/dist/cli.mjs CHANGED
@@ -14,8 +14,9 @@ import { createHash } from "node:crypto";
14
14
  import { execFile } from "node:child_process";
15
15
  import { promisify, stripVTControlCharacters } from "node:util";
16
16
  import { mkdtemp, rm, writeFile } from "node:fs/promises";
17
+ import { existsSync as existsSync$1, statSync } from "node:fs";
17
18
  import { homedir, tmpdir } from "node:os";
18
- import { dirname as dirname$1, join as join$1, sep as sep$1 } from "node:path";
19
+ import { dirname as dirname$1, isAbsolute as isAbsolute$1, join as join$1, resolve as resolve$1, sep as sep$1 } from "node:path";
19
20
  import { homedir as homedir$1, platform, tmpdir as tmpdir$1 } from "os";
20
21
  import { parse } from "yaml";
21
22
  import * as readline from "readline";
@@ -59,15 +60,6 @@ function parseOwnerRepo(ownerRepo) {
59
60
  };
60
61
  return null;
61
62
  }
62
- async function isRepoPrivate(owner, repo) {
63
- try {
64
- const res = await fetch(`https://api.github.com/repos/${owner}/${repo}`);
65
- if (!res.ok) return null;
66
- return (await res.json()).private === true;
67
- } catch {
68
- return null;
69
- }
70
- }
71
63
  function sanitizeSubpath(subpath) {
72
64
  const segments = subpath.replace(/\\/g, "/").split("/");
73
65
  for (const segment of segments) if (segment === "..") throw new Error(`Unsafe subpath: "${subpath}" contains path traversal segments. Subpaths must not contain ".." components.`);
@@ -266,19 +258,145 @@ function loadCliConfig() {
266
258
  return {};
267
259
  }
268
260
  }
269
- const DEFAULT_API_URL = "https://skillhub.openeuler.org/api/v1";
270
- const DEFAULT_SEARCH_URL = "https://skillhub.openeuler.org/api/v1/index/search";
271
- const DEFAULT_TELEMETRY_URL = "https://skillhub.openeuler.org/api/v1/skills/telemetry";
272
- const DEFAULT_AUDIT_URL = "https://skillhub.openeuler.org/api/v1/skills/{skill_id}/audit";
273
- const DEFAULT_GET_URL = "https://skillhub.openeuler.org/api/v1/skills/{skill_id}";
274
- const DEFAULT_LIST_URL = "https://skillhub.openeuler.org/api/v1/skills/";
275
261
  const cliConfig = loadCliConfig();
276
- const DOWNLOAD_URL = `${typeof cliConfig.api_url === "string" && cliConfig.api_url.trim() ? cliConfig.api_url.trim() : process.env.SKILLS_API_URL ? `${process.env.SKILLS_API_URL.replace(/\/$/, "")}/api/v1` : DEFAULT_API_URL}/skills/{skill_id}/download`;
277
- const SEARCH_URL = typeof cliConfig.search_url === "string" && cliConfig.search_url.trim() ? cliConfig.search_url.trim() : process.env.SKILLS_API_URL ? `${process.env.SKILLS_API_URL.replace(/\/$/, "")}/api/v1/index/search` : DEFAULT_SEARCH_URL;
278
- const TELEMETRY_URL = typeof cliConfig.telemetry_url === "string" && cliConfig.telemetry_url.trim() ? cliConfig.telemetry_url.trim() : DEFAULT_TELEMETRY_URL;
279
- const AUDIT_URL = typeof cliConfig.audit_url === "string" && cliConfig.audit_url.trim() ? cliConfig.audit_url.trim() : DEFAULT_AUDIT_URL;
280
- const GET_URL = typeof cliConfig.get_url === "string" && cliConfig.get_url.trim() ? cliConfig.get_url.trim() : DEFAULT_GET_URL;
281
- const LIST_URL = typeof cliConfig.list_url === "string" && cliConfig.list_url.trim() ? cliConfig.list_url.trim() : GET_URL.replace(/\{skill_id\}[^/]*$/, "") || DEFAULT_LIST_URL;
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
+ }
282
400
  const CSI_RE = /\x1b\[[\x30-\x3f]*[\x20-\x2f]*[\x40-\x7e]/g;
283
401
  const OSC_RE = /\x1b\][\s\S]*?(?:\x07|\x1b\\)/g;
284
402
  const DCS_PM_APC_RE = /\x1b[P^_][\s\S]*?(?:\x1b\\)/g;
@@ -541,12 +659,6 @@ function isAuthFailure(message) {
541
659
  function createGitClient(extraEnv) {
542
660
  return esm_default({
543
661
  timeout: { block: CLONE_TIMEOUT_MS },
544
- env: {
545
- ...process.env,
546
- GIT_TERMINAL_PROMPT: "0",
547
- GIT_LFS_SKIP_SMUDGE: "1",
548
- ...extraEnv
549
- },
550
662
  config: [
551
663
  "filter.lfs.required=false",
552
664
  "filter.lfs.smudge=",
@@ -554,6 +666,11 @@ function createGitClient(extraEnv) {
554
666
  "filter.lfs.process="
555
667
  ],
556
668
  unsafe: { allowUnsafeFilter: true }
669
+ }).env({
670
+ ...process.env,
671
+ GIT_TERMINAL_PROMPT: "0",
672
+ GIT_LFS_SKIP_SMUDGE: "1",
673
+ ...extraEnv
557
674
  });
558
675
  }
559
676
  async function resetTempDir(dir) {
@@ -856,13 +973,14 @@ async function parseSkillMd(skillMdPath, options) {
856
973
  const { data } = parseFrontmatter(content);
857
974
  if (!data.name || !data.description) return null;
858
975
  if (typeof data.name !== "string" || typeof data.description !== "string") return null;
859
- 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;
860
978
  return {
861
979
  name: sanitizeMetadata(data.name),
862
980
  description: sanitizeMetadata(data.description),
863
981
  path: dirname(skillMdPath),
864
982
  rawContent: content,
865
- metadata: data.metadata
983
+ metadata
866
984
  };
867
985
  } catch {
868
986
  return null;
@@ -2207,99 +2325,6 @@ async function listInstalledSkills(options = {}) {
2207
2325
  } catch {}
2208
2326
  return Array.from(skillsMap.values());
2209
2327
  }
2210
- let cliVersion = null;
2211
- let detectedAgentName = null;
2212
- function setDetectedAgent(agentName) {
2213
- detectedAgentName = agentName;
2214
- }
2215
- function isCI() {
2216
- 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);
2217
- }
2218
- function isEnabled() {
2219
- return !process.env.DISABLE_TELEMETRY && !process.env.DO_NOT_TRACK;
2220
- }
2221
- function setVersion(version) {
2222
- cliVersion = version;
2223
- }
2224
- function slugifyTelemetryValue(value) {
2225
- const lowered = value.trim().toLowerCase();
2226
- if (!lowered) return "";
2227
- let normalized = lowered.replace(/[^a-z0-9._-]+/g, "-");
2228
- normalized = normalized.replace(/-{2,}/g, "-");
2229
- return normalized.replace(/^-|-$/g, "");
2230
- }
2231
- function buildSkillId(sourceType, ownerRepo, skillName, skillFiles) {
2232
- if (![
2233
- "github",
2234
- "gitcode",
2235
- "gitlab",
2236
- "gitee"
2237
- ].includes(sourceType)) return null;
2238
- if (!ownerRepo) return null;
2239
- const slugifiedOwnerRepo = ownerRepo.split("/").map(slugifyTelemetryValue).join("/");
2240
- if (skillFiles) {
2241
- const relativePath = skillFiles[skillName];
2242
- if (relativePath) {
2243
- const normalizedPath = relativePath.trim().replace(/\\/g, "/").replace(/\/+$/, "");
2244
- if (normalizedPath === "SKILL.md") return `${sourceType}/${slugifiedOwnerRepo}/${slugifiedOwnerRepo.split("/").pop()}`;
2245
- if (normalizedPath.endsWith("/SKILL.md")) return `${sourceType}/${slugifiedOwnerRepo}/${normalizedPath.slice(0, -9)}`;
2246
- }
2247
- }
2248
- const skillPath = slugifyTelemetryValue(skillName);
2249
- if (!skillPath) return null;
2250
- return `${sourceType}/${slugifiedOwnerRepo}/${skillPath}`;
2251
- }
2252
- async function fetchAuditData(source, skillSlugs, sourceType = "github", skillFiles, timeoutMs = 15e3) {
2253
- if (skillSlugs.length === 0) return null;
2254
- const controller = new AbortController();
2255
- const timeout = setTimeout(() => controller.abort(), timeoutMs);
2256
- try {
2257
- const results = {};
2258
- const promises = skillSlugs.map(async (skillName) => {
2259
- const skillId = buildSkillId(sourceType, source, skillName, skillFiles);
2260
- if (!skillId) return;
2261
- try {
2262
- const response = await fetch(AUDIT_URL.replace("{skill_id}", skillId.split("/").map(encodeURIComponent).join("/")), { signal: controller.signal });
2263
- if (response.ok) {
2264
- const data = await response.json();
2265
- if (!data.error) {
2266
- const auditData = data.data || data;
2267
- results[skillName] = {
2268
- risk_level: auditData.risk_level ?? "unknown",
2269
- risk_score: auditData.risk_score ?? null,
2270
- risk_signals: auditData.risk_signals ?? [],
2271
- audited_at: auditData.audited_at ?? null
2272
- };
2273
- }
2274
- }
2275
- } catch {}
2276
- });
2277
- await Promise.all(promises);
2278
- return Object.keys(results).length > 0 ? results : null;
2279
- } catch {
2280
- return null;
2281
- } finally {
2282
- clearTimeout(timeout);
2283
- }
2284
- }
2285
- const pendingTelemetry = [];
2286
- function track(data) {
2287
- if (!isEnabled()) return;
2288
- try {
2289
- const params = new URLSearchParams();
2290
- if (cliVersion) params.set("v", cliVersion);
2291
- if (isCI()) params.set("ci", "1");
2292
- if (detectedAgentName) params.set("agent", detectedAgentName);
2293
- for (const [key, value] of Object.entries(data)) if (value !== void 0 && value !== null) params.set(key, String(value));
2294
- const p = fetch(`${TELEMETRY_URL}?${params.toString()}`).catch(() => {}).then(() => {});
2295
- pendingTelemetry.push(p);
2296
- } catch {}
2297
- }
2298
- async function flushTelemetry(timeoutMs = 5e3) {
2299
- if (pendingTelemetry.length === 0) return;
2300
- const timeout = new Promise((resolve) => setTimeout(resolve, timeoutMs));
2301
- await Promise.race([Promise.all(pendingTelemetry), timeout]);
2302
- }
2303
2328
  var import_dist = require_dist();
2304
2329
  let cachedResult = null;
2305
2330
  const agentNameToType = {
@@ -3153,7 +3178,7 @@ async function tryBlobInstall(ownerRepo, options = {}) {
3153
3178
  tree
3154
3179
  };
3155
3180
  }
3156
- var version$1 = "0.0.4";
3181
+ var version$1 = "0.0.5";
3157
3182
  const isCancelled$1 = (value) => typeof value === "symbol";
3158
3183
  function getLockSource(parsedUrl, normalizedSource) {
3159
3184
  return parsedUrl.startsWith("git@") || parsedUrl.startsWith("ssh://") ? parsedUrl : normalizedSource;
@@ -3170,14 +3195,37 @@ function normalizeRepoRef(input) {
3170
3195
  s = s.replace(/\/+$/, "");
3171
3196
  return s;
3172
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
+ }
3173
3215
  function looksLikeRepoSource(source) {
3174
3216
  const s = source.trim();
3175
3217
  if (s.startsWith("http://") || s.startsWith("https://")) return true;
3176
3218
  if (s.startsWith("git@") || s.startsWith("ssh://") || s.startsWith("git://")) return true;
3177
3219
  return (s.match(/\//g) || []).length <= 1 && !s.includes(":") && !/\s/.test(s);
3178
3220
  }
3179
- async function resolveSkillId$1(source, skillName) {
3180
- if (!looksLikeRepoSource(source)) return source;
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);
3181
3229
  if (!skillName || !skillName.trim()) throw new Error(`源 "${source}" 看起来是仓库地址,需要同时传入 --skill <skill_name> 才能定位具体技能。`);
3182
3230
  const searchUrl = `${SEARCH_URL}?${new URLSearchParams({
3183
3231
  q: skillName.trim(),
@@ -3203,11 +3251,15 @@ async function resolveSkillId$1(source, skillName) {
3203
3251
  if (!match) throw new Error(`未找到名称匹配 "${skillName}" 的技能。`);
3204
3252
  return match.skill_id;
3205
3253
  }
3206
- async function downloadAndExtractSkill(skillId) {
3207
- const downloadUrl = DOWNLOAD_URL.replace("{skill_id}", encodeURIComponent(skillId));
3254
+ async function downloadAndExtractSkill(skillId, targetDir, version) {
3255
+ let downloadUrl = DOWNLOAD_URL.replace("{skill_id}", skillId);
3256
+ if (version) downloadUrl += `?version=${encodeURIComponent(version)}`;
3208
3257
  const response = await fetch(downloadUrl);
3209
- if (!response.ok) throw new Error(`HTTP ${response.status} ${response.statusText}`);
3210
- const tempDir = await mkdtemp(join$1(tmpdir(), "skills-"));
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-"));
3211
3263
  const zipPath = join$1(tempDir, ".skill.zip");
3212
3264
  try {
3213
3265
  await writeFile(zipPath, Buffer.from(await response.arrayBuffer()));
@@ -3365,12 +3417,28 @@ function buildCopyResultLines(results, cwd) {
3365
3417
  }
3366
3418
  return lines;
3367
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
+ }
3368
3436
  function multiselect(opts) {
3369
- return fe({
3437
+ return withQuitOnQ(fe({
3370
3438
  ...opts,
3371
3439
  options: opts.options,
3372
- message: `${opts.message} ${import_picocolors.default.dim("(space to toggle)")}`
3373
- });
3440
+ message: `${opts.message} ${import_picocolors.default.dim("(space to toggle, q to quit)")}`
3441
+ }));
3374
3442
  }
3375
3443
  async function promptForAgents(message, choices) {
3376
3444
  let lastSelected;
@@ -3431,6 +3499,171 @@ async function selectAgentsInteractive(options) {
3431
3499
  return selected;
3432
3500
  }
3433
3501
  setVersion(version$1);
3502
+ async function installLocalSkills(source, localDir, skills, options, agentResult) {
3503
+ const spinner = Y();
3504
+ if (options.list) {
3505
+ console.log();
3506
+ M.step(import_picocolors.default.bold("Available Skills"));
3507
+ for (const skill of skills) {
3508
+ M.message(` ${import_picocolors.default.cyan(getSkillDisplayName(skill))}`);
3509
+ M.message(` ${import_picocolors.default.dim(skill.description)}`);
3510
+ }
3511
+ console.log();
3512
+ Se("Use --skill <name> to install specific skills");
3513
+ return;
3514
+ }
3515
+ let selectedSkills;
3516
+ if (options.skill?.includes("*")) {
3517
+ selectedSkills = skills;
3518
+ M.info(`Installing all ${skills.length} skills`);
3519
+ } else if (options.skill && options.skill.length > 0) {
3520
+ selectedSkills = filterSkills(skills, options.skill);
3521
+ if (selectedSkills.length === 0) {
3522
+ M.error(`No matching skills found for: ${options.skill.join(", ")}`);
3523
+ M.info("Available skills:");
3524
+ for (const s of skills) M.message(` - ${getSkillDisplayName(s)}`);
3525
+ process.exit(1);
3526
+ }
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) {
3529
+ selectedSkills = 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`);
3534
+ } else {
3535
+ const selected = await multiselect({
3536
+ message: "Select skills to install",
3537
+ options: [...skills].sort((a, b) => getSkillDisplayName(a).localeCompare(getSkillDisplayName(b))).map((s) => ({
3538
+ value: s,
3539
+ label: getSkillDisplayName(s),
3540
+ hint: s.description.length > 60 ? s.description.slice(0, 57) + "..." : s.description
3541
+ })),
3542
+ required: true
3543
+ });
3544
+ if (pD(selected)) {
3545
+ xe("Installation cancelled");
3546
+ process.exit(0);
3547
+ }
3548
+ selectedSkills = selected;
3549
+ }
3550
+ let targetAgents;
3551
+ const validAgents = Object.keys(agents);
3552
+ if (options.agent?.includes("*")) {
3553
+ targetAgents = validAgents;
3554
+ M.info(`Installing to all ${targetAgents.length} agents`);
3555
+ } else if (options.agent && options.agent.length > 0) {
3556
+ const invalidAgents = options.agent.filter((a) => !validAgents.includes(a));
3557
+ if (invalidAgents.length > 0) {
3558
+ M.error(`Invalid agents: ${invalidAgents.join(", ")}`);
3559
+ M.info(`Valid agents: ${validAgents.join(", ")}`);
3560
+ process.exit(1);
3561
+ }
3562
+ targetAgents = options.agent;
3563
+ } else {
3564
+ spinner.start("Loading agents...");
3565
+ const installedAgents = await detectInstalledAgents();
3566
+ const totalAgents = Object.keys(agents).length;
3567
+ spinner.stop(`${totalAgents} agents`);
3568
+ if (installedAgents.length === 0) if (options.yes) {
3569
+ targetAgents = validAgents;
3570
+ M.info("Installing to all agents");
3571
+ } else {
3572
+ M.info("Select agents to install skills to");
3573
+ const selected = await promptForAgents("Which agents do you want to install to?", Object.entries(agents).filter(([key]) => key !== "eve").map(([key, config]) => ({
3574
+ value: key,
3575
+ label: config.displayName
3576
+ })));
3577
+ if (pD(selected)) {
3578
+ xe("Installation cancelled");
3579
+ process.exit(0);
3580
+ }
3581
+ targetAgents = selected;
3582
+ }
3583
+ else {
3584
+ targetAgents = ensureUniversalAgents(installedAgents);
3585
+ M.info(`Installing to: ${installedAgents.map((a) => import_picocolors.default.cyan(agents[a].displayName)).join(", ")}`);
3586
+ }
3587
+ }
3588
+ const installTargets = buildInstallTargets(targetAgents, [void 0]);
3589
+ let installGlobally = options.global ?? false;
3590
+ if (options.global === void 0 && !options.yes) {
3591
+ const scope = await ve({
3592
+ message: "Installation scope",
3593
+ options: [{
3594
+ value: false,
3595
+ label: "Project",
3596
+ hint: "Install in current directory (committed with your project)"
3597
+ }, {
3598
+ value: true,
3599
+ label: "Global",
3600
+ hint: "Install in home directory (available across all projects)"
3601
+ }]
3602
+ });
3603
+ if (pD(scope)) {
3604
+ xe("Installation cancelled");
3605
+ process.exit(0);
3606
+ }
3607
+ installGlobally = scope;
3608
+ }
3609
+ if (!options.yes) {
3610
+ const confirmed = await ye({
3611
+ message: "Proceed with installation?",
3612
+ initialValue: true
3613
+ });
3614
+ if (pD(confirmed) || !confirmed) {
3615
+ xe("Installation cancelled");
3616
+ process.exit(0);
3617
+ }
3618
+ }
3619
+ spinner.start("Installing skills...");
3620
+ const results = [];
3621
+ for (const skill of selectedSkills) for (const target of installTargets) {
3622
+ const result = await installSkillForAgent(skill, target.agent, {
3623
+ global: installGlobally,
3624
+ mode: "copy",
3625
+ eveSubagent: target.subagent
3626
+ });
3627
+ results.push({
3628
+ skill: getSkillDisplayName(skill),
3629
+ agent: targetDisplayName(target),
3630
+ ...result
3631
+ });
3632
+ }
3633
+ spinner.stop("Installation complete");
3634
+ console.log();
3635
+ const successful = results.filter((r) => r.success);
3636
+ const failed = results.filter((r) => !r.success);
3637
+ if (successful.length > 0) {
3638
+ const bySkill = /* @__PURE__ */ new Map();
3639
+ for (const r of successful) {
3640
+ if (!bySkill.has(r.skill)) bySkill.set(r.skill, []);
3641
+ bySkill.get(r.skill).push(r);
3642
+ }
3643
+ const resultLines = [];
3644
+ for (const [skillName, skillResults] of bySkill) {
3645
+ resultLines.push(`${import_picocolors.default.green("✓")} ${skillName} ${import_picocolors.default.dim("(copied)")}`);
3646
+ resultLines.push(...buildCopyResultLines(skillResults, process.cwd()));
3647
+ }
3648
+ Me(resultLines.join("\n"), import_picocolors.default.green(`Installed ${bySkill.size} skill${bySkill.size !== 1 ? "s" : ""}`));
3649
+ }
3650
+ if (failed.length > 0) {
3651
+ console.log();
3652
+ M.error(import_picocolors.default.red(`Failed to install ${failed.length}`));
3653
+ for (const r of failed) M.message(` ${import_picocolors.default.red("✗")} ${r.skill} → ${r.agent}: ${import_picocolors.default.dim(r.error)}`);
3654
+ }
3655
+ console.log();
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 {}
3665
+ await promptForFindSkills(options, targetAgents);
3666
+ }
3434
3667
  async function runAdd(args, options = {}) {
3435
3668
  const source = args[0];
3436
3669
  let installTipShown = false;
@@ -3469,42 +3702,124 @@ async function runAdd(args, options = {}) {
3469
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");
3470
3703
  else if (!process.stdin.isTTY) showInstallTip();
3471
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
+ }
3472
3715
  try {
3473
3716
  const spinner = Y();
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}`));
3721
+ process.exit(1);
3722
+ }
3723
+ if (!isDirectory(expanded)) {
3724
+ Se(import_picocolors.default.red(`Local path is not a directory: ${expanded}`));
3725
+ process.exit(1);
3726
+ }
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, {
3732
+ includeInternal,
3733
+ fullDepth: options.fullDepth
3734
+ });
3735
+ } finally {
3736
+ spinner.stop("Discovery complete");
3737
+ }
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;
3745
+ }
3474
3746
  const skillNameForLookup = options.skill?.[0];
3475
- let skillId;
3476
- try {
3477
- skillId = await resolveSkillId$1(source, skillNameForLookup);
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];
3478
3802
  } catch (e) {
3479
3803
  const message = e instanceof Error ? e.message : String(e);
3480
3804
  Se(import_picocolors.default.red(message));
3481
3805
  process.exit(1);
3482
3806
  }
3483
- spinner.start(`Downloading skill ${import_picocolors.default.cyan(skillId)} from API...`);
3807
+ spinner.start(`Downloading ${skillIds.length > 1 ? `${skillIds.length} skills` : `skill ${import_picocolors.default.cyan(skillIds[0])}`} from API...`);
3484
3808
  try {
3485
- tempDir = await downloadAndExtractSkill(skillId);
3809
+ tempDir = await mkdtemp(join$1(tmpdir(), "skills-"));
3810
+ for (const id of skillIds) await downloadAndExtractSkill(id, tempDir, options.version);
3486
3811
  } catch (e) {
3487
3812
  spinner.stop(import_picocolors.default.red("Download failed"));
3488
3813
  const message = e instanceof Error ? e.message : String(e);
3489
- Se(import_picocolors.default.red(`Failed to download skill "${skillId}": ${message}`));
3814
+ Se(import_picocolors.default.red(`Failed to download skill: ${message}`));
3490
3815
  process.exit(1);
3491
3816
  }
3492
3817
  spinner.stop(`Skill archive downloaded from ${import_picocolors.default.cyan("API")}`);
3493
3818
  const parsed = {
3494
3819
  type: "git",
3495
- url: skillId
3820
+ url: skillIds[0]
3496
3821
  };
3497
3822
  const repoPrivacyPromise = Promise.resolve(null);
3498
- if (source.split("/")[0]?.toLowerCase() === "openclaw" && !options.dangerouslyAcceptOpenclawRisks) {
3499
- console.log();
3500
- M.warn(import_picocolors.default.yellow(import_picocolors.default.bold("⚠ OpenClaw skills are unverified community submissions.")));
3501
- M.message(import_picocolors.default.yellow("This source contains user-submitted skills that have not been reviewed for safety or quality."));
3502
- M.message(import_picocolors.default.yellow("Skills run with full agent permissions and could be malicious."));
3503
- console.log();
3504
- M.message(`If you understand the risks, re-run with:\n\n ${import_picocolors.default.cyan(`npx wittyhub add ${source} --dangerously-accept-openclaw-risks`)}\n`);
3505
- Se(import_picocolors.default.red("Installation blocked"));
3506
- process.exit(1);
3507
- }
3508
3823
  const includeInternal = !!(options.skill && options.skill.length > 0);
3509
3824
  let skills;
3510
3825
  let blobResult = null;
@@ -3553,7 +3868,13 @@ async function runAdd(args, options = {}) {
3553
3868
  process.exit(0);
3554
3869
  }
3555
3870
  let selectedSkills;
3556
- 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("*")) {
3557
3878
  selectedSkills = skills;
3558
3879
  M.info(`Installing all ${skills.length} skills`);
3559
3880
  } else if (options.skill && options.skill.length > 0) {
@@ -3616,9 +3937,10 @@ async function runAdd(args, options = {}) {
3616
3937
  }
3617
3938
  selectedSkills = selected;
3618
3939
  }
3619
- const ownerRepoForAudit = getOwnerRepo(parsed);
3620
- const auditSkillFiles = buildSkillFiles(selectedSkills, tempDir, blobResult);
3621
- const auditPromise = ownerRepoForAudit ? fetchAuditData(ownerRepoForAudit, selectedSkills.map((s) => s.name), parsed.type, auditSkillFiles) : 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
+ }), {}));
3622
3944
  let targetAgents;
3623
3945
  const validAgents = Object.keys(agents);
3624
3946
  if (options.agent?.includes("*")) {
@@ -3826,7 +4148,7 @@ async function runAdd(args, options = {}) {
3826
4148
  let auditData = null;
3827
4149
  try {
3828
4150
  auditData = await auditPromise;
3829
- if (auditData && ownerRepoForAudit) {
4151
+ if (auditData) {
3830
4152
  const securityLines = buildSecurityLines(auditData, auditSkills);
3831
4153
  if (securityLines.length > 0) Me(securityLines.join("\n"), "Security Risk Assessments");
3832
4154
  }
@@ -4127,139 +4449,26 @@ function parseAddOptions(args) {
4127
4449
  }
4128
4450
  i--;
4129
4451
  } else if (arg === "--dangerously-accept-openclaw-risks") options.dangerouslyAcceptOpenclawRisks = true;
4130
- 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);
4131
4456
  }
4132
4457
  return {
4133
4458
  source,
4134
4459
  options
4135
4460
  };
4136
4461
  }
4137
- const SOURCE_TYPE_PREFIXES = [
4138
- "github",
4139
- "gitcode",
4140
- "gitlab",
4141
- "gitee"
4142
- ];
4143
- function normalizeSkillId(input) {
4144
- const trimmed = input.trim();
4145
- const first = trimmed.split("/")[0];
4146
- if (first && SOURCE_TYPE_PREFIXES.includes(first.toLowerCase())) return trimmed;
4147
- return `github/${trimmed}`;
4148
- }
4149
- function buildSkillIdFromSource(source, skillName) {
4150
- const parsed = parseSource(source);
4151
- if (!SOURCE_TYPE_PREFIXES.includes(parsed.type)) return null;
4152
- const ownerRepo = getOwnerRepo(parsed);
4153
- if (!ownerRepo) return null;
4154
- const slugifiedOwnerRepo = ownerRepo.split("/").map(slugifyTelemetryValue).join("/");
4155
- const skillPath = slugifyTelemetryValue(skillName);
4156
- if (!skillPath) return null;
4157
- return `${parsed.type}/${slugifiedOwnerRepo}/${skillPath}`;
4158
- }
4159
- async function listSkillsByRepo(source) {
4160
- const parsed = parseSource(source);
4161
- if (!SOURCE_TYPE_PREFIXES.includes(parsed.type)) return [];
4162
- const ownerRepo = getOwnerRepo(parsed);
4163
- if (!ownerRepo) return [];
4164
- const slugifiedOwnerRepo = ownerRepo.split("/").map(slugifyTelemetryValue).join("/");
4165
- if (!slugifiedOwnerRepo || slugifiedOwnerRepo.split("/").length < 2) return [];
4166
- const url = `${LIST_URL}?${new URLSearchParams({
4167
- source_type: parsed.type,
4168
- repo: slugifiedOwnerRepo,
4169
- limit: "100"
4170
- }).toString()}`;
4171
- try {
4172
- const res = await fetch(url);
4173
- if (!res.ok) return [];
4174
- const raw = await res.json();
4175
- return ((raw.data || raw).skills ?? []).filter((skill) => skill && typeof skill.skill_id === "string" && skill.skill_id).map((skill) => ({
4176
- skill_id: skill.skill_id,
4177
- name: skill.name ?? "",
4178
- source_url: skill.source_url ?? ""
4179
- }));
4180
- } catch {
4181
- return [];
4182
- }
4183
- }
4184
- async function resolveBySearch(source, skillName) {
4185
- let results = [];
4186
- try {
4187
- const params = new URLSearchParams({
4188
- q: skillName,
4189
- limit: "10",
4190
- mode: "text"
4191
- });
4192
- const res = await fetch(`${SEARCH_URL}?${params.toString()}`);
4193
- if (res.ok) {
4194
- const raw = await res.json();
4195
- results = (raw.data || raw).results ?? [];
4196
- }
4197
- } catch {
4198
- return null;
4199
- }
4200
- const sourceLower = source.trim().toLowerCase();
4201
- const matches = results.filter((skill) => {
4202
- const url = (skill.source_url || "").toLowerCase();
4203
- if (!url) return false;
4204
- if (url.startsWith(sourceLower)) return true;
4205
- if (!sourceLower.includes("://") && sourceLower.split("/").length === 2) return url.includes(`/${sourceLower}/`);
4206
- return false;
4207
- });
4208
- const exact = matches.find((skill) => skill.name.toLowerCase() === skillName.trim().toLowerCase());
4209
- return exact ? exact.skill_id : matches.length > 0 ? matches[0].skill_id : null;
4210
- }
4211
- async function resolveSkillIdBySourceAndName(source, skillName) {
4212
- const repoSkills = await listSkillsByRepo(source);
4213
- if (repoSkills.length > 0) {
4214
- const nameLower = skillName.trim().toLowerCase();
4215
- const exact = repoSkills.find((skill) => skill.name.toLowerCase() === nameLower);
4216
- if (exact) return exact.skill_id;
4217
- const slug = slugifyTelemetryValue(skillName);
4218
- if (slug) {
4219
- const bySlug = repoSkills.find((skill) => {
4220
- return (skill.skill_id.split("/").pop() ?? "").toLowerCase() === slug;
4221
- });
4222
- if (bySlug) return bySlug.skill_id;
4223
- }
4224
- return null;
4225
- }
4226
- return resolveBySearch(source, skillName);
4227
- }
4228
- async function resolveSkillId(source, skillName) {
4229
- if (!skillName) return {
4230
- skillId: normalizeSkillId(source),
4231
- derived: false
4232
- };
4233
- const resolved = await resolveSkillIdBySourceAndName(source, skillName);
4234
- if (resolved) return {
4235
- skillId: resolved,
4236
- derived: false
4237
- };
4238
- const derived = buildSkillIdFromSource(source, skillName);
4239
- if (derived) return {
4240
- skillId: derived,
4241
- derived: true
4242
- };
4243
- return {
4244
- skillId: null,
4245
- derived: false
4246
- };
4247
- }
4248
4462
  function parseAuditOptions(args) {
4249
- let source = "";
4250
- let skill = "";
4251
4463
  const errors = [];
4252
- for (let i = 0; i < args.length; i++) {
4253
- const arg = args[i];
4254
- if (arg === "-s" || arg === "--skill") {
4255
- i++;
4256
- skill = args[i]?.trim() ?? "";
4257
- } else if (arg && !arg.startsWith("-")) source = arg.trim();
4464
+ let skillId = "";
4465
+ for (const arg of args) if (arg && !arg.startsWith("-")) {
4466
+ skillId = arg.trim();
4467
+ break;
4258
4468
  }
4259
- if (!source) errors.push("Missing source or skill id");
4469
+ if (!skillId) errors.push("Missing skill id");
4260
4470
  return {
4261
- source,
4262
- skill,
4471
+ skillId: normalizeSkillId(skillId),
4263
4472
  errors
4264
4473
  };
4265
4474
  }
@@ -4267,7 +4476,7 @@ async function fetchSkillAudit(skillId, timeoutMs = 15e3) {
4267
4476
  const controller = new AbortController();
4268
4477
  const timeout = setTimeout(() => controller.abort(), timeoutMs);
4269
4478
  try {
4270
- const url = AUDIT_URL.replace("{skill_id}", skillId.split("/").map(encodeURIComponent).join("/"));
4479
+ const url = AUDIT_URL.replace("{skill_id}", skillId);
4271
4480
  let response;
4272
4481
  try {
4273
4482
  response = await fetch(url, { signal: controller.signal });
@@ -4361,20 +4570,12 @@ function buildAuditOutput(skillId, data) {
4361
4570
  return lines;
4362
4571
  }
4363
4572
  async function runAudit(args) {
4364
- const { source, skill, errors } = parseAuditOptions(args);
4573
+ const { skillId, errors } = parseAuditOptions(args);
4365
4574
  if (errors.length > 0) {
4366
4575
  for (const error of errors) console.error(import_picocolors.default.red(error));
4367
- console.error("Usage: wittyhub audit <source> --skill <skill>");
4368
- console.error(" wittyhub audit <skill_id>");
4576
+ console.error("Usage: wittyhub audit <skill_id>");
4369
4577
  return;
4370
4578
  }
4371
- const { skillId, derived } = await resolveSkillId(source, skill || void 0);
4372
- if (!skillId) {
4373
- console.error(import_picocolors.default.red(`Unable to resolve skill id from source: ${source}`));
4374
- console.error("Usage: wittyhub audit <source> --skill <skill>");
4375
- return;
4376
- }
4377
- if (derived) console.error(import_picocolors.default.yellow(`Could not locate skill in repo; trying derived id: ${skillId}`));
4378
4579
  const result = await fetchSkillAudit(skillId);
4379
4580
  track({
4380
4581
  event: "audit",
@@ -4406,18 +4607,21 @@ const YELLOW$1 = "\x1B[33m";
4406
4607
  const RED = "\x1B[31m";
4407
4608
  const GREEN = "\x1B[32m";
4408
4609
  function formatInstalls$1(count) {
4409
- if (count === void 0 || count === null || Number.isNaN(count)) return "";
4410
- if (count <= 0) return "0 installs";
4411
- if (count >= 1e6) return `${(count / 1e6).toFixed(1).replace(/\.0$/, "")}M installs`;
4412
- if (count >= 1e3) return `${(count / 1e3).toFixed(1).replace(/\.0$/, "")}K installs`;
4413
- return `${count} install${count === 1 ? "" : "s"}`;
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);
4414
4614
  }
4415
4615
  function formatRiskLevel$1(riskScore) {
4416
4616
  if (riskScore === void 0 || riskScore === null) return "";
4417
- if (riskScore <= 20) return `${GREEN}safe risk${RESET$3}`;
4418
- if (riskScore <= 50) return `${GREEN}low risk${RESET$3}`;
4419
- if (riskScore <= 80) return `${YELLOW$1}medium risk${RESET$3}`;
4420
- return `${RED}high risk${RESET$3}`;
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}`;
4421
4625
  }
4422
4626
  function escapeRegExp(s) {
4423
4627
  return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
@@ -4545,27 +4749,22 @@ async function runSearchPrompt(initialQuery = "", owner) {
4545
4749
  else if (results.length === 0 && searchError) lines.push(`${RED}${searchError}${RESET$3}`);
4546
4750
  else if (results.length === 0) lines.push(`${DIM$3}No skills found${RESET$3}`);
4547
4751
  else {
4548
- const visible = results.slice(0, 8);
4752
+ const visible = results.slice(0, 6);
4549
4753
  for (let i = 0; i < visible.length; i++) {
4550
4754
  const skill = visible[i];
4551
4755
  const isSelected = i === selectedIndex;
4552
4756
  const arrow = isSelected ? `${BOLD$3}>${RESET$3}` : " ";
4553
4757
  const hlName = highlightMatches(skill.name, query);
4554
4758
  const name = isSelected ? `${BOLD$3}${hlName}${RESET$3}` : `${TEXT$2}${hlName}${RESET$3}`;
4555
- const source = skill.source ? ` ${DIM$3}${skill.source}${RESET$3}` : "";
4556
- const installs = formatInstalls$1(skill.installs);
4557
- const installsBadge = installs ? ` ${CYAN$1}${installs}${RESET$3}` : "";
4558
4759
  const risk = formatRiskLevel$1(skill.riskScore);
4559
- const riskBadge = risk ? ` ${risk}` : "";
4560
4760
  const loadingIndicator = loading && i === 0 ? ` ${DIM$3}...${RESET$3}` : "";
4561
- lines.push(` ${arrow} ${name}${source}${loadingIndicator}`);
4562
- if (installsBadge || riskBadge) {
4563
- const meta = [installsBadge.trimStart(), riskBadge.trimStart()].filter(Boolean).join(" ");
4564
- lines.push(` ${meta}`);
4565
- }
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}`);
4566
4765
  if (skill.description) {
4567
4766
  const descPlain = skill.description.length > 80 ? `${skill.description.slice(0, 77)}...` : skill.description;
4568
- lines.push(` ${DIM$3}${highlightMatches(descPlain, query)}${RESET$3}`);
4767
+ lines.push(` ${kvKey("description:")} ${DIM$3}${highlightMatches(descPlain, query)}${RESET$3}`);
4569
4768
  }
4570
4769
  }
4571
4770
  }
@@ -4655,44 +4854,16 @@ async function runSearchPrompt(initialQuery = "", owner) {
4655
4854
  process.stdin.on("keypress", handleKeypress);
4656
4855
  });
4657
4856
  }
4658
- function getOwnerRepoFromString(pkg) {
4659
- const atIndex = pkg.lastIndexOf("@");
4660
- const repoPath = atIndex > 0 ? pkg.slice(0, atIndex) : pkg;
4661
- const urlMatch = repoPath.match(/^https?:\/\/[^/]+\/([^/]+)\/([^/]+?)(?:\.git)?\/?$/);
4662
- if (urlMatch) return {
4663
- owner: urlMatch[1],
4664
- repo: urlMatch[2]
4665
- };
4666
- const blobMatch = repoPath.match(/^https?:\/\/[^/]+\/([^/]+)\/([^/]+?)\/blob\//);
4667
- if (blobMatch) return {
4668
- owner: blobMatch[1],
4669
- repo: blobMatch[2]
4670
- };
4671
- const shorthandMatch = repoPath.match(/^([^/]+)\/([^/]+)$/);
4672
- if (shorthandMatch) return {
4673
- owner: shorthandMatch[1],
4674
- repo: shorthandMatch[2]
4675
- };
4676
- const slugMatch = repoPath.match(/^[^/]+\/([^/]+)\/([^/]+)/);
4677
- if (slugMatch) return {
4678
- owner: slugMatch[1],
4679
- repo: slugMatch[2]
4680
- };
4681
- return null;
4682
- }
4683
- async function isRepoPublic(owner, repo) {
4684
- return await isRepoPrivate(owner, repo) === false;
4685
- }
4686
4857
  async function runFind(args) {
4687
4858
  const { query, options: findOptions, errors } = parseFindOptions(args);
4688
4859
  const owner = findOptions.owner;
4689
4860
  const isNonInteractive = !process.stdin.isTTY;
4690
4861
  const agentTip = `${DIM$3}Tip: if running in a coding agent, follow these steps:${RESET$3}
4691
4862
  ${DIM$3} 1) npx wittyhub find [query] [--owner <owner>]${RESET$3}
4692
- ${DIM$3} 2) npx wittyhub add <owner/repo@skill>${RESET$3}`;
4863
+ ${DIM$3} 2) npx wittyhub add <skill_id>${RESET$3}`;
4693
4864
  if (errors.length > 0) {
4694
4865
  for (const error of errors) console.error(error);
4695
- console.error("Usage: npx skills find <query> [--owner <owner>]");
4866
+ console.error("Usage: npx wittyhub find <query> [--owner <owner>]");
4696
4867
  return;
4697
4868
  }
4698
4869
  if (query) {
@@ -4711,26 +4882,19 @@ ${DIM$3} 2) npx wittyhub add <owner/repo@skill>${RESET$3}`;
4711
4882
  console.log(`${DIM$3}No skills found for "${query}"${ownerSuffix}${RESET$3}`);
4712
4883
  return;
4713
4884
  }
4714
- console.log(`${DIM$3}Install with${RESET$3} npx wittyhub add <repo-url> --skill <skill>`);
4885
+ console.log(`${DIM$3}Install with${RESET$3} npx wittyhub add ${TEXT$2}<skill_id>${RESET$3}`);
4715
4886
  console.log();
4716
4887
  for (const skill of results.slice(0, 6)) {
4717
- const raw = skill.sourceUrl || skill.source || skill.slug;
4718
4888
  const hlName = highlightMatches(skill.name, query);
4719
- const installTarget = skill.sourceUrl ? `${skill.sourceUrl} --skill ${hlName}` : `${raw}@${hlName}`;
4720
- const installs = formatInstalls$1(skill.installs);
4721
- const installsBadge = installs ? ` ${CYAN$1}${installs}${RESET$3}` : "";
4722
4889
  const risk = formatRiskLevel$1(skill.riskScore);
4723
- const riskBadge = risk ? ` ${risk}` : "";
4724
- console.log(`${TEXT$2}${installTarget}${RESET$3}`);
4725
- if (installsBadge || riskBadge) {
4726
- const meta = [installsBadge.trimStart(), riskBadge.trimStart()].filter(Boolean).join(" ");
4727
- console.log(` ${meta}`);
4728
- }
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}`);
4729
4894
  if (skill.description) {
4730
4895
  const descPlain = skill.description.length > 100 ? `${skill.description.slice(0, 97)}...` : skill.description;
4731
- console.log(`${DIM$3} ${highlightMatches(descPlain, query)}${RESET$3}`);
4896
+ console.log(` ${kvKey("description:")} ${DIM$3}${highlightMatches(descPlain, query)}${RESET$3}`);
4732
4897
  }
4733
- console.log(`${DIM$3}└ ${skill.sourceUrl || `https://skills.sh/${skill.slug}`}${RESET$3}`);
4734
4898
  console.log();
4735
4899
  }
4736
4900
  return;
@@ -4753,38 +4917,27 @@ ${DIM$3} 2) npx wittyhub add <owner/repo@skill>${RESET$3}`;
4753
4917
  console.log();
4754
4918
  return;
4755
4919
  }
4756
- const pkg = selected.sourceUrl || selected.source || selected.slug;
4920
+ const skillId = selected.slug;
4757
4921
  const skillName = selected.name;
4758
4922
  console.log();
4759
- 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}...`);
4760
4924
  console.log();
4761
- const { source, options: addOptions } = parseAddOptions([
4762
- pkg,
4763
- "--skill",
4764
- skillName
4765
- ]);
4925
+ const { source, options: addOptions } = parseAddOptions([skillId]);
4766
4926
  await runAdd(source, addOptions);
4767
4927
  console.log();
4768
- const info = getOwnerRepoFromString(pkg);
4769
- 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}`);
4770
- 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}`);
4771
4929
  console.log();
4772
4930
  }
4773
4931
  function parseGetOptions(args) {
4774
- let source = "";
4775
- let skill = "";
4776
4932
  const errors = [];
4777
- for (let i = 0; i < args.length; i++) {
4778
- const arg = args[i];
4779
- if (arg === "-s" || arg === "--skill") {
4780
- i++;
4781
- skill = args[i]?.trim() ?? "";
4782
- } else if (arg && !arg.startsWith("-")) source = arg.trim();
4933
+ let skillId = "";
4934
+ for (const arg of args) if (arg && !arg.startsWith("-")) {
4935
+ skillId = arg.trim();
4936
+ break;
4783
4937
  }
4784
- if (!source) errors.push("Missing source or skill id");
4938
+ if (!skillId) errors.push("Missing skill id");
4785
4939
  return {
4786
- source,
4787
- skill,
4940
+ skillId: normalizeSkillId(skillId),
4788
4941
  errors
4789
4942
  };
4790
4943
  }
@@ -4792,7 +4945,7 @@ async function fetchSkillDetail(skillId, timeoutMs = 15e3) {
4792
4945
  const controller = new AbortController();
4793
4946
  const timeout = setTimeout(() => controller.abort(), timeoutMs);
4794
4947
  try {
4795
- const url = GET_URL.replace("{skill_id}", skillId.split("/").map(encodeURIComponent).join("/"));
4948
+ const url = GET_URL.replace("{skill_id}", skillId);
4796
4949
  let response;
4797
4950
  try {
4798
4951
  response = await fetch(url, { signal: controller.signal });
@@ -4885,20 +5038,12 @@ function buildGetOutput(data) {
4885
5038
  return lines;
4886
5039
  }
4887
5040
  async function runGet(args) {
4888
- const { source, skill, errors } = parseGetOptions(args);
5041
+ const { skillId, errors } = parseGetOptions(args);
4889
5042
  if (errors.length > 0) {
4890
5043
  for (const error of errors) console.error(import_picocolors.default.red(error));
4891
- console.error("Usage: wittyhub get <source> --skill <skill>");
4892
- console.error(" wittyhub get <skill_id>");
5044
+ console.error("Usage: wittyhub get <skill_id>");
4893
5045
  return;
4894
5046
  }
4895
- const { skillId, derived } = await resolveSkillId(source, skill || void 0);
4896
- if (!skillId) {
4897
- console.error(import_picocolors.default.red(`Unable to resolve skill id from source: ${source}`));
4898
- console.error("Usage: wittyhub get <source> --skill <skill>");
4899
- return;
4900
- }
4901
- if (derived) console.error(import_picocolors.default.yellow(`Could not locate skill in repo; trying derived id: ${skillId}`));
4902
5047
  const result = await fetchSkillDetail(skillId);
4903
5048
  track({
4904
5049
  event: "get",
@@ -5405,6 +5550,26 @@ async function runList(args) {
5405
5550
  console.log();
5406
5551
  }
5407
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
+ }
5408
5573
  async function removeCommand(skillNames, options) {
5409
5574
  const agentResult = await detectAgent();
5410
5575
  if (agentResult.isAgent) {
@@ -5450,7 +5615,10 @@ async function removeCommand(skillNames, options) {
5450
5615
  let selectedSkills = [];
5451
5616
  if (options.all) selectedSkills = installedSkills;
5452
5617
  else if (skillNames.length > 0) {
5453
- 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
+ }));
5454
5622
  if (selectedSkills.length === 0) {
5455
5623
  M.error(`No matching skills found for: ${skillNames.join(", ")}`);
5456
5624
  return;
@@ -6822,7 +6990,7 @@ function showBanner() {
6822
6990
  console.log(` ${DIM}$${RESET} ${TEXT}npx wittyhub validate ${DIM}<path>${RESET} ${DIM}Validate a local skill${RESET}`);
6823
6991
  console.log(` ${DIM}$${RESET} ${TEXT}npx wittyhub experimental_sync${RESET} ${DIM}Sync skills from node_modules${RESET}`);
6824
6992
  console.log();
6825
- 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`);
6826
6994
  console.log();
6827
6995
  console.log(`Discover more skills at ${TEXT}https://skillhub.openeuler.org/${RESET}`);
6828
6996
  console.log();
@@ -6840,12 +7008,8 @@ ${BOLD}Manage Skills:${RESET}
6840
7008
  remove [skills] Remove installed skills
6841
7009
  list, ls List installed skills
6842
7010
  find [query] Search for skills interactively
6843
- audit <source> --skill <skill>
6844
- Show security audit result (risk level + risk signals)
6845
- e.g. audit https://github.com/huggingface/transformers --skill add-or-fix-type-checking
6846
- get <source> --skill <skill>
6847
- View skill details (author/category/version/description/tags)
6848
- e.g. get https://github.com/huggingface/transformers --skill add-or-fix-type-checking
7011
+ audit <skill_id> Show security audit result (risk level + risk signals)
7012
+ get <skill_id> View skill details (author/category/version/description/tags)
6849
7013
  validate <path> Validate a local skill for spec compliance (SKILL.md filename,
6850
7014
  frontmatter, required fields name/description, etc.)
6851
7015
 
@@ -6875,6 +7039,7 @@ ${BOLD}Add Options:${RESET}
6875
7039
  --subagent <names> Install to Eve subagents (use 'root' for the root agent)
6876
7040
  --all Shorthand for --skill '*' --agent '*' -y
6877
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
6878
7043
 
6879
7044
  ${BOLD}Use Options:${RESET}
6880
7045
  -s, --skill <skill> Specify the skill to use