Download packages/cli/src/gemini.tsx from SaylorTwift/gemini-cli: direct link, hf CLI and curl.
- Browser
- Download file 31.7 kB
-
https://huggingface.co/SaylorTwift/gemini-cli/resolve/main/packages/cli/src/gemini.tsx
- Command line
-
hf download hf://SaylorTwift/gemini-cli/packages/cli/src/gemini.tsx
-
curl -L -o gemini.tsx https://huggingface.co/SaylorTwift/gemini-cli/resolve/main/packages/cli/src/gemini.tsx
31.7 kB
| /** | |
| * @license | |
| * Copyright 2025 Google LLC | |
| * SPDX-License-Identifier: Apache-2.0 | |
| */ | |
| import { | |
| type StartupWarning, | |
| WarningPriority, | |
| type Config, | |
| type ResumedSessionData, | |
| type WorktreeInfo, | |
| type OutputPayload, | |
| type ConsoleLogPayload, | |
| type UserFeedbackPayload, | |
| type CoreEvents, | |
| createSessionId, | |
| logUserPrompt, | |
| AuthType, | |
| UserPromptEvent, | |
| coreEvents, | |
| CoreEvent, | |
| getOauthClient, | |
| patchStdio, | |
| writeToStdout, | |
| writeToStderr, | |
| shouldEnterAlternateScreen, | |
| startupProfiler, | |
| ExitCodes, | |
| SessionStartSource, | |
| SessionEndReason, | |
| ValidationCancelledError, | |
| ValidationRequiredError, | |
| type AdminControlsSettings, | |
| debugLogger, | |
| isHeadlessMode, | |
| Storage, | |
| getProjectHash, | |
| loadConversationRecord, | |
| type MessageRecord, | |
| } from '@google/gemini-cli-core'; | |
| import { loadCliConfig, parseArguments } from './config/config.js'; | |
| import * as cliConfig from './config/config.js'; | |
| import { readStdin } from './utils/readStdin.js'; | |
| import { createHash } from 'node:crypto'; | |
| import v8 from 'node:v8'; | |
| import os from 'node:os'; | |
| import dns from 'node:dns'; | |
| import * as path from 'node:path'; | |
| import * as fsPromises from 'node:fs/promises'; | |
| import { start_sandbox } from './utils/sandbox.js'; | |
| import { | |
| loadSettings, | |
| SettingScope, | |
| type DnsResolutionOrder, | |
| type LoadedSettings, | |
| } from './config/settings.js'; | |
| import { | |
| loadTrustedFolders, | |
| type TrustedFoldersError, | |
| } from './config/trustedFolders.js'; | |
| import { getStartupWarnings } from './utils/startupWarnings.js'; | |
| import { getUserStartupWarnings } from './utils/userStartupWarnings.js'; | |
| import { ConsolePatcher } from './ui/utils/ConsolePatcher.js'; | |
| import { runNonInteractive } from './nonInteractiveCli.js'; | |
| import { | |
| cleanupCheckpoints, | |
| registerCleanup, | |
| registerSyncCleanup, | |
| runExitCleanup, | |
| registerTelemetryConfig, | |
| setupSignalHandlers, | |
| } from './utils/cleanup.js'; | |
| import { setupWorktree } from './utils/worktreeSetup.js'; | |
| import { | |
| cleanupToolOutputFiles, | |
| cleanupExpiredSessions, | |
| } from './utils/sessionCleanup.js'; | |
| import { | |
| initializeApp, | |
| type InitializationResult, | |
| } from './core/initializer.js'; | |
| import { validateAuthMethod } from './config/auth.js'; | |
| import { runAcpClient } from './acp/acpStdioTransport.js'; | |
| import { validateNonInteractiveAuth } from './validateNonInterActiveAuth.js'; | |
| import { appEvents, AppEvent } from './utils/events.js'; | |
| import { | |
| RESUME_LATEST, | |
| SessionError, | |
| SessionSelector, | |
| } from './utils/sessionUtils.js'; | |
| import { relaunchOnExitCode } from './utils/relaunch.js'; | |
| import { loadSandboxConfig } from './config/sandboxConfig.js'; | |
| import { deleteSession, listSessions } from './utils/sessions.js'; | |
| import { createPolicyUpdater } from './config/policy.js'; | |
| import { setupTerminalAndTheme } from './utils/terminalTheme.js'; | |
| import { runDeferredCommand } from './deferred.js'; | |
| import { cleanupBackgroundLogs } from './utils/logCleanup.js'; | |
| import { SlashCommandConflictHandler } from './services/SlashCommandConflictHandler.js'; | |
| export function validateDnsResolutionOrder( | |
| order: string | undefined, | |
| ): DnsResolutionOrder { | |
| const defaultValue: DnsResolutionOrder = 'ipv4first'; | |
| if (order === undefined) { | |
| return defaultValue; | |
| } | |
| if (order === 'ipv4first' || order === 'verbatim') { | |
| return order; | |
| } | |
| // We don't want to throw here, just warn and use the default. | |
| debugLogger.warn( | |
| `Invalid value for dnsResolutionOrder in settings: "${order}". Using default "${defaultValue}".`, | |
| ); | |
| return defaultValue; | |
| } | |
| const DEFAULT_EPT_SIZE = (256 * 1024 * 1024).toString(); | |
| export function getNodeMemoryArgs(isDebugMode: boolean): string[] { | |
| const totalMemoryMB = os.totalmem() / (1024 * 1024); | |
| const heapStats = v8.getHeapStatistics(); | |
| const currentMaxOldSpaceSizeMb = Math.floor( | |
| heapStats.heap_size_limit / 1024 / 1024, | |
| ); | |
| // Set target to 50% of total memory | |
| const targetMaxOldSpaceSizeInMB = Math.floor(totalMemoryMB * 0.5); | |
| if (isDebugMode) { | |
| debugLogger.debug( | |
| `Current heap size ${currentMaxOldSpaceSizeMb.toFixed(2)} MB`, | |
| ); | |
| } | |
| if (process.env['GEMINI_CLI_NO_RELAUNCH']) { | |
| return []; | |
| } | |
| const args: string[] = []; | |
| // Automatically expand the V8 External Pointer Table to 256MB to prevent | |
| // out-of-memory crashes during high native-handle concurrency. | |
| // Note: Only supported in specific Node.js versions compiled with V8 Sandbox enabled. | |
| const eptFlag = `--max-external-pointer-table-size=${DEFAULT_EPT_SIZE}`; | |
| const isV8SandboxEnabled = | |
| // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-type-assertion | |
| (process.config?.variables as any)?.v8_enable_sandbox === 1; | |
| if ( | |
| isV8SandboxEnabled && | |
| !process.execArgv.some((arg) => | |
| arg.startsWith('--max-external-pointer-table-size'), | |
| ) | |
| ) { | |
| args.push(eptFlag); | |
| } | |
| if (targetMaxOldSpaceSizeInMB > currentMaxOldSpaceSizeMb) { | |
| if (isDebugMode) { | |
| debugLogger.debug( | |
| `Need to relaunch with more memory: ${targetMaxOldSpaceSizeInMB.toFixed(2)} MB`, | |
| ); | |
| } | |
| args.push(`--max-old-space-size=${targetMaxOldSpaceSizeInMB}`); | |
| } | |
| return args; | |
| } | |
| export function setupUnhandledRejectionHandler() { | |
| let unhandledRejectionOccurred = false; | |
| process.on('unhandledRejection', (reason, _promise) => { | |
| // AbortError is expected when the user cancels a request (e.g. pressing ESC). | |
| // It may surface as an unhandled rejection due to async timing in the | |
| // streaming pipeline, but it is not a bug. | |
| if (reason instanceof Error && reason.name === 'AbortError') { | |
| debugLogger.log(`Suppressed unhandled AbortError: ${reason.message}`); | |
| return; | |
| } | |
| const errorMessage = `========================================= | |
| This is an unexpected error. Please file a bug report using the /bug tool. | |
| CRITICAL: Unhandled Promise Rejection! | |
| ========================================= | |
| Reason: ${reason}${ | |
| reason instanceof Error && reason.stack | |
| ? ` | |
| Stack trace: | |
| ${reason.stack}` | |
| : '' | |
| }`; | |
| debugLogger.error(errorMessage); | |
| if (!unhandledRejectionOccurred) { | |
| unhandledRejectionOccurred = true; | |
| appEvents.emit(AppEvent.OpenDebugConsole); | |
| } | |
| }); | |
| } | |
| export async function resolveSessionId( | |
| resumeArg: string | undefined, | |
| sessionIdArg?: string | undefined, | |
| sessionFileArg?: string | undefined, | |
| ): Promise<{ | |
| sessionId: string; | |
| resumedSessionData?: ResumedSessionData; | |
| }> { | |
| if (!resumeArg && !sessionIdArg && !sessionFileArg) { | |
| return { sessionId: createSessionId() }; | |
| } | |
| const storage = new Storage(process.cwd()); | |
| await storage.initialize(); | |
| const sessionSelector = new SessionSelector(storage); | |
| if (sessionFileArg) { | |
| try { | |
| const sessionData = await loadConversationRecord(sessionFileArg); | |
| if (!sessionData) { | |
| throw new Error(`File not found or invalid format: ${sessionFileArg}`); | |
| } | |
| const now = Date.now(); | |
| const isoNow = new Date(now).toISOString(); | |
| // Filter out old system/info messages that are specific to the previous run | |
| // and only keep actual conversation messages (user/gemini). | |
| // Best effort parse: ensure message is an object and has required fields. | |
| sessionData.messages = (sessionData.messages || []).filter( | |
| (m) => | |
| typeof m === 'object' && | |
| m !== null && | |
| (m.type === 'user' || m.type === 'gemini') && | |
| m.content !== undefined, | |
| ); | |
| // Add a single info message to the history to confirm the import | |
| sessionData.messages.unshift({ | |
| id: `import-${now}`, | |
| type: 'info', | |
| content: `Imported session from ${sessionFileArg}`, | |
| timestamp: isoNow, | |
| } as MessageRecord); | |
| const newSessionId = createSessionId(); | |
| sessionData.sessionId = newSessionId; | |
| sessionData.projectHash = getProjectHash(storage.getProjectRoot()); | |
| sessionData.startTime = isoNow; | |
| sessionData.lastUpdated = isoNow; | |
| const chatsDir = path.join(storage.getProjectTempDir(), 'chats'); | |
| const newSessionPath = path.join( | |
| chatsDir, | |
| `session-${now}-${newSessionId.slice(0, 8)}.jsonl`, | |
| ); | |
| const { messages: _messages, ...initialMetadata } = sessionData; | |
| const lines = [JSON.stringify(initialMetadata)]; | |
| if (sessionData.messages) { | |
| for (const msg of sessionData.messages) { | |
| lines.push(JSON.stringify(msg)); | |
| } | |
| } | |
| await fsPromises.mkdir(chatsDir, { recursive: true }); | |
| await fsPromises.writeFile( | |
| newSessionPath, | |
| lines.join('\n') + '\n', | |
| 'utf-8', | |
| ); | |
| return { | |
| sessionId: newSessionId, | |
| resumedSessionData: { | |
| conversation: sessionData, | |
| filePath: newSessionPath, | |
| }, | |
| }; | |
| } catch (error) { | |
| coreEvents.emitFeedback( | |
| 'error', | |
| `Error importing session from file: ${error instanceof Error ? error.message : 'Unknown error'}`, | |
| ); | |
| await runExitCleanup(); | |
| process.exit(ExitCodes.FATAL_INPUT_ERROR); | |
| } | |
| } | |
| if (sessionIdArg) { | |
| if (await sessionSelector.sessionExists(sessionIdArg)) { | |
| coreEvents.emitFeedback( | |
| 'error', | |
| `Error starting session: Session ID "${sessionIdArg}" already exists. Use --resume to resume it, or provide a different ID.`, | |
| ); | |
| await runExitCleanup(); | |
| process.exit(ExitCodes.FATAL_INPUT_ERROR); | |
| } | |
| return { sessionId: sessionIdArg }; | |
| } | |
| try { | |
| const { sessionData, sessionPath } = await sessionSelector.resolveSession( | |
| resumeArg!, | |
| ); | |
| return { | |
| sessionId: sessionData.sessionId, | |
| resumedSessionData: { conversation: sessionData, filePath: sessionPath }, | |
| }; | |
| } catch (error) { | |
| if (error instanceof SessionError && error.code === 'NO_SESSIONS_FOUND') { | |
| if (resumeArg === RESUME_LATEST) { | |
| coreEvents.emitFeedback('warning', error.message); | |
| return { sessionId: createSessionId() }; | |
| } | |
| } | |
| coreEvents.emitFeedback( | |
| 'error', | |
| `Error resuming session: ${error instanceof Error ? error.message : 'Unknown error'}`, | |
| ); | |
| await runExitCleanup(); | |
| process.exit(ExitCodes.FATAL_INPUT_ERROR); | |
| } | |
| } | |
| export async function startInteractiveUI( | |
| config: Config, | |
| settings: LoadedSettings, | |
| startupWarnings: StartupWarning[], | |
| workspaceRoot: string = process.cwd(), | |
| resumedSessionData: ResumedSessionData | undefined, | |
| initializationResult: InitializationResult, | |
| ) { | |
| // Dynamically import the heavy UI module so React/Ink are only parsed when needed | |
| const { startInteractiveUI: doStartUI } = await import('./interactiveCli.js'); | |
| await doStartUI( | |
| config, | |
| settings, | |
| startupWarnings, | |
| workspaceRoot, | |
| resumedSessionData, | |
| initializationResult, | |
| ); | |
| } | |
| export async function main() { | |
| let config: Config | undefined; | |
| const cliStartupHandle = startupProfiler.start('cli_startup'); | |
| // Listen for admin controls from parent process (IPC) in non-sandbox mode. In | |
| // sandbox mode, we re-fetch the admin controls from the server once we enter | |
| // the sandbox. | |
| // TODO: Cache settings in sandbox mode as well. | |
| const adminControlsListner = setupAdminControlsListener(); | |
| registerCleanup(adminControlsListner.cleanup); | |
| const cleanupStdio = patchStdio(); | |
| registerSyncCleanup(() => { | |
| // This is needed to ensure we don't lose any buffered output. | |
| initializeOutputListenersAndFlush(config); | |
| cleanupStdio(); | |
| }); | |
| setupUnhandledRejectionHandler(); | |
| setupSignalHandlers(); | |
| const slashCommandConflictHandler = new SlashCommandConflictHandler(); | |
| slashCommandConflictHandler.start(); | |
| registerCleanup(() => slashCommandConflictHandler.stop()); | |
| const loadSettingsHandle = startupProfiler.start('load_settings'); | |
| const settings = loadSettings(); | |
| loadSettingsHandle?.end(); | |
| // If a worktree is requested and enabled, set it up early. | |
| // This must be awaited before any other async tasks that depend on CWD (like loadCliConfig) | |
| // because setupWorktree calls process.chdir(). | |
| const requestedWorktree = cliConfig.getRequestedWorktreeName(settings); | |
| let worktreeInfo: WorktreeInfo | undefined; | |
| if (requestedWorktree !== undefined) { | |
| const worktreeHandle = startupProfiler.start('setup_worktree'); | |
| worktreeInfo = await setupWorktree(requestedWorktree || undefined); | |
| worktreeHandle?.end(); | |
| } | |
| const cleanupOpsHandle = startupProfiler.start('cleanup_ops'); | |
| Promise.all([ | |
| cleanupCheckpoints(), | |
| cleanupToolOutputFiles(settings.merged), | |
| cleanupBackgroundLogs(), | |
| ]) | |
| .catch((e) => { | |
| debugLogger.error('Early cleanup failed:', e); | |
| }) | |
| .finally(() => { | |
| cleanupOpsHandle?.end(); | |
| }); | |
| const parseArgsHandle = startupProfiler.start('parse_arguments'); | |
| const argvPromise = parseArguments(settings.merged).finally(() => { | |
| parseArgsHandle?.end(); | |
| }); | |
| const rawStartupWarningsPromise = getStartupWarnings(); | |
| // Report settings errors once during startup | |
| settings.errors.forEach((error) => { | |
| coreEvents.emitFeedback('warning', error.message); | |
| }); | |
| const trustedFolders = loadTrustedFolders(); | |
| trustedFolders.errors.forEach((error: TrustedFoldersError) => { | |
| coreEvents.emitFeedback( | |
| 'warning', | |
| `Error in ${error.path}: ${error.message}`, | |
| ); | |
| }); | |
| const argv = await argvPromise; | |
| const { sessionId, resumedSessionData } = await resolveSessionId( | |
| argv.resume, | |
| argv.sessionId, | |
| argv.sessionFile, | |
| ); | |
| if ( | |
| (argv.allowedTools && argv.allowedTools.length > 0) || | |
| (settings.merged.tools?.allowed && settings.merged.tools.allowed.length > 0) | |
| ) { | |
| coreEvents.emitFeedback( | |
| 'warning', | |
| 'Warning: --allowed-tools cli argument and tools.allowed in settings.json are deprecated and will be removed in 1.0: Migrate to Policy Engine: https://geminicli.com/docs/core/policy-engine/', | |
| ); | |
| } | |
| if ( | |
| settings.merged.tools?.exclude && | |
| settings.merged.tools.exclude.length > 0 | |
| ) { | |
| coreEvents.emitFeedback( | |
| 'warning', | |
| 'Warning: tools.exclude in settings.json is deprecated and will be removed in 1.0. Migrate to Policy Engine: https://geminicli.com/docs/core/policy-engine/', | |
| ); | |
| } | |
| if (argv.startupMessages) { | |
| argv.startupMessages.forEach((msg) => { | |
| coreEvents.emitFeedback('info', msg); | |
| }); | |
| } | |
| // Check for invalid input combinations early to prevent crashes | |
| if (argv.promptInteractive && !process.stdin.isTTY) { | |
| writeToStderr( | |
| 'Error: The --prompt-interactive flag cannot be used when input is piped from stdin.\n', | |
| ); | |
| await runExitCleanup(); | |
| process.exit(ExitCodes.FATAL_INPUT_ERROR); | |
| } | |
| const isDebugMode = cliConfig.isDebugMode(argv); | |
| const consolePatcher = new ConsolePatcher({ | |
| stderr: argv.isCommand ? false : true, | |
| interactive: isHeadlessMode() && !argv.isCommand ? false : true, | |
| debugMode: isDebugMode, | |
| onNewMessage: (msg) => { | |
| coreEvents.emitConsoleLog(msg.type, msg.content); | |
| }, | |
| }); | |
| consolePatcher.patch(); | |
| dns.setDefaultResultOrder( | |
| validateDnsResolutionOrder(settings.merged.advanced.dnsResolutionOrder), | |
| ); | |
| // Set a default auth type if one isn't set or is set to a legacy type | |
| if ( | |
| !settings.merged.security.auth.selectedType || | |
| settings.merged.security.auth.selectedType === AuthType.LEGACY_CLOUD_SHELL | |
| ) { | |
| if ( | |
| process.env['CLOUD_SHELL'] === 'true' || | |
| process.env['GEMINI_CLI_USE_COMPUTE_ADC'] === 'true' | |
| ) { | |
| settings.setValue( | |
| SettingScope.User, | |
| 'security.auth.selectedType', | |
| AuthType.COMPUTE_ADC, | |
| ); | |
| } | |
| } | |
| const partialConfig = await loadCliConfig(settings.merged, sessionId, argv, { | |
| projectHooks: settings.workspace.settings.hooks, | |
| skipExtensions: true, | |
| loadedSettings: settings, | |
| }); | |
| adminControlsListner.setConfig(partialConfig); | |
| // Refresh auth to fetch remote admin settings from CCPA and before entering | |
| // the sandbox because the sandbox will interfere with the Oauth2 web | |
| // redirect. | |
| let initialAuthFailed = false; | |
| if (!settings.merged.security.auth.useExternal && !argv.isCommand) { | |
| try { | |
| if ( | |
| partialConfig.isInteractive() && | |
| settings.merged.security.auth.selectedType | |
| ) { | |
| const err = await validateAuthMethod( | |
| settings.merged.security.auth.selectedType, | |
| ); | |
| if (err) { | |
| throw new Error(err); | |
| } | |
| await partialConfig.refreshAuth( | |
| settings.merged.security.auth.selectedType, | |
| ); | |
| } else if (!partialConfig.isInteractive()) { | |
| const authType = await validateNonInteractiveAuth( | |
| settings.merged.security.auth.selectedType, | |
| settings.merged.security.auth.useExternal, | |
| partialConfig, | |
| settings, | |
| ); | |
| await partialConfig.refreshAuth(authType); | |
| } | |
| } catch (err) { | |
| if (err instanceof ValidationCancelledError) { | |
| // User cancelled verification, exit immediately. | |
| await runExitCleanup(); | |
| process.exit(ExitCodes.SUCCESS); | |
| } | |
| // If validation is required, we don't treat it as a fatal failure. | |
| // We allow the app to start, and the React-based ValidationDialog | |
| // will handle it. | |
| if (!(err instanceof ValidationRequiredError)) { | |
| debugLogger.error('Error authenticating:', err); | |
| initialAuthFailed = true; | |
| } | |
| } | |
| } | |
| const remoteAdminSettings = partialConfig.getRemoteAdminSettings(); | |
| // Set remote admin settings if returned from CCPA. | |
| if (remoteAdminSettings) { | |
| settings.setRemoteAdminSettings(remoteAdminSettings); | |
| if (process.send) { | |
| process.send({ | |
| type: 'admin-settings-update', | |
| settings: remoteAdminSettings, | |
| }); | |
| } | |
| } | |
| // Run deferred command now that we have admin settings. | |
| await runDeferredCommand(settings.merged); | |
| // hop into sandbox if we are outside and sandboxing is enabled | |
| if (!process.env['SANDBOX'] && !argv.isCommand) { | |
| const memoryArgs = settings.merged.advanced.autoConfigureMemory | |
| ? getNodeMemoryArgs(isDebugMode) | |
| : []; | |
| const sandboxConfig = await loadSandboxConfig(settings.merged, argv); | |
| // We intentionally omit the list of extensions here because extensions | |
| // should not impact auth or setting up the sandbox. | |
| // TODO(jacobr): refactor loadCliConfig so there is a minimal version | |
| // that only initializes enough config to enable refreshAuth or find | |
| // another way to decouple refreshAuth from requiring a config. | |
| if (sandboxConfig) { | |
| if (initialAuthFailed) { | |
| await runExitCleanup(); | |
| process.exit(ExitCodes.FATAL_AUTHENTICATION_ERROR); | |
| } | |
| let stdinData = ''; | |
| if (!process.stdin.isTTY) { | |
| stdinData = await readStdin(); | |
| } | |
| // This function is a copy of the one from sandbox.ts | |
| // It is moved here to decouple sandbox.ts from the CLI's argument structure. | |
| const injectStdinIntoArgs = ( | |
| args: string[], | |
| stdinData?: string, | |
| ): string[] => { | |
| const finalArgs = [...args]; | |
| if (stdinData) { | |
| const promptIndex = finalArgs.findIndex( | |
| (arg) => arg === '--prompt' || arg === '-p', | |
| ); | |
| if (promptIndex > -1 && finalArgs.length > promptIndex + 1) { | |
| // If there's a prompt argument, prepend stdin to it | |
| finalArgs[promptIndex + 1] = | |
| `${stdinData}\n\n${finalArgs[promptIndex + 1]}`; | |
| } else { | |
| // If there's no prompt argument, add stdin as the prompt | |
| finalArgs.push('--prompt', stdinData); | |
| } | |
| } | |
| return finalArgs; | |
| }; | |
| const sandboxArgs = injectStdinIntoArgs(process.argv, stdinData); | |
| await relaunchOnExitCode(() => | |
| start_sandbox(sandboxConfig, memoryArgs, partialConfig, sandboxArgs), | |
| ); | |
| await runExitCleanup(); | |
| process.exit(ExitCodes.SUCCESS); | |
| } | |
| } | |
| // We are now past the logic handling potentially launching a child process | |
| // to run Gemini CLI. It is now safe to perform expensive initialization that | |
| // may have side effects. | |
| { | |
| const loadConfigHandle = startupProfiler.start('load_cli_config'); | |
| config = await loadCliConfig(settings.merged, sessionId, argv, { | |
| projectHooks: settings.workspace.settings.hooks, | |
| worktreeSettings: worktreeInfo, | |
| loadedSettings: settings, | |
| }); | |
| loadConfigHandle?.end(); | |
| // Initialize storage immediately after loading config to ensure that | |
| // storage-related operations (like listing or resuming sessions) have | |
| // access to the project identifier. | |
| await config.storage.initialize(); | |
| adminControlsListner.setConfig(config); | |
| if (config.isInteractive() && settings.merged.general.devtools) { | |
| const { setupInitialActivityLogger } = await import( | |
| './utils/devtoolsService.js' | |
| ); | |
| setupInitialActivityLogger(config); | |
| } | |
| // Register config for telemetry shutdown | |
| // This ensures telemetry (including SessionEnd hooks) is properly flushed on exit | |
| registerTelemetryConfig(config); | |
| const policyEngine = config.getPolicyEngine(); | |
| const messageBus = config.getMessageBus(); | |
| createPolicyUpdater(policyEngine, messageBus, config.storage); | |
| // Register SessionEnd hook to fire on graceful exit | |
| // This runs before telemetry shutdown in runExitCleanup() | |
| registerCleanup(async () => { | |
| await config?.getHookSystem()?.fireSessionEndEvent(SessionEndReason.Exit); | |
| }); | |
| // Register ConsolePatcher cleanup last to ensure logs from shutdown hooks | |
| // are correctly redirected to stderr (especially for non-interactive JSON output). | |
| if (!config.getAcpMode()) { | |
| registerCleanup(consolePatcher.cleanup); | |
| } | |
| // Launch cleanup expired sessions as a background task | |
| cleanupExpiredSessions(config, settings.merged).catch((e) => { | |
| debugLogger.error('Failed to cleanup expired sessions:', e); | |
| }); | |
| if (config.getListExtensions()) { | |
| debugLogger.log('Installed extensions:'); | |
| for (const extension of config.getExtensions()) { | |
| debugLogger.log(`- ${extension.name}`); | |
| } | |
| await runExitCleanup(); | |
| process.exit(ExitCodes.SUCCESS); | |
| } | |
| // Handle --list-sessions flag | |
| if (config.getListSessions()) { | |
| // Attempt auth for summary generation (gracefully skips if not configured) | |
| const authType = settings.merged.security.auth.selectedType; | |
| if (authType) { | |
| try { | |
| await config.refreshAuth(authType); | |
| } catch (e) { | |
| // Auth failed - continue without summary generation capability | |
| debugLogger.debug( | |
| 'Auth failed for --list-sessions, summaries may not be generated:', | |
| e, | |
| ); | |
| } | |
| } | |
| await listSessions(config); | |
| await runExitCleanup(); | |
| process.exit(ExitCodes.SUCCESS); | |
| } | |
| // Handle --delete-session flag | |
| const sessionToDelete = config.getDeleteSession(); | |
| if (sessionToDelete) { | |
| await deleteSession(config, sessionToDelete); | |
| await runExitCleanup(); | |
| process.exit(ExitCodes.SUCCESS); | |
| } | |
| const wasRaw = process.stdin.isRaw; | |
| if (config.isInteractive() && !wasRaw && process.stdin.isTTY) { | |
| // Set this as early as possible to avoid spurious characters from | |
| // input showing up in the output. | |
| process.stdin.setRawMode(true); | |
| // This cleanup isn't strictly needed but may help in certain situations. | |
| registerSyncCleanup(() => { | |
| process.stdin.setRawMode(wasRaw); | |
| }); | |
| } | |
| const terminalHandle = startupProfiler.start('setup_terminal'); | |
| await setupTerminalAndTheme(config, settings); | |
| terminalHandle?.end(); | |
| const initAppHandle = startupProfiler.start('initialize_app'); | |
| const initializationResult = await initializeApp(config, settings); | |
| initAppHandle?.end(); | |
| import('./services/liteRtServerManager.js') | |
| .then(({ LiteRtServerManager }) => { | |
| const mergedGemma = settings.merged.experimental?.gemmaModelRouter; | |
| if (!mergedGemma) return; | |
| // Security: binaryPath and autoStartServer must come from user-scoped | |
| // settings only to prevent workspace configs from triggering arbitrary | |
| // binary execution. | |
| const userGemma = settings.forScope(SettingScope.User).settings | |
| .experimental?.gemmaModelRouter; | |
| return LiteRtServerManager.ensureRunning({ | |
| ...mergedGemma, | |
| binaryPath: userGemma?.binaryPath, | |
| autoStartServer: userGemma?.autoStartServer, | |
| }); | |
| }) | |
| .catch((e) => debugLogger.warn('LiteRT auto-start import failed:', e)); | |
| if ( | |
| settings.merged.security.auth.selectedType === | |
| AuthType.LOGIN_WITH_GOOGLE && | |
| config.isBrowserLaunchSuppressed() | |
| ) { | |
| // Do oauth before app renders to make copying the link possible. | |
| await getOauthClient(settings.merged.security.auth.selectedType, config); | |
| } | |
| if (config.getAcpMode()) { | |
| return runAcpClient(config, settings, argv); | |
| } | |
| let input = config.getQuestion(); | |
| const useAlternateBuffer = shouldEnterAlternateScreen( | |
| config.getUseAlternateBuffer(), | |
| config.getScreenReader(), | |
| ); | |
| const rawStartupWarnings = await rawStartupWarningsPromise; | |
| const startupWarnings: StartupWarning[] = [ | |
| ...rawStartupWarnings.map((message) => ({ | |
| id: `startup-${createHash('sha256').update(message).digest('hex').substring(0, 16)}`, | |
| message, | |
| priority: WarningPriority.High, | |
| })), | |
| ...(await getUserStartupWarnings(settings.merged, undefined, { | |
| isAlternateBuffer: useAlternateBuffer, | |
| })), | |
| ]; | |
| cliStartupHandle?.end(); | |
| if (!config.isInteractive()) { | |
| for (const warning of startupWarnings) { | |
| writeToStderr(warning.message + '\n'); | |
| } | |
| } | |
| // Render UI, passing necessary config values. Check that there is no command line question. | |
| if (config.isInteractive()) { | |
| // Earlier initialization phases (like TerminalCapabilityManager resolving | |
| // or authWithWeb) may have added and removed 'data' listeners on process.stdin. | |
| // When the listener count drops to 0, Node.js implicitly pauses the stream buffer. | |
| // React Ink's useInput hooks will silently fail to receive keystrokes if the stream remains paused. | |
| if (process.stdin.isTTY) { | |
| process.stdin.resume(); | |
| } | |
| await startInteractiveUI( | |
| config, | |
| settings, | |
| startupWarnings, | |
| process.cwd(), | |
| resumedSessionData, | |
| initializationResult, | |
| ); | |
| return; | |
| } | |
| await config.initialize(); | |
| startupProfiler.flush(config); | |
| // If not a TTY, read from stdin | |
| // This is for cases where the user pipes input directly into the command | |
| let stdinData: string | undefined = undefined; | |
| if (!process.stdin.isTTY) { | |
| stdinData = await readStdin(); | |
| if (stdinData) { | |
| input = input ? `${stdinData}\n\n${input}` : stdinData; | |
| } | |
| } | |
| // Fire SessionStart hook through MessageBus (only if hooks are enabled) | |
| // Must be called AFTER config.initialize() to ensure HookRegistry is loaded | |
| const sessionStartSource = resumedSessionData | |
| ? SessionStartSource.Resume | |
| : SessionStartSource.Startup; | |
| const hookSystem = config?.getHookSystem(); | |
| if (hookSystem) { | |
| const result = await hookSystem.fireSessionStartEvent(sessionStartSource); | |
| if (result) { | |
| if (result.systemMessage) { | |
| writeToStderr(result.systemMessage + '\n'); | |
| } | |
| const additionalContext = result.getAdditionalContext(); | |
| if (additionalContext) { | |
| // Prepend context to input (System Context -> Stdin -> Question) | |
| const wrappedContext = `<hook_context>${additionalContext}</hook_context>`; | |
| input = input ? `${wrappedContext}\n\n${input}` : wrappedContext; | |
| } | |
| } | |
| } | |
| if (!input) { | |
| debugLogger.error( | |
| `No input provided via stdin. Input can be provided by piping data into gemini or using the --prompt option.`, | |
| ); | |
| await runExitCleanup(); | |
| process.exit(ExitCodes.FATAL_INPUT_ERROR); | |
| } | |
| const prompt_id = sessionId; | |
| logUserPrompt( | |
| config, | |
| new UserPromptEvent( | |
| input.length, | |
| prompt_id, | |
| config.getContentGeneratorConfig()?.authType, | |
| input, | |
| ), | |
| ); | |
| const authType = await validateNonInteractiveAuth( | |
| settings.merged.security.auth.selectedType, | |
| settings.merged.security.auth.useExternal, | |
| config, | |
| settings, | |
| ); | |
| await config.refreshAuth(authType); | |
| if (config.getDebugMode()) { | |
| debugLogger.log('Session ID: %s', sessionId); | |
| } | |
| initializeOutputListenersAndFlush(config); | |
| await runNonInteractive({ | |
| config, | |
| settings, | |
| input, | |
| prompt_id, | |
| resumedSessionData, | |
| }); | |
| // Call cleanup before process.exit, which causes cleanup to not run | |
| await runExitCleanup(); | |
| process.exit(ExitCodes.SUCCESS); | |
| } | |
| } | |
| export function initializeOutputListenersAndFlush(config?: Config) { | |
| // If there are no listeners for output, make sure we flush so output is not | |
| // lost. | |
| if (coreEvents.listenerCount(CoreEvent.Output) === 0) { | |
| // In non-interactive mode, ensure we drain any buffered output or logs to stderr | |
| coreEvents.on(CoreEvent.Output, (payload: OutputPayload) => { | |
| if (payload.isStderr) { | |
| writeToStderr(payload.chunk, payload.encoding); | |
| } else { | |
| writeToStdout(payload.chunk, payload.encoding); | |
| } | |
| }); | |
| } | |
| if (coreEvents.listenerCount(CoreEvent.ConsoleLog) === 0) { | |
| coreEvents.on(CoreEvent.ConsoleLog, (payload: ConsoleLogPayload) => { | |
| if (payload.type === 'error' || payload.type === 'warn') { | |
| writeToStderr(payload.content + '\n'); | |
| } else { | |
| writeToStderr(payload.content + '\n'); | |
| } | |
| }); | |
| } | |
| if (coreEvents.listenerCount(CoreEvent.UserFeedback) === 0) { | |
| coreEvents.on(CoreEvent.UserFeedback, (payload: UserFeedbackPayload) => { | |
| writeToStderr(payload.message + '\n'); | |
| }); | |
| } | |
| const outputFormat = config?.getOutputFormat(); | |
| const forceToStderr = outputFormat === 'json'; | |
| coreEvents.drainBacklogs( | |
| <K extends keyof CoreEvents>(event: K, args: CoreEvents[K]) => { | |
| if (forceToStderr && event === (CoreEvent.Output as string)) { | |
| // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion | |
| const payload = args[0] as OutputPayload; | |
| if (!payload.isStderr) { | |
| return { | |
| event, | |
| // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion | |
| args: [{ ...payload, isStderr: true }] as unknown as CoreEvents[K], | |
| }; | |
| } | |
| } | |
| return { event, args }; | |
| }, | |
| ); | |
| } | |
| function setupAdminControlsListener() { | |
| let pendingSettings: AdminControlsSettings | undefined; | |
| let config: Config | undefined; | |
| const messageHandler = (msg: unknown) => { | |
| // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion | |
| const message = msg as { | |
| type?: string; | |
| settings?: AdminControlsSettings; | |
| }; | |
| if (message?.type === 'admin-settings' && message.settings) { | |
| if (config) { | |
| config.setRemoteAdminSettings(message.settings); | |
| } else { | |
| pendingSettings = message.settings; | |
| } | |
| } | |
| }; | |
| process.on('message', messageHandler); | |
| return { | |
| setConfig: (newConfig: Config) => { | |
| config = newConfig; | |
| if (pendingSettings) { | |
| config.setRemoteAdminSettings(pendingSettings); | |
| } | |
| }, | |
| cleanup: () => { | |
| process.off('message', messageHandler); | |
| }, | |
| }; | |
| } | |