@gguf/claw
Version:
Multi-channel AI gateway with extensible messaging integrations
580 lines (575 loc) • 18.5 kB
JavaScript
import { c as ensureDir, g as resolveConfigDir, y as resolveUserPath } from "./utils-CP9YLh6M.js";
import { t as runCommandWithTimeout } from "./exec-DYqRzFbo.js";
import { d as hasBinary } from "./frontmatter-6HZ0_y2V.js";
import { i as loadWorkspaceSkillEntries, t as resolveSkillsInstallPreferences, x as resolveSkillKey } from "./skills-CLmzWt48.js";
import { t as fetchWithSsrFGuard } from "./fetch-guard-C1sYsWgl.js";
import { t as resolveBrewExecutable } from "./brew-Cs0Ihax8.js";
import { t as isWithinDir } from "./path-safety-DVHEe0sD.js";
import { a as extractArchive$1, d as resolveArchiveOutputPath, f as stripArchivePath, p as validateArchiveEntryPath, r as safePathSegmentHashed, u as isWindowsDrivePath } from "./install-safe-path-DSS8Pu5f.js";
import { t as scanDirectoryWithSummary } from "./skill-scanner-D56xvwQF.js";
import fs from "node:fs";
import path from "node:path";
import { pipeline } from "node:stream/promises";
import { Readable } from "node:stream";
//#region src/agents/skills-install-output.ts
function summarizeInstallOutput(text) {
const raw = text.trim();
if (!raw) return;
const lines = raw.split("\n").map((line) => line.trim()).filter(Boolean);
if (lines.length === 0) return;
const preferred = lines.find((line) => /^error\b/i.test(line)) ?? lines.find((line) => /\b(err!|error:|failed)\b/i.test(line)) ?? lines.at(-1);
if (!preferred) return;
const normalized = preferred.replace(/\s+/g, " ").trim();
const maxLen = 200;
return normalized.length > maxLen ? `${normalized.slice(0, maxLen - 1)}…` : normalized;
}
function formatInstallFailureMessage(result) {
const code = typeof result.code === "number" ? `exit ${result.code}` : "unknown exit";
const summary = summarizeInstallOutput(result.stderr) ?? summarizeInstallOutput(result.stdout);
if (!summary) return `Install failed (${code})`;
return `Install failed (${code}): ${summary}`;
}
//#endregion
//#region src/agents/skills/tools-dir.ts
function resolveSkillToolsRootDir(entry) {
const safeKey = safePathSegmentHashed(resolveSkillKey(entry.skill, entry));
return path.join(resolveConfigDir(), "tools", safeKey);
}
//#endregion
//#region src/agents/skills-install-download.ts
function isNodeReadableStream(value) {
return Boolean(value && typeof value.pipe === "function");
}
function resolveDownloadTargetDir(entry, spec) {
const safeRoot = resolveSkillToolsRootDir(entry);
const raw = spec.targetDir?.trim();
if (!raw) return safeRoot;
const resolved = raw.startsWith("~") || path.isAbsolute(raw) || isWindowsDrivePath(raw) ? resolveUserPath(raw) : path.resolve(safeRoot, raw);
if (!isWithinDir(safeRoot, resolved)) throw new Error(`Refusing to install outside the skill tools directory. targetDir="${raw}" resolves to "${resolved}". Allowed root: "${safeRoot}".`);
return resolved;
}
function resolveArchiveType(spec, filename) {
const explicit = spec.archive?.trim().toLowerCase();
if (explicit) return explicit;
const lower = filename.toLowerCase();
if (lower.endsWith(".tar.gz") || lower.endsWith(".tgz")) return "tar.gz";
if (lower.endsWith(".tar.bz2") || lower.endsWith(".tbz2")) return "tar.bz2";
if (lower.endsWith(".zip")) return "zip";
}
async function downloadFile(url, destPath, timeoutMs) {
const { response, release } = await fetchWithSsrFGuard({
url,
timeoutMs: Math.max(1e3, timeoutMs)
});
try {
if (!response.ok || !response.body) throw new Error(`Download failed (${response.status} ${response.statusText})`);
await ensureDir(path.dirname(destPath));
const file = fs.createWriteStream(destPath);
const body = response.body;
await pipeline(isNodeReadableStream(body) ? body : Readable.fromWeb(body), file);
return { bytes: (await fs.promises.stat(destPath)).size };
} finally {
await release();
}
}
async function extractArchive(params) {
const { archivePath, archiveType, targetDir, stripComponents, timeoutMs } = params;
const strip = typeof stripComponents === "number" && Number.isFinite(stripComponents) ? Math.max(0, Math.floor(stripComponents)) : 0;
try {
if (archiveType === "zip") {
await extractArchive$1({
archivePath,
destDir: targetDir,
timeoutMs,
kind: "zip",
stripComponents: strip
});
return {
stdout: "",
stderr: "",
code: 0
};
}
if (archiveType === "tar.gz") {
await extractArchive$1({
archivePath,
destDir: targetDir,
timeoutMs,
kind: "tar",
stripComponents: strip,
tarGzip: true
});
return {
stdout: "",
stderr: "",
code: 0
};
}
if (archiveType === "tar.bz2") {
if (!hasBinary("tar")) return {
stdout: "",
stderr: "tar not found on PATH",
code: null
};
const listResult = await runCommandWithTimeout([
"tar",
"tf",
archivePath
], { timeoutMs });
if (listResult.code !== 0) return {
stdout: listResult.stdout,
stderr: listResult.stderr || "tar list failed",
code: listResult.code
};
const entries = listResult.stdout.split("\n").map((line) => line.trim()).filter(Boolean);
const verboseResult = await runCommandWithTimeout([
"tar",
"tvf",
archivePath
], { timeoutMs });
if (verboseResult.code !== 0) return {
stdout: verboseResult.stdout,
stderr: verboseResult.stderr || "tar verbose list failed",
code: verboseResult.code
};
for (const line of verboseResult.stdout.split("\n")) {
const trimmed = line.trim();
if (!trimmed) continue;
const typeChar = trimmed[0];
if (typeChar === "l" || typeChar === "h" || trimmed.includes(" -> ")) return {
stdout: verboseResult.stdout,
stderr: "tar archive contains link entries; refusing to extract for safety",
code: 1
};
}
for (const entry of entries) {
validateArchiveEntryPath(entry, { escapeLabel: "targetDir" });
const relPath = stripArchivePath(entry, strip);
if (!relPath) continue;
validateArchiveEntryPath(relPath, { escapeLabel: "targetDir" });
resolveArchiveOutputPath({
rootDir: targetDir,
relPath,
originalPath: entry,
escapeLabel: "targetDir"
});
}
const argv = [
"tar",
"xf",
archivePath,
"-C",
targetDir
];
if (strip > 0) argv.push("--strip-components", String(strip));
return await runCommandWithTimeout(argv, { timeoutMs });
}
return {
stdout: "",
stderr: `unsupported archive type: ${archiveType}`,
code: null
};
} catch (err) {
return {
stdout: "",
stderr: err instanceof Error ? err.message : String(err),
code: 1
};
}
}
async function installDownloadSpec(params) {
const { entry, spec, timeoutMs } = params;
const url = spec.url?.trim();
if (!url) return {
ok: false,
message: "missing download url",
stdout: "",
stderr: "",
code: null
};
let filename = "";
try {
const parsed = new URL(url);
filename = path.basename(parsed.pathname);
} catch {
filename = path.basename(url);
}
if (!filename) filename = "download";
let targetDir = "";
try {
targetDir = resolveDownloadTargetDir(entry, spec);
await ensureDir(targetDir);
const stat = await fs.promises.lstat(targetDir);
if (stat.isSymbolicLink()) throw new Error(`targetDir is a symlink: ${targetDir}`);
if (!stat.isDirectory()) throw new Error(`targetDir is not a directory: ${targetDir}`);
} catch (err) {
const message = err instanceof Error ? err.message : String(err);
return {
ok: false,
message,
stdout: "",
stderr: message,
code: null
};
}
const archivePath = path.join(targetDir, filename);
let downloaded = 0;
try {
downloaded = (await downloadFile(url, archivePath, timeoutMs)).bytes;
} catch (err) {
const message = err instanceof Error ? err.message : String(err);
return {
ok: false,
message,
stdout: "",
stderr: message,
code: null
};
}
const archiveType = resolveArchiveType(spec, filename);
if (!(spec.extract ?? Boolean(archiveType))) return {
ok: true,
message: `Downloaded to ${archivePath}`,
stdout: `downloaded=${downloaded}`,
stderr: "",
code: 0
};
if (!archiveType) return {
ok: false,
message: "extract requested but archive type could not be detected",
stdout: "",
stderr: "",
code: null
};
const extractResult = await extractArchive({
archivePath,
archiveType,
targetDir,
stripComponents: spec.stripComponents,
timeoutMs
});
const success = extractResult.code === 0;
return {
ok: success,
message: success ? `Downloaded and extracted to ${targetDir}` : formatInstallFailureMessage(extractResult),
stdout: extractResult.stdout.trim(),
stderr: extractResult.stderr.trim(),
code: extractResult.code
};
}
//#endregion
//#region src/agents/skills-install.ts
function withWarnings(result, warnings) {
if (warnings.length === 0) return result;
return {
...result,
warnings: warnings.slice()
};
}
function formatScanFindingDetail(rootDir, finding) {
const relativePath = path.relative(rootDir, finding.file);
const filePath = relativePath && relativePath !== "." && !relativePath.startsWith("..") ? relativePath : path.basename(finding.file);
return `${finding.message} (${filePath}:${finding.line})`;
}
async function collectSkillInstallScanWarnings(entry) {
const warnings = [];
const skillName = entry.skill.name;
const skillDir = path.resolve(entry.skill.baseDir);
try {
const summary = await scanDirectoryWithSummary(skillDir);
if (summary.critical > 0) {
const criticalDetails = summary.findings.filter((finding) => finding.severity === "critical").map((finding) => formatScanFindingDetail(skillDir, finding)).join("; ");
warnings.push(`WARNING: Skill "${skillName}" contains dangerous code patterns: ${criticalDetails}`);
} else if (summary.warn > 0) warnings.push(`Skill "${skillName}" has ${summary.warn} suspicious code pattern(s). Run "openclaw security audit --deep" for details.`);
} catch (err) {
warnings.push(`Skill "${skillName}" code safety scan failed (${String(err)}). Installation continues; run "openclaw security audit --deep" after install.`);
}
return warnings;
}
function resolveInstallId(spec, index) {
return (spec.id ?? `${spec.kind}-${index}`).trim();
}
function findInstallSpec(entry, installId) {
const specs = entry.metadata?.install ?? [];
for (const [index, spec] of specs.entries()) if (resolveInstallId(spec, index) === installId) return spec;
}
function buildNodeInstallCommand(packageName, prefs) {
switch (prefs.nodeManager) {
case "pnpm": return [
"pnpm",
"add",
"-g",
"--ignore-scripts",
packageName
];
case "yarn": return [
"yarn",
"global",
"add",
"--ignore-scripts",
packageName
];
case "bun": return [
"bun",
"add",
"-g",
"--ignore-scripts",
packageName
];
default: return [
"npm",
"install",
"-g",
"--ignore-scripts",
packageName
];
}
}
function buildInstallCommand(spec, prefs) {
switch (spec.kind) {
case "brew":
if (!spec.formula) return {
argv: null,
error: "missing brew formula"
};
return { argv: [
"brew",
"install",
spec.formula
] };
case "node":
if (!spec.package) return {
argv: null,
error: "missing node package"
};
return { argv: buildNodeInstallCommand(spec.package, prefs) };
case "go":
if (!spec.module) return {
argv: null,
error: "missing go module"
};
return { argv: [
"go",
"install",
spec.module
] };
case "uv":
if (!spec.package) return {
argv: null,
error: "missing uv package"
};
return { argv: [
"uv",
"tool",
"install",
spec.package
] };
case "download": return {
argv: null,
error: "download install handled separately"
};
default: return {
argv: null,
error: "unsupported installer"
};
}
}
async function resolveBrewBinDir(timeoutMs, brewExe) {
const exe = brewExe ?? (hasBinary("brew") ? "brew" : resolveBrewExecutable());
if (!exe) return;
const prefixResult = await runCommandWithTimeout([exe, "--prefix"], { timeoutMs: Math.min(timeoutMs, 3e4) });
if (prefixResult.code === 0) {
const prefix = prefixResult.stdout.trim();
if (prefix) return path.join(prefix, "bin");
}
const envPrefix = process.env.HOMEBREW_PREFIX?.trim();
if (envPrefix) return path.join(envPrefix, "bin");
for (const candidate of ["/opt/homebrew/bin", "/usr/local/bin"]) try {
if (fs.existsSync(candidate)) return candidate;
} catch {}
}
function createInstallFailure(params) {
return {
ok: false,
message: params.message,
stdout: params.stdout?.trim() ?? "",
stderr: params.stderr?.trim() ?? "",
code: params.code ?? null
};
}
function createInstallSuccess(result) {
return {
ok: true,
message: "Installed",
stdout: result.stdout.trim(),
stderr: result.stderr.trim(),
code: result.code
};
}
async function runCommandSafely(argv, optionsOrTimeout) {
try {
const result = await runCommandWithTimeout(argv, optionsOrTimeout);
return {
code: result.code,
stdout: result.stdout,
stderr: result.stderr
};
} catch (err) {
return {
code: null,
stdout: "",
stderr: err instanceof Error ? err.message : String(err)
};
}
}
async function runBestEffortCommand(argv, optionsOrTimeout) {
await runCommandSafely(argv, optionsOrTimeout);
}
function resolveBrewMissingFailure(spec) {
const formula = spec.formula ?? "this package";
return createInstallFailure({ message: `brew not installed — ${process.platform === "linux" ? `Homebrew is not installed. Install it from https://brew.sh or install "${formula}" manually using your system package manager (e.g. apt, dnf, pacman).` : "Homebrew is not installed. Install it from https://brew.sh"}` });
}
async function ensureUvInstalled(params) {
if (params.spec.kind !== "uv" || hasBinary("uv")) return;
if (!params.brewExe) return createInstallFailure({ message: "uv not installed — install manually: https://docs.astral.sh/uv/getting-started/installation/" });
const brewResult = await runCommandSafely([
params.brewExe,
"install",
"uv"
], { timeoutMs: params.timeoutMs });
if (brewResult.code === 0) return;
return createInstallFailure({
message: "Failed to install uv (brew)",
...brewResult
});
}
async function installGoViaApt(timeoutMs) {
const aptInstallArgv = [
"apt-get",
"install",
"-y",
"golang-go"
];
const aptUpdateArgv = [
"apt-get",
"update",
"-qq"
];
const aptFailureMessage = "go not installed — automatic install via apt failed. Install manually: https://go.dev/doc/install";
if (typeof process.getuid === "function" && process.getuid() === 0) {
await runBestEffortCommand(aptUpdateArgv, { timeoutMs });
const aptResult = await runCommandSafely(aptInstallArgv, { timeoutMs });
if (aptResult.code === 0) return;
return createInstallFailure({
message: aptFailureMessage,
...aptResult
});
}
if (!hasBinary("sudo")) return createInstallFailure({ message: "go not installed — apt-get is available but sudo is not installed. Install manually: https://go.dev/doc/install" });
const sudoCheck = await runCommandSafely([
"sudo",
"-n",
"true"
], { timeoutMs: 5e3 });
if (sudoCheck.code !== 0) return createInstallFailure({
message: "go not installed — apt-get is available but sudo is not usable (missing or requires a password). Install manually: https://go.dev/doc/install",
...sudoCheck
});
await runBestEffortCommand(["sudo", ...aptUpdateArgv], { timeoutMs });
const aptResult = await runCommandSafely(["sudo", ...aptInstallArgv], { timeoutMs });
if (aptResult.code === 0) return;
return createInstallFailure({
message: aptFailureMessage,
...aptResult
});
}
async function ensureGoInstalled(params) {
if (params.spec.kind !== "go" || hasBinary("go")) return;
if (params.brewExe) {
const brewResult = await runCommandSafely([
params.brewExe,
"install",
"go"
], { timeoutMs: params.timeoutMs });
if (brewResult.code === 0) return;
return createInstallFailure({
message: "Failed to install go (brew)",
...brewResult
});
}
if (hasBinary("apt-get")) return installGoViaApt(params.timeoutMs);
return createInstallFailure({ message: "go not installed — install manually: https://go.dev/doc/install" });
}
async function executeInstallCommand(params) {
if (!params.argv || params.argv.length === 0) return createInstallFailure({ message: "invalid install command" });
const result = await runCommandSafely(params.argv, {
timeoutMs: params.timeoutMs,
env: params.env
});
if (result.code === 0) return createInstallSuccess(result);
return createInstallFailure({
message: formatInstallFailureMessage(result),
...result
});
}
async function installSkill(params) {
const timeoutMs = Math.min(Math.max(params.timeoutMs ?? 3e5, 1e3), 9e5);
const entry = loadWorkspaceSkillEntries(resolveUserPath(params.workspaceDir)).find((item) => item.skill.name === params.skillName);
if (!entry) return {
ok: false,
message: `Skill not found: ${params.skillName}`,
stdout: "",
stderr: "",
code: null
};
const spec = findInstallSpec(entry, params.installId);
const warnings = await collectSkillInstallScanWarnings(entry);
if (!spec) return withWarnings({
ok: false,
message: `Installer not found: ${params.installId}`,
stdout: "",
stderr: "",
code: null
}, warnings);
if (spec.kind === "download") return withWarnings(await installDownloadSpec({
entry,
spec,
timeoutMs
}), warnings);
const command = buildInstallCommand(spec, resolveSkillsInstallPreferences(params.config));
if (command.error) return withWarnings({
ok: false,
message: command.error,
stdout: "",
stderr: "",
code: null
}, warnings);
const brewExe = hasBinary("brew") ? "brew" : resolveBrewExecutable();
if (spec.kind === "brew" && !brewExe) return withWarnings(resolveBrewMissingFailure(spec), warnings);
const uvInstallFailure = await ensureUvInstalled({
spec,
brewExe,
timeoutMs
});
if (uvInstallFailure) return withWarnings(uvInstallFailure, warnings);
const goInstallFailure = await ensureGoInstalled({
spec,
brewExe,
timeoutMs
});
if (goInstallFailure) return withWarnings(goInstallFailure, warnings);
const argv = command.argv ? [...command.argv] : null;
if (spec.kind === "brew" && brewExe && argv?.[0] === "brew") argv[0] = brewExe;
let env;
if (spec.kind === "go" && brewExe) {
const brewBin = await resolveBrewBinDir(timeoutMs, brewExe);
if (brewBin) env = { GOBIN: brewBin };
}
return withWarnings(await executeInstallCommand({
argv,
timeoutMs,
env
}), warnings);
}
//#endregion
export { installSkill as t };