tokmon 0.28.1 → 0.28.3

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 (57) hide show
  1. package/README.md +156 -234
  2. package/THIRD_PARTY_NOTICES.md +62 -0
  3. package/dist/{bootstrap-ink-L7QSJBMS.js → bootstrap-ink-WG4WG3R2.js} +1579 -1135
  4. package/dist/chunk-7CH6S7BR.js +447 -0
  5. package/dist/{chunk-CVKCVHS7.js → chunk-CFKUZCMB.js} +70 -310
  6. package/dist/chunk-HNWEJ6MS.js +1094 -0
  7. package/dist/chunk-JZSZEHVD.js +406 -0
  8. package/dist/{chunk-S33XIUAW.js → chunk-P57DVFNH.js} +277 -49
  9. package/dist/{chunk-4HU4EI5T.js → chunk-P5JN5DDX.js} +25 -46
  10. package/dist/chunk-TN6V7XSL.js +512 -0
  11. package/dist/chunk-USKOQIE3.js +278 -0
  12. package/dist/chunk-UYPDMVW5.js +21 -0
  13. package/dist/{cli-command-75LP4IDS.js → cli-command-MD3RJRP4.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-EMOA36WB.js +377 -0
  17. package/dist/daemon-handle-JHE5MKMI.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-3RTWFGGD.js +0 -275
  38. package/dist/chunk-FMP3P2WV.js +0 -299
  39. package/dist/chunk-HP5UZCXP.js +0 -436
  40. package/dist/chunk-SMPY52EV.js +0 -125
  41. package/dist/chunk-XDA5RJST.js +0 -193
  42. package/dist/daemon-CL4FJW26.js +0 -219
  43. package/dist/daemon-handle-QQLJE46Y.js +0 -10
  44. package/dist/server-EUO7CWYH.js +0 -11
  45. package/dist/web/assets/analytics-DqdZXOJL.js +0 -2
  46. package/dist/web/assets/breakdown-kOSSJ3mI.js +0 -4
  47. package/dist/web/assets/explore-vcsM8K9I.js +0 -22
  48. package/dist/web/assets/index-BErCXT7f.css +0 -1
  49. package/dist/web/assets/index-DH3dOnbg.js +0 -83
  50. package/dist/web/assets/models-B5ikm83_.js +0 -2
  51. package/dist/web/assets/overview-DD1kP8CP.js +0 -2
  52. package/dist/web/assets/panel-DTQBwExW.js +0 -1
  53. package/dist/web/assets/primitives-C36qJlA2.js +0 -1
  54. package/dist/web/assets/settings-sheet-_i2zTQjf.js +0 -1
  55. package/dist/web/assets/share-sheet-BLHmrFbm.js +0 -2
  56. package/dist/web/assets/timeline-B4uG-Emo.js +0 -1
  57. package/dist/web/assets/use-dialog-trap-bfaXEMz9.js +0 -1
@@ -1,28 +1,32 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
- appVersion,
4
- findWebRoot,
5
- send,
6
- sendJson,
7
- serveStatic
8
- } from "./chunk-SMPY52EV.js";
3
+ resolveDaemonChannel
4
+ } from "./chunk-UYPDMVW5.js";
9
5
  import {
10
6
  buildAccounts,
11
7
  colorHex,
8
+ deriveAccountIdentity,
9
+ deriveProviderHeadroom,
10
+ deriveQuotaViews,
12
11
  fetchPeak,
13
12
  namedHex
14
- } from "./chunk-FMP3P2WV.js";
13
+ } from "./chunk-TN6V7XSL.js";
15
14
  import {
16
15
  PROVIDERS,
16
+ detectProviders,
17
+ resolveTimezone,
18
+ withTimeout
19
+ } from "./chunk-CFKUZCMB.js";
20
+ import {
21
+ ConfigPersistenceFailure,
22
+ ConfigUpdateConflictFailure,
23
+ RefreshFailure,
17
24
  TOKMON_CAPABILITIES,
18
25
  TOKMON_PROTOCOL_VERSION,
19
26
  TOKMON_WS_METHODS,
20
27
  TOKMON_WS_PATH,
21
- TokmonRpcGroup,
22
- detectProviders,
23
- resolveTimezone,
24
- withTimeout
25
- } from "./chunk-CVKCVHS7.js";
28
+ TokmonRpcGroup
29
+ } from "./chunk-JZSZEHVD.js";
26
30
  import {
27
31
  cacheDir,
28
32
  expandHome,
@@ -30,7 +34,7 @@ import {
30
34
  normalizeConfig,
31
35
  saveConfig,
32
36
  snapshotCacheFile
33
- } from "./chunk-HP5UZCXP.js";
37
+ } from "./chunk-HNWEJ6MS.js";
34
38
 
35
39
  // src/web/server.ts
36
40
  import { createServer } from "http";
@@ -66,9 +70,13 @@ async function fetchAccountBilling(account, tz) {
66
70
  return p.fetchBilling(account, tz);
67
71
  }
68
72
  function assembleSnapshot(opts) {
73
+ const providerOrdinals = /* @__PURE__ */ new Map();
69
74
  const accounts = opts.resolved.map((r) => {
70
75
  const u = opts.usage.get(r.account.id);
71
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 ?? []);
72
80
  return {
73
81
  id: r.account.id,
74
82
  providerId: r.account.providerId,
@@ -79,7 +87,22 @@ function assembleSnapshot(opts) {
79
87
  hasBilling: r.hasBilling,
80
88
  email: billing?.email ?? null,
81
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),
82
104
  plan: billing?.plan ?? null,
105
+ lastActivityAt: u?.dashboard?.lastActivityAt ?? null,
83
106
  dashboard: u?.dashboard ?? null,
84
107
  table: u?.table ?? null,
85
108
  billing,
@@ -91,6 +114,12 @@ function assembleSnapshot(opts) {
91
114
  tableUpdatedAt: opts.tableUpdatedAt?.get(r.account.id) ?? null
92
115
  };
93
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
+ }
94
123
  const seen = /* @__PURE__ */ new Set();
95
124
  const providers = [];
96
125
  for (const r of opts.resolved) {
@@ -99,7 +128,17 @@ function assembleSnapshot(opts) {
99
128
  providers.push({
100
129
  id: r.account.providerId,
101
130
  name: PROVIDERS[r.account.providerId].name,
102
- 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
+ )
103
142
  });
104
143
  }
105
144
  return {
@@ -118,22 +157,139 @@ function tzFor(config) {
118
157
  return resolveTimezone(config.timezone);
119
158
  }
120
159
 
160
+ // src/web/static.ts
161
+ import { createReadStream, existsSync, readFileSync } from "fs";
162
+ import { stat } from "fs/promises";
163
+ import { fileURLToPath } from "url";
164
+ import { extname, join, normalize, sep } from "path";
165
+ var MIME = {
166
+ ".html": "text/html; charset=utf-8",
167
+ ".js": "text/javascript; charset=utf-8",
168
+ ".mjs": "text/javascript; charset=utf-8",
169
+ ".css": "text/css; charset=utf-8",
170
+ ".json": "application/json; charset=utf-8",
171
+ ".svg": "image/svg+xml",
172
+ ".png": "image/png",
173
+ ".jpg": "image/jpeg",
174
+ ".jpeg": "image/jpeg",
175
+ ".gif": "image/gif",
176
+ ".ico": "image/x-icon",
177
+ ".woff": "font/woff",
178
+ ".woff2": "font/woff2",
179
+ ".ttf": "font/ttf",
180
+ ".otf": "font/otf",
181
+ ".map": "application/json; charset=utf-8",
182
+ ".webmanifest": "application/manifest+json"
183
+ };
184
+ function findWebRoot() {
185
+ const candidates = ["./web/", "../web/", "../dist/web/", "../../dist/web/"];
186
+ for (const rel of candidates) {
187
+ try {
188
+ const dir = fileURLToPath(new URL(rel, import.meta.url));
189
+ if (existsSync(join(dir, "index.html"))) return dir.replace(/[\\/]+$/, "");
190
+ } catch {
191
+ }
192
+ }
193
+ return null;
194
+ }
195
+ function resolveStaticPath(webRoot, urlPath) {
196
+ let clean;
197
+ try {
198
+ clean = decodeURIComponent(urlPath.split("?")[0]);
199
+ } catch {
200
+ return null;
201
+ }
202
+ const rel = normalize(clean).replace(/^(\.\.[/\\])+/, "").replace(/^[/\\]+/, "");
203
+ const full = join(webRoot, rel);
204
+ if (full !== webRoot && !full.startsWith(webRoot + sep)) return null;
205
+ return full;
206
+ }
207
+ function send(res, status, type, body) {
208
+ res.writeHead(status, {
209
+ "Content-Type": type,
210
+ "Cache-Control": "no-store",
211
+ "Referrer-Policy": "no-referrer",
212
+ "X-Content-Type-Options": "nosniff"
213
+ });
214
+ res.end(body);
215
+ }
216
+ function sendJson(res, status, data) {
217
+ send(res, status, "application/json; charset=utf-8", JSON.stringify(data));
218
+ }
219
+ function serveStatic(webRoot, urlPath, res) {
220
+ const path = urlPath.split("?")[0];
221
+ const filePath = resolveStaticPath(webRoot, path === "/" ? "/index.html" : path);
222
+ if (!filePath) {
223
+ send(res, 403, "text/plain", "forbidden");
224
+ return;
225
+ }
226
+ void stat(filePath).then((st) => {
227
+ if (st.isFile()) {
228
+ const type = MIME[extname(filePath).toLowerCase()] || "application/octet-stream";
229
+ const immutable = filePath.includes(`${sep}assets${sep}`);
230
+ res.writeHead(200, {
231
+ "Content-Type": type,
232
+ "Cache-Control": immutable ? "public, max-age=31536000, immutable" : "no-cache",
233
+ "Referrer-Policy": "no-referrer",
234
+ "X-Content-Type-Options": "nosniff"
235
+ });
236
+ createReadStream(filePath).pipe(res);
237
+ } else {
238
+ throw new Error("not a file");
239
+ }
240
+ }).catch(() => {
241
+ if (extname(path)) {
242
+ send(res, 404, "text/plain", "not found");
243
+ return;
244
+ }
245
+ const indexPath = join(webRoot, "index.html");
246
+ if (!existsSync(indexPath)) {
247
+ send(res, 404, "text/plain", "not found");
248
+ return;
249
+ }
250
+ res.writeHead(200, {
251
+ "Content-Type": MIME[".html"],
252
+ "Cache-Control": "no-cache",
253
+ "Referrer-Policy": "no-referrer",
254
+ "X-Content-Type-Options": "nosniff"
255
+ });
256
+ createReadStream(indexPath).on("error", () => {
257
+ try {
258
+ res.destroy();
259
+ } catch {
260
+ }
261
+ }).pipe(res);
262
+ });
263
+ }
264
+ function appVersion() {
265
+ for (const rel of ["../package.json", "../../package.json"]) {
266
+ try {
267
+ const p = fileURLToPath(new URL(rel, import.meta.url));
268
+ const pkg = JSON.parse(readFileSync(p, "utf-8"));
269
+ if (typeof pkg.version === "string") return pkg.version;
270
+ } catch {
271
+ }
272
+ }
273
+ return "";
274
+ }
275
+
121
276
  // src/web/vite-dev.ts
122
- import { existsSync } from "fs";
123
- import { fileURLToPath, pathToFileURL } from "url";
277
+ import { existsSync as existsSync2 } from "fs";
278
+ import { fileURLToPath as fileURLToPath2, pathToFileURL } from "url";
124
279
  import { createRequire } from "module";
125
- import { join } from "path";
280
+ import { join as join2 } from "path";
126
281
  function isDevMode() {
127
282
  const forced = process.env.TOKMON_WEB_MODE;
128
283
  if (forced === "dev") return true;
129
284
  if (forced === "prod") return false;
285
+ if (process.env.NODE_TEST_CONTEXT) return false;
130
286
  return import.meta.url.includes("/src/");
131
287
  }
132
288
  function findWebSource() {
133
289
  for (const rel of ["../../web/", "../web/", "./web/"]) {
134
290
  try {
135
- const dir = fileURLToPath(new URL(rel, import.meta.url));
136
- if (existsSync(join(dir, "vite.config.ts")) && existsSync(join(dir, "index.html"))) {
291
+ const dir = fileURLToPath2(new URL(rel, import.meta.url));
292
+ if (existsSync2(join2(dir, "vite.config.ts")) && existsSync2(join2(dir, "index.html"))) {
137
293
  return dir.replace(/[\\/]+$/, "");
138
294
  }
139
295
  } catch {
@@ -148,12 +304,12 @@ async function createViteDevServer(httpServer, log) {
148
304
  return null;
149
305
  }
150
306
  try {
151
- const req = createRequire(pathToFileURL(join(root, "package.json")).href);
307
+ const req = createRequire(pathToFileURL(join2(root, "package.json")).href);
152
308
  const vitePath = req.resolve("vite");
153
309
  const vite = await import(pathToFileURL(vitePath).href);
154
310
  const dev = await vite.createServer({
155
311
  root,
156
- configFile: join(root, "vite.config.ts"),
312
+ configFile: join2(root, "vite.config.ts"),
157
313
  server: { middlewareMode: true, hmr: { server: httpServer } },
158
314
  appType: "spa",
159
315
  clearScreen: false,
@@ -177,7 +333,7 @@ code{color:#e6b450}</style></head><body>
177
333
  </body></html>`;
178
334
 
179
335
  // src/web/data-engine.ts
180
- import { readFileSync, writeFileSync, mkdirSync, renameSync } from "fs";
336
+ import { readFileSync as readFileSync2, writeFileSync, mkdirSync, renameSync } from "fs";
181
337
 
182
338
  // src/web/refresh-queue.ts
183
339
  function deferred() {
@@ -331,15 +487,20 @@ function createDataEngine(opts) {
331
487
  billingUpdatedAt,
332
488
  tableUpdatedAt,
333
489
  seeded,
334
- peak
490
+ peak,
491
+ config: currentConfig
335
492
  });
336
493
  const hydrateFromCache = () => {
337
494
  try {
338
- const cached = JSON.parse(readFileSync(snapshotCacheFile(), "utf-8"));
495
+ const cached = JSON.parse(readFileSync2(snapshotCacheFile(), "utf-8"));
339
496
  if (!cached || !Array.isArray(cached.accounts)) return;
340
497
  for (const a of cached.accounts) {
341
498
  if (a.dashboard || a.table) {
342
- 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 });
343
504
  if (a.dashboard) {
344
505
  summaryState.set(a.id, "ready");
345
506
  if (typeof a.summaryUpdatedAt === "number") summaryUpdatedAt.set(a.id, a.summaryUpdatedAt);
@@ -578,6 +739,7 @@ function createDataEngine(opts) {
578
739
  broadcastConfig(config) {
579
740
  if (stopped) return;
580
741
  currentConfig = config;
742
+ rebuild();
581
743
  for (const onConfig of configSubscribers) {
582
744
  try {
583
745
  onConfig(config);
@@ -665,23 +827,51 @@ async function resolveEngineConfig(config) {
665
827
  billingIntervalMs: billingIntervalFor(config)
666
828
  };
667
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
+ }
668
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
+ }
669
858
  async function applyConfigUpdateUnlocked(engine, state, input) {
670
859
  if (input.expectedRevision !== state.config.revision) {
671
860
  throw new ConfigConflictError(toConfigState(state.config));
672
861
  }
673
862
  const normalized = normalizeConfig({
674
- ...input.config,
863
+ ...mergeCapabilityFields(input.config, state.config),
675
864
  revision: state.config.revision + 1
676
865
  });
677
- const engineConfig = await resolveEngineConfig(normalized);
866
+ const reconfigureEngine = configAffectsEngine(state.config, normalized);
867
+ const engineConfig = reconfigureEngine ? await resolveEngineConfig(normalized) : null;
678
868
  try {
679
869
  await saveConfig(normalized);
680
870
  } catch (error) {
681
871
  throw new ConfigPersistenceError(error);
682
872
  }
683
873
  state.config = normalized;
684
- engine.setConfig(engineConfig);
874
+ if (engineConfig) engine.setConfig(engineConfig);
685
875
  engine.broadcastConfig(normalized);
686
876
  return toConfigState(normalized);
687
877
  }
@@ -703,11 +893,11 @@ import { Effect, Exit, Layer, Queue, Scope, Stream } from "effect";
703
893
  import { RpcSerialization, RpcServer } from "effect/unstable/rpc";
704
894
 
705
895
  // src/web/fs.ts
706
- import { readdir, stat, realpath } from "fs/promises";
896
+ import { readdir, stat as stat2, realpath } from "fs/promises";
707
897
  import { homedir } from "os";
708
- import { join as join2, resolve as resolvePath, isAbsolute, sep } from "path";
898
+ import { join as join3, resolve as resolvePath, isAbsolute, sep as sep2 } from "path";
709
899
  function isContained(root, target) {
710
- return target === root || target.startsWith(root + sep);
900
+ return target === root || target.startsWith(root + sep2);
711
901
  }
712
902
  function parentFor(root, abs) {
713
903
  const parentResolved = resolvePath(abs, "..");
@@ -727,7 +917,7 @@ async function listHomeDirectory(rawPath) {
727
917
  const abs = isContained(root, real) ? real : root;
728
918
  let st;
729
919
  try {
730
- st = await stat(abs);
920
+ st = await stat2(abs);
731
921
  } catch {
732
922
  return { path: abs, parent: parentFor(root, abs), entries: [] };
733
923
  }
@@ -742,7 +932,7 @@ async function listHomeDirectory(rawPath) {
742
932
  for (const d of dirents) {
743
933
  if (d.name.startsWith(".")) continue;
744
934
  let dir = d.isDirectory();
745
- const full = join2(abs, d.name);
935
+ const full = join3(abs, d.name);
746
936
  if (d.isSymbolicLink()) {
747
937
  let real2;
748
938
  try {
@@ -752,7 +942,7 @@ async function listHomeDirectory(rawPath) {
752
942
  }
753
943
  if (!isContained(root, real2)) continue;
754
944
  try {
755
- dir = (await stat(full)).isDirectory();
945
+ dir = (await stat2(full)).isDirectory();
756
946
  } catch {
757
947
  continue;
758
948
  }
@@ -847,27 +1037,45 @@ function configStream(engine) {
847
1037
  yield* Scope.addFinalizer(scope, Effect.sync(unsubscribe));
848
1038
  }), { bufferSize: 16, strategy: "sliding" });
849
1039
  }
850
- function toConfigUpdateFailure(error) {
1040
+ function configUpdateFailureEffect(error) {
851
1041
  if (error instanceof ConfigConflictError) {
852
- return { kind: "conflict", state: error.state };
1042
+ return Effect.fail(new ConfigUpdateConflictFailure({ kind: "conflict", state: error.state }));
853
1043
  }
854
1044
  if (error instanceof ConfigPersistenceError) {
855
- return { kind: "persistence", message: error.message };
1045
+ return Effect.fail(new ConfigPersistenceFailure({ kind: "persistence", message: error.message }));
856
1046
  }
857
- 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
+ }));
858
1069
  }
859
1070
  async function mountWsRpc(server, deps) {
860
1071
  const scope = await Effect.runPromise(Scope.make());
861
1072
  const wss = new NodeWS.WebSocketServer({ noServer: true });
862
1073
  const handlersLayer = TokmonRpcGroup.toLayer(
863
1074
  TokmonRpcGroup.of({
864
- [TOKMON_WS_METHODS.getConfig]: () => Effect.tryPromise(() => Promise.resolve(deps.state.config ?? loadConfig()).then(toConfigState)),
865
- [TOKMON_WS_METHODS.setConfig]: (config) => Effect.tryPromise({
866
- try: () => applyConfigUpdate(deps.engine, deps.state, config),
867
- catch: toConfigUpdateFailure
868
- }),
869
- [TOKMON_WS_METHODS.refresh]: ({ scope: scope2 }) => Effect.promise(() => deps.engine.refresh(scope2)),
870
- [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)),
871
1079
  [TOKMON_WS_METHODS.snapshot]: () => snapshotStream(deps.engine),
872
1080
  [TOKMON_WS_METHODS.config]: () => configStream(deps.engine).pipe(Stream.map(toConfigState))
873
1081
  })
@@ -957,7 +1165,7 @@ function guardSameOrigin(req, res) {
957
1165
  }
958
1166
  return true;
959
1167
  }
960
- function createRouter(engine, state, vite, webRoot, wsToken, version) {
1168
+ function createRouter(engine, state, vite, webRoot, wsToken, version, protocolVersion, capabilities, ownerKind, channel) {
961
1169
  return (req, res) => {
962
1170
  const url = req.url || "/";
963
1171
  const path = url.split("?")[0];
@@ -973,6 +1181,10 @@ function createRouter(engine, state, vite, webRoot, wsToken, version) {
973
1181
  ok: true,
974
1182
  ready: engine.snapshot() !== null,
975
1183
  version,
1184
+ protocolVersion,
1185
+ capabilities,
1186
+ ownerKind,
1187
+ channel,
976
1188
  // Discovery requires this proof; public health checks retain their useful 200 response.
977
1189
  owner: tokenMatches(header2(req, "x-tokmon-token"), wsToken)
978
1190
  });
@@ -1003,7 +1215,11 @@ function createRouter(engine, state, vite, webRoot, wsToken, version) {
1003
1215
  async function startWebServer(opts) {
1004
1216
  const state = { config: opts.config };
1005
1217
  const tz = tzFor(state.config);
1006
- const version = appVersion();
1218
+ const version = opts.version ?? appVersion();
1219
+ const protocolVersion = opts.protocolVersion ?? TOKMON_PROTOCOL_VERSION;
1220
+ const capabilities = opts.capabilities ?? TOKMON_CAPABILITIES;
1221
+ const ownerKind = opts.ownerKind ?? "cli";
1222
+ const channel = resolveDaemonChannel(opts.channel);
1007
1223
  const summaryIntervalMs = summaryIntervalFor(state.config);
1008
1224
  const billingIntervalMs = billingIntervalFor(state.config);
1009
1225
  const wsToken = opts.wsToken ?? randomBytes(32).toString("base64url");
@@ -1017,10 +1233,21 @@ async function startWebServer(opts) {
1017
1233
  let closeWsRpc = null;
1018
1234
  try {
1019
1235
  if (isDevMode()) vite = await createViteDevServer(server, log);
1020
- const webRoot = vite ? null : findWebRoot();
1236
+ const webRoot = vite ? null : opts.webRoot ?? findWebRoot();
1021
1237
  if (!vite && !webRoot) log(" \u26A0 no dashboard available \u2014 see the page for build/dev instructions");
1022
1238
  engine = createDataEngine({ version, config: state.config, tz, summaryIntervalMs, billingIntervalMs, resolved });
1023
- server.addListener("request", createRouter(engine, state, vite, webRoot, wsToken, version));
1239
+ server.addListener("request", createRouter(
1240
+ engine,
1241
+ state,
1242
+ vite,
1243
+ webRoot,
1244
+ wsToken,
1245
+ version,
1246
+ protocolVersion,
1247
+ capabilities,
1248
+ ownerKind,
1249
+ channel
1250
+ ));
1024
1251
  closeWsRpc = await mountWsRpc(server, { engine, state });
1025
1252
  const bindHost = state.config.allowNetworkAccess ? NETWORK_HOST : LOOPBACK_HOST;
1026
1253
  const port = await listenWithFallback(server, opts.port ?? DEFAULT_PORT, bindHost);
@@ -1116,5 +1343,6 @@ function listenWithFallback(server, startPort, host) {
1116
1343
  }
1117
1344
 
1118
1345
  export {
1346
+ appVersion,
1119
1347
  startWebServer
1120
1348
  };
@@ -1,20 +1,21 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
- isAlive,
4
- probeHealth,
5
3
  readLock,
6
- reclaimDeadLock,
4
+ retireIncompatibleCliOwner,
7
5
  verifyLock
8
- } from "./chunk-XDA5RJST.js";
6
+ } from "./chunk-USKOQIE3.js";
9
7
  import {
10
- appVersion
11
- } from "./chunk-SMPY52EV.js";
8
+ daemonChannelFromWire,
9
+ resolveDaemonChannel
10
+ } from "./chunk-UYPDMVW5.js";
11
+ import {
12
+ TOKMON_PROTOCOL_VERSION
13
+ } from "./chunk-JZSZEHVD.js";
12
14
 
13
15
  // src/client/daemon-handle.ts
14
16
  import { spawn } from "child_process";
15
17
  import { extname } from "path";
16
18
  var HANDSHAKE_TIMEOUT_MS = process.platform === "win32" ? 15e3 : 1e4;
17
- var UPGRADE_SHUTDOWN_TIMEOUT_MS = 5e3;
18
19
  function runtimeExecArgv(entry, override) {
19
20
  if (override) return override;
20
21
  const ext = extname(entry).toLowerCase();
@@ -37,7 +38,7 @@ function runtimeExecArgv(entry, override) {
37
38
  function parseHandshake(line) {
38
39
  try {
39
40
  const value = JSON.parse(line);
40
- return value?.ready === 1 && typeof value.url === "string" && typeof value.wsToken === "string" && typeof value.version === "string" ? value : null;
41
+ return value?.ready === 1 && typeof value.url === "string" && typeof value.wsToken === "string" && typeof value.version === "string" && typeof value.protocolVersion === "number" && Array.isArray(value.capabilities) && value.capabilities.every((capability) => typeof capability === "string") && (value.ownerKind === "cli" || value.ownerKind === "desktop") && daemonChannelFromWire(value.channel) !== null ? { ...value, channel: daemonChannelFromWire(value.channel) } : null;
41
42
  } catch {
42
43
  return null;
43
44
  }
@@ -46,52 +47,30 @@ function connected(url) {
46
47
  return { kind: "spawned", baseUrl: url, stop: () => {
47
48
  } };
48
49
  }
49
- async function attach(opts, version) {
50
- const lock = await verifyLock(readLock(opts), version);
50
+ async function attach(opts, protocolVersion) {
51
+ const lock = await verifyLock(readLock(opts), protocolVersion);
51
52
  return lock ? connected(lock.url) : null;
52
53
  }
53
- var delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
54
- async function retireIncompatibleDaemon(opts, version, timeoutMs) {
55
- const lock = readLock(opts);
56
- if (!lock || lock.state !== "ready" || lock.version === version || lock.pid === process.pid || !isAlive(lock.pid)) return;
57
- const authenticated = await probeHealth(
58
- lock.url,
59
- lock.wsToken,
60
- lock.version,
61
- Math.min(1e3, timeoutMs)
62
- );
63
- if (!authenticated) return;
64
- try {
65
- process.kill(lock.pid, "SIGTERM");
66
- } catch {
67
- if (!isAlive(lock.pid)) reclaimDeadLock(opts);
68
- return;
69
- }
70
- const deadline = Date.now() + Math.min(timeoutMs, UPGRADE_SHUTDOWN_TIMEOUT_MS);
71
- while (Date.now() < deadline) {
72
- const current = readLock(opts);
73
- if (!current || current.ownerId !== lock.ownerId) return;
74
- if (!isAlive(lock.pid)) {
75
- reclaimDeadLock(opts);
76
- return;
77
- }
78
- await delay(50);
79
- }
80
- }
81
54
  async function attachOrSpawn(opts = {}) {
82
- const version = appVersion();
55
+ const protocolVersion = TOKMON_PROTOCOL_VERSION;
83
56
  const timeoutMs = opts.timeoutMs ?? HANDSHAKE_TIMEOUT_MS;
84
- const existing = await attach(opts, version);
57
+ const baseEnv = opts.env ?? process.env;
58
+ const channel = resolveDaemonChannel(opts.channel, baseEnv);
59
+ const lockOpts = { cachePath: opts.cachePath, channel };
60
+ const existing = await attach(lockOpts, protocolVersion);
85
61
  if (existing) return existing;
86
- await retireIncompatibleDaemon(opts, version, timeoutMs);
87
- const upgraded = await attach(opts, version);
62
+ await retireIncompatibleCliOwner(lockOpts, protocolVersion, timeoutMs);
63
+ const upgraded = await attach(lockOpts, protocolVersion);
88
64
  if (upgraded) return upgraded;
89
65
  const entry = opts.entry ?? process.argv[1];
90
66
  if (!entry) return degraded();
91
67
  const execPath = opts.execPath ?? process.execPath;
92
68
  const args = ["__daemon", "--port", "0", "--no-open"];
93
- const baseEnv = opts.env ?? process.env;
94
- const env = opts.cachePath ? { ...baseEnv, TOKMON_DAEMON_CACHE_DIR: opts.cachePath } : baseEnv;
69
+ const env = {
70
+ ...baseEnv,
71
+ TOKMON_CHANNEL: channel,
72
+ ...opts.cachePath ? { TOKMON_DAEMON_CACHE_DIR: opts.cachePath } : {}
73
+ };
95
74
  return new Promise((resolve) => {
96
75
  let child;
97
76
  try {
@@ -117,7 +96,7 @@ async function attachOrSpawn(opts = {}) {
117
96
  resolve(handle);
118
97
  };
119
98
  const tryAttach = (final = false) => {
120
- void attach(opts, version).then((found) => {
99
+ void attach(lockOpts, protocolVersion).then((found) => {
121
100
  if (found) {
122
101
  finish(found);
123
102
  return;
@@ -143,7 +122,7 @@ async function attachOrSpawn(opts = {}) {
143
122
  const line = stdout.slice(0, newline).trim();
144
123
  stdout = stdout.slice(newline + 1);
145
124
  const handshake = parseHandshake(line);
146
- if (!handshake || handshake.version !== version) continue;
125
+ if (!handshake || handshake.protocolVersion !== protocolVersion || handshake.channel !== channel) continue;
147
126
  tryAttach();
148
127
  return;
149
128
  }