tokmon 0.28.5 → 0.28.7

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.
Files changed (33) hide show
  1. package/dist/{bootstrap-ink-RXSGXK7H.js → bootstrap-ink-PYYVQAYP.js} +25 -9
  2. package/dist/{chunk-KLRDULRC.js → chunk-6E2OJCIQ.js} +4 -4
  3. package/dist/{chunk-TN6V7XSL.js → chunk-BZFDTIFV.js} +32 -10
  4. package/dist/{chunk-CFKUZCMB.js → chunk-DESEB73E.js} +7 -6
  5. package/dist/{chunk-NNQGZNX4.js → chunk-MIXBF6QU.js} +96 -12
  6. package/dist/{chunk-HNWEJ6MS.js → chunk-O7ORALKA.js} +2 -0
  7. package/dist/{chunk-6YJ5H3JT.js → chunk-RY24UHMK.js} +2 -2
  8. package/dist/{chunk-EEMD2PXA.js → chunk-UON47FYF.js} +11 -3
  9. package/dist/{chunk-5B3XCEMC.js → chunk-WHOIUSJM.js} +53 -4
  10. package/dist/{cli-command-4XXKT32M.js → cli-command-XPFM6LUP.js} +17 -8
  11. package/dist/cli.js +6 -6
  12. package/dist/{config-3DGZ47F7.js → config-JEDXAKPZ.js} +1 -1
  13. package/dist/{daemon-D42FZQC5.js → daemon-KFMXYG7V.js} +55 -53
  14. package/dist/daemon-handle-6RYUJPC2.js +11 -0
  15. package/dist/server-LNFK5MRX.js +12 -0
  16. package/dist/web/assets/{Area-DSOnmPHW.js → Area-BqyvRn2x.js} +1 -1
  17. package/dist/web/assets/{analytics-CQcXEq3U.js → analytics-B6QwqU9g.js} +2 -2
  18. package/dist/web/assets/{breakdown-DLwDPWvA.js → breakdown-DYnVKScr.js} +1 -1
  19. package/dist/web/assets/{button-D767RAip.js → button-CnT5g-Jl.js} +1 -1
  20. package/dist/web/assets/{chart-CylR-9UK.js → chart-Duz9D-nH.js} +1 -1
  21. package/dist/web/assets/{explore-jBA0FMUo.js → explore-P2EolJrt.js} +1 -1
  22. package/dist/web/assets/{index-DMNfKjk7.js → index-9hkGpa7j.js} +12 -12
  23. package/dist/web/assets/{models-B4X_VrrZ.js → models-8B4FBxr7.js} +2 -2
  24. package/dist/web/assets/{overview-E1xtkiJT.js → overview-DXA_HAsw.js} +2 -2
  25. package/dist/web/assets/{panel-Cna_MX7C.js → panel-gxLFBb8y.js} +1 -1
  26. package/dist/web/assets/settings-sheet-CJJl-3Gw.js +1 -0
  27. package/dist/web/assets/{share-sheet-BBs3U9QV.js → share-sheet-CXdyA1ga.js} +1 -1
  28. package/dist/web/assets/{timeline-DAOfy1yc.js → timeline-CzC85trW.js} +1 -1
  29. package/dist/web/index.html +1 -1
  30. package/package.json +1 -1
  31. package/dist/daemon-handle-RHIX5OYY.js +0 -11
  32. package/dist/server-BNZCKS2A.js +0 -12
  33. package/dist/web/assets/settings-sheet-Uba9f8CS.js +0 -1
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  createDaemonRpcClient
4
- } from "./chunk-6YJ5H3JT.js";
4
+ } from "./chunk-RY24UHMK.js";
5
5
  import {
6
6
  accountIdentityText,
7
7
  accountsByProvider,
@@ -11,7 +11,7 @@ import {
11
11
  resolveQuotaViews,
12
12
  severity,
13
13
  usageFromHeadroom
14
- } from "./chunk-TN6V7XSL.js";
14
+ } from "./chunk-BZFDTIFV.js";
15
15
  import {
16
16
  PROVIDERS,
17
17
  PROVIDER_ORDER,
@@ -28,8 +28,8 @@ import {
28
28
  time,
29
29
  tokens,
30
30
  withTimeout
31
- } from "./chunk-CFKUZCMB.js";
32
- import "./chunk-EEMD2PXA.js";
31
+ } from "./chunk-DESEB73E.js";
32
+ import "./chunk-UON47FYF.js";
33
33
  import {
34
34
  COLOR_PALETTE,
35
35
  DEFAULTS,
@@ -54,7 +54,7 @@ import {
54
54
  snapshotCacheFile,
55
55
  themePresetOption,
56
56
  toggleProviderSelection
57
- } from "./chunk-HNWEJ6MS.js";
57
+ } from "./chunk-O7ORALKA.js";
58
58
  import {
59
59
  glyphs
60
60
  } from "./chunk-RF4GGQGM.js";
@@ -655,6 +655,12 @@ function quotaViews(stats) {
655
655
  }
656
656
  function QuotaRow({ quota, color, barW, labelW, resetW, resetDisplay, tz }) {
657
657
  const theme = useTuiTheme();
658
+ if (quota.value?.kind === "money") {
659
+ return /* @__PURE__ */ jsxs2(Box2, { children: [
660
+ /* @__PURE__ */ jsx3(Box2, { width: labelW, flexShrink: 0, children: /* @__PURE__ */ jsx3(Text2, { dimColor: true, wrap: "truncate", children: quota.label }) }),
661
+ /* @__PURE__ */ jsx3(Text2, { bold: true, color: theme.cost, wrap: "truncate-end", children: quota.valueText })
662
+ ] });
663
+ }
658
664
  if (quota.bounded && quota.usedPct !== null) {
659
665
  const used = quota.usedPct;
660
666
  const remaining = quota.remainingPct ?? 0;
@@ -1293,6 +1299,14 @@ var DESKTOP_FIXED_SETTINGS = [
1293
1299
  if (isAdjustKey(input, key)) ctx.global.updateConfig((c) => ({ ...c, tray: { ...c.tray, showMenuBarText: !c.tray.showMenuBarText } }));
1294
1300
  }
1295
1301
  },
1302
+ {
1303
+ key: "menuBarContent",
1304
+ label: "Menu bar content",
1305
+ render: (rc) => caret(/* @__PURE__ */ jsx7(Text6, { bold: true, color: rc.theme.accent, children: rc.config.tray.menuBarValue === "todayTokens" ? "tokens today" : "usage" })),
1306
+ onAdjust: (input, key, ctx) => {
1307
+ if (isAdjustKey(input, key)) ctx.global.updateConfig((c) => ({ ...c, tray: { ...c.tray, menuBarValue: c.tray.menuBarValue === "usage" ? "todayTokens" : "usage" } }));
1308
+ }
1309
+ },
1296
1310
  {
1297
1311
  key: "summary",
1298
1312
  label: "Summary",
@@ -3622,7 +3636,7 @@ function useAccountForm({ cfg, detected, updateConfig, trackedAccountRows, setSe
3622
3636
 
3623
3637
  // src/app.tsx
3624
3638
  import { Fragment as Fragment6, jsx as jsx13, jsxs as jsxs12 } from "react/jsx-runtime";
3625
- function App({ interval: cliInterval, initialConfig, baseUrl = null, mode = "degraded" }) {
3639
+ function App({ interval: cliInterval, initialConfig, baseUrl = null, mode = "degraded", degradedMessage }) {
3626
3640
  const connected = mode === "connected" && baseUrl !== null;
3627
3641
  const degraded = !connected;
3628
3642
  const daemon = useDaemon(connected ? baseUrl : null);
@@ -3996,7 +4010,7 @@ function App({ interval: cliInterval, initialConfig, baseUrl = null, mode = "deg
3996
4010
  if (webStartingRef.current) return;
3997
4011
  webStartingRef.current = true;
3998
4012
  try {
3999
- const { startWebServer } = await import("./server-BNZCKS2A.js");
4013
+ const { startWebServer } = await import("./server-LNFK5MRX.js");
4000
4014
  const ctrl = await startWebServer({ config: cfg, log: false });
4001
4015
  webRef.current = ctrl;
4002
4016
  openUrl(ctrl.url);
@@ -4185,7 +4199,8 @@ function App({ interval: cliInterval, initialConfig, baseUrl = null, mode = "deg
4185
4199
  glyphs().warn,
4186
4200
  " degraded ",
4187
4201
  glyphs().middot,
4188
- " background service unavailable, running in-process"
4202
+ " ",
4203
+ degradedMessage ?? "background service unavailable, running in-process"
4189
4204
  ] }),
4190
4205
  connected && daemon.conn !== "live" && /* @__PURE__ */ jsxs12(Text12, { dimColor: true, children: [
4191
4206
  glyphs().warn,
@@ -4333,7 +4348,8 @@ async function bootstrapInk({ interval, config, daemon, mode }) {
4333
4348
  interval,
4334
4349
  initialConfig: config,
4335
4350
  baseUrl: daemon.baseUrl,
4336
- mode
4351
+ mode,
4352
+ degradedMessage: daemon.issue?.message
4337
4353
  }
4338
4354
  ) })
4339
4355
  );
@@ -10,13 +10,13 @@ import {
10
10
  deriveQuotaViews,
11
11
  fetchPeak,
12
12
  namedHex
13
- } from "./chunk-TN6V7XSL.js";
13
+ } from "./chunk-BZFDTIFV.js";
14
14
  import {
15
15
  PROVIDERS,
16
16
  detectProviders,
17
17
  resolveTimezone,
18
18
  withTimeout
19
- } from "./chunk-CFKUZCMB.js";
19
+ } from "./chunk-DESEB73E.js";
20
20
  import {
21
21
  ConfigPersistenceFailure,
22
22
  ConfigUpdateConflictFailure,
@@ -26,7 +26,7 @@ import {
26
26
  TOKMON_WS_METHODS,
27
27
  TOKMON_WS_PATH,
28
28
  TokmonRpcGroup
29
- } from "./chunk-EEMD2PXA.js";
29
+ } from "./chunk-UON47FYF.js";
30
30
  import {
31
31
  cacheDir,
32
32
  expandHome,
@@ -34,7 +34,7 @@ import {
34
34
  normalizeConfig,
35
35
  saveConfig,
36
36
  snapshotCacheFile
37
- } from "./chunk-HNWEJ6MS.js";
37
+ } from "./chunk-O7ORALKA.js";
38
38
 
39
39
  // src/web/server.ts
40
40
  import { createServer } from "http";
@@ -5,13 +5,13 @@ import {
5
5
  identityFromIdToken,
6
6
  readClaudeIdentity,
7
7
  readJson
8
- } from "./chunk-CFKUZCMB.js";
8
+ } from "./chunk-DESEB73E.js";
9
9
  import {
10
10
  containsEmail,
11
11
  expandHome,
12
12
  redactEmail,
13
13
  slugify
14
- } from "./chunk-HNWEJ6MS.js";
14
+ } from "./chunk-O7ORALKA.js";
15
15
 
16
16
  // src/accounts.ts
17
17
  import { existsSync, readdirSync, readFileSync as readFileSync2, statSync } from "fs";
@@ -281,15 +281,34 @@ function inferRole(metric) {
281
281
  if (metric.modelId || !metric.primary && metric.format.kind === "percent" && !["usage", "api", "auto"].includes(label)) return "model";
282
282
  return "other";
283
283
  }
284
- function valueText(metric, usedPct) {
284
+ function formatMoney(value, currency) {
285
+ try {
286
+ return new Intl.NumberFormat("en-US", { style: "currency", currency }).format(value);
287
+ } catch {
288
+ return `$${value.toFixed(2)}`;
289
+ }
290
+ }
291
+ function moneyValue(metric) {
292
+ if (metric.format.kind !== "dollars" || !Number.isFinite(metric.used)) return void 0;
293
+ const limit = metric.limit != null && Number.isFinite(metric.limit) ? metric.limit : null;
294
+ return {
295
+ kind: "money",
296
+ used: metric.used,
297
+ limit,
298
+ remaining: limit === null ? null : limit - metric.used,
299
+ currency: metric.format.currency?.trim().toUpperCase() || "USD"
300
+ };
301
+ }
302
+ function valueText(metric, usedPct, value) {
303
+ if (value) {
304
+ if (value.remaining === null) return formatMoney(value.used, value.currency);
305
+ const used = `${formatMoney(value.used, value.currency)} used`;
306
+ return value.remaining >= 0 ? `${used} \xB7 ${formatMoney(value.remaining, value.currency)} left` : `${used} \xB7 ${formatMoney(-value.remaining, value.currency)} over`;
307
+ }
285
308
  if (usedPct !== null) return `${Math.round(usedPct)}% used`;
286
309
  if (!Number.isFinite(metric.used)) return "\u2014";
287
310
  if (metric.format.kind === "dollars") {
288
- try {
289
- return new Intl.NumberFormat("en-US", { style: "currency", currency: metric.format.currency ?? "USD", maximumFractionDigits: 2 }).format(metric.used);
290
- } catch {
291
- return `$${metric.used.toFixed(2)}`;
292
- }
311
+ return formatMoney(metric.used, metric.format.currency?.trim().toUpperCase() || "USD");
293
312
  }
294
313
  if (metric.format.kind === "count") {
295
314
  const suffix = metric.format.suffix?.trim();
@@ -303,6 +322,7 @@ function deriveQuotaView(metric, sourceIndex = 0) {
303
322
  const usedPct = metric.format.kind === "percent" ? metric.used : metric.limit != null && metric.limit > 0 ? metric.used / metric.limit * 100 : null;
304
323
  const finiteUsedPct = usedPct == null || !Number.isFinite(usedPct) ? null : clampPct(usedPct);
305
324
  const resets = metric.resetsAt ? Date.parse(metric.resetsAt) : Number.NaN;
325
+ const value = moneyValue(metric);
306
326
  return {
307
327
  key: metric.key ?? `${metric.label}:${sourceIndex}`,
308
328
  label: metric.label.replace(/\s*(limit|usage)$/i, "").trim() || metric.label,
@@ -315,7 +335,8 @@ function deriveQuotaView(metric, sourceIndex = 0) {
315
335
  primary: metric.primary === true,
316
336
  active: metric.active === true,
317
337
  displayOrder: sourceIndex,
318
- valueText: valueText(metric, finiteUsedPct)
338
+ valueText: valueText(metric, finiteUsedPct, value),
339
+ ...value ? { value } : {}
319
340
  };
320
341
  }
321
342
  function deriveQuotaViews(metrics) {
@@ -425,8 +446,9 @@ function cumulativeQuotaViews(accounts) {
425
446
  const remainingPct = rows.reduce((sum, quota) => sum + quota.remainingPct, 0) / rows.length;
426
447
  const usedPct = 100 - remainingPct;
427
448
  const resets = rows.map((quota) => quota.resetsAt).filter((value) => value !== null);
449
+ const { value: _value, ...firstWithoutValue } = first;
428
450
  return {
429
- ...first,
451
+ ...firstWithoutValue,
430
452
  usedPct,
431
453
  remainingPct,
432
454
  resetsAt: resets.length > 0 ? Math.min(...resets) : null,
@@ -5,7 +5,7 @@ import {
5
5
  envDir,
6
6
  expandHome,
7
7
  isValidTimezone
8
- } from "./chunk-HNWEJ6MS.js";
8
+ } from "./chunk-O7ORALKA.js";
9
9
 
10
10
  // src/async.ts
11
11
  var DEFAULT_TIMEOUT_MS = 3e4;
@@ -1706,7 +1706,6 @@ function identityFromIdToken(idToken) {
1706
1706
  // src/providers/codex/billing.ts
1707
1707
  var USAGE_URL = "https://chatgpt.com/backend-api/wham/usage";
1708
1708
  var RESET_CREDITS_URL = "https://chatgpt.com/backend-api/wham/rate-limit-reset-credits";
1709
- var CREDIT_USD_RATE = 0.04;
1710
1709
  function chatGptPlanLabel(planType) {
1711
1710
  if (typeof planType !== "string" || !planType.trim()) return null;
1712
1711
  const p = planType.trim().toLowerCase();
@@ -1830,11 +1829,13 @@ function appendWindowMetrics(metrics, rl, headerPct) {
1830
1829
  metrics.push(percentMetric(label, used, resetFrom2(item)));
1831
1830
  }
1832
1831
  }
1833
- function appendCredits(metrics, source) {
1832
+ function creditBalanceMetric(source) {
1834
1833
  const balance = numberValue(source?.credits?.balance ?? source?.credit_balance);
1835
- if (balance !== void 0 && balance >= 0) {
1836
- metrics.push({ label: "Credits", used: balance * CREDIT_USD_RATE, limit: null, format: { kind: "dollars" } });
1837
- }
1834
+ return balance !== void 0 && balance >= 0 ? { key: "credits", role: "unbounded", label: "Credits", used: balance, limit: null, format: { kind: "count", suffix: "available" } } : null;
1835
+ }
1836
+ function appendCredits(metrics, source) {
1837
+ const metric = creditBalanceMetric(source);
1838
+ if (metric) metrics.push(metric);
1838
1839
  }
1839
1840
  async function fetchResetCredits(headers) {
1840
1841
  try {
@@ -1,16 +1,21 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
+ classifyDaemonCompatibility,
4
+ daemonConflictMessage,
5
+ isAlive,
6
+ probeHealth,
7
+ readForeignLock,
3
8
  readLock,
4
9
  retireIncompatibleCliOwner,
5
10
  verifyLock
6
- } from "./chunk-5B3XCEMC.js";
11
+ } from "./chunk-WHOIUSJM.js";
7
12
  import {
8
13
  daemonChannelFromWire,
9
14
  resolveDaemonChannel
10
15
  } from "./chunk-UYPDMVW5.js";
11
16
  import {
12
17
  TOKMON_PROTOCOL_VERSION
13
- } from "./chunk-EEMD2PXA.js";
18
+ } from "./chunk-UON47FYF.js";
14
19
 
15
20
  // src/client/daemon-handle.ts
16
21
  import { spawn } from "child_process";
@@ -51,6 +56,45 @@ async function attach(opts, protocolVersion) {
51
56
  const lock = await verifyLock(readLock(opts), protocolVersion);
52
57
  return lock ? connected(lock.url) : null;
53
58
  }
59
+ function incompatibleOwnerIssue(owner, protocolVersion, options = {}) {
60
+ return {
61
+ kind: owner.ownerKind === "desktop" ? "incompatible-desktop" : owner.ownerKind === "cli" ? "incompatible-cli" : "incompatible-owner",
62
+ message: daemonConflictMessage(owner, {
63
+ clientKind: "cli",
64
+ clientProtocolVersion: protocolVersion,
65
+ ...options
66
+ }),
67
+ ownerKind: owner.ownerKind ?? null,
68
+ ownerVersion: owner.version ?? null,
69
+ ownerProtocolVersion: owner.protocolVersion ?? null,
70
+ clientProtocolVersion: protocolVersion
71
+ };
72
+ }
73
+ async function arbitrateIncompatibleOwner(opts, protocolVersion, timeoutMs) {
74
+ const owner = readForeignLock(opts);
75
+ if (!owner || !isAlive(owner.pid)) return { retired: false, issue: null };
76
+ const decision = classifyDaemonCompatibility(owner, protocolVersion);
77
+ const verified = await probeHealth(owner.url, owner.wsToken, {
78
+ channel: owner.channel,
79
+ ...owner.ownerKind ? { ownerKind: owner.ownerKind } : {},
80
+ ...owner.protocolVersion === void 0 ? {} : { protocolVersion: owner.protocolVersion },
81
+ ...owner.version ? { version: owner.version } : {}
82
+ }, Math.min(500, timeoutMs));
83
+ if (!verified) {
84
+ return {
85
+ retired: false,
86
+ issue: incompatibleOwnerIssue(owner, protocolVersion, { verificationFailed: true })
87
+ };
88
+ }
89
+ if (decision.action !== "retire") {
90
+ return { retired: false, issue: incompatibleOwnerIssue(owner, protocolVersion) };
91
+ }
92
+ const retired = await retireIncompatibleCliOwner(opts, protocolVersion, timeoutMs);
93
+ return retired ? { retired: true, issue: null } : {
94
+ retired: false,
95
+ issue: incompatibleOwnerIssue(owner, protocolVersion, { retirementFailed: true })
96
+ };
97
+ }
54
98
  async function attachOrSpawn(opts = {}) {
55
99
  const protocolVersion = TOKMON_PROTOCOL_VERSION;
56
100
  const timeoutMs = opts.timeoutMs ?? HANDSHAKE_TIMEOUT_MS;
@@ -59,9 +103,21 @@ async function attachOrSpawn(opts = {}) {
59
103
  const lockOpts = { cachePath: opts.cachePath, channel };
60
104
  const existing = await attach(lockOpts, protocolVersion);
61
105
  if (existing) return existing;
62
- await retireIncompatibleCliOwner(lockOpts, protocolVersion, timeoutMs);
63
- const upgraded = await attach(lockOpts, protocolVersion);
64
- if (upgraded) return upgraded;
106
+ for (let attempt = 0; attempt < 2; attempt++) {
107
+ const arbitration = await arbitrateIncompatibleOwner(lockOpts, protocolVersion, timeoutMs);
108
+ if (arbitration.issue) return degraded(arbitration.issue);
109
+ if (!arbitration.retired) break;
110
+ const upgraded = await attach(lockOpts, protocolVersion);
111
+ if (upgraded) return upgraded;
112
+ }
113
+ const raced = await attach(lockOpts, protocolVersion);
114
+ if (raced) return raced;
115
+ const raceArbitration = await arbitrateIncompatibleOwner(lockOpts, protocolVersion, timeoutMs);
116
+ if (raceArbitration.issue) return degraded(raceArbitration.issue);
117
+ if (raceArbitration.retired) {
118
+ const replacement = await attach(lockOpts, protocolVersion);
119
+ if (replacement) return replacement;
120
+ }
65
121
  const entry = opts.entry ?? process.argv[1];
66
122
  if (!entry) return degraded();
67
123
  const execPath = opts.execPath ?? process.execPath;
@@ -79,13 +135,19 @@ async function attachOrSpawn(opts = {}) {
79
135
  detached: process.platform !== "win32",
80
136
  env
81
137
  });
82
- } catch {
83
- resolve(degraded());
138
+ } catch (error) {
139
+ const detail = error instanceof Error && error.message ? `: ${error.message}` : "";
140
+ resolve(degraded({
141
+ kind: "spawn-failed",
142
+ message: `Could not start the Tokmon background service${detail}. Check Node permissions and retry; local-only mode is active.`
143
+ }));
84
144
  return;
85
145
  }
86
146
  let settled = false;
87
147
  let stdout = "";
88
148
  let pollTimer = null;
149
+ let startupError = null;
150
+ let startupExit = null;
89
151
  const finish = (handle) => {
90
152
  if (settled) return;
91
153
  settled = true;
@@ -102,7 +164,23 @@ async function attachOrSpawn(opts = {}) {
102
164
  return;
103
165
  }
104
166
  if (final) {
105
- finish(degraded());
167
+ if (startupError) {
168
+ finish(degraded({
169
+ kind: "spawn-failed",
170
+ message: `Could not start the Tokmon background service: ${startupError}. Check Node permissions and retry; local-only mode is active.`
171
+ }));
172
+ } else if (startupExit) {
173
+ const result = startupExit.signal ? `signal ${startupExit.signal}` : `exit code ${startupExit.code ?? "unknown"}`;
174
+ finish(degraded({
175
+ kind: "startup-exit",
176
+ message: `The Tokmon background service exited before it was ready (${result}). Update or reinstall Tokmon, then retry; local-only mode is active.`
177
+ }));
178
+ } else {
179
+ finish(degraded({
180
+ kind: "startup-timeout",
181
+ message: `The Tokmon background service did not become ready within ${Math.ceil(timeoutMs / 1e3)}s. Quit any stuck Tokmon process and retry; local-only mode is active.`
182
+ }));
183
+ }
106
184
  return;
107
185
  }
108
186
  if (!settled && !pollTimer) {
@@ -127,12 +205,18 @@ async function attachOrSpawn(opts = {}) {
127
205
  return;
128
206
  }
129
207
  });
130
- child.once("error", () => tryAttach());
131
- child.once("exit", () => tryAttach());
208
+ child.once("error", (error) => {
209
+ startupError = error.message || error.name;
210
+ tryAttach();
211
+ });
212
+ child.once("exit", (code, signal) => {
213
+ startupExit = { code, signal };
214
+ tryAttach();
215
+ });
132
216
  });
133
217
  }
134
- function degraded() {
135
- return { kind: "degraded", baseUrl: null, stop: () => {
218
+ function degraded(issue) {
219
+ return { kind: "degraded", baseUrl: null, ...issue ? { issue } : {}, stop: () => {
136
220
  } };
137
221
  }
138
222
 
@@ -503,6 +503,7 @@ function moveProviderSelection(values, providerId, direction) {
503
503
  var DEFAULT_TRAY_CONFIG = {
504
504
  enabled: true,
505
505
  showMenuBarText: true,
506
+ menuBarValue: "usage",
506
507
  displayMetric: "smartHeadroom",
507
508
  pollIntervalSec: 30,
508
509
  activeTimeoutMin: 10,
@@ -821,6 +822,7 @@ function repairConfig(input) {
821
822
  const tray = {
822
823
  enabled: typeof rawTray.enabled === "boolean" ? rawTray.enabled : DEFAULT_TRAY_CONFIG.enabled,
823
824
  showMenuBarText: typeof rawTray.showMenuBarText === "boolean" ? rawTray.showMenuBarText : DEFAULT_TRAY_CONFIG.showMenuBarText,
825
+ menuBarValue: rawTray.menuBarValue === "todayTokens" || rawTray.menuBarValue === "usage" ? rawTray.menuBarValue : DEFAULT_TRAY_CONFIG.menuBarValue,
824
826
  displayMetric: rawTray.displayMetric === "tightestRemaining" || rawTray.displayMetric === "smartHeadroom" ? rawTray.displayMetric : DEFAULT_TRAY_CONFIG.displayMetric,
825
827
  pollIntervalSec: finiteInRange(rawTray.pollIntervalSec, DEFAULT_TRAY_CONFIG.pollIntervalSec, 1, 86400),
826
828
  activeTimeoutMin: finiteInRange(rawTray.activeTimeoutMin, DEFAULT_TRAY_CONFIG.activeTimeoutMin, 1, 1440),
@@ -4,10 +4,10 @@ import {
4
4
  TOKMON_WS_METHODS,
5
5
  TOKMON_WS_PATH,
6
6
  TokmonRpcGroup
7
- } from "./chunk-EEMD2PXA.js";
7
+ } from "./chunk-UON47FYF.js";
8
8
  import {
9
9
  normalizeConfig
10
- } from "./chunk-HNWEJ6MS.js";
10
+ } from "./chunk-O7ORALKA.js";
11
11
 
12
12
  // src/client/daemon-rpc-client.ts
13
13
  import { Cause, Context, Duration, Effect, Exit, Fiber, Layer, ManagedRuntime, Schedule, Schema, Stream } from "effect";
@@ -4,7 +4,7 @@ import {
4
4
  DESKTOP_GRAPH_RANGES,
5
5
  PROVIDER_IDS,
6
6
  THEME_PRESET_IDS
7
- } from "./chunk-HNWEJ6MS.js";
7
+ } from "./chunk-O7ORALKA.js";
8
8
 
9
9
  // src/rpc/contract.ts
10
10
  import { Schema } from "effect";
@@ -12,7 +12,7 @@ import * as Rpc from "effect/unstable/rpc/Rpc";
12
12
  import * as RpcGroup from "effect/unstable/rpc/RpcGroup";
13
13
  var TOKMON_WS_PATH = "/ws";
14
14
  var TOKMON_PROTOCOL_VERSION = 4;
15
- var TOKMON_CAPABILITIES = ["config-cas", "config-revision", "allowed-hosts", "tray-config", "usage-activity", "tray-pins", "provider-pins", "desktop-disclosure", "desktop-graph-range", "provider-headroom", "canonical-identity", "appearance-v1", "theme-engine", "account-detection-v1"];
15
+ var TOKMON_CAPABILITIES = ["config-cas", "config-revision", "allowed-hosts", "tray-config", "usage-activity", "tray-pins", "provider-pins", "desktop-disclosure", "desktop-graph-range", "provider-headroom", "canonical-identity", "appearance-v1", "theme-engine", "account-detection-v1", "menu-bar-today-tokens"];
16
16
  var TOKMON_WS_METHODS = {
17
17
  getConfig: "tokmon.getConfig",
18
18
  setConfig: "tokmon.setConfig",
@@ -41,6 +41,7 @@ var AccountSchema = Schema.Struct({
41
41
  var TrayConfigSchema = Schema.Struct({
42
42
  enabled: Schema.Boolean,
43
43
  showMenuBarText: Schema.Boolean,
44
+ menuBarValue: Schema.optionalKey(Schema.Literals(["usage", "todayTokens"])),
44
45
  displayMetric: Schema.Literals(["smartHeadroom", "tightestRemaining"]),
45
46
  pollIntervalSec: Schema.Finite.check(Schema.isBetween({ minimum: 1, maximum: 86400 })),
46
47
  activeTimeoutMin: Schema.Finite.check(Schema.isBetween({ minimum: 1, maximum: 1440 })),
@@ -271,7 +272,14 @@ var QuotaViewSchema = Schema.Struct({
271
272
  primary: Schema.Boolean,
272
273
  active: Schema.Boolean,
273
274
  displayOrder: NonNegativeIntegerSchema,
274
- valueText: Schema.String
275
+ valueText: Schema.String,
276
+ value: Schema.optionalKey(Schema.Struct({
277
+ kind: Schema.Literal("money"),
278
+ used: Schema.Finite,
279
+ limit: Schema.NullOr(Schema.Finite),
280
+ remaining: Schema.NullOr(Schema.Finite),
281
+ currency: Schema.String
282
+ }))
275
283
  });
276
284
  var HeadroomFactorSchema = Schema.Struct({
277
285
  key: Schema.String,
@@ -5,7 +5,53 @@ import {
5
5
  } from "./chunk-UYPDMVW5.js";
6
6
  import {
7
7
  cacheDir
8
- } from "./chunk-HNWEJ6MS.js";
8
+ } from "./chunk-O7ORALKA.js";
9
+
10
+ // src/web/daemon-compatibility.ts
11
+ function classifyDaemonCompatibility(owner, clientProtocolVersion) {
12
+ if (owner.protocolVersion === clientProtocolVersion) {
13
+ return { action: "attach", reason: "same-protocol" };
14
+ }
15
+ if (owner.ownerKind === "desktop") {
16
+ return { action: "refuse", reason: "desktop-owner" };
17
+ }
18
+ if (owner.ownerKind === "cli" && owner.protocolVersion !== void 0) {
19
+ return owner.protocolVersion < clientProtocolVersion ? { action: "retire", reason: "older-cli" } : { action: "refuse", reason: "newer-cli" };
20
+ }
21
+ if (owner.ownerKind === void 0 && owner.protocolVersion === void 0) {
22
+ return { action: "retire", reason: "legacy-cli" };
23
+ }
24
+ return { action: "refuse", reason: "ambiguous-owner" };
25
+ }
26
+ function daemonConflictMessage(owner, context) {
27
+ const kind = owner.ownerKind === "desktop" ? "Tokmon Desktop" : owner.ownerKind === "cli" ? "Tokmon CLI background service" : "Legacy Tokmon background service";
28
+ const name = owner.version ? `${kind} ${owner.version}` : kind;
29
+ const ownerProtocol = owner.protocolVersion === void 0 ? "an implicit legacy protocol" : `protocol ${owner.protocolVersion}`;
30
+ const client = context.clientKind === "desktop" ? "this desktop app" : "this CLI";
31
+ if (context.verificationFailed) {
32
+ return `A live lock claims ${name} using ${ownerProtocol}, but its owner proof could not be verified. It was not signalled. Quit the existing Tokmon process, remove a stale lock only after confirming it has stopped, then retry.`;
33
+ }
34
+ if (owner.protocolVersion === context.clientProtocolVersion) {
35
+ return `${name} claims compatible protocol ${context.clientProtocolVersion}, but its owner proof could not be verified. It was not signalled. Quit the existing Tokmon process, then retry.`;
36
+ }
37
+ const summary = `${name} owns the background service using ${ownerProtocol}; ${client} needs protocol ${context.clientProtocolVersion}.`;
38
+ if (context.retirementFailed) {
39
+ return `${summary} Tokmon could not stop that older CLI service safely, so it was left running. Quit the existing Tokmon CLI/background service, then retry.`;
40
+ }
41
+ if (owner.ownerKind === "desktop") {
42
+ if (context.clientKind === "desktop") {
43
+ return owner.protocolVersion !== void 0 && owner.protocolVersion < context.clientProtocolVersion ? `${summary} Fully quit the older Tokmon Desktop instance, install the current release, then reopen Tokmon.` : `${summary} This app is older than the running Tokmon Desktop instance. Reopen the newer app, or replace this installation with the current release.`;
44
+ }
45
+ if (owner.protocolVersion !== void 0 && owner.protocolVersion < context.clientProtocolVersion) {
46
+ return `${summary} In Tokmon Desktop, open Settings \u2192 Desktop App \u2192 Check for Updates, install the update, then restart Tokmon. Alternatively, quit Tokmon Desktop before retrying the CLI.`;
47
+ }
48
+ return `${summary} Update the CLI with \`pnpm --config.minimum-release-age=0 dlx tokmon@latest\`, or quit Tokmon Desktop before retrying this CLI.`;
49
+ }
50
+ if (owner.ownerKind === "cli" && owner.protocolVersion !== void 0 && owner.protocolVersion > context.clientProtocolVersion) {
51
+ return context.clientKind === "desktop" ? `${summary} Update Tokmon Desktop and restart it, or stop the newer CLI background service before reopening the app.` : `${summary} Update the CLI with \`pnpm --config.minimum-release-age=0 dlx tokmon@latest\`, or stop the newer background service before retrying.`;
52
+ }
53
+ return `${summary} The owner could not be identified safely and was not signalled. Quit the existing Tokmon process, then retry.`;
54
+ }
9
55
 
10
56
  // src/web/lockfile.ts
11
57
  import { closeSync, fchmodSync, fsyncSync, lstatSync, mkdirSync, openSync, readFileSync, renameSync, statSync, unlinkSync, writeFileSync } from "fs";
@@ -247,11 +293,11 @@ async function retireIncompatibleCliOwner(opts, protocolVersion, timeoutMs = TAK
247
293
  const compatible = readLock(opts);
248
294
  if (compatible?.protocolVersion === protocolVersion) return false;
249
295
  const foreign = readForeignLock(opts);
250
- const legacyCli = foreign?.ownerKind === void 0 && foreign?.protocolVersion === void 0;
251
- if (!foreign || foreign.state !== "ready" || foreign.ownerKind === "desktop" || foreign.ownerKind !== "cli" && !legacyCli || foreign.pid === process.pid || !isAlive(foreign.pid)) return false;
296
+ const decision = foreign && classifyDaemonCompatibility(foreign, protocolVersion);
297
+ if (!foreign || foreign.state !== "ready" || decision?.action !== "retire" || foreign.pid === process.pid || !isAlive(foreign.pid)) return false;
252
298
  const health = await ownerHealth(foreign.url, foreign.wsToken, Math.min(1e3, timeoutMs));
253
299
  const healthChannel = daemonChannelFromWire(health?.channel);
254
- if (health?.ok !== true || health.owner !== true || health.ownerKind !== void 0 && health.ownerKind !== "cli" || healthChannel !== foreign.channel) return false;
300
+ if (health?.ok !== true || health.owner !== true || (decision.reason === "legacy-cli" ? health.ownerKind !== void 0 || health.protocolVersion !== void 0 : health.ownerKind !== "cli" || health.protocolVersion !== foreign.protocolVersion) || healthChannel !== foreign.channel) return false;
255
301
  try {
256
302
  process.kill(foreign.pid, "SIGTERM");
257
303
  } catch {
@@ -279,6 +325,8 @@ async function retireIncompatibleCliOwner(opts, protocolVersion, timeoutMs = TAK
279
325
  }
280
326
 
281
327
  export {
328
+ classifyDaemonCompatibility,
329
+ daemonConflictMessage,
282
330
  lockfilePath,
283
331
  readLock,
284
332
  readForeignLock,
@@ -289,6 +337,7 @@ export {
289
337
  reclaimUnhealthyLock,
290
338
  reclaimAbandonedLock,
291
339
  isAlive,
340
+ probeHealth,
292
341
  verifyLock,
293
342
  retireIncompatibleCliOwner
294
343
  };