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 +20 -0
- package/dist/{bootstrap-ink-WA3ENW2M.js → bootstrap-ink-HL2BYNW3.js} +14 -279
- package/dist/chunk-42H7R376.js +298 -0
- package/dist/chunk-45ZP755H.js +275 -0
- package/dist/{chunk-IELQ5EY3.js → chunk-O27I2XFN.js} +277 -539
- package/dist/{chunk-MJWFCDBB.js → chunk-REO4O5VE.js} +7 -5
- package/dist/{daemon-handle-Y2N2NA6D.js → chunk-W2WGBXZG.js} +1 -1
- package/dist/cli-command-TLGIGF63.js +526 -0
- package/dist/cli.js +29 -4
- package/dist/{daemon-PVYTXJMS.js → daemon-F4EGY77J.js} +5 -4
- package/dist/daemon-handle-NLCLYWFU.js +10 -0
- package/dist/{server-XUCQWX6N.js → server-X63FGHIZ.js} +3 -2
- package/package.json +1 -1
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
|
-
|
|
4
|
-
|
|
5
|
-
|
|
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-
|
|
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-
|
|
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
|
+
};
|