codex-token-tracker 0.2.0 → 0.2.2

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
@@ -1138,9 +1138,20 @@ var init_src = __esm({
1138
1138
  }
1139
1139
  });
1140
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.2" : "0.0.0-dev";
1147
+ APP_CHANNEL = true ? "prod" : "dev";
1148
+ IS_DEV_BUILD = APP_CHANNEL === "dev";
1149
+ }
1150
+ });
1151
+
1141
1152
  // src/core/config.ts
1142
1153
  function configDir() {
1143
- 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");
1144
1155
  }
1145
1156
  function readJson(file, fallback) {
1146
1157
  try {
@@ -1193,7 +1204,7 @@ function loadConfig() {
1193
1204
  cfg.sources = normalizeSources(stored.sources);
1194
1205
  cfg.trackAllProviders = stored.trackAllProviders === true;
1195
1206
  cfg.liveRateLimits = stored.liveRateLimits !== false;
1196
- cfg.checkUpdates = stored.checkUpdates !== false;
1207
+ cfg.checkUpdates = typeof stored.checkUpdates === "boolean" ? stored.checkUpdates : DEFAULT_CONFIG.checkUpdates;
1197
1208
  if (!(cfg.usageRefreshSec >= 15)) cfg.usageRefreshSec = DEFAULT_CONFIG.usageRefreshSec;
1198
1209
  if (!["auto", "en", "zh"].includes(cfg.language)) cfg.language = "auto";
1199
1210
  if (!(cfg.uploadIntervalSec >= 10)) cfg.uploadIntervalSec = DEFAULT_CONFIG.uploadIntervalSec;
@@ -1300,14 +1311,17 @@ function loadPricingOverrides() {
1300
1311
  }
1301
1312
  return Object.keys(out).length ? out : void 0;
1302
1313
  }
1303
- 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;
1304
1315
  var init_config = __esm({
1305
1316
  "src/core/config.ts"() {
1306
1317
  "use strict";
1307
1318
  import_node_fs2 = __toESM(require("node:fs"));
1308
1319
  import_node_os2 = __toESM(require("node:os"));
1309
1320
  import_node_path2 = __toESM(require("node:path"));
1310
- 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;
1311
1325
  SOURCE_FORMATS = ["codex", "pi", "generic", "opencode", "cline"];
1312
1326
  DEFAULT_SOURCES = { codex: true, pi: true, hermes: true, opencode: true, cline: true, roo: true, kilo: true };
1313
1327
  SOURCE_IDS = Object.keys(DEFAULT_SOURCES);
@@ -1327,7 +1341,8 @@ var init_config = __esm({
1327
1341
  trackAllProviders: false,
1328
1342
  liveRateLimits: true,
1329
1343
  usageRefreshSec: 60,
1330
- checkUpdates: true
1344
+ // a dev build must never offer to replace itself with the published package
1345
+ checkUpdates: !IS_DEV_BUILD
1331
1346
  };
1332
1347
  EDITABLE_KEYS = [
1333
1348
  "dashboardUrl",
@@ -1397,6 +1412,7 @@ var init_en = __esm({
1397
1412
  estimatedPricingHint: "Pricing estimated: model not in the price table",
1398
1413
  other: "Other",
1399
1414
  lastUpload: "Last upload {time}",
1415
+ lastSync: "Last full sync {time}",
1400
1416
  neverUploaded: "Not uploaded yet",
1401
1417
  uploadError: "Upload error: {message}",
1402
1418
  allDevicesToday: "All devices today",
@@ -1415,10 +1431,26 @@ var init_en = __esm({
1415
1431
  launchAtLogin: "Launch at login",
1416
1432
  refresh: "Refresh",
1417
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}",
1418
1446
  sessions: "{n} sessions",
1419
1447
  files: "{n} files",
1420
1448
  noData: "No usage found yet. Run Codex and this will fill in.",
1421
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}",
1422
1454
  justNow: "just now",
1423
1455
  never: "never",
1424
1456
  local: "Local",
@@ -1446,6 +1478,7 @@ Commands:
1446
1478
  login Connect this device to your dashboard (--dashboard <url>)
1447
1479
  logout Disconnect this device
1448
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)
1449
1482
  paths Show detected session directories (Codex, pi, OpenCode, Cline/Roo/Kilo, Hermes, custom)
1450
1483
  update Install the newest published version (--check only reports)
1451
1484
  config get|set Read or change settings (config set uploadIntervalSec 30, config set sources.pi false)
@@ -1460,9 +1493,9 @@ Options:
1460
1493
  cliVersion: "codex-token-tracker {version}",
1461
1494
  cliUnknownCommand: "Unknown command: {command}",
1462
1495
  cliNoDisplay: "No display detected \u2014 starting agent mode (run `codex-tracker menubar` on a desktop).",
1463
- cliNoElectron: "Electron is not installed (optional dependency) \u2014 starting agent mode.",
1496
+ cliNoElectron: "No Electron runtime available \u2014 starting agent mode.",
1464
1497
  cliDownloadingElectron: "Electron runtime not found \u2014 downloading it now (about 100 MB, one time)\u2026",
1465
- cliElectronDownloadFailed: "Electron download failed. Retry with `npm install -g codex-token-tracker --allow-scripts=electron` or `npm rebuild -g electron`; the headless `codex-tracker agent` works without it.",
1498
+ cliElectronDownloadFailed: "Electron download failed. Check the network (set ELECTRON_MIRROR, e.g. https://npmmirror.com/mirrors/electron/, or HTTPS_PROXY) and run `codex-tracker menubar` again; the headless `codex-tracker agent` works without Electron.",
1466
1499
  cliStartingMenubar: "Starting menu bar app\u2026",
1467
1500
  cliLoginStart: "Connecting this device to {dashboard}",
1468
1501
  cliLoginCode: "Your code: {code}",
@@ -1500,7 +1533,15 @@ Options:
1500
1533
  cliUpdateRunning: "Running: {command}",
1501
1534
  cliUpdateDone: "Updated to {version}. Restart the menu bar app (or rerun the CLI) to use it.",
1502
1535
  cliUpdateFailed: "Update failed (exit {code}). Run it yourself: {command}",
1503
- cliUpdateCheckFailed: "Could not reach the npm registry: {message}"
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."
1504
1545
  };
1505
1546
  }
1506
1547
  });
@@ -1554,6 +1595,7 @@ var init_zh = __esm({
1554
1595
  estimatedPricingHint: "\u4EF7\u683C\u4E3A\u4F30\u7B97\u503C\uFF1A\u8BE5\u6A21\u578B\u4E0D\u5728\u4EF7\u76EE\u8868\u4E2D",
1555
1596
  other: "\u5176\u4ED6",
1556
1597
  lastUpload: "\u4E0A\u6B21\u4E0A\u4F20 {time}",
1598
+ lastSync: "\u4E0A\u6B21\u5B8C\u6574\u540C\u6B65 {time}",
1557
1599
  neverUploaded: "\u5C1A\u672A\u4E0A\u4F20",
1558
1600
  uploadError: "\u4E0A\u4F20\u5931\u8D25\uFF1A{message}",
1559
1601
  allDevicesToday: "\u6240\u6709\u8BBE\u5907\u4ECA\u65E5",
@@ -1572,10 +1614,26 @@ var init_zh = __esm({
1572
1614
  launchAtLogin: "\u5F00\u673A\u81EA\u542F\u52A8",
1573
1615
  refresh: "\u5237\u65B0",
1574
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}",
1575
1629
  sessions: "{n} \u4E2A\u4F1A\u8BDD",
1576
1630
  files: "{n} \u4E2A\u6587\u4EF6",
1577
1631
  noData: "\u8FD8\u6CA1\u6709\u7528\u91CF\u6570\u636E\u3002\u8FD0\u884C Codex \u540E\u8FD9\u91CC\u4F1A\u81EA\u52A8\u66F4\u65B0\u3002",
1578
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}",
1579
1637
  justNow: "\u521A\u521A",
1580
1638
  never: "\u4ECE\u672A",
1581
1639
  local: "\u672C\u673A",
@@ -1601,6 +1659,7 @@ var init_zh = __esm({
1601
1659
  login \u5C06\u672C\u8BBE\u5907\u8FDE\u63A5\u5230\u4EEA\u8868\u76D8\uFF08--dashboard <url>\uFF09
1602
1660
  logout \u65AD\u5F00\u672C\u8BBE\u5907
1603
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
1604
1663
  paths \u663E\u793A\u68C0\u6D4B\u5230\u7684\u4F1A\u8BDD\u76EE\u5F55\uFF08Codex\u3001pi\u3001OpenCode\u3001Cline/Roo/Kilo\u3001Hermes\u3001\u81EA\u5B9A\u4E49\uFF09
1605
1664
  update \u5B89\u88C5\u6700\u65B0\u53D1\u5E03\u7248\u672C\uFF08--check \u53EA\u68C0\u67E5\u4E0D\u5B89\u88C5\uFF09
1606
1665
  config get|set \u8BFB\u53D6\u6216\u4FEE\u6539\u8BBE\u7F6E\uFF08config set uploadIntervalSec 30\uFF0Cconfig set sources.pi false\uFF09
@@ -1615,9 +1674,9 @@ var init_zh = __esm({
1615
1674
  cliVersion: "codex-token-tracker {version}",
1616
1675
  cliUnknownCommand: "\u672A\u77E5\u547D\u4EE4\uFF1A{command}",
1617
1676
  cliNoDisplay: "\u672A\u68C0\u6D4B\u5230\u663E\u793A\u73AF\u5883 \u2014 \u4EE5 agent \u6A21\u5F0F\u542F\u52A8\uFF08\u684C\u9762\u73AF\u5883\u8BF7\u8FD0\u884C `codex-tracker menubar`\uFF09\u3002",
1618
- cliNoElectron: "\u672A\u5B89\u88C5 Electron\uFF08\u53EF\u9009\u4F9D\u8D56\uFF09\u2014 \u4EE5 agent \u6A21\u5F0F\u542F\u52A8\u3002",
1677
+ cliNoElectron: "\u6CA1\u6709\u53EF\u7528\u7684 Electron \u8FD0\u884C\u65F6 \u2014 \u4EE5 agent \u6A21\u5F0F\u542F\u52A8\u3002",
1619
1678
  cliDownloadingElectron: "\u672A\u627E\u5230 Electron \u8FD0\u884C\u65F6\uFF0C\u6B63\u5728\u4E0B\u8F7D\uFF08\u7EA6 100 MB\uFF0C\u4EC5\u9700\u4E00\u6B21\uFF09\u2026",
1620
- cliElectronDownloadFailed: "Electron \u4E0B\u8F7D\u5931\u8D25\u3002\u53EF\u91CD\u8BD5 `npm install -g codex-token-tracker --allow-scripts=electron` \u6216 `npm rebuild -g electron`\uFF1B\u65E0\u754C\u9762\u7684 `codex-tracker agent` \u65E0\u9700 Electron\u3002",
1679
+ cliElectronDownloadFailed: "Electron \u4E0B\u8F7D\u5931\u8D25\u3002\u8BF7\u68C0\u67E5\u7F51\u7EDC\uFF08\u53EF\u8BBE\u7F6E ELECTRON_MIRROR\uFF0C\u4F8B\u5982 https://npmmirror.com/mirrors/electron/\uFF0C\u6216 HTTPS_PROXY\uFF09\u540E\u518D\u6B21\u8FD0\u884C `codex-tracker menubar`\uFF1B\u65E0\u754C\u9762\u7684 `codex-tracker agent` \u65E0\u9700 Electron\u3002",
1621
1680
  cliStartingMenubar: "\u6B63\u5728\u542F\u52A8\u83DC\u5355\u680F\u5E94\u7528\u2026",
1622
1681
  cliLoginStart: "\u6B63\u5728\u5C06\u672C\u8BBE\u5907\u8FDE\u63A5\u5230 {dashboard}",
1623
1682
  cliLoginCode: "\u4F60\u7684\u4EE3\u7801\uFF1A{code}",
@@ -1655,7 +1714,15 @@ var init_zh = __esm({
1655
1714
  cliUpdateRunning: "\u6B63\u5728\u6267\u884C\uFF1A{command}",
1656
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",
1657
1716
  cliUpdateFailed: "\u66F4\u65B0\u5931\u8D25\uFF08\u9000\u51FA\u7801 {code}\uFF09\u3002\u8BF7\u624B\u52A8\u6267\u884C\uFF1A{command}",
1658
- cliUpdateCheckFailed: "\u65E0\u6CD5\u8BBF\u95EE npm registry\uFF1A{message}"
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"
1659
1726
  };
1660
1727
  }
1661
1728
  });
@@ -8630,11 +8697,25 @@ var init_uploader = __esm({
8630
8697
  throw err;
8631
8698
  }
8632
8699
  }
8633
- /** Upload changed hour buckets and session summaries. Returns counts of pushed items. */
8634
- 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();
8635
8712
  if (this.inFlight) return { buckets: 0, sessions: 0 };
8636
8713
  this.inFlight = true;
8637
8714
  try {
8715
+ if (opts.full) {
8716
+ this.state.pushedBuckets = {};
8717
+ this.state.pushedSessions = {};
8718
+ }
8638
8719
  const changed = [];
8639
8720
  const hashes = /* @__PURE__ */ new Map();
8640
8721
  for (const b of buckets.sort((a, b2) => a.hourStart - b2.hourStart)) {
@@ -8814,6 +8895,7 @@ var import_node_fs = __toESM(require("node:fs"));
8814
8895
  var import_node_os = __toESM(require("node:os"));
8815
8896
  var import_node_path = __toESM(require("node:path"));
8816
8897
  var import_node_child_process = require("node:child_process");
8898
+ var DEFAULT_ELECTRON_VERSION = true ? "38.8.6" : "38.8.6";
8817
8899
  function archName() {
8818
8900
  switch (process.arch) {
8819
8901
  case "x64":
@@ -8955,6 +9037,33 @@ async function installElectronBinary(pkgDir, log2) {
8955
9037
  import_node_fs.default.writeFileSync(import_node_path.default.join(pkgDir, "path.txt"), platformExecutablePath());
8956
9038
  return import_node_fs.default.existsSync(exe) ? exe : null;
8957
9039
  }
9040
+ function managedElectronDir(configHome, version2 = DEFAULT_ELECTRON_VERSION) {
9041
+ return import_node_path.default.join(configHome, "electron", version2);
9042
+ }
9043
+ function ensureManagedElectronDir(configHome, version2 = DEFAULT_ELECTRON_VERSION) {
9044
+ const dir = managedElectronDir(configHome, version2);
9045
+ const manifest = import_node_path.default.join(dir, "package.json");
9046
+ let current = null;
9047
+ try {
9048
+ current = JSON.parse(import_node_fs.default.readFileSync(manifest, "utf8")).version ?? null;
9049
+ } catch {
9050
+ }
9051
+ if (current !== version2) {
9052
+ import_node_fs.default.mkdirSync(dir, { recursive: true });
9053
+ import_node_fs.default.writeFileSync(manifest, JSON.stringify({ name: "electron", version: version2, private: true }, null, 2) + "\n");
9054
+ }
9055
+ return dir;
9056
+ }
9057
+ function binaryFromPackageDir(pkgDir) {
9058
+ try {
9059
+ const rel = import_node_fs.default.readFileSync(import_node_path.default.join(pkgDir, "path.txt"), "utf8").trim();
9060
+ if (!rel) return null;
9061
+ const exe = import_node_path.default.join(pkgDir, "dist", rel);
9062
+ return import_node_fs.default.existsSync(exe) ? exe : null;
9063
+ } catch {
9064
+ return null;
9065
+ }
9066
+ }
8958
9067
 
8959
9068
  // src/cli.ts
8960
9069
  init_src();
@@ -9708,6 +9817,15 @@ var SessionStore = class {
9708
9817
  onChange = null;
9709
9818
  debounceTimer = null;
9710
9819
  sessionCache = null;
9820
+ /**
9821
+ * Forget the parsed-file index so the next deep refresh re-reads and re-parses every transcript.
9822
+ * Used by the full sync: files are otherwise skipped while their size and mtime are unchanged, which
9823
+ * would keep stale numbers around after a parser or pricing change.
9824
+ */
9825
+ reset() {
9826
+ this.files.clear();
9827
+ this.sessionCache = null;
9828
+ }
9711
9829
  async refreshDeep() {
9712
9830
  const o = this.getOptions();
9713
9831
  this.roots = discoverSessionRoots({ extraSessionDirs: o.extraSessionDirs, sources: o.sources });
@@ -10107,12 +10225,8 @@ var import_node_fs9 = __toESM(require("node:fs"));
10107
10225
  var import_node_path11 = __toESM(require("node:path"));
10108
10226
  var import_node_child_process4 = require("node:child_process");
10109
10227
  init_config();
10110
-
10111
- // src/version.ts
10112
- var APP_VERSION = true ? "0.2.0" : "0.0.0-dev";
10113
-
10114
- // src/core/update.ts
10115
- var NPM_PACKAGE = "codex-token-tracker";
10228
+ init_version();
10229
+ var NPM_PACKAGE = true ? "codex-token-tracker" : "codex-token-tracker";
10116
10230
  var CHECK_TTL_MS = 6 * 60 * 60 * 1e3;
10117
10231
  var REQUEST_TIMEOUT_MS = 8e3;
10118
10232
  var INSTALL_TIMEOUT_MS = 10 * 60 * 1e3;
@@ -10135,7 +10249,7 @@ function compareVersions(a, b) {
10135
10249
  return A.pre < B.pre ? -1 : 1;
10136
10250
  }
10137
10251
  function isDevBuild(version2) {
10138
- return version2.startsWith("0.0.0");
10252
+ return IS_DEV_BUILD || version2.startsWith("0.0.0");
10139
10253
  }
10140
10254
  function registryBase() {
10141
10255
  const r = process.env.CODEX_TRACKER_REGISTRY || process.env.npm_config_registry || process.env.NPM_CONFIG_REGISTRY || "https://registry.npmjs.org";
@@ -10186,7 +10300,8 @@ function updateCommand(pm, spec) {
10186
10300
  return `${pm} ${updateArgs(pm, spec).join(" ")}`;
10187
10301
  }
10188
10302
  function cachePath() {
10189
- return import_node_path11.default.join(configDir(), "update.json");
10303
+ const suffix = NPM_PACKAGE === "codex-token-tracker" ? "" : `-${NPM_PACKAGE}`;
10304
+ return import_node_path11.default.join(configDir(), `update${suffix}.json`);
10190
10305
  }
10191
10306
  function readCache() {
10192
10307
  try {
@@ -10271,6 +10386,7 @@ function runUpdate(opts = {}) {
10271
10386
  }
10272
10387
 
10273
10388
  // src/core/engine.ts
10389
+ init_version();
10274
10390
  var SHALLOW_MS = 3e3;
10275
10391
  var DEEP_MS = 6e4;
10276
10392
  var TICK_MS = 2e3;
@@ -10278,6 +10394,7 @@ var REMOTE_MS = 6e4;
10278
10394
  var LIVE_LIMITS_DEBOUNCE_MS = 1e4;
10279
10395
  var LIVE_LIMITS_MIN_GAP_MS = 2e4;
10280
10396
  var UPDATE_CHECK_MS = 6 * 60 * 60 * 1e3;
10397
+ var SYNC_BANNER_MS = 25e3;
10281
10398
  var Engine = class extends import_node_events.EventEmitter {
10282
10399
  constructor(opts) {
10283
10400
  super();
@@ -10317,6 +10434,13 @@ var Engine = class extends import_node_events.EventEmitter {
10317
10434
  update = null;
10318
10435
  updateStatus = "idle";
10319
10436
  updateLog = null;
10437
+ syncStatus = "idle";
10438
+ syncPhase = null;
10439
+ syncStartedAt = null;
10440
+ syncFinishedAt = null;
10441
+ syncError = null;
10442
+ syncResult = null;
10443
+ syncBannerTimer = null;
10320
10444
  get heatmapWeeks() {
10321
10445
  return this.opts.heatmapWeeks ?? 16;
10322
10446
  }
@@ -10367,6 +10491,8 @@ var Engine = class extends import_node_events.EventEmitter {
10367
10491
  this.timers = [];
10368
10492
  if (this.liveLimitsTimer) clearTimeout(this.liveLimitsTimer);
10369
10493
  this.liveLimitsTimer = null;
10494
+ if (this.syncBannerTimer) clearTimeout(this.syncBannerTimer);
10495
+ this.syncBannerTimer = null;
10370
10496
  this.store.stopWatching();
10371
10497
  this.pending?.abort.abort();
10372
10498
  }
@@ -10470,6 +10596,112 @@ var Engine = class extends import_node_events.EventEmitter {
10470
10596
  await this.uploader.fetchRemote(this.heatmapWeeks);
10471
10597
  this.recompute();
10472
10598
  }
10599
+ /**
10600
+ * Full sync ("calibrate this device"). Unlike the periodic incremental upload this deliberately
10601
+ * throws away every cache on the way:
10602
+ * 1. re-read the config so sources enabled since start-up are picked up,
10603
+ * 2. drop the parsed-file index and re-discover + re-parse every transcript of every agent
10604
+ * (Codex plus the agents running on the Codex subscription: pi, OpenCode, Cline/Roo/Kilo,
10605
+ * Hermes and any custom `extraSessionDirs`),
10606
+ * 3. recompute the aggregates with the current pricing table,
10607
+ * 4. re-upload *everything* — not just what changed — so the dashboard's totals for this device
10608
+ * are replaced by the freshly computed ones,
10609
+ * 5. pull the other devices' rows and the live rate limits back down.
10610
+ *
10611
+ * Returns null when a sync is already running. Never throws: failures land in the returned state.
10612
+ */
10613
+ async syncNow() {
10614
+ if (this.syncStatus === "running") return null;
10615
+ if (this.syncBannerTimer) {
10616
+ clearTimeout(this.syncBannerTimer);
10617
+ this.syncBannerTimer = null;
10618
+ }
10619
+ const startedAt = Date.now();
10620
+ this.syncStatus = "running";
10621
+ this.syncPhase = null;
10622
+ this.syncStartedAt = startedAt;
10623
+ this.syncFinishedAt = null;
10624
+ this.syncError = null;
10625
+ const phase = (p) => {
10626
+ this.syncPhase = p;
10627
+ this.emitSnapshot();
10628
+ };
10629
+ try {
10630
+ phase("scanning");
10631
+ this.reloadConfig();
10632
+ this.store.reset();
10633
+ await this.store.refreshDeep();
10634
+ phase("computing");
10635
+ this.recompute();
10636
+ let uploadedBuckets = 0;
10637
+ let uploadedSessions = 0;
10638
+ const uploaded = this.opts.upload && this.signedIn;
10639
+ if (uploaded) {
10640
+ phase("uploading");
10641
+ const r = await this.uploader.pushAll(this.stats.buckets, this.stats.sessions, this.stats.sessionCosts, { full: true });
10642
+ uploadedBuckets = r.buckets;
10643
+ uploadedSessions = r.sessions;
10644
+ await this.heartbeatNow().catch(() => {
10645
+ });
10646
+ phase("downloading");
10647
+ await this.fetchRemoteNow();
10648
+ }
10649
+ phase("limits");
10650
+ await this.refreshLiveLimits(true);
10651
+ const finishedAt = Date.now();
10652
+ this.syncResult = {
10653
+ startedAt,
10654
+ finishedAt,
10655
+ durationMs: finishedAt - startedAt,
10656
+ files: this.store.fileCount,
10657
+ sessions: this.store.sessions().length,
10658
+ roots: this.store.roots.length,
10659
+ agents: [...new Set(this.store.roots.map((r) => r.agent))].sort(),
10660
+ uploadedBuckets,
10661
+ uploadedSessions,
10662
+ uploaded
10663
+ };
10664
+ this.syncStatus = "done";
10665
+ this.syncFinishedAt = finishedAt;
10666
+ this.opts.log?.(
10667
+ `sync: ${this.syncResult.files} files, ${this.syncResult.sessions} sessions, ${uploadedBuckets} buckets / ${uploadedSessions} sessions uploaded in ${this.syncResult.durationMs} ms`
10668
+ );
10669
+ return this.syncResult;
10670
+ } catch (err) {
10671
+ this.syncStatus = "error";
10672
+ this.syncFinishedAt = Date.now();
10673
+ this.syncError = err instanceof SignedOutError ? "not signed in" : errorMessage(err);
10674
+ this.opts.log?.(`sync failed: ${this.syncError}`);
10675
+ return null;
10676
+ } finally {
10677
+ this.syncPhase = null;
10678
+ this.emitSnapshot();
10679
+ this.scheduleSyncBannerClear();
10680
+ }
10681
+ }
10682
+ /** Return the sync banner to "idle" a little after it finished, keeping `last` for the footer. */
10683
+ scheduleSyncBannerClear() {
10684
+ if (this.opts.watch === false) return;
10685
+ if (this.syncBannerTimer) clearTimeout(this.syncBannerTimer);
10686
+ this.syncBannerTimer = setTimeout(() => {
10687
+ this.syncBannerTimer = null;
10688
+ if (this.syncStatus === "done" || this.syncStatus === "error") {
10689
+ this.syncStatus = "idle";
10690
+ this.emitSnapshot();
10691
+ }
10692
+ }, SYNC_BANNER_MS);
10693
+ this.syncBannerTimer.unref?.();
10694
+ }
10695
+ syncState() {
10696
+ return {
10697
+ status: this.syncStatus,
10698
+ phase: this.syncPhase,
10699
+ startedAt: this.syncStartedAt,
10700
+ finishedAt: this.syncFinishedAt,
10701
+ error: this.syncError,
10702
+ last: this.syncResult
10703
+ };
10704
+ }
10473
10705
  /**
10474
10706
  * Ask the npm registry for the newest published version (cached for 6 h unless `force`).
10475
10707
  * Best-effort: a failure is recorded on the snapshot, never thrown.
@@ -10587,6 +10819,7 @@ ${r.output}`.trim();
10587
10819
  const sessions = this.store.sessions();
10588
10820
  return {
10589
10821
  version: APP_VERSION,
10822
+ channel: APP_CHANNEL,
10590
10823
  generatedAt: Date.now(),
10591
10824
  language: this.language(),
10592
10825
  languageSetting: this.config.language,
@@ -10605,6 +10838,7 @@ ${r.output}`.trim();
10605
10838
  rateLimitsError: this.config.liveRateLimits ? this.liveLimitsError : null,
10606
10839
  rateLimitsUpdatedAt: this.liveLimits ? this.liveLimitsAt : s.logRateLimits?.observedAt ?? null,
10607
10840
  update: this.updateState(),
10841
+ sync: this.syncState(),
10608
10842
  modelsToday: s.modelsToday,
10609
10843
  modelsMonth: s.modelsMonth,
10610
10844
  byAgentToday: s.byAgentToday,
@@ -10621,6 +10855,7 @@ ${r.output}`.trim();
10621
10855
  },
10622
10856
  sessionDirs: SessionStore.rootDirs(this.store.roots),
10623
10857
  sessionRoots: this.store.roots.map((r) => ({ dir: r.dir, agent: r.agent, format: r.format, origin: r.origin })),
10858
+ configDir: configDir(),
10624
10859
  launchAtLogin: this.config.launchAtLogin,
10625
10860
  trayTitle: this.config.trayTitle
10626
10861
  };
@@ -10632,6 +10867,7 @@ init_platform();
10632
10867
  init_config();
10633
10868
  init_uploader();
10634
10869
  init_i18n();
10870
+ init_version();
10635
10871
  function parseArgs2(argv) {
10636
10872
  const out = { command: null, positional: [], flags: {} };
10637
10873
  for (let i2 = 0; i2 < argv.length; i2++) {
@@ -10663,10 +10899,10 @@ function applyDashboardFlag(flags) {
10663
10899
  function electronBinary() {
10664
10900
  try {
10665
10901
  const p = require("electron");
10666
- return typeof p === "string" && p && import_node_fs10.default.existsSync(p) ? p : null;
10902
+ if (typeof p === "string" && p && import_node_fs10.default.existsSync(p)) return p;
10667
10903
  } catch {
10668
- return null;
10669
10904
  }
10905
+ return binaryFromPackageDir(managedElectronDir(configDir()));
10670
10906
  }
10671
10907
  function electronPackageDir() {
10672
10908
  try {
@@ -10678,8 +10914,7 @@ function electronPackageDir() {
10678
10914
  async function ensureElectron() {
10679
10915
  const existing = electronBinary();
10680
10916
  if (existing) return existing;
10681
- const dir = electronPackageDir();
10682
- if (!dir) return null;
10917
+ const dir = electronPackageDir() ?? ensureManagedElectronDir(configDir());
10683
10918
  const t2 = makeT(lang());
10684
10919
  console.log(t2("cliDownloadingElectron"));
10685
10920
  const log2 = { info: (m) => console.log(m), error: (m) => console.error(m) };
@@ -10797,6 +11032,7 @@ async function runStatus(json) {
10797
11032
  s.auth.status === "signedIn" ? `${t2("signedInAs", { name: s.auth.user?.name || s.auth.user?.email || "?" })} \xB7 ${s.auth.dashboardUrl}` : t2("cliNotSignedIn")
10798
11033
  );
10799
11034
  line(t2("cliStatusDirs"), s.sessionDirs.join("\n" + " ".repeat(15)) || "-");
11035
+ if (IS_DEV_BUILD) console.log(t2("cliChannelDev", { url: s.auth.dashboardUrl, dir: configDir() }));
10800
11036
  const byAgent = Object.entries(s.counts.byAgent).sort((a, b) => b[1].sessions - a[1].sessions).map(([agent, c]) => `${agent} ${c.sessions}`).join(", ");
10801
11037
  console.log(t2("sessions", { n: s.counts.sessions }) + " \xB7 " + t2("files", { n: s.counts.files }) + (byAgent ? ` (${byAgent})` : ""));
10802
11038
  if (cfg.checkUpdates) {
@@ -10805,6 +11041,36 @@ async function runStatus(json) {
10805
11041
  }
10806
11042
  return 0;
10807
11043
  }
11044
+ async function runSync(flags) {
11045
+ const cfg = applyDashboardFlag(flags);
11046
+ const L = lang(cfg);
11047
+ const t2 = makeT(L);
11048
+ const engine = new Engine({
11049
+ upload: true,
11050
+ watch: false,
11051
+ systemLocale: systemLocale(),
11052
+ log: process.env.CODEX_TRACKER_DEBUG ? (m) => console.error("[sync]", m) : void 0
11053
+ });
11054
+ console.log(t2("cliSyncStart"));
11055
+ const result = await engine.syncNow();
11056
+ if (!result) {
11057
+ console.error(t2("cliSyncFailed", { message: engine.snapshot().sync.error ?? "?" }));
11058
+ return 1;
11059
+ }
11060
+ console.log(t2("cliSyncScanned", { files: result.files, roots: result.roots, agents: result.agents.join(", ") || "-" }));
11061
+ if (result.uploaded) console.log(t2("cliSyncUploaded", { buckets: result.uploadedBuckets, sessions: result.uploadedSessions }));
11062
+ else console.log(t2("cliSyncLocal"));
11063
+ const s = engine.snapshot();
11064
+ console.log(
11065
+ t2("cliSyncDone", {
11066
+ seconds: (result.durationMs / 1e3).toFixed(1),
11067
+ sessions: result.sessions,
11068
+ tokens: formatTokens(s.today.usage.total),
11069
+ cost: formatUSD(s.today.cost)
11070
+ })
11071
+ );
11072
+ return 0;
11073
+ }
10808
11074
  async function runLogin(flags) {
10809
11075
  const cfg = applyDashboardFlag(flags);
10810
11076
  const L = lang(cfg);
@@ -10908,6 +11174,7 @@ function runPaths() {
10908
11174
  const off = SOURCE_IDS.filter((id) => !cfg.sources[id]);
10909
11175
  if (off.length) console.log(` (disabled: ${off.join(", ")})`);
10910
11176
  console.log(t2("cliConfigDir", { dir: configDir() }));
11177
+ if (IS_DEV_BUILD) console.log(t2("cliChannelDev", { url: cfg.dashboardUrl, dir: configDir() }));
10911
11178
  return 0;
10912
11179
  }
10913
11180
  function runConfig(positional) {
@@ -10954,6 +11221,10 @@ function runConfig(positional) {
10954
11221
  }
10955
11222
  async function runUpdateCommand(flags) {
10956
11223
  const t2 = makeT(lang());
11224
+ if (IS_DEV_BUILD) {
11225
+ console.error(t2("cliUpdateDevBuild"));
11226
+ return 1;
11227
+ }
10957
11228
  const info2 = await checkForUpdate({ force: true });
10958
11229
  if (info2.error && !info2.latest) {
10959
11230
  console.error(t2("cliUpdateCheckFailed", { message: info2.error }));
@@ -10992,6 +11263,7 @@ async function main() {
10992
11263
  const t2 = makeT(lang());
10993
11264
  if (args.flags.version) {
10994
11265
  console.log(t2("cliVersion", { version: APP_VERSION }));
11266
+ if (IS_DEV_BUILD) console.log(t2("cliChannelDev", { url: loadConfig().dashboardUrl, dir: configDir() }));
10995
11267
  return 0;
10996
11268
  }
10997
11269
  if (args.flags.help || args.command === "help") {
@@ -11028,6 +11300,8 @@ async function main() {
11028
11300
  }
11029
11301
  case "status":
11030
11302
  return runStatus(args.flags.json === true);
11303
+ case "sync":
11304
+ return runSync(args.flags);
11031
11305
  case "paths":
11032
11306
  return runPaths();
11033
11307
  case "config":