Files
gastown/internal/cmd/config.go
Mike Lady 92042d679c feat: Add Cursor, Auggie, and Sourcegraph AMP agent presets (#247)
* feat: add Cursor Agent as compatible agent for Gas Town

Add AgentCursor preset with ProcessNames field for multi-agent detection:
- AgentCursor preset: cursor-agent -p -f (headless + force mode)
- ProcessNames field on AgentPresetInfo for agent detection
- IsAgentRunning(session, processNames) in tmux package
- GetProcessNames(agentName) helper function

Closes: ga-vwr

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* refactor: centralize agent preset list in config.go

Replace hardcoded ["claude", "gemini", "codex"] arrays with calls to
config.ListAgentPresets() to dynamically include all registered agents.

This fixes cursor agent not appearing in `gt config agent list` and
ensures new agent presets are automatically included everywhere.

Also updated doc comments to include "cursor" in example lists.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* test: add comprehensive agent client tests

Add tests for agent detection and command generation:

- TestIsAgentRunning: validates process name detection for all agents
  (claude/node, gemini, codex, cursor-agent)
- TestIsAgentRunning_NonexistentSession: edge case handling
- TestIsClaudeRunning: backwards compatibility wrapper
- TestListAgentPresetsMatchesConstants: ensures ListAgentPresets()
  returns all AgentPreset constants
- TestAgentCommandGeneration: validates full command line generation
  for all supported agents

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* feat: add Auggie agent, fix Cursor interactive mode

Add Auggie CLI as supported agent:
- Command: auggie
- Args: --allow-indexing
- Supports session resume via --resume flag

Fix Cursor agent configuration:
- Remove -p flag (requires prompt, breaks interactive mode)
- Clear SessionIDEnv (cursor uses --resume with chatId directly)
- Keep -f flag for force/YOLO mode

Updated all test cases for both agents.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* feat(agents): add Sourcegraph AMP as agent preset

Add AgentAmp constant and builtinPresets entry for Sourcegraph AMP CLI.

Configuration:
- Command: amp
- Args: --dangerously-allow-all --no-ide
- ResumeStyle: subcommand (amp threads continue <threadId>)
- ProcessNames: amp

Closes: ga-guq

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix: lint error in cleanBeadsRuntimeFiles

Change function to not return error (was always nil).

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix: beads v0.46.0 compatibility and test fixes

- Add custom types config (agent,role,rig,convoy,event) after bd init calls
- Fix tmux_test.go to use variadic IsAgentRunning signature

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* docs: update agent documentation for new presets

- README.md: Update agent examples to show cursor/auggie, add built-in presets list
- docs/reference.md: Add cursor, auggie, amp to built-in agents list
- CHANGELOG.md: Add entry for new agent presets under [Unreleased]

Addresses PR #247 review feedback.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-07 20:35:06 -08:00

469 lines
12 KiB
Go

// Package cmd provides CLI commands for the gt tool.
package cmd
import (
"encoding/json"
"fmt"
"os"
"sort"
"strings"
"github.com/spf13/cobra"
"github.com/steveyegge/gastown/internal/config"
"github.com/steveyegge/gastown/internal/style"
"github.com/steveyegge/gastown/internal/workspace"
)
var configCmd = &cobra.Command{
Use: "config",
GroupID: GroupConfig,
Short: "Manage Gas Town configuration",
RunE: requireSubcommand,
Long: `Manage Gas Town configuration settings.
This command allows you to view and modify configuration settings
for your Gas Town workspace, including agent aliases and defaults.
Commands:
gt config agent list List all agents (built-in and custom)
gt config agent get <name> Show agent configuration
gt config agent set <name> <cmd> Set custom agent command
gt config agent remove <name> Remove custom agent
gt config default-agent [name] Get or set default agent`,
}
// Agent subcommands
var configAgentListCmd = &cobra.Command{
Use: "list",
Short: "List all agents",
Long: `List all available agents (built-in and custom).
Shows all built-in agent presets (claude, gemini, codex) and any
custom agents defined in your town settings.
Examples:
gt config agent list # Text output
gt config agent list --json # JSON output`,
RunE: runConfigAgentList,
}
var configAgentGetCmd = &cobra.Command{
Use: "get <name>",
Short: "Show agent configuration",
Long: `Show the configuration for a specific agent.
Displays the full configuration for an agent, including command,
arguments, and other settings. Works for both built-in and custom agents.
Examples:
gt config agent get claude
gt config agent get my-custom-agent`,
Args: cobra.ExactArgs(1),
RunE: runConfigAgentGet,
}
var configAgentSetCmd = &cobra.Command{
Use: "set <name> <command>",
Short: "Set custom agent command",
Long: `Set a custom agent command in town settings.
This creates or updates a custom agent definition that overrides
or extends the built-in presets. The custom agent will be available
to all rigs in the town.
The command can include arguments. Use quotes if the command or
arguments contain spaces.
Examples:
gt config agent set claude-glm \"claude-glm --model glm-4\"
gt config agent set gemini-custom gemini --approval-mode yolo
gt config agent set claude \"claude-glm\" # Override built-in claude`,
Args: cobra.ExactArgs(2),
RunE: runConfigAgentSet,
}
var configAgentRemoveCmd = &cobra.Command{
Use: "remove <name>",
Short: "Remove custom agent",
Long: `Remove a custom agent definition from town settings.
This removes a custom agent from your town settings. Built-in agents
(claude, gemini, codex) cannot be removed.
Examples:
gt config agent remove claude-glm`,
Args: cobra.ExactArgs(1),
RunE: runConfigAgentRemove,
}
// Default-agent subcommand
var configDefaultAgentCmd = &cobra.Command{
Use: "default-agent [name]",
Short: "Get or set default agent",
Long: `Get or set the default agent for the town.
With no arguments, shows the current default agent.
With an argument, sets the default agent to the specified name.
The default agent is used when a rig doesn't specify its own agent
setting. Can be a built-in preset (claude, gemini, codex) or a
custom agent name.
Examples:
gt config default-agent # Show current default
gt config default-agent claude # Set to claude
gt config default-agent gemini # Set to gemini
gt config default-agent my-custom # Set to custom agent`,
RunE: runConfigDefaultAgent,
}
// Flags
var (
configAgentListJSON bool
)
// AgentListItem represents an agent in list output.
type AgentListItem struct {
Name string `json:"name"`
Command string `json:"command"`
Args string `json:"args,omitempty"`
Type string `json:"type"` // "built-in" or "custom"
IsCustom bool `json:"is_custom"`
}
func runConfigAgentList(cmd *cobra.Command, args []string) error {
townRoot, err := workspace.FindFromCwd()
if err != nil {
return fmt.Errorf("finding town root: %w", err)
}
// Load town settings
settingsPath := config.TownSettingsPath(townRoot)
townSettings, err := config.LoadOrCreateTownSettings(settingsPath)
if err != nil {
return fmt.Errorf("loading town settings: %w", err)
}
// Load agent registry
registryPath := config.DefaultAgentRegistryPath(townRoot)
if err := config.LoadAgentRegistry(registryPath); err != nil {
return fmt.Errorf("loading agent registry: %w", err)
}
// Collect all agents
builtInAgents := config.ListAgentPresets()
customAgents := make(map[string]*config.RuntimeConfig)
if townSettings.Agents != nil {
for name, runtime := range townSettings.Agents {
customAgents[name] = runtime
}
}
// Build list items
var items []AgentListItem
for _, name := range builtInAgents {
preset := config.GetAgentPresetByName(name)
if preset != nil {
items = append(items, AgentListItem{
Name: name,
Command: preset.Command,
Args: strings.Join(preset.Args, " "),
Type: "built-in",
IsCustom: false,
})
}
}
for name, runtime := range customAgents {
argsStr := ""
if runtime.Args != nil {
argsStr = strings.Join(runtime.Args, " ")
}
items = append(items, AgentListItem{
Name: name,
Command: runtime.Command,
Args: argsStr,
Type: "custom",
IsCustom: true,
})
}
// Sort by name
sort.Slice(items, func(i, j int) bool {
return items[i].Name < items[j].Name
})
if configAgentListJSON {
enc := json.NewEncoder(os.Stdout)
enc.SetIndent("", " ")
return enc.Encode(items)
}
// Text output
fmt.Printf("%s\n\n", style.Bold.Render("Available Agents"))
for _, item := range items {
typeLabel := style.Dim.Render("[" + item.Type + "]")
fmt.Printf(" %s %s %s", style.Bold.Render(item.Name), typeLabel, item.Command)
if item.Args != "" {
fmt.Printf(" %s", item.Args)
}
fmt.Println()
}
// Show default
defaultAgent := townSettings.DefaultAgent
if defaultAgent == "" {
defaultAgent = "claude"
}
fmt.Printf("\nDefault: %s\n", style.Bold.Render(defaultAgent))
return nil
}
func runConfigAgentGet(cmd *cobra.Command, args []string) error {
name := args[0]
townRoot, err := workspace.FindFromCwd()
if err != nil {
return fmt.Errorf("finding town root: %w", err)
}
// Load town settings for custom agents
settingsPath := config.TownSettingsPath(townRoot)
townSettings, err := config.LoadOrCreateTownSettings(settingsPath)
if err != nil {
return fmt.Errorf("loading town settings: %w", err)
}
// Load agent registry
registryPath := config.DefaultAgentRegistryPath(townRoot)
if err := config.LoadAgentRegistry(registryPath); err != nil {
return fmt.Errorf("loading agent registry: %w", err)
}
// Check custom agents first
if townSettings.Agents != nil {
if runtime, ok := townSettings.Agents[name]; ok {
displayAgentConfig(name, runtime, nil, true)
return nil
}
}
// Check built-in agents
preset := config.GetAgentPresetByName(name)
if preset != nil {
runtime := &config.RuntimeConfig{
Command: preset.Command,
Args: preset.Args,
}
displayAgentConfig(name, runtime, preset, false)
return nil
}
return fmt.Errorf("agent '%s' not found", name)
}
func displayAgentConfig(name string, runtime *config.RuntimeConfig, preset *config.AgentPresetInfo, isCustom bool) {
fmt.Printf("%s\n\n", style.Bold.Render("Agent: "+name))
typeLabel := "custom"
if !isCustom {
typeLabel = "built-in"
}
fmt.Printf("Type: %s\n", typeLabel)
fmt.Printf("Command: %s\n", runtime.Command)
if runtime.Args != nil && len(runtime.Args) > 0 {
fmt.Printf("Args: %s\n", strings.Join(runtime.Args, " "))
}
if preset != nil {
if preset.SessionIDEnv != "" {
fmt.Printf("Session ID Env: %s\n", preset.SessionIDEnv)
}
if preset.ResumeFlag != "" {
fmt.Printf("Resume Style: %s (%s)\n", preset.ResumeStyle, preset.ResumeFlag)
}
fmt.Printf("Supports Hooks: %v\n", preset.SupportsHooks)
}
}
func runConfigAgentSet(cmd *cobra.Command, args []string) error {
name := args[0]
commandLine := args[1]
townRoot, err := workspace.FindFromCwd()
if err != nil {
return fmt.Errorf("finding town root: %w", err)
}
// Load town settings
settingsPath := config.TownSettingsPath(townRoot)
townSettings, err := config.LoadOrCreateTownSettings(settingsPath)
if err != nil {
return fmt.Errorf("loading town settings: %w", err)
}
// Parse command line into command and args
parts := strings.Fields(commandLine)
if len(parts) == 0 {
return fmt.Errorf("command cannot be empty")
}
// Initialize agents map if needed
if townSettings.Agents == nil {
townSettings.Agents = make(map[string]*config.RuntimeConfig)
}
// Create or update the agent
townSettings.Agents[name] = &config.RuntimeConfig{
Command: parts[0],
Args: parts[1:],
}
// Save settings
if err := config.SaveTownSettings(settingsPath, townSettings); err != nil {
return fmt.Errorf("saving town settings: %w", err)
}
fmt.Printf("Agent '%s' set to: %s\n", style.Bold.Render(name), commandLine)
// Check if this overrides a built-in
builtInAgents := config.ListAgentPresets()
for _, builtin := range builtInAgents {
if name == builtin {
fmt.Printf("\n%s\n", style.Dim.Render("(overriding built-in '"+builtin+"' preset)"))
break
}
}
return nil
}
func runConfigAgentRemove(cmd *cobra.Command, args []string) error {
name := args[0]
townRoot, err := workspace.FindFromCwd()
if err != nil {
return fmt.Errorf("finding town root: %w", err)
}
// Check if trying to remove built-in
builtInAgents := config.ListAgentPresets()
for _, builtin := range builtInAgents {
if name == builtin {
return fmt.Errorf("cannot remove built-in agent '%s' (use 'gt config agent set' to override it)", name)
}
}
// Load town settings
settingsPath := config.TownSettingsPath(townRoot)
townSettings, err := config.LoadOrCreateTownSettings(settingsPath)
if err != nil {
return fmt.Errorf("loading town settings: %w", err)
}
// Check if agent exists
if townSettings.Agents == nil || townSettings.Agents[name] == nil {
return fmt.Errorf("custom agent '%s' not found", name)
}
// Remove the agent
delete(townSettings.Agents, name)
// Save settings
if err := config.SaveTownSettings(settingsPath, townSettings); err != nil {
return fmt.Errorf("saving town settings: %w", err)
}
fmt.Printf("Removed custom agent '%s'\n", style.Bold.Render(name))
return nil
}
func runConfigDefaultAgent(cmd *cobra.Command, args []string) error {
townRoot, err := workspace.FindFromCwd()
if err != nil {
return fmt.Errorf("finding town root: %w", err)
}
// Load town settings
settingsPath := config.TownSettingsPath(townRoot)
townSettings, err := config.LoadOrCreateTownSettings(settingsPath)
if err != nil {
return fmt.Errorf("loading town settings: %w", err)
}
// Load agent registry
registryPath := config.DefaultAgentRegistryPath(townRoot)
if err := config.LoadAgentRegistry(registryPath); err != nil {
return fmt.Errorf("loading agent registry: %w", err)
}
if len(args) == 0 {
// Show current default
defaultAgent := townSettings.DefaultAgent
if defaultAgent == "" {
defaultAgent = "claude"
}
fmt.Printf("Default agent: %s\n", style.Bold.Render(defaultAgent))
return nil
}
// Set new default
name := args[0]
// Verify agent exists
isValid := false
builtInAgents := config.ListAgentPresets()
for _, builtin := range builtInAgents {
if name == builtin {
isValid = true
break
}
}
if !isValid && townSettings.Agents != nil {
if _, ok := townSettings.Agents[name]; ok {
isValid = true
}
}
if !isValid {
return fmt.Errorf("agent '%s' not found (use 'gt config agent list' to see available agents)", name)
}
// Set default
townSettings.DefaultAgent = name
// Save settings
if err := config.SaveTownSettings(settingsPath, townSettings); err != nil {
return fmt.Errorf("saving town settings: %w", err)
}
fmt.Printf("Default agent set to '%s'\n", style.Bold.Render(name))
return nil
}
func init() {
// Add flags
configAgentListCmd.Flags().BoolVar(&configAgentListJSON, "json", false, "Output as JSON")
// Add agent subcommands
configAgentCmd := &cobra.Command{
Use: "agent",
Short: "Manage agent configuration",
RunE: requireSubcommand,
}
configAgentCmd.AddCommand(configAgentListCmd)
configAgentCmd.AddCommand(configAgentGetCmd)
configAgentCmd.AddCommand(configAgentSetCmd)
configAgentCmd.AddCommand(configAgentRemoveCmd)
// Add subcommands to config
configCmd.AddCommand(configAgentCmd)
configCmd.AddCommand(configDefaultAgentCmd)
// Register with root
rootCmd.AddCommand(configCmd)
}