codex-token-tracker 0.2.0 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +52 -0
- package/README.md +63 -2
- package/dist/cli.js +261 -15
- package/dist/main.js +225 -18
- package/dist/preload.js +1 -0
- package/dist/renderer/renderer.js +110 -3
- package/dist/renderer/styles.css +99 -0
- package/package.json +14 -14
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,58 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to `codex-token-tracker`. This project follows [Semantic Versioning](https://semver.org/).
|
|
4
4
|
|
|
5
|
+
## 0.2.1 — 2026-09-01
|
|
6
|
+
|
|
7
|
+
A manual **Sync** button that recalibrates this device's numbers on the dashboard.
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
|
|
11
|
+
- **Full sync.** The popover header gets a **⟳** button (also *Sync now* in the Account card and the tray
|
|
12
|
+
menu, and `codex-token-tracker sync` on the CLI). Unlike the background upload — which every 60 s pushes
|
|
13
|
+
only the hour buckets and sessions that changed — a full sync throws away every cache on the way:
|
|
14
|
+
1. re-reads the config, so agents enabled since start-up are picked up;
|
|
15
|
+
2. re-discovers every session directory and re-parses every transcript from scratch — Codex plus the
|
|
16
|
+
coding agents running on your Codex subscription (pi, OpenCode, Cline / Roo / Kilo, Hermes) and any
|
|
17
|
+
configured `extraSessionDirs` — instead of skipping files whose size and mtime are unchanged;
|
|
18
|
+
3. recomputes the aggregates with the current pricing table;
|
|
19
|
+
4. re-uploads **everything**, so the dashboard's totals for this device are replaced by the freshly
|
|
20
|
+
computed ones;
|
|
21
|
+
5. pulls the other devices' rows and the live rate limits back down.
|
|
22
|
+
|
|
23
|
+
Use it when a machine's numbers look wrong, after installing a new agent, or after an update that
|
|
24
|
+
changes the pricing table. It is manual only — never on a timer — because it re-sends the whole history.
|
|
25
|
+
- A banner in the popover reports the phase while a sync runs (*Rescanning every agent's transcripts…*,
|
|
26
|
+
*Re-uploading this device's history…*) and then the result, e.g. `Synced codex, pi · 75 files ·
|
|
27
|
+
75 sessions · 70 hours re-uploaded`. Signed out, the rescan still runs and nothing leaves the machine.
|
|
28
|
+
- `codex-token-tracker sync` prints the same summary and exits non-zero if the sync failed.
|
|
29
|
+
|
|
30
|
+
- **Dev builds target the dev environment.** A build now carries its channel, stamped by
|
|
31
|
+
`scripts/build.mjs`: only `--release` (what `prepack` runs) produces a production build, so every
|
|
32
|
+
published tarball is prod and every local `pnpm build` / `pnpm dev` is dev. A dev build
|
|
33
|
+
- defaults to the local dashboard `http://localhost:3000` instead of `https://codex.chenli.dev`, and
|
|
34
|
+
therefore uploads to whatever Convex deployment that dashboard advertises — the dev one;
|
|
35
|
+
- keeps its config, device token and upload state in `~/.codex-tracker-dev`, so a local run can
|
|
36
|
+
never overwrite the production token or the record of what was already pushed to production;
|
|
37
|
+
- defaults `checkUpdates` to `false` and refuses `codex-token-tracker update`, which would otherwise
|
|
38
|
+
install the published package over the checkout;
|
|
39
|
+
- runs as `Codex Tracker (dev)` with its own Electron userData directory and macOS LaunchAgent, so it
|
|
40
|
+
can run **beside** an installed copy instead of fighting it for the single-instance lock;
|
|
41
|
+
- shows an orange **DEV** badge in the popover header and names its dashboard and config directory in
|
|
42
|
+
`--version`, `status` and `paths`.
|
|
43
|
+
|
|
44
|
+
`--dashboard <url>`, `config set dashboardUrl` and `CODEX_TRACKER_HOME` still override all of it.
|
|
45
|
+
|
|
46
|
+
### Changed
|
|
47
|
+
|
|
48
|
+
- `Uploader.pushAll()` takes a `{ full }` option that clears the pushed-buckets/pushed-sessions record
|
|
49
|
+
before building the payload, and waits for an in-flight incremental push instead of skipping the call.
|
|
50
|
+
- `SessionStore.reset()` drops the parsed-file index so a following deep refresh re-reads everything.
|
|
51
|
+
- Packaging scripts: `build:release` builds with the prod channel; `prepack` runs it (so `npm pack` and
|
|
52
|
+
`npm publish` always ship a prod build) and `postpack` restores the dev build in `dist/`.
|
|
53
|
+
`prepublishOnly` now only typechecks.
|
|
54
|
+
- `app.setName()` moved ahead of `requestSingleInstanceLock()` — the lock is keyed on the userData
|
|
55
|
+
directory, which is derived from the name.
|
|
56
|
+
|
|
5
57
|
## 0.2.0 — 2026-09-01
|
|
6
58
|
|
|
7
59
|
Pricing accuracy, an honest tokens/s number, and self-update.
|
package/README.md
CHANGED
|
@@ -61,6 +61,7 @@ The tracker discovers the Convex deployment through `<dashboard>/api/config`.
|
|
|
61
61
|
| `codex-tracker login [--dashboard <url>]` | Device-code login |
|
|
62
62
|
| `codex-tracker logout` | Forget the device token (local data stays) |
|
|
63
63
|
| `codex-tracker status [--json]` | Today / 7d / 30d usage, live session, rate limits, models |
|
|
64
|
+
| `codex-tracker sync` | Full sync: rescan every agent and re-upload this device's whole history |
|
|
64
65
|
| `codex-tracker paths` | Detected Codex session directories |
|
|
65
66
|
| `codex-tracker config get [key]` / `config set <key> <value>` | Settings (see below) |
|
|
66
67
|
| `codex-tracker lang <en\|zh\|auto>` | Display language |
|
|
@@ -71,10 +72,35 @@ The tracker discovers the Convex deployment through `<dashboard>/api/config`.
|
|
|
71
72
|
|
|
72
73
|
- Tray title shows today's tokens (`12.4k`) — `config set trayTitle tokens|cost|none`
|
|
73
74
|
- Click: popover with Today, Live session (tokens/s, context window, rate limits), Activity heatmap, Models, Account
|
|
74
|
-
- Right-click: Open dashboard, Sign in/out, Language, Launch at login, Refresh, Check for updates, Quit
|
|
75
|
+
- Right-click: Open dashboard, Sign in/out, Language, Launch at login, Refresh, **Sync now**, Check for updates, Quit
|
|
75
76
|
- A banner appears at the top of the popover when a newer version is published; **Update** installs it and the app asks you to restart
|
|
76
77
|
- Launch at login uses a LaunchAgent on macOS and the registry run key on Windows
|
|
77
78
|
|
|
79
|
+
## Sync
|
|
80
|
+
|
|
81
|
+
The tracker uploads continuously in the background: every 60 s it sends the hour buckets and sessions
|
|
82
|
+
that *changed* since the last push. **Sync** does the full version instead — use it when the dashboard's
|
|
83
|
+
numbers for this machine look wrong, after you install a new agent, or after an update that changes the
|
|
84
|
+
pricing table.
|
|
85
|
+
|
|
86
|
+
Press the **⟳** button in the popover header (or *Sync now* in the Account card and the tray menu, or run
|
|
87
|
+
`codex-tracker sync`). It:
|
|
88
|
+
|
|
89
|
+
1. re-reads the config, so agents enabled since the app started are picked up;
|
|
90
|
+
2. re-discovers **every** session directory and re-parses **every** transcript from scratch — Codex plus
|
|
91
|
+
the coding agents running on your Codex subscription (pi, OpenCode, Cline / Roo / Kilo, Hermes) and any
|
|
92
|
+
`extraSessionDirs` you configured — instead of skipping files whose size and mtime are unchanged;
|
|
93
|
+
3. recomputes all aggregates with the current pricing table;
|
|
94
|
+
4. re-uploads **everything**, not just what changed, so the dashboard's totals for this device are
|
|
95
|
+
replaced by the freshly computed ones — this is the calibration step;
|
|
96
|
+
5. pulls the other devices' rows and the live rate limits back down.
|
|
97
|
+
|
|
98
|
+
A banner reports the phase while it runs and then what it found (`Synced codex, pi · 75 files ·
|
|
99
|
+
75 sessions · 70 hours re-uploaded`). Signed out, steps 1-3 still run and nothing leaves the machine.
|
|
100
|
+
|
|
101
|
+
A full sync re-sends your whole history, so it costs more bandwidth than a normal push — it is a manual
|
|
102
|
+
action, never on a timer.
|
|
103
|
+
|
|
78
104
|
## Windows and WSL2
|
|
79
105
|
|
|
80
106
|
**Native Windows**: `npm i -g codex-token-tracker` in PowerShell, then `codex-tracker`. The tray app also scans every WSL distro (`\\wsl$\<distro>\home\*\.codex\sessions`) so sessions run inside WSL are counted.
|
|
@@ -199,7 +225,42 @@ CODEX_TRACKER_DEVTOOLS=1 ... # op
|
|
|
199
225
|
pnpm --filter codex-token-tracker build:icons # regenerate tray icons
|
|
200
226
|
```
|
|
201
227
|
|
|
202
|
-
|
|
228
|
+
### Dev builds vs published builds
|
|
229
|
+
|
|
230
|
+
A build knows which environment it belongs to, so a local test run can never write into production.
|
|
231
|
+
`scripts/build.mjs` stamps the channel at bundle time: **only `--release` produces a prod build**, which
|
|
232
|
+
is what `prepack` runs — so every tarball and every `npm publish` is prod, and every `pnpm build`,
|
|
233
|
+
`pnpm dev` and watch-mode rebuild is dev.
|
|
234
|
+
|
|
235
|
+
| | dev build (`pnpm build`) | published build (`npm i -g codex-token-tracker`) |
|
|
236
|
+
| --- | --- | --- |
|
|
237
|
+
| Dashboard default | `http://localhost:3000` | `https://codex.chenli.dev` |
|
|
238
|
+
| Convex deployment | whatever the local dashboard's `/api/config` advertises — the **dev** one | production |
|
|
239
|
+
| Config, device token, upload state | `~/.codex-tracker-dev` | `~/.codex-tracker` |
|
|
240
|
+
| `checkUpdates` default | `false` (`update` refuses to run) | `true` |
|
|
241
|
+
| App name / Electron userData | `Codex Tracker (dev)` | `Codex Tracker` |
|
|
242
|
+
| macOS LaunchAgent | `dev.codex-tracker.menubar.dev` | `dev.codex-tracker.menubar` |
|
|
243
|
+
| Popover | orange **DEV** badge in the header | — |
|
|
244
|
+
|
|
245
|
+
Because the two differ in app name and config directory, **a dev build and an installed one can run at
|
|
246
|
+
the same time**, each with its own tray icon, device token and upload state.
|
|
247
|
+
|
|
248
|
+
To test against a dev environment, start the dashboard and Convex, then run the local build:
|
|
249
|
+
|
|
250
|
+
```bash
|
|
251
|
+
cd apps/dashboard && npx convex dev # terminal 1
|
|
252
|
+
pnpm dev # terminal 2 — http://localhost:3000
|
|
253
|
+
pnpm --filter codex-token-tracker build # terminal 3
|
|
254
|
+
node packages/menubar/bin/codex-tracker.js login # → localhost:3000, no --dashboard needed
|
|
255
|
+
node packages/menubar/bin/codex-tracker.js menubar
|
|
256
|
+
```
|
|
257
|
+
|
|
258
|
+
Both defaults are only *defaults*: `--dashboard <url>` and `config set dashboardUrl <url>` still point
|
|
259
|
+
either build anywhere, and `CODEX_TRACKER_HOME` still overrides the config directory.
|
|
260
|
+
|
|
261
|
+
Publishing (from the repo root): `pnpm release:menubar` — `prepublishOnly` typechecks and `prepack`
|
|
262
|
+
makes the release build, so a published tarball is never accidentally a dev build. `postpack` restores
|
|
263
|
+
the dev build in `dist/` afterwards, so your working copy keeps pointing at localhost.
|
|
203
264
|
|
|
204
265
|
## License
|
|
205
266
|
|
package/dist/cli.js
CHANGED
|
@@ -1138,9 +1138,20 @@ var init_src = __esm({
|
|
|
1138
1138
|
}
|
|
1139
1139
|
});
|
|
1140
1140
|
|
|
1141
|
+
// src/version.ts
|
|
1142
|
+
var APP_VERSION, APP_CHANNEL, IS_DEV_BUILD;
|
|
1143
|
+
var init_version = __esm({
|
|
1144
|
+
"src/version.ts"() {
|
|
1145
|
+
"use strict";
|
|
1146
|
+
APP_VERSION = true ? "0.2.1" : "0.0.0-dev";
|
|
1147
|
+
APP_CHANNEL = true ? "prod" : "dev";
|
|
1148
|
+
IS_DEV_BUILD = APP_CHANNEL === "dev";
|
|
1149
|
+
}
|
|
1150
|
+
});
|
|
1151
|
+
|
|
1141
1152
|
// src/core/config.ts
|
|
1142
1153
|
function configDir() {
|
|
1143
|
-
return process.env.CODEX_TRACKER_HOME || import_node_path2.default.join(import_node_os2.default.homedir(), ".codex-tracker");
|
|
1154
|
+
return process.env.CODEX_TRACKER_HOME || import_node_path2.default.join(import_node_os2.default.homedir(), IS_DEV_BUILD ? ".codex-tracker-dev" : ".codex-tracker");
|
|
1144
1155
|
}
|
|
1145
1156
|
function readJson(file, fallback) {
|
|
1146
1157
|
try {
|
|
@@ -1193,7 +1204,7 @@ function loadConfig() {
|
|
|
1193
1204
|
cfg.sources = normalizeSources(stored.sources);
|
|
1194
1205
|
cfg.trackAllProviders = stored.trackAllProviders === true;
|
|
1195
1206
|
cfg.liveRateLimits = stored.liveRateLimits !== false;
|
|
1196
|
-
cfg.checkUpdates = stored.checkUpdates
|
|
1207
|
+
cfg.checkUpdates = typeof stored.checkUpdates === "boolean" ? stored.checkUpdates : DEFAULT_CONFIG.checkUpdates;
|
|
1197
1208
|
if (!(cfg.usageRefreshSec >= 15)) cfg.usageRefreshSec = DEFAULT_CONFIG.usageRefreshSec;
|
|
1198
1209
|
if (!["auto", "en", "zh"].includes(cfg.language)) cfg.language = "auto";
|
|
1199
1210
|
if (!(cfg.uploadIntervalSec >= 10)) cfg.uploadIntervalSec = DEFAULT_CONFIG.uploadIntervalSec;
|
|
@@ -1300,14 +1311,17 @@ function loadPricingOverrides() {
|
|
|
1300
1311
|
}
|
|
1301
1312
|
return Object.keys(out).length ? out : void 0;
|
|
1302
1313
|
}
|
|
1303
|
-
var import_node_fs2, import_node_os2, import_node_path2, DEFAULT_DASHBOARD_URL, SOURCE_FORMATS, DEFAULT_SOURCES, SOURCE_IDS, DEFAULT_CONFIG, EDITABLE_KEYS, AGENT_NAME_RE;
|
|
1314
|
+
var import_node_fs2, import_node_os2, import_node_path2, PROD_DASHBOARD_URL, DEV_DASHBOARD_URL, DEFAULT_DASHBOARD_URL, SOURCE_FORMATS, DEFAULT_SOURCES, SOURCE_IDS, DEFAULT_CONFIG, EDITABLE_KEYS, AGENT_NAME_RE;
|
|
1304
1315
|
var init_config = __esm({
|
|
1305
1316
|
"src/core/config.ts"() {
|
|
1306
1317
|
"use strict";
|
|
1307
1318
|
import_node_fs2 = __toESM(require("node:fs"));
|
|
1308
1319
|
import_node_os2 = __toESM(require("node:os"));
|
|
1309
1320
|
import_node_path2 = __toESM(require("node:path"));
|
|
1310
|
-
|
|
1321
|
+
init_version();
|
|
1322
|
+
PROD_DASHBOARD_URL = "https://codex.chenli.dev";
|
|
1323
|
+
DEV_DASHBOARD_URL = "http://localhost:3000";
|
|
1324
|
+
DEFAULT_DASHBOARD_URL = IS_DEV_BUILD ? DEV_DASHBOARD_URL : PROD_DASHBOARD_URL;
|
|
1311
1325
|
SOURCE_FORMATS = ["codex", "pi", "generic", "opencode", "cline"];
|
|
1312
1326
|
DEFAULT_SOURCES = { codex: true, pi: true, hermes: true, opencode: true, cline: true, roo: true, kilo: true };
|
|
1313
1327
|
SOURCE_IDS = Object.keys(DEFAULT_SOURCES);
|
|
@@ -1327,7 +1341,8 @@ var init_config = __esm({
|
|
|
1327
1341
|
trackAllProviders: false,
|
|
1328
1342
|
liveRateLimits: true,
|
|
1329
1343
|
usageRefreshSec: 60,
|
|
1330
|
-
|
|
1344
|
+
// a dev build must never offer to replace itself with the published package
|
|
1345
|
+
checkUpdates: !IS_DEV_BUILD
|
|
1331
1346
|
};
|
|
1332
1347
|
EDITABLE_KEYS = [
|
|
1333
1348
|
"dashboardUrl",
|
|
@@ -1397,6 +1412,7 @@ var init_en = __esm({
|
|
|
1397
1412
|
estimatedPricingHint: "Pricing estimated: model not in the price table",
|
|
1398
1413
|
other: "Other",
|
|
1399
1414
|
lastUpload: "Last upload {time}",
|
|
1415
|
+
lastSync: "Last full sync {time}",
|
|
1400
1416
|
neverUploaded: "Not uploaded yet",
|
|
1401
1417
|
uploadError: "Upload error: {message}",
|
|
1402
1418
|
allDevicesToday: "All devices today",
|
|
@@ -1415,10 +1431,26 @@ var init_en = __esm({
|
|
|
1415
1431
|
launchAtLogin: "Launch at login",
|
|
1416
1432
|
refresh: "Refresh",
|
|
1417
1433
|
quit: "Quit",
|
|
1434
|
+
// full sync
|
|
1435
|
+
syncNow: "Sync now",
|
|
1436
|
+
syncing: "Syncing\u2026",
|
|
1437
|
+
syncTitle: "Rescan every agent on this device and re-upload its full usage history",
|
|
1438
|
+
syncScanning: "Rescanning every agent's transcripts\u2026",
|
|
1439
|
+
syncComputing: "Recomputing token usage\u2026",
|
|
1440
|
+
syncUploading: "Re-uploading this device's history\u2026",
|
|
1441
|
+
syncDownloading: "Fetching other devices\u2026",
|
|
1442
|
+
syncLimits: "Refreshing rate limits\u2026",
|
|
1443
|
+
syncDone: "Synced {agents} \xB7 {files} files \xB7 {sessions} sessions \xB7 {buckets} hours re-uploaded",
|
|
1444
|
+
syncDoneLocal: "Rescanned {agents} \xB7 {files} files \xB7 {sessions} sessions \u2014 sign in to upload",
|
|
1445
|
+
syncFailed: "Sync failed: {message}",
|
|
1418
1446
|
sessions: "{n} sessions",
|
|
1419
1447
|
files: "{n} files",
|
|
1420
1448
|
noData: "No usage found yet. Run Codex and this will fill in.",
|
|
1421
1449
|
version: "v{version}",
|
|
1450
|
+
// dev builds
|
|
1451
|
+
devBuild: "DEV",
|
|
1452
|
+
devBuildTitle: "Local build \u2014 uploading to {url} (dev environment). Config: {dir}",
|
|
1453
|
+
devBuildMenu: "Dev build \u2192 {url}",
|
|
1422
1454
|
justNow: "just now",
|
|
1423
1455
|
never: "never",
|
|
1424
1456
|
local: "Local",
|
|
@@ -1446,6 +1478,7 @@ Commands:
|
|
|
1446
1478
|
login Connect this device to your dashboard (--dashboard <url>)
|
|
1447
1479
|
logout Disconnect this device
|
|
1448
1480
|
status Print today's usage, live session and rate limits
|
|
1481
|
+
sync Rescan every agent and re-upload this device's full history (calibrate)
|
|
1449
1482
|
paths Show detected session directories (Codex, pi, OpenCode, Cline/Roo/Kilo, Hermes, custom)
|
|
1450
1483
|
update Install the newest published version (--check only reports)
|
|
1451
1484
|
config get|set Read or change settings (config set uploadIntervalSec 30, config set sources.pi false)
|
|
@@ -1500,7 +1533,15 @@ Options:
|
|
|
1500
1533
|
cliUpdateRunning: "Running: {command}",
|
|
1501
1534
|
cliUpdateDone: "Updated to {version}. Restart the menu bar app (or rerun the CLI) to use it.",
|
|
1502
1535
|
cliUpdateFailed: "Update failed (exit {code}). Run it yourself: {command}",
|
|
1503
|
-
cliUpdateCheckFailed: "Could not reach the npm registry: {message}"
|
|
1536
|
+
cliUpdateCheckFailed: "Could not reach the npm registry: {message}",
|
|
1537
|
+
cliSyncStart: "Full sync: rescanning every agent on this device\u2026",
|
|
1538
|
+
cliSyncScanned: " scanned {files} files in {roots} directories ({agents})",
|
|
1539
|
+
cliSyncUploaded: " re-uploaded {buckets} hour buckets and {sessions} sessions",
|
|
1540
|
+
cliSyncLocal: " not signed in \u2014 nothing uploaded (run `codex-tracker login` first)",
|
|
1541
|
+
cliSyncDone: "Sync complete in {seconds}s \u2014 {sessions} sessions, today {tokens} tok \xB7 {cost}",
|
|
1542
|
+
cliSyncFailed: "Sync failed: {message}",
|
|
1543
|
+
cliChannelDev: "Dev build \u2014 dashboard {url} \xB7 config {dir}",
|
|
1544
|
+
cliUpdateDevBuild: "This is a local dev build; `update` would install the published package over it. Run `pnpm build` in the repo instead."
|
|
1504
1545
|
};
|
|
1505
1546
|
}
|
|
1506
1547
|
});
|
|
@@ -1554,6 +1595,7 @@ var init_zh = __esm({
|
|
|
1554
1595
|
estimatedPricingHint: "\u4EF7\u683C\u4E3A\u4F30\u7B97\u503C\uFF1A\u8BE5\u6A21\u578B\u4E0D\u5728\u4EF7\u76EE\u8868\u4E2D",
|
|
1555
1596
|
other: "\u5176\u4ED6",
|
|
1556
1597
|
lastUpload: "\u4E0A\u6B21\u4E0A\u4F20 {time}",
|
|
1598
|
+
lastSync: "\u4E0A\u6B21\u5B8C\u6574\u540C\u6B65 {time}",
|
|
1557
1599
|
neverUploaded: "\u5C1A\u672A\u4E0A\u4F20",
|
|
1558
1600
|
uploadError: "\u4E0A\u4F20\u5931\u8D25\uFF1A{message}",
|
|
1559
1601
|
allDevicesToday: "\u6240\u6709\u8BBE\u5907\u4ECA\u65E5",
|
|
@@ -1572,10 +1614,26 @@ var init_zh = __esm({
|
|
|
1572
1614
|
launchAtLogin: "\u5F00\u673A\u81EA\u542F\u52A8",
|
|
1573
1615
|
refresh: "\u5237\u65B0",
|
|
1574
1616
|
quit: "\u9000\u51FA",
|
|
1617
|
+
// 完整同步
|
|
1618
|
+
syncNow: "\u7ACB\u5373\u540C\u6B65",
|
|
1619
|
+
syncing: "\u540C\u6B65\u4E2D\u2026",
|
|
1620
|
+
syncTitle: "\u91CD\u65B0\u626B\u63CF\u672C\u8BBE\u5907\u4E0A\u7684\u6240\u6709\u667A\u80FD\u4F53\uFF0C\u5E76\u91CD\u65B0\u4E0A\u4F20\u5B8C\u6574\u7528\u91CF\u5386\u53F2",
|
|
1621
|
+
syncScanning: "\u6B63\u5728\u91CD\u65B0\u626B\u63CF\u6240\u6709\u667A\u80FD\u4F53\u7684\u4F1A\u8BDD\u8BB0\u5F55\u2026",
|
|
1622
|
+
syncComputing: "\u6B63\u5728\u91CD\u65B0\u8BA1\u7B97 token \u7528\u91CF\u2026",
|
|
1623
|
+
syncUploading: "\u6B63\u5728\u91CD\u65B0\u4E0A\u4F20\u672C\u8BBE\u5907\u7684\u5386\u53F2\u6570\u636E\u2026",
|
|
1624
|
+
syncDownloading: "\u6B63\u5728\u62C9\u53D6\u5176\u4ED6\u8BBE\u5907\u7684\u6570\u636E\u2026",
|
|
1625
|
+
syncLimits: "\u6B63\u5728\u5237\u65B0\u9650\u989D\u2026",
|
|
1626
|
+
syncDone: "\u5DF2\u540C\u6B65 {agents} \xB7 {files} \u4E2A\u6587\u4EF6 \xB7 {sessions} \u4E2A\u4F1A\u8BDD \xB7 \u91CD\u65B0\u4E0A\u4F20 {buckets} \u4E2A\u5C0F\u65F6\u6BB5",
|
|
1627
|
+
syncDoneLocal: "\u5DF2\u91CD\u65B0\u626B\u63CF {agents} \xB7 {files} \u4E2A\u6587\u4EF6 \xB7 {sessions} \u4E2A\u4F1A\u8BDD \u2014 \u767B\u5F55\u540E\u53EF\u4E0A\u4F20",
|
|
1628
|
+
syncFailed: "\u540C\u6B65\u5931\u8D25\uFF1A{message}",
|
|
1575
1629
|
sessions: "{n} \u4E2A\u4F1A\u8BDD",
|
|
1576
1630
|
files: "{n} \u4E2A\u6587\u4EF6",
|
|
1577
1631
|
noData: "\u8FD8\u6CA1\u6709\u7528\u91CF\u6570\u636E\u3002\u8FD0\u884C Codex \u540E\u8FD9\u91CC\u4F1A\u81EA\u52A8\u66F4\u65B0\u3002",
|
|
1578
1632
|
version: "v{version}",
|
|
1633
|
+
// 开发版
|
|
1634
|
+
devBuild: "\u5F00\u53D1\u7248",
|
|
1635
|
+
devBuildTitle: "\u672C\u5730\u6784\u5EFA \u2014\u2014 \u6570\u636E\u4E0A\u4F20\u81F3 {url}\uFF08\u5F00\u53D1\u73AF\u5883\uFF09\u3002\u914D\u7F6E\u76EE\u5F55\uFF1A{dir}",
|
|
1636
|
+
devBuildMenu: "\u5F00\u53D1\u7248 \u2192 {url}",
|
|
1579
1637
|
justNow: "\u521A\u521A",
|
|
1580
1638
|
never: "\u4ECE\u672A",
|
|
1581
1639
|
local: "\u672C\u673A",
|
|
@@ -1601,6 +1659,7 @@ var init_zh = __esm({
|
|
|
1601
1659
|
login \u5C06\u672C\u8BBE\u5907\u8FDE\u63A5\u5230\u4EEA\u8868\u76D8\uFF08--dashboard <url>\uFF09
|
|
1602
1660
|
logout \u65AD\u5F00\u672C\u8BBE\u5907
|
|
1603
1661
|
status \u6253\u5370\u4ECA\u65E5\u7528\u91CF\u3001\u5B9E\u65F6\u4F1A\u8BDD\u4E0E\u9650\u989D
|
|
1662
|
+
sync \u91CD\u65B0\u626B\u63CF\u6240\u6709\u667A\u80FD\u4F53\u5E76\u91CD\u65B0\u4E0A\u4F20\u672C\u8BBE\u5907\u5B8C\u6574\u5386\u53F2\uFF08\u6821\u51C6\uFF09
|
|
1604
1663
|
paths \u663E\u793A\u68C0\u6D4B\u5230\u7684\u4F1A\u8BDD\u76EE\u5F55\uFF08Codex\u3001pi\u3001OpenCode\u3001Cline/Roo/Kilo\u3001Hermes\u3001\u81EA\u5B9A\u4E49\uFF09
|
|
1605
1664
|
update \u5B89\u88C5\u6700\u65B0\u53D1\u5E03\u7248\u672C\uFF08--check \u53EA\u68C0\u67E5\u4E0D\u5B89\u88C5\uFF09
|
|
1606
1665
|
config get|set \u8BFB\u53D6\u6216\u4FEE\u6539\u8BBE\u7F6E\uFF08config set uploadIntervalSec 30\uFF0Cconfig set sources.pi false\uFF09
|
|
@@ -1655,7 +1714,15 @@ var init_zh = __esm({
|
|
|
1655
1714
|
cliUpdateRunning: "\u6B63\u5728\u6267\u884C\uFF1A{command}",
|
|
1656
1715
|
cliUpdateDone: "\u5DF2\u66F4\u65B0\u5230 {version}\u3002\u8BF7\u91CD\u542F\u83DC\u5355\u680F\u5E94\u7528\uFF08\u6216\u91CD\u65B0\u8FD0\u884C CLI\uFF09\u4EE5\u751F\u6548\u3002",
|
|
1657
1716
|
cliUpdateFailed: "\u66F4\u65B0\u5931\u8D25\uFF08\u9000\u51FA\u7801 {code}\uFF09\u3002\u8BF7\u624B\u52A8\u6267\u884C\uFF1A{command}",
|
|
1658
|
-
cliUpdateCheckFailed: "\u65E0\u6CD5\u8BBF\u95EE npm registry\uFF1A{message}"
|
|
1717
|
+
cliUpdateCheckFailed: "\u65E0\u6CD5\u8BBF\u95EE npm registry\uFF1A{message}",
|
|
1718
|
+
cliSyncStart: "\u5B8C\u6574\u540C\u6B65\uFF1A\u6B63\u5728\u91CD\u65B0\u626B\u63CF\u672C\u8BBE\u5907\u4E0A\u7684\u6240\u6709\u667A\u80FD\u4F53\u2026",
|
|
1719
|
+
cliSyncScanned: " \u5DF2\u626B\u63CF {roots} \u4E2A\u76EE\u5F55\u4E2D\u7684 {files} \u4E2A\u6587\u4EF6\uFF08{agents}\uFF09",
|
|
1720
|
+
cliSyncUploaded: " \u5DF2\u91CD\u65B0\u4E0A\u4F20 {buckets} \u4E2A\u5C0F\u65F6\u6BB5\u4E0E {sessions} \u4E2A\u4F1A\u8BDD",
|
|
1721
|
+
cliSyncLocal: " \u672A\u767B\u5F55 \u2014 \u6CA1\u6709\u4E0A\u4F20\u4EFB\u4F55\u6570\u636E\uFF08\u8BF7\u5148\u8FD0\u884C `codex-tracker login`\uFF09",
|
|
1722
|
+
cliSyncDone: "\u540C\u6B65\u5B8C\u6210\uFF0C\u7528\u65F6 {seconds} \u79D2 \u2014 {sessions} \u4E2A\u4F1A\u8BDD\uFF0C\u4ECA\u65E5 {tokens} tok \xB7 {cost}",
|
|
1723
|
+
cliSyncFailed: "\u540C\u6B65\u5931\u8D25\uFF1A{message}",
|
|
1724
|
+
cliChannelDev: "\u5F00\u53D1\u7248 \u2014\u2014 \u4EEA\u8868\u76D8 {url} \xB7 \u914D\u7F6E\u76EE\u5F55 {dir}",
|
|
1725
|
+
cliUpdateDevBuild: "\u8FD9\u662F\u672C\u5730\u5F00\u53D1\u7248\uFF1B`update` \u4F1A\u7528\u5DF2\u53D1\u5E03\u7684\u5305\u8986\u76D6\u5B83\u3002\u8BF7\u6539\u4E3A\u5728\u4ED3\u5E93\u4E2D\u8FD0\u884C `pnpm build`\u3002"
|
|
1659
1726
|
};
|
|
1660
1727
|
}
|
|
1661
1728
|
});
|
|
@@ -8630,11 +8697,25 @@ var init_uploader = __esm({
|
|
|
8630
8697
|
throw err;
|
|
8631
8698
|
}
|
|
8632
8699
|
}
|
|
8633
|
-
/**
|
|
8634
|
-
async
|
|
8700
|
+
/** Wait for an in-flight incremental push to finish so a full sync isn't skipped by the guard. */
|
|
8701
|
+
async awaitIdle(timeoutMs = 15e3) {
|
|
8702
|
+
const deadline = Date.now() + timeoutMs;
|
|
8703
|
+
while (this.inFlight && Date.now() < deadline) await new Promise((r) => setTimeout(r, 100));
|
|
8704
|
+
}
|
|
8705
|
+
/**
|
|
8706
|
+
* Upload changed hour buckets and session summaries. Returns counts of pushed items.
|
|
8707
|
+
* With `full`, the record of what was already pushed is dropped first so *everything* is re-sent —
|
|
8708
|
+
* that is what recalibrates this device's totals on the dashboard after a parser or pricing change.
|
|
8709
|
+
*/
|
|
8710
|
+
async pushAll(buckets, sessions, sessionCosts, opts = {}) {
|
|
8711
|
+
if (opts.full) await this.awaitIdle();
|
|
8635
8712
|
if (this.inFlight) return { buckets: 0, sessions: 0 };
|
|
8636
8713
|
this.inFlight = true;
|
|
8637
8714
|
try {
|
|
8715
|
+
if (opts.full) {
|
|
8716
|
+
this.state.pushedBuckets = {};
|
|
8717
|
+
this.state.pushedSessions = {};
|
|
8718
|
+
}
|
|
8638
8719
|
const changed = [];
|
|
8639
8720
|
const hashes = /* @__PURE__ */ new Map();
|
|
8640
8721
|
for (const b of buckets.sort((a, b2) => a.hourStart - b2.hourStart)) {
|
|
@@ -9708,6 +9789,15 @@ var SessionStore = class {
|
|
|
9708
9789
|
onChange = null;
|
|
9709
9790
|
debounceTimer = null;
|
|
9710
9791
|
sessionCache = null;
|
|
9792
|
+
/**
|
|
9793
|
+
* Forget the parsed-file index so the next deep refresh re-reads and re-parses every transcript.
|
|
9794
|
+
* Used by the full sync: files are otherwise skipped while their size and mtime are unchanged, which
|
|
9795
|
+
* would keep stale numbers around after a parser or pricing change.
|
|
9796
|
+
*/
|
|
9797
|
+
reset() {
|
|
9798
|
+
this.files.clear();
|
|
9799
|
+
this.sessionCache = null;
|
|
9800
|
+
}
|
|
9711
9801
|
async refreshDeep() {
|
|
9712
9802
|
const o = this.getOptions();
|
|
9713
9803
|
this.roots = discoverSessionRoots({ extraSessionDirs: o.extraSessionDirs, sources: o.sources });
|
|
@@ -10107,11 +10197,7 @@ var import_node_fs9 = __toESM(require("node:fs"));
|
|
|
10107
10197
|
var import_node_path11 = __toESM(require("node:path"));
|
|
10108
10198
|
var import_node_child_process4 = require("node:child_process");
|
|
10109
10199
|
init_config();
|
|
10110
|
-
|
|
10111
|
-
// src/version.ts
|
|
10112
|
-
var APP_VERSION = true ? "0.2.0" : "0.0.0-dev";
|
|
10113
|
-
|
|
10114
|
-
// src/core/update.ts
|
|
10200
|
+
init_version();
|
|
10115
10201
|
var NPM_PACKAGE = "codex-token-tracker";
|
|
10116
10202
|
var CHECK_TTL_MS = 6 * 60 * 60 * 1e3;
|
|
10117
10203
|
var REQUEST_TIMEOUT_MS = 8e3;
|
|
@@ -10135,7 +10221,7 @@ function compareVersions(a, b) {
|
|
|
10135
10221
|
return A.pre < B.pre ? -1 : 1;
|
|
10136
10222
|
}
|
|
10137
10223
|
function isDevBuild(version2) {
|
|
10138
|
-
return version2.startsWith("0.0.0");
|
|
10224
|
+
return IS_DEV_BUILD || version2.startsWith("0.0.0");
|
|
10139
10225
|
}
|
|
10140
10226
|
function registryBase() {
|
|
10141
10227
|
const r = process.env.CODEX_TRACKER_REGISTRY || process.env.npm_config_registry || process.env.NPM_CONFIG_REGISTRY || "https://registry.npmjs.org";
|
|
@@ -10271,6 +10357,7 @@ function runUpdate(opts = {}) {
|
|
|
10271
10357
|
}
|
|
10272
10358
|
|
|
10273
10359
|
// src/core/engine.ts
|
|
10360
|
+
init_version();
|
|
10274
10361
|
var SHALLOW_MS = 3e3;
|
|
10275
10362
|
var DEEP_MS = 6e4;
|
|
10276
10363
|
var TICK_MS = 2e3;
|
|
@@ -10278,6 +10365,7 @@ var REMOTE_MS = 6e4;
|
|
|
10278
10365
|
var LIVE_LIMITS_DEBOUNCE_MS = 1e4;
|
|
10279
10366
|
var LIVE_LIMITS_MIN_GAP_MS = 2e4;
|
|
10280
10367
|
var UPDATE_CHECK_MS = 6 * 60 * 60 * 1e3;
|
|
10368
|
+
var SYNC_BANNER_MS = 25e3;
|
|
10281
10369
|
var Engine = class extends import_node_events.EventEmitter {
|
|
10282
10370
|
constructor(opts) {
|
|
10283
10371
|
super();
|
|
@@ -10317,6 +10405,13 @@ var Engine = class extends import_node_events.EventEmitter {
|
|
|
10317
10405
|
update = null;
|
|
10318
10406
|
updateStatus = "idle";
|
|
10319
10407
|
updateLog = null;
|
|
10408
|
+
syncStatus = "idle";
|
|
10409
|
+
syncPhase = null;
|
|
10410
|
+
syncStartedAt = null;
|
|
10411
|
+
syncFinishedAt = null;
|
|
10412
|
+
syncError = null;
|
|
10413
|
+
syncResult = null;
|
|
10414
|
+
syncBannerTimer = null;
|
|
10320
10415
|
get heatmapWeeks() {
|
|
10321
10416
|
return this.opts.heatmapWeeks ?? 16;
|
|
10322
10417
|
}
|
|
@@ -10367,6 +10462,8 @@ var Engine = class extends import_node_events.EventEmitter {
|
|
|
10367
10462
|
this.timers = [];
|
|
10368
10463
|
if (this.liveLimitsTimer) clearTimeout(this.liveLimitsTimer);
|
|
10369
10464
|
this.liveLimitsTimer = null;
|
|
10465
|
+
if (this.syncBannerTimer) clearTimeout(this.syncBannerTimer);
|
|
10466
|
+
this.syncBannerTimer = null;
|
|
10370
10467
|
this.store.stopWatching();
|
|
10371
10468
|
this.pending?.abort.abort();
|
|
10372
10469
|
}
|
|
@@ -10470,6 +10567,112 @@ var Engine = class extends import_node_events.EventEmitter {
|
|
|
10470
10567
|
await this.uploader.fetchRemote(this.heatmapWeeks);
|
|
10471
10568
|
this.recompute();
|
|
10472
10569
|
}
|
|
10570
|
+
/**
|
|
10571
|
+
* Full sync ("calibrate this device"). Unlike the periodic incremental upload this deliberately
|
|
10572
|
+
* throws away every cache on the way:
|
|
10573
|
+
* 1. re-read the config so sources enabled since start-up are picked up,
|
|
10574
|
+
* 2. drop the parsed-file index and re-discover + re-parse every transcript of every agent
|
|
10575
|
+
* (Codex plus the agents running on the Codex subscription: pi, OpenCode, Cline/Roo/Kilo,
|
|
10576
|
+
* Hermes and any custom `extraSessionDirs`),
|
|
10577
|
+
* 3. recompute the aggregates with the current pricing table,
|
|
10578
|
+
* 4. re-upload *everything* — not just what changed — so the dashboard's totals for this device
|
|
10579
|
+
* are replaced by the freshly computed ones,
|
|
10580
|
+
* 5. pull the other devices' rows and the live rate limits back down.
|
|
10581
|
+
*
|
|
10582
|
+
* Returns null when a sync is already running. Never throws: failures land in the returned state.
|
|
10583
|
+
*/
|
|
10584
|
+
async syncNow() {
|
|
10585
|
+
if (this.syncStatus === "running") return null;
|
|
10586
|
+
if (this.syncBannerTimer) {
|
|
10587
|
+
clearTimeout(this.syncBannerTimer);
|
|
10588
|
+
this.syncBannerTimer = null;
|
|
10589
|
+
}
|
|
10590
|
+
const startedAt = Date.now();
|
|
10591
|
+
this.syncStatus = "running";
|
|
10592
|
+
this.syncPhase = null;
|
|
10593
|
+
this.syncStartedAt = startedAt;
|
|
10594
|
+
this.syncFinishedAt = null;
|
|
10595
|
+
this.syncError = null;
|
|
10596
|
+
const phase = (p) => {
|
|
10597
|
+
this.syncPhase = p;
|
|
10598
|
+
this.emitSnapshot();
|
|
10599
|
+
};
|
|
10600
|
+
try {
|
|
10601
|
+
phase("scanning");
|
|
10602
|
+
this.reloadConfig();
|
|
10603
|
+
this.store.reset();
|
|
10604
|
+
await this.store.refreshDeep();
|
|
10605
|
+
phase("computing");
|
|
10606
|
+
this.recompute();
|
|
10607
|
+
let uploadedBuckets = 0;
|
|
10608
|
+
let uploadedSessions = 0;
|
|
10609
|
+
const uploaded = this.opts.upload && this.signedIn;
|
|
10610
|
+
if (uploaded) {
|
|
10611
|
+
phase("uploading");
|
|
10612
|
+
const r = await this.uploader.pushAll(this.stats.buckets, this.stats.sessions, this.stats.sessionCosts, { full: true });
|
|
10613
|
+
uploadedBuckets = r.buckets;
|
|
10614
|
+
uploadedSessions = r.sessions;
|
|
10615
|
+
await this.heartbeatNow().catch(() => {
|
|
10616
|
+
});
|
|
10617
|
+
phase("downloading");
|
|
10618
|
+
await this.fetchRemoteNow();
|
|
10619
|
+
}
|
|
10620
|
+
phase("limits");
|
|
10621
|
+
await this.refreshLiveLimits(true);
|
|
10622
|
+
const finishedAt = Date.now();
|
|
10623
|
+
this.syncResult = {
|
|
10624
|
+
startedAt,
|
|
10625
|
+
finishedAt,
|
|
10626
|
+
durationMs: finishedAt - startedAt,
|
|
10627
|
+
files: this.store.fileCount,
|
|
10628
|
+
sessions: this.store.sessions().length,
|
|
10629
|
+
roots: this.store.roots.length,
|
|
10630
|
+
agents: [...new Set(this.store.roots.map((r) => r.agent))].sort(),
|
|
10631
|
+
uploadedBuckets,
|
|
10632
|
+
uploadedSessions,
|
|
10633
|
+
uploaded
|
|
10634
|
+
};
|
|
10635
|
+
this.syncStatus = "done";
|
|
10636
|
+
this.syncFinishedAt = finishedAt;
|
|
10637
|
+
this.opts.log?.(
|
|
10638
|
+
`sync: ${this.syncResult.files} files, ${this.syncResult.sessions} sessions, ${uploadedBuckets} buckets / ${uploadedSessions} sessions uploaded in ${this.syncResult.durationMs} ms`
|
|
10639
|
+
);
|
|
10640
|
+
return this.syncResult;
|
|
10641
|
+
} catch (err) {
|
|
10642
|
+
this.syncStatus = "error";
|
|
10643
|
+
this.syncFinishedAt = Date.now();
|
|
10644
|
+
this.syncError = err instanceof SignedOutError ? "not signed in" : errorMessage(err);
|
|
10645
|
+
this.opts.log?.(`sync failed: ${this.syncError}`);
|
|
10646
|
+
return null;
|
|
10647
|
+
} finally {
|
|
10648
|
+
this.syncPhase = null;
|
|
10649
|
+
this.emitSnapshot();
|
|
10650
|
+
this.scheduleSyncBannerClear();
|
|
10651
|
+
}
|
|
10652
|
+
}
|
|
10653
|
+
/** Return the sync banner to "idle" a little after it finished, keeping `last` for the footer. */
|
|
10654
|
+
scheduleSyncBannerClear() {
|
|
10655
|
+
if (this.opts.watch === false) return;
|
|
10656
|
+
if (this.syncBannerTimer) clearTimeout(this.syncBannerTimer);
|
|
10657
|
+
this.syncBannerTimer = setTimeout(() => {
|
|
10658
|
+
this.syncBannerTimer = null;
|
|
10659
|
+
if (this.syncStatus === "done" || this.syncStatus === "error") {
|
|
10660
|
+
this.syncStatus = "idle";
|
|
10661
|
+
this.emitSnapshot();
|
|
10662
|
+
}
|
|
10663
|
+
}, SYNC_BANNER_MS);
|
|
10664
|
+
this.syncBannerTimer.unref?.();
|
|
10665
|
+
}
|
|
10666
|
+
syncState() {
|
|
10667
|
+
return {
|
|
10668
|
+
status: this.syncStatus,
|
|
10669
|
+
phase: this.syncPhase,
|
|
10670
|
+
startedAt: this.syncStartedAt,
|
|
10671
|
+
finishedAt: this.syncFinishedAt,
|
|
10672
|
+
error: this.syncError,
|
|
10673
|
+
last: this.syncResult
|
|
10674
|
+
};
|
|
10675
|
+
}
|
|
10473
10676
|
/**
|
|
10474
10677
|
* Ask the npm registry for the newest published version (cached for 6 h unless `force`).
|
|
10475
10678
|
* Best-effort: a failure is recorded on the snapshot, never thrown.
|
|
@@ -10587,6 +10790,7 @@ ${r.output}`.trim();
|
|
|
10587
10790
|
const sessions = this.store.sessions();
|
|
10588
10791
|
return {
|
|
10589
10792
|
version: APP_VERSION,
|
|
10793
|
+
channel: APP_CHANNEL,
|
|
10590
10794
|
generatedAt: Date.now(),
|
|
10591
10795
|
language: this.language(),
|
|
10592
10796
|
languageSetting: this.config.language,
|
|
@@ -10605,6 +10809,7 @@ ${r.output}`.trim();
|
|
|
10605
10809
|
rateLimitsError: this.config.liveRateLimits ? this.liveLimitsError : null,
|
|
10606
10810
|
rateLimitsUpdatedAt: this.liveLimits ? this.liveLimitsAt : s.logRateLimits?.observedAt ?? null,
|
|
10607
10811
|
update: this.updateState(),
|
|
10812
|
+
sync: this.syncState(),
|
|
10608
10813
|
modelsToday: s.modelsToday,
|
|
10609
10814
|
modelsMonth: s.modelsMonth,
|
|
10610
10815
|
byAgentToday: s.byAgentToday,
|
|
@@ -10621,6 +10826,7 @@ ${r.output}`.trim();
|
|
|
10621
10826
|
},
|
|
10622
10827
|
sessionDirs: SessionStore.rootDirs(this.store.roots),
|
|
10623
10828
|
sessionRoots: this.store.roots.map((r) => ({ dir: r.dir, agent: r.agent, format: r.format, origin: r.origin })),
|
|
10829
|
+
configDir: configDir(),
|
|
10624
10830
|
launchAtLogin: this.config.launchAtLogin,
|
|
10625
10831
|
trayTitle: this.config.trayTitle
|
|
10626
10832
|
};
|
|
@@ -10632,6 +10838,7 @@ init_platform();
|
|
|
10632
10838
|
init_config();
|
|
10633
10839
|
init_uploader();
|
|
10634
10840
|
init_i18n();
|
|
10841
|
+
init_version();
|
|
10635
10842
|
function parseArgs2(argv) {
|
|
10636
10843
|
const out = { command: null, positional: [], flags: {} };
|
|
10637
10844
|
for (let i2 = 0; i2 < argv.length; i2++) {
|
|
@@ -10797,6 +11004,7 @@ async function runStatus(json) {
|
|
|
10797
11004
|
s.auth.status === "signedIn" ? `${t2("signedInAs", { name: s.auth.user?.name || s.auth.user?.email || "?" })} \xB7 ${s.auth.dashboardUrl}` : t2("cliNotSignedIn")
|
|
10798
11005
|
);
|
|
10799
11006
|
line(t2("cliStatusDirs"), s.sessionDirs.join("\n" + " ".repeat(15)) || "-");
|
|
11007
|
+
if (IS_DEV_BUILD) console.log(t2("cliChannelDev", { url: s.auth.dashboardUrl, dir: configDir() }));
|
|
10800
11008
|
const byAgent = Object.entries(s.counts.byAgent).sort((a, b) => b[1].sessions - a[1].sessions).map(([agent, c]) => `${agent} ${c.sessions}`).join(", ");
|
|
10801
11009
|
console.log(t2("sessions", { n: s.counts.sessions }) + " \xB7 " + t2("files", { n: s.counts.files }) + (byAgent ? ` (${byAgent})` : ""));
|
|
10802
11010
|
if (cfg.checkUpdates) {
|
|
@@ -10805,6 +11013,36 @@ async function runStatus(json) {
|
|
|
10805
11013
|
}
|
|
10806
11014
|
return 0;
|
|
10807
11015
|
}
|
|
11016
|
+
async function runSync(flags) {
|
|
11017
|
+
const cfg = applyDashboardFlag(flags);
|
|
11018
|
+
const L = lang(cfg);
|
|
11019
|
+
const t2 = makeT(L);
|
|
11020
|
+
const engine = new Engine({
|
|
11021
|
+
upload: true,
|
|
11022
|
+
watch: false,
|
|
11023
|
+
systemLocale: systemLocale(),
|
|
11024
|
+
log: process.env.CODEX_TRACKER_DEBUG ? (m) => console.error("[sync]", m) : void 0
|
|
11025
|
+
});
|
|
11026
|
+
console.log(t2("cliSyncStart"));
|
|
11027
|
+
const result = await engine.syncNow();
|
|
11028
|
+
if (!result) {
|
|
11029
|
+
console.error(t2("cliSyncFailed", { message: engine.snapshot().sync.error ?? "?" }));
|
|
11030
|
+
return 1;
|
|
11031
|
+
}
|
|
11032
|
+
console.log(t2("cliSyncScanned", { files: result.files, roots: result.roots, agents: result.agents.join(", ") || "-" }));
|
|
11033
|
+
if (result.uploaded) console.log(t2("cliSyncUploaded", { buckets: result.uploadedBuckets, sessions: result.uploadedSessions }));
|
|
11034
|
+
else console.log(t2("cliSyncLocal"));
|
|
11035
|
+
const s = engine.snapshot();
|
|
11036
|
+
console.log(
|
|
11037
|
+
t2("cliSyncDone", {
|
|
11038
|
+
seconds: (result.durationMs / 1e3).toFixed(1),
|
|
11039
|
+
sessions: result.sessions,
|
|
11040
|
+
tokens: formatTokens(s.today.usage.total),
|
|
11041
|
+
cost: formatUSD(s.today.cost)
|
|
11042
|
+
})
|
|
11043
|
+
);
|
|
11044
|
+
return 0;
|
|
11045
|
+
}
|
|
10808
11046
|
async function runLogin(flags) {
|
|
10809
11047
|
const cfg = applyDashboardFlag(flags);
|
|
10810
11048
|
const L = lang(cfg);
|
|
@@ -10908,6 +11146,7 @@ function runPaths() {
|
|
|
10908
11146
|
const off = SOURCE_IDS.filter((id) => !cfg.sources[id]);
|
|
10909
11147
|
if (off.length) console.log(` (disabled: ${off.join(", ")})`);
|
|
10910
11148
|
console.log(t2("cliConfigDir", { dir: configDir() }));
|
|
11149
|
+
if (IS_DEV_BUILD) console.log(t2("cliChannelDev", { url: cfg.dashboardUrl, dir: configDir() }));
|
|
10911
11150
|
return 0;
|
|
10912
11151
|
}
|
|
10913
11152
|
function runConfig(positional) {
|
|
@@ -10954,6 +11193,10 @@ function runConfig(positional) {
|
|
|
10954
11193
|
}
|
|
10955
11194
|
async function runUpdateCommand(flags) {
|
|
10956
11195
|
const t2 = makeT(lang());
|
|
11196
|
+
if (IS_DEV_BUILD) {
|
|
11197
|
+
console.error(t2("cliUpdateDevBuild"));
|
|
11198
|
+
return 1;
|
|
11199
|
+
}
|
|
10957
11200
|
const info2 = await checkForUpdate({ force: true });
|
|
10958
11201
|
if (info2.error && !info2.latest) {
|
|
10959
11202
|
console.error(t2("cliUpdateCheckFailed", { message: info2.error }));
|
|
@@ -10992,6 +11235,7 @@ async function main() {
|
|
|
10992
11235
|
const t2 = makeT(lang());
|
|
10993
11236
|
if (args.flags.version) {
|
|
10994
11237
|
console.log(t2("cliVersion", { version: APP_VERSION }));
|
|
11238
|
+
if (IS_DEV_BUILD) console.log(t2("cliChannelDev", { url: loadConfig().dashboardUrl, dir: configDir() }));
|
|
10995
11239
|
return 0;
|
|
10996
11240
|
}
|
|
10997
11241
|
if (args.flags.help || args.command === "help") {
|
|
@@ -11028,6 +11272,8 @@ async function main() {
|
|
|
11028
11272
|
}
|
|
11029
11273
|
case "status":
|
|
11030
11274
|
return runStatus(args.flags.json === true);
|
|
11275
|
+
case "sync":
|
|
11276
|
+
return runSync(args.flags);
|
|
11031
11277
|
case "paths":
|
|
11032
11278
|
return runPaths();
|
|
11033
11279
|
case "config":
|