llm-ide-rules 0.4.0__py3-none-any.whl → 0.6.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- llm_ide_rules/__init__.py +53 -7
- llm_ide_rules/__main__.py +1 -1
- llm_ide_rules/agents/__init__.py +28 -0
- llm_ide_rules/agents/base.py +283 -0
- llm_ide_rules/agents/claude.py +92 -0
- llm_ide_rules/agents/cursor.py +178 -0
- llm_ide_rules/agents/gemini.py +161 -0
- llm_ide_rules/agents/github.py +207 -0
- llm_ide_rules/agents/opencode.py +126 -0
- llm_ide_rules/commands/delete.py +24 -34
- llm_ide_rules/commands/download.py +52 -56
- llm_ide_rules/commands/explode.py +227 -243
- llm_ide_rules/commands/implode.py +229 -202
- llm_ide_rules/commands/mcp.py +119 -0
- llm_ide_rules/constants.py +17 -14
- llm_ide_rules/log.py +9 -0
- llm_ide_rules/mcp/__init__.py +7 -0
- llm_ide_rules/mcp/models.py +21 -0
- llm_ide_rules/sections.json +4 -5
- {llm_ide_rules-0.4.0.dist-info → llm_ide_rules-0.6.0.dist-info}/METADATA +35 -59
- llm_ide_rules-0.6.0.dist-info/RECORD +23 -0
- {llm_ide_rules-0.4.0.dist-info → llm_ide_rules-0.6.0.dist-info}/WHEEL +2 -2
- llm_ide_rules-0.4.0.dist-info/RECORD +0 -12
- {llm_ide_rules-0.4.0.dist-info → llm_ide_rules-0.6.0.dist-info}/entry_points.txt +0 -0
|
@@ -1,18 +1,15 @@
|
|
|
1
1
|
"""Download command: Download LLM instruction files from GitHub repositories."""
|
|
2
2
|
|
|
3
|
-
import logging
|
|
4
3
|
import re
|
|
5
4
|
import tempfile
|
|
6
5
|
import zipfile
|
|
7
6
|
from pathlib import Path
|
|
8
|
-
from typing import List
|
|
9
7
|
|
|
10
8
|
import requests
|
|
11
|
-
import structlog
|
|
12
9
|
import typer
|
|
13
10
|
from typing_extensions import Annotated
|
|
14
11
|
|
|
15
|
-
|
|
12
|
+
from llm_ide_rules.log import log
|
|
16
13
|
|
|
17
14
|
DEFAULT_REPO = "iloveitaly/llm-ide-rules"
|
|
18
15
|
DEFAULT_BRANCH = "master"
|
|
@@ -20,7 +17,7 @@ DEFAULT_BRANCH = "master"
|
|
|
20
17
|
|
|
21
18
|
def normalize_repo(repo: str) -> str:
|
|
22
19
|
"""Normalize repository input to user/repo format.
|
|
23
|
-
|
|
20
|
+
|
|
24
21
|
Handles both formats:
|
|
25
22
|
- user/repo (unchanged)
|
|
26
23
|
- https://github.com/user/repo/ (extracts user/repo)
|
|
@@ -28,17 +25,18 @@ def normalize_repo(repo: str) -> str:
|
|
|
28
25
|
# If it's already in user/repo format, return as-is
|
|
29
26
|
if "/" in repo and not repo.startswith("http"):
|
|
30
27
|
return repo
|
|
31
|
-
|
|
28
|
+
|
|
32
29
|
# Extract user/repo from GitHub URL
|
|
33
30
|
github_pattern = r"https?://github\.com/([^/]+/[^/]+)/?.*"
|
|
34
31
|
match = re.match(github_pattern, repo)
|
|
35
|
-
|
|
32
|
+
|
|
36
33
|
if match:
|
|
37
34
|
return match.group(1)
|
|
38
|
-
|
|
35
|
+
|
|
39
36
|
# If no pattern matches, assume it's already in the correct format
|
|
40
37
|
return repo
|
|
41
38
|
|
|
39
|
+
|
|
42
40
|
# Define what files/directories each instruction type includes
|
|
43
41
|
INSTRUCTION_TYPES = {
|
|
44
42
|
"cursor": {"directories": [".cursor"], "files": []},
|
|
@@ -47,8 +45,8 @@ INSTRUCTION_TYPES = {
|
|
|
47
45
|
"files": [],
|
|
48
46
|
"exclude_patterns": ["workflows/*"],
|
|
49
47
|
},
|
|
50
|
-
"gemini": {"directories": [], "files": ["GEMINI.md"]},
|
|
51
|
-
"claude": {"directories": [], "files": ["CLAUDE.md"]},
|
|
48
|
+
"gemini": {"directories": [".gemini/commands"], "files": ["GEMINI.md"]},
|
|
49
|
+
"claude": {"directories": [".claude/commands"], "files": ["CLAUDE.md"]},
|
|
52
50
|
"agent": {"directories": [], "files": ["AGENT.md"]},
|
|
53
51
|
"agents": {"directories": [], "files": [], "recursive_files": ["AGENTS.md"]},
|
|
54
52
|
}
|
|
@@ -62,13 +60,19 @@ def download_and_extract_repo(repo: str, branch: str = DEFAULT_BRANCH) -> Path:
|
|
|
62
60
|
normalized_repo = normalize_repo(repo)
|
|
63
61
|
zip_url = f"https://github.com/{normalized_repo}/archive/{branch}.zip"
|
|
64
62
|
|
|
65
|
-
|
|
63
|
+
log.info(
|
|
64
|
+
"downloading repository",
|
|
65
|
+
repo=repo,
|
|
66
|
+
normalized_repo=normalized_repo,
|
|
67
|
+
branch=branch,
|
|
68
|
+
url=zip_url,
|
|
69
|
+
)
|
|
66
70
|
|
|
67
71
|
try:
|
|
68
72
|
response = requests.get(zip_url, timeout=30)
|
|
69
73
|
response.raise_for_status()
|
|
70
74
|
except requests.RequestException as e:
|
|
71
|
-
|
|
75
|
+
log.error("failed to download repository", error=str(e), url=zip_url)
|
|
72
76
|
raise typer.Exit(1)
|
|
73
77
|
|
|
74
78
|
# Create temporary directory and file
|
|
@@ -88,24 +92,24 @@ def download_and_extract_repo(repo: str, branch: str = DEFAULT_BRANCH) -> Path:
|
|
|
88
92
|
# Find the extracted repository directory (should be the only directory)
|
|
89
93
|
repo_dirs = [d for d in extract_dir.iterdir() if d.is_dir()]
|
|
90
94
|
if not repo_dirs:
|
|
91
|
-
|
|
95
|
+
log.error("no directories found in extracted zip")
|
|
92
96
|
raise typer.Exit(1)
|
|
93
97
|
|
|
94
98
|
repo_dir = repo_dirs[0]
|
|
95
|
-
|
|
99
|
+
log.info("repository extracted", path=str(repo_dir))
|
|
96
100
|
|
|
97
101
|
return repo_dir
|
|
98
102
|
|
|
99
103
|
|
|
100
104
|
def copy_instruction_files(
|
|
101
|
-
repo_dir: Path, instruction_types:
|
|
105
|
+
repo_dir: Path, instruction_types: list[str], target_dir: Path
|
|
102
106
|
):
|
|
103
107
|
"""Copy instruction files from the repository to the target directory."""
|
|
104
108
|
copied_items = []
|
|
105
109
|
|
|
106
110
|
for inst_type in instruction_types:
|
|
107
111
|
if inst_type not in INSTRUCTION_TYPES:
|
|
108
|
-
|
|
112
|
+
log.warning("unknown instruction type", type=inst_type)
|
|
109
113
|
continue
|
|
110
114
|
|
|
111
115
|
config = INSTRUCTION_TYPES[inst_type]
|
|
@@ -116,8 +120,8 @@ def copy_instruction_files(
|
|
|
116
120
|
target_subdir = target_dir / dir_name
|
|
117
121
|
|
|
118
122
|
if source_dir.exists():
|
|
119
|
-
|
|
120
|
-
"
|
|
123
|
+
log.info(
|
|
124
|
+
"copying directory",
|
|
121
125
|
source=str(source_dir),
|
|
122
126
|
target=str(target_subdir),
|
|
123
127
|
)
|
|
@@ -137,8 +141,8 @@ def copy_instruction_files(
|
|
|
137
141
|
target_file = target_dir / file_name
|
|
138
142
|
|
|
139
143
|
if source_file.exists():
|
|
140
|
-
|
|
141
|
-
"
|
|
144
|
+
log.info(
|
|
145
|
+
"copying file", source=str(source_file), target=str(target_file)
|
|
142
146
|
)
|
|
143
147
|
|
|
144
148
|
# Create parent directories if needed
|
|
@@ -158,55 +162,53 @@ def copy_instruction_files(
|
|
|
158
162
|
|
|
159
163
|
def copy_recursive_files(
|
|
160
164
|
repo_dir: Path, target_dir: Path, file_pattern: str
|
|
161
|
-
) ->
|
|
165
|
+
) -> list[str]:
|
|
162
166
|
"""Recursively copy files matching pattern, preserving directory structure.
|
|
163
|
-
|
|
167
|
+
|
|
164
168
|
Only copies files to locations where the target directory already exists.
|
|
165
169
|
Warns and skips files where target directories don't exist.
|
|
166
|
-
|
|
170
|
+
|
|
167
171
|
Args:
|
|
168
172
|
repo_dir: Source repository directory
|
|
169
173
|
target_dir: Target directory to copy to
|
|
170
174
|
file_pattern: File pattern to search for (e.g., "AGENTS.md")
|
|
171
|
-
|
|
175
|
+
|
|
172
176
|
Returns:
|
|
173
177
|
List of copied file paths relative to target_dir
|
|
174
178
|
"""
|
|
175
179
|
copied_items = []
|
|
176
|
-
|
|
180
|
+
|
|
177
181
|
# Find all matching files recursively
|
|
178
182
|
matching_files = list(repo_dir.rglob(file_pattern))
|
|
179
|
-
|
|
183
|
+
|
|
180
184
|
for source_file in matching_files:
|
|
181
185
|
# Calculate relative path from repo root
|
|
182
186
|
relative_path = source_file.relative_to(repo_dir)
|
|
183
187
|
target_file = target_dir / relative_path
|
|
184
|
-
|
|
188
|
+
|
|
185
189
|
# Check if target directory already exists
|
|
186
190
|
target_parent = target_file.parent
|
|
187
191
|
if not target_parent.exists():
|
|
188
|
-
|
|
189
|
-
"
|
|
192
|
+
log.warning(
|
|
193
|
+
"target directory does not exist, skipping file copy",
|
|
190
194
|
target_directory=str(target_parent),
|
|
191
|
-
file=str(relative_path)
|
|
195
|
+
file=str(relative_path),
|
|
192
196
|
)
|
|
193
197
|
continue
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
"
|
|
197
|
-
source=str(source_file),
|
|
198
|
-
target=str(target_file)
|
|
198
|
+
|
|
199
|
+
log.info(
|
|
200
|
+
"copying recursive file", source=str(source_file), target=str(target_file)
|
|
199
201
|
)
|
|
200
|
-
|
|
202
|
+
|
|
201
203
|
# Copy file (parent directory already exists)
|
|
202
204
|
target_file.write_bytes(source_file.read_bytes())
|
|
203
205
|
copied_items.append(str(relative_path))
|
|
204
|
-
|
|
206
|
+
|
|
205
207
|
return copied_items
|
|
206
208
|
|
|
207
209
|
|
|
208
210
|
def copy_directory_contents(
|
|
209
|
-
source_dir: Path, target_dir: Path, exclude_patterns:
|
|
211
|
+
source_dir: Path, target_dir: Path, exclude_patterns: list[str]
|
|
210
212
|
):
|
|
211
213
|
"""Recursively copy directory contents, excluding specified patterns."""
|
|
212
214
|
for item in source_dir.rglob("*"):
|
|
@@ -216,6 +218,7 @@ def copy_directory_contents(
|
|
|
216
218
|
|
|
217
219
|
# Check if file matches any exclude pattern
|
|
218
220
|
should_exclude = False
|
|
221
|
+
pattern = ""
|
|
219
222
|
for pattern in exclude_patterns:
|
|
220
223
|
if pattern.endswith("/*"):
|
|
221
224
|
# Pattern like "workflows/*" - exclude if path starts with "workflows/"
|
|
@@ -228,7 +231,7 @@ def copy_directory_contents(
|
|
|
228
231
|
break
|
|
229
232
|
|
|
230
233
|
if should_exclude:
|
|
231
|
-
|
|
234
|
+
log.debug("excluding file", file=relative_str, pattern=pattern)
|
|
232
235
|
continue
|
|
233
236
|
|
|
234
237
|
target_file = target_dir / relative_path
|
|
@@ -238,7 +241,7 @@ def copy_directory_contents(
|
|
|
238
241
|
|
|
239
242
|
def download_main(
|
|
240
243
|
instruction_types: Annotated[
|
|
241
|
-
|
|
244
|
+
list[str] | None,
|
|
242
245
|
typer.Argument(
|
|
243
246
|
help="Types of instructions to download (cursor, github, gemini, claude, agent, agents). Downloads everything by default."
|
|
244
247
|
),
|
|
@@ -252,9 +255,6 @@ def download_main(
|
|
|
252
255
|
target_dir: Annotated[
|
|
253
256
|
str, typer.Option("--target", "-t", help="Target directory to download to")
|
|
254
257
|
] = ".",
|
|
255
|
-
verbose: Annotated[
|
|
256
|
-
bool, typer.Option("--verbose", "-v", help="Enable verbose logging")
|
|
257
|
-
] = False,
|
|
258
258
|
):
|
|
259
259
|
"""Download LLM instruction files from GitHub repositories.
|
|
260
260
|
|
|
@@ -279,12 +279,6 @@ def download_main(
|
|
|
279
279
|
# Download to a specific directory
|
|
280
280
|
llm_ide_rules download --target ./my-project
|
|
281
281
|
"""
|
|
282
|
-
if verbose:
|
|
283
|
-
logging.basicConfig(level=logging.DEBUG)
|
|
284
|
-
structlog.configure(
|
|
285
|
-
wrapper_class=structlog.make_filtering_bound_logger(logging.DEBUG),
|
|
286
|
-
)
|
|
287
|
-
|
|
288
282
|
# Use default types if none specified
|
|
289
283
|
if not instruction_types:
|
|
290
284
|
instruction_types = DEFAULT_TYPES
|
|
@@ -292,17 +286,19 @@ def download_main(
|
|
|
292
286
|
# Validate instruction types
|
|
293
287
|
invalid_types = [t for t in instruction_types if t not in INSTRUCTION_TYPES]
|
|
294
288
|
if invalid_types:
|
|
295
|
-
|
|
296
|
-
"
|
|
289
|
+
log.error(
|
|
290
|
+
"invalid instruction types",
|
|
297
291
|
invalid_types=invalid_types,
|
|
298
292
|
valid_types=list(INSTRUCTION_TYPES.keys()),
|
|
299
293
|
)
|
|
294
|
+
error_msg = f"Invalid instruction types: {', '.join(invalid_types)}"
|
|
295
|
+
typer.echo(typer.style(error_msg, fg=typer.colors.RED), err=True)
|
|
300
296
|
raise typer.Exit(1)
|
|
301
297
|
|
|
302
298
|
target_path = Path(target_dir).resolve()
|
|
303
299
|
|
|
304
|
-
|
|
305
|
-
"
|
|
300
|
+
log.info(
|
|
301
|
+
"starting download",
|
|
306
302
|
repo=repo,
|
|
307
303
|
branch=branch,
|
|
308
304
|
instruction_types=instruction_types,
|
|
@@ -317,12 +313,12 @@ def download_main(
|
|
|
317
313
|
copied_items = copy_instruction_files(repo_dir, instruction_types, target_path)
|
|
318
314
|
|
|
319
315
|
if copied_items:
|
|
320
|
-
|
|
321
|
-
typer.echo(
|
|
316
|
+
success_msg = f"Downloaded {len(copied_items)} items to {target_path}:"
|
|
317
|
+
typer.echo(typer.style(success_msg, fg=typer.colors.GREEN))
|
|
322
318
|
for item in copied_items:
|
|
323
319
|
typer.echo(f" - {item}")
|
|
324
320
|
else:
|
|
325
|
-
|
|
321
|
+
log.warning("no files were copied")
|
|
326
322
|
typer.echo("No matching instruction files found in the repository.")
|
|
327
323
|
|
|
328
324
|
finally:
|