@rejacky/opencode-insights 0.1.5 → 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 +31 -59
- package/dist/{capture-B6sM1QQA.d.ts → capture-Dy799i3Z.d.ts} +2 -1
- package/dist/{chunk-PBX4AJCR.js → chunk-XCHFXQIL.js} +26 -2
- package/dist/cli.d.ts +1 -1
- package/dist/cli.js +28 -17
- package/dist/index.d.ts +1 -1
- package/dist/index.js +3 -1
- package/package.json +1 -1
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
|
+

|
|
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
|
+

|
|
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).
|
|
@@ -43,6 +43,7 @@ interface SqliteDb {
|
|
|
43
43
|
close(): void;
|
|
44
44
|
}
|
|
45
45
|
declare function openDatabase(path: string): 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,
|
|
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 };
|
|
@@ -235,6 +235,10 @@ async function openDatabase(path) {
|
|
|
235
235
|
}
|
|
236
236
|
return void 0;
|
|
237
237
|
}
|
|
238
|
+
function extractEventType(payload) {
|
|
239
|
+
const event = isRecord(payload.event) ? payload.event : {};
|
|
240
|
+
return typeof event.type === "string" ? event.type : null;
|
|
241
|
+
}
|
|
238
242
|
var SqliteCaptureStore = class {
|
|
239
243
|
constructor(path, retentionMs = retentionMsFromDays(DEFAULT_RETENTION_DAYS)) {
|
|
240
244
|
this.path = path;
|
|
@@ -263,12 +267,29 @@ var SqliteCaptureStore = class {
|
|
|
263
267
|
message_id text,
|
|
264
268
|
provider_id text,
|
|
265
269
|
model_id text,
|
|
270
|
+
event_type text,
|
|
266
271
|
payload_json text not null
|
|
267
272
|
)`
|
|
268
273
|
);
|
|
269
274
|
this.db.run(`create index if not exists captures_timestamp_idx on captures(timestamp)`);
|
|
270
275
|
this.db.run(`create index if not exists captures_session_idx on captures(session_id)`);
|
|
271
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 {
|
|
288
|
+
}
|
|
289
|
+
try {
|
|
290
|
+
db.run(`pragma journal_mode = wal`);
|
|
291
|
+
} catch {
|
|
292
|
+
}
|
|
272
293
|
this.pruneExpired();
|
|
273
294
|
this.db.sync();
|
|
274
295
|
}
|
|
@@ -289,10 +310,11 @@ var SqliteCaptureStore = class {
|
|
|
289
310
|
}
|
|
290
311
|
this.db = db;
|
|
291
312
|
}
|
|
313
|
+
const eventType = record.kind === "event" ? extractEventType(record.payload) : null;
|
|
292
314
|
this.db.run(
|
|
293
315
|
`insert into captures (
|
|
294
|
-
id, kind, timestamp, session_id, message_id, provider_id, model_id, payload_json
|
|
295
|
-
) values (?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
316
|
+
id, kind, timestamp, session_id, message_id, provider_id, model_id, event_type, payload_json
|
|
317
|
+
) values (?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
296
318
|
record.id,
|
|
297
319
|
record.kind,
|
|
298
320
|
record.timestamp,
|
|
@@ -300,6 +322,7 @@ var SqliteCaptureStore = class {
|
|
|
300
322
|
record.messageID ?? null,
|
|
301
323
|
record.providerID ?? null,
|
|
302
324
|
record.modelID ?? null,
|
|
325
|
+
eventType,
|
|
303
326
|
JSON.stringify(record.payload)
|
|
304
327
|
);
|
|
305
328
|
this.pruneExpired();
|
|
@@ -344,6 +367,7 @@ export {
|
|
|
344
367
|
normalizeToolCapture,
|
|
345
368
|
JsonlCaptureStore,
|
|
346
369
|
openDatabase,
|
|
370
|
+
extractEventType,
|
|
347
371
|
SqliteCaptureStore,
|
|
348
372
|
createCaptureStore,
|
|
349
373
|
resolveRetentionDays
|
package/dist/cli.d.ts
CHANGED
package/dist/cli.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import {
|
|
3
3
|
openDatabase,
|
|
4
4
|
resolveCapturePath
|
|
5
|
-
} from "./chunk-
|
|
5
|
+
} from "./chunk-XCHFXQIL.js";
|
|
6
6
|
|
|
7
7
|
// src/cli.ts
|
|
8
8
|
import { execFile as execFile2 } from "child_process";
|
|
@@ -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
272
|
const db = await openDatabase(path);
|
|
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
|
|
278
|
-
if (!existsSync(path)) return void 0;
|
|
285
|
+
async function runSqlite3Json(path, sql) {
|
|
279
286
|
try {
|
|
280
|
-
const { stdout } = await execFileAsync("sqlite3", ["-json", path,
|
|
287
|
+
const { stdout } = await execFileAsync("sqlite3", ["-json", path, sql], {
|
|
281
288
|
maxBuffer: 128 * 1024 * 1024
|
|
282
289
|
});
|
|
283
|
-
|
|
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
306
|
const db = await openDatabase(path);
|
|
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
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
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
|
|
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
|
|
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,
|
|
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-Dy799i3Z.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-
|
|
33
|
+
} from "./chunk-XCHFXQIL.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.
|
|
4
|
+
"version": "0.1.6",
|
|
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",
|