| author | |
| committer | |
| log | 087cfb8ae262f344f7e409f5a8c0eda6f0ef12f9 |
| tree | 8a97f962fb8fb57ed08faf917052d3297a94be83 |
| parent | 5a433244d0b827176df8b76a05959cd1a7a98ce2 |
| signature | Signed by PGP key B5690EEEBB952194 |
2 files changed, 64 insertions(+), 9 deletions(-)
apps/server/src/provider/Drivers/CursorSkills.ts+14-9| ... | ... | @@ -147,16 +147,18 @@ const discoverSkillsInRoot = Effect.fn("discoverCursorSkillsInRoot")(function* ( |
| 147 | 147 | if (!resolvedDirectory) { |
| 148 | 148 | return; |
| 149 | 149 | } |
| 150 | if ( | |
| 151 | visitedDirectories.has(resolvedDirectory) || | |
| 152 | (resolvedDirectory !== rootDirectory && | |
| 153 | !resolvedDirectory.startsWith(`${rootDirectory}${path.sep}`)) | |
| 154 | ) { | |
| 150 | if (visitedDirectories.has(resolvedDirectory)) { | |
| 155 | 151 | return; |
| 156 | 152 | } |
| 157 | 153 | visitedDirectories.add(resolvedDirectory); |
| 154 | // A symlink whose target lives outside the root is a skill package | |
| 155 | // boundary: read its own SKILL.md so linked skill libraries show up, but | |
| 156 | // never walk the target tree. | |
| 157 | const insideRoot = | |
| 158 | resolvedDirectory === rootDirectory || | |
| 159 | resolvedDirectory.startsWith(`${rootDirectory}${path.sep}`); | |
| 158 | 160 | |
| 159 | const skillPath = path.join(resolvedDirectory, "SKILL.md"); | |
| 161 | const skillPath = path.join(directory, "SKILL.md"); | |
| 160 | 162 | const skillInfo = yield* orUndefined(fileSystem.stat(skillPath), input.budget); |
| 161 | 163 | if (skillInfo?.type === "File") { |
| 162 | 164 | let frontmatter: CursorSkillFrontmatter | undefined = { cliVisible: true }; |
| ... | ... | @@ -167,7 +169,7 @@ const discoverSkillsInRoot = Effect.fn("discoverCursorSkillsInRoot")(function* ( |
| 167 | 169 | frontmatter = parseSkillFrontmatter(contents); |
| 168 | 170 | } |
| 169 | 171 | } |
| 170 | const name = path.basename(resolvedDirectory).trim(); | |
| 172 | const name = path.basename(directory).trim(); | |
| 171 | 173 | if (frontmatter?.cliVisible && name) { |
| 172 | 174 | skills.push({ |
| 173 | 175 | name, |
| ... | ... | @@ -184,7 +186,10 @@ const discoverSkillsInRoot = Effect.fn("discoverCursorSkillsInRoot")(function* ( |
| 184 | 186 | } |
| 185 | 187 | } |
| 186 | 188 | |
| 187 | const entries = yield* orUndefined(fileSystem.readDirectory(resolvedDirectory), input.budget); | |
| 189 | if (!insideRoot) { | |
| 190 | return; | |
| 191 | } | |
| 192 | const entries = yield* orUndefined(fileSystem.readDirectory(directory), input.budget); | |
| 188 | 193 | if (!entries) { |
| 189 | 194 | return; |
| 190 | 195 | } |
| ... | ... | @@ -194,7 +199,7 @@ const discoverSkillsInRoot = Effect.fn("discoverCursorSkillsInRoot")(function* ( |
| 194 | 199 | return; |
| 195 | 200 | } |
| 196 | 201 | input.budget.remainingEntries -= 1; |
| 197 | const child = path.join(resolvedDirectory, entry); | |
| 202 | const child = path.join(directory, entry); | |
| 198 | 203 | const info = yield* orUndefined(fileSystem.stat(child), input.budget); |
| 199 | 204 | if (info?.type !== "Directory") continue; |
| 200 | 205 | if (depth >= MAX_SKILL_DEPTH) { |
apps/server/src/provider/Layers/CursorProvider.test.ts+50| ... | ... | @@ -395,6 +395,56 @@ describe("Cursor skills", () => { |
| 395 | 395 | }), |
| 396 | 396 | )); |
| 397 | 397 | |
| 398 | it("treats a symlinked skill outside the root as a package boundary", async () => | |
| 399 | await runNode( | |
| 400 | Effect.gen(function* () { | |
| 401 | const fileSystem = yield* FileSystem.FileSystem; | |
| 402 | const path = yield* Path.Path; | |
| 403 | const userHome = yield* fileSystem.makeTempDirectory({ | |
| 404 | directory: NodeOS.tmpdir(), | |
| 405 | prefix: "cursor-skills-home-", | |
| 406 | }); | |
| 407 | const workspace = yield* fileSystem.makeTempDirectory({ | |
| 408 | directory: NodeOS.tmpdir(), | |
| 409 | prefix: "cursor-skills-workspace-", | |
| 410 | }); | |
| 411 | const library = yield* fileSystem.makeTempDirectory({ | |
| 412 | directory: NodeOS.tmpdir(), | |
| 413 | prefix: "cursor-skills-library-", | |
| 414 | }); | |
| 415 | const writeSkill = Effect.fn("writeCursorSkill")(function* ( | |
| 416 | directory: string, | |
| 417 | contents: string, | |
| 418 | ) { | |
| 419 | yield* fileSystem.makeDirectory(directory, { recursive: true }); | |
| 420 | yield* fileSystem.writeFileString(path.join(directory, "SKILL.md"), contents); | |
| 421 | }); | |
| 422 | ||
| 423 | // A skill package managed in a config repo and installed by symlink. | |
| 424 | // Its own SKILL.md must be discovered under the link name, but nothing | |
| 425 | // below the target may be walked. | |
| 426 | yield* writeSkill(path.join(library, "shared-review"), "---\ndescription: shared\n---\n"); | |
| 427 | yield* writeSkill(path.join(library, "shared-review", "hidden"), "---\n---\n"); | |
| 428 | const root = path.join(workspace, ".cursor", "skills"); | |
| 429 | yield* fileSystem.makeDirectory(root, { recursive: true }); | |
| 430 | yield* fileSystem.symlink(path.join(library, "shared-review"), path.join(root, "review")); | |
| 431 | ||
| 432 | const skills = yield* discoverCursorSkills(workspace, { HOME: userHome }); | |
| 433 | expect(skills).toEqual([ | |
| 434 | { | |
| 435 | name: "review", | |
| 436 | description: "shared", | |
| 437 | path: path.join(root, "review", "SKILL.md"), | |
| 438 | scope: "project", | |
| 439 | enabled: true, | |
| 440 | }, | |
| 441 | ]); | |
| 442 | expect( | |
| 443 | (yield* probeCursorSkills(workspace, { HOME: userHome }).pipe(Effect.result))._tag, | |
| 444 | ).toBe("Success"); | |
| 445 | }), | |
| 446 | )); | |
| 447 | ||
| 398 | 448 | it("rewrites only discovered skill mentions into Cursor slash invocations", () => { |
| 399 | 449 | expect(hasCursorSkillMention("use $Review_Pr:V2 here")).toBe(true); |
| 400 | 450 | expect(hasCursorSkillMention("please $review this")).toBe(true); |