import { type SqliteDriverName } from '../db/driver.js'; import { type SyncState } from '../db/repositories/sync-repository.js'; import { type Page, type GroupAxis, type PageRequest, type TimeGrain, type SessionRow, type UsageFilter } from '../db/repositories/usage-repository.js'; import { type ClaudeIntegrations } from '../collectors/claude-code/settings.js'; import type { ClientId, StoreInfo } from '../models/usage-record.js'; import { type BreakdownReport, type CacheHealthReport, type ClientReport, type DailyReport, type ModelReport, type ProjectReport, type SessionDetail, type SummaryReport } from './aggregation-service.js'; import { type ContextLimits, type ContextReading } from './context-service.js'; import { CostService } from './cost-service.js'; import { type ExportOptions, type ExportResult } from './export-service.js'; import { LifecycleService } from './lifecycle-service.js'; import { type BudgetBasis, type BudgetPeriod, type BudgetReport } from './budget-service.js'; import { type CounterfactualReport } from './counterfactual-service.js'; import { type PeriodInput } from './period.js'; import { type PricingRefreshOptions, type PricingRefreshResult } from './pricing-refresh.js'; import { type RepriceResult } from './reprice-service.js'; import { type SyncOptions, type SyncReport } from './sync-service.js'; import { type VerifyReport } from './verify-service.js'; import { type CacheBreakOptions } from './cache-breaks.js'; import { type UsageReport } from './report-service.js'; import { type PlanUsageReport, type StatusLineReport } from './plan-limit-service.js'; import type { CommunityPricingState } from '../pricing/index.js'; /** A refresh, and what applying it did to the stored rows. */ export type PricingRefreshReport = PricingRefreshResult & { /** Present when the refresh changed the table and that priced stored rows. */ repriced?: RepriceResult; }; export interface UsageQuery extends PeriodInput { /** Restrict to any of these clients. */ clients?: ClientId[]; /** Restrict to any of these model ids. */ models?: string[]; /** Restrict to any of these project working directories. */ projectPaths?: string[]; /** Defaults to true -- subagent turns are real spend. */ includeSubagents?: boolean; } export interface CollectorStatus { name: string; client: ClientId; available: boolean; reason?: string; stores: StoreInfo[]; lastSyncAt?: string; records: number; lastRecordAt?: string; /** When the client last wrote to its default source, where the collector can tell. */ lastWriteAt?: string; /** Set when a sync after the source's last write found no turn near it. */ sourceAhead?: SourceAhead; } /** * A source written to well after the newest turn collected from it, by a client * a sync has looked at since. The usual cause is a format change -- OpenCode 2.x * moving new turns to a table we did not read hid two days of usage from a user, * and nothing said so. Opening a client without sending a prompt also writes, so * this is a prompt to check, never a claim that data is missing. */ export interface SourceAhead { lastWriteAt: string; lastRecordAt: string; gapMs: number; } /** * How far a source may run ahead of its newest turn before `status` says so. * On the machine this was calibrated on, OpenCode wrote 56 minutes after its * last turn as it shut down; the format change that motivated this left a gap of * 39 hours and growing. */ export declare const SOURCE_AHEAD_THRESHOLD_MS: number; export declare function sourceAhead(status: Pick, thresholdMs?: number): SourceAhead | undefined; export interface StatusReport { databasePath: string; /** Which SQLite implementation produced these numbers. */ sqliteDriver: SqliteDriverName; schemaVersion: number; totalRecords: number; collectors: CollectorStatus[]; pricing: { version: string; provenance: string; overridePath?: string; /** How the table was assembled: built-in, a user overlay, or a full replacement. */ mode: 'builtin' | 'overlay' | 'replace'; /** The built-in table an overlay sits on. Present only for `overlay`. */ baseVersion?: string; /** What the community price list contributes, or why it is off. */ community?: CommunityPricingState; }; syncState: SyncState[]; /** Whether the Claude Code status line and prompt hook are switched on. */ claudeIntegrations: ClaudeIntegrations; } /** * The single entry point for every frontend. * * The MCP tools and the CLI both go through this class and nothing else. That is * what keeps the MCP layer ignorant of where data comes from, and it is why * `ai-usage stats --today` and the `usage_summary` tool cannot disagree: they run * the same method with the same arguments. */ export declare class UsageService { private readonly db; private readonly dbPath; readonly costService: CostService; private readonly usageRepo; private readonly syncRepo; private readonly aggregation; private readonly syncService; private readonly verifyService; private readonly reportService; private readonly counterfactualService; private readonly exportService; private readonly budgetService; private readonly lifecycleService; private readonly repriceService; private readonly planLimitService; private readonly contextService; private readonly collectors; private constructor(); static open(options?: { dbPath?: string; }): UsageService; close(): void; /** Translates a caller-facing query into a repository filter. */ private filterFor; status(): Promise; /** * Collects, then prices any stored rows the table has since learned to price. * * The second step runs on every sync, not only after a pricing change: a * background refresh can swap the table while a collector is mid-read, and * rows that collector priced with the old table land after the swap. The check * is one DISTINCT query, and writes nothing when no unpriced row can be priced. */ sync(options?: SyncOptions): Promise; /** Whether any collector has ever synced into this database. */ hasSynced(): boolean; /** * Syncs only when the newest sync of any collector is older than `maxAgeMs`. * * For callers that run far more often than the data changes -- a status line * runs on every assistant message. Returns whether a sync ran. */ syncIfStale(maxAgeMs: number, now?: Date): Promise; /** * Re-reads the pricing in force and prices whatever stored rows it now can. * Returns undefined when no price changed. Throws, leaving the current table * in force, if the override on disk has become malformed. */ reloadPricing(): RepriceResult | undefined; /** * Downloads the community price list when the cached copy is over a day old, * and applies it at once. Never throws: a failed download, or a new list that * cannot be applied, leaves the prices in force as they were. */ refreshPricing(options?: PricingRefreshOptions): Promise; verify(options?: { allStores?: boolean; cutoff?: Date; }): Promise; summary(query?: UsageQuery, options?: { compare?: boolean; }): SummaryReport; modelUsage(query?: UsageQuery, page?: PageRequest): ModelReport; clientUsage(query?: UsageQuery, page?: PageRequest): ClientReport; projectUsage(query?: UsageQuery, page?: PageRequest): ProjectReport; recentSessions(query?: UsageQuery, page?: PageRequest): Page; sessionUsage(sessionId: string, includeSubagents?: boolean): SessionDetail | { ambiguous: string[]; } | undefined; /** * Where a period's usage went and how much of it did no new work, with fixes. * A report of all time is rarely what anyone wants first, so with no period * given it covers the last 7 days -- and its label says so. */ usageReport(query?: UsageQuery): UsageReport; cacheHealth(sessionId: string, includeSubagents?: boolean, options?: CacheBreakOptions): CacheHealthReport | { ambiguous: string[]; } | undefined; dailyUsage(query?: UsageQuery, grain?: TimeGrain): DailyReport; /** * Totals cut by two or more dimensions at once -- `project x day`, `model x day`. * * Answering "which of my projects is getting more expensive" previously meant * enumerating projects, issuing one `daily --project` call each, and joining * the results: an N+1 pattern that is not feasible as a tool call at all. */ breakdown(axes: GroupAxis[], query?: UsageQuery, page?: PageRequest): BreakdownReport; counterfactualCost(query?: UsageQuery, models?: string[]): CounterfactualReport; /** * Spend against a target for the current calendar month or week, with the * run rate and where it lands at period end. */ budget(options: { amount: number; basis: BudgetBasis; period: BudgetPeriod; query?: UsageQuery; now?: Date; }): BudgetReport; /** * Reads the JSON Claude Code hands a status-line command and keeps the * plan-limit readings it carried. Never throws on bad input: anything * unreadable is no data. Returns the session being rendered, and how many new * readings were stored. */ recordStatusLine(raw: string, now?: Date): { sessionId?: string; transcriptPath?: string; written: number; }; /** * Usage against the Pro/Max 5-hour and weekly limits, as Claude Code last * reported it, with this machine's Claude Code turns in each window beside it. * Each window is measured over its own span, so no period applies. */ planUsage(options?: { includeSubagents?: boolean; now?: Date; }): PlanUsageReport; /** * What a status line shows: current plan-limit readings, today's totals, and * the cache in the session being rendered, and -- given its transcript and the * limits -- how large that session's context is. Reads only -- record the * payload with `recordStatusLine` first. */ statusLine(sessionId: string | undefined, now?: Date, context?: { transcriptPath: string | undefined; limits: ContextLimits; }): StatusLineReport; /** * The session's context size against the limits, read from its transcript, or * undefined when unknown. Touches no database and never throws. */ contextSize(transcriptPath: string | undefined, limits: ContextLimits): ContextReading | undefined; /** * The repository filter a query resolves to, for a caller that needs the * filter itself rather than a report -- currently only the record-level export. */ exportFilter(query?: UsageQuery): { filter: UsageFilter; label: string; }; /** * One row per stored turn, streamed to `write`. * * The supported route to the underlying records. Previously the only way to * reach them was `sqlite3 usage.db '.mode csv' 'SELECT * FROM usage_records'`, * which bypasses the product entirely and depends on a schema the docs call * internal and unversioned. */ exportRecords(filter: UsageFilter, write: (line: string) => void, options?: ExportOptions): ExportResult; /** * Removes records older than a cutoff. Dry run unless `apply` is true: the * only irreversible operation in this package reports what it would remove and * changes nothing until told twice. */ prune(cutoff: string, options?: { apply?: boolean; query?: UsageQuery; }): ReturnType; /** Compacts the database file after a prune. */ vacuum(): ReturnType; /** Merges JSON Lines from `ai-usage export --format jsonl`, idempotently. */ importRecords(lines: Iterable): ReturnType; /** True when there is no data at all, so frontends can say so instead of printing zeros. */ isEmpty(): boolean; }