// Path helpers and slug validation for the skills module. // // Skills live in two scopes: // - user: ~/.claude/skills//SKILL.md (read-only from MulmoClaude) // - project: /.claude/skills//SKILL.md (MulmoClaude can CRUD) // // The slug doubles as a filename and appears in Claude CLI slash // commands (`/`), so it has to be strict: no uppercase, no // spaces, no path separators, no leading/trailing hyphens, 1-64 // chars. Same rule as `server/sources/paths.ts#isValidSlug` — keep // them in sync manually. import { join } from "node:path"; import { claudeSkillsDir } from "../../utils/claudeConfigPath.js"; export const SKILL_FILE = "SKILL.md"; /** `/skills/` — user scope, read-only from MulmoClaude. * Default `~/.claude/skills/`; override via `CLAUDE_CONFIG_DIR` (issue #87 §2). */ export const USER_SKILLS_DIR = claudeSkillsDir(); /** `/.claude/skills/` — project scope, writable. */ export function projectSkillsDir(workspaceRoot: string): string { return join(workspaceRoot, ".claude", "skills"); } /** Full path to the project-scope SKILL.md for a given slug. */ export function projectSkillPath(workspaceRoot: string, slug: string): string { return join(projectSkillsDir(workspaceRoot), slug, SKILL_FILE); } /** Directory holding a project skill (one level above SKILL.md). */ export function projectSkillDir(workspaceRoot: string, slug: string): string { return join(projectSkillsDir(workspaceRoot), slug); } /** * Strict slug validator. Rejects anything that could surprise the * filesystem, the Claude CLI slash-command resolver, or the URL * parser. Single source of truth for `save` / `delete` input. */ // isValidSlug moved to server/utils/slug.ts — import from there.