tokmon 0.23.5 → 0.25.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 +25 -3
- package/dist/{bootstrap-ink-WA3ENW2M.js → bootstrap-ink-O55MPGXJ.js} +52 -314
- package/dist/chunk-45YYP2FJ.js +299 -0
- package/dist/{daemon-handle-Y2N2NA6D.js → chunk-AMD4PXDG.js} +2 -2
- package/dist/{chunk-5CWOJMAH.js → chunk-E2YXYU73.js} +2 -0
- package/dist/{chunk-IELQ5EY3.js → chunk-INBZLHJQ.js} +306 -549
- package/dist/{chunk-MJWFCDBB.js → chunk-IOIAXKW2.js} +38 -16
- package/dist/{chunk-3TJVFKXV.js → chunk-QM5E5RJZ.js} +1 -1
- package/dist/chunk-QUGH2XA6.js +275 -0
- package/dist/cli-command-YQ5T4R73.js +526 -0
- package/dist/cli.js +30 -5
- package/dist/{config-QC5QSP3G.js → config-2PXUENQL.js} +1 -1
- package/dist/{daemon-PVYTXJMS.js → daemon-QBOXJFRA.js} +7 -6
- package/dist/daemon-handle-2GVZT55B.js +10 -0
- package/dist/server-23ERUOJ7.js +11 -0
- package/dist/web/assets/{Area-DCES17aZ.js → Area-DbVPPS7M.js} +1 -1
- package/dist/web/assets/{analytics-DDW6zcnj.js → analytics-DS5p9Duq.js} +2 -2
- package/dist/web/assets/{breakdown-D5LPPtqh.js → breakdown-zYF0TXN5.js} +1 -1
- package/dist/web/assets/{chart-bHY7MQV0.js → chart-BZHliIXk.js} +1 -1
- package/dist/web/assets/{explore-DdWB-Pu7.js → explore-DYv8bb2r.js} +1 -1
- package/dist/web/assets/{index-C9jxb2o3.js → index-BO8m4dq1.js} +24 -24
- package/dist/web/assets/index-NnKaHxPO.css +1 -0
- package/dist/web/assets/{models-o1wJnG6w.js → models-P3OwakgO.js} +2 -2
- package/dist/web/assets/overview-5WswYjWK.js +2 -0
- package/dist/web/assets/{panel-DBnK1oy0.js → panel-CpwRWsNg.js} +1 -1
- package/dist/web/assets/{primitives-CBmteetD.js → primitives-Do0zWLZ4.js} +1 -1
- package/dist/web/assets/settings-sheet-uD6-A3tV.js +1 -0
- package/dist/web/assets/{share-sheet-DUee3FlK.js → share-sheet-CimegH2K.js} +2 -2
- package/dist/web/assets/{timeline-BxHwOJA_.js → timeline-Ch94Yv4z.js} +1 -1
- package/dist/web/assets/{use-dialog-trap-30EqxYF-.js → use-dialog-trap-LxEFLutK.js} +1 -1
- package/dist/web/index.html +2 -2
- package/package.json +1 -1
- package/dist/server-XUCQWX6N.js +0 -10
- package/dist/web/assets/index-m0KfWnN2.css +0 -1
- package/dist/web/assets/overview-wq2uAEk4.js +0 -2
- package/dist/web/assets/settings-sheet-CZ6GEPl3.js +0 -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):
|
|
@@ -60,7 +76,7 @@ A responsive grid of provider cards (or one card at a time — see **Dashboard l
|
|
|
60
76
|
- **Today / This Week / This Month** — cost and token summaries
|
|
61
77
|
- **Burn rate** — current $/hr
|
|
62
78
|
- **Cache saved** — what caching has saved you
|
|
63
|
-
- **Rate limits** — live utilization bars with reset countdowns
|
|
79
|
+
- **Rate limits** — live utilization bars with reset countdowns or exact reset dates
|
|
64
80
|
- **Sparkline** — recent daily activity
|
|
65
81
|
|
|
66
82
|
The grid reflows to fit your terminal — more columns when it's wide, compacting cards when it's short. With more providers than fit on screen, it splits into **pages**; **scroll** (mouse wheel) to move between them (or `↑`/`↓` / `[` `]`). When you track more than one account, a focus strip lets you view **All** together or zoom into a single account.
|
|
@@ -89,7 +105,7 @@ tokmon serve --port 8080
|
|
|
89
105
|
tokmon serve --no-open # don't auto-open the browser
|
|
90
106
|
```
|
|
91
107
|
|
|
92
|
-
It binds to `127.0.0.1` by default and reads the same local daemon state — nothing leaves your machine. The browser connects directly over a same-origin WebSocket; dashboard URLs have no token or login step. Optional LAN access can be enabled in settings, with an explicit unsafe-access warning, and takes effect after the daemon restarts. The dashboard renders instantly from a cached snapshot, then streams live updates and goes idle when no tab is open. Filter by provider, model, account, and period, flip between dark and light, and export any panel — or a summary card — as a PNG with the **Share** button.
|
|
108
|
+
It binds to `127.0.0.1` by default and reads the same local daemon state — nothing leaves your machine. The browser connects directly over a same-origin WebSocket; dashboard URLs have no token or login step. Optional LAN access can be enabled in settings, with an explicit unsafe-access warning, and takes effect after the daemon restarts. The dashboard renders instantly from a cached snapshot, then streams live updates and goes idle when no tab is open. Press `R` or use the visible **Refresh** control to update it. Filter by provider, model, account, and period, flip between dark and light, and export any panel — or a summary card — as a PNG with the **Share** button.
|
|
93
109
|
|
|
94
110
|
### Overview
|
|
95
111
|
|
|
@@ -176,6 +192,8 @@ Press `s` to open.
|
|
|
176
192
|
- **Dashboard layout** — `grid` (all providers at once) or `single` (one at a time)
|
|
177
193
|
- **Default focus** — start on `all`, or remember your `last` focused account
|
|
178
194
|
- **ASCII glyphs** — `auto` (detect), `on` (force ASCII), or `off` (force Unicode)
|
|
195
|
+
- **Network access** — loopback-only by default; optional LAN access is explicitly marked unsafe
|
|
196
|
+
- **Reset times** — show time remaining or the exact reset date/time in the configured timezone
|
|
179
197
|
|
|
180
198
|
**Providers** — toggle each provider on or off.
|
|
181
199
|
|
|
@@ -186,6 +204,10 @@ Press `s` to open.
|
|
|
186
204
|
```
|
|
187
205
|
tokmon [options] Launch the terminal dashboard
|
|
188
206
|
tokmon serve [options] Launch the web dashboard (http://127.0.0.1:4317)
|
|
207
|
+
tokmon usage [options] Query usage by model (human or JSON)
|
|
208
|
+
tokmon providers [options] Show accounts and local provider paths
|
|
209
|
+
tokmon snapshot [options] Print the raw daemon snapshot as JSON
|
|
210
|
+
tokmon config [path] Print the tokmon config file location
|
|
189
211
|
|
|
190
212
|
Options:
|
|
191
213
|
-i, --interval <seconds> Refresh interval in seconds (default: from config, or 2)
|
|
@@ -233,7 +255,7 @@ tokmon runs a small local **daemon** that does all the data collection. The term
|
|
|
233
255
|
- 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.
|
|
234
256
|
|
|
235
257
|
**Limits & billing**
|
|
236
|
-
- Rate limits and remaining spend/quota come from each provider's own official API
|
|
258
|
+
- Rate limits and remaining spend/quota come from each provider's own official API. Tokmon refreshes them on the configured billing interval; terminal focus and extra dashboard viewers reuse fresh data instead of issuing extra provider requests.
|
|
237
259
|
|
|
238
260
|
**Responsiveness**
|
|
239
261
|
- Dashboard summaries and table history load independently and refresh on separate intervals, so the UI stays responsive even on large histories.
|
|
@@ -1,28 +1,29 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
TOKMON_WS_PATH,
|
|
7
|
-
TokmonRpcGroup,
|
|
3
|
+
createDaemonRpcClient
|
|
4
|
+
} from "./chunk-QUGH2XA6.js";
|
|
5
|
+
import {
|
|
8
6
|
accountsByProvider,
|
|
9
7
|
buildAccounts,
|
|
8
|
+
fetchPeak,
|
|
9
|
+
modelColor
|
|
10
|
+
} from "./chunk-45YYP2FJ.js";
|
|
11
|
+
import {
|
|
12
|
+
PROVIDERS,
|
|
13
|
+
PROVIDER_ORDER,
|
|
10
14
|
coalesceTables,
|
|
11
15
|
col,
|
|
12
16
|
currency,
|
|
13
17
|
detectProviders,
|
|
14
|
-
|
|
15
|
-
|
|
18
|
+
formatResetAt,
|
|
19
|
+
resetAt,
|
|
16
20
|
resolveTimezone,
|
|
17
21
|
shortDate,
|
|
18
22
|
systemTimezone,
|
|
19
23
|
time,
|
|
20
24
|
tokens,
|
|
21
25
|
withTimeout
|
|
22
|
-
} from "./chunk-
|
|
23
|
-
import {
|
|
24
|
-
glyphs
|
|
25
|
-
} from "./chunk-RF4GGQGM.js";
|
|
26
|
+
} from "./chunk-INBZLHJQ.js";
|
|
26
27
|
import {
|
|
27
28
|
COLOR_PALETTE,
|
|
28
29
|
DEFAULTS,
|
|
@@ -37,7 +38,10 @@ import {
|
|
|
37
38
|
sanitizeTyped,
|
|
38
39
|
saveConfigSync,
|
|
39
40
|
snapshotCacheFile
|
|
40
|
-
} from "./chunk-
|
|
41
|
+
} from "./chunk-E2YXYU73.js";
|
|
42
|
+
import {
|
|
43
|
+
glyphs
|
|
44
|
+
} from "./chunk-RF4GGQGM.js";
|
|
41
45
|
|
|
42
46
|
// src/bootstrap-ink.tsx
|
|
43
47
|
import { render } from "ink";
|
|
@@ -159,27 +163,22 @@ var TabBar = memo(function TabBar2({ tabs, active, onSelect }) {
|
|
|
159
163
|
" "
|
|
160
164
|
] }) }, t)) });
|
|
161
165
|
});
|
|
162
|
-
var PeakBadge = memo(function PeakBadge2({ peak }) {
|
|
166
|
+
var PeakBadge = memo(function PeakBadge2({ peak, resetDisplay = "relative", tz }) {
|
|
163
167
|
const color = peak.state === "peak" ? "red" : "green";
|
|
168
|
+
const changesAt = peak.changesAt ?? (peak.minutesUntilChange !== null ? new Date(Date.now() + peak.minutesUntilChange * 6e4).toISOString() : null);
|
|
164
169
|
return /* @__PURE__ */ jsxs(Box, { children: [
|
|
165
170
|
/* @__PURE__ */ jsxs(Text, { color, children: [
|
|
166
171
|
glyphs().dot,
|
|
167
172
|
" "
|
|
168
173
|
] }),
|
|
169
174
|
/* @__PURE__ */ jsx(Text, { bold: true, color, children: peak.label }),
|
|
170
|
-
|
|
175
|
+
changesAt && /* @__PURE__ */ jsxs(Text, { dimColor: true, children: [
|
|
171
176
|
" (",
|
|
172
|
-
|
|
177
|
+
formatResetAt(changesAt, resetDisplay, Date.now(), tz),
|
|
173
178
|
")"
|
|
174
179
|
] })
|
|
175
180
|
] });
|
|
176
181
|
});
|
|
177
|
-
function fmtMinutes(mins) {
|
|
178
|
-
if (mins < 60) return `${mins}m`;
|
|
179
|
-
const h = Math.floor(mins / 60);
|
|
180
|
-
const m = mins % 60;
|
|
181
|
-
return m === 0 ? `${h}h` : `${h}h ${m}m`;
|
|
182
|
-
}
|
|
183
182
|
function currencySymbol(cur) {
|
|
184
183
|
return cur === "EUR" ? glyphs().eur : cur === "GBP" ? glyphs().gbp : "$";
|
|
185
184
|
}
|
|
@@ -290,7 +289,7 @@ function computeDashLayout(groups, stats, cols, budget, focusId, layoutPref) {
|
|
|
290
289
|
}
|
|
291
290
|
return chooseLayout(content, budget, groups.length, single, cols, heights);
|
|
292
291
|
}
|
|
293
|
-
var DashboardView = memo2(function DashboardView2({ groups, stats, cols, budget, focusId, layout, page = 0, privacyMode = false }) {
|
|
292
|
+
var DashboardView = memo2(function DashboardView2({ groups, stats, cols, budget, focusId, layout, page = 0, privacyMode = false, resetDisplay = "relative", tz }) {
|
|
294
293
|
if (groups.length === 0) {
|
|
295
294
|
return /* @__PURE__ */ jsxs2(Text2, { dimColor: true, children: [
|
|
296
295
|
"No providers enabled ",
|
|
@@ -305,7 +304,7 @@ var DashboardView = memo2(function DashboardView2({ groups, stats, cols, budget,
|
|
|
305
304
|
const pg = pageCount > 1 ? (page % pageCount + pageCount) % pageCount : 0;
|
|
306
305
|
const visible = pageCount > 1 ? groups.slice(pg * cardsPerPage, pg * cardsPerPage + cardsPerPage) : groups;
|
|
307
306
|
return /* @__PURE__ */ jsxs2(Box2, { height: budget, flexDirection: "column", overflow: "hidden", children: [
|
|
308
|
-
/* @__PURE__ */ jsx2(Box2, { width: content, flexWrap: "wrap", columnGap: GAP, rowGap: 1, alignItems: "flex-start", children: visible.map((g) => /* @__PURE__ */ jsx2(Box2, { flexShrink: 0, children: /* @__PURE__ */ jsx2(ProviderCard, { provider: g.provider, accounts: g.accounts, stats, width: cardW, variant, privacyMode }) }, g.provider)) }),
|
|
307
|
+
/* @__PURE__ */ jsx2(Box2, { width: content, flexWrap: "wrap", columnGap: GAP, rowGap: 1, alignItems: "flex-start", children: visible.map((g) => /* @__PURE__ */ jsx2(Box2, { flexShrink: 0, children: /* @__PURE__ */ jsx2(ProviderCard, { provider: g.provider, accounts: g.accounts, stats, width: cardW, variant, privacyMode, resetDisplay, tz }) }, g.provider)) }),
|
|
309
308
|
pageCount > 1 && /* @__PURE__ */ jsxs2(Text2, { dimColor: true, children: [
|
|
310
309
|
" ",
|
|
311
310
|
glyphs().middot,
|
|
@@ -321,7 +320,7 @@ var DashboardView = memo2(function DashboardView2({ groups, stats, cols, budget,
|
|
|
321
320
|
] })
|
|
322
321
|
] });
|
|
323
322
|
});
|
|
324
|
-
function ProviderCard({ provider, accounts, stats, width, variant, privacyMode = false }) {
|
|
323
|
+
function ProviderCard({ provider, accounts, stats, width, variant, privacyMode = false, resetDisplay, tz }) {
|
|
325
324
|
const meta = PROVIDERS[provider];
|
|
326
325
|
const items = accounts.map((a) => ({ account: a, s: stats.get(a.id) }));
|
|
327
326
|
const dashboards = items.map((i) => i.s?.dashboard).filter((d) => !!d);
|
|
@@ -357,7 +356,7 @@ function ProviderCard({ provider, accounts, stats, width, variant, privacyMode =
|
|
|
357
356
|
] })),
|
|
358
357
|
meta.hasBilling && showBars && /* @__PURE__ */ jsxs2(Fragment, { children: [
|
|
359
358
|
meta.hasUsage && /* @__PURE__ */ jsx2(Rule, { inner }),
|
|
360
|
-
/* @__PURE__ */ jsx2(LimitsBlock, { items, inner, privacyMode })
|
|
359
|
+
/* @__PURE__ */ jsx2(LimitsBlock, { items, inner, privacyMode, resetDisplay, tz })
|
|
361
360
|
] }),
|
|
362
361
|
meta.hasBilling && !showBars && !meta.hasUsage && /* @__PURE__ */ jsx2(CompactBilling, { items, privacyMode }),
|
|
363
362
|
hasSpark && showSpark && /* @__PURE__ */ jsxs2(Fragment, { children: [
|
|
@@ -430,11 +429,12 @@ function accountTitle(account, billing, privacyMode = false) {
|
|
|
430
429
|
const title = email && !account.name.includes("@") ? `${account.name} ${email}` : account.name;
|
|
431
430
|
return privacyMode ? redactEmail(title) : title;
|
|
432
431
|
}
|
|
433
|
-
function LimitsBlock({ items, inner, privacyMode }) {
|
|
432
|
+
function LimitsBlock({ items, inner, privacyMode, resetDisplay, tz }) {
|
|
434
433
|
const showName = items.length > 1;
|
|
435
434
|
const labels = items.flatMap((i) => i.s?.billing?.metrics ?? []).map((m) => m.label.length);
|
|
436
435
|
const labelW = Math.min(Math.max(7, ...labels) + 1, 14);
|
|
437
|
-
const
|
|
436
|
+
const resetW = resetDisplay === "absolute" ? 17 : 8;
|
|
437
|
+
const barW = Math.max(10, Math.min(46, inner - labelW - resetW - 5));
|
|
438
438
|
return /* @__PURE__ */ jsx2(Box2, { flexDirection: "column", children: items.map(({ account, s }, idx) => {
|
|
439
439
|
const billing = s?.billing;
|
|
440
440
|
return /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", marginTop: showName && idx > 0 ? 1 : 0, children: [
|
|
@@ -448,11 +448,11 @@ function LimitsBlock({ items, inner, privacyMode }) {
|
|
|
448
448
|
!billing ? /* @__PURE__ */ jsxs2(Text2, { dimColor: true, children: [
|
|
449
449
|
"Fetching",
|
|
450
450
|
glyphs().ellipsis
|
|
451
|
-
] }) : billing.error ? /* @__PURE__ */ jsx2(Text2, { color: "red", wrap: "truncate-end", children: privacyMode ? redactEmail(billing.error) : billing.error }) : billing.metrics.length === 0 ? /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: "No data" }) : billing.metrics.map((m, i) => /* @__PURE__ */ jsx2(MetricRow, { m, color: account.color, barW, labelW }, `${m.label}${i}`))
|
|
451
|
+
] }) : billing.error ? /* @__PURE__ */ jsx2(Text2, { color: "red", wrap: "truncate-end", children: privacyMode ? redactEmail(billing.error) : billing.error }) : billing.metrics.length === 0 ? /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: "No data" }) : billing.metrics.map((m, i) => /* @__PURE__ */ jsx2(MetricRow, { m, color: account.color, barW, labelW, resetW, resetDisplay, tz }, `${m.label}${i}`))
|
|
452
452
|
] }, account.id);
|
|
453
453
|
}) });
|
|
454
454
|
}
|
|
455
|
-
function MetricRow({ m, color, barW, labelW }) {
|
|
455
|
+
function MetricRow({ m, color, barW, labelW, resetW, resetDisplay, tz }) {
|
|
456
456
|
if (m.format.kind === "percent") {
|
|
457
457
|
const barColor = m.used >= 90 ? "red" : m.used >= 75 ? "yellow" : color;
|
|
458
458
|
return /* @__PURE__ */ jsxs2(Box2, { children: [
|
|
@@ -462,7 +462,7 @@ function MetricRow({ m, color, barW, labelW }) {
|
|
|
462
462
|
Math.round(m.used),
|
|
463
463
|
"%"
|
|
464
464
|
] }) }),
|
|
465
|
-
/* @__PURE__ */ jsx2(Box2, { width:
|
|
465
|
+
/* @__PURE__ */ jsx2(Box2, { width: resetW, justifyContent: "flex-end", children: m.resetsAt ? /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: resetAt(m.resetsAt, resetDisplay, Date.now(), tz) }) : /* @__PURE__ */ jsx2(Text2, { children: " " }) })
|
|
466
466
|
] });
|
|
467
467
|
}
|
|
468
468
|
return /* @__PURE__ */ jsxs2(Box2, { children: [
|
|
@@ -530,7 +530,7 @@ import { Box as Box4, Text as Text4 } from "ink";
|
|
|
530
530
|
import { memo as memo3 } from "react";
|
|
531
531
|
import { Box as Box3, Text as Text3 } from "ink";
|
|
532
532
|
import { Fragment as Fragment2, jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
|
|
533
|
-
var GENERAL_ROWS =
|
|
533
|
+
var GENERAL_ROWS = 10;
|
|
534
534
|
var SETTINGS_TABS = ["general", "providers", "accounts"];
|
|
535
535
|
var SETTINGS_TAB_LABELS = {
|
|
536
536
|
general: "General",
|
|
@@ -626,7 +626,18 @@ var SettingsView = memo3(function SettingsView2({
|
|
|
626
626
|
] })
|
|
627
627
|
] }),
|
|
628
628
|
/* @__PURE__ */ jsx3(Row, { cursor, idx: 8, label: "Network access", children: /* @__PURE__ */ jsx3(Text3, { bold: true, color: config.allowNetworkAccess ? "red" : "green", children: config.allowNetworkAccess ? "LAN (unsafe)" : "local only" }) }),
|
|
629
|
-
config.allowNetworkAccess && /* @__PURE__ */ jsx3(Text3, { color: "red", children: " Warning: dashboard data and settings will be exposed to your local network after daemon restart." })
|
|
629
|
+
config.allowNetworkAccess && /* @__PURE__ */ jsx3(Text3, { color: "red", children: " Warning: dashboard data and settings will be exposed to your local network after daemon restart." }),
|
|
630
|
+
/* @__PURE__ */ jsxs3(Row, { cursor, idx: 9, label: "Reset times", children: [
|
|
631
|
+
/* @__PURE__ */ jsxs3(Text3, { dimColor: true, children: [
|
|
632
|
+
glyphs().caretL,
|
|
633
|
+
" "
|
|
634
|
+
] }),
|
|
635
|
+
/* @__PURE__ */ jsx3(Text3, { bold: true, color: "yellow", children: config.resetDisplay === "relative" ? "time remaining" : "exact date/time" }),
|
|
636
|
+
/* @__PURE__ */ jsxs3(Text3, { dimColor: true, children: [
|
|
637
|
+
" ",
|
|
638
|
+
glyphs().caretR
|
|
639
|
+
] })
|
|
640
|
+
] })
|
|
630
641
|
] }),
|
|
631
642
|
activeTab === "providers" && /* @__PURE__ */ jsxs3(Fragment2, { children: [
|
|
632
643
|
/* @__PURE__ */ jsx3(Text3, { bold: true, dimColor: true, children: "Providers" }),
|
|
@@ -1607,273 +1618,6 @@ function TinyRow({ provider, accounts, stats, width }) {
|
|
|
1607
1618
|
|
|
1608
1619
|
// src/client/use-daemon.ts
|
|
1609
1620
|
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
1621
|
function useDaemon(baseUrl) {
|
|
1878
1622
|
const [snapshot, setSnapshot] = useState3(null);
|
|
1879
1623
|
const [conn, setConn] = useState3("connecting");
|
|
@@ -2388,6 +2132,10 @@ function handleSettings(input, key, ctx) {
|
|
|
2388
2132
|
}
|
|
2389
2133
|
if (index === 8 && (inputKey.leftArrow || inputKey.rightArrow || inputKey.return)) {
|
|
2390
2134
|
updateConfig((current) => ({ ...current, allowNetworkAccess: !current.allowNetworkAccess }));
|
|
2135
|
+
return;
|
|
2136
|
+
}
|
|
2137
|
+
if (index === 9 && (inputKey.leftArrow || inputKey.rightArrow || inputKey.return)) {
|
|
2138
|
+
updateConfig((current) => ({ ...current, resetDisplay: current.resetDisplay === "relative" ? "absolute" : "relative" }));
|
|
2391
2139
|
}
|
|
2392
2140
|
}
|
|
2393
2141
|
}
|
|
@@ -2543,10 +2291,9 @@ function terminalFocusEvent(input) {
|
|
|
2543
2291
|
if (input === "[O") return "out";
|
|
2544
2292
|
return null;
|
|
2545
2293
|
}
|
|
2546
|
-
function handleTerminalFocusInput(input
|
|
2294
|
+
function handleTerminalFocusInput(input) {
|
|
2547
2295
|
const event = terminalFocusEvent(input);
|
|
2548
2296
|
if (!event) return false;
|
|
2549
|
-
if (event === "in") onFocusIn();
|
|
2550
2297
|
return true;
|
|
2551
2298
|
}
|
|
2552
2299
|
function isRefreshAllShortcut(input, mode) {
|
|
@@ -3654,7 +3401,7 @@ function App({ interval: cliInterval, initialConfig, baseUrl = null, mode = "deg
|
|
|
3654
3401
|
if (webStartingRef.current) return;
|
|
3655
3402
|
webStartingRef.current = true;
|
|
3656
3403
|
try {
|
|
3657
|
-
const { startWebServer } = await import("./server-
|
|
3404
|
+
const { startWebServer } = await import("./server-23ERUOJ7.js");
|
|
3658
3405
|
const ctrl = await startWebServer({ config: cfg, log: false });
|
|
3659
3406
|
webRef.current = ctrl;
|
|
3660
3407
|
openUrl(ctrl.url);
|
|
@@ -3701,17 +3448,8 @@ function App({ interval: cliInterval, initialConfig, baseUrl = null, mode = "deg
|
|
|
3701
3448
|
() => tab === 1 ? sortRows(filterTokenRows(filterRowsByModel(rawTokenRows, activeTableModel), search), sort) : [],
|
|
3702
3449
|
[tab, rawTokenRows, activeTableModel, search, sort]
|
|
3703
3450
|
);
|
|
3704
|
-
const lastFocusRefreshRef = useRef8(Date.now());
|
|
3705
|
-
const refreshOnFocus = useCallback7(() => {
|
|
3706
|
-
const now = Date.now();
|
|
3707
|
-
if (now - lastFocusRefreshRef.current < 3e4) return;
|
|
3708
|
-
lastFocusRefreshRef.current = now;
|
|
3709
|
-
const task = connected ? daemonRefreshRef.current("billing") : degradedRefreshRef.current();
|
|
3710
|
-
void task.catch(() => {
|
|
3711
|
-
});
|
|
3712
|
-
}, [connected]);
|
|
3713
3451
|
useInput((input, key) => {
|
|
3714
|
-
if (handleTerminalFocusInput(input
|
|
3452
|
+
if (handleTerminalFocusInput(input)) return;
|
|
3715
3453
|
handleKey(input, key, {
|
|
3716
3454
|
onboarding: {
|
|
3717
3455
|
show: showPicker,
|
|
@@ -3833,7 +3571,7 @@ function App({ interval: cliInterval, initialConfig, baseUrl = null, mode = "deg
|
|
|
3833
3571
|
] }),
|
|
3834
3572
|
/* @__PURE__ */ jsxs12(Box11, { children: [
|
|
3835
3573
|
peak && /* @__PURE__ */ jsxs12(Fragment4, { children: [
|
|
3836
|
-
/* @__PURE__ */ jsx12(PeakBadge, { peak }),
|
|
3574
|
+
/* @__PURE__ */ jsx12(PeakBadge, { peak, resetDisplay: cfg.resetDisplay, tz }),
|
|
3837
3575
|
/* @__PURE__ */ jsxs12(Text12, { dimColor: true, children: [
|
|
3838
3576
|
" ",
|
|
3839
3577
|
glyphs().middot,
|
|
@@ -3892,7 +3630,7 @@ function App({ interval: cliInterval, initialConfig, baseUrl = null, mode = "deg
|
|
|
3892
3630
|
] })
|
|
3893
3631
|
] }),
|
|
3894
3632
|
tab === 0 && /* @__PURE__ */ jsxs12(Fragment4, { children: [
|
|
3895
|
-
/* @__PURE__ */ jsx12(DashboardView, { groups, stats, cols, budget: gridBudget, focusId, layout: cfg.dashboardLayout, page: dashPage, privacyMode: cfg.privacyMode }),
|
|
3633
|
+
/* @__PURE__ */ jsx12(DashboardView, { groups, stats, cols, budget: gridBudget, focusId, layout: cfg.dashboardLayout, page: dashPage, privacyMode: cfg.privacyMode, resetDisplay: cfg.resetDisplay, tz }),
|
|
3896
3634
|
slots.length > 1 && /* @__PURE__ */ jsxs12(Box11, { marginTop: 1, children: [
|
|
3897
3635
|
/* @__PURE__ */ jsx12(Text12, { dimColor: true, children: "focus " }),
|
|
3898
3636
|
/* @__PURE__ */ jsx12(
|