cctrack-ai 0.2.0__tar.gz → 0.3.0__tar.gz

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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: cctrack-ai
3
- Version: 0.2.0
3
+ Version: 0.3.0
4
4
  Summary: Track and analyze AI token usage from Claude Code and other CLI agents
5
5
  Project-URL: Homepage, https://github.com/Sk786/cctrack
6
6
  Project-URL: Bug Tracker, https://github.com/Sk786/cctrack/issues
@@ -28,9 +28,11 @@ Description-Content-Type: text/markdown
28
28
 
29
29
  # cctrack
30
30
 
31
- Track and analyze your AI token usage from Claude Code and other CLI agents — directly in the terminal.
31
+ Know your AI activity. Keep it local. Share a heatmap.
32
32
 
33
- No API key needed. Reads local session files, stores in SQLite, shows cost breakdowns with charts.
33
+ `cctrack` tracks AI usage from Claude Code and OpenAI, then turns it into useful terminal reports and a GitHub-style activity heatmap. Claude Code tracking reads local session files—no account or API key required. Export a heatmap as SVG for your profile or README.
34
+
35
+ **Local-first:** your session history stays on your machine. No account or upload is required to use the CLI.
34
36
 
35
37
  ```
36
38
  $ cctrack today
@@ -54,13 +56,25 @@ pip install cctrack-ai
54
56
  Or from source:
55
57
 
56
58
  ```bash
57
- git clone https://github.com/yourusername/cctrack
59
+ git clone https://github.com/Sk786/cctrack.git
58
60
  cd cctrack
59
61
  pip install -e .
60
62
  ```
61
63
 
62
64
  ## Commands
63
65
 
66
+ ### `cctrack heatmap`
67
+ See your AI activity across the last year, directly in the terminal. The default intensity is total tokens; choose estimated spend or active days instead.
68
+
69
+ ```bash
70
+ cctrack heatmap
71
+ cctrack heatmap --metric cost --days 90
72
+ cctrack heatmap --metric active-days
73
+ cctrack heatmap --output ~/ai-activity.svg
74
+ ```
75
+
76
+ The exported SVG contains dates and relative activity levels only—never prompts or project names. It is generated locally and is not uploaded anywhere. Heatmap intensity is a picture of usage, not a productivity score.
77
+
64
78
  ### `cctrack today`
65
79
  Token usage and cost for the current day, broken down by model. Includes a stacked token-type chart.
66
80
 
@@ -183,8 +197,8 @@ $ cctrack prices
183
197
  ┃ Model ┃ Input ┃ Output ┃ Cache Write ┃ Cache Read ┃
184
198
  ┡━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━╇━━━━━━━━╇━━━━━━━━━━━━━╇━━━━━━━━━━━━┩
185
199
  │ claude-sonnet-4-6 │ $3.00 │ $15.00 │ $3.75 │ $0.30 │
186
- │ claude-opus-4-8 │ $15.00 │ $75.00 │ $18.75 │ $1.50 │
187
- │ claude-haiku-4-5 │ $0.80 │ $4.00 │ $1.00 │ $0.08 │
200
+ │ claude-opus-4-8 │ $5.00 │ $25.00 │ $6.25 │ $0.50 │
201
+ │ claude-haiku-4-5 │ $1.00 │ $5.00 │ $1.25 │ $0.10 │
188
202
  │ default (fallback) │ $3.00 │ $15.00 │ $3.75 │ $0.30 │
189
203
  └────────────────────┴────────┴────────┴─────────────┴────────────┘
190
204
 
@@ -236,7 +250,7 @@ cctrack-prompt --format cost # [CC $5.43]
236
250
  2. `cctrack` parses these files and stores token counts in `~/.config/cctrack/usage.db`
237
251
  3. All commands query SQLite — fast and offline
238
252
 
239
- No API key needed. Everything is local.
253
+ Claude Code tracking and local reports work offline. OpenAI usage can be imported on demand with `cctrack openai-sync` and requires an OpenAI organization API key. The CLI does not upload local transcripts.
240
254
 
241
255
  ## Customizing Prices
242
256
 
@@ -263,9 +277,11 @@ All cost values are color coded consistently across every command:
263
277
 
264
278
  ## Roadmap
265
279
 
266
- - **v0.2** — Codex / OpenAI support via the Usage API
267
- - **v0.3** — Gemini CLI support
268
- - **v0.4** — Budget alerts and weekly digest
280
+ - **Available** — Claude Code tracking, OpenAI Usage API import, reports, and local activity heatmap/SVG export
281
+ - **Exploring** — Opt-in shareable profiles and private team boards
282
+ - **Future** — More local AI usage sources based on demand
283
+
284
+ Product site: [cctrack.sandeepk.dev](https://cctrack.sandeepk.dev/)
269
285
 
270
286
  ## License
271
287
 
@@ -1,8 +1,10 @@
1
1
  # cctrack
2
2
 
3
- Track and analyze your AI token usage from Claude Code and other CLI agents — directly in the terminal.
3
+ Know your AI activity. Keep it local. Share a heatmap.
4
4
 
5
- No API key needed. Reads local session files, stores in SQLite, shows cost breakdowns with charts.
5
+ `cctrack` tracks AI usage from Claude Code and OpenAI, then turns it into useful terminal reports and a GitHub-style activity heatmap. Claude Code tracking reads local session files—no account or API key required. Export a heatmap as SVG for your profile or README.
6
+
7
+ **Local-first:** your session history stays on your machine. No account or upload is required to use the CLI.
6
8
 
7
9
  ```
8
10
  $ cctrack today
@@ -26,13 +28,25 @@ pip install cctrack-ai
26
28
  Or from source:
27
29
 
28
30
  ```bash
29
- git clone https://github.com/yourusername/cctrack
31
+ git clone https://github.com/Sk786/cctrack.git
30
32
  cd cctrack
31
33
  pip install -e .
32
34
  ```
33
35
 
34
36
  ## Commands
35
37
 
38
+ ### `cctrack heatmap`
39
+ See your AI activity across the last year, directly in the terminal. The default intensity is total tokens; choose estimated spend or active days instead.
40
+
41
+ ```bash
42
+ cctrack heatmap
43
+ cctrack heatmap --metric cost --days 90
44
+ cctrack heatmap --metric active-days
45
+ cctrack heatmap --output ~/ai-activity.svg
46
+ ```
47
+
48
+ The exported SVG contains dates and relative activity levels only—never prompts or project names. It is generated locally and is not uploaded anywhere. Heatmap intensity is a picture of usage, not a productivity score.
49
+
36
50
  ### `cctrack today`
37
51
  Token usage and cost for the current day, broken down by model. Includes a stacked token-type chart.
38
52
 
@@ -155,8 +169,8 @@ $ cctrack prices
155
169
  ┃ Model ┃ Input ┃ Output ┃ Cache Write ┃ Cache Read ┃
156
170
  ┡━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━╇━━━━━━━━╇━━━━━━━━━━━━━╇━━━━━━━━━━━━┩
157
171
  │ claude-sonnet-4-6 │ $3.00 │ $15.00 │ $3.75 │ $0.30 │
158
- │ claude-opus-4-8 │ $15.00 │ $75.00 │ $18.75 │ $1.50 │
159
- │ claude-haiku-4-5 │ $0.80 │ $4.00 │ $1.00 │ $0.08 │
172
+ │ claude-opus-4-8 │ $5.00 │ $25.00 │ $6.25 │ $0.50 │
173
+ │ claude-haiku-4-5 │ $1.00 │ $5.00 │ $1.25 │ $0.10 │
160
174
  │ default (fallback) │ $3.00 │ $15.00 │ $3.75 │ $0.30 │
161
175
  └────────────────────┴────────┴────────┴─────────────┴────────────┘
162
176
 
@@ -208,7 +222,7 @@ cctrack-prompt --format cost # [CC $5.43]
208
222
  2. `cctrack` parses these files and stores token counts in `~/.config/cctrack/usage.db`
209
223
  3. All commands query SQLite — fast and offline
210
224
 
211
- No API key needed. Everything is local.
225
+ Claude Code tracking and local reports work offline. OpenAI usage can be imported on demand with `cctrack openai-sync` and requires an OpenAI organization API key. The CLI does not upload local transcripts.
212
226
 
213
227
  ## Customizing Prices
214
228
 
@@ -235,9 +249,11 @@ All cost values are color coded consistently across every command:
235
249
 
236
250
  ## Roadmap
237
251
 
238
- - **v0.2** — Codex / OpenAI support via the Usage API
239
- - **v0.3** — Gemini CLI support
240
- - **v0.4** — Budget alerts and weekly digest
252
+ - **Available** — Claude Code tracking, OpenAI Usage API import, reports, and local activity heatmap/SVG export
253
+ - **Exploring** — Opt-in shareable profiles and private team boards
254
+ - **Future** — More local AI usage sources based on demand
255
+
256
+ Product site: [cctrack.sandeepk.dev](https://cctrack.sandeepk.dev/)
241
257
 
242
258
  ## License
243
259
 
@@ -409,6 +409,7 @@ def render_setup_guide(projects_dir: str, db_path: str, prices_path: str, user_p
409
409
  cmds = [
410
410
  ("cctrack today", "Token usage for today"),
411
411
  ("cctrack weekly", "Last 7 days breakdown + bar chart"),
412
+ ("cctrack heatmap", "AI activity calendar + shareable SVG export"),
412
413
  ("cctrack stats", "All-time summary by model"),
413
414
  ("cctrack projects", "Usage broken down by project"),
414
415
  ("cctrack prices", "Show model pricing"),
@@ -0,0 +1,200 @@
1
+ """Daily usage aggregation and GitHub-style activity heatmaps."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from datetime import date, timedelta
6
+ from html import escape
7
+ from pathlib import Path
8
+
9
+ from rich.text import Text
10
+
11
+ from .config import get_pricing
12
+
13
+ METRICS = ("tokens", "cost", "active-days")
14
+ _LEVEL_COLORS = ("#161b22", "#0e4429", "#006d32", "#26a641", "#39d353")
15
+ _LEVEL_STYLES = ("#484f58", "#3fb950", "#3fb950", "#56d364", "#7ee787")
16
+
17
+
18
+ def aggregate_daily(rows: list[dict], metric: str = "tokens") -> dict[date, float]:
19
+ """Aggregate stored usage rows into a date -> metric value mapping."""
20
+ if metric not in METRICS:
21
+ raise ValueError(f"metric must be one of: {', '.join(METRICS)}")
22
+
23
+ values: dict[date, float] = {}
24
+ prices_by_model: dict[str, dict[str, float]] = {}
25
+ for row in rows:
26
+ day = date.fromisoformat(row["timestamp"][:10])
27
+ tokens = sum(
28
+ row.get(key, 0)
29
+ for key in ("input_tokens", "output_tokens", "cache_write_tokens", "cache_read_tokens")
30
+ )
31
+ if metric == "tokens":
32
+ amount = tokens
33
+ elif metric == "cost":
34
+ model = row["model"]
35
+ if model not in prices_by_model:
36
+ prices_by_model[model], _ = get_pricing(model)
37
+ prices = prices_by_model[model]
38
+ amount = (
39
+ row.get("input_tokens", 0) * prices["input"]
40
+ + row.get("output_tokens", 0) * prices["output"]
41
+ + row.get("cache_write_tokens", 0) * prices["cache_write"]
42
+ + row.get("cache_read_tokens", 0) * prices["cache_read"]
43
+ ) / 1_000_000
44
+ else:
45
+ amount = 1 if tokens > 0 else 0
46
+ if metric == "active-days":
47
+ values[day] = float(bool(values.get(day, 0) or amount))
48
+ else:
49
+ values[day] = values.get(day, 0) + amount
50
+
51
+ return values
52
+
53
+
54
+ def _levels(values: dict[date, float]) -> dict[date, int]:
55
+ """Map values to zero plus four relative-intensity levels."""
56
+ active = sorted(value for value in values.values() if value > 0)
57
+ if not active:
58
+ return {day: 0 for day in values}
59
+
60
+ result = {}
61
+ for day, value in values.items():
62
+ if value <= 0:
63
+ result[day] = 0
64
+ else:
65
+ rank = sum(item <= value for item in active) / len(active)
66
+ result[day] = min(4, max(1, int(rank * 4)))
67
+ return result
68
+
69
+
70
+ def _calendar(start: date, end: date) -> tuple[date, list[list[date]]]:
71
+ first_monday = start - timedelta(days=start.weekday())
72
+ weeks: list[list[date]] = []
73
+ current = first_monday
74
+ while current <= end:
75
+ weeks.append([current + timedelta(days=offset) for offset in range(7)])
76
+ current += timedelta(days=7)
77
+ return first_monday, weeks
78
+
79
+
80
+ def _metric_total(values: dict[date, float], metric: str) -> str:
81
+ if metric == "tokens":
82
+ return f"{sum(values.values()):,.0f} tokens"
83
+ if metric == "cost":
84
+ return f"${sum(values.values()):,.2f} estimated spend"
85
+ return f"{sum(value > 0 for value in values.values())} active days"
86
+
87
+
88
+ def render_heatmap(
89
+ values: dict[date, float], start: date, end: date, metric: str = "tokens"
90
+ ) -> Text:
91
+ """Render a compact, accessible-in-terminal activity grid."""
92
+ _, weeks = _calendar(start, end)
93
+ level_by_day = _levels(values)
94
+ output = Text()
95
+ output.append(f"AI activity · {start:%b %d, %Y} – {end:%b %d, %Y}\n", style="bold")
96
+ output.append(f"{_metric_total(values, metric)} · relative intensity\n\n", style="dim")
97
+
98
+ month_labels = [" "] * len(weeks)
99
+ previous_month: tuple[int, int] | None = None
100
+ for column, week in enumerate(weeks):
101
+ visible_day = next((day for day in week if start <= day <= end), None)
102
+ if visible_day and (visible_day.year, visible_day.month) != previous_month:
103
+ for offset, char in enumerate(visible_day.strftime("%b")):
104
+ if column + offset < len(month_labels):
105
+ month_labels[column + offset] = char
106
+ previous_month = (visible_day.year, visible_day.month)
107
+
108
+ output.append(" ")
109
+ output.append("".join(month_labels))
110
+ output.append("\n")
111
+
112
+ for weekday, label in enumerate(("Mo", "Tu", "We", "Th", "Fr", "Sa", "Su")):
113
+ output.append(f"{label} ", style="dim")
114
+ for week in weeks:
115
+ day = week[weekday]
116
+ if day < start or day > end:
117
+ output.append(" ")
118
+ continue
119
+ level = level_by_day.get(day, 0)
120
+ output.append("■", style=_LEVEL_STYLES[level])
121
+ output.append("\n")
122
+
123
+ output.append("\nLess ", style="dim")
124
+ for level in range(5):
125
+ output.append("■", style=_LEVEL_STYLES[level])
126
+ output.append(" ")
127
+ output.append("More · ", style="dim")
128
+ output.append(metric, style="bold")
129
+ return output
130
+
131
+
132
+ def export_svg(
133
+ values: dict[date, float], start: date, end: date, metric: str, path: Path
134
+ ) -> Path:
135
+ """Write a self-contained SVG heatmap and return its path."""
136
+ _, weeks = _calendar(start, end)
137
+ level_by_day = _levels(values)
138
+ cell = 12
139
+ gap = 3
140
+ step = cell + gap
141
+ left = 42
142
+ top = 42
143
+ width = left + len(weeks) * step + 8
144
+ height = top + 7 * step + 54
145
+ months: list[str] = []
146
+ prev_month = None
147
+ for column, week in enumerate(weeks):
148
+ month_day = next((day for day in week if start <= day <= end), None)
149
+ if month_day and (month_day.year, month_day.month) != prev_month:
150
+ months.append(
151
+ f'<text x="{left + column * step}" y="{top - 12}" class="month">{month_day:%b}</text>'
152
+ )
153
+ prev_month = (month_day.year, month_day.month)
154
+
155
+ cells: list[str] = []
156
+ for column, week in enumerate(weeks):
157
+ for weekday, day in enumerate(week):
158
+ if day < start or day > end:
159
+ continue
160
+ level = level_by_day.get(day, 0)
161
+ x = left + column * step
162
+ y = top + weekday * step
163
+ tooltip = "no activity" if level == 0 else f"intensity {level} of 4"
164
+ cells.append(
165
+ f'<rect x="{x}" y="{y}" width="{cell}" height="{cell}" rx="3" '
166
+ f'fill="{_LEVEL_COLORS[level]}"><title>{day.isoformat()}: {tooltip}</title></rect>'
167
+ )
168
+
169
+ weekdays = "".join(
170
+ f'<text x="0" y="{top + i * step + 10}" class="weekday">{name}</text>'
171
+ for i, name in enumerate(("Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"))
172
+ )
173
+ title = f"AI activity · {start:%b %d, %Y} – {end:%b %d, %Y}"
174
+ description = f"Selected metric: {metric}. Color shows relative daily activity intensity."
175
+ legend_y = top + 7 * step + 15
176
+ legend = (
177
+ f'<text x="{left}" y="{legend_y + 9}" class="legend-label">Less</text>'
178
+ + "".join(
179
+ f'<rect x="{left + 34 + level * 16}" y="{legend_y}" width="11" height="11" '
180
+ f'rx="3" fill="{_LEVEL_COLORS[level]}"/>'
181
+ for level in range(5)
182
+ )
183
+ + f'<text x="{left + 122}" y="{legend_y + 9}" class="legend-label">More</text>'
184
+ )
185
+ svg = f'''<svg xmlns="http://www.w3.org/2000/svg" role="img" aria-labelledby="svg-title svg-desc"
186
+ viewBox="0 0 {width} {height}" width="{width}" height="{height}">
187
+ <title id="svg-title">{escape(title)}</title>
188
+ <desc id="svg-desc">{escape(description)}</desc>
189
+ <style>text {{ font-family: system-ui, sans-serif; fill: #c9d1d9; }}
190
+ .title {{ font-size: 14px; font-weight: 650; }} .subtitle {{ font-size: 11px; fill: #8b949e; }}
191
+ .month {{ font-size: 10px; fill: #8b949e; }} .weekday {{ font-size: 9px; fill: #8b949e; }}
192
+ .legend-label {{ font-size: 9px; fill: #8b949e; }}</style>
193
+ <rect width="100%" height="100%" rx="12" fill="#0d1117"/>
194
+ <text x="0" y="16" class="title">{escape(title)}</text>
195
+ <text x="0" y="32" class="subtitle">{escape(description)}</text>
196
+ {''.join(months)}{weekdays}{''.join(cells)}{legend}
197
+ </svg>'''
198
+ path.parent.mkdir(parents=True, exist_ok=True)
199
+ path.write_text(svg, encoding="utf-8")
200
+ return path
@@ -1,4 +1,5 @@
1
1
  from datetime import date, timedelta
2
+ from pathlib import Path
2
3
 
3
4
  import typer
4
5
 
@@ -9,6 +10,7 @@ from .config import (
9
10
  )
10
11
  from .storage import sync, query_range, query_all, query_by_project, get_date_bounds, get_db_path, query_today_totals
11
12
  from .stats import summarize_rows, daily_breakdown
13
+ from .heatmap import METRICS, aggregate_daily, export_svg, render_heatmap
12
14
  from .display import (
13
15
  console,
14
16
  render_today_table,
@@ -27,7 +29,7 @@ from .display import (
27
29
 
28
30
  from rich.table import Table
29
31
 
30
- app = typer.Typer(help="Track AI token usage from Claude Code and other CLI agents.")
32
+ app = typer.Typer(help="Track AI usage and visualize your activity.")
31
33
 
32
34
 
33
35
  @app.command()
@@ -107,6 +109,33 @@ def projects(no_chart: bool = typer.Option(False, "--no-chart", help="Skip the p
107
109
  print_sync_note(new)
108
110
 
109
111
 
112
+ @app.command()
113
+ def heatmap(
114
+ metric: str = typer.Option("tokens", "--metric", help="Heatmap intensity: tokens, cost, or active-days."),
115
+ days: int = typer.Option(365, "--days", min=1, max=1825, help="History window (1–1825 days)."),
116
+ output: Path | None = typer.Option(None, "--output", "-o", help="Export a shareable SVG to this path."),
117
+ ):
118
+ """Show your local AI activity heatmap, or export it as an SVG."""
119
+ if metric not in METRICS:
120
+ raise typer.BadParameter(f"choose one of: {', '.join(METRICS)}", param_hint="--metric")
121
+
122
+ new = sync()
123
+ end = date.today()
124
+ start = end - timedelta(days=days - 1)
125
+ rows = query_range(start, end)
126
+ values = aggregate_daily(rows, metric)
127
+
128
+ if output:
129
+ saved = export_svg(values, start, end, metric, output)
130
+ console.print(f"[green]Heatmap saved to {saved}[/green]")
131
+ else:
132
+ console.print(render_heatmap(values, start, end, metric))
133
+
134
+ if not rows:
135
+ console.print("[dim]No usage in this period yet. cctrack reads supported local usage data.[/dim]")
136
+ print_sync_note(new)
137
+
138
+
110
139
  @app.command()
111
140
  def prompt(
112
141
  format: str = typer.Option("both", "--format", "-f", help="Output format: 'tokens', 'cost', or 'both'."),
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "cctrack-ai"
7
- version = "0.2.0"
7
+ version = "0.3.0"
8
8
  description = "Track and analyze AI token usage from Claude Code and other CLI agents"
9
9
  readme = "README.md"
10
10
  license = { text = "MIT" }
@@ -0,0 +1,25 @@
1
+ # Website deployment
2
+
3
+ The site is a static HTML/CSS landing page in this directory. GitHub Pages serves it
4
+ for free; there is no server, build step, JavaScript framework, or analytics.
5
+
6
+ ## One-time GitHub setup
7
+
8
+ 1. In the repository, open **Settings → Pages**.
9
+ 2. Set the build and deployment source to **GitHub Actions**.
10
+ 3. Merge the site to `master`; `.github/workflows/website.yml` will deploy it.
11
+ 4. Confirm the workflow's Pages environment reports a successful deployment.
12
+
13
+ The custom domain is stored in `CNAME` as `cctrack.sandeepk.dev`. At the DNS provider
14
+ for `sandeepk.dev` (Squarespace Domains), add a CNAME record:
15
+
16
+ | Host | Type | Data |
17
+ | --- | --- | --- |
18
+ | `cctrack` | `CNAME` | `sk786.github.io` |
19
+
20
+ Remove any conflicting record for the `cctrack` host. Once DNS resolves and Pages
21
+ shows the custom domain as verified, enable **Enforce HTTPS** in Settings → Pages.
22
+ DNS propagation and certificate issuance can take some time.
23
+
24
+ The current preview is deliberately honest about product status: local CLI heatmaps
25
+ are available in this branch; hosted profiles and team boards are not live yet.
File without changes
File without changes
File without changes
File without changes