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