tokmon 0.23.5 → 0.24.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.
package/README.md CHANGED
@@ -26,6 +26,22 @@ npm install -g tokmon
26
26
 
27
27
  Then run `tokmon`. On first launch you'll pick which providers to track; press `q` to quit any time.
28
28
 
29
+ ## CLI Data Queries
30
+
31
+ Scripts and coding agents can query the same daemon directly without opening the interactive dashboard:
32
+
33
+ ```bash
34
+ tokmon usage # current-month usage by provider/account/model
35
+ tokmon usage --period week --provider codex
36
+ tokmon usage --model opus --json # stable machine-readable schema
37
+ tokmon usage --period all --json --compact
38
+ tokmon providers --json # accounts plus local config/auth/usage paths
39
+ tokmon snapshot --refresh # complete raw daemon snapshot
40
+ tokmon config path # tokmon config file location
41
+ ```
42
+
43
+ `usage` refreshes local history by default; use `--cached` for the fastest cached answer or `--refresh` to refresh billing as well. JSON reports include `schemaVersion`, exact provider/account source IDs, per-model token and cost fields, and a `sources` collection that maps every model row to its provider home and discovered local paths. Run `tokmon usage --help` or `tokmon providers --help` for every filter and example.
44
+
29
45
  ## Providers
30
46
 
31
47
  **Usage providers** — full cost & token history (Today / Week / Month, sparkline, per-model table):
@@ -186,6 +202,10 @@ Press `s` to open.
186
202
  ```
187
203
  tokmon [options] Launch the terminal dashboard
188
204
  tokmon serve [options] Launch the web dashboard (http://127.0.0.1:4317)
205
+ tokmon usage [options] Query usage by model (human or JSON)
206
+ tokmon providers [options] Show accounts and local provider paths
207
+ tokmon snapshot [options] Print the raw daemon snapshot as JSON
208
+ tokmon config [path] Print the tokmon config file location
189
209
 
190
210
  Options:
191
211
  -i, --interval <seconds> Refresh interval in seconds (default: from config, or 2)
@@ -1,28 +1,27 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
- PROVIDERS,
4
- PROVIDER_ORDER,
5
- TOKMON_WS_METHODS,
6
- TOKMON_WS_PATH,
7
- TokmonRpcGroup,
3
+ createDaemonRpcClient
4
+ } from "./chunk-45ZP755H.js";
5
+ import {
8
6
  accountsByProvider,
9
7
  buildAccounts,
8
+ fetchPeak,
9
+ modelColor
10
+ } from "./chunk-42H7R376.js";
11
+ import {
12
+ PROVIDERS,
13
+ PROVIDER_ORDER,
10
14
  coalesceTables,
11
15
  col,
12
16
  currency,
13
17
  detectProviders,
14
- fetchPeak,
15
- modelColor,
16
18
  resolveTimezone,
17
19
  shortDate,
18
20
  systemTimezone,
19
21
  time,
20
22
  tokens,
21
23
  withTimeout
22
- } from "./chunk-IELQ5EY3.js";
23
- import {
24
- glyphs
25
- } from "./chunk-RF4GGQGM.js";
24
+ } from "./chunk-O27I2XFN.js";
26
25
  import {
27
26
  COLOR_PALETTE,
28
27
  DEFAULTS,
@@ -38,6 +37,9 @@ import {
38
37
  saveConfigSync,
39
38
  snapshotCacheFile
40
39
  } from "./chunk-5CWOJMAH.js";
40
+ import {
41
+ glyphs
42
+ } from "./chunk-RF4GGQGM.js";
41
43
 
42
44
  // src/bootstrap-ink.tsx
43
45
  import { render } from "ink";
@@ -1607,273 +1609,6 @@ function TinyRow({ provider, accounts, stats, width }) {
1607
1609
 
1608
1610
  // src/client/use-daemon.ts
1609
1611
  import { useEffect as useEffect3, useMemo, useRef as useRef2, useState as useState3 } from "react";
1610
-
1611
- // src/client/daemon-rpc-client.ts
1612
- import { Cause, Context, Duration, Effect, Exit, Fiber, Layer, ManagedRuntime, Schedule, Stream } from "effect";
1613
- import { RpcClient, RpcSerialization } from "effect/unstable/rpc";
1614
- import * as Socket from "effect/unstable/socket/Socket";
1615
- var TokmonRpcClient = class extends Context.Service()(
1616
- "tokmon/client/DaemonRpcClient/TokmonRpcClient"
1617
- ) {
1618
- };
1619
- var dynamicImport = new Function("specifier", "return import(specifier)");
1620
- function toWsUrl(baseUrl) {
1621
- const base = typeof window === "undefined" ? void 0 : window.location.origin;
1622
- const url = new URL(baseUrl, base);
1623
- if (url.protocol === "http:") url.protocol = "ws:";
1624
- else if (url.protocol === "https:") url.protocol = "wss:";
1625
- if (url.protocol !== "ws:" && url.protocol !== "wss:") {
1626
- throw new Error(`unsupported daemon RPC protocol: ${url.protocol}`);
1627
- }
1628
- url.hash = "";
1629
- url.searchParams.delete("tokmonToken");
1630
- url.searchParams.delete("wsToken");
1631
- url.pathname = TOKMON_WS_PATH;
1632
- return url.toString();
1633
- }
1634
- function shouldUseNodeTransport(transport) {
1635
- if (transport === "node") return true;
1636
- if (transport === "browser") return false;
1637
- return typeof window === "undefined";
1638
- }
1639
- async function socketLayerFor(url, transport) {
1640
- if (shouldUseNodeTransport(transport)) {
1641
- const NodeSocket = await dynamicImport("@effect/platform-node/NodeSocket");
1642
- return NodeSocket.layerWebSocket(url);
1643
- }
1644
- return Socket.layerWebSocket(url).pipe(
1645
- Layer.provide(Socket.layerWebSocketConstructorGlobal)
1646
- );
1647
- }
1648
- function retryPolicy(options) {
1649
- const baseDelay = options.reconnectBaseDelayMs ?? 250;
1650
- const policy = Schedule.exponential(Duration.millis(baseDelay), 1.5).pipe(
1651
- Schedule.either(Schedule.spaced(Duration.millis(2500)))
1652
- );
1653
- return typeof options.reconnectAttempts === "number" ? policy.pipe(Schedule.both(Schedule.recurs(options.reconnectAttempts))) : policy;
1654
- }
1655
- function createDaemonRpcClient(baseUrl, options = {}) {
1656
- const url = toWsUrl(baseUrl);
1657
- const fibers = /* @__PURE__ */ new Set();
1658
- let session = null;
1659
- let sessionPromise = null;
1660
- let closed = false;
1661
- const setConn = (state, error) => {
1662
- options.onConn?.(state, error);
1663
- };
1664
- const resetSession = (active) => {
1665
- if (active !== void 0 && active !== null && active !== session) {
1666
- void active.runtime.dispose().catch(() => {
1667
- });
1668
- return;
1669
- }
1670
- const dead = active ?? session;
1671
- session = null;
1672
- sessionPromise = null;
1673
- if (dead) void dead.runtime.dispose().catch(() => {
1674
- });
1675
- };
1676
- const makeProtocolLayer = async () => {
1677
- const socketLayer = await socketLayerFor(url, options.transport);
1678
- const connectionHooksLayer = Layer.succeed(
1679
- RpcClient.ConnectionHooks,
1680
- RpcClient.ConnectionHooks.of({
1681
- onConnect: Effect.sync(() => {
1682
- setConn("live");
1683
- }),
1684
- onDisconnect: Effect.sync(() => {
1685
- if (!closed) {
1686
- setConn("reconnecting");
1687
- }
1688
- })
1689
- })
1690
- );
1691
- return Layer.effect(
1692
- RpcClient.Protocol,
1693
- RpcClient.makeProtocolSocket({
1694
- retryPolicy: retryPolicy(options),
1695
- retryTransientErrors: true
1696
- })
1697
- ).pipe(
1698
- Layer.provide(Layer.mergeAll(socketLayer, RpcSerialization.layerJson, connectionHooksLayer))
1699
- );
1700
- };
1701
- const ensureSession = async () => {
1702
- if (closed) throw new Error("daemon RPC client is closed");
1703
- if (session) return session;
1704
- if (sessionPromise) return sessionPromise;
1705
- setConn("connecting");
1706
- sessionPromise = (async () => {
1707
- let runtime;
1708
- try {
1709
- const protocolLayer = await makeProtocolLayer();
1710
- const clientLayer = Layer.effect(
1711
- TokmonRpcClient,
1712
- RpcClient.make(TokmonRpcGroup)
1713
- ).pipe(
1714
- Layer.provide(protocolLayer)
1715
- );
1716
- runtime = ManagedRuntime.make(clientLayer);
1717
- await runtime.runPromise(TokmonRpcClient.asEffect());
1718
- if (closed) {
1719
- await runtime.dispose();
1720
- throw new Error("daemon RPC client is closed");
1721
- }
1722
- session = { runtime };
1723
- return session;
1724
- } catch (error) {
1725
- sessionPromise = null;
1726
- await runtime?.dispose().catch(() => {
1727
- });
1728
- if (!closed) setConn("error", error);
1729
- throw error;
1730
- }
1731
- })();
1732
- return sessionPromise;
1733
- };
1734
- const run = async (effectFor) => {
1735
- const active = await ensureSession();
1736
- try {
1737
- return await active.runtime.runPromise(
1738
- TokmonRpcClient.use((client) => effectFor(client))
1739
- );
1740
- } catch (error) {
1741
- if (!closed) {
1742
- resetSession(active);
1743
- setConn("error", error);
1744
- }
1745
- throw error;
1746
- }
1747
- };
1748
- const subscribe = (streamFor, onValue, staleAfterFor) => {
1749
- if (closed) return () => {
1750
- };
1751
- let fiber = null;
1752
- let unsubscribed = false;
1753
- let retryTimer = null;
1754
- let watchdogTimer = null;
1755
- let lastValueAt = 0;
1756
- let staleAfterMs = Number.POSITIVE_INFINITY;
1757
- let watchdogRestart = false;
1758
- const stopFiber = () => {
1759
- if (!fiber) return;
1760
- const current = fiber;
1761
- fiber = null;
1762
- fibers.delete(current);
1763
- void (session?.runtime.runPromise(Fiber.interrupt(current)) ?? Effect.runPromise(Fiber.interrupt(current))).catch(() => {
1764
- });
1765
- };
1766
- const scheduleRetry = () => {
1767
- if (closed || unsubscribed || retryTimer) return;
1768
- retryTimer = setTimeout(() => {
1769
- retryTimer = null;
1770
- start();
1771
- }, options.reconnectBaseDelayMs ?? 250);
1772
- retryTimer.unref?.();
1773
- };
1774
- if (staleAfterFor) {
1775
- const checkEveryMs = Math.min(5e3, Math.max(10, (options.snapshotStaleFloorMs ?? 9e4) / 2));
1776
- watchdogTimer = setInterval(() => {
1777
- if (!fiber || lastValueAt === 0 || Date.now() - lastValueAt <= staleAfterMs) return;
1778
- watchdogRestart = true;
1779
- lastValueAt = Date.now();
1780
- setConn("reconnecting");
1781
- stopFiber();
1782
- }, checkEveryMs);
1783
- watchdogTimer.unref?.();
1784
- }
1785
- const start = () => {
1786
- void (async () => {
1787
- try {
1788
- const active = await ensureSession();
1789
- if (closed || unsubscribed) return;
1790
- lastValueAt = Date.now();
1791
- staleAfterMs = options.snapshotStaleFloorMs ?? 9e4;
1792
- const currentFiber = active.runtime.runFork(
1793
- TokmonRpcClient.use(
1794
- (client) => streamFor(client).pipe(
1795
- Stream.runForEach(
1796
- (value) => Effect.sync(() => {
1797
- lastValueAt = Date.now();
1798
- if (staleAfterFor) staleAfterMs = staleAfterFor(value);
1799
- try {
1800
- onValue(value);
1801
- } catch {
1802
- }
1803
- })
1804
- )
1805
- )
1806
- )
1807
- );
1808
- fiber = currentFiber;
1809
- fibers.add(currentFiber);
1810
- currentFiber.addObserver((exit) => {
1811
- fibers.delete(currentFiber);
1812
- if (fiber === currentFiber) fiber = null;
1813
- if (closed || unsubscribed) return;
1814
- const stale = watchdogRestart;
1815
- watchdogRestart = false;
1816
- resetSession(active);
1817
- if (stale || Exit.isSuccess(exit)) setConn("reconnecting");
1818
- else setConn("error", Cause.squash(exit.cause));
1819
- scheduleRetry();
1820
- });
1821
- } catch (error) {
1822
- if (!closed && !unsubscribed) {
1823
- resetSession();
1824
- setConn("error", error);
1825
- scheduleRetry();
1826
- }
1827
- }
1828
- })();
1829
- };
1830
- start();
1831
- return () => {
1832
- unsubscribed = true;
1833
- if (retryTimer) {
1834
- clearTimeout(retryTimer);
1835
- retryTimer = null;
1836
- }
1837
- if (watchdogTimer) {
1838
- clearInterval(watchdogTimer);
1839
- watchdogTimer = null;
1840
- }
1841
- stopFiber();
1842
- };
1843
- };
1844
- return {
1845
- getConfig: () => run((client) => client[TOKMON_WS_METHODS.getConfig]({})).then((state) => state),
1846
- setConfig: (config) => run((client) => client[TOKMON_WS_METHODS.setConfig](config)).then((state) => state),
1847
- refresh: (scope = "all") => run((client) => client[TOKMON_WS_METHODS.refresh]({ scope })),
1848
- browseFs: (path) => run((client) => client[TOKMON_WS_METHODS.browseFs]({ path })),
1849
- subscribeSnapshot: (onSnapshot) => subscribe(
1850
- (client) => client[TOKMON_WS_METHODS.snapshot]({}).pipe(Stream.map((value) => value)),
1851
- onSnapshot,
1852
- (snapshot) => Math.max(options.snapshotStaleFloorMs ?? 9e4, snapshot.intervalMs * 3)
1853
- ),
1854
- subscribeConfig: (onConfig) => subscribe((client) => client[TOKMON_WS_METHODS.config]({}).pipe(Stream.map((value) => value)), onConfig),
1855
- async close() {
1856
- if (closed) return;
1857
- closed = true;
1858
- setConn("closed");
1859
- const active = [...fibers];
1860
- fibers.clear();
1861
- const activeSession = session ?? await sessionPromise?.catch(() => null) ?? null;
1862
- await Promise.all(active.map(
1863
- (fiber) => (activeSession?.runtime.runPromise(Fiber.interrupt(fiber)) ?? Effect.runPromise(Fiber.interrupt(fiber))).catch(() => {
1864
- })
1865
- ));
1866
- session = null;
1867
- sessionPromise = null;
1868
- if (activeSession) {
1869
- await activeSession.runtime.dispose().catch(() => {
1870
- });
1871
- }
1872
- }
1873
- };
1874
- }
1875
-
1876
- // src/client/use-daemon.ts
1877
1612
  function useDaemon(baseUrl) {
1878
1613
  const [snapshot, setSnapshot] = useState3(null);
1879
1614
  const [conn, setConn] = useState3("connecting");
@@ -3654,7 +3389,7 @@ function App({ interval: cliInterval, initialConfig, baseUrl = null, mode = "deg
3654
3389
  if (webStartingRef.current) return;
3655
3390
  webStartingRef.current = true;
3656
3391
  try {
3657
- const { startWebServer } = await import("./server-XUCQWX6N.js");
3392
+ const { startWebServer } = await import("./server-X63FGHIZ.js");
3658
3393
  const ctrl = await startWebServer({ config: cfg, log: false });
3659
3394
  webRef.current = ctrl;
3660
3395
  openUrl(ctrl.url);
@@ -0,0 +1,298 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ PROVIDERS,
4
+ PROVIDER_ORDER,
5
+ identityFromIdToken,
6
+ readClaudeIdentity,
7
+ readJson
8
+ } from "./chunk-O27I2XFN.js";
9
+ import {
10
+ expandHome,
11
+ slugify
12
+ } from "./chunk-5CWOJMAH.js";
13
+
14
+ // src/accounts.ts
15
+ import { existsSync, readdirSync, readFileSync as readFileSync2, statSync } from "fs";
16
+ import { homedir } from "os";
17
+ import { basename, join as join2, resolve } from "path";
18
+
19
+ // src/providers/codex/identity.ts
20
+ import { readFileSync } from "fs";
21
+ import { join } from "path";
22
+ function codexAuthPaths(homeDir) {
23
+ return [join(homeDir, ".codex", "auth.json"), join(homeDir, "auth.json")];
24
+ }
25
+ function readCodexIdentity(homeDir) {
26
+ for (const path of codexAuthPaths(homeDir)) {
27
+ try {
28
+ const parsed = JSON.parse(readFileSync(path, "utf-8"));
29
+ const { email, displayName, payload } = identityFromIdToken(parsed?.tokens?.id_token);
30
+ if (!payload) continue;
31
+ return { email, displayName };
32
+ } catch {
33
+ }
34
+ }
35
+ return {};
36
+ }
37
+
38
+ // src/accounts.ts
39
+ function accountKey(providerId, homeDir) {
40
+ return `${providerId}:${homeDir ? resolve(expandHome(homeDir)) : homedir()}`;
41
+ }
42
+ function uniqueId(base, used) {
43
+ let id = slugify(base) || "account";
44
+ if (!used.has(id)) {
45
+ used.add(id);
46
+ return id;
47
+ }
48
+ for (let i = 2; i < 1e3; i++) {
49
+ const next = `${id}_${i}`;
50
+ if (!used.has(next)) {
51
+ used.add(next);
52
+ return next;
53
+ }
54
+ }
55
+ id = `${id}_${Date.now()}`;
56
+ used.add(id);
57
+ return id;
58
+ }
59
+ function hasClaudeState(homeDir) {
60
+ return existsSync(join2(homeDir, ".claude.json")) || existsSync(join2(homeDir, ".claude", ".credentials.json")) || existsSync(join2(homeDir, ".claude", "projects")) || existsSync(join2(homeDir, ".config", "claude", ".credentials.json")) || existsSync(join2(homeDir, ".config", "claude", "projects"));
61
+ }
62
+ function candidateAlternateHomes(prefix) {
63
+ const home = homedir();
64
+ let entries;
65
+ try {
66
+ entries = readdirSync(home);
67
+ } catch {
68
+ return [];
69
+ }
70
+ const out = [];
71
+ const pattern = new RegExp(`^\\.${prefix}[_-]`);
72
+ for (const name of entries) {
73
+ if (!pattern.test(name)) continue;
74
+ const path = join2(home, name);
75
+ try {
76
+ if (!statSync(path).isDirectory()) continue;
77
+ out.push(path);
78
+ } catch {
79
+ }
80
+ }
81
+ return out.sort();
82
+ }
83
+ function labelForClaudeHome(homeDir) {
84
+ const identity = readClaudeIdentity(homeDir);
85
+ if (identity.email) return `Claude ${identity.email}`;
86
+ if (identity.displayName) return `Claude ${identity.displayName}`;
87
+ const raw = basename(homeDir).replace(/^\.claude[_-]?/, "").replace(/[_-]+/g, " ").trim();
88
+ return raw ? `Claude ${raw}` : "Claude";
89
+ }
90
+ function hasCodexAuth(homeDir) {
91
+ for (const path of codexAuthPaths(homeDir)) {
92
+ try {
93
+ const parsed = JSON.parse(readFileSync2(path, "utf-8"));
94
+ const accessToken = parsed?.tokens?.access_token;
95
+ if (typeof accessToken === "string" && accessToken.trim()) return true;
96
+ } catch {
97
+ }
98
+ }
99
+ return false;
100
+ }
101
+ function labelForCodexHome(homeDir) {
102
+ const identity = readCodexIdentity(homeDir);
103
+ if (identity.email) return `Codex ${identity.email}`;
104
+ if (identity.displayName) return `Codex ${identity.displayName}`;
105
+ const raw = basename(homeDir).replace(/^\.codex[_-]?/, "").replace(/[_-]+/g, " ").trim();
106
+ return raw ? `Codex ${raw}` : "Codex";
107
+ }
108
+ function discoverClaudeAccounts(usedIds) {
109
+ const provider = PROVIDERS.claude;
110
+ const out = [];
111
+ for (const homeDir of candidateAlternateHomes("claude")) {
112
+ if (!hasClaudeState(homeDir)) continue;
113
+ const suffix = basename(homeDir).replace(/^\.claude[_-]?/, "") || basename(homeDir);
114
+ out.push({
115
+ id: uniqueId(`claude_${suffix}`, usedIds),
116
+ providerId: "claude",
117
+ name: labelForClaudeHome(homeDir),
118
+ color: provider.color,
119
+ homeDir
120
+ });
121
+ }
122
+ return out;
123
+ }
124
+ function discoverCodexAccounts(usedIds) {
125
+ const provider = PROVIDERS.codex;
126
+ const out = [];
127
+ for (const homeDir of candidateAlternateHomes("codex")) {
128
+ if (!hasCodexAuth(homeDir)) continue;
129
+ const suffix = basename(homeDir).replace(/^\.codex[_-]?/, "") || basename(homeDir);
130
+ out.push({
131
+ id: uniqueId(`codex_${suffix}`, usedIds),
132
+ providerId: "codex",
133
+ name: labelForCodexHome(homeDir),
134
+ color: provider.color,
135
+ homeDir
136
+ });
137
+ }
138
+ return out;
139
+ }
140
+ function discoverProviderAccounts(providerId, usedIds) {
141
+ if (providerId === "claude") return discoverClaudeAccounts(usedIds);
142
+ if (providerId === "codex") return discoverCodexAccounts(usedIds);
143
+ return [];
144
+ }
145
+ function buildAccounts(config, detected) {
146
+ const out = [];
147
+ const usedIds = new Set(config.accounts.map((a) => a.id));
148
+ const seenKeys = /* @__PURE__ */ new Set();
149
+ const add = (account) => {
150
+ const key = accountKey(account.providerId, account.homeDir);
151
+ if (seenKeys.has(key)) return;
152
+ seenKeys.add(key);
153
+ out.push(account);
154
+ };
155
+ for (const pid of PROVIDER_ORDER) {
156
+ if (config.disabledProviders.includes(pid)) continue;
157
+ const provider = PROVIDERS[pid];
158
+ const configured = config.accounts.filter((a) => a.providerId === pid);
159
+ for (const a of configured) {
160
+ add({
161
+ id: a.id,
162
+ providerId: pid,
163
+ name: a.name,
164
+ color: a.color || provider.color,
165
+ homeDir: a.homeDir && a.homeDir !== "~" ? expandHome(a.homeDir) : void 0
166
+ });
167
+ }
168
+ const discovered = discoverProviderAccounts(pid, usedIds);
169
+ if (detected.includes(pid)) {
170
+ add({ id: pid, providerId: pid, name: provider.name, color: provider.color, homeDir: void 0 });
171
+ }
172
+ for (const account of discovered) {
173
+ add(account);
174
+ }
175
+ }
176
+ return out;
177
+ }
178
+ function accountsByProvider(accounts) {
179
+ const groups = [];
180
+ for (const pid of PROVIDER_ORDER) {
181
+ const list = accounts.filter((a) => a.providerId === pid);
182
+ if (list.length > 0) groups.push({ provider: pid, accounts: list });
183
+ }
184
+ return groups;
185
+ }
186
+
187
+ // src/shared/colors.ts
188
+ var FALLBACK_HEX = "#8d9090";
189
+ var NAMED_HEX = {
190
+ green: "#6caa71",
191
+ greenBright: "#79be7e",
192
+ cyan: "#7ccbcd",
193
+ cyanBright: "#84dde0",
194
+ blue: "#6d96b4",
195
+ blueBright: "#67b5ed",
196
+ magenta: "#bd7bcd",
197
+ magentaBright: "#d389e5",
198
+ yellow: "#c4ac62",
199
+ yellowBright: "#d9c074",
200
+ red: "#b45648",
201
+ redBright: "#cf6a5a",
202
+ white: "#dee5eb",
203
+ whiteBright: "#f3f5f5",
204
+ gray: FALLBACK_HEX,
205
+ grey: FALLBACK_HEX
206
+ };
207
+ var PROVIDER_HEX = {
208
+ claude: NAMED_HEX.green,
209
+ codex: NAMED_HEX.cyan,
210
+ cursor: NAMED_HEX.magenta,
211
+ copilot: NAMED_HEX.white,
212
+ pi: NAMED_HEX.blue,
213
+ opencode: NAMED_HEX.yellow,
214
+ antigravity: NAMED_HEX.red,
215
+ gemini: NAMED_HEX.greenBright,
216
+ grok: NAMED_HEX.yellowBright
217
+ };
218
+ function namedHex(name) {
219
+ if (!name) return FALLBACK_HEX;
220
+ if (name.startsWith("#")) return name;
221
+ return NAMED_HEX[name] ?? FALLBACK_HEX;
222
+ }
223
+ function colorHex(accountColor, providerColorName) {
224
+ if (accountColor) {
225
+ if (accountColor.startsWith("#")) return accountColor;
226
+ const named = NAMED_HEX[accountColor];
227
+ if (named) return named;
228
+ }
229
+ return namedHex(providerColorName);
230
+ }
231
+ var MODEL_PALETTE = [
232
+ "#00d7ff",
233
+ "#00d787",
234
+ "#e6b450",
235
+ "#d75f87",
236
+ "#5f87ff",
237
+ "#af87ff",
238
+ "#5fd7a7",
239
+ "#ff8787",
240
+ "#d7af5f",
241
+ "#87d7ff",
242
+ "#d787d7",
243
+ "#9ee493",
244
+ "#ffb454",
245
+ "#7aa2f7",
246
+ "#bb9af7"
247
+ ];
248
+ var modelColorCache = /* @__PURE__ */ new Map();
249
+ function modelColor(name) {
250
+ const cached = modelColorCache.get(name);
251
+ if (cached) return cached;
252
+ let hash = 0;
253
+ for (let i = 0; i < name.length; i++) hash = hash * 31 + name.charCodeAt(i) >>> 0;
254
+ const color = MODEL_PALETTE[hash % MODEL_PALETTE.length];
255
+ modelColorCache.set(name, color);
256
+ return color;
257
+ }
258
+ var TOKEN_BUCKET = {
259
+ input: NAMED_HEX.blue,
260
+ output: NAMED_HEX.green,
261
+ cacheCreate: NAMED_HEX.yellow,
262
+ cacheRead: NAMED_HEX.cyan
263
+ };
264
+
265
+ // src/peak.ts
266
+ async function fetchPeak() {
267
+ try {
268
+ const res = await fetch("https://promoclock.co/api/status", {
269
+ headers: { "Accept": "application/json", "User-Agent": "tokmon" },
270
+ signal: AbortSignal.timeout(3e3)
271
+ });
272
+ if (!res.ok) return null;
273
+ const data = await readJson(res);
274
+ if (!data) return null;
275
+ let state;
276
+ if (data.isPeak === true || data.status === "peak") state = "peak";
277
+ else if (data.isWeekend === true || data.status === "weekend") state = "weekend";
278
+ else if (data.isOffPeak === true || data.status === "off_peak" || data.status === "off-peak") state = "off-peak";
279
+ else return null;
280
+ const minutesUntilChange = typeof data.minutesUntilChange === "number" && Number.isFinite(data.minutesUntilChange) ? data.minutesUntilChange : null;
281
+ return {
282
+ state,
283
+ label: state === "peak" ? "Peak" : state === "weekend" ? "Weekend" : "Off-Peak",
284
+ minutesUntilChange
285
+ };
286
+ } catch {
287
+ return null;
288
+ }
289
+ }
290
+
291
+ export {
292
+ buildAccounts,
293
+ accountsByProvider,
294
+ namedHex,
295
+ colorHex,
296
+ modelColor,
297
+ fetchPeak
298
+ };