tokmon 0.20.0 → 0.20.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/README.md CHANGED
@@ -96,25 +96,25 @@ It binds to `127.0.0.1` only and reads the same data read-only — nothing leave
96
96
 
97
97
  KPIs with inline sparklines, provider cards with live rate-limit bars, and a cost-over-time chart that spans your full history by default. Toggle **merged** (one combined total) vs **split** (a line per provider), **all-time** vs the selected period, and linear vs log.
98
98
 
99
- ![tokmon web dashboard — overview](docs/web/overview.png)
99
+ ![tokmon web dashboard — overview](assets/web/overview.png)
100
100
 
101
101
  ### Analytics
102
102
 
103
103
  A full-width, all-time daily-spend calendar — hover any day for a per-model spend breakdown — with at-a-glance stats (busiest day, daily average, top weekday, current streak), alongside cost-by-model, an interactive provider split, token composition, cache savings, and cumulative spend.
104
104
 
105
- ![tokmon web dashboard — analytics](docs/web/analytics.png)
105
+ ![tokmon web dashboard — analytics](assets/web/analytics.png)
106
106
 
107
107
  ### Models
108
108
 
109
109
  A leaderboard sortable by cost / tokens / calls, each row showing a per-model trend sparkline, cost-per-call, tokens, and calls — over tokens-by-model and cache-savings-by-model charts.
110
110
 
111
- ![tokmon web dashboard — models](docs/web/models.png)
111
+ ![tokmon web dashboard — models](assets/web/models.png)
112
112
 
113
113
  ### Explore
114
114
 
115
115
  The full daily / weekly / monthly table — searchable, sortable on every column, with expandable per-model breakdowns.
116
116
 
117
- ![tokmon web dashboard — explore](docs/web/explore.png)
117
+ ![tokmon web dashboard — explore](assets/web/explore.png)
118
118
 
119
119
  The dashboard is a prebuilt static bundle shipped in the package — no build step, fully offline.
120
120
 
@@ -219,12 +219,23 @@ tokmon runs entirely on your machine and reads everything **read-only**:
219
219
 
220
220
  ## How It Works
221
221
 
222
- - Parses local CLI session logs and aggregates cost/token usage per day, week, and month.
222
+ tokmon runs a small local **daemon** that does all the data collection. The terminal UI and the web dashboard are both thin clients of it, talking over a loopback-only WebSocket — so a single process does the work and the TUI and web always show the same numbers. The daemon starts automatically with the TUI (and standalone via `tokmon serve`), and idle-pauses when nothing is watching.
223
+
224
+ **Usage & cost**
225
+ - Parses each tool's local session logs — Claude / Codex / pi `JSONL`, Cursor / opencode `SQLite` — and aggregates cost and token usage per day, week, and month.
226
+ - Cost is an API-equivalent estimate from each model's published pricing, counting cached input at the discounted cache-read rate (not the full input rate, not free).
223
227
  - A persistent parse cache keyed by file **mtime + size** makes repeat launches near-instant; edited or deleted files are re-read automatically.
224
- - Dashboard summaries and table history load independently, so the UI stays responsive on large histories.
225
- - Rate limits and spend are fetched from each provider's API on the billing poll interval.
226
228
 
227
- Cross-platform: macOS, Linux, Windows.
229
+ **Accounts**
230
+ - Each enabled provider is detected automatically, and its real account identity — email and plan — is read from local auth (e.g. Claude `~/.claude.json`, the Codex `id_token`, Cursor's state DB). Extra accounts, like additional Claude homes, are auto-discovered too.
231
+
232
+ **Limits & billing**
233
+ - Rate limits and remaining spend/quota come from each provider's own official API, refreshed on the billing poll interval.
234
+
235
+ **Responsiveness**
236
+ - Dashboard summaries and table history load independently and refresh on separate intervals, so the UI stays responsive even on large histories.
237
+
238
+ Cross-platform: macOS, Linux, Windows. Everything is local and read-only — see [Privacy](#privacy).
228
239
 
229
240
  ## Requirements
230
241
 
@@ -18,11 +18,10 @@ import {
18
18
  systemTimezone,
19
19
  time,
20
20
  tokens
21
- } from "./chunk-MB6LRSEZ.js";
21
+ } from "./chunk-7HJIP4U6.js";
22
22
  import {
23
23
  COLOR_PALETTE,
24
24
  DEFAULTS,
25
- cacheDir,
26
25
  configLocation,
27
26
  generateAccountId,
28
27
  getTrackedAccountRows,
@@ -31,8 +30,9 @@ import {
31
30
  normalizeConfig,
32
31
  pickAccentColor,
33
32
  sanitizeTyped,
34
- saveConfigSync
35
- } from "./chunk-MVMPQJ5S.js";
33
+ saveConfigSync,
34
+ snapshotCacheFile
35
+ } from "./chunk-XQEJ4WQ5.js";
36
36
  import {
37
37
  glyphs
38
38
  } from "./chunk-RF4GGQGM.js";
@@ -82,8 +82,6 @@ function reconcileDaemonConfig(previous, daemonConfig, pendingLocalConfig) {
82
82
 
83
83
  // src/client/seed-cache.ts
84
84
  import { readFile } from "fs/promises";
85
- import { join } from "path";
86
- var snapshotCacheFile = () => join(cacheDir(), "web-snapshot.json");
87
85
  async function loadSeedSnapshot() {
88
86
  try {
89
87
  const parsed = JSON.parse(await readFile(snapshotCacheFile(), "utf-8"));
@@ -3196,7 +3194,7 @@ function App({ interval: cliInterval, initialConfig, baseUrl = null, wsToken = n
3196
3194
  if (webStartingRef.current) return;
3197
3195
  webStartingRef.current = true;
3198
3196
  try {
3199
- const { startWebServer } = await import("./server-SP3FOHM2.js");
3197
+ const { startWebServer } = await import("./server-BXMRN774.js");
3200
3198
  const ctrl = await startWebServer({ config: cfg, log: false });
3201
3199
  webRef.current = ctrl;
3202
3200
  openUrl(ctrl.url);
@@ -7,16 +7,16 @@ import {
7
7
  buildAccounts,
8
8
  detectProviders,
9
9
  fetchPeak,
10
- resolveTimezone,
11
- toJsonSafe
12
- } from "./chunk-MB6LRSEZ.js";
10
+ resolveTimezone
11
+ } from "./chunk-7HJIP4U6.js";
13
12
  import {
14
13
  cacheDir,
15
14
  expandHome,
16
15
  loadConfig,
17
16
  normalizeConfig,
18
- saveConfig
19
- } from "./chunk-MVMPQJ5S.js";
17
+ saveConfig,
18
+ snapshotCacheFile
19
+ } from "./chunk-XQEJ4WQ5.js";
20
20
 
21
21
  // src/web/server.ts
22
22
  import { createServer } from "http";
@@ -135,7 +135,7 @@ function assembleSnapshot(opts) {
135
135
  color: namedHex(PROVIDERS[r.account.providerId].color)
136
136
  });
137
137
  }
138
- return toJsonSafe({
138
+ return {
139
139
  version: opts.version,
140
140
  generatedAt: Date.now(),
141
141
  tz: opts.tz,
@@ -144,7 +144,7 @@ function assembleSnapshot(opts) {
144
144
  accounts,
145
145
  seeded: opts.seeded ?? false,
146
146
  peak: opts.peak ?? null
147
- });
147
+ };
148
148
  }
149
149
  function tzFor(config) {
150
150
  return resolveTimezone(config.timezone);
@@ -314,13 +314,11 @@ code{color:#e6b450}</style></head><body>
314
314
 
315
315
  // src/web/data-engine.ts
316
316
  import { readFileSync as readFileSync2, writeFileSync, mkdirSync, renameSync } from "fs";
317
- import { join as join3 } from "path";
318
317
  var TABLE_INTERVAL_MS = 3e5;
319
318
  var PEAK_INTERVAL_MS = 3e5;
320
319
  var IDLE_PAUSE_MS = 6e4;
321
320
  var SNAPSHOT_CACHE_THROTTLE_MS = 2e4;
322
321
  var REVEAL_THROTTLE_MS = 500;
323
- var snapshotCacheFile = () => join3(cacheDir(), "web-snapshot.json");
324
322
  function createDataEngine(opts) {
325
323
  const { version } = opts;
326
324
  let tz = opts.tz;
@@ -397,7 +395,7 @@ function createDataEngine(opts) {
397
395
  lastPersist = Date.now();
398
396
  try {
399
397
  mkdirSync(cacheDir(), { recursive: true, mode: 448 });
400
- const tmp = join3(cacheDir(), `web-snapshot.json.${process.pid}.tmp`);
398
+ const tmp = `${snapshotCacheFile()}.${process.pid}.tmp`;
401
399
  writeFileSync(tmp, JSON.stringify(current), { mode: 384 });
402
400
  renameSync(tmp, snapshotCacheFile());
403
401
  } catch {
@@ -700,7 +698,7 @@ import { RpcSerialization, RpcServer } from "effect/unstable/rpc";
700
698
  // src/web/fs.ts
701
699
  import { readdir, stat as stat2, realpath } from "fs/promises";
702
700
  import { homedir } from "os";
703
- import { join as join4, resolve as resolvePath, isAbsolute, sep as sep2 } from "path";
701
+ import { join as join3, resolve as resolvePath, isAbsolute, sep as sep2 } from "path";
704
702
  function isContained(root, target) {
705
703
  return target === root || target.startsWith(root + sep2);
706
704
  }
@@ -723,7 +721,7 @@ async function listHomeDirectory(rawPath) {
723
721
  for (const d of dirents) {
724
722
  if (d.name.startsWith(".")) continue;
725
723
  let dir = d.isDirectory();
726
- const full = join4(abs, d.name);
724
+ const full = join3(abs, d.name);
727
725
  if (d.isSymbolicLink()) {
728
726
  let real2;
729
727
  try {
@@ -1,11 +1,12 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
+ PROVIDER_IDS,
3
4
  cacheDir,
4
5
  envDir,
5
6
  expandHome,
6
7
  isValidTimezone,
7
8
  slugify
8
- } from "./chunk-MVMPQJ5S.js";
9
+ } from "./chunk-XQEJ4WQ5.js";
9
10
 
10
11
  // src/providers/usage-core.ts
11
12
  import { readFile, writeFile, rename, mkdir } from "fs/promises";
@@ -112,6 +113,31 @@ function weekKey(ts, tz) {
112
113
  return dayKey(startOfWeek(ts, tz), tz);
113
114
  }
114
115
 
116
+ // src/providers/_shared/metric.ts
117
+ var finite = (value, fallback = 0) => typeof value === "number" && Number.isFinite(value) ? value : fallback;
118
+ var finiteNumber = (value) => typeof value === "number" && Number.isFinite(value);
119
+ function finitePositive(value) {
120
+ return finiteNumber(value) && value > 0 ? value : 0;
121
+ }
122
+ function safeNum(value) {
123
+ return finiteNumber(value) && value > 0 ? Math.floor(value) : 0;
124
+ }
125
+ function finitePositiveCoerced(value) {
126
+ const n = Number(value);
127
+ return Number.isFinite(n) && n > 0 ? n : 0;
128
+ }
129
+ function percentMetric(label, used, resetsAt, primary) {
130
+ return {
131
+ label,
132
+ used: finite(used),
133
+ limit: 100,
134
+ format: { kind: "percent" },
135
+ resetsAt,
136
+ ...primary === void 0 ? {} : { primary }
137
+ };
138
+ }
139
+ var dollars = (cents) => finite(cents) / 100;
140
+
115
141
  // src/providers/usage-core.ts
116
142
  var SPARK_DAYS = 14;
117
143
  var DAY_MS = 864e5;
@@ -226,9 +252,6 @@ async function loadCachedEntries(files, parse, since) {
226
252
  if (dirty) scheduleFlush();
227
253
  return dedupe(chunks.flat().filter((e) => e.ts >= since));
228
254
  }
229
- function safeNum(v) {
230
- return typeof v === "number" && Number.isFinite(v) && v > 0 ? Math.floor(v) : 0;
231
- }
232
255
  function dashboardSince(tz) {
233
256
  const now = Date.now();
234
257
  return Math.min(startOfMonth(now, tz), startOfWeek(now, tz), now - SPARK_DAYS * DAY_MS);
@@ -236,9 +259,6 @@ function dashboardSince(tz) {
236
259
  function tableSince(tz) {
237
260
  return monthsAgoStart(Date.now(), 6, tz);
238
261
  }
239
- function finitePositive(v) {
240
- return typeof v === "number" && Number.isFinite(v) && v > 0 ? v : 0;
241
- }
242
262
  function cleanEntry(e) {
243
263
  return {
244
264
  ...e,
@@ -495,20 +515,6 @@ async function readJson(res) {
495
515
  }
496
516
  }
497
517
 
498
- // src/providers/_shared/metric.ts
499
- var finite = (value, fallback = 0) => typeof value === "number" && Number.isFinite(value) ? value : fallback;
500
- function percentMetric(label, used, resetsAt, primary) {
501
- return {
502
- label,
503
- used: finite(used),
504
- limit: 100,
505
- format: { kind: "percent" },
506
- resetsAt,
507
- ...primary === void 0 ? {} : { primary }
508
- };
509
- }
510
- var dollars = (cents) => finite(cents) / 100;
511
-
512
518
  // src/providers/_shared/time.ts
513
519
  function msToIso(ms) {
514
520
  return Number.isFinite(ms) && Math.abs(ms) <= 864e13 ? new Date(ms).toISOString() : null;
@@ -635,7 +641,6 @@ async function cursorActivity(homeDir) {
635
641
  var BASE = "https://api2.cursor.sh/aiserver.v1.DashboardService";
636
642
  var USAGE_URL = `${BASE}/GetCurrentPeriodUsage`;
637
643
  var PLAN_URL = `${BASE}/GetPlanInfo`;
638
- var finiteNumber = (value) => typeof value === "number" && Number.isFinite(value);
639
644
  function cursorStateDb(homeDir) {
640
645
  const base = homeDir ?? homedir2();
641
646
  const tail = ["Cursor", "User", "globalStorage", "state.vscdb"];
@@ -786,10 +791,6 @@ async function cursorBillingCore(account) {
786
791
  }
787
792
 
788
793
  // src/providers/cursor/composer.ts
789
- var finiteNonNegative = (value) => {
790
- const n = Number(value);
791
- return Number.isFinite(n) && n > 0 ? n : 0;
792
- };
793
794
  async function cursorModelSpend(homeDir) {
794
795
  const db = cursorStateDb(homeDir);
795
796
  const sql = "SELECT mk.key AS name, 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' GROUP BY mk.key ORDER BY cents DESC;";
@@ -798,9 +799,9 @@ async function cursorModelSpend(homeDir) {
798
799
  const models = [];
799
800
  let total = 0;
800
801
  for (const row of res.rows) {
801
- const usd = finiteNonNegative(row.cents) / 100;
802
+ const usd = finitePositiveCoerced(row.cents) / 100;
802
803
  if (usd <= 0) continue;
803
- models.push({ name: String(row.name ?? ""), usd, requests: finiteNonNegative(row.amt) });
804
+ models.push({ name: String(row.name ?? ""), usd, requests: finitePositiveCoerced(row.amt) });
804
805
  total += usd;
805
806
  }
806
807
  if (total <= 0) return null;
@@ -830,8 +831,8 @@ async function cursorUsageTable(tz, homeDir) {
830
831
  for (const r of res.rows) {
831
832
  const ts = Number(r.createdAt);
832
833
  if (!Number.isFinite(ts) || ts <= 0) continue;
833
- const usd = finiteNonNegative(r.cents) / 100;
834
- const reqs = finiteNonNegative(r.amt);
834
+ const usd = finitePositiveCoerced(r.cents) / 100;
835
+ const reqs = finitePositiveCoerced(r.amt);
835
836
  if (usd <= 0 && reqs <= 0) continue;
836
837
  const model = String(r.model ?? "unknown");
837
838
  put(buckets.daily, dayKey(ts, tz), model, usd, reqs);
@@ -1180,7 +1181,7 @@ var PRICING2 = {
1180
1181
  var ZERO_PRICE2 = { in: 0, cr: 0, out: 0 };
1181
1182
  var PRICE_KEYS2 = Object.keys(PRICING2).sort((a, b) => b.length - a.length);
1182
1183
  function codexHomes(homeDir) {
1183
- if (homeDir) return [join6(homeDir, ".codex")];
1184
+ if (homeDir) return [.../* @__PURE__ */ new Set([join6(homeDir, ".codex"), homeDir])];
1184
1185
  const homes = [];
1185
1186
  const codexHome = envDir("CODEX_HOME");
1186
1187
  if (codexHome) homes.push(codexHome);
@@ -2559,6 +2560,51 @@ import { homedir as homedir11 } from "os";
2559
2560
  function geminiCredsPath(homeDir) {
2560
2561
  return join13(homeDir ?? homedir11(), ".gemini", "oauth_creds.json");
2561
2562
  }
2563
+ function geminiDir(homeDir) {
2564
+ return join13(homeDir ?? homedir11(), ".gemini");
2565
+ }
2566
+ function authTypeFromSettings(settings) {
2567
+ const raw = settings?.security?.auth?.selectedType ?? settings?.selectedAuthType;
2568
+ if (typeof raw !== "string") return "none";
2569
+ const value = raw.trim().toLowerCase();
2570
+ if (!value) return "none";
2571
+ if (value.includes("vertex") || value.includes("use_vertex_ai")) return "vertex";
2572
+ if (value.includes("gemini-api-key") || value.includes("api-key") || value.includes("use_gemini")) return "api-key";
2573
+ return "none";
2574
+ }
2575
+ async function authMethodFromSettings(homeDir) {
2576
+ try {
2577
+ const raw = await readFile6(join13(geminiDir(homeDir), "settings.json"), "utf8");
2578
+ return authTypeFromSettings(JSON.parse(raw));
2579
+ } catch {
2580
+ return "none";
2581
+ }
2582
+ }
2583
+ async function hasGeminiApiKeyFile(homeDir) {
2584
+ try {
2585
+ await access8(join13(geminiDir(homeDir), "api_key"));
2586
+ return true;
2587
+ } catch {
2588
+ }
2589
+ try {
2590
+ const env = await readFile6(join13(geminiDir(homeDir), ".env"), "utf8");
2591
+ return /^\s*GEMINI_API_KEY\s*=/m.test(env);
2592
+ } catch {
2593
+ return false;
2594
+ }
2595
+ }
2596
+ function hasGeminiApiKeyEnv() {
2597
+ return ["GEMINI_API_KEY", "GOOGLE_API_KEY", "GOOGLE_GENAI_API_KEY"].some((name) => typeof process.env[name] === "string" && process.env[name].trim() !== "");
2598
+ }
2599
+ async function noOAuthAuthMessage(homeDir) {
2600
+ const settingsMethod = await authMethodFromSettings(homeDir);
2601
+ if (settingsMethod === "api-key") return "API key auth \u2014 quota needs Google login (run gemini)";
2602
+ if (settingsMethod === "vertex") return "Vertex AI auth \u2014 quota needs Google login (run gemini)";
2603
+ if (await hasGeminiApiKeyFile(homeDir) || hasGeminiApiKeyEnv()) {
2604
+ return "API key auth \u2014 quota needs Google login (run gemini)";
2605
+ }
2606
+ return "Not signed in \u2014 run gemini and log in with Google";
2607
+ }
2562
2608
  async function detectGemini(homeDir) {
2563
2609
  try {
2564
2610
  await access8(geminiCredsPath(homeDir));
@@ -2595,7 +2641,14 @@ async function geminiBilling(account) {
2595
2641
  const identity = geminiIdentity(creds);
2596
2642
  const accessToken = typeof creds?.access_token === "string" ? creds.access_token.trim() : "";
2597
2643
  const refreshToken = typeof creds?.refresh_token === "string" ? creds.refresh_token.trim() : null;
2598
- if (!creds || !accessToken && !refreshToken) return { plan: null, metrics: [], error: "Not signed in \u2014 run gemini", ...identity };
2644
+ if (!creds || !accessToken && !refreshToken) {
2645
+ return {
2646
+ plan: null,
2647
+ metrics: [],
2648
+ error: await noOAuthAuthMessage(account.homeDir),
2649
+ ...identity
2650
+ };
2651
+ }
2599
2652
  const quota = await fetchCloudCodeQuota({
2600
2653
  accessToken,
2601
2654
  refreshToken,
@@ -2714,7 +2767,7 @@ function installSignals(id) {
2714
2767
  }
2715
2768
 
2716
2769
  // src/providers/index.ts
2717
- var PROVIDER_ORDER = ["claude", "codex", "cursor", "copilot", "pi", "opencode", "antigravity", "gemini"];
2770
+ var PROVIDER_ORDER = [...PROVIDER_IDS];
2718
2771
  var PROVIDERS = {
2719
2772
  claude: claudeProvider,
2720
2773
  codex: codexProvider,
@@ -2725,7 +2778,6 @@ var PROVIDERS = {
2725
2778
  antigravity: antigravityProvider,
2726
2779
  gemini: geminiProvider
2727
2780
  };
2728
- var ALL_PROVIDERS = PROVIDER_ORDER.map((id) => PROVIDERS[id]);
2729
2781
  async function detectProviders() {
2730
2782
  const found = await Promise.all(
2731
2783
  PROVIDER_ORDER.map(async (id) => {
@@ -2779,7 +2831,7 @@ function readClaudeIdentity2(homeDir) {
2779
2831
  function hasClaudeState(homeDir) {
2780
2832
  return existsSync(join15(homeDir, ".claude.json")) || existsSync(join15(homeDir, ".claude", ".credentials.json")) || existsSync(join15(homeDir, ".claude", "projects")) || existsSync(join15(homeDir, ".config", "claude", ".credentials.json")) || existsSync(join15(homeDir, ".config", "claude", "projects"));
2781
2833
  }
2782
- function candidateAlternateHomes() {
2834
+ function candidateAlternateHomes(prefix) {
2783
2835
  const home = homedir13();
2784
2836
  let entries;
2785
2837
  try {
@@ -2788,8 +2840,9 @@ function candidateAlternateHomes() {
2788
2840
  return [];
2789
2841
  }
2790
2842
  const out = [];
2843
+ const pattern = new RegExp(`^\\.${prefix}[_-]`);
2791
2844
  for (const name of entries) {
2792
- if (!/^\.claude[_-]/.test(name)) continue;
2845
+ if (!pattern.test(name)) continue;
2793
2846
  const path = join15(home, name);
2794
2847
  try {
2795
2848
  if (!statSync(path).isDirectory()) continue;
@@ -2806,10 +2859,57 @@ function labelForClaudeHome(homeDir) {
2806
2859
  const raw = basename(homeDir).replace(/^\.claude[_-]?/, "").replace(/[_-]+/g, " ").trim();
2807
2860
  return raw ? `Claude ${raw}` : "Claude";
2808
2861
  }
2862
+ function decodeBase64UrlJson3(segment) {
2863
+ try {
2864
+ const normalized = segment.replace(/-/g, "+").replace(/_/g, "/");
2865
+ const padded = normalized + "=".repeat((4 - normalized.length % 4) % 4);
2866
+ return JSON.parse(Buffer.from(padded, "base64").toString("utf8"));
2867
+ } catch {
2868
+ return null;
2869
+ }
2870
+ }
2871
+ function codexAuthPaths(homeDir) {
2872
+ return [join15(homeDir, ".codex", "auth.json"), join15(homeDir, "auth.json")];
2873
+ }
2874
+ function readCodexIdentity(homeDir) {
2875
+ for (const path of codexAuthPaths(homeDir)) {
2876
+ try {
2877
+ const parsed = JSON.parse(readFileSync(path, "utf-8"));
2878
+ const idToken = parsed?.tokens?.id_token;
2879
+ if (typeof idToken !== "string" || !idToken.includes(".")) continue;
2880
+ const payload = decodeBase64UrlJson3(idToken.split(".")[1]);
2881
+ if (!payload || typeof payload !== "object") continue;
2882
+ return {
2883
+ email: typeof payload?.email === "string" && payload.email.trim() ? payload.email.trim() : void 0,
2884
+ displayName: typeof payload?.name === "string" && payload.name.trim() ? payload.name.trim() : typeof payload?.given_name === "string" && payload.given_name.trim() ? payload.given_name.trim() : void 0
2885
+ };
2886
+ } catch {
2887
+ }
2888
+ }
2889
+ return {};
2890
+ }
2891
+ function hasCodexAuth(homeDir) {
2892
+ for (const path of codexAuthPaths(homeDir)) {
2893
+ try {
2894
+ const parsed = JSON.parse(readFileSync(path, "utf-8"));
2895
+ const accessToken = parsed?.tokens?.access_token;
2896
+ if (typeof accessToken === "string" && accessToken.trim()) return true;
2897
+ } catch {
2898
+ }
2899
+ }
2900
+ return false;
2901
+ }
2902
+ function labelForCodexHome(homeDir) {
2903
+ const identity = readCodexIdentity(homeDir);
2904
+ if (identity.email) return `Codex ${identity.email}`;
2905
+ if (identity.displayName) return `Codex ${identity.displayName}`;
2906
+ const raw = basename(homeDir).replace(/^\.codex[_-]?/, "").replace(/[_-]+/g, " ").trim();
2907
+ return raw ? `Codex ${raw}` : "Codex";
2908
+ }
2809
2909
  function discoverClaudeAccounts(usedIds) {
2810
2910
  const provider = PROVIDERS.claude;
2811
2911
  const out = [];
2812
- for (const homeDir of candidateAlternateHomes()) {
2912
+ for (const homeDir of candidateAlternateHomes("claude")) {
2813
2913
  if (!hasClaudeState(homeDir)) continue;
2814
2914
  const suffix = basename(homeDir).replace(/^\.claude[_-]?/, "") || basename(homeDir);
2815
2915
  out.push({
@@ -2822,8 +2922,25 @@ function discoverClaudeAccounts(usedIds) {
2822
2922
  }
2823
2923
  return out;
2824
2924
  }
2925
+ function discoverCodexAccounts(usedIds) {
2926
+ const provider = PROVIDERS.codex;
2927
+ const out = [];
2928
+ for (const homeDir of candidateAlternateHomes("codex")) {
2929
+ if (!hasCodexAuth(homeDir)) continue;
2930
+ const suffix = basename(homeDir).replace(/^\.codex[_-]?/, "") || basename(homeDir);
2931
+ out.push({
2932
+ id: uniqueId(`codex_${suffix}`, usedIds),
2933
+ providerId: "codex",
2934
+ name: labelForCodexHome(homeDir),
2935
+ color: provider.color,
2936
+ homeDir
2937
+ });
2938
+ }
2939
+ return out;
2940
+ }
2825
2941
  function discoverProviderAccounts(providerId, usedIds) {
2826
2942
  if (providerId === "claude") return discoverClaudeAccounts(usedIds);
2943
+ if (providerId === "codex") return discoverCodexAccounts(usedIds);
2827
2944
  return [];
2828
2945
  }
2829
2946
  function buildAccounts(config, detected) {
@@ -2868,12 +2985,6 @@ function accountsByProvider(accounts) {
2868
2985
  return groups;
2869
2986
  }
2870
2987
 
2871
- // src/json-safe.ts
2872
- function toJsonSafe(value) {
2873
- const serialized = JSON.stringify(value);
2874
- return serialized === void 0 ? null : JSON.parse(serialized);
2875
- }
2876
-
2877
2988
  // src/peak.ts
2878
2989
  async function fetchPeak() {
2879
2990
  try {
@@ -2889,10 +3000,11 @@ async function fetchPeak() {
2889
3000
  else if (data.isWeekend === true || data.status === "weekend") state = "weekend";
2890
3001
  else if (data.isOffPeak === true || data.status === "off_peak" || data.status === "off-peak") state = "off-peak";
2891
3002
  else return null;
3003
+ const minutesUntilChange = typeof data.minutesUntilChange === "number" && Number.isFinite(data.minutesUntilChange) ? data.minutesUntilChange : null;
2892
3004
  return {
2893
3005
  state,
2894
3006
  label: state === "peak" ? "Peak" : state === "weekend" ? "Weekend" : "Off-Peak",
2895
- minutesUntilChange: typeof data.minutesUntilChange === "number" ? data.minutesUntilChange : null
3007
+ minutesUntilChange
2896
3008
  };
2897
3009
  } catch {
2898
3010
  return null;
@@ -2900,7 +3012,7 @@ async function fetchPeak() {
2900
3012
  }
2901
3013
 
2902
3014
  // src/rpc/contract.ts
2903
- import { Schema, SchemaGetter } from "effect";
3015
+ import { Schema } from "effect";
2904
3016
  import * as Rpc from "effect/unstable/rpc/Rpc";
2905
3017
  import * as RpcGroup from "effect/unstable/rpc/RpcGroup";
2906
3018
  var TOKMON_WS_PATH = "/ws";
@@ -2919,16 +3031,6 @@ var RefreshScopeSchema = Schema.Literals([
2919
3031
  "billing",
2920
3032
  "peak"
2921
3033
  ]);
2922
- var PROVIDER_IDS = [
2923
- "claude",
2924
- "codex",
2925
- "cursor",
2926
- "copilot",
2927
- "pi",
2928
- "opencode",
2929
- "antigravity",
2930
- "gemini"
2931
- ];
2932
3034
  var ProviderIdSchema = Schema.Literals(PROVIDER_IDS);
2933
3035
  var AccountSchema = Schema.Struct({
2934
3036
  id: Schema.String,
@@ -2937,10 +3039,7 @@ var AccountSchema = Schema.Struct({
2937
3039
  homeDir: Schema.String,
2938
3040
  color: Schema.optionalKey(Schema.String)
2939
3041
  });
2940
- var jsonSafePassthrough = () => Schema.Unknown.pipe(Schema.encode({
2941
- decode: SchemaGetter.transform((value) => value),
2942
- encode: SchemaGetter.transform((value) => toJsonSafe(value))
2943
- }));
3042
+ var jsonSafePassthrough = () => Schema.Unknown;
2944
3043
  var ConfigSchema = Schema.Struct({
2945
3044
  interval: Schema.Number,
2946
3045
  billingInterval: Schema.Number,
@@ -3019,7 +3118,6 @@ export {
3019
3118
  detectProviders,
3020
3119
  buildAccounts,
3021
3120
  accountsByProvider,
3022
- toJsonSafe,
3023
3121
  fetchPeak,
3024
3122
  TOKMON_WS_PATH,
3025
3123
  TOKMON_WS_METHODS,
@@ -6,6 +6,9 @@ import { mkdirSync, renameSync, writeFileSync } from "fs";
6
6
  import { join, isAbsolute } from "path";
7
7
  import { homedir } from "os";
8
8
 
9
+ // src/providers/types.ts
10
+ var PROVIDER_IDS = ["claude", "codex", "cursor", "copilot", "pi", "opencode", "antigravity", "gemini"];
11
+
9
12
  // src/config-schema.ts
10
13
  var DEFAULTS = {
11
14
  interval: 2,
@@ -23,8 +26,7 @@ var DEFAULTS = {
23
26
  };
24
27
  var LEGACY_KNOWN = ["claude", "codex", "cursor"];
25
28
  var ACCENT_COLORS = ["cyan", "magenta", "green", "yellow", "blue", "red"];
26
- var PROVIDER_ORDER = ["claude", "codex", "cursor", "copilot", "pi", "opencode", "antigravity", "gemini"];
27
- var PROVIDER_IDS = [...PROVIDER_ORDER];
29
+ var PROVIDER_ORDER = [...PROVIDER_IDS];
28
30
  var COLOR_PALETTE = [
29
31
  "cyan",
30
32
  "magenta",
@@ -188,6 +190,9 @@ function cacheDir() {
188
190
  }
189
191
  return join(envDir("XDG_CACHE_HOME") ?? join(homedir(), ".cache"), "tokmon");
190
192
  }
193
+ function snapshotCacheFile() {
194
+ return join(cacheDir(), "web-snapshot.json");
195
+ }
191
196
  async function loadConfig() {
192
197
  let raw;
193
198
  try {
@@ -246,9 +251,9 @@ function findAccount(config, id) {
246
251
  }
247
252
 
248
253
  export {
254
+ PROVIDER_IDS,
249
255
  DEFAULTS,
250
256
  ACCENT_COLORS,
251
- PROVIDER_IDS,
252
257
  COLOR_PALETTE,
253
258
  PROVIDER_META,
254
259
  getTrackedAccountRows,
@@ -262,6 +267,7 @@ export {
262
267
  envDir,
263
268
  configLocation,
264
269
  cacheDir,
270
+ snapshotCacheFile,
265
271
  loadConfig,
266
272
  saveConfig,
267
273
  saveConfigSync,
package/dist/cli.js CHANGED
@@ -14,12 +14,12 @@ process.emitWarning = ((warning, ...rest) => {
14
14
  var args = process.argv.slice(2);
15
15
  var subcommand = args[0]?.toLowerCase();
16
16
  if (subcommand === "__daemon") {
17
- const { runDaemon } = await import("./daemon-UB2PZDX6.js");
17
+ const { runDaemon } = await import("./daemon-NVWX3RRK.js");
18
18
  await runDaemon(args.slice(1), { foreground: false });
19
19
  process.exit(typeof process.exitCode === "number" ? process.exitCode : 0);
20
20
  }
21
21
  if (subcommand === "serve" || subcommand === "web") {
22
- const { runDaemon } = await import("./daemon-UB2PZDX6.js");
22
+ const { runDaemon } = await import("./daemon-NVWX3RRK.js");
23
23
  await runDaemon(args.slice(1), { foreground: true });
24
24
  process.exit(typeof process.exitCode === "number" ? process.exitCode : 0);
25
25
  }
@@ -54,7 +54,7 @@ for (let i = 0; i < args.length; i++) {
54
54
  process.exit(0);
55
55
  }
56
56
  }
57
- var { loadConfig } = await import("./config-64VVUH42.js");
57
+ var { loadConfig } = await import("./config-C6Z65JUP.js");
58
58
  var { resolveGlyphs, setGlyphs } = await import("./glyphs-NKCSZLGO.js");
59
59
  var { attachOrSpawn } = await import("./daemon-handle-ZHECQZ6Q.js");
60
60
  var config = await loadConfig();
@@ -68,5 +68,5 @@ setGlyphs(resolveGlyphs({
68
68
  }));
69
69
  var daemon = await attachOrSpawn();
70
70
  var mode = daemon.kind === "spawned" ? "connected" : "degraded";
71
- var { bootstrapInk } = await import("./bootstrap-ink-R3PFUV5N.js");
71
+ var { bootstrapInk } = await import("./bootstrap-ink-KWHXVQYS.js");
72
72
  await bootstrapInk({ interval, config, daemon, mode });
@@ -19,8 +19,9 @@ import {
19
19
  sanitizeTyped,
20
20
  saveConfig,
21
21
  saveConfigSync,
22
- slugify
23
- } from "./chunk-MVMPQJ5S.js";
22
+ slugify,
23
+ snapshotCacheFile
24
+ } from "./chunk-XQEJ4WQ5.js";
24
25
  export {
25
26
  ACCENT_COLORS,
26
27
  COLOR_PALETTE,
@@ -41,5 +42,6 @@ export {
41
42
  sanitizeTyped,
42
43
  saveConfig,
43
44
  saveConfigSync,
44
- slugify
45
+ slugify,
46
+ snapshotCacheFile
45
47
  };