diff --git a/cmd/costs.go b/cmd/costs.go index d09a9d8..5d8eefe 100644 --- a/cmd/costs.go +++ b/cmd/costs.go @@ -161,4 +161,3 @@ func shortModel(name string) string { } return name } - diff --git a/cmd/root.go b/cmd/root.go index 1b312e9..2d345a0 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -76,7 +76,7 @@ func loadData() (*pipeline.LoadResult, error) { fmt.Fprintf(os.Stderr, " Cache unavailable, doing full parse\n") } } else { - defer cache.Close() + defer func() { _ = cache.Close() }() cr, err := pipeline.LoadWithCache(flagDataDir, !flagNoSubagents, cache, progressFn) if err != nil { diff --git a/cmd/summary.go b/cmd/summary.go index 0466b67..122fe23 100644 --- a/cmd/summary.go +++ b/cmd/summary.go @@ -51,7 +51,7 @@ func runSummary(_ *cobra.Command, _ []string) error { fmt.Println() // Build the summary table - rows := [][]string{ + rows := [][]string{ //nolint:prealloc // appended conditionally below {"Sessions", cli.FormatNumber(int64(stats.TotalSessions))}, {"Prompts", cli.FormatNumber(int64(stats.TotalPrompts))}, {"Total Time", cli.FormatDuration(stats.TotalDurationSecs)}, @@ -70,7 +70,7 @@ func runSummary(_ *cobra.Command, _ []string) error { } // Cost per day with delta - costDayStr := fmt.Sprintf("%s/day", cli.FormatCost(stats.CostPerDay)) + costDayStr := cli.FormatCost(stats.CostPerDay) + "/day" if prevStats.CostPerDay > 0 { costDayStr += fmt.Sprintf(" (%s vs prev %dd)", cli.FormatDelta(stats.CostPerDay, prevStats.CostPerDay), flagDays) diff --git a/internal/cli/format.go b/internal/cli/format.go index 716d106..2898cc2 100644 --- a/internal/cli/format.go +++ b/internal/cli/format.go @@ -1,8 +1,10 @@ +// Package cli provides formatting and rendering utilities for terminal output. package cli import ( "fmt" "math" + "strconv" "strings" ) @@ -22,14 +24,14 @@ func FormatTokens(n int64) string { case abs >= 1_000: return fmt.Sprintf("%.1fK", float64(n)/1_000) default: - return fmt.Sprintf("%d", n) + return strconv.FormatInt(n, 10) } } // FormatCost formats a USD cost value. func FormatCost(cost float64) string { if cost >= 1000 { - return fmt.Sprintf("$%s", FormatNumber(int64(math.Round(cost)))) + return "$" + FormatNumber(int64(math.Round(cost))) } if cost >= 100 { return fmt.Sprintf("$%.0f", cost) @@ -66,7 +68,7 @@ func FormatNumber(n int64) string { return "-" + FormatNumber(-n) } - s := fmt.Sprintf("%d", n) + s := strconv.FormatInt(n, 10) if len(s) <= 3 { return s } @@ -95,9 +97,9 @@ func FormatPercent(f float64) string { func FormatDelta(current, previous float64) string { delta := current - previous if delta >= 0 { - return fmt.Sprintf("+%s", FormatCost(delta)) + return "+" + FormatCost(delta) } - return fmt.Sprintf("-%s", FormatCost(-delta)) + return "-" + FormatCost(-delta) } // FormatDayOfWeek returns a 3-letter day abbreviation from a weekday number. diff --git a/internal/config/plan.go b/internal/config/plan.go index 25a3cad..d5b4d2d 100644 --- a/internal/config/plan.go +++ b/internal/config/plan.go @@ -15,7 +15,7 @@ type PlanInfo struct { // DetectPlan reads ~/.claude/.claude.json to determine the billing plan. func DetectPlan(claudeDir string) PlanInfo { path := filepath.Join(claudeDir, ".claude.json") - data, err := os.ReadFile(path) + data, err := os.ReadFile(path) //nolint:gosec // path is constructed from known claudeDir if err != nil { return PlanInfo{PlanCeiling: 200} // default to Max plan } diff --git a/internal/model/metrics.go b/internal/model/metrics.go index 5b8ff58..35bc870 100644 --- a/internal/model/metrics.go +++ b/internal/model/metrics.go @@ -4,11 +4,11 @@ import "time" // SummaryStats holds the top-level aggregate across all sessions. type SummaryStats struct { - TotalSessions int - TotalPrompts int - TotalAPICalls int + TotalSessions int + TotalPrompts int + TotalAPICalls int TotalDurationSecs int64 - ActiveDays int + ActiveDays int InputTokens int64 OutputTokens int64 @@ -46,7 +46,7 @@ type DailyStats struct { } // ModelStats holds aggregated metrics for a single model. -type ModelStats struct { +type ModelStats struct { //nolint:revive // renaming would break many call sites Model string APICalls int InputTokens int64 @@ -79,11 +79,11 @@ type HourlyStats struct { // WeeklyStats holds metrics for one calendar week. type WeeklyStats struct { - WeekStart time.Time - Sessions int - Prompts int - TotalTokens int64 - DurationSecs int64 + WeekStart time.Time + Sessions int + Prompts int + TotalTokens int64 + DurationSecs int64 EstimatedCost float64 } diff --git a/internal/model/session.go b/internal/model/session.go index fe079e5..c945944 100644 --- a/internal/model/session.go +++ b/internal/model/session.go @@ -1,3 +1,4 @@ +// Package model defines domain types for cburn metrics and sessions. package model import "time" @@ -17,7 +18,7 @@ type APICall struct { } // ModelUsage tracks per-model token usage within a session. -type ModelUsage struct { +type ModelUsage struct { //nolint:revive // renaming would break many call sites APICalls int InputTokens int64 OutputTokens int64 diff --git a/internal/pipeline/aggregator.go b/internal/pipeline/aggregator.go index 39f191b..80f884c 100644 --- a/internal/pipeline/aggregator.go +++ b/internal/pipeline/aggregator.go @@ -1,3 +1,4 @@ +// Package pipeline orchestrates session loading, caching, and metric aggregation. package pipeline import ( diff --git a/internal/pipeline/bench_test.go b/internal/pipeline/bench_test.go index 5bcd311..d84669d 100644 --- a/internal/pipeline/bench_test.go +++ b/internal/pipeline/bench_test.go @@ -79,7 +79,7 @@ func BenchmarkLoadWithCache(b *testing.B) { if err != nil { b.Fatal(err) } - defer cache.Close() + defer func() { _ = cache.Close() }() b.ResetTimer() for i := 0; i < b.N; i++ { diff --git a/internal/pipeline/loader.go b/internal/pipeline/loader.go index e106d93..85b78a1 100644 --- a/internal/pipeline/loader.go +++ b/internal/pipeline/loader.go @@ -67,11 +67,6 @@ func Load(claudeDir string, includeSubagents bool, progressFn ProgressFunc) (*Lo numWorkers = len(toProcess) } - type indexedResult struct { - idx int - result source.ParseResult - } - work := make(chan int, len(toProcess)) results := make([]source.ParseResult, len(toProcess)) var wg sync.WaitGroup diff --git a/internal/source/parser.go b/internal/source/parser.go index 071eee1..b50cca1 100644 --- a/internal/source/parser.go +++ b/internal/source/parser.go @@ -1,3 +1,4 @@ +// Package source discovers and parses Claude Code JSONL session files. package source import ( @@ -41,7 +42,7 @@ func ParseFile(df DiscoveredFile) ParseResult { if err != nil { return ParseResult{Err: err} } - defer f.Close() + defer func() { _ = f.Close() }() calls := make(map[string]*model.APICall) @@ -274,14 +275,17 @@ func classifyType(line []byte, pos int) (val string, isKey bool) { } // skipJSONString advances past a JSON string starting at the opening quote. +// +//nolint:gosec // manual bounds checking throughout func skipJSONString(line []byte, i int) int { i++ // skip opening quote for i < len(line) { - if line[i] == '\\' { + switch line[i] { + case '\\': i += 2 - } else if line[i] == '"' { + case '"': return i + 1 - } else { + default: i++ } } diff --git a/internal/source/scanner.go b/internal/source/scanner.go index 90f8d5a..6b176cb 100644 --- a/internal/source/scanner.go +++ b/internal/source/scanner.go @@ -26,7 +26,7 @@ func ScanDir(claudeDir string) ([]DiscoveredFile, error) { err = filepath.WalkDir(projectsDir, func(path string, d os.DirEntry, err error) error { if err != nil { - return nil // skip unreadable entries + return nil //nolint:nilerr // intentionally skip unreadable entries } if d.IsDir() { return nil @@ -77,8 +77,9 @@ func ScanDir(claudeDir string) ([]DiscoveredFile, error) { // decodeProjectName extracts a human-readable project name from the encoded directory name. // Claude Code encodes absolute paths by replacing "/" with "-", so: -// "-Users-tayloreernisse-projects-gitlore" -> "gitlore" -// "-Users-tayloreernisse-projects-my-cool-project" -> "my-cool-project" +// +// "-Users-tayloreernisse-projects-gitlore" -> "gitlore" +// "-Users-tayloreernisse-projects-my-cool-project" -> "my-cool-project" // // We find the last known path component ("projects", "repos", "src", "code", "home") // and take everything after it. Falls back to the last non-empty segment. diff --git a/internal/store/cache.go b/internal/store/cache.go index f6e5958..55a91ee 100644 --- a/internal/store/cache.go +++ b/internal/store/cache.go @@ -1,3 +1,4 @@ +// Package store provides a SQLite-backed cache for parsed session data. package store import ( @@ -9,7 +10,7 @@ import ( "cburn/internal/model" - _ "modernc.org/sqlite" + _ "modernc.org/sqlite" // register sqlite driver ) // Cache provides SQLite-backed session caching. @@ -20,7 +21,7 @@ type Cache struct { // Open opens or creates the cache database at the given path. func Open(dbPath string) (*Cache, error) { dir := filepath.Dir(dbPath) - if err := os.MkdirAll(dir, 0o755); err != nil { + if err := os.MkdirAll(dir, 0o750); err != nil { return nil, fmt.Errorf("creating cache dir: %w", err) } @@ -30,7 +31,7 @@ func Open(dbPath string) (*Cache, error) { } if _, err := db.Exec(schemaSQL); err != nil { - db.Close() + _ = db.Close() return nil, fmt.Errorf("creating schema: %w", err) } @@ -54,7 +55,7 @@ func (c *Cache) GetTrackedFiles() (map[string]FileInfo, error) { if err != nil { return nil, err } - defer rows.Close() + defer func() { _ = rows.Close() }() result := make(map[string]FileInfo) for rows.Next() { @@ -74,7 +75,7 @@ func (c *Cache) SaveSession(s model.SessionStats, mtimeNs, sizeBytes int64) erro if err != nil { return err } - defer tx.Rollback() + defer func() { _ = tx.Rollback() }() now := time.Now().UTC().Format(time.RFC3339) startTime := "" @@ -147,7 +148,7 @@ func (c *Cache) LoadAllSessions() ([]model.SessionStats, error) { if err != nil { return nil, err } - defer rows.Close() + defer func() { _ = rows.Close() }() var sessions []model.SessionStats for rows.Next() { @@ -195,7 +196,7 @@ func (c *Cache) LoadAllSessions() ([]model.SessionStats, error) { if err != nil { return nil, err } - defer modelRows.Close() + defer func() { _ = modelRows.Close() }() // Build session index for fast lookup sessionIdx := make(map[string]int) diff --git a/main.go b/main.go index d654254..ebefdd7 100644 --- a/main.go +++ b/main.go @@ -1,3 +1,4 @@ +// cburn analyzes Claude Code usage from local JSONL session logs. package main import "cburn/cmd"