11"""
22Cursor target implementation for lola.
33
4- Cursor uses .mdc rule files with alwaysApply: true for instructions,
5- avoiding inconsistent AGENTS.md loading behavior.
4+ Cursor 2.4+ supports:
5+ - Skills in .cursor/skills/<skill-name>/SKILL.md (Agent Skills standard)
6+ - Subagents in .cursor/agents/<name>.md
7+ - Rules in .cursor/rules/*.mdc for always-on instructions
68"""
79
810from __future__ import annotations
911
10- import re
12+ import shutil
1113from pathlib import Path
1214
1315import lola .config as config
14- import lola .frontmatter as fm
15- from .base import MCPSupportMixin , BaseAssistantTarget , _generate_passthrough_command
16-
17-
18- def _rewrite_relative_paths (content : str , assets_path : str ) -> str :
19- """Rewrite relative paths in content to point to the assets location."""
20- patterns = [
21- (r'(\s|^|"|\x27|\(|`)(\.\./[^\s"\x27)\]`]+)' , r"\1" + assets_path + r"/\2" ),
22- (r'(\s|^|"|\x27|\(|`)(\./([^\s"\x27)\]`]+))' , r"\1" + assets_path + r"/\3" ),
23- ]
24- result = content
25- for pattern , replacement in patterns :
26- result = re .sub (pattern , replacement , result )
27- result = re .sub (r"(?<!:)//+" , "/" , result )
28- return result
16+ from .base import (
17+ MCPSupportMixin ,
18+ BaseAssistantTarget ,
19+ _generate_passthrough_command ,
20+ _generate_agent_with_frontmatter ,
21+ )
2922
3023
3124class CursorTarget (MCPSupportMixin , BaseAssistantTarget ):
32- """Target for Cursor assistant.
33-
34- Cursor uses .mdc rule files with alwaysApply: true for instructions,
35- avoiding inconsistent AGENTS.md loading behavior.
36- """
25+ """Target for Cursor assistant."""
3726
3827 name = "cursor"
39- supports_agents = False
28+ supports_agents = True
4029
4130 def get_skill_path (self , project_path : str ) -> Path :
42- return Path (project_path ) / ".cursor" / "rules "
31+ return Path (project_path ) / ".cursor" / "skills "
4332
4433 def get_command_path (self , project_path : str ) -> Path :
4534 return Path (project_path ) / ".cursor" / "commands"
4635
36+ def get_agent_path (self , project_path : str ) -> Path :
37+ return Path (project_path ) / ".cursor" / "agents"
38+
4739 def get_instructions_path (self , project_path : str ) -> Path :
4840 return Path (project_path ) / ".cursor" / "rules"
4941
@@ -55,44 +47,36 @@ def generate_skill(
5547 source_path : Path ,
5648 dest_path : Path ,
5749 skill_name : str ,
58- project_path : str | None = None ,
50+ project_path : str | None = None , # noqa: ARG002
5951 ) -> bool :
60- """Convert skill to Cursor MDC format."""
52+ """Copy skill directory with SKILL.md and supporting files.
53+
54+ Cursor 2.4+ uses the Agent Skills standard with SKILL.md files.
55+ """
6156 if not source_path .exists ():
6257 return False
6358
64- dest_path .mkdir (parents = True , exist_ok = True )
59+ skill_dest = dest_path / skill_name
60+ skill_dest .mkdir (parents = True , exist_ok = True )
6561
66- # Calculate assets path for relative path rewriting
67- if project_path :
68- try :
69- relative_source = source_path .relative_to (Path (project_path ))
70- assets_path = str (relative_source )
71- except ValueError :
72- assets_path = str (source_path )
73- else :
74- assets_path = str (source_path )
75-
76- # Convert SKILL.md to MDC format
62+ # Copy SKILL.md
7763 skill_file = source_path / config .SKILL_FILE
7864 if not skill_file .exists ():
7965 return False
8066
81- content = skill_file .read_text ()
82- frontmatter , body = fm .parse (content )
83-
84- if assets_path :
85- body = _rewrite_relative_paths (body , assets_path )
86-
87- mdc_lines = ["---" ]
88- mdc_lines .append (f"description: { frontmatter .get ('description' , '' )} " )
89- mdc_lines .append ("globs:" )
90- mdc_lines .append ("alwaysApply: false" )
91- mdc_lines .append ("---" )
92- mdc_lines .append ("" )
93- mdc_lines .append (body )
94-
95- (dest_path / f"{ skill_name } .mdc" ).write_text ("\n " .join (mdc_lines ))
67+ (skill_dest / "SKILL.md" ).write_text (skill_file .read_text ())
68+
69+ # Copy supporting files
70+ for item in source_path .iterdir ():
71+ if item .name == "SKILL.md" :
72+ continue
73+ dest_item = skill_dest / item .name
74+ if item .is_dir ():
75+ if dest_item .exists ():
76+ shutil .rmtree (dest_item )
77+ shutil .copytree (item , dest_item )
78+ else :
79+ shutil .copy2 (item , dest_item )
9680 return True
9781
9882 def generate_command (
@@ -105,6 +89,29 @@ def generate_command(
10589 filename = self .get_command_filename (module_name , cmd_name )
10690 return _generate_passthrough_command (source_path , dest_dir , filename )
10791
92+ def generate_agent (
93+ self ,
94+ source_path : Path ,
95+ dest_dir : Path ,
96+ agent_name : str ,
97+ module_name : str ,
98+ ) -> bool :
99+ """Generate agent file with Cursor-compatible frontmatter.
100+
101+ Cursor subagents use:
102+ - name: unique identifier (defaults to filename)
103+ - description: when to use this agent
104+ - model: "fast", "inherit", or specific model ID
105+ """
106+ filename = self .get_agent_filename (module_name , agent_name )
107+ agent_full_name = filename .removesuffix (".md" )
108+ return _generate_agent_with_frontmatter (
109+ source_path ,
110+ dest_dir ,
111+ filename ,
112+ {"name" : agent_full_name , "model" : "inherit" },
113+ )
114+
108115 def generate_instructions (
109116 self ,
110117 source_path : Path ,
@@ -135,14 +142,6 @@ def generate_instructions(
135142 mdc_file .write_text ("\n " .join (mdc_lines ))
136143 return True
137144
138- def remove_skill (self , dest_path : Path , skill_name : str ) -> bool :
139- """Remove .mdc file instead of directory."""
140- mdc_file = dest_path / f"{ skill_name } .mdc"
141- if mdc_file .exists ():
142- mdc_file .unlink ()
143- return True
144- return False
145-
146145 def remove_instructions (self , dest_path : Path , module_name : str ) -> bool :
147146 """Remove the module's instructions .mdc file."""
148147 mdc_file = dest_path / f"{ module_name } -instructions.mdc"
0 commit comments