wittyhub 0.0.3 → 0.0.4
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 +4 -3
- package/dist/_chunks/libs/simple-git.mjs +1 -1
- package/dist/cli.mjs +1286 -674
- package/package.json +1 -1
package/dist/cli.mjs
CHANGED
|
@@ -11,14 +11,18 @@ import { existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync } from
|
|
|
11
11
|
import { basename, dirname, extname, isAbsolute, join, normalize, relative, resolve, sep } from "path";
|
|
12
12
|
import { fileURLToPath } from "url";
|
|
13
13
|
import { createHash } from "node:crypto";
|
|
14
|
-
import {
|
|
15
|
-
import {
|
|
14
|
+
import { execFile } from "node:child_process";
|
|
15
|
+
import { promisify, stripVTControlCharacters } from "node:util";
|
|
16
|
+
import { mkdtemp, rm, writeFile } from "node:fs/promises";
|
|
17
|
+
import { homedir, tmpdir } from "node:os";
|
|
18
|
+
import { dirname as dirname$1, join as join$1, sep as sep$1 } from "node:path";
|
|
19
|
+
import { homedir as homedir$1, platform, tmpdir as tmpdir$1 } from "os";
|
|
20
|
+
import { parse } from "yaml";
|
|
16
21
|
import * as readline from "readline";
|
|
17
22
|
import { Writable } from "stream";
|
|
18
|
-
import { promisify } from "util";
|
|
19
|
-
import { execFile, execSync, spawn, spawnSync } from "child_process";
|
|
20
|
-
import { access, cp, lstat, mkdir, mkdtemp, readFile, readdir, readlink, realpath, rm, stat, symlink, writeFile } from "fs/promises";
|
|
21
|
-
import { parse } from "yaml";
|
|
23
|
+
import { promisify as promisify$1 } from "util";
|
|
24
|
+
import { execFile as execFile$1, execSync, spawn, spawnSync } from "child_process";
|
|
25
|
+
import { access, cp, lstat, mkdir, mkdtemp as mkdtemp$1, readFile, readdir, readlink, realpath, rm as rm$1, stat, symlink, writeFile as writeFile$1 } from "fs/promises";
|
|
22
26
|
import { createHash as createHash$1 } from "crypto";
|
|
23
27
|
import { gunzipSync, inflateRawSync } from "node:zlib";
|
|
24
28
|
var import_picocolors = /* @__PURE__ */ __toESM(require_picocolors(), 1);
|
|
@@ -251,6 +255,30 @@ function isWellKnownUrl(input) {
|
|
|
251
255
|
return false;
|
|
252
256
|
}
|
|
253
257
|
}
|
|
258
|
+
const CLI_CONFIG_PATH = join(homedir$1(), ".config", "wittyhub", "cli.yaml");
|
|
259
|
+
function loadCliConfig() {
|
|
260
|
+
if (!existsSync(CLI_CONFIG_PATH)) return {};
|
|
261
|
+
try {
|
|
262
|
+
const parsed = parse(readFileSync(CLI_CONFIG_PATH, "utf-8"));
|
|
263
|
+
if (!parsed || typeof parsed !== "object") return {};
|
|
264
|
+
return parsed;
|
|
265
|
+
} catch {
|
|
266
|
+
return {};
|
|
267
|
+
}
|
|
268
|
+
}
|
|
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
|
+
const cliConfig = loadCliConfig();
|
|
276
|
+
const DOWNLOAD_URL = `${typeof cliConfig.api_url === "string" && cliConfig.api_url.trim() ? cliConfig.api_url.trim() : process.env.SKILLS_API_URL ? `${process.env.SKILLS_API_URL.replace(/\/$/, "")}/api/v1` : DEFAULT_API_URL}/skills/{skill_id}/download`;
|
|
277
|
+
const SEARCH_URL = typeof cliConfig.search_url === "string" && cliConfig.search_url.trim() ? cliConfig.search_url.trim() : process.env.SKILLS_API_URL ? `${process.env.SKILLS_API_URL.replace(/\/$/, "")}/api/v1/index/search` : DEFAULT_SEARCH_URL;
|
|
278
|
+
const TELEMETRY_URL = typeof cliConfig.telemetry_url === "string" && cliConfig.telemetry_url.trim() ? cliConfig.telemetry_url.trim() : DEFAULT_TELEMETRY_URL;
|
|
279
|
+
const AUDIT_URL = typeof cliConfig.audit_url === "string" && cliConfig.audit_url.trim() ? cliConfig.audit_url.trim() : DEFAULT_AUDIT_URL;
|
|
280
|
+
const GET_URL = typeof cliConfig.get_url === "string" && cliConfig.get_url.trim() ? cliConfig.get_url.trim() : DEFAULT_GET_URL;
|
|
281
|
+
const LIST_URL = typeof cliConfig.list_url === "string" && cliConfig.list_url.trim() ? cliConfig.list_url.trim() : GET_URL.replace(/\{skill_id\}[^/]*$/, "") || DEFAULT_LIST_URL;
|
|
254
282
|
const CSI_RE = /\x1b\[[\x30-\x3f]*[\x20-\x2f]*[\x40-\x7e]/g;
|
|
255
283
|
const OSC_RE = /\x1b\][\s\S]*?(?:\x07|\x1b\\)/g;
|
|
256
284
|
const DCS_PM_APC_RE = /\x1b[P^_][\s\S]*?(?:\x1b\\)/g;
|
|
@@ -453,7 +481,7 @@ const CLONE_TIMEOUT_MS = (() => {
|
|
|
453
481
|
const parsed = Number.parseInt(raw, 10);
|
|
454
482
|
return Number.isFinite(parsed) && parsed > 0 ? parsed : DEFAULT_CLONE_TIMEOUT_MS;
|
|
455
483
|
})();
|
|
456
|
-
const execFileAsync = promisify(execFile);
|
|
484
|
+
const execFileAsync$1 = promisify$1(execFile$1);
|
|
457
485
|
var GitCloneError = class extends Error {
|
|
458
486
|
url;
|
|
459
487
|
isTimeout;
|
|
@@ -524,11 +552,12 @@ function createGitClient(extraEnv) {
|
|
|
524
552
|
"filter.lfs.smudge=",
|
|
525
553
|
"filter.lfs.clean=",
|
|
526
554
|
"filter.lfs.process="
|
|
527
|
-
]
|
|
555
|
+
],
|
|
556
|
+
unsafe: { allowUnsafeFilter: true }
|
|
528
557
|
});
|
|
529
558
|
}
|
|
530
559
|
async function resetTempDir(dir) {
|
|
531
|
-
await rm(dir, {
|
|
560
|
+
await rm$1(dir, {
|
|
532
561
|
recursive: true,
|
|
533
562
|
force: true
|
|
534
563
|
}).catch(() => {});
|
|
@@ -537,7 +566,7 @@ async function resetTempDir(dir) {
|
|
|
537
566
|
async function tryGhClone(repo, tempDir, ref) {
|
|
538
567
|
let cloneTarget = repo.slug;
|
|
539
568
|
try {
|
|
540
|
-
const { stdout, stderr } = await execFileAsync("gh", [
|
|
569
|
+
const { stdout, stderr } = await execFileAsync$1("gh", [
|
|
541
570
|
"auth",
|
|
542
571
|
"status",
|
|
543
572
|
"-h",
|
|
@@ -554,7 +583,7 @@ async function tryGhClone(repo, tempDir, ref) {
|
|
|
554
583
|
} catch {
|
|
555
584
|
return false;
|
|
556
585
|
}
|
|
557
|
-
await execFileAsync("gh", [
|
|
586
|
+
await execFileAsync$1("gh", [
|
|
558
587
|
"repo",
|
|
559
588
|
"clone",
|
|
560
589
|
cloneTarget,
|
|
@@ -580,7 +609,7 @@ function buildGitHubAuthError(url, repo, message) {
|
|
|
580
609
|
return `Authentication failed for ${url}.\n - For private repos, ensure you have access\n - For SSH: Check your keys with 'ssh -T git@github.com'\n - For HTTPS: Run 'gh auth login' or configure git credentials`;
|
|
581
610
|
}
|
|
582
611
|
async function cloneRepo(url, ref) {
|
|
583
|
-
const tempDir = await mkdtemp(join(tmpdir(), "skills-"));
|
|
612
|
+
const tempDir = await mkdtemp$1(join(tmpdir$1(), "skills-"));
|
|
584
613
|
const cloneOptions = ref ? [
|
|
585
614
|
"--depth",
|
|
586
615
|
"1",
|
|
@@ -596,7 +625,7 @@ async function cloneRepo(url, ref) {
|
|
|
596
625
|
const isTimeout = errorMessage.includes("block timeout") || errorMessage.includes("timed out");
|
|
597
626
|
const isAuthError = isAuthFailure(errorMessage);
|
|
598
627
|
if (isTimeout) {
|
|
599
|
-
await rm(tempDir, {
|
|
628
|
+
await rm$1(tempDir, {
|
|
600
629
|
recursive: true,
|
|
601
630
|
force: true
|
|
602
631
|
}).catch(() => {});
|
|
@@ -613,7 +642,7 @@ async function cloneRepo(url, ref) {
|
|
|
613
642
|
return tempDir;
|
|
614
643
|
} catch {}
|
|
615
644
|
}
|
|
616
|
-
await rm(tempDir, {
|
|
645
|
+
await rm$1(tempDir, {
|
|
617
646
|
recursive: true,
|
|
618
647
|
force: true
|
|
619
648
|
}).catch(() => {});
|
|
@@ -623,9 +652,9 @@ async function cloneRepo(url, ref) {
|
|
|
623
652
|
}
|
|
624
653
|
async function cleanupTempDir(dir) {
|
|
625
654
|
const normalizedDir = normalize(resolve(dir));
|
|
626
|
-
const normalizedTmpDir = normalize(resolve(tmpdir()));
|
|
655
|
+
const normalizedTmpDir = normalize(resolve(tmpdir$1()));
|
|
627
656
|
if (!normalizedDir.startsWith(normalizedTmpDir + sep) && normalizedDir !== normalizedTmpDir) throw new Error("Attempted to clean up directory outside of temp directory");
|
|
628
|
-
await rm(dir, {
|
|
657
|
+
await rm$1(dir, {
|
|
629
658
|
recursive: true,
|
|
630
659
|
force: true
|
|
631
660
|
});
|
|
@@ -731,7 +760,7 @@ async function writeLocalLock(lock, cwd) {
|
|
|
731
760
|
version: lock.version,
|
|
732
761
|
skills: sortedSkills
|
|
733
762
|
};
|
|
734
|
-
await writeFile(lockPath, JSON.stringify(sorted, null, 2) + "\n", "utf-8");
|
|
763
|
+
await writeFile$1(lockPath, JSON.stringify(sorted, null, 2) + "\n", "utf-8");
|
|
735
764
|
}
|
|
736
765
|
async function computeSkillFolderHash(skillDir) {
|
|
737
766
|
const files = [];
|
|
@@ -950,7 +979,7 @@ function filterSkills(skills, inputNames) {
|
|
|
950
979
|
return normalizedInputs.some((input) => input === name || input === displayName);
|
|
951
980
|
});
|
|
952
981
|
}
|
|
953
|
-
const home = homedir();
|
|
982
|
+
const home = homedir$1();
|
|
954
983
|
const configHome = xdgConfig ?? join(home, ".config");
|
|
955
984
|
const codexHome = process.env.CODEX_HOME?.trim() || join(home, ".codex");
|
|
956
985
|
const claudeHome = process.env.CLAUDE_CONFIG_DIR?.trim() || join(home, ".claude");
|
|
@@ -1679,7 +1708,7 @@ async function isDirEntryOrSymlinkToDir(entry, entryPath) {
|
|
|
1679
1708
|
}
|
|
1680
1709
|
}
|
|
1681
1710
|
function getCanonicalSkillsDir(global, cwd) {
|
|
1682
|
-
return join(global ? homedir() : cwd || process.cwd(), AGENTS_DIR$1, SKILLS_SUBDIR);
|
|
1711
|
+
return join(global ? homedir$1() : cwd || process.cwd(), AGENTS_DIR$1, SKILLS_SUBDIR);
|
|
1683
1712
|
}
|
|
1684
1713
|
function getEveSubagentSkillsDir(subagent, cwd) {
|
|
1685
1714
|
return join(cwd || process.cwd(), EVE_SUBAGENTS_DIR, sanitizeName(subagent), "skills");
|
|
@@ -1688,7 +1717,7 @@ function getAgentBaseDir(agentType, global, cwd, eveSubagent) {
|
|
|
1688
1717
|
if (isUniversalAgent(agentType)) return getCanonicalSkillsDir(global, cwd);
|
|
1689
1718
|
if (agentType === "eve" && eveSubagent) return getEveSubagentSkillsDir(eveSubagent, cwd);
|
|
1690
1719
|
const agent = agents[agentType];
|
|
1691
|
-
const baseDir = global ? homedir() : cwd || process.cwd();
|
|
1720
|
+
const baseDir = global ? homedir$1() : cwd || process.cwd();
|
|
1692
1721
|
if (global) {
|
|
1693
1722
|
if (agent.globalSkillsDir === void 0) return join(baseDir, agent.skillsDir);
|
|
1694
1723
|
return agent.globalSkillsDir;
|
|
@@ -1700,7 +1729,7 @@ function resolveSymlinkTarget(linkPath, linkTarget) {
|
|
|
1700
1729
|
}
|
|
1701
1730
|
async function cleanAndCreateDirectory(path) {
|
|
1702
1731
|
try {
|
|
1703
|
-
await rm(path, {
|
|
1732
|
+
await rm$1(path, {
|
|
1704
1733
|
recursive: true,
|
|
1705
1734
|
force: true
|
|
1706
1735
|
});
|
|
@@ -1727,11 +1756,11 @@ async function createSymlink(target, linkPath) {
|
|
|
1727
1756
|
try {
|
|
1728
1757
|
if ((await lstat(linkPath)).isSymbolicLink()) {
|
|
1729
1758
|
if (resolveSymlinkTarget(linkPath, await readlink(linkPath)) === resolvedTarget) return true;
|
|
1730
|
-
await rm(linkPath);
|
|
1731
|
-
} else await rm(linkPath, { recursive: true });
|
|
1759
|
+
await rm$1(linkPath);
|
|
1760
|
+
} else await rm$1(linkPath, { recursive: true });
|
|
1732
1761
|
} catch (err) {
|
|
1733
1762
|
if (err && typeof err === "object" && "code" in err && err.code === "ELOOP") try {
|
|
1734
|
-
await rm(linkPath, { force: true });
|
|
1763
|
+
await rm$1(linkPath, { force: true });
|
|
1735
1764
|
} catch {}
|
|
1736
1765
|
}
|
|
1737
1766
|
const linkDir = dirname(linkPath);
|
|
@@ -1872,7 +1901,7 @@ async function copyDirectory(src, dest, agentType) {
|
|
|
1872
1901
|
if (entry.isDirectory()) await copyDirectory(srcPath, destPath, agentType);
|
|
1873
1902
|
else try {
|
|
1874
1903
|
if (agentType === "eve" && entry.name.toLowerCase() === "skill.md") {
|
|
1875
|
-
await writeFile(destPath, stripIgnoredEveFrontmatter(await readFile(srcPath, "utf-8")));
|
|
1904
|
+
await writeFile$1(destPath, stripIgnoredEveFrontmatter(await readFile(srcPath, "utf-8")));
|
|
1876
1905
|
return;
|
|
1877
1906
|
}
|
|
1878
1907
|
await cp(srcPath, destPath, {
|
|
@@ -1927,88 +1956,6 @@ function getCanonicalPath(skillName, options = {}) {
|
|
|
1927
1956
|
if (!isPathSafe$1(canonicalBase, canonicalPath)) throw new Error("Invalid skill name: potential path traversal detected");
|
|
1928
1957
|
return canonicalPath;
|
|
1929
1958
|
}
|
|
1930
|
-
async function installWellKnownSkillForAgent(skill, agentType, options = {}) {
|
|
1931
|
-
const agent = agents[agentType];
|
|
1932
|
-
const isGlobal = options.global ?? false;
|
|
1933
|
-
const cwd = options.cwd || process.cwd();
|
|
1934
|
-
const installMode = options.mode ?? "symlink";
|
|
1935
|
-
const eveSubagent = options.eveSubagent;
|
|
1936
|
-
if (isGlobal && agent.globalSkillsDir === void 0) return {
|
|
1937
|
-
success: false,
|
|
1938
|
-
path: "",
|
|
1939
|
-
mode: installMode,
|
|
1940
|
-
error: `${agent.displayName} does not support global skill installation`
|
|
1941
|
-
};
|
|
1942
|
-
const skillName = sanitizeName(skill.installName);
|
|
1943
|
-
const canonicalBase = agentType === "eve" && installMode === "symlink" ? getAgentBaseDir(agentType, isGlobal, cwd, eveSubagent) : getCanonicalSkillsDir(isGlobal, cwd);
|
|
1944
|
-
const canonicalDir = join(canonicalBase, skillName);
|
|
1945
|
-
const agentBase = getAgentBaseDir(agentType, isGlobal, cwd, eveSubagent);
|
|
1946
|
-
const agentDir = join(agentBase, skillName);
|
|
1947
|
-
if (!isPathSafe$1(canonicalBase, canonicalDir)) return {
|
|
1948
|
-
success: false,
|
|
1949
|
-
path: agentDir,
|
|
1950
|
-
mode: installMode,
|
|
1951
|
-
error: "Invalid skill name: potential path traversal detected"
|
|
1952
|
-
};
|
|
1953
|
-
if (!isPathSafe$1(agentBase, agentDir)) return {
|
|
1954
|
-
success: false,
|
|
1955
|
-
path: agentDir,
|
|
1956
|
-
mode: installMode,
|
|
1957
|
-
error: "Invalid skill name: potential path traversal detected"
|
|
1958
|
-
};
|
|
1959
|
-
async function writeSkillFiles(targetDir) {
|
|
1960
|
-
for (const [filePath, content] of skill.files) {
|
|
1961
|
-
const fullPath = join(targetDir, filePath);
|
|
1962
|
-
if (!isPathSafe$1(targetDir, fullPath)) continue;
|
|
1963
|
-
const parentDir = dirname(fullPath);
|
|
1964
|
-
if (parentDir !== targetDir) await mkdir(parentDir, { recursive: true });
|
|
1965
|
-
await writeFile(fullPath, agentType === "eve" && basename(filePath).toLowerCase() === "skill.md" && typeof content === "string" ? stripIgnoredEveFrontmatter(content) : content);
|
|
1966
|
-
}
|
|
1967
|
-
}
|
|
1968
|
-
try {
|
|
1969
|
-
if (installMode === "copy") {
|
|
1970
|
-
await cleanAndCreateDirectory(agentDir);
|
|
1971
|
-
await writeSkillFiles(agentDir);
|
|
1972
|
-
return {
|
|
1973
|
-
success: true,
|
|
1974
|
-
path: agentDir,
|
|
1975
|
-
mode: "copy"
|
|
1976
|
-
};
|
|
1977
|
-
}
|
|
1978
|
-
await cleanAndCreateDirectory(canonicalDir);
|
|
1979
|
-
await writeSkillFiles(canonicalDir);
|
|
1980
|
-
if (isGlobal && isUniversalAgent(agentType)) return {
|
|
1981
|
-
success: true,
|
|
1982
|
-
path: canonicalDir,
|
|
1983
|
-
canonicalPath: canonicalDir,
|
|
1984
|
-
mode: "symlink"
|
|
1985
|
-
};
|
|
1986
|
-
if (!await createSymlink(canonicalDir, agentDir)) {
|
|
1987
|
-
await cleanAndCreateDirectory(agentDir);
|
|
1988
|
-
await writeSkillFiles(agentDir);
|
|
1989
|
-
return {
|
|
1990
|
-
success: true,
|
|
1991
|
-
path: agentDir,
|
|
1992
|
-
canonicalPath: canonicalDir,
|
|
1993
|
-
mode: "symlink",
|
|
1994
|
-
symlinkFailed: true
|
|
1995
|
-
};
|
|
1996
|
-
}
|
|
1997
|
-
return {
|
|
1998
|
-
success: true,
|
|
1999
|
-
path: agentDir,
|
|
2000
|
-
canonicalPath: canonicalDir,
|
|
2001
|
-
mode: "symlink"
|
|
2002
|
-
};
|
|
2003
|
-
} catch (error) {
|
|
2004
|
-
return {
|
|
2005
|
-
success: false,
|
|
2006
|
-
path: agentDir,
|
|
2007
|
-
mode: installMode,
|
|
2008
|
-
error: error instanceof Error ? error.message : "Unknown error"
|
|
2009
|
-
};
|
|
2010
|
-
}
|
|
2011
|
-
}
|
|
2012
1959
|
async function installBlobSkillForAgent(skill, agentType, options = {}) {
|
|
2013
1960
|
const agent = agents[agentType];
|
|
2014
1961
|
const isGlobal = options.global ?? false;
|
|
@@ -2033,11 +1980,11 @@ async function installBlobSkillForAgent(skill, agentType, options = {}) {
|
|
|
2033
1980
|
};
|
|
2034
1981
|
try {
|
|
2035
1982
|
await mkdir(agentBase, { recursive: true });
|
|
2036
|
-
await rm(flatSkillPath, {
|
|
1983
|
+
await rm$1(flatSkillPath, {
|
|
2037
1984
|
recursive: true,
|
|
2038
1985
|
force: true
|
|
2039
1986
|
});
|
|
2040
|
-
await writeFile(flatSkillPath, getEveFlatSkillMarkdown(skill.files), "utf-8");
|
|
1987
|
+
await writeFile$1(flatSkillPath, getEveFlatSkillMarkdown(skill.files), "utf-8");
|
|
2041
1988
|
return {
|
|
2042
1989
|
success: true,
|
|
2043
1990
|
path: flatSkillPath,
|
|
@@ -2073,7 +2020,7 @@ async function installBlobSkillForAgent(skill, agentType, options = {}) {
|
|
|
2073
2020
|
if (!isPathSafe$1(targetDir, fullPath)) continue;
|
|
2074
2021
|
const parentDir = dirname(fullPath);
|
|
2075
2022
|
if (parentDir !== targetDir) await mkdir(parentDir, { recursive: true });
|
|
2076
|
-
await writeFile(fullPath, agentType === "eve" && basename(file.path).toLowerCase() === "skill.md" ? stripIgnoredEveFrontmatter(file.contents) : file.contents, "utf-8");
|
|
2023
|
+
await writeFile$1(fullPath, agentType === "eve" && basename(file.path).toLowerCase() === "skill.md" ? stripIgnoredEveFrontmatter(file.contents) : file.contents, "utf-8");
|
|
2077
2024
|
}
|
|
2078
2025
|
}
|
|
2079
2026
|
try {
|
|
@@ -2260,21 +2207,6 @@ async function listInstalledSkills(options = {}) {
|
|
|
2260
2207
|
} catch {}
|
|
2261
2208
|
return Array.from(skillsMap.values());
|
|
2262
2209
|
}
|
|
2263
|
-
const CLI_CONFIG_PATH = join(homedir(), ".config", "wittyhub", "cli.yaml");
|
|
2264
|
-
function loadCliConfig() {
|
|
2265
|
-
if (!existsSync(CLI_CONFIG_PATH)) return {};
|
|
2266
|
-
try {
|
|
2267
|
-
const parsed = parse(readFileSync(CLI_CONFIG_PATH, "utf-8"));
|
|
2268
|
-
if (!parsed || typeof parsed !== "object") return {};
|
|
2269
|
-
return parsed;
|
|
2270
|
-
} catch {
|
|
2271
|
-
return {};
|
|
2272
|
-
}
|
|
2273
|
-
}
|
|
2274
|
-
const DEFAULT_TELEMETRY_URL = "http://localhost:8080/api/v1/skills/telemetry";
|
|
2275
|
-
const cliConfig = loadCliConfig();
|
|
2276
|
-
const TELEMETRY_URL = typeof cliConfig.telemetry_url === "string" && cliConfig.telemetry_url.trim() ? cliConfig.telemetry_url.trim() : DEFAULT_TELEMETRY_URL;
|
|
2277
|
-
typeof cliConfig.audit_url === "string" && cliConfig.audit_url.trim() && cliConfig.audit_url.trim();
|
|
2278
2210
|
let cliVersion = null;
|
|
2279
2211
|
let detectedAgentName = null;
|
|
2280
2212
|
function setDetectedAgent(agentName) {
|
|
@@ -2289,6 +2221,67 @@ function isEnabled() {
|
|
|
2289
2221
|
function setVersion(version) {
|
|
2290
2222
|
cliVersion = version;
|
|
2291
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
|
+
}
|
|
2292
2285
|
const pendingTelemetry = [];
|
|
2293
2286
|
function track(data) {
|
|
2294
2287
|
if (!isEnabled()) return;
|
|
@@ -2806,7 +2799,7 @@ const CURRENT_VERSION = 3;
|
|
|
2806
2799
|
function getSkillLockPath() {
|
|
2807
2800
|
const xdgStateHome = process.env.XDG_STATE_HOME;
|
|
2808
2801
|
if (xdgStateHome) return join(xdgStateHome, "skills", LOCK_FILE);
|
|
2809
|
-
return join(homedir(), AGENTS_DIR, LOCK_FILE);
|
|
2802
|
+
return join(homedir$1(), AGENTS_DIR, LOCK_FILE);
|
|
2810
2803
|
}
|
|
2811
2804
|
async function readSkillLock() {
|
|
2812
2805
|
const lockPath = getSkillLockPath();
|
|
@@ -2823,7 +2816,7 @@ async function readSkillLock() {
|
|
|
2823
2816
|
async function writeSkillLock(lock) {
|
|
2824
2817
|
const lockPath = getSkillLockPath();
|
|
2825
2818
|
await mkdir(dirname(lockPath), { recursive: true });
|
|
2826
|
-
await writeFile(lockPath, JSON.stringify(lock, null, 2), "utf-8");
|
|
2819
|
+
await writeFile$1(lockPath, JSON.stringify(lock, null, 2), "utf-8");
|
|
2827
2820
|
}
|
|
2828
2821
|
let _ghWarningShown = false;
|
|
2829
2822
|
function getGitHubToken() {
|
|
@@ -3160,65 +3153,126 @@ async function tryBlobInstall(ownerRepo, options = {}) {
|
|
|
3160
3153
|
tree
|
|
3161
3154
|
};
|
|
3162
3155
|
}
|
|
3163
|
-
var version$1 = "0.0.
|
|
3156
|
+
var version$1 = "0.0.4";
|
|
3164
3157
|
const isCancelled$1 = (value) => typeof value === "symbol";
|
|
3165
|
-
async function isSourcePrivate(source) {
|
|
3166
|
-
const ownerRepo = parseOwnerRepo(source);
|
|
3167
|
-
if (!ownerRepo) return false;
|
|
3168
|
-
return isRepoPrivate(ownerRepo.owner, ownerRepo.repo);
|
|
3169
|
-
}
|
|
3170
3158
|
function getLockSource(parsedUrl, normalizedSource) {
|
|
3171
3159
|
return parsedUrl.startsWith("git@") || parsedUrl.startsWith("ssh://") ? parsedUrl : normalizedSource;
|
|
3172
3160
|
}
|
|
3173
3161
|
function initTelemetry(version) {
|
|
3174
3162
|
setVersion(version);
|
|
3175
3163
|
}
|
|
3176
|
-
|
|
3164
|
+
const execFileAsync = promisify(execFile);
|
|
3165
|
+
function normalizeRepoRef(input) {
|
|
3166
|
+
let s = input.trim().toLowerCase();
|
|
3167
|
+
s = s.replace(/^(https?:\/\/|git:\/\/|ssh:\/\/)/, "");
|
|
3168
|
+
s = s.replace(/^git@([^:]+):/, "$1/");
|
|
3169
|
+
s = s.replace(/\.git$/, "");
|
|
3170
|
+
s = s.replace(/\/+$/, "");
|
|
3171
|
+
return s;
|
|
3172
|
+
}
|
|
3173
|
+
function looksLikeRepoSource(source) {
|
|
3174
|
+
const s = source.trim();
|
|
3175
|
+
if (s.startsWith("http://") || s.startsWith("https://")) return true;
|
|
3176
|
+
if (s.startsWith("git@") || s.startsWith("ssh://") || s.startsWith("git://")) return true;
|
|
3177
|
+
return (s.match(/\//g) || []).length <= 1 && !s.includes(":") && !/\s/.test(s);
|
|
3178
|
+
}
|
|
3179
|
+
async function resolveSkillId$1(source, skillName) {
|
|
3180
|
+
if (!looksLikeRepoSource(source)) return source;
|
|
3181
|
+
if (!skillName || !skillName.trim()) throw new Error(`源 "${source}" 看起来是仓库地址,需要同时传入 --skill <skill_name> 才能定位具体技能。`);
|
|
3182
|
+
const searchUrl = `${SEARCH_URL}?${new URLSearchParams({
|
|
3183
|
+
q: skillName.trim(),
|
|
3184
|
+
limit: "20",
|
|
3185
|
+
mode: "text",
|
|
3186
|
+
scope: "summary"
|
|
3187
|
+
}).toString()}`;
|
|
3188
|
+
let res;
|
|
3189
|
+
try {
|
|
3190
|
+
res = await fetch(searchUrl);
|
|
3191
|
+
} catch {
|
|
3192
|
+
throw new Error(`无法连接搜索服务: ${SEARCH_URL}`);
|
|
3193
|
+
}
|
|
3194
|
+
if (!res.ok) throw new Error(`搜索服务返回 HTTP ${res.status}`);
|
|
3195
|
+
const payload = await res.json();
|
|
3196
|
+
const results = payload.data?.results ?? payload.results ?? [];
|
|
3197
|
+
if (results.length === 0) throw new Error(`未找到名称匹配 "${skillName}" 的技能。`);
|
|
3198
|
+
const wantedRef = normalizeRepoRef(source);
|
|
3199
|
+
const match = results.find((r) => {
|
|
3200
|
+
const ref = normalizeRepoRef(r.source_url);
|
|
3201
|
+
return ref === wantedRef || ref.startsWith(`${wantedRef}/`);
|
|
3202
|
+
}) ?? results[0];
|
|
3203
|
+
if (!match) throw new Error(`未找到名称匹配 "${skillName}" 的技能。`);
|
|
3204
|
+
return match.skill_id;
|
|
3205
|
+
}
|
|
3206
|
+
async function downloadAndExtractSkill(skillId) {
|
|
3207
|
+
const downloadUrl = DOWNLOAD_URL.replace("{skill_id}", encodeURIComponent(skillId));
|
|
3208
|
+
const response = await fetch(downloadUrl);
|
|
3209
|
+
if (!response.ok) throw new Error(`HTTP ${response.status} ${response.statusText}`);
|
|
3210
|
+
const tempDir = await mkdtemp(join$1(tmpdir(), "skills-"));
|
|
3211
|
+
const zipPath = join$1(tempDir, ".skill.zip");
|
|
3212
|
+
try {
|
|
3213
|
+
await writeFile(zipPath, Buffer.from(await response.arrayBuffer()));
|
|
3214
|
+
try {
|
|
3215
|
+
await execFileAsync("unzip", [
|
|
3216
|
+
"-q",
|
|
3217
|
+
zipPath,
|
|
3218
|
+
"-d",
|
|
3219
|
+
tempDir
|
|
3220
|
+
]);
|
|
3221
|
+
} catch {
|
|
3222
|
+
throw new Error("Failed to extract skill archive. Ensure the 'unzip' command is available on PATH.");
|
|
3223
|
+
}
|
|
3224
|
+
} finally {
|
|
3225
|
+
await rm(zipPath, { force: true });
|
|
3226
|
+
}
|
|
3227
|
+
return tempDir;
|
|
3228
|
+
}
|
|
3229
|
+
function riskLabel$1(risk) {
|
|
3177
3230
|
switch (risk) {
|
|
3178
|
-
case "critical": return import_picocolors.default.red(import_picocolors.default.bold("Critical
|
|
3179
|
-
case "high": return import_picocolors.default.red("High
|
|
3180
|
-
case "medium": return import_picocolors.default.yellow("Med
|
|
3181
|
-
case "low": return import_picocolors.default.green("Low
|
|
3231
|
+
case "critical": return import_picocolors.default.red(import_picocolors.default.bold("Critical"));
|
|
3232
|
+
case "high": return import_picocolors.default.red("High");
|
|
3233
|
+
case "medium": return import_picocolors.default.yellow("Med");
|
|
3234
|
+
case "low": return import_picocolors.default.green("Low");
|
|
3182
3235
|
case "safe": return import_picocolors.default.green("Safe");
|
|
3183
|
-
default: return import_picocolors.default.dim("
|
|
3236
|
+
default: return import_picocolors.default.dim("Unknown");
|
|
3184
3237
|
}
|
|
3185
3238
|
}
|
|
3186
|
-
function socketLabel(audit) {
|
|
3187
|
-
if (!audit) return import_picocolors.default.dim("--");
|
|
3188
|
-
const count = audit.alerts ?? 0;
|
|
3189
|
-
return count > 0 ? import_picocolors.default.red(`${count} alert${count !== 1 ? "s" : ""}`) : import_picocolors.default.green("0 alerts");
|
|
3190
|
-
}
|
|
3191
3239
|
function padEnd(str, width) {
|
|
3192
3240
|
const visible = stripTerminalEscapes(str);
|
|
3193
3241
|
const pad = Math.max(0, width - visible.length);
|
|
3194
3242
|
return str + " ".repeat(pad);
|
|
3195
3243
|
}
|
|
3196
|
-
function buildSecurityLines(auditData, skills
|
|
3244
|
+
function buildSecurityLines(auditData, skills) {
|
|
3197
3245
|
if (!auditData) return [];
|
|
3198
|
-
|
|
3199
|
-
|
|
3200
|
-
|
|
3201
|
-
})) return [];
|
|
3202
|
-
const nameWidth = Math.min(Math.max(...skills.map((s) => s.displayName.length)), 36);
|
|
3246
|
+
const rows = skills.filter((s) => auditData[s.slug]);
|
|
3247
|
+
if (rows.length === 0) return [];
|
|
3248
|
+
const nameWidth = Math.min(Math.max(...rows.map((s) => s.displayName.length)), 36);
|
|
3203
3249
|
const lines = [];
|
|
3204
|
-
const header = padEnd("", nameWidth + 2) + padEnd(import_picocolors.default.dim("
|
|
3250
|
+
const header = padEnd("", nameWidth + 2) + padEnd(import_picocolors.default.dim("Risk"), 18) + padEnd(import_picocolors.default.dim("Score"), 8) + import_picocolors.default.dim("Signals");
|
|
3205
3251
|
lines.push(header);
|
|
3206
|
-
for (const skill of
|
|
3252
|
+
for (const skill of rows) {
|
|
3207
3253
|
const data = auditData[skill.slug];
|
|
3208
3254
|
const name = skill.displayName.length > nameWidth ? skill.displayName.slice(0, nameWidth - 1) + "…" : skill.displayName;
|
|
3209
|
-
const
|
|
3210
|
-
const
|
|
3211
|
-
const
|
|
3212
|
-
lines.push(padEnd(import_picocolors.default.cyan(name), nameWidth + 2) + padEnd(
|
|
3255
|
+
const risk = riskLabel$1(data.risk_level);
|
|
3256
|
+
const score = data.risk_score != null ? import_picocolors.default.dim(String(data.risk_score)) : import_picocolors.default.dim("--");
|
|
3257
|
+
const signals = data.risk_signals.length > 0 ? import_picocolors.default.yellow(String(data.risk_signals.length)) : import_picocolors.default.dim("0");
|
|
3258
|
+
lines.push(padEnd(import_picocolors.default.cyan(name), nameWidth + 2) + padEnd(risk, 18) + padEnd(score, 8) + signals);
|
|
3213
3259
|
}
|
|
3214
|
-
lines.push("");
|
|
3215
|
-
lines.push(`${import_picocolors.default.dim("Details:")} ${import_picocolors.default.dim(`https://skills.sh/${source}`)}`);
|
|
3216
3260
|
return lines;
|
|
3217
3261
|
}
|
|
3262
|
+
function getHighRiskSkills(auditData, skills) {
|
|
3263
|
+
if (!auditData) return [];
|
|
3264
|
+
return skills.filter((s) => {
|
|
3265
|
+
const data = auditData[s.slug];
|
|
3266
|
+
return data && (data.risk_level === "high" || data.risk_level === "critical");
|
|
3267
|
+
}).map((s) => ({
|
|
3268
|
+
name: s.displayName,
|
|
3269
|
+
riskLevel: auditData[s.slug].risk_level
|
|
3270
|
+
}));
|
|
3271
|
+
}
|
|
3218
3272
|
function shortenPath$2(fullPath, cwd) {
|
|
3219
3273
|
const home = homedir();
|
|
3220
|
-
if (fullPath === home || fullPath.startsWith(home + sep)) return "~" + fullPath.slice(home.length);
|
|
3221
|
-
if (fullPath === cwd || fullPath.startsWith(cwd + sep)) return "." + fullPath.slice(cwd.length);
|
|
3274
|
+
if (fullPath === home || fullPath.startsWith(home + sep$1)) return "~" + fullPath.slice(home.length);
|
|
3275
|
+
if (fullPath === cwd || fullPath.startsWith(cwd + sep$1)) return "." + fullPath.slice(cwd.length);
|
|
3222
3276
|
return fullPath;
|
|
3223
3277
|
}
|
|
3224
3278
|
function computeSingleFileSkillHash(contents) {
|
|
@@ -3233,6 +3287,13 @@ function formatList$1(items, maxShow = 5) {
|
|
|
3233
3287
|
const remaining = items.length - maxShow;
|
|
3234
3288
|
return `${shown.join(", ")} +${remaining} more`;
|
|
3235
3289
|
}
|
|
3290
|
+
function buildSkillFiles(selectedSkills, tempDir, blobResult) {
|
|
3291
|
+
const skillFiles = {};
|
|
3292
|
+
for (const skill of selectedSkills) if (blobResult && "repoPath" in skill) skillFiles[skill.name] = skill.repoPath;
|
|
3293
|
+
else if (tempDir && skill.path === tempDir) skillFiles[skill.name] = "SKILL.md";
|
|
3294
|
+
else if (tempDir && skill.path.startsWith(tempDir + sep$1)) skillFiles[skill.name] = skill.path.slice(tempDir.length + 1).split(sep$1).join("/") + "/SKILL.md";
|
|
3295
|
+
return skillFiles;
|
|
3296
|
+
}
|
|
3236
3297
|
function splitAgentsByType(agentTypes) {
|
|
3237
3298
|
const universal = [];
|
|
3238
3299
|
const symlinked = [];
|
|
@@ -3243,18 +3304,6 @@ function splitAgentsByType(agentTypes) {
|
|
|
3243
3304
|
symlinked
|
|
3244
3305
|
};
|
|
3245
3306
|
}
|
|
3246
|
-
function buildAgentSummaryLines(targetAgents, installMode) {
|
|
3247
|
-
const lines = [];
|
|
3248
|
-
const { universal, symlinked } = splitAgentsByType(targetAgents);
|
|
3249
|
-
if (installMode === "symlink") {
|
|
3250
|
-
if (universal.length > 0) lines.push(` ${import_picocolors.default.green("universal:")} ${formatList$1(universal)}`);
|
|
3251
|
-
if (symlinked.length > 0) lines.push(` ${import_picocolors.default.dim("symlink →")} ${formatList$1(symlinked)}`);
|
|
3252
|
-
} else {
|
|
3253
|
-
const allNames = targetAgents.map((a) => agents[a].displayName);
|
|
3254
|
-
lines.push(` ${import_picocolors.default.dim("copy →")} ${formatList$1(allNames)}`);
|
|
3255
|
-
}
|
|
3256
|
-
return lines;
|
|
3257
|
-
}
|
|
3258
3307
|
function targetDisplayName(target) {
|
|
3259
3308
|
const base = agents[target.agent].displayName;
|
|
3260
3309
|
return target.subagent ? `${base} (${target.subagent})` : base;
|
|
@@ -3382,419 +3431,95 @@ async function selectAgentsInteractive(options) {
|
|
|
3382
3431
|
return selected;
|
|
3383
3432
|
}
|
|
3384
3433
|
setVersion(version$1);
|
|
3385
|
-
async function
|
|
3386
|
-
|
|
3387
|
-
|
|
3388
|
-
|
|
3389
|
-
|
|
3390
|
-
|
|
3434
|
+
async function runAdd(args, options = {}) {
|
|
3435
|
+
const source = args[0];
|
|
3436
|
+
let installTipShown = false;
|
|
3437
|
+
const showInstallTip = () => {
|
|
3438
|
+
if (installTipShown) return;
|
|
3439
|
+
M.message(import_picocolors.default.dim("Tip: use the --yes (-y) and --global (-g) flags to install without prompts."));
|
|
3440
|
+
installTipShown = true;
|
|
3441
|
+
};
|
|
3442
|
+
if (!source) {
|
|
3443
|
+
console.log();
|
|
3444
|
+
console.log(import_picocolors.default.bgRed(import_picocolors.default.white(import_picocolors.default.bold(" ERROR "))) + " " + import_picocolors.default.red("Missing required argument: source"));
|
|
3445
|
+
console.log();
|
|
3446
|
+
console.log(import_picocolors.default.dim(" Usage:"));
|
|
3447
|
+
console.log(` ${import_picocolors.default.cyan("npx wittyhub add")} ${import_picocolors.default.yellow("<source>")} ${import_picocolors.default.dim("[options]")}`);
|
|
3448
|
+
console.log();
|
|
3449
|
+
console.log(import_picocolors.default.dim(" Example:"));
|
|
3450
|
+
console.log(` ${import_picocolors.default.cyan("npx wittyhub add")} ${import_picocolors.default.yellow("vercel-labs/agent-skills")}`);
|
|
3451
|
+
console.log();
|
|
3391
3452
|
process.exit(1);
|
|
3392
3453
|
}
|
|
3393
|
-
|
|
3394
|
-
|
|
3395
|
-
|
|
3396
|
-
|
|
3397
|
-
if (skill.files.size > 1) M.message(import_picocolors.default.dim(` Files: ${Array.from(skill.files.keys()).join(", ")}`));
|
|
3454
|
+
if (options.all) {
|
|
3455
|
+
options.skill = ["*"];
|
|
3456
|
+
options.agent = ["*"];
|
|
3457
|
+
options.yes = true;
|
|
3398
3458
|
}
|
|
3399
|
-
|
|
3400
|
-
|
|
3401
|
-
|
|
3402
|
-
|
|
3403
|
-
|
|
3404
|
-
|
|
3405
|
-
if (skill.files.size > 1) M.message(` ${import_picocolors.default.dim(`Files: ${skill.files.size}`)}`);
|
|
3459
|
+
const agentResult = await detectAgent();
|
|
3460
|
+
if (agentResult.isAgent) {
|
|
3461
|
+
options.yes = true;
|
|
3462
|
+
if (!options.agent || options.agent.length === 0) {
|
|
3463
|
+
const mappedAgent = getAgentType(agentResult.agent.name);
|
|
3464
|
+
if (mappedAgent) options.agent = ensureUniversalAgents([mappedAgent]);
|
|
3406
3465
|
}
|
|
3407
|
-
console.log();
|
|
3408
|
-
Se("Run without --list to install");
|
|
3409
|
-
process.exit(0);
|
|
3410
3466
|
}
|
|
3411
|
-
|
|
3412
|
-
if (
|
|
3413
|
-
|
|
3414
|
-
|
|
3415
|
-
|
|
3416
|
-
|
|
3417
|
-
|
|
3418
|
-
|
|
3419
|
-
|
|
3420
|
-
|
|
3467
|
+
console.log();
|
|
3468
|
+
if (!agentResult.isAgent) Ie(import_picocolors.default.bgCyan(import_picocolors.default.black(" skills ")));
|
|
3469
|
+
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
|
+
else if (!process.stdin.isTTY) showInstallTip();
|
|
3471
|
+
let tempDir = null;
|
|
3472
|
+
try {
|
|
3473
|
+
const spinner = Y();
|
|
3474
|
+
const skillNameForLookup = options.skill?.[0];
|
|
3475
|
+
let skillId;
|
|
3476
|
+
try {
|
|
3477
|
+
skillId = await resolveSkillId$1(source, skillNameForLookup);
|
|
3478
|
+
} catch (e) {
|
|
3479
|
+
const message = e instanceof Error ? e.message : String(e);
|
|
3480
|
+
Se(import_picocolors.default.red(message));
|
|
3421
3481
|
process.exit(1);
|
|
3422
3482
|
}
|
|
3423
|
-
|
|
3424
|
-
|
|
3425
|
-
|
|
3426
|
-
|
|
3427
|
-
|
|
3428
|
-
|
|
3429
|
-
|
|
3430
|
-
} else {
|
|
3431
|
-
const selected = await multiselect({
|
|
3432
|
-
message: "Select skills to install",
|
|
3433
|
-
options: skills.map((s) => ({
|
|
3434
|
-
value: s,
|
|
3435
|
-
label: s.installName,
|
|
3436
|
-
hint: s.description.length > 60 ? s.description.slice(0, 57) + "..." : s.description
|
|
3437
|
-
})),
|
|
3438
|
-
required: true
|
|
3439
|
-
});
|
|
3440
|
-
if (pD(selected)) {
|
|
3441
|
-
xe("Installation cancelled");
|
|
3442
|
-
process.exit(0);
|
|
3443
|
-
}
|
|
3444
|
-
selectedSkills = selected;
|
|
3445
|
-
}
|
|
3446
|
-
let targetAgents;
|
|
3447
|
-
const validAgents = Object.keys(agents);
|
|
3448
|
-
if (options.agent?.includes("*")) {
|
|
3449
|
-
targetAgents = validAgents;
|
|
3450
|
-
M.info(`Installing to all ${targetAgents.length} agents`);
|
|
3451
|
-
} else if (options.agent && options.agent.length > 0) {
|
|
3452
|
-
const invalidAgents = options.agent.filter((a) => !validAgents.includes(a));
|
|
3453
|
-
if (invalidAgents.length > 0) {
|
|
3454
|
-
M.error(`Invalid agents: ${invalidAgents.join(", ")}`);
|
|
3455
|
-
M.info(`Valid agents: ${validAgents.join(", ")}`);
|
|
3483
|
+
spinner.start(`Downloading skill ${import_picocolors.default.cyan(skillId)} from API...`);
|
|
3484
|
+
try {
|
|
3485
|
+
tempDir = await downloadAndExtractSkill(skillId);
|
|
3486
|
+
} catch (e) {
|
|
3487
|
+
spinner.stop(import_picocolors.default.red("Download failed"));
|
|
3488
|
+
const message = e instanceof Error ? e.message : String(e);
|
|
3489
|
+
Se(import_picocolors.default.red(`Failed to download skill "${skillId}": ${message}`));
|
|
3456
3490
|
process.exit(1);
|
|
3457
3491
|
}
|
|
3458
|
-
|
|
3459
|
-
|
|
3460
|
-
|
|
3461
|
-
|
|
3462
|
-
|
|
3463
|
-
|
|
3464
|
-
if (
|
|
3465
|
-
|
|
3466
|
-
M.
|
|
3467
|
-
|
|
3468
|
-
M.
|
|
3469
|
-
|
|
3470
|
-
|
|
3471
|
-
|
|
3472
|
-
|
|
3473
|
-
if (pD(selected)) {
|
|
3474
|
-
xe("Installation cancelled");
|
|
3475
|
-
process.exit(0);
|
|
3476
|
-
}
|
|
3477
|
-
targetAgents = selected;
|
|
3478
|
-
}
|
|
3479
|
-
else if (installedAgents.length === 1 || options.yes) {
|
|
3480
|
-
targetAgents = ensureUniversalAgents(installedAgents);
|
|
3481
|
-
if (installedAgents.length === 1) {
|
|
3482
|
-
const firstAgent = installedAgents[0];
|
|
3483
|
-
M.info(`Installing to: ${import_picocolors.default.cyan(agents[firstAgent].displayName)}`);
|
|
3484
|
-
} else M.info(`Installing to: ${installedAgents.map((a) => import_picocolors.default.cyan(agents[a].displayName)).join(", ")}`);
|
|
3485
|
-
} else {
|
|
3486
|
-
const selected = await selectAgentsInteractive({ global: options.global });
|
|
3487
|
-
if (pD(selected)) {
|
|
3488
|
-
xe("Installation cancelled");
|
|
3489
|
-
process.exit(0);
|
|
3490
|
-
}
|
|
3491
|
-
targetAgents = selected;
|
|
3492
|
-
}
|
|
3493
|
-
}
|
|
3494
|
-
let installGlobally = options.global ?? false;
|
|
3495
|
-
const supportsGlobal = targetAgents.some((a) => agents[a].globalSkillsDir !== void 0);
|
|
3496
|
-
if (options.global === void 0 && !options.yes && supportsGlobal) {
|
|
3497
|
-
const scope = await ve({
|
|
3498
|
-
message: "Installation scope",
|
|
3499
|
-
options: [{
|
|
3500
|
-
value: false,
|
|
3501
|
-
label: "Project",
|
|
3502
|
-
hint: "Install in current directory (committed with your project)"
|
|
3503
|
-
}, {
|
|
3504
|
-
value: true,
|
|
3505
|
-
label: "Global",
|
|
3506
|
-
hint: "Install in home directory (available across all projects)"
|
|
3507
|
-
}]
|
|
3508
|
-
});
|
|
3509
|
-
if (pD(scope)) {
|
|
3510
|
-
xe("Installation cancelled");
|
|
3511
|
-
process.exit(0);
|
|
3492
|
+
spinner.stop(`Skill archive downloaded from ${import_picocolors.default.cyan("API")}`);
|
|
3493
|
+
const parsed = {
|
|
3494
|
+
type: "git",
|
|
3495
|
+
url: skillId
|
|
3496
|
+
};
|
|
3497
|
+
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);
|
|
3512
3507
|
}
|
|
3513
|
-
|
|
3514
|
-
|
|
3515
|
-
|
|
3516
|
-
|
|
3517
|
-
|
|
3518
|
-
|
|
3519
|
-
|
|
3520
|
-
options: [{
|
|
3521
|
-
value: "symlink",
|
|
3522
|
-
label: "Symlink (Recommended)",
|
|
3523
|
-
hint: "Single source of truth, easy updates"
|
|
3524
|
-
}, {
|
|
3525
|
-
value: "copy",
|
|
3526
|
-
label: "Copy to all agents",
|
|
3527
|
-
hint: "Independent copies for each agent"
|
|
3528
|
-
}]
|
|
3508
|
+
const includeInternal = !!(options.skill && options.skill.length > 0);
|
|
3509
|
+
let skills;
|
|
3510
|
+
let blobResult = null;
|
|
3511
|
+
spinner.start("Discovering skills...");
|
|
3512
|
+
skills = await discoverSkills(tempDir, void 0, {
|
|
3513
|
+
includeInternal,
|
|
3514
|
+
fullDepth: options.fullDepth
|
|
3529
3515
|
});
|
|
3530
|
-
if (
|
|
3531
|
-
|
|
3532
|
-
|
|
3516
|
+
if (skills.length === 0) {
|
|
3517
|
+
spinner.stop(import_picocolors.default.red("No skills found"));
|
|
3518
|
+
Se(import_picocolors.default.red("No valid skills found. Skills require a SKILL.md with name and description."));
|
|
3519
|
+
await cleanup(tempDir);
|
|
3520
|
+
process.exit(1);
|
|
3533
3521
|
}
|
|
3534
|
-
|
|
3535
|
-
} else if (uniqueDirs.size <= 1) installMode = "copy";
|
|
3536
|
-
const cwd = process.cwd();
|
|
3537
|
-
const summaryLines = [];
|
|
3538
|
-
targetAgents.map((a) => agents[a].displayName);
|
|
3539
|
-
const overwriteChecks = await Promise.all(selectedSkills.flatMap((skill) => targetAgents.map(async (agent) => ({
|
|
3540
|
-
skillName: skill.installName,
|
|
3541
|
-
agent,
|
|
3542
|
-
installed: await isSkillInstalled(skill.installName, agent, { global: installGlobally })
|
|
3543
|
-
}))));
|
|
3544
|
-
const overwriteStatus = /* @__PURE__ */ new Map();
|
|
3545
|
-
for (const { skillName, agent, installed } of overwriteChecks) {
|
|
3546
|
-
if (!overwriteStatus.has(skillName)) overwriteStatus.set(skillName, /* @__PURE__ */ new Map());
|
|
3547
|
-
overwriteStatus.get(skillName).set(agent, installed);
|
|
3548
|
-
}
|
|
3549
|
-
for (const skill of selectedSkills) {
|
|
3550
|
-
if (summaryLines.length > 0) summaryLines.push("");
|
|
3551
|
-
const shortCanonical = shortenPath$2(getCanonicalPath(skill.installName, { global: installGlobally }), cwd);
|
|
3552
|
-
summaryLines.push(`${import_picocolors.default.cyan(shortCanonical)}`);
|
|
3553
|
-
summaryLines.push(...buildAgentSummaryLines(targetAgents, installMode));
|
|
3554
|
-
if (skill.files.size > 1) summaryLines.push(` ${import_picocolors.default.dim("files:")} ${skill.files.size}`);
|
|
3555
|
-
const skillOverwrites = overwriteStatus.get(skill.installName);
|
|
3556
|
-
const overwriteAgents = targetAgents.filter((a) => skillOverwrites?.get(a)).map((a) => agents[a].displayName);
|
|
3557
|
-
if (overwriteAgents.length > 0) summaryLines.push(` ${import_picocolors.default.yellow("overwrites:")} ${formatList$1(overwriteAgents)}`);
|
|
3558
|
-
}
|
|
3559
|
-
console.log();
|
|
3560
|
-
Me(summaryLines.join("\n"), "Installation Summary");
|
|
3561
|
-
if (!options.yes) {
|
|
3562
|
-
const confirmed = await ye({ message: "Proceed with installation?" });
|
|
3563
|
-
if (pD(confirmed) || !confirmed) {
|
|
3564
|
-
xe("Installation cancelled");
|
|
3565
|
-
process.exit(0);
|
|
3566
|
-
}
|
|
3567
|
-
}
|
|
3568
|
-
const sourceIdentifier = wellKnownProvider.getSourceIdentifier(url);
|
|
3569
|
-
const wellKnownPrivacyPromise = isSourcePrivate(sourceIdentifier).catch(() => null);
|
|
3570
|
-
spinner.start("Installing skills...");
|
|
3571
|
-
const results = [];
|
|
3572
|
-
for (const skill of selectedSkills) for (const agent of targetAgents) {
|
|
3573
|
-
const result = await installWellKnownSkillForAgent(skill, agent, {
|
|
3574
|
-
global: installGlobally,
|
|
3575
|
-
mode: installMode
|
|
3576
|
-
});
|
|
3577
|
-
results.push({
|
|
3578
|
-
skill: skill.installName,
|
|
3579
|
-
agent: agents[agent].displayName,
|
|
3580
|
-
...result
|
|
3581
|
-
});
|
|
3582
|
-
}
|
|
3583
|
-
spinner.stop("Installation complete");
|
|
3584
|
-
console.log();
|
|
3585
|
-
const successful = results.filter((r) => r.success);
|
|
3586
|
-
const failed = results.filter((r) => !r.success);
|
|
3587
|
-
const skillFiles = {};
|
|
3588
|
-
for (const skill of selectedSkills) skillFiles[skill.installName] = skill.sourceUrl;
|
|
3589
|
-
if (await wellKnownPrivacyPromise !== true) track({
|
|
3590
|
-
event: "install",
|
|
3591
|
-
source: sourceIdentifier,
|
|
3592
|
-
skills: selectedSkills.map((s) => s.installName).join(","),
|
|
3593
|
-
agents: targetAgents.join(","),
|
|
3594
|
-
...installGlobally && { global: "1" },
|
|
3595
|
-
skillFiles: JSON.stringify(skillFiles),
|
|
3596
|
-
sourceType: "well-known"
|
|
3597
|
-
});
|
|
3598
|
-
if (successful.length > 0 && installGlobally) {
|
|
3599
|
-
const successfulSkillNames = new Set(successful.map((r) => r.skill));
|
|
3600
|
-
for (const skill of selectedSkills) if (successfulSkillNames.has(skill.installName)) try {
|
|
3601
|
-
await addSkillToLock(skill.installName, {
|
|
3602
|
-
source: sourceIdentifier,
|
|
3603
|
-
sourceType: "well-known",
|
|
3604
|
-
sourceUrl: skill.sourceUrl,
|
|
3605
|
-
skillFolderHash: ""
|
|
3606
|
-
});
|
|
3607
|
-
} catch {}
|
|
3608
|
-
}
|
|
3609
|
-
if (successful.length > 0 && !installGlobally) {
|
|
3610
|
-
const successfulSkillNames = new Set(successful.map((r) => r.skill));
|
|
3611
|
-
for (const skill of selectedSkills) if (successfulSkillNames.has(skill.installName)) try {
|
|
3612
|
-
const matchingResult = successful.find((r) => r.skill === skill.installName);
|
|
3613
|
-
const installDir = matchingResult?.canonicalPath || matchingResult?.path;
|
|
3614
|
-
if (installDir) {
|
|
3615
|
-
const computedHash = await computeSkillFolderHash(installDir);
|
|
3616
|
-
await addSkillToLocalLock(skill.installName, {
|
|
3617
|
-
source: sourceIdentifier,
|
|
3618
|
-
sourceType: "well-known",
|
|
3619
|
-
computedHash
|
|
3620
|
-
}, cwd);
|
|
3621
|
-
}
|
|
3622
|
-
} catch {}
|
|
3623
|
-
}
|
|
3624
|
-
if (successful.length > 0) {
|
|
3625
|
-
const bySkill = /* @__PURE__ */ new Map();
|
|
3626
|
-
for (const r of successful) {
|
|
3627
|
-
const skillResults = bySkill.get(r.skill) || [];
|
|
3628
|
-
skillResults.push(r);
|
|
3629
|
-
bySkill.set(r.skill, skillResults);
|
|
3630
|
-
}
|
|
3631
|
-
const skillCount = bySkill.size;
|
|
3632
|
-
const symlinkFailures = successful.filter((r) => r.mode === "symlink" && r.symlinkFailed);
|
|
3633
|
-
const copiedAgents = symlinkFailures.map((r) => r.agent);
|
|
3634
|
-
const resultLines = [];
|
|
3635
|
-
for (const [skillName, skillResults] of bySkill) {
|
|
3636
|
-
const firstResult = skillResults[0];
|
|
3637
|
-
if (firstResult.mode === "copy") {
|
|
3638
|
-
resultLines.push(`${import_picocolors.default.green("✓")} ${skillName} ${import_picocolors.default.dim("(copied)")}`);
|
|
3639
|
-
resultLines.push(...buildCopyResultLines(skillResults, cwd));
|
|
3640
|
-
} else {
|
|
3641
|
-
if (firstResult.canonicalPath) {
|
|
3642
|
-
const shortPath = shortenPath$2(firstResult.canonicalPath, cwd);
|
|
3643
|
-
resultLines.push(`${import_picocolors.default.green("✓")} ${shortPath}`);
|
|
3644
|
-
} else resultLines.push(`${import_picocolors.default.green("✓")} ${skillName}`);
|
|
3645
|
-
resultLines.push(...buildResultLines(skillResults, targetAgents));
|
|
3646
|
-
}
|
|
3647
|
-
}
|
|
3648
|
-
const title = import_picocolors.default.green(`Installed ${skillCount} skill${skillCount !== 1 ? "s" : ""}`);
|
|
3649
|
-
Me(resultLines.join("\n"), title);
|
|
3650
|
-
if (symlinkFailures.length > 0) {
|
|
3651
|
-
M.warn(import_picocolors.default.yellow(`Symlinks failed for: ${formatList$1(copiedAgents)}`));
|
|
3652
|
-
M.message(import_picocolors.default.dim(" Files were copied instead. On Windows, enable Developer Mode for symlink support."));
|
|
3653
|
-
}
|
|
3654
|
-
}
|
|
3655
|
-
if (failed.length > 0) {
|
|
3656
|
-
console.log();
|
|
3657
|
-
M.error(import_picocolors.default.red(`Failed to install ${failed.length}`));
|
|
3658
|
-
for (const r of failed) M.message(` ${import_picocolors.default.red("✗")} ${r.skill} → ${r.agent}: ${import_picocolors.default.dim(r.error)}`);
|
|
3659
|
-
}
|
|
3660
|
-
console.log();
|
|
3661
|
-
Se(import_picocolors.default.green("Done!") + import_picocolors.default.dim(" Review skills before use; they run with full agent permissions."));
|
|
3662
|
-
await promptForFindSkills(options, targetAgents);
|
|
3663
|
-
}
|
|
3664
|
-
async function runAdd(args, options = {}) {
|
|
3665
|
-
const source = args[0];
|
|
3666
|
-
let installTipShown = false;
|
|
3667
|
-
const showInstallTip = () => {
|
|
3668
|
-
if (installTipShown) return;
|
|
3669
|
-
M.message(import_picocolors.default.dim("Tip: use the --yes (-y) and --global (-g) flags to install without prompts."));
|
|
3670
|
-
installTipShown = true;
|
|
3671
|
-
};
|
|
3672
|
-
if (!source) {
|
|
3673
|
-
console.log();
|
|
3674
|
-
console.log(import_picocolors.default.bgRed(import_picocolors.default.white(import_picocolors.default.bold(" ERROR "))) + " " + import_picocolors.default.red("Missing required argument: source"));
|
|
3675
|
-
console.log();
|
|
3676
|
-
console.log(import_picocolors.default.dim(" Usage:"));
|
|
3677
|
-
console.log(` ${import_picocolors.default.cyan("npx wittyhub add")} ${import_picocolors.default.yellow("<source>")} ${import_picocolors.default.dim("[options]")}`);
|
|
3678
|
-
console.log();
|
|
3679
|
-
console.log(import_picocolors.default.dim(" Example:"));
|
|
3680
|
-
console.log(` ${import_picocolors.default.cyan("npx wittyhub add")} ${import_picocolors.default.yellow("vercel-labs/agent-skills")}`);
|
|
3681
|
-
console.log();
|
|
3682
|
-
process.exit(1);
|
|
3683
|
-
}
|
|
3684
|
-
if (options.all) {
|
|
3685
|
-
options.skill = ["*"];
|
|
3686
|
-
options.agent = ["*"];
|
|
3687
|
-
options.yes = true;
|
|
3688
|
-
}
|
|
3689
|
-
const agentResult = await detectAgent();
|
|
3690
|
-
if (agentResult.isAgent) {
|
|
3691
|
-
options.yes = true;
|
|
3692
|
-
if (!options.agent || options.agent.length === 0) {
|
|
3693
|
-
const mappedAgent = getAgentType(agentResult.agent.name);
|
|
3694
|
-
if (mappedAgent) options.agent = ensureUniversalAgents([mappedAgent]);
|
|
3695
|
-
}
|
|
3696
|
-
}
|
|
3697
|
-
console.log();
|
|
3698
|
-
if (!agentResult.isAgent) Ie(import_picocolors.default.bgCyan(import_picocolors.default.black(" skills ")));
|
|
3699
|
-
if (agentResult.isAgent) M.info(import_picocolors.default.bgCyan(import_picocolors.default.black(import_picocolors.default.bold(` ${agentResult.agent.name} `))) + " Agent detected — installing non-interactively");
|
|
3700
|
-
else if (!process.stdin.isTTY) showInstallTip();
|
|
3701
|
-
let tempDir = null;
|
|
3702
|
-
try {
|
|
3703
|
-
const spinner = Y();
|
|
3704
|
-
spinner.start("Parsing source...");
|
|
3705
|
-
const parsed = parseSource(source);
|
|
3706
|
-
spinner.stop(`Source: ${parsed.type === "local" ? parsed.localPath : parsed.url}${parsed.ref ? ` @ ${import_picocolors.default.yellow(parsed.ref)}` : ""}${parsed.subpath ? ` (${parsed.subpath})` : ""}${parsed.skillFilter ? ` ${import_picocolors.default.dim("@")}${import_picocolors.default.cyan(parsed.skillFilter)}` : ""}`);
|
|
3707
|
-
const ownerRepoRaw = getOwnerRepo(parsed);
|
|
3708
|
-
const repoPrivacyPromise = (() => {
|
|
3709
|
-
if (!ownerRepoRaw || parsed.type !== "github") return Promise.resolve(null);
|
|
3710
|
-
const ownerRepo = parseOwnerRepo(ownerRepoRaw);
|
|
3711
|
-
if (!ownerRepo) return Promise.resolve(null);
|
|
3712
|
-
return isRepoPrivate(ownerRepo.owner, ownerRepo.repo).catch(() => null);
|
|
3713
|
-
})();
|
|
3714
|
-
if (ownerRepoRaw?.split("/")[0]?.toLowerCase() === "openclaw" && !options.dangerouslyAcceptOpenclawRisks) {
|
|
3715
|
-
console.log();
|
|
3716
|
-
M.warn(import_picocolors.default.yellow(import_picocolors.default.bold("⚠ OpenClaw skills are unverified community submissions.")));
|
|
3717
|
-
M.message(import_picocolors.default.yellow("This source contains user-submitted skills that have not been reviewed for safety or quality."));
|
|
3718
|
-
M.message(import_picocolors.default.yellow("Skills run with full agent permissions and could be malicious."));
|
|
3719
|
-
console.log();
|
|
3720
|
-
M.message(`If you understand the risks, re-run with:\n\n ${import_picocolors.default.cyan(`npx wittyhub add ${source} --dangerously-accept-openclaw-risks`)}\n`);
|
|
3721
|
-
Se(import_picocolors.default.red("Installation blocked"));
|
|
3722
|
-
process.exit(1);
|
|
3723
|
-
}
|
|
3724
|
-
if (parsed.type === "well-known") {
|
|
3725
|
-
await handleWellKnownSkills(source, parsed.url, options, spinner);
|
|
3726
|
-
return;
|
|
3727
|
-
}
|
|
3728
|
-
if (parsed.skillFilter) {
|
|
3729
|
-
options.skill = options.skill || [];
|
|
3730
|
-
if (!options.skill.includes(parsed.skillFilter)) options.skill.push(parsed.skillFilter);
|
|
3731
|
-
}
|
|
3732
|
-
const includeInternal = !!(options.skill && options.skill.length > 0);
|
|
3733
|
-
let skills;
|
|
3734
|
-
let blobResult = null;
|
|
3735
|
-
if (parsed.type === "local") {
|
|
3736
|
-
spinner.start("Validating local path...");
|
|
3737
|
-
if (!existsSync(parsed.localPath)) {
|
|
3738
|
-
spinner.stop(import_picocolors.default.red("Path not found"));
|
|
3739
|
-
Se(import_picocolors.default.red(`Local path does not exist: ${parsed.localPath}`));
|
|
3740
|
-
process.exit(1);
|
|
3741
|
-
}
|
|
3742
|
-
spinner.stop("Local path validated");
|
|
3743
|
-
spinner.start("Discovering skills...");
|
|
3744
|
-
skills = await discoverSkills(parsed.localPath, parsed.subpath, {
|
|
3745
|
-
includeInternal,
|
|
3746
|
-
fullDepth: options.fullDepth
|
|
3747
|
-
});
|
|
3748
|
-
} else if (parsed.type === "github" && !options.fullDepth) {
|
|
3749
|
-
const BLOB_ALLOWED_OWNERS = [
|
|
3750
|
-
"vercel",
|
|
3751
|
-
"vercel-labs",
|
|
3752
|
-
"heygen-com"
|
|
3753
|
-
];
|
|
3754
|
-
const ownerRepo = getOwnerRepo(parsed);
|
|
3755
|
-
const owner = ownerRepo?.split("/")[0]?.toLowerCase();
|
|
3756
|
-
const isSelfHostedRepo = !!ownerRepo && Object.hasOwn(BLOB_ALLOWED_REPOS, ownerRepo.toLowerCase());
|
|
3757
|
-
if (ownerRepo && owner && (isSelfHostedRepo || BLOB_ALLOWED_OWNERS.includes(owner))) {
|
|
3758
|
-
spinner.start("Fetching skills...");
|
|
3759
|
-
blobResult = await tryBlobInstall(ownerRepo, {
|
|
3760
|
-
subpath: parsed.subpath,
|
|
3761
|
-
skillFilter: parsed.skillFilter,
|
|
3762
|
-
ref: parsed.ref,
|
|
3763
|
-
getToken: getGitHubToken,
|
|
3764
|
-
includeInternal
|
|
3765
|
-
});
|
|
3766
|
-
if (!blobResult) spinner.stop(import_picocolors.default.dim("Falling back to clone..."));
|
|
3767
|
-
}
|
|
3768
|
-
if (blobResult) {
|
|
3769
|
-
skills = blobResult.skills;
|
|
3770
|
-
spinner.stop(`Found ${import_picocolors.default.green(skills.length)} skill${skills.length > 1 ? "s" : ""}`);
|
|
3771
|
-
} else {
|
|
3772
|
-
spinner.start("Cloning repository...");
|
|
3773
|
-
tempDir = await cloneRepo(parsed.url, parsed.ref);
|
|
3774
|
-
spinner.stop("Repository cloned");
|
|
3775
|
-
spinner.start("Discovering skills...");
|
|
3776
|
-
skills = await discoverSkills(tempDir, parsed.subpath, {
|
|
3777
|
-
includeInternal,
|
|
3778
|
-
fullDepth: options.fullDepth
|
|
3779
|
-
});
|
|
3780
|
-
}
|
|
3781
|
-
} else {
|
|
3782
|
-
spinner.start("Cloning repository...");
|
|
3783
|
-
tempDir = await cloneRepo(parsed.url, parsed.ref);
|
|
3784
|
-
spinner.stop("Repository cloned");
|
|
3785
|
-
spinner.start("Discovering skills...");
|
|
3786
|
-
skills = await discoverSkills(tempDir, parsed.subpath, {
|
|
3787
|
-
includeInternal,
|
|
3788
|
-
fullDepth: options.fullDepth
|
|
3789
|
-
});
|
|
3790
|
-
}
|
|
3791
|
-
if (skills.length === 0) {
|
|
3792
|
-
spinner.stop(import_picocolors.default.red("No skills found"));
|
|
3793
|
-
Se(import_picocolors.default.red("No valid skills found. Skills require a SKILL.md with name and description."));
|
|
3794
|
-
await cleanup(tempDir);
|
|
3795
|
-
process.exit(1);
|
|
3796
|
-
}
|
|
3797
|
-
if (!blobResult) spinner.stop(`Found ${import_picocolors.default.green(skills.length)} skill${skills.length > 1 ? "s" : ""}`);
|
|
3522
|
+
spinner.stop(`Found ${import_picocolors.default.green(skills.length)} skill${skills.length > 1 ? "s" : ""}`);
|
|
3798
3523
|
if (options.list) {
|
|
3799
3524
|
console.log();
|
|
3800
3525
|
M.step(import_picocolors.default.bold("Available Skills"));
|
|
@@ -3892,7 +3617,8 @@ async function runAdd(args, options = {}) {
|
|
|
3892
3617
|
selectedSkills = selected;
|
|
3893
3618
|
}
|
|
3894
3619
|
const ownerRepoForAudit = getOwnerRepo(parsed);
|
|
3895
|
-
const
|
|
3620
|
+
const auditSkillFiles = buildSkillFiles(selectedSkills, tempDir, blobResult);
|
|
3621
|
+
const auditPromise = ownerRepoForAudit ? fetchAuditData(ownerRepoForAudit, selectedSkills.map((s) => s.name), parsed.type, auditSkillFiles) : Promise.resolve(null);
|
|
3896
3622
|
let targetAgents;
|
|
3897
3623
|
const validAgents = Object.keys(agents);
|
|
3898
3624
|
if (options.agent?.includes("*")) {
|
|
@@ -4093,18 +3819,28 @@ async function runAdd(args, options = {}) {
|
|
|
4093
3819
|
}
|
|
4094
3820
|
console.log();
|
|
4095
3821
|
Me(summaryLines.join("\n"), "Installation Summary");
|
|
3822
|
+
const auditSkills = selectedSkills.map((s) => ({
|
|
3823
|
+
slug: getSkillDisplayName(s),
|
|
3824
|
+
displayName: getSkillDisplayName(s)
|
|
3825
|
+
}));
|
|
3826
|
+
let auditData = null;
|
|
4096
3827
|
try {
|
|
4097
|
-
|
|
3828
|
+
auditData = await auditPromise;
|
|
4098
3829
|
if (auditData && ownerRepoForAudit) {
|
|
4099
|
-
const securityLines = buildSecurityLines(auditData,
|
|
4100
|
-
slug: getSkillDisplayName(s),
|
|
4101
|
-
displayName: getSkillDisplayName(s)
|
|
4102
|
-
})), ownerRepoForAudit);
|
|
3830
|
+
const securityLines = buildSecurityLines(auditData, auditSkills);
|
|
4103
3831
|
if (securityLines.length > 0) Me(securityLines.join("\n"), "Security Risk Assessments");
|
|
4104
3832
|
}
|
|
4105
3833
|
} catch {}
|
|
3834
|
+
const highRiskSkills = getHighRiskSkills(auditData, auditSkills);
|
|
3835
|
+
if (highRiskSkills.length > 0) {
|
|
3836
|
+
const list = highRiskSkills.map((s) => `${s.name} 经安全审计标记为 ${s.riskLevel} 风险`).join(",");
|
|
3837
|
+
M.warn(`${import_picocolors.default.red(import_picocolors.default.bold("⚠ 高风险警告:"))} ${list},请谨慎安装。`);
|
|
3838
|
+
}
|
|
4106
3839
|
if (!options.yes) {
|
|
4107
|
-
const confirmed = await ye({
|
|
3840
|
+
const confirmed = await ye({
|
|
3841
|
+
message: highRiskSkills.length > 0 ? `检测到以下技能存在安全风险:${highRiskSkills.map((s) => `${s.name} (${s.riskLevel})`).join(", ")}。仍要继续安装?` : "Proceed with installation?",
|
|
3842
|
+
initialValue: highRiskSkills.length === 0
|
|
3843
|
+
});
|
|
4108
3844
|
if (pD(confirmed) || !confirmed) {
|
|
4109
3845
|
xe("Installation cancelled");
|
|
4110
3846
|
await cleanup(tempDir);
|
|
@@ -4116,17 +3852,7 @@ async function runAdd(args, options = {}) {
|
|
|
4116
3852
|
for (const skill of selectedSkills) for (const target of installTargets) {
|
|
4117
3853
|
const { agent, subagent } = target;
|
|
4118
3854
|
let result;
|
|
4119
|
-
if (
|
|
4120
|
-
const blobSkill = skill;
|
|
4121
|
-
result = await installBlobSkillForAgent({
|
|
4122
|
-
installName: blobSkill.name,
|
|
4123
|
-
files: blobSkill.files
|
|
4124
|
-
}, agent, {
|
|
4125
|
-
global: installGlobally,
|
|
4126
|
-
mode: installMode,
|
|
4127
|
-
eveSubagent: subagent
|
|
4128
|
-
});
|
|
4129
|
-
} else if (tempDir && skill.path === tempDir && skill.rawContent) result = await installBlobSkillForAgent({
|
|
3855
|
+
if (tempDir && skill.path === tempDir && skill.rawContent) result = await installBlobSkillForAgent({
|
|
4130
3856
|
installName: skill.name,
|
|
4131
3857
|
files: [{
|
|
4132
3858
|
path: "SKILL.md",
|
|
@@ -4152,16 +3878,37 @@ async function runAdd(args, options = {}) {
|
|
|
4152
3878
|
console.log();
|
|
4153
3879
|
const successful = results.filter((r) => r.success);
|
|
4154
3880
|
const failed = results.filter((r) => !r.success);
|
|
4155
|
-
const skillFiles =
|
|
4156
|
-
for (const skill of selectedSkills) if (blobResult && "repoPath" in skill) skillFiles[skill.name] = skill.repoPath;
|
|
4157
|
-
else if (tempDir && skill.path === tempDir) skillFiles[skill.name] = "SKILL.md";
|
|
4158
|
-
else if (tempDir && skill.path.startsWith(tempDir + sep)) skillFiles[skill.name] = skill.path.slice(tempDir.length + 1).split(sep).join("/") + "/SKILL.md";
|
|
4159
|
-
else continue;
|
|
3881
|
+
const skillFiles = buildSkillFiles(selectedSkills, tempDir, blobResult);
|
|
4160
3882
|
const normalizedSource = getOwnerRepo(parsed);
|
|
4161
3883
|
const lockSource = getLockSource(parsed.url, normalizedSource);
|
|
4162
3884
|
if (normalizedSource) if (parseOwnerRepo(normalizedSource)) {
|
|
4163
3885
|
const isPrivate = await repoPrivacyPromise;
|
|
4164
|
-
if (isPrivate === false || isPrivate === null && parsed.type !== "github")
|
|
3886
|
+
if (isPrivate === false || isPrivate === null && parsed.type !== "github") {
|
|
3887
|
+
console.log("[telemetry-debug-1]", JSON.stringify({
|
|
3888
|
+
isPrivate,
|
|
3889
|
+
parsedType: parsed.type,
|
|
3890
|
+
normalizedSource,
|
|
3891
|
+
sourceType: parsed.type,
|
|
3892
|
+
skillFiles: JSON.stringify(skillFiles)
|
|
3893
|
+
}));
|
|
3894
|
+
track({
|
|
3895
|
+
event: "install",
|
|
3896
|
+
source: normalizedSource,
|
|
3897
|
+
skills: selectedSkills.map((s) => s.name).join(","),
|
|
3898
|
+
agents: targetAgents.join(","),
|
|
3899
|
+
...installGlobally && { global: "1" },
|
|
3900
|
+
skillFiles: JSON.stringify(skillFiles),
|
|
3901
|
+
sourceType: parsed.type
|
|
3902
|
+
});
|
|
3903
|
+
}
|
|
3904
|
+
} else {
|
|
3905
|
+
console.log("[telemetry-debug-2]", JSON.stringify({
|
|
3906
|
+
parsedType: parsed.type,
|
|
3907
|
+
normalizedSource,
|
|
3908
|
+
sourceType: parsed.type,
|
|
3909
|
+
skillFiles: JSON.stringify(skillFiles)
|
|
3910
|
+
}));
|
|
3911
|
+
track({
|
|
4165
3912
|
event: "install",
|
|
4166
3913
|
source: normalizedSource,
|
|
4167
3914
|
skills: selectedSkills.map((s) => s.name).join(","),
|
|
@@ -4170,32 +3917,21 @@ async function runAdd(args, options = {}) {
|
|
|
4170
3917
|
skillFiles: JSON.stringify(skillFiles),
|
|
4171
3918
|
sourceType: parsed.type
|
|
4172
3919
|
});
|
|
4173
|
-
}
|
|
4174
|
-
event: "install",
|
|
4175
|
-
source: normalizedSource,
|
|
4176
|
-
skills: selectedSkills.map((s) => s.name).join(","),
|
|
4177
|
-
agents: targetAgents.join(","),
|
|
4178
|
-
...installGlobally && { global: "1" },
|
|
4179
|
-
skillFiles: JSON.stringify(skillFiles),
|
|
4180
|
-
sourceType: parsed.type
|
|
4181
|
-
});
|
|
3920
|
+
}
|
|
4182
3921
|
if (successful.length > 0 && installGlobally && normalizedSource) {
|
|
4183
3922
|
const successfulSkillNames = new Set(successful.map((r) => r.skill));
|
|
4184
3923
|
let cachedTree;
|
|
4185
|
-
if (parsed.type === "github" &&
|
|
3924
|
+
if (parsed.type === "github" && true) cachedTree = await fetchRepoTree(normalizedSource, parsed.ref, getGitHubToken);
|
|
4186
3925
|
for (const skill of selectedSkills) {
|
|
4187
3926
|
const skillDisplayName = getSkillDisplayName(skill);
|
|
4188
3927
|
if (successfulSkillNames.has(skillDisplayName)) try {
|
|
4189
3928
|
let skillFolderHash = "";
|
|
4190
3929
|
const skillPathValue = skillFiles[skill.name];
|
|
4191
|
-
if (
|
|
4192
|
-
const hash = getSkillFolderHashFromTree(blobResult.tree, skillPathValue);
|
|
4193
|
-
if (hash) skillFolderHash = hash;
|
|
4194
|
-
} else if (parsed.type === "github" && skillPathValue && cachedTree) {
|
|
3930
|
+
if (parsed.type === "github" && skillPathValue && cachedTree) {
|
|
4195
3931
|
const hash = getSkillFolderHashFromTree(cachedTree, skillPathValue);
|
|
4196
3932
|
if (hash) skillFolderHash = hash;
|
|
4197
3933
|
} else if (skillPathValue && tempDir) {
|
|
4198
|
-
const hash = await computeSkillFolderHash(join(tempDir, dirname(skillPathValue)));
|
|
3934
|
+
const hash = await computeSkillFolderHash(join$1(tempDir, dirname$1(skillPathValue)));
|
|
4199
3935
|
if (hash) skillFolderHash = hash;
|
|
4200
3936
|
}
|
|
4201
3937
|
await addSkillToLock(skill.name, {
|
|
@@ -4217,7 +3953,7 @@ async function runAdd(args, options = {}) {
|
|
|
4217
3953
|
for (const skill of selectedSkills) {
|
|
4218
3954
|
const skillDisplayName = getSkillDisplayName(skill);
|
|
4219
3955
|
if (successfulSkillNames.has(skillDisplayName)) try {
|
|
4220
|
-
const computedHash =
|
|
3956
|
+
const computedHash = tempDir && skill.path === tempDir && skill.rawContent ? computeSingleFileSkillHash(skill.rawContent) : await computeSkillFolderHash(skill.path);
|
|
4221
3957
|
const skillPathValue = skillFiles[skill.name];
|
|
4222
3958
|
await addSkillToLocalLock(skill.name, {
|
|
4223
3959
|
source: lockSource || parsed.url,
|
|
@@ -4398,18 +4134,300 @@ function parseAddOptions(args) {
|
|
|
4398
4134
|
options
|
|
4399
4135
|
};
|
|
4400
4136
|
}
|
|
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
|
+
function parseAuditOptions(args) {
|
|
4249
|
+
let source = "";
|
|
4250
|
+
let skill = "";
|
|
4251
|
+
const errors = [];
|
|
4252
|
+
for (let i = 0; i < args.length; i++) {
|
|
4253
|
+
const arg = args[i];
|
|
4254
|
+
if (arg === "-s" || arg === "--skill") {
|
|
4255
|
+
i++;
|
|
4256
|
+
skill = args[i]?.trim() ?? "";
|
|
4257
|
+
} else if (arg && !arg.startsWith("-")) source = arg.trim();
|
|
4258
|
+
}
|
|
4259
|
+
if (!source) errors.push("Missing source or skill id");
|
|
4260
|
+
return {
|
|
4261
|
+
source,
|
|
4262
|
+
skill,
|
|
4263
|
+
errors
|
|
4264
|
+
};
|
|
4265
|
+
}
|
|
4266
|
+
async function fetchSkillAudit(skillId, timeoutMs = 15e3) {
|
|
4267
|
+
const controller = new AbortController();
|
|
4268
|
+
const timeout = setTimeout(() => controller.abort(), timeoutMs);
|
|
4269
|
+
try {
|
|
4270
|
+
const url = AUDIT_URL.replace("{skill_id}", skillId.split("/").map(encodeURIComponent).join("/"));
|
|
4271
|
+
let response;
|
|
4272
|
+
try {
|
|
4273
|
+
response = await fetch(url, { signal: controller.signal });
|
|
4274
|
+
} catch {
|
|
4275
|
+
return {
|
|
4276
|
+
status: "error",
|
|
4277
|
+
message: `无法连接审计服务: ${AUDIT_URL}`
|
|
4278
|
+
};
|
|
4279
|
+
}
|
|
4280
|
+
if (response.status === 404) return { status: "not_found" };
|
|
4281
|
+
if (!response.ok) return {
|
|
4282
|
+
status: "error",
|
|
4283
|
+
message: `审计服务返回 HTTP ${response.status}`
|
|
4284
|
+
};
|
|
4285
|
+
let data;
|
|
4286
|
+
try {
|
|
4287
|
+
data = await response.json();
|
|
4288
|
+
} catch {
|
|
4289
|
+
return {
|
|
4290
|
+
status: "error",
|
|
4291
|
+
message: "审计服务响应格式无效"
|
|
4292
|
+
};
|
|
4293
|
+
}
|
|
4294
|
+
if (data.error) return {
|
|
4295
|
+
status: "no_audit",
|
|
4296
|
+
message: String(data.error)
|
|
4297
|
+
};
|
|
4298
|
+
const auditData = data.data || data;
|
|
4299
|
+
return {
|
|
4300
|
+
status: "ok",
|
|
4301
|
+
data: {
|
|
4302
|
+
risk_level: auditData.risk_level ?? "unknown",
|
|
4303
|
+
risk_score: auditData.risk_score ?? null,
|
|
4304
|
+
risk_signals: auditData.risk_signals ?? [],
|
|
4305
|
+
audited_at: auditData.audited_at ?? null,
|
|
4306
|
+
audit_type: auditData.audit_type ?? null,
|
|
4307
|
+
details: auditData.details ?? null
|
|
4308
|
+
}
|
|
4309
|
+
};
|
|
4310
|
+
} finally {
|
|
4311
|
+
clearTimeout(timeout);
|
|
4312
|
+
}
|
|
4313
|
+
}
|
|
4314
|
+
function riskColor(level) {
|
|
4315
|
+
switch (level) {
|
|
4316
|
+
case "critical":
|
|
4317
|
+
case "high": return import_picocolors.default.red;
|
|
4318
|
+
case "medium": return import_picocolors.default.yellow;
|
|
4319
|
+
case "low":
|
|
4320
|
+
case "safe": return import_picocolors.default.green;
|
|
4321
|
+
default: return import_picocolors.default.dim;
|
|
4322
|
+
}
|
|
4323
|
+
}
|
|
4324
|
+
const RISK_LABELS = {
|
|
4325
|
+
critical: "Critical",
|
|
4326
|
+
high: "High",
|
|
4327
|
+
medium: "Medium",
|
|
4328
|
+
low: "Low",
|
|
4329
|
+
safe: "Safe"
|
|
4330
|
+
};
|
|
4331
|
+
function riskLabel(level) {
|
|
4332
|
+
const color = riskColor(level);
|
|
4333
|
+
const label = RISK_LABELS[level] || level || "Unknown";
|
|
4334
|
+
return level === "critical" ? color(import_picocolors.default.bold(label)) : color(label);
|
|
4335
|
+
}
|
|
4336
|
+
function severityColor(severity) {
|
|
4337
|
+
const s = severity.toLowerCase();
|
|
4338
|
+
if (s === "critical" || s === "high") return import_picocolors.default.red(severity);
|
|
4339
|
+
if (s === "medium") return import_picocolors.default.yellow(severity);
|
|
4340
|
+
if (s === "low") return import_picocolors.default.green(severity);
|
|
4341
|
+
return import_picocolors.default.dim(severity);
|
|
4342
|
+
}
|
|
4343
|
+
function buildAuditOutput(skillId, data) {
|
|
4344
|
+
const lines = [];
|
|
4345
|
+
lines.push(`${import_picocolors.default.bold("Skill:")} ${import_picocolors.default.cyan(skillId)}`);
|
|
4346
|
+
const color = riskColor(data.risk_level);
|
|
4347
|
+
const score = data.risk_score != null ? color(`(score ${data.risk_score})`) : color("(score --)");
|
|
4348
|
+
lines.push(`${import_picocolors.default.bold("Risk:")} ${riskLabel(data.risk_level)} ${score}`);
|
|
4349
|
+
if (data.audited_at) lines.push(`${import_picocolors.default.bold("Audited at:")} ${import_picocolors.default.dim(data.audited_at)}`);
|
|
4350
|
+
lines.push("");
|
|
4351
|
+
if (data.risk_signals.length === 0) {
|
|
4352
|
+
lines.push(import_picocolors.default.green("No risk signals detected."));
|
|
4353
|
+
return lines;
|
|
4354
|
+
}
|
|
4355
|
+
lines.push(import_picocolors.default.bold("Risk signals:"));
|
|
4356
|
+
for (const signal of data.risk_signals) {
|
|
4357
|
+
const severity = severityColor(signal.severity || "unknown");
|
|
4358
|
+
lines.push(` ${import_picocolors.default.bold(signal.name)} ${import_picocolors.default.dim(`[${severity}]`)}`);
|
|
4359
|
+
if (signal.description) lines.push(` ${import_picocolors.default.dim(signal.description)}`);
|
|
4360
|
+
}
|
|
4361
|
+
return lines;
|
|
4362
|
+
}
|
|
4363
|
+
async function runAudit(args) {
|
|
4364
|
+
const { source, skill, errors } = parseAuditOptions(args);
|
|
4365
|
+
if (errors.length > 0) {
|
|
4366
|
+
for (const error of errors) console.error(import_picocolors.default.red(error));
|
|
4367
|
+
console.error("Usage: wittyhub audit <source> --skill <skill>");
|
|
4368
|
+
console.error(" wittyhub audit <skill_id>");
|
|
4369
|
+
return;
|
|
4370
|
+
}
|
|
4371
|
+
const { skillId, derived } = await resolveSkillId(source, skill || void 0);
|
|
4372
|
+
if (!skillId) {
|
|
4373
|
+
console.error(import_picocolors.default.red(`Unable to resolve skill id from source: ${source}`));
|
|
4374
|
+
console.error("Usage: wittyhub audit <source> --skill <skill>");
|
|
4375
|
+
return;
|
|
4376
|
+
}
|
|
4377
|
+
if (derived) console.error(import_picocolors.default.yellow(`Could not locate skill in repo; trying derived id: ${skillId}`));
|
|
4378
|
+
const result = await fetchSkillAudit(skillId);
|
|
4379
|
+
track({
|
|
4380
|
+
event: "audit",
|
|
4381
|
+
skillId,
|
|
4382
|
+
found: result.status === "ok" ? "1" : "0"
|
|
4383
|
+
});
|
|
4384
|
+
switch (result.status) {
|
|
4385
|
+
case "ok":
|
|
4386
|
+
for (const line of buildAuditOutput(skillId, result.data)) console.log(line);
|
|
4387
|
+
break;
|
|
4388
|
+
case "no_audit":
|
|
4389
|
+
console.log(`${import_picocolors.default.yellow("No audit result yet.")} ${import_picocolors.default.dim(result.message)}`);
|
|
4390
|
+
break;
|
|
4391
|
+
case "not_found":
|
|
4392
|
+
console.error(import_picocolors.default.red(`Skill not found: ${skillId}`));
|
|
4393
|
+
break;
|
|
4394
|
+
case "error":
|
|
4395
|
+
console.error(import_picocolors.default.red(result.message));
|
|
4396
|
+
break;
|
|
4397
|
+
}
|
|
4398
|
+
}
|
|
4401
4399
|
const RESET$3 = "\x1B[0m";
|
|
4402
4400
|
const BOLD$3 = "\x1B[1m";
|
|
4403
4401
|
const DIM$3 = "\x1B[38;5;102m";
|
|
4404
4402
|
const TEXT$2 = "\x1B[38;5;145m";
|
|
4405
4403
|
const CYAN$1 = "\x1B[36m";
|
|
4406
|
-
const
|
|
4407
|
-
|
|
4408
|
-
|
|
4404
|
+
const MAGENTA = "\x1B[35m";
|
|
4405
|
+
const YELLOW$1 = "\x1B[33m";
|
|
4406
|
+
const RED = "\x1B[31m";
|
|
4407
|
+
const GREEN = "\x1B[32m";
|
|
4408
|
+
function formatInstalls$1(count) {
|
|
4409
|
+
if (count === void 0 || count === null || Number.isNaN(count)) return "";
|
|
4410
|
+
if (count <= 0) return "0 installs";
|
|
4409
4411
|
if (count >= 1e6) return `${(count / 1e6).toFixed(1).replace(/\.0$/, "")}M installs`;
|
|
4410
4412
|
if (count >= 1e3) return `${(count / 1e3).toFixed(1).replace(/\.0$/, "")}K installs`;
|
|
4411
4413
|
return `${count} install${count === 1 ? "" : "s"}`;
|
|
4412
4414
|
}
|
|
4415
|
+
function formatRiskLevel$1(riskScore) {
|
|
4416
|
+
if (riskScore === void 0 || riskScore === null) return "";
|
|
4417
|
+
if (riskScore <= 20) return `${GREEN}safe risk${RESET$3}`;
|
|
4418
|
+
if (riskScore <= 50) return `${GREEN}low risk${RESET$3}`;
|
|
4419
|
+
if (riskScore <= 80) return `${YELLOW$1}medium risk${RESET$3}`;
|
|
4420
|
+
return `${RED}high risk${RESET$3}`;
|
|
4421
|
+
}
|
|
4422
|
+
function escapeRegExp(s) {
|
|
4423
|
+
return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
4424
|
+
}
|
|
4425
|
+
function highlightMatches(text, query) {
|
|
4426
|
+
if (!text || !query) return text;
|
|
4427
|
+
const terms = query.trim().split(/\s+/).filter(Boolean).map(escapeRegExp);
|
|
4428
|
+
if (terms.length === 0) return text;
|
|
4429
|
+
return text.replace(new RegExp(`(${terms.join("|")})`, "gi"), `${MAGENTA}$1${RESET$3}`);
|
|
4430
|
+
}
|
|
4413
4431
|
const GITHUB_OWNER_PATTERN = /^[a-z0-9](?:[a-z0-9-]{0,38})$/i;
|
|
4414
4432
|
function parseFindOptions(args) {
|
|
4415
4433
|
const queryParts = [];
|
|
@@ -4422,7 +4440,7 @@ function parseFindOptions(args) {
|
|
|
4422
4440
|
if (arg === "--owner") {
|
|
4423
4441
|
const value = args[i + 1];
|
|
4424
4442
|
if (!value || value.startsWith("-")) {
|
|
4425
|
-
errors.push("--owner requires
|
|
4443
|
+
errors.push("--owner requires an owner");
|
|
4426
4444
|
continue;
|
|
4427
4445
|
}
|
|
4428
4446
|
ownerValue = value;
|
|
@@ -4430,7 +4448,7 @@ function parseFindOptions(args) {
|
|
|
4430
4448
|
} else if (arg.startsWith("--owner=")) {
|
|
4431
4449
|
ownerValue = arg.slice(8);
|
|
4432
4450
|
if (!ownerValue) {
|
|
4433
|
-
errors.push("--owner requires
|
|
4451
|
+
errors.push("--owner requires an owner");
|
|
4434
4452
|
continue;
|
|
4435
4453
|
}
|
|
4436
4454
|
} else {
|
|
@@ -4439,7 +4457,7 @@ function parseFindOptions(args) {
|
|
|
4439
4457
|
}
|
|
4440
4458
|
const owner = ownerValue.trim().toLowerCase();
|
|
4441
4459
|
if (!GITHUB_OWNER_PATTERN.test(owner)) {
|
|
4442
|
-
errors.push("--owner must be a valid
|
|
4460
|
+
errors.push("--owner must be a valid owner");
|
|
4443
4461
|
continue;
|
|
4444
4462
|
}
|
|
4445
4463
|
options.owner = owner;
|
|
@@ -4451,24 +4469,52 @@ function parseFindOptions(args) {
|
|
|
4451
4469
|
};
|
|
4452
4470
|
}
|
|
4453
4471
|
async function searchSkillsAPI(query, owner) {
|
|
4472
|
+
const url = `${SEARCH_URL}?${new URLSearchParams({
|
|
4473
|
+
q: query,
|
|
4474
|
+
limit: "10",
|
|
4475
|
+
mode: "text"
|
|
4476
|
+
}).toString()}`;
|
|
4477
|
+
let res;
|
|
4454
4478
|
try {
|
|
4455
|
-
|
|
4456
|
-
q: query,
|
|
4457
|
-
limit: "10"
|
|
4458
|
-
});
|
|
4459
|
-
if (owner) params.set("owner", owner);
|
|
4460
|
-
const url = `${SEARCH_API_BASE}/api/search?${params.toString()}`;
|
|
4461
|
-
const res = await fetch(url);
|
|
4462
|
-
if (!res.ok) return [];
|
|
4463
|
-
return (await res.json()).skills.map((skill) => ({
|
|
4464
|
-
name: sanitizeMetadata(skill.name),
|
|
4465
|
-
slug: sanitizeMetadata(skill.id),
|
|
4466
|
-
source: sanitizeMetadata(skill.source || ""),
|
|
4467
|
-
installs: skill.installs
|
|
4468
|
-
})).sort((a, b) => (b.installs || 0) - (a.installs || 0));
|
|
4479
|
+
res = await fetch(url);
|
|
4469
4480
|
} catch {
|
|
4470
|
-
return
|
|
4481
|
+
return {
|
|
4482
|
+
skills: [],
|
|
4483
|
+
error: `无法连接搜索服务: ${SEARCH_URL}`
|
|
4484
|
+
};
|
|
4485
|
+
}
|
|
4486
|
+
if (!res.ok) return {
|
|
4487
|
+
skills: [],
|
|
4488
|
+
error: `搜索服务返回 HTTP ${res.status}`
|
|
4489
|
+
};
|
|
4490
|
+
let data;
|
|
4491
|
+
try {
|
|
4492
|
+
data = await res.json();
|
|
4493
|
+
} catch {
|
|
4494
|
+
return {
|
|
4495
|
+
skills: [],
|
|
4496
|
+
error: "搜索服务响应格式无效"
|
|
4497
|
+
};
|
|
4498
|
+
}
|
|
4499
|
+
let skills = (data.data?.results ?? data.results ?? []).map((skill) => ({
|
|
4500
|
+
name: sanitizeMetadata(skill.name),
|
|
4501
|
+
slug: sanitizeMetadata(skill.skill_id),
|
|
4502
|
+
source: sanitizeMetadata(skill.source || ""),
|
|
4503
|
+
installs: skill.download_count || 0,
|
|
4504
|
+
description: skill.description ? sanitizeMetadata(skill.description) : void 0,
|
|
4505
|
+
riskScore: skill.risk_score ?? void 0,
|
|
4506
|
+
sourceUrl: skill.source_url ? sanitizeMetadata(skill.source_url) : void 0
|
|
4507
|
+
}));
|
|
4508
|
+
if (owner) {
|
|
4509
|
+
const ownerLower = owner.toLowerCase();
|
|
4510
|
+
skills = skills.filter((skill) => {
|
|
4511
|
+
if (skill.sourceUrl) try {
|
|
4512
|
+
if (new URL(skill.sourceUrl).pathname.split("/").filter(Boolean)[0]?.toLowerCase() === ownerLower) return true;
|
|
4513
|
+
} catch {}
|
|
4514
|
+
return skill.slug.toLowerCase().includes(`/${ownerLower}/`);
|
|
4515
|
+
});
|
|
4471
4516
|
}
|
|
4517
|
+
return { skills: skills.sort((a, b) => (b.installs || 0) - (a.installs || 0)) };
|
|
4472
4518
|
}
|
|
4473
4519
|
const HIDE_CURSOR = "\x1B[?25l";
|
|
4474
4520
|
const SHOW_CURSOR = "\x1B[?25h";
|
|
@@ -4480,6 +4526,7 @@ async function runSearchPrompt(initialQuery = "", owner) {
|
|
|
4480
4526
|
let selectedIndex = 0;
|
|
4481
4527
|
let query = initialQuery;
|
|
4482
4528
|
let loading = false;
|
|
4529
|
+
let searchError = null;
|
|
4483
4530
|
let debounceTimer = null;
|
|
4484
4531
|
let lastRenderedLines = 0;
|
|
4485
4532
|
if (process.stdin.isTTY) process.stdin.setRawMode(true);
|
|
@@ -4495,6 +4542,7 @@ async function runSearchPrompt(initialQuery = "", owner) {
|
|
|
4495
4542
|
lines.push("");
|
|
4496
4543
|
if (!query || query.length < 2) lines.push(`${DIM$3}Start typing to search (min 2 chars)${RESET$3}`);
|
|
4497
4544
|
else if (results.length === 0 && loading) lines.push(`${DIM$3}Searching...${RESET$3}`);
|
|
4545
|
+
else if (results.length === 0 && searchError) lines.push(`${RED}${searchError}${RESET$3}`);
|
|
4498
4546
|
else if (results.length === 0) lines.push(`${DIM$3}No skills found${RESET$3}`);
|
|
4499
4547
|
else {
|
|
4500
4548
|
const visible = results.slice(0, 8);
|
|
@@ -4502,12 +4550,23 @@ async function runSearchPrompt(initialQuery = "", owner) {
|
|
|
4502
4550
|
const skill = visible[i];
|
|
4503
4551
|
const isSelected = i === selectedIndex;
|
|
4504
4552
|
const arrow = isSelected ? `${BOLD$3}>${RESET$3}` : " ";
|
|
4505
|
-
const
|
|
4553
|
+
const hlName = highlightMatches(skill.name, query);
|
|
4554
|
+
const name = isSelected ? `${BOLD$3}${hlName}${RESET$3}` : `${TEXT$2}${hlName}${RESET$3}`;
|
|
4506
4555
|
const source = skill.source ? ` ${DIM$3}${skill.source}${RESET$3}` : "";
|
|
4507
|
-
const installs = formatInstalls(skill.installs);
|
|
4556
|
+
const installs = formatInstalls$1(skill.installs);
|
|
4508
4557
|
const installsBadge = installs ? ` ${CYAN$1}${installs}${RESET$3}` : "";
|
|
4558
|
+
const risk = formatRiskLevel$1(skill.riskScore);
|
|
4559
|
+
const riskBadge = risk ? ` ${risk}` : "";
|
|
4509
4560
|
const loadingIndicator = loading && i === 0 ? ` ${DIM$3}...${RESET$3}` : "";
|
|
4510
|
-
lines.push(` ${arrow} ${name}${source}${
|
|
4561
|
+
lines.push(` ${arrow} ${name}${source}${loadingIndicator}`);
|
|
4562
|
+
if (installsBadge || riskBadge) {
|
|
4563
|
+
const meta = [installsBadge.trimStart(), riskBadge.trimStart()].filter(Boolean).join(" ");
|
|
4564
|
+
lines.push(` ${meta}`);
|
|
4565
|
+
}
|
|
4566
|
+
if (skill.description) {
|
|
4567
|
+
const descPlain = skill.description.length > 80 ? `${skill.description.slice(0, 77)}...` : skill.description;
|
|
4568
|
+
lines.push(` ${DIM$3}${highlightMatches(descPlain, query)}${RESET$3}`);
|
|
4569
|
+
}
|
|
4511
4570
|
}
|
|
4512
4571
|
}
|
|
4513
4572
|
lines.push("");
|
|
@@ -4523,6 +4582,7 @@ async function runSearchPrompt(initialQuery = "", owner) {
|
|
|
4523
4582
|
loading = false;
|
|
4524
4583
|
if (!q || q.length < 2) {
|
|
4525
4584
|
results = [];
|
|
4585
|
+
searchError = null;
|
|
4526
4586
|
selectedIndex = 0;
|
|
4527
4587
|
render();
|
|
4528
4588
|
return;
|
|
@@ -4532,10 +4592,13 @@ async function runSearchPrompt(initialQuery = "", owner) {
|
|
|
4532
4592
|
const debounceMs = Math.max(150, 350 - q.length * 50);
|
|
4533
4593
|
debounceTimer = setTimeout(async () => {
|
|
4534
4594
|
try {
|
|
4535
|
-
|
|
4595
|
+
const result = await searchSkillsAPI(q, owner);
|
|
4596
|
+
results = result.skills;
|
|
4597
|
+
searchError = result.error ?? null;
|
|
4536
4598
|
selectedIndex = 0;
|
|
4537
4599
|
} catch {
|
|
4538
4600
|
results = [];
|
|
4601
|
+
searchError = null;
|
|
4539
4602
|
} finally {
|
|
4540
4603
|
loading = false;
|
|
4541
4604
|
debounceTimer = null;
|
|
@@ -4594,10 +4657,26 @@ async function runSearchPrompt(initialQuery = "", owner) {
|
|
|
4594
4657
|
}
|
|
4595
4658
|
function getOwnerRepoFromString(pkg) {
|
|
4596
4659
|
const atIndex = pkg.lastIndexOf("@");
|
|
4597
|
-
const
|
|
4598
|
-
|
|
4599
|
-
|
|
4600
|
-
|
|
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]
|
|
4601
4680
|
};
|
|
4602
4681
|
return null;
|
|
4603
4682
|
}
|
|
@@ -4617,24 +4696,41 @@ ${DIM$3} 2) npx wittyhub add <owner/repo@skill>${RESET$3}`;
|
|
|
4617
4696
|
return;
|
|
4618
4697
|
}
|
|
4619
4698
|
if (query) {
|
|
4620
|
-
const results = await searchSkillsAPI(query, owner);
|
|
4699
|
+
const { skills: results, error: searchError } = await searchSkillsAPI(query, owner);
|
|
4621
4700
|
track({
|
|
4622
4701
|
event: "find",
|
|
4623
4702
|
query,
|
|
4624
4703
|
resultCount: String(results.length)
|
|
4625
4704
|
});
|
|
4705
|
+
if (searchError) {
|
|
4706
|
+
console.error(`${RED}${searchError}${RESET$3}`);
|
|
4707
|
+
return;
|
|
4708
|
+
}
|
|
4626
4709
|
if (results.length === 0) {
|
|
4627
4710
|
const ownerSuffix = owner ? ` from owner "${owner}"` : "";
|
|
4628
4711
|
console.log(`${DIM$3}No skills found for "${query}"${ownerSuffix}${RESET$3}`);
|
|
4629
4712
|
return;
|
|
4630
4713
|
}
|
|
4631
|
-
console.log(`${DIM$3}Install with${RESET$3} npx wittyhub add <
|
|
4714
|
+
console.log(`${DIM$3}Install with${RESET$3} npx wittyhub add <repo-url> --skill <skill>`);
|
|
4632
4715
|
console.log();
|
|
4633
4716
|
for (const skill of results.slice(0, 6)) {
|
|
4634
|
-
const
|
|
4635
|
-
const
|
|
4636
|
-
|
|
4637
|
-
|
|
4717
|
+
const raw = skill.sourceUrl || skill.source || skill.slug;
|
|
4718
|
+
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
|
+
const risk = formatRiskLevel$1(skill.riskScore);
|
|
4723
|
+
const riskBadge = risk ? ` ${risk}` : "";
|
|
4724
|
+
console.log(`${TEXT$2}${installTarget}${RESET$3}`);
|
|
4725
|
+
if (installsBadge || riskBadge) {
|
|
4726
|
+
const meta = [installsBadge.trimStart(), riskBadge.trimStart()].filter(Boolean).join(" ");
|
|
4727
|
+
console.log(` ${meta}`);
|
|
4728
|
+
}
|
|
4729
|
+
if (skill.description) {
|
|
4730
|
+
const descPlain = skill.description.length > 100 ? `${skill.description.slice(0, 97)}...` : skill.description;
|
|
4731
|
+
console.log(`${DIM$3} ${highlightMatches(descPlain, query)}${RESET$3}`);
|
|
4732
|
+
}
|
|
4733
|
+
console.log(`${DIM$3}└ ${skill.sourceUrl || `https://skills.sh/${skill.slug}`}${RESET$3}`);
|
|
4638
4734
|
console.log();
|
|
4639
4735
|
}
|
|
4640
4736
|
return;
|
|
@@ -4657,7 +4753,7 @@ ${DIM$3} 2) npx wittyhub add <owner/repo@skill>${RESET$3}`;
|
|
|
4657
4753
|
console.log();
|
|
4658
4754
|
return;
|
|
4659
4755
|
}
|
|
4660
|
-
const pkg = selected.source || selected.slug;
|
|
4756
|
+
const pkg = selected.sourceUrl || selected.source || selected.slug;
|
|
4661
4757
|
const skillName = selected.name;
|
|
4662
4758
|
console.log();
|
|
4663
4759
|
console.log(`${TEXT$2}Installing ${BOLD$3}${skillName}${RESET$3} from ${DIM$3}${pkg}${RESET$3}...`);
|
|
@@ -4674,9 +4770,156 @@ ${DIM$3} 2) npx wittyhub add <owner/repo@skill>${RESET$3}`;
|
|
|
4674
4770
|
else console.log(`${DIM$3}Discover more skills at${RESET$3} ${TEXT$2}https://skills.sh${RESET$3}`);
|
|
4675
4771
|
console.log();
|
|
4676
4772
|
}
|
|
4773
|
+
function parseGetOptions(args) {
|
|
4774
|
+
let source = "";
|
|
4775
|
+
let skill = "";
|
|
4776
|
+
const errors = [];
|
|
4777
|
+
for (let i = 0; i < args.length; i++) {
|
|
4778
|
+
const arg = args[i];
|
|
4779
|
+
if (arg === "-s" || arg === "--skill") {
|
|
4780
|
+
i++;
|
|
4781
|
+
skill = args[i]?.trim() ?? "";
|
|
4782
|
+
} else if (arg && !arg.startsWith("-")) source = arg.trim();
|
|
4783
|
+
}
|
|
4784
|
+
if (!source) errors.push("Missing source or skill id");
|
|
4785
|
+
return {
|
|
4786
|
+
source,
|
|
4787
|
+
skill,
|
|
4788
|
+
errors
|
|
4789
|
+
};
|
|
4790
|
+
}
|
|
4791
|
+
async function fetchSkillDetail(skillId, timeoutMs = 15e3) {
|
|
4792
|
+
const controller = new AbortController();
|
|
4793
|
+
const timeout = setTimeout(() => controller.abort(), timeoutMs);
|
|
4794
|
+
try {
|
|
4795
|
+
const url = GET_URL.replace("{skill_id}", skillId.split("/").map(encodeURIComponent).join("/"));
|
|
4796
|
+
let response;
|
|
4797
|
+
try {
|
|
4798
|
+
response = await fetch(url, { signal: controller.signal });
|
|
4799
|
+
} catch {
|
|
4800
|
+
return {
|
|
4801
|
+
status: "error",
|
|
4802
|
+
message: `无法连接详情服务: ${GET_URL}`
|
|
4803
|
+
};
|
|
4804
|
+
}
|
|
4805
|
+
if (response.status === 404) return { status: "not_found" };
|
|
4806
|
+
if (!response.ok) return {
|
|
4807
|
+
status: "error",
|
|
4808
|
+
message: `详情服务返回 HTTP ${response.status}`
|
|
4809
|
+
};
|
|
4810
|
+
let data;
|
|
4811
|
+
try {
|
|
4812
|
+
data = await response.json();
|
|
4813
|
+
} catch {
|
|
4814
|
+
return {
|
|
4815
|
+
status: "error",
|
|
4816
|
+
message: "详情服务响应格式无效"
|
|
4817
|
+
};
|
|
4818
|
+
}
|
|
4819
|
+
if (data.error) return {
|
|
4820
|
+
status: "error",
|
|
4821
|
+
message: String(data.error)
|
|
4822
|
+
};
|
|
4823
|
+
const skillData = data.data || data;
|
|
4824
|
+
return {
|
|
4825
|
+
status: "ok",
|
|
4826
|
+
data: {
|
|
4827
|
+
skill_id: skillData.skill_id ?? skillId,
|
|
4828
|
+
name: skillData.name ?? "",
|
|
4829
|
+
description: skillData.description ?? null,
|
|
4830
|
+
version: skillData.version ?? null,
|
|
4831
|
+
commit_id: skillData.commit_id ?? null,
|
|
4832
|
+
author: skillData.author ?? null,
|
|
4833
|
+
source: skillData.source ?? "",
|
|
4834
|
+
source_url: skillData.source_url ?? "",
|
|
4835
|
+
repo_url: skillData.repo_url ?? null,
|
|
4836
|
+
category: skillData.category ?? null,
|
|
4837
|
+
tags: Array.isArray(skillData.tags) ? skillData.tags : null,
|
|
4838
|
+
platform: skillData.platform ?? null,
|
|
4839
|
+
metadata: skillData.metadata ?? null,
|
|
4840
|
+
risk_score: skillData.risk_score ?? null,
|
|
4841
|
+
download_count: skillData.download_count ?? 0,
|
|
4842
|
+
period_downloads: skillData.period_downloads ?? null,
|
|
4843
|
+
rating: skillData.rating ?? null,
|
|
4844
|
+
created_at: skillData.created_at ?? null,
|
|
4845
|
+
updated_at: skillData.updated_at ?? null,
|
|
4846
|
+
last_indexed_at: skillData.last_indexed_at ?? null
|
|
4847
|
+
}
|
|
4848
|
+
};
|
|
4849
|
+
} finally {
|
|
4850
|
+
clearTimeout(timeout);
|
|
4851
|
+
}
|
|
4852
|
+
}
|
|
4853
|
+
function formatInstalls(count) {
|
|
4854
|
+
if (count === void 0 || count === null || Number.isNaN(count)) return "0 installs";
|
|
4855
|
+
if (count <= 0) return "0 installs";
|
|
4856
|
+
if (count >= 1e6) return `${(count / 1e6).toFixed(1).replace(/\.0$/, "")}M installs`;
|
|
4857
|
+
if (count >= 1e3) return `${(count / 1e3).toFixed(1).replace(/\.0$/, "")}K installs`;
|
|
4858
|
+
return `${count} install${count === 1 ? "" : "s"}`;
|
|
4859
|
+
}
|
|
4860
|
+
function formatRiskLevel(riskScore) {
|
|
4861
|
+
if (riskScore === void 0 || riskScore === null) return import_picocolors.default.dim("unknown risk");
|
|
4862
|
+
if (riskScore <= 20) return import_picocolors.default.green("safe risk");
|
|
4863
|
+
if (riskScore <= 50) return import_picocolors.default.green("low risk");
|
|
4864
|
+
if (riskScore <= 80) return import_picocolors.default.yellow("medium risk");
|
|
4865
|
+
return import_picocolors.default.red("high risk");
|
|
4866
|
+
}
|
|
4867
|
+
function buildGetOutput(data) {
|
|
4868
|
+
const lines = [];
|
|
4869
|
+
lines.push(`${import_picocolors.default.bold("Skill:")} ${import_picocolors.default.cyan(data.skill_id)}`);
|
|
4870
|
+
if (data.name) lines.push(`${import_picocolors.default.bold("Name:")} ${data.name}`);
|
|
4871
|
+
if (data.description) lines.push(`${import_picocolors.default.bold("Description:")} ${data.description}`);
|
|
4872
|
+
if (data.author) lines.push(`${import_picocolors.default.bold("Author:")} ${data.author}`);
|
|
4873
|
+
if (data.category) lines.push(`${import_picocolors.default.bold("Category:")} ${data.category}`);
|
|
4874
|
+
if (data.version) lines.push(`${import_picocolors.default.bold("Version:")} ${data.version}`);
|
|
4875
|
+
if (data.tags && data.tags.length > 0) lines.push(`${import_picocolors.default.bold("Tags:")} ${data.tags.join(", ")}`);
|
|
4876
|
+
if (data.platform) lines.push(`${import_picocolors.default.bold("Platform:")} ${data.platform}`);
|
|
4877
|
+
if (data.source) lines.push(`${import_picocolors.default.bold("Source:")} ${data.source}`);
|
|
4878
|
+
if (data.source_url) lines.push(`${import_picocolors.default.bold("Source URL:")} ${import_picocolors.default.dim(data.source_url)}`);
|
|
4879
|
+
if (data.repo_url) lines.push(`${import_picocolors.default.bold("Repo URL:")} ${import_picocolors.default.dim(data.repo_url)}`);
|
|
4880
|
+
lines.push(`${import_picocolors.default.bold("Installs:")} ${import_picocolors.default.cyan(formatInstalls(data.download_count))}`);
|
|
4881
|
+
const riskLabel = formatRiskLevel(data.risk_score);
|
|
4882
|
+
const score = data.risk_score === void 0 || data.risk_score === null ? "" : import_picocolors.default.dim(` (score ${data.risk_score})`);
|
|
4883
|
+
lines.push(`${import_picocolors.default.bold("Risk:")} ${riskLabel}${score}`);
|
|
4884
|
+
if (data.updated_at) lines.push(`${import_picocolors.default.bold("Updated at:")} ${import_picocolors.default.dim(data.updated_at)}`);
|
|
4885
|
+
return lines;
|
|
4886
|
+
}
|
|
4887
|
+
async function runGet(args) {
|
|
4888
|
+
const { source, skill, errors } = parseGetOptions(args);
|
|
4889
|
+
if (errors.length > 0) {
|
|
4890
|
+
for (const error of errors) console.error(import_picocolors.default.red(error));
|
|
4891
|
+
console.error("Usage: wittyhub get <source> --skill <skill>");
|
|
4892
|
+
console.error(" wittyhub get <skill_id>");
|
|
4893
|
+
return;
|
|
4894
|
+
}
|
|
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
|
+
const result = await fetchSkillDetail(skillId);
|
|
4903
|
+
track({
|
|
4904
|
+
event: "get",
|
|
4905
|
+
skillId,
|
|
4906
|
+
found: result.status === "ok" ? "1" : "0"
|
|
4907
|
+
});
|
|
4908
|
+
switch (result.status) {
|
|
4909
|
+
case "ok":
|
|
4910
|
+
for (const line of buildGetOutput(result.data)) console.log(line);
|
|
4911
|
+
break;
|
|
4912
|
+
case "not_found":
|
|
4913
|
+
console.error(import_picocolors.default.red(`Skill not found: ${skillId}`));
|
|
4914
|
+
break;
|
|
4915
|
+
case "error":
|
|
4916
|
+
console.error(import_picocolors.default.red(result.message));
|
|
4917
|
+
break;
|
|
4918
|
+
}
|
|
4919
|
+
}
|
|
4677
4920
|
const isCancelled = (value) => typeof value === "symbol";
|
|
4678
4921
|
function shortenPath$1(fullPath, cwd) {
|
|
4679
|
-
const home = homedir();
|
|
4922
|
+
const home = homedir$1();
|
|
4680
4923
|
if (fullPath === home || fullPath.startsWith(home + sep)) return "~" + fullPath.slice(home.length);
|
|
4681
4924
|
if (fullPath === cwd || fullPath.startsWith(cwd + sep)) return "." + fullPath.slice(cwd.length);
|
|
4682
4925
|
return fullPath;
|
|
@@ -5031,7 +5274,7 @@ const DIM$2 = "\x1B[38;5;102m";
|
|
|
5031
5274
|
const CYAN = "\x1B[36m";
|
|
5032
5275
|
const YELLOW = "\x1B[33m";
|
|
5033
5276
|
function shortenPath(fullPath, cwd) {
|
|
5034
|
-
const home = homedir();
|
|
5277
|
+
const home = homedir$1();
|
|
5035
5278
|
if (fullPath.startsWith(home)) return fullPath.replace(home, "~");
|
|
5036
5279
|
if (fullPath.startsWith(cwd)) return "." + fullPath.slice(cwd.length);
|
|
5037
5280
|
return fullPath;
|
|
@@ -5268,7 +5511,7 @@ async function removeCommand(skillNames, options) {
|
|
|
5268
5511
|
for (const pathToCleanup of pathsToCleanup) {
|
|
5269
5512
|
if (pathToCleanup === canonicalPath) continue;
|
|
5270
5513
|
try {
|
|
5271
|
-
if (await lstat(pathToCleanup).catch(() => null)) await rm(pathToCleanup, {
|
|
5514
|
+
if (await lstat(pathToCleanup).catch(() => null)) await rm$1(pathToCleanup, {
|
|
5272
5515
|
recursive: true,
|
|
5273
5516
|
force: true
|
|
5274
5517
|
});
|
|
@@ -5286,7 +5529,7 @@ async function removeCommand(skillNames, options) {
|
|
|
5286
5529
|
isStillUsed = true;
|
|
5287
5530
|
break;
|
|
5288
5531
|
}
|
|
5289
|
-
if (!isStillUsed) await rm(canonicalPath, {
|
|
5532
|
+
if (!isStillUsed) await rm$1(canonicalPath, {
|
|
5290
5533
|
recursive: true,
|
|
5291
5534
|
force: true
|
|
5292
5535
|
});
|
|
@@ -5934,7 +6177,7 @@ function buildUsePrompt(input) {
|
|
|
5934
6177
|
return sections.join("\n\n") + "\n";
|
|
5935
6178
|
}
|
|
5936
6179
|
async function materializeUseSkill(skill) {
|
|
5937
|
-
const tempRoot = await mkdtemp(join(tmpdir(), "skills-use-"));
|
|
6180
|
+
const tempRoot = await mkdtemp$1(join(tmpdir$1(), "skills-use-"));
|
|
5938
6181
|
const skillDir = join(tempRoot, sanitizeName(skill.directoryName || skill.name));
|
|
5939
6182
|
if (!isPathSafe(tempRoot, skillDir)) throw new Error("Invalid skill name: potential path traversal detected");
|
|
5940
6183
|
await mkdir(skillDir, { recursive: true });
|
|
@@ -6161,8 +6404,8 @@ async function writeSafeFile(targetDir, filePath, contents) {
|
|
|
6161
6404
|
const fullPath = join(targetDir, filePath);
|
|
6162
6405
|
if (!isPathSafe(targetDir, fullPath)) return;
|
|
6163
6406
|
await mkdir(dirname(fullPath), { recursive: true });
|
|
6164
|
-
if (typeof contents === "string") await writeFile(fullPath, contents, "utf-8");
|
|
6165
|
-
else await writeFile(fullPath, contents);
|
|
6407
|
+
if (typeof contents === "string") await writeFile$1(fullPath, contents, "utf-8");
|
|
6408
|
+
else await writeFile$1(fullPath, contents);
|
|
6166
6409
|
}
|
|
6167
6410
|
async function copySkillDirectory(src, dest) {
|
|
6168
6411
|
await mkdir(dest, { recursive: true });
|
|
@@ -6222,6 +6465,308 @@ function fail(message) {
|
|
|
6222
6465
|
process.exit(1);
|
|
6223
6466
|
}
|
|
6224
6467
|
var UseCommandError = class extends Error {};
|
|
6468
|
+
const CANONICAL_CATEGORIES = new Set([
|
|
6469
|
+
"Research and Design",
|
|
6470
|
+
"Development and Build",
|
|
6471
|
+
"Engineering and Compilation",
|
|
6472
|
+
"Quality and Validation",
|
|
6473
|
+
"Release and Deployment",
|
|
6474
|
+
"Monitoring and Operations",
|
|
6475
|
+
"Performance Optimization",
|
|
6476
|
+
"Security Hardening",
|
|
6477
|
+
"others"
|
|
6478
|
+
]);
|
|
6479
|
+
const NAME_PATTERN = /^[a-z][a-z0-9-]*$/;
|
|
6480
|
+
const SEMVER_PATTERN = /^\d+\.\d+\.\d+(?:-[\w.]+)?(?:\+[\w.]+)?$/;
|
|
6481
|
+
function parseValidateOptions(args) {
|
|
6482
|
+
let path = "";
|
|
6483
|
+
const options = {};
|
|
6484
|
+
const errors = [];
|
|
6485
|
+
for (let i = 0; i < args.length; i++) {
|
|
6486
|
+
const arg = args[i];
|
|
6487
|
+
if (arg === "--json") options.json = true;
|
|
6488
|
+
else if (arg === "-h" || arg === "--help") {} else if (arg && !arg.startsWith("-")) if (!path) path = arg.trim();
|
|
6489
|
+
else errors.push(`Unexpected argument: ${arg}`);
|
|
6490
|
+
else if (arg) errors.push(`Unknown option: ${arg}`);
|
|
6491
|
+
}
|
|
6492
|
+
if (!path) errors.push("Missing required argument: <path>");
|
|
6493
|
+
return {
|
|
6494
|
+
path,
|
|
6495
|
+
options,
|
|
6496
|
+
errors
|
|
6497
|
+
};
|
|
6498
|
+
}
|
|
6499
|
+
async function runValidate(args) {
|
|
6500
|
+
const { path, options, errors } = parseValidateOptions(args);
|
|
6501
|
+
if (errors.length > 0) {
|
|
6502
|
+
for (const e of errors) console.error(import_picocolors.default.red(`error: ${e}`));
|
|
6503
|
+
console.error(`\nRun ${import_picocolors.default.cyan("wittyhub validate --help")} for usage.\n`);
|
|
6504
|
+
return null;
|
|
6505
|
+
}
|
|
6506
|
+
const result = await validateSkill(resolve(path));
|
|
6507
|
+
if (options.json) console.log(JSON.stringify(result, null, 2));
|
|
6508
|
+
else printResult(result);
|
|
6509
|
+
return result;
|
|
6510
|
+
}
|
|
6511
|
+
async function validateSkill(absPath) {
|
|
6512
|
+
const errors = [];
|
|
6513
|
+
const warnings = [];
|
|
6514
|
+
let skillDir = absPath;
|
|
6515
|
+
let skillMdPath = null;
|
|
6516
|
+
let stats;
|
|
6517
|
+
try {
|
|
6518
|
+
stats = await stat(absPath);
|
|
6519
|
+
} catch {
|
|
6520
|
+
errors.push({
|
|
6521
|
+
id: "path-not-found",
|
|
6522
|
+
severity: "error",
|
|
6523
|
+
message: `Path does not exist: ${absPath}`,
|
|
6524
|
+
hint: "Pass the skill directory (e.g. ./my-skill) or the SKILL.md file path."
|
|
6525
|
+
});
|
|
6526
|
+
return {
|
|
6527
|
+
path: absPath,
|
|
6528
|
+
errors,
|
|
6529
|
+
warnings,
|
|
6530
|
+
ok: false
|
|
6531
|
+
};
|
|
6532
|
+
}
|
|
6533
|
+
if (stats.isFile()) {
|
|
6534
|
+
const filename = basename(absPath);
|
|
6535
|
+
if (filename !== "SKILL.md") {
|
|
6536
|
+
errors.push({
|
|
6537
|
+
id: "filename-must-be-skill-md",
|
|
6538
|
+
severity: "error",
|
|
6539
|
+
message: `File must be named "SKILL.md" (got "${filename}")`,
|
|
6540
|
+
hint: "Rename the file to SKILL.md (uppercase). The agent skill spec requires this exact filename."
|
|
6541
|
+
});
|
|
6542
|
+
return {
|
|
6543
|
+
path: absPath,
|
|
6544
|
+
errors,
|
|
6545
|
+
warnings,
|
|
6546
|
+
ok: false
|
|
6547
|
+
};
|
|
6548
|
+
}
|
|
6549
|
+
skillMdPath = absPath;
|
|
6550
|
+
skillDir = dirname(absPath);
|
|
6551
|
+
} else if (stats.isDirectory()) {
|
|
6552
|
+
const expected = join(absPath, "SKILL.md");
|
|
6553
|
+
try {
|
|
6554
|
+
await stat(expected);
|
|
6555
|
+
skillMdPath = expected;
|
|
6556
|
+
} catch {
|
|
6557
|
+
errors.push({
|
|
6558
|
+
id: "missing-skill-md",
|
|
6559
|
+
severity: "error",
|
|
6560
|
+
message: `No SKILL.md found in directory: ${absPath}`,
|
|
6561
|
+
hint: "Create one with `wittyhub init <name>` or rename an existing markdown file to SKILL.md (uppercase)."
|
|
6562
|
+
});
|
|
6563
|
+
return {
|
|
6564
|
+
path: absPath,
|
|
6565
|
+
errors,
|
|
6566
|
+
warnings,
|
|
6567
|
+
ok: false
|
|
6568
|
+
};
|
|
6569
|
+
}
|
|
6570
|
+
} else {
|
|
6571
|
+
errors.push({
|
|
6572
|
+
id: "path-invalid-type",
|
|
6573
|
+
severity: "error",
|
|
6574
|
+
message: `Path is neither a file nor a directory: ${absPath}`
|
|
6575
|
+
});
|
|
6576
|
+
return {
|
|
6577
|
+
path: absPath,
|
|
6578
|
+
errors,
|
|
6579
|
+
warnings,
|
|
6580
|
+
ok: false
|
|
6581
|
+
};
|
|
6582
|
+
}
|
|
6583
|
+
try {
|
|
6584
|
+
const lowerMdFile = (await readdir(skillDir)).find((e) => e.toLowerCase() === "skill.md" && e !== "SKILL.md");
|
|
6585
|
+
if (lowerMdFile) warnings.push({
|
|
6586
|
+
id: "case-variant-found",
|
|
6587
|
+
severity: "warning",
|
|
6588
|
+
message: `Found a case-variant file "${lowerMdFile}" alongside SKILL.md`,
|
|
6589
|
+
hint: "Only \"SKILL.md\" (all uppercase) is recognized by the agent spec. Remove the variant to avoid confusion."
|
|
6590
|
+
});
|
|
6591
|
+
} catch {}
|
|
6592
|
+
const content = await readFile(skillMdPath, "utf-8");
|
|
6593
|
+
const match = content.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n?([\s\S]*)$/);
|
|
6594
|
+
if (!match) {
|
|
6595
|
+
errors.push({
|
|
6596
|
+
id: "frontmatter-missing",
|
|
6597
|
+
severity: "error",
|
|
6598
|
+
message: "SKILL.md must start with a YAML frontmatter block delimited by \"---\"",
|
|
6599
|
+
hint: "Add a frontmatter block at the top:\n\n ---\n name: my-skill\n description: what it does\n ---"
|
|
6600
|
+
});
|
|
6601
|
+
return {
|
|
6602
|
+
path: skillMdPath,
|
|
6603
|
+
errors,
|
|
6604
|
+
warnings,
|
|
6605
|
+
ok: false
|
|
6606
|
+
};
|
|
6607
|
+
}
|
|
6608
|
+
match[1];
|
|
6609
|
+
const body = match[2] ?? "";
|
|
6610
|
+
const { data: fm } = parseFrontmatter(content);
|
|
6611
|
+
if (!fm || typeof fm !== "object" || Array.isArray(fm)) {
|
|
6612
|
+
errors.push({
|
|
6613
|
+
id: "frontmatter-not-object",
|
|
6614
|
+
severity: "error",
|
|
6615
|
+
message: "Frontmatter must be a YAML mapping (key-value pairs), not a scalar or list."
|
|
6616
|
+
});
|
|
6617
|
+
return {
|
|
6618
|
+
path: skillMdPath,
|
|
6619
|
+
errors,
|
|
6620
|
+
warnings,
|
|
6621
|
+
ok: false
|
|
6622
|
+
};
|
|
6623
|
+
}
|
|
6624
|
+
if (fm.name === void 0 || fm.name === null || fm.name === "") errors.push({
|
|
6625
|
+
id: "name-required",
|
|
6626
|
+
severity: "error",
|
|
6627
|
+
message: "Missing required frontmatter field: name",
|
|
6628
|
+
hint: "Add `name: <skill-name>` to the frontmatter."
|
|
6629
|
+
});
|
|
6630
|
+
else if (typeof fm.name !== "string") errors.push({
|
|
6631
|
+
id: "name-not-string",
|
|
6632
|
+
severity: "error",
|
|
6633
|
+
message: `Frontmatter field "name" must be a string (got ${typeof fm.name})`,
|
|
6634
|
+
hint: "Wrap the value in quotes if it contains special characters."
|
|
6635
|
+
});
|
|
6636
|
+
else {
|
|
6637
|
+
if (!NAME_PATTERN.test(fm.name)) warnings.push({
|
|
6638
|
+
id: "name-not-kebab",
|
|
6639
|
+
severity: "warning",
|
|
6640
|
+
message: `Skill name "${fm.name}" is not in kebab-case (lowercase letters, digits, hyphens)`,
|
|
6641
|
+
hint: "Use a name like \"add-or-fix-type-checking\" for best compatibility."
|
|
6642
|
+
});
|
|
6643
|
+
if (basename(skillDir) !== fm.name) warnings.push({
|
|
6644
|
+
id: "name-dirname-mismatch",
|
|
6645
|
+
severity: "warning",
|
|
6646
|
+
message: `Skill name "${fm.name}" does not match directory name "${basename(skillDir)}"`,
|
|
6647
|
+
hint: "For consistency, the directory name and `name` field should match."
|
|
6648
|
+
});
|
|
6649
|
+
}
|
|
6650
|
+
if (fm.description === void 0 || fm.description === null || fm.description === "") errors.push({
|
|
6651
|
+
id: "description-required",
|
|
6652
|
+
severity: "error",
|
|
6653
|
+
message: "Missing required frontmatter field: description",
|
|
6654
|
+
hint: "Add `description: <one-line summary>` to the frontmatter."
|
|
6655
|
+
});
|
|
6656
|
+
else if (typeof fm.description !== "string") errors.push({
|
|
6657
|
+
id: "description-not-string",
|
|
6658
|
+
severity: "error",
|
|
6659
|
+
message: `Frontmatter field "description" must be a string (got ${typeof fm.description})`
|
|
6660
|
+
});
|
|
6661
|
+
else {
|
|
6662
|
+
const desc = fm.description.trim();
|
|
6663
|
+
if (desc.length < 10) warnings.push({
|
|
6664
|
+
id: "description-too-short",
|
|
6665
|
+
severity: "warning",
|
|
6666
|
+
message: `Description is very short (${desc.length} chars): "${desc}"`,
|
|
6667
|
+
hint: "Aim for at least 10-20 characters so users can quickly understand the skill."
|
|
6668
|
+
});
|
|
6669
|
+
if (desc.length > 200) warnings.push({
|
|
6670
|
+
id: "description-too-long",
|
|
6671
|
+
severity: "warning",
|
|
6672
|
+
message: `Description is very long (${desc.length} chars)`,
|
|
6673
|
+
hint: "Keep it under 200 characters for readability in listings."
|
|
6674
|
+
});
|
|
6675
|
+
}
|
|
6676
|
+
if (fm.category !== void 0 && fm.category !== null) {
|
|
6677
|
+
if (typeof fm.category !== "string") warnings.push({
|
|
6678
|
+
id: "category-not-string",
|
|
6679
|
+
severity: "warning",
|
|
6680
|
+
message: `Frontmatter field "category" should be a string (got ${typeof fm.category})`
|
|
6681
|
+
});
|
|
6682
|
+
else if (!CANONICAL_CATEGORIES.has(fm.category)) warnings.push({
|
|
6683
|
+
id: "category-not-canonical",
|
|
6684
|
+
severity: "warning",
|
|
6685
|
+
message: `Category "${fm.category}" is not in the canonical list`,
|
|
6686
|
+
hint: `Valid values: ${[...CANONICAL_CATEGORIES].join(", ")}. See https://gitcode.com/openeuler/openEuler-skills.`
|
|
6687
|
+
});
|
|
6688
|
+
}
|
|
6689
|
+
if (fm.version !== void 0 && fm.version !== null && fm.version !== "") {
|
|
6690
|
+
if (typeof fm.version !== "string") warnings.push({
|
|
6691
|
+
id: "version-not-string",
|
|
6692
|
+
severity: "warning",
|
|
6693
|
+
message: `Frontmatter field "version" should be a string (got ${typeof fm.version})`,
|
|
6694
|
+
hint: "Quote the value, e.g. `version: \"1.0.0\"`."
|
|
6695
|
+
});
|
|
6696
|
+
else if (!SEMVER_PATTERN.test(fm.version.trim())) warnings.push({
|
|
6697
|
+
id: "version-not-semver",
|
|
6698
|
+
severity: "warning",
|
|
6699
|
+
message: `Version "${fm.version}" is not a valid semver`,
|
|
6700
|
+
hint: "Use the form X.Y.Z (e.g. 1.0.0, 0.2.1-beta)."
|
|
6701
|
+
});
|
|
6702
|
+
}
|
|
6703
|
+
if (fm.metadata !== void 0 && (typeof fm.metadata !== "object" || Array.isArray(fm.metadata))) warnings.push({
|
|
6704
|
+
id: "metadata-not-object",
|
|
6705
|
+
severity: "warning",
|
|
6706
|
+
message: `Frontmatter field "metadata" should be a mapping (got ${Array.isArray(fm.metadata) ? "array" : typeof fm.metadata})`
|
|
6707
|
+
});
|
|
6708
|
+
if (body.trim().length === 0) warnings.push({
|
|
6709
|
+
id: "body-empty",
|
|
6710
|
+
severity: "warning",
|
|
6711
|
+
message: "SKILL.md has no body content after the frontmatter",
|
|
6712
|
+
hint: "Add at least a heading and a few instructions for the agent to follow."
|
|
6713
|
+
});
|
|
6714
|
+
else if (!/^#{1,6}\s+\S/m.test(body)) warnings.push({
|
|
6715
|
+
id: "body-no-heading",
|
|
6716
|
+
severity: "warning",
|
|
6717
|
+
message: "Body content has no Markdown headings",
|
|
6718
|
+
hint: "Structure the instructions with headings like `## When to use`, `## Instructions`."
|
|
6719
|
+
});
|
|
6720
|
+
const knownKeys = new Set([
|
|
6721
|
+
"name",
|
|
6722
|
+
"description",
|
|
6723
|
+
"version",
|
|
6724
|
+
"category",
|
|
6725
|
+
"metadata"
|
|
6726
|
+
]);
|
|
6727
|
+
const unknownKeys = Object.keys(fm).filter((k) => !knownKeys.has(k));
|
|
6728
|
+
if (unknownKeys.length > 0) warnings.push({
|
|
6729
|
+
id: "unknown-frontmatter-keys",
|
|
6730
|
+
severity: "warning",
|
|
6731
|
+
message: `Unknown frontmatter keys: ${unknownKeys.join(", ")}`,
|
|
6732
|
+
hint: `Recognized keys: ${[...knownKeys].join(", ")}.`
|
|
6733
|
+
});
|
|
6734
|
+
return {
|
|
6735
|
+
path: skillMdPath,
|
|
6736
|
+
errors,
|
|
6737
|
+
warnings,
|
|
6738
|
+
ok: errors.length === 0
|
|
6739
|
+
};
|
|
6740
|
+
}
|
|
6741
|
+
function printResult(result) {
|
|
6742
|
+
const { path, errors, warnings, ok } = result;
|
|
6743
|
+
console.log(`${import_picocolors.default.cyan("Path:")} ${path}`);
|
|
6744
|
+
console.log();
|
|
6745
|
+
if (errors.length === 0 && warnings.length === 0) {
|
|
6746
|
+
console.log(import_picocolors.default.green(" All checks passed."));
|
|
6747
|
+
console.log();
|
|
6748
|
+
return;
|
|
6749
|
+
}
|
|
6750
|
+
if (errors.length > 0) {
|
|
6751
|
+
console.log(import_picocolors.default.red(`${errors.length} error(s):`));
|
|
6752
|
+
for (const rule of errors) {
|
|
6753
|
+
console.log(` ${import_picocolors.default.red("✗")} [${rule.id}] ${rule.message}`);
|
|
6754
|
+
if (rule.hint) console.log(` ${import_picocolors.default.dim(rule.hint)}`);
|
|
6755
|
+
}
|
|
6756
|
+
console.log();
|
|
6757
|
+
}
|
|
6758
|
+
if (warnings.length > 0) {
|
|
6759
|
+
console.log(import_picocolors.default.yellow(`${warnings.length} warning(s):`));
|
|
6760
|
+
for (const rule of warnings) {
|
|
6761
|
+
console.log(` ${import_picocolors.default.yellow("!")} [${rule.id}] ${rule.message}`);
|
|
6762
|
+
if (rule.hint) console.log(` ${import_picocolors.default.dim(rule.hint)}`);
|
|
6763
|
+
}
|
|
6764
|
+
console.log();
|
|
6765
|
+
}
|
|
6766
|
+
if (ok) console.log(import_picocolors.default.green(" Skill is valid.") + import_picocolors.default.yellow(warnings.length > 0 ? ` (with ${warnings.length} warning${warnings.length > 1 ? "s" : ""})` : ""));
|
|
6767
|
+
else console.log(import_picocolors.default.red(" Skill is NOT valid. Fix the errors above."));
|
|
6768
|
+
console.log();
|
|
6769
|
+
}
|
|
6225
6770
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
6226
6771
|
function getVersion() {
|
|
6227
6772
|
try {
|
|
@@ -6274,11 +6819,12 @@ function showBanner() {
|
|
|
6274
6819
|
console.log();
|
|
6275
6820
|
console.log(` ${DIM}$${RESET} ${TEXT}npx wittyhub experimental_install${RESET} ${DIM}Restore from skills-lock.json${RESET}`);
|
|
6276
6821
|
console.log(` ${DIM}$${RESET} ${TEXT}npx wittyhub init ${DIM}[name]${RESET} ${DIM}Create a new skill${RESET}`);
|
|
6822
|
+
console.log(` ${DIM}$${RESET} ${TEXT}npx wittyhub validate ${DIM}<path>${RESET} ${DIM}Validate a local skill${RESET}`);
|
|
6277
6823
|
console.log(` ${DIM}$${RESET} ${TEXT}npx wittyhub experimental_sync${RESET} ${DIM}Sync skills from node_modules${RESET}`);
|
|
6278
6824
|
console.log();
|
|
6279
6825
|
console.log(`${DIM}try:${RESET} npx wittyhub add vercel-labs/agent-skills`);
|
|
6280
6826
|
console.log();
|
|
6281
|
-
console.log(`Discover more skills at ${TEXT}https://
|
|
6827
|
+
console.log(`Discover more skills at ${TEXT}https://skillhub.openeuler.org/${RESET}`);
|
|
6282
6828
|
console.log();
|
|
6283
6829
|
}
|
|
6284
6830
|
function showHelp() {
|
|
@@ -6294,6 +6840,14 @@ ${BOLD}Manage Skills:${RESET}
|
|
|
6294
6840
|
remove [skills] Remove installed skills
|
|
6295
6841
|
list, ls List installed skills
|
|
6296
6842
|
find [query] Search for skills interactively
|
|
6843
|
+
audit <source> --skill <skill>
|
|
6844
|
+
Show security audit result (risk level + risk signals)
|
|
6845
|
+
e.g. audit https://github.com/huggingface/transformers --skill add-or-fix-type-checking
|
|
6846
|
+
get <source> --skill <skill>
|
|
6847
|
+
View skill details (author/category/version/description/tags)
|
|
6848
|
+
e.g. get https://github.com/huggingface/transformers --skill add-or-fix-type-checking
|
|
6849
|
+
validate <path> Validate a local skill for spec compliance (SKILL.md filename,
|
|
6850
|
+
frontmatter, required fields name/description, etc.)
|
|
6297
6851
|
|
|
6298
6852
|
${BOLD}Find Options:${RESET}
|
|
6299
6853
|
--owner <owner> Search only repositories from a GitHub owner
|
|
@@ -6308,7 +6862,7 @@ ${BOLD}Update Options:${RESET}
|
|
|
6308
6862
|
|
|
6309
6863
|
${BOLD}Project:${RESET}
|
|
6310
6864
|
experimental_install Restore skills from skills-lock.json
|
|
6311
|
-
init [name] Initialize a skill (creates <name>/SKILL.md
|
|
6865
|
+
init [name] Initialize a skill (creates <name>/SKILL.md; defaults to my-skill/)
|
|
6312
6866
|
experimental_sync Sync skills from node_modules into agent directories
|
|
6313
6867
|
|
|
6314
6868
|
${BOLD}Add Options:${RESET}
|
|
@@ -6350,12 +6904,9 @@ ${BOLD}Options:${RESET}
|
|
|
6350
6904
|
--version, -v Show version number
|
|
6351
6905
|
|
|
6352
6906
|
${BOLD}Examples:${RESET}
|
|
6353
|
-
${DIM}$${RESET} wittyhub add
|
|
6354
|
-
${DIM}$${RESET} wittyhub
|
|
6355
|
-
${DIM}$${RESET} wittyhub
|
|
6356
|
-
${DIM}$${RESET} wittyhub add vercel-labs/agent-skills -g
|
|
6357
|
-
${DIM}$${RESET} wittyhub add vercel-labs/agent-skills --agent claude-code cursor
|
|
6358
|
-
${DIM}$${RESET} wittyhub add vercel-labs/agent-skills --skill pr-review commit
|
|
6907
|
+
${DIM}$${RESET} wittyhub add https://gitcode.com/openeuler/witty-diagnosis-agent --skill system-resource-diagnosis
|
|
6908
|
+
${DIM}$${RESET} wittyhub add https://gitcode.com/openeuler/witty-diagnosis-agent --skill system-resource-diagnosis -g
|
|
6909
|
+
${DIM}$${RESET} wittyhub add https://gitcode.com/openeuler/witty-diagnosis-agent --skill system-resource-diagnosis --agent claude-code cursor
|
|
6359
6910
|
${DIM}$${RESET} wittyhub remove ${DIM}# interactive remove${RESET}
|
|
6360
6911
|
${DIM}$${RESET} wittyhub remove web-design ${DIM}# remove by name${RESET}
|
|
6361
6912
|
${DIM}$${RESET} wittyhub rm --global frontend-design
|
|
@@ -6374,7 +6925,45 @@ ${BOLD}Examples:${RESET}
|
|
|
6374
6925
|
${DIM}$${RESET} wittyhub experimental_sync ${DIM}# sync from node_modules${RESET}
|
|
6375
6926
|
${DIM}$${RESET} wittyhub experimental_sync -y ${DIM}# sync without prompts${RESET}
|
|
6376
6927
|
|
|
6377
|
-
Discover more skills at ${TEXT}https://
|
|
6928
|
+
Discover more skills at ${TEXT}https://skillhub.openeuler.org/${RESET}
|
|
6929
|
+
`);
|
|
6930
|
+
}
|
|
6931
|
+
function showInitHelp() {
|
|
6932
|
+
console.log(`
|
|
6933
|
+
${BOLD}Usage:${RESET} wittyhub init [name]
|
|
6934
|
+
|
|
6935
|
+
${BOLD}Description:${RESET}
|
|
6936
|
+
Initialize a new skill by creating a SKILL.md template.
|
|
6937
|
+
|
|
6938
|
+
${BOLD}Arguments:${RESET}
|
|
6939
|
+
name Optional skill name. Creates <name>/SKILL.md.
|
|
6940
|
+
If omitted, defaults to my-skill/ (never the cwd itself).
|
|
6941
|
+
|
|
6942
|
+
${BOLD}Examples:${RESET}
|
|
6943
|
+
${DIM}$${RESET} wittyhub init ${DIM}# creates my-skill/SKILL.md${RESET}
|
|
6944
|
+
${DIM}$${RESET} wittyhub init my-skill ${DIM}# creates my-skill/SKILL.md${RESET}
|
|
6945
|
+
${DIM}$${RESET} wittyhub init code-reviewer ${DIM}# creates code-reviewer/SKILL.md${RESET}
|
|
6946
|
+
`);
|
|
6947
|
+
}
|
|
6948
|
+
function showValidateHelp() {
|
|
6949
|
+
console.log(`
|
|
6950
|
+
${BOLD}Usage:${RESET} wittyhub validate <path> [options]
|
|
6951
|
+
|
|
6952
|
+
${BOLD}Description:${RESET}
|
|
6953
|
+
Validate a local skill for spec compliance (SKILL.md filename, frontmatter,
|
|
6954
|
+
required fields, etc.). Reports errors (must-fix) and warnings (recommended).
|
|
6955
|
+
|
|
6956
|
+
${BOLD}Arguments:${RESET}
|
|
6957
|
+
path Path to the skill directory or SKILL.md file.
|
|
6958
|
+
|
|
6959
|
+
${BOLD}Options:${RESET}
|
|
6960
|
+
--json Output result as JSON (no ANSI colors).
|
|
6961
|
+
-h, --help Show this help message.
|
|
6962
|
+
|
|
6963
|
+
${BOLD}Examples:${RESET}
|
|
6964
|
+
${DIM}$${RESET} wittyhub validate ./my-skill
|
|
6965
|
+
${DIM}$${RESET} wittyhub validate ./my-skill/SKILL.md
|
|
6966
|
+
${DIM}$${RESET} wittyhub validate ./my-skill --json
|
|
6378
6967
|
`);
|
|
6379
6968
|
}
|
|
6380
6969
|
function showRemoveHelp() {
|
|
@@ -6404,24 +6993,25 @@ ${BOLD}Examples:${RESET}
|
|
|
6404
6993
|
${DIM}$${RESET} wittyhub remove --all ${DIM}# remove all skills${RESET}
|
|
6405
6994
|
${DIM}$${RESET} wittyhub remove --skill '*' -a cursor ${DIM}# remove all skills from cursor${RESET}
|
|
6406
6995
|
|
|
6407
|
-
Discover more skills at ${TEXT}https://
|
|
6996
|
+
Discover more skills at ${TEXT}https://skillhub.openeuler.org/${RESET}
|
|
6408
6997
|
`);
|
|
6409
6998
|
}
|
|
6410
6999
|
function runInit(args) {
|
|
6411
7000
|
const cwd = process.cwd();
|
|
6412
|
-
const skillName = args[0]
|
|
6413
|
-
const
|
|
6414
|
-
const skillDir = hasName ? join(cwd, skillName) : cwd;
|
|
7001
|
+
const skillName = args[0] ?? "my-skill";
|
|
7002
|
+
const skillDir = join(cwd, skillName);
|
|
6415
7003
|
const skillFile = join(skillDir, "SKILL.md");
|
|
6416
|
-
const displayPath =
|
|
7004
|
+
const displayPath = `${skillName}/SKILL.md`;
|
|
6417
7005
|
if (existsSync(skillFile)) {
|
|
6418
7006
|
console.log(`${TEXT}Skill already exists at ${DIM}${displayPath}${RESET}`);
|
|
6419
7007
|
return;
|
|
6420
7008
|
}
|
|
6421
|
-
|
|
7009
|
+
mkdirSync(skillDir, { recursive: true });
|
|
6422
7010
|
writeFileSync(skillFile, `---
|
|
6423
7011
|
name: ${skillName}
|
|
6424
7012
|
description: A brief description of what this skill does
|
|
7013
|
+
version: 0.1.0 # optional; increment as needed
|
|
7014
|
+
category: others # optional;change if needed — full list at https://gitcode.com/openeuler/openEuler-skills
|
|
6425
7015
|
---
|
|
6426
7016
|
|
|
6427
7017
|
# ${skillName}
|
|
@@ -6446,12 +7036,12 @@ Describe when this skill should be used.
|
|
|
6446
7036
|
console.log(`${DIM}Next steps:${RESET}`);
|
|
6447
7037
|
console.log(` 1. Edit ${TEXT}${displayPath}${RESET} to define your skill instructions`);
|
|
6448
7038
|
console.log(` 2. Update the ${TEXT}name${RESET} and ${TEXT}description${RESET} in the frontmatter`);
|
|
7039
|
+
console.log(` 3. Optionally set ${TEXT}version${RESET} and ${TEXT}category${RESET} (see the comment in ${TEXT}${displayPath}${RESET} for the full list of categories)`);
|
|
6449
7040
|
console.log();
|
|
6450
7041
|
console.log(`${DIM}Publishing:${RESET}`);
|
|
6451
|
-
console.log(`
|
|
6452
|
-
console.log(` ${DIM}URL:${RESET} Host the file, then ${TEXT}npx wittyhub add https://example.com/${displayPath}${RESET}`);
|
|
7042
|
+
console.log(` Submit your skill to ${TEXT}https://gitcode.com/openeuler/openEuler-skills${RESET}`);
|
|
6453
7043
|
console.log();
|
|
6454
|
-
console.log(`Browse existing skills for inspiration at ${TEXT}https://
|
|
7044
|
+
console.log(`Browse existing skills for inspiration at ${TEXT}https://skillhub.openeuler.org/${RESET}`);
|
|
6455
7045
|
console.log();
|
|
6456
7046
|
}
|
|
6457
7047
|
async function main() {
|
|
@@ -6473,6 +7063,10 @@ async function main() {
|
|
|
6473
7063
|
await runFind(restArgs);
|
|
6474
7064
|
break;
|
|
6475
7065
|
case "init":
|
|
7066
|
+
if (restArgs.includes("--help") || restArgs.includes("-h")) {
|
|
7067
|
+
showInitHelp();
|
|
7068
|
+
break;
|
|
7069
|
+
}
|
|
6476
7070
|
if (!inAgent) showLogo();
|
|
6477
7071
|
console.log();
|
|
6478
7072
|
runInit(restArgs);
|
|
@@ -6515,6 +7109,24 @@ async function main() {
|
|
|
6515
7109
|
case "ls":
|
|
6516
7110
|
await runList(restArgs);
|
|
6517
7111
|
break;
|
|
7112
|
+
case "audit":
|
|
7113
|
+
if (!inAgent) showLogo();
|
|
7114
|
+
await runAudit(restArgs);
|
|
7115
|
+
break;
|
|
7116
|
+
case "get":
|
|
7117
|
+
if (!inAgent) showLogo();
|
|
7118
|
+
await runGet(restArgs);
|
|
7119
|
+
break;
|
|
7120
|
+
case "validate":
|
|
7121
|
+
case "check-skill":
|
|
7122
|
+
if (restArgs.includes("--help") || restArgs.includes("-h")) {
|
|
7123
|
+
showValidateHelp();
|
|
7124
|
+
break;
|
|
7125
|
+
}
|
|
7126
|
+
if (!inAgent) showLogo();
|
|
7127
|
+
console.log();
|
|
7128
|
+
await runValidate(restArgs);
|
|
7129
|
+
break;
|
|
6518
7130
|
case "check":
|
|
6519
7131
|
case "update":
|
|
6520
7132
|
case "upgrade":
|