token-usage-sync 1.5.0 → 1.5.2

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 CHANGED
@@ -20,17 +20,12 @@ const flags = {
20
20
  help: args.includes('--help') || args.includes('-h'),
21
21
  json: args.includes('--json'),
22
22
  send: args.includes('--send'),
23
- fetchApi: args.includes('--fetch-api'),
24
23
  verbose: args.includes('--verbose') || args.includes('-v'),
25
24
  };
26
25
 
27
26
  // API URL(環境変数 or コマンドライン引数)
28
27
  const API_URL = process.env.SYNC_API_URL || getArgValue('api-url');
29
- const USER_ID = process.env.SYNC_USER_ID || getArgValue('user-id') || 'default-user';
30
-
31
- // Claude API認証情報
32
- const CLAUDE_SESSION_KEY = process.env.CLAUDE_SESSION_KEY || getArgValue('session-key');
33
- const CLAUDE_ORG_ID = process.env.CLAUDE_ORG_ID || getArgValue('org-id');
28
+ const USER_ID = process.env.SYNC_USER_ID || getArgValue('user-id') || 'msgmacbookair';
34
29
 
35
30
  // ヘルプ表示
36
31
  if (flags.help) {
@@ -43,34 +38,28 @@ token-usage-sync - Claude Code使用量をダッシュボードに同期
43
38
  オプション:
44
39
  --json JSON形式で出力
45
40
  --send ダッシュボードに送信(--api-url必須)
46
- --fetch-api Claude APIから使用率を取得(--session-key, --org-id必須)
47
41
  --api-url=URL 送信先APIのURL
48
42
  --user-id=ID ユーザーID(デフォルト: default-user)
49
- --session-key=KEY Claude セッションキー(ブラウザCookieから取得)
50
- --org-id=ID Claude 組織ID(ブラウザCookieから取得)
51
43
  --verbose 詳細ログ出力
52
44
  --help このヘルプを表示
53
45
 
54
46
  環境変数:
55
47
  SYNC_API_URL 送信先URL(--api-urlの代わり)
56
48
  SYNC_USER_ID ユーザーID(--user-idの代わり)
57
- CLAUDE_SESSION_KEY Claudeセッションキー(--session-keyの代わり)
58
- CLAUDE_ORG_ID Claude組織ID(--org-idの代わり)
59
49
 
60
50
  例:
61
51
  # 使用量を表示(ローカルJSONLから計算)
62
52
  npx token-usage-sync
63
53
 
64
- # Claude APIから使用率を取得して表示
65
- npx token-usage-sync --fetch-api --session-key=sk-xxx --org-id=xxx
54
+ # ダッシュボードに送信
55
+ npx token-usage-sync --send
66
56
 
67
- # APIから取得してダッシュボードに送信
68
- npx token-usage-sync --fetch-api --send
57
+ # JSON形式で出力
58
+ npx token-usage-sync --json
69
59
 
70
- セッションキー/組織IDの取得方法:
71
- 1. claude.ai にログイン
72
- 2. 開発者ツール > Application > Cookies
73
- 3. sessionKey と lastActiveOrg の値をコピー
60
+ 推奨ワークフロー:
61
+ 1. npx ccusage blocks → セッションブロックの詳細を確認
62
+ 2. npx token-usage-sync --send → ダッシュボードに同期
74
63
  `);
75
64
  process.exit(0);
76
65
  }
@@ -136,12 +125,67 @@ function calculateUsageForPeriod(entries, hoursAgo) {
136
125
  }
137
126
 
138
127
  // 重み付けトークン計算(Claude Code実測ベース)
139
- // Cache Read は無料(0x)、Cache Create は 1.0x
128
+ // Cache Read は 0.1x、Cache Create は 1.25x
140
129
  const weightedTokens = Math.round(
141
130
  inputTokens * 1.0 +
142
131
  outputTokens * 1.0 +
143
- cacheCreationTokens * 1.0 +
144
- cacheReadTokens * 0
132
+ cacheCreationTokens * 1.25 +
133
+ cacheReadTokens * 0.1
134
+ );
135
+
136
+ return {
137
+ inputTokens,
138
+ outputTokens,
139
+ cacheCreationTokens,
140
+ cacheReadTokens,
141
+ totalTokens: inputTokens + outputTokens + cacheCreationTokens + cacheReadTokens,
142
+ weightedTokens,
143
+ };
144
+ }
145
+
146
+ const WEEKLY_RESET = { weekday: 6, hour: 8, minute: 59 }; // Saturday 08:59 (local time)
147
+
148
+ function getLastWeeklyReset(now = new Date()) {
149
+ const reset = new Date(now);
150
+ const day = reset.getDay();
151
+ const diff = (day - WEEKLY_RESET.weekday + 7) % 7;
152
+ reset.setDate(reset.getDate() - diff);
153
+ reset.setHours(WEEKLY_RESET.hour, WEEKLY_RESET.minute, 0, 0);
154
+
155
+ if (now < reset) {
156
+ reset.setDate(reset.getDate() - 7);
157
+ }
158
+
159
+ return reset;
160
+ }
161
+
162
+ function calculateUsageSince(entries, startTime, now = new Date()) {
163
+ let inputTokens = 0;
164
+ let outputTokens = 0;
165
+ let cacheCreationTokens = 0;
166
+ let cacheReadTokens = 0;
167
+
168
+ for (const entry of entries) {
169
+ const timestamp = entry.timestamp || entry.message?.timestamp;
170
+ if (!timestamp) continue;
171
+
172
+ const entryTime = new Date(timestamp);
173
+ if (entryTime < startTime || entryTime > now) continue;
174
+
175
+ const usage = entry.message?.usage || entry.usage;
176
+ if (!usage) continue;
177
+
178
+ inputTokens += usage.input_tokens || 0;
179
+ outputTokens += usage.output_tokens || 0;
180
+ cacheCreationTokens += usage.cache_creation_input_tokens || 0;
181
+ cacheReadTokens += usage.cache_read_input_tokens || 0;
182
+ }
183
+
184
+ const weightedTokens = Math.round(
185
+ inputTokens * 1.0 +
186
+ outputTokens * 1.0 +
187
+ cacheCreationTokens * 1.25 +
188
+ cacheReadTokens * 0.1
145
189
  );
146
190
 
147
191
  return {
@@ -236,8 +280,8 @@ function calculateActiveBlockUsage(entries, now = new Date()) {
236
280
  const weightedTokens = Math.round(
237
281
  inputTokens * 1.0 +
238
282
  outputTokens * 1.0 +
239
- cacheCreationTokens * 1.0 +
240
- cacheReadTokens * 0
283
+ cacheCreationTokens * 1.25 +
284
+ cacheReadTokens * 0.1
241
285
  );
242
286
 
243
287
  const elapsedMinutes = Math.round((nowTime - activeBlock.startTime) / 60000);
@@ -316,7 +360,7 @@ function aggregateUsage(entries) {
316
360
  // ローリングウィンドウの使用量を計算
317
361
  const last5Hours = calculateUsageForPeriod(entries, 5);
318
362
  const last24Hours = calculateUsageForPeriod(entries, 24);
319
- const lastWeek = calculateUsageForPeriod(entries, 24 * 7);
363
+ const lastWeek = calculateUsageSince(entries, getLastWeeklyReset());
320
364
 
321
365
  return {
322
366
  summary: {
@@ -340,123 +384,9 @@ function aggregateUsage(entries) {
340
384
  };
341
385
  }
342
386
 
343
- // Claude APIから使用率を取得
344
- async function fetchClaudeApiUsage(sessionKey, orgId) {
345
- const url = `https://claude.ai/api/organizations/${orgId}/usage`;
346
-
347
- const response = await fetch(url, {
348
- method: 'GET',
349
- headers: {
350
- 'Accept': 'application/json',
351
- 'Cookie': `sessionKey=${sessionKey}`,
352
- },
353
- });
354
-
355
- if (!response.ok) {
356
- throw new Error(`Claude API error: ${response.status} ${response.statusText}`);
357
- }
358
-
359
- const data = await response.json();
360
-
361
- // APIレスポンスをパース
362
- // utilization は 0.0〜1.0 の値(0%〜100%)
363
- const fiveHour = data.five_hour || {};
364
- const sevenDay = data.seven_day || {};
365
-
366
- return {
367
- fiveHourPercent: fiveHour.utilization != null ? Math.round(fiveHour.utilization * 100) : null,
368
- fiveHourResetAt: fiveHour.resets_at || null,
369
- weeklyPercent: sevenDay.utilization != null ? Math.round(sevenDay.utilization * 100) : null,
370
- weeklyResetAt: sevenDay.resets_at || null,
371
- raw: data,
372
- };
373
- }
374
-
375
387
  // メイン処理
376
388
  async function main() {
377
- // --fetch-api モード: Claude APIから使用率を取得
378
- if (flags.fetchApi) {
379
- if (!CLAUDE_SESSION_KEY || !CLAUDE_ORG_ID) {
380
- console.error('✗ --fetch-api requires --session-key and --org-id');
381
- console.error('');
382
- console.error('Usage:');
383
- console.error(' npx token-usage-sync --fetch-api --session-key=YOUR_KEY --org-id=YOUR_ORG');
384
- console.error('');
385
- console.error('Or set environment variables:');
386
- console.error(' export CLAUDE_SESSION_KEY=YOUR_KEY');
387
- console.error(' export CLAUDE_ORG_ID=YOUR_ORG');
388
- process.exit(1);
389
- }
390
-
391
- if (flags.verbose) {
392
- console.error('Fetching usage from Claude API...');
393
- }
394
-
395
- try {
396
- const apiUsage = await fetchClaudeApiUsage(CLAUDE_SESSION_KEY, CLAUDE_ORG_ID);
397
-
398
- if (flags.json) {
399
- console.log(JSON.stringify(apiUsage, null, 2));
400
- return;
401
- }
402
-
403
- // 表示
404
- console.log('\n📊 Claude Code Usage (from API)\n');
405
- console.log('=== Usage Limits ===');
406
- console.log(`5-Hour: ${apiUsage.fiveHourPercent ?? '--'}%`);
407
- console.log(`Weekly: ${apiUsage.weeklyPercent ?? '--'}%`);
408
-
409
- if (apiUsage.fiveHourResetAt) {
410
- const resetTime = new Date(apiUsage.fiveHourResetAt);
411
- const remaining = Math.max(0, Math.round((resetTime - new Date()) / 60000));
412
- console.log(`\n5H Reset: ${Math.floor(remaining / 60)}h ${remaining % 60}m remaining`);
413
- }
414
-
415
- // 送信
416
- if (flags.send) {
417
- if (!API_URL) {
418
- console.error('\n✗ --api-url is required for --send');
419
- process.exit(1);
420
- }
421
-
422
- const payload = {
423
- source: 'api',
424
- apiUsage: {
425
- fiveHourPercent: apiUsage.fiveHourPercent,
426
- fiveHourResetAt: apiUsage.fiveHourResetAt,
427
- weeklyPercent: apiUsage.weeklyPercent,
428
- weeklyResetAt: apiUsage.weeklyResetAt,
429
- },
430
- syncedAt: new Date().toISOString(),
431
- };
432
-
433
- const response = await fetch(API_URL, {
434
- method: 'POST',
435
- headers: {
436
- 'Content-Type': 'application/json',
437
- 'X-User-Id': USER_ID,
438
- },
439
- body: JSON.stringify(payload),
440
- });
441
-
442
- if (response.ok) {
443
- console.log('\n✓ Usage data synced successfully!');
444
- } else {
445
- console.error(`\n✗ Failed to sync: ${response.status} ${response.statusText}`);
446
- process.exit(1);
447
- }
448
- } else {
449
- console.log('\n💡 Run with --send to sync to dashboard');
450
- }
451
-
452
- return;
453
- } catch (e) {
454
- console.error(`✗ Failed to fetch from Claude API: ${e.message}`);
455
- process.exit(1);
456
- }
457
- }
458
-
459
- // ローカルJSONLモード(デフォルト)
389
+ // ローカルJSONLモード
460
390
  // ファイル検索
461
391
  if (flags.verbose) {
462
392
  console.error(`Searching for JSONL files in: ${PROJECTS_DIR}`);
@@ -541,9 +471,10 @@ async function main() {
541
471
  console.log('\n📊 Claude Code Usage Summary\n');
542
472
 
543
473
  // 制限値
544
- const LIMIT_WEEK_RAW = 195_000_000; // 週間制限: 195M raw tokens
474
+ const LIMIT_WEEK_RAW = 180_000_000; // 週間制限: 180M raw tokens
545
475
 
546
- const pctWeek = Math.round((usage.limits.lastWeek.totalTokens / LIMIT_WEEK_RAW) * 100);
476
+ const lastWeekWeighted = usage.limits.lastWeek.weightedTokens ?? usage.limits.lastWeek.totalTokens;
477
+ const pctWeek = Math.round((lastWeekWeighted / LIMIT_WEEK_RAW) * 100);
547
478
 
548
479
  // アクティブブロック(Claude Code準拠のセッションブロック方式)
549
480
  console.log('=== Active Session Block ===');
@@ -554,7 +485,8 @@ async function main() {
554
485
  const remainingStr = `${Math.floor(remaining / 60)}h ${remaining % 60}m`;
555
486
 
556
487
  console.log(`Status: ACTIVE (${elapsedStr} elapsed, ${remainingStr} remaining)`);
557
- console.log(`Tokens: ${usage.activeBlock.totalTokens.toLocaleString()}`);
488
+ const activeBlockWeighted = usage.activeBlock.weightedTokens ?? usage.activeBlock.totalTokens;
489
+ console.log(`Tokens: ${activeBlockWeighted.toLocaleString()}`);
558
490
  console.log(` Input: ${usage.activeBlock.inputTokens.toLocaleString()}`);
559
491
  console.log(` Output: ${usage.activeBlock.outputTokens.toLocaleString()}`);
560
492
  console.log(` Cache: ${usage.activeBlock.cacheCreationTokens.toLocaleString()} (create) + ${usage.activeBlock.cacheReadTokens.toLocaleString()} (read)`);
@@ -563,7 +495,7 @@ async function main() {
563
495
  }
564
496
 
565
497
  console.log('\n=== Weekly Limit ===');
566
- console.log(`Weekly: ${usage.limits.lastWeek.totalTokens.toLocaleString()} / ${LIMIT_WEEK_RAW.toLocaleString()} (${pctWeek}%)`);
498
+ console.log(`Weekly: ${lastWeekWeighted.toLocaleString()} / ${LIMIT_WEEK_RAW.toLocaleString()} (${pctWeek}%)`);
567
499
 
568
500
  console.log('\n=== All Time Total ===');
569
501
  console.log(`Total Tokens: ${usage.summary.totalTokens.toLocaleString()}`);
@@ -87,8 +87,8 @@ export function calculateActiveBlockUsage(entries, now = new Date()) {
87
87
  const weightedTokens = Math.round(
88
88
  inputTokens * 1.0 +
89
89
  outputTokens * 1.0 +
90
- cacheCreationTokens * 1.0 +
91
- cacheReadTokens * 0
90
+ cacheCreationTokens * 1.25 +
91
+ cacheReadTokens * 0.1
92
92
  );
93
93
 
94
94
  const elapsedMinutes = Math.round((nowTime - activeBlock.startTime) / 60000);
@@ -134,12 +134,67 @@ export function calculateUsageForPeriod(entries, hoursAgo, now = new Date()) {
134
134
  }
135
135
 
136
136
  // 重み付けトークン計算(Claude Code実測ベース)
137
- // Cache Read は無料(0x)、Cache Create は 1.0x
137
+ // Cache Read は 0.1x、Cache Create は 1.25x
138
138
  const weightedTokens = Math.round(
139
139
  inputTokens * 1.0 +
140
140
  outputTokens * 1.0 +
141
- cacheCreationTokens * 1.0 +
142
- cacheReadTokens * 0
141
+ cacheCreationTokens * 1.25 +
142
+ cacheReadTokens * 0.1
143
+ );
144
+
145
+ return {
146
+ inputTokens,
147
+ outputTokens,
148
+ cacheCreationTokens,
149
+ cacheReadTokens,
150
+ totalTokens: inputTokens + outputTokens + cacheCreationTokens + cacheReadTokens,
151
+ weightedTokens,
152
+ };
153
+ }
154
+
155
+ const WEEKLY_RESET = { weekday: 6, hour: 8, minute: 59 }; // Saturday 08:59 (local time)
156
+
157
+ function getLastWeeklyReset(now = new Date()) {
158
+ const reset = new Date(now);
159
+ const day = reset.getDay();
160
+ const diff = (day - WEEKLY_RESET.weekday + 7) % 7;
161
+ reset.setDate(reset.getDate() - diff);
162
+ reset.setHours(WEEKLY_RESET.hour, WEEKLY_RESET.minute, 0, 0);
163
+
164
+ if (now < reset) {
165
+ reset.setDate(reset.getDate() - 7);
166
+ }
167
+
168
+ return reset;
169
+ }
170
+
171
+ function calculateUsageSince(entries, startTime, now = new Date()) {
172
+ let inputTokens = 0;
173
+ let outputTokens = 0;
174
+ let cacheCreationTokens = 0;
175
+ let cacheReadTokens = 0;
176
+
177
+ for (const entry of entries) {
178
+ const timestamp = entry.timestamp || entry.message?.timestamp;
179
+ if (!timestamp) continue;
180
+
181
+ const entryTime = new Date(timestamp);
182
+ if (entryTime < startTime || entryTime > now) continue;
183
+
184
+ const usage = entry.message?.usage || entry.usage;
185
+ if (!usage) continue;
186
+
187
+ inputTokens += usage.input_tokens || 0;
188
+ outputTokens += usage.output_tokens || 0;
189
+ cacheCreationTokens += usage.cache_creation_input_tokens || 0;
190
+ cacheReadTokens += usage.cache_read_input_tokens || 0;
191
+ }
192
+
193
+ const weightedTokens = Math.round(
194
+ inputTokens * 1.0 +
195
+ outputTokens * 1.0 +
196
+ cacheCreationTokens * 1.25 +
197
+ cacheReadTokens * 0.1
143
198
  );
144
199
 
145
200
  return {
@@ -210,7 +265,7 @@ export function aggregateUsage(entries, now = new Date()) {
210
265
  // ローリングウィンドウの使用量を計算(後方互換性)
211
266
  const last5Hours = calculateUsageForPeriod(entries, 5, now);
212
267
  const last24Hours = calculateUsageForPeriod(entries, 24, now);
213
- const lastWeek = calculateUsageForPeriod(entries, 24 * 7, now);
268
+ const lastWeek = calculateUsageSince(entries, getLastWeeklyReset(now), now);
214
269
 
215
270
  return {
216
271
  summary: {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "token-usage-sync",
3
- "version": "1.5.0",
3
+ "version": "1.5.2",
4
4
  "description": "Sync Claude Code token usage to your self-hosted dashboard",
5
5
  "type": "module",
6
6
  "bin": {