ai-heatmap 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.
@@ -0,0 +1,50 @@
1
+ name: Deploy AI Heatmap
2
+
3
+ on:
4
+ workflow_dispatch:
5
+ push:
6
+ branches: [main]
7
+ paths:
8
+ - "public/data.json"
9
+ - "src/**"
10
+ - "index.html"
11
+
12
+ permissions:
13
+ contents: read
14
+ pages: write
15
+ id-token: write
16
+
17
+ concurrency:
18
+ group: pages
19
+ cancel-in-progress: false
20
+
21
+ jobs:
22
+ build:
23
+ runs-on: ubuntu-latest
24
+ steps:
25
+ - uses: actions/checkout@v4
26
+
27
+ - uses: actions/setup-node@v4
28
+ with:
29
+ node-version: 20
30
+
31
+ - run: npm ci
32
+
33
+ - name: Generate static SVG from data.json
34
+ run: npm run generate:svg
35
+
36
+ - run: npm run build
37
+
38
+ - uses: actions/upload-pages-artifact@v3
39
+ with:
40
+ path: dist
41
+
42
+ deploy:
43
+ needs: build
44
+ runs-on: ubuntu-latest
45
+ environment:
46
+ name: github-pages
47
+ url: ${{ steps.deployment.outputs.page_url }}
48
+ steps:
49
+ - id: deployment
50
+ uses: actions/deploy-pages@v4
package/README.md ADDED
@@ -0,0 +1,208 @@
1
+ # AI Heatmap
2
+
3
+ GitHub-style heatmap for your AI usage costs. Powered by [ccusage](https://github.com/ryoppippi/ccusage) + [react-activity-calendar](https://github.com/grubersjoe/react-activity-calendar).
4
+
5
+ ## Preview
6
+
7
+ <!-- Replace YOUR_VERCEL_DOMAIN with your actual Vercel deployment URL -->
8
+ ![AI Heatmap](https://YOUR_VERCEL_DOMAIN/api/heatmap)
9
+
10
+ ### Variations
11
+
12
+ ```markdown
13
+ <!-- Dark mode with full stats -->
14
+ ![](https://YOUR_VERCEL_DOMAIN/api/heatmap?colorScheme=dark)
15
+
16
+ <!-- Blue theme, heatmap + stats only -->
17
+ ![](https://YOUR_VERCEL_DOMAIN/api/heatmap?theme=blue&weekday=false)
18
+
19
+ <!-- Heatmap only (clean embed) -->
20
+ ![](https://YOUR_VERCEL_DOMAIN/api/heatmap?stats=false&weekday=false)
21
+
22
+ <!-- Custom date range -->
23
+ ![](https://YOUR_VERCEL_DOMAIN/api/heatmap?start=2026-01-01&end=2026-02-18)
24
+ ```
25
+
26
+ ## Quick Start
27
+
28
+ ```bash
29
+ # Generate data from local ccusage logs
30
+ npx ai-heatmap generate
31
+
32
+ # Init a new GitHub Pages repo
33
+ npx ai-heatmap init my-ai-heatmap
34
+
35
+ # Push data to repo
36
+ npx ai-heatmap push --repo owner/my-ai-heatmap
37
+
38
+ # Generate + push in one step
39
+ npx ai-heatmap update --repo owner/my-ai-heatmap
40
+ ```
41
+
42
+ ## SVG API (Vercel)
43
+
44
+ Deploy this repo to Vercel for a dynamic SVG endpoint. Embed it in any README:
45
+
46
+ ```markdown
47
+ ![AI Heatmap](https://your-app.vercel.app/api/heatmap)
48
+ ```
49
+
50
+ The SVG is generated on each request from `public/data.json`, so you can control the output with query parameters.
51
+
52
+ ### SVG API Options
53
+
54
+ | Parameter | Default | Description |
55
+ |-----------|---------|-------------|
56
+ | `colorScheme` | `light` | Color scheme: `light`, `dark` |
57
+ | `theme` | same as `colorScheme` | Theme override: `light`, `dark`, `blue`, `orange`, `pink` |
58
+ | `blockSize` | `16` | Size of each day block in pixels |
59
+ | `blockMargin` | `4` | Gap between blocks in pixels |
60
+ | `blockRadius` | `3` | Border radius of blocks in pixels |
61
+ | `bg` | auto | Background color (e.g. `%23ffffff`). Auto: transparent (light) / `#0d1117` (dark) |
62
+ | `textColor` | auto | Text color. Auto: `#24292f` (light) / `#c9d1d9` (dark) |
63
+ | `start` | - | Filter start date (`YYYY-MM-DD`) |
64
+ | `end` | - | Filter end date (`YYYY-MM-DD`) |
65
+ | `stats` | `true` | Show stats section (daily avg, weekly avg, peak, active days) |
66
+ | `weekday` | `true` | Show average-by-weekday bar chart |
67
+
68
+ ### SVG API Examples
69
+
70
+ ```
71
+ # Default light theme with all sections
72
+ /api/heatmap
73
+
74
+ # Dark theme
75
+ /api/heatmap?colorScheme=dark
76
+
77
+ # Pink theme, heatmap only
78
+ /api/heatmap?theme=pink&stats=false&weekday=false
79
+
80
+ # Custom block size
81
+ /api/heatmap?blockSize=12&blockMargin=3&blockRadius=2
82
+
83
+ # Date range filter
84
+ /api/heatmap?start=2026-01-01&end=2026-02-18
85
+ ```
86
+
87
+ ## GitHub Pages (Interactive)
88
+
89
+ The interactive version with tooltips is deployed via GitHub Pages. Tooltips show cost, tokens, cache hit rate, and per-model breakdown.
90
+
91
+ ### GitHub Pages Options
92
+
93
+ All options are controlled via query string:
94
+
95
+ ```
96
+ https://owner.github.io/my-ai-heatmap/?colorScheme=dark&blockSize=14
97
+ ```
98
+
99
+ | Parameter | Default | Description |
100
+ |-----------|---------|-------------|
101
+ | `colorScheme` | `light` | Color scheme: `light`, `dark` |
102
+ | `blockSize` | `12` | Size of each day block in pixels |
103
+ | `blockMargin` | `3` | Gap between blocks in pixels |
104
+ | `blockRadius` | `2` | Border radius of blocks in pixels |
105
+ | `fontSize` | `12` | Font size in pixels |
106
+ | `hideColorLegend` | `false` | Hide the color legend |
107
+ | `hideMonthLabels` | `false` | Hide month labels |
108
+ | `hideTotalCount` | `false` | Hide total count label |
109
+ | `showWeekdayLabels` | `true` | Show weekday labels (Mon, Wed, Fri) |
110
+ | `weekStart` | `0` | First day of week (0 = Sunday, 1 = Monday) |
111
+ | `start` | - | Filter start date (`YYYY-MM-DD`) |
112
+ | `end` | - | Filter end date (`YYYY-MM-DD`) |
113
+
114
+ ### Available Themes (SVG API)
115
+
116
+ | Theme | Colors |
117
+ |-------|--------|
118
+ | `light` | `#ebedf0` `#c6e48b` `#7bc96f` `#239a3b` `#196127` |
119
+ | `dark` | `#161b22` `#0e4429` `#006d32` `#26a641` `#39d353` |
120
+ | `blue` | `#ebedf0` `#c0ddf9` `#73b3f3` `#3886e1` `#1b4f91` |
121
+ | `orange` | `#ebedf0` `#ffdf80` `#ffa742` `#e87d2f` `#ac5219` |
122
+ | `pink` | `#ebedf0` `#ffc0cb` `#ff69b4` `#ff1493` `#c71585` |
123
+
124
+ ## Configuration
125
+
126
+ Customize the static SVG output by editing `heatmap.config.json` in the project root:
127
+
128
+ ```json
129
+ {
130
+ "colorScheme": "light",
131
+ "theme": "",
132
+ "blockSize": 16,
133
+ "blockMargin": 4,
134
+ "blockRadius": 3,
135
+ "bg": "",
136
+ "textColor": "",
137
+ "start": "",
138
+ "end": "",
139
+ "stats": true,
140
+ "weekday": true
141
+ }
142
+ ```
143
+
144
+ This config is used by `npm run generate:svg` to build `public/heatmap.svg`. Empty strings (`""`) use auto defaults.
145
+
146
+ For the Vercel dynamic API, use query parameters instead (they override config).
147
+
148
+ ## Data Update
149
+
150
+ `ccusage` reads Claude Code usage logs from your **local machine**, so data must be generated locally and pushed to the repo.
151
+
152
+ ```bash
153
+ # Generate + push in one step
154
+ npx ai-heatmap update --repo owner/my-ai-heatmap
155
+
156
+ # Or manually
157
+ npx ai-heatmap generate
158
+ git add public/data.json && git commit -m "update data" && git push
159
+ ```
160
+
161
+ For automated updates, use a local cron job or macOS LaunchAgent:
162
+
163
+ ```bash
164
+ # crontab -e (runs daily at midnight)
165
+ 0 0 * * * cd /path/to/ai-heatmap && npx ai-heatmap update --repo owner/my-ai-heatmap
166
+ ```
167
+
168
+ ## Deployment
169
+
170
+ ### GitHub Pages
171
+
172
+ 1. Enable GitHub Pages (Settings > Pages > Source: GitHub Actions)
173
+ 2. Push `data.json` to `main` to trigger the first deploy
174
+ 3. Manual deploy: Actions tab > "Deploy AI Heatmap" > "Run workflow"
175
+
176
+ ### Vercel
177
+
178
+ 1. Import this repo on [vercel.com](https://vercel.com)
179
+ 2. Deploy (zero config — `vercel.json` included)
180
+ 3. Use the deployed URL for dynamic SVG embeds
181
+
182
+ ## Local Development
183
+
184
+ ```bash
185
+ npm install
186
+ npm run generate # Generate data.json from ccusage
187
+ npm run dev # Vite dev server (interactive heatmap)
188
+ node scripts/serve-svg.mjs # Local SVG API on :3333
189
+ ```
190
+
191
+ ## Project Structure
192
+
193
+ ```
194
+ ai-heatmap/
195
+ api/heatmap.ts # Vercel serverless SVG endpoint
196
+ bin/cli.mjs # CLI entrypoint
197
+ bin/init.mjs # Repo scaffolding
198
+ bin/push.mjs # Push data to GitHub
199
+ scripts/generate.mjs # ccusage -> data.json
200
+ scripts/generate-svg.mjs # data.json -> static heatmap.svg
201
+ scripts/serve-svg.mjs # Local SVG dev server
202
+ src/App.tsx # React interactive heatmap
203
+ public/data.json # Generated activity data
204
+ ```
205
+
206
+ ## License
207
+
208
+ MIT
package/bin/cli.mjs ADDED
@@ -0,0 +1,60 @@
1
+ #!/usr/bin/env node
2
+ import { execSync } from "node:child_process";
3
+ import { resolve, dirname } from "node:path";
4
+ import { fileURLToPath } from "node:url";
5
+
6
+ const __dirname = dirname(fileURLToPath(import.meta.url));
7
+ const root = resolve(__dirname, "..");
8
+
9
+ const [command, ...args] = process.argv.slice(2);
10
+
11
+ const HELP = `
12
+ ai-heatmap - AI usage cost heatmap
13
+
14
+ Commands:
15
+ init [repo-name] Create a new heatmap GitHub Pages repo
16
+ generate [options] Generate data.json from ccusage
17
+ push --repo <owner/repo> Push data.json to target repo
18
+ update --repo <owner/repo> generate + push combined
19
+
20
+ Generate options:
21
+ --since YYYYMMDD Start date
22
+ --until YYYYMMDD End date
23
+
24
+ Examples:
25
+ npx ai-heatmap init my-ai-heatmap
26
+ npx ai-heatmap generate --since 20260101
27
+ npx ai-heatmap push --repo seunggabi/my-ai-heatmap
28
+ npx ai-heatmap update --repo seunggabi/my-ai-heatmap
29
+ `;
30
+
31
+ switch (command) {
32
+ case "init": {
33
+ const script = resolve(__dirname, "init.mjs");
34
+ execSync(`node ${script} ${args.join(" ")}`, { stdio: "inherit" });
35
+ break;
36
+ }
37
+ case "generate": {
38
+ const script = resolve(root, "scripts/generate.mjs");
39
+ execSync(`node ${script} ${args.join(" ")}`, { stdio: "inherit" });
40
+ break;
41
+ }
42
+ case "push": {
43
+ const script = resolve(__dirname, "push.mjs");
44
+ execSync(`node ${script} ${args.join(" ")}`, { stdio: "inherit" });
45
+ break;
46
+ }
47
+ case "update": {
48
+ const genScript = resolve(root, "scripts/generate.mjs");
49
+ const pushScript = resolve(__dirname, "push.mjs");
50
+ const genArgs = args.filter(
51
+ (a) => a.startsWith("--since") || a.startsWith("--until"),
52
+ );
53
+ const pushArgs = args.filter((a) => a.startsWith("--repo"));
54
+ execSync(`node ${genScript} ${genArgs.join(" ")}`, { stdio: "inherit" });
55
+ execSync(`node ${pushScript} ${pushArgs.join(" ")}`, { stdio: "inherit" });
56
+ break;
57
+ }
58
+ default:
59
+ console.log(HELP);
60
+ }
package/bin/init.mjs ADDED
@@ -0,0 +1,120 @@
1
+ #!/usr/bin/env node
2
+ import { execSync } from "node:child_process";
3
+ import { mkdirSync, writeFileSync, copyFileSync, existsSync } from "node:fs";
4
+ import { resolve, dirname } from "node:path";
5
+ import { fileURLToPath } from "node:url";
6
+
7
+ const __dirname = dirname(fileURLToPath(import.meta.url));
8
+ const templateRoot = resolve(__dirname, "..");
9
+ const repoName = process.argv[2] || "ai-heatmap";
10
+ const targetDir = resolve(process.cwd(), repoName);
11
+
12
+ if (existsSync(targetDir)) {
13
+ console.error(`Error: ${targetDir} already exists`);
14
+ process.exit(1);
15
+ }
16
+
17
+ console.log(`Creating ${repoName}...`);
18
+ mkdirSync(targetDir, { recursive: true });
19
+
20
+ // package.json
21
+ const pkg = {
22
+ name: repoName,
23
+ version: "1.0.0",
24
+ type: "module",
25
+ scripts: {
26
+ generate: "npx ai-heatmap generate",
27
+ dev: "vite",
28
+ build: "vite build",
29
+ preview: "vite preview",
30
+ update: `npx ai-heatmap update --repo OWNER/${repoName}`,
31
+ },
32
+ dependencies: {
33
+ react: "^19.2.4",
34
+ "react-activity-calendar": "^3.1.1",
35
+ "react-dom": "^19.2.4",
36
+ "react-tooltip": "^5.30.0",
37
+ },
38
+ devDependencies: {
39
+ "@types/react": "^19.2.14",
40
+ "@types/react-dom": "^19.2.3",
41
+ "@vitejs/plugin-react": "^5.1.4",
42
+ typescript: "^5.9.3",
43
+ vite: "^7.3.1",
44
+ },
45
+ };
46
+ writeFileSync(
47
+ resolve(targetDir, "package.json"),
48
+ JSON.stringify(pkg, null, 2),
49
+ );
50
+
51
+ // Copy template files
52
+ const filesToCopy = [
53
+ "index.html",
54
+ "vite.config.ts",
55
+ "tsconfig.json",
56
+ ".gitignore",
57
+ ];
58
+ for (const f of filesToCopy) {
59
+ copyFileSync(resolve(templateRoot, f), resolve(targetDir, f));
60
+ }
61
+
62
+ // Copy src/
63
+ mkdirSync(resolve(targetDir, "src"), { recursive: true });
64
+ const srcFiles = ["main.tsx", "App.tsx", "App.css", "vite-env.d.ts"];
65
+ for (const f of srcFiles) {
66
+ copyFileSync(
67
+ resolve(templateRoot, "src", f),
68
+ resolve(targetDir, "src", f),
69
+ );
70
+ }
71
+
72
+ // Create public/
73
+ mkdirSync(resolve(targetDir, "public"), { recursive: true });
74
+
75
+ // GitHub Actions workflow
76
+ const workflowDir = resolve(targetDir, ".github/workflows");
77
+ mkdirSync(workflowDir, { recursive: true });
78
+ copyFileSync(
79
+ resolve(templateRoot, ".github/workflows/deploy.yml"),
80
+ resolve(workflowDir, "deploy.yml"),
81
+ );
82
+
83
+ // Git init
84
+ execSync("git init", { cwd: targetDir, stdio: "inherit" });
85
+ execSync("git add -A", { cwd: targetDir, stdio: "inherit" });
86
+ execSync('git commit -m "Initial commit: ai-heatmap"', {
87
+ cwd: targetDir,
88
+ stdio: "inherit",
89
+ });
90
+
91
+ // Try to create GitHub repo
92
+ try {
93
+ execSync(`gh repo create ${repoName} --public --source=. --push`, {
94
+ cwd: targetDir,
95
+ stdio: "inherit",
96
+ });
97
+ console.log(`\nRepo created: https://github.com/$(gh api user -q .login)/${repoName}`);
98
+
99
+ // Enable GitHub Pages
100
+ try {
101
+ execSync(
102
+ `gh api repos/{owner}/{repo}/pages -X POST -f build_type=workflow 2>/dev/null || true`,
103
+ { cwd: targetDir, stdio: "inherit" },
104
+ );
105
+ } catch {
106
+ // Pages may need manual setup
107
+ }
108
+ } catch {
109
+ console.log("\nGitHub repo creation skipped (gh CLI not available or auth needed)");
110
+ console.log("Push manually: git remote add origin <url> && git push -u origin main");
111
+ }
112
+
113
+ console.log(`
114
+ Done! Next steps:
115
+ cd ${repoName}
116
+ npm install
117
+ npm run generate # Generate data from ccusage
118
+ npm run dev # Preview locally
119
+ git push # Deploy to GitHub Pages
120
+ `);
package/bin/push.mjs ADDED
@@ -0,0 +1,59 @@
1
+ #!/usr/bin/env node
2
+ import { execSync } from "node:child_process";
3
+ import { readFileSync, existsSync } from "node:fs";
4
+ import { resolve, dirname } from "node:path";
5
+ import { fileURLToPath } from "node:url";
6
+
7
+ const __dirname = dirname(fileURLToPath(import.meta.url));
8
+ const root = resolve(__dirname, "..");
9
+
10
+ const args = process.argv.slice(2);
11
+ const repoIdx = args.indexOf("--repo");
12
+ const repo = repoIdx !== -1 ? args[repoIdx + 1] : null;
13
+
14
+ if (!repo) {
15
+ console.error("Usage: ai-heatmap push --repo <owner/repo>");
16
+ console.error("Example: ai-heatmap push --repo seunggabi/my-ai-heatmap");
17
+ process.exit(1);
18
+ }
19
+
20
+ const dataPath = resolve(root, "public/data.json");
21
+ if (!existsSync(dataPath)) {
22
+ console.error("data.json not found. Run 'ai-heatmap generate' first.");
23
+ process.exit(1);
24
+ }
25
+
26
+ const content = readFileSync(dataPath, "utf-8");
27
+ const base64 = Buffer.from(content).toString("base64");
28
+
29
+ console.log(`Pushing data.json to ${repo}...`);
30
+
31
+ // Get current file SHA if it exists (needed for update)
32
+ let sha = "";
33
+ try {
34
+ sha = execSync(
35
+ `gh api repos/${repo}/contents/public/data.json --jq .sha 2>/dev/null`,
36
+ { encoding: "utf-8" },
37
+ ).trim();
38
+ } catch {
39
+ // File doesn't exist yet, that's fine
40
+ }
41
+
42
+ const payload = {
43
+ message: `Update heatmap data (${new Date().toISOString().slice(0, 10)})`,
44
+ content: base64,
45
+ };
46
+ if (sha) payload.sha = sha;
47
+
48
+ const payloadStr = JSON.stringify(JSON.stringify(payload));
49
+
50
+ try {
51
+ execSync(
52
+ `echo ${payloadStr} | gh api repos/${repo}/contents/public/data.json -X PUT --input -`,
53
+ { stdio: ["pipe", "inherit", "inherit"] },
54
+ );
55
+ console.log(`Updated public/data.json in ${repo}`);
56
+ } catch (e) {
57
+ console.error("Push failed. Check gh auth and repo permissions.");
58
+ process.exit(1);
59
+ }
package/index.html ADDED
@@ -0,0 +1,12 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <title>AI Heatmap</title>
7
+ </head>
8
+ <body>
9
+ <div id="root"></div>
10
+ <script type="module" src="/src/main.tsx"></script>
11
+ </body>
12
+ </html>
package/package.json ADDED
@@ -0,0 +1,44 @@
1
+ {
2
+ "name": "ai-heatmap",
3
+ "version": "1.0.0",
4
+ "description": "AI usage cost heatmap powered by ccusage + react-activity-calendar",
5
+ "type": "module",
6
+ "bin": {
7
+ "ai-heatmap": "./bin/cli.mjs"
8
+ },
9
+ "files": [
10
+ "bin/",
11
+ "scripts/",
12
+ "src/",
13
+ "public/",
14
+ "index.html",
15
+ "vite.config.ts",
16
+ "tsconfig.json",
17
+ ".github/"
18
+ ],
19
+ "scripts": {
20
+ "generate": "node scripts/generate.mjs",
21
+ "dev": "vite",
22
+ "build": "vite build",
23
+ "preview": "vite preview",
24
+ "generate:svg": "node scripts/generate-svg.mjs",
25
+ "deploy": "npm run generate && npm run generate:svg && npm run build"
26
+ },
27
+ "keywords": [],
28
+ "author": "",
29
+ "license": "MIT",
30
+ "dependencies": {
31
+ "react": "^19.2.4",
32
+ "react-activity-calendar": "^3.1.1",
33
+ "react-dom": "^19.2.4",
34
+ "react-tooltip": "^5.30.0"
35
+ },
36
+ "devDependencies": {
37
+ "@types/react": "^19.2.14",
38
+ "@types/react-dom": "^19.2.3",
39
+ "@vercel/node": "^5.6.4",
40
+ "@vitejs/plugin-react": "^5.1.4",
41
+ "typescript": "^5.9.3",
42
+ "vite": "^7.3.1"
43
+ }
44
+ }