Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -139,13 +139,15 @@ Defined in `targets.py` TARGETS dict. Each assistant has different output format
| Assistant | Skills | Commands | Agents |
|-----------|--------|----------|--------|
| claude-code | `.claude/skills/<skill>/SKILL.md` | `.claude/commands/<cmd>.md` | `.claude/agents/<agent>.md` |
| copilot | `.github/skills/<skill>/SKILL.md` (project) / `~/.copilot/skills/<skill>/SKILL.md` (user) | `.github/prompts/<cmd>.prompt.md` (project) / `~/.copilot/prompts/<cmd>.prompt.md` (user) | `.github/agents/<agent>.agent.md` (project) / `~/.copilot/agents/<agent>.agent.md` (user) |
| cursor | `.cursor/skills/<skill>/SKILL.md` | `.cursor/commands/<cmd>.md` | `.cursor/agents/<agent>.md` |
| gemini-cli | `GEMINI.md` (managed section) | `.gemini/commands/<cmd>.toml` | N/A |
| openclaw | `~/.openclaw/workspace/skills/<skill>/SKILL.md` | N/A | N/A |
| opencode | `AGENTS.md` (managed section) | `.opencode/commands/<cmd>.md` | `.opencode/agents/<agent>.md` |

Agent frontmatter is modified during generation:
- Claude Code: `name` (agent name) and `model: inherit` are added
- Copilot: `generate_agent` is passthrough (content copied as-is); skill frontmatter is rewritten to include `name` and `description`
- Cursor: `name` (agent name) and `model: inherit` are added
Comment thread
coderabbitai[bot] marked this conversation as resolved.
- OpenCode: `mode: subagent` is added

Expand Down
3 changes: 3 additions & 0 deletions src/lola/targets/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@

# Concrete target implementations
from lola.targets.claude_code import ClaudeCodeTarget
from lola.targets.copilot import CopilotTarget
from lola.targets.cursor import CursorTarget
from lola.targets.gemini import GeminiTarget, _convert_to_gemini_args
from lola.targets.openclaw import OpenClawTarget
Expand All @@ -49,6 +50,7 @@

TARGETS: dict[str, AssistantTarget] = {
"claude-code": ClaudeCodeTarget(),
"copilot": CopilotTarget(),
"cursor": CursorTarget(),
"gemini-cli": GeminiTarget(),
"openclaw": OpenClawTarget(),
Expand Down Expand Up @@ -76,6 +78,7 @@ def get_target(assistant: str) -> AssistantTarget:
"MCPSupportMixin",
# Concrete targets
"ClaudeCodeTarget",
"CopilotTarget",
"CursorTarget",
"GeminiTarget",
"OpenClawTarget",
Expand Down
198 changes: 198 additions & 0 deletions src/lola/targets/copilot.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,198 @@
"""GitHub Copilot target implementation."""

from __future__ import annotations

from pathlib import Path

import lola.config as config
import lola.frontmatter as fm
from .base import (
BaseAssistantTarget,
ManagedInstructionsTarget,
MCPSupportMixin,
_generate_passthrough_command,
)


class CopilotTarget(MCPSupportMixin, ManagedInstructionsTarget, BaseAssistantTarget):
"""Target for GitHub Copilot (VS Code + Visual Studio).

Copilot supports:
- Skills in .copilot/skills/<name>/SKILL.md (with name+description frontmatter)
- Prompt files in .github/prompts/*.prompt.md
- Agents in .github/agents/*.agent.md
- Global instructions in .github/copilot-instructions.md
- MCP servers in .github/copilot/mcp.json
"""

name = "copilot"
supports_agents = True
INSTRUCTIONS_FILE = "copilot-instructions.md"

def get_skill_path(self, project_path: str, scope: str = "project") -> Path:
if scope == "user":
return Path.home() / ".copilot" / "skills"
return Path(project_path) / ".github" / "skills"

def get_command_path(self, project_path: str, scope: str = "project") -> Path:
if scope == "user":
return Path.home() / ".copilot" / "prompts"
return Path(project_path) / ".github" / "prompts"

def get_agent_path(self, project_path: str, scope: str = "project") -> Path:
if scope == "user":
return Path.home() / ".copilot" / "agents"
return Path(project_path) / ".github" / "agents"

def get_instructions_path(self, project_path: str, scope: str = "project") -> Path:
if scope == "user":
return Path.home() / ".copilot" / self.INSTRUCTIONS_FILE
return Path(project_path) / ".github" / self.INSTRUCTIONS_FILE

def get_mcp_path(self, project_path: str, scope: str = "project") -> Path:
if scope == "user":
return Path.home() / ".copilot" / "mcp.json"
return Path(project_path) / ".github" / "copilot" / "mcp.json"

def generate_skill(
self,
source_path: Path,
dest_path: Path,
skill_name: str,
project_path: str | None = None, # noqa: ARG002
) -> bool:
"""Generate SKILL.md in .copilot/skills/<name>/ directory.

Copilot skills use a directory-per-skill structure with
name + description in YAML frontmatter.
"""
if not source_path.exists():
return False

skill_file = source_path / config.SKILL_FILE
if not skill_file.exists():
return False

content = skill_file.read_text()
frontmatter, body = fm.parse(content)

description = frontmatter.get("description")
if not description:
return False

skill_dir = dest_path / skill_name
skill_dir.mkdir(parents=True, exist_ok=True)

# Build Copilot-compatible frontmatter (requires name + description)
import yaml

copilot_fm: dict = {
"name": skill_name,
"description": description,
}
if frontmatter.get("applyTo"):
copilot_fm["applyTo"] = frontmatter["applyTo"]
elif frontmatter.get("globs"):
copilot_fm["applyTo"] = frontmatter["globs"]

Comment thread
coderabbitai[bot] marked this conversation as resolved.
fm_str = yaml.dump(
copilot_fm, default_flow_style=False, sort_keys=False
).rstrip()
output = f"---\n{fm_str}\n---\n{body}"

dest_file = skill_dir / "SKILL.md"
dest_file.write_text(output)
return True

def remove_skill(self, dest_path: Path, skill_name: str) -> bool:
"""Remove a skill's directory."""
import shutil

removed = False
skill_dir = dest_path / skill_name
if skill_dir.exists():
shutil.rmtree(skill_dir)
removed = True
# Legacy cleanup: old .instructions.md format
legacy_file = (
dest_path.parent / "instructions" / f"{skill_name}.instructions.md"
)
Comment thread
coderabbitai[bot] marked this conversation as resolved.
if legacy_file.exists():
legacy_file.unlink()
removed = True
return removed

def generate_command(
self,
source_path: Path,
dest_dir: Path,
cmd_name: str,
module_name: str,
) -> bool:
filename = self.get_command_filename(module_name, cmd_name)
return _generate_passthrough_command(source_path, dest_dir, filename)

def get_command_filename(self, module_name: str, cmd_name: str) -> str: # noqa: ARG002
"""Copilot uses .prompt.md extension for commands."""
return f"{cmd_name}.prompt.md"

def generate_agent(
self,
source_path: Path,
dest_dir: Path,
agent_name: str,
module_name: str,
) -> bool:
"""Generate agent file with .agent.md extension.

Copilot agents use YAML frontmatter with fields like:
- description: when to use this agent
- tools: list of tools the agent can use
"""
if not source_path.exists():
return False
dest_dir.mkdir(parents=True, exist_ok=True)

filename = self.get_agent_filename(module_name, agent_name)
content = source_path.read_text()

(dest_dir / filename).write_text(content)
return True

def get_agent_filename(self, module_name: str, agent_name: str) -> str: # noqa: ARG002
"""Copilot uses .agent.md extension for agents."""
return f"{agent_name}.agent.md"

def remove_command(
self,
dest_dir: Path,
cmd_name: str,
module_name: str,
) -> bool:
"""Delete command file (.prompt.md)."""
filename = self.get_command_filename(module_name, cmd_name)
cmd_file = dest_dir / filename
if cmd_file.exists():
cmd_file.unlink()
# Legacy cleanup
legacy_file = dest_dir / f"{module_name}.{cmd_name}.prompt.md"
if legacy_file.exists():
legacy_file.unlink()
return True

def remove_agent(
self,
dest_dir: Path,
agent_name: str,
module_name: str,
) -> bool:
"""Delete agent file (.agent.md)."""
filename = self.get_agent_filename(module_name, agent_name)
agent_file = dest_dir / filename
if agent_file.exists():
agent_file.unlink()
# Legacy cleanup
legacy_file = dest_dir / f"{module_name}.{agent_name}.agent.md"
if legacy_file.exists():
legacy_file.unlink()
return True
Loading
Loading