opencode-usage-insights 0.0.0-stage → 0.1.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.
@@ -0,0 +1,39 @@
1
+ /** @jsxImportSource @opentui/solid */
2
+ import type { Database } from "bun:sqlite";
3
+ import type { Context, PanelInput } from "@opencode/plugin/tui/context";
4
+ import { type LineFormatOptions, type PanelSettings } from "./panel-data";
5
+ /**
6
+ * The `/usage` panel. One read-only view over the stats database at a time,
7
+ * driven entirely from the keyboard: a cursor over the selectable lines,
8
+ * Enter drills into the row under it, and the views, range, sort, bucket and
9
+ * filters are part of the state the host persists between openings.
10
+ *
11
+ * The panel owns input while it is open, so every key lives in the layer this
12
+ * component registers — the prompt is not involved.
13
+ */
14
+ export type PanelDeps = {
15
+ /** Lazily opened stats database; undefined when it cannot be opened. */
16
+ db: () => Database | undefined;
17
+ /** Where that database lives, for the `db` view. */
18
+ dbPath: () => string;
19
+ /**
20
+ * Short-lived write connection for maintenance (prune, compact). The panel
21
+ * itself only reads; opening a writer on demand is what keeps a read from
22
+ * ever taking the recorder's lock. Undefined when the write cannot be made.
23
+ */
24
+ withWrite?: (<Value>(run: (db: Database) => Value) => Value | undefined) | undefined;
25
+ /** Injectable clock, for tests. */
26
+ now: () => number;
27
+ /** Creation time of a session, used by the `session` range. */
28
+ sessionStart: (sessionID: string) => number | undefined;
29
+ /** Bumped by the TUI when a step or an execution ends, so views re-read. */
30
+ refresh?: (() => number) | undefined;
31
+ };
32
+ export declare function Panel(props: {
33
+ readonly input: PanelInput;
34
+ readonly ctx: Context;
35
+ readonly deps: PanelDeps;
36
+ readonly settings: PanelSettings;
37
+ readonly update: (mutation: (draft: PanelSettings) => void) => void;
38
+ readonly format?: LineFormatOptions | undefined;
39
+ }): any;
@@ -0,0 +1,69 @@
1
+ import type { Database } from "bun:sqlite";
2
+ /**
3
+ * Event-driven recorder. Every handler is idempotent and defensive: the
4
+ * plugin must never break a session because an event arrived in an
5
+ * unexpected shape, so unknown events are ignored and malformed payloads are
6
+ * dropped rather than thrown.
7
+ *
8
+ * Everything written comes from the event stream: the step's provider, model
9
+ * and variant, its tokens, its timing and its outcome. Pure with respect to
10
+ * its inputs (database plus events) so the whole recording policy can be
11
+ * unit-tested without the plugin runtime.
12
+ */
13
+ export type SessionModelRef = {
14
+ id: string;
15
+ providerID: string;
16
+ variant?: string | undefined;
17
+ };
18
+ export type TokenUsageLike = {
19
+ input?: number | undefined;
20
+ output?: number | undefined;
21
+ reasoning?: number | undefined;
22
+ cache?: {
23
+ read?: number | undefined;
24
+ write?: number | undefined;
25
+ } | undefined;
26
+ };
27
+ export type RecorderEventData = {
28
+ sessionID?: string | undefined;
29
+ assistantMessageID?: string | undefined;
30
+ inboxID?: string | undefined;
31
+ item?: {
32
+ type?: string | undefined;
33
+ } | undefined;
34
+ started?: number | undefined;
35
+ model?: SessionModelRef | undefined;
36
+ previous?: SessionModelRef | undefined;
37
+ tokens?: TokenUsageLike | undefined;
38
+ cost?: number | undefined;
39
+ finish?: string | undefined;
40
+ rawFinish?: string | undefined;
41
+ reason?: string | undefined;
42
+ location?: {
43
+ directory?: string | undefined;
44
+ } | undefined;
45
+ parentID?: string | undefined;
46
+ title?: string | undefined;
47
+ };
48
+ export type RecorderEvent = {
49
+ type: string;
50
+ created?: number | undefined;
51
+ data?: RecorderEventData | undefined;
52
+ };
53
+ export type PendingStep = {
54
+ messageID: string;
55
+ sessionID: string;
56
+ started: number | undefined;
57
+ streamed: number | undefined;
58
+ model: SessionModelRef | undefined;
59
+ };
60
+ export type RecorderOptions = {
61
+ /** Clock override, for tests. */
62
+ now?: (() => number) | undefined;
63
+ };
64
+ export type Recorder = {
65
+ /** Apply one session event. Unknown or malformed events are ignored. */
66
+ handleEvent(event: RecorderEvent): void;
67
+ pendingFor(sessionID: string): PendingStep | undefined;
68
+ };
69
+ export declare function createRecorder(db: Database, options?: RecorderOptions): Recorder;
@@ -0,0 +1,25 @@
1
+ /** @jsxImportSource @opentui/solid */
2
+ import type { Database } from "bun:sqlite";
3
+ import type { Context } from "@opencode/plugin/tui/context";
4
+ import { type LineFormatOptions } from "./panel-data";
5
+ /**
6
+ * Sidebar history: the session's accumulated usage, then the last finished
7
+ * turns — when and through which provider, the model when it changed, and the
8
+ * prompt volumes. Reads the same database, the same buckets and the same
9
+ * formatter as the panel and the live line, and refreshes whenever a turn ends.
10
+ */
11
+ export type TurnsSidebarDeps = {
12
+ db: () => Database | undefined;
13
+ /** Bumped by the TUI when a step or an execution ends. */
14
+ refresh: () => number;
15
+ /** How many turns to show. */
16
+ limit: number;
17
+ /** Column budget of the sidebar, shared with the summary. */
18
+ budget?: number | undefined;
19
+ };
20
+ export declare function TurnsSidebar(props: {
21
+ readonly sessionID: string;
22
+ readonly ctx: Context;
23
+ readonly deps: TurnsSidebarDeps;
24
+ readonly options?: LineFormatOptions | undefined;
25
+ }): any;
@@ -0,0 +1,283 @@
1
+ /**
2
+ * The analysis layer: the reports the panel and the CLI show.
3
+ *
4
+ * Two shapes, chosen by measurement rather than taste:
5
+ *
6
+ * 1. **Aggregates in SQLite** — totals, counts, distinct values. `coverageOf`
7
+ * and the filter pickers never move a row into JavaScript.
8
+ * 2. **Narrow rows plus arithmetic in JS** — percentiles, pair rules, calendar
9
+ * buckets. SQLite's window functions cost about 17 µs per row per report
10
+ * here (a sort plus a materialisation), while reading the four to twelve
11
+ * columns a report actually needs costs 1–4 µs per row. The percentiles and
12
+ * the pair comparisons are therefore computed over lean projections: on the
13
+ * audit's 120k-step database a whole view costs a few hundred milliseconds,
14
+ * and on a normal one it is instant.
15
+ *
16
+ * The rules are not restated here. Cache reuse, idle gaps and the noise
17
+ * threshold come from `core.ts` — the same functions the live line uses — so
18
+ * the panel and the line can never disagree about what a cache loss is.
19
+ * `test/reports.test.ts` holds every report to those rules.
20
+ */
21
+ import type { Database } from "bun:sqlite";
22
+ import { type Filters, type LatencyStats, type Range, type Usage } from "./queries";
23
+ /** The step's own clock, the expression the `steps_at` index covers. */
24
+ export declare const STEP_AT = "COALESCE(created_at, recorded_at)";
25
+ type Options = {
26
+ range?: Range | undefined;
27
+ filters?: Filters | undefined;
28
+ };
29
+ /** One recorded step, as the reports read it. */
30
+ export type StepRow = {
31
+ messageID: string;
32
+ sessionID: string;
33
+ providerID: string | undefined;
34
+ modelID: string | undefined;
35
+ variant: string | undefined;
36
+ input: number;
37
+ cached: number;
38
+ cacheWrite: number;
39
+ output: number;
40
+ reasoning: number;
41
+ cost: number;
42
+ createdAt: number | undefined;
43
+ streamedAt: number | undefined;
44
+ completedAt: number | undefined;
45
+ /** `COALESCE(created_at, recorded_at)`: the ordering time of the step. */
46
+ at: number;
47
+ /** Set on the single step loaded before the range, as a reference. */
48
+ boundary?: boolean | undefined;
49
+ };
50
+ /**
51
+ * A step as the pair reports read it. Their projection leaves the generated
52
+ * tokens out (nothing they compute sums them), and the type says so: an
53
+ * aggregate that reads `output` cannot be handed a pair row by mistake.
54
+ */
55
+ export type PairRow = Omit<StepRow, "output" | "reasoning">;
56
+ /** The steps of the range, narrow. Ordering is only paid for where it is read. */
57
+ export declare function stepRows(db: Database, options?: Options): StepRow[];
58
+ /**
59
+ * The steps of the range plus, for every session with a step inside it, the one
60
+ * step before the range: the pair reports compare a step with the one that
61
+ * preceded it, so the first step of a range has to see it.
62
+ *
63
+ * The boundary is one seek per session on the `(session_id, time)` index — a
64
+ * few milliseconds for a far wider history — never the whole history of the
65
+ * sessions in range.
66
+ */
67
+ export declare function cachePairs(db: Database, options?: Options): PairRow[];
68
+ export type CoverageReport = {
69
+ steps: number;
70
+ models: number;
71
+ /** Providers the steps ran through. A step whose provider is unknown is counted nowhere. */
72
+ providers: number;
73
+ missingModel: number;
74
+ unsettled: number;
75
+ missingTps: number;
76
+ firstAt: number | undefined;
77
+ lastAt: number | undefined;
78
+ };
79
+ /**
80
+ * What the panel cannot answer honestly: how many steps lack a model, and how
81
+ * many never settled. A zero in the switch counts means "never switched" only
82
+ * when this coverage is complete.
83
+ */
84
+ export declare function coverageOf(db: Database, options?: Options): CoverageReport;
85
+ /** The steps a latency report is built from. It reads no generated tokens. */
86
+ type GroupedRow = StepRow;
87
+ export type LatencyReport = {
88
+ wall: LatencyStats;
89
+ stream: LatencyStats;
90
+ tps: LatencyStats;
91
+ };
92
+ /** The timings of the range: one read, shared by the overall report and the groups. */
93
+ export declare function timingRows(db: Database, options?: Options): GroupedRow[];
94
+ /** Overall wall time, streaming window and throughput percentiles. */
95
+ export declare function latencyOf(rows: readonly StepRow[]): LatencyReport;
96
+ export type LatencyGroupRow = {
97
+ key: string;
98
+ label: string;
99
+ count: number;
100
+ usage: Usage;
101
+ wall: LatencyStats;
102
+ stream: LatencyStats;
103
+ tps: LatencyStats;
104
+ };
105
+ export type Dimension = "model" | "provider";
106
+ /** Wall, streaming window and throughput percentiles per group. */
107
+ export declare function latencyGroupsOf(rows: readonly StepRow[], by: Dimension): LatencyGroupRow[];
108
+ export type CacheStats = {
109
+ steps: number;
110
+ input: number;
111
+ cached: number;
112
+ cacheWrite: number;
113
+ /** The whole prompt the provider saw: `cached + input + cache_write`. */
114
+ prompt: number;
115
+ /** Token-weighted reuse over the whole selection. */
116
+ hit: number | undefined;
117
+ /** Median of the per-step reuse ratios: a few huge steps cannot hide a split pattern. */
118
+ hitP50: number | undefined;
119
+ };
120
+ /** Reuse of the prompt cache over the range, as one row. */
121
+ export declare function cacheStatsOf(db: Database, options?: Options): CacheStats;
122
+ export type CacheModelRow = {
123
+ modelID: string;
124
+ variant: string | undefined;
125
+ cache: CacheStats;
126
+ };
127
+ /**
128
+ * Reuse of the prompt cache per model, most cached first, over the steps of the
129
+ * range. A boundary step is not part of the range, so it is left out.
130
+ */
131
+ export declare function cacheStatsByModel(rows: readonly PairRow[]): CacheModelRow[];
132
+ export type SwitchRow = {
133
+ modelID: string;
134
+ variant: string | undefined;
135
+ steps: number;
136
+ pairs: number;
137
+ /** Steps whose model, variant or provider differed from the step before. */
138
+ changes: number;
139
+ /** Of those, the ones whose model (or variant) differed. */
140
+ modelChanges: number;
141
+ /** Of those, the ones whose provider differed. */
142
+ providerChanges: number;
143
+ /** Cache the changes cost: only a provider switch can forfeit a reusable cache. */
144
+ cacheLost: number;
145
+ cacheLostEvents: number;
146
+ };
147
+ export type SwitchReport = {
148
+ rows: SwitchRow[];
149
+ totals: {
150
+ pairs: number;
151
+ changes: number;
152
+ cacheLost: number;
153
+ cacheLostEvents: number;
154
+ };
155
+ };
156
+ /**
157
+ * Switches between consecutive steps of a session, by the two dimensions that
158
+ * decide which cache holds the work: the model (with its variant) and the
159
+ * provider.
160
+ *
161
+ * A switch is not by itself a fault — it is usually the user choosing another
162
+ * model — so the report only names a cost where one is measurable: a provider
163
+ * switch that leaves the model intact forfeits the cache the previous provider
164
+ * held. A model switch has no measurable cost by definition, because two models
165
+ * never share a cache to lose.
166
+ */
167
+ export declare function switchesOf(rows: readonly PairRow[]): SwitchReport;
168
+ export type GapBucket = {
169
+ label: string;
170
+ samples: number;
171
+ /** Pairs that still reused the prompt cache. */
172
+ retained: number;
173
+ drops: number;
174
+ lostTokens: number;
175
+ retainedRatio: number | undefined;
176
+ };
177
+ export type ProviderReuseRow = {
178
+ /** The provider, or `(unknown)` when a step ran without one. */
179
+ provider: string;
180
+ samples: number;
181
+ retained: number;
182
+ drops: number;
183
+ lostTokens: number;
184
+ retainedRatio: number | undefined;
185
+ };
186
+ export type GapReport = {
187
+ buckets: GapBucket[];
188
+ providers: ProviderReuseRow[];
189
+ samples: number;
190
+ };
191
+ /** Idle-gap buckets, coarse enough to read at a glance. */
192
+ export declare const GAP_BUCKETS: ReadonlyArray<readonly [string, number, number | undefined]>;
193
+ /**
194
+ * How long an idle gap the prompt cache survives, per gap bucket and per
195
+ * provider. Only pairs of the same model are comparable, which is exactly the
196
+ * set the cache rule already uses.
197
+ */
198
+ export declare function gapReportOf(rows: readonly PairRow[]): GapReport;
199
+ export type TtlEvidenceRow = {
200
+ providerID: string;
201
+ modelID: string;
202
+ samples: number;
203
+ /** Largest idle gap that still hit the cache. */
204
+ maxHitGapMs: number | undefined;
205
+ /** Smallest idle gap after which reuse was lost. */
206
+ minDropGapMs: number | undefined;
207
+ };
208
+ /**
209
+ * Empirical bounds on the provider cache TTL: the longest idle gap that still
210
+ * reused the cache, and the shortest gap that lost it. Comparing the two per
211
+ * provider turns TTL guesses into observations.
212
+ */
213
+ export declare function ttlEvidenceOf(rows: readonly PairRow[]): TtlEvidenceRow[];
214
+ export type DropReason = "compaction" | "expired" | "unknown";
215
+ export type CacheDropRow = {
216
+ sessionID: string;
217
+ messageID: string;
218
+ providerID: string | undefined;
219
+ modelID: string | undefined;
220
+ variant: string | undefined;
221
+ at: number;
222
+ tokens: number;
223
+ gapMs: number | undefined;
224
+ prevCached: number;
225
+ reason: DropReason;
226
+ };
227
+ /**
228
+ * Cache reuse losses with a best-effort reason: a compaction in between, an
229
+ * idle gap long enough to be provider-side expiry, or unknown.
230
+ *
231
+ * Compactions come from the `events` table: one query for the sessions in
232
+ * play, never one per loss.
233
+ */
234
+ export declare function cacheDropsOf(db: Database, rows: readonly PairRow[], options?: {
235
+ ttlHintMs?: number;
236
+ }): CacheDropRow[];
237
+ export type ProviderSummary = {
238
+ /** The provider, or undefined for steps recorded without one. */
239
+ provider: string | undefined;
240
+ /** Models this provider served. */
241
+ models: string[];
242
+ share: number;
243
+ usage: Usage;
244
+ cache: CacheStats;
245
+ tps: number | undefined;
246
+ wall: number | undefined;
247
+ firstAt: number | undefined;
248
+ lastAt: number | undefined;
249
+ };
250
+ export type ProviderRow = ProviderSummary & {
251
+ modelID: string;
252
+ variant: string | undefined;
253
+ };
254
+ /** One row per provider, over every model it served. */
255
+ export declare function providerSummaryOf(rows: readonly StepRow[]): ProviderSummary[];
256
+ /** One row per model × provider: through which provider each model ran, and how much. */
257
+ export declare function providerBreakdownOf(rows: readonly StepRow[]): ProviderRow[];
258
+ export type PeriodGranularity = "day" | "week" | "month";
259
+ export type PeriodRow = {
260
+ key: string;
261
+ label: string;
262
+ /** Start of the bucket, local time. */
263
+ at: number;
264
+ steps: number;
265
+ sessions: number;
266
+ usage: Usage;
267
+ cache: CacheStats;
268
+ };
269
+ /** ISO-8601 week number and its Monday, in local time. */
270
+ export declare function isoWeek(date: Date): {
271
+ year: number;
272
+ week: number;
273
+ monday: Date;
274
+ };
275
+ /** Key, label and local start of the period `at` falls in. */
276
+ export declare function periodStart(at: number, granularity: PeriodGranularity): {
277
+ key: string;
278
+ label: string;
279
+ start: number;
280
+ };
281
+ /** Totals per local calendar day, week or month, oldest first. */
282
+ export declare function periodsOf(rows: readonly StepRow[], granularity: PeriodGranularity): PeriodRow[];
283
+ export {};
@@ -0,0 +1,99 @@
1
+ import { Database } from "bun:sqlite";
2
+ /**
3
+ * Persistent usage store. One SQLite database holds every recorded step,
4
+ * prompt, event and session row — and nothing else: the recorder only writes
5
+ * what a view reads.
6
+ *
7
+ * The database is opened in WAL mode with a busy timeout so the server
8
+ * recorder process and the TUI reader process can share it safely. The two
9
+ * sides open it differently on purpose:
10
+ *
11
+ * - `openStatsDb` writes: schema, migrations and recording (the server).
12
+ * - `openStatsDbReadOnly` only reads (the TUI), so no view can change the file
13
+ * and no reader pays for a schema write.
14
+ * - `openStatsDbWrite` is a short-lived maintenance connection (prune, vacuum)
15
+ * that gives up quickly instead of holding the recorder up.
16
+ */
17
+ export declare const SCHEMA_VERSION = 9;
18
+ /** Tables carried by `exportNdjson` / `importNdjson`, in import-safe order. */
19
+ export declare const TABLES: readonly ["sessions", "prompts", "events", "steps"];
20
+ export type TableName = (typeof TABLES)[number];
21
+ export type StepRecord = {
22
+ messageID: string;
23
+ sessionID: string;
24
+ parentID?: string | undefined;
25
+ /** The provider the model ran through, as OpenCode configured it (`opencode-go`). */
26
+ providerID?: string | undefined;
27
+ modelID?: string | undefined;
28
+ variant?: string | undefined;
29
+ input?: number | undefined;
30
+ output?: number | undefined;
31
+ reasoning?: number | undefined;
32
+ cacheRead?: number | undefined;
33
+ cacheWrite?: number | undefined;
34
+ cost?: number | undefined;
35
+ createdAt?: number | undefined;
36
+ streamedAt?: number | undefined;
37
+ completedAt?: number | undefined;
38
+ finish?: string | undefined;
39
+ /** Clock of the recording itself, defaults to `Date.now()`. */
40
+ recordedAt?: number | undefined;
41
+ };
42
+ export type PromptRecord = {
43
+ sessionID: string;
44
+ messageID: string;
45
+ createdAt?: number | undefined;
46
+ };
47
+ export type EventRecord = {
48
+ sessionID: string;
49
+ kind: string;
50
+ messageID?: string | undefined;
51
+ at?: number | undefined;
52
+ data?: unknown;
53
+ };
54
+ export type SessionRecord = {
55
+ sessionID: string;
56
+ parentID?: string | undefined;
57
+ directory?: string | undefined;
58
+ title?: string | undefined;
59
+ startedAt?: number | undefined;
60
+ updatedAt?: number | undefined;
61
+ };
62
+ /** Open (creating when needed) the stats database and apply the schema. */
63
+ export declare function openStatsDb(path: string): Database;
64
+ /**
65
+ * Reader connection for the TUI: no schema, no migration, no write. It throws
66
+ * when the database is absent or was never written by the recorder, so the
67
+ * caller can say so instead of showing an empty panel.
68
+ */
69
+ export declare function openStatsDbReadOnly(path: string): Database;
70
+ /**
71
+ * Short-lived write connection for maintenance. The busy timeout is short on
72
+ * purpose: a prune or a vacuum that cannot take the lock must fail in front of
73
+ * the user, not hold the recorder's response path for seconds.
74
+ */
75
+ export declare function openStatsDbWrite(path: string): Database;
76
+ /**
77
+ * Insert or refresh an assistant step. Idempotent by message ID so replayed
78
+ * events, restarts and NDJSON imports never duplicate usage. A non-null field
79
+ * (the provider, the model, the finish reason) survives a re-record with `NULL`
80
+ * through COALESCE.
81
+ *
82
+ * The provider is the identity every view groups by, so it is validated as a
83
+ * token before it is stored: a value that is not a provider id is recorded as
84
+ * absent instead of becoming a grouping key.
85
+ */
86
+ export declare function recordStep(db: Database, record: StepRecord): void;
87
+ export declare function recordPrompt(db: Database, record: PromptRecord): void;
88
+ export declare function recordEvent(db: Database, record: EventRecord): void;
89
+ export declare function upsertSession(db: Database, record: SessionRecord): void;
90
+ /** Dump every table as NDJSON (`{"table": "...", "row": {...}}` per line). */
91
+ export declare function exportNdjson(db: Database): string;
92
+ /**
93
+ * Load an `exportNdjson` dump back in. Rows are inserted with `OR IGNORE`, so
94
+ * importing the same dump twice is harmless. Unknown tables and rows that are
95
+ * not objects are rejected; a row that cannot fill a required column is
96
+ * skipped rather than aborting the rest of the import. A column the row does
97
+ * not carry is left to its default, the same way a hand-written insert is.
98
+ */
99
+ export declare function importNdjson(db: Database, text: string): number;
@@ -0,0 +1,49 @@
1
+ import type { Database } from "bun:sqlite";
2
+ import { type TableName } from "./db";
3
+ /**
4
+ * Database maintenance: what the file holds, how much of it is worth keeping,
5
+ * and the two operations that give the space back.
6
+ *
7
+ * Everything is explicit — nothing here runs on its own. The panel's `db` view
8
+ * and `bun run report` are the only callers.
9
+ */
10
+ export type TableSize = {
11
+ name: string;
12
+ rows: number;
13
+ };
14
+ export type DbInfo = {
15
+ path: string;
16
+ /** Size of the database file, in bytes. */
17
+ bytes: number;
18
+ /** Size of its write-ahead log, when one exists. */
19
+ walBytes: number | undefined;
20
+ schemaVersion: number;
21
+ tables: TableSize[];
22
+ /** Oldest and newest recorded step, the window the views can answer for. */
23
+ firstAt: number | undefined;
24
+ lastAt: number | undefined;
25
+ /** Steps a prune at each cutoff would remove, for the panel to offer choices. */
26
+ removable: Array<{
27
+ days: number;
28
+ steps: number;
29
+ }>;
30
+ };
31
+ export declare function dbInfo(db: Database, path: string, now?: number): DbInfo;
32
+ export type PruneResult = {
33
+ steps: number;
34
+ prompts: number;
35
+ events: number;
36
+ };
37
+ /**
38
+ * Delete recorded usage older than `cutoff`. Steps, their prompts and the
39
+ * compaction events that explain their cache losses go together: a compaction
40
+ * that no step references answers nothing.
41
+ */
42
+ export declare function pruneOlderThan(db: Database, cutoff: number): PruneResult;
43
+ /** Reclaim the space a prune freed. Returns the bytes the database lost. */
44
+ export declare function vacuum(db: Database, path: string): {
45
+ before: number;
46
+ after: number;
47
+ freed: number;
48
+ };
49
+ export type { TableName };
@@ -0,0 +1,35 @@
1
+ /**
2
+ * SQLite database used when no `db` option is configured: the per-user state
3
+ * directory of the current OS, never a shared temp directory.
4
+ *
5
+ * The subdirectory is the plugin's package name, never a generic one: the
6
+ * `opencode` tree is shared with OpenCode itself and with every other plugin,
7
+ * so a `stats/` of our own would be a collision waiting to happen.
8
+ *
9
+ * - Windows: `%LOCALAPPDATA%\opencode\opencode-usage-insights\usage.db`
10
+ * - macOS: `~/Library/Application Support/opencode/opencode-usage-insights/usage.db`
11
+ * - Linux/other: `$XDG_STATE_HOME/opencode/opencode-usage-insights/usage.db`,
12
+ * falling back to `~/.local/state/opencode/opencode-usage-insights/usage.db`
13
+ *
14
+ * Pure by design so every OS can be asserted from any machine.
15
+ */
16
+ export declare function defaultDbPath(env?: Record<string, string | undefined>, platform?: string): string;
17
+ /**
18
+ * Expand a leading `~` (or `~/`), so a configured path reads the way every
19
+ * shell does. Windows accepts the backslash form too.
20
+ */
21
+ export declare function expandHome(path: string, home?: string): string;
22
+ /**
23
+ * Resolve the effective database path: explicit plugin option first, then the
24
+ * `OPENCODE_USAGE_INSIGHTS_DB` escape hatch (handy for dev and tests), then
25
+ * the per-OS state directory. A leading `~` is expanded in all three.
26
+ */
27
+ export declare function resolveDbPath(configured?: string | undefined, env?: Record<string, string | undefined>, platform?: string): string;
28
+ /**
29
+ * A configured path must not be a directory, a device or a FIFO: SQLite would
30
+ * fail obscurely, or write where nobody expects it. Symlinks to a regular file
31
+ * are followed (`statSync`, not `lstat`) because moving the state directory to
32
+ * another disk is a legitimate setup. A missing file is fine — the plugin
33
+ * creates it.
34
+ */
35
+ export declare function ensureRegularFile(path: string): void;