@rejacky/opencode-insights 0.1.4 → 0.1.6

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.
package/README.md CHANGED
@@ -12,45 +12,89 @@ opencode plugin @rejacky/opencode-insights --global
12
12
 
13
13
  Restart OpenCode after installing the plugin.
14
14
 
15
+ On startup, the plugin creates a user-local `opencode-insights` command shim in:
16
+
17
+ ```text
18
+ ~/.local/bin
19
+ ```
20
+
21
+ Make sure that directory is on your `PATH`, then run the CLI directly:
22
+
23
+ ```bash
24
+ opencode-insights doctor
25
+ ```
26
+
27
+ ## Update
28
+
29
+ `opencode plugin` does not re-install or upgrade already-cached packages. To update to the latest version, clear the cached copy and reinstall:
30
+
31
+ ```bash
32
+ rm -rf ~/.cache/opencode/packages/node_modules/@rejacky/opencode-insights
33
+ opencode plugin @rejacky/opencode-insights --global
34
+ ```
35
+
36
+ Then restart OpenCode.
37
+
38
+ You can also run the latest published version directly via `npx` without reinstalling:
39
+
40
+ ```bash
41
+ npx -y -p @rejacky/opencode-insights opencode-insights doctor
42
+ ```
43
+
44
+ ## Preview
45
+
46
+ ![opencode-insights TUI](assets/tui.png)
47
+
48
+ Inspect captured sessions with the web viewer:
49
+
50
+ ```bash
51
+ opencode-insights open
52
+ ```
53
+
54
+ OpenCode Insights viewer listening at http://127.0.0.1:8765
55
+
56
+ ![opencode-insights web viewer](assets/insights.png)
57
+
15
58
  ## Uninstall
16
59
 
17
60
  Remove this plugin from `opencode.json` / `opencode.jsonc`, remove it from `tui.json`, and delete the local Insights database files:
18
61
 
19
62
  ```bash
20
- npx opencode-insights uninstall
63
+ opencode-insights uninstall
21
64
  ```
22
65
 
23
66
  Preview the cleanup without changing files:
24
67
 
25
68
  ```bash
26
- npx opencode-insights uninstall --dry-run
69
+ opencode-insights uninstall --dry-run
27
70
  ```
28
71
 
29
72
  Keep captured data while removing only the plugin config entries:
30
73
 
31
74
  ```bash
32
- npx opencode-insights uninstall --keep-data
75
+ opencode-insights uninstall --keep-data
33
76
  ```
34
77
 
35
78
  Use a custom OpenCode config directory or data location:
36
79
 
37
80
  ```bash
38
- npx opencode-insights uninstall --config-dir ~/.config/opencode --data-dir ~/.opencode-insights
81
+ opencode-insights uninstall --config-dir ~/.config/opencode --data-dir ~/.opencode-insights
39
82
  ```
40
83
 
41
- After uninstalling, restart OpenCode. If you also want to remove the npm package from your OpenCode config/package directory, run:
84
+ After uninstalling, restart OpenCode. Packages installed with `opencode plugin ... --global` are stored under OpenCode's package cache. On macOS/Linux this is typically:
42
85
 
43
- ```bash
44
- cd ~/.config/opencode
45
- npm rm @rejacky/opencode-insights
86
+ ```text
87
+ ~/.cache/opencode/packages
46
88
  ```
47
89
 
90
+ The `uninstall` command removes plugin config entries and local Insights data; it does not remove cached OpenCode package directories automatically.
91
+
48
92
  ## What You Get
49
93
 
50
94
  - Live TPS, average TPS, and average TTFT in the OpenCode session prompt zone.
51
95
  - Subagent status (running, done, failed, elapsed time, and token/context usage) in the sidebar.
52
96
  - Local capture of OpenCode hook/event data without redaction.
53
- - A local web viewer for sessions, messages, hooks, request context, system/messages transforms, and assistant responses.
97
+ - A local web viewer for reconstructed sessions, user turns, hidden request context, system/messages transforms, and assistant thinking/response sequences.
54
98
  - Native OpenCode footer components (project directory and version) remain visible — the plugin does not override `sidebar_footer` or `home_prompt_right` slots.
55
99
 
56
100
  ## Open The Viewer
@@ -58,13 +102,13 @@ npm rm @rejacky/opencode-insights
58
102
  Start the local web viewer and open it in your browser:
59
103
 
60
104
  ```bash
61
- npx opencode-insights open --limit 5000 --port 8765
105
+ opencode-insights open --limit 5000 --port 8765
62
106
  ```
63
107
 
64
108
  Or run the server only:
65
109
 
66
110
  ```bash
67
- npx opencode-insights serve --limit 5000 --port 8765
111
+ opencode-insights serve --limit 5000 --port 8765
68
112
  ```
69
113
 
70
114
  Then open:
@@ -75,59 +119,66 @@ http://127.0.0.1:8765/
75
119
 
76
120
  The viewer shows:
77
121
 
78
- - `MSG` rows for user messages.
79
- - `HOOK` rows for OpenCode plugin hooks.
80
- - `Summary`, `Request`, `Response`, and `Raw` tabs.
81
- - Expandable/collapsible JSON trees with `Expand All` and `Collapse All`.
122
+ - Project/session filters with subagent sessions nested under their parent session.
123
+ - User-message rows only, with each row showing visible assistant steps and hidden context count.
124
+ - A `Summary` view with the agent thinking/response sequence.
125
+ - Collapsed hidden-context previews that expand to plain text system prompt or hidden prompt-like content.
126
+ - A dark/light theme switcher.
82
127
 
83
128
  ## Common Commands
84
129
 
85
130
  List recent raw captures:
86
131
 
87
132
  ```bash
88
- npx opencode-insights recent --limit 20
133
+ opencode-insights recent --limit 20
89
134
  ```
90
135
 
91
136
  List reconstructed sessions:
92
137
 
93
138
  ```bash
94
- npx opencode-insights sessions --limit 5000
139
+ opencode-insights sessions --limit 5000
95
140
  ```
96
141
 
97
142
  Print one reconstructed session:
98
143
 
99
144
  ```bash
100
- npx opencode-insights show ses_xxx --limit 10000
145
+ opencode-insights show ses_xxx --limit 10000
101
146
  ```
102
147
 
103
148
  Export one session to JSON:
104
149
 
105
150
  ```bash
106
- npx opencode-insights export ses_xxx --limit 10000 --output ./session.json
151
+ opencode-insights export ses_xxx --limit 10000 --output ./session.json
107
152
  ```
108
153
 
109
154
  Check DB path, table health, row counts, and SQLite readability:
110
155
 
111
156
  ```bash
112
- npx opencode-insights doctor
157
+ opencode-insights doctor
113
158
  ```
114
159
 
115
160
  Compact the local SQLite DB after heavy testing:
116
161
 
117
162
  ```bash
118
- npx opencode-insights vacuum
163
+ opencode-insights vacuum
119
164
  ```
120
165
 
121
166
  Remove plugin config entries and delete local captured data:
122
167
 
123
168
  ```bash
124
- npx opencode-insights uninstall
169
+ opencode-insights uninstall
170
+ ```
171
+
172
+ If the command is not available, confirm `~/.local/bin` is on `PATH`, or run the installed binary directly from OpenCode's package cache:
173
+
174
+ ```bash
175
+ ~/.cache/opencode/packages/node_modules/.bin/opencode-insights doctor
125
176
  ```
126
177
 
127
- If the command is not available through `npx`, run the installed binary directly from your OpenCode config directory:
178
+ You can also run the published package through npm without relying on the OpenCode cache:
128
179
 
129
180
  ```bash
130
- ./node_modules/.bin/opencode-insights doctor
181
+ npx -y -p @rejacky/opencode-insights opencode-insights doctor
131
182
  ```
132
183
 
133
184
  ## Storage
@@ -144,7 +195,9 @@ If SQLite is unavailable in the plugin runtime, the fallback path is:
144
195
  ~/.opencode-insights/insights.sqlite.jsonl
145
196
  ```
146
197
 
147
- You can override storage in `opencode.json` or `opencode.jsonc`:
198
+ The database keeps one day of captures by default and auto-cleans older rows on startup and after new captures. Set `retentionDays` to another number of days, or `0` to disable auto-cleaning.
199
+
200
+ You can override storage and retention in `opencode.json` or `opencode.jsonc`:
148
201
 
149
202
  ```json
150
203
  {
@@ -152,7 +205,8 @@ You can override storage in `opencode.json` or `opencode.jsonc`:
152
205
  [
153
206
  "@rejacky/opencode-insights",
154
207
  {
155
- "dbPath": "/absolute/path/to/insights.sqlite"
208
+ "dbPath": "/absolute/path/to/insights.sqlite",
209
+ "retentionDays": 1
156
210
  }
157
211
  ]
158
212
  ]
@@ -164,71 +218,3 @@ You can override storage in `opencode.json` or `opencode.jsonc`:
164
218
  This plugin intentionally does not redact anything. It stores data locally exactly as OpenCode exposes it to plugin hooks and events.
165
219
 
166
220
  Captured data can include prompts, system messages, provider metadata, API keys exposed inside hook payloads, tool arguments, headers, reasoning text, and response events. Use it only on machines where local full-fidelity capture is acceptable.
167
-
168
- ## Experimental Features
169
-
170
- The request-interception hooks (`chat.headers`, `experimental.chat.messages.transform`, `experimental.chat.system.transform`) are disabled by default. They capture additional request context (HTTP headers, transformed messages, system prompts) but are not yet mature.
171
-
172
- To enable them, pass `experimental: true` in the plugin options:
173
-
174
- ```json
175
- {
176
- "plugin": [
177
- [
178
- "@rejacky/opencode-insights",
179
- {
180
- "experimental": true
181
- }
182
- ]
183
- ]
184
- }
185
- ```
186
-
187
- ## Captured Hooks
188
-
189
- The viewer labels OpenCode hook records as `HOOK` because they are not raw HTTP requests.
190
-
191
- Common hook rows:
192
-
193
- - `HOOK title`: OpenCode title-generation model call, usually only on the first turn.
194
- - `HOOK build`: Main assistant response model-call hook.
195
- - `HOOK messages.transform`: Final conversation messages OpenCode prepared before model execution.
196
- - `HOOK system.transform`: System prompt strings OpenCode prepared before model execution.
197
-
198
- Hook payload meaning:
199
-
200
- - `payload.input`: Context OpenCode passed into the plugin hook.
201
- - `payload.output`: Value returned by the hook, such as model settings or transformed messages.
202
- - `headers.output.headers`: Headers returned by the headers hook.
203
- - Response text is captured from OpenCode event stream rows such as `message.part.delta` and `message.part.updated`.
204
-
205
- The plugin reconstructs a logical LLM request from hooks and events. It does not capture the final provider HTTP body unless OpenCode exposes a lower-level transport hook in the future.
206
-
207
- ## SQLite Queries
208
-
209
- Count captured rows:
210
-
211
- ```bash
212
- sqlite3 ~/.opencode-insights/insights.sqlite \
213
- "select kind, count(*) from captures group by kind order by kind;"
214
- ```
215
-
216
- Find text in captured payloads:
217
-
218
- ```bash
219
- sqlite3 ~/.opencode-insights/insights.sqlite "
220
- select datetime(timestamp/1000,'unixepoch','localtime') as time,
221
- kind,
222
- session_id,
223
- message_id,
224
- substr(payload_json, 1, 1200) as preview
225
- from captures
226
- where payload_json like '%search text%'
227
- order by timestamp desc
228
- limit 20;
229
- "
230
- ```
231
-
232
- ## Development
233
-
234
- Development and publish notes live in [DEVELOPMENT.md](./DEVELOPMENT.md).
@@ -17,9 +17,7 @@ type CaptureStore = {
17
17
  type InsightsOptions = {
18
18
  dataDir?: unknown;
19
19
  dbPath?: unknown;
20
- /** Enable experimental request capture (chat.headers, messages/system transforms).
21
- * Disabled by default — these hooks intercept request data and are not yet mature. */
22
- experimental?: boolean;
20
+ retentionDays?: unknown;
23
21
  };
24
22
  declare function defaultDataDir(): string;
25
23
  declare function resolveCapturePath(options?: InsightsOptions): string;
@@ -32,20 +30,32 @@ declare function normalizeEventCapture(event: unknown, timestamp?: number): Capt
32
30
  declare function normalizeToolCapture(kind: "tool.execute.before" | "tool.execute.after", input: unknown, output: unknown, timestamp?: number): CaptureRecord;
33
31
  declare class JsonlCaptureStore implements CaptureStore {
34
32
  private readonly path;
35
- constructor(path: string);
33
+ private readonly retentionMs;
34
+ constructor(path: string, retentionMs?: number | undefined);
36
35
  initialize(): Promise<void>;
37
36
  append(record: CaptureRecord): Promise<void>;
37
+ private pruneExpired;
38
38
  }
39
+ interface SqliteDb {
40
+ all(sql: string, ...params: unknown[]): Record<string, unknown>[];
41
+ run(sql: string, ...params: unknown[]): void;
42
+ sync(): void;
43
+ close(): void;
44
+ }
45
+ declare function openDatabase(path: string): Promise<SqliteDb | undefined>;
46
+ declare function extractEventType(payload: Record<string, unknown>): string | null;
39
47
  declare class SqliteCaptureStore implements CaptureStore {
40
48
  private readonly path;
49
+ private readonly retentionMs;
41
50
  private db;
42
- private unavailable;
43
- constructor(path: string);
51
+ private fallbackStore;
52
+ constructor(path: string, retentionMs?: number | undefined);
44
53
  initialize(): Promise<void>;
45
54
  append(record: CaptureRecord): Promise<void>;
46
55
  close(): Promise<void>;
47
- private database;
56
+ private pruneExpired;
48
57
  }
49
58
  declare function createCaptureStore(options?: InsightsOptions): CaptureStore;
59
+ declare function resolveRetentionDays(value: unknown): number;
50
60
 
51
- export { type CaptureRecord as C, type InsightsOptions as I, JsonlCaptureStore as J, SqliteCaptureStore as S, type CaptureKind as a, type CaptureStore as b, createCaptureStore as c, defaultDataDir as d, normalizeChatMessageCapture as e, normalizeChatParamsCapture as f, normalizeEventCapture as g, normalizeExperimentalChatMessagesTransformCapture as h, normalizeExperimentalChatSystemTransformCapture as i, normalizeToolCapture as j, normalizeChatHeadersCapture as n, resolveCapturePath as r };
61
+ export { type CaptureRecord as C, type InsightsOptions as I, JsonlCaptureStore as J, SqliteCaptureStore as S, type CaptureKind as a, type CaptureStore as b, type SqliteDb as c, createCaptureStore as d, defaultDataDir as e, extractEventType as f, normalizeChatMessageCapture as g, normalizeChatParamsCapture as h, normalizeEventCapture as i, normalizeExperimentalChatMessagesTransformCapture as j, normalizeExperimentalChatSystemTransformCapture as k, normalizeToolCapture as l, resolveRetentionDays as m, normalizeChatHeadersCapture as n, openDatabase as o, resolveCapturePath as r };
@@ -1,7 +1,10 @@
1
1
  // src/capture.ts
2
- import { mkdir, appendFile, writeFile } from "fs/promises";
2
+ import { mkdir, appendFile, readFile, writeFile } from "fs/promises";
3
+ import { existsSync } from "fs";
3
4
  import { dirname, join } from "path";
4
5
  import { homedir } from "os";
6
+ var DEFAULT_RETENTION_DAYS = 1;
7
+ var DAY_MS = 24 * 60 * 60 * 1e3;
5
8
  var sequence = 0;
6
9
  function nextID(timestamp) {
7
10
  sequence += 1;
@@ -100,13 +103,14 @@ function normalizeChatHeadersCapture(input, output, timestamp = Date.now()) {
100
103
  });
101
104
  }
102
105
  function normalizeExperimentalChatMessagesTransformCapture(input, output, timestamp = Date.now()) {
106
+ const inputRecord = isRecord(input) ? input : {};
103
107
  const latestUserMessage = latestUserTransformedMessage(output);
104
108
  const info = infoFromTransformedMessage(latestUserMessage);
105
109
  return captureRecord({
106
110
  id: nextID(timestamp),
107
111
  kind: "experimental.chat.messages.transform",
108
112
  timestamp,
109
- sessionID: sessionIDFrom(info),
113
+ sessionID: sessionIDFrom(inputRecord),
110
114
  messageID: messageIDFrom(info) ?? optionalString(info?.id),
111
115
  payload: { input, output }
112
116
  });
@@ -149,45 +153,168 @@ function normalizeToolCapture(kind, input, output, timestamp = Date.now()) {
149
153
  });
150
154
  }
151
155
  var JsonlCaptureStore = class {
152
- constructor(path) {
156
+ constructor(path, retentionMs = retentionMsFromDays(DEFAULT_RETENTION_DAYS)) {
153
157
  this.path = path;
158
+ this.retentionMs = retentionMs;
154
159
  }
155
160
  path;
161
+ retentionMs;
156
162
  async initialize() {
157
163
  await mkdir(dirname(this.path), { recursive: true });
158
164
  await writeFile(this.path, "", { flag: "a" });
165
+ await this.pruneExpired();
159
166
  }
160
167
  async append(record) {
161
168
  await mkdir(dirname(this.path), { recursive: true });
162
169
  await appendFile(this.path, `${JSON.stringify(record)}
163
170
  `, "utf8");
171
+ await this.pruneExpired();
172
+ }
173
+ async pruneExpired(now = Date.now()) {
174
+ const cutoff = retentionCutoff(now, this.retentionMs);
175
+ if (cutoff === void 0 || !existsSync(this.path)) return;
176
+ try {
177
+ const lines = (await readFile(this.path, "utf8")).split(/\r?\n/);
178
+ const kept = lines.filter((line) => {
179
+ const trimmed = line.trim();
180
+ if (!trimmed) return false;
181
+ try {
182
+ const record = JSON.parse(trimmed);
183
+ return typeof record.timestamp !== "number" || record.timestamp >= cutoff;
184
+ } catch {
185
+ return true;
186
+ }
187
+ });
188
+ await writeFile(this.path, kept.length ? `${kept.join("\n")}
189
+ ` : "", "utf8");
190
+ } catch {
191
+ }
164
192
  }
165
193
  };
194
+ async function openDatabase(path) {
195
+ try {
196
+ const mod = await import("bun:sqlite").catch(() => void 0);
197
+ if (mod) {
198
+ const db = new mod.Database(path);
199
+ return {
200
+ all(sql, ...params) {
201
+ return db.query(sql).all(...params);
202
+ },
203
+ run(sql, ...params) {
204
+ db.run(sql, ...params);
205
+ },
206
+ sync() {
207
+ },
208
+ close() {
209
+ db.close();
210
+ }
211
+ };
212
+ }
213
+ } catch {
214
+ }
215
+ try {
216
+ const mod = await import("better-sqlite3").catch(() => void 0);
217
+ const Database = mod?.default;
218
+ if (Database) {
219
+ const db = new Database(path);
220
+ return {
221
+ all(sql, ...params) {
222
+ return db.prepare(sql).all(...params);
223
+ },
224
+ run(sql, ...params) {
225
+ db.prepare(sql).run(...params);
226
+ },
227
+ sync() {
228
+ },
229
+ close() {
230
+ db.close();
231
+ }
232
+ };
233
+ }
234
+ } catch {
235
+ }
236
+ return void 0;
237
+ }
238
+ function extractEventType(payload) {
239
+ const event = isRecord(payload.event) ? payload.event : {};
240
+ return typeof event.type === "string" ? event.type : null;
241
+ }
166
242
  var SqliteCaptureStore = class {
167
- constructor(path) {
243
+ constructor(path, retentionMs = retentionMsFromDays(DEFAULT_RETENTION_DAYS)) {
168
244
  this.path = path;
245
+ this.retentionMs = retentionMs;
169
246
  }
170
247
  path;
248
+ retentionMs;
171
249
  db;
172
- unavailable = false;
250
+ fallbackStore;
173
251
  async initialize() {
174
- const db = await this.database();
252
+ await mkdir(dirname(this.path), { recursive: true });
253
+ const db = await openDatabase(this.path);
175
254
  if (!db) {
176
255
  const fallbackPath = this.path.endsWith(".sqlite") ? `${this.path}.jsonl` : this.path;
177
- await new JsonlCaptureStore(fallbackPath).initialize();
256
+ this.fallbackStore = new JsonlCaptureStore(fallbackPath, this.retentionMs);
257
+ await this.fallbackStore.initialize();
258
+ return;
259
+ }
260
+ this.db = db;
261
+ this.db.run(
262
+ `create table if not exists captures (
263
+ id text primary key,
264
+ kind text not null,
265
+ timestamp integer not null,
266
+ session_id text,
267
+ message_id text,
268
+ provider_id text,
269
+ model_id text,
270
+ event_type text,
271
+ payload_json text not null
272
+ )`
273
+ );
274
+ this.db.run(`create index if not exists captures_timestamp_idx on captures(timestamp)`);
275
+ this.db.run(`create index if not exists captures_session_idx on captures(session_id)`);
276
+ this.db.run(`create index if not exists captures_kind_timestamp_idx on captures(kind, timestamp)`);
277
+ this.db.run(`create index if not exists captures_kind_type_ts_idx on captures(kind, event_type, timestamp)`);
278
+ const existingColumns = db.all("select name from pragma_table_info('captures') where name = 'event_type'");
279
+ if (existingColumns.length === 0) {
280
+ try {
281
+ db.run(`alter table captures add column event_type text`);
282
+ } catch {
283
+ }
284
+ }
285
+ try {
286
+ db.run(`update captures set event_type = json_extract(payload_json, '$.event.type') where kind = 'event' and event_type is null`);
287
+ } catch {
178
288
  }
289
+ try {
290
+ db.run(`pragma journal_mode = wal`);
291
+ } catch {
292
+ }
293
+ this.pruneExpired();
294
+ this.db.sync();
179
295
  }
180
296
  async append(record) {
181
- const db = await this.database();
182
- if (!db) {
183
- const fallbackPath = this.path.endsWith(".sqlite") ? `${this.path}.jsonl` : this.path;
184
- await new JsonlCaptureStore(fallbackPath).append(record);
297
+ if (this.fallbackStore) {
298
+ await this.fallbackStore.append(record);
185
299
  return;
186
300
  }
187
- db.run(
301
+ if (!this.db) {
302
+ await mkdir(dirname(this.path), { recursive: true });
303
+ const db = await openDatabase(this.path);
304
+ if (!db) {
305
+ const fallbackPath = this.path.endsWith(".sqlite") ? `${this.path}.jsonl` : this.path;
306
+ this.fallbackStore = new JsonlCaptureStore(fallbackPath, this.retentionMs);
307
+ await this.fallbackStore.initialize();
308
+ await this.fallbackStore.append(record);
309
+ return;
310
+ }
311
+ this.db = db;
312
+ }
313
+ const eventType = record.kind === "event" ? extractEventType(record.payload) : null;
314
+ this.db.run(
188
315
  `insert into captures (
189
- id, kind, timestamp, session_id, message_id, provider_id, model_id, payload_json
190
- ) values (?, ?, ?, ?, ?, ?, ?, ?)`,
316
+ id, kind, timestamp, session_id, message_id, provider_id, model_id, event_type, payload_json
317
+ ) values (?, ?, ?, ?, ?, ?, ?, ?, ?)`,
191
318
  record.id,
192
319
  record.kind,
193
320
  record.timestamp,
@@ -195,49 +322,37 @@ var SqliteCaptureStore = class {
195
322
  record.messageID ?? null,
196
323
  record.providerID ?? null,
197
324
  record.modelID ?? null,
325
+ eventType,
198
326
  JSON.stringify(record.payload)
199
327
  );
328
+ this.pruneExpired();
329
+ this.db.sync();
200
330
  }
201
331
  async close() {
202
- this.db?.close?.();
332
+ this.db?.close();
203
333
  this.db = void 0;
334
+ this.fallbackStore = void 0;
204
335
  }
205
- async database() {
206
- if (this.db) return this.db;
207
- if (this.unavailable) return void 0;
208
- try {
209
- await mkdir(dirname(this.path), { recursive: true });
210
- const mod = await import("bun:sqlite").catch(() => void 0);
211
- if (!mod) {
212
- this.unavailable = true;
213
- return void 0;
214
- }
215
- const db = new mod.Database(this.path);
216
- initializeSchema(db);
217
- this.db = db;
218
- return db;
219
- } catch {
220
- this.unavailable = true;
221
- return void 0;
222
- }
336
+ pruneExpired(now = Date.now()) {
337
+ const cutoff = retentionCutoff(now, this.retentionMs);
338
+ if (cutoff === void 0) return;
339
+ this.db?.run("delete from captures where timestamp < ?", cutoff);
223
340
  }
224
341
  };
225
- function initializeSchema(db) {
226
- db.run(`create table if not exists captures (
227
- id text primary key,
228
- kind text not null,
229
- timestamp integer not null,
230
- session_id text,
231
- message_id text,
232
- provider_id text,
233
- model_id text,
234
- payload_json text not null
235
- )`);
236
- db.run(`create index if not exists captures_timestamp_idx on captures(timestamp)`);
237
- db.run(`create index if not exists captures_session_idx on captures(session_id)`);
238
- }
239
342
  function createCaptureStore(options = {}) {
240
- return new SqliteCaptureStore(resolveCapturePath(options));
343
+ return new SqliteCaptureStore(resolveCapturePath(options), retentionMsFromDays(resolveRetentionDays(options.retentionDays)));
344
+ }
345
+ function resolveRetentionDays(value) {
346
+ if (value === void 0 || value === null || value === "") return DEFAULT_RETENTION_DAYS;
347
+ const numeric = typeof value === "number" ? value : typeof value === "string" ? Number.parseFloat(value) : Number.NaN;
348
+ if (!Number.isFinite(numeric) || numeric < 0) return DEFAULT_RETENTION_DAYS;
349
+ return numeric;
350
+ }
351
+ function retentionMsFromDays(days) {
352
+ return days <= 0 ? void 0 : days * DAY_MS;
353
+ }
354
+ function retentionCutoff(now, retentionMs) {
355
+ return retentionMs === void 0 ? void 0 : now - retentionMs;
241
356
  }
242
357
 
243
358
  export {
@@ -251,6 +366,9 @@ export {
251
366
  normalizeEventCapture,
252
367
  normalizeToolCapture,
253
368
  JsonlCaptureStore,
369
+ openDatabase,
370
+ extractEventType,
254
371
  SqliteCaptureStore,
255
- createCaptureStore
372
+ createCaptureStore,
373
+ resolveRetentionDays
256
374
  };
package/dist/cli.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { C as CaptureRecord } from './capture-DkASdFpu.js';
2
+ import { C as CaptureRecord } from './capture-Dy799i3Z.js';
3
3
 
4
4
  type HistoryMessage = {
5
5
  id: string;
@@ -22,6 +22,7 @@ type HistoryRequest = {
22
22
  modelID?: string | undefined;
23
23
  summary: string;
24
24
  payload: Record<string, unknown>;
25
+ system?: HistoryRequestHeaders | undefined;
25
26
  headers?: HistoryRequestHeaders | undefined;
26
27
  response?: HistoryResponse | undefined;
27
28
  };
@@ -46,8 +47,12 @@ type HistoryResponse = {
46
47
  };
47
48
  type HistorySession = {
48
49
  id: string;
50
+ parentID?: string | undefined;
49
51
  title?: string | undefined;
50
52
  updatedAt?: number | undefined;
53
+ cwd?: string | undefined;
54
+ root?: string | undefined;
55
+ project?: string | undefined;
51
56
  messages: HistoryMessage[];
52
57
  requests: HistoryRequest[];
53
58
  };
@@ -67,6 +72,7 @@ type CliOptions = {
67
72
  port?: number | undefined;
68
73
  output?: string | undefined;
69
74
  configDir?: string | undefined;
75
+ retentionDays?: number | undefined;
70
76
  dryRun: boolean;
71
77
  keepData: boolean;
72
78
  };