import os from "node:os"; import path from "node:path"; import { fileURLToPath } from "node:url"; import type { AdapterSkillContext, AdapterSkillEntry, AdapterSkillSnapshot, } from "@paperclipai/adapter-utils"; import { readPaperclipRuntimeSkillEntries, readInstalledSkillTargets, resolvePaperclipDesiredSkillNames, } from "@paperclipai/adapter-utils/server-utils"; const __moduleDir = path.dirname(fileURLToPath(import.meta.url)); function asString(value: unknown): string | null { return typeof value === "string" && value.trim().length > 0 ? value.trim() : null; } function resolveClaudeSkillsHome(config: Record) { const env = typeof config.env === "object" && config.env !== null && !Array.isArray(config.env) ? (config.env as Record) : {}; const configuredHome = asString(env.HOME); const home = configuredHome ? path.resolve(configuredHome) : os.homedir(); return path.join(home, ".claude", "skills"); } async function buildClaudeSkillSnapshot(config: Record): Promise { const availableEntries = await readPaperclipRuntimeSkillEntries(config, __moduleDir); const availableByKey = new Map(availableEntries.map((entry) => [entry.key, entry])); const desiredSkills = resolvePaperclipDesiredSkillNames(config, availableEntries); const desiredSet = new Set(desiredSkills); const skillsHome = resolveClaudeSkillsHome(config); const installed = await readInstalledSkillTargets(skillsHome); const entries: AdapterSkillEntry[] = availableEntries.map((entry) => ({ key: entry.key, runtimeName: entry.runtimeName, desired: desiredSet.has(entry.key), managed: true, state: desiredSet.has(entry.key) ? "configured" : "available", origin: entry.required ? "paperclip_required" : "company_managed", originLabel: entry.required ? "Required by Paperclip" : "Managed by Paperclip", readOnly: false, sourcePath: entry.source, targetPath: null, detail: desiredSet.has(entry.key) ? "Will be mounted into the ephemeral Claude skill directory on the next run." : null, required: Boolean(entry.required), requiredReason: entry.requiredReason ?? null, })); const warnings: string[] = []; for (const desiredSkill of desiredSkills) { if (availableByKey.has(desiredSkill)) continue; warnings.push(`Desired skill "${desiredSkill}" is not available from the Paperclip skills directory.`); entries.push({ key: desiredSkill, runtimeName: null, desired: true, managed: true, state: "missing", origin: "external_unknown", originLabel: "External or unavailable", readOnly: false, sourcePath: undefined, targetPath: undefined, detail: "Paperclip cannot find this skill in the local runtime skills directory.", }); } for (const [name, installedEntry] of installed.entries()) { if (availableEntries.some((entry) => entry.runtimeName === name)) continue; entries.push({ key: name, runtimeName: name, desired: false, managed: false, state: "external", origin: "user_installed", originLabel: "User-installed", locationLabel: "~/.claude/skills", readOnly: true, sourcePath: null, targetPath: installedEntry.targetPath ?? path.join(skillsHome, name), detail: "Installed outside Paperclip management in the Claude skills home.", }); } entries.sort((left, right) => left.key.localeCompare(right.key)); return { adapterType: "claude_local", supported: true, mode: "ephemeral", desiredSkills, entries, warnings, }; } export async function listClaudeSkills(ctx: AdapterSkillContext): Promise { return buildClaudeSkillSnapshot(ctx.config); } export async function syncClaudeSkills( ctx: AdapterSkillContext, _desiredSkills: string[], ): Promise { return buildClaudeSkillSnapshot(ctx.config); } export function resolveClaudeDesiredSkillNames( config: Record, availableEntries: Array<{ key: string; required?: boolean }>, ) { return resolvePaperclipDesiredSkillNames(config, availableEntries); }