diff --git a/.gitignore b/.gitignore index b218957..ee02a6d 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,5 @@ -node_modules/\n.DS_Store +node_modules/ +.DS_Store + +# accuracy audit scripts (dev-only) +audit/ diff --git a/lib/aggregator.js b/lib/aggregator.js index 46087b7..0dec934 100644 --- a/lib/aggregator.js +++ b/lib/aggregator.js @@ -4,18 +4,34 @@ import os from "node:os"; import { spawn } from "node:child_process"; const fileCache = /* @__PURE__ */ new Map(); function findSessionFiles(dir) { - const results = []; - try { - const entries = fs.readdirSync(dir, { withFileTypes: true }); - for (const entry of entries) { - const fullPath = path.join(dir, entry.name); - if (entry.isDirectory()) { - results.push(...findSessionFiles(fullPath)); - } else if (entry.name.endsWith(".jsonl.zstd") || entry.name.endsWith(".jsonl")) { - results.push(fullPath); + const byDir = /* @__PURE__ */ new Map(); + const collect = (d) => { + try { + const entries = fs.readdirSync(d, { withFileTypes: true }); + for (const entry of entries) { + const fullPath = path.join(d, entry.name); + if (entry.isDirectory()) { + collect(fullPath); + } else if (entry.name.endsWith(".jsonl.zstd") || entry.name.endsWith(".jsonl")) { + if (!byDir.has(d)) byDir.set(d, []); + byDir.get(d).push(fullPath); + } } + } catch { } - } catch { + }; + collect(dir); + const results = []; + for (const files of byDir.values()) { + if (files.length === 1) { + results.push(files[0]); + continue; + } + const ranked = files.map((f) => { + const m = path.basename(f).match(/\.v(\d+)\.jsonl/); + return { f, v: m ? parseInt(m[1], 10) : 0 }; + }).sort((a, b) => b.v - a.v); + results.push(ranked[0].f); } return results; } @@ -65,8 +81,9 @@ async function parseSessionFile(filePath) { if (line.includes('"usage"')) { try { const obj = JSON.parse(line); + if (obj.type !== "assistant/message") return; const time = obj.time || obj.data && obj.data.time; - const usage = obj.usage || obj.data && obj.data.usage; + const usage = obj.data && obj.data.usage || obj.usage; if (time && usage) { const d = new Date(time); const dateKey = `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}-${String(d.getDate()).padStart(2, "0")}`; diff --git a/lib/aggregator.js.map b/lib/aggregator.js.map index 4f2c732..ee6c61e 100644 --- a/lib/aggregator.js.map +++ b/lib/aggregator.js.map @@ -1,7 +1,7 @@ { "version": 3, "sources": ["../src/server/aggregator.ts"], - "sourcesContent": ["import fs from \"node:fs\";\nimport path from \"node:path\";\nimport os from \"node:os\";\nimport { spawn } from \"node:child_process\";\n\nexport interface DaySessionDetail {\n sessionId: string;\n sessionTitle: string;\n createdAt: number;\n totalTokens: number;\n inputTokens: number;\n outputTokens: number;\n cacheReadTokens: number;\n turns: number;\n}\n\nexport interface DayStats {\n totalTokens: number;\n inputTokens: number;\n outputTokens: number;\n cacheReadTokens: number;\n cacheWriteTokens: number;\n turns: number;\n level?: number; // 0..4\n sessions?: DaySessionDetail[];\n}\n\nexport interface ModelUsage {\n model: string;\n totalTokens: number;\n inputTokens: number;\n outputTokens: number;\n cacheReadTokens: number;\n turns: number;\n percentage: number;\n}\n\nexport interface SessionSummary {\n id: string;\n title: string;\n createdAt: number;\n totalTokens: number;\n inputTokens: number;\n outputTokens: number;\n cacheReadTokens: number;\n turns: number;\n days: Record;\n models?: Record;\n}\n\nexport interface SummaryStats {\n totalTokens: number;\n totalInputTokens: number;\n totalOutputTokens: number;\n totalCacheReadTokens: number;\n totalTurns: number;\n activeDays: number;\n currentStreak: number;\n longestStreak: number;\n peakDay: { date: string; tokens: number; turns: number } | null;\n dailyAverage: number;\n}\n\nexport interface HeatmapResponse {\n summary: SummaryStats;\n days: Record;\n sessions: SessionSummary[];\n models: ModelUsage[];\n startDate: string;\n endDate: string;\n}\n\ninterface CacheEntry {\n mtimeMs: number;\n session: SessionSummary;\n}\n\nconst fileCache = new Map();\n\nfunction findSessionFiles(dir: string): string[] {\n const results: string[] = [];\n try {\n const entries = fs.readdirSync(dir, { withFileTypes: true });\n for (const entry of entries) {\n const fullPath = path.join(dir, entry.name);\n if (entry.isDirectory()) {\n results.push(...findSessionFiles(fullPath));\n } else if (entry.name.endsWith(\".jsonl.zstd\") || entry.name.endsWith(\".jsonl\")) {\n results.push(fullPath);\n }\n }\n } catch {\n // Ignore unreadable dirs\n }\n return results;\n}\n\nasync function parseSessionFile(filePath: string): Promise {\n try {\n const stat = fs.statSync(filePath);\n const cached = fileCache.get(filePath);\n if (cached && cached.mtimeMs === stat.mtimeMs) {\n return cached.session;\n }\n\n const isZstd = filePath.endsWith(\".zstd\");\n const days: Record = {};\n const sessionModels: Record = {};\n let sessionId = path.basename(path.dirname(filePath));\n let title = \"\";\n let createdAt = stat.birthtimeMs || stat.mtimeMs;\n let totalTokens = 0;\n let totalInputTokens = 0;\n let totalOutputTokens = 0;\n let totalCacheReadTokens = 0;\n let totalTurns = 0;\n\n await new Promise((resolve) => {\n let buffer = \"\";\n const handleLine = (line: string) => {\n if (line.includes('\"type\":\"session\"')) {\n try {\n const obj = JSON.parse(line);\n if (obj.id) sessionId = obj.id;\n if (obj.createdAt) createdAt = obj.createdAt;\n } catch {}\n }\n if (!title && line.includes('\"session/title\"')) {\n try {\n const obj = JSON.parse(line);\n if (obj.data?.title) title = obj.data.title;\n } catch {}\n }\n if (!title && line.includes('\"subagent/descriptor\"')) {\n try {\n const obj = JSON.parse(line);\n if (obj.data?.label) title = obj.data.label;\n } catch {}\n }\n if (line.includes('\"usage\"')) {\n try {\n const obj = JSON.parse(line);\n const time = obj.time || (obj.data && obj.data.time);\n const usage = obj.usage || (obj.data && obj.data.usage);\n if (time && usage) {\n const d = new Date(time);\n const dateKey = `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, \"0\")}-${String(d.getDate()).padStart(2, \"0\")}`;\n const input = Number(usage.inputTokens) || 0;\n const output = Number(usage.outputTokens) || 0;\n const cacheRead = Number(usage.cacheReadTokens) || 0;\n const cacheWrite = Number(usage.cacheWriteTokens) || 0;\n const total = input + output + cacheRead;\n const model = obj.data?.message?.source?.model || obj.data?.model || obj.model || \"gemini-3.8-flash-tiered\";\n\n if (!days[dateKey]) {\n days[dateKey] = {\n totalTokens: 0,\n inputTokens: 0,\n outputTokens: 0,\n cacheReadTokens: 0,\n cacheWriteTokens: 0,\n turns: 0,\n };\n }\n days[dateKey].totalTokens += total;\n days[dateKey].inputTokens += input;\n days[dateKey].outputTokens += output;\n days[dateKey].cacheReadTokens += cacheRead;\n days[dateKey].cacheWriteTokens += cacheWrite;\n days[dateKey].turns += 1;\n\n if (!sessionModels[model]) {\n sessionModels[model] = { totalTokens: 0, inputTokens: 0, outputTokens: 0, cacheReadTokens: 0, turns: 0 };\n }\n sessionModels[model].totalTokens += total;\n sessionModels[model].inputTokens += input;\n sessionModels[model].outputTokens += output;\n sessionModels[model].cacheReadTokens += cacheRead;\n sessionModels[model].turns += 1;\n\n totalTokens += total;\n totalInputTokens += input;\n totalOutputTokens += output;\n totalCacheReadTokens += cacheRead;\n totalTurns += 1;\n }\n } catch {\n // Ignore malformed lines\n }\n }\n };\n\n if (isZstd) {\n const proc = spawn(\"zstd\", [\"-dc\", filePath], { stdio: [\"ignore\", \"pipe\", \"ignore\"] });\n proc.stdout.on(\"data\", (chunk: Buffer) => {\n buffer += chunk.toString(\"utf8\");\n let idx: number;\n while ((idx = buffer.indexOf(\"\\n\")) !== -1) {\n const line = buffer.slice(0, idx);\n buffer = buffer.slice(idx + 1);\n handleLine(line);\n }\n });\n proc.on(\"close\", () => {\n if (buffer.trim().length > 0) handleLine(buffer);\n resolve();\n });\n proc.on(\"error\", () => resolve());\n } else {\n const stream = fs.createReadStream(filePath, { encoding: \"utf8\" });\n stream.on(\"data\", (chunk: string) => {\n buffer += chunk;\n let idx: number;\n while ((idx = buffer.indexOf(\"\\n\")) !== -1) {\n const line = buffer.slice(0, idx);\n buffer = buffer.slice(idx + 1);\n handleLine(line);\n }\n });\n stream.on(\"end\", () => {\n if (buffer.trim().length > 0) handleLine(buffer);\n resolve();\n });\n stream.on(\"error\", () => resolve());\n }\n });\n\n const session: SessionSummary = {\n id: sessionId,\n title: title || `Session ${sessionId.slice(0, 8)}`,\n createdAt,\n totalTokens,\n inputTokens: totalInputTokens,\n outputTokens: totalOutputTokens,\n cacheReadTokens: totalCacheReadTokens,\n turns: totalTurns,\n days,\n models: sessionModels,\n };\n\n fileCache.set(filePath, { mtimeMs: stat.mtimeMs, session });\n return session;\n } catch {\n return null;\n }\n}\n\nexport async function aggregateTokens(customSessionsRoot?: string): Promise {\n const sessionsRoot = customSessionsRoot || path.join(os.homedir(), \".dsh\", \"sessions\");\n const files = findSessionFiles(sessionsRoot);\n const parsedSessions = (await Promise.all(files.map(parseSessionFile))).filter(Boolean) as SessionSummary[];\n\n // Sort sessions newest first\n parsedSessions.sort((a, b) => b.createdAt - a.createdAt);\n\n const days: Record = {};\n const allModelsMap: Record = {};\n\n for (const session of parsedSessions) {\n for (const [date, stats] of Object.entries(session.days)) {\n if (!days[date]) {\n days[date] = {\n totalTokens: 0,\n inputTokens: 0,\n outputTokens: 0,\n cacheReadTokens: 0,\n cacheWriteTokens: 0,\n turns: 0,\n sessions: [],\n };\n }\n days[date].totalTokens += stats.totalTokens;\n days[date].inputTokens += stats.inputTokens;\n days[date].outputTokens += stats.outputTokens;\n days[date].cacheReadTokens += stats.cacheReadTokens;\n days[date].cacheWriteTokens += stats.cacheWriteTokens;\n days[date].turns += stats.turns;\n\n days[date].sessions!.push({\n sessionId: session.id,\n sessionTitle: session.title,\n createdAt: session.createdAt,\n totalTokens: stats.totalTokens,\n inputTokens: stats.inputTokens,\n outputTokens: stats.outputTokens,\n cacheReadTokens: stats.cacheReadTokens,\n turns: stats.turns,\n });\n }\n\n if (session.models) {\n for (const [modelName, mStats] of Object.entries(session.models)) {\n if (!allModelsMap[modelName]) {\n allModelsMap[modelName] = { totalTokens: 0, inputTokens: 0, outputTokens: 0, cacheReadTokens: 0, turns: 0 };\n }\n allModelsMap[modelName].totalTokens += mStats.totalTokens;\n allModelsMap[modelName].inputTokens += mStats.inputTokens;\n allModelsMap[modelName].outputTokens += mStats.outputTokens;\n allModelsMap[modelName].cacheReadTokens += mStats.cacheReadTokens;\n allModelsMap[modelName].turns += mStats.turns;\n }\n }\n }\n\n // Sort sessions within each day by totalTokens descending\n for (const day of Object.values(days)) {\n if (day.sessions) {\n day.sessions.sort((a, b) => b.totalTokens - a.totalTokens);\n }\n }\n\n // Calculate Levels (0..4 intensity scale based on positive token days)\n const positiveValues = Object.values(days)\n .map((d) => d.totalTokens)\n .filter((t) => t > 0)\n .sort((a, b) => a - b);\n\n let q1 = 5_000_000;\n let q2 = 25_000_000;\n let q3 = 75_000_000;\n\n if (positiveValues.length >= 4) {\n q1 = positiveValues[Math.floor(positiveValues.length * 0.25)];\n q2 = positiveValues[Math.floor(positiveValues.length * 0.50)];\n q3 = positiveValues[Math.floor(positiveValues.length * 0.75)];\n }\n\n for (const day of Object.values(days)) {\n if (day.totalTokens === 0) day.level = 0;\n else if (day.totalTokens <= q1) day.level = 1;\n else if (day.totalTokens <= q2) day.level = 2;\n else if (day.totalTokens <= q3) day.level = 3;\n else day.level = 4;\n }\n\n // Calculate Summary & Streaks\n let totalTokens = 0;\n let totalInputTokens = 0;\n let totalOutputTokens = 0;\n let totalCacheReadTokens = 0;\n let totalTurns = 0;\n let peakDay: { date: string; tokens: number; turns: number } | null = null;\n\n for (const [date, stats] of Object.entries(days)) {\n totalTokens += stats.totalTokens;\n totalInputTokens += stats.inputTokens;\n totalOutputTokens += stats.outputTokens;\n totalCacheReadTokens += stats.cacheReadTokens;\n totalTurns += stats.turns;\n\n if (!peakDay || stats.totalTokens > peakDay.tokens) {\n peakDay = { date, tokens: stats.totalTokens, turns: stats.turns };\n }\n }\n\n // Build sorted model breakdown array\n const models: ModelUsage[] = Object.entries(allModelsMap)\n .map(([model, m]) => ({\n model,\n totalTokens: m.totalTokens,\n inputTokens: m.inputTokens,\n outputTokens: m.outputTokens,\n cacheReadTokens: m.cacheReadTokens,\n turns: m.turns,\n percentage: totalTokens > 0 ? Math.round((m.totalTokens / totalTokens) * 1000) / 10 : 0,\n }))\n .sort((a, b) => b.totalTokens - a.totalTokens);\n\n const activeDays = Object.values(days).filter((d) => d.totalTokens > 0).length;\n const dailyAverage = activeDays > 0 ? Math.round(totalTokens / activeDays) : 0;\n\n // Streak calculation (iterating backwards from today)\n const now = new Date();\n const formatDate = (d: Date) =>\n `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, \"0\")}-${String(d.getDate()).padStart(2, \"0\")}`;\n\n let currentStreak = 0;\n let cursor = new Date(now);\n const todayKey = formatDate(cursor);\n cursor.setDate(cursor.getDate() - 1);\n const yesterdayKey = formatDate(cursor);\n\n let streakDate = (days[todayKey]?.totalTokens ?? 0) > 0 ? new Date(now) : (days[yesterdayKey]?.totalTokens ?? 0) > 0 ? new Date(cursor) : null;\n\n if (streakDate) {\n while (true) {\n const key = formatDate(streakDate);\n if ((days[key]?.totalTokens ?? 0) > 0) {\n currentStreak++;\n streakDate.setDate(streakDate.getDate() - 1);\n } else {\n break;\n }\n }\n }\n\n // Longest streak\n const sortedDates = Object.keys(days)\n .filter((k) => days[k].totalTokens > 0)\n .sort();\n\n let longestStreak = 0;\n let tempStreak = 0;\n let prevDate: Date | null = null;\n\n for (const dateStr of sortedDates) {\n const cur = new Date(dateStr + \"T00:00:00\");\n if (!prevDate) {\n tempStreak = 1;\n } else {\n const diffDays = Math.round((cur.getTime() - prevDate.getTime()) / (1000 * 60 * 60 * 24));\n if (diffDays === 1) {\n tempStreak++;\n } else {\n tempStreak = 1;\n }\n }\n if (tempStreak > longestStreak) longestStreak = tempStreak;\n prevDate = cur;\n }\n\n const endDate = formatDate(now);\n const start = new Date(now);\n start.setDate(start.getDate() - 364);\n const startDate = formatDate(start);\n\n return {\n summary: {\n totalTokens,\n totalInputTokens,\n totalOutputTokens,\n totalCacheReadTokens,\n totalTurns,\n activeDays,\n currentStreak,\n longestStreak: Math.max(longestStreak, currentStreak),\n peakDay,\n dailyAverage,\n },\n days,\n sessions: parsedSessions,\n models,\n startDate,\n endDate,\n };\n}\n"], - "mappings": "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", + "sourcesContent": ["import fs from \"node:fs\";\nimport path from \"node:path\";\nimport os from \"node:os\";\nimport { spawn } from \"node:child_process\";\n\nexport interface DaySessionDetail {\n sessionId: string;\n sessionTitle: string;\n createdAt: number;\n totalTokens: number;\n inputTokens: number;\n outputTokens: number;\n cacheReadTokens: number;\n turns: number;\n}\n\nexport interface DayStats {\n totalTokens: number;\n inputTokens: number;\n outputTokens: number;\n cacheReadTokens: number;\n cacheWriteTokens: number;\n turns: number;\n level?: number; // 0..4\n sessions?: DaySessionDetail[];\n}\n\nexport interface ModelUsage {\n model: string;\n totalTokens: number;\n inputTokens: number;\n outputTokens: number;\n cacheReadTokens: number;\n turns: number;\n percentage: number;\n}\n\nexport interface SessionSummary {\n id: string;\n title: string;\n createdAt: number;\n totalTokens: number;\n inputTokens: number;\n outputTokens: number;\n cacheReadTokens: number;\n turns: number;\n days: Record;\n models?: Record;\n}\n\nexport interface SummaryStats {\n totalTokens: number;\n totalInputTokens: number;\n totalOutputTokens: number;\n totalCacheReadTokens: number;\n totalTurns: number;\n activeDays: number;\n currentStreak: number;\n longestStreak: number;\n peakDay: { date: string; tokens: number; turns: number } | null;\n dailyAverage: number;\n}\n\nexport interface HeatmapResponse {\n summary: SummaryStats;\n days: Record;\n sessions: SessionSummary[];\n models: ModelUsage[];\n startDate: string;\n endDate: string;\n}\n\ninterface CacheEntry {\n mtimeMs: number;\n session: SessionSummary;\n}\n\nconst fileCache = new Map();\n\nfunction findSessionFiles(dir: string): string[] {\n // Group files by session directory. DSH keeps a migrated copy of each\n // session (session.v3.jsonl.zstd alongside session.v4.jsonl.zstd) that\n // contains the SAME records \u2014 reading both double-counts every turn.\n // Prefer the newest schema version per directory.\n const byDir = new Map();\n const collect = (d: string) => {\n try {\n const entries = fs.readdirSync(d, { withFileTypes: true });\n for (const entry of entries) {\n const fullPath = path.join(d, entry.name);\n if (entry.isDirectory()) {\n collect(fullPath);\n } else if (entry.name.endsWith(\".jsonl.zstd\") || entry.name.endsWith(\".jsonl\")) {\n if (!byDir.has(d)) byDir.set(d, []);\n byDir.get(d)!.push(fullPath);\n }\n }\n } catch {\n // Ignore unreadable dirs\n }\n };\n collect(dir);\n const results: string[] = [];\n for (const files of byDir.values()) {\n if (files.length === 1) {\n results.push(files[0]);\n continue;\n }\n // Prefer highest session version (v4 > v3)\n const ranked = files\n .map((f) => {\n const m = path.basename(f).match(/\\.v(\\d+)\\.jsonl/);\n return { f, v: m ? parseInt(m[1], 10) : 0 };\n })\n .sort((a, b) => b.v - a.v);\n results.push(ranked[0].f);\n }\n return results;\n}\n\nasync function parseSessionFile(filePath: string): Promise {\n try {\n const stat = fs.statSync(filePath);\n const cached = fileCache.get(filePath);\n if (cached && cached.mtimeMs === stat.mtimeMs) {\n return cached.session;\n }\n\n const isZstd = filePath.endsWith(\".zstd\");\n const days: Record = {};\n const sessionModels: Record = {};\n let sessionId = path.basename(path.dirname(filePath));\n let title = \"\";\n let createdAt = stat.birthtimeMs || stat.mtimeMs;\n let totalTokens = 0;\n let totalInputTokens = 0;\n let totalOutputTokens = 0;\n let totalCacheReadTokens = 0;\n let totalTurns = 0;\n\n await new Promise((resolve) => {\n let buffer = \"\";\n const handleLine = (line: string) => {\n if (line.includes('\"type\":\"session\"')) {\n try {\n const obj = JSON.parse(line);\n if (obj.id) sessionId = obj.id;\n if (obj.createdAt) createdAt = obj.createdAt;\n } catch {}\n }\n if (!title && line.includes('\"session/title\"')) {\n try {\n const obj = JSON.parse(line);\n if (obj.data?.title) title = obj.data.title;\n } catch {}\n }\n if (!title && line.includes('\"subagent/descriptor\"')) {\n try {\n const obj = JSON.parse(line);\n if (obj.data?.label) title = obj.data.label;\n } catch {}\n }\n if (line.includes('\"usage\"')) {\n try {\n const obj = JSON.parse(line);\n // Only assistant/message records carry real billed usage.\n // compaction/summary records re-report the summarizer call and\n // assistant/attempt lines carry no data.usage at all.\n if (obj.type !== \"assistant/message\") return;\n const time = obj.time || (obj.data && obj.data.time);\n const usage = (obj.data && obj.data.usage) || obj.usage;\n if (time && usage) {\n const d = new Date(time);\n const dateKey = `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, \"0\")}-${String(d.getDate()).padStart(2, \"0\")}`;\n const input = Number(usage.inputTokens) || 0;\n const output = Number(usage.outputTokens) || 0;\n const cacheRead = Number(usage.cacheReadTokens) || 0;\n const cacheWrite = Number(usage.cacheWriteTokens) || 0;\n const total = input + output + cacheRead;\n const model = obj.data?.message?.source?.model || obj.data?.model || obj.model || \"gemini-3.8-flash-tiered\";\n\n if (!days[dateKey]) {\n days[dateKey] = {\n totalTokens: 0,\n inputTokens: 0,\n outputTokens: 0,\n cacheReadTokens: 0,\n cacheWriteTokens: 0,\n turns: 0,\n };\n }\n days[dateKey].totalTokens += total;\n days[dateKey].inputTokens += input;\n days[dateKey].outputTokens += output;\n days[dateKey].cacheReadTokens += cacheRead;\n days[dateKey].cacheWriteTokens += cacheWrite;\n days[dateKey].turns += 1;\n\n if (!sessionModels[model]) {\n sessionModels[model] = { totalTokens: 0, inputTokens: 0, outputTokens: 0, cacheReadTokens: 0, turns: 0 };\n }\n sessionModels[model].totalTokens += total;\n sessionModels[model].inputTokens += input;\n sessionModels[model].outputTokens += output;\n sessionModels[model].cacheReadTokens += cacheRead;\n sessionModels[model].turns += 1;\n\n totalTokens += total;\n totalInputTokens += input;\n totalOutputTokens += output;\n totalCacheReadTokens += cacheRead;\n totalTurns += 1;\n }\n } catch {\n // Ignore malformed lines\n }\n }\n };\n\n if (isZstd) {\n const proc = spawn(\"zstd\", [\"-dc\", filePath], { stdio: [\"ignore\", \"pipe\", \"ignore\"] });\n proc.stdout.on(\"data\", (chunk: Buffer) => {\n buffer += chunk.toString(\"utf8\");\n let idx: number;\n while ((idx = buffer.indexOf(\"\\n\")) !== -1) {\n const line = buffer.slice(0, idx);\n buffer = buffer.slice(idx + 1);\n handleLine(line);\n }\n });\n proc.on(\"close\", () => {\n if (buffer.trim().length > 0) handleLine(buffer);\n resolve();\n });\n proc.on(\"error\", () => resolve());\n } else {\n const stream = fs.createReadStream(filePath, { encoding: \"utf8\" });\n stream.on(\"data\", (chunk: string) => {\n buffer += chunk;\n let idx: number;\n while ((idx = buffer.indexOf(\"\\n\")) !== -1) {\n const line = buffer.slice(0, idx);\n buffer = buffer.slice(idx + 1);\n handleLine(line);\n }\n });\n stream.on(\"end\", () => {\n if (buffer.trim().length > 0) handleLine(buffer);\n resolve();\n });\n stream.on(\"error\", () => resolve());\n }\n });\n\n const session: SessionSummary = {\n id: sessionId,\n title: title || `Session ${sessionId.slice(0, 8)}`,\n createdAt,\n totalTokens,\n inputTokens: totalInputTokens,\n outputTokens: totalOutputTokens,\n cacheReadTokens: totalCacheReadTokens,\n turns: totalTurns,\n days,\n models: sessionModels,\n };\n\n fileCache.set(filePath, { mtimeMs: stat.mtimeMs, session });\n return session;\n } catch {\n return null;\n }\n}\n\nexport async function aggregateTokens(customSessionsRoot?: string): Promise {\n const sessionsRoot = customSessionsRoot || path.join(os.homedir(), \".dsh\", \"sessions\");\n const files = findSessionFiles(sessionsRoot);\n const parsedSessions = (await Promise.all(files.map(parseSessionFile))).filter(Boolean) as SessionSummary[];\n\n // Sort sessions newest first\n parsedSessions.sort((a, b) => b.createdAt - a.createdAt);\n\n const days: Record = {};\n const allModelsMap: Record = {};\n\n for (const session of parsedSessions) {\n for (const [date, stats] of Object.entries(session.days)) {\n if (!days[date]) {\n days[date] = {\n totalTokens: 0,\n inputTokens: 0,\n outputTokens: 0,\n cacheReadTokens: 0,\n cacheWriteTokens: 0,\n turns: 0,\n sessions: [],\n };\n }\n days[date].totalTokens += stats.totalTokens;\n days[date].inputTokens += stats.inputTokens;\n days[date].outputTokens += stats.outputTokens;\n days[date].cacheReadTokens += stats.cacheReadTokens;\n days[date].cacheWriteTokens += stats.cacheWriteTokens;\n days[date].turns += stats.turns;\n\n days[date].sessions!.push({\n sessionId: session.id,\n sessionTitle: session.title,\n createdAt: session.createdAt,\n totalTokens: stats.totalTokens,\n inputTokens: stats.inputTokens,\n outputTokens: stats.outputTokens,\n cacheReadTokens: stats.cacheReadTokens,\n turns: stats.turns,\n });\n }\n\n if (session.models) {\n for (const [modelName, mStats] of Object.entries(session.models)) {\n if (!allModelsMap[modelName]) {\n allModelsMap[modelName] = { totalTokens: 0, inputTokens: 0, outputTokens: 0, cacheReadTokens: 0, turns: 0 };\n }\n allModelsMap[modelName].totalTokens += mStats.totalTokens;\n allModelsMap[modelName].inputTokens += mStats.inputTokens;\n allModelsMap[modelName].outputTokens += mStats.outputTokens;\n allModelsMap[modelName].cacheReadTokens += mStats.cacheReadTokens;\n allModelsMap[modelName].turns += mStats.turns;\n }\n }\n }\n\n // Sort sessions within each day by totalTokens descending\n for (const day of Object.values(days)) {\n if (day.sessions) {\n day.sessions.sort((a, b) => b.totalTokens - a.totalTokens);\n }\n }\n\n // Calculate Levels (0..4 intensity scale based on positive token days)\n const positiveValues = Object.values(days)\n .map((d) => d.totalTokens)\n .filter((t) => t > 0)\n .sort((a, b) => a - b);\n\n let q1 = 5_000_000;\n let q2 = 25_000_000;\n let q3 = 75_000_000;\n\n if (positiveValues.length >= 4) {\n q1 = positiveValues[Math.floor(positiveValues.length * 0.25)];\n q2 = positiveValues[Math.floor(positiveValues.length * 0.50)];\n q3 = positiveValues[Math.floor(positiveValues.length * 0.75)];\n }\n\n for (const day of Object.values(days)) {\n if (day.totalTokens === 0) day.level = 0;\n else if (day.totalTokens <= q1) day.level = 1;\n else if (day.totalTokens <= q2) day.level = 2;\n else if (day.totalTokens <= q3) day.level = 3;\n else day.level = 4;\n }\n\n // Calculate Summary & Streaks\n let totalTokens = 0;\n let totalInputTokens = 0;\n let totalOutputTokens = 0;\n let totalCacheReadTokens = 0;\n let totalTurns = 0;\n let peakDay: { date: string; tokens: number; turns: number } | null = null;\n\n for (const [date, stats] of Object.entries(days)) {\n totalTokens += stats.totalTokens;\n totalInputTokens += stats.inputTokens;\n totalOutputTokens += stats.outputTokens;\n totalCacheReadTokens += stats.cacheReadTokens;\n totalTurns += stats.turns;\n\n if (!peakDay || stats.totalTokens > peakDay.tokens) {\n peakDay = { date, tokens: stats.totalTokens, turns: stats.turns };\n }\n }\n\n // Build sorted model breakdown array\n const models: ModelUsage[] = Object.entries(allModelsMap)\n .map(([model, m]) => ({\n model,\n totalTokens: m.totalTokens,\n inputTokens: m.inputTokens,\n outputTokens: m.outputTokens,\n cacheReadTokens: m.cacheReadTokens,\n turns: m.turns,\n percentage: totalTokens > 0 ? Math.round((m.totalTokens / totalTokens) * 1000) / 10 : 0,\n }))\n .sort((a, b) => b.totalTokens - a.totalTokens);\n\n const activeDays = Object.values(days).filter((d) => d.totalTokens > 0).length;\n const dailyAverage = activeDays > 0 ? Math.round(totalTokens / activeDays) : 0;\n\n // Streak calculation (iterating backwards from today)\n const now = new Date();\n const formatDate = (d: Date) =>\n `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, \"0\")}-${String(d.getDate()).padStart(2, \"0\")}`;\n\n let currentStreak = 0;\n let cursor = new Date(now);\n const todayKey = formatDate(cursor);\n cursor.setDate(cursor.getDate() - 1);\n const yesterdayKey = formatDate(cursor);\n\n let streakDate = (days[todayKey]?.totalTokens ?? 0) > 0 ? new Date(now) : (days[yesterdayKey]?.totalTokens ?? 0) > 0 ? new Date(cursor) : null;\n\n if (streakDate) {\n while (true) {\n const key = formatDate(streakDate);\n if ((days[key]?.totalTokens ?? 0) > 0) {\n currentStreak++;\n streakDate.setDate(streakDate.getDate() - 1);\n } else {\n break;\n }\n }\n }\n\n // Longest streak\n const sortedDates = Object.keys(days)\n .filter((k) => days[k].totalTokens > 0)\n .sort();\n\n let longestStreak = 0;\n let tempStreak = 0;\n let prevDate: Date | null = null;\n\n for (const dateStr of sortedDates) {\n const cur = new Date(dateStr + \"T00:00:00\");\n if (!prevDate) {\n tempStreak = 1;\n } else {\n const diffDays = Math.round((cur.getTime() - prevDate.getTime()) / (1000 * 60 * 60 * 24));\n if (diffDays === 1) {\n tempStreak++;\n } else {\n tempStreak = 1;\n }\n }\n if (tempStreak > longestStreak) longestStreak = tempStreak;\n prevDate = cur;\n }\n\n const endDate = formatDate(now);\n const start = new Date(now);\n start.setDate(start.getDate() - 364);\n const startDate = formatDate(start);\n\n return {\n summary: {\n totalTokens,\n totalInputTokens,\n totalOutputTokens,\n totalCacheReadTokens,\n totalTurns,\n activeDays,\n currentStreak,\n longestStreak: Math.max(longestStreak, currentStreak),\n peakDay,\n dailyAverage,\n },\n days,\n sessions: parsedSessions,\n models,\n startDate,\n endDate,\n };\n}\n"], + "mappings": "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", "names": [] } diff --git a/package.json b/package.json index f360160..87898dc 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "token-contribution", - "version": "0.1.0", + "version": "0.1.1", "description": "GitHub-style token usage contribution calendar and analytics for DeepSeek Harness", "type": "module", "main": "./lib/index.js", diff --git a/src/server/aggregator.ts b/src/server/aggregator.ts index cc935eb..cc76d6b 100644 --- a/src/server/aggregator.ts +++ b/src/server/aggregator.ts @@ -78,19 +78,42 @@ interface CacheEntry { const fileCache = new Map(); function findSessionFiles(dir: string): string[] { - const results: string[] = []; - try { - const entries = fs.readdirSync(dir, { withFileTypes: true }); - for (const entry of entries) { - const fullPath = path.join(dir, entry.name); - if (entry.isDirectory()) { - results.push(...findSessionFiles(fullPath)); - } else if (entry.name.endsWith(".jsonl.zstd") || entry.name.endsWith(".jsonl")) { - results.push(fullPath); + // Group files by session directory. DSH keeps a migrated copy of each + // session (session.v3.jsonl.zstd alongside session.v4.jsonl.zstd) that + // contains the SAME records — reading both double-counts every turn. + // Prefer the newest schema version per directory. + const byDir = new Map(); + const collect = (d: string) => { + try { + const entries = fs.readdirSync(d, { withFileTypes: true }); + for (const entry of entries) { + const fullPath = path.join(d, entry.name); + if (entry.isDirectory()) { + collect(fullPath); + } else if (entry.name.endsWith(".jsonl.zstd") || entry.name.endsWith(".jsonl")) { + if (!byDir.has(d)) byDir.set(d, []); + byDir.get(d)!.push(fullPath); + } } + } catch { + // Ignore unreadable dirs } - } catch { - // Ignore unreadable dirs + }; + collect(dir); + const results: string[] = []; + for (const files of byDir.values()) { + if (files.length === 1) { + results.push(files[0]); + continue; + } + // Prefer highest session version (v4 > v3) + const ranked = files + .map((f) => { + const m = path.basename(f).match(/\.v(\d+)\.jsonl/); + return { f, v: m ? parseInt(m[1], 10) : 0 }; + }) + .sort((a, b) => b.v - a.v); + results.push(ranked[0].f); } return results; } @@ -140,8 +163,12 @@ async function parseSessionFile(filePath: string): Promise