tokentracker-cli 0.83.11 → 0.84.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/README.de.md +1 -0
- package/README.ja.md +1 -0
- package/README.ko.md +1 -0
- package/README.md +1 -0
- package/README.zh-CN.md +1 -0
- package/dashboard/dist/assets/{AchievementBadge-Ddtn6vg2.js → AchievementBadge-BhAc3c3S.js} +1 -1
- package/dashboard/dist/assets/{AchievementsPage-AMo1Ib5I.js → AchievementsPage-waGlLQtl.js} +2 -2
- package/dashboard/dist/assets/{AchievementsSection-CUMjcYst.js → AchievementsSection-T-v65Q1T.js} +1 -1
- package/dashboard/dist/assets/{BadgeDetailModal-CA03iBRx.js → BadgeDetailModal-5FcY9olH.js} +1 -1
- package/dashboard/dist/assets/{Card-DscI10IE.js → Card-BBCwyki4.js} +1 -1
- package/dashboard/dist/assets/{ClawdAnimated-5cIXJ0NQ.js → ClawdAnimated-BZwEWFcR.js} +1 -1
- package/dashboard/dist/assets/{CommandPalette-qCpUMoAO.js → CommandPalette-tKp20y4V.js} +1 -1
- package/dashboard/dist/assets/{CommunityStatsModal-FT_T1UzH.js → CommunityStatsModal-s9IKLboA.js} +1 -1
- package/dashboard/dist/assets/{ConfirmModal-GI2RxzAE.js → ConfirmModal-_Mz-U7UW.js} +1 -1
- package/dashboard/dist/assets/{DashboardPage-CkA3pfjj.js → DashboardPage-BZ2Z_lqN.js} +1 -1
- package/dashboard/dist/assets/DevicePage-Bx5FaEYs.js +1 -0
- package/dashboard/dist/assets/{DialogClose-cfdVTNyN.js → DialogClose-DaB9Ae7L.js} +1 -1
- package/dashboard/dist/assets/DialogDescription-BFVeSjTD.js +1 -0
- package/dashboard/dist/assets/{DialogTitle-CtptgR8Z.js → DialogTitle-mSFue3N1.js} +1 -1
- package/dashboard/dist/assets/{FadeIn-CPpE5UGk.js → FadeIn-D4PsEvSg.js} +1 -1
- package/dashboard/dist/assets/{HeaderGithubStar-vN3e1w0o.js → HeaderGithubStar-8sRKKeCh.js} +1 -1
- package/dashboard/dist/assets/HoverTooltip-8s49xhej.js +1 -0
- package/dashboard/dist/assets/{IpCheckPage-BqJPwFGV.js → IpCheckPage-DwoeRCBI.js} +1 -1
- package/dashboard/dist/assets/LandingPage-DCsOsDK3.js +2 -0
- package/dashboard/dist/assets/{LeaderboardAvatar-BRWPXDZl.js → LeaderboardAvatar-CRO38fgs.js} +1 -1
- package/dashboard/dist/assets/{LeaderboardPage-Dy55Qbr0.js → LeaderboardPage-DlyrceM-.js} +2 -2
- package/dashboard/dist/assets/{LeaderboardProfileModal-Dm5bW2pX.js → LeaderboardProfileModal-DwpalEE6.js} +3 -3
- package/dashboard/dist/assets/{LeaderboardProfilePage-CWmLrKsM.js → LeaderboardProfilePage-BqyfnJa1.js} +1 -1
- package/dashboard/dist/assets/LimitsPage-DS0_zKji.js +4 -0
- package/dashboard/dist/assets/{LocalOnlyNotice-Dso8I7Y4.js → LocalOnlyNotice-BZolN8xC.js} +1 -1
- package/dashboard/dist/assets/{LoginCard-CnghY3oG.js → LoginCard-CyOwV4vy.js} +1 -1
- package/dashboard/dist/assets/{LoginModal-B30O2N_x.js → LoginModal-C6CAcIUw.js} +1 -1
- package/dashboard/dist/assets/{LoginPage-DglsaA23.js → LoginPage-BqG9CVw7.js} +1 -1
- package/dashboard/dist/assets/{PetPage-C7-xrZix.js → PetPage-BtXcBqm3.js} +1 -1
- package/dashboard/dist/assets/{PopoverPopup-CTbuRUy2.js → PopoverPopup-JsU4A3UY.js} +1 -1
- package/dashboard/dist/assets/{ProviderIcon-D-sr7Qko.js → ProviderIcon-C8fKhRQt.js} +1 -1
- package/dashboard/dist/assets/{ResetPasswordPage-CIbj5Hpp.js → ResetPasswordPage-CKmgCNNp.js} +1 -1
- package/dashboard/dist/assets/{SegmentedControl-dxer1vVZ.js → SegmentedControl-LnVjL9no.js} +1 -1
- package/dashboard/dist/assets/{Select-BSQkd3VW.js → Select-DKmJyUtS.js} +1 -1
- package/dashboard/dist/assets/{SelectItemText-EGKNiN7_.js → SelectItemText-BZFLz6Op.js} +1 -1
- package/dashboard/dist/assets/SessionsPage-D3kT552d.js +2 -0
- package/dashboard/dist/assets/{SettingsPage-3MQDH9bF.js → SettingsPage-D2BLe1ZU.js} +1 -1
- package/dashboard/dist/assets/{SkillsPage-DZ8k3xK4.js → SkillsPage-CSmQ6G7z.js} +1 -1
- package/dashboard/dist/assets/{TokenGalaxy-B5q_U7lQ.js → TokenGalaxy-D6D4Uvo5.js} +2 -2
- package/dashboard/dist/assets/{ToolbarRootContext-DIy-0I9L.js → ToolbarRootContext-Bs1pZOOn.js} +1 -1
- package/dashboard/dist/assets/{TrendMonitor-Bj558t-a.js → TrendMonitor-C7g8DZSW.js} +1 -1
- package/dashboard/dist/assets/{WidgetsPage-t9EgVnVK.js → WidgetsPage-Cz1mLk9N.js} +1 -1
- package/dashboard/dist/assets/{WrappedPage-B-8f6-oI.js → WrappedPage-COBjnmTi.js} +1 -1
- package/dashboard/dist/assets/{__vite-browser-external-DwFlHgQR.js → __vite-browser-external-CeOnjjgl.js} +27 -17
- package/dashboard/dist/assets/{__vite-browser-external-CvhQU1Bj.css → __vite-browser-external-WKilYZnq.css} +1 -1
- package/dashboard/dist/assets/{agent-logos-C6ooyoY_.js → agent-logos-B9ssYD4q.js} +1 -1
- package/dashboard/dist/assets/{arrow-up-right-CWOhbEwU.js → arrow-up-right-DAWmHSlU.js} +1 -1
- package/dashboard/dist/assets/{check-DqrkOjUC.js → check-DPhmDgw5.js} +1 -1
- package/dashboard/dist/assets/{chevron-right-DlP0Hiu-.js → chevron-right-kOKJJLco.js} +1 -1
- package/dashboard/dist/assets/{copy-Dh_ZalIb.js → copy-gCs3DnsV.js} +1 -1
- package/dashboard/dist/assets/{download-fssIVO33.js → download-CH_YcQ58.js} +1 -1
- package/dashboard/dist/assets/{flame-COf56i7e.js → flame-BEU-cqaV.js} +1 -1
- package/dashboard/dist/assets/{format-tokens-DA4cG58O.js → format-tokens-DJ-Y0jv5.js} +1 -1
- package/dashboard/dist/assets/host-mode-Bc_iTzHk.js +1 -0
- package/dashboard/dist/assets/{icons-DigJtAfR.js → icons-mjdAc-2x.js} +1 -1
- package/dashboard/dist/assets/{index-b_fO7MtI.js → index-BX-ZVFrB.js} +1 -1
- package/dashboard/dist/assets/{index-DIEScXsc.js → index-DfAyiuyv.js} +1 -1
- package/dashboard/dist/assets/{index-BR90lSkB.js → index-DtrsMn3Y.js} +1 -1
- package/dashboard/dist/assets/{info-mwER43h2.js → info-CkFEh5Zu.js} +1 -1
- package/dashboard/dist/assets/{limits-providers-DyEdxJUi.js → limits-providers-DoHPHfeA.js} +1 -1
- package/dashboard/dist/assets/{link-2-Bfgtp0co.js → link-2-Cx8hG5wc.js} +1 -1
- package/dashboard/dist/assets/{loader-circle-B8ASQdW3.js → loader-circle-9rHTQTld.js} +1 -1
- package/dashboard/dist/assets/{maximize-2-88sF_sdM.js → maximize-2-6nkX0TnZ.js} +1 -1
- package/dashboard/dist/assets/{provider-display-CYWy8Wjh.js → provider-display-RBujrp08.js} +1 -1
- package/dashboard/dist/assets/{react-CcvGEOfz.js → react-CgmQzSd0.js} +1 -1
- package/dashboard/dist/assets/{search-C3gFd5W0.js → search-BpgAnH3M.js} +1 -1
- package/dashboard/dist/assets/{skills-api-BkjJfFxG.js → skills-api-CcfxTvl9.js} +1 -1
- package/dashboard/dist/assets/{store-CGE8neJG.js → store-BLjK5TrP.js} +1 -1
- package/dashboard/dist/assets/{terminal-CjoZ2jlv.js → terminal-Dbn_YmPM.js} +1 -1
- package/dashboard/dist/assets/{trash-2-Buwry_AW.js → trash-2-CUUFECPP.js} +1 -1
- package/dashboard/dist/assets/{use-community-stats-DZW_XQoS.js → use-community-stats-CQQhhlw9.js} +1 -1
- package/dashboard/dist/assets/{use-limits-display-prefs-BD0CXAYR.js → use-limits-display-prefs-CSxCVsC3.js} +1 -1
- package/dashboard/dist/assets/{use-native-settings-CPVIdR4z.js → use-native-settings-DlDAj7ZJ.js} +1 -1
- package/dashboard/dist/assets/{use-ordered-list-_yETN2gW.js → use-ordered-list-BTBKsubs.js} +1 -1
- package/dashboard/dist/assets/use-reduced-motion-DUgAHZln.js +1 -0
- package/dashboard/dist/assets/{use-session-efficiency-pref-mOQHAb6j.js → use-session-efficiency-pref-C_65WyiQ.js} +1 -1
- package/dashboard/dist/assets/{use-transform-CvmlOKm0.js → use-transform-OnxHQlyh.js} +1 -1
- package/dashboard/dist/assets/{use-usage-limits-9W0OuOdp.js → use-usage-limits-Mnco2YgQ.js} +1 -1
- package/dashboard/dist/assets/{useCurrency-BZVRsDTW.js → useCurrency-BS_xyhhg.js} +1 -1
- package/dashboard/dist/assets/{useScrollLock-Dw-jQnFB.js → useScrollLock-Ct5GyoVg.js} +1 -1
- package/dashboard/dist/assets/{useTokenFormat-BHw_Jkes.js → useTokenFormat-RvsCI_oE.js} +1 -1
- package/dashboard/dist/assets/{zap-CeYmAnn-.js → zap-D0DQz21w.js} +1 -1
- package/dashboard/dist/index.html +2 -2
- package/dashboard/dist/ip-check.html +2 -2
- package/dashboard/dist/leaderboard.html +2 -2
- package/dashboard/dist/share.html +2 -2
- package/package.json +1 -1
- package/src/commands/init.js +5 -6
- package/src/commands/status.js +10 -9
- package/src/lib/codex-config.js +45 -4
- package/src/lib/git-outcomes.js +67 -5
- package/src/lib/local-api.js +4 -1
- package/src/lib/pricing/curated-overrides.json +4 -0
- package/src/lib/pricing/seed-snapshot.json +1 -1
- package/src/lib/session-analytics.js +154 -32
- package/dashboard/dist/assets/DevicePage-CdtlanVo.js +0 -1
- package/dashboard/dist/assets/DialogDescription-CKjbQpir.js +0 -1
- package/dashboard/dist/assets/LandingPage-DrjAIdTL.js +0 -2
- package/dashboard/dist/assets/LimitsPage-B538PSle.js +0 -4
- package/dashboard/dist/assets/SessionsPage-CugLdscs.js +0 -1
- package/dashboard/dist/assets/use-reduced-motion-BOf4gJaX.js +0 -1
|
@@ -1,7 +1,26 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
|
|
3
|
-
// Metadata-only session analytics sidecar.
|
|
4
|
-
//
|
|
3
|
+
// Metadata-only session analytics sidecar.
|
|
4
|
+
//
|
|
5
|
+
// Never persisted, anywhere: prompts, assistant message bodies, tool arguments,
|
|
6
|
+
// command output, diffs. Prompts are read only to fingerprint a repeated
|
|
7
|
+
// request (a one-way hash, never stored).
|
|
8
|
+
//
|
|
9
|
+
// Persisted to the LOCAL sidecar only (~/.tokentracker/tracker, 0600 file in a
|
|
10
|
+
// 0700 dir), because the session browser needs them to identify and resume a
|
|
11
|
+
// session:
|
|
12
|
+
// - `title` — the one line the agent wrote to name the session (Claude's
|
|
13
|
+
// "ai-title" record, Codex's thread_name). Agent-authored, not the user's
|
|
14
|
+
// prompt, but it does summarize what the session was about, so treat it as
|
|
15
|
+
// session content: local-only, never uploaded.
|
|
16
|
+
// - `session_id` — the vendor session UUID, needed for `--resume`.
|
|
17
|
+
// - `project_ref` — the session's working directory. The resume command only
|
|
18
|
+
// works from that directory, so the UI shows it and lets the user copy it.
|
|
19
|
+
//
|
|
20
|
+
// All three are stripped in summarizeSessions() before anything reaches the
|
|
21
|
+
// cloud or a CSV export, and the browser endpoint that keeps them is served
|
|
22
|
+
// only over loopback. `test/session-analytics.test.js` guards that boundary —
|
|
23
|
+
// if you add a field here, decide which side of it the field belongs on.
|
|
5
24
|
|
|
6
25
|
const crypto = require("node:crypto");
|
|
7
26
|
const fs = require("node:fs");
|
|
@@ -22,7 +41,14 @@ const { computeRowCost } = require("./pricing");
|
|
|
22
41
|
// the agent wrote itself, never the raw prompt body, and stay local-only
|
|
23
42
|
// alongside session_id (no self-parsed prompt fallback, so the strict
|
|
24
43
|
// metadata-only guarantee holds).
|
|
25
|
-
|
|
44
|
+
// v9 changes derived metrics, so every cached row must be rebuilt:
|
|
45
|
+
// - duration_ms is now *active* time (see IDLE_GAP_MS), not the wall-clock
|
|
46
|
+
// span between the first and last log line, which read as 89 days on
|
|
47
|
+
// resumed/forked sessions.
|
|
48
|
+
// - session_id is only set when the log actually contained a sessionId
|
|
49
|
+
// record. It used to fall back to the file's basename, which produced
|
|
50
|
+
// resume commands for non-session files (`claude --resume journal`).
|
|
51
|
+
const SIDECAR_VERSION = 9;
|
|
26
52
|
const EDIT_TOOLS = new Set(["apply_patch", "edit", "write", "multiedit", "notebookedit"]);
|
|
27
53
|
const PLACEHOLDER_MODELS = new Set(["<synthetic>", "synthetic", "<unknown>", "unknown"]);
|
|
28
54
|
const CLAUDE_MEM_OBSERVER_PROJECT_SUFFIX = "--claude-mem-observer-sessions";
|
|
@@ -74,11 +100,39 @@ function safeTimestamp(value) {
|
|
|
74
100
|
return new Date(value).toISOString();
|
|
75
101
|
}
|
|
76
102
|
|
|
103
|
+
// A session that was resumed days later, or whose file replays inherited
|
|
104
|
+
// history from a fork, spans a wall-clock range that says nothing about how
|
|
105
|
+
// long the user actually worked (observed: 2142h / 89 days on a session with
|
|
106
|
+
// zero turns). Accumulate *active* time instead: the sum of gaps between
|
|
107
|
+
// consecutive log timestamps, excluding any gap longer than IDLE_GAP_MS. The
|
|
108
|
+
// long jump from replayed history to real work is one such gap, so it drops
|
|
109
|
+
// out on its own. started_at / ended_at still carry the true span.
|
|
110
|
+
const IDLE_GAP_MS = 30 * 60 * 1000;
|
|
111
|
+
|
|
112
|
+
function emptyBounds() {
|
|
113
|
+
return { started_at: null, ended_at: null, active_ms: 0, _last_ts_ms: null };
|
|
114
|
+
}
|
|
115
|
+
|
|
77
116
|
function updateBounds(bounds, value) {
|
|
78
117
|
const timestamp = safeTimestamp(value);
|
|
79
118
|
if (!timestamp) return;
|
|
80
119
|
if (!bounds.started_at || timestamp < bounds.started_at) bounds.started_at = timestamp;
|
|
81
120
|
if (!bounds.ended_at || timestamp > bounds.ended_at) bounds.ended_at = timestamp;
|
|
121
|
+
const ms = Date.parse(timestamp);
|
|
122
|
+
if (!Number.isFinite(ms)) return;
|
|
123
|
+
if (Number.isFinite(bounds._last_ts_ms)) {
|
|
124
|
+
const delta = ms - bounds._last_ts_ms;
|
|
125
|
+
// Ignore out-of-order lines and idle gaps; only real working time counts.
|
|
126
|
+
if (delta > 0 && delta <= IDLE_GAP_MS) bounds.active_ms += delta;
|
|
127
|
+
}
|
|
128
|
+
bounds._last_ts_ms = ms;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Throwaway per-agent checkouts (`.claude/worktrees/<id>`, `.codex/worktrees/…`)
|
|
132
|
+
// are not the project the user is working on, and the directory is usually gone
|
|
133
|
+
// by the time the session is browsed.
|
|
134
|
+
function isWorktreeRef(ref) {
|
|
135
|
+
return /[\\/](worktrees|subagents)[\\/]/.test(String(ref || ""));
|
|
82
136
|
}
|
|
83
137
|
|
|
84
138
|
function projectKey(cwd, filePath) {
|
|
@@ -162,11 +216,9 @@ function extractCodexSignalTools(payload) {
|
|
|
162
216
|
}
|
|
163
217
|
|
|
164
218
|
function finalizeRecord(record) {
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
record.duration_ms =
|
|
168
|
-
? Math.max(0, endedMs - startedMs)
|
|
169
|
-
: 0;
|
|
219
|
+
delete record._last_ts_ms;
|
|
220
|
+
record.active_ms = Math.max(0, finite(record.active_ms));
|
|
221
|
+
record.duration_ms = record.active_ms;
|
|
170
222
|
record.total_tokens = finite(record.total_tokens || record.tokens?.total_tokens);
|
|
171
223
|
record.cost_usd = computeRowCost({ source: record.source, model: record.model, ...record.tokens });
|
|
172
224
|
record.productive = record.edit_turns > 0;
|
|
@@ -184,9 +236,26 @@ async function scanClaudeSession(filePath) {
|
|
|
184
236
|
const input = fs.createReadStream(filePath, { encoding: "utf8" });
|
|
185
237
|
const lines = readline.createInterface({ input, crlfDelay: Infinity });
|
|
186
238
|
const tokens = emptyTotals();
|
|
239
|
+
// Per-file, deliberately. rollout.js persists its dedup set across files to
|
|
240
|
+
// catch cross-file duplicates, but that is only sound there because it owns a
|
|
241
|
+
// single global total. Here each file becomes an independently cached row, so
|
|
242
|
+
// a cross-file set would make a row's tokens depend on which other files were
|
|
243
|
+
// scanned first — and the incremental cache reuses rows without replaying
|
|
244
|
+
// that state. Measured cost of the per-file scope on real data: 808 assistant
|
|
245
|
+
// messages appear in more than one file (fork/resume replays history into a
|
|
246
|
+
// new session file), inflating per-session tokens by ~1.5% in aggregate.
|
|
247
|
+
// Fixing it needs a deterministic owner (e.g. earliest started_at) plus
|
|
248
|
+
// message keys persisted per row — a sidecar redesign, not a one-liner. Do
|
|
249
|
+
// NOT "fix" this by widening the set without solving the cache interaction.
|
|
187
250
|
const seenMessages = new Set();
|
|
188
|
-
const bounds =
|
|
251
|
+
const bounds = emptyBounds();
|
|
252
|
+
// The basename keeps grouping stable for files that never write a sessionId
|
|
253
|
+
// record, but only an *observed* sessionId may become a resumable
|
|
254
|
+
// session_id — otherwise non-session logs under ~/.claude/projects (e.g.
|
|
255
|
+
// skill-injections.jsonl, journal.jsonl) yield `claude --resume journal`,
|
|
256
|
+
// a command that always fails.
|
|
189
257
|
let rawSessionId = path.basename(filePath, ".jsonl");
|
|
258
|
+
let observedSessionId = null;
|
|
190
259
|
let cwd = null;
|
|
191
260
|
let model = "unknown";
|
|
192
261
|
let aiTitle = null;
|
|
@@ -209,7 +278,10 @@ async function scanClaudeSession(filePath) {
|
|
|
209
278
|
let obj;
|
|
210
279
|
try { obj = JSON.parse(line); } catch { continue; }
|
|
211
280
|
updateBounds(bounds, obj.timestamp || obj.message?.timestamp);
|
|
212
|
-
if (typeof obj.sessionId === "string" && obj.sessionId)
|
|
281
|
+
if (typeof obj.sessionId === "string" && obj.sessionId) {
|
|
282
|
+
rawSessionId = obj.sessionId;
|
|
283
|
+
observedSessionId = obj.sessionId;
|
|
284
|
+
}
|
|
213
285
|
if (typeof obj.cwd === "string" && obj.cwd) cwd = obj.cwd;
|
|
214
286
|
// Claude writes its own generated one-line summary as an "ai-title" record.
|
|
215
287
|
// It is agent-authored metadata (not the raw prompt body); keep the latest.
|
|
@@ -257,7 +329,7 @@ async function scanClaudeSession(filePath) {
|
|
|
257
329
|
return finalizeRecord({
|
|
258
330
|
version: SIDECAR_VERSION,
|
|
259
331
|
session_hash: sessionHash("claude", rawSessionId),
|
|
260
|
-
session_id:
|
|
332
|
+
session_id: observedSessionId || null,
|
|
261
333
|
// Claude's own generated one-line title (the "ai-title" record). Agent-
|
|
262
334
|
// authored metadata, never the raw prompt body. Local-only: stripped in
|
|
263
335
|
// summarizeSessions before any cloud/CSV export. Null when Claude never
|
|
@@ -279,7 +351,7 @@ async function scanClaudeSession(filePath) {
|
|
|
279
351
|
}
|
|
280
352
|
|
|
281
353
|
async function scanCodexDeliverySignals(filePath) {
|
|
282
|
-
const bounds =
|
|
354
|
+
const bounds = emptyBounds();
|
|
283
355
|
const input = fs.createReadStream(filePath, { encoding: "utf8" });
|
|
284
356
|
const lines = readline.createInterface({ input, crlfDelay: Infinity });
|
|
285
357
|
let turns = 0;
|
|
@@ -541,16 +613,32 @@ async function buildSessionAnalyticsInternal({ home = os.homedir(), force = fals
|
|
|
541
613
|
...discovered.claude.map((filePath) => ({ source: "claude", filePath, scan: scanClaudeSession })),
|
|
542
614
|
...discovered.codex.map((filePath) => ({ source: "codex", filePath, scan: scanCodexSession })),
|
|
543
615
|
];
|
|
616
|
+
// Files we could not turn into a row (permission denied, half-written line,
|
|
617
|
+
// vanished mid-scan). Swallowing these silently made sessions disappear with
|
|
618
|
+
// no signal at all; count them so the API can say so.
|
|
619
|
+
let skippedFiles = 0;
|
|
544
620
|
for (const entry of entries) {
|
|
545
621
|
const cacheKey = sessionFileCacheKey(entry.source, entry.filePath);
|
|
546
622
|
const statKey = analyticsEntryStatKey(entry.source, entry.filePath);
|
|
547
|
-
if (!statKey)
|
|
623
|
+
if (!statKey) {
|
|
624
|
+
skippedFiles += 1;
|
|
625
|
+
continue;
|
|
626
|
+
}
|
|
548
627
|
let row = null;
|
|
549
628
|
if (!force && previousFiles[cacheKey]?.stat_key === statKey) {
|
|
550
629
|
row = previousRowsByFile.get(cacheKey) || null;
|
|
551
630
|
}
|
|
552
631
|
if (!row) {
|
|
553
|
-
try {
|
|
632
|
+
try {
|
|
633
|
+
row = await entry.scan(entry.filePath);
|
|
634
|
+
} catch (error) {
|
|
635
|
+
// One active/partial session must not poison the sidecar, but do not
|
|
636
|
+
// pretend it never existed either.
|
|
637
|
+
skippedFiles += 1;
|
|
638
|
+
if (!process.env.NODE_TEST_CONTEXT) {
|
|
639
|
+
console.warn(`[session-analytics] skipped ${entry.source} session file: ${error?.message || error}`);
|
|
640
|
+
}
|
|
641
|
+
}
|
|
554
642
|
}
|
|
555
643
|
if (!row) continue;
|
|
556
644
|
// A one-way file hash enables incremental reuse without persisting or
|
|
@@ -570,6 +658,9 @@ async function buildSessionAnalyticsInternal({ home = os.homedir(), force = fals
|
|
|
570
658
|
checked_at: generatedAt,
|
|
571
659
|
files: nextFiles,
|
|
572
660
|
})}\n`);
|
|
661
|
+
// Non-enumerable so the array still behaves exactly like a plain row list
|
|
662
|
+
// for every existing caller (map/filter/JSON of the rows is unaffected).
|
|
663
|
+
Object.defineProperty(sessions, "skippedFiles", { value: skippedFiles, enumerable: false });
|
|
573
664
|
return sessions;
|
|
574
665
|
}
|
|
575
666
|
|
|
@@ -582,23 +673,41 @@ const sessionAnalyticsBuilds = new Map();
|
|
|
582
673
|
function buildSessionAnalytics(options = {}) {
|
|
583
674
|
const normalizedOptions = options && typeof options === "object" ? options : {};
|
|
584
675
|
const home = path.resolve(String(normalizedOptions.home || os.homedir()));
|
|
676
|
+
const force = Boolean(normalizedOptions.force);
|
|
585
677
|
const existing = sessionAnalyticsBuilds.get(home);
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
678
|
+
// Joining an in-flight build is only correct when that build is at least as
|
|
679
|
+
// thorough as what this caller asked for. A refresh that landed while a plain
|
|
680
|
+
// build was running used to be handed the un-refreshed result: the spinner
|
|
681
|
+
// stopped and nothing had actually been re-scanned. Chain instead of joining
|
|
682
|
+
// — never run two scans at once, they race the atomic sidecar write.
|
|
683
|
+
if (existing && (!force || existing.force)) return existing.promise;
|
|
684
|
+
|
|
685
|
+
const run = () => buildSessionAnalyticsInternal({ ...normalizedOptions, home, force });
|
|
686
|
+
const promise = existing ? existing.promise.then(run, run) : run();
|
|
687
|
+
const entry = { promise, force: force || Boolean(existing?.force) };
|
|
688
|
+
sessionAnalyticsBuilds.set(home, entry);
|
|
590
689
|
const clear = () => {
|
|
591
|
-
if (sessionAnalyticsBuilds.get(home) ===
|
|
690
|
+
if (sessionAnalyticsBuilds.get(home) === entry) sessionAnalyticsBuilds.delete(home);
|
|
592
691
|
};
|
|
593
692
|
promise.then(clear, clear);
|
|
594
693
|
return promise;
|
|
595
694
|
}
|
|
596
695
|
|
|
696
|
+
// Does a session overlap the [from, to] day window? Testing started_at alone
|
|
697
|
+
// dropped every session that began before the window and ended inside it —
|
|
698
|
+
// exactly the long/resumed ones, which also sort to the top because the sort
|
|
699
|
+
// key is ended_at. On real data a 7-day window lost 6 sessions, the largest
|
|
700
|
+
// 224M tokens. Treat the window as an interval intersection instead.
|
|
701
|
+
function withinDayRange(row, from, to) {
|
|
702
|
+
const startDay = String(row?.started_at || row?.ended_at || "").slice(0, 10);
|
|
703
|
+
const endDay = String(row?.ended_at || row?.started_at || "").slice(0, 10);
|
|
704
|
+
if (from && (!endDay || endDay < from)) return false;
|
|
705
|
+
if (to && (!startDay || startDay > to)) return false;
|
|
706
|
+
return true;
|
|
707
|
+
}
|
|
708
|
+
|
|
597
709
|
function summarizeSessions(sessions, { from = "", to = "", includeSessions = true } = {}) {
|
|
598
|
-
const filtered = (sessions || []).filter((row) =>
|
|
599
|
-
const day = String(row.started_at || row.ended_at || "").slice(0, 10);
|
|
600
|
-
return (!from || day >= from) && (!to || day <= to);
|
|
601
|
-
});
|
|
710
|
+
const filtered = (sessions || []).filter((row) => withinDayRange(row, from, to));
|
|
602
711
|
const byModel = new Map();
|
|
603
712
|
const subagents = new Map();
|
|
604
713
|
for (const row of filtered) {
|
|
@@ -750,14 +859,22 @@ function mergeSessionFragments(rows) {
|
|
|
750
859
|
cur.total_tokens = finite(cur.total_tokens) + finite(row.total_tokens);
|
|
751
860
|
cur.cost_usd = finite(cur.cost_usd) + finite(row.cost_usd);
|
|
752
861
|
cur.productive = Boolean(cur.productive) || Boolean(row.productive);
|
|
862
|
+
cur.active_ms = finite(cur.active_ms) + finite(row.active_ms);
|
|
753
863
|
if (!cur.title && row.title) cur.title = row.title;
|
|
754
864
|
// Representative model/project comes from the busiest fragment (most
|
|
755
865
|
// tokens) so a tiny sidechain cannot overwrite the real coding model.
|
|
756
866
|
if (finite(row.total_tokens) > finite(cur._repr_tokens)) {
|
|
757
867
|
cur._repr_tokens = finite(row.total_tokens);
|
|
758
868
|
if (row.model && row.model !== "unknown") cur.model = row.model;
|
|
759
|
-
|
|
760
|
-
|
|
869
|
+
// Project needs its own guard: a subagent running in a git worktree has
|
|
870
|
+
// that throwaway directory as its cwd, so the busiest fragment can label
|
|
871
|
+
// the whole session with a temp name like "agent-a3cb8089a11d3471a" and
|
|
872
|
+
// point project_ref at a directory that no longer exists. Only let a
|
|
873
|
+
// worktree fragment name the session when nothing better is available.
|
|
874
|
+
if (!isWorktreeRef(row.project_ref) || isWorktreeRef(cur.project_ref)) {
|
|
875
|
+
cur.project_key = row.project_key;
|
|
876
|
+
cur.project_ref = row.project_ref;
|
|
877
|
+
}
|
|
761
878
|
}
|
|
762
879
|
if (row.started_at && (!cur.started_at || row.started_at < cur.started_at)) cur.started_at = row.started_at;
|
|
763
880
|
if (row.ended_at && (!cur.ended_at || row.ended_at > cur.ended_at)) cur.ended_at = row.ended_at;
|
|
@@ -765,9 +882,9 @@ function mergeSessionFragments(rows) {
|
|
|
765
882
|
const merged = [];
|
|
766
883
|
for (const row of byHash.values()) {
|
|
767
884
|
delete row._repr_tokens;
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
row.duration_ms =
|
|
885
|
+
// Active time is additive across fragments (see IDLE_GAP_MS); the
|
|
886
|
+
// wall-clock span between first and last line is not.
|
|
887
|
+
row.duration_ms = finite(row.active_ms);
|
|
771
888
|
row.first_pass = finite(row.edit_turns) === 1 && finite(row.retry_turns) === 0;
|
|
772
889
|
row.one_shot = row.first_pass;
|
|
773
890
|
merged.push(row);
|
|
@@ -780,10 +897,14 @@ function mergeSessionFragments(rows) {
|
|
|
780
897
|
// the UI can offer one-click resume. Callers must only expose it over the
|
|
781
898
|
// local API.
|
|
782
899
|
function listSessionsForBrowser(sessions, { from = "", to = "", limit = 0 } = {}) {
|
|
783
|
-
const filtered = mergeSessionFragments(sessions)
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
900
|
+
const filtered = mergeSessionFragments(sessions)
|
|
901
|
+
// Only sessions that actually spent tokens are worth listing. This drops
|
|
902
|
+
// two kinds of noise: non-session logs under ~/.claude/projects
|
|
903
|
+
// (skill-injections.jsonl, journal.jsonl, …), and sessions abandoned before
|
|
904
|
+
// the model replied. Both render as "unknown · 0 tokens · $0.00" rows with
|
|
905
|
+
// no model, no cost and nothing to analyze.
|
|
906
|
+
.filter((row) => finite(row.total_tokens) > 0)
|
|
907
|
+
.filter((row) => withinDayRange(row, from, to));
|
|
787
908
|
filtered.sort((a, b) => String(b.ended_at || "").localeCompare(String(a.ended_at || "")));
|
|
788
909
|
const cap = Number(limit) > 0 ? Number(limit) : 0;
|
|
789
910
|
const limited = cap > 0 ? filtered.slice(0, cap) : filtered;
|
|
@@ -791,6 +912,7 @@ function listSessionsForBrowser(sessions, { from = "", to = "", limit = 0 } = {}
|
|
|
791
912
|
available: filtered.length > 0,
|
|
792
913
|
session_count: filtered.length,
|
|
793
914
|
returned_count: limited.length,
|
|
915
|
+
skipped_files: finite(sessions?.skippedFiles),
|
|
794
916
|
sessions: limited.map(toSessionBrowserRow),
|
|
795
917
|
provenance: {
|
|
796
918
|
source: "local-session-log",
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
import{a as b,r as l,j as r,c as e,h as v}from"./__vite-browser-external-DwFlHgQR.js";const x={BASE_URL:"/",DEV:!1,MODE:"production",PROD:!0,SSR:!1,VITE_APP_VERSION:"0.83.11",VITE_INSFORGE_ANON_KEY:"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3OC0xMjM0LTU2NzgtOTBhYi1jZGVmMTIzNDU2NzgiLCJlbWFpbCI6ImFub25AaW5zZm9yZ2UuY29tIiwicm9sZSI6ImFub24iLCJpYXQiOjE3ODExNDU5NDd9.T0auta_IrVIh0uXW1bob5QSnzvsnJmN28r5XkSGEuQY",VITE_INSFORGE_BASE_URL:"https://srctyff5.us-east.insforge.app"};function f(){return v(),`${(x&&"https://srctyff5.us-east.insforge.app"||"https://srctyff5.us-east.insforge.app").replace(/\/$/,"")}/functions/tokentracker-device-flow-grant`}function p(s){return(s||"").toUpperCase().replace(/\s+/g,"").replace(/[^A-Z0-9-]/g,"")}function h(){const s=b(),[m,g]=l.useState(""),[i,a]=l.useState({kind:"idle",message:""});l.useEffect(()=>{try{const d=new URLSearchParams(window.location.search).get("user_code");d&&g(p(d))}catch{}},[]);async function u(c){if(c?.preventDefault?.(),!s?.signedIn){a({kind:"error",message:e("device.approval.error.sign_in")});return}const d=p(m);if(!/^[A-HJKMNP-Z2-9]{4}-[A-HJKMNP-Z2-9]{4}$/.test(d)){a({kind:"error",message:e("device.approval.error.invalid_code")});return}a({kind:"working",message:e("device.approval.action.approving")});try{const n=await s.getAccessToken?.();if(!n){a({kind:"error",message:e("device.approval.error.no_token")});return}const o=await fetch(f(),{method:"POST",headers:{"Content-Type":"application/json",Authorization:`Bearer ${n}`},body:JSON.stringify({user_code:d})}),t=await o.json().catch(()=>({}));o.ok?a({kind:"success",message:t.status==="already_approved"?t.client_info?e("device.approval.success.already_client",{client:t.client_info}):e("device.approval.success.already"):t.client_info?e("device.approval.success.approved_client",{client:t.client_info}):e("device.approval.success.approved")}):o.status===404?a({kind:"error",message:e("device.approval.error.not_found")}):o.status===410?a({kind:"error",message:e("device.approval.error.expired")}):o.status===401?a({kind:"error",message:e("device.approval.error.session_expired")}):a({kind:"error",message:t?.error?e("device.approval.error.server_detail",{message:t.error}):e("device.approval.error.server",{status:o.status})})}catch(n){a({kind:"error",message:e("device.approval.error.network",{message:n?.message||n})})}}return r.jsx("div",{className:"min-h-screen flex items-center justify-center bg-white dark:bg-oai-gray-950 px-6 py-12",children:r.jsxs("div",{className:"w-full max-w-md rounded-xl border border-oai-gray-200 dark:border-oai-gray-800 bg-white dark:bg-oai-gray-900 p-8",children:[r.jsx("h1",{className:"text-2xl font-semibold text-oai-gray-900 dark:text-white mb-2",children:e("device.approval.title")}),r.jsx("p",{className:"text-sm text-oai-gray-500 dark:text-oai-gray-400 mb-6",children:e("device.approval.description")}),s?.signedIn?null:r.jsx("div",{className:"rounded-md border border-amber-200 bg-amber-50 dark:bg-amber-900/20 dark:border-amber-800 p-4 mb-4",children:r.jsx("p",{className:"text-sm text-amber-700 dark:text-amber-300",children:e("device.approval.sign_in_banner")})}),r.jsxs("form",{onSubmit:u,className:"space-y-4",children:[r.jsxs("label",{className:"block",children:[r.jsx("span",{className:"block text-xs font-medium uppercase tracking-wide text-oai-gray-500 dark:text-oai-gray-400 mb-1",children:e("device.approval.code_label")}),r.jsx("input",{type:"text",value:m,onChange:c=>g(p(c.target.value)),placeholder:"XXXX-XXXX",spellCheck:!1,autoCapitalize:"characters",maxLength:9,className:"w-full font-mono text-xl tracking-widest text-center rounded-md border border-oai-gray-300 dark:border-oai-gray-700 bg-white dark:bg-oai-gray-900 text-oai-gray-900 dark:text-white px-4 py-3 focus:outline-none focus:ring-inset focus:ring-2 focus:ring-emerald-600"})]}),r.jsx("button",{type:"submit",disabled:i.kind==="working"||!s?.signedIn,className:"w-full rounded-md bg-emerald-600 hover:bg-emerald-500 disabled:bg-oai-gray-300 disabled:cursor-not-allowed text-white font-medium py-3 transition-colors",children:i.kind==="working"?e("device.approval.action.approving"):e("device.approval.action.approve")})]}),i.kind==="success"&&r.jsx("div",{className:"mt-4 rounded-md border border-emerald-200 bg-emerald-50 dark:bg-emerald-900/20 dark:border-emerald-800 p-4",children:r.jsx("p",{className:"text-sm text-emerald-700 dark:text-emerald-300",children:i.message})}),i.kind==="error"&&r.jsx("div",{className:"mt-4 rounded-md border border-red-200 bg-red-50 dark:bg-red-900/20 dark:border-red-800 p-4",children:r.jsx("p",{className:"text-sm text-red-700 dark:text-red-300",children:i.message})})]})})}export{h as default};
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
import{r as n,z as a}from"./__vite-browser-external-DwFlHgQR.js";import{u as p}from"./DialogTitle-CtptgR8Z.js";import{u as c}from"./useScrollLock-Dw-jQnFB.js";const x=n.forwardRef(function(e,r){const{render:l,className:u,style:m,id:s,...t}=e,{store:i}=p(),o=c(s);return i.useSyncedValueWithCleanup("descriptionElementId",o),a("p",e,{ref:r,props:[{id:o},t]})});export{x as D};
|