@tracebench/adapter-cursor 0.2.1 → 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.
Files changed (58) hide show
  1. package/README.md +9 -7
  2. package/dist/db-fixture.d.ts +2 -0
  3. package/dist/db-fixture.d.ts.map +1 -0
  4. package/dist/db-fixture.js +75 -0
  5. package/dist/db-fixture.js.map +1 -0
  6. package/dist/db-read.d.ts +26 -0
  7. package/dist/db-read.d.ts.map +1 -0
  8. package/dist/db-read.js +142 -0
  9. package/dist/db-read.js.map +1 -0
  10. package/dist/db-snapshot.d.ts +13 -0
  11. package/dist/db-snapshot.d.ts.map +1 -0
  12. package/dist/db-snapshot.js +34 -0
  13. package/dist/db-snapshot.js.map +1 -0
  14. package/dist/db-types.d.ts +70 -0
  15. package/dist/db-types.d.ts.map +1 -0
  16. package/dist/db-types.js +2 -0
  17. package/dist/db-types.js.map +1 -0
  18. package/dist/db-uri.d.ts +8 -0
  19. package/dist/db-uri.d.ts.map +1 -0
  20. package/dist/db-uri.js +22 -0
  21. package/dist/db-uri.js.map +1 -0
  22. package/dist/discover-db.d.ts +11 -0
  23. package/dist/discover-db.d.ts.map +1 -0
  24. package/dist/discover-db.js +19 -0
  25. package/dist/discover-db.js.map +1 -0
  26. package/dist/discover.d.ts +13 -1
  27. package/dist/discover.d.ts.map +1 -1
  28. package/dist/discover.js +38 -3
  29. package/dist/discover.js.map +1 -1
  30. package/dist/index.d.ts +8 -1
  31. package/dist/index.d.ts.map +1 -1
  32. package/dist/index.js +8 -1
  33. package/dist/index.js.map +1 -1
  34. package/dist/load-db.d.ts +5 -0
  35. package/dist/load-db.d.ts.map +1 -0
  36. package/dist/load-db.js +19 -0
  37. package/dist/load-db.js.map +1 -0
  38. package/dist/normalize-db.d.ts +10 -0
  39. package/dist/normalize-db.d.ts.map +1 -0
  40. package/dist/normalize-db.js +292 -0
  41. package/dist/normalize-db.js.map +1 -0
  42. package/dist/normalize.d.ts.map +1 -1
  43. package/dist/normalize.js +5 -0
  44. package/dist/normalize.js.map +1 -1
  45. package/package.json +3 -3
  46. package/src/db-fixture.ts +97 -0
  47. package/src/db-read.ts +199 -0
  48. package/src/db-snapshot.ts +41 -0
  49. package/src/db-types.ts +60 -0
  50. package/src/db-uri.ts +25 -0
  51. package/src/discover-db.ts +33 -0
  52. package/src/discover.test.ts +4 -2
  53. package/src/discover.ts +55 -3
  54. package/src/index.ts +13 -1
  55. package/src/load-db.ts +25 -0
  56. package/src/normalize-db.test.ts +88 -0
  57. package/src/normalize-db.ts +332 -0
  58. 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 {