tokmon 0.28.2 → 0.28.4

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 (56) hide show
  1. package/README.md +156 -234
  2. package/THIRD_PARTY_NOTICES.md +62 -0
  3. package/dist/{bootstrap-ink-IMT3S5HU.js → bootstrap-ink-WG4WG3R2.js} +1579 -1136
  4. package/dist/chunk-5B3XCEMC.js +294 -0
  5. package/dist/chunk-7CH6S7BR.js +447 -0
  6. package/dist/{chunk-LCAHTRGP.js → chunk-CFKUZCMB.js} +56 -29
  7. package/dist/{chunk-LKYOWBD4.js → chunk-FLANGRI6.js} +21 -42
  8. package/dist/chunk-HNWEJ6MS.js +1094 -0
  9. package/dist/{chunk-WUK3MQUB.js → chunk-JZSZEHVD.js} +167 -10
  10. package/dist/{chunk-6ZEW2M6P.js → chunk-P57DVFNH.js} +125 -25
  11. package/dist/chunk-TN6V7XSL.js +512 -0
  12. package/dist/chunk-UYPDMVW5.js +21 -0
  13. package/dist/{cli-command-DCENRCO3.js → cli-command-I4GRR3MF.js} +249 -15
  14. package/dist/cli.js +6 -6
  15. package/dist/{config-HXFJTNLM.js → config-3DGZ47F7.js} +23 -3
  16. package/dist/daemon-ZXV3V5CS.js +384 -0
  17. package/dist/daemon-handle-G4OGPFR3.js +11 -0
  18. package/dist/server-EPSH52DR.js +12 -0
  19. package/dist/web/assets/{Area-CMXvOw33.js → Area-stRnp8Km.js} +1 -1
  20. package/dist/web/assets/analytics-moZZt9L_.js +2 -0
  21. package/dist/web/assets/breakdown-km9w7myU.js +4 -0
  22. package/dist/web/assets/button-DrX04Cc2.js +1 -0
  23. package/dist/web/assets/{chart-cZoLgpmG.js → chart-D0CtuL2k.js} +1 -1
  24. package/dist/web/assets/explore-B11uNJV1.js +22 -0
  25. package/dist/web/assets/index-RDhXsCNG.js +83 -0
  26. package/dist/web/assets/index-ZmOS8p9O.css +1 -0
  27. package/dist/web/assets/models-DYbZnY34.js +2 -0
  28. package/dist/web/assets/overview-CruICbfJ.js +2 -0
  29. package/dist/web/assets/panel-xckM3_GC.js +1 -0
  30. package/dist/web/assets/settings-sheet-G-BTSIF6.js +1 -0
  31. package/dist/web/assets/share-sheet-weIdd4DW.js +2 -0
  32. package/dist/web/assets/timeline-BHgniliZ.js +1 -0
  33. package/dist/web/index.html +30 -4
  34. package/package.json +13 -4
  35. package/site/THIRD_PARTY_NOTICES.md +83 -0
  36. package/site/public/fonts/OFL.txt +93 -0
  37. package/dist/chunk-44ZSQEOS.js +0 -299
  38. package/dist/chunk-HP5UZCXP.js +0 -436
  39. package/dist/chunk-J3JW3RFP.js +0 -196
  40. package/dist/chunk-M7XMV36F.js +0 -275
  41. package/dist/daemon-6Y4O7NXS.js +0 -241
  42. package/dist/daemon-handle-GVCYVDPO.js +0 -10
  43. package/dist/server-GXXOWUVZ.js +0 -11
  44. package/dist/web/assets/analytics-DqdZXOJL.js +0 -2
  45. package/dist/web/assets/breakdown-kOSSJ3mI.js +0 -4
  46. package/dist/web/assets/explore-vcsM8K9I.js +0 -22
  47. package/dist/web/assets/index-BErCXT7f.css +0 -1
  48. package/dist/web/assets/index-DH3dOnbg.js +0 -83
  49. package/dist/web/assets/models-B5ikm83_.js +0 -2
  50. package/dist/web/assets/overview-DD1kP8CP.js +0 -2
  51. package/dist/web/assets/panel-DTQBwExW.js +0 -1
  52. package/dist/web/assets/primitives-C36qJlA2.js +0 -1
  53. package/dist/web/assets/settings-sheet-_i2zTQjf.js +0 -1
  54. package/dist/web/assets/share-sheet-BLHmrFbm.js +0 -2
  55. package/dist/web/assets/timeline-B4uG-Emo.js +0 -1
  56. package/dist/web/assets/use-dialog-trap-bfaXEMz9.js +0 -1
@@ -1,7 +1,10 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
- PROVIDER_IDS
4
- } from "./chunk-HP5UZCXP.js";
3
+ BUILT_IN_THEME_PRESET_IDS,
4
+ DESKTOP_GRAPH_RANGES,
5
+ PROVIDER_IDS,
6
+ THEME_PRESET_IDS
7
+ } from "./chunk-HNWEJ6MS.js";
5
8
 
6
9
  // src/rpc/contract.ts
7
10
  import { Schema } from "effect";
@@ -9,7 +12,7 @@ import * as Rpc from "effect/unstable/rpc/Rpc";
9
12
  import * as RpcGroup from "effect/unstable/rpc/RpcGroup";
10
13
  var TOKMON_WS_PATH = "/ws";
11
14
  var TOKMON_PROTOCOL_VERSION = 3;
12
- var TOKMON_CAPABILITIES = ["config-cas", "config-revision", "allowed-hosts"];
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"];
13
16
  var TOKMON_WS_METHODS = {
14
17
  getConfig: "tokmon.getConfig",
15
18
  setConfig: "tokmon.setConfig",
@@ -35,6 +38,65 @@ var AccountSchema = Schema.Struct({
35
38
  homeDir: Schema.String,
36
39
  color: Schema.optionalKey(Schema.String)
37
40
  });
41
+ var TrayConfigSchema = Schema.Struct({
42
+ enabled: Schema.Boolean,
43
+ showMenuBarText: Schema.Boolean,
44
+ displayMetric: Schema.Literals(["smartHeadroom", "tightestRemaining"]),
45
+ pollIntervalSec: Schema.Finite.check(Schema.isBetween({ minimum: 1, maximum: 86400 })),
46
+ activeTimeoutMin: Schema.Finite.check(Schema.isBetween({ minimum: 1, maximum: 1440 })),
47
+ graceMin: Schema.Finite.check(Schema.isBetween({ minimum: 1, maximum: 10080 })),
48
+ promotionHoldMin: Schema.Finite.check(Schema.isBetween({ minimum: 0, maximum: 1440 })),
49
+ lowWatermarkPct: Schema.Finite.check(Schema.isBetween({ minimum: 0, maximum: 100 })),
50
+ criticalWatermarkPct: Schema.Finite.check(Schema.isBetween({ minimum: 0, maximum: 100 })),
51
+ pinnedAccount: Schema.NullOr(Schema.String),
52
+ pins: Schema.Array(Schema.String),
53
+ // Additive since `tray-pins`: older daemons omit it, so tolerate absence and let
54
+ // `repairConfig` materialise the empty default rather than failing the decode.
55
+ pinnedProviders: Schema.optionalKey(Schema.Array(Schema.String)),
56
+ launchAtLogin: Schema.Boolean,
57
+ theme: Schema.Literal("dark")
58
+ });
59
+ var DesktopConfigSchema = Schema.Struct({
60
+ expandedProviders: Schema.Array(Schema.String),
61
+ graphRangeDays: Schema.optionalKey(Schema.Literals(DESKTOP_GRAPH_RANGES))
62
+ });
63
+ var AccountDetectionConfigSchema = Schema.Struct({
64
+ enabled: Schema.Boolean,
65
+ disabledProviders: Schema.Array(ProviderIdSchema),
66
+ excludedAccounts: Schema.Array(Schema.Struct({
67
+ providerId: ProviderIdSchema,
68
+ homeDir: Schema.String
69
+ }))
70
+ });
71
+ var HexColorSchema = Schema.String.check(Schema.isPattern(/^#[0-9a-f]{6}$/i));
72
+ var ThemeOverridesSchema = Schema.Struct({
73
+ canvas: Schema.optionalKey(HexColorSchema),
74
+ panel: Schema.optionalKey(HexColorSchema),
75
+ inset: Schema.optionalKey(HexColorSchema),
76
+ insetStrong: Schema.optionalKey(HexColorSchema),
77
+ chrome: Schema.optionalKey(HexColorSchema),
78
+ line: Schema.optionalKey(HexColorSchema),
79
+ lineStrong: Schema.optionalKey(HexColorSchema),
80
+ lineFaint: Schema.optionalKey(HexColorSchema),
81
+ text: Schema.optionalKey(HexColorSchema),
82
+ textDim: Schema.optionalKey(HexColorSchema),
83
+ textFaint: Schema.optionalKey(HexColorSchema),
84
+ textStrong: Schema.optionalKey(HexColorSchema),
85
+ accent: Schema.optionalKey(HexColorSchema),
86
+ cost: Schema.optionalKey(HexColorSchema),
87
+ positive: Schema.optionalKey(HexColorSchema)
88
+ });
89
+ var AppearanceConfigSchema = Schema.Struct({
90
+ version: Schema.Literal(1),
91
+ mode: Schema.Literals(["auto", "light", "dark"]),
92
+ preset: Schema.Literals(THEME_PRESET_IDS),
93
+ terminal: Schema.Literals(["ansi", "dark", "light", "off"]),
94
+ custom: Schema.optionalKey(Schema.Struct({
95
+ base: Schema.Literals(BUILT_IN_THEME_PRESET_IDS),
96
+ light: ThemeOverridesSchema,
97
+ dark: ThemeOverridesSchema
98
+ }))
99
+ });
38
100
  var ConfigSchema = Schema.Struct({
39
101
  revision: NonNegativeIntegerSchema,
40
102
  interval: PositiveFiniteSchema,
@@ -53,7 +115,17 @@ var ConfigSchema = Schema.Struct({
53
115
  allowNetworkAccess: Schema.Boolean,
54
116
  allowedHosts: Schema.Array(Schema.String),
55
117
  resetDisplay: Schema.Literals(["relative", "absolute"]),
56
- knownProviders: Schema.Array(ProviderIdSchema)
118
+ knownProviders: Schema.Array(ProviderIdSchema),
119
+ // Additive under `appearance-v1`; omission is accepted for old clients and
120
+ // preserved by the daemon's CAS boundary rather than resetting the theme.
121
+ appearance: Schema.optionalKey(AppearanceConfigSchema),
122
+ // Additive: older clients omit detector policy, so the daemon preserves its
123
+ // current value and repairConfig supplies legacy current-behaviour defaults.
124
+ accountDetection: Schema.optionalKey(AccountDetectionConfigSchema),
125
+ // The complete tray block was added within protocol v3. Accept omission from
126
+ // older peers and normalize it at the client / preserve it at the CAS boundary.
127
+ tray: Schema.optionalKey(TrayConfigSchema),
128
+ desktop: Schema.optionalKey(DesktopConfigSchema)
57
129
  });
58
130
  var ProtocolInfoSchema = Schema.Struct({
59
131
  version: Schema.Literal(TOKMON_PROTOCOL_VERSION),
@@ -70,14 +142,46 @@ var ConfigUpdateRequestSchema = Schema.Struct({
70
142
  expectedRevision: NonNegativeIntegerSchema,
71
143
  config: ConfigSchema
72
144
  });
73
- var ConfigUpdateConflictSchema = Schema.Struct({
145
+ var LegacyConfigUpdateConflictSchema = Schema.Struct({
74
146
  kind: Schema.Literal("conflict"),
75
147
  state: ConfigStateSchema
76
148
  });
77
- var ConfigPersistenceFailureSchema = Schema.Struct({
149
+ var ConfigUpdateConflictFailure = class extends Schema.TaggedErrorClass()(
150
+ "ConfigUpdateConflictFailure",
151
+ {
152
+ kind: Schema.Literal("conflict"),
153
+ state: ConfigStateSchema
154
+ }
155
+ ) {
156
+ };
157
+ var LegacyConfigPersistenceFailureSchema = Schema.Struct({
78
158
  kind: Schema.Literal("persistence"),
79
159
  message: Schema.String
80
160
  });
161
+ var ConfigPersistenceFailure = class extends Schema.TaggedErrorClass()(
162
+ "ConfigPersistenceFailure",
163
+ {
164
+ kind: Schema.Literal("persistence"),
165
+ message: Schema.String
166
+ }
167
+ ) {
168
+ };
169
+ var ConfigUpdateConflictSchema = Schema.Union([
170
+ ConfigUpdateConflictFailure,
171
+ LegacyConfigUpdateConflictSchema
172
+ ]);
173
+ var ConfigPersistenceFailureSchema = Schema.Union([
174
+ ConfigPersistenceFailure,
175
+ LegacyConfigPersistenceFailureSchema
176
+ ]);
177
+ var RefreshFailure = class extends Schema.TaggedErrorClass()(
178
+ "RefreshFailure",
179
+ {
180
+ kind: Schema.Literal("refresh"),
181
+ message: Schema.String
182
+ }
183
+ ) {
184
+ };
81
185
  var FsEntrySchema = Schema.Struct({
82
186
  name: Schema.String,
83
187
  path: Schema.String,
@@ -123,7 +227,8 @@ var DashboardDataSchema = Schema.Struct({
123
227
  week: UsageSummarySchema,
124
228
  month: UsageSummarySchema,
125
229
  burnRate: Schema.Finite,
126
- series: Schema.Array(Schema.Finite)
230
+ series: Schema.Array(Schema.Finite),
231
+ lastActivityAt: Schema.NullOr(NonNegativeIntegerSchema)
127
232
  });
128
233
  var TableDataSchema = Schema.Struct({
129
234
  daily: Schema.Array(TableRowSchema),
@@ -136,6 +241,10 @@ var MetricFormatSchema = Schema.Union([
136
241
  Schema.Struct({ kind: Schema.Literal("count"), suffix: Schema.optionalKey(Schema.String) })
137
242
  ]);
138
243
  var MetricSchema = Schema.Struct({
244
+ key: Schema.optionalKey(Schema.String),
245
+ role: Schema.optionalKey(Schema.Literals(["session", "weekly", "model", "other", "unbounded"])),
246
+ modelId: Schema.optionalKey(Schema.NullOr(Schema.String)),
247
+ active: Schema.optionalKey(Schema.Boolean),
139
248
  label: Schema.String,
140
249
  used: Schema.Finite,
141
250
  limit: Schema.NullOr(Schema.Finite),
@@ -143,6 +252,45 @@ var MetricSchema = Schema.Struct({
143
252
  resetsAt: Schema.optionalKey(Schema.NullOr(Schema.String)),
144
253
  primary: Schema.optionalKey(Schema.Boolean)
145
254
  });
255
+ var MetricRoleSchema = Schema.Literals(["session", "weekly", "model", "other", "unbounded"]);
256
+ var AccountIdentityViewSchema = Schema.Struct({
257
+ title: Schema.String,
258
+ subtitle: Schema.NullOr(Schema.String),
259
+ accessibleLabel: Schema.String,
260
+ redacted: Schema.Boolean
261
+ });
262
+ var QuotaViewSchema = Schema.Struct({
263
+ key: Schema.String,
264
+ label: Schema.String,
265
+ role: MetricRoleSchema,
266
+ modelId: Schema.NullOr(Schema.String),
267
+ usedPct: Schema.NullOr(Schema.Finite),
268
+ remainingPct: Schema.NullOr(Schema.Finite),
269
+ resetsAt: Schema.NullOr(NonNegativeIntegerSchema),
270
+ bounded: Schema.Boolean,
271
+ primary: Schema.Boolean,
272
+ active: Schema.Boolean,
273
+ displayOrder: NonNegativeIntegerSchema,
274
+ valueText: Schema.String
275
+ });
276
+ var HeadroomFactorSchema = Schema.Struct({
277
+ key: Schema.String,
278
+ label: Schema.String,
279
+ role: MetricRoleSchema,
280
+ remainingPct: Schema.Finite,
281
+ included: Schema.Boolean,
282
+ reason: Schema.Literals(["session", "active-model", "weekly-cap", "primary", "fallback-floor"])
283
+ });
284
+ var HeadroomViewSchema = Schema.Struct({
285
+ value: Schema.NullOr(Schema.Finite),
286
+ unit: Schema.Literal("index-100"),
287
+ mode: Schema.Literals(["smart", "single-window", "fallback-floor", "unavailable"]),
288
+ basis: Schema.Literals(["active", "idle-runway", "unavailable"]),
289
+ representativeAccountId: Schema.NullOr(Schema.String),
290
+ activeAccountIds: Schema.Array(Schema.String),
291
+ factors: Schema.Array(HeadroomFactorSchema),
292
+ explanation: Schema.String
293
+ });
146
294
  var BillingResultSchema = Schema.Struct({
147
295
  plan: Schema.NullOr(Schema.String),
148
296
  metrics: Schema.Array(MetricSchema),
@@ -158,7 +306,7 @@ var BillingResultSchema = Schema.Struct({
158
306
  usd: Schema.Finite,
159
307
  requests: Schema.Finite
160
308
  })))),
161
- asOfMs: Schema.optionalKey(Schema.Finite)
309
+ asOfMs: Schema.optionalKey(NonNegativeIntegerSchema)
162
310
  });
163
311
  var WebAccountSchema = Schema.Struct({
164
312
  id: Schema.String,
@@ -170,7 +318,11 @@ var WebAccountSchema = Schema.Struct({
170
318
  hasBilling: Schema.Boolean,
171
319
  email: Schema.optionalKey(Schema.NullOr(Schema.String)),
172
320
  displayName: Schema.optionalKey(Schema.NullOr(Schema.String)),
321
+ identity: Schema.optionalKey(AccountIdentityViewSchema),
322
+ quotas: Schema.optionalKey(Schema.Array(QuotaViewSchema)),
323
+ headroom: Schema.optionalKey(HeadroomViewSchema),
173
324
  plan: Schema.optionalKey(Schema.NullOr(Schema.String)),
325
+ lastActivityAt: Schema.NullOr(NonNegativeIntegerSchema),
174
326
  dashboard: Schema.NullOr(DashboardDataSchema),
175
327
  table: Schema.NullOr(TableDataSchema),
176
328
  billing: Schema.NullOr(BillingResultSchema),
@@ -190,7 +342,8 @@ var WebSnapshotSchema = Schema.Struct({
190
342
  providers: Schema.Array(Schema.Struct({
191
343
  id: ProviderIdSchema,
192
344
  name: Schema.String,
193
- color: Schema.String
345
+ color: Schema.String,
346
+ headroom: Schema.optionalKey(HeadroomViewSchema)
194
347
  })),
195
348
  accounts: Schema.Array(WebAccountSchema),
196
349
  seeded: Schema.Boolean,
@@ -215,7 +368,8 @@ var SetConfigRpc = Rpc.make(TOKMON_WS_METHODS.setConfig, {
215
368
  });
216
369
  var RefreshRpc = Rpc.make(TOKMON_WS_METHODS.refresh, {
217
370
  payload: Schema.Struct({ scope: RefreshScopeSchema }),
218
- success: Schema.Void
371
+ success: Schema.Void,
372
+ error: RefreshFailure
219
373
  });
220
374
  var BrowseFsRpc = Rpc.make(TOKMON_WS_METHODS.browseFs, {
221
375
  payload: Schema.Struct({ path: Schema.String }),
@@ -245,5 +399,8 @@ export {
245
399
  TOKMON_PROTOCOL_VERSION,
246
400
  TOKMON_CAPABILITIES,
247
401
  TOKMON_WS_METHODS,
402
+ ConfigUpdateConflictFailure,
403
+ ConfigPersistenceFailure,
404
+ RefreshFailure,
248
405
  TokmonRpcGroup
249
406
  };
@@ -1,23 +1,32 @@
1
1
  #!/usr/bin/env node
2
+ import {
3
+ resolveDaemonChannel
4
+ } from "./chunk-UYPDMVW5.js";
2
5
  import {
3
6
  buildAccounts,
4
7
  colorHex,
8
+ deriveAccountIdentity,
9
+ deriveProviderHeadroom,
10
+ deriveQuotaViews,
5
11
  fetchPeak,
6
12
  namedHex
7
- } from "./chunk-44ZSQEOS.js";
13
+ } from "./chunk-TN6V7XSL.js";
8
14
  import {
9
15
  PROVIDERS,
10
16
  detectProviders,
11
17
  resolveTimezone,
12
18
  withTimeout
13
- } from "./chunk-LCAHTRGP.js";
19
+ } from "./chunk-CFKUZCMB.js";
14
20
  import {
21
+ ConfigPersistenceFailure,
22
+ ConfigUpdateConflictFailure,
23
+ RefreshFailure,
15
24
  TOKMON_CAPABILITIES,
16
25
  TOKMON_PROTOCOL_VERSION,
17
26
  TOKMON_WS_METHODS,
18
27
  TOKMON_WS_PATH,
19
28
  TokmonRpcGroup
20
- } from "./chunk-WUK3MQUB.js";
29
+ } from "./chunk-JZSZEHVD.js";
21
30
  import {
22
31
  cacheDir,
23
32
  expandHome,
@@ -25,7 +34,7 @@ import {
25
34
  normalizeConfig,
26
35
  saveConfig,
27
36
  snapshotCacheFile
28
- } from "./chunk-HP5UZCXP.js";
37
+ } from "./chunk-HNWEJ6MS.js";
29
38
 
30
39
  // src/web/server.ts
31
40
  import { createServer } from "http";
@@ -61,9 +70,13 @@ async function fetchAccountBilling(account, tz) {
61
70
  return p.fetchBilling(account, tz);
62
71
  }
63
72
  function assembleSnapshot(opts) {
73
+ const providerOrdinals = /* @__PURE__ */ new Map();
64
74
  const accounts = opts.resolved.map((r) => {
65
75
  const u = opts.usage.get(r.account.id);
66
76
  const billing = opts.billing.get(r.account.id) ?? null;
77
+ const ordinal = (providerOrdinals.get(r.account.providerId) ?? 0) + 1;
78
+ providerOrdinals.set(r.account.providerId, ordinal);
79
+ const quotas = deriveQuotaViews(billing?.metrics ?? []);
67
80
  return {
68
81
  id: r.account.id,
69
82
  providerId: r.account.providerId,
@@ -74,7 +87,22 @@ function assembleSnapshot(opts) {
74
87
  hasBilling: r.hasBilling,
75
88
  email: billing?.email ?? null,
76
89
  displayName: billing?.displayName ?? null,
90
+ identity: deriveAccountIdentity({
91
+ name: r.account.name,
92
+ email: billing?.email,
93
+ displayName: billing?.displayName,
94
+ providerName: PROVIDERS[r.account.providerId].name,
95
+ ordinal,
96
+ privacyMode: opts.config.privacyMode
97
+ }),
98
+ quotas,
99
+ headroom: deriveProviderHeadroom([{
100
+ id: r.account.id,
101
+ lastActivityAt: u?.dashboard?.lastActivityAt ?? null,
102
+ quotas
103
+ }], opts.config.tray.activeTimeoutMin, Date.now(), opts.config.tray.displayMetric),
77
104
  plan: billing?.plan ?? null,
105
+ lastActivityAt: u?.dashboard?.lastActivityAt ?? null,
78
106
  dashboard: u?.dashboard ?? null,
79
107
  table: u?.table ?? null,
80
108
  billing,
@@ -86,6 +114,12 @@ function assembleSnapshot(opts) {
86
114
  tableUpdatedAt: opts.tableUpdatedAt?.get(r.account.id) ?? null
87
115
  };
88
116
  });
117
+ const accountsByProvider = /* @__PURE__ */ new Map();
118
+ for (const account of accounts) {
119
+ const group = accountsByProvider.get(account.providerId) ?? [];
120
+ group.push(account);
121
+ accountsByProvider.set(account.providerId, group);
122
+ }
89
123
  const seen = /* @__PURE__ */ new Set();
90
124
  const providers = [];
91
125
  for (const r of opts.resolved) {
@@ -94,7 +128,17 @@ function assembleSnapshot(opts) {
94
128
  providers.push({
95
129
  id: r.account.providerId,
96
130
  name: PROVIDERS[r.account.providerId].name,
97
- color: namedHex(PROVIDERS[r.account.providerId].color)
131
+ color: namedHex(PROVIDERS[r.account.providerId].color),
132
+ headroom: deriveProviderHeadroom(
133
+ (accountsByProvider.get(r.account.providerId) ?? []).map((account) => ({
134
+ id: account.id,
135
+ lastActivityAt: account.lastActivityAt,
136
+ quotas: account.quotas ?? []
137
+ })),
138
+ opts.config.tray.activeTimeoutMin,
139
+ Date.now(),
140
+ opts.config.tray.displayMetric
141
+ )
98
142
  });
99
143
  }
100
144
  return {
@@ -238,6 +282,7 @@ function isDevMode() {
238
282
  const forced = process.env.TOKMON_WEB_MODE;
239
283
  if (forced === "dev") return true;
240
284
  if (forced === "prod") return false;
285
+ if (process.env.NODE_TEST_CONTEXT) return false;
241
286
  return import.meta.url.includes("/src/");
242
287
  }
243
288
  function findWebSource() {
@@ -442,7 +487,8 @@ function createDataEngine(opts) {
442
487
  billingUpdatedAt,
443
488
  tableUpdatedAt,
444
489
  seeded,
445
- peak
490
+ peak,
491
+ config: currentConfig
446
492
  });
447
493
  const hydrateFromCache = () => {
448
494
  try {
@@ -450,7 +496,11 @@ function createDataEngine(opts) {
450
496
  if (!cached || !Array.isArray(cached.accounts)) return;
451
497
  for (const a of cached.accounts) {
452
498
  if (a.dashboard || a.table) {
453
- usage.set(a.id, { dashboard: a.dashboard, table: a.table });
499
+ const dashboard = a.dashboard ? {
500
+ ...a.dashboard,
501
+ lastActivityAt: typeof a.dashboard.lastActivityAt === "number" && Number.isSafeInteger(a.dashboard.lastActivityAt) && a.dashboard.lastActivityAt >= 0 ? a.dashboard.lastActivityAt : null
502
+ } : null;
503
+ usage.set(a.id, { dashboard, table: a.table });
454
504
  if (a.dashboard) {
455
505
  summaryState.set(a.id, "ready");
456
506
  if (typeof a.summaryUpdatedAt === "number") summaryUpdatedAt.set(a.id, a.summaryUpdatedAt);
@@ -689,6 +739,7 @@ function createDataEngine(opts) {
689
739
  broadcastConfig(config) {
690
740
  if (stopped) return;
691
741
  currentConfig = config;
742
+ rebuild();
692
743
  for (const onConfig of configSubscribers) {
693
744
  try {
694
745
  onConfig(config);
@@ -776,23 +827,51 @@ async function resolveEngineConfig(config) {
776
827
  billingIntervalMs: billingIntervalFor(config)
777
828
  };
778
829
  }
830
+ function configAffectsEngine(previous, next) {
831
+ return previous.interval !== next.interval || previous.billingInterval !== next.billingInterval || previous.timezone !== next.timezone || JSON.stringify(previous.accounts) !== JSON.stringify(next.accounts) || JSON.stringify(previous.disabledProviders) !== JSON.stringify(next.disabledProviders) || JSON.stringify(previous.accountDetection) !== JSON.stringify(next.accountDetection);
832
+ }
779
833
  var updateQueues = /* @__PURE__ */ new WeakMap();
834
+ function hasOwn(value, key) {
835
+ return Object.prototype.hasOwnProperty.call(value, key);
836
+ }
837
+ function mergeCapabilityFields(incomingConfig, current) {
838
+ const incoming = incomingConfig;
839
+ const incomingTray = incoming.tray;
840
+ const incomingDesktop = incoming.desktop;
841
+ return {
842
+ ...incoming,
843
+ appearance: hasOwn(incoming, "appearance") ? incoming.appearance : current.appearance,
844
+ accountDetection: hasOwn(incoming, "accountDetection") ? incoming.accountDetection : current.accountDetection,
845
+ tray: hasOwn(incoming, "tray") && incomingTray ? {
846
+ ...current.tray,
847
+ ...incomingTray,
848
+ pinnedProviders: hasOwn(incomingTray, "pinnedProviders") ? incomingTray.pinnedProviders : current.tray.pinnedProviders
849
+ } : current.tray,
850
+ desktop: hasOwn(incoming, "desktop") && incomingDesktop ? {
851
+ ...current.desktop,
852
+ ...incomingDesktop,
853
+ expandedProviders: hasOwn(incomingDesktop, "expandedProviders") ? incomingDesktop.expandedProviders : current.desktop.expandedProviders,
854
+ graphRangeDays: hasOwn(incomingDesktop, "graphRangeDays") ? incomingDesktop.graphRangeDays : current.desktop.graphRangeDays
855
+ } : current.desktop
856
+ };
857
+ }
780
858
  async function applyConfigUpdateUnlocked(engine, state, input) {
781
859
  if (input.expectedRevision !== state.config.revision) {
782
860
  throw new ConfigConflictError(toConfigState(state.config));
783
861
  }
784
862
  const normalized = normalizeConfig({
785
- ...input.config,
863
+ ...mergeCapabilityFields(input.config, state.config),
786
864
  revision: state.config.revision + 1
787
865
  });
788
- const engineConfig = await resolveEngineConfig(normalized);
866
+ const reconfigureEngine = configAffectsEngine(state.config, normalized);
867
+ const engineConfig = reconfigureEngine ? await resolveEngineConfig(normalized) : null;
789
868
  try {
790
869
  await saveConfig(normalized);
791
870
  } catch (error) {
792
871
  throw new ConfigPersistenceError(error);
793
872
  }
794
873
  state.config = normalized;
795
- engine.setConfig(engineConfig);
874
+ if (engineConfig) engine.setConfig(engineConfig);
796
875
  engine.broadcastConfig(normalized);
797
876
  return toConfigState(normalized);
798
877
  }
@@ -958,27 +1037,45 @@ function configStream(engine) {
958
1037
  yield* Scope.addFinalizer(scope, Effect.sync(unsubscribe));
959
1038
  }), { bufferSize: 16, strategy: "sliding" });
960
1039
  }
961
- function toConfigUpdateFailure(error) {
1040
+ function configUpdateFailureEffect(error) {
962
1041
  if (error instanceof ConfigConflictError) {
963
- return { kind: "conflict", state: error.state };
1042
+ return Effect.fail(new ConfigUpdateConflictFailure({ kind: "conflict", state: error.state }));
964
1043
  }
965
1044
  if (error instanceof ConfigPersistenceError) {
966
- return { kind: "persistence", message: error.message };
1045
+ return Effect.fail(new ConfigPersistenceFailure({ kind: "persistence", message: error.message }));
967
1046
  }
968
- return { kind: "persistence", message: error instanceof Error ? error.message : "config update failed" };
1047
+ return Effect.die(error);
1048
+ }
1049
+ function configUpdateEffect(engine, state, input) {
1050
+ return Effect.tryPromise({
1051
+ try: () => applyConfigUpdate(engine, state, input),
1052
+ catch: (error) => error
1053
+ }).pipe(Effect.matchEffect({
1054
+ onFailure: configUpdateFailureEffect,
1055
+ onSuccess: Effect.succeed
1056
+ }));
1057
+ }
1058
+ function refreshFailureEffect(error) {
1059
+ return error instanceof AggregateError ? Effect.fail(new RefreshFailure({ kind: "refresh", message: error.message })) : Effect.die(error);
1060
+ }
1061
+ function refreshEffect(engine, scope) {
1062
+ return Effect.tryPromise({
1063
+ try: () => engine.refresh(scope),
1064
+ catch: (error) => error
1065
+ }).pipe(Effect.matchEffect({
1066
+ onFailure: refreshFailureEffect,
1067
+ onSuccess: Effect.succeed
1068
+ }));
969
1069
  }
970
1070
  async function mountWsRpc(server, deps) {
971
1071
  const scope = await Effect.runPromise(Scope.make());
972
1072
  const wss = new NodeWS.WebSocketServer({ noServer: true });
973
1073
  const handlersLayer = TokmonRpcGroup.toLayer(
974
1074
  TokmonRpcGroup.of({
975
- [TOKMON_WS_METHODS.getConfig]: () => Effect.tryPromise(() => Promise.resolve(deps.state.config ?? loadConfig()).then(toConfigState)),
976
- [TOKMON_WS_METHODS.setConfig]: (config) => Effect.tryPromise({
977
- try: () => applyConfigUpdate(deps.engine, deps.state, config),
978
- catch: toConfigUpdateFailure
979
- }),
980
- [TOKMON_WS_METHODS.refresh]: ({ scope: scope2 }) => Effect.promise(() => deps.engine.refresh(scope2)),
981
- [TOKMON_WS_METHODS.browseFs]: ({ path }) => Effect.tryPromise(() => listHomeDirectory(path)),
1075
+ [TOKMON_WS_METHODS.getConfig]: () => Effect.promise(() => Promise.resolve(deps.state.config ?? loadConfig()).then(toConfigState)),
1076
+ [TOKMON_WS_METHODS.setConfig]: (config) => configUpdateEffect(deps.engine, deps.state, config),
1077
+ [TOKMON_WS_METHODS.refresh]: ({ scope: scope2 }) => refreshEffect(deps.engine, scope2),
1078
+ [TOKMON_WS_METHODS.browseFs]: ({ path }) => Effect.promise(() => listHomeDirectory(path)),
982
1079
  [TOKMON_WS_METHODS.snapshot]: () => snapshotStream(deps.engine),
983
1080
  [TOKMON_WS_METHODS.config]: () => configStream(deps.engine).pipe(Stream.map(toConfigState))
984
1081
  })
@@ -1068,7 +1165,7 @@ function guardSameOrigin(req, res) {
1068
1165
  }
1069
1166
  return true;
1070
1167
  }
1071
- function createRouter(engine, state, vite, webRoot, wsToken, version, protocolVersion, capabilities, ownerKind) {
1168
+ function createRouter(engine, state, vite, webRoot, wsToken, version, protocolVersion, capabilities, ownerKind, channel) {
1072
1169
  return (req, res) => {
1073
1170
  const url = req.url || "/";
1074
1171
  const path = url.split("?")[0];
@@ -1087,6 +1184,7 @@ function createRouter(engine, state, vite, webRoot, wsToken, version, protocolVe
1087
1184
  protocolVersion,
1088
1185
  capabilities,
1089
1186
  ownerKind,
1187
+ channel,
1090
1188
  // Discovery requires this proof; public health checks retain their useful 200 response.
1091
1189
  owner: tokenMatches(header2(req, "x-tokmon-token"), wsToken)
1092
1190
  });
@@ -1117,10 +1215,11 @@ function createRouter(engine, state, vite, webRoot, wsToken, version, protocolVe
1117
1215
  async function startWebServer(opts) {
1118
1216
  const state = { config: opts.config };
1119
1217
  const tz = tzFor(state.config);
1120
- const version = appVersion();
1218
+ const version = opts.version ?? appVersion();
1121
1219
  const protocolVersion = opts.protocolVersion ?? TOKMON_PROTOCOL_VERSION;
1122
1220
  const capabilities = opts.capabilities ?? TOKMON_CAPABILITIES;
1123
1221
  const ownerKind = opts.ownerKind ?? "cli";
1222
+ const channel = resolveDaemonChannel(opts.channel);
1124
1223
  const summaryIntervalMs = summaryIntervalFor(state.config);
1125
1224
  const billingIntervalMs = billingIntervalFor(state.config);
1126
1225
  const wsToken = opts.wsToken ?? randomBytes(32).toString("base64url");
@@ -1134,7 +1233,7 @@ async function startWebServer(opts) {
1134
1233
  let closeWsRpc = null;
1135
1234
  try {
1136
1235
  if (isDevMode()) vite = await createViteDevServer(server, log);
1137
- const webRoot = vite ? null : findWebRoot();
1236
+ const webRoot = vite ? null : opts.webRoot ?? findWebRoot();
1138
1237
  if (!vite && !webRoot) log(" \u26A0 no dashboard available \u2014 see the page for build/dev instructions");
1139
1238
  engine = createDataEngine({ version, config: state.config, tz, summaryIntervalMs, billingIntervalMs, resolved });
1140
1239
  server.addListener("request", createRouter(
@@ -1146,7 +1245,8 @@ async function startWebServer(opts) {
1146
1245
  version,
1147
1246
  protocolVersion,
1148
1247
  capabilities,
1149
- ownerKind
1248
+ ownerKind,
1249
+ channel
1150
1250
  ));
1151
1251
  closeWsRpc = await mountWsRpc(server, { engine, state });
1152
1252
  const bindHost = state.config.allowNetworkAccess ? NETWORK_HOST : LOOPBACK_HOST;