Skip to content

Commit 9a990de

Browse files
authored
Merge pull request #29 from agentsdance/droid
feat(agent): add Factory Droid agent support
2 parents 34c3ff9 + 67aeeb5 commit 9a990de

4 files changed

Lines changed: 348 additions & 0 deletions

File tree

internal/agent/agent.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ func GetAllAgents() []Agent {
5858
NewClaudeAgent(),
5959
NewCodexAgent(),
6060
NewCursorAgent(),
61+
NewDroidAgent(),
6162
NewGeminiAgent(),
6263
NewOpenCodeAgent(),
6364
}
@@ -83,6 +84,8 @@ func matchAgentName(agentName, input string) bool {
8384
return agentName == "Codex"
8485
case "cursor":
8586
return agentName == "Cursor"
87+
case "droid", "factory", "factory-droid", "factory_droid":
88+
return agentName == "Droid"
8689
case "gemini", "geminicli", "gemini-cli", "gemini_cli":
8790
return agentName == "Gemini cli"
8891
case "opencode", "open-code", "open_code":

internal/agent/droid.go

Lines changed: 303 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,303 @@
1+
package agent
2+
3+
import (
4+
"encoding/json"
5+
"fmt"
6+
"os"
7+
"path/filepath"
8+
9+
"github.com/agentsdance/agentx/internal/plugins"
10+
"github.com/agentsdance/agentx/internal/skills"
11+
)
12+
13+
// DroidAgent represents Factory Droid agent
14+
type DroidAgent struct {
15+
configPath string
16+
}
17+
18+
// NewDroidAgent creates a new Factory Droid agent
19+
func NewDroidAgent() *DroidAgent {
20+
home, _ := os.UserHomeDir()
21+
return &DroidAgent{
22+
configPath: filepath.Join(home, ".factory", "mcp.json"),
23+
}
24+
}
25+
26+
func (a *DroidAgent) Name() string {
27+
return "Droid"
28+
}
29+
30+
func (a *DroidAgent) ConfigPath() string {
31+
return a.configPath
32+
}
33+
34+
func (a *DroidAgent) Exists() bool {
35+
// Check if .factory directory exists
36+
home, _ := os.UserHomeDir()
37+
factoryDir := filepath.Join(home, ".factory")
38+
_, err := os.Stat(factoryDir)
39+
return err == nil
40+
}
41+
42+
func (a *DroidAgent) readConfig() (map[string]interface{}, error) {
43+
data, err := os.ReadFile(a.configPath)
44+
if err != nil {
45+
if os.IsNotExist(err) {
46+
return map[string]interface{}{}, nil
47+
}
48+
return nil, err
49+
}
50+
var cfg map[string]interface{}
51+
if err := json.Unmarshal(data, &cfg); err != nil {
52+
return nil, err
53+
}
54+
return cfg, nil
55+
}
56+
57+
func (a *DroidAgent) writeConfig(cfg map[string]interface{}) error {
58+
// Ensure directory exists
59+
dir := filepath.Dir(a.configPath)
60+
if err := os.MkdirAll(dir, 0755); err != nil {
61+
return err
62+
}
63+
data, err := json.MarshalIndent(cfg, "", " ")
64+
if err != nil {
65+
return err
66+
}
67+
return os.WriteFile(a.configPath, data, 0644)
68+
}
69+
70+
func (a *DroidAgent) getMCPServers(cfg map[string]interface{}) map[string]interface{} {
71+
if servers, ok := cfg["mcpServers"].(map[string]interface{}); ok {
72+
return servers
73+
}
74+
return nil
75+
}
76+
77+
func (a *DroidAgent) ensureMCPServers(cfg map[string]interface{}) map[string]interface{} {
78+
if cfg["mcpServers"] == nil {
79+
cfg["mcpServers"] = make(map[string]interface{})
80+
}
81+
return cfg["mcpServers"].(map[string]interface{})
82+
}
83+
84+
func (a *DroidAgent) HasPlaywright() (bool, error) {
85+
cfg, err := a.readConfig()
86+
if err != nil {
87+
return false, err
88+
}
89+
servers := a.getMCPServers(cfg)
90+
if servers == nil {
91+
return false, nil
92+
}
93+
_, ok := servers["playwright"]
94+
return ok, nil
95+
}
96+
97+
func (a *DroidAgent) InstallPlaywright() error {
98+
cfg, err := a.readConfig()
99+
if err != nil {
100+
return err
101+
}
102+
servers := a.ensureMCPServers(cfg)
103+
servers["playwright"] = map[string]interface{}{
104+
"type": "stdio",
105+
"command": "npx",
106+
"args": []interface{}{"-y", "@playwright/mcp@latest"},
107+
}
108+
return a.writeConfig(cfg)
109+
}
110+
111+
func (a *DroidAgent) RemovePlaywright() error {
112+
cfg, err := a.readConfig()
113+
if err != nil {
114+
return err
115+
}
116+
servers := a.getMCPServers(cfg)
117+
if servers != nil {
118+
delete(servers, "playwright")
119+
}
120+
return a.writeConfig(cfg)
121+
}
122+
123+
func (a *DroidAgent) HasContext7() (bool, error) {
124+
cfg, err := a.readConfig()
125+
if err != nil {
126+
return false, err
127+
}
128+
servers := a.getMCPServers(cfg)
129+
if servers == nil {
130+
return false, nil
131+
}
132+
_, ok := servers["context7"]
133+
return ok, nil
134+
}
135+
136+
func (a *DroidAgent) InstallContext7() error {
137+
cfg, err := a.readConfig()
138+
if err != nil {
139+
return err
140+
}
141+
servers := a.ensureMCPServers(cfg)
142+
servers["context7"] = map[string]interface{}{
143+
"type": "stdio",
144+
"command": "npx",
145+
"args": []interface{}{"-y", "@context7/mcp@latest"},
146+
}
147+
return a.writeConfig(cfg)
148+
}
149+
150+
func (a *DroidAgent) RemoveContext7() error {
151+
cfg, err := a.readConfig()
152+
if err != nil {
153+
return err
154+
}
155+
servers := a.getMCPServers(cfg)
156+
if servers != nil {
157+
delete(servers, "context7")
158+
}
159+
return a.writeConfig(cfg)
160+
}
161+
162+
func (a *DroidAgent) HasRemixIcon() (bool, error) {
163+
cfg, err := a.readConfig()
164+
if err != nil {
165+
return false, err
166+
}
167+
servers := a.getMCPServers(cfg)
168+
if servers == nil {
169+
return false, nil
170+
}
171+
_, ok := servers["remix-icon"]
172+
return ok, nil
173+
}
174+
175+
func (a *DroidAgent) InstallRemixIcon() error {
176+
cfg, err := a.readConfig()
177+
if err != nil {
178+
return err
179+
}
180+
servers := a.ensureMCPServers(cfg)
181+
servers["remix-icon"] = map[string]interface{}{
182+
"type": "stdio",
183+
"command": "npx",
184+
"args": []interface{}{"-y", "@nicepkg/gpt-runner", "mcp", "--remix-icon"},
185+
}
186+
return a.writeConfig(cfg)
187+
}
188+
189+
func (a *DroidAgent) RemoveRemixIcon() error {
190+
cfg, err := a.readConfig()
191+
if err != nil {
192+
return err
193+
}
194+
servers := a.getMCPServers(cfg)
195+
if servers != nil {
196+
delete(servers, "remix-icon")
197+
}
198+
return a.writeConfig(cfg)
199+
}
200+
201+
func (a *DroidAgent) HasMCP(name string) (bool, error) {
202+
cfg, err := a.readConfig()
203+
if err != nil {
204+
return false, err
205+
}
206+
servers := a.getMCPServers(cfg)
207+
if servers == nil {
208+
return false, nil
209+
}
210+
_, ok := servers[name]
211+
return ok, nil
212+
}
213+
214+
func (a *DroidAgent) InstallMCP(name string, mcpConfig map[string]interface{}) error {
215+
if mcpConfig == nil {
216+
return fmt.Errorf("missing mcp config")
217+
}
218+
cfg, err := a.readConfig()
219+
if err != nil {
220+
return err
221+
}
222+
servers := a.ensureMCPServers(cfg)
223+
servers[name] = cloneMCPConfig(mcpConfig)
224+
return a.writeConfig(cfg)
225+
}
226+
227+
func (a *DroidAgent) RemoveMCP(name string) error {
228+
cfg, err := a.readConfig()
229+
if err != nil {
230+
return err
231+
}
232+
servers := a.getMCPServers(cfg)
233+
if servers != nil {
234+
delete(servers, name)
235+
}
236+
return a.writeConfig(cfg)
237+
}
238+
239+
func (a *DroidAgent) ListMCPs() (map[string]map[string]interface{}, error) {
240+
cfg, err := a.readConfig()
241+
if err != nil {
242+
return nil, err
243+
}
244+
servers := a.getMCPServers(cfg)
245+
if servers == nil {
246+
return map[string]map[string]interface{}{}, nil
247+
}
248+
result := make(map[string]map[string]interface{})
249+
for name, server := range servers {
250+
if serverMap, ok := server.(map[string]interface{}); ok {
251+
result[name] = serverMap
252+
}
253+
}
254+
return result, nil
255+
}
256+
257+
func (a *DroidAgent) SupportsSkills() bool {
258+
return true
259+
}
260+
261+
func (a *DroidAgent) HasSkill(skillName string) (bool, error) {
262+
mgr := skills.NewDroidSkillManager()
263+
skill, err := mgr.Get(skillName)
264+
if err != nil {
265+
return false, nil
266+
}
267+
return skill != nil, nil
268+
}
269+
270+
func (a *DroidAgent) InstallSkill(skillName, source string) error {
271+
mgr := skills.NewDroidSkillManager()
272+
_, err := mgr.Install(source, skills.ScopePersonal)
273+
return err
274+
}
275+
276+
func (a *DroidAgent) RemoveSkill(skillName string) error {
277+
mgr := skills.NewDroidSkillManager()
278+
return mgr.Remove(skillName, skills.ScopePersonal)
279+
}
280+
281+
func (a *DroidAgent) SupportsPlugins() bool {
282+
return true
283+
}
284+
285+
func (a *DroidAgent) HasPlugin(pluginName string) (bool, error) {
286+
mgr := plugins.NewPluginManager()
287+
plugin, err := mgr.Get(pluginName)
288+
if err != nil {
289+
return false, nil
290+
}
291+
return plugin != nil, nil
292+
}
293+
294+
func (a *DroidAgent) InstallPlugin(pluginName, source string) error {
295+
mgr := plugins.NewPluginManager()
296+
_, err := mgr.Install(source)
297+
return err
298+
}
299+
300+
func (a *DroidAgent) RemovePlugin(pluginName string) error {
301+
mgr := plugins.NewPluginManager()
302+
return mgr.Remove(pluginName)
303+
}

internal/skills/manager.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,15 @@ func NewCodexSkillManager() *DefaultSkillManager {
3333
}
3434
}
3535

36+
// NewDroidSkillManager creates a new skill manager for Factory Droid
37+
func NewDroidSkillManager() *DefaultSkillManager {
38+
return &DefaultSkillManager{
39+
getCommandsDir: nil,
40+
getSkillsDir: GetDroidSkillsDir,
41+
supportsCommands: false,
42+
}
43+
}
44+
3645
func (m *DefaultSkillManager) commandsDir(scope SkillScope) (string, error) {
3746
if m.getCommandsDir == nil {
3847
return "", fmt.Errorf("commands directory resolver not configured")

internal/skills/paths.go

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,39 @@ func GetCodexSkillsDir(scope SkillScope) (string, error) {
103103
return filepath.Join(base, "skills"), nil
104104
}
105105

106+
// GetDroidBasePaths returns the base paths for Factory Droid configuration
107+
func GetDroidBasePaths() (personal, project string, err error) {
108+
home, err := os.UserHomeDir()
109+
if err != nil {
110+
return "", "", err
111+
}
112+
113+
personal = filepath.Join(home, ".factory")
114+
115+
// Project path is relative to current directory
116+
cwd, err := os.Getwd()
117+
if err != nil {
118+
return personal, "", err
119+
}
120+
project = filepath.Join(cwd, ".factory")
121+
122+
return personal, project, nil
123+
}
124+
125+
// GetDroidSkillsDir returns the Droid skills directory for a scope
126+
func GetDroidSkillsDir(scope SkillScope) (string, error) {
127+
personal, project, err := GetDroidBasePaths()
128+
if err != nil {
129+
return "", err
130+
}
131+
132+
base := personal
133+
if scope == ScopeProject {
134+
base = project
135+
}
136+
return filepath.Join(base, "skills"), nil
137+
}
138+
106139
// EnsureDir creates a directory if it doesn't exist
107140
func EnsureDir(path string) error {
108141
return os.MkdirAll(path, 0755)

0 commit comments

Comments
 (0)