tokmon 0.20.5 → 0.21.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.
@@ -122,13 +122,13 @@ function isValidTimezone(tz) {
122
122
  }
123
123
  function normalizeConfig(parsed) {
124
124
  try {
125
- const accounts = (Array.isArray(parsed.accounts) ? parsed.accounts : []).map((a) => ({ ...a, providerId: a.providerId ?? "claude" })).filter((a) => typeof a?.id === "string" && typeof a?.name === "string" && PROVIDER_IDS.includes(a.providerId));
125
+ const accounts = (Array.isArray(parsed.accounts) ? parsed.accounts : []).map((a) => ({ ...a, providerId: a.providerId ?? "claude" })).filter((a) => typeof a?.id === "string" && typeof a?.name === "string" && PROVIDER_IDS.includes(a.providerId)).map((a) => ({ ...a, homeDir: typeof a.homeDir === "string" && a.homeDir.trim() ? a.homeDir : "~" }));
126
126
  return {
127
127
  ...DEFAULTS,
128
128
  interval: clampNum(parsed.interval, DEFAULTS.interval, 1),
129
129
  billingInterval: clampNum(parsed.billingInterval, DEFAULTS.billingInterval, 1),
130
130
  clearScreen: typeof parsed.clearScreen === "boolean" ? parsed.clearScreen : DEFAULTS.clearScreen,
131
- timezone: typeof parsed.timezone === "string" && parsed.timezone.trim() && isValidTimezone(parsed.timezone.trim()) ? parsed.timezone : null,
131
+ timezone: typeof parsed.timezone === "string" && parsed.timezone.trim() && isValidTimezone(parsed.timezone.trim()) ? parsed.timezone.trim() : null,
132
132
  accounts,
133
133
  activeAccountId: typeof parsed.activeAccountId === "string" ? parsed.activeAccountId : null,
134
134
  disabledProviders: (Array.isArray(parsed.disabledProviders) ? parsed.disabledProviders : []).filter((p) => PROVIDER_IDS.includes(p)),
@@ -8,7 +8,7 @@ import {
8
8
  detectProviders,
9
9
  fetchPeak,
10
10
  resolveTimezone
11
- } from "./chunk-EHIQHGJL.js";
11
+ } from "./chunk-YCIUMAKK.js";
12
12
  import {
13
13
  cacheDir,
14
14
  expandHome,
@@ -16,7 +16,7 @@ import {
16
16
  normalizeConfig,
17
17
  saveConfig,
18
18
  snapshotCacheFile
19
- } from "./chunk-XQEJ4WQ5.js";
19
+ } from "./chunk-7MFZMI5C.js";
20
20
 
21
21
  // src/web/server.ts
22
22
  import { createServer } from "http";
@@ -96,10 +96,10 @@ async function fetchAccountTable(account, tz) {
96
96
  if (!p.fetchTable) return null;
97
97
  return p.fetchTable(account, tz);
98
98
  }
99
- async function fetchAccountBilling(account) {
99
+ async function fetchAccountBilling(account, tz) {
100
100
  const p = PROVIDERS[account.providerId];
101
101
  if (!p.fetchBilling) return null;
102
- return p.fetchBilling(account);
102
+ return p.fetchBilling(account, tz);
103
103
  }
104
104
  function assembleSnapshot(opts) {
105
105
  const accounts = opts.resolved.map((r) => {
@@ -429,123 +429,78 @@ function createDataEngine(opts) {
429
429
  };
430
430
  let usageAccounts = resolved.filter((r) => r.hasUsage);
431
431
  let billingAccounts = resolved.filter((r) => r.hasBilling);
432
- let summaryBusy = false;
433
- let summaryForcePending = false;
434
- const refreshSummary = async (force = false) => {
435
- if (stopped) return;
436
- if (summaryBusy) {
437
- if (force) summaryForcePending = true;
438
- return;
439
- }
440
- if (!force && idle()) return;
441
- const epoch = configEpoch;
442
- summaryBusy = true;
443
- try {
444
- for (const r of usageAccounts) {
445
- if (stopped) return;
446
- let dashboard = null;
447
- let ok = true;
448
- try {
449
- dashboard = await withTimeout(fetchAccountSummary(r.account, tz), FETCH_TIMEOUT_MS);
450
- } catch {
451
- ok = false;
452
- }
453
- if (stopped || epoch !== configEpoch) return;
454
- if (ok) {
455
- usageEntry(r.account.id).dashboard = dashboard;
456
- summaryState.set(r.account.id, "ready");
457
- } else summaryState.set(r.account.id, "error");
458
- reveal();
459
- }
460
- rebuild();
461
- } finally {
462
- summaryBusy = false;
463
- if (summaryForcePending && !stopped) {
464
- summaryForcePending = false;
465
- void refreshSummary(true);
432
+ const makeRefreshLoop = (opts2) => {
433
+ let busy = false;
434
+ let forcePending = false;
435
+ const run = async (force = false) => {
436
+ if (stopped) return;
437
+ if (busy) {
438
+ if (force) forcePending = true;
439
+ return;
466
440
  }
467
- }
468
- };
469
- let tableBusy = false;
470
- let tableForcePending = false;
471
- const refreshTable = async (force = false) => {
472
- if (stopped) return;
473
- if (tableBusy) {
474
- if (force) tableForcePending = true;
475
- return;
476
- }
477
- if (!force && idle()) return;
478
- const epoch = configEpoch;
479
- tableBusy = true;
480
- try {
481
- for (const r of usageAccounts) {
482
- if (stopped) return;
483
- let table = null;
484
- let ok = true;
485
- try {
486
- table = await withTimeout(fetchAccountTable(r.account, tz), FETCH_TIMEOUT_MS);
487
- } catch {
488
- ok = false;
441
+ if (!force && idle()) return;
442
+ const epoch = configEpoch;
443
+ busy = true;
444
+ try {
445
+ for (const r of opts2.accounts()) {
446
+ if (stopped) return;
447
+ let value = null;
448
+ let ok = true;
449
+ try {
450
+ value = await withTimeout(opts2.fetch(r), FETCH_TIMEOUT_MS);
451
+ } catch {
452
+ ok = false;
453
+ }
454
+ if (stopped || epoch !== configEpoch) return;
455
+ if (ok) {
456
+ opts2.apply(r.account.id, value);
457
+ opts2.state.set(r.account.id, "ready");
458
+ } else opts2.state.set(r.account.id, "error");
459
+ reveal();
489
460
  }
490
- if (stopped || epoch !== configEpoch) return;
491
- if (ok) {
492
- usageEntry(r.account.id).table = table;
493
- tableState.set(r.account.id, "ready");
494
- } else tableState.set(r.account.id, "error");
495
- reveal();
496
- }
497
- rebuild();
498
- } finally {
499
- tableBusy = false;
500
- if (tableForcePending && !stopped) {
501
- tableForcePending = false;
502
- void refreshTable(true);
503
- }
504
- }
505
- };
506
- let billingBusy = false;
507
- let billingForcePending = false;
508
- const refreshBilling = async (force = false) => {
509
- if (stopped) return;
510
- if (billingBusy) {
511
- if (force) billingForcePending = true;
512
- return;
513
- }
514
- if (!force && idle()) return;
515
- const epoch = configEpoch;
516
- billingBusy = true;
517
- try {
518
- for (const r of billingAccounts) {
519
- if (stopped) return;
520
- let result = null;
521
- let ok = true;
522
- try {
523
- result = await withTimeout(fetchAccountBilling(r.account), FETCH_TIMEOUT_MS);
524
- } catch {
525
- ok = false;
461
+ rebuild();
462
+ } finally {
463
+ busy = false;
464
+ if (forcePending && !stopped) {
465
+ forcePending = false;
466
+ void run(true);
526
467
  }
527
- if (stopped || epoch !== configEpoch) return;
528
- if (ok) {
529
- billing.set(r.account.id, result);
530
- billingState.set(r.account.id, "ready");
531
- } else billingState.set(r.account.id, "error");
532
- reveal();
533
468
  }
534
- rebuild();
535
- } finally {
536
- billingBusy = false;
537
- if (billingForcePending && !stopped) {
538
- billingForcePending = false;
539
- void refreshBilling(true);
540
- }
541
- }
469
+ };
470
+ return run;
542
471
  };
472
+ const refreshSummary = makeRefreshLoop({
473
+ accounts: () => usageAccounts,
474
+ fetch: (r) => fetchAccountSummary(r.account, tz),
475
+ apply: (id, dashboard) => {
476
+ usageEntry(id).dashboard = dashboard;
477
+ },
478
+ state: summaryState
479
+ });
480
+ const refreshTable = makeRefreshLoop({
481
+ accounts: () => usageAccounts,
482
+ fetch: (r) => fetchAccountTable(r.account, tz),
483
+ apply: (id, table) => {
484
+ usageEntry(id).table = table;
485
+ },
486
+ state: tableState
487
+ });
488
+ const refreshBilling = makeRefreshLoop({
489
+ accounts: () => billingAccounts,
490
+ fetch: (r) => fetchAccountBilling(r.account, tz),
491
+ apply: (id, result) => {
492
+ billing.set(id, result);
493
+ },
494
+ state: billingState
495
+ });
543
496
  let peakBusy = false;
544
497
  const refreshPeak = async (force = false) => {
545
498
  if (stopped || peakBusy || !hasClaude || !force && idle()) return;
499
+ const epoch = configEpoch;
546
500
  peakBusy = true;
547
501
  try {
548
502
  const next = await fetchPeak();
503
+ if (stopped || epoch !== configEpoch || !hasClaude) return;
549
504
  if (next) {
550
505
  peak = next;
551
506
  rebuild();
@@ -611,12 +566,16 @@ function createDataEngine(opts) {
611
566
  tz = next.tz;
612
567
  summaryIntervalMs = next.summaryIntervalMs;
613
568
  billingIntervalMs = next.billingIntervalMs;
569
+ const sourceKey = (r) => `${r.account.providerId}:${r.account.homeDir ?? ""}`;
570
+ const prevSources = new Map(resolved.map((r) => [r.account.id, sourceKey(r)]));
614
571
  resolved = next.resolved;
615
572
  hasClaude = resolved.some((r) => r.account.providerId === "claude");
616
573
  if (!hasClaude) peak = null;
617
574
  usageAccounts = resolved.filter((r) => r.hasUsage);
618
575
  billingAccounts = resolved.filter((r) => r.hasBilling);
619
- const survivors = new Set(resolved.map((r) => r.account.id));
576
+ const survivors = new Set(
577
+ resolved.filter((r) => !prevSources.has(r.account.id) || prevSources.get(r.account.id) === sourceKey(r)).map((r) => r.account.id)
578
+ );
620
579
  for (const id of [...usage.keys()]) if (!survivors.has(id)) usage.delete(id);
621
580
  for (const id of [...billing.keys()]) if (!survivors.has(id)) billing.delete(id);
622
581
  for (const map of [summaryState, billingState, tableState]) {