convert_to_cursor.py 6.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222
  1. #!/usr/bin/env python3
  2. """
  3. Convert Agent Skills to Cursor format
  4. This script converts Agent Skills to Cursor custom instruction format.
  5. Cursor supports custom instructions via .cursorrules files or project context.
  6. Usage:
  7. python convert_to_cursor.py <skill_path> [output_path]
  8. Example:
  9. python convert_to_cursor.py ../../skills/course-designer .cursor/rules/
  10. """
  11. import sys
  12. import re
  13. import shutil
  14. from pathlib import Path
  15. try:
  16. import yaml
  17. HAS_YAML = True
  18. except ImportError:
  19. HAS_YAML = False
  20. def parse_yaml_simple(text):
  21. """Simple YAML parser for frontmatter (handles basic key: value pairs)"""
  22. result = {}
  23. for line in text.split('\n'):
  24. line = line.strip()
  25. if ':' in line and not line.startswith('#'):
  26. key, value = line.split(':', 1)
  27. key = key.strip()
  28. value = value.strip().strip('"').strip("'")
  29. result[key] = value
  30. return result
  31. def extract_frontmatter(content):
  32. """Extract YAML frontmatter from SKILL.md"""
  33. frontmatter_pattern = r'^---\n(.*?)\n---\n'
  34. match = re.match(frontmatter_pattern, content, re.DOTALL)
  35. if match:
  36. frontmatter_text = match.group(1)
  37. if HAS_YAML:
  38. frontmatter = yaml.safe_load(frontmatter_text)
  39. else:
  40. frontmatter = parse_yaml_simple(frontmatter_text)
  41. body_start = match.end()
  42. body = content[body_start:].strip()
  43. return frontmatter, body
  44. else:
  45. return {}, content.strip()
  46. def rewrite_markdown_links(content, skill_name, resource_dirs):
  47. """
  48. Rewrite markdown links to point to assets directory
  49. Args:
  50. content: Markdown content
  51. skill_name: Name of the skill
  52. resource_dirs: List of resource directories that were copied
  53. Returns:
  54. Updated content
  55. """
  56. def replace_link(match):
  57. text = match.group(1)
  58. url = match.group(2)
  59. # Skip absolute links, anchors, or existing asset links
  60. if url.startswith(('http://', 'https://', '#', '/')):
  61. return match.group(0)
  62. # Check if link points to a resource directory
  63. for res_dir in resource_dirs:
  64. if url.startswith(res_dir + '/') or url == res_dir:
  65. return f"[{text}](assets/{skill_name}/{url})"
  66. return match.group(0)
  67. # Replace [text](url) links
  68. content = re.sub(r'\[([^\]]+)\]\(([^\)]+)\)', replace_link, content)
  69. # Replace `path` references (common in our skills)
  70. def replace_code_path(match):
  71. path = match.group(1)
  72. # Check if path points to a resource directory
  73. for res_dir in resource_dirs:
  74. if path.startswith(res_dir + '/') or path == res_dir:
  75. return f"`assets/{skill_name}/{path}`"
  76. return match.group(0)
  77. content = re.sub(r'`([^`\n]+)`', replace_code_path, content)
  78. return content
  79. def convert_to_cursor_rule(skill_path, output_dir=None):
  80. """
  81. Convert Agent Skill to Cursor custom instruction format
  82. Args:
  83. skill_path: Path to skill directory containing SKILL.md
  84. output_dir: Output directory for .cursorrules file (optional)
  85. """
  86. skill_path = Path(skill_path)
  87. skill_file = skill_path / "SKILL.md"
  88. if not skill_file.exists():
  89. print(f"Error: SKILL.md not found in {skill_path}")
  90. return None
  91. # Read skill file
  92. with open(skill_file, "r", encoding="utf-8") as f:
  93. content = f.read()
  94. # Extract frontmatter and body
  95. frontmatter, body = extract_frontmatter(content)
  96. if not frontmatter:
  97. print(f"Warning: No frontmatter found in {skill_file}")
  98. frontmatter = {"name": skill_path.name, "description": ""}
  99. skill_name = frontmatter.get('name', skill_path.name)
  100. # Handle resources
  101. resource_dirs = ["scripts", "references", "assets", "api", "templates", "examples"]
  102. copied_resources = []
  103. if output_dir:
  104. output_dir = Path(output_dir)
  105. assets_dir = output_dir / "assets" / skill_name
  106. for res_dir in resource_dirs:
  107. src_dir = skill_path / res_dir
  108. if src_dir.exists() and src_dir.is_dir():
  109. dest_dir = assets_dir / res_dir
  110. if dest_dir.exists():
  111. shutil.rmtree(dest_dir)
  112. shutil.copytree(src_dir, dest_dir)
  113. copied_resources.append(res_dir)
  114. # Rewrite links in body
  115. if copied_resources:
  116. body = rewrite_markdown_links(body, skill_name, copied_resources)
  117. # Create Cursor rule format (MDC)
  118. # See: https://docs.nvidia.com/nemo/agent-toolkit/1.2/extend/cursor-rules-developer-guide.html
  119. mdc_content = f"""---
  120. description: {frontmatter.get('description', 'No description available')}
  121. globs: {frontmatter.get('globs', ['**/*'])}
  122. ---
  123. # {skill_name.replace('-', ' ').title()}
  124. ## Description
  125. {frontmatter.get('description', 'No description available')}
  126. ## When to Use
  127. Use this skill when the user asks about: {frontmatter.get('description', 'related tasks')}
  128. ## Instructions
  129. {body}
  130. """
  131. # Write output
  132. if output_dir:
  133. output_dir.mkdir(parents=True, exist_ok=True)
  134. # Use .mdc extension for Cursor rules
  135. output_file = output_dir / f"{skill_name}.mdc"
  136. with open(output_file, "w", encoding="utf-8") as f:
  137. f.write(mdc_content)
  138. log_msg = f"✅ Converted to Cursor format: {output_file}"
  139. if copied_resources:
  140. log_msg += f" (Resources copied: {', '.join(copied_resources)})"
  141. print(log_msg)
  142. return output_file
  143. else:
  144. return mdc_content
  145. def convert_all_skills(skills_dir, output_dir):
  146. """Convert all skills in a directory"""
  147. skills_dir = Path(skills_dir)
  148. output_dir = Path(output_dir)
  149. output_dir.mkdir(parents=True, exist_ok=True)
  150. converted = []
  151. for skill_dir in skills_dir.iterdir():
  152. if skill_dir.is_dir() and (skill_dir / "SKILL.md").exists():
  153. try:
  154. result = convert_to_cursor_rule(skill_dir, output_dir)
  155. if result:
  156. converted.append(skill_dir.name)
  157. except Exception as e:
  158. print(f"Error converting {skill_dir.name}: {e}")
  159. print(f"\n✅ Converted {len(converted)} skills: {', '.join(converted)}")
  160. return converted
  161. if __name__ == "__main__":
  162. if len(sys.argv) < 2:
  163. print("Usage: python convert_to_cursor.py <skill_path> [output_path]")
  164. print(" or: python convert_to_cursor.py --all <skills_dir> <output_dir>")
  165. sys.exit(1)
  166. if sys.argv[1] == "--all":
  167. if len(sys.argv) < 4:
  168. print("Usage: python convert_to_cursor.py --all <skills_dir> <output_dir>")
  169. sys.exit(1)
  170. convert_all_skills(sys.argv[2], sys.argv[3])
  171. else:
  172. skill_path = sys.argv[1]
  173. output_path = sys.argv[2] if len(sys.argv) > 2 else None
  174. convert_to_cursor_rule(skill_path, output_path)