repospend 0.0.8 → 0.1.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/CHANGELOG.md +31 -0
- package/README.md +135 -122
- package/dist/cli.js +2268 -444
- package/docs/data-sources.md +121 -0
- package/docs/llms.txt +59 -0
- package/docs/pricing.md +44 -0
- package/docs/screenshots/agent-friction.png +0 -0
- package/docs/screenshots/dashboard-overview.png +0 -0
- package/docs/screenshots/models-view.png +0 -0
- package/docs/screenshots/repo-detail.png +0 -0
- package/docs/screenshots/repos-view.png +0 -0
- package/docs/screenshots/session-detail.png +0 -0
- package/docs/screenshots/sessions-view.png +0 -0
- package/docs/token-accounting.md +66 -0
- package/package.json +12 -2
- package/web-dist/assets/index-CdTTbCdO.css +1 -0
- package/web-dist/assets/index-DxlpLT8B.js +308 -0
- package/web-dist/index.html +2 -2
- package/web-dist/assets/index-CcjRLAsK.css +0 -1
- package/web-dist/assets/index-CruklDZf.js +0 -306
package/dist/cli.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// apps/server/src/cli.ts
|
|
4
|
-
import { spawn } from "node:child_process";
|
|
4
|
+
import { spawn as spawn2 } from "node:child_process";
|
|
5
5
|
|
|
6
6
|
// packages/core/src/aggregate.ts
|
|
7
7
|
function summarize(sessions) {
|
|
@@ -51,7 +51,12 @@ function groupByRepo(sessions) {
|
|
|
51
51
|
}));
|
|
52
52
|
}
|
|
53
53
|
function groupByModel(sessions) {
|
|
54
|
-
|
|
54
|
+
const groups = groupBy(sessions, (session) => session.model ?? "unknown-model", (session) => session.model ?? "Model not recorded");
|
|
55
|
+
const unknownModelSessions = sessions.filter((session) => !session.model);
|
|
56
|
+
if (unknownModelSessions.length > 0 && unknownModelSessions.every(isClaudeSyntheticZeroUsageSession)) {
|
|
57
|
+
return groups.map((group) => group.id === "unknown-model" ? { ...group, label: "Claude synthetic / no usage" } : group);
|
|
58
|
+
}
|
|
59
|
+
return groups;
|
|
55
60
|
}
|
|
56
61
|
function groupBySourceApp(sessions) {
|
|
57
62
|
return groupBy(sessions, (session) => session.sourceApp, (session) => session.sourceApp);
|
|
@@ -100,6 +105,9 @@ function groupBy(sessions, keyFn, labelFn) {
|
|
|
100
105
|
}
|
|
101
106
|
return [...groups.values()].sort((a, b) => (b.estimatedCostUsd ?? 0) - (a.estimatedCostUsd ?? 0) || b.totalTokens - a.totalTokens);
|
|
102
107
|
}
|
|
108
|
+
function isClaudeSyntheticZeroUsageSession(session) {
|
|
109
|
+
return session.warnings.includes("claude_synthetic_zero_usage");
|
|
110
|
+
}
|
|
103
111
|
function toCsv(sessions) {
|
|
104
112
|
const columns = [
|
|
105
113
|
"id",
|
|
@@ -195,7 +203,8 @@ function unique(values) {
|
|
|
195
203
|
return [...new Set(values)].sort();
|
|
196
204
|
}
|
|
197
205
|
|
|
198
|
-
// packages/core/src/
|
|
206
|
+
// packages/core/src/cache.ts
|
|
207
|
+
import crypto from "node:crypto";
|
|
199
208
|
import fs2 from "node:fs";
|
|
200
209
|
import path2 from "node:path";
|
|
201
210
|
|
|
@@ -203,12 +212,99 @@ import path2 from "node:path";
|
|
|
203
212
|
import fs from "node:fs";
|
|
204
213
|
import os from "node:os";
|
|
205
214
|
import path from "node:path";
|
|
215
|
+
|
|
216
|
+
// packages/types/src/index.ts
|
|
217
|
+
function normalizePricingModelId(model) {
|
|
218
|
+
return model.toLowerCase().replace(/^github_copilot\//, "").replace(/^github-copilot\//, "").replace(/^copilot\//, "").replace(/claude-(opus|sonnet|haiku)-(\d+)\.(\d+)(?=$|[-.])/, "claude-$1-$2-$3");
|
|
219
|
+
}
|
|
220
|
+
function claudePricingFamilyModel(model, isUsable = () => true) {
|
|
221
|
+
const families = [
|
|
222
|
+
"claude-opus-4-7",
|
|
223
|
+
"claude-opus-4-6",
|
|
224
|
+
"claude-opus-4-5",
|
|
225
|
+
"claude-opus-4-1",
|
|
226
|
+
"claude-opus-4",
|
|
227
|
+
"claude-sonnet-4-6",
|
|
228
|
+
"claude-sonnet-4-5",
|
|
229
|
+
"claude-sonnet-4",
|
|
230
|
+
"claude-haiku-4-5",
|
|
231
|
+
"claude-3-5-haiku"
|
|
232
|
+
];
|
|
233
|
+
const normalized = normalizePricingModelId(model);
|
|
234
|
+
if (normalized === "claude-opus-4-6-fast-mode") return void 0;
|
|
235
|
+
return families.find((candidate) => (normalized === candidate || normalized.startsWith(`${candidate}-`) || normalized.startsWith(`${candidate}.`)) && isUsable(candidate));
|
|
236
|
+
}
|
|
237
|
+
function copilotPricingFamilyModel(model, isUsable = () => true) {
|
|
238
|
+
const normalized = normalizePricingModelId(model);
|
|
239
|
+
const claudeFamily = claudePricingFamilyModel(normalized, isUsable);
|
|
240
|
+
if (claudeFamily) return claudeFamily;
|
|
241
|
+
if ((normalized === "gpt-4.1" || normalized.startsWith("gpt-4.1-") || normalized.startsWith("gpt-41")) && isUsable("gpt-4.1")) return "gpt-4.1";
|
|
242
|
+
if ((normalized === "gemini-2.5-pro" || normalized.startsWith("gemini-2.5-pro-")) && isUsable("gemini-2.5-pro")) return "gemini-2.5-pro";
|
|
243
|
+
if ((normalized === "gemini-3-flash" || normalized.startsWith("gemini-3-flash-")) && isUsable("gemini-3-flash")) return "gemini-3-flash";
|
|
244
|
+
if ((normalized === "gemini-3.1-pro" || normalized.startsWith("gemini-3.1-pro-")) && isUsable("gemini-3.1-pro")) return "gemini-3.1-pro";
|
|
245
|
+
if ((normalized === "gemini-3.5-flash" || normalized.startsWith("gemini-3.5-flash-")) && isUsable("gemini-3.5-flash")) return "gemini-3.5-flash";
|
|
246
|
+
if ((normalized === "raptor-mini" || normalized.startsWith("raptor-mini-") || normalized.startsWith("oswe-vscode")) && isUsable("raptor-mini")) return "raptor-mini";
|
|
247
|
+
if ((normalized === "lark" || normalized.startsWith("lark-")) && isUsable("lark")) return "lark";
|
|
248
|
+
if ((normalized === "goldeneye" || normalized.startsWith("goldeneye-")) && isUsable("goldeneye")) return "goldeneye";
|
|
249
|
+
return void 0;
|
|
250
|
+
}
|
|
251
|
+
function parseVersionedModel(normalized) {
|
|
252
|
+
if (normalized.includes("fast-mode")) return void 0;
|
|
253
|
+
const claude = normalized.match(/^(claude-(?:opus|sonnet|haiku))-(\d+)(?:-(\d+))?/);
|
|
254
|
+
if (claude) {
|
|
255
|
+
const minor = claude[3] ? Number(claude[3]) : 0;
|
|
256
|
+
return { tier: claude[1], version: Number(claude[2]) + minor / 100 };
|
|
257
|
+
}
|
|
258
|
+
const gpt = normalized.match(/^(gpt)-(\d)(?:\.(\d+))?(-[a-z][a-z0-9-]*)?$/);
|
|
259
|
+
if (gpt) {
|
|
260
|
+
const minor = gpt[3] ? Number(gpt[3]) : 0;
|
|
261
|
+
return { tier: `gpt${gpt[4] ?? ""}`, version: Number(gpt[2]) + minor / 100 };
|
|
262
|
+
}
|
|
263
|
+
return void 0;
|
|
264
|
+
}
|
|
265
|
+
function versionedFallbackModel(model, knownModels, isUsable = () => true) {
|
|
266
|
+
const target = parseVersionedModel(normalizePricingModelId(model));
|
|
267
|
+
if (!target) return void 0;
|
|
268
|
+
let best;
|
|
269
|
+
for (const candidate of knownModels) {
|
|
270
|
+
const parsed = parseVersionedModel(normalizePricingModelId(candidate));
|
|
271
|
+
if (!parsed || parsed.tier !== target.tier || parsed.version > target.version) continue;
|
|
272
|
+
if (!isUsable(candidate)) continue;
|
|
273
|
+
if (!best || parsed.version > best.version) best = { id: candidate, version: parsed.version };
|
|
274
|
+
}
|
|
275
|
+
return best?.id;
|
|
276
|
+
}
|
|
277
|
+
function isCopilotAliasModel(model) {
|
|
278
|
+
const normalized = normalizePricingModelId(model);
|
|
279
|
+
return normalized.startsWith("raptor-mini") || normalized.startsWith("oswe-vscode") || normalized === "lark" || normalized.startsWith("lark-") || normalized.startsWith("goldeneye");
|
|
280
|
+
}
|
|
281
|
+
function positiveRate(value) {
|
|
282
|
+
return typeof value === "number" && Number.isFinite(value) && value > 0;
|
|
283
|
+
}
|
|
284
|
+
function isUsableModelPricing(pricing) {
|
|
285
|
+
return positiveRate(pricing?.inputPerMillion) && positiveRate(pricing?.outputPerMillion);
|
|
286
|
+
}
|
|
287
|
+
function findModelPricing(model, pricing) {
|
|
288
|
+
const lower = normalizePricingModelId(model);
|
|
289
|
+
const exact = pricing[model] ?? pricing[lower];
|
|
290
|
+
if (isUsableModelPricing(exact)) return exact;
|
|
291
|
+
const versioned = versionedFallbackModel(lower, Object.keys(pricing), (candidate) => isUsableModelPricing(pricing[candidate]));
|
|
292
|
+
if (versioned) return pricing[versioned];
|
|
293
|
+
const claudeFamily = claudePricingFamilyModel(lower, (candidate) => isUsableModelPricing(pricing[candidate]));
|
|
294
|
+
if (claudeFamily) return pricing[claudeFamily];
|
|
295
|
+
const copilotFamily = copilotPricingFamilyModel(lower, (candidate) => isUsableModelPricing(pricing[candidate]));
|
|
296
|
+
if (copilotFamily) return pricing[copilotFamily];
|
|
297
|
+
return void 0;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
// packages/core/src/pricing.ts
|
|
206
301
|
var pricingInfo = {
|
|
207
|
-
sourceName: "Bundled
|
|
302
|
+
sourceName: "Bundled API-equivalent model pricing",
|
|
208
303
|
sourceUrl: "https://developers.openai.com/api/docs/pricing",
|
|
209
304
|
sourceUrls: [
|
|
210
305
|
{ label: "OpenAI pricing reference", url: "https://developers.openai.com/api/docs/pricing" },
|
|
211
|
-
{ label: "Claude pricing reference", url: "https://platform.claude.com/docs/en/about-claude/pricing" }
|
|
306
|
+
{ label: "Claude pricing reference", url: "https://platform.claude.com/docs/en/about-claude/pricing" },
|
|
307
|
+
{ label: "GitHub Copilot model pricing reference", url: "https://docs.github.com/en/copilot/reference/copilot-billing/models-and-pricing" }
|
|
212
308
|
],
|
|
213
309
|
unit: "USD per 1M tokens",
|
|
214
310
|
updatedAt: "2026-05-18",
|
|
@@ -237,8 +333,19 @@ var defaultPricing = {
|
|
|
237
333
|
"gpt-5-pro": { inputPerMillion: 15, outputPerMillion: 120, reasoningOutputPerMillion: 120 },
|
|
238
334
|
"gpt-5-mini": { inputPerMillion: 0.25, cachedInputPerMillion: 0.025, outputPerMillion: 2, reasoningOutputPerMillion: 2 },
|
|
239
335
|
"gpt-5-nano": { inputPerMillion: 0.05, cachedInputPerMillion: 5e-3, outputPerMillion: 0.4, reasoningOutputPerMillion: 0.4 },
|
|
336
|
+
"gpt-4.1": { inputPerMillion: 2, cachedInputPerMillion: 0.5, outputPerMillion: 8 },
|
|
337
|
+
"gemini-2.5-pro": { inputPerMillion: 1.25, cachedInputPerMillion: 0.125, outputPerMillion: 10, note: "GitHub Copilot supported Google-hosted model; API-equivalent estimate, not a Copilot bill." },
|
|
338
|
+
"gemini-3-flash": { inputPerMillion: 0.5, cachedInputPerMillion: 0.05, outputPerMillion: 3, note: "GitHub Copilot supported Google-hosted model; API-equivalent estimate, not a Copilot bill." },
|
|
339
|
+
"gemini-3.1-pro": { inputPerMillion: 2, cachedInputPerMillion: 0.2, outputPerMillion: 12, note: "GitHub Copilot supported Google-hosted model; API-equivalent estimate, not a Copilot bill." },
|
|
340
|
+
"gemini-3.5-flash": { inputPerMillion: 1.5, cachedInputPerMillion: 0.15, outputPerMillion: 9, note: "GitHub Copilot supported Google-hosted model; API-equivalent estimate, not a Copilot bill." },
|
|
341
|
+
"raptor-mini": { inputPerMillion: 0.25, cachedInputPerMillion: 0.025, outputPerMillion: 2, note: "GitHub Copilot fine-tuned model using GPT-5 mini pricing." },
|
|
342
|
+
"oswe-vscode-prime": { inputPerMillion: 0.25, cachedInputPerMillion: 0.025, outputPerMillion: 2, note: "GitHub Copilot Raptor mini internal model id." },
|
|
343
|
+
"lark": { inputPerMillion: 0.25, cachedInputPerMillion: 0.025, outputPerMillion: 2, note: "GitHub Copilot preview model; estimated with lightweight Copilot pricing." },
|
|
344
|
+
"goldeneye": { inputPerMillion: 1.25, cachedInputPerMillion: 0.125, outputPerMillion: 10, note: "GitHub Copilot fine-tuned model using GPT-5.1-Codex pricing." },
|
|
345
|
+
"claude-opus-4-8": { inputPerMillion: 5, cacheCreationInputPerMillion: 6.25, cachedInputPerMillion: 0.5, outputPerMillion: 25 },
|
|
240
346
|
"claude-opus-4-7": { inputPerMillion: 5, cacheCreationInputPerMillion: 6.25, cachedInputPerMillion: 0.5, outputPerMillion: 25 },
|
|
241
347
|
"claude-opus-4-6": { inputPerMillion: 5, cacheCreationInputPerMillion: 6.25, cachedInputPerMillion: 0.5, outputPerMillion: 25 },
|
|
348
|
+
"claude-opus-4-6-fast-mode": { inputPerMillion: 0, cacheCreationInputPerMillion: 0, cachedInputPerMillion: 0, outputPerMillion: 0, note: "GitHub Copilot supported model, but public per-token pricing is not listed separately. RepoSpend leaves API-equivalent pricing unset." },
|
|
242
349
|
"claude-opus-4-5": { inputPerMillion: 5, cacheCreationInputPerMillion: 6.25, cachedInputPerMillion: 0.5, outputPerMillion: 25 },
|
|
243
350
|
"claude-opus-4-1": { inputPerMillion: 15, cacheCreationInputPerMillion: 18.75, cachedInputPerMillion: 1.5, outputPerMillion: 75 },
|
|
244
351
|
"claude-opus-4": { inputPerMillion: 15, cacheCreationInputPerMillion: 18.75, cachedInputPerMillion: 1.5, outputPerMillion: 75 },
|
|
@@ -286,67 +393,266 @@ function calculateCostUsd(usage, pricing) {
|
|
|
286
393
|
const cost = billableInput / 1e6 * modelPricing.inputPerMillion + usage.cachedInputTokens / 1e6 * cachedRate + cacheCreationInput / 1e6 * cacheCreationRate + usage.outputTokens / 1e6 * modelPricing.outputPerMillion + usage.reasoningTokens / 1e6 * reasoningRate;
|
|
287
394
|
return Number(cost.toFixed(6));
|
|
288
395
|
}
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
396
|
+
|
|
397
|
+
// packages/core/src/cache.ts
|
|
398
|
+
var activeCacheVersion = "parse-v1";
|
|
399
|
+
var preparedCacheRoot;
|
|
400
|
+
function parseCacheRoot() {
|
|
401
|
+
return path2.join(repospendHome(), "cache", activeCacheVersion);
|
|
402
|
+
}
|
|
403
|
+
function readJsonCache(namespace, keyParts) {
|
|
404
|
+
const cachePath = cacheEntryPath(namespace, keyParts);
|
|
405
|
+
if (!cachePath || !fs2.existsSync(cachePath)) return void 0;
|
|
406
|
+
try {
|
|
407
|
+
return JSON.parse(fs2.readFileSync(cachePath, "utf8"));
|
|
408
|
+
} catch {
|
|
409
|
+
fs2.rmSync(cachePath, { force: true });
|
|
410
|
+
return void 0;
|
|
411
|
+
}
|
|
296
412
|
}
|
|
297
|
-
function
|
|
298
|
-
const
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
413
|
+
function writeJsonCache(namespace, keyParts, value) {
|
|
414
|
+
const cachePath = cacheEntryPath(namespace, keyParts);
|
|
415
|
+
if (!cachePath) return;
|
|
416
|
+
try {
|
|
417
|
+
fs2.mkdirSync(path2.dirname(cachePath), { recursive: true });
|
|
418
|
+
fs2.writeFileSync(cachePath, `${JSON.stringify(value)}
|
|
419
|
+
`);
|
|
420
|
+
} catch {
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
function clearRepoSpendCache() {
|
|
424
|
+
const target = path2.join(repospendHome(), "cache");
|
|
425
|
+
const removed = fs2.existsSync(target);
|
|
426
|
+
fs2.rmSync(target, { recursive: true, force: true });
|
|
427
|
+
preparedCacheRoot = void 0;
|
|
428
|
+
return { path: target, removed };
|
|
429
|
+
}
|
|
430
|
+
function cacheEntryPath(namespace, keyParts) {
|
|
431
|
+
prepareCacheRoot();
|
|
432
|
+
const safeNamespace = namespace.replace(/[^a-z0-9_-]/gi, "-");
|
|
433
|
+
const key = crypto.createHash("sha256").update(JSON.stringify(keyParts)).digest("hex");
|
|
434
|
+
return path2.join(parseCacheRoot(), safeNamespace, `${key}.json`);
|
|
312
435
|
}
|
|
313
|
-
function
|
|
314
|
-
|
|
436
|
+
function prepareCacheRoot() {
|
|
437
|
+
const root = path2.join(repospendHome(), "cache");
|
|
438
|
+
if (preparedCacheRoot === root) return;
|
|
439
|
+
preparedCacheRoot = root;
|
|
440
|
+
try {
|
|
441
|
+
fs2.mkdirSync(parseCacheRoot(), { recursive: true });
|
|
442
|
+
for (const entry of fs2.readdirSync(root, { withFileTypes: true })) {
|
|
443
|
+
if (entry.isDirectory() && entry.name !== activeCacheVersion) {
|
|
444
|
+
fs2.rmSync(path2.join(root, entry.name), { recursive: true, force: true });
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
} catch {
|
|
448
|
+
}
|
|
315
449
|
}
|
|
316
450
|
|
|
317
451
|
// packages/core/src/config.ts
|
|
452
|
+
import fs3 from "node:fs";
|
|
453
|
+
import path3 from "node:path";
|
|
318
454
|
function configPath() {
|
|
319
|
-
return
|
|
455
|
+
return path3.join(repospendHome(), "config.json");
|
|
320
456
|
}
|
|
321
457
|
function loadConfig() {
|
|
322
458
|
return loadConfigWithWarnings().config;
|
|
323
459
|
}
|
|
324
460
|
function loadConfigWithWarnings() {
|
|
325
461
|
const localConfigPath = configPath();
|
|
326
|
-
if (!
|
|
462
|
+
if (!fs3.existsSync(localConfigPath)) {
|
|
327
463
|
return { config: {}, warnings: [] };
|
|
328
464
|
}
|
|
329
465
|
try {
|
|
330
|
-
return { config: JSON.parse(
|
|
466
|
+
return { config: JSON.parse(fs3.readFileSync(localConfigPath, "utf8")), warnings: [] };
|
|
331
467
|
} catch (error) {
|
|
332
468
|
const message = error instanceof Error ? error.message : String(error);
|
|
333
469
|
return { config: {}, warnings: [`Unable to parse ${localConfigPath}: ${message}`] };
|
|
334
470
|
}
|
|
335
471
|
}
|
|
472
|
+
function saveConfig(config) {
|
|
473
|
+
const localConfigPath = configPath();
|
|
474
|
+
fs3.mkdirSync(path3.dirname(localConfigPath), { recursive: true });
|
|
475
|
+
fs3.writeFileSync(localConfigPath, `${JSON.stringify(config, null, 2)}
|
|
476
|
+
`);
|
|
477
|
+
return loadConfig();
|
|
478
|
+
}
|
|
336
479
|
function normalizePathForMatch(inputPath) {
|
|
337
|
-
return
|
|
480
|
+
return path3.resolve(inputPath).replace(/\/+$/, "");
|
|
338
481
|
}
|
|
339
482
|
function aliasForPath(repoRoot, config) {
|
|
340
483
|
const resolvedRoot = normalizePathForMatch(repoRoot);
|
|
341
484
|
const match = config.repos?.find(
|
|
342
485
|
(repo) => repo.paths.some((repoPath) => {
|
|
343
486
|
const candidate = normalizePathForMatch(repoPath);
|
|
344
|
-
return resolvedRoot === candidate || resolvedRoot.startsWith(`${candidate}${
|
|
487
|
+
return resolvedRoot === candidate || resolvedRoot.startsWith(`${candidate}${path3.sep}`);
|
|
345
488
|
})
|
|
346
489
|
);
|
|
347
490
|
return match?.name;
|
|
348
491
|
}
|
|
349
492
|
|
|
493
|
+
// packages/core/src/data-confidence.ts
|
|
494
|
+
function buildDataConfidence(input) {
|
|
495
|
+
const sessions = input.sessions;
|
|
496
|
+
const availableSources = input.sources.filter((source) => source.available);
|
|
497
|
+
const importedSessions = input.sourceStats.reduce((sum2, source) => sum2 + (source.sessionsImported ?? 0), 0);
|
|
498
|
+
const tokenDataSessions = sessions.filter((session) => session.totalTokens > 0);
|
|
499
|
+
const missingTokenSessions = sessions.filter((session) => session.totalTokens === 0 || session.warnings.includes("missing_token_breakdown"));
|
|
500
|
+
const pricedTokenSessions = tokenDataSessions.filter((session) => session.estimatedCostUsd !== void 0);
|
|
501
|
+
const unpricedTokenSessions = tokenDataSessions.filter((session) => session.estimatedCostUsd === void 0);
|
|
502
|
+
const highConfidenceTokenSessions = tokenDataSessions.filter((session) => session.tokenConfidence === "high");
|
|
503
|
+
const unknownRepoSessions = sessions.filter((session) => session.warnings.includes("repo_unverified_no_git_root"));
|
|
504
|
+
const verifiedRepoSessions = sessions.filter((session) => !unknownRepoSessions.includes(session));
|
|
505
|
+
const unknownSurfaceSessions = sessions.filter((session) => (session.detectedSurface ?? "unknown") === "unknown");
|
|
506
|
+
const parseIssueCount = Math.max(
|
|
507
|
+
input.scan.parseFailureCount ?? 0,
|
|
508
|
+
input.sourceStats.reduce((sum2, source) => sum2 + (source.parseFailureCount ?? 0), 0)
|
|
509
|
+
);
|
|
510
|
+
const sourceWarningCount = input.sources.reduce((sum2, source) => sum2 + source.warnings.length, 0);
|
|
511
|
+
const tokenDataPct = ratio(tokenDataSessions.length, sessions.length);
|
|
512
|
+
const pricingCoveragePct = tokenDataSessions.length > 0 ? ratio(pricedTokenSessions.length, tokenDataSessions.length) : sessions.length > 0 ? 0 : 1;
|
|
513
|
+
const highConfidenceTokenPct = tokenDataSessions.length > 0 ? ratio(highConfidenceTokenSessions.length, tokenDataSessions.length) : sessions.length > 0 ? 0 : 1;
|
|
514
|
+
const verifiedRepoPct = ratio(verifiedRepoSessions.length, sessions.length);
|
|
515
|
+
const parseHealthPct = parseIssueCount === 0 ? 1 : 1 - ratio(parseIssueCount, Math.max(input.scan.rawSessionCount ?? sessions.length, sessions.length, 1));
|
|
516
|
+
const rawScore = Math.round(100 * (tokenDataPct * 0.3 + pricingCoveragePct * 0.25 + verifiedRepoPct * 0.18 + highConfidenceTokenPct * 0.12 + Math.max(parseHealthPct, 0) * 0.15));
|
|
517
|
+
const score = emptyDataScore(availableSources.length, importedSessions) ?? rawScore;
|
|
518
|
+
const issues = dataConfidenceIssues({
|
|
519
|
+
sessions,
|
|
520
|
+
availableSources,
|
|
521
|
+
importedSessions,
|
|
522
|
+
tokenDataSessions,
|
|
523
|
+
missingTokenSessions,
|
|
524
|
+
unpricedTokenSessions,
|
|
525
|
+
unknownRepoSessions,
|
|
526
|
+
unknownSurfaceSessions,
|
|
527
|
+
parseIssueCount,
|
|
528
|
+
sourceWarningCount
|
|
529
|
+
});
|
|
530
|
+
return {
|
|
531
|
+
score,
|
|
532
|
+
label: score >= 80 ? "High" : score >= 55 ? "Medium" : "Low",
|
|
533
|
+
sessionCount: sessions.length,
|
|
534
|
+
sourceCount: availableSources.length,
|
|
535
|
+
tokenDataSessions: tokenDataSessions.length,
|
|
536
|
+
missingTokenSessions: missingTokenSessions.length,
|
|
537
|
+
pricedTokenSessions: pricedTokenSessions.length,
|
|
538
|
+
unpricedTokenSessions: unpricedTokenSessions.length,
|
|
539
|
+
highConfidenceTokenSessions: highConfidenceTokenSessions.length,
|
|
540
|
+
verifiedRepoSessions: verifiedRepoSessions.length,
|
|
541
|
+
unknownRepoSessions: unknownRepoSessions.length,
|
|
542
|
+
unknownSurfaceSessions: unknownSurfaceSessions.length,
|
|
543
|
+
parseIssueCount,
|
|
544
|
+
sourceWarningCount,
|
|
545
|
+
tokenDataPct,
|
|
546
|
+
pricingCoveragePct,
|
|
547
|
+
highConfidenceTokenPct,
|
|
548
|
+
verifiedRepoPct,
|
|
549
|
+
issues
|
|
550
|
+
};
|
|
551
|
+
}
|
|
552
|
+
function dataConfidenceIssues({
|
|
553
|
+
sessions,
|
|
554
|
+
availableSources,
|
|
555
|
+
importedSessions,
|
|
556
|
+
tokenDataSessions,
|
|
557
|
+
missingTokenSessions,
|
|
558
|
+
unpricedTokenSessions,
|
|
559
|
+
unknownRepoSessions,
|
|
560
|
+
unknownSurfaceSessions,
|
|
561
|
+
parseIssueCount,
|
|
562
|
+
sourceWarningCount
|
|
563
|
+
}) {
|
|
564
|
+
const issues = [];
|
|
565
|
+
if (availableSources.length === 0) {
|
|
566
|
+
issues.push({
|
|
567
|
+
id: "no-local-sources",
|
|
568
|
+
title: "No local source data found",
|
|
569
|
+
detail: "RepoSpend did not find enabled local Codex, Claude Code, GitHub Copilot, or experimental Cursor data paths.",
|
|
570
|
+
tone: "critical",
|
|
571
|
+
affectedSessionIds: []
|
|
572
|
+
});
|
|
573
|
+
} else if (sessions.length === 0 && importedSessions === 0) {
|
|
574
|
+
issues.push({
|
|
575
|
+
id: "no-sessions-imported",
|
|
576
|
+
title: "No sessions imported",
|
|
577
|
+
detail: "Source paths exist, but no local sessions were imported for the current scan window.",
|
|
578
|
+
tone: "attention",
|
|
579
|
+
affectedSessionIds: []
|
|
580
|
+
});
|
|
581
|
+
}
|
|
582
|
+
if (tokenDataSessions.length === 0 && sessions.length > 0) {
|
|
583
|
+
issues.push({
|
|
584
|
+
id: "no-token-data",
|
|
585
|
+
title: "No token-bearing sessions",
|
|
586
|
+
detail: "Sessions were imported, but local files did not include token counts that RepoSpend can price or roll up.",
|
|
587
|
+
tone: "critical",
|
|
588
|
+
affectedSessionIds: sessions.map((session) => session.id)
|
|
589
|
+
});
|
|
590
|
+
} else if (missingTokenSessions.length > 0) {
|
|
591
|
+
issues.push({
|
|
592
|
+
id: "missing-token-data",
|
|
593
|
+
title: "Some sessions are missing token data",
|
|
594
|
+
detail: `${missingTokenSessions.length} session${missingTokenSessions.length === 1 ? "" : "s"} have no token total or are missing a full token breakdown.`,
|
|
595
|
+
tone: "attention",
|
|
596
|
+
affectedSessionIds: missingTokenSessions.map((session) => session.id)
|
|
597
|
+
});
|
|
598
|
+
}
|
|
599
|
+
if (unpricedTokenSessions.length > 0) {
|
|
600
|
+
issues.push({
|
|
601
|
+
id: "unpriced-token-sessions",
|
|
602
|
+
title: "Some token-bearing sessions are unpriced",
|
|
603
|
+
detail: `${unpricedTokenSessions.length} token-bearing session${unpricedTokenSessions.length === 1 ? "" : "s"} cannot show API-equivalent cost because pricing or token split coverage is missing.`,
|
|
604
|
+
tone: "attention",
|
|
605
|
+
affectedSessionIds: unpricedTokenSessions.map((session) => session.id)
|
|
606
|
+
});
|
|
607
|
+
}
|
|
608
|
+
if (parseIssueCount > 0) {
|
|
609
|
+
issues.push({
|
|
610
|
+
id: "parse-issues",
|
|
611
|
+
title: "Parser issues detected",
|
|
612
|
+
detail: `${parseIssueCount} local file${parseIssueCount === 1 ? "" : "s"} reported parse issues or skipped records.`,
|
|
613
|
+
tone: "attention",
|
|
614
|
+
affectedSessionIds: sessions.filter((session) => session.parseStatus === "failed" || (session.parseErrors?.length ?? 0) > 0).map((session) => session.id)
|
|
615
|
+
});
|
|
616
|
+
}
|
|
617
|
+
if (unknownRepoSessions.length > 0) {
|
|
618
|
+
issues.push({
|
|
619
|
+
id: "unknown-repo-grouping",
|
|
620
|
+
title: "Some repo grouping is unverified",
|
|
621
|
+
detail: `${unknownRepoSessions.length} session${unknownRepoSessions.length === 1 ? "" : "s"} could not be tied to a verified Git root.`,
|
|
622
|
+
tone: "attention",
|
|
623
|
+
affectedSessionIds: unknownRepoSessions.map((session) => session.id)
|
|
624
|
+
});
|
|
625
|
+
}
|
|
626
|
+
if (unknownSurfaceSessions.length > 0) {
|
|
627
|
+
issues.push({
|
|
628
|
+
id: "unknown-surfaces",
|
|
629
|
+
title: "Some app surfaces are unknown",
|
|
630
|
+
detail: `${unknownSurfaceSessions.length} session${unknownSurfaceSessions.length === 1 ? "" : "s"} did not include enough metadata to identify the app or surface.`,
|
|
631
|
+
tone: "attention",
|
|
632
|
+
affectedSessionIds: unknownSurfaceSessions.map((session) => session.id)
|
|
633
|
+
});
|
|
634
|
+
}
|
|
635
|
+
if (sourceWarningCount > 0) {
|
|
636
|
+
issues.push({
|
|
637
|
+
id: "source-warnings",
|
|
638
|
+
title: "Source warnings were reported",
|
|
639
|
+
detail: `${sourceWarningCount} source warning${sourceWarningCount === 1 ? " was" : "s were"} reported during local discovery.`,
|
|
640
|
+
tone: "attention",
|
|
641
|
+
affectedSessionIds: []
|
|
642
|
+
});
|
|
643
|
+
}
|
|
644
|
+
return issues;
|
|
645
|
+
}
|
|
646
|
+
function ratio(numerator, denominator) {
|
|
647
|
+
if (denominator <= 0) return 1;
|
|
648
|
+
return numerator / denominator;
|
|
649
|
+
}
|
|
650
|
+
function emptyDataScore(availableSourceCount, importedSessions) {
|
|
651
|
+
if (availableSourceCount === 0) return 0;
|
|
652
|
+
if (importedSessions === 0) return 50;
|
|
653
|
+
return void 0;
|
|
654
|
+
}
|
|
655
|
+
|
|
350
656
|
// packages/core/src/filters.ts
|
|
351
657
|
function filterUsage(sessions, filters) {
|
|
352
658
|
return sessions.filter((session) => {
|
|
@@ -427,7 +733,20 @@ function repoWarnings(group, sessions) {
|
|
|
427
733
|
return warnings;
|
|
428
734
|
}
|
|
429
735
|
function compactNumber(value) {
|
|
430
|
-
|
|
736
|
+
if (!Number.isFinite(value)) return "0";
|
|
737
|
+
const sign = value < 0 ? "-" : "";
|
|
738
|
+
const absolute = Math.abs(value);
|
|
739
|
+
const units = [
|
|
740
|
+
{ threshold: 1e12, suffix: "T" },
|
|
741
|
+
{ threshold: 1e9, suffix: "B" },
|
|
742
|
+
{ threshold: 1e6, suffix: "M" },
|
|
743
|
+
{ threshold: 1e3, suffix: "K" }
|
|
744
|
+
];
|
|
745
|
+
const unit = units.find((item) => absolute >= item.threshold);
|
|
746
|
+
if (!unit) return new Intl.NumberFormat(void 0, { maximumFractionDigits: 1 }).format(value);
|
|
747
|
+
const scaled = absolute / unit.threshold;
|
|
748
|
+
const formatted = new Intl.NumberFormat(void 0, { maximumFractionDigits: 1 }).format(scaled);
|
|
749
|
+
return `${sign}${formatted}${unit.suffix}`;
|
|
431
750
|
}
|
|
432
751
|
function unique2(values) {
|
|
433
752
|
return [...new Set(values)].sort();
|
|
@@ -591,7 +910,7 @@ var healthSignalDetectors = [
|
|
|
591
910
|
return {
|
|
592
911
|
id: "command-friction",
|
|
593
912
|
tone: "attention",
|
|
594
|
-
title: "
|
|
913
|
+
title: "Possible failed command rate looks high",
|
|
595
914
|
detail: `${percent(commandIssueRate)} of detected shell commands look like blocking, repeated, or token-expensive issues in this filtered view.`,
|
|
596
915
|
action: "Look for repeated setup, dependency, permission, test, or build issues in the Sessions page.",
|
|
597
916
|
actionTarget: "sessions",
|
|
@@ -662,9 +981,9 @@ var healthSignalDetectors = [
|
|
|
662
981
|
tone: "attention",
|
|
663
982
|
title: "Some sessions cannot be priced",
|
|
664
983
|
detail: `${count(unknownCostSessions.length, "session")} are missing a model price or detailed token split.`,
|
|
665
|
-
action: "
|
|
666
|
-
actionTarget: "
|
|
667
|
-
actionLabel: "
|
|
984
|
+
action: "Review the affected sessions first. Add a model rate only when the session has token details but no usable price.",
|
|
985
|
+
actionTarget: "sessions",
|
|
986
|
+
actionLabel: "Review unpriced sessions",
|
|
668
987
|
metric: count(unknownCostSessions.length, "session"),
|
|
669
988
|
critical: true,
|
|
670
989
|
affectedSessionIds: unknownCostSessions.map((session) => session.id)
|
|
@@ -758,7 +1077,7 @@ function buildWasteSignals(sessions) {
|
|
|
758
1077
|
var wasteSignalDetectors = [
|
|
759
1078
|
wasteSignalDetector("high-token-no-edit", "High-token no-edit sessions", "Large sessions where local events did not show file edits.", (session) => (session.fileEditCount ?? 0) === 0 && session.totalTokens >= 1e6),
|
|
760
1079
|
wasteSignalDetector("high-token-low-output", "High-token low-output sessions", "Large sessions where most tokens were input or cached context.", (session) => session.totalTokens >= 1e6 && session.outputTokens / Math.max(session.totalTokens, 1) < 0.03),
|
|
761
|
-
wasteSignalDetector("command-issue-sessions", "
|
|
1080
|
+
wasteSignalDetector("command-issue-sessions", "Possible failed command sessions", "Sessions with blocking, repeated, or token-expensive command failures.", (session) => importantCommandFailures(session) > 0),
|
|
762
1081
|
wasteSignalDetector("unknown-repos", "Unknown repo sessions", "Repo root could not be verified from local Git metadata.", (session) => session.warnings.includes("repo_unverified_no_git_root")),
|
|
763
1082
|
wasteSignalDetector("unknown-surfaces", "Unknown surface sessions", "Local logs did not identify VS Code, terminal, exec, or app surface.", (session) => (session.detectedSurface ?? "unknown") === "unknown"),
|
|
764
1083
|
wasteSignalDetector("repeated-errors", "Repeated error signals", "Repeated-error hints found in session warnings.", (session) => session.warnings.some((warning) => warning.includes("repeated_error")))
|
|
@@ -810,7 +1129,20 @@ function percent(value) {
|
|
|
810
1129
|
return `${new Intl.NumberFormat(void 0, { maximumFractionDigits: 1 }).format(value * 100)}%`;
|
|
811
1130
|
}
|
|
812
1131
|
function compactNumber2(value) {
|
|
813
|
-
|
|
1132
|
+
if (!Number.isFinite(value)) return "0";
|
|
1133
|
+
const sign = value < 0 ? "-" : "";
|
|
1134
|
+
const absolute = Math.abs(value);
|
|
1135
|
+
const units = [
|
|
1136
|
+
{ threshold: 1e12, suffix: "T" },
|
|
1137
|
+
{ threshold: 1e9, suffix: "B" },
|
|
1138
|
+
{ threshold: 1e6, suffix: "M" },
|
|
1139
|
+
{ threshold: 1e3, suffix: "K" }
|
|
1140
|
+
];
|
|
1141
|
+
const unit = units.find((item) => absolute >= item.threshold);
|
|
1142
|
+
if (!unit) return new Intl.NumberFormat(void 0, { maximumFractionDigits: 1 }).format(value);
|
|
1143
|
+
const scaled = absolute / unit.threshold;
|
|
1144
|
+
const formatted = new Intl.NumberFormat(void 0, { maximumFractionDigits: 1 }).format(scaled);
|
|
1145
|
+
return `${sign}${formatted}${unit.suffix}`;
|
|
814
1146
|
}
|
|
815
1147
|
function currency(value, minimumFractionDigits, maximumFractionDigits) {
|
|
816
1148
|
return `$${new Intl.NumberFormat(void 0, { maximumFractionDigits, minimumFractionDigits }).format(value)}`;
|
|
@@ -853,24 +1185,25 @@ function buildDashboardSnapshot(input) {
|
|
|
853
1185
|
hours: groupByHour(sessions),
|
|
854
1186
|
models: groupByModel(sessions),
|
|
855
1187
|
sourceApps,
|
|
856
|
-
health: buildUsageHealth({ sessions, summary, repos, sourceApps, scan })
|
|
1188
|
+
health: buildUsageHealth({ sessions, summary, repos, sourceApps, scan }),
|
|
1189
|
+
confidence: buildDataConfidence({ sources: input.sources, sourceStats: input.sourceStats, sessions, scan })
|
|
857
1190
|
};
|
|
858
1191
|
}
|
|
859
1192
|
function isDashboardVisibleSession(session) {
|
|
860
|
-
return session.totalTokens > 0 || session.sourceClient === "claude" || session.sourceClient === "cursor";
|
|
1193
|
+
return session.totalTokens > 0 || session.sourceClient === "claude" || session.sourceClient === "cursor" || session.sourceClient === "copilot";
|
|
861
1194
|
}
|
|
862
1195
|
|
|
863
1196
|
// packages/core/src/repo.ts
|
|
864
|
-
import
|
|
865
|
-
import
|
|
1197
|
+
import fs4 from "node:fs";
|
|
1198
|
+
import path4 from "node:path";
|
|
866
1199
|
function normalizeRoot(p) {
|
|
867
1200
|
const stripped = p.startsWith("\\\\?\\") ? p.slice(4) : p;
|
|
868
1201
|
return stripped.replace(/^([a-zA-Z]):/, (_, letter) => letter.toUpperCase() + ":");
|
|
869
1202
|
}
|
|
870
|
-
function findGitRoot(cwd, fileSystem =
|
|
871
|
-
let current = normalizeRoot(
|
|
1203
|
+
function findGitRoot(cwd, fileSystem = fs4) {
|
|
1204
|
+
let current = normalizeRoot(path4.resolve(cwd));
|
|
872
1205
|
while (true) {
|
|
873
|
-
const gitPath =
|
|
1206
|
+
const gitPath = path4.join(current, ".git");
|
|
874
1207
|
if (fileSystem.existsSync(gitPath)) {
|
|
875
1208
|
try {
|
|
876
1209
|
const stat = fileSystem.statSync(gitPath);
|
|
@@ -881,19 +1214,19 @@ function findGitRoot(cwd, fileSystem = fs3) {
|
|
|
881
1214
|
return current;
|
|
882
1215
|
}
|
|
883
1216
|
}
|
|
884
|
-
const parent =
|
|
1217
|
+
const parent = path4.dirname(current);
|
|
885
1218
|
if (parent === current) {
|
|
886
1219
|
return void 0;
|
|
887
1220
|
}
|
|
888
1221
|
current = parent;
|
|
889
1222
|
}
|
|
890
1223
|
}
|
|
891
|
-
function resolveRepoInfo(cwd, config = {}, fileSystem =
|
|
1224
|
+
function resolveRepoInfo(cwd, config = {}, fileSystem = fs4) {
|
|
892
1225
|
const warnings = [];
|
|
893
1226
|
const gitRoot = safeFindGitRoot(cwd, fileSystem);
|
|
894
1227
|
const repoRoot = gitRoot ?? fallbackWorkspaceRoot(cwd);
|
|
895
1228
|
const alias = aliasForPath(repoRoot, config);
|
|
896
|
-
const repoName = alias ?? (
|
|
1229
|
+
const repoName = alias ?? (path4.basename(repoRoot) || "unknown-workspace");
|
|
897
1230
|
const gitMeta = gitRoot ? readGitMetadata(gitRoot, fileSystem) : { remote: void 0, branch: void 0 };
|
|
898
1231
|
if (!gitRoot) {
|
|
899
1232
|
warnings.push("repo_unverified_no_git_root");
|
|
@@ -915,23 +1248,23 @@ function safeFindGitRoot(cwd, fileSystem) {
|
|
|
915
1248
|
}
|
|
916
1249
|
}
|
|
917
1250
|
function fallbackWorkspaceRoot(cwd) {
|
|
918
|
-
const resolved = normalizeRoot(
|
|
919
|
-
const home = process.env.HOME ? normalizeRoot(
|
|
920
|
-
if (home && (resolved === home || resolved.startsWith(`${home}${
|
|
921
|
-
const relative =
|
|
1251
|
+
const resolved = normalizeRoot(path4.resolve(cwd || process.cwd()));
|
|
1252
|
+
const home = process.env.HOME ? normalizeRoot(path4.resolve(process.env.HOME)) : void 0;
|
|
1253
|
+
if (home && (resolved === home || resolved.startsWith(`${home}${path4.sep}`))) {
|
|
1254
|
+
const relative = path4.relative(home, resolved).split(path4.sep).filter(Boolean);
|
|
922
1255
|
const first = relative[0];
|
|
923
|
-
return first ?
|
|
1256
|
+
return first ? path4.join(home, first) : home;
|
|
924
1257
|
}
|
|
925
|
-
const parts = resolved.split(
|
|
926
|
-
return `${
|
|
1258
|
+
const parts = resolved.split(path4.sep).filter(Boolean);
|
|
1259
|
+
return `${path4.sep}${parts.slice(0, Math.min(parts.length, 3)).join(path4.sep)}`;
|
|
927
1260
|
}
|
|
928
1261
|
function readGitMetadata(repoRoot, fileSystem) {
|
|
929
|
-
const gitPath =
|
|
1262
|
+
const gitPath = path4.join(repoRoot, ".git");
|
|
930
1263
|
try {
|
|
931
1264
|
const stat = fileSystem.statSync(gitPath);
|
|
932
1265
|
const gitDir = stat.isFile() ? resolveGitFile(repoRoot, fileSystem.readFileSync(gitPath, "utf8")) : gitPath;
|
|
933
|
-
const configPath2 =
|
|
934
|
-
const headPath =
|
|
1266
|
+
const configPath2 = path4.join(gitDir, "config");
|
|
1267
|
+
const headPath = path4.join(gitDir, "HEAD");
|
|
935
1268
|
const remote = fileSystem.existsSync(configPath2) ? parseRemote(fileSystem.readFileSync(configPath2, "utf8")) : void 0;
|
|
936
1269
|
const branch = fileSystem.existsSync(headPath) ? parseBranch(fileSystem.readFileSync(headPath, "utf8")) : void 0;
|
|
937
1270
|
return { remote, branch };
|
|
@@ -942,9 +1275,9 @@ function readGitMetadata(repoRoot, fileSystem) {
|
|
|
942
1275
|
function resolveGitFile(repoRoot, contents) {
|
|
943
1276
|
const match = contents.match(/^gitdir:\s*(.+)$/m);
|
|
944
1277
|
if (!match?.[1]) {
|
|
945
|
-
return
|
|
1278
|
+
return path4.join(repoRoot, ".git");
|
|
946
1279
|
}
|
|
947
|
-
return
|
|
1280
|
+
return path4.resolve(repoRoot, match[1].trim());
|
|
948
1281
|
}
|
|
949
1282
|
function parseRemote(config) {
|
|
950
1283
|
const originMatch = config.match(/\[remote "origin"\][\s\S]*?\n\s*url\s*=\s*(.+)/);
|
|
@@ -957,9 +1290,9 @@ function parseBranch(head) {
|
|
|
957
1290
|
|
|
958
1291
|
// packages/core/src/sources/codex.ts
|
|
959
1292
|
import { spawnSync } from "node:child_process";
|
|
960
|
-
import
|
|
1293
|
+
import fs5 from "node:fs";
|
|
961
1294
|
import os2 from "node:os";
|
|
962
|
-
import
|
|
1295
|
+
import path5 from "node:path";
|
|
963
1296
|
import { fileURLToPath } from "node:url";
|
|
964
1297
|
import Database from "better-sqlite3";
|
|
965
1298
|
|
|
@@ -967,8 +1300,9 @@ import Database from "better-sqlite3";
|
|
|
967
1300
|
function analyzeAgentFriction(records, totalTokens) {
|
|
968
1301
|
const analysis = emptyAgentFrictionAnalysis();
|
|
969
1302
|
const messages = [];
|
|
1303
|
+
const toolCallsById = indexToolCallsById(records);
|
|
970
1304
|
for (const record of records) {
|
|
971
|
-
applyActionMetadata(analysis, record, messages);
|
|
1305
|
+
applyActionMetadata(analysis, record, messages, toolCallsById);
|
|
972
1306
|
}
|
|
973
1307
|
finalizeCommandIssues(analysis, totalTokens);
|
|
974
1308
|
return analysis;
|
|
@@ -994,7 +1328,7 @@ function emptyAgentFrictionAnalysis() {
|
|
|
994
1328
|
assistantMessageCount: 0
|
|
995
1329
|
};
|
|
996
1330
|
}
|
|
997
|
-
function applyActionMetadata(analysis, record, messages) {
|
|
1331
|
+
function applyActionMetadata(analysis, record, messages, toolCallsById) {
|
|
998
1332
|
if (!record || typeof record !== "object") return;
|
|
999
1333
|
const object = record;
|
|
1000
1334
|
const payload = firstObject(object.payload) ?? object;
|
|
@@ -1005,12 +1339,73 @@ function applyActionMetadata(analysis, record, messages) {
|
|
|
1005
1339
|
const text = JSON.stringify(record).toLowerCase();
|
|
1006
1340
|
const isTool = type?.includes("tool") || type?.includes("function_call") || text.includes('"tool_call"') || text.includes('"function_call"');
|
|
1007
1341
|
if (isTool) analysis.toolCallCount += 1;
|
|
1008
|
-
const isNonZeroCommand =
|
|
1009
|
-
if (isNonZeroCommand) analysis.commandIssues.push(classifyCommandIssue(record, analysis.commandIssues));
|
|
1342
|
+
const isNonZeroCommand = hasNonZeroCommandExit(record);
|
|
1343
|
+
if (isNonZeroCommand) analysis.commandIssues.push(classifyCommandIssue(record, analysis.commandIssues, commandTextForToolOutput(record, toolCallsById)));
|
|
1010
1344
|
if (/\b(exec_command|shell|bash|zsh|command)\b/.test(text)) analysis.shellCommandCount += 1;
|
|
1011
1345
|
if (/\b(read_file|view_image|cat |sed |rg |grep |open_file)\b/.test(text)) analysis.fileReadCount += 1;
|
|
1012
1346
|
if (/\b(apply_patch|write_file|edit|patch|created|deleted)\b/.test(text)) analysis.fileEditCount += 1;
|
|
1013
1347
|
}
|
|
1348
|
+
function indexToolCallsById(records) {
|
|
1349
|
+
const calls = /* @__PURE__ */ new Map();
|
|
1350
|
+
const sessionCommands = /* @__PURE__ */ new Map();
|
|
1351
|
+
for (const record of records) {
|
|
1352
|
+
if (!record || typeof record !== "object") continue;
|
|
1353
|
+
const object = record;
|
|
1354
|
+
const payload = firstObject(object.payload) ?? object;
|
|
1355
|
+
const type = `${stringValue(object.type) ?? ""} ${stringValue(payload.type) ?? ""}`.toLowerCase();
|
|
1356
|
+
const callId = stringValue(payload.call_id ?? object.call_id ?? payload.callId ?? object.callId);
|
|
1357
|
+
if (type.includes("function_call_output") || type.includes("custom_tool_call_output") || type.includes("tool_result")) {
|
|
1358
|
+
const output = stringValue(payload.output ?? object.output);
|
|
1359
|
+
const sessionId = output?.match(/\bProcess running with session ID\s+(\d+)\b/i)?.[1];
|
|
1360
|
+
const command3 = callId ? calls.get(callId) : void 0;
|
|
1361
|
+
if (sessionId && command3) sessionCommands.set(sessionId, command3);
|
|
1362
|
+
continue;
|
|
1363
|
+
}
|
|
1364
|
+
if (!type.includes("function_call") && !type.includes("custom_tool_call") && !type.includes("tool_call")) continue;
|
|
1365
|
+
const command2 = toolCallCommandText(payload, sessionCommands) ?? toolCallCommandText(object, sessionCommands);
|
|
1366
|
+
if (callId && command2) calls.set(callId, command2);
|
|
1367
|
+
}
|
|
1368
|
+
return calls;
|
|
1369
|
+
}
|
|
1370
|
+
function commandTextForToolOutput(record, toolCallsById) {
|
|
1371
|
+
if (!record || typeof record !== "object") return void 0;
|
|
1372
|
+
const object = record;
|
|
1373
|
+
const payload = firstObject(object.payload) ?? object;
|
|
1374
|
+
const callId = stringValue(payload.call_id ?? object.call_id ?? payload.callId ?? object.callId);
|
|
1375
|
+
return callId ? toolCallsById.get(callId) : void 0;
|
|
1376
|
+
}
|
|
1377
|
+
function toolCallCommandText(object, sessionCommands) {
|
|
1378
|
+
const name = `${stringValue(object.name) ?? ""} ${stringValue(object.recipient_name) ?? ""}`.toLowerCase();
|
|
1379
|
+
const args2 = object.arguments ?? object.args ?? object.input;
|
|
1380
|
+
const parsedArgs = typeof args2 === "string" ? parseJsonObject(args2) : firstObject(args2);
|
|
1381
|
+
const sessionId = parsedArgs ? idValue(parsedArgs.session_id) : void 0;
|
|
1382
|
+
if (sessionId && sessionCommands.has(sessionId)) return sessionCommands.get(sessionId);
|
|
1383
|
+
const command2 = parsedArgs ? extractCommandText(parsedArgs) : void 0;
|
|
1384
|
+
if (command2) return command2;
|
|
1385
|
+
if ((name.includes("exec_command") || name.includes("shell")) && typeof args2 === "string" && args2.trim()) return args2;
|
|
1386
|
+
const toolName = stringValue(object.name) ?? stringValue(object.recipient_name);
|
|
1387
|
+
if (!toolName) return void 0;
|
|
1388
|
+
return sessionId ? `${toolName} session ${sessionId}` : toolName;
|
|
1389
|
+
}
|
|
1390
|
+
function hasNonZeroCommandExit(record) {
|
|
1391
|
+
if (!record || typeof record !== "object") return false;
|
|
1392
|
+
const object = record;
|
|
1393
|
+
const payload = firstObject(object.payload) ?? object;
|
|
1394
|
+
const type = `${stringValue(object.type) ?? ""} ${stringValue(payload.type) ?? ""}`.toLowerCase();
|
|
1395
|
+
const isCommandRecord = /(function_call_output|custom_tool_call_output|tool_result|exec_command|shell|command)/.test(type) || Boolean(extractCommandText(payload) ?? extractCommandText(object));
|
|
1396
|
+
const explicitExitCode = exitCodeValue(payload) ?? exitCodeValue(object);
|
|
1397
|
+
if (explicitExitCode !== void 0) return isCommandRecord && explicitExitCode !== 0;
|
|
1398
|
+
const output = stringValue(payload.output) ?? stringValue(object.output);
|
|
1399
|
+
if (!output || !/(function_call_output|custom_tool_call_output|tool_result|exec_command)/.test(type)) return false;
|
|
1400
|
+
const processExitCode = output.match(/\bProcess exited with code\s+(-?\d+)\b/i)?.[1];
|
|
1401
|
+
return processExitCode !== void 0 && Number(processExitCode) !== 0;
|
|
1402
|
+
}
|
|
1403
|
+
function exitCodeValue(object) {
|
|
1404
|
+
const value = object.exit_code ?? object.exitCode;
|
|
1405
|
+
if (typeof value === "number" && Number.isFinite(value)) return value;
|
|
1406
|
+
if (typeof value === "string" && /^-?\d+$/.test(value.trim())) return Number(value);
|
|
1407
|
+
return void 0;
|
|
1408
|
+
}
|
|
1014
1409
|
function finalizeCommandIssues(analysis, totalTokens) {
|
|
1015
1410
|
const repeatedCommands = /* @__PURE__ */ new Map();
|
|
1016
1411
|
const repeatableIssues = analysis.commandIssues.filter((issue2) => issue2.classification !== "harmless_nonzero" && issue2.classification !== "exploratory_miss");
|
|
@@ -1055,30 +1450,38 @@ function issuePriority(issue2) {
|
|
|
1055
1450
|
return severityScore * 10 + impactScore;
|
|
1056
1451
|
}
|
|
1057
1452
|
function commandIssueReason(issue2) {
|
|
1058
|
-
if (issue2.severity === "critical") return "Repeated or blocking command
|
|
1059
|
-
if (issue2.severity === "warning") return "Non-zero command event classified as
|
|
1453
|
+
if (issue2.severity === "critical") return "Repeated or blocking command failure that may have stopped progress.";
|
|
1454
|
+
if (issue2.severity === "warning") return "Non-zero command event classified as worth reviewing for this session.";
|
|
1060
1455
|
if (issue2.classification === "exploratory_miss") return "Likely exploratory command that found no result.";
|
|
1061
1456
|
if (issue2.classification === "harmless_nonzero") return "Likely harmless non-zero exit during normal shell exploration.";
|
|
1062
1457
|
return "Non-zero command event with limited local context.";
|
|
1063
1458
|
}
|
|
1064
|
-
function classifyCommandIssue(record, previousIssues) {
|
|
1459
|
+
function classifyCommandIssue(record, previousIssues, commandHint) {
|
|
1065
1460
|
const raw = JSON.stringify(record);
|
|
1066
1461
|
const text = raw.toLowerCase();
|
|
1067
|
-
const command2 = extractCommandText(record) ?? compactCommandText(raw);
|
|
1068
|
-
const
|
|
1462
|
+
const command2 = commandHint ?? extractCommandText(record) ?? compactCommandText(raw);
|
|
1463
|
+
const commandText = command2.toLowerCase();
|
|
1464
|
+
const classificationText = commandHint ? `${commandText} ${commandOutputText(record) ?? ""}` : text;
|
|
1465
|
+
const category = commandCategory(commandHint ? commandText : text);
|
|
1069
1466
|
const commandKey = normalizeCommand(command2);
|
|
1070
1467
|
const repeated = previousIssues.filter((issue2) => issue2.command === commandKey).length + 1 >= 3;
|
|
1071
|
-
if (isHarmlessNonZero(
|
|
1072
|
-
return { command: commandKey, category, classification: isExploratoryMiss(
|
|
1468
|
+
if (isHarmlessNonZero(classificationText)) {
|
|
1469
|
+
return { command: commandKey, category, classification: isExploratoryMiss(classificationText) ? "exploratory_miss" : "harmless_nonzero", severity: "ignored", impact: "low" };
|
|
1073
1470
|
}
|
|
1074
|
-
if (isImportantFailure(
|
|
1075
|
-
return { command: commandKey, category, classification: "blocking_failure", severity: repeated ? "critical" : importantSeverity(
|
|
1471
|
+
if (isImportantFailure(classificationText, category) || repeated) {
|
|
1472
|
+
return { command: commandKey, category, classification: "blocking_failure", severity: repeated ? "critical" : importantSeverity(classificationText, category), impact: repeated ? "high" : importantImpact(category) };
|
|
1076
1473
|
}
|
|
1077
1474
|
if (category === "search" || category === "filesystem") {
|
|
1078
1475
|
return { command: commandKey, category, classification: "exploratory_miss", severity: "ignored", impact: "low" };
|
|
1079
1476
|
}
|
|
1080
1477
|
return { command: commandKey, category, classification: "unknown", severity: "info", impact: "low" };
|
|
1081
1478
|
}
|
|
1479
|
+
function commandOutputText(record) {
|
|
1480
|
+
if (!record || typeof record !== "object") return void 0;
|
|
1481
|
+
const object = record;
|
|
1482
|
+
const payload = firstObject(object.payload) ?? object;
|
|
1483
|
+
return stringValue(payload.output ?? object.output)?.toLowerCase().slice(0, 2e3);
|
|
1484
|
+
}
|
|
1082
1485
|
function isHarmlessNonZero(text) {
|
|
1083
1486
|
return /\b(rg|grep)\b/.test(text) || /git\s+(diff|status)/.test(text) || /\bfind\b/.test(text) || /\btest\s+(-f|-d|-e)\b/.test(text) || /\b(ls|cat)\b/.test(text) && /(optional|exists|existence|not found|no such file)/.test(text);
|
|
1084
1487
|
}
|
|
@@ -1158,9 +1561,21 @@ function compactCommandText(text) {
|
|
|
1158
1561
|
function firstObject(...values) {
|
|
1159
1562
|
return values.find((value) => Boolean(value) && typeof value === "object" && !Array.isArray(value));
|
|
1160
1563
|
}
|
|
1564
|
+
function parseJsonObject(value) {
|
|
1565
|
+
try {
|
|
1566
|
+
return firstObject(JSON.parse(value));
|
|
1567
|
+
} catch {
|
|
1568
|
+
return void 0;
|
|
1569
|
+
}
|
|
1570
|
+
}
|
|
1161
1571
|
function stringValue(value) {
|
|
1162
1572
|
return typeof value === "string" && value.length > 0 ? value : void 0;
|
|
1163
1573
|
}
|
|
1574
|
+
function idValue(value) {
|
|
1575
|
+
if (typeof value === "string" && value.length > 0) return value;
|
|
1576
|
+
if (typeof value === "number" && Number.isFinite(value)) return String(value);
|
|
1577
|
+
return void 0;
|
|
1578
|
+
}
|
|
1164
1579
|
function applyMessageCount(analysis, messages, role, payload, object) {
|
|
1165
1580
|
const message = extractMessageText(payload) ?? extractMessageText(object);
|
|
1166
1581
|
if (!message) return;
|
|
@@ -1398,36 +1813,44 @@ function numberFromKeys(object, keys) {
|
|
|
1398
1813
|
}
|
|
1399
1814
|
|
|
1400
1815
|
// packages/core/src/sources/codex.ts
|
|
1816
|
+
var codexSessionCacheVersion = "codex-session-v1";
|
|
1401
1817
|
var nativeSqliteRebuildAttempted = false;
|
|
1402
1818
|
function scanCodex(options) {
|
|
1403
|
-
const codexHome = options.codexHome ??
|
|
1404
|
-
const statePath =
|
|
1405
|
-
const sessionsPath =
|
|
1819
|
+
const codexHome = options.codexHome ?? path5.join(os2.homedir(), ".codex");
|
|
1820
|
+
const statePath = path5.join(codexHome, "state_5.sqlite");
|
|
1821
|
+
const sessionsPath = path5.join(codexHome, "sessions");
|
|
1822
|
+
const configPath2 = path5.join(codexHome, "config.toml");
|
|
1823
|
+
const configExists = fs5.existsSync(configPath2);
|
|
1824
|
+
const serviceTier = readCodexServiceTier(configPath2);
|
|
1406
1825
|
const warnings = [];
|
|
1407
1826
|
const sessions = [];
|
|
1408
|
-
const stateExists =
|
|
1409
|
-
const sessionsExists =
|
|
1827
|
+
const stateExists = fs5.existsSync(statePath);
|
|
1828
|
+
const sessionsExists = fs5.existsSync(sessionsPath);
|
|
1410
1829
|
if (!stateExists) {
|
|
1411
1830
|
warnings.push(`Codex SQLite state not found at ${statePath}`);
|
|
1412
1831
|
}
|
|
1413
1832
|
if (!sessionsExists) {
|
|
1414
1833
|
warnings.push(`Codex sessions directory not found at ${sessionsPath}`);
|
|
1415
1834
|
}
|
|
1416
|
-
const sessionFiles = sessionsExists ? indexSessionFiles(sessionsPath, warnings) : /* @__PURE__ */ new Map();
|
|
1835
|
+
const sessionFiles = sessionsExists ? indexSessionFiles(sessionsPath, warnings, options.scanWindow) : /* @__PURE__ */ new Map();
|
|
1417
1836
|
if (stateExists) {
|
|
1418
1837
|
const stateSessions = readThreads(statePath, sessionFiles, options, warnings);
|
|
1419
1838
|
sessions.push(...stateSessions);
|
|
1420
1839
|
}
|
|
1421
1840
|
if (sessionsExists) {
|
|
1422
|
-
sessions.push(...readStandaloneSessions(sessionFiles, options).filter((session) => !hasImportedSession(sessions, session)));
|
|
1841
|
+
sessions.push(...readStandaloneSessions(remainingStandaloneSessionFiles(sessionFiles, sessions), options).filter((session) => !hasImportedSession(sessions, session)));
|
|
1423
1842
|
}
|
|
1424
1843
|
return {
|
|
1425
1844
|
source: {
|
|
1426
1845
|
id: "codex",
|
|
1427
1846
|
label: "Codex",
|
|
1428
1847
|
available: stateExists || sessionsExists,
|
|
1429
|
-
paths: [statePath, sessionsPath],
|
|
1430
|
-
warnings
|
|
1848
|
+
paths: configExists ? [statePath, sessionsPath, configPath2] : [statePath, sessionsPath],
|
|
1849
|
+
warnings,
|
|
1850
|
+
serviceTier,
|
|
1851
|
+
serviceTierSource: serviceTier ? "current_config" : void 0,
|
|
1852
|
+
serviceTierConfidence: serviceTier ? "medium" : void 0,
|
|
1853
|
+
serviceTierDetail: serviceTier ? `Current Codex config service_tier: ${serviceTier}` : void 0
|
|
1431
1854
|
},
|
|
1432
1855
|
sessions,
|
|
1433
1856
|
stats: {
|
|
@@ -1443,10 +1866,23 @@ function scanCodex(options) {
|
|
|
1443
1866
|
sessionsImported: sessions.length,
|
|
1444
1867
|
parseFailureCount: sessions.filter((session) => session.parseStatus === "failed").length,
|
|
1445
1868
|
unreadableFileCount: sessions.filter((session) => session.parseErrors?.some((error) => error.startsWith("unable_to_read_session_file:"))).length,
|
|
1869
|
+
serviceTier,
|
|
1870
|
+
serviceTierSource: serviceTier ? "current_config" : void 0,
|
|
1871
|
+
serviceTierConfidence: serviceTier ? "medium" : void 0,
|
|
1872
|
+
serviceTierDetail: serviceTier ? `Current Codex config service_tier: ${serviceTier}` : void 0,
|
|
1446
1873
|
lastScannedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1447
1874
|
}
|
|
1448
1875
|
};
|
|
1449
1876
|
}
|
|
1877
|
+
function readCodexServiceTier(configPath2) {
|
|
1878
|
+
try {
|
|
1879
|
+
const text = fs5.readFileSync(configPath2, "utf8");
|
|
1880
|
+
const match = text.match(/^\s*service_tier\s*=\s*["']?([^"'\s#]+)["']?/m);
|
|
1881
|
+
return match?.[1]?.trim().toLowerCase() || void 0;
|
|
1882
|
+
} catch {
|
|
1883
|
+
return void 0;
|
|
1884
|
+
}
|
|
1885
|
+
}
|
|
1450
1886
|
function readThreads(statePath, sessionFiles, options, warnings) {
|
|
1451
1887
|
let db;
|
|
1452
1888
|
try {
|
|
@@ -1462,8 +1898,8 @@ function readThreads(statePath, sessionFiles, options, warnings) {
|
|
|
1462
1898
|
warnings.push("Codex threads table has no recognized columns.");
|
|
1463
1899
|
return [];
|
|
1464
1900
|
}
|
|
1465
|
-
const importedModels = readImportedModelMap(options.codexHome ??
|
|
1466
|
-
const rows = db.prepare(`SELECT ${selectedColumns.map((column) => `"${column}"`).join(", ")} FROM threads`).all();
|
|
1901
|
+
const importedModels = readImportedModelMap(options.codexHome ?? path5.join(os2.homedir(), ".codex"), warnings);
|
|
1902
|
+
const rows = db.prepare(`SELECT ${selectedColumns.map((column) => `"${column}"`).join(", ")} FROM threads`).all().filter((row) => rowMatchesScanWindow(row, options.scanWindow));
|
|
1467
1903
|
const sessions = rows.map((row, index) => threadToUsage(row, index, sessionFiles, options, importedModels));
|
|
1468
1904
|
return attributeSubagentsToParentSurfaces(sessions, rows);
|
|
1469
1905
|
} catch (error) {
|
|
@@ -1522,7 +1958,7 @@ function attemptNativeSqliteRebuild(warnings) {
|
|
|
1522
1958
|
warnings.push("Codex SQLite native module mismatch detected, but RepoSpend could not locate its package root for an automatic rebuild.");
|
|
1523
1959
|
return false;
|
|
1524
1960
|
}
|
|
1525
|
-
const packageManager =
|
|
1961
|
+
const packageManager = fs5.existsSync(path5.join(packageRoot, "pnpm-lock.yaml")) ? "pnpm" : "npm";
|
|
1526
1962
|
const result = spawnSync(packageManager, ["rebuild", "better-sqlite3"], {
|
|
1527
1963
|
cwd: packageRoot,
|
|
1528
1964
|
encoding: "utf8",
|
|
@@ -1536,8 +1972,8 @@ function attemptNativeSqliteRebuild(warnings) {
|
|
|
1536
1972
|
}
|
|
1537
1973
|
function findRepoSpendPackageRoot() {
|
|
1538
1974
|
const starts = [
|
|
1539
|
-
process.argv[1] ?
|
|
1540
|
-
|
|
1975
|
+
process.argv[1] ? path5.dirname(fs5.realpathSync(process.argv[1])) : void 0,
|
|
1976
|
+
path5.dirname(fileURLToPath(import.meta.url)),
|
|
1541
1977
|
process.cwd()
|
|
1542
1978
|
].filter((item) => Boolean(item));
|
|
1543
1979
|
for (const start of starts) {
|
|
@@ -1547,17 +1983,17 @@ function findRepoSpendPackageRoot() {
|
|
|
1547
1983
|
return void 0;
|
|
1548
1984
|
}
|
|
1549
1985
|
function findPackageRootNamedRepoSpend(start) {
|
|
1550
|
-
let current =
|
|
1986
|
+
let current = path5.resolve(start);
|
|
1551
1987
|
while (true) {
|
|
1552
|
-
const packagePath =
|
|
1553
|
-
if (
|
|
1988
|
+
const packagePath = path5.join(current, "package.json");
|
|
1989
|
+
if (fs5.existsSync(packagePath)) {
|
|
1554
1990
|
try {
|
|
1555
|
-
const packageJson = JSON.parse(
|
|
1991
|
+
const packageJson = JSON.parse(fs5.readFileSync(packagePath, "utf8"));
|
|
1556
1992
|
if (packageJson.name === "repospend") return current;
|
|
1557
1993
|
} catch {
|
|
1558
1994
|
}
|
|
1559
1995
|
}
|
|
1560
|
-
const parent =
|
|
1996
|
+
const parent = path5.dirname(current);
|
|
1561
1997
|
if (parent === current) return void 0;
|
|
1562
1998
|
current = parent;
|
|
1563
1999
|
}
|
|
@@ -1619,7 +2055,7 @@ function threadToUsage(row, index, sessionFiles, options, importedModels = /* @_
|
|
|
1619
2055
|
messageCount: sessionBreakdown.messageCount,
|
|
1620
2056
|
rawTokenTotal: dbTokens || sessionBreakdown.totalTokens || void 0,
|
|
1621
2057
|
warnings,
|
|
1622
|
-
codexHome: options.codexHome ??
|
|
2058
|
+
codexHome: options.codexHome ?? path5.join(os2.homedir(), ".codex"),
|
|
1623
2059
|
durationMs: durationMs(startedAt, endedAt),
|
|
1624
2060
|
rawEventCount: sessionBreakdown.rawEventCount,
|
|
1625
2061
|
parseStatus: sessionBreakdown.parseStatus,
|
|
@@ -1693,7 +2129,7 @@ function readStandaloneSessions(sessionFiles, options) {
|
|
|
1693
2129
|
messageCount: breakdown.messageCount,
|
|
1694
2130
|
rawTokenTotal: breakdown.totalTokens || void 0,
|
|
1695
2131
|
warnings: [...repo.warnings, ...breakdown.warnings],
|
|
1696
|
-
codexHome: options.codexHome ??
|
|
2132
|
+
codexHome: options.codexHome ?? path5.join(os2.homedir(), ".codex"),
|
|
1697
2133
|
durationMs: durationMs(breakdown.startedAt, breakdown.endedAt),
|
|
1698
2134
|
rawEventCount: breakdown.rawEventCount,
|
|
1699
2135
|
parseStatus: breakdown.parseStatus,
|
|
@@ -1738,13 +2174,18 @@ function hasImportedSession(sessions, candidate) {
|
|
|
1738
2174
|
return Boolean(candidatePath && sessionPath && candidatePath === sessionPath);
|
|
1739
2175
|
});
|
|
1740
2176
|
}
|
|
2177
|
+
function remainingStandaloneSessionFiles(sessionFiles, importedSessions) {
|
|
2178
|
+
const importedIds = new Set(importedSessions.map((session) => session.id).filter(Boolean));
|
|
2179
|
+
const importedPaths = new Set(importedSessions.map((session) => comparableSourcePath(session.sourcePath)).filter(Boolean));
|
|
2180
|
+
return new Map([...sessionFiles.entries()].filter(([id, sourcePath]) => !importedIds.has(id) && !importedPaths.has(comparableSourcePath(sourcePath))));
|
|
2181
|
+
}
|
|
1741
2182
|
function readImportedModelMap(codexHome, warnings) {
|
|
1742
|
-
const importsPath =
|
|
2183
|
+
const importsPath = path5.join(codexHome, "external_agent_session_imports.json");
|
|
1743
2184
|
const models = /* @__PURE__ */ new Map();
|
|
1744
|
-
if (!
|
|
2185
|
+
if (!fs5.existsSync(importsPath)) return models;
|
|
1745
2186
|
let parsed;
|
|
1746
2187
|
try {
|
|
1747
|
-
parsed = JSON.parse(
|
|
2188
|
+
parsed = JSON.parse(fs5.readFileSync(importsPath, "utf8"));
|
|
1748
2189
|
} catch (error) {
|
|
1749
2190
|
warnings.push(`Unable to read Codex external import index at ${importsPath}: ${errorMessage(error)}`);
|
|
1750
2191
|
return models;
|
|
@@ -1754,7 +2195,7 @@ function readImportedModelMap(codexHome, warnings) {
|
|
|
1754
2195
|
const item = asRecord(record);
|
|
1755
2196
|
const importedThreadId = stringValue2(item?.imported_thread_id);
|
|
1756
2197
|
const sourcePath = normalizeVerbatimPath(stringValue2(item?.source_path));
|
|
1757
|
-
if (!importedThreadId || !sourcePath || !
|
|
2198
|
+
if (!importedThreadId || !sourcePath || !fs5.existsSync(sourcePath)) continue;
|
|
1758
2199
|
const model = readImportedSourceModel(sourcePath, warnings);
|
|
1759
2200
|
if (model) models.set(importedThreadId, { model, provider: providerForModel(model) });
|
|
1760
2201
|
}
|
|
@@ -1764,7 +2205,7 @@ function readImportedSourceModel(sourcePath, warnings) {
|
|
|
1764
2205
|
const counts = /* @__PURE__ */ new Map();
|
|
1765
2206
|
let content;
|
|
1766
2207
|
try {
|
|
1767
|
-
content =
|
|
2208
|
+
content = fs5.readFileSync(sourcePath, "utf8");
|
|
1768
2209
|
} catch (error) {
|
|
1769
2210
|
warnings.push(`Unable to read Codex external import source at ${sourcePath}: ${errorMessage(error)}`);
|
|
1770
2211
|
return void 0;
|
|
@@ -1802,12 +2243,13 @@ function normalizeVerbatimPath(value) {
|
|
|
1802
2243
|
function comparableSourcePath(value) {
|
|
1803
2244
|
return normalizeVerbatimPath(value)?.replace(/\\/g, "/");
|
|
1804
2245
|
}
|
|
1805
|
-
function indexSessionFiles(sessionsPath, warnings) {
|
|
2246
|
+
function indexSessionFiles(sessionsPath, warnings, scanWindow) {
|
|
1806
2247
|
const files = /* @__PURE__ */ new Map();
|
|
1807
2248
|
try {
|
|
1808
2249
|
walk(sessionsPath, (filePath) => {
|
|
1809
2250
|
if (!/\.(jsonl|json|log)$/i.test(filePath)) return;
|
|
1810
|
-
|
|
2251
|
+
if (!fileMayOverlapScanWindow(filePath, scanWindow)) return;
|
|
2252
|
+
const id = path5.basename(filePath).replace(/\.(jsonl|json|log)$/i, "");
|
|
1811
2253
|
files.set(id, filePath);
|
|
1812
2254
|
});
|
|
1813
2255
|
} catch (error) {
|
|
@@ -1815,9 +2257,26 @@ function indexSessionFiles(sessionsPath, warnings) {
|
|
|
1815
2257
|
}
|
|
1816
2258
|
return files;
|
|
1817
2259
|
}
|
|
2260
|
+
function rowMatchesScanWindow(row, scanWindow) {
|
|
2261
|
+
if (!scanWindow?.fromMs && !scanWindow?.toMs) return true;
|
|
2262
|
+
const updatedAt = timestampMs(row.updated_at);
|
|
2263
|
+
const createdAt = timestampMs(row.created_at);
|
|
2264
|
+
const latestTimestamp = updatedAt ?? createdAt;
|
|
2265
|
+
if (scanWindow.fromMs !== void 0 && latestTimestamp !== void 0 && latestTimestamp < scanWindow.fromMs) return false;
|
|
2266
|
+
if (scanWindow.toMs !== void 0 && createdAt !== void 0 && createdAt > scanWindow.toMs) return false;
|
|
2267
|
+
return true;
|
|
2268
|
+
}
|
|
2269
|
+
function fileMayOverlapScanWindow(filePath, scanWindow) {
|
|
2270
|
+
if (!scanWindow?.fromMs) return true;
|
|
2271
|
+
try {
|
|
2272
|
+
return fs5.statSync(filePath).mtimeMs >= scanWindow.fromMs;
|
|
2273
|
+
} catch {
|
|
2274
|
+
return true;
|
|
2275
|
+
}
|
|
2276
|
+
}
|
|
1818
2277
|
function walk(root, onFile) {
|
|
1819
|
-
for (const entry of
|
|
1820
|
-
const fullPath =
|
|
2278
|
+
for (const entry of fs5.readdirSync(root, { withFileTypes: true })) {
|
|
2279
|
+
const fullPath = path5.join(root, entry.name);
|
|
1821
2280
|
if (entry.isDirectory()) {
|
|
1822
2281
|
walk(fullPath, onFile);
|
|
1823
2282
|
} else if (entry.isFile()) {
|
|
@@ -1826,9 +2285,12 @@ function walk(root, onFile) {
|
|
|
1826
2285
|
}
|
|
1827
2286
|
}
|
|
1828
2287
|
function readSessionBreakdown(filePath) {
|
|
2288
|
+
const cacheKey = sessionBreakdownCacheKey(filePath);
|
|
2289
|
+
const cached = cacheKey ? readJsonCache("codex-session", cacheKey) : void 0;
|
|
2290
|
+
if (cached) return cached;
|
|
1829
2291
|
const breakdown = emptyBreakdown();
|
|
1830
2292
|
try {
|
|
1831
|
-
const text =
|
|
2293
|
+
const text = fs5.readFileSync(filePath, "utf8");
|
|
1832
2294
|
const parse = parseSessionRecords(text);
|
|
1833
2295
|
const records = parse.records;
|
|
1834
2296
|
breakdown.rawEventCount = records.length;
|
|
@@ -1860,8 +2322,22 @@ function readSessionBreakdown(filePath) {
|
|
|
1860
2322
|
breakdown.parseErrors.push(message);
|
|
1861
2323
|
breakdown.warnings.push(message);
|
|
1862
2324
|
}
|
|
2325
|
+
if (cacheKey) writeJsonCache("codex-session", cacheKey, breakdown);
|
|
1863
2326
|
return breakdown;
|
|
1864
2327
|
}
|
|
2328
|
+
function sessionBreakdownCacheKey(filePath) {
|
|
2329
|
+
try {
|
|
2330
|
+
const stat = fs5.statSync(filePath);
|
|
2331
|
+
return {
|
|
2332
|
+
version: codexSessionCacheVersion,
|
|
2333
|
+
filePath: path5.resolve(filePath),
|
|
2334
|
+
size: stat.size,
|
|
2335
|
+
mtimeMs: stat.mtimeMs
|
|
2336
|
+
};
|
|
2337
|
+
} catch {
|
|
2338
|
+
return void 0;
|
|
2339
|
+
}
|
|
2340
|
+
}
|
|
1865
2341
|
function parseSessionRecords(text) {
|
|
1866
2342
|
const trimmed = text.trim();
|
|
1867
2343
|
if (!trimmed) return { records: [], errors: [] };
|
|
@@ -1966,7 +2442,7 @@ function emptyBreakdown() {
|
|
|
1966
2442
|
};
|
|
1967
2443
|
}
|
|
1968
2444
|
function fallbackCwdFromPath(filePath) {
|
|
1969
|
-
return filePath ?
|
|
2445
|
+
return filePath ? path5.dirname(filePath) : process.cwd();
|
|
1970
2446
|
}
|
|
1971
2447
|
function applySessionMetadata(breakdown, record) {
|
|
1972
2448
|
if (!record || typeof record !== "object") return;
|
|
@@ -2146,6 +2622,12 @@ function dateValue2(value) {
|
|
|
2146
2622
|
}
|
|
2147
2623
|
return void 0;
|
|
2148
2624
|
}
|
|
2625
|
+
function timestampMs(value) {
|
|
2626
|
+
const date = dateValue2(value);
|
|
2627
|
+
if (!date) return void 0;
|
|
2628
|
+
const timestamp3 = new Date(date).getTime();
|
|
2629
|
+
return Number.isFinite(timestamp3) ? timestamp3 : void 0;
|
|
2630
|
+
}
|
|
2149
2631
|
function errorMessage(error) {
|
|
2150
2632
|
const message = error instanceof Error ? error.message : String(error);
|
|
2151
2633
|
if (message.includes("NODE_MODULE_VERSION") && message.includes("better_sqlite3.node")) {
|
|
@@ -2157,24 +2639,25 @@ function errorMessage(error) {
|
|
|
2157
2639
|
}
|
|
2158
2640
|
|
|
2159
2641
|
// packages/core/src/sources/claude.ts
|
|
2160
|
-
import
|
|
2642
|
+
import fs6 from "node:fs";
|
|
2161
2643
|
import os3 from "node:os";
|
|
2162
|
-
import
|
|
2644
|
+
import path6 from "node:path";
|
|
2163
2645
|
function scanClaude(options) {
|
|
2164
2646
|
const claudeHome = options.claudeHome ?? defaultClaudeHome();
|
|
2165
|
-
const historyPath =
|
|
2647
|
+
const historyPath = path6.join(claudeHome, "history.jsonl");
|
|
2166
2648
|
const warnings = [];
|
|
2167
2649
|
const discovery = discoverClaudeSessionFiles(claudeHome, warnings, Boolean(options.claudeHome));
|
|
2168
|
-
const historyExists =
|
|
2169
|
-
const orderedFiles = discovery.files.map((file) => ({ file, mtimeMs: safeMtimeMs(file.filePath) })).sort((a, b) => a.mtimeMs - b.mtimeMs);
|
|
2650
|
+
const historyExists = fs6.existsSync(historyPath);
|
|
2651
|
+
const orderedFiles = discovery.files.map((file) => ({ file, mtimeMs: safeMtimeMs(file.filePath) })).filter(({ mtimeMs }) => !options.scanWindow?.fromMs || mtimeMs >= options.scanWindow.fromMs).sort((a, b) => a.mtimeMs - b.mtimeMs);
|
|
2170
2652
|
const usageSelection = selectClaudeUsageOccurrences(orderedFiles.map(({ file }) => file));
|
|
2171
2653
|
const sessions = orderedFiles.flatMap(({ file }, index) => claudeFileToUsages(file, index, options, usageSelection));
|
|
2654
|
+
const serviceTierSummary = summarizeServiceTiers(sessions.map((session) => session.serviceTier), "Claude session usage");
|
|
2172
2655
|
const historyEntryCount = countHistoryEntries(historyPath);
|
|
2173
2656
|
const parseFailureCount = sessions.filter((session) => session.parseStatus === "failed" || (session.parseErrors?.length ?? 0) > 0).length;
|
|
2174
2657
|
const unreadableFileCount = sessions.filter((session) => session.parseErrors?.some((error) => error.startsWith("unable_to_read_session_file:"))).length;
|
|
2175
2658
|
const malformedFileCount = sessions.filter((session) => session.parseErrors?.some((error) => error.startsWith("jsonl_line_"))).length;
|
|
2176
|
-
const projectsPath =
|
|
2177
|
-
if (!discovery.projectsPaths.some((candidate) =>
|
|
2659
|
+
const projectsPath = path6.join(claudeHome, "projects");
|
|
2660
|
+
if (!discovery.projectsPaths.some((candidate) => fs6.existsSync(candidate))) {
|
|
2178
2661
|
warnings.push(`Claude Code projects directory not found at ${projectsPath}`);
|
|
2179
2662
|
}
|
|
2180
2663
|
return {
|
|
@@ -2183,7 +2666,8 @@ function scanClaude(options) {
|
|
|
2183
2666
|
label: "Claude Code",
|
|
2184
2667
|
available: discovery.files.length > 0 || historyExists,
|
|
2185
2668
|
paths: unique3([...discovery.projectsPaths, historyPath, ...discovery.desktopSessionRoots]),
|
|
2186
|
-
warnings
|
|
2669
|
+
warnings,
|
|
2670
|
+
...serviceTierSummary
|
|
2187
2671
|
},
|
|
2188
2672
|
sessions,
|
|
2189
2673
|
stats: {
|
|
@@ -2193,7 +2677,7 @@ function scanClaude(options) {
|
|
|
2193
2677
|
claudeHome,
|
|
2194
2678
|
projectsPath,
|
|
2195
2679
|
historyPath,
|
|
2196
|
-
projectsExists: discovery.projectsPaths.some((candidate) =>
|
|
2680
|
+
projectsExists: discovery.projectsPaths.some((candidate) => fs6.existsSync(candidate)),
|
|
2197
2681
|
historyExists,
|
|
2198
2682
|
sessionFileCount: discovery.files.length,
|
|
2199
2683
|
projectDirCount: discovery.projectDirs.size,
|
|
@@ -2202,6 +2686,7 @@ function scanClaude(options) {
|
|
|
2202
2686
|
unreadableFileCount,
|
|
2203
2687
|
malformedFileCount,
|
|
2204
2688
|
historyEntryCount,
|
|
2689
|
+
...serviceTierSummary,
|
|
2205
2690
|
lastScannedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
2206
2691
|
}
|
|
2207
2692
|
};
|
|
@@ -2209,7 +2694,7 @@ function scanClaude(options) {
|
|
|
2209
2694
|
function discoverClaudeSessionFiles(claudeHome, warnings, explicitHome) {
|
|
2210
2695
|
const files = [];
|
|
2211
2696
|
const projectDirs = /* @__PURE__ */ new Set();
|
|
2212
|
-
const projectsPaths = explicitHome ? [
|
|
2697
|
+
const projectsPaths = explicitHome ? [path6.join(claudeHome, "projects")] : unique3([...configuredClaudeHomes(claudeHome).map((root) => path6.join(root, "projects")), path6.join(os3.homedir(), ".config", "claude", "projects")]);
|
|
2213
2698
|
for (const projectsPath of projectsPaths) {
|
|
2214
2699
|
collectProjectFiles(projectsPath, files, projectDirs, warnings);
|
|
2215
2700
|
}
|
|
@@ -2226,16 +2711,16 @@ function discoverClaudeSessionFiles(claudeHome, warnings, explicitHome) {
|
|
|
2226
2711
|
}
|
|
2227
2712
|
function configuredClaudeHomes(primaryHome) {
|
|
2228
2713
|
if (process.env.CLAUDE_CONFIG_DIR) {
|
|
2229
|
-
return process.env.CLAUDE_CONFIG_DIR.split(",").map((item) => item.trim()).filter(Boolean).map((item) =>
|
|
2714
|
+
return process.env.CLAUDE_CONFIG_DIR.split(",").map((item) => item.trim()).filter(Boolean).map((item) => path6.resolve(item));
|
|
2230
2715
|
}
|
|
2231
2716
|
return [primaryHome];
|
|
2232
2717
|
}
|
|
2233
2718
|
function collectProjectFiles(projectsPath, files, projectDirs, warnings) {
|
|
2234
|
-
if (!
|
|
2719
|
+
if (!fs6.existsSync(projectsPath)) return;
|
|
2235
2720
|
try {
|
|
2236
|
-
for (const entry of
|
|
2721
|
+
for (const entry of fs6.readdirSync(projectsPath, { withFileTypes: true })) {
|
|
2237
2722
|
if (!entry.isDirectory()) continue;
|
|
2238
|
-
const projectDir =
|
|
2723
|
+
const projectDir = path6.join(projectsPath, entry.name);
|
|
2239
2724
|
projectDirs.add(projectDir);
|
|
2240
2725
|
walk2(projectDir, 5, (filePath) => {
|
|
2241
2726
|
if (filePath.endsWith(".jsonl")) files.push({ filePath, projectDir, projectsPath });
|
|
@@ -2246,15 +2731,15 @@ function collectProjectFiles(projectsPath, files, projectDirs, warnings) {
|
|
|
2246
2731
|
}
|
|
2247
2732
|
}
|
|
2248
2733
|
function collectDesktopSessionFiles(root, files, projectDirs, warnings) {
|
|
2249
|
-
if (!
|
|
2734
|
+
if (!fs6.existsSync(root)) return;
|
|
2250
2735
|
try {
|
|
2251
2736
|
walk2(root, 8, (filePath) => {
|
|
2252
2737
|
if (!filePath.endsWith(".jsonl")) return;
|
|
2253
|
-
const parts = filePath.split(
|
|
2738
|
+
const parts = filePath.split(path6.sep);
|
|
2254
2739
|
const projectsIndex = parts.lastIndexOf("projects");
|
|
2255
2740
|
const hasProjectSegment = projectsIndex >= 0 && projectsIndex < parts.length - 2;
|
|
2256
|
-
const projectsPath = hasProjectSegment ? parts.slice(0, projectsIndex + 1).join(
|
|
2257
|
-
const projectDir = hasProjectSegment ? parts.slice(0, projectsIndex + 2).join(
|
|
2741
|
+
const projectsPath = hasProjectSegment ? parts.slice(0, projectsIndex + 1).join(path6.sep) || path6.sep : root;
|
|
2742
|
+
const projectDir = hasProjectSegment ? parts.slice(0, projectsIndex + 2).join(path6.sep) || path6.sep : path6.dirname(filePath);
|
|
2258
2743
|
projectDirs.add(projectDir);
|
|
2259
2744
|
files.push({ filePath, projectDir, projectsPath });
|
|
2260
2745
|
});
|
|
@@ -2264,9 +2749,9 @@ function collectDesktopSessionFiles(root, files, projectDirs, warnings) {
|
|
|
2264
2749
|
}
|
|
2265
2750
|
function walk2(root, maxDepth, onFile, depth = 0) {
|
|
2266
2751
|
if (depth > maxDepth) return;
|
|
2267
|
-
for (const entry of
|
|
2752
|
+
for (const entry of fs6.readdirSync(root, { withFileTypes: true })) {
|
|
2268
2753
|
if (entry.name === "node_modules" || entry.name === ".git") continue;
|
|
2269
|
-
const fullPath =
|
|
2754
|
+
const fullPath = path6.join(root, entry.name);
|
|
2270
2755
|
if (entry.isDirectory()) {
|
|
2271
2756
|
walk2(fullPath, maxDepth, onFile, depth + 1);
|
|
2272
2757
|
} else if (entry.isFile()) {
|
|
@@ -2280,14 +2765,17 @@ function claudeFileToUsages(file, index, options, usageSelection) {
|
|
|
2280
2765
|
return segments.map(({ breakdown: segment, suffix }) => claudeBreakdownToUsage(file, index, options, segment, suffix));
|
|
2281
2766
|
}
|
|
2282
2767
|
function claudeBreakdownToUsage(file, index, options, breakdown, idSuffix = "") {
|
|
2283
|
-
const id = breakdown.id ?? (
|
|
2768
|
+
const id = breakdown.id ?? (path6.basename(file.filePath).replace(/\.jsonl$/i, "") || `claude-session-${index}`);
|
|
2284
2769
|
const fallbackCwd = inferProjectPath(file.projectDir, file.projectsPath);
|
|
2285
2770
|
const cwd = breakdown.cwd ?? fallbackCwd;
|
|
2286
2771
|
const repo = resolveRepoInfo(cwd, options.config);
|
|
2287
2772
|
const warnings = [...repo.warnings, ...breakdown.warnings];
|
|
2288
2773
|
const entrypoint = breakdown.entrypoint ?? inferClaudeEntrypointFromPath(file.filePath);
|
|
2289
2774
|
if (!breakdown.cwd) warnings.push("repo_inferred_from_claude_project_dir");
|
|
2775
|
+
if (!breakdown.model && breakdown.usageCount === 0 && breakdown.syntheticZeroUsageCount > 0) warnings.push("claude_synthetic_zero_usage");
|
|
2290
2776
|
const hasTokenBreakdown = breakdown.totalTokens > 0 && breakdown.usageCount > 0;
|
|
2777
|
+
const serviceTierSummary = summarizeClaudeServiceTier(breakdown.serviceTiers, breakdown.serviceSpeeds);
|
|
2778
|
+
const serviceSpeedSummary = summarizeServiceTiers(breakdown.serviceSpeeds, "Claude usage.speed");
|
|
2291
2779
|
const usage = {
|
|
2292
2780
|
id: `${id}${idSuffix}`,
|
|
2293
2781
|
sourceClient: "claude",
|
|
@@ -2325,6 +2813,18 @@ function claudeBreakdownToUsage(file, index, options, breakdown, idSuffix = "")
|
|
|
2325
2813
|
detectedSurface: claudeSurface(entrypoint),
|
|
2326
2814
|
surfaceConfidence: "medium",
|
|
2327
2815
|
surfaceReason: entrypoint ? `Claude Code entrypoint: ${entrypoint}` : "Claude Code transcript",
|
|
2816
|
+
serviceTier: serviceTierSummary.serviceTier,
|
|
2817
|
+
serviceTierSource: serviceTierSummary.serviceTier ? "session_usage" : void 0,
|
|
2818
|
+
serviceTierConfidence: serviceTierSummary.serviceTier ? "high" : void 0,
|
|
2819
|
+
serviceTierDetail: serviceTierSummary.serviceTierDetail,
|
|
2820
|
+
sourceMetadata: breakdown.serviceTiers.length || breakdown.serviceSpeeds.length ? {
|
|
2821
|
+
claude: {
|
|
2822
|
+
serviceTiers: breakdown.serviceTiers,
|
|
2823
|
+
speeds: breakdown.serviceSpeeds,
|
|
2824
|
+
serviceTier: serviceTierSummary.serviceTier,
|
|
2825
|
+
speed: serviceSpeedSummary.serviceTier
|
|
2826
|
+
}
|
|
2827
|
+
} : void 0,
|
|
2328
2828
|
userPromptCount: breakdown.userPromptCount,
|
|
2329
2829
|
assistantMessageCount: breakdown.assistantMessageCount,
|
|
2330
2830
|
toolCallCount: breakdown.toolCallCount,
|
|
@@ -2352,12 +2852,12 @@ function claudeBreakdownToUsage(file, index, options, breakdown, idSuffix = "")
|
|
|
2352
2852
|
};
|
|
2353
2853
|
}
|
|
2354
2854
|
function inferClaudeEntrypointFromPath(filePath) {
|
|
2355
|
-
return filePath.split(
|
|
2855
|
+
return filePath.split(path6.sep).includes("local-agent-mode-sessions") ? "local-agent" : void 0;
|
|
2356
2856
|
}
|
|
2357
2857
|
function readClaudeBreakdown(filePath, usageSelection) {
|
|
2358
2858
|
const breakdown = emptyBreakdown2();
|
|
2359
2859
|
try {
|
|
2360
|
-
const text =
|
|
2860
|
+
const text = fs6.readFileSync(filePath, "utf8");
|
|
2361
2861
|
const parse = parseJsonLines2(text);
|
|
2362
2862
|
breakdown.rawEventCount = parse.records.length;
|
|
2363
2863
|
breakdown.parseErrors = parse.errors;
|
|
@@ -2395,6 +2895,8 @@ function applyClaudeRecord(breakdown, record, usageSelection, usageOccurrenceKey
|
|
|
2395
2895
|
applyPromptTimeline2(breakdown, role, message.content, timestamp3);
|
|
2396
2896
|
const usage = firstObject4(message.usage);
|
|
2397
2897
|
const usageTokens = usage ? usageTotal(usage) : 0;
|
|
2898
|
+
const rawModel = stringValue3(message.model);
|
|
2899
|
+
if (rawModel === "<synthetic>" && usage && usageTokens === 0) breakdown.syntheticZeroUsageCount += 1;
|
|
2398
2900
|
const model = normalizeClaudeModel(message.model);
|
|
2399
2901
|
if (model && (usageTokens > 0 || !breakdown.model)) breakdown.model = model;
|
|
2400
2902
|
const selectedUsage = usageTokens > 0 ? usageSelection.usageByOccurrence.get(usageOccurrenceKey) : void 0;
|
|
@@ -2412,7 +2914,7 @@ function selectClaudeUsageOccurrences(files) {
|
|
|
2412
2914
|
for (const file of files) {
|
|
2413
2915
|
let parse;
|
|
2414
2916
|
try {
|
|
2415
|
-
parse = parseJsonLines2(
|
|
2917
|
+
parse = parseJsonLines2(fs6.readFileSync(file.filePath, "utf8"));
|
|
2416
2918
|
} catch {
|
|
2417
2919
|
continue;
|
|
2418
2920
|
}
|
|
@@ -2446,14 +2948,16 @@ function usageDedupKey(record, message) {
|
|
|
2446
2948
|
return `${requestId}:${messageId}`;
|
|
2447
2949
|
}
|
|
2448
2950
|
function occurrenceKey(filePath, recordIndex) {
|
|
2449
|
-
return `${
|
|
2951
|
+
return `${path6.resolve(filePath)}:${recordIndex}`;
|
|
2450
2952
|
}
|
|
2451
2953
|
function cloneUsage(usage) {
|
|
2452
2954
|
return {
|
|
2453
2955
|
input_tokens: numberValue(usage.input_tokens),
|
|
2454
2956
|
cache_read_input_tokens: numberValue(usage.cache_read_input_tokens),
|
|
2455
2957
|
cache_creation_input_tokens: numberValue(usage.cache_creation_input_tokens),
|
|
2456
|
-
output_tokens: numberValue(usage.output_tokens)
|
|
2958
|
+
output_tokens: numberValue(usage.output_tokens),
|
|
2959
|
+
service_tier: stringValue3(usage.service_tier),
|
|
2960
|
+
speed: stringValue3(usage.speed)
|
|
2457
2961
|
};
|
|
2458
2962
|
}
|
|
2459
2963
|
function mergeUsageMax(target, usage) {
|
|
@@ -2461,6 +2965,8 @@ function mergeUsageMax(target, usage) {
|
|
|
2461
2965
|
target.cache_read_input_tokens = Math.max(numberValue(target.cache_read_input_tokens), numberValue(usage.cache_read_input_tokens));
|
|
2462
2966
|
target.cache_creation_input_tokens = Math.max(numberValue(target.cache_creation_input_tokens), numberValue(usage.cache_creation_input_tokens));
|
|
2463
2967
|
target.output_tokens = Math.max(numberValue(target.output_tokens), numberValue(usage.output_tokens));
|
|
2968
|
+
target.service_tier ??= stringValue3(usage.service_tier);
|
|
2969
|
+
target.speed ??= stringValue3(usage.speed);
|
|
2464
2970
|
}
|
|
2465
2971
|
function applyUsage(breakdown, usage) {
|
|
2466
2972
|
const input = numberValue(usage.input_tokens);
|
|
@@ -2472,6 +2978,8 @@ function applyUsage(breakdown, usage) {
|
|
|
2472
2978
|
breakdown.cacheCreationInputTokens += cacheCreation;
|
|
2473
2979
|
breakdown.outputTokens += output;
|
|
2474
2980
|
breakdown.usageCount += 1;
|
|
2981
|
+
pushUnique(breakdown.serviceTiers, normalizedServiceTier(usage.service_tier));
|
|
2982
|
+
pushUnique(breakdown.serviceSpeeds, normalizedServiceTier(usage.speed));
|
|
2475
2983
|
}
|
|
2476
2984
|
function segmentClaudeBreakdownByDayAndModel(breakdown) {
|
|
2477
2985
|
const events = breakdown.usageEvents.filter((event) => event.timestamp);
|
|
@@ -2581,9 +3089,46 @@ function emptyBreakdown2() {
|
|
|
2581
3089
|
parseErrors: [],
|
|
2582
3090
|
promptTimeline: [],
|
|
2583
3091
|
warnings: [],
|
|
2584
|
-
usageEvents: []
|
|
3092
|
+
usageEvents: [],
|
|
3093
|
+
serviceTiers: [],
|
|
3094
|
+
serviceSpeeds: [],
|
|
3095
|
+
syntheticZeroUsageCount: 0
|
|
3096
|
+
};
|
|
3097
|
+
}
|
|
3098
|
+
function summarizeClaudeServiceTier(serviceTiers, speeds) {
|
|
3099
|
+
const tierSummary = summarizeServiceTiers(serviceTiers, "Claude usage.service_tier");
|
|
3100
|
+
return tierSummary.serviceTier ? tierSummary : summarizeServiceTiers(speeds, "Claude usage.speed");
|
|
3101
|
+
}
|
|
3102
|
+
function summarizeServiceTiers(values, source) {
|
|
3103
|
+
const uniqueValues = unique3(values.filter((value) => Boolean(value)));
|
|
3104
|
+
if (uniqueValues.length === 0) return {};
|
|
3105
|
+
if (uniqueValues.length === 1) {
|
|
3106
|
+
const serviceTier = uniqueValues[0];
|
|
3107
|
+
if (!serviceTier) return {};
|
|
3108
|
+
return {
|
|
3109
|
+
serviceTier,
|
|
3110
|
+
serviceTierSource: "session_usage",
|
|
3111
|
+
serviceTierConfidence: "high",
|
|
3112
|
+
serviceTierDetail: serviceTierDetail(source, uniqueValues)
|
|
3113
|
+
};
|
|
3114
|
+
}
|
|
3115
|
+
return {
|
|
3116
|
+
serviceTier: "mixed",
|
|
3117
|
+
serviceTierSource: "session_usage",
|
|
3118
|
+
serviceTierConfidence: "high",
|
|
3119
|
+
serviceTierDetail: serviceTierDetail(source, uniqueValues)
|
|
2585
3120
|
};
|
|
2586
3121
|
}
|
|
3122
|
+
function serviceTierDetail(source, values) {
|
|
3123
|
+
return `${source}: ${unique3(values).join(", ")}`;
|
|
3124
|
+
}
|
|
3125
|
+
function normalizedServiceTier(value) {
|
|
3126
|
+
const tier = stringValue3(value)?.trim().toLowerCase();
|
|
3127
|
+
return tier || void 0;
|
|
3128
|
+
}
|
|
3129
|
+
function pushUnique(target, value) {
|
|
3130
|
+
if (value && !target.includes(value)) target.push(value);
|
|
3131
|
+
}
|
|
2587
3132
|
function applyPromptTimeline2(breakdown, role, content, timestamp3) {
|
|
2588
3133
|
if (breakdown.promptTimeline.length >= 80 || role !== "user" && role !== "assistant") return;
|
|
2589
3134
|
const text = extractClaudeMessageText(content);
|
|
@@ -2610,12 +3155,12 @@ function normalizeTimelineText2(text) {
|
|
|
2610
3155
|
return text.replace(/\s+/g, " ").trim().slice(0, 1e3);
|
|
2611
3156
|
}
|
|
2612
3157
|
function inferProjectPath(projectDir, projectsPath) {
|
|
2613
|
-
const name =
|
|
3158
|
+
const name = path6.basename(projectDir);
|
|
2614
3159
|
if (name.startsWith("-")) {
|
|
2615
|
-
return `${
|
|
3160
|
+
return `${path6.sep}${name.slice(1).split("-").filter(Boolean).join(path6.sep)}`;
|
|
2616
3161
|
}
|
|
2617
|
-
const relative =
|
|
2618
|
-
return relative && !relative.startsWith("..") ?
|
|
3162
|
+
const relative = path6.relative(projectsPath, projectDir);
|
|
3163
|
+
return relative && !relative.startsWith("..") ? path6.join(projectsPath, relative) : projectDir;
|
|
2619
3164
|
}
|
|
2620
3165
|
function inferOutcome2(breakdown) {
|
|
2621
3166
|
if (breakdown.parseStatus === "failed") return "failed";
|
|
@@ -2639,20 +3184,20 @@ function claudeSourceAppLabel(entrypoint) {
|
|
|
2639
3184
|
return "Claude Code";
|
|
2640
3185
|
}
|
|
2641
3186
|
function defaultClaudeHome() {
|
|
2642
|
-
return process.env.CLAUDE_CONFIG_DIR?.split(",").map((item) => item.trim()).filter(Boolean)[0] ??
|
|
3187
|
+
return process.env.CLAUDE_CONFIG_DIR?.split(",").map((item) => item.trim()).filter(Boolean)[0] ?? path6.join(os3.homedir(), ".claude");
|
|
2643
3188
|
}
|
|
2644
3189
|
function defaultDesktopSessionRoots() {
|
|
2645
3190
|
const home = os3.homedir();
|
|
2646
|
-
const xdg = process.env.XDG_CONFIG_HOME ||
|
|
3191
|
+
const xdg = process.env.XDG_CONFIG_HOME || path6.join(home, ".config");
|
|
2647
3192
|
return [
|
|
2648
|
-
|
|
2649
|
-
|
|
3193
|
+
path6.join(home, "Library", "Application Support", "Claude", "local-agent-mode-sessions"),
|
|
3194
|
+
path6.join(xdg, "Claude", "local-agent-mode-sessions")
|
|
2650
3195
|
];
|
|
2651
3196
|
}
|
|
2652
3197
|
function uniqueSessionFiles(files) {
|
|
2653
3198
|
const seen = /* @__PURE__ */ new Set();
|
|
2654
3199
|
return files.filter((file) => {
|
|
2655
|
-
const key =
|
|
3200
|
+
const key = path6.resolve(file.filePath);
|
|
2656
3201
|
if (seen.has(key)) return false;
|
|
2657
3202
|
seen.add(key);
|
|
2658
3203
|
return true;
|
|
@@ -2662,10 +3207,10 @@ function unique3(values) {
|
|
|
2662
3207
|
return [...new Set(values)];
|
|
2663
3208
|
}
|
|
2664
3209
|
function countHistoryEntries(historyPath) {
|
|
2665
|
-
if (!
|
|
3210
|
+
if (!fs6.existsSync(historyPath)) return 0;
|
|
2666
3211
|
let entryCount = 0;
|
|
2667
3212
|
try {
|
|
2668
|
-
for (const line of
|
|
3213
|
+
for (const line of fs6.readFileSync(historyPath, "utf8").split(/\r?\n/).filter(Boolean)) {
|
|
2669
3214
|
try {
|
|
2670
3215
|
JSON.parse(line);
|
|
2671
3216
|
entryCount += 1;
|
|
@@ -2725,62 +3270,65 @@ function errorMessage2(error) {
|
|
|
2725
3270
|
}
|
|
2726
3271
|
function safeMtimeMs(filePath) {
|
|
2727
3272
|
try {
|
|
2728
|
-
return
|
|
3273
|
+
return fs6.statSync(filePath).mtimeMs;
|
|
2729
3274
|
} catch {
|
|
2730
3275
|
return 0;
|
|
2731
3276
|
}
|
|
2732
3277
|
}
|
|
2733
3278
|
|
|
2734
|
-
// packages/core/src/sources/
|
|
2735
|
-
import
|
|
3279
|
+
// packages/core/src/sources/copilot.ts
|
|
3280
|
+
import fs7 from "node:fs";
|
|
2736
3281
|
import os4 from "node:os";
|
|
2737
|
-
import
|
|
2738
|
-
|
|
2739
|
-
var
|
|
2740
|
-
|
|
2741
|
-
|
|
2742
|
-
|
|
2743
|
-
|
|
2744
|
-
|
|
2745
|
-
const
|
|
2746
|
-
const
|
|
2747
|
-
|
|
2748
|
-
|
|
2749
|
-
|
|
2750
|
-
|
|
2751
|
-
|
|
2752
|
-
const unreadableFileCount = sessions.filter((session) => session.parseErrors?.some((error) => error.startsWith("unable_to_read_"))).length;
|
|
2753
|
-
const malformedFileCount = sessions.filter((session) => session.parseErrors?.some((error) => error.startsWith("jsonl_line_") || error.startsWith("sqlite_json_parse:"))).length;
|
|
2754
|
-
if (!fs6.existsSync(cursorHome)) {
|
|
2755
|
-
discovery.warnings.push(`Cursor home directory not found at ${cursorHome}`);
|
|
2756
|
-
}
|
|
2757
|
-
if (!fs6.existsSync(discovery.statePath)) {
|
|
2758
|
-
discovery.warnings.push(`Cursor global state database not found at ${discovery.statePath}`);
|
|
3282
|
+
import path7 from "node:path";
|
|
3283
|
+
var maxJsonlBytes = 50 * 1024 * 1024;
|
|
3284
|
+
var copilotOtelEnv = "COPILOT_OTEL_FILE_EXPORTER_PATH";
|
|
3285
|
+
function scanCopilot(options) {
|
|
3286
|
+
const copilotHome = options.copilotHome ?? path7.join(os4.homedir(), ".copilot");
|
|
3287
|
+
const discovery = discoverCopilotFiles(copilotHome, Boolean(options.copilotHome), options.codeUserRoots);
|
|
3288
|
+
const files = discovery.files.filter((file) => fileMayOverlapScanWindow2(file.filePath, options.scanWindow));
|
|
3289
|
+
const sessionMap = /* @__PURE__ */ new Map();
|
|
3290
|
+
const fileStats = [];
|
|
3291
|
+
for (const [index, file] of files.entries()) {
|
|
3292
|
+
const read = readCopilotFile(file, index);
|
|
3293
|
+
fileStats.push(read.stats);
|
|
3294
|
+
for (const breakdown of read.breakdowns) {
|
|
3295
|
+
mergeBreakdown(sessionMap, breakdown);
|
|
3296
|
+
}
|
|
2759
3297
|
}
|
|
3298
|
+
applyModelHints(sessionMap, discovery.modelHints);
|
|
3299
|
+
const sessions = [...sessionMap.values()].sort((a, b) => (a.startedAt ?? "").localeCompare(b.startedAt ?? "")).map((breakdown, index) => breakdownToUsage(breakdown, index, options));
|
|
3300
|
+
const parseFailureCount = fileStats.filter((stats) => stats.parseFailed).length;
|
|
3301
|
+
const unreadableFileCount = fileStats.filter((stats) => stats.unreadable).length;
|
|
3302
|
+
const malformedFileCount = fileStats.filter((stats) => stats.malformed).length;
|
|
3303
|
+
if (!fs7.existsSync(copilotHome)) discovery.warnings.push(`GitHub Copilot home directory not found at ${copilotHome}`);
|
|
3304
|
+
if (!fs7.existsSync(discovery.otelPath)) discovery.warnings.push(`GitHub Copilot OpenTelemetry directory not found at ${discovery.otelPath}`);
|
|
3305
|
+
const otelFiles = files.filter((file) => file.kind === "otel").length;
|
|
3306
|
+
const debugLogFiles = files.filter((file) => file.kind === "debug").length;
|
|
3307
|
+
const transcriptFiles = files.filter((file) => file.kind === "transcript").length;
|
|
3308
|
+
const sessionStateFiles = files.filter((file) => file.kind === "session_state").length;
|
|
2760
3309
|
return {
|
|
2761
3310
|
source: {
|
|
2762
|
-
id: "
|
|
2763
|
-
label: "
|
|
2764
|
-
available:
|
|
2765
|
-
paths: unique4([
|
|
3311
|
+
id: "copilot",
|
|
3312
|
+
label: "GitHub Copilot",
|
|
3313
|
+
available: files.length > 0,
|
|
3314
|
+
paths: unique4([copilotHome, discovery.otelPath, discovery.workspaceStoragePath, ...discovery.codeUserRoots, ...files.map((file) => file.filePath), ...[...discovery.modelHints.values()].map((hint) => hint.sourcePath)]),
|
|
2766
3315
|
warnings: discovery.warnings
|
|
2767
3316
|
},
|
|
2768
3317
|
sessions,
|
|
2769
3318
|
stats: {
|
|
2770
|
-
sourceId: "
|
|
2771
|
-
sourceLabel: "
|
|
2772
|
-
homePath:
|
|
2773
|
-
|
|
2774
|
-
|
|
3319
|
+
sourceId: "copilot",
|
|
3320
|
+
sourceLabel: "GitHub Copilot",
|
|
3321
|
+
homePath: copilotHome,
|
|
3322
|
+
copilotHome,
|
|
3323
|
+
otelPath: discovery.otelPath,
|
|
2775
3324
|
workspaceStoragePath: discovery.workspaceStoragePath,
|
|
2776
|
-
|
|
2777
|
-
|
|
2778
|
-
|
|
2779
|
-
|
|
2780
|
-
|
|
2781
|
-
|
|
2782
|
-
|
|
2783
|
-
skippedFileCount: discovery.skippedFileCount,
|
|
3325
|
+
otelExists: fs7.existsSync(discovery.otelPath),
|
|
3326
|
+
sessionsExists: files.length > 0,
|
|
3327
|
+
sessionFileCount: files.length,
|
|
3328
|
+
otelFileCount: otelFiles,
|
|
3329
|
+
debugLogFileCount: debugLogFiles,
|
|
3330
|
+
transcriptFileCount: transcriptFiles,
|
|
3331
|
+
sessionStateFileCount: sessionStateFiles,
|
|
2784
3332
|
sessionsImported: sessions.length,
|
|
2785
3333
|
parseFailureCount,
|
|
2786
3334
|
unreadableFileCount,
|
|
@@ -2789,141 +3337,966 @@ function scanCursor(options) {
|
|
|
2789
3337
|
}
|
|
2790
3338
|
};
|
|
2791
3339
|
}
|
|
2792
|
-
function
|
|
3340
|
+
function discoverCopilotFiles(copilotHome, explicitHome, explicitCodeUserRoots) {
|
|
2793
3341
|
const warnings = [];
|
|
2794
|
-
const
|
|
2795
|
-
const
|
|
2796
|
-
const
|
|
2797
|
-
const
|
|
2798
|
-
|
|
2799
|
-
|
|
2800
|
-
|
|
2801
|
-
|
|
2802
|
-
|
|
2803
|
-
|
|
2804
|
-
|
|
2805
|
-
|
|
2806
|
-
|
|
2807
|
-
const lower = filePath.toLowerCase();
|
|
2808
|
-
if (lower.endsWith(".jsonl")) {
|
|
2809
|
-
if (isReadableRegularFile(filePath, maxJsonlBytes)) jsonlFiles.push(filePath);
|
|
2810
|
-
else skippedFileCount += 1;
|
|
2811
|
-
return;
|
|
2812
|
-
}
|
|
2813
|
-
if (/\.(sqlite|db|vscdb)$/i.test(filePath)) {
|
|
2814
|
-
if (isReadableRegularFile(filePath, maxDatabaseBytes)) databaseFiles.push(filePath);
|
|
2815
|
-
else skippedFileCount += 1;
|
|
2816
|
-
}
|
|
2817
|
-
});
|
|
2818
|
-
} catch (error) {
|
|
2819
|
-
warnings.push(`Unable to inspect Cursor path ${root}: ${errorMessage3(error)}`);
|
|
2820
|
-
}
|
|
3342
|
+
const otelPath = path7.join(copilotHome, "otel");
|
|
3343
|
+
const codeUserRoots = explicitCodeUserRoots ?? (explicitHome ? [] : defaultCodeUserRoots());
|
|
3344
|
+
const workspaceStoragePath = codeUserRoots[0] ? path7.join(codeUserRoots[0], "workspaceStorage") : "";
|
|
3345
|
+
const files = [];
|
|
3346
|
+
const modelHints = /* @__PURE__ */ new Map();
|
|
3347
|
+
collectJsonlFiles(otelPath, "otel", "cli", void 0, files, warnings);
|
|
3348
|
+
collectCopilotSessionStateFiles(copilotHome, files, warnings);
|
|
3349
|
+
const explicitOtelPath = process.env[copilotOtelEnv]?.trim();
|
|
3350
|
+
if (explicitOtelPath && isReadableRegularFile(explicitOtelPath, maxJsonlBytes)) {
|
|
3351
|
+
files.push({ filePath: path7.resolve(explicitOtelPath), kind: "otel", surface: "cli", fallbackCwd: void 0 });
|
|
3352
|
+
}
|
|
3353
|
+
for (const userRoot of codeUserRoots) {
|
|
3354
|
+
collectCodeCopilotFiles(userRoot, files, modelHints, warnings);
|
|
2821
3355
|
}
|
|
2822
3356
|
return {
|
|
2823
|
-
|
|
2824
|
-
|
|
2825
|
-
|
|
2826
|
-
|
|
2827
|
-
|
|
2828
|
-
jsonlFiles: unique4(jsonlFiles).sort(cursorJsonlSort),
|
|
2829
|
-
databaseFiles: unique4(databaseFiles).sort(),
|
|
2830
|
-
skippedFileCount,
|
|
3357
|
+
otelPath,
|
|
3358
|
+
workspaceStoragePath,
|
|
3359
|
+
files: uniqueFiles(files),
|
|
3360
|
+
codeUserRoots,
|
|
3361
|
+
modelHints,
|
|
2831
3362
|
warnings
|
|
2832
3363
|
};
|
|
2833
3364
|
}
|
|
2834
|
-
function
|
|
2835
|
-
|
|
3365
|
+
function collectCopilotSessionStateFiles(copilotHome, files, warnings) {
|
|
3366
|
+
const sessionStateRoot = path7.join(copilotHome, "session-state");
|
|
3367
|
+
if (!fs7.existsSync(sessionStateRoot)) return;
|
|
3368
|
+
try {
|
|
3369
|
+
for (const entry of fs7.readdirSync(sessionStateRoot, { withFileTypes: true })) {
|
|
3370
|
+
if (!entry.isDirectory()) continue;
|
|
3371
|
+
const sessionDir = path7.join(sessionStateRoot, entry.name);
|
|
3372
|
+
const eventsPath = path7.join(sessionDir, "events.jsonl");
|
|
3373
|
+
if (isReadableRegularFile(eventsPath, maxJsonlBytes)) {
|
|
3374
|
+
files.push({ filePath: eventsPath, kind: "session_state", surface: "cli", fallbackCwd: cwdFromWorkspaceYaml(path7.join(sessionDir, "workspace.yaml")) });
|
|
3375
|
+
}
|
|
3376
|
+
}
|
|
3377
|
+
} catch (error) {
|
|
3378
|
+
warnings.push(`Unable to read GitHub Copilot session state at ${sessionStateRoot}: ${errorMessage3(error)}`);
|
|
3379
|
+
}
|
|
2836
3380
|
}
|
|
2837
|
-
function
|
|
2838
|
-
|
|
2839
|
-
|
|
3381
|
+
function collectCodeCopilotFiles(userRoot, files, modelHints, warnings) {
|
|
3382
|
+
const workspaceStorage = path7.join(userRoot, "workspaceStorage");
|
|
3383
|
+
if (!fs7.existsSync(workspaceStorage)) return;
|
|
2840
3384
|
try {
|
|
2841
|
-
for (const workspace of
|
|
3385
|
+
for (const workspace of fs7.readdirSync(workspaceStorage, { withFileTypes: true })) {
|
|
2842
3386
|
if (!workspace.isDirectory()) continue;
|
|
2843
|
-
|
|
3387
|
+
const workspaceRoot = path7.join(workspaceStorage, workspace.name);
|
|
3388
|
+
const copilotRoot = path7.join(workspaceRoot, "GitHub.copilot-chat");
|
|
3389
|
+
if (!fs7.existsSync(copilotRoot)) continue;
|
|
3390
|
+
const fallbackCwd = workspaceFolderFromStorageJson(path7.join(workspaceRoot, "workspace.json"));
|
|
3391
|
+
collectJsonlFiles(path7.join(copilotRoot, "debug-logs"), "debug", "vscode", fallbackCwd, files, warnings);
|
|
3392
|
+
collectJsonlFiles(path7.join(copilotRoot, "transcripts"), "transcript", "vscode", fallbackCwd, files, warnings);
|
|
3393
|
+
collectCodeChatSessionModelHints(path7.join(workspaceRoot, "chatSessions"), modelHints, warnings);
|
|
2844
3394
|
}
|
|
2845
|
-
} catch {
|
|
2846
|
-
|
|
3395
|
+
} catch (error) {
|
|
3396
|
+
warnings.push(`Unable to read GitHub Copilot VS Code storage at ${workspaceStorage}: ${errorMessage3(error)}`);
|
|
2847
3397
|
}
|
|
2848
|
-
return roots;
|
|
2849
3398
|
}
|
|
2850
|
-
function
|
|
2851
|
-
if (!
|
|
3399
|
+
function collectCodeChatSessionModelHints(root, modelHints, warnings) {
|
|
3400
|
+
if (!fs7.existsSync(root)) return;
|
|
2852
3401
|
try {
|
|
2853
|
-
|
|
2854
|
-
|
|
3402
|
+
walk3(root, 2, (filePath) => {
|
|
3403
|
+
if (!filePath.endsWith(".jsonl") || !isReadableRegularFile(filePath, maxJsonlBytes)) return;
|
|
3404
|
+
const hint = readCodeChatSessionModelHint(filePath);
|
|
3405
|
+
if (hint) modelHints.set(hint.sessionId, { model: hint.model, sourcePath: filePath });
|
|
3406
|
+
});
|
|
3407
|
+
} catch (error) {
|
|
3408
|
+
warnings.push(`Unable to read VS Code chat session metadata at ${root}: ${errorMessage3(error)}`);
|
|
3409
|
+
}
|
|
3410
|
+
}
|
|
3411
|
+
function readCodeChatSessionModelHint(filePath) {
|
|
3412
|
+
const fallbackSessionId = path7.basename(filePath).replace(/\.jsonl$/i, "");
|
|
3413
|
+
let sessionId = fallbackSessionId;
|
|
3414
|
+
let model;
|
|
3415
|
+
try {
|
|
3416
|
+
for (const record of parseJsonLines3(fs7.readFileSync(filePath, "utf8")).records) {
|
|
3417
|
+
const body = firstObject5(record.v) ?? firstObject5(record.data) ?? record;
|
|
3418
|
+
const metadata = firstObject5(body.metadata);
|
|
3419
|
+
sessionId = stringValue4(body.sessionId) ?? stringValue4(metadata?.sessionId) ?? sessionId;
|
|
3420
|
+
model = chatSessionSelectedModel(body) ?? chatSessionResolvedModel(body) ?? model;
|
|
3421
|
+
}
|
|
3422
|
+
} catch {
|
|
3423
|
+
return void 0;
|
|
3424
|
+
}
|
|
3425
|
+
return sessionId && model ? { sessionId, model } : void 0;
|
|
3426
|
+
}
|
|
3427
|
+
function chatSessionSelectedModel(body) {
|
|
3428
|
+
const inputState = firstObject5(body.inputState);
|
|
3429
|
+
const selectedModel = firstObject5(inputState?.selectedModel) ?? firstObject5(body.selectedModel);
|
|
3430
|
+
const metadata = firstObject5(selectedModel?.metadata);
|
|
3431
|
+
return firstModelCandidate([
|
|
3432
|
+
stringValue4(metadata?.version),
|
|
3433
|
+
stringValue4(metadata?.family),
|
|
3434
|
+
stringValue4(selectedModel?.id),
|
|
3435
|
+
stringValue4(selectedModel?.identifier)
|
|
3436
|
+
]);
|
|
3437
|
+
}
|
|
3438
|
+
function chatSessionResolvedModel(body) {
|
|
3439
|
+
const metadata = firstObject5(body.metadata);
|
|
3440
|
+
const response = firstObject5(body.response);
|
|
3441
|
+
const responseMetadata = firstObject5(response?.metadata);
|
|
3442
|
+
return firstModelCandidate([
|
|
3443
|
+
stringValue4(metadata?.resolvedModel),
|
|
3444
|
+
stringValue4(responseMetadata?.resolvedModel)
|
|
3445
|
+
]);
|
|
3446
|
+
}
|
|
3447
|
+
function firstModelCandidate(values) {
|
|
3448
|
+
for (const value of values) {
|
|
3449
|
+
const normalized = value?.replace(/^github[-_]?copilot\//i, "").replace(/^copilot\//i, "").trim();
|
|
3450
|
+
if (normalized && normalized.toLowerCase() !== "auto") return normalized;
|
|
3451
|
+
}
|
|
3452
|
+
return void 0;
|
|
3453
|
+
}
|
|
3454
|
+
function collectJsonlFiles(root, kind, surface, fallbackCwd, files, warnings) {
|
|
3455
|
+
if (!fs7.existsSync(root)) return;
|
|
3456
|
+
try {
|
|
3457
|
+
walk3(root, 6, (filePath) => {
|
|
3458
|
+
if (filePath.endsWith(".jsonl") && isReadableRegularFile(filePath, maxJsonlBytes)) {
|
|
3459
|
+
files.push({ filePath, kind, surface, fallbackCwd });
|
|
3460
|
+
}
|
|
3461
|
+
});
|
|
3462
|
+
} catch (error) {
|
|
3463
|
+
warnings.push(`Unable to read GitHub Copilot path ${root}: ${errorMessage3(error)}`);
|
|
3464
|
+
}
|
|
3465
|
+
}
|
|
3466
|
+
function readCopilotFile(file, index) {
|
|
3467
|
+
const fallbackId = path7.basename(file.filePath).replace(/\.jsonl$/i, "") || `copilot-session-${index}`;
|
|
3468
|
+
const fallbackTimestamp = fileModifiedDate(file.filePath);
|
|
3469
|
+
const fallback = emptyBreakdown3(file, fallbackId);
|
|
3470
|
+
try {
|
|
3471
|
+
const parse = parseJsonLines3(fs7.readFileSync(file.filePath, "utf8"));
|
|
3472
|
+
const traceContexts = collectTraceContexts(parse.records);
|
|
3473
|
+
const candidates = parse.records.map((record, recordIndex) => usageCandidateFromRecord(record, recordIndex, fallbackTimestamp, traceContexts)).filter((candidate) => Boolean(candidate));
|
|
3474
|
+
const selectedCandidates = uniqueCandidates(candidates.filter((candidate) => shouldEmitCandidate(candidate, candidates)));
|
|
3475
|
+
const groups = /* @__PURE__ */ new Map();
|
|
3476
|
+
fallback.rawEventCount = parse.records.length;
|
|
3477
|
+
fallback.parseErrors = parse.errors;
|
|
3478
|
+
fallback.parseStatus = parse.errors.length ? parse.records.length ? "partial" : "failed" : "ok";
|
|
3479
|
+
if (file.kind === "transcript" || file.kind === "session_state") {
|
|
3480
|
+
for (const record of parse.records) applyTranscriptRecord(fallback, record, file, file.kind === "transcript" ? fallbackTimestamp : void 0);
|
|
3481
|
+
}
|
|
3482
|
+
for (const candidate of selectedCandidates) {
|
|
3483
|
+
const group = groups.get(candidate.sessionId) ?? emptyBreakdown3(file, candidate.sessionId);
|
|
3484
|
+
group.rawEventCount = parse.records.length;
|
|
3485
|
+
group.parseErrors = parse.errors;
|
|
3486
|
+
group.parseStatus = parse.errors.length ? "partial" : "ok";
|
|
3487
|
+
applyCandidate(group, candidate);
|
|
3488
|
+
groups.set(candidate.sessionId, group);
|
|
3489
|
+
}
|
|
3490
|
+
const stats = statsFromParseErrors(parse.errors);
|
|
3491
|
+
if (groups.size > 0) {
|
|
3492
|
+
return { breakdowns: [...groups.values()].map((group) => mergeActivity(group, fallback)), stats };
|
|
3493
|
+
}
|
|
3494
|
+
if (fallback.messageCount > 0 || fallback.rawEventCount > 0 || fallback.parseErrors.length > 0) return { breakdowns: [fallbackWithMissingTokenWarning(fallback)], stats };
|
|
3495
|
+
return { breakdowns: [], stats };
|
|
3496
|
+
} catch (error) {
|
|
3497
|
+
const breakdown = emptyBreakdown3(file, fallbackId);
|
|
3498
|
+
const message = `unable_to_read_copilot_file:${errorMessage3(error)}`;
|
|
3499
|
+
breakdown.parseStatus = "failed";
|
|
3500
|
+
breakdown.parseErrors.push(message);
|
|
3501
|
+
breakdown.warnings.push(message);
|
|
3502
|
+
return { breakdowns: [breakdown], stats: { parseFailed: true, unreadable: true, malformed: false } };
|
|
3503
|
+
}
|
|
3504
|
+
}
|
|
3505
|
+
function statsFromParseErrors(errors) {
|
|
3506
|
+
return { parseFailed: errors.length > 0, unreadable: false, malformed: errors.some((error) => error.startsWith("jsonl_line_")) };
|
|
3507
|
+
}
|
|
3508
|
+
function collectTraceContexts(records) {
|
|
3509
|
+
const contexts = /* @__PURE__ */ new Map();
|
|
3510
|
+
for (const record of records) {
|
|
3511
|
+
const traceId = traceIdFromRecord(record);
|
|
3512
|
+
const attributes = firstObject5(record.attributes);
|
|
3513
|
+
if (!traceId || !attributes) continue;
|
|
3514
|
+
const context = contexts.get(traceId) ?? { model: void 0, sessionId: void 0, sessionPriority: 0 };
|
|
3515
|
+
context.model ??= firstNonEmptyAttr(attributes, modelAttrs);
|
|
3516
|
+
const session = bestSessionAttr(attributes);
|
|
3517
|
+
if (session && session.priority > context.sessionPriority) {
|
|
3518
|
+
context.sessionId = session.value;
|
|
3519
|
+
context.sessionPriority = session.priority;
|
|
3520
|
+
}
|
|
3521
|
+
contexts.set(traceId, context);
|
|
3522
|
+
}
|
|
3523
|
+
return contexts;
|
|
3524
|
+
}
|
|
3525
|
+
function usageCandidateFromRecord(record, index, fallbackTimestamp, traceContexts) {
|
|
3526
|
+
const attributes = firstObject5(record.attributes);
|
|
3527
|
+
if (!attributes) return void 0;
|
|
3528
|
+
const source = usageSource(record, attributes);
|
|
3529
|
+
if (!source) return void 0;
|
|
3530
|
+
const input = numberValue2(attributes["gen_ai.usage.input_tokens"]);
|
|
3531
|
+
const output = numberValue2(attributes["gen_ai.usage.output_tokens"]);
|
|
3532
|
+
const cached = numberValue2(attributes["gen_ai.usage.cache_read.input_tokens"]);
|
|
3533
|
+
const cacheCreation = firstPositiveNumber(attributes, ["gen_ai.usage.cache_write.input_tokens", "gen_ai.usage.cache_creation.input_tokens"]);
|
|
3534
|
+
const reasoning = firstPositiveNumber(attributes, ["gen_ai.usage.reasoning.output_tokens", "gen_ai.usage.reasoning_tokens"]);
|
|
3535
|
+
const total = firstPositiveNumber(attributes, ["gen_ai.usage.total_tokens", "gen_ai.usage.total.token_count"]);
|
|
3536
|
+
const outputWithFallback = output || outputTokensFromTotal(total, input, reasoning);
|
|
3537
|
+
if (input + outputWithFallback + cached + cacheCreation + reasoning <= 0) return void 0;
|
|
3538
|
+
const traceId = traceIdFromRecord(record);
|
|
3539
|
+
const traceContext = traceId ? traceContexts.get(traceId) : void 0;
|
|
3540
|
+
const session = bestSessionAttr(attributes)?.value ?? traceContext?.sessionId ?? traceId ?? "unknown-session";
|
|
3541
|
+
const timestamp3 = timestampFromRecord(record) ?? fallbackTimestamp;
|
|
3542
|
+
return {
|
|
3543
|
+
source,
|
|
3544
|
+
traceId,
|
|
3545
|
+
responseId: stringValue4(attributes["gen_ai.response.id"]),
|
|
3546
|
+
sessionId: session,
|
|
3547
|
+
model: firstNonEmptyAttr(attributes, modelAttrs) ?? traceContext?.model,
|
|
3548
|
+
timestamp: timestamp3,
|
|
3549
|
+
durationMs: durationMsFromRecord(record),
|
|
3550
|
+
inputTokens: input,
|
|
3551
|
+
cachedInputTokens: cached,
|
|
3552
|
+
cacheCreationInputTokens: cacheCreation,
|
|
3553
|
+
outputTokens: outputWithFallback,
|
|
3554
|
+
reasoningTokens: reasoning,
|
|
3555
|
+
dedupKey: dedupKeyForRecord(source, record, attributes, traceId, session, timestamp3, index)
|
|
3556
|
+
};
|
|
3557
|
+
}
|
|
3558
|
+
function outputTokensFromTotal(total, input, reasoning) {
|
|
3559
|
+
return total > 0 && total >= input + reasoning ? total - input - reasoning : 0;
|
|
3560
|
+
}
|
|
3561
|
+
function usageSource(record, attributes) {
|
|
3562
|
+
const operation = stringValue4(attributes["gen_ai.operation.name"]);
|
|
3563
|
+
const eventName = stringValue4(attributes["event.name"]);
|
|
3564
|
+
const name = stringValue4(record.name);
|
|
3565
|
+
const body = stringValue4(record.body) ?? stringValue4(record._body);
|
|
3566
|
+
const span = isSpanRecord(record);
|
|
3567
|
+
if (span && (operation === "chat" || name?.startsWith("chat "))) return "chat_span";
|
|
3568
|
+
if (!span && (eventName === "gen_ai.client.inference.operation.details" || body?.startsWith("GenAI inference:"))) return "inference_log";
|
|
3569
|
+
if (!span && (eventName === "copilot_chat.agent.turn" || body?.startsWith("copilot_chat.agent.turn"))) return "agent_turn_log";
|
|
3570
|
+
if (span && (operation === "invoke_agent" || name?.startsWith("invoke_agent "))) return "agent_summary_span";
|
|
3571
|
+
return void 0;
|
|
3572
|
+
}
|
|
3573
|
+
function isSpanRecord(record) {
|
|
3574
|
+
const type = stringValue4(record.type);
|
|
3575
|
+
if (type) return type === "span";
|
|
3576
|
+
return Boolean(stringValue4(record.name) && (stringValue4(record.spanId) || stringValue4(record.traceId) || record.startTime || record.endTime || record.duration || record.kind));
|
|
3577
|
+
}
|
|
3578
|
+
function shouldEmitCandidate(candidate, candidates) {
|
|
3579
|
+
const has = (source, field, value) => Boolean(value && candidates.some((item) => item.source === source && item[field] === value));
|
|
3580
|
+
if (candidate.source === "chat_span") return true;
|
|
3581
|
+
if (candidate.source === "inference_log") return !has("chat_span", "traceId", candidate.traceId) && !has("chat_span", "responseId", candidate.responseId);
|
|
3582
|
+
if (candidate.source === "agent_turn_log") {
|
|
3583
|
+
return !has("chat_span", "traceId", candidate.traceId) && !has("inference_log", "traceId", candidate.traceId) && !has("chat_span", "responseId", candidate.responseId) && !has("inference_log", "responseId", candidate.responseId);
|
|
3584
|
+
}
|
|
3585
|
+
return !has("chat_span", "traceId", candidate.traceId) && !has("inference_log", "traceId", candidate.traceId) && !has("agent_turn_log", "traceId", candidate.traceId) && !has("chat_span", "responseId", candidate.responseId) && !has("inference_log", "responseId", candidate.responseId) && !has("agent_turn_log", "responseId", candidate.responseId);
|
|
3586
|
+
}
|
|
3587
|
+
function uniqueCandidates(candidates) {
|
|
3588
|
+
const seen = /* @__PURE__ */ new Set();
|
|
3589
|
+
return candidates.filter((candidate) => {
|
|
3590
|
+
if (seen.has(candidate.dedupKey)) return false;
|
|
3591
|
+
seen.add(candidate.dedupKey);
|
|
3592
|
+
return true;
|
|
3593
|
+
});
|
|
3594
|
+
}
|
|
3595
|
+
var modelAttrs = ["gen_ai.response.model", "gen_ai.request.model"];
|
|
3596
|
+
var sessionAttrs = [
|
|
3597
|
+
{ key: "gen_ai.conversation.id", priority: 3 },
|
|
3598
|
+
{ key: "copilot_chat.session_id", priority: 3 },
|
|
3599
|
+
{ key: "copilot_chat.chat_session_id", priority: 3 },
|
|
3600
|
+
{ key: "session.id", priority: 3 },
|
|
3601
|
+
{ key: "github.copilot.interaction_id", priority: 2 },
|
|
3602
|
+
{ key: "gen_ai.response.id", priority: 1 }
|
|
3603
|
+
];
|
|
3604
|
+
function applyCandidate(breakdown, candidate) {
|
|
3605
|
+
breakdown.id = candidate.sessionId;
|
|
3606
|
+
breakdown.model = candidate.model ?? breakdown.model;
|
|
3607
|
+
const candidateStartedAt = candidate.timestamp && candidate.durationMs ? new Date(Date.parse(candidate.timestamp) - candidate.durationMs).toISOString() : candidate.timestamp;
|
|
3608
|
+
breakdown.startedAt = minDate2(breakdown.startedAt, candidateStartedAt);
|
|
3609
|
+
breakdown.endedAt = maxDate2(breakdown.endedAt, candidate.timestamp);
|
|
3610
|
+
applyTokenEvent(breakdown, {
|
|
3611
|
+
dedupKey: candidate.dedupKey,
|
|
3612
|
+
detailed: candidate.inputTokens > 0 || candidate.cachedInputTokens > 0 || candidate.cacheCreationInputTokens > 0 || candidate.reasoningTokens > 0,
|
|
3613
|
+
inputTokens: candidate.inputTokens,
|
|
3614
|
+
cachedInputTokens: candidate.cachedInputTokens,
|
|
3615
|
+
cacheCreationInputTokens: candidate.cacheCreationInputTokens,
|
|
3616
|
+
outputTokens: candidate.outputTokens,
|
|
3617
|
+
reasoningTokens: candidate.reasoningTokens
|
|
3618
|
+
});
|
|
3619
|
+
breakdown.messageCount += 1;
|
|
3620
|
+
breakdown.assistantMessageCount += 1;
|
|
3621
|
+
}
|
|
3622
|
+
function applyTranscriptRecord(breakdown, record, file, fallbackTimestamp) {
|
|
3623
|
+
const data = firstObject5(record.data);
|
|
3624
|
+
const timestamp3 = dateValue4(record.timestamp) ?? dateValue4(record.ts) ?? dateValue4(data?.startTime) ?? fallbackTimestamp;
|
|
3625
|
+
const type = stringValue4(record.type);
|
|
3626
|
+
const role = type?.startsWith("user.") ? "user" : type?.startsWith("assistant.") ? "assistant" : stringValue4(data?.role);
|
|
3627
|
+
const sessionId = stringValue4(data?.sessionId) ?? stringValue4(record.sessionId) ?? stringValue4(record.sid);
|
|
3628
|
+
if (sessionId) breakdown.id = sessionId;
|
|
3629
|
+
if (timestamp3) {
|
|
3630
|
+
breakdown.startedAt = minDate2(breakdown.startedAt, timestamp3);
|
|
3631
|
+
breakdown.endedAt = maxDate2(breakdown.endedAt, timestamp3);
|
|
3632
|
+
}
|
|
3633
|
+
breakdown.cwd ??= stringValue4(data?.cwd) ?? stringValue4(record.cwd) ?? file.fallbackCwd;
|
|
3634
|
+
const context = firstObject5(data?.context);
|
|
3635
|
+
breakdown.cwd ??= stringValue4(context?.cwd) ?? stringValue4(context?.gitRoot);
|
|
3636
|
+
const modelCandidate = stringValue4(data?.newModel) ?? stringValue4(data?.currentModel) ?? stringValue4(data?.model) ?? stringValue4(record.model);
|
|
3637
|
+
if (modelCandidate && (modelCandidate !== "auto" || !breakdown.model)) breakdown.model = modelCandidate;
|
|
3638
|
+
if (type === "session.start") breakdown.title ??= stringValue4(data?.title);
|
|
3639
|
+
const outputTokens = numberValue2(data?.outputTokens);
|
|
3640
|
+
if (outputTokens > 0) {
|
|
3641
|
+
const partialSessionId = sessionId ?? breakdown.id ?? path7.basename(file.filePath);
|
|
3642
|
+
applyTokenEvent(breakdown, {
|
|
3643
|
+
dedupKey: `transcript-output:${file.filePath}:${partialSessionId}:${timestamp3 ?? "unknown"}:${breakdown.usageCount}`,
|
|
3644
|
+
detailed: false,
|
|
3645
|
+
inputTokens: 0,
|
|
3646
|
+
cachedInputTokens: 0,
|
|
3647
|
+
cacheCreationInputTokens: 0,
|
|
3648
|
+
outputTokens,
|
|
3649
|
+
reasoningTokens: 0
|
|
3650
|
+
});
|
|
3651
|
+
}
|
|
3652
|
+
if (role === "user" || role === "assistant") {
|
|
3653
|
+
breakdown.messageCount += 1;
|
|
3654
|
+
if (role === "user") breakdown.userPromptCount += 1;
|
|
3655
|
+
if (role === "assistant") breakdown.assistantMessageCount += 1;
|
|
3656
|
+
applyTimeline(breakdown, role, data?.content ?? record.content, timestamp3);
|
|
3657
|
+
}
|
|
3658
|
+
const toolRequests = Array.isArray(data?.toolRequests) ? data.toolRequests : [];
|
|
3659
|
+
breakdown.toolCallCount += toolRequests.length;
|
|
3660
|
+
for (const request of toolRequests) {
|
|
3661
|
+
const object = firstObject5(request);
|
|
3662
|
+
const name = stringValue4(object?.name)?.toLowerCase() ?? "";
|
|
3663
|
+
const args2 = stringValue4(object?.arguments)?.toLowerCase() ?? "";
|
|
3664
|
+
if (name.includes("terminal") || args2.includes('"command"')) breakdown.shellCommandCount += 1;
|
|
3665
|
+
if (name.includes("edit") || args2.includes("edit")) breakdown.fileEditCount += 1;
|
|
3666
|
+
if (name.includes("read") || args2.includes("read_file")) breakdown.fileReadCount += 1;
|
|
3667
|
+
}
|
|
3668
|
+
}
|
|
3669
|
+
function breakdownToUsage(breakdown, index, options) {
|
|
3670
|
+
const id = breakdown.id ?? `copilot-session-${index}`;
|
|
3671
|
+
const cwd = breakdown.cwd ?? "";
|
|
3672
|
+
const repo = breakdown.cwd ? resolveRepoInfo(breakdown.cwd, options.config) : unknownCopilotRepoInfo();
|
|
3673
|
+
const hasTokenBreakdown = breakdown.inputTokens + breakdown.outputTokens + breakdown.reasoningTokens > 0 && breakdown.usageCount > 0;
|
|
3674
|
+
const hasPricedBreakdown = hasTokenBreakdown && breakdown.inputTokens > 0;
|
|
3675
|
+
const warnings = unique4([...repo.warnings, ...breakdown.warnings]);
|
|
3676
|
+
if (hasTokenBreakdown && !hasPricedBreakdown) warnings.push("copilot_partial_token_breakdown");
|
|
3677
|
+
const usage = {
|
|
3678
|
+
id,
|
|
3679
|
+
sourceClient: "copilot",
|
|
3680
|
+
sourceApp: breakdown.surface === "cli" ? "Copilot CLI" : breakdown.surface === "vscode" ? "VS Code" : "GitHub Copilot",
|
|
3681
|
+
sourceAppRaw: breakdown.surface,
|
|
3682
|
+
sourcePath: breakdown.sourcePaths[0] ?? "",
|
|
3683
|
+
repoRoot: repo.repoRoot,
|
|
3684
|
+
repoName: repo.repoName,
|
|
3685
|
+
cwd,
|
|
3686
|
+
gitRemoteUrl: repo.gitRemoteUrl,
|
|
3687
|
+
gitBranch: repo.gitBranch,
|
|
3688
|
+
title: breakdown.title,
|
|
3689
|
+
startedAt: breakdown.startedAt,
|
|
3690
|
+
endedAt: breakdown.endedAt,
|
|
3691
|
+
model: normalizeCopilotModel(breakdown.model),
|
|
3692
|
+
provider: providerFromModel(breakdown.model),
|
|
3693
|
+
inputTokens: breakdown.inputTokens,
|
|
3694
|
+
cachedInputTokens: breakdown.cachedInputTokens,
|
|
3695
|
+
cacheCreationInputTokens: breakdown.cacheCreationInputTokens,
|
|
3696
|
+
outputTokens: breakdown.outputTokens,
|
|
3697
|
+
reasoningTokens: breakdown.reasoningTokens,
|
|
3698
|
+
reasoningOutputTokens: breakdown.reasoningTokens,
|
|
3699
|
+
totalTokens: breakdown.inputTokens + breakdown.outputTokens + breakdown.reasoningTokens,
|
|
3700
|
+
tokenAggregationMethod: hasTokenBreakdown ? "direct_usage" : "unknown",
|
|
3701
|
+
tokenConfidence: hasTokenBreakdown ? "high" : "low",
|
|
3702
|
+
tokenSnapshotCount: breakdown.usageCount,
|
|
3703
|
+
estimatedCostUsd: void 0,
|
|
3704
|
+
messageCount: breakdown.messageCount,
|
|
3705
|
+
rawTokenTotal: hasTokenBreakdown ? breakdown.inputTokens + breakdown.outputTokens + breakdown.reasoningTokens : void 0,
|
|
3706
|
+
warnings: hasTokenBreakdown ? warnings : unique4([...warnings, "missing_token_breakdown"]),
|
|
3707
|
+
durationMs: durationMs3(breakdown.startedAt, breakdown.endedAt),
|
|
3708
|
+
rawEventCount: breakdown.rawEventCount,
|
|
3709
|
+
parseStatus: breakdown.parseStatus,
|
|
3710
|
+
parseErrors: breakdown.parseErrors,
|
|
3711
|
+
detectedSurface: breakdown.surface === "vscode" ? "vscode_extension" : "terminal_cli",
|
|
3712
|
+
surfaceConfidence: breakdown.surface === "unknown" ? "low" : "high",
|
|
3713
|
+
surfaceReason: breakdown.surface === "vscode" ? "GitHub Copilot VS Code workspace storage" : "GitHub Copilot OpenTelemetry export",
|
|
3714
|
+
promptTimeline: breakdown.promptTimeline,
|
|
3715
|
+
userPromptCount: breakdown.userPromptCount,
|
|
3716
|
+
assistantMessageCount: breakdown.assistantMessageCount,
|
|
3717
|
+
toolCallCount: breakdown.toolCallCount,
|
|
3718
|
+
shellCommandCount: breakdown.shellCommandCount,
|
|
3719
|
+
failedToolCallCount: breakdown.failedToolCallCount,
|
|
3720
|
+
nonZeroCommandEvents: 0,
|
|
3721
|
+
importantCommandFailures: 0,
|
|
3722
|
+
harmlessNonZeroEvents: 0,
|
|
3723
|
+
exploratoryMisses: 0,
|
|
3724
|
+
repeatedFailureClusters: 0,
|
|
3725
|
+
commandIssueSeverity: breakdown.failedToolCallCount > 0 ? "warning" : "none",
|
|
3726
|
+
commandIssueImpact: breakdown.failedToolCallCount > 0 ? "medium" : "none",
|
|
3727
|
+
topFailureType: breakdown.failedToolCallCount > 0 ? "unknown" : void 0,
|
|
3728
|
+
commandIssueSamples: [],
|
|
3729
|
+
fileReadCount: breakdown.fileReadCount,
|
|
3730
|
+
fileEditCount: breakdown.fileEditCount,
|
|
3731
|
+
sourceMetadata: { copilot: { sourcePaths: breakdown.sourcePaths, surface: breakdown.surface } }
|
|
3732
|
+
};
|
|
3733
|
+
const cost = hasPricedBreakdown ? calculateCostUsd(usage, options.pricing) : void 0;
|
|
3734
|
+
return { ...usage, estimatedCostUsd: cost, warnings: hasPricedBreakdown && cost === void 0 ? [...usage.warnings, "unknown_pricing"] : usage.warnings };
|
|
3735
|
+
}
|
|
3736
|
+
function unknownCopilotRepoInfo() {
|
|
3737
|
+
return {
|
|
3738
|
+
repoRoot: "unknown-copilot-repo",
|
|
3739
|
+
repoName: "Unknown repo/folder",
|
|
3740
|
+
gitRemoteUrl: void 0,
|
|
3741
|
+
gitBranch: void 0,
|
|
3742
|
+
warnings: ["repo_unverified_no_git_root", "copilot_missing_cwd"]
|
|
3743
|
+
};
|
|
3744
|
+
}
|
|
3745
|
+
function emptyBreakdown3(file, id) {
|
|
3746
|
+
return {
|
|
3747
|
+
id,
|
|
3748
|
+
title: void 0,
|
|
3749
|
+
cwd: file.fallbackCwd,
|
|
3750
|
+
startedAt: void 0,
|
|
3751
|
+
endedAt: void 0,
|
|
3752
|
+
model: void 0,
|
|
3753
|
+
inputTokens: 0,
|
|
3754
|
+
cachedInputTokens: 0,
|
|
3755
|
+
cacheCreationInputTokens: 0,
|
|
3756
|
+
outputTokens: 0,
|
|
3757
|
+
reasoningTokens: 0,
|
|
3758
|
+
usageCount: 0,
|
|
3759
|
+
messageCount: 0,
|
|
3760
|
+
userPromptCount: 0,
|
|
3761
|
+
assistantMessageCount: 0,
|
|
3762
|
+
toolCallCount: 0,
|
|
3763
|
+
shellCommandCount: 0,
|
|
3764
|
+
failedToolCallCount: 0,
|
|
3765
|
+
fileReadCount: 0,
|
|
3766
|
+
fileEditCount: 0,
|
|
3767
|
+
rawEventCount: 0,
|
|
3768
|
+
parseStatus: "ok",
|
|
3769
|
+
parseErrors: [],
|
|
3770
|
+
promptTimeline: [],
|
|
3771
|
+
warnings: [],
|
|
3772
|
+
sourcePaths: [file.filePath],
|
|
3773
|
+
surface: file.surface,
|
|
3774
|
+
tokenEvents: []
|
|
3775
|
+
};
|
|
3776
|
+
}
|
|
3777
|
+
function mergeBreakdown(sessions, breakdown) {
|
|
3778
|
+
const key = breakdown.id ?? breakdown.sourcePaths[0] ?? "unknown-session";
|
|
3779
|
+
const existing = sessions.get(key);
|
|
3780
|
+
if (!existing) {
|
|
3781
|
+
sessions.set(key, breakdown);
|
|
3782
|
+
return;
|
|
3783
|
+
}
|
|
3784
|
+
mergeActivity(existing, breakdown);
|
|
3785
|
+
mergeTokenData(existing, breakdown);
|
|
3786
|
+
existing.sourcePaths = unique4([...existing.sourcePaths, ...breakdown.sourcePaths]);
|
|
3787
|
+
}
|
|
3788
|
+
function mergeTokenData(target, source) {
|
|
3789
|
+
if (source.usageCount <= 0) return;
|
|
3790
|
+
const targetHadUsage = target.usageCount > 0;
|
|
3791
|
+
const targetHasDetailed = hasDetailedTokenBreakdown(target);
|
|
3792
|
+
const sourceHasDetailed = hasDetailedTokenBreakdown(source);
|
|
3793
|
+
if (targetHadUsage) target.warnings = unique4([...target.warnings, "copilot_duplicate_session_merged"]);
|
|
3794
|
+
if (targetHadUsage && targetHasDetailed && !sourceHasDetailed) return;
|
|
3795
|
+
if (targetHadUsage && !targetHasDetailed && sourceHasDetailed) {
|
|
3796
|
+
replaceTokenData(target, source);
|
|
3797
|
+
return;
|
|
3798
|
+
}
|
|
3799
|
+
if (source.tokenEvents.length > 0) {
|
|
3800
|
+
const existingKeys = new Set(target.tokenEvents.map((event) => event.dedupKey));
|
|
3801
|
+
for (const event of source.tokenEvents) {
|
|
3802
|
+
if (existingKeys.has(event.dedupKey)) continue;
|
|
3803
|
+
applyTokenEvent(target, event);
|
|
3804
|
+
existingKeys.add(event.dedupKey);
|
|
3805
|
+
}
|
|
3806
|
+
return;
|
|
3807
|
+
}
|
|
3808
|
+
applyTokenEvent(target, {
|
|
3809
|
+
dedupKey: `merged-total:${source.sourcePaths.join("|")}:${source.usageCount}`,
|
|
3810
|
+
detailed: sourceHasDetailed,
|
|
3811
|
+
inputTokens: source.inputTokens,
|
|
3812
|
+
cachedInputTokens: source.cachedInputTokens,
|
|
3813
|
+
cacheCreationInputTokens: source.cacheCreationInputTokens,
|
|
3814
|
+
outputTokens: source.outputTokens,
|
|
3815
|
+
reasoningTokens: source.reasoningTokens
|
|
3816
|
+
});
|
|
3817
|
+
}
|
|
3818
|
+
function replaceTokenData(target, source) {
|
|
3819
|
+
target.inputTokens = source.inputTokens;
|
|
3820
|
+
target.cachedInputTokens = source.cachedInputTokens;
|
|
3821
|
+
target.cacheCreationInputTokens = source.cacheCreationInputTokens;
|
|
3822
|
+
target.outputTokens = source.outputTokens;
|
|
3823
|
+
target.reasoningTokens = source.reasoningTokens;
|
|
3824
|
+
target.usageCount = source.usageCount;
|
|
3825
|
+
target.tokenEvents = source.tokenEvents.map((event) => ({ ...event }));
|
|
3826
|
+
}
|
|
3827
|
+
function hasDetailedTokenBreakdown(breakdown) {
|
|
3828
|
+
return breakdown.inputTokens > 0 || breakdown.cachedInputTokens > 0 || breakdown.cacheCreationInputTokens > 0 || breakdown.reasoningTokens > 0;
|
|
3829
|
+
}
|
|
3830
|
+
function applyTokenEvent(breakdown, event) {
|
|
3831
|
+
if (breakdown.tokenEvents.some((existing) => existing.dedupKey === event.dedupKey)) return;
|
|
3832
|
+
breakdown.inputTokens += event.inputTokens;
|
|
3833
|
+
breakdown.cachedInputTokens += event.cachedInputTokens;
|
|
3834
|
+
breakdown.cacheCreationInputTokens += event.cacheCreationInputTokens;
|
|
3835
|
+
breakdown.outputTokens += event.outputTokens;
|
|
3836
|
+
breakdown.reasoningTokens += event.reasoningTokens;
|
|
3837
|
+
breakdown.usageCount += 1;
|
|
3838
|
+
breakdown.tokenEvents.push({ ...event });
|
|
3839
|
+
}
|
|
3840
|
+
function applyModelHints(sessions, modelHints) {
|
|
3841
|
+
for (const breakdown of sessions.values()) {
|
|
3842
|
+
if (!breakdown.id) continue;
|
|
3843
|
+
const hint = modelHints.get(breakdown.id);
|
|
3844
|
+
if (!hint || hasKnownModel(breakdown.model)) continue;
|
|
3845
|
+
breakdown.model = hint.model;
|
|
3846
|
+
breakdown.sourcePaths = unique4([...breakdown.sourcePaths, hint.sourcePath]);
|
|
3847
|
+
}
|
|
3848
|
+
}
|
|
3849
|
+
function hasKnownModel(model) {
|
|
3850
|
+
const normalized = model?.trim().toLowerCase().replace(/^github[-_]?copilot\//, "").replace(/^copilot\//, "");
|
|
3851
|
+
return Boolean(normalized && normalized !== "auto");
|
|
3852
|
+
}
|
|
3853
|
+
function mergeActivity(target, source) {
|
|
3854
|
+
target.title ??= source.title;
|
|
3855
|
+
target.cwd ??= source.cwd;
|
|
3856
|
+
target.model ??= source.model;
|
|
3857
|
+
target.startedAt = minDate2(target.startedAt, source.startedAt);
|
|
3858
|
+
target.endedAt = maxDate2(target.endedAt, source.endedAt);
|
|
3859
|
+
target.messageCount = Math.max(target.messageCount, source.messageCount);
|
|
3860
|
+
target.userPromptCount = Math.max(target.userPromptCount, source.userPromptCount);
|
|
3861
|
+
target.assistantMessageCount = Math.max(target.assistantMessageCount, source.assistantMessageCount);
|
|
3862
|
+
target.toolCallCount = Math.max(target.toolCallCount, source.toolCallCount);
|
|
3863
|
+
target.shellCommandCount = Math.max(target.shellCommandCount, source.shellCommandCount);
|
|
3864
|
+
target.fileReadCount = Math.max(target.fileReadCount, source.fileReadCount);
|
|
3865
|
+
target.fileEditCount = Math.max(target.fileEditCount, source.fileEditCount);
|
|
3866
|
+
target.rawEventCount = Math.max(target.rawEventCount, source.rawEventCount);
|
|
3867
|
+
target.parseErrors = unique4([...target.parseErrors, ...source.parseErrors]);
|
|
3868
|
+
target.parseStatus = target.parseErrors.length ? "partial" : target.parseStatus;
|
|
3869
|
+
target.promptTimeline = [...target.promptTimeline, ...source.promptTimeline].slice(0, 80);
|
|
3870
|
+
target.warnings = unique4([...target.warnings, ...source.warnings]);
|
|
3871
|
+
return target;
|
|
3872
|
+
}
|
|
3873
|
+
function fallbackWithMissingTokenWarning(breakdown) {
|
|
3874
|
+
if (breakdown.usageCount === 0) breakdown.warnings.push("missing_token_breakdown");
|
|
3875
|
+
return breakdown;
|
|
3876
|
+
}
|
|
3877
|
+
function parseJsonLines3(text) {
|
|
3878
|
+
const records = [];
|
|
3879
|
+
const errors = [];
|
|
3880
|
+
text.split(/\r?\n/).forEach((line, index) => {
|
|
3881
|
+
const trimmed = line.trim();
|
|
3882
|
+
if (!trimmed) return;
|
|
3883
|
+
try {
|
|
3884
|
+
const parsed = JSON.parse(trimmed);
|
|
3885
|
+
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) records.push(parsed);
|
|
3886
|
+
} catch (error) {
|
|
3887
|
+
errors.push(`jsonl_line_${index + 1}:${errorMessage3(error)}`);
|
|
3888
|
+
}
|
|
3889
|
+
});
|
|
3890
|
+
return { records, errors };
|
|
3891
|
+
}
|
|
3892
|
+
function applyTimeline(breakdown, role, content, timestamp3) {
|
|
3893
|
+
if (breakdown.promptTimeline.length >= 80) return;
|
|
3894
|
+
const text = typeof content === "string" ? content : void 0;
|
|
3895
|
+
if (!text?.trim()) return;
|
|
3896
|
+
breakdown.promptTimeline.push({ role, text: text.replace(/\s+/g, " ").trim().slice(0, 1e3), timestamp: timestamp3 });
|
|
3897
|
+
}
|
|
3898
|
+
function firstNonEmptyAttr(attributes, keys) {
|
|
3899
|
+
return keys.map((key) => stringValue4(attributes[key])).find(Boolean);
|
|
3900
|
+
}
|
|
3901
|
+
function bestSessionAttr(attributes) {
|
|
3902
|
+
return sessionAttrs.map((attr) => ({ value: stringValue4(attributes[attr.key]), priority: attr.priority })).filter((item) => Boolean(item.value)).sort((a, b) => b.priority - a.priority)[0];
|
|
3903
|
+
}
|
|
3904
|
+
function traceIdFromRecord(record) {
|
|
3905
|
+
const spanContext = firstObject5(record.spanContext);
|
|
3906
|
+
return stringValue4(record.traceId) ?? stringValue4(spanContext?.traceId);
|
|
3907
|
+
}
|
|
3908
|
+
function spanIdFromRecord(record) {
|
|
3909
|
+
const spanContext = firstObject5(record.spanContext);
|
|
3910
|
+
return stringValue4(record.spanId) ?? stringValue4(spanContext?.spanId);
|
|
3911
|
+
}
|
|
3912
|
+
function dedupKeyForRecord(source, record, attributes, traceId, sessionId, timestamp3, index) {
|
|
3913
|
+
const spanId = spanIdFromRecord(record);
|
|
3914
|
+
if ((source === "chat_span" || source === "agent_summary_span") && traceId && spanId) return `${traceId}:${spanId}`;
|
|
3915
|
+
if (source === "inference_log" && traceId && spanId) return `log:${traceId}:${spanId}`;
|
|
3916
|
+
if (source === "agent_turn_log") {
|
|
3917
|
+
const turn = stringValue4(attributes["turn.index"]) ?? stringValue4(attributes["copilot_chat.turn.index"]) ?? `idx-${index}`;
|
|
3918
|
+
return traceId ? `agent-turn:${traceId}:${turn}` : `agent-turn:${sessionId}:${turn}:${index}`;
|
|
3919
|
+
}
|
|
3920
|
+
return `${source}:${sessionId}:${timestamp3 ?? "unknown"}:${index}`;
|
|
3921
|
+
}
|
|
3922
|
+
function timestampFromRecord(record) {
|
|
3923
|
+
return timestampMsFromParts(record.endTime) ?? timestampMsFromParts(record.startTime) ?? timestampMsFromParts(record.hrTime) ?? timestampMsFromParts(record._hrTime) ?? timestampMsFromScalar(record.timestamp) ?? timestampMsFromScalar(record.observedTimestamp) ?? timestampMsFromUnixNanos(record.timeUnixNano);
|
|
3924
|
+
}
|
|
3925
|
+
function timestampMsFromParts(value) {
|
|
3926
|
+
if (!Array.isArray(value)) return void 0;
|
|
3927
|
+
const seconds = numberValue2(value[0]);
|
|
3928
|
+
const nanos = numberValue2(value[1]);
|
|
3929
|
+
if (seconds <= 0) return void 0;
|
|
3930
|
+
return new Date(seconds * 1e3 + Math.floor(nanos / 1e6)).toISOString();
|
|
3931
|
+
}
|
|
3932
|
+
function timestampMsFromScalar(value) {
|
|
3933
|
+
const raw = numberValue2(value);
|
|
3934
|
+
if (raw <= 0) return void 0;
|
|
3935
|
+
const absolute = Math.abs(raw);
|
|
3936
|
+
const ms = absolute >= 1e17 ? raw / 1e6 : absolute >= 1e14 ? raw / 1e3 : absolute >= 1e11 ? raw : raw * 1e3;
|
|
3937
|
+
return new Date(ms).toISOString();
|
|
3938
|
+
}
|
|
3939
|
+
function timestampMsFromUnixNanos(value) {
|
|
3940
|
+
const raw = numberValue2(value);
|
|
3941
|
+
return raw > 0 ? new Date(Math.floor(raw / 1e6)).toISOString() : void 0;
|
|
3942
|
+
}
|
|
3943
|
+
function durationMsFromRecord(record) {
|
|
3944
|
+
const start = timestampMs2(record.startTime);
|
|
3945
|
+
const end = timestampMs2(record.endTime);
|
|
3946
|
+
if (start !== void 0 && end !== void 0 && end > start) return end - start;
|
|
3947
|
+
const duration = numberValue2(record.duration);
|
|
3948
|
+
return duration > 0 ? duration >= 1e6 ? Math.floor(duration / 1e6) : duration : void 0;
|
|
3949
|
+
}
|
|
3950
|
+
function timestampMs2(value) {
|
|
3951
|
+
const iso = timestampMsFromParts(value) ?? timestampMsFromScalar(value);
|
|
3952
|
+
return iso ? Date.parse(iso) : void 0;
|
|
3953
|
+
}
|
|
3954
|
+
function defaultCodeUserRoots() {
|
|
3955
|
+
const home = os4.homedir();
|
|
3956
|
+
if (process.platform === "win32") {
|
|
3957
|
+
const appData = process.env.APPDATA ?? path7.join(home, "AppData", "Roaming");
|
|
3958
|
+
return ["Code", "Code - Insiders", "VSCodium"].map((name) => path7.join(appData, name, "User"));
|
|
3959
|
+
}
|
|
3960
|
+
if (process.platform === "darwin") {
|
|
3961
|
+
return ["Code", "Code - Insiders", "VSCodium"].map((name) => path7.join(home, "Library", "Application Support", name, "User"));
|
|
3962
|
+
}
|
|
3963
|
+
const xdg = process.env.XDG_CONFIG_HOME || path7.join(home, ".config");
|
|
3964
|
+
return ["Code", "Code - Insiders", "VSCodium"].map((name) => path7.join(xdg, name, "User"));
|
|
3965
|
+
}
|
|
3966
|
+
function workspaceFolderFromStorageJson(filePath) {
|
|
3967
|
+
try {
|
|
3968
|
+
const parsed = JSON.parse(fs7.readFileSync(filePath, "utf8"));
|
|
3969
|
+
const folder = stringValue4(parsed.folder);
|
|
3970
|
+
if (!folder) return void 0;
|
|
3971
|
+
return folder.startsWith("file://") ? decodeURIComponent(new URL(folder).pathname) : folder;
|
|
3972
|
+
} catch {
|
|
3973
|
+
return void 0;
|
|
3974
|
+
}
|
|
3975
|
+
}
|
|
3976
|
+
function cwdFromWorkspaceYaml(filePath) {
|
|
3977
|
+
try {
|
|
3978
|
+
const text = fs7.readFileSync(filePath, "utf8");
|
|
3979
|
+
const match = /^cwd:\s*(.+)$/m.exec(text);
|
|
3980
|
+
return match?.[1]?.trim();
|
|
3981
|
+
} catch {
|
|
3982
|
+
return void 0;
|
|
3983
|
+
}
|
|
3984
|
+
}
|
|
3985
|
+
function fileMayOverlapScanWindow2(filePath, scanWindow) {
|
|
3986
|
+
if (!scanWindow?.fromMs && !scanWindow?.toMs) return true;
|
|
3987
|
+
try {
|
|
3988
|
+
const mtime = fs7.statSync(filePath).mtimeMs;
|
|
3989
|
+
if (scanWindow.fromMs !== void 0 && mtime < scanWindow.fromMs) return false;
|
|
3990
|
+
if (scanWindow.toMs !== void 0 && mtime > scanWindow.toMs) return false;
|
|
3991
|
+
} catch {
|
|
3992
|
+
return true;
|
|
3993
|
+
}
|
|
3994
|
+
return true;
|
|
3995
|
+
}
|
|
3996
|
+
function walk3(root, maxDepth, onFile, depth = 0) {
|
|
3997
|
+
if (depth > maxDepth) return;
|
|
3998
|
+
for (const entry of fs7.readdirSync(root, { withFileTypes: true })) {
|
|
3999
|
+
const fullPath = path7.join(root, entry.name);
|
|
4000
|
+
if (entry.isDirectory()) walk3(fullPath, maxDepth, onFile, depth + 1);
|
|
4001
|
+
else if (entry.isFile()) onFile(fullPath);
|
|
4002
|
+
}
|
|
4003
|
+
}
|
|
4004
|
+
function isReadableRegularFile(filePath, maxBytes) {
|
|
4005
|
+
try {
|
|
4006
|
+
const stat = fs7.statSync(filePath);
|
|
4007
|
+
return stat.isFile() && stat.size <= maxBytes;
|
|
4008
|
+
} catch {
|
|
4009
|
+
return false;
|
|
4010
|
+
}
|
|
4011
|
+
}
|
|
4012
|
+
function normalizeCopilotModel(model) {
|
|
4013
|
+
if (!model) return void 0;
|
|
4014
|
+
return normalizePricingModelId(model);
|
|
4015
|
+
}
|
|
4016
|
+
function providerFromModel(model) {
|
|
4017
|
+
const normalized = normalizeCopilotModel(model);
|
|
4018
|
+
if (!normalized) return "github-copilot";
|
|
4019
|
+
if (normalized.includes("claude")) return "anthropic";
|
|
4020
|
+
if (normalized.includes("gemini")) return "google";
|
|
4021
|
+
if (isCopilotAliasModel(normalized)) return "github-copilot";
|
|
4022
|
+
if (normalized.includes("gpt")) return "openai";
|
|
4023
|
+
return "github-copilot";
|
|
4024
|
+
}
|
|
4025
|
+
function firstPositiveNumber(object, keys) {
|
|
4026
|
+
return keys.map((key) => numberValue2(object[key])).find((value) => value > 0) ?? 0;
|
|
4027
|
+
}
|
|
4028
|
+
function firstObject5(value) {
|
|
4029
|
+
return value && typeof value === "object" && !Array.isArray(value) ? value : void 0;
|
|
4030
|
+
}
|
|
4031
|
+
function stringValue4(value) {
|
|
4032
|
+
return typeof value === "string" && value.trim() ? value.trim() : void 0;
|
|
4033
|
+
}
|
|
4034
|
+
function numberValue2(value) {
|
|
4035
|
+
if (typeof value === "number" && Number.isFinite(value)) return Math.max(Math.trunc(value), 0);
|
|
4036
|
+
if (typeof value === "string" && value.trim()) {
|
|
4037
|
+
const parsed = Number(value);
|
|
4038
|
+
return Number.isFinite(parsed) ? Math.max(Math.trunc(parsed), 0) : 0;
|
|
4039
|
+
}
|
|
4040
|
+
return 0;
|
|
4041
|
+
}
|
|
4042
|
+
function dateValue4(value) {
|
|
4043
|
+
if (typeof value === "string") {
|
|
4044
|
+
const parsed = Date.parse(value);
|
|
4045
|
+
return Number.isFinite(parsed) ? new Date(parsed).toISOString() : void 0;
|
|
4046
|
+
}
|
|
4047
|
+
return timestampMsFromScalar(value);
|
|
4048
|
+
}
|
|
4049
|
+
function fileModifiedDate(filePath) {
|
|
4050
|
+
try {
|
|
4051
|
+
return new Date(fs7.statSync(filePath).mtimeMs).toISOString();
|
|
4052
|
+
} catch {
|
|
4053
|
+
return void 0;
|
|
4054
|
+
}
|
|
4055
|
+
}
|
|
4056
|
+
function minDate2(a, b) {
|
|
4057
|
+
if (!a) return b;
|
|
4058
|
+
if (!b) return a;
|
|
4059
|
+
return a < b ? a : b;
|
|
4060
|
+
}
|
|
4061
|
+
function maxDate2(a, b) {
|
|
4062
|
+
if (!a) return b;
|
|
4063
|
+
if (!b) return a;
|
|
4064
|
+
return a > b ? a : b;
|
|
4065
|
+
}
|
|
4066
|
+
function durationMs3(startedAt, endedAt) {
|
|
4067
|
+
if (!startedAt || !endedAt) return void 0;
|
|
4068
|
+
const start = Date.parse(startedAt);
|
|
4069
|
+
const end = Date.parse(endedAt);
|
|
4070
|
+
return Number.isFinite(start) && Number.isFinite(end) && end >= start ? end - start : void 0;
|
|
4071
|
+
}
|
|
4072
|
+
function unique4(items) {
|
|
4073
|
+
return [...new Set(items)];
|
|
4074
|
+
}
|
|
4075
|
+
function uniqueFiles(files) {
|
|
4076
|
+
const seen = /* @__PURE__ */ new Set();
|
|
4077
|
+
return files.filter((file) => {
|
|
4078
|
+
const key = path7.resolve(file.filePath);
|
|
4079
|
+
if (seen.has(key)) return false;
|
|
4080
|
+
seen.add(key);
|
|
4081
|
+
return true;
|
|
4082
|
+
}).sort((a, b) => a.filePath.localeCompare(b.filePath));
|
|
4083
|
+
}
|
|
4084
|
+
function errorMessage3(error) {
|
|
4085
|
+
return error instanceof Error ? error.message : String(error);
|
|
4086
|
+
}
|
|
4087
|
+
|
|
4088
|
+
// packages/core/src/sources/cursor.ts
|
|
4089
|
+
import fs8 from "node:fs";
|
|
4090
|
+
import os5 from "node:os";
|
|
4091
|
+
import path8 from "node:path";
|
|
4092
|
+
import Database2 from "better-sqlite3";
|
|
4093
|
+
var parserVersion = "cursor-v1";
|
|
4094
|
+
var maxJsonlBytes2 = 20 * 1024 * 1024;
|
|
4095
|
+
var maxDatabaseBytes = 100 * 1024 * 1024;
|
|
4096
|
+
var cursorStoragePrefixes = ["cursor.", "anysphere."];
|
|
4097
|
+
var interestingTerms = ["cursor", "composer", "aichat", "ai_chat", "aiservice", "agent-transcript", "prompttokenbreakdown"];
|
|
4098
|
+
function scanCursor(options) {
|
|
4099
|
+
const cursorHome = options.cursorHome ?? path8.join(os5.homedir(), ".cursor");
|
|
4100
|
+
const discovery = discoverCursorFiles(cursorHome, Boolean(options.cursorHome));
|
|
4101
|
+
const jsonlFiles = discovery.jsonlFiles.filter((filePath) => fileMayOverlapScanWindow3(filePath, options.scanWindow));
|
|
4102
|
+
const databaseFiles = discovery.databaseFiles.filter((filePath) => fileMayOverlapScanWindow3(filePath, options.scanWindow));
|
|
4103
|
+
const jsonlCandidates = jsonlFiles.map((filePath) => cursorJsonlToCandidate(filePath, cursorHome));
|
|
4104
|
+
const databaseCandidates = databaseFiles.flatMap((filePath) => cursorDatabaseToCandidates(filePath, cursorHome, discovery.warnings));
|
|
4105
|
+
const candidates = dedupeCandidates([...jsonlCandidates, ...databaseCandidates]);
|
|
4106
|
+
const sessions = candidates.map((candidate, index) => candidateToUsage(candidate, index, options));
|
|
4107
|
+
const parseFailureCount = sessions.filter((session) => session.parseStatus === "failed" || (session.parseErrors?.length ?? 0) > 0).length;
|
|
4108
|
+
const unreadableFileCount = sessions.filter((session) => session.parseErrors?.some((error) => error.startsWith("unable_to_read_"))).length;
|
|
4109
|
+
const malformedFileCount = sessions.filter((session) => session.parseErrors?.some((error) => error.startsWith("jsonl_line_") || error.startsWith("sqlite_json_parse:"))).length;
|
|
4110
|
+
if (!fs8.existsSync(cursorHome)) {
|
|
4111
|
+
discovery.warnings.push(`Cursor home directory not found at ${cursorHome}`);
|
|
4112
|
+
}
|
|
4113
|
+
if (!fs8.existsSync(discovery.statePath)) {
|
|
4114
|
+
discovery.warnings.push(`Cursor global state database not found at ${discovery.statePath}`);
|
|
4115
|
+
}
|
|
4116
|
+
return {
|
|
4117
|
+
source: {
|
|
4118
|
+
id: "cursor",
|
|
4119
|
+
label: "Cursor",
|
|
4120
|
+
available: discovery.jsonlFiles.length > 0 || discovery.databaseFiles.length > 0,
|
|
4121
|
+
paths: unique5([cursorHome, discovery.projectsPath, discovery.statePath, discovery.workspaceStoragePath, ...discovery.roots]),
|
|
4122
|
+
warnings: discovery.warnings
|
|
4123
|
+
},
|
|
4124
|
+
sessions,
|
|
4125
|
+
stats: {
|
|
4126
|
+
sourceId: "cursor",
|
|
4127
|
+
sourceLabel: "Cursor",
|
|
4128
|
+
homePath: cursorHome,
|
|
4129
|
+
cursorHome,
|
|
4130
|
+
globalStoragePath: discovery.globalStoragePath,
|
|
4131
|
+
workspaceStoragePath: discovery.workspaceStoragePath,
|
|
4132
|
+
projectsPath: discovery.projectsPath,
|
|
4133
|
+
statePath: discovery.statePath,
|
|
4134
|
+
projectsExists: fs8.existsSync(discovery.projectsPath),
|
|
4135
|
+
stateExists: fs8.existsSync(discovery.statePath),
|
|
4136
|
+
sessionsExists: jsonlFiles.length > 0,
|
|
4137
|
+
sessionFileCount: jsonlFiles.length,
|
|
4138
|
+
databaseFileCount: databaseFiles.length,
|
|
4139
|
+
skippedFileCount: discovery.skippedFileCount,
|
|
4140
|
+
sessionsImported: sessions.length,
|
|
4141
|
+
parseFailureCount,
|
|
4142
|
+
unreadableFileCount,
|
|
4143
|
+
malformedFileCount,
|
|
4144
|
+
lastScannedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
4145
|
+
}
|
|
4146
|
+
};
|
|
4147
|
+
}
|
|
4148
|
+
function discoverCursorFiles(cursorHome, explicitHome) {
|
|
4149
|
+
const warnings = [];
|
|
4150
|
+
const appPaths = defaultCursorAppPaths();
|
|
4151
|
+
const roots = explicitHome ? [cursorHome] : unique5([cursorHome, appPaths.statePath, ...cursorOwnedGlobalStorageRoots(appPaths.globalStoragePath), ...cursorOwnedWorkspaceStorageRoots(appPaths.workspaceStoragePath)]);
|
|
4152
|
+
const jsonlFiles = [];
|
|
4153
|
+
const databaseFiles = [];
|
|
4154
|
+
let skippedFileCount = 0;
|
|
4155
|
+
for (const root of roots) {
|
|
4156
|
+
if (!fs8.existsSync(root)) continue;
|
|
4157
|
+
try {
|
|
4158
|
+
if (isReadableRegularFile2(root, maxDatabaseBytes)) {
|
|
4159
|
+
databaseFiles.push(root);
|
|
4160
|
+
continue;
|
|
4161
|
+
}
|
|
4162
|
+
walk4(root, 10, (filePath) => {
|
|
4163
|
+
const lower = filePath.toLowerCase();
|
|
4164
|
+
if (lower.endsWith(".jsonl")) {
|
|
4165
|
+
if (isReadableRegularFile2(filePath, maxJsonlBytes2)) jsonlFiles.push(filePath);
|
|
4166
|
+
else skippedFileCount += 1;
|
|
4167
|
+
return;
|
|
4168
|
+
}
|
|
4169
|
+
if (/\.(sqlite|db|vscdb)$/i.test(filePath)) {
|
|
4170
|
+
if (isReadableRegularFile2(filePath, maxDatabaseBytes)) databaseFiles.push(filePath);
|
|
4171
|
+
else skippedFileCount += 1;
|
|
4172
|
+
}
|
|
4173
|
+
});
|
|
4174
|
+
} catch (error) {
|
|
4175
|
+
warnings.push(`Unable to inspect Cursor path ${root}: ${errorMessage4(error)}`);
|
|
4176
|
+
}
|
|
4177
|
+
}
|
|
4178
|
+
return {
|
|
4179
|
+
roots,
|
|
4180
|
+
projectsPath: path8.join(cursorHome, "projects"),
|
|
4181
|
+
globalStoragePath: appPaths.globalStoragePath,
|
|
4182
|
+
workspaceStoragePath: appPaths.workspaceStoragePath,
|
|
4183
|
+
statePath: appPaths.statePath,
|
|
4184
|
+
jsonlFiles: unique5(jsonlFiles).sort(cursorJsonlSort),
|
|
4185
|
+
databaseFiles: unique5(databaseFiles).sort(),
|
|
4186
|
+
skippedFileCount,
|
|
4187
|
+
warnings
|
|
4188
|
+
};
|
|
4189
|
+
}
|
|
4190
|
+
function cursorOwnedGlobalStorageRoots(globalStoragePath) {
|
|
4191
|
+
return cursorOwnedChildDirs(globalStoragePath);
|
|
4192
|
+
}
|
|
4193
|
+
function cursorOwnedWorkspaceStorageRoots(workspaceStoragePath) {
|
|
4194
|
+
if (!fs8.existsSync(workspaceStoragePath)) return [];
|
|
4195
|
+
const roots = [];
|
|
4196
|
+
try {
|
|
4197
|
+
for (const workspace of fs8.readdirSync(workspaceStoragePath, { withFileTypes: true })) {
|
|
4198
|
+
if (!workspace.isDirectory()) continue;
|
|
4199
|
+
roots.push(...cursorOwnedChildDirs(path8.join(workspaceStoragePath, workspace.name)));
|
|
4200
|
+
}
|
|
4201
|
+
} catch {
|
|
4202
|
+
return [];
|
|
4203
|
+
}
|
|
4204
|
+
return roots;
|
|
4205
|
+
}
|
|
4206
|
+
function cursorOwnedChildDirs(root) {
|
|
4207
|
+
if (!fs8.existsSync(root)) return [];
|
|
4208
|
+
try {
|
|
4209
|
+
return fs8.readdirSync(root, { withFileTypes: true }).filter((entry) => entry.isDirectory() && cursorStoragePrefixes.some((prefix) => entry.name.toLowerCase().startsWith(prefix))).map((entry) => path8.join(root, entry.name));
|
|
4210
|
+
} catch {
|
|
2855
4211
|
return [];
|
|
2856
4212
|
}
|
|
2857
4213
|
}
|
|
2858
4214
|
function defaultCursorAppPaths() {
|
|
2859
|
-
const home =
|
|
4215
|
+
const home = os5.homedir();
|
|
2860
4216
|
if (process.platform === "win32") {
|
|
2861
|
-
const appData = process.env.APPDATA ??
|
|
2862
|
-
const globalStoragePath2 =
|
|
4217
|
+
const appData = process.env.APPDATA ?? path8.join(home, "AppData", "Roaming");
|
|
4218
|
+
const globalStoragePath2 = path8.join(appData, "Cursor", "User", "globalStorage");
|
|
2863
4219
|
return {
|
|
2864
4220
|
globalStoragePath: globalStoragePath2,
|
|
2865
|
-
workspaceStoragePath:
|
|
2866
|
-
statePath:
|
|
4221
|
+
workspaceStoragePath: path8.join(appData, "Cursor", "User", "workspaceStorage"),
|
|
4222
|
+
statePath: path8.join(globalStoragePath2, "state.vscdb")
|
|
2867
4223
|
};
|
|
2868
4224
|
}
|
|
2869
4225
|
if (process.platform === "darwin") {
|
|
2870
|
-
const globalStoragePath2 =
|
|
4226
|
+
const globalStoragePath2 = path8.join(home, "Library", "Application Support", "Cursor", "User", "globalStorage");
|
|
2871
4227
|
return {
|
|
2872
4228
|
globalStoragePath: globalStoragePath2,
|
|
2873
|
-
workspaceStoragePath:
|
|
2874
|
-
statePath:
|
|
4229
|
+
workspaceStoragePath: path8.join(home, "Library", "Application Support", "Cursor", "User", "workspaceStorage"),
|
|
4230
|
+
statePath: path8.join(globalStoragePath2, "state.vscdb")
|
|
2875
4231
|
};
|
|
2876
4232
|
}
|
|
2877
|
-
const xdg = process.env.XDG_CONFIG_HOME ||
|
|
2878
|
-
const globalStoragePath =
|
|
4233
|
+
const xdg = process.env.XDG_CONFIG_HOME || path8.join(home, ".config");
|
|
4234
|
+
const globalStoragePath = path8.join(xdg, "Cursor", "User", "globalStorage");
|
|
2879
4235
|
return {
|
|
2880
4236
|
globalStoragePath,
|
|
2881
|
-
workspaceStoragePath:
|
|
2882
|
-
statePath:
|
|
4237
|
+
workspaceStoragePath: path8.join(xdg, "Cursor", "User", "workspaceStorage"),
|
|
4238
|
+
statePath: path8.join(globalStoragePath, "state.vscdb")
|
|
2883
4239
|
};
|
|
2884
4240
|
}
|
|
2885
|
-
function
|
|
4241
|
+
function walk4(root, maxDepth, onFile, depth = 0) {
|
|
2886
4242
|
if (depth > maxDepth) return;
|
|
2887
|
-
for (const entry of
|
|
4243
|
+
for (const entry of fs8.readdirSync(root, { withFileTypes: true })) {
|
|
2888
4244
|
if (entry.name === "node_modules" || entry.name === ".git" || entry.name === "CachedData" || entry.name === "Cache") continue;
|
|
2889
|
-
const fullPath =
|
|
4245
|
+
const fullPath = path8.join(root, entry.name);
|
|
2890
4246
|
if (entry.isDirectory()) {
|
|
2891
|
-
|
|
4247
|
+
walk4(fullPath, maxDepth, onFile, depth + 1);
|
|
2892
4248
|
} else if (entry.isFile()) {
|
|
2893
4249
|
onFile(fullPath);
|
|
2894
4250
|
}
|
|
2895
4251
|
}
|
|
2896
4252
|
}
|
|
2897
|
-
function
|
|
4253
|
+
function isReadableRegularFile2(filePath, maxBytes) {
|
|
2898
4254
|
try {
|
|
2899
|
-
|
|
2900
|
-
const stat =
|
|
4255
|
+
fs8.accessSync(filePath, fs8.constants.R_OK);
|
|
4256
|
+
const stat = fs8.statSync(filePath);
|
|
2901
4257
|
return stat.isFile() && stat.size <= maxBytes;
|
|
2902
4258
|
} catch {
|
|
2903
4259
|
return false;
|
|
2904
4260
|
}
|
|
2905
4261
|
}
|
|
2906
4262
|
function cursorJsonlSort(a, b) {
|
|
2907
|
-
const aTranscript = a.includes(`${
|
|
2908
|
-
const bTranscript = b.includes(`${
|
|
4263
|
+
const aTranscript = a.includes(`${path8.sep}agent-transcripts${path8.sep}`) ? 0 : 1;
|
|
4264
|
+
const bTranscript = b.includes(`${path8.sep}agent-transcripts${path8.sep}`) ? 0 : 1;
|
|
2909
4265
|
return aTranscript - bTranscript || a.localeCompare(b);
|
|
2910
4266
|
}
|
|
4267
|
+
function fileMayOverlapScanWindow3(filePath, scanWindow) {
|
|
4268
|
+
if (!scanWindow?.fromMs && !scanWindow?.toMs) return true;
|
|
4269
|
+
const inferredTimestamp = timestampMs3(inferDateFromCursorPath(filePath));
|
|
4270
|
+
if (scanWindow.fromMs !== void 0 && inferredTimestamp !== void 0 && inferredTimestamp < scanWindow.fromMs) return false;
|
|
4271
|
+
if (scanWindow.toMs !== void 0 && inferredTimestamp !== void 0 && inferredTimestamp > scanWindow.toMs) return false;
|
|
4272
|
+
if (scanWindow.fromMs === void 0) return true;
|
|
4273
|
+
try {
|
|
4274
|
+
return fs8.statSync(filePath).mtimeMs >= scanWindow.fromMs;
|
|
4275
|
+
} catch {
|
|
4276
|
+
return true;
|
|
4277
|
+
}
|
|
4278
|
+
}
|
|
2911
4279
|
function cursorJsonlToCandidate(filePath, cursorHome) {
|
|
2912
|
-
const breakdown =
|
|
4280
|
+
const breakdown = emptyBreakdown4();
|
|
2913
4281
|
try {
|
|
2914
|
-
const parse =
|
|
4282
|
+
const parse = parseJsonLines4(fs8.readFileSync(filePath, "utf8"));
|
|
2915
4283
|
breakdown.rawEventCount = parse.records.length;
|
|
2916
4284
|
breakdown.parseErrors = parse.errors;
|
|
2917
4285
|
breakdown.parseStatus = parse.errors.length ? parse.records.length ? "partial" : "failed" : "ok";
|
|
2918
4286
|
for (const record of parse.records) applyCursorRecord(breakdown, record);
|
|
2919
4287
|
finalizeBreakdown(breakdown);
|
|
2920
4288
|
} catch (error) {
|
|
2921
|
-
const message = `unable_to_read_cursor_jsonl:${
|
|
4289
|
+
const message = `unable_to_read_cursor_jsonl:${errorMessage4(error)}`;
|
|
2922
4290
|
breakdown.parseStatus = "failed";
|
|
2923
4291
|
breakdown.parseErrors.push(message);
|
|
2924
4292
|
breakdown.warnings.push(message);
|
|
2925
4293
|
}
|
|
2926
|
-
if (!breakdown.id) breakdown.id =
|
|
4294
|
+
if (!breakdown.id) breakdown.id = path8.basename(filePath).replace(/\.jsonl$/i, "") || void 0;
|
|
4295
|
+
const fallbackDate = inferDateFromCursorPath(filePath) ?? fileModifiedDate2(filePath);
|
|
4296
|
+
if (fallbackDate) {
|
|
4297
|
+
breakdown.startedAt ??= fallbackDate;
|
|
4298
|
+
breakdown.endedAt ??= fallbackDate;
|
|
4299
|
+
}
|
|
2927
4300
|
return {
|
|
2928
4301
|
sourcePath: filePath,
|
|
2929
4302
|
surface: inferSurfaceFromPath(filePath),
|
|
@@ -2949,7 +4322,7 @@ function cursorDatabaseToCandidates(filePath, cursorHome, warnings) {
|
|
|
2949
4322
|
}
|
|
2950
4323
|
}
|
|
2951
4324
|
} catch (error) {
|
|
2952
|
-
warnings.push(`Unable to read Cursor SQLite database at ${filePath}: ${
|
|
4325
|
+
warnings.push(`Unable to read Cursor SQLite database at ${filePath}: ${errorMessage4(error)}`);
|
|
2953
4326
|
} finally {
|
|
2954
4327
|
db?.close();
|
|
2955
4328
|
}
|
|
@@ -2959,7 +4332,7 @@ function safeColumns(db, table, warnings) {
|
|
|
2959
4332
|
try {
|
|
2960
4333
|
return db.prepare(`PRAGMA table_info(${quoteIdentifier(table)})`).all();
|
|
2961
4334
|
} catch (error) {
|
|
2962
|
-
warnings.push(`Unable to inspect Cursor SQLite table ${table}: ${
|
|
4335
|
+
warnings.push(`Unable to inspect Cursor SQLite table ${table}: ${errorMessage4(error)}`);
|
|
2963
4336
|
return [];
|
|
2964
4337
|
}
|
|
2965
4338
|
}
|
|
@@ -2968,7 +4341,7 @@ function safeRows(db, table, columns, warnings) {
|
|
|
2968
4341
|
const select = columns.map(quoteIdentifier).join(", ");
|
|
2969
4342
|
return db.prepare(`SELECT ${select} FROM ${quoteIdentifier(table)} LIMIT 500`).all();
|
|
2970
4343
|
} catch (error) {
|
|
2971
|
-
warnings.push(`Unable to scan Cursor SQLite table ${table}: ${
|
|
4344
|
+
warnings.push(`Unable to scan Cursor SQLite table ${table}: ${errorMessage4(error)}`);
|
|
2972
4345
|
return [];
|
|
2973
4346
|
}
|
|
2974
4347
|
}
|
|
@@ -2996,11 +4369,11 @@ function candidateFromDatabaseRow(filePath, cursorHome, table, row) {
|
|
|
2996
4369
|
if (!parsed.ok && text.trim().startsWith("{")) parseErrors.push(`sqlite_json_parse:${table}.${key}:${parsed.error}`);
|
|
2997
4370
|
}
|
|
2998
4371
|
if (!records.length) return void 0;
|
|
2999
|
-
const breakdown =
|
|
4372
|
+
const breakdown = emptyBreakdown4();
|
|
3000
4373
|
breakdown.parseErrors = parseErrors;
|
|
3001
4374
|
breakdown.parseStatus = parseErrors.length ? "partial" : "ok";
|
|
3002
4375
|
breakdown.rawEventCount = records.length;
|
|
3003
|
-
breakdown.matchedKeys =
|
|
4376
|
+
breakdown.matchedKeys = unique5(matchedKeys);
|
|
3004
4377
|
for (const record of records) applyCursorRecord(breakdown, record);
|
|
3005
4378
|
finalizeBreakdown(breakdown);
|
|
3006
4379
|
if (isCursorMetadataOnlyBreakdown(breakdown)) return void 0;
|
|
@@ -3025,29 +4398,29 @@ function parseJsonBlob(text) {
|
|
|
3025
4398
|
try {
|
|
3026
4399
|
return { ok: true, value: JSON.parse(trimmed) };
|
|
3027
4400
|
} catch (error) {
|
|
3028
|
-
return { ok: false, error:
|
|
4401
|
+
return { ok: false, error: errorMessage4(error) };
|
|
3029
4402
|
}
|
|
3030
4403
|
}
|
|
3031
|
-
function
|
|
4404
|
+
function parseJsonLines4(text) {
|
|
3032
4405
|
const records = [];
|
|
3033
4406
|
const errors = [];
|
|
3034
4407
|
text.split(/\r?\n/).map((line, index) => ({ line: line.trim(), index })).filter(({ line }) => Boolean(line)).forEach(({ line, index }) => {
|
|
3035
4408
|
try {
|
|
3036
4409
|
records.push(JSON.parse(line));
|
|
3037
4410
|
} catch (error) {
|
|
3038
|
-
errors.push(`jsonl_line_${index + 1}:${
|
|
4411
|
+
errors.push(`jsonl_line_${index + 1}:${errorMessage4(error)}`);
|
|
3039
4412
|
}
|
|
3040
4413
|
});
|
|
3041
4414
|
return { records, errors };
|
|
3042
4415
|
}
|
|
3043
4416
|
function applyCursorRecord(breakdown, record) {
|
|
3044
4417
|
const objects = collectObjects(record, 6, 150);
|
|
3045
|
-
const root =
|
|
4418
|
+
const root = firstObject6(record);
|
|
3046
4419
|
const timestamp3 = firstValueByKeys(objects, ["timestamp", "createdAt", "created_at", "lastUpdatedAt", "conversationCheckpointLastUpdatedAt", "time", "date"]);
|
|
3047
|
-
const date =
|
|
4420
|
+
const date = dateValue5(timestamp3);
|
|
3048
4421
|
if (date) {
|
|
3049
|
-
breakdown.startedAt =
|
|
3050
|
-
breakdown.endedAt =
|
|
4422
|
+
breakdown.startedAt = minDate3(breakdown.startedAt, date);
|
|
4423
|
+
breakdown.endedAt = maxDate3(breakdown.endedAt, date);
|
|
3051
4424
|
}
|
|
3052
4425
|
breakdown.id ??= firstStringByKeys(objects, ["sessionId", "session_id", "conversationId", "conversation_id", "composerId", "composer_id", "chatId", "chat_id"]);
|
|
3053
4426
|
breakdown.title ??= firstStringByKeys(objects, ["title", "name", "summary"]);
|
|
@@ -3092,13 +4465,13 @@ function collectObjects(value, maxDepth, maxObjects, objects = []) {
|
|
|
3092
4465
|
for (const child of Object.values(object)) collectObjects(child, maxDepth - 1, maxObjects, objects);
|
|
3093
4466
|
return objects;
|
|
3094
4467
|
}
|
|
3095
|
-
function
|
|
4468
|
+
function firstObject6(value) {
|
|
3096
4469
|
return value && typeof value === "object" && !Array.isArray(value) ? value : void 0;
|
|
3097
4470
|
}
|
|
3098
4471
|
function firstStringByKeys(objects, keys) {
|
|
3099
4472
|
for (const object of objects) {
|
|
3100
4473
|
for (const key of keys) {
|
|
3101
|
-
const value =
|
|
4474
|
+
const value = stringValue5(object[key]);
|
|
3102
4475
|
if (value) return value;
|
|
3103
4476
|
}
|
|
3104
4477
|
}
|
|
@@ -3121,7 +4494,7 @@ function firstPathByKeys(objects, keys) {
|
|
|
3121
4494
|
function firstNumberByKeys(objects, keys) {
|
|
3122
4495
|
for (const object of objects) {
|
|
3123
4496
|
for (const key of keys) {
|
|
3124
|
-
const value =
|
|
4497
|
+
const value = numberValue3(object[key]);
|
|
3125
4498
|
if (value !== void 0) return value;
|
|
3126
4499
|
}
|
|
3127
4500
|
}
|
|
@@ -3131,7 +4504,7 @@ function inferRole(objects, root) {
|
|
|
3131
4504
|
const role = firstStringByKeys(objects, ["role", "author", "speaker", "from"])?.toLowerCase();
|
|
3132
4505
|
if (role === "user" || role === "human") return "user";
|
|
3133
4506
|
if (role === "assistant" || role === "ai" || role === "agent") return "assistant";
|
|
3134
|
-
const type = (
|
|
4507
|
+
const type = (stringValue5(root?.type) ?? stringValue5(root?.event) ?? "").toLowerCase();
|
|
3135
4508
|
if (type.includes("user") || type.includes("prompt")) return "user";
|
|
3136
4509
|
if (type.includes("assistant") || type.includes("response") || type.includes("completion")) return "assistant";
|
|
3137
4510
|
return void 0;
|
|
@@ -3152,9 +4525,9 @@ function textFromUnknown(value) {
|
|
|
3152
4525
|
const parts = value.map(textFromUnknown).filter((item) => Boolean(item));
|
|
3153
4526
|
return parts.length ? parts.join("\n") : void 0;
|
|
3154
4527
|
}
|
|
3155
|
-
const object =
|
|
4528
|
+
const object = firstObject6(value);
|
|
3156
4529
|
if (!object) return void 0;
|
|
3157
|
-
return
|
|
4530
|
+
return stringValue5(object.text) ?? stringValue5(object.content) ?? stringValue5(object.value);
|
|
3158
4531
|
}
|
|
3159
4532
|
function extractUsage(objects) {
|
|
3160
4533
|
const usageObjects = objects.filter((object) => Object.keys(object).some((key) => key.toLowerCase().includes("token") || key.toLowerCase() === "usage"));
|
|
@@ -3169,7 +4542,7 @@ function extractUsage(objects) {
|
|
|
3169
4542
|
};
|
|
3170
4543
|
}
|
|
3171
4544
|
function applyToolSignals(breakdown, objects) {
|
|
3172
|
-
const typeText = objects.flatMap((object) => [
|
|
4545
|
+
const typeText = objects.flatMap((object) => [stringValue5(object.type), stringValue5(object.event), stringValue5(object.toolName), stringValue5(object.tool_name), stringValue5(object.name)]).filter((item) => Boolean(item)).join(" ").toLowerCase();
|
|
3173
4546
|
if (typeText.includes("tool")) breakdown.toolCallCount += 1;
|
|
3174
4547
|
if (typeText.includes("terminal") || typeText.includes("shell") || typeText.includes("command")) breakdown.shellCommandCount += 1;
|
|
3175
4548
|
if (typeText.includes("read")) breakdown.fileReadCount += 1;
|
|
@@ -3178,9 +4551,9 @@ function applyToolSignals(breakdown, objects) {
|
|
|
3178
4551
|
}
|
|
3179
4552
|
function looksFailed(object) {
|
|
3180
4553
|
if (object.isError === true || object.is_error === true || object.error === true) return true;
|
|
3181
|
-
const status =
|
|
4554
|
+
const status = stringValue5(object.status)?.toLowerCase();
|
|
3182
4555
|
if (status === "failed" || status === "error") return true;
|
|
3183
|
-
const exitCode =
|
|
4556
|
+
const exitCode = numberValue3(object.exitCode ?? object.exit_code ?? object.statusCode ?? object.status_code);
|
|
3184
4557
|
return exitCode !== void 0 && exitCode !== 0;
|
|
3185
4558
|
}
|
|
3186
4559
|
function finalizeBreakdown(breakdown) {
|
|
@@ -3193,7 +4566,7 @@ function finalizeBreakdown(breakdown) {
|
|
|
3193
4566
|
}
|
|
3194
4567
|
function candidateToUsage(candidate, index, options) {
|
|
3195
4568
|
const breakdown = candidate.breakdown;
|
|
3196
|
-
const id = breakdown.id ?? `${
|
|
4569
|
+
const id = breakdown.id ?? `${path8.basename(candidate.sourcePath).replace(/\.(jsonl|sqlite|db|vscdb)$/i, "") || "cursor-session"}-${index}`;
|
|
3197
4570
|
const cwd = breakdown.cwd ?? candidate.fallbackCwd ?? process.cwd();
|
|
3198
4571
|
const repo = resolveRepoInfo(cwd, options.config);
|
|
3199
4572
|
const hasTokenBreakdown = breakdown.inputTokens + breakdown.cachedInputTokens + breakdown.outputTokens + breakdown.reasoningTokens > 0;
|
|
@@ -3228,7 +4601,7 @@ function candidateToUsage(candidate, index, options) {
|
|
|
3228
4601
|
messageCount: breakdown.messageCount,
|
|
3229
4602
|
rawTokenTotal: breakdown.totalTokens || void 0,
|
|
3230
4603
|
warnings: [...repo.warnings, ...breakdown.warnings, ...tokenWarnings, ...hasTokenBreakdown ? [] : ["missing_token_breakdown"]],
|
|
3231
|
-
durationMs:
|
|
4604
|
+
durationMs: durationMs4(breakdown.startedAt, breakdown.endedAt),
|
|
3232
4605
|
rawEventCount: breakdown.rawEventCount,
|
|
3233
4606
|
parseStatus: breakdown.parseStatus,
|
|
3234
4607
|
parseErrors: breakdown.parseErrors,
|
|
@@ -3272,21 +4645,81 @@ function candidateToUsage(candidate, index, options) {
|
|
|
3272
4645
|
};
|
|
3273
4646
|
}
|
|
3274
4647
|
function inferProjectPathFromCursorPath(filePath, cursorHome) {
|
|
3275
|
-
const parts = filePath.split(
|
|
4648
|
+
const parts = filePath.split(path8.sep);
|
|
3276
4649
|
const projectsIndex = parts.lastIndexOf("projects");
|
|
3277
4650
|
if (projectsIndex >= 0 && projectsIndex < parts.length - 1) {
|
|
3278
4651
|
const projectId = parts[projectsIndex + 1];
|
|
4652
|
+
const decodedProjectPath = decodeCursorProjectId(projectId);
|
|
4653
|
+
if (decodedProjectPath) return decodedProjectPath;
|
|
3279
4654
|
if (projectId?.startsWith("-")) {
|
|
3280
|
-
return `${
|
|
4655
|
+
return `${path8.sep}${projectId.slice(1).split("-").filter(Boolean).join(path8.sep)}`;
|
|
4656
|
+
}
|
|
4657
|
+
if (projectId) return path8.join(cursorHome, "projects", projectId);
|
|
4658
|
+
}
|
|
4659
|
+
return void 0;
|
|
4660
|
+
}
|
|
4661
|
+
function inferDateFromCursorPath(filePath) {
|
|
4662
|
+
const parts = filePath.split(path8.sep);
|
|
4663
|
+
for (const part of parts) {
|
|
4664
|
+
if (!/^\d{10,13}$/.test(part)) continue;
|
|
4665
|
+
const date = dateValue5(Number(part));
|
|
4666
|
+
if (date) return date;
|
|
4667
|
+
}
|
|
4668
|
+
return void 0;
|
|
4669
|
+
}
|
|
4670
|
+
function timestampMs3(value) {
|
|
4671
|
+
if (!value) return void 0;
|
|
4672
|
+
const timestamp3 = new Date(value).getTime();
|
|
4673
|
+
return Number.isFinite(timestamp3) ? timestamp3 : void 0;
|
|
4674
|
+
}
|
|
4675
|
+
function fileModifiedDate2(filePath) {
|
|
4676
|
+
try {
|
|
4677
|
+
return fs8.statSync(filePath).mtime.toISOString();
|
|
4678
|
+
} catch {
|
|
4679
|
+
return void 0;
|
|
4680
|
+
}
|
|
4681
|
+
}
|
|
4682
|
+
function decodeCursorProjectId(projectId) {
|
|
4683
|
+
if (!projectId) return void 0;
|
|
4684
|
+
const id = projectId.startsWith("-") ? projectId.slice(1) : projectId;
|
|
4685
|
+
const tokens2 = id.split("-").filter(Boolean);
|
|
4686
|
+
if (tokens2.length < 2) return void 0;
|
|
4687
|
+
const root = cursorProjectRoot(tokens2);
|
|
4688
|
+
if (!root) return void 0;
|
|
4689
|
+
const matched = matchEncodedPath(root.path, tokens2.slice(root.consumed));
|
|
4690
|
+
if (matched) return matched;
|
|
4691
|
+
return path8.join(root.path, ...tokens2.slice(root.consumed));
|
|
4692
|
+
}
|
|
4693
|
+
function cursorProjectRoot(tokens2) {
|
|
4694
|
+
if (tokens2[0] === "Users" && tokens2[1]) return { path: path8.join(path8.sep, "Users", tokens2[1]), consumed: 2 };
|
|
4695
|
+
if (tokens2[0] === "home" && tokens2[1]) return { path: path8.join(path8.sep, "home", tokens2[1]), consumed: 2 };
|
|
4696
|
+
if (tokens2[0] === "tmp") return { path: path8.join(path8.sep, "tmp"), consumed: 1 };
|
|
4697
|
+
if (/^[A-Za-z]$/.test(tokens2[0] ?? "") && tokens2[1]) return { path: `${tokens2[0]?.toUpperCase()}:\\`, consumed: 1 };
|
|
4698
|
+
return void 0;
|
|
4699
|
+
}
|
|
4700
|
+
function matchEncodedPath(root, tokens2) {
|
|
4701
|
+
if (!tokens2.length) return fs8.existsSync(root) ? root : void 0;
|
|
4702
|
+
if (!fs8.existsSync(root)) return void 0;
|
|
4703
|
+
try {
|
|
4704
|
+
for (const entry of fs8.readdirSync(root, { withFileTypes: true })) {
|
|
4705
|
+
if (!entry.isDirectory()) continue;
|
|
4706
|
+
const entryTokens = entry.name.split("-").filter(Boolean);
|
|
4707
|
+
if (!startsWithTokens(tokens2, entryTokens)) continue;
|
|
4708
|
+
const matched = matchEncodedPath(path8.join(root, entry.name), tokens2.slice(entryTokens.length));
|
|
4709
|
+
if (matched) return matched;
|
|
3281
4710
|
}
|
|
3282
|
-
|
|
4711
|
+
} catch {
|
|
4712
|
+
return void 0;
|
|
3283
4713
|
}
|
|
3284
4714
|
return void 0;
|
|
3285
4715
|
}
|
|
4716
|
+
function startsWithTokens(tokens2, prefix) {
|
|
4717
|
+
return prefix.length <= tokens2.length && prefix.every((token, index) => tokens2[index] === token);
|
|
4718
|
+
}
|
|
3286
4719
|
function inferSurfaceFromPath(filePath) {
|
|
3287
4720
|
const lower = filePath.toLowerCase();
|
|
3288
|
-
if (lower.includes(`${
|
|
3289
|
-
if (lower.includes(`${
|
|
4721
|
+
if (lower.includes(`${path8.sep}agent-transcripts${path8.sep}`) || lower.endsWith(".jsonl")) return "cli";
|
|
4722
|
+
if (lower.includes(`${path8.sep}workspaceStorage${path8.sep}`.toLowerCase()) || lower.includes(`${path8.sep}globalStorage${path8.sep}`.toLowerCase()) || lower.endsWith(".vscdb")) return "desktop";
|
|
3290
4723
|
return "unknown";
|
|
3291
4724
|
}
|
|
3292
4725
|
function cursorSourceAppLabel(surface) {
|
|
@@ -3312,7 +4745,7 @@ function inferOutcome3(breakdown) {
|
|
|
3312
4745
|
if (breakdown.userPromptCount > 0 || breakdown.assistantMessageCount > 0) return "research_only";
|
|
3313
4746
|
return "unknown";
|
|
3314
4747
|
}
|
|
3315
|
-
function
|
|
4748
|
+
function emptyBreakdown4() {
|
|
3316
4749
|
return {
|
|
3317
4750
|
id: void 0,
|
|
3318
4751
|
title: void 0,
|
|
@@ -3395,10 +4828,10 @@ function mergeCursorBreakdowns(primary, secondary) {
|
|
|
3395
4828
|
fileEditCount: Math.max(primary.fileEditCount, secondary.fileEditCount),
|
|
3396
4829
|
rawEventCount: Math.max(primary.rawEventCount, secondary.rawEventCount),
|
|
3397
4830
|
parseStatus: mergeParseStatus(primary.parseStatus, secondary.parseStatus),
|
|
3398
|
-
parseErrors:
|
|
4831
|
+
parseErrors: unique5([...primary.parseErrors, ...secondary.parseErrors]),
|
|
3399
4832
|
promptTimeline: primary.promptTimeline.length >= secondary.promptTimeline.length ? primary.promptTimeline : secondary.promptTimeline,
|
|
3400
|
-
warnings:
|
|
3401
|
-
matchedKeys:
|
|
4833
|
+
warnings: unique5([...primary.warnings, ...secondary.warnings, "cursor_duplicate_session_merged"]),
|
|
4834
|
+
matchedKeys: unique5([...primary.matchedKeys, ...secondary.matchedKeys])
|
|
3402
4835
|
};
|
|
3403
4836
|
}
|
|
3404
4837
|
function mergeParseStatus(a, b) {
|
|
@@ -3407,10 +4840,10 @@ function mergeParseStatus(a, b) {
|
|
|
3407
4840
|
return "ok";
|
|
3408
4841
|
}
|
|
3409
4842
|
function optionalMinDate(current, candidate) {
|
|
3410
|
-
return candidate ?
|
|
4843
|
+
return candidate ? minDate3(current, candidate) : current;
|
|
3411
4844
|
}
|
|
3412
4845
|
function optionalMaxDate(current, candidate) {
|
|
3413
|
-
return candidate ?
|
|
4846
|
+
return candidate ? maxDate3(current, candidate) : current;
|
|
3414
4847
|
}
|
|
3415
4848
|
function providerForModel2(model) {
|
|
3416
4849
|
const normalized = model.toLowerCase();
|
|
@@ -3419,7 +4852,7 @@ function providerForModel2(model) {
|
|
|
3419
4852
|
if (normalized.startsWith("gpt") || normalized.startsWith("o")) return "openai";
|
|
3420
4853
|
return void 0;
|
|
3421
4854
|
}
|
|
3422
|
-
function
|
|
4855
|
+
function stringValue5(value) {
|
|
3423
4856
|
return typeof value === "string" && value.trim() ? value : void 0;
|
|
3424
4857
|
}
|
|
3425
4858
|
function textValue(value) {
|
|
@@ -3427,24 +4860,24 @@ function textValue(value) {
|
|
|
3427
4860
|
if (Buffer.isBuffer(value)) return value.toString("utf8");
|
|
3428
4861
|
return void 0;
|
|
3429
4862
|
}
|
|
3430
|
-
function
|
|
4863
|
+
function numberValue3(value) {
|
|
3431
4864
|
if (typeof value === "number" && Number.isFinite(value)) return value;
|
|
3432
4865
|
if (typeof value === "string" && value.trim() && Number.isFinite(Number(value))) return Number(value);
|
|
3433
4866
|
return void 0;
|
|
3434
4867
|
}
|
|
3435
|
-
function
|
|
4868
|
+
function dateValue5(value) {
|
|
3436
4869
|
if (typeof value === "number" && Number.isFinite(value)) return new Date(value < 1e10 ? value * 1e3 : value).toISOString();
|
|
3437
4870
|
if (typeof value !== "string") return void 0;
|
|
3438
4871
|
const date = new Date(value);
|
|
3439
4872
|
return Number.isNaN(date.getTime()) ? void 0 : date.toISOString();
|
|
3440
4873
|
}
|
|
3441
|
-
function
|
|
4874
|
+
function minDate3(current, candidate) {
|
|
3442
4875
|
return !current || candidate < current ? candidate : current;
|
|
3443
4876
|
}
|
|
3444
|
-
function
|
|
4877
|
+
function maxDate3(current, candidate) {
|
|
3445
4878
|
return !current || candidate > current ? candidate : current;
|
|
3446
4879
|
}
|
|
3447
|
-
function
|
|
4880
|
+
function durationMs4(startedAt, endedAt) {
|
|
3448
4881
|
if (!startedAt || !endedAt) return void 0;
|
|
3449
4882
|
const start = new Date(startedAt).getTime();
|
|
3450
4883
|
const end = new Date(endedAt).getTime();
|
|
@@ -3456,64 +4889,123 @@ function normalizeTimelineText3(text) {
|
|
|
3456
4889
|
function roundCurrency2(value) {
|
|
3457
4890
|
return Number(value.toFixed(6));
|
|
3458
4891
|
}
|
|
3459
|
-
function
|
|
4892
|
+
function unique5(values) {
|
|
3460
4893
|
return [...new Set(values.filter(Boolean))];
|
|
3461
4894
|
}
|
|
3462
|
-
function
|
|
4895
|
+
function errorMessage4(error) {
|
|
3463
4896
|
return error instanceof Error ? error.message : String(error);
|
|
3464
4897
|
}
|
|
3465
4898
|
|
|
3466
4899
|
// packages/core/src/sources/index.ts
|
|
4900
|
+
var codexAdapter = { id: "codex", scan: scanCodex };
|
|
4901
|
+
var claudeAdapter = { id: "claude", scan: scanClaude };
|
|
4902
|
+
var copilotAdapter = { id: "copilot", scan: scanCopilot };
|
|
4903
|
+
var cursorAdapter = { id: "cursor", scan: scanCursor };
|
|
4904
|
+
var adapters = [
|
|
4905
|
+
{ adapter: codexAdapter, homeKey: "codexHome", homeOption: "codexHome" },
|
|
4906
|
+
{ adapter: claudeAdapter, homeKey: "claudeHome", homeOption: "claudeHome" },
|
|
4907
|
+
{ adapter: copilotAdapter, homeKey: "copilotHome", homeOption: "copilotHome" },
|
|
4908
|
+
{
|
|
4909
|
+
adapter: cursorAdapter,
|
|
4910
|
+
homeKey: "cursorHome",
|
|
4911
|
+
homeOption: "cursorHome",
|
|
4912
|
+
enabled: (options) => options.config?.experimentalSources?.cursor === true || Boolean(options.cursorHome)
|
|
4913
|
+
}
|
|
4914
|
+
];
|
|
3467
4915
|
function scanUsageSources(options) {
|
|
3468
|
-
const
|
|
3469
|
-
|
|
3470
|
-
|
|
3471
|
-
|
|
3472
|
-
|
|
3473
|
-
|
|
3474
|
-
|
|
3475
|
-
|
|
3476
|
-
|
|
3477
|
-
|
|
3478
|
-
|
|
3479
|
-
|
|
3480
|
-
|
|
3481
|
-
|
|
3482
|
-
|
|
3483
|
-
|
|
3484
|
-
|
|
3485
|
-
sourceStats: [codex.stats, claude.stats, cursor.stats],
|
|
3486
|
-
sessions: [...codex.sessions, ...claude.sessions, ...cursor.sessions]
|
|
3487
|
-
};
|
|
4916
|
+
const sources = [];
|
|
4917
|
+
const sourceStats = [];
|
|
4918
|
+
const sessions = [];
|
|
4919
|
+
for (const entry of adapters) {
|
|
4920
|
+
if (entry.enabled && !entry.enabled(options)) continue;
|
|
4921
|
+
const home = options[entry.homeKey];
|
|
4922
|
+
const result = entry.adapter.scan({
|
|
4923
|
+
...home ? { [entry.homeOption]: home } : {},
|
|
4924
|
+
...options.config ? { config: options.config } : {},
|
|
4925
|
+
...options.scanWindow ? { scanWindow: options.scanWindow } : {},
|
|
4926
|
+
pricing: options.pricing
|
|
4927
|
+
});
|
|
4928
|
+
sources.push(result.source);
|
|
4929
|
+
sourceStats.push(result.stats);
|
|
4930
|
+
sessions.push(...result.sessions);
|
|
4931
|
+
}
|
|
4932
|
+
return { sources, sourceStats, sessions };
|
|
3488
4933
|
}
|
|
3489
4934
|
|
|
3490
4935
|
// apps/server/src/data.ts
|
|
3491
|
-
import
|
|
4936
|
+
import fs9 from "node:fs";
|
|
4937
|
+
import path9 from "node:path";
|
|
4938
|
+
import { spawn } from "node:child_process";
|
|
4939
|
+
import { fileURLToPath as fileURLToPath2 } from "node:url";
|
|
3492
4940
|
var scanCacheTtlMs = 1e4;
|
|
3493
4941
|
var scanCache;
|
|
4942
|
+
var warmScanInFlight = false;
|
|
4943
|
+
var warmScanChild;
|
|
4944
|
+
var warmedScanKeys = /* @__PURE__ */ new Set();
|
|
3494
4945
|
function readDashboardData(filters = {}, options = {}) {
|
|
3495
|
-
const raw = readRawUsageData();
|
|
4946
|
+
const raw = readRawUsageData(filters);
|
|
3496
4947
|
return buildDashboardSnapshot({ ...raw, sessions: displaySessions(raw.sessions, options), filters });
|
|
3497
4948
|
}
|
|
3498
4949
|
function clearScanCache() {
|
|
3499
4950
|
scanCache = void 0;
|
|
4951
|
+
warmScanInFlight = false;
|
|
4952
|
+
warmScanChild?.kill();
|
|
4953
|
+
warmScanChild = void 0;
|
|
4954
|
+
warmedScanKeys.clear();
|
|
3500
4955
|
}
|
|
3501
|
-
function readRawUsageData() {
|
|
4956
|
+
function readRawUsageData(filters = {}) {
|
|
3502
4957
|
const cwd = runtimeCwd();
|
|
3503
|
-
|
|
4958
|
+
const { config, warnings: configWarnings } = loadConfigWithWarnings();
|
|
4959
|
+
const cacheKey = scanCacheKey({ cwd, filters, cursorEnabled: config.experimentalSources?.cursor === true });
|
|
4960
|
+
if (scanCache && scanCache.key === cacheKey && scanCache.expiresAt > Date.now()) {
|
|
3504
4961
|
return scanCache.data;
|
|
3505
4962
|
}
|
|
3506
|
-
const { config, warnings: configWarnings } = loadConfigWithWarnings();
|
|
3507
4963
|
const pricing = loadPricingTable(resolvePricingPath(config));
|
|
3508
|
-
const
|
|
4964
|
+
const scanWindow = scanWindowFromFilters(filters);
|
|
4965
|
+
const scan = scanUsageSources({
|
|
4966
|
+
config,
|
|
4967
|
+
pricing,
|
|
4968
|
+
...scanWindow ? { scanWindow } : {}
|
|
4969
|
+
});
|
|
3509
4970
|
const data = {
|
|
3510
4971
|
sources: scan.sources.map((source) => ({ ...source, warnings: [...source.warnings, ...configWarnings] })),
|
|
3511
4972
|
sourceStats: scan.sourceStats,
|
|
3512
4973
|
sessions: scan.sessions
|
|
3513
4974
|
};
|
|
3514
|
-
scanCache = {
|
|
4975
|
+
scanCache = { key: cacheKey, data, expiresAt: Date.now() + scanCacheTtlMs };
|
|
3515
4976
|
return data;
|
|
3516
4977
|
}
|
|
4978
|
+
function warmDashboardCache(filters = {}, options = {}) {
|
|
4979
|
+
const cwd = runtimeCwd();
|
|
4980
|
+
const config = loadConfig();
|
|
4981
|
+
const key = scanCacheKey({ cwd, filters, cursorEnabled: config.experimentalSources?.cursor === true });
|
|
4982
|
+
if (scanCache?.key === key || warmedScanKeys.has(key)) return { started: false, key, reason: "already_warm" };
|
|
4983
|
+
if (warmScanInFlight) return { started: false, key, reason: "warm_scan_in_flight" };
|
|
4984
|
+
warmScanInFlight = true;
|
|
4985
|
+
let child;
|
|
4986
|
+
try {
|
|
4987
|
+
child = spawn(process.execPath, [...process.execArgv, cliEntryPath(), "__warm-cache", ...warmCacheArgs(filters, options)], {
|
|
4988
|
+
detached: true,
|
|
4989
|
+
env: process.env,
|
|
4990
|
+
stdio: "ignore"
|
|
4991
|
+
});
|
|
4992
|
+
} catch (error) {
|
|
4993
|
+
warmScanInFlight = false;
|
|
4994
|
+
return { started: false, key, reason: error instanceof Error ? error.message : String(error) };
|
|
4995
|
+
}
|
|
4996
|
+
warmScanChild = child;
|
|
4997
|
+
child.once("error", () => {
|
|
4998
|
+
warmScanInFlight = false;
|
|
4999
|
+
if (warmScanChild === child) warmScanChild = void 0;
|
|
5000
|
+
});
|
|
5001
|
+
child.once("exit", (code) => {
|
|
5002
|
+
warmScanInFlight = false;
|
|
5003
|
+
if (warmScanChild === child) warmScanChild = void 0;
|
|
5004
|
+
if (code === 0) warmedScanKeys.add(key);
|
|
5005
|
+
});
|
|
5006
|
+
child.unref();
|
|
5007
|
+
return { started: true, key };
|
|
5008
|
+
}
|
|
3517
5009
|
function parseFilters(query) {
|
|
3518
5010
|
const filters = {};
|
|
3519
5011
|
const source = listQuery(query.source);
|
|
@@ -3543,6 +5035,20 @@ function readPricingData() {
|
|
|
3543
5035
|
models: loadPricingTable(pricingPath)
|
|
3544
5036
|
};
|
|
3545
5037
|
}
|
|
5038
|
+
function readConfigData() {
|
|
5039
|
+
return {
|
|
5040
|
+
path: configPath(),
|
|
5041
|
+
config: loadConfig()
|
|
5042
|
+
};
|
|
5043
|
+
}
|
|
5044
|
+
function writeConfigData(nextConfig) {
|
|
5045
|
+
const saved = saveConfig(nextConfig);
|
|
5046
|
+
clearScanCache();
|
|
5047
|
+
return {
|
|
5048
|
+
path: configPath(),
|
|
5049
|
+
config: saved
|
|
5050
|
+
};
|
|
5051
|
+
}
|
|
3546
5052
|
function writePricingData(models) {
|
|
3547
5053
|
const config = loadConfig();
|
|
3548
5054
|
const pricingPath = resolvePricingPath(config);
|
|
@@ -3555,16 +5061,21 @@ function writePricingData(models) {
|
|
|
3555
5061
|
}
|
|
3556
5062
|
function clearRepoSpendLocalData() {
|
|
3557
5063
|
const target = repospendHome();
|
|
3558
|
-
const removed =
|
|
3559
|
-
|
|
5064
|
+
const removed = fs9.existsSync(target);
|
|
5065
|
+
fs9.rmSync(target, { recursive: true, force: true });
|
|
3560
5066
|
clearScanCache();
|
|
3561
5067
|
return { path: target, removed };
|
|
3562
5068
|
}
|
|
3563
|
-
function
|
|
3564
|
-
|
|
5069
|
+
function clearRepoSpendParseCache() {
|
|
5070
|
+
const result = clearRepoSpendCache();
|
|
5071
|
+
clearScanCache();
|
|
5072
|
+
return result;
|
|
5073
|
+
}
|
|
5074
|
+
function exportJson(filters = {}, options = {}) {
|
|
5075
|
+
return JSON.stringify(readDashboardData(filters, options).sessions, null, 2);
|
|
3565
5076
|
}
|
|
3566
|
-
function exportCsv() {
|
|
3567
|
-
return toCsv(readDashboardData().sessions);
|
|
5077
|
+
function exportCsv(filters = {}, options = {}) {
|
|
5078
|
+
return toCsv(readDashboardData(filters, options).sessions);
|
|
3568
5079
|
}
|
|
3569
5080
|
function stringQuery(value) {
|
|
3570
5081
|
return typeof value === "string" && value.length > 0 ? value : void 0;
|
|
@@ -3584,9 +5095,39 @@ function displaySessions(sessions, options) {
|
|
|
3584
5095
|
function sourceScopedAppLabel(session) {
|
|
3585
5096
|
const app = session.sourceApp || fallbackSurfaceLabel(session.detectedSurface);
|
|
3586
5097
|
if (!app || app === "Unknown") return sourceClientLabel(session.sourceClient);
|
|
5098
|
+
if (session.sourceClient === "copilot" && app === "Copilot CLI") return "GitHub Copilot CLI";
|
|
3587
5099
|
if (!shouldScopeApp(app)) return app;
|
|
3588
5100
|
return `${sourceClientLabel(session.sourceClient)} on ${app}`;
|
|
3589
5101
|
}
|
|
5102
|
+
function scanWindowFromFilters(filters) {
|
|
5103
|
+
const fromMs = filters.from ? lowerBound(filters.from) : void 0;
|
|
5104
|
+
const toMs = filters.to ? upperBound(filters.to) : void 0;
|
|
5105
|
+
const scanWindow = {};
|
|
5106
|
+
if (fromMs !== void 0) scanWindow.fromMs = fromMs;
|
|
5107
|
+
if (toMs !== void 0) scanWindow.toMs = toMs;
|
|
5108
|
+
return scanWindow.fromMs === void 0 && scanWindow.toMs === void 0 ? void 0 : scanWindow;
|
|
5109
|
+
}
|
|
5110
|
+
function scanCacheKey({ cwd, filters, cursorEnabled }) {
|
|
5111
|
+
return JSON.stringify({
|
|
5112
|
+
cwd,
|
|
5113
|
+
from: filters.from ?? "",
|
|
5114
|
+
to: filters.to ?? "",
|
|
5115
|
+
cursor: cursorEnabled
|
|
5116
|
+
});
|
|
5117
|
+
}
|
|
5118
|
+
function warmCacheArgs(filters, options) {
|
|
5119
|
+
const args2 = [];
|
|
5120
|
+
if (filters.from) args2.push("--from", filters.from);
|
|
5121
|
+
if (filters.to) args2.push("--to", filters.to);
|
|
5122
|
+
if (options.splitSourceApps) args2.push("--splitSourceApps");
|
|
5123
|
+
return args2;
|
|
5124
|
+
}
|
|
5125
|
+
function cliEntryPath() {
|
|
5126
|
+
const currentDir = path9.dirname(fileURLToPath2(import.meta.url));
|
|
5127
|
+
const sourceCli = path9.join(currentDir, "cli.ts");
|
|
5128
|
+
if (fs9.existsSync(sourceCli)) return sourceCli;
|
|
5129
|
+
return path9.join(currentDir, "cli.js");
|
|
5130
|
+
}
|
|
3590
5131
|
function shouldScopeApp(app) {
|
|
3591
5132
|
const normalized = app.toLowerCase();
|
|
3592
5133
|
return normalized.includes("vs code") || normalized.includes("vscode") || normalized.includes("terminal") || normalized.includes("cli");
|
|
@@ -3594,6 +5135,7 @@ function shouldScopeApp(app) {
|
|
|
3594
5135
|
function sourceClientLabel(source) {
|
|
3595
5136
|
if (source === "codex") return "Codex";
|
|
3596
5137
|
if (source === "claude") return "Claude Code";
|
|
5138
|
+
if (source === "copilot") return "GitHub Copilot";
|
|
3597
5139
|
if (source === "gemini-cli") return "Gemini CLI";
|
|
3598
5140
|
if (source === "opencode") return "OpenCode";
|
|
3599
5141
|
if (source === "cursor") return "Cursor";
|
|
@@ -3611,14 +5153,64 @@ function runtimeCwd() {
|
|
|
3611
5153
|
return process.env.INIT_CWD || process.cwd();
|
|
3612
5154
|
}
|
|
3613
5155
|
|
|
5156
|
+
// apps/server/src/cli-options.ts
|
|
5157
|
+
function parseCliFilters(args2) {
|
|
5158
|
+
const filters = {};
|
|
5159
|
+
const source = listOption(args2, ["--source"]).filter((value) => value !== "all");
|
|
5160
|
+
const sourceApp = listOption(args2, ["--sourceApp", "--source-app", "--app"]);
|
|
5161
|
+
const repo = listOption(args2, ["--repo"]);
|
|
5162
|
+
const model = listOption(args2, ["--model"]);
|
|
5163
|
+
const from = valueAfter(args2, "--from");
|
|
5164
|
+
const to = valueAfter(args2, "--to");
|
|
5165
|
+
if (source.length) filters.source = source;
|
|
5166
|
+
if (sourceApp.length) filters.sourceApp = sourceApp;
|
|
5167
|
+
if (repo.length) filters.repo = repo;
|
|
5168
|
+
if (model.length) filters.model = model;
|
|
5169
|
+
if (from) filters.from = from;
|
|
5170
|
+
if (to) filters.to = to;
|
|
5171
|
+
return filters;
|
|
5172
|
+
}
|
|
5173
|
+
function parseCliDateFilters(args2) {
|
|
5174
|
+
const filters = {};
|
|
5175
|
+
const from = valueAfter(args2, "--from");
|
|
5176
|
+
const to = valueAfter(args2, "--to");
|
|
5177
|
+
if (from) filters.from = from;
|
|
5178
|
+
if (to) filters.to = to;
|
|
5179
|
+
return filters;
|
|
5180
|
+
}
|
|
5181
|
+
function parseCliDashboardOptions(args2) {
|
|
5182
|
+
return {
|
|
5183
|
+
splitSourceApps: args2.includes("--splitSourceApps") || args2.includes("--split-source-apps")
|
|
5184
|
+
};
|
|
5185
|
+
}
|
|
5186
|
+
function valueAfter(args2, flag) {
|
|
5187
|
+
const index = args2.indexOf(flag);
|
|
5188
|
+
const value = index >= 0 ? args2[index + 1] : void 0;
|
|
5189
|
+
return value && !value.startsWith("--") ? value : void 0;
|
|
5190
|
+
}
|
|
5191
|
+
function listOption(args2, flags) {
|
|
5192
|
+
const values = [];
|
|
5193
|
+
for (const flag of flags) {
|
|
5194
|
+
args2.forEach((arg, index) => {
|
|
5195
|
+
if (arg === flag) {
|
|
5196
|
+
const value = args2[index + 1];
|
|
5197
|
+
if (value && !value.startsWith("--")) values.push(value);
|
|
5198
|
+
} else if (arg.startsWith(`${flag}=`)) {
|
|
5199
|
+
values.push(arg.slice(flag.length + 1));
|
|
5200
|
+
}
|
|
5201
|
+
});
|
|
5202
|
+
}
|
|
5203
|
+
return [...new Set(values.flatMap((value) => value.split(",")).map((value) => value.trim()).filter(Boolean))];
|
|
5204
|
+
}
|
|
5205
|
+
|
|
3614
5206
|
// apps/server/src/server.ts
|
|
3615
|
-
import
|
|
3616
|
-
import
|
|
3617
|
-
import { fileURLToPath as
|
|
5207
|
+
import fs10 from "node:fs";
|
|
5208
|
+
import path10 from "node:path";
|
|
5209
|
+
import { fileURLToPath as fileURLToPath3 } from "node:url";
|
|
3618
5210
|
import fastifyStatic from "@fastify/static";
|
|
3619
5211
|
import Fastify from "fastify";
|
|
3620
5212
|
|
|
3621
|
-
// apps/server/src/demo
|
|
5213
|
+
// apps/server/src/demo/fixtures.ts
|
|
3622
5214
|
var baseDate = "2026-05-18";
|
|
3623
5215
|
var demoPricing = {
|
|
3624
5216
|
...defaultPricing,
|
|
@@ -3626,68 +5218,14 @@ var demoPricing = {
|
|
|
3626
5218
|
"gpt-5.3-codex-spark": { inputPerMillion: 1.75, cachedInputPerMillion: 0.175, outputPerMillion: 14, reasoningOutputPerMillion: 14 },
|
|
3627
5219
|
"claude-haiku-4-5-20251001": { inputPerMillion: 1, cacheCreationInputPerMillion: 1.25, cachedInputPerMillion: 0.1, outputPerMillion: 5 }
|
|
3628
5220
|
};
|
|
3629
|
-
function
|
|
3630
|
-
return process.env.REPOSPEND_DEMO_DATA === "lotr";
|
|
3631
|
-
}
|
|
3632
|
-
function readDemoDashboardData(filters = {}, options = {}) {
|
|
3633
|
-
return buildDashboardSnapshot({
|
|
3634
|
-
sources: demoSources,
|
|
3635
|
-
sourceStats: demoSourceStats,
|
|
3636
|
-
sessions: displaySessions(demoSessions, options),
|
|
3637
|
-
filters
|
|
3638
|
-
});
|
|
3639
|
-
}
|
|
3640
|
-
function readDemoRtkGain() {
|
|
5221
|
+
function issue(command2, category, severity, impact, reason) {
|
|
3641
5222
|
return {
|
|
3642
|
-
|
|
3643
|
-
|
|
3644
|
-
|
|
3645
|
-
|
|
3646
|
-
|
|
3647
|
-
|
|
3648
|
-
totalCommands: 248,
|
|
3649
|
-
inputTokens: "18.4M",
|
|
3650
|
-
outputTokens: "2.6M",
|
|
3651
|
-
tokensSaved: "5.8M",
|
|
3652
|
-
savedPercent: 23.9,
|
|
3653
|
-
totalExecTime: "41m",
|
|
3654
|
-
averageExecTime: "9.9s",
|
|
3655
|
-
rtkTokensSaved: "5.8M",
|
|
3656
|
-
rtkSavingsRate: 23.9,
|
|
3657
|
-
rtkCommandsProcessed: 248,
|
|
3658
|
-
rtkAverageCommandRuntime: "9.9s",
|
|
3659
|
-
rtkTopSavingsCommands: [
|
|
3660
|
-
{ command: "rg", count: 74, saved: "1.9M", averageSavedPercent: 31.2, time: "8m" },
|
|
3661
|
-
{ command: "pnpm test", count: 18, saved: "840K", averageSavedPercent: 17.4, time: "12m" },
|
|
3662
|
-
{ command: "git diff", count: 39, saved: "620K", averageSavedPercent: 26.8, time: "2m" }
|
|
3663
|
-
],
|
|
3664
|
-
rtkRecentActivity: [
|
|
3665
|
-
"rg council-of-elrond planner",
|
|
3666
|
-
"pnpm test --filter gondor-api",
|
|
3667
|
-
"git diff -- shire-mobile"
|
|
3668
|
-
],
|
|
3669
|
-
rtkDiscoverAvailable: true,
|
|
3670
|
-
rtkDiscoverError: void 0,
|
|
3671
|
-
rtkDiscoverSummary: "Total: 248 fictional command events scanned.",
|
|
3672
|
-
rtkCoverageGaps: [
|
|
3673
|
-
{ command: "cat", count: 21, rtkEquivalent: "rtk sed", status: "easy win", estimatedSavings: "410K" },
|
|
3674
|
-
{ command: "grep", count: 13, rtkEquivalent: "rtk rg", status: "easy win", estimatedSavings: "360K" }
|
|
3675
|
-
],
|
|
3676
|
-
rtkUnhandledCommands: [
|
|
3677
|
-
{ command: "palantirctl inspect", count: 5, example: "palantirctl inspect eye-of-sauron-alerts" }
|
|
3678
|
-
],
|
|
3679
|
-
topCommands: [
|
|
3680
|
-
{ command: "rg", count: 74, saved: "1.9M", averageSavedPercent: 31.2, time: "8m" },
|
|
3681
|
-
{ command: "git", count: 46, saved: "720K", averageSavedPercent: 18.3, time: "3m" },
|
|
3682
|
-
{ command: "pnpm", count: 31, saved: "1.1M", averageSavedPercent: 16.8, time: "17m" }
|
|
3683
|
-
],
|
|
3684
|
-
recentCommands: [
|
|
3685
|
-
"rtk rg auth gates-of-moria",
|
|
3686
|
-
"rtk pnpm test --filter palantir-observability",
|
|
3687
|
-
"rtk git diff -- one-ring-infra"
|
|
3688
|
-
],
|
|
3689
|
-
raw: "RepoSpend LOTR demo RTK output",
|
|
3690
|
-
error: void 0
|
|
5223
|
+
command: command2,
|
|
5224
|
+
category,
|
|
5225
|
+
classification: severity === "critical" ? "blocking_failure" : "warning",
|
|
5226
|
+
severity,
|
|
5227
|
+
impact,
|
|
5228
|
+
reason
|
|
3691
5229
|
};
|
|
3692
5230
|
}
|
|
3693
5231
|
var demoSources = [
|
|
@@ -3704,6 +5242,17 @@ var demoSources = [
|
|
|
3704
5242
|
available: true,
|
|
3705
5243
|
paths: ["/demo/middle-earth/claude/projects"],
|
|
3706
5244
|
warnings: []
|
|
5245
|
+
},
|
|
5246
|
+
{
|
|
5247
|
+
id: "copilot",
|
|
5248
|
+
label: "GitHub Copilot",
|
|
5249
|
+
available: true,
|
|
5250
|
+
paths: [
|
|
5251
|
+
"/demo/middle-earth/copilot/otel",
|
|
5252
|
+
"/demo/middle-earth/copilot/session-state",
|
|
5253
|
+
"/demo/middle-earth/vscode/User/workspaceStorage"
|
|
5254
|
+
],
|
|
5255
|
+
warnings: []
|
|
3707
5256
|
}
|
|
3708
5257
|
];
|
|
3709
5258
|
var demoSourceStats = [
|
|
@@ -3735,6 +5284,26 @@ var demoSourceStats = [
|
|
|
3735
5284
|
malformedFileCount: 0,
|
|
3736
5285
|
projectDirCount: 4,
|
|
3737
5286
|
lastScannedAt: "2026-05-18T19:01:00.000Z"
|
|
5287
|
+
},
|
|
5288
|
+
{
|
|
5289
|
+
sourceId: "copilot",
|
|
5290
|
+
sourceLabel: "GitHub Copilot",
|
|
5291
|
+
homePath: "/demo/middle-earth/copilot",
|
|
5292
|
+
copilotHome: "/demo/middle-earth/copilot",
|
|
5293
|
+
otelPath: "/demo/middle-earth/copilot/otel",
|
|
5294
|
+
workspaceStoragePath: "/demo/middle-earth/vscode/User/workspaceStorage",
|
|
5295
|
+
otelExists: true,
|
|
5296
|
+
sessionsExists: true,
|
|
5297
|
+
otelFileCount: 5,
|
|
5298
|
+
transcriptFileCount: 3,
|
|
5299
|
+
debugLogFileCount: 2,
|
|
5300
|
+
sessionStateFileCount: 2,
|
|
5301
|
+
sessionFileCount: 10,
|
|
5302
|
+
sessionsImported: 3,
|
|
5303
|
+
parseFailureCount: 0,
|
|
5304
|
+
unreadableFileCount: 0,
|
|
5305
|
+
malformedFileCount: 0,
|
|
5306
|
+
lastScannedAt: "2026-05-18T19:02:00.000Z"
|
|
3738
5307
|
}
|
|
3739
5308
|
];
|
|
3740
5309
|
var demoInputs = [
|
|
@@ -3911,6 +5480,32 @@ var demoInputs = [
|
|
|
3911
5480
|
outcome: "completed",
|
|
3912
5481
|
branch: "planner/elrond-v2"
|
|
3913
5482
|
},
|
|
5483
|
+
{
|
|
5484
|
+
id: "lotr-session-015",
|
|
5485
|
+
repo: "rivendell-dashboard",
|
|
5486
|
+
title: "Use Copilot to draft council agenda actions",
|
|
5487
|
+
sourceClient: "copilot",
|
|
5488
|
+
sourceApp: "VS Code",
|
|
5489
|
+
sourceAppRaw: "vscode.copilot-chat",
|
|
5490
|
+
model: "lark-vscode-preview",
|
|
5491
|
+
provider: "github-copilot",
|
|
5492
|
+
hour: 14,
|
|
5493
|
+
minute: 55,
|
|
5494
|
+
durationMinutes: 36,
|
|
5495
|
+
inputTokens: 51e4,
|
|
5496
|
+
cachedInputTokens: 18e4,
|
|
5497
|
+
outputTokens: 74e3,
|
|
5498
|
+
reasoningTokens: 0,
|
|
5499
|
+
prompts: 4,
|
|
5500
|
+
replies: 7,
|
|
5501
|
+
tools: 24,
|
|
5502
|
+
commands: 6,
|
|
5503
|
+
edits: 6,
|
|
5504
|
+
reads: 29,
|
|
5505
|
+
outcome: "completed",
|
|
5506
|
+
branch: "planner/copilot-actions",
|
|
5507
|
+
detectedSurface: "vscode_extension"
|
|
5508
|
+
},
|
|
3914
5509
|
{
|
|
3915
5510
|
id: "lotr-session-007",
|
|
3916
5511
|
repo: "palantir-observability",
|
|
@@ -3937,6 +5532,39 @@ var demoInputs = [
|
|
|
3937
5532
|
harmlessNonZero: 2,
|
|
3938
5533
|
exploratoryMisses: 3
|
|
3939
5534
|
},
|
|
5535
|
+
{
|
|
5536
|
+
id: "lotr-session-016",
|
|
5537
|
+
repo: "palantir-observability",
|
|
5538
|
+
title: "Trace Copilot alert summaries through the seeing-stone feed",
|
|
5539
|
+
sourceClient: "copilot",
|
|
5540
|
+
sourceApp: "Copilot CLI",
|
|
5541
|
+
sourceAppRaw: "copilot-cli",
|
|
5542
|
+
model: "copilot/gpt-5-mini",
|
|
5543
|
+
provider: "openai",
|
|
5544
|
+
hour: 16,
|
|
5545
|
+
minute: 35,
|
|
5546
|
+
durationMinutes: 43,
|
|
5547
|
+
inputTokens: 69e4,
|
|
5548
|
+
cachedInputTokens: 21e4,
|
|
5549
|
+
outputTokens: 88e3,
|
|
5550
|
+
reasoningTokens: 0,
|
|
5551
|
+
prompts: 5,
|
|
5552
|
+
replies: 8,
|
|
5553
|
+
tools: 30,
|
|
5554
|
+
commands: 13,
|
|
5555
|
+
edits: 3,
|
|
5556
|
+
reads: 34,
|
|
5557
|
+
outcome: "partial",
|
|
5558
|
+
branch: "alerts/copilot-summaries",
|
|
5559
|
+
warnings: ["copilot_partial_token_breakdown"],
|
|
5560
|
+
importantFailures: 1,
|
|
5561
|
+
topFailureType: "test",
|
|
5562
|
+
detectedSurface: "terminal_cli",
|
|
5563
|
+
tokenConfidence: "medium",
|
|
5564
|
+
issueSamples: [
|
|
5565
|
+
issue("pnpm test --filter palantir-alerts", "test", "warning", "medium", "A generated summary snapshot changed before the matcher was tightened.")
|
|
5566
|
+
]
|
|
5567
|
+
},
|
|
3940
5568
|
{
|
|
3941
5569
|
id: "lotr-session-008",
|
|
3942
5570
|
repo: "mordor-ci",
|
|
@@ -4012,6 +5640,32 @@ var demoInputs = [
|
|
|
4012
5640
|
outcome: "completed",
|
|
4013
5641
|
branch: "docs/ring-config"
|
|
4014
5642
|
},
|
|
5643
|
+
{
|
|
5644
|
+
id: "lotr-session-017",
|
|
5645
|
+
repo: "fellowship-docs",
|
|
5646
|
+
title: "Ask Copilot to review the ring-config redaction guide",
|
|
5647
|
+
sourceClient: "copilot",
|
|
5648
|
+
sourceApp: "VS Code",
|
|
5649
|
+
sourceAppRaw: "vscode.copilot-chat",
|
|
5650
|
+
model: "copilot/claude-sonnet-4.5",
|
|
5651
|
+
provider: "anthropic",
|
|
5652
|
+
hour: 18,
|
|
5653
|
+
minute: 42,
|
|
5654
|
+
durationMinutes: 18,
|
|
5655
|
+
inputTokens: 24e4,
|
|
5656
|
+
cachedInputTokens: 96e3,
|
|
5657
|
+
outputTokens: 31e3,
|
|
5658
|
+
reasoningTokens: 0,
|
|
5659
|
+
prompts: 2,
|
|
5660
|
+
replies: 4,
|
|
5661
|
+
tools: 12,
|
|
5662
|
+
commands: 3,
|
|
5663
|
+
edits: 2,
|
|
5664
|
+
reads: 15,
|
|
5665
|
+
outcome: "completed",
|
|
5666
|
+
branch: "docs/copilot-redaction",
|
|
5667
|
+
detectedSurface: "vscode_extension"
|
|
5668
|
+
},
|
|
4015
5669
|
{
|
|
4016
5670
|
id: "lotr-session-011",
|
|
4017
5671
|
repo: "one-ring-infra",
|
|
@@ -4119,6 +5773,74 @@ var demoInputs = [
|
|
|
4119
5773
|
branch: "offline/bag-end"
|
|
4120
5774
|
}
|
|
4121
5775
|
];
|
|
5776
|
+
|
|
5777
|
+
// apps/server/src/demo/rtk.ts
|
|
5778
|
+
function readDemoRtkGain() {
|
|
5779
|
+
return {
|
|
5780
|
+
available: true,
|
|
5781
|
+
rtkDetected: true,
|
|
5782
|
+
rtkVersion: "rtk-demo 1.0.0",
|
|
5783
|
+
rtkCodexHookStatus: "active",
|
|
5784
|
+
rtkLastActivityAt: "2026-05-18T18:42:00.000Z",
|
|
5785
|
+
rtkDataSource: "fictional demo data",
|
|
5786
|
+
totalCommands: 248,
|
|
5787
|
+
inputTokens: "18.4M",
|
|
5788
|
+
outputTokens: "2.6M",
|
|
5789
|
+
tokensSaved: "5.8M",
|
|
5790
|
+
savedPercent: 23.9,
|
|
5791
|
+
totalExecTime: "41m",
|
|
5792
|
+
averageExecTime: "9.9s",
|
|
5793
|
+
rtkTokensSaved: "5.8M",
|
|
5794
|
+
rtkSavingsRate: 23.9,
|
|
5795
|
+
rtkCommandsProcessed: 248,
|
|
5796
|
+
rtkAverageCommandRuntime: "9.9s",
|
|
5797
|
+
rtkTopSavingsCommands: [
|
|
5798
|
+
{ command: "rg", count: 74, saved: "1.9M", averageSavedPercent: 31.2, time: "8m" },
|
|
5799
|
+
{ command: "pnpm test", count: 18, saved: "840K", averageSavedPercent: 17.4, time: "12m" },
|
|
5800
|
+
{ command: "git diff", count: 39, saved: "620K", averageSavedPercent: 26.8, time: "2m" }
|
|
5801
|
+
],
|
|
5802
|
+
rtkRecentActivity: [
|
|
5803
|
+
"rg council-of-elrond planner",
|
|
5804
|
+
"pnpm test --filter gondor-api",
|
|
5805
|
+
"git diff -- shire-mobile"
|
|
5806
|
+
],
|
|
5807
|
+
rtkDiscoverAvailable: true,
|
|
5808
|
+
rtkDiscoverError: void 0,
|
|
5809
|
+
rtkDiscoverSummary: "Total: 248 fictional command events scanned.",
|
|
5810
|
+
rtkCoverageGaps: [
|
|
5811
|
+
{ command: "cat", count: 21, rtkEquivalent: "rtk sed", status: "easy win", estimatedSavings: "410K" },
|
|
5812
|
+
{ command: "grep", count: 13, rtkEquivalent: "rtk rg", status: "easy win", estimatedSavings: "360K" }
|
|
5813
|
+
],
|
|
5814
|
+
rtkUnhandledCommands: [
|
|
5815
|
+
{ command: "palantirctl inspect", count: 5, example: "palantirctl inspect eye-of-sauron-alerts" }
|
|
5816
|
+
],
|
|
5817
|
+
topCommands: [
|
|
5818
|
+
{ command: "rg", count: 74, saved: "1.9M", averageSavedPercent: 31.2, time: "8m" },
|
|
5819
|
+
{ command: "git", count: 46, saved: "720K", averageSavedPercent: 18.3, time: "3m" },
|
|
5820
|
+
{ command: "pnpm", count: 31, saved: "1.1M", averageSavedPercent: 16.8, time: "17m" }
|
|
5821
|
+
],
|
|
5822
|
+
recentCommands: [
|
|
5823
|
+
"rtk rg auth gates-of-moria",
|
|
5824
|
+
"rtk pnpm test --filter palantir-observability",
|
|
5825
|
+
"rtk git diff -- one-ring-infra"
|
|
5826
|
+
],
|
|
5827
|
+
raw: "RepoSpend LOTR demo RTK output",
|
|
5828
|
+
error: void 0
|
|
5829
|
+
};
|
|
5830
|
+
}
|
|
5831
|
+
|
|
5832
|
+
// apps/server/src/demo/index.ts
|
|
5833
|
+
function demoModeEnabled() {
|
|
5834
|
+
return process.env.REPOSPEND_DEMO_DATA === "lotr";
|
|
5835
|
+
}
|
|
5836
|
+
function readDemoDashboardData(filters = {}, options = {}) {
|
|
5837
|
+
return buildDashboardSnapshot({
|
|
5838
|
+
sources: demoSources,
|
|
5839
|
+
sourceStats: demoSourceStats,
|
|
5840
|
+
sessions: displaySessions(demoSessions, options),
|
|
5841
|
+
filters
|
|
5842
|
+
});
|
|
5843
|
+
}
|
|
4122
5844
|
var demoSessions = demoInputs.map(toSession);
|
|
4123
5845
|
function toSession(input) {
|
|
4124
5846
|
const start = timestamp2(input.hour, input.minute);
|
|
@@ -4207,16 +5929,6 @@ function detectedSurfaceForApp(sourceApp) {
|
|
|
4207
5929
|
function timestamp2(hour, minute) {
|
|
4208
5930
|
return `${baseDate}T${String(hour).padStart(2, "0")}:${String(minute).padStart(2, "0")}:00.000Z`;
|
|
4209
5931
|
}
|
|
4210
|
-
function issue(command2, category, severity, impact, reason) {
|
|
4211
|
-
return {
|
|
4212
|
-
command: command2,
|
|
4213
|
-
category,
|
|
4214
|
-
classification: severity === "critical" ? "blocking_failure" : "warning",
|
|
4215
|
-
severity,
|
|
4216
|
-
impact,
|
|
4217
|
-
reason
|
|
4218
|
-
};
|
|
4219
|
-
}
|
|
4220
5932
|
function demoReply(repo, edits, importantFailures) {
|
|
4221
5933
|
if (importantFailures > 0) {
|
|
4222
5934
|
return `Found the noisy path in ${repo}, separated the risky failures from ordinary exploration, and left a short list of follow-up checks.`;
|
|
@@ -4419,6 +6131,15 @@ function recentCommandLines(lines) {
|
|
|
4419
6131
|
// apps/server/src/server.ts
|
|
4420
6132
|
function createServer(options = {}) {
|
|
4421
6133
|
const app = Fastify({ logger: false });
|
|
6134
|
+
app.addHook("onRequest", async (request, reply) => {
|
|
6135
|
+
if (!isProtectedLocalMutation(request.method, request.url)) return;
|
|
6136
|
+
if (!isAllowedLocalHost(request.headers.host)) {
|
|
6137
|
+
return reply.code(403).send({ error: "Mutating RepoSpend local settings requires a localhost Host header." });
|
|
6138
|
+
}
|
|
6139
|
+
if (!isAllowedLocalOrigin(request.headers.origin)) {
|
|
6140
|
+
return reply.code(403).send({ error: "Mutating RepoSpend local settings requires a localhost Origin." });
|
|
6141
|
+
}
|
|
6142
|
+
});
|
|
4422
6143
|
app.get("/api/health", async () => ({ ok: true, app: "RepoSpend", version: readPackageVersion() }));
|
|
4423
6144
|
app.get("/api/dashboard", async (request) => {
|
|
4424
6145
|
const query = request.query;
|
|
@@ -4427,6 +6148,7 @@ function createServer(options = {}) {
|
|
|
4427
6148
|
const demoMode = demoModeEnabled();
|
|
4428
6149
|
const data = demoMode ? readDemoDashboardData(filters, dashboardOptions) : readDashboardData(filters, dashboardOptions);
|
|
4429
6150
|
const rtkGain = demoMode ? readDemoRtkGain() : await readRtkGain();
|
|
6151
|
+
const configData = readConfigData();
|
|
4430
6152
|
return {
|
|
4431
6153
|
...data,
|
|
4432
6154
|
appVersion: readPackageVersion(),
|
|
@@ -4435,6 +6157,8 @@ function createServer(options = {}) {
|
|
|
4435
6157
|
info: pricingInfo,
|
|
4436
6158
|
...readPricingData()
|
|
4437
6159
|
},
|
|
6160
|
+
config: configData.config,
|
|
6161
|
+
configPath: configData.path,
|
|
4438
6162
|
rtkGain
|
|
4439
6163
|
};
|
|
4440
6164
|
});
|
|
@@ -4442,20 +6166,32 @@ function createServer(options = {}) {
|
|
|
4442
6166
|
info: pricingInfo,
|
|
4443
6167
|
...writePricingData(validatePricingBody(request.body))
|
|
4444
6168
|
}));
|
|
6169
|
+
app.put("/api/settings/config", async (request) => writeConfigData(validateConfigBody(request.body)));
|
|
4445
6170
|
app.delete("/api/settings/local-data", async () => clearRepoSpendLocalData());
|
|
4446
|
-
app.
|
|
6171
|
+
app.delete("/api/settings/cache", async () => {
|
|
6172
|
+
if (demoModeEnabled()) return { path: "demo mode (cache disabled)", removed: false, demo: true };
|
|
6173
|
+
return clearRepoSpendParseCache();
|
|
6174
|
+
});
|
|
6175
|
+
app.post("/api/cache/warm", async (request) => {
|
|
6176
|
+
if (demoModeEnabled()) return { started: false, reason: "demo_mode" };
|
|
6177
|
+
const query = request.query;
|
|
6178
|
+
return warmDashboardCache(warmCacheFilters(), parseDashboardOptions(query));
|
|
6179
|
+
});
|
|
6180
|
+
app.get("/api/export.json", async (request, reply) => {
|
|
6181
|
+
const query = request.query;
|
|
4447
6182
|
reply.header("content-type", "application/json; charset=utf-8");
|
|
4448
|
-
return exportJson();
|
|
6183
|
+
return exportJson(parseFilters(query), parseDashboardOptions(query));
|
|
4449
6184
|
});
|
|
4450
|
-
app.get("/api/export.csv", async (
|
|
6185
|
+
app.get("/api/export.csv", async (request, reply) => {
|
|
6186
|
+
const query = request.query;
|
|
4451
6187
|
reply.header("content-type", "text/csv; charset=utf-8");
|
|
4452
6188
|
reply.header("content-disposition", 'attachment; filename="repospend-export.csv"');
|
|
4453
|
-
return exportCsv();
|
|
6189
|
+
return exportCsv(parseFilters(query), parseDashboardOptions(query));
|
|
4454
6190
|
});
|
|
4455
6191
|
if (options.serveWeb !== false) {
|
|
4456
|
-
const serverDist =
|
|
4457
|
-
const packagedWebDist =
|
|
4458
|
-
const workspaceWebDist =
|
|
6192
|
+
const serverDist = path10.dirname(fileURLToPath3(import.meta.url));
|
|
6193
|
+
const packagedWebDist = path10.resolve(serverDist, "../web-dist");
|
|
6194
|
+
const workspaceWebDist = path10.resolve(serverDist, "../../web/dist");
|
|
4459
6195
|
const webDist = fsExists(packagedWebDist) ? packagedWebDist : workspaceWebDist;
|
|
4460
6196
|
if (!fsExists(webDist)) {
|
|
4461
6197
|
throw new Error(`RepoSpend web assets are missing. Expected ${packagedWebDist} in the published package, or ${workspaceWebDist} during local development. Run "pnpm pack:prepare" before packaging.`);
|
|
@@ -4476,13 +6212,13 @@ function createServer(options = {}) {
|
|
|
4476
6212
|
return app;
|
|
4477
6213
|
}
|
|
4478
6214
|
function fsExists(inputPath) {
|
|
4479
|
-
return Boolean(inputPath) &&
|
|
6215
|
+
return Boolean(inputPath) && path10.isAbsolute(inputPath) && fs10.existsSync(inputPath);
|
|
4480
6216
|
}
|
|
4481
6217
|
function readPackageVersion() {
|
|
4482
|
-
const serverDist =
|
|
4483
|
-
const packagePath =
|
|
6218
|
+
const serverDist = path10.dirname(fileURLToPath3(import.meta.url));
|
|
6219
|
+
const packagePath = path10.resolve(serverDist, "../package.json");
|
|
4484
6220
|
try {
|
|
4485
|
-
const parsed = JSON.parse(
|
|
6221
|
+
const parsed = JSON.parse(fs10.readFileSync(packagePath, "utf8"));
|
|
4486
6222
|
return typeof parsed.version === "string" ? parsed.version : "unknown";
|
|
4487
6223
|
} catch {
|
|
4488
6224
|
return "unknown";
|
|
@@ -4514,6 +6250,33 @@ function portCandidates(startPort, fallbackCount) {
|
|
|
4514
6250
|
function isAddressInUse(error) {
|
|
4515
6251
|
return Boolean(error && typeof error === "object" && "code" in error && error.code === "EADDRINUSE");
|
|
4516
6252
|
}
|
|
6253
|
+
function isProtectedLocalMutation(method, url) {
|
|
6254
|
+
if (method !== "PUT" && method !== "POST" && method !== "DELETE") return false;
|
|
6255
|
+
return url.startsWith("/api/settings/") || url.startsWith("/api/cache/");
|
|
6256
|
+
}
|
|
6257
|
+
function isAllowedLocalHost(hostHeader) {
|
|
6258
|
+
if (!hostHeader) return false;
|
|
6259
|
+
const host = hostFromHeader(hostHeader).toLowerCase();
|
|
6260
|
+
return host === "localhost" || host === "127.0.0.1" || host === "::1";
|
|
6261
|
+
}
|
|
6262
|
+
function isAllowedLocalOrigin(originHeader) {
|
|
6263
|
+
if (!originHeader) return true;
|
|
6264
|
+
try {
|
|
6265
|
+
return isAllowedLocalHost(new URL(originHeader).host);
|
|
6266
|
+
} catch {
|
|
6267
|
+
return false;
|
|
6268
|
+
}
|
|
6269
|
+
}
|
|
6270
|
+
function hostFromHeader(hostHeader) {
|
|
6271
|
+
const value = hostHeader.trim();
|
|
6272
|
+
if (value.startsWith("[")) {
|
|
6273
|
+
const end = value.indexOf("]");
|
|
6274
|
+
return end >= 0 ? value.slice(1, end) : value;
|
|
6275
|
+
}
|
|
6276
|
+
const colonCount = (value.match(/:/g) ?? []).length;
|
|
6277
|
+
if (colonCount === 1) return value.slice(0, value.indexOf(":"));
|
|
6278
|
+
return value;
|
|
6279
|
+
}
|
|
4517
6280
|
function validatePricingBody(body) {
|
|
4518
6281
|
if (!body || typeof body !== "object" || Array.isArray(body)) {
|
|
4519
6282
|
throw new Error("Expected JSON body with a models object.");
|
|
@@ -4544,6 +6307,31 @@ function validatePricingBody(body) {
|
|
|
4544
6307
|
}
|
|
4545
6308
|
return validated;
|
|
4546
6309
|
}
|
|
6310
|
+
function validateConfigBody(body) {
|
|
6311
|
+
if (!body || typeof body !== "object" || Array.isArray(body)) return {};
|
|
6312
|
+
const input = body;
|
|
6313
|
+
const config = {};
|
|
6314
|
+
if (Array.isArray(input.repos)) config.repos = input.repos;
|
|
6315
|
+
if (typeof input.pricingPath === "string") config.pricingPath = input.pricingPath;
|
|
6316
|
+
const experimentalSources = input.experimentalSources;
|
|
6317
|
+
if (experimentalSources && typeof experimentalSources === "object" && !Array.isArray(experimentalSources)) {
|
|
6318
|
+
config.experimentalSources = {
|
|
6319
|
+
cursor: experimentalSources.cursor === true
|
|
6320
|
+
};
|
|
6321
|
+
}
|
|
6322
|
+
return config;
|
|
6323
|
+
}
|
|
6324
|
+
function warmCacheFilters() {
|
|
6325
|
+
const days = 30;
|
|
6326
|
+
const now = /* @__PURE__ */ new Date();
|
|
6327
|
+
return {
|
|
6328
|
+
from: dateOnly(new Date(now.getTime() - days * 24 * 60 * 60 * 1e3)),
|
|
6329
|
+
to: dateOnly(now)
|
|
6330
|
+
};
|
|
6331
|
+
}
|
|
6332
|
+
function dateOnly(value) {
|
|
6333
|
+
return value.toISOString().slice(0, 10);
|
|
6334
|
+
}
|
|
4547
6335
|
function finiteNumber(value) {
|
|
4548
6336
|
if (value === "" || value === null || value === void 0) return void 0;
|
|
4549
6337
|
const parsed = typeof value === "number" ? value : Number(value);
|
|
@@ -4561,23 +6349,27 @@ try {
|
|
|
4561
6349
|
if (port !== requestedPort) console.log(`Port ${requestedPort} is already in use; using ${port} instead.`);
|
|
4562
6350
|
console.log(`RepoSpend dashboard: ${url}`);
|
|
4563
6351
|
openBrowser(url);
|
|
6352
|
+
} else if (command === "__warm-cache") {
|
|
6353
|
+
readDashboardData(parseCliDateFilters(args), parseCliDashboardOptions(args));
|
|
4564
6354
|
} else if (command === "scan") {
|
|
4565
|
-
const data = readDashboardData(
|
|
6355
|
+
const data = readDashboardData(parseCliFilters(args), parseCliDashboardOptions(args));
|
|
4566
6356
|
console.log(JSON.stringify({ sources: data.sources, sessionCount: data.sessions.length, summary: data.summary }, null, 2));
|
|
4567
6357
|
} else if (command === "by-repo") {
|
|
4568
|
-
printGroups(groupByRepo(readDashboardData(
|
|
6358
|
+
printGroups(groupByRepo(readDashboardData(parseCliFilters(args), parseCliDashboardOptions(args)).sessions));
|
|
4569
6359
|
} else if (command === "by-day") {
|
|
4570
|
-
printGroups(groupByDay(readDashboardData(
|
|
6360
|
+
printGroups(groupByDay(readDashboardData(parseCliFilters(args), parseCliDashboardOptions(args)).sessions));
|
|
4571
6361
|
} else if (command === "by-hour") {
|
|
4572
|
-
printGroups(groupByHour(readDashboardData(
|
|
6362
|
+
printGroups(groupByHour(readDashboardData(parseCliFilters(args), parseCliDashboardOptions(args)).sessions));
|
|
4573
6363
|
} else if (command === "by-model") {
|
|
4574
|
-
printGroups(groupByModel(readDashboardData(
|
|
6364
|
+
printGroups(groupByModel(readDashboardData(parseCliFilters(args), parseCliDashboardOptions(args)).sessions));
|
|
4575
6365
|
} else if (command === "by-app") {
|
|
4576
|
-
printGroups(groupBySourceApp(readDashboardData(
|
|
6366
|
+
printGroups(groupBySourceApp(readDashboardData(parseCliFilters(args), parseCliDashboardOptions(args)).sessions));
|
|
6367
|
+
} else if (command === "doctor") {
|
|
6368
|
+
printDoctor(readDashboardData(parseCliFilters(args), parseCliDashboardOptions(args)));
|
|
4577
6369
|
} else if (command === "export") {
|
|
4578
6370
|
const format = valueAfter(args, "--format") ?? "json";
|
|
4579
|
-
const filters =
|
|
4580
|
-
const sessions = readDashboardData(filters).sessions;
|
|
6371
|
+
const filters = parseCliFilters(args);
|
|
6372
|
+
const sessions = readDashboardData(filters, parseCliDashboardOptions(args)).sessions;
|
|
4581
6373
|
console.log(format === "csv" ? toCsv(sessions) : JSON.stringify(sessions, null, 2));
|
|
4582
6374
|
} else {
|
|
4583
6375
|
console.error(`Unknown command: ${command}`);
|
|
@@ -4592,14 +6384,14 @@ function openBrowser(url) {
|
|
|
4592
6384
|
if (process.env.REPOSPEND_NO_OPEN === "1" || process.env.CI === "true") return;
|
|
4593
6385
|
const command2 = process.platform === "darwin" ? "open" : process.platform === "win32" ? "cmd" : "xdg-open";
|
|
4594
6386
|
const args2 = process.platform === "win32" ? ["/c", "start", "", url] : [url];
|
|
4595
|
-
const child =
|
|
6387
|
+
const child = spawn2(command2, args2, { detached: true, stdio: "ignore" });
|
|
4596
6388
|
child.on("error", () => {
|
|
4597
6389
|
});
|
|
4598
6390
|
child.unref();
|
|
4599
6391
|
}
|
|
4600
6392
|
function printGroups(groups) {
|
|
4601
6393
|
if (!groups.length) {
|
|
4602
|
-
console.log("No usage found. Scanned
|
|
6394
|
+
console.log("No usage found. Scanned enabled local data paths.");
|
|
4603
6395
|
return;
|
|
4604
6396
|
}
|
|
4605
6397
|
const rows = groups.map((group) => ({
|
|
@@ -4610,18 +6402,42 @@ function printGroups(groups) {
|
|
|
4610
6402
|
}));
|
|
4611
6403
|
console.table(rows);
|
|
4612
6404
|
}
|
|
4613
|
-
function
|
|
4614
|
-
const
|
|
4615
|
-
|
|
4616
|
-
|
|
6405
|
+
function printDoctor(data) {
|
|
6406
|
+
const confidence = data.confidence;
|
|
6407
|
+
console.log(`RepoSpend data confidence: ${confidence.label} (${confidence.score}/100)`);
|
|
6408
|
+
console.table([
|
|
6409
|
+
{ metric: "Sessions", value: confidence.sessionCount },
|
|
6410
|
+
{ metric: "Token data coverage", value: `${Math.round(confidence.tokenDataPct * 100)}% (${confidence.tokenDataSessions}/${confidence.sessionCount})` },
|
|
6411
|
+
{ metric: "Pricing coverage", value: `${Math.round(confidence.pricingCoveragePct * 100)}% (${confidence.pricedTokenSessions}/${confidence.tokenDataSessions})` },
|
|
6412
|
+
{ metric: "Repo grouping", value: `${Math.round(confidence.verifiedRepoPct * 100)}% verified (${confidence.verifiedRepoSessions}/${confidence.sessionCount})` },
|
|
6413
|
+
{ metric: "Parse issues", value: confidence.parseIssueCount },
|
|
6414
|
+
{ metric: "Source warnings", value: confidence.sourceWarningCount }
|
|
6415
|
+
]);
|
|
6416
|
+
if (confidence.issues.length) {
|
|
6417
|
+
console.table(confidence.issues.map((issue2) => ({
|
|
6418
|
+
severity: issue2.tone,
|
|
6419
|
+
issue: issue2.title,
|
|
6420
|
+
detail: issue2.detail,
|
|
6421
|
+
sessions: issue2.affectedSessionIds.length
|
|
6422
|
+
})));
|
|
6423
|
+
} else {
|
|
6424
|
+
console.log("No data-confidence issues detected.");
|
|
6425
|
+
}
|
|
6426
|
+
console.table(data.sourceStats.map((source) => ({
|
|
6427
|
+
source: source.sourceLabel ?? source.sourceId ?? "unknown",
|
|
6428
|
+
imported: source.sessionsImported ?? 0,
|
|
6429
|
+
files: source.sessionFileCount,
|
|
6430
|
+
parseIssues: source.parseFailureCount ?? 0,
|
|
6431
|
+
primaryData: sourcePrimaryDataFound(source) ? "found" : "missing"
|
|
6432
|
+
})));
|
|
4617
6433
|
}
|
|
4618
|
-
function
|
|
4619
|
-
|
|
4620
|
-
return index >= 0 ? args2[index + 1] : void 0;
|
|
6434
|
+
function sourcePrimaryDataFound(source) {
|
|
6435
|
+
return Boolean(source.stateExists || source.sessionsExists || source.projectsExists || source.historyExists || source.otelExists || (source.otelFileCount ?? 0) > 0 || (source.databaseFileCount ?? 0) > 0);
|
|
4621
6436
|
}
|
|
4622
6437
|
function printHelp() {
|
|
4623
6438
|
console.log(`RepoSpend commands:
|
|
4624
6439
|
repospend serve
|
|
6440
|
+
repospend doctor
|
|
4625
6441
|
repospend scan
|
|
4626
6442
|
repospend by-repo
|
|
4627
6443
|
repospend by-day
|
|
@@ -4632,5 +6448,13 @@ function printHelp() {
|
|
|
4632
6448
|
repospend export --format csv
|
|
4633
6449
|
|
|
4634
6450
|
Options:
|
|
4635
|
-
--source all|codex|claude|cursor
|
|
6451
|
+
--source all|codex|claude|copilot|cursor
|
|
6452
|
+
--sourceApp "VS Code" | --source-app "VS Code" | --app "VS Code"
|
|
6453
|
+
--repo <repo name or path>
|
|
6454
|
+
--model <model id>
|
|
6455
|
+
--from YYYY-MM-DD
|
|
6456
|
+
--to YYYY-MM-DD
|
|
6457
|
+
--split-source-apps
|
|
6458
|
+
|
|
6459
|
+
Cursor is experimental and only scans when enabled in ~/.repospend/config.json.`);
|
|
4636
6460
|
}
|