@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.
- package/dist/cli.js +1025 -46
- package/dist/db-reader.d.ts.map +1 -1
- package/dist/{pricing-DTmya3JY.mjs → pricing-5MZ5_SQc.mjs} +1 -1
- package/dist/{pricing-DTmya3JY.mjs.map → pricing-5MZ5_SQc.mjs.map} +1 -1
- package/dist/pricing-B9Z0E171.mjs +2 -0
- package/dist/server.cjs +69 -2
- package/dist/web/assets/{index-Dr0FGYfS.js → index-BbY4gr3z.js} +1 -1
- package/dist/web/index.html +1 -1
- package/package.json +1 -1
- package/dist/db-reader-BrPRGqww.mjs +0 -1028
- package/dist/db-reader-BrPRGqww.mjs.map +0 -1
- package/dist/db-reader-CPXmkt55.mjs +0 -2
- package/dist/pricing-B-rwfwDB.mjs +0 -2
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"db-reader-BrPRGqww.mjs","names":[],"sources":["../packages/extractor/src/classifier.ts","../packages/extractor/src/dedup.ts","../packages/extractor/src/utils.ts","../packages/extractor/src/subagent-locator.ts","../packages/extractor/src/tool-extraction.ts","../packages/extractor/src/subagent-resolver.ts","../packages/extractor/src/db-reader.ts"],"sourcesContent":["import type { ClassifiedMessage, MessageCategory, RawJsonlRecord } from \"./types\"\n\n/** Entry types that are always noise */\nconst NOISE_TYPES = new Set([\"system\", \"summary\", \"file-history-snapshot\", \"queue-operation\", \"attachment\", \"last-prompt\", \"permission-mode\", \"ai-title\"])\n\n/** Hard noise tags that should be filtered out entirely */\nconst HARD_NOISE_TAGS = [\"<local-command-caveat>\", \"<system-reminder>\"]\n\n/** Command output tags that map to system category */\nconst COMMAND_OUTPUT_TAGS = [\"<local-command-stdout>\", \"<local-command-stderr>\"]\n\n/** Check if content string starts with any of the given tags */\nfunction startsWithTag(content: string, tags: readonly string[]): boolean {\n for (const tag of tags) {\n if (content.startsWith(tag)) return true\n }\n return false\n}\n\n/** Check if the content array has at least one text or image block */\nfunction hasTextOrImageContent(\n content: Array<Record<string, unknown>> | string,\n): boolean {\n if (typeof content === \"string\") return content.length > 0\n return content.some((block) => block.type === \"text\" || block.type === \"image\")\n}\n\n/**\n * Check if an array content is tool_result-only (no text/image blocks).\n * These are tool execution results coming back from the CLI — they represent\n * assistant context, not actual user-typed input.\n */\nfunction isToolResultOnly(\n content: Array<Record<string, unknown>> | string,\n): boolean {\n if (typeof content === \"string\") return false\n return (\n content.length > 0 &&\n content.every((block) => block.type === \"tool_result\")\n )\n}\n\n// ─── Type guard functions ───────────────────────────────────────────\n\n/**\n * Hard noise: system/summary/file-history-snapshot/queue-operation/attachment/last-prompt/permission-mode types,\n * sidechain, synthetic assistant, hard noise tags, interruptions.\n */\nexport function isHardNoise(record: RawJsonlRecord): boolean {\n const type = record.type\n\n // Noise entry types\n if (NOISE_TYPES.has(type)) return true\n\n // Sidechain (subagent) messages\n if (record.isSidechain) return true\n\n const message = record.message\n\n // Synthetic assistant messages\n if (type === \"assistant\" && message?.model === \"<synthetic>\") return true\n\n // User messages: check content for hard noise tags and interruptions\n if (type === \"user\" && message?.content !== undefined) {\n const { content } = message\n if (typeof content === \"string\") {\n if (startsWithTag(content, HARD_NOISE_TAGS)) return true\n if (content === \"[Request interrupted by user]\") return true\n }\n }\n\n return false\n}\n\n/** Compact messages are marked by isCompactSummary flag */\nexport function isCompactMessage(record: RawJsonlRecord): boolean {\n return record.isCompactSummary === true\n}\n\n/**\n * System messages: user-type messages that contain command output\n * (local-command-stdout/stderr). These arrive as type=\"user\" in JSONL\n * but represent command output, not user input.\n */\nexport function isSystemMessage(record: RawJsonlRecord): boolean {\n if (record.type !== \"user\") return false\n const content = record.message?.content\n if (typeof content !== \"string\") return false\n return startsWithTag(content, COMMAND_OUTPUT_TAGS)\n}\n\n/**\n * User messages: type=user, isMeta=false, has text/image content\n * (not just tool_result blocks). Meta messages (tool results) are\n * classified as assistant because they represent assistant context.\n * Tool-result-only records (isMeta=null, content is array of tool_result)\n * are also classified as assistant — they're CLI tool outputs, not user input.\n */\nexport function isUserMessage(record: RawJsonlRecord): boolean {\n if (record.type !== \"user\") return false\n if (record.isMeta) return false\n\n const content = record.message?.content\n if (content === undefined) return false\n\n // String content is always user text\n if (typeof content === \"string\") return true\n\n // Array content: tool_result-only = NOT a user message (it's CLI tool output)\n if (isToolResultOnly(content)) return false\n\n // Array content: must have at least one text or image block\n return hasTextOrImageContent(content)\n}\n\n// ─── Classification functions ───────────────────────────────────────\n\n/**\n * Classify a single JSONL record into a category using the priority cascade:\n * 1. hardNoise — noise types, sidechain, synthetic, hard noise tags, interruptions\n * 2. compact — isCompactSummary === true\n * 3. system — user messages with command output (local-command-stdout/stderr)\n * 4. user — type=user, not meta, has text/image content\n * 5. assistant — everything else (catch-all)\n */\nexport function classifyMessage(record: RawJsonlRecord): MessageCategory {\n if (isHardNoise(record)) return \"hardNoise\"\n if (isCompactMessage(record)) return \"compact\"\n if (isSystemMessage(record)) return \"system\"\n if (isUserMessage(record)) return \"user\"\n return \"assistant\"\n}\n\n/**\n * Classify an array of messages, returning ClassifiedMessage objects\n * that pair each record with its category.\n */\nexport function classifyMessages(records: RawJsonlRecord[]): ClassifiedMessage[] {\n return records.map((record) => ({\n record,\n category: classifyMessage(record),\n }))\n}\n","import type { RawJsonlRecord } from \"./types\"\n\n/**\n * Deduplicate streaming assistant entries by requestId.\n *\n * Claude Code writes multiple JSONL entries per API response during streaming:\n * - Streaming duplicates: same requestId with incrementally increasing output_tokens\n * - Content blocks: same requestId with identical output_tokens (thinking/text/tool_use)\n *\n * Strategy:\n * 1. For streaming duplicates (strictly increasing tokens): keep only the last entry\n * 2. For content blocks (equal tokens): MERGE into one record by concatenating content arrays\n *\n * Entries without a requestId (user, system, tool results) pass through unchanged.\n */\nexport function deduplicateByRequestId(records: RawJsonlRecord[]): RawJsonlRecord[] {\n // Map from requestId -> merged entry info\n const mergedByRequestId = new Map<\n string,\n { index: number; outputTokens: number; merged: RawJsonlRecord }\n >()\n\n // Track whether output_tokens actually increased for any requestId\n // (indicates real streaming continuation vs different content blocks)\n const hasStrictIncrease = new Set<string>()\n\n for (let i = 0; i < records.length; i++) {\n const rid = records[i].requestId\n if (!rid) continue\n\n const outputTokens = records[i].message?.usage?.output_tokens ?? 0\n const existing = mergedByRequestId.get(rid)\n\n if (existing) {\n if (outputTokens > existing.outputTokens) {\n // Strict increase — real streaming continuation, replace with this entry\n hasStrictIncrease.add(rid)\n mergedByRequestId.set(rid, { index: i, outputTokens, merged: records[i] })\n } else if (outputTokens === existing.outputTokens) {\n // Equal tokens — different content blocks (thinking/text/tool_use)\n // Merge content arrays into the existing entry\n existing.merged = mergeContentBlocks(existing.merged, records[i])\n }\n // Lower tokens: ignore (earlier streaming entry)\n } else {\n mergedByRequestId.set(rid, { index: i, outputTokens, merged: records[i] })\n }\n }\n\n // If no requestIds found, no dedup needed\n if (mergedByRequestId.size === 0) {\n return records\n }\n\n // Build set of original indices to replace\n const requestIdIndices = new Map<string, Set<number>>()\n for (let i = 0; i < records.length; i++) {\n const rid = records[i].requestId\n if (!rid) continue\n let indices = requestIdIndices.get(rid)\n if (!indices) {\n indices = new Set()\n requestIdIndices.set(rid, indices)\n }\n indices.add(i)\n }\n\n // Build result: for each requestId, output the merged record at the FIRST index,\n // skip all other indices for that requestId\n const result: RawJsonlRecord[] = []\n const emittedRequestIds = new Set<string>()\n\n for (let i = 0; i < records.length; i++) {\n const rid = records[i].requestId\n if (!rid) {\n // No requestId: pass through\n result.push(records[i])\n continue\n }\n\n const merged = mergedByRequestId.get(rid)\n if (!merged) continue\n\n if (!emittedRequestIds.has(rid)) {\n // Emit the merged record at the first occurrence\n result.push(merged.merged)\n emittedRequestIds.add(rid)\n }\n // Skip all other occurrences of this requestId\n }\n\n return result\n}\n\n/**\n * Merge content blocks from two records with the same requestId.\n * Concatenates the content arrays, keeping all unique content types.\n */\nfunction mergeContentBlocks(\n existing: RawJsonlRecord,\n incoming: RawJsonlRecord,\n): RawJsonlRecord {\n const existingContent = existing.message?.content\n const incomingContent = incoming.message?.content\n\n // If either doesn't have array content, keep the existing\n if (!Array.isArray(existingContent) || !Array.isArray(incomingContent)) {\n return existing\n }\n\n // Concatenate content arrays (thinking + text + tool_use blocks)\n const mergedContent = [...existingContent, ...incomingContent]\n\n return {\n ...existing,\n message: existing.message ? {\n ...existing.message,\n content: mergedContent,\n } : existing.message,\n }\n}\n","import { existsSync } from \"node:fs\"\nimport { homedir } from \"node:os\"\nimport { join } from \"node:path\"\n\n/**\n * Get the path to usage.db\n * Priority: customPath > CLAUDE_CONFIG_DIR env > ~/.claude\n */\nexport function getDbPath(customPath?: string): string {\n if (customPath) return customPath\n const configDir = process.env.CLAUDE_CONFIG_DIR || join(homedir(), \".claude\")\n return join(configDir, \"usage.db\")\n}\n\n/**\n * Get the path to the projects directory\n * Priority: customPath > CLAUDE_CONFIG_DIR env > ~/.claude\n */\nexport function getProjectsDir(customPath?: string): string {\n if (customPath) return customPath\n const configDir = process.env.CLAUDE_CONFIG_DIR || join(homedir(), \".claude\")\n return join(configDir, \"projects\")\n}\n\n/**\n * Encode project name for directory lookup\n * Replaces all '/' with '-' (e.g., '/Users/test' → '-Users-test')\n */\nexport function encodeProjectName(projectName: string): string {\n return projectName.replaceAll(\"/\", \"-\")\n}\n\n/**\n * Resolve the path to a session's JSONL file\n * Tries multiple encodings to handle DB storing project_name with or without leading '/'\n */\nexport function resolveSessionJsonlPath(\n session: { projectName: string; sessionId: string },\n projectsDir: string,\n): string | null {\n const candidates: string[] = []\n\n // Direct encoding of what's in the DB\n candidates.push(encodeProjectName(session.projectName))\n\n // If no leading '/', try with leading '/' (DB sometimes strips it)\n if (!session.projectName.startsWith(\"/\")) {\n candidates.push(encodeProjectName(`/${session.projectName}`))\n }\n\n // If has leading '/', try without it\n if (session.projectName.startsWith(\"/\")) {\n candidates.push(encodeProjectName(session.projectName.slice(1)))\n }\n\n // URL-encoded fallback\n candidates.push(encodeURIComponent(session.projectName))\n\n for (const encoded of candidates) {\n const filePath = join(projectsDir, encoded, `${session.sessionId}.jsonl`)\n if (existsSync(filePath)) return filePath\n }\n\n return null\n}\n","import { existsSync, readdirSync, readFileSync } from \"node:fs\"\nimport { basename, join } from \"node:path\"\nimport { encodeProjectName } from \"./utils\"\nimport type { SubagentFile } from \"./types\"\n\n/**\n * List subagent files for a session.\n * Scans two directory structures:\n * - New nested: {projectsDir}/{project}/{session}/subagents/agent-{id}.jsonl\n * - Legacy flat: {projectsDir}/{project}/agent-{id}.jsonl (filtered by sessionId)\n *\n * Returns NEW structure files first, then legacy flat files.\n */\nexport function listSubagentFiles(\n projectsDir: string,\n projectName: string,\n sessionId: string,\n): SubagentFile[] {\n const encodedProject = encodeProjectName(projectName)\n const allFiles: SubagentFile[] = []\n\n // Try both encoded name and with leading '-' (Claude Code uses '-' prefix for paths)\n const candidates = [encodedProject]\n if (!encodedProject.startsWith(\"-\")) {\n candidates.push(`-${encodedProject}`)\n }\n\n for (const projectDirName of candidates) {\n // 1. Scan NEW nested structure: {project}/{session}/subagents/agent-*.jsonl\n const newSubagentsDir = join(projectsDir, projectDirName, sessionId, \"subagents\")\n if (existsSync(newSubagentsDir)) {\n try {\n const entries = readdirSync(newSubagentsDir)\n for (const entry of entries) {\n if (entry.startsWith(\"agent-\") && entry.endsWith(\".jsonl\")) {\n const agentId = extractAgentId(entry)\n if (agentId) {\n allFiles.push({\n filePath: join(newSubagentsDir, entry),\n agentId,\n isNewStructure: true,\n })\n }\n }\n }\n } catch {\n // Ignore directory read errors\n }\n }\n\n // 2. Scan legacy flat structure: {project}/agent-*.jsonl\n const projectDir = join(projectsDir, projectDirName)\n if (existsSync(projectDir)) {\n try {\n const entries = readdirSync(projectDir)\n for (const entry of entries) {\n if (entry.startsWith(\"agent-\") && entry.endsWith(\".jsonl\")) {\n const agentId = extractAgentId(entry)\n if (!agentId) continue\n\n // Skip compact agents\n if (isCompactAgent(agentId)) continue\n\n // Legacy files are in project root — must filter by sessionId\n const filePath = join(projectDir, entry)\n if (subagentBelongsToSession(filePath, sessionId)) {\n allFiles.push({\n filePath,\n agentId,\n isNewStructure: false,\n })\n }\n }\n }\n } catch {\n // Ignore directory read errors\n }\n }\n }\n\n return allFiles\n}\n\n/**\n * Check if a session has any subagent files.\n * Fast check — returns true if at least one subagent file exists.\n */\nexport function hasSubagents(\n projectsDir: string,\n projectName: string,\n sessionId: string,\n): boolean {\n const encodedProject = encodeProjectName(projectName)\n\n // Try both encoded name and with leading '-'\n const candidates = [encodedProject]\n if (!encodedProject.startsWith(\"-\")) {\n candidates.push(`-${encodedProject}`)\n }\n\n for (const projectDirName of candidates) {\n // Check NEW nested structure first\n const newSubagentsDir = join(projectsDir, projectDirName, sessionId, \"subagents\")\n if (existsSync(newSubagentsDir)) {\n try {\n const entries = readdirSync(newSubagentsDir)\n for (const entry of entries) {\n if (entry.startsWith(\"agent-\") && entry.endsWith(\".jsonl\")) {\n const filePath = join(newSubagentsDir, entry)\n const stat = readFileSync(filePath, \"utf-8\")\n if (stat.trim().length > 0) return true\n }\n }\n } catch {\n // Ignore errors\n }\n }\n\n // Check legacy flat structure\n const projectDir = join(projectsDir, projectDirName)\n if (existsSync(projectDir)) {\n try {\n const entries = readdirSync(projectDir)\n for (const entry of entries) {\n if (entry.startsWith(\"agent-\") && entry.endsWith(\".jsonl\")) {\n const agentId = extractAgentId(entry)\n if (!agentId || isCompactAgent(agentId)) continue\n\n const filePath = join(projectDir, entry)\n if (subagentBelongsToSession(filePath, sessionId)) {\n return true\n }\n }\n }\n } catch {\n // Ignore errors\n }\n }\n }\n\n return false\n}\n\n/**\n * Extract agent ID from filename.\n * e.g., \"agent-abc123.jsonl\" → \"abc123\"\n */\nexport function extractAgentId(filename: string): string | null {\n const name = basename(filename)\n const match = name.match(/^agent-([^.]+)\\.jsonl$/)\n return match ? match[1] : null\n}\n\n/**\n * Check if agent ID belongs to a compact agent (starts with \"acompact\").\n */\nexport function isCompactAgent(agentId: string): boolean {\n return agentId.startsWith(\"acompact\")\n}\n\n/**\n * Check if a legacy subagent file belongs to a specific session.\n * Reads the first line to check the sessionId field.\n */\nexport function subagentBelongsToSession(filePath: string, sessionId: string): boolean {\n try {\n const content = readFileSync(filePath, \"utf-8\")\n const firstNewline = content.indexOf(\"\\n\")\n const firstLine = firstNewline > 0 ? content.slice(0, firstNewline) : content\n\n if (!firstLine.trim()) return false\n\n const entry = JSON.parse(firstLine) as { sessionId?: string }\n return entry.sessionId === sessionId\n } catch {\n return false\n }\n}\n","import type { ToolCall } from \"./types\"\n\n/**\n * Extract tool calls from assistant message content.\n * Filters tool_use blocks and identifies Task tools specially.\n */\nexport function extractToolCalls(\n content: Array<Record<string, unknown>> | string,\n timestamp?: string,\n): ToolCall[] {\n if (typeof content === \"string\") return []\n if (!Array.isArray(content)) return []\n\n const calls: ToolCall[] = []\n for (const block of content) {\n if (block.type !== \"tool_use\") continue\n\n const toolUseId = String(block.id ?? block.toolUseId ?? \"\")\n const name = String(block.name ?? \"\")\n const input = (block.input as Record<string, unknown>) ?? {}\n\n // Identify Task tool (subagent spawn)\n const isTask = name === \"Task\"\n let taskDescription: string | undefined\n let taskSubagentType: string | undefined\n\n if (isTask && input) {\n taskDescription = typeof input.description === \"string\" ? input.description : undefined\n taskSubagentType = typeof input.subagent_type === \"string\" ? input.subagent_type : undefined\n }\n\n calls.push({\n toolUseId,\n name,\n input,\n timestamp,\n isTask,\n taskDescription,\n taskSubagentType,\n })\n }\n return calls\n}\n\n/**\n * Extract tool results from user message content.\n * Filters tool_result blocks from content array.\n */\nexport function extractToolResults(content: Array<Record<string, unknown>> | string): ToolResult[] {\n if (typeof content === \"string\") return []\n if (!Array.isArray(content)) return []\n\n const results: ToolResult[] = []\n for (const block of content) {\n if (block.type !== \"tool_result\") continue\n\n results.push({\n toolUseId: String(block.tool_use_id ?? block.toolUseId ?? \"\"),\n content: block.content,\n isError: Boolean(block.is_error) || Boolean(block.isError),\n })\n }\n return results\n}\n\n/**\n * Raw tool result extracted from message content.\n */\nexport interface ToolResult {\n toolUseId: string\n content: unknown\n isError?: boolean\n}\n\n/**\n * Link tool results to tool calls by toolUseId.\n * Sets result string and isError flag on matched calls.\n * Returns a new array (does not mutate input).\n */\nexport function linkToolResults(calls: ToolCall[], results: ToolResult[]): ToolCall[] {\n // Build a map from toolUseId to results\n const resultMap = new Map<string, ToolResult>()\n for (const result of results) {\n resultMap.set(result.toolUseId, result)\n }\n\n // Link results to calls\n return calls.map((call) => {\n const result = resultMap.get(call.toolUseId)\n if (!result) return call\n\n return {\n ...call,\n result: formatToolResult(result.content),\n isError: result.isError ?? call.isError,\n }\n })\n}\n\n/**\n * Format tool result content into a readable string.\n * Handles:\n * - stdout/stderr (command execution results)\n * - questions/answers (interactive prompts)\n * - generic JSON (everything else)\n */\nexport function formatToolResult(content: unknown): string {\n if (content === null || content === undefined) return \"\"\n if (typeof content === \"string\") return content\n\n // Handle object content\n if (typeof content === \"object\" && !Array.isArray(content)) {\n const obj = content as Record<string, unknown>\n\n // Command execution results with stdout/stderr\n if (\"stdout\" in obj) {\n let result = String(obj.stdout ?? \"\")\n if (obj.stderr) {\n result += `\\n[stderr]: ${obj.stderr}`\n }\n return result\n }\n\n // Interactive prompt results with questions/answers\n if (\"questions\" in obj) {\n return JSON.stringify({ questions: obj.questions, answers: obj.answers })\n }\n\n // Generic JSON fallback\n return JSON.stringify(content)\n }\n\n // Array or other types\n return JSON.stringify(content)\n}\n","import { existsSync, readFileSync } from \"node:fs\"\nimport { deduplicateByRequestId } from \"./dedup\"\nimport {\n extractToolCalls,\n extractToolResults,\n linkToolResults,\n} from \"./tool-extraction\"\nimport { calculateTurnCost, getPricing } from \"./pricing\"\nimport { normalizeModelName } from \"./model-parser\"\nimport type {\n Message,\n MessageContent,\n RawJsonlRecord,\n Subagent,\n SubagentFile,\n TokenUsage,\n ToolCall,\n Turn,\n TurnPricing,\n} from \"./types\"\n\n/**\n * Parallel detection window in milliseconds.\n * Subagents starting within this window are considered parallel.\n */\nconst PARALLEL_WINDOW_MS = 100\n\n/**\n * Result of parsing a subagent JSONL file.\n */\ninterface SubagentParseResult {\n records: RawJsonlRecord[]\n messages: Message[]\n toolCalls: ToolCall[]\n description: string\n agentType?: string\n model?: string\n totalTokens: TokenUsage\n}\n\n/**\n * Resolve subagents from discovered files.\n * Links subagents to parent Task calls with 3-phase linking:\n * Phase 1: agentId matching (from Task tool result JSON)\n * Phase 2: description matching (fuzzy match on taskDescription)\n * Phase 3: positional fallback (unmatched Task calls in order)\n *\n * Also detects parallel execution and aggregates tokens.\n */\nexport function resolveSubagents(\n subagentFiles: SubagentFile[],\n parentToolCalls: ToolCall[],\n): Subagent[] {\n // Parse all subagent files\n const parsed: Array<{ file: SubagentFile; result: SubagentParseResult }> = []\n for (const file of subagentFiles) {\n const result = parseSubagentFile(file.filePath)\n if (!result) continue\n\n // Skip warmup subagents\n if (isWarmupAgent(result.records)) continue\n\n // Skip compact agents (safety check)\n if (file.agentId.startsWith(\"acompact\")) continue\n\n parsed.push({ file, result })\n }\n\n // Get Task/Agent tool calls from parent (both \"Task\" and \"Agent\" named tools)\n const taskCalls = parentToolCalls.filter((tc) => tc.isTask || tc.name === \"Agent\")\n const unmatchedTaskCalls = [...taskCalls]\n\n const subagents: Subagent[] = []\n\n for (const { file, result } of parsed) {\n // Phase 1: Match by agentId from Task tool result\n let parentTaskId = \"\"\n let matchedTaskCall: ToolCall | undefined\n\n for (let i = 0; i < unmatchedTaskCalls.length; i++) {\n const tc = unmatchedTaskCalls[i]\n if (tc.result) {\n try {\n const resultObj = JSON.parse(tc.result) as { agentId?: string }\n if (resultObj.agentId === file.agentId) {\n parentTaskId = tc.toolUseId\n matchedTaskCall = tc\n unmatchedTaskCalls.splice(i, 1)\n break\n }\n } catch {\n // Result might not be JSON\n }\n }\n }\n\n // Phase 2: Match by description if no agentId match\n if (!parentTaskId && result.description) {\n for (let i = 0; i < unmatchedTaskCalls.length; i++) {\n const tc = unmatchedTaskCalls[i]\n if (tc.taskDescription) {\n const desc = result.description.toLowerCase()\n const taskDesc = tc.taskDescription.toLowerCase()\n if (desc.includes(taskDesc) || taskDesc.includes(desc)) {\n parentTaskId = tc.toolUseId\n matchedTaskCall = tc\n unmatchedTaskCalls.splice(i, 1)\n break\n }\n }\n }\n }\n\n // Phase 3: Positional fallback — assign next unmatched Task call\n if (!parentTaskId && unmatchedTaskCalls.length > 0) {\n const tc = unmatchedTaskCalls.shift()!\n parentTaskId = tc.toolUseId\n matchedTaskCall = tc\n }\n\n subagents.push({\n id: file.agentId,\n parentTaskId,\n description: result.description,\n startTime: result.records.length > 0 ? findStartTime(result.records) : \"\",\n endTime: result.records.length > 0 ? findEndTime(result.records) : \"\",\n turnCount: result.records.filter((r) => r.type === \"assistant\").length,\n status: \"completed\",\n isParallel: false,\n model: result.model,\n agentType: result.agentType,\n totalTokens: result.totalTokens,\n totalCost: computeSubagentCost(result.totalTokens, result.model),\n messages: result.messages,\n toolCalls: result.toolCalls,\n })\n }\n\n // Detect parallel execution (100ms overlap window)\n detectParallelExecution(subagents)\n\n // Sort by startTime\n return subagents.sort((a, b) => {\n if (!a.startTime) return 1\n if (!b.startTime) return -1\n return a.startTime.localeCompare(b.startTime)\n })\n}\n\n/**\n * Parse a subagent JSONL file into structured data.\n * Returns null if file doesn't exist or is empty.\n */\nexport function parseSubagentFile(filePath: string): SubagentParseResult | null {\n if (!existsSync(filePath)) return null\n\n try {\n const content = readFileSync(filePath, \"utf-8\")\n const lines = content.split(\"\\n\").filter((line) => line.trim().length > 0)\n\n if (lines.length === 0) return null\n\n const records: RawJsonlRecord[] = []\n let malformedCount = 0\n\n for (const line of lines) {\n try {\n const entry = JSON.parse(line) as RawJsonlRecord\n records.push(entry)\n } catch {\n malformedCount++\n }\n }\n\n if (records.length === 0) return null\n\n // Normalize cache creation breakdown from JSONL\n for (const record of records) {\n if (record.message?.usage?.cache_creation) {\n const cc = record.message.usage.cache_creation\n record.message.usage.cacheCreation5mTokens = cc.ephemeral_5m_input_tokens ?? 0\n record.message.usage.cacheCreation1hTokens = cc.ephemeral_1h_input_tokens ?? 0\n }\n }\n\n // Deduplicate by requestId (streaming artifact)\n const deduped = deduplicateByRequestId(records)\n\n // Extract tool calls\n const toolCalls: ToolCall[] = []\n const assistantToolCallIndices = new Map<string, number[]>()\n\n for (const record of deduped) {\n if (record.type === \"assistant\" && record.message?.content) {\n const newCalls = extractToolCalls(record.message.content, record.timestamp)\n const startIdx = toolCalls.length\n toolCalls.push(...newCalls)\n\n if (newCalls.length > 0 && record.uuid) {\n const indices = Array.from({ length: newCalls.length }, (_, i) => startIdx + i)\n assistantToolCallIndices.set(record.uuid, indices)\n }\n }\n\n // Extract tool results from user messages\n if (record.type === \"user\" && record.isMeta && record.message?.content) {\n const results = extractToolResults(record.message.content)\n if (results.length > 0) {\n const updatedCalls = linkToolResults(toolCalls, results)\n for (let i = 0; i < updatedCalls.length; i++) {\n if (updatedCalls[i].result !== toolCalls[i].result) {\n toolCalls[i] = updatedCalls[i]\n }\n }\n }\n }\n\n // Match toolUseResult via parentUuid\n if (record.toolUseResult && record.parentUuid) {\n const indices = assistantToolCallIndices.get(record.parentUuid)\n if (indices) {\n for (const idx of indices) {\n toolCalls[idx].result = JSON.stringify(record.toolUseResult)\n }\n }\n }\n }\n\n // Convert records to Messages\n const messages: Message[] = deduped.map((r) => ({\n type: (r.type as \"assistant\" | \"user\" | \"system\") ?? \"assistant\",\n timestamp: r.timestamp,\n content: normalizeContent(r.message?.content ?? []),\n }))\n\n // Extract model from first assistant message\n let model: string | undefined\n for (const record of deduped) {\n if (record.type === \"assistant\" && record.message?.model) {\n model = record.message.model\n break\n }\n }\n\n // Aggregate tokens with request-id dedup\n const totalTokens = aggregateTokens(deduped)\n\n // Extract description from first assistant text block\n let description = \"\"\n const firstAssistant = deduped.find(\n (r) => r.type === \"assistant\" && r.message?.content,\n )\n if (firstAssistant?.message?.content && Array.isArray(firstAssistant.message.content)) {\n const textBlock = firstAssistant.message.content.find(\n (b) => (b as Record<string, unknown>).type === \"text\",\n )\n if (textBlock && (textBlock as Record<string, unknown>).text) {\n description = String((textBlock as Record<string, unknown>).text).slice(0, 200)\n }\n }\n\n // Read meta.json for agentType and higher-quality description\n const metaPath = filePath.replace(/\\.jsonl$/, \".meta.json\")\n let agentType: string | undefined\n if (existsSync(metaPath)) {\n try {\n const meta = JSON.parse(readFileSync(metaPath, \"utf-8\")) as {\n agentType?: string\n description?: string\n }\n agentType = meta.agentType\n // Prefer meta.json description (user-facing, higher quality)\n if (meta.description) {\n description = meta.description\n }\n } catch {\n // Malformed meta.json — use JSONL-extracted description\n }\n }\n\n return {\n records: deduped,\n messages,\n toolCalls,\n description,\n agentType,\n model,\n totalTokens,\n }\n } catch {\n return null\n }\n}\n\n/**\n * Check if a subagent is a warmup agent.\n * Warmup agents have first user message === \"Warmup\".\n */\nexport function isWarmupAgent(records: RawJsonlRecord[]): boolean {\n const firstUser = records.find((r) => r.type === \"user\")\n if (!firstUser) return false\n const content = firstUser.message?.content\n return typeof content === \"string\" && content === \"Warmup\"\n}\n\n/**\n * Find the earliest timestamp in records.\n */\nfunction findStartTime(records: RawJsonlRecord[]): string {\n const timestamps = records\n .filter((r) => r.timestamp)\n .map((r) => new Date(r.timestamp ?? \"\").getTime())\n .filter((t) => !Number.isNaN(t))\n\n if (timestamps.length === 0) return \"\"\n return new Date(Math.min(...timestamps)).toISOString()\n}\n\n/**\n * Find the latest timestamp in records.\n */\nfunction findEndTime(records: RawJsonlRecord[]): string {\n const timestamps = records\n .filter((r) => r.timestamp)\n .map((r) => new Date(r.timestamp ?? \"\").getTime())\n .filter((t) => !Number.isNaN(t))\n\n if (timestamps.length === 0) return \"\"\n return new Date(Math.max(...timestamps)).toISOString()\n}\n\n/**\n * Detect parallel execution among subagents.\n * Subagents are considered parallel if their time ranges overlap\n * within a 100ms window.\n */\nfunction detectParallelExecution(subagents: Subagent[]): void {\n for (let i = 0; i < subagents.length; i++) {\n for (let j = i + 1; j < subagents.length; j++) {\n const a = subagents[i]\n const b = subagents[j]\n\n if (!a.startTime || !b.startTime || !a.endTime || !b.endTime) continue\n\n const aStart = new Date(a.startTime).getTime()\n const aEnd = new Date(a.endTime).getTime()\n const bStart = new Date(b.startTime).getTime()\n const bEnd = new Date(b.endTime).getTime()\n\n // Check for overlap within 100ms window\n if (aStart <= bEnd + PARALLEL_WINDOW_MS && bStart <= aEnd + PARALLEL_WINDOW_MS) {\n a.isParallel = true\n b.isParallel = true\n }\n }\n }\n}\n\n/**\n * Aggregate token usage across records with request-id dedup.\n * Same logic as dedup.ts: only the last entry per requestId counts.\n */\nfunction aggregateTokens(records: RawJsonlRecord[]): TokenUsage {\n const totals: TokenUsage = {\n inputTokens: 0,\n outputTokens: 0,\n cacheReadTokens: 0,\n cacheCreation5mTokens: 0,\n cacheCreation1hTokens: 0,\n }\n\n // Track best entry per requestId for assistant messages\n const bestByRequestId = new Map<string, { outputTokens: number; usage: TokenUsage }>()\n\n for (const record of records) {\n const usage = record.message?.usage\n if (!usage) continue\n\n const tokens: TokenUsage = {\n inputTokens: usage.input_tokens ?? 0,\n outputTokens: usage.output_tokens ?? 0,\n cacheReadTokens: usage.cache_read_input_tokens ?? 0,\n cacheCreation5mTokens: usage.cacheCreation5mTokens ?? 0,\n cacheCreation1hTokens: usage.cacheCreation1hTokens ?? 0,\n }\n\n const rid = record.requestId\n\n // User/system messages without requestId: add directly\n if (!rid) {\n totals.inputTokens += tokens.inputTokens\n totals.outputTokens += tokens.outputTokens\n totals.cacheReadTokens += tokens.cacheReadTokens\n totals.cacheCreation5mTokens += tokens.cacheCreation5mTokens\n totals.cacheCreation1hTokens += tokens.cacheCreation1hTokens\n continue\n }\n\n // Assistant messages with requestId: keep best per requestId\n const existing = bestByRequestId.get(rid)\n if (!existing || tokens.outputTokens > existing.outputTokens) {\n bestByRequestId.set(rid, { outputTokens: tokens.outputTokens, usage: tokens })\n }\n }\n\n // Sum up deduplicated assistant tokens\n for (const { usage } of bestByRequestId.values()) {\n totals.inputTokens += usage.inputTokens\n totals.outputTokens += usage.outputTokens\n totals.cacheReadTokens += usage.cacheReadTokens\n totals.cacheCreation5mTokens += usage.cacheCreation5mTokens\n totals.cacheCreation1hTokens += usage.cacheCreation1hTokens\n }\n\n return totals\n}\n\n/**\n * Compute total cost for a subagent from aggregated tokens and model.\n * Uses a single synthetic Turn to calculate cost via the shared pricing logic.\n */\nfunction computeSubagentCost(tokens: TokenUsage, model?: string): number {\n const rate = getPricing(normalizeModelName(model ?? \"\"))\n const syntheticTurn: Turn = {\n timestamp: new Date().toISOString(),\n tokenUsage: tokens,\n model,\n messages: [],\n toolCalls: [],\n cacheWriteType: \"none\",\n cacheReadType: \"unknown\",\n cacheCreationTokensThisTurn: 0,\n }\n const pricing: TurnPricing = calculateTurnCost(syntheticTurn, rate)\n return pricing.totalCost\n}\n\n/**\n * Normalize content blocks to MessageContent[].\n */\nfunction normalizeContent(content: Array<Record<string, unknown>> | string): MessageContent[] {\n if (typeof content === \"string\") {\n return [{ type: \"text\" as const, text: content }]\n }\n if (!Array.isArray(content)) {\n return []\n }\n return content.map((block) => {\n const type = block.type as string\n if (type === \"text\") {\n return { type: \"text\" as const, text: String(block.text ?? \"\") }\n }\n if (type === \"tool_use\") {\n return {\n type: \"tool_use\" as const,\n name: String(block.name ?? \"\"),\n input: (block.input as Record<string, unknown>) ?? {},\n toolUseId: String(block.id ?? block.toolUseId ?? \"\"),\n }\n }\n if (type === \"tool_result\") {\n return {\n type: \"tool_result\" as const,\n toolUseId: String(block.tool_use_id ?? block.toolUseId ?? \"\"),\n content: block.content ?? \"\",\n isError: (block.is_error ?? block.isError) as boolean | undefined,\n }\n }\n return { type: \"text\" as const, text: JSON.stringify(block) }\n })\n}\n\n// Re-export locator functions for backward compatibility\nexport { extractAgentId, isCompactAgent } from \"./subagent-locator\"\n","import Database from \"better-sqlite3\"\nimport { existsSync, readdirSync, readFileSync, statSync } from \"node:fs\"\nimport { dirname, join } from \"node:path\"\nimport { classifyMessage } from \"./classifier.js\"\nimport { deduplicateByRequestId } from \"./dedup.js\"\nimport { calculateSessionCost } from \"./pricing.js\"\nimport { listSubagentFiles } from \"./subagent-locator.js\"\nimport { resolveSubagents } from \"./subagent-resolver.js\"\nimport type { SessionMetadata, TokenUsage, Turn } from \"./types.js\"\n\n/**\n * Compute active duration by summing gaps between consecutive timestamps\n * that are below a threshold (5 minutes). Large gaps represent idle/closed\n * sessions and are excluded.\n */\nfunction computeActiveDurationMs(\n timestamps: string[],\n thresholdMs = 5 * 60 * 1000,\n): number {\n if (timestamps.length < 2) return 0\n let activeMs = 0\n for (let i = 1; i < timestamps.length; i++) {\n const gap = new Date(timestamps[i]).getTime() - new Date(timestamps[i - 1]).getTime()\n if (gap > 0 && gap < thresholdMs) {\n activeMs += gap\n }\n }\n return activeMs\n}\n\n/** Error when SQLite DB cannot be opened */\nexport class DbOpenError extends Error {\n code = 3\n constructor(message: string) {\n super(message)\n this.name = \"DbOpenError\"\n }\n}\n\n/** Error when session_id not found in DB */\nexport class SessionNotFoundError extends Error {\n code = 2\n constructor(sessionId: string) {\n super(`Session not found: ${sessionId}`)\n this.name = \"SessionNotFoundError\"\n }\n}\n\ninterface SessionRow {\n session_id: string\n project_name: string\n turn_count: number\n total_input_tokens: number\n total_output_tokens: number\n total_cache_read: number\n total_cache_creation: number\n first_timestamp: string\n last_timestamp: string\n git_branch: string | null\n model: string | null\n}\n\ninterface TurnRow {\n session_id: string\n timestamp: string\n tool_name: string | null\n cwd: string | null\n input_tokens: number\n output_tokens: number\n cache_read_tokens: number\n cache_creation_tokens: number\n model: string | null\n}\n\n/**\n * Get session metadata from SQLite DB\n */\nexport function getSession(dbPath: string, sessionId: string): SessionMetadata {\n let db: Database.Database\n try {\n db = new Database(dbPath, { readonly: true })\n } catch (_err) {\n throw new DbOpenError(`Failed to open database: ${dbPath}`)\n }\n\n try {\n const row = db.prepare(\"SELECT * FROM sessions WHERE session_id = ?\").get(sessionId) as\n | SessionRow\n | undefined\n\n if (!row) {\n throw new SessionNotFoundError(sessionId)\n }\n\n const model = row.model || getModelForSession(dbPath, sessionId)\n\n // Infer working directory from most common cwd in turns\n const cwdRow = db\n .prepare(\n \"SELECT cwd, COUNT(*) as cnt FROM turns WHERE session_id = ? AND cwd IS NOT NULL GROUP BY cwd ORDER BY cnt DESC LIMIT 1\",\n )\n .get(sessionId) as { cwd: string } | undefined\n\n const totalTokens: TokenUsage = {\n inputTokens: row.total_input_tokens,\n outputTokens: row.total_output_tokens,\n cacheReadTokens: row.total_cache_read,\n cacheCreation5mTokens: row.total_cache_creation,\n cacheCreation1hTokens: 0,\n }\n\n return {\n sessionId: row.session_id,\n projectName: row.project_name,\n model,\n workingDirectory: cwdRow?.cwd ?? \"\",\n turnCount: row.turn_count,\n totalTokens,\n startTime: row.first_timestamp,\n endTime: row.last_timestamp,\n isOngoing: false,\n }\n } finally {\n db.close()\n }\n}\n\n/**\n * Get all turns for a session from SQLite DB\n */\nexport function getTurns(dbPath: string, sessionId: string): Turn[] {\n let db: Database.Database\n try {\n db = new Database(dbPath, { readonly: true })\n } catch (_err) {\n throw new DbOpenError(`Failed to open database: ${dbPath}`)\n }\n\n try {\n const rows = db\n .prepare(\"SELECT * FROM turns WHERE session_id = ? ORDER BY timestamp ASC\")\n .all(sessionId) as TurnRow[]\n\n return rows.map((row) => ({\n timestamp: row.timestamp,\n tokenUsage: {\n inputTokens: row.input_tokens,\n outputTokens: row.output_tokens,\n cacheReadTokens: row.cache_read_tokens,\n cacheCreation5mTokens: row.cache_creation_tokens,\n cacheCreation1hTokens: 0,\n },\n toolName: row.tool_name ?? undefined,\n cwd: row.cwd ?? undefined,\n messages: [],\n toolCalls: [],\n cacheWriteType: (row.cache_creation_tokens > 0 ? \"5m\" : \"none\") as \"5m\" | \"1h\" | \"none\",\n cacheReadType: \"unknown\" as const,\n cacheCreationTokensThisTurn: row.cache_creation_tokens,\n }))\n } finally {\n db.close()\n }\n}\n\n/**\n * Get the model for a session (from first turn)\n * Falls back to 'claude-sonnet-4-6' if not found\n */\nexport function getModelForSession(dbPath: string, sessionId: string): string {\n let db: Database.Database\n try {\n db = new Database(dbPath, { readonly: true })\n } catch (_err) {\n throw new DbOpenError(`Failed to open database: ${dbPath}`)\n }\n\n try {\n const row = db\n .prepare(\"SELECT model FROM turns WHERE session_id = ? ORDER BY timestamp ASC LIMIT 1\")\n .get(sessionId) as { model: string | null } | undefined\n\n return row?.model ?? \"claude-sonnet-4-6\"\n } finally {\n db.close()\n }\n}\n\n/** Processed file entry */\nexport interface ProcessedFile {\n path: string\n mtime: number\n lines: number\n sessionId: string | null\n}\n\n/**\n * Extract session ID from JSONL file path.\n * e.g., '/.../abc-123.jsonl' -> 'abc-123'\n */\nfunction extractSessionIdFromPath(filePath: string): string | null {\n const match = filePath.match(/([^/]+)\\.jsonl$/)\n return match ? match[1] : null\n}\n\n/**\n * Get processed files from the DB.\n * Returns empty array if table doesn't exist.\n */\nexport function getProcessedFiles(dbPath: string): ProcessedFile[] {\n let db: Database.Database\n try {\n db = new Database(dbPath, { readonly: true })\n } catch (_err) {\n throw new DbOpenError(`Failed to open database: ${dbPath}`)\n }\n\n try {\n const tableExists = db\n .prepare(\"SELECT name FROM sqlite_master WHERE type='table' AND name='processed_files'\")\n .get()\n\n if (!tableExists) return []\n\n const rows = db.prepare(\"SELECT * FROM processed_files\").all() as Array<{\n path: string\n mtime: number\n lines: number\n }>\n\n return rows.map((row) => ({\n path: row.path,\n mtime: row.mtime,\n lines: row.lines,\n sessionId: extractSessionIdFromPath(row.path),\n }))\n } finally {\n db.close()\n }\n}\n\n/** Session summary for listing */\nexport interface SessionSummary {\n sessionId: string\n projectName: string\n model: string\n turnCount: number\n lastTimestamp: string\n totalCostEstimate: number\n hasThinking: boolean\n activeDurationMs?: number\n}\n\n/**\n * List all sessions from the DB, ordered by most recent first.\n */\nexport function listSessions(dbPath: string, limit = 20): SessionSummary[] {\n let db: Database.Database\n try {\n db = new Database(dbPath, { readonly: true })\n } catch (_err) {\n throw new DbOpenError(`Failed to open database: ${dbPath}`)\n }\n\n try {\n const rows = db\n .prepare(\n `SELECT session_id, project_name, model, turn_count, first_timestamp, last_timestamp,\n total_input_tokens, total_output_tokens, total_cache_read, total_cache_creation\n FROM sessions ORDER BY last_timestamp DESC LIMIT ?`,\n )\n .all(limit) as Array<{\n session_id: string\n project_name: string\n model: string | null\n turn_count: number\n first_timestamp: string\n last_timestamp: string\n total_input_tokens: number\n total_output_tokens: number\n total_cache_read: number\n total_cache_creation: number\n }>\n\n return rows.map((row) => {\n const model = row.model || \"claude-sonnet-4-6\"\n // Build a minimal SessionMetadata + Turn[] to use the canonical cost calculator\n const session: SessionMetadata = {\n sessionId: row.session_id,\n projectName: row.project_name,\n model,\n workingDirectory: \"\",\n turnCount: row.turn_count,\n totalTokens: {\n inputTokens: row.total_input_tokens,\n outputTokens: row.total_output_tokens,\n cacheReadTokens: row.total_cache_read,\n cacheCreation5mTokens: row.total_cache_creation,\n cacheCreation1hTokens: 0,\n },\n startTime: row.first_timestamp,\n endTime: row.last_timestamp,\n isOngoing: false,\n }\n // Use one synthetic turn with totals so calculateSessionCost applies correct rates\n const syntheticTurns: Turn[] = [\n {\n timestamp: row.last_timestamp,\n tokenUsage: session.totalTokens,\n messages: [],\n toolCalls: [],\n cacheWriteType: (row.total_cache_creation > 0 ? \"5m\" : \"none\") as \"5m\" | \"none\",\n cacheReadType: \"unknown\" as const,\n cacheCreationTokensThisTurn: row.total_cache_creation,\n },\n ]\n const pricing = calculateSessionCost(session, syntheticTurns)\n\n return {\n sessionId: row.session_id,\n projectName: row.project_name,\n model,\n turnCount: row.turn_count,\n lastTimestamp: row.last_timestamp,\n totalCostEstimate: pricing.totalCost,\n hasThinking: false,\n }\n })\n } finally {\n db.close()\n }\n}\n\n/**\n * Get the set of session IDs that exist in the SQLite DB.\n */\nfunction getExistingSessionIds(dbPath: string): Set<string> {\n try {\n const db = new Database(dbPath, { readonly: true })\n try {\n const rows = db.prepare(\"SELECT session_id FROM sessions\").all() as Array<{\n session_id: string\n }>\n return new Set(rows.map((r) => r.session_id))\n } finally {\n db.close()\n }\n } catch {\n return new Set()\n }\n}\n\n/**\n * Parse a JSONL file header to extract session summary metadata.\n * Reads the file incrementally — stops after finding enough data.\n */\nfunction parseJsonlSummary(\n filePath: string,\n sessionId: string,\n projectName: string,\n): SessionSummary | null {\n try {\n const content = readFileSync(filePath, \"utf-8\")\n const lines = content.split(\"\\n\").filter((l) => l.trim().length > 0)\n if (lines.length === 0) return null\n\n // Parse all records, filter noise, deduplicate — same pipeline as parseSessionJsonl\n const allRecords: Record<string, unknown>[] = []\n for (const line of lines) {\n try {\n allRecords.push(JSON.parse(line))\n } catch {\n continue\n }\n }\n\n const nonNoise = allRecords.filter(\n (r) => classifyMessage(r as unknown as import(\"./types.js\").RawJsonlRecord) !== \"hardNoise\",\n )\n const records = deduplicateByRequestId(\n nonNoise as unknown as import(\"./types.js\").RawJsonlRecord[],\n )\n\n let model = \"claude-sonnet-4-6\"\n let turnCount = 0\n let lastTimestamp = \"\"\n let totalInput = 0\n let totalOutput = 0\n let totalCacheRead = 0\n let totalCacheCreation5m = 0\n let totalCacheCreation1h = 0\n let hasThinking = false\n let lastFileTimestamp = \"\"\n const allTimestamps: string[] = []\n\n for (const record of records) {\n const category = classifyMessage(record)\n\n const msg = record.message as Record<string, unknown> | undefined\n\n // Track timestamps — only from records with actual content\n const ts = record.timestamp\n if (ts) lastFileTimestamp = ts\n const hasContent = (totalInput + totalOutput + totalCacheRead + totalCacheCreation5m + totalCacheCreation1h > 0) ||\n (msg?.usage as Record<string, unknown> | undefined != null) ||\n (typeof msg?.content === \"string\" && (msg.content as string).length > 0) ||\n (Array.isArray((msg?.content as unknown[]) ?? []) && ((msg?.content as unknown[]) ?? []).length > 0)\n if (ts && hasContent) {\n lastTimestamp = ts\n allTimestamps.push(ts)\n }\n\n // Extract model from assistant messages\n if (record.type === \"assistant\" && msg?.model) {\n model = msg.model as string\n }\n\n // Count turns: assistant + real user messages (matches buildTurnsFromJsonl)\n if (category === \"assistant\" || category === \"user\") {\n turnCount++\n }\n\n // Accumulate token usage from any message with usage data\n const usage = msg?.usage as Record<string, unknown> | undefined\n if (usage) {\n totalInput += (usage.input_tokens as number) ?? 0\n totalOutput += (usage.output_tokens as number) ?? 0\n totalCacheRead += (usage.cache_read_input_tokens as number) ?? 0\n const cc = usage.cache_creation as Record<string, number> | undefined\n totalCacheCreation5m +=\n (usage.cacheCreation5mTokens as number) ?? cc?.ephemeral_5m_input_tokens ?? 0\n totalCacheCreation1h +=\n (usage.cacheCreation1hTokens as number) ?? cc?.ephemeral_1h_input_tokens ?? 0\n }\n\n // Detect thinking blocks in assistant content (empty but present = thinking was used)\n if (!hasThinking && record.type === \"assistant\" && Array.isArray(msg?.content)) {\n hasThinking = (msg.content as Array<Record<string, unknown>>).some(\n (b) => b.type === \"thinking\",\n )\n }\n }\n\n // Use canonical cost calculator\n const session: SessionMetadata = {\n sessionId,\n projectName,\n model,\n workingDirectory: \"\",\n turnCount,\n totalTokens: {\n inputTokens: totalInput,\n outputTokens: totalOutput,\n cacheReadTokens: totalCacheRead,\n cacheCreation5mTokens: totalCacheCreation5m,\n cacheCreation1hTokens: totalCacheCreation1h,\n },\n startTime: \"\",\n endTime: lastTimestamp || lastFileTimestamp || new Date().toISOString(),\n isOngoing: false,\n }\n const syntheticTurns: Turn[] =\n totalInput > 0 || totalOutput > 0\n ? [\n {\n timestamp: lastTimestamp || lastFileTimestamp || new Date().toISOString(),\n tokenUsage: session.totalTokens,\n messages: [],\n toolCalls: [],\n cacheWriteType: (totalCacheCreation5m > 0\n ? \"5m\"\n : totalCacheCreation1h > 0\n ? \"1h\"\n : \"none\") as \"5m\" | \"1h\" | \"none\",\n cacheReadType: \"unknown\" as const,\n cacheCreationTokensThisTurn: totalCacheCreation5m + totalCacheCreation1h,\n },\n ]\n : []\n const pricing = calculateSessionCost(session, syntheticTurns)\n\n // Include agent costs from agent-*.jsonl files\n let agentCost = 0\n try {\n const agentFiles = listSubagentFiles(\n join(dirname(filePath), \"..\"), // projectsDir is parent of project dir\n projectName,\n sessionId,\n )\n if (agentFiles.length > 0) {\n const subagents = resolveSubagents(agentFiles, [])\n agentCost = subagents.reduce((sum, s) => sum + (s.totalCost ?? 0), 0)\n }\n } catch {\n // Agent resolution is best-effort for summary\n }\n\n return {\n sessionId,\n projectName,\n model,\n turnCount,\n lastTimestamp: lastTimestamp || lastFileTimestamp || new Date().toISOString(),\n totalCostEstimate: pricing.totalCost + agentCost,\n hasThinking,\n activeDurationMs: computeActiveDurationMs(allTimestamps),\n }\n } catch {\n return null\n }\n}\n\n/**\n * List sessions discovered from JSONL files on disk.\n * Skips sessions that already exist in the SQLite DB.\n * Skips subagent files (agent-*.jsonl).\n */\nexport function listJsonlSessions(\n projectsDir: string,\n dbPath: string,\n limit = 100,\n): SessionSummary[] {\n const existingIds = getExistingSessionIds(dbPath)\n const results: SessionSummary[] = []\n\n if (!existsSync(projectsDir)) return results\n\n try {\n const projectDirs = readdirSync(projectsDir)\n\n for (const dirName of projectDirs) {\n const projectDir = join(projectsDir, dirName)\n // Skip if not a directory\n try {\n const s = statSync(projectDir)\n if (!s.isDirectory()) continue\n } catch {\n continue\n }\n\n // Strip leading '-' (from leading '/' in original path)\n // Use raw directory name — decodeProjectName is lossy for paths with hyphens\n const projectName = dirName.startsWith(\"-\") ? dirName.slice(1) : dirName\n\n // Find .jsonl files (skip agent-*.jsonl subagent files)\n try {\n const files = readdirSync(projectDir)\n for (const file of files) {\n if (!file.endsWith(\".jsonl\")) continue\n if (file.startsWith(\"agent-\")) continue\n\n const sessionId = file.replace(\".jsonl\", \"\")\n\n // Skip if already in SQLite\n if (existingIds.has(sessionId)) continue\n\n const filePath = join(projectDir, file)\n const summary = parseJsonlSummary(filePath, sessionId, projectName)\n if (summary) results.push(summary)\n }\n } catch {\n // Skip unreadable directories\n }\n }\n } catch {\n // Skip if projects dir doesn't exist\n }\n\n // Sort by most recent first, apply limit\n results.sort((a, b) => b.lastTimestamp.localeCompare(a.lastTimestamp))\n return results.slice(0, limit)\n}\n"],"mappings":"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"}
|