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.
- package/README.md +149 -97
- package/dist/cli.mjs +557 -392
- 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
|
|
277
|
-
const SEARCH_URL =
|
|
278
|
-
const TELEMETRY_URL =
|
|
279
|
-
const AUDIT_URL =
|
|
280
|
-
const GET_URL =
|
|
281
|
-
const 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
|
-
|
|
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
|
|
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.
|
|
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
|
|
3180
|
-
if (
|
|
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
|
-
|
|
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)
|
|
3210
|
-
|
|
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
|
|
3476
|
-
|
|
3477
|
-
|
|
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(
|
|
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
|
|
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
|
|
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:
|
|
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 (
|
|
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
|
|
3620
|
-
|
|
3621
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
4253
|
-
|
|
4254
|
-
|
|
4255
|
-
|
|
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 (!
|
|
4469
|
+
if (!skillId) errors.push("Missing skill id");
|
|
4260
4470
|
return {
|
|
4261
|
-
|
|
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
|
|
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 {
|
|
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 <
|
|
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
|
|
4411
|
-
if (count >=
|
|
4412
|
-
|
|
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
|
|
4418
|
-
if (riskScore <= 50) return `${GREEN}low
|
|
4419
|
-
if (riskScore <= 80) return `${YELLOW$1}medium
|
|
4420
|
-
return `${RED}high
|
|
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,
|
|
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} ${
|
|
4562
|
-
|
|
4563
|
-
|
|
4564
|
-
|
|
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 <
|
|
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
|
|
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 <
|
|
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
|
-
|
|
4724
|
-
console.log(
|
|
4725
|
-
|
|
4726
|
-
|
|
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(
|
|
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
|
|
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}
|
|
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
|
-
|
|
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
|
-
|
|
4778
|
-
|
|
4779
|
-
|
|
4780
|
-
|
|
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 (!
|
|
4938
|
+
if (!skillId) errors.push("Missing skill id");
|
|
4785
4939
|
return {
|
|
4786
|
-
|
|
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
|
|
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 {
|
|
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 <
|
|
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) =>
|
|
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
|
|
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 <
|
|
6844
|
-
|
|
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
|