/** * CursorSkills — workspace-aware discovery and native invocation for Cursor. * * Cursor discovers Agent Skills recursively from user and project roots but * its ACP command catalog only appears after opening a real session. Scanning * the same roots avoids starting an agent and its MCP servers just to populate * a composer menu. * * @module provider/Drivers/CursorSkills */ import * as NodeOS from "node:os"; import type { ServerProviderSkill } from "@t3tools/contracts"; import * as Effect from "effect/Effect"; import * as FileSystem from "effect/FileSystem"; import * as Path from "effect/Path"; import * as PlatformError from "effect/PlatformError"; import * as Schema from "effect/Schema"; import { parse as parseYamlDocument } from "yaml"; const FRONTMATTER_PATTERN = /^---\r?\n([\s\S]*?)\r?\n---(?:\r?\n|$)/; const SKILL_MENTION_PATTERN = /(^|\s)\$(?![0-9][0-9_]*(?:[kKmMbBtT]|[eE][0-9]+)?(?:\s|$))(?=[a-zA-Z0-9:_-]*[a-zA-Z])([a-zA-Z0-9][a-zA-Z0-9:_-]*)(?=\s|$)/g; const HAS_SKILL_MENTION_PATTERN = new RegExp(SKILL_MENTION_PATTERN.source); const MAX_SKILL_DEPTH = 10; const MAX_SKILL_BYTES = FileSystem.Size(1_000_000); const MAX_SKILL_SCAN_ENTRIES = 10_000; const MAX_SKILL_SCAN_BYTES = FileSystem.Size(8_000_000); interface CursorSkillFrontmatter { readonly description?: string; readonly displayName?: string; readonly userInvocationOnly?: boolean; readonly userInvocable?: boolean; readonly cliVisible: boolean; } interface CursorSkillScanBudget { remainingEntries: number; remainingBytes: bigint; exhausted: boolean; incomplete: boolean; } class CursorSkillsProbeError extends Schema.TaggedError()( "CursorSkillsProbeError", { reason: Schema.Literals(["scan-budget-exhausted", "filesystem-error"]), cwd: Schema.optional(Schema.String), }, ) { override get message(): string { const location = this.cwd === undefined ? "" : ` for '${this.cwd}'`; return `Cursor skill discovery${location} was incomplete (${this.reason}).`; } } const orUndefined = ( effect: Effect.Effect, budget?: CursorSkillScanBudget, ): Effect.Effect => effect.pipe( Effect.map((value): A | undefined => value), Effect.catchTags({ PlatformError: (error) => { if (error.reason._tag !== "NotFound" && budget) budget.incomplete = true; return Effect.void.pipe(Effect.as(undefined)); }, }), ); function parseFrontmatterBoolean(value: unknown): boolean | undefined { if (typeof value === "boolean") return value; if (typeof value === "number") return value === 1 ? true : value === 0 ? false : undefined; if (typeof value !== "string") return undefined; switch (value.trim().toLowerCase()) { case "true": case "yes": case "on": return true; case "false": case "no": case "off": return false; default: return undefined; } } function parseSkillFrontmatter(contents: string): CursorSkillFrontmatter | undefined { const match = FRONTMATTER_PATTERN.exec(contents); if (!match) return { cliVisible: true }; let parsed: unknown; try { parsed = parseYamlDocument(match[1] ?? ""); } catch { return undefined; } if (typeof parsed !== "object" || parsed === null) return undefined; const record = parsed as Record; const metadata = typeof record.metadata === "object" && record.metadata !== null ? (record.metadata as Record) : undefined; const rawSurfaces = metadata?.surfaces; const surfaces = Array.isArray(rawSurfaces) ? rawSurfaces.filter((surface): surface is string => typeof surface === "string") : typeof rawSurfaces === "string" ? rawSurfaces.split(",") : []; const description = typeof record.description === "string" ? record.description.trim() : ""; const displayName = typeof record.name === "string" ? record.name.trim() : ""; return { cliVisible: surfaces.length === 0 || surfaces.some((surface) => surface.trim().toLowerCase() === "cli"), ...(description ? { description } : {}), ...(displayName ? { displayName } : {}), ...(parseFrontmatterBoolean(record["disable-model-invocation"]) === true ? { userInvocationOnly: true } : {}), ...(parseFrontmatterBoolean(record["user-invocable"]) === false ? { userInvocable: false } : {}), }; } const discoverSkillsInRoot = Effect.fn("discoverCursorSkillsInRoot")(function* (input: { readonly directory: string; readonly scope: "user" | "project"; readonly budget: CursorSkillScanBudget; }): Effect.fn.Return, never, FileSystem.FileSystem | Path.Path> { const fileSystem = yield* FileSystem.FileSystem; const path = yield* Path.Path; const skills: ServerProviderSkill[] = []; if (input.budget.exhausted) return skills; const rootDirectory = yield* orUndefined(fileSystem.realPath(input.directory), input.budget); if (!rootDirectory) return skills; const visitedDirectories = new Set(); const visit = Effect.fn("visitCursorSkillDirectory")(function* ( directory: string, depth: number, ): Effect.fn.Return { if (input.budget.exhausted) return; const resolvedDirectory = yield* orUndefined(fileSystem.realPath(directory), input.budget); if (!resolvedDirectory) { return; } if (visitedDirectories.has(resolvedDirectory)) { return; } visitedDirectories.add(resolvedDirectory); // A symlink whose target lives outside the root is a skill package // boundary: read its own SKILL.md so linked skill libraries show up, but // never walk the target tree. const insideRoot = resolvedDirectory === rootDirectory || resolvedDirectory.startsWith(`${rootDirectory}${path.sep}`); const skillPath = path.join(directory, "SKILL.md"); const skillInfo = yield* orUndefined(fileSystem.stat(skillPath), input.budget); if (skillInfo?.type === "File") { let frontmatter: CursorSkillFrontmatter | undefined = { cliVisible: true }; if (skillInfo.size <= MAX_SKILL_BYTES && skillInfo.size <= input.budget.remainingBytes) { const contents = yield* orUndefined(fileSystem.readFileString(skillPath)); if (contents !== undefined) { input.budget.remainingBytes -= skillInfo.size; frontmatter = parseSkillFrontmatter(contents); } } const name = path.basename(directory).trim(); if (frontmatter?.cliVisible && name) { skills.push({ name, path: skillPath, scope: input.scope, enabled: true, ...(frontmatter.displayName && frontmatter.displayName !== name ? { displayName: frontmatter.displayName } : {}), ...(frontmatter.description ? { description: frontmatter.description } : {}), ...(frontmatter.userInvocationOnly ? { userInvocationOnly: true } : {}), ...(frontmatter.userInvocable === false ? { userInvocable: false } : {}), }); } } if (!insideRoot) { return; } const entries = yield* orUndefined(fileSystem.readDirectory(directory), input.budget); if (!entries) { return; } for (const entry of [...entries].sort()) { if (input.budget.remainingEntries === 0) { input.budget.exhausted = true; return; } input.budget.remainingEntries -= 1; const child = path.join(directory, entry); const info = yield* orUndefined(fileSystem.stat(child), input.budget); if (info?.type !== "Directory") continue; if (depth >= MAX_SKILL_DEPTH) { input.budget.exhausted = true; return; } yield* visit(child, depth + 1); } }); yield* visit(rootDirectory, 0); return skills; }); const inspectCursorSkills = Effect.fn("inspectCursorSkills")(function* ( cwd?: string, environment: NodeJS.ProcessEnv = process.env, ) { const path = yield* Path.Path; const userHome = environment.HOME?.trim() || environment.USERPROFILE?.trim() || NodeOS.homedir(); const rootsBelow = (base: string, scope: "user" | "project") => [ { directory: path.join(base, ".cursor", "skills"), scope }, { directory: path.join(base, ".agents", "skills"), scope }, { directory: path.join(base, ".codex", "skills"), scope }, { directory: path.join(base, ".claude", "skills"), scope }, ]; const roots = [...(cwd ? rootsBelow(cwd, "project") : []), ...rootsBelow(userHome, "user")]; const skillsByName = new Map(); const budget: CursorSkillScanBudget = { remainingEntries: MAX_SKILL_SCAN_ENTRIES, remainingBytes: MAX_SKILL_SCAN_BYTES, exhausted: false, incomplete: false, }; for (const root of roots) { if (budget.exhausted) break; const skills = yield* discoverSkillsInRoot({ ...root, budget }); for (const skill of skills) { if (!skillsByName.has(skill.name)) skillsByName.set(skill.name, skill); } } return { skills: [...skillsByName.values()].sort((left, right) => left.name.localeCompare(right.name)), failureReason: budget.exhausted ? ("scan-budget-exhausted" as const) : budget.incomplete ? ("filesystem-error" as const) : undefined, }; }); export const discoverCursorSkills = Effect.fn("discoverCursorSkills")(function* ( cwd?: string, environment: NodeJS.ProcessEnv = process.env, ) { return (yield* inspectCursorSkills(cwd, environment)).skills; }); export const probeCursorSkills = Effect.fn("probeCursorSkills")(function* ( cwd?: string, environment: NodeJS.ProcessEnv = process.env, ) { const inspection = yield* inspectCursorSkills(cwd, environment); if (inspection.failureReason) { return yield* new CursorSkillsProbeError({ reason: inspection.failureReason, ...(cwd ? { cwd } : {}), }); } return inspection.skills; }); /** Cursor invokes Agent Skills with `/name`; T3 composers insert `$name`. */ export function hasCursorSkillMention(prompt: string): boolean { return HAS_SKILL_MENTION_PATTERN.test(prompt); } export function rewriteCursorSkillMentions( prompt: string, skillNames: ReadonlySet, ): string { return prompt.replace(SKILL_MENTION_PATTERN, (match, prefix: string, name: string) => skillNames.has(name) ? `${prefix}/${name}` : match, ); }