diff --git a/CLAUDE.md b/CLAUDE.md index 3a0fb82..feffc01 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -302,18 +302,19 @@ All three tools should pass with no issues before merging a feature branch. **IMPORTANT**: When running `nix build`, `nix run`, or similar commands, new files must be tracked by git first. Nix flakes only see git-tracked files. If you create new files, run `git add ` before attempting nix operations. ### Version Bumping -Version bumps should be done once per feature branch, not per commit. Rules: +Version bumps should be done once per feature branch, not per commit. **Only bump versions for packages that were actually changed** — different packages can have different version numbers. + +Rules for determining bump type: - **Patch bump** (0.1.0 → 0.1.1): Changes to Go code within `internal/` that affect a program - **Minor bump** (0.1.0 → 0.2.0): Changes to Go code outside `internal/` (e.g., `cmd/`) - **Major bump** (0.1.0 → 1.0.0): Breaking changes to CLI usage or MCP protocol -Version is defined in multiple places that must stay in sync: -- `cmd/nixpkgs-search/main.go` -- `cmd/nixos-options/main.go` -- `cmd/hm-options/main.go` -- `cmd/lab-monitoring/main.go` -- `internal/mcp/server.go` (in `DefaultNixOSConfig`, `DefaultHomeManagerConfig`, `DefaultNixpkgsPackagesConfig`, `DefaultMonitoringConfig`) -- `nix/package.nix` +Each package's version is defined in multiple places that must stay in sync *for that package*: +- **lab-monitoring**: `cmd/lab-monitoring/main.go` + `internal/mcp/server.go` (`DefaultMonitoringConfig`) +- **nixpkgs-search**: `cmd/nixpkgs-search/main.go` + `internal/mcp/server.go` (`DefaultNixOSConfig`, `DefaultNixpkgsPackagesConfig`) +- **nixos-options**: `cmd/nixos-options/main.go` + `internal/mcp/server.go` (`DefaultNixOSConfig`) +- **hm-options**: `cmd/hm-options/main.go` + `internal/mcp/server.go` (`DefaultHomeManagerConfig`) +- **nix/package.nix**: Shared across all packages (bump to highest version when any package changes) ### User Preferences - User prefers PostgreSQL over SQLite (has homelab infrastructure) diff --git a/README.md b/README.md index 3ee02f1..e25fbaf 100644 --- a/README.md +++ b/README.md @@ -244,9 +244,10 @@ hm-options get programs.git.enable **Lab Monitoring CLI:** ```bash -# List alerts +# List alerts (defaults to active only) lab-monitoring alerts -lab-monitoring alerts --state active +lab-monitoring alerts --all # Include silenced/inhibited alerts +lab-monitoring alerts --state all # Same as --all lab-monitoring alerts --severity critical # Execute PromQL queries @@ -341,7 +342,7 @@ hm-options -d "sqlite://my.db" index hm-unstable | Tool | Description | |------|-------------| -| `list_alerts` | List alerts with optional filters (state, severity, receiver) | +| `list_alerts` | List alerts with optional filters (state, severity, receiver). Defaults to active alerts only; use state=all to include silenced/inhibited | | `get_alert` | Get full details for a specific alert by fingerprint | | `search_metrics` | Search metric names with substring filter, enriched with metadata | | `get_metric_metadata` | Get type, help text, and unit for a specific metric | diff --git a/cmd/hm-options/main.go b/cmd/hm-options/main.go index 8a1b76f..89416ea 100644 --- a/cmd/hm-options/main.go +++ b/cmd/hm-options/main.go @@ -20,7 +20,7 @@ import ( const ( defaultDatabase = "sqlite://hm-options.db" - version = "0.2.1" + version = "0.3.0" ) func main() { diff --git a/cmd/lab-monitoring/main.go b/cmd/lab-monitoring/main.go index c1e080c..7e38270 100644 --- a/cmd/lab-monitoring/main.go +++ b/cmd/lab-monitoring/main.go @@ -15,7 +15,7 @@ import ( "git.t-juice.club/torjus/labmcp/internal/monitoring" ) -const version = "0.2.0" +const version = "0.3.0" func main() { app := &cli.App{ @@ -113,12 +113,16 @@ func alertsCommand() *cli.Command { Flags: []cli.Flag{ &cli.StringFlag{ Name: "state", - Usage: "Filter by state: active, suppressed, unprocessed", + Usage: "Filter by state: active (default), suppressed, unprocessed, all", }, &cli.StringFlag{ Name: "severity", Usage: "Filter by severity label", }, + &cli.BoolFlag{ + Name: "all", + Usage: "Show all alerts including silenced and inhibited (shorthand for --state all)", + }, }, Action: func(c *cli.Context) error { return runAlerts(c) @@ -226,23 +230,32 @@ func runAlerts(c *cli.Context) error { am := monitoring.NewAlertmanagerClient(c.String("alertmanager-url")) filters := monitoring.AlertFilters{} - if state := c.String("state"); state != "" { - switch state { - case "active": - active := true - filters.Active = &active - silenced := false - filters.Silenced = &silenced - inhibited := false - filters.Inhibited = &inhibited - case "suppressed": - active := false - filters.Active = &active - case "unprocessed": - unprocessed := true - filters.Unprocessed = &unprocessed - } + + // Determine state filter: --all flag takes precedence, then --state, then default to active + state := c.String("state") + if c.Bool("all") { + state = "all" } + + switch state { + case "active", "": + // Default to active alerts only (non-silenced, non-inhibited) + active := true + filters.Active = &active + silenced := false + filters.Silenced = &silenced + inhibited := false + filters.Inhibited = &inhibited + case "suppressed": + active := false + filters.Active = &active + case "unprocessed": + unprocessed := true + filters.Unprocessed = &unprocessed + case "all": + // No filters - return everything + } + if severity := c.String("severity"); severity != "" { filters.Filter = append(filters.Filter, fmt.Sprintf(`severity="%s"`, severity)) } diff --git a/cmd/nixos-options/main.go b/cmd/nixos-options/main.go index 5ef9efd..fa03cbb 100644 --- a/cmd/nixos-options/main.go +++ b/cmd/nixos-options/main.go @@ -19,7 +19,7 @@ import ( const ( defaultDatabase = "sqlite://nixos-options.db" - version = "0.2.1" + version = "0.3.0" ) func main() { diff --git a/cmd/nixpkgs-search/main.go b/cmd/nixpkgs-search/main.go index 47b0e20..5ec1a3c 100644 --- a/cmd/nixpkgs-search/main.go +++ b/cmd/nixpkgs-search/main.go @@ -20,7 +20,7 @@ import ( const ( defaultDatabase = "sqlite://nixpkgs-search.db" - version = "0.2.1" + version = "0.3.0" ) func main() { diff --git a/internal/mcp/server.go b/internal/mcp/server.go index b49fd3b..1e2c145 100644 --- a/internal/mcp/server.go +++ b/internal/mcp/server.go @@ -45,7 +45,7 @@ type ServerConfig struct { func DefaultNixOSConfig() ServerConfig { return ServerConfig{ Name: "nixos-options", - Version: "0.2.1", + Version: "0.3.0", DefaultChannel: "nixos-stable", SourceName: "nixpkgs", Mode: ModeOptions, @@ -65,7 +65,7 @@ This ensures option documentation matches the nixpkgs version the project actual func DefaultNixpkgsPackagesConfig() ServerConfig { return ServerConfig{ Name: "nixpkgs-packages", - Version: "0.2.1", + Version: "0.3.0", DefaultChannel: "nixos-stable", SourceName: "nixpkgs", Mode: ModePackages, @@ -83,7 +83,7 @@ This ensures package information matches the nixpkgs version the project actuall func DefaultMonitoringConfig() ServerConfig { return ServerConfig{ Name: "lab-monitoring", - Version: "0.2.0", + Version: "0.3.0", Mode: ModeCustom, Instructions: `Lab Monitoring MCP Server - Query Prometheus metrics and Alertmanager alerts. @@ -102,7 +102,7 @@ All queries are executed against live Prometheus, Alertmanager, and Loki HTTP AP func DefaultHomeManagerConfig() ServerConfig { return ServerConfig{ Name: "hm-options", - Version: "0.2.1", + Version: "0.3.0", DefaultChannel: "hm-stable", SourceName: "home-manager", Mode: ModeOptions, diff --git a/internal/monitoring/handlers.go b/internal/monitoring/handlers.go index 21bc207..b97c946 100644 --- a/internal/monitoring/handlers.go +++ b/internal/monitoring/handlers.go @@ -91,8 +91,8 @@ func listAlertsTool() mcp.Tool { Properties: map[string]mcp.Property{ "state": { Type: "string", - Description: "Filter by alert state: 'active', 'suppressed', or 'unprocessed'", - Enum: []string{"active", "suppressed", "unprocessed"}, + Description: "Filter by alert state: 'active', 'suppressed', 'unprocessed', or 'all' (default: active)", + Enum: []string{"active", "suppressed", "unprocessed", "all"}, }, "severity": { Type: "string", @@ -236,22 +236,24 @@ func makeListAlertsHandler(am *AlertmanagerClient) mcp.ToolHandler { return func(ctx context.Context, args map[string]interface{}) (mcp.CallToolResult, error) { filters := AlertFilters{} - if state, ok := args["state"].(string); ok && state != "" { - switch state { - case "active": - active := true - filters.Active = &active - silenced := false - filters.Silenced = &silenced - inhibited := false - filters.Inhibited = &inhibited - case "suppressed": - active := false - filters.Active = &active - case "unprocessed": - unprocessed := true - filters.Unprocessed = &unprocessed - } + state, _ := args["state"].(string) + switch state { + case "active", "": + // Default to active alerts only (non-silenced, non-inhibited) + active := true + filters.Active = &active + silenced := false + filters.Silenced = &silenced + inhibited := false + filters.Inhibited = &inhibited + case "suppressed": + active := false + filters.Active = &active + case "unprocessed": + unprocessed := true + filters.Unprocessed = &unprocessed + case "all": + // No filters - return everything } if severity, ok := args["severity"].(string); ok && severity != "" { diff --git a/internal/monitoring/handlers_test.go b/internal/monitoring/handlers_test.go index ef6b0f5..f361d3b 100644 --- a/internal/monitoring/handlers_test.go +++ b/internal/monitoring/handlers_test.go @@ -81,6 +81,92 @@ func TestHandler_ListAlerts(t *testing.T) { } } +func TestHandler_ListAlertsDefaultsToActive(t *testing.T) { + // Test that list_alerts with no state param defaults to active filters + server, cleanup := setupTestServer(t, + nil, + func(w http.ResponseWriter, r *http.Request) { + q := r.URL.Query() + // Default should apply active filters + if q.Get("active") != "true" { + t.Errorf("expected default active=true, got %s", q.Get("active")) + } + if q.Get("silenced") != "false" { + t.Errorf("expected default silenced=false, got %s", q.Get("silenced")) + } + if q.Get("inhibited") != "false" { + t.Errorf("expected default inhibited=false, got %s", q.Get("inhibited")) + } + + w.Header().Set("Content-Type", "application/json") + _, _ = w.Write([]byte(`[]`)) + }, + ) + defer cleanup() + + result := callTool(t, server, "list_alerts", map[string]interface{}{}) + if result.IsError { + t.Fatalf("unexpected error: %s", result.Content[0].Text) + } +} + +func TestHandler_ListAlertsStateAll(t *testing.T) { + // Test that list_alerts with state=all applies no filters + server, cleanup := setupTestServer(t, + nil, + func(w http.ResponseWriter, r *http.Request) { + q := r.URL.Query() + // state=all should not set any filter params + if q.Get("active") != "" { + t.Errorf("expected no active param for state=all, got %s", q.Get("active")) + } + if q.Get("silenced") != "" { + t.Errorf("expected no silenced param for state=all, got %s", q.Get("silenced")) + } + if q.Get("inhibited") != "" { + t.Errorf("expected no inhibited param for state=all, got %s", q.Get("inhibited")) + } + + w.Header().Set("Content-Type", "application/json") + _, _ = w.Write([]byte(`[ + { + "annotations": {}, + "endsAt": "2024-01-01T01:00:00Z", + "fingerprint": "fp1", + "receivers": [{"name": "default"}], + "startsAt": "2024-01-01T00:00:00Z", + "status": {"inhibitedBy": [], "silencedBy": [], "state": "active"}, + "updatedAt": "2024-01-01T00:00:00Z", + "generatorURL": "", + "labels": {"alertname": "ActiveAlert", "severity": "critical"} + }, + { + "annotations": {}, + "endsAt": "2024-01-01T01:00:00Z", + "fingerprint": "fp2", + "receivers": [{"name": "default"}], + "startsAt": "2024-01-01T00:00:00Z", + "status": {"inhibitedBy": [], "silencedBy": ["s1"], "state": "suppressed"}, + "updatedAt": "2024-01-01T00:00:00Z", + "generatorURL": "", + "labels": {"alertname": "SilencedAlert", "severity": "warning"} + } + ]`)) + }, + ) + defer cleanup() + + result := callTool(t, server, "list_alerts", map[string]interface{}{ + "state": "all", + }) + if result.IsError { + t.Fatalf("unexpected error: %s", result.Content[0].Text) + } + if !strings.Contains(result.Content[0].Text, "2 alert") { + t.Errorf("expected output to contain '2 alert', got: %s", result.Content[0].Text) + } +} + func TestHandler_GetAlert(t *testing.T) { server, cleanup := setupTestServer(t, nil, diff --git a/nix/package.nix b/nix/package.nix index a20e6bd..f992e84 100644 --- a/nix/package.nix +++ b/nix/package.nix @@ -7,7 +7,7 @@ buildGoModule { inherit pname src; - version = "0.2.1"; + version = "0.3.0"; vendorHash = "sha256-D0KIxQC9ctIAaHBFTvkhBE06uOZwDUcIw8471Ug2doY=";