claude-matrix 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: f223740ea18dd305956e0d1a21b8424dec3a19bd467e9a78310fb40702f7697d
4
+ data.tar.gz: 5136193a040c79ba2b55d7de2720a89db388acfa78b5f825c19783ab7878e0b8
5
+ SHA512:
6
+ metadata.gz: 50c596495620a83d34d38c604ddd1f6f3d951f4c624c788ad056a0f47a9b10e63e352c178d94a00a918b8c041c4ac8817cb4ac41e00f7b8258181894910e1449
7
+ data.tar.gz: f016892c5c0e9d120fa2bba9be0004d3e823dc39729db5e3060e7eba9fc579f413ddea8e19bdd722be3166897f7be51e7687e569d37d24ee7dba9796fb07bc24
data/CHANGELOG.md ADDED
@@ -0,0 +1,21 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project will be documented here.
4
+ Format follows [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
5
+
6
+ ## [1.0.0] - 2026-03-10
7
+
8
+ ### Added
9
+ - Full-screen btop-style TUI dashboard
10
+ - Streaks: current, longest, active days
11
+ - Personal bests: longest session, most productive day, busiest hour, favorite tool
12
+ - Work mode breakdown: Explore / Build / Test
13
+ - Top tools ranked bar chart with gradient coloring
14
+ - Tokens & cost panel with per-model cost estimation
15
+ - Activity chart (last 30 days)
16
+ - Hourly heatmap (day × hour, shown on taller terminals)
17
+ - Time filters: Today / Week / Month / All Time (`t`/`w`/`m`/`a`)
18
+ - Per-project breakdown with git branch tracking
19
+ - `stats` command with `--today`, `--week`, `--month` flags
20
+ - `doctor` command to verify data sources
21
+ - Zero configuration — reads `~/.claude/` directly
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Kapil Bhosale
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,183 @@
1
+ # Claude Matrix
2
+
3
+ A privacy-first, terminal analytics dashboard for [Claude Code](https://claude.ai/code). It reads your local Claude Code session data (reads ~/.claude/ directly, never touches the network) — sessions, tools, tokens, cost, streaks — and renders a live TUI right in your terminal. No cloud, no API calls, no configuration. Just run it and see your usage, coding patterns.
4
+
5
+
6
+ ![Claude Matrix Dashboard](./docs/screenshot-1.png)
7
+
8
+ ---
9
+
10
+ ## Features
11
+
12
+ - **Full-screen TUI** — btop-inspired two-column layout that fills your terminal exactly, no scrolling
13
+ - **Streaks** — current and longest coding streaks, active days
14
+ - **Personal bests** — longest session, most productive day, busiest hour, favorite tool
15
+ - **Work mode** — Explore / Build / Test breakdown based on tool usage patterns
16
+ - **Top tools** — ranked bar chart with gradient coloring
17
+ - **Tokens & cost** — input/output token bars with estimated cost (color-coded green → yellow → red)
18
+ - **Activity chart** — last 30-day bar chart
19
+ - **Heatmap** — sessions by day-of-week × hour (on taller terminals)
20
+ - **Time filters** — Today / Week / Month / All Time, switch with a single key
21
+ - **100% local** — reads `~/.claude/` directly, never touches the network
22
+ - **Git branch tracking** — shows the branch you were on per project
23
+
24
+ ---
25
+
26
+ ## Requirements
27
+
28
+ - Ruby 3.0+
29
+ - macOS or Linux
30
+ - Claude Code installed and used at least once
31
+ - A terminal with Unicode and color support (iTerm2, Warp, Kitty, Alacritty, etc.)
32
+
33
+ ---
34
+
35
+ ## Installation
36
+
37
+ ```bash
38
+ gem install claude-matrix
39
+ ```
40
+
41
+ **From source:**
42
+
43
+ ```bash
44
+ git clone https://github.com/kapilbhosale/claude-matrix
45
+ cd claude-matrix
46
+ bundle install
47
+ bundle exec bin/claude-matrix
48
+ ```
49
+
50
+ ---
51
+
52
+ ## Usage
53
+
54
+ ### Launch the dashboard
55
+
56
+ ```bash
57
+ claude-matrix
58
+ # or
59
+ claude-matrix dashboard
60
+ ```
61
+
62
+ ### Dashboard keyboard controls
63
+
64
+ | Key | Action |
65
+ |-----|--------|
66
+ | `t` | Filter: Today |
67
+ | `w` | Filter: This Week |
68
+ | `m` | Filter: This Month |
69
+ | `a` | Filter: All Time |
70
+ | `r` | Refresh data |
71
+ | `q` | Quit |
72
+
73
+ ### Quick stats (no TUI)
74
+
75
+ ### Check data sources
76
+
77
+ ```bash
78
+ claude-matrix doctor
79
+ ```
80
+
81
+ ---
82
+
83
+ ## How it works
84
+
85
+ Claude Code writes session transcripts to `~/.claude/projects/`. Claude Matrix reads those JSONL files directly on each refresh — no background daemon, no database, no setup.
86
+
87
+ ```
88
+ ~/.claude/
89
+ ├── stats-cache.json # pre-computed stats (optional bonus)
90
+ ├── history.jsonl # all prompts ever typed
91
+ └── projects/
92
+ └── {encoded-path}/
93
+ └── {session-uuid}.jsonl ← parsed on every run
94
+ ```
95
+
96
+ Each session file is parsed for:
97
+ - Tool calls (`Bash`, `Read`, `Write`, `Edit`, …)
98
+ - Token usage and model name (for cost estimation)
99
+ - Timestamps (for streaks and heatmap)
100
+ - Git branch (shown in Projects panel)
101
+
102
+ ---
103
+
104
+ ## Cost estimation
105
+
106
+ Estimated costs use published Claude API pricing (per 1M tokens):
107
+
108
+ | Model | Input | Output | Cache read | Cache write |
109
+ |-------|------:|-------:|-----------:|------------:|
110
+ | claude-opus-4.x | $15 | $75 | $1.50 | $18.75 |
111
+ | claude-sonnet-4.x | $3 | $15 | $0.30 | $3.75 |
112
+ | claude-haiku-4.x | $0.80 | $4 | $0.08 | $1.00 |
113
+
114
+ > These are estimates. Actual billed amounts may differ.
115
+
116
+ ---
117
+
118
+ ## Project structure
119
+
120
+ ```
121
+ claude-matrix/
122
+ ├── bin/
123
+ │ └── claude-matrix # executable
124
+ ├── lib/
125
+ │ ├── claude_matrix.rb # entry point
126
+ │ └── claude_matrix/
127
+ │ ├── cli.rb # command router + interactive loop
128
+ │ ├── readers/
129
+ │ │ ├── session_parser.rb # JSONL parser
130
+ │ │ ├── stats_reader.rb # stats-cache.json reader
131
+ │ │ └── history_reader.rb # history.jsonl reader
132
+ │ ├── analyzers/
133
+ │ │ └── metrics.rb # streaks, work modes, cost, heatmap grid
134
+ │ └── visualizers/
135
+ │ └── dashboard.rb # full-screen TUI renderer
136
+ ├── Gemfile
137
+ ├── claude-matrix.gemspec
138
+ └── README.md
139
+ ```
140
+
141
+ ---
142
+
143
+
144
+
145
+ ## Releasing a new version
146
+
147
+ 1. Bump `VERSION` in `lib/claude_matrix/version.rb`
148
+ 2. Add an entry to `CHANGELOG.md`
149
+ 3. Commit, tag, and push:
150
+
151
+ ```bash
152
+ git add -A
153
+ git commit -m "Release v1.x.x"
154
+ git tag v1.x.x
155
+ git push && git push --tags
156
+ ```
157
+
158
+ GitHub Actions picks up the tag and publishes to RubyGems automatically.
159
+
160
+ > **One-time setup:** add your RubyGems API key as a repository secret named `RUBYGEMS_API_KEY`
161
+ > (GitHub repo → Settings → Secrets and variables → Actions → New repository secret).
162
+ > Get your key from [rubygems.org/profile/edit](https://rubygems.org/profile/edit).
163
+
164
+ ---
165
+
166
+ ## Contributing
167
+
168
+ 1. Fork the repo
169
+ 2. Create a branch: `git checkout -b my-feature`
170
+ 3. Make your changes
171
+ 4. Open a pull request
172
+
173
+ Bug reports and feature requests are welcome via [GitHub Issues](https://github.com/kapilbhosale/claude-matrix/issues).
174
+
175
+ ---
176
+
177
+ ## License
178
+
179
+ MIT — see [LICENSE](LICENSE).
180
+
181
+ ---
182
+
183
+ *Built with Ruby, [TTY toolkit](https://ttytoolkit.org), and [Pastel](https://github.com/piotrmurach/pastel).*
data/bin/claude-matrix ADDED
@@ -0,0 +1,7 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ $LOAD_PATH.unshift(File.expand_path("../lib", __dir__))
4
+
5
+ require "claude_matrix/cli"
6
+
7
+ ClaudeMatrix::CLI.run(ARGV)
@@ -0,0 +1,206 @@
1
+ require "date"
2
+
3
+ module ClaudeMatrix
4
+ module Analyzers
5
+ class Metrics
6
+ # Claude pricing per 1M tokens (as of early 2026)
7
+ MODEL_PRICING = {
8
+ "claude-opus-4-6" => { input: 15.0, output: 75.0, cache_read: 1.5, cache_write: 18.75 },
9
+ "claude-opus-4-5" => { input: 15.0, output: 75.0, cache_read: 1.5, cache_write: 18.75 },
10
+ "claude-sonnet-4-6" => { input: 3.0, output: 15.0, cache_read: 0.3, cache_write: 3.75 },
11
+ "claude-sonnet-4-5" => { input: 3.0, output: 15.0, cache_read: 0.3, cache_write: 3.75 },
12
+ "claude-haiku-4-5-20251001" => { input: 0.8, output: 4.0, cache_read: 0.08, cache_write: 1.0 },
13
+ "claude-haiku-4-5" => { input: 0.8, output: 4.0, cache_read: 0.08, cache_write: 1.0 },
14
+ }.freeze
15
+
16
+ DEFAULT_PRICE = { input: 3.0, output: 15.0, cache_read: 0.3, cache_write: 3.75 }.freeze
17
+
18
+ WORK_MODE_TOOLS = {
19
+ exploration: %w[Read Glob Grep WebSearch WebFetch read_file list_files search_files grep],
20
+ building: %w[Write Edit NotebookEdit write_to_file edit_file new_file],
21
+ testing: %w[Bash execute_command bash]
22
+ }.freeze
23
+
24
+ def self.compute(sessions)
25
+ return empty_metrics if sessions.empty?
26
+
27
+ total_input = sessions.sum { |s| s[:input_tokens] }
28
+ total_output = sessions.sum { |s| s[:output_tokens] }
29
+ total_cache_r = sessions.sum { |s| s[:cache_read] }
30
+ total_cache_w = sessions.sum { |s| s[:cache_create] }
31
+
32
+ all_tools = sessions.flat_map { |s| s[:tools] }
33
+ tool_counts = all_tools.tally.sort_by { |_, c| -c }.to_h
34
+
35
+ daily = build_daily_activity(sessions)
36
+ streaks = calculate_streaks(daily)
37
+ hour_counts = build_hour_counts(sessions)
38
+
39
+ longest = sessions.max_by { |s| s[:duration_secs] }
40
+ most_messages = sessions.max_by { |s| s[:message_count] }
41
+
42
+ projects = sessions.group_by { |s| s[:project] }
43
+ most_active_project = projects.max_by { |_, ss| ss.sum { |s| s[:message_count] } }&.first
44
+
45
+ {
46
+ total_sessions: sessions.size,
47
+ total_messages: sessions.sum { |s| s[:message_count] },
48
+ total_tool_calls: all_tools.size,
49
+ total_input_tokens: total_input,
50
+ total_output_tokens: total_output,
51
+ total_cache_read: total_cache_r,
52
+ total_cache_write: total_cache_w,
53
+ estimated_cost: estimate_cost(sessions),
54
+ first_session: sessions.map { |s| s[:started_at] }.min,
55
+ projects: projects.keys.size,
56
+ project_names: projects.keys,
57
+ most_active_project: most_active_project,
58
+ tool_counts: tool_counts,
59
+ top_tools: tool_counts.first(10).to_h,
60
+ favorite_tool: tool_counts.first&.first,
61
+ daily_activity: daily,
62
+ streaks: streaks,
63
+ hour_counts: hour_counts,
64
+ hour_day_grid: build_hour_day_grid(sessions),
65
+ work_modes: calculate_work_modes(all_tools),
66
+ longest_session: longest,
67
+ most_messages_session: most_messages,
68
+ most_productive_day: most_productive_day(daily),
69
+ busiest_hour: hour_counts.max_by { |_, c| c }&.first,
70
+ models_used: sessions.map { |s| s[:model] }.compact.uniq,
71
+ per_project: build_per_project(sessions)
72
+ }
73
+ end
74
+
75
+ def self.estimate_cost(sessions)
76
+ total = 0.0
77
+ sessions.each do |s|
78
+ price = find_price(s[:model])
79
+ total += (s[:input_tokens].to_f / 1_000_000) * price[:input]
80
+ total += (s[:output_tokens].to_f / 1_000_000) * price[:output]
81
+ total += (s[:cache_read].to_f / 1_000_000) * price[:cache_read]
82
+ total += (s[:cache_create].to_f / 1_000_000) * price[:cache_write]
83
+ end
84
+ total.round(4)
85
+ end
86
+
87
+ private
88
+
89
+ def self.find_price(model)
90
+ return DEFAULT_PRICE unless model
91
+ MODEL_PRICING.find { |k, _| model.start_with?(k) }&.last || DEFAULT_PRICE
92
+ end
93
+
94
+ def self.build_daily_activity(sessions)
95
+ daily = Hash.new { |h, k| h[k] = { sessions: 0, messages: 0, tools: 0 } }
96
+
97
+ sessions.each do |s|
98
+ date = s[:started_at].to_date.to_s
99
+ daily[date][:sessions] += 1
100
+ daily[date][:messages] += s[:message_count]
101
+ daily[date][:tools] += s[:tool_count]
102
+ end
103
+
104
+ daily.sort.to_h
105
+ end
106
+
107
+ def self.calculate_streaks(daily)
108
+ return { current: 0, longest: 0, active_days: 0 } if daily.empty?
109
+
110
+ dates = daily.keys.map { |d| Date.parse(d) }.sort
111
+ active_days = dates.size
112
+
113
+ # Current streak
114
+ current = 0
115
+ expected = Date.today
116
+ dates.sort.reverse.each do |date|
117
+ break if date < expected - 1
118
+ if date == expected || date == expected - 1
119
+ current += 1
120
+ expected = date - 1
121
+ end
122
+ end
123
+
124
+ # Also allow today to count if used today
125
+ current = 1 if current == 0 && dates.include?(Date.today)
126
+
127
+ # Longest streak
128
+ longest = 1
129
+ temp = 1
130
+ dates.each_cons(2) do |d1, d2|
131
+ if (d2 - d1).to_i == 1
132
+ temp += 1
133
+ longest = [longest, temp].max
134
+ else
135
+ temp = 1
136
+ end
137
+ end
138
+
139
+ { current: current, longest: longest, active_days: active_days }
140
+ end
141
+
142
+ def self.build_hour_counts(sessions)
143
+ counts = Hash.new(0)
144
+ sessions.each do |s|
145
+ counts[s[:started_at].hour] += 1
146
+ end
147
+ counts
148
+ end
149
+
150
+ def self.calculate_work_modes(tools)
151
+ total = tools.size.to_f
152
+ return { exploration: 0, building: 0, testing: 0 } if total.zero?
153
+
154
+ result = {}
155
+ WORK_MODE_TOOLS.each do |mode, mode_tools|
156
+ count = tools.count { |t| mode_tools.include?(t) }
157
+ result[mode] = (count / total * 100).round
158
+ end
159
+ result
160
+ end
161
+
162
+ def self.most_productive_day(daily)
163
+ return nil if daily.empty?
164
+ date, data = daily.max_by { |_, d| d[:sessions] }
165
+ { date: date, sessions: data[:sessions], messages: data[:messages] }
166
+ end
167
+
168
+ def self.build_hour_day_grid(sessions)
169
+ # 7 rows (wday: 0=Sun..6=Sat) × 24 cols (hours)
170
+ grid = Array.new(7) { Array.new(24, 0) }
171
+ sessions.each do |s|
172
+ grid[s[:started_at].wday][s[:started_at].hour] += 1
173
+ end
174
+ grid
175
+ end
176
+
177
+ def self.build_per_project(sessions)
178
+ sessions.group_by { |s| s[:project] }.transform_values do |ss|
179
+ {
180
+ sessions: ss.size,
181
+ messages: ss.sum { |s| s[:message_count] },
182
+ tools: ss.sum { |s| s[:tool_count] },
183
+ last_active: ss.map { |s| s[:ended_at] }.max,
184
+ branches: ss.map { |s| s[:git_branch] }.compact.uniq
185
+ }
186
+ end.sort_by { |_, v| -v[:sessions] }.to_h
187
+ end
188
+
189
+ def self.empty_metrics
190
+ {
191
+ total_sessions: 0, total_messages: 0, total_tool_calls: 0,
192
+ total_input_tokens: 0, total_output_tokens: 0,
193
+ total_cache_read: 0, total_cache_write: 0, hour_day_grid: Array.new(7) { Array.new(24, 0) },
194
+ estimated_cost: 0.0, first_session: nil, projects: 0,
195
+ project_names: [], most_active_project: nil,
196
+ tool_counts: {}, top_tools: {}, favorite_tool: nil,
197
+ daily_activity: {}, streaks: { current: 0, longest: 0, active_days: 0 },
198
+ hour_counts: {}, work_modes: { exploration: 0, building: 0, testing: 0 },
199
+ longest_session: nil, most_messages_session: nil,
200
+ most_productive_day: nil, busiest_hour: nil,
201
+ models_used: [], per_project: {}
202
+ }
203
+ end
204
+ end
205
+ end
206
+ end
@@ -0,0 +1,177 @@
1
+ require_relative "../claude_matrix"
2
+ require "tty-reader"
3
+ require "tty-cursor"
4
+ require "date"
5
+
6
+ module ClaudeMatrix
7
+ class CLI
8
+ def self.run(args)
9
+ case (args.first || "dashboard")
10
+ when "dashboard", "d", nil then DashboardCommand.new.run
11
+ when "stats" then StatsCommand.new(args[1..]).run
12
+ when "sync" then SyncCommand.new.run
13
+ when "doctor" then DoctorCommand.new.run
14
+ when "version", "--version", "-v" then puts "claude-matrix #{VERSION}"
15
+ when "help", "--help", "-h" then puts help_text
16
+ else
17
+ warn "Unknown command: #{args.first}"
18
+ warn help_text
19
+ exit 1
20
+ end
21
+ end
22
+
23
+ def self.help_text
24
+ <<~HELP
25
+ Claude Matrix #{VERSION} — Claude Code Analytics Dashboard
26
+
27
+ Usage:
28
+ claude-matrix Launch interactive dashboard
29
+ claude-matrix stats Print stats to stdout
30
+ claude-matrix stats --today / --week / --month
31
+ claude-matrix doctor Check data sources
32
+ claude-matrix version
33
+
34
+ Dashboard keys:
35
+ t/w/m/a Filter: Today / Week / Month / All Time
36
+ r Refresh
37
+ q Quit
38
+ HELP
39
+ end
40
+
41
+ # ─── Interactive dashboard ────────────────────────────────────────────────
42
+
43
+ class DashboardCommand
44
+ FILTERS = %i[today week month all].freeze
45
+
46
+ def run
47
+ cursor = TTY::Cursor
48
+ reader = TTY::Reader.new(interrupt: :exit)
49
+ filter = :all
50
+ all_sessions = nil # lazy-load once, refresh on 'r'
51
+
52
+ loop do
53
+ all_sessions ||= ClaudeMatrix.load_sessions
54
+ metrics = Analyzers::Metrics.compute(filter_sessions(all_sessions, filter))
55
+
56
+ print cursor.clear_screen
57
+ print cursor.move_to(0, 0)
58
+ Visualizers::Dashboard.new(metrics, filter: filter).render
59
+
60
+ char = reader.read_char
61
+ case char&.downcase
62
+ when "q", "\e" then break
63
+ when "r" then all_sessions = nil # force reload
64
+ when "t" then filter = :today
65
+ when "w" then filter = :week
66
+ when "m" then filter = :month
67
+ when "a" then filter = :all
68
+ end
69
+ end
70
+ rescue TTY::Reader::InputInterrupt
71
+ # clean exit on Ctrl-C
72
+ ensure
73
+ puts TTY::Cursor.show
74
+ puts # move off last dashboard line
75
+ end
76
+
77
+ private
78
+
79
+ def filter_sessions(sessions, filter)
80
+ case filter
81
+ when :today
82
+ d = Date.today
83
+ sessions.select { |s| s[:started_at].to_date == d }
84
+ when :week
85
+ start = Date.today - Date.today.wday
86
+ sessions.select { |s| s[:started_at].to_date >= start }
87
+ when :month
88
+ sessions.select { |s|
89
+ s[:started_at].year == Date.today.year &&
90
+ s[:started_at].month == Date.today.month
91
+ }
92
+ else
93
+ sessions
94
+ end
95
+ end
96
+ end
97
+
98
+ # ─── Stats command ────────────────────────────────────────────────────────
99
+
100
+ class StatsCommand
101
+ def initialize(flags)
102
+ @filter = if flags.include?("--today") then :today
103
+ elsif flags.include?("--week") then :week
104
+ elsif flags.include?("--month") then :month
105
+ else :all
106
+ end
107
+ end
108
+
109
+ def run
110
+ all = ClaudeMatrix.load_sessions
111
+ sess = filter_sessions(all, @filter)
112
+ m = Analyzers::Metrics.compute(sess)
113
+
114
+ label = { today: "Today", week: "This Week", month: "This Month", all: "All Time" }[@filter]
115
+ puts "\n Claude Matrix — #{label}"
116
+ puts " " + "─" * 40
117
+ puts " Sessions: #{m[:total_sessions]}"
118
+ puts " Messages: #{m[:total_messages]}"
119
+ puts " Tools: #{m[:total_tool_calls]}"
120
+ puts " Tokens In: #{fmt(m[:total_input_tokens])}"
121
+ puts " Tokens Out: #{fmt(m[:total_output_tokens])}"
122
+ puts " Est. Cost: $#{"%.4f" % m[:estimated_cost]}"
123
+ puts " Streak: #{m[:streaks][:current]} days (best: #{m[:streaks][:longest]})"
124
+ puts " Top Tool: #{m[:favorite_tool]} (#{m[:tool_counts][m[:favorite_tool]]})" if m[:favorite_tool]
125
+ puts
126
+ end
127
+
128
+ private
129
+
130
+ def filter_sessions(sessions, filter)
131
+ DashboardCommand.new.send(:filter_sessions, sessions, filter)
132
+ end
133
+
134
+ def fmt(n)
135
+ n >= 1_000_000 ? "%.1fM" % (n / 1_000_000.0) :
136
+ n >= 1_000 ? "%.1fK" % (n / 1_000.0) : n.to_s
137
+ end
138
+ end
139
+
140
+ # ─── Sync command ─────────────────────────────────────────────────────────
141
+
142
+ class SyncCommand
143
+ def run
144
+ files = Readers::SessionParser.jsonl_files
145
+ puts "\nScanning #{files.size} session files..."
146
+ count = 0
147
+ files.each_with_index do |f, i|
148
+ print "\rParsing #{i + 1}/#{files.size}..."
149
+ count += 1 if Readers::SessionParser.parse_file(f)
150
+ end
151
+ puts "\rDone! #{count}/#{files.size} sessions parsed. "
152
+ puts "Run 'claude-matrix' to view the dashboard."
153
+ end
154
+ end
155
+
156
+ # ─── Doctor command ───────────────────────────────────────────────────────
157
+
158
+ class DoctorCommand
159
+ def run
160
+ puts "\n Claude Matrix — Doctor\n " + "─" * 36
161
+ check "~/.claude/ exists", Dir.exist?(File.expand_path("~/.claude"))
162
+ check "~/.claude/projects/ exists", Dir.exist?(File.expand_path("~/.claude/projects"))
163
+ check "~/.claude/history.jsonl", File.exist?(File.expand_path("~/.claude/history.jsonl"))
164
+ check "stats-cache.json", Readers::StatsReader.available?
165
+ files = Readers::SessionParser.jsonl_files
166
+ check "Session files found (#{files.size})", files.any?
167
+ puts
168
+ end
169
+
170
+ private
171
+
172
+ def check(label, ok)
173
+ puts " #{ok ? "\e[32m✓\e[0m" : "\e[31m✗\e[0m"} #{label}"
174
+ end
175
+ end
176
+ end
177
+ end
@@ -0,0 +1,38 @@
1
+ require "json"
2
+
3
+ module ClaudeMatrix
4
+ module Readers
5
+ class HistoryReader
6
+ HISTORY_PATH = File.expand_path("~/.claude/history.jsonl")
7
+
8
+ def self.read
9
+ return [] unless File.exist?(HISTORY_PATH)
10
+
11
+ File.readlines(HISTORY_PATH, chomp: true).filter_map do |line|
12
+ JSON.parse(line)
13
+ rescue JSON::ParserError
14
+ nil
15
+ end
16
+ end
17
+
18
+ def self.top_words(limit: 10)
19
+ stop_words = %w[i a the an is it in on at to for of and or but
20
+ this that with from can you what how do be have
21
+ are was were will would could should may might
22
+ its your my we our let me now please just also
23
+ if so there not no yes then when they he she]
24
+
25
+ all_words = read.flat_map do |entry|
26
+ text = entry["display"].to_s.downcase
27
+ text.scan(/[a-z]{3,}/)
28
+ end
29
+
30
+ all_words
31
+ .reject { |w| stop_words.include?(w) }
32
+ .tally
33
+ .sort_by { |_, count| -count }
34
+ .first(limit)
35
+ end
36
+ end
37
+ end
38
+ end