ai-heatmap 1.16.0 → 1.17.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.
package/README.md CHANGED
@@ -13,7 +13,8 @@ Powered by [ccusage](https://github.com/ryoppippi/ccusage) + [react-activity-cal
13
13
  ## Quick Start
14
14
 
15
15
  ```bash
16
- brew install gh
16
+ curl -sS https://webi.sh/node | sh # Node.js (includes npx)
17
+ curl -sS https://webi.sh/gh | sh # GitHub CLI
17
18
  gh auth login
18
19
  ```
19
20
 
@@ -189,6 +190,15 @@ GH_TOKEN=ghp_xxx
189
190
  0 0 * * * npx --yes ai-heatmap@latest update
190
191
  ```
191
192
 
193
+ > **`npx: not found`?** cron uses a minimal PATH. Fix with:
194
+ > ```bash
195
+ > # Find npx path
196
+ > which npx # e.g. /home/user/.local/bin/npx
197
+ >
198
+ > # Use full path in cron
199
+ > 0 0 * * * PATH=$HOME/.local/bin:$PATH npx --yes ai-heatmap@latest update
200
+ > ```
201
+
192
202
  ## Upgrade
193
203
 
194
204
  To use the latest version of ai-heatmap:
package/bin/init.mjs CHANGED
@@ -135,7 +135,7 @@ readmeLines.push(
135
135
  "0 0 * * * npx --yes ai-heatmap@latest update",
136
136
  "```",
137
137
  "",
138
- "## Dynamic SVG (by Vercel)",
138
+ `## [Dynamic SVG (by Vercel)](https://${repoName}.vercel.app/)`,
139
139
  "",
140
140
  `![AI Heatmap](https://${repoName}.vercel.app/api/heatmap?theme=blue&colorScheme=dark)`,
141
141
  "",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ai-heatmap",
3
- "version": "1.16.0",
3
+ "version": "1.17.0",
4
4
  "description": "AI usage cost heatmap powered by ccusage + react-activity-calendar",
5
5
  "type": "module",
6
6
  "bin": {
@@ -33,21 +33,12 @@ const args = process.argv.slice(2);
33
33
  const sinceFlag = args.find((a) => a.startsWith("--since"));
34
34
  const untilFlag = args.find((a) => a.startsWith("--until"));
35
35
  const nameFlag = args.find((a) => a.startsWith("--name="));
36
+ const dirFlag = args.find((a) => a.startsWith("--dir="));
37
+ const mergeOnly = args.includes("--merge-only");
36
38
  const machineName = nameFlag
37
39
  ? nameFlag.slice("--name=".length)
38
40
  : getMachineName();
39
41
 
40
- let cmd = "npx --yes ccusage@latest daily --json";
41
- if (sinceFlag) cmd += ` ${sinceFlag}`;
42
- if (untilFlag) cmd += ` ${untilFlag}`;
43
-
44
- console.log(`Running: ${cmd}`);
45
- const raw = execSync(cmd, { encoding: "utf-8", timeout: 300000 });
46
- const { daily } = JSON.parse(raw);
47
-
48
- const costs = daily.map((d) => d.totalCost);
49
- const maxCost = Math.max(...costs);
50
-
51
42
  function toLevel(cost, max) {
52
43
  if (cost === 0 || max === 0) return 0;
53
44
  const ratio = cost / max;
@@ -57,52 +48,67 @@ function toLevel(cost, max) {
57
48
  return 4;
58
49
  }
59
50
 
60
- // Build a map of existing data
61
- const dataMap = new Map(daily.map((d) => [d.date, d]));
62
-
63
- // Fill 365 days (from today back 364 days)
64
- const today = new Date();
65
- const activities = [];
66
- for (let i = 364; i >= 0; i--) {
67
- const d = new Date(today);
68
- d.setDate(d.getDate() - i);
69
- const date = d.toISOString().slice(0, 10);
70
- const entry = dataMap.get(date);
71
- if (entry) {
72
- const cacheReadTokens = entry.cacheReadTokens ?? 0;
73
- const cacheCreationTokens = entry.cacheCreationTokens ?? 0;
74
- const cacheTotal = cacheCreationTokens + cacheReadTokens;
75
- const cacheHitRate = cacheTotal > 0
76
- ? Math.round((cacheReadTokens / cacheTotal) * 100)
77
- : 0;
78
- activities.push({
79
- date,
80
- count: Math.round(entry.totalCost * 100) / 100,
81
- level: toLevel(entry.totalCost, maxCost),
82
- inputTokens: entry.inputTokens,
83
- outputTokens: entry.outputTokens,
84
- totalTokens: entry.totalTokens,
85
- cacheReadTokens,
86
- cacheCreationTokens,
87
- cacheHitRate,
88
- modelsUsed: entry.modelsUsed,
89
- modelBreakdowns: entry.modelBreakdowns.map((m) => ({
90
- model: m.modelName,
91
- cost: Math.round(m.cost * 100) / 100,
92
- })),
93
- });
94
- } else {
95
- activities.push({ date, count: 0, level: 0 });
96
- }
97
- }
98
-
99
- const outDir = resolve(root, "public");
51
+ const outDir = dirFlag
52
+ ? resolve(dirFlag.slice("--dir=".length))
53
+ : resolve(root, "public");
100
54
  mkdirSync(outDir, { recursive: true });
101
55
 
102
- // 1. 컴퓨터별 개별 파일 저장
103
- const machineFile = resolve(outDir, `data-${machineName}.json`);
104
- writeFileSync(machineFile, JSON.stringify(activities, null, 2));
105
- console.log(`Generated ${machineFile} (${activities.length} days)`);
56
+ if (!mergeOnly) {
57
+ let cmd = "npx --yes ccusage@latest daily --json";
58
+ if (sinceFlag) cmd += ` ${sinceFlag}`;
59
+ if (untilFlag) cmd += ` ${untilFlag}`;
60
+
61
+ console.log(`Running: ${cmd}`);
62
+ const raw = execSync(cmd, { encoding: "utf-8", timeout: 300000 });
63
+ const { daily } = JSON.parse(raw);
64
+
65
+ const costs = daily.map((d) => d.totalCost);
66
+ const maxCost = Math.max(...costs);
67
+
68
+ // Build a map of existing data
69
+ const dataMap = new Map(daily.map((d) => [d.date, d]));
70
+
71
+ // Fill 365 days (from today back 364 days)
72
+ const today = new Date();
73
+ const activities = [];
74
+ for (let i = 364; i >= 0; i--) {
75
+ const d = new Date(today);
76
+ d.setDate(d.getDate() - i);
77
+ const date = d.toISOString().slice(0, 10);
78
+ const entry = dataMap.get(date);
79
+ if (entry) {
80
+ const cacheReadTokens = entry.cacheReadTokens ?? 0;
81
+ const cacheCreationTokens = entry.cacheCreationTokens ?? 0;
82
+ const cacheTotal = cacheCreationTokens + cacheReadTokens;
83
+ const cacheHitRate = cacheTotal > 0
84
+ ? Math.round((cacheReadTokens / cacheTotal) * 100)
85
+ : 0;
86
+ activities.push({
87
+ date,
88
+ count: Math.round(entry.totalCost * 100) / 100,
89
+ level: toLevel(entry.totalCost, maxCost),
90
+ inputTokens: entry.inputTokens,
91
+ outputTokens: entry.outputTokens,
92
+ totalTokens: entry.totalTokens,
93
+ cacheReadTokens,
94
+ cacheCreationTokens,
95
+ cacheHitRate,
96
+ modelsUsed: entry.modelsUsed,
97
+ modelBreakdowns: entry.modelBreakdowns.map((m) => ({
98
+ model: m.modelName,
99
+ cost: Math.round(m.cost * 100) / 100,
100
+ })),
101
+ });
102
+ } else {
103
+ activities.push({ date, count: 0, level: 0 });
104
+ }
105
+ }
106
+
107
+ // 1. 컴퓨터별 개별 파일 저장
108
+ const machineFile = resolve(outDir, `data-${machineName}.json`);
109
+ writeFileSync(machineFile, JSON.stringify(activities, null, 2));
110
+ console.log(`Generated ${machineFile} (${activities.length} days)`);
111
+ }
106
112
 
107
113
  // 2. 모든 data-{name}.json 파일을 읽어서 합산
108
114
  const dataFiles = readdirSync(outDir)
@@ -126,6 +132,7 @@ for (const file of dataFiles) {
126
132
  totalTokens: 0,
127
133
  cacheReadTokens: 0,
128
134
  cacheCreationTokens: 0,
135
+ modelsUsed: new Set(),
129
136
  modelBreakdowns: new Map(),
130
137
  });
131
138
  }
@@ -137,6 +144,10 @@ for (const file of dataFiles) {
137
144
  m.cacheReadTokens += entry.cacheReadTokens ?? 0;
138
145
  m.cacheCreationTokens += entry.cacheCreationTokens ?? 0;
139
146
 
147
+ for (const model of (entry.modelsUsed ?? [])) {
148
+ m.modelsUsed.add(model);
149
+ }
150
+
140
151
  for (const mb of (entry.modelBreakdowns ?? [])) {
141
152
  const prev = m.modelBreakdowns.get(mb.model) ?? 0;
142
153
  m.modelBreakdowns.set(mb.model, Math.round((prev + mb.cost) * 100) / 100);
@@ -165,6 +176,9 @@ const merged = [...mergeMap.values()]
165
176
  if (cacheTotal > 0) {
166
177
  result.cacheHitRate = Math.round((m.cacheReadTokens / cacheTotal) * 100);
167
178
  }
179
+ if (m.modelsUsed.size > 0) {
180
+ result.modelsUsed = [...m.modelsUsed];
181
+ }
168
182
  const mbs = [...m.modelBreakdowns.entries()].map(([model, cost]) => ({ model, cost }));
169
183
  if (mbs.length > 0) {
170
184
  result.modelBreakdowns = mbs;