codex-token-tracker 0.1.0 → 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
@@ -1239,6 +1294,8 @@ Options:
1239
1294
  cliUnknownCommand: "Unknown command: {command}",
1240
1295
  cliNoDisplay: "No display detected \u2014 starting agent mode (run `codex-tracker menubar` on a desktop).",
1241
1296
  cliNoElectron: "Electron is not installed (optional dependency) \u2014 starting agent mode.",
1297
+ cliDownloadingElectron: "Electron runtime not found \u2014 downloading it now (about 100 MB, one time)\u2026",
1298
+ cliElectronDownloadFailed: "Electron download failed. Retry with `npm install -g codex-token-tracker --allow-scripts=electron` or `npm rebuild -g electron`; the headless `codex-tracker agent` works without it.",
1242
1299
  cliStartingMenubar: "Starting menu bar app\u2026",
1243
1300
  cliLoginStart: "Connecting this device to {dashboard}",
1244
1301
  cliLoginCode: "Your code: {code}",
@@ -1270,7 +1327,13 @@ Options:
1270
1327
  cliConfigSet: "Set {key} = {value}",
1271
1328
  cliConfigUnknownKey: "Unknown config key: {key}. Keys: {keys}",
1272
1329
  cliLangSet: "Language set to {lang}",
1273
- 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}"
1274
1337
  };
1275
1338
 
1276
1339
  // src/i18n/zh.ts
@@ -1344,7 +1407,18 @@ var zh = {
1344
1407
  never: "\u4ECE\u672A",
1345
1408
  local: "\u672C\u673A",
1346
1409
  remote: "\u5176\u4ED6\u8BBE\u5907",
1347
- 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
1348
1422
 
1349
1423
  \u547D\u4EE4\uFF1A
1350
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
@@ -1355,10 +1429,12 @@ var zh = {
1355
1429
  logout \u65AD\u5F00\u672C\u8BBE\u5907
1356
1430
  status \u6253\u5370\u4ECA\u65E5\u7528\u91CF\u3001\u5B9E\u65F6\u4F1A\u8BDD\u4E0E\u9650\u989D
1357
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
1358
1433
  config get|set \u8BFB\u53D6\u6216\u4FEE\u6539\u8BBE\u7F6E\uFF08config set uploadIntervalSec 30\uFF0Cconfig set sources.pi false\uFF09
1359
1434
  lang <en|zh|auto> \u8BBE\u7F6E\u663E\u793A\u8BED\u8A00
1360
1435
 
1361
1436
  \u9009\u9879\uFF1A
1437
+ --check update\uFF1A\u53EA\u62A5\u544A\u6700\u65B0\u7248\u672C\uFF0C\u4E0D\u6267\u884C\u5B89\u88C5
1362
1438
  --dashboard <url> \u4EEA\u8868\u76D8\u5730\u5740\uFF08\u81EA\u6258\u7BA1\uFF09
1363
1439
  --background \u4EE5\u540E\u53F0\u65B9\u5F0F\u542F\u52A8\u83DC\u5355\u680F\u5E94\u7528\u5E76\u7ACB\u5373\u8FD4\u56DE
1364
1440
  --version, -v \u663E\u793A\u7248\u672C
@@ -1367,6 +1443,8 @@ var zh = {
1367
1443
  cliUnknownCommand: "\u672A\u77E5\u547D\u4EE4\uFF1A{command}",
1368
1444
  cliNoDisplay: "\u672A\u68C0\u6D4B\u5230\u663E\u793A\u73AF\u5883 \u2014 \u4EE5 agent \u6A21\u5F0F\u542F\u52A8\uFF08\u684C\u9762\u73AF\u5883\u8BF7\u8FD0\u884C `codex-tracker menubar`\uFF09\u3002",
1369
1445
  cliNoElectron: "\u672A\u5B89\u88C5 Electron\uFF08\u53EF\u9009\u4F9D\u8D56\uFF09\u2014 \u4EE5 agent \u6A21\u5F0F\u542F\u52A8\u3002",
1446
+ cliDownloadingElectron: "\u672A\u627E\u5230 Electron \u8FD0\u884C\u65F6\uFF0C\u6B63\u5728\u4E0B\u8F7D\uFF08\u7EA6 100 MB\uFF0C\u4EC5\u9700\u4E00\u6B21\uFF09\u2026",
1447
+ cliElectronDownloadFailed: "Electron \u4E0B\u8F7D\u5931\u8D25\u3002\u53EF\u91CD\u8BD5 `npm install -g codex-token-tracker --allow-scripts=electron` \u6216 `npm rebuild -g electron`\uFF1B\u65E0\u754C\u9762\u7684 `codex-tracker agent` \u65E0\u9700 Electron\u3002",
1370
1448
  cliStartingMenubar: "\u6B63\u5728\u542F\u52A8\u83DC\u5355\u680F\u5E94\u7528\u2026",
1371
1449
  cliLoginStart: "\u6B63\u5728\u5C06\u672C\u8BBE\u5907\u8FDE\u63A5\u5230 {dashboard}",
1372
1450
  cliLoginCode: "\u4F60\u7684\u4EE3\u7801\uFF1A{code}",
@@ -1398,7 +1476,13 @@ var zh = {
1398
1476
  cliConfigSet: "\u5DF2\u8BBE\u7F6E {key} = {value}",
1399
1477
  cliConfigUnknownKey: "\u672A\u77E5\u914D\u7F6E\u9879\uFF1A{key}\u3002\u53EF\u7528\uFF1A{keys}",
1400
1478
  cliLangSet: "\u8BED\u8A00\u5DF2\u8BBE\u7F6E\u4E3A {lang}",
1401
- 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}"
1402
1486
  };
1403
1487
 
1404
1488
  // src/i18n/index.ts
@@ -1845,21 +1929,21 @@ function findSessionInfo(storage, sessionID) {
1845
1929
  const key = `${storage}|${sessionID}`;
1846
1930
  const cached = sessionCache.get(key);
1847
1931
  if (cached && (cached.info || Date.now() - cached.at < 6e4)) return cached.info;
1848
- let info = null;
1932
+ let info2 = null;
1849
1933
  const sessionDir = import_node_path6.default.join(storage, "session");
1850
1934
  const direct = import_node_path6.default.join(sessionDir, "info", `${sessionID}.json`);
1851
- if (import_node_fs4.default.existsSync(direct)) info = readJsonFile(direct);
1852
- if (!info) {
1935
+ if (import_node_fs4.default.existsSync(direct)) info2 = readJsonFile(direct);
1936
+ if (!info2) {
1853
1937
  for (const project of listDirs(sessionDir)) {
1854
1938
  const p = import_node_path6.default.join(sessionDir, project, `${sessionID}.json`);
1855
1939
  if (import_node_fs4.default.existsSync(p)) {
1856
- info = readJsonFile(p);
1940
+ info2 = readJsonFile(p);
1857
1941
  break;
1858
1942
  }
1859
1943
  }
1860
1944
  }
1861
- sessionCache.set(key, { at: Date.now(), info });
1862
- return info;
1945
+ sessionCache.set(key, { at: Date.now(), info: info2 });
1946
+ return info2;
1863
1947
  }
1864
1948
  function openaiIsOAuth(dataDir) {
1865
1949
  const file = import_node_path6.default.join(dataDir, "auth.json");
@@ -1929,16 +2013,16 @@ var opencodeSource = {
1929
2013
  provider,
1930
2014
  usage: { input, cached, cacheWrite, output, reasoning: t2.reasoning ?? 0, total: input + output, requests: 1 }
1931
2015
  };
1932
- const info = storage ? findSessionInfo(storage, m.sessionID) : null;
1933
- const cwd = info?.directory ?? null;
2016
+ const info2 = storage ? findSessionInfo(storage, m.sessionID) : null;
2017
+ const cwd = info2?.directory ?? null;
1934
2018
  return {
1935
2019
  sessionId: m.sessionID,
1936
2020
  agent: file.root.agent,
1937
2021
  provider,
1938
- startedAt: info?.time?.created ?? ts2,
2022
+ startedAt: info2?.time?.created ?? ts2,
1939
2023
  lastActivityAt: ts2,
1940
2024
  cwd,
1941
- projectName: projectNameOf(cwd) ?? info?.title ?? null,
2025
+ projectName: projectNameOf(cwd) ?? info2?.title ?? null,
1942
2026
  originator: "opencode",
1943
2027
  source: "opencode",
1944
2028
  cliVersion: null,
@@ -2362,7 +2446,20 @@ var SessionStore = class {
2362
2446
 
2363
2447
  // src/core/stats.ts
2364
2448
  var LIVE_WINDOW_MS = 5 * 60 * 1e3;
2449
+ var RATE_WINDOW_MS = 6e4;
2450
+ var BURST_WINDOW_MS = 1e4;
2365
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
+ }
2366
2463
  var PriceCache = class {
2367
2464
  constructor(overrides) {
2368
2465
  this.overrides = overrides;
@@ -2439,7 +2536,9 @@ function agentStats(sessions, since, prices) {
2439
2536
  function computeStats(input) {
2440
2537
  const now = input.now ?? Date.now();
2441
2538
  const prices = new PriceCache(input.pricing);
2442
- 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
+ );
2443
2542
  const allEvents = [];
2444
2543
  const sessionCosts = /* @__PURE__ */ new Map();
2445
2544
  let lastActivityAt = null;
@@ -2465,14 +2564,8 @@ function computeStats(input) {
2465
2564
  let live = null;
2466
2565
  const liveSession = sessions.filter((s) => s.events.length && now - s.lastActivityAt <= LIVE_WINDOW_MS).sort((a, b) => b.lastActivityAt - a.lastActivityAt)[0];
2467
2566
  if (liveSession) {
2468
- let t60 = 0;
2469
- let t10 = 0;
2470
- for (const e of liveSession.events) {
2471
- const age = now - e.ts;
2472
- if (age <= 6e4) t60 += e.usage.total;
2473
- if (age <= 1e4) t10 += e.usage.total;
2474
- }
2475
2567
  const last = liveSession.events[liveSession.events.length - 1];
2568
+ const start = liveSession.startedAt || liveSession.events[0].ts;
2476
2569
  live = {
2477
2570
  sessionId: liveSession.sessionId,
2478
2571
  agent: liveSession.agent,
@@ -2480,8 +2573,8 @@ function computeStats(input) {
2480
2573
  model: liveSession.model,
2481
2574
  startedAt: liveSession.startedAt,
2482
2575
  lastEventAt: last.ts,
2483
- tokensPerSecond: t60 / 60,
2484
- tokensPerSecond10s: t10 / 10,
2576
+ tokensPerSecond: outputRate(liveSession.events, now, RATE_WINDOW_MS, start),
2577
+ tokensPerSecond10s: outputRate(liveSession.events, now, BURST_WINDOW_MS, start),
2485
2578
  contextUsed: last.usage.input + last.usage.output,
2486
2579
  contextWindow: liveSession.contextWindow,
2487
2580
  sessionUsage: { ...liveSession.cumulative },
@@ -2508,6 +2601,7 @@ function computeStats(input) {
2508
2601
  }
2509
2602
  return {
2510
2603
  buckets,
2604
+ sessions: sessions.filter((s) => s.events.length),
2511
2605
  today,
2512
2606
  week,
2513
2607
  month,
@@ -3865,12 +3959,12 @@ function createApi(pathParts = []) {
3865
3959
  `API path is expected to be of the form \`api.moduleName.functionName\`. Found: \`${found}\``
3866
3960
  );
3867
3961
  }
3868
- const path11 = pathParts.slice(0, -1).join("/");
3962
+ const path12 = pathParts.slice(0, -1).join("/");
3869
3963
  const exportName = pathParts[pathParts.length - 1];
3870
3964
  if (exportName === "default") {
3871
- return path11;
3965
+ return path12;
3872
3966
  } else {
3873
- return path11 + ":" + exportName;
3967
+ return path12 + ":" + exportName;
3874
3968
  }
3875
3969
  } else if (prop === Symbol.toStringTag) {
3876
3970
  return "FunctionReference";
@@ -4034,8 +4128,8 @@ var require_constants = __commonJS({
4034
4128
  });
4035
4129
  var require_node_gyp_build = __commonJS({
4036
4130
  "../node_modules/.pnpm/node-gyp-build@4.8.4/node_modules/node-gyp-build/node-gyp-build.js"(exports2, module2) {
4037
- var fs9 = __require("fs");
4038
- var path11 = __require("path");
4131
+ var fs10 = __require("fs");
4132
+ var path12 = __require("path");
4039
4133
  var os8 = __require("os");
4040
4134
  var runtimeRequire = typeof __webpack_require__ === "function" ? __non_webpack_require__ : __require;
4041
4135
  var vars2 = process.config && process.config.variables || {};
@@ -4052,21 +4146,21 @@ var require_node_gyp_build = __commonJS({
4052
4146
  return runtimeRequire(load.resolve(dir));
4053
4147
  }
4054
4148
  load.resolve = load.path = function(dir) {
4055
- dir = path11.resolve(dir || ".");
4149
+ dir = path12.resolve(dir || ".");
4056
4150
  try {
4057
- 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, "_");
4058
4152
  if (process.env[name + "_PREBUILD"]) dir = process.env[name + "_PREBUILD"];
4059
4153
  } catch (err) {
4060
4154
  }
4061
4155
  if (!prebuildsOnly) {
4062
- var release = getFirst(path11.join(dir, "build/Release"), matchBuild);
4156
+ var release = getFirst(path12.join(dir, "build/Release"), matchBuild);
4063
4157
  if (release) return release;
4064
- var debug = getFirst(path11.join(dir, "build/Debug"), matchBuild);
4158
+ var debug = getFirst(path12.join(dir, "build/Debug"), matchBuild);
4065
4159
  if (debug) return debug;
4066
4160
  }
4067
4161
  var prebuild = resolve(dir);
4068
4162
  if (prebuild) return prebuild;
4069
- var nearby = resolve(path11.dirname(process.execPath));
4163
+ var nearby = resolve(path12.dirname(process.execPath));
4070
4164
  if (nearby) return nearby;
4071
4165
  var target = [
4072
4166
  "platform=" + platform,
@@ -4083,26 +4177,26 @@ var require_node_gyp_build = __commonJS({
4083
4177
  ].filter(Boolean).join(" ");
4084
4178
  throw new Error("No native build was found for " + target + "\n loaded from: " + dir + "\n");
4085
4179
  function resolve(dir2) {
4086
- var tuples = readdirSync(path11.join(dir2, "prebuilds")).map(parseTuple);
4180
+ var tuples = readdirSync(path12.join(dir2, "prebuilds")).map(parseTuple);
4087
4181
  var tuple = tuples.filter(matchTuple(platform, arch)).sort(compareTuples)[0];
4088
4182
  if (!tuple) return;
4089
- var prebuilds = path11.join(dir2, "prebuilds", tuple.name);
4183
+ var prebuilds = path12.join(dir2, "prebuilds", tuple.name);
4090
4184
  var parsed = readdirSync(prebuilds).map(parseTags);
4091
4185
  var candidates = parsed.filter(matchTags(runtime, abi));
4092
4186
  var winner = candidates.sort(compareTags(runtime))[0];
4093
- if (winner) return path11.join(prebuilds, winner.file);
4187
+ if (winner) return path12.join(prebuilds, winner.file);
4094
4188
  }
4095
4189
  };
4096
4190
  function readdirSync(dir) {
4097
4191
  try {
4098
- return fs9.readdirSync(dir);
4192
+ return fs10.readdirSync(dir);
4099
4193
  } catch (err) {
4100
4194
  return [];
4101
4195
  }
4102
4196
  }
4103
4197
  function getFirst(dir, filter) {
4104
4198
  var files = readdirSync(dir).filter(filter);
4105
- return files[0] && path11.join(dir, files[0]);
4199
+ return files[0] && path12.join(dir, files[0]);
4106
4200
  }
4107
4201
  function matchBuild(name) {
4108
4202
  return /\.node$/.test(name);
@@ -4189,7 +4283,7 @@ var require_node_gyp_build = __commonJS({
4189
4283
  return typeof window !== "undefined" && window.process && window.process.type === "renderer";
4190
4284
  }
4191
4285
  function isAlpine(platform2) {
4192
- return platform2 === "linux" && fs9.existsSync("/etc/alpine-release");
4286
+ return platform2 === "linux" && fs10.existsSync("/etc/alpine-release");
4193
4287
  }
4194
4288
  load.parseTags = parseTags;
4195
4289
  load.matchTags = matchTags;
@@ -7735,13 +7829,13 @@ var require_websocket_server = __commonJS({
7735
7829
  }
7736
7830
  }
7737
7831
  if (this.options.verifyClient) {
7738
- const info = {
7832
+ const info2 = {
7739
7833
  origin: req.headers[`${version2 === 8 ? "sec-websocket-origin" : "origin"}`],
7740
7834
  secure: !!(req.socket.authorized || req.socket.encrypted),
7741
7835
  req
7742
7836
  };
7743
7837
  if (this.options.verifyClient.length === 2) {
7744
- this.options.verifyClient(info, (verified, code2, message, headers) => {
7838
+ this.options.verifyClient(info2, (verified, code2, message, headers) => {
7745
7839
  if (!verified) {
7746
7840
  return abortHandshake(socket, code2 || 401, message, headers);
7747
7841
  }
@@ -7757,7 +7851,7 @@ var require_websocket_server = __commonJS({
7757
7851
  });
7758
7852
  return;
7759
7853
  }
7760
- if (!this.options.verifyClient(info)) return abortHandshake(socket, 401);
7854
+ if (!this.options.verifyClient(info2)) return abortHandshake(socket, 401);
7761
7855
  }
7762
7856
  this.completeUpgrade(extensions, key, protocols, req, socket, head, cb);
7763
7857
  }
@@ -8963,10 +9057,174 @@ async function fetchLiveRateLimits(appVersion, timeoutMs = 1e4) {
8963
9057
  }
8964
9058
  }
8965
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
+
8966
9065
  // src/version.ts
8967
- var APP_VERSION = true ? "0.1.0" : "0.0.0-dev";
9066
+ var APP_VERSION = true ? "0.2.0" : "0.0.0-dev";
8968
9067
  var APP_NAME = "Codex Tracker";
8969
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
+
8970
9228
  // src/core/engine.ts
8971
9229
  var SHALLOW_MS = 3e3;
8972
9230
  var DEEP_MS = 6e4;
@@ -8974,6 +9232,7 @@ var TICK_MS = 2e3;
8974
9232
  var REMOTE_MS = 6e4;
8975
9233
  var LIVE_LIMITS_DEBOUNCE_MS = 1e4;
8976
9234
  var LIVE_LIMITS_MIN_GAP_MS = 2e4;
9235
+ var UPDATE_CHECK_MS = 6 * 60 * 60 * 1e3;
8977
9236
  var Engine = class extends import_node_events.EventEmitter {
8978
9237
  constructor(opts) {
8979
9238
  super();
@@ -9010,6 +9269,9 @@ var Engine = class extends import_node_events.EventEmitter {
9010
9269
  liveLimitsInFlight = false;
9011
9270
  liveLimitsTimer = null;
9012
9271
  lastLiveLimitsAttempt = 0;
9272
+ update = null;
9273
+ updateStatus = "idle";
9274
+ updateLog = null;
9013
9275
  get heatmapWeeks() {
9014
9276
  return this.opts.heatmapWeeks ?? 16;
9015
9277
  }
@@ -9031,6 +9293,8 @@ var Engine = class extends import_node_events.EventEmitter {
9031
9293
  return;
9032
9294
  }
9033
9295
  void this.refreshLiveLimits(true);
9296
+ void this.checkUpdate(false);
9297
+ this.timers.push(setInterval(() => void this.checkUpdate(false), UPDATE_CHECK_MS));
9034
9298
  this.store.startWatching(() => void this.refresh(false));
9035
9299
  this.timers.push(setInterval(() => void this.refresh(false), SHALLOW_MS));
9036
9300
  this.timers.push(setInterval(() => void this.refresh(true), DEEP_MS));
@@ -9137,7 +9401,7 @@ var Engine = class extends import_node_events.EventEmitter {
9137
9401
  if (!this.signedIn) return { buckets: 0, sessions: 0 };
9138
9402
  if (!this.stats) this.recompute();
9139
9403
  try {
9140
- 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);
9141
9405
  this.emitSnapshot();
9142
9406
  return r;
9143
9407
  } catch (err) {
@@ -9161,6 +9425,48 @@ var Engine = class extends import_node_events.EventEmitter {
9161
9425
  await this.uploader.fetchRemote(this.heatmapWeeks);
9162
9426
  this.recompute();
9163
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
+ }
9164
9470
  /** Start the device-code login flow (resolves when approved / denied / expired / cancelled). */
9165
9471
  async login(openBrowser, onCode) {
9166
9472
  if (this.pending) return { status: "cancelled" };
@@ -9253,6 +9559,7 @@ var Engine = class extends import_node_events.EventEmitter {
9253
9559
  rateLimits: this.liveLimits ?? fromLogRateLimits(s.logRateLimits),
9254
9560
  rateLimitsError: this.config.liveRateLimits ? this.liveLimitsError : null,
9255
9561
  rateLimitsUpdatedAt: this.liveLimits ? this.liveLimitsAt : s.logRateLimits?.observedAt ?? null,
9562
+ update: this.updateState(),
9256
9563
  modelsToday: s.modelsToday,
9257
9564
  modelsMonth: s.modelsMonth,
9258
9565
  byAgentToday: s.byAgentToday,
@@ -9277,9 +9584,9 @@ var Engine = class extends import_node_events.EventEmitter {
9277
9584
 
9278
9585
  // src/main.ts
9279
9586
  var DEBUG = Boolean(process.env.CODEX_TRACKER_DEBUG);
9280
- var assetsDir = import_node_path10.default.join(__dirname, "..", "assets");
9281
- var rendererIndex = import_node_path10.default.join(__dirname, "renderer", "index.html");
9282
- 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");
9283
9590
  var isMac = process.platform === "darwin";
9284
9591
  var isWin = process.platform === "win32";
9285
9592
  var mb = null;
@@ -9290,12 +9597,12 @@ function log2(...args) {
9290
9597
  }
9291
9598
  function trayIcon() {
9292
9599
  if (isMac) {
9293
- 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"));
9294
9601
  img2.setTemplateImage(true);
9295
9602
  return img2;
9296
9603
  }
9297
9604
  const file = import_electron.nativeTheme.shouldUseDarkColors || !isWin ? "tray-win.png" : "tray-win-light.png";
9298
- 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));
9299
9606
  return isWin ? img.resize({ width: 16, height: 16 }) : img;
9300
9607
  }
9301
9608
  function trayText(s) {
@@ -9315,7 +9622,7 @@ function updateTray(s) {
9315
9622
  mb.tray.setToolTip(`${APP_NAME} \u2014 ${t(L, "today")}: ${formatTokens(s.today.usage.total)} \xB7 ${formatUSD(s.today.cost)}${live}`);
9316
9623
  if (isMac) mb.tray.setTitle(trayText(s), { fontType: "monospacedDigit" });
9317
9624
  }
9318
- 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");
9319
9626
  function setLaunchAtLogin(enabled) {
9320
9627
  if (isMac) {
9321
9628
  if (enabled) {
@@ -9323,20 +9630,20 @@ function setLaunchAtLogin(enabled) {
9323
9630
  <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
9324
9631
  <plist version="1.0"><dict>
9325
9632
  <key>Label</key><string>dev.codex-tracker.menubar</string>
9326
- <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>
9327
9634
  <key>RunAtLoad</key><true/>
9328
9635
  <key>KeepAlive</key><false/>
9329
9636
  <key>ProcessType</key><string>Interactive</string>
9330
9637
  </dict></plist>
9331
9638
  `;
9332
- import_node_fs8.default.mkdirSync(import_node_path10.default.dirname(launchAgentPath), { recursive: true });
9333
- import_node_fs8.default.writeFileSync(launchAgentPath, plist);
9334
- (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], () => {
9335
9642
  });
9336
- } else if (import_node_fs8.default.existsSync(launchAgentPath)) {
9337
- (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], () => {
9338
9645
  try {
9339
- import_node_fs8.default.unlinkSync(launchAgentPath);
9646
+ import_node_fs9.default.unlinkSync(launchAgentPath);
9340
9647
  } catch {
9341
9648
  }
9342
9649
  });
@@ -9344,7 +9651,7 @@ function setLaunchAtLogin(enabled) {
9344
9651
  return;
9345
9652
  }
9346
9653
  if (isWin) {
9347
- 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")] });
9348
9655
  }
9349
9656
  }
9350
9657
  function buildMenu(s) {
@@ -9381,6 +9688,9 @@ function buildMenu(s) {
9381
9688
  }
9382
9689
  ] : [],
9383
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
+ ] : [],
9384
9694
  { type: "separator" },
9385
9695
  { label: t(L, "quit"), click: () => import_electron.app.quit() }
9386
9696
  ];
@@ -9437,6 +9747,8 @@ if (!import_electron.app.requestSingleInstanceLock()) {
9437
9747
  import_electron.ipcMain.handle("auth:cancel", () => engine?.cancelLogin());
9438
9748
  import_electron.ipcMain.handle("auth:logout", () => engine?.logout());
9439
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));
9440
9752
  import_electron.ipcMain.handle("quit", () => import_electron.app.quit());
9441
9753
  mb.on("ready", async () => {
9442
9754
  if (isMac) import_electron.app.dock?.hide();
@@ -9462,7 +9774,7 @@ if (!import_electron.app.requestSingleInstanceLock()) {
9462
9774
  setTimeout(async () => {
9463
9775
  try {
9464
9776
  const img = await mb.window.webContents.capturePage();
9465
- import_node_fs8.default.writeFileSync(shot, img.toPNG());
9777
+ import_node_fs9.default.writeFileSync(shot, img.toPNG());
9466
9778
  log2("screenshot written", shot);
9467
9779
  } catch (err) {
9468
9780
  log2("screenshot failed", errorMessage(err));
@@ -9478,7 +9790,7 @@ if (!import_electron.app.requestSingleInstanceLock()) {
9478
9790
  return { action: "deny" };
9479
9791
  });
9480
9792
  win.webContents.on("console-message", (_e, level, message, line, sourceId) => {
9481
- 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})`);
9482
9794
  });
9483
9795
  win.webContents.on("render-process-gone", (_e, details) => log2("renderer gone", details));
9484
9796
  if (process.env.CODEX_TRACKER_DEVTOOLS) win.webContents.openDevTools({ mode: "detach" });