llm-ide-rules 0.7.0__tar.gz → 0.8.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (26) hide show
  1. {llm_ide_rules-0.7.0 → llm_ide_rules-0.8.0}/PKG-INFO +3 -3
  2. {llm_ide_rules-0.7.0 → llm_ide_rules-0.8.0}/README.md +2 -2
  3. {llm_ide_rules-0.7.0 → llm_ide_rules-0.8.0}/pyproject.toml +4 -4
  4. {llm_ide_rules-0.7.0 → llm_ide_rules-0.8.0}/src/llm_ide_rules/__init__.py +20 -1
  5. {llm_ide_rules-0.7.0 → llm_ide_rules-0.8.0}/src/llm_ide_rules/agents/__init__.py +4 -0
  6. llm_ide_rules-0.8.0/src/llm_ide_rules/agents/agents.py +124 -0
  7. {llm_ide_rules-0.7.0 → llm_ide_rules-0.8.0}/src/llm_ide_rules/agents/base.py +11 -0
  8. {llm_ide_rules-0.7.0 → llm_ide_rules-0.8.0}/src/llm_ide_rules/agents/claude.py +15 -4
  9. {llm_ide_rules-0.7.0 → llm_ide_rules-0.8.0}/src/llm_ide_rules/agents/cursor.py +36 -7
  10. {llm_ide_rules-0.7.0 → llm_ide_rules-0.8.0}/src/llm_ide_rules/agents/gemini.py +28 -4
  11. {llm_ide_rules-0.7.0 → llm_ide_rules-0.8.0}/src/llm_ide_rules/agents/github.py +34 -3
  12. {llm_ide_rules-0.7.0 → llm_ide_rules-0.8.0}/src/llm_ide_rules/agents/opencode.py +6 -0
  13. llm_ide_rules-0.8.0/src/llm_ide_rules/agents/vscode.py +88 -0
  14. llm_ide_rules-0.8.0/src/llm_ide_rules/commands/config.py +46 -0
  15. {llm_ide_rules-0.7.0 → llm_ide_rules-0.8.0}/src/llm_ide_rules/commands/delete.py +111 -5
  16. {llm_ide_rules-0.7.0 → llm_ide_rules-0.8.0}/src/llm_ide_rules/commands/download.py +33 -14
  17. {llm_ide_rules-0.7.0 → llm_ide_rules-0.8.0}/src/llm_ide_rules/commands/explode.py +67 -16
  18. {llm_ide_rules-0.7.0 → llm_ide_rules-0.8.0}/src/llm_ide_rules/commands/implode.py +18 -18
  19. {llm_ide_rules-0.7.0 → llm_ide_rules-0.8.0}/src/llm_ide_rules/commands/mcp.py +1 -1
  20. {llm_ide_rules-0.7.0 → llm_ide_rules-0.8.0}/src/llm_ide_rules/constants.py +1 -1
  21. llm_ide_rules-0.8.0/src/llm_ide_rules/utils.py +118 -0
  22. {llm_ide_rules-0.7.0 → llm_ide_rules-0.8.0}/src/llm_ide_rules/__main__.py +0 -0
  23. {llm_ide_rules-0.7.0 → llm_ide_rules-0.8.0}/src/llm_ide_rules/log.py +0 -0
  24. {llm_ide_rules-0.7.0 → llm_ide_rules-0.8.0}/src/llm_ide_rules/markdown_parser.py +0 -0
  25. {llm_ide_rules-0.7.0 → llm_ide_rules-0.8.0}/src/llm_ide_rules/mcp/__init__.py +0 -0
  26. {llm_ide_rules-0.7.0 → llm_ide_rules-0.8.0}/src/llm_ide_rules/mcp/models.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: llm-ide-rules
3
- Version: 0.7.0
3
+ Version: 0.8.0
4
4
  Summary: CLI tool for managing LLM IDE prompts and rules
5
5
  Keywords: llm,ide,prompts,cursor,copilot
6
6
  Author: Michael Bianco
@@ -18,7 +18,7 @@ Description-Content-Type: text/markdown
18
18
 
19
19
  # Copilot, Cursor, Claude, Gemini, etc LLM Instructions
20
20
 
21
- This project makes it easy to download prompts and implode/explode them so they can be used by various providers.
21
+ This project makes it easy to download prompts and implode/explode them so they can be used by various providers. It's completely vibe coded, but it works.
22
22
 
23
23
  I don't want to be tied to a specific IDE and it's a pain to have to edit instructions for various languages across a ton of different files.
24
24
 
@@ -40,7 +40,7 @@ Different AI coding assistants use different formats for instructions and comman
40
40
  | **GitHub Copilot** | instructions | `.github/copilot-instructions.md` | Single markdown file |
41
41
  | **GitHub Copilot** | instructions | `.github/instructions/*.instructions.md` | Multiple instruction files |
42
42
  | **GitHub Copilot** | prompts | `.github/prompts/*.prompt.md` | YAML frontmatter with `mode: 'agent'` |
43
- | **Gemini CLI** | instructions | `GEMINI.md` | Single markdown file at root |
43
+ | **Gemini CLI** | instructions | `AGENTS.md` | Single markdown file at root |
44
44
  | **Gemini CLI** | commands | `.gemini/commands/*.toml` | TOML format, supports `{{args}}` and shell commands |
45
45
  | **OpenCode** | commands | `.opencode/commands/*.md` | Plain markdown, no frontmatter |
46
46
 
@@ -1,6 +1,6 @@
1
1
  # Copilot, Cursor, Claude, Gemini, etc LLM Instructions
2
2
 
3
- This project makes it easy to download prompts and implode/explode them so they can be used by various providers.
3
+ This project makes it easy to download prompts and implode/explode them so they can be used by various providers. It's completely vibe coded, but it works.
4
4
 
5
5
  I don't want to be tied to a specific IDE and it's a pain to have to edit instructions for various languages across a ton of different files.
6
6
 
@@ -22,7 +22,7 @@ Different AI coding assistants use different formats for instructions and comman
22
22
  | **GitHub Copilot** | instructions | `.github/copilot-instructions.md` | Single markdown file |
23
23
  | **GitHub Copilot** | instructions | `.github/instructions/*.instructions.md` | Multiple instruction files |
24
24
  | **GitHub Copilot** | prompts | `.github/prompts/*.prompt.md` | YAML frontmatter with `mode: 'agent'` |
25
- | **Gemini CLI** | instructions | `GEMINI.md` | Single markdown file at root |
25
+ | **Gemini CLI** | instructions | `AGENTS.md` | Single markdown file at root |
26
26
  | **Gemini CLI** | commands | `.gemini/commands/*.toml` | TOML format, supports `{{args}}` and shell commands |
27
27
  | **OpenCode** | commands | `.opencode/commands/*.md` | Plain markdown, no frontmatter |
28
28
 
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "llm-ide-rules"
3
- version = "0.7.0"
3
+ version = "0.8.0"
4
4
  description = "CLI tool for managing LLM IDE prompts and rules"
5
5
  keywords = ["llm", "ide", "prompts", "cursor", "copilot"]
6
6
  readme = "README.md"
@@ -25,9 +25,9 @@ llm-ide-rules = "llm_ide_rules:main"
25
25
  requires = ["uv_build>=0.9.0,<0.10.0"]
26
26
  build-backend = "uv_build"
27
27
 
28
- # [tool.uv.build-backend]
29
- # # avoids the src/ directory structure
30
- # module-root = ""
28
+ [tool.uv.build-backend]
29
+ # avoids the src/ directory structure
30
+ module-root = "src"
31
31
 
32
32
  [dependency-groups]
33
33
  dev = ["pytest>=8.3.3", "pyright[nodejs]>=1.1.380", "ruff>=0.8.0", "pytest-mock>=3.10.0"]
@@ -12,9 +12,18 @@ from llm_ide_rules.commands.explode import explode_main
12
12
  from llm_ide_rules.commands.implode import cursor, github, claude, gemini, opencode
13
13
  from llm_ide_rules.commands.download import download_main
14
14
  from llm_ide_rules.commands.delete import delete_main
15
+ from llm_ide_rules.commands.config import config_main
15
16
  from llm_ide_rules.commands.mcp import mcp_app
16
17
 
17
- __version__ = "0.7.0"
18
+ __version__ = "0.8.0"
19
+
20
+
21
+ def version_callback(value: bool):
22
+ """Callback to display the version and exit."""
23
+ if value:
24
+ print(f"llm-ide-rules version {__version__}")
25
+ raise typer.Exit()
26
+
18
27
 
19
28
  app = typer.Typer(
20
29
  name="llm_ide_rules",
@@ -31,6 +40,15 @@ def main_callback(
31
40
  "--verbose", "-v", help="Enable verbose logging (sets LOG_LEVEL=DEBUG)"
32
41
  ),
33
42
  ] = False,
43
+ version: Annotated[
44
+ bool | None,
45
+ typer.Option(
46
+ "--version",
47
+ help="Show the version and exit",
48
+ callback=version_callback,
49
+ is_eager=True,
50
+ ),
51
+ ] = None,
34
52
  ):
35
53
  """Global CLI options."""
36
54
  if verbose:
@@ -48,6 +66,7 @@ app.command("download", help="Download LLM instruction files from GitHub reposit
48
66
  download_main
49
67
  )
50
68
  app.command("delete", help="Remove downloaded LLM instruction files")(delete_main)
69
+ app.command("config", help="Configure agents to use AGENTS.md")(config_main)
51
70
 
52
71
  # Create implode sub-typer
53
72
  implode_app = typer.Typer(help="Bundle rule files into a single instruction file")
@@ -6,6 +6,8 @@ from llm_ide_rules.agents.github import GitHubAgent
6
6
  from llm_ide_rules.agents.claude import ClaudeAgent
7
7
  from llm_ide_rules.agents.gemini import GeminiAgent
8
8
  from llm_ide_rules.agents.opencode import OpenCodeAgent
9
+ from llm_ide_rules.agents.agents import AgentsAgent
10
+ from llm_ide_rules.agents.vscode import VSCodeAgent
9
11
 
10
12
  AGENTS: dict[str, type[BaseAgent]] = {
11
13
  "cursor": CursorAgent,
@@ -13,6 +15,8 @@ AGENTS: dict[str, type[BaseAgent]] = {
13
15
  "claude": ClaudeAgent,
14
16
  "gemini": GeminiAgent,
15
17
  "opencode": OpenCodeAgent,
18
+ "agents": AgentsAgent,
19
+ "vscode": VSCodeAgent,
16
20
  }
17
21
 
18
22
 
@@ -0,0 +1,124 @@
1
+ """Agents documentation agent implementation."""
2
+
3
+ from pathlib import Path
4
+ import typer
5
+
6
+ from llm_ide_rules.agents.base import BaseAgent
7
+
8
+
9
+ class AgentsAgent(BaseAgent):
10
+ """Agent for generating AGENTS.md documentation."""
11
+
12
+ name = "agents"
13
+ rules_dir = None
14
+ commands_dir = None
15
+ rule_extension = None
16
+ command_extension = None
17
+
18
+ mcp_global_path = None
19
+ mcp_project_path = None
20
+
21
+ def bundle_rules(
22
+ self, output_file: Path, section_globs: dict[str, str | None] | None = None
23
+ ) -> bool:
24
+ """Agents doesn't support bundling rules."""
25
+ return False
26
+
27
+ def bundle_commands(
28
+ self, output_file: Path, section_globs: dict[str, str | None] | None = None
29
+ ) -> bool:
30
+ """Agents doesn't support bundling commands."""
31
+ return False
32
+
33
+ def write_rule(
34
+ self,
35
+ content_lines: list[str],
36
+ filename: str,
37
+ rules_dir: Path,
38
+ glob_pattern: str | None = None,
39
+ description: str | None = None,
40
+ ) -> None:
41
+ """Agents doesn't support writing rules."""
42
+ pass
43
+
44
+ def write_command(
45
+ self,
46
+ content_lines: list[str],
47
+ filename: str,
48
+ commands_dir: Path,
49
+ section_name: str | None = None,
50
+ ) -> None:
51
+ """Agents doesn't support writing commands."""
52
+ pass
53
+
54
+ def generate_root_doc(
55
+ self,
56
+ general_lines: list[str],
57
+ rules_sections: dict[str, list[str]],
58
+ command_sections: dict[str, list[str]],
59
+ output_dir: Path,
60
+ section_globs: dict[str, str | None] | None = None,
61
+ ) -> None:
62
+ """Generate AGENTS.md files, potentially distributed based on globs."""
63
+ if not section_globs:
64
+ # Fallback to single root AGENTS.md
65
+ content = self.build_root_doc_content(general_lines, rules_sections)
66
+ if content.strip():
67
+ (output_dir / "AGENTS.md").write_text(content)
68
+ return
69
+
70
+ # Group rules by target directory
71
+ rules_by_dir: dict[Path, dict[str, list[str]]] = {}
72
+
73
+ # Always include root directory for rules without specific directory targets
74
+ rules_by_dir[output_dir] = {}
75
+
76
+ for section_name, lines in rules_sections.items():
77
+ target_dir = output_dir
78
+ glob_pattern = section_globs.get(section_name)
79
+
80
+ if glob_pattern and "**" in glob_pattern:
81
+ # Extract path before **
82
+ prefix = glob_pattern.split("**")[0].strip("/")
83
+ potential_dir = output_dir / prefix
84
+
85
+ # Check if directory exists, if not traverse up
86
+ check_dir = potential_dir
87
+ while not check_dir.exists() and check_dir != output_dir:
88
+ check_dir = check_dir.parent
89
+
90
+ if check_dir != potential_dir:
91
+ # We fell back
92
+ if check_dir == output_dir:
93
+ # Only warn if falling back to root from a deep path
94
+ pass
95
+
96
+ # We can log this if we want, but simple traversal is fine.
97
+ # The requirement says "warn to the user".
98
+ rel_potential = potential_dir.relative_to(output_dir)
99
+ rel_actual = check_dir.relative_to(output_dir)
100
+ typer.secho(
101
+ f"Warning: Directory '{rel_potential}' for section '{section_name}' does not exist. "
102
+ f"Placing in '{rel_actual}' instead.",
103
+ fg=typer.colors.YELLOW,
104
+ err=True,
105
+ )
106
+
107
+ target_dir = check_dir
108
+
109
+ if target_dir not in rules_by_dir:
110
+ rules_by_dir[target_dir] = {}
111
+
112
+ rules_by_dir[target_dir][section_name] = lines
113
+
114
+ # Generate AGENTS.md for each directory
115
+ for target_dir, sections in rules_by_dir.items():
116
+ if not sections:
117
+ continue
118
+
119
+ # Only include general instructions in the root AGENTS.md
120
+ current_general_lines = general_lines if target_dir == output_dir else []
121
+
122
+ content = self.build_root_doc_content(current_general_lines, sections)
123
+ if content.strip():
124
+ (target_dir / "AGENTS.md").write_text(content)
@@ -42,6 +42,7 @@ class BaseAgent(ABC):
42
42
  filename: str,
43
43
  rules_dir: Path,
44
44
  glob_pattern: str | None = None,
45
+ description: str | None = None,
45
46
  ) -> None:
46
47
  """Write a single rule file."""
47
48
  ...
@@ -57,12 +58,21 @@ class BaseAgent(ABC):
57
58
  """Write a single command file."""
58
59
  ...
59
60
 
61
+ def configure_agents_md(self, base_dir: Path) -> bool:
62
+ """Configure the agent to use AGENTS.md as context (default: no-op).
63
+
64
+ Returns:
65
+ bool: True if configuration was applied, False otherwise.
66
+ """
67
+ return False
68
+
60
69
  def generate_root_doc(
61
70
  self,
62
71
  general_lines: list[str],
63
72
  rules_sections: dict[str, list[str]],
64
73
  command_sections: dict[str, list[str]],
65
74
  output_dir: Path,
75
+ section_globs: dict[str, str | None] | None = None,
66
76
  ) -> None:
67
77
  """Generate a root documentation file (e.g. CLAUDE.md) if supported."""
68
78
  pass
@@ -278,6 +288,7 @@ def write_rule_file(path: Path, header_yaml: str, content_lines: list[str]) -> N
278
288
  """Write a rule file with front matter and content."""
279
289
  trimmed_content = trim_content(content_lines)
280
290
  output = header_yaml.strip() + "\n" + "".join(trimmed_content)
291
+ path.parent.mkdir(parents=True, exist_ok=True)
281
292
  path.write_text(output)
282
293
 
283
294
 
@@ -77,6 +77,7 @@ class ClaudeAgent(BaseAgent):
77
77
  filename: str,
78
78
  rules_dir: Path,
79
79
  glob_pattern: str | None = None,
80
+ description: str | None = None,
80
81
  ) -> None:
81
82
  """Claude Code doesn't support rules."""
82
83
  pass
@@ -93,6 +94,7 @@ class ClaudeAgent(BaseAgent):
93
94
  filepath = commands_dir / f"{filename}{extension}"
94
95
 
95
96
  trimmed = trim_content(content_lines)
97
+ filepath.parent.mkdir(parents=True, exist_ok=True)
96
98
  filepath.write_text("".join(trimmed))
97
99
 
98
100
  def generate_root_doc(
@@ -101,8 +103,17 @@ class ClaudeAgent(BaseAgent):
101
103
  rules_sections: dict[str, list[str]],
102
104
  command_sections: dict[str, list[str]],
103
105
  output_dir: Path,
106
+ section_globs: dict[str, str | None] | None = None,
104
107
  ) -> None:
105
- """Generate CLAUDE.md from rules."""
106
- content = self.build_root_doc_content(general_lines, rules_sections)
107
- if content.strip():
108
- (output_dir / "CLAUDE.md").write_text(content)
108
+ """Generate CLAUDE.md that references AGENTS.md."""
109
+ (output_dir / "CLAUDE.md").write_text("@./AGENTS.md\n")
110
+
111
+ def configure_agents_md(self, base_dir: Path) -> bool:
112
+ """Create CLAUDE.md pointing to AGENTS.md if AGENTS.md exists and CLAUDE.md doesn't."""
113
+ agents_md = base_dir / "AGENTS.md"
114
+ claude_md = base_dir / "CLAUDE.md"
115
+
116
+ if agents_md.exists() and not claude_md.exists():
117
+ claude_md.write_text("@./AGENTS.md\n")
118
+ return True
119
+ return False
@@ -56,18 +56,20 @@ class CursorAgent(BaseAgent):
56
56
  if not file_content:
57
57
  continue
58
58
 
59
- # Extract header from file content before stripping it
60
59
  lines = file_content.splitlines()
61
60
  extracted_header = None
61
+ glob_pattern = None
62
+
62
63
  for line in lines:
63
64
  if line.startswith("## "):
64
65
  extracted_header = line[3:].strip()
65
66
  break
66
67
 
68
+ glob_pattern = self._extract_glob_from_frontmatter(file_content)
69
+
67
70
  content = strip_yaml_frontmatter(file_content)
68
71
  content = strip_header(content)
69
72
 
70
- # Use extracted header if available, otherwise resolve from filename
71
73
  if extracted_header:
72
74
  header = extracted_header
73
75
  else:
@@ -77,6 +79,8 @@ class CursorAgent(BaseAgent):
77
79
 
78
80
  if rule_file.stem != "general":
79
81
  content_parts.append(f"## {header}\n\n")
82
+ if glob_pattern:
83
+ content_parts.append(f"globs: {glob_pattern}\n\n")
80
84
 
81
85
  content_parts.append(content)
82
86
  content_parts.append("\n\n")
@@ -87,6 +91,21 @@ class CursorAgent(BaseAgent):
87
91
  output_file.write_text("".join(content_parts))
88
92
  return True
89
93
 
94
+ def _extract_glob_from_frontmatter(self, content: str) -> str | None:
95
+ """Extract glob pattern from YAML frontmatter."""
96
+ lines = content.splitlines()
97
+ if not lines or lines[0].strip() != "---":
98
+ return None
99
+
100
+ for i in range(1, len(lines)):
101
+ if lines[i].strip() == "---":
102
+ break
103
+ if lines[i].startswith("globs:"):
104
+ glob_value = lines[i][6:].strip()
105
+ return glob_value if glob_value else None
106
+
107
+ return None
108
+
90
109
  def bundle_commands(
91
110
  self, output_file: Path, section_globs: dict[str, str | None] | None = None
92
111
  ) -> bool:
@@ -136,27 +155,32 @@ class CursorAgent(BaseAgent):
136
155
  filename: str,
137
156
  rules_dir: Path,
138
157
  glob_pattern: str | None = None,
158
+ description: str | None = None,
139
159
  ) -> None:
140
160
  """Write a Cursor rule file (.mdc) with YAML frontmatter."""
141
161
  extension = self.rule_extension or ".mdc"
142
162
  filepath = rules_dir / f"{filename}{extension}"
143
163
 
164
+ desc = description or filename.replace("-", " ").title()
165
+
144
166
  if glob_pattern and glob_pattern != "manual":
145
167
  header_yaml = f"""---
146
- description:
168
+ description: {desc}
147
169
  globs: {glob_pattern}
148
170
  alwaysApply: false
149
171
  ---
150
172
  """
151
173
  elif glob_pattern == "manual":
152
- header_yaml = """---
153
- description:
174
+ header_yaml = f"""---
175
+ description: {desc}
176
+ globs:
154
177
  alwaysApply: false
155
178
  ---
156
179
  """
157
180
  else:
158
- header_yaml = """---
159
- description:
181
+ header_yaml = f"""---
182
+ description: {desc}
183
+ globs:
160
184
  alwaysApply: true
161
185
  ---
162
186
  """
@@ -174,6 +198,7 @@ alwaysApply: true
174
198
  filepath = commands_dir / f"{filename}{extension}"
175
199
 
176
200
  trimmed = trim_content(content_lines)
201
+ filepath.parent.mkdir(parents=True, exist_ok=True)
177
202
  filepath.write_text("".join(trimmed))
178
203
 
179
204
  def write_prompt(
@@ -197,3 +222,7 @@ alwaysApply: true
197
222
 
198
223
  output_parts.extend(filtered_content)
199
224
  filepath.write_text("".join(output_parts))
225
+
226
+ def configure_agents_md(self, base_dir: Path) -> bool:
227
+ """Cursor doesn't require explicit configuration for AGENTS.md."""
228
+ return False
@@ -82,6 +82,7 @@ class GeminiAgent(BaseAgent):
82
82
  filename: str,
83
83
  rules_dir: Path,
84
84
  glob_pattern: str | None = None,
85
+ description: str | None = None,
85
86
  ) -> None:
86
87
  """Gemini CLI doesn't support rules."""
87
88
  pass
@@ -124,6 +125,7 @@ class GeminiAgent(BaseAgent):
124
125
 
125
126
  # tomli-w will handle escaping and multiline strings automatically
126
127
  output = tomli_w.dumps(data)
128
+ filepath.parent.mkdir(parents=True, exist_ok=True)
127
129
  filepath.write_text(output)
128
130
 
129
131
  def transform_mcp_server(self, server: McpServer) -> dict:
@@ -170,8 +172,30 @@ class GeminiAgent(BaseAgent):
170
172
  rules_sections: dict[str, list[str]],
171
173
  command_sections: dict[str, list[str]],
172
174
  output_dir: Path,
175
+ section_globs: dict[str, str | None] | None = None,
173
176
  ) -> None:
174
- """Generate GEMINI.md from rules."""
175
- content = self.build_root_doc_content(general_lines, rules_sections)
176
- if content.strip():
177
- (output_dir / "GEMINI.md").write_text(content)
177
+ """Gemini CLI uses AGENTS.md (generated by 'agents' agent). This is a no-op."""
178
+ pass
179
+
180
+ def configure_agents_md(self, base_dir: Path) -> bool:
181
+ """Configure Gemini CLI to use AGENTS.md."""
182
+ from llm_ide_rules.utils import modify_json_file
183
+
184
+ settings_path = base_dir / ".gemini" / "settings.json"
185
+
186
+ # Based on research, generic context setting might be:
187
+ updates = {"agent.instructionFile": "AGENTS.md"}
188
+
189
+ return modify_json_file(settings_path, updates)
190
+
191
+ def check_agents_md_config(self, base_dir: Path) -> bool:
192
+ """Check if Gemini CLI is configured to use AGENTS.md."""
193
+ settings_path = base_dir / ".gemini" / "settings.json"
194
+ if not settings_path.exists():
195
+ return False
196
+
197
+ try:
198
+ data = json.loads(settings_path.read_text())
199
+ return data.get("agent.instructionFile") == "AGENTS.md"
200
+ except Exception:
201
+ return False
@@ -57,17 +57,21 @@ class GitHubAgent(BaseAgent):
57
57
  content_parts.append("\n\n")
58
58
 
59
59
  for instr_file in ordered_instructions:
60
- content = instr_file.read_text().strip()
61
- if not content:
60
+ file_content = instr_file.read_text().strip()
61
+ if not file_content:
62
62
  continue
63
63
 
64
- content = strip_yaml_frontmatter(content)
64
+ apply_to_pattern = self._extract_apply_to_from_frontmatter(file_content)
65
+
66
+ content = strip_yaml_frontmatter(file_content)
65
67
  content = strip_header(content)
66
68
  base_stem = instr_file.stem.replace(".instructions", "")
67
69
  header = resolve_header_from_stem(
68
70
  base_stem, section_globs if section_globs else {}
69
71
  )
70
72
  content_parts.append(f"## {header}\n\n")
73
+ if apply_to_pattern:
74
+ content_parts.append(f"globs: {apply_to_pattern}\n\n")
71
75
  content_parts.append(content)
72
76
  content_parts.append("\n\n")
73
77
 
@@ -77,6 +81,21 @@ class GitHubAgent(BaseAgent):
77
81
  output_file.write_text("".join(content_parts))
78
82
  return True
79
83
 
84
+ def _extract_apply_to_from_frontmatter(self, content: str) -> str | None:
85
+ """Extract applyTo pattern from YAML frontmatter."""
86
+ lines = content.splitlines()
87
+ if not lines or lines[0].strip() != "---":
88
+ return None
89
+
90
+ for i in range(1, len(lines)):
91
+ if lines[i].strip() == "---":
92
+ break
93
+ if lines[i].startswith("applyTo:"):
94
+ apply_to_value = lines[i][8:].strip().strip('"').strip("'")
95
+ return apply_to_value if apply_to_value else None
96
+
97
+ return None
98
+
80
99
  def bundle_commands(
81
100
  self, output_file: Path, section_globs: dict[str, str | None] | None = None
82
101
  ) -> bool:
@@ -141,6 +160,7 @@ class GitHubAgent(BaseAgent):
141
160
  filename: str,
142
161
  rules_dir: Path,
143
162
  glob_pattern: str | None = None,
163
+ description: str | None = None,
144
164
  ) -> None:
145
165
  """Write a GitHub instruction file (.instructions.md) with YAML frontmatter."""
146
166
  extension = self.rule_extension or ".instructions.md"
@@ -172,6 +192,7 @@ applyTo: "{glob_pattern}"
172
192
  )
173
193
 
174
194
  frontmatter = f"---\nmode: 'agent'\ndescription: '{description}'\n---\n"
195
+ filepath.parent.mkdir(parents=True, exist_ok=True)
175
196
  filepath.write_text(frontmatter + "".join(filtered_content))
176
197
 
177
198
  def write_general_instructions(
@@ -210,3 +231,13 @@ applyTo: "{glob_pattern}"
210
231
  args=config.get("args", []),
211
232
  env=config.get("env"),
212
233
  )
234
+
235
+ def configure_agents_md(self, base_dir: Path) -> bool:
236
+ """Configure VS Code to use AGENTS.md."""
237
+ from llm_ide_rules.utils import modify_json_file
238
+
239
+ settings_path = base_dir / ".vscode" / "settings.json"
240
+
241
+ updates = {"chat.useAgentsMdFile": True, "chat.useNestedAgentsMdFiles": True}
242
+
243
+ return modify_json_file(settings_path, updates)
@@ -79,6 +79,7 @@ class OpenCodeAgent(BaseAgent):
79
79
  filename: str,
80
80
  rules_dir: Path,
81
81
  glob_pattern: str | None = None,
82
+ description: str | None = None,
82
83
  ) -> None:
83
84
  """OpenCode doesn't support rules."""
84
85
  pass
@@ -95,6 +96,7 @@ class OpenCodeAgent(BaseAgent):
95
96
  filepath = commands_dir / f"{filename}{extension}"
96
97
 
97
98
  trimmed = trim_content(content_lines)
99
+ filepath.parent.mkdir(parents=True, exist_ok=True)
98
100
  filepath.write_text("".join(trimmed))
99
101
 
100
102
  def transform_mcp_server(self, server: McpServer) -> dict:
@@ -128,3 +130,7 @@ class OpenCodeAgent(BaseAgent):
128
130
  args=command_array[1:] if len(command_array) > 1 else [],
129
131
  env=config.get("environment"),
130
132
  )
133
+
134
+ def configure_agents_md(self, base_dir: Path) -> bool:
135
+ """OpenCode has native support, no configuration needed."""
136
+ return False
@@ -0,0 +1,88 @@
1
+ """VS Code agent implementation."""
2
+
3
+ from pathlib import Path
4
+
5
+ from llm_ide_rules.agents.base import BaseAgent
6
+ from llm_ide_rules.mcp import McpServer
7
+
8
+
9
+ class VSCodeAgent(BaseAgent):
10
+ """Agent for VS Code (native MCP support)."""
11
+
12
+ name = "vscode"
13
+ rules_dir = None # VS Code typically uses .github (handled by GitHubAgent)
14
+ commands_dir = None
15
+ rule_extension = None
16
+ command_extension = None
17
+
18
+ mcp_global_path = None # VS Code user settings are complex, focusing on workspace
19
+ mcp_project_path = ".vscode/mcp.json"
20
+ mcp_root_key = "servers"
21
+
22
+ def bundle_rules(
23
+ self, output_file: Path, section_globs: dict[str, str | None] | None = None
24
+ ) -> bool:
25
+ """VS Code doesn't support rules directly (uses GitHub Copilot)."""
26
+ return False
27
+
28
+ def bundle_commands(
29
+ self, output_file: Path, section_globs: dict[str, str | None] | None = None
30
+ ) -> bool:
31
+ """VS Code doesn't support commands directly."""
32
+ return False
33
+
34
+ def write_rule(
35
+ self,
36
+ content_lines: list[str],
37
+ filename: str,
38
+ rules_dir: Path,
39
+ glob_pattern: str | None = None,
40
+ description: str | None = None,
41
+ ) -> None:
42
+ """VS Code doesn't support rules."""
43
+ pass
44
+
45
+ def write_command(
46
+ self,
47
+ content_lines: list[str],
48
+ filename: str,
49
+ commands_dir: Path,
50
+ section_name: str | None = None,
51
+ ) -> None:
52
+ """VS Code doesn't support commands."""
53
+ pass
54
+
55
+ def transform_mcp_server(self, server: McpServer) -> dict:
56
+ """Transform unified server to VS Code format."""
57
+ # VS Code uses "env" key, similar to standard MCP
58
+ base = {}
59
+ if server.env:
60
+ base["env"] = server.env
61
+
62
+ if server.url:
63
+ # VS Code supports SSE via "url" (or "type": "sse"?)
64
+ # Research indicates basic MCP config in VS Code is similar to Claude
65
+ # but usually requires "command" for stdio.
66
+ # However, for remote/SSE, it might just need 'url'.
67
+ # Let's assume standard 'url' for now based on 'mcp.json' schema compatibility.
68
+ return {"url": server.url, **base}
69
+
70
+ return {
71
+ "command": server.command,
72
+ "args": server.args or [],
73
+ **base,
74
+ }
75
+
76
+ def reverse_transform_mcp_server(self, name: str, config: dict) -> McpServer:
77
+ """Transform VS Code config back to unified format."""
78
+ if "url" in config:
79
+ return McpServer(
80
+ url=config["url"],
81
+ env=config.get("env"),
82
+ )
83
+
84
+ return McpServer(
85
+ command=config["command"],
86
+ args=config.get("args", []),
87
+ env=config.get("env"),
88
+ )