@abnersajr/claude-timeline 1.0.0 → 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,1028 +0,0 @@
1
- import { i as getPricing, n as calculateTurnCost, s as normalizeModelName, t as calculateSessionCost } from "./pricing-DTmya3JY.mjs";
2
- import { existsSync, readFileSync, readdirSync, statSync } from "node:fs";
3
- import { basename, dirname, join } from "node:path";
4
- import { homedir } from "node:os";
5
- import Database from "better-sqlite3";
6
- //#region src/classifier.ts
7
- /** Entry types that are always noise */
8
- const NOISE_TYPES = new Set([
9
- "system",
10
- "summary",
11
- "file-history-snapshot",
12
- "queue-operation",
13
- "attachment",
14
- "last-prompt",
15
- "permission-mode",
16
- "ai-title"
17
- ]);
18
- /** Hard noise tags that should be filtered out entirely */
19
- const HARD_NOISE_TAGS = ["<local-command-caveat>", "<system-reminder>"];
20
- /** Command output tags that map to system category */
21
- const COMMAND_OUTPUT_TAGS = ["<local-command-stdout>", "<local-command-stderr>"];
22
- /** Check if content string starts with any of the given tags */
23
- function startsWithTag(content, tags) {
24
- for (const tag of tags) if (content.startsWith(tag)) return true;
25
- return false;
26
- }
27
- /** Check if the content array has at least one text or image block */
28
- function hasTextOrImageContent(content) {
29
- if (typeof content === "string") return content.length > 0;
30
- return content.some((block) => block.type === "text" || block.type === "image");
31
- }
32
- /**
33
- * Check if an array content is tool_result-only (no text/image blocks).
34
- * These are tool execution results coming back from the CLI — they represent
35
- * assistant context, not actual user-typed input.
36
- */
37
- function isToolResultOnly(content) {
38
- if (typeof content === "string") return false;
39
- return content.length > 0 && content.every((block) => block.type === "tool_result");
40
- }
41
- /**
42
- * Hard noise: system/summary/file-history-snapshot/queue-operation/attachment/last-prompt/permission-mode types,
43
- * sidechain, synthetic assistant, hard noise tags, interruptions.
44
- */
45
- function isHardNoise(record) {
46
- const type = record.type;
47
- if (NOISE_TYPES.has(type)) return true;
48
- if (record.isSidechain) return true;
49
- const message = record.message;
50
- if (type === "assistant" && message?.model === "<synthetic>") return true;
51
- if (type === "user" && message?.content !== void 0) {
52
- const { content } = message;
53
- if (typeof content === "string") {
54
- if (startsWithTag(content, HARD_NOISE_TAGS)) return true;
55
- if (content === "[Request interrupted by user]") return true;
56
- }
57
- }
58
- return false;
59
- }
60
- /** Compact messages are marked by isCompactSummary flag */
61
- function isCompactMessage(record) {
62
- return record.isCompactSummary === true;
63
- }
64
- /**
65
- * System messages: user-type messages that contain command output
66
- * (local-command-stdout/stderr). These arrive as type="user" in JSONL
67
- * but represent command output, not user input.
68
- */
69
- function isSystemMessage(record) {
70
- if (record.type !== "user") return false;
71
- const content = record.message?.content;
72
- if (typeof content !== "string") return false;
73
- return startsWithTag(content, COMMAND_OUTPUT_TAGS);
74
- }
75
- /**
76
- * User messages: type=user, isMeta=false, has text/image content
77
- * (not just tool_result blocks). Meta messages (tool results) are
78
- * classified as assistant because they represent assistant context.
79
- * Tool-result-only records (isMeta=null, content is array of tool_result)
80
- * are also classified as assistant — they're CLI tool outputs, not user input.
81
- */
82
- function isUserMessage(record) {
83
- if (record.type !== "user") return false;
84
- if (record.isMeta) return false;
85
- const content = record.message?.content;
86
- if (content === void 0) return false;
87
- if (typeof content === "string") return true;
88
- if (isToolResultOnly(content)) return false;
89
- return hasTextOrImageContent(content);
90
- }
91
- /**
92
- * Classify a single JSONL record into a category using the priority cascade:
93
- * 1. hardNoise — noise types, sidechain, synthetic, hard noise tags, interruptions
94
- * 2. compact — isCompactSummary === true
95
- * 3. system — user messages with command output (local-command-stdout/stderr)
96
- * 4. user — type=user, not meta, has text/image content
97
- * 5. assistant — everything else (catch-all)
98
- */
99
- function classifyMessage(record) {
100
- if (isHardNoise(record)) return "hardNoise";
101
- if (isCompactMessage(record)) return "compact";
102
- if (isSystemMessage(record)) return "system";
103
- if (isUserMessage(record)) return "user";
104
- return "assistant";
105
- }
106
- //#endregion
107
- //#region src/dedup.ts
108
- /**
109
- * Deduplicate streaming assistant entries by requestId.
110
- *
111
- * Claude Code writes multiple JSONL entries per API response during streaming:
112
- * - Streaming duplicates: same requestId with incrementally increasing output_tokens
113
- * - Content blocks: same requestId with identical output_tokens (thinking/text/tool_use)
114
- *
115
- * Strategy:
116
- * 1. For streaming duplicates (strictly increasing tokens): keep only the last entry
117
- * 2. For content blocks (equal tokens): MERGE into one record by concatenating content arrays
118
- *
119
- * Entries without a requestId (user, system, tool results) pass through unchanged.
120
- */
121
- function deduplicateByRequestId(records) {
122
- const mergedByRequestId = /* @__PURE__ */ new Map();
123
- const hasStrictIncrease = /* @__PURE__ */ new Set();
124
- for (let i = 0; i < records.length; i++) {
125
- const rid = records[i].requestId;
126
- if (!rid) continue;
127
- const outputTokens = records[i].message?.usage?.output_tokens ?? 0;
128
- const existing = mergedByRequestId.get(rid);
129
- if (existing) {
130
- if (outputTokens > existing.outputTokens) {
131
- hasStrictIncrease.add(rid);
132
- mergedByRequestId.set(rid, {
133
- index: i,
134
- outputTokens,
135
- merged: records[i]
136
- });
137
- } else if (outputTokens === existing.outputTokens) existing.merged = mergeContentBlocks(existing.merged, records[i]);
138
- } else mergedByRequestId.set(rid, {
139
- index: i,
140
- outputTokens,
141
- merged: records[i]
142
- });
143
- }
144
- if (mergedByRequestId.size === 0) return records;
145
- const requestIdIndices = /* @__PURE__ */ new Map();
146
- for (let i = 0; i < records.length; i++) {
147
- const rid = records[i].requestId;
148
- if (!rid) continue;
149
- let indices = requestIdIndices.get(rid);
150
- if (!indices) {
151
- indices = /* @__PURE__ */ new Set();
152
- requestIdIndices.set(rid, indices);
153
- }
154
- indices.add(i);
155
- }
156
- const result = [];
157
- const emittedRequestIds = /* @__PURE__ */ new Set();
158
- for (let i = 0; i < records.length; i++) {
159
- const rid = records[i].requestId;
160
- if (!rid) {
161
- result.push(records[i]);
162
- continue;
163
- }
164
- const merged = mergedByRequestId.get(rid);
165
- if (!merged) continue;
166
- if (!emittedRequestIds.has(rid)) {
167
- result.push(merged.merged);
168
- emittedRequestIds.add(rid);
169
- }
170
- }
171
- return result;
172
- }
173
- /**
174
- * Merge content blocks from two records with the same requestId.
175
- * Concatenates the content arrays, keeping all unique content types.
176
- */
177
- function mergeContentBlocks(existing, incoming) {
178
- const existingContent = existing.message?.content;
179
- const incomingContent = incoming.message?.content;
180
- if (!Array.isArray(existingContent) || !Array.isArray(incomingContent)) return existing;
181
- const mergedContent = [...existingContent, ...incomingContent];
182
- return {
183
- ...existing,
184
- message: existing.message ? {
185
- ...existing.message,
186
- content: mergedContent
187
- } : existing.message
188
- };
189
- }
190
- //#endregion
191
- //#region src/utils.ts
192
- /**
193
- * Get the path to usage.db
194
- * Priority: customPath > CLAUDE_CONFIG_DIR env > ~/.claude
195
- */
196
- function getDbPath(customPath) {
197
- if (customPath) return customPath;
198
- return join(process.env.CLAUDE_CONFIG_DIR || join(homedir(), ".claude"), "usage.db");
199
- }
200
- /**
201
- * Get the path to the projects directory
202
- * Priority: customPath > CLAUDE_CONFIG_DIR env > ~/.claude
203
- */
204
- function getProjectsDir(customPath) {
205
- if (customPath) return customPath;
206
- return join(process.env.CLAUDE_CONFIG_DIR || join(homedir(), ".claude"), "projects");
207
- }
208
- /**
209
- * Encode project name for directory lookup
210
- * Replaces all '/' with '-' (e.g., '/Users/test' → '-Users-test')
211
- */
212
- function encodeProjectName(projectName) {
213
- return projectName.replaceAll("/", "-");
214
- }
215
- /**
216
- * Resolve the path to a session's JSONL file
217
- * Tries multiple encodings to handle DB storing project_name with or without leading '/'
218
- */
219
- function resolveSessionJsonlPath(session, projectsDir) {
220
- const candidates = [];
221
- candidates.push(encodeProjectName(session.projectName));
222
- if (!session.projectName.startsWith("/")) candidates.push(encodeProjectName(`/${session.projectName}`));
223
- if (session.projectName.startsWith("/")) candidates.push(encodeProjectName(session.projectName.slice(1)));
224
- candidates.push(encodeURIComponent(session.projectName));
225
- for (const encoded of candidates) {
226
- const filePath = join(projectsDir, encoded, `${session.sessionId}.jsonl`);
227
- if (existsSync(filePath)) return filePath;
228
- }
229
- return null;
230
- }
231
- //#endregion
232
- //#region src/subagent-locator.ts
233
- /**
234
- * List subagent files for a session.
235
- * Scans two directory structures:
236
- * - New nested: {projectsDir}/{project}/{session}/subagents/agent-{id}.jsonl
237
- * - Legacy flat: {projectsDir}/{project}/agent-{id}.jsonl (filtered by sessionId)
238
- *
239
- * Returns NEW structure files first, then legacy flat files.
240
- */
241
- function listSubagentFiles(projectsDir, projectName, sessionId) {
242
- const encodedProject = encodeProjectName(projectName);
243
- const allFiles = [];
244
- const candidates = [encodedProject];
245
- if (!encodedProject.startsWith("-")) candidates.push(`-${encodedProject}`);
246
- for (const projectDirName of candidates) {
247
- const newSubagentsDir = join(projectsDir, projectDirName, sessionId, "subagents");
248
- if (existsSync(newSubagentsDir)) try {
249
- const entries = readdirSync(newSubagentsDir);
250
- for (const entry of entries) if (entry.startsWith("agent-") && entry.endsWith(".jsonl")) {
251
- const agentId = extractAgentId(entry);
252
- if (agentId) allFiles.push({
253
- filePath: join(newSubagentsDir, entry),
254
- agentId,
255
- isNewStructure: true
256
- });
257
- }
258
- } catch {}
259
- const projectDir = join(projectsDir, projectDirName);
260
- if (existsSync(projectDir)) try {
261
- const entries = readdirSync(projectDir);
262
- for (const entry of entries) if (entry.startsWith("agent-") && entry.endsWith(".jsonl")) {
263
- const agentId = extractAgentId(entry);
264
- if (!agentId) continue;
265
- if (isCompactAgent(agentId)) continue;
266
- const filePath = join(projectDir, entry);
267
- if (subagentBelongsToSession(filePath, sessionId)) allFiles.push({
268
- filePath,
269
- agentId,
270
- isNewStructure: false
271
- });
272
- }
273
- } catch {}
274
- }
275
- return allFiles;
276
- }
277
- /**
278
- * Extract agent ID from filename.
279
- * e.g., "agent-abc123.jsonl" → "abc123"
280
- */
281
- function extractAgentId(filename) {
282
- const match = basename(filename).match(/^agent-([^.]+)\.jsonl$/);
283
- return match ? match[1] : null;
284
- }
285
- /**
286
- * Check if agent ID belongs to a compact agent (starts with "acompact").
287
- */
288
- function isCompactAgent(agentId) {
289
- return agentId.startsWith("acompact");
290
- }
291
- /**
292
- * Check if a legacy subagent file belongs to a specific session.
293
- * Reads the first line to check the sessionId field.
294
- */
295
- function subagentBelongsToSession(filePath, sessionId) {
296
- try {
297
- const content = readFileSync(filePath, "utf-8");
298
- const firstNewline = content.indexOf("\n");
299
- const firstLine = firstNewline > 0 ? content.slice(0, firstNewline) : content;
300
- if (!firstLine.trim()) return false;
301
- return JSON.parse(firstLine).sessionId === sessionId;
302
- } catch {
303
- return false;
304
- }
305
- }
306
- //#endregion
307
- //#region src/tool-extraction.ts
308
- /**
309
- * Extract tool calls from assistant message content.
310
- * Filters tool_use blocks and identifies Task tools specially.
311
- */
312
- function extractToolCalls(content, timestamp) {
313
- if (typeof content === "string") return [];
314
- if (!Array.isArray(content)) return [];
315
- const calls = [];
316
- for (const block of content) {
317
- if (block.type !== "tool_use") continue;
318
- const toolUseId = String(block.id ?? block.toolUseId ?? "");
319
- const name = String(block.name ?? "");
320
- const input = block.input ?? {};
321
- const isTask = name === "Task";
322
- let taskDescription;
323
- let taskSubagentType;
324
- if (isTask && input) {
325
- taskDescription = typeof input.description === "string" ? input.description : void 0;
326
- taskSubagentType = typeof input.subagent_type === "string" ? input.subagent_type : void 0;
327
- }
328
- calls.push({
329
- toolUseId,
330
- name,
331
- input,
332
- timestamp,
333
- isTask,
334
- taskDescription,
335
- taskSubagentType
336
- });
337
- }
338
- return calls;
339
- }
340
- /**
341
- * Extract tool results from user message content.
342
- * Filters tool_result blocks from content array.
343
- */
344
- function extractToolResults(content) {
345
- if (typeof content === "string") return [];
346
- if (!Array.isArray(content)) return [];
347
- const results = [];
348
- for (const block of content) {
349
- if (block.type !== "tool_result") continue;
350
- results.push({
351
- toolUseId: String(block.tool_use_id ?? block.toolUseId ?? ""),
352
- content: block.content,
353
- isError: Boolean(block.is_error) || Boolean(block.isError)
354
- });
355
- }
356
- return results;
357
- }
358
- /**
359
- * Link tool results to tool calls by toolUseId.
360
- * Sets result string and isError flag on matched calls.
361
- * Returns a new array (does not mutate input).
362
- */
363
- function linkToolResults(calls, results) {
364
- const resultMap = /* @__PURE__ */ new Map();
365
- for (const result of results) resultMap.set(result.toolUseId, result);
366
- return calls.map((call) => {
367
- const result = resultMap.get(call.toolUseId);
368
- if (!result) return call;
369
- return {
370
- ...call,
371
- result: formatToolResult(result.content),
372
- isError: result.isError ?? call.isError
373
- };
374
- });
375
- }
376
- /**
377
- * Format tool result content into a readable string.
378
- * Handles:
379
- * - stdout/stderr (command execution results)
380
- * - questions/answers (interactive prompts)
381
- * - generic JSON (everything else)
382
- */
383
- function formatToolResult(content) {
384
- if (content === null || content === void 0) return "";
385
- if (typeof content === "string") return content;
386
- if (typeof content === "object" && !Array.isArray(content)) {
387
- const obj = content;
388
- if ("stdout" in obj) {
389
- let result = String(obj.stdout ?? "");
390
- if (obj.stderr) result += `\n[stderr]: ${obj.stderr}`;
391
- return result;
392
- }
393
- if ("questions" in obj) return JSON.stringify({
394
- questions: obj.questions,
395
- answers: obj.answers
396
- });
397
- return JSON.stringify(content);
398
- }
399
- return JSON.stringify(content);
400
- }
401
- //#endregion
402
- //#region src/subagent-resolver.ts
403
- /**
404
- * Parallel detection window in milliseconds.
405
- * Subagents starting within this window are considered parallel.
406
- */
407
- const PARALLEL_WINDOW_MS = 100;
408
- /**
409
- * Resolve subagents from discovered files.
410
- * Links subagents to parent Task calls with 3-phase linking:
411
- * Phase 1: agentId matching (from Task tool result JSON)
412
- * Phase 2: description matching (fuzzy match on taskDescription)
413
- * Phase 3: positional fallback (unmatched Task calls in order)
414
- *
415
- * Also detects parallel execution and aggregates tokens.
416
- */
417
- function resolveSubagents(subagentFiles, parentToolCalls) {
418
- const parsed = [];
419
- for (const file of subagentFiles) {
420
- const result = parseSubagentFile(file.filePath);
421
- if (!result) continue;
422
- if (isWarmupAgent(result.records)) continue;
423
- if (file.agentId.startsWith("acompact")) continue;
424
- parsed.push({
425
- file,
426
- result
427
- });
428
- }
429
- const unmatchedTaskCalls = [...parentToolCalls.filter((tc) => tc.isTask || tc.name === "Agent")];
430
- const subagents = [];
431
- for (const { file, result } of parsed) {
432
- let parentTaskId = "";
433
- for (let i = 0; i < unmatchedTaskCalls.length; i++) {
434
- const tc = unmatchedTaskCalls[i];
435
- if (tc.result) try {
436
- if (JSON.parse(tc.result).agentId === file.agentId) {
437
- parentTaskId = tc.toolUseId;
438
- unmatchedTaskCalls.splice(i, 1);
439
- break;
440
- }
441
- } catch {}
442
- }
443
- if (!parentTaskId && result.description) for (let i = 0; i < unmatchedTaskCalls.length; i++) {
444
- const tc = unmatchedTaskCalls[i];
445
- if (tc.taskDescription) {
446
- const desc = result.description.toLowerCase();
447
- const taskDesc = tc.taskDescription.toLowerCase();
448
- if (desc.includes(taskDesc) || taskDesc.includes(desc)) {
449
- parentTaskId = tc.toolUseId;
450
- unmatchedTaskCalls.splice(i, 1);
451
- break;
452
- }
453
- }
454
- }
455
- if (!parentTaskId && unmatchedTaskCalls.length > 0) parentTaskId = unmatchedTaskCalls.shift().toolUseId;
456
- subagents.push({
457
- id: file.agentId,
458
- parentTaskId,
459
- description: result.description,
460
- startTime: result.records.length > 0 ? findStartTime(result.records) : "",
461
- endTime: result.records.length > 0 ? findEndTime(result.records) : "",
462
- turnCount: result.records.filter((r) => r.type === "assistant").length,
463
- status: "completed",
464
- isParallel: false,
465
- model: result.model,
466
- agentType: result.agentType,
467
- totalTokens: result.totalTokens,
468
- totalCost: computeSubagentCost(result.totalTokens, result.model),
469
- messages: result.messages,
470
- toolCalls: result.toolCalls
471
- });
472
- }
473
- detectParallelExecution(subagents);
474
- return subagents.sort((a, b) => {
475
- if (!a.startTime) return 1;
476
- if (!b.startTime) return -1;
477
- return a.startTime.localeCompare(b.startTime);
478
- });
479
- }
480
- /**
481
- * Parse a subagent JSONL file into structured data.
482
- * Returns null if file doesn't exist or is empty.
483
- */
484
- function parseSubagentFile(filePath) {
485
- if (!existsSync(filePath)) return null;
486
- try {
487
- const lines = readFileSync(filePath, "utf-8").split("\n").filter((line) => line.trim().length > 0);
488
- if (lines.length === 0) return null;
489
- const records = [];
490
- let malformedCount = 0;
491
- for (const line of lines) try {
492
- const entry = JSON.parse(line);
493
- records.push(entry);
494
- } catch {
495
- malformedCount++;
496
- }
497
- if (records.length === 0) return null;
498
- for (const record of records) if (record.message?.usage?.cache_creation) {
499
- const cc = record.message.usage.cache_creation;
500
- record.message.usage.cacheCreation5mTokens = cc.ephemeral_5m_input_tokens ?? 0;
501
- record.message.usage.cacheCreation1hTokens = cc.ephemeral_1h_input_tokens ?? 0;
502
- }
503
- const deduped = deduplicateByRequestId(records);
504
- const toolCalls = [];
505
- const assistantToolCallIndices = /* @__PURE__ */ new Map();
506
- for (const record of deduped) {
507
- if (record.type === "assistant" && record.message?.content) {
508
- const newCalls = extractToolCalls(record.message.content, record.timestamp);
509
- const startIdx = toolCalls.length;
510
- toolCalls.push(...newCalls);
511
- if (newCalls.length > 0 && record.uuid) {
512
- const indices = Array.from({ length: newCalls.length }, (_, i) => startIdx + i);
513
- assistantToolCallIndices.set(record.uuid, indices);
514
- }
515
- }
516
- if (record.type === "user" && record.isMeta && record.message?.content) {
517
- const results = extractToolResults(record.message.content);
518
- if (results.length > 0) {
519
- const updatedCalls = linkToolResults(toolCalls, results);
520
- for (let i = 0; i < updatedCalls.length; i++) if (updatedCalls[i].result !== toolCalls[i].result) toolCalls[i] = updatedCalls[i];
521
- }
522
- }
523
- if (record.toolUseResult && record.parentUuid) {
524
- const indices = assistantToolCallIndices.get(record.parentUuid);
525
- if (indices) for (const idx of indices) toolCalls[idx].result = JSON.stringify(record.toolUseResult);
526
- }
527
- }
528
- const messages = deduped.map((r) => ({
529
- type: r.type ?? "assistant",
530
- timestamp: r.timestamp,
531
- content: normalizeContent(r.message?.content ?? [])
532
- }));
533
- let model;
534
- for (const record of deduped) if (record.type === "assistant" && record.message?.model) {
535
- model = record.message.model;
536
- break;
537
- }
538
- const totalTokens = aggregateTokens(deduped);
539
- let description = "";
540
- const firstAssistant = deduped.find((r) => r.type === "assistant" && r.message?.content);
541
- if (firstAssistant?.message?.content && Array.isArray(firstAssistant.message.content)) {
542
- const textBlock = firstAssistant.message.content.find((b) => b.type === "text");
543
- if (textBlock && textBlock.text) description = String(textBlock.text).slice(0, 200);
544
- }
545
- const metaPath = filePath.replace(/\.jsonl$/, ".meta.json");
546
- let agentType;
547
- if (existsSync(metaPath)) try {
548
- const meta = JSON.parse(readFileSync(metaPath, "utf-8"));
549
- agentType = meta.agentType;
550
- if (meta.description) description = meta.description;
551
- } catch {}
552
- return {
553
- records: deduped,
554
- messages,
555
- toolCalls,
556
- description,
557
- agentType,
558
- model,
559
- totalTokens
560
- };
561
- } catch {
562
- return null;
563
- }
564
- }
565
- /**
566
- * Check if a subagent is a warmup agent.
567
- * Warmup agents have first user message === "Warmup".
568
- */
569
- function isWarmupAgent(records) {
570
- const firstUser = records.find((r) => r.type === "user");
571
- if (!firstUser) return false;
572
- const content = firstUser.message?.content;
573
- return typeof content === "string" && content === "Warmup";
574
- }
575
- /**
576
- * Find the earliest timestamp in records.
577
- */
578
- function findStartTime(records) {
579
- const timestamps = records.filter((r) => r.timestamp).map((r) => new Date(r.timestamp ?? "").getTime()).filter((t) => !Number.isNaN(t));
580
- if (timestamps.length === 0) return "";
581
- return new Date(Math.min(...timestamps)).toISOString();
582
- }
583
- /**
584
- * Find the latest timestamp in records.
585
- */
586
- function findEndTime(records) {
587
- const timestamps = records.filter((r) => r.timestamp).map((r) => new Date(r.timestamp ?? "").getTime()).filter((t) => !Number.isNaN(t));
588
- if (timestamps.length === 0) return "";
589
- return new Date(Math.max(...timestamps)).toISOString();
590
- }
591
- /**
592
- * Detect parallel execution among subagents.
593
- * Subagents are considered parallel if their time ranges overlap
594
- * within a 100ms window.
595
- */
596
- function detectParallelExecution(subagents) {
597
- for (let i = 0; i < subagents.length; i++) for (let j = i + 1; j < subagents.length; j++) {
598
- const a = subagents[i];
599
- const b = subagents[j];
600
- if (!a.startTime || !b.startTime || !a.endTime || !b.endTime) continue;
601
- const aStart = new Date(a.startTime).getTime();
602
- const aEnd = new Date(a.endTime).getTime();
603
- const bStart = new Date(b.startTime).getTime();
604
- if (aStart <= new Date(b.endTime).getTime() + PARALLEL_WINDOW_MS && bStart <= aEnd + PARALLEL_WINDOW_MS) {
605
- a.isParallel = true;
606
- b.isParallel = true;
607
- }
608
- }
609
- }
610
- /**
611
- * Aggregate token usage across records with request-id dedup.
612
- * Same logic as dedup.ts: only the last entry per requestId counts.
613
- */
614
- function aggregateTokens(records) {
615
- const totals = {
616
- inputTokens: 0,
617
- outputTokens: 0,
618
- cacheReadTokens: 0,
619
- cacheCreation5mTokens: 0,
620
- cacheCreation1hTokens: 0
621
- };
622
- const bestByRequestId = /* @__PURE__ */ new Map();
623
- for (const record of records) {
624
- const usage = record.message?.usage;
625
- if (!usage) continue;
626
- const tokens = {
627
- inputTokens: usage.input_tokens ?? 0,
628
- outputTokens: usage.output_tokens ?? 0,
629
- cacheReadTokens: usage.cache_read_input_tokens ?? 0,
630
- cacheCreation5mTokens: usage.cacheCreation5mTokens ?? 0,
631
- cacheCreation1hTokens: usage.cacheCreation1hTokens ?? 0
632
- };
633
- const rid = record.requestId;
634
- if (!rid) {
635
- totals.inputTokens += tokens.inputTokens;
636
- totals.outputTokens += tokens.outputTokens;
637
- totals.cacheReadTokens += tokens.cacheReadTokens;
638
- totals.cacheCreation5mTokens += tokens.cacheCreation5mTokens;
639
- totals.cacheCreation1hTokens += tokens.cacheCreation1hTokens;
640
- continue;
641
- }
642
- const existing = bestByRequestId.get(rid);
643
- if (!existing || tokens.outputTokens > existing.outputTokens) bestByRequestId.set(rid, {
644
- outputTokens: tokens.outputTokens,
645
- usage: tokens
646
- });
647
- }
648
- for (const { usage } of bestByRequestId.values()) {
649
- totals.inputTokens += usage.inputTokens;
650
- totals.outputTokens += usage.outputTokens;
651
- totals.cacheReadTokens += usage.cacheReadTokens;
652
- totals.cacheCreation5mTokens += usage.cacheCreation5mTokens;
653
- totals.cacheCreation1hTokens += usage.cacheCreation1hTokens;
654
- }
655
- return totals;
656
- }
657
- /**
658
- * Compute total cost for a subagent from aggregated tokens and model.
659
- * Uses a single synthetic Turn to calculate cost via the shared pricing logic.
660
- */
661
- function computeSubagentCost(tokens, model) {
662
- const rate = getPricing(normalizeModelName(model ?? ""));
663
- return calculateTurnCost({
664
- timestamp: (/* @__PURE__ */ new Date()).toISOString(),
665
- tokenUsage: tokens,
666
- model,
667
- messages: [],
668
- toolCalls: [],
669
- cacheWriteType: "none",
670
- cacheReadType: "unknown",
671
- cacheCreationTokensThisTurn: 0
672
- }, rate).totalCost;
673
- }
674
- /**
675
- * Normalize content blocks to MessageContent[].
676
- */
677
- function normalizeContent(content) {
678
- if (typeof content === "string") return [{
679
- type: "text",
680
- text: content
681
- }];
682
- if (!Array.isArray(content)) return [];
683
- return content.map((block) => {
684
- const type = block.type;
685
- if (type === "text") return {
686
- type: "text",
687
- text: String(block.text ?? "")
688
- };
689
- if (type === "tool_use") return {
690
- type: "tool_use",
691
- name: String(block.name ?? ""),
692
- input: block.input ?? {},
693
- toolUseId: String(block.id ?? block.toolUseId ?? "")
694
- };
695
- if (type === "tool_result") return {
696
- type: "tool_result",
697
- toolUseId: String(block.tool_use_id ?? block.toolUseId ?? ""),
698
- content: block.content ?? "",
699
- isError: block.is_error ?? block.isError
700
- };
701
- return {
702
- type: "text",
703
- text: JSON.stringify(block)
704
- };
705
- });
706
- }
707
- //#endregion
708
- //#region src/db-reader.ts
709
- /**
710
- * Compute active duration by summing gaps between consecutive timestamps
711
- * that are below a threshold (5 minutes). Large gaps represent idle/closed
712
- * sessions and are excluded.
713
- */
714
- function computeActiveDurationMs(timestamps, thresholdMs = 300 * 1e3) {
715
- if (timestamps.length < 2) return 0;
716
- let activeMs = 0;
717
- for (let i = 1; i < timestamps.length; i++) {
718
- const gap = new Date(timestamps[i]).getTime() - new Date(timestamps[i - 1]).getTime();
719
- if (gap > 0 && gap < thresholdMs) activeMs += gap;
720
- }
721
- return activeMs;
722
- }
723
- /** Error when SQLite DB cannot be opened */
724
- var DbOpenError = class extends Error {
725
- code = 3;
726
- constructor(message) {
727
- super(message);
728
- this.name = "DbOpenError";
729
- }
730
- };
731
- /** Error when session_id not found in DB */
732
- var SessionNotFoundError = class extends Error {
733
- code = 2;
734
- constructor(sessionId) {
735
- super(`Session not found: ${sessionId}`);
736
- this.name = "SessionNotFoundError";
737
- }
738
- };
739
- /**
740
- * Get session metadata from SQLite DB
741
- */
742
- function getSession(dbPath, sessionId) {
743
- let db;
744
- try {
745
- db = new Database(dbPath, { readonly: true });
746
- } catch (_err) {
747
- throw new DbOpenError(`Failed to open database: ${dbPath}`);
748
- }
749
- try {
750
- const row = db.prepare("SELECT * FROM sessions WHERE session_id = ?").get(sessionId);
751
- if (!row) throw new SessionNotFoundError(sessionId);
752
- const model = row.model || getModelForSession(dbPath, sessionId);
753
- const cwdRow = db.prepare("SELECT cwd, COUNT(*) as cnt FROM turns WHERE session_id = ? AND cwd IS NOT NULL GROUP BY cwd ORDER BY cnt DESC LIMIT 1").get(sessionId);
754
- const totalTokens = {
755
- inputTokens: row.total_input_tokens,
756
- outputTokens: row.total_output_tokens,
757
- cacheReadTokens: row.total_cache_read,
758
- cacheCreation5mTokens: row.total_cache_creation,
759
- cacheCreation1hTokens: 0
760
- };
761
- return {
762
- sessionId: row.session_id,
763
- projectName: row.project_name,
764
- model,
765
- workingDirectory: cwdRow?.cwd ?? "",
766
- turnCount: row.turn_count,
767
- totalTokens,
768
- startTime: row.first_timestamp,
769
- endTime: row.last_timestamp,
770
- isOngoing: false
771
- };
772
- } finally {
773
- db.close();
774
- }
775
- }
776
- /**
777
- * Get all turns for a session from SQLite DB
778
- */
779
- function getTurns(dbPath, sessionId) {
780
- let db;
781
- try {
782
- db = new Database(dbPath, { readonly: true });
783
- } catch (_err) {
784
- throw new DbOpenError(`Failed to open database: ${dbPath}`);
785
- }
786
- try {
787
- return db.prepare("SELECT * FROM turns WHERE session_id = ? ORDER BY timestamp ASC").all(sessionId).map((row) => ({
788
- timestamp: row.timestamp,
789
- tokenUsage: {
790
- inputTokens: row.input_tokens,
791
- outputTokens: row.output_tokens,
792
- cacheReadTokens: row.cache_read_tokens,
793
- cacheCreation5mTokens: row.cache_creation_tokens,
794
- cacheCreation1hTokens: 0
795
- },
796
- toolName: row.tool_name ?? void 0,
797
- cwd: row.cwd ?? void 0,
798
- messages: [],
799
- toolCalls: [],
800
- cacheWriteType: row.cache_creation_tokens > 0 ? "5m" : "none",
801
- cacheReadType: "unknown",
802
- cacheCreationTokensThisTurn: row.cache_creation_tokens
803
- }));
804
- } finally {
805
- db.close();
806
- }
807
- }
808
- /**
809
- * Get the model for a session (from first turn)
810
- * Falls back to 'claude-sonnet-4-6' if not found
811
- */
812
- function getModelForSession(dbPath, sessionId) {
813
- let db;
814
- try {
815
- db = new Database(dbPath, { readonly: true });
816
- } catch (_err) {
817
- throw new DbOpenError(`Failed to open database: ${dbPath}`);
818
- }
819
- try {
820
- return db.prepare("SELECT model FROM turns WHERE session_id = ? ORDER BY timestamp ASC LIMIT 1").get(sessionId)?.model ?? "claude-sonnet-4-6";
821
- } finally {
822
- db.close();
823
- }
824
- }
825
- /**
826
- * List all sessions from the DB, ordered by most recent first.
827
- */
828
- function listSessions(dbPath, limit = 20) {
829
- let db;
830
- try {
831
- db = new Database(dbPath, { readonly: true });
832
- } catch (_err) {
833
- throw new DbOpenError(`Failed to open database: ${dbPath}`);
834
- }
835
- try {
836
- return db.prepare(`SELECT session_id, project_name, model, turn_count, first_timestamp, last_timestamp,
837
- total_input_tokens, total_output_tokens, total_cache_read, total_cache_creation
838
- FROM sessions ORDER BY last_timestamp DESC LIMIT ?`).all(limit).map((row) => {
839
- const model = row.model || "claude-sonnet-4-6";
840
- const session = {
841
- sessionId: row.session_id,
842
- projectName: row.project_name,
843
- model,
844
- workingDirectory: "",
845
- turnCount: row.turn_count,
846
- totalTokens: {
847
- inputTokens: row.total_input_tokens,
848
- outputTokens: row.total_output_tokens,
849
- cacheReadTokens: row.total_cache_read,
850
- cacheCreation5mTokens: row.total_cache_creation,
851
- cacheCreation1hTokens: 0
852
- },
853
- startTime: row.first_timestamp,
854
- endTime: row.last_timestamp,
855
- isOngoing: false
856
- };
857
- const pricing = calculateSessionCost(session, [{
858
- timestamp: row.last_timestamp,
859
- tokenUsage: session.totalTokens,
860
- messages: [],
861
- toolCalls: [],
862
- cacheWriteType: row.total_cache_creation > 0 ? "5m" : "none",
863
- cacheReadType: "unknown",
864
- cacheCreationTokensThisTurn: row.total_cache_creation
865
- }]);
866
- return {
867
- sessionId: row.session_id,
868
- projectName: row.project_name,
869
- model,
870
- turnCount: row.turn_count,
871
- lastTimestamp: row.last_timestamp,
872
- totalCostEstimate: pricing.totalCost,
873
- hasThinking: false
874
- };
875
- });
876
- } finally {
877
- db.close();
878
- }
879
- }
880
- /**
881
- * Get the set of session IDs that exist in the SQLite DB.
882
- */
883
- function getExistingSessionIds(dbPath) {
884
- try {
885
- const db = new Database(dbPath, { readonly: true });
886
- try {
887
- const rows = db.prepare("SELECT session_id FROM sessions").all();
888
- return new Set(rows.map((r) => r.session_id));
889
- } finally {
890
- db.close();
891
- }
892
- } catch {
893
- return /* @__PURE__ */ new Set();
894
- }
895
- }
896
- /**
897
- * Parse a JSONL file header to extract session summary metadata.
898
- * Reads the file incrementally — stops after finding enough data.
899
- */
900
- function parseJsonlSummary(filePath, sessionId, projectName) {
901
- try {
902
- const lines = readFileSync(filePath, "utf-8").split("\n").filter((l) => l.trim().length > 0);
903
- if (lines.length === 0) return null;
904
- const allRecords = [];
905
- for (const line of lines) try {
906
- allRecords.push(JSON.parse(line));
907
- } catch {
908
- continue;
909
- }
910
- const records = deduplicateByRequestId(allRecords.filter((r) => classifyMessage(r) !== "hardNoise"));
911
- let model = "claude-sonnet-4-6";
912
- let turnCount = 0;
913
- let lastTimestamp = "";
914
- let totalInput = 0;
915
- let totalOutput = 0;
916
- let totalCacheRead = 0;
917
- let totalCacheCreation5m = 0;
918
- let totalCacheCreation1h = 0;
919
- let hasThinking = false;
920
- let lastFileTimestamp = "";
921
- const allTimestamps = [];
922
- for (const record of records) {
923
- const category = classifyMessage(record);
924
- const msg = record.message;
925
- const ts = record.timestamp;
926
- if (ts) lastFileTimestamp = ts;
927
- const hasContent = totalInput + totalOutput + totalCacheRead + totalCacheCreation5m + totalCacheCreation1h > 0 || msg?.usage != null || typeof msg?.content === "string" && msg.content.length > 0 || Array.isArray(msg?.content ?? []) && (msg?.content ?? []).length > 0;
928
- if (ts && hasContent) {
929
- lastTimestamp = ts;
930
- allTimestamps.push(ts);
931
- }
932
- if (record.type === "assistant" && msg?.model) model = msg.model;
933
- if (category === "assistant" || category === "user") turnCount++;
934
- const usage = msg?.usage;
935
- if (usage) {
936
- totalInput += usage.input_tokens ?? 0;
937
- totalOutput += usage.output_tokens ?? 0;
938
- totalCacheRead += usage.cache_read_input_tokens ?? 0;
939
- const cc = usage.cache_creation;
940
- totalCacheCreation5m += usage.cacheCreation5mTokens ?? cc?.ephemeral_5m_input_tokens ?? 0;
941
- totalCacheCreation1h += usage.cacheCreation1hTokens ?? cc?.ephemeral_1h_input_tokens ?? 0;
942
- }
943
- if (!hasThinking && record.type === "assistant" && Array.isArray(msg?.content)) hasThinking = msg.content.some((b) => b.type === "thinking");
944
- }
945
- const session = {
946
- sessionId,
947
- projectName,
948
- model,
949
- workingDirectory: "",
950
- turnCount,
951
- totalTokens: {
952
- inputTokens: totalInput,
953
- outputTokens: totalOutput,
954
- cacheReadTokens: totalCacheRead,
955
- cacheCreation5mTokens: totalCacheCreation5m,
956
- cacheCreation1hTokens: totalCacheCreation1h
957
- },
958
- startTime: "",
959
- endTime: lastTimestamp || lastFileTimestamp || (/* @__PURE__ */ new Date()).toISOString(),
960
- isOngoing: false
961
- };
962
- const pricing = calculateSessionCost(session, totalInput > 0 || totalOutput > 0 ? [{
963
- timestamp: lastTimestamp || lastFileTimestamp || (/* @__PURE__ */ new Date()).toISOString(),
964
- tokenUsage: session.totalTokens,
965
- messages: [],
966
- toolCalls: [],
967
- cacheWriteType: totalCacheCreation5m > 0 ? "5m" : totalCacheCreation1h > 0 ? "1h" : "none",
968
- cacheReadType: "unknown",
969
- cacheCreationTokensThisTurn: totalCacheCreation5m + totalCacheCreation1h
970
- }] : []);
971
- let agentCost = 0;
972
- try {
973
- const agentFiles = listSubagentFiles(join(dirname(filePath), ".."), projectName, sessionId);
974
- if (agentFiles.length > 0) agentCost = resolveSubagents(agentFiles, []).reduce((sum, s) => sum + (s.totalCost ?? 0), 0);
975
- } catch {}
976
- return {
977
- sessionId,
978
- projectName,
979
- model,
980
- turnCount,
981
- lastTimestamp: lastTimestamp || lastFileTimestamp || (/* @__PURE__ */ new Date()).toISOString(),
982
- totalCostEstimate: pricing.totalCost + agentCost,
983
- hasThinking,
984
- activeDurationMs: computeActiveDurationMs(allTimestamps)
985
- };
986
- } catch {
987
- return null;
988
- }
989
- }
990
- /**
991
- * List sessions discovered from JSONL files on disk.
992
- * Skips sessions that already exist in the SQLite DB.
993
- * Skips subagent files (agent-*.jsonl).
994
- */
995
- function listJsonlSessions(projectsDir, dbPath, limit = 100) {
996
- const existingIds = getExistingSessionIds(dbPath);
997
- const results = [];
998
- if (!existsSync(projectsDir)) return results;
999
- try {
1000
- const projectDirs = readdirSync(projectsDir);
1001
- for (const dirName of projectDirs) {
1002
- const projectDir = join(projectsDir, dirName);
1003
- try {
1004
- if (!statSync(projectDir).isDirectory()) continue;
1005
- } catch {
1006
- continue;
1007
- }
1008
- const projectName = dirName.startsWith("-") ? dirName.slice(1) : dirName;
1009
- try {
1010
- const files = readdirSync(projectDir);
1011
- for (const file of files) {
1012
- if (!file.endsWith(".jsonl")) continue;
1013
- if (file.startsWith("agent-")) continue;
1014
- const sessionId = file.replace(".jsonl", "");
1015
- if (existingIds.has(sessionId)) continue;
1016
- const summary = parseJsonlSummary(join(projectDir, file), sessionId, projectName);
1017
- if (summary) results.push(summary);
1018
- }
1019
- } catch {}
1020
- }
1021
- } catch {}
1022
- results.sort((a, b) => b.lastTimestamp.localeCompare(a.lastTimestamp));
1023
- return results.slice(0, limit);
1024
- }
1025
- //#endregion
1026
- export { deduplicateByRequestId as _, getTurns as a, resolveSubagents as c, formatToolResult as d, linkToolResults as f, resolveSessionJsonlPath as g, getProjectsDir as h, getSession as i, extractToolCalls as l, getDbPath as m, SessionNotFoundError as n, listJsonlSessions as o, listSubagentFiles as p, getModelForSession as r, listSessions as s, DbOpenError as t, extractToolResults as u, classifyMessage as v };
1027
-
1028
- //# sourceMappingURL=db-reader-BrPRGqww.mjs.map