Files
BlackBean/pigo/internal/cli/config/config.go
T
2026-08-14 23:41:57 +08:00

129 lines
5.3 KiB
Go

// Package config implements pigo's optional user config file at
// ~/.config/pigo/config.toml (honoring $XDG_CONFIG_HOME when set) plus the
// provider-agnostic base-url env-var name derivation. Values in the file
// replace pigo's built-in defaults, but an explicit command-line flag always
// wins over the file:
//
// command-line flag > config.toml > built-in default
//
// A missing file is not an error (defaults apply); a malformed file is surfaced
// to the caller so it can warn rather than silently ignore user intent.
//
// The package is intentionally free of any cliOptions/run-assembly concern: it
// only loads and decodes the file and derives env-var names. Overlaying a
// FileConfig onto the parsed CLI options lives in cmd/pigo, alongside the
// options struct it mutates.
package config
import (
"fmt"
"os"
"path/filepath"
"strings"
"github.com/BurntSushi/toml"
)
// FileConfig is the on-disk shape of config.toml. Every field is optional; an
// absent (zero-value) field leaves the corresponding default/flag untouched.
// Keys are snake_case to read naturally in TOML.
type FileConfig struct {
Model string `toml:"model"`
BaseURL string `toml:"base_url"`
APIKey string `toml:"api_key"`
Protocol string `toml:"protocol"`
Provider string `toml:"provider"`
ThinkingLevel string `toml:"thinking_level"`
OutputFormat string `toml:"output_format"`
NoTools bool `toml:"no_tools"`
NoSkills bool `toml:"no_skills"`
Approve bool `toml:"approve"`
SystemPrompt string `toml:"system_prompt"`
// AllowedTools and DisallowedTools are the tool-level admission boundary:
// the config-file tier of --allowed-tools / --disallowed-tools. Names match
// case-insensitively and DisallowedTools wins when a name appears in both.
// A CLI flag replaces the file value wholesale rather than merging with it,
// so passing --allowed-tools can widen a boundary the file narrowed.
AllowedTools []string `toml:"allowed_tools"`
DisallowedTools []string `toml:"disallowed_tools"`
// Prompts is the config.toml `prompts` array: paths (files or dirs) to load
// prompt templates from at the settings tier (mirrors pi's settings prompts).
Prompts []string `toml:"prompts"`
// Memory, Checkpoint, and Compaction are nested TOML tables for the
// persistent-memory / infinite-context feature. They are pure config
// plumbing here; defaults/parsing live in memory.go (Resolve* helpers) and
// the overlay into runtime options lives in cmd/pigo. See
// tasks/spec-persistent-memory-infinite-context.md §3/§4/§5.2.
Memory MemoryConfig `toml:"memory"`
Checkpoint CheckpointConfig `toml:"checkpoint"`
Compaction CompactionConfig `toml:"compaction"`
// Dream is the [dream] TOML table for the /dream memory-consolidation
// feature. Pure config plumbing here; defaults/normalization live in
// internal/dream (Config). See tasks/spec-dream-memory-consolidation.md
// §3.3.
Dream DreamConfig `toml:"dream"`
}
// DreamConfig is the [dream] TOML table for /dream memory consolidation.
// Enabled is a pointer so an absent key (nil) is distinguishable from an
// explicit false: nil is treated as true, only enabled = false disables
// auto-trigger. IntervalDays and RecentSessions use zero as "apply default"
// (7 and 20 respectively); normalization lives in dream.Config.
type DreamConfig struct {
Enabled *bool `toml:"enabled"`
IntervalDays int `toml:"interval_days"`
RecentSessions int `toml:"recent_sessions"`
}
// FileConfigPath returns the path to the user config file:
// $XDG_CONFIG_HOME/pigo/config.toml, or ~/.config/pigo/config.toml by default.
// It returns "" when neither can be resolved, so the caller treats the file as
// absent.
func FileConfigPath() string {
if dir := os.Getenv("XDG_CONFIG_HOME"); dir != "" {
return filepath.Join(dir, "pigo", "config.toml")
}
home, err := os.UserHomeDir()
if err != nil {
return ""
}
return filepath.Join(home, ".config", "pigo", "config.toml")
}
// LoadFileConfig reads and decodes config.toml. A missing file (or an empty
// path) returns a zero config with no error; a malformed file is an error.
func LoadFileConfig(path string) (FileConfig, error) {
if path == "" {
return FileConfig{}, nil
}
data, err := os.ReadFile(path)
if err != nil {
if os.IsNotExist(err) {
return FileConfig{}, nil
}
return FileConfig{}, fmt.Errorf("read config %s: %w", path, err)
}
var cfg FileConfig
if err := toml.Unmarshal(data, &cfg); err != nil {
return FileConfig{}, fmt.Errorf("parse config %s: %w", path, err)
}
return cfg, nil
}
// GenericBaseURLEnvVar derives the generic base-url override env var name for a
// provider: the provider name uppercased with hyphens rewritten to underscores,
// suffixed with _BASE_URL. For example "zai-coding-cn" → "ZAI_CODING_CN_BASE_URL"
// and "deepseek" → "DEEPSEEK_BASE_URL". An empty provider name yields "".
//
// It lives here (not with ResolveBaseURL) because it is a pure name derivation
// with no dependency on the provider registry — the provider-agnostic part of
// base-url resolution. ResolveBaseURL itself lives in internal/provider.
func GenericBaseURLEnvVar(providerName string) string {
n := strings.TrimSpace(providerName)
if n == "" {
return ""
}
n = strings.ReplaceAll(n, "-", "_")
return strings.ToUpper(n) + "_BASE_URL"
}