llm-ide-rules 0.6.0__py3-none-any.whl → 0.8.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 +20 -1
- llm_ide_rules/agents/__init__.py +4 -0
- llm_ide_rules/agents/agents.py +124 -0
- llm_ide_rules/agents/base.py +63 -6
- llm_ide_rules/agents/claude.py +31 -4
- llm_ide_rules/agents/cursor.py +64 -14
- llm_ide_rules/agents/gemini.py +44 -4
- llm_ide_rules/agents/github.py +49 -13
- llm_ide_rules/agents/opencode.py +14 -4
- llm_ide_rules/agents/vscode.py +88 -0
- llm_ide_rules/commands/config.py +46 -0
- llm_ide_rules/commands/delete.py +111 -5
- llm_ide_rules/commands/download.py +123 -14
- llm_ide_rules/commands/explode.py +169 -170
- llm_ide_rules/commands/implode.py +25 -56
- llm_ide_rules/commands/mcp.py +1 -1
- llm_ide_rules/constants.py +1 -27
- llm_ide_rules/markdown_parser.py +108 -0
- llm_ide_rules/utils.py +118 -0
- {llm_ide_rules-0.6.0.dist-info → llm_ide_rules-0.8.0.dist-info}/METADATA +4 -3
- llm_ide_rules-0.8.0.dist-info/RECORD +27 -0
- llm_ide_rules/sections.json +0 -17
- llm_ide_rules-0.6.0.dist-info/RECORD +0 -23
- {llm_ide_rules-0.6.0.dist-info → llm_ide_rules-0.8.0.dist-info}/WHEEL +0 -0
- {llm_ide_rules-0.6.0.dist-info → llm_ide_rules-0.8.0.dist-info}/entry_points.txt +0 -0
|
@@ -1,29 +1,23 @@
|
|
|
1
1
|
"""Implode command: Bundle rule files into a single instruction file."""
|
|
2
2
|
|
|
3
|
-
from pathlib import Path
|
|
4
3
|
from typing_extensions import Annotated
|
|
5
4
|
|
|
6
5
|
import typer
|
|
7
6
|
|
|
8
7
|
from llm_ide_rules.agents import get_agent
|
|
9
|
-
from llm_ide_rules.constants import load_section_globs
|
|
10
8
|
from llm_ide_rules.log import log
|
|
9
|
+
from llm_ide_rules.utils import find_project_root
|
|
11
10
|
|
|
12
11
|
|
|
13
12
|
def cursor(
|
|
14
13
|
output: Annotated[
|
|
15
14
|
str, typer.Argument(help="Output file for rules")
|
|
16
15
|
] = "instructions.md",
|
|
17
|
-
config: Annotated[
|
|
18
|
-
str | None,
|
|
19
|
-
typer.Option("--config", "-c", help="Custom configuration file path"),
|
|
20
|
-
] = None,
|
|
21
16
|
) -> None:
|
|
22
17
|
"""Bundle Cursor rules into instructions.md and commands into commands.md."""
|
|
23
18
|
|
|
24
|
-
section_globs = load_section_globs(config)
|
|
25
19
|
agent = get_agent("cursor")
|
|
26
|
-
|
|
20
|
+
base_dir = find_project_root()
|
|
27
21
|
|
|
28
22
|
rules_dir = agent.rules_dir
|
|
29
23
|
if not rules_dir:
|
|
@@ -34,18 +28,17 @@ def cursor(
|
|
|
34
28
|
"bundling cursor rules and commands",
|
|
35
29
|
rules_dir=rules_dir,
|
|
36
30
|
commands_dir=agent.commands_dir,
|
|
37
|
-
config=config,
|
|
38
31
|
)
|
|
39
32
|
|
|
40
|
-
rules_path =
|
|
33
|
+
rules_path = base_dir / rules_dir
|
|
41
34
|
if not rules_path.exists():
|
|
42
35
|
log.error("cursor rules directory not found", rules_dir=str(rules_path))
|
|
43
36
|
error_msg = f"Cursor rules directory not found: {rules_path}"
|
|
44
37
|
typer.echo(typer.style(error_msg, fg=typer.colors.RED), err=True)
|
|
45
38
|
raise typer.Exit(1)
|
|
46
39
|
|
|
47
|
-
output_path =
|
|
48
|
-
rules_written = agent.bundle_rules(output_path
|
|
40
|
+
output_path = base_dir / output
|
|
41
|
+
rules_written = agent.bundle_rules(output_path)
|
|
49
42
|
if rules_written:
|
|
50
43
|
success_msg = f"Bundled cursor rules into {output}"
|
|
51
44
|
typer.echo(typer.style(success_msg, fg=typer.colors.GREEN))
|
|
@@ -53,8 +46,8 @@ def cursor(
|
|
|
53
46
|
output_path.unlink(missing_ok=True)
|
|
54
47
|
log.info("no cursor rules to bundle")
|
|
55
48
|
|
|
56
|
-
commands_output_path =
|
|
57
|
-
commands_written = agent.bundle_commands(commands_output_path
|
|
49
|
+
commands_output_path = base_dir / "commands.md"
|
|
50
|
+
commands_written = agent.bundle_commands(commands_output_path)
|
|
58
51
|
if commands_written:
|
|
59
52
|
success_msg = "Bundled cursor commands into commands.md"
|
|
60
53
|
typer.echo(typer.style(success_msg, fg=typer.colors.GREEN))
|
|
@@ -66,16 +59,11 @@ def github(
|
|
|
66
59
|
output: Annotated[
|
|
67
60
|
str, typer.Argument(help="Output file for instructions")
|
|
68
61
|
] = "instructions.md",
|
|
69
|
-
config: Annotated[
|
|
70
|
-
str | None,
|
|
71
|
-
typer.Option("--config", "-c", help="Custom configuration file path"),
|
|
72
|
-
] = None,
|
|
73
62
|
) -> None:
|
|
74
63
|
"""Bundle GitHub instructions into instructions.md and prompts into commands.md."""
|
|
75
64
|
|
|
76
|
-
section_globs = load_section_globs(config)
|
|
77
65
|
agent = get_agent("github")
|
|
78
|
-
|
|
66
|
+
base_dir = find_project_root()
|
|
79
67
|
|
|
80
68
|
rules_dir = agent.rules_dir
|
|
81
69
|
if not rules_dir:
|
|
@@ -86,10 +74,9 @@ def github(
|
|
|
86
74
|
"bundling github instructions and prompts",
|
|
87
75
|
instructions_dir=rules_dir,
|
|
88
76
|
prompts_dir=agent.commands_dir,
|
|
89
|
-
config=config,
|
|
90
77
|
)
|
|
91
78
|
|
|
92
|
-
rules_path =
|
|
79
|
+
rules_path = base_dir / rules_dir
|
|
93
80
|
if not rules_path.exists():
|
|
94
81
|
log.error(
|
|
95
82
|
"github instructions directory not found", instructions_dir=str(rules_path)
|
|
@@ -98,8 +85,8 @@ def github(
|
|
|
98
85
|
typer.echo(typer.style(error_msg, fg=typer.colors.RED), err=True)
|
|
99
86
|
raise typer.Exit(1)
|
|
100
87
|
|
|
101
|
-
output_path =
|
|
102
|
-
instructions_written = agent.bundle_rules(output_path
|
|
88
|
+
output_path = base_dir / output
|
|
89
|
+
instructions_written = agent.bundle_rules(output_path)
|
|
103
90
|
if instructions_written:
|
|
104
91
|
success_msg = f"Bundled github instructions into {output}"
|
|
105
92
|
typer.echo(typer.style(success_msg, fg=typer.colors.GREEN))
|
|
@@ -107,8 +94,8 @@ def github(
|
|
|
107
94
|
output_path.unlink(missing_ok=True)
|
|
108
95
|
log.info("no github instructions to bundle")
|
|
109
96
|
|
|
110
|
-
commands_output_path =
|
|
111
|
-
prompts_written = agent.bundle_commands(commands_output_path
|
|
97
|
+
commands_output_path = base_dir / "commands.md"
|
|
98
|
+
prompts_written = agent.bundle_commands(commands_output_path)
|
|
112
99
|
if prompts_written:
|
|
113
100
|
success_msg = "Bundled github prompts into commands.md"
|
|
114
101
|
typer.echo(typer.style(success_msg, fg=typer.colors.GREEN))
|
|
@@ -118,16 +105,11 @@ def github(
|
|
|
118
105
|
|
|
119
106
|
def claude(
|
|
120
107
|
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
108
|
) -> None:
|
|
126
109
|
"""Bundle Claude Code commands into commands.md."""
|
|
127
110
|
|
|
128
|
-
section_globs = load_section_globs(config)
|
|
129
111
|
agent = get_agent("claude")
|
|
130
|
-
|
|
112
|
+
base_dir = find_project_root()
|
|
131
113
|
|
|
132
114
|
commands_dir = agent.commands_dir
|
|
133
115
|
if not commands_dir:
|
|
@@ -137,10 +119,9 @@ def claude(
|
|
|
137
119
|
log.info(
|
|
138
120
|
"bundling claude code commands",
|
|
139
121
|
commands_dir=commands_dir,
|
|
140
|
-
config=config,
|
|
141
122
|
)
|
|
142
123
|
|
|
143
|
-
commands_path =
|
|
124
|
+
commands_path = base_dir / commands_dir
|
|
144
125
|
if not commands_path.exists():
|
|
145
126
|
log.error(
|
|
146
127
|
"claude code commands directory not found", commands_dir=str(commands_path)
|
|
@@ -149,8 +130,8 @@ def claude(
|
|
|
149
130
|
typer.echo(typer.style(error_msg, fg=typer.colors.RED), err=True)
|
|
150
131
|
raise typer.Exit(1)
|
|
151
132
|
|
|
152
|
-
output_path =
|
|
153
|
-
commands_written = agent.bundle_commands(output_path
|
|
133
|
+
output_path = base_dir / output
|
|
134
|
+
commands_written = agent.bundle_commands(output_path)
|
|
154
135
|
if commands_written:
|
|
155
136
|
success_msg = f"Bundled claude commands into {output}"
|
|
156
137
|
typer.echo(typer.style(success_msg, fg=typer.colors.GREEN))
|
|
@@ -161,16 +142,11 @@ def claude(
|
|
|
161
142
|
|
|
162
143
|
def gemini(
|
|
163
144
|
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
145
|
) -> None:
|
|
169
146
|
"""Bundle Gemini CLI commands into commands.md."""
|
|
170
147
|
|
|
171
|
-
section_globs = load_section_globs(config)
|
|
172
148
|
agent = get_agent("gemini")
|
|
173
|
-
|
|
149
|
+
base_dir = find_project_root()
|
|
174
150
|
|
|
175
151
|
commands_dir = agent.commands_dir
|
|
176
152
|
if not commands_dir:
|
|
@@ -180,10 +156,9 @@ def gemini(
|
|
|
180
156
|
log.info(
|
|
181
157
|
"bundling gemini cli commands",
|
|
182
158
|
commands_dir=commands_dir,
|
|
183
|
-
config=config,
|
|
184
159
|
)
|
|
185
160
|
|
|
186
|
-
commands_path =
|
|
161
|
+
commands_path = base_dir / commands_dir
|
|
187
162
|
if not commands_path.exists():
|
|
188
163
|
log.error(
|
|
189
164
|
"gemini cli commands directory not found", commands_dir=str(commands_path)
|
|
@@ -192,8 +167,8 @@ def gemini(
|
|
|
192
167
|
typer.echo(typer.style(error_msg, fg=typer.colors.RED), err=True)
|
|
193
168
|
raise typer.Exit(1)
|
|
194
169
|
|
|
195
|
-
output_path =
|
|
196
|
-
commands_written = agent.bundle_commands(output_path
|
|
170
|
+
output_path = base_dir / output
|
|
171
|
+
commands_written = agent.bundle_commands(output_path)
|
|
197
172
|
if commands_written:
|
|
198
173
|
success_msg = f"Bundled gemini commands into {output}"
|
|
199
174
|
typer.echo(typer.style(success_msg, fg=typer.colors.GREEN))
|
|
@@ -204,16 +179,11 @@ def gemini(
|
|
|
204
179
|
|
|
205
180
|
def opencode(
|
|
206
181
|
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
182
|
) -> None:
|
|
212
183
|
"""Bundle OpenCode commands into commands.md."""
|
|
213
184
|
|
|
214
|
-
section_globs = load_section_globs(config)
|
|
215
185
|
agent = get_agent("opencode")
|
|
216
|
-
|
|
186
|
+
base_dir = find_project_root()
|
|
217
187
|
|
|
218
188
|
commands_dir = agent.commands_dir
|
|
219
189
|
if not commands_dir:
|
|
@@ -223,10 +193,9 @@ def opencode(
|
|
|
223
193
|
log.info(
|
|
224
194
|
"bundling opencode commands",
|
|
225
195
|
commands_dir=commands_dir,
|
|
226
|
-
config=config,
|
|
227
196
|
)
|
|
228
197
|
|
|
229
|
-
commands_path =
|
|
198
|
+
commands_path = base_dir / commands_dir
|
|
230
199
|
if not commands_path.exists():
|
|
231
200
|
log.error(
|
|
232
201
|
"opencode commands directory not found", commands_dir=str(commands_path)
|
|
@@ -235,8 +204,8 @@ def opencode(
|
|
|
235
204
|
typer.echo(typer.style(error_msg, fg=typer.colors.RED), err=True)
|
|
236
205
|
raise typer.Exit(1)
|
|
237
206
|
|
|
238
|
-
output_path =
|
|
239
|
-
commands_written = agent.bundle_commands(output_path
|
|
207
|
+
output_path = base_dir / output
|
|
208
|
+
commands_written = agent.bundle_commands(output_path)
|
|
240
209
|
if commands_written:
|
|
241
210
|
success_msg = f"Bundled opencode commands into {output}"
|
|
242
211
|
typer.echo(typer.style(success_msg, fg=typer.colors.GREEN))
|
llm_ide_rules/commands/mcp.py
CHANGED
|
@@ -23,7 +23,7 @@ def explode(
|
|
|
23
23
|
"all",
|
|
24
24
|
"--agent",
|
|
25
25
|
"-a",
|
|
26
|
-
help="Agent: claude, cursor, gemini, opencode, copilot, or all",
|
|
26
|
+
help="Agent: claude, cursor, gemini, opencode, copilot, vscode, or all",
|
|
27
27
|
),
|
|
28
28
|
) -> None:
|
|
29
29
|
"""Convert unified mcp.json to platform-specific configs."""
|
llm_ide_rules/constants.py
CHANGED
|
@@ -1,32 +1,6 @@
|
|
|
1
1
|
"""Shared constants for explode and implode functionality."""
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
from pathlib import Path
|
|
5
|
-
|
|
6
|
-
VALID_AGENTS = ["cursor", "github", "claude", "gemini", "opencode", "all"]
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
def load_section_globs(custom_config_path: str | None = None) -> dict[str, str | None]:
|
|
10
|
-
"""Load section globs from JSON config file.
|
|
11
|
-
|
|
12
|
-
Args:
|
|
13
|
-
custom_config_path: Path to custom configuration file to override defaults
|
|
14
|
-
|
|
15
|
-
Returns:
|
|
16
|
-
Dictionary mapping section headers to their file globs or None for prompts
|
|
17
|
-
"""
|
|
18
|
-
if custom_config_path and Path(custom_config_path).exists():
|
|
19
|
-
config_path = Path(custom_config_path)
|
|
20
|
-
else:
|
|
21
|
-
config_path = Path(__file__).parent / "sections.json"
|
|
22
|
-
|
|
23
|
-
config = json.loads(config_path.read_text())
|
|
24
|
-
|
|
25
|
-
return config["section_globs"]
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
# Default section globs - loaded from bundled JSON
|
|
29
|
-
SECTION_GLOBS = load_section_globs()
|
|
3
|
+
VALID_AGENTS = ["cursor", "github", "claude", "gemini", "opencode", "agents", "all"]
|
|
30
4
|
|
|
31
5
|
|
|
32
6
|
def header_to_filename(header: str) -> str:
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
"""Markdown parsing utilities using markdown-it-py."""
|
|
2
|
+
|
|
3
|
+
from typing import NamedTuple
|
|
4
|
+
|
|
5
|
+
from markdown_it import MarkdownIt
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class SectionData(NamedTuple):
|
|
9
|
+
"""Data for a parsed section."""
|
|
10
|
+
|
|
11
|
+
content: list[str]
|
|
12
|
+
glob_pattern: str | None
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def extract_glob_directive(
|
|
16
|
+
content_lines: list[str],
|
|
17
|
+
) -> tuple[list[str], str | None]:
|
|
18
|
+
"""Extract glob directive from section content if present.
|
|
19
|
+
|
|
20
|
+
Checks the first non-empty line after the header for 'globs: PATTERN'.
|
|
21
|
+
|
|
22
|
+
Args:
|
|
23
|
+
content_lines: Lines including the header
|
|
24
|
+
|
|
25
|
+
Returns:
|
|
26
|
+
Tuple of (content_without_directive, glob_pattern)
|
|
27
|
+
- glob_pattern is None if no directive found (means alwaysApply)
|
|
28
|
+
- glob_pattern is "manual" for manual-only sections
|
|
29
|
+
"""
|
|
30
|
+
if not content_lines:
|
|
31
|
+
return content_lines, None
|
|
32
|
+
|
|
33
|
+
header_idx = None
|
|
34
|
+
for i, line in enumerate(content_lines):
|
|
35
|
+
if line.startswith("## "):
|
|
36
|
+
header_idx = i
|
|
37
|
+
break
|
|
38
|
+
|
|
39
|
+
if header_idx is None:
|
|
40
|
+
return content_lines, None
|
|
41
|
+
|
|
42
|
+
for i in range(header_idx + 1, len(content_lines)):
|
|
43
|
+
line = content_lines[i].strip()
|
|
44
|
+
if not line:
|
|
45
|
+
continue
|
|
46
|
+
|
|
47
|
+
if line.lower().startswith("globs: "):
|
|
48
|
+
glob_value = line[7:].strip()
|
|
49
|
+
filtered_content = content_lines[:i] + content_lines[i + 1 :]
|
|
50
|
+
return filtered_content, glob_value
|
|
51
|
+
|
|
52
|
+
break
|
|
53
|
+
|
|
54
|
+
return content_lines, None
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def parse_sections(text: str) -> tuple[list[str], dict[str, SectionData]]:
|
|
58
|
+
"""Parse markdown text into general section and named sections.
|
|
59
|
+
|
|
60
|
+
Returns:
|
|
61
|
+
Tuple of (general_lines, sections_dict) where:
|
|
62
|
+
- general_lines: Lines before the first H2 header
|
|
63
|
+
- sections_dict: Dict mapping section names to SectionData (content + glob_pattern)
|
|
64
|
+
"""
|
|
65
|
+
md = MarkdownIt()
|
|
66
|
+
tokens = md.parse(text)
|
|
67
|
+
lines = text.splitlines(keepends=True)
|
|
68
|
+
|
|
69
|
+
# Find all H2 headers
|
|
70
|
+
section_starts = []
|
|
71
|
+
for i, token in enumerate(tokens):
|
|
72
|
+
if token.type == "heading_open" and token.tag == "h2":
|
|
73
|
+
# Get the content of the header
|
|
74
|
+
# The next token is usually inline, which contains the text
|
|
75
|
+
if i + 1 < len(tokens) and tokens[i + 1].type == "inline":
|
|
76
|
+
header_content = tokens[i + 1].content.strip()
|
|
77
|
+
# token.map contains [start_line, end_line] (0-based)
|
|
78
|
+
if token.map:
|
|
79
|
+
start_line = token.map[0]
|
|
80
|
+
section_starts.append((start_line, header_content))
|
|
81
|
+
|
|
82
|
+
if not section_starts:
|
|
83
|
+
return lines, {}
|
|
84
|
+
|
|
85
|
+
# Extract general content (everything before first H2)
|
|
86
|
+
first_section_start = section_starts[0][0]
|
|
87
|
+
general_lines = lines[:first_section_start]
|
|
88
|
+
|
|
89
|
+
# Extract named sections
|
|
90
|
+
sections = {}
|
|
91
|
+
for i, (start_line, header_name) in enumerate(section_starts):
|
|
92
|
+
# End line is the start of the next section, or end of file
|
|
93
|
+
if i + 1 < len(section_starts):
|
|
94
|
+
end_line = section_starts[i + 1][0]
|
|
95
|
+
else:
|
|
96
|
+
end_line = len(lines)
|
|
97
|
+
|
|
98
|
+
# Extract all lines for this section
|
|
99
|
+
section_content = lines[start_line:end_line]
|
|
100
|
+
|
|
101
|
+
# Extract glob directive if present
|
|
102
|
+
filtered_content, glob_pattern = extract_glob_directive(section_content)
|
|
103
|
+
|
|
104
|
+
sections[header_name] = SectionData(
|
|
105
|
+
content=filtered_content, glob_pattern=glob_pattern
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
return general_lines, sections
|
llm_ide_rules/utils.py
ADDED
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
"""Utility functions for LLM IDE rules."""
|
|
2
|
+
|
|
3
|
+
import re
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def modify_json_file(file_path: Path, updates: dict[str, Any]) -> bool:
|
|
9
|
+
"""Modify a JSON/JSONC file by adding MISSING keys using string manipulation to preserve comments.
|
|
10
|
+
|
|
11
|
+
Returns:
|
|
12
|
+
bool: True if changes were written to the file, False otherwise.
|
|
13
|
+
"""
|
|
14
|
+
if not file_path.exists():
|
|
15
|
+
# Create new file with standard JSON if it doesn't exist
|
|
16
|
+
import json
|
|
17
|
+
|
|
18
|
+
file_path.parent.mkdir(parents=True, exist_ok=True)
|
|
19
|
+
file_path.write_text(json.dumps(updates, indent=2))
|
|
20
|
+
return True
|
|
21
|
+
|
|
22
|
+
original_content = file_path.read_text()
|
|
23
|
+
content = original_content
|
|
24
|
+
|
|
25
|
+
for key, value in updates.items():
|
|
26
|
+
# Prepare the value representation (basic JSON serialization)
|
|
27
|
+
if isinstance(value, bool):
|
|
28
|
+
val_str = "true" if value else "false"
|
|
29
|
+
elif isinstance(value, (int, float)):
|
|
30
|
+
val_str = str(value)
|
|
31
|
+
elif isinstance(value, str):
|
|
32
|
+
val_str = f'"{value}"'
|
|
33
|
+
else:
|
|
34
|
+
import json
|
|
35
|
+
|
|
36
|
+
val_str = json.dumps(value)
|
|
37
|
+
|
|
38
|
+
# Updated pattern:
|
|
39
|
+
# 1. Match key part: (["']?key["']?\s* : \s*)
|
|
40
|
+
# 2. Match value part: ([^,\n\r}]+?)
|
|
41
|
+
# 3. Lookahead to stop before a comma, newline, closing brace, or start of a comment
|
|
42
|
+
escaped_key = re.escape(key)
|
|
43
|
+
pattern_str = (
|
|
44
|
+
rf'(["\\]?{escaped_key}["\\]?\s*:\s*)([^,\n\r}}]+?)'
|
|
45
|
+
r"(?=\s*(?:,|\n|\r|\}|\/\/|\/\*))"
|
|
46
|
+
)
|
|
47
|
+
pattern = re.compile(pattern_str, re.MULTILINE)
|
|
48
|
+
|
|
49
|
+
match = pattern.search(content)
|
|
50
|
+
if match:
|
|
51
|
+
# Key exists, replace the value part
|
|
52
|
+
# full_match = match.group(0)
|
|
53
|
+
key_part = match.group(1)
|
|
54
|
+
# Replace the value part (group 2) with new value
|
|
55
|
+
new_entry = f"{key_part}{val_str}"
|
|
56
|
+
content = content[: match.start()] + new_entry + content[match.end() :]
|
|
57
|
+
else:
|
|
58
|
+
# Insert new key
|
|
59
|
+
last_brace_idx = content.rfind("}")
|
|
60
|
+
if last_brace_idx != -1:
|
|
61
|
+
insertion_point = last_brace_idx
|
|
62
|
+
|
|
63
|
+
# Look backwards for the first non-whitespace character before the brace
|
|
64
|
+
prev_char_idx = insertion_point - 1
|
|
65
|
+
while prev_char_idx >= 0 and content[prev_char_idx].isspace():
|
|
66
|
+
prev_char_idx -= 1
|
|
67
|
+
|
|
68
|
+
# Detect indentation from the previous line if possible
|
|
69
|
+
line_start = content.rfind("\n", 0, insertion_point)
|
|
70
|
+
if line_start != -1:
|
|
71
|
+
indent_match = re.match(r"^(\s*)", content[line_start + 1 :])
|
|
72
|
+
indent = indent_match.group(1) if indent_match else " "
|
|
73
|
+
else:
|
|
74
|
+
indent = " "
|
|
75
|
+
|
|
76
|
+
if prev_char_idx >= 0:
|
|
77
|
+
prev_char = content[prev_char_idx]
|
|
78
|
+
# If the last thing wasn't a comma or opening brace, we need a comma
|
|
79
|
+
if prev_char not in ["{", ","]:
|
|
80
|
+
new_entry = f',\n{indent}"{key}": {val_str}'
|
|
81
|
+
else:
|
|
82
|
+
new_entry = f'\n{indent}"{key}": {val_str}'
|
|
83
|
+
|
|
84
|
+
content = (
|
|
85
|
+
content[:insertion_point]
|
|
86
|
+
+ new_entry
|
|
87
|
+
+ content[insertion_point:]
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
if content != original_content:
|
|
91
|
+
file_path.write_text(content)
|
|
92
|
+
return True
|
|
93
|
+
|
|
94
|
+
return False
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def find_project_root(start_path: Path | None = None) -> Path:
|
|
98
|
+
"""Find the project root by looking for common markers."""
|
|
99
|
+
if start_path is None:
|
|
100
|
+
start_path = Path.cwd()
|
|
101
|
+
|
|
102
|
+
path = start_path.resolve()
|
|
103
|
+
# Check current directory and parents
|
|
104
|
+
for parent in [path] + list(path.parents):
|
|
105
|
+
if (parent / ".git").exists():
|
|
106
|
+
return parent
|
|
107
|
+
if (parent / "pyproject.toml").exists():
|
|
108
|
+
return parent
|
|
109
|
+
if (parent / ".cursor").exists():
|
|
110
|
+
return parent
|
|
111
|
+
if (parent / ".claude").exists():
|
|
112
|
+
return parent
|
|
113
|
+
if (parent / ".gemini").exists():
|
|
114
|
+
return parent
|
|
115
|
+
if (parent / ".github").exists():
|
|
116
|
+
return parent
|
|
117
|
+
|
|
118
|
+
return start_path # Fallback to current directory
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.3
|
|
2
2
|
Name: llm-ide-rules
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.8.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
|
|
@@ -11,13 +11,14 @@ Requires-Dist: requests>=2.25.0
|
|
|
11
11
|
Requires-Dist: pydantic>=2.0.0
|
|
12
12
|
Requires-Dist: json5>=0.9.0
|
|
13
13
|
Requires-Dist: tomli-w>=1.0.0
|
|
14
|
+
Requires-Dist: markdown-it-py>=4.0.0
|
|
14
15
|
Requires-Python: >=3.11
|
|
15
16
|
Project-URL: Repository, https://github.com/iloveitaly/llm-ide-rules
|
|
16
17
|
Description-Content-Type: text/markdown
|
|
17
18
|
|
|
18
19
|
# Copilot, Cursor, Claude, Gemini, etc LLM Instructions
|
|
19
20
|
|
|
20
|
-
This project makes it easy to download prompts and implode/explode them so they can be used by various providers.
|
|
21
|
+
This project makes it easy to download prompts and implode/explode them so they can be used by various providers. It's completely vibe coded, but it works.
|
|
21
22
|
|
|
22
23
|
I don't want to be tied to a specific IDE and it's a pain to have to edit instructions for various languages across a ton of different files.
|
|
23
24
|
|
|
@@ -39,7 +40,7 @@ Different AI coding assistants use different formats for instructions and comman
|
|
|
39
40
|
| **GitHub Copilot** | instructions | `.github/copilot-instructions.md` | Single markdown file |
|
|
40
41
|
| **GitHub Copilot** | instructions | `.github/instructions/*.instructions.md` | Multiple instruction files |
|
|
41
42
|
| **GitHub Copilot** | prompts | `.github/prompts/*.prompt.md` | YAML frontmatter with `mode: 'agent'` |
|
|
42
|
-
| **Gemini CLI** | instructions | `
|
|
43
|
+
| **Gemini CLI** | instructions | `AGENTS.md` | Single markdown file at root |
|
|
43
44
|
| **Gemini CLI** | commands | `.gemini/commands/*.toml` | TOML format, supports `{{args}}` and shell commands |
|
|
44
45
|
| **OpenCode** | commands | `.opencode/commands/*.md` | Plain markdown, no frontmatter |
|
|
45
46
|
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
llm_ide_rules/__init__.py,sha256=azpwor_AmypcI7JkDSgoOf-gyUJ66q2rPQ_WEzdJvVo,2746
|
|
2
|
+
llm_ide_rules/__main__.py,sha256=8maIDGnEcSUBs9lXg7YEevCXPC0fisYPC2gAEXHfHGM,145
|
|
3
|
+
llm_ide_rules/agents/__init__.py,sha256=pJ9sifirxbFGbajyFM6ZAnuE0kQAWiNw_bWanA4ujOY,1063
|
|
4
|
+
llm_ide_rules/agents/agents.py,sha256=QxXwlneARcPyKrsoQIlDhBMwmEKAXZDulkxZGJo6xNw,4296
|
|
5
|
+
llm_ide_rules/agents/base.py,sha256=WdzFBp_IUcOmh34joAu4FIyysZIFHbamT_4NH4IzIso,10843
|
|
6
|
+
llm_ide_rules/agents/claude.py,sha256=21D8s0FEMuD3_hAV8m_vK_zlA5BD15sPd1V_RhXRzPs,3647
|
|
7
|
+
llm_ide_rules/agents/cursor.py,sha256=QOrroQK3vHmJhpg1QsdIwfcvxjMkCNwcwwiGtxa2TNA,6890
|
|
8
|
+
llm_ide_rules/agents/gemini.py,sha256=bkigB1eUOpB6qz3N61IEF7kVH5TNZpSs0a2pqABkssE,6427
|
|
9
|
+
llm_ide_rules/agents/github.py,sha256=NZse_v5bogIyiUq0eBozZUrP0tzyYCcgiUfRpURmfNE,8196
|
|
10
|
+
llm_ide_rules/agents/opencode.py,sha256=GRtuJf8LU75KY9By4g0-Cr5e212rFeO4VnA64gnhdEk,4241
|
|
11
|
+
llm_ide_rules/agents/vscode.py,sha256=Xlte0I1TXTMV_F57hOY-6hROnh8L7arAOYXYRveAP6I,2764
|
|
12
|
+
llm_ide_rules/commands/config.py,sha256=_aVbQ4L39rYi5sQAQvK7Nd_AaMFHxn3ardNf_283mW0,1377
|
|
13
|
+
llm_ide_rules/commands/delete.py,sha256=8x0x6Hl0x-tKGy6X9yDggwREEKPUfNJ6NIanoxF05lM,9819
|
|
14
|
+
llm_ide_rules/commands/download.py,sha256=H0NX116aK1PahGTON8Ei5zysViAEIjDy_QF72OPXp4k,15189
|
|
15
|
+
llm_ide_rules/commands/explode.py,sha256=m8QUKzfX3-yRjMO4Rxwv_DPpZwBhumcaggarem99IH0,10989
|
|
16
|
+
llm_ide_rules/commands/implode.py,sha256=CEFcQkUCD8F1X3gpUscKr769qwCU8Lirg4Pp1-bPM7w,7126
|
|
17
|
+
llm_ide_rules/commands/mcp.py,sha256=KDG5RBVay6zK81RV9Msk4JJmQOpBKRsxA1JuKydzrRQ,3688
|
|
18
|
+
llm_ide_rules/constants.py,sha256=i1fWk39ZmtMgsf71xwWNCn-2U_2zOfJyVxCZ4JXJ_Ow,437
|
|
19
|
+
llm_ide_rules/log.py,sha256=hfkCLaTf2juQ7n67BYNREUrFxXDh6hqNcN2Pt9YTOo8,322
|
|
20
|
+
llm_ide_rules/markdown_parser.py,sha256=-S3sxrfFe1DRkNdu0dgUId1SMhUnfCZFMm9P7CZagUs,3418
|
|
21
|
+
llm_ide_rules/mcp/__init__.py,sha256=g73PAMhN7jDqmTBGskWJg2atpPj_-tiVY9ww7YqO2Yw,118
|
|
22
|
+
llm_ide_rules/mcp/models.py,sha256=gYzhgdWQwhH3pmj6OWVFWNNKRgxcblXeE3car2Tv8O4,440
|
|
23
|
+
llm_ide_rules/utils.py,sha256=n4gS-97pE1BE1ImypEvLUNbdAhxxHvtbZDvaIdciCgY,4288
|
|
24
|
+
llm_ide_rules-0.8.0.dist-info/WHEEL,sha256=XV0cjMrO7zXhVAIyyc8aFf1VjZ33Fen4IiJk5zFlC3g,80
|
|
25
|
+
llm_ide_rules-0.8.0.dist-info/entry_points.txt,sha256=xsALXWBwSEifz-2Mike7s2SwqNu1taLs_-EcmGrONeM,54
|
|
26
|
+
llm_ide_rules-0.8.0.dist-info/METADATA,sha256=bse_eIJTkQ0-YoNSY7V2NSS-ARvMurpMEXcSkPKs7_k,5397
|
|
27
|
+
llm_ide_rules-0.8.0.dist-info/RECORD,,
|
llm_ide_rules/sections.json
DELETED
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"section_globs": {
|
|
3
|
-
"Python": "**/*.py,pyproject.toml",
|
|
4
|
-
"Python App": "**/*.py",
|
|
5
|
-
"Pytest Integration Tests": "tests/integration/**/*.py",
|
|
6
|
-
"Pytest Tests": "tests/**/*.py",
|
|
7
|
-
"Python Route Tests": "tests/routes/**/*.py",
|
|
8
|
-
"Alembic Migrations": "migrations/versions/*.py",
|
|
9
|
-
"FastAPI": "app/routes/**/*.py",
|
|
10
|
-
"React": "**/*.tsx",
|
|
11
|
-
"React Router": "web/app/routes/**/*.tsx",
|
|
12
|
-
"Shell": "**/*.sh",
|
|
13
|
-
"TypeScript": "**/*.ts,**/*.tsx",
|
|
14
|
-
"Secrets": "env/*.sh,.envrc",
|
|
15
|
-
"Stripe Backend": "manual"
|
|
16
|
-
}
|
|
17
|
-
}
|
|
@@ -1,23 +0,0 @@
|
|
|
1
|
-
llm_ide_rules/__init__.py,sha256=HiB0P4fycSvvJvVeCQYH3WuoJ3jB8NvJV3Cc3k5U2CA,2201
|
|
2
|
-
llm_ide_rules/__main__.py,sha256=8maIDGnEcSUBs9lXg7YEevCXPC0fisYPC2gAEXHfHGM,145
|
|
3
|
-
llm_ide_rules/agents/__init__.py,sha256=-KfKLr_qG26t2OXi1dA-gCesPDqS1ARGa6hELdoyCo0,905
|
|
4
|
-
llm_ide_rules/agents/base.py,sha256=dp7Z3YJtbpQCtEBRdsFedNmljJuqe-C5Vyi8c91vQ2U,8915
|
|
5
|
-
llm_ide_rules/agents/claude.py,sha256=MpdrbhudiD54-EakCb2YPQEKKCd0P2R9c7Gp4umkA2E,2624
|
|
6
|
-
llm_ide_rules/agents/cursor.py,sha256=CDdarb9_SRDmwAVpQWPqX6NoyTyuuWVB_fr-lzxWSGU,5195
|
|
7
|
-
llm_ide_rules/agents/gemini.py,sha256=NnXlOlt4NLzoASjpmNBo079UCGdRR94-2DSC2VxKr7Y,4966
|
|
8
|
-
llm_ide_rules/agents/github.py,sha256=fhM3KpS3TP0LIA0TM6waOuuEsKRsCj3xZRwTetaOImI,6739
|
|
9
|
-
llm_ide_rules/agents/opencode.py,sha256=fNvN91_GCs9b_hFIBhmSdEiky6u7o4Rn5kqQlnj2PXw,3861
|
|
10
|
-
llm_ide_rules/commands/delete.py,sha256=m8hSDD5jruj6QN0w8zuRmrmX-SfscXjuEWwrGy3v0fs,5490
|
|
11
|
-
llm_ide_rules/commands/download.py,sha256=3ipuZwYDKVDtl82Yxyv-VgjFZa8ZCTIxbXGAu0HV6Qc,10792
|
|
12
|
-
llm_ide_rules/commands/explode.py,sha256=M12pXyo5dGKqsgTFWfJC7xNGuXeBqV5ZIp6kHaipjwM,11761
|
|
13
|
-
llm_ide_rules/commands/implode.py,sha256=nVsK3PB9hGYukA7iPEGP5cRwQn3Ma79nC8txf-ES8m8,8161
|
|
14
|
-
llm_ide_rules/commands/mcp.py,sha256=61juJyWe7BQrOisWH67-ynTBe3xjBPGq-s-HAzPWVrU,3680
|
|
15
|
-
llm_ide_rules/constants.py,sha256=2m4yYcWOcLw82P8VF6py7LXDtwfBywOrJK4SHFfyPOY,1163
|
|
16
|
-
llm_ide_rules/log.py,sha256=hfkCLaTf2juQ7n67BYNREUrFxXDh6hqNcN2Pt9YTOo8,322
|
|
17
|
-
llm_ide_rules/mcp/__init__.py,sha256=g73PAMhN7jDqmTBGskWJg2atpPj_-tiVY9ww7YqO2Yw,118
|
|
18
|
-
llm_ide_rules/mcp/models.py,sha256=gYzhgdWQwhH3pmj6OWVFWNNKRgxcblXeE3car2Tv8O4,440
|
|
19
|
-
llm_ide_rules/sections.json,sha256=fY5dY7hIMfHAiLT7OHMj-_iV83LYv5-BL6o9iOXSmOE,536
|
|
20
|
-
llm_ide_rules-0.6.0.dist-info/WHEEL,sha256=XV0cjMrO7zXhVAIyyc8aFf1VjZ33Fen4IiJk5zFlC3g,80
|
|
21
|
-
llm_ide_rules-0.6.0.dist-info/entry_points.txt,sha256=xsALXWBwSEifz-2Mike7s2SwqNu1taLs_-EcmGrONeM,54
|
|
22
|
-
llm_ide_rules-0.6.0.dist-info/METADATA,sha256=WHPO5PBdkCh1g6diZjOBmDzTGnsI8P621LUvCuJbxTk,5318
|
|
23
|
-
llm_ide_rules-0.6.0.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|