@rejacky/opencode-insights 0.1.0

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/DEVELOPMENT.md ADDED
@@ -0,0 +1,97 @@
1
+ # Development
2
+
3
+ Notes for maintaining and publishing `opencode-insights`.
4
+
5
+ ## Local Testing
6
+
7
+ From this repo:
8
+
9
+ ```bash
10
+ cd /Users/zyao/Desktop/opencode-insights
11
+ npm install
12
+ npm run verify
13
+ npm pack
14
+ ```
15
+
16
+ Install the packed plugin from your OpenCode config/package directory:
17
+
18
+ ```bash
19
+ cd ~/.config/opencode
20
+ npm i /Users/zyao/Desktop/opencode-insights/rejacky-opencode-insights-0.1.0.tgz
21
+ npx opencode-insights configure
22
+ ```
23
+
24
+ For local repo development without relying on the installed package binary:
25
+
26
+ ```bash
27
+ node /Users/zyao/Desktop/opencode-insights/dist/cli.js configure --config-dir ~/.config/opencode
28
+ ```
29
+
30
+ Restart OpenCode after reinstalling or rebuilding. Existing sessions will not gain missing events retroactively, so create a new session to test capture changes.
31
+
32
+ ## Faster Local Iteration
33
+
34
+ Link instead of packing each time:
35
+
36
+ ```bash
37
+ cd /Users/zyao/Desktop/opencode-insights
38
+ npm link
39
+
40
+ cd ~/.config/opencode
41
+ npm link @rejacky/opencode-insights
42
+ ```
43
+
44
+ After code changes:
45
+
46
+ ```bash
47
+ cd /Users/zyao/Desktop/opencode-insights
48
+ npm run build
49
+ node /Users/zyao/Desktop/opencode-insights/dist/cli.js configure --config-dir ~/.config/opencode
50
+ ```
51
+
52
+ ## Verification
53
+
54
+ Run all checks:
55
+
56
+ ```bash
57
+ npm run verify
58
+ ```
59
+
60
+ Individual commands:
61
+
62
+ ```bash
63
+ npm run typecheck
64
+ npm test
65
+ npm run build
66
+ ```
67
+
68
+ ## Publish Checklist
69
+
70
+ Before publishing:
71
+
72
+ ```bash
73
+ npm run verify
74
+ npm pack --dry-run
75
+ ```
76
+
77
+ Review package contents:
78
+
79
+ ```bash
80
+ npm pack
81
+ tar -tf rejacky-opencode-insights-0.1.0.tgz
82
+ ```
83
+
84
+ Publish:
85
+
86
+ ```bash
87
+ npm login
88
+ npm publish --access public
89
+ ```
90
+
91
+ After publishing, users should be able to install from their OpenCode config/package directory with:
92
+
93
+ ```bash
94
+ cd ~/.config/opencode
95
+ npm i @rejacky/opencode-insights
96
+ npx opencode-insights configure
97
+ ```
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 opencode-insights contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,211 @@
1
+ # opencode-insights
2
+
3
+ Local OpenCode observability for live TPS, subagent status, and full-fidelity request/session inspection.
4
+
5
+ ## Install
6
+
7
+ Install from your OpenCode config/package directory:
8
+
9
+ ```bash
10
+ cd ~/.config/opencode
11
+ npm i @rejacky/opencode-insights
12
+ npx opencode-insights configure
13
+ ```
14
+
15
+ Restart OpenCode after configuring the plugin.
16
+
17
+ `opencode-insights configure` updates two config files because OpenCode loads server plugins and TUI plugins separately:
18
+
19
+ ```json
20
+ // ~/.config/opencode/opencode.json or opencode.jsonc
21
+ {
22
+ "plugin": ["@rejacky/opencode-insights"]
23
+ }
24
+ ```
25
+
26
+ ```json
27
+ // ~/.config/opencode/tui.json
28
+ {
29
+ "plugin": ["@rejacky/opencode-insights/tui"]
30
+ }
31
+ ```
32
+
33
+ Preview config changes without writing files:
34
+
35
+ ```bash
36
+ npx opencode-insights configure --dry-run
37
+ ```
38
+
39
+ OpenCode 1.17.14 loads `tui.json` for TUI plugins. It does not load `tui.jsonc`, so `configure` always writes the TUI plugin entry to `tui.json`.
40
+
41
+ The TUI plugin value is the package export `@rejacky/opencode-insights/tui`, not an absolute `dist/tui.js` path.
42
+
43
+ OpenCode resolves TUI plugin entries through module resolution, so this works after installing the package in your OpenCode config/package directory.
44
+
45
+ Use a custom OpenCode config directory:
46
+
47
+ ```bash
48
+ npx opencode-insights configure --config-dir ~/.config/opencode
49
+ ```
50
+
51
+ ## What You Get
52
+
53
+ - Live TPS, average TPS, and average TTFT in the OpenCode prompt zone.
54
+ - Right-sidebar subagent status: running, done, failed, elapsed time, and token/context usage when OpenCode exposes it.
55
+ - Local capture of OpenCode hook/event data without redaction.
56
+ - A local web viewer for sessions, messages, hooks, request context, system/messages transforms, and assistant responses.
57
+
58
+ ## Open The Viewer
59
+
60
+ Start the local web viewer and open it in your browser:
61
+
62
+ ```bash
63
+ npx opencode-insights open --limit 5000 --port 8765
64
+ ```
65
+
66
+ Or run the server only:
67
+
68
+ ```bash
69
+ npx opencode-insights serve --limit 5000 --port 8765
70
+ ```
71
+
72
+ Then open:
73
+
74
+ ```text
75
+ http://127.0.0.1:8765/
76
+ ```
77
+
78
+ The viewer shows:
79
+
80
+ - `MSG` rows for user messages.
81
+ - `HOOK` rows for OpenCode plugin hooks.
82
+ - `Summary`, `Request`, `Response`, and `Raw` tabs.
83
+ - Expandable/collapsible JSON trees with `Expand All` and `Collapse All`.
84
+
85
+ ## Common Commands
86
+
87
+ List recent raw captures:
88
+
89
+ ```bash
90
+ npx opencode-insights recent --limit 20
91
+ ```
92
+
93
+ List reconstructed sessions:
94
+
95
+ ```bash
96
+ npx opencode-insights sessions --limit 5000
97
+ ```
98
+
99
+ Print one reconstructed session:
100
+
101
+ ```bash
102
+ npx opencode-insights show ses_xxx --limit 10000
103
+ ```
104
+
105
+ Export one session to JSON:
106
+
107
+ ```bash
108
+ npx opencode-insights export ses_xxx --limit 10000 --output ./session.json
109
+ ```
110
+
111
+ Check DB path, table health, row counts, and SQLite readability:
112
+
113
+ ```bash
114
+ npx opencode-insights doctor
115
+ ```
116
+
117
+ Compact the local SQLite DB after heavy testing:
118
+
119
+ ```bash
120
+ npx opencode-insights vacuum
121
+ ```
122
+
123
+ If the command is not available through `npx`, run the installed binary directly from your OpenCode config directory:
124
+
125
+ ```bash
126
+ ./node_modules/.bin/opencode-insights doctor
127
+ ```
128
+
129
+ ## Storage
130
+
131
+ Default database path:
132
+
133
+ ```text
134
+ ~/.opencode-insights/insights.sqlite
135
+ ```
136
+
137
+ If SQLite is unavailable in the plugin runtime, the fallback path is:
138
+
139
+ ```text
140
+ ~/.opencode-insights/insights.sqlite.jsonl
141
+ ```
142
+
143
+ You can override storage in `opencode.json` or `opencode.jsonc`:
144
+
145
+ ```json
146
+ {
147
+ "plugin": [
148
+ [
149
+ "@rejacky/opencode-insights",
150
+ {
151
+ "dbPath": "/absolute/path/to/insights.sqlite"
152
+ }
153
+ ]
154
+ ]
155
+ }
156
+ ```
157
+
158
+ ## Privacy Model
159
+
160
+ This plugin intentionally does not redact anything. It stores data locally exactly as OpenCode exposes it to plugin hooks and events.
161
+
162
+ 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.
163
+
164
+ ## Captured Hooks
165
+
166
+ The viewer labels OpenCode hook records as `HOOK` because they are not raw HTTP requests.
167
+
168
+ Common hook rows:
169
+
170
+ - `HOOK title`: OpenCode title-generation model call, usually only on the first turn.
171
+ - `HOOK build`: Main assistant response model-call hook.
172
+ - `HOOK messages.transform`: Final conversation messages OpenCode prepared before model execution.
173
+ - `HOOK system.transform`: System prompt strings OpenCode prepared before model execution.
174
+
175
+ Hook payload meaning:
176
+
177
+ - `payload.input`: Context OpenCode passed into the plugin hook.
178
+ - `payload.output`: Value returned by the hook, such as model settings or transformed messages.
179
+ - `headers.output.headers`: Headers returned by the headers hook.
180
+ - Response text is captured from OpenCode event stream rows such as `message.part.delta` and `message.part.updated`.
181
+
182
+ 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.
183
+
184
+ ## SQLite Queries
185
+
186
+ Count captured rows:
187
+
188
+ ```bash
189
+ sqlite3 ~/.opencode-insights/insights.sqlite \
190
+ "select kind, count(*) from captures group by kind order by kind;"
191
+ ```
192
+
193
+ Find text in captured payloads:
194
+
195
+ ```bash
196
+ sqlite3 ~/.opencode-insights/insights.sqlite "
197
+ select datetime(timestamp/1000,'unixepoch','localtime') as time,
198
+ kind,
199
+ session_id,
200
+ message_id,
201
+ substr(payload_json, 1, 1200) as preview
202
+ from captures
203
+ where payload_json like '%search text%'
204
+ order by timestamp desc
205
+ limit 20;
206
+ "
207
+ ```
208
+
209
+ ## Development
210
+
211
+ Development and publish notes live in [DEVELOPMENT.md](./DEVELOPMENT.md).
@@ -0,0 +1,48 @@
1
+ type CaptureKind = "chat.message" | "chat.params" | "chat.headers" | "experimental.chat.messages.transform" | "experimental.chat.system.transform" | "event" | "tool.execute.before" | "tool.execute.after";
2
+ type CaptureRecord = {
3
+ id: string;
4
+ kind: CaptureKind;
5
+ timestamp: number;
6
+ sessionID?: string | undefined;
7
+ messageID?: string | undefined;
8
+ providerID?: string | undefined;
9
+ modelID?: string | undefined;
10
+ payload: Record<string, unknown>;
11
+ };
12
+ type CaptureStore = {
13
+ initialize?(): Promise<void>;
14
+ append(record: CaptureRecord): Promise<void>;
15
+ close?(): Promise<void>;
16
+ };
17
+ type InsightsOptions = {
18
+ dataDir?: unknown;
19
+ dbPath?: unknown;
20
+ };
21
+ declare function defaultDataDir(): string;
22
+ declare function resolveCapturePath(options?: InsightsOptions): string;
23
+ declare function normalizeChatMessageCapture(input: unknown, output: unknown, timestamp?: number): CaptureRecord;
24
+ declare function normalizeChatParamsCapture(input: unknown, output: unknown, timestamp?: number): CaptureRecord;
25
+ declare function normalizeChatHeadersCapture(input: unknown, output: unknown, timestamp?: number): CaptureRecord;
26
+ declare function normalizeExperimentalChatMessagesTransformCapture(input: unknown, output: unknown, timestamp?: number): CaptureRecord;
27
+ declare function normalizeExperimentalChatSystemTransformCapture(input: unknown, output: unknown, timestamp?: number): CaptureRecord;
28
+ declare function normalizeEventCapture(event: unknown, timestamp?: number): CaptureRecord;
29
+ declare function normalizeToolCapture(kind: "tool.execute.before" | "tool.execute.after", input: unknown, output: unknown, timestamp?: number): CaptureRecord;
30
+ declare class JsonlCaptureStore implements CaptureStore {
31
+ private readonly path;
32
+ constructor(path: string);
33
+ initialize(): Promise<void>;
34
+ append(record: CaptureRecord): Promise<void>;
35
+ }
36
+ declare class SqliteCaptureStore implements CaptureStore {
37
+ private readonly path;
38
+ private db;
39
+ private unavailable;
40
+ constructor(path: string);
41
+ initialize(): Promise<void>;
42
+ append(record: CaptureRecord): Promise<void>;
43
+ close(): Promise<void>;
44
+ private database;
45
+ }
46
+ declare function createCaptureStore(options?: InsightsOptions): CaptureStore;
47
+
48
+ 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 };
@@ -0,0 +1,257 @@
1
+ // src/capture.ts
2
+ import { mkdir, appendFile, writeFile } from "fs/promises";
3
+ import { dirname, join } from "path";
4
+ import { homedir } from "os";
5
+ var sequence = 0;
6
+ function nextID(timestamp) {
7
+ sequence += 1;
8
+ return `${timestamp.toString(36)}-${sequence.toString(36)}`;
9
+ }
10
+ function isRecord(value) {
11
+ return !!value && typeof value === "object" && !Array.isArray(value);
12
+ }
13
+ function optionalString(value) {
14
+ return typeof value === "string" && value.length > 0 ? value : void 0;
15
+ }
16
+ function modelIDFrom(input) {
17
+ if (!isRecord(input)) return void 0;
18
+ return optionalString(input.modelID) ?? optionalString(input.id) ?? optionalString(input.name);
19
+ }
20
+ function providerIDFrom(input) {
21
+ if (!isRecord(input)) return void 0;
22
+ const info = isRecord(input.info) ? input.info : void 0;
23
+ return optionalString(info?.id) ?? optionalString(info?.name) ?? optionalString(input.id);
24
+ }
25
+ function sessionIDFrom(input) {
26
+ if (!isRecord(input)) return void 0;
27
+ return optionalString(input.sessionID) ?? optionalString(input.sessionId);
28
+ }
29
+ function messageIDFrom(input) {
30
+ if (!isRecord(input)) return void 0;
31
+ return optionalString(input.messageID) ?? optionalString(input.messageId);
32
+ }
33
+ function transformedMessages(output) {
34
+ if (!isRecord(output) || !Array.isArray(output.messages)) return [];
35
+ return output.messages.filter((message) => isRecord(message));
36
+ }
37
+ function infoFromTransformedMessage(message) {
38
+ return isRecord(message?.info) ? message.info : void 0;
39
+ }
40
+ function latestUserTransformedMessage(output) {
41
+ return transformedMessages(output).slice().reverse().find((message) => optionalString(infoFromTransformedMessage(message)?.role) === "user");
42
+ }
43
+ function compactUndefined(value) {
44
+ for (const key of Object.keys(value)) {
45
+ if (value[key] === void 0) delete value[key];
46
+ }
47
+ return value;
48
+ }
49
+ function captureRecord(input) {
50
+ return compactUndefined({ ...input });
51
+ }
52
+ function defaultDataDir() {
53
+ return join(homedir(), ".opencode-insights");
54
+ }
55
+ function resolveCapturePath(options = {}) {
56
+ if (typeof options.dbPath === "string" && options.dbPath.length > 0) {
57
+ return options.dbPath;
58
+ }
59
+ const dataDir = typeof options.dataDir === "string" && options.dataDir.length > 0 ? options.dataDir : defaultDataDir();
60
+ return join(dataDir, "insights.sqlite");
61
+ }
62
+ function normalizeChatMessageCapture(input, output, timestamp = Date.now()) {
63
+ const inputRecord = isRecord(input) ? input : {};
64
+ const model = isRecord(inputRecord.model) ? inputRecord.model : void 0;
65
+ return captureRecord({
66
+ id: nextID(timestamp),
67
+ kind: "chat.message",
68
+ timestamp,
69
+ sessionID: sessionIDFrom(inputRecord),
70
+ messageID: messageIDFrom(inputRecord),
71
+ providerID: model ? optionalString(model.providerID) : void 0,
72
+ modelID: model ? optionalString(model.modelID) : void 0,
73
+ payload: { input, output }
74
+ });
75
+ }
76
+ function normalizeChatParamsCapture(input, output, timestamp = Date.now()) {
77
+ const inputRecord = isRecord(input) ? input : {};
78
+ return captureRecord({
79
+ id: nextID(timestamp),
80
+ kind: "chat.params",
81
+ timestamp,
82
+ sessionID: sessionIDFrom(inputRecord),
83
+ messageID: messageIDFrom(inputRecord.message),
84
+ providerID: providerIDFrom(inputRecord.provider),
85
+ modelID: modelIDFrom(inputRecord.model),
86
+ payload: { input, output }
87
+ });
88
+ }
89
+ function normalizeChatHeadersCapture(input, output, timestamp = Date.now()) {
90
+ const inputRecord = isRecord(input) ? input : {};
91
+ return captureRecord({
92
+ id: nextID(timestamp),
93
+ kind: "chat.headers",
94
+ timestamp,
95
+ sessionID: sessionIDFrom(inputRecord),
96
+ messageID: messageIDFrom(inputRecord.message),
97
+ providerID: providerIDFrom(inputRecord.provider),
98
+ modelID: modelIDFrom(inputRecord.model),
99
+ payload: { input, output }
100
+ });
101
+ }
102
+ function normalizeExperimentalChatMessagesTransformCapture(input, output, timestamp = Date.now()) {
103
+ const latestUserMessage = latestUserTransformedMessage(output);
104
+ const info = infoFromTransformedMessage(latestUserMessage);
105
+ return captureRecord({
106
+ id: nextID(timestamp),
107
+ kind: "experimental.chat.messages.transform",
108
+ timestamp,
109
+ sessionID: sessionIDFrom(info),
110
+ messageID: messageIDFrom(info) ?? optionalString(info?.id),
111
+ payload: { input, output }
112
+ });
113
+ }
114
+ function normalizeExperimentalChatSystemTransformCapture(input, output, timestamp = Date.now()) {
115
+ const inputRecord = isRecord(input) ? input : {};
116
+ const model = isRecord(inputRecord.model) ? inputRecord.model : void 0;
117
+ return captureRecord({
118
+ id: nextID(timestamp),
119
+ kind: "experimental.chat.system.transform",
120
+ timestamp,
121
+ sessionID: sessionIDFrom(inputRecord),
122
+ providerID: model ? optionalString(model.providerID) : void 0,
123
+ modelID: model ? modelIDFrom(model) : void 0,
124
+ payload: { input, output }
125
+ });
126
+ }
127
+ function normalizeEventCapture(event, timestamp = Date.now()) {
128
+ const record = isRecord(event) ? event : {};
129
+ const properties = isRecord(record.properties) ? record.properties : {};
130
+ const info = isRecord(properties.info) ? properties.info : {};
131
+ const part = isRecord(properties.part) ? properties.part : {};
132
+ return captureRecord({
133
+ id: nextID(timestamp),
134
+ kind: "event",
135
+ timestamp,
136
+ sessionID: sessionIDFrom(properties) ?? sessionIDFrom(info) ?? sessionIDFrom(part) ?? sessionIDFrom(record),
137
+ messageID: messageIDFrom(properties) ?? messageIDFrom(info) ?? messageIDFrom(part),
138
+ payload: { event }
139
+ });
140
+ }
141
+ function normalizeToolCapture(kind, input, output, timestamp = Date.now()) {
142
+ const inputRecord = isRecord(input) ? input : {};
143
+ return captureRecord({
144
+ id: nextID(timestamp),
145
+ kind,
146
+ timestamp,
147
+ sessionID: sessionIDFrom(inputRecord),
148
+ payload: { input, output }
149
+ });
150
+ }
151
+ var JsonlCaptureStore = class {
152
+ constructor(path) {
153
+ this.path = path;
154
+ }
155
+ path;
156
+ async initialize() {
157
+ await mkdir(dirname(this.path), { recursive: true });
158
+ await writeFile(this.path, "", { flag: "a" });
159
+ }
160
+ async append(record) {
161
+ await mkdir(dirname(this.path), { recursive: true });
162
+ await appendFile(this.path, `${JSON.stringify(record)}
163
+ `, "utf8");
164
+ }
165
+ };
166
+ var SqliteCaptureStore = class {
167
+ constructor(path) {
168
+ this.path = path;
169
+ }
170
+ path;
171
+ db;
172
+ unavailable = false;
173
+ async initialize() {
174
+ const db = await this.database();
175
+ if (!db) {
176
+ const fallbackPath = this.path.endsWith(".sqlite") ? `${this.path}.jsonl` : this.path;
177
+ await new JsonlCaptureStore(fallbackPath).initialize();
178
+ }
179
+ }
180
+ 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);
185
+ return;
186
+ }
187
+ db.run(
188
+ `insert into captures (
189
+ id, kind, timestamp, session_id, message_id, provider_id, model_id, payload_json
190
+ ) values (?, ?, ?, ?, ?, ?, ?, ?)`,
191
+ record.id,
192
+ record.kind,
193
+ record.timestamp,
194
+ record.sessionID ?? null,
195
+ record.messageID ?? null,
196
+ record.providerID ?? null,
197
+ record.modelID ?? null,
198
+ JSON.stringify(record.payload)
199
+ );
200
+ }
201
+ async close() {
202
+ this.db?.close?.();
203
+ this.db = void 0;
204
+ }
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
+ }
223
+ }
224
+ };
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
+ function createCaptureStore(options = {}) {
240
+ return new SqliteCaptureStore(resolveCapturePath(options));
241
+ }
242
+
243
+ export {
244
+ defaultDataDir,
245
+ resolveCapturePath,
246
+ normalizeChatMessageCapture,
247
+ normalizeChatParamsCapture,
248
+ normalizeChatHeadersCapture,
249
+ normalizeExperimentalChatMessagesTransformCapture,
250
+ normalizeExperimentalChatSystemTransformCapture,
251
+ normalizeEventCapture,
252
+ normalizeToolCapture,
253
+ JsonlCaptureStore,
254
+ SqliteCaptureStore,
255
+ createCaptureStore
256
+ };
257
+ //# sourceMappingURL=chunk-G7E4J6OW.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/capture.ts"],"sourcesContent":["import { mkdir, appendFile, writeFile } from \"node:fs/promises\";\nimport { dirname, join } from \"node:path\";\nimport { homedir } from \"node:os\";\n\nexport type CaptureKind =\n | \"chat.message\"\n | \"chat.params\"\n | \"chat.headers\"\n | \"experimental.chat.messages.transform\"\n | \"experimental.chat.system.transform\"\n | \"event\"\n | \"tool.execute.before\"\n | \"tool.execute.after\";\n\nexport type CaptureRecord = {\n id: string;\n kind: CaptureKind;\n timestamp: number;\n sessionID?: string | undefined;\n messageID?: string | undefined;\n providerID?: string | undefined;\n modelID?: string | undefined;\n payload: Record<string, unknown>;\n};\n\nexport type CaptureStore = {\n initialize?(): Promise<void>;\n append(record: CaptureRecord): Promise<void>;\n close?(): Promise<void>;\n};\n\nexport type InsightsOptions = {\n dataDir?: unknown;\n dbPath?: unknown;\n};\n\nlet sequence = 0;\n\nfunction nextID(timestamp: number) {\n sequence += 1;\n return `${timestamp.toString(36)}-${sequence.toString(36)}`;\n}\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return !!value && typeof value === \"object\" && !Array.isArray(value);\n}\n\nfunction optionalString(value: unknown): string | undefined {\n return typeof value === \"string\" && value.length > 0 ? value : undefined;\n}\n\nfunction modelIDFrom(input: unknown): string | undefined {\n if (!isRecord(input)) return undefined;\n return optionalString(input.modelID) ?? optionalString(input.id) ?? optionalString(input.name);\n}\n\nfunction providerIDFrom(input: unknown): string | undefined {\n if (!isRecord(input)) return undefined;\n const info = isRecord(input.info) ? input.info : undefined;\n return optionalString(info?.id) ?? optionalString(info?.name) ?? optionalString(input.id);\n}\n\nfunction sessionIDFrom(input: unknown): string | undefined {\n if (!isRecord(input)) return undefined;\n return optionalString(input.sessionID) ?? optionalString(input.sessionId);\n}\n\nfunction messageIDFrom(input: unknown): string | undefined {\n if (!isRecord(input)) return undefined;\n return optionalString(input.messageID) ?? optionalString(input.messageId);\n}\n\nfunction transformedMessages(output: unknown): Record<string, unknown>[] {\n if (!isRecord(output) || !Array.isArray(output.messages)) return [];\n return output.messages.filter((message) => isRecord(message));\n}\n\nfunction infoFromTransformedMessage(message: Record<string, unknown> | undefined): Record<string, unknown> | undefined {\n return isRecord(message?.info) ? message.info : undefined;\n}\n\nfunction latestUserTransformedMessage(output: unknown): Record<string, unknown> | undefined {\n return transformedMessages(output)\n .slice()\n .reverse()\n .find((message) => optionalString(infoFromTransformedMessage(message)?.role) === \"user\");\n}\n\nfunction compactUndefined<T extends Record<string, unknown>>(value: T): T {\n for (const key of Object.keys(value)) {\n if (value[key] === undefined) delete value[key];\n }\n return value;\n}\n\nfunction captureRecord(input: {\n id: string;\n kind: CaptureKind;\n timestamp: number;\n sessionID?: string | undefined;\n messageID?: string | undefined;\n providerID?: string | undefined;\n modelID?: string | undefined;\n payload: Record<string, unknown>;\n}): CaptureRecord {\n return compactUndefined({ ...input }) as CaptureRecord;\n}\n\nexport function defaultDataDir() {\n return join(homedir(), \".opencode-insights\");\n}\n\nexport function resolveCapturePath(options: InsightsOptions = {}) {\n if (typeof options.dbPath === \"string\" && options.dbPath.length > 0) {\n return options.dbPath;\n }\n\n const dataDir =\n typeof options.dataDir === \"string\" && options.dataDir.length > 0\n ? options.dataDir\n : defaultDataDir();\n return join(dataDir, \"insights.sqlite\");\n}\n\nexport function normalizeChatMessageCapture(input: unknown, output: unknown, timestamp = Date.now()): CaptureRecord {\n const inputRecord = isRecord(input) ? input : {};\n const model = isRecord(inputRecord.model) ? inputRecord.model : undefined;\n return captureRecord({\n id: nextID(timestamp),\n kind: \"chat.message\",\n timestamp,\n sessionID: sessionIDFrom(inputRecord),\n messageID: messageIDFrom(inputRecord),\n providerID: model ? optionalString(model.providerID) : undefined,\n modelID: model ? optionalString(model.modelID) : undefined,\n payload: { input, output }\n });\n}\n\nexport function normalizeChatParamsCapture(input: unknown, output: unknown, timestamp = Date.now()): CaptureRecord {\n const inputRecord = isRecord(input) ? input : {};\n return captureRecord({\n id: nextID(timestamp),\n kind: \"chat.params\",\n timestamp,\n sessionID: sessionIDFrom(inputRecord),\n messageID: messageIDFrom(inputRecord.message),\n providerID: providerIDFrom(inputRecord.provider),\n modelID: modelIDFrom(inputRecord.model),\n payload: { input, output }\n });\n}\n\nexport function normalizeChatHeadersCapture(input: unknown, output: unknown, timestamp = Date.now()): CaptureRecord {\n const inputRecord = isRecord(input) ? input : {};\n return captureRecord({\n id: nextID(timestamp),\n kind: \"chat.headers\",\n timestamp,\n sessionID: sessionIDFrom(inputRecord),\n messageID: messageIDFrom(inputRecord.message),\n providerID: providerIDFrom(inputRecord.provider),\n modelID: modelIDFrom(inputRecord.model),\n payload: { input, output }\n });\n}\n\nexport function normalizeExperimentalChatMessagesTransformCapture(input: unknown, output: unknown, timestamp = Date.now()): CaptureRecord {\n const latestUserMessage = latestUserTransformedMessage(output);\n const info = infoFromTransformedMessage(latestUserMessage);\n return captureRecord({\n id: nextID(timestamp),\n kind: \"experimental.chat.messages.transform\",\n timestamp,\n sessionID: sessionIDFrom(info),\n messageID: messageIDFrom(info) ?? optionalString(info?.id),\n payload: { input, output }\n });\n}\n\nexport function normalizeExperimentalChatSystemTransformCapture(input: unknown, output: unknown, timestamp = Date.now()): CaptureRecord {\n const inputRecord = isRecord(input) ? input : {};\n const model = isRecord(inputRecord.model) ? inputRecord.model : undefined;\n return captureRecord({\n id: nextID(timestamp),\n kind: \"experimental.chat.system.transform\",\n timestamp,\n sessionID: sessionIDFrom(inputRecord),\n providerID: model ? optionalString(model.providerID) : undefined,\n modelID: model ? modelIDFrom(model) : undefined,\n payload: { input, output }\n });\n}\n\nexport function normalizeEventCapture(event: unknown, timestamp = Date.now()): CaptureRecord {\n const record = isRecord(event) ? event : {};\n const properties = isRecord(record.properties) ? record.properties : {};\n const info = isRecord(properties.info) ? properties.info : {};\n const part = isRecord(properties.part) ? properties.part : {};\n return captureRecord({\n id: nextID(timestamp),\n kind: \"event\",\n timestamp,\n sessionID: sessionIDFrom(properties) ?? sessionIDFrom(info) ?? sessionIDFrom(part) ?? sessionIDFrom(record),\n messageID: messageIDFrom(properties) ?? messageIDFrom(info) ?? messageIDFrom(part),\n payload: { event }\n });\n}\n\nexport function normalizeToolCapture(\n kind: \"tool.execute.before\" | \"tool.execute.after\",\n input: unknown,\n output: unknown,\n timestamp = Date.now()\n): CaptureRecord {\n const inputRecord = isRecord(input) ? input : {};\n return captureRecord({\n id: nextID(timestamp),\n kind,\n timestamp,\n sessionID: sessionIDFrom(inputRecord),\n payload: { input, output }\n });\n}\n\nexport class JsonlCaptureStore implements CaptureStore {\n constructor(private readonly path: string) {}\n\n async initialize() {\n await mkdir(dirname(this.path), { recursive: true });\n await writeFile(this.path, \"\", { flag: \"a\" });\n }\n\n async append(record: CaptureRecord) {\n await mkdir(dirname(this.path), { recursive: true });\n await appendFile(this.path, `${JSON.stringify(record)}\\n`, \"utf8\");\n }\n}\n\ntype BunSqliteDatabase = {\n run(sql: string, ...params: unknown[]): unknown;\n close?(): void;\n};\n\nexport class SqliteCaptureStore implements CaptureStore {\n private db: BunSqliteDatabase | undefined;\n private unavailable = false;\n\n constructor(private readonly path: string) {}\n\n async initialize() {\n const db = await this.database();\n if (!db) {\n const fallbackPath = this.path.endsWith(\".sqlite\") ? `${this.path}.jsonl` : this.path;\n await new JsonlCaptureStore(fallbackPath).initialize();\n }\n }\n\n async append(record: CaptureRecord) {\n const db = await this.database();\n if (!db) {\n const fallbackPath = this.path.endsWith(\".sqlite\") ? `${this.path}.jsonl` : this.path;\n await new JsonlCaptureStore(fallbackPath).append(record);\n return;\n }\n\n db.run(\n `insert into captures (\n id, kind, timestamp, session_id, message_id, provider_id, model_id, payload_json\n ) values (?, ?, ?, ?, ?, ?, ?, ?)`,\n record.id,\n record.kind,\n record.timestamp,\n record.sessionID ?? null,\n record.messageID ?? null,\n record.providerID ?? null,\n record.modelID ?? null,\n JSON.stringify(record.payload)\n );\n }\n\n async close() {\n this.db?.close?.();\n this.db = undefined;\n }\n\n private async database() {\n if (this.db) return this.db;\n if (this.unavailable) return undefined;\n\n try {\n await mkdir(dirname(this.path), { recursive: true });\n const mod = (await import(\"bun:sqlite\").catch(() => undefined)) as\n | { Database: new (path: string) => BunSqliteDatabase }\n | undefined;\n if (!mod) {\n this.unavailable = true;\n return undefined;\n }\n const db = new mod.Database(this.path);\n initializeSchema(db);\n this.db = db;\n return db;\n } catch {\n this.unavailable = true;\n return undefined;\n }\n }\n}\n\nfunction initializeSchema(db: BunSqliteDatabase) {\n db.run(`create table if not exists captures (\n id text primary key,\n kind text not null,\n timestamp integer not null,\n session_id text,\n message_id text,\n provider_id text,\n model_id text,\n payload_json text not null\n )`);\n db.run(`create index if not exists captures_timestamp_idx on captures(timestamp)`);\n db.run(`create index if not exists captures_session_idx on captures(session_id)`);\n}\n\nexport function createCaptureStore(options: InsightsOptions = {}): CaptureStore {\n return new SqliteCaptureStore(resolveCapturePath(options));\n}\n"],"mappings":"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","names":[]}