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 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
+ }