llm-ide-rules 0.7.0__py3-none-any.whl → 0.8.0__py3-none-any.whl
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.
- llm_ide_rules/__init__.py +20 -1
- llm_ide_rules/agents/__init__.py +4 -0
- llm_ide_rules/agents/agents.py +124 -0
- llm_ide_rules/agents/base.py +11 -0
- llm_ide_rules/agents/claude.py +15 -4
- llm_ide_rules/agents/cursor.py +36 -7
- llm_ide_rules/agents/gemini.py +28 -4
- llm_ide_rules/agents/github.py +34 -3
- llm_ide_rules/agents/opencode.py +6 -0
- llm_ide_rules/agents/vscode.py +88 -0
- llm_ide_rules/commands/config.py +46 -0
- llm_ide_rules/commands/delete.py +111 -5
- llm_ide_rules/commands/download.py +33 -14
- llm_ide_rules/commands/explode.py +67 -16
- llm_ide_rules/commands/implode.py +18 -18
- llm_ide_rules/commands/mcp.py +1 -1
- llm_ide_rules/constants.py +1 -1
- llm_ide_rules/utils.py +118 -0
- {llm_ide_rules-0.7.0.dist-info → llm_ide_rules-0.8.0.dist-info}/METADATA +3 -3
- llm_ide_rules-0.8.0.dist-info/RECORD +27 -0
- llm_ide_rules-0.7.0.dist-info/RECORD +0 -23
- {llm_ide_rules-0.7.0.dist-info → llm_ide_rules-0.8.0.dist-info}/WHEEL +0 -0
- {llm_ide_rules-0.7.0.dist-info → llm_ide_rules-0.8.0.dist-info}/entry_points.txt +0 -0
llm_ide_rules/__init__.py
CHANGED
|
@@ -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.
|
|
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")
|
llm_ide_rules/agents/__init__.py
CHANGED
|
@@ -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)
|
llm_ide_rules/agents/base.py
CHANGED
|
@@ -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
|
|
llm_ide_rules/agents/claude.py
CHANGED
|
@@ -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
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
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
|
llm_ide_rules/agents/cursor.py
CHANGED
|
@@ -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
|
llm_ide_rules/agents/gemini.py
CHANGED
|
@@ -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
|
-
"""
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
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
|
llm_ide_rules/agents/github.py
CHANGED
|
@@ -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
|
-
|
|
61
|
-
if not
|
|
60
|
+
file_content = instr_file.read_text().strip()
|
|
61
|
+
if not file_content:
|
|
62
62
|
continue
|
|
63
63
|
|
|
64
|
-
|
|
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)
|
llm_ide_rules/agents/opencode.py
CHANGED
|
@@ -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
|
+
)
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
"""Command to configure agents to use AGENTS.md."""
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
import typer
|
|
6
|
+
from typing_extensions import Annotated
|
|
7
|
+
|
|
8
|
+
from llm_ide_rules.agents import get_agent, get_all_agents
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def config_main(
|
|
12
|
+
agent: Annotated[
|
|
13
|
+
str | None,
|
|
14
|
+
typer.Option(help="Specific agent to configure (cursor, github, etc.)"),
|
|
15
|
+
] = None,
|
|
16
|
+
):
|
|
17
|
+
"""
|
|
18
|
+
Configure agents to use AGENTS.md as their context source.
|
|
19
|
+
"""
|
|
20
|
+
base_dir = Path.cwd()
|
|
21
|
+
|
|
22
|
+
agents_to_configure = []
|
|
23
|
+
if agent:
|
|
24
|
+
try:
|
|
25
|
+
agents_to_configure.append(get_agent(agent))
|
|
26
|
+
except ValueError as e:
|
|
27
|
+
typer.echo(f"Error: {e}", err=True)
|
|
28
|
+
raise typer.Exit(code=1)
|
|
29
|
+
else:
|
|
30
|
+
agents_to_configure = get_all_agents()
|
|
31
|
+
|
|
32
|
+
for agent_inst in agents_to_configure:
|
|
33
|
+
if agent_inst.name == "agents":
|
|
34
|
+
continue
|
|
35
|
+
|
|
36
|
+
try:
|
|
37
|
+
configured = agent_inst.configure_agents_md(base_dir)
|
|
38
|
+
if configured:
|
|
39
|
+
typer.echo(
|
|
40
|
+
typer.style(f"Configured {agent_inst.name}", fg=typer.colors.GREEN)
|
|
41
|
+
)
|
|
42
|
+
else:
|
|
43
|
+
msg = f"Skipped {agent_inst.name} (no changes needed or not applicable)"
|
|
44
|
+
typer.echo(typer.style(msg, fg=typer.colors.YELLOW))
|
|
45
|
+
except Exception as e:
|
|
46
|
+
typer.echo(f"Failed to configure {agent_inst.name}: {e}", err=True)
|