Skip to content
Merged
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
12 changes: 12 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -139,13 +139,25 @@ 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-cli | `.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) |
| copilot-vscode | `.github/skills/<skill>/SKILL.md` (project) / `~/.copilot/skills/<skill>/SKILL.md` (user) | `.github/prompts/<cmd>.prompt.md` (project only) | `.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` |

`copilot-cli` and `copilot-vscode` share the same `.github/` (project) and
`~/.copilot/` (user) files and differ only in MCP handling: `copilot-cli` writes
MCP servers with the `mcpServers` key (`~/.copilot/mcp-config.json` at user
scope), while `copilot-vscode` writes them to `.vscode/mcp.json` using VS Code's
`servers` key. VS Code has no user-scope location for slash commands or MCP, so
those are skipped (with a warning) when installing `copilot-vscode` at user
scope. When no assistant is selected explicitly, `copilot-vscode` is preferred
over `copilot-cli` to avoid writing the same project files twice.

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
28 changes: 22 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,28 @@ Lola is a universal AI Package Manager. If an agent's skills were an RPM, Lola i

## Supported AI Assistants

| Assistant | Skills | Commands | Agents |
| ----------- | ------ | -------- | ------ |
| Claude Code | Yes | Yes | Yes |
| Cursor | Yes | Yes | Yes |
| Gemini CLI | Yes | Yes | N/A |
| OpenCode | Yes | Yes | Yes |
| Assistant | Skills | Commands | Agents |
| ----------------- | ------ | -------- | ------ |
| Claude Code | Yes | Yes | Yes |
| Copilot CLI | Yes | Yes | Yes |
| Copilot (VS Code) | Yes | Yes¹ | Yes |
| Cursor | Yes | Yes | Yes |
| Gemini CLI | Yes | Yes | N/A |
| OpenCode | Yes | Yes | Yes |

¹ Project scope only — VS Code has no user-scope location for slash commands.

GitHub Copilot has two targets that share the same `.github/` (project) and
`~/.copilot/` (user) files but differ in MCP handling:

- `copilot-cli` - writes MCP servers using the `mcpServers` key
(`~/.copilot/mcp-config.json` for user scope).
- `copilot-vscode` - writes MCP servers to `.vscode/mcp.json` using VS
Code's `servers` key. VS Code has no user-scope location for slash commands
or MCP, so those are skipped (with a warning) when installing at user scope.

When no assistant is selected explicitly, `copilot-vscode` is preferred over
`copilot-cli` to avoid writing the same project files twice.

## Installation

Expand Down
29 changes: 19 additions & 10 deletions src/lola/cli/install.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@
_get_skill_description,
_skill_source_dir,
copy_module_to_local,
default_assistants,
get_registry,
get_target,
install_to_assistant,
Expand Down Expand Up @@ -295,6 +296,8 @@ def _remove_orphaned_commands(ctx: UpdateContext, verbose: bool) -> int:
path_context = ctx.inst.project_path or ""
scope = ctx.inst.scope
command_dest = ctx.target.get_command_path(path_context, scope)
if command_dest is None:
return 0
for cmd_name in ctx.orphaned_commands:
if ctx.target.remove_command(command_dest, cmd_name, ctx.inst.module_name):
removed += 1
Expand Down Expand Up @@ -458,6 +461,8 @@ def _update_commands(ctx: UpdateContext, verbose: bool) -> tuple[int, int]:
path_context = ctx.inst.project_path or ""
scope = ctx.inst.scope
command_dest = ctx.target.get_command_path(path_context, scope)
if command_dest is None:
return 0, 0
content_path = _get_content_path(ctx.source_module)
commands_dir = content_path / "commands"

Expand Down Expand Up @@ -907,8 +912,9 @@ def install_cmd(
raise SystemExit(130)
assistants_to_install = chosen
else:
# Non-interactive: preserve original default (all assistants)
assistants_to_install = list(TARGETS.keys())
# Non-interactive: default to all assistants (collapsing the copilot
# variants to the project-granular copilot-vscode to avoid collisions).
assistants_to_install = default_assistants()

# Resolve hooks with precedence: CLI flags > module lola.yaml > marketplace
effective_pre_install = (
Expand Down Expand Up @@ -1163,14 +1169,17 @@ def uninstall_cmd(
if inst.commands:
command_dest = target.get_command_path(path_context, inst_scope)

for cmd_name in inst.commands:
if target.remove_command(command_dest, cmd_name, module_name):
removed_count += 1
if verbose:
filename = target.get_command_filename(module_name, cmd_name)
console.print(
f" [green]Removed {command_dest / filename}[/green]"
)
if command_dest:
for cmd_name in inst.commands:
if target.remove_command(command_dest, cmd_name, module_name):
removed_count += 1
if verbose:
filename = target.get_command_filename(
module_name, cmd_name
)
console.print(
f" [green]Removed {command_dest / filename}[/green]"
)

# Remove agent files
if inst.agents:
Expand Down
4 changes: 2 additions & 2 deletions src/lola/cli/sync.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
from lola.config import MODULES_DIR, MARKET_DIR, CACHE_DIR
from lola.sync import load_lolareq, ModuleSpec
from lola.cli.mod import load_registered_module, save_source_info
from lola.targets import get_registry, TARGETS
from lola.targets import get_registry, TARGETS, default_assistants
from lola.targets.install import install_to_assistant
from lola.market.manager import parse_market_ref, MarketplaceRegistry
from lola.parsers import detect_source_type, fetch_module, fetch_module_as_name
Expand Down Expand Up @@ -197,7 +197,7 @@ def sync_module_spec(
raise ValueError(f"Unknown assistant in fragment: {asst}")
target_assistants = unique_assistants
else:
target_assistants = list(TARGETS.keys())
target_assistants = default_assistants()

# Check if already installed
already_installed_assistants = {inst.assistant for inst in project_installations}
Expand Down
23 changes: 23 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 CopilotCliTarget, CopilotVSCodeTarget
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,8 @@

TARGETS: dict[str, AssistantTarget] = {
"claude-code": ClaudeCodeTarget(),
"copilot-cli": CopilotCliTarget(),
"copilot-vscode": CopilotVSCodeTarget(),
"cursor": CursorTarget(),
"gemini-cli": GeminiTarget(),
"openclaw": OpenClawTarget(),
Expand All @@ -67,6 +70,23 @@ def get_target(assistant: str) -> AssistantTarget:
return TARGETS[assistant]


# Targets skipped when expanding to "all assistants" implicitly (sync, or a
# non-interactive install with no -a). copilot-cli and copilot-vscode write the
# same project-scope .github/ files and differ only in MCP handling, so an
# implicit install of both collides. We prefer the project-granular
# copilot-vscode; copilot-cli remains explicitly selectable via -a.
_IMPLICIT_ALL_EXCLUDE = {"copilot-cli"}


def default_assistants() -> list[str]:
"""Assistant names to use when none is explicitly selected.

Excludes targets that would collide with a more granular sibling when
installed implicitly (see ``_IMPLICIT_ALL_EXCLUDE``).
"""
return [name for name in TARGETS if name not in _IMPLICIT_ALL_EXCLUDE]


__all__ = [
# ABC and base classes
"AssistantTarget",
Expand All @@ -76,6 +96,8 @@ def get_target(assistant: str) -> AssistantTarget:
"MCPSupportMixin",
# Concrete targets
"ClaudeCodeTarget",
"CopilotCliTarget",
"CopilotVSCodeTarget",
"CursorTarget",
"GeminiTarget",
"OpenClawTarget",
Expand All @@ -84,6 +106,7 @@ def get_target(assistant: str) -> AssistantTarget:
"TARGETS",
"get_target",
"get_registry",
"default_assistants",
# Install functions
"console",
"copy_module_to_local",
Expand Down
6 changes: 5 additions & 1 deletion src/lola/targets/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,9 +61,13 @@ def get_skill_path(self, project_path: str, scope: str = "project") -> Path:
...

@abstractmethod
def get_command_path(self, project_path: str, scope: str = "project") -> Path:
def get_command_path(
self, project_path: str, scope: str = "project"
) -> Path | None:
"""Get the command output path for this assistant.

Returns None if commands are not supported for the given scope.

Args:
project_path: Project directory path (ignored for user scope)
scope: "project" for project-local, "user" for user-global
Expand Down
Loading
Loading