llm-ide-rules 0.5.0__py3-none-any.whl → 0.7.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- llm_ide_rules/__init__.py +53 -9
- llm_ide_rules/__main__.py +1 -1
- llm_ide_rules/agents/__init__.py +28 -0
- llm_ide_rules/agents/base.py +329 -0
- llm_ide_rules/agents/claude.py +108 -0
- llm_ide_rules/agents/cursor.py +199 -0
- llm_ide_rules/agents/gemini.py +177 -0
- llm_ide_rules/agents/github.py +212 -0
- llm_ide_rules/agents/opencode.py +130 -0
- llm_ide_rules/commands/delete.py +24 -34
- llm_ide_rules/commands/download.py +146 -60
- llm_ide_rules/commands/explode.py +222 -382
- llm_ide_rules/commands/implode.py +174 -360
- llm_ide_rules/commands/mcp.py +119 -0
- llm_ide_rules/constants.py +6 -29
- llm_ide_rules/log.py +9 -0
- llm_ide_rules/markdown_parser.py +108 -0
- llm_ide_rules/mcp/__init__.py +7 -0
- llm_ide_rules/mcp/models.py +21 -0
- {llm_ide_rules-0.5.0.dist-info → llm_ide_rules-0.7.0.dist-info}/METADATA +36 -59
- llm_ide_rules-0.7.0.dist-info/RECORD +23 -0
- {llm_ide_rules-0.5.0.dist-info → llm_ide_rules-0.7.0.dist-info}/WHEEL +2 -2
- llm_ide_rules/sections.json +0 -27
- llm_ide_rules-0.5.0.dist-info/RECORD +0 -12
- {llm_ide_rules-0.5.0.dist-info → llm_ide_rules-0.7.0.dist-info}/entry_points.txt +0 -0
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
"""Cursor IDE 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
|
+
strip_yaml_frontmatter,
|
|
10
|
+
strip_header,
|
|
11
|
+
trim_content,
|
|
12
|
+
write_rule_file,
|
|
13
|
+
extract_description_and_filter_content,
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class CursorAgent(BaseAgent):
|
|
18
|
+
"""Agent for Cursor IDE."""
|
|
19
|
+
|
|
20
|
+
name = "cursor"
|
|
21
|
+
rules_dir = ".cursor/rules"
|
|
22
|
+
commands_dir = ".cursor/commands"
|
|
23
|
+
rule_extension = ".mdc"
|
|
24
|
+
command_extension = ".md"
|
|
25
|
+
|
|
26
|
+
mcp_global_path = ".cursor/mcp.json"
|
|
27
|
+
mcp_project_path = ".cursor/mcp.json"
|
|
28
|
+
|
|
29
|
+
def bundle_rules(
|
|
30
|
+
self, output_file: Path, section_globs: dict[str, str | None] | None = None
|
|
31
|
+
) -> bool:
|
|
32
|
+
"""Bundle Cursor rule files (.mdc) into a single output file."""
|
|
33
|
+
rules_dir = self.rules_dir
|
|
34
|
+
if not rules_dir:
|
|
35
|
+
return False
|
|
36
|
+
|
|
37
|
+
rules_path = output_file.parent / rules_dir
|
|
38
|
+
|
|
39
|
+
rule_ext = self.rule_extension
|
|
40
|
+
if not rule_ext:
|
|
41
|
+
return False
|
|
42
|
+
|
|
43
|
+
rule_files = list(rules_path.glob(f"*{rule_ext}"))
|
|
44
|
+
|
|
45
|
+
general = [f for f in rule_files if f.stem == "general"]
|
|
46
|
+
others = [f for f in rule_files if f.stem != "general"]
|
|
47
|
+
|
|
48
|
+
ordered_others = get_ordered_files(
|
|
49
|
+
others, list(section_globs.keys()) if section_globs else None
|
|
50
|
+
)
|
|
51
|
+
ordered = general + ordered_others
|
|
52
|
+
|
|
53
|
+
content_parts: list[str] = []
|
|
54
|
+
for rule_file in ordered:
|
|
55
|
+
file_content = rule_file.read_text().strip()
|
|
56
|
+
if not file_content:
|
|
57
|
+
continue
|
|
58
|
+
|
|
59
|
+
# Extract header from file content before stripping it
|
|
60
|
+
lines = file_content.splitlines()
|
|
61
|
+
extracted_header = None
|
|
62
|
+
for line in lines:
|
|
63
|
+
if line.startswith("## "):
|
|
64
|
+
extracted_header = line[3:].strip()
|
|
65
|
+
break
|
|
66
|
+
|
|
67
|
+
content = strip_yaml_frontmatter(file_content)
|
|
68
|
+
content = strip_header(content)
|
|
69
|
+
|
|
70
|
+
# Use extracted header if available, otherwise resolve from filename
|
|
71
|
+
if extracted_header:
|
|
72
|
+
header = extracted_header
|
|
73
|
+
else:
|
|
74
|
+
header = resolve_header_from_stem(
|
|
75
|
+
rule_file.stem, section_globs if section_globs else {}
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
if rule_file.stem != "general":
|
|
79
|
+
content_parts.append(f"## {header}\n\n")
|
|
80
|
+
|
|
81
|
+
content_parts.append(content)
|
|
82
|
+
content_parts.append("\n\n")
|
|
83
|
+
|
|
84
|
+
if not content_parts:
|
|
85
|
+
return False
|
|
86
|
+
|
|
87
|
+
output_file.write_text("".join(content_parts))
|
|
88
|
+
return True
|
|
89
|
+
|
|
90
|
+
def bundle_commands(
|
|
91
|
+
self, output_file: Path, section_globs: dict[str, str | None] | None = None
|
|
92
|
+
) -> bool:
|
|
93
|
+
"""Bundle Cursor command files (.md) into a single output file."""
|
|
94
|
+
commands_dir = self.commands_dir
|
|
95
|
+
if not commands_dir:
|
|
96
|
+
return False
|
|
97
|
+
|
|
98
|
+
commands_path = output_file.parent / commands_dir
|
|
99
|
+
if not commands_path.exists():
|
|
100
|
+
return False
|
|
101
|
+
|
|
102
|
+
command_ext = self.command_extension
|
|
103
|
+
if not command_ext:
|
|
104
|
+
return False
|
|
105
|
+
|
|
106
|
+
command_files = list(commands_path.glob(f"*{command_ext}"))
|
|
107
|
+
if not command_files:
|
|
108
|
+
return False
|
|
109
|
+
|
|
110
|
+
ordered_commands = get_ordered_files(
|
|
111
|
+
command_files, list(section_globs.keys()) if section_globs else None
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
content_parts: list[str] = []
|
|
115
|
+
for command_file in ordered_commands:
|
|
116
|
+
content = command_file.read_text().strip()
|
|
117
|
+
if not content:
|
|
118
|
+
continue
|
|
119
|
+
|
|
120
|
+
header = resolve_header_from_stem(
|
|
121
|
+
command_file.stem, section_globs if section_globs else {}
|
|
122
|
+
)
|
|
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 Cursor rule file (.mdc) with YAML frontmatter."""
|
|
141
|
+
extension = self.rule_extension or ".mdc"
|
|
142
|
+
filepath = rules_dir / f"{filename}{extension}"
|
|
143
|
+
|
|
144
|
+
if glob_pattern and glob_pattern != "manual":
|
|
145
|
+
header_yaml = f"""---
|
|
146
|
+
description:
|
|
147
|
+
globs: {glob_pattern}
|
|
148
|
+
alwaysApply: false
|
|
149
|
+
---
|
|
150
|
+
"""
|
|
151
|
+
elif glob_pattern == "manual":
|
|
152
|
+
header_yaml = """---
|
|
153
|
+
description:
|
|
154
|
+
alwaysApply: false
|
|
155
|
+
---
|
|
156
|
+
"""
|
|
157
|
+
else:
|
|
158
|
+
header_yaml = """---
|
|
159
|
+
description:
|
|
160
|
+
alwaysApply: true
|
|
161
|
+
---
|
|
162
|
+
"""
|
|
163
|
+
write_rule_file(filepath, header_yaml, content_lines)
|
|
164
|
+
|
|
165
|
+
def write_command(
|
|
166
|
+
self,
|
|
167
|
+
content_lines: list[str],
|
|
168
|
+
filename: str,
|
|
169
|
+
commands_dir: Path,
|
|
170
|
+
section_name: str | None = None,
|
|
171
|
+
) -> None:
|
|
172
|
+
"""Write a Cursor command file (.md) - plain markdown, no frontmatter."""
|
|
173
|
+
extension = self.command_extension or ".md"
|
|
174
|
+
filepath = commands_dir / f"{filename}{extension}"
|
|
175
|
+
|
|
176
|
+
trimmed = trim_content(content_lines)
|
|
177
|
+
filepath.write_text("".join(trimmed))
|
|
178
|
+
|
|
179
|
+
def write_prompt(
|
|
180
|
+
self,
|
|
181
|
+
content_lines: list[str],
|
|
182
|
+
filename: str,
|
|
183
|
+
prompts_dir: Path,
|
|
184
|
+
section_name: str | None = None,
|
|
185
|
+
) -> None:
|
|
186
|
+
"""Write a Cursor prompt file (.mdc) with optional frontmatter."""
|
|
187
|
+
extension = self.rule_extension or ".mdc"
|
|
188
|
+
filepath = prompts_dir / f"{filename}{extension}"
|
|
189
|
+
|
|
190
|
+
description, filtered_content = extract_description_and_filter_content(
|
|
191
|
+
content_lines, ""
|
|
192
|
+
)
|
|
193
|
+
|
|
194
|
+
output_parts: list[str] = []
|
|
195
|
+
if description:
|
|
196
|
+
output_parts.append(f"---\ndescription: {description}\n---\n")
|
|
197
|
+
|
|
198
|
+
output_parts.extend(filtered_content)
|
|
199
|
+
filepath.write_text("".join(output_parts))
|
|
@@ -0,0 +1,177 @@
|
|
|
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] | None = 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] | None = 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(
|
|
56
|
+
command_files, list(section_globs.keys()) if section_globs else None
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
content_parts: list[str] = []
|
|
60
|
+
for command_file in ordered_commands:
|
|
61
|
+
content = command_file.read_text().strip()
|
|
62
|
+
if not content:
|
|
63
|
+
continue
|
|
64
|
+
|
|
65
|
+
content = strip_toml_metadata(content)
|
|
66
|
+
header = resolve_header_from_stem(
|
|
67
|
+
command_file.stem, section_globs if section_globs else {}
|
|
68
|
+
)
|
|
69
|
+
content_parts.append(f"## {header}\n\n")
|
|
70
|
+
content_parts.append(content)
|
|
71
|
+
content_parts.append("\n\n")
|
|
72
|
+
|
|
73
|
+
if not content_parts:
|
|
74
|
+
return False
|
|
75
|
+
|
|
76
|
+
output_file.write_text("".join(content_parts))
|
|
77
|
+
return True
|
|
78
|
+
|
|
79
|
+
def write_rule(
|
|
80
|
+
self,
|
|
81
|
+
content_lines: list[str],
|
|
82
|
+
filename: str,
|
|
83
|
+
rules_dir: Path,
|
|
84
|
+
glob_pattern: str | None = None,
|
|
85
|
+
) -> None:
|
|
86
|
+
"""Gemini CLI doesn't support rules."""
|
|
87
|
+
pass
|
|
88
|
+
|
|
89
|
+
def write_command(
|
|
90
|
+
self,
|
|
91
|
+
content_lines: list[str],
|
|
92
|
+
filename: str,
|
|
93
|
+
commands_dir: Path,
|
|
94
|
+
section_name: str | None = None,
|
|
95
|
+
) -> None:
|
|
96
|
+
"""Write a Gemini CLI command file (.toml) with TOML format."""
|
|
97
|
+
import tomli_w
|
|
98
|
+
|
|
99
|
+
extension = self.command_extension or ".toml"
|
|
100
|
+
filepath = commands_dir / f"{filename}{extension}"
|
|
101
|
+
|
|
102
|
+
description, filtered_content = extract_description_and_filter_content(
|
|
103
|
+
content_lines, ""
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
final_content = []
|
|
107
|
+
found_header = False
|
|
108
|
+
for line in filtered_content:
|
|
109
|
+
if not found_header and line.startswith("## "):
|
|
110
|
+
found_header = True
|
|
111
|
+
continue
|
|
112
|
+
final_content.append(line)
|
|
113
|
+
|
|
114
|
+
final_content = trim_content(final_content)
|
|
115
|
+
content_str = "".join(final_content).strip()
|
|
116
|
+
|
|
117
|
+
desc = description if description else (section_name or filename)
|
|
118
|
+
|
|
119
|
+
# Construct dict and dump to TOML
|
|
120
|
+
data = {
|
|
121
|
+
"description": desc,
|
|
122
|
+
"prompt": content_str + "\n", # Ensure trailing newline in multiline string
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
# tomli-w will handle escaping and multiline strings automatically
|
|
126
|
+
output = tomli_w.dumps(data)
|
|
127
|
+
filepath.write_text(output)
|
|
128
|
+
|
|
129
|
+
def transform_mcp_server(self, server: McpServer) -> dict:
|
|
130
|
+
"""Transform unified server to Gemini format (uses httpUrl instead of url)."""
|
|
131
|
+
if server.url:
|
|
132
|
+
result: dict = {"httpUrl": server.url}
|
|
133
|
+
if server.env:
|
|
134
|
+
result["env"] = server.env
|
|
135
|
+
return result
|
|
136
|
+
|
|
137
|
+
result: dict = {"command": server.command, "args": server.args or []}
|
|
138
|
+
if server.env:
|
|
139
|
+
result["env"] = server.env
|
|
140
|
+
return result
|
|
141
|
+
|
|
142
|
+
def reverse_transform_mcp_server(self, name: str, config: dict) -> McpServer:
|
|
143
|
+
"""Transform Gemini config back to unified format."""
|
|
144
|
+
if "httpUrl" in config:
|
|
145
|
+
return McpServer(
|
|
146
|
+
url=config["httpUrl"],
|
|
147
|
+
env=config.get("env"),
|
|
148
|
+
)
|
|
149
|
+
|
|
150
|
+
return McpServer(
|
|
151
|
+
command=config["command"],
|
|
152
|
+
args=config.get("args", []),
|
|
153
|
+
env=config.get("env"),
|
|
154
|
+
)
|
|
155
|
+
|
|
156
|
+
def write_mcp_config(self, servers: dict, path: Path) -> None:
|
|
157
|
+
"""Write MCP config to path, merging with existing settings."""
|
|
158
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
159
|
+
|
|
160
|
+
existing = {}
|
|
161
|
+
if path.exists():
|
|
162
|
+
existing = json.loads(path.read_text())
|
|
163
|
+
|
|
164
|
+
existing[self.mcp_root_key] = servers
|
|
165
|
+
path.write_text(json.dumps(existing, indent=2))
|
|
166
|
+
|
|
167
|
+
def generate_root_doc(
|
|
168
|
+
self,
|
|
169
|
+
general_lines: list[str],
|
|
170
|
+
rules_sections: dict[str, list[str]],
|
|
171
|
+
command_sections: dict[str, list[str]],
|
|
172
|
+
output_dir: Path,
|
|
173
|
+
) -> None:
|
|
174
|
+
"""Generate GEMINI.md from rules."""
|
|
175
|
+
content = self.build_root_doc_content(general_lines, rules_sections)
|
|
176
|
+
if content.strip():
|
|
177
|
+
(output_dir / "GEMINI.md").write_text(content)
|
|
@@ -0,0 +1,212 @@
|
|
|
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] | None = 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()) if section_globs else None
|
|
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(
|
|
68
|
+
base_stem, section_globs if section_globs else {}
|
|
69
|
+
)
|
|
70
|
+
content_parts.append(f"## {header}\n\n")
|
|
71
|
+
content_parts.append(content)
|
|
72
|
+
content_parts.append("\n\n")
|
|
73
|
+
|
|
74
|
+
if not content_parts:
|
|
75
|
+
return False
|
|
76
|
+
|
|
77
|
+
output_file.write_text("".join(content_parts))
|
|
78
|
+
return True
|
|
79
|
+
|
|
80
|
+
def bundle_commands(
|
|
81
|
+
self, output_file: Path, section_globs: dict[str, str | None] | None = None
|
|
82
|
+
) -> bool:
|
|
83
|
+
"""Bundle GitHub prompt files into a single output file."""
|
|
84
|
+
commands_dir = self.commands_dir
|
|
85
|
+
if not commands_dir:
|
|
86
|
+
return False
|
|
87
|
+
|
|
88
|
+
prompts_path = output_file.parent / commands_dir
|
|
89
|
+
if not prompts_path.exists():
|
|
90
|
+
return False
|
|
91
|
+
|
|
92
|
+
command_ext = self.command_extension
|
|
93
|
+
if not command_ext:
|
|
94
|
+
return False
|
|
95
|
+
|
|
96
|
+
prompt_files = list(prompts_path.glob(f"*{command_ext}"))
|
|
97
|
+
if not prompt_files:
|
|
98
|
+
return False
|
|
99
|
+
|
|
100
|
+
prompt_dict = {}
|
|
101
|
+
for f in prompt_files:
|
|
102
|
+
base_stem = f.stem.replace(".prompt", "")
|
|
103
|
+
prompt_dict[base_stem] = f
|
|
104
|
+
|
|
105
|
+
ordered_prompts = []
|
|
106
|
+
if section_globs:
|
|
107
|
+
for section_name in section_globs.keys():
|
|
108
|
+
filename = header_to_filename(section_name)
|
|
109
|
+
if filename in prompt_dict:
|
|
110
|
+
ordered_prompts.append(prompt_dict[filename])
|
|
111
|
+
del prompt_dict[filename]
|
|
112
|
+
|
|
113
|
+
remaining_prompts = sorted(prompt_dict.values(), key=lambda p: p.name)
|
|
114
|
+
ordered_prompts.extend(remaining_prompts)
|
|
115
|
+
|
|
116
|
+
content_parts: list[str] = []
|
|
117
|
+
for prompt_file in ordered_prompts:
|
|
118
|
+
content = prompt_file.read_text().strip()
|
|
119
|
+
if not content:
|
|
120
|
+
continue
|
|
121
|
+
|
|
122
|
+
content = strip_yaml_frontmatter(content)
|
|
123
|
+
content = strip_header(content)
|
|
124
|
+
base_stem = prompt_file.stem.replace(".prompt", "")
|
|
125
|
+
header = resolve_header_from_stem(
|
|
126
|
+
base_stem, section_globs if section_globs else {}
|
|
127
|
+
)
|
|
128
|
+
content_parts.append(f"## {header}\n\n")
|
|
129
|
+
content_parts.append(content)
|
|
130
|
+
content_parts.append("\n\n")
|
|
131
|
+
|
|
132
|
+
if not content_parts:
|
|
133
|
+
return False
|
|
134
|
+
|
|
135
|
+
output_file.write_text("".join(content_parts))
|
|
136
|
+
return True
|
|
137
|
+
|
|
138
|
+
def write_rule(
|
|
139
|
+
self,
|
|
140
|
+
content_lines: list[str],
|
|
141
|
+
filename: str,
|
|
142
|
+
rules_dir: Path,
|
|
143
|
+
glob_pattern: str | None = None,
|
|
144
|
+
) -> None:
|
|
145
|
+
"""Write a GitHub instruction file (.instructions.md) with YAML frontmatter."""
|
|
146
|
+
extension = self.rule_extension or ".instructions.md"
|
|
147
|
+
filepath = rules_dir / f"{filename}{extension}"
|
|
148
|
+
|
|
149
|
+
if glob_pattern and glob_pattern != "manual":
|
|
150
|
+
header_yaml = f"""---
|
|
151
|
+
applyTo: "{glob_pattern}"
|
|
152
|
+
---
|
|
153
|
+
"""
|
|
154
|
+
else:
|
|
155
|
+
header_yaml = ""
|
|
156
|
+
|
|
157
|
+
write_rule_file(filepath, header_yaml, content_lines)
|
|
158
|
+
|
|
159
|
+
def write_command(
|
|
160
|
+
self,
|
|
161
|
+
content_lines: list[str],
|
|
162
|
+
filename: str,
|
|
163
|
+
commands_dir: Path,
|
|
164
|
+
section_name: str | None = None,
|
|
165
|
+
) -> None:
|
|
166
|
+
"""Write a GitHub prompt file (.prompt.md) with YAML frontmatter."""
|
|
167
|
+
extension = self.command_extension or ".prompt.md"
|
|
168
|
+
filepath = commands_dir / f"{filename}{extension}"
|
|
169
|
+
|
|
170
|
+
description, filtered_content = extract_description_and_filter_content(
|
|
171
|
+
content_lines, ""
|
|
172
|
+
)
|
|
173
|
+
|
|
174
|
+
frontmatter = f"---\nmode: 'agent'\ndescription: '{description}'\n---\n"
|
|
175
|
+
filepath.write_text(frontmatter + "".join(filtered_content))
|
|
176
|
+
|
|
177
|
+
def write_general_instructions(
|
|
178
|
+
self, content_lines: list[str], base_dir: Path
|
|
179
|
+
) -> None:
|
|
180
|
+
"""Write the general copilot-instructions.md file (no frontmatter)."""
|
|
181
|
+
filepath = base_dir / ".github" / "copilot-instructions.md"
|
|
182
|
+
write_rule_file(filepath, "", content_lines)
|
|
183
|
+
|
|
184
|
+
def transform_mcp_server(self, server: McpServer) -> dict:
|
|
185
|
+
"""Transform unified server to GitHub Copilot format (adds type and tools)."""
|
|
186
|
+
base: dict = {"tools": ["*"]}
|
|
187
|
+
if server.env:
|
|
188
|
+
base["env"] = server.env
|
|
189
|
+
|
|
190
|
+
if server.url:
|
|
191
|
+
return {"type": "http", "url": server.url, **base}
|
|
192
|
+
|
|
193
|
+
return {
|
|
194
|
+
"type": "local",
|
|
195
|
+
"command": server.command,
|
|
196
|
+
"args": server.args or [],
|
|
197
|
+
**base,
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
def reverse_transform_mcp_server(self, name: str, config: dict) -> McpServer:
|
|
201
|
+
"""Transform GitHub Copilot config back to unified format."""
|
|
202
|
+
if config.get("type") == "http":
|
|
203
|
+
return McpServer(
|
|
204
|
+
url=config["url"],
|
|
205
|
+
env=config.get("env"),
|
|
206
|
+
)
|
|
207
|
+
|
|
208
|
+
return McpServer(
|
|
209
|
+
command=config["command"],
|
|
210
|
+
args=config.get("args", []),
|
|
211
|
+
env=config.get("env"),
|
|
212
|
+
)
|