import * as fs from 'node:fs'; import * as path from 'node:path'; import { streamedUsageCostUsd } from '../../../lib/pricing'; /** The session ledger's cost field for a PTY session: present only when its transcript prices. */ export function pricedTranscript(session: { transcriptCostUsd?(): number | undefined } | undefined): { streamed_cost_usd?: number } { const cost = session?.transcriptCostUsd?.(); return cost === undefined ? {} : { streamed_cost_usd: cost }; } /** Top-level session transcripts (`projects//.jsonl`) under a config dir; empty when unreadable. */ export function transcriptFiles(configDir: string | undefined): Set { if (!configDir) return new Set(); try { const projects = path.join(configDir, 'projects'); return new Set(fs.readdirSync(projects, { withFileTypes: true }).filter(dir => dir.isDirectory()) .flatMap(dir => fs.readdirSync(path.join(projects, dir.name)).filter(f => f.endsWith('.jsonl')).map(f => path.join(projects, dir.name, f)))); } catch { return new Set(); } } /** * The session id of the one transcript that appeared since `before` and was written for `cwd`. Concurrent PTY tests in * one process share a hermetic config dir, so a launch claims only a new transcript for its own cwd; none or several * (two sessions in one cwd) is undefined, which leaves the cost unknown. */ export function newTranscriptSessionId(configDir: string | undefined, before: ReadonlySet, cwd: string): string | undefined { try { const cwds = new Set([cwd, fs.realpathSync(cwd)]); const mine = [...transcriptFiles(configDir)].filter(file => !before.has(file) && fs.readFileSync(file, 'utf8').split('\n').some(line => { try { return cwds.has(JSON.parse(line)?.cwd); } catch { return false; } })); return mine.length === 1 ? path.basename(mine[0], '.jsonl') : undefined; } catch { return undefined; } } /** * Price one PTY session from the transcripts Claude Code wrote under its owned config dir: * `projects//.jsonl` (inline sidechains included) and `projects///subagents/*.jsonl`, * rows deduplicated by message id. Undefined (cost-unknown, never $0) without a config dir, session id, transcript or * usage, when a message's model has no price row, or when any read fails: it runs in ledger writers before close and * cleanup, so it never throws. */ export function ptyTranscriptCostUsd(configDir: string | null | undefined, sessionId: string | undefined): number | undefined { if (!configDir || !sessionId) return undefined; try { const projects = path.join(configDir, 'projects'); const files: string[] = []; for (const dir of fs.readdirSync(projects, { withFileTypes: true })) { if (!dir.isDirectory()) continue; const main = path.join(projects, dir.name, `${sessionId}.jsonl`); if (fs.existsSync(main)) files.push(main); const subagents = path.join(projects, dir.name, sessionId, 'subagents'); if (fs.existsSync(subagents)) files.push(...fs.readdirSync(subagents).filter(f => f.endsWith('.jsonl')).map(f => path.join(subagents, f))); } const rows = files.flatMap(file => fs.readFileSync(file, 'utf8').split('\n').flatMap(line => { try { return [JSON.parse(line)]; } catch { return []; } })); return streamedUsageCostUsd(rows); } catch { return undefined; } }