UNPKG

openclaw

Version:

Multi-channel AI gateway with extensible messaging integrations

128 lines (127 loc) 4.13 kB
import { a as normalizeLowercaseStringOrEmpty } from "./string-coerce-mnp54Vah.js"; import { n as resolvePreferredOpenClawTmpDir } from "./tmp-openclaw-dir-DOKojISm.js"; import { r as cliBackendLog } from "./log-RtgQ_MP8.js"; import { accessSync } from "node:fs"; import path from "node:path"; import fs$1 from "node:fs/promises"; //#region src/agents/cli-runner/claude-skills-plugin.ts /** * Materializes selected OpenClaw skills as a temporary Claude CLI plugin. */ const CLAUDE_CLI_BACKEND_ID = "claude-cli"; const OPENCLAW_CLAUDE_PLUGIN_NAME = "openclaw-skills"; function sanitizeSkillDirName(name, used) { const base = name.trim().replace(/[^A-Za-z0-9._-]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 80) || "skill"; const safeBase = base.startsWith(".") ? `skill-${base.replace(/^\.+/, "") || "skill"}` : base; let candidate = safeBase; for (let index = 2; used.has(candidate); index += 1) candidate = `${safeBase}-${index}`; used.add(candidate); return candidate; } /** Returns whether a resolved skill file is readable before linking it into the Claude plugin. */ function isClaudeCliSkillFileAccessible(skillFilePath) { try { accessSync(skillFilePath); return true; } catch { return false; } } async function collectClaudePluginSkills(snapshot) { const skills = snapshot?.resolvedSkills ?? []; if (skills.length === 0) return []; const usedTargetNames = /* @__PURE__ */ new Set(); const materialized = []; for (const skill of skills) { const name = skill.name?.trim(); const skillFilePath = skill.filePath?.trim(); if (!name || !skillFilePath) continue; if (!isClaudeCliSkillFileAccessible(skillFilePath)) { cliBackendLog.warn(`claude skill plugin skipped missing skill file: ${skillFilePath}`); continue; } materialized.push({ name, sourceDir: path.dirname(skillFilePath), targetDirName: sanitizeSkillDirName(name, usedTargetNames) }); } return materialized; } async function linkOrCopySkillDir(params) { try { await fs$1.symlink(params.sourceDir, params.targetDir, process.platform === "win32" ? "junction" : "dir"); } catch { await fs$1.cp(params.sourceDir, params.targetDir, { recursive: true, force: true, verbatimSymlinks: true }); } } /** Prepares Claude CLI `--plugin-dir` args for the current session skill snapshot. */ async function prepareClaudeCliSkillsPlugin(params) { if (normalizeLowercaseStringOrEmpty(params.backendId) !== CLAUDE_CLI_BACKEND_ID) return { args: [], cleanup: async () => {} }; const skills = await collectClaudePluginSkills(params.skillsSnapshot); if (skills.length === 0) return { args: [], cleanup: async () => {} }; const tempDir = await fs$1.mkdtemp(path.join(resolvePreferredOpenClawTmpDir(), "openclaw-claude-skills-")); const pluginDir = path.join(tempDir, OPENCLAW_CLAUDE_PLUGIN_NAME); const manifestDir = path.join(pluginDir, ".claude-plugin"); const skillsDir = path.join(pluginDir, "skills"); await fs$1.mkdir(manifestDir, { recursive: true, mode: 448 }); await fs$1.mkdir(skillsDir, { recursive: true, mode: 448 }); const manifest = { name: OPENCLAW_CLAUDE_PLUGIN_NAME, version: "0.0.0", description: "Session-scoped OpenClaw skills selected for this agent run.", skills: "./skills" }; await fs$1.writeFile(path.join(manifestDir, "plugin.json"), `${JSON.stringify(manifest, null, 2)}\n`, { encoding: "utf-8", mode: 384 }); let linkedSkillCount = 0; for (const skill of skills) try { await linkOrCopySkillDir({ sourceDir: skill.sourceDir, targetDir: path.join(skillsDir, skill.targetDirName) }); linkedSkillCount += 1; } catch (error) { cliBackendLog.warn(`claude skill plugin skipped ${skill.name}: ${error instanceof Error ? error.message : String(error)}`); } if (linkedSkillCount === 0) { await fs$1.rm(tempDir, { recursive: true, force: true }); return { args: [], cleanup: async () => {} }; } return { args: ["--plugin-dir", pluginDir], pluginDir, cleanup: async () => { await fs$1.rm(tempDir, { recursive: true, force: true }); } }; } //#endregion export { prepareClaudeCliSkillsPlugin as t };