/** * Claude Code transcripts, read as a usage log — the 1.69 arc's one move. * * Claude Code writes a transcript for every session, and each assistant * line carries the API's own `usage` object: the counts, and the * `cache_creation` TTL split that settles whether caching paid off. This * module turns that transcript into usage-log records **without reading * what was said**: the conversion touches `message.model`, `message.usage`, * `timestamp`, `sessionId` and `requestId`, and nothing else survives into * the output — no message text, no `cwd`, no `gitBranch`, held by a test * that plants a secret in each and greps the whole output for it. * * **One API call is written as several lines.** A multi-block response * repeats the same `usage` object on one line per content block — in the * session this was designed against, 25,490 assistant lines collapsed to * 16,079 distinct `requestId`s, so a line-by-line conversion overbills by a * third. Records are deduplicated by `requestId`, keeping the **last** * line's usage — the call's final state. Two ways the lines of one call * differ, measured on real transcripts and told apart on purpose: counts * that only ever grow are a response written while still streaming (the * norm — 311 of them across one real project's 195 transcripts), counted * in `streamed` without alarm; anything else is a genuine `disagreement`, * and the caller says that one out loud. */ /** * One directory prefix, and the label the work under it belongs to. * * The answer to a question one session cannot answer any other way: **which * project was this call for**, when two of them share a transcript. Claude Code * records a `cwd` on every line and this module has never emitted it, on * purpose — a working directory is a file path, and a file path says something * about somebody's machine that a bill does not need. * * Reading it to *choose* a label is not the same act as emitting it, and the * difference is the whole contract here: the prefix and the label are written * by the person running the conversion, the `cwd` decides which of *their own* * labels applies, and **nothing derived from the path reaches the output**. * `claude-code.test.js` plants a secret in `cwd` and greps the whole output for * it, which is the same test that already holds for message text and branch * names, and `label-by-cwd.test.js` does it again through the CLI where the * stderr summary is searched too. * * A guessed label would be worse than none. Nothing here decodes, splits or * shortens a path: the longest matching prefix wins and an unmatched line * falls back to `label`, so a directory nobody wrote a rule for is * unattributed rather than attributed to a neighbour. */ export interface CwdLabel { /** An absolute directory prefix, compared literally. */ prefix: string; label: string; } /** One converted record, shaped exactly as `parseUsageLine` reads it. */ export interface ClaudeCodeRecord { model: string; ts: string; session: string; label?: string; /** * What the provider said ended the turn, when the transcript recorded it. * * The field was on the assistant message all along and this converter did * not read it, so every report that depends on it answered "cannot be * measured" about a log whose own source knew. Absent when the transcript * is absent: nothing here is inferred from an output length that looks * suspiciously round. */ stop_reason?: string; usage: { input_tokens: number; output_tokens: number; cache_read_input_tokens?: number; cache_creation_input_tokens?: number; cache_creation?: { ephemeral_5m_input_tokens?: number; ephemeral_1h_input_tokens?: number; }; }; } export interface ClaudeCodeConversion { records: ClaudeCodeRecord[]; /** Extra lines of already-seen requests, collapsed — not spend. */ collapsed: number; /** Assistant lines carrying usage but no requestId: kept, and counted. */ noRequestId: number; /** Lines of the transcript's other business: user turns, attachments, system. */ otherLines: number; /** Lines that did not parse as JSON at all. */ unparseable: number; /** Assistant lines with no usage object — nothing to price. */ assistantWithoutUsage: number; /** * Requests written while still streaming: later lines carry larger counts * (the in-flight total growing), and the last line is the final state. * The measured norm on real transcripts — counted, not alarmed about. */ streamed: number; /** * Requests whose lines disagreed in a way streaming cannot explain — a * count shrank, or a different field changed. The last line stood, and * the caller should say so loudly: this is a finding, not bookkeeping. */ disagreements: number; /** * Turns Claude Code produced locally, never sent to a provider. * * It writes `` in the model field for interrupts and error * notices. They carry a usage object of zeros and nobody billed them. * Priced, they are noise; dropped in silence, they are a hole. So they * are excluded by name and counted here, and the caller says how many. */ synthetic: number; /** * Where a later run may safely pick this transcript up again. * * A transcript is append-only, so re-reading two hundred megabytes to learn * what the last thirty seconds added is waste, and on the largest real * session on one machine it is six and a half seconds of waste. The obstacle * to resuming is this converter's own rule: **one call arrives as several * lines and the last one stands**, so a call whose lines straddle the point * where a run stopped would be recorded from its later half only. * * So the resume point is not the end of what was read. It is the first line * of the last call seen, which is the only call that can still gain lines. * `records` says how many of `records` above are settled and will never be * revised; everything from `line` onwards is re-derived next time, and the * caller drops the unsettled tail before appending. * * Measured on 208 real transcripts, 36,468 lines carrying a `requestId`: a * call's lines are contiguous, and no `requestId` ever reappeared after * another had begun. The design does not lean on that. It re-derives the * final call whether or not it needed re-deriving, because a measurement on * one machine is evidence and not a guarantee. */ resume: { /** Line index to start from next time, counting every line of the text read. */ line: number; /** How many leading entries of `records` are settled. */ records: number; }; } export declare function claudeCodeRecords(text: string, options?: { label?: string; labelByCwd?: readonly CwdLabel[]; }): ClaudeCodeConversion; /** * Does this text look like a Claude Code transcript rather than a usage log? * * The web app accepts a dropped folder that mixes both, and has to tell them * apart per file with no help from the reader: a transcript's lines are * conversation events (`type: 'assistant'`, `'user'`, `'system'`…) with the * usage buried in `message.usage`, while a usage log's lines are the usage * records themselves (`model` and `usage` at the top). Deliberately dumb — * no scoring, no tunable threshold: a file is a transcript when at least one * line is an assistant event carrying `message.usage`, and not otherwise. * That is the exact shape `claudeCodeRecords` converts, so "looks like one" * and "converts to something" cannot disagree. * * Reads at most the first `limit` non-empty lines: a transcript announces * itself early, and a hundred-megabyte session should not be parsed in full * just to route it. */ export declare function looksLikeClaudeCodeTranscript(text: string, limit?: number): boolean; //# sourceMappingURL=claude-code.d.ts.map