llm-ide-rules 0.5.0__py3-none-any.whl → 0.6.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 +53 -9
- llm_ide_rules/__main__.py +1 -1
- llm_ide_rules/agents/__init__.py +28 -0
- llm_ide_rules/agents/base.py +283 -0
- llm_ide_rules/agents/claude.py +92 -0
- llm_ide_rules/agents/cursor.py +178 -0
- llm_ide_rules/agents/gemini.py +161 -0
- llm_ide_rules/agents/github.py +207 -0
- llm_ide_rules/agents/opencode.py +126 -0
- llm_ide_rules/commands/delete.py +24 -34
- llm_ide_rules/commands/download.py +52 -56
- llm_ide_rules/commands/explode.py +225 -333
- llm_ide_rules/commands/implode.py +209 -364
- llm_ide_rules/commands/mcp.py +119 -0
- llm_ide_rules/constants.py +17 -14
- llm_ide_rules/log.py +9 -0
- llm_ide_rules/mcp/__init__.py +7 -0
- llm_ide_rules/mcp/models.py +21 -0
- llm_ide_rules/sections.json +4 -14
- {llm_ide_rules-0.5.0.dist-info → llm_ide_rules-0.6.0.dist-info}/METADATA +35 -59
- llm_ide_rules-0.6.0.dist-info/RECORD +23 -0
- {llm_ide_rules-0.5.0.dist-info → llm_ide_rules-0.6.0.dist-info}/WHEEL +2 -2
- llm_ide_rules-0.5.0.dist-info/RECORD +0 -12
- {llm_ide_rules-0.5.0.dist-info → llm_ide_rules-0.6.0.dist-info}/entry_points.txt +0 -0
|
@@ -1,52 +1,34 @@
|
|
|
1
1
|
"""Explode command: Convert instruction file to separate rule files."""
|
|
2
2
|
|
|
3
|
-
import os
|
|
4
|
-
import sys
|
|
5
3
|
from pathlib import Path
|
|
6
4
|
from typing_extensions import Annotated
|
|
7
5
|
|
|
8
6
|
import typer
|
|
9
|
-
import structlog
|
|
10
|
-
import logging
|
|
11
7
|
|
|
12
|
-
from llm_ide_rules.
|
|
8
|
+
from llm_ide_rules.agents import get_agent
|
|
9
|
+
from llm_ide_rules.agents.base import (
|
|
10
|
+
BaseAgent,
|
|
11
|
+
replace_header_with_proper_casing,
|
|
12
|
+
write_rule_file,
|
|
13
|
+
)
|
|
14
|
+
from llm_ide_rules.log import log
|
|
15
|
+
from llm_ide_rules.constants import load_section_globs, header_to_filename, VALID_AGENTS
|
|
13
16
|
|
|
14
|
-
logger = structlog.get_logger()
|
|
15
17
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
"""Generate Cursor rule frontmatter for a given glob pattern."""
|
|
19
|
-
return f"""---
|
|
20
|
-
description:
|
|
21
|
-
globs: {glob}
|
|
22
|
-
alwaysApply: false
|
|
23
|
-
---
|
|
24
|
-
"""
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
def generate_copilot_frontmatter(glob):
|
|
28
|
-
"""Generate Copilot instruction frontmatter for a given glob pattern."""
|
|
29
|
-
return f"""---
|
|
30
|
-
applyTo: "{glob}"
|
|
31
|
-
---
|
|
32
|
-
"""
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
def extract_general(lines):
|
|
36
|
-
"""
|
|
37
|
-
Extract lines before the first section header '## '.
|
|
38
|
-
"""
|
|
18
|
+
def extract_general(lines: list[str]) -> list[str]:
|
|
19
|
+
"""Extract lines before the first section header '## '."""
|
|
39
20
|
general = []
|
|
40
21
|
for line in lines:
|
|
41
22
|
if line.startswith("## "):
|
|
42
23
|
break
|
|
43
24
|
general.append(line)
|
|
25
|
+
|
|
44
26
|
return general
|
|
45
27
|
|
|
46
28
|
|
|
47
|
-
def extract_section(lines, header):
|
|
48
|
-
"""
|
|
49
|
-
|
|
29
|
+
def extract_section(lines: list[str], header: str) -> list[str]:
|
|
30
|
+
"""Extract lines under a given section header until the next header or EOF.
|
|
31
|
+
|
|
50
32
|
Includes the header itself in the output.
|
|
51
33
|
"""
|
|
52
34
|
content = []
|
|
@@ -58,287 +40,157 @@ def extract_section(lines, header):
|
|
|
58
40
|
content.append(line)
|
|
59
41
|
elif line.strip().lower() == header.lower():
|
|
60
42
|
in_section = True
|
|
61
|
-
content.append(line)
|
|
62
|
-
return content
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
def write_rule(path, header_yaml, content_lines):
|
|
66
|
-
"""
|
|
67
|
-
Write a rule file with front matter and content.
|
|
68
|
-
"""
|
|
69
|
-
trimmed_content = trim_content(content_lines)
|
|
70
|
-
with open(path, "w") as f:
|
|
71
|
-
f.write(header_yaml.strip() + "\n")
|
|
72
|
-
for line in trimmed_content:
|
|
73
|
-
f.write(line)
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
def trim_content(content_lines):
|
|
77
|
-
"""Remove leading and trailing empty lines from content."""
|
|
78
|
-
# Find first non-empty line
|
|
79
|
-
start = 0
|
|
80
|
-
for i, line in enumerate(content_lines):
|
|
81
|
-
if line.strip():
|
|
82
|
-
start = i
|
|
83
|
-
break
|
|
84
|
-
else:
|
|
85
|
-
# All lines are empty
|
|
86
|
-
return []
|
|
87
|
-
|
|
88
|
-
# Find last non-empty line
|
|
89
|
-
end = len(content_lines)
|
|
90
|
-
for i in range(len(content_lines) - 1, -1, -1):
|
|
91
|
-
if content_lines[i].strip():
|
|
92
|
-
end = i + 1
|
|
93
|
-
break
|
|
43
|
+
content.append(line)
|
|
94
44
|
|
|
95
|
-
return
|
|
45
|
+
return content
|
|
96
46
|
|
|
97
47
|
|
|
98
|
-
def
|
|
99
|
-
"""
|
|
100
|
-
|
|
101
|
-
|
|
48
|
+
def extract_all_sections(lines: list[str]) -> dict[str, list[str]]:
|
|
49
|
+
"""Extract all sections from lines, returning dict of section_name -> content_lines."""
|
|
50
|
+
sections: dict[str, list[str]] = {}
|
|
51
|
+
current_section: str | None = None
|
|
52
|
+
current_content: list[str] = []
|
|
102
53
|
|
|
103
|
-
|
|
104
|
-
for i, line in enumerate(content_lines):
|
|
54
|
+
for line in lines:
|
|
105
55
|
if line.startswith("## "):
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
return content_lines
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
def extract_description_and_filter_content(content_lines, default_description):
|
|
113
|
-
"""Extract description from first non-empty line that starts with 'Description:' and return filtered content."""
|
|
114
|
-
trimmed_content = trim_content(content_lines)
|
|
115
|
-
description = ""
|
|
116
|
-
description_line = None
|
|
117
|
-
|
|
118
|
-
# Find the first non-empty, non-header line that starts with "Description:"
|
|
119
|
-
for i, line in enumerate(trimmed_content):
|
|
120
|
-
stripped_line = line.strip()
|
|
121
|
-
if (
|
|
122
|
-
stripped_line
|
|
123
|
-
and not stripped_line.startswith("#")
|
|
124
|
-
and not stripped_line.startswith("##")
|
|
125
|
-
):
|
|
126
|
-
if stripped_line.startswith("Description:"):
|
|
127
|
-
# Extract the description text after "Description:"
|
|
128
|
-
description = stripped_line[len("Description:") :].strip()
|
|
129
|
-
description_line = i
|
|
130
|
-
break
|
|
131
|
-
else:
|
|
132
|
-
# Found a non-header line that doesn't start with Description:, stop looking
|
|
133
|
-
break
|
|
134
|
-
|
|
135
|
-
# Only use explicit descriptions - no fallback extraction
|
|
136
|
-
if description and description_line is not None:
|
|
137
|
-
# Remove the description line from content
|
|
138
|
-
filtered_content = (
|
|
139
|
-
trimmed_content[:description_line] + trimmed_content[description_line + 1 :]
|
|
140
|
-
)
|
|
141
|
-
# Trim again after removing description line
|
|
142
|
-
filtered_content = trim_content(filtered_content)
|
|
143
|
-
else:
|
|
144
|
-
# No description found, keep all content
|
|
145
|
-
filtered_content = trimmed_content
|
|
146
|
-
|
|
147
|
-
return description, filtered_content
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
def write_cursor_prompt(content_lines, filename, prompts_dir, section_name=None):
|
|
151
|
-
"""Write a Cursor prompt file with frontmatter including description."""
|
|
152
|
-
filepath = os.path.join(prompts_dir, filename + ".mdc")
|
|
153
|
-
|
|
154
|
-
# Don't generate a default description, leave empty if none found
|
|
155
|
-
default_description = ""
|
|
156
|
-
description, filtered_content = extract_description_and_filter_content(
|
|
157
|
-
content_lines, default_description
|
|
158
|
-
)
|
|
56
|
+
if current_section:
|
|
57
|
+
sections[current_section] = current_content
|
|
159
58
|
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
description: {description}
|
|
165
|
-
---
|
|
166
|
-
"""
|
|
167
|
-
f.write(frontmatter)
|
|
168
|
-
|
|
169
|
-
for line in filtered_content:
|
|
170
|
-
f.write(line)
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
def write_github_prompt(content_lines, filename, prompts_dir, section_name=None):
|
|
174
|
-
"""Write a GitHub prompt file with proper frontmatter."""
|
|
175
|
-
filepath = os.path.join(prompts_dir, filename + ".prompt.md")
|
|
176
|
-
|
|
177
|
-
# Don't generate a default description, leave empty if none found
|
|
178
|
-
default_description = ""
|
|
179
|
-
description, filtered_content = extract_description_and_filter_content(
|
|
180
|
-
content_lines, default_description
|
|
181
|
-
)
|
|
182
|
-
|
|
183
|
-
frontmatter = f"""---
|
|
184
|
-
mode: 'agent'
|
|
185
|
-
description: '{description}'
|
|
186
|
-
---
|
|
187
|
-
"""
|
|
59
|
+
current_section = line.strip()[3:]
|
|
60
|
+
current_content = [line]
|
|
61
|
+
elif current_section:
|
|
62
|
+
current_content.append(line)
|
|
188
63
|
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
for line in filtered_content:
|
|
192
|
-
f.write(line)
|
|
64
|
+
if current_section:
|
|
65
|
+
sections[current_section] = current_content
|
|
193
66
|
|
|
67
|
+
return sections
|
|
194
68
|
|
|
195
|
-
def write_cursor_command(content_lines, filename, commands_dir, section_name=None):
|
|
196
|
-
"""Write a Cursor command file (plain markdown, no frontmatter)."""
|
|
197
|
-
filepath = os.path.join(commands_dir, filename + ".md")
|
|
198
69
|
|
|
199
|
-
|
|
70
|
+
def process_command_section(
|
|
71
|
+
section_name: str,
|
|
72
|
+
section_content: list[str],
|
|
73
|
+
agents: list[BaseAgent],
|
|
74
|
+
dirs: dict[str, Path],
|
|
75
|
+
) -> bool:
|
|
76
|
+
"""Process a section as a command for all agents."""
|
|
77
|
+
if not any(line.strip() for line in section_content):
|
|
78
|
+
return False
|
|
200
79
|
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
found_header = False
|
|
204
|
-
for line in trimmed:
|
|
205
|
-
if not found_header and line.startswith("## "):
|
|
206
|
-
found_header = True
|
|
207
|
-
continue
|
|
208
|
-
filtered_content.append(line)
|
|
80
|
+
filename = header_to_filename(section_name)
|
|
81
|
+
section_content = replace_header_with_proper_casing(section_content, section_name)
|
|
209
82
|
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
f.write(line)
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
def write_claude_command(content_lines, filename, commands_dir, section_name=None):
|
|
219
|
-
"""Write a Claude Code command file (plain markdown, no frontmatter)."""
|
|
220
|
-
filepath = os.path.join(commands_dir, filename + ".md")
|
|
221
|
-
|
|
222
|
-
trimmed = trim_content(content_lines)
|
|
223
|
-
|
|
224
|
-
# Strip the header from content (first line starting with ##)
|
|
225
|
-
filtered_content = []
|
|
226
|
-
found_header = False
|
|
227
|
-
for line in trimmed:
|
|
228
|
-
if not found_header and line.startswith("## "):
|
|
229
|
-
found_header = True
|
|
230
|
-
continue
|
|
231
|
-
filtered_content.append(line)
|
|
83
|
+
for agent in agents:
|
|
84
|
+
if agent.commands_dir:
|
|
85
|
+
agent.write_command(
|
|
86
|
+
section_content, filename, dirs[agent.name], section_name
|
|
87
|
+
)
|
|
232
88
|
|
|
233
|
-
|
|
234
|
-
filtered_content = trim_content(filtered_content)
|
|
89
|
+
return True
|
|
235
90
|
|
|
236
|
-
with open(filepath, "w") as f:
|
|
237
|
-
for line in filtered_content:
|
|
238
|
-
f.write(line)
|
|
239
91
|
|
|
92
|
+
def process_unmapped_as_always_apply(
|
|
93
|
+
section_name: str,
|
|
94
|
+
section_content: list[str],
|
|
95
|
+
cursor_agent,
|
|
96
|
+
github_agent,
|
|
97
|
+
cursor_rules_dir: Path,
|
|
98
|
+
copilot_dir: Path,
|
|
99
|
+
) -> bool:
|
|
100
|
+
"""Process an unmapped section as an always-apply rule."""
|
|
101
|
+
if not any(line.strip() for line in section_content):
|
|
102
|
+
return False
|
|
240
103
|
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
filepath = os.path.join(commands_dir, filename + ".toml")
|
|
104
|
+
filename = header_to_filename(section_name)
|
|
105
|
+
section_content = replace_header_with_proper_casing(section_content, section_name)
|
|
244
106
|
|
|
245
|
-
|
|
246
|
-
|
|
107
|
+
cursor_agent.write_rule(
|
|
108
|
+
section_content, filename, cursor_rules_dir, glob_pattern=None
|
|
247
109
|
)
|
|
110
|
+
github_agent.write_rule(section_content, filename, copilot_dir, glob_pattern=None)
|
|
248
111
|
|
|
249
|
-
|
|
250
|
-
final_content = []
|
|
251
|
-
found_header = False
|
|
252
|
-
for line in filtered_content:
|
|
253
|
-
if not found_header and line.startswith("## "):
|
|
254
|
-
found_header = True
|
|
255
|
-
continue
|
|
256
|
-
final_content.append(line)
|
|
257
|
-
|
|
258
|
-
# Trim and convert to string
|
|
259
|
-
final_content = trim_content(final_content)
|
|
260
|
-
content_str = "".join(final_content).strip()
|
|
261
|
-
|
|
262
|
-
with open(filepath, "w") as f:
|
|
263
|
-
f.write(f'name = "{filename}"\n')
|
|
264
|
-
if description:
|
|
265
|
-
f.write(f'description = "{description}"\n')
|
|
266
|
-
else:
|
|
267
|
-
f.write(f'description = "{section_name or filename}"\n')
|
|
268
|
-
f.write('\n[command]\n')
|
|
269
|
-
f.write('shell = """\n')
|
|
270
|
-
f.write(content_str)
|
|
271
|
-
f.write('\n"""\n')
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
def process_unmapped_section(lines, section_name, cursor_commands_dir, github_prompts_dir, claude_commands_dir, gemini_commands_dir):
|
|
275
|
-
"""Process an unmapped section as a manually applied rule (command)."""
|
|
276
|
-
section_content = extract_section(lines, f"## {section_name}")
|
|
277
|
-
if any(line.strip() for line in section_content):
|
|
278
|
-
filename = header_to_filename(section_name)
|
|
279
|
-
|
|
280
|
-
# Replace header with proper casing
|
|
281
|
-
section_content = replace_header_with_proper_casing(
|
|
282
|
-
section_content, section_name
|
|
283
|
-
)
|
|
284
|
-
|
|
285
|
-
# Create command files (same as None case in SECTION_GLOBS)
|
|
286
|
-
write_cursor_command(section_content, filename, cursor_commands_dir, section_name)
|
|
287
|
-
write_github_prompt(section_content, filename, github_prompts_dir, section_name)
|
|
288
|
-
write_claude_command(section_content, filename, claude_commands_dir, section_name)
|
|
289
|
-
write_gemini_command(section_content, filename, gemini_commands_dir, section_name)
|
|
290
|
-
return True
|
|
291
|
-
return False
|
|
112
|
+
return True
|
|
292
113
|
|
|
293
114
|
|
|
294
115
|
def explode_main(
|
|
295
116
|
input_file: Annotated[
|
|
296
117
|
str, typer.Argument(help="Input markdown file")
|
|
297
118
|
] = "instructions.md",
|
|
298
|
-
verbose: Annotated[
|
|
299
|
-
bool, typer.Option("--verbose", "-v", help="Enable verbose logging")
|
|
300
|
-
] = False,
|
|
301
119
|
config: Annotated[
|
|
302
|
-
str
|
|
120
|
+
str | None,
|
|
121
|
+
typer.Option("--config", "-c", help="Custom configuration file path"),
|
|
303
122
|
] = None,
|
|
304
|
-
|
|
123
|
+
agent: Annotated[
|
|
124
|
+
str,
|
|
125
|
+
typer.Option(
|
|
126
|
+
"--agent",
|
|
127
|
+
"-a",
|
|
128
|
+
help="Agent to explode for (cursor, github, claude, gemini, or all)",
|
|
129
|
+
),
|
|
130
|
+
] = "all",
|
|
131
|
+
) -> None:
|
|
305
132
|
"""Convert instruction file to separate rule files."""
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
133
|
+
|
|
134
|
+
if agent not in VALID_AGENTS:
|
|
135
|
+
log.error("invalid agent", agent=agent, valid_agents=VALID_AGENTS)
|
|
136
|
+
error_msg = (
|
|
137
|
+
f"Invalid agent '{agent}'. Must be one of: {', '.join(VALID_AGENTS)}"
|
|
310
138
|
)
|
|
139
|
+
typer.echo(typer.style(error_msg, fg=typer.colors.RED), err=True)
|
|
140
|
+
raise typer.Exit(1)
|
|
311
141
|
|
|
312
|
-
|
|
313
|
-
SECTION_GLOBS = load_section_globs(config)
|
|
142
|
+
section_globs = load_section_globs(config)
|
|
314
143
|
|
|
315
|
-
|
|
144
|
+
log.info(
|
|
145
|
+
"starting explode operation", input_file=input_file, agent=agent, config=config
|
|
146
|
+
)
|
|
316
147
|
|
|
317
|
-
|
|
318
|
-
rules_dir = os.path.join(os.getcwd(), ".cursor", "rules")
|
|
319
|
-
cursor_commands_dir = os.path.join(os.getcwd(), ".cursor", "commands")
|
|
320
|
-
copilot_dir = os.path.join(os.getcwd(), ".github", "instructions")
|
|
321
|
-
github_prompts_dir = os.path.join(os.getcwd(), ".github", "prompts")
|
|
322
|
-
claude_commands_dir = os.path.join(os.getcwd(), ".claude", "commands")
|
|
323
|
-
gemini_commands_dir = os.path.join(os.getcwd(), ".gemini", "commands")
|
|
148
|
+
cwd = Path.cwd()
|
|
324
149
|
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
150
|
+
# Initialize only the agents we need
|
|
151
|
+
agents_to_process = []
|
|
152
|
+
if agent == "all":
|
|
153
|
+
agents_to_process = ["cursor", "github", "claude", "gemini", "opencode"]
|
|
154
|
+
else:
|
|
155
|
+
agents_to_process = [agent]
|
|
156
|
+
|
|
157
|
+
# Initialize agents and create directories
|
|
158
|
+
agent_instances = {}
|
|
159
|
+
agent_dirs = {}
|
|
160
|
+
|
|
161
|
+
for agent_name in agents_to_process:
|
|
162
|
+
agent_instances[agent_name] = get_agent(agent_name)
|
|
163
|
+
|
|
164
|
+
if agent_name in ["cursor", "github"]:
|
|
165
|
+
# These agents have both rules and commands
|
|
166
|
+
rules_dir = cwd / agent_instances[agent_name].rules_dir
|
|
167
|
+
commands_dir = cwd / agent_instances[agent_name].commands_dir
|
|
168
|
+
rules_dir.mkdir(parents=True, exist_ok=True)
|
|
169
|
+
commands_dir.mkdir(parents=True, exist_ok=True)
|
|
170
|
+
agent_dirs[agent_name] = {"rules": rules_dir, "commands": commands_dir}
|
|
171
|
+
else:
|
|
172
|
+
# claude, gemini, and opencode only have commands
|
|
173
|
+
commands_dir = cwd / agent_instances[agent_name].commands_dir
|
|
174
|
+
commands_dir.mkdir(parents=True, exist_ok=True)
|
|
175
|
+
agent_dirs[agent_name] = {"commands": commands_dir}
|
|
331
176
|
|
|
332
|
-
input_path =
|
|
177
|
+
input_path = cwd / input_file
|
|
333
178
|
|
|
334
179
|
try:
|
|
335
|
-
|
|
336
|
-
lines = f.readlines()
|
|
180
|
+
lines = input_path.read_text().splitlines(keepends=True)
|
|
337
181
|
except FileNotFoundError:
|
|
338
|
-
|
|
182
|
+
log.error("input file not found", input_file=str(input_path))
|
|
183
|
+
error_msg = f"Input file not found: {input_path}"
|
|
184
|
+
typer.echo(typer.style(error_msg, fg=typer.colors.RED), err=True)
|
|
339
185
|
raise typer.Exit(1)
|
|
340
186
|
|
|
341
|
-
|
|
187
|
+
commands_path = input_path.parent / "commands.md"
|
|
188
|
+
commands_lines = []
|
|
189
|
+
if commands_path.exists():
|
|
190
|
+
commands_lines = commands_path.read_text().splitlines(keepends=True)
|
|
191
|
+
log.info("found commands file", commands_file=str(commands_path))
|
|
192
|
+
|
|
193
|
+
# Process general instructions for agents that support rules
|
|
342
194
|
general = extract_general(lines)
|
|
343
195
|
if any(line.strip() for line in general):
|
|
344
196
|
general_header = """
|
|
@@ -347,82 +199,122 @@ description:
|
|
|
347
199
|
alwaysApply: true
|
|
348
200
|
---
|
|
349
201
|
"""
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
202
|
+
if "cursor" in agent_instances:
|
|
203
|
+
write_rule_file(
|
|
204
|
+
agent_dirs["cursor"]["rules"] / "general.mdc", general_header, general
|
|
205
|
+
)
|
|
206
|
+
if "github" in agent_instances:
|
|
207
|
+
agent_instances["github"].write_general_instructions(general, cwd)
|
|
355
208
|
|
|
356
|
-
# Process
|
|
209
|
+
# Process mapped sections for agents that support rules
|
|
357
210
|
found_sections = set()
|
|
358
|
-
for section_name,
|
|
211
|
+
for section_name, glob_pattern in section_globs.items():
|
|
359
212
|
section_content = extract_section(lines, f"## {section_name}")
|
|
360
213
|
if any(line.strip() for line in section_content):
|
|
361
214
|
found_sections.add(section_name)
|
|
362
215
|
filename = header_to_filename(section_name)
|
|
363
216
|
|
|
364
|
-
# Replace header with proper casing from SECTION_GLOBS
|
|
365
217
|
section_content = replace_header_with_proper_casing(
|
|
366
218
|
section_content, section_name
|
|
367
219
|
)
|
|
368
220
|
|
|
369
|
-
if
|
|
370
|
-
|
|
371
|
-
cursor_header = generate_cursor_frontmatter(glob_or_description)
|
|
372
|
-
write_rule(
|
|
373
|
-
os.path.join(rules_dir, filename + ".mdc"),
|
|
374
|
-
cursor_header,
|
|
221
|
+
if "cursor" in agent_instances:
|
|
222
|
+
agent_instances["cursor"].write_rule(
|
|
375
223
|
section_content,
|
|
224
|
+
filename,
|
|
225
|
+
agent_dirs["cursor"]["rules"],
|
|
226
|
+
glob_pattern,
|
|
376
227
|
)
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
write_rule(
|
|
380
|
-
os.path.join(copilot_dir, filename + ".instructions.md"),
|
|
381
|
-
copilot_header,
|
|
228
|
+
if "github" in agent_instances:
|
|
229
|
+
agent_instances["github"].write_rule(
|
|
382
230
|
section_content,
|
|
231
|
+
filename,
|
|
232
|
+
agent_dirs["github"]["rules"],
|
|
233
|
+
glob_pattern,
|
|
383
234
|
)
|
|
384
|
-
else:
|
|
385
|
-
# It's a command - create command files using the original section name for header
|
|
386
|
-
write_cursor_command(section_content, filename, cursor_commands_dir, section_name)
|
|
387
|
-
write_github_prompt(
|
|
388
|
-
section_content, filename, github_prompts_dir, section_name
|
|
389
|
-
)
|
|
390
|
-
write_claude_command(section_content, filename, claude_commands_dir, section_name)
|
|
391
|
-
write_gemini_command(section_content, filename, gemini_commands_dir, section_name)
|
|
392
235
|
|
|
393
|
-
|
|
394
|
-
for section_name in SECTION_GLOBS:
|
|
236
|
+
for section_name in section_globs:
|
|
395
237
|
if section_name not in found_sections:
|
|
396
|
-
|
|
238
|
+
log.warning("section not found in file", section=section_name)
|
|
397
239
|
|
|
398
|
-
# Process unmapped sections
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
# Case insensitive check and avoid duplicate processing
|
|
405
|
-
if (
|
|
406
|
-
not any(
|
|
240
|
+
# Process unmapped sections for agents that support rules
|
|
241
|
+
if "cursor" in agent_instances or "github" in agent_instances:
|
|
242
|
+
for line in lines:
|
|
243
|
+
if line.startswith("## "):
|
|
244
|
+
section_name = line.strip()[3:]
|
|
245
|
+
if not any(
|
|
407
246
|
section_name.lower() == mapped_section.lower()
|
|
408
|
-
for mapped_section in
|
|
409
|
-
)
|
|
410
|
-
and section_name not in processed_unmapped
|
|
411
|
-
):
|
|
412
|
-
if process_unmapped_section(
|
|
413
|
-
lines, section_name, cursor_commands_dir, github_prompts_dir, claude_commands_dir, gemini_commands_dir
|
|
247
|
+
for mapped_section in section_globs
|
|
414
248
|
):
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
249
|
+
log.warning(
|
|
250
|
+
"unmapped section in instructions.md, treating as always-apply rule",
|
|
251
|
+
section=section_name,
|
|
252
|
+
)
|
|
253
|
+
section_content = extract_section(lines, f"## {section_name}")
|
|
254
|
+
|
|
255
|
+
if "cursor" in agent_instances and "github" in agent_instances:
|
|
256
|
+
process_unmapped_as_always_apply(
|
|
257
|
+
section_name,
|
|
258
|
+
section_content,
|
|
259
|
+
agent_instances["cursor"],
|
|
260
|
+
agent_instances["github"],
|
|
261
|
+
agent_dirs["cursor"]["rules"],
|
|
262
|
+
agent_dirs["github"]["rules"],
|
|
263
|
+
)
|
|
264
|
+
elif "cursor" in agent_instances:
|
|
265
|
+
# Only cursor - write just cursor rules
|
|
266
|
+
if any(line.strip() for line in section_content):
|
|
267
|
+
filename = header_to_filename(section_name)
|
|
268
|
+
section_content = replace_header_with_proper_casing(
|
|
269
|
+
section_content, section_name
|
|
270
|
+
)
|
|
271
|
+
agent_instances["cursor"].write_rule(
|
|
272
|
+
section_content,
|
|
273
|
+
filename,
|
|
274
|
+
agent_dirs["cursor"]["rules"],
|
|
275
|
+
glob_pattern=None,
|
|
276
|
+
)
|
|
277
|
+
elif "github" in agent_instances:
|
|
278
|
+
# Only github - write just github rules
|
|
279
|
+
if any(line.strip() for line in section_content):
|
|
280
|
+
filename = header_to_filename(section_name)
|
|
281
|
+
section_content = replace_header_with_proper_casing(
|
|
282
|
+
section_content, section_name
|
|
283
|
+
)
|
|
284
|
+
agent_instances["github"].write_rule(
|
|
285
|
+
section_content,
|
|
286
|
+
filename,
|
|
287
|
+
agent_dirs["github"]["rules"],
|
|
288
|
+
glob_pattern=None,
|
|
289
|
+
)
|
|
290
|
+
|
|
291
|
+
# Process commands for all agents
|
|
292
|
+
if commands_lines:
|
|
293
|
+
command_sections = extract_all_sections(commands_lines)
|
|
294
|
+
agents = [agent_instances[name] for name in agents_to_process]
|
|
295
|
+
command_dirs = {
|
|
296
|
+
name: agent_dirs[name]["commands"] for name in agents_to_process
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
for section_name, section_content in command_sections.items():
|
|
300
|
+
process_command_section(section_name, section_content, agents, command_dirs)
|
|
301
|
+
|
|
302
|
+
# Build log message and user output based on processed agents
|
|
303
|
+
log_data = {"agent": agent}
|
|
304
|
+
created_dirs = []
|
|
305
|
+
|
|
306
|
+
for agent_name in agents_to_process:
|
|
307
|
+
if agent_name in ["cursor", "github"]:
|
|
308
|
+
log_data[f"{agent_name}_rules"] = str(agent_dirs[agent_name]["rules"])
|
|
309
|
+
log_data[f"{agent_name}_commands"] = str(agent_dirs[agent_name]["commands"])
|
|
310
|
+
created_dirs.append(f".{agent_name}/")
|
|
311
|
+
else:
|
|
312
|
+
log_data[f"{agent_name}_commands"] = str(agent_dirs[agent_name]["commands"])
|
|
313
|
+
created_dirs.append(f".{agent_name}/")
|
|
314
|
+
|
|
315
|
+
if len(created_dirs) == 1:
|
|
316
|
+
success_msg = f"Created files in {created_dirs[0]} directory"
|
|
317
|
+
typer.echo(typer.style(success_msg, fg=typer.colors.GREEN))
|
|
318
|
+
else:
|
|
319
|
+
success_msg = f"Created files in {', '.join(created_dirs)} directories"
|
|
320
|
+
typer.echo(typer.style(success_msg, fg=typer.colors.GREEN))
|