From d7f4189e3e230c56878639a189676282d7de83a3 Mon Sep 17 00:00:00 2001 From: Steve Yegge Date: Fri, 21 Nov 2025 19:20:09 -0500 Subject: [PATCH] feat: Add 'bd count' command for counting and grouping issues MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Implements a new 'bd count' command that provides efficient issue counting with filtering and grouping capabilities. Features: - Basic count: Returns total count of issues matching filters - All filtering options from 'bd list' (status, priority, type, assignee, labels, dates, etc.) - Grouping via --by-* flags: status, priority, type, assignee, label - JSON output support for both simple and grouped counts - Both daemon and direct mode support Implementation: - Added OpCount operation and CountArgs to RPC protocol - Added Count() method to RPC client - Implemented handleCount() server-side handler with optimized bulk label fetching - Created cmd/bd/count.go with full CLI implementation Performance optimization: - Pre-fetches all labels in a single query when using --by-label to avoid N+1 queries 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- cmd/bd/count.go | 462 ++++++++++++++++++ internal/rpc/client.go | 5 + internal/rpc/protocol.go | 39 ++ internal/rpc/server_issues_epics.go | 232 +++++++++ .../server_routing_validation_diagnostics.go | 2 + 5 files changed, 740 insertions(+) create mode 100644 cmd/bd/count.go diff --git a/cmd/bd/count.go b/cmd/bd/count.go new file mode 100644 index 00000000..07780030 --- /dev/null +++ b/cmd/bd/count.go @@ -0,0 +1,462 @@ +package main + +import ( + "encoding/json" + "fmt" + "os" + "sort" + "strings" + + "github.com/spf13/cobra" + "github.com/steveyegge/beads/internal/rpc" + "github.com/steveyegge/beads/internal/types" + "github.com/steveyegge/beads/internal/util" +) + +var countCmd = &cobra.Command{ + Use: "count", + Short: "Count issues matching filters", + Long: `Count issues matching the specified filters. + +By default, returns the total count of issues matching the filters. +Use --by-* flags to group counts by different attributes. + +Examples: + bd count # Count all issues + bd count --status open # Count open issues + bd count --by-status # Group count by status + bd count --by-priority # Group count by priority + bd count --by-type # Group count by issue type + bd count --by-assignee # Group count by assignee + bd count --by-label # Group count by label + bd count --assignee alice --by-status # Count alice's issues by status +`, + Run: func(cmd *cobra.Command, args []string) { + status, _ := cmd.Flags().GetString("status") + assignee, _ := cmd.Flags().GetString("assignee") + issueType, _ := cmd.Flags().GetString("type") + labels, _ := cmd.Flags().GetStringSlice("label") + labelsAny, _ := cmd.Flags().GetStringSlice("label-any") + titleSearch, _ := cmd.Flags().GetString("title") + idFilter, _ := cmd.Flags().GetString("id") + + // Pattern matching flags + titleContains, _ := cmd.Flags().GetString("title-contains") + descContains, _ := cmd.Flags().GetString("desc-contains") + notesContains, _ := cmd.Flags().GetString("notes-contains") + + // Date range flags + createdAfter, _ := cmd.Flags().GetString("created-after") + createdBefore, _ := cmd.Flags().GetString("created-before") + updatedAfter, _ := cmd.Flags().GetString("updated-after") + updatedBefore, _ := cmd.Flags().GetString("updated-before") + closedAfter, _ := cmd.Flags().GetString("closed-after") + closedBefore, _ := cmd.Flags().GetString("closed-before") + + // Empty/null check flags + emptyDesc, _ := cmd.Flags().GetBool("empty-description") + noAssignee, _ := cmd.Flags().GetBool("no-assignee") + noLabels, _ := cmd.Flags().GetBool("no-labels") + + // Priority range flags + priorityMin, _ := cmd.Flags().GetInt("priority-min") + priorityMax, _ := cmd.Flags().GetInt("priority-max") + + // Group by flags + byStatus, _ := cmd.Flags().GetBool("by-status") + byPriority, _ := cmd.Flags().GetBool("by-priority") + byType, _ := cmd.Flags().GetBool("by-type") + byAssignee, _ := cmd.Flags().GetBool("by-assignee") + byLabel, _ := cmd.Flags().GetBool("by-label") + + // Determine groupBy value + groupBy := "" + groupCount := 0 + if byStatus { + groupBy = "status" + groupCount++ + } + if byPriority { + groupBy = "priority" + groupCount++ + } + if byType { + groupBy = "type" + groupCount++ + } + if byAssignee { + groupBy = "assignee" + groupCount++ + } + if byLabel { + groupBy = "label" + groupCount++ + } + + if groupCount > 1 { + fmt.Fprintf(os.Stderr, "Error: only one --by-* flag can be specified\n") + os.Exit(1) + } + + // Normalize labels + labels = util.NormalizeLabels(labels) + labelsAny = util.NormalizeLabels(labelsAny) + + // Check database freshness before reading + ctx := rootCtx + if daemonClient == nil { + if err := ensureDatabaseFresh(ctx); err != nil { + fmt.Fprintf(os.Stderr, "Error: %v\n", err) + os.Exit(1) + } + } + + // If daemon is running, use RPC + if daemonClient != nil { + countArgs := &rpc.CountArgs{ + Status: status, + IssueType: issueType, + Assignee: assignee, + GroupBy: groupBy, + } + if cmd.Flags().Changed("priority") { + priority, _ := cmd.Flags().GetInt("priority") + countArgs.Priority = &priority + } + if len(labels) > 0 { + countArgs.Labels = labels + } + if len(labelsAny) > 0 { + countArgs.LabelsAny = labelsAny + } + if titleSearch != "" { + countArgs.Query = titleSearch + } + if idFilter != "" { + ids := util.NormalizeLabels(strings.Split(idFilter, ",")) + if len(ids) > 0 { + countArgs.IDs = ids + } + } + + // Pattern matching + countArgs.TitleContains = titleContains + countArgs.DescriptionContains = descContains + countArgs.NotesContains = notesContains + + // Date ranges + countArgs.CreatedAfter = createdAfter + countArgs.CreatedBefore = createdBefore + countArgs.UpdatedAfter = updatedAfter + countArgs.UpdatedBefore = updatedBefore + countArgs.ClosedAfter = closedAfter + countArgs.ClosedBefore = closedBefore + + // Empty/null checks + countArgs.EmptyDescription = emptyDesc + countArgs.NoAssignee = noAssignee + countArgs.NoLabels = noLabels + + // Priority range + if cmd.Flags().Changed("priority-min") { + countArgs.PriorityMin = &priorityMin + } + if cmd.Flags().Changed("priority-max") { + countArgs.PriorityMax = &priorityMax + } + + resp, err := daemonClient.Count(countArgs) + if err != nil { + fmt.Fprintf(os.Stderr, "Error: %v\n", err) + os.Exit(1) + } + + if groupBy == "" { + // Simple count + var result struct { + Count int `json:"count"` + } + if err := json.Unmarshal(resp.Data, &result); err != nil { + fmt.Fprintf(os.Stderr, "Error parsing response: %v\n", err) + os.Exit(1) + } + + if jsonOutput { + outputJSON(result) + } else { + fmt.Println(result.Count) + } + } else { + // Grouped count + var result struct { + Total int `json:"total"` + Groups []struct { + Group string `json:"group"` + Count int `json:"count"` + } `json:"groups"` + } + if err := json.Unmarshal(resp.Data, &result); err != nil { + fmt.Fprintf(os.Stderr, "Error parsing response: %v\n", err) + os.Exit(1) + } + + if jsonOutput { + outputJSON(result) + } else { + // Sort groups for consistent output + sort.Slice(result.Groups, func(i, j int) bool { + return result.Groups[i].Group < result.Groups[j].Group + }) + + fmt.Printf("Total: %d\n\n", result.Total) + for _, g := range result.Groups { + fmt.Printf("%s: %d\n", g.Group, g.Count) + } + } + } + return + } + + // Direct mode + filter := types.IssueFilter{} + if status != "" && status != "all" { + s := types.Status(status) + filter.Status = &s + } + if cmd.Flags().Changed("priority") { + priority, _ := cmd.Flags().GetInt("priority") + filter.Priority = &priority + } + if assignee != "" { + filter.Assignee = &assignee + } + if issueType != "" { + t := types.IssueType(issueType) + filter.IssueType = &t + } + if len(labels) > 0 { + filter.Labels = labels + } + if len(labelsAny) > 0 { + filter.LabelsAny = labelsAny + } + if titleSearch != "" { + filter.TitleSearch = titleSearch + } + if idFilter != "" { + ids := util.NormalizeLabels(strings.Split(idFilter, ",")) + if len(ids) > 0 { + filter.IDs = ids + } + } + + // Pattern matching + filter.TitleContains = titleContains + filter.DescriptionContains = descContains + filter.NotesContains = notesContains + + // Date ranges + if createdAfter != "" { + t, err := parseTimeFlag(createdAfter) + if err != nil { + fmt.Fprintf(os.Stderr, "Error parsing --created-after: %v\n", err) + os.Exit(1) + } + filter.CreatedAfter = &t + } + if createdBefore != "" { + t, err := parseTimeFlag(createdBefore) + if err != nil { + fmt.Fprintf(os.Stderr, "Error parsing --created-before: %v\n", err) + os.Exit(1) + } + filter.CreatedBefore = &t + } + if updatedAfter != "" { + t, err := parseTimeFlag(updatedAfter) + if err != nil { + fmt.Fprintf(os.Stderr, "Error parsing --updated-after: %v\n", err) + os.Exit(1) + } + filter.UpdatedAfter = &t + } + if updatedBefore != "" { + t, err := parseTimeFlag(updatedBefore) + if err != nil { + fmt.Fprintf(os.Stderr, "Error parsing --updated-before: %v\n", err) + os.Exit(1) + } + filter.UpdatedBefore = &t + } + if closedAfter != "" { + t, err := parseTimeFlag(closedAfter) + if err != nil { + fmt.Fprintf(os.Stderr, "Error parsing --closed-after: %v\n", err) + os.Exit(1) + } + filter.ClosedAfter = &t + } + if closedBefore != "" { + t, err := parseTimeFlag(closedBefore) + if err != nil { + fmt.Fprintf(os.Stderr, "Error parsing --closed-before: %v\n", err) + os.Exit(1) + } + filter.ClosedBefore = &t + } + + // Empty/null checks + filter.EmptyDescription = emptyDesc + filter.NoAssignee = noAssignee + filter.NoLabels = noLabels + + // Priority range + if cmd.Flags().Changed("priority-min") { + filter.PriorityMin = &priorityMin + } + if cmd.Flags().Changed("priority-max") { + filter.PriorityMax = &priorityMax + } + + issues, err := store.SearchIssues(ctx, "", filter) + if err != nil { + fmt.Fprintf(os.Stderr, "Error: %v\n", err) + os.Exit(1) + } + + // If no grouping, just print count + if groupBy == "" { + if jsonOutput { + result := struct { + Count int `json:"count"` + }{Count: len(issues)} + outputJSON(result) + } else { + fmt.Println(len(issues)) + } + return + } + + // Group by the specified field + counts := make(map[string]int) + + // For label grouping, fetch all labels in one query to avoid N+1 + var labelsMap map[string][]string + if groupBy == "label" { + issueIDs := make([]string, len(issues)) + for i, issue := range issues { + issueIDs[i] = issue.ID + } + var err error + labelsMap, err = store.GetLabelsForIssues(ctx, issueIDs) + if err != nil { + fmt.Fprintf(os.Stderr, "Error getting labels: %v\n", err) + os.Exit(1) + } + } + + for _, issue := range issues { + var groupKey string + switch groupBy { + case "status": + groupKey = string(issue.Status) + case "priority": + groupKey = fmt.Sprintf("P%d", issue.Priority) + case "type": + groupKey = string(issue.IssueType) + case "assignee": + if issue.Assignee == "" { + groupKey = "(unassigned)" + } else { + groupKey = issue.Assignee + } + case "label": + // For labels, count each label separately + labels := labelsMap[issue.ID] + if len(labels) > 0 { + for _, label := range labels { + counts[label]++ + } + continue + } else { + groupKey = "(no labels)" + } + } + counts[groupKey]++ + } + + type GroupCount struct { + Group string `json:"group"` + Count int `json:"count"` + } + + groups := make([]GroupCount, 0, len(counts)) + for group, count := range counts { + groups = append(groups, GroupCount{Group: group, Count: count}) + } + + // Sort for consistent output + sort.Slice(groups, func(i, j int) bool { + return groups[i].Group < groups[j].Group + }) + + if jsonOutput { + result := struct { + Total int `json:"total"` + Groups []GroupCount `json:"groups"` + }{ + Total: len(issues), + Groups: groups, + } + outputJSON(result) + } else { + fmt.Printf("Total: %d\n\n", len(issues)) + for _, g := range groups { + fmt.Printf("%s: %d\n", g.Group, g.Count) + } + } + }, +} + +func init() { + // Filter flags (same as list command) + countCmd.Flags().StringP("status", "s", "", "Filter by status (open, in_progress, blocked, closed)") + countCmd.Flags().IntP("priority", "p", 0, "Filter by priority (0-4: 0=critical, 1=high, 2=medium, 3=low, 4=backlog)") + countCmd.Flags().StringP("assignee", "a", "", "Filter by assignee") + countCmd.Flags().StringP("type", "t", "", "Filter by type (bug, feature, task, epic, chore)") + countCmd.Flags().StringSliceP("label", "l", []string{}, "Filter by labels (AND: must have ALL)") + countCmd.Flags().StringSlice("label-any", []string{}, "Filter by labels (OR: must have AT LEAST ONE)") + countCmd.Flags().String("title", "", "Filter by title text (case-insensitive substring match)") + countCmd.Flags().String("id", "", "Filter by specific issue IDs (comma-separated)") + + // Pattern matching + countCmd.Flags().String("title-contains", "", "Filter by title substring") + countCmd.Flags().String("desc-contains", "", "Filter by description substring") + countCmd.Flags().String("notes-contains", "", "Filter by notes substring") + + // Date ranges + countCmd.Flags().String("created-after", "", "Filter issues created after date (YYYY-MM-DD or RFC3339)") + countCmd.Flags().String("created-before", "", "Filter issues created before date (YYYY-MM-DD or RFC3339)") + countCmd.Flags().String("updated-after", "", "Filter issues updated after date (YYYY-MM-DD or RFC3339)") + countCmd.Flags().String("updated-before", "", "Filter issues updated before date (YYYY-MM-DD or RFC3339)") + countCmd.Flags().String("closed-after", "", "Filter issues closed after date (YYYY-MM-DD or RFC3339)") + countCmd.Flags().String("closed-before", "", "Filter issues closed before date (YYYY-MM-DD or RFC3339)") + + // Empty/null checks + countCmd.Flags().Bool("empty-description", false, "Filter issues with empty description") + countCmd.Flags().Bool("no-assignee", false, "Filter issues with no assignee") + countCmd.Flags().Bool("no-labels", false, "Filter issues with no labels") + + // Priority ranges + countCmd.Flags().Int("priority-min", 0, "Filter by minimum priority (inclusive)") + countCmd.Flags().Int("priority-max", 0, "Filter by maximum priority (inclusive)") + + // Grouping flags + countCmd.Flags().Bool("by-status", false, "Group count by status") + countCmd.Flags().Bool("by-priority", false, "Group count by priority") + countCmd.Flags().Bool("by-type", false, "Group count by issue type") + countCmd.Flags().Bool("by-assignee", false, "Group count by assignee") + countCmd.Flags().Bool("by-label", false, "Group count by label") + + rootCmd.AddCommand(countCmd) +} diff --git a/internal/rpc/client.go b/internal/rpc/client.go index a2cc9b1e..97af4330 100644 --- a/internal/rpc/client.go +++ b/internal/rpc/client.go @@ -294,6 +294,11 @@ func (c *Client) List(args *ListArgs) (*Response, error) { return c.Execute(OpList, args) } +// Count counts issues via the daemon +func (c *Client) Count(args *CountArgs) (*Response, error) { + return c.Execute(OpCount, args) +} + // Show shows an issue via the daemon func (c *Client) Show(args *ShowArgs) (*Response, error) { return c.Execute(OpShow, args) diff --git a/internal/rpc/protocol.go b/internal/rpc/protocol.go index 913b3fd6..1628c6c3 100644 --- a/internal/rpc/protocol.go +++ b/internal/rpc/protocol.go @@ -14,6 +14,7 @@ const ( OpUpdate = "update" OpClose = "close" OpList = "list" + OpCount = "count" OpShow = "show" OpReady = "ready" OpStale = "stale" @@ -127,6 +128,44 @@ type ListArgs struct { PriorityMax *int `json:"priority_max,omitempty"` } +// CountArgs represents arguments for the count operation +type CountArgs struct { + // Supports all the same filters as ListArgs + Query string `json:"query,omitempty"` + Status string `json:"status,omitempty"` + Priority *int `json:"priority,omitempty"` + IssueType string `json:"issue_type,omitempty"` + Assignee string `json:"assignee,omitempty"` + Labels []string `json:"labels,omitempty"` + LabelsAny []string `json:"labels_any,omitempty"` + IDs []string `json:"ids,omitempty"` + + // Pattern matching + TitleContains string `json:"title_contains,omitempty"` + DescriptionContains string `json:"description_contains,omitempty"` + NotesContains string `json:"notes_contains,omitempty"` + + // Date ranges + CreatedAfter string `json:"created_after,omitempty"` + CreatedBefore string `json:"created_before,omitempty"` + UpdatedAfter string `json:"updated_after,omitempty"` + UpdatedBefore string `json:"updated_before,omitempty"` + ClosedAfter string `json:"closed_after,omitempty"` + ClosedBefore string `json:"closed_before,omitempty"` + + // Empty/null checks + EmptyDescription bool `json:"empty_description,omitempty"` + NoAssignee bool `json:"no_assignee,omitempty"` + NoLabels bool `json:"no_labels,omitempty"` + + // Priority range + PriorityMin *int `json:"priority_min,omitempty"` + PriorityMax *int `json:"priority_max,omitempty"` + + // Grouping option (only one can be specified) + GroupBy string `json:"group_by,omitempty"` // "status", "priority", "type", "assignee", "label" +} + // ShowArgs represents arguments for the show operation type ShowArgs struct { ID string `json:"id"` diff --git a/internal/rpc/server_issues_epics.go b/internal/rpc/server_issues_epics.go index 4e6b5d49..f1360305 100644 --- a/internal/rpc/server_issues_epics.go +++ b/internal/rpc/server_issues_epics.go @@ -529,6 +529,238 @@ func (s *Server) handleList(req *Request) Response { } } +func (s *Server) handleCount(req *Request) Response { + var countArgs CountArgs + if err := json.Unmarshal(req.Args, &countArgs); err != nil { + return Response{ + Success: false, + Error: fmt.Sprintf("invalid count args: %v", err), + } + } + + store := s.storage + if store == nil { + return Response{ + Success: false, + Error: "storage not available (global daemon deprecated - use local daemon instead with 'bd daemon' in your project)", + } + } + + filter := types.IssueFilter{} + + // Normalize status: treat "" or "all" as unset (no filter) + if countArgs.Status != "" && countArgs.Status != "all" { + status := types.Status(countArgs.Status) + filter.Status = &status + } + + if countArgs.IssueType != "" { + issueType := types.IssueType(countArgs.IssueType) + filter.IssueType = &issueType + } + if countArgs.Assignee != "" { + filter.Assignee = &countArgs.Assignee + } + if countArgs.Priority != nil { + filter.Priority = countArgs.Priority + } + + // Normalize and apply label filters + labels := util.NormalizeLabels(countArgs.Labels) + labelsAny := util.NormalizeLabels(countArgs.LabelsAny) + if len(labels) > 0 { + filter.Labels = labels + } + if len(labelsAny) > 0 { + filter.LabelsAny = labelsAny + } + if len(countArgs.IDs) > 0 { + ids := util.NormalizeLabels(countArgs.IDs) + if len(ids) > 0 { + filter.IDs = ids + } + } + + // Pattern matching + filter.TitleContains = countArgs.TitleContains + filter.DescriptionContains = countArgs.DescriptionContains + filter.NotesContains = countArgs.NotesContains + + // Date ranges - use parseTimeRPC helper for flexible formats + if countArgs.CreatedAfter != "" { + t, err := parseTimeRPC(countArgs.CreatedAfter) + if err != nil { + return Response{ + Success: false, + Error: fmt.Sprintf("invalid --created-after date: %v", err), + } + } + filter.CreatedAfter = &t + } + if countArgs.CreatedBefore != "" { + t, err := parseTimeRPC(countArgs.CreatedBefore) + if err != nil { + return Response{ + Success: false, + Error: fmt.Sprintf("invalid --created-before date: %v", err), + } + } + filter.CreatedBefore = &t + } + if countArgs.UpdatedAfter != "" { + t, err := parseTimeRPC(countArgs.UpdatedAfter) + if err != nil { + return Response{ + Success: false, + Error: fmt.Sprintf("invalid --updated-after date: %v", err), + } + } + filter.UpdatedAfter = &t + } + if countArgs.UpdatedBefore != "" { + t, err := parseTimeRPC(countArgs.UpdatedBefore) + if err != nil { + return Response{ + Success: false, + Error: fmt.Sprintf("invalid --updated-before date: %v", err), + } + } + filter.UpdatedBefore = &t + } + if countArgs.ClosedAfter != "" { + t, err := parseTimeRPC(countArgs.ClosedAfter) + if err != nil { + return Response{ + Success: false, + Error: fmt.Sprintf("invalid --closed-after date: %v", err), + } + } + filter.ClosedAfter = &t + } + if countArgs.ClosedBefore != "" { + t, err := parseTimeRPC(countArgs.ClosedBefore) + if err != nil { + return Response{ + Success: false, + Error: fmt.Sprintf("invalid --closed-before date: %v", err), + } + } + filter.ClosedBefore = &t + } + + // Empty/null checks + filter.EmptyDescription = countArgs.EmptyDescription + filter.NoAssignee = countArgs.NoAssignee + filter.NoLabels = countArgs.NoLabels + + // Priority range + filter.PriorityMin = countArgs.PriorityMin + filter.PriorityMax = countArgs.PriorityMax + + ctx := s.reqCtx(req) + issues, err := store.SearchIssues(ctx, countArgs.Query, filter) + if err != nil { + return Response{ + Success: false, + Error: fmt.Sprintf("failed to count issues: %v", err), + } + } + + // If no grouping, just return the count + if countArgs.GroupBy == "" { + type CountResult struct { + Count int `json:"count"` + } + data, _ := json.Marshal(CountResult{Count: len(issues)}) + return Response{ + Success: true, + Data: data, + } + } + + // Group by the specified field + type GroupCount struct { + Group string `json:"group"` + Count int `json:"count"` + } + + counts := make(map[string]int) + + // For label grouping, fetch all labels in one query to avoid N+1 + var labelsMap map[string][]string + if countArgs.GroupBy == "label" { + issueIDs := make([]string, len(issues)) + for i, issue := range issues { + issueIDs[i] = issue.ID + } + var err error + labelsMap, err = store.GetLabelsForIssues(ctx, issueIDs) + if err != nil { + return Response{ + Success: false, + Error: fmt.Sprintf("failed to get labels: %v", err), + } + } + } + + for _, issue := range issues { + var groupKey string + switch countArgs.GroupBy { + case "status": + groupKey = string(issue.Status) + case "priority": + groupKey = fmt.Sprintf("P%d", issue.Priority) + case "type": + groupKey = string(issue.IssueType) + case "assignee": + if issue.Assignee == "" { + groupKey = "(unassigned)" + } else { + groupKey = issue.Assignee + } + case "label": + // For labels, count each label separately + labels := labelsMap[issue.ID] + if len(labels) > 0 { + for _, label := range labels { + counts[label]++ + } + continue + } else { + groupKey = "(no labels)" + } + default: + return Response{ + Success: false, + Error: fmt.Sprintf("invalid group_by value: %s (must be one of: status, priority, type, assignee, label)", countArgs.GroupBy), + } + } + counts[groupKey]++ + } + + // Convert map to sorted slice + groups := make([]GroupCount, 0, len(counts)) + for group, count := range counts { + groups = append(groups, GroupCount{Group: group, Count: count}) + } + + type GroupedCountResult struct { + Total int `json:"total"` + Groups []GroupCount `json:"groups"` + } + + result := GroupedCountResult{ + Total: len(issues), + Groups: groups, + } + + data, _ := json.Marshal(result) + return Response{ + Success: true, + Data: data, + } +} + func (s *Server) handleResolveID(req *Request) Response { var args ResolveIDArgs if err := json.Unmarshal(req.Args, &args); err != nil { diff --git a/internal/rpc/server_routing_validation_diagnostics.go b/internal/rpc/server_routing_validation_diagnostics.go index 088992ef..ebebb599 100644 --- a/internal/rpc/server_routing_validation_diagnostics.go +++ b/internal/rpc/server_routing_validation_diagnostics.go @@ -178,6 +178,8 @@ func (s *Server) handleRequest(req *Request) Response { resp = s.handleClose(req) case OpList: resp = s.handleList(req) + case OpCount: + resp = s.handleCount(req) case OpShow: resp = s.handleShow(req) case OpResolveID: