@@ -8,6 +8,8 @@ import * as os from 'os';
88import chalk from 'chalk' ;
99import { SessionDiscovery } from '../../src/sessionDiscovery' ;
1010import { OpenCodeDataAccess } from '../../src/opencode' ;
11+ import { CrushDataAccess } from '../../src/crush' ;
12+ import { ContinueDataAccess } from '../../src/continue' ;
1113import { parseSessionFileContent } from '../../src/sessionParser' ;
1214import { estimateTokensFromText , getModelFromRequest , isJsonlContent , estimateTokensFromJsonlSession , calculateEstimatedCost , getModelTier } from '../../src/tokenEstimation' ;
1315import type { DetailedStats , PeriodStats , ModelUsage , EditorUsage , SessionFileCache , UsageAnalysisStats , UsageAnalysisPeriod } from '../../src/types' ;
@@ -40,10 +42,25 @@ function createOpenCode(): OpenCodeDataAccess {
4042 return new OpenCodeDataAccess ( fakeUri as any ) ;
4143}
4244
45+ /** Create Crush data access instance for CLI */
46+ function createCrush ( ) : CrushDataAccess {
47+ const fakeUri = vscodeStub . Uri . file ( __dirname ) ;
48+ return new CrushDataAccess ( fakeUri as any ) ;
49+ }
50+
51+ /** Create Continue data access instance for CLI */
52+ function createContinue ( ) : ContinueDataAccess {
53+ return new ContinueDataAccess ( ) ;
54+ }
55+
56+ // Module-level singletons so sql.js WASM is only initialised once across all session files
57+ const _openCodeInstance = createOpenCode ( ) ;
58+ const _crushInstance = createCrush ( ) ;
59+ const _continueInstance = createContinue ( ) ;
60+
4361/** Create session discovery instance for CLI */
4462function createSessionDiscovery ( ) : SessionDiscovery {
45- const openCode = createOpenCode ( ) ;
46- return new SessionDiscovery ( { log, warn, error, openCode } ) ;
63+ return new SessionDiscovery ( { log, warn, error, openCode : _openCodeInstance , crush : _crushInstance , continue_ : _continueInstance } ) ;
4764}
4865
4966/** Discover all session files on this machine */
@@ -80,10 +97,20 @@ function isOpenCodeDbSession(filePath: string): boolean {
8097}
8198
8299/**
83- * Stat a session file, handling OpenCode DB virtual paths.
84- * Virtual DB paths (opencode.db#ses_<id>) are resolved to the actual DB file.
100+ * Check if a session file path is a Crush DB virtual path.
101+ */
102+ function isCrushSessionFile ( filePath : string ) : boolean {
103+ return _crushInstance . isCrushSessionFile ( filePath ) ;
104+ }
105+
106+ /**
107+ * Stat a session file, handling DB virtual paths (OpenCode and Crush).
108+ * Virtual DB paths are resolved to the actual DB file.
85109 */
86110async function statSessionFile ( filePath : string ) : Promise < fs . Stats > {
111+ if ( isCrushSessionFile ( filePath ) ) {
112+ return _crushInstance . statSessionFile ( filePath ) ;
113+ }
87114 if ( isOpenCodeDbSession ( filePath ) ) {
88115 const dbPath = filePath . split ( '#' ) [ 0 ] ;
89116 return fs . promises . stat ( dbPath ) ;
@@ -99,6 +126,7 @@ function getEditorSourceFromPath(filePath: string): string {
99126 if ( normalized . includes ( '/code - exploration/' ) ) { return 'vscode-exploration' ; }
100127 if ( normalized . includes ( '/vscodium/' ) ) { return 'vscodium' ; }
101128 if ( normalized . includes ( '/.copilot/' ) ) { return 'copilot-cli' ; }
129+ if ( normalized . includes ( '/.crush/crush.db#' ) ) { return 'crush' ; }
102130 if ( normalized . includes ( '/opencode/' ) ) { return 'opencode' ; }
103131 if ( normalized . includes ( '.vscode-server' ) ) { return 'vscode-remote' ; }
104132 return 'vscode' ;
@@ -120,9 +148,40 @@ export interface SessionData {
120148export async function processSessionFile ( filePath : string ) : Promise < SessionData | null > {
121149 try {
122150 const stats = await statSessionFile ( filePath ) ;
123- const content = isOpenCodeDbSession ( filePath )
124- ? '' // OpenCode DB sessions are handled by the opencode module
125- : await fs . promises . readFile ( filePath , 'utf-8' ) ;
151+
152+ // Handle Crush DB virtual paths directly via the crush module
153+ if ( isCrushSessionFile ( filePath ) ) {
154+ const result = await _crushInstance . getTokensFromCrushSession ( filePath ) ;
155+ const interactions = await _crushInstance . countCrushInteractions ( filePath ) ;
156+ const modelUsage = await _crushInstance . getCrushModelUsage ( filePath ) ;
157+ return {
158+ file : filePath ,
159+ tokens : result . tokens ,
160+ thinkingTokens : result . thinkingTokens ,
161+ interactions,
162+ modelUsage,
163+ lastModified : stats . mtime ,
164+ editorSource : getEditorSourceFromPath ( filePath ) ,
165+ } ;
166+ }
167+
168+ // Handle OpenCode DB virtual paths directly via the opencode module
169+ if ( isOpenCodeDbSession ( filePath ) ) {
170+ const result = await _openCodeInstance . getTokensFromOpenCodeSession ( filePath ) ;
171+ const interactions = await _openCodeInstance . countOpenCodeInteractions ( filePath ) ;
172+ const modelUsage = await _openCodeInstance . getOpenCodeModelUsage ( filePath ) ;
173+ return {
174+ file : filePath ,
175+ tokens : result . tokens ,
176+ thinkingTokens : result . thinkingTokens ,
177+ interactions,
178+ modelUsage,
179+ lastModified : stats . mtime ,
180+ editorSource : getEditorSourceFromPath ( filePath ) ,
181+ } ;
182+ }
183+
184+ const content = await fs . promises . readFile ( filePath , 'utf-8' ) ;
126185
127186 if ( ! content . trim ( ) ) {
128187 return null ;
@@ -306,11 +365,10 @@ function aggregateIntoPeriod(period: PeriodStats, data: SessionData): void {
306365 * This is a simplified version that uses the shared usageAnalysis module.
307366 */
308367export async function calculateUsageAnalysisStats ( sessionFiles : string [ ] ) : Promise < UsageAnalysisStats > {
309- const openCode = createOpenCode ( ) ;
310-
311368 const deps = {
312369 warn,
313- openCode,
370+ openCode : _openCodeInstance ,
371+ crush : _crushInstance ,
314372 tokenEstimators,
315373 modelPricing,
316374 toolNameMap,
0 commit comments