289 lines · 10.2 KB · TypeScript
RawDownload
/** * 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>()(  "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 = <A, R>(  effect: Effect.Effect<A, PlatformError.PlatformError, R>,  budget?: CursorSkillScanBudget,): Effect.Effect<A | undefined, never, R> =>  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<string, unknown>;  const metadata =    typeof record.metadata === "object" && record.metadata !== null      ? (record.metadata as Record<string, unknown>)      : 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<ReadonlyArray<ServerProviderSkill>, 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<string>();   const visit = Effect.fn("visitCursorSkillDirectory")(function* (    directory: string,    depth: number,  ): Effect.fn.Return<void, never> {    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<string, ServerProviderSkill>();  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>,): string {  return prompt.replace(SKILL_MENTION_PATTERN, (match, prefix: string, name: string) =>    skillNames.has(name) ? `${prefix}/${name}` : match,  );}