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/cli.js CHANGED
@@ -67,6 +67,11 @@ function normalizeModelName(model) {
67
67
  m = m.replace(/-preview$/, "");
68
68
  return m;
69
69
  }
70
+ function isOpenAIModel(model) {
71
+ const m = normalizeModelName(model);
72
+ if (m === "unknown") return true;
73
+ return /^(gpt[-.]|chatgpt|chat-latest|o[1-9](?:[-.]|$)|codex|text-|davinci|babbage|ada|curie)/.test(m);
74
+ }
70
75
  function tierOf(name) {
71
76
  for (const t2 of TIERS) if (name.split("-").includes(t2)) return t2;
72
77
  return null;
@@ -106,31 +111,65 @@ function resolvePrice(model, overrides) {
106
111
  }
107
112
  function computeCost(u, p) {
108
113
  const input = u.input ?? 0;
114
+ const rate = p.long && input > p.long.threshold ? p.long : p;
109
115
  const cached = Math.min(u.cached ?? 0, input);
110
116
  const cacheWrite = Math.min(u.cacheWrite ?? 0, Math.max(0, input - cached));
111
117
  const fresh = Math.max(0, input - cached - cacheWrite);
112
118
  const output = u.output ?? 0;
113
- const cost = fresh * p.input + cached * p.cachedInput + cacheWrite * (p.cacheWrite ?? p.input) + output * p.output;
119
+ const cost = fresh * rate.input + cached * rate.cachedInput + cacheWrite * (p.cacheWrite ?? rate.input) + output * rate.output;
114
120
  return cost / 1e6;
115
121
  }
116
- var DEFAULT_PRICING, FALLBACK_PRICE_KEY, TIERS;
122
+ var LONG_CONTEXT_THRESHOLD, long, DEFAULT_PRICING, FALLBACK_PRICE_KEY, TIERS;
117
123
  var init_pricing = __esm({
118
124
  "../shared/src/pricing.ts"() {
119
125
  "use strict";
126
+ LONG_CONTEXT_THRESHOLD = 272e3;
127
+ long = (threshold, input, cachedInput, output) => ({
128
+ threshold,
129
+ input,
130
+ cachedInput,
131
+ output
132
+ });
120
133
  DEFAULT_PRICING = {
121
- // GPT-5.x family
134
+ // GPT-5.6 family
135
+ "gpt-5.6-sol": { input: 4, cachedInput: 0.4, output: 20, long: long(LONG_CONTEXT_THRESHOLD, 8, 0.8, 30) },
136
+ "gpt-5.6-sol-codex": { input: 4, cachedInput: 0.4, output: 20, long: long(LONG_CONTEXT_THRESHOLD, 8, 0.8, 30) },
137
+ "gpt-5.6-terra": { input: 2, cachedInput: 0.2, output: 12, long: long(LONG_CONTEXT_THRESHOLD, 4, 0.4, 18) },
138
+ "gpt-5.6-terra-codex": { input: 2, cachedInput: 0.2, output: 12, long: long(LONG_CONTEXT_THRESHOLD, 4, 0.4, 18) },
139
+ "gpt-5.6-luna": { input: 0.2, cachedInput: 0.02, output: 1.2, long: long(LONG_CONTEXT_THRESHOLD, 0.4, 0.04, 1.8) },
140
+ "gpt-5.6-cyber": { input: 12.5, cachedInput: 1.25, output: 75 },
141
+ // GPT-5.5 family
142
+ "gpt-5.5": { input: 5, cachedInput: 0.5, output: 30, long: long(LONG_CONTEXT_THRESHOLD, 10, 1, 45) },
143
+ "gpt-5.5-codex": { input: 5, cachedInput: 0.5, output: 30, long: long(LONG_CONTEXT_THRESHOLD, 10, 1, 45) },
144
+ "gpt-5.5-pro": { input: 30, cachedInput: 30, output: 180, long: long(LONG_CONTEXT_THRESHOLD, 60, 60, 270) },
145
+ "gpt-5.5-cyber": { input: 12.5, cachedInput: 1.25, output: 75 },
146
+ // GPT-5.4 family
147
+ "gpt-5.4": { input: 2.5, cachedInput: 0.25, output: 15, long: long(LONG_CONTEXT_THRESHOLD, 5, 0.5, 22.5) },
148
+ "gpt-5.4-codex": { input: 2.5, cachedInput: 0.25, output: 15, long: long(LONG_CONTEXT_THRESHOLD, 5, 0.5, 22.5) },
149
+ "gpt-5.4-mini": { input: 0.75, cachedInput: 0.075, output: 4.5 },
150
+ "gpt-5.4-nano": { input: 0.2, cachedInput: 0.02, output: 1.25 },
151
+ "gpt-5.4-pro": { input: 30, cachedInput: 30, output: 180, long: long(LONG_CONTEXT_THRESHOLD, 60, 60, 270) },
152
+ // GPT-5.3 family (Codex-only release)
153
+ "gpt-5.3": { input: 1.75, cachedInput: 0.175, output: 14 },
154
+ "gpt-5.3-codex": { input: 1.75, cachedInput: 0.175, output: 14 },
155
+ // GPT-5.2 family
156
+ "gpt-5.2": { input: 1.75, cachedInput: 0.175, output: 14 },
157
+ "gpt-5.2-codex": { input: 1.75, cachedInput: 0.175, output: 14 },
158
+ "gpt-5.2-pro": { input: 21, cachedInput: 21, output: 168 },
159
+ // GPT-5.1 family
160
+ "gpt-5.1": { input: 1.25, cachedInput: 0.125, output: 10 },
161
+ "gpt-5.1-codex": { input: 1.25, cachedInput: 0.125, output: 10 },
162
+ "gpt-5.1-codex-max": { input: 1.25, cachedInput: 0.125, output: 10 },
163
+ "gpt-5.1-codex-mini": { input: 0.25, cachedInput: 0.025, output: 2 },
164
+ // GPT-5 family
122
165
  "gpt-5": { input: 1.25, cachedInput: 0.125, output: 10 },
123
166
  "gpt-5-codex": { input: 1.25, cachedInput: 0.125, output: 10 },
124
167
  "gpt-5-mini": { input: 0.25, cachedInput: 0.025, output: 2 },
125
168
  "gpt-5-nano": { input: 0.05, cachedInput: 5e-3, output: 0.4 },
126
169
  "gpt-5-pro": { input: 15, cachedInput: 15, output: 120 },
127
- "gpt-5.1": { input: 1.25, cachedInput: 0.125, output: 10 },
128
- "gpt-5.1-codex": { input: 1.25, cachedInput: 0.125, output: 10 },
129
- "gpt-5.1-codex-max": { input: 1.25, cachedInput: 0.125, output: 10 },
130
- "gpt-5.1-codex-mini": { input: 0.25, cachedInput: 0.025, output: 2 },
131
- "gpt-5.2": { input: 1.75, cachedInput: 0.175, output: 14 },
132
- "gpt-5.2-codex": { input: 1.75, cachedInput: 0.175, output: 14 },
133
- "gpt-5.2-pro": { input: 21, cachedInput: 21, output: 168 },
170
+ "gpt-5-search-api": { input: 1.25, cachedInput: 0.125, output: 10 },
171
+ // ChatGPT-tuned endpoint
172
+ "chat-latest": { input: 5, cachedInput: 0.5, output: 30 },
134
173
  // GPT-4.1 / 4o family
135
174
  "gpt-4.1": { input: 2, cachedInput: 0.5, output: 8 },
136
175
  "gpt-4.1-mini": { input: 0.4, cachedInput: 0.1, output: 1.6 },
@@ -146,7 +185,7 @@ var init_pricing = __esm({
146
185
  "o4-mini": { input: 1.1, cachedInput: 0.275, output: 4.4 },
147
186
  "codex-mini-latest": { input: 1.5, cachedInput: 0.375, output: 6 }
148
187
  };
149
- FALLBACK_PRICE_KEY = "gpt-5.2";
188
+ FALLBACK_PRICE_KEY = "gpt-5.3-codex";
150
189
  TIERS = ["mini", "nano", "pro"];
151
190
  }
152
191
  });
@@ -339,14 +378,14 @@ function createSessionParser(fallbackSessionId) {
339
378
  if (ts2 > lastActivityAt) lastActivityAt = ts2;
340
379
  }
341
380
  function onTokenCount(ts2, payload) {
342
- const info = payload.info;
381
+ const info2 = payload.info;
343
382
  let cumulative = null;
344
383
  let last = null;
345
- if (info && typeof info === "object") {
346
- cumulative = toUsage(info.total_token_usage);
347
- last = toUsage(info.last_token_usage);
348
- if (typeof info.model_context_window === "number") contextWindow = info.model_context_window;
349
- } else if (info === void 0) {
384
+ if (info2 && typeof info2 === "object") {
385
+ cumulative = toUsage(info2.total_token_usage);
386
+ last = toUsage(info2.last_token_usage);
387
+ if (typeof info2.model_context_window === "number") contextWindow = info2.model_context_window;
388
+ } else if (info2 === void 0) {
350
389
  cumulative = toUsage(payload);
351
390
  }
352
391
  const rl = payload.rate_limits;
@@ -1099,9 +1138,20 @@ var init_src = __esm({
1099
1138
  }
1100
1139
  });
1101
1140
 
1141
+ // src/version.ts
1142
+ var APP_VERSION, APP_CHANNEL, IS_DEV_BUILD;
1143
+ var init_version = __esm({
1144
+ "src/version.ts"() {
1145
+ "use strict";
1146
+ APP_VERSION = true ? "0.2.1" : "0.0.0-dev";
1147
+ APP_CHANNEL = true ? "prod" : "dev";
1148
+ IS_DEV_BUILD = APP_CHANNEL === "dev";
1149
+ }
1150
+ });
1151
+
1102
1152
  // src/core/config.ts
1103
1153
  function configDir() {
1104
- return process.env.CODEX_TRACKER_HOME || import_node_path2.default.join(import_node_os2.default.homedir(), ".codex-tracker");
1154
+ return process.env.CODEX_TRACKER_HOME || import_node_path2.default.join(import_node_os2.default.homedir(), IS_DEV_BUILD ? ".codex-tracker-dev" : ".codex-tracker");
1105
1155
  }
1106
1156
  function readJson(file, fallback) {
1107
1157
  try {
@@ -1154,6 +1204,7 @@ function loadConfig() {
1154
1204
  cfg.sources = normalizeSources(stored.sources);
1155
1205
  cfg.trackAllProviders = stored.trackAllProviders === true;
1156
1206
  cfg.liveRateLimits = stored.liveRateLimits !== false;
1207
+ cfg.checkUpdates = typeof stored.checkUpdates === "boolean" ? stored.checkUpdates : DEFAULT_CONFIG.checkUpdates;
1157
1208
  if (!(cfg.usageRefreshSec >= 15)) cfg.usageRefreshSec = DEFAULT_CONFIG.usageRefreshSec;
1158
1209
  if (!["auto", "en", "zh"].includes(cfg.language)) cfg.language = "auto";
1159
1210
  if (!(cfg.uploadIntervalSec >= 10)) cfg.uploadIntervalSec = DEFAULT_CONFIG.uploadIntervalSec;
@@ -1183,6 +1234,7 @@ function coerceConfigValue(key, raw) {
1183
1234
  case "launchAtLogin":
1184
1235
  case "trackAllProviders":
1185
1236
  case "liveRateLimits":
1237
+ case "checkUpdates":
1186
1238
  return parseBool(raw);
1187
1239
  case "extraSessionDirs": {
1188
1240
  const trimmed = raw.trim();
@@ -1259,14 +1311,17 @@ function loadPricingOverrides() {
1259
1311
  }
1260
1312
  return Object.keys(out).length ? out : void 0;
1261
1313
  }
1262
- var import_node_fs2, import_node_os2, import_node_path2, DEFAULT_DASHBOARD_URL, SOURCE_FORMATS, DEFAULT_SOURCES, SOURCE_IDS, DEFAULT_CONFIG, EDITABLE_KEYS, AGENT_NAME_RE;
1314
+ var import_node_fs2, import_node_os2, import_node_path2, PROD_DASHBOARD_URL, DEV_DASHBOARD_URL, DEFAULT_DASHBOARD_URL, SOURCE_FORMATS, DEFAULT_SOURCES, SOURCE_IDS, DEFAULT_CONFIG, EDITABLE_KEYS, AGENT_NAME_RE;
1263
1315
  var init_config = __esm({
1264
1316
  "src/core/config.ts"() {
1265
1317
  "use strict";
1266
1318
  import_node_fs2 = __toESM(require("node:fs"));
1267
1319
  import_node_os2 = __toESM(require("node:os"));
1268
1320
  import_node_path2 = __toESM(require("node:path"));
1269
- DEFAULT_DASHBOARD_URL = "https://codex.chenli.dev";
1321
+ init_version();
1322
+ PROD_DASHBOARD_URL = "https://codex.chenli.dev";
1323
+ DEV_DASHBOARD_URL = "http://localhost:3000";
1324
+ DEFAULT_DASHBOARD_URL = IS_DEV_BUILD ? DEV_DASHBOARD_URL : PROD_DASHBOARD_URL;
1270
1325
  SOURCE_FORMATS = ["codex", "pi", "generic", "opencode", "cline"];
1271
1326
  DEFAULT_SOURCES = { codex: true, pi: true, hermes: true, opencode: true, cline: true, roo: true, kilo: true };
1272
1327
  SOURCE_IDS = Object.keys(DEFAULT_SOURCES);
@@ -1285,7 +1340,9 @@ var init_config = __esm({
1285
1340
  sources: { ...DEFAULT_SOURCES },
1286
1341
  trackAllProviders: false,
1287
1342
  liveRateLimits: true,
1288
- usageRefreshSec: 60
1343
+ usageRefreshSec: 60,
1344
+ // a dev build must never offer to replace itself with the published package
1345
+ checkUpdates: !IS_DEV_BUILD
1289
1346
  };
1290
1347
  EDITABLE_KEYS = [
1291
1348
  "dashboardUrl",
@@ -1298,7 +1355,8 @@ var init_config = __esm({
1298
1355
  "sources",
1299
1356
  "trackAllProviders",
1300
1357
  "liveRateLimits",
1301
- "usageRefreshSec"
1358
+ "usageRefreshSec",
1359
+ "checkUpdates"
1302
1360
  ];
1303
1361
  AGENT_NAME_RE = /^[a-z0-9][a-z0-9_.-]{0,31}$/;
1304
1362
  }
@@ -1354,6 +1412,7 @@ var init_en = __esm({
1354
1412
  estimatedPricingHint: "Pricing estimated: model not in the price table",
1355
1413
  other: "Other",
1356
1414
  lastUpload: "Last upload {time}",
1415
+ lastSync: "Last full sync {time}",
1357
1416
  neverUploaded: "Not uploaded yet",
1358
1417
  uploadError: "Upload error: {message}",
1359
1418
  allDevicesToday: "All devices today",
@@ -1372,16 +1431,44 @@ var init_en = __esm({
1372
1431
  launchAtLogin: "Launch at login",
1373
1432
  refresh: "Refresh",
1374
1433
  quit: "Quit",
1434
+ // full sync
1435
+ syncNow: "Sync now",
1436
+ syncing: "Syncing\u2026",
1437
+ syncTitle: "Rescan every agent on this device and re-upload its full usage history",
1438
+ syncScanning: "Rescanning every agent's transcripts\u2026",
1439
+ syncComputing: "Recomputing token usage\u2026",
1440
+ syncUploading: "Re-uploading this device's history\u2026",
1441
+ syncDownloading: "Fetching other devices\u2026",
1442
+ syncLimits: "Refreshing rate limits\u2026",
1443
+ syncDone: "Synced {agents} \xB7 {files} files \xB7 {sessions} sessions \xB7 {buckets} hours re-uploaded",
1444
+ syncDoneLocal: "Rescanned {agents} \xB7 {files} files \xB7 {sessions} sessions \u2014 sign in to upload",
1445
+ syncFailed: "Sync failed: {message}",
1375
1446
  sessions: "{n} sessions",
1376
1447
  files: "{n} files",
1377
1448
  noData: "No usage found yet. Run Codex and this will fill in.",
1378
1449
  version: "v{version}",
1450
+ // dev builds
1451
+ devBuild: "DEV",
1452
+ devBuildTitle: "Local build \u2014 uploading to {url} (dev environment). Config: {dir}",
1453
+ devBuildMenu: "Dev build \u2192 {url}",
1379
1454
  justNow: "just now",
1380
1455
  never: "never",
1381
1456
  local: "Local",
1382
1457
  remote: "Other devices",
1458
+ // updates
1459
+ update: "Update",
1460
+ updateAvailable: "Update to v{version}",
1461
+ updateNewVersion: "Version {version} is available (you have v{current})",
1462
+ updateUpToDate: "Up to date",
1463
+ updateChecking: "Checking\u2026",
1464
+ updateInstalling: "Installing\u2026",
1465
+ updateInstalled: "Updated to v{version} \u2014 restart to apply",
1466
+ updateFailed: "Update failed \u2014 run this yourself:",
1467
+ updateCheckFailed: "Update check failed: {message}",
1468
+ checkForUpdates: "Check for updates",
1469
+ releaseNotes: "Release notes",
1383
1470
  // CLI
1384
- cliUsage: `Usage: codex-tracker [command] [options]
1471
+ cliUsage: `Usage: codex-token-tracker [command] [options] (alias: codex-tracker)
1385
1472
 
1386
1473
  Commands:
1387
1474
  (none) Start the menu bar app (falls back to agent mode without a display)
@@ -1391,11 +1478,14 @@ Commands:
1391
1478
  login Connect this device to your dashboard (--dashboard <url>)
1392
1479
  logout Disconnect this device
1393
1480
  status Print today's usage, live session and rate limits
1481
+ sync Rescan every agent and re-upload this device's full history (calibrate)
1394
1482
  paths Show detected session directories (Codex, pi, OpenCode, Cline/Roo/Kilo, Hermes, custom)
1483
+ update Install the newest published version (--check only reports)
1395
1484
  config get|set Read or change settings (config set uploadIntervalSec 30, config set sources.pi false)
1396
1485
  lang <en|zh|auto> Set the display language
1397
1486
 
1398
1487
  Options:
1488
+ --check update: report the newest version without installing it
1399
1489
  --dashboard <url> Dashboard URL (self-hosted)
1400
1490
  --background Start the menu bar app detached and return
1401
1491
  --version, -v Print version
@@ -1437,7 +1527,21 @@ Options:
1437
1527
  cliConfigSet: "Set {key} = {value}",
1438
1528
  cliConfigUnknownKey: "Unknown config key: {key}. Keys: {keys}",
1439
1529
  cliLangSet: "Language set to {lang}",
1440
- cliConfigDir: "Config: {dir}"
1530
+ cliConfigDir: "Config: {dir}",
1531
+ cliUpdateLatest: "codex-token-tracker {version} is the latest version.",
1532
+ cliUpdateAvailable: "Update available: {current} \u2192 {latest}",
1533
+ cliUpdateRunning: "Running: {command}",
1534
+ cliUpdateDone: "Updated to {version}. Restart the menu bar app (or rerun the CLI) to use it.",
1535
+ cliUpdateFailed: "Update failed (exit {code}). Run it yourself: {command}",
1536
+ cliUpdateCheckFailed: "Could not reach the npm registry: {message}",
1537
+ cliSyncStart: "Full sync: rescanning every agent on this device\u2026",
1538
+ cliSyncScanned: " scanned {files} files in {roots} directories ({agents})",
1539
+ cliSyncUploaded: " re-uploaded {buckets} hour buckets and {sessions} sessions",
1540
+ cliSyncLocal: " not signed in \u2014 nothing uploaded (run `codex-tracker login` first)",
1541
+ cliSyncDone: "Sync complete in {seconds}s \u2014 {sessions} sessions, today {tokens} tok \xB7 {cost}",
1542
+ cliSyncFailed: "Sync failed: {message}",
1543
+ cliChannelDev: "Dev build \u2014 dashboard {url} \xB7 config {dir}",
1544
+ cliUpdateDevBuild: "This is a local dev build; `update` would install the published package over it. Run `pnpm build` in the repo instead."
1441
1545
  };
1442
1546
  }
1443
1547
  });
@@ -1491,6 +1595,7 @@ var init_zh = __esm({
1491
1595
  estimatedPricingHint: "\u4EF7\u683C\u4E3A\u4F30\u7B97\u503C\uFF1A\u8BE5\u6A21\u578B\u4E0D\u5728\u4EF7\u76EE\u8868\u4E2D",
1492
1596
  other: "\u5176\u4ED6",
1493
1597
  lastUpload: "\u4E0A\u6B21\u4E0A\u4F20 {time}",
1598
+ lastSync: "\u4E0A\u6B21\u5B8C\u6574\u540C\u6B65 {time}",
1494
1599
  neverUploaded: "\u5C1A\u672A\u4E0A\u4F20",
1495
1600
  uploadError: "\u4E0A\u4F20\u5931\u8D25\uFF1A{message}",
1496
1601
  allDevicesToday: "\u6240\u6709\u8BBE\u5907\u4ECA\u65E5",
@@ -1509,15 +1614,42 @@ var init_zh = __esm({
1509
1614
  launchAtLogin: "\u5F00\u673A\u81EA\u542F\u52A8",
1510
1615
  refresh: "\u5237\u65B0",
1511
1616
  quit: "\u9000\u51FA",
1617
+ // 完整同步
1618
+ syncNow: "\u7ACB\u5373\u540C\u6B65",
1619
+ syncing: "\u540C\u6B65\u4E2D\u2026",
1620
+ 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",
1621
+ syncScanning: "\u6B63\u5728\u91CD\u65B0\u626B\u63CF\u6240\u6709\u667A\u80FD\u4F53\u7684\u4F1A\u8BDD\u8BB0\u5F55\u2026",
1622
+ syncComputing: "\u6B63\u5728\u91CD\u65B0\u8BA1\u7B97 token \u7528\u91CF\u2026",
1623
+ syncUploading: "\u6B63\u5728\u91CD\u65B0\u4E0A\u4F20\u672C\u8BBE\u5907\u7684\u5386\u53F2\u6570\u636E\u2026",
1624
+ syncDownloading: "\u6B63\u5728\u62C9\u53D6\u5176\u4ED6\u8BBE\u5907\u7684\u6570\u636E\u2026",
1625
+ syncLimits: "\u6B63\u5728\u5237\u65B0\u9650\u989D\u2026",
1626
+ 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",
1627
+ syncDoneLocal: "\u5DF2\u91CD\u65B0\u626B\u63CF {agents} \xB7 {files} \u4E2A\u6587\u4EF6 \xB7 {sessions} \u4E2A\u4F1A\u8BDD \u2014 \u767B\u5F55\u540E\u53EF\u4E0A\u4F20",
1628
+ syncFailed: "\u540C\u6B65\u5931\u8D25\uFF1A{message}",
1512
1629
  sessions: "{n} \u4E2A\u4F1A\u8BDD",
1513
1630
  files: "{n} \u4E2A\u6587\u4EF6",
1514
1631
  noData: "\u8FD8\u6CA1\u6709\u7528\u91CF\u6570\u636E\u3002\u8FD0\u884C Codex \u540E\u8FD9\u91CC\u4F1A\u81EA\u52A8\u66F4\u65B0\u3002",
1515
1632
  version: "v{version}",
1633
+ // 开发版
1634
+ devBuild: "\u5F00\u53D1\u7248",
1635
+ 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}",
1636
+ devBuildMenu: "\u5F00\u53D1\u7248 \u2192 {url}",
1516
1637
  justNow: "\u521A\u521A",
1517
1638
  never: "\u4ECE\u672A",
1518
1639
  local: "\u672C\u673A",
1519
1640
  remote: "\u5176\u4ED6\u8BBE\u5907",
1520
- cliUsage: `\u7528\u6CD5\uFF1Acodex-tracker [\u547D\u4EE4] [\u9009\u9879]
1641
+ update: "\u66F4\u65B0",
1642
+ updateAvailable: "\u66F4\u65B0\u5230 v{version}",
1643
+ updateNewVersion: "\u6709\u65B0\u7248\u672C {version}\uFF08\u5F53\u524D v{current}\uFF09",
1644
+ updateUpToDate: "\u5DF2\u662F\u6700\u65B0\u7248\u672C",
1645
+ updateChecking: "\u68C0\u67E5\u4E2D\u2026",
1646
+ updateInstalling: "\u5B89\u88C5\u4E2D\u2026",
1647
+ updateInstalled: "\u5DF2\u66F4\u65B0\u5230 v{version} \u2014 \u91CD\u542F\u540E\u751F\u6548",
1648
+ updateFailed: "\u66F4\u65B0\u5931\u8D25 \u2014 \u8BF7\u624B\u52A8\u6267\u884C\uFF1A",
1649
+ updateCheckFailed: "\u68C0\u67E5\u66F4\u65B0\u5931\u8D25\uFF1A{message}",
1650
+ checkForUpdates: "\u68C0\u67E5\u66F4\u65B0",
1651
+ releaseNotes: "\u66F4\u65B0\u65E5\u5FD7",
1652
+ cliUsage: `\u7528\u6CD5\uFF1Acodex-token-tracker [\u547D\u4EE4] [\u9009\u9879] \uFF08\u522B\u540D\uFF1Acodex-tracker\uFF09
1521
1653
 
1522
1654
  \u547D\u4EE4\uFF1A
1523
1655
  (\u65E0) \u542F\u52A8\u83DC\u5355\u680F\u5E94\u7528\uFF08\u65E0\u663E\u793A\u73AF\u5883\u65F6\u81EA\u52A8\u5207\u6362\u4E3A agent \u6A21\u5F0F\uFF09
@@ -1527,11 +1659,14 @@ var init_zh = __esm({
1527
1659
  login \u5C06\u672C\u8BBE\u5907\u8FDE\u63A5\u5230\u4EEA\u8868\u76D8\uFF08--dashboard <url>\uFF09
1528
1660
  logout \u65AD\u5F00\u672C\u8BBE\u5907
1529
1661
  status \u6253\u5370\u4ECA\u65E5\u7528\u91CF\u3001\u5B9E\u65F6\u4F1A\u8BDD\u4E0E\u9650\u989D
1662
+ 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
1530
1663
  paths \u663E\u793A\u68C0\u6D4B\u5230\u7684\u4F1A\u8BDD\u76EE\u5F55\uFF08Codex\u3001pi\u3001OpenCode\u3001Cline/Roo/Kilo\u3001Hermes\u3001\u81EA\u5B9A\u4E49\uFF09
1664
+ update \u5B89\u88C5\u6700\u65B0\u53D1\u5E03\u7248\u672C\uFF08--check \u53EA\u68C0\u67E5\u4E0D\u5B89\u88C5\uFF09
1531
1665
  config get|set \u8BFB\u53D6\u6216\u4FEE\u6539\u8BBE\u7F6E\uFF08config set uploadIntervalSec 30\uFF0Cconfig set sources.pi false\uFF09
1532
1666
  lang <en|zh|auto> \u8BBE\u7F6E\u663E\u793A\u8BED\u8A00
1533
1667
 
1534
1668
  \u9009\u9879\uFF1A
1669
+ --check update\uFF1A\u53EA\u62A5\u544A\u6700\u65B0\u7248\u672C\uFF0C\u4E0D\u6267\u884C\u5B89\u88C5
1535
1670
  --dashboard <url> \u4EEA\u8868\u76D8\u5730\u5740\uFF08\u81EA\u6258\u7BA1\uFF09
1536
1671
  --background \u4EE5\u540E\u53F0\u65B9\u5F0F\u542F\u52A8\u83DC\u5355\u680F\u5E94\u7528\u5E76\u7ACB\u5373\u8FD4\u56DE
1537
1672
  --version, -v \u663E\u793A\u7248\u672C
@@ -1573,7 +1708,21 @@ var init_zh = __esm({
1573
1708
  cliConfigSet: "\u5DF2\u8BBE\u7F6E {key} = {value}",
1574
1709
  cliConfigUnknownKey: "\u672A\u77E5\u914D\u7F6E\u9879\uFF1A{key}\u3002\u53EF\u7528\uFF1A{keys}",
1575
1710
  cliLangSet: "\u8BED\u8A00\u5DF2\u8BBE\u7F6E\u4E3A {lang}",
1576
- cliConfigDir: "\u914D\u7F6E\u76EE\u5F55\uFF1A{dir}"
1711
+ cliConfigDir: "\u914D\u7F6E\u76EE\u5F55\uFF1A{dir}",
1712
+ cliUpdateLatest: "codex-token-tracker {version} \u5DF2\u662F\u6700\u65B0\u7248\u672C\u3002",
1713
+ cliUpdateAvailable: "\u53D1\u73B0\u65B0\u7248\u672C\uFF1A{current} \u2192 {latest}",
1714
+ cliUpdateRunning: "\u6B63\u5728\u6267\u884C\uFF1A{command}",
1715
+ 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",
1716
+ cliUpdateFailed: "\u66F4\u65B0\u5931\u8D25\uFF08\u9000\u51FA\u7801 {code}\uFF09\u3002\u8BF7\u624B\u52A8\u6267\u884C\uFF1A{command}",
1717
+ cliUpdateCheckFailed: "\u65E0\u6CD5\u8BBF\u95EE npm registry\uFF1A{message}",
1718
+ cliSyncStart: "\u5B8C\u6574\u540C\u6B65\uFF1A\u6B63\u5728\u91CD\u65B0\u626B\u63CF\u672C\u8BBE\u5907\u4E0A\u7684\u6240\u6709\u667A\u80FD\u4F53\u2026",
1719
+ cliSyncScanned: " \u5DF2\u626B\u63CF {roots} \u4E2A\u76EE\u5F55\u4E2D\u7684 {files} \u4E2A\u6587\u4EF6\uFF08{agents}\uFF09",
1720
+ cliSyncUploaded: " \u5DF2\u91CD\u65B0\u4E0A\u4F20 {buckets} \u4E2A\u5C0F\u65F6\u6BB5\u4E0E {sessions} \u4E2A\u4F1A\u8BDD",
1721
+ cliSyncLocal: " \u672A\u767B\u5F55 \u2014 \u6CA1\u6709\u4E0A\u4F20\u4EFB\u4F55\u6570\u636E\uFF08\u8BF7\u5148\u8FD0\u884C `codex-tracker login`\uFF09",
1722
+ cliSyncDone: "\u540C\u6B65\u5B8C\u6210\uFF0C\u7528\u65F6 {seconds} \u79D2 \u2014 {sessions} \u4E2A\u4F1A\u8BDD\uFF0C\u4ECA\u65E5 {tokens} tok \xB7 {cost}",
1723
+ cliSyncFailed: "\u540C\u6B65\u5931\u8D25\uFF1A{message}",
1724
+ cliChannelDev: "\u5F00\u53D1\u7248 \u2014\u2014 \u4EEA\u8868\u76D8 {url} \xB7 \u914D\u7F6E\u76EE\u5F55 {dir}",
1725
+ 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"
1577
1726
  };
1578
1727
  }
1579
1728
  });
@@ -3194,12 +3343,12 @@ function createApi(pathParts = []) {
3194
3343
  `API path is expected to be of the form \`api.moduleName.functionName\`. Found: \`${found}\``
3195
3344
  );
3196
3345
  }
3197
- const path12 = pathParts.slice(0, -1).join("/");
3346
+ const path13 = pathParts.slice(0, -1).join("/");
3198
3347
  const exportName = pathParts[pathParts.length - 1];
3199
3348
  if (exportName === "default") {
3200
- return path12;
3349
+ return path13;
3201
3350
  } else {
3202
- return path12 + ":" + exportName;
3351
+ return path13 + ":" + exportName;
3203
3352
  }
3204
3353
  } else if (prop === Symbol.toStringTag) {
3205
3354
  return "FunctionReference";
@@ -3494,8 +3643,8 @@ var init_simple_client_node = __esm({
3494
3643
  });
3495
3644
  require_node_gyp_build = __commonJS({
3496
3645
  "../node_modules/.pnpm/node-gyp-build@4.8.4/node_modules/node-gyp-build/node-gyp-build.js"(exports2, module2) {
3497
- var fs10 = __require("fs");
3498
- var path12 = __require("path");
3646
+ var fs11 = __require("fs");
3647
+ var path13 = __require("path");
3499
3648
  var os8 = __require("os");
3500
3649
  var runtimeRequire = typeof __webpack_require__ === "function" ? __non_webpack_require__ : __require;
3501
3650
  var vars2 = process.config && process.config.variables || {};
@@ -3512,21 +3661,21 @@ var init_simple_client_node = __esm({
3512
3661
  return runtimeRequire(load.resolve(dir));
3513
3662
  }
3514
3663
  load.resolve = load.path = function(dir) {
3515
- dir = path12.resolve(dir || ".");
3664
+ dir = path13.resolve(dir || ".");
3516
3665
  try {
3517
- var name = runtimeRequire(path12.join(dir, "package.json")).name.toUpperCase().replace(/-/g, "_");
3666
+ var name = runtimeRequire(path13.join(dir, "package.json")).name.toUpperCase().replace(/-/g, "_");
3518
3667
  if (process.env[name + "_PREBUILD"]) dir = process.env[name + "_PREBUILD"];
3519
3668
  } catch (err) {
3520
3669
  }
3521
3670
  if (!prebuildsOnly) {
3522
- var release = getFirst(path12.join(dir, "build/Release"), matchBuild);
3671
+ var release = getFirst(path13.join(dir, "build/Release"), matchBuild);
3523
3672
  if (release) return release;
3524
- var debug = getFirst(path12.join(dir, "build/Debug"), matchBuild);
3673
+ var debug = getFirst(path13.join(dir, "build/Debug"), matchBuild);
3525
3674
  if (debug) return debug;
3526
3675
  }
3527
3676
  var prebuild = resolve(dir);
3528
3677
  if (prebuild) return prebuild;
3529
- var nearby = resolve(path12.dirname(process.execPath));
3678
+ var nearby = resolve(path13.dirname(process.execPath));
3530
3679
  if (nearby) return nearby;
3531
3680
  var target = [
3532
3681
  "platform=" + platform,
@@ -3543,26 +3692,26 @@ var init_simple_client_node = __esm({
3543
3692
  ].filter(Boolean).join(" ");
3544
3693
  throw new Error("No native build was found for " + target + "\n loaded from: " + dir + "\n");
3545
3694
  function resolve(dir2) {
3546
- var tuples = readdirSync(path12.join(dir2, "prebuilds")).map(parseTuple);
3695
+ var tuples = readdirSync(path13.join(dir2, "prebuilds")).map(parseTuple);
3547
3696
  var tuple = tuples.filter(matchTuple(platform, arch)).sort(compareTuples)[0];
3548
3697
  if (!tuple) return;
3549
- var prebuilds = path12.join(dir2, "prebuilds", tuple.name);
3698
+ var prebuilds = path13.join(dir2, "prebuilds", tuple.name);
3550
3699
  var parsed = readdirSync(prebuilds).map(parseTags);
3551
3700
  var candidates = parsed.filter(matchTags(runtime, abi));
3552
3701
  var winner = candidates.sort(compareTags(runtime))[0];
3553
- if (winner) return path12.join(prebuilds, winner.file);
3702
+ if (winner) return path13.join(prebuilds, winner.file);
3554
3703
  }
3555
3704
  };
3556
3705
  function readdirSync(dir) {
3557
3706
  try {
3558
- return fs10.readdirSync(dir);
3707
+ return fs11.readdirSync(dir);
3559
3708
  } catch (err) {
3560
3709
  return [];
3561
3710
  }
3562
3711
  }
3563
3712
  function getFirst(dir, filter) {
3564
3713
  var files = readdirSync(dir).filter(filter);
3565
- return files[0] && path12.join(dir, files[0]);
3714
+ return files[0] && path13.join(dir, files[0]);
3566
3715
  }
3567
3716
  function matchBuild(name) {
3568
3717
  return /\.node$/.test(name);
@@ -3649,7 +3798,7 @@ var init_simple_client_node = __esm({
3649
3798
  return typeof window !== "undefined" && window.process && window.process.type === "renderer";
3650
3799
  }
3651
3800
  function isAlpine(platform2) {
3652
- return platform2 === "linux" && fs10.existsSync("/etc/alpine-release");
3801
+ return platform2 === "linux" && fs11.existsSync("/etc/alpine-release");
3653
3802
  }
3654
3803
  load.parseTags = parseTags;
3655
3804
  load.matchTags = matchTags;
@@ -7195,13 +7344,13 @@ var init_simple_client_node = __esm({
7195
7344
  }
7196
7345
  }
7197
7346
  if (this.options.verifyClient) {
7198
- const info = {
7347
+ const info2 = {
7199
7348
  origin: req.headers[`${version2 === 8 ? "sec-websocket-origin" : "origin"}`],
7200
7349
  secure: !!(req.socket.authorized || req.socket.encrypted),
7201
7350
  req
7202
7351
  };
7203
7352
  if (this.options.verifyClient.length === 2) {
7204
- this.options.verifyClient(info, (verified, code2, message, headers) => {
7353
+ this.options.verifyClient(info2, (verified, code2, message, headers) => {
7205
7354
  if (!verified) {
7206
7355
  return abortHandshake(socket, code2 || 401, message, headers);
7207
7356
  }
@@ -7217,7 +7366,7 @@ var init_simple_client_node = __esm({
7217
7366
  });
7218
7367
  return;
7219
7368
  }
7220
- if (!this.options.verifyClient(info)) return abortHandshake(socket, 401);
7369
+ if (!this.options.verifyClient(info2)) return abortHandshake(socket, 401);
7221
7370
  }
7222
7371
  this.completeUpgrade(extensions, key, protocols, req, socket, head, cb);
7223
7372
  }
@@ -8548,11 +8697,25 @@ var init_uploader = __esm({
8548
8697
  throw err;
8549
8698
  }
8550
8699
  }
8551
- /** Upload changed hour buckets and session summaries. Returns counts of pushed items. */
8552
- async pushAll(buckets, sessions, sessionCosts) {
8700
+ /** Wait for an in-flight incremental push to finish so a full sync isn't skipped by the guard. */
8701
+ async awaitIdle(timeoutMs = 15e3) {
8702
+ const deadline = Date.now() + timeoutMs;
8703
+ while (this.inFlight && Date.now() < deadline) await new Promise((r) => setTimeout(r, 100));
8704
+ }
8705
+ /**
8706
+ * Upload changed hour buckets and session summaries. Returns counts of pushed items.
8707
+ * With `full`, the record of what was already pushed is dropped first so *everything* is re-sent —
8708
+ * that is what recalibrates this device's totals on the dashboard after a parser or pricing change.
8709
+ */
8710
+ async pushAll(buckets, sessions, sessionCosts, opts = {}) {
8711
+ if (opts.full) await this.awaitIdle();
8553
8712
  if (this.inFlight) return { buckets: 0, sessions: 0 };
8554
8713
  this.inFlight = true;
8555
8714
  try {
8715
+ if (opts.full) {
8716
+ this.state.pushedBuckets = {};
8717
+ this.state.pushedSessions = {};
8718
+ }
8556
8719
  const changed = [];
8557
8720
  const hashes = /* @__PURE__ */ new Map();
8558
8721
  for (const b of buckets.sort((a, b2) => a.hourStart - b2.hourStart)) {
@@ -8723,9 +8886,9 @@ var init_auth = __esm({
8723
8886
  });
8724
8887
 
8725
8888
  // src/cli.ts
8726
- var import_node_path11 = __toESM(require("node:path"));
8727
- var import_node_fs9 = __toESM(require("node:fs"));
8728
- var import_node_child_process4 = require("node:child_process");
8889
+ var import_node_path12 = __toESM(require("node:path"));
8890
+ var import_node_fs10 = __toESM(require("node:fs"));
8891
+ var import_node_child_process5 = require("node:child_process");
8729
8892
 
8730
8893
  // src/core/electron-install.ts
8731
8894
  var import_node_fs = __toESM(require("node:fs"));
@@ -9258,21 +9421,21 @@ function findSessionInfo(storage, sessionID) {
9258
9421
  const key = `${storage}|${sessionID}`;
9259
9422
  const cached = sessionCache.get(key);
9260
9423
  if (cached && (cached.info || Date.now() - cached.at < 6e4)) return cached.info;
9261
- let info = null;
9424
+ let info2 = null;
9262
9425
  const sessionDir = import_node_path7.default.join(storage, "session");
9263
9426
  const direct = import_node_path7.default.join(sessionDir, "info", `${sessionID}.json`);
9264
- if (import_node_fs5.default.existsSync(direct)) info = readJsonFile(direct);
9265
- if (!info) {
9427
+ if (import_node_fs5.default.existsSync(direct)) info2 = readJsonFile(direct);
9428
+ if (!info2) {
9266
9429
  for (const project of listDirs(sessionDir)) {
9267
9430
  const p = import_node_path7.default.join(sessionDir, project, `${sessionID}.json`);
9268
9431
  if (import_node_fs5.default.existsSync(p)) {
9269
- info = readJsonFile(p);
9432
+ info2 = readJsonFile(p);
9270
9433
  break;
9271
9434
  }
9272
9435
  }
9273
9436
  }
9274
- sessionCache.set(key, { at: Date.now(), info });
9275
- return info;
9437
+ sessionCache.set(key, { at: Date.now(), info: info2 });
9438
+ return info2;
9276
9439
  }
9277
9440
  function openaiIsOAuth(dataDir) {
9278
9441
  const file = import_node_path7.default.join(dataDir, "auth.json");
@@ -9342,16 +9505,16 @@ var opencodeSource = {
9342
9505
  provider,
9343
9506
  usage: { input, cached, cacheWrite, output, reasoning: t2.reasoning ?? 0, total: input + output, requests: 1 }
9344
9507
  };
9345
- const info = storage ? findSessionInfo(storage, m.sessionID) : null;
9346
- const cwd = info?.directory ?? null;
9508
+ const info2 = storage ? findSessionInfo(storage, m.sessionID) : null;
9509
+ const cwd = info2?.directory ?? null;
9347
9510
  return {
9348
9511
  sessionId: m.sessionID,
9349
9512
  agent: file.root.agent,
9350
9513
  provider,
9351
- startedAt: info?.time?.created ?? ts2,
9514
+ startedAt: info2?.time?.created ?? ts2,
9352
9515
  lastActivityAt: ts2,
9353
9516
  cwd,
9354
- projectName: projectNameOf(cwd) ?? info?.title ?? null,
9517
+ projectName: projectNameOf(cwd) ?? info2?.title ?? null,
9355
9518
  originator: "opencode",
9356
9519
  source: "opencode",
9357
9520
  cliVersion: null,
@@ -9626,6 +9789,15 @@ var SessionStore = class {
9626
9789
  onChange = null;
9627
9790
  debounceTimer = null;
9628
9791
  sessionCache = null;
9792
+ /**
9793
+ * Forget the parsed-file index so the next deep refresh re-reads and re-parses every transcript.
9794
+ * Used by the full sync: files are otherwise skipped while their size and mtime are unchanged, which
9795
+ * would keep stale numbers around after a parser or pricing change.
9796
+ */
9797
+ reset() {
9798
+ this.files.clear();
9799
+ this.sessionCache = null;
9800
+ }
9629
9801
  async refreshDeep() {
9630
9802
  const o = this.getOptions();
9631
9803
  this.roots = discoverSessionRoots({ extraSessionDirs: o.extraSessionDirs, sources: o.sources });
@@ -9784,7 +9956,20 @@ var SessionStore = class {
9784
9956
  // src/core/stats.ts
9785
9957
  init_src();
9786
9958
  var LIVE_WINDOW_MS = 5 * 60 * 1e3;
9959
+ var RATE_WINDOW_MS = 6e4;
9960
+ var BURST_WINDOW_MS = 1e4;
9787
9961
  var DAY3 = 864e5;
9962
+ function outputRate(events, now, windowMs, sessionStart) {
9963
+ const from = now - windowMs;
9964
+ let output = 0;
9965
+ for (const e of events) {
9966
+ if (e.ts <= from) continue;
9967
+ output += e.usage.output;
9968
+ }
9969
+ if (output <= 0) return 0;
9970
+ const elapsedSec = Math.max(1, (now - Math.max(from, sessionStart)) / 1e3);
9971
+ return output / elapsedSec;
9972
+ }
9788
9973
  var PriceCache = class {
9789
9974
  constructor(overrides) {
9790
9975
  this.overrides = overrides;
@@ -9861,7 +10046,9 @@ function agentStats(sessions, since, prices) {
9861
10046
  function computeStats(input) {
9862
10047
  const now = input.now ?? Date.now();
9863
10048
  const prices = new PriceCache(input.pricing);
9864
- const sessions = input.sessions;
10049
+ const sessions = input.sessions.map(
10050
+ (s) => s.events.every((e) => isOpenAIModel(e.model)) ? s : { ...s, events: s.events.filter((e) => isOpenAIModel(e.model)) }
10051
+ );
9865
10052
  const allEvents = [];
9866
10053
  const sessionCosts = /* @__PURE__ */ new Map();
9867
10054
  let lastActivityAt = null;
@@ -9887,14 +10074,8 @@ function computeStats(input) {
9887
10074
  let live = null;
9888
10075
  const liveSession = sessions.filter((s) => s.events.length && now - s.lastActivityAt <= LIVE_WINDOW_MS).sort((a, b) => b.lastActivityAt - a.lastActivityAt)[0];
9889
10076
  if (liveSession) {
9890
- let t60 = 0;
9891
- let t10 = 0;
9892
- for (const e of liveSession.events) {
9893
- const age = now - e.ts;
9894
- if (age <= 6e4) t60 += e.usage.total;
9895
- if (age <= 1e4) t10 += e.usage.total;
9896
- }
9897
10077
  const last = liveSession.events[liveSession.events.length - 1];
10078
+ const start = liveSession.startedAt || liveSession.events[0].ts;
9898
10079
  live = {
9899
10080
  sessionId: liveSession.sessionId,
9900
10081
  agent: liveSession.agent,
@@ -9902,8 +10083,8 @@ function computeStats(input) {
9902
10083
  model: liveSession.model,
9903
10084
  startedAt: liveSession.startedAt,
9904
10085
  lastEventAt: last.ts,
9905
- tokensPerSecond: t60 / 60,
9906
- tokensPerSecond10s: t10 / 10,
10086
+ tokensPerSecond: outputRate(liveSession.events, now, RATE_WINDOW_MS, start),
10087
+ tokensPerSecond10s: outputRate(liveSession.events, now, BURST_WINDOW_MS, start),
9907
10088
  contextUsed: last.usage.input + last.usage.output,
9908
10089
  contextWindow: liveSession.contextWindow,
9909
10090
  sessionUsage: { ...liveSession.cumulative },
@@ -9930,6 +10111,7 @@ function computeStats(input) {
9930
10111
  }
9931
10112
  return {
9932
10113
  buckets,
10114
+ sessions: sessions.filter((s) => s.events.length),
9933
10115
  today,
9934
10116
  week,
9935
10117
  month,
@@ -10010,16 +10192,180 @@ async function fetchLiveRateLimits(appVersion, timeoutMs = 1e4) {
10010
10192
  // src/core/engine.ts
10011
10193
  init_i18n();
10012
10194
 
10013
- // src/version.ts
10014
- var APP_VERSION = true ? "0.1.1" : "0.0.0-dev";
10195
+ // src/core/update.ts
10196
+ var import_node_fs9 = __toESM(require("node:fs"));
10197
+ var import_node_path11 = __toESM(require("node:path"));
10198
+ var import_node_child_process4 = require("node:child_process");
10199
+ init_config();
10200
+ init_version();
10201
+ var NPM_PACKAGE = "codex-token-tracker";
10202
+ var CHECK_TTL_MS = 6 * 60 * 60 * 1e3;
10203
+ var REQUEST_TIMEOUT_MS = 8e3;
10204
+ var INSTALL_TIMEOUT_MS = 10 * 60 * 1e3;
10205
+ var LOG_TAIL_CHARS = 4e3;
10206
+ function parseVersion(v2) {
10207
+ const s = String(v2 ?? "").trim().replace(/^v/, "");
10208
+ const i2 = s.indexOf("-");
10209
+ const core = i2 === -1 ? s : s.slice(0, i2);
10210
+ const pre = i2 === -1 ? "" : s.slice(i2 + 1);
10211
+ const n = core.split(".").map((x) => Number.parseInt(x, 10) || 0);
10212
+ return { nums: [n[0] ?? 0, n[1] ?? 0, n[2] ?? 0], pre };
10213
+ }
10214
+ function compareVersions(a, b) {
10215
+ const A = parseVersion(a);
10216
+ const B = parseVersion(b);
10217
+ for (let i2 = 0; i2 < 3; i2++) if (A.nums[i2] !== B.nums[i2]) return A.nums[i2] < B.nums[i2] ? -1 : 1;
10218
+ if (A.pre === B.pre) return 0;
10219
+ if (!A.pre) return 1;
10220
+ if (!B.pre) return -1;
10221
+ return A.pre < B.pre ? -1 : 1;
10222
+ }
10223
+ function isDevBuild(version2) {
10224
+ return IS_DEV_BUILD || version2.startsWith("0.0.0");
10225
+ }
10226
+ function registryBase() {
10227
+ const r = process.env.CODEX_TRACKER_REGISTRY || process.env.npm_config_registry || process.env.NPM_CONFIG_REGISTRY || "https://registry.npmjs.org";
10228
+ return r.replace(/\/+$/, "");
10229
+ }
10230
+ async function fetchLatestVersion(signal) {
10231
+ const ctrl = new AbortController();
10232
+ const timer = setTimeout(() => ctrl.abort(), REQUEST_TIMEOUT_MS);
10233
+ signal?.addEventListener("abort", () => ctrl.abort(), { once: true });
10234
+ try {
10235
+ const res = await fetch(`${registryBase()}/-/package/${NPM_PACKAGE}/dist-tags`, {
10236
+ signal: ctrl.signal,
10237
+ headers: { accept: "application/json" }
10238
+ });
10239
+ if (!res.ok) throw new Error(`registry responded ${res.status}`);
10240
+ const json = await res.json();
10241
+ const latest = json?.latest;
10242
+ if (typeof latest !== "string" || !latest) throw new Error("registry returned no `latest` tag");
10243
+ return latest;
10244
+ } finally {
10245
+ clearTimeout(timer);
10246
+ }
10247
+ }
10248
+ function detectPackageManager(installDir = import_node_path11.default.resolve(__dirname, "..")) {
10249
+ const p = installDir.replace(/\\/g, "/").toLowerCase();
10250
+ if (/\/\.?bun\//.test(p)) return "bun";
10251
+ if (/\/\.?pnpm[/-]/.test(p)) return "pnpm";
10252
+ if (/\/\.?yarn\//.test(p)) return "yarn";
10253
+ const ua = (process.env.npm_config_user_agent ?? "").toLowerCase();
10254
+ if (ua.startsWith("pnpm")) return "pnpm";
10255
+ if (ua.startsWith("yarn")) return "yarn";
10256
+ if (ua.startsWith("bun")) return "bun";
10257
+ return "npm";
10258
+ }
10259
+ function updateArgs(pm, spec = `${NPM_PACKAGE}@latest`) {
10260
+ switch (pm) {
10261
+ case "pnpm":
10262
+ return ["add", "-g", spec];
10263
+ case "yarn":
10264
+ return ["global", "add", spec];
10265
+ case "bun":
10266
+ return ["add", "-g", spec];
10267
+ default:
10268
+ return ["install", "-g", spec];
10269
+ }
10270
+ }
10271
+ function updateCommand(pm, spec) {
10272
+ return `${pm} ${updateArgs(pm, spec).join(" ")}`;
10273
+ }
10274
+ function cachePath() {
10275
+ return import_node_path11.default.join(configDir(), "update.json");
10276
+ }
10277
+ function readCache() {
10278
+ try {
10279
+ const raw = JSON.parse(import_node_fs9.default.readFileSync(cachePath(), "utf8"));
10280
+ if (typeof raw?.latest === "string" && typeof raw.checkedAt === "number") {
10281
+ return { latest: raw.latest, checkedAt: raw.checkedAt };
10282
+ }
10283
+ } catch {
10284
+ }
10285
+ return null;
10286
+ }
10287
+ function writeCache(c) {
10288
+ try {
10289
+ import_node_fs9.default.mkdirSync(import_node_path11.default.dirname(cachePath()), { recursive: true });
10290
+ import_node_fs9.default.writeFileSync(cachePath(), JSON.stringify(c, null, 2));
10291
+ } catch {
10292
+ }
10293
+ }
10294
+ function info(current, latest, checkedAt, error) {
10295
+ const pm = detectPackageManager();
10296
+ return {
10297
+ current,
10298
+ latest,
10299
+ available: Boolean(latest) && !isDevBuild(current) && compareVersions(latest, current) > 0,
10300
+ checkedAt,
10301
+ error,
10302
+ packageManager: pm,
10303
+ command: updateCommand(pm)
10304
+ };
10305
+ }
10306
+ async function checkForUpdate(opts = {}) {
10307
+ const current = opts.current ?? APP_VERSION;
10308
+ const cached = readCache();
10309
+ if (!opts.force && cached && Date.now() - cached.checkedAt < CHECK_TTL_MS) {
10310
+ return info(current, cached.latest, cached.checkedAt, null);
10311
+ }
10312
+ try {
10313
+ const latest = await fetchLatestVersion(opts.signal);
10314
+ const checkedAt = Date.now();
10315
+ writeCache({ latest, checkedAt });
10316
+ return info(current, latest, checkedAt, null);
10317
+ } catch (err) {
10318
+ return info(current, cached?.latest ?? null, cached?.checkedAt ?? null, err.message);
10319
+ }
10320
+ }
10321
+ function runUpdate(opts = {}) {
10322
+ const pm = detectPackageManager();
10323
+ const spec = opts.version ? `${NPM_PACKAGE}@${opts.version}` : `${NPM_PACKAGE}@latest`;
10324
+ const args = updateArgs(pm, spec);
10325
+ const command = `${pm} ${args.join(" ")}`;
10326
+ return new Promise((resolve) => {
10327
+ let output = "";
10328
+ const collect = (chunk) => {
10329
+ const text = chunk.toString();
10330
+ opts.onOutput?.(text);
10331
+ output = (output + text).slice(-LOG_TAIL_CHARS);
10332
+ };
10333
+ let child;
10334
+ try {
10335
+ child = (0, import_node_child_process4.spawn)(pm, args, {
10336
+ // Electron's bundled Node has no shell PATH resolution for `npm.cmd` on Windows.
10337
+ shell: process.platform === "win32",
10338
+ env: { ...process.env, ELECTRON_RUN_AS_NODE: void 0 },
10339
+ windowsHide: true
10340
+ });
10341
+ } catch (err) {
10342
+ resolve({ ok: false, code: null, command, output: err.message });
10343
+ return;
10344
+ }
10345
+ const timer = setTimeout(() => child.kill(), INSTALL_TIMEOUT_MS);
10346
+ child.stdout?.on("data", collect);
10347
+ child.stderr?.on("data", collect);
10348
+ child.on("error", (err) => {
10349
+ clearTimeout(timer);
10350
+ resolve({ ok: false, code: null, command, output: output + err.message });
10351
+ });
10352
+ child.on("close", (code2) => {
10353
+ clearTimeout(timer);
10354
+ resolve({ ok: code2 === 0, code: code2, command, output });
10355
+ });
10356
+ });
10357
+ }
10015
10358
 
10016
10359
  // src/core/engine.ts
10360
+ init_version();
10017
10361
  var SHALLOW_MS = 3e3;
10018
10362
  var DEEP_MS = 6e4;
10019
10363
  var TICK_MS = 2e3;
10020
10364
  var REMOTE_MS = 6e4;
10021
10365
  var LIVE_LIMITS_DEBOUNCE_MS = 1e4;
10022
10366
  var LIVE_LIMITS_MIN_GAP_MS = 2e4;
10367
+ var UPDATE_CHECK_MS = 6 * 60 * 60 * 1e3;
10368
+ var SYNC_BANNER_MS = 25e3;
10023
10369
  var Engine = class extends import_node_events.EventEmitter {
10024
10370
  constructor(opts) {
10025
10371
  super();
@@ -10056,6 +10402,16 @@ var Engine = class extends import_node_events.EventEmitter {
10056
10402
  liveLimitsInFlight = false;
10057
10403
  liveLimitsTimer = null;
10058
10404
  lastLiveLimitsAttempt = 0;
10405
+ update = null;
10406
+ updateStatus = "idle";
10407
+ updateLog = null;
10408
+ syncStatus = "idle";
10409
+ syncPhase = null;
10410
+ syncStartedAt = null;
10411
+ syncFinishedAt = null;
10412
+ syncError = null;
10413
+ syncResult = null;
10414
+ syncBannerTimer = null;
10059
10415
  get heatmapWeeks() {
10060
10416
  return this.opts.heatmapWeeks ?? 16;
10061
10417
  }
@@ -10077,6 +10433,8 @@ var Engine = class extends import_node_events.EventEmitter {
10077
10433
  return;
10078
10434
  }
10079
10435
  void this.refreshLiveLimits(true);
10436
+ void this.checkUpdate(false);
10437
+ this.timers.push(setInterval(() => void this.checkUpdate(false), UPDATE_CHECK_MS));
10080
10438
  this.store.startWatching(() => void this.refresh(false));
10081
10439
  this.timers.push(setInterval(() => void this.refresh(false), SHALLOW_MS));
10082
10440
  this.timers.push(setInterval(() => void this.refresh(true), DEEP_MS));
@@ -10104,6 +10462,8 @@ var Engine = class extends import_node_events.EventEmitter {
10104
10462
  this.timers = [];
10105
10463
  if (this.liveLimitsTimer) clearTimeout(this.liveLimitsTimer);
10106
10464
  this.liveLimitsTimer = null;
10465
+ if (this.syncBannerTimer) clearTimeout(this.syncBannerTimer);
10466
+ this.syncBannerTimer = null;
10107
10467
  this.store.stopWatching();
10108
10468
  this.pending?.abort.abort();
10109
10469
  }
@@ -10183,7 +10543,7 @@ var Engine = class extends import_node_events.EventEmitter {
10183
10543
  if (!this.signedIn) return { buckets: 0, sessions: 0 };
10184
10544
  if (!this.stats) this.recompute();
10185
10545
  try {
10186
- const r = await this.uploader.pushAll(this.stats.buckets, this.store.sessions(), this.stats.sessionCosts);
10546
+ const r = await this.uploader.pushAll(this.stats.buckets, this.stats.sessions, this.stats.sessionCosts);
10187
10547
  this.emitSnapshot();
10188
10548
  return r;
10189
10549
  } catch (err) {
@@ -10207,6 +10567,154 @@ var Engine = class extends import_node_events.EventEmitter {
10207
10567
  await this.uploader.fetchRemote(this.heatmapWeeks);
10208
10568
  this.recompute();
10209
10569
  }
10570
+ /**
10571
+ * Full sync ("calibrate this device"). Unlike the periodic incremental upload this deliberately
10572
+ * throws away every cache on the way:
10573
+ * 1. re-read the config so sources enabled since start-up are picked up,
10574
+ * 2. drop the parsed-file index and re-discover + re-parse every transcript of every agent
10575
+ * (Codex plus the agents running on the Codex subscription: pi, OpenCode, Cline/Roo/Kilo,
10576
+ * Hermes and any custom `extraSessionDirs`),
10577
+ * 3. recompute the aggregates with the current pricing table,
10578
+ * 4. re-upload *everything* — not just what changed — so the dashboard's totals for this device
10579
+ * are replaced by the freshly computed ones,
10580
+ * 5. pull the other devices' rows and the live rate limits back down.
10581
+ *
10582
+ * Returns null when a sync is already running. Never throws: failures land in the returned state.
10583
+ */
10584
+ async syncNow() {
10585
+ if (this.syncStatus === "running") return null;
10586
+ if (this.syncBannerTimer) {
10587
+ clearTimeout(this.syncBannerTimer);
10588
+ this.syncBannerTimer = null;
10589
+ }
10590
+ const startedAt = Date.now();
10591
+ this.syncStatus = "running";
10592
+ this.syncPhase = null;
10593
+ this.syncStartedAt = startedAt;
10594
+ this.syncFinishedAt = null;
10595
+ this.syncError = null;
10596
+ const phase = (p) => {
10597
+ this.syncPhase = p;
10598
+ this.emitSnapshot();
10599
+ };
10600
+ try {
10601
+ phase("scanning");
10602
+ this.reloadConfig();
10603
+ this.store.reset();
10604
+ await this.store.refreshDeep();
10605
+ phase("computing");
10606
+ this.recompute();
10607
+ let uploadedBuckets = 0;
10608
+ let uploadedSessions = 0;
10609
+ const uploaded = this.opts.upload && this.signedIn;
10610
+ if (uploaded) {
10611
+ phase("uploading");
10612
+ const r = await this.uploader.pushAll(this.stats.buckets, this.stats.sessions, this.stats.sessionCosts, { full: true });
10613
+ uploadedBuckets = r.buckets;
10614
+ uploadedSessions = r.sessions;
10615
+ await this.heartbeatNow().catch(() => {
10616
+ });
10617
+ phase("downloading");
10618
+ await this.fetchRemoteNow();
10619
+ }
10620
+ phase("limits");
10621
+ await this.refreshLiveLimits(true);
10622
+ const finishedAt = Date.now();
10623
+ this.syncResult = {
10624
+ startedAt,
10625
+ finishedAt,
10626
+ durationMs: finishedAt - startedAt,
10627
+ files: this.store.fileCount,
10628
+ sessions: this.store.sessions().length,
10629
+ roots: this.store.roots.length,
10630
+ agents: [...new Set(this.store.roots.map((r) => r.agent))].sort(),
10631
+ uploadedBuckets,
10632
+ uploadedSessions,
10633
+ uploaded
10634
+ };
10635
+ this.syncStatus = "done";
10636
+ this.syncFinishedAt = finishedAt;
10637
+ this.opts.log?.(
10638
+ `sync: ${this.syncResult.files} files, ${this.syncResult.sessions} sessions, ${uploadedBuckets} buckets / ${uploadedSessions} sessions uploaded in ${this.syncResult.durationMs} ms`
10639
+ );
10640
+ return this.syncResult;
10641
+ } catch (err) {
10642
+ this.syncStatus = "error";
10643
+ this.syncFinishedAt = Date.now();
10644
+ this.syncError = err instanceof SignedOutError ? "not signed in" : errorMessage(err);
10645
+ this.opts.log?.(`sync failed: ${this.syncError}`);
10646
+ return null;
10647
+ } finally {
10648
+ this.syncPhase = null;
10649
+ this.emitSnapshot();
10650
+ this.scheduleSyncBannerClear();
10651
+ }
10652
+ }
10653
+ /** Return the sync banner to "idle" a little after it finished, keeping `last` for the footer. */
10654
+ scheduleSyncBannerClear() {
10655
+ if (this.opts.watch === false) return;
10656
+ if (this.syncBannerTimer) clearTimeout(this.syncBannerTimer);
10657
+ this.syncBannerTimer = setTimeout(() => {
10658
+ this.syncBannerTimer = null;
10659
+ if (this.syncStatus === "done" || this.syncStatus === "error") {
10660
+ this.syncStatus = "idle";
10661
+ this.emitSnapshot();
10662
+ }
10663
+ }, SYNC_BANNER_MS);
10664
+ this.syncBannerTimer.unref?.();
10665
+ }
10666
+ syncState() {
10667
+ return {
10668
+ status: this.syncStatus,
10669
+ phase: this.syncPhase,
10670
+ startedAt: this.syncStartedAt,
10671
+ finishedAt: this.syncFinishedAt,
10672
+ error: this.syncError,
10673
+ last: this.syncResult
10674
+ };
10675
+ }
10676
+ /**
10677
+ * Ask the npm registry for the newest published version (cached for 6 h unless `force`).
10678
+ * Best-effort: a failure is recorded on the snapshot, never thrown.
10679
+ */
10680
+ async checkUpdate(force) {
10681
+ if (!this.config.checkUpdates) {
10682
+ this.update = null;
10683
+ return null;
10684
+ }
10685
+ if (this.updateStatus === "checking" || this.updateStatus === "installing") return this.update;
10686
+ this.updateStatus = "checking";
10687
+ this.emitSnapshot();
10688
+ this.update = await checkForUpdate({ force });
10689
+ this.updateStatus = "idle";
10690
+ if (this.update.error) this.opts.log?.(`update check failed: ${this.update.error}`);
10691
+ this.emitSnapshot();
10692
+ return this.update;
10693
+ }
10694
+ /**
10695
+ * Install the newest version globally with the package manager this copy came from.
10696
+ * The new code only takes effect once the app is restarted, so the UI says so rather than
10697
+ * pretending to hot-swap itself.
10698
+ */
10699
+ async installUpdate() {
10700
+ if (this.updateStatus === "installing") return false;
10701
+ if (!this.update?.available) await this.checkUpdate(true);
10702
+ if (!this.update?.available) return false;
10703
+ this.updateStatus = "installing";
10704
+ this.updateLog = null;
10705
+ this.emitSnapshot();
10706
+ const r = await runUpdate({ version: this.update.latest ?? void 0 });
10707
+ this.updateStatus = r.ok ? "installed" : "failed";
10708
+ this.updateLog = r.ok ? null : `${r.command}
10709
+ ${r.output}`.trim();
10710
+ if (!r.ok) this.opts.log?.(`update install failed (${r.code}): ${r.output}`);
10711
+ this.emitSnapshot();
10712
+ return r.ok;
10713
+ }
10714
+ updateState() {
10715
+ if (!this.config.checkUpdates || !this.update) return null;
10716
+ return { ...this.update, status: this.updateStatus, log: this.updateLog };
10717
+ }
10210
10718
  /** Start the device-code login flow (resolves when approved / denied / expired / cancelled). */
10211
10719
  async login(openBrowser, onCode) {
10212
10720
  if (this.pending) return { status: "cancelled" };
@@ -10282,6 +10790,7 @@ var Engine = class extends import_node_events.EventEmitter {
10282
10790
  const sessions = this.store.sessions();
10283
10791
  return {
10284
10792
  version: APP_VERSION,
10793
+ channel: APP_CHANNEL,
10285
10794
  generatedAt: Date.now(),
10286
10795
  language: this.language(),
10287
10796
  languageSetting: this.config.language,
@@ -10299,6 +10808,8 @@ var Engine = class extends import_node_events.EventEmitter {
10299
10808
  rateLimits: this.liveLimits ?? fromLogRateLimits(s.logRateLimits),
10300
10809
  rateLimitsError: this.config.liveRateLimits ? this.liveLimitsError : null,
10301
10810
  rateLimitsUpdatedAt: this.liveLimits ? this.liveLimitsAt : s.logRateLimits?.observedAt ?? null,
10811
+ update: this.updateState(),
10812
+ sync: this.syncState(),
10302
10813
  modelsToday: s.modelsToday,
10303
10814
  modelsMonth: s.modelsMonth,
10304
10815
  byAgentToday: s.byAgentToday,
@@ -10315,6 +10826,7 @@ var Engine = class extends import_node_events.EventEmitter {
10315
10826
  },
10316
10827
  sessionDirs: SessionStore.rootDirs(this.store.roots),
10317
10828
  sessionRoots: this.store.roots.map((r) => ({ dir: r.dir, agent: r.agent, format: r.format, origin: r.origin })),
10829
+ configDir: configDir(),
10318
10830
  launchAtLogin: this.config.launchAtLogin,
10319
10831
  trayTitle: this.config.trayTitle
10320
10832
  };
@@ -10326,6 +10838,7 @@ init_platform();
10326
10838
  init_config();
10327
10839
  init_uploader();
10328
10840
  init_i18n();
10841
+ init_version();
10329
10842
  function parseArgs2(argv) {
10330
10843
  const out = { command: null, positional: [], flags: {} };
10331
10844
  for (let i2 = 0; i2 < argv.length; i2++) {
@@ -10357,14 +10870,14 @@ function applyDashboardFlag(flags) {
10357
10870
  function electronBinary() {
10358
10871
  try {
10359
10872
  const p = require("electron");
10360
- return typeof p === "string" && p && import_node_fs9.default.existsSync(p) ? p : null;
10873
+ return typeof p === "string" && p && import_node_fs10.default.existsSync(p) ? p : null;
10361
10874
  } catch {
10362
10875
  return null;
10363
10876
  }
10364
10877
  }
10365
10878
  function electronPackageDir() {
10366
10879
  try {
10367
- return import_node_path11.default.dirname(require.resolve("electron/package.json"));
10880
+ return import_node_path12.default.dirname(require.resolve("electron/package.json"));
10368
10881
  } catch {
10369
10882
  return null;
10370
10883
  }
@@ -10384,11 +10897,11 @@ async function ensureElectron() {
10384
10897
  log2.error(` ${err.message}`);
10385
10898
  }
10386
10899
  if (!bin) {
10387
- const installer = import_node_path11.default.join(dir, "install.js");
10388
- if (import_node_fs9.default.existsSync(installer)) {
10900
+ const installer = import_node_path12.default.join(dir, "install.js");
10901
+ if (import_node_fs10.default.existsSync(installer)) {
10389
10902
  const env = { ...process.env };
10390
10903
  delete env.ELECTRON_SKIP_BINARY_DOWNLOAD;
10391
- (0, import_node_child_process4.spawnSync)(process.execPath, [installer], { stdio: "inherit", cwd: dir, env });
10904
+ (0, import_node_child_process5.spawnSync)(process.execPath, [installer], { stdio: "inherit", cwd: dir, env });
10392
10905
  }
10393
10906
  }
10394
10907
  try {
@@ -10408,9 +10921,9 @@ async function startMenubar(background) {
10408
10921
  }
10409
10922
  const env = { ...process.env, CODEX_TRACKER_HOME: configDir() };
10410
10923
  delete env.ELECTRON_RUN_AS_NODE;
10411
- const mainPath = import_node_path11.default.join(__dirname, "main.js");
10924
+ const mainPath = import_node_path12.default.join(__dirname, "main.js");
10412
10925
  console.log(t2("cliStartingMenubar"));
10413
- const child = (0, import_node_child_process4.spawn)(bin, [mainPath], {
10926
+ const child = (0, import_node_child_process5.spawn)(bin, [mainPath], {
10414
10927
  stdio: background ? "ignore" : "inherit",
10415
10928
  detached: background,
10416
10929
  env,
@@ -10491,8 +11004,43 @@ async function runStatus(json) {
10491
11004
  s.auth.status === "signedIn" ? `${t2("signedInAs", { name: s.auth.user?.name || s.auth.user?.email || "?" })} \xB7 ${s.auth.dashboardUrl}` : t2("cliNotSignedIn")
10492
11005
  );
10493
11006
  line(t2("cliStatusDirs"), s.sessionDirs.join("\n" + " ".repeat(15)) || "-");
11007
+ if (IS_DEV_BUILD) console.log(t2("cliChannelDev", { url: s.auth.dashboardUrl, dir: configDir() }));
10494
11008
  const byAgent = Object.entries(s.counts.byAgent).sort((a, b) => b[1].sessions - a[1].sessions).map(([agent, c]) => `${agent} ${c.sessions}`).join(", ");
10495
11009
  console.log(t2("sessions", { n: s.counts.sessions }) + " \xB7 " + t2("files", { n: s.counts.files }) + (byAgent ? ` (${byAgent})` : ""));
11010
+ if (cfg.checkUpdates) {
11011
+ const u = await checkForUpdate();
11012
+ if (u.available) console.log("\n" + t2("cliUpdateAvailable", { current: u.current, latest: u.latest ?? "?" }) + ` \u2014 ${u.command}`);
11013
+ }
11014
+ return 0;
11015
+ }
11016
+ async function runSync(flags) {
11017
+ const cfg = applyDashboardFlag(flags);
11018
+ const L = lang(cfg);
11019
+ const t2 = makeT(L);
11020
+ const engine = new Engine({
11021
+ upload: true,
11022
+ watch: false,
11023
+ systemLocale: systemLocale(),
11024
+ log: process.env.CODEX_TRACKER_DEBUG ? (m) => console.error("[sync]", m) : void 0
11025
+ });
11026
+ console.log(t2("cliSyncStart"));
11027
+ const result = await engine.syncNow();
11028
+ if (!result) {
11029
+ console.error(t2("cliSyncFailed", { message: engine.snapshot().sync.error ?? "?" }));
11030
+ return 1;
11031
+ }
11032
+ console.log(t2("cliSyncScanned", { files: result.files, roots: result.roots, agents: result.agents.join(", ") || "-" }));
11033
+ if (result.uploaded) console.log(t2("cliSyncUploaded", { buckets: result.uploadedBuckets, sessions: result.uploadedSessions }));
11034
+ else console.log(t2("cliSyncLocal"));
11035
+ const s = engine.snapshot();
11036
+ console.log(
11037
+ t2("cliSyncDone", {
11038
+ seconds: (result.durationMs / 1e3).toFixed(1),
11039
+ sessions: result.sessions,
11040
+ tokens: formatTokens(s.today.usage.total),
11041
+ cost: formatUSD(s.today.cost)
11042
+ })
11043
+ );
10496
11044
  return 0;
10497
11045
  }
10498
11046
  async function runLogin(flags) {
@@ -10598,6 +11146,7 @@ function runPaths() {
10598
11146
  const off = SOURCE_IDS.filter((id) => !cfg.sources[id]);
10599
11147
  if (off.length) console.log(` (disabled: ${off.join(", ")})`);
10600
11148
  console.log(t2("cliConfigDir", { dir: configDir() }));
11149
+ if (IS_DEV_BUILD) console.log(t2("cliChannelDev", { url: cfg.dashboardUrl, dir: configDir() }));
10601
11150
  return 0;
10602
11151
  }
10603
11152
  function runConfig(positional) {
@@ -10642,6 +11191,35 @@ function runConfig(positional) {
10642
11191
  console.log(t2("cliConfigDir", { dir: configDir() }));
10643
11192
  return 0;
10644
11193
  }
11194
+ async function runUpdateCommand(flags) {
11195
+ const t2 = makeT(lang());
11196
+ if (IS_DEV_BUILD) {
11197
+ console.error(t2("cliUpdateDevBuild"));
11198
+ return 1;
11199
+ }
11200
+ const info2 = await checkForUpdate({ force: true });
11201
+ if (info2.error && !info2.latest) {
11202
+ console.error(t2("cliUpdateCheckFailed", { message: info2.error }));
11203
+ return 1;
11204
+ }
11205
+ if (!info2.available) {
11206
+ console.log(t2("cliUpdateLatest", { version: info2.current }));
11207
+ return 0;
11208
+ }
11209
+ console.log(t2("cliUpdateAvailable", { current: info2.current, latest: info2.latest ?? "?" }));
11210
+ if (flags.check === true) {
11211
+ console.log(` ${info2.command}`);
11212
+ return 0;
11213
+ }
11214
+ console.log(t2("cliUpdateRunning", { command: info2.command }));
11215
+ const r = await runUpdate({ version: info2.latest ?? void 0, onOutput: (c) => process.stdout.write(c) });
11216
+ if (!r.ok) {
11217
+ console.error(t2("cliUpdateFailed", { code: r.code ?? "?", command: r.command }));
11218
+ return 1;
11219
+ }
11220
+ console.log(t2("cliUpdateDone", { version: info2.latest ?? "?" }));
11221
+ return 0;
11222
+ }
10645
11223
  function runLang(positional) {
10646
11224
  const value = positional[0];
10647
11225
  if (!value || !["en", "zh", "auto"].includes(value)) {
@@ -10657,6 +11235,7 @@ async function main() {
10657
11235
  const t2 = makeT(lang());
10658
11236
  if (args.flags.version) {
10659
11237
  console.log(t2("cliVersion", { version: APP_VERSION }));
11238
+ if (IS_DEV_BUILD) console.log(t2("cliChannelDev", { url: loadConfig().dashboardUrl, dir: configDir() }));
10660
11239
  return 0;
10661
11240
  }
10662
11241
  if (args.flags.help || args.command === "help") {
@@ -10693,12 +11272,17 @@ async function main() {
10693
11272
  }
10694
11273
  case "status":
10695
11274
  return runStatus(args.flags.json === true);
11275
+ case "sync":
11276
+ return runSync(args.flags);
10696
11277
  case "paths":
10697
11278
  return runPaths();
10698
11279
  case "config":
10699
11280
  return runConfig(args.positional);
10700
11281
  case "lang":
10701
11282
  return runLang(args.positional);
11283
+ case "update":
11284
+ case "upgrade":
11285
+ return runUpdateCommand(args.flags);
10702
11286
  default:
10703
11287
  console.error(t2("cliUnknownCommand", { command: args.command }));
10704
11288
  console.log(t2("cliUsage"));