llm-ide-rules 0.6.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 +63 -6
- llm_ide_rules/agents/claude.py +31 -4
- llm_ide_rules/agents/cursor.py +64 -14
- llm_ide_rules/agents/gemini.py +44 -4
- llm_ide_rules/agents/github.py +49 -13
- llm_ide_rules/agents/opencode.py +14 -4
- 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 +123 -14
- llm_ide_rules/commands/explode.py +169 -170
- llm_ide_rules/commands/implode.py +25 -56
- llm_ide_rules/commands/mcp.py +1 -1
- llm_ide_rules/constants.py +1 -27
- llm_ide_rules/markdown_parser.py +108 -0
- llm_ide_rules/utils.py +118 -0
- {llm_ide_rules-0.6.0.dist-info → llm_ide_rules-0.8.0.dist-info}/METADATA +4 -3
- llm_ide_rules-0.8.0.dist-info/RECORD +27 -0
- llm_ide_rules/sections.json +0 -17
- llm_ide_rules-0.6.0.dist-info/RECORD +0 -23
- {llm_ide_rules-0.6.0.dist-info → llm_ide_rules-0.8.0.dist-info}/WHEEL +0 -0
- {llm_ide_rules-0.6.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
|
@@ -23,14 +23,14 @@ class BaseAgent(ABC):
|
|
|
23
23
|
|
|
24
24
|
@abstractmethod
|
|
25
25
|
def bundle_rules(
|
|
26
|
-
self, output_file: Path, section_globs: dict[str, str | None]
|
|
26
|
+
self, output_file: Path, section_globs: dict[str, str | None] | None = None
|
|
27
27
|
) -> bool:
|
|
28
28
|
"""Bundle rule files into a single output file."""
|
|
29
29
|
...
|
|
30
30
|
|
|
31
31
|
@abstractmethod
|
|
32
32
|
def bundle_commands(
|
|
33
|
-
self, output_file: Path, section_globs: dict[str, str | None]
|
|
33
|
+
self, output_file: Path, section_globs: dict[str, str | None] | None = None
|
|
34
34
|
) -> bool:
|
|
35
35
|
"""Bundle command files into a single output file."""
|
|
36
36
|
...
|
|
@@ -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,6 +58,49 @@ 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
|
+
|
|
69
|
+
def generate_root_doc(
|
|
70
|
+
self,
|
|
71
|
+
general_lines: list[str],
|
|
72
|
+
rules_sections: dict[str, list[str]],
|
|
73
|
+
command_sections: dict[str, list[str]],
|
|
74
|
+
output_dir: Path,
|
|
75
|
+
section_globs: dict[str, str | None] | None = None,
|
|
76
|
+
) -> None:
|
|
77
|
+
"""Generate a root documentation file (e.g. CLAUDE.md) if supported."""
|
|
78
|
+
pass
|
|
79
|
+
|
|
80
|
+
def build_root_doc_content(
|
|
81
|
+
self,
|
|
82
|
+
general_lines: list[str],
|
|
83
|
+
rules_sections: dict[str, list[str]],
|
|
84
|
+
) -> str:
|
|
85
|
+
"""Build the content string for a root documentation file by aggregating rules."""
|
|
86
|
+
content = []
|
|
87
|
+
|
|
88
|
+
# Add general instructions
|
|
89
|
+
if general_lines:
|
|
90
|
+
trimmed = trim_content(general_lines)
|
|
91
|
+
if trimmed:
|
|
92
|
+
content.extend(trimmed)
|
|
93
|
+
content.append("\n\n")
|
|
94
|
+
|
|
95
|
+
# Add sections in document order (dict maintains insertion order in Python 3.7+)
|
|
96
|
+
for section_name, lines in rules_sections.items():
|
|
97
|
+
trimmed = trim_content(lines)
|
|
98
|
+
if trimmed:
|
|
99
|
+
content.extend(trimmed)
|
|
100
|
+
content.append("\n\n")
|
|
101
|
+
|
|
102
|
+
return "".join(content).strip() + "\n" if content else ""
|
|
103
|
+
|
|
60
104
|
def get_rules_path(self, base_dir: Path) -> Path:
|
|
61
105
|
"""Get the full path to the rules directory."""
|
|
62
106
|
if not self.rules_dir:
|
|
@@ -155,9 +199,15 @@ def strip_toml_metadata(text: str) -> str:
|
|
|
155
199
|
|
|
156
200
|
|
|
157
201
|
def get_ordered_files(
|
|
158
|
-
file_list: list[Path], section_globs_keys: list[str]
|
|
202
|
+
file_list: list[Path], section_globs_keys: list[str] | None = None
|
|
159
203
|
) -> list[Path]:
|
|
160
|
-
"""Order files based on section_globs key order, with unmapped files at the end.
|
|
204
|
+
"""Order files based on section_globs key order, with unmapped files at the end.
|
|
205
|
+
|
|
206
|
+
If section_globs_keys is None, returns files sorted alphabetically.
|
|
207
|
+
"""
|
|
208
|
+
if not section_globs_keys:
|
|
209
|
+
return sorted(file_list, key=lambda p: p.name)
|
|
210
|
+
|
|
161
211
|
file_dict = {f.stem: f for f in file_list}
|
|
162
212
|
ordered_files = []
|
|
163
213
|
|
|
@@ -174,9 +224,15 @@ def get_ordered_files(
|
|
|
174
224
|
|
|
175
225
|
|
|
176
226
|
def get_ordered_files_github(
|
|
177
|
-
file_list: list[Path], section_globs_keys: list[str]
|
|
227
|
+
file_list: list[Path], section_globs_keys: list[str] | None = None
|
|
178
228
|
) -> list[Path]:
|
|
179
|
-
"""Order GitHub instruction files, handling .instructions suffix.
|
|
229
|
+
"""Order GitHub instruction files, handling .instructions suffix.
|
|
230
|
+
|
|
231
|
+
If section_globs_keys is None, returns files sorted alphabetically.
|
|
232
|
+
"""
|
|
233
|
+
if not section_globs_keys:
|
|
234
|
+
return sorted(file_list, key=lambda p: p.name)
|
|
235
|
+
|
|
180
236
|
file_dict = {}
|
|
181
237
|
for f in file_list:
|
|
182
238
|
base_stem = f.stem.replace(".instructions", "")
|
|
@@ -232,6 +288,7 @@ def write_rule_file(path: Path, header_yaml: str, content_lines: list[str]) -> N
|
|
|
232
288
|
"""Write a rule file with front matter and content."""
|
|
233
289
|
trimmed_content = trim_content(content_lines)
|
|
234
290
|
output = header_yaml.strip() + "\n" + "".join(trimmed_content)
|
|
291
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
235
292
|
path.write_text(output)
|
|
236
293
|
|
|
237
294
|
|
llm_ide_rules/agents/claude.py
CHANGED
|
@@ -23,13 +23,13 @@ class ClaudeAgent(BaseAgent):
|
|
|
23
23
|
mcp_project_path = ".mcp.json"
|
|
24
24
|
|
|
25
25
|
def bundle_rules(
|
|
26
|
-
self, output_file: Path, section_globs: dict[str, str | None]
|
|
26
|
+
self, output_file: Path, section_globs: dict[str, str | None] | None = None
|
|
27
27
|
) -> bool:
|
|
28
28
|
"""Claude Code doesn't support rules, only commands."""
|
|
29
29
|
return False
|
|
30
30
|
|
|
31
31
|
def bundle_commands(
|
|
32
|
-
self, output_file: Path, section_globs: dict[str, str | None]
|
|
32
|
+
self, output_file: Path, section_globs: dict[str, str | None] | None = None
|
|
33
33
|
) -> bool:
|
|
34
34
|
"""Bundle Claude Code command files (.md) into a single output file."""
|
|
35
35
|
commands_dir = self.commands_dir
|
|
@@ -48,7 +48,9 @@ class ClaudeAgent(BaseAgent):
|
|
|
48
48
|
if not command_files:
|
|
49
49
|
return False
|
|
50
50
|
|
|
51
|
-
ordered_commands = get_ordered_files(
|
|
51
|
+
ordered_commands = get_ordered_files(
|
|
52
|
+
command_files, list(section_globs.keys()) if section_globs else None
|
|
53
|
+
)
|
|
52
54
|
|
|
53
55
|
content_parts: list[str] = []
|
|
54
56
|
for command_file in ordered_commands:
|
|
@@ -56,7 +58,9 @@ class ClaudeAgent(BaseAgent):
|
|
|
56
58
|
if not content:
|
|
57
59
|
continue
|
|
58
60
|
|
|
59
|
-
header = resolve_header_from_stem(
|
|
61
|
+
header = resolve_header_from_stem(
|
|
62
|
+
command_file.stem, section_globs if section_globs else {}
|
|
63
|
+
)
|
|
60
64
|
content_parts.append(f"## {header}\n\n")
|
|
61
65
|
content_parts.append(content)
|
|
62
66
|
content_parts.append("\n\n")
|
|
@@ -73,6 +77,7 @@ class ClaudeAgent(BaseAgent):
|
|
|
73
77
|
filename: str,
|
|
74
78
|
rules_dir: Path,
|
|
75
79
|
glob_pattern: str | None = None,
|
|
80
|
+
description: str | None = None,
|
|
76
81
|
) -> None:
|
|
77
82
|
"""Claude Code doesn't support rules."""
|
|
78
83
|
pass
|
|
@@ -89,4 +94,26 @@ class ClaudeAgent(BaseAgent):
|
|
|
89
94
|
filepath = commands_dir / f"{filename}{extension}"
|
|
90
95
|
|
|
91
96
|
trimmed = trim_content(content_lines)
|
|
97
|
+
filepath.parent.mkdir(parents=True, exist_ok=True)
|
|
92
98
|
filepath.write_text("".join(trimmed))
|
|
99
|
+
|
|
100
|
+
def generate_root_doc(
|
|
101
|
+
self,
|
|
102
|
+
general_lines: list[str],
|
|
103
|
+
rules_sections: dict[str, list[str]],
|
|
104
|
+
command_sections: dict[str, list[str]],
|
|
105
|
+
output_dir: Path,
|
|
106
|
+
section_globs: dict[str, str | None] | None = None,
|
|
107
|
+
) -> None:
|
|
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
|
@@ -27,7 +27,7 @@ class CursorAgent(BaseAgent):
|
|
|
27
27
|
mcp_project_path = ".cursor/mcp.json"
|
|
28
28
|
|
|
29
29
|
def bundle_rules(
|
|
30
|
-
self, output_file: Path, section_globs: dict[str, str | None]
|
|
30
|
+
self, output_file: Path, section_globs: dict[str, str | None] | None = None
|
|
31
31
|
) -> bool:
|
|
32
32
|
"""Bundle Cursor rule files (.mdc) into a single output file."""
|
|
33
33
|
rules_dir = self.rules_dir
|
|
@@ -45,21 +45,42 @@ class CursorAgent(BaseAgent):
|
|
|
45
45
|
general = [f for f in rule_files if f.stem == "general"]
|
|
46
46
|
others = [f for f in rule_files if f.stem != "general"]
|
|
47
47
|
|
|
48
|
-
ordered_others = get_ordered_files(
|
|
48
|
+
ordered_others = get_ordered_files(
|
|
49
|
+
others, list(section_globs.keys()) if section_globs else None
|
|
50
|
+
)
|
|
49
51
|
ordered = general + ordered_others
|
|
50
52
|
|
|
51
53
|
content_parts: list[str] = []
|
|
52
54
|
for rule_file in ordered:
|
|
53
|
-
|
|
54
|
-
if not
|
|
55
|
+
file_content = rule_file.read_text().strip()
|
|
56
|
+
if not file_content:
|
|
55
57
|
continue
|
|
56
58
|
|
|
57
|
-
|
|
59
|
+
lines = file_content.splitlines()
|
|
60
|
+
extracted_header = None
|
|
61
|
+
glob_pattern = None
|
|
62
|
+
|
|
63
|
+
for line in lines:
|
|
64
|
+
if line.startswith("## "):
|
|
65
|
+
extracted_header = line[3:].strip()
|
|
66
|
+
break
|
|
67
|
+
|
|
68
|
+
glob_pattern = self._extract_glob_from_frontmatter(file_content)
|
|
69
|
+
|
|
70
|
+
content = strip_yaml_frontmatter(file_content)
|
|
58
71
|
content = strip_header(content)
|
|
59
|
-
|
|
72
|
+
|
|
73
|
+
if extracted_header:
|
|
74
|
+
header = extracted_header
|
|
75
|
+
else:
|
|
76
|
+
header = resolve_header_from_stem(
|
|
77
|
+
rule_file.stem, section_globs if section_globs else {}
|
|
78
|
+
)
|
|
60
79
|
|
|
61
80
|
if rule_file.stem != "general":
|
|
62
81
|
content_parts.append(f"## {header}\n\n")
|
|
82
|
+
if glob_pattern:
|
|
83
|
+
content_parts.append(f"globs: {glob_pattern}\n\n")
|
|
63
84
|
|
|
64
85
|
content_parts.append(content)
|
|
65
86
|
content_parts.append("\n\n")
|
|
@@ -70,8 +91,23 @@ class CursorAgent(BaseAgent):
|
|
|
70
91
|
output_file.write_text("".join(content_parts))
|
|
71
92
|
return True
|
|
72
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
|
+
|
|
73
109
|
def bundle_commands(
|
|
74
|
-
self, output_file: Path, section_globs: dict[str, str | None]
|
|
110
|
+
self, output_file: Path, section_globs: dict[str, str | None] | None = None
|
|
75
111
|
) -> bool:
|
|
76
112
|
"""Bundle Cursor command files (.md) into a single output file."""
|
|
77
113
|
commands_dir = self.commands_dir
|
|
@@ -90,7 +126,9 @@ class CursorAgent(BaseAgent):
|
|
|
90
126
|
if not command_files:
|
|
91
127
|
return False
|
|
92
128
|
|
|
93
|
-
ordered_commands = get_ordered_files(
|
|
129
|
+
ordered_commands = get_ordered_files(
|
|
130
|
+
command_files, list(section_globs.keys()) if section_globs else None
|
|
131
|
+
)
|
|
94
132
|
|
|
95
133
|
content_parts: list[str] = []
|
|
96
134
|
for command_file in ordered_commands:
|
|
@@ -98,7 +136,9 @@ class CursorAgent(BaseAgent):
|
|
|
98
136
|
if not content:
|
|
99
137
|
continue
|
|
100
138
|
|
|
101
|
-
header = resolve_header_from_stem(
|
|
139
|
+
header = resolve_header_from_stem(
|
|
140
|
+
command_file.stem, section_globs if section_globs else {}
|
|
141
|
+
)
|
|
102
142
|
content_parts.append(f"## {header}\n\n")
|
|
103
143
|
content_parts.append(content)
|
|
104
144
|
content_parts.append("\n\n")
|
|
@@ -115,27 +155,32 @@ class CursorAgent(BaseAgent):
|
|
|
115
155
|
filename: str,
|
|
116
156
|
rules_dir: Path,
|
|
117
157
|
glob_pattern: str | None = None,
|
|
158
|
+
description: str | None = None,
|
|
118
159
|
) -> None:
|
|
119
160
|
"""Write a Cursor rule file (.mdc) with YAML frontmatter."""
|
|
120
161
|
extension = self.rule_extension or ".mdc"
|
|
121
162
|
filepath = rules_dir / f"{filename}{extension}"
|
|
122
163
|
|
|
164
|
+
desc = description or filename.replace("-", " ").title()
|
|
165
|
+
|
|
123
166
|
if glob_pattern and glob_pattern != "manual":
|
|
124
167
|
header_yaml = f"""---
|
|
125
|
-
description:
|
|
168
|
+
description: {desc}
|
|
126
169
|
globs: {glob_pattern}
|
|
127
170
|
alwaysApply: false
|
|
128
171
|
---
|
|
129
172
|
"""
|
|
130
173
|
elif glob_pattern == "manual":
|
|
131
|
-
header_yaml = """---
|
|
132
|
-
description:
|
|
174
|
+
header_yaml = f"""---
|
|
175
|
+
description: {desc}
|
|
176
|
+
globs:
|
|
133
177
|
alwaysApply: false
|
|
134
178
|
---
|
|
135
179
|
"""
|
|
136
180
|
else:
|
|
137
|
-
header_yaml = """---
|
|
138
|
-
description:
|
|
181
|
+
header_yaml = f"""---
|
|
182
|
+
description: {desc}
|
|
183
|
+
globs:
|
|
139
184
|
alwaysApply: true
|
|
140
185
|
---
|
|
141
186
|
"""
|
|
@@ -153,6 +198,7 @@ alwaysApply: true
|
|
|
153
198
|
filepath = commands_dir / f"{filename}{extension}"
|
|
154
199
|
|
|
155
200
|
trimmed = trim_content(content_lines)
|
|
201
|
+
filepath.parent.mkdir(parents=True, exist_ok=True)
|
|
156
202
|
filepath.write_text("".join(trimmed))
|
|
157
203
|
|
|
158
204
|
def write_prompt(
|
|
@@ -176,3 +222,7 @@ alwaysApply: true
|
|
|
176
222
|
|
|
177
223
|
output_parts.extend(filtered_content)
|
|
178
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
|
@@ -27,13 +27,13 @@ class GeminiAgent(BaseAgent):
|
|
|
27
27
|
mcp_project_path = ".gemini/settings.json"
|
|
28
28
|
|
|
29
29
|
def bundle_rules(
|
|
30
|
-
self, output_file: Path, section_globs: dict[str, str | None]
|
|
30
|
+
self, output_file: Path, section_globs: dict[str, str | None] | None = None
|
|
31
31
|
) -> bool:
|
|
32
32
|
"""Gemini CLI doesn't support rules, only commands."""
|
|
33
33
|
return False
|
|
34
34
|
|
|
35
35
|
def bundle_commands(
|
|
36
|
-
self, output_file: Path, section_globs: dict[str, str | None]
|
|
36
|
+
self, output_file: Path, section_globs: dict[str, str | None] | None = None
|
|
37
37
|
) -> bool:
|
|
38
38
|
"""Bundle Gemini CLI command files (.toml) into a single output file."""
|
|
39
39
|
commands_dir = self.commands_dir
|
|
@@ -52,7 +52,9 @@ class GeminiAgent(BaseAgent):
|
|
|
52
52
|
if not command_files:
|
|
53
53
|
return False
|
|
54
54
|
|
|
55
|
-
ordered_commands = get_ordered_files(
|
|
55
|
+
ordered_commands = get_ordered_files(
|
|
56
|
+
command_files, list(section_globs.keys()) if section_globs else None
|
|
57
|
+
)
|
|
56
58
|
|
|
57
59
|
content_parts: list[str] = []
|
|
58
60
|
for command_file in ordered_commands:
|
|
@@ -61,7 +63,9 @@ class GeminiAgent(BaseAgent):
|
|
|
61
63
|
continue
|
|
62
64
|
|
|
63
65
|
content = strip_toml_metadata(content)
|
|
64
|
-
header = resolve_header_from_stem(
|
|
66
|
+
header = resolve_header_from_stem(
|
|
67
|
+
command_file.stem, section_globs if section_globs else {}
|
|
68
|
+
)
|
|
65
69
|
content_parts.append(f"## {header}\n\n")
|
|
66
70
|
content_parts.append(content)
|
|
67
71
|
content_parts.append("\n\n")
|
|
@@ -78,6 +82,7 @@ class GeminiAgent(BaseAgent):
|
|
|
78
82
|
filename: str,
|
|
79
83
|
rules_dir: Path,
|
|
80
84
|
glob_pattern: str | None = None,
|
|
85
|
+
description: str | None = None,
|
|
81
86
|
) -> None:
|
|
82
87
|
"""Gemini CLI doesn't support rules."""
|
|
83
88
|
pass
|
|
@@ -120,6 +125,7 @@ class GeminiAgent(BaseAgent):
|
|
|
120
125
|
|
|
121
126
|
# tomli-w will handle escaping and multiline strings automatically
|
|
122
127
|
output = tomli_w.dumps(data)
|
|
128
|
+
filepath.parent.mkdir(parents=True, exist_ok=True)
|
|
123
129
|
filepath.write_text(output)
|
|
124
130
|
|
|
125
131
|
def transform_mcp_server(self, server: McpServer) -> dict:
|
|
@@ -159,3 +165,37 @@ class GeminiAgent(BaseAgent):
|
|
|
159
165
|
|
|
160
166
|
existing[self.mcp_root_key] = servers
|
|
161
167
|
path.write_text(json.dumps(existing, indent=2))
|
|
168
|
+
|
|
169
|
+
def generate_root_doc(
|
|
170
|
+
self,
|
|
171
|
+
general_lines: list[str],
|
|
172
|
+
rules_sections: dict[str, list[str]],
|
|
173
|
+
command_sections: dict[str, list[str]],
|
|
174
|
+
output_dir: Path,
|
|
175
|
+
section_globs: dict[str, str | None] | None = None,
|
|
176
|
+
) -> None:
|
|
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
|