(xcodeBase: string)
| 304 | } |
| 305 | |
| 306 | export function parseXcodeDatabases(xcodeBase: string): Session[] { |
| 307 | const sessions: Session[] = []; |
| 308 | const dbFiles = findXcodeDbFiles(xcodeBase); |
| 309 | |
| 310 | // Check sqlite3 is available (neutral cwd so the bare name can't resolve |
| 311 | // against an attacker-controlled workspace dir — see the query calls above). |
| 312 | try { execFileSync('sqlite3', ['--version'], { encoding: 'utf-8', ...SQLITE_PROBE_OPTS }); } |
| 313 | catch (e) { |
| 314 | console.debug('sqlite3 not available, skipping Xcode parsing:', e instanceof Error ? e.message : e); |
| 315 | return sessions; |
| 316 | } |
| 317 | |
| 318 | for (const dbPath of dbFiles) { |
| 319 | let conversations: XcodeConversationRow[]; |
| 320 | try { |
| 321 | const raw = sqliteQuery(dbPath, 'SELECT id, title, createdAt, updatedAt FROM Conversation'); |
| 322 | if (!raw.trim()) continue; |
| 323 | conversations = JSON.parse(raw) as XcodeConversationRow[]; |
| 324 | } catch (e) { |
| 325 | console.debug(`Failed to parse conversations from ${dbPath}:`, e instanceof Error ? e.message : e); |
| 326 | continue; |
| 327 | } |
| 328 | |
| 329 | for (const conv of conversations) { |
| 330 | // Validate conversation ID to prevent SQL injection |
| 331 | if (!/^[\w-]+$/.test(conv.id)) continue; |
| 332 | |
| 333 | let turns: XcodeTurnRow[]; |
| 334 | try { |
| 335 | const raw = sqliteQuery(dbPath, |
| 336 | `SELECT rowID, id, role, data, createdAt FROM Turn WHERE conversationID = ${sqlQuote(conv.id)} ORDER BY rowID`); |
| 337 | if (!raw.trim()) continue; |
| 338 | turns = JSON.parse(raw) as XcodeTurnRow[]; |
| 339 | } catch (e) { |
| 340 | console.debug(`Failed to parse turns for conversation ${conv.id}:`, e instanceof Error ? e.message : e); |
| 341 | continue; |
| 342 | } |
| 343 | |
| 344 | const session = buildXcodeSession(conv, turns, (turnId, error) => { |
| 345 | console.debug(`Failed to parse turn data for turn ${turnId}:`, error instanceof Error ? error.message : error); |
| 346 | }); |
| 347 | if (session) sessions.push(session); |
| 348 | } |
| 349 | } |
| 350 | |
| 351 | return sessions; |
| 352 | } |
| 353 | |
| 354 | /** Async version with per-conversation yields and non-blocking sqlite3 queries. */ |
| 355 | export async function parseXcodeDatabasesAsync(xcodeBase: string): Promise<Session[]> { |
no test coverage detected