@jinn-network/core 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/dist/canonical-json.d.ts +1 -0
- package/dist/canonical-json.js +67 -0
- package/dist/captured-task.d.ts +140 -0
- package/dist/captured-task.js +122 -0
- package/dist/contribution-store.d.ts +97 -0
- package/dist/contribution-store.js +770 -0
- package/dist/corpus-read/acquire.d.ts +39 -0
- package/dist/corpus-read/acquire.js +212 -0
- package/dist/corpus-read/cache.d.ts +14 -0
- package/dist/corpus-read/cache.js +17 -0
- package/dist/corpus-read/capture-meta.d.ts +16 -0
- package/dist/corpus-read/capture-meta.js +18 -0
- package/dist/corpus-read/create-corpus.d.ts +2 -0
- package/dist/corpus-read/create-corpus.js +97 -0
- package/dist/corpus-read/fetch-artifact.d.ts +21 -0
- package/dist/corpus-read/fetch-artifact.js +32 -0
- package/dist/corpus-read/fetch.d.ts +12 -0
- package/dist/corpus-read/fetch.js +24 -0
- package/dist/corpus-read/http-discovery.d.ts +9 -0
- package/dist/corpus-read/http-discovery.js +128 -0
- package/dist/corpus-read/index.d.ts +10 -0
- package/dist/corpus-read/index.js +10 -0
- package/dist/corpus-read/ipfs.d.ts +5 -0
- package/dist/corpus-read/ipfs.js +72 -0
- package/dist/corpus-read/route-resolver.d.ts +16 -0
- package/dist/corpus-read/route-resolver.js +19 -0
- package/dist/corpus-read/types.d.ts +177 -0
- package/dist/corpus-read/types.js +42 -0
- package/dist/envelope.d.ts +134 -0
- package/dist/envelope.js +162 -0
- package/dist/evidence-adapter.d.ts +26 -0
- package/dist/evidence-adapter.js +321 -0
- package/dist/evidence-filesystem.d.ts +40 -0
- package/dist/evidence-filesystem.js +267 -0
- package/dist/evidence-index.d.ts +117 -0
- package/dist/evidence-index.js +1083 -0
- package/dist/evidence-store-lock.d.ts +2 -0
- package/dist/evidence-store-lock.js +208 -0
- package/dist/execution-envelope.d.ts +3142 -0
- package/dist/execution-envelope.js +203 -0
- package/dist/index.d.ts +12 -0
- package/dist/index.js +12 -0
- package/dist/manifest.d.ts +75 -0
- package/dist/manifest.js +184 -0
- package/dist/paired.d.ts +68 -0
- package/dist/paired.js +92 -0
- package/dist/scrub/build.d.ts +52 -0
- package/dist/scrub/build.js +84 -0
- package/dist/scrub/emit-scrub.d.ts +41 -0
- package/dist/scrub/emit-scrub.js +87 -0
- package/dist/scrub/index.d.ts +12 -0
- package/dist/scrub/index.js +12 -0
- package/dist/scrub/key-policy.d.ts +20 -0
- package/dist/scrub/key-policy.js +42 -0
- package/dist/scrub/layer2.d.ts +23 -0
- package/dist/scrub/layer2.js +32 -0
- package/dist/scrub/ml-pii-stage.d.ts +24 -0
- package/dist/scrub/ml-pii-stage.js +42 -0
- package/dist/scrub/openredaction-stage.d.ts +196 -0
- package/dist/scrub/openredaction-stage.js +270 -0
- package/dist/scrub/pii-build.d.ts +38 -0
- package/dist/scrub/pii-build.js +67 -0
- package/dist/scrub/pipeline.d.ts +38 -0
- package/dist/scrub/pipeline.js +83 -0
- package/dist/scrub/plain-patterns-stage.d.ts +31 -0
- package/dist/scrub/plain-patterns-stage.js +84 -0
- package/dist/scrub/secretlint-stage.d.ts +36 -0
- package/dist/scrub/secretlint-stage.js +249 -0
- package/dist/scrub/transformers-detector.d.ts +35 -0
- package/dist/scrub/transformers-detector.js +67 -0
- package/dist/scrub/types.d.ts +25 -0
- package/dist/scrub/types.js +1 -0
- package/dist/session-provenance.d.ts +75 -0
- package/dist/session-provenance.js +20 -0
- package/dist/skill-artifact.d.ts +442 -0
- package/dist/skill-artifact.js +136 -0
- package/dist/trajectory/hash-chain.d.ts +18 -0
- package/dist/trajectory/hash-chain.js +47 -0
- package/dist/trajectory/index.d.ts +4 -0
- package/dist/trajectory/index.js +4 -0
- package/dist/trajectory/schema.d.ts +606 -0
- package/dist/trajectory/schema.js +110 -0
- package/dist/trajectory/transcript-parsers/aider-history.d.ts +42 -0
- package/dist/trajectory/transcript-parsers/aider-history.js +181 -0
- package/dist/trajectory/transcript-parsers/claude-code-jsonl.d.ts +37 -0
- package/dist/trajectory/transcript-parsers/claude-code-jsonl.js +142 -0
- package/dist/trajectory/transcript-parsers/codex-session.d.ts +42 -0
- package/dist/trajectory/transcript-parsers/codex-session.js +235 -0
- package/dist/trajectory/transcript-parsers/continue-devdata.d.ts +41 -0
- package/dist/trajectory/transcript-parsers/continue-devdata.js +190 -0
- package/dist/trajectory/transcript-parsers/cursor-sqlite.d.ts +38 -0
- package/dist/trajectory/transcript-parsers/cursor-sqlite.js +118 -0
- package/dist/trajectory/transcript-parsers/gemini-session.d.ts +35 -0
- package/dist/trajectory/transcript-parsers/gemini-session.js +137 -0
- package/dist/trajectory/transcript-parsers/index.d.ts +7 -0
- package/dist/trajectory/transcript-parsers/index.js +7 -0
- package/dist/trajectory/transcript-parsers/types.d.ts +101 -0
- package/dist/trajectory/transcript-parsers/types.js +32 -0
- package/dist/trajectory/transcript-to-spans/attrs.d.ts +4 -0
- package/dist/trajectory/transcript-to-spans/attrs.js +30 -0
- package/dist/trajectory/transcript-to-spans/claude-code-stream-json.d.ts +8 -0
- package/dist/trajectory/transcript-to-spans/claude-code-stream-json.js +162 -0
- package/dist/trajectory/transcript-to-spans/codex-exec-json.d.ts +8 -0
- package/dist/trajectory/transcript-to-spans/codex-exec-json.js +253 -0
- package/dist/trajectory/transcript-to-spans/index.d.ts +4 -0
- package/dist/trajectory/transcript-to-spans/index.js +4 -0
- package/dist/trajectory/transcript-to-spans/types.d.ts +18 -0
- package/dist/trajectory/transcript-to-spans/types.js +1 -0
- package/dist/window.d.ts +12 -0
- package/dist/window.js +5 -0
- package/package.json +67 -0
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Codex CLI session transcript parser.
|
|
3
|
+
*
|
|
4
|
+
* Reads `~/.codex/sessions/<id>.jsonl` line-by-line, mapping per-record content
|
|
5
|
+
* to canonical TranscriptEvent shapes (Task 3.1):
|
|
6
|
+
* - role: 'user' with string content → user-message
|
|
7
|
+
* - role: 'assistant' with string content → assistant-message
|
|
8
|
+
* - role: 'function' with function_call → tool-call (also: assistant with
|
|
9
|
+
* a top-level function_call field)
|
|
10
|
+
* - role: 'tool' with content → tool-result
|
|
11
|
+
*
|
|
12
|
+
* Codex CLI 0.129.0+ wraps each line as `{ timestamp, type, payload }` where
|
|
13
|
+
* `type` is `session_meta`, `turn_context`, `response_item`, or `event_msg`.
|
|
14
|
+
* Tool calls and messages live inside `payload` (e.g. `function_call`,
|
|
15
|
+
* `function_call_output`, `message` with `input_text` / `output_text` blocks).
|
|
16
|
+
* Older sessions use a flat per-line shape with `role` at the top level; both
|
|
17
|
+
* are detected by record shape.
|
|
18
|
+
*
|
|
19
|
+
* `parseChunk` preserves any incomplete trailing line across calls so the
|
|
20
|
+
* watcher (Task 3.9) can stream tail bytes without losing partial records.
|
|
21
|
+
*
|
|
22
|
+
* Spec: spec/2026-05-07-telemetry-collector-and-task-generator.md §4.2 path B.
|
|
23
|
+
*/
|
|
24
|
+
import { readFile } from 'node:fs/promises';
|
|
25
|
+
export class CodexSessionParser {
|
|
26
|
+
tool = 'codex';
|
|
27
|
+
buffer = '';
|
|
28
|
+
/** Maps function_call `call_id` → tool name for function_call_output rows. */
|
|
29
|
+
callNames = new Map();
|
|
30
|
+
parseChunk(input) {
|
|
31
|
+
const text = typeof input.chunk === 'string' ? input.chunk : input.chunk.toString('utf-8');
|
|
32
|
+
this.buffer += text;
|
|
33
|
+
const lines = this.buffer.split('\n');
|
|
34
|
+
this.buffer = lines.pop() ?? '';
|
|
35
|
+
const events = [];
|
|
36
|
+
for (const line of lines) {
|
|
37
|
+
if (!line.trim())
|
|
38
|
+
continue;
|
|
39
|
+
let raw;
|
|
40
|
+
try {
|
|
41
|
+
raw = JSON.parse(line);
|
|
42
|
+
}
|
|
43
|
+
catch {
|
|
44
|
+
continue;
|
|
45
|
+
}
|
|
46
|
+
events.push(...this.lineToEvents(raw));
|
|
47
|
+
}
|
|
48
|
+
return events;
|
|
49
|
+
}
|
|
50
|
+
async parseFull(input) {
|
|
51
|
+
const text = await readFile(input.path, 'utf-8');
|
|
52
|
+
const normalised = text.endsWith('\n') ? text : text + '\n';
|
|
53
|
+
const previousBuffer = this.buffer;
|
|
54
|
+
const previousCallNames = new Map(this.callNames);
|
|
55
|
+
this.buffer = '';
|
|
56
|
+
this.callNames.clear();
|
|
57
|
+
try {
|
|
58
|
+
return this.parseChunk({ sessionId: input.sessionId, chunk: normalised });
|
|
59
|
+
}
|
|
60
|
+
finally {
|
|
61
|
+
this.buffer = previousBuffer;
|
|
62
|
+
this.callNames = previousCallNames;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
lineToEvents(raw) {
|
|
66
|
+
if (!raw || typeof raw !== 'object')
|
|
67
|
+
return [];
|
|
68
|
+
if (isWrappedEnvelope(raw)) {
|
|
69
|
+
return this.wrappedEnvelopeToEvents(raw);
|
|
70
|
+
}
|
|
71
|
+
return this.recordToEvents(raw);
|
|
72
|
+
}
|
|
73
|
+
wrappedEnvelopeToEvents(envelope) {
|
|
74
|
+
const ts = envelope.timestamp;
|
|
75
|
+
if (typeof ts !== 'string')
|
|
76
|
+
return [];
|
|
77
|
+
const payload = envelope.payload;
|
|
78
|
+
if (!payload || typeof payload !== 'object')
|
|
79
|
+
return [];
|
|
80
|
+
if (envelope.type === 'response_item') {
|
|
81
|
+
return this.responseItemToEvents(ts, payload);
|
|
82
|
+
}
|
|
83
|
+
// `event_msg` rows (user_message, agent_message, mcp_tool_call_end) duplicate
|
|
84
|
+
// `response_item` message/tool rows — skip them to avoid double-counting.
|
|
85
|
+
return [];
|
|
86
|
+
}
|
|
87
|
+
responseItemToEvents(ts, payload) {
|
|
88
|
+
const itemType = payload.type;
|
|
89
|
+
if (itemType === 'function_call') {
|
|
90
|
+
const name = payload.name;
|
|
91
|
+
if (typeof name !== 'string')
|
|
92
|
+
return [];
|
|
93
|
+
const callId = payload.call_id;
|
|
94
|
+
if (typeof callId === 'string') {
|
|
95
|
+
this.callNames.set(callId, name);
|
|
96
|
+
}
|
|
97
|
+
return [
|
|
98
|
+
{
|
|
99
|
+
kind: 'tool-call',
|
|
100
|
+
timestamp: ts,
|
|
101
|
+
name,
|
|
102
|
+
args: parseArgs(payload.arguments),
|
|
103
|
+
},
|
|
104
|
+
];
|
|
105
|
+
}
|
|
106
|
+
if (itemType === 'function_call_output') {
|
|
107
|
+
const output = payload.output;
|
|
108
|
+
const content = typeof output === 'string' ? output : stringifyContent(output);
|
|
109
|
+
if (!content)
|
|
110
|
+
return [];
|
|
111
|
+
const callId = payload.call_id;
|
|
112
|
+
const name = (typeof callId === 'string' ? this.callNames.get(callId) : undefined) ??
|
|
113
|
+
(typeof callId === 'string' ? callId : undefined);
|
|
114
|
+
if (typeof name !== 'string')
|
|
115
|
+
return [];
|
|
116
|
+
return [{ kind: 'tool-result', timestamp: ts, name, content, isError: false }];
|
|
117
|
+
}
|
|
118
|
+
if (itemType === 'message') {
|
|
119
|
+
const role = payload.role;
|
|
120
|
+
const text = extractMessageText(payload.content);
|
|
121
|
+
if (!text)
|
|
122
|
+
return [];
|
|
123
|
+
if (role === 'user') {
|
|
124
|
+
return [{ kind: 'user-message', timestamp: ts, content: text }];
|
|
125
|
+
}
|
|
126
|
+
if (role === 'assistant') {
|
|
127
|
+
return [{ kind: 'assistant-message', timestamp: ts, content: text }];
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
return [];
|
|
131
|
+
}
|
|
132
|
+
recordToEvents(record) {
|
|
133
|
+
if (!record || typeof record !== 'object')
|
|
134
|
+
return [];
|
|
135
|
+
const ts = record.ts ?? record.timestamp;
|
|
136
|
+
if (typeof ts !== 'string')
|
|
137
|
+
return [];
|
|
138
|
+
const role = record.role;
|
|
139
|
+
// function_call may sit on either a 'function' role record or alongside an
|
|
140
|
+
// 'assistant' role record. Emit a tool-call event when we find one.
|
|
141
|
+
if (record.function_call && typeof record.function_call === 'object') {
|
|
142
|
+
const fn = record.function_call;
|
|
143
|
+
if (typeof fn.name === 'string') {
|
|
144
|
+
return [
|
|
145
|
+
{
|
|
146
|
+
kind: 'tool-call',
|
|
147
|
+
timestamp: ts,
|
|
148
|
+
name: fn.name,
|
|
149
|
+
args: parseArgs(fn.arguments),
|
|
150
|
+
},
|
|
151
|
+
];
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
if (role === 'user') {
|
|
155
|
+
const text = extractMessageText(record.content);
|
|
156
|
+
if (text) {
|
|
157
|
+
return [{ kind: 'user-message', timestamp: ts, content: text }];
|
|
158
|
+
}
|
|
159
|
+
return [];
|
|
160
|
+
}
|
|
161
|
+
if (role === 'assistant') {
|
|
162
|
+
const text = extractMessageText(record.content);
|
|
163
|
+
if (text) {
|
|
164
|
+
return [{ kind: 'assistant-message', timestamp: ts, content: text }];
|
|
165
|
+
}
|
|
166
|
+
return [];
|
|
167
|
+
}
|
|
168
|
+
if (role === 'tool' || role === 'function-result') {
|
|
169
|
+
const content = stringifyContent(record.content);
|
|
170
|
+
if (!content)
|
|
171
|
+
return [];
|
|
172
|
+
// Prefer `name` (tool name) when present; fall back to tool_call_id so
|
|
173
|
+
// downstream consumers always see a stable identifier. v0 gap: when
|
|
174
|
+
// only an id is available it carries less display signal than the name.
|
|
175
|
+
const name = record.name ?? record.tool_call_id;
|
|
176
|
+
if (typeof name !== 'string')
|
|
177
|
+
return [];
|
|
178
|
+
return [{ kind: 'tool-result', timestamp: ts, name, content, isError: false }];
|
|
179
|
+
}
|
|
180
|
+
return [];
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
function isWrappedEnvelope(raw) {
|
|
184
|
+
const rec = raw;
|
|
185
|
+
return (typeof rec.type === 'string' &&
|
|
186
|
+
rec.payload !== undefined &&
|
|
187
|
+
rec.payload !== null &&
|
|
188
|
+
typeof rec.payload === 'object' &&
|
|
189
|
+
!('role' in rec));
|
|
190
|
+
}
|
|
191
|
+
function extractMessageText(content) {
|
|
192
|
+
if (typeof content === 'string')
|
|
193
|
+
return content;
|
|
194
|
+
if (!Array.isArray(content))
|
|
195
|
+
return null;
|
|
196
|
+
const parts = [];
|
|
197
|
+
for (const block of content) {
|
|
198
|
+
if (!block || typeof block !== 'object')
|
|
199
|
+
continue;
|
|
200
|
+
const b = block;
|
|
201
|
+
const blockType = b.type;
|
|
202
|
+
if ((blockType === 'input_text' || blockType === 'output_text') &&
|
|
203
|
+
typeof b.text === 'string') {
|
|
204
|
+
parts.push(b.text);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
return parts.length > 0 ? parts.join('\n') : null;
|
|
208
|
+
}
|
|
209
|
+
function parseArgs(raw) {
|
|
210
|
+
if (!raw)
|
|
211
|
+
return {};
|
|
212
|
+
if (typeof raw === 'object')
|
|
213
|
+
return raw;
|
|
214
|
+
if (typeof raw === 'string') {
|
|
215
|
+
try {
|
|
216
|
+
const parsed = JSON.parse(raw);
|
|
217
|
+
if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
|
|
218
|
+
return parsed;
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
catch {
|
|
222
|
+
// Fall through — store the raw string under a sentinel key so the args
|
|
223
|
+
// round-trip through the canonical schema's `record(unknown)` shape.
|
|
224
|
+
return { _raw: raw };
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
return {};
|
|
228
|
+
}
|
|
229
|
+
function stringifyContent(content) {
|
|
230
|
+
if (typeof content === 'string')
|
|
231
|
+
return content;
|
|
232
|
+
if (content == null)
|
|
233
|
+
return '';
|
|
234
|
+
return JSON.stringify(content);
|
|
235
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Continue dev_data transcript parser.
|
|
3
|
+
*
|
|
4
|
+
* Continue (continue.dev) writes telemetry to `<workspace>/.continue/dev_data/`
|
|
5
|
+
* with one JSONL file per event kind and schema version:
|
|
6
|
+
*
|
|
7
|
+
* .continue/dev_data/chat/<schema-version>.jsonl
|
|
8
|
+
* .continue/dev_data/edit/<schema-version>.jsonl
|
|
9
|
+
* .continue/dev_data/autocomplete/<schema-version>.jsonl
|
|
10
|
+
*
|
|
11
|
+
* For Phase 3 v0 we ingest `chat` and `edit` directories. `autocomplete` is a
|
|
12
|
+
* different signal (per-keystroke completions) and is intentionally skipped.
|
|
13
|
+
*
|
|
14
|
+
* Per-event-kind record shapes (best-effort; schema varies slightly across
|
|
15
|
+
* Continue versions):
|
|
16
|
+
* chat: { role: 'user' | 'assistant', timestamp, content }
|
|
17
|
+
* edit: { timestamp, filepath, diff } (also accepts `path` for filepath)
|
|
18
|
+
*
|
|
19
|
+
* `parseFull` is given the dev_data directory; it globs the kind-subdirs we
|
|
20
|
+
* care about, reads each JSONL file, and dispatches per kind.
|
|
21
|
+
*
|
|
22
|
+
* `parseChunk` supports streaming a single file: callers may include a `kind`
|
|
23
|
+
* field on each record (the watcher knows which file it is tailing and tags
|
|
24
|
+
* each line); when missing, the parser tries chat first, then edit. Records
|
|
25
|
+
* that match neither are skipped.
|
|
26
|
+
*
|
|
27
|
+
* Spec: spec/2026-05-07-telemetry-collector-and-task-generator.md §4.2 path B.
|
|
28
|
+
*/
|
|
29
|
+
import type { TranscriptEvent, TranscriptParser } from './types.js';
|
|
30
|
+
export declare class ContinueDevDataParser implements TranscriptParser {
|
|
31
|
+
readonly tool: "continue";
|
|
32
|
+
private buffer;
|
|
33
|
+
parseChunk(input: {
|
|
34
|
+
sessionId: string;
|
|
35
|
+
chunk: Buffer | string;
|
|
36
|
+
}): TranscriptEvent[];
|
|
37
|
+
parseFull(input: {
|
|
38
|
+
sessionId: string;
|
|
39
|
+
path: string;
|
|
40
|
+
}): Promise<TranscriptEvent[]>;
|
|
41
|
+
}
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Continue dev_data transcript parser.
|
|
3
|
+
*
|
|
4
|
+
* Continue (continue.dev) writes telemetry to `<workspace>/.continue/dev_data/`
|
|
5
|
+
* with one JSONL file per event kind and schema version:
|
|
6
|
+
*
|
|
7
|
+
* .continue/dev_data/chat/<schema-version>.jsonl
|
|
8
|
+
* .continue/dev_data/edit/<schema-version>.jsonl
|
|
9
|
+
* .continue/dev_data/autocomplete/<schema-version>.jsonl
|
|
10
|
+
*
|
|
11
|
+
* For Phase 3 v0 we ingest `chat` and `edit` directories. `autocomplete` is a
|
|
12
|
+
* different signal (per-keystroke completions) and is intentionally skipped.
|
|
13
|
+
*
|
|
14
|
+
* Per-event-kind record shapes (best-effort; schema varies slightly across
|
|
15
|
+
* Continue versions):
|
|
16
|
+
* chat: { role: 'user' | 'assistant', timestamp, content }
|
|
17
|
+
* edit: { timestamp, filepath, diff } (also accepts `path` for filepath)
|
|
18
|
+
*
|
|
19
|
+
* `parseFull` is given the dev_data directory; it globs the kind-subdirs we
|
|
20
|
+
* care about, reads each JSONL file, and dispatches per kind.
|
|
21
|
+
*
|
|
22
|
+
* `parseChunk` supports streaming a single file: callers may include a `kind`
|
|
23
|
+
* field on each record (the watcher knows which file it is tailing and tags
|
|
24
|
+
* each line); when missing, the parser tries chat first, then edit. Records
|
|
25
|
+
* that match neither are skipped.
|
|
26
|
+
*
|
|
27
|
+
* Spec: spec/2026-05-07-telemetry-collector-and-task-generator.md §4.2 path B.
|
|
28
|
+
*/
|
|
29
|
+
import { readFile, readdir, stat } from 'node:fs/promises';
|
|
30
|
+
import * as path from 'node:path';
|
|
31
|
+
const SUPPORTED_KINDS = new Set(['chat', 'edit']);
|
|
32
|
+
export class ContinueDevDataParser {
|
|
33
|
+
tool = 'continue';
|
|
34
|
+
buffer = '';
|
|
35
|
+
parseChunk(input) {
|
|
36
|
+
const text = typeof input.chunk === 'string' ? input.chunk : input.chunk.toString('utf-8');
|
|
37
|
+
this.buffer += text;
|
|
38
|
+
const lines = this.buffer.split('\n');
|
|
39
|
+
this.buffer = lines.pop() ?? '';
|
|
40
|
+
const events = [];
|
|
41
|
+
for (const line of lines) {
|
|
42
|
+
if (!line.trim())
|
|
43
|
+
continue;
|
|
44
|
+
let record;
|
|
45
|
+
try {
|
|
46
|
+
record = JSON.parse(line);
|
|
47
|
+
}
|
|
48
|
+
catch {
|
|
49
|
+
continue;
|
|
50
|
+
}
|
|
51
|
+
const event = recordToEvent(record);
|
|
52
|
+
if (event)
|
|
53
|
+
events.push(event);
|
|
54
|
+
}
|
|
55
|
+
return events;
|
|
56
|
+
}
|
|
57
|
+
async parseFull(input) {
|
|
58
|
+
// For dev_data, the path is the dev_data directory (or a single file).
|
|
59
|
+
// If it's a directory, walk supported kind-subdirs; otherwise treat as
|
|
60
|
+
// a single JSONL file (delegate to parseChunk).
|
|
61
|
+
let stats;
|
|
62
|
+
try {
|
|
63
|
+
stats = await stat(input.path);
|
|
64
|
+
}
|
|
65
|
+
catch {
|
|
66
|
+
return [];
|
|
67
|
+
}
|
|
68
|
+
if (stats.isFile()) {
|
|
69
|
+
const text = await readFile(input.path, 'utf-8');
|
|
70
|
+
const normalised = text.endsWith('\n') ? text : text + '\n';
|
|
71
|
+
const previousBuffer = this.buffer;
|
|
72
|
+
this.buffer = '';
|
|
73
|
+
try {
|
|
74
|
+
// Tag records with the inferred kind from the parent directory so
|
|
75
|
+
// ambiguous shapes resolve correctly.
|
|
76
|
+
const inferredKind = inferKindFromPath(input.path);
|
|
77
|
+
return this.parseChunk({
|
|
78
|
+
sessionId: input.sessionId,
|
|
79
|
+
chunk: tagChunkWithKind(normalised, inferredKind),
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
finally {
|
|
83
|
+
this.buffer = previousBuffer;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
if (!stats.isDirectory())
|
|
87
|
+
return [];
|
|
88
|
+
const events = [];
|
|
89
|
+
let entries;
|
|
90
|
+
try {
|
|
91
|
+
entries = await readdir(input.path);
|
|
92
|
+
}
|
|
93
|
+
catch {
|
|
94
|
+
return [];
|
|
95
|
+
}
|
|
96
|
+
for (const entry of entries) {
|
|
97
|
+
if (!SUPPORTED_KINDS.has(entry))
|
|
98
|
+
continue;
|
|
99
|
+
const subdir = path.join(input.path, entry);
|
|
100
|
+
let subEntries;
|
|
101
|
+
try {
|
|
102
|
+
subEntries = await readdir(subdir);
|
|
103
|
+
}
|
|
104
|
+
catch {
|
|
105
|
+
continue;
|
|
106
|
+
}
|
|
107
|
+
for (const sub of subEntries) {
|
|
108
|
+
if (!sub.endsWith('.jsonl'))
|
|
109
|
+
continue;
|
|
110
|
+
const filePath = path.join(subdir, sub);
|
|
111
|
+
const text = await readFile(filePath, 'utf-8');
|
|
112
|
+
for (const line of text.split('\n')) {
|
|
113
|
+
if (!line.trim())
|
|
114
|
+
continue;
|
|
115
|
+
let record;
|
|
116
|
+
try {
|
|
117
|
+
record = JSON.parse(line);
|
|
118
|
+
}
|
|
119
|
+
catch {
|
|
120
|
+
continue;
|
|
121
|
+
}
|
|
122
|
+
// Tag with the dir-inferred kind so ambiguous records resolve.
|
|
123
|
+
if (!record.kind)
|
|
124
|
+
record.kind = entry;
|
|
125
|
+
const event = recordToEvent(record);
|
|
126
|
+
if (event)
|
|
127
|
+
events.push(event);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
return events;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
function recordToEvent(record) {
|
|
135
|
+
if (!record || typeof record !== 'object')
|
|
136
|
+
return null;
|
|
137
|
+
const ts = record.timestamp ?? record.ts;
|
|
138
|
+
if (typeof ts !== 'string')
|
|
139
|
+
return null;
|
|
140
|
+
const kind = record.kind;
|
|
141
|
+
if (kind === 'chat' || (!kind && typeof record.role === 'string')) {
|
|
142
|
+
if (typeof record.content !== 'string')
|
|
143
|
+
return null;
|
|
144
|
+
if (record.role === 'user') {
|
|
145
|
+
return { kind: 'user-message', timestamp: ts, content: record.content };
|
|
146
|
+
}
|
|
147
|
+
if (record.role === 'assistant') {
|
|
148
|
+
return { kind: 'assistant-message', timestamp: ts, content: record.content };
|
|
149
|
+
}
|
|
150
|
+
return null;
|
|
151
|
+
}
|
|
152
|
+
if (kind === 'edit' || (!kind && (record.filepath || record.path) && record.diff)) {
|
|
153
|
+
const filePath = record.filepath ?? record.path;
|
|
154
|
+
if (typeof filePath !== 'string')
|
|
155
|
+
return null;
|
|
156
|
+
if (typeof record.diff !== 'string')
|
|
157
|
+
return null;
|
|
158
|
+
return { kind: 'edit', timestamp: ts, path: filePath, diff: record.diff };
|
|
159
|
+
}
|
|
160
|
+
return null;
|
|
161
|
+
}
|
|
162
|
+
function inferKindFromPath(filePath) {
|
|
163
|
+
const parent = path.basename(path.dirname(filePath));
|
|
164
|
+
if (SUPPORTED_KINDS.has(parent))
|
|
165
|
+
return parent;
|
|
166
|
+
return null;
|
|
167
|
+
}
|
|
168
|
+
function tagChunkWithKind(text, kind) {
|
|
169
|
+
if (!kind)
|
|
170
|
+
return text;
|
|
171
|
+
// Splice a `"kind":"<kind>"` field into each JSON object. Cheap approach:
|
|
172
|
+
// re-parse + re-stringify each record to set the field. Avoids string-
|
|
173
|
+
// surgery edge cases on malformed lines.
|
|
174
|
+
return (text
|
|
175
|
+
.split('\n')
|
|
176
|
+
.map((line) => {
|
|
177
|
+
if (!line.trim())
|
|
178
|
+
return line;
|
|
179
|
+
try {
|
|
180
|
+
const obj = JSON.parse(line);
|
|
181
|
+
if (typeof obj.kind !== 'string')
|
|
182
|
+
obj.kind = kind;
|
|
183
|
+
return JSON.stringify(obj);
|
|
184
|
+
}
|
|
185
|
+
catch {
|
|
186
|
+
return line;
|
|
187
|
+
}
|
|
188
|
+
})
|
|
189
|
+
.join('\n'));
|
|
190
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cursor SQLite chat-store parser.
|
|
3
|
+
*
|
|
4
|
+
* Cursor stores its workspace chat history in a SQLite key-value DB at
|
|
5
|
+
* `<workspace>/.cursor/state.vscdb` (or `<state-dir>/User/workspaceStorage/
|
|
6
|
+
* <hash>/state.vscdb` for the global store). Chat data lives under the key
|
|
7
|
+
* `workbench.panel.aichat.view.aichat.chatdata` in the `ItemTable` row, with
|
|
8
|
+
* the value being a JSON blob structured as `{ tabs: [{ bubbles: [...] }] }`.
|
|
9
|
+
*
|
|
10
|
+
* Each bubble is one of:
|
|
11
|
+
* - { type: 'user' | 'human', text/content } → user-message
|
|
12
|
+
* - { type: 'ai' | 'assistant', text/content } → assistant-message
|
|
13
|
+
* - { type: 'tool_call', toolName, args } → tool-call
|
|
14
|
+
* - { type: 'tool_result', toolName, content } → tool-result
|
|
15
|
+
*
|
|
16
|
+
* Cursor's exact on-disk shape varies by version; this parser is best-effort
|
|
17
|
+
* and skips bubbles it doesn't recognise. Hand-crafted fixtures cover the
|
|
18
|
+
* canonical happy paths; no binary `.vscdb` files are committed (the test
|
|
19
|
+
* suite builds an SQLite DB at runtime via better-sqlite3).
|
|
20
|
+
*
|
|
21
|
+
* `parseChunk` is a no-op for SQLite — the watcher polls the DB and replays
|
|
22
|
+
* via `parseFull`. `parseFull` opens read-only, queries the chat-data row,
|
|
23
|
+
* decodes the JSON, and walks bubbles in order.
|
|
24
|
+
*
|
|
25
|
+
* Spec: spec/2026-05-07-telemetry-collector-and-task-generator.md §4.2 path B.
|
|
26
|
+
*/
|
|
27
|
+
import type { TranscriptEvent, TranscriptParser } from './types.js';
|
|
28
|
+
export declare class CursorSqliteParser implements TranscriptParser {
|
|
29
|
+
readonly tool: "cursor";
|
|
30
|
+
parseChunk(_input: {
|
|
31
|
+
sessionId: string;
|
|
32
|
+
chunk: Buffer | string;
|
|
33
|
+
}): TranscriptEvent[];
|
|
34
|
+
parseFull(input: {
|
|
35
|
+
sessionId: string;
|
|
36
|
+
path: string;
|
|
37
|
+
}): Promise<TranscriptEvent[]>;
|
|
38
|
+
}
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cursor SQLite chat-store parser.
|
|
3
|
+
*
|
|
4
|
+
* Cursor stores its workspace chat history in a SQLite key-value DB at
|
|
5
|
+
* `<workspace>/.cursor/state.vscdb` (or `<state-dir>/User/workspaceStorage/
|
|
6
|
+
* <hash>/state.vscdb` for the global store). Chat data lives under the key
|
|
7
|
+
* `workbench.panel.aichat.view.aichat.chatdata` in the `ItemTable` row, with
|
|
8
|
+
* the value being a JSON blob structured as `{ tabs: [{ bubbles: [...] }] }`.
|
|
9
|
+
*
|
|
10
|
+
* Each bubble is one of:
|
|
11
|
+
* - { type: 'user' | 'human', text/content } → user-message
|
|
12
|
+
* - { type: 'ai' | 'assistant', text/content } → assistant-message
|
|
13
|
+
* - { type: 'tool_call', toolName, args } → tool-call
|
|
14
|
+
* - { type: 'tool_result', toolName, content } → tool-result
|
|
15
|
+
*
|
|
16
|
+
* Cursor's exact on-disk shape varies by version; this parser is best-effort
|
|
17
|
+
* and skips bubbles it doesn't recognise. Hand-crafted fixtures cover the
|
|
18
|
+
* canonical happy paths; no binary `.vscdb` files are committed (the test
|
|
19
|
+
* suite builds an SQLite DB at runtime via better-sqlite3).
|
|
20
|
+
*
|
|
21
|
+
* `parseChunk` is a no-op for SQLite — the watcher polls the DB and replays
|
|
22
|
+
* via `parseFull`. `parseFull` opens read-only, queries the chat-data row,
|
|
23
|
+
* decodes the JSON, and walks bubbles in order.
|
|
24
|
+
*
|
|
25
|
+
* Spec: spec/2026-05-07-telemetry-collector-and-task-generator.md §4.2 path B.
|
|
26
|
+
*/
|
|
27
|
+
import Database from 'better-sqlite3';
|
|
28
|
+
const CHAT_DATA_KEY = 'workbench.panel.aichat.view.aichat.chatdata';
|
|
29
|
+
export class CursorSqliteParser {
|
|
30
|
+
tool = 'cursor';
|
|
31
|
+
parseChunk(_input) {
|
|
32
|
+
// SQLite is not a streaming format — the watcher polls and re-runs parseFull.
|
|
33
|
+
return [];
|
|
34
|
+
}
|
|
35
|
+
async parseFull(input) {
|
|
36
|
+
const db = new Database(input.path, { readonly: true, fileMustExist: true });
|
|
37
|
+
try {
|
|
38
|
+
const row = db
|
|
39
|
+
.prepare('SELECT value FROM ItemTable WHERE key = ?')
|
|
40
|
+
.get(CHAT_DATA_KEY);
|
|
41
|
+
if (!row || typeof row.value !== 'string')
|
|
42
|
+
return [];
|
|
43
|
+
let parsed;
|
|
44
|
+
try {
|
|
45
|
+
parsed = JSON.parse(row.value);
|
|
46
|
+
}
|
|
47
|
+
catch {
|
|
48
|
+
return [];
|
|
49
|
+
}
|
|
50
|
+
if (!parsed || !Array.isArray(parsed.tabs))
|
|
51
|
+
return [];
|
|
52
|
+
const events = [];
|
|
53
|
+
for (const tab of parsed.tabs) {
|
|
54
|
+
if (!tab || !Array.isArray(tab.bubbles))
|
|
55
|
+
continue;
|
|
56
|
+
for (const raw of tab.bubbles) {
|
|
57
|
+
const event = bubbleToEvent(raw);
|
|
58
|
+
if (event)
|
|
59
|
+
events.push(event);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
return events;
|
|
63
|
+
}
|
|
64
|
+
finally {
|
|
65
|
+
db.close();
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
function bubbleToEvent(raw) {
|
|
70
|
+
if (!raw || typeof raw !== 'object')
|
|
71
|
+
return null;
|
|
72
|
+
const bubble = raw;
|
|
73
|
+
const ts = bubble.timestamp ?? bubble.ts;
|
|
74
|
+
if (typeof ts !== 'string')
|
|
75
|
+
return null;
|
|
76
|
+
const type = bubble.type;
|
|
77
|
+
if (type === 'user' || type === 'human') {
|
|
78
|
+
const content = bubble.text ?? (typeof bubble.content === 'string' ? bubble.content : null);
|
|
79
|
+
if (typeof content !== 'string')
|
|
80
|
+
return null;
|
|
81
|
+
return { kind: 'user-message', timestamp: ts, content };
|
|
82
|
+
}
|
|
83
|
+
if (type === 'ai' || type === 'assistant') {
|
|
84
|
+
const content = bubble.text ?? (typeof bubble.content === 'string' ? bubble.content : null);
|
|
85
|
+
if (typeof content !== 'string')
|
|
86
|
+
return null;
|
|
87
|
+
return { kind: 'assistant-message', timestamp: ts, content };
|
|
88
|
+
}
|
|
89
|
+
if (type === 'tool_call') {
|
|
90
|
+
const name = bubble.toolName ?? bubble.name;
|
|
91
|
+
if (typeof name !== 'string')
|
|
92
|
+
return null;
|
|
93
|
+
return {
|
|
94
|
+
kind: 'tool-call',
|
|
95
|
+
timestamp: ts,
|
|
96
|
+
name,
|
|
97
|
+
args: bubble.args && typeof bubble.args === 'object' ? bubble.args : {},
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
if (type === 'tool_result') {
|
|
101
|
+
const name = bubble.toolName ?? bubble.name;
|
|
102
|
+
if (typeof name !== 'string')
|
|
103
|
+
return null;
|
|
104
|
+
const content = typeof bubble.content === 'string'
|
|
105
|
+
? bubble.content
|
|
106
|
+
: bubble.content == null
|
|
107
|
+
? ''
|
|
108
|
+
: JSON.stringify(bubble.content);
|
|
109
|
+
return {
|
|
110
|
+
kind: 'tool-result',
|
|
111
|
+
timestamp: ts,
|
|
112
|
+
name,
|
|
113
|
+
content,
|
|
114
|
+
isError: bubble.isError ?? false,
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
return null;
|
|
118
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Gemini CLI session transcript parser.
|
|
3
|
+
*
|
|
4
|
+
* Reads `~/.gemini/sessions/<id>.jsonl` (or comparable path) line-by-line,
|
|
5
|
+
* mapping per-record content to canonical TranscriptEvent shapes (Task 3.1).
|
|
6
|
+
*
|
|
7
|
+
* Gemini's content shape follows the GenerativeAI Content schema: each record
|
|
8
|
+
* has a `role` ('user' or 'model') and a `parts` array. Each part may be:
|
|
9
|
+
* - { text: string } → user-message / assistant-message
|
|
10
|
+
* - { functionCall: { name, args } } → tool-call
|
|
11
|
+
* - { functionResponse: { name, response } } → tool-result
|
|
12
|
+
*
|
|
13
|
+
* Multiple parts in a single record can produce multiple events, mirroring
|
|
14
|
+
* the Claude Code parser's tool_use-block handling. Text parts within the
|
|
15
|
+
* same model record are concatenated into a single assistant-message; mixing
|
|
16
|
+
* text and functionCall parts emits both, in order.
|
|
17
|
+
*
|
|
18
|
+
* `parseChunk` preserves any incomplete trailing line across calls.
|
|
19
|
+
*
|
|
20
|
+
* Spec: spec/2026-05-07-telemetry-collector-and-task-generator.md §4.2 path B.
|
|
21
|
+
*/
|
|
22
|
+
import type { TranscriptEvent, TranscriptParser } from './types.js';
|
|
23
|
+
export declare class GeminiSessionParser implements TranscriptParser {
|
|
24
|
+
readonly tool: "gemini-cli";
|
|
25
|
+
private buffer;
|
|
26
|
+
parseChunk(input: {
|
|
27
|
+
sessionId: string;
|
|
28
|
+
chunk: Buffer | string;
|
|
29
|
+
}): TranscriptEvent[];
|
|
30
|
+
parseFull(input: {
|
|
31
|
+
sessionId: string;
|
|
32
|
+
path: string;
|
|
33
|
+
}): Promise<TranscriptEvent[]>;
|
|
34
|
+
private recordToEvents;
|
|
35
|
+
}
|