diff --git a/integration-tests/session-summary.test.ts b/integration-tests/session-summary.test.ts new file mode 100644 index 0000000000..7b88a75626 --- /dev/null +++ b/integration-tests/session-summary.test.ts @@ -0,0 +1,37 @@ +/** + * @license + * Copyright 2025 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import { TestRig } from './test-helper.js'; +import { describe, it, expect, beforeEach, afterEach } from 'vitest'; +import { join } from 'node:path'; +import { readFileSync } from 'node:fs'; + +describe('session-summary flag', () => { + let rig: TestRig; + + beforeEach(function (context) { + rig = new TestRig(); + if (context.task.name) { + rig.setup(context.task.name); + } + }); + + afterEach(async () => { + await rig.cleanup(); + }); + + it('should write a session summary in non-interactive mode', async () => { + const summaryPath = join(rig.testDir!, 'summary.json'); + await rig.run('Say hello', '--session-summary', summaryPath); + + const summaryContent = readFileSync(summaryPath, 'utf-8'); + const summary = JSON.parse(summaryContent); + + expect(summary).toBeDefined(); + expect(summary.models).toBeDefined(); + expect(summary.tools).toBeDefined(); + }); +}); diff --git a/packages/cli/src/config/config.ts b/packages/cli/src/config/config.ts index 873211d40a..9fab419108 100755 --- a/packages/cli/src/config/config.ts +++ b/packages/cli/src/config/config.ts @@ -78,6 +78,7 @@ export interface CliArgs { proxy: string | undefined; includeDirectories: string[] | undefined; screenReader: boolean | undefined; + sessionSummary: string | undefined; } export async function parseArguments(settings: Settings): Promise { @@ -227,6 +228,10 @@ export async function parseArguments(settings: Settings): Promise { description: 'Enable screen reader mode for accessibility.', default: false, }) + .option('session-summary', { + type: 'string', + description: 'File to write session summary to.', + }) .deprecateOption( 'telemetry', 'Use settings.json instead. This flag will be removed in a future version.', diff --git a/packages/cli/src/gemini.tsx b/packages/cli/src/gemini.tsx index b11b0f29ec..f805d0c7c5 100644 --- a/packages/cli/src/gemini.tsx +++ b/packages/cli/src/gemini.tsx @@ -24,7 +24,11 @@ import { getUserStartupWarnings } from './utils/userStartupWarnings.js'; import { ConsolePatcher } from './ui/utils/ConsolePatcher.js'; import { runNonInteractive } from './nonInteractiveCli.js'; import { loadExtensions } from './config/extension.js'; -import { cleanupCheckpoints, registerCleanup } from './utils/cleanup.js'; +import { + cleanupCheckpoints, + registerCleanup, + runExitCleanup, +} from './utils/cleanup.js'; import { getCliVersion } from './utils/version.js'; import type { Config } from '@google/gemini-cli-core'; import { @@ -36,6 +40,7 @@ import { IdeConnectionEvent, IdeConnectionType, FatalConfigError, + uiTelemetryService, } from '@google/gemini-cli-core'; import { validateAuthMethod } from './config/auth.js'; import { setMaxSizedBoxDebugging } from './ui/components/shared/MaxSizedBox.js'; @@ -45,6 +50,7 @@ import { checkForUpdates } from './ui/utils/updateCheck.js'; import { handleAutoUpdate } from './utils/handleAutoUpdate.js'; import { appEvents, AppEvent } from './utils/events.js'; import { SettingsContext } from './ui/contexts/SettingsContext.js'; +import { writeFileSync } from 'node:fs'; export function validateDnsResolutionOrder( order: string | undefined, @@ -225,6 +231,16 @@ export async function main() { argv, ); + if (argv.sessionSummary) { + registerCleanup(() => { + const metrics = uiTelemetryService.getMetrics(); + writeFileSync( + argv.sessionSummary!, + JSON.stringify({ sessionMetrics: metrics }, null, 2), + ); + }); + } + const consolePatcher = new ConsolePatcher({ stderr: true, debugMode: config.getDebugMode(), @@ -434,6 +450,8 @@ export async function main() { } await runNonInteractive(nonInteractiveConfig, input, prompt_id); + // Call cleanup before process.exit, which causes cleanup to not run + await runExitCleanup(); process.exit(0); }