tokmon 0.16.1 → 0.18.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.
@@ -5,7 +5,7 @@ import {
5
5
  cacheDir,
6
6
  detectProviders,
7
7
  resolveTimezone
8
- } from "./chunk-ZPH4754N.js";
8
+ } from "./chunk-UAPL47GL.js";
9
9
 
10
10
  // src/web/server.ts
11
11
  import { createServer } from "http";
@@ -47,12 +47,12 @@ function colorHex(accountColor, providerColorName) {
47
47
  // src/web/data.ts
48
48
  async function resolveAccounts(config) {
49
49
  const detected = await detectProviders();
50
- const accounts = buildAccounts(config, detected);
50
+ const accounts = buildAccounts({ ...config, disabledProviders: [] }, detected);
51
51
  return accounts.map((a) => {
52
52
  const p = PROVIDERS[a.providerId];
53
53
  return {
54
54
  account: a,
55
- hasUsage: p.hasUsage,
55
+ hasUsage: p.hasUsage || !!p.fetchTable,
56
56
  hasBilling: p.hasBilling,
57
57
  color: colorHex(a.color, PROVIDERS[a.providerId].color)
58
58
  };
@@ -282,11 +282,16 @@ var SSE_HEARTBEAT_MS = 25e3;
282
282
  var IDLE_PAUSE_MS = 6e4;
283
283
  var SNAPSHOT_CACHE_THROTTLE_MS = 2e4;
284
284
  var snapshotCacheFile = () => join3(cacheDir(), "web-snapshot.json");
285
+ var sseFrame = (s) => `event: snapshot
286
+ data: ${JSON.stringify(s)}
287
+
288
+ `;
285
289
  function createDataEngine(opts) {
286
290
  const { version, tz, summaryIntervalMs, billingIntervalMs, resolved } = opts;
287
291
  const usage = /* @__PURE__ */ new Map();
288
292
  const billing = /* @__PURE__ */ new Map();
289
293
  let current = null;
294
+ let currentFrame = null;
290
295
  const sseClients = /* @__PURE__ */ new Map();
291
296
  let lastActivity = Date.now();
292
297
  let stopped = false;
@@ -312,6 +317,7 @@ function createDataEngine(opts) {
312
317
  if (a.billing) billing.set(a.id, a.billing);
313
318
  }
314
319
  current = assembleSnapshot({ version, tz, intervalMs: summaryIntervalMs, resolved, usage, billing });
320
+ currentFrame = sseFrame(current);
315
321
  } catch {
316
322
  }
317
323
  };
@@ -329,15 +335,12 @@ function createDataEngine(opts) {
329
335
  const rebuild = () => {
330
336
  if (stopped) return;
331
337
  current = assembleSnapshot({ version, tz, intervalMs: summaryIntervalMs, resolved, usage, billing });
338
+ currentFrame = sseFrame(current);
332
339
  persist();
333
340
  if (sseClients.size === 0) return;
334
- const payload = `event: snapshot
335
- data: ${JSON.stringify(current)}
336
-
337
- `;
338
341
  for (const res of sseClients.keys()) {
339
342
  try {
340
- res.write(payload);
343
+ res.write(currentFrame);
341
344
  } catch {
342
345
  }
343
346
  }
@@ -416,10 +419,7 @@ data: ${JSON.stringify(current)}
416
419
  Connection: "keep-alive"
417
420
  });
418
421
  res.write("retry: 3000\n\n");
419
- if (current) res.write(`event: snapshot
420
- data: ${JSON.stringify(current)}
421
-
422
- `);
422
+ if (currentFrame) res.write(currentFrame);
423
423
  const beat = setInterval(() => {
424
424
  try {
425
425
  res.write(": ping\n\n");
@@ -755,7 +755,8 @@ async function cursorBilling(account) {
755
755
  summary: lines ? `${lines} \xB7 ${spendLabel}` : spendLabel
756
756
  };
757
757
  }
758
- return { ...core, activity: merged };
758
+ const modelSpend = spend?.models?.length ? spend.models.slice(0, 6).map((m) => ({ name: m.name, usd: m.usd, requests: m.requests })) : null;
759
+ return { ...core, activity: merged, modelSpend };
759
760
  }
760
761
  async function cursorBillingCore(account) {
761
762
  const db = cursorStateDb(account.homeDir);
@@ -845,6 +846,46 @@ async function cursorModelSpend(homeDir) {
845
846
  if (total <= 0) return null;
846
847
  return { total, models };
847
848
  }
849
+ var USAGE_SQL = "SELECT json_extract(c.value,'$.createdAt') AS createdAt, mk.key AS model, sum(json_extract(mk.value,'$.costInCents')) AS cents, sum(json_extract(mk.value,'$.amount')) AS amt FROM cursorDiskKV c, json_each(c.value,'$.usageData') mk WHERE c.key LIKE 'composerData:%' AND json_valid(c.value) AND json_type(c.value,'$.usageData')='object' AND json_extract(c.value,'$.createdAt') IS NOT NULL GROUP BY createdAt, model;";
850
+ async function cursorUsageTable(tz, homeDir) {
851
+ const res = await runSqlite(cursorStateDb(homeDir), USAGE_SQL);
852
+ if (res.status !== "ok" || res.rows.length === 0) return null;
853
+ const buckets = { daily: /* @__PURE__ */ new Map(), weekly: /* @__PURE__ */ new Map(), monthly: /* @__PURE__ */ new Map() };
854
+ const put = (map, label, model, usd, reqs) => {
855
+ let row = map.get(label);
856
+ if (!row) {
857
+ row = { label, models: [], input: 0, output: 0, cacheCreate: 0, cacheRead: 0, cacheSavings: 0, total: 0, cost: 0, count: 0, breakdown: [] };
858
+ map.set(label, row);
859
+ }
860
+ row.cost += usd;
861
+ row.count += reqs;
862
+ let md = row.breakdown.find((b) => b.name === model);
863
+ if (!md) {
864
+ md = { name: model, input: 0, output: 0, cacheCreate: 0, cacheRead: 0, cacheSavings: 0, cost: 0, count: 0 };
865
+ row.breakdown.push(md);
866
+ }
867
+ md.cost += usd;
868
+ md.count += reqs;
869
+ };
870
+ for (const r of res.rows) {
871
+ const ts = Number(r.createdAt);
872
+ if (!Number.isFinite(ts) || ts <= 0) continue;
873
+ const usd = (Number(r.cents) || 0) / 100;
874
+ const reqs = Number(r.amt) || 0;
875
+ if (usd <= 0 && reqs <= 0) continue;
876
+ const model = String(r.model ?? "unknown");
877
+ put(buckets.daily, dayKey(ts, tz), model, usd, reqs);
878
+ put(buckets.weekly, weekKey(ts, tz), model, usd, reqs);
879
+ put(buckets.monthly, monthKey(ts, tz), model, usd, reqs);
880
+ }
881
+ const finalize = (map) => [...map.values()].map((row) => {
882
+ row.breakdown.sort((a, b) => b.cost - a.cost);
883
+ row.models = row.breakdown.map((b) => b.name);
884
+ return row;
885
+ }).sort((a, b) => a.label.localeCompare(b.label));
886
+ const table = { daily: finalize(buckets.daily), weekly: finalize(buckets.weekly), monthly: finalize(buckets.monthly) };
887
+ return table.daily.length ? table : null;
888
+ }
848
889
 
849
890
  // src/providers/claude/usage.ts
850
891
  import { readdir, stat as fsStat, access as access2 } from "fs/promises";
@@ -1446,15 +1487,165 @@ var codexProvider = {
1446
1487
  fetchBilling: (account) => codexBilling(account)
1447
1488
  };
1448
1489
 
1490
+ // src/providers/cursor/usage.ts
1491
+ var EVENTS_URL = "https://api2.cursor.sh/aiserver.v1.DashboardService/GetFilteredUsageEvents";
1492
+ var WINDOW_DAYS = 90;
1493
+ var PAGE_SIZE = 1e3;
1494
+ var MAX_PAGES = 12;
1495
+ var SKIP_KINDS = /* @__PURE__ */ new Set(["USAGE_EVENT_KIND_ABORTED_NOT_CHARGED", "USAGE_EVENT_KIND_ERRORED_NOT_CHARGED"]);
1496
+ async function readToken(homeDir) {
1497
+ const r = await runSqlite(cursorStateDb(homeDir), "SELECT value FROM ItemTable WHERE key='cursorAuth/accessToken' LIMIT 1;");
1498
+ const raw = r.status === "ok" ? r.rows[0]?.value : void 0;
1499
+ if (typeof raw !== "string" || !raw.trim()) return null;
1500
+ return raw.trim().replace(/^"|"$/g, "");
1501
+ }
1502
+ async function fetchPage(token, startMs, endMs, page) {
1503
+ try {
1504
+ const res = await fetch(EVENTS_URL, {
1505
+ method: "POST",
1506
+ headers: {
1507
+ Authorization: `Bearer ${token}`,
1508
+ "Content-Type": "application/json",
1509
+ "Connect-Protocol-Version": "1",
1510
+ "User-Agent": "tokmon"
1511
+ },
1512
+ body: JSON.stringify({ startDate: String(startMs), endDate: String(endMs), page, pageSize: PAGE_SIZE }),
1513
+ signal: AbortSignal.timeout(15e3)
1514
+ });
1515
+ if (!res.ok) return null;
1516
+ return await res.json();
1517
+ } catch {
1518
+ return null;
1519
+ }
1520
+ }
1521
+ async function cursorApiUsage(tz, homeDir) {
1522
+ const token = await readToken(homeDir);
1523
+ if (!token) return null;
1524
+ const endMs = Date.now();
1525
+ const startMs = endMs - WINDOW_DAYS * 864e5;
1526
+ const events = [];
1527
+ for (let page = 1; page <= MAX_PAGES; page++) {
1528
+ const resp = await fetchPage(token, startMs, endMs, page);
1529
+ if (!resp) return page === 1 ? null : finalizeTable(events, tz);
1530
+ const batch = resp.usageEventsDisplay ?? [];
1531
+ events.push(...batch);
1532
+ if (batch.length < PAGE_SIZE) break;
1533
+ }
1534
+ return finalizeTable(events, tz);
1535
+ }
1536
+ function finalizeTable(events, tz) {
1537
+ if (events.length === 0) return null;
1538
+ const buckets = { daily: /* @__PURE__ */ new Map(), weekly: /* @__PURE__ */ new Map(), monthly: /* @__PURE__ */ new Map() };
1539
+ const put = (map, label, model, usd, input, output, cacheRead) => {
1540
+ let row = map.get(label);
1541
+ if (!row) {
1542
+ row = { label, models: [], input: 0, output: 0, cacheCreate: 0, cacheRead: 0, cacheSavings: 0, total: 0, cost: 0, count: 0, breakdown: [] };
1543
+ map.set(label, row);
1544
+ }
1545
+ row.input += input;
1546
+ row.output += output;
1547
+ row.cacheRead += cacheRead;
1548
+ row.total += input + output + cacheRead;
1549
+ row.cost += usd;
1550
+ row.count += 1;
1551
+ let md = row.breakdown.find((b) => b.name === model);
1552
+ if (!md) {
1553
+ md = { name: model, input: 0, output: 0, cacheCreate: 0, cacheRead: 0, cacheSavings: 0, cost: 0, count: 0 };
1554
+ row.breakdown.push(md);
1555
+ }
1556
+ md.input += input;
1557
+ md.output += output;
1558
+ md.cacheRead += cacheRead;
1559
+ md.cost += usd;
1560
+ md.count += 1;
1561
+ };
1562
+ for (const e of events) {
1563
+ if (e.kind && SKIP_KINDS.has(e.kind)) continue;
1564
+ const ts = Number(e.timestamp);
1565
+ if (!Number.isFinite(ts) || ts <= 0) continue;
1566
+ const tu = e.tokenUsage ?? {};
1567
+ const input = safeNum(tu.inputTokens);
1568
+ const output = safeNum(tu.outputTokens);
1569
+ const cacheRead = safeNum(tu.cacheReadTokens);
1570
+ const cents = Number(e.chargedCents);
1571
+ const usd = Number.isFinite(cents) && cents > 0 ? cents / 100 : 0;
1572
+ if (usd <= 0 && input + output + cacheRead === 0) continue;
1573
+ const model = String(e.model ?? "unknown");
1574
+ put(buckets.daily, dayKey(ts, tz), model, usd, input, output, cacheRead);
1575
+ put(buckets.weekly, weekKey(ts, tz), model, usd, input, output, cacheRead);
1576
+ put(buckets.monthly, monthKey(ts, tz), model, usd, input, output, cacheRead);
1577
+ }
1578
+ const sortRows = (map) => [...map.values()].map((row) => {
1579
+ row.breakdown.sort((a, b) => b.cost - a.cost);
1580
+ row.models = row.breakdown.map((b) => b.name);
1581
+ return row;
1582
+ }).sort((a, b) => a.label.localeCompare(b.label));
1583
+ const table = { daily: sortRows(buckets.daily), weekly: sortRows(buckets.weekly), monthly: sortRows(buckets.monthly) };
1584
+ return table.daily.length ? table : null;
1585
+ }
1586
+
1449
1587
  // src/providers/cursor/index.ts
1588
+ var EMPTY = { daily: [], weekly: [], monthly: [] };
1589
+ var overlayDaily = (lo, hi) => {
1590
+ const m = new Map(lo.map((r) => [r.label, r]));
1591
+ for (const r of hi) m.set(r.label, r);
1592
+ return [...m.values()].sort((a, b) => a.label.localeCompare(b.label));
1593
+ };
1594
+ function reBucket(daily, tz, keyOf) {
1595
+ const out = /* @__PURE__ */ new Map();
1596
+ for (const day of daily) {
1597
+ const [y, mo, d] = day.label.split("-").map(Number);
1598
+ const label = keyOf(Date.UTC(y, mo - 1, d, 12), tz);
1599
+ let row = out.get(label);
1600
+ if (!row) {
1601
+ row = { label, models: [], input: 0, output: 0, cacheCreate: 0, cacheRead: 0, cacheSavings: 0, total: 0, cost: 0, count: 0, breakdown: [] };
1602
+ out.set(label, row);
1603
+ }
1604
+ row.input += day.input;
1605
+ row.output += day.output;
1606
+ row.cacheCreate += day.cacheCreate;
1607
+ row.cacheRead += day.cacheRead;
1608
+ row.cacheSavings += day.cacheSavings;
1609
+ row.total += day.total;
1610
+ row.cost += day.cost;
1611
+ row.count += day.count;
1612
+ for (const b of day.breakdown) {
1613
+ let md = row.breakdown.find((x) => x.name === b.name);
1614
+ if (!md) {
1615
+ md = { name: b.name, input: 0, output: 0, cacheCreate: 0, cacheRead: 0, cacheSavings: 0, cost: 0, count: 0 };
1616
+ row.breakdown.push(md);
1617
+ }
1618
+ md.input += b.input;
1619
+ md.output += b.output;
1620
+ md.cacheCreate += b.cacheCreate;
1621
+ md.cacheRead += b.cacheRead;
1622
+ md.cacheSavings += b.cacheSavings;
1623
+ md.cost += b.cost;
1624
+ md.count += b.count;
1625
+ }
1626
+ }
1627
+ return [...out.values()].map((r) => {
1628
+ r.breakdown.sort((a, b) => b.cost - a.cost);
1629
+ r.models = r.breakdown.map((b) => b.name);
1630
+ return r;
1631
+ }).sort((a, b) => a.label.localeCompare(b.label));
1632
+ }
1633
+ async function cursorTable(tz, homeDir) {
1634
+ const [api, local] = await Promise.all([cursorApiUsage(tz, homeDir), cursorUsageTable(tz, homeDir)]);
1635
+ if (!api && !local) return EMPTY;
1636
+ const daily = overlayDaily(local?.daily ?? [], api?.daily ?? []);
1637
+ if (daily.length === 0) return EMPTY;
1638
+ return { daily, weekly: reBucket(daily, tz, weekKey), monthly: reBucket(daily, tz, monthKey) };
1639
+ }
1450
1640
  var cursorProvider = {
1451
1641
  id: "cursor",
1452
1642
  name: "Cursor",
1453
1643
  color: "magenta",
1644
+ // hasUsage: false — TUI keys its dedicated Cursor spend-table off this flag directly.
1454
1645
  hasUsage: false,
1455
- // Cursor exposes spend/limits, not a token history
1456
1646
  hasBilling: true,
1457
1647
  detect: (homeDir) => detectCursor(homeDir),
1648
+ fetchTable: (account, tz) => cursorTable(tz, account.homeDir),
1458
1649
  fetchBilling: (account) => cursorBilling(account)
1459
1650
  };
1460
1651
 
package/dist/cli.js CHANGED
@@ -23,7 +23,7 @@ import {
23
23
  systemTimezone,
24
24
  time,
25
25
  tokens
26
- } from "./chunk-ZPH4754N.js";
26
+ } from "./chunk-UAPL47GL.js";
27
27
 
28
28
  // src/cli.tsx
29
29
  import { EventEmitter } from "events";
@@ -1606,11 +1606,7 @@ function App({ interval: cliInterval, initialConfig }) {
1606
1606
  const hasStrip = slots.length > 1;
1607
1607
  const stripChipW = (s) => 2 + 2 + truncateName(s.name, 16).length + 2;
1608
1608
  const stripChars = slots.reduce((sum, s) => sum + stripChipW(s), 0);
1609
- const stripLines = hasStrip ? Math.max(1, Math.ceil(stripChars / Math.max(
1610
- 1,
1611
- cols - 4 - 7
1612
- /*"focus "*/
1613
- ))) : 0;
1609
+ const stripLines = hasStrip ? Math.max(1, Math.ceil(stripChars / Math.max(1, cols - 4 - 7))) : 0;
1614
1610
  const headerRows = cols < 70 ? 2 : 1;
1615
1611
  const CHROME = 2 + headerRows + 3 + (hasStrip ? 1 + stripLines : 0) + 2 + 2;
1616
1612
  const gridBudget = Math.max(1, rows - CHROME);
@@ -1856,7 +1852,6 @@ function App({ interval: cliInterval, initialConfig }) {
1856
1852
  function toggleProvider(pid) {
1857
1853
  updateConfig((c) => ({
1858
1854
  ...c,
1859
- // Toggling in settings is also an explicit decision → mark it known.
1860
1855
  knownProviders: c.knownProviders.includes(pid) ? c.knownProviders : [...c.knownProviders, pid],
1861
1856
  disabledProviders: c.disabledProviders.includes(pid) ? c.disabledProviders.filter((p) => p !== pid) : [...c.disabledProviders, pid]
1862
1857
  }));
@@ -1996,7 +1991,7 @@ function App({ interval: cliInterval, initialConfig }) {
1996
1991
  setWebStatus("off");
1997
1992
  } else {
1998
1993
  setWebStatus("starting");
1999
- const { startWebServer } = await import("./server-NO7JYH7U.js");
1994
+ const { startWebServer } = await import("./server-VMB5ZLZC.js");
2000
1995
  const ctrl = await startWebServer({ config: cfg, log: false });
2001
1996
  webRef.current = ctrl;
2002
1997
  setWebUrl(ctrl.url);
@@ -2674,7 +2669,7 @@ process.emitWarning = ((warning, ...rest) => {
2674
2669
  var args = process.argv.slice(2);
2675
2670
  var subcommand = args[0]?.toLowerCase();
2676
2671
  if (subcommand === "serve" || subcommand === "web") {
2677
- const { startWeb } = await import("./web-KQUELAT7.js");
2672
+ const { startWeb } = await import("./web-DYAEMCAE.js");
2678
2673
  await startWeb(args.slice(1));
2679
2674
  process.exit(typeof process.exitCode === "number" ? process.exitCode : 0);
2680
2675
  }
@@ -1,8 +1,8 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  startWebServer
4
- } from "./chunk-Z7JLP2Y2.js";
5
- import "./chunk-ZPH4754N.js";
4
+ } from "./chunk-STCMWCFW.js";
5
+ import "./chunk-UAPL47GL.js";
6
6
  export {
7
7
  startWebServer
8
8
  };