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
@@ -0,0 +1,278 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ daemonChannelFromWire,
4
+ resolveDaemonChannel
5
+ } from "./chunk-UYPDMVW5.js";
6
+ import {
7
+ cacheDir
8
+ } from "./chunk-HNWEJ6MS.js";
9
+
10
+ // src/web/lockfile.ts
11
+ import { closeSync, fchmodSync, fsyncSync, lstatSync, mkdirSync, openSync, readFileSync, renameSync, statSync, unlinkSync, writeFileSync } from "fs";
12
+ import { isAbsolute, join } from "path";
13
+ function lockDir(opts = {}) {
14
+ const injected = opts.cachePath ?? process.env.TOKMON_DAEMON_CACHE_DIR;
15
+ if (injected && isAbsolute(injected)) return injected;
16
+ const root = cacheDir();
17
+ return resolveDaemonChannel(opts.channel) === "dev" ? join(root, "dev") : root;
18
+ }
19
+ function lockfilePath(opts = {}) {
20
+ return join(lockDir(opts), "daemon.json");
21
+ }
22
+ function isLoopbackUrl(value) {
23
+ try {
24
+ const url = new URL(value);
25
+ return url.protocol === "http:" && (url.hostname === "127.0.0.1" || url.hostname === "localhost" || url.hostname === "::1");
26
+ } catch {
27
+ return false;
28
+ }
29
+ }
30
+ function validLock(value) {
31
+ const lock = value;
32
+ return !!lock && typeof lock.pid === "number" && Number.isInteger(lock.pid) && lock.pid > 0 && typeof lock.port === "number" && Number.isInteger(lock.port) && lock.port >= 0 && lock.port <= 65535 && typeof lock.url === "string" && (lock.state === "starting" || isLoopbackUrl(lock.url)) && typeof lock.wsToken === "string" && lock.wsToken.length >= 32 && typeof lock.version === "string" && typeof lock.protocolVersion === "number" && Number.isSafeInteger(lock.protocolVersion) && lock.protocolVersion >= 1 && Array.isArray(lock.capabilities) && lock.capabilities.every((capability) => typeof capability === "string") && (lock.ownerKind === "cli" || lock.ownerKind === "desktop") && daemonChannelFromWire(lock.channel) !== null && typeof lock.startedAt === "number" && typeof lock.ownerId === "string" && lock.ownerId.length >= 32 && (lock.state === "starting" || lock.state === "ready");
33
+ }
34
+ function readLock(opts = {}) {
35
+ try {
36
+ const path = lockfilePath(opts);
37
+ const stat = statSync(path);
38
+ if (!stat.isFile() || (stat.mode & 63) !== 0) return null;
39
+ const parsed = JSON.parse(readFileSync(path, "utf-8"));
40
+ if (!validLock(parsed)) return null;
41
+ const channel = daemonChannelFromWire(parsed.channel);
42
+ return channel === resolveDaemonChannel(opts.channel) ? { ...parsed, channel } : null;
43
+ } catch {
44
+ return null;
45
+ }
46
+ }
47
+ function readForeignLock(opts = {}) {
48
+ try {
49
+ const path = lockfilePath(opts);
50
+ const stat = statSync(path);
51
+ if (!stat.isFile() || (stat.mode & 63) !== 0) return null;
52
+ const value = JSON.parse(readFileSync(path, "utf-8"));
53
+ const channel = daemonChannelFromWire(value.channel);
54
+ if (!Number.isInteger(value.pid) || value.pid <= 0 || typeof value.url !== "string" || !isLoopbackUrl(value.url) || typeof value.wsToken !== "string" || value.wsToken.length < 32 || value.state !== "ready" || channel !== resolveDaemonChannel(opts.channel)) return null;
55
+ if (value.ownerKind !== void 0 && value.ownerKind !== "cli" && value.ownerKind !== "desktop") return null;
56
+ if (value.protocolVersion !== void 0 && (!Number.isSafeInteger(value.protocolVersion) || value.protocolVersion < 1)) return null;
57
+ return {
58
+ pid: value.pid,
59
+ ...Number.isInteger(value.port) ? { port: value.port } : {},
60
+ url: value.url,
61
+ wsToken: value.wsToken,
62
+ ...typeof value.version === "string" ? { version: value.version } : {},
63
+ ...typeof value.protocolVersion === "number" ? { protocolVersion: value.protocolVersion } : {},
64
+ ...Array.isArray(value.capabilities) && value.capabilities.every((item) => typeof item === "string") ? { capabilities: value.capabilities } : {},
65
+ ...value.ownerKind === "cli" || value.ownerKind === "desktop" ? { ownerKind: value.ownerKind } : {},
66
+ channel,
67
+ ...typeof value.startedAt === "number" ? { startedAt: value.startedAt } : {},
68
+ ...typeof value.ownerId === "string" ? { ownerId: value.ownerId } : {},
69
+ state: "ready"
70
+ };
71
+ } catch {
72
+ return null;
73
+ }
74
+ }
75
+ function ensureLockDir(opts) {
76
+ const dir = lockDir(opts);
77
+ mkdirSync(dir, { recursive: true, mode: 448 });
78
+ let fd;
79
+ try {
80
+ fd = openSync(dir, "r");
81
+ fchmodSync(fd, 448);
82
+ } catch {
83
+ } finally {
84
+ if (fd !== void 0) try {
85
+ closeSync(fd);
86
+ } catch {
87
+ }
88
+ }
89
+ }
90
+ function writeNew(path, lock) {
91
+ let fd;
92
+ let created = false;
93
+ try {
94
+ fd = openSync(path, "wx", 384);
95
+ created = true;
96
+ fchmodSync(fd, 384);
97
+ writeFileSync(fd, JSON.stringify(lock));
98
+ fsyncSync(fd);
99
+ return true;
100
+ } catch {
101
+ if (created) try {
102
+ unlinkSync(path);
103
+ } catch {
104
+ }
105
+ return false;
106
+ } finally {
107
+ if (fd !== void 0) try {
108
+ closeSync(fd);
109
+ } catch {
110
+ }
111
+ }
112
+ }
113
+ function acquireLock(lock, opts = {}) {
114
+ if (lock.channel !== resolveDaemonChannel(opts.channel)) return false;
115
+ try {
116
+ ensureLockDir(opts);
117
+ return writeNew(lockfilePath(opts), lock);
118
+ } catch {
119
+ return false;
120
+ }
121
+ }
122
+ function writeLock(lock, opts = {}) {
123
+ const current = readLock(opts);
124
+ if (!current || current.ownerId !== lock.ownerId || current.pid !== process.pid || lock.channel !== current.channel) return false;
125
+ const path = lockfilePath(opts);
126
+ const tmp = join(lockDir(opts), `daemon.json.${process.pid}.${lock.ownerId}.tmp`);
127
+ let fd;
128
+ try {
129
+ writeFileSync(tmp, JSON.stringify(lock), { mode: 384 });
130
+ try {
131
+ fd = openSync(tmp, "r");
132
+ fchmodSync(fd, 384);
133
+ fsyncSync(fd);
134
+ } catch {
135
+ }
136
+ renameSync(tmp, path);
137
+ return true;
138
+ } catch {
139
+ try {
140
+ unlinkSync(tmp);
141
+ } catch {
142
+ }
143
+ return false;
144
+ } finally {
145
+ if (fd !== void 0) try {
146
+ closeSync(fd);
147
+ } catch {
148
+ }
149
+ }
150
+ }
151
+ function unlinkLock(ownerId, opts = {}) {
152
+ const current = readLock(opts);
153
+ if (!current || current.ownerId !== ownerId || current.pid !== process.pid) return false;
154
+ try {
155
+ unlinkSync(lockfilePath(opts));
156
+ return true;
157
+ } catch {
158
+ return false;
159
+ }
160
+ }
161
+ function reclaimDeadLock(opts = {}) {
162
+ const current = readLock(opts);
163
+ if (!current || isAlive(current.pid)) return false;
164
+ try {
165
+ unlinkSync(lockfilePath(opts));
166
+ return true;
167
+ } catch {
168
+ return false;
169
+ }
170
+ }
171
+ function reclaimAbandonedLock(opts = {}, graceMs = 1e4) {
172
+ const path = lockfilePath(opts);
173
+ try {
174
+ const before = lstatSync(path);
175
+ if (!before.isFile()) return false;
176
+ let pid = null;
177
+ try {
178
+ const parsed = JSON.parse(readFileSync(path, "utf-8"));
179
+ if (Number.isInteger(parsed.pid) && parsed.pid > 0) pid = parsed.pid;
180
+ } catch {
181
+ }
182
+ if (pid !== null && isAlive(pid)) return false;
183
+ if (pid === null && Date.now() - before.mtimeMs < graceMs) return false;
184
+ const after = lstatSync(path);
185
+ if (after.dev !== before.dev || after.ino !== before.ino) return false;
186
+ unlinkSync(path);
187
+ return true;
188
+ } catch {
189
+ return false;
190
+ }
191
+ }
192
+ function isAlive(pid) {
193
+ if (!Number.isInteger(pid) || pid <= 0) return false;
194
+ try {
195
+ process.kill(pid, 0);
196
+ return true;
197
+ } catch (err) {
198
+ return err.code === "EPERM";
199
+ }
200
+ }
201
+ function sameCapabilities(actual, expected) {
202
+ return Array.isArray(actual) && actual.length === expected.length && actual.every((capability, index) => capability === expected[index]);
203
+ }
204
+ async function ownerHealth(url, token, timeoutMs = 500) {
205
+ if (!isLoopbackUrl(url) || !token) return null;
206
+ try {
207
+ const res = await fetch(`${url.replace(/\/+$/, "")}/healthz`, {
208
+ headers: { "x-tokmon-token": token },
209
+ signal: AbortSignal.timeout(timeoutMs)
210
+ });
211
+ if (!res.ok) return null;
212
+ return await res.json();
213
+ } catch {
214
+ return null;
215
+ }
216
+ }
217
+ async function probeHealth(url, token, expected = {}, timeoutMs = 500) {
218
+ const body = await ownerHealth(url, token, timeoutMs);
219
+ const bodyChannel = daemonChannelFromWire(body?.channel);
220
+ return body !== null && body.ok === true && body.owner === true && (expected.version === void 0 || body.version === expected.version) && (expected.protocolVersion === void 0 || body.protocolVersion === expected.protocolVersion) && (expected.capabilities === void 0 || sameCapabilities(body.capabilities, expected.capabilities)) && (expected.ownerKind === void 0 || body.ownerKind === expected.ownerKind) && (expected.channel === void 0 || bodyChannel === expected.channel);
221
+ }
222
+ async function verifyLock(lock, protocolVersion, timeoutMs) {
223
+ if (!lock || lock.state !== "ready" || lock.protocolVersion !== protocolVersion || !isAlive(lock.pid)) return null;
224
+ return await probeHealth(lock.url, lock.wsToken, lock, timeoutMs) ? lock : null;
225
+ }
226
+ var TAKEOVER_TIMEOUT_MS = 5e3;
227
+ var takeoverDelay = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
228
+ function sameForeignOwner(left, right) {
229
+ return left.pid === right.pid && left.wsToken === right.wsToken && left.ownerId === right.ownerId;
230
+ }
231
+ async function retireIncompatibleCliOwner(opts, protocolVersion, timeoutMs = TAKEOVER_TIMEOUT_MS) {
232
+ const compatible = readLock(opts);
233
+ if (compatible?.protocolVersion === protocolVersion) return false;
234
+ const foreign = readForeignLock(opts);
235
+ const legacyCli = foreign?.ownerKind === void 0 && foreign?.protocolVersion === void 0;
236
+ if (!foreign || foreign.state !== "ready" || foreign.ownerKind === "desktop" || foreign.ownerKind !== "cli" && !legacyCli || foreign.pid === process.pid || !isAlive(foreign.pid)) return false;
237
+ const health = await ownerHealth(foreign.url, foreign.wsToken, Math.min(1e3, timeoutMs));
238
+ const healthChannel = daemonChannelFromWire(health?.channel);
239
+ if (health?.ok !== true || health.owner !== true || health.ownerKind !== void 0 && health.ownerKind !== "cli" || healthChannel !== foreign.channel) return false;
240
+ try {
241
+ process.kill(foreign.pid, "SIGTERM");
242
+ } catch {
243
+ if (!isAlive(foreign.pid)) {
244
+ reclaimAbandonedLock(opts, 0);
245
+ return true;
246
+ }
247
+ return false;
248
+ }
249
+ const deadline = Date.now() + Math.max(0, timeoutMs);
250
+ while (Date.now() < deadline) {
251
+ const current = readForeignLock(opts);
252
+ if (!current || !sameForeignOwner(current, foreign)) return true;
253
+ if (!isAlive(foreign.pid)) {
254
+ reclaimAbandonedLock(opts, 0);
255
+ return true;
256
+ }
257
+ await takeoverDelay(50);
258
+ }
259
+ if (!isAlive(foreign.pid)) {
260
+ reclaimAbandonedLock(opts, 0);
261
+ return true;
262
+ }
263
+ return false;
264
+ }
265
+
266
+ export {
267
+ lockfilePath,
268
+ readLock,
269
+ readForeignLock,
270
+ acquireLock,
271
+ writeLock,
272
+ unlinkLock,
273
+ reclaimDeadLock,
274
+ reclaimAbandonedLock,
275
+ isAlive,
276
+ verifyLock,
277
+ retireIncompatibleCliOwner
278
+ };
@@ -0,0 +1,21 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/web/daemon-channel.ts
4
+ var RELEASE_DAEMON_CHANNEL = "release";
5
+ var DEV_DAEMON_CHANNEL = "dev";
6
+ function resolveDaemonChannel(explicit, env = process.env) {
7
+ if (explicit) return explicit;
8
+ const configured = env.TOKMON_CHANNEL ?? env.TOKMON_RUNTIME_CHANNEL ?? env.TOKMON_RUNTIME_TAG;
9
+ return configured === DEV_DAEMON_CHANNEL ? DEV_DAEMON_CHANNEL : RELEASE_DAEMON_CHANNEL;
10
+ }
11
+ function daemonChannelFromWire(value) {
12
+ if (value === void 0) return RELEASE_DAEMON_CHANNEL;
13
+ if (value === RELEASE_DAEMON_CHANNEL) return RELEASE_DAEMON_CHANNEL;
14
+ if (value === DEV_DAEMON_CHANNEL) return DEV_DAEMON_CHANNEL;
15
+ return null;
16
+ }
17
+
18
+ export {
19
+ resolveDaemonChannel,
20
+ daemonChannelFromWire
21
+ };
@@ -1,12 +1,12 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  attachOrSpawn
4
- } from "./chunk-4HU4EI5T.js";
5
- import "./chunk-XDA5RJST.js";
6
- import "./chunk-SMPY52EV.js";
4
+ } from "./chunk-P5JN5DDX.js";
5
+ import "./chunk-USKOQIE3.js";
6
+ import "./chunk-UYPDMVW5.js";
7
7
  import {
8
8
  createDaemonRpcClient
9
- } from "./chunk-3RTWFGGD.js";
9
+ } from "./chunk-7CH6S7BR.js";
10
10
  import {
11
11
  PROVIDERS,
12
12
  antigravityStateDb,
@@ -26,11 +26,13 @@ import {
26
26
  startOfMonth,
27
27
  startOfWeek,
28
28
  withTimeout
29
- } from "./chunk-CVKCVHS7.js";
29
+ } from "./chunk-CFKUZCMB.js";
30
+ import "./chunk-JZSZEHVD.js";
30
31
  import {
32
+ DESKTOP_GRAPH_RANGES,
31
33
  PROVIDER_IDS,
32
34
  configLocation
33
- } from "./chunk-HP5UZCXP.js";
35
+ } from "./chunk-HNWEJ6MS.js";
34
36
 
35
37
  // src/provider-locations.ts
36
38
  import { access } from "fs/promises";
@@ -133,7 +135,7 @@ async function cliSource(account) {
133
135
  providerId: account.providerId,
134
136
  provider: PROVIDERS[account.providerId].name,
135
137
  accountId: account.id,
136
- account: account.email || account.displayName || account.name,
138
+ account: account.identity?.accessibleLabel ?? (account.email || account.displayName || account.name),
137
139
  homeDir: account.homeDir,
138
140
  locations: await providerLocations(account.providerId, account.homeDir ?? void 0)
139
141
  };
@@ -341,15 +343,38 @@ Options:
341
343
  --timeout <seconds> Startup/refresh timeout (default: 45)
342
344
  -h, --help Show this help
343
345
  `;
344
- var CONFIG_HELP = `tokmon config - Print tokmon's configuration file location
346
+ var CONFIG_HELP = `tokmon config - Read and update tokmon settings
345
347
 
346
348
  Usage:
347
349
  tokmon config [path]
348
350
  tokmon config --json
351
+ tokmon config get [--json]
352
+ tokmon config set <setting> <value> [--json]
353
+
354
+ Settings:
355
+ privacy <on|off>
356
+ privacy-key <char>
357
+ menu-bar-pins <ids|none> Comma-separated provider ids; at most 2
358
+ menu-bar-text <on|off>
359
+ summary-mode <smart|tightest>
360
+ expanded-providers <ids|none> Comma-separated provider ids
361
+ active-window <minutes> 1..1440
362
+ graph-range <7|14|30> Desktop spend graph days
363
+ auto-detect <on|off> Automatic account discovery
364
+ auto-detect-providers <ids|none>
365
+ launch-at-login <on|off>
349
366
 
350
367
  Options:
351
- --json Emit { "path": "..." }
368
+ --json Emit machine-readable JSON
369
+ --compact Emit compact JSON
370
+ --timeout <seconds> Daemon connection timeout (default: 45)
352
371
  -h, --help Show this help
372
+
373
+ Examples:
374
+ tokmon config get
375
+ tokmon config set privacy off
376
+ tokmon config set menu-bar-pins claude,codex
377
+ tokmon config set summary-mode smart --json
353
378
  `;
354
379
  function valueAfter(args, index, name) {
355
380
  const value = args[index + 1];
@@ -430,6 +455,15 @@ function isSnapshot(value) {
430
455
  var delay = (ms) => new Promise((resolve) => {
431
456
  setTimeout(resolve, ms);
432
457
  });
458
+ async function connectDaemonConfig(timeoutMs) {
459
+ const handle = await attachOrSpawn({ timeoutMs });
460
+ if (handle.kind !== "spawned" || !handle.baseUrl) throw new Error("tokmon daemon is unavailable");
461
+ return createDaemonRpcClient(handle.baseUrl, {
462
+ transport: "node",
463
+ reconnectAttempts: 2,
464
+ reconnectBaseDelayMs: 100
465
+ });
466
+ }
433
467
  async function fetchDaemonSnapshot(timeoutMs, refresh) {
434
468
  const handle = await attachOrSpawn({ timeoutMs });
435
469
  if (handle.kind !== "spawned" || !handle.baseUrl) throw new Error("tokmon daemon is unavailable");
@@ -475,6 +509,209 @@ function rejectsOption(args, names) {
475
509
  }
476
510
  return null;
477
511
  }
512
+ var CONFIG_SETTINGS = [
513
+ "privacy",
514
+ "privacy-key",
515
+ "menu-bar-pins",
516
+ "menu-bar-text",
517
+ "summary-mode",
518
+ "expanded-providers",
519
+ "active-window",
520
+ "graph-range",
521
+ "auto-detect",
522
+ "auto-detect-providers",
523
+ "launch-at-login"
524
+ ];
525
+ function configReport(config) {
526
+ return {
527
+ revision: config.revision,
528
+ privacy: config.privacyMode ? "on" : "off",
529
+ privacyKey: config.privacyToggleKey,
530
+ menuBarPins: [...config.tray.pinnedProviders],
531
+ menuBarText: config.tray.showMenuBarText ? "on" : "off",
532
+ summaryMode: config.tray.displayMetric === "smartHeadroom" ? "smart" : "tightest",
533
+ expandedProviders: [...config.desktop.expandedProviders],
534
+ activeWindowMinutes: config.tray.activeTimeoutMin,
535
+ graphRangeDays: config.desktop.graphRangeDays,
536
+ autoDetect: config.accountDetection.enabled ? "on" : "off",
537
+ autoDetectProviders: PROVIDER_IDS.filter((id) => !config.accountDetection.disabledProviders.includes(id)),
538
+ launchAtLogin: config.tray.launchAtLogin ? "on" : "off"
539
+ };
540
+ }
541
+ function formatConfigReport(report) {
542
+ const list = (values) => values.length > 0 ? values.join(",") : "none";
543
+ return [
544
+ `privacy ${report.privacy}`,
545
+ `privacy-key ${report.privacyKey}`,
546
+ `menu-bar-pins ${list(report.menuBarPins)}`,
547
+ `menu-bar-text ${report.menuBarText}`,
548
+ `summary-mode ${report.summaryMode}`,
549
+ `expanded-providers ${list(report.expandedProviders)}`,
550
+ `active-window ${report.activeWindowMinutes}m`,
551
+ `graph-range ${report.graphRangeDays}d`,
552
+ `auto-detect ${report.autoDetect}`,
553
+ `auto-detect-providers ${list(report.autoDetectProviders)}`,
554
+ `launch-at-login ${report.launchAtLogin}`
555
+ ].join("\n") + "\n";
556
+ }
557
+ function onOff(value, setting) {
558
+ if (value === "on") return true;
559
+ if (value === "off") return false;
560
+ throw new Error(`${setting} must be on or off`);
561
+ }
562
+ function providerList(value, setting, max = PROVIDER_IDS.length) {
563
+ if (value === "none") return [];
564
+ const raw = value.split(",").map((item) => item.trim());
565
+ if (raw.length === 0 || raw.some((item) => item.length === 0)) {
566
+ throw new Error(`${setting} must be a comma-separated provider list or none`);
567
+ }
568
+ const unique2 = [...new Set(raw)];
569
+ for (const provider of unique2) {
570
+ if (!PROVIDER_IDS.includes(provider)) throw new Error(`unknown provider: ${provider}`);
571
+ }
572
+ if (unique2.length > max) throw new Error(`${setting} accepts at most ${max} providers`);
573
+ return unique2;
574
+ }
575
+ function settingMutation(setting, value) {
576
+ if (setting === "privacy") {
577
+ const enabled2 = onOff(value, setting);
578
+ return { mutate: (config) => ({ ...config, privacyMode: enabled2 }), display: value };
579
+ }
580
+ if (setting === "privacy-key") {
581
+ if (value.length !== 1 || new RegExp("\\s|\\p{Cc}", "u").test(value)) throw new Error("privacy-key must be one printable non-whitespace character");
582
+ return { mutate: (config) => ({ ...config, privacyToggleKey: value }), display: value };
583
+ }
584
+ if (setting === "menu-bar-pins") {
585
+ const providers = providerList(value, setting, 2);
586
+ return {
587
+ mutate: (config) => ({
588
+ ...config,
589
+ tray: { ...config.tray, pinnedProviders: providers, pins: [], pinnedAccount: null }
590
+ }),
591
+ display: providers
592
+ };
593
+ }
594
+ if (setting === "menu-bar-text") {
595
+ const enabled2 = onOff(value, setting);
596
+ return { mutate: (config) => ({ ...config, tray: { ...config.tray, showMenuBarText: enabled2 } }), display: value };
597
+ }
598
+ if (setting === "summary-mode") {
599
+ if (value !== "smart" && value !== "tightest") throw new Error("summary-mode must be smart or tightest");
600
+ const displayMetric = value === "smart" ? "smartHeadroom" : "tightestRemaining";
601
+ return { mutate: (config) => ({ ...config, tray: { ...config.tray, displayMetric } }), display: value };
602
+ }
603
+ if (setting === "expanded-providers") {
604
+ const providers = providerList(value, setting);
605
+ return {
606
+ mutate: (config) => ({ ...config, desktop: { ...config.desktop, expandedProviders: providers } }),
607
+ display: providers
608
+ };
609
+ }
610
+ if (setting === "active-window") {
611
+ const minutes = Number(value);
612
+ if (!Number.isInteger(minutes) || minutes < 1 || minutes > 1440) throw new Error("active-window must be an integer from 1 to 1440 minutes");
613
+ return { mutate: (config) => ({ ...config, tray: { ...config.tray, activeTimeoutMin: minutes } }), display: minutes };
614
+ }
615
+ if (setting === "graph-range") {
616
+ const days = Number(value);
617
+ if (!DESKTOP_GRAPH_RANGES.includes(days)) throw new Error("graph-range must be 7, 14, or 30 days");
618
+ return {
619
+ mutate: (config) => ({ ...config, desktop: { ...config.desktop, graphRangeDays: days } }),
620
+ display: days
621
+ };
622
+ }
623
+ if (setting === "auto-detect") {
624
+ const enabled2 = onOff(value, setting);
625
+ return {
626
+ mutate: (config) => ({ ...config, accountDetection: { ...config.accountDetection, enabled: enabled2 } }),
627
+ display: value
628
+ };
629
+ }
630
+ if (setting === "auto-detect-providers") {
631
+ const enabled2 = providerList(value, setting);
632
+ return {
633
+ mutate: (config) => ({
634
+ ...config,
635
+ accountDetection: {
636
+ ...config.accountDetection,
637
+ disabledProviders: PROVIDER_IDS.filter((provider) => !enabled2.includes(provider))
638
+ }
639
+ }),
640
+ display: enabled2
641
+ };
642
+ }
643
+ if (setting !== "launch-at-login") throw new Error(`unsupported config setting: ${setting}`);
644
+ const enabled = onOff(value, setting);
645
+ return { mutate: (config) => ({ ...config, tray: { ...config.tray, launchAtLogin: enabled } }), display: value };
646
+ }
647
+ function isConfigConflict(cause) {
648
+ return !!cause && typeof cause === "object" && cause.kind === "conflict";
649
+ }
650
+ async function readDaemonConfig(timeoutMs, connect) {
651
+ const client = await connect(timeoutMs);
652
+ try {
653
+ const state = await withTimeout(client.getConfig(), timeoutMs);
654
+ return {
655
+ state,
656
+ close: () => client.close(),
657
+ get: () => withTimeout(client.getConfig(), timeoutMs),
658
+ set: (update) => withTimeout(client.setConfig(update), timeoutMs)
659
+ };
660
+ } catch (cause) {
661
+ await client.close().catch(() => {
662
+ });
663
+ throw cause;
664
+ }
665
+ }
666
+ async function runConfigCommand(args, configPath, connect) {
667
+ const [action, settingName, value, ...extra] = args.positionals;
668
+ if (!action || action === "path") {
669
+ if (settingName || value || extra.length > 0) throw new Error("usage: tokmon config [path] [--json]");
670
+ return args.json ? json({ path: configPath() }, args.compact) : `${configPath()}
671
+ `;
672
+ }
673
+ if (action !== "get" && action !== "set") throw new Error("usage: tokmon config [path|get|set]");
674
+ if (action === "get") {
675
+ if (settingName || value || extra.length > 0) throw new Error("usage: tokmon config get [--json]");
676
+ const daemon2 = await readDaemonConfig(args.timeoutMs, connect);
677
+ try {
678
+ const report = configReport(daemon2.state.config);
679
+ return args.json ? json(report, args.compact) : formatConfigReport(report);
680
+ } finally {
681
+ await daemon2.close().catch(() => {
682
+ });
683
+ }
684
+ }
685
+ if (!settingName || !CONFIG_SETTINGS.includes(settingName) || value === void 0 || extra.length > 0) {
686
+ throw new Error("usage: tokmon config set <setting> <value>");
687
+ }
688
+ const setting = settingName;
689
+ const update = settingMutation(setting, value);
690
+ const daemon = await readDaemonConfig(args.timeoutMs, connect);
691
+ try {
692
+ let base = daemon.state;
693
+ let saved = null;
694
+ for (let attempt = 0; attempt < 2; attempt++) {
695
+ try {
696
+ saved = await daemon.set({
697
+ expectedRevision: base.config.revision,
698
+ config: update.mutate(base.config)
699
+ });
700
+ break;
701
+ } catch (cause) {
702
+ if (attempt > 0 || !isConfigConflict(cause)) throw cause;
703
+ base = await daemon.get();
704
+ }
705
+ }
706
+ if (!saved) throw new Error("config update failed");
707
+ const result = { setting, value: update.display, revision: saved.config.revision };
708
+ return args.json ? json(result, args.compact) : `${setting} ${Array.isArray(update.display) ? update.display.join(",") || "none" : update.display}
709
+ `;
710
+ } finally {
711
+ await daemon.close().catch(() => {
712
+ });
713
+ }
714
+ }
478
715
  function queryHelp(command) {
479
716
  if (command === "providers") return PROVIDERS_HELP;
480
717
  if (command === "snapshot") return SNAPSHOT_HELP;
@@ -486,13 +723,9 @@ async function runQueryCommand(command, args, dependencies = {}) {
486
723
  if (parsed.help) return queryHelp(command);
487
724
  const getConfigPath = dependencies.configPath ?? configLocation;
488
725
  if (command === "config") {
489
- const invalid = rejectsOption(args, ["--period", "--provider", "--account", "--model", "--refresh", "--cached", "--no-refresh", "--timeout"]);
726
+ const invalid = rejectsOption(args, ["--period", "--provider", "--account", "--model", "--refresh", "--cached", "--no-refresh"]);
490
727
  if (invalid) throw new Error(`${invalid} is not valid for tokmon config`);
491
- if (parsed.positionals.length > 1 || parsed.positionals[0] && parsed.positionals[0] !== "path") {
492
- throw new Error("usage: tokmon config [path] [--json]");
493
- }
494
- return parsed.json ? json({ path: getConfigPath() }, parsed.compact) : `${getConfigPath()}
495
- `;
728
+ return runConfigCommand(parsed, getConfigPath, dependencies.connectConfig ?? connectDaemonConfig);
496
729
  }
497
730
  if (command === "providers" || command === "snapshot") {
498
731
  const invalid = rejectsOption(args, ["--period", "--provider", "--account", "--model", "--cached", "--no-refresh"]);
@@ -519,6 +752,7 @@ async function runQueryCommand(command, args, dependencies = {}) {
519
752
  `;
520
753
  }
521
754
  export {
755
+ connectDaemonConfig,
522
756
  fetchDaemonSnapshot,
523
757
  parseQueryArgs,
524
758
  queryHelp,
package/dist/cli.js CHANGED
@@ -41,7 +41,7 @@ function validateServeArgs(serveArgs) {
41
41
  }
42
42
  async function main() {
43
43
  if (subcommand && ["usage", "models", "query", "providers", "snapshot", "config"].includes(subcommand)) {
44
- const { runQueryCommand } = await import("./cli-command-75LP4IDS.js");
44
+ const { runQueryCommand } = await import("./cli-command-MD3RJRP4.js");
45
45
  try {
46
46
  const output = await runQueryCommand(
47
47
  subcommand,
@@ -60,14 +60,14 @@ Run tokmon ${subcommand} --help for usage.
60
60
  return;
61
61
  }
62
62
  if (subcommand === "__daemon") {
63
- const { runDaemon } = await import("./daemon-CL4FJW26.js");
63
+ const { runDaemon } = await import("./daemon-EMOA36WB.js");
64
64
  await runDaemon(args.slice(1), { foreground: false });
65
65
  process.exitCode ??= 0;
66
66
  return;
67
67
  }
68
68
  if (subcommand === "serve" || subcommand === "web") {
69
69
  validateServeArgs(args.slice(1));
70
- const { runDaemon } = await import("./daemon-CL4FJW26.js");
70
+ const { runDaemon } = await import("./daemon-EMOA36WB.js");
71
71
  await runDaemon(args.slice(1), { foreground: true });
72
72
  process.exitCode ??= 0;
73
73
  return;
@@ -115,9 +115,9 @@ Run tokmon ${subcommand} --help for usage.
115
115
  return;
116
116
  }
117
117
  }
118
- const { loadConfig } = await import("./config-HXFJTNLM.js");
118
+ const { loadConfig } = await import("./config-3DGZ47F7.js");
119
119
  const { resolveGlyphs, setGlyphs } = await import("./glyphs-NKCSZLGO.js");
120
- const { attachOrSpawn } = await import("./daemon-handle-QQLJE46Y.js");
120
+ const { attachOrSpawn } = await import("./daemon-handle-JHE5MKMI.js");
121
121
  const config = await loadConfig();
122
122
  setGlyphs(resolveGlyphs({
123
123
  flag: asciiFlag,
@@ -128,7 +128,7 @@ Run tokmon ${subcommand} --help for usage.
128
128
  }));
129
129
  const daemon = await attachOrSpawn();
130
130
  const mode = daemon.kind === "spawned" ? "connected" : "degraded";
131
- const { bootstrapInk } = await import("./bootstrap-ink-L7QSJBMS.js");
131
+ const { bootstrapInk } = await import("./bootstrap-ink-WG4WG3R2.js");
132
132
  await bootstrapInk({ interval, config, daemon, mode });
133
133
  }
134
134
  void main().catch((error) => {