├── .npmrc ├── .vscode ├── settings.json ├── tasks.json └── launch.json ├── docs ├── assets │ ├── theme-ansi.png │ ├── theme-ayu.png │ ├── theme-github.png │ ├── theme-atom-one.png │ ├── theme-ayu-light.png │ ├── theme-default.png │ ├── theme-dracula.png │ ├── gemini-screenshot.png │ ├── theme-ansi-light.png │ ├── theme-xcode-light.png │ ├── connected_devtools.png │ ├── theme-default-light.png │ ├── theme-github-light.png │ └── theme-google-light.png ├── cli │ ├── token-caching.md │ ├── index.md │ └── themes.md ├── tools │ ├── web-search.md │ ├── memory.md │ └── web-fetch.md ├── extension.md └── tos-privacy.md ├── .prettierrc.json ├── packages ├── cli │ ├── src │ │ ├── ui │ │ │ ├── hooks │ │ │ │ ├── useRefreshMemoryCommand.ts │ │ │ │ ├── useTerminalSize.ts │ │ │ │ ├── useLogger.ts │ │ │ │ ├── useStateAndRef.ts │ │ │ │ ├── useAutoAcceptIndicator.ts │ │ │ │ ├── useTimer.ts │ │ │ │ ├── useEditorSettings.ts │ │ │ │ ├── useLoadingIndicator.ts │ │ │ │ ├── useShowMemoryCommand.ts │ │ │ │ └── useGitBranchName.ts │ │ │ ├── constants.ts │ │ │ ├── components │ │ │ │ ├── ShellModeIndicator.tsx │ │ │ │ ├── UpdateNotification.tsx │ │ │ │ ├── messages │ │ │ │ │ ├── UserShellMessage.tsx │ │ │ │ │ ├── UserMessage.tsx │ │ │ │ │ ├── ErrorMessage.tsx │ │ │ │ │ ├── InfoMessage.tsx │ │ │ │ │ ├── GeminiMessage.tsx │ │ │ │ │ ├── GeminiMessageContent.tsx │ │ │ │ │ ├── CompressionMessage.tsx │ │ │ │ │ └── ToolConfirmationMessage.test.tsx │ │ │ │ ├── ConsoleSummaryDisplay.tsx │ │ │ │ ├── shared │ │ │ │ │ └── ConfirmationDialog.tsx │ │ │ │ ├── GeminiRespondingSpinner.tsx │ │ │ │ ├── ShowMoreLines.tsx │ │ │ │ ├── MemoryUsageDisplay.tsx │ │ │ │ ├── AutoAcceptIndicator.tsx │ │ │ │ ├── Header.tsx │ │ │ │ ├── AuthInProgress.tsx │ │ │ │ ├── AsciiArt.ts │ │ │ │ ├── Tips.tsx │ │ │ │ ├── SessionSummaryDisplay.test.tsx │ │ │ │ ├── __snapshots__ │ │ │ │ │ └── SessionSummaryDisplay.test.tsx.snap │ │ │ │ ├── LoadingIndicator.tsx │ │ │ │ ├── ConsolePatcher.tsx │ │ │ │ ├── StatsDisplay.test.tsx │ │ │ │ ├── ContextSummaryDisplay.tsx │ │ │ │ ├── AboutBox.tsx │ │ │ │ └── SessionSummaryDisplay.tsx │ │ │ ├── contexts │ │ │ │ ├── StreamingContext.tsx │ │ │ │ └── OverflowContext.tsx │ │ │ ├── utils │ │ │ │ ├── commandUtils.ts │ │ │ │ ├── updateCheck.ts │ │ │ │ ├── textUtils.test.ts │ │ │ │ ├── formatters.ts │ │ │ │ ├── markdownUtilities.test.ts │ │ │ │ ├── formatters.test.ts │ │ │ │ └── textUtils.ts │ │ │ ├── colors.ts │ │ │ ├── editors │ │ │ │ └── editorSettingsManager.ts │ │ │ └── themes │ │ │ │ └── no-color.ts │ │ ├── utils │ │ │ ├── version.ts │ │ │ ├── cleanup.ts │ │ │ ├── sandbox-macos-permissive-open.sb │ │ │ ├── sandbox-macos-permissive-closed.sb │ │ │ ├── package.ts │ │ │ ├── readStdin.ts │ │ │ ├── sandbox-macos-permissive-proxied.sb │ │ │ ├── startupWarnings.ts │ │ │ └── startupWarnings.test.ts │ │ └── config │ │ │ └── auth.ts │ ├── tsconfig.json │ ├── index.ts │ ├── vitest.config.ts │ └── package.json └── core │ ├── src │ ├── utils │ │ ├── session.ts │ │ ├── messageInspectors.ts │ │ ├── LruCache.ts │ │ ├── fetch.ts │ │ ├── user_id.ts │ │ ├── gitUtils.ts │ │ ├── errors.ts │ │ ├── schemaValidator.ts │ │ ├── gitIgnoreParser.ts │ │ ├── testUtils.ts │ │ └── bfsFileSearch.ts │ ├── tools │ │ ├── diffOptions.ts │ │ ├── shell.json │ │ └── shell.md │ ├── index.test.ts │ ├── config │ │ └── models.ts │ ├── providers │ │ ├── providerFactory.ts │ │ └── geminiProvider.ts │ ├── code_assist │ │ ├── codeAssist.ts │ │ └── setup.ts │ ├── core │ │ ├── tokenLimits.ts │ │ ├── llmProvider.ts │ │ ├── contentGenerator.test.ts │ │ ├── geminiRequest.ts │ │ └── modelCheck.ts │ ├── __mocks__ │ │ └── fs │ │ │ └── promises.ts │ ├── telemetry │ │ ├── constants.ts │ │ ├── index.ts │ │ └── telemetry.test.ts │ └── index.ts │ ├── test-setup.ts │ ├── index.ts │ ├── tsconfig.json │ ├── vitest.config.ts │ └── package.json ├── .gemini └── config.yaml ├── .github ├── CODEOWNERS ├── ISSUE_TEMPLATE │ └── bug_report.md ├── pull_request_template.md └── workflows │ └── e2e.yml ├── scripts ├── esbuild-banner.js ├── create_alias.sh ├── clean.js ├── build_package.js ├── bind_package_version.js ├── setup-dev.js ├── bind_package_dependencies.js ├── publish-sandbox.js ├── copy_bundle_assets.js ├── copy_files.js ├── build.js ├── generate-git-commit-info.js ├── prepare-cli-packagejson.js ├── start.js └── telemetry.js ├── tsconfig.json ├── integration-tests ├── google_web_search.test.js ├── run_shell_command.test.js ├── save_memory.test.js ├── write_file.test.js ├── list_directory.test.js ├── replace.test.js ├── read_many_files.test.js ├── file-system.test.js ├── simple-mcp-server.test.js └── test-helper.js ├── .gitignore ├── .gitattributes ├── esbuild.config.js ├── .gcp ├── publish-dry-run.yaml └── dogfood.yaml ├── Dockerfile └── Makefile /.npmrc: -------------------------------------------------------------------------------- 1 | @google:registry=https://wombat-dressing-room.appspot.com -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "typescript.tsserver.experimental.enableProjectDiagnostics": true 3 | } 4 | -------------------------------------------------------------------------------- /docs/assets/theme-ansi.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CrazyBoyM/gemini-cli/main/docs/assets/theme-ansi.png -------------------------------------------------------------------------------- /docs/assets/theme-ayu.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CrazyBoyM/gemini-cli/main/docs/assets/theme-ayu.png -------------------------------------------------------------------------------- /docs/assets/theme-github.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CrazyBoyM/gemini-cli/main/docs/assets/theme-github.png -------------------------------------------------------------------------------- /docs/assets/theme-atom-one.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CrazyBoyM/gemini-cli/main/docs/assets/theme-atom-one.png -------------------------------------------------------------------------------- /docs/assets/theme-ayu-light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CrazyBoyM/gemini-cli/main/docs/assets/theme-ayu-light.png -------------------------------------------------------------------------------- /docs/assets/theme-default.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CrazyBoyM/gemini-cli/main/docs/assets/theme-default.png -------------------------------------------------------------------------------- /docs/assets/theme-dracula.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CrazyBoyM/gemini-cli/main/docs/assets/theme-dracula.png -------------------------------------------------------------------------------- /docs/assets/gemini-screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CrazyBoyM/gemini-cli/main/docs/assets/gemini-screenshot.png -------------------------------------------------------------------------------- /docs/assets/theme-ansi-light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CrazyBoyM/gemini-cli/main/docs/assets/theme-ansi-light.png -------------------------------------------------------------------------------- /docs/assets/theme-xcode-light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CrazyBoyM/gemini-cli/main/docs/assets/theme-xcode-light.png -------------------------------------------------------------------------------- /docs/assets/connected_devtools.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CrazyBoyM/gemini-cli/main/docs/assets/connected_devtools.png -------------------------------------------------------------------------------- /docs/assets/theme-default-light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CrazyBoyM/gemini-cli/main/docs/assets/theme-default-light.png -------------------------------------------------------------------------------- /docs/assets/theme-github-light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CrazyBoyM/gemini-cli/main/docs/assets/theme-github-light.png -------------------------------------------------------------------------------- /docs/assets/theme-google-light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CrazyBoyM/gemini-cli/main/docs/assets/theme-google-light.png -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "semi": true, 3 | "trailingComma": "all", 4 | "singleQuote": true, 5 | "printWidth": 80, 6 | "tabWidth": 2 7 | } 8 | -------------------------------------------------------------------------------- /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/utils/session.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright 2025 Google LLC 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | import { randomUUID } from 'crypto'; 8 | 9 | export const sessionId = randomUUID(); 10 | -------------------------------------------------------------------------------- /.gemini/config.yaml: -------------------------------------------------------------------------------- 1 | have_fun: false 2 | code_review: 3 | disable: true 4 | comment_severity_threshold: HIGH 5 | max_review_comments: -1 6 | pull_request_opened: 7 | help: false 8 | summary: false 9 | code_review: false 10 | ignore_patterns: [] 11 | -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | # By default, require reviews from the release approvers for all files. 2 | * @google-gemini/gemini-cli-askmode-approvers 3 | 4 | # The following files don't need reviews from the release approvers. 5 | # These patterns override the rule above. 6 | **/*.md 7 | /docs/ -------------------------------------------------------------------------------- /packages/core/test-setup.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright 2025 Google LLC 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | import { setSimulate429 } from './src/utils/testUtils.js'; 8 | 9 | // Disable 429 simulation globally for all tests 10 | setSimulate429(false); 11 | -------------------------------------------------------------------------------- /packages/core/src/tools/diffOptions.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright 2025 Google LLC 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | import * as Diff from 'diff'; 8 | 9 | export const DEFAULT_DIFF_OPTIONS: Diff.PatchOptions = { 10 | context: 3, 11 | ignoreWhitespace: true, 12 | }; 13 | -------------------------------------------------------------------------------- /packages/core/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 | export { 9 | DEFAULT_GEMINI_MODEL, 10 | DEFAULT_GEMINI_FLASH_MODEL, 11 | DEFAULT_GEMINI_EMBEDDING_MODEL, 12 | } from './src/config/models.js'; 13 | -------------------------------------------------------------------------------- /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/core/src/config/models.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright 2025 Google LLC 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | export const DEFAULT_GEMINI_MODEL = 'gemini-2.5-pro'; 8 | export const DEFAULT_GEMINI_FLASH_MODEL = 'gemini-2.5-flash'; 9 | export const DEFAULT_GEMINI_EMBEDDING_MODEL = 'gemini-embedding-001'; 10 | -------------------------------------------------------------------------------- /.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 | } 17 | -------------------------------------------------------------------------------- /packages/core/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "dist", 5 | "lib": ["DOM", "DOM.Iterable", "ES2021"], 6 | "composite": true, 7 | "types": ["node", "vitest/globals"] 8 | }, 9 | "include": ["index.ts", "src/**/*.ts", "src/**/*.json"], 10 | "exclude": ["node_modules", "dist"] 11 | } 12 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /scripts/esbuild-banner.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright 2025 Google LLC 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | // esbuild-banner.js 8 | import { createRequire } from 'module'; 9 | const require = createRequire(import.meta.url); 10 | globalThis.__filename = require('url').fileURLToPath(import.meta.url); 11 | globalThis.__dirname = require('path').dirname(globalThis.__filename); 12 | -------------------------------------------------------------------------------- /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 { 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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /packages/cli/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "dist", 5 | "jsx": "react-jsx", 6 | "lib": ["DOM", "DOM.Iterable", "ES2020"], 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", "src/**/*.test.ts"], 17 | "references": [{ "path": "../core" }] 18 | } 19 | -------------------------------------------------------------------------------- /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 React from 'react'; 8 | import { Box, Text } from 'ink'; 9 | import { Colors } from '../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/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 | 12 | // --- Global Entry Point --- 13 | main().catch((error) => { 14 | console.error('An unexpected critical error occurred:'); 15 | if (error instanceof Error) { 16 | console.error(error.stack); 17 | } else { 18 | console.error(String(error)); 19 | } 20 | process.exit(1); 21 | }); 22 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | --- 8 | 9 | **Describe the bug** 10 | A clear and concise description of what the bug is. 11 | 12 | **Client information** 13 | Please paste the full text from the /about command run from Gemini CLI. Also include which platform (MacOS, Windows, Linux). 14 | 15 | **Login information** 16 | Describe how you are logging in. 17 | 18 | **Additional context** 19 | Add any other context about the problem here. 20 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "strict": true, 4 | "esModuleInterop": true, 5 | "skipLibCheck": true, 6 | "forceConsistentCasingInFileNames": true, 7 | "resolveJsonModule": true, 8 | "sourceMap": true, 9 | "composite": true, 10 | "incremental": true, 11 | "declaration": true, 12 | "allowSyntheticDefaultImports": true, 13 | "lib": ["ES2023"], 14 | "module": "NodeNext", 15 | "moduleResolution": "nodenext", 16 | "target": "es2022", 17 | "types": ["node", "vitest/globals"] 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /integration-tests/google_web_search.test.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright 2025 Google LLC 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | import { test } from 'node:test'; 8 | import { strict as assert } from 'assert'; 9 | import { TestRig } from './test-helper.js'; 10 | 11 | test('should be able to search the web', async (t) => { 12 | const rig = new TestRig(); 13 | rig.setup(t.name); 14 | 15 | const prompt = `what planet do we live on`; 16 | const result = await rig.run(prompt); 17 | 18 | assert.ok(result.toLowerCase().includes('earth')); 19 | }); 20 | -------------------------------------------------------------------------------- /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 { Colors } from '../colors.js'; 9 | 10 | interface UpdateNotificationProps { 11 | message: string; 12 | } 13 | 14 | export const UpdateNotification = ({ message }: UpdateNotificationProps) => ( 15 | 21 | {message} 22 | 23 | ); 24 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # API keys and secrets 2 | .env 3 | .env~ 4 | 5 | # gemini-cli settings 6 | .gemini/ 7 | !gemini/config.yaml 8 | 9 | # Dependency directory 10 | node_modules 11 | bower_components 12 | 13 | # Editors 14 | .idea 15 | *.iml 16 | 17 | # OS metadata 18 | .DS_Store 19 | Thumbs.db 20 | 21 | # TypeScript build info files 22 | *.tsbuildinfo 23 | 24 | # Ignore built ts files 25 | dist 26 | 27 | # Docker folder to help skip auth refreshes 28 | .docker 29 | 30 | bundle 31 | 32 | # Test report files 33 | junit.xml 34 | packages/*/coverage/ 35 | 36 | # Generated files 37 | packages/cli/src/generated/ 38 | .integration-tests/ 39 | -------------------------------------------------------------------------------- /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 'fs'; 8 | import { join } from 'path'; 9 | import { getProjectTempDir } from '@google/gemini-cli-core'; 10 | 11 | export async function cleanupCheckpoints() { 12 | const tempDir = getProjectTempDir(process.cwd()); 13 | const checkpointsDir = join(tempDir, 'checkpoints'); 14 | try { 15 | await fs.rm(checkpointsDir, { recursive: true, force: true }); 16 | } catch { 17 | // Ignore errors if the directory doesn't exist or fails to delete. 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /integration-tests/run_shell_command.test.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright 2025 Google LLC 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | import { test } from 'node:test'; 8 | import { strict as assert } from 'assert'; 9 | import { TestRig } from './test-helper.js'; 10 | 11 | test('should be able to run a shell command', async (t) => { 12 | const rig = new TestRig(); 13 | rig.setup(t.name); 14 | rig.createFile('blah.txt', 'some content'); 15 | 16 | const prompt = `Can you use ls to list the contexts of the current folder`; 17 | const result = await rig.run(prompt); 18 | 19 | assert.ok(result.includes('blah.txt')); 20 | }); 21 | -------------------------------------------------------------------------------- /integration-tests/save_memory.test.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright 2025 Google LLC 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | import { test } from 'node:test'; 8 | import { strict as assert } from 'assert'; 9 | import { TestRig } from './test-helper.js'; 10 | 11 | test('should be able to save to memory', async (t) => { 12 | const rig = new TestRig(); 13 | rig.setup(t.name); 14 | 15 | const prompt = `remember that my favorite color is blue. 16 | 17 | what is my favorite color? tell me that and surround it with $ symbol`; 18 | const result = await rig.run(prompt); 19 | 20 | assert.ok(result.toLowerCase().includes('$blue$')); 21 | }); 22 | -------------------------------------------------------------------------------- /integration-tests/write_file.test.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright 2025 Google LLC 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | import { test } from 'node:test'; 8 | import { strict as assert } from 'assert'; 9 | import { TestRig } from './test-helper.js'; 10 | 11 | test('should be able to write a file', async (t) => { 12 | const rig = new TestRig(); 13 | rig.setup(t.name); 14 | const prompt = `show me an example of using the write tool. put a dad joke in dad.txt`; 15 | 16 | await rig.run(prompt); 17 | const newFilePath = 'dad.txt'; 18 | 19 | const newFileContent = rig.readFile(newFilePath); 20 | assert.notEqual(newFileContent, ''); 21 | }); 22 | -------------------------------------------------------------------------------- /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 { 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/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 | (literal "/dev/stdout") 17 | (literal "/dev/stderr") 18 | (literal "/dev/null") 19 | ) -------------------------------------------------------------------------------- /integration-tests/list_directory.test.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright 2025 Google LLC 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | import { test } from 'node:test'; 8 | import { strict as assert } from 'assert'; 9 | import { TestRig } from './test-helper.js'; 10 | 11 | test('should be able to list a directory', async (t) => { 12 | const rig = new TestRig(); 13 | rig.setup(t.name); 14 | rig.createFile('file1.txt', 'file 1 content'); 15 | rig.mkdir('subdir'); 16 | 17 | const prompt = `Can you list the files in the current directory`; 18 | const result = await rig.run(prompt); 19 | 20 | assert.ok(result.includes('file1.txt')); 21 | assert.ok(result.includes('subdir')); 22 | }); 23 | -------------------------------------------------------------------------------- /packages/core/src/tools/shell.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "object", 3 | "properties": { 4 | "command": { 5 | "description": "Exact bash command to execute as `bash -c `", 6 | "type": "string" 7 | }, 8 | "description": { 9 | "description": "Brief description of the command for the user. Be specific and concise. Ideally a single sentence. Can be up to 3 sentences for clarity. No line breaks.", 10 | "type": "string" 11 | }, 12 | "directory": { 13 | "description": "(OPTIONAL) Directory to run the command in, if not the project root directory. Must be relative to the project root directory and must already exist.", 14 | "type": "string" 15 | } 16 | }, 17 | "required": ["command"] 18 | } 19 | -------------------------------------------------------------------------------- /.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/ui/components/messages/UserShellMessage.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright 2025 Google LLC 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | import React from 'react'; 8 | import { Box, Text } from 'ink'; 9 | import { Colors } from '../../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 | -------------------------------------------------------------------------------- /integration-tests/replace.test.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright 2025 Google LLC 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | import { test } from 'node:test'; 8 | import { strict as assert } from 'assert'; 9 | import { TestRig } from './test-helper.js'; 10 | 11 | test('should be able to replace content in a file', async (t) => { 12 | const rig = new TestRig(); 13 | rig.setup(t.name); 14 | 15 | const fileName = 'file_to_replace.txt'; 16 | rig.createFile(fileName, 'original content'); 17 | const prompt = `Can you replace 'original' with 'replaced' in the file 'file_to_replace.txt'`; 18 | 19 | await rig.run(prompt); 20 | const newFileContent = rig.readFile(fileName); 21 | assert.strictEqual(newFileContent, 'replaced content'); 22 | }); 23 | -------------------------------------------------------------------------------- /integration-tests/read_many_files.test.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright 2025 Google LLC 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | import { test } from 'node:test'; 8 | import { strict as assert } from 'assert'; 9 | import { TestRig } from './test-helper.js'; 10 | 11 | test.skip('should be able to read multiple files', async (t) => { 12 | const rig = new TestRig(); 13 | rig.setup(t.name); 14 | rig.createFile('file1.txt', 'file 1 content'); 15 | rig.createFile('file2.txt', 'file 2 content'); 16 | 17 | const prompt = `Read the files in this directory, list them and print them to the screen`; 18 | const result = await rig.run(prompt); 19 | 20 | assert.ok(result.includes('file 1 content')); 21 | assert.ok(result.includes('file 2 content')); 22 | }); 23 | -------------------------------------------------------------------------------- /docs/cli/token-caching.md: -------------------------------------------------------------------------------- 1 | # Token Caching and Cost Optimization 2 | 3 | Gemini CLI automatically optimizes API costs through token caching when using API key authentication (Gemini API key or Vertex AI). This feature reuses previous system instructions and context to reduce the number of tokens processed in subsequent requests. 4 | 5 | **Token caching is available for:** 6 | 7 | - API key users (Gemini API key) 8 | - Vertex AI users (with project and location setup) 9 | 10 | **Token caching is not available for:** 11 | 12 | - OAuth users (Google Personal/Enterprise accounts) - the Code Assist API does not support cached content creation at this time 13 | 14 | You can view your token usage and cached token savings using the `/stats` command. When cached tokens are available, they will be displayed in the stats output. 15 | -------------------------------------------------------------------------------- /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 React from 'react'; 8 | import { Text, Box } from 'ink'; 9 | import { Colors } from '../../colors.js'; 10 | 11 | interface UserMessageProps { 12 | text: string; 13 | } 14 | 15 | export const UserMessage: 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/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 | }, 32 | }); 33 | -------------------------------------------------------------------------------- /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 React from 'react'; 8 | import { Text, Box } from 'ink'; 9 | import { Colors } from '../../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/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 React from 'react'; 8 | import { Text, Box } from 'ink'; 9 | import { Colors } from '../../colors.js'; 10 | 11 | interface InfoMessageProps { 12 | text: string; 13 | } 14 | 15 | export const InfoMessage: 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 | -------------------------------------------------------------------------------- /integration-tests/file-system.test.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright 2025 Google LLC 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | import { strict as assert } from 'assert'; 8 | import { test } from 'node:test'; 9 | import { TestRig } from './test-helper.js'; 10 | 11 | test('reads a file', (t) => { 12 | const rig = new TestRig(); 13 | rig.setup(t.name); 14 | rig.createFile('test.txt', 'hello world'); 15 | 16 | const output = rig.run(`read the file name test.txt`); 17 | 18 | assert.ok(output.toLowerCase().includes('hello')); 19 | }); 20 | 21 | test('writes a file', (t) => { 22 | const rig = new TestRig(); 23 | rig.setup(t.name); 24 | rig.createFile('test.txt', ''); 25 | 26 | rig.run(`edit test.txt to have a hello world message`); 27 | 28 | const fileContent = rig.readFile('test.txt'); 29 | assert.ok(fileContent.toLowerCase().includes('hello')); 30 | }); 31 | -------------------------------------------------------------------------------- /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 | const TERMINAL_PADDING_X = 8; 10 | 11 | export function useTerminalSize(): { columns: number; rows: number } { 12 | const [size, setSize] = useState({ 13 | columns: (process.stdout.columns || 60) - TERMINAL_PADDING_X, 14 | rows: process.stdout.rows || 20, 15 | }); 16 | 17 | useEffect(() => { 18 | function updateSize() { 19 | setSize({ 20 | columns: (process.stdout.columns || 60) - TERMINAL_PADDING_X, 21 | rows: process.stdout.rows || 20, 22 | }); 23 | } 24 | 25 | process.stdout.on('resize', updateSize); 26 | return () => { 27 | process.stdout.off('resize', updateSize); 28 | }; 29 | }, []); 30 | 31 | return size; 32 | } 33 | -------------------------------------------------------------------------------- /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 | (literal "/dev/stdout") 17 | (literal "/dev/stderr") 18 | (literal "/dev/null") 19 | ) 20 | 21 | ;; deny all inbound network traffic EXCEPT on debugger port 22 | (deny network-inbound) 23 | (allow network-inbound (local ip "localhost:9229")) 24 | 25 | ;; deny all outbound network traffic 26 | (deny network-outbound) 27 | -------------------------------------------------------------------------------- /packages/core/src/providers/providerFactory.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright 2025 Google LLC 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | import { LlmProvider } from '../core/llmProvider'; 8 | import { GeminiProvider } from './geminiProvider'; 9 | import { OpenAIProvider } from './openAiProvider'; 10 | import { ClaudeProvider } from './claudeProvider'; 11 | import { OllamaProvider } from './ollamaProvider'; 12 | import { Config } from '../config/config'; 13 | 14 | export function providerFactory(config: Config): LlmProvider { 15 | const provider = config.getProvider(); 16 | switch (provider) { 17 | case 'openai': 18 | return new OpenAIProvider(config); 19 | case 'claude': 20 | return new ClaudeProvider(config); 21 | case 'ollama': 22 | return new OllamaProvider(config); 23 | case 'gemini': 24 | default: 25 | return new GeminiProvider(config); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /packages/core/src/code_assist/codeAssist.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright 2025 Google LLC 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | import { AuthType, ContentGenerator } from '../core/contentGenerator.js'; 8 | import { getOauthClient } from './oauth2.js'; 9 | import { setupUser } from './setup.js'; 10 | import { CodeAssistServer, HttpOptions } from './server.js'; 11 | 12 | export async function createCodeAssistContentGenerator( 13 | httpOptions: HttpOptions, 14 | authType: AuthType, 15 | ): Promise { 16 | if ( 17 | authType === AuthType.LOGIN_WITH_GOOGLE_ENTERPRISE || 18 | authType === AuthType.LOGIN_WITH_GOOGLE_PERSONAL 19 | ) { 20 | const authClient = await getOauthClient(); 21 | const projectId = await setupUser(authClient); 22 | return new CodeAssistServer(authClient, projectId, httpOptions); 23 | } 24 | 25 | throw new Error(`Unsupported authType: ${authType}`); 26 | } 27 | -------------------------------------------------------------------------------- /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 { sessionId, Logger } from '@google/gemini-cli-core'; 9 | 10 | /** 11 | * Hook to manage the logger instance. 12 | */ 13 | export const useLogger = () => { 14 | const [logger, setLogger] = useState(null); 15 | 16 | useEffect(() => { 17 | const newLogger = new Logger(sessionId); 18 | /** 19 | * Start async initialization, no need to await. Using await slows down the 20 | * time from launch to see the gemini-cli prompt and it's better to not save 21 | * messages than for the cli to hanging waiting for the logger to loading. 22 | */ 23 | newLogger 24 | .initialize() 25 | .then(() => { 26 | setLogger(newLogger); 27 | }) 28 | .catch(() => {}); 29 | }, []); 30 | 31 | return logger; 32 | }; 33 | -------------------------------------------------------------------------------- /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 'url'; 12 | import path from '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/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.0-flash': 25 | return 1_048_576; 26 | case 'gemini-2.0-flash-preview-image-generation': 27 | return 32_000; 28 | default: 29 | return DEFAULT_TOKEN_LIMIT; 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 React from 'react'; 8 | import { Box, Text } from 'ink'; 9 | import { Colors } from '../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 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | ## TLDR 2 | 3 | 4 | 5 | ## Dive Deeper 6 | 7 | 8 | 9 | ## Reviewer Test Plan 10 | 11 | 12 | 13 | ## Testing Matrix 14 | 15 | 16 | 17 | | | 🍏 | 🪟 | 🐧 | 18 | | -------- | --- | --- | --- | 19 | | npm run | ❓ | ❓ | ❓ | 20 | | npx | ❓ | ❓ | ❓ | 21 | | Docker | ❓ | ❓ | ❓ | 22 | | Podman | ❓ | - | - | 23 | | Seatbelt | ❓ | - | - | 24 | 25 | ## Linked issues / bugs 26 | 27 | 28 | -------------------------------------------------------------------------------- /packages/cli/src/ui/utils/commandUtils.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright 2025 Google LLC 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | /** 8 | * Checks if a query string potentially represents an '@' command. 9 | * It triggers if the query starts with '@' or contains '@' preceded by whitespace 10 | * and followed by a non-whitespace character. 11 | * 12 | * @param query The input query string. 13 | * @returns True if the query looks like an '@' command, false otherwise. 14 | */ 15 | export const isAtCommand = (query: string): boolean => 16 | // Check if starts with @ OR has a space, then @ 17 | query.startsWith('@') || /\s@/.test(query); 18 | 19 | /** 20 | * Checks if a query string potentially represents an '/' command. 21 | * It triggers if the query starts with '/' 22 | * 23 | * @param query The input query string. 24 | * @returns True if the query looks like an '/' command, false otherwise. 25 | */ 26 | export const isSlashCommand = (query: string): boolean => query.startsWith('/'); 27 | -------------------------------------------------------------------------------- /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/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 | 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 | }, 36 | }); 37 | -------------------------------------------------------------------------------- /packages/core/src/tools/shell.md: -------------------------------------------------------------------------------- 1 | This tool executes a given shell command as `bash -c `. 2 | Command can start background processes using `&`. 3 | Command is executed as a subprocess that leads its own process group. 4 | Command process group can be terminated as `kill -- -PGID` or signaled as `kill -s SIGNAL -- -PGID`. 5 | 6 | The following information is returned: 7 | 8 | Command: Executed command. 9 | Directory: Directory (relative to project root) where command was executed, or `(root)`. 10 | Stdout: Output on stdout stream. Can be `(empty)` or partial on error and for any unwaited background processes. 11 | Stderr: Output on stderr stream. Can be `(empty)` or partial on error and for any unwaited background processes. 12 | Error: Error or `(none)` if no error was reported for the subprocess. 13 | Exit Code: Exit code or `(none)` if terminated by signal. 14 | Signal: Signal number or `(none)` if no signal was received. 15 | Background PIDs: List of background processes started or `(none)`. 16 | Process Group PGID: Process group started or `(none)` 17 | -------------------------------------------------------------------------------- /packages/cli/src/utils/readStdin.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright 2025 Google LLC 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | export async function readStdin(): Promise { 8 | return new Promise((resolve, reject) => { 9 | let data = ''; 10 | process.stdin.setEncoding('utf8'); 11 | 12 | const onReadable = () => { 13 | let chunk; 14 | while ((chunk = process.stdin.read()) !== null) { 15 | data += chunk; 16 | } 17 | }; 18 | 19 | const onEnd = () => { 20 | cleanup(); 21 | resolve(data); 22 | }; 23 | 24 | const onError = (err: Error) => { 25 | cleanup(); 26 | reject(err); 27 | }; 28 | 29 | const cleanup = () => { 30 | process.stdin.removeListener('readable', onReadable); 31 | process.stdin.removeListener('end', onEnd); 32 | process.stdin.removeListener('error', onError); 33 | }; 34 | 35 | process.stdin.on('readable', onReadable); 36 | process.stdin.on('end', onEnd); 37 | process.stdin.on('error', onError); 38 | }); 39 | } 40 | -------------------------------------------------------------------------------- /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/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 | 9 | export const EVENT_USER_PROMPT = 'gemini_cli.user_prompt'; 10 | export const EVENT_TOOL_CALL = 'gemini_cli.tool_call'; 11 | export const EVENT_API_REQUEST = 'gemini_cli.api_request'; 12 | export const EVENT_API_ERROR = 'gemini_cli.api_error'; 13 | export const EVENT_API_RESPONSE = 'gemini_cli.api_response'; 14 | export const EVENT_CLI_CONFIG = 'gemini_cli.config'; 15 | 16 | export const METRIC_TOOL_CALL_COUNT = 'gemini_cli.tool.call.count'; 17 | export const METRIC_TOOL_CALL_LATENCY = 'gemini_cli.tool.call.latency'; 18 | export const METRIC_API_REQUEST_COUNT = 'gemini_cli.api.request.count'; 19 | export const METRIC_API_REQUEST_LATENCY = 'gemini_cli.api.request.latency'; 20 | export const METRIC_TOKEN_USAGE = 'gemini_cli.token.usage'; 21 | export const METRIC_SESSION_COUNT = 'gemini_cli.session.count'; 22 | export const METRIC_FILE_OPERATION_COUNT = 'gemini_cli.file.operation.count'; 23 | -------------------------------------------------------------------------------- /packages/core/src/telemetry/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright 2025 Google LLC 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | export enum TelemetryTarget { 8 | GCP = 'gcp', 9 | LOCAL = 'local', 10 | } 11 | 12 | const DEFAULT_TELEMETRY_TARGET = TelemetryTarget.LOCAL; 13 | const DEFAULT_OTLP_ENDPOINT = 'http://localhost:4317'; 14 | 15 | export { DEFAULT_TELEMETRY_TARGET, DEFAULT_OTLP_ENDPOINT }; 16 | export { 17 | initializeTelemetry, 18 | shutdownTelemetry, 19 | isTelemetrySdkInitialized, 20 | } from './sdk.js'; 21 | export { 22 | logCliConfiguration, 23 | logUserPrompt, 24 | logToolCall, 25 | logApiRequest, 26 | logApiError, 27 | logApiResponse, 28 | } from './loggers.js'; 29 | export { 30 | StartSessionEvent, 31 | EndSessionEvent, 32 | UserPromptEvent, 33 | ToolCallEvent, 34 | ApiRequestEvent, 35 | ApiErrorEvent, 36 | ApiResponseEvent, 37 | TelemetryEvent, 38 | } from './types.js'; 39 | export { SpanStatusCode, ValueType } from '@opentelemetry/api'; 40 | export { SemanticAttributes } from '@opentelemetry/semantic-conventions'; 41 | -------------------------------------------------------------------------------- /esbuild.config.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright 2025 Google LLC 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | import esbuild from 'esbuild'; 8 | import path from 'path'; 9 | import { fileURLToPath } from 'url'; 10 | import { createRequire } from 'module'; 11 | 12 | const __filename = fileURLToPath(import.meta.url); 13 | const __dirname = path.dirname(__filename); 14 | const require = createRequire(import.meta.url); 15 | const pkg = require(path.resolve(__dirname, 'package.json')); 16 | 17 | esbuild 18 | .build({ 19 | entryPoints: ['packages/cli/index.ts'], 20 | bundle: true, 21 | outfile: 'bundle/gemini.js', 22 | platform: 'node', 23 | format: 'esm', 24 | define: { 25 | 'process.env.CLI_VERSION': JSON.stringify(pkg.version), 26 | }, 27 | banner: { 28 | js: `import { createRequire } from 'module'; const require = createRequire(import.meta.url); globalThis.__filename = require('url').fileURLToPath(import.meta.url); globalThis.__dirname = require('path').dirname(globalThis.__filename);`, 29 | }, 30 | }) 31 | .catch(() => process.exit(1)); 32 | -------------------------------------------------------------------------------- /.gcp/publish-dry-run.yaml: -------------------------------------------------------------------------------- 1 | steps: 2 | - name: 'us-west1-docker.pkg.dev/gemini-code-dev/gemini-code-containers/gemini-code-builder' 3 | entrypoint: 'npm' 4 | args: ['install'] 5 | 6 | - name: 'us-west1-docker.pkg.dev/gemini-code-dev/gemini-code-containers/gemini-code-builder' 7 | entrypoint: 'npm' 8 | args: ['run', 'auth'] 9 | 10 | - name: 'us-west1-docker.pkg.dev/gemini-code-dev/gemini-code-containers/gemini-code-builder' 11 | entrypoint: 'npm' 12 | args: 13 | [ 14 | 'run', 15 | 'prerelease:version', 16 | '--workspaces', 17 | '--', 18 | '--suffix="$SHORT_SHA.$_REVISION"', 19 | ] 20 | 21 | - name: 'us-west1-docker.pkg.dev/gemini-code-dev/gemini-code-containers/gemini-code-builder' 22 | entrypoint: 'npm' 23 | args: ['run', 'prerelease:deps', '--workspaces'] 24 | 25 | - name: 'us-west1-docker.pkg.dev/gemini-code-dev/gemini-code-containers/gemini-code-builder' 26 | entrypoint: 'npm' 27 | args: 28 | ['publish', '--tag=head', '--dry-run', '--workspace=@google/gemini-cli'] 29 | 30 | options: 31 | defaultLogsBucketBehavior: REGIONAL_USER_OWNED_BUCKET 32 | -------------------------------------------------------------------------------- /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 [_, 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 [ref, setStateInternal] as const; 36 | }; 37 | -------------------------------------------------------------------------------- /packages/cli/src/ui/components/shared/ConfirmationDialog.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright 2025 Google LLC 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | import React from 'react'; 8 | import { Box, Text, useInput } from 'ink'; 9 | import { Colors } from '../../colors.js'; 10 | 11 | interface ConfirmationDialogProps { 12 | prompt: string; 13 | onConfirmation: (confirmed: boolean) => void; 14 | } 15 | 16 | export const ConfirmationDialog: React.FC = ({ 17 | prompt, 18 | onConfirmation, 19 | }) => { 20 | useInput((input, key) => { 21 | if (key.return) { 22 | onConfirmation(true); 23 | } else if (input.toLowerCase() === 'y') { 24 | onConfirmation(true); 25 | } else if (key.escape || input.toLowerCase() === 'n') { 26 | onConfirmation(false); 27 | } 28 | }); 29 | 30 | return ( 31 | 37 | {prompt} 38 | (y/N) 39 | 40 | ); 41 | }; 42 | -------------------------------------------------------------------------------- /packages/cli/src/ui/components/GeminiRespondingSpinner.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright 2025 Google LLC 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | import React from 'react'; 8 | import { Text } from 'ink'; 9 | import Spinner from 'ink-spinner'; 10 | import type { SpinnerName } from 'cli-spinners'; 11 | import { useStreamingContext } from '../contexts/StreamingContext.js'; 12 | import { StreamingState } from '../types.js'; 13 | 14 | interface GeminiRespondingSpinnerProps { 15 | /** 16 | * Optional string to display when not in Responding state. 17 | * If not provided and not Responding, renders null. 18 | */ 19 | nonRespondingDisplay?: string; 20 | spinnerType?: SpinnerName; 21 | } 22 | 23 | export const GeminiRespondingSpinner: React.FC< 24 | GeminiRespondingSpinnerProps 25 | > = ({ nonRespondingDisplay, spinnerType = 'dots' }) => { 26 | const streamingState = useStreamingContext(); 27 | 28 | if (streamingState === StreamingState.Responding) { 29 | return ; 30 | } else if (nonRespondingDisplay) { 31 | return {nonRespondingDisplay}; 32 | } 33 | return null; 34 | }; 35 | -------------------------------------------------------------------------------- /packages/cli/src/ui/utils/updateCheck.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright 2025 Google LLC 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | import updateNotifier from 'update-notifier'; 8 | import { getPackageJson } from '../../utils/package.js'; 9 | 10 | export async function checkForUpdates(): Promise { 11 | try { 12 | const packageJson = await getPackageJson(); 13 | if (!packageJson || !packageJson.name || !packageJson.version) { 14 | return null; 15 | } 16 | const notifier = updateNotifier({ 17 | pkg: { 18 | name: packageJson.name, 19 | version: packageJson.version, 20 | }, 21 | // check every time 22 | updateCheckInterval: 0, 23 | // allow notifier to run in scripts 24 | shouldNotifyInNpmScript: true, 25 | }); 26 | 27 | if (notifier.update) { 28 | return `Gemini CLI update available! ${notifier.update.current} → ${notifier.update.latest}\nRun npm install -g ${packageJson.name} to update`; 29 | } 30 | 31 | return null; 32 | } catch (e) { 33 | console.warn('Failed to check for updates: ' + e); 34 | return null; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /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 { Colors } from '../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/cli/src/utils/sandbox-macos-permissive-proxied.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 | (literal "/dev/stdout") 17 | (literal "/dev/stderr") 18 | (literal "/dev/null") 19 | ) 20 | 21 | ;; deny all inbound network traffic EXCEPT on debugger port 22 | (deny network-inbound) 23 | (allow network-inbound (local ip "localhost:9229")) 24 | 25 | ;; deny all outbound network traffic EXCEPT through proxy on localhost:8877 26 | ;; set `GEMINI_SANDBOX_PROXY_COMMAND=` to run proxy alongside sandbox 27 | ;; proxy must listen on :::8877 (see scripts/example-proxy.js) 28 | (deny network-outbound) 29 | (allow network-outbound (remote tcp "localhost:8877")) 30 | 31 | (allow network-bind (local ip "*:*")) 32 | -------------------------------------------------------------------------------- /packages/cli/src/ui/components/messages/GeminiMessage.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright 2025 Google LLC 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | import React from 'react'; 8 | import { Text, Box } from 'ink'; 9 | import { MarkdownDisplay } from '../../utils/MarkdownDisplay.js'; 10 | import { Colors } from '../../colors.js'; 11 | 12 | interface GeminiMessageProps { 13 | text: string; 14 | isPending: boolean; 15 | availableTerminalHeight?: number; 16 | terminalWidth: number; 17 | } 18 | 19 | export const GeminiMessage: React.FC = ({ 20 | text, 21 | isPending, 22 | availableTerminalHeight, 23 | terminalWidth, 24 | }) => { 25 | const prefix = '✦ '; 26 | const prefixWidth = prefix.length; 27 | 28 | return ( 29 | 30 | 31 | {prefix} 32 | 33 | 34 | 40 | 41 | 42 | ); 43 | }; 44 | -------------------------------------------------------------------------------- /packages/cli/src/ui/components/MemoryUsageDisplay.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright 2025 Google LLC 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | import React, { useEffect, useState } from 'react'; 8 | import { Box, Text } from 'ink'; 9 | import { Colors } from '../colors.js'; 10 | import process from 'node:process'; 11 | import { formatMemoryUsage } from '../utils/formatters.js'; 12 | 13 | export const MemoryUsageDisplay: React.FC = () => { 14 | const [memoryUsage, setMemoryUsage] = useState(''); 15 | const [memoryUsageColor, setMemoryUsageColor] = useState(Colors.Gray); 16 | 17 | useEffect(() => { 18 | const updateMemory = () => { 19 | const usage = process.memoryUsage().rss; 20 | setMemoryUsage(formatMemoryUsage(usage)); 21 | setMemoryUsageColor( 22 | usage >= 2 * 1024 * 1024 * 1024 ? Colors.AccentRed : Colors.Gray, 23 | ); 24 | }; 25 | const intervalId = setInterval(updateMemory, 2000); 26 | updateMemory(); // Initial update 27 | return () => clearInterval(intervalId); 28 | }, []); 29 | 30 | return ( 31 | 32 | | 33 | {memoryUsage} 34 | 35 | ); 36 | }; 37 | -------------------------------------------------------------------------------- /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 Gemini API. The `google_web_search` tool returns a summary of web results with sources. 8 | 9 | ### Arguments 10 | 11 | `google_web_search` takes one argument: 12 | 13 | - `query` (string, required): The search query. 14 | 15 | ## How to use `google_web_search` with the Gemini CLI 16 | 17 | The `google_web_search` tool sends a query to the Gemini API, which then performs a web search. `google_web_search` will return a generated response based on the search results, including citations and sources. 18 | 19 | Usage: 20 | 21 | ``` 22 | google_web_search(query="Your query goes here.") 23 | ``` 24 | 25 | ## `google_web_search` examples 26 | 27 | Get information on a topic: 28 | 29 | ``` 30 | google_web_search(query="latest advancements in AI-powered code generation") 31 | ``` 32 | 33 | ## Important notes 34 | 35 | - **Response returned:** The `google_web_search` tool returns a processed summary, not a raw list of search results. 36 | - **Citations:** The response includes citations to the sources used to generate the summary. 37 | -------------------------------------------------------------------------------- /scripts/create_alias.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # This script creates an alias for the Gemini CLI 4 | 5 | # Determine the project directory 6 | PROJECT_DIR=$(cd "$(dirname "$0")/.." && pwd) 7 | ALIAS_COMMAND="alias gemini='$PROJECT_DIR/scripts/start.sh'" 8 | 9 | # Detect shell and set config file path 10 | if [[ "$SHELL" == *"/bash" ]]; then 11 | CONFIG_FILE="$HOME/.bashrc" 12 | elif [[ "$SHELL" == *"/zsh" ]]; then 13 | CONFIG_FILE="$HOME/.zshrc" 14 | else 15 | echo "Unsupported shell. Only bash and zsh are supported." 16 | exit 1 17 | fi 18 | 19 | echo "This script will add the following alias to your shell configuration file ($CONFIG_FILE):" 20 | echo " $ALIAS_COMMAND" 21 | echo "" 22 | 23 | # Check if the alias already exists 24 | if grep -q "alias gemini=" "$CONFIG_FILE"; then 25 | echo "A 'gemini' alias already exists in $CONFIG_FILE. No changes were made." 26 | exit 0 27 | fi 28 | 29 | read -p "Do you want to proceed? (y/n) " -n 1 -r 30 | echo "" 31 | if [[ $REPLY =~ ^[Yy]$ ]]; then 32 | echo "$ALIAS_COMMAND" >> "$CONFIG_FILE" 33 | echo "" 34 | echo "Alias added to $CONFIG_FILE." 35 | echo "Please run 'source $CONFIG_FILE' or open a new terminal to use the 'gemini' command." 36 | else 37 | echo "Aborted. No changes were made." 38 | fi 39 | -------------------------------------------------------------------------------- /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 React from 'react'; 8 | import { Box, Text } from 'ink'; 9 | import { Colors } from '../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 = Colors.AccentGreen; 26 | textContent = 'accepting edits'; 27 | subText = ' (shift + tab to toggle)'; 28 | break; 29 | case ApprovalMode.YOLO: 30 | textColor = Colors.AccentRed; 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 | -------------------------------------------------------------------------------- /scripts/clean.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 'child_process'; 21 | import { rmSync } from 'fs'; 22 | import { dirname, join } from 'path'; 23 | import { fileURLToPath } from 'url'; 24 | 25 | const __dirname = dirname(fileURLToPath(import.meta.url)); 26 | const root = join(__dirname, '..'); 27 | 28 | // remove npm install/build artifacts 29 | rmSync(join(root, 'node_modules'), { recursive: true, force: true }); 30 | rmSync(join(root, 'packages/cli/src/generated/'), { 31 | recursive: true, 32 | force: true, 33 | }); 34 | execSync('npm run clean --workspaces', { stdio: 'inherit', cwd: root }); 35 | -------------------------------------------------------------------------------- /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 'child_process'; 21 | import { writeFileSync } from 'fs'; 22 | import { join } from '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 | -------------------------------------------------------------------------------- /.github/workflows/e2e.yml: -------------------------------------------------------------------------------- 1 | # .github/workflows/e2e.yml 2 | 3 | name: E2E Tests 4 | 5 | on: 6 | push: 7 | branches: [main] 8 | merge_group: 9 | 10 | jobs: 11 | e2e-test: 12 | name: E2E Test - ${{ matrix.sandbox }} 13 | runs-on: ubuntu-latest 14 | strategy: 15 | matrix: 16 | sandbox: [sandbox:none, sandbox:docker] 17 | steps: 18 | - name: Checkout repository 19 | uses: actions/checkout@v4 20 | 21 | - name: Set up Node.js 22 | uses: actions/setup-node@v4 23 | with: 24 | node-version: 20.x 25 | cache: 'npm' 26 | 27 | - name: Install dependencies 28 | run: npm ci 29 | 30 | - name: Build project 31 | run: npm run build 32 | 33 | - name: Set up Docker 34 | if: matrix.sandbox == 'sandbox:docker' 35 | uses: docker/setup-buildx-action@v3 36 | 37 | - name: Set up Podman 38 | if: matrix.sandbox == 'sandbox:podman' 39 | uses: redhat-actions/podman-login@v1 40 | with: 41 | registry: docker.io 42 | username: ${{ secrets.DOCKERHUB_USERNAME }} 43 | password: ${{ secrets.DOCKERHUB_TOKEN }} 44 | 45 | - name: Run E2E tests 46 | env: 47 | GEMINI_API_KEY: ${{ secrets.GEMINI_API_KEY }} 48 | run: npm run test:integration:${{ matrix.sandbox }} -- --verbose --keep-output 49 | -------------------------------------------------------------------------------- /packages/cli/src/ui/utils/textUtils.test.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright 2025 Google LLC 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | import { isBinary } from './textUtils'; 8 | 9 | describe('textUtils', () => { 10 | describe('isBinary', () => { 11 | it('should return true for a buffer containing a null byte', () => { 12 | const buffer = Buffer.from([ 13 | 0x89, 0x50, 0x4e, 0x47, 0x0d, 0x1a, 0x0a, 0x00, 14 | ]); 15 | expect(isBinary(buffer)).toBe(true); 16 | }); 17 | 18 | it('should return false for a buffer containing only text', () => { 19 | const buffer = Buffer.from('This is a test string.'); 20 | expect(isBinary(buffer)).toBe(false); 21 | }); 22 | 23 | it('should return false for an empty buffer', () => { 24 | const buffer = Buffer.from([]); 25 | expect(isBinary(buffer)).toBe(false); 26 | }); 27 | 28 | it('should return false for a null or undefined buffer', () => { 29 | expect(isBinary(null)).toBe(false); 30 | expect(isBinary(undefined)).toBe(false); 31 | }); 32 | 33 | it('should only check the sample size', () => { 34 | const longBufferWithNullByteAtEnd = Buffer.concat([ 35 | Buffer.from('a'.repeat(1024)), 36 | Buffer.from([0x00]), 37 | ]); 38 | expect(isBinary(longBufferWithNullByteAtEnd, 512)).toBe(false); 39 | }); 40 | }); 41 | }); 42 | -------------------------------------------------------------------------------- /packages/cli/src/ui/components/messages/GeminiMessageContent.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright 2025 Google LLC 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | import React from 'react'; 8 | import { Box } from 'ink'; 9 | import { MarkdownDisplay } from '../../utils/MarkdownDisplay.js'; 10 | 11 | interface GeminiMessageContentProps { 12 | text: string; 13 | isPending: boolean; 14 | availableTerminalHeight?: number; 15 | terminalWidth: number; 16 | } 17 | 18 | /* 19 | * Gemini message content is a semi-hacked component. The intention is to represent a partial 20 | * of GeminiMessage and is only used when a response gets too long. In that instance messages 21 | * are split into multiple GeminiMessageContent's to enable the root component in 22 | * App.tsx to be as performant as humanly possible. 23 | */ 24 | export const GeminiMessageContent: React.FC = ({ 25 | text, 26 | isPending, 27 | availableTerminalHeight, 28 | terminalWidth, 29 | }) => { 30 | const originalPrefix = '✦ '; 31 | const prefixWidth = originalPrefix.length; 32 | 33 | return ( 34 | 35 | 41 | 42 | ); 43 | }; 44 | -------------------------------------------------------------------------------- /packages/cli/src/ui/components/Header.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright 2025 Google LLC 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | import React from 'react'; 8 | import { Box, Text } from 'ink'; 9 | import Gradient from 'ink-gradient'; 10 | import { Colors } from '../colors.js'; 11 | import { shortAsciiLogo, longAsciiLogo } from './AsciiArt.js'; 12 | import { getAsciiArtWidth } from '../utils/textUtils.js'; 13 | 14 | interface HeaderProps { 15 | customAsciiArt?: string; // For user-defined ASCII art 16 | terminalWidth: number; // For responsive logo 17 | } 18 | 19 | export const Header: React.FC = ({ 20 | customAsciiArt, 21 | terminalWidth, 22 | }) => { 23 | let displayTitle; 24 | const widthOfLongLogo = getAsciiArtWidth(longAsciiLogo); 25 | 26 | if (customAsciiArt) { 27 | displayTitle = customAsciiArt; 28 | } else { 29 | displayTitle = 30 | terminalWidth >= widthOfLongLogo ? longAsciiLogo : shortAsciiLogo; 31 | } 32 | 33 | const artWidth = getAsciiArtWidth(displayTitle); 34 | 35 | return ( 36 | 42 | {Colors.GradientColors ? ( 43 | 44 | {displayTitle} 45 | 46 | ) : ( 47 | {displayTitle} 48 | )} 49 | 50 | ); 51 | }; 52 | -------------------------------------------------------------------------------- /packages/cli/src/ui/components/AuthInProgress.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright 2025 Google LLC 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | import React, { useState, useEffect } from 'react'; 8 | import { Box, Text, useInput } from 'ink'; 9 | import Spinner from 'ink-spinner'; 10 | import { Colors } from '../colors.js'; 11 | 12 | interface AuthInProgressProps { 13 | onTimeout: () => void; 14 | } 15 | 16 | export function AuthInProgress({ 17 | onTimeout, 18 | }: AuthInProgressProps): React.JSX.Element { 19 | const [timedOut, setTimedOut] = useState(false); 20 | 21 | useInput((_, key) => { 22 | if (key.escape) { 23 | onTimeout(); 24 | } 25 | }); 26 | 27 | useEffect(() => { 28 | const timer = setTimeout(() => { 29 | setTimedOut(true); 30 | onTimeout(); 31 | }, 180000); 32 | 33 | return () => clearTimeout(timer); 34 | }, [onTimeout]); 35 | 36 | return ( 37 | 44 | {timedOut ? ( 45 | 46 | Authentication timed out. Please try again. 47 | 48 | ) : ( 49 | 50 | 51 | Waiting for auth... (Press ESC to cancel) 52 | 53 | 54 | )} 55 | 56 | ); 57 | } 58 | -------------------------------------------------------------------------------- /packages/cli/src/ui/components/AsciiArt.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright 2025 Google LLC 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | export const shortAsciiLogo = ` 8 | █████████ ██████████ ██████ ██████ █████ ██████ █████ █████ 9 | ███░░░░░███░░███░░░░░█░░██████ ██████ ░░███ ░░██████ ░░███ ░░███ 10 | ███ ░░░ ░███ █ ░ ░███░█████░███ ░███ ░███░███ ░███ ░███ 11 | ░███ ░██████ ░███░░███ ░███ ░███ ░███░░███░███ ░███ 12 | ░███ █████ ░███░░█ ░███ ░░░ ░███ ░███ ░███ ░░██████ ░███ 13 | ░░███ ░░███ ░███ ░ █ ░███ ░███ ░███ ░███ ░░█████ ░███ 14 | ░░█████████ ██████████ █████ █████ █████ █████ ░░█████ █████ 15 | ░░░░░░░░░ ░░░░░░░░░░ ░░░░░ ░░░░░ ░░░░░ ░░░░░ ░░░░░ ░░░░░ 16 | `; 17 | 18 | export const longAsciiLogo = ` 19 | ███ █████████ ██████████ ██████ ██████ █████ ██████ █████ █████ 20 | ░░░███ ███░░░░░███░░███░░░░░█░░██████ ██████ ░░███ ░░██████ ░░███ ░░███ 21 | ░░░███ ███ ░░░ ░███ █ ░ ░███░█████░███ ░███ ░███░███ ░███ ░███ 22 | ░░░███ ░███ ░██████ ░███░░███ ░███ ░███ ░███░░███░███ ░███ 23 | ███░ ░███ █████ ░███░░█ ░███ ░░░ ░███ ░███ ░███ ░░██████ ░███ 24 | ███░ ░░███ ░░███ ░███ ░ █ ░███ ░███ ░███ ░███ ░░█████ ░███ 25 | ███░ ░░█████████ ██████████ █████ █████ █████ █████ ░░█████ █████ 26 | ░░░ ░░░░░░░░░ ░░░░░░░░░░ ░░░░░ ░░░░░ ░░░░░ ░░░░░ ░░░░░ ░░░░░ 27 | `; 28 | -------------------------------------------------------------------------------- /scripts/bind_package_version.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright 2025 Google LLC 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | import fs from 'node:fs'; 8 | import path from 'node:path'; 9 | 10 | // Assuming script is run from a package directory (e.g., packages/cli) 11 | const packageDir = process.cwd(); 12 | const rootDir = path.join(packageDir, '..', '..'); // Go up two directories to find the repo root 13 | 14 | function getRepoVersion() { 15 | // Read root package.json 16 | const rootPackageJsonPath = path.join(rootDir, 'package.json'); 17 | const rootPackage = JSON.parse(fs.readFileSync(rootPackageJsonPath, 'utf8')); 18 | return rootPackage.version; // This version is now expected to be the full version string 19 | } 20 | 21 | const newVersion = getRepoVersion(); 22 | console.log(`Setting package version to: ${newVersion}`); 23 | 24 | const packageJsonPath = path.join(packageDir, 'package.json'); 25 | 26 | if (fs.existsSync(packageJsonPath)) { 27 | console.log(`Updating version for ${packageJsonPath}`); 28 | const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8')); 29 | packageJson.version = newVersion; 30 | fs.writeFileSync( 31 | packageJsonPath, 32 | JSON.stringify(packageJson, null, 2) + '\n', 33 | 'utf8', 34 | ); 35 | } else { 36 | console.error( 37 | `Error: package.json not found in the current directory: ${packageJsonPath}`, 38 | ); 39 | process.exit(1); 40 | } 41 | 42 | console.log('Done.'); 43 | -------------------------------------------------------------------------------- /packages/cli/src/ui/components/Tips.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright 2025 Google LLC 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | import React from 'react'; 8 | import { Box, Text } from 'ink'; 9 | import { Colors } from '../colors.js'; 10 | import { type Config } from '@google/gemini-cli-core'; 11 | 12 | interface TipsProps { 13 | config: Config; 14 | } 15 | 16 | export const Tips: React.FC = ({ config }) => { 17 | const geminiMdFileCount = config.getGeminiMdFileCount(); 18 | return ( 19 | 20 | Tips for getting started: 21 | 22 | 1. Ask questions, edit files, or run commands. 23 | 24 | 25 | 2. Be specific for the best results. 26 | 27 | {geminiMdFileCount === 0 && ( 28 | 29 | 3. Create{' '} 30 | 31 | GEMINI.md 32 | {' '} 33 | files to customize your interactions with Gemini. 34 | 35 | )} 36 | 37 | {geminiMdFileCount === 0 ? '4.' : '3.'}{' '} 38 | 39 | /help 40 | {' '} 41 | for more information. 42 | 43 | 44 | ); 45 | }; 46 | -------------------------------------------------------------------------------- /packages/core/src/providers/geminiProvider.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright 2025 Google LLC 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | import { 8 | CountTokensRequest, 9 | CountTokensResponse, 10 | EmbedContentRequest, 11 | EmbedContentResponse, 12 | GenerateContentRequest, 13 | GenerateContentResult, 14 | GenerativeModel, 15 | } from '@google/generative-ai'; 16 | import { LlmProvider } from '../core/llmProvider'; 17 | import { Config } from '../config/config'; 18 | import { createContentGenerator } from '../core/contentGenerator'; 19 | 20 | export class GeminiProvider implements LlmProvider { 21 | private model: GenerativeModel; 22 | 23 | constructor(private config: Config) { 24 | const contentGenerator = createContentGenerator( 25 | this.config.getContentGeneratorConfig(), 26 | ); 27 | this.model = contentGenerator; 28 | } 29 | 30 | async generateContent( 31 | request: GenerateContentRequest, 32 | ): Promise { 33 | const result = await this.model.generateContent(request); 34 | return result; 35 | } 36 | 37 | async countTokens( 38 | request: CountTokensRequest, 39 | ): Promise { 40 | const result = await this.model.countTokens(request); 41 | return result; 42 | } 43 | 44 | async embedContent( 45 | request: EmbedContentRequest, 46 | ): Promise { 47 | const result = await this.model.embedContent(request); 48 | return result; 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /docs/cli/index.md: -------------------------------------------------------------------------------- 1 | # Gemini CLI 2 | 3 | Within Gemini CLI, `packages/cli` is the frontend for users to send and receive prompts with the Gemini AI model and its associated tools. For a general overview of Gemini CLI, see the [main documentation page](../index.md). 4 | 5 | ## Navigating this section 6 | 7 | - **[Authentication](./authentication.md):** A guide to setting up authentication with Google's AI services. 8 | - **[Commands](./commands.md):** A reference for Gemini CLI commands (e.g., `/help`, `/tools`, `/theme`). 9 | - **[Configuration](./configuration.md):** A guide to tailoring Gemini CLI behavior using configuration files. 10 | - **[Token Caching](./token-caching.md):** Optimize API costs through token caching. 11 | - **[Themes](./themes.md)**: A guide to customizing the CLI's appearance with different themes. 12 | - **[Tutorials](tutorials.md)**: A tutorial showing how to use Gemini CLI to automate a development task. 13 | 14 | ## Non-interactive mode 15 | 16 | Gemini CLI can be run in a non-interactive mode, which is useful for scripting and automation. In this mode, you pipe input to the CLI, it executes the command, and then it exits. 17 | 18 | The following example pipes a command to Gemini CLI from your terminal: 19 | 20 | ```bash 21 | echo "What is fine tuning?" | gemini 22 | ``` 23 | 24 | Gemini CLI executes the command and prints the output to your terminal. Note that you can achieve the same behavior by using the `--prompt` or `-p` flag. For example: 25 | 26 | ```bash 27 | gemini -p "What is fine tuning?" 28 | ``` 29 | -------------------------------------------------------------------------------- /packages/cli/src/ui/components/messages/CompressionMessage.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright 2025 Google LLC 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | import React from 'react'; 8 | import { Box, Text } from 'ink'; 9 | import { CompressionProps } from '../../types.js'; 10 | import Spinner from 'ink-spinner'; 11 | import { Colors } from '../../colors.js'; 12 | 13 | export interface CompressionDisplayProps { 14 | compression: CompressionProps; 15 | } 16 | 17 | /* 18 | * Compression messages appear when the /compress command is ran, and show a loading spinner 19 | * while compression is in progress, followed up by some compression stats. 20 | */ 21 | export const CompressionMessage: React.FC = ({ 22 | compression, 23 | }) => { 24 | const text = compression.isPending 25 | ? 'Compressing chat history' 26 | : `Chat history compressed from ${compression.originalTokenCount ?? 'unknown'}` + 27 | ` to ${compression.newTokenCount ?? 'unknown'} tokens.`; 28 | 29 | return ( 30 | 31 | 32 | {compression.isPending ? ( 33 | 34 | ) : ( 35 | 36 | )} 37 | 38 | 39 | 44 | {text} 45 | 46 | 47 | 48 | ); 49 | }; 50 | -------------------------------------------------------------------------------- /packages/core/src/utils/fetch.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright 2025 Google LLC 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | import { getErrorMessage, isNodeError } from './errors.js'; 8 | import { URL } from 'url'; 9 | 10 | const PRIVATE_IP_RANGES = [ 11 | /^10\./, 12 | /^127\./, 13 | /^172\.(1[6-9]|2[0-9]|3[0-1])\./, 14 | /^192\.168\./, 15 | /^::1$/, 16 | /^fc00:/, 17 | /^fe80:/, 18 | ]; 19 | 20 | export class FetchError extends Error { 21 | constructor( 22 | message: string, 23 | public code?: string, 24 | ) { 25 | super(message); 26 | this.name = 'FetchError'; 27 | } 28 | } 29 | 30 | export function isPrivateIp(url: string): boolean { 31 | try { 32 | const hostname = new URL(url).hostname; 33 | return PRIVATE_IP_RANGES.some((range) => range.test(hostname)); 34 | } catch (_e) { 35 | return false; 36 | } 37 | } 38 | 39 | export async function fetchWithTimeout( 40 | url: string, 41 | timeout: number, 42 | ): Promise { 43 | const controller = new AbortController(); 44 | const timeoutId = setTimeout(() => controller.abort(), timeout); 45 | 46 | try { 47 | const response = await fetch(url, { signal: controller.signal }); 48 | return response; 49 | } catch (error) { 50 | if (isNodeError(error) && error.code === 'ABORT_ERR') { 51 | throw new FetchError(`Request timed out after ${timeout}ms`, 'ETIMEDOUT'); 52 | } 53 | throw new FetchError(getErrorMessage(error)); 54 | } finally { 55 | clearTimeout(timeoutId); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM docker.io/library/node:20-slim 2 | 3 | ARG SANDBOX_NAME="gemini-cli-sandbox" 4 | ARG CLI_VERSION_ARG 5 | ENV SANDBOX="$SANDBOX_NAME" 6 | ENV CLI_VERSION=$CLI_VERSION_ARG 7 | 8 | # install minimal set of packages, then clean up 9 | RUN apt-get update && apt-get install -y --no-install-recommends \ 10 | python3 \ 11 | make \ 12 | g++ \ 13 | man-db \ 14 | curl \ 15 | dnsutils \ 16 | less \ 17 | jq \ 18 | bc \ 19 | gh \ 20 | git \ 21 | unzip \ 22 | rsync \ 23 | ripgrep \ 24 | procps \ 25 | psmisc \ 26 | lsof \ 27 | socat \ 28 | ca-certificates \ 29 | && apt-get clean \ 30 | && rm -rf /var/lib/apt/lists/* 31 | 32 | # set up npm global package folder under /usr/local/share 33 | # give it to non-root user node, already set up in base image 34 | RUN mkdir -p /usr/local/share/npm-global \ 35 | && chown -R node:node /usr/local/share/npm-global 36 | ENV NPM_CONFIG_PREFIX=/usr/local/share/npm-global 37 | ENV PATH=$PATH:/usr/local/share/npm-global/bin 38 | 39 | # switch to non-root user node 40 | USER node 41 | 42 | # install gemini-cli and clean up 43 | COPY packages/cli/dist/google-gemini-cli-*.tgz /usr/local/share/npm-global/gemini-cli.tgz 44 | COPY packages/core/dist/google-gemini-cli-core-*.tgz /usr/local/share/npm-global/gemini-core.tgz 45 | RUN npm install -g /usr/local/share/npm-global/gemini-cli.tgz /usr/local/share/npm-global/gemini-core.tgz \ 46 | && npm cache clean --force \ 47 | && rm -f /usr/local/share/npm-global/gemini-{cli,core}.tgz 48 | 49 | # default entrypoint when none specified 50 | CMD ["gemini"] 51 | -------------------------------------------------------------------------------- /packages/cli/src/ui/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 { ColorsTheme } from './themes/theme.js'; 9 | 10 | export const Colors: ColorsTheme = { 11 | get type() { 12 | return themeManager.getActiveTheme().colors.type; 13 | }, 14 | get Foreground() { 15 | return themeManager.getActiveTheme().colors.Foreground; 16 | }, 17 | get Background() { 18 | return themeManager.getActiveTheme().colors.Background; 19 | }, 20 | get LightBlue() { 21 | return themeManager.getActiveTheme().colors.LightBlue; 22 | }, 23 | get AccentBlue() { 24 | return themeManager.getActiveTheme().colors.AccentBlue; 25 | }, 26 | get AccentPurple() { 27 | return themeManager.getActiveTheme().colors.AccentPurple; 28 | }, 29 | get AccentCyan() { 30 | return themeManager.getActiveTheme().colors.AccentCyan; 31 | }, 32 | get AccentGreen() { 33 | return themeManager.getActiveTheme().colors.AccentGreen; 34 | }, 35 | get AccentYellow() { 36 | return themeManager.getActiveTheme().colors.AccentYellow; 37 | }, 38 | get AccentRed() { 39 | return themeManager.getActiveTheme().colors.AccentRed; 40 | }, 41 | get Comment() { 42 | return themeManager.getActiveTheme().colors.Comment; 43 | }, 44 | get Gray() { 45 | return themeManager.getActiveTheme().colors.Gray; 46 | }, 47 | get GradientColors() { 48 | return themeManager.getActiveTheme().colors.GradientColors; 49 | }, 50 | }; 51 | -------------------------------------------------------------------------------- /scripts/setup-dev.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 'child_process'; 21 | 22 | try { 23 | execSync('command -v npm', { stdio: 'ignore' }); 24 | } catch { 25 | console.log('npm not found. Installing npm via nvm...'); 26 | try { 27 | execSync( 28 | 'curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.3/install.sh | bash', 29 | { stdio: 'inherit' }, 30 | ); 31 | const nvmsh = `\\. "$HOME/.nvm/nvm.sh"`; 32 | execSync(`${nvmsh} && nvm install 22`, { stdio: 'inherit' }); 33 | execSync(`${nvmsh} && node -v`, { stdio: 'inherit' }); 34 | execSync(`${nvmsh} && nvm current`, { stdio: 'inherit' }); 35 | execSync(`${nvmsh} && npm -v`, { stdio: 'inherit' }); 36 | } catch { 37 | console.error('Failed to install nvm or node.'); 38 | process.exit(1); 39 | } 40 | } 41 | 42 | console.log('Development environment setup complete.'); 43 | -------------------------------------------------------------------------------- /packages/cli/src/ui/hooks/useAutoAcceptIndicator.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 { useInput } from 'ink'; 9 | import { ApprovalMode, type Config } from '@google/gemini-cli-core'; 10 | 11 | export interface UseAutoAcceptIndicatorArgs { 12 | config: Config; 13 | } 14 | 15 | export function useAutoAcceptIndicator({ 16 | config, 17 | }: UseAutoAcceptIndicatorArgs): ApprovalMode { 18 | const currentConfigValue = config.getApprovalMode(); 19 | const [showAutoAcceptIndicator, setShowAutoAcceptIndicator] = 20 | useState(currentConfigValue); 21 | 22 | useEffect(() => { 23 | setShowAutoAcceptIndicator(currentConfigValue); 24 | }, [currentConfigValue]); 25 | 26 | useInput((input, key) => { 27 | let nextApprovalMode: ApprovalMode | undefined; 28 | 29 | if (key.ctrl && input === 'y') { 30 | nextApprovalMode = 31 | config.getApprovalMode() === ApprovalMode.YOLO 32 | ? ApprovalMode.DEFAULT 33 | : ApprovalMode.YOLO; 34 | } else if (key.tab && key.shift) { 35 | nextApprovalMode = 36 | config.getApprovalMode() === ApprovalMode.AUTO_EDIT 37 | ? ApprovalMode.DEFAULT 38 | : ApprovalMode.AUTO_EDIT; 39 | } 40 | 41 | if (nextApprovalMode) { 42 | config.setApprovalMode(nextApprovalMode); 43 | // Update local state immediately for responsiveness 44 | setShowAutoAcceptIndicator(nextApprovalMode); 45 | } 46 | }); 47 | 48 | return showAutoAcceptIndicator; 49 | } 50 | -------------------------------------------------------------------------------- /packages/core/src/core/llmProvider.ts: -------------------------------------------------------------------------------- 1 | import { 2 | CountTokensRequest, 3 | CountTokensResponse, 4 | EmbedContentRequest, 5 | EmbedContentResponse, 6 | GenerateContentRequest, 7 | GenerateContentResult, 8 | } from '@google/generative-ai'; 9 | 10 | /** 11 | * Defines the standard interface for a Large Language Model (LLM) provider. 12 | * All provider implementations (e.g., Gemini, OpenAI, Claude) must adhere to this contract. 13 | * It uses the Google Generative AI types as the canonical data structure to minimize 14 | * changes across the existing system. 15 | */ 16 | export interface LlmProvider { 17 | /** 18 | * Sends a content generation request to the LLM provider. 19 | * 20 | * @param request The content generation request, conforming to the Gemini API's structure. 21 | * @returns A promise that resolves with the content generation result, also conforming 22 | * to the Gemini API's structure. 23 | */ 24 | generateContent( 25 | request: GenerateContentRequest, 26 | ): Promise; 27 | 28 | /** 29 | * Counts the number of tokens in a given request. 30 | * @param request The request to count tokens for. 31 | * @returns A promise that resolves with the token count. 32 | */ 33 | countTokens(request: CountTokensRequest): Promise; 34 | 35 | /** 36 | * Generates an embedding for a given request. 37 | * @param request The request to generate an embedding for. 38 | * @returns A promise that resolves with the embedding. 39 | */ 40 | embedContent(request: EmbedContentRequest): Promise; 41 | } 42 | -------------------------------------------------------------------------------- /packages/cli/src/ui/components/SessionSummaryDisplay.test.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright 2025 Google LLC 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | import { render } from 'ink-testing-library'; 8 | import { describe, it, expect } from 'vitest'; 9 | import { SessionSummaryDisplay } from './SessionSummaryDisplay.js'; 10 | import { type CumulativeStats } from '../contexts/SessionContext.js'; 11 | 12 | describe('', () => { 13 | const mockStats: CumulativeStats = { 14 | turnCount: 10, 15 | promptTokenCount: 1000, 16 | candidatesTokenCount: 2000, 17 | totalTokenCount: 3500, 18 | cachedContentTokenCount: 500, 19 | toolUsePromptTokenCount: 200, 20 | thoughtsTokenCount: 300, 21 | apiTimeMs: 50234, 22 | }; 23 | 24 | const mockDuration = '1h 23m 45s'; 25 | 26 | it('renders correctly with given stats and duration', () => { 27 | const { lastFrame } = render( 28 | , 29 | ); 30 | 31 | expect(lastFrame()).toMatchSnapshot(); 32 | }); 33 | 34 | it('renders zero state correctly', () => { 35 | const zeroStats: CumulativeStats = { 36 | turnCount: 0, 37 | promptTokenCount: 0, 38 | candidatesTokenCount: 0, 39 | totalTokenCount: 0, 40 | cachedContentTokenCount: 0, 41 | toolUsePromptTokenCount: 0, 42 | thoughtsTokenCount: 0, 43 | apiTimeMs: 0, 44 | }; 45 | 46 | const { lastFrame } = render( 47 | , 48 | ); 49 | 50 | expect(lastFrame()).toMatchSnapshot(); 51 | }); 52 | }); 53 | -------------------------------------------------------------------------------- /packages/core/src/utils/user_id.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright 2025 Google LLC 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | import * as os from 'os'; 8 | import * as fs from 'fs'; 9 | import * as path from 'path'; 10 | import { randomUUID } from 'crypto'; 11 | import { GEMINI_DIR } from './paths.js'; 12 | 13 | const homeDir = os.homedir() ?? ''; 14 | const geminiDir = path.join(homeDir, GEMINI_DIR); 15 | const userIdFile = path.join(geminiDir, 'user_id'); 16 | 17 | function ensureGeminiDirExists() { 18 | if (!fs.existsSync(geminiDir)) { 19 | fs.mkdirSync(geminiDir, { recursive: true }); 20 | } 21 | } 22 | 23 | function readUserIdFromFile(): string | null { 24 | if (fs.existsSync(userIdFile)) { 25 | const userId = fs.readFileSync(userIdFile, 'utf-8').trim(); 26 | return userId || null; 27 | } 28 | return null; 29 | } 30 | 31 | function writeUserIdToFile(userId: string) { 32 | fs.writeFileSync(userIdFile, userId, 'utf-8'); 33 | } 34 | 35 | /** 36 | * Retrieves the persistent user ID from a file, creating it if it doesn't exist. 37 | * This ID is used for unique user tracking. 38 | * @returns A UUID string for the user. 39 | */ 40 | export function getPersistentUserId(): string { 41 | try { 42 | ensureGeminiDirExists(); 43 | let userId = readUserIdFromFile(); 44 | 45 | if (!userId) { 46 | userId = randomUUID(); 47 | writeUserIdToFile(userId); 48 | } 49 | 50 | return userId; 51 | } catch (error) { 52 | console.error( 53 | 'Error accessing persistent user ID file, generating ephemeral ID:', 54 | error, 55 | ); 56 | return '123456789'; 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /scripts/bind_package_dependencies.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright 2025 Google LLC 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | import fs from 'node:fs'; 8 | import path from 'node:path'; 9 | import _ from 'lodash'; 10 | 11 | function bindPackageDependencies() { 12 | const scriptDir = process.cwd(); 13 | const currentPkgJsonPath = path.join(scriptDir, 'package.json'); 14 | const currentPkg = JSON.parse(fs.readFileSync(currentPkgJsonPath)); 15 | // assume packages are all under //packages/ 16 | const packagesDir = path.join(path.dirname(scriptDir)); 17 | 18 | const geminiCodePkgs = fs 19 | .readdirSync(packagesDir) 20 | .filter( 21 | (name) => 22 | fs.statSync(path.join(packagesDir, name)).isDirectory() && 23 | fs.existsSync(path.join(packagesDir, name, 'package.json')), 24 | ) 25 | .map((packageDirname) => { 26 | const packageJsonPath = path.join( 27 | packagesDir, 28 | packageDirname, 29 | 'package.json', 30 | ); 31 | return JSON.parse(fs.readFileSync(packageJsonPath, 'utf8')); 32 | }) 33 | .reduce((pkgs, pkg) => ({ ...pkgs, [pkg.name]: pkg }), {}); 34 | currentPkg.dependencies = _.mapValues( 35 | currentPkg.dependencies, 36 | (value, key) => { 37 | if (geminiCodePkgs[key]) { 38 | console.log( 39 | `Package ${currentPkg.name} has a dependency on ${key}. Updating dependent version.`, 40 | ); 41 | return geminiCodePkgs[key].version; 42 | } 43 | return value; 44 | }, 45 | ); 46 | const updatedPkgJson = JSON.stringify(currentPkg, null, 2) + '\n'; 47 | fs.writeFileSync(currentPkgJsonPath, updatedPkgJson); 48 | } 49 | 50 | bindPackageDependencies(); 51 | -------------------------------------------------------------------------------- /docs/tools/memory.md: -------------------------------------------------------------------------------- 1 | # Memory Tool (`save_memory`) 2 | 3 | This document describes the `save_memory` tool for the Gemini CLI. 4 | 5 | ## Description 6 | 7 | Use `save_memory` to save and recall information across your Gemini CLI sessions. With `save_memory`, you can direct the CLI to remember key details across sessions, providing personalized and directed assistance. 8 | 9 | ### Arguments 10 | 11 | `save_memory` takes one argument: 12 | 13 | - `fact` (string, required): The specific fact or piece of information to remember. This should be a clear, self-contained statement written in natural language. 14 | 15 | ## How to use `save_memory` with the Gemini CLI 16 | 17 | The tool appends the provided `fact` to a special `GEMINI.md` file located in the user's home directory (`~/.gemini/GEMINI.md`). This file can be configured to have a different name. 18 | 19 | Once added, the facts are stored under a `## Gemini Added Memories` section. This file is loaded as context in subsequent sessions, allowing the CLI to recall the saved information. 20 | 21 | Usage: 22 | 23 | ``` 24 | save_memory(fact="Your fact here.") 25 | ``` 26 | 27 | ### `save_memory` examples 28 | 29 | Remember a user preference: 30 | 31 | ``` 32 | save_memory(fact="My preferred programming language is Python.") 33 | ``` 34 | 35 | Store a project-specific detail: 36 | 37 | ``` 38 | save_memory(fact="The project I'm currently working on is called 'gemini-cli'.") 39 | ``` 40 | 41 | ## Important notes 42 | 43 | - **General usage:** This tool should be used for concise, important facts. It is not intended for storing large amounts of data or conversational history. 44 | - **Memory file:** The memory file is a plain text Markdown file, so you can view and edit it manually if needed. 45 | -------------------------------------------------------------------------------- /packages/cli/src/config/auth.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright 2025 Google LLC 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | import { AuthType } from '@google/gemini-cli-core'; 8 | import { loadEnvironment } from './config.js'; 9 | 10 | export const validateAuthMethod = (authMethod: string): string | null => { 11 | loadEnvironment(); 12 | if (authMethod === AuthType.LOGIN_WITH_GOOGLE_PERSONAL) { 13 | return null; 14 | } 15 | 16 | if (authMethod === AuthType.LOGIN_WITH_GOOGLE_ENTERPRISE) { 17 | if (!process.env.GOOGLE_CLOUD_PROJECT) { 18 | return 'GOOGLE_CLOUD_PROJECT environment variable not found. Add that to your .env and try again, no reload needed!'; 19 | } 20 | return null; 21 | } 22 | 23 | if (authMethod === AuthType.USE_GEMINI) { 24 | if (!process.env.GEMINI_API_KEY) { 25 | return 'GEMINI_API_KEY environment variable not found. Add that to your .env and try again, no reload needed!'; 26 | } 27 | return null; 28 | } 29 | 30 | if (authMethod === AuthType.USE_VERTEX_AI) { 31 | const hasVertexProjectLocationConfig = 32 | !!process.env.GOOGLE_CLOUD_PROJECT && !!process.env.GOOGLE_CLOUD_LOCATION; 33 | const hasGoogleApiKey = !!process.env.GOOGLE_API_KEY; 34 | if (!hasVertexProjectLocationConfig && !hasGoogleApiKey) { 35 | return ( 36 | 'Must specify GOOGLE_GENAI_USE_VERTEXAI=true and either:\n' + 37 | '• GOOGLE_CLOUD_PROJECT and GOOGLE_CLOUD_LOCATION environment variables.\n' + 38 | '• GOOGLE_API_KEY environment variable (if using express mode).\n' + 39 | 'Update your .env and try again, no reload needed!' 40 | ); 41 | } 42 | return null; 43 | } 44 | 45 | return 'Invalid auth method selected.'; 46 | }; 47 | -------------------------------------------------------------------------------- /packages/cli/src/utils/startupWarnings.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright 2025 Google LLC 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | import fs from 'fs/promises'; 8 | import os from 'os'; 9 | import { join as pathJoin } from 'node:path'; 10 | import { getErrorMessage } from '@google/gemini-cli-core'; 11 | 12 | const warningsFilePath = pathJoin(os.tmpdir(), 'gemini-cli-warnings.txt'); 13 | 14 | export async function getStartupWarnings(): Promise { 15 | try { 16 | await fs.access(warningsFilePath); // Check if file exists 17 | const warningsContent = await fs.readFile(warningsFilePath, 'utf-8'); 18 | const warnings = warningsContent 19 | .split('\n') 20 | .filter((line) => line.trim() !== ''); 21 | try { 22 | await fs.unlink(warningsFilePath); 23 | } catch { 24 | warnings.push('Warning: Could not delete temporary warnings file.'); 25 | } 26 | return warnings; 27 | } catch (err: unknown) { 28 | // If fs.access throws, it means the file doesn't exist or is not accessible. 29 | // This is not an error in the context of fetching warnings, so return empty. 30 | // Only return an error message if it's not a "file not found" type error. 31 | // However, the original logic returned an error message for any fs.existsSync failure. 32 | // To maintain closer parity while making it async, we'll check the error code. 33 | // ENOENT is "Error NO ENTry" (file not found). 34 | if (err instanceof Error && 'code' in err && err.code === 'ENOENT') { 35 | return []; // File not found, no warnings to return. 36 | } 37 | // For other errors (permissions, etc.), return the error message. 38 | return [`Error checking/reading warnings file: ${getErrorMessage(err)}`]; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # Makefile for gemini-cli 2 | 3 | .PHONY: help install build build-sandbox build-all test lint format preflight clean start debug release run-npx create-alias 4 | 5 | help: 6 | @echo "Makefile for gemini-cli" 7 | @echo "" 8 | @echo "Usage:" 9 | @echo " make install - Install npm dependencies" 10 | @echo " make build - Build the entire project" 11 | @echo " make build-sandbox - Build the sandbox container" 12 | @echo " make build-all - Build the project and the sandbox" 13 | @echo " make test - Run the test suite" 14 | @echo " make lint - Lint the code" 15 | @echo " make format - Format the code" 16 | @echo " make preflight - Run formatting, linting, and tests" 17 | @echo " make clean - Remove generated files" 18 | @echo " make start - Start the Gemini CLI" 19 | @echo " make debug - Start the Gemini CLI in debug mode" 20 | @echo " make release - Publish a new release" 21 | @echo " make run-npx - Run the CLI using npx (for testing the published package)" 22 | @echo " make create-alias - Create a 'gemini' alias for your shell" 23 | 24 | install: 25 | npm install 26 | 27 | build: 28 | npm run build 29 | 30 | build-sandbox: 31 | npm run build:sandbox 32 | 33 | build-all: 34 | npm run build:all 35 | 36 | test: 37 | npm run test 38 | 39 | lint: 40 | npm run lint 41 | 42 | format: 43 | npm run format 44 | 45 | preflight: 46 | npm run preflight 47 | 48 | clean: 49 | npm run clean 50 | 51 | start: 52 | npm run start 53 | 54 | debug: 55 | npm run debug 56 | 57 | release: 58 | npm run publish:release 59 | 60 | run-npx: 61 | npx https://github.com/google-gemini/gemini-cli 62 | 63 | create-alias: 64 | scripts/create_alias.sh 65 | -------------------------------------------------------------------------------- /scripts/publish-sandbox.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 'child_process'; 21 | 22 | const { 23 | SANDBOX_IMAGE_REGISTRY, 24 | SANDBOX_IMAGE_NAME, 25 | npm_package_version, 26 | DOCKER_DRY_RUN, 27 | } = process.env; 28 | 29 | if (!SANDBOX_IMAGE_REGISTRY) { 30 | console.error( 31 | 'Error: SANDBOX_IMAGE_REGISTRY environment variable is not set.', 32 | ); 33 | process.exit(1); 34 | } 35 | 36 | if (!SANDBOX_IMAGE_NAME) { 37 | console.error('Error: SANDBOX_IMAGE_NAME environment variable is not set.'); 38 | process.exit(1); 39 | } 40 | 41 | if (!npm_package_version) { 42 | console.error( 43 | 'Error: npm_package_version environment variable is not set (should be run via npm).', 44 | ); 45 | process.exit(1); 46 | } 47 | 48 | const imageUri = `${SANDBOX_IMAGE_REGISTRY}/${SANDBOX_IMAGE_NAME}:${npm_package_version}`; 49 | 50 | if (DOCKER_DRY_RUN) { 51 | console.log(`DRY RUN: Would execute: docker push "${imageUri}"`); 52 | } else { 53 | console.log(`Executing: docker push "${imageUri}"`); 54 | execSync(`docker push "${imageUri}"`, { stdio: 'inherit' }); 55 | } 56 | -------------------------------------------------------------------------------- /packages/core/src/core/contentGenerator.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 { createContentGenerator, AuthType } from './contentGenerator.js'; 9 | import { createCodeAssistContentGenerator } from '../code_assist/codeAssist.js'; 10 | import { GoogleGenAI } from '@google/genai'; 11 | 12 | vi.mock('../code_assist/codeAssist.js'); 13 | vi.mock('@google/genai'); 14 | 15 | describe('contentGenerator', () => { 16 | it('should create a CodeAssistContentGenerator', async () => { 17 | const mockGenerator = {} as unknown; 18 | vi.mocked(createCodeAssistContentGenerator).mockResolvedValue( 19 | mockGenerator as never, 20 | ); 21 | const generator = await createContentGenerator({ 22 | model: 'test-model', 23 | authType: AuthType.LOGIN_WITH_GOOGLE_PERSONAL, 24 | }); 25 | expect(createCodeAssistContentGenerator).toHaveBeenCalled(); 26 | expect(generator).toBe(mockGenerator); 27 | }); 28 | 29 | it('should create a GoogleGenAI content generator', async () => { 30 | const mockGenerator = { 31 | models: {}, 32 | } as unknown; 33 | vi.mocked(GoogleGenAI).mockImplementation(() => mockGenerator as never); 34 | const generator = await createContentGenerator({ 35 | model: 'test-model', 36 | apiKey: 'test-api-key', 37 | authType: AuthType.USE_GEMINI, 38 | }); 39 | expect(GoogleGenAI).toHaveBeenCalledWith({ 40 | apiKey: 'test-api-key', 41 | vertexai: undefined, 42 | httpOptions: { 43 | headers: { 44 | 'User-Agent': expect.any(String), 45 | }, 46 | }, 47 | }); 48 | expect(generator).toBe((mockGenerator as GoogleGenAI).models); 49 | }); 50 | }); 51 | -------------------------------------------------------------------------------- /packages/cli/src/ui/editors/editorSettingsManager.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright 2025 Google LLC 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | import { 8 | allowEditorTypeInSandbox, 9 | checkHasEditorType, 10 | type EditorType, 11 | } from '@google/gemini-cli-core'; 12 | 13 | export interface EditorDisplay { 14 | name: string; 15 | type: EditorType | 'not_set'; 16 | disabled: boolean; 17 | } 18 | 19 | export const EDITOR_DISPLAY_NAMES: Record = { 20 | zed: 'Zed', 21 | vscode: 'VS Code', 22 | windsurf: 'Windsurf', 23 | cursor: 'Cursor', 24 | vim: 'Vim', 25 | }; 26 | 27 | class EditorSettingsManager { 28 | private readonly availableEditors: EditorDisplay[]; 29 | 30 | constructor() { 31 | const editorTypes: EditorType[] = [ 32 | 'zed', 33 | 'vscode', 34 | 'windsurf', 35 | 'cursor', 36 | 'vim', 37 | ]; 38 | this.availableEditors = [ 39 | { 40 | name: 'None', 41 | type: 'not_set', 42 | disabled: false, 43 | }, 44 | ...editorTypes.map((type) => { 45 | const hasEditor = checkHasEditorType(type); 46 | const isAllowedInSandbox = allowEditorTypeInSandbox(type); 47 | 48 | let labelSuffix = !isAllowedInSandbox 49 | ? ' (Not available in sandbox)' 50 | : ''; 51 | labelSuffix = !hasEditor ? ' (Not installed)' : labelSuffix; 52 | 53 | return { 54 | name: EDITOR_DISPLAY_NAMES[type] + labelSuffix, 55 | type, 56 | disabled: !hasEditor || !isAllowedInSandbox, 57 | }; 58 | }), 59 | ]; 60 | } 61 | 62 | getAvailableEditorDisplays(): EditorDisplay[] { 63 | return this.availableEditors; 64 | } 65 | } 66 | 67 | export const editorSettingsManager = new EditorSettingsManager(); 68 | -------------------------------------------------------------------------------- /scripts/copy_bundle_assets.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 { copyFileSync, existsSync, mkdirSync } from 'fs'; 21 | import { dirname, join, basename } from 'path'; 22 | import { fileURLToPath } from 'url'; 23 | import { glob } from 'glob'; 24 | 25 | const __dirname = dirname(fileURLToPath(import.meta.url)); 26 | const root = join(__dirname, '..'); 27 | const bundleDir = join(root, 'bundle'); 28 | 29 | // Create the bundle directory if it doesn't exist 30 | if (!existsSync(bundleDir)) { 31 | mkdirSync(bundleDir); 32 | } 33 | 34 | // Copy specific shell files to the root of the bundle directory 35 | copyFileSync( 36 | join(root, 'packages/core/src/tools/shell.md'), 37 | join(bundleDir, 'shell.md'), 38 | ); 39 | copyFileSync( 40 | join(root, 'packages/core/src/tools/shell.json'), 41 | join(bundleDir, 'shell.json'), 42 | ); 43 | 44 | // Find and copy all .sb files from packages to the root of the bundle directory 45 | const sbFiles = glob.sync('packages/**/*.sb', { cwd: root }); 46 | for (const file of sbFiles) { 47 | copyFileSync(join(root, file), join(bundleDir, basename(file))); 48 | } 49 | 50 | console.log('Assets copied to bundle/'); 51 | -------------------------------------------------------------------------------- /packages/cli/src/ui/components/__snapshots__/SessionSummaryDisplay.test.tsx.snap: -------------------------------------------------------------------------------- 1 | // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html 2 | 3 | exports[` > renders correctly with given stats and duration 1`] = ` 4 | "╭─────────────────────────────────────╮ 5 | │ │ 6 | │ Agent powering down. Goodbye! │ 7 | │ │ 8 | │ │ 9 | │ Cumulative Stats (10 Turns) │ 10 | │ │ 11 | │ Input Tokens 1,000 │ 12 | │ Output Tokens 2,000 │ 13 | │ Tool Use Tokens 200 │ 14 | │ Thoughts Tokens 300 │ 15 | │ Cached Tokens 500 (14.3%) │ 16 | │ ───────────────────────────────── │ 17 | │ Total Tokens 3,500 │ 18 | │ │ 19 | │ Total duration (API) 50.2s │ 20 | │ Total duration (wall) 1h 23m 45s │ 21 | │ │ 22 | ╰─────────────────────────────────────╯" 23 | `; 24 | 25 | exports[` > renders zero state correctly 1`] = ` 26 | "╭─────────────────────────────────╮ 27 | │ │ 28 | │ Agent powering down. Goodbye! │ 29 | │ │ 30 | │ │ 31 | │ Cumulative Stats (0 Turns) │ 32 | │ │ 33 | │ Input Tokens 0 │ 34 | │ Output Tokens 0 │ 35 | │ Thoughts Tokens 0 │ 36 | │ ────────────────────────── │ 37 | │ Total Tokens 0 │ 38 | │ │ 39 | │ Total duration (API) 0s │ 40 | │ Total duration (wall) 0s │ 41 | │ │ 42 | ╰─────────────────────────────────╯" 43 | `; 44 | -------------------------------------------------------------------------------- /packages/cli/src/ui/utils/formatters.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright 2025 Google LLC 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | export const formatMemoryUsage = (bytes: number): string => { 8 | const gb = bytes / (1024 * 1024 * 1024); 9 | if (bytes < 1024 * 1024) { 10 | return `${(bytes / 1024).toFixed(1)} KB`; 11 | } 12 | if (bytes < 1024 * 1024 * 1024) { 13 | return `${(bytes / (1024 * 1024)).toFixed(1)} MB`; 14 | } 15 | return `${gb.toFixed(2)} GB`; 16 | }; 17 | 18 | /** 19 | * Formats a duration in milliseconds into a concise, human-readable string (e.g., "1h 5s"). 20 | * It omits any time units that are zero. 21 | * @param milliseconds The duration in milliseconds. 22 | * @returns A formatted string representing the duration. 23 | */ 24 | export const formatDuration = (milliseconds: number): string => { 25 | if (milliseconds <= 0) { 26 | return '0s'; 27 | } 28 | 29 | if (milliseconds < 1000) { 30 | return `${milliseconds}ms`; 31 | } 32 | 33 | const totalSeconds = milliseconds / 1000; 34 | 35 | if (totalSeconds < 60) { 36 | return `${totalSeconds.toFixed(1)}s`; 37 | } 38 | 39 | const hours = Math.floor(totalSeconds / 3600); 40 | const minutes = Math.floor((totalSeconds % 3600) / 60); 41 | const seconds = Math.floor(totalSeconds % 60); 42 | 43 | const parts: string[] = []; 44 | 45 | if (hours > 0) { 46 | parts.push(`${hours}h`); 47 | } 48 | if (minutes > 0) { 49 | parts.push(`${minutes}m`); 50 | } 51 | if (seconds > 0) { 52 | parts.push(`${seconds}s`); 53 | } 54 | 55 | // If all parts are zero (e.g., exactly 1 hour), return the largest unit. 56 | if (parts.length === 0) { 57 | if (hours > 0) return `${hours}h`; 58 | if (minutes > 0) return `${minutes}m`; 59 | return `${seconds}s`; 60 | } 61 | 62 | return parts.join(' '); 63 | }; 64 | -------------------------------------------------------------------------------- /scripts/copy_files.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | /** 4 | * @license 5 | * Copyright 2025 Google LLC 6 | * SPDX-License-Identifier: Apache-2.0 7 | */ 8 | 9 | // Copyright 2025 Google LLC 10 | // 11 | // Licensed under the Apache License, Version 2.0 (the "License"); 12 | // you may not use this file except in compliance with the License. 13 | // You may obtain a copy of the License at 14 | // 15 | // http://www.apache.org/licenses/LICENSE-2.0 16 | // 17 | // Unless required by applicable law or agreed to in writing, software 18 | // distributed under the License is distributed on an "AS IS" BASIS, 19 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 20 | // See the License for the specific language governing permissions and 21 | // limitations under the License. 22 | 23 | import fs from 'fs'; 24 | import path from 'path'; 25 | 26 | const sourceDir = path.join('src'); 27 | const targetDir = path.join('dist', 'src'); 28 | 29 | const extensionsToCopy = ['.md', '.json', '.sb']; 30 | 31 | function copyFilesRecursive(source, target) { 32 | if (!fs.existsSync(target)) { 33 | fs.mkdirSync(target, { recursive: true }); 34 | } 35 | 36 | const items = fs.readdirSync(source, { withFileTypes: true }); 37 | 38 | for (const item of items) { 39 | const sourcePath = path.join(source, item.name); 40 | const targetPath = path.join(target, item.name); 41 | 42 | if (item.isDirectory()) { 43 | copyFilesRecursive(sourcePath, targetPath); 44 | } else if (extensionsToCopy.includes(path.extname(item.name))) { 45 | fs.copyFileSync(sourcePath, targetPath); 46 | } 47 | } 48 | } 49 | 50 | if (!fs.existsSync(sourceDir)) { 51 | console.error(`Source directory ${sourceDir} not found.`); 52 | process.exit(1); 53 | } 54 | 55 | copyFilesRecursive(sourceDir, targetDir); 56 | console.log('Successfully copied files.'); 57 | -------------------------------------------------------------------------------- /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, type Part } from '@google/genai'; 8 | 9 | /** 10 | * Represents a request to be sent to the Gemini API. 11 | * For now, it's an alias to PartListUnion as the primary content. 12 | * This can be expanded later to include other request parameters. 13 | */ 14 | export type GeminiCodeRequest = PartListUnion; 15 | 16 | export function partListUnionToString(value: PartListUnion): string { 17 | if (typeof value === 'string') { 18 | return value; 19 | } 20 | 21 | if (Array.isArray(value)) { 22 | return value.map(partListUnionToString).join(''); 23 | } 24 | 25 | // Cast to Part, assuming it might contain project-specific fields 26 | const part = value as Part & { 27 | videoMetadata?: unknown; 28 | thought?: string; 29 | codeExecutionResult?: unknown; 30 | executableCode?: unknown; 31 | }; 32 | 33 | if (part.videoMetadata !== undefined) { 34 | return `[Video Metadata]`; 35 | } 36 | 37 | if (part.thought !== undefined) { 38 | return `[Thought: ${part.thought}]`; 39 | } 40 | 41 | if (part.codeExecutionResult !== undefined) { 42 | return `[Code Execution Result]`; 43 | } 44 | 45 | if (part.executableCode !== undefined) { 46 | return `[Executable Code]`; 47 | } 48 | 49 | // Standard Part fields 50 | if (part.fileData !== undefined) { 51 | return `[File Data]`; 52 | } 53 | 54 | if (part.functionCall !== undefined) { 55 | return `[Function Call: ${part.functionCall.name}]`; 56 | } 57 | 58 | if (part.functionResponse !== undefined) { 59 | return `[Function Response: ${part.functionResponse.name}]`; 60 | } 61 | 62 | if (part.inlineData !== undefined) { 63 | return `<${part.inlineData.mimeType}>`; 64 | } 65 | 66 | if (part.text !== undefined) { 67 | return part.text; 68 | } 69 | 70 | return ''; 71 | } 72 | -------------------------------------------------------------------------------- /packages/cli/src/ui/components/LoadingIndicator.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright 2025 Google LLC 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | import { ThoughtSummary } from '@google/gemini-cli-core'; 8 | import React from 'react'; 9 | import { Box, Text } from 'ink'; 10 | import { Colors } from '../colors.js'; 11 | import { useStreamingContext } from '../contexts/StreamingContext.js'; 12 | import { StreamingState } from '../types.js'; 13 | import { GeminiRespondingSpinner } from './GeminiRespondingSpinner.js'; 14 | 15 | interface LoadingIndicatorProps { 16 | currentLoadingPhrase?: string; 17 | elapsedTime: number; 18 | rightContent?: React.ReactNode; 19 | thought?: ThoughtSummary | null; 20 | } 21 | 22 | export const LoadingIndicator: React.FC = ({ 23 | currentLoadingPhrase, 24 | elapsedTime, 25 | rightContent, 26 | thought, 27 | }) => { 28 | const streamingState = useStreamingContext(); 29 | 30 | if (streamingState === StreamingState.Idle) { 31 | return null; 32 | } 33 | 34 | const primaryText = thought?.subject || currentLoadingPhrase; 35 | 36 | return ( 37 | 38 | {/* Main loading line */} 39 | 40 | 41 | 48 | 49 | {primaryText && {primaryText}} 50 | 51 | {streamingState === StreamingState.WaitingForConfirmation 52 | ? '' 53 | : ` (esc to cancel, ${elapsedTime}s)`} 54 | 55 | {/* Spacer */} 56 | {rightContent && {rightContent}} 57 | 58 | 59 | ); 60 | }; 61 | -------------------------------------------------------------------------------- /packages/core/src/utils/gitUtils.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright 2025 Google LLC 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | import * as fs from 'fs'; 8 | import * as path from 'path'; 9 | 10 | /** 11 | * Checks if a directory is within a git repository 12 | * @param directory The directory to check 13 | * @returns true if the directory is in a git repository, false otherwise 14 | */ 15 | export function isGitRepository(directory: string): boolean { 16 | try { 17 | let currentDir = path.resolve(directory); 18 | 19 | while (true) { 20 | const gitDir = path.join(currentDir, '.git'); 21 | 22 | // Check if .git exists (either as directory or file for worktrees) 23 | if (fs.existsSync(gitDir)) { 24 | return true; 25 | } 26 | 27 | const parentDir = path.dirname(currentDir); 28 | 29 | // If we've reached the root directory, stop searching 30 | if (parentDir === currentDir) { 31 | break; 32 | } 33 | 34 | currentDir = parentDir; 35 | } 36 | 37 | return false; 38 | } catch (_error) { 39 | // If any filesystem error occurs, assume not a git repo 40 | return false; 41 | } 42 | } 43 | 44 | /** 45 | * Finds the root directory of a git repository 46 | * @param directory Starting directory to search from 47 | * @returns The git repository root path, or null if not in a git repository 48 | */ 49 | export function findGitRoot(directory: string): string | null { 50 | try { 51 | let currentDir = path.resolve(directory); 52 | 53 | while (true) { 54 | const gitDir = path.join(currentDir, '.git'); 55 | 56 | if (fs.existsSync(gitDir)) { 57 | return currentDir; 58 | } 59 | 60 | const parentDir = path.dirname(currentDir); 61 | 62 | if (parentDir === currentDir) { 63 | break; 64 | } 65 | 66 | currentDir = parentDir; 67 | } 68 | 69 | return null; 70 | } catch (_error) { 71 | return null; 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /scripts/build.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 not use this file except in compliance with the License. 11 | // You may obtain a copy of the License at 12 | // 13 | // http://www.apache.org/licenses/LICENSE-2.0 14 | // 15 | // Unless required by applicable law or agreed to in writing, software 16 | // distributed under the License is distributed on an "AS IS" BASIS, 17 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 18 | // See the License for the specific language governing permissions and 19 | // limitations under the License. 20 | 21 | import { execSync } from 'child_process'; 22 | import { existsSync } from 'fs'; 23 | import { dirname, join } from 'path'; 24 | import { fileURLToPath } from 'url'; 25 | 26 | const __dirname = dirname(fileURLToPath(import.meta.url)); 27 | const root = join(__dirname, '..'); 28 | 29 | // npm install if node_modules was removed (e.g. via npm run clean or scripts/clean.js) 30 | if (!existsSync(join(root, 'node_modules'))) { 31 | execSync('npm install', { stdio: 'inherit', cwd: root }); 32 | } 33 | 34 | // build all workspaces/packages 35 | execSync('npm run generate', { stdio: 'inherit', cwd: root }); 36 | execSync('npm run build --workspaces', { stdio: 'inherit', cwd: root }); 37 | 38 | // also build container image if sandboxing is enabled 39 | // skip (-s) npm install + build since we did that above 40 | try { 41 | execSync('node scripts/sandbox_command.js -q', { 42 | stdio: 'inherit', 43 | cwd: root, 44 | }); 45 | if ( 46 | process.env.BUILD_SANDBOX === '1' || 47 | process.env.BUILD_SANDBOX === 'true' 48 | ) { 49 | execSync('node scripts/build_sandbox.js -s', { 50 | stdio: 'inherit', 51 | cwd: root, 52 | }); 53 | } 54 | } catch { 55 | // ignore 56 | } 57 | -------------------------------------------------------------------------------- /packages/cli/src/ui/components/messages/ToolConfirmationMessage.test.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright 2025 Google LLC 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | import { render } from 'ink-testing-library'; 8 | import { describe, it, expect, vi } from 'vitest'; 9 | import { ToolConfirmationMessage } from './ToolConfirmationMessage.js'; 10 | import { ToolCallConfirmationDetails } from '@google/gemini-cli-core'; 11 | 12 | describe('ToolConfirmationMessage', () => { 13 | it('should not display urls if prompt and url are the same', () => { 14 | const confirmationDetails: ToolCallConfirmationDetails = { 15 | type: 'info', 16 | title: 'Confirm Web Fetch', 17 | prompt: 'https://example.com', 18 | urls: ['https://example.com'], 19 | onConfirm: vi.fn(), 20 | }; 21 | 22 | const { lastFrame } = render( 23 | , 28 | ); 29 | 30 | expect(lastFrame()).not.toContain('URLs to fetch:'); 31 | }); 32 | 33 | it('should display urls if prompt and url are different', () => { 34 | const confirmationDetails: ToolCallConfirmationDetails = { 35 | type: 'info', 36 | title: 'Confirm Web Fetch', 37 | prompt: 38 | 'fetch https://github.com/google/gemini-react/blob/main/README.md', 39 | urls: [ 40 | 'https://raw.githubusercontent.com/google/gemini-react/main/README.md', 41 | ], 42 | onConfirm: vi.fn(), 43 | }; 44 | 45 | const { lastFrame } = render( 46 | , 51 | ); 52 | 53 | expect(lastFrame()).toContain('URLs to fetch:'); 54 | expect(lastFrame()).toContain( 55 | '- https://raw.githubusercontent.com/google/gemini-react/main/README.md', 56 | ); 57 | }); 58 | }); 59 | -------------------------------------------------------------------------------- /packages/cli/src/ui/components/ConsolePatcher.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright 2025 Google LLC 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | import { useEffect } from 'react'; 8 | import util from 'util'; 9 | import { ConsoleMessageItem } from '../types.js'; 10 | 11 | interface UseConsolePatcherParams { 12 | onNewMessage: (message: Omit) => void; 13 | debugMode: boolean; 14 | } 15 | 16 | export const useConsolePatcher = ({ 17 | onNewMessage, 18 | debugMode, 19 | }: UseConsolePatcherParams): void => { 20 | useEffect(() => { 21 | const originalConsoleLog = console.log; 22 | const originalConsoleWarn = console.warn; 23 | const originalConsoleError = console.error; 24 | const originalConsoleDebug = console.debug; 25 | 26 | const formatArgs = (args: unknown[]): string => util.format(...args); 27 | 28 | const patchConsoleMethod = 29 | ( 30 | type: 'log' | 'warn' | 'error' | 'debug', 31 | originalMethod: (...args: unknown[]) => void, 32 | ) => 33 | (...args: unknown[]) => { 34 | if (debugMode) { 35 | originalMethod.apply(console, args); 36 | } 37 | 38 | // Then, if it's not a debug message or debugMode is on, pass to onNewMessage 39 | if (type !== 'debug' || debugMode) { 40 | onNewMessage({ 41 | type, 42 | content: formatArgs(args), 43 | count: 1, 44 | }); 45 | } 46 | }; 47 | 48 | console.log = patchConsoleMethod('log', originalConsoleLog); 49 | console.warn = patchConsoleMethod('warn', originalConsoleWarn); 50 | console.error = patchConsoleMethod('error', originalConsoleError); 51 | console.debug = patchConsoleMethod('debug', originalConsoleDebug); 52 | 53 | return () => { 54 | console.log = originalConsoleLog; 55 | console.warn = originalConsoleWarn; 56 | console.error = originalConsoleError; 57 | console.debug = originalConsoleDebug; 58 | }; 59 | }, [onNewMessage, debugMode]); 60 | }; 61 | -------------------------------------------------------------------------------- /packages/core/src/telemetry/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, vi, beforeEach, afterEach } from 'vitest'; 8 | import { 9 | initializeTelemetry, 10 | shutdownTelemetry, 11 | isTelemetrySdkInitialized, 12 | } from './sdk.js'; 13 | import { Config } from '../config/config.js'; 14 | import { NodeSDK } from '@opentelemetry/sdk-node'; 15 | 16 | vi.mock('@opentelemetry/sdk-node'); 17 | vi.mock('../config/config.js'); 18 | 19 | describe('telemetry', () => { 20 | let mockConfig: Config; 21 | let mockNodeSdk: NodeSDK; 22 | 23 | beforeEach(() => { 24 | vi.resetAllMocks(); 25 | 26 | mockConfig = new Config({ 27 | sessionId: 'test-session-id', 28 | model: 'test-model', 29 | targetDir: '/test/dir', 30 | debugMode: false, 31 | cwd: '/test/dir', 32 | }); 33 | vi.spyOn(mockConfig, 'getTelemetryEnabled').mockReturnValue(true); 34 | vi.spyOn(mockConfig, 'getTelemetryOtlpEndpoint').mockReturnValue( 35 | 'http://localhost:4317', 36 | ); 37 | vi.spyOn(mockConfig, 'getSessionId').mockReturnValue('test-session-id'); 38 | mockNodeSdk = { 39 | start: vi.fn(), 40 | shutdown: vi.fn().mockResolvedValue(undefined), 41 | } as unknown as NodeSDK; 42 | vi.mocked(NodeSDK).mockImplementation(() => mockNodeSdk); 43 | }); 44 | 45 | afterEach(async () => { 46 | // Ensure we shut down telemetry even if a test fails. 47 | if (isTelemetrySdkInitialized()) { 48 | await shutdownTelemetry(); 49 | } 50 | }); 51 | 52 | it('should initialize the telemetry service', () => { 53 | initializeTelemetry(mockConfig); 54 | expect(NodeSDK).toHaveBeenCalled(); 55 | expect(mockNodeSdk.start).toHaveBeenCalled(); 56 | }); 57 | 58 | it('should shutdown the telemetry service', async () => { 59 | initializeTelemetry(mockConfig); 60 | await shutdownTelemetry(); 61 | 62 | expect(mockNodeSdk.shutdown).toHaveBeenCalled(); 63 | }); 64 | }); 65 | -------------------------------------------------------------------------------- /packages/core/src/utils/errors.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright 2025 Google LLC 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | import { GaxiosError } from 'gaxios'; 8 | 9 | export function isNodeError(error: unknown): error is NodeJS.ErrnoException { 10 | return error instanceof Error && 'code' in error; 11 | } 12 | 13 | export function getErrorMessage(error: unknown): string { 14 | if (error instanceof Error) { 15 | return error.message; 16 | } 17 | try { 18 | return String(error); 19 | } catch { 20 | return 'Failed to get error details'; 21 | } 22 | } 23 | 24 | export class ForbiddenError extends Error {} 25 | export class UnauthorizedError extends Error {} 26 | export class BadRequestError extends Error {} 27 | export class MalformedToolCallError extends Error {} 28 | 29 | interface ResponseData { 30 | error?: { 31 | code?: number; 32 | message?: string; 33 | }; 34 | } 35 | 36 | export function toFriendlyError(error: unknown): unknown { 37 | if (error instanceof GaxiosError) { 38 | const data = parseResponseData(error); 39 | if (data.error && data.error.message && data.error.code) { 40 | switch (data.error.code) { 41 | case 400: 42 | return new BadRequestError(data.error.message); 43 | case 401: 44 | return new UnauthorizedError(data.error.message); 45 | case 403: 46 | // It's import to pass the message here since it might 47 | // explain the cause like "the cloud project you're 48 | // using doesn't have code assist enabled". 49 | return new ForbiddenError(data.error.message); 50 | default: 51 | } 52 | } 53 | } 54 | return error; 55 | } 56 | 57 | function parseResponseData(error: GaxiosError): ResponseData { 58 | // Inexplicably, Gaxios sometimes doesn't JSONify the response data. 59 | if (typeof error.response?.data === 'string') { 60 | return JSON.parse(error.response?.data) as ResponseData; 61 | } 62 | return typeof error.response?.data as ResponseData; 63 | } 64 | -------------------------------------------------------------------------------- /packages/core/src/utils/schemaValidator.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright 2025 Google LLC 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | /** 8 | * Simple utility to validate objects against JSON Schemas 9 | */ 10 | export class SchemaValidator { 11 | /** 12 | * Validates data against a JSON schema 13 | * @param schema JSON Schema to validate against 14 | * @param data Data to validate 15 | * @returns True if valid, false otherwise 16 | */ 17 | static validate(schema: Record, data: unknown): boolean { 18 | // This is a simplified implementation 19 | // In a real application, you would use a library like Ajv for proper validation 20 | 21 | // Check for required fields 22 | if (schema.required && Array.isArray(schema.required)) { 23 | const required = schema.required as string[]; 24 | const dataObj = data as Record; 25 | 26 | for (const field of required) { 27 | if (dataObj[field] === undefined) { 28 | console.error(`Missing required field: ${field}`); 29 | return false; 30 | } 31 | } 32 | } 33 | 34 | // Check property types if properties are defined 35 | if (schema.properties && typeof schema.properties === 'object') { 36 | const properties = schema.properties as Record; 37 | const dataObj = data as Record; 38 | 39 | for (const [key, prop] of Object.entries(properties)) { 40 | if (dataObj[key] !== undefined && prop.type) { 41 | const expectedType = prop.type; 42 | const actualType = Array.isArray(dataObj[key]) 43 | ? 'array' 44 | : typeof dataObj[key]; 45 | 46 | if (expectedType !== actualType) { 47 | console.error( 48 | `Type mismatch for property "${key}": expected ${expectedType}, got ${actualType}`, 49 | ); 50 | return false; 51 | } 52 | } 53 | } 54 | } 55 | 56 | return true; 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /packages/core/src/code_assist/setup.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright 2025 Google LLC 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | import { ClientMetadata, OnboardUserRequest } from './types.js'; 8 | import { CodeAssistServer } from './server.js'; 9 | import { OAuth2Client } from 'google-auth-library'; 10 | 11 | /** 12 | * 13 | * @param projectId the user's project id, if any 14 | * @returns the user's actual project id 15 | */ 16 | export async function setupUser(authClient: OAuth2Client): Promise { 17 | const projectId = process.env.GOOGLE_CLOUD_PROJECT; 18 | const caServer = new CodeAssistServer(authClient, projectId); 19 | 20 | const clientMetadata: ClientMetadata = { 21 | ideType: 'IDE_UNSPECIFIED', 22 | platform: 'PLATFORM_UNSPECIFIED', 23 | pluginType: 'GEMINI', 24 | duetProject: projectId, 25 | }; 26 | 27 | // TODO: Support Free Tier user without projectId. 28 | const loadRes = await caServer.loadCodeAssist({ 29 | cloudaicompanionProject: projectId, 30 | metadata: clientMetadata, 31 | }); 32 | 33 | const onboardTier: string = 34 | loadRes.allowedTiers?.find((tier) => tier.isDefault)?.id ?? 'legacy-tier'; 35 | 36 | const onboardReq: OnboardUserRequest = { 37 | tierId: onboardTier, 38 | cloudaicompanionProject: loadRes.cloudaicompanionProject || projectId || '', 39 | metadata: clientMetadata, 40 | }; 41 | try { 42 | // Poll onboardUser until long running operation is complete. 43 | let lroRes = await caServer.onboardUser(onboardReq); 44 | while (!lroRes.done) { 45 | await new Promise((f) => setTimeout(f, 5000)); 46 | lroRes = await caServer.onboardUser(onboardReq); 47 | } 48 | return lroRes.response?.cloudaicompanionProject?.id || ''; 49 | } catch (e) { 50 | console.log( 51 | '\n\nError onboarding with Code Assist.\n' + 52 | 'Google Workspace Account (e.g. your-name@your-company.com)' + 53 | ' must specify a GOOGLE_CLOUD_PROJECT environment variable.\n\n', 54 | ); 55 | throw e; 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /packages/core/src/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright 2025 Google LLC 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | // Export config 8 | export * from './config/config.js'; 9 | 10 | // Export Core Logic 11 | export * from './core/client.js'; 12 | export * from './core/contentGenerator.js'; 13 | export * from './core/geminiChat.js'; 14 | export * from './core/logger.js'; 15 | export * from './core/prompts.js'; 16 | export * from './core/tokenLimits.js'; 17 | export * from './core/turn.js'; 18 | export * from './core/geminiRequest.js'; 19 | export * from './core/coreToolScheduler.js'; 20 | export * from './core/nonInteractiveToolExecutor.js'; 21 | 22 | export * from './code_assist/codeAssist.js'; 23 | export * from './code_assist/oauth2.js'; 24 | 25 | // Export utilities 26 | export * from './utils/paths.js'; 27 | export * from './utils/schemaValidator.js'; 28 | export * from './utils/errors.js'; 29 | export * from './utils/getFolderStructure.js'; 30 | export * from './utils/memoryDiscovery.js'; 31 | export * from './utils/gitIgnoreParser.js'; 32 | export * from './utils/editor.js'; 33 | 34 | // Export services 35 | export * from './services/fileDiscoveryService.js'; 36 | export * from './services/gitService.js'; 37 | 38 | // Export base tool definitions 39 | export * from './tools/tools.js'; 40 | export * from './tools/tool-registry.js'; 41 | 42 | // Export specific tool logic 43 | export * from './tools/read-file.js'; 44 | export * from './tools/ls.js'; 45 | export * from './tools/grep.js'; 46 | export * from './tools/glob.js'; 47 | export * from './tools/edit.js'; 48 | export * from './tools/write-file.js'; 49 | export * from './tools/web-fetch.js'; 50 | export * from './tools/memoryTool.js'; 51 | export * from './tools/shell.js'; 52 | export * from './tools/web-search.js'; 53 | export * from './tools/read-many-files.js'; 54 | export * from './tools/mcp-client.js'; 55 | export * from './tools/mcp-tool.js'; 56 | 57 | // Export telemetry functions 58 | export * from './telemetry/index.js'; 59 | export { sessionId } from './utils/session.js'; 60 | -------------------------------------------------------------------------------- /packages/cli/src/ui/components/StatsDisplay.test.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright 2025 Google LLC 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | import { render } from 'ink-testing-library'; 8 | import { describe, it, expect } from 'vitest'; 9 | import { StatsDisplay } from './StatsDisplay.js'; 10 | import { type CumulativeStats } from '../contexts/SessionContext.js'; 11 | 12 | describe('', () => { 13 | const mockStats: CumulativeStats = { 14 | turnCount: 10, 15 | promptTokenCount: 1000, 16 | candidatesTokenCount: 2000, 17 | totalTokenCount: 3500, 18 | cachedContentTokenCount: 500, 19 | toolUsePromptTokenCount: 200, 20 | thoughtsTokenCount: 300, 21 | apiTimeMs: 50234, 22 | }; 23 | 24 | const mockLastTurnStats: CumulativeStats = { 25 | turnCount: 1, 26 | promptTokenCount: 100, 27 | candidatesTokenCount: 200, 28 | totalTokenCount: 350, 29 | cachedContentTokenCount: 50, 30 | toolUsePromptTokenCount: 20, 31 | thoughtsTokenCount: 30, 32 | apiTimeMs: 1234, 33 | }; 34 | 35 | const mockDuration = '1h 23m 45s'; 36 | 37 | it('renders correctly with given stats and duration', () => { 38 | const { lastFrame } = render( 39 | , 44 | ); 45 | 46 | expect(lastFrame()).toMatchSnapshot(); 47 | }); 48 | 49 | it('renders zero state correctly', () => { 50 | const zeroStats: CumulativeStats = { 51 | turnCount: 0, 52 | promptTokenCount: 0, 53 | candidatesTokenCount: 0, 54 | totalTokenCount: 0, 55 | cachedContentTokenCount: 0, 56 | toolUsePromptTokenCount: 0, 57 | thoughtsTokenCount: 0, 58 | apiTimeMs: 0, 59 | }; 60 | 61 | const { lastFrame } = render( 62 | , 67 | ); 68 | 69 | expect(lastFrame()).toMatchSnapshot(); 70 | }); 71 | }); 72 | -------------------------------------------------------------------------------- /.gcp/dogfood.yaml: -------------------------------------------------------------------------------- 1 | steps: 2 | # Step 1: Install root dependencies (includes workspaces) 3 | - name: 'us-west1-docker.pkg.dev/gemini-code-dev/gemini-code-containers/gemini-code-builder' 4 | entrypoint: 'npm' 5 | args: ['install'] 6 | 7 | # Step 2: Update version in root package.json 8 | - name: 'us-west1-docker.pkg.dev/gemini-code-dev/gemini-code-containers/gemini-code-builder' 9 | entrypoint: 'bash' 10 | args: 11 | - -c # Use bash -c to allow for command substitution and string manipulation 12 | - | 13 | current_version=$(npm pkg get version | sed 's/"//g') 14 | new_version="$${current_version}-$SHORT_SHA.$_REVISION" 15 | npm pkg set "version=$${new_version}" 16 | echo "Set root package.json version to: $${new_version}" 17 | 18 | # Step 3: Run prerelease:dev to update workspace versions and dependencies 19 | - name: 'us-west1-docker.pkg.dev/gemini-code-dev/gemini-code-containers/gemini-code-builder' 20 | entrypoint: 'npm' 21 | args: ['run', 'prerelease:dev'] # This will run prerelease:version and prerelease:deps 22 | 23 | # Step 4: Authenticate for Docker and NPM 24 | - name: 'us-west1-docker.pkg.dev/gemini-code-dev/gemini-code-containers/gemini-code-builder' 25 | entrypoint: 'npm' 26 | args: ['run', 'auth'] 27 | 28 | # Step 5: Run the master release script 29 | - name: 'us-west1-docker.pkg.dev/gemini-code-dev/gemini-code-containers/gemini-code-builder' 30 | entrypoint: 'npm' 31 | args: ['run', 'publish:release'] 32 | env: 33 | - 'GEMINI_SANDBOX=$_CONTAINER_TOOL' 34 | - 'SANDBOX_IMAGE_REGISTRY=$_SANDBOX_IMAGE_REGISTRY' 35 | - 'SANDBOX_IMAGE_NAME=$_SANDBOX_IMAGE_NAME' 36 | - 'NPM_PUBLISH_TAG=$_NPM_PUBLISH_TAG' 37 | 38 | options: 39 | defaultLogsBucketBehavior: REGIONAL_USER_OWNED_BUCKET 40 | dynamicSubstitutions: true 41 | 42 | substitutions: 43 | _REVISION: '0' 44 | _SANDBOX_IMAGE_REGISTRY: 'us-west1-docker.pkg.dev/gemini-code-dev/gemini-cli' 45 | _SANDBOX_IMAGE_NAME: 'sandbox-ci' 46 | _NPM_PUBLISH_TAG: 'head' 47 | _CONTAINER_TOOL: 'docker' 48 | -------------------------------------------------------------------------------- /packages/cli/src/ui/components/ContextSummaryDisplay.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright 2025 Google LLC 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | import React from 'react'; 8 | import { Text } from 'ink'; 9 | import { Colors } from '../colors.js'; 10 | import { type MCPServerConfig } from '@google/gemini-cli-core'; 11 | 12 | interface ContextSummaryDisplayProps { 13 | geminiMdFileCount: number; 14 | contextFileNames: string[]; 15 | mcpServers?: Record; 16 | showToolDescriptions?: boolean; 17 | } 18 | 19 | export const ContextSummaryDisplay: React.FC = ({ 20 | geminiMdFileCount, 21 | contextFileNames, 22 | mcpServers, 23 | showToolDescriptions, 24 | }) => { 25 | const mcpServerCount = Object.keys(mcpServers || {}).length; 26 | 27 | if (geminiMdFileCount === 0 && mcpServerCount === 0) { 28 | return ; // Render an empty space to reserve height 29 | } 30 | 31 | const geminiMdText = (() => { 32 | if (geminiMdFileCount === 0) { 33 | return ''; 34 | } 35 | const allNamesTheSame = new Set(contextFileNames).size < 2; 36 | const name = allNamesTheSame ? contextFileNames[0] : 'context'; 37 | return `${geminiMdFileCount} ${name} file${ 38 | geminiMdFileCount > 1 ? 's' : '' 39 | }`; 40 | })(); 41 | 42 | const mcpText = 43 | mcpServerCount > 0 44 | ? `${mcpServerCount} MCP server${mcpServerCount > 1 ? 's' : ''}` 45 | : ''; 46 | 47 | let summaryText = 'Using '; 48 | if (geminiMdText) { 49 | summaryText += geminiMdText; 50 | } 51 | if (geminiMdText && mcpText) { 52 | summaryText += ' and '; 53 | } 54 | if (mcpText) { 55 | summaryText += mcpText; 56 | // Add ctrl+t hint when MCP servers are available 57 | if (mcpServers && Object.keys(mcpServers).length > 0) { 58 | if (showToolDescriptions) { 59 | summaryText += ' (ctrl+t to toggle)'; 60 | } else { 61 | summaryText += ' (ctrl+t to view)'; 62 | } 63 | } 64 | } 65 | 66 | return {summaryText}; 67 | }; 68 | -------------------------------------------------------------------------------- /packages/cli/src/ui/hooks/useTimer.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright 2025 Google LLC 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | import { useState, useEffect, useRef } from 'react'; 8 | 9 | /** 10 | * Custom hook to manage a timer that increments every second. 11 | * @param isActive Whether the timer should be running. 12 | * @param resetKey A key that, when changed, will reset the timer to 0 and restart the interval. 13 | * @returns The elapsed time in seconds. 14 | */ 15 | export const useTimer = (isActive: boolean, resetKey: unknown) => { 16 | const [elapsedTime, setElapsedTime] = useState(0); 17 | const timerRef = useRef(null); 18 | const prevResetKeyRef = useRef(resetKey); 19 | const prevIsActiveRef = useRef(isActive); 20 | 21 | useEffect(() => { 22 | let shouldResetTime = false; 23 | 24 | if (prevResetKeyRef.current !== resetKey) { 25 | shouldResetTime = true; 26 | prevResetKeyRef.current = resetKey; 27 | } 28 | 29 | if (prevIsActiveRef.current === false && isActive) { 30 | // Transitioned from inactive to active 31 | shouldResetTime = true; 32 | } 33 | 34 | if (shouldResetTime) { 35 | setElapsedTime(0); 36 | } 37 | prevIsActiveRef.current = isActive; 38 | 39 | // Manage interval 40 | if (isActive) { 41 | // Clear previous interval unconditionally before starting a new one 42 | // This handles resetKey changes while active, ensuring a fresh interval start. 43 | if (timerRef.current) { 44 | clearInterval(timerRef.current); 45 | } 46 | timerRef.current = setInterval(() => { 47 | setElapsedTime((prev) => prev + 1); 48 | }, 1000); 49 | } else { 50 | if (timerRef.current) { 51 | clearInterval(timerRef.current); 52 | timerRef.current = null; 53 | } 54 | } 55 | 56 | return () => { 57 | if (timerRef.current) { 58 | clearInterval(timerRef.current); 59 | timerRef.current = null; 60 | } 61 | }; 62 | }, [isActive, resetKey]); 63 | 64 | return elapsedTime; 65 | }; 66 | -------------------------------------------------------------------------------- /scripts/generate-git-commit-info.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 'child_process'; 21 | import { existsSync, mkdirSync, writeFileSync } from 'fs'; 22 | import { dirname, join } from 'path'; 23 | import { fileURLToPath } from 'url'; 24 | 25 | const __dirname = dirname(fileURLToPath(import.meta.url)); 26 | const root = join(__dirname, '..'); 27 | const generatedDir = join(root, 'packages/cli/src/generated'); 28 | const gitCommitFile = join(generatedDir, 'git-commit.ts'); 29 | let gitCommitInfo = 'N/A'; 30 | 31 | if (!existsSync(generatedDir)) { 32 | mkdirSync(generatedDir, { recursive: true }); 33 | } 34 | 35 | try { 36 | const gitHash = execSync('git rev-parse --short HEAD', { 37 | encoding: 'utf-8', 38 | }).trim(); 39 | if (gitHash) { 40 | gitCommitInfo = gitHash; 41 | const gitStatus = execSync('git status --porcelain', { 42 | encoding: 'utf-8', 43 | }).trim(); 44 | if (gitStatus) { 45 | gitCommitInfo = `${gitHash} (local modifications)`; 46 | } 47 | } 48 | } catch { 49 | // ignore 50 | } 51 | 52 | const fileContent = `/** 53 | * @license 54 | * Copyright ${new Date().getFullYear()} Google LLC 55 | * SPDX-License-Identifier: Apache-2.0 56 | */ 57 | 58 | // This file is auto-generated by the build script (scripts/build.js) 59 | // Do not edit this file manually. 60 | export const GIT_COMMIT_INFO = '${gitCommitInfo}'; 61 | `; 62 | 63 | writeFileSync(gitCommitFile, fileContent); 64 | -------------------------------------------------------------------------------- /docs/extension.md: -------------------------------------------------------------------------------- 1 | # Gemini CLI Extensions 2 | 3 | Gemini CLI supports extensions that can be used to configure and extend its functionality. 4 | 5 | ## How it works 6 | 7 | On startup, Gemini CLI looks for extensions in two locations: 8 | 9 | 1. `/.gemini/extensions` 10 | 2. `/.gemini/extensions` 11 | 12 | Gemini CLI loads all extensions from both locations. If an extension with the same name exists in both locations, the extension in the workspace directory takes precedence. 13 | 14 | Within each location, individual extensions exist as a directory that contains a `gemini-extension.json` file. For example: 15 | 16 | `/.gemini/extensions/my-extension/gemini-extension.json` 17 | 18 | ### `gemini-extension.json` 19 | 20 | The `gemini-extension.json` file contains the configuration for the extension. The file has the following structure: 21 | 22 | ```json 23 | { 24 | "name": "my-extension", 25 | "version": "1.0.0", 26 | "mcpServers": { 27 | "my-server": { 28 | "command": "node my-server.js" 29 | } 30 | }, 31 | "contextFileName": "GEMINI.md" 32 | } 33 | ``` 34 | 35 | - `name`: The name of the extension. This is used to uniquely identify the extension. This should match the name of your extension directory. 36 | - `version`: The version of the extension. 37 | - `mcpServers`: A map of MCP servers to configure. The key is the name of the server, and the value is the server configuration. These servers will be loaded on startup just like MCP servers configured in a [`settings.json` file](./cli/configuration.md). If both an extension and a `settings.json` file configure an MCP server with the same name, the server defined in the `settings.json` file takes precedence. 38 | - `contextFileName`: The name of the file that contains the context for the extension. This will be used to load the context from the workspace. If this property is not used but a `GEMINI.md` file is present in your extension directory, then that file will be loaded. 39 | 40 | When Gemini CLI starts, it loads all the extensions and merges their configurations. If there are any conflicts, the workspace configuration takes precedence. 41 | -------------------------------------------------------------------------------- /integration-tests/simple-mcp-server.test.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright 2025 Google LLC 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | import { test, describe, before, after } from 'node:test'; 8 | import { strict as assert } from 'node:assert'; 9 | import { TestRig } from './test-helper.js'; 10 | import { spawn } from 'child_process'; 11 | import { join } from 'path'; 12 | import { fileURLToPath } from 'url'; 13 | import { writeFileSync, unlinkSync } from 'fs'; 14 | 15 | const __dirname = fileURLToPath(new URL('.', import.meta.url)); 16 | const serverScriptPath = join(__dirname, './temp-server.js'); 17 | 18 | const serverScript = ` 19 | import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; 20 | import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'; 21 | import { z } from 'zod'; 22 | 23 | const server = new McpServer({ 24 | name: 'addition-server', 25 | version: '1.0.0', 26 | }); 27 | 28 | server.registerTool( 29 | 'add', 30 | { 31 | title: 'Addition Tool', 32 | description: 'Add two numbers', 33 | inputSchema: { a: z.number(), b: z.number() }, 34 | }, 35 | async ({ a, b }) => ({ 36 | content: [{ type: 'text', text: String(a + b) }], 37 | }), 38 | ); 39 | 40 | const transport = new StdioServerTransport(); 41 | await server.connect(transport); 42 | `; 43 | 44 | describe('simple-mcp-server', () => { 45 | const rig = new TestRig(); 46 | let child; 47 | 48 | before(() => { 49 | writeFileSync(serverScriptPath, serverScript); 50 | child = spawn('node', [serverScriptPath], { 51 | stdio: ['pipe', 'pipe', 'pipe'], 52 | }); 53 | child.stderr.on('data', (data) => { 54 | console.error(`stderr: ${data}`); 55 | }); 56 | // Wait for the server to be ready 57 | return new Promise((resolve) => setTimeout(resolve, 500)); 58 | }); 59 | 60 | after(() => { 61 | child.kill(); 62 | unlinkSync(serverScriptPath); 63 | }); 64 | 65 | test('should add two numbers', () => { 66 | rig.setup('should add two numbers'); 67 | const output = rig.run('add 5 and 10'); 68 | assert.ok(output.includes('15')); 69 | }); 70 | }); 71 | -------------------------------------------------------------------------------- /packages/core/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@google/gemini-cli-core", 3 | "version": "0.1.1", 4 | "description": "Gemini CLI Server", 5 | "type": "module", 6 | "main": "dist/index.js", 7 | "scripts": { 8 | "start": "node dist/src/index.js", 9 | "build": "node ../../scripts/build_package.js", 10 | "clean": "rm -rf dist", 11 | "lint": "eslint . --ext .ts,.tsx", 12 | "format": "prettier --write .", 13 | "test": "vitest run", 14 | "test:ci": "vitest run --coverage", 15 | "typecheck": "tsc --noEmit", 16 | "prerelease:version": "node ../../scripts/bind_package_version.js", 17 | "prerelease:deps": "node ../../scripts/bind_package_dependencies.js", 18 | "prepack": "npm run build" 19 | }, 20 | "files": [ 21 | "dist" 22 | ], 23 | "dependencies": { 24 | "@anthropic-ai/sdk": "^0.55.0", 25 | "@google/genai": "^1.4.0", 26 | "@modelcontextprotocol/sdk": "^1.11.0", 27 | "@opentelemetry/api": "^1.9.0", 28 | "@opentelemetry/exporter-logs-otlp-grpc": "^0.52.0", 29 | "@opentelemetry/exporter-metrics-otlp-grpc": "^0.52.0", 30 | "@opentelemetry/exporter-trace-otlp-grpc": "^0.52.0", 31 | "@opentelemetry/instrumentation-http": "^0.52.0", 32 | "@opentelemetry/sdk-node": "^0.52.0", 33 | "@types/glob": "^8.1.0", 34 | "@types/html-to-text": "^9.0.4", 35 | "diff": "^7.0.0", 36 | "dotenv": "^16.4.7", 37 | "glob": "^10.4.5", 38 | "google-auth-library": "^9.11.0", 39 | "html-to-text": "^9.0.5", 40 | "ignore": "^7.0.0", 41 | "micromatch": "^4.0.8", 42 | "ollama": "^0.5.16", 43 | "open": "^10.1.2", 44 | "openai": "^5.7.0", 45 | "shell-quote": "^1.8.2", 46 | "simple-git": "^3.28.0", 47 | "strip-ansi": "^7.1.0", 48 | "tiktoken": "^1.0.21", 49 | "undici": "^7.10.0", 50 | "ws": "^8.18.0" 51 | }, 52 | "devDependencies": { 53 | "@types/diff": "^7.0.2", 54 | "@types/dotenv": "^6.1.1", 55 | "@types/micromatch": "^4.0.8", 56 | "@types/minimatch": "^5.1.2", 57 | "@types/ws": "^8.5.10", 58 | "typescript": "^5.3.3", 59 | "vitest": "^3.1.1" 60 | }, 61 | "engines": { 62 | "node": ">=18" 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /docs/tools/web-fetch.md: -------------------------------------------------------------------------------- 1 | # Web Fetch Tool (`web_fetch`) 2 | 3 | This document describes the `web_fetch` tool for the Gemini CLI. 4 | 5 | ## Description 6 | 7 | Use `web_fetch` to summarize, compare, or extract information from web pages. The `web_fetch` tool processes content from one or more URLs (up to 20) embedded in a prompt. `web_fetch` takes a natural language prompt and returns a generated response. 8 | 9 | ### Arguments 10 | 11 | `web_fetch` takes one argument: 12 | 13 | - `prompt` (string, required): A comprehensive prompt that includes the URL(s) (up to 20) to fetch and specific instructions on how to process their content. For example: `"Summarize https://example.com/article and extract key points from https://another.com/data"`. The prompt must contain at least one URL starting with `http://` or `https://`. 14 | 15 | ## How to use `web_fetch` with the Gemini CLI 16 | 17 | To use `web_fetch` with the Gemini CLI, provide a natural language prompt that contains URLs. The tool will ask for confirmation before fetching any URLs. Once confirmed, the tool will process URLs through Gemini API's `urlContext`. 18 | 19 | If the Gemini API cannot access the URL, the tool will fall back to fetching content directly from the local machine. The tool will format the response, including source attribution and citations where possible. The tool will then provide the response to the user. 20 | 21 | Usage: 22 | 23 | ``` 24 | web_fetch(prompt="Your prompt, including a URL such as https://google.com.") 25 | ``` 26 | 27 | ## `web_fetch` examples 28 | 29 | Summarize a single article: 30 | 31 | ``` 32 | web_fetch(prompt="Can you summarize the main points of https://example.com/news/latest") 33 | ``` 34 | 35 | Compare two articles: 36 | 37 | ``` 38 | web_fetch(prompt="What are the differences in the conclusions of these two papers: https://arxiv.org/abs/2401.0001 and https://arxiv.org/abs/2401.0002?") 39 | ``` 40 | 41 | ## Important notes 42 | 43 | - **URL processing:** `web_fetch` relies on the Gemini API's ability to access and process the given URLs. 44 | - **Output quality:** The quality of the output will depend on the clarity of the instructions in the prompt. 45 | -------------------------------------------------------------------------------- /packages/cli/src/ui/themes/no-color.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright 2025 Google LLC 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | import { Theme, ColorsTheme } from './theme.js'; 8 | 9 | const noColorColorsTheme: ColorsTheme = { 10 | type: 'ansi', 11 | Background: '', 12 | Foreground: '', 13 | LightBlue: '', 14 | AccentBlue: '', 15 | AccentPurple: '', 16 | AccentCyan: '', 17 | AccentGreen: '', 18 | AccentYellow: '', 19 | AccentRed: '', 20 | Comment: '', 21 | Gray: '', 22 | }; 23 | 24 | export const NoColorTheme: Theme = new Theme( 25 | 'No Color', 26 | 'dark', 27 | { 28 | hljs: { 29 | display: 'block', 30 | overflowX: 'auto', 31 | padding: '0.5em', 32 | }, 33 | 'hljs-keyword': {}, 34 | 'hljs-literal': {}, 35 | 'hljs-symbol': {}, 36 | 'hljs-name': {}, 37 | 'hljs-link': { 38 | textDecoration: 'underline', 39 | }, 40 | 'hljs-built_in': {}, 41 | 'hljs-type': {}, 42 | 'hljs-number': {}, 43 | 'hljs-class': {}, 44 | 'hljs-string': {}, 45 | 'hljs-meta-string': {}, 46 | 'hljs-regexp': {}, 47 | 'hljs-template-tag': {}, 48 | 'hljs-subst': {}, 49 | 'hljs-function': {}, 50 | 'hljs-title': {}, 51 | 'hljs-params': {}, 52 | 'hljs-formula': {}, 53 | 'hljs-comment': { 54 | fontStyle: 'italic', 55 | }, 56 | 'hljs-quote': { 57 | fontStyle: 'italic', 58 | }, 59 | 'hljs-doctag': {}, 60 | 'hljs-meta': {}, 61 | 'hljs-meta-keyword': {}, 62 | 'hljs-tag': {}, 63 | 'hljs-variable': {}, 64 | 'hljs-template-variable': {}, 65 | 'hljs-attr': {}, 66 | 'hljs-attribute': {}, 67 | 'hljs-builtin-name': {}, 68 | 'hljs-section': {}, 69 | 'hljs-emphasis': { 70 | fontStyle: 'italic', 71 | }, 72 | 'hljs-strong': { 73 | fontWeight: 'bold', 74 | }, 75 | 'hljs-bullet': {}, 76 | 'hljs-selector-tag': {}, 77 | 'hljs-selector-id': {}, 78 | 'hljs-selector-class': {}, 79 | 'hljs-selector-attr': {}, 80 | 'hljs-selector-pseudo': {}, 81 | 'hljs-addition': { 82 | display: 'inline-block', 83 | width: '100%', 84 | }, 85 | 'hljs-deletion': { 86 | display: 'inline-block', 87 | width: '100%', 88 | }, 89 | }, 90 | noColorColorsTheme, 91 | ); 92 | -------------------------------------------------------------------------------- /packages/cli/src/ui/utils/markdownUtilities.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 { findLastSafeSplitPoint } from './markdownUtilities.js'; 9 | 10 | describe('markdownUtilities', () => { 11 | describe('findLastSafeSplitPoint', () => { 12 | it('should split at the last double newline if not in a code block', () => { 13 | const content = 'paragraph1\n\nparagraph2\n\nparagraph3'; 14 | expect(findLastSafeSplitPoint(content)).toBe(24); // After the second \n\n 15 | }); 16 | 17 | it('should return content.length if no safe split point is found', () => { 18 | const content = 'longstringwithoutanysafesplitpoint'; 19 | expect(findLastSafeSplitPoint(content)).toBe(content.length); 20 | }); 21 | 22 | it('should prioritize splitting at \n\n over being at the very end of the string if the end is not in a code block', () => { 23 | const content = 'Some text here.\n\nAnd more text here.'; 24 | expect(findLastSafeSplitPoint(content)).toBe(17); // after the \n\n 25 | }); 26 | 27 | it('should return content.length if the only \n\n is inside a code block and the end of content is not', () => { 28 | const content = '```\nignore this\n\nnewline\n```KeepThis'; 29 | expect(findLastSafeSplitPoint(content)).toBe(content.length); 30 | }); 31 | 32 | it('should correctly identify the last \n\n even if it is followed by text not in a code block', () => { 33 | const content = 34 | 'First part.\n\nSecond part.\n\nThird part, then some more text.'; 35 | // Split should be after "Second part.\n\n" 36 | // "First part.\n\n" is 13 chars. "Second part.\n\n" is 14 chars. Total 27. 37 | expect(findLastSafeSplitPoint(content)).toBe(27); 38 | }); 39 | 40 | it('should return content.length if content is empty', () => { 41 | const content = ''; 42 | expect(findLastSafeSplitPoint(content)).toBe(0); 43 | }); 44 | 45 | it('should return content.length if content has no newlines and no code blocks', () => { 46 | const content = 'Single line of text'; 47 | expect(findLastSafeSplitPoint(content)).toBe(content.length); 48 | }); 49 | }); 50 | }); 51 | -------------------------------------------------------------------------------- /packages/cli/src/ui/contexts/OverflowContext.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright 2025 Google LLC 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | import React, { 8 | createContext, 9 | useContext, 10 | useState, 11 | useCallback, 12 | useMemo, 13 | } from 'react'; 14 | 15 | interface OverflowState { 16 | overflowingIds: ReadonlySet; 17 | } 18 | 19 | interface OverflowActions { 20 | addOverflowingId: (id: string) => void; 21 | removeOverflowingId: (id: string) => void; 22 | } 23 | 24 | const OverflowStateContext = createContext( 25 | undefined, 26 | ); 27 | 28 | const OverflowActionsContext = createContext( 29 | undefined, 30 | ); 31 | 32 | export const useOverflowState = (): OverflowState | undefined => 33 | useContext(OverflowStateContext); 34 | 35 | export const useOverflowActions = (): OverflowActions | undefined => 36 | useContext(OverflowActionsContext); 37 | 38 | export const OverflowProvider: React.FC<{ children: React.ReactNode }> = ({ 39 | children, 40 | }) => { 41 | const [overflowingIds, setOverflowingIds] = useState(new Set()); 42 | 43 | const addOverflowingId = useCallback((id: string) => { 44 | setOverflowingIds((prevIds) => { 45 | if (prevIds.has(id)) { 46 | return prevIds; 47 | } 48 | const newIds = new Set(prevIds); 49 | newIds.add(id); 50 | return newIds; 51 | }); 52 | }, []); 53 | 54 | const removeOverflowingId = useCallback((id: string) => { 55 | setOverflowingIds((prevIds) => { 56 | if (!prevIds.has(id)) { 57 | return prevIds; 58 | } 59 | const newIds = new Set(prevIds); 60 | newIds.delete(id); 61 | return newIds; 62 | }); 63 | }, []); 64 | 65 | const stateValue = useMemo( 66 | () => ({ 67 | overflowingIds, 68 | }), 69 | [overflowingIds], 70 | ); 71 | 72 | const actionsValue = useMemo( 73 | () => ({ 74 | addOverflowingId, 75 | removeOverflowingId, 76 | }), 77 | [addOverflowingId, removeOverflowingId], 78 | ); 79 | 80 | return ( 81 | 82 | 83 | {children} 84 | 85 | 86 | ); 87 | }; 88 | -------------------------------------------------------------------------------- /packages/cli/src/ui/hooks/useEditorSettings.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 | import { LoadedSettings, SettingScope } from '../../config/settings.js'; 9 | import { type HistoryItem, MessageType } from '../types.js'; 10 | import { 11 | allowEditorTypeInSandbox, 12 | checkHasEditorType, 13 | EditorType, 14 | } from '@google/gemini-cli-core'; 15 | 16 | interface UseEditorSettingsReturn { 17 | isEditorDialogOpen: boolean; 18 | openEditorDialog: () => void; 19 | handleEditorSelect: ( 20 | editorType: EditorType | undefined, 21 | scope: SettingScope, 22 | ) => void; 23 | exitEditorDialog: () => void; 24 | } 25 | 26 | export const useEditorSettings = ( 27 | loadedSettings: LoadedSettings, 28 | setEditorError: (error: string | null) => void, 29 | addItem: (item: Omit, timestamp: number) => void, 30 | ): UseEditorSettingsReturn => { 31 | const [isEditorDialogOpen, setIsEditorDialogOpen] = useState(false); 32 | 33 | const openEditorDialog = useCallback(() => { 34 | setIsEditorDialogOpen(true); 35 | }, []); 36 | 37 | const handleEditorSelect = useCallback( 38 | (editorType: EditorType | undefined, scope: SettingScope) => { 39 | if ( 40 | editorType && 41 | (!checkHasEditorType(editorType) || 42 | !allowEditorTypeInSandbox(editorType)) 43 | ) { 44 | return; 45 | } 46 | 47 | try { 48 | loadedSettings.setValue(scope, 'preferredEditor', editorType); 49 | addItem( 50 | { 51 | type: MessageType.INFO, 52 | text: `Editor preference ${editorType ? `set to "${editorType}"` : 'cleared'} in ${scope} settings.`, 53 | }, 54 | Date.now(), 55 | ); 56 | setEditorError(null); 57 | setIsEditorDialogOpen(false); 58 | } catch (error) { 59 | setEditorError(`Failed to set editor preference: ${error}`); 60 | } 61 | }, 62 | [loadedSettings, setEditorError, addItem], 63 | ); 64 | 65 | const exitEditorDialog = useCallback(() => { 66 | setIsEditorDialogOpen(false); 67 | }, []); 68 | 69 | return { 70 | isEditorDialogOpen, 71 | openEditorDialog, 72 | handleEditorSelect, 73 | exitEditorDialog, 74 | }; 75 | }; 76 | -------------------------------------------------------------------------------- /packages/cli/src/ui/components/AboutBox.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright 2025 Google LLC 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | import React from 'react'; 8 | import { Box, Text } from 'ink'; 9 | import { Colors } from '../colors.js'; 10 | import { GIT_COMMIT_INFO } from '../../generated/git-commit.js'; 11 | 12 | interface AboutBoxProps { 13 | cliVersion: string; 14 | osVersion: string; 15 | sandboxEnv: string; 16 | modelVersion: string; 17 | } 18 | 19 | export const AboutBox: React.FC = ({ 20 | cliVersion, 21 | osVersion, 22 | sandboxEnv, 23 | modelVersion, 24 | }) => ( 25 | 33 | 34 | 35 | About Gemini CLI 36 | 37 | 38 | 39 | 40 | 41 | CLI Version 42 | 43 | 44 | 45 | {cliVersion} 46 | 47 | 48 | {GIT_COMMIT_INFO && !['N/A'].includes(GIT_COMMIT_INFO) && ( 49 | 50 | 51 | 52 | Git Commit 53 | 54 | 55 | 56 | {GIT_COMMIT_INFO} 57 | 58 | 59 | )} 60 | 61 | 62 | 63 | Model 64 | 65 | 66 | 67 | {modelVersion} 68 | 69 | 70 | 71 | 72 | 73 | Sandbox 74 | 75 | 76 | 77 | {sandboxEnv} 78 | 79 | 80 | 81 | 82 | 83 | OS 84 | 85 | 86 | 87 | {osVersion} 88 | 89 | 90 | 91 | ); 92 | -------------------------------------------------------------------------------- /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 React from 'react'; 8 | import { Box, Text } from 'ink'; 9 | import Gradient from 'ink-gradient'; 10 | import { Colors } from '../colors.js'; 11 | import { formatDuration } from '../utils/formatters.js'; 12 | import { CumulativeStats } from '../contexts/SessionContext.js'; 13 | import { FormattedStats, StatRow, StatsColumn } from './Stats.js'; 14 | 15 | // --- Prop and Data Structures --- 16 | 17 | interface SessionSummaryDisplayProps { 18 | stats: CumulativeStats; 19 | duration: string; 20 | } 21 | 22 | // --- Main Component --- 23 | 24 | export const SessionSummaryDisplay: React.FC = ({ 25 | stats, 26 | duration, 27 | }) => { 28 | const cumulativeFormatted: FormattedStats = { 29 | inputTokens: stats.promptTokenCount, 30 | outputTokens: stats.candidatesTokenCount, 31 | toolUseTokens: stats.toolUsePromptTokenCount, 32 | thoughtsTokens: stats.thoughtsTokenCount, 33 | cachedTokens: stats.cachedContentTokenCount, 34 | totalTokens: stats.totalTokenCount, 35 | }; 36 | 37 | const title = 'Agent powering down. Goodbye!'; 38 | 39 | return ( 40 | 48 | 49 | {Colors.GradientColors ? ( 50 | 51 | {title} 52 | 53 | ) : ( 54 | {title} 55 | )} 56 | 57 | 58 | 59 | 64 | 65 | 69 | 70 | 71 | 72 | 73 | 74 | ); 75 | }; 76 | -------------------------------------------------------------------------------- /packages/cli/src/ui/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 | import { formatDuration, formatMemoryUsage } from './formatters.js'; 9 | 10 | describe('formatters', () => { 11 | describe('formatMemoryUsage', () => { 12 | it('should format bytes into KB', () => { 13 | expect(formatMemoryUsage(12345)).toBe('12.1 KB'); 14 | }); 15 | 16 | it('should format bytes into MB', () => { 17 | expect(formatMemoryUsage(12345678)).toBe('11.8 MB'); 18 | }); 19 | 20 | it('should format bytes into GB', () => { 21 | expect(formatMemoryUsage(12345678901)).toBe('11.50 GB'); 22 | }); 23 | }); 24 | 25 | describe('formatDuration', () => { 26 | it('should format milliseconds less than a second', () => { 27 | expect(formatDuration(500)).toBe('500ms'); 28 | }); 29 | 30 | it('should format a duration of 0', () => { 31 | expect(formatDuration(0)).toBe('0s'); 32 | }); 33 | 34 | it('should format an exact number of seconds', () => { 35 | expect(formatDuration(5000)).toBe('5.0s'); 36 | }); 37 | 38 | it('should format a duration in seconds with one decimal place', () => { 39 | expect(formatDuration(12345)).toBe('12.3s'); 40 | }); 41 | 42 | it('should format an exact number of minutes', () => { 43 | expect(formatDuration(120000)).toBe('2m'); 44 | }); 45 | 46 | it('should format a duration in minutes and seconds', () => { 47 | expect(formatDuration(123000)).toBe('2m 3s'); 48 | }); 49 | 50 | it('should format an exact number of hours', () => { 51 | expect(formatDuration(3600000)).toBe('1h'); 52 | }); 53 | 54 | it('should format a duration in hours and seconds', () => { 55 | expect(formatDuration(3605000)).toBe('1h 5s'); 56 | }); 57 | 58 | it('should format a duration in hours, minutes, and seconds', () => { 59 | expect(formatDuration(3723000)).toBe('1h 2m 3s'); 60 | }); 61 | 62 | it('should handle large durations', () => { 63 | expect(formatDuration(86400000 + 3600000 + 120000 + 1000)).toBe( 64 | '25h 2m 1s', 65 | ); 66 | }); 67 | 68 | it('should handle negative durations', () => { 69 | expect(formatDuration(-100)).toBe('0s'); 70 | }); 71 | }); 72 | }); 73 | -------------------------------------------------------------------------------- /packages/cli/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@google/gemini-cli", 3 | "version": "0.1.1", 4 | "description": "Gemini CLI", 5 | "type": "module", 6 | "main": "dist/index.js", 7 | "bin": { 8 | "gemini": "dist/index.js" 9 | }, 10 | "scripts": { 11 | "build": "node ../../scripts/build_package.js", 12 | "clean": "rm -rf dist", 13 | "start": "node dist/index.js", 14 | "debug": "node --inspect-brk dist/index.js", 15 | "lint": "eslint . --ext .ts,.tsx", 16 | "format": "prettier --write .", 17 | "test": "vitest run", 18 | "test:ci": "vitest run --coverage", 19 | "typecheck": "tsc --noEmit", 20 | "prerelease:version": "node ../../scripts/bind_package_version.js", 21 | "prerelease:deps": "node ../../scripts/bind_package_dependencies.js", 22 | "prepack": "npm run build" 23 | }, 24 | "files": [ 25 | "dist" 26 | ], 27 | "config": { 28 | "sandboxImageUri": "us-docker.pkg.dev/gemini-code-dev/gemini-cli/sandbox:0.1.1" 29 | }, 30 | "dependencies": { 31 | "@google/gemini-cli-core": "0.1.1", 32 | "@types/update-notifier": "^6.0.8", 33 | "command-exists": "^1.2.9", 34 | "diff": "^7.0.0", 35 | "dotenv": "^16.4.7", 36 | "glob": "^10.4.1", 37 | "highlight.js": "^11.11.1", 38 | "ink": "^5.2.0", 39 | "ink-big-text": "^2.0.0", 40 | "ink-gradient": "^3.0.0", 41 | "ink-select-input": "^6.0.0", 42 | "ink-spinner": "^5.0.0", 43 | "ink-link": "^4.0.0", 44 | "ink-text-input": "^6.0.0", 45 | "lowlight": "^3.3.0", 46 | "mime-types": "^2.1.4", 47 | "open": "^10.1.2", 48 | "react": "^18.3.1", 49 | "read-package-up": "^11.0.0", 50 | "shell-quote": "^1.8.2", 51 | "string-width": "^7.1.0", 52 | "strip-ansi": "^7.1.0", 53 | "strip-json-comments": "^3.1.1", 54 | "update-notifier": "^7.3.1", 55 | "yargs": "^17.7.2" 56 | }, 57 | "devDependencies": { 58 | "@testing-library/react": "^14.0.0", 59 | "@types/command-exists": "^1.2.3", 60 | "@types/diff": "^7.0.2", 61 | "@types/dotenv": "^6.1.1", 62 | "@types/node": "^20.11.24", 63 | "@types/react": "^18.3.1", 64 | "@types/shell-quote": "^1.7.5", 65 | "@types/yargs": "^17.0.32", 66 | "ink-testing-library": "^4.0.0", 67 | "jsdom": "^26.1.0", 68 | "typescript": "^5.3.3", 69 | "vitest": "^3.1.1" 70 | }, 71 | "engines": { 72 | "node": ">=18" 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /packages/cli/src/ui/hooks/useLoadingIndicator.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright 2025 Google LLC 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | import { StreamingState } from '../types.js'; 8 | import { useTimer } from './useTimer.js'; 9 | import { usePhraseCycler } from './usePhraseCycler.js'; 10 | import { useState, useEffect, useRef } from 'react'; // Added useRef 11 | 12 | export const useLoadingIndicator = (streamingState: StreamingState) => { 13 | const [timerResetKey, setTimerResetKey] = useState(0); 14 | const isTimerActive = streamingState === StreamingState.Responding; 15 | 16 | const elapsedTimeFromTimer = useTimer(isTimerActive, timerResetKey); 17 | 18 | const isPhraseCyclingActive = streamingState === StreamingState.Responding; 19 | const isWaiting = streamingState === StreamingState.WaitingForConfirmation; 20 | const currentLoadingPhrase = usePhraseCycler( 21 | isPhraseCyclingActive, 22 | isWaiting, 23 | ); 24 | 25 | const [retainedElapsedTime, setRetainedElapsedTime] = useState(0); 26 | const prevStreamingStateRef = useRef(null); 27 | 28 | useEffect(() => { 29 | if ( 30 | prevStreamingStateRef.current === StreamingState.WaitingForConfirmation && 31 | streamingState === StreamingState.Responding 32 | ) { 33 | setTimerResetKey((prevKey) => prevKey + 1); 34 | setRetainedElapsedTime(0); // Clear retained time when going back to responding 35 | } else if ( 36 | streamingState === StreamingState.Idle && 37 | prevStreamingStateRef.current === StreamingState.Responding 38 | ) { 39 | setTimerResetKey((prevKey) => prevKey + 1); // Reset timer when becoming idle from responding 40 | setRetainedElapsedTime(0); 41 | } else if (streamingState === StreamingState.WaitingForConfirmation) { 42 | // Capture the time when entering WaitingForConfirmation 43 | // elapsedTimeFromTimer will hold the last value from when isTimerActive was true. 44 | setRetainedElapsedTime(elapsedTimeFromTimer); 45 | } 46 | 47 | prevStreamingStateRef.current = streamingState; 48 | }, [streamingState, elapsedTimeFromTimer]); 49 | 50 | return { 51 | elapsedTime: 52 | streamingState === StreamingState.WaitingForConfirmation 53 | ? retainedElapsedTime 54 | : elapsedTimeFromTimer, 55 | currentLoadingPhrase, 56 | }; 57 | }; 58 | -------------------------------------------------------------------------------- /integration-tests/test-helper.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright 2025 Google LLC 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | import { execSync } from 'child_process'; 8 | import { mkdirSync, writeFileSync, readFileSync } from 'fs'; 9 | import { join, dirname } from 'path'; 10 | import { fileURLToPath } from 'url'; 11 | import { env } from 'process'; 12 | 13 | const __dirname = dirname(fileURLToPath(import.meta.url)); 14 | 15 | function sanitizeTestName(name) { 16 | return name 17 | .toLowerCase() 18 | .replace(/[^a-z0-9]/g, '-') 19 | .replace(/-+/g, '-'); 20 | } 21 | 22 | export class TestRig { 23 | constructor() { 24 | this.bundlePath = join(__dirname, '..', 'bundle/gemini.js'); 25 | this.testDir = null; 26 | } 27 | 28 | setup(testName) { 29 | this.testName = testName; 30 | const sanitizedName = sanitizeTestName(testName); 31 | this.testDir = join(env.INTEGRATION_TEST_FILE_DIR, sanitizedName); 32 | mkdirSync(this.testDir, { recursive: true }); 33 | } 34 | 35 | createFile(fileName, content) { 36 | const filePath = join(this.testDir, fileName); 37 | writeFileSync(filePath, content); 38 | return filePath; 39 | } 40 | 41 | mkdir(dir) { 42 | mkdirSync(join(this.testDir, dir)); 43 | } 44 | 45 | run(prompt, ...args) { 46 | const output = execSync( 47 | `node ${this.bundlePath} --yolo --prompt "${prompt}" ${args.join(' ')}`, 48 | { 49 | cwd: this.testDir, 50 | encoding: 'utf-8', 51 | }, 52 | ); 53 | 54 | if (env.KEEP_OUTPUT === 'true') { 55 | const testId = `${env.TEST_FILE_NAME.replace( 56 | '.test.js', 57 | '', 58 | )}:${this.testName.replace(/ /g, '-')}`; 59 | console.log(`--- TEST: ${testId} ---`); 60 | console.log(output); 61 | console.log(`--- END TEST: ${testId} ---`); 62 | } 63 | 64 | return output; 65 | } 66 | 67 | readFile(fileName) { 68 | const content = readFileSync(join(this.testDir, fileName), 'utf-8'); 69 | if (env.KEEP_OUTPUT === 'true') { 70 | const testId = `${env.TEST_FILE_NAME.replace( 71 | '.test.js', 72 | '', 73 | )}:${this.testName.replace(/ /g, '-')}`; 74 | console.log(`--- FILE: ${testId}/${fileName} ---`); 75 | console.log(content); 76 | console.log(`--- END FILE: ${testId}/${fileName} ---`); 77 | } 78 | return content; 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /packages/core/src/utils/gitIgnoreParser.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright 2025 Google LLC 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | import * as fs from 'fs'; 8 | import * as path from 'path'; 9 | import ignore, { type Ignore } from 'ignore'; 10 | import { isGitRepository } from './gitUtils.js'; 11 | 12 | export interface GitIgnoreFilter { 13 | isIgnored(filePath: string): boolean; 14 | getPatterns(): string[]; 15 | } 16 | 17 | export class GitIgnoreParser implements GitIgnoreFilter { 18 | private projectRoot: string; 19 | private ig: Ignore = ignore(); 20 | private patterns: string[] = []; 21 | 22 | constructor(projectRoot: string) { 23 | this.projectRoot = path.resolve(projectRoot); 24 | } 25 | 26 | loadGitRepoPatterns(): void { 27 | if (!isGitRepository(this.projectRoot)) return; 28 | 29 | // Always ignore .git directory regardless of .gitignore content 30 | this.addPatterns(['.git']); 31 | 32 | const patternFiles = ['.gitignore', path.join('.git', 'info', 'exclude')]; 33 | for (const pf of patternFiles) { 34 | this.loadPatterns(pf); 35 | } 36 | } 37 | 38 | loadPatterns(patternsFileName: string): void { 39 | const patternsFilePath = path.join(this.projectRoot, patternsFileName); 40 | let content: string; 41 | try { 42 | content = fs.readFileSync(patternsFilePath, 'utf-8'); 43 | } catch (_error) { 44 | // ignore file not found 45 | return; 46 | } 47 | const patterns = (content ?? '') 48 | .split('\n') 49 | .map((p) => p.trim()) 50 | .filter((p) => p !== '' && !p.startsWith('#')); 51 | this.addPatterns(patterns); 52 | } 53 | 54 | private addPatterns(patterns: string[]) { 55 | this.ig.add(patterns); 56 | this.patterns.push(...patterns); 57 | } 58 | 59 | isIgnored(filePath: string): boolean { 60 | const relativePath = path.isAbsolute(filePath) 61 | ? path.relative(this.projectRoot, filePath) 62 | : filePath; 63 | 64 | if (relativePath === '' || relativePath.startsWith('..')) { 65 | return false; 66 | } 67 | 68 | let normalizedPath = relativePath.replace(/\\/g, '/'); 69 | if (normalizedPath.startsWith('./')) { 70 | normalizedPath = normalizedPath.substring(2); 71 | } 72 | 73 | return this.ig.ignores(normalizedPath); 74 | } 75 | 76 | getPatterns(): string[] { 77 | return this.patterns; 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /packages/core/src/utils/testUtils.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright 2025 Google LLC 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | /** 8 | * Testing utilities for simulating 429 errors in unit tests 9 | */ 10 | 11 | let requestCounter = 0; 12 | let simulate429Enabled = false; 13 | let simulate429AfterRequests = 0; 14 | let simulate429ForAuthType: string | undefined; 15 | let fallbackOccurred = false; 16 | 17 | /** 18 | * Check if we should simulate a 429 error for the current request 19 | */ 20 | export function shouldSimulate429(authType?: string): boolean { 21 | if (!simulate429Enabled || fallbackOccurred) { 22 | return false; 23 | } 24 | 25 | // If auth type filter is set, only simulate for that auth type 26 | if (simulate429ForAuthType && authType !== simulate429ForAuthType) { 27 | return false; 28 | } 29 | 30 | requestCounter++; 31 | 32 | // If afterRequests is set, only simulate after that many requests 33 | if (simulate429AfterRequests > 0) { 34 | return requestCounter > simulate429AfterRequests; 35 | } 36 | 37 | // Otherwise, simulate for every request 38 | return true; 39 | } 40 | 41 | /** 42 | * Reset the request counter (useful for tests) 43 | */ 44 | export function resetRequestCounter(): void { 45 | requestCounter = 0; 46 | } 47 | 48 | /** 49 | * Disable 429 simulation after successful fallback 50 | */ 51 | export function disableSimulationAfterFallback(): void { 52 | fallbackOccurred = true; 53 | } 54 | 55 | /** 56 | * Create a simulated 429 error response 57 | */ 58 | export function createSimulated429Error(): Error { 59 | const error = new Error('Rate limit exceeded (simulated)') as Error & { 60 | status: number; 61 | }; 62 | error.status = 429; 63 | return error; 64 | } 65 | 66 | /** 67 | * Reset simulation state when switching auth methods 68 | */ 69 | export function resetSimulationState(): void { 70 | fallbackOccurred = false; 71 | resetRequestCounter(); 72 | } 73 | 74 | /** 75 | * Enable/disable 429 simulation programmatically (for tests) 76 | */ 77 | export function setSimulate429( 78 | enabled: boolean, 79 | afterRequests = 0, 80 | forAuthType?: string, 81 | ): void { 82 | simulate429Enabled = enabled; 83 | simulate429AfterRequests = afterRequests; 84 | simulate429ForAuthType = forAuthType; 85 | fallbackOccurred = false; // Reset fallback state when simulation is re-enabled 86 | resetRequestCounter(); 87 | } 88 | -------------------------------------------------------------------------------- /packages/cli/src/ui/utils/textUtils.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright 2025 Google LLC 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | /** 8 | * Calculates the maximum width of a multi-line ASCII art string. 9 | * @param asciiArt The ASCII art string. 10 | * @returns The length of the longest line in the ASCII art. 11 | */ 12 | export const getAsciiArtWidth = (asciiArt: string): number => { 13 | if (!asciiArt) { 14 | return 0; 15 | } 16 | const lines = asciiArt.split('\n'); 17 | return Math.max(...lines.map((line) => line.length)); 18 | }; 19 | 20 | /** 21 | * Checks if a Buffer is likely binary by testing for the presence of a NULL byte. 22 | * The presence of a NULL byte is a strong indicator that the data is not plain text. 23 | * @param data The Buffer to check. 24 | * @param sampleSize The number of bytes from the start of the buffer to test. 25 | * @returns True if a NULL byte is found, false otherwise. 26 | */ 27 | export function isBinary( 28 | data: Buffer | null | undefined, 29 | sampleSize = 512, 30 | ): boolean { 31 | if (!data) { 32 | return false; 33 | } 34 | 35 | const sample = data.length > sampleSize ? data.subarray(0, sampleSize) : data; 36 | 37 | for (let i = 0; i < sample.length; i++) { 38 | // The presence of a NULL byte (0x00) is one of the most reliable 39 | // indicators of a binary file. Text files should not contain them. 40 | if (sample[i] === 0) { 41 | return true; 42 | } 43 | } 44 | 45 | // If no NULL bytes were found in the sample, we assume it's text. 46 | return false; 47 | } 48 | 49 | /* 50 | * ------------------------------------------------------------------------- 51 | * Unicode‑aware helpers (work at the code‑point level rather than UTF‑16 52 | * code units so that surrogate‑pair emoji count as one "column".) 53 | * ---------------------------------------------------------------------- */ 54 | 55 | export function toCodePoints(str: string): string[] { 56 | // [...str] or Array.from both iterate by UTF‑32 code point, handling 57 | // surrogate pairs correctly. 58 | return Array.from(str); 59 | } 60 | 61 | export function cpLen(str: string): number { 62 | return toCodePoints(str).length; 63 | } 64 | 65 | export function cpSlice(str: string, start: number, end?: number): string { 66 | // Slice by code‑point indices and re‑join. 67 | const arr = toCodePoints(str).slice(start, end); 68 | return arr.join(''); 69 | } 70 | -------------------------------------------------------------------------------- /scripts/prepare-cli-packagejson.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright 2025 Google LLC 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | import fs from 'fs'; 8 | import path from 'path'; 9 | import { fileURLToPath } from 'url'; 10 | 11 | // ES module equivalent of __dirname 12 | const __filename = fileURLToPath(import.meta.url); 13 | const __dirname = path.dirname(__filename); 14 | 15 | const cliPackageJsonPath = path.resolve( 16 | __dirname, 17 | '../packages/cli/package.json', 18 | ); 19 | const cliPackageJson = JSON.parse(fs.readFileSync(cliPackageJsonPath, 'utf8')); 20 | 21 | // Get version from root package.json (accessible via env var in npm scripts) 22 | const version = process.env.npm_package_version; 23 | 24 | // Get Docker registry and image name directly from PUBLISH_ environment variables. 25 | // These are expected to be set by the CI/build environment. 26 | const containerImageRegistry = process.env.SANDBOX_IMAGE_REGISTRY; 27 | const containerImageName = process.env.SANDBOX_IMAGE_NAME; 28 | 29 | if (!version || !containerImageRegistry || !containerImageName) { 30 | console.error( 31 | 'Error: Missing required environment variables. Need: ' + 32 | 'npm_package_version, SANDBOX_IMAGE_REGISTRY, and SANDBOX_IMAGE_NAME.', 33 | ); 34 | console.error( 35 | 'These should be passed from the CI environment (e.g., Cloud Build substitutions) ' + 36 | 'to the npm publish:release script.', 37 | ); 38 | process.exit(1); 39 | } 40 | 41 | const containerImageUri = `${containerImageRegistry}/${containerImageName}:${version}`; 42 | 43 | // Add or update fields in cliPackageJson.config to store this information 44 | if (!cliPackageJson.config) { 45 | cliPackageJson.config = {}; 46 | } 47 | cliPackageJson.config.sandboxImageUri = containerImageUri; 48 | 49 | // Remove 'prepublishOnly' from scripts if it exists 50 | if (cliPackageJson.scripts && cliPackageJson.scripts.prepublishOnly) { 51 | delete cliPackageJson.scripts.prepublishOnly; 52 | console.log('Removed prepublishOnly script from packages/cli/package.json'); 53 | } 54 | 55 | fs.writeFileSync( 56 | cliPackageJsonPath, 57 | JSON.stringify(cliPackageJson, null, 2) + '\n', 58 | ); 59 | console.log( 60 | `Updated ${path.relative(process.cwd(), cliPackageJsonPath)} with Docker image details:`, 61 | ); 62 | console.log(` URI: ${containerImageUri}`); 63 | console.log(` Registry: ${containerImageRegistry}`); 64 | console.log(` Image Name: ${containerImageName}`); 65 | -------------------------------------------------------------------------------- /docs/cli/themes.md: -------------------------------------------------------------------------------- 1 | # Themes 2 | 3 | Gemini CLI supports a variety of themes to customize its color scheme and appearance. You can change the theme to suit your preferences via the `/theme` command or `"theme":` configuration setting. 4 | 5 | ## Available Themes 6 | 7 | Gemini CLI comes with a selection of pre-defined themes, which you can list using the `/theme` command within Gemini CLI: 8 | 9 | - **Dark Themes:** 10 | - `ANSI` 11 | - `Atom One` 12 | - `Ayu` 13 | - `Default` 14 | - `Dracula` 15 | - `GitHub` 16 | - **Light Themes:** 17 | - `ANSI Light` 18 | - `Ayu Light` 19 | - `Default Light` 20 | - `GitHub Light` 21 | - `Google Code` 22 | - `Xcode` 23 | 24 | ### Changing Themes 25 | 26 | 1. Enter `/theme` into Gemini CLI. 27 | 2. A dialog or selection prompt appears, listing the available themes. 28 | 3. Using the arrow keys, select a theme. Some interfaces might offer a live preview or highlight as you select. 29 | 4. Confirm your selection to apply the theme. 30 | 31 | ### Theme Persistence 32 | 33 | Selected themes are saved in Gemini CLI's [configuration](./docs/cli/configuration.md) so your preference is remembered across sessions. 34 | 35 | ## Dark Themes 36 | 37 | ### ANSI 38 | 39 | ANSI theme 40 | 41 | ### Atom OneDark 42 | 43 | Atom One theme 44 | 45 | ### Ayu 46 | 47 | Ayu theme 48 | 49 | ### Default 50 | 51 | Default theme 52 | 53 | ### Dracula 54 | 55 | Dracula theme 56 | 57 | ### GitHub 58 | 59 | GitHub theme 60 | 61 | ## Light Themes 62 | 63 | ### ANSI Light 64 | 65 | ANSI Light theme 66 | 67 | ### Ayu Light 68 | 69 | Ayu Light theme 70 | 71 | ### Default Light 72 | 73 | Default Light theme 74 | 75 | ### GitHub Light 76 | 77 | GitHub Light theme 78 | 79 | ### Google Code 80 | 81 | Google Code theme 82 | 83 | ### Xcode 84 | 85 | Xcode Light theme 86 | -------------------------------------------------------------------------------- /packages/core/src/core/modelCheck.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright 2025 Google LLC 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | import { 8 | DEFAULT_GEMINI_MODEL, 9 | DEFAULT_GEMINI_FLASH_MODEL, 10 | } from '../config/models.js'; 11 | 12 | /** 13 | * Checks if the default "pro" model is rate-limited and returns a fallback "flash" 14 | * model if necessary. This function is designed to be silent. 15 | * @param apiKey The API key to use for the check. 16 | * @param currentConfiguredModel The model currently configured in settings. 17 | * @returns An object indicating the model to use, whether a switch occurred, 18 | * and the original model if a switch happened. 19 | */ 20 | export async function getEffectiveModel( 21 | apiKey: string, 22 | currentConfiguredModel: string, 23 | ): Promise { 24 | if (currentConfiguredModel !== DEFAULT_GEMINI_MODEL) { 25 | // Only check if the user is trying to use the specific pro model we want to fallback from. 26 | return currentConfiguredModel; 27 | } 28 | 29 | const modelToTest = DEFAULT_GEMINI_MODEL; 30 | const fallbackModel = DEFAULT_GEMINI_FLASH_MODEL; 31 | const endpoint = `https://generativelanguage.googleapis.com/v1beta/models/${modelToTest}:generateContent?key=${apiKey}`; 32 | const body = JSON.stringify({ 33 | contents: [{ parts: [{ text: 'test' }] }], 34 | generationConfig: { 35 | maxOutputTokens: 1, 36 | temperature: 0, 37 | topK: 1, 38 | thinkingConfig: { thinkingBudget: 0, includeThoughts: false }, 39 | }, 40 | }); 41 | 42 | const controller = new AbortController(); 43 | const timeoutId = setTimeout(() => controller.abort(), 2000); // 500ms timeout for the request 44 | 45 | try { 46 | const response = await fetch(endpoint, { 47 | method: 'POST', 48 | headers: { 'Content-Type': 'application/json' }, 49 | body, 50 | signal: controller.signal, 51 | }); 52 | 53 | clearTimeout(timeoutId); 54 | 55 | if (response.status === 429) { 56 | console.log( 57 | `[INFO] Your configured model (${modelToTest}) was temporarily unavailable. Switched to ${fallbackModel} for this session.`, 58 | ); 59 | return fallbackModel; 60 | } 61 | // For any other case (success, other error codes), we stick to the original model. 62 | return currentConfiguredModel; 63 | } catch (_error) { 64 | clearTimeout(timeoutId); 65 | // On timeout or any other fetch error, stick to the original model. 66 | return currentConfiguredModel; 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /scripts/start.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 { spawn, execSync } from 'child_process'; 21 | import { dirname, join } from 'path'; 22 | import { fileURLToPath } from 'url'; 23 | import { readFileSync } from 'fs'; 24 | 25 | const __dirname = dirname(fileURLToPath(import.meta.url)); 26 | const root = join(__dirname, '..'); 27 | const pkg = JSON.parse(readFileSync(join(root, 'package.json'), 'utf-8')); 28 | 29 | // check build status, write warnings to file for app to display if needed 30 | execSync('node ./scripts/check-build-status.js', { 31 | stdio: 'inherit', 32 | cwd: root, 33 | }); 34 | 35 | const nodeArgs = []; 36 | let sandboxCommand = undefined; 37 | try { 38 | sandboxCommand = execSync('node scripts/sandbox_command.js', { 39 | cwd: root, 40 | }) 41 | .toString() 42 | .trim(); 43 | } catch { 44 | // ignore 45 | } 46 | // if debugging is enabled and sandboxing is disabled, use --inspect-brk flag 47 | // note with sandboxing this flag is passed to the binary inside the sandbox 48 | // inside sandbox SANDBOX should be set and sandbox_command.js should fail 49 | if (process.env.DEBUG && !sandboxCommand) { 50 | if (process.env.SANDBOX) { 51 | const port = process.env.DEBUG_PORT || '9229'; 52 | nodeArgs.push(`--inspect-brk=0.0.0.0:${port}`); 53 | } else { 54 | nodeArgs.push('--inspect-brk'); 55 | } 56 | } 57 | 58 | nodeArgs.push('./packages/cli'); 59 | nodeArgs.push(...process.argv.slice(2)); 60 | 61 | const env = { 62 | ...process.env, 63 | CLI_VERSION: pkg.version, 64 | DEV: 'true', 65 | }; 66 | 67 | if (process.env.DEBUG) { 68 | // If this is not set, the debugger will pause on the outer process rather 69 | // than the relauncehd process making it harder to debug. 70 | env.GEMINI_CLI_NO_RELAUNCH = 'true'; 71 | } 72 | const child = spawn('node', nodeArgs, { stdio: 'inherit', env }); 73 | 74 | child.on('close', (code) => { 75 | process.exit(code); 76 | }); 77 | -------------------------------------------------------------------------------- /packages/cli/src/ui/hooks/useShowMemoryCommand.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright 2025 Google LLC 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | import { Message, MessageType } from '../types.js'; 8 | import { Config } from '@google/gemini-cli-core'; 9 | import { LoadedSettings } from '../../config/settings.js'; 10 | 11 | export function createShowMemoryAction( 12 | config: Config | null, 13 | settings: LoadedSettings, 14 | addMessage: (message: Message) => void, 15 | ) { 16 | return async () => { 17 | if (!config) { 18 | addMessage({ 19 | type: MessageType.ERROR, 20 | content: 'Configuration not available. Cannot show memory.', 21 | timestamp: new Date(), 22 | }); 23 | return; 24 | } 25 | 26 | const debugMode = config.getDebugMode(); 27 | 28 | if (debugMode) { 29 | console.log('[DEBUG] Show Memory command invoked.'); 30 | } 31 | 32 | const currentMemory = config.getUserMemory(); 33 | const fileCount = config.getGeminiMdFileCount(); 34 | const contextFileName = settings.merged.contextFileName; 35 | const contextFileNames = Array.isArray(contextFileName) 36 | ? contextFileName 37 | : [contextFileName]; 38 | 39 | if (debugMode) { 40 | console.log( 41 | `[DEBUG] Showing memory. Content from config.getUserMemory() (first 200 chars): ${currentMemory.substring(0, 200)}...`, 42 | ); 43 | console.log(`[DEBUG] Number of context files loaded: ${fileCount}`); 44 | } 45 | 46 | if (fileCount > 0) { 47 | const allNamesTheSame = new Set(contextFileNames).size < 2; 48 | const name = allNamesTheSame ? contextFileNames[0] : 'context'; 49 | addMessage({ 50 | type: MessageType.INFO, 51 | content: `Loaded memory from ${fileCount} ${name} file${ 52 | fileCount > 1 ? 's' : '' 53 | }.`, 54 | timestamp: new Date(), 55 | }); 56 | } 57 | 58 | if (currentMemory && currentMemory.trim().length > 0) { 59 | addMessage({ 60 | type: MessageType.INFO, 61 | content: `Current combined memory content:\n\`\`\`markdown\n${currentMemory}\n\`\`\``, 62 | timestamp: new Date(), 63 | }); 64 | } else { 65 | addMessage({ 66 | type: MessageType.INFO, 67 | content: 68 | fileCount > 0 69 | ? 'Hierarchical memory (GEMINI.md or other context files) is loaded but content is empty.' 70 | : 'No hierarchical memory (GEMINI.md or other context files) is currently loaded.', 71 | timestamp: new Date(), 72 | }); 73 | } 74 | }; 75 | } 76 | -------------------------------------------------------------------------------- /docs/tos-privacy.md: -------------------------------------------------------------------------------- 1 | # Gemini CLI: Terms of Service and Privacy Notice 2 | 3 | Gemini CLI is an open-source tool that allows you to interact with Google's powerful language models directly from your command-line interface. The terms of service and privacy notices that apply to your usage of Gemini CLI depend on the type of account you use to authenticate with Google. 4 | 5 | This article outlines the specific terms and privacy policies applicable to each account type. 6 | 7 | ## Login with Google (Code Assist Free Tier) 8 | 9 | For users who authenticate using their Google account to access the Code Assist free tier: 10 | 11 | - Terms of Service: Your use of Gemini CLI is governed by the general [Google Terms of Service](https://policies.google.com/terms?hl=en-US). 12 | - Privacy Notice: The collection and use of your data are described in the [Gemini Code Assist Privacy Notice for Individuals](https://developers.google.com/gemini-code-assist/resources/privacy-notice-gemini-code-assist-individuals). 13 | - Usage Statistics Opt-Out: You may opt-out from Usage Statistics data by following the instructions available here: Usage Statistics Configuration. 14 | 15 | ## Gemini API key 16 | 17 | If you are using a Gemini API key for authentication, the following terms apply: 18 | 19 | - Terms of Service: Your use is subject to the [Gemini API Terms of Service](https://ai.google.dev/gemini-api/terms). 20 | - Privacy Notice: Information regarding data handling and privacy is detailed in the general [Google Privacy Policy](https://policies.google.com/privacy). 21 | 22 | ## Licensed Code Assist Users 23 | 24 | For users with a licensed version of Code Assist (e.g. Standard and Enterprise editions): 25 | 26 | - Terms of Service: The [Google Cloud Platform Terms of Service](https://cloud.google.com/terms) govern your use of the service. 27 | - Privacy Notice: The handling of your data is outlined in the [Gemini Code Assist Privacy Notices](https://developers.google.com/gemini-code-assist/resources/privacy-notices). 28 | 29 | ## Vertex AI 30 | 31 | For users leveraging Gemini CLI with a Vertex AI backend: 32 | 33 | - Terms of Service: Your usage is governed by the [Google Cloud Platform Service Terms](https://cloud.google.com/terms/service-terms/). 34 | - Privacy Notice: The [Google Cloud Privacy Notice](https://cloud.google.com/terms/cloud-privacy-notice) describes how your data is collected and managed. 35 | 36 | ## Usage Statistics Opt-Out 37 | 38 | You may opt-out from sending Usage Statistics to Google data by following the instructions available here: [Usage Statistics Configuration](./cli/configuration.md#usage-statistics). 39 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "type": "node", 9 | "request": "launch", 10 | "name": "Launch CLI", 11 | "runtimeExecutable": "npm", 12 | "runtimeArgs": ["run", "start"], 13 | "skipFiles": ["/**"], 14 | "cwd": "${workspaceFolder}", 15 | "console": "integratedTerminal", 16 | "env": { 17 | "GEMINI_SANDBOX": "false" 18 | } 19 | }, 20 | { 21 | "type": "node", 22 | "request": "launch", 23 | "name": "Launch E2E", 24 | "runtimeExecutable": "npm", 25 | "runtimeArgs": ["run", "test:e2e", "read_many_files"], 26 | "skipFiles": ["/**"], 27 | "cwd": "${workspaceFolder}" 28 | }, 29 | { 30 | "name": "Attach", 31 | "port": 9229, 32 | "request": "attach", 33 | "skipFiles": ["/**"], 34 | "type": "node", 35 | // fix source mapping when debugging in sandbox using global installation 36 | // note this does not interfere when remoteRoot is also ${workspaceFolder}/packages 37 | "remoteRoot": "/usr/local/share/npm-global/lib/node_modules/@gemini-cli", 38 | "localRoot": "${workspaceFolder}/packages" 39 | }, 40 | { 41 | "type": "node", 42 | "request": "launch", 43 | "name": "Launch Program", 44 | "skipFiles": ["/**"], 45 | "program": "${file}", 46 | "outFiles": ["${workspaceFolder}/**/*.js"] 47 | }, 48 | { 49 | "type": "node", 50 | "request": "launch", 51 | "name": "Debug Test File", 52 | "runtimeExecutable": "npm", 53 | "runtimeArgs": [ 54 | "run", 55 | "test", 56 | "-w", 57 | "packages", 58 | "--", 59 | "--inspect-brk=9229", 60 | "--no-file-parallelism", 61 | "${input:testFile}" 62 | ], 63 | "cwd": "${workspaceFolder}", 64 | "console": "integratedTerminal", 65 | "internalConsoleOptions": "neverOpen", 66 | "skipFiles": ["/**"] 67 | } 68 | ], 69 | "inputs": [ 70 | { 71 | "id": "testFile", 72 | "type": "promptString", 73 | "description": "Enter the path to the test file (e.g., ${workspaceFolder}/packages/cli/src/ui/components/LoadingIndicator.test.tsx)", 74 | "default": "${workspaceFolder}/packages/cli/src/ui/components/LoadingIndicator.test.tsx" 75 | } 76 | ] 77 | } 78 | -------------------------------------------------------------------------------- /packages/cli/src/ui/hooks/useGitBranchName.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright 2025 Google LLC 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | import { useState, useEffect, useCallback } from 'react'; 8 | import { exec } from 'node:child_process'; 9 | import fs from 'node:fs'; 10 | import fsPromises from 'node:fs/promises'; 11 | import path from 'path'; 12 | 13 | export function useGitBranchName(cwd: string): string | undefined { 14 | const [branchName, setBranchName] = useState(undefined); 15 | 16 | const fetchBranchName = useCallback( 17 | () => 18 | exec( 19 | 'git rev-parse --abbrev-ref HEAD', 20 | { cwd }, 21 | (error, stdout, _stderr) => { 22 | if (error) { 23 | setBranchName(undefined); 24 | return; 25 | } 26 | const branch = stdout.toString().trim(); 27 | if (branch && branch !== 'HEAD') { 28 | setBranchName(branch); 29 | } else { 30 | exec( 31 | 'git rev-parse --short HEAD', 32 | { cwd }, 33 | (error, stdout, _stderr) => { 34 | if (error) { 35 | setBranchName(undefined); 36 | return; 37 | } 38 | setBranchName(stdout.toString().trim()); 39 | }, 40 | ); 41 | } 42 | }, 43 | ), 44 | [cwd, setBranchName], 45 | ); 46 | 47 | useEffect(() => { 48 | fetchBranchName(); // Initial fetch 49 | 50 | const gitLogsHeadPath = path.join(cwd, '.git', 'logs', 'HEAD'); 51 | let watcher: fs.FSWatcher | undefined; 52 | 53 | const setupWatcher = async () => { 54 | try { 55 | // Check if .git/logs/HEAD exists, as it might not in a new repo or orphaned head 56 | await fsPromises.access(gitLogsHeadPath, fs.constants.F_OK); 57 | watcher = fs.watch(gitLogsHeadPath, (eventType: string) => { 58 | // Changes to .git/logs/HEAD (appends) indicate HEAD has likely changed 59 | if (eventType === 'change' || eventType === 'rename') { 60 | // Handle rename just in case 61 | fetchBranchName(); 62 | } 63 | }); 64 | } catch (_watchError) { 65 | // Silently ignore watcher errors (e.g. permissions or file not existing), 66 | // similar to how exec errors are handled. 67 | // The branch name will simply not update automatically. 68 | } 69 | }; 70 | 71 | setupWatcher(); 72 | 73 | return () => { 74 | watcher?.close(); 75 | }; 76 | }, [cwd, fetchBranchName]); 77 | 78 | return branchName; 79 | } 80 | -------------------------------------------------------------------------------- /packages/cli/src/utils/startupWarnings.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, beforeEach } from 'vitest'; 8 | import { getStartupWarnings } from './startupWarnings.js'; 9 | import * as fs from 'fs/promises'; 10 | import { getErrorMessage } from '@google/gemini-cli-core'; 11 | 12 | vi.mock('fs/promises'); 13 | vi.mock('@google/gemini-cli-core', async (importOriginal) => { 14 | const actual = await importOriginal(); 15 | return { 16 | ...actual, 17 | getErrorMessage: vi.fn(), 18 | }; 19 | }); 20 | 21 | describe.skip('startupWarnings', () => { 22 | beforeEach(() => { 23 | vi.resetAllMocks(); 24 | }); 25 | 26 | it('should return warnings from the file and delete it', async () => { 27 | const mockWarnings = 'Warning 1\nWarning 2'; 28 | vi.spyOn(fs, 'access').mockResolvedValue(); 29 | vi.spyOn(fs, 'readFile').mockResolvedValue(mockWarnings); 30 | vi.spyOn(fs, 'unlink').mockResolvedValue(); 31 | 32 | const warnings = await getStartupWarnings(); 33 | 34 | expect(fs.access).toHaveBeenCalled(); 35 | expect(fs.readFile).toHaveBeenCalled(); 36 | expect(fs.unlink).toHaveBeenCalled(); 37 | expect(warnings).toEqual(['Warning 1', 'Warning 2']); 38 | }); 39 | 40 | it('should return an empty array if the file does not exist', async () => { 41 | const error = new Error('File not found'); 42 | (error as Error & { code: string }).code = 'ENOENT'; 43 | vi.spyOn(fs, 'access').mockRejectedValue(error); 44 | 45 | const warnings = await getStartupWarnings(); 46 | 47 | expect(warnings).toEqual([]); 48 | }); 49 | 50 | it('should return an error message if reading the file fails', async () => { 51 | const error = new Error('Permission denied'); 52 | vi.spyOn(fs, 'access').mockRejectedValue(error); 53 | vi.mocked(getErrorMessage).mockReturnValue('Permission denied'); 54 | 55 | const warnings = await getStartupWarnings(); 56 | 57 | expect(warnings).toEqual([ 58 | 'Error checking/reading warnings file: Permission denied', 59 | ]); 60 | }); 61 | 62 | it('should return a warning if deleting the file fails', async () => { 63 | const mockWarnings = 'Warning 1'; 64 | vi.spyOn(fs, 'access').mockResolvedValue(); 65 | vi.spyOn(fs, 'readFile').mockResolvedValue(mockWarnings); 66 | vi.spyOn(fs, 'unlink').mockRejectedValue(new Error('Permission denied')); 67 | 68 | const warnings = await getStartupWarnings(); 69 | 70 | expect(warnings).toEqual([ 71 | 'Warning 1', 72 | 'Warning: Could not delete temporary warnings file.', 73 | ]); 74 | }); 75 | }); 76 | -------------------------------------------------------------------------------- /scripts/telemetry.js: -------------------------------------------------------------------------------- 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 { execSync } from 'child_process'; 10 | import { join } from 'path'; 11 | import { existsSync, readFileSync } from 'fs'; 12 | 13 | const projectRoot = join(import.meta.dirname, '..'); 14 | 15 | const SETTINGS_DIRECTORY_NAME = '.gemini'; 16 | const USER_SETTINGS_DIR = join( 17 | process.env.HOME || process.env.USERPROFILE || process.env.HOMEPATH || '', 18 | SETTINGS_DIRECTORY_NAME, 19 | ); 20 | const USER_SETTINGS_PATH = join(USER_SETTINGS_DIR, 'settings.json'); 21 | const WORKSPACE_SETTINGS_PATH = join( 22 | projectRoot, 23 | SETTINGS_DIRECTORY_NAME, 24 | 'settings.json', 25 | ); 26 | 27 | let settingsTarget = undefined; 28 | 29 | function loadSettingsValue(filePath) { 30 | try { 31 | if (existsSync(filePath)) { 32 | const content = readFileSync(filePath, 'utf-8'); 33 | const jsonContent = content.replace(/\/\/[^\n]*/g, ''); 34 | const settings = JSON.parse(jsonContent); 35 | return settings.telemetry?.target; 36 | } 37 | } catch (e) { 38 | console.warn( 39 | `⚠️ Warning: Could not parse settings file at ${filePath}: ${e.message}`, 40 | ); 41 | } 42 | return undefined; 43 | } 44 | 45 | settingsTarget = loadSettingsValue(WORKSPACE_SETTINGS_PATH); 46 | 47 | if (!settingsTarget) { 48 | settingsTarget = loadSettingsValue(USER_SETTINGS_PATH); 49 | } 50 | 51 | let target = settingsTarget || 'local'; 52 | const allowedTargets = ['local', 'gcp']; 53 | 54 | const targetArg = process.argv.find((arg) => arg.startsWith('--target=')); 55 | if (targetArg) { 56 | const potentialTarget = targetArg.split('=')[1]; 57 | if (allowedTargets.includes(potentialTarget)) { 58 | target = potentialTarget; 59 | console.log(`⚙️ Using command-line target: ${target}`); 60 | } else { 61 | console.error( 62 | `🛑 Error: Invalid target '${potentialTarget}'. Allowed targets are: ${allowedTargets.join(', ')}.`, 63 | ); 64 | process.exit(1); 65 | } 66 | } else if (settingsTarget) { 67 | console.log( 68 | `⚙️ Using telemetry target from settings.json: ${settingsTarget}`, 69 | ); 70 | } 71 | 72 | const scriptPath = join( 73 | projectRoot, 74 | 'scripts', 75 | target === 'gcp' ? 'telemetry_gcp.js' : 'local_telemetry.js', 76 | ); 77 | 78 | try { 79 | console.log(`🚀 Running telemetry script for target: ${target}.`); 80 | execSync(`node ${scriptPath}`, { stdio: 'inherit', cwd: projectRoot }); 81 | } catch (error) { 82 | console.error(`🛑 Failed to run telemetry script for target: ${target}`); 83 | console.error(error); 84 | process.exit(1); 85 | } 86 | -------------------------------------------------------------------------------- /packages/core/src/utils/bfsFileSearch.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright 2025 Google LLC 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | import * as fs from 'fs/promises'; 8 | import * as path from 'path'; 9 | import { Dirent } from 'fs'; 10 | import { FileDiscoveryService } from '../services/fileDiscoveryService.js'; 11 | 12 | // Simple console logger for now. 13 | // TODO: Integrate with a more robust server-side logger. 14 | const logger = { 15 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 16 | debug: (...args: any[]) => console.debug('[DEBUG] [BfsFileSearch]', ...args), 17 | }; 18 | 19 | interface BfsFileSearchOptions { 20 | fileName: string; 21 | ignoreDirs?: string[]; 22 | maxDirs?: number; 23 | debug?: boolean; 24 | fileService?: FileDiscoveryService; 25 | } 26 | 27 | /** 28 | * Performs a breadth-first search for a specific file within a directory structure. 29 | * 30 | * @param rootDir The directory to start the search from. 31 | * @param options Configuration for the search. 32 | * @returns A promise that resolves to an array of paths where the file was found. 33 | */ 34 | export async function bfsFileSearch( 35 | rootDir: string, 36 | options: BfsFileSearchOptions, 37 | ): Promise { 38 | const { 39 | fileName, 40 | ignoreDirs = [], 41 | maxDirs = Infinity, 42 | debug = false, 43 | fileService, 44 | } = options; 45 | const foundFiles: string[] = []; 46 | const queue: string[] = [rootDir]; 47 | const visited = new Set(); 48 | let scannedDirCount = 0; 49 | 50 | while (queue.length > 0 && scannedDirCount < maxDirs) { 51 | const currentDir = queue.shift()!; 52 | if (visited.has(currentDir)) { 53 | continue; 54 | } 55 | visited.add(currentDir); 56 | scannedDirCount++; 57 | 58 | if (debug) { 59 | logger.debug(`Scanning [${scannedDirCount}/${maxDirs}]: ${currentDir}`); 60 | } 61 | 62 | let entries: Dirent[]; 63 | try { 64 | entries = await fs.readdir(currentDir, { withFileTypes: true }); 65 | } catch { 66 | // Ignore errors for directories we can't read (e.g., permissions) 67 | continue; 68 | } 69 | 70 | for (const entry of entries) { 71 | const fullPath = path.join(currentDir, entry.name); 72 | if (fileService?.shouldGitIgnoreFile(fullPath)) { 73 | continue; 74 | } 75 | 76 | if (entry.isDirectory()) { 77 | if (!ignoreDirs.includes(entry.name)) { 78 | queue.push(fullPath); 79 | } 80 | } else if (entry.isFile() && entry.name === fileName) { 81 | foundFiles.push(fullPath); 82 | } 83 | } 84 | } 85 | 86 | return foundFiles; 87 | } 88 | --------------------------------------------------------------------------------