Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 14 additions & 9 deletions apps/server/src/provider/Drivers/CursorSkills.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -147,16 +147,18 @@ const discoverSkillsInRoot = Effect.fn("discoverCursorSkillsInRoot")(function* (
if (!resolvedDirectory) {
return;
}
if (
visitedDirectories.has(resolvedDirectory) ||
(resolvedDirectory !== rootDirectory &&
!resolvedDirectory.startsWith(`${rootDirectory}${path.sep}`))
) {
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(resolvedDirectory, "SKILL.md");
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 };
Expand All@@ -167,7 +169,7 @@ const discoverSkillsInRoot = Effect.fn("discoverCursorSkillsInRoot")(function* (
frontmatter = parseSkillFrontmatter(contents);
}
}
const name = path.basename(resolvedDirectory).trim();
const name = path.basename(directory).trim();
if (frontmatter?.cliVisible && name) {
skills.push({
name,
Expand All@@ -184,7 +186,10 @@ const discoverSkillsInRoot = Effect.fn("discoverCursorSkillsInRoot")(function* (
}
}

const entries = yield* orUndefined(fileSystem.readDirectory(resolvedDirectory), input.budget);
if (!insideRoot) {
return;
}
const entries = yield* orUndefined(fileSystem.readDirectory(directory), input.budget);
if (!entries) {
return;
}
Expand All@@ -194,7 +199,7 @@ const discoverSkillsInRoot = Effect.fn("discoverCursorSkillsInRoot")(function* (
return;
}
input.budget.remainingEntries -= 1;
const child = path.join(resolvedDirectory, entry);
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) {
Expand Down
50 changes: 50 additions & 0 deletions apps/server/src/provider/Layers/CursorProvider.test.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -398,6 +398,56 @@ describe("Cursor skills", () => {
}),
));

it("treats a symlinked skill outside the root as a package boundary", async () =>
await runNode(
Effect.gen(function* () {
const fileSystem = yield* FileSystem.FileSystem;
const path = yield* Path.Path;
const userHome = yield* fileSystem.makeTempDirectory({
directory: NodeOS.tmpdir(),
prefix: "cursor-skills-home-",
});
const workspace = yield* fileSystem.makeTempDirectory({
directory: NodeOS.tmpdir(),
prefix: "cursor-skills-workspace-",
});
const library = yield* fileSystem.makeTempDirectory({
directory: NodeOS.tmpdir(),
prefix: "cursor-skills-library-",
});
const writeSkill = Effect.fn("writeCursorSkill")(function* (
directory: string,
contents: string,
) {
yield* fileSystem.makeDirectory(directory, { recursive: true });
yield* fileSystem.writeFileString(path.join(directory, "SKILL.md"), contents);
});

// A skill package managed in a config repo and installed by symlink.
// Its own SKILL.md must be discovered under the link name, but nothing
// below the target may be walked.
yield* writeSkill(path.join(library, "shared-review"), "---\ndescription: shared\n---\n");
yield* writeSkill(path.join(library, "shared-review", "hidden"), "---\n---\n");
const root = path.join(workspace, ".cursor", "skills");
yield* fileSystem.makeDirectory(root, { recursive: true });
yield* fileSystem.symlink(path.join(library, "shared-review"), path.join(root, "review"));

const skills = yield* discoverCursorSkills(workspace, { HOME: userHome });
expect(skills).toEqual([
{
name: "review",
description: "shared",
path: path.join(root, "review", "SKILL.md"),
scope: "project",
enabled: true,
},
]);
expect(
(yield* probeCursorSkills(workspace, { HOME: userHome }).pipe(Effect.result))._tag,
).toBe("Success");
}),
));

it("rewrites only discovered skill mentions into Cursor slash invocations", () => {
expect(hasCursorSkillMention("use $Review_Pr:V2 here")).toBe(true);
expect(hasCursorSkillMention("please $review this")).toBe(true);
Expand Down
Loading