codex-token-tracker 0.1.1 → 0.2.0

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.
package/dist/main.js CHANGED
@@ -24,10 +24,10 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
24
24
 
25
25
  // src/main.ts
26
26
  var import_electron = require("electron");
27
- var import_node_fs8 = __toESM(require("node:fs"));
27
+ var import_node_fs9 = __toESM(require("node:fs"));
28
28
  var import_node_os7 = __toESM(require("node:os"));
29
- var import_node_path10 = __toESM(require("node:path"));
30
- var import_node_child_process3 = require("node:child_process");
29
+ var import_node_path11 = __toESM(require("node:path"));
30
+ var import_node_child_process4 = require("node:child_process");
31
31
  var import_menubar = require("menubar");
32
32
 
33
33
  // ../shared/src/usage.ts
@@ -50,20 +50,53 @@ function cacheHitRate(u) {
50
50
  }
51
51
 
52
52
  // ../shared/src/pricing.ts
53
+ var LONG_CONTEXT_THRESHOLD = 272e3;
54
+ var long = (threshold, input, cachedInput, output) => ({
55
+ threshold,
56
+ input,
57
+ cachedInput,
58
+ output
59
+ });
53
60
  var DEFAULT_PRICING = {
54
- // GPT-5.x family
61
+ // GPT-5.6 family
62
+ "gpt-5.6-sol": { input: 4, cachedInput: 0.4, output: 20, long: long(LONG_CONTEXT_THRESHOLD, 8, 0.8, 30) },
63
+ "gpt-5.6-sol-codex": { input: 4, cachedInput: 0.4, output: 20, long: long(LONG_CONTEXT_THRESHOLD, 8, 0.8, 30) },
64
+ "gpt-5.6-terra": { input: 2, cachedInput: 0.2, output: 12, long: long(LONG_CONTEXT_THRESHOLD, 4, 0.4, 18) },
65
+ "gpt-5.6-terra-codex": { input: 2, cachedInput: 0.2, output: 12, long: long(LONG_CONTEXT_THRESHOLD, 4, 0.4, 18) },
66
+ "gpt-5.6-luna": { input: 0.2, cachedInput: 0.02, output: 1.2, long: long(LONG_CONTEXT_THRESHOLD, 0.4, 0.04, 1.8) },
67
+ "gpt-5.6-cyber": { input: 12.5, cachedInput: 1.25, output: 75 },
68
+ // GPT-5.5 family
69
+ "gpt-5.5": { input: 5, cachedInput: 0.5, output: 30, long: long(LONG_CONTEXT_THRESHOLD, 10, 1, 45) },
70
+ "gpt-5.5-codex": { input: 5, cachedInput: 0.5, output: 30, long: long(LONG_CONTEXT_THRESHOLD, 10, 1, 45) },
71
+ "gpt-5.5-pro": { input: 30, cachedInput: 30, output: 180, long: long(LONG_CONTEXT_THRESHOLD, 60, 60, 270) },
72
+ "gpt-5.5-cyber": { input: 12.5, cachedInput: 1.25, output: 75 },
73
+ // GPT-5.4 family
74
+ "gpt-5.4": { input: 2.5, cachedInput: 0.25, output: 15, long: long(LONG_CONTEXT_THRESHOLD, 5, 0.5, 22.5) },
75
+ "gpt-5.4-codex": { input: 2.5, cachedInput: 0.25, output: 15, long: long(LONG_CONTEXT_THRESHOLD, 5, 0.5, 22.5) },
76
+ "gpt-5.4-mini": { input: 0.75, cachedInput: 0.075, output: 4.5 },
77
+ "gpt-5.4-nano": { input: 0.2, cachedInput: 0.02, output: 1.25 },
78
+ "gpt-5.4-pro": { input: 30, cachedInput: 30, output: 180, long: long(LONG_CONTEXT_THRESHOLD, 60, 60, 270) },
79
+ // GPT-5.3 family (Codex-only release)
80
+ "gpt-5.3": { input: 1.75, cachedInput: 0.175, output: 14 },
81
+ "gpt-5.3-codex": { input: 1.75, cachedInput: 0.175, output: 14 },
82
+ // GPT-5.2 family
83
+ "gpt-5.2": { input: 1.75, cachedInput: 0.175, output: 14 },
84
+ "gpt-5.2-codex": { input: 1.75, cachedInput: 0.175, output: 14 },
85
+ "gpt-5.2-pro": { input: 21, cachedInput: 21, output: 168 },
86
+ // GPT-5.1 family
87
+ "gpt-5.1": { input: 1.25, cachedInput: 0.125, output: 10 },
88
+ "gpt-5.1-codex": { input: 1.25, cachedInput: 0.125, output: 10 },
89
+ "gpt-5.1-codex-max": { input: 1.25, cachedInput: 0.125, output: 10 },
90
+ "gpt-5.1-codex-mini": { input: 0.25, cachedInput: 0.025, output: 2 },
91
+ // GPT-5 family
55
92
  "gpt-5": { input: 1.25, cachedInput: 0.125, output: 10 },
56
93
  "gpt-5-codex": { input: 1.25, cachedInput: 0.125, output: 10 },
57
94
  "gpt-5-mini": { input: 0.25, cachedInput: 0.025, output: 2 },
58
95
  "gpt-5-nano": { input: 0.05, cachedInput: 5e-3, output: 0.4 },
59
96
  "gpt-5-pro": { input: 15, cachedInput: 15, output: 120 },
60
- "gpt-5.1": { input: 1.25, cachedInput: 0.125, output: 10 },
61
- "gpt-5.1-codex": { input: 1.25, cachedInput: 0.125, output: 10 },
62
- "gpt-5.1-codex-max": { input: 1.25, cachedInput: 0.125, output: 10 },
63
- "gpt-5.1-codex-mini": { input: 0.25, cachedInput: 0.025, output: 2 },
64
- "gpt-5.2": { input: 1.75, cachedInput: 0.175, output: 14 },
65
- "gpt-5.2-codex": { input: 1.75, cachedInput: 0.175, output: 14 },
66
- "gpt-5.2-pro": { input: 21, cachedInput: 21, output: 168 },
97
+ "gpt-5-search-api": { input: 1.25, cachedInput: 0.125, output: 10 },
98
+ // ChatGPT-tuned endpoint
99
+ "chat-latest": { input: 5, cachedInput: 0.5, output: 30 },
67
100
  // GPT-4.1 / 4o family
68
101
  "gpt-4.1": { input: 2, cachedInput: 0.5, output: 8 },
69
102
  "gpt-4.1-mini": { input: 0.4, cachedInput: 0.1, output: 1.6 },
@@ -79,7 +112,7 @@ var DEFAULT_PRICING = {
79
112
  "o4-mini": { input: 1.1, cachedInput: 0.275, output: 4.4 },
80
113
  "codex-mini-latest": { input: 1.5, cachedInput: 0.375, output: 6 }
81
114
  };
82
- var FALLBACK_PRICE_KEY = "gpt-5.2";
115
+ var FALLBACK_PRICE_KEY = "gpt-5.3-codex";
83
116
  var TIERS = ["mini", "nano", "pro"];
84
117
  function normalizeModelName(model) {
85
118
  let m = (model || "").trim().toLowerCase();
@@ -89,6 +122,11 @@ function normalizeModelName(model) {
89
122
  m = m.replace(/-preview$/, "");
90
123
  return m;
91
124
  }
125
+ function isOpenAIModel(model) {
126
+ const m = normalizeModelName(model);
127
+ if (m === "unknown") return true;
128
+ return /^(gpt[-.]|chatgpt|chat-latest|o[1-9](?:[-.]|$)|codex|text-|davinci|babbage|ada|curie)/.test(m);
129
+ }
92
130
  function tierOf(name) {
93
131
  for (const t2 of TIERS) if (name.split("-").includes(t2)) return t2;
94
132
  return null;
@@ -128,11 +166,12 @@ function resolvePrice(model, overrides) {
128
166
  }
129
167
  function computeCost(u, p) {
130
168
  const input = u.input ?? 0;
169
+ const rate = p.long && input > p.long.threshold ? p.long : p;
131
170
  const cached = Math.min(u.cached ?? 0, input);
132
171
  const cacheWrite = Math.min(u.cacheWrite ?? 0, Math.max(0, input - cached));
133
172
  const fresh = Math.max(0, input - cached - cacheWrite);
134
173
  const output = u.output ?? 0;
135
- const cost = fresh * p.input + cached * p.cachedInput + cacheWrite * (p.cacheWrite ?? p.input) + output * p.output;
174
+ const cost = fresh * rate.input + cached * rate.cachedInput + cacheWrite * (p.cacheWrite ?? rate.input) + output * rate.output;
136
175
  return cost / 1e6;
137
176
  }
138
177
 
@@ -312,14 +351,14 @@ function createSessionParser(fallbackSessionId) {
312
351
  if (ts2 > lastActivityAt) lastActivityAt = ts2;
313
352
  }
314
353
  function onTokenCount(ts2, payload) {
315
- const info = payload.info;
354
+ const info2 = payload.info;
316
355
  let cumulative = null;
317
356
  let last = null;
318
- if (info && typeof info === "object") {
319
- cumulative = toUsage(info.total_token_usage);
320
- last = toUsage(info.last_token_usage);
321
- if (typeof info.model_context_window === "number") contextWindow = info.model_context_window;
322
- } else if (info === void 0) {
357
+ if (info2 && typeof info2 === "object") {
358
+ cumulative = toUsage(info2.total_token_usage);
359
+ last = toUsage(info2.last_token_usage);
360
+ if (typeof info2.model_context_window === "number") contextWindow = info2.model_context_window;
361
+ } else if (info2 === void 0) {
323
362
  cumulative = toUsage(payload);
324
363
  }
325
364
  const rl = payload.rate_limits;
@@ -1022,7 +1061,8 @@ var DEFAULT_CONFIG = {
1022
1061
  sources: { ...DEFAULT_SOURCES },
1023
1062
  trackAllProviders: false,
1024
1063
  liveRateLimits: true,
1025
- usageRefreshSec: 60
1064
+ usageRefreshSec: 60,
1065
+ checkUpdates: true
1026
1066
  };
1027
1067
  function configDir() {
1028
1068
  return process.env.CODEX_TRACKER_HOME || import_node_path.default.join(import_node_os.default.homedir(), ".codex-tracker");
@@ -1076,6 +1116,7 @@ function loadConfig() {
1076
1116
  cfg.sources = normalizeSources(stored.sources);
1077
1117
  cfg.trackAllProviders = stored.trackAllProviders === true;
1078
1118
  cfg.liveRateLimits = stored.liveRateLimits !== false;
1119
+ cfg.checkUpdates = stored.checkUpdates !== false;
1079
1120
  if (!(cfg.usageRefreshSec >= 15)) cfg.usageRefreshSec = DEFAULT_CONFIG.usageRefreshSec;
1080
1121
  if (!["auto", "en", "zh"].includes(cfg.language)) cfg.language = "auto";
1081
1122
  if (!(cfg.uploadIntervalSec >= 10)) cfg.uploadIntervalSec = DEFAULT_CONFIG.uploadIntervalSec;
@@ -1215,8 +1256,20 @@ var en = {
1215
1256
  never: "never",
1216
1257
  local: "Local",
1217
1258
  remote: "Other devices",
1259
+ // updates
1260
+ update: "Update",
1261
+ updateAvailable: "Update to v{version}",
1262
+ updateNewVersion: "Version {version} is available (you have v{current})",
1263
+ updateUpToDate: "Up to date",
1264
+ updateChecking: "Checking\u2026",
1265
+ updateInstalling: "Installing\u2026",
1266
+ updateInstalled: "Updated to v{version} \u2014 restart to apply",
1267
+ updateFailed: "Update failed \u2014 run this yourself:",
1268
+ updateCheckFailed: "Update check failed: {message}",
1269
+ checkForUpdates: "Check for updates",
1270
+ releaseNotes: "Release notes",
1218
1271
  // CLI
1219
- cliUsage: `Usage: codex-tracker [command] [options]
1272
+ cliUsage: `Usage: codex-token-tracker [command] [options] (alias: codex-tracker)
1220
1273
 
1221
1274
  Commands:
1222
1275
  (none) Start the menu bar app (falls back to agent mode without a display)
@@ -1227,10 +1280,12 @@ Commands:
1227
1280
  logout Disconnect this device
1228
1281
  status Print today's usage, live session and rate limits
1229
1282
  paths Show detected session directories (Codex, pi, OpenCode, Cline/Roo/Kilo, Hermes, custom)
1283
+ update Install the newest published version (--check only reports)
1230
1284
  config get|set Read or change settings (config set uploadIntervalSec 30, config set sources.pi false)
1231
1285
  lang <en|zh|auto> Set the display language
1232
1286
 
1233
1287
  Options:
1288
+ --check update: report the newest version without installing it
1234
1289
  --dashboard <url> Dashboard URL (self-hosted)
1235
1290
  --background Start the menu bar app detached and return
1236
1291
  --version, -v Print version
@@ -1272,7 +1327,13 @@ Options:
1272
1327
  cliConfigSet: "Set {key} = {value}",
1273
1328
  cliConfigUnknownKey: "Unknown config key: {key}. Keys: {keys}",
1274
1329
  cliLangSet: "Language set to {lang}",
1275
- cliConfigDir: "Config: {dir}"
1330
+ cliConfigDir: "Config: {dir}",
1331
+ cliUpdateLatest: "codex-token-tracker {version} is the latest version.",
1332
+ cliUpdateAvailable: "Update available: {current} \u2192 {latest}",
1333
+ cliUpdateRunning: "Running: {command}",
1334
+ cliUpdateDone: "Updated to {version}. Restart the menu bar app (or rerun the CLI) to use it.",
1335
+ cliUpdateFailed: "Update failed (exit {code}). Run it yourself: {command}",
1336
+ cliUpdateCheckFailed: "Could not reach the npm registry: {message}"
1276
1337
  };
1277
1338
 
1278
1339
  // src/i18n/zh.ts
@@ -1346,7 +1407,18 @@ var zh = {
1346
1407
  never: "\u4ECE\u672A",
1347
1408
  local: "\u672C\u673A",
1348
1409
  remote: "\u5176\u4ED6\u8BBE\u5907",
1349
- cliUsage: `\u7528\u6CD5\uFF1Acodex-tracker [\u547D\u4EE4] [\u9009\u9879]
1410
+ update: "\u66F4\u65B0",
1411
+ updateAvailable: "\u66F4\u65B0\u5230 v{version}",
1412
+ updateNewVersion: "\u6709\u65B0\u7248\u672C {version}\uFF08\u5F53\u524D v{current}\uFF09",
1413
+ updateUpToDate: "\u5DF2\u662F\u6700\u65B0\u7248\u672C",
1414
+ updateChecking: "\u68C0\u67E5\u4E2D\u2026",
1415
+ updateInstalling: "\u5B89\u88C5\u4E2D\u2026",
1416
+ updateInstalled: "\u5DF2\u66F4\u65B0\u5230 v{version} \u2014 \u91CD\u542F\u540E\u751F\u6548",
1417
+ updateFailed: "\u66F4\u65B0\u5931\u8D25 \u2014 \u8BF7\u624B\u52A8\u6267\u884C\uFF1A",
1418
+ updateCheckFailed: "\u68C0\u67E5\u66F4\u65B0\u5931\u8D25\uFF1A{message}",
1419
+ checkForUpdates: "\u68C0\u67E5\u66F4\u65B0",
1420
+ releaseNotes: "\u66F4\u65B0\u65E5\u5FD7",
1421
+ cliUsage: `\u7528\u6CD5\uFF1Acodex-token-tracker [\u547D\u4EE4] [\u9009\u9879] \uFF08\u522B\u540D\uFF1Acodex-tracker\uFF09
1350
1422
 
1351
1423
  \u547D\u4EE4\uFF1A
1352
1424
  (\u65E0) \u542F\u52A8\u83DC\u5355\u680F\u5E94\u7528\uFF08\u65E0\u663E\u793A\u73AF\u5883\u65F6\u81EA\u52A8\u5207\u6362\u4E3A agent \u6A21\u5F0F\uFF09
@@ -1357,10 +1429,12 @@ var zh = {
1357
1429
  logout \u65AD\u5F00\u672C\u8BBE\u5907
1358
1430
  status \u6253\u5370\u4ECA\u65E5\u7528\u91CF\u3001\u5B9E\u65F6\u4F1A\u8BDD\u4E0E\u9650\u989D
1359
1431
  paths \u663E\u793A\u68C0\u6D4B\u5230\u7684\u4F1A\u8BDD\u76EE\u5F55\uFF08Codex\u3001pi\u3001OpenCode\u3001Cline/Roo/Kilo\u3001Hermes\u3001\u81EA\u5B9A\u4E49\uFF09
1432
+ update \u5B89\u88C5\u6700\u65B0\u53D1\u5E03\u7248\u672C\uFF08--check \u53EA\u68C0\u67E5\u4E0D\u5B89\u88C5\uFF09
1360
1433
  config get|set \u8BFB\u53D6\u6216\u4FEE\u6539\u8BBE\u7F6E\uFF08config set uploadIntervalSec 30\uFF0Cconfig set sources.pi false\uFF09
1361
1434
  lang <en|zh|auto> \u8BBE\u7F6E\u663E\u793A\u8BED\u8A00
1362
1435
 
1363
1436
  \u9009\u9879\uFF1A
1437
+ --check update\uFF1A\u53EA\u62A5\u544A\u6700\u65B0\u7248\u672C\uFF0C\u4E0D\u6267\u884C\u5B89\u88C5
1364
1438
  --dashboard <url> \u4EEA\u8868\u76D8\u5730\u5740\uFF08\u81EA\u6258\u7BA1\uFF09
1365
1439
  --background \u4EE5\u540E\u53F0\u65B9\u5F0F\u542F\u52A8\u83DC\u5355\u680F\u5E94\u7528\u5E76\u7ACB\u5373\u8FD4\u56DE
1366
1440
  --version, -v \u663E\u793A\u7248\u672C
@@ -1402,7 +1476,13 @@ var zh = {
1402
1476
  cliConfigSet: "\u5DF2\u8BBE\u7F6E {key} = {value}",
1403
1477
  cliConfigUnknownKey: "\u672A\u77E5\u914D\u7F6E\u9879\uFF1A{key}\u3002\u53EF\u7528\uFF1A{keys}",
1404
1478
  cliLangSet: "\u8BED\u8A00\u5DF2\u8BBE\u7F6E\u4E3A {lang}",
1405
- cliConfigDir: "\u914D\u7F6E\u76EE\u5F55\uFF1A{dir}"
1479
+ cliConfigDir: "\u914D\u7F6E\u76EE\u5F55\uFF1A{dir}",
1480
+ cliUpdateLatest: "codex-token-tracker {version} \u5DF2\u662F\u6700\u65B0\u7248\u672C\u3002",
1481
+ cliUpdateAvailable: "\u53D1\u73B0\u65B0\u7248\u672C\uFF1A{current} \u2192 {latest}",
1482
+ cliUpdateRunning: "\u6B63\u5728\u6267\u884C\uFF1A{command}",
1483
+ cliUpdateDone: "\u5DF2\u66F4\u65B0\u5230 {version}\u3002\u8BF7\u91CD\u542F\u83DC\u5355\u680F\u5E94\u7528\uFF08\u6216\u91CD\u65B0\u8FD0\u884C CLI\uFF09\u4EE5\u751F\u6548\u3002",
1484
+ cliUpdateFailed: "\u66F4\u65B0\u5931\u8D25\uFF08\u9000\u51FA\u7801 {code}\uFF09\u3002\u8BF7\u624B\u52A8\u6267\u884C\uFF1A{command}",
1485
+ cliUpdateCheckFailed: "\u65E0\u6CD5\u8BBF\u95EE npm registry\uFF1A{message}"
1406
1486
  };
1407
1487
 
1408
1488
  // src/i18n/index.ts
@@ -1849,21 +1929,21 @@ function findSessionInfo(storage, sessionID) {
1849
1929
  const key = `${storage}|${sessionID}`;
1850
1930
  const cached = sessionCache.get(key);
1851
1931
  if (cached && (cached.info || Date.now() - cached.at < 6e4)) return cached.info;
1852
- let info = null;
1932
+ let info2 = null;
1853
1933
  const sessionDir = import_node_path6.default.join(storage, "session");
1854
1934
  const direct = import_node_path6.default.join(sessionDir, "info", `${sessionID}.json`);
1855
- if (import_node_fs4.default.existsSync(direct)) info = readJsonFile(direct);
1856
- if (!info) {
1935
+ if (import_node_fs4.default.existsSync(direct)) info2 = readJsonFile(direct);
1936
+ if (!info2) {
1857
1937
  for (const project of listDirs(sessionDir)) {
1858
1938
  const p = import_node_path6.default.join(sessionDir, project, `${sessionID}.json`);
1859
1939
  if (import_node_fs4.default.existsSync(p)) {
1860
- info = readJsonFile(p);
1940
+ info2 = readJsonFile(p);
1861
1941
  break;
1862
1942
  }
1863
1943
  }
1864
1944
  }
1865
- sessionCache.set(key, { at: Date.now(), info });
1866
- return info;
1945
+ sessionCache.set(key, { at: Date.now(), info: info2 });
1946
+ return info2;
1867
1947
  }
1868
1948
  function openaiIsOAuth(dataDir) {
1869
1949
  const file = import_node_path6.default.join(dataDir, "auth.json");
@@ -1933,16 +2013,16 @@ var opencodeSource = {
1933
2013
  provider,
1934
2014
  usage: { input, cached, cacheWrite, output, reasoning: t2.reasoning ?? 0, total: input + output, requests: 1 }
1935
2015
  };
1936
- const info = storage ? findSessionInfo(storage, m.sessionID) : null;
1937
- const cwd = info?.directory ?? null;
2016
+ const info2 = storage ? findSessionInfo(storage, m.sessionID) : null;
2017
+ const cwd = info2?.directory ?? null;
1938
2018
  return {
1939
2019
  sessionId: m.sessionID,
1940
2020
  agent: file.root.agent,
1941
2021
  provider,
1942
- startedAt: info?.time?.created ?? ts2,
2022
+ startedAt: info2?.time?.created ?? ts2,
1943
2023
  lastActivityAt: ts2,
1944
2024
  cwd,
1945
- projectName: projectNameOf(cwd) ?? info?.title ?? null,
2025
+ projectName: projectNameOf(cwd) ?? info2?.title ?? null,
1946
2026
  originator: "opencode",
1947
2027
  source: "opencode",
1948
2028
  cliVersion: null,
@@ -2366,7 +2446,20 @@ var SessionStore = class {
2366
2446
 
2367
2447
  // src/core/stats.ts
2368
2448
  var LIVE_WINDOW_MS = 5 * 60 * 1e3;
2449
+ var RATE_WINDOW_MS = 6e4;
2450
+ var BURST_WINDOW_MS = 1e4;
2369
2451
  var DAY3 = 864e5;
2452
+ function outputRate(events, now, windowMs, sessionStart) {
2453
+ const from = now - windowMs;
2454
+ let output = 0;
2455
+ for (const e of events) {
2456
+ if (e.ts <= from) continue;
2457
+ output += e.usage.output;
2458
+ }
2459
+ if (output <= 0) return 0;
2460
+ const elapsedSec = Math.max(1, (now - Math.max(from, sessionStart)) / 1e3);
2461
+ return output / elapsedSec;
2462
+ }
2370
2463
  var PriceCache = class {
2371
2464
  constructor(overrides) {
2372
2465
  this.overrides = overrides;
@@ -2443,7 +2536,9 @@ function agentStats(sessions, since, prices) {
2443
2536
  function computeStats(input) {
2444
2537
  const now = input.now ?? Date.now();
2445
2538
  const prices = new PriceCache(input.pricing);
2446
- const sessions = input.sessions;
2539
+ const sessions = input.sessions.map(
2540
+ (s) => s.events.every((e) => isOpenAIModel(e.model)) ? s : { ...s, events: s.events.filter((e) => isOpenAIModel(e.model)) }
2541
+ );
2447
2542
  const allEvents = [];
2448
2543
  const sessionCosts = /* @__PURE__ */ new Map();
2449
2544
  let lastActivityAt = null;
@@ -2469,14 +2564,8 @@ function computeStats(input) {
2469
2564
  let live = null;
2470
2565
  const liveSession = sessions.filter((s) => s.events.length && now - s.lastActivityAt <= LIVE_WINDOW_MS).sort((a, b) => b.lastActivityAt - a.lastActivityAt)[0];
2471
2566
  if (liveSession) {
2472
- let t60 = 0;
2473
- let t10 = 0;
2474
- for (const e of liveSession.events) {
2475
- const age = now - e.ts;
2476
- if (age <= 6e4) t60 += e.usage.total;
2477
- if (age <= 1e4) t10 += e.usage.total;
2478
- }
2479
2567
  const last = liveSession.events[liveSession.events.length - 1];
2568
+ const start = liveSession.startedAt || liveSession.events[0].ts;
2480
2569
  live = {
2481
2570
  sessionId: liveSession.sessionId,
2482
2571
  agent: liveSession.agent,
@@ -2484,8 +2573,8 @@ function computeStats(input) {
2484
2573
  model: liveSession.model,
2485
2574
  startedAt: liveSession.startedAt,
2486
2575
  lastEventAt: last.ts,
2487
- tokensPerSecond: t60 / 60,
2488
- tokensPerSecond10s: t10 / 10,
2576
+ tokensPerSecond: outputRate(liveSession.events, now, RATE_WINDOW_MS, start),
2577
+ tokensPerSecond10s: outputRate(liveSession.events, now, BURST_WINDOW_MS, start),
2489
2578
  contextUsed: last.usage.input + last.usage.output,
2490
2579
  contextWindow: liveSession.contextWindow,
2491
2580
  sessionUsage: { ...liveSession.cumulative },
@@ -2512,6 +2601,7 @@ function computeStats(input) {
2512
2601
  }
2513
2602
  return {
2514
2603
  buckets,
2604
+ sessions: sessions.filter((s) => s.events.length),
2515
2605
  today,
2516
2606
  week,
2517
2607
  month,
@@ -3869,12 +3959,12 @@ function createApi(pathParts = []) {
3869
3959
  `API path is expected to be of the form \`api.moduleName.functionName\`. Found: \`${found}\``
3870
3960
  );
3871
3961
  }
3872
- const path11 = pathParts.slice(0, -1).join("/");
3962
+ const path12 = pathParts.slice(0, -1).join("/");
3873
3963
  const exportName = pathParts[pathParts.length - 1];
3874
3964
  if (exportName === "default") {
3875
- return path11;
3965
+ return path12;
3876
3966
  } else {
3877
- return path11 + ":" + exportName;
3967
+ return path12 + ":" + exportName;
3878
3968
  }
3879
3969
  } else if (prop === Symbol.toStringTag) {
3880
3970
  return "FunctionReference";
@@ -4038,8 +4128,8 @@ var require_constants = __commonJS({
4038
4128
  });
4039
4129
  var require_node_gyp_build = __commonJS({
4040
4130
  "../node_modules/.pnpm/node-gyp-build@4.8.4/node_modules/node-gyp-build/node-gyp-build.js"(exports2, module2) {
4041
- var fs9 = __require("fs");
4042
- var path11 = __require("path");
4131
+ var fs10 = __require("fs");
4132
+ var path12 = __require("path");
4043
4133
  var os8 = __require("os");
4044
4134
  var runtimeRequire = typeof __webpack_require__ === "function" ? __non_webpack_require__ : __require;
4045
4135
  var vars2 = process.config && process.config.variables || {};
@@ -4056,21 +4146,21 @@ var require_node_gyp_build = __commonJS({
4056
4146
  return runtimeRequire(load.resolve(dir));
4057
4147
  }
4058
4148
  load.resolve = load.path = function(dir) {
4059
- dir = path11.resolve(dir || ".");
4149
+ dir = path12.resolve(dir || ".");
4060
4150
  try {
4061
- var name = runtimeRequire(path11.join(dir, "package.json")).name.toUpperCase().replace(/-/g, "_");
4151
+ var name = runtimeRequire(path12.join(dir, "package.json")).name.toUpperCase().replace(/-/g, "_");
4062
4152
  if (process.env[name + "_PREBUILD"]) dir = process.env[name + "_PREBUILD"];
4063
4153
  } catch (err) {
4064
4154
  }
4065
4155
  if (!prebuildsOnly) {
4066
- var release = getFirst(path11.join(dir, "build/Release"), matchBuild);
4156
+ var release = getFirst(path12.join(dir, "build/Release"), matchBuild);
4067
4157
  if (release) return release;
4068
- var debug = getFirst(path11.join(dir, "build/Debug"), matchBuild);
4158
+ var debug = getFirst(path12.join(dir, "build/Debug"), matchBuild);
4069
4159
  if (debug) return debug;
4070
4160
  }
4071
4161
  var prebuild = resolve(dir);
4072
4162
  if (prebuild) return prebuild;
4073
- var nearby = resolve(path11.dirname(process.execPath));
4163
+ var nearby = resolve(path12.dirname(process.execPath));
4074
4164
  if (nearby) return nearby;
4075
4165
  var target = [
4076
4166
  "platform=" + platform,
@@ -4087,26 +4177,26 @@ var require_node_gyp_build = __commonJS({
4087
4177
  ].filter(Boolean).join(" ");
4088
4178
  throw new Error("No native build was found for " + target + "\n loaded from: " + dir + "\n");
4089
4179
  function resolve(dir2) {
4090
- var tuples = readdirSync(path11.join(dir2, "prebuilds")).map(parseTuple);
4180
+ var tuples = readdirSync(path12.join(dir2, "prebuilds")).map(parseTuple);
4091
4181
  var tuple = tuples.filter(matchTuple(platform, arch)).sort(compareTuples)[0];
4092
4182
  if (!tuple) return;
4093
- var prebuilds = path11.join(dir2, "prebuilds", tuple.name);
4183
+ var prebuilds = path12.join(dir2, "prebuilds", tuple.name);
4094
4184
  var parsed = readdirSync(prebuilds).map(parseTags);
4095
4185
  var candidates = parsed.filter(matchTags(runtime, abi));
4096
4186
  var winner = candidates.sort(compareTags(runtime))[0];
4097
- if (winner) return path11.join(prebuilds, winner.file);
4187
+ if (winner) return path12.join(prebuilds, winner.file);
4098
4188
  }
4099
4189
  };
4100
4190
  function readdirSync(dir) {
4101
4191
  try {
4102
- return fs9.readdirSync(dir);
4192
+ return fs10.readdirSync(dir);
4103
4193
  } catch (err) {
4104
4194
  return [];
4105
4195
  }
4106
4196
  }
4107
4197
  function getFirst(dir, filter) {
4108
4198
  var files = readdirSync(dir).filter(filter);
4109
- return files[0] && path11.join(dir, files[0]);
4199
+ return files[0] && path12.join(dir, files[0]);
4110
4200
  }
4111
4201
  function matchBuild(name) {
4112
4202
  return /\.node$/.test(name);
@@ -4193,7 +4283,7 @@ var require_node_gyp_build = __commonJS({
4193
4283
  return typeof window !== "undefined" && window.process && window.process.type === "renderer";
4194
4284
  }
4195
4285
  function isAlpine(platform2) {
4196
- return platform2 === "linux" && fs9.existsSync("/etc/alpine-release");
4286
+ return platform2 === "linux" && fs10.existsSync("/etc/alpine-release");
4197
4287
  }
4198
4288
  load.parseTags = parseTags;
4199
4289
  load.matchTags = matchTags;
@@ -7739,13 +7829,13 @@ var require_websocket_server = __commonJS({
7739
7829
  }
7740
7830
  }
7741
7831
  if (this.options.verifyClient) {
7742
- const info = {
7832
+ const info2 = {
7743
7833
  origin: req.headers[`${version2 === 8 ? "sec-websocket-origin" : "origin"}`],
7744
7834
  secure: !!(req.socket.authorized || req.socket.encrypted),
7745
7835
  req
7746
7836
  };
7747
7837
  if (this.options.verifyClient.length === 2) {
7748
- this.options.verifyClient(info, (verified, code2, message, headers) => {
7838
+ this.options.verifyClient(info2, (verified, code2, message, headers) => {
7749
7839
  if (!verified) {
7750
7840
  return abortHandshake(socket, code2 || 401, message, headers);
7751
7841
  }
@@ -7761,7 +7851,7 @@ var require_websocket_server = __commonJS({
7761
7851
  });
7762
7852
  return;
7763
7853
  }
7764
- if (!this.options.verifyClient(info)) return abortHandshake(socket, 401);
7854
+ if (!this.options.verifyClient(info2)) return abortHandshake(socket, 401);
7765
7855
  }
7766
7856
  this.completeUpgrade(extensions, key, protocols, req, socket, head, cb);
7767
7857
  }
@@ -8967,10 +9057,174 @@ async function fetchLiveRateLimits(appVersion, timeoutMs = 1e4) {
8967
9057
  }
8968
9058
  }
8969
9059
 
9060
+ // src/core/update.ts
9061
+ var import_node_fs8 = __toESM(require("node:fs"));
9062
+ var import_node_path10 = __toESM(require("node:path"));
9063
+ var import_node_child_process3 = require("node:child_process");
9064
+
8970
9065
  // src/version.ts
8971
- var APP_VERSION = true ? "0.1.1" : "0.0.0-dev";
9066
+ var APP_VERSION = true ? "0.2.0" : "0.0.0-dev";
8972
9067
  var APP_NAME = "Codex Tracker";
8973
9068
 
9069
+ // src/core/update.ts
9070
+ var NPM_PACKAGE = "codex-token-tracker";
9071
+ var CHECK_TTL_MS = 6 * 60 * 60 * 1e3;
9072
+ var REQUEST_TIMEOUT_MS = 8e3;
9073
+ var INSTALL_TIMEOUT_MS = 10 * 60 * 1e3;
9074
+ var LOG_TAIL_CHARS = 4e3;
9075
+ function parseVersion(v2) {
9076
+ const s = String(v2 ?? "").trim().replace(/^v/, "");
9077
+ const i2 = s.indexOf("-");
9078
+ const core = i2 === -1 ? s : s.slice(0, i2);
9079
+ const pre = i2 === -1 ? "" : s.slice(i2 + 1);
9080
+ const n = core.split(".").map((x) => Number.parseInt(x, 10) || 0);
9081
+ return { nums: [n[0] ?? 0, n[1] ?? 0, n[2] ?? 0], pre };
9082
+ }
9083
+ function compareVersions(a, b) {
9084
+ const A = parseVersion(a);
9085
+ const B = parseVersion(b);
9086
+ for (let i2 = 0; i2 < 3; i2++) if (A.nums[i2] !== B.nums[i2]) return A.nums[i2] < B.nums[i2] ? -1 : 1;
9087
+ if (A.pre === B.pre) return 0;
9088
+ if (!A.pre) return 1;
9089
+ if (!B.pre) return -1;
9090
+ return A.pre < B.pre ? -1 : 1;
9091
+ }
9092
+ function isDevBuild(version2) {
9093
+ return version2.startsWith("0.0.0");
9094
+ }
9095
+ function registryBase() {
9096
+ const r = process.env.CODEX_TRACKER_REGISTRY || process.env.npm_config_registry || process.env.NPM_CONFIG_REGISTRY || "https://registry.npmjs.org";
9097
+ return r.replace(/\/+$/, "");
9098
+ }
9099
+ async function fetchLatestVersion(signal) {
9100
+ const ctrl = new AbortController();
9101
+ const timer = setTimeout(() => ctrl.abort(), REQUEST_TIMEOUT_MS);
9102
+ signal?.addEventListener("abort", () => ctrl.abort(), { once: true });
9103
+ try {
9104
+ const res = await fetch(`${registryBase()}/-/package/${NPM_PACKAGE}/dist-tags`, {
9105
+ signal: ctrl.signal,
9106
+ headers: { accept: "application/json" }
9107
+ });
9108
+ if (!res.ok) throw new Error(`registry responded ${res.status}`);
9109
+ const json = await res.json();
9110
+ const latest = json?.latest;
9111
+ if (typeof latest !== "string" || !latest) throw new Error("registry returned no `latest` tag");
9112
+ return latest;
9113
+ } finally {
9114
+ clearTimeout(timer);
9115
+ }
9116
+ }
9117
+ function detectPackageManager(installDir = import_node_path10.default.resolve(__dirname, "..")) {
9118
+ const p = installDir.replace(/\\/g, "/").toLowerCase();
9119
+ if (/\/\.?bun\//.test(p)) return "bun";
9120
+ if (/\/\.?pnpm[/-]/.test(p)) return "pnpm";
9121
+ if (/\/\.?yarn\//.test(p)) return "yarn";
9122
+ const ua = (process.env.npm_config_user_agent ?? "").toLowerCase();
9123
+ if (ua.startsWith("pnpm")) return "pnpm";
9124
+ if (ua.startsWith("yarn")) return "yarn";
9125
+ if (ua.startsWith("bun")) return "bun";
9126
+ return "npm";
9127
+ }
9128
+ function updateArgs(pm, spec = `${NPM_PACKAGE}@latest`) {
9129
+ switch (pm) {
9130
+ case "pnpm":
9131
+ return ["add", "-g", spec];
9132
+ case "yarn":
9133
+ return ["global", "add", spec];
9134
+ case "bun":
9135
+ return ["add", "-g", spec];
9136
+ default:
9137
+ return ["install", "-g", spec];
9138
+ }
9139
+ }
9140
+ function updateCommand(pm, spec) {
9141
+ return `${pm} ${updateArgs(pm, spec).join(" ")}`;
9142
+ }
9143
+ function cachePath() {
9144
+ return import_node_path10.default.join(configDir(), "update.json");
9145
+ }
9146
+ function readCache() {
9147
+ try {
9148
+ const raw = JSON.parse(import_node_fs8.default.readFileSync(cachePath(), "utf8"));
9149
+ if (typeof raw?.latest === "string" && typeof raw.checkedAt === "number") {
9150
+ return { latest: raw.latest, checkedAt: raw.checkedAt };
9151
+ }
9152
+ } catch {
9153
+ }
9154
+ return null;
9155
+ }
9156
+ function writeCache(c) {
9157
+ try {
9158
+ import_node_fs8.default.mkdirSync(import_node_path10.default.dirname(cachePath()), { recursive: true });
9159
+ import_node_fs8.default.writeFileSync(cachePath(), JSON.stringify(c, null, 2));
9160
+ } catch {
9161
+ }
9162
+ }
9163
+ function info(current, latest, checkedAt, error) {
9164
+ const pm = detectPackageManager();
9165
+ return {
9166
+ current,
9167
+ latest,
9168
+ available: Boolean(latest) && !isDevBuild(current) && compareVersions(latest, current) > 0,
9169
+ checkedAt,
9170
+ error,
9171
+ packageManager: pm,
9172
+ command: updateCommand(pm)
9173
+ };
9174
+ }
9175
+ async function checkForUpdate(opts = {}) {
9176
+ const current = opts.current ?? APP_VERSION;
9177
+ const cached = readCache();
9178
+ if (!opts.force && cached && Date.now() - cached.checkedAt < CHECK_TTL_MS) {
9179
+ return info(current, cached.latest, cached.checkedAt, null);
9180
+ }
9181
+ try {
9182
+ const latest = await fetchLatestVersion(opts.signal);
9183
+ const checkedAt = Date.now();
9184
+ writeCache({ latest, checkedAt });
9185
+ return info(current, latest, checkedAt, null);
9186
+ } catch (err) {
9187
+ return info(current, cached?.latest ?? null, cached?.checkedAt ?? null, err.message);
9188
+ }
9189
+ }
9190
+ function runUpdate(opts = {}) {
9191
+ const pm = detectPackageManager();
9192
+ const spec = opts.version ? `${NPM_PACKAGE}@${opts.version}` : `${NPM_PACKAGE}@latest`;
9193
+ const args = updateArgs(pm, spec);
9194
+ const command = `${pm} ${args.join(" ")}`;
9195
+ return new Promise((resolve) => {
9196
+ let output = "";
9197
+ const collect = (chunk) => {
9198
+ const text = chunk.toString();
9199
+ opts.onOutput?.(text);
9200
+ output = (output + text).slice(-LOG_TAIL_CHARS);
9201
+ };
9202
+ let child;
9203
+ try {
9204
+ child = (0, import_node_child_process3.spawn)(pm, args, {
9205
+ // Electron's bundled Node has no shell PATH resolution for `npm.cmd` on Windows.
9206
+ shell: process.platform === "win32",
9207
+ env: { ...process.env, ELECTRON_RUN_AS_NODE: void 0 },
9208
+ windowsHide: true
9209
+ });
9210
+ } catch (err) {
9211
+ resolve({ ok: false, code: null, command, output: err.message });
9212
+ return;
9213
+ }
9214
+ const timer = setTimeout(() => child.kill(), INSTALL_TIMEOUT_MS);
9215
+ child.stdout?.on("data", collect);
9216
+ child.stderr?.on("data", collect);
9217
+ child.on("error", (err) => {
9218
+ clearTimeout(timer);
9219
+ resolve({ ok: false, code: null, command, output: output + err.message });
9220
+ });
9221
+ child.on("close", (code2) => {
9222
+ clearTimeout(timer);
9223
+ resolve({ ok: code2 === 0, code: code2, command, output });
9224
+ });
9225
+ });
9226
+ }
9227
+
8974
9228
  // src/core/engine.ts
8975
9229
  var SHALLOW_MS = 3e3;
8976
9230
  var DEEP_MS = 6e4;
@@ -8978,6 +9232,7 @@ var TICK_MS = 2e3;
8978
9232
  var REMOTE_MS = 6e4;
8979
9233
  var LIVE_LIMITS_DEBOUNCE_MS = 1e4;
8980
9234
  var LIVE_LIMITS_MIN_GAP_MS = 2e4;
9235
+ var UPDATE_CHECK_MS = 6 * 60 * 60 * 1e3;
8981
9236
  var Engine = class extends import_node_events.EventEmitter {
8982
9237
  constructor(opts) {
8983
9238
  super();
@@ -9014,6 +9269,9 @@ var Engine = class extends import_node_events.EventEmitter {
9014
9269
  liveLimitsInFlight = false;
9015
9270
  liveLimitsTimer = null;
9016
9271
  lastLiveLimitsAttempt = 0;
9272
+ update = null;
9273
+ updateStatus = "idle";
9274
+ updateLog = null;
9017
9275
  get heatmapWeeks() {
9018
9276
  return this.opts.heatmapWeeks ?? 16;
9019
9277
  }
@@ -9035,6 +9293,8 @@ var Engine = class extends import_node_events.EventEmitter {
9035
9293
  return;
9036
9294
  }
9037
9295
  void this.refreshLiveLimits(true);
9296
+ void this.checkUpdate(false);
9297
+ this.timers.push(setInterval(() => void this.checkUpdate(false), UPDATE_CHECK_MS));
9038
9298
  this.store.startWatching(() => void this.refresh(false));
9039
9299
  this.timers.push(setInterval(() => void this.refresh(false), SHALLOW_MS));
9040
9300
  this.timers.push(setInterval(() => void this.refresh(true), DEEP_MS));
@@ -9141,7 +9401,7 @@ var Engine = class extends import_node_events.EventEmitter {
9141
9401
  if (!this.signedIn) return { buckets: 0, sessions: 0 };
9142
9402
  if (!this.stats) this.recompute();
9143
9403
  try {
9144
- const r = await this.uploader.pushAll(this.stats.buckets, this.store.sessions(), this.stats.sessionCosts);
9404
+ const r = await this.uploader.pushAll(this.stats.buckets, this.stats.sessions, this.stats.sessionCosts);
9145
9405
  this.emitSnapshot();
9146
9406
  return r;
9147
9407
  } catch (err) {
@@ -9165,6 +9425,48 @@ var Engine = class extends import_node_events.EventEmitter {
9165
9425
  await this.uploader.fetchRemote(this.heatmapWeeks);
9166
9426
  this.recompute();
9167
9427
  }
9428
+ /**
9429
+ * Ask the npm registry for the newest published version (cached for 6 h unless `force`).
9430
+ * Best-effort: a failure is recorded on the snapshot, never thrown.
9431
+ */
9432
+ async checkUpdate(force) {
9433
+ if (!this.config.checkUpdates) {
9434
+ this.update = null;
9435
+ return null;
9436
+ }
9437
+ if (this.updateStatus === "checking" || this.updateStatus === "installing") return this.update;
9438
+ this.updateStatus = "checking";
9439
+ this.emitSnapshot();
9440
+ this.update = await checkForUpdate({ force });
9441
+ this.updateStatus = "idle";
9442
+ if (this.update.error) this.opts.log?.(`update check failed: ${this.update.error}`);
9443
+ this.emitSnapshot();
9444
+ return this.update;
9445
+ }
9446
+ /**
9447
+ * Install the newest version globally with the package manager this copy came from.
9448
+ * The new code only takes effect once the app is restarted, so the UI says so rather than
9449
+ * pretending to hot-swap itself.
9450
+ */
9451
+ async installUpdate() {
9452
+ if (this.updateStatus === "installing") return false;
9453
+ if (!this.update?.available) await this.checkUpdate(true);
9454
+ if (!this.update?.available) return false;
9455
+ this.updateStatus = "installing";
9456
+ this.updateLog = null;
9457
+ this.emitSnapshot();
9458
+ const r = await runUpdate({ version: this.update.latest ?? void 0 });
9459
+ this.updateStatus = r.ok ? "installed" : "failed";
9460
+ this.updateLog = r.ok ? null : `${r.command}
9461
+ ${r.output}`.trim();
9462
+ if (!r.ok) this.opts.log?.(`update install failed (${r.code}): ${r.output}`);
9463
+ this.emitSnapshot();
9464
+ return r.ok;
9465
+ }
9466
+ updateState() {
9467
+ if (!this.config.checkUpdates || !this.update) return null;
9468
+ return { ...this.update, status: this.updateStatus, log: this.updateLog };
9469
+ }
9168
9470
  /** Start the device-code login flow (resolves when approved / denied / expired / cancelled). */
9169
9471
  async login(openBrowser, onCode) {
9170
9472
  if (this.pending) return { status: "cancelled" };
@@ -9257,6 +9559,7 @@ var Engine = class extends import_node_events.EventEmitter {
9257
9559
  rateLimits: this.liveLimits ?? fromLogRateLimits(s.logRateLimits),
9258
9560
  rateLimitsError: this.config.liveRateLimits ? this.liveLimitsError : null,
9259
9561
  rateLimitsUpdatedAt: this.liveLimits ? this.liveLimitsAt : s.logRateLimits?.observedAt ?? null,
9562
+ update: this.updateState(),
9260
9563
  modelsToday: s.modelsToday,
9261
9564
  modelsMonth: s.modelsMonth,
9262
9565
  byAgentToday: s.byAgentToday,
@@ -9281,9 +9584,9 @@ var Engine = class extends import_node_events.EventEmitter {
9281
9584
 
9282
9585
  // src/main.ts
9283
9586
  var DEBUG = Boolean(process.env.CODEX_TRACKER_DEBUG);
9284
- var assetsDir = import_node_path10.default.join(__dirname, "..", "assets");
9285
- var rendererIndex = import_node_path10.default.join(__dirname, "renderer", "index.html");
9286
- var preloadPath = import_node_path10.default.join(__dirname, "preload.js");
9587
+ var assetsDir = import_node_path11.default.join(__dirname, "..", "assets");
9588
+ var rendererIndex = import_node_path11.default.join(__dirname, "renderer", "index.html");
9589
+ var preloadPath = import_node_path11.default.join(__dirname, "preload.js");
9287
9590
  var isMac = process.platform === "darwin";
9288
9591
  var isWin = process.platform === "win32";
9289
9592
  var mb = null;
@@ -9294,12 +9597,12 @@ function log2(...args) {
9294
9597
  }
9295
9598
  function trayIcon() {
9296
9599
  if (isMac) {
9297
- const img2 = import_electron.nativeImage.createFromPath(import_node_path10.default.join(assetsDir, "trayTemplate.png"));
9600
+ const img2 = import_electron.nativeImage.createFromPath(import_node_path11.default.join(assetsDir, "trayTemplate.png"));
9298
9601
  img2.setTemplateImage(true);
9299
9602
  return img2;
9300
9603
  }
9301
9604
  const file = import_electron.nativeTheme.shouldUseDarkColors || !isWin ? "tray-win.png" : "tray-win-light.png";
9302
- const img = import_electron.nativeImage.createFromPath(import_node_path10.default.join(assetsDir, file));
9605
+ const img = import_electron.nativeImage.createFromPath(import_node_path11.default.join(assetsDir, file));
9303
9606
  return isWin ? img.resize({ width: 16, height: 16 }) : img;
9304
9607
  }
9305
9608
  function trayText(s) {
@@ -9319,7 +9622,7 @@ function updateTray(s) {
9319
9622
  mb.tray.setToolTip(`${APP_NAME} \u2014 ${t(L, "today")}: ${formatTokens(s.today.usage.total)} \xB7 ${formatUSD(s.today.cost)}${live}`);
9320
9623
  if (isMac) mb.tray.setTitle(trayText(s), { fontType: "monospacedDigit" });
9321
9624
  }
9322
- var launchAgentPath = import_node_path10.default.join(import_node_os7.default.homedir(), "Library", "LaunchAgents", "dev.codex-tracker.menubar.plist");
9625
+ var launchAgentPath = import_node_path11.default.join(import_node_os7.default.homedir(), "Library", "LaunchAgents", "dev.codex-tracker.menubar.plist");
9323
9626
  function setLaunchAtLogin(enabled) {
9324
9627
  if (isMac) {
9325
9628
  if (enabled) {
@@ -9327,20 +9630,20 @@ function setLaunchAtLogin(enabled) {
9327
9630
  <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
9328
9631
  <plist version="1.0"><dict>
9329
9632
  <key>Label</key><string>dev.codex-tracker.menubar</string>
9330
- <key>ProgramArguments</key><array><string>${process.execPath}</string><string>${import_node_path10.default.join(__dirname, "main.js")}</string></array>
9633
+ <key>ProgramArguments</key><array><string>${process.execPath}</string><string>${import_node_path11.default.join(__dirname, "main.js")}</string></array>
9331
9634
  <key>RunAtLoad</key><true/>
9332
9635
  <key>KeepAlive</key><false/>
9333
9636
  <key>ProcessType</key><string>Interactive</string>
9334
9637
  </dict></plist>
9335
9638
  `;
9336
- import_node_fs8.default.mkdirSync(import_node_path10.default.dirname(launchAgentPath), { recursive: true });
9337
- import_node_fs8.default.writeFileSync(launchAgentPath, plist);
9338
- (0, import_node_child_process3.execFile)("launchctl", ["load", "-w", launchAgentPath], () => {
9639
+ import_node_fs9.default.mkdirSync(import_node_path11.default.dirname(launchAgentPath), { recursive: true });
9640
+ import_node_fs9.default.writeFileSync(launchAgentPath, plist);
9641
+ (0, import_node_child_process4.execFile)("launchctl", ["load", "-w", launchAgentPath], () => {
9339
9642
  });
9340
- } else if (import_node_fs8.default.existsSync(launchAgentPath)) {
9341
- (0, import_node_child_process3.execFile)("launchctl", ["unload", "-w", launchAgentPath], () => {
9643
+ } else if (import_node_fs9.default.existsSync(launchAgentPath)) {
9644
+ (0, import_node_child_process4.execFile)("launchctl", ["unload", "-w", launchAgentPath], () => {
9342
9645
  try {
9343
- import_node_fs8.default.unlinkSync(launchAgentPath);
9646
+ import_node_fs9.default.unlinkSync(launchAgentPath);
9344
9647
  } catch {
9345
9648
  }
9346
9649
  });
@@ -9348,7 +9651,7 @@ function setLaunchAtLogin(enabled) {
9348
9651
  return;
9349
9652
  }
9350
9653
  if (isWin) {
9351
- import_electron.app.setLoginItemSettings({ openAtLogin: enabled, path: process.execPath, args: [import_node_path10.default.join(__dirname, "main.js")] });
9654
+ import_electron.app.setLoginItemSettings({ openAtLogin: enabled, path: process.execPath, args: [import_node_path11.default.join(__dirname, "main.js")] });
9352
9655
  }
9353
9656
  }
9354
9657
  function buildMenu(s) {
@@ -9385,6 +9688,9 @@ function buildMenu(s) {
9385
9688
  }
9386
9689
  ] : [],
9387
9690
  { label: t(L, "refresh"), click: () => void engine?.refresh(true) },
9691
+ ...s.update ? [
9692
+ s.update.available ? { label: t(L, "updateAvailable", { version: s.update.latest ?? "?" }), click: () => void engine?.installUpdate() } : { label: t(L, "checkForUpdates"), click: () => void engine?.checkUpdate(true) }
9693
+ ] : [],
9388
9694
  { type: "separator" },
9389
9695
  { label: t(L, "quit"), click: () => import_electron.app.quit() }
9390
9696
  ];
@@ -9441,6 +9747,8 @@ if (!import_electron.app.requestSingleInstanceLock()) {
9441
9747
  import_electron.ipcMain.handle("auth:cancel", () => engine?.cancelLogin());
9442
9748
  import_electron.ipcMain.handle("auth:logout", () => engine?.logout());
9443
9749
  import_electron.ipcMain.handle("refresh", () => engine?.refresh(true).then(() => void 0));
9750
+ import_electron.ipcMain.handle("update:check", () => engine?.checkUpdate(true).then(() => void 0));
9751
+ import_electron.ipcMain.handle("update:install", () => engine?.installUpdate().then(() => void 0));
9444
9752
  import_electron.ipcMain.handle("quit", () => import_electron.app.quit());
9445
9753
  mb.on("ready", async () => {
9446
9754
  if (isMac) import_electron.app.dock?.hide();
@@ -9466,7 +9774,7 @@ if (!import_electron.app.requestSingleInstanceLock()) {
9466
9774
  setTimeout(async () => {
9467
9775
  try {
9468
9776
  const img = await mb.window.webContents.capturePage();
9469
- import_node_fs8.default.writeFileSync(shot, img.toPNG());
9777
+ import_node_fs9.default.writeFileSync(shot, img.toPNG());
9470
9778
  log2("screenshot written", shot);
9471
9779
  } catch (err) {
9472
9780
  log2("screenshot failed", errorMessage(err));
@@ -9482,7 +9790,7 @@ if (!import_electron.app.requestSingleInstanceLock()) {
9482
9790
  return { action: "deny" };
9483
9791
  });
9484
9792
  win.webContents.on("console-message", (_e, level, message, line, sourceId) => {
9485
- if (DEBUG || level >= 2) console.error(`[renderer:${level}] ${message} (${import_node_path10.default.basename(sourceId)}:${line})`);
9793
+ if (DEBUG || level >= 2) console.error(`[renderer:${level}] ${message} (${import_node_path11.default.basename(sourceId)}:${line})`);
9486
9794
  });
9487
9795
  win.webContents.on("render-process-gone", (_e, details) => log2("renderer gone", details));
9488
9796
  if (process.env.CODEX_TRACKER_DEVTOOLS) win.webContents.openDevTools({ mode: "detach" });