Skip to content

Commit 9c34e0a

Browse files
kpenfoundgsnaiper
authored andcommitted
cu config agent (dagger#137)
* Create env upward-mule Creating environment to implement the cu configure subcommand for automatic MCP server configuration * Update env upward-mule Adding Go development tools for implementing the configure command * Write cmd/cu/configure.go Adding configure command to support automatic MCP server configuration for different agents * Write cmd/cu/configure.go Fixed missing comma after Long field on line 13 * Update env upward-mule Updating to Go 1.24 to meet the go.mod requirements * Write cmd/cu/configure.go Removed unused imports and variables, added proper error messages for unimplemented functions * Write cmd/cu/configure.go Implementing full configuration logic for all agents based on README specifications * Write cmd/cu/configure.go Removing TOML dependency and simplifying Codex configuration to just print instructions * Write cmd/cu/configure.go Fixed syntax error in VSCode configuration function and implemented interactive agent selection * Write cmd/cu/configure.go Removed unused strings import * Write cmd/cu/configure.go Updated Goose configuration to use a generic map to preserve all YAML data * Write cmd/cu/configure.go Updated Goose configuration to use proper interface types for compatibility with YAML marshaling * reduce selections to claude, goose, cursor, codex, amazonq Signed-off-by: kpenfound <kyle@dagger.io> * Creating environment to modify the configureCodex function to handle TOML configuration updates like configureGoose does. * Adding a TOML library to handle TOML configuration files. * Updating the configure.go file to make configureCodex actually modify the TOML configuration file, similar to configureGoose. * Formatting the code to ensure it follows Go conventions. * add autoapprove to codex config Signed-off-by: kpenfound <kyle@dagger.io> * Adding bubbletea dependency for the improved interactive interface. * Creating a new file for the bubbletea interactive interface for agent selection. * Creating the bubbletea interactive UI for agent selection with a modern, colorful interface. * Updating the configure.go file to use the new bubbletea interactive interface by replacing the interactiveConfiguration function. * Formatting the Go code to ensure it follows proper style guidelines. * Removing icons from the agent selection interface - just showing agent names without icons to avoid any branding concerns. * FileRead: rename start_line_one_indexed to start_line_one_indexed_inclusive Signed-off-by: kpenfound <kyle@dagger.io> * FileRead: improve invalid range error handling Signed-off-by: kpenfound <kyle@dagger.io> * update agent descriptions Signed-off-by: kpenfound <kyle@dagger.io> * embedded rules files, loaded mcp tools, and added todos Signed-off-by: kpenfound <kyle@dagger.io> * configure claude allow list Signed-off-by: kpenfound <kyle@dagger.io> * claude double underscore Signed-off-by: kpenfound <kyle@dagger.io> * make configure re-entrant Signed-off-by: kpenfound <kyle@dagger.io> * refactor configurable agents into interface Signed-off-by: kpenfound <kyle@dagger.io> * fix lints Signed-off-by: kpenfound <kyle@dagger.io> * update configs to be local where possible Signed-off-by: kpenfound <kyle@dagger.io> * rename container-use binary Signed-off-by: kpenfound <kyle@dagger.io> * add tests for configure tasks Signed-off-by: kpenfound <kyle@dagger.io> * merge main conflict Signed-off-by: kpenfound <kyle@dagger.io> * Creating environment to restructure the configure subcommand to be accessible via config agent instead. * Creating a new config.go file that will contain the parent config command. * Updating configure.go to change the command name from 'configure' to 'agent' and add it as a subcommand of config instead of root. * better configure tests Signed-off-by: kpenfound <kyle@dagger.io> --------- Signed-off-by: kpenfound <kyle@dagger.io>
1 parent a17b265 commit 9c34e0a

File tree

15 files changed

+1071
-10
lines changed

15 files changed

+1071
-10
lines changed

.container-use/environment.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,5 +8,6 @@
88
"git config --global user.name \"Test User\"",
99
"git config --global user.email \"test@dagger.com\"",
1010
"curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b /usr/local/bin v1.61.0"
11-
]
11+
],
12+
"Locked": false
1213
}

cmd/container-use/config.go

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
package main
2+
3+
import (
4+
"github.com/spf13/cobra"
5+
)
6+
7+
var configCmd = &cobra.Command{
8+
Use: "config",
9+
Short: "Configuration management for container-use",
10+
Long: `Manage configuration for container-use including agent setup and other settings.`,
11+
}
12+
13+
func init() {
14+
rootCmd.AddCommand(configCmd)
15+
}

cmd/container-use/configure.go

Lines changed: 170 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,170 @@
1+
package main
2+
3+
import (
4+
"fmt"
5+
"os"
6+
"path/filepath"
7+
"strings"
8+
9+
"github.com/dagger/container-use/mcpserver"
10+
"github.com/spf13/cobra"
11+
)
12+
13+
type MCPServersConfig struct {
14+
MCPServers map[string]MCPServer `json:"mcpServers"`
15+
}
16+
17+
type MCPServer struct {
18+
Command string `json:"command"`
19+
Args []string `json:"args"`
20+
Env map[string]string `json:"env,omitempty"`
21+
Timeout *int `json:"timeout,omitempty"`
22+
Disabled *bool `json:"disabled,omitempty"`
23+
AutoApprove []string `json:"autoApprove,omitempty"`
24+
AlwaysAllow []string `json:"alwaysAllow,omitempty"`
25+
WorkingDir *string `json:"working_directory,omitempty"`
26+
StartOnLaunch *bool `json:"start_on_launch,omitempty"`
27+
}
28+
29+
const ContainerUseBinary = "container-use"
30+
31+
var configureCmd = &cobra.Command{
32+
Use: "agent [agent]",
33+
Short: "Configure MCP server for different agents",
34+
Long: `Setup the container-use MCP server according to the specified agent including Claude Code, Goose, Cursor, and others.`,
35+
RunE: func(cmd *cobra.Command, args []string) error {
36+
if len(args) == 0 {
37+
return interactiveConfiguration()
38+
}
39+
agent, err := selectAgent(args[0])
40+
if err != nil {
41+
return err
42+
}
43+
return configureAgent(agent)
44+
},
45+
}
46+
47+
func interactiveConfiguration() error {
48+
selectedAgent, err := RunAgentSelector()
49+
if err != nil {
50+
// If the user quits, it's not an error, just exit gracefully.
51+
if err.Error() == "no agent selected" {
52+
return nil
53+
}
54+
return fmt.Errorf("failed to select agent: %w", err)
55+
}
56+
57+
agent, err := selectAgent(selectedAgent)
58+
if err != nil {
59+
return err
60+
}
61+
return configureAgent(agent)
62+
}
63+
64+
type ConfigurableAgent interface {
65+
name() string
66+
description() string
67+
editMcpConfig() error
68+
editRules() error
69+
isInstalled() bool
70+
}
71+
72+
// Add agents here
73+
func selectAgent(agentKey string) (ConfigurableAgent, error) {
74+
switch agentKey {
75+
case "claude":
76+
return &ConfigureClaude{}, nil
77+
case "goose":
78+
return &ConfigureGoose{}, nil
79+
case "cursor":
80+
return &ConfigureCursor{}, nil
81+
case "codex":
82+
return &ConfigureCodex{}, nil
83+
case "amazonq":
84+
return &ConfigureQ{}, nil
85+
}
86+
return nil, fmt.Errorf("unknown agent: %s", agentKey)
87+
}
88+
89+
func configureAgent(agent ConfigurableAgent) error {
90+
fmt.Printf("Configuring %s...\n", agent.name())
91+
92+
// Save MCP config
93+
err := agent.editMcpConfig()
94+
if err != nil {
95+
return err
96+
}
97+
fmt.Printf("✓ Configured %s MCP configuration\n", agent.name())
98+
99+
// Save rules
100+
err = agent.editRules()
101+
if err != nil {
102+
return err
103+
}
104+
fmt.Printf("✓ Saved %s container-use rules\n", agent.name())
105+
106+
fmt.Printf("\n%s configuration complete!\n", agent.name())
107+
return nil
108+
}
109+
110+
// Helper functions
111+
func saveRulesFile(rulesFile, content string) error {
112+
dir := filepath.Dir(rulesFile)
113+
if err := os.MkdirAll(dir, 0755); err != nil {
114+
return err
115+
}
116+
117+
// Append to file if it exists, create if it doesn't TODO make it re-entrant with a marker
118+
existing, err := os.ReadFile(rulesFile)
119+
if err != nil && !os.IsNotExist(err) {
120+
return fmt.Errorf("failed to read existing rules: %w", err)
121+
}
122+
existingStr := string(existing)
123+
124+
editedRules, err := editRulesFile(existingStr, content)
125+
if err != nil {
126+
return err
127+
}
128+
129+
err = os.WriteFile(rulesFile, []byte(editedRules), 0644)
130+
if err != nil {
131+
return fmt.Errorf("failed to update rules: %w", err)
132+
}
133+
134+
return nil
135+
}
136+
137+
func editRulesFile(existingRules, content string) (string, error) {
138+
// Look for section markers
139+
const marker = "<!-- container-use-rules -->"
140+
141+
if strings.Contains(existingRules, marker) {
142+
// Update existing section
143+
parts := strings.Split(existingRules, marker)
144+
if len(parts) != 3 {
145+
return "", fmt.Errorf("malformed rules file - expected single section marked with %s", marker)
146+
}
147+
newContent := parts[0] + marker + "\n" + content + "\n" + marker + parts[2]
148+
return newContent, nil
149+
} else {
150+
// Append new section
151+
newContent := existingRules
152+
if len(newContent) > 0 && !strings.HasSuffix(newContent, "\n") {
153+
newContent += "\n"
154+
}
155+
newContent += "\n" + marker + "\n" + content + "\n" + marker + "\n"
156+
return newContent, nil
157+
}
158+
}
159+
160+
func tools(prefix string) []string {
161+
tools := []string{}
162+
for _, t := range mcpserver.Tools() {
163+
tools = append(tools, fmt.Sprintf("%s%s", prefix, t.Definition.Name))
164+
}
165+
return tools
166+
}
167+
168+
func init() {
169+
configCmd.AddCommand(configureCmd)
170+
}
Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
package main
2+
3+
import (
4+
"encoding/json"
5+
"fmt"
6+
"os"
7+
"os/exec"
8+
"path/filepath"
9+
"strings"
10+
11+
"github.com/dagger/container-use/rules"
12+
)
13+
14+
type ConfigureClaude struct {
15+
Name string
16+
Description string
17+
}
18+
19+
func NewConfigureClaude() *ConfigureClaude {
20+
return &ConfigureClaude{
21+
Name: "Claude Code",
22+
Description: "Anthropic's Claude Code",
23+
}
24+
}
25+
26+
type ClaudeSettingsLocal struct {
27+
Permissions *ClaudePermissions `json:"permissions,omitempty"`
28+
Env map[string]string `json:"env,omitempty"`
29+
}
30+
31+
type ClaudePermissions struct {
32+
Allow []string `json:"allow,omitempty"`
33+
Deny []string `json:"deny,omitempty"`
34+
}
35+
36+
func (c *ConfigureClaude) name() string {
37+
return c.Name
38+
}
39+
40+
func (c *ConfigureClaude) description() string {
41+
return c.Description
42+
}
43+
44+
func (c *ConfigureClaude) editMcpConfig() error {
45+
// Add MCP server
46+
cmd := exec.Command("claude", "mcp", "add", "container-use", "--", ContainerUseBinary, "stdio")
47+
err := cmd.Run()
48+
if err != nil {
49+
return fmt.Errorf("could not automatically add MCP server: %w", err)
50+
}
51+
52+
// Configure auto approve settings
53+
configPath := filepath.Join(".claude", "settings.local.json")
54+
// Create directory if it doesn't exist
55+
if err := os.MkdirAll(filepath.Dir(configPath), 0755); err != nil {
56+
return fmt.Errorf("failed to create config directory: %w", err)
57+
}
58+
var config ClaudeSettingsLocal
59+
if data, err := os.ReadFile(configPath); err == nil {
60+
if err := json.Unmarshal(data, &config); err != nil {
61+
return fmt.Errorf("failed to parse existing config: %w", err)
62+
}
63+
}
64+
65+
data, err := c.updateSettingsLocal(config)
66+
if err != nil {
67+
return err
68+
}
69+
70+
err = os.WriteFile(configPath, data, 0644)
71+
if err != nil {
72+
return fmt.Errorf("failed to write config: %w", err)
73+
}
74+
return nil
75+
}
76+
77+
func (c *ConfigureClaude) updateSettingsLocal(config ClaudeSettingsLocal) ([]byte, error) {
78+
// Initialize permissions map if nil
79+
if config.Permissions == nil {
80+
config.Permissions = &ClaudePermissions{Allow: []string{}}
81+
}
82+
83+
// remove save non-container-use items from allow
84+
allows := []string{}
85+
for _, tool := range config.Permissions.Allow {
86+
if !strings.HasPrefix(tool, "mcp__container-use") {
87+
allows = append(allows, tool)
88+
}
89+
}
90+
91+
// Add container-use tools to allow
92+
tools := tools("mcp__container-use__")
93+
allows = append(allows, tools...)
94+
config.Permissions.Allow = allows
95+
96+
// Write config back
97+
data, err := json.MarshalIndent(config, "", " ")
98+
if err != nil {
99+
return nil, fmt.Errorf("failed to marshal config: %w", err)
100+
}
101+
return data, nil
102+
}
103+
104+
func (c *ConfigureClaude) editRules() error {
105+
return saveRulesFile("CLAUDE.md", rules.AgentRules)
106+
}
107+
108+
func (c *ConfigureClaude) isInstalled() bool {
109+
_, err := exec.LookPath("claude")
110+
return err == nil
111+
}

0 commit comments

Comments
 (0)