package config import ( "errors" "fmt" "os" "path/filepath" "time" "github.com/BurntSushi/toml" ) // Project represents a named workspace project used for webhook routing. type Project struct { Name string `toml:"name"` Dir string `toml:"dir"` } // LocalModel configures an OpenAI-compatible local LLM endpoint used for // internal helpers (classifier, elaboration, future summarization) and as // the backend for the "local" runner. If Endpoint is empty, the LocalRunner // is not registered and the classifier falls back to the Gemini CLI. // // PreferForElaborate gates whether the API server's elaboration handler // uses this client. It defaults to true when Endpoint is set; users with a // slow or low-quality local model can disable it. type LocalModel struct { Endpoint string `toml:"endpoint"` // e.g. "http://localhost:11434/v1" Model string `toml:"model"` // e.g. "llama3.1:8b" TimeoutSeconds int `toml:"timeout_seconds"` // default 60 DefaultTemperature float64 `toml:"default_temperature"` // default 0.2 APIKey string `toml:"api_key"` // optional bearer token PreferForElaborate *bool `toml:"prefer_for_elaborate"` // pointer so default-true survives parse } // UseForElaborate returns true when elaboration should try this local model // before falling back to Claude/Gemini. Default is true when Endpoint is set. func (m LocalModel) UseForElaborate() bool { if m.Endpoint == "" { return false } if m.PreferForElaborate == nil { return true } return *m.PreferForElaborate } type Config struct { DataDir string `toml:"data_dir"` DBPath string `toml:"-"` LogDir string `toml:"-"` DropsDir string `toml:"-"` SSHAuthSock string `toml:"ssh_auth_sock"` ClaudeBinaryPath string `toml:"claude_binary_path"` GeminiBinaryPath string `toml:"gemini_binary_path"` ClaudeImage string `toml:"claude_image"` GeminiImage string `toml:"gemini_image"` MaxConcurrent int `toml:"max_concurrent"` ShutdownTimeout time.Duration `toml:"shutdown_timeout"` DefaultTimeout string `toml:"default_timeout"` ServerAddr string `toml:"server_addr"` WebhookURL string `toml:"webhook_url"` WorkspaceRoot string `toml:"workspace_root"` WebhookSecret string `toml:"webhook_secret"` Projects []Project `toml:"projects"` VAPIDPublicKey string `toml:"vapid_public_key"` VAPIDPrivateKey string `toml:"vapid_private_key"` VAPIDEmail string `toml:"vapid_email"` ClaudeConfigDir string `toml:"claude_config_dir"` LocalModel LocalModel `toml:"local_model"` } func Default() (*Config, error) { home, err := os.UserHomeDir() if err != nil { return nil, fmt.Errorf("cannot determine home directory: %w", err) } if home == "" { return nil, errors.New("cannot determine home directory: HOME is empty") } dataDir := filepath.Join(home, ".claudomator") return &Config{ DataDir: dataDir, DBPath: filepath.Join(dataDir, "claudomator.db"), LogDir: filepath.Join(dataDir, "executions"), DropsDir: filepath.Join(dataDir, "drops"), SSHAuthSock: os.Getenv("SSH_AUTH_SOCK"), ClaudeBinaryPath: "claude", GeminiBinaryPath: "gemini", ClaudeImage: "claudomator-agent:latest", GeminiImage: "claudomator-agent:latest", MaxConcurrent: 3, DefaultTimeout: "15m", ServerAddr: ":8484", WorkspaceRoot: "/workspace", ClaudeConfigDir: "/workspace/claudomator/credentials/claude", }, nil } // LoadFile loads a TOML config file on top of the defaults. // Fields not present in the file retain their default values. func LoadFile(path string) (*Config, error) { cfg, err := Default() if err != nil { return nil, err } if _, err := toml.DecodeFile(path, cfg); err != nil { return nil, fmt.Errorf("loading config file %q: %w", path, err) } return cfg, nil } // EnsureDirs creates the data directory structure. func (c *Config) EnsureDirs() error { for _, dir := range []string{c.DataDir, c.LogDir, c.DropsDir} { if err := os.MkdirAll(dir, 0700); err != nil { return err } } return nil }