llm-ide-rules 0.5.0__py3-none-any.whl → 0.7.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 +329 -0
- llm_ide_rules/agents/claude.py +108 -0
- llm_ide_rules/agents/cursor.py +199 -0
- llm_ide_rules/agents/gemini.py +177 -0
- llm_ide_rules/agents/github.py +212 -0
- llm_ide_rules/agents/opencode.py +130 -0
- llm_ide_rules/commands/delete.py +24 -34
- llm_ide_rules/commands/download.py +146 -60
- llm_ide_rules/commands/explode.py +222 -382
- llm_ide_rules/commands/implode.py +174 -360
- llm_ide_rules/commands/mcp.py +119 -0
- llm_ide_rules/constants.py +6 -29
- llm_ide_rules/log.py +9 -0
- llm_ide_rules/markdown_parser.py +108 -0
- llm_ide_rules/mcp/__init__.py +7 -0
- llm_ide_rules/mcp/models.py +21 -0
- {llm_ide_rules-0.5.0.dist-info → llm_ide_rules-0.7.0.dist-info}/METADATA +36 -59
- llm_ide_rules-0.7.0.dist-info/RECORD +23 -0
- {llm_ide_rules-0.5.0.dist-info → llm_ide_rules-0.7.0.dist-info}/WHEEL +2 -2
- llm_ide_rules/sections.json +0 -27
- llm_ide_rules-0.5.0.dist-info/RECORD +0 -12
- {llm_ide_rules-0.5.0.dist-info → llm_ide_rules-0.7.0.dist-info}/entry_points.txt +0 -0
|
@@ -1,400 +1,214 @@
|
|
|
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
|
-
|
|
10
|
-
|
|
11
|
-
from llm_ide_rules.
|
|
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")
|
|
7
|
+
|
|
8
|
+
from llm_ide_rules.agents import get_agent
|
|
9
|
+
from llm_ide_rules.log import log
|
|
282
10
|
|
|
283
11
|
|
|
284
12
|
def cursor(
|
|
285
|
-
output: Annotated[
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
):
|
|
289
|
-
"""Bundle Cursor rules and commands into
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
13
|
+
output: Annotated[
|
|
14
|
+
str, typer.Argument(help="Output file for rules")
|
|
15
|
+
] = "instructions.md",
|
|
16
|
+
) -> None:
|
|
17
|
+
"""Bundle Cursor rules into instructions.md and commands into commands.md."""
|
|
18
|
+
|
|
19
|
+
agent = get_agent("cursor")
|
|
20
|
+
cwd = Path.cwd()
|
|
21
|
+
|
|
22
|
+
rules_dir = agent.rules_dir
|
|
23
|
+
if not rules_dir:
|
|
24
|
+
log.error("cursor rules directory not configured")
|
|
25
|
+
raise typer.Exit(1)
|
|
295
26
|
|
|
296
|
-
|
|
297
|
-
|
|
27
|
+
log.info(
|
|
28
|
+
"bundling cursor rules and commands",
|
|
29
|
+
rules_dir=rules_dir,
|
|
30
|
+
commands_dir=agent.commands_dir,
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
rules_path = cwd / rules_dir
|
|
34
|
+
if not rules_path.exists():
|
|
35
|
+
log.error("cursor rules directory not found", rules_dir=str(rules_path))
|
|
36
|
+
error_msg = f"Cursor rules directory not found: {rules_path}"
|
|
37
|
+
typer.echo(typer.style(error_msg, fg=typer.colors.RED), err=True)
|
|
38
|
+
raise typer.Exit(1)
|
|
298
39
|
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
40
|
+
output_path = cwd / output
|
|
41
|
+
rules_written = agent.bundle_rules(output_path)
|
|
42
|
+
if rules_written:
|
|
43
|
+
success_msg = f"Bundled cursor rules into {output}"
|
|
44
|
+
typer.echo(typer.style(success_msg, fg=typer.colors.GREEN))
|
|
45
|
+
else:
|
|
46
|
+
output_path.unlink(missing_ok=True)
|
|
47
|
+
log.info("no cursor rules to bundle")
|
|
302
48
|
|
|
303
|
-
|
|
49
|
+
commands_output_path = cwd / "commands.md"
|
|
50
|
+
commands_written = agent.bundle_commands(commands_output_path)
|
|
51
|
+
if commands_written:
|
|
52
|
+
success_msg = "Bundled cursor commands into commands.md"
|
|
53
|
+
typer.echo(typer.style(success_msg, fg=typer.colors.GREEN))
|
|
54
|
+
else:
|
|
55
|
+
commands_output_path.unlink(missing_ok=True)
|
|
304
56
|
|
|
305
|
-
if not Path(rules_dir).exists():
|
|
306
|
-
logger.error("Cursor rules directory not found", rules_dir=rules_dir)
|
|
307
|
-
raise typer.Exit(1)
|
|
308
57
|
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
58
|
+
def github(
|
|
59
|
+
output: Annotated[
|
|
60
|
+
str, typer.Argument(help="Output file for instructions")
|
|
61
|
+
] = "instructions.md",
|
|
62
|
+
) -> None:
|
|
63
|
+
"""Bundle GitHub instructions into instructions.md and prompts into commands.md."""
|
|
64
|
+
|
|
65
|
+
agent = get_agent("github")
|
|
66
|
+
cwd = Path.cwd()
|
|
67
|
+
|
|
68
|
+
rules_dir = agent.rules_dir
|
|
69
|
+
if not rules_dir:
|
|
70
|
+
log.error("github rules directory not configured")
|
|
71
|
+
raise typer.Exit(1)
|
|
312
72
|
|
|
73
|
+
log.info(
|
|
74
|
+
"bundling github instructions and prompts",
|
|
75
|
+
instructions_dir=rules_dir,
|
|
76
|
+
prompts_dir=agent.commands_dir,
|
|
77
|
+
)
|
|
313
78
|
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
):
|
|
319
|
-
"""Bundle GitHub/Copilot instructions and prompts into a single file."""
|
|
320
|
-
if verbose:
|
|
321
|
-
logging.basicConfig(level=logging.DEBUG)
|
|
322
|
-
structlog.configure(
|
|
323
|
-
wrapper_class=structlog.make_filtering_bound_logger(logging.DEBUG),
|
|
79
|
+
rules_path = cwd / rules_dir
|
|
80
|
+
if not rules_path.exists():
|
|
81
|
+
log.error(
|
|
82
|
+
"github instructions directory not found", instructions_dir=str(rules_path)
|
|
324
83
|
)
|
|
84
|
+
error_msg = f"GitHub instructions directory not found: {rules_path}"
|
|
85
|
+
typer.echo(typer.style(error_msg, fg=typer.colors.RED), err=True)
|
|
86
|
+
raise typer.Exit(1)
|
|
325
87
|
|
|
326
|
-
|
|
327
|
-
|
|
88
|
+
output_path = cwd / output
|
|
89
|
+
instructions_written = agent.bundle_rules(output_path)
|
|
90
|
+
if instructions_written:
|
|
91
|
+
success_msg = f"Bundled github instructions into {output}"
|
|
92
|
+
typer.echo(typer.style(success_msg, fg=typer.colors.GREEN))
|
|
93
|
+
else:
|
|
94
|
+
output_path.unlink(missing_ok=True)
|
|
95
|
+
log.info("no github instructions to bundle")
|
|
328
96
|
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
97
|
+
commands_output_path = cwd / "commands.md"
|
|
98
|
+
prompts_written = agent.bundle_commands(commands_output_path)
|
|
99
|
+
if prompts_written:
|
|
100
|
+
success_msg = "Bundled github prompts into commands.md"
|
|
101
|
+
typer.echo(typer.style(success_msg, fg=typer.colors.GREEN))
|
|
102
|
+
else:
|
|
103
|
+
commands_output_path.unlink(missing_ok=True)
|
|
332
104
|
|
|
333
|
-
logger.info("Bundling GitHub instructions and prompts", instructions_dir=instructions_dir, prompts_dir=prompts_dir, output_file=output_path, config=config)
|
|
334
105
|
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
106
|
+
def claude(
|
|
107
|
+
output: Annotated[str, typer.Argument(help="Output file")] = "commands.md",
|
|
108
|
+
) -> None:
|
|
109
|
+
"""Bundle Claude Code commands into commands.md."""
|
|
110
|
+
|
|
111
|
+
agent = get_agent("claude")
|
|
112
|
+
cwd = Path.cwd()
|
|
338
113
|
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
114
|
+
commands_dir = agent.commands_dir
|
|
115
|
+
if not commands_dir:
|
|
116
|
+
log.error("claude code commands directory not configured")
|
|
117
|
+
raise typer.Exit(1)
|
|
342
118
|
|
|
119
|
+
log.info(
|
|
120
|
+
"bundling claude code commands",
|
|
121
|
+
commands_dir=commands_dir,
|
|
122
|
+
)
|
|
343
123
|
|
|
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),
|
|
124
|
+
commands_path = cwd / commands_dir
|
|
125
|
+
if not commands_path.exists():
|
|
126
|
+
log.error(
|
|
127
|
+
"claude code commands directory not found", commands_dir=str(commands_path)
|
|
354
128
|
)
|
|
129
|
+
error_msg = f"Claude Code commands directory not found: {commands_path}"
|
|
130
|
+
typer.echo(typer.style(error_msg, fg=typer.colors.RED), err=True)
|
|
131
|
+
raise typer.Exit(1)
|
|
355
132
|
|
|
356
|
-
|
|
357
|
-
|
|
133
|
+
output_path = cwd / output
|
|
134
|
+
commands_written = agent.bundle_commands(output_path)
|
|
135
|
+
if commands_written:
|
|
136
|
+
success_msg = f"Bundled claude commands into {output}"
|
|
137
|
+
typer.echo(typer.style(success_msg, fg=typer.colors.GREEN))
|
|
138
|
+
else:
|
|
139
|
+
output_path.unlink(missing_ok=True)
|
|
140
|
+
log.info("no claude commands to bundle")
|
|
358
141
|
|
|
359
|
-
commands_dir = os.path.join(os.getcwd(), ".claude", "commands")
|
|
360
|
-
output_path = os.path.join(os.getcwd(), output)
|
|
361
142
|
|
|
362
|
-
|
|
143
|
+
def gemini(
|
|
144
|
+
output: Annotated[str, typer.Argument(help="Output file")] = "commands.md",
|
|
145
|
+
) -> None:
|
|
146
|
+
"""Bundle Gemini CLI commands into commands.md."""
|
|
363
147
|
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
raise typer.Exit(1)
|
|
148
|
+
agent = get_agent("gemini")
|
|
149
|
+
cwd = Path.cwd()
|
|
367
150
|
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
151
|
+
commands_dir = agent.commands_dir
|
|
152
|
+
if not commands_dir:
|
|
153
|
+
log.error("gemini cli commands directory not configured")
|
|
154
|
+
raise typer.Exit(1)
|
|
371
155
|
|
|
156
|
+
log.info(
|
|
157
|
+
"bundling gemini cli commands",
|
|
158
|
+
commands_dir=commands_dir,
|
|
159
|
+
)
|
|
372
160
|
|
|
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),
|
|
161
|
+
commands_path = cwd / commands_dir
|
|
162
|
+
if not commands_path.exists():
|
|
163
|
+
log.error(
|
|
164
|
+
"gemini cli commands directory not found", commands_dir=str(commands_path)
|
|
383
165
|
)
|
|
166
|
+
error_msg = f"Gemini CLI commands directory not found: {commands_path}"
|
|
167
|
+
typer.echo(typer.style(error_msg, fg=typer.colors.RED), err=True)
|
|
168
|
+
raise typer.Exit(1)
|
|
169
|
+
|
|
170
|
+
output_path = cwd / output
|
|
171
|
+
commands_written = agent.bundle_commands(output_path)
|
|
172
|
+
if commands_written:
|
|
173
|
+
success_msg = f"Bundled gemini commands into {output}"
|
|
174
|
+
typer.echo(typer.style(success_msg, fg=typer.colors.GREEN))
|
|
175
|
+
else:
|
|
176
|
+
output_path.unlink(missing_ok=True)
|
|
177
|
+
log.info("no gemini commands to bundle")
|
|
384
178
|
|
|
385
|
-
# Load section globs (with optional custom config)
|
|
386
|
-
SECTION_GLOBS = load_section_globs(config)
|
|
387
179
|
|
|
388
|
-
|
|
389
|
-
|
|
180
|
+
def opencode(
|
|
181
|
+
output: Annotated[str, typer.Argument(help="Output file")] = "commands.md",
|
|
182
|
+
) -> None:
|
|
183
|
+
"""Bundle OpenCode commands into commands.md."""
|
|
390
184
|
|
|
391
|
-
|
|
185
|
+
agent = get_agent("opencode")
|
|
186
|
+
cwd = Path.cwd()
|
|
392
187
|
|
|
393
|
-
|
|
394
|
-
|
|
188
|
+
commands_dir = agent.commands_dir
|
|
189
|
+
if not commands_dir:
|
|
190
|
+
log.error("opencode commands directory not configured")
|
|
395
191
|
raise typer.Exit(1)
|
|
396
192
|
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
193
|
+
log.info(
|
|
194
|
+
"bundling opencode commands",
|
|
195
|
+
commands_dir=commands_dir,
|
|
196
|
+
)
|
|
197
|
+
|
|
198
|
+
commands_path = cwd / commands_dir
|
|
199
|
+
if not commands_path.exists():
|
|
200
|
+
log.error(
|
|
201
|
+
"opencode commands directory not found", commands_dir=str(commands_path)
|
|
202
|
+
)
|
|
203
|
+
error_msg = f"OpenCode commands directory not found: {commands_path}"
|
|
204
|
+
typer.echo(typer.style(error_msg, fg=typer.colors.RED), err=True)
|
|
205
|
+
raise typer.Exit(1)
|
|
400
206
|
|
|
207
|
+
output_path = cwd / output
|
|
208
|
+
commands_written = agent.bundle_commands(output_path)
|
|
209
|
+
if commands_written:
|
|
210
|
+
success_msg = f"Bundled opencode commands into {output}"
|
|
211
|
+
typer.echo(typer.style(success_msg, fg=typer.colors.GREEN))
|
|
212
|
+
else:
|
|
213
|
+
output_path.unlink(missing_ok=True)
|
|
214
|
+
log.info("no opencode commands to bundle")
|