@tracebench/adapter-cursor 0.2.2 → 0.2.5
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 +9 -7
- package/dist/db-fixture.d.ts +2 -0
- package/dist/db-fixture.d.ts.map +1 -0
- package/dist/db-fixture.js +75 -0
- package/dist/db-fixture.js.map +1 -0
- package/dist/db-read.d.ts +26 -0
- package/dist/db-read.d.ts.map +1 -0
- package/dist/db-read.js +142 -0
- package/dist/db-read.js.map +1 -0
- package/dist/db-snapshot.d.ts +13 -0
- package/dist/db-snapshot.d.ts.map +1 -0
- package/dist/db-snapshot.js +34 -0
- package/dist/db-snapshot.js.map +1 -0
- package/dist/db-types.d.ts +70 -0
- package/dist/db-types.d.ts.map +1 -0
- package/dist/db-types.js +2 -0
- package/dist/db-types.js.map +1 -0
- package/dist/db-uri.d.ts +8 -0
- package/dist/db-uri.d.ts.map +1 -0
- package/dist/db-uri.js +22 -0
- package/dist/db-uri.js.map +1 -0
- package/dist/discover-db.d.ts +11 -0
- package/dist/discover-db.d.ts.map +1 -0
- package/dist/discover-db.js +19 -0
- package/dist/discover-db.js.map +1 -0
- package/dist/discover.d.ts +13 -1
- package/dist/discover.d.ts.map +1 -1
- package/dist/discover.js +38 -3
- package/dist/discover.js.map +1 -1
- package/dist/index.d.ts +8 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +8 -1
- package/dist/index.js.map +1 -1
- package/dist/load-db.d.ts +5 -0
- package/dist/load-db.d.ts.map +1 -0
- package/dist/load-db.js +19 -0
- package/dist/load-db.js.map +1 -0
- package/dist/normalize-db.d.ts +10 -0
- package/dist/normalize-db.d.ts.map +1 -0
- package/dist/normalize-db.js +292 -0
- package/dist/normalize-db.js.map +1 -0
- package/dist/normalize.d.ts.map +1 -1
- package/dist/normalize.js +5 -0
- package/dist/normalize.js.map +1 -1
- package/package.json +3 -3
- package/src/db-fixture.ts +97 -0
- package/src/db-read.ts +199 -0
- package/src/db-snapshot.ts +41 -0
- package/src/db-types.ts +60 -0
- package/src/db-uri.ts +25 -0
- package/src/discover-db.ts +33 -0
- package/src/discover.test.ts +4 -2
- package/src/discover.ts +55 -3
- package/src/index.ts +13 -1
- package/src/load-db.ts +25 -0
- package/src/normalize-db.test.ts +88 -0
- package/src/normalize-db.ts +332 -0
- package/src/normalize.ts +6 -0
|
@@ -0,0 +1,332 @@
|
|
|
1
|
+
// Normalize Cursor Composer DB bubbles into canonical Tracebench events.
|
|
2
|
+
|
|
3
|
+
import type { CanonicalEvent, EventType, Session } from '@tracebench/core';
|
|
4
|
+
import type { CursorBubble } from './db-types.js';
|
|
5
|
+
import type { LoadedComposer } from './db-read.js';
|
|
6
|
+
import type { NormalizeResult } from './normalize.js';
|
|
7
|
+
|
|
8
|
+
export const FORMAT_VERSION_DB = '2026-q1-composer';
|
|
9
|
+
|
|
10
|
+
function emptyTool() {
|
|
11
|
+
return { name: null, input: null, output: null, status: null, error_message: null };
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const emptyTokens = {
|
|
15
|
+
input: null,
|
|
16
|
+
output: null,
|
|
17
|
+
cache_read: null,
|
|
18
|
+
cache_creation: null,
|
|
19
|
+
reasoning: null,
|
|
20
|
+
} as const;
|
|
21
|
+
|
|
22
|
+
function stripUserQuery(text: string | null): string | null {
|
|
23
|
+
if (text == null) return null;
|
|
24
|
+
const stripped = text.replace(/<\/?user_query>/gi, '').trim();
|
|
25
|
+
return stripped.length > 0 ? stripped : null;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function deriveTitle(text: string | null): string | null {
|
|
29
|
+
const s = stripUserQuery(text);
|
|
30
|
+
if (!s) return null;
|
|
31
|
+
const single = s.replace(/\s+/g, ' ').trim();
|
|
32
|
+
if (!single) return null;
|
|
33
|
+
return single.length > 120 ? single.slice(0, 117) + '...' : single;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function parseToolParams(params: string | undefined): Record<string, unknown> | null {
|
|
37
|
+
if (!params) return null;
|
|
38
|
+
try {
|
|
39
|
+
return JSON.parse(params) as Record<string, unknown>;
|
|
40
|
+
} catch {
|
|
41
|
+
return { raw: params };
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function parseToolResultOutput(
|
|
46
|
+
result: string | undefined,
|
|
47
|
+
): { output: string | Record<string, unknown> | null; status: 'success' | 'error' | null; error: string | null } {
|
|
48
|
+
if (!result) return { output: null, status: null, error: null };
|
|
49
|
+
try {
|
|
50
|
+
const parsed = JSON.parse(result) as Record<string, unknown>;
|
|
51
|
+
const rejected = parsed.rejected === true;
|
|
52
|
+
const output =
|
|
53
|
+
typeof parsed.output === 'string'
|
|
54
|
+
? parsed.output
|
|
55
|
+
: typeof parsed.content === 'string'
|
|
56
|
+
? parsed.content
|
|
57
|
+
: parsed;
|
|
58
|
+
return {
|
|
59
|
+
output,
|
|
60
|
+
status: rejected ? 'error' : 'success',
|
|
61
|
+
error: rejected ? String(parsed.reason ?? 'rejected') : null,
|
|
62
|
+
};
|
|
63
|
+
} catch {
|
|
64
|
+
return { output: result, status: 'success', error: null };
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function bubbleTimestamp(b: CursorBubble, fallbackMs: number): string {
|
|
69
|
+
if (b.createdAt) {
|
|
70
|
+
const t = Date.parse(b.createdAt);
|
|
71
|
+
if (!Number.isNaN(t)) return new Date(t).toISOString();
|
|
72
|
+
}
|
|
73
|
+
return new Date(fallbackMs).toISOString();
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function mapToolName(name: string | undefined): string | null {
|
|
77
|
+
if (!name) return null;
|
|
78
|
+
const map: Record<string, string> = {
|
|
79
|
+
ripgrep_raw_search: 'Grep',
|
|
80
|
+
run_terminal_command_v2: 'Bash',
|
|
81
|
+
read_file: 'Read',
|
|
82
|
+
edit_file: 'Edit',
|
|
83
|
+
write_file: 'Write',
|
|
84
|
+
list_dir: 'Glob',
|
|
85
|
+
web_search: 'WebSearch',
|
|
86
|
+
};
|
|
87
|
+
return map[name] ?? name;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export interface NormalizeDbOptions {
|
|
91
|
+
rawPath: string;
|
|
92
|
+
globalDbPath: string;
|
|
93
|
+
formatVersion?: string;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export function normalizeComposerSession(
|
|
97
|
+
loaded: LoadedComposer,
|
|
98
|
+
opts: NormalizeDbOptions,
|
|
99
|
+
): NormalizeResult {
|
|
100
|
+
const { composerId, data, bubbles } = loaded;
|
|
101
|
+
const eventFormatVersion = opts.formatVersion ?? FORMAT_VERSION_DB;
|
|
102
|
+
/** Stored on sessions row — matches harness FORMAT_VERSION for indexer skip logic. */
|
|
103
|
+
const sessionFormatVersion = '2026-q1';
|
|
104
|
+
|
|
105
|
+
const projectPath =
|
|
106
|
+
data?.workspaceIdentifier?.uri?.fsPath ??
|
|
107
|
+
data?.workspaceIdentifier?.uri?.path ??
|
|
108
|
+
null;
|
|
109
|
+
|
|
110
|
+
const modelName =
|
|
111
|
+
data?.modelConfig?.modelName ??
|
|
112
|
+
bubbles.find((b) => b.modelInfo?.modelName)?.modelInfo?.modelName ??
|
|
113
|
+
null;
|
|
114
|
+
|
|
115
|
+
const source = {
|
|
116
|
+
harness: 'cursor' as const,
|
|
117
|
+
format_version: eventFormatVersion,
|
|
118
|
+
raw_path: opts.rawPath,
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
const sessionMeta: Record<string, unknown> = {
|
|
122
|
+
source: 'composer_db',
|
|
123
|
+
global_db_path: opts.globalDbPath,
|
|
124
|
+
unified_mode: data?.unifiedMode ?? null,
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
const events: CanonicalEvent[] = [];
|
|
128
|
+
let turnIndex = 0;
|
|
129
|
+
let currentTurnId = `${composerId}::t0`;
|
|
130
|
+
const endMs = data?.lastUpdatedAt ?? data?.createdAt ?? Date.now();
|
|
131
|
+
const startMs = data?.createdAt ?? endMs;
|
|
132
|
+
|
|
133
|
+
function newTurn(): string {
|
|
134
|
+
turnIndex += 1;
|
|
135
|
+
currentTurnId = `${composerId}::t${turnIndex}`;
|
|
136
|
+
return currentTurnId;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function baseMeta(extra: Record<string, unknown> = {}): Record<string, unknown> {
|
|
140
|
+
return { ...sessionMeta, ...extra };
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
let firstUserText: string | null = null;
|
|
144
|
+
|
|
145
|
+
for (let i = 0; i < bubbles.length; i++) {
|
|
146
|
+
const b = bubbles[i]!;
|
|
147
|
+
const ts = bubbleTimestamp(b, endMs);
|
|
148
|
+
const bubbleType = b.type ?? 0;
|
|
149
|
+
|
|
150
|
+
if (bubbleType === 1) {
|
|
151
|
+
newTurn();
|
|
152
|
+
const content = stripUserQuery(b.text ?? null);
|
|
153
|
+
if (!firstUserText && content) firstUserText = content;
|
|
154
|
+
events.push({
|
|
155
|
+
event_id: `cursor:${composerId}:u:${b.bubbleId ?? i}`,
|
|
156
|
+
session_id: composerId,
|
|
157
|
+
turn_id: currentTurnId,
|
|
158
|
+
parent_event_id: null,
|
|
159
|
+
timestamp: ts,
|
|
160
|
+
source,
|
|
161
|
+
role: 'user',
|
|
162
|
+
event_type: 'message',
|
|
163
|
+
model: null,
|
|
164
|
+
tokens: { ...emptyTokens },
|
|
165
|
+
cost_usd: null,
|
|
166
|
+
cost_method: null,
|
|
167
|
+
duration_ms: null,
|
|
168
|
+
content,
|
|
169
|
+
tool: emptyTool(),
|
|
170
|
+
metadata: baseMeta({ bubble_id: b.bubbleId }),
|
|
171
|
+
raw: b as unknown as Record<string, unknown>,
|
|
172
|
+
});
|
|
173
|
+
continue;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
if (b.capabilityType === 30 && b.thinking?.text) {
|
|
177
|
+
events.push({
|
|
178
|
+
event_id: `cursor:${composerId}:think:${b.bubbleId ?? i}`,
|
|
179
|
+
session_id: composerId,
|
|
180
|
+
turn_id: currentTurnId,
|
|
181
|
+
parent_event_id: null,
|
|
182
|
+
timestamp: ts,
|
|
183
|
+
source,
|
|
184
|
+
role: 'assistant',
|
|
185
|
+
event_type: 'thinking',
|
|
186
|
+
model: modelName,
|
|
187
|
+
tokens: { ...emptyTokens },
|
|
188
|
+
cost_usd: null,
|
|
189
|
+
cost_method: null,
|
|
190
|
+
duration_ms: b.thinkingDurationMs ?? null,
|
|
191
|
+
content: b.thinking.text,
|
|
192
|
+
tool: emptyTool(),
|
|
193
|
+
metadata: baseMeta({ bubble_id: b.bubbleId }),
|
|
194
|
+
raw: b as unknown as Record<string, unknown>,
|
|
195
|
+
});
|
|
196
|
+
continue;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
const tool = b.toolFormerData;
|
|
200
|
+
if (b.capabilityType === 15 && tool?.toolCallId) {
|
|
201
|
+
const callId = tool.toolCallId;
|
|
202
|
+
const input = parseToolParams(tool.params ?? tool.rawArgs);
|
|
203
|
+
const toolName = mapToolName(tool.name);
|
|
204
|
+
|
|
205
|
+
events.push({
|
|
206
|
+
event_id: callId,
|
|
207
|
+
session_id: composerId,
|
|
208
|
+
turn_id: currentTurnId,
|
|
209
|
+
parent_event_id: null,
|
|
210
|
+
timestamp: ts,
|
|
211
|
+
source,
|
|
212
|
+
role: 'assistant',
|
|
213
|
+
event_type: 'tool_call',
|
|
214
|
+
model: modelName,
|
|
215
|
+
tokens: {
|
|
216
|
+
input: b.tokenCount?.inputTokens ?? null,
|
|
217
|
+
output: null,
|
|
218
|
+
cache_read: null,
|
|
219
|
+
cache_creation: null,
|
|
220
|
+
reasoning: null,
|
|
221
|
+
},
|
|
222
|
+
cost_usd: null,
|
|
223
|
+
cost_method: null,
|
|
224
|
+
duration_ms: null,
|
|
225
|
+
content: null,
|
|
226
|
+
tool: {
|
|
227
|
+
name: toolName,
|
|
228
|
+
input,
|
|
229
|
+
output: null,
|
|
230
|
+
status: null,
|
|
231
|
+
error_message: null,
|
|
232
|
+
},
|
|
233
|
+
metadata: baseMeta({
|
|
234
|
+
bubble_id: b.bubbleId,
|
|
235
|
+
tool_status: tool.status ?? null,
|
|
236
|
+
}),
|
|
237
|
+
raw: b as unknown as Record<string, unknown>,
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
const { output, status, error } = parseToolResultOutput(tool.result);
|
|
241
|
+
if (output != null || tool.status === 'completed' || tool.status === 'error') {
|
|
242
|
+
events.push({
|
|
243
|
+
event_id: `${callId}:result`,
|
|
244
|
+
session_id: composerId,
|
|
245
|
+
turn_id: currentTurnId,
|
|
246
|
+
parent_event_id: callId,
|
|
247
|
+
timestamp: ts,
|
|
248
|
+
source,
|
|
249
|
+
role: 'tool',
|
|
250
|
+
event_type: 'tool_result',
|
|
251
|
+
model: modelName,
|
|
252
|
+
tokens: {
|
|
253
|
+
input: null,
|
|
254
|
+
output: b.tokenCount?.outputTokens ?? null,
|
|
255
|
+
cache_read: null,
|
|
256
|
+
cache_creation: null,
|
|
257
|
+
reasoning: null,
|
|
258
|
+
},
|
|
259
|
+
cost_usd: null,
|
|
260
|
+
cost_method: null,
|
|
261
|
+
duration_ms: null,
|
|
262
|
+
content: null,
|
|
263
|
+
tool: {
|
|
264
|
+
name: toolName,
|
|
265
|
+
input: null,
|
|
266
|
+
output,
|
|
267
|
+
status: status ?? (tool.status === 'error' ? 'error' : tool.status === 'completed' ? 'success' : null),
|
|
268
|
+
error_message: error,
|
|
269
|
+
},
|
|
270
|
+
metadata: baseMeta({ bubble_id: b.bubbleId }),
|
|
271
|
+
raw: { result: tool.result ?? null },
|
|
272
|
+
});
|
|
273
|
+
}
|
|
274
|
+
continue;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
const text = (b.text ?? '').trim();
|
|
278
|
+
if (text) {
|
|
279
|
+
let evtType: EventType = 'message';
|
|
280
|
+
events.push({
|
|
281
|
+
event_id: `cursor:${composerId}:a:${b.bubbleId ?? i}`,
|
|
282
|
+
session_id: composerId,
|
|
283
|
+
turn_id: currentTurnId,
|
|
284
|
+
parent_event_id: null,
|
|
285
|
+
timestamp: ts,
|
|
286
|
+
source,
|
|
287
|
+
role: 'assistant',
|
|
288
|
+
event_type: evtType,
|
|
289
|
+
model: modelName,
|
|
290
|
+
tokens: {
|
|
291
|
+
input: b.tokenCount?.inputTokens ?? null,
|
|
292
|
+
output: b.tokenCount?.outputTokens ?? null,
|
|
293
|
+
cache_read: null,
|
|
294
|
+
cache_creation: null,
|
|
295
|
+
reasoning: null,
|
|
296
|
+
},
|
|
297
|
+
cost_usd: null,
|
|
298
|
+
cost_method: null,
|
|
299
|
+
duration_ms: null,
|
|
300
|
+
content: text,
|
|
301
|
+
tool: emptyTool(),
|
|
302
|
+
metadata: baseMeta({ bubble_id: b.bubbleId }),
|
|
303
|
+
raw: b as unknown as Record<string, unknown>,
|
|
304
|
+
});
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
const firstTs =
|
|
309
|
+
events[0]?.timestamp ?? new Date(startMs).toISOString();
|
|
310
|
+
const lastTs =
|
|
311
|
+
events[events.length - 1]?.timestamp ?? new Date(endMs).toISOString();
|
|
312
|
+
|
|
313
|
+
const title =
|
|
314
|
+
deriveTitle(firstUserText) ??
|
|
315
|
+
(data?.name && data.name !== 'New Chat' ? data.name : null) ??
|
|
316
|
+
deriveTitle(data?.subtitle ?? null);
|
|
317
|
+
|
|
318
|
+
const session: Session = {
|
|
319
|
+
session_id: composerId,
|
|
320
|
+
harness: 'cursor',
|
|
321
|
+
project_path: projectPath ?? 'unknown',
|
|
322
|
+
title,
|
|
323
|
+
started_at: firstTs,
|
|
324
|
+
ended_at: lastTs,
|
|
325
|
+
model: modelName,
|
|
326
|
+
raw_path: opts.rawPath,
|
|
327
|
+
format_version: sessionFormatVersion,
|
|
328
|
+
mtime_ms: 0,
|
|
329
|
+
};
|
|
330
|
+
|
|
331
|
+
return { session, events };
|
|
332
|
+
}
|
package/src/normalize.ts
CHANGED
|
@@ -307,11 +307,17 @@ export function normalizeSession(
|
|
|
307
307
|
|
|
308
308
|
import { parseSession } from './parse.js';
|
|
309
309
|
import { promises as fs } from 'node:fs';
|
|
310
|
+
import { isComposerDbUri } from './db-uri.js';
|
|
311
|
+
import { loadComposerSession } from './load-db.js';
|
|
310
312
|
|
|
311
313
|
export async function loadSession(
|
|
312
314
|
filePath: string,
|
|
313
315
|
opts: { formatVersion?: string; encodedProjectDir?: string } = {},
|
|
314
316
|
): Promise<NormalizeResult> {
|
|
317
|
+
if (isComposerDbUri(filePath)) {
|
|
318
|
+
return loadComposerSession(filePath, opts);
|
|
319
|
+
}
|
|
320
|
+
|
|
315
321
|
const raws = await parseSession(filePath);
|
|
316
322
|
let fileMtimeMs: number | undefined;
|
|
317
323
|
try {
|