llm-ide-rules 0.4.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 -7
- 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 +227 -243
- llm_ide_rules/commands/implode.py +229 -202
- 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 -5
- {llm_ide_rules-0.4.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.4.0.dist-info → llm_ide_rules-0.6.0.dist-info}/WHEEL +2 -2
- llm_ide_rules-0.4.0.dist-info/RECORD +0 -12
- {llm_ide_rules-0.4.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,200 +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
|
-
|
|
56
|
+
if current_section:
|
|
57
|
+
sections[current_section] = current_content
|
|
149
58
|
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
59
|
+
current_section = line.strip()[3:]
|
|
60
|
+
current_content = [line]
|
|
61
|
+
elif current_section:
|
|
62
|
+
current_content.append(line)
|
|
153
63
|
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
description, filtered_content = extract_description_and_filter_content(
|
|
157
|
-
content_lines, default_description
|
|
158
|
-
)
|
|
64
|
+
if current_section:
|
|
65
|
+
sections[current_section] = current_content
|
|
159
66
|
|
|
160
|
-
|
|
161
|
-
# Only add frontmatter if description is not empty
|
|
162
|
-
if description:
|
|
163
|
-
frontmatter = f"""---
|
|
164
|
-
description: {description}
|
|
165
|
-
---
|
|
166
|
-
"""
|
|
167
|
-
f.write(frontmatter)
|
|
67
|
+
return sections
|
|
168
68
|
|
|
169
|
-
for line in filtered_content:
|
|
170
|
-
f.write(line)
|
|
171
69
|
|
|
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
|
|
172
79
|
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
filepath = os.path.join(prompts_dir, filename + ".prompt.md")
|
|
80
|
+
filename = header_to_filename(section_name)
|
|
81
|
+
section_content = replace_header_with_proper_casing(section_content, section_name)
|
|
176
82
|
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
83
|
+
for agent in agents:
|
|
84
|
+
if agent.commands_dir:
|
|
85
|
+
agent.write_command(
|
|
86
|
+
section_content, filename, dirs[agent.name], section_name
|
|
87
|
+
)
|
|
182
88
|
|
|
183
|
-
|
|
184
|
-
mode: 'agent'
|
|
185
|
-
description: '{description}'
|
|
186
|
-
---
|
|
187
|
-
"""
|
|
89
|
+
return True
|
|
188
90
|
|
|
189
|
-
with open(filepath, "w") as f:
|
|
190
|
-
f.write(frontmatter)
|
|
191
|
-
for line in filtered_content:
|
|
192
|
-
f.write(line)
|
|
193
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
|
|
194
103
|
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
section_content = extract_section(lines, f"## {section_name}")
|
|
198
|
-
if any(line.strip() for line in section_content):
|
|
199
|
-
filename = header_to_filename(section_name)
|
|
104
|
+
filename = header_to_filename(section_name)
|
|
105
|
+
section_content = replace_header_with_proper_casing(section_content, section_name)
|
|
200
106
|
|
|
201
|
-
|
|
202
|
-
section_content =
|
|
203
|
-
|
|
204
|
-
|
|
107
|
+
cursor_agent.write_rule(
|
|
108
|
+
section_content, filename, cursor_rules_dir, glob_pattern=None
|
|
109
|
+
)
|
|
110
|
+
github_agent.write_rule(section_content, filename, copilot_dir, glob_pattern=None)
|
|
205
111
|
|
|
206
|
-
|
|
207
|
-
write_cursor_prompt(section_content, filename, rules_dir, section_name)
|
|
208
|
-
write_github_prompt(section_content, filename, github_prompts_dir, section_name)
|
|
209
|
-
return True
|
|
210
|
-
return False
|
|
112
|
+
return True
|
|
211
113
|
|
|
212
114
|
|
|
213
115
|
def explode_main(
|
|
214
116
|
input_file: Annotated[
|
|
215
117
|
str, typer.Argument(help="Input markdown file")
|
|
216
118
|
] = "instructions.md",
|
|
217
|
-
verbose: Annotated[
|
|
218
|
-
bool, typer.Option("--verbose", "-v", help="Enable verbose logging")
|
|
219
|
-
] = False,
|
|
220
119
|
config: Annotated[
|
|
221
|
-
str
|
|
120
|
+
str | None,
|
|
121
|
+
typer.Option("--config", "-c", help="Custom configuration file path"),
|
|
222
122
|
] = None,
|
|
223
|
-
|
|
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:
|
|
224
132
|
"""Convert instruction file to separate rule files."""
|
|
225
|
-
if verbose:
|
|
226
|
-
logging.basicConfig(level=logging.DEBUG)
|
|
227
|
-
structlog.configure(
|
|
228
|
-
wrapper_class=structlog.make_filtering_bound_logger(logging.DEBUG),
|
|
229
|
-
)
|
|
230
133
|
|
|
231
|
-
|
|
232
|
-
|
|
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)}"
|
|
138
|
+
)
|
|
139
|
+
typer.echo(typer.style(error_msg, fg=typer.colors.RED), err=True)
|
|
140
|
+
raise typer.Exit(1)
|
|
233
141
|
|
|
234
|
-
|
|
142
|
+
section_globs = load_section_globs(config)
|
|
235
143
|
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
github_prompts_dir = os.path.join(os.getcwd(), ".github", "prompts")
|
|
144
|
+
log.info(
|
|
145
|
+
"starting explode operation", input_file=input_file, agent=agent, config=config
|
|
146
|
+
)
|
|
240
147
|
|
|
241
|
-
|
|
242
|
-
os.makedirs(copilot_dir, exist_ok=True)
|
|
243
|
-
os.makedirs(github_prompts_dir, exist_ok=True)
|
|
148
|
+
cwd = Path.cwd()
|
|
244
149
|
|
|
245
|
-
|
|
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}
|
|
176
|
+
|
|
177
|
+
input_path = cwd / input_file
|
|
246
178
|
|
|
247
179
|
try:
|
|
248
|
-
|
|
249
|
-
lines = f.readlines()
|
|
180
|
+
lines = input_path.read_text().splitlines(keepends=True)
|
|
250
181
|
except FileNotFoundError:
|
|
251
|
-
|
|
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)
|
|
252
185
|
raise typer.Exit(1)
|
|
253
186
|
|
|
254
|
-
|
|
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
|
|
255
194
|
general = extract_general(lines)
|
|
256
195
|
if any(line.strip() for line in general):
|
|
257
196
|
general_header = """
|
|
@@ -260,77 +199,122 @@ description:
|
|
|
260
199
|
alwaysApply: true
|
|
261
200
|
---
|
|
262
201
|
"""
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
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)
|
|
268
208
|
|
|
269
|
-
# Process
|
|
209
|
+
# Process mapped sections for agents that support rules
|
|
270
210
|
found_sections = set()
|
|
271
|
-
for section_name,
|
|
211
|
+
for section_name, glob_pattern in section_globs.items():
|
|
272
212
|
section_content = extract_section(lines, f"## {section_name}")
|
|
273
213
|
if any(line.strip() for line in section_content):
|
|
274
214
|
found_sections.add(section_name)
|
|
275
215
|
filename = header_to_filename(section_name)
|
|
276
216
|
|
|
277
|
-
# Replace header with proper casing from SECTION_GLOBS
|
|
278
217
|
section_content = replace_header_with_proper_casing(
|
|
279
218
|
section_content, section_name
|
|
280
219
|
)
|
|
281
220
|
|
|
282
|
-
if
|
|
283
|
-
|
|
284
|
-
cursor_header = generate_cursor_frontmatter(glob_or_description)
|
|
285
|
-
write_rule(
|
|
286
|
-
os.path.join(rules_dir, filename + ".mdc"),
|
|
287
|
-
cursor_header,
|
|
221
|
+
if "cursor" in agent_instances:
|
|
222
|
+
agent_instances["cursor"].write_rule(
|
|
288
223
|
section_content,
|
|
224
|
+
filename,
|
|
225
|
+
agent_dirs["cursor"]["rules"],
|
|
226
|
+
glob_pattern,
|
|
289
227
|
)
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
write_rule(
|
|
293
|
-
os.path.join(copilot_dir, filename + ".instructions.md"),
|
|
294
|
-
copilot_header,
|
|
228
|
+
if "github" in agent_instances:
|
|
229
|
+
agent_instances["github"].write_rule(
|
|
295
230
|
section_content,
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
write_cursor_prompt(section_content, filename, rules_dir, section_name)
|
|
300
|
-
write_github_prompt(
|
|
301
|
-
section_content, filename, github_prompts_dir, section_name
|
|
231
|
+
filename,
|
|
232
|
+
agent_dirs["github"]["rules"],
|
|
233
|
+
glob_pattern,
|
|
302
234
|
)
|
|
303
235
|
|
|
304
|
-
|
|
305
|
-
for section_name in SECTION_GLOBS:
|
|
236
|
+
for section_name in section_globs:
|
|
306
237
|
if section_name not in found_sections:
|
|
307
|
-
|
|
238
|
+
log.warning("section not found in file", section=section_name)
|
|
308
239
|
|
|
309
|
-
# Process unmapped sections
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
# Case insensitive check and avoid duplicate processing
|
|
316
|
-
if (
|
|
317
|
-
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(
|
|
318
246
|
section_name.lower() == mapped_section.lower()
|
|
319
|
-
for mapped_section in
|
|
320
|
-
)
|
|
321
|
-
and section_name not in processed_unmapped
|
|
322
|
-
):
|
|
323
|
-
if process_unmapped_section(
|
|
324
|
-
lines, section_name, rules_dir, github_prompts_dir
|
|
247
|
+
for mapped_section in section_globs
|
|
325
248
|
):
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
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))
|