codex-token-tracker 0.2.0 → 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/CHANGELOG.md +52 -0
- package/README.md +63 -2
- package/dist/cli.js +261 -15
- package/dist/main.js +225 -18
- package/dist/preload.js +1 -0
- package/dist/renderer/renderer.js +110 -3
- package/dist/renderer/styles.css +99 -0
- package/package.json +14 -14
package/dist/main.js
CHANGED
|
@@ -1042,7 +1042,17 @@ var import_node_events = require("node:events");
|
|
|
1042
1042
|
var import_node_fs = __toESM(require("node:fs"));
|
|
1043
1043
|
var import_node_os = __toESM(require("node:os"));
|
|
1044
1044
|
var import_node_path = __toESM(require("node:path"));
|
|
1045
|
-
|
|
1045
|
+
|
|
1046
|
+
// src/version.ts
|
|
1047
|
+
var APP_VERSION = true ? "0.2.1" : "0.0.0-dev";
|
|
1048
|
+
var APP_CHANNEL = true ? "prod" : "dev";
|
|
1049
|
+
var IS_DEV_BUILD = APP_CHANNEL === "dev";
|
|
1050
|
+
var APP_NAME = IS_DEV_BUILD ? "Codex Tracker (dev)" : "Codex Tracker";
|
|
1051
|
+
|
|
1052
|
+
// src/core/config.ts
|
|
1053
|
+
var PROD_DASHBOARD_URL = "https://codex.chenli.dev";
|
|
1054
|
+
var DEV_DASHBOARD_URL = "http://localhost:3000";
|
|
1055
|
+
var DEFAULT_DASHBOARD_URL = IS_DEV_BUILD ? DEV_DASHBOARD_URL : PROD_DASHBOARD_URL;
|
|
1046
1056
|
var SOURCE_FORMATS = ["codex", "pi", "generic", "opencode", "cline"];
|
|
1047
1057
|
var DEFAULT_SOURCES = { codex: true, pi: true, hermes: true, opencode: true, cline: true, roo: true, kilo: true };
|
|
1048
1058
|
var SOURCE_IDS = Object.keys(DEFAULT_SOURCES);
|
|
@@ -1062,10 +1072,11 @@ var DEFAULT_CONFIG = {
|
|
|
1062
1072
|
trackAllProviders: false,
|
|
1063
1073
|
liveRateLimits: true,
|
|
1064
1074
|
usageRefreshSec: 60,
|
|
1065
|
-
|
|
1075
|
+
// a dev build must never offer to replace itself with the published package
|
|
1076
|
+
checkUpdates: !IS_DEV_BUILD
|
|
1066
1077
|
};
|
|
1067
1078
|
function configDir() {
|
|
1068
|
-
return process.env.CODEX_TRACKER_HOME || import_node_path.default.join(import_node_os.default.homedir(), ".codex-tracker");
|
|
1079
|
+
return process.env.CODEX_TRACKER_HOME || import_node_path.default.join(import_node_os.default.homedir(), IS_DEV_BUILD ? ".codex-tracker-dev" : ".codex-tracker");
|
|
1069
1080
|
}
|
|
1070
1081
|
function readJson(file, fallback) {
|
|
1071
1082
|
try {
|
|
@@ -1116,7 +1127,7 @@ function loadConfig() {
|
|
|
1116
1127
|
cfg.sources = normalizeSources(stored.sources);
|
|
1117
1128
|
cfg.trackAllProviders = stored.trackAllProviders === true;
|
|
1118
1129
|
cfg.liveRateLimits = stored.liveRateLimits !== false;
|
|
1119
|
-
cfg.checkUpdates = stored.checkUpdates
|
|
1130
|
+
cfg.checkUpdates = typeof stored.checkUpdates === "boolean" ? stored.checkUpdates : DEFAULT_CONFIG.checkUpdates;
|
|
1120
1131
|
if (!(cfg.usageRefreshSec >= 15)) cfg.usageRefreshSec = DEFAULT_CONFIG.usageRefreshSec;
|
|
1121
1132
|
if (!["auto", "en", "zh"].includes(cfg.language)) cfg.language = "auto";
|
|
1122
1133
|
if (!(cfg.uploadIntervalSec >= 10)) cfg.uploadIntervalSec = DEFAULT_CONFIG.uploadIntervalSec;
|
|
@@ -1230,6 +1241,7 @@ var en = {
|
|
|
1230
1241
|
estimatedPricingHint: "Pricing estimated: model not in the price table",
|
|
1231
1242
|
other: "Other",
|
|
1232
1243
|
lastUpload: "Last upload {time}",
|
|
1244
|
+
lastSync: "Last full sync {time}",
|
|
1233
1245
|
neverUploaded: "Not uploaded yet",
|
|
1234
1246
|
uploadError: "Upload error: {message}",
|
|
1235
1247
|
allDevicesToday: "All devices today",
|
|
@@ -1248,10 +1260,26 @@ var en = {
|
|
|
1248
1260
|
launchAtLogin: "Launch at login",
|
|
1249
1261
|
refresh: "Refresh",
|
|
1250
1262
|
quit: "Quit",
|
|
1263
|
+
// full sync
|
|
1264
|
+
syncNow: "Sync now",
|
|
1265
|
+
syncing: "Syncing\u2026",
|
|
1266
|
+
syncTitle: "Rescan every agent on this device and re-upload its full usage history",
|
|
1267
|
+
syncScanning: "Rescanning every agent's transcripts\u2026",
|
|
1268
|
+
syncComputing: "Recomputing token usage\u2026",
|
|
1269
|
+
syncUploading: "Re-uploading this device's history\u2026",
|
|
1270
|
+
syncDownloading: "Fetching other devices\u2026",
|
|
1271
|
+
syncLimits: "Refreshing rate limits\u2026",
|
|
1272
|
+
syncDone: "Synced {agents} \xB7 {files} files \xB7 {sessions} sessions \xB7 {buckets} hours re-uploaded",
|
|
1273
|
+
syncDoneLocal: "Rescanned {agents} \xB7 {files} files \xB7 {sessions} sessions \u2014 sign in to upload",
|
|
1274
|
+
syncFailed: "Sync failed: {message}",
|
|
1251
1275
|
sessions: "{n} sessions",
|
|
1252
1276
|
files: "{n} files",
|
|
1253
1277
|
noData: "No usage found yet. Run Codex and this will fill in.",
|
|
1254
1278
|
version: "v{version}",
|
|
1279
|
+
// dev builds
|
|
1280
|
+
devBuild: "DEV",
|
|
1281
|
+
devBuildTitle: "Local build \u2014 uploading to {url} (dev environment). Config: {dir}",
|
|
1282
|
+
devBuildMenu: "Dev build \u2192 {url}",
|
|
1255
1283
|
justNow: "just now",
|
|
1256
1284
|
never: "never",
|
|
1257
1285
|
local: "Local",
|
|
@@ -1279,6 +1307,7 @@ Commands:
|
|
|
1279
1307
|
login Connect this device to your dashboard (--dashboard <url>)
|
|
1280
1308
|
logout Disconnect this device
|
|
1281
1309
|
status Print today's usage, live session and rate limits
|
|
1310
|
+
sync Rescan every agent and re-upload this device's full history (calibrate)
|
|
1282
1311
|
paths Show detected session directories (Codex, pi, OpenCode, Cline/Roo/Kilo, Hermes, custom)
|
|
1283
1312
|
update Install the newest published version (--check only reports)
|
|
1284
1313
|
config get|set Read or change settings (config set uploadIntervalSec 30, config set sources.pi false)
|
|
@@ -1333,7 +1362,15 @@ Options:
|
|
|
1333
1362
|
cliUpdateRunning: "Running: {command}",
|
|
1334
1363
|
cliUpdateDone: "Updated to {version}. Restart the menu bar app (or rerun the CLI) to use it.",
|
|
1335
1364
|
cliUpdateFailed: "Update failed (exit {code}). Run it yourself: {command}",
|
|
1336
|
-
cliUpdateCheckFailed: "Could not reach the npm registry: {message}"
|
|
1365
|
+
cliUpdateCheckFailed: "Could not reach the npm registry: {message}",
|
|
1366
|
+
cliSyncStart: "Full sync: rescanning every agent on this device\u2026",
|
|
1367
|
+
cliSyncScanned: " scanned {files} files in {roots} directories ({agents})",
|
|
1368
|
+
cliSyncUploaded: " re-uploaded {buckets} hour buckets and {sessions} sessions",
|
|
1369
|
+
cliSyncLocal: " not signed in \u2014 nothing uploaded (run `codex-tracker login` first)",
|
|
1370
|
+
cliSyncDone: "Sync complete in {seconds}s \u2014 {sessions} sessions, today {tokens} tok \xB7 {cost}",
|
|
1371
|
+
cliSyncFailed: "Sync failed: {message}",
|
|
1372
|
+
cliChannelDev: "Dev build \u2014 dashboard {url} \xB7 config {dir}",
|
|
1373
|
+
cliUpdateDevBuild: "This is a local dev build; `update` would install the published package over it. Run `pnpm build` in the repo instead."
|
|
1337
1374
|
};
|
|
1338
1375
|
|
|
1339
1376
|
// src/i18n/zh.ts
|
|
@@ -1381,6 +1418,7 @@ var zh = {
|
|
|
1381
1418
|
estimatedPricingHint: "\u4EF7\u683C\u4E3A\u4F30\u7B97\u503C\uFF1A\u8BE5\u6A21\u578B\u4E0D\u5728\u4EF7\u76EE\u8868\u4E2D",
|
|
1382
1419
|
other: "\u5176\u4ED6",
|
|
1383
1420
|
lastUpload: "\u4E0A\u6B21\u4E0A\u4F20 {time}",
|
|
1421
|
+
lastSync: "\u4E0A\u6B21\u5B8C\u6574\u540C\u6B65 {time}",
|
|
1384
1422
|
neverUploaded: "\u5C1A\u672A\u4E0A\u4F20",
|
|
1385
1423
|
uploadError: "\u4E0A\u4F20\u5931\u8D25\uFF1A{message}",
|
|
1386
1424
|
allDevicesToday: "\u6240\u6709\u8BBE\u5907\u4ECA\u65E5",
|
|
@@ -1399,10 +1437,26 @@ var zh = {
|
|
|
1399
1437
|
launchAtLogin: "\u5F00\u673A\u81EA\u542F\u52A8",
|
|
1400
1438
|
refresh: "\u5237\u65B0",
|
|
1401
1439
|
quit: "\u9000\u51FA",
|
|
1440
|
+
// 完整同步
|
|
1441
|
+
syncNow: "\u7ACB\u5373\u540C\u6B65",
|
|
1442
|
+
syncing: "\u540C\u6B65\u4E2D\u2026",
|
|
1443
|
+
syncTitle: "\u91CD\u65B0\u626B\u63CF\u672C\u8BBE\u5907\u4E0A\u7684\u6240\u6709\u667A\u80FD\u4F53\uFF0C\u5E76\u91CD\u65B0\u4E0A\u4F20\u5B8C\u6574\u7528\u91CF\u5386\u53F2",
|
|
1444
|
+
syncScanning: "\u6B63\u5728\u91CD\u65B0\u626B\u63CF\u6240\u6709\u667A\u80FD\u4F53\u7684\u4F1A\u8BDD\u8BB0\u5F55\u2026",
|
|
1445
|
+
syncComputing: "\u6B63\u5728\u91CD\u65B0\u8BA1\u7B97 token \u7528\u91CF\u2026",
|
|
1446
|
+
syncUploading: "\u6B63\u5728\u91CD\u65B0\u4E0A\u4F20\u672C\u8BBE\u5907\u7684\u5386\u53F2\u6570\u636E\u2026",
|
|
1447
|
+
syncDownloading: "\u6B63\u5728\u62C9\u53D6\u5176\u4ED6\u8BBE\u5907\u7684\u6570\u636E\u2026",
|
|
1448
|
+
syncLimits: "\u6B63\u5728\u5237\u65B0\u9650\u989D\u2026",
|
|
1449
|
+
syncDone: "\u5DF2\u540C\u6B65 {agents} \xB7 {files} \u4E2A\u6587\u4EF6 \xB7 {sessions} \u4E2A\u4F1A\u8BDD \xB7 \u91CD\u65B0\u4E0A\u4F20 {buckets} \u4E2A\u5C0F\u65F6\u6BB5",
|
|
1450
|
+
syncDoneLocal: "\u5DF2\u91CD\u65B0\u626B\u63CF {agents} \xB7 {files} \u4E2A\u6587\u4EF6 \xB7 {sessions} \u4E2A\u4F1A\u8BDD \u2014 \u767B\u5F55\u540E\u53EF\u4E0A\u4F20",
|
|
1451
|
+
syncFailed: "\u540C\u6B65\u5931\u8D25\uFF1A{message}",
|
|
1402
1452
|
sessions: "{n} \u4E2A\u4F1A\u8BDD",
|
|
1403
1453
|
files: "{n} \u4E2A\u6587\u4EF6",
|
|
1404
1454
|
noData: "\u8FD8\u6CA1\u6709\u7528\u91CF\u6570\u636E\u3002\u8FD0\u884C Codex \u540E\u8FD9\u91CC\u4F1A\u81EA\u52A8\u66F4\u65B0\u3002",
|
|
1405
1455
|
version: "v{version}",
|
|
1456
|
+
// 开发版
|
|
1457
|
+
devBuild: "\u5F00\u53D1\u7248",
|
|
1458
|
+
devBuildTitle: "\u672C\u5730\u6784\u5EFA \u2014\u2014 \u6570\u636E\u4E0A\u4F20\u81F3 {url}\uFF08\u5F00\u53D1\u73AF\u5883\uFF09\u3002\u914D\u7F6E\u76EE\u5F55\uFF1A{dir}",
|
|
1459
|
+
devBuildMenu: "\u5F00\u53D1\u7248 \u2192 {url}",
|
|
1406
1460
|
justNow: "\u521A\u521A",
|
|
1407
1461
|
never: "\u4ECE\u672A",
|
|
1408
1462
|
local: "\u672C\u673A",
|
|
@@ -1428,6 +1482,7 @@ var zh = {
|
|
|
1428
1482
|
login \u5C06\u672C\u8BBE\u5907\u8FDE\u63A5\u5230\u4EEA\u8868\u76D8\uFF08--dashboard <url>\uFF09
|
|
1429
1483
|
logout \u65AD\u5F00\u672C\u8BBE\u5907
|
|
1430
1484
|
status \u6253\u5370\u4ECA\u65E5\u7528\u91CF\u3001\u5B9E\u65F6\u4F1A\u8BDD\u4E0E\u9650\u989D
|
|
1485
|
+
sync \u91CD\u65B0\u626B\u63CF\u6240\u6709\u667A\u80FD\u4F53\u5E76\u91CD\u65B0\u4E0A\u4F20\u672C\u8BBE\u5907\u5B8C\u6574\u5386\u53F2\uFF08\u6821\u51C6\uFF09
|
|
1431
1486
|
paths \u663E\u793A\u68C0\u6D4B\u5230\u7684\u4F1A\u8BDD\u76EE\u5F55\uFF08Codex\u3001pi\u3001OpenCode\u3001Cline/Roo/Kilo\u3001Hermes\u3001\u81EA\u5B9A\u4E49\uFF09
|
|
1432
1487
|
update \u5B89\u88C5\u6700\u65B0\u53D1\u5E03\u7248\u672C\uFF08--check \u53EA\u68C0\u67E5\u4E0D\u5B89\u88C5\uFF09
|
|
1433
1488
|
config get|set \u8BFB\u53D6\u6216\u4FEE\u6539\u8BBE\u7F6E\uFF08config set uploadIntervalSec 30\uFF0Cconfig set sources.pi false\uFF09
|
|
@@ -1482,7 +1537,15 @@ var zh = {
|
|
|
1482
1537
|
cliUpdateRunning: "\u6B63\u5728\u6267\u884C\uFF1A{command}",
|
|
1483
1538
|
cliUpdateDone: "\u5DF2\u66F4\u65B0\u5230 {version}\u3002\u8BF7\u91CD\u542F\u83DC\u5355\u680F\u5E94\u7528\uFF08\u6216\u91CD\u65B0\u8FD0\u884C CLI\uFF09\u4EE5\u751F\u6548\u3002",
|
|
1484
1539
|
cliUpdateFailed: "\u66F4\u65B0\u5931\u8D25\uFF08\u9000\u51FA\u7801 {code}\uFF09\u3002\u8BF7\u624B\u52A8\u6267\u884C\uFF1A{command}",
|
|
1485
|
-
cliUpdateCheckFailed: "\u65E0\u6CD5\u8BBF\u95EE npm registry\uFF1A{message}"
|
|
1540
|
+
cliUpdateCheckFailed: "\u65E0\u6CD5\u8BBF\u95EE npm registry\uFF1A{message}",
|
|
1541
|
+
cliSyncStart: "\u5B8C\u6574\u540C\u6B65\uFF1A\u6B63\u5728\u91CD\u65B0\u626B\u63CF\u672C\u8BBE\u5907\u4E0A\u7684\u6240\u6709\u667A\u80FD\u4F53\u2026",
|
|
1542
|
+
cliSyncScanned: " \u5DF2\u626B\u63CF {roots} \u4E2A\u76EE\u5F55\u4E2D\u7684 {files} \u4E2A\u6587\u4EF6\uFF08{agents}\uFF09",
|
|
1543
|
+
cliSyncUploaded: " \u5DF2\u91CD\u65B0\u4E0A\u4F20 {buckets} \u4E2A\u5C0F\u65F6\u6BB5\u4E0E {sessions} \u4E2A\u4F1A\u8BDD",
|
|
1544
|
+
cliSyncLocal: " \u672A\u767B\u5F55 \u2014 \u6CA1\u6709\u4E0A\u4F20\u4EFB\u4F55\u6570\u636E\uFF08\u8BF7\u5148\u8FD0\u884C `codex-tracker login`\uFF09",
|
|
1545
|
+
cliSyncDone: "\u540C\u6B65\u5B8C\u6210\uFF0C\u7528\u65F6 {seconds} \u79D2 \u2014 {sessions} \u4E2A\u4F1A\u8BDD\uFF0C\u4ECA\u65E5 {tokens} tok \xB7 {cost}",
|
|
1546
|
+
cliSyncFailed: "\u540C\u6B65\u5931\u8D25\uFF1A{message}",
|
|
1547
|
+
cliChannelDev: "\u5F00\u53D1\u7248 \u2014\u2014 \u4EEA\u8868\u76D8 {url} \xB7 \u914D\u7F6E\u76EE\u5F55 {dir}",
|
|
1548
|
+
cliUpdateDevBuild: "\u8FD9\u662F\u672C\u5730\u5F00\u53D1\u7248\uFF1B`update` \u4F1A\u7528\u5DF2\u53D1\u5E03\u7684\u5305\u8986\u76D6\u5B83\u3002\u8BF7\u6539\u4E3A\u5728\u4ED3\u5E93\u4E2D\u8FD0\u884C `pnpm build`\u3002"
|
|
1486
1549
|
};
|
|
1487
1550
|
|
|
1488
1551
|
// src/i18n/index.ts
|
|
@@ -2289,6 +2352,15 @@ var SessionStore = class {
|
|
|
2289
2352
|
onChange = null;
|
|
2290
2353
|
debounceTimer = null;
|
|
2291
2354
|
sessionCache = null;
|
|
2355
|
+
/**
|
|
2356
|
+
* Forget the parsed-file index so the next deep refresh re-reads and re-parses every transcript.
|
|
2357
|
+
* Used by the full sync: files are otherwise skipped while their size and mtime are unchanged, which
|
|
2358
|
+
* would keep stale numbers around after a parser or pricing change.
|
|
2359
|
+
*/
|
|
2360
|
+
reset() {
|
|
2361
|
+
this.files.clear();
|
|
2362
|
+
this.sessionCache = null;
|
|
2363
|
+
}
|
|
2292
2364
|
async refreshDeep() {
|
|
2293
2365
|
const o = this.getOptions();
|
|
2294
2366
|
this.roots = discoverSessionRoots({ extraSessionDirs: o.extraSessionDirs, sources: o.sources });
|
|
@@ -8847,11 +8919,25 @@ var Uploader = class {
|
|
|
8847
8919
|
throw err;
|
|
8848
8920
|
}
|
|
8849
8921
|
}
|
|
8850
|
-
/**
|
|
8851
|
-
async
|
|
8922
|
+
/** Wait for an in-flight incremental push to finish so a full sync isn't skipped by the guard. */
|
|
8923
|
+
async awaitIdle(timeoutMs = 15e3) {
|
|
8924
|
+
const deadline = Date.now() + timeoutMs;
|
|
8925
|
+
while (this.inFlight && Date.now() < deadline) await new Promise((r) => setTimeout(r, 100));
|
|
8926
|
+
}
|
|
8927
|
+
/**
|
|
8928
|
+
* Upload changed hour buckets and session summaries. Returns counts of pushed items.
|
|
8929
|
+
* With `full`, the record of what was already pushed is dropped first so *everything* is re-sent —
|
|
8930
|
+
* that is what recalibrates this device's totals on the dashboard after a parser or pricing change.
|
|
8931
|
+
*/
|
|
8932
|
+
async pushAll(buckets, sessions, sessionCosts, opts = {}) {
|
|
8933
|
+
if (opts.full) await this.awaitIdle();
|
|
8852
8934
|
if (this.inFlight) return { buckets: 0, sessions: 0 };
|
|
8853
8935
|
this.inFlight = true;
|
|
8854
8936
|
try {
|
|
8937
|
+
if (opts.full) {
|
|
8938
|
+
this.state.pushedBuckets = {};
|
|
8939
|
+
this.state.pushedSessions = {};
|
|
8940
|
+
}
|
|
8855
8941
|
const changed = [];
|
|
8856
8942
|
const hashes = /* @__PURE__ */ new Map();
|
|
8857
8943
|
for (const b of buckets.sort((a, b2) => a.hourStart - b2.hourStart)) {
|
|
@@ -9061,12 +9147,6 @@ async function fetchLiveRateLimits(appVersion, timeoutMs = 1e4) {
|
|
|
9061
9147
|
var import_node_fs8 = __toESM(require("node:fs"));
|
|
9062
9148
|
var import_node_path10 = __toESM(require("node:path"));
|
|
9063
9149
|
var import_node_child_process3 = require("node:child_process");
|
|
9064
|
-
|
|
9065
|
-
// src/version.ts
|
|
9066
|
-
var APP_VERSION = true ? "0.2.0" : "0.0.0-dev";
|
|
9067
|
-
var APP_NAME = "Codex Tracker";
|
|
9068
|
-
|
|
9069
|
-
// src/core/update.ts
|
|
9070
9150
|
var NPM_PACKAGE = "codex-token-tracker";
|
|
9071
9151
|
var CHECK_TTL_MS = 6 * 60 * 60 * 1e3;
|
|
9072
9152
|
var REQUEST_TIMEOUT_MS = 8e3;
|
|
@@ -9090,7 +9170,7 @@ function compareVersions(a, b) {
|
|
|
9090
9170
|
return A.pre < B.pre ? -1 : 1;
|
|
9091
9171
|
}
|
|
9092
9172
|
function isDevBuild(version2) {
|
|
9093
|
-
return version2.startsWith("0.0.0");
|
|
9173
|
+
return IS_DEV_BUILD || version2.startsWith("0.0.0");
|
|
9094
9174
|
}
|
|
9095
9175
|
function registryBase() {
|
|
9096
9176
|
const r = process.env.CODEX_TRACKER_REGISTRY || process.env.npm_config_registry || process.env.NPM_CONFIG_REGISTRY || "https://registry.npmjs.org";
|
|
@@ -9233,6 +9313,7 @@ var REMOTE_MS = 6e4;
|
|
|
9233
9313
|
var LIVE_LIMITS_DEBOUNCE_MS = 1e4;
|
|
9234
9314
|
var LIVE_LIMITS_MIN_GAP_MS = 2e4;
|
|
9235
9315
|
var UPDATE_CHECK_MS = 6 * 60 * 60 * 1e3;
|
|
9316
|
+
var SYNC_BANNER_MS = 25e3;
|
|
9236
9317
|
var Engine = class extends import_node_events.EventEmitter {
|
|
9237
9318
|
constructor(opts) {
|
|
9238
9319
|
super();
|
|
@@ -9272,6 +9353,13 @@ var Engine = class extends import_node_events.EventEmitter {
|
|
|
9272
9353
|
update = null;
|
|
9273
9354
|
updateStatus = "idle";
|
|
9274
9355
|
updateLog = null;
|
|
9356
|
+
syncStatus = "idle";
|
|
9357
|
+
syncPhase = null;
|
|
9358
|
+
syncStartedAt = null;
|
|
9359
|
+
syncFinishedAt = null;
|
|
9360
|
+
syncError = null;
|
|
9361
|
+
syncResult = null;
|
|
9362
|
+
syncBannerTimer = null;
|
|
9275
9363
|
get heatmapWeeks() {
|
|
9276
9364
|
return this.opts.heatmapWeeks ?? 16;
|
|
9277
9365
|
}
|
|
@@ -9322,6 +9410,8 @@ var Engine = class extends import_node_events.EventEmitter {
|
|
|
9322
9410
|
this.timers = [];
|
|
9323
9411
|
if (this.liveLimitsTimer) clearTimeout(this.liveLimitsTimer);
|
|
9324
9412
|
this.liveLimitsTimer = null;
|
|
9413
|
+
if (this.syncBannerTimer) clearTimeout(this.syncBannerTimer);
|
|
9414
|
+
this.syncBannerTimer = null;
|
|
9325
9415
|
this.store.stopWatching();
|
|
9326
9416
|
this.pending?.abort.abort();
|
|
9327
9417
|
}
|
|
@@ -9425,6 +9515,112 @@ var Engine = class extends import_node_events.EventEmitter {
|
|
|
9425
9515
|
await this.uploader.fetchRemote(this.heatmapWeeks);
|
|
9426
9516
|
this.recompute();
|
|
9427
9517
|
}
|
|
9518
|
+
/**
|
|
9519
|
+
* Full sync ("calibrate this device"). Unlike the periodic incremental upload this deliberately
|
|
9520
|
+
* throws away every cache on the way:
|
|
9521
|
+
* 1. re-read the config so sources enabled since start-up are picked up,
|
|
9522
|
+
* 2. drop the parsed-file index and re-discover + re-parse every transcript of every agent
|
|
9523
|
+
* (Codex plus the agents running on the Codex subscription: pi, OpenCode, Cline/Roo/Kilo,
|
|
9524
|
+
* Hermes and any custom `extraSessionDirs`),
|
|
9525
|
+
* 3. recompute the aggregates with the current pricing table,
|
|
9526
|
+
* 4. re-upload *everything* — not just what changed — so the dashboard's totals for this device
|
|
9527
|
+
* are replaced by the freshly computed ones,
|
|
9528
|
+
* 5. pull the other devices' rows and the live rate limits back down.
|
|
9529
|
+
*
|
|
9530
|
+
* Returns null when a sync is already running. Never throws: failures land in the returned state.
|
|
9531
|
+
*/
|
|
9532
|
+
async syncNow() {
|
|
9533
|
+
if (this.syncStatus === "running") return null;
|
|
9534
|
+
if (this.syncBannerTimer) {
|
|
9535
|
+
clearTimeout(this.syncBannerTimer);
|
|
9536
|
+
this.syncBannerTimer = null;
|
|
9537
|
+
}
|
|
9538
|
+
const startedAt = Date.now();
|
|
9539
|
+
this.syncStatus = "running";
|
|
9540
|
+
this.syncPhase = null;
|
|
9541
|
+
this.syncStartedAt = startedAt;
|
|
9542
|
+
this.syncFinishedAt = null;
|
|
9543
|
+
this.syncError = null;
|
|
9544
|
+
const phase = (p) => {
|
|
9545
|
+
this.syncPhase = p;
|
|
9546
|
+
this.emitSnapshot();
|
|
9547
|
+
};
|
|
9548
|
+
try {
|
|
9549
|
+
phase("scanning");
|
|
9550
|
+
this.reloadConfig();
|
|
9551
|
+
this.store.reset();
|
|
9552
|
+
await this.store.refreshDeep();
|
|
9553
|
+
phase("computing");
|
|
9554
|
+
this.recompute();
|
|
9555
|
+
let uploadedBuckets = 0;
|
|
9556
|
+
let uploadedSessions = 0;
|
|
9557
|
+
const uploaded = this.opts.upload && this.signedIn;
|
|
9558
|
+
if (uploaded) {
|
|
9559
|
+
phase("uploading");
|
|
9560
|
+
const r = await this.uploader.pushAll(this.stats.buckets, this.stats.sessions, this.stats.sessionCosts, { full: true });
|
|
9561
|
+
uploadedBuckets = r.buckets;
|
|
9562
|
+
uploadedSessions = r.sessions;
|
|
9563
|
+
await this.heartbeatNow().catch(() => {
|
|
9564
|
+
});
|
|
9565
|
+
phase("downloading");
|
|
9566
|
+
await this.fetchRemoteNow();
|
|
9567
|
+
}
|
|
9568
|
+
phase("limits");
|
|
9569
|
+
await this.refreshLiveLimits(true);
|
|
9570
|
+
const finishedAt = Date.now();
|
|
9571
|
+
this.syncResult = {
|
|
9572
|
+
startedAt,
|
|
9573
|
+
finishedAt,
|
|
9574
|
+
durationMs: finishedAt - startedAt,
|
|
9575
|
+
files: this.store.fileCount,
|
|
9576
|
+
sessions: this.store.sessions().length,
|
|
9577
|
+
roots: this.store.roots.length,
|
|
9578
|
+
agents: [...new Set(this.store.roots.map((r) => r.agent))].sort(),
|
|
9579
|
+
uploadedBuckets,
|
|
9580
|
+
uploadedSessions,
|
|
9581
|
+
uploaded
|
|
9582
|
+
};
|
|
9583
|
+
this.syncStatus = "done";
|
|
9584
|
+
this.syncFinishedAt = finishedAt;
|
|
9585
|
+
this.opts.log?.(
|
|
9586
|
+
`sync: ${this.syncResult.files} files, ${this.syncResult.sessions} sessions, ${uploadedBuckets} buckets / ${uploadedSessions} sessions uploaded in ${this.syncResult.durationMs} ms`
|
|
9587
|
+
);
|
|
9588
|
+
return this.syncResult;
|
|
9589
|
+
} catch (err) {
|
|
9590
|
+
this.syncStatus = "error";
|
|
9591
|
+
this.syncFinishedAt = Date.now();
|
|
9592
|
+
this.syncError = err instanceof SignedOutError ? "not signed in" : errorMessage(err);
|
|
9593
|
+
this.opts.log?.(`sync failed: ${this.syncError}`);
|
|
9594
|
+
return null;
|
|
9595
|
+
} finally {
|
|
9596
|
+
this.syncPhase = null;
|
|
9597
|
+
this.emitSnapshot();
|
|
9598
|
+
this.scheduleSyncBannerClear();
|
|
9599
|
+
}
|
|
9600
|
+
}
|
|
9601
|
+
/** Return the sync banner to "idle" a little after it finished, keeping `last` for the footer. */
|
|
9602
|
+
scheduleSyncBannerClear() {
|
|
9603
|
+
if (this.opts.watch === false) return;
|
|
9604
|
+
if (this.syncBannerTimer) clearTimeout(this.syncBannerTimer);
|
|
9605
|
+
this.syncBannerTimer = setTimeout(() => {
|
|
9606
|
+
this.syncBannerTimer = null;
|
|
9607
|
+
if (this.syncStatus === "done" || this.syncStatus === "error") {
|
|
9608
|
+
this.syncStatus = "idle";
|
|
9609
|
+
this.emitSnapshot();
|
|
9610
|
+
}
|
|
9611
|
+
}, SYNC_BANNER_MS);
|
|
9612
|
+
this.syncBannerTimer.unref?.();
|
|
9613
|
+
}
|
|
9614
|
+
syncState() {
|
|
9615
|
+
return {
|
|
9616
|
+
status: this.syncStatus,
|
|
9617
|
+
phase: this.syncPhase,
|
|
9618
|
+
startedAt: this.syncStartedAt,
|
|
9619
|
+
finishedAt: this.syncFinishedAt,
|
|
9620
|
+
error: this.syncError,
|
|
9621
|
+
last: this.syncResult
|
|
9622
|
+
};
|
|
9623
|
+
}
|
|
9428
9624
|
/**
|
|
9429
9625
|
* Ask the npm registry for the newest published version (cached for 6 h unless `force`).
|
|
9430
9626
|
* Best-effort: a failure is recorded on the snapshot, never thrown.
|
|
@@ -9542,6 +9738,7 @@ ${r.output}`.trim();
|
|
|
9542
9738
|
const sessions = this.store.sessions();
|
|
9543
9739
|
return {
|
|
9544
9740
|
version: APP_VERSION,
|
|
9741
|
+
channel: APP_CHANNEL,
|
|
9545
9742
|
generatedAt: Date.now(),
|
|
9546
9743
|
language: this.language(),
|
|
9547
9744
|
languageSetting: this.config.language,
|
|
@@ -9560,6 +9757,7 @@ ${r.output}`.trim();
|
|
|
9560
9757
|
rateLimitsError: this.config.liveRateLimits ? this.liveLimitsError : null,
|
|
9561
9758
|
rateLimitsUpdatedAt: this.liveLimits ? this.liveLimitsAt : s.logRateLimits?.observedAt ?? null,
|
|
9562
9759
|
update: this.updateState(),
|
|
9760
|
+
sync: this.syncState(),
|
|
9563
9761
|
modelsToday: s.modelsToday,
|
|
9564
9762
|
modelsMonth: s.modelsMonth,
|
|
9565
9763
|
byAgentToday: s.byAgentToday,
|
|
@@ -9576,6 +9774,7 @@ ${r.output}`.trim();
|
|
|
9576
9774
|
},
|
|
9577
9775
|
sessionDirs: SessionStore.rootDirs(this.store.roots),
|
|
9578
9776
|
sessionRoots: this.store.roots.map((r) => ({ dir: r.dir, agent: r.agent, format: r.format, origin: r.origin })),
|
|
9777
|
+
configDir: configDir(),
|
|
9579
9778
|
launchAtLogin: this.config.launchAtLogin,
|
|
9580
9779
|
trayTitle: this.config.trayTitle
|
|
9581
9780
|
};
|
|
@@ -9622,14 +9821,15 @@ function updateTray(s) {
|
|
|
9622
9821
|
mb.tray.setToolTip(`${APP_NAME} \u2014 ${t(L, "today")}: ${formatTokens(s.today.usage.total)} \xB7 ${formatUSD(s.today.cost)}${live}`);
|
|
9623
9822
|
if (isMac) mb.tray.setTitle(trayText(s), { fontType: "monospacedDigit" });
|
|
9624
9823
|
}
|
|
9625
|
-
var
|
|
9824
|
+
var launchAgentLabel = IS_DEV_BUILD ? "dev.codex-tracker.menubar.dev" : "dev.codex-tracker.menubar";
|
|
9825
|
+
var launchAgentPath = import_node_path11.default.join(import_node_os7.default.homedir(), "Library", "LaunchAgents", `${launchAgentLabel}.plist`);
|
|
9626
9826
|
function setLaunchAtLogin(enabled) {
|
|
9627
9827
|
if (isMac) {
|
|
9628
9828
|
if (enabled) {
|
|
9629
9829
|
const plist = `<?xml version="1.0" encoding="UTF-8"?>
|
|
9630
9830
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
9631
9831
|
<plist version="1.0"><dict>
|
|
9632
|
-
<key>Label</key><string
|
|
9832
|
+
<key>Label</key><string>${launchAgentLabel}</string>
|
|
9633
9833
|
<key>ProgramArguments</key><array><string>${process.execPath}</string><string>${import_node_path11.default.join(__dirname, "main.js")}</string></array>
|
|
9634
9834
|
<key>RunAtLoad</key><true/>
|
|
9635
9835
|
<key>KeepAlive</key><false/>
|
|
@@ -9664,6 +9864,7 @@ function buildMenu(s) {
|
|
|
9664
9864
|
});
|
|
9665
9865
|
const template = [
|
|
9666
9866
|
{ label: `${APP_NAME} ${t(L, "version", { version: APP_VERSION })}`, enabled: false },
|
|
9867
|
+
...IS_DEV_BUILD ? [{ label: t(L, "devBuildMenu", { url: s.auth.dashboardUrl }), enabled: false }] : [],
|
|
9667
9868
|
{
|
|
9668
9869
|
label: s.auth.status === "signedIn" ? t(L, "signedInAs", { name: s.auth.user?.name || s.auth.user?.email || "?" }) : t(L, "signedOut"),
|
|
9669
9870
|
enabled: false
|
|
@@ -9688,6 +9889,11 @@ function buildMenu(s) {
|
|
|
9688
9889
|
}
|
|
9689
9890
|
] : [],
|
|
9690
9891
|
{ label: t(L, "refresh"), click: () => void engine?.refresh(true) },
|
|
9892
|
+
{
|
|
9893
|
+
label: s.sync.status === "running" ? t(L, "syncing") : t(L, "syncNow"),
|
|
9894
|
+
enabled: s.sync.status !== "running",
|
|
9895
|
+
click: () => void engine?.syncNow()
|
|
9896
|
+
},
|
|
9691
9897
|
...s.update ? [
|
|
9692
9898
|
s.update.available ? { label: t(L, "updateAvailable", { version: s.update.latest ?? "?" }), click: () => void engine?.installUpdate() } : { label: t(L, "checkForUpdates"), click: () => void engine?.checkUpdate(true) }
|
|
9693
9899
|
] : [],
|
|
@@ -9704,10 +9910,10 @@ async function startLogin() {
|
|
|
9704
9910
|
log2("login failed", errorMessage(err));
|
|
9705
9911
|
}
|
|
9706
9912
|
}
|
|
9913
|
+
import_electron.app.setName(APP_NAME);
|
|
9707
9914
|
if (!import_electron.app.requestSingleInstanceLock()) {
|
|
9708
9915
|
import_electron.app.quit();
|
|
9709
9916
|
} else {
|
|
9710
|
-
import_electron.app.setName(APP_NAME);
|
|
9711
9917
|
import_electron.app.on("second-instance", () => mb?.showWindow());
|
|
9712
9918
|
import_electron.app.on("window-all-closed", () => {
|
|
9713
9919
|
});
|
|
@@ -9747,6 +9953,7 @@ if (!import_electron.app.requestSingleInstanceLock()) {
|
|
|
9747
9953
|
import_electron.ipcMain.handle("auth:cancel", () => engine?.cancelLogin());
|
|
9748
9954
|
import_electron.ipcMain.handle("auth:logout", () => engine?.logout());
|
|
9749
9955
|
import_electron.ipcMain.handle("refresh", () => engine?.refresh(true).then(() => void 0));
|
|
9956
|
+
import_electron.ipcMain.handle("sync:now", () => engine?.syncNow().then(() => void 0));
|
|
9750
9957
|
import_electron.ipcMain.handle("update:check", () => engine?.checkUpdate(true).then(() => void 0));
|
|
9751
9958
|
import_electron.ipcMain.handle("update:install", () => engine?.installUpdate().then(() => void 0));
|
|
9752
9959
|
import_electron.ipcMain.handle("quit", () => import_electron.app.quit());
|
package/dist/preload.js
CHANGED
|
@@ -16,6 +16,7 @@ var bridge = {
|
|
|
16
16
|
cancelLogin: () => import_electron.ipcRenderer.invoke("auth:cancel"),
|
|
17
17
|
logout: () => import_electron.ipcRenderer.invoke("auth:logout"),
|
|
18
18
|
refresh: () => import_electron.ipcRenderer.invoke("refresh"),
|
|
19
|
+
syncNow: () => import_electron.ipcRenderer.invoke("sync:now"),
|
|
19
20
|
checkUpdate: () => import_electron.ipcRenderer.invoke("update:check"),
|
|
20
21
|
installUpdate: () => import_electron.ipcRenderer.invoke("update:install"),
|
|
21
22
|
quit: () => import_electron.ipcRenderer.invoke("quit")
|