feat(skills): add .cursor/skills to skill discovery paths (#817)

* feat(skills): add .cursor/skills to skill discovery paths

    Adds Cursor IDE interop by scanning .cursor/skills alongside the
    existing .opencode/skills and .claude/skills directories (#432).
    Precedence: .agents/skills > skills > .opencode/skills >
    .claude/skills > .cursor/skills > ~/.deepseek/skills.

* test(skills): cover cursor skills discovery

---------

Co-authored-by: ornn.ban <ornn.ban@biuya.com>
Co-authored-by: Hunter Bown <hmbown@gmail.com>
This commit is contained in:
banqii
2026-05-06 17:22:54 +08:00
committed by GitHub
parent 6e2b854fdb
commit 0a17f144c0
3 changed files with 34 additions and 5 deletions
+2 -2
View File
@@ -382,8 +382,8 @@ pub fn system_prompt_for_mode_with_context_skills_session_and_approval(
// 3. Skills block. #432: walks every candidate workspace
// skills directory (`.agents/skills`, `skills`,
// `.opencode/skills`, `.claude/skills`) plus the global
// default so skills installed for any AI-tool convention show
// `.opencode/skills`, `.claude/skills`, `.cursor/skills`) plus the
// global default so skills installed for any AI-tool convention show
// up in the catalogue. The legacy single-`skills_dir` path is
// honoured as a fallback for callers that don't supply a
// workspace-aware view; it falls through to the same merged
+30 -2
View File
@@ -300,7 +300,8 @@ pub fn resolve_skills_dir(workspace: &Path) -> PathBuf {
/// 2. `<workspace>/skills` — flat, project-local.
/// 3. `<workspace>/.opencode/skills` — OpenCode interop.
/// 4. `<workspace>/.claude/skills` — Claude Code interop.
/// 5. [`default_skills_dir`] — global, user-installed.
/// 5. `<workspace>/.cursor/skills` — Cursor interop.
/// 6. [`default_skills_dir`] — global, user-installed.
///
/// Only directories that exist on disk are returned — callers don't
/// need to filter further. Returns an empty vec when nothing is
@@ -312,6 +313,7 @@ pub fn skills_directories(workspace: &Path) -> Vec<PathBuf> {
workspace.join("skills"),
workspace.join(".opencode").join("skills"),
workspace.join(".claude").join("skills"),
workspace.join(".cursor").join("skills"),
default_skills_dir(),
];
let mut out = Vec::new();
@@ -665,10 +667,11 @@ mod tests {
let tmpdir = TempDir::new().unwrap();
let workspace = tmpdir.path();
// Create three of the four candidate dirs (skip `.opencode`).
// Create four of the five workspace candidate dirs (skip `.opencode`).
std::fs::create_dir_all(workspace.join(".agents").join("skills")).unwrap();
std::fs::create_dir_all(workspace.join("skills")).unwrap();
std::fs::create_dir_all(workspace.join(".claude").join("skills")).unwrap();
std::fs::create_dir_all(workspace.join(".cursor").join("skills")).unwrap();
let dirs = super::skills_directories(workspace);
// We don't assert on the global default position because it's
@@ -677,6 +680,7 @@ mod tests {
let agents = workspace.join(".agents").join("skills");
let local = workspace.join("skills");
let claude = workspace.join(".claude").join("skills");
let cursor = workspace.join(".cursor").join("skills");
assert_eq!(dirs.get(idx), Some(&agents), "agents must come first");
idx += 1;
@@ -690,6 +694,12 @@ mod tests {
"missing dir must be omitted, got: {dirs:?}"
);
assert_eq!(dirs.get(idx), Some(&claude), "claude must come after local");
idx += 1;
assert_eq!(
dirs.get(idx),
Some(&cursor),
"cursor must come after claude"
);
}
#[test]
@@ -757,6 +767,24 @@ mod tests {
);
}
#[test]
fn discover_in_workspace_pulls_skills_from_cursor_dir() {
let tmpdir = TempDir::new().unwrap();
let workspace = tmpdir.path();
write_skill(
&workspace.join(".cursor").join("skills"),
"cursor-only",
"for cursor interop",
"body",
);
let registry = super::discover_in_workspace(workspace);
assert!(
registry.get("cursor-only").is_some(),
".cursor/skills must be scanned"
);
}
#[test]
fn render_available_skills_context_for_workspace_picks_up_cross_tool_dirs() {
let tmpdir = TempDir::new().unwrap();
+2 -1
View File
@@ -86,7 +86,8 @@ impl ToolSpec for LoadSkillTool {
// #432: walk every candidate skill directory (workspace
// .agents/skills, skills, .opencode/skills, .claude/skills,
// global default), merging with first-wins precedence. The
// .cursor/skills, global default), merging with first-wins
// precedence. The
// tool's lookup mirrors what the system-prompt skills block
// already lists, so the model never asks for a name it
// can't find.