@@ -14,6 +14,8 @@ import { VisualStudioDataAccess } from '../../vscode-extension/src/visualstudio'
1414import { ClaudeCodeDataAccess } from '../../vscode-extension/src/claudecode' ;
1515import { ClaudeDesktopCoworkDataAccess } from '../../vscode-extension/src/claudedesktop' ;
1616import { MistralVibeDataAccess } from '../../vscode-extension/src/mistralvibe' ;
17+ import type { IEcosystemAdapter } from '../../vscode-extension/src/ecosystemAdapter' ;
18+ import { OpenCodeAdapter , CrushAdapter , ContinueAdapter , ClaudeDesktopAdapter , ClaudeCodeAdapter , VisualStudioAdapter , MistralVibeAdapter } from '../../vscode-extension/src/adapters' ;
1719import { parseSessionFileContent } from '../../vscode-extension/src/sessionParser' ;
1820import { estimateTokensFromText , getModelFromRequest , isJsonlContent , estimateTokensFromJsonlSession , calculateEstimatedCost , getModelTier } from '../../vscode-extension/src/tokenEstimation' ;
1921import type { DetailedStats , PeriodStats , ModelUsage , EditorUsage , SessionFileCache , UsageAnalysisStats , UsageAnalysisPeriod } from '../../vscode-extension/src/types' ;
@@ -87,9 +89,20 @@ const _claudeCodeInstance = createClaudeCode();
8789const _claudeDesktopCoworkInstance = createClaudeDesktopCowork ( ) ;
8890const _mistralVibeInstance = createMistralVibe ( ) ;
8991
92+ /** Ordered registry of ecosystem adapters — first match wins. */
93+ const _ecosystems : IEcosystemAdapter [ ] = [
94+ new OpenCodeAdapter ( _openCodeInstance ) ,
95+ new CrushAdapter ( _crushInstance ) ,
96+ new VisualStudioAdapter ( _visualStudioInstance , ( t , m ) => estimateTokensFromText ( t , m ?? 'gpt-4' , tokenEstimators ) ) ,
97+ new ContinueAdapter ( _continueInstance ) ,
98+ new ClaudeDesktopAdapter ( _claudeDesktopCoworkInstance ) ,
99+ new ClaudeCodeAdapter ( _claudeCodeInstance ) ,
100+ new MistralVibeAdapter ( _mistralVibeInstance ) ,
101+ ] ;
102+
90103/** Create session discovery instance for CLI */
91104function createSessionDiscovery ( ) : SessionDiscovery {
92- return new SessionDiscovery ( { log, warn, error, openCode : _openCodeInstance , crush : _crushInstance , continue_ : _continueInstance , visualStudio : _visualStudioInstance , claudeCode : _claudeCodeInstance , claudeDesktopCowork : _claudeDesktopCoworkInstance , mistralVibe : _mistralVibeInstance } ) ;
105+ return new SessionDiscovery ( { log, warn, error, ecosystems : _ecosystems } ) ;
93106}
94107
95108/** Discover all session files on this machine */
@@ -118,32 +131,13 @@ function resolveModel(request: any): string {
118131 return getModelFromRequest ( request , modelPricing ) ;
119132}
120133
121- /**
122- * Check if a session file path is an OpenCode DB virtual path.
123- */
124- function isOpenCodeDbSession ( filePath : string ) : boolean {
125- return filePath . includes ( 'opencode.db#ses_' ) ;
126- }
127-
128- /**
129- * Check if a session file path is a Crush DB virtual path.
130- */
131- function isCrushSessionFile ( filePath : string ) : boolean {
132- return _crushInstance . isCrushSessionFile ( filePath ) ;
133- }
134-
135134/**
136135 * Stat a session file, handling DB virtual paths (OpenCode and Crush).
137136 * Virtual DB paths are resolved to the actual DB file.
138137 */
139138async function statSessionFile ( filePath : string ) : Promise < fs . Stats > {
140- if ( isCrushSessionFile ( filePath ) ) {
141- return _crushInstance . statSessionFile ( filePath ) ;
142- }
143- if ( isOpenCodeDbSession ( filePath ) ) {
144- const dbPath = filePath . split ( '#' ) [ 0 ] ;
145- return fs . promises . stat ( dbPath ) ;
146- }
139+ const eco = _ecosystems . find ( e => e . handles ( filePath ) ) ;
140+ if ( eco ) { return eco . stat ( filePath ) ; }
147141 return fs . promises . stat ( filePath ) ;
148142}
149143
@@ -210,111 +204,25 @@ export async function processSessionFile(filePath: string): Promise<SessionData
210204 return cached ;
211205 }
212206
213- // Handle Crush DB virtual paths directly via the crush module
214- if ( isCrushSessionFile ( filePath ) ) {
215- const result = await _crushInstance . getTokensFromCrushSession ( filePath ) ;
216- const interactions = await _crushInstance . countCrushInteractions ( filePath ) ;
217- const modelUsage = await _crushInstance . getCrushModelUsage ( filePath ) ;
218- const crushResult : SessionData = {
219- file : filePath ,
220- tokens : result . tokens ,
221- thinkingTokens : result . thinkingTokens ,
222- interactions,
223- modelUsage,
224- lastModified : stats . mtime ,
225- editorSource : getEditorSourceFromPath ( filePath ) ,
226- } ;
227- setCached ( filePath , stats . mtimeMs , stats . size , crushResult ) ;
228- return crushResult ;
229- }
230-
231- // Handle OpenCode DB virtual paths directly via the opencode module
232- if ( isOpenCodeDbSession ( filePath ) ) {
233- const result = await _openCodeInstance . getTokensFromOpenCodeSession ( filePath ) ;
234- const interactions = await _openCodeInstance . countOpenCodeInteractions ( filePath ) ;
235- const modelUsage = await _openCodeInstance . getOpenCodeModelUsage ( filePath ) ;
236- const openCodeResult : SessionData = {
207+ // Dispatch to ecosystem adapters (OpenCode, Crush, VS, Continue, ClaudeDesktop, ClaudeCode, MistralVibe)
208+ const eco = _ecosystems . find ( e => e . handles ( filePath ) ) ;
209+ if ( eco ) {
210+ const [ tokenResult , interactions , modelUsage ] = await Promise . all ( [
211+ eco . getTokens ( filePath ) ,
212+ eco . countInteractions ( filePath ) ,
213+ eco . getModelUsage ( filePath ) ,
214+ ] ) ;
215+ const ecoResult : SessionData = {
237216 file : filePath ,
238- tokens : result . tokens ,
239- thinkingTokens : result . thinkingTokens ,
217+ tokens : tokenResult . tokens ,
218+ thinkingTokens : tokenResult . thinkingTokens ,
240219 interactions,
241220 modelUsage,
242221 lastModified : stats . mtime ,
243222 editorSource : getEditorSourceFromPath ( filePath ) ,
244223 } ;
245- setCached ( filePath , stats . mtimeMs , stats . size , openCodeResult ) ;
246- return openCodeResult ;
247- }
248-
249- // Handle Visual Studio session files (binary MessagePack)
250- if ( _visualStudioInstance . isVSSessionFile ( filePath ) ) {
251- const result = _visualStudioInstance . getTokenEstimates ( filePath , estimateTokens ) ;
252- const objects = _visualStudioInstance . decodeSessionFile ( filePath ) ;
253- const interactions = _visualStudioInstance . countInteractions ( objects ) ;
254- const modelUsage = _visualStudioInstance . getModelUsage ( filePath , estimateTokens ) ;
255- const vsResult : SessionData = {
256- file : filePath ,
257- tokens : result . tokens ,
258- thinkingTokens : result . thinkingTokens ,
259- interactions,
260- modelUsage,
261- lastModified : stats . mtime ,
262- editorSource : getEditorSourceFromPath ( filePath ) ,
263- } ;
264- setCached ( filePath , stats . mtimeMs , stats . size , vsResult ) ;
265- return vsResult ;
266- }
267-
268- // Handle Claude Desktop Cowork sessions (JSONL with actual Anthropic API token counts)
269- if ( _claudeDesktopCoworkInstance . isCoworkSessionFile ( filePath ) ) {
270- const result = _claudeDesktopCoworkInstance . getTokensFromCoworkSession ( filePath ) ;
271- const interactions = _claudeDesktopCoworkInstance . countCoworkInteractions ( filePath ) ;
272- const modelUsage = _claudeDesktopCoworkInstance . getCoworkModelUsage ( filePath ) ;
273- const coworkResult : SessionData = {
274- file : filePath ,
275- tokens : result . tokens ,
276- thinkingTokens : result . thinkingTokens ,
277- interactions,
278- modelUsage,
279- lastModified : stats . mtime ,
280- editorSource : getEditorSourceFromPath ( filePath ) ,
281- } ;
282- setCached ( filePath , stats . mtimeMs , stats . size , coworkResult ) ;
283- return coworkResult ;
284- }
285-
286- // Handle Claude Code sessions (JSONL with actual Anthropic API token counts)
287- if ( _claudeCodeInstance . isClaudeCodeSessionFile ( filePath ) ) {
288- const result = _claudeCodeInstance . getTokensFromClaudeCodeSession ( filePath ) ;
289- const interactions = _claudeCodeInstance . countClaudeCodeInteractions ( filePath ) ;
290- const modelUsage = _claudeCodeInstance . getClaudeCodeModelUsage ( filePath ) ;
291- const claudeResult : SessionData = {
292- file : filePath ,
293- tokens : result . tokens ,
294- thinkingTokens : result . thinkingTokens ,
295- interactions,
296- modelUsage,
297- lastModified : stats . mtime ,
298- editorSource : getEditorSourceFromPath ( filePath ) ,
299- } ;
300- setCached ( filePath , stats . mtimeMs , stats . size , claudeResult ) ;
301- return claudeResult ;
302- }
303-
304- // Handle Mistral Vibe sessions (JSON meta.json with actual token counts in stats)
305- if ( _mistralVibeInstance . isVibeSessionFile ( filePath ) ) {
306- const sessionData = _mistralVibeInstance . getSessionData ( filePath ) ;
307- const vibeResult : SessionData = {
308- file : filePath ,
309- tokens : sessionData . tokens ,
310- thinkingTokens : 0 ,
311- interactions : sessionData . interactions ,
312- modelUsage : sessionData . modelUsage ,
313- lastModified : stats . mtime ,
314- editorSource : getEditorSourceFromPath ( filePath ) ,
315- } ;
316- setCached ( filePath , stats . mtimeMs , stats . size , vibeResult ) ;
317- return vibeResult ;
224+ setCached ( filePath , stats . mtimeMs , stats . size , ecoResult ) ;
225+ return ecoResult ;
318226 }
319227
320228 const content = await fs . promises . readFile ( filePath , 'utf-8' ) ;
@@ -505,15 +413,10 @@ function aggregateIntoPeriod(period: PeriodStats, data: SessionData): void {
505413export async function calculateUsageAnalysisStats ( sessionFiles : string [ ] ) : Promise < UsageAnalysisStats > {
506414 const deps = {
507415 warn,
508- openCode : _openCodeInstance ,
509- crush : _crushInstance ,
510- continue_ : _continueInstance ,
511- visualStudio : _visualStudioInstance ,
512- claudeCode : _claudeCodeInstance ,
513- claudeDesktopCowork : _claudeDesktopCoworkInstance ,
514416 tokenEstimators,
515417 modelPricing,
516418 toolNameMap,
419+ ecosystems : _ecosystems ,
517420 } ;
518421
519422 const now = new Date ( ) ;
0 commit comments