ai-heatmap 1.16.0 → 1.17.1
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 +11 -1
- package/bin/cli.mjs +5 -1
- package/bin/init.mjs +1 -1
- package/package.json +1 -1
- package/scripts/generate.mjs +69 -55
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
|
-
|
|
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/cli.mjs
CHANGED
|
@@ -145,7 +145,11 @@ switch (command) {
|
|
|
145
145
|
}
|
|
146
146
|
|
|
147
147
|
// 2. generate: 이 컴퓨터 데이터 수집 + 모든 data-*.json 합산 → data.json 생성
|
|
148
|
-
|
|
148
|
+
// machineName을 명시적으로 전달해 cli.mjs와 generate.mjs가 동일한 값을 사용하도록 보장
|
|
149
|
+
const genArgsWithName = genArgs.some((a) => a.startsWith("--name="))
|
|
150
|
+
? genArgs
|
|
151
|
+
: [`--name=${machineName}`, ...genArgs];
|
|
152
|
+
execSync(`node ${genScript} ${genArgsWithName.join(" ")}`, { stdio: "inherit" });
|
|
149
153
|
|
|
150
154
|
// 3. data-{name}.json push (이 컴퓨터 개별 파일)
|
|
151
155
|
const machineFile = `data-${machineName}.json`;
|
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
|
-
|
|
138
|
+
`## [Dynamic SVG (by Vercel)](https://${repoName}.vercel.app/)`,
|
|
139
139
|
"",
|
|
140
140
|
``,
|
|
141
141
|
"",
|
package/package.json
CHANGED
package/scripts/generate.mjs
CHANGED
|
@@ -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
|
-
|
|
61
|
-
|
|
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
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
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;
|