Skip to content

Commit 67aeeb5

Browse files
committed
feat(agent): add Factory Droid agent support
This commit introduces a new Factory Droid agent to the system. It adds the agent to the list of available agents and includes matching logic for various name aliases. The implementation provides full MCP server management capabilities including installation, removal, and listing of MCP servers like Playwright, Context7, and Remix Icon. It also integrates with the existing skills and plugins systems. This enables users to utilize Factory Droid as another agent option within the application, expanding the range of supported AI agents.
1 parent 34c3ff9 commit 67aeeb5

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)