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.
- package/README.md +165 -98
- package/dist/cli.mjs +673 -403
- 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
|
|
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;
|
|
@@ -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
|
-
|
|
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.
|
|
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
|
|
3180
|
-
if (
|
|
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
|
-
|
|
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)
|
|
3210
|
-
|
|
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)
|
|
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
|
|
3476
|
-
|
|
3477
|
-
|
|
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(
|
|
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
|
|
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
|
|
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:
|
|
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 (
|
|
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
|
|
3620
|
-
|
|
3621
|
-
|
|
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
|
-
|
|
3795
|
-
|
|
3796
|
-
|
|
3797
|
-
|
|
3798
|
-
|
|
3799
|
-
|
|
3800
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
4253
|
-
|
|
4254
|
-
|
|
4255
|
-
|
|
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 (!
|
|
4574
|
+
if (!skillId) errors.push("Missing skill id");
|
|
4260
4575
|
return {
|
|
4261
|
-
|
|
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
|
|
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 {
|
|
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 <
|
|
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
|
|
4411
|
-
if (count >=
|
|
4412
|
-
|
|
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
|
|
4418
|
-
if (riskScore <= 50) return `${GREEN}low
|
|
4419
|
-
if (riskScore <= 80) return `${YELLOW$1}medium
|
|
4420
|
-
return `${RED}high
|
|
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,
|
|
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} ${
|
|
4562
|
-
|
|
4563
|
-
|
|
4564
|
-
|
|
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 <
|
|
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
|
|
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 <
|
|
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
|
-
|
|
4724
|
-
console.log(
|
|
4725
|
-
|
|
4726
|
-
|
|
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(
|
|
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
|
|
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}
|
|
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
|
-
|
|
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
|
-
|
|
4778
|
-
|
|
4779
|
-
|
|
4780
|
-
|
|
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 (!
|
|
5043
|
+
if (!skillId) errors.push("Missing skill id");
|
|
4785
5044
|
return {
|
|
4786
|
-
|
|
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
|
|
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 {
|
|
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 <
|
|
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) =>
|
|
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
|
|
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 <
|
|
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
|
|
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
|