codex-token-tracker 0.1.0 → 0.2.0
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 +58 -0
- package/README.md +40 -7
- package/dist/cli.js +709 -180
- package/dist/main.js +393 -81
- package/dist/preload.js +2 -0
- package/dist/renderer/renderer.js +131 -4
- package/dist/renderer/styles.css +30 -0
- package/package.json +17 -14
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;
|
|
@@ -1022,7 +1061,8 @@ var DEFAULT_CONFIG = {
|
|
|
1022
1061
|
sources: { ...DEFAULT_SOURCES },
|
|
1023
1062
|
trackAllProviders: false,
|
|
1024
1063
|
liveRateLimits: true,
|
|
1025
|
-
usageRefreshSec: 60
|
|
1064
|
+
usageRefreshSec: 60,
|
|
1065
|
+
checkUpdates: true
|
|
1026
1066
|
};
|
|
1027
1067
|
function configDir() {
|
|
1028
1068
|
return process.env.CODEX_TRACKER_HOME || import_node_path.default.join(import_node_os.default.homedir(), ".codex-tracker");
|
|
@@ -1076,6 +1116,7 @@ function loadConfig() {
|
|
|
1076
1116
|
cfg.sources = normalizeSources(stored.sources);
|
|
1077
1117
|
cfg.trackAllProviders = stored.trackAllProviders === true;
|
|
1078
1118
|
cfg.liveRateLimits = stored.liveRateLimits !== false;
|
|
1119
|
+
cfg.checkUpdates = stored.checkUpdates !== false;
|
|
1079
1120
|
if (!(cfg.usageRefreshSec >= 15)) cfg.usageRefreshSec = DEFAULT_CONFIG.usageRefreshSec;
|
|
1080
1121
|
if (!["auto", "en", "zh"].includes(cfg.language)) cfg.language = "auto";
|
|
1081
1122
|
if (!(cfg.uploadIntervalSec >= 10)) cfg.uploadIntervalSec = DEFAULT_CONFIG.uploadIntervalSec;
|
|
@@ -1215,8 +1256,20 @@ var en = {
|
|
|
1215
1256
|
never: "never",
|
|
1216
1257
|
local: "Local",
|
|
1217
1258
|
remote: "Other devices",
|
|
1259
|
+
// updates
|
|
1260
|
+
update: "Update",
|
|
1261
|
+
updateAvailable: "Update to v{version}",
|
|
1262
|
+
updateNewVersion: "Version {version} is available (you have v{current})",
|
|
1263
|
+
updateUpToDate: "Up to date",
|
|
1264
|
+
updateChecking: "Checking\u2026",
|
|
1265
|
+
updateInstalling: "Installing\u2026",
|
|
1266
|
+
updateInstalled: "Updated to v{version} \u2014 restart to apply",
|
|
1267
|
+
updateFailed: "Update failed \u2014 run this yourself:",
|
|
1268
|
+
updateCheckFailed: "Update check failed: {message}",
|
|
1269
|
+
checkForUpdates: "Check for updates",
|
|
1270
|
+
releaseNotes: "Release notes",
|
|
1218
1271
|
// CLI
|
|
1219
|
-
cliUsage: `Usage: codex-tracker [command] [options]
|
|
1272
|
+
cliUsage: `Usage: codex-token-tracker [command] [options] (alias: codex-tracker)
|
|
1220
1273
|
|
|
1221
1274
|
Commands:
|
|
1222
1275
|
(none) Start the menu bar app (falls back to agent mode without a display)
|
|
@@ -1227,10 +1280,12 @@ Commands:
|
|
|
1227
1280
|
logout Disconnect this device
|
|
1228
1281
|
status Print today's usage, live session and rate limits
|
|
1229
1282
|
paths Show detected session directories (Codex, pi, OpenCode, Cline/Roo/Kilo, Hermes, custom)
|
|
1283
|
+
update Install the newest published version (--check only reports)
|
|
1230
1284
|
config get|set Read or change settings (config set uploadIntervalSec 30, config set sources.pi false)
|
|
1231
1285
|
lang <en|zh|auto> Set the display language
|
|
1232
1286
|
|
|
1233
1287
|
Options:
|
|
1288
|
+
--check update: report the newest version without installing it
|
|
1234
1289
|
--dashboard <url> Dashboard URL (self-hosted)
|
|
1235
1290
|
--background Start the menu bar app detached and return
|
|
1236
1291
|
--version, -v Print version
|
|
@@ -1239,6 +1294,8 @@ Options:
|
|
|
1239
1294
|
cliUnknownCommand: "Unknown command: {command}",
|
|
1240
1295
|
cliNoDisplay: "No display detected \u2014 starting agent mode (run `codex-tracker menubar` on a desktop).",
|
|
1241
1296
|
cliNoElectron: "Electron is not installed (optional dependency) \u2014 starting agent mode.",
|
|
1297
|
+
cliDownloadingElectron: "Electron runtime not found \u2014 downloading it now (about 100 MB, one time)\u2026",
|
|
1298
|
+
cliElectronDownloadFailed: "Electron download failed. Retry with `npm install -g codex-token-tracker --allow-scripts=electron` or `npm rebuild -g electron`; the headless `codex-tracker agent` works without it.",
|
|
1242
1299
|
cliStartingMenubar: "Starting menu bar app\u2026",
|
|
1243
1300
|
cliLoginStart: "Connecting this device to {dashboard}",
|
|
1244
1301
|
cliLoginCode: "Your code: {code}",
|
|
@@ -1270,7 +1327,13 @@ Options:
|
|
|
1270
1327
|
cliConfigSet: "Set {key} = {value}",
|
|
1271
1328
|
cliConfigUnknownKey: "Unknown config key: {key}. Keys: {keys}",
|
|
1272
1329
|
cliLangSet: "Language set to {lang}",
|
|
1273
|
-
cliConfigDir: "Config: {dir}"
|
|
1330
|
+
cliConfigDir: "Config: {dir}",
|
|
1331
|
+
cliUpdateLatest: "codex-token-tracker {version} is the latest version.",
|
|
1332
|
+
cliUpdateAvailable: "Update available: {current} \u2192 {latest}",
|
|
1333
|
+
cliUpdateRunning: "Running: {command}",
|
|
1334
|
+
cliUpdateDone: "Updated to {version}. Restart the menu bar app (or rerun the CLI) to use it.",
|
|
1335
|
+
cliUpdateFailed: "Update failed (exit {code}). Run it yourself: {command}",
|
|
1336
|
+
cliUpdateCheckFailed: "Could not reach the npm registry: {message}"
|
|
1274
1337
|
};
|
|
1275
1338
|
|
|
1276
1339
|
// src/i18n/zh.ts
|
|
@@ -1344,7 +1407,18 @@ var zh = {
|
|
|
1344
1407
|
never: "\u4ECE\u672A",
|
|
1345
1408
|
local: "\u672C\u673A",
|
|
1346
1409
|
remote: "\u5176\u4ED6\u8BBE\u5907",
|
|
1347
|
-
|
|
1410
|
+
update: "\u66F4\u65B0",
|
|
1411
|
+
updateAvailable: "\u66F4\u65B0\u5230 v{version}",
|
|
1412
|
+
updateNewVersion: "\u6709\u65B0\u7248\u672C {version}\uFF08\u5F53\u524D v{current}\uFF09",
|
|
1413
|
+
updateUpToDate: "\u5DF2\u662F\u6700\u65B0\u7248\u672C",
|
|
1414
|
+
updateChecking: "\u68C0\u67E5\u4E2D\u2026",
|
|
1415
|
+
updateInstalling: "\u5B89\u88C5\u4E2D\u2026",
|
|
1416
|
+
updateInstalled: "\u5DF2\u66F4\u65B0\u5230 v{version} \u2014 \u91CD\u542F\u540E\u751F\u6548",
|
|
1417
|
+
updateFailed: "\u66F4\u65B0\u5931\u8D25 \u2014 \u8BF7\u624B\u52A8\u6267\u884C\uFF1A",
|
|
1418
|
+
updateCheckFailed: "\u68C0\u67E5\u66F4\u65B0\u5931\u8D25\uFF1A{message}",
|
|
1419
|
+
checkForUpdates: "\u68C0\u67E5\u66F4\u65B0",
|
|
1420
|
+
releaseNotes: "\u66F4\u65B0\u65E5\u5FD7",
|
|
1421
|
+
cliUsage: `\u7528\u6CD5\uFF1Acodex-token-tracker [\u547D\u4EE4] [\u9009\u9879] \uFF08\u522B\u540D\uFF1Acodex-tracker\uFF09
|
|
1348
1422
|
|
|
1349
1423
|
\u547D\u4EE4\uFF1A
|
|
1350
1424
|
(\u65E0) \u542F\u52A8\u83DC\u5355\u680F\u5E94\u7528\uFF08\u65E0\u663E\u793A\u73AF\u5883\u65F6\u81EA\u52A8\u5207\u6362\u4E3A agent \u6A21\u5F0F\uFF09
|
|
@@ -1355,10 +1429,12 @@ var zh = {
|
|
|
1355
1429
|
logout \u65AD\u5F00\u672C\u8BBE\u5907
|
|
1356
1430
|
status \u6253\u5370\u4ECA\u65E5\u7528\u91CF\u3001\u5B9E\u65F6\u4F1A\u8BDD\u4E0E\u9650\u989D
|
|
1357
1431
|
paths \u663E\u793A\u68C0\u6D4B\u5230\u7684\u4F1A\u8BDD\u76EE\u5F55\uFF08Codex\u3001pi\u3001OpenCode\u3001Cline/Roo/Kilo\u3001Hermes\u3001\u81EA\u5B9A\u4E49\uFF09
|
|
1432
|
+
update \u5B89\u88C5\u6700\u65B0\u53D1\u5E03\u7248\u672C\uFF08--check \u53EA\u68C0\u67E5\u4E0D\u5B89\u88C5\uFF09
|
|
1358
1433
|
config get|set \u8BFB\u53D6\u6216\u4FEE\u6539\u8BBE\u7F6E\uFF08config set uploadIntervalSec 30\uFF0Cconfig set sources.pi false\uFF09
|
|
1359
1434
|
lang <en|zh|auto> \u8BBE\u7F6E\u663E\u793A\u8BED\u8A00
|
|
1360
1435
|
|
|
1361
1436
|
\u9009\u9879\uFF1A
|
|
1437
|
+
--check update\uFF1A\u53EA\u62A5\u544A\u6700\u65B0\u7248\u672C\uFF0C\u4E0D\u6267\u884C\u5B89\u88C5
|
|
1362
1438
|
--dashboard <url> \u4EEA\u8868\u76D8\u5730\u5740\uFF08\u81EA\u6258\u7BA1\uFF09
|
|
1363
1439
|
--background \u4EE5\u540E\u53F0\u65B9\u5F0F\u542F\u52A8\u83DC\u5355\u680F\u5E94\u7528\u5E76\u7ACB\u5373\u8FD4\u56DE
|
|
1364
1440
|
--version, -v \u663E\u793A\u7248\u672C
|
|
@@ -1367,6 +1443,8 @@ var zh = {
|
|
|
1367
1443
|
cliUnknownCommand: "\u672A\u77E5\u547D\u4EE4\uFF1A{command}",
|
|
1368
1444
|
cliNoDisplay: "\u672A\u68C0\u6D4B\u5230\u663E\u793A\u73AF\u5883 \u2014 \u4EE5 agent \u6A21\u5F0F\u542F\u52A8\uFF08\u684C\u9762\u73AF\u5883\u8BF7\u8FD0\u884C `codex-tracker menubar`\uFF09\u3002",
|
|
1369
1445
|
cliNoElectron: "\u672A\u5B89\u88C5 Electron\uFF08\u53EF\u9009\u4F9D\u8D56\uFF09\u2014 \u4EE5 agent \u6A21\u5F0F\u542F\u52A8\u3002",
|
|
1446
|
+
cliDownloadingElectron: "\u672A\u627E\u5230 Electron \u8FD0\u884C\u65F6\uFF0C\u6B63\u5728\u4E0B\u8F7D\uFF08\u7EA6 100 MB\uFF0C\u4EC5\u9700\u4E00\u6B21\uFF09\u2026",
|
|
1447
|
+
cliElectronDownloadFailed: "Electron \u4E0B\u8F7D\u5931\u8D25\u3002\u53EF\u91CD\u8BD5 `npm install -g codex-token-tracker --allow-scripts=electron` \u6216 `npm rebuild -g electron`\uFF1B\u65E0\u754C\u9762\u7684 `codex-tracker agent` \u65E0\u9700 Electron\u3002",
|
|
1370
1448
|
cliStartingMenubar: "\u6B63\u5728\u542F\u52A8\u83DC\u5355\u680F\u5E94\u7528\u2026",
|
|
1371
1449
|
cliLoginStart: "\u6B63\u5728\u5C06\u672C\u8BBE\u5907\u8FDE\u63A5\u5230 {dashboard}",
|
|
1372
1450
|
cliLoginCode: "\u4F60\u7684\u4EE3\u7801\uFF1A{code}",
|
|
@@ -1398,7 +1476,13 @@ var zh = {
|
|
|
1398
1476
|
cliConfigSet: "\u5DF2\u8BBE\u7F6E {key} = {value}",
|
|
1399
1477
|
cliConfigUnknownKey: "\u672A\u77E5\u914D\u7F6E\u9879\uFF1A{key}\u3002\u53EF\u7528\uFF1A{keys}",
|
|
1400
1478
|
cliLangSet: "\u8BED\u8A00\u5DF2\u8BBE\u7F6E\u4E3A {lang}",
|
|
1401
|
-
cliConfigDir: "\u914D\u7F6E\u76EE\u5F55\uFF1A{dir}"
|
|
1479
|
+
cliConfigDir: "\u914D\u7F6E\u76EE\u5F55\uFF1A{dir}",
|
|
1480
|
+
cliUpdateLatest: "codex-token-tracker {version} \u5DF2\u662F\u6700\u65B0\u7248\u672C\u3002",
|
|
1481
|
+
cliUpdateAvailable: "\u53D1\u73B0\u65B0\u7248\u672C\uFF1A{current} \u2192 {latest}",
|
|
1482
|
+
cliUpdateRunning: "\u6B63\u5728\u6267\u884C\uFF1A{command}",
|
|
1483
|
+
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
|
+
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}"
|
|
1402
1486
|
};
|
|
1403
1487
|
|
|
1404
1488
|
// src/i18n/index.ts
|
|
@@ -1845,21 +1929,21 @@ function findSessionInfo(storage, sessionID) {
|
|
|
1845
1929
|
const key = `${storage}|${sessionID}`;
|
|
1846
1930
|
const cached = sessionCache.get(key);
|
|
1847
1931
|
if (cached && (cached.info || Date.now() - cached.at < 6e4)) return cached.info;
|
|
1848
|
-
let
|
|
1932
|
+
let info2 = null;
|
|
1849
1933
|
const sessionDir = import_node_path6.default.join(storage, "session");
|
|
1850
1934
|
const direct = import_node_path6.default.join(sessionDir, "info", `${sessionID}.json`);
|
|
1851
|
-
if (import_node_fs4.default.existsSync(direct))
|
|
1852
|
-
if (!
|
|
1935
|
+
if (import_node_fs4.default.existsSync(direct)) info2 = readJsonFile(direct);
|
|
1936
|
+
if (!info2) {
|
|
1853
1937
|
for (const project of listDirs(sessionDir)) {
|
|
1854
1938
|
const p = import_node_path6.default.join(sessionDir, project, `${sessionID}.json`);
|
|
1855
1939
|
if (import_node_fs4.default.existsSync(p)) {
|
|
1856
|
-
|
|
1940
|
+
info2 = readJsonFile(p);
|
|
1857
1941
|
break;
|
|
1858
1942
|
}
|
|
1859
1943
|
}
|
|
1860
1944
|
}
|
|
1861
|
-
sessionCache.set(key, { at: Date.now(), info });
|
|
1862
|
-
return
|
|
1945
|
+
sessionCache.set(key, { at: Date.now(), info: info2 });
|
|
1946
|
+
return info2;
|
|
1863
1947
|
}
|
|
1864
1948
|
function openaiIsOAuth(dataDir) {
|
|
1865
1949
|
const file = import_node_path6.default.join(dataDir, "auth.json");
|
|
@@ -1929,16 +2013,16 @@ var opencodeSource = {
|
|
|
1929
2013
|
provider,
|
|
1930
2014
|
usage: { input, cached, cacheWrite, output, reasoning: t2.reasoning ?? 0, total: input + output, requests: 1 }
|
|
1931
2015
|
};
|
|
1932
|
-
const
|
|
1933
|
-
const cwd =
|
|
2016
|
+
const info2 = storage ? findSessionInfo(storage, m.sessionID) : null;
|
|
2017
|
+
const cwd = info2?.directory ?? null;
|
|
1934
2018
|
return {
|
|
1935
2019
|
sessionId: m.sessionID,
|
|
1936
2020
|
agent: file.root.agent,
|
|
1937
2021
|
provider,
|
|
1938
|
-
startedAt:
|
|
2022
|
+
startedAt: info2?.time?.created ?? ts2,
|
|
1939
2023
|
lastActivityAt: ts2,
|
|
1940
2024
|
cwd,
|
|
1941
|
-
projectName: projectNameOf(cwd) ??
|
|
2025
|
+
projectName: projectNameOf(cwd) ?? info2?.title ?? null,
|
|
1942
2026
|
originator: "opencode",
|
|
1943
2027
|
source: "opencode",
|
|
1944
2028
|
cliVersion: null,
|
|
@@ -2362,7 +2446,20 @@ var SessionStore = class {
|
|
|
2362
2446
|
|
|
2363
2447
|
// src/core/stats.ts
|
|
2364
2448
|
var LIVE_WINDOW_MS = 5 * 60 * 1e3;
|
|
2449
|
+
var RATE_WINDOW_MS = 6e4;
|
|
2450
|
+
var BURST_WINDOW_MS = 1e4;
|
|
2365
2451
|
var DAY3 = 864e5;
|
|
2452
|
+
function outputRate(events, now, windowMs, sessionStart) {
|
|
2453
|
+
const from = now - windowMs;
|
|
2454
|
+
let output = 0;
|
|
2455
|
+
for (const e of events) {
|
|
2456
|
+
if (e.ts <= from) continue;
|
|
2457
|
+
output += e.usage.output;
|
|
2458
|
+
}
|
|
2459
|
+
if (output <= 0) return 0;
|
|
2460
|
+
const elapsedSec = Math.max(1, (now - Math.max(from, sessionStart)) / 1e3);
|
|
2461
|
+
return output / elapsedSec;
|
|
2462
|
+
}
|
|
2366
2463
|
var PriceCache = class {
|
|
2367
2464
|
constructor(overrides) {
|
|
2368
2465
|
this.overrides = overrides;
|
|
@@ -2439,7 +2536,9 @@ function agentStats(sessions, since, prices) {
|
|
|
2439
2536
|
function computeStats(input) {
|
|
2440
2537
|
const now = input.now ?? Date.now();
|
|
2441
2538
|
const prices = new PriceCache(input.pricing);
|
|
2442
|
-
const sessions = input.sessions
|
|
2539
|
+
const sessions = input.sessions.map(
|
|
2540
|
+
(s) => s.events.every((e) => isOpenAIModel(e.model)) ? s : { ...s, events: s.events.filter((e) => isOpenAIModel(e.model)) }
|
|
2541
|
+
);
|
|
2443
2542
|
const allEvents = [];
|
|
2444
2543
|
const sessionCosts = /* @__PURE__ */ new Map();
|
|
2445
2544
|
let lastActivityAt = null;
|
|
@@ -2465,14 +2564,8 @@ function computeStats(input) {
|
|
|
2465
2564
|
let live = null;
|
|
2466
2565
|
const liveSession = sessions.filter((s) => s.events.length && now - s.lastActivityAt <= LIVE_WINDOW_MS).sort((a, b) => b.lastActivityAt - a.lastActivityAt)[0];
|
|
2467
2566
|
if (liveSession) {
|
|
2468
|
-
let t60 = 0;
|
|
2469
|
-
let t10 = 0;
|
|
2470
|
-
for (const e of liveSession.events) {
|
|
2471
|
-
const age = now - e.ts;
|
|
2472
|
-
if (age <= 6e4) t60 += e.usage.total;
|
|
2473
|
-
if (age <= 1e4) t10 += e.usage.total;
|
|
2474
|
-
}
|
|
2475
2567
|
const last = liveSession.events[liveSession.events.length - 1];
|
|
2568
|
+
const start = liveSession.startedAt || liveSession.events[0].ts;
|
|
2476
2569
|
live = {
|
|
2477
2570
|
sessionId: liveSession.sessionId,
|
|
2478
2571
|
agent: liveSession.agent,
|
|
@@ -2480,8 +2573,8 @@ function computeStats(input) {
|
|
|
2480
2573
|
model: liveSession.model,
|
|
2481
2574
|
startedAt: liveSession.startedAt,
|
|
2482
2575
|
lastEventAt: last.ts,
|
|
2483
|
-
tokensPerSecond:
|
|
2484
|
-
tokensPerSecond10s:
|
|
2576
|
+
tokensPerSecond: outputRate(liveSession.events, now, RATE_WINDOW_MS, start),
|
|
2577
|
+
tokensPerSecond10s: outputRate(liveSession.events, now, BURST_WINDOW_MS, start),
|
|
2485
2578
|
contextUsed: last.usage.input + last.usage.output,
|
|
2486
2579
|
contextWindow: liveSession.contextWindow,
|
|
2487
2580
|
sessionUsage: { ...liveSession.cumulative },
|
|
@@ -2508,6 +2601,7 @@ function computeStats(input) {
|
|
|
2508
2601
|
}
|
|
2509
2602
|
return {
|
|
2510
2603
|
buckets,
|
|
2604
|
+
sessions: sessions.filter((s) => s.events.length),
|
|
2511
2605
|
today,
|
|
2512
2606
|
week,
|
|
2513
2607
|
month,
|
|
@@ -3865,12 +3959,12 @@ function createApi(pathParts = []) {
|
|
|
3865
3959
|
`API path is expected to be of the form \`api.moduleName.functionName\`. Found: \`${found}\``
|
|
3866
3960
|
);
|
|
3867
3961
|
}
|
|
3868
|
-
const
|
|
3962
|
+
const path12 = pathParts.slice(0, -1).join("/");
|
|
3869
3963
|
const exportName = pathParts[pathParts.length - 1];
|
|
3870
3964
|
if (exportName === "default") {
|
|
3871
|
-
return
|
|
3965
|
+
return path12;
|
|
3872
3966
|
} else {
|
|
3873
|
-
return
|
|
3967
|
+
return path12 + ":" + exportName;
|
|
3874
3968
|
}
|
|
3875
3969
|
} else if (prop === Symbol.toStringTag) {
|
|
3876
3970
|
return "FunctionReference";
|
|
@@ -4034,8 +4128,8 @@ var require_constants = __commonJS({
|
|
|
4034
4128
|
});
|
|
4035
4129
|
var require_node_gyp_build = __commonJS({
|
|
4036
4130
|
"../node_modules/.pnpm/node-gyp-build@4.8.4/node_modules/node-gyp-build/node-gyp-build.js"(exports2, module2) {
|
|
4037
|
-
var
|
|
4038
|
-
var
|
|
4131
|
+
var fs10 = __require("fs");
|
|
4132
|
+
var path12 = __require("path");
|
|
4039
4133
|
var os8 = __require("os");
|
|
4040
4134
|
var runtimeRequire = typeof __webpack_require__ === "function" ? __non_webpack_require__ : __require;
|
|
4041
4135
|
var vars2 = process.config && process.config.variables || {};
|
|
@@ -4052,21 +4146,21 @@ var require_node_gyp_build = __commonJS({
|
|
|
4052
4146
|
return runtimeRequire(load.resolve(dir));
|
|
4053
4147
|
}
|
|
4054
4148
|
load.resolve = load.path = function(dir) {
|
|
4055
|
-
dir =
|
|
4149
|
+
dir = path12.resolve(dir || ".");
|
|
4056
4150
|
try {
|
|
4057
|
-
var name = runtimeRequire(
|
|
4151
|
+
var name = runtimeRequire(path12.join(dir, "package.json")).name.toUpperCase().replace(/-/g, "_");
|
|
4058
4152
|
if (process.env[name + "_PREBUILD"]) dir = process.env[name + "_PREBUILD"];
|
|
4059
4153
|
} catch (err) {
|
|
4060
4154
|
}
|
|
4061
4155
|
if (!prebuildsOnly) {
|
|
4062
|
-
var release = getFirst(
|
|
4156
|
+
var release = getFirst(path12.join(dir, "build/Release"), matchBuild);
|
|
4063
4157
|
if (release) return release;
|
|
4064
|
-
var debug = getFirst(
|
|
4158
|
+
var debug = getFirst(path12.join(dir, "build/Debug"), matchBuild);
|
|
4065
4159
|
if (debug) return debug;
|
|
4066
4160
|
}
|
|
4067
4161
|
var prebuild = resolve(dir);
|
|
4068
4162
|
if (prebuild) return prebuild;
|
|
4069
|
-
var nearby = resolve(
|
|
4163
|
+
var nearby = resolve(path12.dirname(process.execPath));
|
|
4070
4164
|
if (nearby) return nearby;
|
|
4071
4165
|
var target = [
|
|
4072
4166
|
"platform=" + platform,
|
|
@@ -4083,26 +4177,26 @@ var require_node_gyp_build = __commonJS({
|
|
|
4083
4177
|
].filter(Boolean).join(" ");
|
|
4084
4178
|
throw new Error("No native build was found for " + target + "\n loaded from: " + dir + "\n");
|
|
4085
4179
|
function resolve(dir2) {
|
|
4086
|
-
var tuples = readdirSync(
|
|
4180
|
+
var tuples = readdirSync(path12.join(dir2, "prebuilds")).map(parseTuple);
|
|
4087
4181
|
var tuple = tuples.filter(matchTuple(platform, arch)).sort(compareTuples)[0];
|
|
4088
4182
|
if (!tuple) return;
|
|
4089
|
-
var prebuilds =
|
|
4183
|
+
var prebuilds = path12.join(dir2, "prebuilds", tuple.name);
|
|
4090
4184
|
var parsed = readdirSync(prebuilds).map(parseTags);
|
|
4091
4185
|
var candidates = parsed.filter(matchTags(runtime, abi));
|
|
4092
4186
|
var winner = candidates.sort(compareTags(runtime))[0];
|
|
4093
|
-
if (winner) return
|
|
4187
|
+
if (winner) return path12.join(prebuilds, winner.file);
|
|
4094
4188
|
}
|
|
4095
4189
|
};
|
|
4096
4190
|
function readdirSync(dir) {
|
|
4097
4191
|
try {
|
|
4098
|
-
return
|
|
4192
|
+
return fs10.readdirSync(dir);
|
|
4099
4193
|
} catch (err) {
|
|
4100
4194
|
return [];
|
|
4101
4195
|
}
|
|
4102
4196
|
}
|
|
4103
4197
|
function getFirst(dir, filter) {
|
|
4104
4198
|
var files = readdirSync(dir).filter(filter);
|
|
4105
|
-
return files[0] &&
|
|
4199
|
+
return files[0] && path12.join(dir, files[0]);
|
|
4106
4200
|
}
|
|
4107
4201
|
function matchBuild(name) {
|
|
4108
4202
|
return /\.node$/.test(name);
|
|
@@ -4189,7 +4283,7 @@ var require_node_gyp_build = __commonJS({
|
|
|
4189
4283
|
return typeof window !== "undefined" && window.process && window.process.type === "renderer";
|
|
4190
4284
|
}
|
|
4191
4285
|
function isAlpine(platform2) {
|
|
4192
|
-
return platform2 === "linux" &&
|
|
4286
|
+
return platform2 === "linux" && fs10.existsSync("/etc/alpine-release");
|
|
4193
4287
|
}
|
|
4194
4288
|
load.parseTags = parseTags;
|
|
4195
4289
|
load.matchTags = matchTags;
|
|
@@ -7735,13 +7829,13 @@ var require_websocket_server = __commonJS({
|
|
|
7735
7829
|
}
|
|
7736
7830
|
}
|
|
7737
7831
|
if (this.options.verifyClient) {
|
|
7738
|
-
const
|
|
7832
|
+
const info2 = {
|
|
7739
7833
|
origin: req.headers[`${version2 === 8 ? "sec-websocket-origin" : "origin"}`],
|
|
7740
7834
|
secure: !!(req.socket.authorized || req.socket.encrypted),
|
|
7741
7835
|
req
|
|
7742
7836
|
};
|
|
7743
7837
|
if (this.options.verifyClient.length === 2) {
|
|
7744
|
-
this.options.verifyClient(
|
|
7838
|
+
this.options.verifyClient(info2, (verified, code2, message, headers) => {
|
|
7745
7839
|
if (!verified) {
|
|
7746
7840
|
return abortHandshake(socket, code2 || 401, message, headers);
|
|
7747
7841
|
}
|
|
@@ -7757,7 +7851,7 @@ var require_websocket_server = __commonJS({
|
|
|
7757
7851
|
});
|
|
7758
7852
|
return;
|
|
7759
7853
|
}
|
|
7760
|
-
if (!this.options.verifyClient(
|
|
7854
|
+
if (!this.options.verifyClient(info2)) return abortHandshake(socket, 401);
|
|
7761
7855
|
}
|
|
7762
7856
|
this.completeUpgrade(extensions, key, protocols, req, socket, head, cb);
|
|
7763
7857
|
}
|
|
@@ -8963,10 +9057,174 @@ async function fetchLiveRateLimits(appVersion, timeoutMs = 1e4) {
|
|
|
8963
9057
|
}
|
|
8964
9058
|
}
|
|
8965
9059
|
|
|
9060
|
+
// src/core/update.ts
|
|
9061
|
+
var import_node_fs8 = __toESM(require("node:fs"));
|
|
9062
|
+
var import_node_path10 = __toESM(require("node:path"));
|
|
9063
|
+
var import_node_child_process3 = require("node:child_process");
|
|
9064
|
+
|
|
8966
9065
|
// src/version.ts
|
|
8967
|
-
var APP_VERSION = true ? "0.
|
|
9066
|
+
var APP_VERSION = true ? "0.2.0" : "0.0.0-dev";
|
|
8968
9067
|
var APP_NAME = "Codex Tracker";
|
|
8969
9068
|
|
|
9069
|
+
// src/core/update.ts
|
|
9070
|
+
var NPM_PACKAGE = "codex-token-tracker";
|
|
9071
|
+
var CHECK_TTL_MS = 6 * 60 * 60 * 1e3;
|
|
9072
|
+
var REQUEST_TIMEOUT_MS = 8e3;
|
|
9073
|
+
var INSTALL_TIMEOUT_MS = 10 * 60 * 1e3;
|
|
9074
|
+
var LOG_TAIL_CHARS = 4e3;
|
|
9075
|
+
function parseVersion(v2) {
|
|
9076
|
+
const s = String(v2 ?? "").trim().replace(/^v/, "");
|
|
9077
|
+
const i2 = s.indexOf("-");
|
|
9078
|
+
const core = i2 === -1 ? s : s.slice(0, i2);
|
|
9079
|
+
const pre = i2 === -1 ? "" : s.slice(i2 + 1);
|
|
9080
|
+
const n = core.split(".").map((x) => Number.parseInt(x, 10) || 0);
|
|
9081
|
+
return { nums: [n[0] ?? 0, n[1] ?? 0, n[2] ?? 0], pre };
|
|
9082
|
+
}
|
|
9083
|
+
function compareVersions(a, b) {
|
|
9084
|
+
const A = parseVersion(a);
|
|
9085
|
+
const B = parseVersion(b);
|
|
9086
|
+
for (let i2 = 0; i2 < 3; i2++) if (A.nums[i2] !== B.nums[i2]) return A.nums[i2] < B.nums[i2] ? -1 : 1;
|
|
9087
|
+
if (A.pre === B.pre) return 0;
|
|
9088
|
+
if (!A.pre) return 1;
|
|
9089
|
+
if (!B.pre) return -1;
|
|
9090
|
+
return A.pre < B.pre ? -1 : 1;
|
|
9091
|
+
}
|
|
9092
|
+
function isDevBuild(version2) {
|
|
9093
|
+
return version2.startsWith("0.0.0");
|
|
9094
|
+
}
|
|
9095
|
+
function registryBase() {
|
|
9096
|
+
const r = process.env.CODEX_TRACKER_REGISTRY || process.env.npm_config_registry || process.env.NPM_CONFIG_REGISTRY || "https://registry.npmjs.org";
|
|
9097
|
+
return r.replace(/\/+$/, "");
|
|
9098
|
+
}
|
|
9099
|
+
async function fetchLatestVersion(signal) {
|
|
9100
|
+
const ctrl = new AbortController();
|
|
9101
|
+
const timer = setTimeout(() => ctrl.abort(), REQUEST_TIMEOUT_MS);
|
|
9102
|
+
signal?.addEventListener("abort", () => ctrl.abort(), { once: true });
|
|
9103
|
+
try {
|
|
9104
|
+
const res = await fetch(`${registryBase()}/-/package/${NPM_PACKAGE}/dist-tags`, {
|
|
9105
|
+
signal: ctrl.signal,
|
|
9106
|
+
headers: { accept: "application/json" }
|
|
9107
|
+
});
|
|
9108
|
+
if (!res.ok) throw new Error(`registry responded ${res.status}`);
|
|
9109
|
+
const json = await res.json();
|
|
9110
|
+
const latest = json?.latest;
|
|
9111
|
+
if (typeof latest !== "string" || !latest) throw new Error("registry returned no `latest` tag");
|
|
9112
|
+
return latest;
|
|
9113
|
+
} finally {
|
|
9114
|
+
clearTimeout(timer);
|
|
9115
|
+
}
|
|
9116
|
+
}
|
|
9117
|
+
function detectPackageManager(installDir = import_node_path10.default.resolve(__dirname, "..")) {
|
|
9118
|
+
const p = installDir.replace(/\\/g, "/").toLowerCase();
|
|
9119
|
+
if (/\/\.?bun\//.test(p)) return "bun";
|
|
9120
|
+
if (/\/\.?pnpm[/-]/.test(p)) return "pnpm";
|
|
9121
|
+
if (/\/\.?yarn\//.test(p)) return "yarn";
|
|
9122
|
+
const ua = (process.env.npm_config_user_agent ?? "").toLowerCase();
|
|
9123
|
+
if (ua.startsWith("pnpm")) return "pnpm";
|
|
9124
|
+
if (ua.startsWith("yarn")) return "yarn";
|
|
9125
|
+
if (ua.startsWith("bun")) return "bun";
|
|
9126
|
+
return "npm";
|
|
9127
|
+
}
|
|
9128
|
+
function updateArgs(pm, spec = `${NPM_PACKAGE}@latest`) {
|
|
9129
|
+
switch (pm) {
|
|
9130
|
+
case "pnpm":
|
|
9131
|
+
return ["add", "-g", spec];
|
|
9132
|
+
case "yarn":
|
|
9133
|
+
return ["global", "add", spec];
|
|
9134
|
+
case "bun":
|
|
9135
|
+
return ["add", "-g", spec];
|
|
9136
|
+
default:
|
|
9137
|
+
return ["install", "-g", spec];
|
|
9138
|
+
}
|
|
9139
|
+
}
|
|
9140
|
+
function updateCommand(pm, spec) {
|
|
9141
|
+
return `${pm} ${updateArgs(pm, spec).join(" ")}`;
|
|
9142
|
+
}
|
|
9143
|
+
function cachePath() {
|
|
9144
|
+
return import_node_path10.default.join(configDir(), "update.json");
|
|
9145
|
+
}
|
|
9146
|
+
function readCache() {
|
|
9147
|
+
try {
|
|
9148
|
+
const raw = JSON.parse(import_node_fs8.default.readFileSync(cachePath(), "utf8"));
|
|
9149
|
+
if (typeof raw?.latest === "string" && typeof raw.checkedAt === "number") {
|
|
9150
|
+
return { latest: raw.latest, checkedAt: raw.checkedAt };
|
|
9151
|
+
}
|
|
9152
|
+
} catch {
|
|
9153
|
+
}
|
|
9154
|
+
return null;
|
|
9155
|
+
}
|
|
9156
|
+
function writeCache(c) {
|
|
9157
|
+
try {
|
|
9158
|
+
import_node_fs8.default.mkdirSync(import_node_path10.default.dirname(cachePath()), { recursive: true });
|
|
9159
|
+
import_node_fs8.default.writeFileSync(cachePath(), JSON.stringify(c, null, 2));
|
|
9160
|
+
} catch {
|
|
9161
|
+
}
|
|
9162
|
+
}
|
|
9163
|
+
function info(current, latest, checkedAt, error) {
|
|
9164
|
+
const pm = detectPackageManager();
|
|
9165
|
+
return {
|
|
9166
|
+
current,
|
|
9167
|
+
latest,
|
|
9168
|
+
available: Boolean(latest) && !isDevBuild(current) && compareVersions(latest, current) > 0,
|
|
9169
|
+
checkedAt,
|
|
9170
|
+
error,
|
|
9171
|
+
packageManager: pm,
|
|
9172
|
+
command: updateCommand(pm)
|
|
9173
|
+
};
|
|
9174
|
+
}
|
|
9175
|
+
async function checkForUpdate(opts = {}) {
|
|
9176
|
+
const current = opts.current ?? APP_VERSION;
|
|
9177
|
+
const cached = readCache();
|
|
9178
|
+
if (!opts.force && cached && Date.now() - cached.checkedAt < CHECK_TTL_MS) {
|
|
9179
|
+
return info(current, cached.latest, cached.checkedAt, null);
|
|
9180
|
+
}
|
|
9181
|
+
try {
|
|
9182
|
+
const latest = await fetchLatestVersion(opts.signal);
|
|
9183
|
+
const checkedAt = Date.now();
|
|
9184
|
+
writeCache({ latest, checkedAt });
|
|
9185
|
+
return info(current, latest, checkedAt, null);
|
|
9186
|
+
} catch (err) {
|
|
9187
|
+
return info(current, cached?.latest ?? null, cached?.checkedAt ?? null, err.message);
|
|
9188
|
+
}
|
|
9189
|
+
}
|
|
9190
|
+
function runUpdate(opts = {}) {
|
|
9191
|
+
const pm = detectPackageManager();
|
|
9192
|
+
const spec = opts.version ? `${NPM_PACKAGE}@${opts.version}` : `${NPM_PACKAGE}@latest`;
|
|
9193
|
+
const args = updateArgs(pm, spec);
|
|
9194
|
+
const command = `${pm} ${args.join(" ")}`;
|
|
9195
|
+
return new Promise((resolve) => {
|
|
9196
|
+
let output = "";
|
|
9197
|
+
const collect = (chunk) => {
|
|
9198
|
+
const text = chunk.toString();
|
|
9199
|
+
opts.onOutput?.(text);
|
|
9200
|
+
output = (output + text).slice(-LOG_TAIL_CHARS);
|
|
9201
|
+
};
|
|
9202
|
+
let child;
|
|
9203
|
+
try {
|
|
9204
|
+
child = (0, import_node_child_process3.spawn)(pm, args, {
|
|
9205
|
+
// Electron's bundled Node has no shell PATH resolution for `npm.cmd` on Windows.
|
|
9206
|
+
shell: process.platform === "win32",
|
|
9207
|
+
env: { ...process.env, ELECTRON_RUN_AS_NODE: void 0 },
|
|
9208
|
+
windowsHide: true
|
|
9209
|
+
});
|
|
9210
|
+
} catch (err) {
|
|
9211
|
+
resolve({ ok: false, code: null, command, output: err.message });
|
|
9212
|
+
return;
|
|
9213
|
+
}
|
|
9214
|
+
const timer = setTimeout(() => child.kill(), INSTALL_TIMEOUT_MS);
|
|
9215
|
+
child.stdout?.on("data", collect);
|
|
9216
|
+
child.stderr?.on("data", collect);
|
|
9217
|
+
child.on("error", (err) => {
|
|
9218
|
+
clearTimeout(timer);
|
|
9219
|
+
resolve({ ok: false, code: null, command, output: output + err.message });
|
|
9220
|
+
});
|
|
9221
|
+
child.on("close", (code2) => {
|
|
9222
|
+
clearTimeout(timer);
|
|
9223
|
+
resolve({ ok: code2 === 0, code: code2, command, output });
|
|
9224
|
+
});
|
|
9225
|
+
});
|
|
9226
|
+
}
|
|
9227
|
+
|
|
8970
9228
|
// src/core/engine.ts
|
|
8971
9229
|
var SHALLOW_MS = 3e3;
|
|
8972
9230
|
var DEEP_MS = 6e4;
|
|
@@ -8974,6 +9232,7 @@ var TICK_MS = 2e3;
|
|
|
8974
9232
|
var REMOTE_MS = 6e4;
|
|
8975
9233
|
var LIVE_LIMITS_DEBOUNCE_MS = 1e4;
|
|
8976
9234
|
var LIVE_LIMITS_MIN_GAP_MS = 2e4;
|
|
9235
|
+
var UPDATE_CHECK_MS = 6 * 60 * 60 * 1e3;
|
|
8977
9236
|
var Engine = class extends import_node_events.EventEmitter {
|
|
8978
9237
|
constructor(opts) {
|
|
8979
9238
|
super();
|
|
@@ -9010,6 +9269,9 @@ var Engine = class extends import_node_events.EventEmitter {
|
|
|
9010
9269
|
liveLimitsInFlight = false;
|
|
9011
9270
|
liveLimitsTimer = null;
|
|
9012
9271
|
lastLiveLimitsAttempt = 0;
|
|
9272
|
+
update = null;
|
|
9273
|
+
updateStatus = "idle";
|
|
9274
|
+
updateLog = null;
|
|
9013
9275
|
get heatmapWeeks() {
|
|
9014
9276
|
return this.opts.heatmapWeeks ?? 16;
|
|
9015
9277
|
}
|
|
@@ -9031,6 +9293,8 @@ var Engine = class extends import_node_events.EventEmitter {
|
|
|
9031
9293
|
return;
|
|
9032
9294
|
}
|
|
9033
9295
|
void this.refreshLiveLimits(true);
|
|
9296
|
+
void this.checkUpdate(false);
|
|
9297
|
+
this.timers.push(setInterval(() => void this.checkUpdate(false), UPDATE_CHECK_MS));
|
|
9034
9298
|
this.store.startWatching(() => void this.refresh(false));
|
|
9035
9299
|
this.timers.push(setInterval(() => void this.refresh(false), SHALLOW_MS));
|
|
9036
9300
|
this.timers.push(setInterval(() => void this.refresh(true), DEEP_MS));
|
|
@@ -9137,7 +9401,7 @@ var Engine = class extends import_node_events.EventEmitter {
|
|
|
9137
9401
|
if (!this.signedIn) return { buckets: 0, sessions: 0 };
|
|
9138
9402
|
if (!this.stats) this.recompute();
|
|
9139
9403
|
try {
|
|
9140
|
-
const r = await this.uploader.pushAll(this.stats.buckets, this.
|
|
9404
|
+
const r = await this.uploader.pushAll(this.stats.buckets, this.stats.sessions, this.stats.sessionCosts);
|
|
9141
9405
|
this.emitSnapshot();
|
|
9142
9406
|
return r;
|
|
9143
9407
|
} catch (err) {
|
|
@@ -9161,6 +9425,48 @@ var Engine = class extends import_node_events.EventEmitter {
|
|
|
9161
9425
|
await this.uploader.fetchRemote(this.heatmapWeeks);
|
|
9162
9426
|
this.recompute();
|
|
9163
9427
|
}
|
|
9428
|
+
/**
|
|
9429
|
+
* Ask the npm registry for the newest published version (cached for 6 h unless `force`).
|
|
9430
|
+
* Best-effort: a failure is recorded on the snapshot, never thrown.
|
|
9431
|
+
*/
|
|
9432
|
+
async checkUpdate(force) {
|
|
9433
|
+
if (!this.config.checkUpdates) {
|
|
9434
|
+
this.update = null;
|
|
9435
|
+
return null;
|
|
9436
|
+
}
|
|
9437
|
+
if (this.updateStatus === "checking" || this.updateStatus === "installing") return this.update;
|
|
9438
|
+
this.updateStatus = "checking";
|
|
9439
|
+
this.emitSnapshot();
|
|
9440
|
+
this.update = await checkForUpdate({ force });
|
|
9441
|
+
this.updateStatus = "idle";
|
|
9442
|
+
if (this.update.error) this.opts.log?.(`update check failed: ${this.update.error}`);
|
|
9443
|
+
this.emitSnapshot();
|
|
9444
|
+
return this.update;
|
|
9445
|
+
}
|
|
9446
|
+
/**
|
|
9447
|
+
* Install the newest version globally with the package manager this copy came from.
|
|
9448
|
+
* The new code only takes effect once the app is restarted, so the UI says so rather than
|
|
9449
|
+
* pretending to hot-swap itself.
|
|
9450
|
+
*/
|
|
9451
|
+
async installUpdate() {
|
|
9452
|
+
if (this.updateStatus === "installing") return false;
|
|
9453
|
+
if (!this.update?.available) await this.checkUpdate(true);
|
|
9454
|
+
if (!this.update?.available) return false;
|
|
9455
|
+
this.updateStatus = "installing";
|
|
9456
|
+
this.updateLog = null;
|
|
9457
|
+
this.emitSnapshot();
|
|
9458
|
+
const r = await runUpdate({ version: this.update.latest ?? void 0 });
|
|
9459
|
+
this.updateStatus = r.ok ? "installed" : "failed";
|
|
9460
|
+
this.updateLog = r.ok ? null : `${r.command}
|
|
9461
|
+
${r.output}`.trim();
|
|
9462
|
+
if (!r.ok) this.opts.log?.(`update install failed (${r.code}): ${r.output}`);
|
|
9463
|
+
this.emitSnapshot();
|
|
9464
|
+
return r.ok;
|
|
9465
|
+
}
|
|
9466
|
+
updateState() {
|
|
9467
|
+
if (!this.config.checkUpdates || !this.update) return null;
|
|
9468
|
+
return { ...this.update, status: this.updateStatus, log: this.updateLog };
|
|
9469
|
+
}
|
|
9164
9470
|
/** Start the device-code login flow (resolves when approved / denied / expired / cancelled). */
|
|
9165
9471
|
async login(openBrowser, onCode) {
|
|
9166
9472
|
if (this.pending) return { status: "cancelled" };
|
|
@@ -9253,6 +9559,7 @@ var Engine = class extends import_node_events.EventEmitter {
|
|
|
9253
9559
|
rateLimits: this.liveLimits ?? fromLogRateLimits(s.logRateLimits),
|
|
9254
9560
|
rateLimitsError: this.config.liveRateLimits ? this.liveLimitsError : null,
|
|
9255
9561
|
rateLimitsUpdatedAt: this.liveLimits ? this.liveLimitsAt : s.logRateLimits?.observedAt ?? null,
|
|
9562
|
+
update: this.updateState(),
|
|
9256
9563
|
modelsToday: s.modelsToday,
|
|
9257
9564
|
modelsMonth: s.modelsMonth,
|
|
9258
9565
|
byAgentToday: s.byAgentToday,
|
|
@@ -9277,9 +9584,9 @@ var Engine = class extends import_node_events.EventEmitter {
|
|
|
9277
9584
|
|
|
9278
9585
|
// src/main.ts
|
|
9279
9586
|
var DEBUG = Boolean(process.env.CODEX_TRACKER_DEBUG);
|
|
9280
|
-
var assetsDir =
|
|
9281
|
-
var rendererIndex =
|
|
9282
|
-
var preloadPath =
|
|
9587
|
+
var assetsDir = import_node_path11.default.join(__dirname, "..", "assets");
|
|
9588
|
+
var rendererIndex = import_node_path11.default.join(__dirname, "renderer", "index.html");
|
|
9589
|
+
var preloadPath = import_node_path11.default.join(__dirname, "preload.js");
|
|
9283
9590
|
var isMac = process.platform === "darwin";
|
|
9284
9591
|
var isWin = process.platform === "win32";
|
|
9285
9592
|
var mb = null;
|
|
@@ -9290,12 +9597,12 @@ function log2(...args) {
|
|
|
9290
9597
|
}
|
|
9291
9598
|
function trayIcon() {
|
|
9292
9599
|
if (isMac) {
|
|
9293
|
-
const img2 = import_electron.nativeImage.createFromPath(
|
|
9600
|
+
const img2 = import_electron.nativeImage.createFromPath(import_node_path11.default.join(assetsDir, "trayTemplate.png"));
|
|
9294
9601
|
img2.setTemplateImage(true);
|
|
9295
9602
|
return img2;
|
|
9296
9603
|
}
|
|
9297
9604
|
const file = import_electron.nativeTheme.shouldUseDarkColors || !isWin ? "tray-win.png" : "tray-win-light.png";
|
|
9298
|
-
const img = import_electron.nativeImage.createFromPath(
|
|
9605
|
+
const img = import_electron.nativeImage.createFromPath(import_node_path11.default.join(assetsDir, file));
|
|
9299
9606
|
return isWin ? img.resize({ width: 16, height: 16 }) : img;
|
|
9300
9607
|
}
|
|
9301
9608
|
function trayText(s) {
|
|
@@ -9315,7 +9622,7 @@ function updateTray(s) {
|
|
|
9315
9622
|
mb.tray.setToolTip(`${APP_NAME} \u2014 ${t(L, "today")}: ${formatTokens(s.today.usage.total)} \xB7 ${formatUSD(s.today.cost)}${live}`);
|
|
9316
9623
|
if (isMac) mb.tray.setTitle(trayText(s), { fontType: "monospacedDigit" });
|
|
9317
9624
|
}
|
|
9318
|
-
var launchAgentPath =
|
|
9625
|
+
var launchAgentPath = import_node_path11.default.join(import_node_os7.default.homedir(), "Library", "LaunchAgents", "dev.codex-tracker.menubar.plist");
|
|
9319
9626
|
function setLaunchAtLogin(enabled) {
|
|
9320
9627
|
if (isMac) {
|
|
9321
9628
|
if (enabled) {
|
|
@@ -9323,20 +9630,20 @@ function setLaunchAtLogin(enabled) {
|
|
|
9323
9630
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
9324
9631
|
<plist version="1.0"><dict>
|
|
9325
9632
|
<key>Label</key><string>dev.codex-tracker.menubar</string>
|
|
9326
|
-
<key>ProgramArguments</key><array><string>${process.execPath}</string><string>${
|
|
9633
|
+
<key>ProgramArguments</key><array><string>${process.execPath}</string><string>${import_node_path11.default.join(__dirname, "main.js")}</string></array>
|
|
9327
9634
|
<key>RunAtLoad</key><true/>
|
|
9328
9635
|
<key>KeepAlive</key><false/>
|
|
9329
9636
|
<key>ProcessType</key><string>Interactive</string>
|
|
9330
9637
|
</dict></plist>
|
|
9331
9638
|
`;
|
|
9332
|
-
|
|
9333
|
-
|
|
9334
|
-
(0,
|
|
9639
|
+
import_node_fs9.default.mkdirSync(import_node_path11.default.dirname(launchAgentPath), { recursive: true });
|
|
9640
|
+
import_node_fs9.default.writeFileSync(launchAgentPath, plist);
|
|
9641
|
+
(0, import_node_child_process4.execFile)("launchctl", ["load", "-w", launchAgentPath], () => {
|
|
9335
9642
|
});
|
|
9336
|
-
} else if (
|
|
9337
|
-
(0,
|
|
9643
|
+
} else if (import_node_fs9.default.existsSync(launchAgentPath)) {
|
|
9644
|
+
(0, import_node_child_process4.execFile)("launchctl", ["unload", "-w", launchAgentPath], () => {
|
|
9338
9645
|
try {
|
|
9339
|
-
|
|
9646
|
+
import_node_fs9.default.unlinkSync(launchAgentPath);
|
|
9340
9647
|
} catch {
|
|
9341
9648
|
}
|
|
9342
9649
|
});
|
|
@@ -9344,7 +9651,7 @@ function setLaunchAtLogin(enabled) {
|
|
|
9344
9651
|
return;
|
|
9345
9652
|
}
|
|
9346
9653
|
if (isWin) {
|
|
9347
|
-
import_electron.app.setLoginItemSettings({ openAtLogin: enabled, path: process.execPath, args: [
|
|
9654
|
+
import_electron.app.setLoginItemSettings({ openAtLogin: enabled, path: process.execPath, args: [import_node_path11.default.join(__dirname, "main.js")] });
|
|
9348
9655
|
}
|
|
9349
9656
|
}
|
|
9350
9657
|
function buildMenu(s) {
|
|
@@ -9381,6 +9688,9 @@ function buildMenu(s) {
|
|
|
9381
9688
|
}
|
|
9382
9689
|
] : [],
|
|
9383
9690
|
{ label: t(L, "refresh"), click: () => void engine?.refresh(true) },
|
|
9691
|
+
...s.update ? [
|
|
9692
|
+
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
|
+
] : [],
|
|
9384
9694
|
{ type: "separator" },
|
|
9385
9695
|
{ label: t(L, "quit"), click: () => import_electron.app.quit() }
|
|
9386
9696
|
];
|
|
@@ -9437,6 +9747,8 @@ if (!import_electron.app.requestSingleInstanceLock()) {
|
|
|
9437
9747
|
import_electron.ipcMain.handle("auth:cancel", () => engine?.cancelLogin());
|
|
9438
9748
|
import_electron.ipcMain.handle("auth:logout", () => engine?.logout());
|
|
9439
9749
|
import_electron.ipcMain.handle("refresh", () => engine?.refresh(true).then(() => void 0));
|
|
9750
|
+
import_electron.ipcMain.handle("update:check", () => engine?.checkUpdate(true).then(() => void 0));
|
|
9751
|
+
import_electron.ipcMain.handle("update:install", () => engine?.installUpdate().then(() => void 0));
|
|
9440
9752
|
import_electron.ipcMain.handle("quit", () => import_electron.app.quit());
|
|
9441
9753
|
mb.on("ready", async () => {
|
|
9442
9754
|
if (isMac) import_electron.app.dock?.hide();
|
|
@@ -9462,7 +9774,7 @@ if (!import_electron.app.requestSingleInstanceLock()) {
|
|
|
9462
9774
|
setTimeout(async () => {
|
|
9463
9775
|
try {
|
|
9464
9776
|
const img = await mb.window.webContents.capturePage();
|
|
9465
|
-
|
|
9777
|
+
import_node_fs9.default.writeFileSync(shot, img.toPNG());
|
|
9466
9778
|
log2("screenshot written", shot);
|
|
9467
9779
|
} catch (err) {
|
|
9468
9780
|
log2("screenshot failed", errorMessage(err));
|
|
@@ -9478,7 +9790,7 @@ if (!import_electron.app.requestSingleInstanceLock()) {
|
|
|
9478
9790
|
return { action: "deny" };
|
|
9479
9791
|
});
|
|
9480
9792
|
win.webContents.on("console-message", (_e, level, message, line, sourceId) => {
|
|
9481
|
-
if (DEBUG || level >= 2) console.error(`[renderer:${level}] ${message} (${
|
|
9793
|
+
if (DEBUG || level >= 2) console.error(`[renderer:${level}] ${message} (${import_node_path11.default.basename(sourceId)}:${line})`);
|
|
9482
9794
|
});
|
|
9483
9795
|
win.webContents.on("render-process-gone", (_e, details) => log2("renderer gone", details));
|
|
9484
9796
|
if (process.env.CODEX_TRACKER_DEVTOOLS) win.webContents.openDevTools({ mode: "detach" });
|