@@ -2,10 +2,11 @@ import * as fs from 'fs';
22import * as path from 'path' ;
33import type { ModelUsage , ChatTurn } from '../types' ;
44import type { IEcosystemAdapter } from '../ecosystemAdapter' ;
5+ import type { IDiscoverableEcosystem , DiscoveryResult , CandidatePath } from '../ecosystemAdapter' ;
56import { OpenCodeDataAccess } from '../opencode' ;
67import { createEmptyContextRefs } from '../tokenEstimation' ;
78
8- export class OpenCodeAdapter implements IEcosystemAdapter {
9+ export class OpenCodeAdapter implements IEcosystemAdapter , IDiscoverableEcosystem {
910 readonly id = 'opencode' ;
1011 readonly displayName = 'OpenCode' ;
1112
@@ -80,6 +81,74 @@ export class OpenCodeAdapter implements IEcosystemAdapter {
8081 return this . openCode . getOpenCodeDataDir ( ) ;
8182 }
8283
84+ async discover ( log : ( msg : string ) => void ) : Promise < DiscoveryResult > {
85+ const candidatePaths = this . getCandidatePaths ( ) ;
86+ const sessionFiles : string [ ] = [ ] ;
87+ const dataDir = this . openCode . getOpenCodeDataDir ( ) ;
88+ const sessionDir = path . join ( dataDir , 'storage' , 'session' ) ;
89+ const dbPath = path . join ( dataDir , 'opencode.db' ) ;
90+
91+ // Scan JSON session files
92+ log ( `📁 Checking OpenCode JSON path: ${ sessionDir } ` ) ;
93+ log ( `📁 Checking OpenCode DB path: ${ dbPath } ` ) ;
94+ try {
95+ await fs . promises . access ( sessionDir ) ;
96+ const scanDir = async ( dir : string ) => {
97+ try {
98+ const entries = await fs . promises . readdir ( dir , { withFileTypes : true } ) ;
99+ for ( const entry of entries ) {
100+ if ( entry . isDirectory ( ) ) {
101+ await scanDir ( path . join ( dir , entry . name ) ) ;
102+ } else if ( entry . name . startsWith ( 'ses_' ) && entry . name . endsWith ( '.json' ) ) {
103+ const fullPath = path . join ( dir , entry . name ) ;
104+ try {
105+ const stats = await fs . promises . stat ( fullPath ) ;
106+ if ( stats . size > 0 ) { sessionFiles . push ( fullPath ) ; }
107+ } catch { /* ignore */ }
108+ }
109+ }
110+ } catch { /* ignore */ }
111+ } ;
112+ await scanDir ( sessionDir ) ;
113+ const jsonCount = sessionFiles . length ;
114+ if ( jsonCount > 0 ) {
115+ log ( `📄 Found ${ jsonCount } session files in OpenCode storage` ) ;
116+ }
117+ } catch { /* sessionDir doesn't exist — skip */ }
118+
119+ // Scan SQLite database for additional sessions (deduplicating against JSON)
120+ try {
121+ await fs . promises . access ( dbPath ) ;
122+ const existingIds = new Set (
123+ sessionFiles
124+ . filter ( f => this . openCode . isOpenCodeSessionFile ( f ) )
125+ . map ( f => this . openCode . getOpenCodeSessionId ( f ) )
126+ . filter ( Boolean )
127+ ) ;
128+ const dbSessionIds = await this . openCode . discoverOpenCodeDbSessions ( ) ;
129+ let dbNewCount = 0 ;
130+ for ( const sessionId of dbSessionIds ) {
131+ if ( ! existingIds . has ( sessionId ) ) {
132+ sessionFiles . push ( path . join ( dataDir , `opencode.db#${ sessionId } ` ) ) ;
133+ dbNewCount ++ ;
134+ }
135+ }
136+ if ( dbNewCount > 0 ) {
137+ log ( `📄 Found ${ dbNewCount } additional session(s) in OpenCode database` ) ;
138+ }
139+ } catch { /* DB doesn't exist — skip */ }
140+
141+ return { sessionFiles, candidatePaths } ;
142+ }
143+
144+ getCandidatePaths ( ) : CandidatePath [ ] {
145+ const dataDir = this . openCode . getOpenCodeDataDir ( ) ;
146+ return [
147+ { path : path . join ( dataDir , 'storage' , 'session' ) , source : 'OpenCode (JSON)' } ,
148+ { path : path . join ( dataDir , 'opencode.db' ) , source : 'OpenCode (DB)' } ,
149+ ] ;
150+ }
151+
83152 async buildTurns ( sessionFile : string ) : Promise < { turns : ChatTurn [ ] ; actualTokens ?: number } > {
84153 const turns : ChatTurn [ ] = [ ] ;
85154 const messages = await this . openCode . getOpenCodeMessagesForSession ( sessionFile ) ;
0 commit comments