llm-ide-rules 0.3.0__tar.gz → 0.5.0__tar.gz

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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: llm-ide-rules
3
- Version: 0.3.0
3
+ Version: 0.5.0
4
4
  Summary: CLI tool for managing LLM IDE prompts and rules
5
5
  Keywords: llm,ide,prompts,cursor,copilot
6
6
  Author: Michael Bianco
@@ -62,6 +62,10 @@ uvx llm-ide-rules download [instruction_types] # Download everything by defau
62
62
  uvx llm-ide-rules download cursor github # Download specific types
63
63
  uvx llm-ide-rules download --repo other/repo # Download from different repo
64
64
 
65
+ # Delete downloaded instruction files
66
+ uvx llm-ide-rules delete [instruction_types] # Delete everything by default
67
+ uvx llm-ide-rules delete cursor gemini # Delete specific types
68
+ uvx llm-ide-rules delete --yes # Skip confirmation prompt
65
69
 
66
70
  ```
67
71
 
@@ -85,8 +89,28 @@ uvx llm-ide-rules download cursor github
85
89
 
86
90
  # Download from a different repository
87
91
  uvx llm-ide-rules download --repo other-user/other-repo --target ./my-project
92
+
93
+ # Delete all downloaded files (with confirmation)
94
+ uvx llm-ide-rules delete
95
+
96
+ # Delete specific instruction types
97
+ uvx llm-ide-rules delete cursor gemini --target ./my-project
98
+
99
+ # Delete without confirmation prompt
100
+ uvx llm-ide-rules delete --yes
88
101
  ```
89
102
 
103
+ ### IDE Command Format Comparison
104
+
105
+ Different AI coding assistants use different formats for commands:
106
+
107
+ | IDE | Directory | Format | Notes |
108
+ |-----|-----------|--------|-------|
109
+ | **Cursor** | `.cursor/commands/` | `.md` (plain markdown) | Simple, no frontmatter |
110
+ | **Claude Code** | `.claude/commands/` | `.md` (plain markdown) | Simple, no frontmatter |
111
+ | **GitHub Copilot** | `.github/prompts/` | `.prompt.md` (YAML + markdown) | Requires frontmatter with `mode: 'agent'` |
112
+ | **Gemini CLI** | `.gemini/commands/` | `.toml` | Uses TOML format, supports `{{args}}` and shell commands |
113
+
90
114
  ## Development
91
115
 
92
116
  ### Using the CLI for Development
@@ -48,6 +48,10 @@ uvx llm-ide-rules download [instruction_types] # Download everything by defau
48
48
  uvx llm-ide-rules download cursor github # Download specific types
49
49
  uvx llm-ide-rules download --repo other/repo # Download from different repo
50
50
 
51
+ # Delete downloaded instruction files
52
+ uvx llm-ide-rules delete [instruction_types] # Delete everything by default
53
+ uvx llm-ide-rules delete cursor gemini # Delete specific types
54
+ uvx llm-ide-rules delete --yes # Skip confirmation prompt
51
55
 
52
56
  ```
53
57
 
@@ -71,8 +75,28 @@ uvx llm-ide-rules download cursor github
71
75
 
72
76
  # Download from a different repository
73
77
  uvx llm-ide-rules download --repo other-user/other-repo --target ./my-project
78
+
79
+ # Delete all downloaded files (with confirmation)
80
+ uvx llm-ide-rules delete
81
+
82
+ # Delete specific instruction types
83
+ uvx llm-ide-rules delete cursor gemini --target ./my-project
84
+
85
+ # Delete without confirmation prompt
86
+ uvx llm-ide-rules delete --yes
74
87
  ```
75
88
 
89
+ ### IDE Command Format Comparison
90
+
91
+ Different AI coding assistants use different formats for commands:
92
+
93
+ | IDE | Directory | Format | Notes |
94
+ |-----|-----------|--------|-------|
95
+ | **Cursor** | `.cursor/commands/` | `.md` (plain markdown) | Simple, no frontmatter |
96
+ | **Claude Code** | `.claude/commands/` | `.md` (plain markdown) | Simple, no frontmatter |
97
+ | **GitHub Copilot** | `.github/prompts/` | `.prompt.md` (YAML + markdown) | Requires frontmatter with `mode: 'agent'` |
98
+ | **Gemini CLI** | `.gemini/commands/` | `.toml` | Uses TOML format, supports `{{args}}` and shell commands |
99
+
76
100
  ## Development
77
101
 
78
102
  ### Using the CLI for Development
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "llm-ide-rules"
3
- version = "0.3.0"
3
+ version = "0.5.0"
4
4
  description = "CLI tool for managing LLM IDE prompts and rules"
5
5
  keywords = ["llm", "ide", "prompts", "cursor", "copilot"]
6
6
  readme = "README.md"
@@ -4,10 +4,11 @@ import typer
4
4
  from typing_extensions import Annotated
5
5
 
6
6
  from llm_ide_rules.commands.explode import explode_main
7
- from llm_ide_rules.commands.implode import cursor, github
7
+ from llm_ide_rules.commands.implode import cursor, github, claude, gemini
8
8
  from llm_ide_rules.commands.download import download_main
9
+ from llm_ide_rules.commands.delete import delete_main
9
10
 
10
- __version__ = "0.3.0"
11
+ __version__ = "0.5.0"
11
12
 
12
13
  app = typer.Typer(
13
14
  name="llm_ide_rules",
@@ -18,11 +19,14 @@ app = typer.Typer(
18
19
  # Add commands directly
19
20
  app.command("explode", help="Convert instruction file to separate rule files")(explode_main)
20
21
  app.command("download", help="Download LLM instruction files from GitHub repositories")(download_main)
22
+ app.command("delete", help="Remove downloaded LLM instruction files")(delete_main)
21
23
 
22
24
  # Create implode sub-typer
23
25
  implode_app = typer.Typer(help="Bundle rule files into a single instruction file")
24
- implode_app.command("cursor", help="Bundle Cursor rules into a single file")(cursor)
25
- implode_app.command("github", help="Bundle GitHub/Copilot instructions into a single file")(github)
26
+ implode_app.command("cursor", help="Bundle Cursor rules and commands into a single file")(cursor)
27
+ implode_app.command("github", help="Bundle GitHub/Copilot instructions and prompts into a single file")(github)
28
+ implode_app.command("claude", help="Bundle Claude Code commands into a single file")(claude)
29
+ implode_app.command("gemini", help="Bundle Gemini CLI commands into a single file")(gemini)
26
30
  app.add_typer(implode_app, name="implode")
27
31
 
28
32
  def main():
@@ -0,0 +1,179 @@
1
+ """Delete command: Remove downloaded LLM instruction files."""
2
+
3
+ import logging
4
+ import shutil
5
+ from pathlib import Path
6
+ from typing import List
7
+
8
+ import structlog
9
+ import typer
10
+ from typing_extensions import Annotated
11
+
12
+ from llm_ide_rules.commands.download import INSTRUCTION_TYPES, DEFAULT_TYPES
13
+
14
+ logger = structlog.get_logger()
15
+
16
+
17
+ def find_files_to_delete(
18
+ instruction_types: List[str], target_dir: Path
19
+ ) -> tuple[List[Path], List[Path]]:
20
+ """Find all files and directories that would be deleted.
21
+
22
+ Returns:
23
+ Tuple of (directories, files) to delete
24
+ """
25
+ dirs_to_delete = []
26
+ files_to_delete = []
27
+
28
+ for inst_type in instruction_types:
29
+ if inst_type not in INSTRUCTION_TYPES:
30
+ logger.warning("Unknown instruction type", type=inst_type)
31
+ continue
32
+
33
+ config = INSTRUCTION_TYPES[inst_type]
34
+
35
+ for dir_name in config["directories"]:
36
+ dir_path = target_dir / dir_name
37
+ if dir_path.exists() and dir_path.is_dir():
38
+ dirs_to_delete.append(dir_path)
39
+
40
+ for file_name in config["files"]:
41
+ file_path = target_dir / file_name
42
+ if file_path.exists() and file_path.is_file():
43
+ files_to_delete.append(file_path)
44
+
45
+ for file_pattern in config.get("recursive_files", []):
46
+ matching_files = list(target_dir.rglob(file_pattern))
47
+ files_to_delete.extend([f for f in matching_files if f.is_file()])
48
+
49
+ return dirs_to_delete, files_to_delete
50
+
51
+
52
+ def delete_main(
53
+ instruction_types: Annotated[
54
+ List[str],
55
+ typer.Argument(
56
+ help="Types of instructions to delete (cursor, github, gemini, claude, agent, agents). Deletes everything by default."
57
+ ),
58
+ ] = None,
59
+ target_dir: Annotated[
60
+ str, typer.Option("--target", "-t", help="Target directory to delete from")
61
+ ] = ".",
62
+ yes: Annotated[
63
+ bool,
64
+ typer.Option("--yes", "-y", help="Skip confirmation prompt and delete immediately"),
65
+ ] = False,
66
+ verbose: Annotated[
67
+ bool, typer.Option("--verbose", "-v", help="Enable verbose logging")
68
+ ] = False,
69
+ ):
70
+ """Remove downloaded LLM instruction files.
71
+
72
+ This command removes files and directories that were downloaded by the 'download' command.
73
+ It will show you what will be deleted and ask for confirmation before proceeding.
74
+
75
+ Examples:
76
+
77
+ \b
78
+ # Delete everything (with confirmation)
79
+ llm_ide_rules delete
80
+
81
+ \b
82
+ # Delete only Cursor and Gemini files
83
+ llm_ide_rules delete cursor gemini
84
+
85
+ \b
86
+ # Delete without confirmation prompt
87
+ llm_ide_rules delete --yes
88
+
89
+ \b
90
+ # Delete from a specific directory
91
+ llm_ide_rules delete --target ./my-project
92
+ """
93
+ if verbose:
94
+ logging.basicConfig(level=logging.DEBUG)
95
+ structlog.configure(
96
+ wrapper_class=structlog.make_filtering_bound_logger(logging.DEBUG),
97
+ )
98
+
99
+ if not instruction_types:
100
+ instruction_types = DEFAULT_TYPES
101
+
102
+ invalid_types = [t for t in instruction_types if t not in INSTRUCTION_TYPES]
103
+ if invalid_types:
104
+ logger.error(
105
+ "Invalid instruction types",
106
+ invalid_types=invalid_types,
107
+ valid_types=list(INSTRUCTION_TYPES.keys()),
108
+ )
109
+ raise typer.Exit(1)
110
+
111
+ target_path = Path(target_dir).resolve()
112
+
113
+ if not target_path.exists():
114
+ logger.error("Target directory does not exist", target_dir=str(target_path))
115
+ typer.echo(f"Error: Target directory does not exist: {target_path}")
116
+ raise typer.Exit(1)
117
+
118
+ logger.info(
119
+ "Finding files to delete",
120
+ instruction_types=instruction_types,
121
+ target_dir=str(target_path),
122
+ )
123
+
124
+ dirs_to_delete, files_to_delete = find_files_to_delete(
125
+ instruction_types, target_path
126
+ )
127
+
128
+ if not dirs_to_delete and not files_to_delete:
129
+ logger.info("No files found to delete")
130
+ typer.echo("No matching instruction files found to delete.")
131
+ return
132
+
133
+ typer.echo("\nThe following files and directories will be deleted:\n")
134
+
135
+ if dirs_to_delete:
136
+ typer.echo("Directories:")
137
+ for dir_path in sorted(dirs_to_delete):
138
+ relative_path = dir_path.relative_to(target_path)
139
+ typer.echo(f" - {relative_path}/")
140
+
141
+ if files_to_delete:
142
+ typer.echo("\nFiles:")
143
+ for file_path in sorted(files_to_delete):
144
+ relative_path = file_path.relative_to(target_path)
145
+ typer.echo(f" - {relative_path}")
146
+
147
+ total_items = len(dirs_to_delete) + len(files_to_delete)
148
+ typer.echo(f"\nTotal: {total_items} items")
149
+
150
+ if not yes:
151
+ typer.echo()
152
+ confirm = typer.confirm("Are you sure you want to delete these files?")
153
+ if not confirm:
154
+ logger.info("Deletion cancelled by user")
155
+ typer.echo("Deletion cancelled.")
156
+ raise typer.Exit(0)
157
+
158
+ deleted_count = 0
159
+
160
+ for dir_path in dirs_to_delete:
161
+ try:
162
+ logger.info("Deleting directory", path=str(dir_path))
163
+ shutil.rmtree(dir_path)
164
+ deleted_count += 1
165
+ except Exception as e:
166
+ logger.error("Failed to delete directory", path=str(dir_path), error=str(e))
167
+ typer.echo(f"Error deleting {dir_path}: {e}", err=True)
168
+
169
+ for file_path in files_to_delete:
170
+ try:
171
+ logger.info("Deleting file", path=str(file_path))
172
+ file_path.unlink()
173
+ deleted_count += 1
174
+ except Exception as e:
175
+ logger.error("Failed to delete file", path=str(file_path), error=str(e))
176
+ typer.echo(f"Error deleting {file_path}: {e}", err=True)
177
+
178
+ logger.info("Deletion completed", deleted_count=deleted_count, total_items=total_items)
179
+ typer.echo(f"\nSuccessfully deleted {deleted_count} of {total_items} items.")
@@ -192,8 +192,87 @@ description: '{description}'
192
192
  f.write(line)
193
193
 
194
194
 
195
- def process_unmapped_section(lines, section_name, rules_dir, github_prompts_dir):
196
- """Process an unmapped section as a manually applied rule (prompt)."""
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
+
199
+ trimmed = trim_content(content_lines)
200
+
201
+ # Strip the header from content (first line starting with ##)
202
+ filtered_content = []
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)
209
+
210
+ # Trim again after removing header
211
+ filtered_content = trim_content(filtered_content)
212
+
213
+ with open(filepath, "w") as f:
214
+ for line in filtered_content:
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)
232
+
233
+ # Trim again after removing header
234
+ filtered_content = trim_content(filtered_content)
235
+
236
+ with open(filepath, "w") as f:
237
+ for line in filtered_content:
238
+ f.write(line)
239
+
240
+
241
+ def write_gemini_command(content_lines, filename, commands_dir, section_name=None):
242
+ """Write a Gemini CLI command file (TOML format)."""
243
+ filepath = os.path.join(commands_dir, filename + ".toml")
244
+
245
+ description, filtered_content = extract_description_and_filter_content(
246
+ content_lines, ""
247
+ )
248
+
249
+ # Strip the header from content (first line starting with ##)
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)."""
197
276
  section_content = extract_section(lines, f"## {section_name}")
198
277
  if any(line.strip() for line in section_content):
199
278
  filename = header_to_filename(section_name)
@@ -203,9 +282,11 @@ def process_unmapped_section(lines, section_name, rules_dir, github_prompts_dir)
203
282
  section_content, section_name
204
283
  )
205
284
 
206
- # Create prompt files (same as None case in SECTION_GLOBS)
207
- write_cursor_prompt(section_content, filename, rules_dir, section_name)
285
+ # Create command files (same as None case in SECTION_GLOBS)
286
+ write_cursor_command(section_content, filename, cursor_commands_dir, section_name)
208
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)
209
290
  return True
210
291
  return False
211
292
 
@@ -235,12 +316,18 @@ def explode_main(
235
316
 
236
317
  # Work in current directory ($PWD)
237
318
  rules_dir = os.path.join(os.getcwd(), ".cursor", "rules")
319
+ cursor_commands_dir = os.path.join(os.getcwd(), ".cursor", "commands")
238
320
  copilot_dir = os.path.join(os.getcwd(), ".github", "instructions")
239
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")
240
324
 
241
325
  os.makedirs(rules_dir, exist_ok=True)
326
+ os.makedirs(cursor_commands_dir, exist_ok=True)
242
327
  os.makedirs(copilot_dir, exist_ok=True)
243
328
  os.makedirs(github_prompts_dir, exist_ok=True)
329
+ os.makedirs(claude_commands_dir, exist_ok=True)
330
+ os.makedirs(gemini_commands_dir, exist_ok=True)
244
331
 
245
332
  input_path = os.path.join(os.getcwd(), input_file)
246
333
 
@@ -295,18 +382,20 @@ alwaysApply: true
295
382
  section_content,
296
383
  )
297
384
  else:
298
- # It's a prompt - create prompt files using the original section name for header
299
- write_cursor_prompt(section_content, filename, rules_dir, section_name)
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)
300
387
  write_github_prompt(
301
388
  section_content, filename, github_prompts_dir, section_name
302
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)
303
392
 
304
393
  # Check for sections in mapping that don't exist in the file
305
394
  for section_name in SECTION_GLOBS:
306
395
  if section_name not in found_sections:
307
396
  logger.warning("Section not found in file", section=section_name)
308
397
 
309
- # Process unmapped sections as manually applied rules (prompts)
398
+ # Process unmapped sections as manually applied rules (commands)
310
399
  processed_unmapped = set()
311
400
  for line in lines:
312
401
  if line.startswith("## "):
@@ -321,16 +410,19 @@ alwaysApply: true
321
410
  and section_name not in processed_unmapped
322
411
  ):
323
412
  if process_unmapped_section(
324
- lines, section_name, rules_dir, github_prompts_dir
413
+ lines, section_name, cursor_commands_dir, github_prompts_dir, claude_commands_dir, gemini_commands_dir
325
414
  ):
326
415
  processed_unmapped.add(section_name)
327
416
 
328
417
  logger.info(
329
418
  "Explode operation completed",
330
419
  cursor_rules=rules_dir,
420
+ cursor_commands=cursor_commands_dir,
331
421
  copilot_instructions=copilot_dir,
332
422
  github_prompts=github_prompts_dir,
423
+ claude_commands=claude_commands_dir,
424
+ gemini_commands=gemini_commands_dir,
333
425
  )
334
426
  typer.echo(
335
- "Created Cursor rules in .cursor/rules/, Copilot instructions in .github/instructions/, and prompts in respective directories"
427
+ "Created rules and commands in .cursor/, .claude/, .github/, and .gemini/ directories"
336
428
  )
@@ -0,0 +1,400 @@
1
+ """Implode command: Bundle rule files into a single instruction file."""
2
+
3
+ import os
4
+ from pathlib import Path
5
+ from typing_extensions import Annotated
6
+ import logging
7
+
8
+ 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
+
283
+
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
+
299
+ rules_dir = os.path.join(os.getcwd(), ".cursor", "rules")
300
+ commands_dir = os.path.join(os.getcwd(), ".cursor", "commands")
301
+ output_path = os.path.join(os.getcwd(), output)
302
+
303
+ logger.info("Bundling Cursor rules and commands", rules_dir=rules_dir, commands_dir=commands_dir, output_file=output_path, config=config)
304
+
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
+
309
+ bundle_cursor_rules(rules_dir, commands_dir, output_path, SECTION_GLOBS)
310
+ logger.info("Cursor rules and commands bundled successfully", output_file=output_path)
311
+ typer.echo(f"Bundled cursor rules and commands into {output}")
312
+
313
+
314
+ def github(
315
+ output: Annotated[str, typer.Argument(help="Output file")] = "instructions.md",
316
+ verbose: Annotated[bool, typer.Option("--verbose", "-v", help="Enable verbose logging")] = False,
317
+ config: Annotated[str, typer.Option("--config", "-c", help="Custom configuration file path")] = None,
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),
324
+ )
325
+
326
+ # Load section globs (with optional custom config)
327
+ SECTION_GLOBS = load_section_globs(config)
328
+
329
+ instructions_dir = os.path.join(os.getcwd(), ".github", "instructions")
330
+ prompts_dir = os.path.join(os.getcwd(), ".github", "prompts")
331
+ output_path = os.path.join(os.getcwd(), output)
332
+
333
+ logger.info("Bundling GitHub instructions and prompts", instructions_dir=instructions_dir, prompts_dir=prompts_dir, output_file=output_path, config=config)
334
+
335
+ if not Path(instructions_dir).exists():
336
+ logger.error("GitHub instructions directory not found", instructions_dir=instructions_dir)
337
+ raise typer.Exit(1)
338
+
339
+ bundle_github_instructions(instructions_dir, prompts_dir, output_path, SECTION_GLOBS)
340
+ logger.info("GitHub instructions and prompts bundled successfully", output_file=output_path)
341
+ typer.echo(f"Bundled github instructions and prompts into {output}")
342
+
343
+
344
+ def claude(
345
+ output: Annotated[str, typer.Argument(help="Output file")] = "instructions.md",
346
+ verbose: Annotated[bool, typer.Option("--verbose", "-v", help="Enable verbose logging")] = False,
347
+ config: Annotated[str, typer.Option("--config", "-c", help="Custom configuration file path")] = None,
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),
354
+ )
355
+
356
+ # Load section globs (with optional custom config)
357
+ SECTION_GLOBS = load_section_globs(config)
358
+
359
+ commands_dir = os.path.join(os.getcwd(), ".claude", "commands")
360
+ output_path = os.path.join(os.getcwd(), output)
361
+
362
+ logger.info("Bundling Claude Code commands", commands_dir=commands_dir, output_file=output_path, config=config)
363
+
364
+ if not Path(commands_dir).exists():
365
+ logger.error("Claude Code commands directory not found", commands_dir=commands_dir)
366
+ raise typer.Exit(1)
367
+
368
+ bundle_claude_commands(commands_dir, output_path, SECTION_GLOBS)
369
+ logger.info("Claude Code commands bundled successfully", output_file=output_path)
370
+ typer.echo(f"Bundled claude commands into {output}")
371
+
372
+
373
+ def gemini(
374
+ output: Annotated[str, typer.Argument(help="Output file")] = "instructions.md",
375
+ verbose: Annotated[bool, typer.Option("--verbose", "-v", help="Enable verbose logging")] = False,
376
+ config: Annotated[str, typer.Option("--config", "-c", help="Custom configuration file path")] = None,
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),
383
+ )
384
+
385
+ # Load section globs (with optional custom config)
386
+ SECTION_GLOBS = load_section_globs(config)
387
+
388
+ commands_dir = os.path.join(os.getcwd(), ".gemini", "commands")
389
+ output_path = os.path.join(os.getcwd(), output)
390
+
391
+ logger.info("Bundling Gemini CLI commands", commands_dir=commands_dir, output_file=output_path, config=config)
392
+
393
+ if not Path(commands_dir).exists():
394
+ logger.error("Gemini CLI commands directory not found", commands_dir=commands_dir)
395
+ raise typer.Exit(1)
396
+
397
+ bundle_gemini_commands(commands_dir, output_path, SECTION_GLOBS)
398
+ logger.info("Gemini CLI commands bundled successfully", output_file=output_path)
399
+ typer.echo(f"Bundled gemini commands into {output}")
400
+
@@ -13,6 +13,15 @@
13
13
  "Shell": "**/*.sh",
14
14
  "TypeScript": "**/*.ts,**/*.tsx",
15
15
  "TypeScript DocString": null,
16
- "Secrets": null
16
+ "Secrets": null,
17
+ "Dev In Browser": null,
18
+ "Fastapi Stripe": null,
19
+ "Fix Tests": null,
20
+ "Implement Fastapi Routes": null,
21
+ "Plan Only": null,
22
+ "Python Command": null,
23
+ "Refactor On Instructions": null,
24
+ "Standalone Python Scripts": null,
25
+ "Stripe Backend": null
17
26
  }
18
27
  }
@@ -1,218 +0,0 @@
1
- """Implode command: Bundle rule files into a single instruction file."""
2
-
3
- import os
4
- from pathlib import Path
5
- from typing_extensions import Annotated
6
- import logging
7
-
8
- 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, output_file, section_globs):
61
- """Bundle Cursor rule files into a single file."""
62
- rule_files = list(Path(rules_dir).glob("*.mdc"))
63
- general = [f for f in rule_files if f.stem == "general"]
64
- others = [f for f in rule_files if f.stem != "general"]
65
-
66
- # Order the non-general files based on section_globs
67
- ordered_others = get_ordered_files(others, section_globs.keys())
68
- ordered = general + ordered_others
69
-
70
- def resolve_header_from_stem(stem):
71
- """Return the canonical header for a given filename stem.
72
-
73
- Prefer exact header names from section_globs (preserves acronyms like FastAPI, TypeScript).
74
- Fallback to title-casing the filename when not found in section_globs.
75
- """
76
- for section_name in section_globs.keys():
77
- if header_to_filename(section_name) == stem:
78
- return section_name
79
- return filename_to_header(stem)
80
-
81
- with open(output_file, "w") as out:
82
- for rule_file in ordered:
83
- with open(rule_file, "r") as f:
84
- content = f.read().strip()
85
- if not content:
86
- continue
87
- content = strip_yaml_frontmatter(content)
88
- content = strip_header(content)
89
- # Use canonical header names from SECTION_GLOBS when available
90
- header = resolve_header_from_stem(rule_file.stem)
91
- if rule_file.stem != "general":
92
- out.write(f"## {header}\n\n")
93
- out.write(content)
94
- out.write("\n\n")
95
-
96
-
97
- def strip_yaml_frontmatter(text):
98
- """Strip YAML frontmatter from text."""
99
- lines = text.splitlines()
100
- if lines and lines[0].strip() == "---":
101
- # Find the next '---' after the first
102
- for i in range(1, len(lines)):
103
- if lines[i].strip() == "---":
104
- return "\n".join(lines[i + 1 :]).lstrip("\n")
105
- return text
106
-
107
-
108
- def strip_header(text):
109
- """Remove the first markdown header (## Header) from text if present."""
110
- lines = text.splitlines()
111
- if lines and lines[0].startswith("## "):
112
- # Remove the header line and any immediately following empty lines
113
- remaining_lines = lines[1:]
114
- while remaining_lines and not remaining_lines[0].strip():
115
- remaining_lines = remaining_lines[1:]
116
- return "\n".join(remaining_lines)
117
- return text
118
-
119
-
120
- def bundle_github_instructions(instructions_dir, output_file, section_globs):
121
- """Bundle GitHub instruction files into a single file."""
122
- copilot_general = Path(os.getcwd()) / ".github" / "copilot-instructions.md"
123
- instr_files = list(Path(instructions_dir).glob("*.instructions.md"))
124
-
125
- # Order the instruction files based on section_globs
126
- # We need to create a modified version that strips .instructions from stems for ordering
127
- ordered_files = get_ordered_files_github(instr_files, section_globs.keys())
128
-
129
- def resolve_header_from_stem(stem):
130
- """Return the canonical header for a given filename stem.
131
-
132
- Prefer exact header names from section_globs (preserves acronyms like FastAPI, TypeScript).
133
- Fallback to title-casing the filename when not found in section_globs.
134
- """
135
- for section_name in section_globs.keys():
136
- if header_to_filename(section_name) == stem:
137
- return section_name
138
- return filename_to_header(stem)
139
-
140
- with open(output_file, "w") as out:
141
- # Write general copilot instructions if present
142
- if copilot_general.exists():
143
- content = copilot_general.read_text().strip()
144
- if content:
145
- out.write(content)
146
- out.write("\n\n")
147
- for instr_file in ordered_files:
148
- with open(instr_file, "r") as f:
149
- content = f.read().strip()
150
- if not content:
151
- continue
152
- content = strip_yaml_frontmatter(content)
153
- content = strip_header(content)
154
- # Use canonical header names from SECTION_GLOBS when available
155
- base_stem = instr_file.stem.replace(".instructions", "")
156
- header = resolve_header_from_stem(base_stem)
157
- out.write(f"## {header}\n\n")
158
- out.write(content)
159
- out.write("\n\n")
160
-
161
-
162
- def cursor(
163
- output: Annotated[str, typer.Argument(help="Output file")] = "instructions.md",
164
- verbose: Annotated[bool, typer.Option("--verbose", "-v", help="Enable verbose logging")] = False,
165
- config: Annotated[str, typer.Option("--config", "-c", help="Custom configuration file path")] = None,
166
- ):
167
- """Bundle Cursor rules into a single file."""
168
- if verbose:
169
- logging.basicConfig(level=logging.DEBUG)
170
- structlog.configure(
171
- wrapper_class=structlog.make_filtering_bound_logger(logging.DEBUG),
172
- )
173
-
174
- # Load section globs (with optional custom config)
175
- SECTION_GLOBS = load_section_globs(config)
176
-
177
- rules_dir = os.path.join(os.getcwd(), ".cursor", "rules")
178
- output_path = os.path.join(os.getcwd(), output)
179
-
180
- logger.info("Bundling Cursor rules", rules_dir=rules_dir, output_file=output_path, config=config)
181
-
182
- if not Path(rules_dir).exists():
183
- logger.error("Cursor rules directory not found", rules_dir=rules_dir)
184
- raise typer.Exit(1)
185
-
186
- bundle_cursor_rules(rules_dir, output_path, SECTION_GLOBS)
187
- logger.info("Cursor rules bundled successfully", output_file=output_path)
188
- typer.echo(f"Bundled cursor rules into {output}")
189
-
190
-
191
- def github(
192
- output: Annotated[str, typer.Argument(help="Output file")] = "instructions.md",
193
- verbose: Annotated[bool, typer.Option("--verbose", "-v", help="Enable verbose logging")] = False,
194
- config: Annotated[str, typer.Option("--config", "-c", help="Custom configuration file path")] = None,
195
- ):
196
- """Bundle GitHub/Copilot instructions into a single file."""
197
- if verbose:
198
- logging.basicConfig(level=logging.DEBUG)
199
- structlog.configure(
200
- wrapper_class=structlog.make_filtering_bound_logger(logging.DEBUG),
201
- )
202
-
203
- # Load section globs (with optional custom config)
204
- SECTION_GLOBS = load_section_globs(config)
205
-
206
- instructions_dir = os.path.join(os.getcwd(), ".github", "instructions")
207
- output_path = os.path.join(os.getcwd(), output)
208
-
209
- logger.info("Bundling GitHub instructions", instructions_dir=instructions_dir, output_file=output_path, config=config)
210
-
211
- if not Path(instructions_dir).exists():
212
- logger.error("GitHub instructions directory not found", instructions_dir=instructions_dir)
213
- raise typer.Exit(1)
214
-
215
- bundle_github_instructions(instructions_dir, output_path, SECTION_GLOBS)
216
- logger.info("GitHub instructions bundled successfully", output_file=output_path)
217
- typer.echo(f"Bundled github instructions into {output}")
218
-