@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 +80 -94
- package/dist/{capture-DkASdFpu.d.ts → capture-Dy799i3Z.d.ts} +18 -8
- package/dist/{chunk-Q5ROKOOJ.js → chunk-XCHFXQIL.js} +167 -49
- package/dist/cli.d.ts +7 -1
- package/dist/cli.js +622 -150
- package/dist/index.d.ts +1 -2
- package/dist/index.js +66 -14
- package/package.json +7 -1
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
|
+

|
|
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
|
+
|
|
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
|
-
|
|
63
|
+
opencode-insights uninstall
|
|
21
64
|
```
|
|
22
65
|
|
|
23
66
|
Preview the cleanup without changing files:
|
|
24
67
|
|
|
25
68
|
```bash
|
|
26
|
-
|
|
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
|
-
|
|
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
|
-
|
|
81
|
+
opencode-insights uninstall --config-dir ~/.config/opencode --data-dir ~/.opencode-insights
|
|
39
82
|
```
|
|
40
83
|
|
|
41
|
-
After uninstalling, restart OpenCode.
|
|
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
|
-
```
|
|
44
|
-
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
-
|
|
79
|
-
-
|
|
80
|
-
- `Summary
|
|
81
|
-
-
|
|
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
|
-
|
|
133
|
+
opencode-insights recent --limit 20
|
|
89
134
|
```
|
|
90
135
|
|
|
91
136
|
List reconstructed sessions:
|
|
92
137
|
|
|
93
138
|
```bash
|
|
94
|
-
|
|
139
|
+
opencode-insights sessions --limit 5000
|
|
95
140
|
```
|
|
96
141
|
|
|
97
142
|
Print one reconstructed session:
|
|
98
143
|
|
|
99
144
|
```bash
|
|
100
|
-
|
|
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
|
-
|
|
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
|
-
|
|
157
|
+
opencode-insights doctor
|
|
113
158
|
```
|
|
114
159
|
|
|
115
160
|
Compact the local SQLite DB after heavy testing:
|
|
116
161
|
|
|
117
162
|
```bash
|
|
118
|
-
|
|
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
|
-
|
|
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
|
-
|
|
178
|
+
You can also run the published package through npm without relying on the OpenCode cache:
|
|
128
179
|
|
|
129
180
|
```bash
|
|
130
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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,
|
|
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(
|
|
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
|
-
|
|
250
|
+
fallbackStore;
|
|
173
251
|
async initialize() {
|
|
174
|
-
|
|
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
|
-
|
|
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
|
-
|
|
182
|
-
|
|
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
|
|
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
|
-
|
|
206
|
-
|
|
207
|
-
if (
|
|
208
|
-
|
|
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-
|
|
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
|
};
|