├── .nvmrc ├── .allstar └── branch_protection.yaml ├── .npmrc ├── packages ├── core │ ├── src │ │ ├── utils │ │ │ ├── __fixtures__ │ │ │ │ └── dummy.wasm │ │ │ ├── session.ts │ │ │ ├── promptIdContext.ts │ │ │ ├── formatters.ts │ │ │ ├── messageInspectors.ts │ │ │ ├── formatters.test.ts │ │ │ ├── getPty.ts │ │ │ ├── LruCache.ts │ │ │ └── debugLogger.ts │ │ ├── test-utils │ │ │ ├── index.ts │ │ │ ├── config.ts │ │ │ └── mockWorkspaceContext.ts │ │ ├── telemetry │ │ │ ├── constants.ts │ │ │ ├── telemetry-utils.ts │ │ │ ├── activity-types.ts │ │ │ ├── telemetryAttributes.ts │ │ │ └── tool-call-decision.ts │ │ ├── policy │ │ │ ├── index.ts │ │ │ └── types.ts │ │ ├── confirmation-bus │ │ │ └── index.ts │ │ ├── mocks │ │ │ └── msw.ts │ │ ├── commands │ │ │ ├── extensions.ts │ │ │ └── extensions.test.ts │ │ ├── index.test.ts │ │ ├── ide │ │ │ └── constants.ts │ │ ├── mcp │ │ │ └── token-storage │ │ │ │ ├── index.ts │ │ │ │ └── types.ts │ │ ├── prompts │ │ │ └── mcp-prompts.ts │ │ ├── config │ │ │ └── constants.ts │ │ ├── core │ │ │ ├── geminiRequest.ts │ │ │ ├── tokenLimits.ts │ │ │ └── nonInteractiveToolExecutor.ts │ │ ├── fallback │ │ │ └── types.ts │ │ ├── routing │ │ │ └── strategies │ │ │ │ ├── defaultStrategy.ts │ │ │ │ ├── defaultStrategy.test.ts │ │ │ │ ├── fallbackStrategy.ts │ │ │ │ └── overrideStrategy.ts │ │ ├── tools │ │ │ └── tool-names.ts │ │ ├── __mocks__ │ │ │ └── fs │ │ │ │ └── promises.ts │ │ └── output │ │ │ └── json-formatter.ts │ ├── tsconfig.json │ ├── test-setup.ts │ ├── vitest.config.ts │ └── index.ts ├── vscode-ide-companion │ ├── .vscodeignore │ ├── assets │ │ └── icon.png │ ├── .vscode │ │ ├── launch.json │ │ └── tasks.json │ ├── src │ │ └── utils │ │ │ └── logger.ts │ ├── eslint.config.mjs │ ├── development.md │ ├── tsconfig.json │ └── README.md ├── cli │ ├── src │ │ ├── commands │ │ │ ├── extensions │ │ │ │ ├── examples │ │ │ │ │ ├── context │ │ │ │ │ │ ├── gemini-extension.json │ │ │ │ │ │ └── GEMINI.md │ │ │ │ │ ├── custom-commands │ │ │ │ │ │ ├── gemini-extension.json │ │ │ │ │ │ └── commands │ │ │ │ │ │ │ └── fs │ │ │ │ │ │ │ └── grep-code.toml │ │ │ │ │ ├── exclude-tools │ │ │ │ │ │ └── gemini-extension.json │ │ │ │ │ └── mcp-server │ │ │ │ │ │ ├── gemini-extension.json │ │ │ │ │ │ ├── tsconfig.json │ │ │ │ │ │ └── package.json │ │ │ │ └── uninstall.test.ts │ │ │ ├── mcp.ts │ │ │ └── extensions.tsx │ │ ├── ui │ │ │ ├── hooks │ │ │ │ ├── useRefreshMemoryCommand.ts │ │ │ │ ├── useSettingsCommand.ts │ │ │ │ ├── useTerminalSize.ts │ │ │ │ ├── useModelCommand.ts │ │ │ │ ├── useKittyKeyboardProtocol.ts │ │ │ │ ├── useLogger.ts │ │ │ │ ├── useKeypress.ts │ │ │ │ ├── useBracketedPaste.ts │ │ │ │ ├── useStateAndRef.ts │ │ │ │ ├── useModelCommand.test.ts │ │ │ │ ├── useMemoryMonitor.ts │ │ │ │ └── useFlickerDetector.ts │ │ │ ├── utils │ │ │ │ ├── isNarrowWidth.ts │ │ │ │ ├── ui-sizing.ts │ │ │ │ └── displayUtils.ts │ │ │ ├── contexts │ │ │ │ ├── ShellFocusContext.tsx │ │ │ │ ├── ConfigContext.tsx │ │ │ │ ├── AppContext.tsx │ │ │ │ ├── SettingsContext.tsx │ │ │ │ └── StreamingContext.tsx │ │ │ ├── textConstants.ts │ │ │ ├── components │ │ │ │ ├── __snapshots__ │ │ │ │ │ ├── LoadingIndicator.test.tsx.snap │ │ │ │ │ ├── LoopDetectionConfirmation.test.tsx.snap │ │ │ │ │ ├── Footer.test.tsx.snap │ │ │ │ │ └── PrepareLabel.test.tsx.snap │ │ │ │ ├── ShellModeIndicator.tsx │ │ │ │ ├── SessionSummaryDisplay.tsx │ │ │ │ ├── shared │ │ │ │ │ └── __snapshots__ │ │ │ │ │ │ ├── EnumSelector.test.tsx.snap │ │ │ │ │ │ └── DescriptiveRadioButtonSelect.test.tsx.snap │ │ │ │ ├── messages │ │ │ │ │ ├── __snapshots__ │ │ │ │ │ │ ├── ToolMessageRawMarkdown.test.tsx.snap │ │ │ │ │ │ └── GeminiMessage.test.tsx.snap │ │ │ │ │ ├── UserShellMessage.tsx │ │ │ │ │ ├── ErrorMessage.tsx │ │ │ │ │ ├── WarningMessage.tsx │ │ │ │ │ ├── InfoMessage.tsx │ │ │ │ │ └── UserMessage.tsx │ │ │ │ ├── UpdateNotification.tsx │ │ │ │ ├── RawMarkdownIndicator.tsx │ │ │ │ ├── CliSpinner.tsx │ │ │ │ ├── views │ │ │ │ │ └── __snapshots__ │ │ │ │ │ │ ├── ChatList.test.tsx.snap │ │ │ │ │ │ └── ToolsList.test.tsx.snap │ │ │ │ ├── ContextUsageDisplay.tsx │ │ │ │ ├── ExitWarning.tsx │ │ │ │ ├── ConsoleSummaryDisplay.tsx │ │ │ │ ├── AppHeader.tsx │ │ │ │ ├── QuittingDisplay.tsx │ │ │ │ ├── ShowMoreLines.tsx │ │ │ │ ├── LoopDetectionConfirmation.test.tsx │ │ │ │ ├── AutoAcceptIndicator.tsx │ │ │ │ ├── MemoryUsageDisplay.tsx │ │ │ │ ├── QueuedMessageDisplay.tsx │ │ │ │ └── Tips.tsx │ │ │ ├── commands │ │ │ │ ├── corgiCommand.ts │ │ │ │ ├── modelCommand.ts │ │ │ │ ├── themeCommand.ts │ │ │ │ ├── authCommand.ts │ │ │ │ ├── editorCommand.ts │ │ │ │ ├── privacyCommand.ts │ │ │ │ ├── permissionsCommand.ts │ │ │ │ ├── settingsCommand.ts │ │ │ │ ├── helpCommand.ts │ │ │ │ ├── vimCommand.ts │ │ │ │ ├── profileCommand.ts │ │ │ │ ├── quitCommand.ts │ │ │ │ ├── editorCommand.test.ts │ │ │ │ ├── clearCommand.ts │ │ │ │ ├── authCommand.test.ts │ │ │ │ ├── corgiCommand.test.ts │ │ │ │ ├── modelCommand.test.ts │ │ │ │ ├── settingsCommand.test.ts │ │ │ │ ├── permissionsCommand.test.ts │ │ │ │ ├── docsCommand.ts │ │ │ │ ├── themeCommand.test.ts │ │ │ │ └── privacyCommand.test.ts │ │ │ ├── semantic-colors.ts │ │ │ ├── constants.ts │ │ │ ├── App.tsx │ │ │ ├── noninteractive │ │ │ │ └── nonInteractiveUi.ts │ │ │ ├── privacy │ │ │ │ └── PrivacyNotice.tsx │ │ │ └── layouts │ │ │ │ └── DefaultAppLayout.tsx │ │ ├── utils │ │ │ ├── spawnWrapper.ts │ │ │ ├── version.ts │ │ │ ├── updateEventEmitter.ts │ │ │ ├── events.ts │ │ │ ├── math.ts │ │ │ ├── processUtils.ts │ │ │ ├── resolvePath.ts │ │ │ ├── checks.ts │ │ │ ├── processUtils.test.ts │ │ │ ├── windowTitle.ts │ │ │ ├── package.ts │ │ │ ├── sandbox-macos-permissive-open.sb │ │ │ ├── cleanup.ts │ │ │ ├── sandbox-macos-permissive-closed.sb │ │ │ └── sandbox-macos-permissive-proxied.sb │ │ ├── config │ │ │ └── extensions │ │ │ │ ├── variables.test.ts │ │ │ │ ├── variableSchema.ts │ │ │ │ ├── github_fetch.ts │ │ │ │ └── storage.ts │ │ ├── patches │ │ │ └── is-in-ci.ts │ │ ├── core │ │ │ ├── theme.ts │ │ │ └── auth.ts │ │ └── services │ │ │ ├── types.ts │ │ │ └── prompt-processors │ │ │ └── argumentProcessor.ts │ ├── test-setup.ts │ ├── tsconfig.json │ ├── index.ts │ └── vitest.config.ts ├── a2a-server │ ├── index.ts │ ├── README.md │ ├── src │ │ ├── index.ts │ │ ├── http │ │ │ ├── requestStorage.ts │ │ │ └── server.ts │ │ ├── commands │ │ │ ├── list-extensions.ts │ │ │ ├── command-registry.ts │ │ │ └── list-extensions.test.ts │ │ └── utils │ │ │ ├── logger.ts │ │ │ └── executor_utils.ts │ ├── tsconfig.json │ ├── vitest.config.ts │ └── package.json └── test-utils │ ├── index.ts │ ├── src │ └── index.ts │ ├── tsconfig.json │ ├── package.json │ └── vitest.config.ts ├── hello ├── gemini-extension.json └── commands │ └── fs │ └── grep-code.toml ├── docs ├── assets │ ├── theme-ansi.png │ ├── theme-ayu.png │ ├── release_patch.png │ ├── theme-custom.png │ ├── theme-default.png │ ├── theme-dracula.png │ ├── theme-github.png │ ├── theme-atom-one.png │ ├── theme-ayu-light.png │ ├── connected_devtools.png │ ├── gemini-screenshot.png │ ├── theme-ansi-light.png │ ├── theme-github-light.png │ ├── theme-google-light.png │ ├── theme-xcode-light.png │ └── theme-default-light.png ├── cli │ ├── authentication.md │ ├── token-caching.md │ └── uninstall.md └── tools │ └── web-search.md ├── .vscode ├── extensions.json ├── settings.json └── tasks.json ├── integration-tests ├── tsconfig.json ├── context-compress-interactive.compress-empty.json ├── vitest.config.ts ├── telemetry.test.ts ├── context-compress-interactive.compress.json ├── context-compress-interactive.compress-failure.json └── flicker.test.ts ├── .editorconfig ├── scripts ├── tests │ ├── test-setup.ts │ └── vitest.config.ts ├── pre-commit.js ├── build_vscode_companion.js ├── build_package.js └── create_alias.sh ├── .husky └── pre-commit ├── .lycheeignore ├── .prettierignore ├── .prettierrc.json ├── .gemini ├── config.yaml └── commands │ └── github │ └── cleanup-back-to-main.toml ├── .github ├── workflows │ ├── docs-rebuild.yml │ ├── links.yml │ ├── trigger_e2e.yml │ ├── no-response.yml │ └── gemini-scheduled-pr-triage.yml ├── CODEOWNERS ├── actions │ ├── setup-npmrc │ │ └── action.yml │ ├── calculate-vars │ │ └── action.yml │ └── run-tests │ │ └── action.yml ├── dependabot.yml ├── ISSUE_TEMPLATE │ ├── feature_request.yml │ └── website_issue.yml └── pull_request_template.md ├── SECURITY.md ├── third_party └── get-ripgrep │ ├── src │ └── index.js │ ├── LICENSE │ └── package.json ├── .gitattributes ├── tsconfig.json ├── .gitignore └── Dockerfile /.nvmrc: -------------------------------------------------------------------------------- 1 | 20 2 | -------------------------------------------------------------------------------- /.allstar/branch_protection.yaml: -------------------------------------------------------------------------------- 1 | action: 'log' 2 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | @google:registry=https://wombat-dressing-room.appspot.com -------------------------------------------------------------------------------- /packages/core/src/utils/__fixtures__/dummy.wasm: -------------------------------------------------------------------------------- 1 | This is a wasm fixture. 2 | -------------------------------------------------------------------------------- /hello/gemini-extension.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "custom-commands", 3 | "version": "1.0.0" 4 | } 5 | -------------------------------------------------------------------------------- /docs/assets/theme-ansi.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rsc/gemini-cli/main/docs/assets/theme-ansi.png -------------------------------------------------------------------------------- /docs/assets/theme-ayu.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rsc/gemini-cli/main/docs/assets/theme-ayu.png -------------------------------------------------------------------------------- /docs/assets/release_patch.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rsc/gemini-cli/main/docs/assets/release_patch.png -------------------------------------------------------------------------------- /docs/assets/theme-custom.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rsc/gemini-cli/main/docs/assets/theme-custom.png -------------------------------------------------------------------------------- /docs/assets/theme-default.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rsc/gemini-cli/main/docs/assets/theme-default.png -------------------------------------------------------------------------------- /docs/assets/theme-dracula.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rsc/gemini-cli/main/docs/assets/theme-dracula.png -------------------------------------------------------------------------------- /docs/assets/theme-github.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rsc/gemini-cli/main/docs/assets/theme-github.png -------------------------------------------------------------------------------- /docs/assets/theme-atom-one.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rsc/gemini-cli/main/docs/assets/theme-atom-one.png -------------------------------------------------------------------------------- /docs/assets/theme-ayu-light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rsc/gemini-cli/main/docs/assets/theme-ayu-light.png -------------------------------------------------------------------------------- /docs/assets/connected_devtools.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rsc/gemini-cli/main/docs/assets/connected_devtools.png -------------------------------------------------------------------------------- /docs/assets/gemini-screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rsc/gemini-cli/main/docs/assets/gemini-screenshot.png -------------------------------------------------------------------------------- /docs/assets/theme-ansi-light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rsc/gemini-cli/main/docs/assets/theme-ansi-light.png -------------------------------------------------------------------------------- /docs/assets/theme-github-light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rsc/gemini-cli/main/docs/assets/theme-github-light.png -------------------------------------------------------------------------------- /docs/assets/theme-google-light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rsc/gemini-cli/main/docs/assets/theme-google-light.png -------------------------------------------------------------------------------- /docs/assets/theme-xcode-light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rsc/gemini-cli/main/docs/assets/theme-xcode-light.png -------------------------------------------------------------------------------- /docs/assets/theme-default-light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rsc/gemini-cli/main/docs/assets/theme-default-light.png -------------------------------------------------------------------------------- /packages/vscode-ide-companion/.vscodeignore: -------------------------------------------------------------------------------- 1 | ** 2 | !dist/ 3 | ../ 4 | ../../ 5 | !LICENSE 6 | !NOTICES.txt 7 | !assets/ 8 | -------------------------------------------------------------------------------- /docs/cli/authentication.md: -------------------------------------------------------------------------------- 1 | # Authentication Setup 2 | 3 | See: [Getting Started - Authentication Setup](../get-started/authentication.md). 4 | -------------------------------------------------------------------------------- /packages/vscode-ide-companion/assets/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rsc/gemini-cli/main/packages/vscode-ide-companion/assets/icon.png -------------------------------------------------------------------------------- /packages/cli/src/commands/extensions/examples/context/gemini-extension.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "context-example", 3 | "version": "1.0.0" 4 | } 5 | -------------------------------------------------------------------------------- /packages/cli/src/commands/extensions/examples/custom-commands/gemini-extension.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "custom-commands", 3 | "version": "1.0.0" 4 | } 5 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "vitest.explorer", 4 | "esbenp.prettier-vscode", 5 | "dbaeumer.vscode-eslint" 6 | ] 7 | } 8 | -------------------------------------------------------------------------------- /hello/commands/fs/grep-code.toml: -------------------------------------------------------------------------------- 1 | prompt = """ 2 | Please summarize the findings for the pattern `{{args}}`. 3 | 4 | Search Results: 5 | !{grep -r {{args}} .} 6 | """ 7 | -------------------------------------------------------------------------------- /packages/a2a-server/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright 2025 Google LLC 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | export * from './src/index.js'; 8 | -------------------------------------------------------------------------------- /packages/core/src/test-utils/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright 2025 Google LLC 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | export * from './mock-tool.js'; 8 | -------------------------------------------------------------------------------- /packages/test-utils/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright 2025 Google LLC 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | export * from './src/file-system-test-helpers.js'; 8 | -------------------------------------------------------------------------------- /packages/test-utils/src/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright 2025 Google LLC 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | export * from './file-system-test-helpers.js'; 8 | -------------------------------------------------------------------------------- /packages/cli/src/commands/extensions/examples/exclude-tools/gemini-extension.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "excludeTools", 3 | "version": "1.0.0", 4 | "excludeTools": ["run_shell_command(rm -rf)"] 5 | } 6 | -------------------------------------------------------------------------------- /packages/core/src/telemetry/constants.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright 2025 Google LLC 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | export const SERVICE_NAME = 'gemini-cli'; 8 | -------------------------------------------------------------------------------- /packages/core/src/policy/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright 2025 Google LLC 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | export * from './policy-engine.js'; 8 | export * from './types.js'; 9 | -------------------------------------------------------------------------------- /packages/a2a-server/README.md: -------------------------------------------------------------------------------- 1 | # Gemini CLI A2A Server 2 | 3 | ## All code in this package is experimental and under active development 4 | 5 | This package contains the A2A server implementation for the Gemini CLI. 6 | -------------------------------------------------------------------------------- /packages/cli/src/commands/extensions/examples/custom-commands/commands/fs/grep-code.toml: -------------------------------------------------------------------------------- 1 | prompt = """ 2 | Please summarize the findings for the pattern `{{args}}`. 3 | 4 | Search Results: 5 | !{grep -r {{args}} .} 6 | """ 7 | -------------------------------------------------------------------------------- /packages/core/src/confirmation-bus/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright 2025 Google LLC 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | export * from './message-bus.js'; 8 | export * from './types.js'; 9 | -------------------------------------------------------------------------------- /packages/cli/src/ui/hooks/useRefreshMemoryCommand.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright 2025 Google LLC 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | export const REFRESH_MEMORY_COMMAND_NAME = '/refreshmemory'; 8 | -------------------------------------------------------------------------------- /packages/core/src/mocks/msw.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright 2025 Google LLC 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | import { setupServer } from 'msw/node'; 8 | 9 | export const server = setupServer(); 10 | -------------------------------------------------------------------------------- /packages/cli/src/utils/spawnWrapper.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright 2025 Google LLC 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | import { spawn } from 'node:child_process'; 8 | 9 | export const spawnWrapper = spawn; 10 | -------------------------------------------------------------------------------- /packages/core/src/utils/session.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright 2025 Google LLC 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | import { randomUUID } from 'node:crypto'; 8 | 9 | export const sessionId = randomUUID(); 10 | -------------------------------------------------------------------------------- /integration-tests/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "noEmit": true, 5 | "allowJs": true 6 | }, 7 | "include": ["**/*.ts"], 8 | "references": [{ "path": "../packages/core" }] 9 | } 10 | -------------------------------------------------------------------------------- /packages/cli/src/ui/utils/isNarrowWidth.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright 2025 Google LLC 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | export function isNarrowWidth(width: number): boolean { 8 | return width < 80; 9 | } 10 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | insert_final_newline = true 6 | end_of_line = lf 7 | indent_style = space 8 | indent_size = 2 9 | max_line_length = 80 10 | 11 | [Makefile] 12 | indent_style = tab 13 | indent_size = 8 14 | -------------------------------------------------------------------------------- /packages/a2a-server/src/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright 2025 Google LLC 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | export * from './agent/executor.js'; 8 | export * from './http/app.js'; 9 | export * from './types.js'; 10 | -------------------------------------------------------------------------------- /scripts/tests/test-setup.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright 2025 Google LLC 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | import { vi } from 'vitest'; 8 | 9 | vi.mock('fs', () => ({ 10 | ...vi.importActual('fs'), 11 | appendFileSync: vi.fn(), 12 | })); 13 | -------------------------------------------------------------------------------- /packages/core/src/utils/promptIdContext.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright 2025 Google LLC 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | import { AsyncLocalStorage } from 'node:async_hooks'; 8 | 9 | export const promptIdContext = new AsyncLocalStorage(); 10 | -------------------------------------------------------------------------------- /packages/core/src/commands/extensions.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright 2025 Google LLC 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | import type { Config } from '../config/config.js'; 8 | 9 | export function listExtensions(config: Config) { 10 | return config.getExtensions(); 11 | } 12 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | npm run pre-commit || { 2 | echo '' 3 | echo '====================================================' 4 | echo 'pre-commit checks failed. in case of emergency, run:' 5 | echo '' 6 | echo 'git commit --no-verify' 7 | echo '====================================================' 8 | exit 1 9 | } 10 | -------------------------------------------------------------------------------- /.lycheeignore: -------------------------------------------------------------------------------- 1 | http://localhost:16686/ 2 | https://github.com/google-gemini/gemini-cli/issues/new/choose 3 | https://github.com/google-gemini/maintainers-gemini-cli/blob/main/npm.md 4 | https://github.com/settings/personal-access-tokens/new 5 | https://github.com/settings/tokens/new 6 | https://www.npmjs.com/package/@google/gemini-cli 7 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | **/bundle 2 | **/coverage 3 | **/dist 4 | **/.git 5 | **/node_modules 6 | .docker 7 | .DS_Store 8 | .env 9 | .gemini/ 10 | .idea 11 | .integration-tests/ 12 | *.iml 13 | *.tsbuildinfo 14 | *.vsix 15 | bower_components 16 | eslint.config.js 17 | **/generated 18 | gha-creds-*.json 19 | junit.xml 20 | Thumbs.db 21 | -------------------------------------------------------------------------------- /packages/core/src/index.test.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright 2025 Google LLC 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | import { describe, it, expect } from 'vitest'; 8 | 9 | describe('placeholder tests', () => { 10 | it('should pass', () => { 11 | expect(true).toBe(true); 12 | }); 13 | }); 14 | -------------------------------------------------------------------------------- /packages/a2a-server/src/http/requestStorage.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright 2025 Google LLC 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | import type express from 'express'; 8 | import { AsyncLocalStorage } from 'node:async_hooks'; 9 | 10 | export const requestStorage = new AsyncLocalStorage<{ req: express.Request }>(); 11 | -------------------------------------------------------------------------------- /packages/cli/src/commands/extensions/examples/mcp-server/gemini-extension.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mcp-server-example", 3 | "version": "1.0.0", 4 | "mcpServers": { 5 | "nodeServer": { 6 | "command": "node", 7 | "args": ["${extensionPath}${/}dist${/}example.js"], 8 | "cwd": "${extensionPath}" 9 | } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /packages/test-utils/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "dist", 5 | "lib": ["DOM", "DOM.Iterable", "ES2021"], 6 | "composite": true, 7 | "types": ["node"] 8 | }, 9 | "include": ["index.ts", "src/**/*.ts", "src/**/*.json"], 10 | "exclude": ["node_modules", "dist"] 11 | } 12 | -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "semi": true, 3 | "trailingComma": "all", 4 | "singleQuote": true, 5 | "printWidth": 80, 6 | "tabWidth": 2, 7 | "overrides": [ 8 | { 9 | "files": ["**/*.md"], 10 | "options": { 11 | "tabWidth": 2, 12 | "printWidth": 80, 13 | "proseWrap": "always" 14 | } 15 | } 16 | ] 17 | } 18 | -------------------------------------------------------------------------------- /packages/cli/src/ui/contexts/ShellFocusContext.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright 2025 Google LLC 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | import { createContext, useContext } from 'react'; 8 | 9 | export const ShellFocusContext = createContext(true); 10 | 11 | export const useShellFocusState = () => useContext(ShellFocusContext); 12 | -------------------------------------------------------------------------------- /packages/a2a-server/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "dist", 5 | "lib": ["DOM", "DOM.Iterable", "ES2023"], 6 | "composite": true, 7 | "types": ["node", "vitest/globals"] 8 | }, 9 | "include": ["index.ts", "src/**/*.ts", "src/**/*.json"], 10 | "exclude": ["node_modules", "dist"] 11 | } 12 | -------------------------------------------------------------------------------- /.gemini/config.yaml: -------------------------------------------------------------------------------- 1 | # Config for the Gemini Pull Request Review Bot. 2 | # https://github.com/marketplace/gemini-code-assist 3 | have_fun: false 4 | code_review: 5 | disable: false 6 | comment_severity_threshold: 'HIGH' 7 | max_review_comments: -1 8 | pull_request_opened: 9 | help: false 10 | summary: true 11 | code_review: true 12 | ignore_patterns: [] 13 | -------------------------------------------------------------------------------- /packages/core/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "dist", 5 | "lib": ["DOM", "DOM.Iterable", "ES2023"], 6 | "composite": true, 7 | "types": ["node", "vitest/globals"] 8 | }, 9 | "include": ["index.ts", "src/**/*.ts", "src/**/*.d.ts", "src/**/*.json"], 10 | "exclude": ["node_modules", "dist"] 11 | } 12 | -------------------------------------------------------------------------------- /packages/cli/src/ui/textConstants.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright 2025 Google LLC 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | export const SCREEN_READER_USER_PREFIX = 'User: '; 8 | 9 | export const SCREEN_READER_MODEL_PREFIX = 'Model: '; 10 | 11 | export const SCREEN_READER_LOADING = 'loading'; 12 | 13 | export const SCREEN_READER_RESPONDING = 'responding'; 14 | -------------------------------------------------------------------------------- /packages/cli/src/utils/version.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright 2025 Google LLC 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | import { getPackageJson } from './package.js'; 8 | 9 | export async function getCliVersion(): Promise { 10 | const pkgJson = await getPackageJson(); 11 | return process.env['CLI_VERSION'] || pkgJson?.version || 'unknown'; 12 | } 13 | -------------------------------------------------------------------------------- /packages/cli/test-setup.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright 2025 Google LLC 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | // Unset NO_COLOR environment variable to ensure consistent theme behavior between local and CI test runs 8 | if (process.env.NO_COLOR !== undefined) { 9 | delete process.env.NO_COLOR; 10 | } 11 | 12 | import './src/test-utils/customMatchers.js'; 13 | -------------------------------------------------------------------------------- /packages/cli/src/utils/updateEventEmitter.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright 2025 Google LLC 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | import { EventEmitter } from 'node:events'; 8 | 9 | /** 10 | * A shared event emitter for application-wide communication 11 | * between decoupled parts of the CLI. 12 | */ 13 | export const updateEventEmitter = new EventEmitter(); 14 | -------------------------------------------------------------------------------- /packages/core/src/ide/constants.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright 2025 Google LLC 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | export const GEMINI_CLI_COMPANION_EXTENSION_NAME = 'Gemini CLI Companion'; 8 | export const IDE_MAX_OPEN_FILES = 10; 9 | export const IDE_MAX_SELECTED_TEXT_LENGTH = 16384; // 16 KiB limit 10 | export const IDE_REQUEST_TIMEOUT_MS = 10 * 60 * 1000; // 10 minutes 11 | -------------------------------------------------------------------------------- /packages/cli/src/commands/extensions/examples/mcp-server/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2022", 4 | "module": "NodeNext", 5 | "moduleResolution": "NodeNext", 6 | "strict": true, 7 | "esModuleInterop": true, 8 | "skipLibCheck": true, 9 | "forceConsistentCasingInFileNames": true, 10 | "outDir": "./dist" 11 | }, 12 | "include": ["example.ts"] 13 | } 14 | -------------------------------------------------------------------------------- /packages/vscode-ide-companion/.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | { 5 | "name": "Run Extension", 6 | "type": "extensionHost", 7 | "request": "launch", 8 | "args": ["--extensionDevelopmentPath=${workspaceFolder}"], 9 | "outFiles": ["${workspaceFolder}/out/**/*.js"], 10 | "preLaunchTask": "${defaultBuildTask}" 11 | } 12 | ] 13 | } 14 | -------------------------------------------------------------------------------- /packages/vscode-ide-companion/.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.0.0", 3 | "tasks": [ 4 | { 5 | "type": "npm", 6 | "script": "watch", 7 | "problemMatcher": "$tsc-watch", 8 | "isBackground": true, 9 | "presentation": { 10 | "reveal": "never" 11 | }, 12 | "group": { 13 | "kind": "build", 14 | "isDefault": true 15 | } 16 | } 17 | ] 18 | } 19 | -------------------------------------------------------------------------------- /packages/cli/src/ui/components/__snapshots__/LoadingIndicator.test.tsx.snap: -------------------------------------------------------------------------------- 1 | // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html 2 | 3 | exports[` > should truncate long primary text instead of wrapping 1`] = ` 4 | "MockResponding This is an extremely long loading phrase that should be truncated in t (esc to 5 | Spinner cancel, 5s)" 6 | `; 7 | -------------------------------------------------------------------------------- /packages/cli/src/utils/events.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright 2025 Google LLC 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | import { EventEmitter } from 'node:events'; 8 | 9 | export enum AppEvent { 10 | OpenDebugConsole = 'open-debug-console', 11 | LogError = 'log-error', 12 | OauthDisplayMessage = 'oauth-display-message', 13 | Flicker = 'flicker', 14 | } 15 | 16 | export const appEvents = new EventEmitter(); 17 | -------------------------------------------------------------------------------- /.github/workflows/docs-rebuild.yml: -------------------------------------------------------------------------------- 1 | name: 'Trigger Docs Rebuild' 2 | on: 3 | push: 4 | branches: 5 | - 'main' 6 | paths: 7 | - 'docs/**' 8 | jobs: 9 | trigger-rebuild: 10 | runs-on: 'ubuntu-latest' 11 | steps: 12 | - name: 'Trigger rebuild' 13 | run: | 14 | curl -X POST \ 15 | -H "Content-Type: application/json" \ 16 | -d '{}' \ 17 | "${{ secrets.DOCS_REBUILD_URL }}" 18 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Reporting Security Issues 2 | 3 | To report a security issue, please use [https://g.co/vulnz](https://g.co/vulnz). 4 | We use g.co/vulnz for our intake, and do coordination and disclosure here on 5 | GitHub (including using GitHub Security Advisory). The Google Security Team will 6 | respond within 5 working days of your report on g.co/vulnz. 7 | 8 | [GitHub Security Advisory]: 9 | https://github.com/google-gemini/gemini-cli/security/advisories 10 | -------------------------------------------------------------------------------- /packages/cli/src/utils/math.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright 2025 Google LLC 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | /** 8 | * Linearly interpolates between two values. 9 | * 10 | * @param start The start value. 11 | * @param end The end value. 12 | * @param t The interpolation amount (typically between 0 and 1). 13 | */ 14 | export const lerp = (start: number, end: number, t: number): number => 15 | start + (end - start) * t; 16 | -------------------------------------------------------------------------------- /third_party/get-ripgrep/src/index.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | /** 3 | * @license 4 | * Copyright 2023 Lvce Editor 5 | * SPDX-License-Identifier: MIT 6 | */ 7 | import { dirname, join } from 'node:path' 8 | import { fileURLToPath } from 'node:url' 9 | 10 | const __dirname = dirname(fileURLToPath(import.meta.url)) 11 | 12 | export const rgPath = join( 13 | __dirname, 14 | '..', 15 | 'bin', 16 | `rg${process.platform === 'win32' ? '.exe' : ''}`, 17 | ) 18 | -------------------------------------------------------------------------------- /integration-tests/context-compress-interactive.compress-empty.json: -------------------------------------------------------------------------------- 1 | { 2 | "generateContent": [ 3 | { 4 | "candidates": [ 5 | { 6 | "content": { 7 | "role": "model", 8 | "parts": [ 9 | { 10 | "text": "This is more than the 5 tokens we return below which will trigger an error" 11 | } 12 | ] 13 | } 14 | } 15 | ] 16 | } 17 | ] 18 | } 19 | -------------------------------------------------------------------------------- /packages/cli/src/ui/commands/corgiCommand.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright 2025 Google LLC 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | import { CommandKind, type SlashCommand } from './types.js'; 8 | 9 | export const corgiCommand: SlashCommand = { 10 | name: 'corgi', 11 | description: 'Toggles corgi mode', 12 | hidden: true, 13 | kind: CommandKind.BUILT_IN, 14 | action: (context, _args) => { 15 | context.ui.toggleCorgiMode(); 16 | }, 17 | }; 18 | -------------------------------------------------------------------------------- /packages/cli/src/ui/commands/modelCommand.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright 2025 Google LLC 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | import { CommandKind, type SlashCommand } from './types.js'; 8 | 9 | export const modelCommand: SlashCommand = { 10 | name: 'model', 11 | description: 'Opens a dialog to configure the model', 12 | kind: CommandKind.BUILT_IN, 13 | action: async () => ({ 14 | type: 'dialog', 15 | dialog: 'model', 16 | }), 17 | }; 18 | -------------------------------------------------------------------------------- /packages/core/src/mcp/token-storage/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright 2025 Google LLC 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | export * from './types.js'; 8 | export * from './base-token-storage.js'; 9 | export * from './file-token-storage.js'; 10 | export * from './hybrid-token-storage.js'; 11 | 12 | export const DEFAULT_SERVICE_NAME = 'gemini-cli-oauth'; 13 | export const FORCE_ENCRYPTED_FILE_ENV_VAR = 14 | 'GEMINI_FORCE_ENCRYPTED_FILE_STORAGE'; 15 | -------------------------------------------------------------------------------- /packages/core/test-setup.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright 2025 Google LLC 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | // Unset NO_COLOR environment variable to ensure consistent theme behavior between local and CI test runs 8 | if (process.env.NO_COLOR !== undefined) { 9 | delete process.env.NO_COLOR; 10 | } 11 | 12 | import { setSimulate429 } from './src/utils/testUtils.js'; 13 | 14 | // Disable 429 simulation globally for all tests 15 | setSimulate429(false); 16 | -------------------------------------------------------------------------------- /packages/test-utils/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@google/gemini-cli-test-utils", 3 | "version": "0.12.0-nightly.20251022.0542de95", 4 | "private": true, 5 | "main": "src/index.ts", 6 | "license": "Apache-2.0", 7 | "type": "module", 8 | "scripts": { 9 | "build": "node ../../scripts/build_package.js", 10 | "typecheck": "tsc --noEmit" 11 | }, 12 | "devDependencies": { 13 | "typescript": "^5.3.3" 14 | }, 15 | "engines": { 16 | "node": ">=20" 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /packages/cli/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "dist", 5 | "jsx": "react-jsx", 6 | "lib": ["DOM", "DOM.Iterable", "ES2023"], 7 | "types": ["node", "vitest/globals"] 8 | }, 9 | "include": [ 10 | "index.ts", 11 | "src/**/*.ts", 12 | "src/**/*.tsx", 13 | "src/**/*.json", 14 | "./package.json" 15 | ], 16 | "exclude": ["node_modules", "dist"], 17 | "references": [{ "path": "../core" }] 18 | } 19 | -------------------------------------------------------------------------------- /packages/cli/src/commands/extensions/examples/mcp-server/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mcp-server-example", 3 | "version": "1.0.0", 4 | "description": "Example MCP Server for Gemini CLI Extension", 5 | "type": "module", 6 | "main": "example.js", 7 | "scripts": { 8 | "build": "tsc" 9 | }, 10 | "devDependencies": { 11 | "typescript": "~5.4.5", 12 | "@types/node": "^20.11.25" 13 | }, 14 | "dependencies": { 15 | "@modelcontextprotocol/sdk": "^1.11.0", 16 | "zod": "^3.22.4" 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /packages/test-utils/vitest.config.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright 2025 Google LLC 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | import { defineConfig } from 'vitest/config'; 8 | 9 | export default defineConfig({ 10 | test: { 11 | reporters: ['default', 'junit'], 12 | silent: true, 13 | outputFile: { 14 | junit: 'junit.xml', 15 | }, 16 | poolOptions: { 17 | threads: { 18 | minThreads: 8, 19 | maxThreads: 16, 20 | }, 21 | }, 22 | }, 23 | }); 24 | -------------------------------------------------------------------------------- /packages/cli/src/ui/components/ShellModeIndicator.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright 2025 Google LLC 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | import type React from 'react'; 8 | import { Box, Text } from 'ink'; 9 | import { theme } from '../semantic-colors.js'; 10 | 11 | export const ShellModeIndicator: React.FC = () => ( 12 | 13 | 14 | shell mode enabled 15 | (esc to disable) 16 | 17 | 18 | ); 19 | -------------------------------------------------------------------------------- /packages/cli/src/ui/components/SessionSummaryDisplay.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright 2025 Google LLC 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | import type React from 'react'; 8 | import { StatsDisplay } from './StatsDisplay.js'; 9 | 10 | interface SessionSummaryDisplayProps { 11 | duration: string; 12 | } 13 | 14 | export const SessionSummaryDisplay: React.FC = ({ 15 | duration, 16 | }) => ( 17 | 18 | ); 19 | -------------------------------------------------------------------------------- /packages/a2a-server/src/commands/list-extensions.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright 2025 Google LLC 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | import { listExtensions, type Config } from '@google/gemini-cli-core'; 8 | import type { Command } from './command-registry.js'; 9 | 10 | export class ListExtensionsCommand implements Command { 11 | readonly names = ['extensions', 'extensions list']; 12 | 13 | async execute(config: Config, _: string[]): Promise { 14 | return listExtensions(config); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /packages/cli/src/ui/commands/themeCommand.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright 2025 Google LLC 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | import type { OpenDialogActionReturn, SlashCommand } from './types.js'; 8 | import { CommandKind } from './types.js'; 9 | 10 | export const themeCommand: SlashCommand = { 11 | name: 'theme', 12 | description: 'Change the theme', 13 | kind: CommandKind.BUILT_IN, 14 | action: (_context, _args): OpenDialogActionReturn => ({ 15 | type: 'dialog', 16 | dialog: 'theme', 17 | }), 18 | }; 19 | -------------------------------------------------------------------------------- /packages/cli/src/ui/commands/authCommand.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright 2025 Google LLC 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | import type { OpenDialogActionReturn, SlashCommand } from './types.js'; 8 | import { CommandKind } from './types.js'; 9 | 10 | export const authCommand: SlashCommand = { 11 | name: 'auth', 12 | description: 'Change the auth method', 13 | kind: CommandKind.BUILT_IN, 14 | action: (_context, _args): OpenDialogActionReturn => ({ 15 | type: 'dialog', 16 | dialog: 'auth', 17 | }), 18 | }; 19 | -------------------------------------------------------------------------------- /packages/cli/src/ui/commands/editorCommand.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright 2025 Google LLC 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | import { 8 | CommandKind, 9 | type OpenDialogActionReturn, 10 | type SlashCommand, 11 | } from './types.js'; 12 | 13 | export const editorCommand: SlashCommand = { 14 | name: 'editor', 15 | description: 'Set external editor preference', 16 | kind: CommandKind.BUILT_IN, 17 | action: (): OpenDialogActionReturn => ({ 18 | type: 'dialog', 19 | dialog: 'editor', 20 | }), 21 | }; 22 | -------------------------------------------------------------------------------- /packages/cli/src/ui/commands/privacyCommand.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright 2025 Google LLC 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | import type { OpenDialogActionReturn, SlashCommand } from './types.js'; 8 | import { CommandKind } from './types.js'; 9 | 10 | export const privacyCommand: SlashCommand = { 11 | name: 'privacy', 12 | description: 'Display the privacy notice', 13 | kind: CommandKind.BUILT_IN, 14 | action: (): OpenDialogActionReturn => ({ 15 | type: 'dialog', 16 | dialog: 'privacy', 17 | }), 18 | }; 19 | -------------------------------------------------------------------------------- /packages/core/src/telemetry/telemetry-utils.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright 2025 Google LLC 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | import { getLanguageFromFilePath } from '../utils/language-detection.js'; 8 | 9 | export function getProgrammingLanguage( 10 | args: Record, 11 | ): string | undefined { 12 | const filePath = args['file_path'] || args['path'] || args['absolute_path']; 13 | if (typeof filePath === 'string') { 14 | return getLanguageFromFilePath(filePath); 15 | } 16 | return undefined; 17 | } 18 | -------------------------------------------------------------------------------- /packages/cli/src/ui/components/shared/__snapshots__/EnumSelector.test.tsx.snap: -------------------------------------------------------------------------------- 1 | // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html 2 | 3 | exports[` > renders inactive state and matches snapshot 1`] = `"← 中文 (简体) →"`; 4 | 5 | exports[` > renders with numeric options and matches snapshot 1`] = `"← Medium →"`; 6 | 7 | exports[` > renders with single option and matches snapshot 1`] = `" Only Option"`; 8 | 9 | exports[` > renders with string options and matches snapshot 1`] = `"← English →"`; 10 | -------------------------------------------------------------------------------- /packages/cli/src/ui/commands/permissionsCommand.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright 2025 Google LLC 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | import type { OpenDialogActionReturn, SlashCommand } from './types.js'; 8 | import { CommandKind } from './types.js'; 9 | 10 | export const permissionsCommand: SlashCommand = { 11 | name: 'permissions', 12 | description: 'Manage folder trust settings', 13 | kind: CommandKind.BUILT_IN, 14 | action: (): OpenDialogActionReturn => ({ 15 | type: 'dialog', 16 | dialog: 'permissions', 17 | }), 18 | }; 19 | -------------------------------------------------------------------------------- /packages/cli/src/ui/commands/settingsCommand.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright 2025 Google LLC 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | import type { OpenDialogActionReturn, SlashCommand } from './types.js'; 8 | import { CommandKind } from './types.js'; 9 | 10 | export const settingsCommand: SlashCommand = { 11 | name: 'settings', 12 | description: 'View and edit Gemini CLI settings', 13 | kind: CommandKind.BUILT_IN, 14 | action: (_context, _args): OpenDialogActionReturn => ({ 15 | type: 'dialog', 16 | dialog: 'settings', 17 | }), 18 | }; 19 | -------------------------------------------------------------------------------- /packages/core/src/prompts/mcp-prompts.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright 2025 Google LLC 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | import type { Config } from '../config/config.js'; 8 | import type { DiscoveredMCPPrompt } from '../tools/mcp-client.js'; 9 | 10 | export function getMCPServerPrompts( 11 | config: Config, 12 | serverName: string, 13 | ): DiscoveredMCPPrompt[] { 14 | const promptRegistry = config.getPromptRegistry(); 15 | if (!promptRegistry) { 16 | return []; 17 | } 18 | return promptRegistry.getPromptsByServer(serverName); 19 | } 20 | -------------------------------------------------------------------------------- /packages/core/src/utils/formatters.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright 2025 Google LLC 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | export const bytesToMB = (bytes: number): number => bytes / (1024 * 1024); 8 | 9 | export const formatMemoryUsage = (bytes: number): string => { 10 | const gb = bytes / (1024 * 1024 * 1024); 11 | if (bytes < 1024 * 1024) { 12 | return `${(bytes / 1024).toFixed(1)} KB`; 13 | } 14 | if (bytes < 1024 * 1024 * 1024) { 15 | return `${bytesToMB(bytes).toFixed(1)} MB`; 16 | } 17 | return `${gb.toFixed(2)} GB`; 18 | }; 19 | -------------------------------------------------------------------------------- /packages/cli/src/utils/processUtils.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright 2025 Google LLC 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | import { runExitCleanup } from './cleanup.js'; 8 | 9 | /** 10 | * Exit code used to signal that the CLI should be relaunched. 11 | */ 12 | export const RELAUNCH_EXIT_CODE = 42; 13 | 14 | /** 15 | * Exits the process with a special code to signal that the parent process should relaunch it. 16 | */ 17 | export async function relaunchApp(): Promise { 18 | await runExitCleanup(); 19 | process.exit(RELAUNCH_EXIT_CODE); 20 | } 21 | -------------------------------------------------------------------------------- /packages/cli/src/ui/contexts/ConfigContext.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright 2025 Google LLC 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | import React, { useContext } from 'react'; 8 | import { type Config } from '@google/gemini-cli-core'; 9 | 10 | export const ConfigContext = React.createContext(undefined); 11 | 12 | export const useConfig = () => { 13 | const context = useContext(ConfigContext); 14 | if (context === undefined) { 15 | throw new Error('useConfig must be used within a ConfigProvider'); 16 | } 17 | return context; 18 | }; 19 | -------------------------------------------------------------------------------- /integration-tests/vitest.config.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright 2025 Google LLC 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | import { defineConfig } from 'vitest/config'; 8 | 9 | export default defineConfig({ 10 | test: { 11 | testTimeout: 300000, // 5 minutes 12 | globalSetup: './globalSetup.ts', 13 | reporters: ['default'], 14 | include: ['**/*.test.ts'], 15 | retry: 2, 16 | fileParallelism: true, 17 | poolOptions: { 18 | threads: { 19 | minThreads: 8, 20 | maxThreads: 16, 21 | }, 22 | }, 23 | }, 24 | }); 25 | -------------------------------------------------------------------------------- /packages/cli/src/config/extensions/variables.test.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright 2025 Google LLC 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | import { expect, describe, it } from 'vitest'; 8 | import { hydrateString } from './variables.js'; 9 | 10 | describe('hydrateString', () => { 11 | it('should replace a single variable', () => { 12 | const context = { 13 | extensionPath: 'path/my-extension', 14 | }; 15 | const result = hydrateString('Hello, ${extensionPath}!', context); 16 | expect(result).toBe('Hello, path/my-extension!'); 17 | }); 18 | }); 19 | -------------------------------------------------------------------------------- /packages/cli/src/ui/components/messages/__snapshots__/ToolMessageRawMarkdown.test.tsx.snap: -------------------------------------------------------------------------------- 1 | // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html 2 | 3 | exports[` - Raw Markdown Display Snapshots > renders with renderMarkdown=false '(raw markdown with syntax highlightin…' 1`] = ` 4 | " ✓ test-tool A tool for testing 5 | 6 | Test **bold** and \`code\` markdown" 7 | `; 8 | 9 | exports[` - Raw Markdown Display Snapshots > renders with renderMarkdown=true '(default)' 1`] = ` 10 | " ✓ test-tool A tool for testing 11 | 12 | Test bold and code markdown" 13 | `; 14 | -------------------------------------------------------------------------------- /scripts/pre-commit.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright 2025 Google LLC 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | import { execSync } from 'node:child_process'; 8 | import lintStaged from 'lint-staged'; 9 | 10 | try { 11 | // Get repository root 12 | const root = execSync('git rev-parse --show-toplevel').toString().trim(); 13 | 14 | // Run lint-staged with API directly 15 | const passed = await lintStaged({ cwd: root }); 16 | 17 | // Exit with appropriate code 18 | process.exit(passed ? 0 : 1); 19 | } catch { 20 | // Exit with error code 21 | process.exit(1); 22 | } 23 | -------------------------------------------------------------------------------- /packages/cli/src/ui/contexts/AppContext.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright 2025 Google LLC 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | import { createContext, useContext } from 'react'; 8 | 9 | export interface AppState { 10 | version: string; 11 | startupWarnings: string[]; 12 | } 13 | 14 | export const AppContext = createContext(null); 15 | 16 | export const useAppContext = () => { 17 | const context = useContext(AppContext); 18 | if (!context) { 19 | throw new Error('useAppContext must be used within an AppProvider'); 20 | } 21 | return context; 22 | }; 23 | -------------------------------------------------------------------------------- /packages/cli/src/ui/components/shared/__snapshots__/DescriptiveRadioButtonSelect.test.tsx.snap: -------------------------------------------------------------------------------- 1 | // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html 2 | 3 | exports[`DescriptiveRadioButtonSelect > should render correctly with custom props 1`] = ` 4 | "▲ 5 | 1. Foo Title 6 | This is Foo. 7 | ● 2. Bar Title 8 | This is Bar. 9 | 3. Baz Title 10 | This is Baz. 11 | ▼" 12 | `; 13 | 14 | exports[`DescriptiveRadioButtonSelect > should render correctly with default props 1`] = ` 15 | "● Foo Title 16 | This is Foo. 17 | Bar Title 18 | This is Bar. 19 | Baz Title 20 | This is Baz." 21 | `; 22 | -------------------------------------------------------------------------------- /packages/cli/src/ui/components/UpdateNotification.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright 2025 Google LLC 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | import { Box, Text } from 'ink'; 8 | import { theme } from '../semantic-colors.js'; 9 | 10 | interface UpdateNotificationProps { 11 | message: string; 12 | } 13 | 14 | export const UpdateNotification = ({ message }: UpdateNotificationProps) => ( 15 | 21 | {message} 22 | 23 | ); 24 | -------------------------------------------------------------------------------- /packages/core/src/config/constants.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright 2025 Google LLC 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | export interface FileFilteringOptions { 8 | respectGitIgnore: boolean; 9 | respectGeminiIgnore: boolean; 10 | } 11 | 12 | // For memory files 13 | export const DEFAULT_MEMORY_FILE_FILTERING_OPTIONS: FileFilteringOptions = { 14 | respectGitIgnore: false, 15 | respectGeminiIgnore: true, 16 | }; 17 | 18 | // For all other files 19 | export const DEFAULT_FILE_FILTERING_OPTIONS: FileFilteringOptions = { 20 | respectGitIgnore: true, 21 | respectGeminiIgnore: true, 22 | }; 23 | -------------------------------------------------------------------------------- /packages/cli/src/ui/components/RawMarkdownIndicator.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright 2025 Google LLC 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | import type React from 'react'; 8 | import { Box, Text } from 'ink'; 9 | import { theme } from '../semantic-colors.js'; 10 | 11 | export const RawMarkdownIndicator: React.FC = () => { 12 | const modKey = process.platform === 'darwin' ? 'option+m' : 'alt+m'; 13 | return ( 14 | 15 | 16 | raw markdown mode 17 | ({modKey} to toggle) 18 | 19 | 20 | ); 21 | }; 22 | -------------------------------------------------------------------------------- /packages/cli/src/ui/contexts/SettingsContext.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright 2025 Google LLC 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | import React, { useContext } from 'react'; 8 | import type { LoadedSettings } from '../../config/settings.js'; 9 | 10 | export const SettingsContext = React.createContext( 11 | undefined, 12 | ); 13 | 14 | export const useSettings = () => { 15 | const context = useContext(SettingsContext); 16 | if (context === undefined) { 17 | throw new Error('useSettings must be used within a SettingsProvider'); 18 | } 19 | return context; 20 | }; 21 | -------------------------------------------------------------------------------- /scripts/tests/vitest.config.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright 2025 Google LLC 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | import { defineConfig } from 'vitest/config'; 8 | 9 | export default defineConfig({ 10 | test: { 11 | globals: true, 12 | environment: 'node', 13 | include: ['scripts/tests/**/*.test.js'], 14 | setupFiles: ['scripts/tests/test-setup.ts'], 15 | coverage: { 16 | provider: 'v8', 17 | reporter: ['text', 'lcov'], 18 | }, 19 | poolOptions: { 20 | threads: { 21 | minThreads: 8, 22 | maxThreads: 16, 23 | }, 24 | }, 25 | }, 26 | }); 27 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "typescript.tsserver.experimental.enableProjectDiagnostics": true, 3 | "editor.tabSize": 2, 4 | "editor.rulers": [80], 5 | "editor.detectIndentation": false, 6 | "editor.insertSpaces": true, 7 | "[typescript]": { 8 | "editor.defaultFormatter": "esbenp.prettier-vscode" 9 | }, 10 | "[json]": { 11 | "editor.defaultFormatter": "esbenp.prettier-vscode" 12 | }, 13 | "[javascript]": { 14 | "editor.defaultFormatter": "esbenp.prettier-vscode" 15 | }, 16 | "[markdown]": { 17 | "editor.defaultFormatter": "esbenp.prettier-vscode" 18 | }, 19 | "vitest.disableWorkspaceWarning": true 20 | } 21 | -------------------------------------------------------------------------------- /packages/core/src/telemetry/activity-types.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright 2025 Google LLC 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | /** 8 | * Types of user activities that can be tracked 9 | */ 10 | export enum ActivityType { 11 | USER_INPUT_START = 'user_input_start', 12 | USER_INPUT_END = 'user_input_end', 13 | MESSAGE_ADDED = 'message_added', 14 | TOOL_CALL_SCHEDULED = 'tool_call_scheduled', 15 | TOOL_CALL_COMPLETED = 'tool_call_completed', 16 | STREAM_START = 'stream_start', 17 | STREAM_END = 'stream_end', 18 | HISTORY_UPDATED = 'history_updated', 19 | MANUAL_TRIGGER = 'manual_trigger', 20 | } 21 | -------------------------------------------------------------------------------- /packages/cli/src/patches/is-in-ci.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright 2025 Google LLC 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | // This is a replacement for the `is-in-ci` package that always returns false. 8 | // We are doing this to avoid the issue where `ink` does not render the UI 9 | // when it detects that it is running in a CI environment. 10 | // This is safe because `ink` (and thus `is-in-ci`) is only used in the 11 | // interactive code path of the CLI. 12 | // See issue #1563 for more details. 13 | 14 | const isInCi = false; 15 | 16 | // eslint-disable-next-line import/no-default-export 17 | export default isInCi; 18 | -------------------------------------------------------------------------------- /packages/cli/src/utils/resolvePath.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright 2025 Google LLC 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | import * as os from 'node:os'; 8 | import * as path from 'node:path'; 9 | 10 | export function resolvePath(p: string): string { 11 | if (!p) { 12 | return ''; 13 | } 14 | let expandedPath = p; 15 | if (p.toLowerCase().startsWith('%userprofile%')) { 16 | expandedPath = os.homedir() + p.substring('%userprofile%'.length); 17 | } else if (p === '~' || p.startsWith('~/')) { 18 | expandedPath = os.homedir() + p.substring(1); 19 | } 20 | return path.normalize(expandedPath); 21 | } 22 | -------------------------------------------------------------------------------- /packages/core/src/utils/messageInspectors.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright 2025 Google LLC 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | import type { Content } from '@google/genai'; 8 | 9 | export function isFunctionResponse(content: Content): boolean { 10 | return ( 11 | content.role === 'user' && 12 | !!content.parts && 13 | content.parts.every((part) => !!part.functionResponse) 14 | ); 15 | } 16 | 17 | export function isFunctionCall(content: Content): boolean { 18 | return ( 19 | content.role === 'model' && 20 | !!content.parts && 21 | content.parts.every((part) => !!part.functionCall) 22 | ); 23 | } 24 | -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.0.0", 3 | "tasks": [ 4 | { 5 | "type": "npm", 6 | "script": "build", 7 | "group": { 8 | "kind": "build", 9 | "isDefault": true 10 | }, 11 | "problemMatcher": [], 12 | "label": "npm: build", 13 | "detail": "scripts/build.sh" 14 | }, 15 | { 16 | "type": "npm", 17 | "script": "build", 18 | "path": "packages/vscode-ide-companion", 19 | "group": "build", 20 | "problemMatcher": [], 21 | "label": "npm: build: vscode-ide-companion", 22 | "detail": "npm run build -w packages/vscode-ide-companion" 23 | } 24 | ] 25 | } 26 | -------------------------------------------------------------------------------- /packages/core/src/core/geminiRequest.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright 2025 Google LLC 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | import { type PartListUnion } from '@google/genai'; 8 | import { partToString } from '../utils/partUtils.js'; 9 | 10 | /** 11 | * Represents a request to be sent to the Gemini API. 12 | * For now, it's an alias to PartListUnion as the primary content. 13 | * This can be expanded later to include other request parameters. 14 | */ 15 | export type GeminiCodeRequest = PartListUnion; 16 | 17 | export function partListUnionToString(value: PartListUnion): string { 18 | return partToString(value, { verbose: true }); 19 | } 20 | -------------------------------------------------------------------------------- /packages/cli/src/ui/components/CliSpinner.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright 2025 Google LLC 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | import Spinner from 'ink-spinner'; 8 | import { type ComponentProps, useEffect } from 'react'; 9 | 10 | // A top-level field to track the total number of active spinners. 11 | export let debugNumSpinners = 0; 12 | 13 | export type SpinnerProps = ComponentProps; 14 | 15 | export const CliSpinner = (props: SpinnerProps) => { 16 | useEffect(() => { 17 | debugNumSpinners++; 18 | return () => { 19 | debugNumSpinners--; 20 | }; 21 | }, []); 22 | 23 | return ; 24 | }; 25 | -------------------------------------------------------------------------------- /packages/cli/src/ui/contexts/StreamingContext.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright 2025 Google LLC 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | import React, { createContext } from 'react'; 8 | import type { StreamingState } from '../types.js'; 9 | 10 | export const StreamingContext = createContext( 11 | undefined, 12 | ); 13 | 14 | export const useStreamingContext = (): StreamingState => { 15 | const context = React.useContext(StreamingContext); 16 | if (context === undefined) { 17 | throw new Error( 18 | 'useStreamingContext must be used within a StreamingContextProvider', 19 | ); 20 | } 21 | return context; 22 | }; 23 | -------------------------------------------------------------------------------- /packages/cli/src/ui/hooks/useSettingsCommand.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright 2025 Google LLC 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | import { useState, useCallback } from 'react'; 8 | 9 | export function useSettingsCommand() { 10 | const [isSettingsDialogOpen, setIsSettingsDialogOpen] = useState(false); 11 | 12 | const openSettingsDialog = useCallback(() => { 13 | setIsSettingsDialogOpen(true); 14 | }, []); 15 | 16 | const closeSettingsDialog = useCallback(() => { 17 | setIsSettingsDialogOpen(false); 18 | }, []); 19 | 20 | return { 21 | isSettingsDialogOpen, 22 | openSettingsDialog, 23 | closeSettingsDialog, 24 | }; 25 | } 26 | -------------------------------------------------------------------------------- /.github/workflows/links.yml: -------------------------------------------------------------------------------- 1 | name: 'Links' 2 | 3 | on: 4 | push: 5 | branches: ['main'] 6 | pull_request: 7 | branches: ['main'] 8 | repository_dispatch: 9 | workflow_dispatch: 10 | schedule: 11 | - cron: '00 18 * * *' 12 | 13 | jobs: 14 | linkChecker: 15 | runs-on: 'ubuntu-latest' 16 | steps: 17 | - uses: 'actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8' # ratchet:actions/checkout@v5 18 | 19 | - name: 'Link Checker' 20 | id: 'lychee' 21 | uses: 'lycheeverse/lychee-action@885c65f3dc543b57c898c8099f4e08c8afd178a2' # ratchet: lycheeverse/lychee-action@v2.6.1 22 | with: 23 | args: '--verbose --no-progress ./**/*.md' 24 | -------------------------------------------------------------------------------- /packages/vscode-ide-companion/src/utils/logger.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright 2025 Google LLC 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | import * as vscode from 'vscode'; 8 | 9 | export function createLogger( 10 | context: vscode.ExtensionContext, 11 | logger: vscode.OutputChannel, 12 | ) { 13 | return (message: string) => { 14 | const isDevMode = 15 | context.extensionMode === vscode.ExtensionMode.Development; 16 | const isLoggingEnabled = vscode.workspace 17 | .getConfiguration('gemini-cli.debug') 18 | .get('logging.enabled'); 19 | 20 | if (isDevMode || isLoggingEnabled) { 21 | logger.appendLine(message); 22 | } 23 | }; 24 | } 25 | -------------------------------------------------------------------------------- /packages/cli/src/ui/components/views/__snapshots__/ChatList.test.tsx.snap: -------------------------------------------------------------------------------- 1 | // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html 2 | 3 | exports[` > handles invalid date formats gracefully 1`] = ` 4 | "List of saved conversations: 5 | 6 | - bad-date-chat (Invalid Date) 7 | 8 | Note: Newest last, oldest first" 9 | `; 10 | 11 | exports[` > renders correctly with a list of chats 1`] = ` 12 | "List of saved conversations: 13 | 14 | - chat-1 (2025-10-02 10:00:00) 15 | - another-chat (2025-10-01 12:30:00) 16 | 17 | Note: Newest last, oldest first" 18 | `; 19 | 20 | exports[` > renders correctly with no chats 1`] = `"No saved conversation checkpoints found."`; 21 | -------------------------------------------------------------------------------- /packages/cli/src/commands/extensions/uninstall.test.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright 2025 Google LLC 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | import { describe, it, expect } from 'vitest'; 8 | import { uninstallCommand } from './uninstall.js'; 9 | import yargs from 'yargs'; 10 | 11 | describe('extensions uninstall command', () => { 12 | it('should fail if no source is provided', () => { 13 | const validationParser = yargs([]) 14 | .command(uninstallCommand) 15 | .fail(false) 16 | .locale('en'); 17 | expect(() => validationParser.parse('uninstall')).toThrow( 18 | 'Not enough non-option arguments: got 0, need at least 1', 19 | ); 20 | }); 21 | }); 22 | -------------------------------------------------------------------------------- /packages/cli/src/utils/checks.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright 2025 Google LLC 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | /* Fail to compile on unexpected values. */ 8 | export function assumeExhaustive(_value: never): void {} 9 | 10 | /** 11 | * Throws an exception on unexpected values. 12 | * 13 | * A common use case is switch statements: 14 | * switch(enumValue) { 15 | * case Enum.A: 16 | * case Enum.B: 17 | * break; 18 | * default: 19 | * checkExhaustive(enumValue); 20 | * } 21 | */ 22 | export function checkExhaustive( 23 | value: never, 24 | msg = `unexpected value ${value}!`, 25 | ): never { 26 | assumeExhaustive(value); 27 | throw new Error(msg); 28 | } 29 | -------------------------------------------------------------------------------- /packages/cli/src/ui/commands/helpCommand.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright 2025 Google LLC 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | import type { SlashCommand } from './types.js'; 8 | import { CommandKind } from './types.js'; 9 | import { MessageType, type HistoryItemHelp } from '../types.js'; 10 | 11 | export const helpCommand: SlashCommand = { 12 | name: 'help', 13 | altNames: ['?'], 14 | kind: CommandKind.BUILT_IN, 15 | description: 'For help on gemini-cli', 16 | action: async (context) => { 17 | const helpItem: Omit = { 18 | type: MessageType.HELP, 19 | timestamp: new Date(), 20 | }; 21 | 22 | context.ui.addItem(helpItem, Date.now()); 23 | }, 24 | }; 25 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Set the default behavior for all files to automatically handle line endings. 2 | # This will ensure that all text files are normalized to use LF (line feed) 3 | # line endings in the repository, which helps prevent cross-platform issues. 4 | * text=auto eol=lf 5 | 6 | # Explicitly declare files that must have LF line endings for proper execution 7 | # on Unix-like systems. 8 | *.sh eol=lf 9 | *.bash eol=lf 10 | Makefile eol=lf 11 | 12 | # Explicitly declare binary file types to prevent Git from attempting to 13 | # normalize their line endings. 14 | *.png binary 15 | *.jpg binary 16 | *.jpeg binary 17 | *.gif binary 18 | *.ico binary 19 | *.pdf binary 20 | *.woff binary 21 | *.woff2 binary 22 | *.eot binary 23 | *.ttf binary 24 | *.otf binary 25 | -------------------------------------------------------------------------------- /packages/cli/src/core/theme.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright 2025 Google LLC 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | import { themeManager } from '../ui/themes/theme-manager.js'; 8 | import { type LoadedSettings } from '../config/settings.js'; 9 | 10 | /** 11 | * Validates the configured theme. 12 | * @param settings The loaded application settings. 13 | * @returns An error message if the theme is not found, otherwise null. 14 | */ 15 | export function validateTheme(settings: LoadedSettings): string | null { 16 | const effectiveTheme = settings.merged.ui?.theme; 17 | if (effectiveTheme && !themeManager.findThemeByName(effectiveTheme)) { 18 | return `Theme "${effectiveTheme}" not found.`; 19 | } 20 | return null; 21 | } 22 | -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | # By default, require reviews from the maintainers for all files. 2 | * @google-gemini/gemini-cli-maintainers 3 | 4 | # Require reviews from the release approvers for critical files. 5 | # These patterns override the rule above. 6 | /package.json @google-gemini/gemini-cli-askmode-approvers 7 | /package-lock.json @google-gemini/gemini-cli-askmode-approvers 8 | /GEMINI.md @google-gemini/gemini-cli-askmode-approvers 9 | /SECURITY.md @google-gemini/gemini-cli-askmode-approvers 10 | /LICENSE @google-gemini/gemini-cli-askmode-approvers 11 | /.github/workflows/ @google-gemini/gemini-cli-askmode-approvers 12 | /packages/cli/package.json @google-gemini/gemini-cli-askmode-approvers 13 | /packages/core/package.json @google-gemini/gemini-cli-askmode-approvers 14 | -------------------------------------------------------------------------------- /packages/cli/src/ui/semantic-colors.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright 2025 Google LLC 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | import { themeManager } from './themes/theme-manager.js'; 8 | import type { SemanticColors } from './themes/semantic-tokens.js'; 9 | 10 | export const theme: SemanticColors = { 11 | get text() { 12 | return themeManager.getSemanticColors().text; 13 | }, 14 | get background() { 15 | return themeManager.getSemanticColors().background; 16 | }, 17 | get border() { 18 | return themeManager.getSemanticColors().border; 19 | }, 20 | get ui() { 21 | return themeManager.getSemanticColors().ui; 22 | }, 23 | get status() { 24 | return themeManager.getSemanticColors().status; 25 | }, 26 | }; 27 | -------------------------------------------------------------------------------- /packages/cli/src/ui/commands/vimCommand.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright 2025 Google LLC 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | import type { SlashCommand } from './types.js'; 8 | import { CommandKind } from './types.js'; 9 | 10 | export const vimCommand: SlashCommand = { 11 | name: 'vim', 12 | description: 'Toggle vim mode on/off', 13 | kind: CommandKind.BUILT_IN, 14 | action: async (context, _args) => { 15 | const newVimState = await context.ui.toggleVimEnabled(); 16 | 17 | const message = newVimState 18 | ? 'Entered Vim mode. Run /vim again to exit.' 19 | : 'Exited Vim mode.'; 20 | return { 21 | type: 'message', 22 | messageType: 'info', 23 | content: message, 24 | }; 25 | }, 26 | }; 27 | -------------------------------------------------------------------------------- /packages/cli/src/ui/commands/profileCommand.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright 2025 Google LLC 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | import { isDevelopment } from '../../utils/installationInfo.js'; 8 | import { CommandKind, type SlashCommand } from './types.js'; 9 | 10 | export const profileCommand: SlashCommand | null = isDevelopment 11 | ? { 12 | name: 'profile', 13 | kind: CommandKind.BUILT_IN, 14 | description: 'Toggle the debug profile display', 15 | action: async (context) => { 16 | context.ui.toggleDebugProfiler(); 17 | return { 18 | type: 'message', 19 | messageType: 'info', 20 | content: 'Toggled profile display.', 21 | }; 22 | }, 23 | } 24 | : null; 25 | -------------------------------------------------------------------------------- /packages/cli/src/utils/processUtils.test.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright 2025 Google LLC 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | import { vi } from 'vitest'; 8 | import { RELAUNCH_EXIT_CODE, relaunchApp } from './processUtils.js'; 9 | import * as cleanup from './cleanup.js'; 10 | 11 | describe('processUtils', () => { 12 | const processExit = vi 13 | .spyOn(process, 'exit') 14 | .mockReturnValue(undefined as never); 15 | const runExitCleanup = vi.spyOn(cleanup, 'runExitCleanup'); 16 | 17 | it('should run cleanup and exit with the relaunch code', async () => { 18 | await relaunchApp(); 19 | expect(runExitCleanup).toHaveBeenCalledTimes(1); 20 | expect(processExit).toHaveBeenCalledWith(RELAUNCH_EXIT_CODE); 21 | }); 22 | }); 23 | -------------------------------------------------------------------------------- /packages/cli/src/commands/extensions/examples/context/GEMINI.md: -------------------------------------------------------------------------------- 1 | # Ink Library Screen Reader Guidance 2 | 3 | When building custom components, it's important to keep accessibility in mind. 4 | While Ink provides the building blocks, ensuring your components are accessible 5 | will make your CLIs usable by a wider audience. 6 | 7 | ## General Principles 8 | 9 | Provide screen reader-friendly output: Use the useIsScreenReaderEnabled hook to 10 | detect if a screen reader is active. You can then render a more descriptive 11 | output for screen reader users. Leverage ARIA props: For components that have a 12 | specific role (e.g., a checkbox or a button), use the aria-role, aria-state, and 13 | aria-label props on and to provide semantic meaning to screen 14 | readers. 15 | -------------------------------------------------------------------------------- /.gemini/commands/github/cleanup-back-to-main.toml: -------------------------------------------------------------------------------- 1 | description = "Go back to main and clean up the branch." 2 | 3 | prompt = """ 4 | I'm done with the work on this branch, and I'm ready to go back to main and clean up. 5 | 6 | Here is the workflow I'd like you to follow: 7 | 8 | 1. **Get Current Branch:** First, I need you to get the name of the current branch and save it. 9 | 2. **Branch Check:** Check if the current branch is `main`. If it is, I need you to stop and let me know. 10 | 3. **Go to Main:** Next, I need you to checkout the main branch. 11 | 4. **Pull Latest:** Once you are on the main branch, I need you to pull down the latest changes to make sure I'm up to date. 12 | 5. **Branch Cleanup:** Finally, I need you to delete the branch that you noted in the first step. 13 | """ 14 | -------------------------------------------------------------------------------- /.github/actions/setup-npmrc/action.yml: -------------------------------------------------------------------------------- 1 | name: 'Setup NPMRC' 2 | description: 'Sets up NPMRC with all the correct repos for readonly access.' 3 | 4 | inputs: 5 | github-token: 6 | description: 'the github token' 7 | required: true 8 | 9 | outputs: 10 | auth-token: 11 | description: 'The generated NPM auth token' 12 | value: '${{ steps.npm_auth_token.outputs.auth-token }}' 13 | 14 | runs: 15 | using: 'composite' 16 | steps: 17 | - name: 'Configure .npmrc' 18 | shell: 'bash' 19 | run: |- 20 | echo ""@google-gemini:registry=https://npm.pkg.github.com"" > ~/.npmrc 21 | echo ""//npm.pkg.github.com/:_authToken=${{ inputs.github-token }}"" >> ~/.npmrc 22 | echo ""@google:registry=https://wombat-dressing-room.appspot.com"" >> ~/.npmrc 23 | -------------------------------------------------------------------------------- /packages/cli/src/ui/components/messages/UserShellMessage.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright 2025 Google LLC 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | import type React from 'react'; 8 | import { Box, Text } from 'ink'; 9 | import { theme } from '../../semantic-colors.js'; 10 | 11 | interface UserShellMessageProps { 12 | text: string; 13 | } 14 | 15 | export const UserShellMessage: React.FC = ({ text }) => { 16 | // Remove leading '!' if present, as App.tsx adds it for the processor. 17 | const commandToDisplay = text.startsWith('!') ? text.substring(1) : text; 18 | 19 | return ( 20 | 21 | $ 22 | {commandToDisplay} 23 | 24 | ); 25 | }; 26 | -------------------------------------------------------------------------------- /packages/cli/src/ui/constants.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright 2025 Google LLC 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | const EstimatedArtWidth = 59; 8 | const BoxBorderWidth = 1; 9 | export const BOX_PADDING_X = 1; 10 | 11 | // Calculate width based on art, padding, and border 12 | export const UI_WIDTH = 13 | EstimatedArtWidth + BOX_PADDING_X * 2 + BoxBorderWidth * 2; // ~63 14 | 15 | export const STREAM_DEBOUNCE_MS = 100; 16 | 17 | export const SHELL_COMMAND_NAME = 'Shell Command'; 18 | 19 | export const SHELL_NAME = 'Shell'; 20 | 21 | // Tool status symbols used in ToolMessage component 22 | export const TOOL_STATUS = { 23 | SUCCESS: '✓', 24 | PENDING: 'o', 25 | EXECUTING: '⊷', 26 | CONFIRMING: '?', 27 | CANCELED: '-', 28 | ERROR: 'x', 29 | } as const; 30 | -------------------------------------------------------------------------------- /packages/cli/src/utils/windowTitle.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright 2025 Google LLC 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | /** 8 | * Computes the window title for the Gemini CLI application. 9 | * 10 | * @param folderName - The name of the current folder/workspace to display in the title 11 | * @returns The computed window title, either from CLI_TITLE environment variable or the default Gemini title 12 | */ 13 | export function computeWindowTitle(folderName: string): string { 14 | const title = process.env['CLI_TITLE'] || `Gemini - ${folderName}`; 15 | 16 | // Remove control characters that could cause issues in terminal titles 17 | return title.replace( 18 | // eslint-disable-next-line no-control-regex 19 | /[\x00-\x1F\x7F]/g, 20 | '', 21 | ); 22 | } 23 | -------------------------------------------------------------------------------- /packages/cli/src/ui/hooks/useTerminalSize.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright 2025 Google LLC 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | import { useEffect, useState } from 'react'; 8 | 9 | export function useTerminalSize(): { columns: number; rows: number } { 10 | const [size, setSize] = useState({ 11 | columns: process.stdout.columns || 60, 12 | rows: process.stdout.rows || 20, 13 | }); 14 | 15 | useEffect(() => { 16 | function updateSize() { 17 | setSize({ 18 | columns: process.stdout.columns || 60, 19 | rows: process.stdout.rows || 20, 20 | }); 21 | } 22 | 23 | process.stdout.on('resize', updateSize); 24 | return () => { 25 | process.stdout.off('resize', updateSize); 26 | }; 27 | }, []); 28 | 29 | return size; 30 | } 31 | -------------------------------------------------------------------------------- /packages/core/src/commands/extensions.test.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright 2025 Google LLC 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | import { describe, it, expect, vi } from 'vitest'; 8 | import { listExtensions } from './extensions.js'; 9 | import type { Config } from '../config/config.js'; 10 | 11 | describe('listExtensions', () => { 12 | it('should call config.getExtensions and return the result', () => { 13 | const mockExtensions = [{ name: 'ext1' }, { name: 'ext2' }]; 14 | const mockConfig = { 15 | getExtensions: vi.fn().mockReturnValue(mockExtensions), 16 | } as unknown as Config; 17 | 18 | const result = listExtensions(mockConfig); 19 | 20 | expect(mockConfig.getExtensions).toHaveBeenCalledTimes(1); 21 | expect(result).toEqual(mockExtensions); 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /packages/cli/src/ui/hooks/useModelCommand.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright 2025 Google LLC 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | import { useState, useCallback } from 'react'; 8 | 9 | interface UseModelCommandReturn { 10 | isModelDialogOpen: boolean; 11 | openModelDialog: () => void; 12 | closeModelDialog: () => void; 13 | } 14 | 15 | export const useModelCommand = (): UseModelCommandReturn => { 16 | const [isModelDialogOpen, setIsModelDialogOpen] = useState(false); 17 | 18 | const openModelDialog = useCallback(() => { 19 | setIsModelDialogOpen(true); 20 | }, []); 21 | 22 | const closeModelDialog = useCallback(() => { 23 | setIsModelDialogOpen(false); 24 | }, []); 25 | 26 | return { 27 | isModelDialogOpen, 28 | openModelDialog, 29 | closeModelDialog, 30 | }; 31 | }; 32 | -------------------------------------------------------------------------------- /packages/core/src/fallback/types.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright 2025 Google LLC 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | /** 8 | * Defines the intent returned by the UI layer during a fallback scenario. 9 | */ 10 | export type FallbackIntent = 11 | | 'retry' // Immediately retry the current request with the fallback model. 12 | | 'stop' // Switch to fallback for future requests, but stop the current request. 13 | | 'auth'; // Stop the current request; user intends to change authentication. 14 | 15 | /** 16 | * The interface for the handler provided by the UI layer (e.g., the CLI) 17 | * to interact with the user during a fallback scenario. 18 | */ 19 | export type FallbackModelHandler = ( 20 | failedModel: string, 21 | fallbackModel: string, 22 | error?: unknown, 23 | ) => Promise; 24 | -------------------------------------------------------------------------------- /docs/cli/token-caching.md: -------------------------------------------------------------------------------- 1 | # Token Caching and Cost Optimization 2 | 3 | Gemini CLI automatically optimizes API costs through token caching when using 4 | API key authentication (Gemini API key or Vertex AI). This feature reuses 5 | previous system instructions and context to reduce the number of tokens 6 | processed in subsequent requests. 7 | 8 | **Token caching is available for:** 9 | 10 | - API key users (Gemini API key) 11 | - Vertex AI users (with project and location setup) 12 | 13 | **Token caching is not available for:** 14 | 15 | - OAuth users (Google Personal/Enterprise accounts) - the Code Assist API does 16 | not support cached content creation at this time 17 | 18 | You can view your token usage and cached token savings using the `/stats` 19 | command. When cached tokens are available, they will be displayed in the stats 20 | output. 21 | -------------------------------------------------------------------------------- /packages/cli/src/ui/components/ContextUsageDisplay.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright 2025 Google LLC 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | import { Text } from 'ink'; 8 | import { theme } from '../semantic-colors.js'; 9 | import { tokenLimit } from '@google/gemini-cli-core'; 10 | 11 | export const ContextUsageDisplay = ({ 12 | promptTokenCount, 13 | model, 14 | terminalWidth, 15 | }: { 16 | promptTokenCount: number; 17 | model: string; 18 | terminalWidth: number; 19 | }) => { 20 | const percentage = promptTokenCount / tokenLimit(model); 21 | const percentageLeft = ((1 - percentage) * 100).toFixed(0); 22 | 23 | const label = terminalWidth < 100 ? '%' : '% context left'; 24 | 25 | return ( 26 | 27 | ({percentageLeft} 28 | {label}) 29 | 30 | ); 31 | }; 32 | -------------------------------------------------------------------------------- /packages/core/src/telemetry/telemetryAttributes.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright 2025 Google LLC 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | import type { Attributes } from '@opentelemetry/api'; 8 | import type { Config } from '../config/config.js'; 9 | import { InstallationManager } from '../utils/installationManager.js'; 10 | import { UserAccountManager } from '../utils/userAccountManager.js'; 11 | 12 | const userAccountManager = new UserAccountManager(); 13 | const installationManager = new InstallationManager(); 14 | 15 | export function getCommonAttributes(config: Config): Attributes { 16 | const email = userAccountManager.getCachedGoogleAccount(); 17 | return { 18 | 'session.id': config.getSessionId(), 19 | 'installation.id': installationManager.getInstallationId(), 20 | ...(email && { 'user.email': email }), 21 | }; 22 | } 23 | -------------------------------------------------------------------------------- /packages/cli/src/ui/components/messages/ErrorMessage.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright 2025 Google LLC 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | import type React from 'react'; 8 | import { Text, Box } from 'ink'; 9 | import { theme } from '../../semantic-colors.js'; 10 | 11 | interface ErrorMessageProps { 12 | text: string; 13 | } 14 | 15 | export const ErrorMessage: React.FC = ({ text }) => { 16 | const prefix = '✕ '; 17 | const prefixWidth = prefix.length; 18 | 19 | return ( 20 | 21 | 22 | {prefix} 23 | 24 | 25 | 26 | {text} 27 | 28 | 29 | 30 | ); 31 | }; 32 | -------------------------------------------------------------------------------- /packages/cli/index.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | /** 4 | * @license 5 | * Copyright 2025 Google LLC 6 | * SPDX-License-Identifier: Apache-2.0 7 | */ 8 | 9 | import './src/gemini.js'; 10 | import { main } from './src/gemini.js'; 11 | import { debugLogger, FatalError } from '@google/gemini-cli-core'; 12 | 13 | // --- Global Entry Point --- 14 | main().catch((error) => { 15 | if (error instanceof FatalError) { 16 | let errorMessage = error.message; 17 | if (!process.env['NO_COLOR']) { 18 | errorMessage = `\x1b[31m${errorMessage}\x1b[0m`; 19 | } 20 | debugLogger.error(errorMessage); 21 | process.exit(error.exitCode); 22 | } 23 | debugLogger.error('An unexpected critical error occurred:'); 24 | if (error instanceof Error) { 25 | debugLogger.error(error.stack); 26 | } else { 27 | debugLogger.error(String(error)); 28 | } 29 | process.exit(1); 30 | }); 31 | -------------------------------------------------------------------------------- /packages/cli/src/ui/hooks/useKittyKeyboardProtocol.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright 2025 Google LLC 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | import { useState } from 'react'; 8 | import { 9 | isKittyProtocolEnabled, 10 | isKittyProtocolSupported, 11 | } from '../utils/kittyProtocolDetector.js'; 12 | 13 | export interface KittyProtocolStatus { 14 | supported: boolean; 15 | enabled: boolean; 16 | checking: boolean; 17 | } 18 | 19 | /** 20 | * Hook that returns the cached Kitty keyboard protocol status. 21 | * Detection is done once at app startup to avoid repeated queries. 22 | */ 23 | export function useKittyKeyboardProtocol(): KittyProtocolStatus { 24 | const [status] = useState({ 25 | supported: isKittyProtocolSupported(), 26 | enabled: isKittyProtocolEnabled(), 27 | checking: false, 28 | }); 29 | 30 | return status; 31 | } 32 | -------------------------------------------------------------------------------- /packages/cli/src/commands/mcp.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright 2025 Google LLC 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | // File for 'gemini mcp' command 8 | import type { CommandModule, Argv } from 'yargs'; 9 | import { addCommand } from './mcp/add.js'; 10 | import { removeCommand } from './mcp/remove.js'; 11 | import { listCommand } from './mcp/list.js'; 12 | 13 | export const mcpCommand: CommandModule = { 14 | command: 'mcp', 15 | describe: 'Manage MCP servers', 16 | builder: (yargs: Argv) => 17 | yargs 18 | .command(addCommand) 19 | .command(removeCommand) 20 | .command(listCommand) 21 | .demandCommand(1, 'You need at least one command before continuing.') 22 | .version(false), 23 | handler: () => { 24 | // yargs will automatically show help if no subcommand is provided 25 | // thanks to demandCommand(1) in the builder. 26 | }, 27 | }; 28 | -------------------------------------------------------------------------------- /packages/cli/src/services/types.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright 2025 Google LLC 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | import type { SlashCommand } from '../ui/commands/types.js'; 8 | 9 | /** 10 | * Defines the contract for any class that can load and provide slash commands. 11 | * This allows the CommandService to be extended with new command sources 12 | * (e.g., file-based, remote APIs) without modification. 13 | * 14 | * Loaders should receive any necessary dependencies (like Config) via their 15 | * constructor. 16 | */ 17 | export interface ICommandLoader { 18 | /** 19 | * Discovers and returns a list of slash commands from the loader's source. 20 | * @param signal An AbortSignal to allow cancellation. 21 | * @returns A promise that resolves to an array of SlashCommand objects. 22 | */ 23 | loadCommands(signal: AbortSignal): Promise; 24 | } 25 | -------------------------------------------------------------------------------- /packages/cli/src/config/extensions/variableSchema.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright 2025 Google LLC 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | export interface VariableDefinition { 8 | type: 'string'; 9 | description: string; 10 | default?: string; 11 | required?: boolean; 12 | } 13 | 14 | export interface VariableSchema { 15 | [key: string]: VariableDefinition; 16 | } 17 | 18 | const PATH_SEPARATOR_DEFINITION = { 19 | type: 'string', 20 | description: 'The path separator.', 21 | } as const; 22 | 23 | export const VARIABLE_SCHEMA = { 24 | extensionPath: { 25 | type: 'string', 26 | description: 'The path of the extension in the filesystem.', 27 | }, 28 | workspacePath: { 29 | type: 'string', 30 | description: 'The absolute path of the current workspace.', 31 | }, 32 | '/': PATH_SEPARATOR_DEFINITION, 33 | pathSeparator: PATH_SEPARATOR_DEFINITION, 34 | } as const; 35 | -------------------------------------------------------------------------------- /packages/cli/src/ui/components/ExitWarning.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright 2025 Google LLC 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | import type React from 'react'; 8 | import { Box, Text } from 'ink'; 9 | import { useUIState } from '../contexts/UIStateContext.js'; 10 | import { theme } from '../semantic-colors.js'; 11 | 12 | export const ExitWarning: React.FC = () => { 13 | const uiState = useUIState(); 14 | return ( 15 | <> 16 | {uiState.dialogsVisible && uiState.ctrlCPressedOnce && ( 17 | 18 | Press Ctrl+C again to exit. 19 | 20 | )} 21 | 22 | {uiState.dialogsVisible && uiState.ctrlDPressedOnce && ( 23 | 24 | Press Ctrl+D again to exit. 25 | 26 | )} 27 | 28 | ); 29 | }; 30 | -------------------------------------------------------------------------------- /integration-tests/telemetry.test.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright 2025 Google LLC 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | import { describe, it, expect } from 'vitest'; 8 | import { TestRig } from './test-helper.js'; 9 | 10 | describe('telemetry', () => { 11 | it('should emit a metric and a log event', async () => { 12 | const rig = new TestRig(); 13 | rig.setup('should emit a metric and a log event'); 14 | 15 | // Run a simple command that should trigger telemetry 16 | await rig.run('just saying hi'); 17 | 18 | // Verify that a user_prompt event was logged 19 | const hasUserPromptEvent = await rig.waitForTelemetryEvent('user_prompt'); 20 | expect(hasUserPromptEvent).toBe(true); 21 | 22 | // Verify that a cli_command_count metric was emitted 23 | const cliCommandCountMetric = rig.readMetric('session.count'); 24 | expect(cliCommandCountMetric).not.toBeNull(); 25 | }); 26 | }); 27 | -------------------------------------------------------------------------------- /packages/a2a-server/src/commands/command-registry.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright 2025 Google LLC 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | import { ListExtensionsCommand } from './list-extensions.js'; 8 | import type { Config } from '@google/gemini-cli-core'; 9 | 10 | export interface Command { 11 | readonly names: string[]; 12 | execute(config: Config, args: string[]): Promise; 13 | } 14 | 15 | class CommandRegistry { 16 | private readonly commands = new Map(); 17 | 18 | constructor() { 19 | this.register(new ListExtensionsCommand()); 20 | } 21 | 22 | register(command: Command) { 23 | for (const name of command.names) { 24 | this.commands.set(name, command); 25 | } 26 | } 27 | 28 | get(commandName: string): Command | undefined { 29 | return this.commands.get(commandName); 30 | } 31 | } 32 | 33 | export const commandRegistry = new CommandRegistry(); 34 | -------------------------------------------------------------------------------- /packages/a2a-server/src/http/server.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | /** 4 | * @license 5 | * Copyright 2025 Google LLC 6 | * SPDX-License-Identifier: Apache-2.0 7 | */ 8 | 9 | import * as url from 'node:url'; 10 | import * as path from 'node:path'; 11 | 12 | import { logger } from '../utils/logger.js'; 13 | import { main } from './app.js'; 14 | 15 | // Check if the module is the main script being run 16 | const isMainModule = 17 | path.basename(process.argv[1]) === 18 | path.basename(url.fileURLToPath(import.meta.url)); 19 | 20 | if ( 21 | import.meta.url.startsWith('file:') && 22 | isMainModule && 23 | process.env['NODE_ENV'] !== 'test' 24 | ) { 25 | process.on('uncaughtException', (error) => { 26 | logger.error('Unhandled exception:', error); 27 | process.exit(1); 28 | }); 29 | 30 | main().catch((error) => { 31 | logger.error('[CoreAgent] Unhandled error in main:', error); 32 | process.exit(1); 33 | }); 34 | } 35 | -------------------------------------------------------------------------------- /packages/vscode-ide-companion/eslint.config.mjs: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright 2025 Google LLC 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | import typescriptEslint from '@typescript-eslint/eslint-plugin'; 8 | import tsParser from '@typescript-eslint/parser'; 9 | 10 | export default [ 11 | { 12 | files: ['**/*.ts'], 13 | }, 14 | { 15 | plugins: { 16 | '@typescript-eslint': typescriptEslint, 17 | }, 18 | 19 | languageOptions: { 20 | parser: tsParser, 21 | ecmaVersion: 2022, 22 | sourceType: 'module', 23 | }, 24 | 25 | rules: { 26 | '@typescript-eslint/naming-convention': [ 27 | 'warn', 28 | { 29 | selector: 'import', 30 | format: ['camelCase', 'PascalCase'], 31 | }, 32 | ], 33 | 34 | curly: 'warn', 35 | eqeqeq: 'warn', 36 | 'no-throw-literal': 'warn', 37 | semi: 'warn', 38 | }, 39 | }, 40 | ]; 41 | -------------------------------------------------------------------------------- /integration-tests/context-compress-interactive.compress.json: -------------------------------------------------------------------------------- 1 | { 2 | "generateContent": [ 3 | { 4 | "candidates": [ 5 | { 6 | "content": { 7 | "role": "model", 8 | "parts": [ 9 | { 10 | "text": "A summary of the conversation." 11 | } 12 | ] 13 | } 14 | } 15 | ] 16 | } 17 | ], 18 | "generateContentStream": [ 19 | [ 20 | { 21 | "candidates": [ 22 | { 23 | "content": { 24 | "role": "model", 25 | "parts": [ 26 | { 27 | "text": "The initial response from the model" 28 | } 29 | ] 30 | }, 31 | "finishReason": "STOP" 32 | } 33 | ], 34 | "usageMetadata": { 35 | "promptTokenCount": 100000 36 | } 37 | } 38 | ] 39 | ] 40 | } 41 | -------------------------------------------------------------------------------- /packages/cli/src/ui/components/messages/WarningMessage.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright 2025 Google LLC 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | import type React from 'react'; 8 | import { Box, Text } from 'ink'; 9 | import { Colors } from '../../colors.js'; 10 | import { RenderInline } from '../../utils/InlineMarkdownRenderer.js'; 11 | 12 | interface WarningMessageProps { 13 | text: string; 14 | } 15 | 16 | export const WarningMessage: React.FC = ({ text }) => { 17 | const prefix = '⚠ '; 18 | const prefixWidth = 3; 19 | 20 | return ( 21 | 22 | 23 | {prefix} 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | ); 32 | }; 33 | -------------------------------------------------------------------------------- /packages/a2a-server/src/utils/logger.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright 2025 Google LLC 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | import winston from 'winston'; 8 | 9 | const logger = winston.createLogger({ 10 | level: 'info', 11 | format: winston.format.combine( 12 | // First, add a timestamp to the log info object 13 | winston.format.timestamp({ 14 | format: 'YYYY-MM-DD HH:mm:ss.SSS A', // Custom timestamp format 15 | }), 16 | // Here we define the custom output format 17 | winston.format.printf((info) => { 18 | const { level, timestamp, message, ...rest } = info; 19 | return ( 20 | `[${level.toUpperCase()}] ${timestamp} -- ${message}` + 21 | `${Object.keys(rest).length > 0 ? `\n${JSON.stringify(rest, null, 2)}` : ''}` 22 | ); // Only print ...rest if present 23 | }), 24 | ), 25 | transports: [new winston.transports.Console()], 26 | }); 27 | 28 | export { logger }; 29 | -------------------------------------------------------------------------------- /packages/cli/src/ui/components/messages/InfoMessage.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright 2025 Google LLC 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | import type React from 'react'; 8 | import { Text, Box } from 'ink'; 9 | import { theme } from '../../semantic-colors.js'; 10 | import { RenderInline } from '../../utils/InlineMarkdownRenderer.js'; 11 | 12 | interface InfoMessageProps { 13 | text: string; 14 | } 15 | 16 | export const InfoMessage: React.FC = ({ text }) => { 17 | const prefix = 'ℹ '; 18 | const prefixWidth = prefix.length; 19 | 20 | return ( 21 | 22 | 23 | {prefix} 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | ); 32 | }; 33 | -------------------------------------------------------------------------------- /packages/core/vitest.config.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright 2025 Google LLC 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | import { defineConfig } from 'vitest/config'; 8 | 9 | export default defineConfig({ 10 | test: { 11 | reporters: ['default', 'junit'], 12 | silent: true, 13 | setupFiles: ['./test-setup.ts'], 14 | outputFile: { 15 | junit: 'junit.xml', 16 | }, 17 | coverage: { 18 | enabled: true, 19 | provider: 'v8', 20 | reportsDirectory: './coverage', 21 | include: ['src/**/*'], 22 | reporter: [ 23 | ['text', { file: 'full-text-summary.txt' }], 24 | 'html', 25 | 'json', 26 | 'lcov', 27 | 'cobertura', 28 | ['json-summary', { outputFile: 'coverage-summary.json' }], 29 | ], 30 | }, 31 | poolOptions: { 32 | threads: { 33 | minThreads: 8, 34 | maxThreads: 16, 35 | }, 36 | }, 37 | }, 38 | }); 39 | -------------------------------------------------------------------------------- /packages/vscode-ide-companion/development.md: -------------------------------------------------------------------------------- 1 | # Local Development ⚙️ 2 | 3 | ## Running the Extension 4 | 5 | To run the extension locally for development, we recommend using the automatic 6 | watch process for continuous compilation: 7 | 8 | 1. **Install Dependencies** (from the root of the repository): 9 | ```bash 10 | npm install 11 | ``` 12 | 2. **Open in VS Code:** Open this directory (`packages/vscode-ide-companion`) 13 | in your VS Code editor. 14 | 3. **Start Watch Mode:** Run the watch script to compile the extension and 15 | monitor changes in both **esbuild** and **TypeScript**: 16 | ```bash 17 | npm run watch 18 | ``` 19 | 4. **Launch Host:** Press **`F5`** (or **`fn+F5`** on Mac) to open a new 20 | **Extension Development Host** window with the extension running. 21 | 22 | ### Manual Build 23 | 24 | If you only need to compile the extension once without watching for changes: 25 | 26 | ```bash 27 | npm run build 28 | ``` 29 | -------------------------------------------------------------------------------- /packages/cli/src/ui/App.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright 2025 Google LLC 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | import { useIsScreenReaderEnabled } from 'ink'; 8 | import { useUIState } from './contexts/UIStateContext.js'; 9 | import { StreamingContext } from './contexts/StreamingContext.js'; 10 | import { QuittingDisplay } from './components/QuittingDisplay.js'; 11 | import { ScreenReaderAppLayout } from './layouts/ScreenReaderAppLayout.js'; 12 | import { DefaultAppLayout } from './layouts/DefaultAppLayout.js'; 13 | 14 | export const App = () => { 15 | const uiState = useUIState(); 16 | const isScreenReaderEnabled = useIsScreenReaderEnabled(); 17 | 18 | if (uiState.quittingMessages) { 19 | return ; 20 | } 21 | 22 | return ( 23 | 24 | {isScreenReaderEnabled ? : } 25 | 26 | ); 27 | }; 28 | -------------------------------------------------------------------------------- /packages/core/src/routing/strategies/defaultStrategy.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright 2025 Google LLC 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | import type { Config } from '../../config/config.js'; 8 | import type { BaseLlmClient } from '../../core/baseLlmClient.js'; 9 | import type { 10 | RoutingContext, 11 | RoutingDecision, 12 | TerminalStrategy, 13 | } from '../routingStrategy.js'; 14 | import { DEFAULT_GEMINI_MODEL } from '../../config/models.js'; 15 | 16 | export class DefaultStrategy implements TerminalStrategy { 17 | readonly name = 'default'; 18 | 19 | async route( 20 | _context: RoutingContext, 21 | _config: Config, 22 | _baseLlmClient: BaseLlmClient, 23 | ): Promise { 24 | return { 25 | model: DEFAULT_GEMINI_MODEL, 26 | metadata: { 27 | source: this.name, 28 | latencyMs: 0, 29 | reasoning: `Routing to default model: ${DEFAULT_GEMINI_MODEL}`, 30 | }, 31 | }; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /integration-tests/context-compress-interactive.compress-failure.json: -------------------------------------------------------------------------------- 1 | { 2 | "generateContent": [ 3 | { 4 | "candidates": [ 5 | { 6 | "content": { 7 | "role": "model", 8 | "parts": [ 9 | { 10 | "text": "This is more than the 5 tokens we return below which will trigger an error" 11 | } 12 | ] 13 | } 14 | } 15 | ] 16 | } 17 | ], 18 | "generateContentStream": [ 19 | [ 20 | { 21 | "candidates": [ 22 | { 23 | "content": { 24 | "role": "model", 25 | "parts": [ 26 | { 27 | "text": "The initial response from the model" 28 | } 29 | ] 30 | }, 31 | "finishReason": "STOP" 32 | } 33 | ], 34 | "usageMetadata": { 35 | "promptTokenCount": 5 36 | } 37 | } 38 | ] 39 | ] 40 | } 41 | -------------------------------------------------------------------------------- /packages/cli/src/core/auth.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright 2025 Google LLC 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | import { 8 | type AuthType, 9 | type Config, 10 | getErrorMessage, 11 | } from '@google/gemini-cli-core'; 12 | 13 | /** 14 | * Handles the initial authentication flow. 15 | * @param config The application config. 16 | * @param authType The selected auth type. 17 | * @returns An error message if authentication fails, otherwise null. 18 | */ 19 | export async function performInitialAuth( 20 | config: Config, 21 | authType: AuthType | undefined, 22 | ): Promise { 23 | if (!authType) { 24 | return null; 25 | } 26 | 27 | try { 28 | await config.refreshAuth(authType); 29 | // The console.log is intentionally left out here. 30 | // We can add a dedicated startup message later if needed. 31 | } catch (e) { 32 | return `Failed to login. Message: ${getErrorMessage(e)}`; 33 | } 34 | 35 | return null; 36 | } 37 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "strict": true, 4 | "esModuleInterop": true, 5 | "skipLibCheck": true, 6 | "noImplicitAny": true, 7 | "noImplicitOverride": true, 8 | "noImplicitReturns": true, 9 | "noImplicitThis": true, 10 | "forceConsistentCasingInFileNames": true, 11 | "noPropertyAccessFromIndexSignature": true, 12 | "noUnusedLocals": true, 13 | "strictBindCallApply": true, 14 | "strictFunctionTypes": true, 15 | "strictNullChecks": true, 16 | "strictPropertyInitialization": true, 17 | "resolveJsonModule": true, 18 | "sourceMap": true, 19 | "composite": true, 20 | "incremental": true, 21 | "declaration": true, 22 | "allowSyntheticDefaultImports": true, 23 | "verbatimModuleSyntax": true, 24 | "lib": ["ES2023"], 25 | "module": "NodeNext", 26 | "moduleResolution": "nodenext", 27 | "target": "es2022", 28 | "types": ["node", "vitest/globals"], 29 | "jsx": "react-jsx" 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /packages/cli/src/utils/package.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright 2025 Google LLC 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | import { 8 | readPackageUp, 9 | type PackageJson as BasePackageJson, 10 | } from 'read-package-up'; 11 | import { fileURLToPath } from 'node:url'; 12 | import path from 'node:path'; 13 | 14 | export type PackageJson = BasePackageJson & { 15 | config?: { 16 | sandboxImageUri?: string; 17 | }; 18 | }; 19 | 20 | const __filename = fileURLToPath(import.meta.url); 21 | const __dirname = path.dirname(__filename); 22 | 23 | let packageJson: PackageJson | undefined; 24 | 25 | export async function getPackageJson(): Promise { 26 | if (packageJson) { 27 | return packageJson; 28 | } 29 | 30 | const result = await readPackageUp({ cwd: __dirname }); 31 | if (!result) { 32 | // TODO: Maybe bubble this up as an error. 33 | return; 34 | } 35 | 36 | packageJson = result.packageJson; 37 | return packageJson; 38 | } 39 | -------------------------------------------------------------------------------- /packages/core/src/tools/tool-names.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright 2025 Google LLC 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | // Centralized constants for tool names. 8 | // This prevents circular dependencies that can occur when other modules (like agents) 9 | // need to reference a tool's name without importing the tool's implementation. 10 | 11 | export const GLOB_TOOL_NAME = 'glob'; 12 | export const WRITE_TODOS_TOOL_NAME = 'write_todos'; 13 | export const WRITE_FILE_TOOL_NAME = 'write_file'; 14 | export const WEB_SEARCH_TOOL_NAME = 'google_web_search'; 15 | export const WEB_FETCH_TOOL_NAME = 'web_fetch'; 16 | export const EDIT_TOOL_NAME = 'replace'; 17 | export const SHELL_TOOL_NAME = 'run_shell_command'; 18 | export const GREP_TOOL_NAME = 'search_file_content'; 19 | export const READ_MANY_FILES_TOOL_NAME = 'read_many_files'; 20 | export const READ_FILE_TOOL_NAME = 'read_file'; 21 | export const LS_TOOL_NAME = 'list_directory'; 22 | export const MEMORY_TOOL_NAME = 'save_memory'; 23 | -------------------------------------------------------------------------------- /packages/cli/src/services/prompt-processors/argumentProcessor.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright 2025 Google LLC 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | import { appendToLastTextPart } from '@google/gemini-cli-core'; 8 | import type { IPromptProcessor, PromptPipelineContent } from './types.js'; 9 | import type { CommandContext } from '../../ui/commands/types.js'; 10 | 11 | /** 12 | * Appends the user's full command invocation to the prompt if arguments are 13 | * provided, allowing the model to perform its own argument parsing. 14 | * 15 | * This processor is only used if the prompt does NOT contain {{args}}. 16 | */ 17 | export class DefaultArgumentProcessor implements IPromptProcessor { 18 | async process( 19 | prompt: PromptPipelineContent, 20 | context: CommandContext, 21 | ): Promise { 22 | if (context.invocation?.args) { 23 | return appendToLastTextPart(prompt, context.invocation.raw); 24 | } 25 | return prompt; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /packages/cli/src/ui/utils/ui-sizing.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright 2025 Google LLC 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | import { lerp } from '../../utils/math.js'; 8 | import { type LoadedSettings } from '../../config/settings.js'; 9 | 10 | const getMainAreaWidthInternal = (terminalWidth: number): number => { 11 | if (terminalWidth <= 80) { 12 | return Math.round(0.98 * terminalWidth); 13 | } 14 | if (terminalWidth >= 132) { 15 | return Math.round(0.9 * terminalWidth); 16 | } 17 | 18 | // Linearly interpolate between 80 columns (98%) and 132 columns (90%). 19 | const t = (terminalWidth - 80) / (132 - 80); 20 | const percentage = lerp(98, 90, t); 21 | 22 | return Math.round(percentage * terminalWidth * 0.01); 23 | }; 24 | 25 | export const calculateMainAreaWidth = ( 26 | terminalWidth: number, 27 | settings: LoadedSettings, 28 | ): number => 29 | settings.merged.ui?.useFullWidth 30 | ? terminalWidth 31 | : getMainAreaWidthInternal(terminalWidth); 32 | -------------------------------------------------------------------------------- /packages/core/src/utils/formatters.test.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright 2025 Google LLC 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | import { describe, it, expect } from 'vitest'; 8 | 9 | import { bytesToMB, formatMemoryUsage } from './formatters.js'; 10 | 11 | describe('bytesToMB', () => { 12 | it('converts bytes to megabytes', () => { 13 | expect(bytesToMB(0)).toBe(0); 14 | expect(bytesToMB(512 * 1024)).toBeCloseTo(0.5, 5); 15 | expect(bytesToMB(5 * 1024 * 1024)).toBe(5); 16 | }); 17 | }); 18 | 19 | describe('formatMemoryUsage', () => { 20 | it('formats values below one megabyte in KB', () => { 21 | expect(formatMemoryUsage(512 * 1024)).toBe('512.0 KB'); 22 | }); 23 | 24 | it('formats values below one gigabyte in MB', () => { 25 | expect(formatMemoryUsage(5 * 1024 * 1024)).toBe('5.0 MB'); 26 | }); 27 | 28 | it('formats values of one gigabyte or larger in GB', () => { 29 | expect(formatMemoryUsage(2 * 1024 * 1024 * 1024)).toBe('2.00 GB'); 30 | }); 31 | }); 32 | -------------------------------------------------------------------------------- /packages/cli/src/ui/components/ConsoleSummaryDisplay.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright 2025 Google LLC 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | import type React from 'react'; 8 | import { Box, Text } from 'ink'; 9 | import { theme } from '../semantic-colors.js'; 10 | 11 | interface ConsoleSummaryDisplayProps { 12 | errorCount: number; 13 | // logCount is not currently in the plan to be displayed in summary 14 | } 15 | 16 | export const ConsoleSummaryDisplay: React.FC = ({ 17 | errorCount, 18 | }) => { 19 | if (errorCount === 0) { 20 | return null; 21 | } 22 | 23 | const errorIcon = '\u2716'; // Heavy multiplication x (✖) 24 | 25 | return ( 26 | 27 | {errorCount > 0 && ( 28 | 29 | {errorIcon} {errorCount} error{errorCount > 1 ? 's' : ''}{' '} 30 | (ctrl+o for details) 31 | 32 | )} 33 | 34 | ); 35 | }; 36 | -------------------------------------------------------------------------------- /packages/core/src/__mocks__/fs/promises.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright 2025 Google LLC 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | import { vi } from 'vitest'; 8 | import * as actualFsPromises from 'node:fs/promises'; 9 | 10 | const readFileMock = vi.fn(); 11 | 12 | // Export a control object so tests can access and manipulate the mock 13 | export const mockControl = { 14 | mockReadFile: readFileMock, 15 | }; 16 | 17 | // Export all other functions from the actual fs/promises module 18 | export const { 19 | access, 20 | appendFile, 21 | chmod, 22 | chown, 23 | copyFile, 24 | cp, 25 | lchmod, 26 | lchown, 27 | link, 28 | lstat, 29 | mkdir, 30 | open, 31 | opendir, 32 | readdir, 33 | readlink, 34 | realpath, 35 | rename, 36 | rmdir, 37 | rm, 38 | stat, 39 | symlink, 40 | truncate, 41 | unlink, 42 | utimes, 43 | watch, 44 | writeFile, 45 | } = actualFsPromises; 46 | 47 | // Override readFile with our mock 48 | export const readFile = readFileMock; 49 | -------------------------------------------------------------------------------- /packages/core/src/core/tokenLimits.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright 2025 Google LLC 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | type Model = string; 8 | type TokenCount = number; 9 | 10 | export const DEFAULT_TOKEN_LIMIT = 1_048_576; 11 | 12 | export function tokenLimit(model: Model): TokenCount { 13 | // Add other models as they become relevant or if specified by config 14 | // Pulled from https://ai.google.dev/gemini-api/docs/models 15 | switch (model) { 16 | case 'gemini-1.5-pro': 17 | return 2_097_152; 18 | case 'gemini-1.5-flash': 19 | case 'gemini-2.5-pro-preview-05-06': 20 | case 'gemini-2.5-pro-preview-06-05': 21 | case 'gemini-2.5-pro': 22 | case 'gemini-2.5-flash-preview-05-20': 23 | case 'gemini-2.5-flash': 24 | case 'gemini-2.5-flash-lite': 25 | case 'gemini-2.0-flash': 26 | return 1_048_576; 27 | case 'gemini-2.0-flash-preview-image-generation': 28 | return 32_000; 29 | default: 30 | return DEFAULT_TOKEN_LIMIT; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /packages/cli/src/ui/commands/quitCommand.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright 2025 Google LLC 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | import { formatDuration } from '../utils/formatters.js'; 8 | import { CommandKind, type SlashCommand } from './types.js'; 9 | 10 | export const quitCommand: SlashCommand = { 11 | name: 'quit', 12 | altNames: ['exit'], 13 | description: 'Exit the cli', 14 | kind: CommandKind.BUILT_IN, 15 | action: (context) => { 16 | const now = Date.now(); 17 | const { sessionStartTime } = context.session.stats; 18 | const wallDuration = now - sessionStartTime.getTime(); 19 | 20 | return { 21 | type: 'quit', 22 | messages: [ 23 | { 24 | type: 'user', 25 | text: `/quit`, // Keep it consistent, even if /exit was used 26 | id: now - 1, 27 | }, 28 | { 29 | type: 'quit', 30 | duration: formatDuration(wallDuration), 31 | id: now, 32 | }, 33 | ], 34 | }; 35 | }, 36 | }; 37 | -------------------------------------------------------------------------------- /packages/vscode-ide-companion/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "NodeNext", 4 | "moduleResolution": "NodeNext", 5 | "target": "ES2022", 6 | "lib": ["ES2022", "dom"], 7 | "sourceMap": true, 8 | /* 9 | * skipLibCheck is necessary because the a2a-server package depends on 10 | * @google-cloud/storage which pulls in @types/request which depends on 11 | * tough-cookie@4.x while jsdom requires tough-cookie@5.x. This causes a 12 | * type checking error in ../../node_modules/@types/request/index.d.ts. 13 | */ 14 | "skipLibCheck": true, 15 | "rootDir": "src", 16 | "strict": true /* enable all strict type-checking options */ 17 | /* Additional Checks */ 18 | // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ 19 | // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ 20 | // "noUnusedParameters": true, /* Report errors on unused parameters. */ 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /packages/cli/src/ui/components/messages/__snapshots__/GeminiMessage.test.tsx.snap: -------------------------------------------------------------------------------- 1 | // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html 2 | 3 | exports[` - Raw Markdown Display Snapshots > renders pending state with renderMarkdown=false 1`] = ` 4 | "✦ Test **bold** and \`code\` markdown 5 | 6 | \`\`\`javascript 7 | const x = 1; 8 | \`\`\`" 9 | `; 10 | 11 | exports[` - Raw Markdown Display Snapshots > renders pending state with renderMarkdown=true 1`] = ` 12 | "✦ Test bold and code markdown 13 | 14 | 1 const x = 1;" 15 | `; 16 | 17 | exports[` - Raw Markdown Display Snapshots > renders with renderMarkdown=false '(raw markdown with syntax highlightin…' 1`] = ` 18 | "✦ Test **bold** and \`code\` markdown 19 | 20 | \`\`\`javascript 21 | const x = 1; 22 | \`\`\`" 23 | `; 24 | 25 | exports[` - Raw Markdown Display Snapshots > renders with renderMarkdown=true '(default)' 1`] = ` 26 | "✦ Test bold and code markdown 27 | 28 | 1 const x = 1;" 29 | `; 30 | -------------------------------------------------------------------------------- /packages/core/src/telemetry/tool-call-decision.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright 2025 Google LLC 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | import { ToolConfirmationOutcome } from '../tools/tools.js'; 8 | 9 | export enum ToolCallDecision { 10 | ACCEPT = 'accept', 11 | REJECT = 'reject', 12 | MODIFY = 'modify', 13 | AUTO_ACCEPT = 'auto_accept', 14 | } 15 | 16 | export function getDecisionFromOutcome( 17 | outcome: ToolConfirmationOutcome, 18 | ): ToolCallDecision { 19 | switch (outcome) { 20 | case ToolConfirmationOutcome.ProceedOnce: 21 | return ToolCallDecision.ACCEPT; 22 | case ToolConfirmationOutcome.ProceedAlways: 23 | case ToolConfirmationOutcome.ProceedAlwaysServer: 24 | case ToolConfirmationOutcome.ProceedAlwaysTool: 25 | return ToolCallDecision.AUTO_ACCEPT; 26 | case ToolConfirmationOutcome.ModifyWithEditor: 27 | return ToolCallDecision.MODIFY; 28 | case ToolConfirmationOutcome.Cancel: 29 | default: 30 | return ToolCallDecision.REJECT; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /packages/core/src/utils/getPty.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright 2025 Google LLC 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | export type PtyImplementation = { 8 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 9 | module: any; 10 | name: 'lydell-node-pty' | 'node-pty'; 11 | } | null; 12 | 13 | export interface PtyProcess { 14 | readonly pid: number; 15 | onData(callback: (data: string) => void): void; 16 | onExit(callback: (e: { exitCode: number; signal?: number }) => void): void; 17 | kill(signal?: string): void; 18 | } 19 | 20 | export const getPty = async (): Promise => { 21 | try { 22 | const lydell = '@lydell/node-pty'; 23 | const module = await import(lydell); 24 | return { module, name: 'lydell-node-pty' }; 25 | } catch (_e) { 26 | try { 27 | const nodePty = 'node-pty'; 28 | const module = await import(nodePty); 29 | return { module, name: 'node-pty' }; 30 | } catch (_e2) { 31 | return null; 32 | } 33 | } 34 | }; 35 | -------------------------------------------------------------------------------- /packages/cli/src/utils/sandbox-macos-permissive-open.sb: -------------------------------------------------------------------------------- 1 | (version 1) 2 | 3 | ;; allow everything by default 4 | (allow default) 5 | 6 | ;; deny all writes EXCEPT under specific paths 7 | (deny file-write*) 8 | (allow file-write* 9 | (subpath (param "TARGET_DIR")) 10 | (subpath (param "TMP_DIR")) 11 | (subpath (param "CACHE_DIR")) 12 | (subpath (string-append (param "HOME_DIR") "/.gemini")) 13 | (subpath (string-append (param "HOME_DIR") "/.npm")) 14 | (subpath (string-append (param "HOME_DIR") "/.cache")) 15 | (subpath (string-append (param "HOME_DIR") "/.gitconfig")) 16 | ;; Allow writes to included directories from --include-directories 17 | (subpath (param "INCLUDE_DIR_0")) 18 | (subpath (param "INCLUDE_DIR_1")) 19 | (subpath (param "INCLUDE_DIR_2")) 20 | (subpath (param "INCLUDE_DIR_3")) 21 | (subpath (param "INCLUDE_DIR_4")) 22 | (literal "/dev/stdout") 23 | (literal "/dev/stderr") 24 | (literal "/dev/null") 25 | (literal "/dev/ptmx") 26 | (regex #"^/dev/ttys[0-9]*$") 27 | ) 28 | -------------------------------------------------------------------------------- /packages/core/src/test-utils/config.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright 2025 Google LLC 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | import type { ConfigParameters } from '../config/config.js'; 8 | import { Config } from '../config/config.js'; 9 | 10 | /** 11 | * Default parameters used for {@link FAKE_CONFIG} 12 | */ 13 | export const DEFAULT_CONFIG_PARAMETERS: ConfigParameters = { 14 | usageStatisticsEnabled: true, 15 | debugMode: false, 16 | sessionId: 'test-session-id', 17 | proxy: undefined, 18 | model: 'gemini-9001-super-duper', 19 | targetDir: '/', 20 | cwd: '/', 21 | }; 22 | 23 | /** 24 | * Produces a config. Default paramters are set to 25 | * {@link DEFAULT_CONFIG_PARAMETERS}, optionally, fields can be specified to 26 | * override those defaults. 27 | */ 28 | export function makeFakeConfig( 29 | config: Partial = { 30 | ...DEFAULT_CONFIG_PARAMETERS, 31 | }, 32 | ): Config { 33 | return new Config({ 34 | ...DEFAULT_CONFIG_PARAMETERS, 35 | ...config, 36 | }); 37 | } 38 | -------------------------------------------------------------------------------- /packages/core/src/core/nonInteractiveToolExecutor.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright 2025 Google LLC 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | import type { ToolCallRequestInfo, Config } from '../index.js'; 8 | import { 9 | CoreToolScheduler, 10 | type CompletedToolCall, 11 | } from './coreToolScheduler.js'; 12 | 13 | /** 14 | * Executes a single tool call non-interactively by leveraging the CoreToolScheduler. 15 | */ 16 | export async function executeToolCall( 17 | config: Config, 18 | toolCallRequest: ToolCallRequestInfo, 19 | abortSignal: AbortSignal, 20 | ): Promise { 21 | return new Promise((resolve, reject) => { 22 | new CoreToolScheduler({ 23 | config, 24 | getPreferredEditor: () => undefined, 25 | onEditorClose: () => {}, 26 | onAllToolCallsComplete: async (completedToolCalls) => { 27 | resolve(completedToolCalls[0]); 28 | }, 29 | }) 30 | .schedule(toolCallRequest, abortSignal) 31 | .catch(reject); 32 | }); 33 | } 34 | -------------------------------------------------------------------------------- /packages/cli/src/ui/components/views/__snapshots__/ToolsList.test.tsx.snap: -------------------------------------------------------------------------------- 1 | // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html 2 | 3 | exports[` > renders correctly with descriptions 1`] = ` 4 | "Available Gemini CLI tools: 5 | 6 | - Test Tool One (test-tool-one) 7 | This is the first test tool. 8 | - Test Tool Two (test-tool-two) 9 | This is the second test tool. 10 | 1. Tool descriptions support markdown formatting. 11 | 2. note use this tool wisely and be sure to consider how this tool interacts with word wrap. 12 | 3. important this tool is awesome. 13 | - Test Tool Three (test-tool-three) 14 | This is the third test tool. 15 | " 16 | `; 17 | 18 | exports[` > renders correctly with no tools 1`] = ` 19 | "Available Gemini CLI tools: 20 | 21 | No tools available 22 | " 23 | `; 24 | 25 | exports[` > renders correctly without descriptions 1`] = ` 26 | "Available Gemini CLI tools: 27 | 28 | - Test Tool One 29 | - Test Tool Two 30 | - Test Tool Three 31 | " 32 | `; 33 | -------------------------------------------------------------------------------- /packages/cli/src/ui/hooks/useLogger.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright 2025 Google LLC 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | import { useState, useEffect } from 'react'; 8 | import type { Storage } from '@google/gemini-cli-core'; 9 | import { sessionId, Logger } from '@google/gemini-cli-core'; 10 | 11 | /** 12 | * Hook to manage the logger instance. 13 | */ 14 | export const useLogger = (storage: Storage) => { 15 | const [logger, setLogger] = useState(null); 16 | 17 | useEffect(() => { 18 | const newLogger = new Logger(sessionId, storage); 19 | /** 20 | * Start async initialization, no need to await. Using await slows down the 21 | * time from launch to see the gemini-cli prompt and it's better to not save 22 | * messages than for the cli to hanging waiting for the logger to loading. 23 | */ 24 | newLogger 25 | .initialize() 26 | .then(() => { 27 | setLogger(newLogger); 28 | }) 29 | .catch(() => {}); 30 | }, [storage]); 31 | 32 | return logger; 33 | }; 34 | -------------------------------------------------------------------------------- /.github/actions/calculate-vars/action.yml: -------------------------------------------------------------------------------- 1 | name: 'Calculate vars' 2 | description: 'Calculate commonly used var in our release process' 3 | 4 | inputs: 5 | dry_run: 6 | description: 'Whether or not this is a dry run' 7 | type: 'boolean' 8 | 9 | outputs: 10 | is_dry_run: 11 | description: 'Boolean flag indicating if the current run is a dry-run or a production release.' 12 | value: '${{ steps.set_vars.outputs.is_dry_run }}' 13 | 14 | runs: 15 | using: 'composite' 16 | steps: 17 | - name: 'Print inputs' 18 | shell: 'bash' 19 | env: 20 | JSON_INPUTS: '${{ toJSON(inputs) }}' 21 | run: 'echo "$JSON_INPUTS"' 22 | 23 | - name: 'Set vars for simplified logic' 24 | id: 'set_vars' 25 | shell: 'bash' 26 | env: 27 | DRY_RUN_INPUT: '${{ inputs.dry_run }}' 28 | run: |- 29 | is_dry_run="true" 30 | if [[ "${DRY_RUN_INPUT}" == "" || "${DRY_RUN_INPUT}" == "false" ]]; then 31 | is_dry_run="false" 32 | fi 33 | echo "is_dry_run=${is_dry_run}" >> "${GITHUB_OUTPUT}" 34 | -------------------------------------------------------------------------------- /packages/cli/src/ui/components/AppHeader.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright 2025 Google LLC 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | import { Box } from 'ink'; 8 | import { Header } from './Header.js'; 9 | import { Tips } from './Tips.js'; 10 | import { useSettings } from '../contexts/SettingsContext.js'; 11 | import { useConfig } from '../contexts/ConfigContext.js'; 12 | import { useUIState } from '../contexts/UIStateContext.js'; 13 | 14 | interface AppHeaderProps { 15 | version: string; 16 | } 17 | 18 | export const AppHeader = ({ version }: AppHeaderProps) => { 19 | const settings = useSettings(); 20 | const config = useConfig(); 21 | const { nightly } = useUIState(); 22 | 23 | return ( 24 | 25 | {!(settings.merged.ui?.hideBanner || config.getScreenReader()) && ( 26 |
27 | )} 28 | {!(settings.merged.ui?.hideTips || config.getScreenReader()) && ( 29 | 30 | )} 31 | 32 | ); 33 | }; 34 | -------------------------------------------------------------------------------- /packages/core/src/utils/LruCache.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright 2025 Google LLC 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | export class LruCache { 8 | private cache: Map; 9 | private maxSize: number; 10 | 11 | constructor(maxSize: number) { 12 | this.cache = new Map(); 13 | this.maxSize = maxSize; 14 | } 15 | 16 | get(key: K): V | undefined { 17 | const value = this.cache.get(key); 18 | if (value) { 19 | // Move to end to mark as recently used 20 | this.cache.delete(key); 21 | this.cache.set(key, value); 22 | } 23 | return value; 24 | } 25 | 26 | set(key: K, value: V): void { 27 | if (this.cache.has(key)) { 28 | this.cache.delete(key); 29 | } else if (this.cache.size >= this.maxSize) { 30 | const firstKey = this.cache.keys().next().value; 31 | if (firstKey !== undefined) { 32 | this.cache.delete(firstKey); 33 | } 34 | } 35 | this.cache.set(key, value); 36 | } 37 | 38 | clear(): void { 39 | this.cache.clear(); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /packages/cli/src/ui/commands/editorCommand.test.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright 2025 Google LLC 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | import { describe, it, expect } from 'vitest'; 8 | import { editorCommand } from './editorCommand.js'; 9 | // 1. Import the mock context utility 10 | import { createMockCommandContext } from '../../test-utils/mockCommandContext.js'; 11 | 12 | describe('editorCommand', () => { 13 | it('should return a dialog action to open the editor dialog', () => { 14 | if (!editorCommand.action) { 15 | throw new Error('The editor command must have an action.'); 16 | } 17 | const mockContext = createMockCommandContext(); 18 | const result = editorCommand.action(mockContext, ''); 19 | 20 | expect(result).toEqual({ 21 | type: 'dialog', 22 | dialog: 'editor', 23 | }); 24 | }); 25 | 26 | it('should have the correct name and description', () => { 27 | expect(editorCommand.name).toBe('editor'); 28 | expect(editorCommand.description).toBe('Set external editor preference'); 29 | }); 30 | }); 31 | -------------------------------------------------------------------------------- /packages/cli/src/ui/utils/displayUtils.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright 2025 Google LLC 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | import { theme } from '../semantic-colors.js'; 8 | 9 | // --- Thresholds --- 10 | export const TOOL_SUCCESS_RATE_HIGH = 95; 11 | export const TOOL_SUCCESS_RATE_MEDIUM = 85; 12 | 13 | export const USER_AGREEMENT_RATE_HIGH = 75; 14 | export const USER_AGREEMENT_RATE_MEDIUM = 45; 15 | 16 | export const CACHE_EFFICIENCY_HIGH = 40; 17 | export const CACHE_EFFICIENCY_MEDIUM = 15; 18 | 19 | // --- Color Logic --- 20 | export const getStatusColor = ( 21 | value: number, 22 | thresholds: { green: number; yellow: number; red?: number }, 23 | options: { defaultColor?: string } = {}, 24 | ) => { 25 | if (value >= thresholds.green) { 26 | return theme.status.success; 27 | } 28 | if (value >= thresholds.yellow) { 29 | return theme.status.warning; 30 | } 31 | if (thresholds.red != null && value >= thresholds.red) { 32 | return theme.status.error; 33 | } 34 | return options.defaultColor ?? theme.status.error; 35 | }; 36 | -------------------------------------------------------------------------------- /packages/core/src/utils/debugLogger.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright 2025 Google LLC 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | /** 8 | * A simple, centralized logger for developer-facing debug messages. 9 | * 10 | * WHY USE THIS? 11 | * - It makes the INTENT of the log clear (it's for developers, not users). 12 | * - It provides a single point of control for debug logging behavior. 13 | * - We can lint against direct `console.*` usage to enforce this pattern. 14 | * 15 | * HOW IT WORKS: 16 | * This is a thin wrapper around the native `console` object. The `ConsolePatcher` 17 | * will intercept these calls and route them to the debug drawer UI. 18 | */ 19 | class DebugLogger { 20 | log(...args: unknown[]): void { 21 | console.log(...args); 22 | } 23 | 24 | warn(...args: unknown[]): void { 25 | console.warn(...args); 26 | } 27 | 28 | error(...args: unknown[]): void { 29 | console.error(...args); 30 | } 31 | 32 | debug(...args: unknown[]): void { 33 | console.debug(...args); 34 | } 35 | } 36 | 37 | export const debugLogger = new DebugLogger(); 38 | -------------------------------------------------------------------------------- /packages/cli/src/ui/commands/clearCommand.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright 2025 Google LLC 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | import { uiTelemetryService } from '@google/gemini-cli-core'; 8 | import type { SlashCommand } from './types.js'; 9 | import { CommandKind } from './types.js'; 10 | 11 | export const clearCommand: SlashCommand = { 12 | name: 'clear', 13 | description: 'Clear the screen and conversation history', 14 | kind: CommandKind.BUILT_IN, 15 | action: async (context, _args) => { 16 | const geminiClient = context.services.config?.getGeminiClient(); 17 | 18 | if (geminiClient) { 19 | context.ui.setDebugMessage('Clearing terminal and resetting chat.'); 20 | // If resetChat fails, the exception will propagate and halt the command, 21 | // which is the correct behavior to signal a failure to the user. 22 | await geminiClient.resetChat(); 23 | } else { 24 | context.ui.setDebugMessage('Clearing terminal.'); 25 | } 26 | 27 | uiTelemetryService.setLastPromptTokenCount(0); 28 | context.ui.clear(); 29 | }, 30 | }; 31 | -------------------------------------------------------------------------------- /packages/core/src/output/json-formatter.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright 2025 Google LLC 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | import stripAnsi from 'strip-ansi'; 8 | import type { SessionMetrics } from '../telemetry/uiTelemetry.js'; 9 | import type { JsonError, JsonOutput } from './types.js'; 10 | 11 | export class JsonFormatter { 12 | format(response?: string, stats?: SessionMetrics, error?: JsonError): string { 13 | const output: JsonOutput = {}; 14 | 15 | if (response !== undefined) { 16 | output.response = stripAnsi(response); 17 | } 18 | 19 | if (stats) { 20 | output.stats = stats; 21 | } 22 | 23 | if (error) { 24 | output.error = error; 25 | } 26 | 27 | return JSON.stringify(output, null, 2); 28 | } 29 | 30 | formatError(error: Error, code?: string | number): string { 31 | const jsonError: JsonError = { 32 | type: error.constructor.name, 33 | message: stripAnsi(error.message), 34 | ...(code && { code }), 35 | }; 36 | 37 | return this.format(undefined, undefined, jsonError); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /packages/core/src/test-utils/mockWorkspaceContext.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright 2025 Google LLC 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | import { vi } from 'vitest'; 8 | import type { WorkspaceContext } from '../utils/workspaceContext.js'; 9 | 10 | /** 11 | * Creates a mock WorkspaceContext for testing 12 | * @param rootDir The root directory to use for the mock 13 | * @param additionalDirs Optional additional directories to include in the workspace 14 | * @returns A mock WorkspaceContext instance 15 | */ 16 | export function createMockWorkspaceContext( 17 | rootDir: string, 18 | additionalDirs: string[] = [], 19 | ): WorkspaceContext { 20 | const allDirs = [rootDir, ...additionalDirs]; 21 | 22 | const mockWorkspaceContext = { 23 | addDirectory: vi.fn(), 24 | getDirectories: vi.fn().mockReturnValue(allDirs), 25 | isPathWithinWorkspace: vi 26 | .fn() 27 | .mockImplementation((path: string) => 28 | allDirs.some((dir) => path.startsWith(dir)), 29 | ), 30 | } as unknown as WorkspaceContext; 31 | 32 | return mockWorkspaceContext; 33 | } 34 | -------------------------------------------------------------------------------- /packages/cli/src/ui/hooks/useKeypress.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright 2025 Google LLC 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | import { useEffect } from 'react'; 8 | import type { KeypressHandler, Key } from '../contexts/KeypressContext.js'; 9 | import { useKeypressContext } from '../contexts/KeypressContext.js'; 10 | 11 | export type { Key }; 12 | 13 | /** 14 | * A hook that listens for keypress events from stdin. 15 | * 16 | * @param onKeypress - The callback function to execute on each keypress. 17 | * @param options - Options to control the hook's behavior. 18 | * @param options.isActive - Whether the hook should be actively listening for input. 19 | */ 20 | export function useKeypress( 21 | onKeypress: KeypressHandler, 22 | { isActive }: { isActive: boolean }, 23 | ) { 24 | const { subscribe, unsubscribe } = useKeypressContext(); 25 | 26 | useEffect(() => { 27 | if (!isActive) { 28 | return; 29 | } 30 | 31 | subscribe(onKeypress); 32 | return () => { 33 | unsubscribe(onKeypress); 34 | }; 35 | }, [isActive, onKeypress, subscribe, unsubscribe]); 36 | } 37 | -------------------------------------------------------------------------------- /packages/cli/src/ui/hooks/useBracketedPaste.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright 2025 Google LLC 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | import { useEffect } from 'react'; 8 | 9 | const ENABLE_BRACKETED_PASTE = '\x1b[?2004h'; 10 | const DISABLE_BRACKETED_PASTE = '\x1b[?2004l'; 11 | 12 | /** 13 | * Enables and disables bracketed paste mode in the terminal. 14 | * 15 | * This hook ensures that bracketed paste mode is enabled when the component 16 | * mounts and disabled when it unmounts or when the process exits. 17 | */ 18 | export const useBracketedPaste = () => { 19 | const cleanup = () => { 20 | process.stdout.write(DISABLE_BRACKETED_PASTE); 21 | }; 22 | 23 | useEffect(() => { 24 | process.stdout.write(ENABLE_BRACKETED_PASTE); 25 | 26 | process.on('exit', cleanup); 27 | process.on('SIGINT', cleanup); 28 | process.on('SIGTERM', cleanup); 29 | 30 | return () => { 31 | cleanup(); 32 | process.removeListener('exit', cleanup); 33 | process.removeListener('SIGINT', cleanup); 34 | process.removeListener('SIGTERM', cleanup); 35 | }; 36 | }, []); 37 | }; 38 | -------------------------------------------------------------------------------- /scripts/build_vscode_companion.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright 2025 Google LLC 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | // 8 | // Licensed under the Apache License, Version 2.0 (the "License"); 9 | // you may not use this file except in compliance with the License. 10 | // You may obtain a copy of the License at 11 | // 12 | // http://www.apache.org/licenses/LICENSE-2.0 13 | // 14 | // Unless required by applicable law or agreed to in writing, software 15 | // distributed under the License is distributed on an "AS IS" BASIS, 16 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 17 | // See the License for the specific language governing permissions and 18 | // limitations under the License. 19 | 20 | import { execSync } from 'node:child_process'; 21 | import { dirname, join } from 'node:path'; 22 | import { fileURLToPath } from 'node:url'; 23 | 24 | const __dirname = dirname(fileURLToPath(import.meta.url)); 25 | const root = join(__dirname, '..'); 26 | 27 | execSync('npm --workspace=gemini-cli-vscode-ide-companion run package', { 28 | stdio: 'inherit', 29 | cwd: root, 30 | }); 31 | -------------------------------------------------------------------------------- /integration-tests/flicker.test.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright 2025 Google LLC 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | import { describe, it, expect } from 'vitest'; 8 | import { TestRig } from './test-helper.js'; 9 | 10 | describe('Flicker Detector', () => { 11 | // TODO: https://github.com/google-gemini/gemini-cli/issues/11170 12 | it.skip('should not detect a flicker under the max height budget', async () => { 13 | const rig = new TestRig(); 14 | await rig.setup('flicker-detector-test'); 15 | 16 | const run = await rig.runInteractive(); 17 | const prompt = 'Tell me a fun fact.'; 18 | await run.type(prompt); 19 | await run.type('\r'); 20 | 21 | const hasUserPromptEvent = await rig.waitForTelemetryEvent('user_prompt'); 22 | expect(hasUserPromptEvent).toBe(true); 23 | 24 | const hasSessionCountMetric = await rig.waitForMetric('session.count'); 25 | expect(hasSessionCountMetric).toBe(true); 26 | 27 | // We expect NO flicker event to be found. 28 | const flickerMetric = rig.readMetric('ui.flicker.count'); 29 | expect(flickerMetric).toBeNull(); 30 | }); 31 | }); 32 | -------------------------------------------------------------------------------- /packages/core/src/mcp/token-storage/types.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright 2025 Google LLC 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | /** 8 | * Interface for OAuth tokens. 9 | */ 10 | export interface OAuthToken { 11 | accessToken: string; 12 | refreshToken?: string; 13 | expiresAt?: number; 14 | tokenType: string; 15 | scope?: string; 16 | } 17 | 18 | /** 19 | * Interface for stored OAuth credentials. 20 | */ 21 | export interface OAuthCredentials { 22 | serverName: string; 23 | token: OAuthToken; 24 | clientId?: string; 25 | tokenUrl?: string; 26 | mcpServerUrl?: string; 27 | updatedAt: number; 28 | } 29 | 30 | export interface TokenStorage { 31 | getCredentials(serverName: string): Promise; 32 | setCredentials(credentials: OAuthCredentials): Promise; 33 | deleteCredentials(serverName: string): Promise; 34 | listServers(): Promise; 35 | getAllCredentials(): Promise>; 36 | clearAll(): Promise; 37 | } 38 | 39 | export enum TokenStorageType { 40 | KEYCHAIN = 'keychain', 41 | ENCRYPTED_FILE = 'encrypted_file', 42 | } 43 | -------------------------------------------------------------------------------- /.github/workflows/trigger_e2e.yml: -------------------------------------------------------------------------------- 1 | name: 'Trigger E2E' 2 | 3 | on: 4 | workflow_dispatch: 5 | inputs: 6 | branch_ref: 7 | description: 'Branch to run on' 8 | required: true 9 | default: 'main' 10 | type: 'string' 11 | 12 | jobs: 13 | save_repo_name: 14 | runs-on: 'gemini-cli-ubuntu-16-core' 15 | steps: 16 | - name: 'Save Repo name' 17 | env: 18 | # Replace with github.event.pull_request.base.repo.full_name when switched to listen on pull request events. This repo name does not contain the org which is needed for checkout. 19 | REPO_NAME: '${{ github.event.repository.name }}' 20 | run: | 21 | mkdir -p ./pr 22 | echo '${{ env.REPO_NAME }}' > ./pr/repo_name 23 | - uses: 'actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02' # ratchet:actions/upload-artifact@v4 24 | with: 25 | name: 'repo_name' 26 | path: 'pr/' 27 | trigger_e2e: 28 | name: 'Trigger e2e' 29 | runs-on: 'gemini-cli-ubuntu-16-core' 30 | steps: 31 | - id: 'trigger-e2e' 32 | run: | 33 | echo "Trigger e2e workflow" 34 | -------------------------------------------------------------------------------- /packages/cli/src/ui/commands/authCommand.test.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright 2025 Google LLC 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | import { describe, it, expect, beforeEach } from 'vitest'; 8 | import { authCommand } from './authCommand.js'; 9 | import { type CommandContext } from './types.js'; 10 | import { createMockCommandContext } from '../../test-utils/mockCommandContext.js'; 11 | 12 | describe('authCommand', () => { 13 | let mockContext: CommandContext; 14 | 15 | beforeEach(() => { 16 | mockContext = createMockCommandContext(); 17 | }); 18 | 19 | it('should return a dialog action to open the auth dialog', () => { 20 | if (!authCommand.action) { 21 | throw new Error('The auth command must have an action.'); 22 | } 23 | 24 | const result = authCommand.action(mockContext, ''); 25 | 26 | expect(result).toEqual({ 27 | type: 'dialog', 28 | dialog: 'auth', 29 | }); 30 | }); 31 | 32 | it('should have the correct name and description', () => { 33 | expect(authCommand.name).toBe('auth'); 34 | expect(authCommand.description).toBe('Change the auth method'); 35 | }); 36 | }); 37 | -------------------------------------------------------------------------------- /packages/cli/src/ui/components/__snapshots__/LoopDetectionConfirmation.test.tsx.snap: -------------------------------------------------------------------------------- 1 | // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html 2 | 3 | exports[`LoopDetectionConfirmation > renders correctly 1`] = ` 4 | " ╭──────────────────────────────────────────────────────────────────────────────────────────────────╮ 5 | │ ? A potential loop was detected │ 6 | │ │ 7 | │ This can happen due to repetitive tool calls or other model behavior. Do you want to keep loop │ 8 | │ detection enabled or disable it for this session? │ 9 | │ │ 10 | │ ● 1. Keep loop detection enabled (esc) │ 11 | │ 2. Disable loop detection for this session │ 12 | ╰──────────────────────────────────────────────────────────────────────────────────────────────────╯" 13 | `; 14 | -------------------------------------------------------------------------------- /packages/cli/src/ui/hooks/useStateAndRef.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright 2025 Google LLC 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | import React from 'react'; 8 | 9 | // Hook to return state, state setter, and ref to most up-to-date value of state. 10 | // We need this in order to setState and reference the updated state multiple 11 | // times in the same function. 12 | export const useStateAndRef = < 13 | // Everything but function. 14 | T extends object | null | undefined | number | string, 15 | >( 16 | initialValue: T, 17 | ) => { 18 | const [state, setState] = React.useState(initialValue); 19 | const ref = React.useRef(initialValue); 20 | 21 | const setStateInternal = React.useCallback( 22 | (newStateOrCallback) => { 23 | let newValue: T; 24 | if (typeof newStateOrCallback === 'function') { 25 | newValue = newStateOrCallback(ref.current); 26 | } else { 27 | newValue = newStateOrCallback; 28 | } 29 | setState(newValue); 30 | ref.current = newValue; 31 | }, 32 | [], 33 | ); 34 | 35 | return [state, ref, setStateInternal] as const; 36 | }; 37 | -------------------------------------------------------------------------------- /packages/cli/src/ui/noninteractive/nonInteractiveUi.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright 2025 Google LLC 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | import type { CommandContext } from '../commands/types.js'; 8 | import type { ExtensionUpdateAction } from '../state/extensions.js'; 9 | 10 | /** 11 | * Creates a UI context object with no-op functions. 12 | * Useful for non-interactive environments where UI operations 13 | * are not applicable. 14 | */ 15 | export function createNonInteractiveUI(): CommandContext['ui'] { 16 | return { 17 | addItem: (_item, _timestamp) => 0, 18 | clear: () => {}, 19 | setDebugMessage: (_message) => {}, 20 | loadHistory: (_newHistory) => {}, 21 | pendingItem: null, 22 | setPendingItem: (_item) => {}, 23 | toggleCorgiMode: () => {}, 24 | toggleDebugProfiler: () => {}, 25 | toggleVimEnabled: async () => false, 26 | setGeminiMdFileCount: (_count) => {}, 27 | reloadCommands: () => {}, 28 | extensionsUpdateState: new Map(), 29 | dispatchExtensionStateUpdate: (_action: ExtensionUpdateAction) => {}, 30 | addConfirmUpdateExtensionRequest: (_request) => {}, 31 | }; 32 | } 33 | -------------------------------------------------------------------------------- /packages/cli/src/utils/cleanup.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright 2025 Google LLC 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | import { promises as fs } from 'node:fs'; 8 | import { join } from 'node:path'; 9 | import { Storage } from '@google/gemini-cli-core'; 10 | 11 | const cleanupFunctions: Array<(() => void) | (() => Promise)> = []; 12 | 13 | export function registerCleanup(fn: (() => void) | (() => Promise)) { 14 | cleanupFunctions.push(fn); 15 | } 16 | 17 | export async function runExitCleanup() { 18 | for (const fn of cleanupFunctions) { 19 | try { 20 | await fn(); 21 | } catch (_) { 22 | // Ignore errors during cleanup. 23 | } 24 | } 25 | cleanupFunctions.length = 0; // Clear the array 26 | } 27 | 28 | export async function cleanupCheckpoints() { 29 | const storage = new Storage(process.cwd()); 30 | const tempDir = storage.getProjectTempDir(); 31 | const checkpointsDir = join(tempDir, 'checkpoints'); 32 | try { 33 | await fs.rm(checkpointsDir, { recursive: true, force: true }); 34 | } catch { 35 | // Ignore errors if the directory doesn't exist or fails to delete. 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /third_party/get-ripgrep/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Lvce Editor 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /packages/cli/src/ui/components/QuittingDisplay.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright 2025 Google LLC 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | import { Box } from 'ink'; 8 | import { useUIState } from '../contexts/UIStateContext.js'; 9 | import { HistoryItemDisplay } from './HistoryItemDisplay.js'; 10 | import { useTerminalSize } from '../hooks/useTerminalSize.js'; 11 | 12 | export const QuittingDisplay = () => { 13 | const uiState = useUIState(); 14 | const { rows: terminalHeight, columns: terminalWidth } = useTerminalSize(); 15 | 16 | const availableTerminalHeight = terminalHeight; 17 | 18 | if (!uiState.quittingMessages) { 19 | return null; 20 | } 21 | 22 | return ( 23 | 24 | {uiState.quittingMessages.map((item) => ( 25 | 34 | ))} 35 | 36 | ); 37 | }; 38 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # API keys and secrets 2 | .env 3 | .env~ 4 | 5 | # gemini-cli settings 6 | # We want to keep the .gemini in the root of the repo and ignore any .gemini 7 | # in subdirectories. In our root .gemini we want to allow for version control 8 | # for subcommands. 9 | **/.gemini/ 10 | !/.gemini/ 11 | .gemini/* 12 | !.gemini/config.yaml 13 | !.gemini/commands/ 14 | 15 | # Note: .gemini-clipboard/ is NOT in gitignore so Gemini can access pasted images 16 | 17 | # Dependency directory 18 | node_modules 19 | bower_components 20 | package-lock.json 21 | 22 | # Editors 23 | .idea 24 | *.iml 25 | 26 | # OS metadata 27 | .DS_Store 28 | Thumbs.db 29 | 30 | # TypeScript build info files 31 | *.tsbuildinfo 32 | 33 | # Ignore built ts files 34 | dist 35 | 36 | # Docker folder to help skip auth refreshes 37 | .docker 38 | 39 | bundle 40 | 41 | # Test report files 42 | junit.xml 43 | packages/*/coverage/ 44 | 45 | # Generated files 46 | packages/cli/src/generated/ 47 | packages/core/src/generated/ 48 | .integration-tests/ 49 | packages/vscode-ide-companion/*.vsix 50 | packages/cli/download-ripgrep*/ 51 | 52 | # GHA credentials 53 | gha-creds-*.json 54 | 55 | # Log files 56 | patch_output.log 57 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # See https://docs.github.com/en/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file 2 | version: 2 3 | updates: 4 | - package-ecosystem: 'npm' 5 | directory: '/' 6 | schedule: 7 | interval: 'daily' 8 | target-branch: 'main' 9 | commit-message: 10 | prefix: 'chore(deps)' 11 | include: 'scope' 12 | reviewers: 13 | - 'google-gemini/gemini-cli-askmode-approvers' 14 | groups: 15 | # Group all non-major updates together. 16 | # This is to reduce the number of PRs that need to be reviewed. 17 | # Major updates will still be created as separate PRs. 18 | npm-minor-patch: 19 | applies-to: 'version-updates' 20 | update-types: 21 | - 'minor' 22 | - 'patch' 23 | open-pull-requests-limit: 0 24 | 25 | - package-ecosystem: 'github-actions' 26 | directory: '/' 27 | schedule: 28 | interval: 'daily' 29 | target-branch: 'main' 30 | commit-message: 31 | prefix: 'chore(deps)' 32 | include: 'scope' 33 | reviewers: 34 | - 'google-gemini/gemini-cli-askmode-approvers' 35 | open-pull-requests-limit: 0 36 | -------------------------------------------------------------------------------- /packages/cli/src/ui/commands/corgiCommand.test.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright 2025 Google LLC 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | import { describe, it, expect, beforeEach, vi } from 'vitest'; 8 | import { corgiCommand } from './corgiCommand.js'; 9 | import { type CommandContext } from './types.js'; 10 | import { createMockCommandContext } from '../../test-utils/mockCommandContext.js'; 11 | 12 | describe('corgiCommand', () => { 13 | let mockContext: CommandContext; 14 | 15 | beforeEach(() => { 16 | mockContext = createMockCommandContext(); 17 | vi.spyOn(mockContext.ui, 'toggleCorgiMode'); 18 | }); 19 | 20 | it('should call the toggleCorgiMode function on the UI context', async () => { 21 | if (!corgiCommand.action) { 22 | throw new Error('The corgi command must have an action.'); 23 | } 24 | 25 | await corgiCommand.action(mockContext, ''); 26 | 27 | expect(mockContext.ui.toggleCorgiMode).toHaveBeenCalledTimes(1); 28 | }); 29 | 30 | it('should have the correct name and description', () => { 31 | expect(corgiCommand.name).toBe('corgi'); 32 | expect(corgiCommand.description).toBe('Toggles corgi mode'); 33 | }); 34 | }); 35 | -------------------------------------------------------------------------------- /packages/cli/src/utils/sandbox-macos-permissive-closed.sb: -------------------------------------------------------------------------------- 1 | (version 1) 2 | 3 | ;; allow everything by default 4 | (allow default) 5 | 6 | ;; deny all writes EXCEPT under specific paths 7 | (deny file-write*) 8 | (allow file-write* 9 | (subpath (param "TARGET_DIR")) 10 | (subpath (param "TMP_DIR")) 11 | (subpath (param "CACHE_DIR")) 12 | (subpath (string-append (param "HOME_DIR") "/.gemini")) 13 | (subpath (string-append (param "HOME_DIR") "/.npm")) 14 | (subpath (string-append (param "HOME_DIR") "/.cache")) 15 | (subpath (string-append (param "HOME_DIR") "/.gitconfig")) 16 | ;; Allow writes to included directories from --include-directories 17 | (subpath (param "INCLUDE_DIR_0")) 18 | (subpath (param "INCLUDE_DIR_1")) 19 | (subpath (param "INCLUDE_DIR_2")) 20 | (subpath (param "INCLUDE_DIR_3")) 21 | (subpath (param "INCLUDE_DIR_4")) 22 | (literal "/dev/stdout") 23 | (literal "/dev/stderr") 24 | (literal "/dev/null") 25 | ) 26 | 27 | ;; deny all inbound network traffic EXCEPT on debugger port 28 | (deny network-inbound) 29 | (allow network-inbound (local ip "localhost:9229")) 30 | 31 | ;; deny all outbound network traffic 32 | (deny network-outbound) 33 | -------------------------------------------------------------------------------- /packages/cli/src/ui/components/ShowMoreLines.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright 2025 Google LLC 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | import { Box, Text } from 'ink'; 8 | import { useOverflowState } from '../contexts/OverflowContext.js'; 9 | import { useStreamingContext } from '../contexts/StreamingContext.js'; 10 | import { StreamingState } from '../types.js'; 11 | import { theme } from '../semantic-colors.js'; 12 | 13 | interface ShowMoreLinesProps { 14 | constrainHeight: boolean; 15 | } 16 | 17 | export const ShowMoreLines = ({ constrainHeight }: ShowMoreLinesProps) => { 18 | const overflowState = useOverflowState(); 19 | const streamingState = useStreamingContext(); 20 | 21 | if ( 22 | overflowState === undefined || 23 | overflowState.overflowingIds.size === 0 || 24 | !constrainHeight || 25 | !( 26 | streamingState === StreamingState.Idle || 27 | streamingState === StreamingState.WaitingForConfirmation 28 | ) 29 | ) { 30 | return null; 31 | } 32 | 33 | return ( 34 | 35 | 36 | Press ctrl-s to show more lines 37 | 38 | 39 | ); 40 | }; 41 | -------------------------------------------------------------------------------- /packages/core/src/routing/strategies/defaultStrategy.test.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright 2025 Google LLC 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | import { describe, it, expect } from 'vitest'; 8 | import { DefaultStrategy } from './defaultStrategy.js'; 9 | import type { RoutingContext } from '../routingStrategy.js'; 10 | import type { BaseLlmClient } from '../../core/baseLlmClient.js'; 11 | import { DEFAULT_GEMINI_MODEL } from '../../config/models.js'; 12 | import type { Config } from '../../config/config.js'; 13 | 14 | describe('DefaultStrategy', () => { 15 | it('should always route to the default Gemini model', async () => { 16 | const strategy = new DefaultStrategy(); 17 | const mockContext = {} as RoutingContext; 18 | const mockConfig = {} as Config; 19 | const mockClient = {} as BaseLlmClient; 20 | 21 | const decision = await strategy.route(mockContext, mockConfig, mockClient); 22 | 23 | expect(decision).toEqual({ 24 | model: DEFAULT_GEMINI_MODEL, 25 | metadata: { 26 | source: 'default', 27 | latencyMs: 0, 28 | reasoning: `Routing to default model: ${DEFAULT_GEMINI_MODEL}`, 29 | }, 30 | }); 31 | }); 32 | }); 33 | -------------------------------------------------------------------------------- /packages/cli/src/ui/commands/modelCommand.test.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright 2025 Google LLC 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | import { describe, it, expect, beforeEach } from 'vitest'; 8 | import { modelCommand } from './modelCommand.js'; 9 | import { type CommandContext } from './types.js'; 10 | import { createMockCommandContext } from '../../test-utils/mockCommandContext.js'; 11 | 12 | describe('modelCommand', () => { 13 | let mockContext: CommandContext; 14 | 15 | beforeEach(() => { 16 | mockContext = createMockCommandContext(); 17 | }); 18 | 19 | it('should return a dialog action to open the model dialog', async () => { 20 | if (!modelCommand.action) { 21 | throw new Error('The model command must have an action.'); 22 | } 23 | 24 | const result = await modelCommand.action(mockContext, ''); 25 | 26 | expect(result).toEqual({ 27 | type: 'dialog', 28 | dialog: 'model', 29 | }); 30 | }); 31 | 32 | it('should have the correct name and description', () => { 33 | expect(modelCommand.name).toBe('model'); 34 | expect(modelCommand.description).toBe( 35 | 'Opens a dialog to configure the model', 36 | ); 37 | }); 38 | }); 39 | -------------------------------------------------------------------------------- /packages/cli/src/config/extensions/github_fetch.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright 2025 Google LLC 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | import * as https from 'node:https'; 8 | 9 | export function getGitHubToken(): string | undefined { 10 | return process.env['GITHUB_TOKEN']; 11 | } 12 | 13 | export async function fetchJson(url: string): Promise { 14 | const headers: { 'User-Agent': string; Authorization?: string } = { 15 | 'User-Agent': 'gemini-cli', 16 | }; 17 | const token = getGitHubToken(); 18 | if (token) { 19 | headers.Authorization = `token ${token}`; 20 | } 21 | return new Promise((resolve, reject) => { 22 | https 23 | .get(url, { headers }, (res) => { 24 | if (res.statusCode !== 200) { 25 | return reject( 26 | new Error(`Request failed with status code ${res.statusCode}`), 27 | ); 28 | } 29 | const chunks: Buffer[] = []; 30 | res.on('data', (chunk) => chunks.push(chunk)); 31 | res.on('end', () => { 32 | const data = Buffer.concat(chunks).toString(); 33 | resolve(JSON.parse(data) as T); 34 | }); 35 | }) 36 | .on('error', reject); 37 | }); 38 | } 39 | -------------------------------------------------------------------------------- /packages/cli/src/ui/commands/settingsCommand.test.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright 2025 Google LLC 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | import { describe, it, expect, beforeEach } from 'vitest'; 8 | import { settingsCommand } from './settingsCommand.js'; 9 | import { type CommandContext } from './types.js'; 10 | import { createMockCommandContext } from '../../test-utils/mockCommandContext.js'; 11 | 12 | describe('settingsCommand', () => { 13 | let mockContext: CommandContext; 14 | 15 | beforeEach(() => { 16 | mockContext = createMockCommandContext(); 17 | }); 18 | 19 | it('should return a dialog action to open the settings dialog', () => { 20 | if (!settingsCommand.action) { 21 | throw new Error('The settings command must have an action.'); 22 | } 23 | const result = settingsCommand.action(mockContext, ''); 24 | expect(result).toEqual({ 25 | type: 'dialog', 26 | dialog: 'settings', 27 | }); 28 | }); 29 | 30 | it('should have the correct name and description', () => { 31 | expect(settingsCommand.name).toBe('settings'); 32 | expect(settingsCommand.description).toBe( 33 | 'View and edit Gemini CLI settings', 34 | ); 35 | }); 36 | }); 37 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.yml: -------------------------------------------------------------------------------- 1 | name: 'Feature Request' 2 | description: 'Suggest an idea for this project' 3 | labels: 4 | - 'kind/enhancement' 5 | - 'status/need-triage' 6 | body: 7 | - type: 'markdown' 8 | attributes: 9 | value: |- 10 | > [!IMPORTANT] 11 | > Thanks for taking the time to suggest an enhancement! 12 | > 13 | > Please search **[existing issues](https://github.com/google-gemini/gemini-cli/issues)** to see if a similar feature has already been requested. 14 | 15 | - type: 'textarea' 16 | id: 'feature' 17 | attributes: 18 | label: 'What would you like to be added?' 19 | description: 'A clear and concise description of the enhancement.' 20 | validations: 21 | required: true 22 | 23 | - type: 'textarea' 24 | id: 'rationale' 25 | attributes: 26 | label: 'Why is this needed?' 27 | description: 'A clear and concise description of why this enhancement is needed.' 28 | validations: 29 | required: true 30 | 31 | - type: 'textarea' 32 | id: 'additional-context' 33 | attributes: 34 | label: 'Additional context' 35 | description: 'Add any other context or screenshots about the feature request here.' 36 | -------------------------------------------------------------------------------- /packages/a2a-server/vitest.config.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright 2025 Google LLC 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | /// 8 | import { defineConfig } from 'vitest/config'; 9 | 10 | export default defineConfig({ 11 | test: { 12 | include: ['**/*.{test,spec}.?(c|m)[jt]s?(x)'], 13 | exclude: ['**/node_modules/**', '**/dist/**'], 14 | environment: 'jsdom', 15 | globals: true, 16 | reporters: ['default', 'junit'], 17 | silent: true, 18 | outputFile: { 19 | junit: 'junit.xml', 20 | }, 21 | coverage: { 22 | enabled: true, 23 | provider: 'v8', 24 | reportsDirectory: './coverage', 25 | include: ['src/**/*'], 26 | reporter: [ 27 | ['text', { file: 'full-text-summary.txt' }], 28 | 'html', 29 | 'json', 30 | 'lcov', 31 | 'cobertura', 32 | ['json-summary', { outputFile: 'coverage-summary.json' }], 33 | ], 34 | }, 35 | poolOptions: { 36 | threads: { 37 | minThreads: 8, 38 | maxThreads: 16, 39 | }, 40 | }, 41 | server: { 42 | deps: { 43 | inline: [/@google\/gemini-cli-core/], 44 | }, 45 | }, 46 | }, 47 | }); 48 | -------------------------------------------------------------------------------- /packages/core/src/routing/strategies/fallbackStrategy.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright 2025 Google LLC 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | import type { Config } from '../../config/config.js'; 8 | import { getEffectiveModel } from '../../config/models.js'; 9 | import type { BaseLlmClient } from '../../core/baseLlmClient.js'; 10 | import type { 11 | RoutingContext, 12 | RoutingDecision, 13 | RoutingStrategy, 14 | } from '../routingStrategy.js'; 15 | 16 | export class FallbackStrategy implements RoutingStrategy { 17 | readonly name = 'fallback'; 18 | 19 | async route( 20 | _context: RoutingContext, 21 | config: Config, 22 | _baseLlmClient: BaseLlmClient, 23 | ): Promise { 24 | const isInFallbackMode: boolean = config.isInFallbackMode(); 25 | 26 | if (!isInFallbackMode) { 27 | return null; 28 | } 29 | 30 | const effectiveModel = getEffectiveModel( 31 | isInFallbackMode, 32 | config.getModel(), 33 | ); 34 | return { 35 | model: effectiveModel, 36 | metadata: { 37 | source: this.name, 38 | latencyMs: 0, 39 | reasoning: `In fallback mode. Using: ${effectiveModel}`, 40 | }, 41 | }; 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /packages/cli/src/ui/components/messages/UserMessage.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright 2025 Google LLC 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | import type React from 'react'; 8 | import { Text, Box } from 'ink'; 9 | import { theme } from '../../semantic-colors.js'; 10 | import { SCREEN_READER_USER_PREFIX } from '../../textConstants.js'; 11 | import { isSlashCommand as checkIsSlashCommand } from '../../utils/commandUtils.js'; 12 | 13 | interface UserMessageProps { 14 | text: string; 15 | } 16 | 17 | export const UserMessage: React.FC = ({ text }) => { 18 | const prefix = '> '; 19 | const prefixWidth = prefix.length; 20 | const isSlashCommand = checkIsSlashCommand(text); 21 | 22 | const textColor = isSlashCommand ? theme.text.accent : theme.text.secondary; 23 | 24 | return ( 25 | 26 | 27 | 28 | {prefix} 29 | 30 | 31 | 32 | 33 | {text} 34 | 35 | 36 | 37 | ); 38 | }; 39 | -------------------------------------------------------------------------------- /packages/cli/src/ui/commands/permissionsCommand.test.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright 2025 Google LLC 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | import { describe, it, expect, beforeEach } from 'vitest'; 8 | import { permissionsCommand } from './permissionsCommand.js'; 9 | import { type CommandContext, CommandKind } from './types.js'; 10 | import { createMockCommandContext } from '../../test-utils/mockCommandContext.js'; 11 | 12 | describe('permissionsCommand', () => { 13 | let mockContext: CommandContext; 14 | 15 | beforeEach(() => { 16 | mockContext = createMockCommandContext(); 17 | }); 18 | 19 | it('should have the correct name and description', () => { 20 | expect(permissionsCommand.name).toBe('permissions'); 21 | expect(permissionsCommand.description).toBe('Manage folder trust settings'); 22 | }); 23 | 24 | it('should be a built-in command', () => { 25 | expect(permissionsCommand.kind).toBe(CommandKind.BUILT_IN); 26 | }); 27 | 28 | it('should return an action to open the permissions dialog', () => { 29 | const actionResult = permissionsCommand.action?.(mockContext, ''); 30 | expect(actionResult).toEqual({ 31 | type: 'dialog', 32 | dialog: 'permissions', 33 | }); 34 | }); 35 | }); 36 | -------------------------------------------------------------------------------- /.github/actions/run-tests/action.yml: -------------------------------------------------------------------------------- 1 | name: 'Run Tests' 2 | description: 'Runs the preflight checks and integration tests.' 3 | 4 | inputs: 5 | gemini_api_key: 6 | description: 'The API key for running integration tests.' 7 | required: true 8 | working-directory: 9 | description: 'The working directory to run the tests in.' 10 | required: false 11 | default: '.' 12 | 13 | runs: 14 | using: 'composite' 15 | steps: 16 | - name: '📝 Print Inputs' 17 | shell: 'bash' 18 | env: 19 | JSON_INPUTS: '${{ toJSON(inputs) }}' 20 | run: 'echo "$JSON_INPUTS"' 21 | - name: 'Run Tests' 22 | env: 23 | GEMINI_API_KEY: '${{ inputs.gemini_api_key }}' 24 | working-directory: '${{ inputs.working-directory }}' 25 | run: |- 26 | echo "::group::Build" 27 | npm run build 28 | echo "::endgroup::" 29 | echo "::group::Unit Tests" 30 | npm run test:ci 31 | echo "::endgroup::" 32 | echo "::group::Integration Tests (no sandbox)" 33 | npm run test:integration:sandbox:none 34 | echo "::endgroup::" 35 | echo "::group::Integration Tests (docker sandbox)" 36 | npm run test:integration:sandbox:docker 37 | echo "::endgroup::" 38 | shell: 'bash' 39 | -------------------------------------------------------------------------------- /docs/tools/web-search.md: -------------------------------------------------------------------------------- 1 | # Web Search Tool (`google_web_search`) 2 | 3 | This document describes the `google_web_search` tool. 4 | 5 | ## Description 6 | 7 | Use `google_web_search` to perform a web search using Google Search via the 8 | Gemini API. The `google_web_search` tool returns a summary of web results with 9 | sources. 10 | 11 | ### Arguments 12 | 13 | `google_web_search` takes one argument: 14 | 15 | - `query` (string, required): The search query. 16 | 17 | ## How to use `google_web_search` with the Gemini CLI 18 | 19 | The `google_web_search` tool sends a query to the Gemini API, which then 20 | performs a web search. `google_web_search` will return a generated response 21 | based on the search results, including citations and sources. 22 | 23 | Usage: 24 | 25 | ``` 26 | google_web_search(query="Your query goes here.") 27 | ``` 28 | 29 | ## `google_web_search` examples 30 | 31 | Get information on a topic: 32 | 33 | ``` 34 | google_web_search(query="latest advancements in AI-powered code generation") 35 | ``` 36 | 37 | ## Important notes 38 | 39 | - **Response returned:** The `google_web_search` tool returns a processed 40 | summary, not a raw list of search results. 41 | - **Citations:** The response includes citations to the sources used to generate 42 | the summary. 43 | -------------------------------------------------------------------------------- /packages/cli/src/ui/components/LoopDetectionConfirmation.test.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright 2025 Google LLC 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | import { renderWithProviders } from '../../test-utils/render.js'; 8 | import { describe, it, expect, vi } from 'vitest'; 9 | import { LoopDetectionConfirmation } from './LoopDetectionConfirmation.js'; 10 | 11 | describe('LoopDetectionConfirmation', () => { 12 | const onComplete = vi.fn(); 13 | 14 | it('renders correctly', () => { 15 | const { lastFrame } = renderWithProviders( 16 | , 17 | ); 18 | expect(lastFrame()).toMatchSnapshot(); 19 | }); 20 | 21 | it('contains the expected options', () => { 22 | const { lastFrame } = renderWithProviders( 23 | , 24 | ); 25 | const output = lastFrame()!.toString(); 26 | 27 | expect(output).toContain('A potential loop was detected'); 28 | expect(output).toContain('Keep loop detection enabled (esc)'); 29 | expect(output).toContain('Disable loop detection for this session'); 30 | expect(output).toContain( 31 | 'This can happen due to repetitive tool calls or other model behavior', 32 | ); 33 | }); 34 | }); 35 | -------------------------------------------------------------------------------- /packages/cli/src/ui/commands/docsCommand.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright 2025 Google LLC 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | import open from 'open'; 8 | import process from 'node:process'; 9 | import { 10 | type CommandContext, 11 | type SlashCommand, 12 | CommandKind, 13 | } from './types.js'; 14 | import { MessageType } from '../types.js'; 15 | 16 | export const docsCommand: SlashCommand = { 17 | name: 'docs', 18 | description: 'Open full Gemini CLI documentation in your browser', 19 | kind: CommandKind.BUILT_IN, 20 | action: async (context: CommandContext): Promise => { 21 | const docsUrl = 'https://goo.gle/gemini-cli-docs'; 22 | 23 | if (process.env['SANDBOX'] && process.env['SANDBOX'] !== 'sandbox-exec') { 24 | context.ui.addItem( 25 | { 26 | type: MessageType.INFO, 27 | text: `Please open the following URL in your browser to view the documentation:\n${docsUrl}`, 28 | }, 29 | Date.now(), 30 | ); 31 | } else { 32 | context.ui.addItem( 33 | { 34 | type: MessageType.INFO, 35 | text: `Opening documentation in your browser: ${docsUrl}`, 36 | }, 37 | Date.now(), 38 | ); 39 | await open(docsUrl); 40 | } 41 | }, 42 | }; 43 | -------------------------------------------------------------------------------- /packages/cli/src/ui/commands/themeCommand.test.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright 2025 Google LLC 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | import { describe, it, expect, beforeEach } from 'vitest'; 8 | import { themeCommand } from './themeCommand.js'; 9 | import { type CommandContext } from './types.js'; 10 | import { createMockCommandContext } from '../../test-utils/mockCommandContext.js'; 11 | 12 | describe('themeCommand', () => { 13 | let mockContext: CommandContext; 14 | 15 | beforeEach(() => { 16 | mockContext = createMockCommandContext(); 17 | }); 18 | 19 | it('should return a dialog action to open the theme dialog', () => { 20 | // Ensure the command has an action to test. 21 | if (!themeCommand.action) { 22 | throw new Error('The theme command must have an action.'); 23 | } 24 | 25 | const result = themeCommand.action(mockContext, ''); 26 | 27 | // Assert that the action returns the correct object to trigger the theme dialog. 28 | expect(result).toEqual({ 29 | type: 'dialog', 30 | dialog: 'theme', 31 | }); 32 | }); 33 | 34 | it('should have the correct name and description', () => { 35 | expect(themeCommand.name).toBe('theme'); 36 | expect(themeCommand.description).toBe('Change the theme'); 37 | }); 38 | }); 39 | -------------------------------------------------------------------------------- /packages/cli/vitest.config.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright 2025 Google LLC 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | /// 8 | import { defineConfig } from 'vitest/config'; 9 | 10 | export default defineConfig({ 11 | test: { 12 | include: ['**/*.{test,spec}.?(c|m)[jt]s?(x)', 'config.test.ts'], 13 | exclude: ['**/node_modules/**', '**/dist/**', '**/cypress/**'], 14 | environment: 'jsdom', 15 | globals: true, 16 | reporters: ['default', 'junit'], 17 | silent: true, 18 | outputFile: { 19 | junit: 'junit.xml', 20 | }, 21 | setupFiles: ['./test-setup.ts'], 22 | coverage: { 23 | enabled: true, 24 | provider: 'v8', 25 | reportsDirectory: './coverage', 26 | include: ['src/**/*'], 27 | reporter: [ 28 | ['text', { file: 'full-text-summary.txt' }], 29 | 'html', 30 | 'json', 31 | 'lcov', 32 | 'cobertura', 33 | ['json-summary', { outputFile: 'coverage-summary.json' }], 34 | ], 35 | }, 36 | poolOptions: { 37 | threads: { 38 | minThreads: 8, 39 | maxThreads: 16, 40 | }, 41 | }, 42 | server: { 43 | deps: { 44 | inline: [/@google\/gemini-cli-core/], 45 | }, 46 | }, 47 | }, 48 | }); 49 | -------------------------------------------------------------------------------- /packages/core/src/policy/types.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright 2025 Google LLC 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | export enum PolicyDecision { 8 | ALLOW = 'allow', 9 | DENY = 'deny', 10 | ASK_USER = 'ask_user', 11 | } 12 | 13 | export interface PolicyRule { 14 | /** 15 | * The name of the tool this rule applies to. 16 | * If undefined, the rule applies to all tools. 17 | */ 18 | toolName?: string; 19 | 20 | /** 21 | * Pattern to match against tool arguments. 22 | * Can be used for more fine-grained control. 23 | */ 24 | argsPattern?: RegExp; 25 | 26 | /** 27 | * The decision to make when this rule matches. 28 | */ 29 | decision: PolicyDecision; 30 | 31 | /** 32 | * Priority of this rule. Higher numbers take precedence. 33 | * Default is 0. 34 | */ 35 | priority?: number; 36 | } 37 | 38 | export interface PolicyEngineConfig { 39 | /** 40 | * List of policy rules to apply. 41 | */ 42 | rules?: PolicyRule[]; 43 | 44 | /** 45 | * Default decision when no rules match. 46 | * Defaults to ASK_USER. 47 | */ 48 | defaultDecision?: PolicyDecision; 49 | 50 | /** 51 | * Whether to allow tools in non-interactive mode. 52 | * When true, ASK_USER decisions become DENY. 53 | */ 54 | nonInteractive?: boolean; 55 | } 56 | -------------------------------------------------------------------------------- /packages/cli/src/ui/components/AutoAcceptIndicator.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright 2025 Google LLC 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | import type React from 'react'; 8 | import { Box, Text } from 'ink'; 9 | import { theme } from '../semantic-colors.js'; 10 | import { ApprovalMode } from '@google/gemini-cli-core'; 11 | 12 | interface AutoAcceptIndicatorProps { 13 | approvalMode: ApprovalMode; 14 | } 15 | 16 | export const AutoAcceptIndicator: React.FC = ({ 17 | approvalMode, 18 | }) => { 19 | let textColor = ''; 20 | let textContent = ''; 21 | let subText = ''; 22 | 23 | switch (approvalMode) { 24 | case ApprovalMode.AUTO_EDIT: 25 | textColor = theme.status.warning; 26 | textContent = 'accepting edits'; 27 | subText = ' (shift + tab to toggle)'; 28 | break; 29 | case ApprovalMode.YOLO: 30 | textColor = theme.status.error; 31 | textContent = 'YOLO mode'; 32 | subText = ' (ctrl + y to toggle)'; 33 | break; 34 | case ApprovalMode.DEFAULT: 35 | default: 36 | break; 37 | } 38 | 39 | return ( 40 | 41 | 42 | {textContent} 43 | {subText && {subText}} 44 | 45 | 46 | ); 47 | }; 48 | -------------------------------------------------------------------------------- /packages/cli/src/config/extensions/storage.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright 2025 Google LLC 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | import * as path from 'node:path'; 8 | import * as fs from 'node:fs'; 9 | import * as os from 'node:os'; 10 | import { 11 | EXTENSION_SETTINGS_FILENAME, 12 | EXTENSIONS_CONFIG_FILENAME, 13 | } from './variables.js'; 14 | import { Storage } from '@google/gemini-cli-core'; 15 | 16 | export class ExtensionStorage { 17 | private readonly extensionName: string; 18 | 19 | constructor(extensionName: string) { 20 | this.extensionName = extensionName; 21 | } 22 | 23 | getExtensionDir(): string { 24 | return path.join( 25 | ExtensionStorage.getUserExtensionsDir(), 26 | this.extensionName, 27 | ); 28 | } 29 | 30 | getConfigPath(): string { 31 | return path.join(this.getExtensionDir(), EXTENSIONS_CONFIG_FILENAME); 32 | } 33 | 34 | getEnvFilePath(): string { 35 | return path.join(this.getExtensionDir(), EXTENSION_SETTINGS_FILENAME); 36 | } 37 | 38 | static getUserExtensionsDir(): string { 39 | return new Storage(os.homedir()).getExtensionsDir(); 40 | } 41 | 42 | static async createTmpDir(): Promise { 43 | return await fs.promises.mkdtemp( 44 | path.join(os.tmpdir(), 'gemini-extension'), 45 | ); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /scripts/build_package.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright 2025 Google LLC 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | // 8 | // Licensed under the Apache License, Version 2.0 (the "License"); 9 | // you may not use this file except in compliance with the License. 10 | // You may obtain a copy of the License at 11 | // 12 | // http://www.apache.org/licenses/LICENSE-2.0 13 | // 14 | // Unless required by applicable law or agreed to in writing, software 15 | // distributed under the License is distributed on an "AS IS" BASIS, 16 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 17 | // See the License for the specific language governing permissions and 18 | // limitations under the License. 19 | 20 | import { execSync } from 'node:child_process'; 21 | import { writeFileSync } from 'node:fs'; 22 | import { join } from 'node:path'; 23 | 24 | if (!process.cwd().includes('packages')) { 25 | console.error('must be invoked from a package directory'); 26 | process.exit(1); 27 | } 28 | 29 | // build typescript files 30 | execSync('tsc --build', { stdio: 'inherit' }); 31 | 32 | // copy .{md,json} files 33 | execSync('node ../../scripts/copy_files.js', { stdio: 'inherit' }); 34 | 35 | // touch dist/.last_build 36 | writeFileSync(join(process.cwd(), 'dist', '.last_build'), ''); 37 | process.exit(0); 38 | -------------------------------------------------------------------------------- /packages/cli/src/ui/commands/privacyCommand.test.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright 2025 Google LLC 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | import { describe, it, expect, beforeEach } from 'vitest'; 8 | import { privacyCommand } from './privacyCommand.js'; 9 | import { type CommandContext } from './types.js'; 10 | import { createMockCommandContext } from '../../test-utils/mockCommandContext.js'; 11 | 12 | describe('privacyCommand', () => { 13 | let mockContext: CommandContext; 14 | 15 | beforeEach(() => { 16 | mockContext = createMockCommandContext(); 17 | }); 18 | 19 | it('should return a dialog action to open the privacy dialog', () => { 20 | // Ensure the command has an action to test. 21 | if (!privacyCommand.action) { 22 | throw new Error('The privacy command must have an action.'); 23 | } 24 | 25 | const result = privacyCommand.action(mockContext, ''); 26 | 27 | // Assert that the action returns the correct object to trigger the privacy dialog. 28 | expect(result).toEqual({ 29 | type: 'dialog', 30 | dialog: 'privacy', 31 | }); 32 | }); 33 | 34 | it('should have the correct name and description', () => { 35 | expect(privacyCommand.name).toBe('privacy'); 36 | expect(privacyCommand.description).toBe('Display the privacy notice'); 37 | }); 38 | }); 39 | -------------------------------------------------------------------------------- /packages/cli/src/ui/components/__snapshots__/Footer.test.tsx.snap: -------------------------------------------------------------------------------- 1 | // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html 2 | 3 | exports[`