wittyhub 0.0.4 → 0.0.6

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 +165 -98
  2. package/dist/cli.mjs +673 -403
  3. package/package.json +4 -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;
@@ -988,6 +1106,19 @@ const hermesHome = process.env.HERMES_HOME?.trim() || join(home, ".hermes");
988
1106
  const autohandHome = process.env.AUTOHAND_HOME?.trim() || join(home, ".autohand");
989
1107
  const zedAppDataHome = process.env.APPDATA?.trim();
990
1108
  const zedFlatpakConfigHome = process.env.FLATPAK_XDG_CONFIG_HOME?.trim();
1109
+ function getDoubaoDataRoots() {
1110
+ if (process.platform === "darwin") return [join(home, "Library", "Application Support", "DoubaoWork"), join(home, "Library", "Application Support", "Doubao")];
1111
+ if (process.platform === "win32") {
1112
+ const localAppData = process.env.LOCALAPPDATA?.trim() || join(home, "AppData", "Local");
1113
+ return [join(localAppData, "DoubaoWork", "User Data"), join(localAppData, "Doubao", "User Data")];
1114
+ }
1115
+ return [join(configHome, "DoubaoWork"), join(configHome, "Doubao")];
1116
+ }
1117
+ const doubaoRoots = getDoubaoDataRoots();
1118
+ const doubaoHome = process.env.DOUBAO_HOME?.trim() || doubaoRoots.find((root) => existsSync(root)) || doubaoRoots[0];
1119
+ function getDoubaoWorkspaceDir() {
1120
+ return join(doubaoHome, "Default", ".doubao", "agent_mode", "workspace");
1121
+ }
991
1122
  function packageJsonHasDependency(packageJsonPath, dependencyName) {
992
1123
  try {
993
1124
  const packageJson = JSON.parse(readFileSync(packageJsonPath, "utf-8"));
@@ -1220,6 +1351,16 @@ const agents = {
1220
1351
  return existsSync(join(home, ".dexto"));
1221
1352
  }
1222
1353
  },
1354
+ doubao: {
1355
+ name: "doubao",
1356
+ displayName: "Doubao",
1357
+ skillsDir: ".doubao/agent_mode/workspace/.user_skills",
1358
+ globalSkillsDir: join(getDoubaoWorkspaceDir(), ".user_skills"),
1359
+ requiresGlobal: true,
1360
+ detectInstalled: async () => {
1361
+ return existsSync(doubaoHome);
1362
+ }
1363
+ },
1223
1364
  droid: {
1224
1365
  name: "droid",
1225
1366
  displayName: "Droid",
@@ -1475,6 +1616,16 @@ const agents = {
1475
1616
  return existsSync(join(home, ".qoder-cn"));
1476
1617
  }
1477
1618
  },
1619
+ qoderwork: {
1620
+ name: "qoderwork",
1621
+ displayName: "QoderWork",
1622
+ skillsDir: ".qoderwork/skills",
1623
+ globalSkillsDir: join(home, ".qoderwork/skills"),
1624
+ requiresGlobal: true,
1625
+ detectInstalled: async () => {
1626
+ return existsSync(join(home, ".qoderwork"));
1627
+ }
1628
+ },
1478
1629
  "qwen-code": {
1479
1630
  name: "qwen-code",
1480
1631
  displayName: "Qwen Code",
@@ -1575,6 +1726,15 @@ const agents = {
1575
1726
  return existsSync(join(home, ".warp"));
1576
1727
  }
1577
1728
  },
1729
+ workbuddy: {
1730
+ name: "workbuddy",
1731
+ displayName: "WorkBuddy",
1732
+ skillsDir: ".workbuddy/skills",
1733
+ globalSkillsDir: join(home, ".workbuddy/skills"),
1734
+ detectInstalled: async () => {
1735
+ return existsSync(join(home, ".workbuddy"));
1736
+ }
1737
+ },
1578
1738
  windsurf: {
1579
1739
  name: "windsurf",
1580
1740
  displayName: "Windsurf",
@@ -2207,99 +2367,6 @@ async function listInstalledSkills(options = {}) {
2207
2367
  } catch {}
2208
2368
  return Array.from(skillsMap.values());
2209
2369
  }
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
2370
  var import_dist = require_dist();
2304
2371
  let cachedResult = null;
2305
2372
  const agentNameToType = {
@@ -2313,12 +2380,56 @@ const agentNameToType = {
2313
2380
  codex: "codex",
2314
2381
  antigravity: "antigravity",
2315
2382
  "augment-cli": "augment",
2383
+ doubao: "doubao",
2316
2384
  opencode: "opencode",
2317
- "github-copilot": "github-copilot"
2385
+ "github-copilot": "github-copilot",
2386
+ openclaw: "openclaw",
2387
+ qoderwork: "qoderwork",
2388
+ trae: "trae",
2389
+ "trae-cn": "trae-cn",
2390
+ workbuddy: "workbuddy"
2318
2391
  };
2392
+ const envPrefixAgents = [
2393
+ {
2394
+ prefixes: ["DOUBAO_"],
2395
+ agent: "doubao"
2396
+ },
2397
+ {
2398
+ prefixes: ["WORKBUDDY_"],
2399
+ agent: "workbuddy"
2400
+ },
2401
+ {
2402
+ prefixes: ["QODERWORK_"],
2403
+ agent: "qoderwork"
2404
+ },
2405
+ {
2406
+ prefixes: [
2407
+ "OPENCLAW_",
2408
+ "CLAWDBOT_",
2409
+ "MOLTBOT_"
2410
+ ],
2411
+ agent: "openclaw"
2412
+ }
2413
+ ];
2414
+ function detectFromEnvPrefixes() {
2415
+ const keys = Object.keys(process.env);
2416
+ for (const { prefixes, agent } of envPrefixAgents) if (keys.some((key) => prefixes.some((prefix) => key.startsWith(prefix)))) return {
2417
+ isAgent: true,
2418
+ agent: { name: agent }
2419
+ };
2420
+ return null;
2421
+ }
2422
+ function detectTrae() {
2423
+ const brand = process.env.TRAE_BRAND_NAME ?? "";
2424
+ if (!(Boolean(process.env.TRAE_RUNTIME) || brand.toLowerCase().includes("trae") || process.env.TERM_PRODUCT === "Trae")) return null;
2425
+ return {
2426
+ isAgent: true,
2427
+ agent: { name: /trae\s*cn|cn\s*trae/i.test(brand) || process.env.TRAE_PRODUCT_REGION?.toLowerCase() === "cn" ? "trae-cn" : "trae" }
2428
+ };
2429
+ }
2319
2430
  async function detectAgent() {
2320
2431
  if (cachedResult) return cachedResult;
2321
- cachedResult = await (0, import_dist.determineAgent)();
2432
+ cachedResult = detectTrae() ?? detectFromEnvPrefixes() ?? await (0, import_dist.determineAgent)();
2322
2433
  if (cachedResult.isAgent) setDetectedAgent(cachedResult.agent.name);
2323
2434
  return cachedResult;
2324
2435
  }
@@ -2326,7 +2437,9 @@ async function isRunningInAgent() {
2326
2437
  return (await detectAgent()).isAgent;
2327
2438
  }
2328
2439
  function getAgentType(agentName) {
2329
- return agentNameToType[agentName] ?? null;
2440
+ const mapped = agentNameToType[agentName];
2441
+ if (mapped) return mapped;
2442
+ return Object.hasOwn(agents, agentName) ? agentName : null;
2330
2443
  }
2331
2444
  var ProviderRegistryImpl = class {
2332
2445
  providers = [];
@@ -3153,7 +3266,7 @@ async function tryBlobInstall(ownerRepo, options = {}) {
3153
3266
  tree
3154
3267
  };
3155
3268
  }
3156
- var version$1 = "0.0.4";
3269
+ var version$1 = "0.0.6";
3157
3270
  const isCancelled$1 = (value) => typeof value === "symbol";
3158
3271
  function getLockSource(parsedUrl, normalizedSource) {
3159
3272
  return parsedUrl.startsWith("git@") || parsedUrl.startsWith("ssh://") ? parsedUrl : normalizedSource;
@@ -3170,14 +3283,37 @@ function normalizeRepoRef(input) {
3170
3283
  s = s.replace(/\/+$/, "");
3171
3284
  return s;
3172
3285
  }
3286
+ function looksLikeLocalPath(source) {
3287
+ const s = source.trim();
3288
+ if (!s) return false;
3289
+ if (isAbsolute$1(s)) return true;
3290
+ if (s.startsWith("./") || s.startsWith("../") || s === "." || s === "..") return true;
3291
+ if (s.startsWith("~/")) return true;
3292
+ if (!s.includes(":") && !s.includes("/") && existsSync$1(resolve$1(process.cwd(), s))) return statSync(resolve$1(process.cwd(), s)).isDirectory();
3293
+ return false;
3294
+ }
3295
+ function expandLocalPath(source) {
3296
+ const s = source.trim();
3297
+ if (s.startsWith("~/")) return join$1(homedir(), s.slice(2));
3298
+ return resolve$1(process.cwd(), s);
3299
+ }
3300
+ function isDirectory(path) {
3301
+ return statSync(path).isDirectory();
3302
+ }
3173
3303
  function looksLikeRepoSource(source) {
3174
3304
  const s = source.trim();
3175
3305
  if (s.startsWith("http://") || s.startsWith("https://")) return true;
3176
3306
  if (s.startsWith("git@") || s.startsWith("ssh://") || s.startsWith("git://")) return true;
3177
3307
  return (s.match(/\//g) || []).length <= 1 && !s.includes(":") && !/\s/.test(s);
3178
3308
  }
3179
- async function resolveSkillId$1(source, skillName) {
3180
- if (!looksLikeRepoSource(source)) return source;
3309
+ async function resolveSkillId(source, skillName) {
3310
+ if (isRepoLevelRef(source)) {
3311
+ const [sourceType, ownerRepo] = source.split(":", 2);
3312
+ const skills = await listSkillsByRepo(sourceType, ownerRepo);
3313
+ if (skills.length === 0) throw new Error(`仓库 "${source}" 下未找到已收录的技能。`);
3314
+ return skills.map((s) => s.skill_id);
3315
+ }
3316
+ if (!looksLikeRepoSource(source)) return normalizeSkillId(source);
3181
3317
  if (!skillName || !skillName.trim()) throw new Error(`源 "${source}" 看起来是仓库地址,需要同时传入 --skill <skill_name> 才能定位具体技能。`);
3182
3318
  const searchUrl = `${SEARCH_URL}?${new URLSearchParams({
3183
3319
  q: skillName.trim(),
@@ -3203,11 +3339,15 @@ async function resolveSkillId$1(source, skillName) {
3203
3339
  if (!match) throw new Error(`未找到名称匹配 "${skillName}" 的技能。`);
3204
3340
  return match.skill_id;
3205
3341
  }
3206
- async function downloadAndExtractSkill(skillId) {
3207
- const downloadUrl = DOWNLOAD_URL.replace("{skill_id}", encodeURIComponent(skillId));
3342
+ async function downloadAndExtractSkill(skillId, targetDir, version) {
3343
+ let downloadUrl = DOWNLOAD_URL.replace("{skill_id}", skillId);
3344
+ if (version) downloadUrl += `?version=${encodeURIComponent(version)}`;
3208
3345
  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-"));
3346
+ if (!response.ok) {
3347
+ const detail = await response.json().then((data) => typeof data?.detail === "string" && data.detail ? `: ${data.detail}` : "").catch(() => "");
3348
+ throw new Error(`HTTP ${response.status} ${response.statusText}${detail}`);
3349
+ }
3350
+ const tempDir = targetDir ?? await mkdtemp(join$1(tmpdir(), "skills-"));
3211
3351
  const zipPath = join$1(tempDir, ".skill.zip");
3212
3352
  try {
3213
3353
  await writeFile(zipPath, Buffer.from(await response.arrayBuffer()));
@@ -3365,12 +3505,28 @@ function buildCopyResultLines(results, cwd) {
3365
3505
  }
3366
3506
  return lines;
3367
3507
  }
3508
+ function withQuitOnQ(promise) {
3509
+ const handler = (_str, key) => {
3510
+ if (key?.name !== "q" || key.ctrl || key.meta) return;
3511
+ process.stdin.removeListener("keypress", handler);
3512
+ if (process.stdin.isTTY) try {
3513
+ process.stdin.setRawMode(false);
3514
+ } catch {}
3515
+ process.stdout.write("\n");
3516
+ xe("Cancelled");
3517
+ process.exit(0);
3518
+ };
3519
+ process.stdin.on("keypress", handler);
3520
+ return promise.finally(() => {
3521
+ process.stdin.removeListener("keypress", handler);
3522
+ });
3523
+ }
3368
3524
  function multiselect(opts) {
3369
- return fe({
3525
+ return withQuitOnQ(fe({
3370
3526
  ...opts,
3371
3527
  options: opts.options,
3372
- message: `${opts.message} ${import_picocolors.default.dim("(space to toggle)")}`
3373
- });
3528
+ message: `${opts.message} ${import_picocolors.default.dim("(space to toggle, q to quit)")}`
3529
+ }));
3374
3530
  }
3375
3531
  async function promptForAgents(message, choices) {
3376
3532
  let lastSelected;
@@ -3431,6 +3587,171 @@ async function selectAgentsInteractive(options) {
3431
3587
  return selected;
3432
3588
  }
3433
3589
  setVersion(version$1);
3590
+ async function installLocalSkills(source, localDir, skills, options, agentResult) {
3591
+ const spinner = Y();
3592
+ if (options.list) {
3593
+ console.log();
3594
+ M.step(import_picocolors.default.bold("Available Skills"));
3595
+ for (const skill of skills) {
3596
+ M.message(` ${import_picocolors.default.cyan(getSkillDisplayName(skill))}`);
3597
+ M.message(` ${import_picocolors.default.dim(skill.description)}`);
3598
+ }
3599
+ console.log();
3600
+ Se("Use --skill <name> to install specific skills");
3601
+ return;
3602
+ }
3603
+ let selectedSkills;
3604
+ if (options.skill?.includes("*")) {
3605
+ selectedSkills = skills;
3606
+ M.info(`Installing all ${skills.length} skills`);
3607
+ } else if (options.skill && options.skill.length > 0) {
3608
+ selectedSkills = filterSkills(skills, options.skill);
3609
+ if (selectedSkills.length === 0) {
3610
+ M.error(`No matching skills found for: ${options.skill.join(", ")}`);
3611
+ M.info("Available skills:");
3612
+ for (const s of skills) M.message(` - ${getSkillDisplayName(s)}`);
3613
+ process.exit(1);
3614
+ }
3615
+ M.info(`Selected ${selectedSkills.length} skill${selectedSkills.length !== 1 ? "s" : ""}: ${selectedSkills.map((s) => import_picocolors.default.cyan(getSkillDisplayName(s))).join(", ")}`);
3616
+ } else if (skills.length === 1 || options.yes) {
3617
+ selectedSkills = skills;
3618
+ if (skills.length === 1) {
3619
+ M.info(`Skill: ${import_picocolors.default.cyan(getSkillDisplayName(skills[0]))}`);
3620
+ M.message(import_picocolors.default.dim(skills[0].description));
3621
+ } else M.info(`Installing all ${skills.length} skills`);
3622
+ } else {
3623
+ const selected = await multiselect({
3624
+ message: "Select skills to install",
3625
+ options: [...skills].sort((a, b) => getSkillDisplayName(a).localeCompare(getSkillDisplayName(b))).map((s) => ({
3626
+ value: s,
3627
+ label: getSkillDisplayName(s),
3628
+ hint: s.description.length > 60 ? s.description.slice(0, 57) + "..." : s.description
3629
+ })),
3630
+ required: true
3631
+ });
3632
+ if (pD(selected)) {
3633
+ xe("Installation cancelled");
3634
+ process.exit(0);
3635
+ }
3636
+ selectedSkills = selected;
3637
+ }
3638
+ let targetAgents;
3639
+ const validAgents = Object.keys(agents);
3640
+ if (options.agent?.includes("*")) {
3641
+ targetAgents = validAgents;
3642
+ M.info(`Installing to all ${targetAgents.length} agents`);
3643
+ } else if (options.agent && options.agent.length > 0) {
3644
+ const invalidAgents = options.agent.filter((a) => !validAgents.includes(a));
3645
+ if (invalidAgents.length > 0) {
3646
+ M.error(`Invalid agents: ${invalidAgents.join(", ")}`);
3647
+ M.info(`Valid agents: ${validAgents.join(", ")}`);
3648
+ process.exit(1);
3649
+ }
3650
+ targetAgents = options.agent;
3651
+ } else {
3652
+ spinner.start("Loading agents...");
3653
+ const installedAgents = await detectInstalledAgents();
3654
+ const totalAgents = Object.keys(agents).length;
3655
+ spinner.stop(`${totalAgents} agents`);
3656
+ if (installedAgents.length === 0) if (options.yes) {
3657
+ targetAgents = validAgents;
3658
+ M.info("Installing to all agents");
3659
+ } else {
3660
+ M.info("Select agents to install skills to");
3661
+ const selected = await promptForAgents("Which agents do you want to install to?", Object.entries(agents).filter(([key]) => key !== "eve").map(([key, config]) => ({
3662
+ value: key,
3663
+ label: config.displayName
3664
+ })));
3665
+ if (pD(selected)) {
3666
+ xe("Installation cancelled");
3667
+ process.exit(0);
3668
+ }
3669
+ targetAgents = selected;
3670
+ }
3671
+ else {
3672
+ targetAgents = ensureUniversalAgents(installedAgents);
3673
+ M.info(`Installing to: ${installedAgents.map((a) => import_picocolors.default.cyan(agents[a].displayName)).join(", ")}`);
3674
+ }
3675
+ }
3676
+ const installTargets = buildInstallTargets(targetAgents, [void 0]);
3677
+ let installGlobally = options.global ?? false;
3678
+ if (options.global === void 0 && !options.yes) {
3679
+ const scope = await ve({
3680
+ message: "Installation scope",
3681
+ options: [{
3682
+ value: false,
3683
+ label: "Project",
3684
+ hint: "Install in current directory (committed with your project)"
3685
+ }, {
3686
+ value: true,
3687
+ label: "Global",
3688
+ hint: "Install in home directory (available across all projects)"
3689
+ }]
3690
+ });
3691
+ if (pD(scope)) {
3692
+ xe("Installation cancelled");
3693
+ process.exit(0);
3694
+ }
3695
+ installGlobally = scope;
3696
+ }
3697
+ if (!options.yes) {
3698
+ const confirmed = await ye({
3699
+ message: "Proceed with installation?",
3700
+ initialValue: true
3701
+ });
3702
+ if (pD(confirmed) || !confirmed) {
3703
+ xe("Installation cancelled");
3704
+ process.exit(0);
3705
+ }
3706
+ }
3707
+ spinner.start("Installing skills...");
3708
+ const results = [];
3709
+ for (const skill of selectedSkills) for (const target of installTargets) {
3710
+ const result = await installSkillForAgent(skill, target.agent, {
3711
+ global: installGlobally,
3712
+ mode: "copy",
3713
+ eveSubagent: target.subagent
3714
+ });
3715
+ results.push({
3716
+ skill: getSkillDisplayName(skill),
3717
+ agent: targetDisplayName(target),
3718
+ ...result
3719
+ });
3720
+ }
3721
+ spinner.stop("Installation complete");
3722
+ console.log();
3723
+ const successful = results.filter((r) => r.success);
3724
+ const failed = results.filter((r) => !r.success);
3725
+ if (successful.length > 0) {
3726
+ const bySkill = /* @__PURE__ */ new Map();
3727
+ for (const r of successful) {
3728
+ if (!bySkill.has(r.skill)) bySkill.set(r.skill, []);
3729
+ bySkill.get(r.skill).push(r);
3730
+ }
3731
+ const resultLines = [];
3732
+ for (const [skillName, skillResults] of bySkill) {
3733
+ resultLines.push(`${import_picocolors.default.green("✓")} ${skillName} ${import_picocolors.default.dim("(copied)")}`);
3734
+ resultLines.push(...buildCopyResultLines(skillResults, process.cwd()));
3735
+ }
3736
+ Me(resultLines.join("\n"), import_picocolors.default.green(`Installed ${bySkill.size} skill${bySkill.size !== 1 ? "s" : ""}`));
3737
+ }
3738
+ if (failed.length > 0) {
3739
+ console.log();
3740
+ M.error(import_picocolors.default.red(`Failed to install ${failed.length}`));
3741
+ for (const r of failed) M.message(` ${import_picocolors.default.red("✗")} ${r.skill} → ${r.agent}: ${import_picocolors.default.dim(r.error)}`);
3742
+ }
3743
+ console.log();
3744
+ Se(import_picocolors.default.green("Done!") + import_picocolors.default.dim(" Review skills before use; they run with full agent permissions."));
3745
+ if (successful.length > 0 && !installGlobally) for (const skill of selectedSkills) try {
3746
+ const computedHash = await computeSkillFolderHash(skill.path);
3747
+ await addSkillToLocalLock(skill.name, {
3748
+ source,
3749
+ sourceType: "local",
3750
+ computedHash
3751
+ }, process.cwd());
3752
+ } catch {}
3753
+ await promptForFindSkills(options, targetAgents);
3754
+ }
3434
3755
  async function runAdd(args, options = {}) {
3435
3756
  const source = args[0];
3436
3757
  let installTipShown = false;
@@ -3461,50 +3782,139 @@ async function runAdd(args, options = {}) {
3461
3782
  options.yes = true;
3462
3783
  if (!options.agent || options.agent.length === 0) {
3463
3784
  const mappedAgent = getAgentType(agentResult.agent.name);
3464
- if (mappedAgent) options.agent = ensureUniversalAgents([mappedAgent]);
3785
+ if (!mappedAgent) {
3786
+ M.error(`Unsupported agent: ${agentResult.agent.name}`);
3787
+ M.info(`Specify the target explicitly with --agent <name>.`);
3788
+ M.info(`Valid agents: ${Object.keys(agents).join(", ")}`);
3789
+ process.exit(1);
3790
+ }
3791
+ options.agent = [mappedAgent];
3465
3792
  }
3466
3793
  }
3794
+ if (options.global === void 0 && options.agent?.some((a) => agents[a]?.requiresGlobal)) options.global = true;
3467
3795
  console.log();
3468
3796
  if (!agentResult.isAgent) Ie(import_picocolors.default.bgCyan(import_picocolors.default.black(" skills ")));
3469
3797
  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
3798
  else if (!process.stdin.isTTY) showInstallTip();
3471
3799
  let tempDir = null;
3800
+ if (source.split("/")[0]?.toLowerCase() === "openclaw" && !options.dangerouslyAcceptOpenclawRisks) {
3801
+ console.log();
3802
+ M.warn(import_picocolors.default.yellow(import_picocolors.default.bold("⚠ OpenClaw skills are unverified community submissions.")));
3803
+ M.message(import_picocolors.default.yellow("This source contains user-submitted skills that have not been reviewed for safety or quality."));
3804
+ M.message(import_picocolors.default.yellow("Skills run with full agent permissions and could be malicious."));
3805
+ console.log();
3806
+ 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`);
3807
+ Se(import_picocolors.default.red("Installation blocked"));
3808
+ process.exit(1);
3809
+ }
3472
3810
  try {
3473
3811
  const spinner = Y();
3812
+ if (looksLikeLocalPath(source)) {
3813
+ const expanded = expandLocalPath(source);
3814
+ if (!existsSync$1(expanded)) {
3815
+ Se(import_picocolors.default.red(`Local path does not exist: ${expanded}`));
3816
+ process.exit(1);
3817
+ }
3818
+ if (!isDirectory(expanded)) {
3819
+ Se(import_picocolors.default.red(`Local path is not a directory: ${expanded}`));
3820
+ process.exit(1);
3821
+ }
3822
+ const includeInternal = !!(options.skill && options.skill.length > 0);
3823
+ spinner.start("Discovering skills...");
3824
+ let skills;
3825
+ try {
3826
+ skills = await discoverSkills(expanded, void 0, {
3827
+ includeInternal,
3828
+ fullDepth: options.fullDepth
3829
+ });
3830
+ } finally {
3831
+ spinner.stop("Discovery complete");
3832
+ }
3833
+ if (skills.length === 0) {
3834
+ M.error(import_picocolors.default.red("No skills found"));
3835
+ Se(import_picocolors.default.red("No valid skills found. Skills require a SKILL.md with name and description."));
3836
+ process.exit(1);
3837
+ }
3838
+ await installLocalSkills(source, expanded, skills, options, agentResult);
3839
+ return;
3840
+ }
3474
3841
  const skillNameForLookup = options.skill?.[0];
3475
- let skillId;
3476
- try {
3477
- skillId = await resolveSkillId$1(source, skillNameForLookup);
3842
+ let skillIds;
3843
+ const repoRef = isRepoLevelRef(source) ? source : repoUrlToRef(source);
3844
+ const repoLevel = repoRef !== null;
3845
+ if (repoLevel) {
3846
+ const [sourceType, ownerRepo] = repoRef.split(":", 2);
3847
+ spinner.start("查询仓库下的技能...");
3848
+ let repoSkills;
3849
+ try {
3850
+ repoSkills = await listSkillsByRepo(sourceType, ownerRepo);
3851
+ } finally {
3852
+ spinner.stop("查询完成");
3853
+ }
3854
+ if (repoSkills.length === 0) {
3855
+ Se(import_picocolors.default.red(`仓库 "${source}" 下未找到已收录的技能。`));
3856
+ process.exit(1);
3857
+ }
3858
+ if (options.list) {
3859
+ console.log();
3860
+ M.step(import_picocolors.default.bold("Available Skills"));
3861
+ const sorted = [...repoSkills].sort((a, b) => a.name.localeCompare(b.name));
3862
+ for (const skill of sorted) {
3863
+ M.message(` ${import_picocolors.default.cyan(skill.name)}`);
3864
+ if (skill.description) M.message(` ${import_picocolors.default.dim(skill.description)}`);
3865
+ }
3866
+ console.log();
3867
+ Se("Use --skill <name> to install specific skills");
3868
+ process.exit(0);
3869
+ }
3870
+ if (options.skill && !options.skill.includes("*")) {
3871
+ const matched = repoSkills.filter((s) => options.skill.some((name) => s.name === name || s.skill_id.endsWith("/" + name)));
3872
+ if (matched.length === 0) {
3873
+ Se(import_picocolors.default.red(`未找到匹配 --skill 的技能: ${options.skill.join(", ")}`));
3874
+ process.exit(1);
3875
+ }
3876
+ skillIds = matched.map((s) => s.skill_id);
3877
+ } else if (options.skill?.includes("*") || options.all || options.yes) skillIds = repoSkills.map((s) => s.skill_id);
3878
+ else {
3879
+ const selected = await multiselect({
3880
+ message: "Select skills to install",
3881
+ options: [...repoSkills].sort((a, b) => a.name.localeCompare(b.name)).map((s) => ({
3882
+ value: s.skill_id,
3883
+ label: s.name,
3884
+ hint: s.description && s.description.length > 60 ? s.description.slice(0, 57) + "..." : s.description ?? ""
3885
+ })),
3886
+ required: true
3887
+ });
3888
+ if (pD(selected)) {
3889
+ xe("Installation cancelled");
3890
+ process.exit(0);
3891
+ }
3892
+ skillIds = selected;
3893
+ }
3894
+ } else try {
3895
+ const result = await resolveSkillId(source, skillNameForLookup);
3896
+ skillIds = Array.isArray(result) ? result : [result];
3478
3897
  } catch (e) {
3479
3898
  const message = e instanceof Error ? e.message : String(e);
3480
3899
  Se(import_picocolors.default.red(message));
3481
3900
  process.exit(1);
3482
3901
  }
3483
- spinner.start(`Downloading skill ${import_picocolors.default.cyan(skillId)} from API...`);
3902
+ spinner.start(`Downloading ${skillIds.length > 1 ? `${skillIds.length} skills` : `skill ${import_picocolors.default.cyan(skillIds[0])}`} from API...`);
3484
3903
  try {
3485
- tempDir = await downloadAndExtractSkill(skillId);
3904
+ tempDir = await mkdtemp(join$1(tmpdir(), "skills-"));
3905
+ for (const id of skillIds) await downloadAndExtractSkill(id, tempDir, options.version);
3486
3906
  } catch (e) {
3487
3907
  spinner.stop(import_picocolors.default.red("Download failed"));
3488
3908
  const message = e instanceof Error ? e.message : String(e);
3489
- Se(import_picocolors.default.red(`Failed to download skill "${skillId}": ${message}`));
3909
+ Se(import_picocolors.default.red(`Failed to download skill: ${message}`));
3490
3910
  process.exit(1);
3491
3911
  }
3492
3912
  spinner.stop(`Skill archive downloaded from ${import_picocolors.default.cyan("API")}`);
3493
3913
  const parsed = {
3494
3914
  type: "git",
3495
- url: skillId
3915
+ url: skillIds[0]
3496
3916
  };
3497
3917
  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
3918
  const includeInternal = !!(options.skill && options.skill.length > 0);
3509
3919
  let skills;
3510
3920
  let blobResult = null;
@@ -3553,7 +3963,13 @@ async function runAdd(args, options = {}) {
3553
3963
  process.exit(0);
3554
3964
  }
3555
3965
  let selectedSkills;
3556
- if (options.skill?.includes("*")) {
3966
+ if (repoLevel) {
3967
+ selectedSkills = skills;
3968
+ if (skills.length === 1) {
3969
+ M.info(`Skill: ${import_picocolors.default.cyan(getSkillDisplayName(skills[0]))}`);
3970
+ M.message(import_picocolors.default.dim(skills[0].description));
3971
+ } else M.info(`Installing ${skills.length} skills`);
3972
+ } else if (options.skill?.includes("*")) {
3557
3973
  selectedSkills = skills;
3558
3974
  M.info(`Installing all ${skills.length} skills`);
3559
3975
  } else if (options.skill && options.skill.length > 0) {
@@ -3616,9 +4032,10 @@ async function runAdd(args, options = {}) {
3616
4032
  }
3617
4033
  selectedSkills = selected;
3618
4034
  }
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);
4035
+ const auditPromise = skillIds.length === 1 ? fetchAuditBySkillId(skillIds[0]) : Promise.all(skillIds.map((id) => fetchAuditBySkillId(id))).then((results) => results.reduce((acc, r) => ({
4036
+ ...acc,
4037
+ ...r
4038
+ }), {}));
3622
4039
  let targetAgents;
3623
4040
  const validAgents = Object.keys(agents);
3624
4041
  if (options.agent?.includes("*")) {
@@ -3791,13 +4208,23 @@ async function runAdd(args, options = {}) {
3791
4208
  const printSkillSummary = (skills) => {
3792
4209
  for (const skill of skills) {
3793
4210
  if (summaryLines.length > 0) summaryLines.push("");
3794
- const shortCanonical = shortenPath$2(installTargets.length === 1 ? getCanonicalPath(skill.name, {
3795
- global: installGlobally,
3796
- agent: installTargets[0].agent,
3797
- eveSubagent: installTargets[0].subagent
3798
- }) : getCanonicalPath(skill.name, { global: installGlobally }), cwd);
3799
- summaryLines.push(`${import_picocolors.default.cyan(shortCanonical)}`);
3800
- summaryLines.push(...buildTargetSummaryLines(installTargets, installMode));
4211
+ if (installMode === "symlink") {
4212
+ const shortCanonical = shortenPath$2(installTargets.length === 1 ? getCanonicalPath(skill.name, {
4213
+ global: installGlobally,
4214
+ agent: installTargets[0].agent,
4215
+ eveSubagent: installTargets[0].subagent
4216
+ }) : getCanonicalPath(skill.name, { global: installGlobally }), cwd);
4217
+ summaryLines.push(`${import_picocolors.default.cyan(shortCanonical)}`);
4218
+ summaryLines.push(...buildTargetSummaryLines(installTargets, installMode));
4219
+ } else for (const target of installTargets) {
4220
+ const installPath = getInstallPath(skill.name, target.agent, {
4221
+ global: installGlobally,
4222
+ cwd,
4223
+ eveSubagent: target.subagent
4224
+ });
4225
+ summaryLines.push(`${import_picocolors.default.cyan(shortenPath$2(installPath, cwd))}`);
4226
+ summaryLines.push(` ${import_picocolors.default.dim("copy →")} ${targetDisplayName(target)}`);
4227
+ }
3801
4228
  const skillOverwrites = overwriteStatus.get(skill.name);
3802
4229
  const overwriteAgents = installTargets.filter((t) => skillOverwrites?.get(targetKey(t))).map(targetDisplayName);
3803
4230
  if (overwriteAgents.length > 0) summaryLines.push(` ${import_picocolors.default.yellow("overwrites:")} ${formatList$1(overwriteAgents)}`);
@@ -3826,7 +4253,7 @@ async function runAdd(args, options = {}) {
3826
4253
  let auditData = null;
3827
4254
  try {
3828
4255
  auditData = await auditPromise;
3829
- if (auditData && ownerRepoForAudit) {
4256
+ if (auditData) {
3830
4257
  const securityLines = buildSecurityLines(auditData, auditSkills);
3831
4258
  if (securityLines.length > 0) Me(securityLines.join("\n"), "Security Risk Assessments");
3832
4259
  }
@@ -4127,139 +4554,26 @@ function parseAddOptions(args) {
4127
4554
  }
4128
4555
  i--;
4129
4556
  } else if (arg === "--dangerously-accept-openclaw-risks") options.dangerouslyAcceptOpenclawRisks = true;
4130
- else if (arg && !arg.startsWith("-")) source.push(arg);
4557
+ else if (arg === "-v" || arg === "--version") {
4558
+ i++;
4559
+ options.version = args[i];
4560
+ } else if (arg && !arg.startsWith("-")) source.push(arg);
4131
4561
  }
4132
4562
  return {
4133
4563
  source,
4134
4564
  options
4135
4565
  };
4136
4566
  }
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
4567
  function parseAuditOptions(args) {
4249
- let source = "";
4250
- let skill = "";
4251
4568
  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();
4569
+ let skillId = "";
4570
+ for (const arg of args) if (arg && !arg.startsWith("-")) {
4571
+ skillId = arg.trim();
4572
+ break;
4258
4573
  }
4259
- if (!source) errors.push("Missing source or skill id");
4574
+ if (!skillId) errors.push("Missing skill id");
4260
4575
  return {
4261
- source,
4262
- skill,
4576
+ skillId: normalizeSkillId(skillId),
4263
4577
  errors
4264
4578
  };
4265
4579
  }
@@ -4267,7 +4581,7 @@ async function fetchSkillAudit(skillId, timeoutMs = 15e3) {
4267
4581
  const controller = new AbortController();
4268
4582
  const timeout = setTimeout(() => controller.abort(), timeoutMs);
4269
4583
  try {
4270
- const url = AUDIT_URL.replace("{skill_id}", skillId.split("/").map(encodeURIComponent).join("/"));
4584
+ const url = AUDIT_URL.replace("{skill_id}", skillId);
4271
4585
  let response;
4272
4586
  try {
4273
4587
  response = await fetch(url, { signal: controller.signal });
@@ -4361,20 +4675,12 @@ function buildAuditOutput(skillId, data) {
4361
4675
  return lines;
4362
4676
  }
4363
4677
  async function runAudit(args) {
4364
- const { source, skill, errors } = parseAuditOptions(args);
4678
+ const { skillId, errors } = parseAuditOptions(args);
4365
4679
  if (errors.length > 0) {
4366
4680
  for (const error of errors) console.error(import_picocolors.default.red(error));
4367
- console.error("Usage: wittyhub audit <source> --skill <skill>");
4368
- console.error(" wittyhub audit <skill_id>");
4369
- return;
4370
- }
4371
- const { skillId, derived } = await resolveSkillId(source, skill || void 0);
4372
- if (!skillId) {
4373
- console.error(import_picocolors.default.red(`Unable to resolve skill id from source: ${source}`));
4374
- console.error("Usage: wittyhub audit <source> --skill <skill>");
4681
+ console.error("Usage: wittyhub audit <skill_id>");
4375
4682
  return;
4376
4683
  }
4377
- if (derived) console.error(import_picocolors.default.yellow(`Could not locate skill in repo; trying derived id: ${skillId}`));
4378
4684
  const result = await fetchSkillAudit(skillId);
4379
4685
  track({
4380
4686
  event: "audit",
@@ -4406,18 +4712,21 @@ const YELLOW$1 = "\x1B[33m";
4406
4712
  const RED = "\x1B[31m";
4407
4713
  const GREEN = "\x1B[32m";
4408
4714
  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"}`;
4715
+ if (count === void 0 || count === null || Number.isNaN(count)) return "0";
4716
+ if (count >= 1e6) return `${(count / 1e6).toFixed(1).replace(/\.0$/, "")}M`;
4717
+ if (count >= 1e3) return `${(count / 1e3).toFixed(1).replace(/\.0$/, "")}K`;
4718
+ return String(count);
4414
4719
  }
4415
4720
  function formatRiskLevel$1(riskScore) {
4416
4721
  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}`;
4722
+ if (riskScore <= 20) return `${GREEN}safe${RESET$3}`;
4723
+ if (riskScore <= 50) return `${GREEN}low${RESET$3}`;
4724
+ if (riskScore <= 80) return `${YELLOW$1}medium${RESET$3}`;
4725
+ return `${RED}high${RESET$3}`;
4726
+ }
4727
+ const KV_KEY_WIDTH = 12;
4728
+ function kvKey(key) {
4729
+ return `${DIM$3}${key.padEnd(KV_KEY_WIDTH)}${RESET$3}`;
4421
4730
  }
4422
4731
  function escapeRegExp(s) {
4423
4732
  return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
@@ -4545,27 +4854,22 @@ async function runSearchPrompt(initialQuery = "", owner) {
4545
4854
  else if (results.length === 0 && searchError) lines.push(`${RED}${searchError}${RESET$3}`);
4546
4855
  else if (results.length === 0) lines.push(`${DIM$3}No skills found${RESET$3}`);
4547
4856
  else {
4548
- const visible = results.slice(0, 8);
4857
+ const visible = results.slice(0, 6);
4549
4858
  for (let i = 0; i < visible.length; i++) {
4550
4859
  const skill = visible[i];
4551
4860
  const isSelected = i === selectedIndex;
4552
4861
  const arrow = isSelected ? `${BOLD$3}>${RESET$3}` : " ";
4553
4862
  const hlName = highlightMatches(skill.name, query);
4554
4863
  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
4864
  const risk = formatRiskLevel$1(skill.riskScore);
4559
- const riskBadge = risk ? ` ${risk}` : "";
4560
4865
  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
- }
4866
+ lines.push(` ${arrow} ${kvKey("skill_name:")} ${name}${loadingIndicator}`);
4867
+ lines.push(` ${kvKey("skill_id:")} ${CYAN$1}${skill.slug}${RESET$3}`);
4868
+ lines.push(` ${kvKey("installs:")} ${CYAN$1}${formatInstalls$1(skill.installs)}${RESET$3}`);
4869
+ if (risk) lines.push(` ${kvKey("risk:")} ${risk}`);
4566
4870
  if (skill.description) {
4567
4871
  const descPlain = skill.description.length > 80 ? `${skill.description.slice(0, 77)}...` : skill.description;
4568
- lines.push(` ${DIM$3}${highlightMatches(descPlain, query)}${RESET$3}`);
4872
+ lines.push(` ${kvKey("description:")} ${DIM$3}${highlightMatches(descPlain, query)}${RESET$3}`);
4569
4873
  }
4570
4874
  }
4571
4875
  }
@@ -4655,44 +4959,16 @@ async function runSearchPrompt(initialQuery = "", owner) {
4655
4959
  process.stdin.on("keypress", handleKeypress);
4656
4960
  });
4657
4961
  }
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
4962
  async function runFind(args) {
4687
4963
  const { query, options: findOptions, errors } = parseFindOptions(args);
4688
4964
  const owner = findOptions.owner;
4689
4965
  const isNonInteractive = !process.stdin.isTTY;
4690
4966
  const agentTip = `${DIM$3}Tip: if running in a coding agent, follow these steps:${RESET$3}
4691
4967
  ${DIM$3} 1) npx wittyhub find [query] [--owner <owner>]${RESET$3}
4692
- ${DIM$3} 2) npx wittyhub add <owner/repo@skill>${RESET$3}`;
4968
+ ${DIM$3} 2) npx wittyhub add <skill_id>${RESET$3}`;
4693
4969
  if (errors.length > 0) {
4694
4970
  for (const error of errors) console.error(error);
4695
- console.error("Usage: npx skills find <query> [--owner <owner>]");
4971
+ console.error("Usage: npx wittyhub find <query> [--owner <owner>]");
4696
4972
  return;
4697
4973
  }
4698
4974
  if (query) {
@@ -4711,26 +4987,19 @@ ${DIM$3} 2) npx wittyhub add <owner/repo@skill>${RESET$3}`;
4711
4987
  console.log(`${DIM$3}No skills found for "${query}"${ownerSuffix}${RESET$3}`);
4712
4988
  return;
4713
4989
  }
4714
- console.log(`${DIM$3}Install with${RESET$3} npx wittyhub add <repo-url> --skill <skill>`);
4990
+ console.log(`${DIM$3}Install with${RESET$3} npx wittyhub add ${TEXT$2}<skill_id>${RESET$3}`);
4715
4991
  console.log();
4716
4992
  for (const skill of results.slice(0, 6)) {
4717
- const raw = skill.sourceUrl || skill.source || skill.slug;
4718
4993
  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
4994
  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
- }
4995
+ console.log(`${BOLD$3}●${RESET$3} ${kvKey("skill_name:")} ${BOLD$3}${hlName}${RESET$3}`);
4996
+ console.log(` ${kvKey("skill_id:")} ${CYAN$1}${skill.slug}${RESET$3}`);
4997
+ console.log(` ${kvKey("installs:")} ${CYAN$1}${formatInstalls$1(skill.installs)}${RESET$3}`);
4998
+ if (risk) console.log(` ${kvKey("risk:")} ${risk}`);
4729
4999
  if (skill.description) {
4730
5000
  const descPlain = skill.description.length > 100 ? `${skill.description.slice(0, 97)}...` : skill.description;
4731
- console.log(`${DIM$3} ${highlightMatches(descPlain, query)}${RESET$3}`);
5001
+ console.log(` ${kvKey("description:")} ${DIM$3}${highlightMatches(descPlain, query)}${RESET$3}`);
4732
5002
  }
4733
- console.log(`${DIM$3}└ ${skill.sourceUrl || `https://skills.sh/${skill.slug}`}${RESET$3}`);
4734
5003
  console.log();
4735
5004
  }
4736
5005
  return;
@@ -4753,38 +5022,27 @@ ${DIM$3} 2) npx wittyhub add <owner/repo@skill>${RESET$3}`;
4753
5022
  console.log();
4754
5023
  return;
4755
5024
  }
4756
- const pkg = selected.sourceUrl || selected.source || selected.slug;
5025
+ const skillId = selected.slug;
4757
5026
  const skillName = selected.name;
4758
5027
  console.log();
4759
- console.log(`${TEXT$2}Installing ${BOLD$3}${skillName}${RESET$3} from ${DIM$3}${pkg}${RESET$3}...`);
5028
+ console.log(`${TEXT$2}Installing ${BOLD$3}${skillName}${RESET$3} ${DIM$3}(${skillId})${RESET$3}...`);
4760
5029
  console.log();
4761
- const { source, options: addOptions } = parseAddOptions([
4762
- pkg,
4763
- "--skill",
4764
- skillName
4765
- ]);
5030
+ const { source, options: addOptions } = parseAddOptions([skillId]);
4766
5031
  await runAdd(source, addOptions);
4767
5032
  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}`);
5033
+ console.log(`${DIM$3}View the skill at${RESET$3} ${TEXT$2}https://skillhub.openeuler.org/skills/${selected.slug}${RESET$3}`);
4771
5034
  console.log();
4772
5035
  }
4773
5036
  function parseGetOptions(args) {
4774
- let source = "";
4775
- let skill = "";
4776
5037
  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();
5038
+ let skillId = "";
5039
+ for (const arg of args) if (arg && !arg.startsWith("-")) {
5040
+ skillId = arg.trim();
5041
+ break;
4783
5042
  }
4784
- if (!source) errors.push("Missing source or skill id");
5043
+ if (!skillId) errors.push("Missing skill id");
4785
5044
  return {
4786
- source,
4787
- skill,
5045
+ skillId: normalizeSkillId(skillId),
4788
5046
  errors
4789
5047
  };
4790
5048
  }
@@ -4792,7 +5050,7 @@ async function fetchSkillDetail(skillId, timeoutMs = 15e3) {
4792
5050
  const controller = new AbortController();
4793
5051
  const timeout = setTimeout(() => controller.abort(), timeoutMs);
4794
5052
  try {
4795
- const url = GET_URL.replace("{skill_id}", skillId.split("/").map(encodeURIComponent).join("/"));
5053
+ const url = GET_URL.replace("{skill_id}", skillId);
4796
5054
  let response;
4797
5055
  try {
4798
5056
  response = await fetch(url, { signal: controller.signal });
@@ -4885,20 +5143,12 @@ function buildGetOutput(data) {
4885
5143
  return lines;
4886
5144
  }
4887
5145
  async function runGet(args) {
4888
- const { source, skill, errors } = parseGetOptions(args);
5146
+ const { skillId, errors } = parseGetOptions(args);
4889
5147
  if (errors.length > 0) {
4890
5148
  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>");
5149
+ console.error("Usage: wittyhub get <skill_id>");
4893
5150
  return;
4894
5151
  }
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
5152
  const result = await fetchSkillDetail(skillId);
4903
5153
  track({
4904
5154
  event: "get",
@@ -5405,6 +5655,26 @@ async function runList(args) {
5405
5655
  console.log();
5406
5656
  }
5407
5657
  }
5658
+ function extractSkillName(input) {
5659
+ try {
5660
+ const parsed = parseSource(input);
5661
+ if (parsed.subpath) {
5662
+ const segments = parsed.subpath.split("/");
5663
+ const last = segments[segments.length - 1];
5664
+ if (last) return last;
5665
+ }
5666
+ if (parsed.type !== "local" && parsed.url) {
5667
+ const match = parsed.url.match(/\/([^/]+)\.git$/);
5668
+ if (match?.[1]) return match[1];
5669
+ }
5670
+ } catch {}
5671
+ if (input.includes("/")) {
5672
+ const segments = input.replace(/\.git$/, "").split("/");
5673
+ const last = segments[segments.length - 1];
5674
+ if (last) return last;
5675
+ }
5676
+ return input;
5677
+ }
5408
5678
  async function removeCommand(skillNames, options) {
5409
5679
  const agentResult = await detectAgent();
5410
5680
  if (agentResult.isAgent) {
@@ -5450,7 +5720,10 @@ async function removeCommand(skillNames, options) {
5450
5720
  let selectedSkills = [];
5451
5721
  if (options.all) selectedSkills = installedSkills;
5452
5722
  else if (skillNames.length > 0) {
5453
- selectedSkills = installedSkills.filter((s) => skillNames.some((name) => name.toLowerCase() === s.toLowerCase()));
5723
+ selectedSkills = installedSkills.filter((s) => skillNames.some((name) => {
5724
+ if (name.toLowerCase() === s.toLowerCase()) return true;
5725
+ return extractSkillName(name).toLowerCase() === s.toLowerCase();
5726
+ }));
5454
5727
  if (selectedSkills.length === 0) {
5455
5728
  M.error(`No matching skills found for: ${skillNames.join(", ")}`);
5456
5729
  return;
@@ -6822,7 +7095,7 @@ function showBanner() {
6822
7095
  console.log(` ${DIM}$${RESET} ${TEXT}npx wittyhub validate ${DIM}<path>${RESET} ${DIM}Validate a local skill${RESET}`);
6823
7096
  console.log(` ${DIM}$${RESET} ${TEXT}npx wittyhub experimental_sync${RESET} ${DIM}Sync skills from node_modules${RESET}`);
6824
7097
  console.log();
6825
- console.log(`${DIM}try:${RESET} npx wittyhub add vercel-labs/agent-skills`);
7098
+ console.log(`${DIM}try:${RESET} npx wittyhub add gitcode:openeuler/witty-diagnosis-agent`);
6826
7099
  console.log();
6827
7100
  console.log(`Discover more skills at ${TEXT}https://skillhub.openeuler.org/${RESET}`);
6828
7101
  console.log();
@@ -6840,12 +7113,8 @@ ${BOLD}Manage Skills:${RESET}
6840
7113
  remove [skills] Remove installed skills
6841
7114
  list, ls List installed skills
6842
7115
  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
7116
+ audit <skill_id> Show security audit result (risk level + risk signals)
7117
+ get <skill_id> View skill details (author/category/version/description/tags)
6849
7118
  validate <path> Validate a local skill for spec compliance (SKILL.md filename,
6850
7119
  frontmatter, required fields name/description, etc.)
6851
7120
 
@@ -6875,6 +7144,7 @@ ${BOLD}Add Options:${RESET}
6875
7144
  --subagent <names> Install to Eve subagents (use 'root' for the root agent)
6876
7145
  --all Shorthand for --skill '*' --agent '*' -y
6877
7146
  --full-depth Search all subdirectories even when a root SKILL.md exists
7147
+ -v, --version <ver> Download a specific skill version from skill_versions table
6878
7148
 
6879
7149
  ${BOLD}Use Options:${RESET}
6880
7150
  -s, --skill <skill> Specify the skill to use