/** * Skill snapshot for the DeepSeek-via-Hermes adapter. * * Hermes manages its own skills under ~/.hermes/skills/ (global; not per-profile). * Paperclip-managed skills declared in adapter config are surfaced as * "company_managed" entries — same behavior as the upstream Hermes adapter. */ import fs from "node:fs/promises"; import path from "node:path"; import { fileURLToPath } from "node:url"; import { readPaperclipRuntimeSkillEntries, resolvePaperclipDesiredSkillNames, } from "@paperclipai/adapter-utils/server-utils"; import { ADAPTER_TYPE } from "../shared/constants.js"; const __moduleDir = path.dirname(fileURLToPath(import.meta.url)); function asString(value) { return typeof value === "string" && value.trim().length > 0 ? value.trim() : null; } function parseSkillFrontmatter(content) { const match = content.match(/^---\s*\n([\s\S]*?)\n---/); if (!match) return {}; const fm = {}; for (const line of match[1].split("\n")) { const idx = line.indexOf(":"); if (idx === -1) continue; const key = line.slice(0, idx).trim(); let val = line.slice(idx + 1).trim(); if ((val.startsWith('"') && val.endsWith('"')) || (val.startsWith("'") && val.endsWith("'"))) { val = val.slice(1, -1); } fm[key] = val; } return fm; } async function buildSkillEntry(key, skillMdPath, categoryPath) { let description = null; try { const content = await fs.readFile(skillMdPath, "utf8"); description = parseSkillFrontmatter(content).description ?? null; } catch { // ignore } return { key, runtimeName: key, desired: true, managed: false, state: "installed", origin: "user_installed", originLabel: "Hermes skill", locationLabel: `~/.hermes/skills/${categoryPath}`, readOnly: true, sourcePath: skillMdPath, targetPath: null, detail: description, }; } async function scanHermesSkills(skillsHome) { const entries = []; try { const cats = await fs.readdir(skillsHome, { withFileTypes: true }); for (const cat of cats) { if (!cat.isDirectory()) continue; const catPath = path.join(skillsHome, cat.name); const topSkill = path.join(catPath, "SKILL.md"); if (await fs.stat(topSkill).catch(() => null)) { entries.push(await buildSkillEntry(cat.name, topSkill, cat.name)); } const items = await fs.readdir(catPath, { withFileTypes: true }).catch(() => []); for (const item of items) { if (!item.isDirectory()) continue; const skillMd = path.join(catPath, item.name, "SKILL.md"); if (await fs.stat(skillMd).catch(() => null)) { entries.push(await buildSkillEntry(item.name, skillMd, `${cat.name}/${item.name}`)); } } } } catch { // ~/.hermes/skills/ doesn't exist } return entries.sort((a, b) => a.key.localeCompare(b.key)); } async function buildSnapshot(config) { const homedir = asString(config.env?.HOME) ?? process.env.HOME ?? "/home/chaim"; const hermesSkillsHome = path.join(homedir, ".hermes", "skills"); const paperclipEntries = await readPaperclipRuntimeSkillEntries(config, __moduleDir); const desiredSkills = resolvePaperclipDesiredSkillNames(config, paperclipEntries); const desiredSet = new Set(desiredSkills); const availableByKey = new Map(paperclipEntries.map((e) => [e.key, e])); const hermesSkillEntries = await scanHermesSkills(hermesSkillsHome); const hermesKeys = new Set(hermesSkillEntries.map((e) => e.key)); const entries = []; const warnings = []; for (const entry of paperclipEntries) { const desired = desiredSet.has(entry.key); entries.push({ key: entry.key, runtimeName: entry.runtimeName, desired, managed: true, state: desired ? "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: desired ? "Will be available on the next run via Hermes skill loading." : null, required: Boolean(entry.required), requiredReason: entry.requiredReason ?? null, }); } for (const entry of hermesSkillEntries) { if (availableByKey.has(entry.key)) continue; entries.push(entry); } for (const desired of desiredSkills) { if (availableByKey.has(desired) || hermesKeys.has(desired)) continue; warnings.push(`Desired skill "${desired}" is not available in Paperclip or Hermes skills.`); entries.push({ key: desired, runtimeName: null, desired: true, managed: true, state: "missing", origin: "external_unknown", originLabel: "External or unavailable", readOnly: false, sourcePath: null, targetPath: null, detail: "Cannot find this skill in Paperclip or ~/.hermes/skills/.", }); } return { adapterType: ADAPTER_TYPE, supported: true, mode: "persistent", desiredSkills, entries, warnings, }; } export async function listSkills(ctx) { return buildSnapshot(ctx.config); } export async function syncSkills(ctx, _desired) { return buildSnapshot(ctx.config); } export function resolveDesiredSkillNames(config, availableEntries) { return resolvePaperclipDesiredSkillNames(config, availableEntries); }