@rejacky/opencode-insights 0.1.5 → 0.1.8

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
@@ -24,6 +24,37 @@ Make sure that directory is on your `PATH`, then run the CLI directly:
24
24
  opencode-insights doctor
25
25
  ```
26
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
+
27
58
  ## Uninstall
28
59
 
29
60
  Remove this plugin from `opencode.json` / `opencode.jsonc`, remove it from `tui.json`, and delete the local Insights database files:
@@ -56,12 +87,6 @@ After uninstalling, restart OpenCode. Packages installed with `opencode plugin .
56
87
  ~/.cache/opencode/packages
57
88
  ```
58
89
 
59
- For this machine, that expands to:
60
-
61
- ```text
62
- /Users/zyao/.cache/opencode/packages
63
- ```
64
-
65
90
  The `uninstall` command removes plugin config entries and local Insights data; it does not remove cached OpenCode package directories automatically.
66
91
 
67
92
  ## What You Get
@@ -193,56 +218,3 @@ You can override storage and retention in `opencode.json` or `opencode.jsonc`:
193
218
  This plugin intentionally does not redact anything. It stores data locally exactly as OpenCode exposes it to plugin hooks and events.
194
219
 
195
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.
196
-
197
- ## Request Context Capture
198
-
199
- Request-context capture is enabled by default. The plugin records OpenCode's `chat.headers`, `experimental.chat.messages.transform`, and `experimental.chat.system.transform` hooks so the viewer can show provider headers, transformed conversation messages, and system prompt content when OpenCode exposes them. The `experimental.chat.*` names are OpenCode hook names; no `experimental` plugin option is required.
200
-
201
- ## Captured Hooks
202
-
203
- The viewer labels OpenCode hook records as `HOOK` because they are not raw HTTP requests.
204
-
205
- Common hook rows:
206
-
207
- - `HOOK title`: OpenCode title-generation model call, usually only on the first turn.
208
- - `HOOK build`: Main assistant response model-call hook.
209
- - `HOOK messages.transform`: Final conversation messages OpenCode prepared before model execution.
210
- - `HOOK system.transform`: System prompt strings OpenCode prepared before model execution.
211
-
212
- Hook payload meaning:
213
-
214
- - `payload.input`: Context OpenCode passed into the plugin hook.
215
- - `payload.output`: Value returned by the hook, such as model settings or transformed messages.
216
- - `headers.output.headers`: Headers returned by the headers hook.
217
- - Response text is captured from OpenCode event stream rows such as `message.part.delta` and `message.part.updated`.
218
-
219
- 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.
220
-
221
- ## SQLite Queries
222
-
223
- Count captured rows:
224
-
225
- ```bash
226
- sqlite3 ~/.opencode-insights/insights.sqlite \
227
- "select kind, count(*) from captures group by kind order by kind;"
228
- ```
229
-
230
- Find text in captured payloads:
231
-
232
- ```bash
233
- sqlite3 ~/.opencode-insights/insights.sqlite "
234
- select datetime(timestamp/1000,'unixepoch','localtime') as time,
235
- kind,
236
- session_id,
237
- message_id,
238
- substr(payload_json, 1, 1200) as preview
239
- from captures
240
- where payload_json like '%search text%'
241
- order by timestamp desc
242
- limit 20;
243
- "
244
- ```
245
-
246
- ## Development
247
-
248
- Development and publish notes live in [DEVELOPMENT.md](./DEVELOPMENT.md).
@@ -42,7 +42,8 @@ interface SqliteDb {
42
42
  sync(): void;
43
43
  close(): void;
44
44
  }
45
- declare function openDatabase(path: string): Promise<SqliteDb | undefined>;
45
+ declare function openDatabase(path: string, readonly?: boolean): Promise<SqliteDb | undefined>;
46
+ declare function extractEventType(payload: Record<string, unknown>): string | null;
46
47
  declare class SqliteCaptureStore implements CaptureStore {
47
48
  private readonly path;
48
49
  private readonly retentionMs;
@@ -57,4 +58,4 @@ declare class SqliteCaptureStore implements CaptureStore {
57
58
  declare function createCaptureStore(options?: InsightsOptions): CaptureStore;
58
59
  declare function resolveRetentionDays(value: unknown): number;
59
60
 
60
- 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, normalizeChatMessageCapture as f, normalizeChatParamsCapture as g, normalizeEventCapture as h, normalizeExperimentalChatMessagesTransformCapture as i, normalizeExperimentalChatSystemTransformCapture as j, normalizeToolCapture as k, resolveRetentionDays as l, normalizeChatHeadersCapture as n, openDatabase as o, 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,6 +1,6 @@
1
1
  // src/capture.ts
2
2
  import { mkdir, appendFile, readFile, writeFile } from "fs/promises";
3
- import { existsSync } from "fs";
3
+ import { existsSync, readFileSync } from "fs";
4
4
  import { dirname, join } from "path";
5
5
  import { homedir } from "os";
6
6
  var DEFAULT_RETENTION_DAYS = 1;
@@ -191,7 +191,7 @@ var JsonlCaptureStore = class {
191
191
  }
192
192
  }
193
193
  };
194
- async function openDatabase(path) {
194
+ async function openDatabase(path, readonly = false) {
195
195
  try {
196
196
  const mod = await import("bun:sqlite").catch(() => void 0);
197
197
  if (mod) {
@@ -233,8 +233,40 @@ async function openDatabase(path) {
233
233
  }
234
234
  } catch {
235
235
  }
236
+ if (readonly) {
237
+ try {
238
+ const initSqlJs = await import("sql.js").catch(() => void 0);
239
+ if (initSqlJs) {
240
+ const SQL = await initSqlJs.default();
241
+ const data = readFileSync(path);
242
+ const db = new SQL.Database(data);
243
+ return {
244
+ all(sql, ...params) {
245
+ const stmt = db.prepare(sql);
246
+ if (params.length > 0) stmt.bind(params);
247
+ const rows = [];
248
+ while (stmt.step()) rows.push(stmt.getAsObject());
249
+ stmt.free();
250
+ return rows;
251
+ },
252
+ run() {
253
+ },
254
+ sync() {
255
+ },
256
+ close() {
257
+ db.close();
258
+ }
259
+ };
260
+ }
261
+ } catch {
262
+ }
263
+ }
236
264
  return void 0;
237
265
  }
266
+ function extractEventType(payload) {
267
+ const event = isRecord(payload.event) ? payload.event : {};
268
+ return typeof event.type === "string" ? event.type : null;
269
+ }
238
270
  var SqliteCaptureStore = class {
239
271
  constructor(path, retentionMs = retentionMsFromDays(DEFAULT_RETENTION_DAYS)) {
240
272
  this.path = path;
@@ -263,12 +295,29 @@ var SqliteCaptureStore = class {
263
295
  message_id text,
264
296
  provider_id text,
265
297
  model_id text,
298
+ event_type text,
266
299
  payload_json text not null
267
300
  )`
268
301
  );
269
302
  this.db.run(`create index if not exists captures_timestamp_idx on captures(timestamp)`);
270
303
  this.db.run(`create index if not exists captures_session_idx on captures(session_id)`);
271
304
  this.db.run(`create index if not exists captures_kind_timestamp_idx on captures(kind, timestamp)`);
305
+ this.db.run(`create index if not exists captures_kind_type_ts_idx on captures(kind, event_type, timestamp)`);
306
+ const existingColumns = db.all("select name from pragma_table_info('captures') where name = 'event_type'");
307
+ if (existingColumns.length === 0) {
308
+ try {
309
+ db.run(`alter table captures add column event_type text`);
310
+ } catch {
311
+ }
312
+ }
313
+ try {
314
+ db.run(`update captures set event_type = json_extract(payload_json, '$.event.type') where kind = 'event' and event_type is null`);
315
+ } catch {
316
+ }
317
+ try {
318
+ db.run(`pragma journal_mode = wal`);
319
+ } catch {
320
+ }
272
321
  this.pruneExpired();
273
322
  this.db.sync();
274
323
  }
@@ -289,10 +338,11 @@ var SqliteCaptureStore = class {
289
338
  }
290
339
  this.db = db;
291
340
  }
341
+ const eventType = record.kind === "event" ? extractEventType(record.payload) : null;
292
342
  this.db.run(
293
343
  `insert into captures (
294
- id, kind, timestamp, session_id, message_id, provider_id, model_id, payload_json
295
- ) values (?, ?, ?, ?, ?, ?, ?, ?)`,
344
+ id, kind, timestamp, session_id, message_id, provider_id, model_id, event_type, payload_json
345
+ ) values (?, ?, ?, ?, ?, ?, ?, ?, ?)`,
296
346
  record.id,
297
347
  record.kind,
298
348
  record.timestamp,
@@ -300,6 +350,7 @@ var SqliteCaptureStore = class {
300
350
  record.messageID ?? null,
301
351
  record.providerID ?? null,
302
352
  record.modelID ?? null,
353
+ eventType,
303
354
  JSON.stringify(record.payload)
304
355
  );
305
356
  this.pruneExpired();
@@ -344,6 +395,7 @@ export {
344
395
  normalizeToolCapture,
345
396
  JsonlCaptureStore,
346
397
  openDatabase,
398
+ extractEventType,
347
399
  SqliteCaptureStore,
348
400
  createCaptureStore,
349
401
  resolveRetentionDays
package/dist/cli.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { C as CaptureRecord } from './capture-B6sM1QQA.js';
2
+ import { C as CaptureRecord } from './capture-BMWWI5GR.js';
3
3
 
4
4
  type HistoryMessage = {
5
5
  id: string;
package/dist/cli.js CHANGED
@@ -2,7 +2,7 @@
2
2
  import {
3
3
  openDatabase,
4
4
  resolveCapturePath
5
- } from "./chunk-PBX4AJCR.js";
5
+ } from "./chunk-FGTKNB7T.js";
6
6
 
7
7
  // src/cli.ts
8
8
  import { execFile as execFile2 } from "child_process";
@@ -62,7 +62,7 @@ async function readCaptureRecord(id, options = {}) {
62
62
  const dbPath = resolveCapturePath(options);
63
63
  if (!existsSync(dbPath)) return void 0;
64
64
  try {
65
- const db = await openDatabase(dbPath);
65
+ const db = await openDatabase(dbPath, true);
66
66
  if (db) {
67
67
  try {
68
68
  const rows = db.all("select id, kind, timestamp, session_id, message_id, provider_id, model_id, payload_json from captures where id = ?", id);
@@ -259,12 +259,20 @@ function buildRequestHistory(records) {
259
259
  requests: requests.sort((a, b) => b.timestamp - a.timestamp)
260
260
  };
261
261
  }
262
+ function ensureEventTypeColumn(db) {
263
+ const existing = db.all("select name from pragma_table_info('captures') where name = 'event_type'");
264
+ if (existing.length === 0) {
265
+ db.run(`alter table captures add column event_type text`);
266
+ }
267
+ db.run(`update captures set event_type = json_extract(payload_json, '$.event.type') where kind = 'event' and event_type is null`);
268
+ }
262
269
  async function readSqliteCaptures(path, limit) {
263
270
  if (!existsSync(path)) return void 0;
264
271
  try {
265
- const db = await openDatabase(path);
272
+ const db = await openDatabase(path, true);
266
273
  if (!db) return readSqliteCapturesWithCli(path, limit);
267
274
  try {
275
+ ensureEventTypeColumn(db);
268
276
  const rows = db.all(recentCaptureSql(Math.max(1, limit)));
269
277
  return dedupeRows(rows).map(rowToCapture);
270
278
  } finally {
@@ -274,24 +282,31 @@ async function readSqliteCaptures(path, limit) {
274
282
  return readSqliteCapturesWithCli(path, limit);
275
283
  }
276
284
  }
277
- async function readSqliteCapturesWithCli(path, limit) {
278
- if (!existsSync(path)) return void 0;
285
+ async function runSqlite3Json(path, sql) {
279
286
  try {
280
- const { stdout } = await execFileAsync("sqlite3", ["-json", path, recentCaptureSql(Math.max(1, Math.trunc(limit)))], {
287
+ const { stdout } = await execFileAsync("sqlite3", ["-json", path, sql], {
281
288
  maxBuffer: 128 * 1024 * 1024
282
289
  });
283
- if (!stdout.trim()) return [];
284
- return dedupeRows(JSON.parse(stdout)).map(rowToCapture);
290
+ return stdout;
285
291
  } catch {
286
292
  return void 0;
287
293
  }
288
294
  }
295
+ async function readSqliteCapturesWithCli(path, limit) {
296
+ if (!existsSync(path)) return void 0;
297
+ await runSqlite3Json(path, "alter table captures add column event_type text");
298
+ await runSqlite3Json(path, "update captures set event_type = json_extract(payload_json, '$.event.type') where kind = 'event' and event_type is null");
299
+ const stdout = await runSqlite3Json(path, recentCaptureSql(Math.max(1, Math.trunc(limit))));
300
+ if (!stdout?.trim()) return [];
301
+ return dedupeRows(JSON.parse(stdout)).map(rowToCapture);
302
+ }
289
303
  async function readSqliteViewerCaptures(path, limit) {
290
304
  if (!existsSync(path)) return void 0;
291
305
  try {
292
- const db = await openDatabase(path);
306
+ const db = await openDatabase(path, true);
293
307
  if (!db) return readSqliteViewerCapturesWithCli(path, limit);
294
308
  try {
309
+ ensureEventTypeColumn(db);
295
310
  const rows = db.all(viewerCaptureSql(Math.max(1, limit)));
296
311
  return dedupeRows(rows).map(rowToCapture);
297
312
  } finally {
@@ -303,15 +318,11 @@ async function readSqliteViewerCaptures(path, limit) {
303
318
  }
304
319
  async function readSqliteViewerCapturesWithCli(path, limit) {
305
320
  if (!existsSync(path)) return void 0;
306
- try {
307
- const { stdout } = await execFileAsync("sqlite3", ["-json", path, viewerCaptureSql(Math.max(1, Math.trunc(limit)))], {
308
- maxBuffer: 128 * 1024 * 1024
309
- });
310
- if (!stdout.trim()) return [];
311
- return dedupeRows(JSON.parse(stdout)).map(rowToCapture);
312
- } catch {
313
- return void 0;
314
- }
321
+ await runSqlite3Json(path, "alter table captures add column event_type text");
322
+ await runSqlite3Json(path, "update captures set event_type = json_extract(payload_json, '$.event.type') where kind = 'event' and event_type is null");
323
+ const stdout = await runSqlite3Json(path, viewerCaptureSql(Math.max(1, Math.trunc(limit))));
324
+ if (!stdout?.trim()) return [];
325
+ return dedupeRows(JSON.parse(stdout)).map(rowToCapture);
315
326
  }
316
327
  function recentCaptureSql(limit) {
317
328
  return `select id, kind, timestamp, session_id, message_id, provider_id, model_id, payload_json
@@ -331,7 +342,7 @@ function recentCaptureSql(limit) {
331
342
  or id in (
332
343
  select id from captures
333
344
  where kind = 'event'
334
- and json_extract(payload_json, '$.event.type') in (
345
+ and event_type in (
335
346
  'message.updated',
336
347
  'message.part.updated',
337
348
  'message.part.delta',
@@ -359,7 +370,7 @@ function viewerCaptureSql(limit) {
359
370
  or id in (
360
371
  select id from captures
361
372
  where kind = 'event'
362
- and json_extract(payload_json, '$.event.type') in (
373
+ and event_type in (
363
374
  'message.updated',
364
375
  'message.part.updated',
365
376
  'message.part.delta',
package/dist/index.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import { Plugin } from '@opencode-ai/plugin';
2
2
  import { TuiPlugin } from '@opencode-ai/plugin/tui';
3
- export { a as CaptureKind, C as CaptureRecord, b as CaptureStore, I as InsightsOptions, J as JsonlCaptureStore, S as SqliteCaptureStore, c as SqliteDb, d as createCaptureStore, e as defaultDataDir, n as normalizeChatHeadersCapture, f as normalizeChatMessageCapture, g as normalizeChatParamsCapture, h as normalizeEventCapture, i as normalizeExperimentalChatMessagesTransformCapture, j as normalizeExperimentalChatSystemTransformCapture, k as normalizeToolCapture, o as openDatabase, r as resolveCapturePath, l as resolveRetentionDays } from './capture-B6sM1QQA.js';
3
+ export { a as CaptureKind, C as CaptureRecord, b as CaptureStore, I as InsightsOptions, J as JsonlCaptureStore, S as SqliteCaptureStore, c as SqliteDb, d as createCaptureStore, e as defaultDataDir, f as extractEventType, n as normalizeChatHeadersCapture, g as normalizeChatMessageCapture, h as normalizeChatParamsCapture, i as normalizeEventCapture, j as normalizeExperimentalChatMessagesTransformCapture, k as normalizeExperimentalChatSystemTransformCapture, l as normalizeToolCapture, o as openDatabase, r as resolveCapturePath, m as resolveRetentionDays } from './capture-BMWWI5GR.js';
4
4
 
5
5
  type StreamSample = {
6
6
  at: number;
package/dist/index.js CHANGED
@@ -19,6 +19,7 @@ import {
19
19
  SqliteCaptureStore,
20
20
  createCaptureStore,
21
21
  defaultDataDir,
22
+ extractEventType,
22
23
  normalizeChatHeadersCapture,
23
24
  normalizeChatMessageCapture,
24
25
  normalizeChatParamsCapture,
@@ -29,7 +30,7 @@ import {
29
30
  openDatabase,
30
31
  resolveCapturePath,
31
32
  resolveRetentionDays
32
- } from "./chunk-PBX4AJCR.js";
33
+ } from "./chunk-FGTKNB7T.js";
33
34
 
34
35
  // src/cli-shim.ts
35
36
  import { existsSync } from "fs";
@@ -141,6 +142,7 @@ export {
141
142
  src_default as default,
142
143
  defaultDataDir,
143
144
  estimateStreamTokens,
145
+ extractEventType,
144
146
  getSubagentItems,
145
147
  getSubagentSidebarModel,
146
148
  id,
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "$schema": "https://json.schemastore.org/package.json",
3
3
  "name": "@rejacky/opencode-insights",
4
- "version": "0.1.5",
4
+ "version": "0.1.8",
5
5
  "description": "OpenCode plugin for local request capture, TPS metrics, and subagent status visibility.",
6
6
  "type": "module",
7
7
  "author": "opencode-insights contributors",
@@ -47,6 +47,7 @@
47
47
  "scripts": {
48
48
  "build": "tsup",
49
49
  "debug": "npm run build && node dist/cli.js debug",
50
+ "postinstall": "npm rebuild better-sqlite3",
50
51
  "test": "vitest run",
51
52
  "typecheck": "tsc --noEmit",
52
53
  "verify": "npm run typecheck && npm run test && npm run build",