openclaw
Version:
Multi-channel AI gateway with extensible messaging integrations
1,103 lines (1,102 loc) • 39 kB
JavaScript
import { i as formatErrorMessage } from "./errors-BXgSefBE.js";
import { w as pathExists } from "./fs-safe-aqmM_n6V.js";
import { i as resolveSafeInstallDir } from "./install-safe-path-C0w7ALW6.js";
import { t as CONFIG_DIR } from "./utils-CCC-BEJH.js";
import { t as createSubsystemLogger } from "./subsystem-BzXSmsuh.js";
import { C as searchClawHubSkills, _ as reportClawHubSkillInstallTelemetry, a as downloadClawHubSkillArchiveUrl, d as fetchClawHubSkillInstallResolution, i as downloadClawHubSkillArchive, m as isDefaultClawHubBaseUrl, n as downloadClawHubGitHubSkillArchive, u as fetchClawHubSkillDetail, v as resolveClawHubBaseUrl } from "./clawhub-CzBrlwb2.js";
import { a as readJsonIfExists, m as writeJson, o as tryReadJson } from "./json-files-2umMHm0W.js";
import { t as resolveEffectiveAgentSkillFilter } from "./agent-filter-Ckm_ksHY.js";
import { a as resolveSkillsInstallPreferences, i as resolveSkillConfig, n as isConfigPathTruthy, r as resolveBundledAllowlist, t as isBundledSkillAllowed } from "./config-CEJFxFUy.js";
import { g as normalizeSkillIndexName, m as buildSkillIndexEntries, p as loadSkillsFromDirSafe, s as loadWorkspaceSkillEntries } from "./workspace-_gXfieOh.js";
import { t as resolveBundledSkillsDir } from "./bundled-dir-BCWH7qz6.js";
import { n as hasBinary } from "./config-eval-CAHzuwOy.js";
import { t as evaluateEntryRequirementsForCurrentPlatform } from "./entry-status-kSQWYkJm.js";
import { i as withExtractedArchiveRoot, t as installPackageDir } from "./install-package-dir-CkyHlu-d.js";
import { t as evaluateSkillInstallPolicy } from "./install-security-scan-DbaM_3Fm.js";
import fs from "node:fs";
import path from "node:path";
//#region src/skills/lifecycle/archive-install.ts
const VALID_SLUG_PATTERN = /^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/i;
const DEFAULT_SKILL_ARCHIVE_ROOT_MARKERS = ["SKILL.md"];
/** Accepted root marker names for ClawHub skill archive uploads. */
const CLAWHUB_SKILL_ARCHIVE_ROOT_MARKERS = [
"SKILL.md",
"skill.md",
"skills.md",
"SKILL.MD"
];
function hasNonAscii(value) {
for (const char of value) if (char.charCodeAt(0) > 127) return true;
return false;
}
/** Normalizes a tracked slug without accepting traversal or path separators. */
function normalizeTrackedSkillSlug(raw) {
const slug = raw.trim();
if (!slug || slug.includes("/") || slug.includes("\\") || slug.includes("..")) throw new Error(`Invalid skill slug: ${raw}`);
return slug;
}
function validateRequestedSkillSlug(raw) {
const slug = normalizeTrackedSkillSlug(raw);
if (hasNonAscii(slug) || !VALID_SLUG_PATTERN.test(slug)) throw new Error(`Invalid skill slug: ${raw}`);
return slug;
}
function resolveWorkspaceSkillInstallDir(workspaceDir, slug) {
const target = resolveSafeInstallDir({
baseDir: path.join(path.resolve(workspaceDir), "skills"),
id: slug,
invalidNameMessage: "invalid skill target path"
});
if (!target.ok) throw new Error(target.error);
return target.path;
}
function installFailure(error, failureKind) {
return {
ok: false,
error,
failureKind
};
}
async function hasSkillArchiveRoot(rootDir, rootMarkers) {
for (const candidate of rootMarkers) if (await pathExists(path.join(rootDir, candidate))) return true;
return false;
}
function scanBlockedFailureKind(blocked) {
return blocked.code === "security_scan_failed" ? "unavailable" : "invalid-request";
}
const TRANSIENT_ARCHIVE_ERROR_PATTERNS = [
"enoent",
"enospc",
"eio",
"eacces",
"eperm",
"ebusy",
"emfile",
"enfile",
"timeout",
"timed out"
];
function archiveFailureKind(error) {
const lower = error.toLowerCase();
if (lower.startsWith("failed to install skill:")) return "unavailable";
for (const pattern of TRANSIENT_ARCHIVE_ERROR_PATTERNS) if (lower.includes(pattern)) return "unavailable";
return "invalid-request";
}
async function installExtractedSkillRoot(params) {
try {
if (!await hasSkillArchiveRoot(params.extractedRoot, params.rootMarkers ?? DEFAULT_SKILL_ARCHIVE_ROOT_MARKERS)) return installFailure("archive is missing SKILL.md", "invalid-request");
let targetDir;
try {
targetDir = resolveWorkspaceSkillInstallDir(params.workspaceDir, params.slug);
} catch (err) {
return installFailure(formatErrorMessage(err), "invalid-request");
}
const targetExists = await pathExists(targetDir);
const effectiveMode = params.mode === "update" && targetExists ? "update" : "install";
if (params.mode === "install" && targetExists) return installFailure(`Skill already exists at ${targetDir}. Re-run with force/update.`, "invalid-request");
if (params.policy) {
const scanResult = await evaluateSkillInstallPolicy({
config: params.policy.config,
installId: params.policy.installId ?? "archive",
logger: params.logger ?? {},
origin: params.policy.origin,
requestedSpecifier: params.policy.requestedSpecifier,
source: params.policy.source,
mode: effectiveMode,
skillName: params.slug,
sourceDir: params.extractedRoot
});
if (scanResult?.blocked) return installFailure(scanResult.blocked.reason, scanBlockedFailureKind(scanResult.blocked));
}
const install = await installPackageDir({
sourceDir: params.extractedRoot,
targetDir,
mode: effectiveMode,
timeoutMs: params.timeoutMs ?? 12e4,
logger: params.logger,
copyErrorPrefix: "failed to install skill",
hasDeps: false,
depsLogMessage: ""
});
if (!install.ok) return installFailure(install.error, "unavailable");
return {
ok: true,
targetDir
};
} catch (err) {
return installFailure(formatErrorMessage(err), "unavailable");
}
}
async function installSkillArchiveFromPath(params) {
const result = await withExtractedArchiveRoot({
archivePath: params.archivePath,
tempDirPrefix: "openclaw-skill-archive-",
timeoutMs: params.timeoutMs ?? 12e4,
logger: params.logger,
rootMarkers: ["SKILL.md"],
onExtracted: async (rootDir) => await installExtractedSkillRoot({
workspaceDir: params.workspaceDir,
slug: params.slug,
extractedRoot: rootDir,
mode: params.force ? "update" : "install",
timeoutMs: params.timeoutMs,
logger: params.logger,
policy: params.policy
})
});
if (!result.ok) {
const error = result.error.includes("unexpected archive layout") ? "archive is missing SKILL.md" : result.error;
return installFailure(error, "failureKind" in result && (result.failureKind === "invalid-request" || result.failureKind === "unavailable") ? result.failureKind : archiveFailureKind(error));
}
return result;
}
//#endregion
//#region src/skills/lifecycle/clawhub.ts
const DOT_DIR = ".clawhub";
const LEGACY_DOT_DIR = ".clawdhub";
const SKILL_ORIGIN_RELATIVE_PATH = path.join(DOT_DIR, "origin.json");
const LOCAL_SKILL_CARD_FILENAME = "skill-card.md";
const LOCAL_SKILL_CARD_MAX_BYTES = 256 * 1024;
async function resolveRequestedUpdateSlug(params) {
const trackedSlug = normalizeTrackedSkillSlug(params.requestedSlug);
if (await readClawHubSkillOrigin(resolveWorkspaceSkillInstallDir(params.workspaceDir, trackedSlug)) || params.lock.skills[trackedSlug]) return trackedSlug;
return validateRequestedSkillSlug(params.requestedSlug);
}
async function readClawHubSkillsLockfile(workspaceDir) {
const candidates = [path.join(workspaceDir, DOT_DIR, "lock.json"), path.join(workspaceDir, LEGACY_DOT_DIR, "lock.json")];
for (const candidate of candidates) try {
const raw = await tryReadJson(candidate);
if (raw?.version === 1 && raw.skills && typeof raw.skills === "object") return {
version: 1,
skills: raw.skills
};
} catch {}
return {
version: 1,
skills: {}
};
}
async function writeClawHubSkillsLockfile(workspaceDir, lockfile) {
await writeJson(path.join(workspaceDir, DOT_DIR, "lock.json"), lockfile, { trailingNewline: true });
}
function readJsonIfExistsSync(candidate) {
try {
return {
exists: true,
value: JSON.parse(fs.readFileSync(candidate, "utf8"))
};
} catch (err) {
if (err && typeof err === "object" && "code" in err && err.code === "ENOENT") return { exists: false };
throw err;
}
}
function normalizeStoredRegistry(registry) {
const trimmed = registry.trim();
return trimmed.replace(/\/+$/, "") || trimmed;
}
function readRealPathSync(candidate) {
try {
return fs.realpathSync.native(candidate);
} catch {
return;
}
}
function readClawHubSkillsLockfileStatusSync(workspaceDir) {
const candidates = [path.join(workspaceDir, DOT_DIR, "lock.json"), path.join(workspaceDir, LEGACY_DOT_DIR, "lock.json")];
for (const candidate of candidates) {
let raw;
try {
const read = readJsonIfExistsSync(candidate);
if (!read.exists) continue;
raw = read.value;
} catch (err) {
return {
kind: "malformed",
path: candidate,
error: formatErrorMessage(err)
};
}
if (raw?.version === 1 && raw.skills && typeof raw.skills === "object") return {
kind: "found",
path: candidate,
lock: {
version: 1,
skills: raw.skills
}
};
return {
kind: "malformed",
path: candidate,
error: "expected version 1 lockfile with skills"
};
}
return { kind: "missing" };
}
function normalizeOptionalSelector(value) {
const trimmed = value?.trim();
return trimmed ? trimmed : void 0;
}
function normalizeClawHubSkillOrigin(raw) {
if (raw?.version === 1 && typeof raw.registry === "string" && raw.registry.trim().length > 0 && typeof raw.slug === "string" && raw.slug.trim().length > 0 && typeof raw.installedVersion === "string" && raw.installedVersion.trim().length > 0 && typeof raw.installedAt === "number") return {
version: 1,
registry: normalizeStoredRegistry(raw.registry),
slug: raw.slug,
installedVersion: raw.installedVersion,
installedAt: raw.installedAt
};
return null;
}
async function readClawHubSkillOrigin(skillDir) {
const candidates = [path.join(skillDir, DOT_DIR, "origin.json"), path.join(skillDir, LEGACY_DOT_DIR, "origin.json")];
for (const candidate of candidates) try {
const origin = normalizeClawHubSkillOrigin(await tryReadJson(candidate));
if (origin) return origin;
} catch {}
return null;
}
function readClawHubSkillOriginStatusSync(skillDir) {
const candidates = [path.join(skillDir, DOT_DIR, "origin.json"), path.join(skillDir, LEGACY_DOT_DIR, "origin.json")];
for (const candidate of candidates) {
let raw;
try {
const read = readJsonIfExistsSync(candidate);
if (!read.exists) continue;
raw = read.value;
} catch (err) {
return {
kind: "malformed",
path: candidate,
error: formatErrorMessage(err)
};
}
const origin = normalizeClawHubSkillOrigin(raw);
if (origin) return {
kind: "found",
origin,
path: candidate
};
return {
kind: "malformed",
path: candidate,
error: "expected version 1 origin with registry, slug, installedVersion, and installedAt"
};
}
return { kind: "missing" };
}
async function readClawHubSkillOriginStrict(skillDir) {
const candidates = [path.join(skillDir, DOT_DIR, "origin.json"), path.join(skillDir, LEGACY_DOT_DIR, "origin.json")];
for (const candidate of candidates) {
let raw;
try {
raw = await readJsonIfExists(candidate);
} catch (err) {
return {
kind: "malformed",
path: candidate,
error: formatErrorMessage(err)
};
}
if (!raw) continue;
const origin = normalizeClawHubSkillOrigin(raw);
if (origin) return {
kind: "found",
origin,
path: candidate
};
return {
kind: "malformed",
path: candidate,
error: "expected version 1 origin with registry, slug, installedVersion, and installedAt"
};
}
return { kind: "missing" };
}
function resolveClawHubSkillStatusLinkSync(params) {
const originRead = readClawHubSkillOriginStatusSync(params.skillDir);
const lockRead = params.lockRead ?? readClawHubSkillsLockfileStatusSync(params.workspaceDir);
if (originRead.kind === "missing") {
let trackedSlug;
try {
trackedSlug = normalizeTrackedSkillSlug(params.skillKey);
} catch {
return;
}
const locked = lockRead.kind === "found" ? lockRead.lock.skills[trackedSlug] : void 0;
if (!locked) return;
return {
status: "invalid",
valid: false,
reason: `Skill "${trackedSlug}" is tracked by the workspace ClawHub lockfile but is missing local ClawHub origin metadata.`,
slug: trackedSlug,
installedVersion: locked.version,
installedAt: locked.installedAt,
registry: normalizeStoredRegistry(locked.registry ?? resolveClawHubBaseUrl()),
lockPath: lockRead.kind === "found" ? lockRead.path : void 0
};
}
if (originRead.kind === "malformed") return {
status: "invalid",
valid: false,
reason: `Malformed ClawHub origin metadata at ${originRead.path}: ${originRead.error}`,
originPath: originRead.path,
lockPath: lockRead.kind === "found" ? lockRead.path : void 0
};
let trackedSlug;
try {
trackedSlug = normalizeTrackedSkillSlug(originRead.origin.slug);
} catch (err) {
return {
status: "invalid",
valid: false,
reason: `Invalid ClawHub origin slug "${originRead.origin.slug}": ${formatErrorMessage(err)}`,
registry: originRead.origin.registry,
slug: originRead.origin.slug,
installedVersion: originRead.origin.installedVersion,
installedAt: originRead.origin.installedAt,
originPath: originRead.path,
lockPath: lockRead.kind === "found" ? lockRead.path : void 0
};
}
if (lockRead.kind === "missing") return {
status: "invalid",
valid: false,
reason: `Skill "${trackedSlug}" has ClawHub origin metadata but is not tracked by the workspace ClawHub lockfile.`,
registry: originRead.origin.registry,
slug: trackedSlug,
installedVersion: originRead.origin.installedVersion,
installedAt: originRead.origin.installedAt,
originPath: originRead.path
};
if (lockRead.kind === "malformed") return {
status: "invalid",
valid: false,
reason: `Malformed workspace ClawHub lockfile at ${lockRead.path}: ${lockRead.error}`,
registry: originRead.origin.registry,
slug: trackedSlug,
installedVersion: originRead.origin.installedVersion,
installedAt: originRead.origin.installedAt,
originPath: originRead.path,
lockPath: lockRead.path
};
const locked = lockRead.lock.skills[trackedSlug];
if (!locked) return {
status: "invalid",
valid: false,
reason: `Skill "${trackedSlug}" has ClawHub origin metadata but is not tracked by the workspace ClawHub lockfile.`,
registry: originRead.origin.registry,
slug: trackedSlug,
installedVersion: originRead.origin.installedVersion,
installedAt: originRead.origin.installedAt,
originPath: originRead.path,
lockPath: lockRead.path
};
const expectedSkillDirRealPath = readRealPathSync(resolveWorkspaceSkillInstallDir(params.workspaceDir, trackedSlug));
const actualSkillDirRealPath = readRealPathSync(params.skillDir);
if (!expectedSkillDirRealPath || actualSkillDirRealPath !== expectedSkillDirRealPath) return {
status: "invalid",
valid: false,
reason: `Skill "${trackedSlug}" ClawHub origin metadata is not in the expected ClawHub install directory.`,
registry: originRead.origin.registry,
slug: trackedSlug,
installedVersion: originRead.origin.installedVersion,
installedAt: originRead.origin.installedAt,
originPath: originRead.path,
lockPath: lockRead.path
};
const originRegistry = normalizeStoredRegistry(originRead.origin.registry);
const lockedRegistry = locked.registry === void 0 ? originRegistry : normalizeStoredRegistry(locked.registry);
if (locked.version !== originRead.origin.installedVersion || locked.installedAt !== originRead.origin.installedAt || lockedRegistry !== originRegistry) return {
status: "invalid",
valid: false,
reason: `Skill "${trackedSlug}" ClawHub origin metadata does not match the workspace ClawHub lockfile.`,
registry: lockedRegistry,
slug: trackedSlug,
installedVersion: originRead.origin.installedVersion,
installedAt: originRead.origin.installedAt,
originPath: originRead.path,
lockPath: lockRead.path
};
return {
status: "linked",
valid: true,
registry: lockedRegistry,
slug: trackedSlug,
installedVersion: locked.version,
installedAt: locked.installedAt,
originPath: originRead.path,
lockPath: lockRead.path
};
}
function resolveLocalSkillCardStatusSync(skillDir) {
return readLocalSkillCardSync(skillDir);
}
function isPathInsideDir(child, parent) {
const relative = path.relative(parent, child);
return relative === "" || relative.length > 0 && !relative.startsWith("..") && !path.isAbsolute(relative);
}
function readLocalSkillCardSync(skillDir, includeContent = false) {
const cardPath = path.join(skillDir, LOCAL_SKILL_CARD_FILENAME);
let lstat;
try {
lstat = fs.lstatSync(cardPath);
} catch (err) {
if (err && typeof err === "object" && "code" in err && err.code === "ENOENT") return;
return;
}
if (!lstat.isFile() || lstat.size > LOCAL_SKILL_CARD_MAX_BYTES) return;
let fd;
try {
const rootRealPath = fs.realpathSync.native(skillDir);
if (!isPathInsideDir(fs.realpathSync.native(cardPath), rootRealPath)) return;
const noFollowFlag = fs.constants.O_NOFOLLOW ?? 0;
fd = fs.openSync(cardPath, fs.constants.O_RDONLY | noFollowFlag);
const fdStat = fs.fstatSync(fd);
if (!fdStat.isFile() || fdStat.size > LOCAL_SKILL_CARD_MAX_BYTES) return;
const result = {
present: true,
path: cardPath,
sizeBytes: fdStat.size
};
if (includeContent) result.content = fs.readFileSync(fd, "utf8");
return result;
} catch {
return;
} finally {
if (fd !== void 0) try {
fs.closeSync(fd);
} catch {}
}
}
function readLocalSkillCardContentSync(skillDir) {
return readLocalSkillCardSync(skillDir, true)?.content;
}
async function writeClawHubSkillOrigin(skillDir, origin) {
await writeJson(path.join(skillDir, SKILL_ORIGIN_RELATIVE_PATH), origin, { trailingNewline: true });
}
async function searchSkillsFromClawHub(params) {
return await searchClawHubSkills({
query: params.query?.trim() || "*",
limit: params.limit,
baseUrl: params.baseUrl
});
}
async function resolveClawHubSkillVerificationTarget(params) {
try {
const version = normalizeOptionalSelector(params.version);
const tag = normalizeOptionalSelector(params.tag);
if (version && tag) return {
ok: false,
error: "Use either --version or --tag."
};
const trackedSlug = normalizeTrackedSkillSlug(params.slug);
const skillDir = resolveWorkspaceSkillInstallDir(params.workspaceDir, trackedSlug);
const originRead = await readClawHubSkillOriginStrict(skillDir);
if (originRead.kind === "malformed") return {
ok: false,
error: `Malformed ClawHub origin metadata at ${originRead.path}: ${originRead.error}`
};
if (originRead.kind === "found") {
const locked = (await readClawHubSkillsLockfile(params.workspaceDir)).skills[trackedSlug];
if (!locked) return {
ok: false,
error: `Skill "${trackedSlug}" has ClawHub origin metadata but is not tracked by the workspace ClawHub lockfile. Reinstall it from ClawHub before verifying it as an installed ClawHub skill.`
};
if (normalizeTrackedSkillSlug(originRead.origin.slug) !== trackedSlug) return {
ok: false,
error: `Skill "${trackedSlug}" has ClawHub origin metadata for "${originRead.origin.slug}". Reinstall it from ClawHub before verifying it as an installed ClawHub skill.`
};
const originRegistry = normalizeStoredRegistry(originRead.origin.registry);
const lockedRegistry = locked.registry === void 0 ? originRegistry : normalizeStoredRegistry(locked.registry);
if (locked.version !== originRead.origin.installedVersion || locked.installedAt !== originRead.origin.installedAt || lockedRegistry !== originRegistry) return {
ok: false,
error: `Skill "${trackedSlug}" ClawHub origin metadata does not match the workspace ClawHub lockfile. Reinstall it from ClawHub before verifying it as an installed ClawHub skill.`
};
const selector = version ? "version" : tag ? "tag" : "installed-version";
return {
ok: true,
slug: trackedSlug,
baseUrl: lockedRegistry,
version: version ?? (tag ? void 0 : locked.version),
tag,
resolution: {
source: "installed",
selector,
registry: lockedRegistry,
skillDir,
installedVersion: locked.version
}
};
}
const lockRead = readClawHubSkillsLockfileStatusSync(params.workspaceDir);
if (lockRead.kind === "malformed") return {
ok: false,
error: `Malformed workspace ClawHub lockfile at ${lockRead.path}: ${lockRead.error}`
};
if (lockRead.kind === "found" && lockRead.lock.skills[trackedSlug]) return {
ok: false,
error: `Skill "${trackedSlug}" is tracked by the workspace ClawHub lockfile but is missing ClawHub origin metadata. Reinstall it from ClawHub before verifying it as an installed ClawHub skill.`
};
const slug = validateRequestedSkillSlug(params.slug);
const registry = resolveClawHubBaseUrl(params.baseUrl);
return {
ok: true,
slug,
baseUrl: registry,
version,
tag,
resolution: {
source: "registry",
selector: version ? "version" : tag ? "tag" : "latest",
registry,
skillDir: void 0,
installedVersion: void 0
}
};
} catch (err) {
return {
ok: false,
error: formatErrorMessage(err)
};
}
}
async function resolveInstallVersion(params) {
const detail = await fetchClawHubSkillDetail({
slug: params.slug,
baseUrl: params.baseUrl
});
if (!detail.skill) throw new Error(`Skill "${params.slug}" not found on ClawHub.`);
const resolvedVersion = params.version ?? detail.latestVersion?.version;
if (!resolvedVersion) throw new Error(`Skill "${params.slug}" has no installable version.`);
return {
detail,
version: resolvedVersion
};
}
function normalizeGitHubSourcePath(raw) {
const parts = raw.replaceAll("\\", "/").split("/").filter(Boolean);
if (parts.length === 0 || parts.some((part) => part === "." || part === "..")) throw new Error(`Invalid GitHub skill source path: ${raw}`);
return parts.join("/");
}
function resolveGitHubSkillSourceDir(repoRoot, sourcePath) {
const normalized = normalizeGitHubSourcePath(sourcePath);
return path.join(repoRoot, ...normalized.split("/"));
}
async function installArchiveResolution(params) {
return await withExtractedArchiveRoot({
archivePath: params.archivePath,
tempDirPrefix: "openclaw-skill-clawhub-",
timeoutMs: 12e4,
rootMarkers: CLAWHUB_SKILL_ARCHIVE_ROOT_MARKERS,
onExtracted: async (rootDir) => await installExtractedSkillRoot({
workspaceDir: params.workspaceDir,
slug: params.slug,
extractedRoot: rootDir,
mode: params.force ? "update" : "install",
logger: params.logger,
policy: {
config: params.config,
installId: "clawhub",
origin: {
type: "clawhub",
registry: params.registry,
slug: params.slug,
version: params.version
},
source: {
kind: "clawhub",
authority: params.authority,
mutable: false,
network: true
},
requestedSpecifier: `clawhub:${params.slug}@${params.version}`
},
rootMarkers: CLAWHUB_SKILL_ARCHIVE_ROOT_MARKERS
})
});
}
async function installGitHubResolution(params) {
return await withExtractedArchiveRoot({
archivePath: params.archivePath,
tempDirPrefix: "openclaw-skill-clawhub-github-",
timeoutMs: 12e4,
onExtracted: async (repoRoot) => await installExtractedSkillRoot({
workspaceDir: params.workspaceDir,
slug: params.slug,
extractedRoot: resolveGitHubSkillSourceDir(repoRoot, params.sourcePath),
mode: params.force ? "update" : "install",
logger: params.logger,
policy: {
config: params.config,
installId: "clawhub",
origin: {
type: "clawhub",
registry: params.registry,
slug: params.slug,
version: params.commit,
repo: params.repo,
path: params.sourcePath,
commit: params.commit
},
source: {
kind: "git",
authority: "third-party",
mutable: false,
network: true
},
requestedSpecifier: `clawhub:${params.slug}@${params.commit}`
},
rootMarkers: CLAWHUB_SKILL_ARCHIVE_ROOT_MARKERS
})
});
}
function assertInstallResolutionAllowed(resolution) {
if (resolution.ok) return resolution;
throw new Error(resolution.message || `Skill "${resolution.slug}" is not installable.`);
}
async function performClawHubSkillInstall(params) {
try {
const targetDir = resolveWorkspaceSkillInstallDir(params.workspaceDir, params.slug);
const registry = resolveClawHubBaseUrl(params.baseUrl);
const clawhubAuthority = isDefaultClawHubBaseUrl(params.baseUrl) ? "openclaw" : "third-party";
if (!params.force && await pathExists(targetDir)) return {
ok: false,
error: `Skill already exists at ${targetDir}. Re-run with force/update.`
};
let version;
let detail;
let latestResolution;
let install;
const archive = params.version ? await (async () => {
const resolved = await resolveInstallVersion({
slug: params.slug,
version: params.version,
baseUrl: params.baseUrl
});
detail = resolved.detail;
version = resolved.version;
params.logger?.info?.(`Downloading ${params.slug}@${version} from ClawHub…`);
return await downloadClawHubSkillArchive({
slug: params.slug,
version,
baseUrl: params.baseUrl
});
})() : await (async () => {
latestResolution = assertInstallResolutionAllowed(await fetchClawHubSkillInstallResolution({
slug: params.slug,
baseUrl: params.baseUrl,
forceInstall: params.forceInstall
}));
if (latestResolution.installKind === "github") {
version = latestResolution.github.commit;
params.logger?.info?.(`Downloading ${params.slug}@${version} from GitHub…`);
return await downloadClawHubGitHubSkillArchive({
repo: latestResolution.github.repo,
commit: latestResolution.github.commit
});
}
version = latestResolution.archive.version;
params.logger?.info?.(`Downloading ${params.slug}@${version} from ClawHub…`);
return await downloadClawHubSkillArchiveUrl({
url: latestResolution.archive.downloadUrl,
baseUrl: params.baseUrl
});
})();
try {
if (!params.version) {
if (!latestResolution) throw new Error(`Skill "${params.slug}" has no install resolution.`);
install = latestResolution.installKind === "github" ? await installGitHubResolution({
workspaceDir: params.workspaceDir,
slug: params.slug,
sourcePath: latestResolution.github.path,
archivePath: archive.archivePath,
registry,
repo: latestResolution.github.repo,
commit: latestResolution.github.commit,
force: params.force,
logger: params.logger,
config: params.config
}) : await installArchiveResolution({
workspaceDir: params.workspaceDir,
slug: params.slug,
version,
archivePath: archive.archivePath,
registry,
authority: clawhubAuthority,
force: params.force,
logger: params.logger,
config: params.config
});
} else install = await installArchiveResolution({
workspaceDir: params.workspaceDir,
slug: params.slug,
version,
archivePath: archive.archivePath,
registry,
authority: clawhubAuthority,
force: params.force,
logger: params.logger,
config: params.config
});
if (!install.ok) return {
ok: false,
error: install.error
};
const installedAt = Date.now();
await writeClawHubSkillOrigin(install.targetDir, {
version: 1,
registry: resolveClawHubBaseUrl(params.baseUrl),
slug: params.slug,
installedVersion: version,
installedAt
});
const lock = await readClawHubSkillsLockfile(params.workspaceDir);
lock.skills[params.slug] = {
version,
installedAt,
registry: resolveClawHubBaseUrl(params.baseUrl)
};
await writeClawHubSkillsLockfile(params.workspaceDir, lock);
await reportClawHubSkillInstallTelemetry({
baseUrl: params.baseUrl,
root: params.workspaceDir,
skills: lock.skills
}).catch(() => void 0);
return {
ok: true,
slug: params.slug,
version,
targetDir: install.targetDir,
...detail ? { detail } : {}
};
} finally {
await archive.cleanup().catch(() => void 0);
}
} catch (err) {
return {
ok: false,
error: formatErrorMessage(err)
};
}
}
async function installRequestedSkillFromClawHub(params) {
try {
return await performClawHubSkillInstall({
...params,
slug: validateRequestedSkillSlug(params.slug)
});
} catch (err) {
return {
ok: false,
error: formatErrorMessage(err)
};
}
}
async function installTrackedSkillFromClawHub(params) {
try {
return await performClawHubSkillInstall({
...params,
slug: normalizeTrackedSkillSlug(params.slug)
});
} catch (err) {
return {
ok: false,
error: formatErrorMessage(err)
};
}
}
async function resolveTrackedUpdateTarget(params) {
const origin = await readClawHubSkillOrigin(resolveWorkspaceSkillInstallDir(params.workspaceDir, params.slug)) ?? null;
if (!origin && !params.lock.skills[params.slug]) return {
ok: false,
slug: params.slug,
error: `Skill "${params.slug}" is not tracked as a ClawHub install.`
};
return {
ok: true,
slug: params.slug,
baseUrl: origin?.registry ?? params.baseUrl,
previousVersion: origin?.installedVersion ?? params.lock.skills[params.slug]?.version ?? null
};
}
async function installSkillFromClawHub(params) {
return await installRequestedSkillFromClawHub(params);
}
async function updateSkillsFromClawHub(params) {
const lock = await readClawHubSkillsLockfile(params.workspaceDir);
const slugs = params.slug ? [await resolveRequestedUpdateSlug({
workspaceDir: params.workspaceDir,
requestedSlug: params.slug,
lock
})] : Object.keys(lock.skills).map((slug) => normalizeTrackedSkillSlug(slug));
const results = [];
for (const slug of slugs) {
const tracked = await resolveTrackedUpdateTarget({
workspaceDir: params.workspaceDir,
slug,
lock,
baseUrl: params.baseUrl
});
if (!tracked.ok) {
results.push({
ok: false,
error: tracked.error
});
continue;
}
const install = await installTrackedSkillFromClawHub({
workspaceDir: params.workspaceDir,
slug: tracked.slug,
baseUrl: tracked.baseUrl,
force: true,
forceInstall: params.forceInstall,
logger: params.logger,
config: params.config
});
if (!install.ok) {
results.push(install);
continue;
}
results.push({
ok: true,
slug: tracked.slug,
previousVersion: tracked.previousVersion,
version: install.version,
changed: tracked.previousVersion !== install.version,
targetDir: install.targetDir
});
}
return results;
}
async function readTrackedClawHubSkillSlugs(workspaceDir) {
const lock = await readClawHubSkillsLockfile(workspaceDir);
return Object.keys(lock.skills).toSorted();
}
async function untrackClawHubSkill(workspaceDir, slug) {
const trackedSlug = normalizeTrackedSkillSlug(slug);
const lock = await readClawHubSkillsLockfile(workspaceDir);
if (!lock.skills[trackedSlug]) return;
delete lock.skills[trackedSlug];
await writeClawHubSkillsLockfile(workspaceDir, lock);
}
//#endregion
//#region src/skills/loading/bundled-context.ts
const skillsLogger = createSubsystemLogger("skills");
let hasWarnedMissingBundledDir = false;
let cachedBundledContext = null;
function resolveBundledSkillsContext(opts = {}) {
const dir = resolveBundledSkillsDir(opts);
const names = /* @__PURE__ */ new Set();
if (!dir) {
if (!hasWarnedMissingBundledDir) {
hasWarnedMissingBundledDir = true;
skillsLogger.warn("Bundled skills directory could not be resolved; built-in skills may be missing.");
}
return {
dir,
names
};
}
if (cachedBundledContext?.dir === dir) return {
dir,
names: new Set(cachedBundledContext.names)
};
const result = loadSkillsFromDirSafe({
dir,
source: "openclaw-bundled"
});
for (const skill of result.skills) if (skill.name.trim()) names.add(skill.name);
cachedBundledContext = {
dir,
names: new Set(names)
};
return {
dir,
names
};
}
//#endregion
//#region src/skills/discovery/status.ts
function resolveSkillStatusEntry(skills, requestedName) {
const raw = requestedName.trim();
if (!raw) return null;
const lower = raw.toLowerCase();
const normalized = normalizeSkillIndexName(raw);
let caseInsensitiveMatch = null;
let caseInsensitiveMatches = 0;
let normalizedMatch = null;
let normalizedMatches = 0;
for (const skill of skills) {
if (skill.name === raw || skill.skillKey === raw) return skill;
const nameLower = skill.name.toLowerCase();
const keyLower = skill.skillKey.toLowerCase();
if (nameLower === lower || keyLower === lower) {
caseInsensitiveMatch = skill;
caseInsensitiveMatches += 1;
continue;
}
if (normalized && (normalizeSkillIndexName(skill.name) === normalized || normalizeSkillIndexName(skill.skillKey) === normalized)) {
normalizedMatch = skill;
normalizedMatches += 1;
}
}
if (caseInsensitiveMatches > 1) return null;
if (caseInsensitiveMatches === 1) return caseInsensitiveMatch;
if (normalizedMatches === 1) return normalizedMatch;
return null;
}
function selectPreferredInstallSpec(install, prefs) {
if (install.length === 0) return;
const indexed = install.map((spec, index) => ({
spec,
index
}));
const findKind = (kind) => indexed.find((item) => item.spec.kind === kind);
const brewSpec = findKind("brew");
const nodeSpec = findKind("node");
const goSpec = findKind("go");
const uvSpec = findKind("uv");
const downloadSpec = findKind("download");
const brewAvailable = hasBinary("brew");
const pickers = [
() => prefs.preferBrew && brewAvailable ? brewSpec : void 0,
() => uvSpec,
() => nodeSpec,
() => brewAvailable ? brewSpec : void 0,
() => goSpec,
() => downloadSpec,
() => brewSpec,
() => indexed[0]
];
for (const pick of pickers) {
const selected = pick();
if (selected) return selected;
}
}
function normalizeInstallOptions(entry, prefs) {
const requiredOs = entry.metadata?.os ?? [];
if (requiredOs.length > 0 && !requiredOs.includes(process.platform)) return [];
const install = entry.metadata?.install ?? [];
if (install.length === 0) return [];
const platform = process.platform;
const filtered = install.filter((spec) => {
const osList = spec.os ?? [];
return osList.length === 0 || osList.includes(platform);
});
if (filtered.length === 0) return [];
const toOption = (spec, index) => {
const id = (spec.id ?? `${spec.kind}-${index}`).trim();
const bins = spec.bins ?? [];
let label = (spec.label ?? "").trim();
if (spec.kind === "node" && spec.package) label = `Install ${spec.package} (${prefs.nodeManager})`;
if (!label) if (spec.kind === "brew" && spec.formula) label = `Install ${spec.formula} (brew)`;
else if (spec.kind === "node" && spec.package) label = `Install ${spec.package} (${prefs.nodeManager})`;
else if (spec.kind === "go" && spec.module) label = `Install ${spec.module} (go)`;
else if (spec.kind === "uv" && spec.package) label = `Install ${spec.package} (uv)`;
else if (spec.kind === "download" && spec.url) {
const url = spec.url.trim();
const last = url.split("/").pop();
label = `Download ${last && last.length > 0 ? last : url}`;
} else label = "Run installer";
return {
id,
kind: spec.kind,
label,
bins
};
};
if (filtered.every((spec) => spec.kind === "download")) return filtered.map((spec, index) => toOption(spec, index));
const preferred = selectPreferredInstallSpec(filtered, prefs);
if (!preferred) return [];
return [toOption(preferred.spec, preferred.index)];
}
function buildSkillStatus(indexed, context) {
const entry = indexed.entry;
const skillKey = indexed.skillKey;
const { config, prefs, eligibility, allowBundled, agentSkillFilter, workspaceDir } = context;
const skillConfig = resolveSkillConfig(config, skillKey);
const disabled = skillConfig?.enabled === false;
const blockedByAllowlist = !isBundledSkillAllowed(entry, allowBundled);
const blockedByAgentFilter = agentSkillFilter !== void 0 && !indexed.agentAllowed;
const always = entry.metadata?.always === true;
const isEnvSatisfied = (envName) => Boolean(process.env[envName] || skillConfig?.env?.[envName] || skillConfig?.apiKey && entry.metadata?.primaryEnv === envName);
const isConfigSatisfied = (pathStr) => isConfigPathTruthy(config, pathStr);
const skillSource = indexed.source;
const bundled = indexed.bundled;
const { emoji, homepage, required, missing, requirementsSatisfied, configChecks } = evaluateEntryRequirementsForCurrentPlatform({
always,
entry,
hasLocalBin: hasBinary,
remote: eligibility?.remote,
isEnvSatisfied,
isConfigSatisfied
});
const eligible = !disabled && !blockedByAllowlist && requirementsSatisfied;
const availableToAgent = eligible && !blockedByAgentFilter;
const userInvocable = indexed.userInvocable;
const clawhub = workspaceDir && !bundled ? resolveClawHubSkillStatusLinkSync({
workspaceDir,
skillDir: entry.skill.baseDir,
skillKey,
lockRead: context.clawhubLockRead
}) : void 0;
const skillCard = resolveLocalSkillCardStatusSync(entry.skill.baseDir);
return {
name: entry.skill.name,
description: entry.skill.description,
source: skillSource,
bundled,
filePath: entry.skill.filePath,
baseDir: entry.skill.baseDir,
skillKey,
primaryEnv: entry.metadata?.primaryEnv,
emoji,
homepage,
always,
disabled,
blockedByAllowlist,
blockedByAgentFilter,
eligible,
modelVisible: availableToAgent && indexed.promptVisible,
userInvocable,
commandVisible: availableToAgent && userInvocable,
requirements: required,
missing,
configChecks,
install: normalizeInstallOptions(entry, prefs),
...clawhub ? { clawhub } : {},
...skillCard ? { skillCard } : {}
};
}
function buildWorkspaceSkillStatus(workspaceDir, opts) {
const managedSkillsDir = opts?.managedSkillsDir ?? path.join(CONFIG_DIR, "skills");
const bundledContext = resolveBundledSkillsContext();
const agentSkillFilter = opts?.agentId ? resolveEffectiveAgentSkillFilter(opts.config, opts.agentId) : void 0;
const skillEntries = opts?.entries ?? loadWorkspaceSkillEntries(workspaceDir, {
config: opts?.config,
managedSkillsDir,
bundledSkillsDir: bundledContext.dir
});
const prefs = resolveSkillsInstallPreferences(opts?.config);
const allowBundled = resolveBundledAllowlist(opts?.config);
const clawhubLockRead = readClawHubSkillsLockfileStatusSync(workspaceDir);
const skillIndexEntries = buildSkillIndexEntries(skillEntries, {
bundledNames: bundledContext.names,
agentSkillFilter
});
return {
workspaceDir,
managedSkillsDir,
agentId: opts?.agentId,
agentSkillFilter,
skills: skillIndexEntries.map((entry) => buildSkillStatus(entry, {
config: opts?.config,
prefs,
eligibility: opts?.eligibility,
allowBundled,
agentSkillFilter,
workspaceDir,
clawhubLockRead
}))
};
}
//#endregion
export { readTrackedClawHubSkillSlugs as a, untrackClawHubSkill as c, installSkillArchiveFromPath as d, validateRequestedSkillSlug as f, readLocalSkillCardContentSync as i, updateSkillsFromClawHub as l, resolveSkillStatusEntry as n, resolveClawHubSkillVerificationTarget as o, installSkillFromClawHub as r, searchSkillsFromClawHub as s, buildWorkspaceSkillStatus as t, installExtractedSkillRoot as u };