ccgauge 0.3.1 → 1.0.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/.next/standalone/.next/BUILD_ID +1 -1
- package/.next/standalone/.next/app-build-manifest.json +41 -41
- package/.next/standalone/.next/app-path-routes-manifest.json +6 -6
- package/.next/standalone/.next/build-manifest.json +2 -2
- package/.next/standalone/.next/prerender-manifest.json +3 -3
- package/.next/standalone/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/api/blocks/route.js +1 -1
- package/.next/standalone/.next/server/app/api/blocks/route_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/api/export/usage/route.js +1 -1
- package/.next/standalone/.next/server/app/api/export/usage/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/export/usage/route_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/api/pricing/route.js +1 -1
- package/.next/standalone/.next/server/app/api/pricing/route_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/api/projects/route.js +1 -1
- package/.next/standalone/.next/server/app/api/projects/route_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/api/scan/route.js +1 -1
- package/.next/standalone/.next/server/app/api/scan/route_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/api/sessions/route.js +1 -1
- package/.next/standalone/.next/server/app/api/sessions/route_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/api/usage/route.js +1 -1
- package/.next/standalone/.next/server/app/api/usage/route_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/models/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/page.js +2 -2
- package/.next/standalone/.next/server/app/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/projects/[id]/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/projects/page.js +1 -1
- package/.next/standalone/.next/server/app/projects/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/sessions/[id]/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/sessions/page.js +1 -1
- package/.next/standalone/.next/server/app/sessions/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/settings/page.js +2 -2
- package/.next/standalone/.next/server/app/settings/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/usage/page.js +2 -2
- package/.next/standalone/.next/server/app/usage/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app-paths-manifest.json +6 -6
- package/.next/standalone/.next/server/chunks/426.js +9 -4
- package/.next/standalone/.next/server/chunks/520.js +1 -1
- package/.next/standalone/.next/server/chunks/716.js +1 -1
- package/.next/standalone/.next/server/chunks/775.js +1 -1
- package/.next/standalone/.next/server/functions-config-manifest.json +4 -4
- package/.next/standalone/.next/server/pages/500.html +1 -1
- package/.next/standalone/.next/server/server-reference-manifest.json +1 -1
- package/.next/standalone/.next/static/chunks/148-557ee562aff993b1.js +1 -0
- package/.next/standalone/.next/static/chunks/app/{error-89ee9e078058915d.js → error-3e48784f89c5ae8d.js} +1 -1
- package/.next/standalone/.next/static/chunks/app/layout-6c973d790f015707.js +1 -0
- package/.next/standalone/.next/static/chunks/app/models/page-dff43b9050382020.js +1 -0
- package/.next/standalone/.next/static/chunks/app/page-6d87d7a8aa752100.js +1 -0
- package/.next/standalone/.next/static/chunks/app/projects/[id]/page-3f812f0e20137f2b.js +1 -0
- package/.next/standalone/.next/static/chunks/app/sessions/[id]/page-3f812f0e20137f2b.js +1 -0
- package/.next/standalone/.next/static/chunks/app/settings/{page-334168b522eac1b1.js → page-d1af886a5c22af9b.js} +1 -1
- package/.next/standalone/.next/static/chunks/app/usage/page-26297e0641d51da8.js +1 -0
- package/.next/standalone/.next/static/css/b07523b7c353538d.css +3 -0
- package/.next/standalone/node_modules/next/node_modules/postcss/package.json +0 -0
- package/.next/standalone/package.json +20 -4
- package/CHANGELOG.md +208 -0
- package/README.md +235 -2
- package/README.zh-CN.md +229 -2
- package/bin/cli.mjs +123 -3
- package/dist/mcp/server.mjs +23622 -0
- package/dist/report/index.mjs +2098 -0
- package/package.json +29 -15
- package/.next/standalone/.next/static/chunks/454-d0e7d0fa6f643c41.js +0 -1
- package/.next/standalone/.next/static/chunks/app/layout-a6e30ba3a7f39737.js +0 -1
- package/.next/standalone/.next/static/chunks/app/models/page-e0e1b5979547421a.js +0 -1
- package/.next/standalone/.next/static/chunks/app/page-9347dfa20dabb24b.js +0 -1
- package/.next/standalone/.next/static/chunks/app/projects/[id]/page-5804875e3dc384df.js +0 -1
- package/.next/standalone/.next/static/chunks/app/sessions/[id]/page-5804875e3dc384df.js +0 -1
- package/.next/standalone/.next/static/chunks/app/usage/page-7789fec27778df9a.js +0 -1
- package/.next/standalone/.next/static/css/2e5f36bcdf442844.css +0 -3
- package/.next/standalone/node_modules/semver/classes/comparator.js +0 -143
- package/.next/standalone/node_modules/semver/classes/range.js +0 -557
- package/.next/standalone/node_modules/semver/classes/semver.js +0 -333
- package/.next/standalone/node_modules/semver/functions/cmp.js +0 -54
- package/.next/standalone/node_modules/semver/functions/coerce.js +0 -62
- package/.next/standalone/node_modules/semver/functions/compare.js +0 -7
- package/.next/standalone/node_modules/semver/functions/eq.js +0 -5
- package/.next/standalone/node_modules/semver/functions/gt.js +0 -5
- package/.next/standalone/node_modules/semver/functions/gte.js +0 -5
- package/.next/standalone/node_modules/semver/functions/lt.js +0 -5
- package/.next/standalone/node_modules/semver/functions/lte.js +0 -5
- package/.next/standalone/node_modules/semver/functions/neq.js +0 -5
- package/.next/standalone/node_modules/semver/functions/parse.js +0 -18
- package/.next/standalone/node_modules/semver/functions/satisfies.js +0 -12
- package/.next/standalone/node_modules/semver/internal/constants.js +0 -37
- package/.next/standalone/node_modules/semver/internal/debug.js +0 -11
- package/.next/standalone/node_modules/semver/internal/identifiers.js +0 -29
- package/.next/standalone/node_modules/semver/internal/lrucache.js +0 -42
- package/.next/standalone/node_modules/semver/internal/parse-options.js +0 -17
- package/.next/standalone/node_modules/semver/internal/re.js +0 -223
- package/.next/standalone/node_modules/semver/package.json +0 -78
- /package/.next/standalone/.next/static/{kmNFwlVOydWtqBX3zI8OH → ZPycmg0NLiIflO5NXMT75}/_buildManifest.js +0 -0
- /package/.next/standalone/.next/static/{kmNFwlVOydWtqBX3zI8OH → ZPycmg0NLiIflO5NXMT75}/_ssgManifest.js +0 -0
|
@@ -0,0 +1,2098 @@
|
|
|
1
|
+
// lib/providers/claude/index.ts
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import os from "node:os";
|
|
4
|
+
|
|
5
|
+
// lib/data-loader/parse-jsonl.ts
|
|
6
|
+
import { createReadStream } from "node:fs";
|
|
7
|
+
import readline from "node:readline";
|
|
8
|
+
var TEXT_PREVIEW_MAX = 200;
|
|
9
|
+
async function parseJsonlFile(file) {
|
|
10
|
+
const stream = createReadStream(file, { encoding: "utf8" });
|
|
11
|
+
const rl = readline.createInterface({ input: stream, crlfDelay: Infinity });
|
|
12
|
+
const assistant = [];
|
|
13
|
+
const user = [];
|
|
14
|
+
const parentLinks = [];
|
|
15
|
+
for await (const line of rl) {
|
|
16
|
+
if (!line.trim()) continue;
|
|
17
|
+
let raw;
|
|
18
|
+
try {
|
|
19
|
+
raw = JSON.parse(line);
|
|
20
|
+
} catch {
|
|
21
|
+
continue;
|
|
22
|
+
}
|
|
23
|
+
if (!raw || typeof raw !== "object") continue;
|
|
24
|
+
if (raw.uuid) parentLinks.push([raw.uuid, raw.parentUuid ?? null]);
|
|
25
|
+
if (raw.type === "assistant") {
|
|
26
|
+
const a = parseAssistant(raw, file);
|
|
27
|
+
if (a) assistant.push(a);
|
|
28
|
+
} else if (raw.type === "user") {
|
|
29
|
+
const u = parseUser(raw, file);
|
|
30
|
+
if (u) user.push(u);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
return { assistant, user, parentLinks };
|
|
34
|
+
}
|
|
35
|
+
function parseAssistant(raw, file) {
|
|
36
|
+
const msg = raw.message;
|
|
37
|
+
if (!msg) return null;
|
|
38
|
+
const usage = msg.usage;
|
|
39
|
+
if (!usage) return null;
|
|
40
|
+
const model = msg.model ?? "";
|
|
41
|
+
if (!model || model === "<synthetic>") return null;
|
|
42
|
+
const messageId = msg.id ?? raw.uuid ?? "";
|
|
43
|
+
if (!messageId && !raw.requestId) return null;
|
|
44
|
+
const cacheCreation = usage.cache_creation;
|
|
45
|
+
const content = Array.isArray(msg.content) ? msg.content : [];
|
|
46
|
+
const toolNames = [];
|
|
47
|
+
let hasThinking = false;
|
|
48
|
+
let textPreview = "";
|
|
49
|
+
for (const c of content) {
|
|
50
|
+
if (c.type === "tool_use" && typeof c.name === "string") {
|
|
51
|
+
toolNames.push(c.name);
|
|
52
|
+
} else if (c.type === "thinking") {
|
|
53
|
+
hasThinking = true;
|
|
54
|
+
} else if (c.type === "text" && typeof c.text === "string" && !textPreview) {
|
|
55
|
+
textPreview = c.text.slice(0, TEXT_PREVIEW_MAX);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
return {
|
|
59
|
+
type: "assistant",
|
|
60
|
+
source: "claude",
|
|
61
|
+
uuid: raw.uuid ?? messageId,
|
|
62
|
+
parentUuid: raw.parentUuid ?? null,
|
|
63
|
+
timestamp: raw.timestamp ?? (/* @__PURE__ */ new Date()).toISOString(),
|
|
64
|
+
sessionId: raw.sessionId ?? "",
|
|
65
|
+
requestId: raw.requestId ?? "",
|
|
66
|
+
cwd: raw.cwd ?? "",
|
|
67
|
+
gitBranch: raw.gitBranch,
|
|
68
|
+
version: raw.version,
|
|
69
|
+
model,
|
|
70
|
+
messageId,
|
|
71
|
+
usage: {
|
|
72
|
+
input_tokens: Number(usage.input_tokens) || 0,
|
|
73
|
+
output_tokens: Number(usage.output_tokens) || 0,
|
|
74
|
+
cache_creation_input_tokens: Number(usage.cache_creation_input_tokens) || 0,
|
|
75
|
+
cache_read_input_tokens: Number(usage.cache_read_input_tokens) || 0,
|
|
76
|
+
cache_creation_5m: Number(cacheCreation?.ephemeral_5m_input_tokens) || 0,
|
|
77
|
+
cache_creation_1h: Number(cacheCreation?.ephemeral_1h_input_tokens) || 0
|
|
78
|
+
},
|
|
79
|
+
toolNames,
|
|
80
|
+
hasThinking,
|
|
81
|
+
textPreview,
|
|
82
|
+
filePath: file
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
function parseUser(raw, file) {
|
|
86
|
+
if (!raw.uuid) return null;
|
|
87
|
+
const msg = raw.message;
|
|
88
|
+
let textPreview = "";
|
|
89
|
+
if (msg) {
|
|
90
|
+
const content = msg.content;
|
|
91
|
+
if (typeof content === "string") {
|
|
92
|
+
textPreview = content.slice(0, TEXT_PREVIEW_MAX);
|
|
93
|
+
} else if (Array.isArray(content)) {
|
|
94
|
+
for (const c of content) {
|
|
95
|
+
if (c.type === "text" && typeof c.text === "string") {
|
|
96
|
+
textPreview = c.text.slice(0, TEXT_PREVIEW_MAX);
|
|
97
|
+
break;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
const isSynthetic = !!textPreview && isSyntheticUserText(textPreview);
|
|
103
|
+
return {
|
|
104
|
+
type: "user",
|
|
105
|
+
source: "claude",
|
|
106
|
+
uuid: raw.uuid,
|
|
107
|
+
parentUuid: raw.parentUuid ?? null,
|
|
108
|
+
timestamp: raw.timestamp ?? (/* @__PURE__ */ new Date()).toISOString(),
|
|
109
|
+
sessionId: raw.sessionId ?? "",
|
|
110
|
+
cwd: raw.cwd ?? "",
|
|
111
|
+
textPreview,
|
|
112
|
+
isSynthetic,
|
|
113
|
+
filePath: file
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
function isSyntheticUserText(text) {
|
|
117
|
+
const t = text.trimStart();
|
|
118
|
+
if (t.startsWith("Base directory for this skill:")) return true;
|
|
119
|
+
if (t.startsWith("<system-reminder>")) return true;
|
|
120
|
+
if (t.startsWith("Caveat: The messages below were generated by")) return true;
|
|
121
|
+
return false;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// lib/pricing/builtin.ts
|
|
125
|
+
var BUILTIN_PRICING = {
|
|
126
|
+
"claude-opus-4-7": {
|
|
127
|
+
input: 5,
|
|
128
|
+
output: 25,
|
|
129
|
+
cacheCreation5m: 6.25,
|
|
130
|
+
cacheCreation1h: 10,
|
|
131
|
+
cacheRead: 0.5
|
|
132
|
+
},
|
|
133
|
+
"claude-opus-4-6": {
|
|
134
|
+
input: 5,
|
|
135
|
+
output: 25,
|
|
136
|
+
cacheCreation5m: 6.25,
|
|
137
|
+
cacheCreation1h: 10,
|
|
138
|
+
cacheRead: 0.5
|
|
139
|
+
},
|
|
140
|
+
"claude-opus-4-5": {
|
|
141
|
+
input: 5,
|
|
142
|
+
output: 25,
|
|
143
|
+
cacheCreation5m: 6.25,
|
|
144
|
+
cacheCreation1h: 10,
|
|
145
|
+
cacheRead: 0.5
|
|
146
|
+
},
|
|
147
|
+
"claude-sonnet-4-6": {
|
|
148
|
+
input: 3,
|
|
149
|
+
output: 15,
|
|
150
|
+
cacheCreation5m: 3.75,
|
|
151
|
+
cacheCreation1h: 6,
|
|
152
|
+
cacheRead: 0.3
|
|
153
|
+
},
|
|
154
|
+
"claude-sonnet-4-5": {
|
|
155
|
+
input: 3,
|
|
156
|
+
output: 15,
|
|
157
|
+
cacheCreation5m: 3.75,
|
|
158
|
+
cacheCreation1h: 6,
|
|
159
|
+
cacheRead: 0.3
|
|
160
|
+
},
|
|
161
|
+
"claude-haiku-4-5": {
|
|
162
|
+
input: 1,
|
|
163
|
+
output: 5,
|
|
164
|
+
cacheCreation5m: 1.25,
|
|
165
|
+
cacheCreation1h: 2,
|
|
166
|
+
cacheRead: 0.1
|
|
167
|
+
},
|
|
168
|
+
"claude-haiku-3-5": {
|
|
169
|
+
input: 0.8,
|
|
170
|
+
output: 4,
|
|
171
|
+
cacheCreation5m: 1,
|
|
172
|
+
cacheCreation1h: 1.6,
|
|
173
|
+
cacheRead: 0.08
|
|
174
|
+
},
|
|
175
|
+
"claude-opus-4-1": {
|
|
176
|
+
input: 15,
|
|
177
|
+
output: 75,
|
|
178
|
+
cacheCreation5m: 18.75,
|
|
179
|
+
cacheCreation1h: 30,
|
|
180
|
+
cacheRead: 1.5
|
|
181
|
+
},
|
|
182
|
+
"claude-opus-4": {
|
|
183
|
+
input: 15,
|
|
184
|
+
output: 75,
|
|
185
|
+
cacheCreation5m: 18.75,
|
|
186
|
+
cacheCreation1h: 30,
|
|
187
|
+
cacheRead: 1.5
|
|
188
|
+
},
|
|
189
|
+
"claude-sonnet-4": {
|
|
190
|
+
input: 3,
|
|
191
|
+
output: 15,
|
|
192
|
+
cacheCreation5m: 3.75,
|
|
193
|
+
cacheCreation1h: 6,
|
|
194
|
+
cacheRead: 0.3
|
|
195
|
+
},
|
|
196
|
+
"claude-sonnet-3-7": {
|
|
197
|
+
input: 3,
|
|
198
|
+
output: 15,
|
|
199
|
+
cacheCreation5m: 3.75,
|
|
200
|
+
cacheCreation1h: 6,
|
|
201
|
+
cacheRead: 0.3
|
|
202
|
+
},
|
|
203
|
+
"claude-haiku-3": {
|
|
204
|
+
input: 0.25,
|
|
205
|
+
output: 1.25,
|
|
206
|
+
cacheCreation5m: 0.3,
|
|
207
|
+
cacheCreation1h: 0.5,
|
|
208
|
+
cacheRead: 0.03
|
|
209
|
+
}
|
|
210
|
+
};
|
|
211
|
+
var FALLBACK_BY_FAMILY = {
|
|
212
|
+
opus: BUILTIN_PRICING["claude-opus-4-7"],
|
|
213
|
+
sonnet: BUILTIN_PRICING["claude-sonnet-4-6"],
|
|
214
|
+
haiku: BUILTIN_PRICING["claude-haiku-4-5"]
|
|
215
|
+
};
|
|
216
|
+
|
|
217
|
+
// lib/pricing/cost-from-usage.ts
|
|
218
|
+
var PER_MTOK = 1e6;
|
|
219
|
+
function costFromUsage(usage, pricing) {
|
|
220
|
+
if (!pricing) {
|
|
221
|
+
return {
|
|
222
|
+
input: 0,
|
|
223
|
+
output: 0,
|
|
224
|
+
cacheCreation5m: 0,
|
|
225
|
+
cacheCreation1h: 0,
|
|
226
|
+
cacheRead: 0,
|
|
227
|
+
total: 0,
|
|
228
|
+
saved: 0
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
const input = usage.input_tokens / PER_MTOK * pricing.input;
|
|
232
|
+
const output = usage.output_tokens / PER_MTOK * pricing.output;
|
|
233
|
+
let cc5 = usage.cache_creation_5m / PER_MTOK * pricing.cacheCreation5m;
|
|
234
|
+
const cc1 = usage.cache_creation_1h / PER_MTOK * pricing.cacheCreation1h;
|
|
235
|
+
if (cc5 + cc1 === 0 && usage.cache_creation_input_tokens > 0) {
|
|
236
|
+
cc5 = usage.cache_creation_input_tokens / PER_MTOK * pricing.cacheCreation5m;
|
|
237
|
+
}
|
|
238
|
+
const cacheRead = usage.cache_read_input_tokens / PER_MTOK * pricing.cacheRead;
|
|
239
|
+
const total = input + output + cc5 + cc1 + cacheRead;
|
|
240
|
+
const saved = usage.cache_read_input_tokens / PER_MTOK * (pricing.input - pricing.cacheRead);
|
|
241
|
+
return {
|
|
242
|
+
input,
|
|
243
|
+
output,
|
|
244
|
+
cacheCreation5m: cc5,
|
|
245
|
+
cacheCreation1h: cc1,
|
|
246
|
+
cacheRead,
|
|
247
|
+
total,
|
|
248
|
+
saved
|
|
249
|
+
};
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// lib/providers/claude/shorten-model.ts
|
|
253
|
+
function capitalize(s) {
|
|
254
|
+
return s.replace(/\b\w/g, (c) => c.toUpperCase());
|
|
255
|
+
}
|
|
256
|
+
function shortenClaudeModel(model) {
|
|
257
|
+
if (!model) return "(unknown)";
|
|
258
|
+
let m = model.replace(/-(\d{8})$/, "").replace(/^(vertex_ai|bedrock|anthropic)\//, "");
|
|
259
|
+
m = m.replace(/^claude-/, "");
|
|
260
|
+
const parts = m.split("-");
|
|
261
|
+
if (parts.length >= 2) {
|
|
262
|
+
const family = parts[0];
|
|
263
|
+
const version = parts.slice(1).join(".");
|
|
264
|
+
return capitalize(family) + " " + version;
|
|
265
|
+
}
|
|
266
|
+
return capitalize(m.replace(/-/g, " "));
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// lib/providers/claude/index.ts
|
|
270
|
+
var dateSuffix = /-\d{8}$/;
|
|
271
|
+
var prefixRe = /^(vertex_ai|bedrock|anthropic)\//;
|
|
272
|
+
function resolvePricing(model) {
|
|
273
|
+
if (!model) return { pricing: null, matchType: "none", matchedKey: null };
|
|
274
|
+
if (BUILTIN_PRICING[model]) {
|
|
275
|
+
return { pricing: BUILTIN_PRICING[model], matchType: "exact", matchedKey: model };
|
|
276
|
+
}
|
|
277
|
+
const stripped = model.replace(dateSuffix, "");
|
|
278
|
+
if (BUILTIN_PRICING[stripped]) {
|
|
279
|
+
return {
|
|
280
|
+
pricing: BUILTIN_PRICING[stripped],
|
|
281
|
+
matchType: "date-stripped",
|
|
282
|
+
matchedKey: stripped
|
|
283
|
+
};
|
|
284
|
+
}
|
|
285
|
+
const noPrefix = stripped.replace(prefixRe, "");
|
|
286
|
+
if (BUILTIN_PRICING[noPrefix]) {
|
|
287
|
+
return {
|
|
288
|
+
pricing: BUILTIN_PRICING[noPrefix],
|
|
289
|
+
matchType: "prefix-stripped",
|
|
290
|
+
matchedKey: noPrefix
|
|
291
|
+
};
|
|
292
|
+
}
|
|
293
|
+
for (const family of ["opus", "sonnet", "haiku"]) {
|
|
294
|
+
if (model.toLowerCase().includes(family)) {
|
|
295
|
+
return {
|
|
296
|
+
pricing: FALLBACK_BY_FAMILY[family],
|
|
297
|
+
matchType: "family-fallback",
|
|
298
|
+
matchedKey: `claude-${family}-(latest)`
|
|
299
|
+
};
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
return { pricing: null, matchType: "none", matchedKey: null };
|
|
303
|
+
}
|
|
304
|
+
function getDirs() {
|
|
305
|
+
const home = os.homedir();
|
|
306
|
+
const candidates = [
|
|
307
|
+
path.join(home, ".claude", "projects"),
|
|
308
|
+
path.join(home, ".config", "claude", "projects")
|
|
309
|
+
];
|
|
310
|
+
if (process.env.CCGAUGE_CONFIG_DIR) {
|
|
311
|
+
candidates.push(path.join(process.env.CCGAUGE_CONFIG_DIR, "projects"));
|
|
312
|
+
}
|
|
313
|
+
if (process.env.CLAUDE_CONFIG_DIR) {
|
|
314
|
+
candidates.push(path.join(process.env.CLAUDE_CONFIG_DIR, "projects"));
|
|
315
|
+
}
|
|
316
|
+
return Array.from(new Set(candidates));
|
|
317
|
+
}
|
|
318
|
+
var claudeAdapter = {
|
|
319
|
+
id: "claude",
|
|
320
|
+
displayName: { en: "Claude", zh: "Claude" },
|
|
321
|
+
shortLabel: "C",
|
|
322
|
+
color: { fg: "#b45309", bg: "#fef3c7" },
|
|
323
|
+
// v2: blank textPreview on synthetic user messages (skill metadata,
|
|
324
|
+
// <system-reminder> blocks) so they don't split a single conversation
|
|
325
|
+
// into multiple "turns" in the usage table.
|
|
326
|
+
// v3: keep textPreview but add an `isSynthetic` flag — child rows in the
|
|
327
|
+
// usage table show skill metadata as their per-call "prompt"; only
|
|
328
|
+
// turn-boundary detection skips synthetic users.
|
|
329
|
+
parserVersion: "claude-v3-synthetic-flag",
|
|
330
|
+
capabilities: {
|
|
331
|
+
hasCacheCreation: true,
|
|
332
|
+
hasReasoningTokens: false,
|
|
333
|
+
blockWindowMs: 5 * 60 * 60 * 1e3
|
|
334
|
+
},
|
|
335
|
+
getDirs,
|
|
336
|
+
shouldSkipDir: (name) => name === "tool-results" || name === "memory",
|
|
337
|
+
parseFile: async (file) => {
|
|
338
|
+
const parsed = await parseJsonlFile(file);
|
|
339
|
+
return parsed;
|
|
340
|
+
},
|
|
341
|
+
resolvePricing,
|
|
342
|
+
shortenModel: shortenClaudeModel,
|
|
343
|
+
costFromUsage,
|
|
344
|
+
costFootnoteKey: null
|
|
345
|
+
};
|
|
346
|
+
|
|
347
|
+
// lib/providers/codex/index.ts
|
|
348
|
+
import path2 from "node:path";
|
|
349
|
+
import os2 from "node:os";
|
|
350
|
+
|
|
351
|
+
// lib/providers/codex/parse-codex-jsonl.ts
|
|
352
|
+
import { createReadStream as createReadStream2, promises as fs } from "node:fs";
|
|
353
|
+
import readline2 from "node:readline";
|
|
354
|
+
var TEXT_PREVIEW_MAX2 = 200;
|
|
355
|
+
async function fileMtimeIso(file) {
|
|
356
|
+
try {
|
|
357
|
+
const stat = await fs.stat(file);
|
|
358
|
+
return new Date(stat.mtimeMs).toISOString();
|
|
359
|
+
} catch {
|
|
360
|
+
return (/* @__PURE__ */ new Date()).toISOString();
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
function asString(v) {
|
|
364
|
+
return typeof v === "string" ? v : "";
|
|
365
|
+
}
|
|
366
|
+
function asNumber(v) {
|
|
367
|
+
const n = Number(v);
|
|
368
|
+
return Number.isFinite(n) ? n : 0;
|
|
369
|
+
}
|
|
370
|
+
function extractMessageText(payload) {
|
|
371
|
+
const msg = payload.message;
|
|
372
|
+
if (typeof msg === "string") return msg;
|
|
373
|
+
const content = payload.content;
|
|
374
|
+
if (typeof content === "string") return content;
|
|
375
|
+
if (Array.isArray(content)) {
|
|
376
|
+
for (const c of content) {
|
|
377
|
+
const t = c?.type;
|
|
378
|
+
if ((t === "input_text" || t === "output_text" || t === "text") && typeof c.text === "string") {
|
|
379
|
+
return c.text;
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
return "";
|
|
384
|
+
}
|
|
385
|
+
async function parseCodexJsonlFile(file) {
|
|
386
|
+
const stream = createReadStream2(file, { encoding: "utf8" });
|
|
387
|
+
const rl = readline2.createInterface({ input: stream, crlfDelay: Infinity });
|
|
388
|
+
const assistant = [];
|
|
389
|
+
const user = [];
|
|
390
|
+
const parentLinks = [];
|
|
391
|
+
let sessionId = "";
|
|
392
|
+
let cliVersion;
|
|
393
|
+
let defaultCwd = "";
|
|
394
|
+
let userIdx = 0;
|
|
395
|
+
let assistantIdx = 0;
|
|
396
|
+
let prevTotal = null;
|
|
397
|
+
const fileMtime = await fileMtimeIso(file);
|
|
398
|
+
let lastValidTs = fileMtime;
|
|
399
|
+
const turn = {
|
|
400
|
+
turnId: null,
|
|
401
|
+
cwd: "",
|
|
402
|
+
model: "gpt-unknown",
|
|
403
|
+
userUuid: null,
|
|
404
|
+
toolNames: [],
|
|
405
|
+
hasThinking: false,
|
|
406
|
+
pendingTextPreview: ""
|
|
407
|
+
};
|
|
408
|
+
for await (const line of rl) {
|
|
409
|
+
if (!line.trim()) continue;
|
|
410
|
+
let evt;
|
|
411
|
+
try {
|
|
412
|
+
evt = JSON.parse(line);
|
|
413
|
+
} catch {
|
|
414
|
+
continue;
|
|
415
|
+
}
|
|
416
|
+
if (!evt || typeof evt !== "object" || !evt.type) continue;
|
|
417
|
+
const payload = evt.payload ?? {};
|
|
418
|
+
const rawTs = asString(evt.timestamp);
|
|
419
|
+
const ts = rawTs || lastValidTs;
|
|
420
|
+
if (rawTs) lastValidTs = rawTs;
|
|
421
|
+
if (evt.type === "session_meta") {
|
|
422
|
+
sessionId = asString(payload.id);
|
|
423
|
+
defaultCwd = asString(payload.cwd);
|
|
424
|
+
cliVersion = asString(payload.cli_version) || void 0;
|
|
425
|
+
const metaTs = asString(payload.timestamp);
|
|
426
|
+
if (metaTs) lastValidTs = metaTs;
|
|
427
|
+
if (!turn.cwd) turn.cwd = defaultCwd;
|
|
428
|
+
continue;
|
|
429
|
+
}
|
|
430
|
+
if (evt.type === "turn_context") {
|
|
431
|
+
turn.turnId = asString(payload.turn_id) || turn.turnId;
|
|
432
|
+
turn.cwd = asString(payload.cwd) || defaultCwd;
|
|
433
|
+
const m = asString(payload.model);
|
|
434
|
+
if (m) turn.model = m;
|
|
435
|
+
const eff = asString(payload.effort);
|
|
436
|
+
if (eff) turn.effort = eff;
|
|
437
|
+
turn.toolNames = [];
|
|
438
|
+
turn.hasThinking = false;
|
|
439
|
+
turn.pendingTextPreview = "";
|
|
440
|
+
continue;
|
|
441
|
+
}
|
|
442
|
+
if (evt.type === "event_msg") {
|
|
443
|
+
const sub = asString(payload.type);
|
|
444
|
+
if (sub === "user_message") {
|
|
445
|
+
const text = extractMessageText(payload);
|
|
446
|
+
if (!text) continue;
|
|
447
|
+
const uuid = `${sessionId}::u${userIdx++}`;
|
|
448
|
+
user.push({
|
|
449
|
+
type: "user",
|
|
450
|
+
source: "codex",
|
|
451
|
+
uuid,
|
|
452
|
+
parentUuid: null,
|
|
453
|
+
timestamp: ts,
|
|
454
|
+
sessionId,
|
|
455
|
+
cwd: turn.cwd || defaultCwd,
|
|
456
|
+
textPreview: text.slice(0, TEXT_PREVIEW_MAX2),
|
|
457
|
+
filePath: file
|
|
458
|
+
});
|
|
459
|
+
parentLinks.push([uuid, null]);
|
|
460
|
+
turn.userUuid = uuid;
|
|
461
|
+
continue;
|
|
462
|
+
}
|
|
463
|
+
if (sub === "agent_message") {
|
|
464
|
+
const text = extractMessageText(payload);
|
|
465
|
+
if (text && !turn.pendingTextPreview) {
|
|
466
|
+
turn.pendingTextPreview = text.slice(0, TEXT_PREVIEW_MAX2);
|
|
467
|
+
}
|
|
468
|
+
continue;
|
|
469
|
+
}
|
|
470
|
+
if (sub === "agent_reasoning") {
|
|
471
|
+
turn.hasThinking = true;
|
|
472
|
+
continue;
|
|
473
|
+
}
|
|
474
|
+
if (sub === "token_count") {
|
|
475
|
+
const info = payload.info;
|
|
476
|
+
if (!info) continue;
|
|
477
|
+
const total = info.total_token_usage;
|
|
478
|
+
const last = info.last_token_usage;
|
|
479
|
+
const cur = total ? {
|
|
480
|
+
input: asNumber(total.input_tokens),
|
|
481
|
+
cached: asNumber(total.cached_input_tokens),
|
|
482
|
+
output: asNumber(total.output_tokens),
|
|
483
|
+
reasoning: asNumber(total.reasoning_output_tokens)
|
|
484
|
+
} : null;
|
|
485
|
+
let deltaInput;
|
|
486
|
+
let deltaCached;
|
|
487
|
+
let deltaOutput;
|
|
488
|
+
let deltaReasoning;
|
|
489
|
+
if (cur) {
|
|
490
|
+
if (prevTotal === null) {
|
|
491
|
+
deltaInput = cur.input;
|
|
492
|
+
deltaCached = cur.cached;
|
|
493
|
+
deltaOutput = cur.output;
|
|
494
|
+
deltaReasoning = cur.reasoning;
|
|
495
|
+
} else {
|
|
496
|
+
deltaInput = Math.max(0, cur.input - prevTotal.input);
|
|
497
|
+
deltaCached = Math.max(0, cur.cached - prevTotal.cached);
|
|
498
|
+
deltaOutput = Math.max(0, cur.output - prevTotal.output);
|
|
499
|
+
deltaReasoning = Math.max(0, cur.reasoning - prevTotal.reasoning);
|
|
500
|
+
}
|
|
501
|
+
if (deltaInput === 0 && deltaCached === 0 && deltaOutput === 0 && deltaReasoning === 0) {
|
|
502
|
+
continue;
|
|
503
|
+
}
|
|
504
|
+
if (prevTotal === null) {
|
|
505
|
+
prevTotal = { ...cur };
|
|
506
|
+
} else {
|
|
507
|
+
prevTotal = {
|
|
508
|
+
input: Math.max(prevTotal.input, cur.input),
|
|
509
|
+
cached: Math.max(prevTotal.cached, cur.cached),
|
|
510
|
+
output: Math.max(prevTotal.output, cur.output),
|
|
511
|
+
reasoning: Math.max(prevTotal.reasoning, cur.reasoning)
|
|
512
|
+
};
|
|
513
|
+
}
|
|
514
|
+
} else if (last) {
|
|
515
|
+
deltaInput = asNumber(last.input_tokens);
|
|
516
|
+
deltaCached = asNumber(last.cached_input_tokens);
|
|
517
|
+
deltaOutput = asNumber(last.output_tokens);
|
|
518
|
+
deltaReasoning = asNumber(last.reasoning_output_tokens);
|
|
519
|
+
if (deltaInput === 0 && deltaCached === 0 && deltaOutput === 0 && deltaReasoning === 0) {
|
|
520
|
+
continue;
|
|
521
|
+
}
|
|
522
|
+
} else {
|
|
523
|
+
continue;
|
|
524
|
+
}
|
|
525
|
+
const uuid = `${sessionId}::a${assistantIdx++}`;
|
|
526
|
+
const requestId = turn.turnId ? `${turn.turnId}::a${assistantIdx}` : `${sessionId}::a${assistantIdx}`;
|
|
527
|
+
assistant.push({
|
|
528
|
+
type: "assistant",
|
|
529
|
+
source: "codex",
|
|
530
|
+
uuid,
|
|
531
|
+
parentUuid: turn.userUuid,
|
|
532
|
+
timestamp: ts,
|
|
533
|
+
sessionId,
|
|
534
|
+
requestId,
|
|
535
|
+
cwd: turn.cwd || defaultCwd,
|
|
536
|
+
version: cliVersion,
|
|
537
|
+
model: turn.model || "gpt-unknown",
|
|
538
|
+
messageId: requestId,
|
|
539
|
+
usage: {
|
|
540
|
+
input_tokens: Math.max(0, deltaInput - deltaCached),
|
|
541
|
+
// output_tokens already includes reasoning (per OpenAI API
|
|
542
|
+
// billing convention). reasoning_tokens below is a display-only
|
|
543
|
+
// breakdown that MUST NOT be added again to total/cost.
|
|
544
|
+
output_tokens: deltaOutput + deltaReasoning,
|
|
545
|
+
cache_creation_input_tokens: 0,
|
|
546
|
+
cache_read_input_tokens: deltaCached,
|
|
547
|
+
cache_creation_5m: 0,
|
|
548
|
+
cache_creation_1h: 0,
|
|
549
|
+
reasoning_tokens: deltaReasoning
|
|
550
|
+
},
|
|
551
|
+
toolNames: [...turn.toolNames],
|
|
552
|
+
hasThinking: turn.hasThinking,
|
|
553
|
+
textPreview: turn.pendingTextPreview,
|
|
554
|
+
filePath: file,
|
|
555
|
+
effort: turn.effort
|
|
556
|
+
});
|
|
557
|
+
parentLinks.push([uuid, turn.userUuid]);
|
|
558
|
+
turn.toolNames = [];
|
|
559
|
+
turn.hasThinking = false;
|
|
560
|
+
turn.pendingTextPreview = "";
|
|
561
|
+
continue;
|
|
562
|
+
}
|
|
563
|
+
continue;
|
|
564
|
+
}
|
|
565
|
+
if (evt.type === "response_item") {
|
|
566
|
+
const sub = asString(payload.type);
|
|
567
|
+
if (sub === "function_call" || sub === "custom_tool_call") {
|
|
568
|
+
const name = asString(payload.name);
|
|
569
|
+
if (name) turn.toolNames.push(name);
|
|
570
|
+
continue;
|
|
571
|
+
}
|
|
572
|
+
if (sub === "reasoning") {
|
|
573
|
+
turn.hasThinking = true;
|
|
574
|
+
continue;
|
|
575
|
+
}
|
|
576
|
+
if (sub === "message") {
|
|
577
|
+
const role = asString(payload.role);
|
|
578
|
+
if (role === "assistant") {
|
|
579
|
+
const text = extractMessageText(payload);
|
|
580
|
+
if (text && !turn.pendingTextPreview) {
|
|
581
|
+
turn.pendingTextPreview = text.slice(0, TEXT_PREVIEW_MAX2);
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
continue;
|
|
585
|
+
}
|
|
586
|
+
continue;
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
return { assistant, user, parentLinks };
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
// lib/providers/codex/pricing.ts
|
|
593
|
+
var BUILTIN_PRICING_OPENAI = {
|
|
594
|
+
"gpt-5": {
|
|
595
|
+
input: 1.25,
|
|
596
|
+
output: 10,
|
|
597
|
+
cacheRead: 0.13,
|
|
598
|
+
cacheCreation5m: 0,
|
|
599
|
+
cacheCreation1h: 0
|
|
600
|
+
},
|
|
601
|
+
"gpt-5-mini": {
|
|
602
|
+
input: 0.25,
|
|
603
|
+
output: 2,
|
|
604
|
+
cacheRead: 0.025,
|
|
605
|
+
cacheCreation5m: 0,
|
|
606
|
+
cacheCreation1h: 0
|
|
607
|
+
},
|
|
608
|
+
"gpt-5-nano": {
|
|
609
|
+
input: 0.05,
|
|
610
|
+
output: 0.4,
|
|
611
|
+
cacheRead: 5e-3,
|
|
612
|
+
cacheCreation5m: 0,
|
|
613
|
+
cacheCreation1h: 0
|
|
614
|
+
},
|
|
615
|
+
"gpt-5.4": {
|
|
616
|
+
input: 1.25,
|
|
617
|
+
output: 10,
|
|
618
|
+
cacheRead: 0.13,
|
|
619
|
+
cacheCreation5m: 0,
|
|
620
|
+
cacheCreation1h: 0
|
|
621
|
+
},
|
|
622
|
+
"gpt-5.5": {
|
|
623
|
+
input: 1.25,
|
|
624
|
+
output: 10,
|
|
625
|
+
cacheRead: 0.13,
|
|
626
|
+
cacheCreation5m: 0,
|
|
627
|
+
cacheCreation1h: 0
|
|
628
|
+
},
|
|
629
|
+
"gpt-5.5-mini": {
|
|
630
|
+
input: 0.25,
|
|
631
|
+
output: 2,
|
|
632
|
+
cacheRead: 0.025,
|
|
633
|
+
cacheCreation5m: 0,
|
|
634
|
+
cacheCreation1h: 0
|
|
635
|
+
},
|
|
636
|
+
"gpt-5.5-nano": {
|
|
637
|
+
input: 0.05,
|
|
638
|
+
output: 0.4,
|
|
639
|
+
cacheRead: 5e-3,
|
|
640
|
+
cacheCreation5m: 0,
|
|
641
|
+
cacheCreation1h: 0
|
|
642
|
+
},
|
|
643
|
+
"gpt-4.1": {
|
|
644
|
+
input: 2,
|
|
645
|
+
output: 8,
|
|
646
|
+
cacheRead: 0.5,
|
|
647
|
+
cacheCreation5m: 0,
|
|
648
|
+
cacheCreation1h: 0
|
|
649
|
+
},
|
|
650
|
+
"gpt-4.1-mini": {
|
|
651
|
+
input: 0.4,
|
|
652
|
+
output: 1.6,
|
|
653
|
+
cacheRead: 0.1,
|
|
654
|
+
cacheCreation5m: 0,
|
|
655
|
+
cacheCreation1h: 0
|
|
656
|
+
},
|
|
657
|
+
"o3": {
|
|
658
|
+
input: 2,
|
|
659
|
+
output: 8,
|
|
660
|
+
cacheRead: 0.5,
|
|
661
|
+
cacheCreation5m: 0,
|
|
662
|
+
cacheCreation1h: 0
|
|
663
|
+
},
|
|
664
|
+
"o4-mini": {
|
|
665
|
+
input: 1.1,
|
|
666
|
+
output: 4.4,
|
|
667
|
+
cacheRead: 0.275,
|
|
668
|
+
cacheCreation5m: 0,
|
|
669
|
+
cacheCreation1h: 0
|
|
670
|
+
}
|
|
671
|
+
};
|
|
672
|
+
var FALLBACK_FAMILY_OPENAI = {
|
|
673
|
+
gpt: BUILTIN_PRICING_OPENAI["gpt-5"],
|
|
674
|
+
o: BUILTIN_PRICING_OPENAI["o3"]
|
|
675
|
+
};
|
|
676
|
+
var dateSuffix2 = /-\d{8}$/;
|
|
677
|
+
var prefixRe2 = /^(openai)\//;
|
|
678
|
+
function resolveCodexPricing(model) {
|
|
679
|
+
if (!model) return { pricing: null, matchType: "none", matchedKey: null };
|
|
680
|
+
if (BUILTIN_PRICING_OPENAI[model]) {
|
|
681
|
+
return {
|
|
682
|
+
pricing: BUILTIN_PRICING_OPENAI[model],
|
|
683
|
+
matchType: "exact",
|
|
684
|
+
matchedKey: model
|
|
685
|
+
};
|
|
686
|
+
}
|
|
687
|
+
const stripped = model.replace(dateSuffix2, "");
|
|
688
|
+
if (BUILTIN_PRICING_OPENAI[stripped]) {
|
|
689
|
+
return {
|
|
690
|
+
pricing: BUILTIN_PRICING_OPENAI[stripped],
|
|
691
|
+
matchType: "date-stripped",
|
|
692
|
+
matchedKey: stripped
|
|
693
|
+
};
|
|
694
|
+
}
|
|
695
|
+
const noPrefix = stripped.replace(prefixRe2, "");
|
|
696
|
+
if (BUILTIN_PRICING_OPENAI[noPrefix]) {
|
|
697
|
+
return {
|
|
698
|
+
pricing: BUILTIN_PRICING_OPENAI[noPrefix],
|
|
699
|
+
matchType: "prefix-stripped",
|
|
700
|
+
matchedKey: noPrefix
|
|
701
|
+
};
|
|
702
|
+
}
|
|
703
|
+
const lower = model.toLowerCase();
|
|
704
|
+
if (lower.startsWith("gpt-") || lower === "gpt") {
|
|
705
|
+
return {
|
|
706
|
+
pricing: FALLBACK_FAMILY_OPENAI.gpt,
|
|
707
|
+
matchType: "family-fallback",
|
|
708
|
+
matchedKey: "gpt-(latest)"
|
|
709
|
+
};
|
|
710
|
+
}
|
|
711
|
+
if (/^o\d/.test(lower)) {
|
|
712
|
+
return {
|
|
713
|
+
pricing: FALLBACK_FAMILY_OPENAI.o,
|
|
714
|
+
matchType: "family-fallback",
|
|
715
|
+
matchedKey: "o-(latest)"
|
|
716
|
+
};
|
|
717
|
+
}
|
|
718
|
+
return { pricing: null, matchType: "none", matchedKey: null };
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
// lib/providers/codex/shorten-model.ts
|
|
722
|
+
function capitalize2(s) {
|
|
723
|
+
return s.replace(/\b\w/g, (c) => c.toUpperCase());
|
|
724
|
+
}
|
|
725
|
+
var FAMILY_LABEL = {
|
|
726
|
+
mini: "Mini",
|
|
727
|
+
nano: "Nano",
|
|
728
|
+
pro: "Pro",
|
|
729
|
+
turbo: "Turbo",
|
|
730
|
+
preview: "Preview"
|
|
731
|
+
};
|
|
732
|
+
function shortenCodexModel(model) {
|
|
733
|
+
if (!model) return "(unknown)";
|
|
734
|
+
const m = model.replace(/^openai\//, "");
|
|
735
|
+
if (m.toLowerCase().startsWith("gpt-")) {
|
|
736
|
+
const rest = m.slice(4);
|
|
737
|
+
const parts = rest.split("-").map((p) => FAMILY_LABEL[p.toLowerCase()] ?? p);
|
|
738
|
+
return "GPT-" + parts.join(" ");
|
|
739
|
+
}
|
|
740
|
+
if (m.toLowerCase().startsWith("o")) {
|
|
741
|
+
return m.toUpperCase();
|
|
742
|
+
}
|
|
743
|
+
return capitalize2(m.replace(/-/g, " "));
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
// lib/providers/codex/index.ts
|
|
747
|
+
function getDirs2() {
|
|
748
|
+
const home = os2.homedir();
|
|
749
|
+
const candidates = [
|
|
750
|
+
path2.join(home, ".codex", "sessions"),
|
|
751
|
+
path2.join(home, ".codex", "archived_sessions")
|
|
752
|
+
];
|
|
753
|
+
if (process.env.CCGAUGE_CODEX_DIR) {
|
|
754
|
+
candidates.push(process.env.CCGAUGE_CODEX_DIR);
|
|
755
|
+
}
|
|
756
|
+
if (process.env.CODEX_HOME) {
|
|
757
|
+
candidates.push(path2.join(process.env.CODEX_HOME, "sessions"));
|
|
758
|
+
candidates.push(path2.join(process.env.CODEX_HOME, "archived_sessions"));
|
|
759
|
+
}
|
|
760
|
+
return Array.from(new Set(candidates));
|
|
761
|
+
}
|
|
762
|
+
var codexAdapter = {
|
|
763
|
+
id: "codex",
|
|
764
|
+
displayName: { en: "Codex", zh: "Codex" },
|
|
765
|
+
shortLabel: "X",
|
|
766
|
+
color: { fg: "#047857", bg: "#d1fae5" },
|
|
767
|
+
// v2: switched from last_token_usage to total_token_usage delta (fixed
|
|
768
|
+
// ~26% over-counting from duplicate/refresh token_count events).
|
|
769
|
+
// v3: split reasoning_tokens out as a display-only breakdown alongside
|
|
770
|
+
// output_tokens (which still includes reasoning for billing).
|
|
771
|
+
// v4: persist `effort` from turn_context onto each emitted record so the
|
|
772
|
+
// UI can tag the model column (e.g. `gpt-5.2-codex · high`).
|
|
773
|
+
parserVersion: "codex-v4-effort",
|
|
774
|
+
capabilities: {
|
|
775
|
+
hasCacheCreation: false,
|
|
776
|
+
hasReasoningTokens: true,
|
|
777
|
+
blockWindowMs: 5 * 60 * 60 * 1e3
|
|
778
|
+
},
|
|
779
|
+
getDirs: getDirs2,
|
|
780
|
+
shouldSkipDir: () => false,
|
|
781
|
+
parseFile: parseCodexJsonlFile,
|
|
782
|
+
resolvePricing: resolveCodexPricing,
|
|
783
|
+
shortenModel: shortenCodexModel,
|
|
784
|
+
costFromUsage,
|
|
785
|
+
costFootnoteKey: "cost.footnote.codex"
|
|
786
|
+
};
|
|
787
|
+
|
|
788
|
+
// lib/providers/index.ts
|
|
789
|
+
var PROVIDERS = {
|
|
790
|
+
claude: claudeAdapter,
|
|
791
|
+
codex: codexAdapter
|
|
792
|
+
};
|
|
793
|
+
var ALL_PROVIDER_IDS = ["claude", "codex"];
|
|
794
|
+
function getProvider(id) {
|
|
795
|
+
return PROVIDERS[id];
|
|
796
|
+
}
|
|
797
|
+
function listProviders() {
|
|
798
|
+
return ALL_PROVIDER_IDS.map((id) => PROVIDERS[id]);
|
|
799
|
+
}
|
|
800
|
+
function isProviderId(v) {
|
|
801
|
+
return typeof v === "string" && (v === "claude" || v === "codex");
|
|
802
|
+
}
|
|
803
|
+
|
|
804
|
+
// lib/data-loader/indexer.ts
|
|
805
|
+
import { promises as fs3, watch as fsWatch } from "node:fs";
|
|
806
|
+
import path4 from "node:path";
|
|
807
|
+
import os4 from "node:os";
|
|
808
|
+
|
|
809
|
+
// lib/dedup.ts
|
|
810
|
+
function dedupKey(r) {
|
|
811
|
+
const prefix = `${r.source}:`;
|
|
812
|
+
if (r.messageId && r.requestId) return `${prefix}${r.messageId}::${r.requestId}`;
|
|
813
|
+
if (r.messageId) return `${prefix}mid:${r.messageId}`;
|
|
814
|
+
if (r.requestId) return `${prefix}req:${r.requestId}`;
|
|
815
|
+
return `${prefix}uuid:${r.uuid}`;
|
|
816
|
+
}
|
|
817
|
+
function dedupAssistantRecords(records) {
|
|
818
|
+
const seen = /* @__PURE__ */ new Map();
|
|
819
|
+
for (const r of records) {
|
|
820
|
+
const k = dedupKey(r);
|
|
821
|
+
const existing = seen.get(k);
|
|
822
|
+
if (!existing) {
|
|
823
|
+
seen.set(k, r);
|
|
824
|
+
continue;
|
|
825
|
+
}
|
|
826
|
+
if (r.timestamp < existing.timestamp) {
|
|
827
|
+
seen.set(k, r);
|
|
828
|
+
}
|
|
829
|
+
}
|
|
830
|
+
return Array.from(seen.values());
|
|
831
|
+
}
|
|
832
|
+
|
|
833
|
+
// lib/data-loader/index-persist.ts
|
|
834
|
+
import { promises as fs2 } from "node:fs";
|
|
835
|
+
import path3 from "node:path";
|
|
836
|
+
import os3 from "node:os";
|
|
837
|
+
var SCHEMA_VERSION = 2;
|
|
838
|
+
var DEFAULT_INDEX_NAME = "default";
|
|
839
|
+
function getStateDir() {
|
|
840
|
+
if (process.env.CCGAUGE_STATE_DIR) return process.env.CCGAUGE_STATE_DIR;
|
|
841
|
+
return path3.join(os3.homedir(), ".ccgauge");
|
|
842
|
+
}
|
|
843
|
+
function getIndexPath(name) {
|
|
844
|
+
const fileName = name === DEFAULT_INDEX_NAME ? `index-v${SCHEMA_VERSION}.json` : `index-${name}-v${SCHEMA_VERSION}.json`;
|
|
845
|
+
return path3.join(getStateDir(), "cache", fileName);
|
|
846
|
+
}
|
|
847
|
+
async function loadPersistedIndex(name = DEFAULT_INDEX_NAME) {
|
|
848
|
+
const filePath = getIndexPath(name);
|
|
849
|
+
try {
|
|
850
|
+
const raw = await fs2.readFile(filePath, "utf8");
|
|
851
|
+
const parsed = JSON.parse(raw);
|
|
852
|
+
if (parsed.schemaVersion !== SCHEMA_VERSION) return null;
|
|
853
|
+
if (!Array.isArray(parsed.files)) return null;
|
|
854
|
+
return parsed;
|
|
855
|
+
} catch {
|
|
856
|
+
return null;
|
|
857
|
+
}
|
|
858
|
+
}
|
|
859
|
+
async function savePersistedIndex(payload, name = DEFAULT_INDEX_NAME) {
|
|
860
|
+
const filePath = getIndexPath(name);
|
|
861
|
+
const dir = path3.dirname(filePath);
|
|
862
|
+
await fs2.mkdir(dir, { recursive: true });
|
|
863
|
+
const data = {
|
|
864
|
+
schemaVersion: SCHEMA_VERSION,
|
|
865
|
+
savedAt: payload.savedAt,
|
|
866
|
+
files: payload.files
|
|
867
|
+
};
|
|
868
|
+
const tmp = `${filePath}.tmp-${process.pid}`;
|
|
869
|
+
await fs2.writeFile(tmp, JSON.stringify(data));
|
|
870
|
+
await fs2.rename(tmp, filePath);
|
|
871
|
+
}
|
|
872
|
+
|
|
873
|
+
// lib/data-loader/indexer.ts
|
|
874
|
+
var RECONCILE_DEBOUNCE_MS = 200;
|
|
875
|
+
var SNAPSHOT_REBUILD_DEBOUNCE_MS = 100;
|
|
876
|
+
var POLL_INTERVAL_MS = 3e4;
|
|
877
|
+
var PERSIST_DEBOUNCE_MS = 2e3;
|
|
878
|
+
var SCAN_DEPTH_LIMIT = 8;
|
|
879
|
+
var MAX_ERROR_HISTORY = 20;
|
|
880
|
+
var FileIndexer = class {
|
|
881
|
+
/** Cache namespace — different consumers (web vs MCP) use different
|
|
882
|
+
* names so they don't fight over the same on-disk persisted file. */
|
|
883
|
+
cacheName;
|
|
884
|
+
files = /* @__PURE__ */ new Map();
|
|
885
|
+
snapshot = null;
|
|
886
|
+
watchers = /* @__PURE__ */ new Map();
|
|
887
|
+
pollTimer = null;
|
|
888
|
+
snapshotRebuildTimer = null;
|
|
889
|
+
persistTimer = null;
|
|
890
|
+
fileDebouncers = /* @__PURE__ */ new Map();
|
|
891
|
+
initPromise = null;
|
|
892
|
+
isIndexing = false;
|
|
893
|
+
lastIndexedAt = null;
|
|
894
|
+
indexDurationMs = null;
|
|
895
|
+
existingDirs = [];
|
|
896
|
+
dirToProvider = /* @__PURE__ */ new Map();
|
|
897
|
+
errors = [];
|
|
898
|
+
loadedFromDisk = false;
|
|
899
|
+
/** When set, rebuildSnapshotNow uses this as duration start so stats.durationMs
|
|
900
|
+
* reflects the full operation (parse + dedup + sort), not just snapshot rebuild. */
|
|
901
|
+
lastWorkStart = null;
|
|
902
|
+
/** In-flight forceRescan promise. Concurrent callers coalesce onto this so we
|
|
903
|
+
* never have two full scans clobbering each other's `files` map. */
|
|
904
|
+
rescanPromise = null;
|
|
905
|
+
constructor(cacheName = DEFAULT_INDEX_NAME) {
|
|
906
|
+
this.cacheName = cacheName;
|
|
907
|
+
}
|
|
908
|
+
init() {
|
|
909
|
+
if (!this.initPromise) {
|
|
910
|
+
this.initPromise = this.doInit();
|
|
911
|
+
}
|
|
912
|
+
return this.initPromise;
|
|
913
|
+
}
|
|
914
|
+
async doInit() {
|
|
915
|
+
const start = Date.now();
|
|
916
|
+
this.isIndexing = true;
|
|
917
|
+
this.lastWorkStart = start;
|
|
918
|
+
try {
|
|
919
|
+
await this.detectProviderDirs();
|
|
920
|
+
const persisted = await loadPersistedIndex(this.cacheName);
|
|
921
|
+
this.loadedFromDisk = persisted !== null;
|
|
922
|
+
const persistedMap = /* @__PURE__ */ new Map();
|
|
923
|
+
if (persisted) {
|
|
924
|
+
for (const entry of persisted.files) persistedMap.set(entry.filePath, entry);
|
|
925
|
+
}
|
|
926
|
+
await this.fullScan(persistedMap);
|
|
927
|
+
this.rebuildSnapshotNow();
|
|
928
|
+
this.indexDurationMs = Date.now() - start;
|
|
929
|
+
this.lastIndexedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
930
|
+
this.syncWatchersToDirs();
|
|
931
|
+
this.setupPolling();
|
|
932
|
+
this.schedulePersist();
|
|
933
|
+
} finally {
|
|
934
|
+
this.isIndexing = false;
|
|
935
|
+
}
|
|
936
|
+
}
|
|
937
|
+
/** Re-detect provider data directories. Updates `dirToProvider` and `existingDirs`
|
|
938
|
+
* in place. Returns the diff so callers can act on added/removed dirs. */
|
|
939
|
+
async detectProviderDirs() {
|
|
940
|
+
const wanted = /* @__PURE__ */ new Map();
|
|
941
|
+
const dirs = [];
|
|
942
|
+
for (const provider of listProviders()) {
|
|
943
|
+
for (const dir of provider.getDirs()) {
|
|
944
|
+
if (await dirExists(dir)) {
|
|
945
|
+
if (!wanted.has(dir)) {
|
|
946
|
+
wanted.set(dir, provider);
|
|
947
|
+
dirs.push(dir);
|
|
948
|
+
}
|
|
949
|
+
}
|
|
950
|
+
}
|
|
951
|
+
}
|
|
952
|
+
const added = [];
|
|
953
|
+
const removed = [];
|
|
954
|
+
for (const dir of wanted.keys()) {
|
|
955
|
+
if (!this.dirToProvider.has(dir)) added.push(dir);
|
|
956
|
+
}
|
|
957
|
+
for (const dir of this.dirToProvider.keys()) {
|
|
958
|
+
if (!wanted.has(dir)) removed.push(dir);
|
|
959
|
+
}
|
|
960
|
+
this.dirToProvider = wanted;
|
|
961
|
+
this.existingDirs = dirs;
|
|
962
|
+
return { added, removed };
|
|
963
|
+
}
|
|
964
|
+
async fullScan(persistedMap) {
|
|
965
|
+
const fileTasks = [];
|
|
966
|
+
for (const [dir, provider] of this.dirToProvider) {
|
|
967
|
+
const files = await listJsonlFiles(dir, provider);
|
|
968
|
+
for (const f of files) fileTasks.push({ file: f, provider });
|
|
969
|
+
}
|
|
970
|
+
const seenPaths = new Set(fileTasks.map((t) => t.file));
|
|
971
|
+
await Promise.all(
|
|
972
|
+
fileTasks.map(async ({ file, provider }) => {
|
|
973
|
+
try {
|
|
974
|
+
const stat = await fs3.stat(file);
|
|
975
|
+
const persistedEntry = persistedMap.get(file);
|
|
976
|
+
if (persistedEntry && persistedEntry.source === provider.id && persistedEntry.parserVersion === provider.parserVersion && persistedEntry.mtimeMs === stat.mtimeMs && persistedEntry.size === stat.size) {
|
|
977
|
+
this.files.set(file, {
|
|
978
|
+
source: provider.id,
|
|
979
|
+
parserVersion: provider.parserVersion,
|
|
980
|
+
mtimeMs: stat.mtimeMs,
|
|
981
|
+
size: stat.size,
|
|
982
|
+
assistantRecords: persistedEntry.assistantRecords,
|
|
983
|
+
userRecords: persistedEntry.userRecords,
|
|
984
|
+
parentLinks: persistedEntry.parentLinks
|
|
985
|
+
});
|
|
986
|
+
return;
|
|
987
|
+
}
|
|
988
|
+
const parsed = await provider.parseFile(file);
|
|
989
|
+
this.files.set(file, {
|
|
990
|
+
source: provider.id,
|
|
991
|
+
parserVersion: provider.parserVersion,
|
|
992
|
+
mtimeMs: stat.mtimeMs,
|
|
993
|
+
size: stat.size,
|
|
994
|
+
assistantRecords: parsed.assistant,
|
|
995
|
+
userRecords: parsed.user,
|
|
996
|
+
parentLinks: parsed.parentLinks
|
|
997
|
+
});
|
|
998
|
+
} catch (err) {
|
|
999
|
+
this.recordError(`parse ${file}: ${err.message}`);
|
|
1000
|
+
}
|
|
1001
|
+
})
|
|
1002
|
+
);
|
|
1003
|
+
for (const tracked of Array.from(this.files.keys())) {
|
|
1004
|
+
if (!seenPaths.has(tracked)) this.files.delete(tracked);
|
|
1005
|
+
}
|
|
1006
|
+
}
|
|
1007
|
+
/** Reconcile watchers to currently-known dirs. Adds watchers for new dirs,
|
|
1008
|
+
* closes watchers for dirs that no longer exist. Idempotent. */
|
|
1009
|
+
syncWatchersToDirs() {
|
|
1010
|
+
for (const [dir, watcher] of this.watchers) {
|
|
1011
|
+
if (!this.dirToProvider.has(dir)) {
|
|
1012
|
+
try {
|
|
1013
|
+
watcher.close();
|
|
1014
|
+
} catch {
|
|
1015
|
+
}
|
|
1016
|
+
this.watchers.delete(dir);
|
|
1017
|
+
}
|
|
1018
|
+
}
|
|
1019
|
+
for (const [dir, provider] of this.dirToProvider) {
|
|
1020
|
+
if (this.watchers.has(dir)) continue;
|
|
1021
|
+
try {
|
|
1022
|
+
const watcher = fsWatch(dir, { recursive: true }, (_eventType, filename) => {
|
|
1023
|
+
if (!filename || typeof filename !== "string") return;
|
|
1024
|
+
if (!filename.endsWith(".jsonl")) return;
|
|
1025
|
+
const fullPath = path4.join(dir, filename);
|
|
1026
|
+
this.scheduleFileReconcile(fullPath, provider);
|
|
1027
|
+
});
|
|
1028
|
+
watcher.on("error", (err) => {
|
|
1029
|
+
this.recordError(`watcher ${dir}: ${err.message}`);
|
|
1030
|
+
});
|
|
1031
|
+
this.watchers.set(dir, watcher);
|
|
1032
|
+
} catch (err) {
|
|
1033
|
+
this.recordError(`watch ${dir}: ${err.message}`);
|
|
1034
|
+
}
|
|
1035
|
+
}
|
|
1036
|
+
}
|
|
1037
|
+
setupPolling() {
|
|
1038
|
+
if (this.pollTimer) clearInterval(this.pollTimer);
|
|
1039
|
+
this.pollTimer = setInterval(() => {
|
|
1040
|
+
this.pollOnce().catch((err) => this.recordError(`poll: ${err.message}`));
|
|
1041
|
+
}, POLL_INTERVAL_MS);
|
|
1042
|
+
this.pollTimer.unref?.();
|
|
1043
|
+
}
|
|
1044
|
+
async pollOnce() {
|
|
1045
|
+
const start = Date.now();
|
|
1046
|
+
const dirDiff = await this.detectProviderDirs();
|
|
1047
|
+
let changed = dirDiff.added.length > 0 || dirDiff.removed.length > 0;
|
|
1048
|
+
if (changed) this.syncWatchersToDirs();
|
|
1049
|
+
const fileTasks = [];
|
|
1050
|
+
for (const [dir, provider] of this.dirToProvider) {
|
|
1051
|
+
const files = await listJsonlFiles(dir, provider);
|
|
1052
|
+
for (const f of files) fileTasks.push({ file: f, provider });
|
|
1053
|
+
}
|
|
1054
|
+
const seenPaths = new Set(fileTasks.map((t) => t.file));
|
|
1055
|
+
await Promise.all(
|
|
1056
|
+
fileTasks.map(async ({ file, provider }) => {
|
|
1057
|
+
try {
|
|
1058
|
+
const stat = await fs3.stat(file);
|
|
1059
|
+
const existing = this.files.get(file);
|
|
1060
|
+
if (existing && existing.mtimeMs === stat.mtimeMs && existing.size === stat.size) {
|
|
1061
|
+
return;
|
|
1062
|
+
}
|
|
1063
|
+
const parsed = await provider.parseFile(file);
|
|
1064
|
+
this.files.set(file, {
|
|
1065
|
+
source: provider.id,
|
|
1066
|
+
parserVersion: provider.parserVersion,
|
|
1067
|
+
mtimeMs: stat.mtimeMs,
|
|
1068
|
+
size: stat.size,
|
|
1069
|
+
assistantRecords: parsed.assistant,
|
|
1070
|
+
userRecords: parsed.user,
|
|
1071
|
+
parentLinks: parsed.parentLinks
|
|
1072
|
+
});
|
|
1073
|
+
changed = true;
|
|
1074
|
+
} catch (err) {
|
|
1075
|
+
this.recordError(`poll-parse ${file}: ${err.message}`);
|
|
1076
|
+
}
|
|
1077
|
+
})
|
|
1078
|
+
);
|
|
1079
|
+
for (const tracked of Array.from(this.files.keys())) {
|
|
1080
|
+
if (!seenPaths.has(tracked)) {
|
|
1081
|
+
this.files.delete(tracked);
|
|
1082
|
+
changed = true;
|
|
1083
|
+
}
|
|
1084
|
+
}
|
|
1085
|
+
if (changed) {
|
|
1086
|
+
this.lastWorkStart = start;
|
|
1087
|
+
this.scheduleSnapshotRebuild();
|
|
1088
|
+
this.schedulePersist();
|
|
1089
|
+
}
|
|
1090
|
+
}
|
|
1091
|
+
scheduleFileReconcile(filePath, provider) {
|
|
1092
|
+
const existing = this.fileDebouncers.get(filePath);
|
|
1093
|
+
if (existing) clearTimeout(existing);
|
|
1094
|
+
const timer = setTimeout(() => {
|
|
1095
|
+
this.fileDebouncers.delete(filePath);
|
|
1096
|
+
this.reconcileFile(filePath, provider).catch(
|
|
1097
|
+
(err) => this.recordError(`reconcile ${filePath}: ${err.message}`)
|
|
1098
|
+
);
|
|
1099
|
+
}, RECONCILE_DEBOUNCE_MS);
|
|
1100
|
+
timer.unref?.();
|
|
1101
|
+
this.fileDebouncers.set(filePath, timer);
|
|
1102
|
+
}
|
|
1103
|
+
async reconcileFile(filePath, provider) {
|
|
1104
|
+
const workStart = Date.now();
|
|
1105
|
+
let stat = null;
|
|
1106
|
+
try {
|
|
1107
|
+
stat = await fs3.stat(filePath);
|
|
1108
|
+
} catch {
|
|
1109
|
+
}
|
|
1110
|
+
if (!stat || !stat.isFile()) {
|
|
1111
|
+
if (this.files.has(filePath)) {
|
|
1112
|
+
this.files.delete(filePath);
|
|
1113
|
+
this.lastWorkStart = workStart;
|
|
1114
|
+
this.scheduleSnapshotRebuild();
|
|
1115
|
+
this.schedulePersist();
|
|
1116
|
+
}
|
|
1117
|
+
return;
|
|
1118
|
+
}
|
|
1119
|
+
const existing = this.files.get(filePath);
|
|
1120
|
+
if (existing && existing.mtimeMs === stat.mtimeMs && existing.size === stat.size) {
|
|
1121
|
+
return;
|
|
1122
|
+
}
|
|
1123
|
+
try {
|
|
1124
|
+
const parsed = await provider.parseFile(filePath);
|
|
1125
|
+
this.files.set(filePath, {
|
|
1126
|
+
source: provider.id,
|
|
1127
|
+
parserVersion: provider.parserVersion,
|
|
1128
|
+
mtimeMs: stat.mtimeMs,
|
|
1129
|
+
size: stat.size,
|
|
1130
|
+
assistantRecords: parsed.assistant,
|
|
1131
|
+
userRecords: parsed.user,
|
|
1132
|
+
parentLinks: parsed.parentLinks
|
|
1133
|
+
});
|
|
1134
|
+
this.lastWorkStart = workStart;
|
|
1135
|
+
this.scheduleSnapshotRebuild();
|
|
1136
|
+
this.schedulePersist();
|
|
1137
|
+
} catch (err) {
|
|
1138
|
+
this.recordError(`parse ${filePath}: ${err.message}`);
|
|
1139
|
+
}
|
|
1140
|
+
}
|
|
1141
|
+
scheduleSnapshotRebuild() {
|
|
1142
|
+
if (this.snapshotRebuildTimer) clearTimeout(this.snapshotRebuildTimer);
|
|
1143
|
+
this.snapshotRebuildTimer = setTimeout(() => {
|
|
1144
|
+
this.snapshotRebuildTimer = null;
|
|
1145
|
+
this.rebuildSnapshotNow();
|
|
1146
|
+
}, SNAPSHOT_REBUILD_DEBOUNCE_MS);
|
|
1147
|
+
this.snapshotRebuildTimer.unref?.();
|
|
1148
|
+
}
|
|
1149
|
+
rebuildSnapshotNow() {
|
|
1150
|
+
const snapshotStart = Date.now();
|
|
1151
|
+
const workStart = this.lastWorkStart ?? snapshotStart;
|
|
1152
|
+
const assistant = [];
|
|
1153
|
+
const user = [];
|
|
1154
|
+
const parentMap = {};
|
|
1155
|
+
let recordsParsed = 0;
|
|
1156
|
+
const bySource = {
|
|
1157
|
+
claude: {
|
|
1158
|
+
source: "claude",
|
|
1159
|
+
filesScanned: 0,
|
|
1160
|
+
recordsParsed: 0,
|
|
1161
|
+
assistantRecords: 0,
|
|
1162
|
+
scannedDirs: []
|
|
1163
|
+
},
|
|
1164
|
+
codex: {
|
|
1165
|
+
source: "codex",
|
|
1166
|
+
filesScanned: 0,
|
|
1167
|
+
recordsParsed: 0,
|
|
1168
|
+
assistantRecords: 0,
|
|
1169
|
+
scannedDirs: []
|
|
1170
|
+
}
|
|
1171
|
+
};
|
|
1172
|
+
for (const [dir, provider] of this.dirToProvider) {
|
|
1173
|
+
bySource[provider.id].scannedDirs.push(dir);
|
|
1174
|
+
}
|
|
1175
|
+
for (const entry of this.files.values()) {
|
|
1176
|
+
assistant.push(...entry.assistantRecords);
|
|
1177
|
+
user.push(...entry.userRecords);
|
|
1178
|
+
for (const [uuid, parent] of entry.parentLinks) parentMap[uuid] = parent;
|
|
1179
|
+
recordsParsed += entry.assistantRecords.length + entry.userRecords.length;
|
|
1180
|
+
bySource[entry.source].filesScanned += 1;
|
|
1181
|
+
bySource[entry.source].recordsParsed += entry.assistantRecords.length + entry.userRecords.length;
|
|
1182
|
+
}
|
|
1183
|
+
const dedupedAssistants = dedupAssistantRecords(assistant).sort(
|
|
1184
|
+
(a, b) => a.timestamp.localeCompare(b.timestamp)
|
|
1185
|
+
);
|
|
1186
|
+
const dedupedUsers = dedupUserRecords(user).sort(
|
|
1187
|
+
(a, b) => a.timestamp.localeCompare(b.timestamp)
|
|
1188
|
+
);
|
|
1189
|
+
for (const rec of dedupedAssistants) bySource[rec.source].assistantRecords += 1;
|
|
1190
|
+
const stats = {
|
|
1191
|
+
filesScanned: this.files.size,
|
|
1192
|
+
recordsParsed,
|
|
1193
|
+
assistantRecords: dedupedAssistants.length,
|
|
1194
|
+
// Wall-clock from when this work started (parse/poll/init) to snapshot ready.
|
|
1195
|
+
// Falls back to snapshot rebuild duration if no parse work preceded.
|
|
1196
|
+
durationMs: Date.now() - workStart,
|
|
1197
|
+
scannedDirs: this.existingDirs
|
|
1198
|
+
};
|
|
1199
|
+
this.snapshot = {
|
|
1200
|
+
records: dedupedAssistants,
|
|
1201
|
+
userRecords: dedupedUsers,
|
|
1202
|
+
parentMap,
|
|
1203
|
+
stats,
|
|
1204
|
+
bySource: Object.values(bySource)
|
|
1205
|
+
};
|
|
1206
|
+
this.lastWorkStart = null;
|
|
1207
|
+
}
|
|
1208
|
+
getSnapshot() {
|
|
1209
|
+
if (!this.snapshot) {
|
|
1210
|
+
throw new Error("Indexer not initialized \u2014 call init() first.");
|
|
1211
|
+
}
|
|
1212
|
+
return this.snapshot;
|
|
1213
|
+
}
|
|
1214
|
+
async forceRescan() {
|
|
1215
|
+
if (!this.initPromise) {
|
|
1216
|
+
await this.init();
|
|
1217
|
+
return this.snapshot;
|
|
1218
|
+
}
|
|
1219
|
+
if (this.rescanPromise) return this.rescanPromise;
|
|
1220
|
+
this.rescanPromise = this.runRescan();
|
|
1221
|
+
try {
|
|
1222
|
+
return await this.rescanPromise;
|
|
1223
|
+
} finally {
|
|
1224
|
+
this.rescanPromise = null;
|
|
1225
|
+
}
|
|
1226
|
+
}
|
|
1227
|
+
async runRescan() {
|
|
1228
|
+
const start = Date.now();
|
|
1229
|
+
this.isIndexing = true;
|
|
1230
|
+
this.lastWorkStart = start;
|
|
1231
|
+
try {
|
|
1232
|
+
this.files.clear();
|
|
1233
|
+
await this.detectProviderDirs();
|
|
1234
|
+
await this.fullScan(/* @__PURE__ */ new Map());
|
|
1235
|
+
this.rebuildSnapshotNow();
|
|
1236
|
+
this.indexDurationMs = Date.now() - start;
|
|
1237
|
+
this.lastIndexedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
1238
|
+
this.syncWatchersToDirs();
|
|
1239
|
+
this.schedulePersist();
|
|
1240
|
+
return this.snapshot;
|
|
1241
|
+
} finally {
|
|
1242
|
+
this.isIndexing = false;
|
|
1243
|
+
}
|
|
1244
|
+
}
|
|
1245
|
+
getStatus() {
|
|
1246
|
+
const snap = this.snapshot;
|
|
1247
|
+
return {
|
|
1248
|
+
initialized: snap !== null,
|
|
1249
|
+
isIndexing: this.isIndexing,
|
|
1250
|
+
lastIndexedAt: this.lastIndexedAt,
|
|
1251
|
+
indexDurationMs: this.indexDurationMs,
|
|
1252
|
+
filesIndexed: this.files.size,
|
|
1253
|
+
recordsIndexed: snap?.stats.assistantRecords ?? 0,
|
|
1254
|
+
bySource: snap?.bySource ?? [],
|
|
1255
|
+
watchers: this.watchers.size,
|
|
1256
|
+
errors: this.errors.slice(-MAX_ERROR_HISTORY),
|
|
1257
|
+
pendingReconciles: this.fileDebouncers.size,
|
|
1258
|
+
loadedFromDisk: this.loadedFromDisk
|
|
1259
|
+
};
|
|
1260
|
+
}
|
|
1261
|
+
schedulePersist() {
|
|
1262
|
+
if (this.persistTimer) clearTimeout(this.persistTimer);
|
|
1263
|
+
this.persistTimer = setTimeout(() => {
|
|
1264
|
+
this.persistTimer = null;
|
|
1265
|
+
const entries = [];
|
|
1266
|
+
for (const [filePath, entry] of this.files) {
|
|
1267
|
+
entries.push({
|
|
1268
|
+
filePath,
|
|
1269
|
+
source: entry.source,
|
|
1270
|
+
parserVersion: entry.parserVersion,
|
|
1271
|
+
mtimeMs: entry.mtimeMs,
|
|
1272
|
+
size: entry.size,
|
|
1273
|
+
assistantRecords: entry.assistantRecords,
|
|
1274
|
+
userRecords: entry.userRecords,
|
|
1275
|
+
parentLinks: entry.parentLinks
|
|
1276
|
+
});
|
|
1277
|
+
}
|
|
1278
|
+
savePersistedIndex(
|
|
1279
|
+
{
|
|
1280
|
+
savedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1281
|
+
files: entries
|
|
1282
|
+
},
|
|
1283
|
+
this.cacheName
|
|
1284
|
+
).catch((err) => this.recordError(`persist: ${err.message}`));
|
|
1285
|
+
}, PERSIST_DEBOUNCE_MS);
|
|
1286
|
+
this.persistTimer.unref?.();
|
|
1287
|
+
}
|
|
1288
|
+
recordError(msg) {
|
|
1289
|
+
const stamped = `${(/* @__PURE__ */ new Date()).toISOString()} ${sanitizeForUser(msg)}`;
|
|
1290
|
+
this.errors.push(stamped);
|
|
1291
|
+
if (this.errors.length > MAX_ERROR_HISTORY * 2) {
|
|
1292
|
+
this.errors.splice(0, this.errors.length - MAX_ERROR_HISTORY);
|
|
1293
|
+
}
|
|
1294
|
+
if (process.env.CCGAUGE_DEBUG) {
|
|
1295
|
+
console.error(`[ccgauge:indexer] ${(/* @__PURE__ */ new Date()).toISOString()} ${msg}`);
|
|
1296
|
+
}
|
|
1297
|
+
}
|
|
1298
|
+
disposeWatchers() {
|
|
1299
|
+
for (const w of this.watchers.values()) {
|
|
1300
|
+
try {
|
|
1301
|
+
w.close();
|
|
1302
|
+
} catch {
|
|
1303
|
+
}
|
|
1304
|
+
}
|
|
1305
|
+
this.watchers.clear();
|
|
1306
|
+
}
|
|
1307
|
+
dispose() {
|
|
1308
|
+
this.disposeWatchers();
|
|
1309
|
+
if (this.pollTimer) clearInterval(this.pollTimer);
|
|
1310
|
+
if (this.snapshotRebuildTimer) clearTimeout(this.snapshotRebuildTimer);
|
|
1311
|
+
if (this.persistTimer) clearTimeout(this.persistTimer);
|
|
1312
|
+
for (const t of this.fileDebouncers.values()) clearTimeout(t);
|
|
1313
|
+
this.fileDebouncers.clear();
|
|
1314
|
+
}
|
|
1315
|
+
};
|
|
1316
|
+
function sanitizeForUser(s) {
|
|
1317
|
+
const home = os4.homedir();
|
|
1318
|
+
if (!home) return s;
|
|
1319
|
+
const escaped = home.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
1320
|
+
return s.replace(new RegExp(escaped, "g"), "~");
|
|
1321
|
+
}
|
|
1322
|
+
async function dirExists(p) {
|
|
1323
|
+
try {
|
|
1324
|
+
const s = await fs3.stat(p);
|
|
1325
|
+
return s.isDirectory();
|
|
1326
|
+
} catch {
|
|
1327
|
+
return false;
|
|
1328
|
+
}
|
|
1329
|
+
}
|
|
1330
|
+
async function listJsonlFiles(rootDir, provider) {
|
|
1331
|
+
const out = [];
|
|
1332
|
+
async function walk(dir, depth) {
|
|
1333
|
+
if (depth > SCAN_DEPTH_LIMIT) return;
|
|
1334
|
+
let entries;
|
|
1335
|
+
try {
|
|
1336
|
+
entries = await fs3.readdir(dir, { withFileTypes: true });
|
|
1337
|
+
} catch {
|
|
1338
|
+
return;
|
|
1339
|
+
}
|
|
1340
|
+
for (const e of entries) {
|
|
1341
|
+
const full = path4.join(dir, e.name);
|
|
1342
|
+
if (e.isDirectory()) {
|
|
1343
|
+
if (provider.shouldSkipDir(e.name)) continue;
|
|
1344
|
+
await walk(full, depth + 1);
|
|
1345
|
+
} else if (e.isFile() && e.name.endsWith(".jsonl")) {
|
|
1346
|
+
out.push(full);
|
|
1347
|
+
}
|
|
1348
|
+
}
|
|
1349
|
+
}
|
|
1350
|
+
await walk(rootDir, 0);
|
|
1351
|
+
return out;
|
|
1352
|
+
}
|
|
1353
|
+
function dedupUserRecords(records) {
|
|
1354
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1355
|
+
return records.filter((r) => {
|
|
1356
|
+
const k = r.uuid;
|
|
1357
|
+
if (!k) return true;
|
|
1358
|
+
if (seen.has(k)) return false;
|
|
1359
|
+
seen.add(k);
|
|
1360
|
+
return true;
|
|
1361
|
+
});
|
|
1362
|
+
}
|
|
1363
|
+
function indexerRegistry() {
|
|
1364
|
+
if (!globalThis.__ccgaugeIndexers) {
|
|
1365
|
+
globalThis.__ccgaugeIndexers = /* @__PURE__ */ new Map();
|
|
1366
|
+
}
|
|
1367
|
+
return globalThis.__ccgaugeIndexers;
|
|
1368
|
+
}
|
|
1369
|
+
function getIndexer(cacheName = DEFAULT_INDEX_NAME) {
|
|
1370
|
+
const reg = indexerRegistry();
|
|
1371
|
+
let inst = reg.get(cacheName);
|
|
1372
|
+
if (!inst) {
|
|
1373
|
+
inst = new FileIndexer(cacheName);
|
|
1374
|
+
reg.set(cacheName, inst);
|
|
1375
|
+
}
|
|
1376
|
+
return inst;
|
|
1377
|
+
}
|
|
1378
|
+
var indexer = getIndexer();
|
|
1379
|
+
|
|
1380
|
+
// lib/data-loader/scan.ts
|
|
1381
|
+
async function getCachedScan(opts = {}) {
|
|
1382
|
+
if (opts.force) {
|
|
1383
|
+
return indexer.forceRescan();
|
|
1384
|
+
}
|
|
1385
|
+
await indexer.init();
|
|
1386
|
+
return indexer.getSnapshot();
|
|
1387
|
+
}
|
|
1388
|
+
|
|
1389
|
+
// lib/pricing/calculate.ts
|
|
1390
|
+
function costOfRecord(rec) {
|
|
1391
|
+
const provider = getProvider(rec.source);
|
|
1392
|
+
const { pricing } = provider.resolvePricing(rec.model);
|
|
1393
|
+
return provider.costFromUsage(rec.usage, pricing);
|
|
1394
|
+
}
|
|
1395
|
+
|
|
1396
|
+
// lib/utils.ts
|
|
1397
|
+
function formatNumber(n, opts) {
|
|
1398
|
+
return new Intl.NumberFormat("en-US", {
|
|
1399
|
+
maximumFractionDigits: opts?.maxFrac ?? 0
|
|
1400
|
+
}).format(n);
|
|
1401
|
+
}
|
|
1402
|
+
function formatTokensCompact(n, locale = "en") {
|
|
1403
|
+
if (!Number.isFinite(n)) return "0";
|
|
1404
|
+
if (locale === "zh") {
|
|
1405
|
+
if (n >= 1e8) return (n / 1e8).toFixed(2) + "\u4EBF";
|
|
1406
|
+
if (n >= 1e4) return (n / 1e4).toFixed(1) + "\u4E07";
|
|
1407
|
+
return formatNumber(n);
|
|
1408
|
+
}
|
|
1409
|
+
if (n >= 1e9) return (n / 1e9).toFixed(2) + "B";
|
|
1410
|
+
if (n >= 1e6) return (n / 1e6).toFixed(2) + "M";
|
|
1411
|
+
if (n >= 1e3) return (n / 1e3).toFixed(1) + "K";
|
|
1412
|
+
return formatNumber(n);
|
|
1413
|
+
}
|
|
1414
|
+
function formatUSD(n, opts) {
|
|
1415
|
+
return new Intl.NumberFormat("en-US", {
|
|
1416
|
+
style: "currency",
|
|
1417
|
+
currency: "USD",
|
|
1418
|
+
minimumFractionDigits: opts?.minFrac ?? 2,
|
|
1419
|
+
maximumFractionDigits: opts?.maxFrac ?? 2
|
|
1420
|
+
}).format(n);
|
|
1421
|
+
}
|
|
1422
|
+
function formatPct(n, frac = 1) {
|
|
1423
|
+
if (!Number.isFinite(n)) return "0%";
|
|
1424
|
+
return `${(n * 100).toFixed(frac)}%`;
|
|
1425
|
+
}
|
|
1426
|
+
function projectNameFromCwd(cwd) {
|
|
1427
|
+
if (!cwd) return "(unknown)";
|
|
1428
|
+
const trimmed = cwd.replace(/[/\\]+$/, "");
|
|
1429
|
+
const parts = trimmed.split(/[/\\]+/);
|
|
1430
|
+
return parts[parts.length - 1] || cwd;
|
|
1431
|
+
}
|
|
1432
|
+
|
|
1433
|
+
// lib/aggregator/index.ts
|
|
1434
|
+
var GRANULARITIES = ["hour", "day", "week", "month"];
|
|
1435
|
+
function isGranularity(v) {
|
|
1436
|
+
return typeof v === "string" && GRANULARITIES.includes(v);
|
|
1437
|
+
}
|
|
1438
|
+
function bucketKey(ts, gran) {
|
|
1439
|
+
const d = new Date(ts);
|
|
1440
|
+
const yyyy = d.getFullYear();
|
|
1441
|
+
const mm = String(d.getMonth() + 1).padStart(2, "0");
|
|
1442
|
+
const dd = String(d.getDate()).padStart(2, "0");
|
|
1443
|
+
const hh = String(d.getHours()).padStart(2, "0");
|
|
1444
|
+
if (gran === "hour") {
|
|
1445
|
+
return { key: `${yyyy}-${mm}-${dd}T${hh}`, label: `${mm}/${dd} ${hh}:00` };
|
|
1446
|
+
}
|
|
1447
|
+
if (gran === "day") {
|
|
1448
|
+
return { key: `${yyyy}-${mm}-${dd}`, label: `${mm}/${dd}` };
|
|
1449
|
+
}
|
|
1450
|
+
if (gran === "week") {
|
|
1451
|
+
const monday = new Date(d);
|
|
1452
|
+
const day = monday.getDay() || 7;
|
|
1453
|
+
monday.setDate(monday.getDate() - day + 1);
|
|
1454
|
+
const wm = String(monday.getMonth() + 1).padStart(2, "0");
|
|
1455
|
+
const wd = String(monday.getDate()).padStart(2, "0");
|
|
1456
|
+
return {
|
|
1457
|
+
key: `${monday.getFullYear()}-W${wm}${wd}`,
|
|
1458
|
+
label: `Wk ${wm}/${wd}`
|
|
1459
|
+
};
|
|
1460
|
+
}
|
|
1461
|
+
return { key: `${yyyy}-${mm}`, label: `${yyyy}-${mm}` };
|
|
1462
|
+
}
|
|
1463
|
+
function withinRange(rec, opts) {
|
|
1464
|
+
if (rec.source !== opts.source) return false;
|
|
1465
|
+
if (opts.from && rec.timestamp < opts.from.toISOString()) return false;
|
|
1466
|
+
if (opts.to && rec.timestamp > opts.to.toISOString()) return false;
|
|
1467
|
+
if (opts.models && opts.models.length && !opts.models.includes(rec.model)) return false;
|
|
1468
|
+
if (opts.projects && opts.projects.length && !opts.projects.includes(rec.cwd)) return false;
|
|
1469
|
+
return true;
|
|
1470
|
+
}
|
|
1471
|
+
function aggregateByTime(records, gran, opts) {
|
|
1472
|
+
const buckets = /* @__PURE__ */ new Map();
|
|
1473
|
+
for (const rec of records) {
|
|
1474
|
+
if (!withinRange(rec, opts)) continue;
|
|
1475
|
+
const { key, label } = bucketKey(rec.timestamp, gran);
|
|
1476
|
+
let b = buckets.get(key);
|
|
1477
|
+
if (!b) {
|
|
1478
|
+
b = makeBucket(key, label);
|
|
1479
|
+
buckets.set(key, b);
|
|
1480
|
+
}
|
|
1481
|
+
pushRecord(b, rec);
|
|
1482
|
+
}
|
|
1483
|
+
return Array.from(buckets.values()).sort((a, b) => a.key.localeCompare(b.key));
|
|
1484
|
+
}
|
|
1485
|
+
function makeBucket(key, label) {
|
|
1486
|
+
return {
|
|
1487
|
+
key,
|
|
1488
|
+
label,
|
|
1489
|
+
inputTokens: 0,
|
|
1490
|
+
outputTokens: 0,
|
|
1491
|
+
cacheReadTokens: 0,
|
|
1492
|
+
cacheCreationTokens: 0,
|
|
1493
|
+
totalTokens: 0,
|
|
1494
|
+
cost: 0,
|
|
1495
|
+
saved: 0,
|
|
1496
|
+
requests: 0,
|
|
1497
|
+
models: {}
|
|
1498
|
+
};
|
|
1499
|
+
}
|
|
1500
|
+
function pushRecord(b, rec) {
|
|
1501
|
+
const cost = costOfRecord(rec);
|
|
1502
|
+
b.inputTokens += rec.usage.input_tokens;
|
|
1503
|
+
b.outputTokens += rec.usage.output_tokens;
|
|
1504
|
+
b.cacheReadTokens += rec.usage.cache_read_input_tokens;
|
|
1505
|
+
b.cacheCreationTokens += rec.usage.cache_creation_input_tokens;
|
|
1506
|
+
b.totalTokens = b.inputTokens + b.outputTokens + b.cacheReadTokens + b.cacheCreationTokens;
|
|
1507
|
+
b.cost += cost.total;
|
|
1508
|
+
b.saved += cost.saved;
|
|
1509
|
+
b.requests += 1;
|
|
1510
|
+
const m = b.models[rec.model] ?? { tokens: 0, cost: 0, requests: 0 };
|
|
1511
|
+
m.tokens += rec.usage.input_tokens + rec.usage.output_tokens + rec.usage.cache_read_input_tokens + rec.usage.cache_creation_input_tokens;
|
|
1512
|
+
m.cost += cost.total;
|
|
1513
|
+
m.requests += 1;
|
|
1514
|
+
b.models[rec.model] = m;
|
|
1515
|
+
}
|
|
1516
|
+
function aggregateByModel(records, opts) {
|
|
1517
|
+
const map = /* @__PURE__ */ new Map();
|
|
1518
|
+
for (const rec of records) {
|
|
1519
|
+
if (!withinRange(rec, opts)) continue;
|
|
1520
|
+
let s = map.get(rec.model);
|
|
1521
|
+
if (!s) {
|
|
1522
|
+
const { pricing, matchType } = getProvider(rec.source).resolvePricing(rec.model);
|
|
1523
|
+
s = {
|
|
1524
|
+
model: rec.model,
|
|
1525
|
+
source: rec.source,
|
|
1526
|
+
requests: 0,
|
|
1527
|
+
inputTokens: 0,
|
|
1528
|
+
outputTokens: 0,
|
|
1529
|
+
cacheReadTokens: 0,
|
|
1530
|
+
cacheCreationTokens: 0,
|
|
1531
|
+
totalTokens: 0,
|
|
1532
|
+
cost: 0,
|
|
1533
|
+
saved: 0,
|
|
1534
|
+
pricing,
|
|
1535
|
+
pricingResolved: matchType === "exact" || matchType === "date-stripped" || matchType === "prefix-stripped"
|
|
1536
|
+
};
|
|
1537
|
+
map.set(rec.model, s);
|
|
1538
|
+
}
|
|
1539
|
+
const cost = costOfRecord(rec);
|
|
1540
|
+
s.requests += 1;
|
|
1541
|
+
s.inputTokens += rec.usage.input_tokens;
|
|
1542
|
+
s.outputTokens += rec.usage.output_tokens;
|
|
1543
|
+
s.cacheReadTokens += rec.usage.cache_read_input_tokens;
|
|
1544
|
+
s.cacheCreationTokens += rec.usage.cache_creation_input_tokens;
|
|
1545
|
+
s.totalTokens = s.inputTokens + s.outputTokens + s.cacheReadTokens + s.cacheCreationTokens;
|
|
1546
|
+
s.cost += cost.total;
|
|
1547
|
+
s.saved += cost.saved;
|
|
1548
|
+
}
|
|
1549
|
+
return Array.from(map.values()).sort((a, b) => b.cost - a.cost);
|
|
1550
|
+
}
|
|
1551
|
+
function aggregateByProject(records, opts) {
|
|
1552
|
+
const map = /* @__PURE__ */ new Map();
|
|
1553
|
+
const sessionsByProject = /* @__PURE__ */ new Map();
|
|
1554
|
+
for (const rec of records) {
|
|
1555
|
+
if (!withinRange(rec, opts)) continue;
|
|
1556
|
+
const cwd = rec.cwd || "(unknown)";
|
|
1557
|
+
let s = map.get(cwd);
|
|
1558
|
+
if (!s) {
|
|
1559
|
+
s = {
|
|
1560
|
+
cwd,
|
|
1561
|
+
projectName: projectNameFromCwd(cwd),
|
|
1562
|
+
sessions: 0,
|
|
1563
|
+
requests: 0,
|
|
1564
|
+
inputTokens: 0,
|
|
1565
|
+
outputTokens: 0,
|
|
1566
|
+
cacheReadTokens: 0,
|
|
1567
|
+
cacheCreationTokens: 0,
|
|
1568
|
+
totalTokens: 0,
|
|
1569
|
+
cost: 0,
|
|
1570
|
+
saved: 0,
|
|
1571
|
+
firstActivity: rec.timestamp,
|
|
1572
|
+
lastActivity: rec.timestamp,
|
|
1573
|
+
models: []
|
|
1574
|
+
};
|
|
1575
|
+
map.set(cwd, s);
|
|
1576
|
+
sessionsByProject.set(cwd, /* @__PURE__ */ new Set());
|
|
1577
|
+
}
|
|
1578
|
+
sessionsByProject.get(cwd).add(rec.sessionId);
|
|
1579
|
+
const cost = costOfRecord(rec);
|
|
1580
|
+
s.requests += 1;
|
|
1581
|
+
s.inputTokens += rec.usage.input_tokens;
|
|
1582
|
+
s.outputTokens += rec.usage.output_tokens;
|
|
1583
|
+
s.cacheReadTokens += rec.usage.cache_read_input_tokens;
|
|
1584
|
+
s.cacheCreationTokens += rec.usage.cache_creation_input_tokens;
|
|
1585
|
+
s.totalTokens = s.inputTokens + s.outputTokens + s.cacheReadTokens + s.cacheCreationTokens;
|
|
1586
|
+
s.cost += cost.total;
|
|
1587
|
+
s.saved += cost.saved;
|
|
1588
|
+
if (rec.timestamp < s.firstActivity) s.firstActivity = rec.timestamp;
|
|
1589
|
+
if (rec.timestamp > s.lastActivity) s.lastActivity = rec.timestamp;
|
|
1590
|
+
if (!s.models.includes(rec.model)) s.models.push(rec.model);
|
|
1591
|
+
}
|
|
1592
|
+
for (const [cwd, set] of sessionsByProject) {
|
|
1593
|
+
const s = map.get(cwd);
|
|
1594
|
+
s.sessions = set.size;
|
|
1595
|
+
}
|
|
1596
|
+
return Array.from(map.values()).sort((a, b) => b.cost - a.cost);
|
|
1597
|
+
}
|
|
1598
|
+
function aggregateBySession(records, userRecords, opts) {
|
|
1599
|
+
const map = /* @__PURE__ */ new Map();
|
|
1600
|
+
for (const rec of records) {
|
|
1601
|
+
if (!withinRange(rec, opts)) continue;
|
|
1602
|
+
const sid = rec.sessionId || rec.uuid;
|
|
1603
|
+
let s = map.get(sid);
|
|
1604
|
+
if (!s) {
|
|
1605
|
+
s = {
|
|
1606
|
+
sessionId: sid,
|
|
1607
|
+
cwd: rec.cwd,
|
|
1608
|
+
projectName: projectNameFromCwd(rec.cwd),
|
|
1609
|
+
startTime: rec.timestamp,
|
|
1610
|
+
endTime: rec.timestamp,
|
|
1611
|
+
durationMs: 0,
|
|
1612
|
+
requests: 0,
|
|
1613
|
+
inputTokens: 0,
|
|
1614
|
+
outputTokens: 0,
|
|
1615
|
+
cacheReadTokens: 0,
|
|
1616
|
+
cacheCreationTokens: 0,
|
|
1617
|
+
totalTokens: 0,
|
|
1618
|
+
cost: 0,
|
|
1619
|
+
saved: 0,
|
|
1620
|
+
models: [],
|
|
1621
|
+
modelBreakdown: {}
|
|
1622
|
+
};
|
|
1623
|
+
map.set(sid, s);
|
|
1624
|
+
}
|
|
1625
|
+
const cost = costOfRecord(rec);
|
|
1626
|
+
s.requests += 1;
|
|
1627
|
+
s.inputTokens += rec.usage.input_tokens;
|
|
1628
|
+
s.outputTokens += rec.usage.output_tokens;
|
|
1629
|
+
s.cacheReadTokens += rec.usage.cache_read_input_tokens;
|
|
1630
|
+
s.cacheCreationTokens += rec.usage.cache_creation_input_tokens;
|
|
1631
|
+
s.totalTokens = s.inputTokens + s.outputTokens + s.cacheReadTokens + s.cacheCreationTokens;
|
|
1632
|
+
s.cost += cost.total;
|
|
1633
|
+
s.saved += cost.saved;
|
|
1634
|
+
if (rec.timestamp < s.startTime) s.startTime = rec.timestamp;
|
|
1635
|
+
if (rec.timestamp > s.endTime) s.endTime = rec.timestamp;
|
|
1636
|
+
if (!s.models.includes(rec.model)) s.models.push(rec.model);
|
|
1637
|
+
const mb = s.modelBreakdown[rec.model] ?? { tokens: 0, cost: 0, requests: 0 };
|
|
1638
|
+
mb.tokens += rec.usage.input_tokens + rec.usage.output_tokens + rec.usage.cache_read_input_tokens + rec.usage.cache_creation_input_tokens;
|
|
1639
|
+
mb.cost += cost.total;
|
|
1640
|
+
mb.requests += 1;
|
|
1641
|
+
s.modelBreakdown[rec.model] = mb;
|
|
1642
|
+
}
|
|
1643
|
+
const firstUserBySession = /* @__PURE__ */ new Map();
|
|
1644
|
+
for (const u of userRecords) {
|
|
1645
|
+
const existing = firstUserBySession.get(u.sessionId);
|
|
1646
|
+
if (!existing || u.timestamp < existing.timestamp) {
|
|
1647
|
+
firstUserBySession.set(u.sessionId, u);
|
|
1648
|
+
}
|
|
1649
|
+
}
|
|
1650
|
+
for (const s of map.values()) {
|
|
1651
|
+
s.durationMs = Math.max(0, new Date(s.endTime).getTime() - new Date(s.startTime).getTime());
|
|
1652
|
+
const u = firstUserBySession.get(s.sessionId);
|
|
1653
|
+
if (u && u.textPreview) {
|
|
1654
|
+
s.firstUserMessage = u.textPreview;
|
|
1655
|
+
s.title = u.textPreview.slice(0, 80);
|
|
1656
|
+
}
|
|
1657
|
+
}
|
|
1658
|
+
return Array.from(map.values()).sort((a, b) => b.endTime.localeCompare(a.endTime));
|
|
1659
|
+
}
|
|
1660
|
+
function aggregateTotals(records, opts) {
|
|
1661
|
+
let inputTokens = 0;
|
|
1662
|
+
let outputTokens = 0;
|
|
1663
|
+
let cacheReadTokens = 0;
|
|
1664
|
+
let cacheCreationTokens = 0;
|
|
1665
|
+
let cost = 0;
|
|
1666
|
+
let saved = 0;
|
|
1667
|
+
let requests = 0;
|
|
1668
|
+
for (const rec of records) {
|
|
1669
|
+
if (!withinRange(rec, opts)) continue;
|
|
1670
|
+
const c = costOfRecord(rec);
|
|
1671
|
+
inputTokens += rec.usage.input_tokens;
|
|
1672
|
+
outputTokens += rec.usage.output_tokens;
|
|
1673
|
+
cacheReadTokens += rec.usage.cache_read_input_tokens;
|
|
1674
|
+
cacheCreationTokens += rec.usage.cache_creation_input_tokens;
|
|
1675
|
+
cost += c.total;
|
|
1676
|
+
saved += c.saved;
|
|
1677
|
+
requests += 1;
|
|
1678
|
+
}
|
|
1679
|
+
return {
|
|
1680
|
+
inputTokens,
|
|
1681
|
+
outputTokens,
|
|
1682
|
+
cacheReadTokens,
|
|
1683
|
+
cacheCreationTokens,
|
|
1684
|
+
totalTokens: inputTokens + outputTokens + cacheReadTokens + cacheCreationTokens,
|
|
1685
|
+
cost,
|
|
1686
|
+
saved,
|
|
1687
|
+
requests
|
|
1688
|
+
};
|
|
1689
|
+
}
|
|
1690
|
+
|
|
1691
|
+
// lib/range.ts
|
|
1692
|
+
var USAGE_RANGES = ["1d", "7d", "30d", "90d", "all"];
|
|
1693
|
+
function isUsageRange(v) {
|
|
1694
|
+
return typeof v === "string" && USAGE_RANGES.includes(v);
|
|
1695
|
+
}
|
|
1696
|
+
function rangeToDates(range) {
|
|
1697
|
+
const now = /* @__PURE__ */ new Date();
|
|
1698
|
+
if (range === "all") return {};
|
|
1699
|
+
if (range === "1d") {
|
|
1700
|
+
const from2 = new Date(now.getFullYear(), now.getMonth(), now.getDate());
|
|
1701
|
+
return { from: from2 };
|
|
1702
|
+
}
|
|
1703
|
+
const m = range.match(/^(\d+)([dwm])$/);
|
|
1704
|
+
if (!m) return {};
|
|
1705
|
+
const n = parseInt(m[1], 10);
|
|
1706
|
+
const unit = m[2];
|
|
1707
|
+
const from = new Date(now);
|
|
1708
|
+
if (unit === "d") from.setDate(from.getDate() - n);
|
|
1709
|
+
else if (unit === "w") from.setDate(from.getDate() - n * 7);
|
|
1710
|
+
else if (unit === "m") from.setMonth(from.getMonth() - n);
|
|
1711
|
+
return { from };
|
|
1712
|
+
}
|
|
1713
|
+
|
|
1714
|
+
// lib/cli-report/index.ts
|
|
1715
|
+
var REPORT_RANGES = ["today", "1d", "7d", "30d", "90d", "all"];
|
|
1716
|
+
var DIMS = ["model", "project", "session"];
|
|
1717
|
+
var DATE_ONLY_RE = /^(\d{4})-(\d{2})-(\d{2})$/;
|
|
1718
|
+
var DEFAULT_REPORT = {
|
|
1719
|
+
range: "7d",
|
|
1720
|
+
source: "all",
|
|
1721
|
+
by: "model",
|
|
1722
|
+
gran: "day",
|
|
1723
|
+
limit: 10,
|
|
1724
|
+
json: false,
|
|
1725
|
+
color: true,
|
|
1726
|
+
showTrend: true,
|
|
1727
|
+
showBreakdown: true
|
|
1728
|
+
};
|
|
1729
|
+
async function runReport(opts) {
|
|
1730
|
+
const filled = normalizeReportOptions(opts);
|
|
1731
|
+
const scan = await getCachedScan();
|
|
1732
|
+
const sources = filled.source === "all" ? ALL_PROVIDER_IDS : [filled.source];
|
|
1733
|
+
const data = computeReportData(scan.records, sources, filled);
|
|
1734
|
+
if (filled.json) return JSON.stringify(data, null, 2);
|
|
1735
|
+
return renderText(data, filled);
|
|
1736
|
+
}
|
|
1737
|
+
function normalizeReportOptions(opts) {
|
|
1738
|
+
const filled = { ...DEFAULT_REPORT, ...opts };
|
|
1739
|
+
if (!isReportRange(filled.range)) {
|
|
1740
|
+
throw new Error(invalidOptionMessage("range", filled.range, REPORT_RANGES));
|
|
1741
|
+
}
|
|
1742
|
+
if (filled.source !== "all" && !isProviderId(filled.source)) {
|
|
1743
|
+
throw new Error(invalidOptionMessage("source", filled.source, ["claude", "codex", "all"]));
|
|
1744
|
+
}
|
|
1745
|
+
if (!isDim(filled.by)) {
|
|
1746
|
+
throw new Error(invalidOptionMessage("by", filled.by, DIMS));
|
|
1747
|
+
}
|
|
1748
|
+
if (!isGranularity(filled.gran)) {
|
|
1749
|
+
throw new Error(invalidOptionMessage("gran", filled.gran, ["hour", "day", "week", "month"]));
|
|
1750
|
+
}
|
|
1751
|
+
if (filled.since) parseReportDate(filled.since, "since");
|
|
1752
|
+
if (filled.until) parseReportDate(filled.until, "until");
|
|
1753
|
+
const dates = resolveRange(filled);
|
|
1754
|
+
if (dates.from && dates.until && dates.from.getTime() > dates.until.getTime()) {
|
|
1755
|
+
throw new Error("invalid date range: --since must be before or equal to --until");
|
|
1756
|
+
}
|
|
1757
|
+
return filled;
|
|
1758
|
+
}
|
|
1759
|
+
function isReportRange(v) {
|
|
1760
|
+
return typeof v === "string" && REPORT_RANGES.includes(v);
|
|
1761
|
+
}
|
|
1762
|
+
function isDim(v) {
|
|
1763
|
+
return typeof v === "string" && DIMS.includes(v);
|
|
1764
|
+
}
|
|
1765
|
+
function invalidOptionMessage(name, value, expected) {
|
|
1766
|
+
return `invalid ${name}: ${JSON.stringify(value)}. Expected one of: ${expected.join(", ")}`;
|
|
1767
|
+
}
|
|
1768
|
+
function computeReportData(allRecords, sources, o) {
|
|
1769
|
+
const dates = resolveRange(o);
|
|
1770
|
+
const baseOpts = {
|
|
1771
|
+
from: dates.from ?? void 0,
|
|
1772
|
+
to: dates.until ?? void 0,
|
|
1773
|
+
models: o.model ? void 0 : void 0,
|
|
1774
|
+
// handled post-filter
|
|
1775
|
+
projects: o.project ? void 0 : void 0
|
|
1776
|
+
};
|
|
1777
|
+
const totals = {
|
|
1778
|
+
input: 0,
|
|
1779
|
+
output: 0,
|
|
1780
|
+
reasoning: 0,
|
|
1781
|
+
cacheRead: 0,
|
|
1782
|
+
cacheWrite: 0,
|
|
1783
|
+
total: 0,
|
|
1784
|
+
cost: 0,
|
|
1785
|
+
saved: 0,
|
|
1786
|
+
requests: 0
|
|
1787
|
+
};
|
|
1788
|
+
const trendBuckets = /* @__PURE__ */ new Map();
|
|
1789
|
+
for (const source of sources) {
|
|
1790
|
+
const opts = { ...baseOpts, source };
|
|
1791
|
+
const sourceRecs = allRecords.filter((r) => withinSrcAndFilters(r, opts, o));
|
|
1792
|
+
const t = aggregateTotals(sourceRecs, opts);
|
|
1793
|
+
totals.input += t.inputTokens;
|
|
1794
|
+
totals.output += t.outputTokens;
|
|
1795
|
+
totals.cacheRead += t.cacheReadTokens;
|
|
1796
|
+
totals.cacheWrite += t.cacheCreationTokens;
|
|
1797
|
+
totals.total += t.totalTokens;
|
|
1798
|
+
totals.cost += t.cost;
|
|
1799
|
+
totals.saved += t.saved;
|
|
1800
|
+
totals.requests += t.requests;
|
|
1801
|
+
for (const r of sourceRecs) totals.reasoning += r.usage.reasoning_tokens ?? 0;
|
|
1802
|
+
const buckets = aggregateByTime(sourceRecs, o.gran, opts);
|
|
1803
|
+
for (const b of buckets) {
|
|
1804
|
+
const ex = trendBuckets.get(b.key);
|
|
1805
|
+
if (ex) {
|
|
1806
|
+
ex.cost += b.cost;
|
|
1807
|
+
ex.tokens += b.totalTokens;
|
|
1808
|
+
} else {
|
|
1809
|
+
trendBuckets.set(b.key, { label: b.label, cost: b.cost, tokens: b.totalTokens });
|
|
1810
|
+
}
|
|
1811
|
+
}
|
|
1812
|
+
}
|
|
1813
|
+
const trend = Array.from(trendBuckets.entries()).sort(([a], [b]) => a.localeCompare(b)).map(([, v]) => v);
|
|
1814
|
+
const breakdown = buildBreakdown(allRecords, sources, baseOpts, o);
|
|
1815
|
+
return {
|
|
1816
|
+
generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1817
|
+
range: o.range,
|
|
1818
|
+
source: o.source,
|
|
1819
|
+
by: o.by,
|
|
1820
|
+
gran: o.gran,
|
|
1821
|
+
fromIso: dates.from?.toISOString() ?? null,
|
|
1822
|
+
untilIso: dates.until?.toISOString() ?? null,
|
|
1823
|
+
totals,
|
|
1824
|
+
trend,
|
|
1825
|
+
breakdown
|
|
1826
|
+
};
|
|
1827
|
+
}
|
|
1828
|
+
function buildBreakdown(allRecords, sources, base, o) {
|
|
1829
|
+
if (o.by === "model") {
|
|
1830
|
+
const rows2 = [];
|
|
1831
|
+
for (const source of sources) {
|
|
1832
|
+
const opts = { ...base, source };
|
|
1833
|
+
const filtered = allRecords.filter((r) => withinSrcAndFilters(r, opts, o));
|
|
1834
|
+
const models = aggregateByModel(filtered, opts);
|
|
1835
|
+
const provider = getProvider(source);
|
|
1836
|
+
for (const m of models) {
|
|
1837
|
+
rows2.push({
|
|
1838
|
+
key: `${source}::${m.model}`,
|
|
1839
|
+
label: provider.shortenModel(m.model),
|
|
1840
|
+
requests: m.requests,
|
|
1841
|
+
tokens: m.totalTokens,
|
|
1842
|
+
cost: m.cost,
|
|
1843
|
+
share: 0,
|
|
1844
|
+
// filled after total
|
|
1845
|
+
sub: m.model
|
|
1846
|
+
});
|
|
1847
|
+
}
|
|
1848
|
+
}
|
|
1849
|
+
return finalizeShare(rows2, o.limit);
|
|
1850
|
+
}
|
|
1851
|
+
if (o.by === "project") {
|
|
1852
|
+
const rows2 = [];
|
|
1853
|
+
for (const source of sources) {
|
|
1854
|
+
const opts = { ...base, source };
|
|
1855
|
+
const filtered = allRecords.filter((r) => withinSrcAndFilters(r, opts, o));
|
|
1856
|
+
const projects = aggregateByProject(filtered, opts);
|
|
1857
|
+
for (const p of projects) {
|
|
1858
|
+
rows2.push({
|
|
1859
|
+
key: `${source}::${p.cwd}`,
|
|
1860
|
+
label: p.projectName,
|
|
1861
|
+
requests: p.requests,
|
|
1862
|
+
tokens: p.totalTokens,
|
|
1863
|
+
cost: p.cost,
|
|
1864
|
+
share: 0,
|
|
1865
|
+
sub: p.cwd
|
|
1866
|
+
});
|
|
1867
|
+
}
|
|
1868
|
+
}
|
|
1869
|
+
return finalizeShare(rows2, o.limit);
|
|
1870
|
+
}
|
|
1871
|
+
const rows = [];
|
|
1872
|
+
for (const source of sources) {
|
|
1873
|
+
const opts = { ...base, source };
|
|
1874
|
+
const filtered = allRecords.filter((r) => withinSrcAndFilters(r, opts, o));
|
|
1875
|
+
const sessions = aggregateBySession(filtered, [], opts);
|
|
1876
|
+
for (const s of sessions) {
|
|
1877
|
+
rows.push({
|
|
1878
|
+
key: `${source}::${s.sessionId}`,
|
|
1879
|
+
label: s.title ?? s.sessionId.slice(0, 8),
|
|
1880
|
+
requests: s.requests,
|
|
1881
|
+
tokens: s.totalTokens,
|
|
1882
|
+
cost: s.cost,
|
|
1883
|
+
share: 0,
|
|
1884
|
+
sub: s.projectName
|
|
1885
|
+
});
|
|
1886
|
+
}
|
|
1887
|
+
}
|
|
1888
|
+
return finalizeShare(rows, o.limit);
|
|
1889
|
+
}
|
|
1890
|
+
function finalizeShare(rows, limit) {
|
|
1891
|
+
const total = rows.reduce((s, r) => s + r.cost, 0);
|
|
1892
|
+
rows.sort((a, b) => b.cost - a.cost);
|
|
1893
|
+
const top = rows.slice(0, Math.max(1, limit));
|
|
1894
|
+
if (total > 0) for (const r of top) r.share = r.cost / total;
|
|
1895
|
+
return top;
|
|
1896
|
+
}
|
|
1897
|
+
function withinSrcAndFilters(rec, opts, o) {
|
|
1898
|
+
if (rec.source !== opts.source) return false;
|
|
1899
|
+
if (opts.from && rec.timestamp < opts.from.toISOString()) return false;
|
|
1900
|
+
if (opts.to && rec.timestamp > opts.to.toISOString()) return false;
|
|
1901
|
+
if (o.model && !rec.model.toLowerCase().includes(o.model.toLowerCase())) return false;
|
|
1902
|
+
if (o.project) {
|
|
1903
|
+
const needle = o.project.toLowerCase();
|
|
1904
|
+
const cwd = (rec.cwd || "").toLowerCase();
|
|
1905
|
+
const leaf = cwd.split(/[/\\]+/).pop() ?? "";
|
|
1906
|
+
if (!cwd.includes(needle) && !leaf.includes(needle)) return false;
|
|
1907
|
+
}
|
|
1908
|
+
return true;
|
|
1909
|
+
}
|
|
1910
|
+
function resolveRange(o) {
|
|
1911
|
+
if (o.since || o.until) {
|
|
1912
|
+
return {
|
|
1913
|
+
from: o.since ? parseReportDate(o.since, "since") : null,
|
|
1914
|
+
until: o.until ? parseReportDate(o.until, "until") : null
|
|
1915
|
+
};
|
|
1916
|
+
}
|
|
1917
|
+
const r = o.range === "today" ? "1d" : o.range;
|
|
1918
|
+
if (!isUsageRange(r)) {
|
|
1919
|
+
throw new Error(invalidOptionMessage("range", o.range, REPORT_RANGES));
|
|
1920
|
+
}
|
|
1921
|
+
const d = rangeToDates(r);
|
|
1922
|
+
return { from: d.from ?? null, until: d.to ?? null };
|
|
1923
|
+
}
|
|
1924
|
+
function parseReportDate(raw, boundary) {
|
|
1925
|
+
const m = raw.match(DATE_ONLY_RE);
|
|
1926
|
+
if (m) {
|
|
1927
|
+
const year = Number(m[1]);
|
|
1928
|
+
const month = Number(m[2]);
|
|
1929
|
+
const day = Number(m[3]);
|
|
1930
|
+
const date2 = boundary === "since" ? new Date(year, month - 1, day, 0, 0, 0, 0) : new Date(year, month - 1, day, 23, 59, 59, 999);
|
|
1931
|
+
if (date2.getFullYear() !== year || date2.getMonth() !== month - 1 || date2.getDate() !== day) {
|
|
1932
|
+
throw new Error(`invalid ${boundary} date: ${raw}`);
|
|
1933
|
+
}
|
|
1934
|
+
return date2;
|
|
1935
|
+
}
|
|
1936
|
+
const date = new Date(raw);
|
|
1937
|
+
if (Number.isNaN(date.getTime())) {
|
|
1938
|
+
throw new Error(`invalid ${boundary} date: ${raw}`);
|
|
1939
|
+
}
|
|
1940
|
+
return date;
|
|
1941
|
+
}
|
|
1942
|
+
function makeColors(enabled) {
|
|
1943
|
+
const wrap = (code) => (s) => enabled ? `\x1B[${code}m${s}\x1B[0m` : String(s);
|
|
1944
|
+
return {
|
|
1945
|
+
bold: wrap("1"),
|
|
1946
|
+
dim: wrap("2"),
|
|
1947
|
+
cyan: wrap("36"),
|
|
1948
|
+
green: wrap("32"),
|
|
1949
|
+
yellow: wrap("33"),
|
|
1950
|
+
red: wrap("31"),
|
|
1951
|
+
blue: wrap("34"),
|
|
1952
|
+
magenta: wrap("35"),
|
|
1953
|
+
brand: wrap("38;2;129;140;248")
|
|
1954
|
+
};
|
|
1955
|
+
}
|
|
1956
|
+
function renderText(d, o) {
|
|
1957
|
+
const c = makeColors(o.color !== false);
|
|
1958
|
+
const lines = [];
|
|
1959
|
+
const ts = new Date(d.generatedAt).toLocaleString();
|
|
1960
|
+
lines.push("");
|
|
1961
|
+
lines.push(`${c.brand(c.bold("ccgauge"))} ${c.bold("report")}`);
|
|
1962
|
+
lines.push(
|
|
1963
|
+
c.dim(
|
|
1964
|
+
[
|
|
1965
|
+
`range: ${d.range}`,
|
|
1966
|
+
`source: ${d.source}`,
|
|
1967
|
+
`by: ${d.by}`,
|
|
1968
|
+
`gran: ${d.gran}`,
|
|
1969
|
+
`generated ${ts}`
|
|
1970
|
+
].join(" \xB7 ")
|
|
1971
|
+
)
|
|
1972
|
+
);
|
|
1973
|
+
lines.push("");
|
|
1974
|
+
lines.push(c.brand("\u25B8") + " " + c.bold("Tokens"));
|
|
1975
|
+
const t = d.totals;
|
|
1976
|
+
const tokenRows = [
|
|
1977
|
+
["Input", formatTokensCompact(t.input), "Output", formatTokensCompact(t.output)],
|
|
1978
|
+
[
|
|
1979
|
+
"Cache R",
|
|
1980
|
+
c.green(formatTokensCompact(t.cacheRead)),
|
|
1981
|
+
"Cache W",
|
|
1982
|
+
formatTokensCompact(t.cacheWrite)
|
|
1983
|
+
]
|
|
1984
|
+
];
|
|
1985
|
+
if (t.reasoning > 0) {
|
|
1986
|
+
tokenRows.push([
|
|
1987
|
+
"Reasoning",
|
|
1988
|
+
c.dim(formatTokensCompact(t.reasoning)),
|
|
1989
|
+
"Requests",
|
|
1990
|
+
t.requests.toLocaleString()
|
|
1991
|
+
]);
|
|
1992
|
+
tokenRows.push(["Total", c.bold(formatTokensCompact(t.total)), "", ""]);
|
|
1993
|
+
} else {
|
|
1994
|
+
tokenRows.push([
|
|
1995
|
+
"Total",
|
|
1996
|
+
c.bold(formatTokensCompact(t.total)),
|
|
1997
|
+
"Requests",
|
|
1998
|
+
t.requests.toLocaleString()
|
|
1999
|
+
]);
|
|
2000
|
+
}
|
|
2001
|
+
lines.push(renderPairedKv(tokenRows, c));
|
|
2002
|
+
lines.push("");
|
|
2003
|
+
lines.push(c.brand("\u25B8") + " " + c.bold("Cost"));
|
|
2004
|
+
const totalInputForCache = t.input + t.cacheRead + t.cacheWrite;
|
|
2005
|
+
const cacheHit = totalInputForCache > 0 ? t.cacheRead / totalInputForCache : 0;
|
|
2006
|
+
const avgPerReq = t.requests > 0 ? t.cost / t.requests : 0;
|
|
2007
|
+
const costRows = [
|
|
2008
|
+
["Total", c.bold(formatUSD(t.cost)), "Saved by cache", c.green(formatUSD(t.saved))],
|
|
2009
|
+
[
|
|
2010
|
+
"Avg / request",
|
|
2011
|
+
avgPerReq < 0.01 ? `$${avgPerReq.toFixed(4)}` : formatUSD(avgPerReq),
|
|
2012
|
+
"Cache hit",
|
|
2013
|
+
c.green(formatPct(cacheHit, 1))
|
|
2014
|
+
]
|
|
2015
|
+
];
|
|
2016
|
+
lines.push(renderPairedKv(costRows, c));
|
|
2017
|
+
lines.push("");
|
|
2018
|
+
if (o.showTrend !== false && d.trend.length > 0) {
|
|
2019
|
+
lines.push(c.brand("\u25B8") + " " + c.bold("Trend") + " " + c.dim(`(${o.gran}, by cost)`));
|
|
2020
|
+
const maxCost = Math.max(...d.trend.map((b) => b.cost), 1e-9);
|
|
2021
|
+
const maxLabelLen = Math.max(...d.trend.map((b) => b.label.length));
|
|
2022
|
+
const maxCostStr = Math.max(...d.trend.map((b) => formatUSD(b.cost).length));
|
|
2023
|
+
for (const b of d.trend) {
|
|
2024
|
+
const bar = barString(b.cost / maxCost, 44);
|
|
2025
|
+
const label = b.label.padEnd(maxLabelLen);
|
|
2026
|
+
const cost = formatUSD(b.cost).padStart(maxCostStr);
|
|
2027
|
+
lines.push(` ${c.dim(label)} ${cost} ${c.brand(bar)}`);
|
|
2028
|
+
}
|
|
2029
|
+
lines.push("");
|
|
2030
|
+
}
|
|
2031
|
+
if (o.showBreakdown !== false && d.breakdown.length > 0) {
|
|
2032
|
+
const dimLabel = d.by[0].toUpperCase() + d.by.slice(1);
|
|
2033
|
+
lines.push(
|
|
2034
|
+
c.brand("\u25B8") + " " + c.bold(`Top ${d.breakdown.length} ${dimLabel}s`) + " " + c.dim("(by cost)")
|
|
2035
|
+
);
|
|
2036
|
+
const headers = ["#", dimLabel, "Reqs", "Tokens", "Cost", "Share"];
|
|
2037
|
+
const rows = d.breakdown.map((r, i) => [
|
|
2038
|
+
String(i + 1),
|
|
2039
|
+
truncate(r.label, 28),
|
|
2040
|
+
r.requests.toLocaleString(),
|
|
2041
|
+
formatTokensCompact(r.tokens),
|
|
2042
|
+
formatUSD(r.cost),
|
|
2043
|
+
formatPct(r.share, 1)
|
|
2044
|
+
]);
|
|
2045
|
+
lines.push(renderTable(headers, rows, c, [false, false, true, true, true, true]));
|
|
2046
|
+
lines.push("");
|
|
2047
|
+
}
|
|
2048
|
+
return lines.join("\n");
|
|
2049
|
+
}
|
|
2050
|
+
function renderPairedKv(rows, c) {
|
|
2051
|
+
const w = [0, 0, 0, 0];
|
|
2052
|
+
for (const r of rows) for (let i = 0; i < 4; i += 1) {
|
|
2053
|
+
const cell = r[i];
|
|
2054
|
+
if (visibleLen(cell) > w[i]) w[i] = visibleLen(cell);
|
|
2055
|
+
}
|
|
2056
|
+
const lines = [];
|
|
2057
|
+
for (const [lk, lv, rk, rv] of rows) {
|
|
2058
|
+
const left = `${c.dim(padEnd(lk, w[0]))} ${padEnd(lv, w[1])}`;
|
|
2059
|
+
const right = rk ? `${c.dim(padEnd(rk, w[2]))} ${rv}` : "";
|
|
2060
|
+
lines.push(` ${left} ${right}`.replace(/\s+$/, ""));
|
|
2061
|
+
}
|
|
2062
|
+
return lines.join("\n");
|
|
2063
|
+
}
|
|
2064
|
+
function renderTable(headers, rows, c, rightAlign) {
|
|
2065
|
+
const widths = headers.map(
|
|
2066
|
+
(h, i) => Math.max(h.length, ...rows.map((r) => visibleLen(r[i] ?? "")))
|
|
2067
|
+
);
|
|
2068
|
+
const headLine = " " + headers.map((h, i) => rightAlign[i] ? padStart(h, widths[i]) : padEnd(h, widths[i])).map((s) => c.dim(s)).join(" ");
|
|
2069
|
+
const sepLine = " " + widths.map((w) => "\u2500".repeat(w)).map((s) => c.dim(s)).join(" ");
|
|
2070
|
+
const bodyLines = rows.map(
|
|
2071
|
+
(row) => " " + row.map((cell, i) => rightAlign[i] ? padStart(cell, widths[i]) : padEnd(cell, widths[i])).join(" ")
|
|
2072
|
+
);
|
|
2073
|
+
return [headLine, sepLine, ...bodyLines].join("\n");
|
|
2074
|
+
}
|
|
2075
|
+
function barString(ratio, width) {
|
|
2076
|
+
const r = Math.max(0, Math.min(1, ratio));
|
|
2077
|
+
const filled = Math.round(r * width);
|
|
2078
|
+
return "\u2587".repeat(filled) + "\u2500".repeat(width - filled);
|
|
2079
|
+
}
|
|
2080
|
+
var ESC = String.fromCharCode(27);
|
|
2081
|
+
var ANSI_RE = new RegExp(ESC + "\\[[0-9;]*m", "g");
|
|
2082
|
+
function visibleLen(s) {
|
|
2083
|
+
return s.replace(ANSI_RE, "").length;
|
|
2084
|
+
}
|
|
2085
|
+
function padEnd(s, w) {
|
|
2086
|
+
return s + " ".repeat(Math.max(0, w - visibleLen(s)));
|
|
2087
|
+
}
|
|
2088
|
+
function padStart(s, w) {
|
|
2089
|
+
return " ".repeat(Math.max(0, w - visibleLen(s))) + s;
|
|
2090
|
+
}
|
|
2091
|
+
function truncate(s, max) {
|
|
2092
|
+
if (s.length <= max) return s;
|
|
2093
|
+
return s.slice(0, max - 1) + "\u2026";
|
|
2094
|
+
}
|
|
2095
|
+
export {
|
|
2096
|
+
DEFAULT_REPORT,
|
|
2097
|
+
runReport
|
|
2098
|
+
};
|