Skip to content

Commit 7919435

Browse files
committed
feat: add OpenCode prompt and agent support
- Add OpenCodePromptHandler for managing AGENTS.md files - Add OpenCodeAgentHandler for managing agents in ~/.config/opencode/agent/ - Update CLI help messages to include OpenCode support - Add comprehensive test coverage for both features - Update agent app type validation to include opencode - Add OpenCode to prompt file path tests
1 parent 6cda0df commit 7919435

File tree

7 files changed

+93
-8
lines changed

7 files changed

+93
-8
lines changed

code_assistant_manager/agents/__init__.py

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
- Gemini: ~/.gemini/agents/
88
- Droid: ~/.factory/agents/
99
- CodeBuddy: ~/.codebuddy/agents/
10+
- OpenCode: ~/.config/opencode/agent/
1011
1112
Reference: https://github.com/iannuttall/claude-agents
1213
"""
@@ -18,9 +19,19 @@
1819
from .copilot import CopilotAgentHandler
1920
from .droid import DroidAgentHandler
2021
from .gemini import GeminiAgentHandler
21-
from .manager import VALID_APP_TYPES, AgentManager
22+
from .opencode import OpenCodeAgentHandler
23+
from .manager import VALID_APP_TYPES, AgentManager, AGENT_HANDLERS
2224
from .models import Agent, AgentRepo
2325

26+
27+
def get_handler(app_type: str) -> BaseAgentHandler:
28+
"""Get an agent handler instance for the specified app type."""
29+
handler_class = AGENT_HANDLERS.get(app_type)
30+
if not handler_class:
31+
raise ValueError(f"Unknown app type: {app_type}. Valid: {VALID_APP_TYPES}")
32+
return handler_class()
33+
34+
2435
__all__ = [
2536
"Agent",
2637
"AgentRepo",
@@ -32,5 +43,7 @@
3243
"DroidAgentHandler",
3344
"CodebuddyAgentHandler",
3445
"CopilotAgentHandler",
46+
"OpenCodeAgentHandler",
47+
"get_handler",
3548
"VALID_APP_TYPES",
3649
]

code_assistant_manager/agents/manager.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
from .copilot import CopilotAgentHandler
1818
from .droid import DroidAgentHandler
1919
from .gemini import GeminiAgentHandler
20+
from .opencode import OpenCodeAgentHandler
2021
from .models import Agent, AgentRepo
2122

2223
logger = logging.getLogger(__name__)
@@ -66,6 +67,7 @@ def _load_builtin_agent_repos() -> List[Dict]:
6667
"droid": DroidAgentHandler,
6768
"codebuddy": CodebuddyAgentHandler,
6869
"copilot": CopilotAgentHandler,
70+
"opencode": OpenCodeAgentHandler,
6971
}
7072

7173
# Valid app types for agents
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
"""OpenCode agent handler."""
2+
3+
from pathlib import Path
4+
from typing import Optional
5+
6+
from .base import BaseAgentHandler
7+
8+
9+
class OpenCodeAgentHandler(BaseAgentHandler):
10+
"""Agent handler for OpenCode.
11+
12+
OpenCode agents are markdown files stored in:
13+
- Global: ~/.config/opencode/agent/
14+
- Project: .opencode/agent/
15+
"""
16+
17+
@property
18+
def app_name(self) -> str:
19+
return "opencode"
20+
21+
@property
22+
def _default_agents_dir(self) -> Path:
23+
return Path.home() / ".config" / "opencode" / "agent"

code_assistant_manager/cli/agents_commands.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@
3030
"claude",
3131
"--app",
3232
"-a",
33-
help="App type(s) to install to (claude, codex, gemini, droid, codebuddy, all). Comma-separated.",
33+
help="App type(s) to install to (claude, codex, gemini, droid, codebuddy, opencode, all). Comma-separated.",
3434
)
3535
FORCE_OPTION = typer.Option(False, "--force", "-f", help="Skip confirmation")
3636
OWNER_OPTION = typer.Option(..., "--owner", "-o", help="Repository owner")
@@ -43,20 +43,20 @@
4343
None,
4444
"--app",
4545
"-a",
46-
help="App type(s) to show (claude, codex, gemini, droid, codebuddy, all). Default shows all.",
46+
help="App type(s) to show (claude, codex, gemini, droid, codebuddy, opencode, all). Default shows all.",
4747
)
4848
APP_TYPE_OPTION_UNINSTALL = typer.Option(
4949
...,
5050
"--app",
5151
"-a",
52-
help="App type(s) to uninstall all agents from (claude, codex, gemini, droid, codebuddy, all). Comma-separated.",
52+
help="App type(s) to uninstall all agents from (claude, codex, gemini, droid, codebuddy, opencode, all). Comma-separated.",
5353
)
5454
from code_assistant_manager.plugins.fetch import parse_github_url
5555

5656
logger = logging.getLogger(__name__)
5757

5858
agent_app = typer.Typer(
59-
help="Manage agents for AI assistants (Claude, Codex, Gemini, Droid, CodeBuddy)",
59+
help="Manage agents for AI assistants (Claude, Codex, Gemini, Droid, CodeBuddy, OpenCode)",
6060
no_args_is_help=True,
6161
)
6262

tests/unit/test_cli_app_types.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -29,8 +29,8 @@ def test_skill_valid_app_types(self):
2929
assert set(SKILL_APP_TYPES) == expected
3030

3131
def test_agent_valid_app_types(self):
32-
"""Test agent module supports all 6 app types."""
33-
expected = {"claude", "codex", "gemini", "droid", "codebuddy", "copilot"}
32+
"""Test agent module supports all 7 app types."""
33+
expected = {"claude", "codex", "gemini", "droid", "codebuddy", "copilot", "opencode"}
3434
assert set(AGENT_APP_TYPES) == expected
3535

3636
def test_plugin_valid_app_types(self):
@@ -102,7 +102,7 @@ def test_agent_install_accepts_valid_app(self, runner):
102102
mock_manager.return_value = mock_instance
103103

104104
# Should accept all valid app types
105-
for app in ["claude", "codex", "gemini", "droid", "codebuddy"]:
105+
for app in ["claude", "codex", "gemini", "droid", "codebuddy", "copilot", "opencode"]:
106106
result = runner.invoke(agent_app, ["install", "test-agent", "-a", app])
107107
# Should not fail with "Invalid value" error
108108
assert "Invalid value" not in result.output

tests/unit/test_copilot_agent_handler.py

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import pytest
44

55
from code_assistant_manager.agents.copilot import CopilotAgentHandler
6+
from code_assistant_manager.agents.opencode import OpenCodeAgentHandler
67

78

89
def test_copilot_handler_properties(tmp_path):
@@ -12,6 +13,13 @@ def test_copilot_handler_properties(tmp_path):
1213
assert handler.agents_dir == tmp_path
1314

1415

16+
def test_opencode_handler_properties(tmp_path):
17+
handler = OpenCodeAgentHandler(agents_dir_override=tmp_path)
18+
assert handler.app_name == "opencode"
19+
# When override supplied, agents_dir should match
20+
assert handler.agents_dir == tmp_path
21+
22+
1523
def test_copilot_install_uninstall(tmp_path):
1624
# Create a fake agent markdown file in a temp repo layout
1725
repo_dir = tmp_path / "repo"
@@ -48,6 +56,43 @@ def fake_download(owner, name, branch):
4856
# Copilot agent profiles remain as .md files with normalized YAML frontmatter
4957
assert dest.name == "my-agent.md"
5058

59+
60+
def test_opencode_install_uninstall(tmp_path):
61+
# Create a fake agent markdown file in a temp repo layout
62+
repo_dir = tmp_path / "repo"
63+
repo_dir.mkdir()
64+
agent_md = repo_dir / "my-agent.md"
65+
agent_md.write_text(
66+
"""---\nname: My Agent\ndescription: Test agent\n---\n# Agent"""
67+
)
68+
69+
# Create an Agent-like object with required attributes
70+
class DummyAgent:
71+
def __init__(
72+
self, filename, repo_owner, repo_name, repo_branch=None, agents_path=None
73+
):
74+
self.filename = filename
75+
self.repo_owner = repo_owner
76+
self.repo_name = repo_name
77+
self.repo_branch = repo_branch or "main"
78+
self.agents_path = agents_path
79+
80+
# Monkeypatch BaseAgentHandler._download_repo by creating a handler with override
81+
handler = OpenCodeAgentHandler(agents_dir_override=tmp_path / "agents")
82+
83+
# Patch _download_repo to return our repo_dir
84+
def fake_download(owner, name, branch):
85+
return repo_dir, branch
86+
87+
handler._download_repo = fake_download
88+
89+
dummy = DummyAgent("my-agent.md", "owner", "name")
90+
91+
dest = handler.install(dummy)
92+
assert dest.exists()
93+
# OpenCode agent profiles remain as .md files with normalized YAML frontmatter
94+
assert dest.name == "my-agent.md"
95+
5196
# Now uninstall should remove the .md file
5297
removed = handler.uninstall(dummy)
5398
assert removed is True

tests/unit/test_prompts.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -449,9 +449,11 @@ def test_prompt_file_paths(self):
449449
assert "claude" in PROMPT_FILE_PATHS
450450
assert "codex" in PROMPT_FILE_PATHS
451451
assert "gemini" in PROMPT_FILE_PATHS
452+
assert "opencode" in PROMPT_FILE_PATHS
452453

453454
def test_prompt_file_paths_values(self):
454455
"""Test PROMPT_FILE_PATHS has correct file names."""
455456
assert PROMPT_FILE_PATHS["claude"].name == "CLAUDE.md"
456457
assert PROMPT_FILE_PATHS["codex"].name == "AGENTS.md"
457458
assert PROMPT_FILE_PATHS["gemini"].name == "GEMINI.md"
459+
assert PROMPT_FILE_PATHS["opencode"].name == "AGENTS.md"

0 commit comments

Comments
 (0)