// Package config provides configuration types and serialization for Gas Town. package config import ( "encoding/json" "os" "path/filepath" ) // AgentPreset identifies a supported LLM agent runtime. // These presets provide sensible defaults that can be overridden in config. type AgentPreset string // Supported agent presets (built-in, E2E tested). const ( // AgentClaude is Claude Code (default). AgentClaude AgentPreset = "claude" // AgentGemini is Gemini CLI. AgentGemini AgentPreset = "gemini" // AgentCodex is OpenAI Codex. AgentCodex AgentPreset = "codex" ) // AgentPresetInfo contains the configuration details for an agent preset. // This extends the basic RuntimeConfig with agent-specific metadata. type AgentPresetInfo struct { // Name is the preset identifier (e.g., "claude", "gemini", "codex"). Name AgentPreset `json:"name"` // Command is the CLI binary to invoke. Command string `json:"command"` // Args are the default command-line arguments for autonomous mode. Args []string `json:"args"` // SessionIDEnv is the environment variable for session ID. // Used for resuming sessions across restarts. SessionIDEnv string `json:"session_id_env,omitempty"` // ResumeFlag is the flag/subcommand for resuming sessions. // For claude/gemini: "--resume" // For codex: "resume" (subcommand) ResumeFlag string `json:"resume_flag,omitempty"` // ResumeStyle indicates how to invoke resume: // "flag" - pass as --resume argument // "subcommand" - pass as 'codex resume ' ResumeStyle string `json:"resume_style,omitempty"` // SupportsHooks indicates if the agent supports hooks system. SupportsHooks bool `json:"supports_hooks,omitempty"` // SupportsForkSession indicates if --fork-session is available. // Claude-only feature for seance command. SupportsForkSession bool `json:"supports_fork_session,omitempty"` // NonInteractive contains settings for non-interactive mode. NonInteractive *NonInteractiveConfig `json:"non_interactive,omitempty"` } // NonInteractiveConfig contains settings for running agents non-interactively. type NonInteractiveConfig struct { // Subcommand is the subcommand for non-interactive execution (e.g., "exec" for codex). Subcommand string `json:"subcommand,omitempty"` // PromptFlag is the flag for passing prompts (e.g., "-p" for gemini). PromptFlag string `json:"prompt_flag,omitempty"` // OutputFlag is the flag for structured output (e.g., "--json", "--output-format json"). OutputFlag string `json:"output_flag,omitempty"` } // AgentRegistry contains all known agent presets. // Can be loaded from JSON config or use built-in defaults. type AgentRegistry struct { // Version is the schema version for the registry. Version int `json:"version"` // Agents maps agent names to their configurations. Agents map[string]*AgentPresetInfo `json:"agents"` } // CurrentAgentRegistryVersion is the current schema version. const CurrentAgentRegistryVersion = 1 // builtinPresets contains the default presets for supported agents. var builtinPresets = map[AgentPreset]*AgentPresetInfo{ AgentClaude: { Name: AgentClaude, Command: "claude", Args: []string{"--dangerously-skip-permissions"}, SessionIDEnv: "CLAUDE_SESSION_ID", ResumeFlag: "--resume", ResumeStyle: "flag", SupportsHooks: true, SupportsForkSession: true, NonInteractive: nil, // Claude is native non-interactive }, AgentGemini: { Name: AgentGemini, Command: "gemini", Args: []string{"--approval-mode", "yolo"}, SessionIDEnv: "GEMINI_SESSION_ID", ResumeFlag: "--resume", ResumeStyle: "flag", SupportsHooks: true, SupportsForkSession: false, NonInteractive: &NonInteractiveConfig{ PromptFlag: "-p", OutputFlag: "--output-format json", }, }, AgentCodex: { Name: AgentCodex, Command: "codex", Args: []string{"--yolo"}, SessionIDEnv: "", // Codex captures from JSONL output ResumeFlag: "resume", ResumeStyle: "subcommand", SupportsHooks: false, // Use env/files instead SupportsForkSession: false, NonInteractive: &NonInteractiveConfig{ Subcommand: "exec", OutputFlag: "--json", }, }, } // globalRegistry is the merged registry of built-in and user-defined agents. var globalRegistry *AgentRegistry // initRegistry initializes the global registry with built-in presets. func initRegistry() { if globalRegistry != nil { return } globalRegistry = &AgentRegistry{ Version: CurrentAgentRegistryVersion, Agents: make(map[string]*AgentPresetInfo), } // Copy built-in presets for name, preset := range builtinPresets { globalRegistry.Agents[string(name)] = preset } } // LoadAgentRegistry loads agent definitions from a JSON file and merges with built-ins. // User-defined agents override built-in presets with the same name. func LoadAgentRegistry(path string) error { initRegistry() data, err := os.ReadFile(path) //nolint:gosec // G304: path is from config if err != nil { if os.IsNotExist(err) { return nil // No custom config, use built-ins only } return err } var userRegistry AgentRegistry if err := json.Unmarshal(data, &userRegistry); err != nil { return err } // Merge user-defined agents (override built-ins) for name, preset := range userRegistry.Agents { preset.Name = AgentPreset(name) globalRegistry.Agents[name] = preset } return nil } // DefaultAgentRegistryPath returns the default path for agent registry. // Located alongside other town settings. func DefaultAgentRegistryPath(townRoot string) string { return filepath.Join(townRoot, "settings", "agents.json") } // GetAgentPreset returns the preset info for a given agent name. // Returns nil if the preset is not found. func GetAgentPreset(name AgentPreset) *AgentPresetInfo { initRegistry() return globalRegistry.Agents[string(name)] } // GetAgentPresetByName returns the preset info by string name. // Returns nil if not found, allowing caller to fall back to defaults. func GetAgentPresetByName(name string) *AgentPresetInfo { initRegistry() return globalRegistry.Agents[name] } // ListAgentPresets returns all known agent preset names. func ListAgentPresets() []string { initRegistry() names := make([]string, 0, len(globalRegistry.Agents)) for name := range globalRegistry.Agents { names = append(names, name) } return names } // DefaultAgentPreset returns the default agent preset (Claude). func DefaultAgentPreset() AgentPreset { return AgentClaude } // RuntimeConfigFromPreset creates a RuntimeConfig from an agent preset. // This provides the basic Command/Args; additional fields from AgentPresetInfo // can be accessed separately for extended functionality. func RuntimeConfigFromPreset(preset AgentPreset) *RuntimeConfig { info := GetAgentPreset(preset) if info == nil { // Fall back to Claude defaults return DefaultRuntimeConfig() } return &RuntimeConfig{ Command: info.Command, Args: append([]string(nil), info.Args...), // Copy to avoid mutation } } // MergeWithPreset applies preset defaults to a RuntimeConfig. // User-specified values take precedence over preset defaults. // Returns a new RuntimeConfig without modifying the original. func (rc *RuntimeConfig) MergeWithPreset(preset AgentPreset) *RuntimeConfig { if rc == nil { return RuntimeConfigFromPreset(preset) } info := GetAgentPreset(preset) if info == nil { return rc } result := &RuntimeConfig{ Command: rc.Command, Args: append([]string(nil), rc.Args...), InitialPrompt: rc.InitialPrompt, } // Apply preset defaults only if not overridden if result.Command == "" { result.Command = info.Command } if len(result.Args) == 0 { result.Args = append([]string(nil), info.Args...) } return result } // IsKnownPreset checks if a string is a known agent preset name. func IsKnownPreset(name string) bool { initRegistry() _, ok := globalRegistry.Agents[name] return ok } // SaveAgentRegistry writes the agent registry to a file. func SaveAgentRegistry(path string, registry *AgentRegistry) error { if err := os.MkdirAll(filepath.Dir(path), 0755); err != nil { return err } data, err := json.MarshalIndent(registry, "", " ") if err != nil { return err } return os.WriteFile(path, data, 0644) //nolint:gosec // G306: config file } // NewExampleAgentRegistry creates an example registry with comments. func NewExampleAgentRegistry() *AgentRegistry { return &AgentRegistry{ Version: CurrentAgentRegistryVersion, Agents: map[string]*AgentPresetInfo{ // Include one example custom agent "my-custom-agent": { Name: "my-custom-agent", Command: "my-agent-cli", Args: []string{"--autonomous", "--no-confirm"}, SessionIDEnv: "MY_AGENT_SESSION_ID", ResumeFlag: "--resume", ResumeStyle: "flag", NonInteractive: &NonInteractiveConfig{ PromptFlag: "-m", OutputFlag: "--json", }, }, }, } }