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.
- {cctrack_ai-0.2.0 → cctrack_ai-0.3.0}/PKG-INFO +26 -10
- {cctrack_ai-0.2.0 → cctrack_ai-0.3.0}/README.md +25 -9
- {cctrack_ai-0.2.0 → cctrack_ai-0.3.0}/cctrack/display.py +1 -0
- cctrack_ai-0.3.0/cctrack/heatmap.py +200 -0
- {cctrack_ai-0.2.0 → cctrack_ai-0.3.0}/cctrack/main.py +30 -1
- {cctrack_ai-0.2.0 → cctrack_ai-0.3.0}/pyproject.toml +1 -1
- cctrack_ai-0.3.0/site/README.md +25 -0
- {cctrack_ai-0.2.0 → cctrack_ai-0.3.0}/.gitignore +0 -0
- {cctrack_ai-0.2.0 → cctrack_ai-0.3.0}/cctrack/__init__.py +0 -0
- {cctrack_ai-0.2.0 → cctrack_ai-0.3.0}/cctrack/config.py +0 -0
- {cctrack_ai-0.2.0 → cctrack_ai-0.3.0}/cctrack/data/prices.json +0 -0
- {cctrack_ai-0.2.0 → cctrack_ai-0.3.0}/cctrack/openai_client.py +0 -0
- {cctrack_ai-0.2.0 → cctrack_ai-0.3.0}/cctrack/parser.py +0 -0
- {cctrack_ai-0.2.0 → cctrack_ai-0.3.0}/cctrack/prompt_cmd.py +0 -0
- {cctrack_ai-0.2.0 → cctrack_ai-0.3.0}/cctrack/stats.py +0 -0
- {cctrack_ai-0.2.0 → cctrack_ai-0.3.0}/cctrack/storage.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: cctrack-ai
|
|
3
|
-
Version: 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
|
-
|
|
31
|
+
Know your AI activity. Keep it local. Share a heatmap.
|
|
32
32
|
|
|
33
|
-
|
|
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/
|
|
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 │
|
|
187
|
-
│ claude-haiku-4-5 │ $
|
|
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
|
-
|
|
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
|
-
- **
|
|
267
|
-
- **
|
|
268
|
-
- **
|
|
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
|
-
|
|
3
|
+
Know your AI activity. Keep it local. Share a heatmap.
|
|
4
4
|
|
|
5
|
-
|
|
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/
|
|
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 │
|
|
159
|
-
│ claude-haiku-4-5 │ $
|
|
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
|
-
|
|
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
|
-
- **
|
|
239
|
-
- **
|
|
240
|
-
- **
|
|
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
|
|
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'."),
|
|
@@ -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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|