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,400 +1,245 @@
|
|
|
1
1
|
"""Implode command: Bundle rule files into a single instruction file."""
|
|
2
2
|
|
|
3
|
-
import os
|
|
4
3
|
from pathlib import Path
|
|
5
4
|
from typing_extensions import Annotated
|
|
6
|
-
import logging
|
|
7
5
|
|
|
8
6
|
import typer
|
|
9
|
-
import structlog
|
|
10
|
-
|
|
11
|
-
from llm_ide_rules.constants import load_section_globs, header_to_filename, filename_to_header
|
|
12
|
-
|
|
13
|
-
logger = structlog.get_logger()
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
def get_ordered_files(file_list, section_globs_keys):
|
|
17
|
-
"""Order files based on SECTION_GLOBS key order, with unmapped files at the end."""
|
|
18
|
-
file_dict = {f.stem: f for f in file_list}
|
|
19
|
-
ordered_files = []
|
|
20
|
-
|
|
21
|
-
# Add files in SECTION_GLOBS order
|
|
22
|
-
for section_name in section_globs_keys:
|
|
23
|
-
filename = header_to_filename(section_name)
|
|
24
|
-
if filename in file_dict:
|
|
25
|
-
ordered_files.append(file_dict[filename])
|
|
26
|
-
del file_dict[filename]
|
|
27
|
-
|
|
28
|
-
# Add any remaining files (not in SECTION_GLOBS) sorted alphabetically
|
|
29
|
-
remaining_files = sorted(file_dict.values(), key=lambda p: p.name)
|
|
30
|
-
ordered_files.extend(remaining_files)
|
|
31
|
-
|
|
32
|
-
return ordered_files
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
def get_ordered_files_github(file_list, section_globs_keys):
|
|
36
|
-
"""Order GitHub instruction files based on SECTION_GLOBS key order, with unmapped files at the end.
|
|
37
|
-
Handles .instructions suffix by stripping it for ordering purposes."""
|
|
38
|
-
# Create dict mapping base filename (without .instructions) to the actual file
|
|
39
|
-
file_dict = {}
|
|
40
|
-
for f in file_list:
|
|
41
|
-
base_stem = f.stem.replace(".instructions", "")
|
|
42
|
-
file_dict[base_stem] = f
|
|
43
|
-
|
|
44
|
-
ordered_files = []
|
|
45
|
-
|
|
46
|
-
# Add files in SECTION_GLOBS order
|
|
47
|
-
for section_name in section_globs_keys:
|
|
48
|
-
filename = header_to_filename(section_name)
|
|
49
|
-
if filename in file_dict:
|
|
50
|
-
ordered_files.append(file_dict[filename])
|
|
51
|
-
del file_dict[filename]
|
|
52
|
-
|
|
53
|
-
# Add any remaining files (not in SECTION_GLOBS) sorted alphabetically
|
|
54
|
-
remaining_files = sorted(file_dict.values(), key=lambda p: p.name)
|
|
55
|
-
ordered_files.extend(remaining_files)
|
|
56
|
-
|
|
57
|
-
return ordered_files
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
def bundle_cursor_rules(rules_dir, commands_dir, output_file, section_globs):
|
|
61
|
-
"""Bundle Cursor rule and command files into a single file."""
|
|
62
|
-
rule_files = list(Path(rules_dir).glob("*.mdc"))
|
|
63
|
-
command_files = list(Path(commands_dir).glob("*.md"))
|
|
64
|
-
|
|
65
|
-
general = [f for f in rule_files if f.stem == "general"]
|
|
66
|
-
others = [f for f in rule_files if f.stem != "general"]
|
|
67
|
-
|
|
68
|
-
# Order the non-general files based on section_globs
|
|
69
|
-
ordered_others = get_ordered_files(others, section_globs.keys())
|
|
70
|
-
ordered_commands = get_ordered_files(command_files, section_globs.keys())
|
|
71
|
-
ordered = general + ordered_others + ordered_commands
|
|
72
|
-
|
|
73
|
-
def resolve_header_from_stem(stem):
|
|
74
|
-
"""Return the canonical header for a given filename stem.
|
|
75
|
-
|
|
76
|
-
Prefer exact header names from section_globs (preserves acronyms like FastAPI, TypeScript).
|
|
77
|
-
Fallback to title-casing the filename when not found in section_globs.
|
|
78
|
-
"""
|
|
79
|
-
for section_name in section_globs.keys():
|
|
80
|
-
if header_to_filename(section_name) == stem:
|
|
81
|
-
return section_name
|
|
82
|
-
return filename_to_header(stem)
|
|
83
|
-
|
|
84
|
-
with open(output_file, "w") as out:
|
|
85
|
-
for rule_file in ordered:
|
|
86
|
-
with open(rule_file, "r") as f:
|
|
87
|
-
content = f.read().strip()
|
|
88
|
-
if not content:
|
|
89
|
-
continue
|
|
90
|
-
content = strip_yaml_frontmatter(content)
|
|
91
|
-
content = strip_header(content)
|
|
92
|
-
# Use canonical header names from SECTION_GLOBS when available
|
|
93
|
-
header = resolve_header_from_stem(rule_file.stem)
|
|
94
|
-
if rule_file.stem != "general":
|
|
95
|
-
out.write(f"## {header}\n\n")
|
|
96
|
-
out.write(content)
|
|
97
|
-
out.write("\n\n")
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
def strip_yaml_frontmatter(text):
|
|
101
|
-
"""Strip YAML frontmatter from text."""
|
|
102
|
-
lines = text.splitlines()
|
|
103
|
-
if lines and lines[0].strip() == "---":
|
|
104
|
-
# Find the next '---' after the first
|
|
105
|
-
for i in range(1, len(lines)):
|
|
106
|
-
if lines[i].strip() == "---":
|
|
107
|
-
return "\n".join(lines[i + 1 :]).lstrip("\n")
|
|
108
|
-
return text
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
def strip_header(text):
|
|
112
|
-
"""Remove the first markdown header (## Header) from text if present."""
|
|
113
|
-
lines = text.splitlines()
|
|
114
|
-
if lines and lines[0].startswith("## "):
|
|
115
|
-
# Remove the header line and any immediately following empty lines
|
|
116
|
-
remaining_lines = lines[1:]
|
|
117
|
-
while remaining_lines and not remaining_lines[0].strip():
|
|
118
|
-
remaining_lines = remaining_lines[1:]
|
|
119
|
-
return "\n".join(remaining_lines)
|
|
120
|
-
return text
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
def bundle_github_instructions(instructions_dir, prompts_dir, output_file, section_globs):
|
|
124
|
-
"""Bundle GitHub instruction and prompt files into a single file."""
|
|
125
|
-
copilot_general = Path(os.getcwd()) / ".github" / "copilot-instructions.md"
|
|
126
|
-
instr_files = list(Path(instructions_dir).glob("*.instructions.md"))
|
|
127
|
-
prompt_files = list(Path(prompts_dir).glob("*.prompt.md"))
|
|
128
|
-
|
|
129
|
-
# Order the instruction files based on section_globs
|
|
130
|
-
# We need to create a modified version that strips .instructions from stems for ordering
|
|
131
|
-
ordered_instructions = get_ordered_files_github(instr_files, section_globs.keys())
|
|
132
|
-
|
|
133
|
-
# For prompts, we need to handle .prompt suffix similarly
|
|
134
|
-
ordered_prompts = []
|
|
135
|
-
prompt_dict = {}
|
|
136
|
-
for f in prompt_files:
|
|
137
|
-
base_stem = f.stem.replace(".prompt", "")
|
|
138
|
-
prompt_dict[base_stem] = f
|
|
139
|
-
|
|
140
|
-
for section_name in section_globs.keys():
|
|
141
|
-
filename = header_to_filename(section_name)
|
|
142
|
-
if filename in prompt_dict:
|
|
143
|
-
ordered_prompts.append(prompt_dict[filename])
|
|
144
|
-
del prompt_dict[filename]
|
|
145
|
-
|
|
146
|
-
# Add remaining prompts alphabetically
|
|
147
|
-
remaining_prompts = sorted(prompt_dict.values(), key=lambda p: p.name)
|
|
148
|
-
ordered_prompts.extend(remaining_prompts)
|
|
149
|
-
|
|
150
|
-
def resolve_header_from_stem(stem):
|
|
151
|
-
"""Return the canonical header for a given filename stem.
|
|
152
|
-
|
|
153
|
-
Prefer exact header names from section_globs (preserves acronyms like FastAPI, TypeScript).
|
|
154
|
-
Fallback to title-casing the filename when not found in section_globs.
|
|
155
|
-
"""
|
|
156
|
-
for section_name in section_globs.keys():
|
|
157
|
-
if header_to_filename(section_name) == stem:
|
|
158
|
-
return section_name
|
|
159
|
-
return filename_to_header(stem)
|
|
160
|
-
|
|
161
|
-
with open(output_file, "w") as out:
|
|
162
|
-
# Write general copilot instructions if present
|
|
163
|
-
if copilot_general.exists():
|
|
164
|
-
content = copilot_general.read_text().strip()
|
|
165
|
-
if content:
|
|
166
|
-
out.write(content)
|
|
167
|
-
out.write("\n\n")
|
|
168
|
-
for instr_file in ordered_instructions:
|
|
169
|
-
with open(instr_file, "r") as f:
|
|
170
|
-
content = f.read().strip()
|
|
171
|
-
if not content:
|
|
172
|
-
continue
|
|
173
|
-
content = strip_yaml_frontmatter(content)
|
|
174
|
-
content = strip_header(content)
|
|
175
|
-
# Use canonical header names from SECTION_GLOBS when available
|
|
176
|
-
base_stem = instr_file.stem.replace(".instructions", "")
|
|
177
|
-
header = resolve_header_from_stem(base_stem)
|
|
178
|
-
out.write(f"## {header}\n\n")
|
|
179
|
-
out.write(content)
|
|
180
|
-
out.write("\n\n")
|
|
181
|
-
for prompt_file in ordered_prompts:
|
|
182
|
-
with open(prompt_file, "r") as f:
|
|
183
|
-
content = f.read().strip()
|
|
184
|
-
if not content:
|
|
185
|
-
continue
|
|
186
|
-
content = strip_yaml_frontmatter(content)
|
|
187
|
-
content = strip_header(content)
|
|
188
|
-
# Use canonical header names from SECTION_GLOBS when available
|
|
189
|
-
base_stem = prompt_file.stem.replace(".prompt", "")
|
|
190
|
-
header = resolve_header_from_stem(base_stem)
|
|
191
|
-
out.write(f"## {header}\n\n")
|
|
192
|
-
out.write(content)
|
|
193
|
-
out.write("\n\n")
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
def bundle_claude_commands(commands_dir, output_file, section_globs):
|
|
197
|
-
"""Bundle Claude Code command files into a single file."""
|
|
198
|
-
command_files = list(Path(commands_dir).glob("*.md"))
|
|
199
|
-
ordered_commands = get_ordered_files(command_files, section_globs.keys())
|
|
200
|
-
|
|
201
|
-
def resolve_header_from_stem(stem):
|
|
202
|
-
"""Return the canonical header for a given filename stem.
|
|
203
|
-
|
|
204
|
-
Prefer exact header names from section_globs (preserves acronyms like FastAPI, TypeScript).
|
|
205
|
-
Fallback to title-casing the filename when not found in section_globs.
|
|
206
|
-
"""
|
|
207
|
-
for section_name in section_globs.keys():
|
|
208
|
-
if header_to_filename(section_name) == stem:
|
|
209
|
-
return section_name
|
|
210
|
-
return filename_to_header(stem)
|
|
211
|
-
|
|
212
|
-
with open(output_file, "w") as out:
|
|
213
|
-
for command_file in ordered_commands:
|
|
214
|
-
with open(command_file, "r") as f:
|
|
215
|
-
content = f.read().strip()
|
|
216
|
-
if not content:
|
|
217
|
-
continue
|
|
218
|
-
# Claude commands don't have frontmatter, just content
|
|
219
|
-
header = resolve_header_from_stem(command_file.stem)
|
|
220
|
-
out.write(f"## {header}\n\n")
|
|
221
|
-
out.write(content)
|
|
222
|
-
out.write("\n\n")
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
def strip_toml_metadata(text):
|
|
226
|
-
"""Extract content from TOML command.shell block."""
|
|
227
|
-
lines = text.splitlines()
|
|
228
|
-
in_shell_block = False
|
|
229
|
-
content_lines = []
|
|
230
|
-
|
|
231
|
-
for line in lines:
|
|
232
|
-
if line.strip() == '[command]':
|
|
233
|
-
in_shell_block = True
|
|
234
|
-
continue
|
|
235
|
-
if in_shell_block:
|
|
236
|
-
if line.strip().startswith('shell = """'):
|
|
237
|
-
# Start of shell content
|
|
238
|
-
# Check if content is on same line
|
|
239
|
-
after_start = line.split('"""', 1)[1] if '"""' in line else ""
|
|
240
|
-
if after_start.strip():
|
|
241
|
-
content_lines.append(after_start)
|
|
242
|
-
continue
|
|
243
|
-
if line.strip() == '"""' or line.strip().endswith('"""'):
|
|
244
|
-
# End of shell content
|
|
245
|
-
if line.strip() != '"""':
|
|
246
|
-
# Content on same line as closing
|
|
247
|
-
content_lines.append(line.rsplit('"""', 1)[0])
|
|
248
|
-
break
|
|
249
|
-
content_lines.append(line)
|
|
250
|
-
|
|
251
|
-
return "\n".join(content_lines).strip()
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
def bundle_gemini_commands(commands_dir, output_file, section_globs):
|
|
255
|
-
"""Bundle Gemini CLI command files into a single file."""
|
|
256
|
-
command_files = list(Path(commands_dir).glob("*.toml"))
|
|
257
|
-
ordered_commands = get_ordered_files(command_files, section_globs.keys())
|
|
258
|
-
|
|
259
|
-
def resolve_header_from_stem(stem):
|
|
260
|
-
"""Return the canonical header for a given filename stem.
|
|
261
|
-
|
|
262
|
-
Prefer exact header names from section_globs (preserves acronyms like FastAPI, TypeScript).
|
|
263
|
-
Fallback to title-casing the filename when not found in section_globs.
|
|
264
|
-
"""
|
|
265
|
-
for section_name in section_globs.keys():
|
|
266
|
-
if header_to_filename(section_name) == stem:
|
|
267
|
-
return section_name
|
|
268
|
-
return filename_to_header(stem)
|
|
269
|
-
|
|
270
|
-
with open(output_file, "w") as out:
|
|
271
|
-
for command_file in ordered_commands:
|
|
272
|
-
with open(command_file, "r") as f:
|
|
273
|
-
content = f.read().strip()
|
|
274
|
-
if not content:
|
|
275
|
-
continue
|
|
276
|
-
# Extract content from TOML shell block
|
|
277
|
-
content = strip_toml_metadata(content)
|
|
278
|
-
header = resolve_header_from_stem(command_file.stem)
|
|
279
|
-
out.write(f"## {header}\n\n")
|
|
280
|
-
out.write(content)
|
|
281
|
-
out.write("\n\n")
|
|
282
7
|
|
|
8
|
+
from llm_ide_rules.agents import get_agent
|
|
9
|
+
from llm_ide_rules.constants import load_section_globs
|
|
10
|
+
from llm_ide_rules.log import log
|
|
283
11
|
|
|
284
|
-
def cursor(
|
|
285
|
-
output: Annotated[str, typer.Argument(help="Output file")] = "instructions.md",
|
|
286
|
-
verbose: Annotated[bool, typer.Option("--verbose", "-v", help="Enable verbose logging")] = False,
|
|
287
|
-
config: Annotated[str, typer.Option("--config", "-c", help="Custom configuration file path")] = None,
|
|
288
|
-
):
|
|
289
|
-
"""Bundle Cursor rules and commands into a single file."""
|
|
290
|
-
if verbose:
|
|
291
|
-
logging.basicConfig(level=logging.DEBUG)
|
|
292
|
-
structlog.configure(
|
|
293
|
-
wrapper_class=structlog.make_filtering_bound_logger(logging.DEBUG),
|
|
294
|
-
)
|
|
295
|
-
|
|
296
|
-
# Load section globs (with optional custom config)
|
|
297
|
-
SECTION_GLOBS = load_section_globs(config)
|
|
298
12
|
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
13
|
+
def cursor(
|
|
14
|
+
output: Annotated[
|
|
15
|
+
str, typer.Argument(help="Output file for rules")
|
|
16
|
+
] = "instructions.md",
|
|
17
|
+
config: Annotated[
|
|
18
|
+
str | None,
|
|
19
|
+
typer.Option("--config", "-c", help="Custom configuration file path"),
|
|
20
|
+
] = None,
|
|
21
|
+
) -> None:
|
|
22
|
+
"""Bundle Cursor rules into instructions.md and commands into commands.md."""
|
|
23
|
+
|
|
24
|
+
section_globs = load_section_globs(config)
|
|
25
|
+
agent = get_agent("cursor")
|
|
26
|
+
cwd = Path.cwd()
|
|
27
|
+
|
|
28
|
+
rules_dir = agent.rules_dir
|
|
29
|
+
if not rules_dir:
|
|
30
|
+
log.error("cursor rules directory not configured")
|
|
31
|
+
raise typer.Exit(1)
|
|
304
32
|
|
|
305
|
-
|
|
306
|
-
|
|
33
|
+
log.info(
|
|
34
|
+
"bundling cursor rules and commands",
|
|
35
|
+
rules_dir=rules_dir,
|
|
36
|
+
commands_dir=agent.commands_dir,
|
|
37
|
+
config=config,
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
rules_path = cwd / rules_dir
|
|
41
|
+
if not rules_path.exists():
|
|
42
|
+
log.error("cursor rules directory not found", rules_dir=str(rules_path))
|
|
43
|
+
error_msg = f"Cursor rules directory not found: {rules_path}"
|
|
44
|
+
typer.echo(typer.style(error_msg, fg=typer.colors.RED), err=True)
|
|
307
45
|
raise typer.Exit(1)
|
|
308
46
|
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
47
|
+
output_path = cwd / output
|
|
48
|
+
rules_written = agent.bundle_rules(output_path, section_globs)
|
|
49
|
+
if rules_written:
|
|
50
|
+
success_msg = f"Bundled cursor rules into {output}"
|
|
51
|
+
typer.echo(typer.style(success_msg, fg=typer.colors.GREEN))
|
|
52
|
+
else:
|
|
53
|
+
output_path.unlink(missing_ok=True)
|
|
54
|
+
log.info("no cursor rules to bundle")
|
|
55
|
+
|
|
56
|
+
commands_output_path = cwd / "commands.md"
|
|
57
|
+
commands_written = agent.bundle_commands(commands_output_path, section_globs)
|
|
58
|
+
if commands_written:
|
|
59
|
+
success_msg = "Bundled cursor commands into commands.md"
|
|
60
|
+
typer.echo(typer.style(success_msg, fg=typer.colors.GREEN))
|
|
61
|
+
else:
|
|
62
|
+
commands_output_path.unlink(missing_ok=True)
|
|
312
63
|
|
|
313
64
|
|
|
314
65
|
def github(
|
|
315
|
-
output: Annotated[
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
66
|
+
output: Annotated[
|
|
67
|
+
str, typer.Argument(help="Output file for instructions")
|
|
68
|
+
] = "instructions.md",
|
|
69
|
+
config: Annotated[
|
|
70
|
+
str | None,
|
|
71
|
+
typer.Option("--config", "-c", help="Custom configuration file path"),
|
|
72
|
+
] = None,
|
|
73
|
+
) -> None:
|
|
74
|
+
"""Bundle GitHub instructions into instructions.md and prompts into commands.md."""
|
|
75
|
+
|
|
76
|
+
section_globs = load_section_globs(config)
|
|
77
|
+
agent = get_agent("github")
|
|
78
|
+
cwd = Path.cwd()
|
|
79
|
+
|
|
80
|
+
rules_dir = agent.rules_dir
|
|
81
|
+
if not rules_dir:
|
|
82
|
+
log.error("github rules directory not configured")
|
|
83
|
+
raise typer.Exit(1)
|
|
84
|
+
|
|
85
|
+
log.info(
|
|
86
|
+
"bundling github instructions and prompts",
|
|
87
|
+
instructions_dir=rules_dir,
|
|
88
|
+
prompts_dir=agent.commands_dir,
|
|
89
|
+
config=config,
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
rules_path = cwd / rules_dir
|
|
93
|
+
if not rules_path.exists():
|
|
94
|
+
log.error(
|
|
95
|
+
"github instructions directory not found", instructions_dir=str(rules_path)
|
|
324
96
|
)
|
|
97
|
+
error_msg = f"GitHub instructions directory not found: {rules_path}"
|
|
98
|
+
typer.echo(typer.style(error_msg, fg=typer.colors.RED), err=True)
|
|
99
|
+
raise typer.Exit(1)
|
|
325
100
|
|
|
326
|
-
|
|
327
|
-
|
|
101
|
+
output_path = cwd / output
|
|
102
|
+
instructions_written = agent.bundle_rules(output_path, section_globs)
|
|
103
|
+
if instructions_written:
|
|
104
|
+
success_msg = f"Bundled github instructions into {output}"
|
|
105
|
+
typer.echo(typer.style(success_msg, fg=typer.colors.GREEN))
|
|
106
|
+
else:
|
|
107
|
+
output_path.unlink(missing_ok=True)
|
|
108
|
+
log.info("no github instructions to bundle")
|
|
328
109
|
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
110
|
+
commands_output_path = cwd / "commands.md"
|
|
111
|
+
prompts_written = agent.bundle_commands(commands_output_path, section_globs)
|
|
112
|
+
if prompts_written:
|
|
113
|
+
success_msg = "Bundled github prompts into commands.md"
|
|
114
|
+
typer.echo(typer.style(success_msg, fg=typer.colors.GREEN))
|
|
115
|
+
else:
|
|
116
|
+
commands_output_path.unlink(missing_ok=True)
|
|
332
117
|
|
|
333
|
-
logger.info("Bundling GitHub instructions and prompts", instructions_dir=instructions_dir, prompts_dir=prompts_dir, output_file=output_path, config=config)
|
|
334
118
|
|
|
335
|
-
|
|
336
|
-
|
|
119
|
+
def claude(
|
|
120
|
+
output: Annotated[str, typer.Argument(help="Output file")] = "commands.md",
|
|
121
|
+
config: Annotated[
|
|
122
|
+
str | None,
|
|
123
|
+
typer.Option("--config", "-c", help="Custom configuration file path"),
|
|
124
|
+
] = None,
|
|
125
|
+
) -> None:
|
|
126
|
+
"""Bundle Claude Code commands into commands.md."""
|
|
127
|
+
|
|
128
|
+
section_globs = load_section_globs(config)
|
|
129
|
+
agent = get_agent("claude")
|
|
130
|
+
cwd = Path.cwd()
|
|
131
|
+
|
|
132
|
+
commands_dir = agent.commands_dir
|
|
133
|
+
if not commands_dir:
|
|
134
|
+
log.error("claude code commands directory not configured")
|
|
337
135
|
raise typer.Exit(1)
|
|
338
136
|
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
137
|
+
log.info(
|
|
138
|
+
"bundling claude code commands",
|
|
139
|
+
commands_dir=commands_dir,
|
|
140
|
+
config=config,
|
|
141
|
+
)
|
|
343
142
|
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
):
|
|
349
|
-
"""Bundle Claude Code commands into a single file."""
|
|
350
|
-
if verbose:
|
|
351
|
-
logging.basicConfig(level=logging.DEBUG)
|
|
352
|
-
structlog.configure(
|
|
353
|
-
wrapper_class=structlog.make_filtering_bound_logger(logging.DEBUG),
|
|
143
|
+
commands_path = cwd / commands_dir
|
|
144
|
+
if not commands_path.exists():
|
|
145
|
+
log.error(
|
|
146
|
+
"claude code commands directory not found", commands_dir=str(commands_path)
|
|
354
147
|
)
|
|
148
|
+
error_msg = f"Claude Code commands directory not found: {commands_path}"
|
|
149
|
+
typer.echo(typer.style(error_msg, fg=typer.colors.RED), err=True)
|
|
150
|
+
raise typer.Exit(1)
|
|
355
151
|
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
152
|
+
output_path = cwd / output
|
|
153
|
+
commands_written = agent.bundle_commands(output_path, section_globs)
|
|
154
|
+
if commands_written:
|
|
155
|
+
success_msg = f"Bundled claude commands into {output}"
|
|
156
|
+
typer.echo(typer.style(success_msg, fg=typer.colors.GREEN))
|
|
157
|
+
else:
|
|
158
|
+
output_path.unlink(missing_ok=True)
|
|
159
|
+
log.info("no claude commands to bundle")
|
|
361
160
|
|
|
362
|
-
logger.info("Bundling Claude Code commands", commands_dir=commands_dir, output_file=output_path, config=config)
|
|
363
161
|
|
|
364
|
-
|
|
365
|
-
|
|
162
|
+
def gemini(
|
|
163
|
+
output: Annotated[str, typer.Argument(help="Output file")] = "commands.md",
|
|
164
|
+
config: Annotated[
|
|
165
|
+
str | None,
|
|
166
|
+
typer.Option("--config", "-c", help="Custom configuration file path"),
|
|
167
|
+
] = None,
|
|
168
|
+
) -> None:
|
|
169
|
+
"""Bundle Gemini CLI commands into commands.md."""
|
|
170
|
+
|
|
171
|
+
section_globs = load_section_globs(config)
|
|
172
|
+
agent = get_agent("gemini")
|
|
173
|
+
cwd = Path.cwd()
|
|
174
|
+
|
|
175
|
+
commands_dir = agent.commands_dir
|
|
176
|
+
if not commands_dir:
|
|
177
|
+
log.error("gemini cli commands directory not configured")
|
|
366
178
|
raise typer.Exit(1)
|
|
367
179
|
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
180
|
+
log.info(
|
|
181
|
+
"bundling gemini cli commands",
|
|
182
|
+
commands_dir=commands_dir,
|
|
183
|
+
config=config,
|
|
184
|
+
)
|
|
372
185
|
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
):
|
|
378
|
-
"""Bundle Gemini CLI commands into a single file."""
|
|
379
|
-
if verbose:
|
|
380
|
-
logging.basicConfig(level=logging.DEBUG)
|
|
381
|
-
structlog.configure(
|
|
382
|
-
wrapper_class=structlog.make_filtering_bound_logger(logging.DEBUG),
|
|
186
|
+
commands_path = cwd / commands_dir
|
|
187
|
+
if not commands_path.exists():
|
|
188
|
+
log.error(
|
|
189
|
+
"gemini cli commands directory not found", commands_dir=str(commands_path)
|
|
383
190
|
)
|
|
191
|
+
error_msg = f"Gemini CLI commands directory not found: {commands_path}"
|
|
192
|
+
typer.echo(typer.style(error_msg, fg=typer.colors.RED), err=True)
|
|
193
|
+
raise typer.Exit(1)
|
|
384
194
|
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
195
|
+
output_path = cwd / output
|
|
196
|
+
commands_written = agent.bundle_commands(output_path, section_globs)
|
|
197
|
+
if commands_written:
|
|
198
|
+
success_msg = f"Bundled gemini commands into {output}"
|
|
199
|
+
typer.echo(typer.style(success_msg, fg=typer.colors.GREEN))
|
|
200
|
+
else:
|
|
201
|
+
output_path.unlink(missing_ok=True)
|
|
202
|
+
log.info("no gemini commands to bundle")
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
def opencode(
|
|
206
|
+
output: Annotated[str, typer.Argument(help="Output file")] = "commands.md",
|
|
207
|
+
config: Annotated[
|
|
208
|
+
str | None,
|
|
209
|
+
typer.Option("--config", "-c", help="Custom configuration file path"),
|
|
210
|
+
] = None,
|
|
211
|
+
) -> None:
|
|
212
|
+
"""Bundle OpenCode commands into commands.md."""
|
|
213
|
+
|
|
214
|
+
section_globs = load_section_globs(config)
|
|
215
|
+
agent = get_agent("opencode")
|
|
216
|
+
cwd = Path.cwd()
|
|
217
|
+
|
|
218
|
+
commands_dir = agent.commands_dir
|
|
219
|
+
if not commands_dir:
|
|
220
|
+
log.error("opencode commands directory not configured")
|
|
221
|
+
raise typer.Exit(1)
|
|
390
222
|
|
|
391
|
-
|
|
223
|
+
log.info(
|
|
224
|
+
"bundling opencode commands",
|
|
225
|
+
commands_dir=commands_dir,
|
|
226
|
+
config=config,
|
|
227
|
+
)
|
|
392
228
|
|
|
393
|
-
|
|
394
|
-
|
|
229
|
+
commands_path = cwd / commands_dir
|
|
230
|
+
if not commands_path.exists():
|
|
231
|
+
log.error(
|
|
232
|
+
"opencode commands directory not found", commands_dir=str(commands_path)
|
|
233
|
+
)
|
|
234
|
+
error_msg = f"OpenCode commands directory not found: {commands_path}"
|
|
235
|
+
typer.echo(typer.style(error_msg, fg=typer.colors.RED), err=True)
|
|
395
236
|
raise typer.Exit(1)
|
|
396
237
|
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
238
|
+
output_path = cwd / output
|
|
239
|
+
commands_written = agent.bundle_commands(output_path, section_globs)
|
|
240
|
+
if commands_written:
|
|
241
|
+
success_msg = f"Bundled opencode commands into {output}"
|
|
242
|
+
typer.echo(typer.style(success_msg, fg=typer.colors.GREEN))
|
|
243
|
+
else:
|
|
244
|
+
output_path.unlink(missing_ok=True)
|
|
245
|
+
log.info("no opencode commands to bundle")
|