token-usage-sync 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.
- package/index.js +349 -0
- package/lib/calculations.js +131 -0
- package/package.json +32 -0
package/index.js
ADDED
|
@@ -0,0 +1,349 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { readFileSync, readdirSync, statSync, existsSync } from 'fs';
|
|
4
|
+
import { join } from 'path';
|
|
5
|
+
import { homedir } from 'os';
|
|
6
|
+
|
|
7
|
+
// 設定
|
|
8
|
+
const CLAUDE_DIR = join(homedir(), '.claude');
|
|
9
|
+
const PROJECTS_DIR = join(CLAUDE_DIR, 'projects');
|
|
10
|
+
|
|
11
|
+
// コマンドライン引数を解析
|
|
12
|
+
const args = process.argv.slice(2);
|
|
13
|
+
|
|
14
|
+
function getArgValue(name) {
|
|
15
|
+
const arg = args.find(a => a.startsWith(`--${name}=`));
|
|
16
|
+
return arg ? arg.split('=')[1] : null;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const flags = {
|
|
20
|
+
help: args.includes('--help') || args.includes('-h'),
|
|
21
|
+
json: args.includes('--json'),
|
|
22
|
+
send: args.includes('--send'),
|
|
23
|
+
verbose: args.includes('--verbose') || args.includes('-v'),
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
// API URL(環境変数 or コマンドライン引数)
|
|
27
|
+
const API_URL = process.env.SYNC_API_URL || getArgValue('api-url');
|
|
28
|
+
const USER_ID = process.env.SYNC_USER_ID || getArgValue('user-id') || 'default-user';
|
|
29
|
+
|
|
30
|
+
// ヘルプ表示
|
|
31
|
+
if (flags.help) {
|
|
32
|
+
console.log(`
|
|
33
|
+
token-usage-sync - Claude Code使用量をダッシュボードに同期
|
|
34
|
+
|
|
35
|
+
使い方:
|
|
36
|
+
npx token-usage-sync [オプション]
|
|
37
|
+
|
|
38
|
+
オプション:
|
|
39
|
+
--json JSON形式で出力
|
|
40
|
+
--send ダッシュボードに送信(--api-url必須)
|
|
41
|
+
--api-url=URL 送信先APIのURL
|
|
42
|
+
--user-id=ID ユーザーID(デフォルト: default-user)
|
|
43
|
+
--verbose 詳細ログ出力
|
|
44
|
+
--help このヘルプを表示
|
|
45
|
+
|
|
46
|
+
環境変数:
|
|
47
|
+
SYNC_API_URL 送信先URL(--api-urlの代わり)
|
|
48
|
+
SYNC_USER_ID ユーザーID(--user-idの代わり)
|
|
49
|
+
|
|
50
|
+
例:
|
|
51
|
+
# 使用量を表示
|
|
52
|
+
npx token-usage-sync
|
|
53
|
+
|
|
54
|
+
# 自分のダッシュボードに送信
|
|
55
|
+
npx token-usage-sync --send --api-url=https://your-worker.workers.dev/api/sync-usage
|
|
56
|
+
|
|
57
|
+
# 環境変数を使用
|
|
58
|
+
export SYNC_API_URL=https://your-worker.workers.dev/api/sync-usage
|
|
59
|
+
npx token-usage-sync --send
|
|
60
|
+
|
|
61
|
+
セットアップ:
|
|
62
|
+
1. リポジトリをフォーク: https://github.com/your-repo/token-usage-dashboard
|
|
63
|
+
2. Cloudflare Workersにデプロイ
|
|
64
|
+
3. 上記コマンドで同期
|
|
65
|
+
`);
|
|
66
|
+
process.exit(0);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// JSONLファイルを再帰的に探す
|
|
70
|
+
function findJsonlFiles(dir, files = []) {
|
|
71
|
+
if (!existsSync(dir)) return files;
|
|
72
|
+
|
|
73
|
+
const entries = readdirSync(dir);
|
|
74
|
+
for (const entry of entries) {
|
|
75
|
+
const fullPath = join(dir, entry);
|
|
76
|
+
const stat = statSync(fullPath);
|
|
77
|
+
|
|
78
|
+
if (stat.isDirectory()) {
|
|
79
|
+
findJsonlFiles(fullPath, files);
|
|
80
|
+
} else if (entry.endsWith('.jsonl')) {
|
|
81
|
+
files.push(fullPath);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
return files;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// JSONLファイルを解析
|
|
88
|
+
function parseJsonlFile(filePath) {
|
|
89
|
+
const content = readFileSync(filePath, 'utf-8');
|
|
90
|
+
const lines = content.trim().split('\n').filter(line => line.trim());
|
|
91
|
+
|
|
92
|
+
const entries = [];
|
|
93
|
+
for (const line of lines) {
|
|
94
|
+
try {
|
|
95
|
+
entries.push(JSON.parse(line));
|
|
96
|
+
} catch (e) {
|
|
97
|
+
// 無効な行はスキップ
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
return entries;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// 期間内の使用量を計算
|
|
104
|
+
function calculateUsageForPeriod(entries, hoursAgo) {
|
|
105
|
+
const now = new Date();
|
|
106
|
+
const cutoff = new Date(now.getTime() - hoursAgo * 60 * 60 * 1000);
|
|
107
|
+
|
|
108
|
+
let inputTokens = 0;
|
|
109
|
+
let outputTokens = 0;
|
|
110
|
+
let cacheCreationTokens = 0;
|
|
111
|
+
let cacheReadTokens = 0;
|
|
112
|
+
|
|
113
|
+
for (const entry of entries) {
|
|
114
|
+
const timestamp = entry.timestamp || entry.message?.timestamp;
|
|
115
|
+
if (!timestamp) continue;
|
|
116
|
+
|
|
117
|
+
const entryTime = new Date(timestamp);
|
|
118
|
+
if (entryTime < cutoff) continue;
|
|
119
|
+
|
|
120
|
+
const usage = entry.message?.usage || entry.usage;
|
|
121
|
+
if (!usage) continue;
|
|
122
|
+
|
|
123
|
+
inputTokens += usage.input_tokens || 0;
|
|
124
|
+
outputTokens += usage.output_tokens || 0;
|
|
125
|
+
cacheCreationTokens += usage.cache_creation_input_tokens || 0;
|
|
126
|
+
cacheReadTokens += usage.cache_read_input_tokens || 0;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// 重み付けトークン計算(Claude API料金ベース)
|
|
130
|
+
const weightedTokens = Math.round(
|
|
131
|
+
inputTokens * 1.0 +
|
|
132
|
+
outputTokens * 1.0 +
|
|
133
|
+
cacheCreationTokens * 1.25 +
|
|
134
|
+
cacheReadTokens * 0.1
|
|
135
|
+
);
|
|
136
|
+
|
|
137
|
+
return {
|
|
138
|
+
inputTokens,
|
|
139
|
+
outputTokens,
|
|
140
|
+
cacheCreationTokens,
|
|
141
|
+
cacheReadTokens,
|
|
142
|
+
totalTokens: inputTokens + outputTokens + cacheCreationTokens + cacheReadTokens,
|
|
143
|
+
weightedTokens,
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// 使用量を集計
|
|
148
|
+
function aggregateUsage(entries) {
|
|
149
|
+
let totalInputTokens = 0;
|
|
150
|
+
let totalOutputTokens = 0;
|
|
151
|
+
let totalCacheCreationInputTokens = 0;
|
|
152
|
+
let totalCacheReadInputTokens = 0;
|
|
153
|
+
let messageCount = 0;
|
|
154
|
+
|
|
155
|
+
const byModel = {};
|
|
156
|
+
const byDate = {};
|
|
157
|
+
|
|
158
|
+
for (const entry of entries) {
|
|
159
|
+
// usage情報を持つエントリのみ処理
|
|
160
|
+
const usage = entry.message?.usage || entry.usage;
|
|
161
|
+
if (!usage) continue;
|
|
162
|
+
|
|
163
|
+
const inputTokens = usage.input_tokens || 0;
|
|
164
|
+
const outputTokens = usage.output_tokens || 0;
|
|
165
|
+
const cacheCreation = usage.cache_creation_input_tokens || 0;
|
|
166
|
+
const cacheRead = usage.cache_read_input_tokens || 0;
|
|
167
|
+
|
|
168
|
+
totalInputTokens += inputTokens;
|
|
169
|
+
totalOutputTokens += outputTokens;
|
|
170
|
+
totalCacheCreationInputTokens += cacheCreation;
|
|
171
|
+
totalCacheReadInputTokens += cacheRead;
|
|
172
|
+
messageCount++;
|
|
173
|
+
|
|
174
|
+
// モデル別集計
|
|
175
|
+
const model = entry.message?.model || entry.model || 'unknown';
|
|
176
|
+
if (!byModel[model]) {
|
|
177
|
+
byModel[model] = { inputTokens: 0, outputTokens: 0, cacheCreationTokens: 0, cacheReadTokens: 0, count: 0 };
|
|
178
|
+
}
|
|
179
|
+
byModel[model].inputTokens += inputTokens;
|
|
180
|
+
byModel[model].outputTokens += outputTokens;
|
|
181
|
+
byModel[model].cacheCreationTokens += cacheCreation;
|
|
182
|
+
byModel[model].cacheReadTokens += cacheRead;
|
|
183
|
+
byModel[model].count++;
|
|
184
|
+
|
|
185
|
+
// 日別集計
|
|
186
|
+
const timestamp = entry.timestamp || entry.message?.timestamp;
|
|
187
|
+
if (timestamp) {
|
|
188
|
+
const date = new Date(timestamp).toISOString().split('T')[0];
|
|
189
|
+
if (!byDate[date]) {
|
|
190
|
+
byDate[date] = { inputTokens: 0, outputTokens: 0, cacheCreationTokens: 0, cacheReadTokens: 0, count: 0 };
|
|
191
|
+
}
|
|
192
|
+
byDate[date].inputTokens += inputTokens;
|
|
193
|
+
byDate[date].outputTokens += outputTokens;
|
|
194
|
+
byDate[date].cacheCreationTokens += cacheCreation;
|
|
195
|
+
byDate[date].cacheReadTokens += cacheRead;
|
|
196
|
+
byDate[date].count++;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// ローリングウィンドウの使用量を計算
|
|
201
|
+
const last5Hours = calculateUsageForPeriod(entries, 5);
|
|
202
|
+
const last24Hours = calculateUsageForPeriod(entries, 24);
|
|
203
|
+
const lastWeek = calculateUsageForPeriod(entries, 24 * 7);
|
|
204
|
+
|
|
205
|
+
return {
|
|
206
|
+
summary: {
|
|
207
|
+
totalInputTokens,
|
|
208
|
+
totalOutputTokens,
|
|
209
|
+
totalTokens: totalInputTokens + totalOutputTokens,
|
|
210
|
+
cacheCreationInputTokens: totalCacheCreationInputTokens,
|
|
211
|
+
cacheReadInputTokens: totalCacheReadInputTokens,
|
|
212
|
+
messageCount,
|
|
213
|
+
},
|
|
214
|
+
// Claude Code制限と同じローリングウィンドウ
|
|
215
|
+
limits: {
|
|
216
|
+
last5Hours,
|
|
217
|
+
last24Hours,
|
|
218
|
+
lastWeek,
|
|
219
|
+
},
|
|
220
|
+
byModel,
|
|
221
|
+
byDate,
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// メイン処理
|
|
226
|
+
async function main() {
|
|
227
|
+
// ファイル検索
|
|
228
|
+
if (flags.verbose) {
|
|
229
|
+
console.error(`Searching for JSONL files in: ${PROJECTS_DIR}`);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
const jsonlFiles = findJsonlFiles(PROJECTS_DIR);
|
|
233
|
+
|
|
234
|
+
if (jsonlFiles.length === 0) {
|
|
235
|
+
console.error('No JSONL files found in ~/.claude/projects/');
|
|
236
|
+
console.error('Make sure you have used Claude Code at least once.');
|
|
237
|
+
process.exit(1);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
if (flags.verbose) {
|
|
241
|
+
console.error(`Found ${jsonlFiles.length} JSONL files`);
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// 全ファイルを解析
|
|
245
|
+
const allEntries = [];
|
|
246
|
+
for (const file of jsonlFiles) {
|
|
247
|
+
const entries = parseJsonlFile(file);
|
|
248
|
+
allEntries.push(...entries);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
if (flags.verbose) {
|
|
252
|
+
console.error(`Parsed ${allEntries.length} entries`);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// 集計
|
|
256
|
+
const usage = aggregateUsage(allEntries);
|
|
257
|
+
usage.syncedAt = new Date().toISOString();
|
|
258
|
+
usage.fileCount = jsonlFiles.length;
|
|
259
|
+
|
|
260
|
+
// JSON出力
|
|
261
|
+
if (flags.json) {
|
|
262
|
+
console.log(JSON.stringify(usage, null, 2));
|
|
263
|
+
return;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
// 送信
|
|
267
|
+
if (flags.send) {
|
|
268
|
+
// API URLが指定されていない場合はエラー
|
|
269
|
+
if (!API_URL) {
|
|
270
|
+
console.error('✗ --api-url is required for --send');
|
|
271
|
+
console.error('');
|
|
272
|
+
console.error('Usage:');
|
|
273
|
+
console.error(' npx token-usage-sync --send --api-url=https://your-worker.workers.dev/api/sync-usage');
|
|
274
|
+
console.error('');
|
|
275
|
+
console.error('Or set environment variable:');
|
|
276
|
+
console.error(' export SYNC_API_URL=https://your-worker.workers.dev/api/sync-usage');
|
|
277
|
+
process.exit(1);
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
try {
|
|
281
|
+
const response = await fetch(API_URL, {
|
|
282
|
+
method: 'POST',
|
|
283
|
+
headers: {
|
|
284
|
+
'Content-Type': 'application/json',
|
|
285
|
+
'X-User-Id': USER_ID,
|
|
286
|
+
},
|
|
287
|
+
body: JSON.stringify(usage),
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
if (response.ok) {
|
|
291
|
+
console.log('✓ Usage data synced successfully!');
|
|
292
|
+
const result = await response.json();
|
|
293
|
+
if (result.dashboardUrl) {
|
|
294
|
+
console.log(` View at: ${result.dashboardUrl}`);
|
|
295
|
+
}
|
|
296
|
+
} else {
|
|
297
|
+
console.error(`✗ Failed to sync: ${response.status} ${response.statusText}`);
|
|
298
|
+
process.exit(1);
|
|
299
|
+
}
|
|
300
|
+
} catch (e) {
|
|
301
|
+
console.error(`✗ Failed to sync: ${e.message}`);
|
|
302
|
+
process.exit(1);
|
|
303
|
+
}
|
|
304
|
+
return;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
// デフォルト: 簡易表示
|
|
308
|
+
console.log('\n📊 Claude Code Usage Summary\n');
|
|
309
|
+
|
|
310
|
+
// 制限値
|
|
311
|
+
// 5時間: 重み付けトークン(cache_read=0.1x)
|
|
312
|
+
// 週間: 生トークン
|
|
313
|
+
const LIMIT_5H_WEIGHTED = 4_000_000; // 5時間制限: 4M weighted tokens
|
|
314
|
+
const LIMIT_WEEK_RAW = 195_000_000; // 週間制限: 195M raw tokens
|
|
315
|
+
|
|
316
|
+
const pct5h = Math.round((usage.limits.last5Hours.weightedTokens / LIMIT_5H_WEIGHTED) * 100);
|
|
317
|
+
const pctWeek = Math.round((usage.limits.lastWeek.totalTokens / LIMIT_WEEK_RAW) * 100);
|
|
318
|
+
|
|
319
|
+
// ローリングウィンドウ(Claude Code制限と同じ形式)
|
|
320
|
+
console.log('=== Usage Limits ===');
|
|
321
|
+
console.log(`5h Session: ${usage.limits.last5Hours.weightedTokens.toLocaleString()} weighted / ${LIMIT_5H_WEIGHTED.toLocaleString()} (${pct5h}%)`);
|
|
322
|
+
console.log(`Weekly: ${usage.limits.lastWeek.totalTokens.toLocaleString()} raw / ${LIMIT_WEEK_RAW.toLocaleString()} (${pctWeek}%)`);
|
|
323
|
+
|
|
324
|
+
console.log('\n=== Last 5 Hours Breakdown ===');
|
|
325
|
+
console.log(` Input: ${usage.limits.last5Hours.inputTokens.toLocaleString()} × 1.0`);
|
|
326
|
+
console.log(` Output: ${usage.limits.last5Hours.outputTokens.toLocaleString()} × 1.0`);
|
|
327
|
+
console.log(` Cache Create: ${usage.limits.last5Hours.cacheCreationTokens.toLocaleString()} × 1.25`);
|
|
328
|
+
console.log(` Cache Read: ${usage.limits.last5Hours.cacheReadTokens.toLocaleString()} × 0.1`);
|
|
329
|
+
|
|
330
|
+
console.log('\n=== All Time Total ===');
|
|
331
|
+
console.log(`Total Tokens: ${usage.summary.totalTokens.toLocaleString()}`);
|
|
332
|
+
console.log(` Input: ${usage.summary.totalInputTokens.toLocaleString()}`);
|
|
333
|
+
console.log(` Output: ${usage.summary.totalOutputTokens.toLocaleString()}`);
|
|
334
|
+
console.log(` Cache (in): ${usage.summary.cacheReadInputTokens.toLocaleString()}`);
|
|
335
|
+
console.log(`Messages: ${usage.summary.messageCount.toLocaleString()}`);
|
|
336
|
+
console.log(`Files: ${usage.fileCount}`);
|
|
337
|
+
|
|
338
|
+
if (Object.keys(usage.byModel).length > 0) {
|
|
339
|
+
console.log('\nBy Model:');
|
|
340
|
+
for (const [model, data] of Object.entries(usage.byModel)) {
|
|
341
|
+
const total = data.inputTokens + data.outputTokens + data.cacheCreationTokens + data.cacheReadTokens;
|
|
342
|
+
console.log(` ${model}: ${total.toLocaleString()} tokens`);
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
console.log('\n💡 Run with --send to sync to dashboard');
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
main().catch(console.error);
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
// 期間内の使用量を計算
|
|
2
|
+
export function calculateUsageForPeriod(entries, hoursAgo, now = new Date()) {
|
|
3
|
+
const cutoff = new Date(now.getTime() - hoursAgo * 60 * 60 * 1000);
|
|
4
|
+
|
|
5
|
+
let inputTokens = 0;
|
|
6
|
+
let outputTokens = 0;
|
|
7
|
+
let cacheCreationTokens = 0;
|
|
8
|
+
let cacheReadTokens = 0;
|
|
9
|
+
|
|
10
|
+
for (const entry of entries) {
|
|
11
|
+
const timestamp = entry.timestamp || entry.message?.timestamp;
|
|
12
|
+
if (!timestamp) continue;
|
|
13
|
+
|
|
14
|
+
const entryTime = new Date(timestamp);
|
|
15
|
+
if (entryTime < cutoff) continue;
|
|
16
|
+
|
|
17
|
+
const usage = entry.message?.usage || entry.usage;
|
|
18
|
+
if (!usage) continue;
|
|
19
|
+
|
|
20
|
+
inputTokens += usage.input_tokens || 0;
|
|
21
|
+
outputTokens += usage.output_tokens || 0;
|
|
22
|
+
cacheCreationTokens += usage.cache_creation_input_tokens || 0;
|
|
23
|
+
cacheReadTokens += usage.cache_read_input_tokens || 0;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// 重み付けトークン計算(Claude API料金ベース)
|
|
27
|
+
const weightedTokens = Math.round(
|
|
28
|
+
inputTokens * 1.0 +
|
|
29
|
+
outputTokens * 1.0 +
|
|
30
|
+
cacheCreationTokens * 1.25 +
|
|
31
|
+
cacheReadTokens * 0.1
|
|
32
|
+
);
|
|
33
|
+
|
|
34
|
+
return {
|
|
35
|
+
inputTokens,
|
|
36
|
+
outputTokens,
|
|
37
|
+
cacheCreationTokens,
|
|
38
|
+
cacheReadTokens,
|
|
39
|
+
totalTokens: inputTokens + outputTokens + cacheCreationTokens + cacheReadTokens,
|
|
40
|
+
weightedTokens,
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// 使用量を集計
|
|
45
|
+
export function aggregateUsage(entries, now = new Date()) {
|
|
46
|
+
let totalInputTokens = 0;
|
|
47
|
+
let totalOutputTokens = 0;
|
|
48
|
+
let totalCacheCreationInputTokens = 0;
|
|
49
|
+
let totalCacheReadInputTokens = 0;
|
|
50
|
+
let messageCount = 0;
|
|
51
|
+
|
|
52
|
+
const byModel = {};
|
|
53
|
+
const byDate = {};
|
|
54
|
+
|
|
55
|
+
for (const entry of entries) {
|
|
56
|
+
const usage = entry.message?.usage || entry.usage;
|
|
57
|
+
if (!usage) continue;
|
|
58
|
+
|
|
59
|
+
const inputTokens = usage.input_tokens || 0;
|
|
60
|
+
const outputTokens = usage.output_tokens || 0;
|
|
61
|
+
const cacheCreation = usage.cache_creation_input_tokens || 0;
|
|
62
|
+
const cacheRead = usage.cache_read_input_tokens || 0;
|
|
63
|
+
|
|
64
|
+
totalInputTokens += inputTokens;
|
|
65
|
+
totalOutputTokens += outputTokens;
|
|
66
|
+
totalCacheCreationInputTokens += cacheCreation;
|
|
67
|
+
totalCacheReadInputTokens += cacheRead;
|
|
68
|
+
messageCount++;
|
|
69
|
+
|
|
70
|
+
// モデル別集計
|
|
71
|
+
const model = entry.message?.model || entry.model || 'unknown';
|
|
72
|
+
if (!byModel[model]) {
|
|
73
|
+
byModel[model] = { inputTokens: 0, outputTokens: 0, cacheCreationTokens: 0, cacheReadTokens: 0, count: 0 };
|
|
74
|
+
}
|
|
75
|
+
byModel[model].inputTokens += inputTokens;
|
|
76
|
+
byModel[model].outputTokens += outputTokens;
|
|
77
|
+
byModel[model].cacheCreationTokens += cacheCreation;
|
|
78
|
+
byModel[model].cacheReadTokens += cacheRead;
|
|
79
|
+
byModel[model].count++;
|
|
80
|
+
|
|
81
|
+
// 日別集計
|
|
82
|
+
const timestamp = entry.timestamp || entry.message?.timestamp;
|
|
83
|
+
if (timestamp) {
|
|
84
|
+
const date = new Date(timestamp).toISOString().split('T')[0];
|
|
85
|
+
if (!byDate[date]) {
|
|
86
|
+
byDate[date] = { inputTokens: 0, outputTokens: 0, cacheCreationTokens: 0, cacheReadTokens: 0, count: 0 };
|
|
87
|
+
}
|
|
88
|
+
byDate[date].inputTokens += inputTokens;
|
|
89
|
+
byDate[date].outputTokens += outputTokens;
|
|
90
|
+
byDate[date].cacheCreationTokens += cacheCreation;
|
|
91
|
+
byDate[date].cacheReadTokens += cacheRead;
|
|
92
|
+
byDate[date].count++;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// ローリングウィンドウの使用量を計算
|
|
97
|
+
const last5Hours = calculateUsageForPeriod(entries, 5, now);
|
|
98
|
+
const last24Hours = calculateUsageForPeriod(entries, 24, now);
|
|
99
|
+
const lastWeek = calculateUsageForPeriod(entries, 24 * 7, now);
|
|
100
|
+
|
|
101
|
+
return {
|
|
102
|
+
summary: {
|
|
103
|
+
totalInputTokens,
|
|
104
|
+
totalOutputTokens,
|
|
105
|
+
totalTokens: totalInputTokens + totalOutputTokens,
|
|
106
|
+
cacheCreationInputTokens: totalCacheCreationInputTokens,
|
|
107
|
+
cacheReadInputTokens: totalCacheReadInputTokens,
|
|
108
|
+
messageCount,
|
|
109
|
+
},
|
|
110
|
+
limits: {
|
|
111
|
+
last5Hours,
|
|
112
|
+
last24Hours,
|
|
113
|
+
lastWeek,
|
|
114
|
+
},
|
|
115
|
+
byModel,
|
|
116
|
+
byDate,
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// パーセント計算
|
|
121
|
+
export function percent(used, limit) {
|
|
122
|
+
if (limit <= 0) return 0;
|
|
123
|
+
return Math.min(100, Math.max(0, Math.round((used / limit) * 100)));
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// トークン数フォーマット
|
|
127
|
+
export function formatTokens(n) {
|
|
128
|
+
if (n >= 1_000_000) return `${(n / 1_000_000).toFixed(1)}M`;
|
|
129
|
+
if (n >= 1_000) return `${(n / 1_000).toFixed(0)}K`;
|
|
130
|
+
return n.toString();
|
|
131
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "token-usage-sync",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Sync Claude Code token usage to your self-hosted dashboard",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"token-usage-sync": "./index.js"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"index.js",
|
|
11
|
+
"lib/"
|
|
12
|
+
],
|
|
13
|
+
"keywords": [
|
|
14
|
+
"claude",
|
|
15
|
+
"claude-code",
|
|
16
|
+
"anthropic",
|
|
17
|
+
"usage",
|
|
18
|
+
"token",
|
|
19
|
+
"dashboard",
|
|
20
|
+
"monitoring"
|
|
21
|
+
],
|
|
22
|
+
"author": "",
|
|
23
|
+
"license": "MIT",
|
|
24
|
+
"repository": {
|
|
25
|
+
"type": "git",
|
|
26
|
+
"url": "https://github.com/your-username/token-usage-dashboard"
|
|
27
|
+
},
|
|
28
|
+
"homepage": "https://github.com/your-username/token-usage-dashboard#readme",
|
|
29
|
+
"engines": {
|
|
30
|
+
"node": ">=18.0.0"
|
|
31
|
+
}
|
|
32
|
+
}
|