codex-token-tracker 0.1.1 → 0.2.1

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;
@@ -1003,7 +1042,17 @@ var import_node_events = require("node:events");
1003
1042
  var import_node_fs = __toESM(require("node:fs"));
1004
1043
  var import_node_os = __toESM(require("node:os"));
1005
1044
  var import_node_path = __toESM(require("node:path"));
1006
- var DEFAULT_DASHBOARD_URL = "https://codex.chenli.dev";
1045
+
1046
+ // src/version.ts
1047
+ var APP_VERSION = true ? "0.2.1" : "0.0.0-dev";
1048
+ var APP_CHANNEL = true ? "prod" : "dev";
1049
+ var IS_DEV_BUILD = APP_CHANNEL === "dev";
1050
+ var APP_NAME = IS_DEV_BUILD ? "Codex Tracker (dev)" : "Codex Tracker";
1051
+
1052
+ // src/core/config.ts
1053
+ var PROD_DASHBOARD_URL = "https://codex.chenli.dev";
1054
+ var DEV_DASHBOARD_URL = "http://localhost:3000";
1055
+ var DEFAULT_DASHBOARD_URL = IS_DEV_BUILD ? DEV_DASHBOARD_URL : PROD_DASHBOARD_URL;
1007
1056
  var SOURCE_FORMATS = ["codex", "pi", "generic", "opencode", "cline"];
1008
1057
  var DEFAULT_SOURCES = { codex: true, pi: true, hermes: true, opencode: true, cline: true, roo: true, kilo: true };
1009
1058
  var SOURCE_IDS = Object.keys(DEFAULT_SOURCES);
@@ -1022,10 +1071,12 @@ var DEFAULT_CONFIG = {
1022
1071
  sources: { ...DEFAULT_SOURCES },
1023
1072
  trackAllProviders: false,
1024
1073
  liveRateLimits: true,
1025
- usageRefreshSec: 60
1074
+ usageRefreshSec: 60,
1075
+ // a dev build must never offer to replace itself with the published package
1076
+ checkUpdates: !IS_DEV_BUILD
1026
1077
  };
1027
1078
  function configDir() {
1028
- return process.env.CODEX_TRACKER_HOME || import_node_path.default.join(import_node_os.default.homedir(), ".codex-tracker");
1079
+ return process.env.CODEX_TRACKER_HOME || import_node_path.default.join(import_node_os.default.homedir(), IS_DEV_BUILD ? ".codex-tracker-dev" : ".codex-tracker");
1029
1080
  }
1030
1081
  function readJson(file, fallback) {
1031
1082
  try {
@@ -1076,6 +1127,7 @@ function loadConfig() {
1076
1127
  cfg.sources = normalizeSources(stored.sources);
1077
1128
  cfg.trackAllProviders = stored.trackAllProviders === true;
1078
1129
  cfg.liveRateLimits = stored.liveRateLimits !== false;
1130
+ cfg.checkUpdates = typeof stored.checkUpdates === "boolean" ? stored.checkUpdates : DEFAULT_CONFIG.checkUpdates;
1079
1131
  if (!(cfg.usageRefreshSec >= 15)) cfg.usageRefreshSec = DEFAULT_CONFIG.usageRefreshSec;
1080
1132
  if (!["auto", "en", "zh"].includes(cfg.language)) cfg.language = "auto";
1081
1133
  if (!(cfg.uploadIntervalSec >= 10)) cfg.uploadIntervalSec = DEFAULT_CONFIG.uploadIntervalSec;
@@ -1189,6 +1241,7 @@ var en = {
1189
1241
  estimatedPricingHint: "Pricing estimated: model not in the price table",
1190
1242
  other: "Other",
1191
1243
  lastUpload: "Last upload {time}",
1244
+ lastSync: "Last full sync {time}",
1192
1245
  neverUploaded: "Not uploaded yet",
1193
1246
  uploadError: "Upload error: {message}",
1194
1247
  allDevicesToday: "All devices today",
@@ -1207,16 +1260,44 @@ var en = {
1207
1260
  launchAtLogin: "Launch at login",
1208
1261
  refresh: "Refresh",
1209
1262
  quit: "Quit",
1263
+ // full sync
1264
+ syncNow: "Sync now",
1265
+ syncing: "Syncing\u2026",
1266
+ syncTitle: "Rescan every agent on this device and re-upload its full usage history",
1267
+ syncScanning: "Rescanning every agent's transcripts\u2026",
1268
+ syncComputing: "Recomputing token usage\u2026",
1269
+ syncUploading: "Re-uploading this device's history\u2026",
1270
+ syncDownloading: "Fetching other devices\u2026",
1271
+ syncLimits: "Refreshing rate limits\u2026",
1272
+ syncDone: "Synced {agents} \xB7 {files} files \xB7 {sessions} sessions \xB7 {buckets} hours re-uploaded",
1273
+ syncDoneLocal: "Rescanned {agents} \xB7 {files} files \xB7 {sessions} sessions \u2014 sign in to upload",
1274
+ syncFailed: "Sync failed: {message}",
1210
1275
  sessions: "{n} sessions",
1211
1276
  files: "{n} files",
1212
1277
  noData: "No usage found yet. Run Codex and this will fill in.",
1213
1278
  version: "v{version}",
1279
+ // dev builds
1280
+ devBuild: "DEV",
1281
+ devBuildTitle: "Local build \u2014 uploading to {url} (dev environment). Config: {dir}",
1282
+ devBuildMenu: "Dev build \u2192 {url}",
1214
1283
  justNow: "just now",
1215
1284
  never: "never",
1216
1285
  local: "Local",
1217
1286
  remote: "Other devices",
1287
+ // updates
1288
+ update: "Update",
1289
+ updateAvailable: "Update to v{version}",
1290
+ updateNewVersion: "Version {version} is available (you have v{current})",
1291
+ updateUpToDate: "Up to date",
1292
+ updateChecking: "Checking\u2026",
1293
+ updateInstalling: "Installing\u2026",
1294
+ updateInstalled: "Updated to v{version} \u2014 restart to apply",
1295
+ updateFailed: "Update failed \u2014 run this yourself:",
1296
+ updateCheckFailed: "Update check failed: {message}",
1297
+ checkForUpdates: "Check for updates",
1298
+ releaseNotes: "Release notes",
1218
1299
  // CLI
1219
- cliUsage: `Usage: codex-tracker [command] [options]
1300
+ cliUsage: `Usage: codex-token-tracker [command] [options] (alias: codex-tracker)
1220
1301
 
1221
1302
  Commands:
1222
1303
  (none) Start the menu bar app (falls back to agent mode without a display)
@@ -1226,11 +1307,14 @@ Commands:
1226
1307
  login Connect this device to your dashboard (--dashboard <url>)
1227
1308
  logout Disconnect this device
1228
1309
  status Print today's usage, live session and rate limits
1310
+ sync Rescan every agent and re-upload this device's full history (calibrate)
1229
1311
  paths Show detected session directories (Codex, pi, OpenCode, Cline/Roo/Kilo, Hermes, custom)
1312
+ update Install the newest published version (--check only reports)
1230
1313
  config get|set Read or change settings (config set uploadIntervalSec 30, config set sources.pi false)
1231
1314
  lang <en|zh|auto> Set the display language
1232
1315
 
1233
1316
  Options:
1317
+ --check update: report the newest version without installing it
1234
1318
  --dashboard <url> Dashboard URL (self-hosted)
1235
1319
  --background Start the menu bar app detached and return
1236
1320
  --version, -v Print version
@@ -1272,7 +1356,21 @@ Options:
1272
1356
  cliConfigSet: "Set {key} = {value}",
1273
1357
  cliConfigUnknownKey: "Unknown config key: {key}. Keys: {keys}",
1274
1358
  cliLangSet: "Language set to {lang}",
1275
- cliConfigDir: "Config: {dir}"
1359
+ cliConfigDir: "Config: {dir}",
1360
+ cliUpdateLatest: "codex-token-tracker {version} is the latest version.",
1361
+ cliUpdateAvailable: "Update available: {current} \u2192 {latest}",
1362
+ cliUpdateRunning: "Running: {command}",
1363
+ cliUpdateDone: "Updated to {version}. Restart the menu bar app (or rerun the CLI) to use it.",
1364
+ cliUpdateFailed: "Update failed (exit {code}). Run it yourself: {command}",
1365
+ cliUpdateCheckFailed: "Could not reach the npm registry: {message}",
1366
+ cliSyncStart: "Full sync: rescanning every agent on this device\u2026",
1367
+ cliSyncScanned: " scanned {files} files in {roots} directories ({agents})",
1368
+ cliSyncUploaded: " re-uploaded {buckets} hour buckets and {sessions} sessions",
1369
+ cliSyncLocal: " not signed in \u2014 nothing uploaded (run `codex-tracker login` first)",
1370
+ cliSyncDone: "Sync complete in {seconds}s \u2014 {sessions} sessions, today {tokens} tok \xB7 {cost}",
1371
+ cliSyncFailed: "Sync failed: {message}",
1372
+ cliChannelDev: "Dev build \u2014 dashboard {url} \xB7 config {dir}",
1373
+ cliUpdateDevBuild: "This is a local dev build; `update` would install the published package over it. Run `pnpm build` in the repo instead."
1276
1374
  };
1277
1375
 
1278
1376
  // src/i18n/zh.ts
@@ -1320,6 +1418,7 @@ var zh = {
1320
1418
  estimatedPricingHint: "\u4EF7\u683C\u4E3A\u4F30\u7B97\u503C\uFF1A\u8BE5\u6A21\u578B\u4E0D\u5728\u4EF7\u76EE\u8868\u4E2D",
1321
1419
  other: "\u5176\u4ED6",
1322
1420
  lastUpload: "\u4E0A\u6B21\u4E0A\u4F20 {time}",
1421
+ lastSync: "\u4E0A\u6B21\u5B8C\u6574\u540C\u6B65 {time}",
1323
1422
  neverUploaded: "\u5C1A\u672A\u4E0A\u4F20",
1324
1423
  uploadError: "\u4E0A\u4F20\u5931\u8D25\uFF1A{message}",
1325
1424
  allDevicesToday: "\u6240\u6709\u8BBE\u5907\u4ECA\u65E5",
@@ -1338,15 +1437,42 @@ var zh = {
1338
1437
  launchAtLogin: "\u5F00\u673A\u81EA\u542F\u52A8",
1339
1438
  refresh: "\u5237\u65B0",
1340
1439
  quit: "\u9000\u51FA",
1440
+ // 完整同步
1441
+ syncNow: "\u7ACB\u5373\u540C\u6B65",
1442
+ syncing: "\u540C\u6B65\u4E2D\u2026",
1443
+ syncTitle: "\u91CD\u65B0\u626B\u63CF\u672C\u8BBE\u5907\u4E0A\u7684\u6240\u6709\u667A\u80FD\u4F53\uFF0C\u5E76\u91CD\u65B0\u4E0A\u4F20\u5B8C\u6574\u7528\u91CF\u5386\u53F2",
1444
+ syncScanning: "\u6B63\u5728\u91CD\u65B0\u626B\u63CF\u6240\u6709\u667A\u80FD\u4F53\u7684\u4F1A\u8BDD\u8BB0\u5F55\u2026",
1445
+ syncComputing: "\u6B63\u5728\u91CD\u65B0\u8BA1\u7B97 token \u7528\u91CF\u2026",
1446
+ syncUploading: "\u6B63\u5728\u91CD\u65B0\u4E0A\u4F20\u672C\u8BBE\u5907\u7684\u5386\u53F2\u6570\u636E\u2026",
1447
+ syncDownloading: "\u6B63\u5728\u62C9\u53D6\u5176\u4ED6\u8BBE\u5907\u7684\u6570\u636E\u2026",
1448
+ syncLimits: "\u6B63\u5728\u5237\u65B0\u9650\u989D\u2026",
1449
+ syncDone: "\u5DF2\u540C\u6B65 {agents} \xB7 {files} \u4E2A\u6587\u4EF6 \xB7 {sessions} \u4E2A\u4F1A\u8BDD \xB7 \u91CD\u65B0\u4E0A\u4F20 {buckets} \u4E2A\u5C0F\u65F6\u6BB5",
1450
+ syncDoneLocal: "\u5DF2\u91CD\u65B0\u626B\u63CF {agents} \xB7 {files} \u4E2A\u6587\u4EF6 \xB7 {sessions} \u4E2A\u4F1A\u8BDD \u2014 \u767B\u5F55\u540E\u53EF\u4E0A\u4F20",
1451
+ syncFailed: "\u540C\u6B65\u5931\u8D25\uFF1A{message}",
1341
1452
  sessions: "{n} \u4E2A\u4F1A\u8BDD",
1342
1453
  files: "{n} \u4E2A\u6587\u4EF6",
1343
1454
  noData: "\u8FD8\u6CA1\u6709\u7528\u91CF\u6570\u636E\u3002\u8FD0\u884C Codex \u540E\u8FD9\u91CC\u4F1A\u81EA\u52A8\u66F4\u65B0\u3002",
1344
1455
  version: "v{version}",
1456
+ // 开发版
1457
+ devBuild: "\u5F00\u53D1\u7248",
1458
+ devBuildTitle: "\u672C\u5730\u6784\u5EFA \u2014\u2014 \u6570\u636E\u4E0A\u4F20\u81F3 {url}\uFF08\u5F00\u53D1\u73AF\u5883\uFF09\u3002\u914D\u7F6E\u76EE\u5F55\uFF1A{dir}",
1459
+ devBuildMenu: "\u5F00\u53D1\u7248 \u2192 {url}",
1345
1460
  justNow: "\u521A\u521A",
1346
1461
  never: "\u4ECE\u672A",
1347
1462
  local: "\u672C\u673A",
1348
1463
  remote: "\u5176\u4ED6\u8BBE\u5907",
1349
- cliUsage: `\u7528\u6CD5\uFF1Acodex-tracker [\u547D\u4EE4] [\u9009\u9879]
1464
+ update: "\u66F4\u65B0",
1465
+ updateAvailable: "\u66F4\u65B0\u5230 v{version}",
1466
+ updateNewVersion: "\u6709\u65B0\u7248\u672C {version}\uFF08\u5F53\u524D v{current}\uFF09",
1467
+ updateUpToDate: "\u5DF2\u662F\u6700\u65B0\u7248\u672C",
1468
+ updateChecking: "\u68C0\u67E5\u4E2D\u2026",
1469
+ updateInstalling: "\u5B89\u88C5\u4E2D\u2026",
1470
+ updateInstalled: "\u5DF2\u66F4\u65B0\u5230 v{version} \u2014 \u91CD\u542F\u540E\u751F\u6548",
1471
+ updateFailed: "\u66F4\u65B0\u5931\u8D25 \u2014 \u8BF7\u624B\u52A8\u6267\u884C\uFF1A",
1472
+ updateCheckFailed: "\u68C0\u67E5\u66F4\u65B0\u5931\u8D25\uFF1A{message}",
1473
+ checkForUpdates: "\u68C0\u67E5\u66F4\u65B0",
1474
+ releaseNotes: "\u66F4\u65B0\u65E5\u5FD7",
1475
+ cliUsage: `\u7528\u6CD5\uFF1Acodex-token-tracker [\u547D\u4EE4] [\u9009\u9879] \uFF08\u522B\u540D\uFF1Acodex-tracker\uFF09
1350
1476
 
1351
1477
  \u547D\u4EE4\uFF1A
1352
1478
  (\u65E0) \u542F\u52A8\u83DC\u5355\u680F\u5E94\u7528\uFF08\u65E0\u663E\u793A\u73AF\u5883\u65F6\u81EA\u52A8\u5207\u6362\u4E3A agent \u6A21\u5F0F\uFF09
@@ -1356,11 +1482,14 @@ var zh = {
1356
1482
  login \u5C06\u672C\u8BBE\u5907\u8FDE\u63A5\u5230\u4EEA\u8868\u76D8\uFF08--dashboard <url>\uFF09
1357
1483
  logout \u65AD\u5F00\u672C\u8BBE\u5907
1358
1484
  status \u6253\u5370\u4ECA\u65E5\u7528\u91CF\u3001\u5B9E\u65F6\u4F1A\u8BDD\u4E0E\u9650\u989D
1485
+ sync \u91CD\u65B0\u626B\u63CF\u6240\u6709\u667A\u80FD\u4F53\u5E76\u91CD\u65B0\u4E0A\u4F20\u672C\u8BBE\u5907\u5B8C\u6574\u5386\u53F2\uFF08\u6821\u51C6\uFF09
1359
1486
  paths \u663E\u793A\u68C0\u6D4B\u5230\u7684\u4F1A\u8BDD\u76EE\u5F55\uFF08Codex\u3001pi\u3001OpenCode\u3001Cline/Roo/Kilo\u3001Hermes\u3001\u81EA\u5B9A\u4E49\uFF09
1487
+ update \u5B89\u88C5\u6700\u65B0\u53D1\u5E03\u7248\u672C\uFF08--check \u53EA\u68C0\u67E5\u4E0D\u5B89\u88C5\uFF09
1360
1488
  config get|set \u8BFB\u53D6\u6216\u4FEE\u6539\u8BBE\u7F6E\uFF08config set uploadIntervalSec 30\uFF0Cconfig set sources.pi false\uFF09
1361
1489
  lang <en|zh|auto> \u8BBE\u7F6E\u663E\u793A\u8BED\u8A00
1362
1490
 
1363
1491
  \u9009\u9879\uFF1A
1492
+ --check update\uFF1A\u53EA\u62A5\u544A\u6700\u65B0\u7248\u672C\uFF0C\u4E0D\u6267\u884C\u5B89\u88C5
1364
1493
  --dashboard <url> \u4EEA\u8868\u76D8\u5730\u5740\uFF08\u81EA\u6258\u7BA1\uFF09
1365
1494
  --background \u4EE5\u540E\u53F0\u65B9\u5F0F\u542F\u52A8\u83DC\u5355\u680F\u5E94\u7528\u5E76\u7ACB\u5373\u8FD4\u56DE
1366
1495
  --version, -v \u663E\u793A\u7248\u672C
@@ -1402,7 +1531,21 @@ var zh = {
1402
1531
  cliConfigSet: "\u5DF2\u8BBE\u7F6E {key} = {value}",
1403
1532
  cliConfigUnknownKey: "\u672A\u77E5\u914D\u7F6E\u9879\uFF1A{key}\u3002\u53EF\u7528\uFF1A{keys}",
1404
1533
  cliLangSet: "\u8BED\u8A00\u5DF2\u8BBE\u7F6E\u4E3A {lang}",
1405
- cliConfigDir: "\u914D\u7F6E\u76EE\u5F55\uFF1A{dir}"
1534
+ cliConfigDir: "\u914D\u7F6E\u76EE\u5F55\uFF1A{dir}",
1535
+ cliUpdateLatest: "codex-token-tracker {version} \u5DF2\u662F\u6700\u65B0\u7248\u672C\u3002",
1536
+ cliUpdateAvailable: "\u53D1\u73B0\u65B0\u7248\u672C\uFF1A{current} \u2192 {latest}",
1537
+ cliUpdateRunning: "\u6B63\u5728\u6267\u884C\uFF1A{command}",
1538
+ 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",
1539
+ cliUpdateFailed: "\u66F4\u65B0\u5931\u8D25\uFF08\u9000\u51FA\u7801 {code}\uFF09\u3002\u8BF7\u624B\u52A8\u6267\u884C\uFF1A{command}",
1540
+ cliUpdateCheckFailed: "\u65E0\u6CD5\u8BBF\u95EE npm registry\uFF1A{message}",
1541
+ cliSyncStart: "\u5B8C\u6574\u540C\u6B65\uFF1A\u6B63\u5728\u91CD\u65B0\u626B\u63CF\u672C\u8BBE\u5907\u4E0A\u7684\u6240\u6709\u667A\u80FD\u4F53\u2026",
1542
+ cliSyncScanned: " \u5DF2\u626B\u63CF {roots} \u4E2A\u76EE\u5F55\u4E2D\u7684 {files} \u4E2A\u6587\u4EF6\uFF08{agents}\uFF09",
1543
+ cliSyncUploaded: " \u5DF2\u91CD\u65B0\u4E0A\u4F20 {buckets} \u4E2A\u5C0F\u65F6\u6BB5\u4E0E {sessions} \u4E2A\u4F1A\u8BDD",
1544
+ cliSyncLocal: " \u672A\u767B\u5F55 \u2014 \u6CA1\u6709\u4E0A\u4F20\u4EFB\u4F55\u6570\u636E\uFF08\u8BF7\u5148\u8FD0\u884C `codex-tracker login`\uFF09",
1545
+ cliSyncDone: "\u540C\u6B65\u5B8C\u6210\uFF0C\u7528\u65F6 {seconds} \u79D2 \u2014 {sessions} \u4E2A\u4F1A\u8BDD\uFF0C\u4ECA\u65E5 {tokens} tok \xB7 {cost}",
1546
+ cliSyncFailed: "\u540C\u6B65\u5931\u8D25\uFF1A{message}",
1547
+ cliChannelDev: "\u5F00\u53D1\u7248 \u2014\u2014 \u4EEA\u8868\u76D8 {url} \xB7 \u914D\u7F6E\u76EE\u5F55 {dir}",
1548
+ cliUpdateDevBuild: "\u8FD9\u662F\u672C\u5730\u5F00\u53D1\u7248\uFF1B`update` \u4F1A\u7528\u5DF2\u53D1\u5E03\u7684\u5305\u8986\u76D6\u5B83\u3002\u8BF7\u6539\u4E3A\u5728\u4ED3\u5E93\u4E2D\u8FD0\u884C `pnpm build`\u3002"
1406
1549
  };
1407
1550
 
1408
1551
  // src/i18n/index.ts
@@ -1849,21 +1992,21 @@ function findSessionInfo(storage, sessionID) {
1849
1992
  const key = `${storage}|${sessionID}`;
1850
1993
  const cached = sessionCache.get(key);
1851
1994
  if (cached && (cached.info || Date.now() - cached.at < 6e4)) return cached.info;
1852
- let info = null;
1995
+ let info2 = null;
1853
1996
  const sessionDir = import_node_path6.default.join(storage, "session");
1854
1997
  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) {
1998
+ if (import_node_fs4.default.existsSync(direct)) info2 = readJsonFile(direct);
1999
+ if (!info2) {
1857
2000
  for (const project of listDirs(sessionDir)) {
1858
2001
  const p = import_node_path6.default.join(sessionDir, project, `${sessionID}.json`);
1859
2002
  if (import_node_fs4.default.existsSync(p)) {
1860
- info = readJsonFile(p);
2003
+ info2 = readJsonFile(p);
1861
2004
  break;
1862
2005
  }
1863
2006
  }
1864
2007
  }
1865
- sessionCache.set(key, { at: Date.now(), info });
1866
- return info;
2008
+ sessionCache.set(key, { at: Date.now(), info: info2 });
2009
+ return info2;
1867
2010
  }
1868
2011
  function openaiIsOAuth(dataDir) {
1869
2012
  const file = import_node_path6.default.join(dataDir, "auth.json");
@@ -1933,16 +2076,16 @@ var opencodeSource = {
1933
2076
  provider,
1934
2077
  usage: { input, cached, cacheWrite, output, reasoning: t2.reasoning ?? 0, total: input + output, requests: 1 }
1935
2078
  };
1936
- const info = storage ? findSessionInfo(storage, m.sessionID) : null;
1937
- const cwd = info?.directory ?? null;
2079
+ const info2 = storage ? findSessionInfo(storage, m.sessionID) : null;
2080
+ const cwd = info2?.directory ?? null;
1938
2081
  return {
1939
2082
  sessionId: m.sessionID,
1940
2083
  agent: file.root.agent,
1941
2084
  provider,
1942
- startedAt: info?.time?.created ?? ts2,
2085
+ startedAt: info2?.time?.created ?? ts2,
1943
2086
  lastActivityAt: ts2,
1944
2087
  cwd,
1945
- projectName: projectNameOf(cwd) ?? info?.title ?? null,
2088
+ projectName: projectNameOf(cwd) ?? info2?.title ?? null,
1946
2089
  originator: "opencode",
1947
2090
  source: "opencode",
1948
2091
  cliVersion: null,
@@ -2209,6 +2352,15 @@ var SessionStore = class {
2209
2352
  onChange = null;
2210
2353
  debounceTimer = null;
2211
2354
  sessionCache = null;
2355
+ /**
2356
+ * Forget the parsed-file index so the next deep refresh re-reads and re-parses every transcript.
2357
+ * Used by the full sync: files are otherwise skipped while their size and mtime are unchanged, which
2358
+ * would keep stale numbers around after a parser or pricing change.
2359
+ */
2360
+ reset() {
2361
+ this.files.clear();
2362
+ this.sessionCache = null;
2363
+ }
2212
2364
  async refreshDeep() {
2213
2365
  const o = this.getOptions();
2214
2366
  this.roots = discoverSessionRoots({ extraSessionDirs: o.extraSessionDirs, sources: o.sources });
@@ -2366,7 +2518,20 @@ var SessionStore = class {
2366
2518
 
2367
2519
  // src/core/stats.ts
2368
2520
  var LIVE_WINDOW_MS = 5 * 60 * 1e3;
2521
+ var RATE_WINDOW_MS = 6e4;
2522
+ var BURST_WINDOW_MS = 1e4;
2369
2523
  var DAY3 = 864e5;
2524
+ function outputRate(events, now, windowMs, sessionStart) {
2525
+ const from = now - windowMs;
2526
+ let output = 0;
2527
+ for (const e of events) {
2528
+ if (e.ts <= from) continue;
2529
+ output += e.usage.output;
2530
+ }
2531
+ if (output <= 0) return 0;
2532
+ const elapsedSec = Math.max(1, (now - Math.max(from, sessionStart)) / 1e3);
2533
+ return output / elapsedSec;
2534
+ }
2370
2535
  var PriceCache = class {
2371
2536
  constructor(overrides) {
2372
2537
  this.overrides = overrides;
@@ -2443,7 +2608,9 @@ function agentStats(sessions, since, prices) {
2443
2608
  function computeStats(input) {
2444
2609
  const now = input.now ?? Date.now();
2445
2610
  const prices = new PriceCache(input.pricing);
2446
- const sessions = input.sessions;
2611
+ const sessions = input.sessions.map(
2612
+ (s) => s.events.every((e) => isOpenAIModel(e.model)) ? s : { ...s, events: s.events.filter((e) => isOpenAIModel(e.model)) }
2613
+ );
2447
2614
  const allEvents = [];
2448
2615
  const sessionCosts = /* @__PURE__ */ new Map();
2449
2616
  let lastActivityAt = null;
@@ -2469,14 +2636,8 @@ function computeStats(input) {
2469
2636
  let live = null;
2470
2637
  const liveSession = sessions.filter((s) => s.events.length && now - s.lastActivityAt <= LIVE_WINDOW_MS).sort((a, b) => b.lastActivityAt - a.lastActivityAt)[0];
2471
2638
  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
2639
  const last = liveSession.events[liveSession.events.length - 1];
2640
+ const start = liveSession.startedAt || liveSession.events[0].ts;
2480
2641
  live = {
2481
2642
  sessionId: liveSession.sessionId,
2482
2643
  agent: liveSession.agent,
@@ -2484,8 +2645,8 @@ function computeStats(input) {
2484
2645
  model: liveSession.model,
2485
2646
  startedAt: liveSession.startedAt,
2486
2647
  lastEventAt: last.ts,
2487
- tokensPerSecond: t60 / 60,
2488
- tokensPerSecond10s: t10 / 10,
2648
+ tokensPerSecond: outputRate(liveSession.events, now, RATE_WINDOW_MS, start),
2649
+ tokensPerSecond10s: outputRate(liveSession.events, now, BURST_WINDOW_MS, start),
2489
2650
  contextUsed: last.usage.input + last.usage.output,
2490
2651
  contextWindow: liveSession.contextWindow,
2491
2652
  sessionUsage: { ...liveSession.cumulative },
@@ -2512,6 +2673,7 @@ function computeStats(input) {
2512
2673
  }
2513
2674
  return {
2514
2675
  buckets,
2676
+ sessions: sessions.filter((s) => s.events.length),
2515
2677
  today,
2516
2678
  week,
2517
2679
  month,
@@ -3869,12 +4031,12 @@ function createApi(pathParts = []) {
3869
4031
  `API path is expected to be of the form \`api.moduleName.functionName\`. Found: \`${found}\``
3870
4032
  );
3871
4033
  }
3872
- const path11 = pathParts.slice(0, -1).join("/");
4034
+ const path12 = pathParts.slice(0, -1).join("/");
3873
4035
  const exportName = pathParts[pathParts.length - 1];
3874
4036
  if (exportName === "default") {
3875
- return path11;
4037
+ return path12;
3876
4038
  } else {
3877
- return path11 + ":" + exportName;
4039
+ return path12 + ":" + exportName;
3878
4040
  }
3879
4041
  } else if (prop === Symbol.toStringTag) {
3880
4042
  return "FunctionReference";
@@ -4038,8 +4200,8 @@ var require_constants = __commonJS({
4038
4200
  });
4039
4201
  var require_node_gyp_build = __commonJS({
4040
4202
  "../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");
4203
+ var fs10 = __require("fs");
4204
+ var path12 = __require("path");
4043
4205
  var os8 = __require("os");
4044
4206
  var runtimeRequire = typeof __webpack_require__ === "function" ? __non_webpack_require__ : __require;
4045
4207
  var vars2 = process.config && process.config.variables || {};
@@ -4056,21 +4218,21 @@ var require_node_gyp_build = __commonJS({
4056
4218
  return runtimeRequire(load.resolve(dir));
4057
4219
  }
4058
4220
  load.resolve = load.path = function(dir) {
4059
- dir = path11.resolve(dir || ".");
4221
+ dir = path12.resolve(dir || ".");
4060
4222
  try {
4061
- var name = runtimeRequire(path11.join(dir, "package.json")).name.toUpperCase().replace(/-/g, "_");
4223
+ var name = runtimeRequire(path12.join(dir, "package.json")).name.toUpperCase().replace(/-/g, "_");
4062
4224
  if (process.env[name + "_PREBUILD"]) dir = process.env[name + "_PREBUILD"];
4063
4225
  } catch (err) {
4064
4226
  }
4065
4227
  if (!prebuildsOnly) {
4066
- var release = getFirst(path11.join(dir, "build/Release"), matchBuild);
4228
+ var release = getFirst(path12.join(dir, "build/Release"), matchBuild);
4067
4229
  if (release) return release;
4068
- var debug = getFirst(path11.join(dir, "build/Debug"), matchBuild);
4230
+ var debug = getFirst(path12.join(dir, "build/Debug"), matchBuild);
4069
4231
  if (debug) return debug;
4070
4232
  }
4071
4233
  var prebuild = resolve(dir);
4072
4234
  if (prebuild) return prebuild;
4073
- var nearby = resolve(path11.dirname(process.execPath));
4235
+ var nearby = resolve(path12.dirname(process.execPath));
4074
4236
  if (nearby) return nearby;
4075
4237
  var target = [
4076
4238
  "platform=" + platform,
@@ -4087,26 +4249,26 @@ var require_node_gyp_build = __commonJS({
4087
4249
  ].filter(Boolean).join(" ");
4088
4250
  throw new Error("No native build was found for " + target + "\n loaded from: " + dir + "\n");
4089
4251
  function resolve(dir2) {
4090
- var tuples = readdirSync(path11.join(dir2, "prebuilds")).map(parseTuple);
4252
+ var tuples = readdirSync(path12.join(dir2, "prebuilds")).map(parseTuple);
4091
4253
  var tuple = tuples.filter(matchTuple(platform, arch)).sort(compareTuples)[0];
4092
4254
  if (!tuple) return;
4093
- var prebuilds = path11.join(dir2, "prebuilds", tuple.name);
4255
+ var prebuilds = path12.join(dir2, "prebuilds", tuple.name);
4094
4256
  var parsed = readdirSync(prebuilds).map(parseTags);
4095
4257
  var candidates = parsed.filter(matchTags(runtime, abi));
4096
4258
  var winner = candidates.sort(compareTags(runtime))[0];
4097
- if (winner) return path11.join(prebuilds, winner.file);
4259
+ if (winner) return path12.join(prebuilds, winner.file);
4098
4260
  }
4099
4261
  };
4100
4262
  function readdirSync(dir) {
4101
4263
  try {
4102
- return fs9.readdirSync(dir);
4264
+ return fs10.readdirSync(dir);
4103
4265
  } catch (err) {
4104
4266
  return [];
4105
4267
  }
4106
4268
  }
4107
4269
  function getFirst(dir, filter) {
4108
4270
  var files = readdirSync(dir).filter(filter);
4109
- return files[0] && path11.join(dir, files[0]);
4271
+ return files[0] && path12.join(dir, files[0]);
4110
4272
  }
4111
4273
  function matchBuild(name) {
4112
4274
  return /\.node$/.test(name);
@@ -4193,7 +4355,7 @@ var require_node_gyp_build = __commonJS({
4193
4355
  return typeof window !== "undefined" && window.process && window.process.type === "renderer";
4194
4356
  }
4195
4357
  function isAlpine(platform2) {
4196
- return platform2 === "linux" && fs9.existsSync("/etc/alpine-release");
4358
+ return platform2 === "linux" && fs10.existsSync("/etc/alpine-release");
4197
4359
  }
4198
4360
  load.parseTags = parseTags;
4199
4361
  load.matchTags = matchTags;
@@ -7739,13 +7901,13 @@ var require_websocket_server = __commonJS({
7739
7901
  }
7740
7902
  }
7741
7903
  if (this.options.verifyClient) {
7742
- const info = {
7904
+ const info2 = {
7743
7905
  origin: req.headers[`${version2 === 8 ? "sec-websocket-origin" : "origin"}`],
7744
7906
  secure: !!(req.socket.authorized || req.socket.encrypted),
7745
7907
  req
7746
7908
  };
7747
7909
  if (this.options.verifyClient.length === 2) {
7748
- this.options.verifyClient(info, (verified, code2, message, headers) => {
7910
+ this.options.verifyClient(info2, (verified, code2, message, headers) => {
7749
7911
  if (!verified) {
7750
7912
  return abortHandshake(socket, code2 || 401, message, headers);
7751
7913
  }
@@ -7761,7 +7923,7 @@ var require_websocket_server = __commonJS({
7761
7923
  });
7762
7924
  return;
7763
7925
  }
7764
- if (!this.options.verifyClient(info)) return abortHandshake(socket, 401);
7926
+ if (!this.options.verifyClient(info2)) return abortHandshake(socket, 401);
7765
7927
  }
7766
7928
  this.completeUpgrade(extensions, key, protocols, req, socket, head, cb);
7767
7929
  }
@@ -8757,11 +8919,25 @@ var Uploader = class {
8757
8919
  throw err;
8758
8920
  }
8759
8921
  }
8760
- /** Upload changed hour buckets and session summaries. Returns counts of pushed items. */
8761
- async pushAll(buckets, sessions, sessionCosts) {
8922
+ /** Wait for an in-flight incremental push to finish so a full sync isn't skipped by the guard. */
8923
+ async awaitIdle(timeoutMs = 15e3) {
8924
+ const deadline = Date.now() + timeoutMs;
8925
+ while (this.inFlight && Date.now() < deadline) await new Promise((r) => setTimeout(r, 100));
8926
+ }
8927
+ /**
8928
+ * Upload changed hour buckets and session summaries. Returns counts of pushed items.
8929
+ * With `full`, the record of what was already pushed is dropped first so *everything* is re-sent —
8930
+ * that is what recalibrates this device's totals on the dashboard after a parser or pricing change.
8931
+ */
8932
+ async pushAll(buckets, sessions, sessionCosts, opts = {}) {
8933
+ if (opts.full) await this.awaitIdle();
8762
8934
  if (this.inFlight) return { buckets: 0, sessions: 0 };
8763
8935
  this.inFlight = true;
8764
8936
  try {
8937
+ if (opts.full) {
8938
+ this.state.pushedBuckets = {};
8939
+ this.state.pushedSessions = {};
8940
+ }
8765
8941
  const changed = [];
8766
8942
  const hashes = /* @__PURE__ */ new Map();
8767
8943
  for (const b of buckets.sort((a, b2) => a.hourStart - b2.hourStart)) {
@@ -8967,9 +9143,167 @@ async function fetchLiveRateLimits(appVersion, timeoutMs = 1e4) {
8967
9143
  }
8968
9144
  }
8969
9145
 
8970
- // src/version.ts
8971
- var APP_VERSION = true ? "0.1.1" : "0.0.0-dev";
8972
- var APP_NAME = "Codex Tracker";
9146
+ // src/core/update.ts
9147
+ var import_node_fs8 = __toESM(require("node:fs"));
9148
+ var import_node_path10 = __toESM(require("node:path"));
9149
+ var import_node_child_process3 = require("node:child_process");
9150
+ var NPM_PACKAGE = "codex-token-tracker";
9151
+ var CHECK_TTL_MS = 6 * 60 * 60 * 1e3;
9152
+ var REQUEST_TIMEOUT_MS = 8e3;
9153
+ var INSTALL_TIMEOUT_MS = 10 * 60 * 1e3;
9154
+ var LOG_TAIL_CHARS = 4e3;
9155
+ function parseVersion(v2) {
9156
+ const s = String(v2 ?? "").trim().replace(/^v/, "");
9157
+ const i2 = s.indexOf("-");
9158
+ const core = i2 === -1 ? s : s.slice(0, i2);
9159
+ const pre = i2 === -1 ? "" : s.slice(i2 + 1);
9160
+ const n = core.split(".").map((x) => Number.parseInt(x, 10) || 0);
9161
+ return { nums: [n[0] ?? 0, n[1] ?? 0, n[2] ?? 0], pre };
9162
+ }
9163
+ function compareVersions(a, b) {
9164
+ const A = parseVersion(a);
9165
+ const B = parseVersion(b);
9166
+ for (let i2 = 0; i2 < 3; i2++) if (A.nums[i2] !== B.nums[i2]) return A.nums[i2] < B.nums[i2] ? -1 : 1;
9167
+ if (A.pre === B.pre) return 0;
9168
+ if (!A.pre) return 1;
9169
+ if (!B.pre) return -1;
9170
+ return A.pre < B.pre ? -1 : 1;
9171
+ }
9172
+ function isDevBuild(version2) {
9173
+ return IS_DEV_BUILD || version2.startsWith("0.0.0");
9174
+ }
9175
+ function registryBase() {
9176
+ const r = process.env.CODEX_TRACKER_REGISTRY || process.env.npm_config_registry || process.env.NPM_CONFIG_REGISTRY || "https://registry.npmjs.org";
9177
+ return r.replace(/\/+$/, "");
9178
+ }
9179
+ async function fetchLatestVersion(signal) {
9180
+ const ctrl = new AbortController();
9181
+ const timer = setTimeout(() => ctrl.abort(), REQUEST_TIMEOUT_MS);
9182
+ signal?.addEventListener("abort", () => ctrl.abort(), { once: true });
9183
+ try {
9184
+ const res = await fetch(`${registryBase()}/-/package/${NPM_PACKAGE}/dist-tags`, {
9185
+ signal: ctrl.signal,
9186
+ headers: { accept: "application/json" }
9187
+ });
9188
+ if (!res.ok) throw new Error(`registry responded ${res.status}`);
9189
+ const json = await res.json();
9190
+ const latest = json?.latest;
9191
+ if (typeof latest !== "string" || !latest) throw new Error("registry returned no `latest` tag");
9192
+ return latest;
9193
+ } finally {
9194
+ clearTimeout(timer);
9195
+ }
9196
+ }
9197
+ function detectPackageManager(installDir = import_node_path10.default.resolve(__dirname, "..")) {
9198
+ const p = installDir.replace(/\\/g, "/").toLowerCase();
9199
+ if (/\/\.?bun\//.test(p)) return "bun";
9200
+ if (/\/\.?pnpm[/-]/.test(p)) return "pnpm";
9201
+ if (/\/\.?yarn\//.test(p)) return "yarn";
9202
+ const ua = (process.env.npm_config_user_agent ?? "").toLowerCase();
9203
+ if (ua.startsWith("pnpm")) return "pnpm";
9204
+ if (ua.startsWith("yarn")) return "yarn";
9205
+ if (ua.startsWith("bun")) return "bun";
9206
+ return "npm";
9207
+ }
9208
+ function updateArgs(pm, spec = `${NPM_PACKAGE}@latest`) {
9209
+ switch (pm) {
9210
+ case "pnpm":
9211
+ return ["add", "-g", spec];
9212
+ case "yarn":
9213
+ return ["global", "add", spec];
9214
+ case "bun":
9215
+ return ["add", "-g", spec];
9216
+ default:
9217
+ return ["install", "-g", spec];
9218
+ }
9219
+ }
9220
+ function updateCommand(pm, spec) {
9221
+ return `${pm} ${updateArgs(pm, spec).join(" ")}`;
9222
+ }
9223
+ function cachePath() {
9224
+ return import_node_path10.default.join(configDir(), "update.json");
9225
+ }
9226
+ function readCache() {
9227
+ try {
9228
+ const raw = JSON.parse(import_node_fs8.default.readFileSync(cachePath(), "utf8"));
9229
+ if (typeof raw?.latest === "string" && typeof raw.checkedAt === "number") {
9230
+ return { latest: raw.latest, checkedAt: raw.checkedAt };
9231
+ }
9232
+ } catch {
9233
+ }
9234
+ return null;
9235
+ }
9236
+ function writeCache(c) {
9237
+ try {
9238
+ import_node_fs8.default.mkdirSync(import_node_path10.default.dirname(cachePath()), { recursive: true });
9239
+ import_node_fs8.default.writeFileSync(cachePath(), JSON.stringify(c, null, 2));
9240
+ } catch {
9241
+ }
9242
+ }
9243
+ function info(current, latest, checkedAt, error) {
9244
+ const pm = detectPackageManager();
9245
+ return {
9246
+ current,
9247
+ latest,
9248
+ available: Boolean(latest) && !isDevBuild(current) && compareVersions(latest, current) > 0,
9249
+ checkedAt,
9250
+ error,
9251
+ packageManager: pm,
9252
+ command: updateCommand(pm)
9253
+ };
9254
+ }
9255
+ async function checkForUpdate(opts = {}) {
9256
+ const current = opts.current ?? APP_VERSION;
9257
+ const cached = readCache();
9258
+ if (!opts.force && cached && Date.now() - cached.checkedAt < CHECK_TTL_MS) {
9259
+ return info(current, cached.latest, cached.checkedAt, null);
9260
+ }
9261
+ try {
9262
+ const latest = await fetchLatestVersion(opts.signal);
9263
+ const checkedAt = Date.now();
9264
+ writeCache({ latest, checkedAt });
9265
+ return info(current, latest, checkedAt, null);
9266
+ } catch (err) {
9267
+ return info(current, cached?.latest ?? null, cached?.checkedAt ?? null, err.message);
9268
+ }
9269
+ }
9270
+ function runUpdate(opts = {}) {
9271
+ const pm = detectPackageManager();
9272
+ const spec = opts.version ? `${NPM_PACKAGE}@${opts.version}` : `${NPM_PACKAGE}@latest`;
9273
+ const args = updateArgs(pm, spec);
9274
+ const command = `${pm} ${args.join(" ")}`;
9275
+ return new Promise((resolve) => {
9276
+ let output = "";
9277
+ const collect = (chunk) => {
9278
+ const text = chunk.toString();
9279
+ opts.onOutput?.(text);
9280
+ output = (output + text).slice(-LOG_TAIL_CHARS);
9281
+ };
9282
+ let child;
9283
+ try {
9284
+ child = (0, import_node_child_process3.spawn)(pm, args, {
9285
+ // Electron's bundled Node has no shell PATH resolution for `npm.cmd` on Windows.
9286
+ shell: process.platform === "win32",
9287
+ env: { ...process.env, ELECTRON_RUN_AS_NODE: void 0 },
9288
+ windowsHide: true
9289
+ });
9290
+ } catch (err) {
9291
+ resolve({ ok: false, code: null, command, output: err.message });
9292
+ return;
9293
+ }
9294
+ const timer = setTimeout(() => child.kill(), INSTALL_TIMEOUT_MS);
9295
+ child.stdout?.on("data", collect);
9296
+ child.stderr?.on("data", collect);
9297
+ child.on("error", (err) => {
9298
+ clearTimeout(timer);
9299
+ resolve({ ok: false, code: null, command, output: output + err.message });
9300
+ });
9301
+ child.on("close", (code2) => {
9302
+ clearTimeout(timer);
9303
+ resolve({ ok: code2 === 0, code: code2, command, output });
9304
+ });
9305
+ });
9306
+ }
8973
9307
 
8974
9308
  // src/core/engine.ts
8975
9309
  var SHALLOW_MS = 3e3;
@@ -8978,6 +9312,8 @@ var TICK_MS = 2e3;
8978
9312
  var REMOTE_MS = 6e4;
8979
9313
  var LIVE_LIMITS_DEBOUNCE_MS = 1e4;
8980
9314
  var LIVE_LIMITS_MIN_GAP_MS = 2e4;
9315
+ var UPDATE_CHECK_MS = 6 * 60 * 60 * 1e3;
9316
+ var SYNC_BANNER_MS = 25e3;
8981
9317
  var Engine = class extends import_node_events.EventEmitter {
8982
9318
  constructor(opts) {
8983
9319
  super();
@@ -9014,6 +9350,16 @@ var Engine = class extends import_node_events.EventEmitter {
9014
9350
  liveLimitsInFlight = false;
9015
9351
  liveLimitsTimer = null;
9016
9352
  lastLiveLimitsAttempt = 0;
9353
+ update = null;
9354
+ updateStatus = "idle";
9355
+ updateLog = null;
9356
+ syncStatus = "idle";
9357
+ syncPhase = null;
9358
+ syncStartedAt = null;
9359
+ syncFinishedAt = null;
9360
+ syncError = null;
9361
+ syncResult = null;
9362
+ syncBannerTimer = null;
9017
9363
  get heatmapWeeks() {
9018
9364
  return this.opts.heatmapWeeks ?? 16;
9019
9365
  }
@@ -9035,6 +9381,8 @@ var Engine = class extends import_node_events.EventEmitter {
9035
9381
  return;
9036
9382
  }
9037
9383
  void this.refreshLiveLimits(true);
9384
+ void this.checkUpdate(false);
9385
+ this.timers.push(setInterval(() => void this.checkUpdate(false), UPDATE_CHECK_MS));
9038
9386
  this.store.startWatching(() => void this.refresh(false));
9039
9387
  this.timers.push(setInterval(() => void this.refresh(false), SHALLOW_MS));
9040
9388
  this.timers.push(setInterval(() => void this.refresh(true), DEEP_MS));
@@ -9062,6 +9410,8 @@ var Engine = class extends import_node_events.EventEmitter {
9062
9410
  this.timers = [];
9063
9411
  if (this.liveLimitsTimer) clearTimeout(this.liveLimitsTimer);
9064
9412
  this.liveLimitsTimer = null;
9413
+ if (this.syncBannerTimer) clearTimeout(this.syncBannerTimer);
9414
+ this.syncBannerTimer = null;
9065
9415
  this.store.stopWatching();
9066
9416
  this.pending?.abort.abort();
9067
9417
  }
@@ -9141,7 +9491,7 @@ var Engine = class extends import_node_events.EventEmitter {
9141
9491
  if (!this.signedIn) return { buckets: 0, sessions: 0 };
9142
9492
  if (!this.stats) this.recompute();
9143
9493
  try {
9144
- const r = await this.uploader.pushAll(this.stats.buckets, this.store.sessions(), this.stats.sessionCosts);
9494
+ const r = await this.uploader.pushAll(this.stats.buckets, this.stats.sessions, this.stats.sessionCosts);
9145
9495
  this.emitSnapshot();
9146
9496
  return r;
9147
9497
  } catch (err) {
@@ -9165,6 +9515,154 @@ var Engine = class extends import_node_events.EventEmitter {
9165
9515
  await this.uploader.fetchRemote(this.heatmapWeeks);
9166
9516
  this.recompute();
9167
9517
  }
9518
+ /**
9519
+ * Full sync ("calibrate this device"). Unlike the periodic incremental upload this deliberately
9520
+ * throws away every cache on the way:
9521
+ * 1. re-read the config so sources enabled since start-up are picked up,
9522
+ * 2. drop the parsed-file index and re-discover + re-parse every transcript of every agent
9523
+ * (Codex plus the agents running on the Codex subscription: pi, OpenCode, Cline/Roo/Kilo,
9524
+ * Hermes and any custom `extraSessionDirs`),
9525
+ * 3. recompute the aggregates with the current pricing table,
9526
+ * 4. re-upload *everything* — not just what changed — so the dashboard's totals for this device
9527
+ * are replaced by the freshly computed ones,
9528
+ * 5. pull the other devices' rows and the live rate limits back down.
9529
+ *
9530
+ * Returns null when a sync is already running. Never throws: failures land in the returned state.
9531
+ */
9532
+ async syncNow() {
9533
+ if (this.syncStatus === "running") return null;
9534
+ if (this.syncBannerTimer) {
9535
+ clearTimeout(this.syncBannerTimer);
9536
+ this.syncBannerTimer = null;
9537
+ }
9538
+ const startedAt = Date.now();
9539
+ this.syncStatus = "running";
9540
+ this.syncPhase = null;
9541
+ this.syncStartedAt = startedAt;
9542
+ this.syncFinishedAt = null;
9543
+ this.syncError = null;
9544
+ const phase = (p) => {
9545
+ this.syncPhase = p;
9546
+ this.emitSnapshot();
9547
+ };
9548
+ try {
9549
+ phase("scanning");
9550
+ this.reloadConfig();
9551
+ this.store.reset();
9552
+ await this.store.refreshDeep();
9553
+ phase("computing");
9554
+ this.recompute();
9555
+ let uploadedBuckets = 0;
9556
+ let uploadedSessions = 0;
9557
+ const uploaded = this.opts.upload && this.signedIn;
9558
+ if (uploaded) {
9559
+ phase("uploading");
9560
+ const r = await this.uploader.pushAll(this.stats.buckets, this.stats.sessions, this.stats.sessionCosts, { full: true });
9561
+ uploadedBuckets = r.buckets;
9562
+ uploadedSessions = r.sessions;
9563
+ await this.heartbeatNow().catch(() => {
9564
+ });
9565
+ phase("downloading");
9566
+ await this.fetchRemoteNow();
9567
+ }
9568
+ phase("limits");
9569
+ await this.refreshLiveLimits(true);
9570
+ const finishedAt = Date.now();
9571
+ this.syncResult = {
9572
+ startedAt,
9573
+ finishedAt,
9574
+ durationMs: finishedAt - startedAt,
9575
+ files: this.store.fileCount,
9576
+ sessions: this.store.sessions().length,
9577
+ roots: this.store.roots.length,
9578
+ agents: [...new Set(this.store.roots.map((r) => r.agent))].sort(),
9579
+ uploadedBuckets,
9580
+ uploadedSessions,
9581
+ uploaded
9582
+ };
9583
+ this.syncStatus = "done";
9584
+ this.syncFinishedAt = finishedAt;
9585
+ this.opts.log?.(
9586
+ `sync: ${this.syncResult.files} files, ${this.syncResult.sessions} sessions, ${uploadedBuckets} buckets / ${uploadedSessions} sessions uploaded in ${this.syncResult.durationMs} ms`
9587
+ );
9588
+ return this.syncResult;
9589
+ } catch (err) {
9590
+ this.syncStatus = "error";
9591
+ this.syncFinishedAt = Date.now();
9592
+ this.syncError = err instanceof SignedOutError ? "not signed in" : errorMessage(err);
9593
+ this.opts.log?.(`sync failed: ${this.syncError}`);
9594
+ return null;
9595
+ } finally {
9596
+ this.syncPhase = null;
9597
+ this.emitSnapshot();
9598
+ this.scheduleSyncBannerClear();
9599
+ }
9600
+ }
9601
+ /** Return the sync banner to "idle" a little after it finished, keeping `last` for the footer. */
9602
+ scheduleSyncBannerClear() {
9603
+ if (this.opts.watch === false) return;
9604
+ if (this.syncBannerTimer) clearTimeout(this.syncBannerTimer);
9605
+ this.syncBannerTimer = setTimeout(() => {
9606
+ this.syncBannerTimer = null;
9607
+ if (this.syncStatus === "done" || this.syncStatus === "error") {
9608
+ this.syncStatus = "idle";
9609
+ this.emitSnapshot();
9610
+ }
9611
+ }, SYNC_BANNER_MS);
9612
+ this.syncBannerTimer.unref?.();
9613
+ }
9614
+ syncState() {
9615
+ return {
9616
+ status: this.syncStatus,
9617
+ phase: this.syncPhase,
9618
+ startedAt: this.syncStartedAt,
9619
+ finishedAt: this.syncFinishedAt,
9620
+ error: this.syncError,
9621
+ last: this.syncResult
9622
+ };
9623
+ }
9624
+ /**
9625
+ * Ask the npm registry for the newest published version (cached for 6 h unless `force`).
9626
+ * Best-effort: a failure is recorded on the snapshot, never thrown.
9627
+ */
9628
+ async checkUpdate(force) {
9629
+ if (!this.config.checkUpdates) {
9630
+ this.update = null;
9631
+ return null;
9632
+ }
9633
+ if (this.updateStatus === "checking" || this.updateStatus === "installing") return this.update;
9634
+ this.updateStatus = "checking";
9635
+ this.emitSnapshot();
9636
+ this.update = await checkForUpdate({ force });
9637
+ this.updateStatus = "idle";
9638
+ if (this.update.error) this.opts.log?.(`update check failed: ${this.update.error}`);
9639
+ this.emitSnapshot();
9640
+ return this.update;
9641
+ }
9642
+ /**
9643
+ * Install the newest version globally with the package manager this copy came from.
9644
+ * The new code only takes effect once the app is restarted, so the UI says so rather than
9645
+ * pretending to hot-swap itself.
9646
+ */
9647
+ async installUpdate() {
9648
+ if (this.updateStatus === "installing") return false;
9649
+ if (!this.update?.available) await this.checkUpdate(true);
9650
+ if (!this.update?.available) return false;
9651
+ this.updateStatus = "installing";
9652
+ this.updateLog = null;
9653
+ this.emitSnapshot();
9654
+ const r = await runUpdate({ version: this.update.latest ?? void 0 });
9655
+ this.updateStatus = r.ok ? "installed" : "failed";
9656
+ this.updateLog = r.ok ? null : `${r.command}
9657
+ ${r.output}`.trim();
9658
+ if (!r.ok) this.opts.log?.(`update install failed (${r.code}): ${r.output}`);
9659
+ this.emitSnapshot();
9660
+ return r.ok;
9661
+ }
9662
+ updateState() {
9663
+ if (!this.config.checkUpdates || !this.update) return null;
9664
+ return { ...this.update, status: this.updateStatus, log: this.updateLog };
9665
+ }
9168
9666
  /** Start the device-code login flow (resolves when approved / denied / expired / cancelled). */
9169
9667
  async login(openBrowser, onCode) {
9170
9668
  if (this.pending) return { status: "cancelled" };
@@ -9240,6 +9738,7 @@ var Engine = class extends import_node_events.EventEmitter {
9240
9738
  const sessions = this.store.sessions();
9241
9739
  return {
9242
9740
  version: APP_VERSION,
9741
+ channel: APP_CHANNEL,
9243
9742
  generatedAt: Date.now(),
9244
9743
  language: this.language(),
9245
9744
  languageSetting: this.config.language,
@@ -9257,6 +9756,8 @@ var Engine = class extends import_node_events.EventEmitter {
9257
9756
  rateLimits: this.liveLimits ?? fromLogRateLimits(s.logRateLimits),
9258
9757
  rateLimitsError: this.config.liveRateLimits ? this.liveLimitsError : null,
9259
9758
  rateLimitsUpdatedAt: this.liveLimits ? this.liveLimitsAt : s.logRateLimits?.observedAt ?? null,
9759
+ update: this.updateState(),
9760
+ sync: this.syncState(),
9260
9761
  modelsToday: s.modelsToday,
9261
9762
  modelsMonth: s.modelsMonth,
9262
9763
  byAgentToday: s.byAgentToday,
@@ -9273,6 +9774,7 @@ var Engine = class extends import_node_events.EventEmitter {
9273
9774
  },
9274
9775
  sessionDirs: SessionStore.rootDirs(this.store.roots),
9275
9776
  sessionRoots: this.store.roots.map((r) => ({ dir: r.dir, agent: r.agent, format: r.format, origin: r.origin })),
9777
+ configDir: configDir(),
9276
9778
  launchAtLogin: this.config.launchAtLogin,
9277
9779
  trayTitle: this.config.trayTitle
9278
9780
  };
@@ -9281,9 +9783,9 @@ var Engine = class extends import_node_events.EventEmitter {
9281
9783
 
9282
9784
  // src/main.ts
9283
9785
  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");
9786
+ var assetsDir = import_node_path11.default.join(__dirname, "..", "assets");
9787
+ var rendererIndex = import_node_path11.default.join(__dirname, "renderer", "index.html");
9788
+ var preloadPath = import_node_path11.default.join(__dirname, "preload.js");
9287
9789
  var isMac = process.platform === "darwin";
9288
9790
  var isWin = process.platform === "win32";
9289
9791
  var mb = null;
@@ -9294,12 +9796,12 @@ function log2(...args) {
9294
9796
  }
9295
9797
  function trayIcon() {
9296
9798
  if (isMac) {
9297
- const img2 = import_electron.nativeImage.createFromPath(import_node_path10.default.join(assetsDir, "trayTemplate.png"));
9799
+ const img2 = import_electron.nativeImage.createFromPath(import_node_path11.default.join(assetsDir, "trayTemplate.png"));
9298
9800
  img2.setTemplateImage(true);
9299
9801
  return img2;
9300
9802
  }
9301
9803
  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));
9804
+ const img = import_electron.nativeImage.createFromPath(import_node_path11.default.join(assetsDir, file));
9303
9805
  return isWin ? img.resize({ width: 16, height: 16 }) : img;
9304
9806
  }
9305
9807
  function trayText(s) {
@@ -9319,28 +9821,29 @@ function updateTray(s) {
9319
9821
  mb.tray.setToolTip(`${APP_NAME} \u2014 ${t(L, "today")}: ${formatTokens(s.today.usage.total)} \xB7 ${formatUSD(s.today.cost)}${live}`);
9320
9822
  if (isMac) mb.tray.setTitle(trayText(s), { fontType: "monospacedDigit" });
9321
9823
  }
9322
- var launchAgentPath = import_node_path10.default.join(import_node_os7.default.homedir(), "Library", "LaunchAgents", "dev.codex-tracker.menubar.plist");
9824
+ var launchAgentLabel = IS_DEV_BUILD ? "dev.codex-tracker.menubar.dev" : "dev.codex-tracker.menubar";
9825
+ var launchAgentPath = import_node_path11.default.join(import_node_os7.default.homedir(), "Library", "LaunchAgents", `${launchAgentLabel}.plist`);
9323
9826
  function setLaunchAtLogin(enabled) {
9324
9827
  if (isMac) {
9325
9828
  if (enabled) {
9326
9829
  const plist = `<?xml version="1.0" encoding="UTF-8"?>
9327
9830
  <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
9328
9831
  <plist version="1.0"><dict>
9329
- <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>
9832
+ <key>Label</key><string>${launchAgentLabel}</string>
9833
+ <key>ProgramArguments</key><array><string>${process.execPath}</string><string>${import_node_path11.default.join(__dirname, "main.js")}</string></array>
9331
9834
  <key>RunAtLoad</key><true/>
9332
9835
  <key>KeepAlive</key><false/>
9333
9836
  <key>ProcessType</key><string>Interactive</string>
9334
9837
  </dict></plist>
9335
9838
  `;
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], () => {
9839
+ import_node_fs9.default.mkdirSync(import_node_path11.default.dirname(launchAgentPath), { recursive: true });
9840
+ import_node_fs9.default.writeFileSync(launchAgentPath, plist);
9841
+ (0, import_node_child_process4.execFile)("launchctl", ["load", "-w", launchAgentPath], () => {
9339
9842
  });
9340
- } else if (import_node_fs8.default.existsSync(launchAgentPath)) {
9341
- (0, import_node_child_process3.execFile)("launchctl", ["unload", "-w", launchAgentPath], () => {
9843
+ } else if (import_node_fs9.default.existsSync(launchAgentPath)) {
9844
+ (0, import_node_child_process4.execFile)("launchctl", ["unload", "-w", launchAgentPath], () => {
9342
9845
  try {
9343
- import_node_fs8.default.unlinkSync(launchAgentPath);
9846
+ import_node_fs9.default.unlinkSync(launchAgentPath);
9344
9847
  } catch {
9345
9848
  }
9346
9849
  });
@@ -9348,7 +9851,7 @@ function setLaunchAtLogin(enabled) {
9348
9851
  return;
9349
9852
  }
9350
9853
  if (isWin) {
9351
- import_electron.app.setLoginItemSettings({ openAtLogin: enabled, path: process.execPath, args: [import_node_path10.default.join(__dirname, "main.js")] });
9854
+ import_electron.app.setLoginItemSettings({ openAtLogin: enabled, path: process.execPath, args: [import_node_path11.default.join(__dirname, "main.js")] });
9352
9855
  }
9353
9856
  }
9354
9857
  function buildMenu(s) {
@@ -9361,6 +9864,7 @@ function buildMenu(s) {
9361
9864
  });
9362
9865
  const template = [
9363
9866
  { label: `${APP_NAME} ${t(L, "version", { version: APP_VERSION })}`, enabled: false },
9867
+ ...IS_DEV_BUILD ? [{ label: t(L, "devBuildMenu", { url: s.auth.dashboardUrl }), enabled: false }] : [],
9364
9868
  {
9365
9869
  label: s.auth.status === "signedIn" ? t(L, "signedInAs", { name: s.auth.user?.name || s.auth.user?.email || "?" }) : t(L, "signedOut"),
9366
9870
  enabled: false
@@ -9385,6 +9889,14 @@ function buildMenu(s) {
9385
9889
  }
9386
9890
  ] : [],
9387
9891
  { label: t(L, "refresh"), click: () => void engine?.refresh(true) },
9892
+ {
9893
+ label: s.sync.status === "running" ? t(L, "syncing") : t(L, "syncNow"),
9894
+ enabled: s.sync.status !== "running",
9895
+ click: () => void engine?.syncNow()
9896
+ },
9897
+ ...s.update ? [
9898
+ s.update.available ? { label: t(L, "updateAvailable", { version: s.update.latest ?? "?" }), click: () => void engine?.installUpdate() } : { label: t(L, "checkForUpdates"), click: () => void engine?.checkUpdate(true) }
9899
+ ] : [],
9388
9900
  { type: "separator" },
9389
9901
  { label: t(L, "quit"), click: () => import_electron.app.quit() }
9390
9902
  ];
@@ -9398,10 +9910,10 @@ async function startLogin() {
9398
9910
  log2("login failed", errorMessage(err));
9399
9911
  }
9400
9912
  }
9913
+ import_electron.app.setName(APP_NAME);
9401
9914
  if (!import_electron.app.requestSingleInstanceLock()) {
9402
9915
  import_electron.app.quit();
9403
9916
  } else {
9404
- import_electron.app.setName(APP_NAME);
9405
9917
  import_electron.app.on("second-instance", () => mb?.showWindow());
9406
9918
  import_electron.app.on("window-all-closed", () => {
9407
9919
  });
@@ -9441,6 +9953,9 @@ if (!import_electron.app.requestSingleInstanceLock()) {
9441
9953
  import_electron.ipcMain.handle("auth:cancel", () => engine?.cancelLogin());
9442
9954
  import_electron.ipcMain.handle("auth:logout", () => engine?.logout());
9443
9955
  import_electron.ipcMain.handle("refresh", () => engine?.refresh(true).then(() => void 0));
9956
+ import_electron.ipcMain.handle("sync:now", () => engine?.syncNow().then(() => void 0));
9957
+ import_electron.ipcMain.handle("update:check", () => engine?.checkUpdate(true).then(() => void 0));
9958
+ import_electron.ipcMain.handle("update:install", () => engine?.installUpdate().then(() => void 0));
9444
9959
  import_electron.ipcMain.handle("quit", () => import_electron.app.quit());
9445
9960
  mb.on("ready", async () => {
9446
9961
  if (isMac) import_electron.app.dock?.hide();
@@ -9466,7 +9981,7 @@ if (!import_electron.app.requestSingleInstanceLock()) {
9466
9981
  setTimeout(async () => {
9467
9982
  try {
9468
9983
  const img = await mb.window.webContents.capturePage();
9469
- import_node_fs8.default.writeFileSync(shot, img.toPNG());
9984
+ import_node_fs9.default.writeFileSync(shot, img.toPNG());
9470
9985
  log2("screenshot written", shot);
9471
9986
  } catch (err) {
9472
9987
  log2("screenshot failed", errorMessage(err));
@@ -9482,7 +9997,7 @@ if (!import_electron.app.requestSingleInstanceLock()) {
9482
9997
  return { action: "deny" };
9483
9998
  });
9484
9999
  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})`);
10000
+ if (DEBUG || level >= 2) console.error(`[renderer:${level}] ${message} (${import_node_path11.default.basename(sourceId)}:${line})`);
9486
10001
  });
9487
10002
  win.webContents.on("render-process-gone", (_e, details) => log2("renderer gone", details));
9488
10003
  if (process.env.CODEX_TRACKER_DEVTOOLS) win.webContents.openDevTools({ mode: "detach" });