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 +76 -144
- package/lib/calculations.js +61 -6
- package/package.json +1 -1
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') || '
|
|
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
|
-
#
|
|
65
|
-
npx token-usage-sync --
|
|
54
|
+
# ダッシュボードに送信
|
|
55
|
+
npx token-usage-sync --send
|
|
66
56
|
|
|
67
|
-
#
|
|
68
|
-
npx token-usage-sync --
|
|
57
|
+
# JSON形式で出力
|
|
58
|
+
npx token-usage-sync --json
|
|
69
59
|
|
|
70
|
-
|
|
71
|
-
1.
|
|
72
|
-
2.
|
|
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
|
|
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.
|
|
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.
|
|
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 =
|
|
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
|
-
//
|
|
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 =
|
|
474
|
+
const LIMIT_WEEK_RAW = 180_000_000; // 週間制限: 180M raw tokens
|
|
545
475
|
|
|
546
|
-
const
|
|
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
|
-
|
|
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: ${
|
|
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()}`);
|
package/lib/calculations.js
CHANGED
|
@@ -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.
|
|
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
|
|
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.
|
|
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 =
|
|
268
|
+
const lastWeek = calculateUsageSince(entries, getLastWeeklyReset(now), now);
|
|
214
269
|
|
|
215
270
|
return {
|
|
216
271
|
summary: {
|