@tasszz2k/agentlens 0.5.10 → 0.5.12
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/dist/cli.js +21 -4
- package/dist/cli.js.map +1 -1
- package/dist/config.d.ts +1 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +17 -4
- package/dist/config.js.map +1 -1
- package/dist/cost-cache.d.ts +10 -0
- package/dist/cost-cache.d.ts.map +1 -0
- package/dist/cost-cache.js +45 -0
- package/dist/cost-cache.js.map +1 -0
- package/dist/cost.d.ts +1 -1
- package/dist/cost.d.ts.map +1 -1
- package/dist/cost.js +413 -111
- package/dist/cost.js.map +1 -1
- package/dist/render.d.ts.map +1 -1
- package/dist/render.js +70 -6
- package/dist/render.js.map +1 -1
- package/dist/types.d.ts +17 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/ui/App.d.ts +2 -1
- package/dist/ui/App.d.ts.map +1 -1
- package/dist/ui/App.js +72 -8
- package/dist/ui/App.js.map +1 -1
- package/dist/ui/CostView.d.ts +3 -1
- package/dist/ui/CostView.d.ts.map +1 -1
- package/dist/ui/CostView.js +158 -41
- package/dist/ui/CostView.js.map +1 -1
- package/dist/ui/SettingsView.d.ts +2 -1
- package/dist/ui/SettingsView.d.ts.map +1 -1
- package/dist/ui/SettingsView.js +13 -1
- package/dist/ui/SettingsView.js.map +1 -1
- package/package.json +1 -1
package/dist/cost.js
CHANGED
|
@@ -5,6 +5,14 @@ import { execFile } from 'node:child_process';
|
|
|
5
5
|
import { promisify } from 'node:util';
|
|
6
6
|
import { loadConfig } from './config.js';
|
|
7
7
|
const execFileAsync = promisify(execFile);
|
|
8
|
+
function extractErrorMessage(err) {
|
|
9
|
+
if (!(err instanceof Error))
|
|
10
|
+
return String(err);
|
|
11
|
+
const cause = err.cause;
|
|
12
|
+
if (cause instanceof Error)
|
|
13
|
+
return `${err.message}: ${cause.message}`;
|
|
14
|
+
return err.message;
|
|
15
|
+
}
|
|
8
16
|
function localDateStr(d) {
|
|
9
17
|
const y = d.getFullYear();
|
|
10
18
|
const m = String(d.getMonth() + 1).padStart(2, '0');
|
|
@@ -161,7 +169,7 @@ export async function fetchClaudeCodeCosts() {
|
|
|
161
169
|
totalOutputTokens: 0,
|
|
162
170
|
models: [],
|
|
163
171
|
period,
|
|
164
|
-
error:
|
|
172
|
+
error: extractErrorMessage(err),
|
|
165
173
|
};
|
|
166
174
|
}
|
|
167
175
|
}
|
|
@@ -183,6 +191,26 @@ const CURSOR_TIER_NAMES = {
|
|
|
183
191
|
function normalizeCursorTierName(key) {
|
|
184
192
|
return CURSOR_TIER_NAMES[key] ?? key;
|
|
185
193
|
}
|
|
194
|
+
function parseUsageBucket(bucket) {
|
|
195
|
+
if (!bucket || typeof bucket !== 'object')
|
|
196
|
+
return undefined;
|
|
197
|
+
const b = bucket;
|
|
198
|
+
if (typeof b.enabled !== 'boolean')
|
|
199
|
+
return undefined;
|
|
200
|
+
const used = typeof b.used === 'number' ? b.used : Number(b.used);
|
|
201
|
+
const limit = typeof b.limit === 'number' ? b.limit : Number(b.limit);
|
|
202
|
+
return {
|
|
203
|
+
enabled: b.enabled,
|
|
204
|
+
usedCents: Number.isFinite(used) ? used : 0,
|
|
205
|
+
limitCents: Number.isFinite(limit) ? limit : 0,
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
function parseIsoMs(value) {
|
|
209
|
+
if (typeof value !== 'string')
|
|
210
|
+
return undefined;
|
|
211
|
+
const ms = Date.parse(value);
|
|
212
|
+
return Number.isFinite(ms) ? ms : undefined;
|
|
213
|
+
}
|
|
186
214
|
async function fetchCursorUsageSummary(token) {
|
|
187
215
|
try {
|
|
188
216
|
const res = await fetch('https://cursor.com/api/usage-summary', {
|
|
@@ -195,27 +223,32 @@ async function fetchCursorUsageSummary(token) {
|
|
|
195
223
|
const mt = data.membershipType;
|
|
196
224
|
if (typeof mt === 'string' && mt !== 'free')
|
|
197
225
|
planType = mt;
|
|
198
|
-
let
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
if (indOd && typeof indOd.enabled === 'boolean') {
|
|
202
|
-
onDemand = {
|
|
203
|
-
enabled: indOd.enabled,
|
|
204
|
-
usedCents: typeof indOd.used === 'number' ? indOd.used : 0,
|
|
205
|
-
limitCents: typeof indOd.limit === 'number' ? indOd.limit : 0,
|
|
206
|
-
};
|
|
226
|
+
let limitType;
|
|
227
|
+
if (data.limitType === 'team' || data.limitType === 'individual') {
|
|
228
|
+
limitType = data.limitType;
|
|
207
229
|
}
|
|
208
|
-
|
|
230
|
+
const ind = data.individualUsage;
|
|
209
231
|
const team = data.teamUsage;
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
232
|
+
// New shape: individualUsage.overall is the user's monthly bucket.
|
|
233
|
+
// Legacy shape: individualUsage.onDemand was the on-demand tracker.
|
|
234
|
+
const included = parseUsageBucket(ind?.overall) ??
|
|
235
|
+
parseUsageBucket(ind?.included);
|
|
236
|
+
// On-demand now lives under teamUsage.onDemand for team plans; fall back
|
|
237
|
+
// to legacy individualUsage.onDemand if present.
|
|
238
|
+
const onDemand = parseUsageBucket(team?.onDemand) ??
|
|
239
|
+
parseUsageBucket(ind?.onDemand);
|
|
240
|
+
const teamOnDemand = parseUsageBucket(team?.onDemand);
|
|
241
|
+
const pooled = parseUsageBucket(team?.pooled);
|
|
242
|
+
return {
|
|
243
|
+
planType,
|
|
244
|
+
limitType,
|
|
245
|
+
included,
|
|
246
|
+
onDemand,
|
|
247
|
+
teamOnDemand,
|
|
248
|
+
pooled,
|
|
249
|
+
billingCycleStartMs: parseIsoMs(data.billingCycleStart),
|
|
250
|
+
billingCycleEndMs: parseIsoMs(data.billingCycleEnd),
|
|
251
|
+
};
|
|
219
252
|
}
|
|
220
253
|
catch {
|
|
221
254
|
return null;
|
|
@@ -350,7 +383,26 @@ async function getCursorEmail() {
|
|
|
350
383
|
}
|
|
351
384
|
return readVscdbKey('cursorAuth/cachedEmail');
|
|
352
385
|
}
|
|
353
|
-
async function
|
|
386
|
+
async function fetchCursorMe(token) {
|
|
387
|
+
try {
|
|
388
|
+
const res = await fetch('https://cursor.com/api/auth/me', {
|
|
389
|
+
headers: { Cookie: `WorkosCursorSessionToken=${token}` },
|
|
390
|
+
});
|
|
391
|
+
if (!res.ok)
|
|
392
|
+
return null;
|
|
393
|
+
const data = (await res.json());
|
|
394
|
+
if (typeof data.id !== 'number')
|
|
395
|
+
return null;
|
|
396
|
+
return {
|
|
397
|
+
userId: data.id,
|
|
398
|
+
email: typeof data.email === 'string' ? data.email : '',
|
|
399
|
+
};
|
|
400
|
+
}
|
|
401
|
+
catch {
|
|
402
|
+
return null;
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
async function fetchCursorTeamInfo(token) {
|
|
354
406
|
try {
|
|
355
407
|
const res = await fetch('https://cursor.com/api/dashboard/teams', {
|
|
356
408
|
method: 'POST',
|
|
@@ -365,7 +417,58 @@ async function fetchCursorTeamId(token) {
|
|
|
365
417
|
return null;
|
|
366
418
|
const data = (await res.json());
|
|
367
419
|
const first = data.teams?.[0];
|
|
368
|
-
|
|
420
|
+
if (!first || typeof first.id !== 'number')
|
|
421
|
+
return null;
|
|
422
|
+
const startMs = typeof first.billingCycleStart === 'string' ? Number(first.billingCycleStart) : NaN;
|
|
423
|
+
const endMs = typeof first.billingCycleEnd === 'string' ? Number(first.billingCycleEnd) : NaN;
|
|
424
|
+
return {
|
|
425
|
+
teamId: first.id,
|
|
426
|
+
billingCycleStartMs: Number.isFinite(startMs) ? startMs : 0,
|
|
427
|
+
billingCycleEndMs: Number.isFinite(endMs) ? endMs : 0,
|
|
428
|
+
};
|
|
429
|
+
}
|
|
430
|
+
catch {
|
|
431
|
+
return null;
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
async function fetchCursorDailySpend(token, teamId, userId, periodStartMs, periodEndMs) {
|
|
435
|
+
try {
|
|
436
|
+
const res = await fetch('https://cursor.com/api/dashboard/get-daily-spend-by-category', {
|
|
437
|
+
method: 'POST',
|
|
438
|
+
headers: {
|
|
439
|
+
Cookie: buildCursorCookie(token, { team_id: teamId }),
|
|
440
|
+
'Content-Type': 'application/json',
|
|
441
|
+
Origin: 'https://cursor.com',
|
|
442
|
+
},
|
|
443
|
+
body: JSON.stringify({ teamId, userId, periodStartMs, periodEndMs, groupBy: 1, spendType: 1 }),
|
|
444
|
+
});
|
|
445
|
+
if (!res.ok)
|
|
446
|
+
return null;
|
|
447
|
+
const data = (await res.json());
|
|
448
|
+
if (!Array.isArray(data.dailySpend))
|
|
449
|
+
return null;
|
|
450
|
+
const byCategory = {};
|
|
451
|
+
for (const entry of data.dailySpend) {
|
|
452
|
+
const cat = typeof entry.category === 'string' ? entry.category : '';
|
|
453
|
+
if (!cat)
|
|
454
|
+
continue;
|
|
455
|
+
const tokens = typeof entry.totalTokens === 'string' ? Number(entry.totalTokens) : 0;
|
|
456
|
+
const spend = typeof entry.spendCents === 'number' ? entry.spendCents : 0;
|
|
457
|
+
if (!byCategory[cat])
|
|
458
|
+
byCategory[cat] = { tokens: 0, spendCents: 0 };
|
|
459
|
+
byCategory[cat].tokens += tokens;
|
|
460
|
+
byCategory[cat].spendCents += spend;
|
|
461
|
+
}
|
|
462
|
+
const models = Object.entries(byCategory).map(([cat, agg]) => ({
|
|
463
|
+
model: cat,
|
|
464
|
+
inputTokens: agg.tokens,
|
|
465
|
+
outputTokens: 0,
|
|
466
|
+
cacheWriteTokens: 0,
|
|
467
|
+
cacheReadTokens: 0,
|
|
468
|
+
costUsd: agg.spendCents / 100,
|
|
469
|
+
}));
|
|
470
|
+
models.sort((a, b) => b.inputTokens - a.inputTokens);
|
|
471
|
+
return models;
|
|
369
472
|
}
|
|
370
473
|
catch {
|
|
371
474
|
return null;
|
|
@@ -387,72 +490,90 @@ export async function fetchCursorCosts() {
|
|
|
387
490
|
+ ' To get your token: cursor.com > DevTools (F12) > Application > Cookies > WorkosCursorSessionToken',
|
|
388
491
|
};
|
|
389
492
|
}
|
|
390
|
-
const usageRes = await
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
} })();
|
|
401
|
-
const detail = parsed?.error ?? `HTTP ${usageRes.status}`;
|
|
402
|
-
const hint = detail.includes('origin') || detail.includes('authenticated')
|
|
403
|
-
? '\n Try updating your token: agentlens config --set-cursor-token <token>\n'
|
|
404
|
-
+ ' Get it from: cursor.com > DevTools (F12) > Application > Cookies > WorkosCursorSessionToken'
|
|
405
|
-
: '';
|
|
406
|
-
return {
|
|
407
|
-
tool: 'Cursor',
|
|
408
|
-
totalCostUsd: 0,
|
|
409
|
-
totalInputTokens: 0,
|
|
410
|
-
totalOutputTokens: 0,
|
|
411
|
-
models: [],
|
|
412
|
-
period,
|
|
413
|
-
error: `API error: ${detail}${hint}`,
|
|
414
|
-
};
|
|
415
|
-
}
|
|
416
|
-
const usageData = (await usageRes.json());
|
|
417
|
-
const models = [];
|
|
418
|
-
let totalTokens = 0;
|
|
493
|
+
const [usageRes, meResult, configResult, usageSummaryResult] = await Promise.all([
|
|
494
|
+
fetch('https://cursor.com/api/usage', {
|
|
495
|
+
headers: { Cookie: `WorkosCursorSessionToken=${token}` },
|
|
496
|
+
}).catch((err) => err),
|
|
497
|
+
fetchCursorMe(token),
|
|
498
|
+
loadConfig().catch(() => ({ cursorTeamId: undefined })),
|
|
499
|
+
fetchCursorUsageSummary(token),
|
|
500
|
+
]);
|
|
501
|
+
const subErrors = [];
|
|
502
|
+
// Premium request counts from /api/usage (legacy; may be empty for new dollar-based plans)
|
|
419
503
|
let totalRequests = 0;
|
|
420
504
|
let maxRequests;
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
}
|
|
505
|
+
if (usageRes instanceof Error) {
|
|
506
|
+
subErrors.push(`usage: ${usageRes.message}`);
|
|
507
|
+
}
|
|
508
|
+
else if (usageRes.ok) {
|
|
509
|
+
try {
|
|
510
|
+
const usageData = (await usageRes.json());
|
|
511
|
+
for (const [key, val] of Object.entries(usageData)) {
|
|
512
|
+
if (key === 'startOfMonth' || typeof val !== 'object' || !val)
|
|
513
|
+
continue;
|
|
514
|
+
const entry = val;
|
|
515
|
+
const reqs = typeof entry.numRequests === 'number' ? entry.numRequests : 0;
|
|
516
|
+
const maxReq = typeof entry.maxRequestUsage === 'number' ? entry.maxRequestUsage : undefined;
|
|
517
|
+
totalRequests += reqs;
|
|
518
|
+
if (maxReq != null)
|
|
519
|
+
maxRequests = (maxRequests ?? 0) + maxReq;
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
catch (err) {
|
|
523
|
+
subErrors.push(`usage parse: ${extractErrorMessage(err)}`);
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
else {
|
|
527
|
+
subErrors.push(`usage: HTTP ${usageRes.status}`);
|
|
441
528
|
}
|
|
442
|
-
models.sort((a, b) => b.inputTokens - a.inputTokens);
|
|
443
529
|
let planType;
|
|
530
|
+
let limitType;
|
|
531
|
+
let included;
|
|
444
532
|
let onDemand;
|
|
445
533
|
let teamOnDemand;
|
|
534
|
+
let pooled;
|
|
535
|
+
let billingCycleStartMs;
|
|
536
|
+
let billingCycleEndMs;
|
|
446
537
|
let leaderboard;
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
538
|
+
if (usageSummaryResult) {
|
|
539
|
+
planType = usageSummaryResult.planType;
|
|
540
|
+
limitType = usageSummaryResult.limitType;
|
|
541
|
+
included = usageSummaryResult.included;
|
|
542
|
+
onDemand = usageSummaryResult.onDemand;
|
|
543
|
+
teamOnDemand = usageSummaryResult.teamOnDemand;
|
|
544
|
+
pooled = usageSummaryResult.pooled;
|
|
545
|
+
billingCycleStartMs = usageSummaryResult.billingCycleStartMs;
|
|
546
|
+
billingCycleEndMs = usageSummaryResult.billingCycleEndMs;
|
|
547
|
+
}
|
|
548
|
+
else {
|
|
549
|
+
subErrors.push('usage-summary: failed');
|
|
550
|
+
}
|
|
551
|
+
const email = meResult?.email || await getCursorEmail();
|
|
453
552
|
let teamId = configResult.cursorTeamId ?? null;
|
|
454
|
-
|
|
455
|
-
|
|
553
|
+
let billingStart = billingCycleStartMs ?? 0;
|
|
554
|
+
let billingEnd = billingCycleEndMs ?? 0;
|
|
555
|
+
const teamInfo = await fetchCursorTeamInfo(token);
|
|
556
|
+
if (teamInfo) {
|
|
557
|
+
teamId = teamInfo.teamId;
|
|
558
|
+
// Prefer team endpoint cycle if usage-summary didn't provide one.
|
|
559
|
+
if (billingStart === 0)
|
|
560
|
+
billingStart = teamInfo.billingCycleStartMs;
|
|
561
|
+
if (billingEnd === 0)
|
|
562
|
+
billingEnd = teamInfo.billingCycleEndMs;
|
|
563
|
+
}
|
|
564
|
+
// Per-model token + cost data from daily-spend API
|
|
565
|
+
let models = [];
|
|
566
|
+
let totalTokens = 0;
|
|
567
|
+
const userId = meResult?.userId ?? null;
|
|
568
|
+
if (teamId != null && userId != null && billingStart > 0 && billingEnd > 0) {
|
|
569
|
+
const dailyModels = await fetchCursorDailySpend(token, teamId, userId, billingStart, billingEnd);
|
|
570
|
+
if (dailyModels && dailyModels.length > 0) {
|
|
571
|
+
models = dailyModels;
|
|
572
|
+
totalTokens = models.reduce((sum, m) => sum + m.inputTokens, 0);
|
|
573
|
+
}
|
|
574
|
+
else {
|
|
575
|
+
subErrors.push('daily-spend: no data');
|
|
576
|
+
}
|
|
456
577
|
}
|
|
457
578
|
let leaderboardResult;
|
|
458
579
|
if (teamId != null && email) {
|
|
@@ -463,15 +584,23 @@ export async function fetchCursorCosts() {
|
|
|
463
584
|
else {
|
|
464
585
|
leaderboardResult = { status: 'fulfilled', value: null };
|
|
465
586
|
}
|
|
466
|
-
if (usageSummaryResult) {
|
|
467
|
-
planType = usageSummaryResult.planType;
|
|
468
|
-
onDemand = usageSummaryResult.onDemand;
|
|
469
|
-
teamOnDemand = usageSummaryResult.teamOnDemand;
|
|
470
|
-
}
|
|
471
587
|
if (leaderboardResult.status === 'fulfilled' && leaderboardResult.value) {
|
|
472
588
|
leaderboard = leaderboardResult.value;
|
|
473
589
|
}
|
|
474
|
-
const
|
|
590
|
+
const includedSpendCents = included?.enabled ? included.usedCents : 0;
|
|
591
|
+
const onDemandSpendCents = onDemand?.enabled ? onDemand.usedCents : 0;
|
|
592
|
+
const dailySpendCents = models.reduce((sum, m) => sum + Math.round(m.costUsd * 100), 0);
|
|
593
|
+
// Prefer the daily-spend total when present (matches dashboard exactly);
|
|
594
|
+
// fall back to included + on-demand from the usage-summary buckets.
|
|
595
|
+
const totalCents = dailySpendCents > 0 ? dailySpendCents : includedSpendCents + onDemandSpendCents;
|
|
596
|
+
const cursorCostUsd = totalCents / 100;
|
|
597
|
+
// Only surface sub-errors when we have nothing useful to show.
|
|
598
|
+
const hasAnyData = cursorCostUsd > 0 ||
|
|
599
|
+
included != null ||
|
|
600
|
+
onDemand != null ||
|
|
601
|
+
pooled != null ||
|
|
602
|
+
models.length > 0 ||
|
|
603
|
+
totalRequests > 0;
|
|
475
604
|
return {
|
|
476
605
|
tool: 'Cursor',
|
|
477
606
|
totalCostUsd: cursorCostUsd,
|
|
@@ -480,11 +609,17 @@ export async function fetchCursorCosts() {
|
|
|
480
609
|
totalRequests,
|
|
481
610
|
maxRequests,
|
|
482
611
|
planType,
|
|
612
|
+
limitType,
|
|
613
|
+
included,
|
|
483
614
|
onDemand,
|
|
484
615
|
teamOnDemand,
|
|
616
|
+
pooled,
|
|
617
|
+
billingCycleStartMs: billingStart > 0 ? billingStart : undefined,
|
|
618
|
+
billingCycleEndMs: billingEnd > 0 ? billingEnd : undefined,
|
|
485
619
|
leaderboard,
|
|
486
620
|
models,
|
|
487
621
|
period,
|
|
622
|
+
error: hasAnyData ? undefined : (subErrors.length > 0 ? subErrors.join('; ') : 'No data returned from Cursor APIs'),
|
|
488
623
|
};
|
|
489
624
|
}
|
|
490
625
|
catch (err) {
|
|
@@ -495,7 +630,7 @@ export async function fetchCursorCosts() {
|
|
|
495
630
|
totalOutputTokens: 0,
|
|
496
631
|
models: [],
|
|
497
632
|
period,
|
|
498
|
-
error:
|
|
633
|
+
error: extractErrorMessage(err),
|
|
499
634
|
};
|
|
500
635
|
}
|
|
501
636
|
}
|
|
@@ -509,6 +644,24 @@ async function getClaudeSessionToken() {
|
|
|
509
644
|
}
|
|
510
645
|
}
|
|
511
646
|
const BROWSER_UA = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36';
|
|
647
|
+
function normalizeClaudePlanType(raw) {
|
|
648
|
+
const lower = raw.toLowerCase();
|
|
649
|
+
if (lower.includes('enterprise'))
|
|
650
|
+
return 'enterprise';
|
|
651
|
+
if (lower.includes('team'))
|
|
652
|
+
return 'team';
|
|
653
|
+
if (lower === 'max' || lower.includes('max'))
|
|
654
|
+
return 'max';
|
|
655
|
+
if (lower === 'pro' || lower.includes('professional') || lower.includes('pro_'))
|
|
656
|
+
return 'pro';
|
|
657
|
+
if (lower === 'free' || lower.includes('free'))
|
|
658
|
+
return 'free';
|
|
659
|
+
if (lower.includes('subscription') || lower.includes('stripe'))
|
|
660
|
+
return undefined;
|
|
661
|
+
if (lower.includes('individual'))
|
|
662
|
+
return 'pro';
|
|
663
|
+
return undefined;
|
|
664
|
+
}
|
|
512
665
|
async function fetchClaudeBootstrap(token) {
|
|
513
666
|
try {
|
|
514
667
|
const res = await fetch('https://claude.ai/api/bootstrap', {
|
|
@@ -526,9 +679,48 @@ async function fetchClaudeBootstrap(token) {
|
|
|
526
679
|
for (const m of memberships) {
|
|
527
680
|
const org = m.organization;
|
|
528
681
|
if (org && typeof org.uuid === 'string') {
|
|
682
|
+
let planType;
|
|
683
|
+
for (const key of ['billing_type', 'plan_type', 'subscription_type', 'plan', 'type']) {
|
|
684
|
+
const val = org[key];
|
|
685
|
+
if (typeof val === 'string' && val) {
|
|
686
|
+
const normalized = normalizeClaudePlanType(val);
|
|
687
|
+
if (normalized) {
|
|
688
|
+
planType = normalized;
|
|
689
|
+
break;
|
|
690
|
+
}
|
|
691
|
+
}
|
|
692
|
+
}
|
|
693
|
+
if (!planType) {
|
|
694
|
+
const settings = org.settings;
|
|
695
|
+
if (settings) {
|
|
696
|
+
for (const key of ['billing_type', 'plan_type']) {
|
|
697
|
+
const val = settings[key];
|
|
698
|
+
if (typeof val === 'string' && val) {
|
|
699
|
+
const normalized = normalizeClaudePlanType(val);
|
|
700
|
+
if (normalized) {
|
|
701
|
+
planType = normalized;
|
|
702
|
+
break;
|
|
703
|
+
}
|
|
704
|
+
}
|
|
705
|
+
}
|
|
706
|
+
}
|
|
707
|
+
}
|
|
708
|
+
const activeFlags = org.active_flags;
|
|
709
|
+
if (!planType && Array.isArray(activeFlags)) {
|
|
710
|
+
for (const flag of activeFlags) {
|
|
711
|
+
if (typeof flag !== 'string')
|
|
712
|
+
continue;
|
|
713
|
+
const normalized = normalizeClaudePlanType(flag);
|
|
714
|
+
if (normalized) {
|
|
715
|
+
planType = normalized;
|
|
716
|
+
break;
|
|
717
|
+
}
|
|
718
|
+
}
|
|
719
|
+
}
|
|
529
720
|
orgs.push({
|
|
530
721
|
uuid: org.uuid,
|
|
531
722
|
name: typeof org.name === 'string' ? org.name : '',
|
|
723
|
+
planType,
|
|
532
724
|
});
|
|
533
725
|
}
|
|
534
726
|
}
|
|
@@ -539,6 +731,78 @@ async function fetchClaudeBootstrap(token) {
|
|
|
539
731
|
return null;
|
|
540
732
|
}
|
|
541
733
|
}
|
|
734
|
+
async function fetchClaudeAdminUsage(adminKey, startDate, endDate) {
|
|
735
|
+
try {
|
|
736
|
+
const params = new URLSearchParams({
|
|
737
|
+
group_by: 'model',
|
|
738
|
+
start_date: startDate,
|
|
739
|
+
end_date: endDate,
|
|
740
|
+
interval: 'month',
|
|
741
|
+
});
|
|
742
|
+
const res = await fetch(`https://api.anthropic.com/v1/organizations/usage_report/messages?${params.toString()}`, {
|
|
743
|
+
headers: {
|
|
744
|
+
'x-api-key': adminKey,
|
|
745
|
+
'anthropic-version': '2023-06-01',
|
|
746
|
+
},
|
|
747
|
+
});
|
|
748
|
+
if (!res.ok)
|
|
749
|
+
return [];
|
|
750
|
+
const data = (await res.json());
|
|
751
|
+
const rows = data.data;
|
|
752
|
+
if (!Array.isArray(rows))
|
|
753
|
+
return [];
|
|
754
|
+
const byModel = {};
|
|
755
|
+
for (const row of rows) {
|
|
756
|
+
const model = typeof row.model === 'string' ? row.model : 'unknown';
|
|
757
|
+
if (!byModel[model]) {
|
|
758
|
+
byModel[model] = { model, inputTokens: 0, outputTokens: 0, cacheCreationTokens: 0, cacheReadTokens: 0 };
|
|
759
|
+
}
|
|
760
|
+
const m = byModel[model];
|
|
761
|
+
m.inputTokens += typeof row.input_tokens === 'number' ? row.input_tokens : 0;
|
|
762
|
+
m.outputTokens += typeof row.output_tokens === 'number' ? row.output_tokens : 0;
|
|
763
|
+
m.cacheCreationTokens += typeof row.cache_creation_input_tokens === 'number' ? row.cache_creation_input_tokens : 0;
|
|
764
|
+
m.cacheReadTokens += typeof row.cache_read_input_tokens === 'number' ? row.cache_read_input_tokens : 0;
|
|
765
|
+
}
|
|
766
|
+
return Object.values(byModel);
|
|
767
|
+
}
|
|
768
|
+
catch {
|
|
769
|
+
return [];
|
|
770
|
+
}
|
|
771
|
+
}
|
|
772
|
+
async function fetchClaudeAdminCost(adminKey, startDate, endDate) {
|
|
773
|
+
try {
|
|
774
|
+
const params = new URLSearchParams({
|
|
775
|
+
group_by: 'model',
|
|
776
|
+
start_date: startDate,
|
|
777
|
+
end_date: endDate,
|
|
778
|
+
interval: 'month',
|
|
779
|
+
});
|
|
780
|
+
const res = await fetch(`https://api.anthropic.com/v1/organizations/cost_report?${params.toString()}`, {
|
|
781
|
+
headers: {
|
|
782
|
+
'x-api-key': adminKey,
|
|
783
|
+
'anthropic-version': '2023-06-01',
|
|
784
|
+
},
|
|
785
|
+
});
|
|
786
|
+
if (!res.ok)
|
|
787
|
+
return [];
|
|
788
|
+
const data = (await res.json());
|
|
789
|
+
const rows = data.data;
|
|
790
|
+
if (!Array.isArray(rows))
|
|
791
|
+
return [];
|
|
792
|
+
const byModel = {};
|
|
793
|
+
for (const row of rows) {
|
|
794
|
+
const model = typeof row.model === 'string' ? row.model : 'unknown';
|
|
795
|
+
if (!byModel[model]) {
|
|
796
|
+
byModel[model] = { model, costUsd: 0 };
|
|
797
|
+
}
|
|
798
|
+
byModel[model].costUsd += typeof row.cost_usd === 'number' ? row.cost_usd : 0;
|
|
799
|
+
}
|
|
800
|
+
return Object.values(byModel);
|
|
801
|
+
}
|
|
802
|
+
catch {
|
|
803
|
+
return [];
|
|
804
|
+
}
|
|
805
|
+
}
|
|
542
806
|
export async function fetchClaudeAiCosts() {
|
|
543
807
|
const period = formatPeriod();
|
|
544
808
|
try {
|
|
@@ -607,13 +871,50 @@ export async function fetchClaudeAiCosts() {
|
|
|
607
871
|
spentCents,
|
|
608
872
|
limitCents,
|
|
609
873
|
orgName: org.name,
|
|
874
|
+
planType: org.planType,
|
|
610
875
|
};
|
|
876
|
+
const now = new Date();
|
|
877
|
+
const startDate = localDateStr(new Date(now.getFullYear(), now.getMonth(), 1));
|
|
878
|
+
const nextMonth = new Date(now.getFullYear(), now.getMonth() + 1, 1);
|
|
879
|
+
const endDate = localDateStr(nextMonth);
|
|
880
|
+
let models = [];
|
|
881
|
+
let totalInput = 0;
|
|
882
|
+
let totalOutput = 0;
|
|
883
|
+
let totalCacheW = 0;
|
|
884
|
+
let totalCacheR = 0;
|
|
885
|
+
if (config.claudeAdminApiKey) {
|
|
886
|
+
const [usageRows, costRows] = await Promise.all([
|
|
887
|
+
fetchClaudeAdminUsage(config.claudeAdminApiKey, startDate, endDate),
|
|
888
|
+
fetchClaudeAdminCost(config.claudeAdminApiKey, startDate, endDate),
|
|
889
|
+
]);
|
|
890
|
+
if (usageRows.length > 0) {
|
|
891
|
+
const costMap = new Map(costRows.map((c) => [c.model, c.costUsd]));
|
|
892
|
+
for (const u of usageRows) {
|
|
893
|
+
totalInput += u.inputTokens;
|
|
894
|
+
totalOutput += u.outputTokens;
|
|
895
|
+
totalCacheW += u.cacheCreationTokens;
|
|
896
|
+
totalCacheR += u.cacheReadTokens;
|
|
897
|
+
models.push({
|
|
898
|
+
model: u.model,
|
|
899
|
+
inputTokens: u.inputTokens,
|
|
900
|
+
outputTokens: u.outputTokens,
|
|
901
|
+
cacheWriteTokens: u.cacheCreationTokens,
|
|
902
|
+
cacheReadTokens: u.cacheReadTokens,
|
|
903
|
+
costUsd: costMap.get(u.model) ?? 0,
|
|
904
|
+
});
|
|
905
|
+
}
|
|
906
|
+
models.sort((a, b) => b.costUsd - a.costUsd);
|
|
907
|
+
}
|
|
908
|
+
}
|
|
611
909
|
return {
|
|
612
910
|
tool: 'Claude.ai',
|
|
613
911
|
totalCostUsd: spentCents / 100,
|
|
614
|
-
totalInputTokens:
|
|
615
|
-
totalOutputTokens:
|
|
616
|
-
|
|
912
|
+
totalInputTokens: totalInput,
|
|
913
|
+
totalOutputTokens: totalOutput,
|
|
914
|
+
totalCacheWriteTokens: totalCacheW > 0 ? totalCacheW : undefined,
|
|
915
|
+
totalCacheReadTokens: totalCacheR > 0 ? totalCacheR : undefined,
|
|
916
|
+
planType: org.planType,
|
|
917
|
+
models,
|
|
617
918
|
claudeAi,
|
|
618
919
|
period,
|
|
619
920
|
};
|
|
@@ -636,11 +937,11 @@ export async function fetchClaudeAiCosts() {
|
|
|
636
937
|
totalOutputTokens: 0,
|
|
637
938
|
models: [],
|
|
638
939
|
period,
|
|
639
|
-
error:
|
|
940
|
+
error: extractErrorMessage(err),
|
|
640
941
|
};
|
|
641
942
|
}
|
|
642
943
|
}
|
|
643
|
-
export async function fetchAllCosts(disabledCostTools) {
|
|
944
|
+
export async function fetchAllCosts(disabledCostTools, onTool) {
|
|
644
945
|
const now = new Date();
|
|
645
946
|
const monthStart = localDateStr(new Date(now.getFullYear(), now.getMonth(), 1));
|
|
646
947
|
const monthNames = ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'];
|
|
@@ -654,25 +955,26 @@ export async function fetchAllCosts(disabledCostTools) {
|
|
|
654
955
|
fetchers.push({ tool: 'Claude Code', promise: fetchClaudeCodeCosts() });
|
|
655
956
|
if (!skip.has('Cursor'))
|
|
656
957
|
fetchers.push({ tool: 'Cursor', promise: fetchCursorCosts() });
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
}
|
|
958
|
+
// Settle each fetcher independently and stream updates via onTool as they
|
|
959
|
+
// resolve, so the UI can render Claude Code (fast/local) immediately
|
|
960
|
+
// without waiting for Cursor (slow/remote).
|
|
961
|
+
const wrapped = fetchers.map((f) => f.promise.then((value) => {
|
|
962
|
+
onTool?.(value);
|
|
963
|
+
return value;
|
|
964
|
+
}, (reason) => {
|
|
965
|
+
const errorSummary = {
|
|
966
|
+
tool: f.tool,
|
|
967
|
+
totalCostUsd: 0,
|
|
968
|
+
totalInputTokens: 0,
|
|
969
|
+
totalOutputTokens: 0,
|
|
970
|
+
models: [],
|
|
971
|
+
period: formatPeriod(),
|
|
972
|
+
error: extractErrorMessage(reason),
|
|
973
|
+
};
|
|
974
|
+
onTool?.(errorSummary);
|
|
975
|
+
return errorSummary;
|
|
976
|
+
}));
|
|
977
|
+
const tools = await Promise.all(wrapped);
|
|
676
978
|
return {
|
|
677
979
|
tools,
|
|
678
980
|
month: monthLabel,
|