@@ -8,6 +8,7 @@ import { startServerIfEnabled, stopGlobalServer } from './server/kolosal-server-
88
99import type { Config } from '@kolosal-code/kolosal-code-core' ;
1010import {
11+ ApprovalMode ,
1112 AuthType ,
1213 FatalConfigError ,
1314 getOauthClient ,
@@ -229,6 +230,134 @@ export async function startInteractiveUI(
229230 registerCleanup ( ( ) => instance . unmount ( ) ) ;
230231}
231232
233+ export async function startServerOnly (
234+ config : Config ,
235+ settings : LoadedSettings ,
236+ workspaceRoot : string ,
237+ ) : Promise < void > {
238+ // Server-only mode - no UI, no interactive elements
239+ // Skip UI initialization, theme loading, and desktop integration
240+ // But keep essential authentication and client initialization
241+
242+ // CRITICAL: Initialize the config - this sets up contentGeneratorConfig
243+ await config . initialize ( ) ;
244+
245+ // Get authType from current model (needed for client initialization)
246+ const { getCurrentModelAuthType, getSavedModelEntry } = await import ( './config/savedModels.js' ) ;
247+ const currentModelName = settings . merged . model ?. name ;
248+ const savedModels = ( settings . merged . model ?. savedModels ?? [ ] ) as SavedModelEntry [ ] ;
249+ const currentAuthType = getCurrentModelAuthType ( currentModelName , savedModels ) ;
250+ const currentModelEntry = getSavedModelEntry ( currentModelName , savedModels ) ;
251+ const hasStoredApiKey = Boolean ( currentModelEntry ?. apiKey ?. trim ( ) ) ;
252+ const hasPersistedKolosalToken = Boolean (
253+ typeof settings . merged . kolosalOAuthToken === 'string' &&
254+ settings . merged . kolosalOAuthToken . trim ( ) ,
255+ ) ;
256+ const usesOpenAICompatibleProvider = currentModelEntry ?. provider === 'openai-compatible' ;
257+
258+ // Set approval mode to YOLO before creating the client to ensure all tools are available
259+ const originalApprovalMode = config . getApprovalMode ( ) ;
260+ config . setApprovalMode ( ApprovalMode . YOLO ) ;
261+
262+ // CRITICAL: Create the Gemini client by calling refreshAuth
263+ // This is what actually creates this.geminiClient
264+ try {
265+ await config . refreshAuth ( currentAuthType || AuthType . NO_AUTH ) ;
266+ } catch ( err ) {
267+ if ( config . getDebugMode ( ) ) {
268+ console . error ( '[server-only] Client initialization failed:' , err ) ;
269+ }
270+ // Continue anyway - some operations might work without auth
271+ }
272+
273+ // Restore original approval mode after client creation
274+ config . setApprovalMode ( originalApprovalMode ) ;
275+
276+ // Handle additional authentication if needed
277+ const shouldPreAuthenticate =
278+ currentAuthType === AuthType . USE_OPENAI &&
279+ config . isBrowserLaunchSuppressed ( ) &&
280+ ! hasStoredApiKey &&
281+ ! hasPersistedKolosalToken &&
282+ ! usesOpenAICompatibleProvider ;
283+
284+ if ( shouldPreAuthenticate ) {
285+ try {
286+ await getOauthClient ( currentAuthType , config ) ;
287+ } catch ( err ) {
288+ if ( config . getDebugMode ( ) ) {
289+ console . error ( '[server-only] Authentication failed:' , err ) ;
290+ }
291+ // Continue anyway - some operations might work without auth
292+ }
293+ }
294+
295+ // Start kolosal-server in the background if enabled
296+ const serverManager = await startServerIfEnabled ( {
297+ debug : config . getDebugMode ( ) ,
298+ autoStart : true ,
299+ port : 8087 ,
300+ } ) ;
301+
302+ // Register cleanup to stop the server when CLI exits
303+ if ( serverManager ) {
304+ registerCleanup ( async ( ) => {
305+ try {
306+ await stopGlobalServer ( ) ;
307+ } catch ( error ) {
308+ if ( config . getDebugMode ( ) ) {
309+ console . error ( 'Error stopping kolosal-server:' , error ) ;
310+ }
311+ }
312+ } ) ;
313+ }
314+
315+ // Start API server with forced enabled state for server-only mode
316+ try {
317+ const { startApiServer } = await import ( './api/server.js' ) ;
318+ const port = Number ( process . env [ 'KOLOSAL_CLI_API_PORT' ] ?? settings . merged . api ?. port ?? 38080 ) ;
319+ const host = process . env [ 'KOLOSAL_CLI_API_HOST' ] ?? settings . merged . api ?. host ?? '127.0.0.1' ;
320+ const authToken = process . env [ 'KOLOSAL_CLI_API_TOKEN' ] ?? settings . merged . api ?. token ;
321+
322+ const apiServer = await startApiServer ( config , {
323+ port,
324+ host,
325+ enableCors : true , // Always enable CORS for desktop app
326+ authToken,
327+ } ) ;
328+
329+ registerCleanup ( async ( ) => {
330+ try { await apiServer . close ( ) ; } catch { /* ignore */ }
331+ } ) ;
332+
333+ // Always log debug info in server-only mode for troubleshooting
334+ console . error ( `[server-only] API server listening on http://${ host } :${ port } ` ) ;
335+ console . error ( `[server-only] Model: ${ currentModelName } , Auth: ${ currentAuthType } ` ) ;
336+ console . error ( `[server-only] Excluded tools:` , config . getExcludeTools ( ) ) ;
337+ } catch ( e ) {
338+ console . error ( 'Failed to start API server in server-only mode:' , e ) ;
339+ throw e ;
340+ }
341+
342+ // Keep the process alive and handle graceful shutdown
343+ const shutdown = async ( ) => {
344+ if ( config . getDebugMode ( ) ) {
345+ console . error ( '[server-only] Shutting down...' ) ;
346+ }
347+ await runExitCleanup ( ) ;
348+ process . exit ( 0 ) ;
349+ } ;
350+
351+ process . once ( 'SIGINT' , shutdown ) ;
352+ process . once ( 'SIGTERM' , shutdown ) ;
353+ process . once ( 'SIGUSR2' , shutdown ) ; // For nodemon
354+
355+ // Log startup completion only in debug mode
356+ if ( config . getDebugMode ( ) ) {
357+ console . error ( `[server-only] Kolosal CLI server ready on port ${ process . env [ 'KOLOSAL_CLI_API_PORT' ] ?? 38080 } ` ) ;
358+ }
359+ }
360+
232361export async function main ( ) {
233362 setupProcessExitHandlers ( ) ;
234363 setupUnhandledRejectionHandler ( ) ;
@@ -289,6 +418,21 @@ export async function main() {
289418
290419 setMaxSizedBoxDebugging ( config . getDebugMode ( ) ) ;
291420
421+ // Check for server-only mode first (before config.initialize())
422+ if ( argv . serverOnly ) {
423+ // Override API settings for server-only mode
424+ process . env [ 'KOLOSAL_CLI_API' ] = 'true' ; // Force API enabled
425+ if ( argv . apiPort ) {
426+ process . env [ 'KOLOSAL_CLI_API_PORT' ] = argv . apiPort . toString ( ) ;
427+ }
428+ if ( argv . apiHost ) {
429+ process . env [ 'KOLOSAL_CLI_API_HOST' ] = argv . apiHost ;
430+ }
431+
432+ await startServerOnly ( config , settings , workspaceRoot ) ;
433+ return ; // Exit early for server-only mode
434+ }
435+
292436 await config . initialize ( ) ;
293437
294438 // Optionally start lightweight HTTP API server to expose generation endpoints
@@ -301,8 +445,8 @@ export async function main() {
301445 if ( apiEnabled ) {
302446 try {
303447 const { startApiServer } = await import ( './api/server.js' ) ;
304- const port = Number ( process . env [ 'KOLOSAL_CLI_API_PORT' ] ?? settings . merged . api ?. port ?? 38080 ) ;
305- const host = process . env [ 'KOLOSAL_CLI_API_HOST' ] ?? settings . merged . api ?. host ?? '127.0.0.1' ;
448+ const port = Number ( argv . apiPort ?? process . env [ 'KOLOSAL_CLI_API_PORT' ] ?? settings . merged . api ?. port ?? 38080 ) ;
449+ const host = argv . apiHost ?? process . env [ 'KOLOSAL_CLI_API_HOST' ] ?? settings . merged . api ?. host ?? '127.0.0.1' ;
306450 const authToken = process . env [ 'KOLOSAL_CLI_API_TOKEN' ] ?? settings . merged . api ?. token ;
307451 const corsEnabled = ( process . env [ 'KOLOSAL_CLI_API_CORS' ] ?? '' )
308452 ? [ '1' , 'true' , 'yes' ] . includes ( String ( process . env [ 'KOLOSAL_CLI_API_CORS' ] ) . toLowerCase ( ) )
0 commit comments