tokentracker-cli 1.1.4 → 1.1.6
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/README.md +15 -0
- package/dashboard/dist/assets/{AchievementBadge-BGA-HT_B.js → AchievementBadge-CpK5rOMf.js} +1 -1
- package/dashboard/dist/assets/{AchievementsPage-CtRh5AjH.js → AchievementsPage-BtcQkHZR.js} +2 -2
- package/dashboard/dist/assets/{AchievementsSection-BarRcKaN.js → AchievementsSection-BT50hdFh.js} +1 -1
- package/dashboard/dist/assets/{BadgeDetailModal-Bg0BDAN2.js → BadgeDetailModal-DFtz7FxR.js} +1 -1
- package/dashboard/dist/assets/{BotAnimated-By9J3-dn.js → BotAnimated-DuZSWD8p.js} +1 -1
- package/dashboard/dist/assets/{Card-CIoJYi2j.js → Card-BzGCR7t6.js} +1 -1
- package/dashboard/dist/assets/{ClawdAnimated-Dkvc2xe8.js → ClawdAnimated-tpie3z4W.js} +2 -2
- package/dashboard/dist/assets/{CommandPalette-BssDtHRl.js → CommandPalette-DxL_wTUY.js} +1 -1
- package/dashboard/dist/assets/{CommunityStatsModal-CQrgLlDb.js → CommunityStatsModal-5FYbcq_Q.js} +1 -1
- package/dashboard/dist/assets/{ConfirmModal-C3hBPgYc.js → ConfirmModal-Dd1z1ofU.js} +1 -1
- package/dashboard/dist/assets/{Controls-BSW6a1HS.js → Controls-DlImhUxL.js} +1 -1
- package/dashboard/dist/assets/{DashboardPage-Dhv7fejy.js → DashboardPage-DIPzwCc4.js} +1 -1
- package/dashboard/dist/assets/{DevicePage-D5eZn0wo.js → DevicePage-CtdxXb6m.js} +1 -1
- package/dashboard/dist/assets/{DialogClose-BltQpaJo.js → DialogClose-FTnu24dV.js} +1 -1
- package/dashboard/dist/assets/DialogDescription-CqQpH51z.js +1 -0
- package/dashboard/dist/assets/{DialogTitle-CVddcRPZ.js → DialogTitle-C8_lCmoh.js} +1 -1
- package/dashboard/dist/assets/{FadeIn-goPv73MT.js → FadeIn-Cw9s9Vyn.js} +1 -1
- package/dashboard/dist/assets/{HeaderGithubStar-BDBrrkzt.js → HeaderGithubStar-C8GHdUnS.js} +1 -1
- package/dashboard/dist/assets/{HoverTooltip-qyMezXOs.js → HoverTooltip-BFgXXFLw.js} +1 -1
- package/dashboard/dist/assets/{IpCheckPage-Ces8JIPb.js → IpCheckPage-C62rs4Sf.js} +1 -1
- package/dashboard/dist/assets/{LandingPage-B2TmLGno.js → LandingPage-qa3wKk7Y.js} +2 -2
- package/dashboard/dist/assets/{LeaderboardAvatar-CVVRoNua.js → LeaderboardAvatar-BkznbeVC.js} +1 -1
- package/dashboard/dist/assets/{LeaderboardPage-xtt-bOY8.js → LeaderboardPage-DTNaWWUP.js} +2 -2
- package/dashboard/dist/assets/{LeaderboardProfileModal-BM4d_I_r.js → LeaderboardProfileModal-DkP4Wttv.js} +3 -3
- package/dashboard/dist/assets/{LeaderboardProfilePage-BWqhFLQX.js → LeaderboardProfilePage-C7K3PNq3.js} +1 -1
- package/dashboard/dist/assets/{LimitsPage-BZL4Pcl8.js → LimitsPage-CdQfWCxG.js} +1 -1
- package/dashboard/dist/assets/{LocalOnlyNotice-D4gnOEJj.js → LocalOnlyNotice-ClvY1_wG.js} +1 -1
- package/dashboard/dist/assets/{LoginCard-CB17FQ5w.js → LoginCard-B1kZLJZd.js} +1 -1
- package/dashboard/dist/assets/{LoginModal-BI-O1rH4.js → LoginModal-GiYCVrbT.js} +1 -1
- package/dashboard/dist/assets/{LoginPage-DnGzsvJ4.js → LoginPage-INlKWFTQ.js} +1 -1
- package/dashboard/dist/assets/{PetPage-NRnIKMaJ.js → PetPage-uq1reCKp.js} +1 -1
- package/dashboard/dist/assets/{PopoverPopup-DHoK6UJZ.js → PopoverPopup-Dc5Z66VI.js} +1 -1
- package/dashboard/dist/assets/{ProviderIcon-BTkdx9o2.js → ProviderIcon-Qh2N0LTI.js} +1 -1
- package/dashboard/dist/assets/{ResetPasswordPage-C6K0w318.js → ResetPasswordPage-BdlpShC8.js} +1 -1
- package/dashboard/dist/assets/{SegmentedControl-gEoWioBE.js → SegmentedControl-DCmwiJYf.js} +1 -1
- package/dashboard/dist/assets/{Select-CI-qtEYu.js → Select-BIMHJc2D.js} +1 -1
- package/dashboard/dist/assets/{SelectItemText-q-yhzzjw.js → SelectItemText-CMkxqxvY.js} +1 -1
- package/dashboard/dist/assets/{ServiceStatusPage-C4xD1CEe.js → ServiceStatusPage-C1ll8UfE.js} +1 -1
- package/dashboard/dist/assets/{SessionsPage-CS0Yun2z.js → SessionsPage-DVnwnAWa.js} +1 -1
- package/dashboard/dist/assets/{SettingsPage-GVY_ktz4.js → SettingsPage-CGs5NN_Z.js} +2 -2
- package/dashboard/dist/assets/{SkillsPage-COPRL95O.js → SkillsPage-5hrIvbe1.js} +1 -1
- package/dashboard/dist/assets/{TokenGalaxy-Czi0Ic48.js → TokenGalaxy-BfD0rv-M.js} +1 -1
- package/dashboard/dist/assets/{ToolbarRootContext-BZkTiBJB.js → ToolbarRootContext-BlMLA5Ez.js} +1 -1
- package/dashboard/dist/assets/{TrendMonitor-CnIsuxV-.js → TrendMonitor-DnJCeEQS.js} +1 -1
- package/dashboard/dist/assets/{WidgetsPage-OdX_Fsul.js → WidgetsPage-DsKnuAcr.js} +1 -1
- package/dashboard/dist/assets/{WrappedPage-h9N63-F8.js → WrappedPage-CXwtk-hY.js} +1 -1
- package/dashboard/dist/assets/{__vite-browser-external-BFNWaGsk.js → __vite-browser-external-cuNuS_b-.js} +2 -2
- package/dashboard/dist/assets/{agent-logos-BDiPxrST.js → agent-logos-DxLnGJgJ.js} +1 -1
- package/dashboard/dist/assets/{arrow-up-right-DtFQUeJG.js → arrow-up-right-Cj7u2wr7.js} +1 -1
- package/dashboard/dist/assets/{check-C35onaNL.js → check-DNFOvbbk.js} +1 -1
- package/dashboard/dist/assets/{composite-qxBzts0W.js → composite-DQR0hWa5.js} +1 -1
- package/dashboard/dist/assets/{copy-D_D71TLc.js → copy-D0SHZEpt.js} +1 -1
- package/dashboard/dist/assets/{download-DMQI2hQe.js → download-UlivGLVm.js} +1 -1
- package/dashboard/dist/assets/{external-link-CemM4dWf.js → external-link-rdZCNL_v.js} +1 -1
- package/dashboard/dist/assets/{flame-CIvk0SP5.js → flame-qfy_Ty_R.js} +1 -1
- package/dashboard/dist/assets/{format-tokens-D3JSvcFl.js → format-tokens-DU-AJn2b.js} +1 -1
- package/dashboard/dist/assets/{icons-Ccf5W0AL.js → icons-CZ7P_cMG.js} +1 -1
- package/dashboard/dist/assets/{index-Brd2evyz.js → index-CXnOAYdM.js} +1 -1
- package/dashboard/dist/assets/{index-C8Wri_HI.js → index-DDfiGiEo.js} +1 -1
- package/dashboard/dist/assets/{index-CFXmD_KX.js → index-DKnHC0bC.js} +1 -1
- package/dashboard/dist/assets/{info-CafrKEG8.js → info-BY3dZuIN.js} +1 -1
- package/dashboard/dist/assets/{link-2-Ds-9Nzlp.js → link-2-CSTcMOFn.js} +1 -1
- package/dashboard/dist/assets/{loader-circle-DA4I55-V.js → loader-circle-yIM6ih7L.js} +1 -1
- package/dashboard/dist/assets/{maximize-2-CjMPt5kU.js → maximize-2-C1P3TlYj.js} +1 -1
- package/dashboard/dist/assets/{modifiers.esm-DIxTS09h.js → modifiers.esm-BoQpSulH.js} +1 -1
- package/dashboard/dist/assets/{monitor-smartphone-IZpIOxn3.js → monitor-smartphone-DafIVSH1.js} +1 -1
- package/dashboard/dist/assets/{plus-vbLFNs70.js → plus-B4l58vrs.js} +1 -1
- package/dashboard/dist/assets/{provider-display-BAu_UIfV.js → provider-display-CN_xz_OA.js} +1 -1
- package/dashboard/dist/assets/{react-Djb4VGKb.js → react-B9dzohvC.js} +1 -1
- package/dashboard/dist/assets/{search-Ct6LY9Ke.js → search-B8ejosPU.js} +1 -1
- package/dashboard/dist/assets/{skills-api-BnGI2gEp.js → skills-api-BLC6ZUle.js} +1 -1
- package/dashboard/dist/assets/{sortable.esm-CjCGd6g5.js → sortable.esm-BLUZnPyk.js} +1 -1
- package/dashboard/dist/assets/{store-CBxkOqRe.js → store-3B91_nUE.js} +1 -1
- package/dashboard/dist/assets/{terminal-Dlpdp5Az.js → terminal-DccsqdRD.js} +1 -1
- package/dashboard/dist/assets/{trash-pxUSKv66.js → trash-DfkcSCtq.js} +1 -1
- package/dashboard/dist/assets/{use-community-stats-tHaixRIw.js → use-community-stats-CuIZHVoQ.js} +1 -1
- package/dashboard/dist/assets/{use-latest-request-guard-Cs44WPfL.js → use-latest-request-guard-B_oV9ZfF.js} +1 -1
- package/dashboard/dist/assets/{use-limits-display-prefs-Dw56PXWy.js → use-limits-display-prefs-CXj8GHMY.js} +1 -1
- package/dashboard/dist/assets/{use-ordered-list-DpLx8oLV.js → use-ordered-list-dAFHiuLi.js} +1 -1
- package/dashboard/dist/assets/use-reduced-motion-UMqqLkcq.js +1 -0
- package/dashboard/dist/assets/{use-session-efficiency-pref-BN3FtBvr.js → use-session-efficiency-pref-B9Q45jnR.js} +1 -1
- package/dashboard/dist/assets/{use-transform-C4nTGgzb.js → use-transform-CWR6dZvr.js} +1 -1
- package/dashboard/dist/assets/{use-usage-limits-Dmwdc1yo.js → use-usage-limits-BmwSEzJh.js} +1 -1
- package/dashboard/dist/assets/{useCurrency-DSc3A_Ih.js → useCurrency-BjHTP-xy.js} +1 -1
- package/dashboard/dist/assets/{useTokenFormat-BHjcMgm8.js → useTokenFormat-DMOiOFlN.js} +1 -1
- package/dashboard/dist/assets/{zap-BE8i_wu2.js → zap-v0h1PWbB.js} +1 -1
- package/dashboard/dist/index.html +1 -1
- package/dashboard/dist/ip-check.html +1 -1
- package/dashboard/dist/leaderboard.html +1 -1
- package/dashboard/dist/share.html +1 -1
- package/package.json +1 -1
- package/src/commands/serve.js +147 -17
- package/src/commands/status.js +57 -2
- package/src/commands/sync.js +90 -11
- package/src/lib/codex-context-breakdown.js +3 -1
- package/src/lib/diagnostics.js +20 -1
- package/src/lib/doctor.js +30 -0
- package/src/lib/local-api.js +6 -2
- package/src/lib/pricing/seed-snapshot.json +1 -1
- package/src/lib/scan-roots.js +277 -0
- package/src/lib/session-analytics.js +98 -16
- package/src/lib/usage-limits.js +27 -1
- package/dashboard/dist/assets/DialogDescription-Cof52Icj.js +0 -1
- package/dashboard/dist/assets/use-reduced-motion-DrlJ9X6p.js +0 -1
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
// Persistent extra scan roots for Codex and Claude Code (#657).
|
|
4
|
+
//
|
|
5
|
+
// Agent harnesses run Codex / Claude Code with a private home
|
|
6
|
+
// (CODEX_HOME=..., CLAUDE_CONFIG_DIR=...). Those sessions were invisible to
|
|
7
|
+
// TokenTracker unless the sync happened to be spawned by a process that
|
|
8
|
+
// inherited the same environment: a hook-fired sync saw them, the native app's
|
|
9
|
+
// background refresh did not. Worse, the two producers then disagreed about
|
|
10
|
+
// where a rollout's cursor lives (the cursor store classifies a rollout as a
|
|
11
|
+
// Codex path — per-day shard — only when it sits under one of ITS codexRoots,
|
|
12
|
+
// everything else goes to core), never saw each other's cursor, and re-parsed
|
|
13
|
+
// the same file from byte 0 on every alternation (#639: one 30-minute bucket
|
|
14
|
+
// re-emitted 138 times, 59B tokens for a 1.2B day).
|
|
15
|
+
//
|
|
16
|
+
// So roots come from ONE persistent list that every producer reads, and the
|
|
17
|
+
// cursor store's codexRoots are derived from that same list.
|
|
18
|
+
//
|
|
19
|
+
// Per provider, in order (first occurrence wins; later duplicates by realpath
|
|
20
|
+
// are dropped):
|
|
21
|
+
// 1. the roots the caller already scans (native home, WSL install)
|
|
22
|
+
// 2. the spawning process's implicit root — CODEX_HOME / CLAUDE_CONFIG_DIR
|
|
23
|
+
// 3. config.scanRoots.<provider>[] from ~/.tokentracker/tracker/config.json
|
|
24
|
+
//
|
|
25
|
+
// Config shape:
|
|
26
|
+
// { "scanRoots": { "codex": ["~/.local/share/agent/codex"],
|
|
27
|
+
// "claude": ["~/.local/share/agent/claude", "~/.claude-native"] } }
|
|
28
|
+
|
|
29
|
+
const fs = require("node:fs");
|
|
30
|
+
const os = require("node:os");
|
|
31
|
+
const path = require("node:path");
|
|
32
|
+
|
|
33
|
+
const { readJson } = require("./fs");
|
|
34
|
+
const { resolveTrackerPaths } = require("./tracker-paths");
|
|
35
|
+
|
|
36
|
+
const PROVIDERS = Object.freeze({
|
|
37
|
+
codex: Object.freeze({ envVar: "CODEX_HOME" }),
|
|
38
|
+
claude: Object.freeze({ envVar: "CLAUDE_CONFIG_DIR" }),
|
|
39
|
+
});
|
|
40
|
+
const PROVIDER_NAMES = Object.freeze(Object.keys(PROVIDERS));
|
|
41
|
+
|
|
42
|
+
// Turn a configured root into a stable absolute path. `~` expands against
|
|
43
|
+
// `home`, and a RELATIVE entry is resolved against `home` too — never against
|
|
44
|
+
// process.cwd(), which differs between a hook-fired sync, the CLI and the
|
|
45
|
+
// desktop app and would make producers scan different directories.
|
|
46
|
+
function expandHome(value, home) {
|
|
47
|
+
if (typeof value !== "string") return null;
|
|
48
|
+
const trimmed = value.trim();
|
|
49
|
+
if (!trimmed) return null;
|
|
50
|
+
if (trimmed === "~") return path.resolve(home);
|
|
51
|
+
if (trimmed.startsWith("~/") || trimmed.startsWith("~\\")) {
|
|
52
|
+
return path.resolve(home, trimmed.slice(2));
|
|
53
|
+
}
|
|
54
|
+
return path.isAbsolute(trimmed) ? path.resolve(trimmed) : path.resolve(home, trimmed);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// The spawning process's implicit root — CODEX_HOME / CLAUDE_CONFIG_DIR —
|
|
58
|
+
// normalized ONCE, the same way configured entries are: `~` and relative
|
|
59
|
+
// values are anchored to `home`, never to process.cwd(). Every producer
|
|
60
|
+
// (sync discovery, the cursor store's codexRoots, status, diagnostics, the
|
|
61
|
+
// session browser) must use this value, or a relative CODEX_HOME such as
|
|
62
|
+
// `profiles/codex` is scanned under one directory and cursor-classified
|
|
63
|
+
// under another, and the same rollout replays. Returns null when unset.
|
|
64
|
+
function resolveEnvRoot(provider, { env = process.env, home = os.homedir() } = {}) {
|
|
65
|
+
const spec = PROVIDERS[provider];
|
|
66
|
+
if (!spec) return null;
|
|
67
|
+
const raw = env && typeof env[spec.envVar] === "string" ? env[spec.envVar] : "";
|
|
68
|
+
return expandHome(raw, home);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Accepts { codex: string | string[], claude: string | string[] }. Anything
|
|
72
|
+
// else — wrong type, empty strings, unknown providers — is ignored rather than
|
|
73
|
+
// failing the sync: a malformed config must not stop collection.
|
|
74
|
+
function normalizeScanRootsConfig(raw) {
|
|
75
|
+
const out = {};
|
|
76
|
+
for (const provider of PROVIDER_NAMES) out[provider] = [];
|
|
77
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) return out;
|
|
78
|
+
for (const provider of PROVIDER_NAMES) {
|
|
79
|
+
const value = raw[provider];
|
|
80
|
+
const list = Array.isArray(value) ? value : (typeof value === "string" ? [value] : []);
|
|
81
|
+
for (const entry of list) {
|
|
82
|
+
if (typeof entry !== "string" || !entry.trim()) continue;
|
|
83
|
+
out[provider].push(entry.trim());
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
return out;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// { exists, error }: `exists` is true only for a directory that can be
|
|
90
|
+
// LISTED, not merely stat'ed — a stat-able directory whose listing fails
|
|
91
|
+
// (mode 000, ACL) would otherwise be admitted to the scan, yield no files,
|
|
92
|
+
// read as "present" in status/doctor, and slip past the deferred-repair
|
|
93
|
+
// guard in sync. ENOENT and ENOTDIR (or a regular file in a directory's
|
|
94
|
+
// place) mean the root is absent (error null); any other failure (EACCES,
|
|
95
|
+
// ELOOP, EIO, ...) is reported as `error` so callers can say "unreadable"
|
|
96
|
+
// instead of "missing".
|
|
97
|
+
function scanRootDirState(target, deps = {}) {
|
|
98
|
+
const statSync = deps.statSync || fs.statSync;
|
|
99
|
+
const opendirSync = deps.opendirSync || fs.opendirSync;
|
|
100
|
+
try {
|
|
101
|
+
if (!statSync(target).isDirectory()) return { exists: false, error: null };
|
|
102
|
+
// Probe listability without reading entries; large roots stay cheap.
|
|
103
|
+
opendirSync(target).closeSync();
|
|
104
|
+
return { exists: true, error: null };
|
|
105
|
+
} catch (e) {
|
|
106
|
+
const code = e && typeof e.code === "string" ? e.code : "EUNKNOWN";
|
|
107
|
+
if (code === "ENOENT" || code === "ENOTDIR") return { exists: false, error: null };
|
|
108
|
+
return { exists: false, error: code };
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function isDirectorySync(target, deps) {
|
|
113
|
+
return scanRootDirState(target, deps).exists;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function realpathOrSelf(target, deps) {
|
|
117
|
+
const realpathSync = deps.realpathSync || fs.realpathSync;
|
|
118
|
+
try {
|
|
119
|
+
return realpathSync(target);
|
|
120
|
+
} catch {
|
|
121
|
+
return target;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function identityKey(target, deps) {
|
|
126
|
+
const resolved = path.resolve(target);
|
|
127
|
+
const real = realpathOrSelf(resolved, deps);
|
|
128
|
+
return real.replace(/\\/g, "/").replace(/\/+$/, "");
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Drop later entries that resolve (realpath) to a directory already listed.
|
|
132
|
+
// Used at the projects/ level too: two Claude profiles may symlink one
|
|
133
|
+
// projects/ dir, and reading it under both spellings would double-parse every
|
|
134
|
+
// file, leaving correctness to the bounded claudeHashes layer.
|
|
135
|
+
function dedupeDirsByRealpath(dirs, deps = {}) {
|
|
136
|
+
const seen = new Set();
|
|
137
|
+
const out = [];
|
|
138
|
+
for (const dir of Array.isArray(dirs) ? dirs : []) {
|
|
139
|
+
if (typeof dir !== "string" || !dir) continue;
|
|
140
|
+
const key = identityKey(dir, deps);
|
|
141
|
+
if (seen.has(key)) continue;
|
|
142
|
+
seen.add(key);
|
|
143
|
+
out.push(dir);
|
|
144
|
+
}
|
|
145
|
+
return out;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// Resolve the full, ordered, deduplicated root list per provider.
|
|
149
|
+
//
|
|
150
|
+
// base: { codex: string[], claude: string[] } — roots the caller scans
|
|
151
|
+
// anyway (native home, WSL install); they keep their position and
|
|
152
|
+
// are labelled origin "native".
|
|
153
|
+
// env: CODEX_HOME / CLAUDE_CONFIG_DIR become origin "env" unless already
|
|
154
|
+
// covered by base.
|
|
155
|
+
// config.scanRoots.<provider>[] become origin "config".
|
|
156
|
+
//
|
|
157
|
+
// Every entry: { path, realPath, exists, error, origin }. `exists` is a
|
|
158
|
+
// readable-directory check and `error` carries a stat error code other than
|
|
159
|
+
// absence (see scanRootDirState); non-existent or unreadable configured roots
|
|
160
|
+
// are kept in the list (so cursor-path classification stays stable while a
|
|
161
|
+
// volume is unmounted) and callers that walk directories skip them via
|
|
162
|
+
// `exists`.
|
|
163
|
+
function resolveScanRoots({
|
|
164
|
+
home = os.homedir(),
|
|
165
|
+
env = process.env,
|
|
166
|
+
config = null,
|
|
167
|
+
base = {},
|
|
168
|
+
deps = {},
|
|
169
|
+
} = {}) {
|
|
170
|
+
const configured = normalizeScanRootsConfig(config && typeof config === "object" ? config.scanRoots : null);
|
|
171
|
+
const out = {};
|
|
172
|
+
for (const provider of PROVIDER_NAMES) {
|
|
173
|
+
const candidates = [];
|
|
174
|
+
for (const root of Array.isArray(base[provider]) ? base[provider] : []) {
|
|
175
|
+
if (typeof root === "string" && root.trim()) candidates.push({ raw: root, origin: "native" });
|
|
176
|
+
}
|
|
177
|
+
const envRoot = resolveEnvRoot(provider, { env, home });
|
|
178
|
+
if (envRoot) candidates.push({ raw: envRoot, origin: "env" });
|
|
179
|
+
for (const root of configured[provider]) candidates.push({ raw: root, origin: "config" });
|
|
180
|
+
|
|
181
|
+
const seen = new Set();
|
|
182
|
+
const entries = [];
|
|
183
|
+
for (const candidate of candidates) {
|
|
184
|
+
const expanded = expandHome(candidate.raw, home);
|
|
185
|
+
if (!expanded) continue;
|
|
186
|
+
const resolved = expanded;
|
|
187
|
+
const key = identityKey(resolved, deps);
|
|
188
|
+
if (seen.has(key)) continue;
|
|
189
|
+
seen.add(key);
|
|
190
|
+
const state = scanRootDirState(resolved, deps);
|
|
191
|
+
entries.push({
|
|
192
|
+
path: resolved,
|
|
193
|
+
realPath: realpathOrSelf(resolved, deps),
|
|
194
|
+
exists: state.exists,
|
|
195
|
+
error: state.error,
|
|
196
|
+
origin: candidate.origin,
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
out[provider] = entries;
|
|
200
|
+
}
|
|
201
|
+
return out;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// Append `extraDirs` to `baseDirs`, dropping any extra that resolves (realpath)
|
|
205
|
+
// to a directory already present. Base entries are never collapsed against
|
|
206
|
+
// each other: the native + WSL pair is listed on purpose (see sync.js) and
|
|
207
|
+
// their dedup is handled downstream by file-identity hashes.
|
|
208
|
+
function appendUniqueDirs(baseDirs, extraDirs, deps = {}) {
|
|
209
|
+
const out = [];
|
|
210
|
+
const seen = new Set();
|
|
211
|
+
for (const dir of Array.isArray(baseDirs) ? baseDirs : []) {
|
|
212
|
+
if (typeof dir !== "string" || !dir) continue;
|
|
213
|
+
seen.add(identityKey(dir, deps));
|
|
214
|
+
out.push(dir);
|
|
215
|
+
}
|
|
216
|
+
for (const dir of Array.isArray(extraDirs) ? extraDirs : []) {
|
|
217
|
+
if (typeof dir !== "string" || !dir) continue;
|
|
218
|
+
const key = identityKey(dir, deps);
|
|
219
|
+
if (seen.has(key)) continue;
|
|
220
|
+
seen.add(key);
|
|
221
|
+
out.push(dir);
|
|
222
|
+
}
|
|
223
|
+
return out;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// Roots beyond what the caller already scans: the "env" and "config" entries
|
|
227
|
+
// that exist on disk. Paths only, in list order.
|
|
228
|
+
function extraScanRootPaths(entries) {
|
|
229
|
+
return (Array.isArray(entries) ? entries : [])
|
|
230
|
+
.filter((entry) => entry && entry.origin !== "native" && entry.exists)
|
|
231
|
+
.map((entry) => entry.path);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function hasAnyScanChild(root, children, deps = {}) {
|
|
235
|
+
for (const child of Array.isArray(children) ? children : [children]) {
|
|
236
|
+
if (isDirectorySync(path.join(root, child), deps)) return true;
|
|
237
|
+
}
|
|
238
|
+
return false;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// Human label for status / doctor output.
|
|
242
|
+
function describeScanRootOrigin(entry, provider) {
|
|
243
|
+
if (!entry || entry.origin === "native") return "native";
|
|
244
|
+
if (entry.origin === "env") return PROVIDERS[provider]?.envVar || "env";
|
|
245
|
+
return "scanRoots";
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// "" for a readable root, " (missing)" for an absent one, " (unreadable: CODE)"
|
|
249
|
+
// for a stat failure other than absence.
|
|
250
|
+
function describeScanRootState(entry) {
|
|
251
|
+
if (!entry || entry.exists) return "";
|
|
252
|
+
return entry.error ? ` (unreadable: ${entry.error})` : " (missing)";
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// config.scanRoots as persisted in ~/.tokentracker/tracker/config.json for
|
|
256
|
+
// callers that do not already hold the config (the session browser).
|
|
257
|
+
async function loadScanRootsConfig({ home = os.homedir() } = {}) {
|
|
258
|
+
const { trackerDir } = await resolveTrackerPaths({ home });
|
|
259
|
+
const config = await readJson(path.join(trackerDir, "config.json")).catch(() => null);
|
|
260
|
+
return normalizeScanRootsConfig(config && typeof config === "object" ? config.scanRoots : null);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
module.exports = {
|
|
264
|
+
PROVIDER_NAMES,
|
|
265
|
+
appendUniqueDirs,
|
|
266
|
+
dedupeDirsByRealpath,
|
|
267
|
+
describeScanRootOrigin,
|
|
268
|
+
describeScanRootState,
|
|
269
|
+
expandHome,
|
|
270
|
+
extraScanRootPaths,
|
|
271
|
+
hasAnyScanChild,
|
|
272
|
+
loadScanRootsConfig,
|
|
273
|
+
normalizeScanRootsConfig,
|
|
274
|
+
resolveEnvRoot,
|
|
275
|
+
resolveScanRoots,
|
|
276
|
+
scanRootDirState,
|
|
277
|
+
};
|
|
@@ -33,6 +33,14 @@ const { parseCodexRolloutFile } = require("./codex-rollout-parser");
|
|
|
33
33
|
const { computeRowCost, getModelPricing } = require("./pricing");
|
|
34
34
|
const { USD_TICKS_PER_USD, normalizeGrokUsage } = require("./grok-usage");
|
|
35
35
|
const wsl = require("./wsl-probe");
|
|
36
|
+
const {
|
|
37
|
+
appendUniqueDirs,
|
|
38
|
+
dedupeDirsByRealpath,
|
|
39
|
+
expandHome,
|
|
40
|
+
loadScanRootsConfig,
|
|
41
|
+
normalizeScanRootsConfig,
|
|
42
|
+
scanRootDirState,
|
|
43
|
+
} = require("./scan-roots");
|
|
36
44
|
|
|
37
45
|
// Bump the sidecar when derived metrics change so cached rows are rebuilt
|
|
38
46
|
// instead of leaving the dashboard on the previous (over-counted) heuristic.
|
|
@@ -1074,8 +1082,11 @@ function providerRoots(home, providerDir, env, deps = {}) {
|
|
|
1074
1082
|
&& path.resolve(home) === path.resolve(homedir())
|
|
1075
1083
|
&& typeof env?.CODEX_HOME === "string"
|
|
1076
1084
|
&& env.CODEX_HOME.trim();
|
|
1085
|
+
// Same normalization as sync / the cursor store (scan-roots.expandHome):
|
|
1086
|
+
// a relative CODEX_HOME is anchored to home, never to process.cwd(), so
|
|
1087
|
+
// the browser walks the exact root the cursors were keyed under.
|
|
1077
1088
|
const nativeRoot = useProcessCodexHome
|
|
1078
|
-
?
|
|
1089
|
+
? expandHome(env.CODEX_HOME, home)
|
|
1079
1090
|
: path.join(home, providerDir);
|
|
1080
1091
|
roots.push(nativeRoot);
|
|
1081
1092
|
}
|
|
@@ -1095,7 +1106,21 @@ function providerRoots(home, providerDir, env, deps = {}) {
|
|
|
1095
1106
|
const wslRoot = discoverWslHome(providerDir, { env });
|
|
1096
1107
|
if (wslRoot) roots.push(wslRoot);
|
|
1097
1108
|
}
|
|
1098
|
-
|
|
1109
|
+
// Extra roots (#657), mirroring src/commands/sync.js: CLAUDE_CONFIG_DIR is
|
|
1110
|
+
// the spawning process's implicit Claude root (additive — ~/.claude stays
|
|
1111
|
+
// scanned), and config.scanRoots.<provider> arrives as deps.extraRoots from
|
|
1112
|
+
// discoverSessionFiles. Extras are realpath-deduped against the roots above
|
|
1113
|
+
// so one directory listed under two spellings is walked once.
|
|
1114
|
+
const extras = [];
|
|
1115
|
+
const useProcessClaudeConfigDir = providerDir === ".claude"
|
|
1116
|
+
&& path.resolve(home) === path.resolve(homedir())
|
|
1117
|
+
&& typeof env?.CLAUDE_CONFIG_DIR === "string"
|
|
1118
|
+
&& env.CLAUDE_CONFIG_DIR.trim();
|
|
1119
|
+
if (useProcessClaudeConfigDir) extras.push(expandHome(env.CLAUDE_CONFIG_DIR, home));
|
|
1120
|
+
for (const extra of Array.isArray(deps.extraRoots) ? deps.extraRoots : []) {
|
|
1121
|
+
if (typeof extra === "string" && extra) extras.push(path.resolve(extra));
|
|
1122
|
+
}
|
|
1123
|
+
return appendUniqueDirs([...new Set(roots)], extras);
|
|
1099
1124
|
}
|
|
1100
1125
|
|
|
1101
1126
|
// Group one logical session discovered under more than one root. A group is
|
|
@@ -1221,10 +1246,37 @@ function groupCodexFiles(filePaths) {
|
|
|
1221
1246
|
|
|
1222
1247
|
async function discoverSessionFiles(home, env = process.env, deps = {}) {
|
|
1223
1248
|
const grokHome = resolveGrokHome(home);
|
|
1224
|
-
|
|
1225
|
-
|
|
1249
|
+
// config.scanRoots (#657): read from the tracker config unless the caller
|
|
1250
|
+
// injects it (tests). A missing or malformed config yields no extras.
|
|
1251
|
+
const scanRootsConfig = deps.scanRootsConfig !== undefined
|
|
1252
|
+
? normalizeScanRootsConfig(deps.scanRootsConfig)
|
|
1253
|
+
: await loadScanRootsConfig({ home });
|
|
1254
|
+
// expandHome resolves `~` and relative entries against `home` (never cwd);
|
|
1255
|
+
// only readable directories are walked, absence and stat errors alike.
|
|
1256
|
+
const extraRootsFor = (provider) => scanRootsConfig[provider]
|
|
1257
|
+
.map((root) => expandHome(root, home))
|
|
1258
|
+
.filter((root) => root && scanRootDirState(root).exists);
|
|
1259
|
+
const claudeRoots = providerRoots(home, ".claude", env, { ...deps, extraRoots: extraRootsFor("claude") });
|
|
1260
|
+
const codexRoots = providerRoots(home, ".codex", env, { ...deps, extraRoots: extraRootsFor("codex") });
|
|
1261
|
+
// Two distinct roots may share one projects/ dir through a symlink; dedupe
|
|
1262
|
+
// the derived projects dirs by realpath (as sync does) so the browser never
|
|
1263
|
+
// lists the same transcript twice under two spellings.
|
|
1264
|
+
const claudeProjectsDirs = dedupeDirsByRealpath(claudeRoots.map((r) => path.join(r, "projects")));
|
|
1265
|
+
// listClaudeProjectFiles / listRolloutFilesDeep turn a listing error into an
|
|
1266
|
+
// empty result, so a root whose projects/ (or sessions/) exists but cannot be
|
|
1267
|
+
// read would look like "no sessions" and get cached as such. Report those
|
|
1268
|
+
// directories as incomplete discovery instead; the caller decides not to
|
|
1269
|
+
// persist a partial inventory (sync defers its repair on the same state).
|
|
1270
|
+
const incomplete = [];
|
|
1271
|
+
for (const dir of [
|
|
1272
|
+
...claudeProjectsDirs,
|
|
1273
|
+
...codexRoots.map((r) => path.join(r, "sessions")),
|
|
1274
|
+
]) {
|
|
1275
|
+
const state = scanRootDirState(dir);
|
|
1276
|
+
if (state.error !== null) incomplete.push({ path: dir, error: state.error });
|
|
1277
|
+
}
|
|
1226
1278
|
const [claudeGroups, codexGroups, archivedGroups, grok] = await Promise.all([
|
|
1227
|
-
Promise.all(
|
|
1279
|
+
Promise.all(claudeProjectsDirs.map((dir) => listClaudeProjectFiles(dir))),
|
|
1228
1280
|
Promise.all(codexRoots.map((r) => listRolloutFilesDeep(path.join(r, "sessions")))),
|
|
1229
1281
|
Promise.all(codexRoots.map((r) => listRolloutFilesDeep(path.join(r, "archived_sessions")))),
|
|
1230
1282
|
listGrokSessionFiles(path.join(grokHome, "sessions")),
|
|
@@ -1239,7 +1291,20 @@ async function discoverSessionFiles(home, env = process.env, deps = {}) {
|
|
|
1239
1291
|
const claude = allClaude.filter((filePaths) => !filePaths.some((filePath) => filePath
|
|
1240
1292
|
.split(path.sep)
|
|
1241
1293
|
.some((segment) => segment.endsWith(CLAUDE_MEM_OBSERVER_PROJECT_SUFFIX))));
|
|
1242
|
-
return { claude, codex: groupCodexFiles([...codex, ...archived]), grok };
|
|
1294
|
+
return { claude, codex: groupCodexFiles([...codex, ...archived]), grok, incomplete };
|
|
1295
|
+
}
|
|
1296
|
+
|
|
1297
|
+
// Warn once per unreadable directory per process: the dashboard polls this
|
|
1298
|
+
// path, and a line per poll would flood the serve log.
|
|
1299
|
+
const warnedIncompleteDiscovery = new Set();
|
|
1300
|
+
function warnIncompleteDiscovery(incomplete) {
|
|
1301
|
+
for (const entry of incomplete) {
|
|
1302
|
+
if (warnedIncompleteDiscovery.has(entry.path)) continue;
|
|
1303
|
+
warnedIncompleteDiscovery.add(entry.path);
|
|
1304
|
+
if (!process.env.NODE_TEST_CONTEXT) {
|
|
1305
|
+
console.warn(`[session-analytics] session directory unreadable (${entry.error}), inventory incomplete: ${entry.path}`);
|
|
1306
|
+
}
|
|
1307
|
+
}
|
|
1243
1308
|
}
|
|
1244
1309
|
|
|
1245
1310
|
function filesSignature(files) {
|
|
@@ -1368,6 +1433,19 @@ async function buildSessionAnalyticsInternal({ home = os.homedir(), force = fals
|
|
|
1368
1433
|
} catch { /* first run */ }
|
|
1369
1434
|
}
|
|
1370
1435
|
const discovered = await discoverSessionFiles(home);
|
|
1436
|
+
// Incomplete discovery (an unreadable projects/ or sessions/ dir): never let
|
|
1437
|
+
// a partial inventory replace a complete snapshot. Serve the last complete
|
|
1438
|
+
// sidecar when there is one; otherwise build from what is readable but do
|
|
1439
|
+
// not persist it, so the next refresh tries again.
|
|
1440
|
+
const incompleteDirs = Array.isArray(discovered.incomplete) ? discovered.incomplete : [];
|
|
1441
|
+
if (incompleteDirs.length > 0) {
|
|
1442
|
+
warnIncompleteDiscovery(incompleteDirs);
|
|
1443
|
+
if (!force && previousMeta?.version === SIDECAR_VERSION) {
|
|
1444
|
+
const cached = readSidecar(sidecarPath);
|
|
1445
|
+
Object.defineProperty(cached, "incompleteDirs", { value: incompleteDirs, enumerable: false });
|
|
1446
|
+
return cached;
|
|
1447
|
+
}
|
|
1448
|
+
}
|
|
1371
1449
|
// Codex thread titles are stored separately from rollout files. Include the
|
|
1372
1450
|
// index in the overall signature so an index-only rename reaches the
|
|
1373
1451
|
// per-file dependency check below on the next refresh. Grok titles/metadata
|
|
@@ -1437,19 +1515,22 @@ async function buildSessionAnalyticsInternal({ home = os.homedir(), force = fals
|
|
|
1437
1515
|
nextFiles[cacheKey] = { stat_key: statKey };
|
|
1438
1516
|
}
|
|
1439
1517
|
sessions.sort((a, b) => String(b.ended_at || "").localeCompare(String(a.ended_at || "")));
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1518
|
+
if (incompleteDirs.length === 0) {
|
|
1519
|
+
const content = sessions.map(serializeSessionRecord).join("\n") + (sessions.length ? "\n" : "");
|
|
1520
|
+
await writeAtomic(sidecarPath, content);
|
|
1521
|
+
const generatedAt = new Date().toISOString();
|
|
1522
|
+
await writeAtomic(metaPath, `${JSON.stringify({
|
|
1523
|
+
version: SIDECAR_VERSION,
|
|
1524
|
+
signature,
|
|
1525
|
+
generated_at: generatedAt,
|
|
1526
|
+
checked_at: generatedAt,
|
|
1527
|
+
files: nextFiles,
|
|
1528
|
+
})}\n`);
|
|
1529
|
+
}
|
|
1450
1530
|
// Non-enumerable so the array still behaves exactly like a plain row list
|
|
1451
1531
|
// for every existing caller (map/filter/JSON of the rows is unaffected).
|
|
1452
1532
|
Object.defineProperty(sessions, "skippedFiles", { value: skippedFiles, enumerable: false });
|
|
1533
|
+
Object.defineProperty(sessions, "incompleteDirs", { value: incompleteDirs, enumerable: false });
|
|
1453
1534
|
return sessions;
|
|
1454
1535
|
}
|
|
1455
1536
|
|
|
@@ -2006,6 +2087,7 @@ module.exports = {
|
|
|
2006
2087
|
resumeCommandFor,
|
|
2007
2088
|
sessionsToCsv,
|
|
2008
2089
|
providerRoots,
|
|
2090
|
+
discoverSessionFiles,
|
|
2009
2091
|
dedupeClaudeFilesAcrossRoots,
|
|
2010
2092
|
analyticsEntryStatKey,
|
|
2011
2093
|
};
|
package/src/lib/usage-limits.js
CHANGED
|
@@ -105,6 +105,9 @@ const ANTIGRAVITY_NOT_RUNNING_MESSAGE = "Antigravity IDE is not running. Launch
|
|
|
105
105
|
const CLAUDE_LIMITS_CACHE_FILE = "claude-usage-limits-cache.json";
|
|
106
106
|
const CLAUDE_LIMITS_CACHE_MAX_AGE_MS = 7 * 24 * 60 * 60 * 1000;
|
|
107
107
|
const CLAUDE_LIMITS_CACHE_FRESH_TTL_MS = 10 * 60 * 1000;
|
|
108
|
+
// Longer than the fresh TTL, so a prompt made after a snapshot was written always
|
|
109
|
+
// falls inside it (see claudeCacheAwaitsNewWindow).
|
|
110
|
+
const CLAUDE_UNSTARTED_WINDOW_RETRY_MS = 15 * 60 * 1000;
|
|
108
111
|
// Codex has no rate-limit cooldown like Claude, but its two sequential chatgpt.com requests
|
|
109
112
|
// (/wham/usage + /wham/rate-limit-reset-credits) make it the provider most exposed to slow
|
|
110
113
|
// networks. Persist the last successful read so a timeout serves stale bars instead of a red
|
|
@@ -2388,9 +2391,32 @@ function claudeCacheCrossedReset(raw, { nowMs } = {}) {
|
|
|
2388
2391
|
});
|
|
2389
2392
|
}
|
|
2390
2393
|
|
|
2394
|
+
// An unstarted 5h/7d window (0%, `resets_at: null`) is started by the next prompt,
|
|
2395
|
+
// and Anthropic can lag several minutes behind it, so refetch on every poll within
|
|
2396
|
+
// the retry period after the last prompt, even when the live read is still empty.
|
|
2397
|
+
// The age is measured from the last prompt, not from the cache, because each empty
|
|
2398
|
+
// read rewrites the cache. history.jsonl gains a line per Claude Code prompt; only
|
|
2399
|
+
// its mtime is read. Model-scoped windows stay unstarted while that model is unused.
|
|
2400
|
+
function claudeCacheAwaitsNewWindow(raw, { home, nowMs } = {}) {
|
|
2401
|
+
const unstarted = [raw?.five_hour, raw?.seven_day].some((window) => (
|
|
2402
|
+
window && typeof window === "object" && parseTimeMs(window.resets_at) === null
|
|
2403
|
+
));
|
|
2404
|
+
if (!unstarted) return false;
|
|
2405
|
+
try {
|
|
2406
|
+
const historyPath = path.join(home || os.homedir(), ".claude", "history.jsonl");
|
|
2407
|
+
const ageMs = nowMs - fs.statSync(historyPath).mtimeMs;
|
|
2408
|
+
// A future mtime (clock correction, copied file) would otherwise extend the retry.
|
|
2409
|
+
return ageMs >= 0 && ageMs < CLAUDE_UNSTARTED_WINDOW_RETRY_MS;
|
|
2410
|
+
} catch (_error) {
|
|
2411
|
+
return false;
|
|
2412
|
+
}
|
|
2413
|
+
}
|
|
2414
|
+
|
|
2391
2415
|
function readFreshClaudeLimitsCache({ home, nowMs = Date.now() } = {}) {
|
|
2392
2416
|
const raw = readClaudeLimitsCacheRaw({ home });
|
|
2393
|
-
if (!raw || claudeCacheCrossedReset(raw, { nowMs }))
|
|
2417
|
+
if (!raw || claudeCacheCrossedReset(raw, { nowMs }) || claudeCacheAwaitsNewWindow(raw, { home, nowMs })) {
|
|
2418
|
+
return null;
|
|
2419
|
+
}
|
|
2394
2420
|
return normalizeClaudeCachedLimits(raw, {
|
|
2395
2421
|
nowMs,
|
|
2396
2422
|
maxAgeMs: CLAUDE_LIMITS_CACHE_FRESH_TTL_MS,
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
import{r as n,v as a}from"./__vite-browser-external-BFNWaGsk.js";import{u as p}from"./DialogTitle-CVddcRPZ.js";import{u as c}from"./composite-qxBzts0W.js";const x=n.forwardRef(function(e,r){const{render:l,className:u,style:m,id:s,...t}=e,i=p(),o=c(s);return i.useSyncedValueWithCleanup("descriptionElementId",o),a("p",e,{ref:r,props:[{id:o},t]})});export{x as D};
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
import{r as o}from"./__vite-browser-external-BFNWaGsk.js";import{h as r,i as t,p as s}from"./react-Djb4VGKb.js";function i(){!r.current&&t();const[e]=o.useState(s.current);return e}export{i as u};
|