@@ -139,12 +139,83 @@ function costModel(model: string): string {
139139 return model === 'cursor-agent-auto' ? CURSOR_AGENT_COST_MODEL : model
140140}
141141
142+ function transcriptStem ( transcriptPath : string ) : string {
143+ const name = basename ( transcriptPath )
144+ if ( name . endsWith ( '.jsonl' ) ) return name . slice ( 0 , - '.jsonl' . length )
145+ if ( name . endsWith ( '.txt' ) ) return name . slice ( 0 , - '.txt' . length )
146+ return name
147+ }
148+
142149function toConversationId ( transcriptPath : string ) : string {
143- const filename = basename ( transcriptPath , '.txt' )
150+ const filename = transcriptStem ( transcriptPath )
144151 if ( filename . length === 36 && UUID_LIKE . test ( filename ) ) return filename
145152 return createHash ( 'sha1' ) . update ( transcriptPath ) . digest ( 'hex' ) . slice ( 0 , 16 )
146153}
147154
155+ async function appendTranscriptSources (
156+ scanDir : string ,
157+ projectId : string ,
158+ sources : SessionSource [ ] ,
159+ ) : Promise < void > {
160+ const transcriptEntries = await readdir ( scanDir , { withFileTypes : true } )
161+ for ( const transcript of transcriptEntries ) {
162+ // Legacy format: .txt files directly in the scan dir
163+ if ( transcript . isFile ( ) && transcript . name . endsWith ( '.txt' ) ) {
164+ sources . push ( {
165+ path : join ( scanDir , transcript . name ) ,
166+ project : projectId ,
167+ provider : 'cursor-agent' ,
168+ } )
169+ continue
170+ }
171+
172+ // Composer 2 format: UUID subdirectories with .jsonl files
173+ if ( transcript . isDirectory ( ) && UUID_LIKE . test ( transcript . name ) ) {
174+ const subdir = join ( scanDir , transcript . name )
175+ const subEntries = await readdir ( subdir , { withFileTypes : true } ) . catch ( ( ) => [ ] )
176+ const transcriptFilesByStem = new Map < string , { jsonl ?: string ; txt ?: string } > ( )
177+
178+ for ( const sub of subEntries ) {
179+ if ( sub . isFile ( ) && ( sub . name . endsWith ( '.jsonl' ) || sub . name . endsWith ( '.txt' ) ) ) {
180+ const stem = transcriptStem ( sub . name )
181+ const existing = transcriptFilesByStem . get ( stem ) ?? { }
182+ if ( sub . name . endsWith ( '.jsonl' ) ) {
183+ transcriptFilesByStem . set ( stem , { ...existing , jsonl : sub . name } )
184+ } else {
185+ transcriptFilesByStem . set ( stem , { ...existing , txt : sub . name } )
186+ }
187+ continue
188+ }
189+
190+ // Subagent transcripts inside a subagents/ directory
191+ if ( sub . isDirectory ( ) && sub . name === 'subagents' ) {
192+ const subagentEntries = await readdir ( join ( subdir , sub . name ) , { withFileTypes : true } ) . catch ( ( ) => [ ] )
193+ for ( const sa of subagentEntries ) {
194+ if ( ! sa . isFile ( ) ) continue
195+ if ( ! sa . name . endsWith ( '.jsonl' ) && ! sa . name . endsWith ( '.txt' ) ) continue
196+ sources . push ( {
197+ path : join ( subdir , sub . name , sa . name ) ,
198+ project : projectId ,
199+ provider : 'cursor-agent' ,
200+ } )
201+ }
202+ }
203+ }
204+
205+ for ( const files of transcriptFilesByStem . values ( ) ) {
206+ const selectedName = files . jsonl ?? files . txt
207+ if ( selectedName ) {
208+ sources . push ( {
209+ path : join ( subdir , selectedName ) ,
210+ project : projectId ,
211+ provider : 'cursor-agent' ,
212+ } )
213+ }
214+ }
215+ }
216+ }
217+ }
218+
148219function extractUserQuery ( userBlock : string ) : string {
149220 const chunks : string [ ] = [ ]
150221 let cursor = 0
@@ -241,7 +312,7 @@ function parseTranscript(raw: string): { turns: ParsedTurn[]; recognized: boolea
241312
242313 let output = ''
243314 let reasoning = ''
244- const toolsByTurn : Record < string , boolean > = Object . create ( null )
315+ const toolsByTurn = new Map < string , true > ( )
245316
246317 for ( const line of assistantLines ) {
247318 if ( TOOL_RESULT_MARKER . test ( line ) ) continue
@@ -257,7 +328,7 @@ function parseTranscript(raw: string): { turns: ParsedTurn[]; recognized: boolea
257328 if ( toolMatch ) {
258329 const parsedTool = parseToolName ( toolMatch [ 1 ] ?? '' )
259330 const toolKey = `cursor:${ parsedTool } `
260- toolsByTurn [ toolKey ] = true
331+ toolsByTurn . set ( toolKey , true )
261332 continue
262333 }
263334
@@ -266,7 +337,7 @@ function parseTranscript(raw: string): { turns: ParsedTurn[]; recognized: boolea
266337
267338 if ( pendingUsers . length > 0 ) {
268339 const userMessage = pendingUsers . shift ( ) !
269- const tools = Object . keys ( toolsByTurn )
340+ const tools = Array . from ( toolsByTurn . keys ( ) )
270341 turns . push ( {
271342 userMessage,
272343 assistant : {
@@ -319,13 +390,13 @@ function createParser(
319390 source : SessionSource ,
320391 seenKeys : Set < string > ,
321392 dbPath : string ,
322- summariesByConversationId : Record < string , ConversationSummary | undefined > ,
393+ summariesByConversationId : Map < string , ConversationSummary > ,
323394) : SessionParser {
324395 return {
325396 async * parse ( ) : AsyncGenerator < ParsedProviderCall > {
326397 const conversationId = toConversationId ( source . path )
327398
328- let summary = summariesByConversationId [ conversationId ]
399+ let summary = summariesByConversationId . get ( conversationId )
329400 let db : SqliteDatabase | null = null
330401
331402 try {
@@ -348,7 +419,7 @@ function createParser(
348419 title : row . title ,
349420 updatedAt : normalizeTimestamp ( row . updatedAt ) ,
350421 }
351- summariesByConversationId [ conversationId ] = summary
422+ summariesByConversationId . set ( conversationId , summary )
352423 }
353424 } catch {
354425 summary = undefined
@@ -426,7 +497,7 @@ export function createCursorAgentProvider(baseDirOverride?: string): Provider {
426497 const baseDir = getCursorAgentBaseDir ( baseDirOverride )
427498 const projectsDir = getProjectsDir ( baseDir )
428499 const dbPath = getAttributionDbPath ( baseDir )
429- const summariesByConversationId : Record < string , ConversationSummary | undefined > = Object . create ( null )
500+ const summariesByConversationId = new Map < string , ConversationSummary > ( )
430501
431502 return {
432503 name : 'cursor-agent' ,
@@ -452,50 +523,15 @@ export function createCursorAgentProvider(baseDirOverride?: string): Provider {
452523 if ( ! entry . isDirectory ( ) ) continue
453524
454525 const projectId = prettifyProjectId ( entry . name )
455- const transcriptDir = join ( projectsDir , entry . name , 'agent-transcripts' )
456- if ( ! existsSync ( transcriptDir ) ) continue
457-
458- const transcriptEntries = await readdir ( transcriptDir , { withFileTypes : true } )
459- for ( const transcript of transcriptEntries ) {
460- // Legacy format: .txt files directly in agent-transcripts/
461- if ( transcript . isFile ( ) && transcript . name . endsWith ( '.txt' ) ) {
462- const transcriptPath = join ( transcriptDir , transcript . name )
463- sources . push ( {
464- path : transcriptPath ,
465- project : projectId ,
466- provider : 'cursor-agent' ,
467- } )
468- continue
469- }
470-
471- // Composer 2 format: UUID subdirectories with .jsonl files
472- if ( transcript . isDirectory ( ) && UUID_LIKE . test ( transcript . name ) ) {
473- const subdir = join ( transcriptDir , transcript . name )
474- const subEntries = await readdir ( subdir , { withFileTypes : true } ) . catch ( ( ) => [ ] )
475- for ( const sub of subEntries ) {
476- if ( sub . isFile ( ) && ( sub . name . endsWith ( '.jsonl' ) || sub . name . endsWith ( '.txt' ) ) ) {
477- sources . push ( {
478- path : join ( subdir , sub . name ) ,
479- project : projectId ,
480- provider : 'cursor-agent' ,
481- } )
482- }
483- // Subagent transcripts inside a subagents/ directory
484- if ( sub . isDirectory ( ) && sub . name === 'subagents' ) {
485- const subagentEntries = await readdir ( join ( subdir , sub . name ) , { withFileTypes : true } ) . catch ( ( ) => [ ] )
486- for ( const sa of subagentEntries ) {
487- if ( ! sa . isFile ( ) ) continue
488- if ( ! sa . name . endsWith ( '.jsonl' ) && ! sa . name . endsWith ( '.txt' ) ) continue
489- sources . push ( {
490- path : join ( subdir , sub . name , sa . name ) ,
491- project : projectId ,
492- provider : 'cursor-agent' ,
493- } )
494- }
495- }
496- }
497- }
526+ const projectDir = join ( projectsDir , entry . name )
527+ if ( entry . name === 'agent-transcripts' ) {
528+ await appendTranscriptSources ( projectDir , projectId , sources )
529+ continue
498530 }
531+
532+ const transcriptDir = join ( projectDir , 'agent-transcripts' )
533+ if ( ! existsSync ( transcriptDir ) ) continue
534+ await appendTranscriptSources ( transcriptDir , projectId , sources )
499535 }
500536
501537 return sources
0 commit comments