tokmon 0.29.3 → 0.29.5

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 (35) hide show
  1. package/README.md +2 -1
  2. package/dist/{bootstrap-ink-MPWICEIJ.js → bootstrap-ink-VORCPVK4.js} +38 -21
  3. package/dist/{chunk-AUUBRLBG.js → chunk-3SXVUTV2.js} +2 -2
  4. package/dist/{chunk-BLUQGJKM.js → chunk-6NJLM5IZ.js} +2 -2
  5. package/dist/{chunk-BYLWQ46U.js → chunk-6RPYSOBA.js} +6 -3
  6. package/dist/{chunk-WPZTPMBX.js → chunk-EZHV4R74.js} +20 -8
  7. package/dist/{chunk-G2A3DVPU.js → chunk-GQCFGAQ6.js} +40 -10
  8. package/dist/{chunk-5Q5HUOP4.js → chunk-LAJV5JMV.js} +160 -133
  9. package/dist/{chunk-3D6TO5TS.js → chunk-U2FQXNIU.js} +98 -66
  10. package/dist/{chunk-YWDMEU3L.js → chunk-UD2IPQK3.js} +1 -1
  11. package/dist/{cli-command-WL2K5WWR.js → cli-command-X75NGQON.js} +6 -6
  12. package/dist/cli.js +6 -6
  13. package/dist/{config-B7QJQTU2.js → config-4JC6X5QT.js} +3 -1
  14. package/dist/{daemon-A3WGDRFT.js → daemon-O7TOISBH.js} +6 -6
  15. package/dist/daemon-handle-NBGZUDKE.js +11 -0
  16. package/dist/server-VWBW53QU.js +12 -0
  17. package/dist/web/assets/{Area-CEpf7_6d.js → Area-B3FdWS80.js} +1 -1
  18. package/dist/web/assets/{analytics-CdjcYtmK.js → analytics-DSLBg5Lx.js} +2 -2
  19. package/dist/web/assets/{breakdown-DM6g0evr.js → breakdown-B1vg0Xn1.js} +1 -1
  20. package/dist/web/assets/{button-Cu4co7ZR.js → button-CDipMD0w.js} +1 -1
  21. package/dist/web/assets/{chart-YlnVH1If.js → chart-BRXIwVtE.js} +1 -1
  22. package/dist/web/assets/{explore-kLpZWR-_.js → explore-DyJiyG0m.js} +1 -1
  23. package/dist/web/assets/index-CEXpTvdh.js +83 -0
  24. package/dist/web/assets/{models-XKfoWTDv.js → models-_DeHctpR.js} +2 -2
  25. package/dist/web/assets/{overview-DC-WVKZ6.js → overview-CmPN-0w3.js} +2 -2
  26. package/dist/web/assets/{panel-B_U9oe1x.js → panel-DzILyYlc.js} +1 -1
  27. package/dist/web/assets/settings-sheet-AfoLwoCy.js +1 -0
  28. package/dist/web/assets/{share-sheet-BaEXe1m9.js → share-sheet-Cfl7PIrk.js} +1 -1
  29. package/dist/web/assets/{timeline-ar9mdhcb.js → timeline-D_JPTxAw.js} +1 -1
  30. package/dist/web/index.html +1 -1
  31. package/package.json +1 -1
  32. package/dist/daemon-handle-4E7RSSDB.js +0 -11
  33. package/dist/server-P6BRXBZ5.js +0 -12
  34. package/dist/web/assets/index-Dd37pBKy.js +0 -83
  35. package/dist/web/assets/settings-sheet-B7BuJ3j8.js +0 -1
@@ -10,13 +10,14 @@ import {
10
10
  deriveQuotaViews,
11
11
  fetchPeak,
12
12
  namedHex
13
- } from "./chunk-3D6TO5TS.js";
13
+ } from "./chunk-U2FQXNIU.js";
14
14
  import {
15
15
  PROVIDERS,
16
- detectProviders,
16
+ detectAccountProviders,
17
+ detectInstalledProviders,
17
18
  resolveTimezone,
18
19
  withTimeout
19
- } from "./chunk-WPZTPMBX.js";
20
+ } from "./chunk-EZHV4R74.js";
20
21
  import {
21
22
  ConfigPersistenceFailure,
22
23
  ConfigUpdateConflictFailure,
@@ -26,137 +27,21 @@ import {
26
27
  TOKMON_WS_METHODS,
27
28
  TOKMON_WS_PATH,
28
29
  TokmonRpcGroup
29
- } from "./chunk-BYLWQ46U.js";
30
+ } from "./chunk-6RPYSOBA.js";
30
31
  import {
31
32
  cacheDir,
33
+ canonicalizeConfigHomeRefs,
32
34
  expandHome,
33
35
  loadConfig,
34
36
  normalizeConfig,
35
37
  saveConfig,
36
38
  snapshotCacheFile
37
- } from "./chunk-G2A3DVPU.js";
39
+ } from "./chunk-GQCFGAQ6.js";
38
40
 
39
41
  // src/web/server.ts
40
42
  import { createServer } from "http";
41
43
  import { randomBytes, timingSafeEqual } from "crypto";
42
44
 
43
- // src/web/data.ts
44
- async function resolveAccounts(config) {
45
- const detected = await detectProviders();
46
- const accounts = buildAccounts(config, detected);
47
- return accounts.map((a) => {
48
- const p = PROVIDERS[a.providerId];
49
- return {
50
- account: a,
51
- hasUsage: p.hasUsage || !!p.fetchTable,
52
- hasBilling: p.hasBilling,
53
- color: colorHex(a.color, PROVIDERS[a.providerId].color)
54
- };
55
- });
56
- }
57
- async function fetchAccountSummary(account, tz) {
58
- const p = PROVIDERS[account.providerId];
59
- if (!p.fetchSummary) return null;
60
- return p.fetchSummary(account, tz);
61
- }
62
- async function fetchAccountTable(account, tz) {
63
- const p = PROVIDERS[account.providerId];
64
- if (!p.fetchTable) return null;
65
- return p.fetchTable(account, tz);
66
- }
67
- async function fetchAccountBilling(account, tz) {
68
- const p = PROVIDERS[account.providerId];
69
- if (!p.fetchBilling) return null;
70
- return p.fetchBilling(account, tz);
71
- }
72
- function assembleSnapshot(opts) {
73
- const providerOrdinals = /* @__PURE__ */ new Map();
74
- const accounts = opts.resolved.map((r) => {
75
- const u = opts.usage.get(r.account.id);
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 ?? []);
80
- return {
81
- id: r.account.id,
82
- providerId: r.account.providerId,
83
- name: r.account.name,
84
- color: r.color,
85
- homeDir: r.account.homeDir ?? null,
86
- hasUsage: r.hasUsage,
87
- hasBilling: r.hasBilling,
88
- email: billing?.email ?? null,
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),
104
- plan: billing?.plan ?? null,
105
- lastActivityAt: u?.dashboard?.lastActivityAt ?? null,
106
- dashboard: u?.dashboard ?? null,
107
- table: u?.table ?? null,
108
- billing,
109
- summaryState: opts.summaryState?.get(r.account.id) ?? "pending",
110
- billingState: opts.billingState?.get(r.account.id) ?? "pending",
111
- tableState: opts.tableState?.get(r.account.id) ?? "pending",
112
- summaryUpdatedAt: opts.summaryUpdatedAt?.get(r.account.id) ?? null,
113
- billingUpdatedAt: opts.billingUpdatedAt?.get(r.account.id) ?? null,
114
- tableUpdatedAt: opts.tableUpdatedAt?.get(r.account.id) ?? null
115
- };
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
- }
123
- const seen = /* @__PURE__ */ new Set();
124
- const providers = [];
125
- for (const r of opts.resolved) {
126
- if (seen.has(r.account.providerId)) continue;
127
- seen.add(r.account.providerId);
128
- providers.push({
129
- id: r.account.providerId,
130
- name: PROVIDERS[r.account.providerId].name,
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
- )
142
- });
143
- }
144
- return {
145
- version: opts.version,
146
- generatedAt: Date.now(),
147
- tz: opts.tz,
148
- intervalMs: opts.intervalMs,
149
- billingIntervalMs: opts.billingIntervalMs,
150
- providers,
151
- accounts,
152
- seeded: opts.seeded ?? false,
153
- peak: opts.peak ?? null
154
- };
155
- }
156
- function tzFor(config) {
157
- return resolveTimezone(config.timezone);
158
- }
159
-
160
45
  // src/web/static.ts
161
46
  import { createReadStream, existsSync, readFileSync } from "fs";
162
47
  import { stat } from "fs/promises";
@@ -335,6 +220,125 @@ code{color:#e6b450}</style></head><body>
335
220
  // src/web/data-engine.ts
336
221
  import { readFileSync as readFileSync2, writeFileSync, mkdirSync, renameSync } from "fs";
337
222
 
223
+ // src/web/data.ts
224
+ async function resolveAccounts(config) {
225
+ const detected = await detectAccountProviders();
226
+ const accounts = buildAccounts(config, detected);
227
+ return accounts.map((a) => {
228
+ const p = PROVIDERS[a.providerId];
229
+ return {
230
+ account: a,
231
+ hasUsage: p.hasUsage || !!p.fetchTable,
232
+ hasBilling: p.hasBilling,
233
+ color: colorHex(a.color, PROVIDERS[a.providerId].color)
234
+ };
235
+ });
236
+ }
237
+ async function fetchAccountSummary(account, tz) {
238
+ const p = PROVIDERS[account.providerId];
239
+ if (!p.fetchSummary) return null;
240
+ return p.fetchSummary(account, tz);
241
+ }
242
+ async function fetchAccountTable(account, tz) {
243
+ const p = PROVIDERS[account.providerId];
244
+ if (!p.fetchTable) return null;
245
+ return p.fetchTable(account, tz);
246
+ }
247
+ async function fetchAccountBilling(account, tz) {
248
+ const p = PROVIDERS[account.providerId];
249
+ if (!p.fetchBilling) return null;
250
+ return p.fetchBilling(account, tz);
251
+ }
252
+ function assembleSnapshot(opts) {
253
+ const providerOrdinals = /* @__PURE__ */ new Map();
254
+ const accounts = opts.resolved.map((r) => {
255
+ const u = opts.usage.get(r.account.id);
256
+ const billing = opts.billing.get(r.account.id) ?? null;
257
+ const ordinal = (providerOrdinals.get(r.account.providerId) ?? 0) + 1;
258
+ providerOrdinals.set(r.account.providerId, ordinal);
259
+ const quotas = deriveQuotaViews(billing?.metrics ?? []);
260
+ return {
261
+ id: r.account.id,
262
+ providerId: r.account.providerId,
263
+ name: r.account.name,
264
+ color: r.color,
265
+ homeDir: r.account.homeDir ?? null,
266
+ source: r.account.source ?? "auto",
267
+ hasUsage: r.hasUsage,
268
+ hasBilling: r.hasBilling,
269
+ email: billing?.email ?? null,
270
+ displayName: billing?.displayName ?? null,
271
+ identity: deriveAccountIdentity({
272
+ name: r.account.name,
273
+ email: billing?.email,
274
+ displayName: billing?.displayName,
275
+ providerName: PROVIDERS[r.account.providerId].name,
276
+ ordinal,
277
+ privacyMode: opts.config.privacyMode
278
+ }),
279
+ quotas,
280
+ headroom: deriveProviderHeadroom([{
281
+ id: r.account.id,
282
+ lastActivityAt: u?.dashboard?.lastActivityAt ?? null,
283
+ quotas
284
+ }], opts.config.tray.activeTimeoutMin, Date.now(), opts.config.tray.displayMetric),
285
+ plan: billing?.plan ?? null,
286
+ lastActivityAt: u?.dashboard?.lastActivityAt ?? null,
287
+ dashboard: u?.dashboard ?? null,
288
+ table: u?.table ?? null,
289
+ billing,
290
+ summaryState: opts.summaryState?.get(r.account.id) ?? "pending",
291
+ billingState: opts.billingState?.get(r.account.id) ?? "pending",
292
+ tableState: opts.tableState?.get(r.account.id) ?? "pending",
293
+ summaryUpdatedAt: opts.summaryUpdatedAt?.get(r.account.id) ?? null,
294
+ billingUpdatedAt: opts.billingUpdatedAt?.get(r.account.id) ?? null,
295
+ tableUpdatedAt: opts.tableUpdatedAt?.get(r.account.id) ?? null
296
+ };
297
+ });
298
+ const accountsByProvider = /* @__PURE__ */ new Map();
299
+ for (const account of accounts) {
300
+ const group = accountsByProvider.get(account.providerId) ?? [];
301
+ group.push(account);
302
+ accountsByProvider.set(account.providerId, group);
303
+ }
304
+ const seen = /* @__PURE__ */ new Set();
305
+ const providers = [];
306
+ for (const r of opts.resolved) {
307
+ if (seen.has(r.account.providerId)) continue;
308
+ seen.add(r.account.providerId);
309
+ providers.push({
310
+ id: r.account.providerId,
311
+ name: PROVIDERS[r.account.providerId].name,
312
+ color: namedHex(PROVIDERS[r.account.providerId].color),
313
+ headroom: deriveProviderHeadroom(
314
+ (accountsByProvider.get(r.account.providerId) ?? []).map((account) => ({
315
+ id: account.id,
316
+ lastActivityAt: account.lastActivityAt,
317
+ quotas: account.quotas ?? []
318
+ })),
319
+ opts.config.tray.activeTimeoutMin,
320
+ Date.now(),
321
+ opts.config.tray.displayMetric
322
+ )
323
+ });
324
+ }
325
+ return {
326
+ version: opts.version,
327
+ generatedAt: Date.now(),
328
+ tz: opts.tz,
329
+ intervalMs: opts.intervalMs,
330
+ billingIntervalMs: opts.billingIntervalMs,
331
+ installedProviders: opts.installedProviders,
332
+ providers,
333
+ accounts,
334
+ seeded: opts.seeded ?? false,
335
+ peak: opts.peak ?? null
336
+ };
337
+ }
338
+ function tzFor(config) {
339
+ return resolveTimezone(config.timezone);
340
+ }
341
+
338
342
  // src/web/refresh-queue.ts
339
343
  function deferred() {
340
344
  let resolve;
@@ -439,6 +443,7 @@ function createDataEngine(opts) {
439
443
  let summaryIntervalMs = opts.summaryIntervalMs;
440
444
  let billingIntervalMs = opts.billingIntervalMs;
441
445
  let resolved = opts.resolved;
446
+ let installedProviders = opts.installedProviders ?? [];
442
447
  let currentConfig = opts.config;
443
448
  const usage = /* @__PURE__ */ new Map();
444
449
  const billing = /* @__PURE__ */ new Map();
@@ -478,6 +483,7 @@ function createDataEngine(opts) {
478
483
  intervalMs: summaryIntervalMs,
479
484
  billingIntervalMs,
480
485
  resolved,
486
+ installedProviders,
481
487
  usage,
482
488
  billing,
483
489
  summaryState,
@@ -714,6 +720,7 @@ function createDataEngine(opts) {
714
720
  const sourceKey = (r) => `${r.account.providerId}:${r.account.homeDir ?? ""}`;
715
721
  const prevSources = new Map(resolved.map((r) => [r.account.id, sourceKey(r)]));
716
722
  resolved = next.resolved;
723
+ installedProviders = next.installedProviders;
717
724
  hasClaude = resolved.some((r) => r.account.providerId === "claude");
718
725
  if (!hasClaude) peak = null;
719
726
  usageAccounts = resolved.filter((r) => r.hasUsage);
@@ -820,13 +827,27 @@ var ConfigPersistenceError = class extends Error {
820
827
  }
821
828
  };
822
829
  async function resolveEngineConfig(config) {
830
+ const [resolved, installedProviders] = await Promise.all([
831
+ resolveAccounts(config),
832
+ detectInstalledProviders()
833
+ ]);
823
834
  return {
824
- resolved: await resolveAccounts(config),
835
+ resolved,
836
+ installedProviders,
825
837
  tz: tzFor(config),
826
838
  summaryIntervalMs: summaryIntervalFor(config),
827
839
  billingIntervalMs: billingIntervalFor(config)
828
840
  };
829
841
  }
842
+ async function rediscoverEngineAccounts(engine, state, resolve = resolveEngineConfig) {
843
+ while (true) {
844
+ const expected = state.config;
845
+ const next = await resolve(expected);
846
+ if (state.config.revision !== expected.revision) continue;
847
+ engine.setConfig(next);
848
+ return;
849
+ }
850
+ }
830
851
  function configAffectsEngine(previous, next) {
831
852
  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
853
  }
@@ -838,8 +859,14 @@ function mergeCapabilityFields(incomingConfig, current) {
838
859
  const incoming = incomingConfig;
839
860
  const incomingTray = incoming.tray;
840
861
  const incomingDesktop = incoming.desktop;
862
+ const currentAccounts = new Map(current.accounts.map((account) => [account.id, account]));
863
+ const accounts = hasOwn(incoming, "accounts") && Array.isArray(incoming.accounts) ? incoming.accounts.map((account) => {
864
+ const previous = currentAccounts.get(account.id);
865
+ return !hasOwn(account, "enabled") && previous?.enabled === false ? { ...account, enabled: false } : account;
866
+ }) : current.accounts;
841
867
  return {
842
868
  ...incoming,
869
+ accounts,
843
870
  appearance: hasOwn(incoming, "appearance") ? incoming.appearance : current.appearance,
844
871
  accountDetection: hasOwn(incoming, "accountDetection") ? incoming.accountDetection : current.accountDetection,
845
872
  tray: hasOwn(incoming, "tray") && incomingTray ? {
@@ -866,10 +893,10 @@ async function applyConfigUpdateUnlocked(engine, state, input) {
866
893
  if (input.expectedRevision !== state.config.revision) {
867
894
  throw new ConfigConflictError(toConfigState(state.config));
868
895
  }
869
- const normalized = normalizeConfig({
896
+ const normalized = canonicalizeConfigHomeRefs(normalizeConfig({
870
897
  ...mergeCapabilityFields(input.config, state.config),
871
898
  revision: state.config.revision + 1
872
- });
899
+ }));
873
900
  const reconfigureEngine = configAffectsEngine(state.config, normalized);
874
901
  const engineConfig = reconfigureEngine ? await resolveEngineConfig(normalized) : null;
875
902
  try {
@@ -1065,9 +1092,12 @@ function configUpdateEffect(engine, state, input) {
1065
1092
  function refreshFailureEffect(error) {
1066
1093
  return error instanceof AggregateError ? Effect.fail(new RefreshFailure({ kind: "refresh", message: error.message })) : Effect.die(error);
1067
1094
  }
1068
- function refreshEffect(engine, scope) {
1095
+ function refreshEffect(engine, state, scope) {
1069
1096
  return Effect.tryPromise({
1070
- try: () => engine.refresh(scope),
1097
+ try: async () => {
1098
+ if (scope === "all") await rediscoverEngineAccounts(engine, state);
1099
+ await engine.refresh(scope);
1100
+ },
1071
1101
  catch: (error) => error
1072
1102
  }).pipe(Effect.matchEffect({
1073
1103
  onFailure: refreshFailureEffect,
@@ -1081,7 +1111,7 @@ async function mountWsRpc(server, deps) {
1081
1111
  TokmonRpcGroup.of({
1082
1112
  [TOKMON_WS_METHODS.getConfig]: () => Effect.promise(() => Promise.resolve(deps.state.config ?? loadConfig()).then(toConfigState)),
1083
1113
  [TOKMON_WS_METHODS.setConfig]: (config) => configUpdateEffect(deps.engine, deps.state, config),
1084
- [TOKMON_WS_METHODS.refresh]: ({ scope: scope2 }) => refreshEffect(deps.engine, scope2),
1114
+ [TOKMON_WS_METHODS.refresh]: ({ scope: scope2 }) => refreshEffect(deps.engine, deps.state, scope2),
1085
1115
  [TOKMON_WS_METHODS.browseFs]: ({ path }) => Effect.promise(() => listHomeDirectory(path)),
1086
1116
  [TOKMON_WS_METHODS.snapshot]: () => snapshotStream(deps.engine),
1087
1117
  [TOKMON_WS_METHODS.config]: () => configStream(deps.engine).pipe(Stream.map(toConfigState))
@@ -1221,19 +1251,16 @@ function createRouter(engine, state, vite, webRoot, wsToken, version, protocolVe
1221
1251
  }
1222
1252
  async function startWebServer(opts) {
1223
1253
  const state = { config: opts.config };
1224
- const tz = tzFor(state.config);
1225
1254
  const version = opts.version ?? appVersion();
1226
1255
  const protocolVersion = opts.protocolVersion ?? TOKMON_PROTOCOL_VERSION;
1227
1256
  const capabilities = opts.capabilities ?? TOKMON_CAPABILITIES;
1228
1257
  const ownerKind = opts.ownerKind ?? "cli";
1229
1258
  const channel = resolveDaemonChannel(opts.channel);
1230
- const summaryIntervalMs = summaryIntervalFor(state.config);
1231
- const billingIntervalMs = billingIntervalFor(state.config);
1232
1259
  const wsToken = opts.wsToken ?? randomBytes(32).toString("base64url");
1233
1260
  const log = (msg) => {
1234
1261
  if (opts.log) process.stdout.write(msg + "\n");
1235
1262
  };
1236
- const resolved = await resolveAccounts(state.config);
1263
+ const engineConfig = await resolveEngineConfig(state.config);
1237
1264
  const server = createServer();
1238
1265
  let vite = null;
1239
1266
  let engine = null;
@@ -1242,7 +1269,7 @@ async function startWebServer(opts) {
1242
1269
  if (isDevMode()) vite = await createViteDevServer(server, log);
1243
1270
  const webRoot = vite ? null : opts.webRoot ?? findWebRoot();
1244
1271
  if (!vite && !webRoot) log(" \u26A0 no dashboard available \u2014 see the page for build/dev instructions");
1245
- engine = createDataEngine({ version, config: state.config, tz, summaryIntervalMs, billingIntervalMs, resolved });
1272
+ engine = createDataEngine({ version, config: state.config, ...engineConfig });
1246
1273
  server.addListener("request", createRouter(
1247
1274
  engine,
1248
1275
  state,
@@ -5,13 +5,38 @@ import {
5
5
  identityFromIdToken,
6
6
  readClaudeIdentity,
7
7
  readJson
8
- } from "./chunk-WPZTPMBX.js";
8
+ } from "./chunk-EZHV4R74.js";
9
9
  import {
10
- containsEmail,
11
10
  expandHome,
12
- redactEmail,
13
11
  slugify
14
- } from "./chunk-G2A3DVPU.js";
12
+ } from "./chunk-GQCFGAQ6.js";
13
+
14
+ // src/peak.ts
15
+ async function fetchPeak() {
16
+ try {
17
+ const res = await fetch("https://promoclock.co/api/status", {
18
+ headers: { "Accept": "application/json", "User-Agent": "tokmon" },
19
+ signal: AbortSignal.timeout(3e3)
20
+ });
21
+ if (!res.ok) return null;
22
+ const data = await readJson(res);
23
+ if (!data) return null;
24
+ let state;
25
+ if (data.isPeak === true || data.status === "peak") state = "peak";
26
+ else if (data.isWeekend === true || data.status === "weekend") state = "weekend";
27
+ else if (data.isOffPeak === true || data.status === "off_peak" || data.status === "off-peak") state = "off-peak";
28
+ else return null;
29
+ const minutesUntilChange = typeof data.minutesUntilChange === "number" && Number.isFinite(data.minutesUntilChange) ? data.minutesUntilChange : null;
30
+ return {
31
+ state,
32
+ label: state === "peak" ? "Peak" : state === "weekend" ? "Weekend" : "Off-Peak",
33
+ minutesUntilChange,
34
+ changesAt: minutesUntilChange !== null ? new Date(Date.now() + minutesUntilChange * 6e4).toISOString() : null
35
+ };
36
+ } catch {
37
+ return null;
38
+ }
39
+ }
15
40
 
16
41
  // src/accounts.ts
17
42
  import { existsSync, readdirSync, readFileSync as readFileSync2, statSync } from "fs";
@@ -41,25 +66,39 @@ function readCodexIdentity(homeDir) {
41
66
  function accountKey(providerId, homeDir) {
42
67
  return `${providerId}:${homeDir ? resolve(expandHome(homeDir)) : homedir()}`;
43
68
  }
44
- function uniqueId(base, used) {
45
- let id = slugify(base) || "account";
46
- if (!used.has(id)) {
47
- used.add(id);
48
- return id;
69
+ function runtimeHomeDir(homeDir) {
70
+ const expanded = resolve(expandHome(homeDir));
71
+ return expanded === resolve(homedir()) ? void 0 : expanded;
72
+ }
73
+ function stableHash(value) {
74
+ let hash = 2166136261;
75
+ for (let i = 0; i < value.length; i++) {
76
+ hash ^= value.charCodeAt(i);
77
+ hash = Math.imul(hash, 16777619);
49
78
  }
50
- for (let i = 2; i < 1e3; i++) {
51
- const next = `${id}_${i}`;
52
- if (!used.has(next)) {
53
- used.add(next);
54
- return next;
79
+ return (hash >>> 0).toString(36);
80
+ }
81
+ function discoveredId(providerId, homeDir) {
82
+ const label = slugify(basename(homeDir).replace(new RegExp(`^\\.${providerId}[_-]?`), "")) || "account";
83
+ return `${providerId}_${label}_${stableHash(resolve(homeDir))}`;
84
+ }
85
+ function containsClaudeSession(root) {
86
+ if (!existsSync(root)) return false;
87
+ const pending = [root];
88
+ while (pending.length > 0) {
89
+ const current = pending.pop();
90
+ try {
91
+ for (const entry of readdirSync(current, { withFileTypes: true })) {
92
+ if (entry.isFile() && entry.name.endsWith(".jsonl")) return true;
93
+ if (entry.isDirectory()) pending.push(join2(current, entry.name));
94
+ }
95
+ } catch {
55
96
  }
56
97
  }
57
- id = `${id}_${Date.now()}`;
58
- used.add(id);
59
- return id;
98
+ return false;
60
99
  }
61
100
  function hasClaudeState(homeDir) {
62
- return existsSync(join2(homeDir, ".claude.json")) || existsSync(join2(homeDir, ".claude", ".credentials.json")) || existsSync(join2(homeDir, ".claude", "projects")) || existsSync(join2(homeDir, ".config", "claude", ".credentials.json")) || existsSync(join2(homeDir, ".config", "claude", "projects"));
101
+ return existsSync(join2(homeDir, ".claude", ".credentials.json")) || existsSync(join2(homeDir, ".config", "claude", ".credentials.json")) || containsClaudeSession(join2(homeDir, ".claude", "projects")) || containsClaudeSession(join2(homeDir, ".config", "claude", "projects"));
63
102
  }
64
103
  function candidateAlternateHomes(prefix) {
65
104
  const home = homedir();
@@ -107,73 +146,93 @@ function labelForCodexHome(homeDir) {
107
146
  const raw = basename(homeDir).replace(/^\.codex[_-]?/, "").replace(/[_-]+/g, " ").trim();
108
147
  return raw ? `Codex ${raw}` : "Codex";
109
148
  }
110
- function discoverClaudeAccounts(usedIds) {
149
+ function discoverClaudeAccounts() {
111
150
  const provider = PROVIDERS.claude;
112
151
  const out = [];
113
152
  for (const homeDir of candidateAlternateHomes("claude")) {
114
153
  if (!hasClaudeState(homeDir)) continue;
115
- const suffix = basename(homeDir).replace(/^\.claude[_-]?/, "") || basename(homeDir);
116
154
  out.push({
117
- id: uniqueId(`claude_${suffix}`, usedIds),
155
+ id: discoveredId("claude", homeDir),
118
156
  providerId: "claude",
119
157
  name: labelForClaudeHome(homeDir),
120
158
  color: provider.color,
121
- homeDir
159
+ homeDir,
160
+ source: "auto"
122
161
  });
123
162
  }
124
163
  return out;
125
164
  }
126
- function discoverCodexAccounts(usedIds) {
165
+ function discoverCodexAccounts() {
127
166
  const provider = PROVIDERS.codex;
128
167
  const out = [];
129
168
  for (const homeDir of candidateAlternateHomes("codex")) {
130
169
  if (!hasCodexAuth(homeDir)) continue;
131
- const suffix = basename(homeDir).replace(/^\.codex[_-]?/, "") || basename(homeDir);
132
170
  out.push({
133
- id: uniqueId(`codex_${suffix}`, usedIds),
171
+ id: discoveredId("codex", homeDir),
134
172
  providerId: "codex",
135
173
  name: labelForCodexHome(homeDir),
136
174
  color: provider.color,
137
- homeDir
175
+ homeDir,
176
+ source: "auto"
138
177
  });
139
178
  }
140
179
  return out;
141
180
  }
142
- function discoverProviderAccounts(providerId, usedIds) {
143
- if (providerId === "claude") return discoverClaudeAccounts(usedIds);
144
- if (providerId === "codex") return discoverCodexAccounts(usedIds);
181
+ function discoverProviderAccounts(providerId) {
182
+ if (providerId === "claude") return discoverClaudeAccounts();
183
+ if (providerId === "codex") return discoverCodexAccounts();
145
184
  return [];
146
185
  }
147
186
  function buildAccounts(config, detected) {
148
187
  const out = [];
149
- const usedIds = new Set(config.accounts.map((a) => a.id));
150
188
  const seenKeys = /* @__PURE__ */ new Set();
189
+ const seenIds = /* @__PURE__ */ new Set();
151
190
  const excludedKeys = new Set(
152
191
  config.accountDetection.excludedAccounts.map((ref) => accountKey(ref.providerId, ref.homeDir))
153
192
  );
154
193
  const add = (account) => {
155
194
  const key = accountKey(account.providerId, account.homeDir);
156
195
  if (seenKeys.has(key)) return;
196
+ let id = account.id;
197
+ if (seenIds.has(id)) {
198
+ const base = `${id}_auto`;
199
+ id = base;
200
+ for (let suffix = 2; seenIds.has(id); suffix++) id = `${base}_${suffix}`;
201
+ }
157
202
  seenKeys.add(key);
158
- out.push(account);
203
+ seenIds.add(id);
204
+ out.push(id === account.id ? account : { ...account, id });
159
205
  };
160
206
  for (const pid of PROVIDER_ORDER) {
161
207
  if (config.disabledProviders.includes(pid)) continue;
162
208
  const provider = PROVIDERS[pid];
163
209
  const configured = config.accounts.filter((a) => a.providerId === pid);
164
210
  for (const a of configured) {
211
+ if (a.enabled === false) {
212
+ seenKeys.add(accountKey(a.providerId, a.homeDir));
213
+ seenIds.add(a.id);
214
+ continue;
215
+ }
165
216
  add({
166
217
  id: a.id,
167
218
  providerId: pid,
168
219
  name: a.name,
169
220
  color: a.color || provider.color,
170
- homeDir: a.homeDir && a.homeDir !== "~" ? expandHome(a.homeDir) : void 0
221
+ homeDir: runtimeHomeDir(a.homeDir || "~"),
222
+ source: "configured"
171
223
  });
172
224
  }
173
225
  if (!config.accountDetection.enabled || config.accountDetection.disabledProviders.includes(pid)) continue;
174
- const discovered = discoverProviderAccounts(pid, usedIds);
226
+ const discovered = discoverProviderAccounts(pid);
175
227
  if (detected.includes(pid)) {
176
- const account = { id: pid, providerId: pid, name: provider.name, color: provider.color, homeDir: void 0 };
228
+ const account = {
229
+ id: pid,
230
+ providerId: pid,
231
+ name: provider.name,
232
+ color: provider.color,
233
+ homeDir: void 0,
234
+ source: "auto"
235
+ };
177
236
  if (!excludedKeys.has(accountKey(pid))) add(account);
178
237
  }
179
238
  for (const account of discovered) {
@@ -358,12 +417,12 @@ function deriveAccountIdentity(input) {
358
417
  const registered = input.name.trim() || input.providerName;
359
418
  const email = input.email?.trim() || null;
360
419
  const displayName = input.displayName?.trim() || null;
361
- if (input.privacyMode && (containsEmail(registered) || containsEmail(email))) {
420
+ if (input.privacyMode) {
362
421
  const title2 = `${input.providerName} account ${input.ordinal}`;
363
422
  return { title: title2, subtitle: null, accessibleLabel: title2, redacted: true };
364
423
  }
365
- const title = input.privacyMode ? redactEmail(registered) : registered;
366
- const subtitleCandidate = !input.privacyMode ? email && !registered.toLowerCase().includes(email.toLowerCase()) ? email : displayName : displayName && !containsEmail(displayName) ? displayName : null;
424
+ const title = registered;
425
+ const subtitleCandidate = email && !registered.toLowerCase().includes(email.toLowerCase()) ? email : displayName;
367
426
  const subtitle = subtitleCandidate && subtitleCandidate !== title ? subtitleCandidate : null;
368
427
  return { title, subtitle, accessibleLabel: subtitle ? `${title}, ${subtitle}` : title, redacted: input.privacyMode && title !== registered };
369
428
  }
@@ -490,34 +549,8 @@ function deriveProviderHeadroom(accounts, activeTimeoutMin, now, displayMetric =
490
549
  return { ...selected.view, basis, representativeAccountId: selected.account.id, activeAccountIds: activeIds, explanation: `${selected.view.explanation}; ${basis === "active" ? "active account" : "best available account"}` };
491
550
  }
492
551
 
493
- // src/peak.ts
494
- async function fetchPeak() {
495
- try {
496
- const res = await fetch("https://promoclock.co/api/status", {
497
- headers: { "Accept": "application/json", "User-Agent": "tokmon" },
498
- signal: AbortSignal.timeout(3e3)
499
- });
500
- if (!res.ok) return null;
501
- const data = await readJson(res);
502
- if (!data) return null;
503
- let state;
504
- if (data.isPeak === true || data.status === "peak") state = "peak";
505
- else if (data.isWeekend === true || data.status === "weekend") state = "weekend";
506
- else if (data.isOffPeak === true || data.status === "off_peak" || data.status === "off-peak") state = "off-peak";
507
- else return null;
508
- const minutesUntilChange = typeof data.minutesUntilChange === "number" && Number.isFinite(data.minutesUntilChange) ? data.minutesUntilChange : null;
509
- return {
510
- state,
511
- label: state === "peak" ? "Peak" : state === "weekend" ? "Weekend" : "Off-Peak",
512
- minutesUntilChange,
513
- changesAt: minutesUntilChange !== null ? new Date(Date.now() + minutesUntilChange * 6e4).toISOString() : null
514
- };
515
- } catch {
516
- return null;
517
- }
518
- }
519
-
520
552
  export {
553
+ fetchPeak,
521
554
  buildAccounts,
522
555
  accountsByProvider,
523
556
  namedHex,
@@ -529,6 +562,5 @@ export {
529
562
  severity,
530
563
  deriveAccountIdentity,
531
564
  accountIdentityText,
532
- deriveProviderHeadroom,
533
- fetchPeak
565
+ deriveProviderHeadroom
534
566
  };
@@ -5,7 +5,7 @@ import {
5
5
  } from "./chunk-UYPDMVW5.js";
6
6
  import {
7
7
  cacheDir
8
- } from "./chunk-G2A3DVPU.js";
8
+ } from "./chunk-GQCFGAQ6.js";
9
9
 
10
10
  // src/web/daemon-compatibility.ts
11
11
  function classifyDaemonCompatibility(owner, clientProtocolVersion) {