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
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
"""Gemini CLI agent implementation."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
from llm_ide_rules.agents.base import (
|
|
7
|
+
BaseAgent,
|
|
8
|
+
get_ordered_files,
|
|
9
|
+
resolve_header_from_stem,
|
|
10
|
+
strip_toml_metadata,
|
|
11
|
+
trim_content,
|
|
12
|
+
extract_description_and_filter_content,
|
|
13
|
+
)
|
|
14
|
+
from llm_ide_rules.mcp import McpServer
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class GeminiAgent(BaseAgent):
|
|
18
|
+
"""Agent for Gemini CLI."""
|
|
19
|
+
|
|
20
|
+
name = "gemini"
|
|
21
|
+
rules_dir = None
|
|
22
|
+
commands_dir = ".gemini/commands"
|
|
23
|
+
rule_extension = None
|
|
24
|
+
command_extension = ".toml"
|
|
25
|
+
|
|
26
|
+
mcp_global_path = ".gemini/settings.json"
|
|
27
|
+
mcp_project_path = ".gemini/settings.json"
|
|
28
|
+
|
|
29
|
+
def bundle_rules(
|
|
30
|
+
self, output_file: Path, section_globs: dict[str, str | None]
|
|
31
|
+
) -> bool:
|
|
32
|
+
"""Gemini CLI doesn't support rules, only commands."""
|
|
33
|
+
return False
|
|
34
|
+
|
|
35
|
+
def bundle_commands(
|
|
36
|
+
self, output_file: Path, section_globs: dict[str, str | None]
|
|
37
|
+
) -> bool:
|
|
38
|
+
"""Bundle Gemini CLI command files (.toml) into a single output file."""
|
|
39
|
+
commands_dir = self.commands_dir
|
|
40
|
+
if not commands_dir:
|
|
41
|
+
return False
|
|
42
|
+
|
|
43
|
+
commands_path = output_file.parent / commands_dir
|
|
44
|
+
if not commands_path.exists():
|
|
45
|
+
return False
|
|
46
|
+
|
|
47
|
+
extension = self.command_extension
|
|
48
|
+
if not extension:
|
|
49
|
+
return False
|
|
50
|
+
|
|
51
|
+
command_files = list(commands_path.glob(f"*{extension}"))
|
|
52
|
+
if not command_files:
|
|
53
|
+
return False
|
|
54
|
+
|
|
55
|
+
ordered_commands = get_ordered_files(command_files, list(section_globs.keys()))
|
|
56
|
+
|
|
57
|
+
content_parts: list[str] = []
|
|
58
|
+
for command_file in ordered_commands:
|
|
59
|
+
content = command_file.read_text().strip()
|
|
60
|
+
if not content:
|
|
61
|
+
continue
|
|
62
|
+
|
|
63
|
+
content = strip_toml_metadata(content)
|
|
64
|
+
header = resolve_header_from_stem(command_file.stem, section_globs)
|
|
65
|
+
content_parts.append(f"## {header}\n\n")
|
|
66
|
+
content_parts.append(content)
|
|
67
|
+
content_parts.append("\n\n")
|
|
68
|
+
|
|
69
|
+
if not content_parts:
|
|
70
|
+
return False
|
|
71
|
+
|
|
72
|
+
output_file.write_text("".join(content_parts))
|
|
73
|
+
return True
|
|
74
|
+
|
|
75
|
+
def write_rule(
|
|
76
|
+
self,
|
|
77
|
+
content_lines: list[str],
|
|
78
|
+
filename: str,
|
|
79
|
+
rules_dir: Path,
|
|
80
|
+
glob_pattern: str | None = None,
|
|
81
|
+
) -> None:
|
|
82
|
+
"""Gemini CLI doesn't support rules."""
|
|
83
|
+
pass
|
|
84
|
+
|
|
85
|
+
def write_command(
|
|
86
|
+
self,
|
|
87
|
+
content_lines: list[str],
|
|
88
|
+
filename: str,
|
|
89
|
+
commands_dir: Path,
|
|
90
|
+
section_name: str | None = None,
|
|
91
|
+
) -> None:
|
|
92
|
+
"""Write a Gemini CLI command file (.toml) with TOML format."""
|
|
93
|
+
import tomli_w
|
|
94
|
+
|
|
95
|
+
extension = self.command_extension or ".toml"
|
|
96
|
+
filepath = commands_dir / f"{filename}{extension}"
|
|
97
|
+
|
|
98
|
+
description, filtered_content = extract_description_and_filter_content(
|
|
99
|
+
content_lines, ""
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
final_content = []
|
|
103
|
+
found_header = False
|
|
104
|
+
for line in filtered_content:
|
|
105
|
+
if not found_header and line.startswith("## "):
|
|
106
|
+
found_header = True
|
|
107
|
+
continue
|
|
108
|
+
final_content.append(line)
|
|
109
|
+
|
|
110
|
+
final_content = trim_content(final_content)
|
|
111
|
+
content_str = "".join(final_content).strip()
|
|
112
|
+
|
|
113
|
+
desc = description if description else (section_name or filename)
|
|
114
|
+
|
|
115
|
+
# Construct dict and dump to TOML
|
|
116
|
+
data = {
|
|
117
|
+
"description": desc,
|
|
118
|
+
"prompt": content_str + "\n", # Ensure trailing newline in multiline string
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
# tomli-w will handle escaping and multiline strings automatically
|
|
122
|
+
output = tomli_w.dumps(data)
|
|
123
|
+
filepath.write_text(output)
|
|
124
|
+
|
|
125
|
+
def transform_mcp_server(self, server: McpServer) -> dict:
|
|
126
|
+
"""Transform unified server to Gemini format (uses httpUrl instead of url)."""
|
|
127
|
+
if server.url:
|
|
128
|
+
result: dict = {"httpUrl": server.url}
|
|
129
|
+
if server.env:
|
|
130
|
+
result["env"] = server.env
|
|
131
|
+
return result
|
|
132
|
+
|
|
133
|
+
result: dict = {"command": server.command, "args": server.args or []}
|
|
134
|
+
if server.env:
|
|
135
|
+
result["env"] = server.env
|
|
136
|
+
return result
|
|
137
|
+
|
|
138
|
+
def reverse_transform_mcp_server(self, name: str, config: dict) -> McpServer:
|
|
139
|
+
"""Transform Gemini config back to unified format."""
|
|
140
|
+
if "httpUrl" in config:
|
|
141
|
+
return McpServer(
|
|
142
|
+
url=config["httpUrl"],
|
|
143
|
+
env=config.get("env"),
|
|
144
|
+
)
|
|
145
|
+
|
|
146
|
+
return McpServer(
|
|
147
|
+
command=config["command"],
|
|
148
|
+
args=config.get("args", []),
|
|
149
|
+
env=config.get("env"),
|
|
150
|
+
)
|
|
151
|
+
|
|
152
|
+
def write_mcp_config(self, servers: dict, path: Path) -> None:
|
|
153
|
+
"""Write MCP config to path, merging with existing settings."""
|
|
154
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
155
|
+
|
|
156
|
+
existing = {}
|
|
157
|
+
if path.exists():
|
|
158
|
+
existing = json.loads(path.read_text())
|
|
159
|
+
|
|
160
|
+
existing[self.mcp_root_key] = servers
|
|
161
|
+
path.write_text(json.dumps(existing, indent=2))
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
"""GitHub/Copilot agent implementation."""
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
from llm_ide_rules.agents.base import (
|
|
6
|
+
BaseAgent,
|
|
7
|
+
get_ordered_files_github,
|
|
8
|
+
resolve_header_from_stem,
|
|
9
|
+
strip_yaml_frontmatter,
|
|
10
|
+
strip_header,
|
|
11
|
+
write_rule_file,
|
|
12
|
+
extract_description_and_filter_content,
|
|
13
|
+
)
|
|
14
|
+
from llm_ide_rules.constants import header_to_filename
|
|
15
|
+
from llm_ide_rules.mcp import McpServer
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class GitHubAgent(BaseAgent):
|
|
19
|
+
"""Agent for GitHub Copilot."""
|
|
20
|
+
|
|
21
|
+
name = "github"
|
|
22
|
+
rules_dir = ".github/instructions"
|
|
23
|
+
commands_dir = ".github/prompts"
|
|
24
|
+
rule_extension = ".instructions.md"
|
|
25
|
+
command_extension = ".prompt.md"
|
|
26
|
+
|
|
27
|
+
mcp_global_path = ".copilot/mcp-config.json"
|
|
28
|
+
mcp_project_path = ".copilot/mcp-config.json"
|
|
29
|
+
|
|
30
|
+
def bundle_rules(
|
|
31
|
+
self, output_file: Path, section_globs: dict[str, str | None]
|
|
32
|
+
) -> bool:
|
|
33
|
+
"""Bundle GitHub instruction files into a single output file."""
|
|
34
|
+
rules_dir = self.rules_dir
|
|
35
|
+
if not rules_dir:
|
|
36
|
+
return False
|
|
37
|
+
|
|
38
|
+
base_dir = output_file.parent
|
|
39
|
+
instructions_path = base_dir / rules_dir
|
|
40
|
+
copilot_general = base_dir / ".github" / "copilot-instructions.md"
|
|
41
|
+
|
|
42
|
+
rule_ext = self.rule_extension
|
|
43
|
+
if not rule_ext:
|
|
44
|
+
return False
|
|
45
|
+
|
|
46
|
+
instr_files = list(instructions_path.glob(f"*{rule_ext}"))
|
|
47
|
+
|
|
48
|
+
ordered_instructions = get_ordered_files_github(
|
|
49
|
+
instr_files, list(section_globs.keys())
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
content_parts: list[str] = []
|
|
53
|
+
if copilot_general.exists():
|
|
54
|
+
content = copilot_general.read_text().strip()
|
|
55
|
+
if content:
|
|
56
|
+
content_parts.append(content)
|
|
57
|
+
content_parts.append("\n\n")
|
|
58
|
+
|
|
59
|
+
for instr_file in ordered_instructions:
|
|
60
|
+
content = instr_file.read_text().strip()
|
|
61
|
+
if not content:
|
|
62
|
+
continue
|
|
63
|
+
|
|
64
|
+
content = strip_yaml_frontmatter(content)
|
|
65
|
+
content = strip_header(content)
|
|
66
|
+
base_stem = instr_file.stem.replace(".instructions", "")
|
|
67
|
+
header = resolve_header_from_stem(base_stem, section_globs)
|
|
68
|
+
content_parts.append(f"## {header}\n\n")
|
|
69
|
+
content_parts.append(content)
|
|
70
|
+
content_parts.append("\n\n")
|
|
71
|
+
|
|
72
|
+
if not content_parts:
|
|
73
|
+
return False
|
|
74
|
+
|
|
75
|
+
output_file.write_text("".join(content_parts))
|
|
76
|
+
return True
|
|
77
|
+
|
|
78
|
+
def bundle_commands(
|
|
79
|
+
self, output_file: Path, section_globs: dict[str, str | None]
|
|
80
|
+
) -> bool:
|
|
81
|
+
"""Bundle GitHub prompt files into a single output file."""
|
|
82
|
+
commands_dir = self.commands_dir
|
|
83
|
+
if not commands_dir:
|
|
84
|
+
return False
|
|
85
|
+
|
|
86
|
+
prompts_path = output_file.parent / commands_dir
|
|
87
|
+
if not prompts_path.exists():
|
|
88
|
+
return False
|
|
89
|
+
|
|
90
|
+
command_ext = self.command_extension
|
|
91
|
+
if not command_ext:
|
|
92
|
+
return False
|
|
93
|
+
|
|
94
|
+
prompt_files = list(prompts_path.glob(f"*{command_ext}"))
|
|
95
|
+
if not prompt_files:
|
|
96
|
+
return False
|
|
97
|
+
|
|
98
|
+
prompt_dict = {}
|
|
99
|
+
for f in prompt_files:
|
|
100
|
+
base_stem = f.stem.replace(".prompt", "")
|
|
101
|
+
prompt_dict[base_stem] = f
|
|
102
|
+
|
|
103
|
+
ordered_prompts = []
|
|
104
|
+
for section_name in section_globs.keys():
|
|
105
|
+
filename = header_to_filename(section_name)
|
|
106
|
+
if filename in prompt_dict:
|
|
107
|
+
ordered_prompts.append(prompt_dict[filename])
|
|
108
|
+
del prompt_dict[filename]
|
|
109
|
+
|
|
110
|
+
remaining_prompts = sorted(prompt_dict.values(), key=lambda p: p.name)
|
|
111
|
+
ordered_prompts.extend(remaining_prompts)
|
|
112
|
+
|
|
113
|
+
content_parts: list[str] = []
|
|
114
|
+
for prompt_file in ordered_prompts:
|
|
115
|
+
content = prompt_file.read_text().strip()
|
|
116
|
+
if not content:
|
|
117
|
+
continue
|
|
118
|
+
|
|
119
|
+
content = strip_yaml_frontmatter(content)
|
|
120
|
+
content = strip_header(content)
|
|
121
|
+
base_stem = prompt_file.stem.replace(".prompt", "")
|
|
122
|
+
header = resolve_header_from_stem(base_stem, section_globs)
|
|
123
|
+
content_parts.append(f"## {header}\n\n")
|
|
124
|
+
content_parts.append(content)
|
|
125
|
+
content_parts.append("\n\n")
|
|
126
|
+
|
|
127
|
+
if not content_parts:
|
|
128
|
+
return False
|
|
129
|
+
|
|
130
|
+
output_file.write_text("".join(content_parts))
|
|
131
|
+
return True
|
|
132
|
+
|
|
133
|
+
def write_rule(
|
|
134
|
+
self,
|
|
135
|
+
content_lines: list[str],
|
|
136
|
+
filename: str,
|
|
137
|
+
rules_dir: Path,
|
|
138
|
+
glob_pattern: str | None = None,
|
|
139
|
+
) -> None:
|
|
140
|
+
"""Write a GitHub instruction file (.instructions.md) with YAML frontmatter."""
|
|
141
|
+
extension = self.rule_extension or ".instructions.md"
|
|
142
|
+
filepath = rules_dir / f"{filename}{extension}"
|
|
143
|
+
|
|
144
|
+
if glob_pattern and glob_pattern != "manual":
|
|
145
|
+
header_yaml = f"""---
|
|
146
|
+
applyTo: "{glob_pattern}"
|
|
147
|
+
---
|
|
148
|
+
"""
|
|
149
|
+
else:
|
|
150
|
+
header_yaml = ""
|
|
151
|
+
|
|
152
|
+
write_rule_file(filepath, header_yaml, content_lines)
|
|
153
|
+
|
|
154
|
+
def write_command(
|
|
155
|
+
self,
|
|
156
|
+
content_lines: list[str],
|
|
157
|
+
filename: str,
|
|
158
|
+
commands_dir: Path,
|
|
159
|
+
section_name: str | None = None,
|
|
160
|
+
) -> None:
|
|
161
|
+
"""Write a GitHub prompt file (.prompt.md) with YAML frontmatter."""
|
|
162
|
+
extension = self.command_extension or ".prompt.md"
|
|
163
|
+
filepath = commands_dir / f"{filename}{extension}"
|
|
164
|
+
|
|
165
|
+
description, filtered_content = extract_description_and_filter_content(
|
|
166
|
+
content_lines, ""
|
|
167
|
+
)
|
|
168
|
+
|
|
169
|
+
frontmatter = f"---\nmode: 'agent'\ndescription: '{description}'\n---\n"
|
|
170
|
+
filepath.write_text(frontmatter + "".join(filtered_content))
|
|
171
|
+
|
|
172
|
+
def write_general_instructions(
|
|
173
|
+
self, content_lines: list[str], base_dir: Path
|
|
174
|
+
) -> None:
|
|
175
|
+
"""Write the general copilot-instructions.md file (no frontmatter)."""
|
|
176
|
+
filepath = base_dir / ".github" / "copilot-instructions.md"
|
|
177
|
+
write_rule_file(filepath, "", content_lines)
|
|
178
|
+
|
|
179
|
+
def transform_mcp_server(self, server: McpServer) -> dict:
|
|
180
|
+
"""Transform unified server to GitHub Copilot format (adds type and tools)."""
|
|
181
|
+
base: dict = {"tools": ["*"]}
|
|
182
|
+
if server.env:
|
|
183
|
+
base["env"] = server.env
|
|
184
|
+
|
|
185
|
+
if server.url:
|
|
186
|
+
return {"type": "http", "url": server.url, **base}
|
|
187
|
+
|
|
188
|
+
return {
|
|
189
|
+
"type": "local",
|
|
190
|
+
"command": server.command,
|
|
191
|
+
"args": server.args or [],
|
|
192
|
+
**base,
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
def reverse_transform_mcp_server(self, name: str, config: dict) -> McpServer:
|
|
196
|
+
"""Transform GitHub Copilot config back to unified format."""
|
|
197
|
+
if config.get("type") == "http":
|
|
198
|
+
return McpServer(
|
|
199
|
+
url=config["url"],
|
|
200
|
+
env=config.get("env"),
|
|
201
|
+
)
|
|
202
|
+
|
|
203
|
+
return McpServer(
|
|
204
|
+
command=config["command"],
|
|
205
|
+
args=config.get("args", []),
|
|
206
|
+
env=config.get("env"),
|
|
207
|
+
)
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
"""OpenCode agent implementation."""
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
from llm_ide_rules.agents.base import (
|
|
6
|
+
BaseAgent,
|
|
7
|
+
get_ordered_files,
|
|
8
|
+
resolve_header_from_stem,
|
|
9
|
+
trim_content,
|
|
10
|
+
)
|
|
11
|
+
from llm_ide_rules.mcp import McpServer
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class OpenCodeAgent(BaseAgent):
|
|
15
|
+
"""Agent for OpenCode."""
|
|
16
|
+
|
|
17
|
+
name = "opencode"
|
|
18
|
+
rules_dir = None
|
|
19
|
+
commands_dir = ".opencode/commands"
|
|
20
|
+
rule_extension = None
|
|
21
|
+
command_extension = ".md"
|
|
22
|
+
|
|
23
|
+
mcp_global_path = ".config/opencode/opencode.json"
|
|
24
|
+
mcp_project_path = "opencode.json"
|
|
25
|
+
mcp_root_key = "mcp"
|
|
26
|
+
|
|
27
|
+
def bundle_rules(
|
|
28
|
+
self, output_file: Path, section_globs: dict[str, str | None]
|
|
29
|
+
) -> bool:
|
|
30
|
+
"""OpenCode doesn't support rules."""
|
|
31
|
+
return False
|
|
32
|
+
|
|
33
|
+
def bundle_commands(
|
|
34
|
+
self, output_file: Path, section_globs: dict[str, str | None]
|
|
35
|
+
) -> bool:
|
|
36
|
+
"""Bundle OpenCode command files (.md) into a single output file."""
|
|
37
|
+
commands_dir = self.commands_dir
|
|
38
|
+
if not commands_dir:
|
|
39
|
+
return False
|
|
40
|
+
|
|
41
|
+
commands_path = output_file.parent / commands_dir
|
|
42
|
+
if not commands_path.exists():
|
|
43
|
+
return False
|
|
44
|
+
|
|
45
|
+
extension = self.command_extension
|
|
46
|
+
if not extension:
|
|
47
|
+
return False
|
|
48
|
+
|
|
49
|
+
command_files = list(commands_path.glob(f"*{extension}"))
|
|
50
|
+
if not command_files:
|
|
51
|
+
return False
|
|
52
|
+
|
|
53
|
+
ordered_commands = get_ordered_files(command_files, list(section_globs.keys()))
|
|
54
|
+
|
|
55
|
+
content_parts: list[str] = []
|
|
56
|
+
for command_file in ordered_commands:
|
|
57
|
+
content = command_file.read_text().strip()
|
|
58
|
+
if not content:
|
|
59
|
+
continue
|
|
60
|
+
|
|
61
|
+
header = resolve_header_from_stem(command_file.stem, section_globs)
|
|
62
|
+
content_parts.append(f"## {header}\n\n")
|
|
63
|
+
content_parts.append(content)
|
|
64
|
+
content_parts.append("\n\n")
|
|
65
|
+
|
|
66
|
+
if not content_parts:
|
|
67
|
+
return False
|
|
68
|
+
|
|
69
|
+
output_file.write_text("".join(content_parts))
|
|
70
|
+
return True
|
|
71
|
+
|
|
72
|
+
def write_rule(
|
|
73
|
+
self,
|
|
74
|
+
content_lines: list[str],
|
|
75
|
+
filename: str,
|
|
76
|
+
rules_dir: Path,
|
|
77
|
+
glob_pattern: str | None = None,
|
|
78
|
+
) -> None:
|
|
79
|
+
"""OpenCode doesn't support rules."""
|
|
80
|
+
pass
|
|
81
|
+
|
|
82
|
+
def write_command(
|
|
83
|
+
self,
|
|
84
|
+
content_lines: list[str],
|
|
85
|
+
filename: str,
|
|
86
|
+
commands_dir: Path,
|
|
87
|
+
section_name: str | None = None,
|
|
88
|
+
) -> None:
|
|
89
|
+
"""Write an OpenCode command file (.md) - plain markdown, no frontmatter."""
|
|
90
|
+
extension = self.command_extension or ".md"
|
|
91
|
+
filepath = commands_dir / f"{filename}{extension}"
|
|
92
|
+
|
|
93
|
+
trimmed = trim_content(content_lines)
|
|
94
|
+
filepath.write_text("".join(trimmed))
|
|
95
|
+
|
|
96
|
+
def transform_mcp_server(self, server: McpServer) -> dict:
|
|
97
|
+
"""Transform unified server to OpenCode format (merged command array, environment key)."""
|
|
98
|
+
if server.url:
|
|
99
|
+
result = {"type": "sse", "url": server.url, "enabled": True}
|
|
100
|
+
if server.env:
|
|
101
|
+
result["environment"] = server.env
|
|
102
|
+
return result
|
|
103
|
+
|
|
104
|
+
result = {
|
|
105
|
+
"type": "local",
|
|
106
|
+
"command": [server.command] + (server.args or []),
|
|
107
|
+
"enabled": True,
|
|
108
|
+
}
|
|
109
|
+
if server.env:
|
|
110
|
+
result["environment"] = server.env
|
|
111
|
+
return result
|
|
112
|
+
|
|
113
|
+
def reverse_transform_mcp_server(self, name: str, config: dict) -> McpServer:
|
|
114
|
+
"""Transform OpenCode config back to unified format."""
|
|
115
|
+
if config.get("type") == "sse":
|
|
116
|
+
return McpServer(
|
|
117
|
+
url=config["url"],
|
|
118
|
+
env=config.get("environment"),
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
command_array = config["command"]
|
|
122
|
+
return McpServer(
|
|
123
|
+
command=command_array[0] if command_array else None,
|
|
124
|
+
args=command_array[1:] if len(command_array) > 1 else [],
|
|
125
|
+
env=config.get("environment"),
|
|
126
|
+
)
|
llm_ide_rules/commands/delete.py
CHANGED
|
@@ -1,24 +1,20 @@
|
|
|
1
1
|
"""Delete command: Remove downloaded LLM instruction files."""
|
|
2
2
|
|
|
3
|
-
import logging
|
|
4
3
|
import shutil
|
|
5
4
|
from pathlib import Path
|
|
6
|
-
from typing import List
|
|
7
5
|
|
|
8
|
-
import structlog
|
|
9
6
|
import typer
|
|
10
7
|
from typing_extensions import Annotated
|
|
11
8
|
|
|
12
9
|
from llm_ide_rules.commands.download import INSTRUCTION_TYPES, DEFAULT_TYPES
|
|
13
|
-
|
|
14
|
-
logger = structlog.get_logger()
|
|
10
|
+
from llm_ide_rules.log import log
|
|
15
11
|
|
|
16
12
|
|
|
17
13
|
def find_files_to_delete(
|
|
18
|
-
instruction_types:
|
|
19
|
-
) -> tuple[
|
|
14
|
+
instruction_types: list[str], target_dir: Path
|
|
15
|
+
) -> tuple[list[Path], list[Path]]:
|
|
20
16
|
"""Find all files and directories that would be deleted.
|
|
21
|
-
|
|
17
|
+
|
|
22
18
|
Returns:
|
|
23
19
|
Tuple of (directories, files) to delete
|
|
24
20
|
"""
|
|
@@ -27,7 +23,7 @@ def find_files_to_delete(
|
|
|
27
23
|
|
|
28
24
|
for inst_type in instruction_types:
|
|
29
25
|
if inst_type not in INSTRUCTION_TYPES:
|
|
30
|
-
|
|
26
|
+
log.warning("unknown instruction type", type=inst_type)
|
|
31
27
|
continue
|
|
32
28
|
|
|
33
29
|
config = INSTRUCTION_TYPES[inst_type]
|
|
@@ -51,7 +47,7 @@ def find_files_to_delete(
|
|
|
51
47
|
|
|
52
48
|
def delete_main(
|
|
53
49
|
instruction_types: Annotated[
|
|
54
|
-
|
|
50
|
+
list[str] | None,
|
|
55
51
|
typer.Argument(
|
|
56
52
|
help="Types of instructions to delete (cursor, github, gemini, claude, agent, agents). Deletes everything by default."
|
|
57
53
|
),
|
|
@@ -61,10 +57,9 @@ def delete_main(
|
|
|
61
57
|
] = ".",
|
|
62
58
|
yes: Annotated[
|
|
63
59
|
bool,
|
|
64
|
-
typer.Option(
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
bool, typer.Option("--verbose", "-v", help="Enable verbose logging")
|
|
60
|
+
typer.Option(
|
|
61
|
+
"--yes", "-y", help="Skip confirmation prompt and delete immediately"
|
|
62
|
+
),
|
|
68
63
|
] = False,
|
|
69
64
|
):
|
|
70
65
|
"""Remove downloaded LLM instruction files.
|
|
@@ -90,19 +85,13 @@ def delete_main(
|
|
|
90
85
|
# Delete from a specific directory
|
|
91
86
|
llm_ide_rules delete --target ./my-project
|
|
92
87
|
"""
|
|
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
88
|
if not instruction_types:
|
|
100
89
|
instruction_types = DEFAULT_TYPES
|
|
101
90
|
|
|
102
91
|
invalid_types = [t for t in instruction_types if t not in INSTRUCTION_TYPES]
|
|
103
92
|
if invalid_types:
|
|
104
|
-
|
|
105
|
-
"
|
|
93
|
+
log.error(
|
|
94
|
+
"invalid instruction types",
|
|
106
95
|
invalid_types=invalid_types,
|
|
107
96
|
valid_types=list(INSTRUCTION_TYPES.keys()),
|
|
108
97
|
)
|
|
@@ -111,12 +100,13 @@ def delete_main(
|
|
|
111
100
|
target_path = Path(target_dir).resolve()
|
|
112
101
|
|
|
113
102
|
if not target_path.exists():
|
|
114
|
-
|
|
115
|
-
|
|
103
|
+
log.error("target directory does not exist", target_dir=str(target_path))
|
|
104
|
+
error_msg = f"Target directory does not exist: {target_path}"
|
|
105
|
+
typer.echo(typer.style(error_msg, fg=typer.colors.RED), err=True)
|
|
116
106
|
raise typer.Exit(1)
|
|
117
107
|
|
|
118
|
-
|
|
119
|
-
"
|
|
108
|
+
log.info(
|
|
109
|
+
"finding files to delete",
|
|
120
110
|
instruction_types=instruction_types,
|
|
121
111
|
target_dir=str(target_path),
|
|
122
112
|
)
|
|
@@ -126,7 +116,7 @@ def delete_main(
|
|
|
126
116
|
)
|
|
127
117
|
|
|
128
118
|
if not dirs_to_delete and not files_to_delete:
|
|
129
|
-
|
|
119
|
+
log.info("no files found to delete")
|
|
130
120
|
typer.echo("No matching instruction files found to delete.")
|
|
131
121
|
return
|
|
132
122
|
|
|
@@ -151,7 +141,7 @@ def delete_main(
|
|
|
151
141
|
typer.echo()
|
|
152
142
|
confirm = typer.confirm("Are you sure you want to delete these files?")
|
|
153
143
|
if not confirm:
|
|
154
|
-
|
|
144
|
+
log.info("deletion cancelled by user")
|
|
155
145
|
typer.echo("Deletion cancelled.")
|
|
156
146
|
raise typer.Exit(0)
|
|
157
147
|
|
|
@@ -159,21 +149,21 @@ def delete_main(
|
|
|
159
149
|
|
|
160
150
|
for dir_path in dirs_to_delete:
|
|
161
151
|
try:
|
|
162
|
-
|
|
152
|
+
log.info("deleting directory", path=str(dir_path))
|
|
163
153
|
shutil.rmtree(dir_path)
|
|
164
154
|
deleted_count += 1
|
|
165
155
|
except Exception as e:
|
|
166
|
-
|
|
156
|
+
log.error("failed to delete directory", path=str(dir_path), error=str(e))
|
|
167
157
|
typer.echo(f"Error deleting {dir_path}: {e}", err=True)
|
|
168
158
|
|
|
169
159
|
for file_path in files_to_delete:
|
|
170
160
|
try:
|
|
171
|
-
|
|
161
|
+
log.info("deleting file", path=str(file_path))
|
|
172
162
|
file_path.unlink()
|
|
173
163
|
deleted_count += 1
|
|
174
164
|
except Exception as e:
|
|
175
|
-
|
|
165
|
+
log.error("failed to delete file", path=str(file_path), error=str(e))
|
|
176
166
|
typer.echo(f"Error deleting {file_path}: {e}", err=True)
|
|
177
167
|
|
|
178
|
-
|
|
179
|
-
typer.echo(
|
|
168
|
+
success_msg = f"Successfully deleted {deleted_count} of {total_items} items."
|
|
169
|
+
typer.echo(typer.style(success_msg, fg=typer.colors.GREEN))
|