-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathcopilot.py
More file actions
188 lines (159 loc) · 6.85 KB
/
copilot.py
File metadata and controls
188 lines (159 loc) · 6.85 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
"""GitHub Copilot applier — writes instructions and MCP configs."""
import json
import os
import stat
from pathlib import Path
from typing import Dict, List
from appliers.base import BaseApplier
from appliers.manifest import ToolManifest
def _copilot_instructions() -> Path:
return Path.cwd() / ".github" / "copilot-instructions.md"
def _copilot_instructions_dir() -> Path:
return Path.cwd() / ".github" / "instructions"
def _vscode_mcp_json() -> Path:
return Path.cwd() / ".vscode" / "mcp.json"
# Module-level aliases kept for backward compatibility with extractors
# (evaluated lazily through the functions above inside the class methods)
COPILOT_INSTRUCTIONS = Path(".github") / "copilot-instructions.md"
COPILOT_INSTRUCTIONS_DIR = Path(".github") / "instructions"
VSCODE_MCP_JSON = Path(".vscode") / "mcp.json"
COPILOT_MEMORY_SCHEMA = """
GitHub Copilot reads custom instructions from two locations:
1. .github/copilot-instructions.md — Repository-wide instructions.
- Plain Markdown, no frontmatter required.
- Automatically attached to every Copilot Chat request in the repository.
- Does NOT affect inline code completion (autocomplete).
- Use headings (##) to organize sections, bullet points for individual rules.
- Keep instructions concise and actionable.
- Example content:
## Project Standards
- Use TypeScript strict mode for all new files
- Follow PEP 8 for Python files
- All API endpoints must include error handling
2. .github/instructions/*.instructions.md — Path-specific instructions.
- Markdown files with YAML frontmatter containing an `applyTo` glob pattern.
- Only included when Copilot is working on files matching the pattern.
- Frontmatter format:
---
applyTo: "**/*.py"
---
- Glob patterns: "**/*.py" (all Python files), "src/**/*.ts" (TS under src/),
"**/*.ts,**/*.tsx" (comma-separated for multiple patterns).
- Name files descriptively: python.instructions.md, testing.instructions.md.
- Example:
---
applyTo: "**/*.py"
---
## Python Conventions
- Use type hints for all function parameters and return values
- Use pytest for all test files
WHAT TO PUT IN INSTRUCTIONS:
- Coding style preferences (language, formatting, naming conventions)
- Architecture decisions and patterns
- Framework-specific guidance
- Testing conventions
- Things to avoid
WHAT NOT TO PUT IN INSTRUCTIONS:
- Personal information (name, timezone)
- Entire style guides — use a linter
- Common tool commands — Copilot already knows these
GUIDELINES:
- Put universal project rules in copilot-instructions.md.
- Put language/path-specific rules in .github/instructions/ with applyTo globs.
- Both are combined when both match — they don't replace each other.
- Keep each file focused on one topic.
OUTPUT: Write files as described above. Use copilot-instructions.md for general rules.
Use .github/instructions/<topic>.instructions.md with applyTo frontmatter for
language or path-specific rules.
"""
class CopilotApplier(BaseApplier):
TOOL_NAME = "github-copilot"
MEMORY_SCHEMA = COPILOT_MEMORY_SCHEMA
@property # type: ignore[override]
def MEMORY_ALLOWED_BASE(self) -> "Path": # noqa: N802
# Copilot writes to .github/ / .vscode/ in the current project directory.
# Using the resolved CWD ensures a stable absolute path even if the
# calling process later changes directory (#42).
return Path.cwd().resolve()
def apply_skills(self, skills: List[Dict], manifest: ToolManifest) -> int:
count = 0
instructions = _copilot_instructions()
for skill in skills:
if skill.get("name") == "copilot-instructions":
instructions.parent.mkdir(parents=True, exist_ok=True)
content = skill.get("body", "")
instructions.write_text(content, encoding="utf-8")
manifest.record_skill(
"copilot-instructions",
file_path=str(instructions.resolve()),
content=content,
)
count += 1
return count
def apply_mcp_servers(
self,
servers: List[Dict],
secrets: Dict[str, str],
manifest: ToolManifest,
override: bool = False,
) -> int:
vscode_mcp = _vscode_mcp_json()
if vscode_mcp.exists():
try:
data = json.loads(vscode_mcp.read_text(encoding="utf-8"))
except json.JSONDecodeError:
data = {}
else:
data = {}
if override:
vscode_servers = {}
else:
vscode_servers = data.get("servers", {})
# Prune orphaned MCP servers
if not manifest.is_first_sync:
current_names = {s.get("name", "unnamed") for s in servers}
for orphan in set(manifest.managed_mcp_names()) - current_names:
vscode_servers.pop(orphan, None)
manifest.remove_mcp_server(orphan)
count = 0
for server in servers:
name = server.get("name", "unnamed")
env = server.get("env", {}).copy()
for key, value in env.items():
if isinstance(value, str) and value.startswith("${") and value.endswith("}"):
secret_name = value[2:-1]
if secret_name in secrets:
env[key] = secrets[secret_name]
vscode_servers[name] = {
"type": server.get("transport", "stdio"),
"command": server.get("command", ""),
"args": server.get("args", []),
}
if env:
vscode_servers[name]["env"] = env
manifest.record_mcp_server(name)
count += 1
data["servers"] = vscode_servers
vscode_mcp.parent.mkdir(parents=True, exist_ok=True)
vscode_mcp.write_text(json.dumps(data, indent=2), encoding="utf-8")
# Restrict to owner-only since the file may contain resolved API keys (#32)
os.chmod(vscode_mcp, stat.S_IRUSR | stat.S_IWUSR)
return count
def _read_existing_memory_files(self) -> Dict[str, str]:
"""Return {file_path: content} for Copilot's instruction files."""
result = {}
instructions = _copilot_instructions()
instructions_dir = _copilot_instructions_dir()
if instructions.exists():
try:
result[str(instructions.resolve())] = instructions.read_text(encoding="utf-8")
except IOError:
pass
if instructions_dir.exists():
for path in instructions_dir.glob("*.instructions.md"):
if path.is_file():
try:
result[str(path.resolve())] = path.read_text(encoding="utf-8")
except IOError:
pass
return result