(dateRange?: DateRange, providerFilter?: string)
| 2341 | } |
| 2342 | |
| 2343 | export async function parseAllSessions(dateRange?: DateRange, providerFilter?: string): Promise<ProjectSummary[]> { |
| 2344 | const key = cacheKey(dateRange, providerFilter) |
| 2345 | const cached = sessionCache.get(key) |
| 2346 | if (cached && Date.now() - cached.ts < CACHE_TTL_MS) return cached.data |
| 2347 | |
| 2348 | const diskCache = await loadCache() |
| 2349 | await cleanupOrphanedTempFiles() |
| 2350 | |
| 2351 | const seenMsgIds = new Set<string>() |
| 2352 | const seenKeys = new Set<string>() |
| 2353 | const allSources = await discoverAllSessions(providerFilter) |
| 2354 | |
| 2355 | const claudeSources = allSources.filter(s => s.provider === 'claude') |
| 2356 | const nonClaudeSources = allSources.filter(s => s.provider !== 'claude') |
| 2357 | |
| 2358 | const claudeDirs = claudeSources.map(s => ({ path: s.path, name: s.project })) |
| 2359 | const claudeProjects = await scanProjectDirs(claudeDirs, seenMsgIds, diskCache, dateRange) |
| 2360 | |
| 2361 | const providerGroups = new Map<string, SessionSource[]>() |
| 2362 | for (const source of nonClaudeSources) { |
| 2363 | const existing = providerGroups.get(source.provider) ?? [] |
| 2364 | existing.push(source) |
| 2365 | providerGroups.set(source.provider, existing) |
| 2366 | } |
| 2367 | |
| 2368 | const otherProjects: ProjectSummary[] = [] |
| 2369 | for (const [providerName, sources] of providerGroups) { |
| 2370 | const projects = await parseProviderSources(providerName, sources, seenKeys, diskCache, dateRange) |
| 2371 | otherProjects.push(...projects) |
| 2372 | } |
| 2373 | |
| 2374 | // Durable providers with cached data but NO discovered sources (all files pruned |
| 2375 | // by VS Code / the external tool) still need their orphan pass to run so the |
| 2376 | // monthly total never drops. Call parseProviderSources with empty sources for |
| 2377 | // any such provider found in the disk cache. |
| 2378 | const processedProviders = new Set(providerGroups.keys()) |
| 2379 | for (const providerName of Object.keys(diskCache.providers)) { |
| 2380 | if (processedProviders.has(providerName)) continue |
| 2381 | // Skip if filtered to a different provider |
| 2382 | if (providerFilter && providerFilter !== 'all' && providerFilter !== providerName) continue |
| 2383 | const section = diskCache.providers[providerName] |
| 2384 | if (!section || Object.keys(section.files).length === 0) continue |
| 2385 | // Use the persisted durable flag (set by parseProviderSources when it first |
| 2386 | // processes a durableSources provider) OR the static DURABLE_PROVIDER_NAMES |
| 2387 | // constant — both checks are O(1) and avoid a getProvider() dynamic-import |
| 2388 | // round-trip for every unprocessed provider in the disk cache. |
| 2389 | if (!section.durable && !DURABLE_PROVIDER_NAMES.has(providerName)) continue |
| 2390 | const projects = await parseProviderSources(providerName, [], seenKeys, diskCache, dateRange) |
| 2391 | otherProjects.push(...projects) |
| 2392 | } |
| 2393 | |
| 2394 | if ((diskCache as { _dirty?: boolean })._dirty) { |
| 2395 | try { await saveCache(diskCache) } catch {} |
| 2396 | } |
| 2397 | |
| 2398 | // Merge across providers by normalised project path so the same repository |
| 2399 | // is not double-counted when it was worked on with more than one tool |
| 2400 | // (e.g. both Claude Code and Codex). Two sub-problems: |
no test coverage detected