diff --git a/internal/settings/settings.go b/internal/settings/settings.go index ae1af56..174681f 100644 --- a/internal/settings/settings.go +++ b/internal/settings/settings.go @@ -9,6 +9,7 @@ import ( "errors" "os" "path/filepath" + "strconv" ) // ErrInvalidBackend marks a settings value whose ai_backend is not a known @@ -16,13 +17,36 @@ import ( // without importing the ai package. var ErrInvalidBackend = errors.New("settings: invalid ai backend") +// ErrInvalidAmbientMode marks a settings value whose ambient_mode is not a +// known mode. The applier returns it (wrapped) so the HTTP layer can map it to +// 400 without importing the ambient package. +var ErrInvalidAmbientMode = errors.New("settings: invalid ambient mode") + +// Ambient mode dial values. +const ( + AmbientOff = "off" + AmbientStatus = "status" + AmbientNotify = "notify" +) + +// ValidAmbientMode reports whether m is a known ambient mode constant. +func ValidAmbientMode(m string) bool { + switch m { + case AmbientOff, AmbientStatus, AmbientNotify: + return true + } + return false +} + // Settings is the user-editable configuration. Field names mirror the env vars // they replace: KEEL_AI_BACKEND, KEEL_MARVIN_CMD, KEEL_KNOWLEDGE_FILE, KEEL_AW_URL. type Settings struct { - AIBackend string `json:"ai_backend"` - MarvinCmd string `json:"marvin_cmd"` - KnowledgePath string `json:"knowledge_path"` - AWURL string `json:"aw_url"` + AIBackend string `json:"ai_backend"` + MarvinCmd string `json:"marvin_cmd"` + KnowledgePath string `json:"knowledge_path"` + AWURL string `json:"aw_url"` + AmbientMode string `json:"ambient_mode"` + AmbientCadenceSecs int `json:"ambient_cadence_secs"` } // DefaultPath returns ~/.keel/settings.json. @@ -77,10 +101,22 @@ func SeedFromEnv() Settings { if awURL == "" { awURL = "http://localhost:5600" } + ambientMode := os.Getenv("KEEL_AMBIENT_MODE") + if ambientMode == "" { + ambientMode = AmbientNotify + } + ambientCadenceSecs := 300 + if v := os.Getenv("KEEL_AMBIENT_CADENCE_SECS"); v != "" { + if n, err := strconv.Atoi(v); err == nil && n > 0 { + ambientCadenceSecs = n + } + } return Settings{ - AIBackend: backend, - MarvinCmd: os.Getenv("KEEL_MARVIN_CMD"), - KnowledgePath: os.Getenv("KEEL_KNOWLEDGE_FILE"), - AWURL: awURL, + AIBackend: backend, + MarvinCmd: os.Getenv("KEEL_MARVIN_CMD"), + KnowledgePath: os.Getenv("KEEL_KNOWLEDGE_FILE"), + AWURL: awURL, + AmbientMode: ambientMode, + AmbientCadenceSecs: ambientCadenceSecs, } } diff --git a/internal/settings/settings_test.go b/internal/settings/settings_test.go index 08a37aa..502968a 100644 --- a/internal/settings/settings_test.go +++ b/internal/settings/settings_test.go @@ -33,8 +33,10 @@ func TestSeedFromEnvReadsVars(t *testing.T) { t.Setenv("KEEL_MARVIN_CMD", "uv run am") t.Setenv("KEEL_KNOWLEDGE_FILE", "/tmp/k.md") t.Setenv("KEEL_AW_URL", "http://aw.local:5600") + t.Setenv("KEEL_AMBIENT_MODE", "") + t.Setenv("KEEL_AMBIENT_CADENCE_SECS", "") got := SeedFromEnv() - want := Settings{AIBackend: "codex", MarvinCmd: "uv run am", KnowledgePath: "/tmp/k.md", AWURL: "http://aw.local:5600"} + want := Settings{AIBackend: "codex", MarvinCmd: "uv run am", KnowledgePath: "/tmp/k.md", AWURL: "http://aw.local:5600", AmbientMode: AmbientNotify, AmbientCadenceSecs: 300} if got != want { t.Errorf("SeedFromEnv = %+v, want %+v", got, want) } @@ -73,3 +75,38 @@ func TestSeedFromEnvAWURLDefault(t *testing.T) { t.Fatalf("default AWURL = %q", got) } } + +func TestValidAmbientMode(t *testing.T) { + for _, m := range []string{AmbientOff, AmbientStatus, AmbientNotify} { + if !ValidAmbientMode(m) { + t.Fatalf("%q should be valid", m) + } + } + if ValidAmbientMode("loud") { + t.Fatal("loud should be invalid") + } + if ValidAmbientMode("") { + t.Fatal("empty should be invalid (callers default before validating)") + } +} + +func TestSeedFromEnvAmbientDefaults(t *testing.T) { + t.Setenv("KEEL_AMBIENT_MODE", "") + t.Setenv("KEEL_AMBIENT_CADENCE_SECS", "") + s := SeedFromEnv() + if s.AmbientMode != AmbientNotify { + t.Fatalf("ambient_mode default = %q, want %q", s.AmbientMode, AmbientNotify) + } + if s.AmbientCadenceSecs != 300 { + t.Fatalf("ambient_cadence_secs default = %d, want 300", s.AmbientCadenceSecs) + } +} + +func TestSeedFromEnvAmbientFromEnv(t *testing.T) { + t.Setenv("KEEL_AMBIENT_MODE", "status") + t.Setenv("KEEL_AMBIENT_CADENCE_SECS", "120") + s := SeedFromEnv() + if s.AmbientMode != "status" || s.AmbientCadenceSecs != 120 { + t.Fatalf("env not honored: mode=%q cadence=%d", s.AmbientMode, s.AmbientCadenceSecs) + } +}