tokentracker-cli 0.81.2 → 0.82.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dashboard/dist/assets/AchievementBadge-D6iPEEbz.js +1 -0
- package/dashboard/dist/assets/AchievementsPage-Jry355YE.js +2 -0
- package/dashboard/dist/assets/AchievementsSection-ah-kG00E.js +1 -0
- package/dashboard/dist/assets/BadgeDetailModal-FyvUrVvF.js +1 -0
- package/dashboard/dist/assets/Card-C3nLScju.js +1 -0
- package/dashboard/dist/assets/ClawdAnimated-CLHhE26z.js +1 -0
- package/dashboard/dist/assets/CommunityStatsModal-DA3PHLV5.js +1 -0
- package/dashboard/dist/assets/ConfirmModal-CTDarLrD.js +1 -0
- package/dashboard/dist/assets/DashboardPage-DZIiWc3Y.js +1 -0
- package/dashboard/dist/assets/DevicePage-CyXjUO8W.js +1 -0
- package/dashboard/dist/assets/DialogDescription-ahxSNUcB.js +1 -0
- package/dashboard/dist/assets/DialogTitle-z86PHHyk.js +1 -0
- package/dashboard/dist/assets/FadeIn-BDWtfIsO.js +1 -0
- package/dashboard/dist/assets/HeaderGithubStar-DE1USQbb.js +1 -0
- package/dashboard/dist/assets/{IpCheckPage-CMf9CHFr.js → IpCheckPage-B2SkgT0P.js} +3 -3
- package/dashboard/dist/assets/{LandingPage-BT61_TqT.js → LandingPage-PU5gxsfY.js} +165 -165
- package/dashboard/dist/assets/LeaderboardAvatar-CaOV_hFE.js +1 -0
- package/dashboard/dist/assets/LeaderboardPage-B0JtEyRP.js +2 -0
- package/dashboard/dist/assets/{LeaderboardProfileModal-C_kxqOMn.js → LeaderboardProfileModal-DtNEbVH8.js} +4 -4
- package/dashboard/dist/assets/{LeaderboardProfilePage-zgNsE0EQ.js → LeaderboardProfilePage-DRklax3-.js} +3 -3
- package/dashboard/dist/assets/LimitsPage-Dg0cJa40.js +4 -0
- package/dashboard/dist/assets/LocalOnlyNotice-si9PMKya.js +1 -0
- package/dashboard/dist/assets/LoginPage-DgLwhXKo.js +1 -0
- package/dashboard/dist/assets/PetPage-DEZbjk99.js +1 -0
- package/dashboard/dist/assets/PopoverPopup-DX7v1XfE.js +1 -0
- package/dashboard/dist/assets/ResetPasswordPage-C6NvRO-Q.js +1 -0
- package/dashboard/dist/assets/Select-CH2xVU8X.js +1 -0
- package/dashboard/dist/assets/SelectItemText-CCW6JeeN.js +1 -0
- package/dashboard/dist/assets/SettingsPage-Cm3uT11D.js +1 -0
- package/dashboard/dist/assets/SkillsPage-CljRL6yF.js +1 -0
- package/dashboard/dist/assets/TrendMonitor-DM1_WNfw.js +19 -0
- package/dashboard/dist/assets/WidgetsPage-mJ1j6hly.js +1 -0
- package/dashboard/dist/assets/WrappedPage-Dz71ln4t.js +1 -0
- package/dashboard/dist/assets/__vite-browser-external-C7JwykID.css +1 -0
- package/dashboard/dist/assets/__vite-browser-external-Eoyj4eG-.js +1341 -0
- package/dashboard/dist/assets/agent-logos-D3K_ZRUj.js +1 -0
- package/dashboard/dist/assets/arrow-up-right-CT4-gmOS.js +1 -0
- package/dashboard/dist/assets/check-B87_qvAg.js +1 -0
- package/dashboard/dist/assets/copy-Dtr68epm.js +1 -0
- package/dashboard/dist/assets/download-DaWsA_Gj.js +1 -0
- package/dashboard/dist/assets/flame-vLBOq0X0.js +1 -0
- package/dashboard/dist/assets/format-tokens-C6Pidz3s.js +1 -0
- package/dashboard/dist/assets/icons-BaaYJxUN.js +1 -0
- package/dashboard/dist/assets/index-lKnLA6Pd.js +1 -0
- package/dashboard/dist/assets/info-B46Qt16h.js +1 -0
- package/dashboard/dist/assets/limits-providers-13af4PEg.js +1 -0
- package/dashboard/dist/assets/link-2-DeN6MJpT.js +1 -0
- package/dashboard/dist/assets/loader-circle-ByEuaaSw.js +1 -0
- package/dashboard/dist/assets/provider-display-CKbOKeV4.js +42 -0
- package/dashboard/dist/assets/terminal-DyicX7lG.js +1 -0
- package/dashboard/dist/assets/trash-2-CVCC3DtQ.js +1 -0
- package/dashboard/dist/assets/use-community-stats-CExk3D6G.js +1 -0
- package/dashboard/dist/assets/use-limits-display-prefs-CRl4uU95.js +1 -0
- package/dashboard/dist/assets/use-native-settings-Bi6E9dsE.js +1 -0
- package/dashboard/dist/assets/{use-ordered-list-DPbiKnPc.js → use-ordered-list-Dk0p2otw.js} +2 -2
- package/dashboard/dist/assets/use-session-efficiency-pref-YAG-OYIh.js +1 -0
- package/dashboard/dist/assets/use-transform-lflyghWN.js +1 -0
- package/dashboard/dist/assets/use-usage-limits-DJXvJ25Q.js +1 -0
- package/dashboard/dist/assets/useCurrency-BtHtcsO5.js +1 -0
- package/dashboard/dist/assets/useScrollLock-C2C-Yazq.js +1 -0
- package/dashboard/dist/assets/useTokenFormat-BdD057ih.js +1 -0
- package/dashboard/dist/assets/zap-DWWma29E.js +1 -0
- package/dashboard/dist/index.html +2 -2
- package/dashboard/dist/share.html +2 -2
- package/package.json +1 -1
- package/src/cli.js +6 -0
- package/src/commands/serve.js +8 -6
- package/src/commands/sessions.js +29 -0
- package/src/commands/status.js +2 -1
- package/src/commands/sync.js +164 -36
- package/src/lib/codex-context-breakdown.js +330 -13
- package/src/lib/codex-rollout-parser.js +323 -42
- package/src/lib/context-health.js +124 -0
- package/src/lib/cursor-store.js +1289 -0
- package/src/lib/diagnostics.js +8 -5
- package/src/lib/fs.js +254 -16
- package/src/lib/git-outcomes.js +147 -0
- package/src/lib/local-api.js +151 -20
- package/src/lib/outcomes-engine.js +23 -0
- package/src/lib/pricing/index.js +8 -0
- package/src/lib/pricing/seed-snapshot.json +1 -1
- package/src/lib/rollout.js +101 -9
- package/src/lib/session-analytics.js +612 -0
- package/src/lib/usage-limits.js +21 -0
- package/dashboard/dist/assets/AchievementBadge-DXWHZrAL.js +0 -1
- package/dashboard/dist/assets/AchievementsPage-BhrQXniL.js +0 -2
- package/dashboard/dist/assets/AchievementsSection-gzP3m1Ml.js +0 -1
- package/dashboard/dist/assets/BadgeDetailModal-DGKPuvDU.js +0 -1
- package/dashboard/dist/assets/Card-BRtYQ-Ot.js +0 -1
- package/dashboard/dist/assets/ClawdAnimated-UsNG-qDv.js +0 -1
- package/dashboard/dist/assets/CommunityStatsModal-CJQw-P4t.js +0 -1
- package/dashboard/dist/assets/ConfirmModal-ByY4GDsy.js +0 -1
- package/dashboard/dist/assets/DashboardPage-CuLVnW98.js +0 -19
- package/dashboard/dist/assets/DevicePage-D67CxpmR.js +0 -1
- package/dashboard/dist/assets/DialogClose-Ci8hkTw5.js +0 -1
- package/dashboard/dist/assets/DialogDescription-C9vXUZU0.js +0 -1
- package/dashboard/dist/assets/DialogTitle-sLJxKWjv.js +0 -1
- package/dashboard/dist/assets/FadeIn-xVds37w6.js +0 -1
- package/dashboard/dist/assets/HeaderGithubStar-kK3omgmB.js +0 -1
- package/dashboard/dist/assets/LeaderboardAvatar-DyKz6NdT.js +0 -1
- package/dashboard/dist/assets/LeaderboardPage-DJ4zihJJ.js +0 -2
- package/dashboard/dist/assets/LimitsPage-BiQpqxj8.js +0 -4
- package/dashboard/dist/assets/LocalOnlyNotice-DTrrUVbZ.js +0 -1
- package/dashboard/dist/assets/LoginPage-BOM9Q2sI.js +0 -1
- package/dashboard/dist/assets/PetPage-BURk7RG-.js +0 -1
- package/dashboard/dist/assets/PopoverPopup-BV_aoNBd.js +0 -1
- package/dashboard/dist/assets/ResetPasswordPage-SBL2LvXC.js +0 -1
- package/dashboard/dist/assets/Select-BsRdHde3.js +0 -1
- package/dashboard/dist/assets/SelectItemText-DgkrZxIq.js +0 -1
- package/dashboard/dist/assets/SettingsPage-DqxVsfnB.js +0 -1
- package/dashboard/dist/assets/SkillsPage-B0VfWkOh.js +0 -1
- package/dashboard/dist/assets/WidgetsPage-4QQ8wI_g.js +0 -1
- package/dashboard/dist/assets/WrappedPage-BK53khVY.js +0 -1
- package/dashboard/dist/assets/agent-logos-DXpW_Wmz.js +0 -1
- package/dashboard/dist/assets/arrow-up-right-DCc--V5G.js +0 -1
- package/dashboard/dist/assets/check-DqxuosV2.js +0 -1
- package/dashboard/dist/assets/copy-qChAT7MY.js +0 -1
- package/dashboard/dist/assets/download-W9R3sBph.js +0 -1
- package/dashboard/dist/assets/flame-0eyhMnfU.js +0 -1
- package/dashboard/dist/assets/format-tokens-CkfBUcoa.js +0 -1
- package/dashboard/dist/assets/info-DY1O85-U.js +0 -1
- package/dashboard/dist/assets/link-2-B_Z66hFT.js +0 -1
- package/dashboard/dist/assets/loader-circle-xH40lon2.js +0 -1
- package/dashboard/dist/assets/main-CeaB5N36.js +0 -1259
- package/dashboard/dist/assets/main-DAc77vwF.css +0 -1
- package/dashboard/dist/assets/provider-display-C_XKZQcF.js +0 -42
- package/dashboard/dist/assets/trash-2-DVhNJ74r.js +0 -1
- package/dashboard/dist/assets/use-community-stats-CO-nr2Vf.js +0 -1
- package/dashboard/dist/assets/use-limits-display-prefs-RRqmyoxt.js +0 -1
- package/dashboard/dist/assets/use-native-settings-79piPeET.js +0 -1
- package/dashboard/dist/assets/use-quality-per-dollar-pref-DyF49QRF.js +0 -1
- package/dashboard/dist/assets/use-transform-BvXyGAtb.js +0 -1
- package/dashboard/dist/assets/use-usage-limits-BiZ_I-KE.js +0 -1
- package/dashboard/dist/assets/useCurrency-D-C1e7eE.js +0 -1
- package/dashboard/dist/assets/useScrollLock-BfU2EMvG.js +0 -1
- package/dashboard/dist/assets/useTokenFormat-Cm4EvrQp.js +0 -1
- package/dashboard/dist/assets/zap-DlAOTwFs.js +0 -1
|
@@ -0,0 +1,612 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
// Metadata-only session analytics sidecar. This scanner deliberately never
|
|
4
|
+
// persists prompts, assistant text, tool arguments, command output, or diffs.
|
|
5
|
+
|
|
6
|
+
const crypto = require("node:crypto");
|
|
7
|
+
const fs = require("node:fs");
|
|
8
|
+
const fsp = require("node:fs/promises");
|
|
9
|
+
const os = require("node:os");
|
|
10
|
+
const path = require("node:path");
|
|
11
|
+
const readline = require("node:readline");
|
|
12
|
+
const { listClaudeProjectFiles, listRolloutFilesDeep, claudeMessageDedupKey } = require("./rollout");
|
|
13
|
+
const { parseCodexRolloutFile } = require("./codex-rollout-parser");
|
|
14
|
+
const { computeRowCost } = require("./pricing");
|
|
15
|
+
|
|
16
|
+
// Bump the sidecar when derived metrics change so cached rows are rebuilt
|
|
17
|
+
// instead of leaving the dashboard on the previous (over-counted) heuristic.
|
|
18
|
+
const SIDECAR_VERSION = 6;
|
|
19
|
+
const EDIT_TOOLS = new Set(["apply_patch", "edit", "write", "multiedit", "notebookedit"]);
|
|
20
|
+
const PLACEHOLDER_MODELS = new Set(["<synthetic>", "synthetic", "<unknown>", "unknown"]);
|
|
21
|
+
const CLAUDE_MEM_OBSERVER_PROJECT_SUFFIX = "--claude-mem-observer-sessions";
|
|
22
|
+
const CODEX_SUBAGENT_TOOLS = new Set(["spawn_agent", "multi_agent_v1__spawn_agent"]);
|
|
23
|
+
const CODEX_SIGNAL_TOOLS = new Set([...EDIT_TOOLS, ...CODEX_SUBAGENT_TOOLS]);
|
|
24
|
+
|
|
25
|
+
function normalizeSessionModel(value) {
|
|
26
|
+
if (typeof value !== "string") return null;
|
|
27
|
+
const model = value.trim();
|
|
28
|
+
if (!model || PLACEHOLDER_MODELS.has(model.toLowerCase())) return null;
|
|
29
|
+
return model;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function resolveSessionSidecarPath(home = os.homedir()) {
|
|
33
|
+
return path.join(home, ".tokentracker", "tracker", "session.queue.jsonl");
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function sessionHash(source, id) {
|
|
37
|
+
return crypto.createHash("sha256").update(`${source}\0${id || "unknown"}`).digest("hex").slice(0, 24);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function finite(value) {
|
|
41
|
+
const number = Number(value || 0);
|
|
42
|
+
return Number.isFinite(number) && number > 0 ? number : 0;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function tokenTotals(usage) {
|
|
46
|
+
const input_tokens = finite(usage?.input_tokens);
|
|
47
|
+
const cached_input_tokens = finite(usage?.cache_read_input_tokens ?? usage?.cached_input_tokens);
|
|
48
|
+
const cache_creation_input_tokens = finite(usage?.cache_creation_input_tokens);
|
|
49
|
+
const output_tokens = finite(usage?.output_tokens);
|
|
50
|
+
const reasoning_output_tokens = finite(usage?.reasoning_output_tokens);
|
|
51
|
+
const total_tokens = input_tokens + cached_input_tokens + cache_creation_input_tokens + output_tokens;
|
|
52
|
+
return { input_tokens, cached_input_tokens, cache_creation_input_tokens, output_tokens, reasoning_output_tokens, total_tokens };
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function addTotals(target, delta) {
|
|
56
|
+
for (const key of ["input_tokens", "cached_input_tokens", "cache_creation_input_tokens", "output_tokens", "reasoning_output_tokens", "total_tokens"]) {
|
|
57
|
+
target[key] = finite(target[key]) + finite(delta?.[key]);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function emptyTotals() {
|
|
62
|
+
return tokenTotals({});
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function safeTimestamp(value) {
|
|
66
|
+
if (typeof value !== "string" || !Number.isFinite(Date.parse(value))) return null;
|
|
67
|
+
return new Date(value).toISOString();
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function updateBounds(bounds, value) {
|
|
71
|
+
const timestamp = safeTimestamp(value);
|
|
72
|
+
if (!timestamp) return;
|
|
73
|
+
if (!bounds.started_at || timestamp < bounds.started_at) bounds.started_at = timestamp;
|
|
74
|
+
if (!bounds.ended_at || timestamp > bounds.ended_at) bounds.ended_at = timestamp;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function projectKey(cwd, filePath) {
|
|
78
|
+
const value = cwd || path.dirname(filePath || "");
|
|
79
|
+
return path.basename(String(value).replace(/[\\/]+$/, "")) || "unknown";
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function extractClaudePrompt(obj) {
|
|
83
|
+
if (!obj || obj.type !== "user" || obj.isMeta) return null;
|
|
84
|
+
const content = obj.message?.content;
|
|
85
|
+
if (Array.isArray(content)) {
|
|
86
|
+
if (content.length > 0 && content.every((block) => block?.type === "tool_result")) return null;
|
|
87
|
+
const text = content
|
|
88
|
+
.filter((block) => block?.type === "text" && typeof block.text === "string")
|
|
89
|
+
.map((block) => block.text)
|
|
90
|
+
.join("\n")
|
|
91
|
+
.trim();
|
|
92
|
+
if (!text) return null;
|
|
93
|
+
if (text === "[Request interrupted by user]" || text.startsWith("<task-notification>")) return null;
|
|
94
|
+
return text;
|
|
95
|
+
}
|
|
96
|
+
if (typeof content !== "string") return null;
|
|
97
|
+
const text = content.trim();
|
|
98
|
+
if (!text || text === "[Request interrupted by user]" || text.startsWith("<task-notification>")) return null;
|
|
99
|
+
return text;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function promptFingerprint(prompt) {
|
|
103
|
+
return crypto.createHash("sha256").update(prompt.replace(/\s+/g, " ")).digest("hex");
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function extractCodexPrompt(obj) {
|
|
107
|
+
if (obj?.type !== "event_msg" || obj.payload?.type !== "user_message") return null;
|
|
108
|
+
if (typeof obj.payload.message === "string") {
|
|
109
|
+
const message = obj.payload.message.trim();
|
|
110
|
+
return message || null;
|
|
111
|
+
}
|
|
112
|
+
const elements = Array.isArray(obj.payload.text_elements) ? obj.payload.text_elements : [];
|
|
113
|
+
const text = elements
|
|
114
|
+
.map((item) => {
|
|
115
|
+
if (typeof item === "string") return item;
|
|
116
|
+
if (typeof item?.text === "string") return item.text;
|
|
117
|
+
if (typeof item?.value === "string") return item.value;
|
|
118
|
+
return "";
|
|
119
|
+
})
|
|
120
|
+
.filter(Boolean)
|
|
121
|
+
.join("\n")
|
|
122
|
+
.trim();
|
|
123
|
+
return text || null;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function canonicalToolName(value) {
|
|
127
|
+
const name = String(value || "").trim().toLowerCase();
|
|
128
|
+
if (!name) return "";
|
|
129
|
+
return name.replace(/^functions[.:/]/, "").replace(/^tools[.:/]/, "");
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function extractCodexSignalTools(payload) {
|
|
133
|
+
if (!payload || !["function_call", "custom_tool_call"].includes(payload.type)) return [];
|
|
134
|
+
const directName = canonicalToolName(payload.name);
|
|
135
|
+
if (directName && directName !== "exec") return [directName];
|
|
136
|
+
if (directName !== "exec" || typeof payload.input !== "string") return [];
|
|
137
|
+
|
|
138
|
+
const names = [];
|
|
139
|
+
for (const name of CODEX_SIGNAL_TOOLS) {
|
|
140
|
+
const pattern = new RegExp(`\\btools\\.${name}\\s*\\(`, "gi");
|
|
141
|
+
for (const _match of payload.input.matchAll(pattern)) names.push(name);
|
|
142
|
+
}
|
|
143
|
+
return names;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function finalizeRecord(record) {
|
|
147
|
+
const startedMs = Date.parse(record.started_at || "");
|
|
148
|
+
const endedMs = Date.parse(record.ended_at || "");
|
|
149
|
+
record.duration_ms = Number.isFinite(startedMs) && Number.isFinite(endedMs)
|
|
150
|
+
? Math.max(0, endedMs - startedMs)
|
|
151
|
+
: 0;
|
|
152
|
+
record.total_tokens = finite(record.total_tokens || record.tokens?.total_tokens);
|
|
153
|
+
record.cost_usd = computeRowCost({ source: record.source, model: record.model, ...record.tokens });
|
|
154
|
+
record.productive = record.edit_turns > 0;
|
|
155
|
+
// A first-pass delivery has exactly one user turn containing an observed
|
|
156
|
+
// edit and no repeated user request. The legacy one_shot field stays as an
|
|
157
|
+
// API/CSV alias, but now follows this cross-provider definition.
|
|
158
|
+
record.first_pass = record.edit_turns === 1 && record.retry_turns === 0;
|
|
159
|
+
record.one_shot = record.first_pass;
|
|
160
|
+
record.tokens_per_edit = record.edit_turns > 0 ? record.total_tokens / record.edit_turns : null;
|
|
161
|
+
record.cost_per_edit = record.edit_turns > 0 ? record.cost_usd / record.edit_turns : null;
|
|
162
|
+
return record;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
async function scanClaudeSession(filePath) {
|
|
166
|
+
const input = fs.createReadStream(filePath, { encoding: "utf8" });
|
|
167
|
+
const lines = readline.createInterface({ input, crlfDelay: Infinity });
|
|
168
|
+
const tokens = emptyTotals();
|
|
169
|
+
const seenMessages = new Set();
|
|
170
|
+
const bounds = { started_at: null, ended_at: null };
|
|
171
|
+
let rawSessionId = path.basename(filePath, ".jsonl");
|
|
172
|
+
let cwd = null;
|
|
173
|
+
let model = "unknown";
|
|
174
|
+
let turns = 0;
|
|
175
|
+
let editTurns = 0;
|
|
176
|
+
let retryTurns = 0;
|
|
177
|
+
let currentHadEdit = false;
|
|
178
|
+
let subagentCalls = 0;
|
|
179
|
+
const subagentTypes = new Map();
|
|
180
|
+
|
|
181
|
+
function closeTurn() {
|
|
182
|
+
if (currentHadEdit) {
|
|
183
|
+
editTurns += 1;
|
|
184
|
+
}
|
|
185
|
+
currentHadEdit = false;
|
|
186
|
+
}
|
|
187
|
+
let lastPromptFingerprint = null;
|
|
188
|
+
|
|
189
|
+
for await (const line of lines) {
|
|
190
|
+
let obj;
|
|
191
|
+
try { obj = JSON.parse(line); } catch { continue; }
|
|
192
|
+
updateBounds(bounds, obj.timestamp || obj.message?.timestamp);
|
|
193
|
+
if (typeof obj.sessionId === "string" && obj.sessionId) rawSessionId = obj.sessionId;
|
|
194
|
+
if (typeof obj.cwd === "string" && obj.cwd) cwd = obj.cwd;
|
|
195
|
+
if (obj.type === "user") {
|
|
196
|
+
const prompt = extractClaudePrompt(obj);
|
|
197
|
+
if (!prompt) continue;
|
|
198
|
+
const fingerprint = promptFingerprint(prompt);
|
|
199
|
+
if (lastPromptFingerprint && fingerprint === lastPromptFingerprint) retryTurns += 1;
|
|
200
|
+
lastPromptFingerprint = fingerprint;
|
|
201
|
+
closeTurn();
|
|
202
|
+
turns += 1;
|
|
203
|
+
continue;
|
|
204
|
+
}
|
|
205
|
+
if (obj.type !== "assistant" || !obj.message) continue;
|
|
206
|
+
const dedupKey = claudeMessageDedupKey(obj);
|
|
207
|
+
if (dedupKey && seenMessages.has(dedupKey)) continue;
|
|
208
|
+
if (dedupKey) seenMessages.add(dedupKey);
|
|
209
|
+
// Claude writes internal summary/observer messages with model
|
|
210
|
+
// "<synthetic>". They are not a billable model and can appear after the
|
|
211
|
+
// real assistant messages in the same session. Keep the latest real
|
|
212
|
+
// model instead of letting that marker overwrite it.
|
|
213
|
+
const candidateModel = normalizeSessionModel(obj.message.model);
|
|
214
|
+
if (candidateModel) model = candidateModel;
|
|
215
|
+
addTotals(tokens, tokenTotals(obj.message.usage));
|
|
216
|
+
const content = Array.isArray(obj.message.content) ? obj.message.content : [];
|
|
217
|
+
for (const block of content) {
|
|
218
|
+
if (!block || block.type !== "tool_use") continue;
|
|
219
|
+
const name = String(block.name || "").toLowerCase();
|
|
220
|
+
if (EDIT_TOOLS.has(name)) currentHadEdit = true;
|
|
221
|
+
if (name === "agent" || name === "task") {
|
|
222
|
+
subagentCalls += 1;
|
|
223
|
+
const subtype = typeof block.input?.subagent_type === "string"
|
|
224
|
+
? block.input.subagent_type.trim().slice(0, 64)
|
|
225
|
+
: "unspecified";
|
|
226
|
+
subagentTypes.set(subtype || "unspecified", (subagentTypes.get(subtype || "unspecified") || 0) + 1);
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
closeTurn();
|
|
231
|
+
return finalizeRecord({
|
|
232
|
+
version: SIDECAR_VERSION,
|
|
233
|
+
session_hash: sessionHash("claude", rawSessionId),
|
|
234
|
+
source: "claude",
|
|
235
|
+
project_key: projectKey(cwd, filePath),
|
|
236
|
+
project_ref: cwd || null,
|
|
237
|
+
model,
|
|
238
|
+
...bounds,
|
|
239
|
+
turns,
|
|
240
|
+
edit_turns: editTurns,
|
|
241
|
+
retry_turns: retryTurns,
|
|
242
|
+
subagent_calls: subagentCalls,
|
|
243
|
+
subagent_types: Object.fromEntries([...subagentTypes.entries()].sort()),
|
|
244
|
+
tokens,
|
|
245
|
+
provenance: { source: "local-session-log", confidence: "observed", retry_confidence: "inferred", content_retained: false },
|
|
246
|
+
});
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
async function scanCodexDeliverySignals(filePath) {
|
|
250
|
+
const bounds = { started_at: null, ended_at: null };
|
|
251
|
+
const input = fs.createReadStream(filePath, { encoding: "utf8" });
|
|
252
|
+
const lines = readline.createInterface({ input, crlfDelay: Infinity });
|
|
253
|
+
let turns = 0;
|
|
254
|
+
let editTurns = 0;
|
|
255
|
+
let retryTurns = 0;
|
|
256
|
+
let currentTurnOpen = false;
|
|
257
|
+
let currentTurnKey = null;
|
|
258
|
+
let currentHadEdit = false;
|
|
259
|
+
let hasTurnContext = false;
|
|
260
|
+
let lastPromptFingerprint = null;
|
|
261
|
+
let subagentCalls = 0;
|
|
262
|
+
const subagentTypes = new Map();
|
|
263
|
+
|
|
264
|
+
function closeTurn() {
|
|
265
|
+
if (currentTurnOpen && currentHadEdit) editTurns += 1;
|
|
266
|
+
currentHadEdit = false;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
function beginTurn(key) {
|
|
270
|
+
if (currentTurnOpen && key && currentTurnKey === key) return;
|
|
271
|
+
if (currentTurnOpen) closeTurn();
|
|
272
|
+
currentTurnOpen = true;
|
|
273
|
+
currentTurnKey = key || null;
|
|
274
|
+
turns += 1;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
for await (const line of lines) {
|
|
278
|
+
let obj;
|
|
279
|
+
try { obj = JSON.parse(line); } catch { continue; }
|
|
280
|
+
updateBounds(bounds, obj.timestamp);
|
|
281
|
+
if (obj.type === "turn_context") {
|
|
282
|
+
hasTurnContext = true;
|
|
283
|
+
beginTurn(String(obj.payload?.turn_id || obj.timestamp || turns + 1));
|
|
284
|
+
continue;
|
|
285
|
+
}
|
|
286
|
+
const prompt = extractCodexPrompt(obj);
|
|
287
|
+
if (prompt) {
|
|
288
|
+
const fingerprint = promptFingerprint(prompt);
|
|
289
|
+
if (lastPromptFingerprint && fingerprint === lastPromptFingerprint) retryTurns += 1;
|
|
290
|
+
lastPromptFingerprint = fingerprint;
|
|
291
|
+
if (!hasTurnContext) beginTurn(String(obj.timestamp || turns + 1));
|
|
292
|
+
continue;
|
|
293
|
+
}
|
|
294
|
+
if (obj.type !== "response_item") continue;
|
|
295
|
+
const toolNames = extractCodexSignalTools(obj.payload);
|
|
296
|
+
if (!toolNames.length) continue;
|
|
297
|
+
if (!currentTurnOpen) beginTurn(String(obj.timestamp || turns + 1));
|
|
298
|
+
if (toolNames.some((name) => EDIT_TOOLS.has(name))) currentHadEdit = true;
|
|
299
|
+
for (const name of toolNames) {
|
|
300
|
+
if (!CODEX_SUBAGENT_TOOLS.has(name)) continue;
|
|
301
|
+
subagentCalls += 1;
|
|
302
|
+
const displayName = name === "multi_agent_v1__spawn_agent" ? "spawn_agent" : name;
|
|
303
|
+
subagentTypes.set(displayName, (subagentTypes.get(displayName) || 0) + 1);
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
closeTurn();
|
|
307
|
+
return {
|
|
308
|
+
bounds,
|
|
309
|
+
turns,
|
|
310
|
+
editTurns,
|
|
311
|
+
retryTurns,
|
|
312
|
+
subagentCalls,
|
|
313
|
+
subagentTypes: Object.fromEntries([...subagentTypes.entries()].sort()),
|
|
314
|
+
};
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
async function scanCodexSession(filePath) {
|
|
318
|
+
const [parsed, signals] = await Promise.all([
|
|
319
|
+
parseCodexRolloutFile(filePath),
|
|
320
|
+
scanCodexDeliverySignals(filePath),
|
|
321
|
+
]);
|
|
322
|
+
const parsedModel = normalizeSessionModel(parsed.model);
|
|
323
|
+
const provider = normalizeSessionModel(parsed.provider);
|
|
324
|
+
// Older Codex rollouts can omit turn_context.model. The shared parser then
|
|
325
|
+
// falls back to model_provider (for example "openai"), which is provenance
|
|
326
|
+
// rather than a model and must not become a model-table row.
|
|
327
|
+
const model = parsedModel && parsedModel.toLowerCase() !== provider?.toLowerCase()
|
|
328
|
+
? parsedModel
|
|
329
|
+
: "unknown";
|
|
330
|
+
return finalizeRecord({
|
|
331
|
+
version: SIDECAR_VERSION,
|
|
332
|
+
session_hash: sessionHash("codex", parsed.sessionId || filePath),
|
|
333
|
+
source: "codex",
|
|
334
|
+
project_key: projectKey(parsed.cwd, filePath),
|
|
335
|
+
project_ref: parsed.cwd || null,
|
|
336
|
+
model,
|
|
337
|
+
...signals.bounds,
|
|
338
|
+
turns: signals.turns || finite(parsed.turnCount),
|
|
339
|
+
edit_turns: signals.editTurns,
|
|
340
|
+
retry_turns: signals.retryTurns,
|
|
341
|
+
subagent_calls: signals.subagentCalls,
|
|
342
|
+
subagent_types: signals.subagentTypes,
|
|
343
|
+
tokens: parsed.totals || emptyTotals(),
|
|
344
|
+
provenance: { source: "local-session-log", confidence: "observed", retry_confidence: "inferred", content_retained: false },
|
|
345
|
+
});
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
async function discoverSessionFiles(home) {
|
|
349
|
+
const [allClaude, codex, archived] = await Promise.all([
|
|
350
|
+
listClaudeProjectFiles(path.join(home, ".claude", "projects")),
|
|
351
|
+
listRolloutFilesDeep(path.join(home, ".codex", "sessions")),
|
|
352
|
+
listRolloutFilesDeep(path.join(home, ".codex", "archived_sessions")),
|
|
353
|
+
]);
|
|
354
|
+
// Claude Memory stores thousands of background observer transcripts beside
|
|
355
|
+
// real Claude Code sessions. They contain <synthetic>/haiku bookkeeping and
|
|
356
|
+
// no user coding outcome, so scanning them both slows the card dramatically
|
|
357
|
+
// and dilutes its efficiency metrics.
|
|
358
|
+
const claude = allClaude.filter((filePath) => !filePath
|
|
359
|
+
.split(path.sep)
|
|
360
|
+
.some((segment) => segment.endsWith(CLAUDE_MEM_OBSERVER_PROJECT_SUFFIX)));
|
|
361
|
+
const codexBySession = new Map();
|
|
362
|
+
const mtimeOf = (filePath) => {
|
|
363
|
+
try { return fs.statSync(filePath).mtimeMs; } catch { return -Infinity; }
|
|
364
|
+
};
|
|
365
|
+
for (const filePath of [...codex, ...archived]) {
|
|
366
|
+
const id = path.basename(filePath).match(/([0-9a-f-]{36})\.jsonl$/i)?.[1] || filePath;
|
|
367
|
+
const previous = codexBySession.get(id);
|
|
368
|
+
if (!previous || mtimeOf(filePath) > mtimeOf(previous)) codexBySession.set(id, filePath);
|
|
369
|
+
}
|
|
370
|
+
return { claude, codex: [...codexBySession.values()] };
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
function filesSignature(files) {
|
|
374
|
+
const hash = crypto.createHash("sha256");
|
|
375
|
+
for (const filePath of files) {
|
|
376
|
+
try {
|
|
377
|
+
const stat = fs.statSync(filePath);
|
|
378
|
+
hash.update(`${filePath}\0${stat.size}\0${stat.mtimeMs}\n`);
|
|
379
|
+
} catch { /* vanished during discovery */ }
|
|
380
|
+
}
|
|
381
|
+
return hash.digest("hex");
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
function sessionFileCacheKey(source, filePath) {
|
|
385
|
+
return crypto
|
|
386
|
+
.createHash("sha256")
|
|
387
|
+
.update(`${source}\0${path.resolve(filePath)}`)
|
|
388
|
+
.digest("hex")
|
|
389
|
+
.slice(0, 24);
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
function sessionFileStatKey(filePath) {
|
|
393
|
+
try {
|
|
394
|
+
const stat = fs.statSync(filePath);
|
|
395
|
+
return `${stat.size}:${stat.mtimeMs}:${stat.ctimeMs}`;
|
|
396
|
+
} catch {
|
|
397
|
+
return null;
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
function readSidecar(sidecarPath) {
|
|
402
|
+
try {
|
|
403
|
+
return fs.readFileSync(sidecarPath, "utf8").split("\n").filter(Boolean).map((line) => JSON.parse(line));
|
|
404
|
+
} catch { return []; }
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
async function writeAtomic(filePath, content) {
|
|
408
|
+
await fsp.mkdir(path.dirname(filePath), { recursive: true, mode: 0o700 });
|
|
409
|
+
const temp = `${filePath}.${process.pid}.${Date.now()}.tmp`;
|
|
410
|
+
await fsp.writeFile(temp, content, { encoding: "utf8", mode: 0o600 });
|
|
411
|
+
await fsp.rename(temp, filePath);
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
async function buildSessionAnalyticsInternal({ home = os.homedir(), force = false, cacheTtlMs = 5 * 60_000 } = {}) {
|
|
415
|
+
const sidecarPath = resolveSessionSidecarPath(home);
|
|
416
|
+
const metaPath = `${sidecarPath}.meta.json`;
|
|
417
|
+
let previousMeta = null;
|
|
418
|
+
if (!force) {
|
|
419
|
+
try {
|
|
420
|
+
previousMeta = JSON.parse(fs.readFileSync(metaPath, "utf8"));
|
|
421
|
+
const checkedAt = Date.parse(previousMeta.checked_at || previousMeta.generated_at || "");
|
|
422
|
+
if (
|
|
423
|
+
previousMeta.version === SIDECAR_VERSION &&
|
|
424
|
+
Number.isFinite(checkedAt) &&
|
|
425
|
+
Date.now() - checkedAt < Math.max(0, Number(cacheTtlMs) || 0)
|
|
426
|
+
) {
|
|
427
|
+
return readSidecar(sidecarPath);
|
|
428
|
+
}
|
|
429
|
+
} catch { /* first run */ }
|
|
430
|
+
}
|
|
431
|
+
const discovered = await discoverSessionFiles(home);
|
|
432
|
+
const signature = filesSignature([...discovered.claude, ...discovered.codex]);
|
|
433
|
+
if (!force && previousMeta?.version === SIDECAR_VERSION && previousMeta.signature === signature) {
|
|
434
|
+
await writeAtomic(metaPath, `${JSON.stringify({ ...previousMeta, checked_at: new Date().toISOString() })}\n`);
|
|
435
|
+
return readSidecar(sidecarPath);
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
const previousRows = !force && previousMeta?.version === SIDECAR_VERSION
|
|
439
|
+
? readSidecar(sidecarPath)
|
|
440
|
+
: [];
|
|
441
|
+
const previousRowsByFile = new Map(previousRows
|
|
442
|
+
.filter((row) => typeof row?._cache_key === "string")
|
|
443
|
+
.map((row) => [row._cache_key, row]));
|
|
444
|
+
const previousFiles = previousMeta?.version === SIDECAR_VERSION && previousMeta.files
|
|
445
|
+
? previousMeta.files
|
|
446
|
+
: {};
|
|
447
|
+
const nextFiles = {};
|
|
448
|
+
const sessions = [];
|
|
449
|
+
const entries = [
|
|
450
|
+
...discovered.claude.map((filePath) => ({ source: "claude", filePath, scan: scanClaudeSession })),
|
|
451
|
+
...discovered.codex.map((filePath) => ({ source: "codex", filePath, scan: scanCodexSession })),
|
|
452
|
+
];
|
|
453
|
+
for (const entry of entries) {
|
|
454
|
+
const cacheKey = sessionFileCacheKey(entry.source, entry.filePath);
|
|
455
|
+
const statKey = sessionFileStatKey(entry.filePath);
|
|
456
|
+
if (!statKey) continue;
|
|
457
|
+
let row = null;
|
|
458
|
+
if (!force && previousFiles[cacheKey]?.stat_key === statKey) {
|
|
459
|
+
row = previousRowsByFile.get(cacheKey) || null;
|
|
460
|
+
}
|
|
461
|
+
if (!row) {
|
|
462
|
+
try { row = await entry.scan(entry.filePath); } catch { /* one active/partial session must not poison the sidecar */ }
|
|
463
|
+
}
|
|
464
|
+
if (!row) continue;
|
|
465
|
+
// A one-way file hash enables incremental reuse without persisting or
|
|
466
|
+
// exposing the user's local session path.
|
|
467
|
+
row._cache_key = cacheKey;
|
|
468
|
+
sessions.push(row);
|
|
469
|
+
nextFiles[cacheKey] = { stat_key: statKey };
|
|
470
|
+
}
|
|
471
|
+
sessions.sort((a, b) => String(b.ended_at || "").localeCompare(String(a.ended_at || "")));
|
|
472
|
+
const content = sessions.map((row) => JSON.stringify(row)).join("\n") + (sessions.length ? "\n" : "");
|
|
473
|
+
await writeAtomic(sidecarPath, content);
|
|
474
|
+
const generatedAt = new Date().toISOString();
|
|
475
|
+
await writeAtomic(metaPath, `${JSON.stringify({
|
|
476
|
+
version: SIDECAR_VERSION,
|
|
477
|
+
signature,
|
|
478
|
+
generated_at: generatedAt,
|
|
479
|
+
checked_at: generatedAt,
|
|
480
|
+
files: nextFiles,
|
|
481
|
+
})}\n`);
|
|
482
|
+
return sessions;
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
// A cold scan walks every local Claude/Codex session file. Period switches can
|
|
486
|
+
// issue overlapping requests while that scan is still running; share the
|
|
487
|
+
// promise per home so those requests wait for one scan instead of multiplying
|
|
488
|
+
// the disk work and racing the atomic sidecar write.
|
|
489
|
+
const sessionAnalyticsBuilds = new Map();
|
|
490
|
+
|
|
491
|
+
function buildSessionAnalytics(options = {}) {
|
|
492
|
+
const normalizedOptions = options && typeof options === "object" ? options : {};
|
|
493
|
+
const home = path.resolve(String(normalizedOptions.home || os.homedir()));
|
|
494
|
+
const existing = sessionAnalyticsBuilds.get(home);
|
|
495
|
+
if (existing) return existing;
|
|
496
|
+
|
|
497
|
+
const promise = buildSessionAnalyticsInternal({ ...normalizedOptions, home });
|
|
498
|
+
sessionAnalyticsBuilds.set(home, promise);
|
|
499
|
+
const clear = () => {
|
|
500
|
+
if (sessionAnalyticsBuilds.get(home) === promise) sessionAnalyticsBuilds.delete(home);
|
|
501
|
+
};
|
|
502
|
+
promise.then(clear, clear);
|
|
503
|
+
return promise;
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
function summarizeSessions(sessions, { from = "", to = "", includeSessions = true } = {}) {
|
|
507
|
+
const filtered = (sessions || []).filter((row) => {
|
|
508
|
+
const day = String(row.started_at || row.ended_at || "").slice(0, 10);
|
|
509
|
+
return (!from || day >= from) && (!to || day <= to);
|
|
510
|
+
});
|
|
511
|
+
const byModel = new Map();
|
|
512
|
+
const subagents = new Map();
|
|
513
|
+
for (const row of filtered) {
|
|
514
|
+
const key = row.model || "unknown";
|
|
515
|
+
const agg = byModel.get(key) || {
|
|
516
|
+
model: key,
|
|
517
|
+
sessions: 0,
|
|
518
|
+
productive_sessions: 0,
|
|
519
|
+
one_shot_sessions: 0,
|
|
520
|
+
edit_turns: 0,
|
|
521
|
+
retries: 0,
|
|
522
|
+
total_tokens: 0,
|
|
523
|
+
cost_usd: 0,
|
|
524
|
+
edit_tokens: 0,
|
|
525
|
+
edit_cost_usd: 0,
|
|
526
|
+
};
|
|
527
|
+
agg.sessions += 1;
|
|
528
|
+
if (row.productive) agg.productive_sessions += 1;
|
|
529
|
+
if (row.first_pass ?? row.one_shot) agg.one_shot_sessions += 1;
|
|
530
|
+
agg.edit_turns += finite(row.edit_turns);
|
|
531
|
+
agg.retries += finite(row.retry_turns);
|
|
532
|
+
agg.total_tokens += finite(row.total_tokens);
|
|
533
|
+
agg.cost_usd += finite(row.cost_usd);
|
|
534
|
+
if (row.productive) {
|
|
535
|
+
agg.edit_tokens += finite(row.total_tokens);
|
|
536
|
+
agg.edit_cost_usd += finite(row.cost_usd);
|
|
537
|
+
}
|
|
538
|
+
byModel.set(key, agg);
|
|
539
|
+
for (const [name, calls] of Object.entries(row.subagent_types || {})) {
|
|
540
|
+
const sub = subagents.get(name) || { name, calls: 0, sessions: 0, total_tokens: 0, cost_usd: 0 };
|
|
541
|
+
sub.calls += finite(calls);
|
|
542
|
+
sub.sessions += 1;
|
|
543
|
+
// Token allocation is an explicit estimate because vendor logs do not
|
|
544
|
+
// consistently expose child usage separately.
|
|
545
|
+
const share = Math.min(1, finite(calls) / Math.max(1, finite(row.turns)));
|
|
546
|
+
sub.total_tokens += finite(row.total_tokens) * share;
|
|
547
|
+
sub.cost_usd += finite(row.cost_usd) * share;
|
|
548
|
+
subagents.set(name, sub);
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
const by_model = [...byModel.values()].map((row) => ({
|
|
552
|
+
...row,
|
|
553
|
+
productive_rate: row.sessions ? row.productive_sessions / row.sessions : null,
|
|
554
|
+
one_shot_rate: row.productive_sessions ? row.one_shot_sessions / row.productive_sessions : null,
|
|
555
|
+
edit_sessions: row.productive_sessions,
|
|
556
|
+
first_pass_sessions: row.one_shot_sessions,
|
|
557
|
+
edit_session_rate: row.sessions ? row.productive_sessions / row.sessions : null,
|
|
558
|
+
first_pass_rate: row.productive_sessions ? row.one_shot_sessions / row.productive_sessions : null,
|
|
559
|
+
tokens_per_edit: row.edit_turns ? row.edit_tokens / row.edit_turns : null,
|
|
560
|
+
cost_per_edit: row.edit_turns ? row.edit_cost_usd / row.edit_turns : null,
|
|
561
|
+
})).sort((a, b) => b.edit_turns - a.edit_turns || b.productive_sessions - a.productive_sessions || b.sessions - a.sessions);
|
|
562
|
+
const totals = by_model.reduce((acc, row) => {
|
|
563
|
+
for (const key of ["sessions", "productive_sessions", "one_shot_sessions", "edit_turns", "retries", "total_tokens", "cost_usd", "edit_tokens", "edit_cost_usd"]) acc[key] += finite(row[key]);
|
|
564
|
+
return acc;
|
|
565
|
+
}, { sessions: 0, productive_sessions: 0, one_shot_sessions: 0, edit_turns: 0, retries: 0, total_tokens: 0, cost_usd: 0, edit_tokens: 0, edit_cost_usd: 0 });
|
|
566
|
+
return {
|
|
567
|
+
available: filtered.length > 0,
|
|
568
|
+
// Local filesystem paths are required internally for Git attribution but
|
|
569
|
+
// never leave the Node process through API/CSV payloads.
|
|
570
|
+
sessions: includeSessions
|
|
571
|
+
? filtered.map(({ project_ref: _projectRef, _cache_key: _cacheKey, ...row }) => row)
|
|
572
|
+
: [],
|
|
573
|
+
session_count: filtered.length,
|
|
574
|
+
summary: {
|
|
575
|
+
...totals,
|
|
576
|
+
productive_rate: totals.sessions ? totals.productive_sessions / totals.sessions : null,
|
|
577
|
+
one_shot_rate: totals.productive_sessions ? totals.one_shot_sessions / totals.productive_sessions : null,
|
|
578
|
+
edit_sessions: totals.productive_sessions,
|
|
579
|
+
first_pass_sessions: totals.one_shot_sessions,
|
|
580
|
+
edit_session_rate: totals.sessions ? totals.productive_sessions / totals.sessions : null,
|
|
581
|
+
first_pass_rate: totals.productive_sessions ? totals.one_shot_sessions / totals.productive_sessions : null,
|
|
582
|
+
tokens_per_edit: totals.edit_turns ? totals.edit_tokens / totals.edit_turns : null,
|
|
583
|
+
cost_per_edit: totals.edit_turns ? totals.edit_cost_usd / totals.edit_turns : null,
|
|
584
|
+
},
|
|
585
|
+
by_model,
|
|
586
|
+
subagents: [...subagents.values()].sort((a, b) => b.calls - a.calls),
|
|
587
|
+
provenance: {
|
|
588
|
+
source: "local-session-log",
|
|
589
|
+
confidence: "observed",
|
|
590
|
+
privacy: "metadata-only",
|
|
591
|
+
methodology: "edit-turn-v2",
|
|
592
|
+
edit_turn: "user turn containing an observed edit tool",
|
|
593
|
+
first_pass: "exactly one edit turn and no repeated user request",
|
|
594
|
+
},
|
|
595
|
+
};
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
function sessionsToCsv(rows) {
|
|
599
|
+
const columns = ["session_hash", "source", "project_key", "model", "started_at", "ended_at", "duration_ms", "turns", "edit_turns", "retry_turns", "subagent_calls", "total_tokens", "cost_usd", "productive", "first_pass", "one_shot"];
|
|
600
|
+
const escape = (value) => `"${String(value ?? "").replaceAll('"', '""')}"`;
|
|
601
|
+
return [columns.join(","), ...(rows || []).map((row) => columns.map((key) => escape(row[key])).join(","))].join("\n") + "\n";
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
module.exports = {
|
|
605
|
+
resolveSessionSidecarPath,
|
|
606
|
+
sessionHash,
|
|
607
|
+
scanClaudeSession,
|
|
608
|
+
scanCodexSession,
|
|
609
|
+
buildSessionAnalytics,
|
|
610
|
+
summarizeSessions,
|
|
611
|
+
sessionsToCsv,
|
|
612
|
+
};
|
package/src/lib/usage-limits.js
CHANGED
|
@@ -3037,6 +3037,27 @@ async function fetchUsageLimitsUncached({
|
|
|
3037
3037
|
opencodeGo: withPlanLabel(opencodeGo, opencodeGo?.plan_label, "OpenCode Go"),
|
|
3038
3038
|
};
|
|
3039
3039
|
|
|
3040
|
+
for (const [providerName, provider] of Object.entries(data)) {
|
|
3041
|
+
if (providerName === "fetched_at" || !provider || typeof provider !== "object") continue;
|
|
3042
|
+
const capturedAt = provider.cached_at || data.fetched_at;
|
|
3043
|
+
const ageMs = Math.max(0, nowMs - Date.parse(capturedAt || ""));
|
|
3044
|
+
const stale = provider.stale === true || (Number.isFinite(ageMs) && ageMs > 10 * 60 * 1000);
|
|
3045
|
+
const explicitSource = typeof provider.source === "string" ? provider.source : "";
|
|
3046
|
+
const source = explicitSource || (stale ? "disk-cache" : "provider-api");
|
|
3047
|
+
const confidence = /estimate|inferred/i.test(source)
|
|
3048
|
+
? "inferred"
|
|
3049
|
+
: /local|database|sqlite|cache/i.test(source)
|
|
3050
|
+
? "observed"
|
|
3051
|
+
: "official";
|
|
3052
|
+
provider.provenance = {
|
|
3053
|
+
source,
|
|
3054
|
+
confidence,
|
|
3055
|
+
captured_at: capturedAt,
|
|
3056
|
+
stale,
|
|
3057
|
+
age_seconds: Number.isFinite(ageMs) ? Math.round(ageMs / 1000) : null,
|
|
3058
|
+
};
|
|
3059
|
+
}
|
|
3060
|
+
|
|
3040
3061
|
cache = { data, expiresAtMs: cacheExpiresAtMs(data, nowMs) };
|
|
3041
3062
|
return data;
|
|
3042
3063
|
}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
import{c as o,R as N,j as r}from"./main-CeaB5N36.js";import{Z as $}from"./zap-DlAOTwFs.js";import{F as A}from"./flame-0eyhMnfU.js";const j=[["path",{d:"M10 22V7a1 1 0 0 0-1-1H4a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2v-5a1 1 0 0 0-1-1H2",key:"1ah6g2"}],["rect",{x:"14",y:"2",width:"8",height:"8",rx:"1",key:"88lufb"}]],E=o("blocks",j);const L=[["path",{d:"M12 18V5",key:"adv99a"}],["path",{d:"M15 13a4.17 4.17 0 0 1-3-4 4.17 4.17 0 0 1-3 4",key:"1e3is1"}],["path",{d:"M17.598 6.5A3 3 0 1 0 12 5a3 3 0 1 0-5.598 1.5",key:"1gqd8o"}],["path",{d:"M17.997 5.125a4 4 0 0 1 2.526 5.77",key:"iwvgf7"}],["path",{d:"M18 18a4 4 0 0 0 2-7.464",key:"efp6ie"}],["path",{d:"M19.967 17.483A4 4 0 1 1 12 18a4 4 0 1 1-7.967-.517",key:"1gq6am"}],["path",{d:"M6 18a4 4 0 0 1-2-7.464",key:"k1g0md"}],["path",{d:"M6.003 5.125a4 4 0 0 0-2.526 5.77",key:"q97ue3"}]],C=o("brain",L);const T=[["path",{d:"M8 2v4",key:"1cmpym"}],["path",{d:"M16 2v4",key:"4m81vk"}],["rect",{width:"18",height:"18",x:"3",y:"4",rx:"2",key:"1hopcy"}],["path",{d:"M3 10h18",key:"8toen8"}],["path",{d:"M8 14h.01",key:"6423bh"}],["path",{d:"M12 14h.01",key:"1etili"}],["path",{d:"M16 14h.01",key:"1gbofw"}],["path",{d:"M8 18h.01",key:"lrp35t"}],["path",{d:"M12 18h.01",key:"mhygvu"}],["path",{d:"M16 18h.01",key:"kzsmim"}]],z=o("calendar-days",T);const B=[["path",{d:"M11.562 3.266a.5.5 0 0 1 .876 0L15.39 8.87a1 1 0 0 0 1.516.294L21.183 5.5a.5.5 0 0 1 .798.519l-2.834 10.246a1 1 0 0 1-.956.734H5.81a1 1 0 0 1-.957-.734L2.02 6.02a.5.5 0 0 1 .798-.519l4.276 3.664a1 1 0 0 0 1.516-.294z",key:"1vdc57"}],["path",{d:"M5 21h14",key:"11awu3"}]],R=o("crown",B);const q=[["path",{d:"M12.67 19a2 2 0 0 0 1.416-.588l6.154-6.172a6 6 0 0 0-8.49-8.49L5.586 9.914A2 2 0 0 0 5 11.328V18a1 1 0 0 0 1 1z",key:"18jl4k"}],["path",{d:"M16 8 2 22",key:"vp34q"}],["path",{d:"M17.5 15H9",key:"1oz8nu"}]],H=o("feather",q);const D=[["path",{d:"M18 19a5 5 0 0 1-5-5v8",key:"sz5oeg"}],["path",{d:"M9 20H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h3.9a2 2 0 0 1 1.69.9l.81 1.2a2 2 0 0 0 1.67.9H20a2 2 0 0 1 2 2v5",key:"1w6njk"}],["circle",{cx:"13",cy:"12",r:"2",key:"1j92g6"}],["circle",{cx:"20",cy:"19",r:"2",key:"1obnsp"}]],F=o("folder-git-2",D);const I=[["path",{d:"M4 16v-2.38C4 11.5 2.97 10.5 3 8c.03-2.72 1.49-6 4.5-6C9.37 2 10 3.8 10 5.5c0 3.11-2 5.66-2 8.68V16a2 2 0 1 1-4 0Z",key:"1dudjm"}],["path",{d:"M20 20v-2.38c0-2.12 1.03-3.12 1-5.62-.03-2.72-1.49-6-4.5-6C14.63 6 14 7.8 14 9.5c0 3.11 2 5.66 2 8.68V20a2 2 0 1 0 4 0Z",key:"l2t8xc"}],["path",{d:"M16 17h4",key:"1dejxt"}],["path",{d:"M4 13h4",key:"1bwh8b"}]],G=o("footprints",I);const V=[["path",{d:"M2 9.5a5.5 5.5 0 0 1 9.591-3.676.56.56 0 0 0 .818 0A5.49 5.49 0 0 1 22 9.5c0 2.29-1.5 4-3 5.5l-5.492 5.313a2 2 0 0 1-3 .019L5 15c-1.5-1.5-3-3.2-3-5.5",key:"mvr1a0"}]],S=o("heart",V);const Z=[["path",{d:"M7.21 15 2.66 7.14a2 2 0 0 1 .13-2.2L4.4 2.8A2 2 0 0 1 6 2h12a2 2 0 0 1 1.6.8l1.6 2.14a2 2 0 0 1 .14 2.2L16.79 15",key:"143lza"}],["path",{d:"M11 12 5.12 2.2",key:"qhuxz6"}],["path",{d:"m13 12 5.88-9.8",key:"hbye0f"}],["path",{d:"M8 7h8",key:"i86dvs"}],["circle",{cx:"12",cy:"17",r:"5",key:"qbz8iq"}],["path",{d:"M12 18v-2h-.5",key:"fawc4q"}]],O=o("medal",Z);const P=[["path",{d:"M18 5h4",key:"1lhgn2"}],["path",{d:"M20 3v4",key:"1olli1"}],["path",{d:"M20.985 12.486a9 9 0 1 1-9.473-9.472c.405-.022.617.46.402.803a6 6 0 0 0 8.268 8.268c.344-.215.825-.004.803.401",key:"kfwtm"}]],X=o("moon-star",P);const K=[["path",{d:"M12 15v5s3.03-.55 4-2c1.08-1.62 0-5 0-5",key:"qeys4"}],["path",{d:"M4.5 16.5c-1.5 1.26-2 5-2 5s3.74-.5 5-2c.71-.84.7-2.13-.09-2.91a2.18 2.18 0 0 0-2.91-.09",key:"u4xsad"}],["path",{d:"M9 12a22 22 0 0 1 2-3.95A12.88 12.88 0 0 1 22 2c0 2.72-.78 7.5-6 11a22.4 22.4 0 0 1-4 2z",key:"676m9"}],["path",{d:"M9 12H4s.55-3.03 2-4c1.62-1.08 5 .05 5 .05",key:"92ym6u"}]],U=o("rocket",K);const W=[["path",{d:"M20 13c0 5-3.5 7.5-7.66 8.95a1 1 0 0 1-.67-.01C7.5 20.5 4 18 4 13V6a1 1 0 0 1 1-1c2 0 4.5-1.2 6.24-2.72a1.17 1.17 0 0 1 1.52 0C14.51 3.81 17 5 19 5a1 1 0 0 1 1 1z",key:"oel41y"}],["path",{d:"m9 12 2 2 4-4",key:"dzmm74"}]],Y=o("shield-check",W);const J=[["path",{d:"M16 7h6v6",key:"box55l"}],["path",{d:"m22 7-8.5 8.5-5-5L2 17",key:"1t1m79"}]],Q=o("trending-up",J),k=[{id:"token_titan",scope:"cloud",icon:R,format:"tokens",art:"/achievements/token-titan.png"},{id:"big_day",scope:"cloud",icon:$,format:"tokens",art:"/achievements/big-day.png"},{id:"wordsmith",scope:"cloud",icon:H,format:"tokens",art:"/achievements/wordsmith.png"},{id:"marathoner",scope:"cloud",icon:G,format:"days",art:"/achievements/marathoner.png"},{id:"streak",scope:"cloud",icon:A,format:"days",art:"/achievements/streak.png"},{id:"weekend_warrior",scope:"cloud",icon:z,format:"days",art:"/achievements/weekend-warrior.png"},{id:"momentum",scope:"cloud",icon:Q,format:"multiplier",art:"/achievements/momentum.png"},{id:"polyglot",scope:"cloud",icon:C,format:"count",art:"/achievements/polyglot.png"},{id:"trendsetter",scope:"cloud",icon:U,format:"count",art:"/achievements/trendsetter.png"},{id:"multitool",scope:"cloud",icon:E,format:"count",art:"/achievements/multitool.png"},{id:"podium",scope:"cloud",icon:O,format:"rank",art:"/achievements/podium.png"},{id:"veteran",scope:"cloud",icon:Y,format:"days",art:"/achievements/veteran.png"},{id:"project_hopper",scope:"local",icon:F,format:"count",art:"/achievements/project-hopper.png"},{id:"project_devotion",scope:"local",icon:S,format:"tokens",art:"/achievements/project-devotion.png"},{id:"night_owl",scope:"local",icon:X,format:"count",art:"/achievements/night-owl.png"}],u=new Map(k.map(e=>[e.id,e])),m=new Map(k.map((e,a)=>[e.id,a]));function de(e,a){return`achievements.badge.${e}.${a}`}function ee(e){return[...e||[]].sort((a,t)=>{const c=(t?.tier||0)-(a?.tier||0);return c!==0?c:(m.get(a?.id)??99)-(m.get(t?.id)??99)})}function re(e){const a=(e||[]).filter(t=>t&&(t.tier||0)>=1&&u.has(t.id));return a.length===0?null:ee(a)[0]}const v={1:"bronze",2:"silver",3:"gold",4:"diamond"},ie=["bronze","silver","gold","diamond"],ae={bronze:{rim:["#a06a44","#5c3a22"],face:["#d29a6c","#96602f"],glyph:"#3f2817",ring:"#e8c3a0",sheen:"rgba(255, 236, 220, 0.45)",label:"#a06a44"},silver:{rim:["#aab3bb","#67707b"],face:["#dde2e7","#9ba5ae"],glyph:"#39424b",ring:"#f2f5f7",sheen:"rgba(255, 255, 255, 0.55)",label:"#8b959f"},gold:{rim:["#c9971c","#8a6508"],face:["#f2ca52","#cf9c26"],glyph:"#5c430e",ring:"#ffe9a8",sheen:"rgba(255, 248, 214, 0.55)",label:"#b8860b"},diamond:{rim:["#62b1e0","#7c6fd9"],face:["#c8ecf6","#84b4ea"],glyph:"#27496d",ring:"#eafcff",sheen:"rgba(255, 255, 255, 0.65)",label:"#5a9bd8"}},y={rim:["#a8ada9","#7c817d"],face:["#cdd2ce","#a3a8a4"],glyph:"#6a706c",ring:"#dde1de",sheen:"rgba(255, 255, 255, 0.25)",label:"#8b908c"};function te(e,a=!1){return a||!e||e<1?y:ae[v[Math.min(e,4)]]||y}function se(e){return v[e]||null}const g={xs:18,sm:24,md:44,xl:88,lg:108},f={xs:.75,sm:1,md:1.5,xl:2,lg:2};function le({badgeId:e,tier:a=0,size:t="md",locked:c=!1,label:i,className:M=""}){const s=u.get(e),h=c||!a||a<1,n=te(a,h),d=t==="fill",l=d?"100%":g[t]||g.md,b=d?4:f[t]||f.md,[_,x]=N.useState(!1),p=s?.icon||null,w=!!s?.art&&!_;return r.jsx("span",{role:i?"img":void 0,"aria-label":i||void 0,"aria-hidden":i?void 0:!0,className:`inline-flex shrink-0 rounded-full ${M}`,style:{width:l,height:l,padding:b,background:`linear-gradient(135deg, ${n.rim[0]} 0%, ${n.ring} 42%, ${n.rim[1]} 100%)`,boxShadow:d||t==="md"||t==="xl"||t==="lg"?"0 0 0 1px rgba(10, 15, 12, 0.06), 0 1px 2px rgba(10, 15, 12, 0.08)":void 0},children:w?r.jsx("img",{src:s.art,alt:"",draggable:!1,loading:"lazy",onError:()=>x(!0),className:"h-full w-full rounded-full object-cover",style:h?{filter:"grayscale(1)",opacity:.55}:void 0}):r.jsx("span",{className:"flex h-full w-full items-center justify-center rounded-full",style:{background:`linear-gradient(145deg, ${n.face[0]}, ${n.face[1]})`},children:p?r.jsx(p,{size:d?64:Math.round(l*.5),color:n.glyph,strokeWidth:2}):null})})}export{le as A,k as B,S as H,ae as T,u as a,de as b,ie as c,re as h,te as p,ee as s,se as t};
|
|
@@ -1,2 +0,0 @@
|
|
|
1
|
-
const __vite__mapDeps=(i,m=__vite__mapDeps,d=(m.f||(m.f=["assets/BadgeDetailModal-DGKPuvDU.js","assets/main-CeaB5N36.js","assets/main-DAc77vwF.css","assets/AchievementBadge-DXWHZrAL.js","assets/zap-DlAOTwFs.js","assets/flame-0eyhMnfU.js","assets/use-transform-BvXyGAtb.js","assets/AchievementsSection-gzP3m1Ml.js","assets/format-tokens-CkfBUcoa.js","assets/DialogTitle-sLJxKWjv.js","assets/useScrollLock-BfU2EMvG.js"])))=>i.map(i=>d[i]);
|
|
2
|
-
import{r as l,V as x,am as f,U as v,j as e,E as c,L as p,R as g,_ as y,a as b,an as k,a4 as j}from"./main-CeaB5N36.js";import{A as w}from"./AchievementsSection-gzP3m1Ml.js";import{B as h}from"./AchievementBadge-DXWHZrAL.js";import"./format-tokens-CkfBUcoa.js";import"./zap-DlAOTwFs.js";import"./flame-0eyhMnfU.js";function N({enabled:a=!0}={}){const[n,t]=l.useState({status:"loading",achievements:[],error:null}),s=l.useMemo(()=>x(),[]);return l.useEffect(()=>{if(!a){t({status:"disabled",achievements:[],error:null});return}let i=!1;return t(r=>({...r,status:"loading"})),f({timeZone:s,tzOffsetMinutes:v()}).then(r=>{i||t({status:"ready",achievements:Array.isArray(r?.achievements)?r.achievements:[],error:null})}).catch(r=>{i||t({status:"error",achievements:[],error:r})}),()=>{i=!0}},[a,s]),n}const A=g.lazy(()=>j(()=>import("./BadgeDetailModal-DGKPuvDU.js"),__vite__mapDeps([0,1,2,3,4,5,6,7,8,9,10])));function I({authLoading:a,authEnabled:n,authUserId:t,mockEnabled:s}){if(s)return{authLoading:!1,signedIn:!0,userId:"mock-user"};const i=!!(n&&t);return{authLoading:!!a,signedIn:i,userId:i?t:null}}function B(){const a=y(),[n,t]=l.useState({status:"loading",achievements:[],userId:null}),s=b(),{authLoading:i,signedIn:r,userId:o}=I({authLoading:a?.loading,authEnabled:a?.enabled,authUserId:a?.user?.id,mockEnabled:s});return l.useEffect(()=>{if(i){t({status:"loading",achievements:[],userId:null});return}if(!r){t({status:"signed-out",achievements:[],userId:null});return}let d=!1;return t({status:"loading",achievements:[],userId:o}),(async()=>{try{const u=s?null:await a.getAccessToken?.(),m=await k({accessToken:u,userId:o});if(d)return;t({status:"ready",achievements:Array.isArray(m?.badges)?m.badges:[],userId:o})}catch{d||t({status:"error",achievements:[],userId:o})}})(),()=>{d=!0}},[i,s,r,o]),{...n,signedIn:r,settled:_({authLoading:i,signedIn:r,userId:o,state:n})}}function _({authLoading:a,signedIn:n,userId:t,state:s}){return a?!1:n?s.userId===t&&(s.status==="ready"||s.status==="error"):s.status==="signed-out"}function E(){return e.jsx("div",{className:"mx-0 grid animate-pulse grid-cols-[repeat(3,max-content)] justify-between gap-y-7 sm:-mx-2 sm:grid-cols-[repeat(4,max-content)] lg:grid-cols-[repeat(5,max-content)]","aria-hidden":!0,children:Array.from({length:h.length}).map((a,n)=>e.jsxs("div",{className:"flex w-[6.75rem] flex-col items-center px-0 pb-3 pt-4 sm:w-[7.75rem]",children:[e.jsx("div",{className:"h-[108px] w-[108px] rounded-full bg-oai-gray-200 dark:bg-oai-gray-800"}),e.jsx("div",{className:"mt-3 h-3 w-16 rounded bg-oai-gray-200 dark:bg-oai-gray-800"}),e.jsx("div",{className:"mt-2 h-2 w-10 rounded bg-oai-gray-100 dark:bg-oai-gray-800/60"})]},n))})}function R(){const a=N(),n=B(),[t,s]=l.useState(null),i=a.status==="loading"||!n.settled,r=l.useMemo(()=>[...n.achievements||[],...a.achievements||[]],[n.achievements,a.achievements]),o=l.useMemo(()=>r.filter(u=>(u?.tier||0)>=1).length,[r]),d=h.length;return e.jsxs("div",{className:"flex flex-1 flex-col font-oai text-oai-black antialiased dark:text-oai-white",children:[e.jsx("main",{className:"flex-1 pb-12 pt-8 sm:pb-16 sm:pt-10",children:e.jsxs("div",{className:"mx-auto max-w-4xl px-4 sm:px-6",children:[e.jsxs("div",{className:"mb-7 flex items-baseline justify-between gap-4",children:[e.jsxs("div",{className:"min-w-0",children:[e.jsx("h1",{className:"text-3xl font-semibold tracking-tight text-oai-black dark:text-white sm:text-4xl",children:c("achievements.page.title")}),e.jsx("p",{className:"mt-1.5 text-sm text-oai-gray-500 dark:text-oai-gray-400",children:c("achievements.page.local_note")})]}),!i&&e.jsxs("div",{className:"shrink-0 text-right",children:[e.jsxs("div",{className:"font-mono text-2xl font-semibold leading-none tabular-nums text-oai-black dark:text-white",children:[o,e.jsxs("span",{className:"text-oai-gray-400 dark:text-oai-gray-600",children:["/",d]})]}),e.jsx("div",{className:"mt-2 h-1 w-24 overflow-hidden rounded-full bg-oai-gray-200 dark:bg-oai-gray-800",children:e.jsx("div",{className:"h-full rounded-full bg-oai-brand-600 transition-[width] duration-500",style:{width:`${Math.round(o/d*100)}%`}})})]})]}),!n.signedIn&&!i&&e.jsxs("div",{className:"mb-6 flex flex-wrap items-center justify-between gap-x-4 gap-y-2 rounded-xl border border-oai-gray-200 bg-oai-gray-50 px-4 py-3 dark:border-oai-gray-800 dark:bg-oai-gray-900/60",children:[e.jsx("p",{className:"text-sm text-oai-gray-600 dark:text-oai-gray-300",children:c("achievements.signin.prompt")}),e.jsx(p,{to:"/login",className:"shrink-0 rounded-lg bg-oai-brand-600 px-3 py-1.5 text-sm font-medium text-white transition-colors duration-200 hover:bg-oai-brand-700 active:scale-[0.98]",children:c("achievements.signin.action")})]}),i?e.jsx(E,{}):e.jsxs(e.Fragment,{children:[e.jsx(w,{achievements:r,isOwn:!0,columns:"wide",size:"lg",animateIn:!0,onSelect:s}),e.jsxs("p",{className:"mt-10 text-center text-xs text-oai-gray-400 dark:text-oai-gray-500",children:[c("achievements.page.suggest")," ",e.jsx("a",{href:"https://github.com/mm7894215/TokenTracker/issues",target:"_blank",rel:"noopener noreferrer",className:"underline decoration-oai-gray-300 underline-offset-2 transition-colors hover:text-oai-gray-700 hover:decoration-oai-gray-500 dark:decoration-oai-gray-600 dark:hover:text-oai-gray-300",children:c("achievements.page.suggest_link")})]})]})]})}),t&&e.jsx(g.Suspense,{fallback:null,children:e.jsx(A,{badge:t,onClose:()=>s(null)})})]})}export{_ as cloudBadgesSettled,R as default,I as resolveCloudBadgeIdentity};
|