├── GEMINI.md ├── .npmrc ├── docs ├── assets │ ├── vllm.png │ ├── openrouter.png │ ├── 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 ├── Uninstall.md └── extension.md ├── .vscode ├── settings.json └── tasks.json ├── .prettierrc.json ├── packages ├── cli │ ├── src │ │ ├── ui │ │ │ ├── hooks │ │ │ │ ├── useRefreshMemoryCommand.ts │ │ │ │ ├── useTerminalSize.ts │ │ │ │ ├── useLogger.ts │ │ │ │ ├── useBracketedPaste.ts │ │ │ │ ├── useStateAndRef.ts │ │ │ │ ├── useAutoAcceptIndicator.ts │ │ │ │ ├── useTimer.ts │ │ │ │ ├── useEditorSettings.ts │ │ │ │ ├── useLoadingIndicator.ts │ │ │ │ ├── useAuthCommand.ts │ │ │ │ ├── useShowMemoryCommand.ts │ │ │ │ └── useGitBranchName.ts │ │ │ ├── constants.ts │ │ │ ├── components │ │ │ │ ├── ShellModeIndicator.tsx │ │ │ │ ├── SessionSummaryDisplay.tsx │ │ │ │ ├── UpdateNotification.tsx │ │ │ │ ├── messages │ │ │ │ │ ├── UserShellMessage.tsx │ │ │ │ │ ├── ErrorMessage.tsx │ │ │ │ │ ├── InfoMessage.tsx │ │ │ │ │ ├── UserMessage.tsx │ │ │ │ │ ├── GeminiMessage.tsx │ │ │ │ │ ├── GeminiMessageContent.tsx │ │ │ │ │ ├── CompressionMessage.tsx │ │ │ │ │ └── ToolConfirmationMessage.test.tsx │ │ │ │ ├── ConsoleSummaryDisplay.tsx │ │ │ │ ├── GeminiRespondingSpinner.tsx │ │ │ │ ├── ShowMoreLines.tsx │ │ │ │ ├── AsciiArt.ts │ │ │ │ ├── MemoryUsageDisplay.tsx │ │ │ │ ├── AutoAcceptIndicator.tsx │ │ │ │ ├── Header.tsx │ │ │ │ ├── AuthInProgress.tsx │ │ │ │ ├── Tips.tsx │ │ │ │ ├── ConsolePatcher.tsx │ │ │ │ ├── ContextSummaryDisplay.tsx │ │ │ │ ├── LoadingIndicator.tsx │ │ │ │ ├── SessionSummaryDisplay.test.tsx │ │ │ │ └── __snapshots__ │ │ │ │ │ └── SessionSummaryDisplay.test.tsx.snap │ │ │ ├── commands │ │ │ │ ├── helpCommand.ts │ │ │ │ ├── clearCommand.ts │ │ │ │ └── helpCommand.test.ts │ │ │ ├── contexts │ │ │ │ ├── StreamingContext.tsx │ │ │ │ └── OverflowContext.tsx │ │ │ ├── utils │ │ │ │ ├── displayUtils.ts │ │ │ │ ├── commandUtils.ts │ │ │ │ ├── updateCheck.ts │ │ │ │ ├── textUtils.test.ts │ │ │ │ ├── formatters.ts │ │ │ │ ├── displayUtils.test.ts │ │ │ │ ├── markdownUtilities.test.ts │ │ │ │ ├── formatters.test.ts │ │ │ │ ├── textUtils.ts │ │ │ │ └── computeStats.ts │ │ │ ├── privacy │ │ │ │ ├── PrivacyNotice.tsx │ │ │ │ ├── CloudPaidPrivacyNotice.tsx │ │ │ │ └── GeminiPrivacyNotice.tsx │ │ │ ├── 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 │ │ │ ├── userStartupWarnings.ts │ │ │ ├── startupWarnings.ts │ │ │ └── userStartupWarnings.test.ts │ │ ├── api │ │ │ ├── types.ts │ │ │ └── util.ts │ │ ├── services │ │ │ └── CommandService.ts │ │ ├── config │ │ │ └── auth.ts │ │ └── test-utils │ │ │ └── mockCommandContext.test.ts │ ├── index.ts │ ├── tsconfig.json │ ├── vitest.config.ts │ └── package.json └── core │ ├── src │ ├── utils │ │ ├── session.ts │ │ ├── messageInspectors.ts │ │ ├── LruCache.ts │ │ ├── fetch.ts │ │ ├── gitUtils.ts │ │ ├── errors.ts │ │ ├── user_id.test.ts │ │ ├── gitIgnoreParser.ts │ │ ├── testUtils.ts │ │ └── schemaValidator.ts │ ├── tools │ │ └── diffOptions.ts │ ├── index.test.ts │ ├── config │ │ └── models.ts │ ├── custom_llm │ │ └── types.ts │ ├── code_assist │ │ ├── codeAssist.ts │ │ └── setup.ts │ ├── core │ │ ├── tokenLimits.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 ├── .editorconfig ├── .github ├── CODEOWNERS ├── dependabot.yml ├── ISSUE_TEMPLATE │ ├── feature_request.yml │ └── bug_report.yml └── pull_request_template.md ├── scripts ├── tests │ ├── test-setup.ts │ └── vitest.config.ts ├── create_alias.sh ├── build_package.js ├── copy_bundle_assets.js ├── prepare-package.js ├── copy_files.js ├── clean.js ├── build.js ├── generate-git-commit-info.js ├── start.js └── version.js ├── tsconfig.json ├── integration-tests ├── google_web_search.test.js ├── save_memory.test.js ├── write_file.test.js ├── replace.test.js ├── read_many_files.test.js ├── list_directory.test.js ├── file-system.test.js ├── run_shell_command.test.js └── simple-mcp-server.test.js ├── .gitignore ├── .gitattributes ├── .env.example ├── Dockerfile ├── Makefile └── esbuild.config.js /GEMINI.md: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | @google:registry=https://wombat-dressing-room.appspot.com -------------------------------------------------------------------------------- /docs/assets/vllm.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nearmetips/DailiCode/HEAD/docs/assets/vllm.png -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "typescript.tsserver.experimental.enableProjectDiagnostics": true 3 | } 4 | -------------------------------------------------------------------------------- /docs/assets/openrouter.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nearmetips/DailiCode/HEAD/docs/assets/openrouter.png -------------------------------------------------------------------------------- /docs/assets/theme-ansi.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nearmetips/DailiCode/HEAD/docs/assets/theme-ansi.png -------------------------------------------------------------------------------- /docs/assets/theme-ayu.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nearmetips/DailiCode/HEAD/docs/assets/theme-ayu.png -------------------------------------------------------------------------------- /docs/assets/theme-github.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nearmetips/DailiCode/HEAD/docs/assets/theme-github.png -------------------------------------------------------------------------------- /docs/assets/theme-atom-one.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nearmetips/DailiCode/HEAD/docs/assets/theme-atom-one.png -------------------------------------------------------------------------------- /docs/assets/theme-ayu-light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nearmetips/DailiCode/HEAD/docs/assets/theme-ayu-light.png -------------------------------------------------------------------------------- /docs/assets/theme-default.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nearmetips/DailiCode/HEAD/docs/assets/theme-default.png -------------------------------------------------------------------------------- /docs/assets/theme-dracula.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nearmetips/DailiCode/HEAD/docs/assets/theme-dracula.png -------------------------------------------------------------------------------- /docs/assets/gemini-screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nearmetips/DailiCode/HEAD/docs/assets/gemini-screenshot.png -------------------------------------------------------------------------------- /docs/assets/theme-ansi-light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nearmetips/DailiCode/HEAD/docs/assets/theme-ansi-light.png -------------------------------------------------------------------------------- /docs/assets/theme-xcode-light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nearmetips/DailiCode/HEAD/docs/assets/theme-xcode-light.png -------------------------------------------------------------------------------- /docs/assets/connected_devtools.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nearmetips/DailiCode/HEAD/docs/assets/connected_devtools.png -------------------------------------------------------------------------------- /docs/assets/theme-default-light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nearmetips/DailiCode/HEAD/docs/assets/theme-default-light.png -------------------------------------------------------------------------------- /docs/assets/theme-github-light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nearmetips/DailiCode/HEAD/docs/assets/theme-github-light.png -------------------------------------------------------------------------------- /docs/assets/theme-google-light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nearmetips/DailiCode/HEAD/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 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | insert_final_newline = true 6 | end_of_line = lf 7 | indent_style = space 8 | indent_size = 2 9 | max_line_length = 80 10 | 11 | [Makefile] 12 | indent_style = tab 13 | indent_size = 8 14 | -------------------------------------------------------------------------------- /.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/ -------------------------------------------------------------------------------- /scripts/tests/test-setup.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright 2025 Google LLC 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | import { vi } from 'vitest'; 8 | 9 | vi.mock('fs', () => ({ 10 | ...vi.importActual('fs'), 11 | appendFileSync: vi.fn(), 12 | })); 13 | -------------------------------------------------------------------------------- /packages/core/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 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: github-actions 4 | directory: '/' 5 | schedule: 6 | interval: weekly 7 | open-pull-requests-limit: 10 8 | 9 | - package-ecosystem: npm 10 | directory: '/' 11 | schedule: 12 | interval: weekly 13 | open-pull-requests-limit: 15 14 | versioning-strategy: widen 15 | -------------------------------------------------------------------------------- /packages/cli/src/utils/version.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright 2025 Google LLC 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | import { getPackageJson } from './package.js'; 8 | 9 | export async function getCliVersion(): Promise { 10 | const pkgJson = await getPackageJson(); 11 | return process.env.CLI_VERSION || pkgJson?.version || 'unknown'; 12 | } 13 | -------------------------------------------------------------------------------- /packages/cli/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 | -------------------------------------------------------------------------------- /scripts/tests/vitest.config.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright 2025 Google LLC 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | import { defineConfig } from 'vitest/config'; 8 | 9 | export default defineConfig({ 10 | test: { 11 | globals: true, 12 | environment: 'node', 13 | include: ['scripts/tests/**/*.test.js'], 14 | setupFiles: ['scripts/tests/test-setup.ts'], 15 | coverage: { 16 | provider: 'v8', 17 | reporter: ['text', 'lcov'], 18 | }, 19 | }, 20 | }); 21 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 { StatsDisplay } from './StatsDisplay.js'; 9 | 10 | interface SessionSummaryDisplayProps { 11 | duration: string; 12 | } 13 | 14 | export const SessionSummaryDisplay: React.FC = ({ 15 | duration, 16 | }) => ( 17 | 18 | ); 19 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /packages/cli/src/ui/commands/helpCommand.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright 2025 Google LLC 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | import { OpenDialogActionReturn, SlashCommand } from './types.js'; 8 | 9 | export const helpCommand: SlashCommand = { 10 | name: 'help', 11 | altName: '?', 12 | description: 'for help on gemini-cli', 13 | action: (_context, _args): OpenDialogActionReturn => { 14 | console.debug('Opening help UI ...'); 15 | return { 16 | type: 'dialog', 17 | dialog: 'help', 18 | }; 19 | }, 20 | }; 21 | -------------------------------------------------------------------------------- /packages/cli/src/ui/commands/clearCommand.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright 2025 Google LLC 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | import { SlashCommand } from './types.js'; 8 | 9 | export const clearCommand: SlashCommand = { 10 | name: 'clear', 11 | description: 'clear the screen and conversation history', 12 | action: async (context, _args) => { 13 | context.ui.setDebugMessage('Clearing terminal and resetting chat.'); 14 | await context.services.config?.getGeminiClient()?.resetChat(); 15 | context.ui.clear(); 16 | }, 17 | }; 18 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | "/**/*.ts", 15 | "./package.json" 16 | ], 17 | "exclude": [ 18 | "node_modules", 19 | "dist", 20 | "src/**/*.test.ts", 21 | "src/**/*.test.tsx", 22 | "src/test-utils" 23 | ], 24 | "references": [{ "path": "../core" }] 25 | } 26 | -------------------------------------------------------------------------------- /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 'daili-code-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 | -------------------------------------------------------------------------------- /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 | 17 | export function isFunctionCall(content: Content): boolean { 18 | return ( 19 | content.role === 'model' && 20 | !!content.parts && 21 | content.parts.every((part) => !!part.functionCall) 22 | ); 23 | } 24 | -------------------------------------------------------------------------------- /.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 | 40 | 41 | example-usage.ts 42 | -------------------------------------------------------------------------------- /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 | ) -------------------------------------------------------------------------------- /packages/core/src/custom_llm/types.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright 2025 Google LLC 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | /** 8 | * Custom LLM content generator configuration 9 | */ 10 | export interface CustomLLMContentGeneratorConfig { 11 | model: string; 12 | temperature: number; 13 | max_tokens: number; 14 | top_p: number; 15 | stream_options?: { 16 | include_usage?: boolean; 17 | }; 18 | } 19 | 20 | /** 21 | * Tool call data structure for streaming 22 | */ 23 | export interface ToolCallData { 24 | name: string; 25 | arguments: string; 26 | } 27 | 28 | /** 29 | * Map for tracking tool calls during streaming 30 | */ 31 | export type ToolCallMap = Map; 32 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | 2 | # 获取 API Key: https://openrouter.ai/keys 3 | USE_CUSTOM_LLM=true 4 | CUSTOM_LLM_PROVIDER="openai" 5 | CUSTOM_LLM_API_KEY="your-openrouter-api-key-here" 6 | CUSTOM_LLM_ENDPOINT="https://openrouter.ai/api/v1" 7 | CUSTOM_LLM_MODEL_NAME="anthropic/claude-3.5-sonnet" 8 | 9 | # 可选参数 10 | CUSTOM_LLM_TEMPERATURE=0.7 11 | CUSTOM_LLM_MAX_TOKENS=8192 12 | CUSTOM_LLM_TOP_P=1 13 | 14 | # 备用配置 - Gemini API Key 15 | # GEMINI_API_KEY="your-Gemini-api-key-here" 16 | 17 | # 备用配置 - Google OAuth 18 | # GOOGLE_CLIENT_ID="681255809395-oo8ft2oprdrnp9e3aqf6av3hmdib135j.apps.googleusercontent.com" 19 | # GOOGLE_CLIENT_SECRET="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" 20 | 21 | # 备用配置 - DailiCode OAuth 22 | # DAILICODE_CLIENT_ID="your-dailicode-client-id-here" 23 | # DAILICODE_CLIENT_SECRET="your-dailicode-client-secret-here" 24 | 25 | -------------------------------------------------------------------------------- /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/api/types.ts: -------------------------------------------------------------------------------- 1 | import { AuthType } from 'daili-code-core'; 2 | 3 | export interface AgentResult { 4 | type: 'content' | 'thought' | 'tool_call' | 'error' | 'user_cancelled'; 5 | content?: string; 6 | thought?: { 7 | summary: string; 8 | details?: string; 9 | }; 10 | toolCall?: { 11 | name: string; 12 | args: Record; 13 | result?: unknown; 14 | }; 15 | error?: string; 16 | timestamp: number; 17 | } 18 | 19 | export interface AgentConfig { 20 | model?: string; 21 | endpoint?: string; 22 | apiKey?: string; 23 | authType?: AuthType; 24 | provider?: string; 25 | temperature?: number; 26 | topP?: number; 27 | maxTokens?: number; 28 | log?: boolean; 29 | readonly?: boolean; 30 | systemPrompt?: string; 31 | rootPath?: string; 32 | extension?: any; 33 | } 34 | -------------------------------------------------------------------------------- /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/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 | rig.sync(); 17 | 18 | const prompt = `Can you list the files in the current directory. Display them in the style of 'ls'`; 19 | const result = rig.run(prompt); 20 | 21 | const lines = result.split('\n').filter((line) => line.trim() !== ''); 22 | assert.ok(lines.some((line) => line.includes('file1.txt'))); 23 | assert.ok(lines.some((line) => line.includes('subdir'))); 24 | }); 25 | -------------------------------------------------------------------------------- /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/cli/src/ui/utils/displayUtils.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright 2025 Google LLC 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | import { Colors } from '../colors.js'; 8 | 9 | // --- Thresholds --- 10 | export const TOOL_SUCCESS_RATE_HIGH = 95; 11 | export const TOOL_SUCCESS_RATE_MEDIUM = 85; 12 | 13 | export const USER_AGREEMENT_RATE_HIGH = 75; 14 | export const USER_AGREEMENT_RATE_MEDIUM = 45; 15 | 16 | export const CACHE_EFFICIENCY_HIGH = 40; 17 | export const CACHE_EFFICIENCY_MEDIUM = 15; 18 | 19 | // --- Color Logic --- 20 | export const getStatusColor = ( 21 | value: number, 22 | thresholds: { green: number; yellow: number }, 23 | options: { defaultColor?: string } = {}, 24 | ) => { 25 | if (value >= thresholds.green) { 26 | return Colors.AccentGreen; 27 | } 28 | if (value >= thresholds.yellow) { 29 | return Colors.AccentYellow; 30 | } 31 | return options.defaultColor || Colors.AccentRed; 32 | }; 33 | -------------------------------------------------------------------------------- /packages/cli/src/ui/hooks/useLogger.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright 2025 Google LLC 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | import { useState, useEffect } from 'react'; 8 | import { sessionId, Logger } from 'daili-code-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/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 | sessionId?: string, 16 | ): Promise { 17 | if ( 18 | authType === AuthType.LOGIN_WITH_GOOGLE || 19 | authType === AuthType.CLOUD_SHELL 20 | ) { 21 | const authClient = await getOauthClient(authType); 22 | const projectId = await setupUser(authClient); 23 | return new CodeAssistServer(authClient, projectId, httpOptions, sessionId); 24 | } 25 | 26 | throw new Error(`Unsupported authType: ${authType}`); 27 | } 28 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | 29 | 30 | {prefix} 31 | 32 | 33 | 34 | {text} 35 | 36 | 37 | 38 | ); 39 | }; 40 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 = rig.run(prompt); 18 | 19 | assert.ok(result.includes('blah.txt')); 20 | }); 21 | 22 | test('should be able to run a shell command via stdin', async (t) => { 23 | const rig = new TestRig(); 24 | rig.setup(t.name); 25 | rig.createFile('blah.txt', 'some content'); 26 | 27 | const prompt = `Can you use ls to list the contexts of the current folder`; 28 | const result = rig.run({ stdin: prompt }); 29 | 30 | assert.ok(result.includes('blah.txt')); 31 | }); 32 | -------------------------------------------------------------------------------- /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/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 | export * from './uiTelemetry.js'; 42 | -------------------------------------------------------------------------------- /packages/cli/src/ui/hooks/useBracketedPaste.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright 2025 Google LLC 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | import { useEffect } from 'react'; 8 | 9 | const ENABLE_BRACKETED_PASTE = '\x1b[?2004h'; 10 | const DISABLE_BRACKETED_PASTE = '\x1b[?2004l'; 11 | 12 | /** 13 | * Enables and disables bracketed paste mode in the terminal. 14 | * 15 | * This hook ensures that bracketed paste mode is enabled when the component 16 | * mounts and disabled when it unmounts or when the process exits. 17 | */ 18 | export const useBracketedPaste = () => { 19 | const cleanup = () => { 20 | process.stdout.write(DISABLE_BRACKETED_PASTE); 21 | }; 22 | 23 | useEffect(() => { 24 | process.stdout.write(ENABLE_BRACKETED_PASTE); 25 | 26 | process.on('exit', cleanup); 27 | process.on('SIGINT', cleanup); 28 | process.on('SIGTERM', cleanup); 29 | 30 | return () => { 31 | cleanup(); 32 | process.removeListener('exit', cleanup); 33 | process.removeListener('SIGINT', cleanup); 34 | process.removeListener('SIGTERM', cleanup); 35 | }; 36 | }, []); 37 | }; 38 | -------------------------------------------------------------------------------- /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/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 semver from 'semver'; 9 | import { getPackageJson } from '../../utils/package.js'; 10 | 11 | export async function checkForUpdates(): Promise { 12 | try { 13 | const packageJson = await getPackageJson(); 14 | if (!packageJson || !packageJson.name || !packageJson.version) { 15 | return null; 16 | } 17 | const notifier = updateNotifier({ 18 | pkg: { 19 | name: packageJson.name, 20 | version: packageJson.version, 21 | }, 22 | // check every time 23 | updateCheckInterval: 0, 24 | // allow notifier to run in scripts 25 | shouldNotifyInNpmScript: true, 26 | }); 27 | 28 | if ( 29 | notifier.update && 30 | semver.gt(notifier.update.latest, notifier.update.current) 31 | ) { 32 | return null; 33 | } 34 | 35 | return null; 36 | } catch (e) { 37 | console.warn('Failed to check for updates: ' + e); 38 | return null; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /packages/cli/src/services/CommandService.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright 2025 Google LLC 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | import { SlashCommand } from '../ui/commands/types.js'; 8 | import { memoryCommand } from '../ui/commands/memoryCommand.js'; 9 | import { helpCommand } from '../ui/commands/helpCommand.js'; 10 | import { clearCommand } from '../ui/commands/clearCommand.js'; 11 | 12 | const loadBuiltInCommands = async (): Promise => [ 13 | clearCommand, 14 | helpCommand, 15 | memoryCommand, 16 | ]; 17 | 18 | export class CommandService { 19 | private commands: SlashCommand[] = []; 20 | 21 | constructor( 22 | private commandLoader: () => Promise = loadBuiltInCommands, 23 | ) { 24 | // The constructor can be used for dependency injection in the future. 25 | } 26 | 27 | async loadCommands(): Promise { 28 | // For now, we only load the built-in commands. 29 | // File-based and remote commands will be added later. 30 | this.commands = await this.commandLoader(); 31 | } 32 | 33 | getCommands(): SlashCommand[] { 34 | return this.commands; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.yml: -------------------------------------------------------------------------------- 1 | name: Feature Request 2 | description: Suggest an idea for this project 3 | labels: ['kind/enhancement', 'status/need-triage'] 4 | body: 5 | - type: markdown 6 | attributes: 7 | value: | 8 | > [!IMPORTANT] 9 | > Thanks for taking the time to suggest an enhancement! 10 | > 11 | > Please search **[existing issues](https://github.com/google-gemini/gemini-cli/issues)** to see if a similar feature has already been requested. 12 | 13 | - type: textarea 14 | id: feature 15 | attributes: 16 | label: What would you like to be added? 17 | description: A clear and concise description of the enhancement. 18 | validations: 19 | required: true 20 | 21 | - type: textarea 22 | id: rationale 23 | attributes: 24 | label: Why is this needed? 25 | description: A clear and concise description of why this enhancement is needed. 26 | validations: 27 | required: true 28 | 29 | - type: textarea 30 | id: additional-context 31 | attributes: 32 | label: Additional context 33 | description: Add any other context or screenshots about the feature request here. 34 | -------------------------------------------------------------------------------- /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/ui/components/AsciiArt.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright 2025 Google LLC 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | 8 | 9 | export const shortAsciiLogo = ` 10 | 11 | ██████╗ █████╗ ██╗ ██╗ ██╗ ██████╗ ██████╗ ██████╗ ███████╗ 12 | ██╔══██╗ ██╔══██╗ ██║ ═ ╗██╔═══ ╗██╔══██╗██╔════╝ 13 | ██║ ██║ ███████║ ██║ ██║ ██║ ██║ ██║ ██║██║ ██║█████╗ 14 | ██║ ██║ ██╔══██║ ██║ ██║ ██║ ██║ ║██╔═══██╗██║ ██║██╔══╝ 15 | ██████╔╝ ██║ ██║ ██║ ███████╗██║ ╚██████╔╝╚██████╔╝██████╔╝███████╗ 16 | ╚═════╝ ╚═╝ ╚═╝ ╚═╝ ╚══════╝╚═╝ ╚═════╝ ╚═════╝ ╚═════╝ ╚══════╝ 17 | `; 18 | 19 | export const longAsciiLogo = ` 20 | 21 | ██████╗ █████╗ ██╗ ██╗ ██╗ ██████╗ ██████╗ ██████╗ ███████╗ 22 | ██╔══██╗██╔══██╗ ██║ ██╔═══ ╗██╔═══██╗██╔══██╗██╔════╝ 23 | ██║ ██║███████║ ██║ ██║ ██║ ██║ ██║ ██║██║ ██║█████╗ 24 | ██║ ██║██╔══██║ ██║ ██║ ██║ ██║ ║██╔═══██╗██║ ██║██╔══╝ 25 | ██████╔╝██║ ██║ ██║ ███████╗██║ ╚██████╔╝╚██████╔╝██████╔╝███████╗ 26 | ╚═════╝ ╚═╝ ╚═╝╚═╝╚══════╝╚═╝ ╚═════╝ ╚═════╝ ╚═════╝ ╚══════╝ 27 | `; -------------------------------------------------------------------------------- /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 docs/examples/proxy-script.md) 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='node $PROJECT_DIR/scripts/start.js'" 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 'daili-code-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/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 | -------------------------------------------------------------------------------- /packages/cli/src/ui/privacy/PrivacyNotice.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright 2025 Google LLC 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | import { Box } from 'ink'; 8 | import { type Config, AuthType } from 'daili-code-core'; 9 | import { GeminiPrivacyNotice } from './GeminiPrivacyNotice.js'; 10 | import { CloudPaidPrivacyNotice } from './CloudPaidPrivacyNotice.js'; 11 | import { CloudFreePrivacyNotice } from './CloudFreePrivacyNotice.js'; 12 | 13 | interface PrivacyNoticeProps { 14 | onExit: () => void; 15 | config: Config; 16 | } 17 | 18 | const PrivacyNoticeText = ({ 19 | config, 20 | onExit, 21 | }: { 22 | config: Config; 23 | onExit: () => void; 24 | }) => { 25 | const authType = config.getContentGeneratorConfig()?.authType; 26 | 27 | switch (authType) { 28 | case AuthType.USE_GEMINI: 29 | return ; 30 | case AuthType.USE_VERTEX_AI: 31 | return ; 32 | case AuthType.LOGIN_WITH_GOOGLE: 33 | default: 34 | return ; 35 | } 36 | }; 37 | 38 | export const PrivacyNotice = ({ onExit, config }: PrivacyNoticeProps) => ( 39 | 40 | 41 | 42 | ); 43 | -------------------------------------------------------------------------------- /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/commands/helpCommand.test.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright 2025 Google LLC 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | import { vi, describe, it, expect, beforeEach } from 'vitest'; 8 | import { helpCommand } from './helpCommand.js'; 9 | import { type CommandContext } from './types.js'; 10 | 11 | describe('helpCommand', () => { 12 | let mockContext: CommandContext; 13 | 14 | beforeEach(() => { 15 | mockContext = {} as unknown as CommandContext; 16 | }); 17 | 18 | it("should return a dialog action and log a debug message for '/help'", () => { 19 | const consoleDebugSpy = vi 20 | .spyOn(console, 'debug') 21 | .mockImplementation(() => {}); 22 | if (!helpCommand.action) { 23 | throw new Error('Help command has no action'); 24 | } 25 | const result = helpCommand.action(mockContext, ''); 26 | 27 | expect(result).toEqual({ 28 | type: 'dialog', 29 | dialog: 'help', 30 | }); 31 | expect(consoleDebugSpy).toHaveBeenCalledWith('Opening help UI ...'); 32 | }); 33 | 34 | it("should also be triggered by its alternative name '?'", () => { 35 | // This test is more conceptual. The routing of altName to the command 36 | // is handled by the slash command processor, but we can assert the 37 | // altName is correctly defined on the command object itself. 38 | expect(helpCommand.altName).toBe('?'); 39 | }); 40 | }); 41 | -------------------------------------------------------------------------------- /docs/Uninstall.md: -------------------------------------------------------------------------------- 1 | ## Uninstalling the CLI 2 | 3 | Your uninstall method depends on how you ran the CLI. Follow the instructions for either npx or a global npm installation. 4 | 5 | ### Method 1: Using npx 6 | 7 | npx runs packages from a temporary cache without a permanent installation. To "uninstall" the CLI, you must clear this cache, which will remove gemini-cli and any other packages previously executed with npx. 8 | 9 | The npx cache is a directory named `_npx` inside your main npm cache folder. You can find your npm cache path by running `npm config get cache`. 10 | 11 | **For macOS / Linux** 12 | 13 | ```bash 14 | # The path is typically ~/.npm/_npx 15 | rm -rf "$(npm config get cache)/_npx" 16 | ``` 17 | 18 | **For Windows** 19 | 20 | _Command Prompt_ 21 | 22 | ```cmd 23 | :: The path is typically %LocalAppData%\npm-cache\_npx 24 | rmdir /s /q "%LocalAppData%\npm-cache\_npx" 25 | ``` 26 | 27 | _PowerShell_ 28 | 29 | ```powershell 30 | # The path is typically $env:LocalAppData\npm-cache\_npx 31 | Remove-Item -Path (Join-Path $env:LocalAppData "npm-cache\_npx") -Recurse -Force 32 | ``` 33 | 34 | ### Method 2: Using npm (Global Install) 35 | 36 | If you installed the CLI globally (e.g., `npm install -g @google/gemini-cli`), use the `npm uninstall` command with the `-g` flag to remove it. 37 | 38 | ```bash 39 | npm uninstall -g @google/gemini-cli 40 | ``` 41 | 42 | This command completely removes the package from your system. 43 | -------------------------------------------------------------------------------- /packages/cli/src/utils/userStartupWarnings.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 * as os from 'os'; 9 | 10 | type WarningCheck = { 11 | id: string; 12 | check: (workspaceRoot: string) => Promise; 13 | }; 14 | 15 | // Individual warning checks 16 | const homeDirectoryCheck: WarningCheck = { 17 | id: 'home-directory', 18 | check: async (workspaceRoot: string) => { 19 | try { 20 | const [workspaceRealPath, homeRealPath] = await Promise.all([ 21 | fs.realpath(workspaceRoot), 22 | fs.realpath(os.homedir()), 23 | ]); 24 | 25 | if (workspaceRealPath === homeRealPath) { 26 | return 'You are running Gemini CLI in your home directory. It is recommended to run in a project-specific directory.'; 27 | } 28 | return null; 29 | } catch (_err: unknown) { 30 | return 'Could not verify the current directory due to a file system error.'; 31 | } 32 | }, 33 | }; 34 | 35 | // All warning checks 36 | const WARNING_CHECKS: readonly WarningCheck[] = [homeDirectoryCheck]; 37 | 38 | export async function getUserStartupWarnings( 39 | workspaceRoot: string, 40 | ): Promise { 41 | const results = await Promise.all( 42 | WARNING_CHECKS.map((check) => check.check(workspaceRoot)), 43 | ); 44 | return results.filter((msg) => msg !== null); 45 | } 46 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /.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 | 42 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | // Find and copy all .sb files from packages to the root of the bundle directory 35 | const sbFiles = glob.sync('packages/**/*.sb', { cwd: root }); 36 | for (const file of sbFiles) { 37 | copyFileSync(join(root, file), join(bundleDir, basename(file))); 38 | } 39 | 40 | console.log('Assets copied to bundle/'); 41 | -------------------------------------------------------------------------------- /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 run, 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 | -------------------------------------------------------------------------------- /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 'daili-code-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 | The Tool is forked from Gemini CLI. 21 | Tips for getting started: 22 | 23 | 1. Ask questions, edit files, or run commands. 24 | 25 | 26 | 2. Be specific for the best results. 27 | 28 | {geminiMdFileCount === 0 && ( 29 | 30 | 3. Create{' '} 31 | 32 | GEMINI.md 33 | {' '} 34 | files to customize your interactions with Gemini. 35 | 36 | )} 37 | 38 | {geminiMdFileCount === 0 ? '4.' : '3.'}{' '} 39 | 40 | /help 41 | {' '} 42 | for more information. 43 | 44 | 45 | ); 46 | }; 47 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /scripts/prepare-package.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 rootDir = path.resolve(__dirname, '..'); 16 | 17 | function copyFiles(packageName, filesToCopy) { 18 | const packageDir = path.resolve(rootDir, 'packages', packageName); 19 | if (!fs.existsSync(packageDir)) { 20 | console.error(`Error: Package directory not found at ${packageDir}`); 21 | process.exit(1); 22 | } 23 | 24 | console.log(`Preparing package: ${packageName}`); 25 | for (const [source, dest] of Object.entries(filesToCopy)) { 26 | const sourcePath = path.resolve(rootDir, source); 27 | const destPath = path.resolve(packageDir, dest); 28 | try { 29 | fs.copyFileSync(sourcePath, destPath); 30 | console.log(`Copied ${source} to packages/${packageName}/`); 31 | } catch (err) { 32 | console.error(`Error copying ${source}:`, err); 33 | process.exit(1); 34 | } 35 | } 36 | } 37 | 38 | // Prepare 'core' package 39 | copyFiles('core', { 40 | 'README.md': 'README.md', 41 | LICENSE: 'LICENSE', 42 | '.npmrc': '.npmrc', 43 | }); 44 | 45 | // Prepare 'cli' package 46 | copyFiles('cli', { 47 | 'README.md': 'README.md', 48 | LICENSE: 'LICENSE', 49 | }); 50 | 51 | console.log('Successfully prepared all packages.'); 52 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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-all - Build the entire project" 12 | @echo " make test - Run the test suite" 13 | @echo " make lint - Lint the code" 14 | @echo " make format - Format the code" 15 | @echo " make preflight - Run formatting, linting, and tests" 16 | @echo " make clean - Remove generated files" 17 | @echo " make start - Start the Gemini CLI" 18 | @echo " make debug - Start the Gemini CLI in debug mode" 19 | @echo "" 20 | @echo " make run-npx - Run the CLI using npx (for testing the published package)" 21 | @echo " make create-alias - Create a 'gemini' alias for your shell" 22 | 23 | install: 24 | npm install 25 | 26 | build: 27 | npm run build 28 | 29 | 30 | build-all: 31 | npm run build:all 32 | 33 | test: 34 | npm run test 35 | 36 | lint: 37 | npm run lint 38 | 39 | format: 40 | npm run format 41 | 42 | preflight: 43 | npm run preflight 44 | 45 | clean: 46 | npm run clean 47 | 48 | start: 49 | npm run start 50 | 51 | debug: 52 | npm run debug 53 | 54 | 55 | run-npx: 56 | npx https://github.com/google-gemini/gemini-cli 57 | 58 | create-alias: 59 | scripts/create_alias.sh 60 | -------------------------------------------------------------------------------- /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 'daili-code-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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.yml: -------------------------------------------------------------------------------- 1 | name: Bug Report 2 | description: Report a bug to help us improve Gemini CLI 3 | labels: ['kind/bug', 'status/need-triage'] 4 | body: 5 | - type: markdown 6 | attributes: 7 | value: | 8 | > [!IMPORTANT] 9 | > Thanks for taking the time to fill out this bug report! 10 | > 11 | > Please search **[existing issues](https://github.com/google-gemini/gemini-cli/issues)** to see if an issue already exists for the bug you encountered. 12 | 13 | - type: textarea 14 | id: problem 15 | attributes: 16 | label: What happened? 17 | description: A clear and concise description of what the bug is. 18 | validations: 19 | required: true 20 | 21 | - type: textarea 22 | id: expected 23 | attributes: 24 | label: What did you expect to happen? 25 | validations: 26 | required: true 27 | 28 | - type: textarea 29 | id: info 30 | attributes: 31 | label: Client information 32 | description: Please paste the full text from the `/about` command run from Gemini CLI. Also include which platform (MacOS, Windows, Linux). 33 | value: | 34 |
35 | 36 | ```console 37 | $ gemini /about 38 | # paste output here 39 | ``` 40 | 41 |
42 | validations: 43 | required: true 44 | 45 | - type: textarea 46 | id: login-info 47 | attributes: 48 | label: Login information 49 | description: Describe how you are logging in (e.g., Google Account, API key). 50 | 51 | - type: textarea 52 | id: additional-context 53 | attributes: 54 | label: Anything else we need to know? 55 | description: Add any other context about the problem here. 56 | -------------------------------------------------------------------------------- /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 'daili-code-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 | -------------------------------------------------------------------------------- /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/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 `${Math.round(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/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 { rmSync, readFileSync } from 'fs'; 21 | import { dirname, join } from 'path'; 22 | import { fileURLToPath } from 'url'; 23 | import { globSync } from 'glob'; 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, 'bundle'), { recursive: true, force: true }); 31 | rmSync(join(root, 'packages/cli/src/generated/'), { 32 | recursive: true, 33 | force: true, 34 | }); 35 | const RMRF_OPTIONS = { recursive: true, force: true }; 36 | rmSync(join(root, 'bundle'), RMRF_OPTIONS); 37 | // Dynamically clean dist directories in all workspaces 38 | const rootPackageJson = JSON.parse( 39 | readFileSync(join(root, 'package.json'), 'utf-8'), 40 | ); 41 | for (const workspace of rootPackageJson.workspaces) { 42 | const packages = globSync(join(workspace, 'package.json'), { cwd: root }); 43 | for (const pkgPath of packages) { 44 | const pkgDir = dirname(join(root, pkgPath)); 45 | rmSync(join(pkgDir, 'dist'), RMRF_OPTIONS); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /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 'daili-code-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 | vscodium: 'VSCodium', 23 | windsurf: 'Windsurf', 24 | cursor: 'Cursor', 25 | vim: 'Vim', 26 | neovim: 'Neovim', 27 | }; 28 | 29 | class EditorSettingsManager { 30 | private readonly availableEditors: EditorDisplay[]; 31 | 32 | constructor() { 33 | const editorTypes: EditorType[] = [ 34 | 'zed', 35 | 'vscode', 36 | 'vscodium', 37 | 'windsurf', 38 | 'cursor', 39 | 'vim', 40 | 'neovim', 41 | ]; 42 | this.availableEditors = [ 43 | { 44 | name: 'None', 45 | type: 'not_set', 46 | disabled: false, 47 | }, 48 | ...editorTypes.map((type) => { 49 | const hasEditor = checkHasEditorType(type); 50 | const isAllowedInSandbox = allowEditorTypeInSandbox(type); 51 | 52 | let labelSuffix = !isAllowedInSandbox 53 | ? ' (Not available in sandbox)' 54 | : ''; 55 | labelSuffix = !hasEditor ? ' (Not installed)' : labelSuffix; 56 | 57 | return { 58 | name: EDITOR_DISPLAY_NAMES[type] + labelSuffix, 59 | type, 60 | disabled: !hasEditor || !isAllowedInSandbox, 61 | }; 62 | }), 63 | ]; 64 | } 65 | 66 | getAvailableEditorDisplays(): EditorDisplay[] { 67 | return this.availableEditors; 68 | } 69 | } 70 | 71 | export const editorSettingsManager = new EditorSettingsManager(); 72 | -------------------------------------------------------------------------------- /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 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 } 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 | // npm install if node_modules was removed (e.g. via npm run clean or scripts/clean.js) 29 | if (!existsSync(join(root, 'node_modules'))) { 30 | execSync('npm install', { stdio: 'inherit', cwd: root }); 31 | } 32 | 33 | // build all workspaces/packages 34 | execSync('npm run generate', { stdio: 'inherit', cwd: root }); 35 | execSync('npm run build --workspaces', { stdio: 'inherit', cwd: root }); 36 | 37 | // also build container image if sandboxing is enabled 38 | // skip (-s) npm install + build since we did that above 39 | try { 40 | execSync('node scripts/sandbox_command.js -q', { 41 | stdio: 'inherit', 42 | cwd: root, 43 | }); 44 | if ( 45 | process.env.BUILD_SANDBOX === '1' || 46 | process.env.BUILD_SANDBOX === 'true' 47 | ) { 48 | execSync('node scripts/build_sandbox.js -s', { 49 | stdio: 'inherit', 50 | cwd: root, 51 | }); 52 | } 53 | } catch { 54 | // ignore 55 | } 56 | -------------------------------------------------------------------------------- /packages/core/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "daili-code-core", 3 | "version": "0.1.5", 4 | "description": "Gemini CLI Core", 5 | "repository": { 6 | "type": "git", 7 | "url": "git+https://github.com/nearmetips/DailiCode.git" 8 | }, 9 | "type": "module", 10 | "main": "dist/index.js", 11 | "scripts": { 12 | "build": "node ../../scripts/build_package.js", 13 | "lint": "eslint . --ext .ts,.tsx", 14 | "format": "prettier --write .", 15 | "test": "vitest run", 16 | "test:ci": "vitest run --coverage", 17 | "typecheck": "tsc --noEmit" 18 | }, 19 | "license": "Apache-2.0", 20 | "files": [ 21 | "dist" 22 | ], 23 | "dependencies": { 24 | "@google/genai": "^1.8.0", 25 | "@modelcontextprotocol/sdk": "^1.11.0", 26 | "@opentelemetry/api": "^1.9.0", 27 | "@opentelemetry/exporter-logs-otlp-grpc": "^0.52.0", 28 | "@opentelemetry/exporter-metrics-otlp-grpc": "^0.52.0", 29 | "@opentelemetry/exporter-trace-otlp-grpc": "^0.52.0", 30 | "@opentelemetry/instrumentation-http": "^0.52.0", 31 | "@opentelemetry/sdk-node": "^0.52.0", 32 | "@types/glob": "^8.1.0", 33 | "@types/html-to-text": "^9.0.4", 34 | "ajv": "^8.17.1", 35 | "diff": "^7.0.0", 36 | "dotenv": "^16.6.1", 37 | "gaxios": "^6.1.1", 38 | "glob": "^10.4.5", 39 | "google-auth-library": "^9.11.0", 40 | "html-to-text": "^9.0.5", 41 | "ignore": "^7.0.0", 42 | "micromatch": "^4.0.8", 43 | "open": "^10.1.2", 44 | "shell-quote": "^1.8.3", 45 | "simple-git": "^3.28.0", 46 | "strip-ansi": "^7.1.0", 47 | "undici": "^7.10.0", 48 | "ws": "^8.18.0", 49 | "openai": "^5.8.2" 50 | }, 51 | "devDependencies": { 52 | "@types/diff": "^7.0.2", 53 | "@types/dotenv": "^6.1.1", 54 | "@types/micromatch": "^4.0.8", 55 | "@types/minimatch": "^5.1.2", 56 | "@types/ws": "^8.5.10", 57 | "typescript": "^5.3.3", 58 | "vitest": "^3.1.1" 59 | }, 60 | "engines": { 61 | "node": ">=20" 62 | } 63 | } -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /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 | 28 | interface ResponseData { 29 | error?: { 30 | code?: number; 31 | message?: string; 32 | }; 33 | } 34 | 35 | export function toFriendlyError(error: unknown): unknown { 36 | if (error instanceof GaxiosError) { 37 | const data = parseResponseData(error); 38 | if (data.error && data.error.message && data.error.code) { 39 | switch (data.error.code) { 40 | case 400: 41 | return new BadRequestError(data.error.message); 42 | case 401: 43 | return new UnauthorizedError(data.error.message); 44 | case 403: 45 | // It's import to pass the message here since it might 46 | // explain the cause like "the cloud project you're 47 | // using doesn't have code assist enabled". 48 | return new ForbiddenError(data.error.message); 49 | default: 50 | } 51 | } 52 | } 53 | return error; 54 | } 55 | 56 | function parseResponseData(error: GaxiosError): ResponseData { 57 | // Inexplicably, Gaxios sometimes doesn't JSONify the response data. 58 | if (typeof error.response?.data === 'string') { 59 | return JSON.parse(error.response?.data) as ResponseData; 60 | } 61 | return typeof error.response?.data as ResponseData; 62 | } 63 | -------------------------------------------------------------------------------- /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 | originalMethod.apply(console, args); 35 | 36 | // Then, if it's not a debug message or debugMode is on, pass to onNewMessage 37 | if (type !== 'debug' || debugMode) { 38 | onNewMessage({ 39 | type, 40 | content: formatArgs(args), 41 | count: 1, 42 | }); 43 | } 44 | }; 45 | 46 | console.log = patchConsoleMethod('log', originalConsoleLog); 47 | console.warn = patchConsoleMethod('warn', originalConsoleWarn); 48 | console.error = patchConsoleMethod('error', originalConsoleError); 49 | console.debug = patchConsoleMethod('debug', originalConsoleDebug); 50 | 51 | return () => { 52 | console.log = originalConsoleLog; 53 | console.warn = originalConsoleWarn; 54 | console.error = originalConsoleError; 55 | console.debug = originalConsoleDebug; 56 | }; 57 | }, [onNewMessage, debugMode]); 58 | }; 59 | -------------------------------------------------------------------------------- /packages/core/src/utils/user_id.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 { getInstallationId, getGoogleAccountId } from './user_id.js'; 9 | 10 | describe('user_id', () => { 11 | describe('getInstallationId', () => { 12 | it('should return a valid UUID format string', () => { 13 | const installationId = getInstallationId(); 14 | 15 | expect(installationId).toBeDefined(); 16 | expect(typeof installationId).toBe('string'); 17 | expect(installationId.length).toBeGreaterThan(0); 18 | 19 | // Should return the same ID on subsequent calls (consistent) 20 | const secondCall = getInstallationId(); 21 | expect(secondCall).toBe(installationId); 22 | }); 23 | }); 24 | 25 | describe('getGoogleAccountId', () => { 26 | it('should return a non-empty string', async () => { 27 | const result = await getGoogleAccountId(); 28 | 29 | expect(result).toBeDefined(); 30 | expect(typeof result).toBe('string'); 31 | 32 | // Should be consistent on subsequent calls 33 | const secondCall = await getGoogleAccountId(); 34 | expect(secondCall).toBe(result); 35 | }); 36 | 37 | it('should return empty string when no Google Account ID is cached, or a valid ID when cached', async () => { 38 | // The function can return either an empty string (if no cached ID) or a valid Google Account ID (if cached) 39 | const googleAccountIdResult = await getGoogleAccountId(); 40 | 41 | expect(googleAccountIdResult).toBeDefined(); 42 | expect(typeof googleAccountIdResult).toBe('string'); 43 | 44 | // Should be either empty string or a numeric string (Google Account ID) 45 | if (googleAccountIdResult !== '') { 46 | // If we have a cached ID, it should be a numeric string 47 | expect(googleAccountIdResult).toMatch(/^\d+$/); 48 | } 49 | }); 50 | }); 51 | }); 52 | -------------------------------------------------------------------------------- /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 'daili-code-core'; 8 | import { loadEnvironment } from './settings.js'; 9 | 10 | export const validateAuthMethod = (authMethod: string): string | null => { 11 | loadEnvironment(); 12 | if ( 13 | authMethod === AuthType.LOGIN_WITH_GOOGLE || 14 | authMethod === AuthType.CLOUD_SHELL 15 | ) { 16 | return null; 17 | } 18 | 19 | if (authMethod === AuthType.USE_GEMINI) { 20 | if (!process.env.GEMINI_API_KEY) { 21 | return 'GEMINI_API_KEY environment variable not found. Add that to your environment and try again (no reload needed if using .env)!'; 22 | } 23 | return null; 24 | } 25 | 26 | if (authMethod === AuthType.USE_VERTEX_AI) { 27 | const hasVertexProjectLocationConfig = 28 | !!process.env.GOOGLE_CLOUD_PROJECT && !!process.env.GOOGLE_CLOUD_LOCATION; 29 | const hasGoogleApiKey = !!process.env.GOOGLE_API_KEY; 30 | if (!hasVertexProjectLocationConfig && !hasGoogleApiKey) { 31 | return ( 32 | 'When using Vertex AI, you must specify either:\n' + 33 | '• GOOGLE_CLOUD_PROJECT and GOOGLE_CLOUD_LOCATION environment variables.\n' + 34 | '• GOOGLE_API_KEY environment variable (if using express mode).\n' + 35 | 'Update your environment and try again (no reload needed if using .env)!' 36 | ); 37 | } 38 | return null; 39 | } 40 | 41 | if (authMethod === AuthType.CUSTOM_LLM_API) { 42 | if (!process.env.CUSTOM_LLM_ENDPOINT) { 43 | return 'CUSTOM_LLM_ENDPOINT environment variable not found. Add that to your .env and try again, no reload needed!'; 44 | } 45 | 46 | if (!process.env.CUSTOM_LLM_MODEL_NAME) { 47 | return 'CUSTOM_LLM_MODEL_NAME environment variable not found. Add that to your .env and try again, no reload needed!'; 48 | } 49 | return null; 50 | } 51 | 52 | return 'Invalid auth method selected.'; 53 | }; 54 | -------------------------------------------------------------------------------- /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 'daili-code-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/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/cli/src/api/util.ts: -------------------------------------------------------------------------------- 1 | import { AgentResult } from './types.js'; 2 | import { 3 | ServerGeminiStreamEvent, 4 | GeminiEventType, 5 | } from 'daili-code-core'; 6 | 7 | export function logStream(chunk: AgentResult) { 8 | if (!chunk) { 9 | return; 10 | } 11 | if (chunk.type === 'content') { 12 | process.stdout.write(chunk.content || ''); 13 | } 14 | if (chunk.type === 'tool_call') { 15 | console.log('=== excute tool ==='); 16 | console.log(JSON.stringify(chunk.toolCall, null, 2)); 17 | console.log(''); 18 | } 19 | } 20 | 21 | export function processStreamEvent( 22 | event: ServerGeminiStreamEvent, 23 | ): AgentResult | null { 24 | const timestamp = Date.now(); 25 | switch (event.type) { 26 | case GeminiEventType.Content: 27 | return { 28 | type: 'content', 29 | content: event.value, 30 | timestamp, 31 | }; 32 | 33 | case GeminiEventType.ToolCallRequest: 34 | return { 35 | type: 'tool_call', 36 | toolCall: { 37 | name: event.value.name, 38 | args: event.value.args, 39 | }, 40 | timestamp, 41 | }; 42 | 43 | case GeminiEventType.ToolCallResponse: 44 | return { 45 | type: 'tool_call', 46 | toolCall: { 47 | name: 'response', 48 | args: {}, 49 | result: event.value.responseParts, 50 | }, 51 | timestamp, 52 | }; 53 | 54 | case GeminiEventType.Thought: 55 | return { 56 | type: 'thought', 57 | thought: { 58 | summary: event.value.subject, 59 | details: event.value.description, 60 | }, 61 | timestamp, 62 | }; 63 | 64 | case GeminiEventType.Error: 65 | return { 66 | type: 'error', 67 | error: event.value.error.message, 68 | timestamp, 69 | }; 70 | 71 | case GeminiEventType.UserCancelled: 72 | return { 73 | type: 'user_cancelled', 74 | timestamp, 75 | }; 76 | 77 | default: 78 | return null; 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /packages/cli/src/ui/privacy/CloudPaidPrivacyNotice.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright 2025 Google LLC 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | import { Box, Newline, Text, useInput } from 'ink'; 8 | import { Colors } from '../colors.js'; 9 | 10 | interface CloudPaidPrivacyNoticeProps { 11 | onExit: () => void; 12 | } 13 | 14 | export const CloudPaidPrivacyNotice = ({ 15 | onExit, 16 | }: CloudPaidPrivacyNoticeProps) => { 17 | useInput((input, key) => { 18 | if (key.escape) { 19 | onExit(); 20 | } 21 | }); 22 | 23 | return ( 24 | 25 | 26 | Vertex AI Notice 27 | 28 | 29 | 30 | Service Specific Terms[1] are 31 | incorporated into the agreement under which Google has agreed to provide 32 | Google Cloud Platform[2] to 33 | Customer (the “Agreement”). If the Agreement authorizes the resale or 34 | supply of Google Cloud Platform under a Google Cloud partner or reseller 35 | program, then except for in the section entitled “Partner-Specific 36 | Terms”, all references to Customer in the Service Specific Terms mean 37 | Partner or Reseller (as applicable), and all references to Customer Data 38 | in the Service Specific Terms mean Partner Data. Capitalized terms used 39 | but not defined in the Service Specific Terms have the meaning given to 40 | them in the Agreement. 41 | 42 | 43 | 44 | [1]{' '} 45 | https://cloud.google.com/terms/service-terms 46 | 47 | 48 | [2]{' '} 49 | https://cloud.google.com/terms/services 50 | 51 | 52 | Press Esc to exit. 53 | 54 | ); 55 | }; 56 | -------------------------------------------------------------------------------- /packages/cli/src/ui/privacy/GeminiPrivacyNotice.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright 2025 Google LLC 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | import { Box, Newline, Text, useInput } from 'ink'; 8 | import { Colors } from '../colors.js'; 9 | 10 | interface GeminiPrivacyNoticeProps { 11 | onExit: () => void; 12 | } 13 | 14 | export const GeminiPrivacyNotice = ({ onExit }: GeminiPrivacyNoticeProps) => { 15 | useInput((input, key) => { 16 | if (key.escape) { 17 | onExit(); 18 | } 19 | }); 20 | 21 | return ( 22 | 23 | 24 | Gemini API Key Notice 25 | 26 | 27 | 28 | By using the Gemini API[1], 29 | Google AI Studio 30 | [2], and the other Google 31 | developer services that reference these terms (collectively, the 32 | "APIs" or "Services"), you are agreeing to Google 33 | APIs Terms of Service (the "API Terms") 34 | [3], and the Gemini API 35 | Additional Terms of Service (the "Additional Terms") 36 | [4]. 37 | 38 | 39 | 40 | [1]{' '} 41 | https://ai.google.dev/docs/gemini_api_overview 42 | 43 | 44 | [2] https://aistudio.google.com/ 45 | 46 | 47 | [3]{' '} 48 | https://developers.google.com/terms 49 | 50 | 51 | [4]{' '} 52 | https://ai.google.dev/gemini-api/terms 53 | 54 | 55 | Press Esc to exit. 56 | 57 | ); 58 | }; 59 | -------------------------------------------------------------------------------- /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 'daili-code-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/components/LoadingIndicator.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright 2025 Google LLC 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | import { ThoughtSummary } from 'daili-code-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 | import { formatDuration } from '../utils/formatters.js'; 15 | 16 | interface LoadingIndicatorProps { 17 | currentLoadingPhrase?: string; 18 | elapsedTime: number; 19 | rightContent?: React.ReactNode; 20 | thought?: ThoughtSummary | null; 21 | } 22 | 23 | export const LoadingIndicator: React.FC = ({ 24 | currentLoadingPhrase, 25 | elapsedTime, 26 | rightContent, 27 | thought, 28 | }) => { 29 | const streamingState = useStreamingContext(); 30 | 31 | if (streamingState === StreamingState.Idle) { 32 | return null; 33 | } 34 | 35 | const primaryText = thought?.subject || currentLoadingPhrase; 36 | 37 | return ( 38 | 39 | {/* Main loading line */} 40 | 41 | 42 | 49 | 50 | {primaryText && {primaryText}} 51 | 52 | {streamingState === StreamingState.WaitingForConfirmation 53 | ? '' 54 | : ` (esc to cancel, ${elapsedTime < 60 ? `${elapsedTime}s` : formatDuration(elapsedTime * 1000)})`} 55 | 56 | {/* Spacer */} 57 | {rightContent && {rightContent}} 58 | 59 | 60 | ); 61 | }; 62 | -------------------------------------------------------------------------------- /packages/cli/src/ui/utils/displayUtils.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 { 9 | getStatusColor, 10 | TOOL_SUCCESS_RATE_HIGH, 11 | TOOL_SUCCESS_RATE_MEDIUM, 12 | USER_AGREEMENT_RATE_HIGH, 13 | USER_AGREEMENT_RATE_MEDIUM, 14 | CACHE_EFFICIENCY_HIGH, 15 | CACHE_EFFICIENCY_MEDIUM, 16 | } from './displayUtils.js'; 17 | import { Colors } from '../colors.js'; 18 | 19 | describe('displayUtils', () => { 20 | describe('getStatusColor', () => { 21 | const thresholds = { 22 | green: 80, 23 | yellow: 50, 24 | }; 25 | 26 | it('should return green for values >= green threshold', () => { 27 | expect(getStatusColor(90, thresholds)).toBe(Colors.AccentGreen); 28 | expect(getStatusColor(80, thresholds)).toBe(Colors.AccentGreen); 29 | }); 30 | 31 | it('should return yellow for values < green and >= yellow threshold', () => { 32 | expect(getStatusColor(79, thresholds)).toBe(Colors.AccentYellow); 33 | expect(getStatusColor(50, thresholds)).toBe(Colors.AccentYellow); 34 | }); 35 | 36 | it('should return red for values < yellow threshold', () => { 37 | expect(getStatusColor(49, thresholds)).toBe(Colors.AccentRed); 38 | expect(getStatusColor(0, thresholds)).toBe(Colors.AccentRed); 39 | }); 40 | 41 | it('should return defaultColor for values < yellow threshold when provided', () => { 42 | expect( 43 | getStatusColor(49, thresholds, { defaultColor: Colors.Foreground }), 44 | ).toBe(Colors.Foreground); 45 | }); 46 | }); 47 | 48 | describe('Threshold Constants', () => { 49 | it('should have the correct values', () => { 50 | expect(TOOL_SUCCESS_RATE_HIGH).toBe(95); 51 | expect(TOOL_SUCCESS_RATE_MEDIUM).toBe(85); 52 | expect(USER_AGREEMENT_RATE_HIGH).toBe(75); 53 | expect(USER_AGREEMENT_RATE_MEDIUM).toBe(45); 54 | expect(CACHE_EFFICIENCY_HIGH).toBe(40); 55 | expect(CACHE_EFFICIENCY_MEDIUM).toBe(15); 56 | }); 57 | }); 58 | }); 59 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | export * from './code_assist/server.js'; 25 | export * from './code_assist/types.js'; 26 | 27 | // Export utilities 28 | export * from './utils/paths.js'; 29 | export * from './utils/schemaValidator.js'; 30 | export * from './utils/errors.js'; 31 | export * from './utils/getFolderStructure.js'; 32 | export * from './utils/memoryDiscovery.js'; 33 | export * from './utils/gitIgnoreParser.js'; 34 | export * from './utils/editor.js'; 35 | 36 | // Export services 37 | export * from './services/fileDiscoveryService.js'; 38 | export * from './services/gitService.js'; 39 | 40 | // Export base tool definitions 41 | export * from './tools/tools.js'; 42 | export * from './tools/tool-registry.js'; 43 | 44 | // Export specific tool logic 45 | export * from './tools/read-file.js'; 46 | export * from './tools/ls.js'; 47 | export * from './tools/grep.js'; 48 | export * from './tools/glob.js'; 49 | export * from './tools/edit.js'; 50 | export * from './tools/write-file.js'; 51 | export * from './tools/web-fetch.js'; 52 | export * from './tools/memoryTool.js'; 53 | export * from './tools/shell.js'; 54 | export * from './tools/web-search.js'; 55 | export * from './tools/read-many-files.js'; 56 | export * from './tools/mcp-client.js'; 57 | export * from './tools/mcp-tool.js'; 58 | 59 | // Export telemetry functions 60 | export * from './telemetry/index.js'; 61 | export { sessionId } from './utils/session.js'; 62 | -------------------------------------------------------------------------------- /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/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, vi } from 'vitest'; 9 | import { SessionSummaryDisplay } from './SessionSummaryDisplay.js'; 10 | import * as SessionContext from '../contexts/SessionContext.js'; 11 | import { SessionMetrics } from '../contexts/SessionContext.js'; 12 | 13 | vi.mock('../contexts/SessionContext.js', async (importOriginal) => { 14 | const actual = await importOriginal(); 15 | return { 16 | ...actual, 17 | useSessionStats: vi.fn(), 18 | }; 19 | }); 20 | 21 | const useSessionStatsMock = vi.mocked(SessionContext.useSessionStats); 22 | 23 | const renderWithMockedStats = (metrics: SessionMetrics) => { 24 | useSessionStatsMock.mockReturnValue({ 25 | stats: { 26 | sessionStartTime: new Date(), 27 | metrics, 28 | lastPromptTokenCount: 0, 29 | }, 30 | }); 31 | 32 | return render(); 33 | }; 34 | 35 | describe('', () => { 36 | it('renders the summary display with a title', () => { 37 | const metrics: SessionMetrics = { 38 | models: { 39 | 'gemini-2.5-pro': { 40 | api: { totalRequests: 10, totalErrors: 1, totalLatencyMs: 50234 }, 41 | tokens: { 42 | prompt: 1000, 43 | candidates: 2000, 44 | total: 3500, 45 | cached: 500, 46 | thoughts: 300, 47 | tool: 200, 48 | }, 49 | }, 50 | }, 51 | tools: { 52 | totalCalls: 0, 53 | totalSuccess: 0, 54 | totalFail: 0, 55 | totalDurationMs: 0, 56 | totalDecisions: { accept: 0, reject: 0, modify: 0 }, 57 | byName: {}, 58 | }, 59 | }; 60 | 61 | const { lastFrame } = renderWithMockedStats(metrics); 62 | const output = lastFrame(); 63 | 64 | expect(output).toContain('Agent powering down. Goodbye!'); 65 | expect(output).toMatchSnapshot(); 66 | }); 67 | }); 68 | -------------------------------------------------------------------------------- /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/test-utils/mockCommandContext.test.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright 2025 Google LLC 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | import { vi, describe, it, expect } from 'vitest'; 8 | import { createMockCommandContext } from './mockCommandContext.js'; 9 | 10 | describe('createMockCommandContext', () => { 11 | it('should return a valid CommandContext object with default mocks', () => { 12 | const context = createMockCommandContext(); 13 | 14 | // Just a few spot checks to ensure the structure is correct 15 | // and functions are mocks. 16 | expect(context).toBeDefined(); 17 | expect(context.ui.addItem).toBeInstanceOf(Function); 18 | expect(vi.isMockFunction(context.ui.addItem)).toBe(true); 19 | }); 20 | 21 | it('should apply top-level overrides correctly', () => { 22 | const mockClear = vi.fn(); 23 | const overrides = { 24 | ui: { 25 | clear: mockClear, 26 | }, 27 | }; 28 | 29 | const context = createMockCommandContext(overrides); 30 | 31 | // Call the function to see if the override was used 32 | context.ui.clear(); 33 | 34 | // Assert that our specific mock was called, not the default 35 | expect(mockClear).toHaveBeenCalled(); 36 | // And that other defaults are still in place 37 | expect(vi.isMockFunction(context.ui.addItem)).toBe(true); 38 | }); 39 | 40 | it('should apply deeply nested overrides correctly', () => { 41 | // This is the most important test for factory's logic. 42 | const mockConfig = { 43 | getProjectRoot: () => '/test/project', 44 | getModel: () => 'gemini-pro', 45 | }; 46 | 47 | const overrides = { 48 | services: { 49 | config: mockConfig, 50 | }, 51 | }; 52 | 53 | const context = createMockCommandContext(overrides); 54 | 55 | expect(context.services.config).toBeDefined(); 56 | expect(context.services.config?.getModel()).toBe('gemini-pro'); 57 | expect(context.services.config?.getProjectRoot()).toBe('/test/project'); 58 | 59 | // Verify a default property on the same nested object is still there 60 | expect(context.services.logger).toBeDefined(); 61 | }); 62 | }); 63 | -------------------------------------------------------------------------------- /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/components/__snapshots__/SessionSummaryDisplay.test.tsx.snap: -------------------------------------------------------------------------------- 1 | // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html 2 | 3 | exports[` > renders the summary display with a title 1`] = ` 4 | "╭──────────────────────────────────────────────────────────────────────────────────────────────────╮ 5 | │ │ 6 | │ Agent powering down. Goodbye! │ 7 | │ │ 8 | │ Performance │ 9 | │ Wall Time: 1h 23m 45s │ 10 | │ Agent Active: 50.2s │ 11 | │ » API Time: 50.2s (100.0%) │ 12 | │ » Tool Time: 0s (0.0%) │ 13 | │ │ 14 | │ │ 15 | │ Model Usage Reqs Input Tokens Output Tokens │ 16 | │ ─────────────────────────────────────────────────────────────── │ 17 | │ gemini-2.5-pro 10 1,000 2,000 │ 18 | │ │ 19 | │ Savings Highlight: 500 (50.0%) of input tokens were served from the cache, reducing costs. │ 20 | │ │ 21 | │ » Tip: For a full token breakdown, run \`/stats model\`. │ 22 | │ │ 23 | ╰──────────────────────────────────────────────────────────────────────────────────────────────────╯" 24 | `; 25 | -------------------------------------------------------------------------------- /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 'daili-code-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/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/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/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 | -------------------------------------------------------------------------------- /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/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 (const byte of sample) { 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 (byte === 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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | 33 | // Build API bundle 34 | 35 | const commonAPIOptions = { 36 | entryPoints: ['index.ts'], 37 | bundle: true, 38 | platform: 'node', 39 | define: { 40 | 'process.env.CLI_VERSION': JSON.stringify(pkg.version), 41 | }, 42 | external: [ 43 | '@google/genai', 44 | 'assert', 45 | 'buffer', 46 | 'child_process', 47 | 'crypto', 48 | 'events', 49 | 'fs', 50 | 'http', 51 | 'https', 52 | 'net', 53 | 'os', 54 | 'path', 55 | 'stream', 56 | 'tty', 57 | 'url', 58 | 'util', 59 | 'zlib', 60 | ], 61 | }; 62 | esbuild 63 | .build({ 64 | ...commonAPIOptions, 65 | outfile: 'bundle/api.js', 66 | format: 'esm', 67 | banner: { 68 | 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);`, 69 | }, 70 | }) 71 | .catch(() => process.exit(1)); 72 | esbuild 73 | .build({ 74 | ...commonAPIOptions, 75 | outfile: 'bundle/api.cjs', 76 | format: 'cjs', 77 | define: { 'import.meta.url': '_importMetaUrl' }, 78 | banner: { 79 | js: "const _importMetaUrl=require('url').pathToFileURL(__filename)", 80 | }, 81 | }) 82 | .catch(() => process.exit(1)); 83 | -------------------------------------------------------------------------------- /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 | "excludeTools": ["run_shell_command"] 33 | } 34 | ``` 35 | 36 | - `name`: The name of the extension. This is used to uniquely identify the extension. This should match the name of your extension directory. 37 | - `version`: The version of the extension. 38 | - `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. 39 | - `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. 40 | - `excludeTools`: An array of tool names to exclude from the model. You can also specify command-specific restrictions for tools that support it, like the `run_shell_command` tool. For example, `"excludeTools": ["run_shell_command(rm -rf)"]` will block the `rm -rf` command. 41 | 42 | When Gemini CLI starts, it loads all the extensions and merges their configurations. If there are any conflicts, the workspace configuration takes precedence. 43 | -------------------------------------------------------------------------------- /packages/cli/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "daili-code", 3 | "version": "0.1.5", 4 | "description": "Daili Code", 5 | "repository": { 6 | "type": "git", 7 | "url": "git+https://github.com/nearmetips/daili-code.git" 8 | }, 9 | "type": "module", 10 | "main": "dist/index.js", 11 | "bin": { 12 | "dlc": "dist/index.js" 13 | }, 14 | "scripts": { 15 | "build": "node ../../scripts/build_package.js", 16 | "start": "node dist/index.js", 17 | "debug": "node --inspect-brk dist/index.js", 18 | "lint": "eslint . --ext .ts,.tsx", 19 | "format": "prettier --write .", 20 | "test": "vitest run", 21 | "test:ci": "vitest run --coverage", 22 | "typecheck": "tsc --noEmit" 23 | }, 24 | "files": [ 25 | "dist" 26 | ], 27 | "config": { 28 | "sandboxImageUri": "us-docker.pkg.dev/gemini-code-dev/gemini-cli/sandbox:0.1.9" 29 | }, 30 | "dependencies": { 31 | "daili-code-core": "file:../core", 32 | "@types/update-notifier": "^6.0.8", 33 | "command-exists": "^1.2.9", 34 | "diff": "^7.0.0", 35 | "dotenv": "^16.6.1", 36 | "gaxios": "^6.1.1", 37 | "glob": "^10.4.1", 38 | "highlight.js": "^11.11.1", 39 | "ink": "^6.0.1", 40 | "ink-big-text": "^2.0.0", 41 | "ink-gradient": "^3.0.0", 42 | "ink-link": "^4.1.0", 43 | "ink-select-input": "^6.2.0", 44 | "ink-spinner": "^5.0.0", 45 | "ink-text-input": "^6.0.0", 46 | "lowlight": "^3.3.0", 47 | "mime-types": "^2.1.4", 48 | "open": "^10.1.2", 49 | "react": "^19.1.0", 50 | "read-package-up": "^11.0.0", 51 | "shell-quote": "^1.8.3", 52 | "string-width": "^7.1.0", 53 | "strip-ansi": "^7.1.0", 54 | "strip-json-comments": "^3.1.1", 55 | "update-notifier": "^7.3.1", 56 | "yargs": "^17.7.2" 57 | }, 58 | "devDependencies": { 59 | "@babel/runtime": "^7.27.6", 60 | "@testing-library/react": "^16.3.0", 61 | "@types/command-exists": "^1.2.3", 62 | "@types/diff": "^7.0.2", 63 | "@types/dotenv": "^6.1.1", 64 | "@types/node": "^20.11.24", 65 | "@types/react": "^19.1.8", 66 | "@types/react-dom": "^19.1.6", 67 | "@types/semver": "^7.7.0", 68 | "@types/shell-quote": "^1.7.5", 69 | "@types/yargs": "^17.0.32", 70 | "ink-testing-library": "^4.0.0", 71 | "jsdom": "^26.1.0", 72 | "pretty-format": "^30.0.2", 73 | "react-dom": "^19.1.0", 74 | "typescript": "^5.3.3", 75 | "vitest": "^3.1.1" 76 | }, 77 | "engines": { 78 | "node": ">=20" 79 | } 80 | } -------------------------------------------------------------------------------- /packages/cli/src/utils/userStartupWarnings.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 { getUserStartupWarnings } from './userStartupWarnings.js'; 9 | import * as os from 'os'; 10 | import fs from 'fs/promises'; 11 | 12 | vi.mock('os', () => ({ 13 | default: { homedir: vi.fn() }, 14 | homedir: vi.fn(), 15 | })); 16 | 17 | vi.mock('fs/promises', () => ({ 18 | default: { realpath: vi.fn() }, 19 | })); 20 | 21 | describe('getUserStartupWarnings', () => { 22 | const homeDir = '/home/user'; 23 | 24 | beforeEach(() => { 25 | vi.mocked(os.homedir).mockReturnValue(homeDir); 26 | vi.mocked(fs.realpath).mockImplementation(async (path) => path.toString()); 27 | }); 28 | 29 | afterEach(() => { 30 | vi.clearAllMocks(); 31 | }); 32 | 33 | describe('home directory check', () => { 34 | it('should return a warning when running in home directory', async () => { 35 | vi.mocked(fs.realpath) 36 | .mockResolvedValueOnce(homeDir) 37 | .mockResolvedValueOnce(homeDir); 38 | 39 | const warnings = await getUserStartupWarnings(homeDir); 40 | 41 | expect(warnings).toContainEqual( 42 | expect.stringContaining('home directory'), 43 | ); 44 | }); 45 | 46 | it('should not return a warning when running in a project directory', async () => { 47 | vi.mocked(fs.realpath) 48 | .mockResolvedValueOnce('/some/project/path') 49 | .mockResolvedValueOnce(homeDir); 50 | 51 | const warnings = await getUserStartupWarnings('/some/project/path'); 52 | expect(warnings).not.toContainEqual( 53 | expect.stringContaining('home directory'), 54 | ); 55 | }); 56 | 57 | it('should handle errors when checking directory', async () => { 58 | vi.mocked(fs.realpath) 59 | .mockRejectedValueOnce(new Error('FS error')) 60 | .mockResolvedValueOnce(homeDir); 61 | 62 | const warnings = await getUserStartupWarnings('/error/path'); 63 | expect(warnings).toContainEqual( 64 | expect.stringContaining('Could not verify'), 65 | ); 66 | }); 67 | }); 68 | 69 | // // Example of how to add a new check: 70 | // describe('node version check', () => { 71 | // // Tests for node version check would go here 72 | // // This shows how easy it is to add new test sections 73 | // }); 74 | }); 75 | -------------------------------------------------------------------------------- /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](./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/utils/schemaValidator.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright 2025 Google LLC 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | import { Schema } from '@google/genai'; 8 | import * as ajv from 'ajv'; 9 | 10 | // 创建 Ajv 实例进行 JSON Schema 验证 11 | const ajValidator = new (ajv as any).default(); 12 | 13 | /** 14 | * Simple utility to validate objects against JSON Schemas 15 | */ 16 | export class SchemaValidator { 17 | /** 18 | * Returns null if the data confroms to the schema described by schema (or if schema 19 | * is null). Otherwise, returns a string describing the error. 20 | */ 21 | static validate(schema: Schema | undefined, data: unknown): string | null { 22 | if (!schema) { 23 | return null; 24 | } 25 | if (typeof data !== 'object' || data === null) { 26 | return 'Value of params must be an object'; 27 | } 28 | const validate = ajValidator.compile(this.toObjectSchema(schema)); 29 | const valid = validate(data); 30 | if (!valid && validate.errors) { 31 | return ajValidator.errorsText(validate.errors, { dataVar: 'params' }); 32 | } 33 | return null; 34 | } 35 | 36 | /** 37 | * Converts @google/genai's Schema to an object compatible with avj. 38 | * This is necessry because it represents Types as an Enum (with 39 | * UPPERCASE values) and minItems and minLength as strings, when they should be numbers. 40 | */ 41 | private static toObjectSchema(schema: Schema): object { 42 | const newSchema: Record = { ...schema }; 43 | if (newSchema.anyOf && Array.isArray(newSchema.anyOf)) { 44 | newSchema.anyOf = newSchema.anyOf.map((v) => this.toObjectSchema(v)); 45 | } 46 | if (newSchema.items) { 47 | newSchema.items = this.toObjectSchema(newSchema.items); 48 | } 49 | if (newSchema.properties && typeof newSchema.properties === 'object') { 50 | const newProperties: Record = {}; 51 | for (const [key, value] of Object.entries(newSchema.properties)) { 52 | newProperties[key] = this.toObjectSchema(value as Schema); 53 | } 54 | newSchema.properties = newProperties; 55 | } 56 | if (newSchema.type) { 57 | newSchema.type = String(newSchema.type).toLowerCase(); 58 | } 59 | if (newSchema.minItems) { 60 | newSchema.minItems = Number(newSchema.minItems); 61 | } 62 | if (newSchema.minLength) { 63 | newSchema.minLength = Number(newSchema.minLength); 64 | } 65 | return newSchema; 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /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: 128, 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 | -------------------------------------------------------------------------------- /packages/cli/src/ui/hooks/useAuthCommand.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright 2025 Google LLC 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | import { useState, useCallback, useEffect } from 'react'; 8 | import { LoadedSettings, SettingScope } from '../../config/settings.js'; 9 | import { 10 | AuthType, 11 | Config, 12 | clearCachedCredentialFile, 13 | getErrorMessage, 14 | } from 'daili-code-core'; 15 | 16 | function isInitAuth(settings: LoadedSettings) { 17 | if (process.env.USE_CUSTOM_LLM && process.env.USE_CUSTOM_LLM !== 'false') { 18 | return false; 19 | } 20 | if (settings.merged.selectedAuthType !== AuthType.CUSTOM_LLM_API) { 21 | return settings.merged.selectedAuthType === undefined; 22 | } 23 | return true; 24 | } 25 | 26 | export const useAuthCommand = ( 27 | settings: LoadedSettings, 28 | setAuthError: (error: string | null) => void, 29 | config: Config, 30 | ) => { 31 | const [isAuthDialogOpen, setIsAuthDialogOpen] = useState( 32 | isInitAuth(settings), 33 | ); 34 | 35 | const openAuthDialog = useCallback(() => { 36 | setIsAuthDialogOpen(true); 37 | }, []); 38 | 39 | const [isAuthenticating, setIsAuthenticating] = useState(false); 40 | 41 | useEffect(() => { 42 | const authFlow = async () => { 43 | const authType = settings.merged.selectedAuthType; 44 | if (isAuthDialogOpen || !authType) { 45 | return; 46 | } 47 | 48 | try { 49 | setIsAuthenticating(true); 50 | await config.refreshAuth(authType); 51 | console.log(`Authenticated via "${authType}".`); 52 | } catch (e) { 53 | setAuthError(`Failed to login. Message: ${getErrorMessage(e)}`); 54 | openAuthDialog(); 55 | } finally { 56 | setIsAuthenticating(false); 57 | } 58 | }; 59 | 60 | void authFlow(); 61 | }, [isAuthDialogOpen, settings, config, setAuthError, openAuthDialog]); 62 | 63 | const handleAuthSelect = useCallback( 64 | async (authType: AuthType | undefined, scope: SettingScope) => { 65 | if (authType) { 66 | await clearCachedCredentialFile(); 67 | settings.setValue(scope, 'selectedAuthType', authType); 68 | } 69 | setIsAuthDialogOpen(false); 70 | setAuthError(null); 71 | }, 72 | [settings, setAuthError], 73 | ); 74 | 75 | const cancelAuthentication = useCallback(() => { 76 | setIsAuthenticating(false); 77 | }, []); 78 | 79 | return { 80 | isAuthDialogOpen, 81 | openAuthDialog, 82 | handleAuthSelect, 83 | isAuthenticating, 84 | cancelAuthentication, 85 | }; 86 | }; 87 | -------------------------------------------------------------------------------- /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 'daili-code-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 | -------------------------------------------------------------------------------- /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 { 8 | ClientMetadata, 9 | GeminiUserTier, 10 | LoadCodeAssistResponse, 11 | OnboardUserRequest, 12 | UserTierId, 13 | } from './types.js'; 14 | import { CodeAssistServer } from './server.js'; 15 | import { OAuth2Client } from 'google-auth-library'; 16 | 17 | export class ProjectIdRequiredError extends Error { 18 | constructor() { 19 | super( 20 | 'This account requires setting the GOOGLE_CLOUD_PROJECT env var. See https://goo.gle/gemini-cli-auth-docs#workspace-gca', 21 | ); 22 | } 23 | } 24 | 25 | /** 26 | * 27 | * @param projectId the user's project id, if any 28 | * @returns the user's actual project id 29 | */ 30 | export async function setupUser(client: OAuth2Client): Promise { 31 | let projectId = process.env.GOOGLE_CLOUD_PROJECT || undefined; 32 | const caServer = new CodeAssistServer(client, projectId); 33 | 34 | const clientMetadata: ClientMetadata = { 35 | ideType: 'IDE_UNSPECIFIED', 36 | platform: 'PLATFORM_UNSPECIFIED', 37 | pluginType: 'GEMINI', 38 | duetProject: projectId, 39 | }; 40 | 41 | const loadRes = await caServer.loadCodeAssist({ 42 | cloudaicompanionProject: projectId, 43 | metadata: clientMetadata, 44 | }); 45 | 46 | if (!projectId && loadRes.cloudaicompanionProject) { 47 | projectId = loadRes.cloudaicompanionProject; 48 | } 49 | 50 | const tier = getOnboardTier(loadRes); 51 | if (tier.userDefinedCloudaicompanionProject && !projectId) { 52 | throw new ProjectIdRequiredError(); 53 | } 54 | 55 | const onboardReq: OnboardUserRequest = { 56 | tierId: tier.id, 57 | cloudaicompanionProject: projectId, 58 | metadata: clientMetadata, 59 | }; 60 | 61 | // Poll onboardUser until long running operation is complete. 62 | let lroRes = await caServer.onboardUser(onboardReq); 63 | while (!lroRes.done) { 64 | await new Promise((f) => setTimeout(f, 5000)); 65 | lroRes = await caServer.onboardUser(onboardReq); 66 | } 67 | return lroRes.response?.cloudaicompanionProject?.id || ''; 68 | } 69 | 70 | function getOnboardTier(res: LoadCodeAssistResponse): GeminiUserTier { 71 | if (res.currentTier) { 72 | return res.currentTier; 73 | } 74 | for (const tier of res.allowedTiers || []) { 75 | if (tier.isDefault) { 76 | return tier; 77 | } 78 | } 79 | return { 80 | name: '', 81 | description: '', 82 | id: UserTierId.LEGACY, 83 | userDefinedCloudaicompanionProject: true, 84 | }; 85 | } 86 | -------------------------------------------------------------------------------- /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 relaunched 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 | -------------------------------------------------------------------------------- /scripts/version.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 { readFileSync, writeFileSync } from 'fs'; 9 | import { resolve } from 'path'; 10 | 11 | // A script to handle versioning and ensure all related changes are in a single, atomic commit. 12 | 13 | function run(command) { 14 | console.log(`> ${command}`); 15 | execSync(command, { stdio: 'inherit' }); 16 | } 17 | 18 | function readJson(filePath) { 19 | return JSON.parse(readFileSync(filePath, 'utf-8')); 20 | } 21 | 22 | function writeJson(filePath, data) { 23 | writeFileSync(filePath, JSON.stringify(data, null, 2) + '\n'); 24 | } 25 | 26 | // 1. Get the version type from the command line arguments. 27 | const versionType = process.argv[2]; 28 | if (!versionType) { 29 | console.error('Error: No version type specified.'); 30 | console.error('Usage: npm run version '); 31 | process.exit(1); 32 | } 33 | 34 | // 2. Bump the version in the root and all workspace package.json files. 35 | run(`npm version ${versionType} --no-git-tag-version --allow-same-version`); 36 | run( 37 | `npm version ${versionType} --workspaces --no-git-tag-version --allow-same-version`, 38 | ); 39 | 40 | // 3. Get the new version number from the root package.json 41 | const rootPackageJsonPath = resolve(process.cwd(), 'package.json'); 42 | const newVersion = readJson(rootPackageJsonPath).version; 43 | 44 | // 4. Update the sandboxImageUri in the root package.json 45 | const rootPackageJson = readJson(rootPackageJsonPath); 46 | if (rootPackageJson.config?.sandboxImageUri) { 47 | rootPackageJson.config.sandboxImageUri = 48 | rootPackageJson.config.sandboxImageUri.replace(/:.*$/, `:${newVersion}`); 49 | console.log(`Updated sandboxImageUri in root to use version ${newVersion}`); 50 | writeJson(rootPackageJsonPath, rootPackageJson); 51 | } 52 | 53 | // 5. Update the sandboxImageUri in the cli package.json 54 | const cliPackageJsonPath = resolve(process.cwd(), 'packages/cli/package.json'); 55 | const cliPackageJson = readJson(cliPackageJsonPath); 56 | if (cliPackageJson.config?.sandboxImageUri) { 57 | cliPackageJson.config.sandboxImageUri = 58 | cliPackageJson.config.sandboxImageUri.replace(/:.*$/, `:${newVersion}`); 59 | console.log( 60 | `Updated sandboxImageUri in cli package to use version ${newVersion}`, 61 | ); 62 | writeJson(cliPackageJsonPath, cliPackageJson); 63 | } 64 | 65 | // 6. Run `npm install` to update package-lock.json. 66 | run('npm install'); 67 | 68 | console.log(`Successfully bumped versions to v${newVersion}.`); 69 | -------------------------------------------------------------------------------- /packages/cli/src/ui/utils/computeStats.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright 2025 Google LLC 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | import { 8 | SessionMetrics, 9 | ComputedSessionStats, 10 | ModelMetrics, 11 | } from '../contexts/SessionContext.js'; 12 | 13 | export function calculateErrorRate(metrics: ModelMetrics): number { 14 | if (metrics.api.totalRequests === 0) { 15 | return 0; 16 | } 17 | return (metrics.api.totalErrors / metrics.api.totalRequests) * 100; 18 | } 19 | 20 | export function calculateAverageLatency(metrics: ModelMetrics): number { 21 | if (metrics.api.totalRequests === 0) { 22 | return 0; 23 | } 24 | return metrics.api.totalLatencyMs / metrics.api.totalRequests; 25 | } 26 | 27 | export function calculateCacheHitRate(metrics: ModelMetrics): number { 28 | if (metrics.tokens.prompt === 0) { 29 | return 0; 30 | } 31 | return (metrics.tokens.cached / metrics.tokens.prompt) * 100; 32 | } 33 | 34 | export const computeSessionStats = ( 35 | metrics: SessionMetrics, 36 | ): ComputedSessionStats => { 37 | const { models, tools } = metrics; 38 | const totalApiTime = Object.values(models).reduce( 39 | (acc, model) => acc + model.api.totalLatencyMs, 40 | 0, 41 | ); 42 | const totalToolTime = tools.totalDurationMs; 43 | const agentActiveTime = totalApiTime + totalToolTime; 44 | const apiTimePercent = 45 | agentActiveTime > 0 ? (totalApiTime / agentActiveTime) * 100 : 0; 46 | const toolTimePercent = 47 | agentActiveTime > 0 ? (totalToolTime / agentActiveTime) * 100 : 0; 48 | 49 | const totalCachedTokens = Object.values(models).reduce( 50 | (acc, model) => acc + model.tokens.cached, 51 | 0, 52 | ); 53 | const totalPromptTokens = Object.values(models).reduce( 54 | (acc, model) => acc + model.tokens.prompt, 55 | 0, 56 | ); 57 | const cacheEfficiency = 58 | totalPromptTokens > 0 ? (totalCachedTokens / totalPromptTokens) * 100 : 0; 59 | 60 | const totalDecisions = 61 | tools.totalDecisions.accept + 62 | tools.totalDecisions.reject + 63 | tools.totalDecisions.modify; 64 | const successRate = 65 | tools.totalCalls > 0 ? (tools.totalSuccess / tools.totalCalls) * 100 : 0; 66 | const agreementRate = 67 | totalDecisions > 0 68 | ? (tools.totalDecisions.accept / totalDecisions) * 100 69 | : 0; 70 | 71 | return { 72 | totalApiTime, 73 | totalToolTime, 74 | agentActiveTime, 75 | apiTimePercent, 76 | toolTimePercent, 77 | cacheEfficiency, 78 | totalDecisions, 79 | successRate, 80 | agreementRate, 81 | totalCachedTokens, 82 | totalPromptTokens, 83 | }; 84 | }; 85 | -------------------------------------------------------------------------------- /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 | --------------------------------------------------------------------------------