Compare commits

..

12 Commits

Author SHA1 Message Date
gemini-cli-robot e666b26d79 chore(release): v0.20.2 2025-12-12 22:34:48 +00:00
gemini-cli-robot af894e4686 fix(patch): cherry-pick edbe548 to release/v0.20.1-pr-15007 [CONFLICTS] (#15016)
Co-authored-by: Abhi <43648792+abhipatel12@users.noreply.github.com>
2025-12-12 14:11:57 -08:00
gemini-cli-robot c9b9435cef chore(release): v0.20.1 2025-12-12 18:25:21 +00:00
gemini-cli-robot ba8643809b fix(patch): cherry-pick 3f5f030 to release/v0.20.0-pr-14843 to patch version v0.20.0 and create version 0.20.1 (#15002)
Co-authored-by: Shreya Keshive <shreyakeshive@google.com>
2025-12-12 10:02:14 -08:00
gemini-cli-robot b05fb545ef chore(release): v0.20.0 2025-12-09 20:18:11 +00:00
gemini-cli-robot d0ce3c4c5d chore(release): v0.20.0-preview.5 2025-12-09 01:52:41 +00:00
gemini-cli-robot 8872ee0ace fix(patch): cherry-pick 171103a to release/v0.20.0-preview.2-pr-14742 to patch version v0.20.0-preview.2 and create version 0.20.0-preview.5 (#14752)
Co-authored-by: Gal Zahavi <38544478+galz10@users.noreply.github.com>
Co-authored-by: Jack Wotherspoon <jackwoth@google.com>
Co-authored-by: christine betts <chrstn@uw.edu>
2025-12-09 01:01:46 +00:00
gemini-cli-robot 9d7b9e6cd1 chore(release): v0.20.0-preview.2 2025-12-08 20:39:26 +00:00
gemini-cli-robot f9997f92c9 fix(patch): cherry-pick 828afe1 to release/v0.20.0-preview.1-pr-14159 to patch version v0.20.0-preview.1 and create version 0.20.0-preview.2 (#14733)
Co-authored-by: Allen Hutchison <adh@google.com>
2025-12-08 19:54:40 +00:00
gemini-cli-robot aae64683ce chore(release): v0.20.0-preview.1 2025-12-05 04:50:06 +00:00
gemini-cli-robot 356eb7ced0 fix(patch): cherry-pick d284fa6 to release/v0.20.0-preview.0-pr-14545 [CONFLICTS] (#14559)
Co-authored-by: Jacob Richman <jacob314@gmail.com>
2025-12-04 20:33:32 -08:00
gemini-cli-robot 2908555435 chore(release): v0.20.0-preview.0 2025-12-02 23:05:16 +00:00
351 changed files with 4739 additions and 22674 deletions
-3
View File
@@ -11,6 +11,3 @@
/.github/workflows/ @google-gemini/gemini-cli-askmode-approvers
/packages/cli/package.json @google-gemini/gemini-cli-askmode-approvers
/packages/core/package.json @google-gemini/gemini-cli-askmode-approvers
# Docs have a dedicated approver group in addition to maintainers
/docs/ @google-gemini/gemini-cli-maintainers @google-gemini/gemini-cli-docs
+247
View File
@@ -0,0 +1,247 @@
name: 'Testing: E2E'
on:
push:
branches:
- 'main'
# This will run for PRs from the base repository, providing secrets.
pull_request:
branches:
- 'main'
- 'release/**'
# This will run for PRs from forks when a label is added.
pull_request_target:
types: ['labeled']
merge_group:
workflow_dispatch:
inputs:
branch_ref:
description: 'Branch to run on'
required: true
default: 'main'
type: 'string'
concurrency:
group: '${{ github.workflow }}-${{ github.head_ref || github.ref }}'
cancel-in-progress: |-
${{ github.ref != 'refs/heads/main' && !startsWith(github.ref, 'refs/heads/release/') }}
jobs:
merge_queue_skipper:
name: 'Merge Queue Skipper'
runs-on: 'gemini-cli-ubuntu-16-core'
permissions: 'read-all'
outputs:
skip: '${{ steps.merge-queue-e2e-skipper.outputs.skip-check }}'
steps:
- id: 'merge-queue-e2e-skipper'
uses: 'cariad-tech/merge-queue-ci-skipper@1032489e59437862c90a08a2c92809c903883772' # ratchet:cariad-tech/merge-queue-ci-skipper@main
with:
secret: '${{ secrets.GITHUB_TOKEN }}'
continue-on-error: true
e2e_linux:
name: 'E2E Test (Linux) - ${{ matrix.sandbox }}'
needs: 'merge_queue_skipper'
if: |
(needs.merge_queue_skipper.result !='success' || needs.merge_queue_skipper.outputs.skip == 'false') &&
(github.event_name == 'push' ||
github.event_name == 'merge_group' ||
github.event_name == 'workflow_dispatch' ||
(github.event.pull_request.head.repo.full_name == github.repository) ||
(github.event.label.name == 'maintainer:e2e:ok'))
runs-on: 'gemini-cli-ubuntu-16-core'
strategy:
fail-fast: false
matrix:
sandbox:
- 'sandbox:none'
- 'sandbox:docker'
node-version:
- '20.x'
steps:
- name: 'Checkout (fork)'
uses: 'actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955' # ratchet:actions/checkout@v5
if: "github.event_name == 'pull_request_target'"
with:
ref: '${{ github.event.pull_request.head.sha }}'
repository: '${{ github.event.pull_request.head.repo.full_name }}'
- name: 'Checkout (internal)'
uses: 'actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955' # ratchet:actions/checkout@v5
if: "github.event_name != 'pull_request_target'"
with:
ref: '${{ github.event.inputs.branch_ref || github.ref }}'
- name: 'Set up Node.js ${{ matrix.node-version }}'
uses: 'actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020' # ratchet:actions-node@v4
with:
node-version: '${{ matrix.node-version }}'
- name: 'Install dependencies'
run: 'npm ci'
- name: 'Build project'
run: 'npm run build'
- name: 'Set up Docker'
if: "matrix.sandbox == 'sandbox:docker'"
uses: 'docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435' # ratchet:docker/setup-buildx-action@v3
- name: 'Run E2E tests'
env:
GEMINI_API_KEY: '${{ secrets.GEMINI_API_KEY }}'
KEEP_OUTPUT: 'true'
VERBOSE: 'true'
shell: 'bash'
run: |
if [[ "${{ matrix.sandbox }}" == "sandbox:docker" ]]; then
npm run test:integration:sandbox:docker
else
npm run test:integration:sandbox:none
fi
e2e_mac:
name: 'E2E Test (macOS)'
needs: 'merge_queue_skipper'
if: |
(needs.merge_queue_skipper.result !='success' || needs.merge_queue_skipper.outputs.skip == 'false') &&
(github.event_name == 'push' ||
github.event_name == 'merge_group' ||
github.event_name == 'workflow_dispatch' ||
(github.event.pull_request.head.repo.full_name == github.repository) ||
(github.event.label.name == 'maintainer:e2e:ok'))
runs-on: 'macos-latest'
steps:
- name: 'Checkout (fork)'
uses: 'actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955' # ratchet:actions/checkout@v5
if: "github.event_name == 'pull_request_target'"
with:
ref: '${{ github.event.pull_request.head.sha }}'
repository: '${{ github.event.pull_request.head.repo.full_name }}'
- name: 'Checkout (internal)'
uses: 'actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955' # ratchet:actions/checkout@v5
if: "github.event_name != 'pull_request_target'"
with:
ref: '${{ github.event.inputs.branch_ref || github.ref }}'
- name: 'Set up Node.js 20.x'
uses: 'actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020' # ratchet:actions-node@v4
with:
node-version: '20.x'
- name: 'Install dependencies'
run: 'npm ci'
- name: 'Build project'
run: 'npm run build'
- name: 'Fix rollup optional dependencies on macOS'
if: "runner.os == 'macOS'"
run: |
npm cache clean --force
- name: 'Run E2E tests (non-Windows)'
if: "runner.os != 'Windows'"
env:
GEMINI_API_KEY: '${{ secrets.GEMINI_API_KEY }}'
KEEP_OUTPUT: 'true'
SANDBOX: 'sandbox:none'
VERBOSE: 'true'
run: 'npm run test:integration:sandbox:none'
e2e_windows:
name: 'Slow E2E - Win'
needs: 'merge_queue_skipper'
if: |
(needs.merge_queue_skipper.result !='success' || needs.merge_queue_skipper.outputs.skip == 'false') &&
(github.event_name == 'push' ||
github.event_name == 'merge_group' ||
github.event_name == 'workflow_dispatch' ||
(github.event.pull_request.head.repo.full_name == github.repository) ||
(github.event.label.name == 'maintainer:e2e:ok'))
runs-on: 'gemini-cli-windows-16-core'
continue-on-error: true
steps:
- name: 'Checkout (fork)'
uses: 'actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955' # ratchet:actions/checkout@v5
if: "github.event_name == 'pull_request_target'"
with:
ref: '${{ github.event.pull_request.head.sha }}'
repository: '${{ github.event.pull_request.head.repo.full_name }}'
- name: 'Checkout (internal)'
uses: 'actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955' # ratchet:actions/checkout@v5
if: "github.event_name != 'pull_request_target'"
with:
ref: '${{ github.event.inputs.branch_ref || github.ref }}'
- name: 'Set up Node.js 20.x'
uses: 'actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020' # ratchet:actions-node@v4
with:
node-version: '20.x'
cache: 'npm'
- name: 'Configure Windows Defender exclusions'
run: |
Add-MpPreference -ExclusionPath $env:GITHUB_WORKSPACE -Force
Add-MpPreference -ExclusionPath "$env:GITHUB_WORKSPACE\node_modules" -Force
Add-MpPreference -ExclusionPath "$env:GITHUB_WORKSPACE\packages" -Force
Add-MpPreference -ExclusionPath "$env:TEMP" -Force
shell: 'pwsh'
- name: 'Configure npm for Windows performance'
run: |
npm config set progress false
npm config set audit false
npm config set fund false
npm config set loglevel error
npm config set maxsockets 32
npm config set registry https://registry.npmjs.org/
shell: 'pwsh'
- name: 'Install dependencies'
run: 'npm ci'
shell: 'pwsh'
- name: 'Build project'
run: 'npm run build'
shell: 'pwsh'
- name: 'Run E2E tests'
env:
GEMINI_API_KEY: '${{ secrets.GEMINI_API_KEY }}'
KEEP_OUTPUT: 'true'
SANDBOX: 'sandbox:none'
VERBOSE: 'true'
NODE_OPTIONS: '--max-old-space-size=32768 --max-semi-space-size=256'
UV_THREADPOOL_SIZE: '32'
NODE_ENV: 'test'
shell: 'pwsh'
run: 'npm run test:integration:sandbox:none'
e2e:
name: 'E2E'
if: |
always() && (
github.event_name == 'push' ||
github.event_name == 'merge_group' ||
github.event_name == 'workflow_dispatch' ||
(github.event.pull_request.head.repo.full_name == github.repository) ||
(github.event.label.name == 'maintainer:e2e:ok')
)
needs:
- 'e2e_linux'
- 'e2e_mac'
runs-on: 'gemini-cli-ubuntu-16-core'
steps:
- name: 'Check E2E test results'
run: |
if [[ (${{ needs.e2e_linux.result }} != 'success' && ${{ needs.e2e_linux.result }} != 'skipped') || \
(${{ needs.e2e_mac.result }} != 'success' && ${{ needs.e2e_mac.result }} != 'skipped') ]]; then
echo "One or more E2E jobs failed."
exit 1
fi
echo "All required E2E jobs passed!"
@@ -46,7 +46,8 @@ jobs:
))
)
) &&
!contains(github.event.issue.labels.*.name, 'area/')
!contains(github.event.issue.labels.*.name, 'area/') &&
!contains(github.event.issue.labels.*.name, 'priority/')
timeout-minutes: 5
runs-on: 'ubuntu-latest'
steps:
@@ -80,8 +81,8 @@ jobs:
exit 1
fi
if echo "${LABELS}" | grep -q 'area/'; then
echo "Issue #${ISSUE_NUMBER_INPUT} already has 'area/' label. Stopping workflow."
if echo "${LABELS}" | grep -q 'area/' || echo "${LABELS}" | grep -q 'priority/'; then
echo "Issue #${ISSUE_NUMBER_INPUT} already has 'area/' or 'priority/' labels. Stopping workflow."
exit 1
fi
@@ -109,6 +110,11 @@ jobs:
repo: context.repo.repo,
});
const allowedLabels = [
'priority/p0',
'priority/p1',
'priority/p2',
'priority/p3',
'priority/unknown',
'area/agent',
'area/enterprise',
'area/non-interactive',
@@ -155,19 +161,21 @@ jobs:
prompt: |-
## Role
You are an issue triage assistant. Your role is to analyze a GitHub issue and determine the single most appropriate area/ label based on the definitions provided.
You are an issue triage assistant. Your role is to analyze a GitHub issue and determine the single most appropriate area/ label and the single most appropriate priority/ label based on the definitions provided.
## Steps
1. Review the issue title and body: ${{ env.ISSUE_TITLE }} and ${{ env.ISSUE_BODY }}.
2. Review the available labels: ${{ env.AVAILABLE_LABELS }}.
3. Select exactly one area/ label that best matches the issue based on Reference 1: Area Definitions.
4. Fallback Logic:
4. Select exactly one priority/ label that best matches the issue based on Reference 2: Priority Definitions.
5. Fallback Logic:
- If you cannot confidently determine the correct area/ label from the definitions, you must use area/unknown.
5. Output your selected label in JSON format and nothing else. Example:
{"labels_to_set": ["area/core"]}
- If you cannot confidently determine the correct priority/ label from the definitions, you must use priority/unknown.
6. Output your two selected labels in JSON format and nothing else. Example:
{"labels_to_set": ["area/core", "priority/p1"]}
## Guidelines
- Your output must contain exactly one area/ label.
- Your output must contain exactly one area/ label and exactly one priority/ label.
- Triage only the current issue based on its title and body.
- Output only valid JSON format.
- Do not include any explanation or additional text, just the JSON.
@@ -250,6 +258,38 @@ jobs:
area/unknown
- Description: Issues that do not clearly fit into any other defined area/ category, or where information is too limited to make a determination. Use this when no other area is appropriate.
Reference 2: Priority Definitions
priority/p0: Critical / Blocker
- Definition: A catastrophic failure that makes the CLI unusable for most users or poses a severe security risk. This includes installation failures, authentication failures, persistent crashes, or critical security vulnerabilities.
- Key Questions:
- Is the CLI failing to install or run?
- Does it fail to authenticate or connect to the Gemini API, making all commands useless?
- Is it consistently crashing on basic, common commands?
- Does this represent a critical security vulnerability?
priority/p1: High
- Definition: A severe issue where a core feature (e.g., text generation, code generation, file processing) is unusable, failing, has severe performance degradation, or providing fundamentally incorrect output formatting (e.g., truncated text, broken JSON). Affects many users, and there is no reasonable workaround.
- Key Questions:
- Is a core feature failing for a specific, large user group (e.g., all Windows users, all users of a specific shell)?
- Is the CLI failing to process a supported input or misinterpreting critical flags?
- Is the CLI's output formatting consistently broken, making the response unusable?
- Is a core command or feature extremely slow, making it impractical to use?
priority/p2: Medium
- Definition: A moderately impactful issue causing inconvenience or a non-optimal experience, but a reasonable workaround exists. This also includes failures in non-core features.
- Key Questions:
- Is a command or flag behaving incorrectly, but the user can achieve their goal via other means?
- Is there a significant, non-blocking UI/UX problem in the terminal (e.g., broken progress indicators, bad terminal coloring)?
priority/p3: Low
- Definition: A minor, low-impact issue with minimal effect on functionality. This includes most cosmetic defects, typos in documentation, or unclear help text.
- Key Questions:
- Is this a typo in the README.md, gemini --help text, or other documentation?
- Is this a minor cosmetic issue (e.g., text alignment in output, an extra newline) that doesn't affect usability?
priority/unknown
- Description: Issues that do not clearly fit into any other defined priority/ category, or where information is too limited to make a determination. Use this when no other priority is appropriate.
- name: 'Apply Labels to Issue'
if: |-
${{ steps.gemini_issue_analysis.outputs.summary != '' }}
@@ -287,8 +327,8 @@ jobs:
const issueNumber = parseInt(process.env.ISSUE_NUMBER);
const labelsToAdd = parsedLabels.labels_to_set || [];
if (labelsToAdd.length !== 1) {
core.setFailed(`Expected exactly 1 label (area/), but got ${labelsToAdd.length}. Labels: ${labelsToAdd.join(', ')}`);
if (labelsToAdd.length !== 2) {
core.setFailed(`Expected exactly 2 labels (one area/ and one priority/), but got ${labelsToAdd.length}. Labels: ${labelsToAdd.join(', ')}`);
return;
}
@@ -1,127 +0,0 @@
name: 'Gemini Automated PR Labeler'
on:
pull_request_target:
types: ['opened', 'reopened', 'synchronize']
jobs:
label-pr:
timeout-minutes: 5
if: |-
${{ github.repository == 'google-gemini/gemini-cli' }}
permissions:
pull-requests: 'write'
contents: 'read'
id-token: 'write'
concurrency:
group: '${{ github.workflow }}-${{ github.event.pull_request.number }}'
cancel-in-progress: true
runs-on: 'ubuntu-latest'
steps:
- name: 'Generate GitHub App Token'
id: 'generate_token'
uses: 'actions/create-github-app-token@df432ceedc7162793a195dd1713ff69aefc7379e' # v2
with:
app-id: '${{ secrets.APP_ID }}'
private-key: '${{ secrets.PRIVATE_KEY }}'
permission-pull-requests: 'write'
- name: 'Run Gemini PR size and complexity labeller'
uses: 'google-github-actions/run-gemini-cli@a3bf79042542528e91937b3a3a6fbc4967ee3c31' # Use the specific commit SHA
env:
GITHUB_TOKEN: '${{ steps.generate_token.outputs.token }}'
PR_NUMBER: '${{ github.event.pull_request.number }}'
REPOSITORY: '${{ github.repository }}'
with:
gcp_workload_identity_provider: '${{ vars.GCP_WIF_PROVIDER }}'
gcp_project_id: '${{ vars.GOOGLE_CLOUD_PROJECT }}'
gcp_location: '${{ vars.GOOGLE_CLOUD_LOCATION }}'
gcp_service_account: '${{ vars.SERVICE_ACCOUNT_EMAIL }}'
gemini_api_key: '${{ secrets.GEMINI_API_KEY }}'
use_vertex_ai: '${{ vars.GOOGLE_GENAI_USE_VERTEXAI }}'
use_gemini_code_assist: '${{ vars.GOOGLE_GENAI_USE_GCA }}'
settings: |
{
"coreTools": [
"run_shell_command(gh pr diff)",
"run_shell_command(gh pr edit)",
"run_shell_command(gh pr comment)",
"run_shell_command(gh pr view)"
],
"telemetry": {
"enabled": true,
"target": "gcp"
},
"sandbox": false
}
prompt: |
You are a Pull Request labeller and Feedback Assistant. Your primary goal is to improve review velocity and help maintainers prioritize their work by automatically labeling pull requests based on size and complexity, and providing guidance for overly large PRs.
Steps:
1. Retrieve Pull Request Information:
- Use `gh pr diff ${{ env.PR_NUMBER }} --repo ${{ env.REPOSITORY }}` to get the diff content.
- Parse the output from `gh pr diff` to determine the total lines of code added and deleted. Calculate `TOTAL_LINES_CHANGED`.
2. Determine Pull Request Size:
- Use `gh pr view ${{ env.PR_NUMBER }} --repo ${{ env.REPOSITORY }} --json labels` to get the current labels on the PR.
- Check the current labels and identify if any `size/*` labels already exist (e.g., `size/xs`, `size/s`, etc.).
- If an old `size/*` label is found and it is different from the newly calculated size, remove it using:
`gh pr edit ${{ env.PR_NUMBER }} --repo ${{ env.REPOSITORY }} --remove-label "size-label-to-remove"`
- Based on `TOTAL_LINES_CHANGED`, select the appropriate new size label:
- `size/xs`: < 10 lines changed
- `size/s`: 10-50 lines changed
- `size/m`: 51-200 lines changed
- `size/l`: 201-1000 lines changed
- `size/xl`: > 1000 lines changed
- Do not invent new size labels.
- Apply the newly determined `size/*` label to the pull request using:
`gh pr edit ${{ env.PR_NUMBER }} --repo ${{ env.REPOSITORY }} --add-label "your-new-size-label"`
3. Analyze Pull Request Complexity:
- Perform Code Change Analysis: Examine the content of the code changes obtained from `gh pr diff ${{ env.PR_NUMBER }} --repo ${{ env.REPOSITORY }}`. Look for indicators of complexity such as:
- Number of files changed (can be inferred from the diff headers).
- Diversity of file types (e.g., changes across different languages, configuration files, documentation).
- Presence of new external dependencies.
- Introduction of new architectural components or significant refactoring.
- Complexity of individual code changes (e.g., deeply nested logic, complex algorithms, extensive conditional statements).
- Apply Heuristic-based Complexity Assessment:
- If the PR touches a small number of files with minor changes (e.g., typos, simple bug fixes, small feature additions), categorize it as `review/quick`.
- If the PR involves changes across multiple files, introduces new features, significantly refactors existing code, or has a high line count (even within `size/l`), categorize it as `review/involved`.
- Pay close attention to changes in critical or core modules as these inherently increase complexity.
- **Only use the labels `review/quick` or `review/involved` for complexity. Do not invent new complexity labels.**
- **Remove any previous `review/*` labels if they no longer apply, similar to the size label process.**
- Apply the determined `review/*` label to the pull request using:
`gh pr edit ${{ env.PR_NUMBER }} --repo ${{ env.REPOSITORY }} --add-label "your-complexity-label"`
4. Handle Overly Large Pull Requests (`size/xl`):
- **Conditional Check:** If the pull request has been labeled `size/xl` (i.e., > 1000 lines of code changed) in Step 2, proceed to the next sub-step.
- **Post Constructive Comment:** Post a polite and helpful comment on the pull request using:
`gh pr comment ${{ env.PR_NUMBER }} --repo ${{ env.REPOSITORY }} --body "Your comment here"`
The comment body should be:
"""
Thanks for your hard work on this pull request!
This pull request is quite large, which can make it challenging and time-consuming for reviewers to go through thoroughly.
To help us review it more efficiently and get your changes merged faster, we kindly request you consider breaking this into smaller, more focused pull requests. Each smaller PR should ideally address a single logical change or a small set of related changes.
For example, you could separate out refactoring, new feature additions, and bug fixes into individual PRs. This makes it easier to understand, review, and test each component independently.
We appreciate your understanding and cooperation. Feel free to reach out if you need any assistance with this!
"""
Guidelines:
- Automation Focus: All actions should be automated and not require manual intervention.
- Non-intrusive: The system should add labels and comments but not modify the code or close the pull request.
- Polite and Constructive: All communication, especially for large PRs, must be polite, encouraging, and constructive.
- Prioritize Clarity: The labels applied should clearly convey the PR's size and complexity to reviewers.
- Adhere to Defined Labels: Only use the specified `size/*` and `review/*` labels. Do not create or apply any other labels.
- Utilize `gh CLI`: Interact with GitHub using the `gh` command-line tool for diffing, label management, and commenting.
- Execute commands strictly as described in the steps. Do not invent new commands.
- In no case should you change other pull request that are not the one you are working on. Which can be found by using env.PR_NUMBER
- Execute each step that is defined in the steps section.
- In no case should you execute code from the pull request because this could be malicious code.
- If you fail to do this step log the errors you received
@@ -1,151 +0,0 @@
name: '🔒 Gemini Scheduled Stale Issue Closer'
on:
schedule:
- cron: '0 0 * * 0' # Every Sunday at midnight UTC
workflow_dispatch:
inputs:
dry_run:
description: 'Run in dry-run mode (no changes applied)'
required: false
default: false
type: 'boolean'
concurrency:
group: '${{ github.workflow }}'
cancel-in-progress: true
defaults:
run:
shell: 'bash'
jobs:
close-stale-issues:
runs-on: 'ubuntu-latest'
permissions:
issues: 'write'
steps:
- name: 'Generate GitHub App Token'
id: 'generate_token'
uses: 'actions/create-github-app-token@v1'
with:
app-id: '${{ secrets.APP_ID }}'
private-key: '${{ secrets.PRIVATE_KEY }}'
permission-issues: 'write'
- name: 'Process Stale Issues'
uses: 'actions/github-script@v7'
env:
DRY_RUN: '${{ inputs.dry_run }}'
with:
github-token: '${{ steps.generate_token.outputs.token }}'
script: |
const dryRun = process.env.DRY_RUN === 'true';
if (dryRun) {
core.info('DRY RUN MODE ENABLED: No changes will be applied.');
}
const batchLabel = 'Stale';
const threeMonthsAgo = new Date();
threeMonthsAgo.setMonth(threeMonthsAgo.getMonth() - 3);
const tenDaysAgo = new Date();
tenDaysAgo.setDate(tenDaysAgo.getDate() - 10);
core.info(`Cutoff date for creation: ${threeMonthsAgo.toISOString()}`);
core.info(`Cutoff date for updates: ${tenDaysAgo.toISOString()}`);
const query = `repo:${context.repo.owner}/${context.repo.repo} is:issue is:open created:<${threeMonthsAgo.toISOString()}`;
core.info(`Searching with query: ${query}`);
const itemsToCheck = await github.paginate(github.rest.search.issuesAndPullRequests, {
q: query,
sort: 'created',
order: 'asc',
per_page: 100
});
core.info(`Found ${itemsToCheck.length} open issues to check.`);
let processedCount = 0;
for (const issue of itemsToCheck) {
const createdAt = new Date(issue.created_at);
const updatedAt = new Date(issue.updated_at);
const reactionCount = issue.reactions.total_count;
// Basic thresholds
if (reactionCount >= 5) {
continue;
}
// Skip if it has a maintainer label
if (issue.labels.some(label => label.name.toLowerCase().includes('maintainer'))) {
continue;
}
let isStale = updatedAt < tenDaysAgo;
// If apparently active, check if it's only bot activity
if (!isStale) {
try {
const comments = await github.rest.issues.listComments({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: issue.number,
per_page: 100,
sort: 'created',
direction: 'desc'
});
const lastHumanComment = comments.data.find(comment => comment.user.type !== 'Bot');
if (lastHumanComment) {
isStale = new Date(lastHumanComment.created_at) < tenDaysAgo;
} else {
// No human comments. Check if creator is human.
if (issue.user.type !== 'Bot') {
isStale = createdAt < tenDaysAgo;
} else {
isStale = true; // Bot created, only bot comments
}
}
} catch (error) {
core.warning(`Failed to fetch comments for issue #${issue.number}: ${error.message}`);
continue;
}
}
if (isStale) {
processedCount++;
const message = `Closing stale issue #${issue.number}: "${issue.title}" (${issue.html_url})`;
core.info(message);
if (!dryRun) {
// Add label
await github.rest.issues.addLabels({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: issue.number,
labels: [batchLabel]
});
// Add comment
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: issue.number,
body: 'Hello! As part of our effort to keep our backlog manageable and focus on the most active issues, we are tidying up older reports.\n\nIt looks like this issue hasn\'t been active for a while, so we are closing it for now. However, if you are still experiencing this bug on the latest stable build, please feel free to comment on this issue or create a new one with updated details.\n\nThank you for your contribution!'
});
// Close issue
await github.rest.issues.update({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: issue.number,
state: 'closed'
});
}
}
}
core.info(`\nTotal issues processed: ${processedCount}`);
@@ -1,4 +1,4 @@
name: 'Testing: E2E (Chained)'
name: 'Test Chained E2E'
on:
push:
@@ -37,15 +37,14 @@ jobs:
- id: 'merge-queue-e2e-skipper'
uses: 'cariad-tech/merge-queue-ci-skipper@1032489e59437862c90a08a2c92809c903883772' # ratchet:cariad-tech/merge-queue-ci-skipper@main
with:
secret: '${{ secrets.GEMINI_CLI_ROBOT_GITHUB_PAT }}'
secret: '${{ secrets.GITHUB_TOKEN }}'
continue-on-error: true
download_repo_name:
runs-on: 'gemini-cli-ubuntu-16-core'
if: "${{github.event_name == 'workflow_dispatch' || github.event_name == 'workflow_run'}}"
if: "github.event_name == 'workflow_dispatch' || github.event_name == 'workflow_run'"
outputs:
repo_name: '${{ steps.output-repo-name.outputs.repo_name }}'
head_sha: '${{ steps.output-repo-name.outputs.head_sha }}'
steps:
- name: 'Mock Repo Artifact'
if: "${{ github.event_name == 'workflow_dispatch' }}"
@@ -67,7 +66,7 @@ jobs:
name: 'repo_name'
run-id: '${{ env.RUN_ID }}'
path: '${{ runner.temp }}/artifacts'
- name: 'Output Repo Name and SHA'
- name: 'Output Repo Name'
id: 'output-repo-name'
uses: 'actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd' # ratchet:actions/github-script@v8
with:
@@ -76,16 +75,8 @@ jobs:
const fs = require('fs');
const path = require('path');
const temp = '${{ runner.temp }}/artifacts';
const repoPath = path.join(temp, 'repo_name');
if (fs.existsSync(repoPath)) {
const repo_name = String(fs.readFileSync(repoPath)).trim();
core.setOutput('repo_name', repo_name);
}
const shaPath = path.join(temp, 'head_sha');
if (fs.existsSync(shaPath)) {
const head_sha = String(fs.readFileSync(shaPath)).trim();
core.setOutput('head_sha', head_sha);
}
const repo_name = String(fs.readFileSync(path.join(temp, 'repo_name')));
core.setOutput('repo_name', repo_name);
parse_run_context:
name: 'Parse run context'
@@ -100,7 +91,7 @@ jobs:
name: 'Set dynamic repository and SHA'
env:
REPO: '${{ needs.download_repo_name.outputs.repo_name || github.repository }}'
SHA: '${{ needs.download_repo_name.outputs.head_sha || github.event.inputs.head_sha || github.event.workflow_run.head_sha || github.sha }}'
SHA: '${{ github.event.inputs.head_sha || github.event.workflow_run.head_sha || github.sha }}'
shell: 'bash'
run: |
echo "REPO=$REPO" >> "$GITHUB_OUTPUT"
@@ -109,20 +100,19 @@ jobs:
set_pending_status:
runs-on: 'gemini-cli-ubuntu-16-core'
permissions: 'write-all'
if: "github.event_name == 'workflow_dispatch' || github.event_name == 'workflow_run'"
needs:
- 'parse_run_context'
if: 'always()'
steps:
- name: 'Set pending status'
uses: 'myrotvorets/set-commit-status-action@16037e056d73b2d3c88e37e393ff369047f70886' # ratchet:myrotvorets/set-commit-status-action@master
if: 'always()'
with:
allowForks: 'true'
repo: '${{ github.repository }}'
repo: '${{ needs.parse_run_context.outputs.repository }}'
sha: '${{ needs.parse_run_context.outputs.sha }}'
token: '${{ secrets.GEMINI_CLI_ROBOT_GITHUB_PAT }}'
token: '${{ secrets.GITHUB_TOKEN }}'
status: 'pending'
context: 'E2E (Chained)'
e2e_linux:
name: 'E2E Test (Linux) - ${{ matrix.sandbox }}'
@@ -131,7 +121,7 @@ jobs:
- 'parse_run_context'
runs-on: 'gemini-cli-ubuntu-16-core'
if: |
always() && (needs.merge_queue_skipper.result !='success' || needs.merge_queue_skipper.outputs.skip != 'true')
always() && (needs.merge_queue_skipper.result !='success' || needs.merge_queue_skipper.outputs.skip == 'false')
strategy:
fail-fast: false
matrix:
@@ -160,7 +150,7 @@ jobs:
run: 'npm run build'
- name: 'Set up Docker'
if: "${{matrix.sandbox == 'sandbox:docker'}}"
if: "matrix.sandbox == 'sandbox:docker'"
uses: 'docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435' # ratchet:docker/setup-buildx-action@v3
- name: 'Run E2E tests'
@@ -183,7 +173,7 @@ jobs:
- 'parse_run_context'
runs-on: 'macos-latest'
if: |
always() && (needs.merge_queue_skipper.result !='success' || needs.merge_queue_skipper.outputs.skip != 'true')
always() && (needs.merge_queue_skipper.result !='success' || needs.merge_queue_skipper.outputs.skip == 'false')
steps:
- name: 'Checkout'
uses: 'actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955' # ratchet:actions/checkout@v5
@@ -203,11 +193,11 @@ jobs:
run: 'npm run build'
- name: 'Fix rollup optional dependencies on macOS'
if: "${{runner.os == 'macOS'}}"
if: "runner.os == 'macOS'"
run: |
npm cache clean --force
- name: 'Run E2E tests (non-Windows)'
if: "${{runner.os != 'Windows'}}"
if: "runner.os != 'Windows'"
env:
GEMINI_API_KEY: '${{ secrets.GEMINI_API_KEY }}'
KEEP_OUTPUT: 'true'
@@ -221,7 +211,7 @@ jobs:
- 'merge_queue_skipper'
- 'parse_run_context'
if: |
always() && (needs.merge_queue_skipper.result !='success' || needs.merge_queue_skipper.outputs.skip != 'true')
always() && (needs.merge_queue_skipper.result !='success' || needs.merge_queue_skipper.outputs.skip == 'false')
runs-on: 'gemini-cli-windows-16-core'
continue-on-error: true
@@ -279,7 +269,7 @@ jobs:
e2e:
name: 'E2E'
if: |
always() && (needs.merge_queue_skipper.result !='success' || needs.merge_queue_skipper.outputs.skip != 'true')
always() && (needs.merge_queue_skipper.result !='success' || needs.merge_queue_skipper.outputs.skip == 'false')
needs:
- 'e2e_linux'
- 'e2e_mac'
@@ -298,7 +288,7 @@ jobs:
set_workflow_status:
runs-on: 'gemini-cli-ubuntu-16-core'
permissions: 'write-all'
if: 'always()'
if: "github.event_name == 'workflow_dispatch' || github.event_name == 'workflow_run'"
needs:
- 'parse_run_context'
- 'e2e'
@@ -308,8 +298,7 @@ jobs:
if: 'always()'
with:
allowForks: 'true'
repo: '${{ github.repository }}'
repo: '${{ needs.parse_run_context.outputs.repository }}'
sha: '${{ needs.parse_run_context.outputs.sha }}'
token: '${{ secrets.GITHUB_TOKEN }}'
status: '${{ needs.e2e.result }}'
context: 'E2E (Chained)'
status: '${{ job.status }}'
+6 -11
View File
@@ -3,15 +3,11 @@ name: 'Trigger E2E'
on:
workflow_dispatch:
inputs:
repo_name:
description: 'Repository name (e.g., owner/repo)'
required: false
branch_ref:
description: 'Branch to run on'
required: true
default: 'main'
type: 'string'
head_sha:
description: 'SHA of the commit to test'
required: false
type: 'string'
pull_request:
jobs:
save_repo_name:
@@ -19,12 +15,11 @@ jobs:
steps:
- name: 'Save Repo name'
env:
REPO_NAME: '${{ github.event.inputs.repo_name || github.event.pull_request.head.repo.full_name }}'
HEAD_SHA: '${{ github.event.inputs.head_sha || github.event.pull_request.head.sha }}'
# Replace with github.event.pull_request.head.repo.full_name when switched to listen on pull request events. This repo name does not contain the org which is needed for checkout.
REPO_NAME: '${{ github.event.repository.name }}'
run: |
mkdir -p ./pr
echo '${{ env.REPO_NAME }}' > ./pr/repo_name
echo '${{ env.HEAD_SHA }}' > ./pr/head_sha
- uses: 'actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02' # ratchet:actions/upload-artifact@v4
with:
name: 'repo_name'
+1 -1
View File
@@ -1,7 +1,7 @@
# Gemini CLI
[![Gemini CLI CI](https://github.com/google-gemini/gemini-cli/actions/workflows/ci.yml/badge.svg)](https://github.com/google-gemini/gemini-cli/actions/workflows/ci.yml)
[![Gemini CLI E2E (Chained)](https://github.com/google-gemini/gemini-cli/actions/workflows/chained_e2e.yml/badge.svg)](https://github.com/google-gemini/gemini-cli/actions/workflows/chained_e2e.yml)
[![Gemini CLI E2E](https://github.com/google-gemini/gemini-cli/actions/workflows/e2e.yml/badge.svg)](https://github.com/google-gemini/gemini-cli/actions/workflows/e2e.yml)
[![Version](https://img.shields.io/npm/v/@google/gemini-cli)](https://www.npmjs.com/package/@google/gemini-cli)
[![License](https://img.shields.io/github/license/google-gemini/gemini-cli)](https://github.com/google-gemini/gemini-cli/blob/main/LICENSE)
[![View Code Wiki](https://www.gstatic.com/_/boq-sdlc-agents-ui/_/r/YUi5dj2UWvE.svg)](https://codewiki.google/github.com/google-gemini/gemini-cli)
+18 -100
View File
@@ -1,97 +1,15 @@
# Gemini CLI release notes
# Gemini CLI Changelog
Gemini CLI has three major release channels: nightly, preview, and stable. For
most users, we recommend the stable release.
Wondering what's new in Gemini CLI? This document provides key highlights and
notable changes to Gemini CLI.
On this page, you can find information regarding the current releases and
announcements from each release.
## v0.15.0 - Gemini CLI weekly update - 2025-11-03
For the full changelog, refer to
[Releases - google-gemini/gemini-cli](https://github.com/google-gemini/gemini-cli/releases)
on GitHub.
## Current releases
| Release channel | Notes |
| :-------------------- | :---------------------------------------------- |
| Nightly | Nightly release with the most recent changes. |
| [Preview](preview.md) | Experimental features ready for early feedback. |
| [Stable](latest.md) | Stable, recommended for general use. |
## Announcements: v0.19.0 - 2025-11-24
- 🎉 **New extensions:**
- **Eleven Labs:** Create, play, manage your audio play tracks with the Eleven
Labs Gemini CLI extension:
`gemini extensions install https://github.com/elevenlabs/elevenlabs-mcp`
- **Zed integration:** Users can now leverage Gemini 3 within the Zed
integration after enabling "Preview Features" in their CLIs `/settings`.
([pr](https://github.com/google-gemini/gemini-cli/pull/13398) by
[@benbrandt](https://github.com/benbrandt))
- **Interactive shell:**
- **Click-to-Focus:** When "Use Alternate Buffer" setting is enabled, users
can click within the embedded shell output to focus it for input.
([pr](https://github.com/google-gemini/gemini-cli/pull/13341) by
[@galz10](https://github.com/galz10))
- **Loading phrase:** Clearly indicates when the interactive shell is awaiting
user input. ([vid](https://imgur.com/a/kjK8bUK),
[pr](https://github.com/google-gemini/gemini-cli/pull/12535) by
[@jackwotherspoon](https://github.com/jackwotherspoon))
## Announcements: v0.18.0 - 2025-11-17
- 🎉 **New extensions:**
- **Google Workspace**: Integrate Gemini CLI with your Workspace data. Write
docs, build slides, chat with others or even get your calc on in sheets:
`gemini extensions install https://github.com/gemini-cli-extensions/workspace`
- Blog:
[https://allen.hutchison.org/2025/11/19/bringing-the-office-to-the-terminal/](https://allen.hutchison.org/2025/11/19/bringing-the-office-to-the-terminal/)
- **Redis:** Manage and search data in Redis with natural language:
`gemini extensions install https://github.com/redis/mcp-redis`
- **Anomalo:** Query your data warehouse table metadata and quality status
through commands and natural language:
`gemini extensions install https://github.com/datagravity-ai/anomalo-gemini-extension`
- **Experimental permission improvements:** We are now experimenting with a new
policy engine in Gemini CLI. This allows users and administrators to create
fine-grained policy for tool calls. Currently behind a flag. See
[https://geminicli.com/docs/core/policy-engine/](../core/policy-engine.md) for
more information.
- Blog:
[https://allen.hutchison.org/2025/11/26/the-guardrails-of-autonomy/](https://allen.hutchison.org/2025/11/26/the-guardrails-of-autonomy/)
- **Gemini 3 support for paid:** Gemini 3 support has been rolled out to all API
key, Google AI Pro or Google AI Ultra (for individuals, not businesses) and
Gemini Code Assist Enterprise users. Enable it via `/settings` and toggling on
**Preview Features**.
- **Updated UI rollback:** Weve temporarily rolled back our updated UI to give
it more time to bake. This means for a time you wont have embedded scrolling
or mouse support. You can re-enable with `/settings` -> **Use Alternate Screen
Buffer** -> `true`.
- **Model in history:** Users can now toggle in `/settings` to display model in
their chat history. ([gif](https://imgur.com/a/uEmNKnQ),
[pr](https://github.com/google-gemini/gemini-cli/pull/13034) by
[@scidomino](https://github.com/scidomino))
- **Multi-uninstall:** Users can now uninstall multiple extensions with a single
command. ([pic](https://imgur.com/a/9Dtq8u2),
[pr](https://github.com/google-gemini/gemini-cli/pull/13016) by
[@JayadityaGit](https://github.com/JayadityaGit))
## Announcements: v0.16.0 - 2025-11-10
- **Gemini 3 + Gemini CLI:** launch 🚀🚀🚀
- **Data Commons Gemini CLI Extension** - A new Data Commons Gemini CLI
extension that lets you query open-source statistical data from
datacommons.org. **To get started, you'll need a Data Commons API key and uv
installed**. These and other details to get you started with the extension can
be found at
[https://github.com/gemini-cli-extensions/datacommons](https://github.com/gemini-cli-extensions/datacommons).
## Announcements: v0.15.0 - 2025-11-03
- **🎉 Seamless scrollable UI and mouse support:** Weve given Gemini CLI a
major facelift to make your terminal experience smoother and much more
polished. You now get a flicker-free display with sticky headers that keep
important context visible and a stable input prompt that doesn't jump around.
We even added mouse support so you can click right where you need to type!
- **🎉 Seamless scrollable UI & mouse support:** Weve given Gemini CLI a major
facelift to make your terminal experience smoother and much more polished. You
now get a flicker-free display with sticky headers that keep important context
visible and a stable input prompt that doesn't jump around. We even added
mouse support so you can click right where you need to type!
([gif](https://imgur.com/a/O6qc7bx),
[@jacob314](https://github.com/jacob314)).
- **Announcement:**
@@ -131,7 +49,7 @@ on GitHub.
correctly. ([pr](https://github.com/google-gemini/gemini-cli/pull/12186) by
[@kevinjwang1](https://github.com/kevinjwang1)).
## Announcements: v0.12.0 - 2025-10-27
## v0.12.0 - Gemini CLI weekly update - 2025-10-27
![Codebase investigator subagent in Gemini CLI.](https://i.imgur.com/4J1njsx.png)
@@ -194,7 +112,7 @@ on GitHub.
([pr](https://github.com/google-gemini/gemini-cli/pull/11593) by
[@joshualitt](https://github.com/joshualitt)).
## Announcements: v0.11.0 - 2025-10-20
## v0.11.0 - Gemini CLI weekly update - 2025-10-20
![Gemini CLI and Jules](https://storage.googleapis.com/gweb-developer-goog-blog-assets/images/Jules_Extension_-_Blog_Header_O346JNt.original.png)
@@ -237,7 +155,7 @@ on GitHub.
([pr](https://github.com/google-gemini/gemini-cli/pull/11318) by
[@allenhutchison](https://github.com/allenhutchison))
## Announcements: v0.10.0 - 2025-10-13
## v0.10.0 - Gemini CLI weekly update - 2025-10-13
- **Polish:** The team has been heads down bug fixing and investing heavily into
polishing existing flows, tools, and interactions.
@@ -254,7 +172,7 @@ on GitHub.
([pr](https://github.com/google-gemini/gemini-cli/pull/10819) by
[@jerop](https://github.com/jerop)).
## Announcements: v0.9.0 - 2025-10-06
## v0.9.0 - Gemini CLI weekly update - 2025-10-06
- 🎉 **Interactive Shell:** Run interactive commands like `vim`, `rebase -i`, or
even `gemini` 😎 directly in Gemini CLI:
@@ -279,7 +197,7 @@ on GitHub.
([pr](https://github.com/google-gemini/gemini-cli/pull/10108) by
[@sgnagnarella](https://github.com/sgnagnarella))
## Announcements: v0.8.0 - 2025-09-29
## v0.8.0 - Gemini CLI weekly update - 2025-09-29
- 🎉 **Announcing Gemini CLI Extensions** 🎉
- Completely customize your Gemini CLI experience to fit your workflow.
@@ -318,7 +236,7 @@ on GitHub.
changes, smaller features, UI updates, reliability and bug fixes + general
polish made it in this week!
## Announcements: v0.7.0 - 2025-09-22
## v0.7.0 - Gemini CLI weekly update - 2025-09-22
- 🎉**Build your own Gemini CLI IDE plugin:** We've published a spec for
creating IDE plugins to enable rich context-aware experiences and native
@@ -356,7 +274,7 @@ on GitHub.
changes, smaller features, UI updates, reliability and bug fixes + general
polish made it in this week!
## Announcements: v0.6.0 - 2025-09-15
## v0.6.0 - Gemini CLI weekly update - 2025-09-15
- 🎉 **Higher limits for Google AI Pro and Ultra subscribers:** Were psyched to
finally announce that Google AI Pro and AI Ultra subscribers now get access to
@@ -437,7 +355,7 @@ on GitHub.
changes, smaller features, UI updates, reliability and bug fixes + general
polish made it in this week!
## Announcements: v0.5.0 - 2025-09-08
## v0.5.0 - Gemini CLI weekly update - 2025-09-08
- 🎉**FastMCP + Gemini CLI**🎉: Quickly install and manage your Gemini CLI MCP
servers with FastMCP ([video](https://imgur.com/a/m8QdCPh),
@@ -482,7 +400,7 @@ on GitHub.
changes, smaller features, UI updates, reliability and bug fixes + general
polish made it in this week!
## Announcements: v0.4.0 - 2025-09-01
## v0.4.0 - Gemini CLI weekly update - 2025-09-01
- 🎉**Gemini CLI CloudRun and Security Integrations**🎉: Automate app deployment
and security analysis with CloudRun and Security extension integrations. Once
-154
View File
@@ -1,154 +0,0 @@
# Latest stable release: v0.19.0 - v0.19.4
Released: December 1, 2025
For most users, our latest stable release is the recommended release. Install
the latest stable version with:
```
npm install -g @google/gemini-cli
```
## Highlights
- **Zed integration:** Users can now leverage Gemini 3 within the Zed
integration after enabling "Preview Features" in their CLIs `/settings`.
- **Interactive shell:**
- **Click-to-Focus:** Go to `/settings` and enable **Use Alternate Buffer** to
click within the embedded shell output to focus it for input.
- **Loading phrase:** Clearly indicates when the interactive shell is awaiting
user input. ([vid](https://imgur.com/a/kjK8bUK),
[pr](https://github.com/google-gemini/gemini-cli/pull/12535) by
[@jackwotherspoon](https://github.com/jackwotherspoon))
## What's Changed
- Use lenient MCP output schema validator by @cornmander in
https://github.com/google-gemini/gemini-cli/pull/13521
- Update persistence state to track counts of messages instead of times banner
has been displayed by @Adib234 in
https://github.com/google-gemini/gemini-cli/pull/13428
- update docs for http proxy by @scidomino in
https://github.com/google-gemini/gemini-cli/pull/13538
- move stdio by @jacob314 in
https://github.com/google-gemini/gemini-cli/pull/13528
- chore(release): bump version to 0.19.0-nightly.20251120.8e531dc02 by
@gemini-cli-robot in https://github.com/google-gemini/gemini-cli/pull/13540
- Skip pre-commit hooks for shadow repo (#13331) by @vishvananda in
https://github.com/google-gemini/gemini-cli/pull/13488
- fix(ui): Correct mouse click cursor positioning for wide characters by
@SandyTao520 in https://github.com/google-gemini/gemini-cli/pull/13537
- fix(core): correct bash @P prompt transformation detection by @pyrytakala in
https://github.com/google-gemini/gemini-cli/pull/13544
- Optimize and improve test coverage for cli/src/config by @megha1188 in
https://github.com/google-gemini/gemini-cli/pull/13485
- Improve code coverage for cli/src/ui/privacy package by @megha1188 in
https://github.com/google-gemini/gemini-cli/pull/13493
- docs: fix typos in source code and documentation by @fancive in
https://github.com/google-gemini/gemini-cli/pull/13577
- Improved code coverage for cli/src/zed-integration by @megha1188 in
https://github.com/google-gemini/gemini-cli/pull/13570
- feat(ui): build interactive session browser component by @bl-ue in
https://github.com/google-gemini/gemini-cli/pull/13351
- Fix multiple bugs with auth flow including using the implemented but unused
restart support. by @jacob314 in
https://github.com/google-gemini/gemini-cli/pull/13565
- feat(core): add modelAvailabilityService for managing and tracking model
health by @adamfweidman in
https://github.com/google-gemini/gemini-cli/pull/13426
- docs: fix grammar typo "a MCP" to "an MCP" by @noahacgn in
https://github.com/google-gemini/gemini-cli/pull/13595
- feat: custom loading phrase when interactive shell requires input by
@jackwotherspoon in https://github.com/google-gemini/gemini-cli/pull/12535
- docs: Update uninstall command to reflect multiple extension support by
@JayadityaGit in https://github.com/google-gemini/gemini-cli/pull/13582
- bug(core): Ensure we use thinking budget on fallback to 2.5 by @joshualitt in
https://github.com/google-gemini/gemini-cli/pull/13596
- Remove useModelRouter experimental flag by @Adib234 in
https://github.com/google-gemini/gemini-cli/pull/13593
- feat(docs): Ensure multiline JS objects are rendered properly. by @joshualitt
in https://github.com/google-gemini/gemini-cli/pull/13535
- Fix exp id logging by @owenofbrien in
https://github.com/google-gemini/gemini-cli/pull/13430
- Moved client id logging into createBasicLogEvent by @owenofbrien in
https://github.com/google-gemini/gemini-cli/pull/13607
- Restore bracketed paste mode after external editor exit by @scidomino in
https://github.com/google-gemini/gemini-cli/pull/13606
- feat(core): Add support for custom aliases for model configs. by @joshualitt
in https://github.com/google-gemini/gemini-cli/pull/13546
- feat(core): Add `BaseLlmClient.generateContent`. by @joshualitt in
https://github.com/google-gemini/gemini-cli/pull/13591
- Turn off alternate buffer mode by default. by @jacob314 in
https://github.com/google-gemini/gemini-cli/pull/13623
- fix(cli): Prevent stdout/stderr patching for extension commands by @chrstnb in
https://github.com/google-gemini/gemini-cli/pull/13600
- Improve test coverage for cli/src/ui/components by @megha1188 in
https://github.com/google-gemini/gemini-cli/pull/13598
- Update ink version to 6.4.6 by @jacob314 in
https://github.com/google-gemini/gemini-cli/pull/13631
- chore/release: bump version to 0.19.0-nightly.20251122.42c2e1b21 by
@gemini-cli-robot in https://github.com/google-gemini/gemini-cli/pull/13637
- chore/release: bump version to 0.19.0-nightly.20251123.dadd606c0 by
@gemini-cli-robot in https://github.com/google-gemini/gemini-cli/pull/13675
- chore/release: bump version to 0.19.0-nightly.20251124.e177314a4 by
@gemini-cli-robot in https://github.com/google-gemini/gemini-cli/pull/13713
- fix(core): Fix context window overflow warning for PDF files by @kkitase in
https://github.com/google-gemini/gemini-cli/pull/13548
- feat :rephrasing the extension logging messages to run the explore command
when there are no extensions installed by @JayadityaGit in
https://github.com/google-gemini/gemini-cli/pull/13740
- Improve code coverage for cli package by @megha1188 in
https://github.com/google-gemini/gemini-cli/pull/13724
- Add session subtask in /stats command by @Adib234 in
https://github.com/google-gemini/gemini-cli/pull/13750
- feat(core): Migrate chatCompressionService to model configs. by @joshualitt in
https://github.com/google-gemini/gemini-cli/pull/12863
- feat(hooks): Hook Telemetry Infrastructure by @Edilmo in
https://github.com/google-gemini/gemini-cli/pull/9082
- fix: (some minor improvements to configs and getPackageJson return behaviour)
by @grMLEqomlkkU5Eeinz4brIrOVCUCkJuN in
https://github.com/google-gemini/gemini-cli/pull/12510
- feat(hooks): Hook Event Handling by @Edilmo in
https://github.com/google-gemini/gemini-cli/pull/9097
- feat(hooks): Hook Agent Lifecycle Integration by @Edilmo in
https://github.com/google-gemini/gemini-cli/pull/9105
- feat(core): Land bool for alternate system prompt. by @joshualitt in
https://github.com/google-gemini/gemini-cli/pull/13764
- bug(core): Add default chat compression config. by @joshualitt in
https://github.com/google-gemini/gemini-cli/pull/13766
- feat(model-availability): introduce ModelPolicy and PolicyCatalog by
@adamfweidman in https://github.com/google-gemini/gemini-cli/pull/13751
- feat(hooks): Hook System Orchestration by @Edilmo in
https://github.com/google-gemini/gemini-cli/pull/9102
- feat(config): add isModelAvailabilityServiceEnabled setting by @adamfweidman
in https://github.com/google-gemini/gemini-cli/pull/13777
- chore/release: bump version to 0.19.0-nightly.20251125.f6d97d448 by
@gemini-cli-robot in https://github.com/google-gemini/gemini-cli/pull/13782
- chore: remove console.error by @adamfweidman in
https://github.com/google-gemini/gemini-cli/pull/13779
- fix: Add $schema property to settings.schema.json by @sacrosanctic in
https://github.com/google-gemini/gemini-cli/pull/12763
- fix(cli): allow non-GitHub SCP-styled URLs for extension installation by @m0ps
in https://github.com/google-gemini/gemini-cli/pull/13800
- fix(resume): allow passing a prompt via stdin while resuming using --resume by
@bl-ue in https://github.com/google-gemini/gemini-cli/pull/13520
- feat(sessions): add /resume slash command to open the session browser by
@bl-ue in https://github.com/google-gemini/gemini-cli/pull/13621
- docs(sessions): add documentation for chat recording and session management by
@bl-ue in https://github.com/google-gemini/gemini-cli/pull/13667
- Fix TypeError: "URL.parse is not a function" for Node.js < v22 by @macarronesc
in https://github.com/google-gemini/gemini-cli/pull/13698
- fallback to flash for TerminalQuota errors by @sehoon38 in
https://github.com/google-gemini/gemini-cli/pull/13791
- Update Code Wiki README badge by @PatoBeltran in
https://github.com/google-gemini/gemini-cli/pull/13768
- Add Databricks auth support and custom header option to gemini cli by
@AarushiShah in https://github.com/google-gemini/gemini-cli/pull/11893
- Update dependency for modelcontextprotocol/sdk to 1.23.0 by @bbiggs in
https://github.com/google-gemini/gemini-cli/pull/13827
- fix(patch): cherry-pick 576fda1 to release/v0.19.0-preview.0-pr-14099
[CONFLICTS] by @gemini-cli-robot in
https://github.com/google-gemini/gemini-cli/pull/14402
**Full Changelog**:
https://github.com/google-gemini/gemini-cli/compare/v0.18.4...v0.19.0
-141
View File
@@ -1,141 +0,0 @@
# Preview release: Release v0.19.0-preview.0
Released: November 25, 2025
Our preview release includes the latest, new, and experimental features. This
release may not be as stable as our [latest weekly release](latest.md).
To install the preview release:
```
npm install -g @google/gemini-cli@preview
```
## What's changed
- Use lenient MCP output schema validator by @cornmander in
https://github.com/google-gemini/gemini-cli/pull/13521
- Update persistence state to track counts of messages instead of times banner
has been displayed by @Adib234 in
https://github.com/google-gemini/gemini-cli/pull/13428
- update docs for http proxy by @scidomino in
https://github.com/google-gemini/gemini-cli/pull/13538
- move stdio by @jacob314 in
https://github.com/google-gemini/gemini-cli/pull/13528
- chore(release): bump version to 0.19.0-nightly.20251120.8e531dc02 by
@gemini-cli-robot in https://github.com/google-gemini/gemini-cli/pull/13540
- Skip pre-commit hooks for shadow repo (#13331) by @vishvananda in
https://github.com/google-gemini/gemini-cli/pull/13488
- fix(ui): Correct mouse click cursor positioning for wide characters by
@SandyTao520 in https://github.com/google-gemini/gemini-cli/pull/13537
- fix(core): correct bash @P prompt transformation detection by @pyrytakala in
https://github.com/google-gemini/gemini-cli/pull/13544
- Optimize and improve test coverage for cli/src/config by @megha1188 in
https://github.com/google-gemini/gemini-cli/pull/13485
- Improve code coverage for cli/src/ui/privacy package by @megha1188 in
https://github.com/google-gemini/gemini-cli/pull/13493
- docs: fix typos in source code and documentation by @fancive in
https://github.com/google-gemini/gemini-cli/pull/13577
- Improved code coverage for cli/src/zed-integration by @megha1188 in
https://github.com/google-gemini/gemini-cli/pull/13570
- feat(ui): build interactive session browser component by @bl-ue in
https://github.com/google-gemini/gemini-cli/pull/13351
- Fix multiple bugs with auth flow including using the implemented but unused
restart support. by @jacob314 in
https://github.com/google-gemini/gemini-cli/pull/13565
- feat(core): add modelAvailabilityService for managing and tracking model
health by @adamfweidman in
https://github.com/google-gemini/gemini-cli/pull/13426
- docs: fix grammar typo "a MCP" to "an MCP" by @noahacgn in
https://github.com/google-gemini/gemini-cli/pull/13595
- feat: custom loading phrase when interactive shell requires input by
@jackwotherspoon in https://github.com/google-gemini/gemini-cli/pull/12535
- docs: Update uninstall command to reflect multiple extension support by
@JayadityaGit in https://github.com/google-gemini/gemini-cli/pull/13582
- bug(core): Ensure we use thinking budget on fallback to 2.5 by @joshualitt in
https://github.com/google-gemini/gemini-cli/pull/13596
- Remove useModelRouter experimental flag by @Adib234 in
https://github.com/google-gemini/gemini-cli/pull/13593
- feat(docs): Ensure multiline JS objects are rendered properly. by @joshualitt
in https://github.com/google-gemini/gemini-cli/pull/13535
- Fix exp id logging by @owenofbrien in
https://github.com/google-gemini/gemini-cli/pull/13430
- Moved client id logging into createBasicLogEvent by @owenofbrien in
https://github.com/google-gemini/gemini-cli/pull/13607
- Restore bracketed paste mode after external editor exit by @scidomino in
https://github.com/google-gemini/gemini-cli/pull/13606
- feat(core): Add support for custom aliases for model configs. by @joshualitt
in https://github.com/google-gemini/gemini-cli/pull/13546
- feat(core): Add `BaseLlmClient.generateContent`. by @joshualitt in
https://github.com/google-gemini/gemini-cli/pull/13591
- Turn off alternate buffer mode by default. by @jacob314 in
https://github.com/google-gemini/gemini-cli/pull/13623
- fix(cli): Prevent stdout/stderr patching for extension commands by @chrstnb in
https://github.com/google-gemini/gemini-cli/pull/13600
- Improve test coverage for cli/src/ui/components by @megha1188 in
https://github.com/google-gemini/gemini-cli/pull/13598
- Update ink version to 6.4.6 by @jacob314 in
https://github.com/google-gemini/gemini-cli/pull/13631
- chore/release: bump version to 0.19.0-nightly.20251122.42c2e1b21 by
@gemini-cli-robot in https://github.com/google-gemini/gemini-cli/pull/13637
- chore/release: bump version to 0.19.0-nightly.20251123.dadd606c0 by
@gemini-cli-robot in https://github.com/google-gemini/gemini-cli/pull/13675
- chore/release: bump version to 0.19.0-nightly.20251124.e177314a4 by
@gemini-cli-robot in https://github.com/google-gemini/gemini-cli/pull/13713
- fix(core): Fix context window overflow warning for PDF files by @kkitase in
https://github.com/google-gemini/gemini-cli/pull/13548
- feat :rephrasing the extension logging messages to run the explore command
when there are no extensions installed by @JayadityaGit in
https://github.com/google-gemini/gemini-cli/pull/13740
- Improve code coverage for cli package by @megha1188 in
https://github.com/google-gemini/gemini-cli/pull/13724
- Add session subtask in /stats command by @Adib234 in
https://github.com/google-gemini/gemini-cli/pull/13750
- feat(core): Migrate chatCompressionService to model configs. by @joshualitt in
https://github.com/google-gemini/gemini-cli/pull/12863
- feat(hooks): Hook Telemetry Infrastructure by @Edilmo in
https://github.com/google-gemini/gemini-cli/pull/9082
- fix: (some minor improvements to configs and getPackageJson return behaviour)
by @grMLEqomlkkU5Eeinz4brIrOVCUCkJuN in
https://github.com/google-gemini/gemini-cli/pull/12510
- feat(hooks): Hook Event Handling by @Edilmo in
https://github.com/google-gemini/gemini-cli/pull/9097
- feat(hooks): Hook Agent Lifecycle Integration by @Edilmo in
https://github.com/google-gemini/gemini-cli/pull/9105
- feat(core): Land bool for alternate system prompt. by @joshualitt in
https://github.com/google-gemini/gemini-cli/pull/13764
- bug(core): Add default chat compression config. by @joshualitt in
https://github.com/google-gemini/gemini-cli/pull/13766
- feat(model-availability): introduce ModelPolicy and PolicyCatalog by
@adamfweidman in https://github.com/google-gemini/gemini-cli/pull/13751
- feat(hooks): Hook System Orchestration by @Edilmo in
https://github.com/google-gemini/gemini-cli/pull/9102
- feat(config): add isModelAvailabilityServiceEnabled setting by @adamfweidman
in https://github.com/google-gemini/gemini-cli/pull/13777
- chore/release: bump version to 0.19.0-nightly.20251125.f6d97d448 by
@gemini-cli-robot in https://github.com/google-gemini/gemini-cli/pull/13782
- chore: remove console.error by @adamfweidman in
https://github.com/google-gemini/gemini-cli/pull/13779
- fix: Add $schema property to settings.schema.json by @sacrosanctic in
https://github.com/google-gemini/gemini-cli/pull/12763
- fix(cli): allow non-GitHub SCP-styled URLs for extension installation by @m0ps
in https://github.com/google-gemini/gemini-cli/pull/13800
- fix(resume): allow passing a prompt via stdin while resuming using --resume by
@bl-ue in https://github.com/google-gemini/gemini-cli/pull/13520
- feat(sessions): add /resume slash command to open the session browser by
@bl-ue in https://github.com/google-gemini/gemini-cli/pull/13621
- docs(sessions): add documentation for chat recording and session management by
@bl-ue in https://github.com/google-gemini/gemini-cli/pull/13667
- Fix TypeError: "URL.parse is not a function" for Node.js < v22 by @macarronesc
in https://github.com/google-gemini/gemini-cli/pull/13698
- fallback to flash for TerminalQuota errors by @sehoon38 in
https://github.com/google-gemini/gemini-cli/pull/13791
- Update Code Wiki README badge by @PatoBeltran in
https://github.com/google-gemini/gemini-cli/pull/13768
- Add Databricks auth support and custom header option to gemini cli by
@AarushiShah in https://github.com/google-gemini/gemini-cli/pull/11893
- Update dependency for modelcontextprotocol/sdk to 1.23.0 by @bbiggs in
https://github.com/google-gemini/gemini-cli/pull/13827
**Full Changelog**:
https://github.com/google-gemini/gemini-cli/compare/v0.18.0-preview.4...v0.19.0-preview.0
-430
View File
@@ -1,430 +0,0 @@
# Gemini CLI changelog
Gemini CLI has three major release channels: nightly, preview, and stable. For
most users, we recommend the stable release.
On this page, you can find information regarding the current releases and
highlights from each release.
For the full changelog, including nightly releases, refer to
[Releases - google-gemini/gemini-cli](https://github.com/google-gemini/gemini-cli/releases)
on GitHub.
## Current Releases
| Release channel | Notes |
| :----------------------------------------- | :---------------------------------------------- |
| Nightly | Nightly release with the most recent changes. |
| [Preview](#release-v0190-preview0-preview) | Experimental features ready for early feedback. |
| [Latest](#release-v0190---v0194-latest) | Stable, recommended for general use. |
## Release v0.19.0 - v0.19.4 (Latest)
## Highlights
- **Zed integration:** Users can now leverage Gemini 3 within the Zed
integration after enabling "Preview Features" in their CLIs `/settings`.
- **Interactive shell:**
- **Click-to-Focus:** Go to `/settings` and enable **Use Alternate Buffer**
WhenUse Alternate Buffer" setting is enabled users can click within the
embedded shell output to focus it for input.
- **Loading phrase:** Clearly indicates when the interactive shell is awaiting
user input. ([vid](https://imgur.com/a/kjK8bUK),
[pr](https://github.com/google-gemini/gemini-cli/pull/12535) by
[@jackwotherspoon](https://github.com/jackwotherspoon))
### What's Changed
- Use lenient MCP output schema validator by @cornmander in
https://github.com/google-gemini/gemini-cli/pull/13521
- Update persistence state to track counts of messages instead of times banner
has been displayed by @Adib234 in
https://github.com/google-gemini/gemini-cli/pull/13428
- update docs for http proxy by @scidomino in
https://github.com/google-gemini/gemini-cli/pull/13538
- move stdio by @jacob314 in
https://github.com/google-gemini/gemini-cli/pull/13528
- chore(release): bump version to 0.19.0-nightly.20251120.8e531dc02 by
@gemini-cli-robot in https://github.com/google-gemini/gemini-cli/pull/13540
- Skip pre-commit hooks for shadow repo (#13331) by @vishvananda in
https://github.com/google-gemini/gemini-cli/pull/13488
- fix(ui): Correct mouse click cursor positioning for wide characters by
@SandyTao520 in https://github.com/google-gemini/gemini-cli/pull/13537
- fix(core): correct bash @P prompt transformation detection by @pyrytakala in
https://github.com/google-gemini/gemini-cli/pull/13544
- Optimize and improve test coverage for cli/src/config by @megha1188 in
https://github.com/google-gemini/gemini-cli/pull/13485
- Improve code coverage for cli/src/ui/privacy package by @megha1188 in
https://github.com/google-gemini/gemini-cli/pull/13493
- docs: fix typos in source code and documentation by @fancive in
https://github.com/google-gemini/gemini-cli/pull/13577
- Improved code coverage for cli/src/zed-integration by @megha1188 in
https://github.com/google-gemini/gemini-cli/pull/13570
- feat(ui): build interactive session browser component by @bl-ue in
https://github.com/google-gemini/gemini-cli/pull/13351
- Fix multiple bugs with auth flow including using the implemented but unused
restart support. by @jacob314 in
https://github.com/google-gemini/gemini-cli/pull/13565
- feat(core): add modelAvailabilityService for managing and tracking model
health by @adamfweidman in
https://github.com/google-gemini/gemini-cli/pull/13426
- docs: fix grammar typo "a MCP" to "an MCP" by @noahacgn in
https://github.com/google-gemini/gemini-cli/pull/13595
- feat: custom loading phrase when interactive shell requires input by
@jackwotherspoon in https://github.com/google-gemini/gemini-cli/pull/12535
- docs: Update uninstall command to reflect multiple extension support by
@JayadityaGit in https://github.com/google-gemini/gemini-cli/pull/13582
- bug(core): Ensure we use thinking budget on fallback to 2.5 by @joshualitt in
https://github.com/google-gemini/gemini-cli/pull/13596
- Remove useModelRouter experimental flag by @Adib234 in
https://github.com/google-gemini/gemini-cli/pull/13593
- feat(docs): Ensure multiline JS objects are rendered properly. by @joshualitt
in https://github.com/google-gemini/gemini-cli/pull/13535
- Fix exp id logging by @owenofbrien in
https://github.com/google-gemini/gemini-cli/pull/13430
- Moved client id logging into createBasicLogEvent by @owenofbrien in
https://github.com/google-gemini/gemini-cli/pull/13607
- Restore bracketed paste mode after external editor exit by @scidomino in
https://github.com/google-gemini/gemini-cli/pull/13606
- feat(core): Add support for custom aliases for model configs. by @joshualitt
in https://github.com/google-gemini/gemini-cli/pull/13546
- feat(core): Add `BaseLlmClient.generateContent`. by @joshualitt in
https://github.com/google-gemini/gemini-cli/pull/13591
- Turn off alternate buffer mode by default. by @jacob314 in
https://github.com/google-gemini/gemini-cli/pull/13623
- fix(cli): Prevent stdout/stderr patching for extension commands by @chrstnb in
https://github.com/google-gemini/gemini-cli/pull/13600
- Improve test coverage for cli/src/ui/components by @megha1188 in
https://github.com/google-gemini/gemini-cli/pull/13598
- Update ink version to 6.4.6 by @jacob314 in
https://github.com/google-gemini/gemini-cli/pull/13631
- chore/release: bump version to 0.19.0-nightly.20251122.42c2e1b21 by
@gemini-cli-robot in https://github.com/google-gemini/gemini-cli/pull/13637
- chore/release: bump version to 0.19.0-nightly.20251123.dadd606c0 by
@gemini-cli-robot in https://github.com/google-gemini/gemini-cli/pull/13675
- chore/release: bump version to 0.19.0-nightly.20251124.e177314a4 by
@gemini-cli-robot in https://github.com/google-gemini/gemini-cli/pull/13713
- fix(core): Fix context window overflow warning for PDF files by @kkitase in
https://github.com/google-gemini/gemini-cli/pull/13548
- feat :rephrasing the extension logging messages to run the explore command
when there are no extensions installed by @JayadityaGit in
https://github.com/google-gemini/gemini-cli/pull/13740
- Improve code coverage for cli package by @megha1188 in
https://github.com/google-gemini/gemini-cli/pull/13724
- Add session subtask in /stats command by @Adib234 in
https://github.com/google-gemini/gemini-cli/pull/13750
- feat(core): Migrate chatCompressionService to model configs. by @joshualitt in
https://github.com/google-gemini/gemini-cli/pull/12863
- feat(hooks): Hook Telemetry Infrastructure by @Edilmo in
https://github.com/google-gemini/gemini-cli/pull/9082
- fix: (some minor improvements to configs and getPackageJson return behaviour)
by @grMLEqomlkkU5Eeinz4brIrOVCUCkJuN in
https://github.com/google-gemini/gemini-cli/pull/12510
- feat(hooks): Hook Event Handling by @Edilmo in
https://github.com/google-gemini/gemini-cli/pull/9097
- feat(hooks): Hook Agent Lifecycle Integration by @Edilmo in
https://github.com/google-gemini/gemini-cli/pull/9105
- feat(core): Land bool for alternate system prompt. by @joshualitt in
https://github.com/google-gemini/gemini-cli/pull/13764
- bug(core): Add default chat compression config. by @joshualitt in
https://github.com/google-gemini/gemini-cli/pull/13766
- feat(model-availability): introduce ModelPolicy and PolicyCatalog by
@adamfweidman in https://github.com/google-gemini/gemini-cli/pull/13751
- feat(hooks): Hook System Orchestration by @Edilmo in
https://github.com/google-gemini/gemini-cli/pull/9102
- feat(config): add isModelAvailabilityServiceEnabled setting by @adamfweidman
in https://github.com/google-gemini/gemini-cli/pull/13777
- chore/release: bump version to 0.19.0-nightly.20251125.f6d97d448 by
@gemini-cli-robot in https://github.com/google-gemini/gemini-cli/pull/13782
- chore: remove console.error by @adamfweidman in
https://github.com/google-gemini/gemini-cli/pull/13779
- fix: Add $schema property to settings.schema.json by @sacrosanctic in
https://github.com/google-gemini/gemini-cli/pull/12763
- fix(cli): allow non-GitHub SCP-styled URLs for extension installation by @m0ps
in https://github.com/google-gemini/gemini-cli/pull/13800
- fix(resume): allow passing a prompt via stdin while resuming using --resume by
@bl-ue in https://github.com/google-gemini/gemini-cli/pull/13520
- feat(sessions): add /resume slash command to open the session browser by
@bl-ue in https://github.com/google-gemini/gemini-cli/pull/13621
- docs(sessions): add documentation for chat recording and session management by
@bl-ue in https://github.com/google-gemini/gemini-cli/pull/13667
- Fix TypeError: "URL.parse is not a function" for Node.js < v22 by @macarronesc
in https://github.com/google-gemini/gemini-cli/pull/13698
- fallback to flash for TerminalQuota errors by @sehoon38 in
https://github.com/google-gemini/gemini-cli/pull/13791
- Update Code Wiki README badge by @PatoBeltran in
https://github.com/google-gemini/gemini-cli/pull/13768
- Add Databricks auth support and custom header option to gemini cli by
@AarushiShah in https://github.com/google-gemini/gemini-cli/pull/11893
- Update dependency for modelcontextprotocol/sdk to 1.23.0 by @bbiggs in
https://github.com/google-gemini/gemini-cli/pull/13827
- fix(patch): cherry-pick 576fda1 to release/v0.19.0-preview.0-pr-14099
[CONFLICTS] by @gemini-cli-robot in
https://github.com/google-gemini/gemini-cli/pull/14402
**Full Changelog**:
https://github.com/google-gemini/gemini-cli/compare/v0.18.4...v0.19.0
## Release v0.19.0-preview.0 (Preview)
### What's Changed
- Use lenient MCP output schema validator by @cornmander in
https://github.com/google-gemini/gemini-cli/pull/13521
- Update persistence state to track counts of messages instead of times banner
has been displayed by @Adib234 in
https://github.com/google-gemini/gemini-cli/pull/13428
- update docs for http proxy by @scidomino in
https://github.com/google-gemini/gemini-cli/pull/13538
- move stdio by @jacob314 in
https://github.com/google-gemini/gemini-cli/pull/13528
- chore(release): bump version to 0.19.0-nightly.20251120.8e531dc02 by
@gemini-cli-robot in https://github.com/google-gemini/gemini-cli/pull/13540
- Skip pre-commit hooks for shadow repo (#13331) by @vishvananda in
https://github.com/google-gemini/gemini-cli/pull/13488
- fix(ui): Correct mouse click cursor positioning for wide characters by
@SandyTao520 in https://github.com/google-gemini/gemini-cli/pull/13537
- fix(core): correct bash @P prompt transformation detection by @pyrytakala in
https://github.com/google-gemini/gemini-cli/pull/13544
- Optimize and improve test coverage for cli/src/config by @megha1188 in
https://github.com/google-gemini/gemini-cli/pull/13485
- Improve code coverage for cli/src/ui/privacy package by @megha1188 in
https://github.com/google-gemini/gemini-cli/pull/13493
- docs: fix typos in source code and documentation by @fancive in
https://github.com/google-gemini/gemini-cli/pull/13577
- Improved code coverage for cli/src/zed-integration by @megha1188 in
https://github.com/google-gemini/gemini-cli/pull/13570
- feat(ui): build interactive session browser component by @bl-ue in
https://github.com/google-gemini/gemini-cli/pull/13351
- Fix multiple bugs with auth flow including using the implemented but unused
restart support. by @jacob314 in
https://github.com/google-gemini/gemini-cli/pull/13565
- feat(core): add modelAvailabilityService for managing and tracking model
health by @adamfweidman in
https://github.com/google-gemini/gemini-cli/pull/13426
- docs: fix grammar typo "a MCP" to "an MCP" by @noahacgn in
https://github.com/google-gemini/gemini-cli/pull/13595
- feat: custom loading phrase when interactive shell requires input by
@jackwotherspoon in https://github.com/google-gemini/gemini-cli/pull/12535
- docs: Update uninstall command to reflect multiple extension support by
@JayadityaGit in https://github.com/google-gemini/gemini-cli/pull/13582
- bug(core): Ensure we use thinking budget on fallback to 2.5 by @joshualitt in
https://github.com/google-gemini/gemini-cli/pull/13596
- Remove useModelRouter experimental flag by @Adib234 in
https://github.com/google-gemini/gemini-cli/pull/13593
- feat(docs): Ensure multiline JS objects are rendered properly. by @joshualitt
in https://github.com/google-gemini/gemini-cli/pull/13535
- Fix exp id logging by @owenofbrien in
https://github.com/google-gemini/gemini-cli/pull/13430
- Moved client id logging into createBasicLogEvent by @owenofbrien in
https://github.com/google-gemini/gemini-cli/pull/13607
- Restore bracketed paste mode after external editor exit by @scidomino in
https://github.com/google-gemini/gemini-cli/pull/13606
- feat(core): Add support for custom aliases for model configs. by @joshualitt
in https://github.com/google-gemini/gemini-cli/pull/13546
- feat(core): Add `BaseLlmClient.generateContent`. by @joshualitt in
https://github.com/google-gemini/gemini-cli/pull/13591
- Turn off alternate buffer mode by default. by @jacob314 in
https://github.com/google-gemini/gemini-cli/pull/13623
- fix(cli): Prevent stdout/stderr patching for extension commands by @chrstnb in
https://github.com/google-gemini/gemini-cli/pull/13600
- Improve test coverage for cli/src/ui/components by @megha1188 in
https://github.com/google-gemini/gemini-cli/pull/13598
- Update ink version to 6.4.6 by @jacob314 in
https://github.com/google-gemini/gemini-cli/pull/13631
- chore/release: bump version to 0.19.0-nightly.20251122.42c2e1b21 by
@gemini-cli-robot in https://github.com/google-gemini/gemini-cli/pull/13637
- chore/release: bump version to 0.19.0-nightly.20251123.dadd606c0 by
@gemini-cli-robot in https://github.com/google-gemini/gemini-cli/pull/13675
- chore/release: bump version to 0.19.0-nightly.20251124.e177314a4 by
@gemini-cli-robot in https://github.com/google-gemini/gemini-cli/pull/13713
- fix(core): Fix context window overflow warning for PDF files by @kkitase in
https://github.com/google-gemini/gemini-cli/pull/13548
- feat :rephrasing the extension logging messages to run the explore command
when there are no extensions installed by @JayadityaGit in
https://github.com/google-gemini/gemini-cli/pull/13740
- Improve code coverage for cli package by @megha1188 in
https://github.com/google-gemini/gemini-cli/pull/13724
- Add session subtask in /stats command by @Adib234 in
https://github.com/google-gemini/gemini-cli/pull/13750
- feat(core): Migrate chatCompressionService to model configs. by @joshualitt in
https://github.com/google-gemini/gemini-cli/pull/12863
- feat(hooks): Hook Telemetry Infrastructure by @Edilmo in
https://github.com/google-gemini/gemini-cli/pull/9082
- fix: (some minor improvements to configs and getPackageJson return behaviour)
by @grMLEqomlkkU5Eeinz4brIrOVCUCkJuN in
https://github.com/google-gemini/gemini-cli/pull/12510
- feat(hooks): Hook Event Handling by @Edilmo in
https://github.com/google-gemini/gemini-cli/pull/9097
- feat(hooks): Hook Agent Lifecycle Integration by @Edilmo in
https://github.com/google-gemini/gemini-cli/pull/9105
- feat(core): Land bool for alternate system prompt. by @joshualitt in
https://github.com/google-gemini/gemini-cli/pull/13764
- bug(core): Add default chat compression config. by @joshualitt in
https://github.com/google-gemini/gemini-cli/pull/13766
- feat(model-availability): introduce ModelPolicy and PolicyCatalog by
@adamfweidman in https://github.com/google-gemini/gemini-cli/pull/13751
- feat(hooks): Hook System Orchestration by @Edilmo in
https://github.com/google-gemini/gemini-cli/pull/9102
- feat(config): add isModelAvailabilityServiceEnabled setting by @adamfweidman
in https://github.com/google-gemini/gemini-cli/pull/13777
- chore/release: bump version to 0.19.0-nightly.20251125.f6d97d448 by
@gemini-cli-robot in https://github.com/google-gemini/gemini-cli/pull/13782
- chore: remove console.error by @adamfweidman in
https://github.com/google-gemini/gemini-cli/pull/13779
- fix: Add $schema property to settings.schema.json by @sacrosanctic in
https://github.com/google-gemini/gemini-cli/pull/12763
- fix(cli): allow non-GitHub SCP-styled URLs for extension installation by @m0ps
in https://github.com/google-gemini/gemini-cli/pull/13800
- fix(resume): allow passing a prompt via stdin while resuming using --resume by
@bl-ue in https://github.com/google-gemini/gemini-cli/pull/13520
- feat(sessions): add /resume slash command to open the session browser by
@bl-ue in https://github.com/google-gemini/gemini-cli/pull/13621
- docs(sessions): add documentation for chat recording and session management by
@bl-ue in https://github.com/google-gemini/gemini-cli/pull/13667
- Fix TypeError: "URL.parse is not a function" for Node.js < v22 by @macarronesc
in https://github.com/google-gemini/gemini-cli/pull/13698
- fallback to flash for TerminalQuota errors by @sehoon38 in
https://github.com/google-gemini/gemini-cli/pull/13791
- Update Code Wiki README badge by @PatoBeltran in
https://github.com/google-gemini/gemini-cli/pull/13768
- Add Databricks auth support and custom header option to gemini cli by
@AarushiShah in https://github.com/google-gemini/gemini-cli/pull/11893
- Update dependency for modelcontextprotocol/sdk to 1.23.0 by @bbiggs in
https://github.com/google-gemini/gemini-cli/pull/13827
**Full Changelog**:
https://github.com/google-gemini/gemini-cli/compare/v0.18.0-preview.4...v0.19.0-preview.0
## Release v0.18.0 - v0.18.4
### Highlights
- **Experimental permission improvements**: We're experimenting with a new
policy engine in Gemini CLI, letting users and administrators create
fine-grained policies for tool calls. This setting is currently behind a flag.
See our [policy engine documentation](../core/policy-engine.md) to learn how
to use this feature.
- **Gemini 3 support rolled out for some users**: Some users can now enable
Gemini 3 by using the `/settings` flag and toggling **Preview Features**. See
our [Gemini 3 on Gemini CLI documentation](../get-started/gemini-3.md) to find
out more about using Gemini 3.
- **Updated UI rollback:** We've temporarily rolled back a previous UI update,
which enabled embedded scrolling and mouse support. This can be re-enabled by
using the `/settings` command and setting **Use Alternate Screen Buffer** to
`true`.
- **Display your model in your chat history**: You can now go use `/settings`
and turn on **Show Model in Chat** to display the model in your chat history.
- **Uninstall multiple extensions**: You can uninstall multiple extensions with
a single command: `gemini extensions uninstall`.
![Uninstalling Gemini extensions with a single command](https://i.imgur.com/pi7nEBI.png)
### What's changed
- Remove obsolete reference to "help wanted" label in CONTRIBUTING.md by
@aswinashok44 in https://github.com/google-gemini/gemini-cli/pull/13291
- chore(release): v0.18.0-nightly.20251118.86828bb56 by @skeshive in
https://github.com/google-gemini/gemini-cli/pull/13309
- Docs: Access clarification. by @jkcinouye in
https://github.com/google-gemini/gemini-cli/pull/13304
- Fix links in Gemini 3 Pro documentation by @gmackall in
https://github.com/google-gemini/gemini-cli/pull/13312
- Improve keyboard code parsing by @scidomino in
https://github.com/google-gemini/gemini-cli/pull/13307
- fix(core): Ensure `read_many_files` tool is available to zed. by @joshualitt
in https://github.com/google-gemini/gemini-cli/pull/13338
- Support 3-parameter modifyOtherKeys sequences by @scidomino in
https://github.com/google-gemini/gemini-cli/pull/13342
- Improve pty resize error handling for Windows by @galz10 in
https://github.com/google-gemini/gemini-cli/pull/13353
- fix(ui): Clear input prompt on Escape key press by @SandyTao520 in
https://github.com/google-gemini/gemini-cli/pull/13335
- bug(ui) showLineNumbers had the wrong default value. by @jacob314 in
https://github.com/google-gemini/gemini-cli/pull/13356
- fix(cli): fix crash on startup in NO_COLOR mode (#13343) due to ungua… by
@avilladsen in https://github.com/google-gemini/gemini-cli/pull/13352
- fix: allow MCP prompts with spaces in name by @jackwotherspoon in
https://github.com/google-gemini/gemini-cli/pull/12910
- Refactor createTransport to duplicate less code by @davidmcwherter in
https://github.com/google-gemini/gemini-cli/pull/13010
- Followup from #10719 by @bl-ue in
https://github.com/google-gemini/gemini-cli/pull/13243
- Capturing github action workflow name if present and send it to clearcut by
@MJjainam in https://github.com/google-gemini/gemini-cli/pull/13132
- feat(sessions): record interactive-only errors and warnings to chat recording
JSON files by @bl-ue in https://github.com/google-gemini/gemini-cli/pull/13300
- fix(zed-integration): Correctly handle cancellation errors by @benbrandt in
https://github.com/google-gemini/gemini-cli/pull/13399
- docs: Add Code Wiki link to README by @holtskinner in
https://github.com/google-gemini/gemini-cli/pull/13289
- Restore keyboard mode when exiting the editor by @scidomino in
https://github.com/google-gemini/gemini-cli/pull/13350
- feat(core, cli): Bump genai version to 1.30.0 by @joshualitt in
https://github.com/google-gemini/gemini-cli/pull/13435
- [cli-ui] Keep header ASCII art colored on non-gradient terminals (#13373) by
@bniladridas in https://github.com/google-gemini/gemini-cli/pull/13374
- Fix Copyright line in LICENSE by @scidomino in
https://github.com/google-gemini/gemini-cli/pull/13449
- Fix typo in write_todos methodology instructions by @Smetalo in
https://github.com/google-gemini/gemini-cli/pull/13411
- feat: update thinking mode support to exclude gemini-2.0 models and simplify
logic. by @kevin-ramdass in
https://github.com/google-gemini/gemini-cli/pull/13454
- remove unneeded log by @scidomino in
https://github.com/google-gemini/gemini-cli/pull/13456
- feat: add click-to-focus support for interactive shell by @galz10 in
https://github.com/google-gemini/gemini-cli/pull/13341
- Add User email detail to about box by @ptone in
https://github.com/google-gemini/gemini-cli/pull/13459
- feat(core): Wire up chat code path for model configs. by @joshualitt in
https://github.com/google-gemini/gemini-cli/pull/12850
- chore/release: bump version to 0.18.0-nightly.20251120.2231497b1 by
@gemini-cli-robot in https://github.com/google-gemini/gemini-cli/pull/13476
- feat(core): Fix bug with incorrect model overriding. by @joshualitt in
https://github.com/google-gemini/gemini-cli/pull/13477
- Use synchronous writes when detecting keyboard modes by @scidomino in
https://github.com/google-gemini/gemini-cli/pull/13478
- fix(cli): prevent race condition when restoring prompt after context overflow
by @SandyTao520 in https://github.com/google-gemini/gemini-cli/pull/13473
- Revert "feat(core): Fix bug with incorrect model overriding." by @adamfweidman
in https://github.com/google-gemini/gemini-cli/pull/13483
- Fix: Update system instruction when GEMINI.md memory is loaded or refreshed by
@lifefloating in https://github.com/google-gemini/gemini-cli/pull/12136
- fix(zed-integration): Ensure that the zed integration is classified as
interactive by @benbrandt in
https://github.com/google-gemini/gemini-cli/pull/13394
- Copy commands as part of setup-github by @gsehgal in
https://github.com/google-gemini/gemini-cli/pull/13464
- Update banner design by @Adib234 in
https://github.com/google-gemini/gemini-cli/pull/13420
- Protect stdout and stderr so JavaScript code can't accidentally write to
stdout corrupting ink rendering by @jacob314 in
https://github.com/google-gemini/gemini-cli/pull/13247
- Enable switching preview features on/off without restart by @Adib234 in
https://github.com/google-gemini/gemini-cli/pull/13515
- feat(core): Use thinking level for Gemini 3 by @joshualitt in
https://github.com/google-gemini/gemini-cli/pull/13445
- Change default compress threshold to 0.5 for api key users by @scidomino in
https://github.com/google-gemini/gemini-cli/pull/13517
- remove duplicated mouse code by @scidomino in
https://github.com/google-gemini/gemini-cli/pull/13525
- feat(zed-integration): Use default model routing for Zed integration by
@benbrandt in https://github.com/google-gemini/gemini-cli/pull/13398
- feat(core): Incorporate Gemini 3 into model config hierarchy. by @joshualitt
in https://github.com/google-gemini/gemini-cli/pull/13447
- fix(patch): cherry-pick 5e218a5 to release/v0.18.0-preview.0-pr-13623 to patch
version v0.18.0-preview.0 and create version 0.18.0-preview.1 by
@gemini-cli-robot in https://github.com/google-gemini/gemini-cli/pull/13626
- fix(patch): cherry-pick d351f07 to release/v0.18.0-preview.1-pr-12535 to patch
version v0.18.0-preview.1 and create version 0.18.0-preview.2 by
@gemini-cli-robot in https://github.com/google-gemini/gemini-cli/pull/13813
- fix(patch): cherry-pick 3e50be1 to release/v0.18.0-preview.2-pr-13428 to patch
version v0.18.0-preview.2 and create version 0.18.0-preview.3 by
@gemini-cli-robot in https://github.com/google-gemini/gemini-cli/pull/13821
- fix(patch): cherry-pick d8a3d08 to release/v0.18.0-preview.3-pr-13791 to patch
version v0.18.0-preview.3 and create version 0.18.0-preview.4 by
@gemini-cli-robot in https://github.com/google-gemini/gemini-cli/pull/13826
**Full Changelog**:
https://github.com/google-gemini/gemini-cli/compare/v0.17.1...v0.18.0
+1 -77
View File
@@ -159,50 +159,6 @@ This results in the following merged configuration:
By using the system settings file, you can enforce the security and
configuration patterns described below.
### Enforcing system settings with a wrapper script
While the `GEMINI_CLI_SYSTEM_SETTINGS_PATH` environment variable provides
flexibility, a user could potentially override it to point to a different
settings file, bypassing the centrally managed configuration. To mitigate this,
enterprises can deploy a wrapper script or alias that ensures the environment
variable is always set to the corporate-controlled path.
This approach ensures that no matter how the user calls the `gemini` command,
the enterprise settings are always loaded with the highest precedence.
**Example wrapper script:**
Administrators can create a script named `gemini` and place it in a directory
that appears earlier in the user's `PATH` than the actual Gemini CLI binary
(e.g., `/usr/local/bin/gemini`).
```bash
#!/bin/bash
# Enforce the path to the corporate system settings file.
# This ensures that the company's configuration is always applied.
export GEMINI_CLI_SYSTEM_SETTINGS_PATH="/etc/gemini-cli/settings.json"
# Find the original gemini executable.
# This is a simple example; a more robust solution might be needed
# depending on the installation method.
REAL_GEMINI_PATH=$(type -aP gemini | grep -v "^$(type -P gemini)$" | head -n 1)
if [ -z "$REAL_GEMINI_PATH" ]; then
echo "Error: The original 'gemini' executable was not found." >&2
exit 1
fi
# Pass all arguments to the real Gemini CLI executable.
exec "$REAL_GEMINI_PATH" "$@"
```
By deploying this script, the `GEMINI_CLI_SYSTEM_SETTINGS_PATH` is set within
the script's environment, and the `exec` command replaces the script process
with the actual Gemini CLI process, which inherits the environment variable.
This makes it significantly more difficult for a user to bypass the enforced
settings.
## Restricting tool access
You can significantly enhance security by controlling which tools the Gemini
@@ -478,39 +434,7 @@ prompted to switch to the enforced method. In non-interactive mode, the CLI will
exit with an error if the configured authentication method does not match the
enforced one.
### Restricting logins to corporate domains
For enterprises using Google Workspace, you can enforce that users only
authenticate with their corporate Google accounts. This is a network-level
control that is configured on a proxy server, not within Gemini CLI itself. It
works by intercepting authentication requests to Google and adding a special
HTTP header.
This policy prevents users from logging in with personal Gmail accounts or other
non-corporate Google accounts.
For detailed instructions, see the Google Workspace Admin Help article on
[blocking access to consumer accounts](https://support.google.com/a/answer/1668854?hl=en#zippy=%2Cstep-choose-a-web-proxy-server%2Cstep-configure-the-network-to-block-certain-accounts).
The general steps are as follows:
1. **Intercept Requests**: Configure your web proxy to intercept all requests
to `google.com`.
2. **Add HTTP Header**: For each intercepted request, add the
`X-GoogApps-Allowed-Domains` HTTP header.
3. **Specify Domains**: The value of the header should be a comma-separated
list of your approved Google Workspace domain names.
**Example header:**
```
X-GoogApps-Allowed-Domains: my-corporate-domain.com, secondary-domain.com
```
When this header is present, Google's authentication service will only allow
logins from accounts belonging to the specified domains.
## Putting it all together: example system `settings.json`
## Putting it all together: Example system `settings.json`
Here is an example of a system `settings.json` file that combines several of the
patterns discussed above to create a secure, controlled environment for Gemini
+27 -31
View File
@@ -1,13 +1,8 @@
# Gemini CLI model selection (`/model` command)
Select your Gemini CLI model. The `/model` command lets you configure the model
used by Gemini CLI, giving you more control over your results. Use **Pro**
models for complex tasks and reasoning, **Flash** models for high speed results,
or the (recommended) **Auto** setting to choose the best model for your tasks.
> **Note:** The `/model` command (and the `--model` flag) does not override the
> model used by sub-agents. Consequently, even when using the `/model` flag you
> may see other models used in your model usage reports.
Select your Gemini CLI model. The `/model` command opens a dialog where you can
configure the model used by Gemini CLI, giving you more control over your
results.
## How to use the `/model` command
@@ -17,25 +12,26 @@ Use the following command in Gemini CLI:
/model
```
Running this command will open a dialog with your options:
Running this command will open a dialog with your model options:
| Option | Description | Models |
| ----------------- | -------------------------------------------------------------- | ---------------------------------------------------------------------- |
| Auto (Gemini 3) | Let the system choose the best Gemini 3 model for your task. | gemini-3-pro-preview (if enabled), gemini-3-flash-preview (if enabled) |
| Auto (Gemini 2.5) | Let the system choose the best Gemini 2.5 model for your task. | gemini-2.5-pro, gemini-2.5-flash |
| Manual | Select a specific model. | Any available model. |
| Option | Description | Models |
| ------------------ | ------------------------------------------------------------- | ------------------------------------------------------------------------------------------ |
| Auto (recommended) | Let the system choose the best model for your task. | gemini-3-pro-preview (if enabled), gemini-2.5-pro, gemini-2.5-flash, gemini-2.5-flash-lite |
| Pro | For complex tasks that require deep reasoning and creativity. | gemini-3-pro-preview (if enabled), gemini-2.5-pro |
| Flash | For tasks that need a balance of speed and reasoning. | gemini-2.5-flash |
| Flash-Lite | For simple tasks that need to be done quickly. | gemini-2.5-flash-lite |
We recommend selecting one of the above **Auto** options. However, you can
select **Manual** to select a specific model from those available.
### Gemini 3 Pro and preview features
### Gemini 3 and preview features
Note: Gemini 3 is not currently available on all account types. To learn more
about Gemini 3 access, refer to
[Gemini 3 Pro on Gemini CLI](../get-started/gemini-3.md).
> **Note:** Gemini 3 is not currently available on all account types. To learn
> more about Gemini 3 access, refer to
> [Gemini 3 on Gemini CLI](../get-started/gemini-3.md).
To enable Gemini 3 Pro and Gemini 3 Flash (if available), enable
[**Preview Features** by using the `settings` command](../cli/settings.md).
To enable Gemini 3 Pro (if available), enable
[**Preview features** by using the `settings` command](../cli/settings.md). Once
enabled, Gemini CLI will attempt to use Gemini 3 Pro when you select **Auto** or
**Pro**. Both **Auto** and **Pro** will try to use Gemini 3 Pro before falling
back to Gemini 2.5 Pro.
You can also use the `--model` flag to specify a particular Gemini model on
startup. For more details, refer to the
@@ -46,16 +42,16 @@ Gemini CLI.
## Best practices for model selection
- **Default to Auto.** For most users, the _Auto_ option model provides a
balance between speed and performance, automatically selecting the correct
model based on the complexity of the task. Example: Developing a web
application could include a mix of complex tasks (building architecture and
scaffolding the project) and simple tasks (generating CSS).
- **Default to Auto (recommended).** For most users, the _Auto (recommended)_
model provides a balance between speed and performance, automatically
selecting the correct model based on the complexity of the task. Example:
Developing a web application could include a mix of complex tasks (building
architecture and scaffolding the project) and simple tasks (generating CSS).
- **Switch to Pro if you aren't getting the results you want.** If you think you
need your model to be a little "smarter," you can manually select Pro. Pro
will provide you with the highest levels of reasoning and creativity. Example:
A complex or multi-stage debugging task.
need your model to be a little "smarter," use Pro. Pro will provide you with
the highest levels of reasoning and creativity. Example: A complex or
multi-stage debugging task.
- **Switch to Flash or Flash-Lite if you need faster results.** If you need a
simple response quickly, Flash or Flash-Lite is the best option. Example:
+9 -38
View File
@@ -74,16 +74,15 @@ observability framework — Gemini CLI's observability system provides:
All telemetry behavior is controlled through your `.gemini/settings.json` file.
Environment variables can be used to override the settings in the file.
| Setting | Environment Variable | Description | Values | Default |
| -------------- | -------------------------------- | --------------------------------------------------- | ----------------- | ----------------------- |
| `enabled` | `GEMINI_TELEMETRY_ENABLED` | Enable or disable telemetry | `true`/`false` | `false` |
| `target` | `GEMINI_TELEMETRY_TARGET` | Where to send telemetry data | `"gcp"`/`"local"` | `"local"` |
| `otlpEndpoint` | `GEMINI_TELEMETRY_OTLP_ENDPOINT` | OTLP collector endpoint | URL string | `http://localhost:4317` |
| `otlpProtocol` | `GEMINI_TELEMETRY_OTLP_PROTOCOL` | OTLP transport protocol | `"grpc"`/`"http"` | `"grpc"` |
| `outfile` | `GEMINI_TELEMETRY_OUTFILE` | Save telemetry to file (overrides `otlpEndpoint`) | file path | - |
| `logPrompts` | `GEMINI_TELEMETRY_LOG_PROMPTS` | Include prompts in telemetry logs | `true`/`false` | `true` |
| `useCollector` | `GEMINI_TELEMETRY_USE_COLLECTOR` | Use external OTLP collector (advanced) | `true`/`false` | `false` |
| `useCliAuth` | `GEMINI_TELEMETRY_USE_CLI_AUTH` | Use CLI credentials for telemetry (GCP target only) | `true`/`false` | `false` |
| Setting | Environment Variable | Description | Values | Default |
| -------------- | -------------------------------- | ------------------------------------------------- | ----------------- | ----------------------- |
| `enabled` | `GEMINI_TELEMETRY_ENABLED` | Enable or disable telemetry | `true`/`false` | `false` |
| `target` | `GEMINI_TELEMETRY_TARGET` | Where to send telemetry data | `"gcp"`/`"local"` | `"local"` |
| `otlpEndpoint` | `GEMINI_TELEMETRY_OTLP_ENDPOINT` | OTLP collector endpoint | URL string | `http://localhost:4317` |
| `otlpProtocol` | `GEMINI_TELEMETRY_OTLP_PROTOCOL` | OTLP transport protocol | `"grpc"`/`"http"` | `"grpc"` |
| `outfile` | `GEMINI_TELEMETRY_OUTFILE` | Save telemetry to file (overrides `otlpEndpoint`) | file path | - |
| `logPrompts` | `GEMINI_TELEMETRY_LOG_PROMPTS` | Include prompts in telemetry logs | `true`/`false` | `true` |
| `useCollector` | `GEMINI_TELEMETRY_USE_COLLECTOR` | Use external OTLP collector (advanced) | `true`/`false` | `false` |
**Note on boolean environment variables:** For the boolean settings (`enabled`,
`logPrompts`, `useCollector`), setting the corresponding environment variable to
@@ -131,34 +130,6 @@ Before using either method below, complete these steps:
--project="$OTLP_GOOGLE_CLOUD_PROJECT"
```
### Authenticating with CLI Credentials
By default, the telemetry collector for Google Cloud uses Application Default
Credentials (ADC). However, you can configure it to use the same OAuth
credentials that you use to log in to the Gemini CLI. This is useful in
environments where you don't have ADC set up.
To enable this, set the `useCliAuth` property in your `telemetry` settings to
`true`:
```json
{
"telemetry": {
"enabled": true,
"target": "gcp",
"useCliAuth": true
}
}
```
**Important:**
- This setting requires the use of **Direct Export** (in-process exporters).
- It **cannot** be used with `useCollector: true`. If you enable both, telemetry
will be disabled and an error will be logged.
- The CLI will automatically use your credentials to authenticate with Google
Cloud Trace, Metrics, and Logging APIs.
### Direct export (recommended)
Sends telemetry directly to Google Cloud services. No collector needed.
+3 -18
View File
@@ -163,11 +163,11 @@ The file has the following structure:
your extension in the CLI. Note that we expect this name to match the
extension directory name.
- `version`: The version of the extension.
- `mcpServers`: A map of MCP servers to settings. The key is the name of the
- `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 settingsd in a
loaded on startup just like MCP servers configured in a
[`settings.json` file](../get-started/configuration.md). If both an extension
and a `settings.json` file settings an MCP server with the same name, the
and a `settings.json` file configure an MCP server with the same name, the
server defined in the `settings.json` file takes precedence.
- Note that all MCP server configuration options are supported except for
`trust`.
@@ -223,21 +223,6 @@ When a user installs this extension, they will be prompted to enter their API
key. The value will be saved to a `.env` file in the extension's directory
(e.g., `<home>/.gemini/extensions/my-api-extension/.env`).
You can view a list of an extension's settings by running:
```
gemini extensions settings list <extension name>
```
and you can update a given setting using:
```
gemini extensions settings set <extension name> <setting name> [--scope <scope>]
```
- `--scope`: The scope to set the setting in (`user` or `workspace`). This is
optional and will default to `user`.
### Custom commands
Extensions can provide [custom commands](../cli/custom-commands.md) by placing
+6 -77
View File
@@ -367,12 +367,6 @@ their corresponding top-level category object in your `settings.json` file.
"model": "gemini-3-pro-preview"
}
},
"gemini-3-flash-preview": {
"extends": "chat-base-3",
"modelConfig": {
"model": "gemini-3-flash-preview"
}
},
"gemini-2.5-pro": {
"extends": "chat-base-2.5",
"modelConfig": {
@@ -502,11 +496,6 @@ their corresponding top-level category object in your `settings.json` file.
"model": "gemini-3-pro-preview"
}
},
"chat-compression-3-flash": {
"modelConfig": {
"model": "gemini-3-flash-preview"
}
},
"chat-compression-2.5-pro": {
"modelConfig": {
"model": "gemini-2.5-pro"
@@ -679,7 +668,7 @@ their corresponding top-level category object in your `settings.json` file.
integration. When enabled, tools automatically respect policy engine
decisions (ALLOW/DENY/ASK_USER) without requiring individual tool
implementations.
- **Default:** `true`
- **Default:** `false`
- **Requires restart:** Yes
- **`tools.enableHooks`** (boolean):
@@ -778,11 +767,6 @@ their corresponding top-level category object in your `settings.json` file.
#### `experimental`
- **`experimental.enableAgents`** (boolean):
- **Description:** Enable local and remote subagents.
- **Default:** `false`
- **Requires restart:** Yes
- **`experimental.extensionManagement`** (boolean):
- **Description:** Enable extension management features.
- **Default:** `true`
@@ -827,70 +811,15 @@ their corresponding top-level category object in your `settings.json` file.
- **`experimental.codebaseInvestigatorSettings.model`** (string):
- **Description:** The model to use for the Codebase Investigator agent.
- **Default:** `"auto"`
- **Default:** `"gemini-2.5-pro"`
- **Requires restart:** Yes
#### `hooks`
- **`hooks.disabled`** (array):
- **Description:** List of hook names (commands) that should be disabled.
Hooks in this list will not execute even if configured.
- **Default:** `[]`
- **`hooks.BeforeTool`** (array):
- **Description:** Hooks that execute before tool execution. Can intercept,
validate, or modify tool calls.
- **Default:** `[]`
- **`hooks.AfterTool`** (array):
- **Description:** Hooks that execute after tool execution. Can process
results, log outputs, or trigger follow-up actions.
- **Default:** `[]`
- **`hooks.BeforeAgent`** (array):
- **Description:** Hooks that execute before agent loop starts. Can set up
context or initialize resources.
- **Default:** `[]`
- **`hooks.AfterAgent`** (array):
- **Description:** Hooks that execute after agent loop completes. Can perform
cleanup or summarize results.
- **Default:** `[]`
- **`hooks.Notification`** (array):
- **Description:** Hooks that execute on notification events (errors,
warnings, info). Can log or alert on specific conditions.
- **Default:** `[]`
- **`hooks.SessionStart`** (array):
- **Description:** Hooks that execute when a session starts. Can initialize
session-specific resources or state.
- **Default:** `[]`
- **`hooks.SessionEnd`** (array):
- **Description:** Hooks that execute when a session ends. Can perform cleanup
or persist session data.
- **Default:** `[]`
- **`hooks.PreCompress`** (array):
- **Description:** Hooks that execute before chat history compression. Can
back up or analyze conversation before compression.
- **Default:** `[]`
- **`hooks.BeforeModel`** (array):
- **Description:** Hooks that execute before LLM requests. Can modify prompts,
inject context, or control model parameters.
- **Default:** `[]`
- **`hooks.AfterModel`** (array):
- **Description:** Hooks that execute after LLM responses. Can process
outputs, extract information, or log interactions.
- **Default:** `[]`
- **`hooks.BeforeToolSelection`** (array):
- **Description:** Hooks that execute before tool selection. Can filter or
prioritize available tools dynamically.
- **Default:** `[]`
- **`hooks`** (object):
- **Description:** Hook configurations for intercepting and customizing agent
behavior.
- **Default:** `{}`
<!-- SETTINGS-AUTOGEN:END -->
#### `mcpServers`
+56 -62
View File
@@ -1,37 +1,64 @@
# Gemini 3 Pro and Gemini 3 Flash on Gemini CLI
# Gemini 3 Pro on Gemini CLI (join the waitlist)
Gemini 3 Pro and Gemini 3 Flash are now available on Gemini CLI! Currently, most
paid customers of Gemini CLI will have access to both Gemini 3 Pro and Gemini 3
Flash, including the following subscribers:
Were excited to bring Gemini 3 Pro to Gemini CLI. For Google AI Ultra users
(Google AI Ultra for Business is not currently supported) and paid Gemini and
Vertex API key holders, Gemini 3 Pro is already available and ready to enable.
For everyone else, we're gradually expanding access
[through a waitlist](https://goo.gle/geminicli-waitlist-signup). Sign up for the
waitlist now to access Gemini 3 Pro once approved.
- Google AI Pro and Google AI Ultra (excluding business customers).
- Gemini Code Assist Standard and Enterprise (requires
[administrative enablement](#administrator-instructions)).
- Paid Gemini API and Vertex API key holders.
**Note:** Please wait until you have been approved to use Gemini 3 Pro to enable
**preview features**. If enabled early, the CLI will fallback to Gemini 2.5 Pro.
For free tier users:
## Do I need to join the waitlist?
- If you signed up for the waitlist, please check your email for details. Weve
onboarded everyone who signed up to the previously available waitlist.
- If you were not on our waitlist, were rolling out additional access gradually
to ensure the experience remains fast and reliable. Stay tuned for more
details.
The following users will be **automatically granted access** to Gemini 3 Pro on
Gemini CLI:
## How to get started with Gemini 3 on Gemini CLI
- Google AI Ultra subscribers (excluding Google AI Ultra for Business, which is
on the roadmap).
- Gemini API key users
[with access to Gemini 3](https://ai.google.dev/gemini-api/docs/rate-limits).
- Vertex API key users
[with access to Gemini 3](https://docs.cloud.google.com/vertex-ai/generative-ai/docs/quotas).
Get started by upgrading Gemini CLI to the latest version (0.21.1):
For **Gemini Code Assist Enterprise users**, access is coming soon.
```bash
npm install -g @google/gemini-cli@latest
```
Users not automatically granted access through one of these account types will
need to join the waitlist. This includes Google AI Pro, Gemini Code Assist
standard, and free tier users.
After youve confirmed your version is 0.21.1 or later:
Note: Whether youre automatically granted access or accepted from the waitlist,
youll still need to enable Gemini 3 Pro
[using the `/settings` command](../cli/settings.md).
1. Use the `/settings` command in Gemini CLI.
2. Toggle **Preview Features** to `true`.
3. Run `/model` and select **Auto (Gemini 3)**.
## How to join the waitlist
For more information, see [Gemini CLI model selection](../cli/model.md).
Users not automatically granted access will need to join the waitlist. Follow
these instructions to sign up:
- Install Gemini CLI.
- Authenticate using the **Login with Google** option. Youll see a banner that
says “Gemini 3 is now available.” If you do not see this banner, update your
installation of Gemini CLI to the most recent version.
- Fill out this Google form:
[Access Gemini 3 in Gemini CLI](https://goo.gle/geminicli-waitlist-signup).
Provide the email address of the account you used to authenticate with Gemini
CLI.
Users will be onboarded in batches, subject to availability. When youve been
granted access to Gemini 3 Pro, youll receive an acceptance email to your
submitted email address.
## How to use Gemini 3 Pro with Gemini CLI
Once you receive your acceptance emailor if you are automatically granted
accessyou still need to enable Gemini 3 Pro within Gemini CLI.
To enable Gemini 3 Pro, use the `/settings` command in Gemini CLI and set
**Preview Features** to `true`.
For more information, see [Gemini CLI Settings](../cli/settings.md).
### Usage limits and fallback
@@ -49,12 +76,12 @@ There may be times when the Gemini 3 Pro model is overloaded. When that happens,
Gemini CLI will ask you to decide whether you want to keep trying Gemini 3 Pro
or fallback to Gemini 2.5 Pro.
> **Note:** The **Keep trying** option uses exponential backoff, in which Gemini
> CLI waits longer between each retry, when the system is busy. If the retry
> doesn't happen immediately, please wait a few minutes for the request to
> process.
**Note:** The **Keep trying** option uses exponential backoff, in which Gemini
CLI waits longer between each retry, when the system is busy. If the retry
doesn't happen immediately, please wait a few minutes for the request to
process.
### Model selection and routing types
## Model selection and routing types
When using Gemini CLI, you may want to control how your requests are routed
between models. By default, Gemini CLI uses **Auto** routing.
@@ -73,39 +100,6 @@ manage your usage limits:
To learn more about selecting a model and routing, refer to
[Gemini CLI Model Selection](../cli/model.md).
## How to enable Gemini 3 with Gemini CLI on Gemini Code Assist
If you're using Gemini Code Assist Standard or Gemini Code Assist Enterprise,
enabling Gemini 3 Pro on Gemini CLI requires configuring your release channels.
Using Gemini 3 Pro will require two steps: administrative enablement and user
enablement.
To learn more about these settings, refer to
[Configure Gemini Code Assist release channels](https://developers.google.com/gemini-code-assist/docs/configure-release-channels).
### Administrator instructions
An administrator with **Google Cloud Settings Admin** permissions must follow
these directions:
- Navigate to the Google Cloud Project you're using with Gemini CLI for Code
Assist.
- Go to **Admin for Gemini** > **Settings**.
- Under **Release channels for Gemini Code Assist in local IDEs** select
**Preview**.
- Click **Save changes**.
### User instructions
Wait for two to three minutes after your administrator has enabled **Preview**,
then:
- Open Gemini CLI.
- Use the `/settings` command.
- Set **Preview Features** to `true`.
Restart Gemini CLI and you should have access to Gemini 3.
## Need help?
If you need help, we recommend searching for an existing
-806
View File
@@ -1,806 +0,0 @@
# Hooks on Gemini CLI: Best practices
This guide covers security considerations, performance optimization, debugging
techniques, and privacy considerations for developing and deploying hooks in
Gemini CLI.
## Security considerations
### Validate all inputs
Never trust data from hooks without validation. Hook inputs may contain
user-provided data that could be malicious:
```bash
#!/usr/bin/env bash
input=$(cat)
# Validate JSON structure
if ! echo "$input" | jq empty 2>/dev/null; then
echo "Invalid JSON input" >&2
exit 1
fi
# Validate required fields
tool_name=$(echo "$input" | jq -r '.tool_name // empty')
if [ -z "$tool_name" ]; then
echo "Missing tool_name field" >&2
exit 1
fi
```
### Use timeouts
Set reasonable timeouts to prevent hooks from hanging indefinitely:
```json
{
"hooks": {
"BeforeTool": [
{
"matcher": "*",
"hooks": [
{
"name": "slow-validator",
"type": "command",
"command": "./hooks/validate.sh",
"timeout": 5000
}
]
}
]
}
}
```
**Recommended timeouts:**
- Fast validation: 1000-5000ms
- Network requests: 10000-30000ms
- Heavy computation: 30000-60000ms
### Limit permissions
Run hooks with minimal required permissions:
```bash
#!/usr/bin/env bash
# Don't run as root
if [ "$EUID" -eq 0 ]; then
echo "Hook should not run as root" >&2
exit 1
fi
# Check file permissions before writing
if [ -w "$file_path" ]; then
# Safe to write
else
echo "Insufficient permissions" >&2
exit 1
fi
```
### Scan for secrets
Use `BeforeTool` hooks to prevent committing sensitive data:
```javascript
const SECRET_PATTERNS = [
/api[_-]?key\s*[:=]\s*['"]?[a-zA-Z0-9_-]{20,}['"]?/i,
/password\s*[:=]\s*['"]?[^\s'"]{8,}['"]?/i,
/secret\s*[:=]\s*['"]?[a-zA-Z0-9_-]{20,}['"]?/i,
/AKIA[0-9A-Z]{16}/, // AWS access key
/ghp_[a-zA-Z0-9]{36}/, // GitHub personal access token
/sk-[a-zA-Z0-9]{48}/, // OpenAI API key
];
function containsSecret(content) {
return SECRET_PATTERNS.some((pattern) => pattern.test(content));
}
```
### Review external scripts
Always review hook scripts from untrusted sources before enabling them:
```bash
# Review before installing
cat third-party-hook.sh | less
# Check for suspicious patterns
grep -E 'curl|wget|ssh|eval' third-party-hook.sh
# Verify hook source
ls -la third-party-hook.sh
```
### Sandbox untrusted hooks
For maximum security, consider running untrusted hooks in isolated environments:
```bash
# Run hook in Docker container
docker run --rm \
-v "$GEMINI_PROJECT_DIR:/workspace:ro" \
-i untrusted-hook-image \
/hook-script.sh < input.json
```
## Performance
### Keep hooks fast
Hooks run synchronously—slow hooks delay the agent loop. Optimize for speed by
using parallel operations:
```javascript
// Sequential operations are slower
const data1 = await fetch(url1).then((r) => r.json());
const data2 = await fetch(url2).then((r) => r.json());
const data3 = await fetch(url3).then((r) => r.json());
// Prefer parallel operations for better performance
const [data1, data2, data3] = await Promise.all([
fetch(url1).then((r) => r.json()),
fetch(url2).then((r) => r.json()),
fetch(url3).then((r) => r.json()),
]);
```
### Cache expensive operations
Store results between invocations to avoid repeated computation:
```javascript
const fs = require('fs');
const path = require('path');
const CACHE_FILE = '.gemini/hook-cache.json';
function readCache() {
try {
return JSON.parse(fs.readFileSync(CACHE_FILE, 'utf8'));
} catch {
return {};
}
}
function writeCache(data) {
fs.writeFileSync(CACHE_FILE, JSON.stringify(data, null, 2));
}
async function main() {
const cache = readCache();
const cacheKey = `tool-list-${(Date.now() / 3600000) | 0}`; // Hourly cache
if (cache[cacheKey]) {
console.log(JSON.stringify(cache[cacheKey]));
return;
}
// Expensive operation
const result = await computeExpensiveResult();
cache[cacheKey] = result;
writeCache(cache);
console.log(JSON.stringify(result));
}
```
### Use appropriate events
Choose hook events that match your use case to avoid unnecessary execution.
`AfterAgent` fires once per agent loop completion, while `AfterModel` fires
after every LLM call (potentially multiple times per loop):
```json
// If checking final completion, use AfterAgent instead of AfterModel
{
"hooks": {
"AfterAgent": [
{
"matcher": "*",
"hooks": [
{
"name": "final-checker",
"command": "./check-completion.sh"
}
]
}
]
}
}
```
### Filter with matchers
Use specific matchers to avoid unnecessary hook execution. Instead of matching
all tools with `*`, specify only the tools you need:
```json
{
"matcher": "WriteFile|Edit",
"hooks": [
{
"name": "validate-writes",
"command": "./validate.sh"
}
]
}
```
### Optimize JSON parsing
For large inputs, use streaming JSON parsers to avoid loading everything into
memory:
```javascript
// Standard approach: parse entire input
const input = JSON.parse(await readStdin());
const content = input.tool_input.content;
// For very large inputs: stream and extract only needed fields
const { createReadStream } = require('fs');
const JSONStream = require('JSONStream');
const stream = createReadStream(0).pipe(JSONStream.parse('tool_input.content'));
let content = '';
stream.on('data', (chunk) => {
content += chunk;
});
```
## Debugging
### Log to files
Write debug information to dedicated log files:
```bash
#!/usr/bin/env bash
LOG_FILE=".gemini/hooks/debug.log"
# Log with timestamp
log() {
echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*" >> "$LOG_FILE"
}
input=$(cat)
log "Received input: ${input:0:100}..."
# Hook logic here
log "Hook completed successfully"
```
### Use stderr for errors
Error messages on stderr are surfaced appropriately based on exit codes:
```javascript
try {
const result = dangerousOperation();
console.log(JSON.stringify({ result }));
} catch (error) {
console.error(`Hook error: ${error.message}`);
process.exit(2); // Blocking error
}
```
### Test hooks independently
Run hook scripts manually with sample JSON input:
```bash
# Create test input
cat > test-input.json << 'EOF'
{
"session_id": "test-123",
"cwd": "/tmp/test",
"hook_event_name": "BeforeTool",
"tool_name": "WriteFile",
"tool_input": {
"file_path": "test.txt",
"content": "Test content"
}
}
EOF
# Test the hook
cat test-input.json | .gemini/hooks/my-hook.sh
# Check exit code
echo "Exit code: $?"
```
### Check exit codes
Ensure your script returns the correct exit code:
```bash
#!/usr/bin/env bash
set -e # Exit on error
# Hook logic
process_input() {
# ...
}
if process_input; then
echo "Success message"
exit 0
else
echo "Error message" >&2
exit 2
fi
```
### Enable telemetry
Hook execution is logged when `telemetry.logPrompts` is enabled:
```json
{
"telemetry": {
"logPrompts": true
}
}
```
View hook telemetry in logs to debug execution issues.
### Use hook panel
The `/hooks panel` command shows execution status and recent output:
```bash
/hooks panel
```
Check for:
- Hook execution counts
- Recent successes/failures
- Error messages
- Execution timing
## Development
### Start simple
Begin with basic logging hooks before implementing complex logic:
```bash
#!/usr/bin/env bash
# Simple logging hook to understand input structure
input=$(cat)
echo "$input" >> .gemini/hook-inputs.log
echo "Logged input"
```
### Use JSON libraries
Parse JSON with proper libraries instead of text processing:
**Bad:**
```bash
# Fragile text parsing
tool_name=$(echo "$input" | grep -oP '"tool_name":\s*"\K[^"]+')
```
**Good:**
```bash
# Robust JSON parsing
tool_name=$(echo "$input" | jq -r '.tool_name')
```
### Make scripts executable
Always make hook scripts executable:
```bash
chmod +x .gemini/hooks/*.sh
chmod +x .gemini/hooks/*.js
```
### Version control
Commit hooks to share with your team:
```bash
git add .gemini/hooks/
git add .gemini/settings.json
git commit -m "Add project hooks for security and testing"
```
**`.gitignore` considerations:**
```gitignore
# Ignore hook cache and logs
.gemini/hook-cache.json
.gemini/hook-debug.log
.gemini/memory/session-*.jsonl
# Keep hook scripts
!.gemini/hooks/*.sh
!.gemini/hooks/*.js
```
### Document behavior
Add descriptions to help others understand your hooks:
```json
{
"hooks": {
"BeforeTool": [
{
"matcher": "WriteFile|Edit",
"hooks": [
{
"name": "secret-scanner",
"type": "command",
"command": "$GEMINI_PROJECT_DIR/.gemini/hooks/block-secrets.sh",
"description": "Scans code changes for API keys, passwords, and other secrets before writing"
}
]
}
]
}
}
```
Add comments in hook scripts:
```javascript
#!/usr/bin/env node
/**
* RAG Tool Filter Hook
*
* This hook reduces the tool space from 100+ tools to ~15 relevant ones
* by extracting keywords from the user's request and filtering tools
* based on semantic similarity.
*
* Performance: ~500ms average, cached tool embeddings
* Dependencies: @google/generative-ai
*/
```
## Troubleshooting
### Hook not executing
**Check hook name in `/hooks panel`:**
```bash
/hooks panel
```
Verify the hook appears in the list and is enabled.
**Verify matcher pattern:**
```bash
# Test regex pattern
echo "WriteFile" | grep -E "Write.*|Edit"
```
**Check disabled list:**
```json
{
"hooks": {
"disabled": ["my-hook-name"]
}
}
```
**Ensure script is executable:**
```bash
ls -la .gemini/hooks/my-hook.sh
chmod +x .gemini/hooks/my-hook.sh
```
**Verify script path:**
```bash
# Check path expansion
echo "$GEMINI_PROJECT_DIR/.gemini/hooks/my-hook.sh"
# Verify file exists
test -f "$GEMINI_PROJECT_DIR/.gemini/hooks/my-hook.sh" && echo "File exists"
```
### Hook timing out
**Check configured timeout:**
```json
{
"name": "slow-hook",
"timeout": 60000
}
```
**Optimize slow operations:**
```javascript
// Before: Sequential operations (slow)
for (const item of items) {
await processItem(item);
}
// After: Parallel operations (fast)
await Promise.all(items.map((item) => processItem(item)));
```
**Use caching:**
```javascript
const cache = new Map();
async function getCachedData(key) {
if (cache.has(key)) {
return cache.get(key);
}
const data = await fetchData(key);
cache.set(key, data);
return data;
}
```
**Consider splitting into multiple faster hooks:**
```json
{
"hooks": {
"BeforeTool": [
{
"matcher": "WriteFile",
"hooks": [
{
"name": "quick-check",
"command": "./quick-validation.sh",
"timeout": 1000
}
]
},
{
"matcher": "WriteFile",
"hooks": [
{
"name": "deep-check",
"command": "./deep-analysis.sh",
"timeout": 30000
}
]
}
]
}
}
```
### Invalid JSON output
**Validate JSON before outputting:**
```bash
#!/usr/bin/env bash
output='{"decision": "allow"}'
# Validate JSON
if echo "$output" | jq empty 2>/dev/null; then
echo "$output"
else
echo "Invalid JSON generated" >&2
exit 1
fi
```
**Ensure proper quoting and escaping:**
```javascript
// Bad: Unescaped string interpolation
const message = `User said: ${userInput}`;
console.log(JSON.stringify({ message }));
// Good: Automatic escaping
console.log(JSON.stringify({ message: `User said: ${userInput}` }));
```
**Check for binary data or control characters:**
```javascript
function sanitizeForJSON(str) {
return str.replace(/[\x00-\x1F\x7F-\x9F]/g, ''); // Remove control chars
}
const cleanContent = sanitizeForJSON(content);
console.log(JSON.stringify({ content: cleanContent }));
```
### Exit code issues
**Verify script returns correct codes:**
```bash
#!/usr/bin/env bash
set -e # Exit on error
# Processing logic
if validate_input; then
echo "Success"
exit 0
else
echo "Validation failed" >&2
exit 2
fi
```
**Check for unintended errors:**
```bash
#!/usr/bin/env bash
# Don't use 'set -e' if you want to handle errors explicitly
# set -e
if ! command_that_might_fail; then
# Handle error
echo "Command failed but continuing" >&2
fi
# Always exit explicitly
exit 0
```
**Use trap for cleanup:**
```bash
#!/usr/bin/env bash
cleanup() {
# Cleanup logic
rm -f /tmp/hook-temp-*
}
trap cleanup EXIT
# Hook logic here
```
### Environment variables not available
**Check if variable is set:**
```bash
#!/usr/bin/env bash
if [ -z "$GEMINI_PROJECT_DIR" ]; then
echo "GEMINI_PROJECT_DIR not set" >&2
exit 1
fi
if [ -z "$CUSTOM_VAR" ]; then
echo "Warning: CUSTOM_VAR not set, using default" >&2
CUSTOM_VAR="default-value"
fi
```
**Debug available variables:**
```bash
#!/usr/bin/env bash
# List all environment variables
env > .gemini/hook-env.log
# Check specific variables
echo "GEMINI_PROJECT_DIR: $GEMINI_PROJECT_DIR" >> .gemini/hook-env.log
echo "GEMINI_SESSION_ID: $GEMINI_SESSION_ID" >> .gemini/hook-env.log
echo "GEMINI_API_KEY: ${GEMINI_API_KEY:+<set>}" >> .gemini/hook-env.log
```
**Use .env files:**
```bash
#!/usr/bin/env bash
# Load .env file if it exists
if [ -f "$GEMINI_PROJECT_DIR/.env" ]; then
source "$GEMINI_PROJECT_DIR/.env"
fi
```
## Privacy considerations
Hook inputs and outputs may contain sensitive information. Gemini CLI respects
the `telemetry.logPrompts` setting for hook data logging.
### What data is collected
Hook telemetry may include:
- **Hook inputs:** User prompts, tool arguments, file contents
- **Hook outputs:** Hook responses, decision reasons, added context
- **Standard streams:** stdout and stderr from hook processes
- **Execution metadata:** Hook name, event type, duration, success/failure
### Privacy settings
**Enabled (default):**
Full hook I/O is logged to telemetry. Use this when:
- Developing and debugging hooks
- Telemetry is redirected to a trusted enterprise system
- You understand and accept the privacy implications
**Disabled:**
Only metadata is logged (event name, duration, success/failure). Hook inputs and
outputs are excluded. Use this when:
- Sending telemetry to third-party systems
- Working with sensitive data
- Privacy regulations require minimizing data collection
### Configuration
**Disable PII logging in settings:**
```json
{
"telemetry": {
"logPrompts": false
}
}
```
**Disable via environment variable:**
```bash
export GEMINI_TELEMETRY_LOG_PROMPTS=false
```
### Sensitive data in hooks
If your hooks process sensitive data:
1. **Minimize logging:** Don't write sensitive data to log files
2. **Sanitize outputs:** Remove sensitive data before outputting
3. **Use secure storage:** Encrypt sensitive data at rest
4. **Limit access:** Restrict hook script permissions
**Example sanitization:**
```javascript
function sanitizeOutput(data) {
const sanitized = { ...data };
// Remove sensitive fields
delete sanitized.apiKey;
delete sanitized.password;
// Redact sensitive strings
if (sanitized.content) {
sanitized.content = sanitized.content.replace(
/api[_-]?key\s*[:=]\s*['"]?[a-zA-Z0-9_-]{20,}['"]?/gi,
'[REDACTED]',
);
}
return sanitized;
}
console.log(JSON.stringify(sanitizeOutput(hookOutput)));
```
## Learn more
- [Hooks Reference](index.md) - Complete API reference
- [Writing Hooks](writing-hooks.md) - Tutorial and examples
- [Configuration](../cli/configuration.md) - Gemini CLI settings
- [Hooks Design Document](../hooks-design.md) - Technical architecture
-560
View File
@@ -1,560 +0,0 @@
# Gemini CLI hooks
Hooks are scripts or programs that Gemini CLI executes at specific points in the
agentic loop, allowing you to intercept and customize behavior without modifying
the CLI's source code.
See [writing hooks guide](writing-hooks.md) for a tutorial on creating your
first hook and a comprehensive example.
See [best practices](best-practices.md) for guidelines on security, performance,
and debugging.
## What are hooks?
With hooks, you can:
- **Add context:** Inject relevant information before the model processes a
request
- **Validate actions:** Review and block potentially dangerous operations
- **Enforce policies:** Implement security and compliance requirements
- **Log interactions:** Track tool usage and model responses
- **Optimize behavior:** Dynamically adjust tool selection or model parameters
Hooks run synchronously as part of the agent loop—when a hook event fires,
Gemini CLI waits for all matching hooks to complete before continuing.
## Core concepts
### Hook events
Hooks are triggered by specific events in Gemini CLI's lifecycle. The following
table lists all available hook events:
| Event | When It Fires | Common Use Cases |
| --------------------- | --------------------------------------------- | ------------------------------------------ |
| `SessionStart` | When a session begins | Initialize resources, load context |
| `SessionEnd` | When a session ends | Clean up, save state |
| `BeforeAgent` | After user submits prompt, before planning | Add context, validate prompts |
| `AfterAgent` | When agent loop ends | Review output, force continuation |
| `BeforeModel` | Before sending request to LLM | Modify prompts, add instructions |
| `AfterModel` | After receiving LLM response | Filter responses, log interactions |
| `BeforeToolSelection` | Before LLM selects tools (after BeforeModel) | Filter available tools, optimize selection |
| `BeforeTool` | Before a tool executes | Validate arguments, block dangerous ops |
| `AfterTool` | After a tool executes | Process results, run tests |
| `PreCompress` | Before context compression | Save state, notify user |
| `Notification` | When a notification occurs (e.g., permission) | Auto-approve, log decisions |
### Hook types
Gemini CLI currently supports **command hooks** that run shell commands or
scripts:
```json
{
"type": "command",
"command": "$GEMINI_PROJECT_DIR/.gemini/hooks/my-hook.sh",
"timeout": 30000
}
```
**Note:** Plugin hooks (npm packages) are planned for a future release.
### Matchers
For tool-related events (`BeforeTool`, `AfterTool`), you can filter which tools
trigger the hook:
```json
{
"hooks": {
"BeforeTool": [
{
"matcher": "WriteFile|Edit",
"hooks": [
/* hooks for write operations */
]
}
]
}
}
```
**Matcher patterns:**
- **Exact match:** `"ReadFile"` matches only `ReadFile`
- **Regex:** `"Write.*|Edit"` matches `WriteFile`, `WriteBinary`, `Edit`
- **Wildcard:** `"*"` or `""` matches all tools
**Session event matchers:**
- **SessionStart:** `startup`, `resume`, `clear`
- **SessionEnd:** `exit`, `clear`, `logout`, `prompt_input_exit`
- **PreCompress:** `manual`, `auto`
- **Notification:** `ToolPermission`
## Hook input/output contract
### Command hook communication
Hooks communicate via:
- **Input:** JSON on stdin
- **Output:** Exit code + stdout/stderr
### Exit codes
- **0:** Success - stdout shown to user (or injected as context for some events)
- **2:** Blocking error - stderr shown to agent/user, operation may be blocked
- **Other:** Non-blocking warning - logged but execution continues
### Common input fields
Every hook receives these base fields:
```json
{
"session_id": "abc123",
"cwd": "/path/to/project",
"hook_event_name": "BeforeTool",
"timestamp": "2025-12-01T10:30:00Z"
// ... event-specific fields
}
```
### Event-specific fields
#### BeforeTool
**Input:**
```json
{
"tool_name": "WriteFile",
"tool_input": {
"file_path": "/path/to/file.ts",
"content": "..."
}
}
```
**Output (JSON on stdout):**
```json
{
"decision": "allow|deny|ask|block",
"reason": "Explanation shown to agent",
"systemMessage": "Message shown to user"
}
```
Or simple exit codes:
- Exit 0 = allow (stdout shown to user)
- Exit 2 = deny (stderr shown to agent)
#### AfterTool
**Input:**
```json
{
"tool_name": "ReadFile",
"tool_input": { "file_path": "..." },
"tool_response": "file contents..."
}
```
**Output:**
```json
{
"decision": "allow|deny",
"hookSpecificOutput": {
"hookEventName": "AfterTool",
"additionalContext": "Extra context for agent"
}
}
```
#### BeforeAgent
**Input:**
```json
{
"prompt": "Fix the authentication bug"
}
```
**Output:**
```json
{
"decision": "allow|deny",
"hookSpecificOutput": {
"hookEventName": "BeforeAgent",
"additionalContext": "Recent project decisions: ..."
}
}
```
#### BeforeModel
**Input:**
```json
{
"llm_request": {
"model": "gemini-2.0-flash-exp",
"messages": [{ "role": "user", "content": "Hello" }],
"config": { "temperature": 0.7 },
"toolConfig": {
"functionCallingConfig": {
"mode": "AUTO",
"allowedFunctionNames": ["ReadFile", "WriteFile"]
}
}
}
}
```
**Output:**
```json
{
"decision": "allow",
"hookSpecificOutput": {
"hookEventName": "BeforeModel",
"llm_request": {
"messages": [
{ "role": "system", "content": "Additional instructions..." },
{ "role": "user", "content": "Hello" }
]
}
}
}
```
#### AfterModel
**Input:**
```json
{
"llm_request": {
"model": "gemini-2.0-flash-exp",
"messages": [
/* ... */
],
"config": {
/* ... */
},
"toolConfig": {
/* ... */
}
},
"llm_response": {
"text": "string",
"candidates": [
{
"content": {
"role": "model",
"parts": ["array of content parts"]
},
"finishReason": "STOP"
}
]
}
}
```
**Output:**
```json
{
"hookSpecificOutput": {
"hookEventName": "AfterModel",
"llm_response": {
"candidate": {
/* modified response */
}
}
}
}
```
#### BeforeToolSelection
**Input:**
```json
{
"llm_request": {
"model": "gemini-2.0-flash-exp",
"messages": [
/* ... */
],
"toolConfig": {
"functionCallingConfig": {
"mode": "AUTO",
"allowedFunctionNames": [
/* 100+ tools */
]
}
}
}
}
```
**Output:**
```json
{
"hookSpecificOutput": {
"hookEventName": "BeforeToolSelection",
"toolConfig": {
"functionCallingConfig": {
"mode": "ANY",
"allowedFunctionNames": ["ReadFile", "WriteFile", "Edit"]
}
}
}
}
```
Or simple output (comma-separated tool names sets mode to ANY):
```bash
echo "ReadFile,WriteFile,Edit"
```
#### SessionStart
**Input:**
```json
{
"source": "startup|resume|clear"
}
```
**Output:**
```json
{
"hookSpecificOutput": {
"hookEventName": "SessionStart",
"additionalContext": "Loaded 5 project memories"
}
}
```
#### SessionEnd
**Input:**
```json
{
"reason": "exit|clear|logout|prompt_input_exit|other"
}
```
No structured output expected (but stdout/stderr logged).
#### PreCompress
**Input:**
```json
{
"trigger": "manual|auto"
}
```
**Output:**
```json
{
"systemMessage": "Compression starting..."
}
```
#### Notification
**Input:**
```json
{
"notification_type": "ToolPermission",
"message": "string",
"details": {
/* notification details */
}
}
```
**Output:**
```json
{
"systemMessage": "Notification logged"
}
```
## Configuration
Hook definitions are configured in `settings.json` files using the `hooks`
object. Configuration can be specified at multiple levels with defined
precedence rules.
### Configuration layers
Hook configurations are applied in the following order of precedence (higher
numbers override lower numbers):
1. **System defaults:** Built-in default settings (lowest precedence)
2. **User settings:** `~/.gemini/settings.json`
3. **Project settings:** `.gemini/settings.json` in your project directory
4. **System settings:** `/etc/gemini-cli/settings.json` (highest precedence)
Within each level, hooks run in the order they are declared in the
configuration.
### Configuration schema
```json
{
"hooks": {
"EventName": [
{
"matcher": "pattern",
"hooks": [
{
"name": "hook-identifier",
"type": "command",
"command": "./path/to/script.sh",
"description": "What this hook does",
"timeout": 30000
}
]
}
]
}
}
```
**Configuration properties:**
- **`name`** (string, required): Unique identifier for the hook used in
`/hooks enable/disable` commands
- **`type`** (string, required): Hook type - currently only `"command"` is
supported
- **`command`** (string, required): Path to the script or command to execute
- **`description`** (string, optional): Human-readable description shown in
`/hooks panel`
- **`timeout`** (number, optional): Timeout in milliseconds (default: 60000)
- **`matcher`** (string, optional): Pattern to filter when hook runs (event
matchers only)
### Environment variables
Hooks have access to:
- `GEMINI_PROJECT_DIR`: Project root directory
- `GEMINI_SESSION_ID`: Current session ID
- `GEMINI_API_KEY`: Gemini API key (if configured)
- All other environment variables from the parent process
## Managing hooks
### View registered hooks
Use the `/hooks panel` command to view all registered hooks:
```bash
/hooks panel
```
This command displays:
- All active hooks organized by event
- Hook source (user, project, system)
- Hook type (command or plugin)
- Execution status and recent output
### Enable and disable hooks
You can temporarily enable or disable individual hooks using commands:
```bash
/hooks enable hook-name
/hooks disable hook-name
```
These commands allow you to control hook execution without editing configuration
files. The hook name should match the `name` field in your hook configuration.
### Disabled hooks configuration
To permanently disable hooks, add them to the `hooks.disabled` array in your
`settings.json`:
```json
{
"hooks": {
"disabled": ["secret-scanner", "auto-test"]
}
}
```
**Note:** The `hooks.disabled` array uses a UNION merge strategy. Disabled hooks
from all configuration levels (user, project, system) are combined and
deduplicated, meaning a hook disabled at any level remains disabled.
## Migration from Claude Code
If you have hooks configured for Claude Code, you can migrate them:
```bash
gemini hooks migrate --from-claude
```
This command:
- Reads `.claude/settings.json`
- Converts event names (`PreToolUse``BeforeTool`, etc.)
- Translates tool names (`Bash``RunShellCommand`, `Edit``Edit`)
- Updates matcher patterns
- Writes to `.gemini/settings.json`
### Event name mapping
| Claude Code | Gemini CLI |
| ------------------ | -------------- |
| `PreToolUse` | `BeforeTool` |
| `PostToolUse` | `AfterTool` |
| `UserPromptSubmit` | `BeforeAgent` |
| `Stop` | `AfterAgent` |
| `Notification` | `Notification` |
| `SessionStart` | `SessionStart` |
| `SessionEnd` | `SessionEnd` |
| `PreCompact` | `PreCompress` |
### Tool name mapping
| Claude Code | Gemini CLI |
| ----------- | ----------------- |
| `Bash` | `RunShellCommand` |
| `Edit` | `Edit` |
| `Read` | `ReadFile` |
| `Write` | `WriteFile` |
## Learn more
- [Writing Hooks](writing-hooks.md) - Tutorial and comprehensive example
- [Best Practices](best-practices.md) - Security, performance, and debugging
- [Custom Commands](../cli/custom-commands.md) - Create reusable prompt
shortcuts
- [Configuration](../cli/configuration.md) - Gemini CLI configuration options
- [Hooks Design Document](../hooks-design.md) - Technical architecture details
File diff suppressed because it is too large Load Diff
-9
View File
@@ -102,15 +102,6 @@ This documentation is organized into the following sections:
- **[Extension releasing](./extensions/extension-releasing.md):** How to release
Gemini CLI extensions.
### Hooks
- **[Hooks](./hooks/index.md):** Intercept and customize Gemini CLI behavior at
key lifecycle points.
- **[Writing Hooks](./hooks/writing-hooks.md):** Learn how to create your first
hook with a comprehensive example.
- **[Best Practices](./hooks/best-practices.md):** Security, performance, and
debugging guidelines for hooks.
### IDE integration
- **[Introduction to IDE integration](./ide-integration/index.md):** Connect the
+3 -3
View File
@@ -199,9 +199,9 @@ file, or case.
## Continuous integration
To ensure the integration tests are always run, a GitHub Actions workflow is
defined in `.github/workflows/chained_e2e.yml`. This workflow automatically runs
the integrations tests for pull requests against the `main` branch, or when a
pull request is added to a merge queue.
defined in `.github/workflows/e2e.yml`. This workflow automatically runs the
integrations tests for pull requests against the `main` branch, or when a pull
request is added to a merge queue.
The workflow runs the tests in different sandboxing environments to ensure
Gemini CLI is tested across each:
+1 -1
View File
@@ -33,7 +33,7 @@ nightly) or the release branch (for preview/stable).
### 2. End-to-end (E2E) tests
All workflows in `.github/workflows/chained_e2e.yml` must pass.
All workflows in `.github/workflows/e2e.yml` must pass.
- **Platforms:** **Linux, macOS and Windows**.
- **Sandboxing:** Tests must pass with both `sandbox:none` and `sandbox:docker`
+5 -39
View File
@@ -24,7 +24,7 @@
"slug": "docs/get-started"
},
{
"label": "Gemini 3 on Gemini CLI",
"label": "Gemini 3 Pro on Gemini CLI",
"slug": "docs/get-started/gemini-3"
},
{
@@ -193,23 +193,6 @@
}
]
},
{
"label": "Hooks",
"items": [
{
"label": "Introduction",
"slug": "docs/hooks"
},
{
"label": "Writing hooks",
"slug": "docs/hooks/writing-hooks"
},
{
"label": "Best practices",
"slug": "docs/hooks/best-practices"
}
]
},
{
"label": "IDE integration",
"items": [
@@ -234,6 +217,10 @@
"label": "Releases",
"slug": "docs/releases"
},
{
"label": "Changelog",
"slug": "docs/changelogs"
},
{
"label": "Integration tests",
"slug": "docs/integration-tests"
@@ -244,27 +231,6 @@
}
]
},
{
"label": "Releases",
"items": [
{
"label": "Release notes",
"slug": "docs/changelogs/"
},
{
"label": "Latest release",
"slug": "docs/changelogs/latest"
},
{
"label": "Preview release",
"slug": "docs/changelogs/preview"
},
{
"label": "Changelog",
"slug": "docs/changelogs/releases"
}
]
},
{
"label": "Support",
"items": [
+6 -7
View File
@@ -36,9 +36,8 @@ glob patterns.
## 2. `read_file` (ReadFile)
`read_file` reads and returns the content of a specified file. This tool handles
text, images (PNG, JPG, GIF, WEBP, SVG, BMP), audio files (MP3, WAV, AIFF, AAC,
OGG, FLAC), and PDF files. For text files, it can read specific line ranges.
Other binary file types are generally skipped.
text, images (PNG, JPG, GIF, WEBP, SVG, BMP), and PDF files. For text files, it
can read specific line ranges. Other binary file types are generally skipped.
- **Tool name:** `read_file`
- **Display name:** ReadFile
@@ -54,16 +53,16 @@ Other binary file types are generally skipped.
- For text files: Returns the content. If `offset` and `limit` are used,
returns only that slice of lines. Indicates if content was truncated due to
line limits or line length limits.
- For image, audio, and PDF files: Returns the file content as a
base64-encoded data structure suitable for model consumption.
- For image and PDF files: Returns the file content as a base64-encoded data
structure suitable for model consumption.
- For other binary files: Attempts to identify and skip them, returning a
message indicating it's a generic binary file.
- **Output:** (`llmContent`):
- For text files: The file content, potentially prefixed with a truncation
message (e.g.,
`[File content truncated: showing lines 1-100 of 500 total lines...]\nActual file content...`).
- For image/audio/PDF files: An object containing `inlineData` with `mimeType`
and base64 `data` (e.g.,
- For image/PDF files: An object containing `inlineData` with `mimeType` and
base64 `data` (e.g.,
`{ inlineData: { mimeType: 'image/png', data: 'base64encodedstring' } }`).
- For other binary files: A message like
`Cannot display content of binary file: /path/to/data.bin`.
+2 -29
View File
@@ -16,8 +16,8 @@ An MCP server enables the Gemini CLI to:
through standardized schema definitions.
- **Execute tools:** Call specific tools with defined arguments and receive
structured responses.
- **Access resources:** Read data from specific resources that the server
exposes (files, API payloads, reports, etc.).
- **Access resources:** Read data from specific resources (though the Gemini CLI
primarily focuses on tool execution).
With an MCP server, you can extend the Gemini CLI's capabilities to perform
actions beyond its built-in features, such as interacting with databases, APIs,
@@ -40,7 +40,6 @@ The discovery process is orchestrated by `discoverMcpTools()`, which:
4. **Sanitizes and validates** tool schemas for compatibility with the Gemini
API
5. **Registers tools** in the global tool registry with conflict resolution
6. **Fetches and registers resources** if the server exposes any
### Execution layer (`mcp-tool.ts`)
@@ -60,32 +59,6 @@ The Gemini CLI supports three MCP transport types:
- **SSE Transport:** Connects to Server-Sent Events endpoints
- **Streamable HTTP Transport:** Uses HTTP streaming for communication
## Working with MCP resources
Some MCP servers expose contextual “resources” in addition to the tools and
prompts. Gemini CLI discovers these automatically and gives you the possibility
to reference them in the chat.
### Discovery and listing
- When discovery runs, the CLI fetches each servers `resources/list` results.
- The `/mcp` command displays a Resources section alongside Tools and Prompts
for every connected server.
This returns a concise, plain-text list of URIs plus metadata.
### Referencing resources in a conversation
You can use the same `@` syntax already known for referencing local files:
```
@server://resource/path
```
Resource URIs appear in the completion menu together with filesystem paths. When
you submit the message, the CLI calls `resources/read` and injects the content
in the conversation.
## How to set up your MCP server
The Gemini CLI uses the `mcpServers` configuration in your `settings.json` file
+1 -1
View File
@@ -26,7 +26,7 @@ plan.
- **Progress tracking:** The agent updates this list as it works, marking tasks
as `completed` when done.
- **Single focus:** Only one task will be marked `in_progress` at a time,
- **Single socus:** Only one task will be marked `in_progress` at a time,
indicating exactly what the agent is currently working on.
- **Dynamic updates:** The plan may evolve as the agent discovers new
information, leading to new tasks being added or unnecessary ones being
+2 -1
View File
@@ -124,7 +124,8 @@ This is especially useful for scripting and automation.
## Debugging tips
- **CLI debugging:**
- Use the `--debug` flag for more detailed output.
- Use the `--verbose` flag (if available) with CLI commands for more detailed
output.
- Check the CLI logs, often found in a user-specific configuration or cache
directory.
-1
View File
@@ -165,7 +165,6 @@ export default tseslint.config(
'prefer-const': ['error', { destructuring: 'all' }],
radix: 'error',
'default-case': 'error',
'@typescript-eslint/no-floating-promises': ['error'],
},
},
{
+6
View File
@@ -0,0 +1,6 @@
prompt = """
Please summarize the findings for the pattern `{{args}}`.
Search Results:
!{grep -r {{args}} .}
"""
+4
View File
@@ -0,0 +1,4 @@
{
"name": "custom-commands",
"version": "1.0.0"
}
+2 -7
View File
@@ -19,14 +19,9 @@ const itIf = (condition: boolean) => (condition ? it : it.skip);
describe('extension reloading', () => {
const sandboxEnv = env['GEMINI_SANDBOX'];
// Fails in linux non-sandbox e2e tests
// TODO(#14527): Re-enable this once fixed
// Fails in sandbox mode, can't check for local extension updates.
itIf(
(!sandboxEnv || sandboxEnv === 'false') &&
platform() !== 'win32' &&
platform() !== 'linux',
)(
itIf((!sandboxEnv || sandboxEnv === 'false') && platform() !== 'win32')(
'installs a local extension, updates it, checks it was reloaded properly',
async () => {
const serverA = new TestMcpServer();
+4 -13
View File
@@ -12,9 +12,7 @@ import { TestRig, printDebugInfo, validateModelOutput } from './test-helper.js';
describe('file-system', () => {
it('should be able to read a file', async () => {
const rig = new TestRig();
await rig.setup('should be able to read a file', {
settings: { tools: { core: ['read_file'] } },
});
await rig.setup('should be able to read a file');
rig.createFile('test.txt', 'hello world');
const result = await rig.run(
@@ -42,9 +40,7 @@ describe('file-system', () => {
it('should be able to write a file', async () => {
const rig = new TestRig();
await rig.setup('should be able to write a file', {
settings: { tools: { core: ['write_file', 'replace', 'read_file'] } },
});
await rig.setup('should be able to write a file');
rig.createFile('test.txt', '');
const result = await rig.run(`edit test.txt to have a hello world message`);
@@ -99,9 +95,7 @@ describe('file-system', () => {
it('should correctly handle file paths with spaces', async () => {
const rig = new TestRig();
await rig.setup('should correctly handle file paths with spaces', {
settings: { tools: { core: ['write_file', 'read_file'] } },
});
await rig.setup('should correctly handle file paths with spaces');
const fileName = 'my test file.txt';
const result = await rig.run(
@@ -123,9 +117,7 @@ describe('file-system', () => {
it('should perform a read-then-write sequence', async () => {
const rig = new TestRig();
await rig.setup('should perform a read-then-write sequence', {
settings: { tools: { core: ['read_file', 'replace', 'write_file'] } },
});
await rig.setup('should perform a read-then-write sequence');
const fileName = 'version.txt';
rig.createFile(fileName, '1.0.0');
@@ -214,7 +206,6 @@ describe('file-system', () => {
const rig = new TestRig();
await rig.setup(
'should fail safely when trying to edit a non-existent file',
{ settings: { tools: { core: ['read_file', 'replace'] } } },
);
const fileName = 'non_existent.txt';
+1 -3
View File
@@ -11,9 +11,7 @@ import { TestRig, printDebugInfo, validateModelOutput } from './test-helper.js';
describe(WEB_SEARCH_TOOL_NAME, () => {
it('should be able to search the web', async () => {
const rig = new TestRig();
await rig.setup('should be able to search the web', {
settings: { tools: { core: [WEB_SEARCH_TOOL_NAME] } },
});
await rig.setup('should be able to search the web');
let result;
try {
@@ -1 +0,0 @@
{"method":"generateContentStream","response":[{"candidates":[{"content":{"parts":[{"text":"**Devising a Greeting Phrase**\n\nI've been occupied by the constraint of constructing a five-word salutation. My goal is to make it natural and concise. I'm exploring various combinations to meet the specified word count precisely.\n\n\n","thought":true}],"role":"model"},"index":0}],"usageMetadata":{"promptTokenCount":12587,"totalTokenCount":12612,"promptTokensDetails":[{"modality":"TEXT","tokenCount":12587}],"thoughtsTokenCount":25}},{"candidates":[{"content":{"parts":[{"text":"Hello! How can I help you?","thoughtSignature":"CiQBcsjafHso9FUsdYOCTv1xOLlW4MnjbeYnUUBocz0KNgHSzOcKZAFyyNp8XuI6j2afRczgPL8v1dxfVwAJ+5XDKhWKIYf1/8TKGVHh7xXnPfdYBdQ07Ohe7OZXr92xL/IC7B1U2SHDuAOozC0CCW7aiDysu6Hbo6jzYfW5epKht4QjdxYgcKHySrkKMQFyyNp8jXWlHmox53O/CJPXXz2FAmw+ubHKBpYgRezBpA+byyEY2RbVYlZlEMSNkhs="}],"role":"model"},"finishReason":"STOP","index":0}],"usageMetadata":{"promptTokenCount":12587,"candidatesTokenCount":7,"totalTokenCount":12619,"promptTokensDetails":[{"modality":"TEXT","tokenCount":12587}],"thoughtsTokenCount":25}}]}
@@ -1,4 +0,0 @@
{"method":"generateContentStream","response":[{"candidates":[{"content":{"parts":[{"text":"**Creating the First Test File**\n\nI'll use the `write_file` tool to create `first-run.txt` with the content \"test1\".\n\n\n","thought":true}],"role":"model"},"index":0}],"usageMetadata":{"promptTokenCount":12779,"totalTokenCount":12824,"cachedContentTokenCount":12204,"promptTokensDetails":[{"modality":"TEXT","tokenCount":12779}],"cacheTokensDetails":[{"modality":"TEXT","tokenCount":12204}],"thoughtsTokenCount":45}},{"candidates":[{"content":{"parts":[{"functionCall":{"name":"write_file","args":{"content":"test1","file_path":"first-run.txt"}},"thoughtSignature":"CiQBcsjafG+JDSqtKOK+ZvSjZQZmS91c1Gz0YyTiirI2u5+rhEIKZAFyyNp8DNXb+xHILTC+FVlEifqEHdrmfFNBLKojci1UIBhcZpQ4UXCMkxUXYKO34IjTlyLgSsjVbbXWEFXatb/z/RtTDcf51uc3YOEwlDScGempkJxfFgcPfIiD7bhuHBqdQfUKfAFyyNp8wZ71h+QjdfVw12PwDXWgGZ0Xed1GuyJXuqAwpWnwxDIvsDaPwDFYyLR1XDiIZZk4AvFCGt6HGMSLRuPh4K3i9CVnDc5hcjyvMIde0idAFMrgs2Mq5SARfCPrWkqyq2f0Q0WonUl2n7yr/sDQ78rx2E6qXyUJ8XMKfAFyyNp8DdTYLttyI0jknqAeZDxdFmHtpJUI8UKP5YHzpQc8Qn80OJcwhZSRH4HRKCqoC7Sukq/A5vJ5T468WqgjOoLlPLq02bYRTf/q6LC1ogEhdLHrcFv2jDeCdXJJ8NHv3O4DZAUAk1W5Gd0428zMFOxH3AgkWwEGuow="}],"role":"model"},"finishReason":"STOP","index":0}],"usageMetadata":{"promptTokenCount":12779,"candidatesTokenCount":24,"totalTokenCount":12848,"cachedContentTokenCount":12204,"promptTokensDetails":[{"modality":"TEXT","tokenCount":12779}],"cacheTokensDetails":[{"modality":"TEXT","tokenCount":12204}],"thoughtsTokenCount":45}}]}
{"method":"generateContentStream","response":[{"candidates":[{"content":{"parts":[{"text":"File created successfully. Active hook executed"}],"role":"model"},"finishReason":"STOP","index":0}],"usageMetadata":{"promptTokenCount":12951,"candidatesTokenCount":7,"totalTokenCount":12958,"cachedContentTokenCount":12202,"promptTokensDetails":[{"modality":"TEXT","tokenCount":12951}],"cacheTokensDetails":[{"modality":"TEXT","tokenCount":12202}]}}]}
{"method":"generateContentStream","response":[{"candidates":[{"content":{"parts":[{"text":"**Creating the Second Test File**\n\nI'll use the `write_file` tool to create `second-run.txt` with the content \"test2\".\n\n\n","thought":true}],"role":"model"},"index":0}],"usageMetadata":{"promptTokenCount":12779,"totalTokenCount":12826,"cachedContentTokenCount":12204,"promptTokensDetails":[{"modality":"TEXT","tokenCount":12779}],"cacheTokensDetails":[{"modality":"TEXT","tokenCount":12204}],"thoughtsTokenCount":47}},{"candidates":[{"content":{"parts":[{"functionCall":{"name":"write_file","args":{"content":"test2","file_path":"second-run.txt"}},"thoughtSignature":"CiQBcsjafG+JDSqtKOK+ZvSjZQZmS91c1Gz0YyTiirI2u5+rhEIKZAFyyNp8DNXb+xHILTC+FVlEifqEHdrmfFNBLKojci1UIBhcZpQ4UXCMkxUXYKO34IjTlyLgSsjVbbXWEFXatb/z/RtTDcf51uc3YOEwlDScGempkJxfFgcPfIiD7bhuHBqdQfUKfAFyyNp8wZ71h+QjdfVw12PwDXWgGZ0Xed1GuyJXuqAwpWnwxDIvsDaPwDFYyLR1XDiIZZk4AvFCGt6HGMSLRuPh4K3i9CVnDc5hcjyvMIde0idAFMrgs2Mq5SARfCPrWkqyq2f0Q0WonUl2n7yr/sDQ78rx2E6qXyUJ8XMKfAFyyNp8DdTYLttyI0jknqAeZDxdFmHtpJUI8UKP5YHzpQc8Qn80OJcwhZSRH4HRKCqoC7Sukq/A5vJ5T468WqgjOoLlPLq02bYRTf/q6LC1ogEhdLHrcFv2jDeCdXJJ8NHv3O4DZAUAk1W5Gd0428zMFOxH3AgkWwEGuow="}],"role":"model"},"finishReason":"STOP","index":0}],"usageMetadata":{"promptTokenCount":12779,"candidatesTokenCount":24,"totalTokenCount":12850,"cachedContentTokenCount":12204,"promptTokensDetails":[{"modality":"TEXT","tokenCount":12779}],"cacheTokensDetails":[{"modality":"TEXT","tokenCount":12204}],"thoughtsTokenCount":47}}]}
{"method":"generateContentStream","response":[{"candidates":[{"content":{"parts":[{"text":"File created successfully. Active hook executed"}],"role":"model"},"finishReason":"STOP","index":0}],"usageMetadata":{"promptTokenCount":12951,"candidatesTokenCount":7,"totalTokenCount":12958,"cachedContentTokenCount":12202,"promptTokensDetails":[{"modality":"TEXT","tokenCount":12951}],"cacheTokensDetails":[{"modality":"TEXT","tokenCount":12202}]}}]}
@@ -1,2 +0,0 @@
{"method":"generateContentStream","response":[{"candidates":[{"content":{"parts":[{"text":"**Creating the Test File**\n\nI'll use the `write_file` tool to create `disabled-test.txt` with the content \"test\".\n\n\n","thought":true}],"role":"model"},"index":0}],"usageMetadata":{"promptTokenCount":12779,"totalTokenCount":12820,"cachedContentTokenCount":12204,"promptTokensDetails":[{"modality":"TEXT","tokenCount":12779}],"cacheTokensDetails":[{"modality":"TEXT","tokenCount":12204}],"thoughtsTokenCount":41}},{"candidates":[{"content":{"parts":[{"functionCall":{"name":"write_file","args":{"content":"test","file_path":"disabled-test.txt"}},"thoughtSignature":"CiQBcsjafG+JDSqtKOK+ZvSjZQZmS91c1Gz0YyTiirI2u5+rhEIKZAFyyNp8DNXb+xHILTC+FVlEifqEHdrmfFNBLKojci1UIBhcZpQ4UXCMkxUXYKO34IjTlyLgSsjVbbXWEFXatb/z/RtTDcf51uc3YOEwlDScGempkJxfFgcPfIiD7bhuHBqdQfUKfAFyyNp8wZ71h+QjdfVw12PwDXWgGZ0Xed1GuyJXuqAwpWnwxDIvsDaPwDFYyLR1XDiIZZk4AvFCGt6HGMSLRuPh4K3i9CVnDc5hcjyvMIde0idAFMrgs2Mq5SARfCPrWkqyq2f0Q0WonUl2n7yr/sDQ78rx2E6qXyUJ8XMKfAFyyNp8DdTYLttyI0jknqAeZDxdFmHtpJUI8UKP5YHzpQc8Qn80OJcwhZSRH4HRKCqoC7Sukq/A5vJ5T468WqgjOoLlPLq02bYRTf/q6LC1ogEhdLHrcFv2jDeCdXJJ8NHv3O4DZAUAk1W5Gd0428zMFOxH3AgkWwEGuow="}],"role":"model"},"finishReason":"STOP","index":0}],"usageMetadata":{"promptTokenCount":12779,"candidatesTokenCount":24,"totalTokenCount":12844,"cachedContentTokenCount":12204,"promptTokensDetails":[{"modality":"TEXT","tokenCount":12779}],"cacheTokensDetails":[{"modality":"TEXT","tokenCount":12204}],"thoughtsTokenCount":41}}]}
{"method":"generateContentStream","response":[{"candidates":[{"content":{"parts":[{"text":"File created successfully. Enabled hook executed."}],"role":"model"},"finishReason":"STOP","index":0}],"usageMetadata":{"promptTokenCount":12951,"candidatesTokenCount":8,"totalTokenCount":12959,"cachedContentTokenCount":12202,"promptTokensDetails":[{"modality":"TEXT","tokenCount":12951}],"cacheTokensDetails":[{"modality":"TEXT","tokenCount":12202}]}}]}
@@ -1,4 +0,0 @@
{"method":"generateContentStream","response":[{"candidates":[{"content":{"parts":[{"text":"**Formulating a Plan**\n\nOkay, I've outlined the initial steps: I'll use the `write_file` tool to make a file named `multi-event-test.txt` containing the text \"testing multiple events\". After that, I'll need to remember to reply with the phrase as requested. It seems straightforward so far.\n\n\n","thought":true}],"role":"model"},"index":0}],"usageMetadata":{"promptTokenCount":12622,"totalTokenCount":12692,"promptTokensDetails":[{"modality":"TEXT","tokenCount":12622}],"thoughtsTokenCount":70}},{"candidates":[{"content":{"parts":[{"text":"**Confirming the Procedure**\n\nI've solidified the steps. First, I'll create `multi-event-test.txt` using the `write_file` tool with the required content. Following that, my response will be \"BeforeAgent: User request processed.\" This ensures I fulfill both parts of the request.\n\n\n","thought":true}],"role":"model"},"index":0}],"usageMetadata":{"promptTokenCount":12622,"totalTokenCount":12713,"promptTokensDetails":[{"modality":"TEXT","tokenCount":12622}],"thoughtsTokenCount":91}},{"candidates":[{"content":{"parts":[{"functionCall":{"name":"write_file","args":{"file_path":"multi-event-test.txt","content":"testing multiple events"}},"thoughtSignature":"CiQBcsjafIqcYtNLIeBwJi3k5k8jho3QiWM+51Kw5vTQ7/V4qVQKZgFyyNp8mIIB0+Mvwhvo2fACDpTWpRYeOFPGrjZrc+N05S0WGEHzE4Dv9peHKdvZkjGNW+HyYHXoRpd5c/ScdhPxQoVZmZ9K7sRjVxv/nWVDoKnHlSsn94nJ8acjLnj1oqt9cHni0ApyAXLI2nwj5WuLHr+UFIxnqRKCUJboLo6bQMkqR1TsqXbjsgHp3zNQYT+xzbse4PKPLJV48FN6cL9MrrZ81E7k7AVo1cKyrC7ky7tdRH6gYHewIqgQWBIUgMKhLkePH/fYZ6fS7SMrf4Q6DFGHh6pIAAdRCooBAXLI2nxpudEZr+5jZAaAcCMIdij5oZq3s0xsQv/7iWVh8IossRuR0J4eMMSN8fV6+fjbSQ6YtJQfrxsm3a6gVIkJNno2b2PRZestS/0Z7DvPDGE6r1sGchvbcz8EW7Z/pvJvPBRFWlMTJ1eqY9vuyuNYMKeWlyt+5V9y2GUbcLWvcNDZSC43vQEKCo0BAXLI2nxP4INgBaSHInyFrG1/SEP0SUimKvP69FkcIBxx60x3iKqdtb2flLIhoOr/QuesASlflRfzNo3J5LOudrjZzNlRfVRqOZIyOVxZlviXtO7+w/oPCV61Sby6xPTGtFsWlt6GxEGF7iYLfvi4KWN9q/W9tlqEqUrpl/WMwS/4pYBi1xPcvXZNlJ6g"}],"role":"model"},"finishReason":"STOP","index":0}],"usageMetadata":{"promptTokenCount":12622,"candidatesTokenCount":28,"totalTokenCount":12741,"promptTokensDetails":[{"modality":"TEXT","tokenCount":12622}],"thoughtsTokenCount":91}}]}
{"method":"generateContentStream","response":[{"candidates":[{"content":{"parts":[{"text":"\n"}],"role":"model"},"index":0}],"usageMetadata":{"promptTokenCount":12836,"totalTokenCount":12836,"cachedContentTokenCount":12204,"promptTokensDetails":[{"modality":"TEXT","tokenCount":12836}],"cacheTokensDetails":[{"modality":"TEXT","tokenCount":12204}]}},{"candidates":[{"content":{"role":"model"},"finishReason":"STOP","index":0}],"usageMetadata":{"promptTokenCount":12836,"totalTokenCount":12836,"cachedContentTokenCount":12204,"promptTokensDetails":[{"modality":"TEXT","tokenCount":12836}],"cacheTokensDetails":[{"modality":"TEXT","tokenCount":12204}]}}]}
{"method":"generateContentStream","response":[{"candidates":[{"content":{"parts":[{"text":"\n"}],"role":"model"},"index":0}],"usageMetadata":{"promptTokenCount":12836,"totalTokenCount":12836,"cachedContentTokenCount":12204,"promptTokensDetails":[{"modality":"TEXT","tokenCount":12836}],"cacheTokensDetails":[{"modality":"TEXT","tokenCount":12204}]}},{"candidates":[{"content":{"role":"model"},"finishReason":"STOP","index":0}],"usageMetadata":{"promptTokenCount":12836,"totalTokenCount":12836,"cachedContentTokenCount":12204,"promptTokensDetails":[{"modality":"TEXT","tokenCount":12836}],"cacheTokensDetails":[{"modality":"TEXT","tokenCount":12204}]}}]}
{"method":"generateContentStream","response":[{"candidates":[{"content":{"parts":[{"text":"**Echoing User Commands**\n\nI'm now tasked with echoing a specific phrase following a particular signal, but it's becoming complex. The user wants me to repeat \"BeforeAgent: User request processed\" when prompted. It appears I need to retain context from the previous turn, the user's initial request to create a file, to correctly respond now.\n\n\n","thought":true}],"role":"model"},"index":0}],"usageMetadata":{"promptTokenCount":12759,"totalTokenCount":12827,"cachedContentTokenCount":12199,"promptTokensDetails":[{"modality":"TEXT","tokenCount":12759}],"cacheTokensDetails":[{"modality":"TEXT","tokenCount":12199}],"thoughtsTokenCount":68}},{"candidates":[{"content":{"parts":[{"text":"**Responding Precisely to Prompt**\n\nI've determined I need to repeat the phrase \"BeforeAgent: User request processed,\" even though the overall context and turn history are complex. The user has given several prompts, but has now provided a more direct command, which I believe is to follow up on the previous request. I am taking care to match the specific instructions the user provided.\n\n\n","thought":true}],"role":"model"},"index":0}],"usageMetadata":{"promptTokenCount":12759,"totalTokenCount":12982,"cachedContentTokenCount":12199,"promptTokensDetails":[{"modality":"TEXT","tokenCount":12759}],"cacheTokensDetails":[{"modality":"TEXT","tokenCount":12199}],"thoughtsTokenCount":223}},{"candidates":[{"content":{"parts":[{"text":"BeforeAgent: User request processed","thoughtSignature":"CiQBcsjafAntJrb1JBgpnZaCNeYhOJXtbH6dKTeM1llglCdoOvUKYwFyyNp8PUj5sihYyITQJhdz4MqEeftyuUc4G+iTprve11gPN04eK9Y1Wi/wyln4RjRgroIrV5kByKzdGhECoyCeInpiILGhY0peIM7dZOKFdIOL7xAR9pmn4wMreqyH7l5WSAqJAQFyyNp8Cugemkt4YZWkIwEJYmUukLFx4d5EwP/9k/e4OH/svpM+uyuN3n1KVN3bFgRV5yuF0HnDLl+P7WVSSxMmWvXO2f7A1HALg+gCvZw9IV7Btgg1qp81dDoNcVkzSbTBtT4UrlJ5R6sclvHZOLUtKGwBEQ6zRonBugAgj9RV4BT1AJNOgdSsCokBAXLI2nyDGU1Iq30QVbqhgEwFa5sB6uPC+35BV8ZKGwK+YglO9rqXMrkXM+GcQi2hVIsOFXBYGTS6E2/mQfFbIKDytrb1JgP3q5xVd/bE23M2Nnf+q5TLbRpLAPmyfg0AGwhN0L7d5W6b/3ydqEPeA1/Vw/cnBzz5ND1LOTOX6BFqEs33/WHj7HIKpAEBcsjafEsn8//cZMWUQcSAucBQauojv/f7h11nbeMrZK84nEotR30BgMIWYiiWM6sGDy/4MzHwr+z2YdAz4PSgRvEf7DPxHps2nvZfAdtskgtdPl2JD81WpokSnJvCqU+cOuz+Nh3+fIiZ6vEsVpi/5cwEiGT0g3Z3I2ubyzv58oH8YnVQlKT3MsKRGb5//aXZJY57jNrexgDPzYAQsBgSuGBmqwqaAQFyyNp8sSIYw3It6GpZqC+oxJCC26pt4RxhG8rDZ3zuoADYlOpoUdSzbNuDB+iVHeen5OoCEAaH0GrFV4iZxgu40wu4ZD/VMfHi/Vm7vku23EUV/94U8mT+VEwPfd2gqv+3xPZ9MEHjOOox1Xq1984w2cA6u0Qn7wWHXeOGFVGSOHtdJtQ7ToNT8VEecblAVq8lm42sSccXQEEKmAEBcsjafONCvBhW2s8Bset20YFdbeSHelnILFDxXlCoYla5nP5UjGk4vpXu2+7RCFtKXfoyYEVEkmiGBRsmwJ82Q1nMkGkXMhuTdNhu4aCwI5m+STGxx26vkp9bcqGwMDHBotZL63PSrJacRoW8zfpDXD1PABLeTIfh5jgipQdgltyjlbc+3qfIfjBYNRSkE8ByErSz5rT7SwqSAQFyyNp8W2kut1PSJISxM7YJtbRdFqPBTikGDM6F/3l6ba6LpeRBfHdtueLChqFpwLH41VdIPQ7lRZflOq3KaZz+TQ11eDnYQbiaIdGOPgHJ/HH/0iQv2hnoOY5vg3gubFWFuZh9Bfun2VCYUI39tIxGC46TZWfgCdiP/O9CFOlpDfidPiz5ZS/4LhG9FA4Q85OuCpEBAXLI2nzpoEUA6jCZopeNTRA2uZ1r0DMm5cWVVXtFO4CoRS+19BbADNBRyNrR5qcf7bUflJBvMRVxx3mtmgK9aE5VmKYxK2Dqg15l9RUxjtqspC3VVmszVd6lOkf1BBQ/VtWDulqRetKE2u62Is9NNGuK9HsLzIBLRRc8QoML41WffuXQ+uxwyXpjx2USC44MGAqIAQFyyNp8gN3lOyHyk674W3Pyv+Egw1ZDUQK4xpvAfgnK+y53gclMGJ2IjOSvg4j0f1WO1OGqY2TBUFS7w21PXasvCkfxpqeStEb+U7Vm0r63LzXdGdug5/b1Ap6Phn4/vAYmfaKISKG4+QpjI+ehgEJzsIee2rgqOaePTP18fq8T7EDbF/B/iscKNQFyyNp8DWt2a8OetaCc5E/KsntbbOcNc7yikPZBdUezphrqIH4ztpicsHvEicYF002qWHoY"}],"role":"model"},"finishReason":"STOP","index":0}],"usageMetadata":{"promptTokenCount":12759,"candidatesTokenCount":4,"totalTokenCount":12986,"cachedContentTokenCount":12199,"promptTokensDetails":[{"modality":"TEXT","tokenCount":12759}],"cacheTokensDetails":[{"modality":"TEXT","tokenCount":12199}],"thoughtsTokenCount":223}}]}
@@ -1 +0,0 @@
{"method":"generateContentStream","response":[{"candidates":[{"content":{"parts":[{"text":"**Executing the Command**\n\nI've got the command, \"echo test,\" ready to go. My focus is entirely on calling the `run_shell_command` tool now. The user's input is processed, and the next step is straightforward: using the tool to execute the supplied command.\n\n\n","thought":true}],"role":"model"},"index":0}],"usageMetadata":{"promptTokenCount":12751,"totalTokenCount":12801,"promptTokensDetails":[{"modality":"TEXT","tokenCount":12751}],"thoughtsTokenCount":50}},{"candidates":[{"content":{"parts":[{"functionCall":{"name":"run_shell_command","args":{"command":"echo test","description":"Running the command 'echo test'"}},"thoughtSignature":"CiQBcsjafL1lDlnUGmt38n1/gjwecXzy9S3qEW5sYMEno5Mr7LEKZgFyyNp8jMABmMAatt49FTdh7UiM62SI1GnjcyG+kV7xzcD73uMKHST/0D0vKP7x1equv5d6YiXnOslhVnnHotYPtVl0/kI/0unBZRdMzkBNrJXKUoSWXJXxNpV6JhJav3Uh9h1sPQqOAQFyyNp8PFeESLk0J5cPFP0EA7a13iA/rXTiKoHnjSCzDV9ALcXM78xv10/V028ZtDeQslYfT82q4++W8AlJwTQRTIrdscu2y+nCS8jnQizYN1V1yR42eMzuBU3txXcqEV8bmP6GGOe58vrqyS2zdnJKCgMntMB/niwlJlr5frhDestSOJk62tVDWKFzOiAKOAFyyNp81FtGXQTX+OSio/2PbzpCCuaQFqpEgCZpkaXXyvmXYDAI1qCq1tA+m/e5ozWdm8zTGuyb"}],"role":"model"},"finishReason":"STOP","index":0}],"usageMetadata":{"promptTokenCount":12751,"candidatesTokenCount":28,"totalTokenCount":12829,"promptTokensDetails":[{"modality":"TEXT","tokenCount":12751}],"thoughtsTokenCount":50}}]}
@@ -1 +0,0 @@
{"method":"generateContentStream","response":[{"candidates":[{"content":{"parts":[{"text":"**Seeking Task Clarity**\n\nI'm currently focused on identifying the precise task. My initial assessment indicates the user is seeking assistance, but the specific requirements remain undefined. I will directly solicit a detailed task description from the user to clarify this.\n\n\n","thought":true}],"role":"model"},"index":0}],"usageMetadata":{"promptTokenCount":12604,"totalTokenCount":12633,"promptTokensDetails":[{"modality":"TEXT","tokenCount":12604}],"thoughtsTokenCount":29}},{"candidates":[{"content":{"parts":[{"text":"Hello! I'm ready to help. Please describe the task you'd like me to assist you with.","thoughtSignature":"CiQBcsjafM2CL00L595T19DK8M8zP5p9/tbFPPwdM2S6669z2FgKYQFyyNp8Ya0YVCtft9Asr/45XOCfNdPWbwZt8SvIeX3IxYzOFcOK14+DnoDIuTIrmRQBeUvdxD59QmEWx+/OaSxj9564L0IU703C1JX20buEtYhkRM4LhK0G4LG/z6IJauEKSQFyyNp8n784BnEcDTQGfZ8/s3pl/TNaNzjQx0o8wYCYZH1qsRbVa3YJAvRGrVXL6y9ka10w0lhEsrQ8vOiw6ilZKirA5DjLz4U="}],"role":"model"},"finishReason":"STOP","index":0}],"usageMetadata":{"promptTokenCount":12604,"candidatesTokenCount":22,"totalTokenCount":12655,"promptTokensDetails":[{"modality":"TEXT","tokenCount":12604}],"thoughtsTokenCount":29}}]}
@@ -1 +0,0 @@
{"method":"generateContentStream","response":[{"candidates":[{"content":{"parts":[{"text":"**Greeting the User**\n\nI've registered the user's greeting. I'm primed to respond with a friendly welcome and signal my availability to assist. My focus now is drafting a suitable response.\n\n\n","thought":true}],"role":"model"},"index":0}],"usageMetadata":{"promptTokenCount":12761,"totalTokenCount":12787,"promptTokensDetails":[{"modality":"TEXT","tokenCount":12761}],"thoughtsTokenCount":26}},{"candidates":[{"content":{"parts":[{"text":"Hello! I'm ready to help. What can I do for you?","thoughtSignature":"CikBcsjafBz/0rqJuIv9woxRvivjZyAqBjpoJhOTSPfcbMWCawTfcyKImQpxAXLI2nxyuBo6dqZmTxkH7XxPxjq7mNoacRa48wc/eT5caK/4tu0Y9fJ1ScpJZb+tCNzrqTNwVXa98ppjB2O/X4eejJN+hUr3LCalDFRdRLO17PFUI5qgYSbSgIGzhbnQASgzOArvvqzDPPgqXWVIDj8KMQFyyNp8ayfqBNRkBykRSTDtzOKVGkjLW1dXWamLB4ojeEVHSOgne4vlYaKs44pitsg="}],"role":"model"},"finishReason":"STOP","index":0}],"usageMetadata":{"promptTokenCount":12761,"candidatesTokenCount":15,"totalTokenCount":12802,"promptTokensDetails":[{"modality":"TEXT","tokenCount":12761}],"thoughtsTokenCount":26}}]}
@@ -1 +0,0 @@
{"method":"generateContentStream","response":[{"candidates":[{"content":{"parts":[{"text":"**Initiating a Dialogue**\n\nI've successfully received and understood the user's initial request. My next move will be to output a simple \"Hello\" as a greeting, fulfilling the basic instruction I was given. This constitutes the first step in the interaction, and I'm ready to move forward based on the user's subsequent input.\n\n\n","thought":true}],"role":"model"},"index":0}],"usageMetadata":{"promptTokenCount":12588,"totalTokenCount":12607,"promptTokensDetails":[{"modality":"TEXT","tokenCount":12588}],"thoughtsTokenCount":19}},{"candidates":[{"content":{"parts":[{"text":"Hello","thoughtSignature":"CikBcsjafB9jXawgyqQ5mpEJ4ihpLD/B2i8GR75sod00ZF3TCbrLHS9YjgpeAXLI2nx1fmJO2VIiwBpF+vLBPhYE/B2992PVW6XM20cEYx4g0leDNs6BIhzEipm6RYOxzgz8KxH9+ZkCnd8bVZr59lbDCgqSCSB6IKA+csXHKsF9g3UMRAtoSBwiBw=="}],"role":"model"},"finishReason":"STOP","index":0}],"usageMetadata":{"promptTokenCount":12588,"totalTokenCount":12607,"promptTokensDetails":[{"modality":"TEXT","tokenCount":12588}],"thoughtsTokenCount":19}}]}
File diff suppressed because it is too large Load Diff
-16
View File
@@ -37,15 +37,6 @@ describe('JSON output', () => {
expect(typeof parsed.stats).toBe('object');
});
it('should return a valid JSON with a session ID', async () => {
const result = await rig.run('Hello', '--output-format', 'json');
const parsed = JSON.parse(result);
expect(parsed).toHaveProperty('session_id');
expect(typeof parsed.session_id).toBe('string');
expect(parsed.session_id).not.toBe('');
});
it('should return a JSON error for sd auth mismatch before running', async () => {
process.env['GOOGLE_GENAI_USE_GCA'] = 'true';
await rig.setup('json-output-auth-mismatch', {
@@ -96,9 +87,6 @@ describe('JSON output', () => {
"enforced authentication type is 'gemini-api-key'",
);
expect(payload.error.message).toContain("current type is 'oauth-personal'");
expect(payload).toHaveProperty('session_id');
expect(typeof payload.session_id).toBe('string');
expect(payload.session_id).not.toBe('');
});
it('should not exit on tool errors and allow model to self-correct in JSON mode', async () => {
@@ -141,9 +129,5 @@ describe('JSON output', () => {
// Should NOT have an error field at the top level
expect(parsed.error).toBeUndefined();
expect(parsed).toHaveProperty('session_id');
expect(typeof parsed.session_id).toBe('string');
expect(parsed.session_id).not.toBe('');
});
});
+1 -3
View File
@@ -17,9 +17,7 @@ import { join } from 'node:path';
describe('list_directory', () => {
it('should be able to list a directory', async () => {
const rig = new TestRig();
await rig.setup('should be able to list a directory', {
settings: { tools: { core: ['list_directory'] } },
});
rig.setup('should be able to list a directory');
rig.createFile('file1.txt', 'file 1 content');
rig.mkdir('subdir');
rig.sync();
+1 -3
View File
@@ -10,9 +10,7 @@ import { TestRig, printDebugInfo, validateModelOutput } from './test-helper.js';
describe('read_many_files', () => {
it.skip('should be able to read multiple files', async () => {
const rig = new TestRig();
await rig.setup('should be able to read multiple files', {
settings: { tools: { core: ['read_many_files', 'read_file'] } },
});
await rig.setup('should be able to read multiple files');
rig.createFile('file1.txt', 'file 1 content');
rig.createFile('file2.txt', 'file 2 content');
+3 -10
View File
@@ -10,9 +10,7 @@ import { TestRig } from './test-helper.js';
describe('replace', () => {
it('should be able to replace content in a file', async () => {
const rig = new TestRig();
await rig.setup('should be able to replace content in a file', {
settings: { tools: { core: ['replace', 'read_file'] } },
});
await rig.setup('should be able to replace content in a file');
const fileName = 'file_to_replace.txt';
const originalContent = 'foo content';
@@ -32,7 +30,6 @@ describe('replace', () => {
const rig = new TestRig();
await rig.setup(
'should handle $ literally when replacing text ending with $',
{ settings: { tools: { core: ['replace', 'read_file'] } } },
);
const fileName = 'regex.yml';
@@ -53,9 +50,7 @@ describe('replace', () => {
it.skip('should insert a multi-line block of text', async () => {
const rig = new TestRig();
await rig.setup('should insert a multi-line block of text', {
settings: { tools: { core: ['replace', 'read_file'] } },
});
await rig.setup('should insert a multi-line block of text');
const fileName = 'insert_block.txt';
const originalContent = 'Line A\n<INSERT_TEXT_HERE>\nLine C';
const newBlock = 'First line\nSecond line\nThird line';
@@ -74,9 +69,7 @@ describe('replace', () => {
it.skip('should delete a block of text', async () => {
const rig = new TestRig();
await rig.setup('should delete a block of text', {
settings: { tools: { core: ['replace', 'read_file'] } },
});
await rig.setup('should delete a block of text');
const fileName = 'delete_block.txt';
const blockToDelete =
'## DELETE THIS ##\nThis is a block of text to delete.\n## END DELETE ##';
+6 -21
View File
@@ -86,9 +86,7 @@ function getChainedEchoCommand(): { allowPattern: string; command: string } {
describe('run_shell_command', () => {
it('should be able to run a shell command', async () => {
const rig = new TestRig();
await rig.setup('should be able to run a shell command', {
settings: { tools: { core: ['run_shell_command'] } },
});
await rig.setup('should be able to run a shell command');
const prompt = `Please run the command "echo hello-world" and show me the output`;
@@ -120,9 +118,7 @@ describe('run_shell_command', () => {
it('should be able to run a shell command via stdin', async () => {
const rig = new TestRig();
await rig.setup('should be able to run a shell command via stdin', {
settings: { tools: { core: ['run_shell_command'] } },
});
await rig.setup('should be able to run a shell command via stdin');
const prompt = `Please run the command "echo test-stdin" and show me what it outputs`;
@@ -234,9 +230,7 @@ describe('run_shell_command', () => {
it('should succeed with --yolo mode', async () => {
const rig = new TestRig();
await rig.setup('should succeed with --yolo mode', {
settings: { tools: { core: ['run_shell_command'] } },
});
await rig.setup('should succeed with --yolo mode');
const testFile = rig.createFile('test.txt', 'Lorem\nIpsum\nDolor\n');
const { command } = getLineCountCommand();
@@ -368,9 +362,7 @@ describe('run_shell_command', () => {
it('should reject commands not on the allowlist', async () => {
const rig = new TestRig();
await rig.setup('should reject commands not on the allowlist', {
settings: { tools: { core: ['run_shell_command'] } },
});
await rig.setup('should reject commands not on the allowlist');
const testFile = rig.createFile('test.txt', 'Disallowed command check\n');
const allowedCommand = getAllowedListCommand();
@@ -469,9 +461,6 @@ describe('run_shell_command', () => {
const rig = new TestRig();
await rig.setup(
'should allow all with "ShellTool" and other specific tools',
{
settings: { tools: { core: ['run_shell_command'] } },
},
);
const { tool } = getLineCountCommand();
@@ -517,9 +506,7 @@ describe('run_shell_command', () => {
it('should propagate environment variables to the child process', async () => {
const rig = new TestRig();
await rig.setup('should propagate environment variables', {
settings: { tools: { core: ['run_shell_command'] } },
});
await rig.setup('should propagate environment variables');
const varName = 'GEMINI_CLI_TEST_VAR';
const varValue = `test-value-${Math.random().toString(36).substring(7)}`;
@@ -579,9 +566,7 @@ describe('run_shell_command', () => {
it('rejects invalid shell expressions', async () => {
const rig = new TestRig();
await rig.setup('rejects invalid shell expressions', {
settings: { tools: { core: ['run_shell_command'] } },
});
await rig.setup('rejects invalid shell expressions');
const invalidCommand = getInvalidCommand();
const result = await rig.run(
`I am testing the error handling of the run_shell_command tool. Please attempt to run the following command, which I know has invalid syntax: \`${invalidCommand}\`. If the command fails as expected, please return the word FAIL, otherwise return the word SUCCESS.`,
+1 -3
View File
@@ -10,9 +10,7 @@ import { TestRig, printDebugInfo, validateModelOutput } from './test-helper.js';
describe('save_memory', () => {
it('should be able to save to memory', async () => {
const rig = new TestRig();
await rig.setup('should be able to save to memory', {
settings: { tools: { core: ['save_memory'] } },
});
await rig.setup('should be able to save to memory');
const prompt = `remember that my favorite color is blue.
@@ -177,7 +177,6 @@ describe('simple-mcp-server', () => {
args: ['mcp-server.cjs'],
},
},
tools: { core: [] },
},
});
+5 -22
View File
@@ -1024,28 +1024,11 @@ export class TestRig {
return null;
}
async runInteractive(
options?: { yolo?: boolean } | string,
...args: string[]
): Promise<InteractiveRun> {
// Handle backward compatibility: if first param is a string, treat as arg
let yolo = true; // Default to YOLO mode
let additionalArgs: string[] = args;
async runInteractive(...args: string[]): Promise<InteractiveRun> {
const { command, initialArgs } = this._getCommandAndArgs(['--yolo']);
const commandArgs = [...initialArgs, ...args];
if (typeof options === 'string') {
// Old-style call: runInteractive('--debug')
additionalArgs = [options, ...args];
} else if (typeof options === 'object' && options !== null) {
// New-style call: runInteractive({ yolo: false })
yolo = options.yolo !== false;
}
const { command, initialArgs } = this._getCommandAndArgs(
yolo ? ['--yolo'] : [],
);
const commandArgs = [...initialArgs, ...additionalArgs];
const ptyOptions: pty.IPtyForkOptions = {
const options: pty.IPtyForkOptions = {
name: 'xterm-color',
cols: 80,
rows: 80,
@@ -1056,7 +1039,7 @@ export class TestRig {
};
const executable = command === 'node' ? process.execPath : command;
const ptyProcess = pty.spawn(executable, commandArgs, ptyOptions);
const ptyProcess = pty.spawn(executable, commandArgs, options);
const run = new InteractiveRun(ptyProcess);
// Wait for the app to be ready
+1 -3
View File
@@ -53,9 +53,7 @@ let dir: string;
describe('BOM end-to-end integraion', () => {
beforeAll(async () => {
rig = new TestRig();
await rig.setup('bom-integration', {
settings: { tools: { core: ['read_file'] } },
});
await rig.setup('bom-integration');
dir = rig.testDir!;
});
+1 -3
View File
@@ -15,9 +15,7 @@ import {
describe('write_file', () => {
it('should be able to write a file', async () => {
const rig = new TestRig();
await rig.setup('should be able to write a file', {
settings: { tools: { core: ['write_file', 'read_file'] } },
});
await rig.setup('should be able to write a file');
const prompt = `show me an example of using the write tool. put a dad joke in dad.txt`;
const result = await rig.run(prompt);
+47 -254
View File
@@ -1,12 +1,12 @@
{
"name": "@google/gemini-cli",
"version": "0.21.1",
"version": "0.20.2",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@google/gemini-cli",
"version": "0.21.1",
"version": "0.20.2",
"workspaces": [
"packages/*"
],
@@ -2112,27 +2112,23 @@
}
},
"node_modules/@modelcontextprotocol/sdk/node_modules/body-parser": {
"version": "2.2.1",
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.1.tgz",
"integrity": "sha512-nfDwkulwiZYQIGwxdy0RUmowMhKcFVcYXUU7m4QlKYim1rUtg83xm2yjZ40QjDuc291AJjjeSc9b++AWHSgSHw==",
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.0.tgz",
"integrity": "sha512-02qvAaxv8tp7fBa/mw1ga98OGm+eCbqzJOKoRt70sLmfEEi+jyBYVTDGfCL/k06/4EMk/z01gCe7HoCH/f2LTg==",
"license": "MIT",
"dependencies": {
"bytes": "^3.1.2",
"content-type": "^1.0.5",
"debug": "^4.4.3",
"debug": "^4.4.0",
"http-errors": "^2.0.0",
"iconv-lite": "^0.7.0",
"iconv-lite": "^0.6.3",
"on-finished": "^2.4.1",
"qs": "^6.14.0",
"raw-body": "^3.0.1",
"type-is": "^2.0.1"
"raw-body": "^3.0.0",
"type-is": "^2.0.0"
},
"engines": {
"node": ">=18"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/express"
}
},
"node_modules/@modelcontextprotocol/sdk/node_modules/content-disposition": {
@@ -2224,22 +2220,6 @@
"node": ">= 0.8"
}
},
"node_modules/@modelcontextprotocol/sdk/node_modules/iconv-lite": {
"version": "0.7.0",
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.0.tgz",
"integrity": "sha512-cf6L2Ds3h57VVmkZe+Pn+5APsT7FpqJtEhhieDCvrE2MK5Qk9MyffgQyuxQTm6BChfeZNtcOLHp9IcWRVcIcBQ==",
"license": "MIT",
"dependencies": {
"safer-buffer": ">= 2.1.2 < 3.0.0"
},
"engines": {
"node": ">=0.10.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/express"
}
},
"node_modules/@modelcontextprotocol/sdk/node_modules/json-schema-traverse": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz",
@@ -5488,7 +5468,6 @@
"resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz",
"integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==",
"license": "MIT",
"peer": true,
"dependencies": {
"mime-types": "~2.1.34",
"negotiator": "0.6.3"
@@ -5502,7 +5481,6 @@
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
"integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
"license": "MIT",
"peer": true,
"dependencies": {
"mime-db": "1.52.0"
},
@@ -5772,9 +5750,9 @@
}
},
"node_modules/archiver-utils/node_modules/glob": {
"version": "10.5.0",
"resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz",
"integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==",
"version": "10.4.5",
"resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz",
"integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==",
"dev": true,
"license": "ISC",
"dependencies": {
@@ -6318,7 +6296,6 @@
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz",
"integrity": "sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==",
"license": "MIT",
"peer": true,
"dependencies": {
"bytes": "3.1.2",
"content-type": "~1.0.5",
@@ -6343,7 +6320,6 @@
"resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
"integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
"license": "MIT",
"peer": true,
"dependencies": {
"ms": "2.0.0"
}
@@ -6353,7 +6329,6 @@
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
"integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==",
"license": "MIT",
"peer": true,
"dependencies": {
"safer-buffer": ">= 2.1.2 < 3"
},
@@ -6365,15 +6340,13 @@
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
"integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
"license": "MIT",
"peer": true
"license": "MIT"
},
"node_modules/body-parser/node_modules/raw-body": {
"version": "2.5.2",
"resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz",
"integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==",
"license": "MIT",
"peer": true,
"dependencies": {
"bytes": "3.1.2",
"http-errors": "2.0.0",
@@ -7259,8 +7232,7 @@
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz",
"integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==",
"license": "MIT",
"peer": true
"license": "MIT"
},
"node_modules/cookiejar": {
"version": "2.1.4",
@@ -7686,7 +7658,6 @@
"resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz",
"integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==",
"license": "MIT",
"peer": true,
"engines": {
"node": ">= 0.8",
"npm": "1.2.8000 || >= 1.4.16"
@@ -9319,7 +9290,6 @@
"resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz",
"integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==",
"license": "MIT",
"peer": true,
"engines": {
"node": ">= 0.6"
}
@@ -10229,7 +10199,6 @@
"version": "0.6.3",
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz",
"integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==",
"dev": true,
"license": "MIT",
"dependencies": {
"safer-buffer": ">= 2.1.2 < 3.0.0"
@@ -12105,7 +12074,6 @@
"resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz",
"integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==",
"license": "MIT",
"peer": true,
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
@@ -12426,7 +12394,6 @@
"resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz",
"integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==",
"license": "MIT",
"peer": true,
"engines": {
"node": ">= 0.6"
}
@@ -13926,54 +13893,18 @@
}
},
"node_modules/raw-body": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.2.tgz",
"integrity": "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==",
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.0.tgz",
"integrity": "sha512-RmkhL8CAyCRPXCE28MMH0z2PNWQBNk2Q09ZdxM9IOOXwxwZbN+qbWaatPkdkWIKL2ZVDImrN/pK5HTRz2PcS4g==",
"license": "MIT",
"dependencies": {
"bytes": "~3.1.2",
"http-errors": "~2.0.1",
"iconv-lite": "~0.7.0",
"unpipe": "~1.0.0"
},
"engines": {
"node": ">= 0.10"
}
},
"node_modules/raw-body/node_modules/http-errors": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz",
"integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==",
"license": "MIT",
"dependencies": {
"depd": "~2.0.0",
"inherits": "~2.0.4",
"setprototypeof": "~1.2.0",
"statuses": "~2.0.2",
"toidentifier": "~1.0.1"
"bytes": "3.1.2",
"http-errors": "2.0.0",
"iconv-lite": "0.6.3",
"unpipe": "1.0.0"
},
"engines": {
"node": ">= 0.8"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/express"
}
},
"node_modules/raw-body/node_modules/iconv-lite": {
"version": "0.7.0",
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.0.tgz",
"integrity": "sha512-cf6L2Ds3h57VVmkZe+Pn+5APsT7FpqJtEhhieDCvrE2MK5Qk9MyffgQyuxQTm6BChfeZNtcOLHp9IcWRVcIcBQ==",
"license": "MIT",
"dependencies": {
"safer-buffer": ">= 2.1.2 < 3.0.0"
},
"engines": {
"node": ">=0.10.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/express"
}
},
"node_modules/rc": {
@@ -14850,7 +14781,6 @@
"resolved": "https://registry.npmjs.org/send/-/send-0.19.0.tgz",
"integrity": "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==",
"license": "MIT",
"peer": true,
"dependencies": {
"debug": "2.6.9",
"depd": "2.0.0",
@@ -14875,7 +14805,6 @@
"resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
"integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
"license": "MIT",
"peer": true,
"dependencies": {
"ms": "2.0.0"
}
@@ -14884,15 +14813,13 @@
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
"integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
"license": "MIT",
"peer": true
"license": "MIT"
},
"node_modules/send/node_modules/encodeurl": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz",
"integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==",
"license": "MIT",
"peer": true,
"engines": {
"node": ">= 0.8"
}
@@ -14902,7 +14829,6 @@
"resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz",
"integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==",
"license": "MIT",
"peer": true,
"bin": {
"mime": "cli.js"
},
@@ -14915,7 +14841,6 @@
"resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz",
"integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==",
"license": "MIT",
"peer": true,
"engines": {
"node": ">= 0.8"
}
@@ -14925,7 +14850,6 @@
"resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.2.tgz",
"integrity": "sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==",
"license": "MIT",
"peer": true,
"dependencies": {
"encodeurl": "~2.0.0",
"escape-html": "~1.0.3",
@@ -16080,9 +16004,9 @@
}
},
"node_modules/test-exclude/node_modules/glob": {
"version": "10.5.0",
"resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz",
"integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==",
"version": "10.4.5",
"resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz",
"integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==",
"dev": true,
"license": "ISC",
"dependencies": {
@@ -16506,7 +16430,6 @@
"resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz",
"integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==",
"license": "MIT",
"peer": true,
"dependencies": {
"media-typer": "0.3.0",
"mime-types": "~2.1.24"
@@ -16520,7 +16443,6 @@
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
"integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
"license": "MIT",
"peer": true,
"dependencies": {
"mime-db": "1.52.0"
},
@@ -17692,12 +17614,12 @@
},
"packages/a2a-server": {
"name": "@google/gemini-cli-a2a-server",
"version": "0.21.1",
"version": "0.20.2",
"dependencies": {
"@a2a-js/sdk": "^0.3.2",
"@google-cloud/storage": "^7.16.0",
"@google/gemini-cli-core": "file:../core",
"express": "^5.2.0",
"express": "^5.1.0",
"fs-extra": "^11.3.0",
"tar": "^7.5.2",
"uuid": "^11.1.0",
@@ -17734,27 +17656,23 @@
}
},
"packages/a2a-server/node_modules/body-parser": {
"version": "2.2.1",
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.1.tgz",
"integrity": "sha512-nfDwkulwiZYQIGwxdy0RUmowMhKcFVcYXUU7m4QlKYim1rUtg83xm2yjZ40QjDuc291AJjjeSc9b++AWHSgSHw==",
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.0.tgz",
"integrity": "sha512-02qvAaxv8tp7fBa/mw1ga98OGm+eCbqzJOKoRt70sLmfEEi+jyBYVTDGfCL/k06/4EMk/z01gCe7HoCH/f2LTg==",
"license": "MIT",
"dependencies": {
"bytes": "^3.1.2",
"content-type": "^1.0.5",
"debug": "^4.4.3",
"debug": "^4.4.0",
"http-errors": "^2.0.0",
"iconv-lite": "^0.7.0",
"iconv-lite": "^0.6.3",
"on-finished": "^2.4.1",
"qs": "^6.14.0",
"raw-body": "^3.0.1",
"type-is": "^2.0.1"
"raw-body": "^3.0.0",
"type-is": "^2.0.0"
},
"engines": {
"node": ">=18"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/express"
}
},
"packages/a2a-server/node_modules/content-disposition": {
@@ -17792,19 +17710,18 @@
}
},
"packages/a2a-server/node_modules/express": {
"version": "5.2.1",
"resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz",
"integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==",
"version": "5.1.0",
"resolved": "https://registry.npmjs.org/express/-/express-5.1.0.tgz",
"integrity": "sha512-DT9ck5YIRU+8GYzzU5kT3eHGA5iL+1Zd0EutOmTE9Dtk+Tvuzd23VBU+ec7HPNSTxXYO55gPV/hq4pSBJDjFpA==",
"license": "MIT",
"dependencies": {
"accepts": "^2.0.0",
"body-parser": "^2.2.1",
"body-parser": "^2.2.0",
"content-disposition": "^1.0.0",
"content-type": "^1.0.5",
"cookie": "^0.7.1",
"cookie-signature": "^1.2.1",
"debug": "^4.4.0",
"depd": "^2.0.0",
"encodeurl": "^2.0.0",
"escape-html": "^1.0.3",
"etag": "^1.8.1",
@@ -17860,22 +17777,6 @@
"node": ">= 0.8"
}
},
"packages/a2a-server/node_modules/iconv-lite": {
"version": "0.7.0",
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.0.tgz",
"integrity": "sha512-cf6L2Ds3h57VVmkZe+Pn+5APsT7FpqJtEhhieDCvrE2MK5Qk9MyffgQyuxQTm6BChfeZNtcOLHp9IcWRVcIcBQ==",
"license": "MIT",
"dependencies": {
"safer-buffer": ">= 2.1.2 < 3.0.0"
},
"engines": {
"node": ">=0.10.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/express"
}
},
"packages/a2a-server/node_modules/media-typer": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz",
@@ -18003,7 +17904,7 @@
},
"packages/cli": {
"name": "@google/gemini-cli",
"version": "0.21.1",
"version": "0.20.2",
"dependencies": {
"@google/gemini-cli-core": "file:../core",
"@google/genai": "1.30.0",
@@ -18104,7 +18005,7 @@
},
"packages/core": {
"name": "@google/gemini-cli-core",
"version": "0.21.1",
"version": "0.20.2",
"dependencies": {
"@google-cloud/logging": "^11.2.1",
"@google-cloud/opentelemetry-cloud-monitoring-exporter": "^0.21.0",
@@ -18248,7 +18149,7 @@
},
"packages/test-utils": {
"name": "@google/gemini-cli-test-utils",
"version": "0.21.1",
"version": "0.20.2",
"license": "Apache-2.0",
"devDependencies": {
"typescript": "^5.3.3"
@@ -18259,12 +18160,12 @@
},
"packages/vscode-ide-companion": {
"name": "gemini-cli-vscode-ide-companion",
"version": "0.21.1",
"version": "0.20.2",
"license": "LICENSE",
"dependencies": {
"@modelcontextprotocol/sdk": "^1.23.0",
"cors": "^2.8.5",
"express": "^5.2.0",
"express": "^5.1.0",
"zod": "^3.25.76"
},
"devDependencies": {
@@ -18292,43 +18193,6 @@
"dev": true,
"license": "MIT"
},
"packages/vscode-ide-companion/node_modules/accepts": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz",
"integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==",
"license": "MIT",
"dependencies": {
"mime-types": "^3.0.0",
"negotiator": "^1.0.0"
},
"engines": {
"node": ">= 0.6"
}
},
"packages/vscode-ide-companion/node_modules/body-parser": {
"version": "2.2.1",
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.1.tgz",
"integrity": "sha512-nfDwkulwiZYQIGwxdy0RUmowMhKcFVcYXUU7m4QlKYim1rUtg83xm2yjZ40QjDuc291AJjjeSc9b++AWHSgSHw==",
"license": "MIT",
"dependencies": {
"bytes": "^3.1.2",
"content-type": "^1.0.5",
"debug": "^4.4.3",
"http-errors": "^2.0.0",
"iconv-lite": "^0.7.0",
"on-finished": "^2.4.1",
"qs": "^6.14.0",
"raw-body": "^3.0.1",
"type-is": "^2.0.1"
},
"engines": {
"node": ">=18"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/express"
}
},
"packages/vscode-ide-companion/node_modules/content-disposition": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.0.tgz",
@@ -18341,29 +18205,19 @@
"node": ">= 0.6"
}
},
"packages/vscode-ide-companion/node_modules/cookie-signature": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz",
"integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==",
"license": "MIT",
"engines": {
"node": ">=6.6.0"
}
},
"packages/vscode-ide-companion/node_modules/express": {
"version": "5.2.1",
"resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz",
"integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==",
"version": "5.1.0",
"resolved": "https://registry.npmjs.org/express/-/express-5.1.0.tgz",
"integrity": "sha512-DT9ck5YIRU+8GYzzU5kT3eHGA5iL+1Zd0EutOmTE9Dtk+Tvuzd23VBU+ec7HPNSTxXYO55gPV/hq4pSBJDjFpA==",
"license": "MIT",
"dependencies": {
"accepts": "^2.0.0",
"body-parser": "^2.2.1",
"body-parser": "^2.2.0",
"content-disposition": "^1.0.0",
"content-type": "^1.0.5",
"cookie": "^0.7.1",
"cookie-signature": "^1.2.1",
"debug": "^4.4.0",
"depd": "^2.0.0",
"encodeurl": "^2.0.0",
"escape-html": "^1.0.3",
"etag": "^1.8.1",
@@ -18410,52 +18264,6 @@
"node": ">= 0.8"
}
},
"packages/vscode-ide-companion/node_modules/fresh": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz",
"integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==",
"license": "MIT",
"engines": {
"node": ">= 0.8"
}
},
"packages/vscode-ide-companion/node_modules/iconv-lite": {
"version": "0.7.0",
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.0.tgz",
"integrity": "sha512-cf6L2Ds3h57VVmkZe+Pn+5APsT7FpqJtEhhieDCvrE2MK5Qk9MyffgQyuxQTm6BChfeZNtcOLHp9IcWRVcIcBQ==",
"license": "MIT",
"dependencies": {
"safer-buffer": ">= 2.1.2 < 3.0.0"
},
"engines": {
"node": ">=0.10.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/express"
}
},
"packages/vscode-ide-companion/node_modules/merge-descriptors": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz",
"integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==",
"license": "MIT",
"engines": {
"node": ">=18"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"packages/vscode-ide-companion/node_modules/negotiator": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz",
"integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==",
"license": "MIT",
"engines": {
"node": ">= 0.6"
}
},
"packages/vscode-ide-companion/node_modules/qs": {
"version": "6.14.0",
"resolved": "https://registry.npmjs.org/qs/-/qs-6.14.0.tgz",
@@ -18493,21 +18301,6 @@
"node": ">= 18"
}
},
"packages/vscode-ide-companion/node_modules/serve-static": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.0.tgz",
"integrity": "sha512-61g9pCh0Vnh7IutZjtLGGpTA355+OPn2TyDv/6ivP2h/AdAVX9azsoxmg2/M6nZeQZNYBEwIcsne1mJd9oQItQ==",
"license": "MIT",
"dependencies": {
"encodeurl": "^2.0.0",
"escape-html": "^1.0.3",
"parseurl": "^1.3.3",
"send": "^1.2.0"
},
"engines": {
"node": ">= 18"
}
},
"packages/vscode-ide-companion/node_modules/type-is": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz",
+2 -2
View File
@@ -1,6 +1,6 @@
{
"name": "@google/gemini-cli",
"version": "0.21.1",
"version": "0.20.2",
"engines": {
"node": ">=20.0.0"
},
@@ -14,7 +14,7 @@
"url": "git+https://github.com/google-gemini/gemini-cli.git"
},
"config": {
"sandboxImageUri": "us-docker.pkg.dev/gemini-code-dev/gemini-cli/sandbox:0.21.1"
"sandboxImageUri": "us-docker.pkg.dev/gemini-code-dev/gemini-cli/sandbox:0.20.2"
},
"scripts": {
"start": "cross-env NODE_ENV=development node scripts/start.js",
+2 -2
View File
@@ -1,6 +1,6 @@
{
"name": "@google/gemini-cli-a2a-server",
"version": "0.21.1",
"version": "0.20.2",
"description": "Gemini CLI A2A Server",
"repository": {
"type": "git",
@@ -28,7 +28,7 @@
"@a2a-js/sdk": "^0.3.2",
"@google-cloud/storage": "^7.16.0",
"@google/gemini-cli-core": "file:../core",
"express": "^5.2.0",
"express": "^5.1.0",
"fs-extra": "^11.3.0",
"tar": "^7.5.2",
"uuid": "^11.1.0",
@@ -397,7 +397,6 @@ export class CoderAgentExecutor implements AgentExecutor {
`[CoderAgentExecutor] Error creating task ${taskId}:`,
error,
);
// eslint-disable-next-line @typescript-eslint/no-floating-promises
pushTaskStateFailed(error, eventBus, taskId, contextId);
return;
}
+1 -232
View File
@@ -18,25 +18,12 @@ import {
GeminiEventType,
type Config,
type ToolCallRequestInfo,
type GitService,
type CompletedToolCall,
} from '@google/gemini-cli-core';
import { createMockConfig } from '../utils/testing_utils.js';
import type { ExecutionEventBus, RequestContext } from '@a2a-js/sdk/server';
import type { ExecutionEventBus } from '@a2a-js/sdk/server';
import { CoderAgentEvent } from '../types.js';
import type { ToolCall } from '@google/gemini-cli-core';
const mockProcessRestorableToolCalls = vi.hoisted(() => vi.fn());
vi.mock('@google/gemini-cli-core', async (importOriginal) => {
const original =
await importOriginal<typeof import('@google/gemini-cli-core')>();
return {
...original,
processRestorableToolCalls: mockProcessRestorableToolCalls,
};
});
describe('Task', () => {
it('scheduleToolCalls should not modify the input requests array', async () => {
const mockConfig = createMockConfig();
@@ -84,141 +71,6 @@ describe('Task', () => {
expect(requests).toEqual(originalRequests);
});
describe('scheduleToolCalls', () => {
const mockConfig = createMockConfig();
const mockEventBus: ExecutionEventBus = {
publish: vi.fn(),
on: vi.fn(),
off: vi.fn(),
once: vi.fn(),
removeAllListeners: vi.fn(),
finished: vi.fn(),
};
beforeEach(() => {
vi.clearAllMocks();
});
it('should not create a checkpoint if no restorable tools are called', async () => {
// @ts-expect-error - Calling private constructor for test purposes.
const task = new Task(
'task-id',
'context-id',
mockConfig as Config,
mockEventBus,
);
const requests: ToolCallRequestInfo[] = [
{
callId: '1',
name: 'run_shell_command',
args: { command: 'ls' },
isClientInitiated: false,
prompt_id: 'prompt-id-1',
},
];
const abortController = new AbortController();
await task.scheduleToolCalls(requests, abortController.signal);
expect(mockProcessRestorableToolCalls).not.toHaveBeenCalled();
});
it('should create a checkpoint if a restorable tool is called', async () => {
const mockConfig = createMockConfig({
getCheckpointingEnabled: () => true,
getGitService: () => Promise.resolve({} as GitService),
});
mockProcessRestorableToolCalls.mockResolvedValue({
checkpointsToWrite: new Map([['test.json', 'test content']]),
toolCallToCheckpointMap: new Map(),
errors: [],
});
// @ts-expect-error - Calling private constructor for test purposes.
const task = new Task(
'task-id',
'context-id',
mockConfig as Config,
mockEventBus,
);
const requests: ToolCallRequestInfo[] = [
{
callId: '1',
name: 'replace',
args: {
file_path: 'test.txt',
old_string: 'old',
new_string: 'new',
},
isClientInitiated: false,
prompt_id: 'prompt-id-1',
},
];
const abortController = new AbortController();
await task.scheduleToolCalls(requests, abortController.signal);
expect(mockProcessRestorableToolCalls).toHaveBeenCalledOnce();
});
it('should process all restorable tools for checkpointing in a single batch', async () => {
const mockConfig = createMockConfig({
getCheckpointingEnabled: () => true,
getGitService: () => Promise.resolve({} as GitService),
});
mockProcessRestorableToolCalls.mockResolvedValue({
checkpointsToWrite: new Map([
['test1.json', 'test content 1'],
['test2.json', 'test content 2'],
]),
toolCallToCheckpointMap: new Map([
['1', 'test1'],
['2', 'test2'],
]),
errors: [],
});
// @ts-expect-error - Calling private constructor for test purposes.
const task = new Task(
'task-id',
'context-id',
mockConfig as Config,
mockEventBus,
);
const requests: ToolCallRequestInfo[] = [
{
callId: '1',
name: 'replace',
args: {
file_path: 'test.txt',
old_string: 'old',
new_string: 'new',
},
isClientInitiated: false,
prompt_id: 'prompt-id-1',
},
{
callId: '2',
name: 'write_file',
args: { file_path: 'test2.txt', content: 'new content' },
isClientInitiated: false,
prompt_id: 'prompt-id-2',
},
{
callId: '3',
name: 'not_restorable',
args: {},
isClientInitiated: false,
prompt_id: 'prompt-id-3',
},
];
const abortController = new AbortController();
await task.scheduleToolCalls(requests, abortController.signal);
expect(mockProcessRestorableToolCalls).toHaveBeenCalledExactlyOnceWith(
[
expect.objectContaining({ callId: '1' }),
expect.objectContaining({ callId: '2' }),
],
expect.anything(),
expect.anything(),
);
});
});
describe('acceptAgentMessage', () => {
it('should set currentTraceId when event has traceId', async () => {
const mockConfig = createMockConfig();
@@ -466,87 +318,4 @@ describe('Task', () => {
expect(finalCall).toBeUndefined();
});
});
describe('currentPromptId and promptCount', () => {
it('should correctly initialize and update promptId and promptCount', async () => {
const mockConfig = createMockConfig();
mockConfig.getGeminiClient = vi.fn().mockReturnValue({
sendMessageStream: vi.fn().mockReturnValue((async function* () {})()),
});
mockConfig.getSessionId = () => 'test-session-id';
const mockEventBus: ExecutionEventBus = {
publish: vi.fn(),
on: vi.fn(),
off: vi.fn(),
once: vi.fn(),
removeAllListeners: vi.fn(),
finished: vi.fn(),
};
// @ts-expect-error - Calling private constructor
const task = new Task(
'task-id',
'context-id',
mockConfig as Config,
mockEventBus,
);
// Initial state
expect(task.currentPromptId).toBeUndefined();
expect(task.promptCount).toBe(0);
// First user message should set prompt_id
const userMessage1 = {
userMessage: {
parts: [{ kind: 'text', text: 'hello' }],
},
} as RequestContext;
const abortController1 = new AbortController();
for await (const _ of task.acceptUserMessage(
userMessage1,
abortController1.signal,
)) {
// no-op
}
const expectedPromptId1 = 'test-session-id########0';
expect(task.promptCount).toBe(1);
expect(task.currentPromptId).toBe(expectedPromptId1);
// A new user message should generate a new prompt_id
const userMessage2 = {
userMessage: {
parts: [{ kind: 'text', text: 'world' }],
},
} as RequestContext;
const abortController2 = new AbortController();
for await (const _ of task.acceptUserMessage(
userMessage2,
abortController2.signal,
)) {
// no-op
}
const expectedPromptId2 = 'test-session-id########1';
expect(task.promptCount).toBe(2);
expect(task.currentPromptId).toBe(expectedPromptId2);
// Subsequent tool call processing should use the same prompt_id
const completedTool = {
request: { callId: 'tool-1' },
response: { responseParts: [{ text: 'tool output' }] },
} as CompletedToolCall;
const abortController3 = new AbortController();
for await (const _ of task.sendCompletedToolsToLlm(
[completedTool],
abortController3.signal,
)) {
// no-op
}
expect(task.promptCount).toBe(2);
expect(task.currentPromptId).toBe(expectedPromptId2);
});
});
});
+8 -61
View File
@@ -15,7 +15,6 @@ import {
isNodeError,
parseAndFormatApiError,
safeLiteralReplace,
DEFAULT_GUI_EDITOR,
type AnyDeclarativeTool,
type ToolCall,
type ToolConfirmationPayload,
@@ -27,8 +26,6 @@ import {
type Config,
type UserTierId,
type AnsiOutput,
EDIT_TOOL_NAMES,
processRestorableToolCalls,
} from '@google/gemini-cli-core';
import type { RequestContext } from '@a2a-js/sdk/server';
import { type ExecutionEventBus } from '@a2a-js/sdk/server';
@@ -42,8 +39,7 @@ import type {
} from '@a2a-js/sdk';
import { v4 as uuidv4 } from 'uuid';
import { logger } from '../utils/logger.js';
import * as fs from 'node:fs/promises';
import * as path from 'node:path';
import * as fs from 'node:fs';
import { CoderAgentEvent } from '../types.js';
import type {
CoderAgentMessage,
@@ -71,8 +67,6 @@ export class Task {
completedToolCalls: CompletedToolCall[];
skipFinalTrueAfterInlineEdit = false;
modelInfo?: string;
currentPromptId: string | undefined;
promptCount = 0;
// For tool waiting logic
private pendingToolCalls: Map<string, string> = new Map(); //toolCallId --> status
@@ -400,7 +394,6 @@ export class Task {
logger.info('[Task] YOLO mode enabled. Auto-approving all tool calls.');
toolCalls.forEach((tc: ToolCall) => {
if (tc.status === 'awaiting_approval' && tc.confirmationDetails) {
// eslint-disable-next-line @typescript-eslint/no-floating-promises
tc.confirmationDetails.onConfirm(ToolConfirmationOutcome.ProceedOnce);
this.pendingToolConfirmationDetails.delete(tc.request.callId);
}
@@ -442,7 +435,7 @@ export class Task {
outputUpdateHandler: this._schedulerOutputUpdate.bind(this),
onAllToolCallsComplete: this._schedulerAllToolCallsComplete.bind(this),
onToolCallsUpdate: this._schedulerToolCallsUpdate.bind(this),
getPreferredEditor: () => DEFAULT_GUI_EDITOR,
getPreferredEditor: () => 'vscode',
config: this.config,
});
return scheduler;
@@ -514,7 +507,7 @@ export class Task {
new_string: string,
): Promise<string> {
try {
const currentContent = await fs.readFile(file_path, 'utf8');
const currentContent = fs.readFileSync(file_path, 'utf8');
return this._applyReplacement(
currentContent,
old_string,
@@ -557,44 +550,6 @@ export class Task {
return;
}
// Set checkpoint file before any file modification tool executes
const restorableToolCalls = requests.filter((request) =>
EDIT_TOOL_NAMES.has(request.name),
);
if (restorableToolCalls.length > 0) {
const gitService = await this.config.getGitService();
if (gitService) {
const { checkpointsToWrite, toolCallToCheckpointMap, errors } =
await processRestorableToolCalls(
restorableToolCalls,
gitService,
this.geminiClient,
);
if (errors.length > 0) {
errors.forEach((error) => logger.error(error));
}
if (checkpointsToWrite.size > 0) {
const checkpointDir =
this.config.storage.getProjectTempCheckpointsDir();
await fs.mkdir(checkpointDir, { recursive: true });
for (const [fileName, content] of checkpointsToWrite) {
const filePath = path.join(checkpointDir, fileName);
await fs.writeFile(filePath, content);
}
}
for (const request of requests) {
const checkpoint = toolCallToCheckpointMap.get(request.callId);
if (checkpoint) {
request.checkpoint = checkpoint;
}
}
}
}
const updatedRequests = await Promise.all(
requests.map(async (request) => {
if (
@@ -791,14 +746,7 @@ export class Task {
} as ToolConfirmationPayload)
: undefined;
this.skipFinalTrueAfterInlineEdit = !!payload;
try {
await confirmationDetails.onConfirm(confirmationOutcome, payload);
} finally {
// Once confirmationDetails.onConfirm finishes (or fails) with a payload,
// reset skipFinalTrueAfterInlineEdit so that external callers receive
// their call has been completed.
this.skipFinalTrueAfterInlineEdit = false;
}
await confirmationDetails.onConfirm(confirmationOutcome, payload);
} else {
await confirmationDetails.onConfirm(confirmationOutcome);
}
@@ -870,7 +818,6 @@ export class Task {
} else {
parts = [response];
}
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.geminiClient.addHistory({
role: 'user',
parts,
@@ -909,10 +856,11 @@ export class Task {
};
// Set task state to working as we are about to call LLM
this.setTaskStateAndPublishUpdate('working', stateChange);
// TODO: Determine what it mean to have, then add a prompt ID.
yield* this.geminiClient.sendMessageStream(
llmParts,
aborted,
completedToolCalls[0]?.request.prompt_id ?? '',
/*prompt_id*/ '',
);
}
@@ -942,18 +890,17 @@ export class Task {
}
if (hasContentForLlm) {
this.currentPromptId =
this.config.getSessionId() + '########' + this.promptCount++;
logger.info('[Task] Sending new parts to LLM.');
const stateChange: StateChange = {
kind: CoderAgentEvent.StateChangeEvent,
};
// Set task state to working as we are about to call LLM
this.setTaskStateAndPublishUpdate('working', stateChange);
// TODO: Determine what it mean to have, then add a prompt ID.
yield* this.geminiClient.sendMessageStream(
llmParts,
aborted,
this.currentPromptId,
/*prompt_id*/ '',
);
} else if (anyConfirmationHandled) {
logger.info(
@@ -5,7 +5,6 @@
*/
import { ExtensionsCommand } from './extensions.js';
import { RestoreCommand } from './restore.js';
import type { Command } from './types.js';
class CommandRegistry {
@@ -13,7 +12,6 @@ class CommandRegistry {
constructor() {
this.register(new ExtensionsCommand());
this.register(new RestoreCommand());
}
register(command: Command) {
@@ -6,7 +6,7 @@
import { describe, it, expect, vi } from 'vitest';
import { ExtensionsCommand, ListExtensionsCommand } from './extensions.js';
import type { CommandContext } from './types.js';
import type { Config } from '@google/gemini-cli-core';
const mockListExtensions = vi.hoisted(() => vi.fn());
vi.mock('@google/gemini-cli-core', async (importOriginal) => {
@@ -42,14 +42,14 @@ describe('ExtensionsCommand', () => {
it('should default to listing extensions', async () => {
const command = new ExtensionsCommand();
const mockConfig = { config: {} } as CommandContext;
const mockConfig = {} as Config;
const mockExtensions = [{ name: 'ext1' }];
mockListExtensions.mockReturnValue(mockExtensions);
const result = await command.execute(mockConfig, []);
expect(result).toEqual({ name: 'extensions list', data: mockExtensions });
expect(mockListExtensions).toHaveBeenCalledWith(mockConfig.config);
expect(mockListExtensions).toHaveBeenCalledWith(mockConfig);
});
});
@@ -61,19 +61,19 @@ describe('ListExtensionsCommand', () => {
it('should call listExtensions with the provided config', async () => {
const command = new ListExtensionsCommand();
const mockConfig = { config: {} } as CommandContext;
const mockConfig = {} as Config;
const mockExtensions = [{ name: 'ext1' }];
mockListExtensions.mockReturnValue(mockExtensions);
const result = await command.execute(mockConfig, []);
expect(result).toEqual({ name: 'extensions list', data: mockExtensions });
expect(mockListExtensions).toHaveBeenCalledWith(mockConfig.config);
expect(mockListExtensions).toHaveBeenCalledWith(mockConfig);
});
it('should return a message when no extensions are installed', async () => {
const command = new ListExtensionsCommand();
const mockConfig = { config: {} } as CommandContext;
const mockConfig = {} as Config;
mockListExtensions.mockReturnValue([]);
const result = await command.execute(mockConfig, []);
@@ -82,6 +82,6 @@ describe('ListExtensionsCommand', () => {
name: 'extensions list',
data: 'No extensions installed.',
});
expect(mockListExtensions).toHaveBeenCalledWith(mockConfig.config);
expect(mockListExtensions).toHaveBeenCalledWith(mockConfig);
});
});
+6 -10
View File
@@ -4,12 +4,8 @@
* SPDX-License-Identifier: Apache-2.0
*/
import { listExtensions } from '@google/gemini-cli-core';
import type {
Command,
CommandContext,
CommandExecutionResponse,
} from './types.js';
import { listExtensions, type Config } from '@google/gemini-cli-core';
import type { Command, CommandExecutionResponse } from './types.js';
export class ExtensionsCommand implements Command {
readonly name = 'extensions';
@@ -18,10 +14,10 @@ export class ExtensionsCommand implements Command {
readonly topLevel = true;
async execute(
context: CommandContext,
config: Config,
_: string[],
): Promise<CommandExecutionResponse> {
return new ListExtensionsCommand().execute(context, _);
return new ListExtensionsCommand().execute(config, _);
}
}
@@ -30,10 +26,10 @@ export class ListExtensionsCommand implements Command {
readonly description = 'Lists all installed extensions.';
async execute(
context: CommandContext,
config: Config,
_: string[],
): Promise<CommandExecutionResponse> {
const extensions = listExtensions(context.config);
const extensions = listExtensions(config);
const data = extensions.length ? extensions : 'No extensions installed.';
return { name: this.name, data };
@@ -1,137 +0,0 @@
/**
* @license
* Copyright 2025 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { RestoreCommand, ListCheckpointsCommand } from './restore.js';
import type { CommandContext } from './types.js';
import type { Config } from '@google/gemini-cli-core';
import { createMockConfig } from '../utils/testing_utils.js';
beforeEach(() => {
vi.clearAllMocks();
});
const mockPerformRestore = vi.hoisted(() => vi.fn());
const mockLoggerInfo = vi.hoisted(() => vi.fn());
const mockGetCheckpointInfoList = vi.hoisted(() => vi.fn());
vi.mock('@google/gemini-cli-core', async (importOriginal) => {
const original =
await importOriginal<typeof import('@google/gemini-cli-core')>();
return {
...original,
performRestore: mockPerformRestore,
getCheckpointInfoList: mockGetCheckpointInfoList,
};
});
const mockFs = vi.hoisted(() => ({
readFile: vi.fn(),
readdir: vi.fn(),
mkdir: vi.fn(),
}));
vi.mock('node:fs/promises', () => mockFs);
vi.mock('../utils/logger.js', () => ({
logger: {
info: mockLoggerInfo,
},
}));
describe('RestoreCommand', () => {
const mockConfig = {
config: createMockConfig() as Config,
git: {},
} as CommandContext;
it('should return error if no checkpoint name is provided', async () => {
const command = new RestoreCommand();
const result = await command.execute(mockConfig, []);
expect(result.data).toEqual({
type: 'message',
messageType: 'error',
content: 'Please provide a checkpoint name to restore.',
});
});
it('should restore a checkpoint when a valid file is provided', async () => {
const command = new RestoreCommand();
const toolCallData = {
toolCall: {
name: 'test-tool',
args: {},
},
history: [],
clientHistory: [],
commitHash: '123',
};
mockFs.readFile.mockResolvedValue(JSON.stringify(toolCallData));
const restoreContent = {
type: 'message',
messageType: 'info',
content: 'Restored',
};
mockPerformRestore.mockReturnValue(
(async function* () {
yield restoreContent;
})(),
);
const result = await command.execute(mockConfig, ['checkpoint1.json']);
expect(result.data).toEqual([restoreContent]);
});
it('should show "file not found" error for a non-existent checkpoint', async () => {
const command = new RestoreCommand();
const error = new Error('File not found');
(error as NodeJS.ErrnoException).code = 'ENOENT';
mockFs.readFile.mockRejectedValue(error);
const result = await command.execute(mockConfig, ['checkpoint2.json']);
expect(result.data).toEqual({
type: 'message',
messageType: 'error',
content: 'File not found: checkpoint2.json',
});
});
it('should handle invalid JSON in checkpoint file', async () => {
const command = new RestoreCommand();
mockFs.readFile.mockResolvedValue('invalid json');
const result = await command.execute(mockConfig, ['checkpoint1.json']);
expect((result.data as { content: string }).content).toContain(
'An unexpected error occurred during restore.',
);
});
});
describe('ListCheckpointsCommand', () => {
const mockConfig = {
config: createMockConfig() as Config,
} as CommandContext;
it('should list all available checkpoints', async () => {
const command = new ListCheckpointsCommand();
const checkpointInfo = [{ file: 'checkpoint1.json', description: 'Test' }];
mockFs.readdir.mockResolvedValue(['checkpoint1.json']);
mockFs.readFile.mockResolvedValue(
JSON.stringify({ toolCall: { name: 'Test', args: {} } }),
);
mockGetCheckpointInfoList.mockReturnValue(checkpointInfo);
const result = await command.execute(mockConfig);
expect((result.data as { content: string }).content).toEqual(
JSON.stringify(checkpointInfo),
);
});
it('should handle errors when listing checkpoints', async () => {
const command = new ListCheckpointsCommand();
mockFs.readdir.mockRejectedValue(new Error('Read error'));
const result = await command.execute(mockConfig);
expect((result.data as { content: string }).content).toContain(
'An unexpected error occurred while listing checkpoints.',
);
});
});
-155
View File
@@ -1,155 +0,0 @@
/**
* @license
* Copyright 2025 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import {
getCheckpointInfoList,
getToolCallDataSchema,
isNodeError,
performRestore,
} from '@google/gemini-cli-core';
import * as fs from 'node:fs/promises';
import * as path from 'node:path';
import type {
Command,
CommandContext,
CommandExecutionResponse,
} from './types.js';
export class RestoreCommand implements Command {
readonly name = 'restore';
readonly description =
'Restore to a previous checkpoint, or list available checkpoints to restore. This will reset the conversation and file history to the state it was in when the checkpoint was created';
readonly topLevel = true;
readonly requiresWorkspace = true;
readonly subCommands = [new ListCheckpointsCommand()];
async execute(
context: CommandContext,
args: string[],
): Promise<CommandExecutionResponse> {
const { config, git: gitService } = context;
const argsStr = args.join(' ');
try {
if (!argsStr) {
return {
name: this.name,
data: {
type: 'message',
messageType: 'error',
content: 'Please provide a checkpoint name to restore.',
},
};
}
const selectedFile = argsStr.endsWith('.json')
? argsStr
: `${argsStr}.json`;
const checkpointDir = config.storage.getProjectTempCheckpointsDir();
const filePath = path.join(checkpointDir, selectedFile);
let data: string;
try {
data = await fs.readFile(filePath, 'utf-8');
} catch (error) {
if (isNodeError(error) && error.code === 'ENOENT') {
return {
name: this.name,
data: {
type: 'message',
messageType: 'error',
content: `File not found: ${selectedFile}`,
},
};
}
throw error;
}
const toolCallData = JSON.parse(data);
const ToolCallDataSchema = getToolCallDataSchema();
const parseResult = ToolCallDataSchema.safeParse(toolCallData);
if (!parseResult.success) {
return {
name: this.name,
data: {
type: 'message',
messageType: 'error',
content: 'Checkpoint file is invalid or corrupted.',
},
};
}
const restoreResultGenerator = performRestore(
parseResult.data,
gitService,
);
const restoreResult = [];
for await (const result of restoreResultGenerator) {
restoreResult.push(result);
}
return {
name: this.name,
data: restoreResult,
};
} catch (_error) {
return {
name: this.name,
data: {
type: 'message',
messageType: 'error',
content: 'An unexpected error occurred during restore.',
},
};
}
}
}
export class ListCheckpointsCommand implements Command {
readonly name = 'restore list';
readonly description = 'Lists all available checkpoints.';
readonly topLevel = false;
async execute(context: CommandContext): Promise<CommandExecutionResponse> {
const { config } = context;
try {
const checkpointDir = config.storage.getProjectTempCheckpointsDir();
await fs.mkdir(checkpointDir, { recursive: true });
const files = await fs.readdir(checkpointDir);
const jsonFiles = files.filter((file) => file.endsWith('.json'));
const checkpointFiles = new Map<string, string>();
for (const file of jsonFiles) {
const filePath = path.join(checkpointDir, file);
const data = await fs.readFile(filePath, 'utf-8');
checkpointFiles.set(file, data);
}
const checkpointInfoList = getCheckpointInfoList(checkpointFiles);
return {
name: this.name,
data: {
type: 'message',
messageType: 'info',
content: JSON.stringify(checkpointInfoList),
},
};
} catch (_error) {
return {
name: this.name,
data: {
type: 'message',
messageType: 'error',
content: 'An unexpected error occurred while listing checkpoints.',
},
};
}
}
}
+2 -11
View File
@@ -4,12 +4,7 @@
* SPDX-License-Identifier: Apache-2.0
*/
import type { Config, GitService } from '@google/gemini-cli-core';
export interface CommandContext {
config: Config;
git?: GitService;
}
import type { Config } from '@google/gemini-cli-core';
export interface CommandArgument {
readonly name: string;
@@ -23,12 +18,8 @@ export interface Command {
readonly arguments?: CommandArgument[];
readonly subCommands?: Command[];
readonly topLevel?: boolean;
readonly requiresWorkspace?: boolean;
execute(
config: CommandContext,
args: string[],
): Promise<CommandExecutionResponse>;
execute(config: Config, args: string[]): Promise<CommandExecutionResponse>;
}
export interface CommandExecutionResponse {
+1 -8
View File
@@ -22,7 +22,6 @@ import {
DEFAULT_GEMINI_MODEL,
type ExtensionLoader,
startupProfiler,
PREVIEW_GEMINI_MODEL,
} from '@google/gemini-cli-core';
import { logger } from '../utils/logger.js';
@@ -39,9 +38,7 @@ export async function loadConfig(
const configParams: ConfigParameters = {
sessionId: taskId,
model: settings.general?.previewFeatures
? PREVIEW_GEMINI_MODEL
: DEFAULT_GEMINI_MODEL,
model: DEFAULT_GEMINI_MODEL,
embeddingModel: DEFAULT_GEMINI_EMBEDDING_MODEL,
sandbox: undefined, // Sandbox might not be relevant for a server-side agent
targetDir: workspaceDir, // Or a specific directory the agent operates on
@@ -74,10 +71,6 @@ export async function loadConfig(
ideMode: false,
folderTrust: settings.folderTrust === true,
extensionLoader,
checkpointing: process.env['CHECKPOINTING']
? process.env['CHECKPOINTING'] === 'true'
: settings.checkpointing?.enabled,
previewFeatures: settings.general?.previewFeatures,
};
const fileService = new FileDiscoveryService(workspaceDir);
@@ -1,197 +0,0 @@
/**
* @license
* Copyright 2025 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
import * as fs from 'node:fs';
import * as path from 'node:path';
import * as os from 'node:os';
import { loadSettings, USER_SETTINGS_PATH } from './settings.js';
const mocks = vi.hoisted(() => {
const suffix = Math.random().toString(36).slice(2);
return {
suffix,
};
});
vi.mock('node:os', async (importOriginal) => {
const actual = await importOriginal<typeof import('node:os')>();
const path = await import('node:path');
return {
...actual,
homedir: () => path.join(actual.tmpdir(), `gemini-home-${mocks.suffix}`),
};
});
vi.mock('@google/gemini-cli-core', () => ({
GEMINI_DIR: '.gemini',
debugLogger: {
error: vi.fn(),
},
getErrorMessage: (error: unknown) => String(error),
}));
describe('loadSettings', () => {
const mockHomeDir = path.join(os.tmpdir(), `gemini-home-${mocks.suffix}`);
const mockWorkspaceDir = path.join(
os.tmpdir(),
`gemini-workspace-${mocks.suffix}`,
);
const mockGeminiHomeDir = path.join(mockHomeDir, '.gemini');
const mockGeminiWorkspaceDir = path.join(mockWorkspaceDir, '.gemini');
beforeEach(() => {
vi.clearAllMocks();
// Create the directories using the real fs
if (!fs.existsSync(mockGeminiHomeDir)) {
fs.mkdirSync(mockGeminiHomeDir, { recursive: true });
}
if (!fs.existsSync(mockGeminiWorkspaceDir)) {
fs.mkdirSync(mockGeminiWorkspaceDir, { recursive: true });
}
// Clean up settings files before each test
if (fs.existsSync(USER_SETTINGS_PATH)) {
fs.rmSync(USER_SETTINGS_PATH);
}
const workspaceSettingsPath = path.join(
mockGeminiWorkspaceDir,
'settings.json',
);
if (fs.existsSync(workspaceSettingsPath)) {
fs.rmSync(workspaceSettingsPath);
}
});
afterEach(() => {
try {
if (fs.existsSync(mockHomeDir)) {
fs.rmSync(mockHomeDir, { recursive: true, force: true });
}
if (fs.existsSync(mockWorkspaceDir)) {
fs.rmSync(mockWorkspaceDir, { recursive: true, force: true });
}
} catch (e) {
console.error('Failed to cleanup temp dirs', e);
}
vi.restoreAllMocks();
});
it('should load nested previewFeatures from user settings', () => {
const settings = {
general: {
previewFeatures: true,
},
};
fs.writeFileSync(USER_SETTINGS_PATH, JSON.stringify(settings));
const result = loadSettings(mockWorkspaceDir);
expect(result.general?.previewFeatures).toBe(true);
});
it('should load nested previewFeatures from workspace settings', () => {
const settings = {
general: {
previewFeatures: true,
},
};
const workspaceSettingsPath = path.join(
mockGeminiWorkspaceDir,
'settings.json',
);
fs.writeFileSync(workspaceSettingsPath, JSON.stringify(settings));
const result = loadSettings(mockWorkspaceDir);
expect(result.general?.previewFeatures).toBe(true);
});
it('should prioritize workspace settings over user settings', () => {
const userSettings = {
general: {
previewFeatures: false,
},
};
fs.writeFileSync(USER_SETTINGS_PATH, JSON.stringify(userSettings));
const workspaceSettings = {
general: {
previewFeatures: true,
},
};
const workspaceSettingsPath = path.join(
mockGeminiWorkspaceDir,
'settings.json',
);
fs.writeFileSync(workspaceSettingsPath, JSON.stringify(workspaceSettings));
const result = loadSettings(mockWorkspaceDir);
expect(result.general?.previewFeatures).toBe(true);
});
it('should handle missing previewFeatures', () => {
const settings = {
general: {},
};
fs.writeFileSync(USER_SETTINGS_PATH, JSON.stringify(settings));
const result = loadSettings(mockWorkspaceDir);
expect(result.general?.previewFeatures).toBeUndefined();
});
it('should load other top-level settings correctly', () => {
const settings = {
showMemoryUsage: true,
coreTools: ['tool1', 'tool2'],
mcpServers: {
server1: {
command: 'cmd',
args: ['arg'],
},
},
fileFiltering: {
respectGitIgnore: true,
},
};
fs.writeFileSync(USER_SETTINGS_PATH, JSON.stringify(settings));
const result = loadSettings(mockWorkspaceDir);
expect(result.showMemoryUsage).toBe(true);
expect(result.coreTools).toEqual(['tool1', 'tool2']);
expect(result.mcpServers).toHaveProperty('server1');
expect(result.fileFiltering?.respectGitIgnore).toBe(true);
});
it('should overwrite top-level settings from workspace (shallow merge)', () => {
const userSettings = {
showMemoryUsage: false,
fileFiltering: {
respectGitIgnore: true,
enableRecursiveFileSearch: true,
},
};
fs.writeFileSync(USER_SETTINGS_PATH, JSON.stringify(userSettings));
const workspaceSettings = {
showMemoryUsage: true,
fileFiltering: {
respectGitIgnore: false,
},
};
const workspaceSettingsPath = path.join(
mockGeminiWorkspaceDir,
'settings.json',
);
fs.writeFileSync(workspaceSettingsPath, JSON.stringify(workspaceSettings));
const result = loadSettings(mockWorkspaceDir);
// Primitive value overwritten
expect(result.showMemoryUsage).toBe(true);
// Object value completely replaced (shallow merge behavior)
expect(result.fileFiltering?.respectGitIgnore).toBe(false);
expect(result.fileFiltering?.enableRecursiveFileSearch).toBeUndefined();
});
});
+1 -6
View File
@@ -20,9 +20,7 @@ import stripJsonComments from 'strip-json-comments';
export const USER_SETTINGS_DIR = path.join(homedir(), GEMINI_DIR);
export const USER_SETTINGS_PATH = path.join(USER_SETTINGS_DIR, 'settings.json');
// TODO: Ensure full compatibility with V2 nested settings structure (settings.schema.json).
// This involves updating the interface and implementing migration logic to support legacy V1 (flat) settings,
// similar to how packages/cli/src/config/settings.ts handles it.
// Reconcile with https://github.com/google-gemini/gemini-cli/blob/b09bc6656080d4d12e1d06734aae2ec33af5c1ed/packages/cli/src/config/settings.ts#L53
export interface Settings {
mcpServers?: Record<string, MCPServerConfig>;
coreTools?: string[];
@@ -31,9 +29,6 @@ export interface Settings {
showMemoryUsage?: boolean;
checkpointing?: CheckpointingSettings;
folderTrust?: boolean;
general?: {
previewFeatures?: boolean;
};
// Git-aware file filtering settings
fileFiltering?: {
+1 -76
View File
@@ -36,7 +36,7 @@ import {
createMockConfig,
} from '../utils/testing_utils.js';
import { MockTool } from '@google/gemini-cli-core';
import type { Command, CommandContext } from '../commands/types.js';
import type { Command } from '../commands/types.js';
const mockToolConfirmationFn = async () =>
({}) as unknown as ToolCallConfirmationDetails;
@@ -97,7 +97,6 @@ vi.mock('@google/gemini-cli-core', async () => {
getUserTier: vi.fn().mockReturnValue('free'),
initialize: vi.fn(),
})),
performRestore: vi.fn(),
};
});
@@ -940,17 +939,6 @@ describe('E2E Tests', () => {
});
it('should return extensions for valid command', async () => {
const mockExtensionsCommand = {
name: 'extensions list',
description: 'a mock command',
execute: vi.fn(async (context: CommandContext) => {
// Simulate the actual command's behavior
const extensions = context.config.getExtensions();
return { name: 'extensions list', data: extensions };
}),
};
vi.spyOn(commandRegistry, 'get').mockReturnValue(mockExtensionsCommand);
const agent = request.agent(app);
const res = await agent
.post('/executeCommand')
@@ -966,8 +954,6 @@ describe('E2E Tests', () => {
});
it('should return 404 for invalid command', async () => {
vi.spyOn(commandRegistry, 'get').mockReturnValue(undefined);
const agent = request.agent(app);
const res = await agent
.post('/executeCommand')
@@ -1000,66 +986,5 @@ describe('E2E Tests', () => {
expect(res.body.error).toBe('"args" field must be an array.');
expect(getExtensionsSpy).not.toHaveBeenCalled();
});
it('should execute a command that does not require a workspace when CODER_AGENT_WORKSPACE_PATH is not set', async () => {
const mockCommand = {
name: 'test-command',
description: 'a mock command',
execute: vi
.fn()
.mockResolvedValue({ name: 'test-command', data: 'success' }),
};
vi.spyOn(commandRegistry, 'get').mockReturnValue(mockCommand);
delete process.env['CODER_AGENT_WORKSPACE_PATH'];
const response = await request(app)
.post('/executeCommand')
.send({ command: 'test-command', args: [] });
expect(response.status).toBe(200);
expect(response.body.data).toBe('success');
});
it('should return 400 for a command that requires a workspace when CODER_AGENT_WORKSPACE_PATH is not set', async () => {
const mockWorkspaceCommand = {
name: 'workspace-command',
description: 'A command that requires a workspace',
requiresWorkspace: true,
execute: vi
.fn()
.mockResolvedValue({ name: 'workspace-command', data: 'success' }),
};
vi.spyOn(commandRegistry, 'get').mockReturnValue(mockWorkspaceCommand);
delete process.env['CODER_AGENT_WORKSPACE_PATH'];
const response = await request(app)
.post('/executeCommand')
.send({ command: 'workspace-command', args: [] });
expect(response.status).toBe(400);
expect(response.body.error).toBe(
'Command "workspace-command" requires a workspace, but CODER_AGENT_WORKSPACE_PATH is not set.',
);
});
it('should execute a command that requires a workspace when CODER_AGENT_WORKSPACE_PATH is set', async () => {
const mockWorkspaceCommand = {
name: 'workspace-command',
description: 'A command that requires a workspace',
requiresWorkspace: true,
execute: vi
.fn()
.mockResolvedValue({ name: 'workspace-command', data: 'success' }),
};
vi.spyOn(commandRegistry, 'get').mockReturnValue(mockWorkspaceCommand);
process.env['CODER_AGENT_WORKSPACE_PATH'] = '/tmp/test-workspace';
const response = await request(app)
.post('/executeCommand')
.send({ command: 'workspace-command', args: [] });
expect(response.status).toBe(200);
expect(response.body.data).toBe('success');
});
});
});
+1 -20
View File
@@ -22,7 +22,6 @@ import { loadExtensions } from '../config/extension.js';
import { commandRegistry } from '../commands/command-registry.js';
import { SimpleExtensionLoader } from '@google/gemini-cli-core';
import type { Command, CommandArgument } from '../commands/types.js';
import { GitService } from '@google/gemini-cli-core';
type CommandResponse = {
name: string;
@@ -86,14 +85,6 @@ export async function createApp() {
'a2a-server',
);
let git: GitService | undefined;
if (config.getCheckpointingEnabled()) {
git = new GitService(config.getTargetDir(), config.storage);
await git.initialize();
}
const context = { config, git };
// loadEnvironment() is called within getConfig now
const bucketName = process.env['GCS_BUCKET_NAME'];
let taskStoreForExecutor: TaskStore;
@@ -153,7 +144,6 @@ export async function createApp() {
});
expressApp.post('/executeCommand', async (req, res) => {
logger.info('[CoreAgent] Received /executeCommand request: ', req.body);
try {
const { command, args } = req.body;
@@ -169,22 +159,13 @@ export async function createApp() {
const commandToExecute = commandRegistry.get(command);
if (commandToExecute?.requiresWorkspace) {
if (!process.env['CODER_AGENT_WORKSPACE_PATH']) {
return res.status(400).json({
error: `Command "${command}" requires a workspace, but CODER_AGENT_WORKSPACE_PATH is not set.`,
});
}
}
if (!commandToExecute) {
return res
.status(404)
.json({ error: `Command not found: ${command}` });
}
const result = await commandToExecute.execute(context, args ?? []);
logger.info('[CoreAgent] Sending /executeCommand response: ', result);
const result = await commandToExecute.execute(config, args ?? []);
return res.status(200).json(result);
} catch (e) {
logger.error('Error executing /executeCommand:', e);
@@ -37,10 +37,8 @@ export function createMockConfig(
isPathWithinWorkspace: () => true,
}),
getTargetDir: () => '/test',
getCheckpointingEnabled: vi.fn().mockReturnValue(false),
storage: {
getProjectTempDir: () => '/tmp',
getProjectTempCheckpointsDir: () => '/tmp/checkpoints',
} as Storage,
getTruncateToolOutputThreshold: () =>
DEFAULT_TRUNCATE_TOOL_OUTPUT_THRESHOLD,
@@ -64,7 +62,6 @@ export function createMockConfig(
getMcpClientManager: vi.fn().mockReturnValue({
getMcpServers: vi.fn().mockReturnValue({}),
}),
getGitService: vi.fn(),
...overrides,
} as unknown as Config;
mockConfig.getMessageBus = vi.fn().mockReturnValue(createMockMessageBus());
+2 -2
View File
@@ -1,6 +1,6 @@
{
"name": "@google/gemini-cli",
"version": "0.21.1",
"version": "0.20.2",
"description": "Gemini CLI",
"repository": {
"type": "git",
@@ -25,7 +25,7 @@
"dist"
],
"config": {
"sandboxImageUri": "us-docker.pkg.dev/gemini-code-dev/gemini-cli/sandbox:0.21.1"
"sandboxImageUri": "us-docker.pkg.dev/gemini-code-dev/gemini-cli/sandbox:0.20.2"
},
"dependencies": {
"@google/gemini-cli-core": "file:../core",
-2
View File
@@ -14,7 +14,6 @@ import { enableCommand } from './extensions/enable.js';
import { linkCommand } from './extensions/link.js';
import { newCommand } from './extensions/new.js';
import { validateCommand } from './extensions/validate.js';
import { settingsCommand } from './extensions/settings.js';
import { initializeOutputListenersAndFlush } from '../gemini.js';
export const extensionsCommand: CommandModule = {
@@ -33,7 +32,6 @@ export const extensionsCommand: CommandModule = {
.command(linkCommand)
.command(newCommand)
.command(validateCommand)
.command(settingsCommand)
.demandCommand(1, 'You need at least one command before continuing.')
.version(false),
handler: () => {
@@ -1,158 +0,0 @@
/**
* @license
* Copyright 2025 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import type { CommandModule } from 'yargs';
import {
updateSetting,
promptForSetting,
ExtensionSettingScope,
getScopedEnvContents,
} from '../../config/extensions/extensionSettings.js';
import { getExtensionAndManager } from './utils.js';
import { debugLogger } from '@google/gemini-cli-core';
import { exitCli } from '../utils.js';
// --- SET COMMAND ---
interface SetArgs {
name: string;
setting: string;
scope: string;
}
const setCommand: CommandModule<object, SetArgs> = {
command: 'set [--scope] <name> <setting>',
describe: 'Set a specific setting for an extension.',
builder: (yargs) =>
yargs
.positional('name', {
describe: 'Name of the extension to configure.',
type: 'string',
demandOption: true,
})
.positional('setting', {
describe: 'The setting to configure (name or env var).',
type: 'string',
demandOption: true,
})
.option('scope', {
describe: 'The scope to set the setting in.',
type: 'string',
choices: ['user', 'workspace'],
default: 'user',
}),
handler: async (args) => {
const { name, setting, scope } = args;
const { extension, extensionManager } = await getExtensionAndManager(name);
if (!extension || !extensionManager) {
return;
}
const extensionConfig = extensionManager.loadExtensionConfig(
extension.path,
);
if (!extensionConfig) {
debugLogger.error(
`Could not find configuration for extension "${name}".`,
);
return;
}
await updateSetting(
extensionConfig,
extension.id,
setting,
promptForSetting,
scope as ExtensionSettingScope,
);
await exitCli();
},
};
// --- LIST COMMAND ---
interface ListArgs {
name: string;
}
const listCommand: CommandModule<object, ListArgs> = {
command: 'list <name>',
describe: 'List all settings for an extension.',
builder: (yargs) =>
yargs.positional('name', {
describe: 'Name of the extension.',
type: 'string',
demandOption: true,
}),
handler: async (args) => {
const { name } = args;
const { extension, extensionManager } = await getExtensionAndManager(name);
if (!extension || !extensionManager) {
return;
}
const extensionConfig = extensionManager.loadExtensionConfig(
extension.path,
);
if (
!extensionConfig ||
!extensionConfig.settings ||
extensionConfig.settings.length === 0
) {
debugLogger.log(`Extension "${name}" has no settings to configure.`);
return;
}
const userSettings = await getScopedEnvContents(
extensionConfig,
extension.id,
ExtensionSettingScope.USER,
);
const workspaceSettings = await getScopedEnvContents(
extensionConfig,
extension.id,
ExtensionSettingScope.WORKSPACE,
);
const mergedSettings = { ...userSettings, ...workspaceSettings };
debugLogger.log(`Settings for "${name}":`);
for (const setting of extensionConfig.settings) {
const value = mergedSettings[setting.envVar];
let displayValue: string;
let scopeInfo = '';
if (workspaceSettings[setting.envVar] !== undefined) {
scopeInfo = ' (workspace)';
} else if (userSettings[setting.envVar] !== undefined) {
scopeInfo = ' (user)';
}
if (value === undefined) {
displayValue = '[not set]';
} else if (setting.sensitive) {
displayValue = '[value stored in keychain]';
} else {
displayValue = value;
}
debugLogger.log(`
- ${setting.name} (${setting.envVar})`);
debugLogger.log(` Description: ${setting.description}`);
debugLogger.log(` Value: ${displayValue}${scopeInfo}`);
}
await exitCli();
},
};
// --- SETTINGS COMMAND ---
export const settingsCommand: CommandModule = {
command: 'settings <command>',
describe: 'Manage extension settings.',
builder: (yargs) =>
yargs
.command(setCommand)
.command(listCommand)
.demandCommand(1, 'You need to specify a command (set or list).')
.version(false),
handler: () => {
// This handler is not called when a subcommand is provided.
// Yargs will show the help menu.
},
};
@@ -1,32 +0,0 @@
/**
* @license
* Copyright 2025 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import { ExtensionManager } from '../../config/extension-manager.js';
import { promptForSetting } from '../../config/extensions/extensionSettings.js';
import { loadSettings } from '../../config/settings.js';
import { requestConsentNonInteractive } from '../../config/extensions/consent.js';
import { debugLogger } from '@google/gemini-cli-core';
export async function getExtensionAndManager(name: string) {
const workspaceDir = process.cwd();
const extensionManager = new ExtensionManager({
workspaceDir,
requestConsent: requestConsentNonInteractive,
requestSetting: promptForSetting,
settings: loadSettings(workspaceDir).merged,
});
await extensionManager.loadExtensions();
const extension = extensionManager
.getExtensions()
.find((ext) => ext.name === name);
if (!extension) {
debugLogger.error(`Extension "${name}" is not installed.`);
return { extension: null, extensionManager: null };
}
return { extension, extensionManager };
}
-25
View File
@@ -1,25 +0,0 @@
/**
* @license
* Copyright 2025 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import type { CommandModule } from 'yargs';
import { migrateCommand } from './hooks/migrate.js';
import { initializeOutputListenersAndFlush } from '../gemini.js';
export const hooksCommand: CommandModule = {
command: 'hooks <command>',
aliases: ['hook'],
describe: 'Manage Gemini CLI hooks.',
builder: (yargs) =>
yargs
.middleware(() => initializeOutputListenersAndFlush())
.command(migrateCommand)
.demandCommand(1, 'You need at least one command before continuing.')
.version(false),
handler: () => {
// This handler is not called when a subcommand is provided.
// Yargs will show the help menu.
},
};
@@ -1,518 +0,0 @@
/**
* @license
* Copyright 2025 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import {
describe,
it,
expect,
vi,
beforeEach,
afterEach,
type Mock,
type MockInstance,
} from 'vitest';
import * as fs from 'node:fs';
import { loadSettings, SettingScope } from '../../config/settings.js';
import { debugLogger } from '@google/gemini-cli-core';
import { handleMigrateFromClaude } from './migrate.js';
vi.mock('node:fs');
vi.mock('../utils.js', () => ({
exitCli: vi.fn(),
}));
vi.mock('../../config/settings.js', async () => {
const actual = await vi.importActual('../../config/settings.js');
return {
...actual,
loadSettings: vi.fn(),
};
});
const mockedLoadSettings = loadSettings as Mock;
const mockedFs = vi.mocked(fs);
describe('migrate command', () => {
let mockSetValue: Mock;
let debugLoggerLogSpy: MockInstance;
let debugLoggerErrorSpy: MockInstance;
let originalCwd: () => string;
beforeEach(() => {
vi.resetAllMocks();
mockSetValue = vi.fn();
debugLoggerLogSpy = vi
.spyOn(debugLogger, 'log')
.mockImplementation(() => {});
debugLoggerErrorSpy = vi
.spyOn(debugLogger, 'error')
.mockImplementation(() => {});
// Mock process.cwd()
originalCwd = process.cwd;
process.cwd = vi.fn(() => '/test/project');
mockedLoadSettings.mockReturnValue({
merged: {
hooks: {},
},
setValue: mockSetValue,
workspace: { path: '/test/project/.gemini' },
});
});
afterEach(() => {
process.cwd = originalCwd;
vi.restoreAllMocks();
});
it('should log error when no Claude settings files exist', async () => {
mockedFs.existsSync.mockReturnValue(false);
await handleMigrateFromClaude();
expect(debugLoggerErrorSpy).toHaveBeenCalledWith(
'No Claude Code settings found in .claude directory. Expected settings.json or settings.local.json',
);
expect(mockSetValue).not.toHaveBeenCalled();
});
it('should migrate hooks from settings.json when it exists', async () => {
const claudeSettings = {
hooks: {
PreToolUse: [
{
matcher: 'Edit',
hooks: [
{
type: 'command',
command: 'echo "Before Edit"',
timeout: 30,
},
],
},
],
},
};
mockedFs.existsSync.mockImplementation((path) =>
path.toString().endsWith('settings.json'),
);
mockedFs.readFileSync.mockReturnValue(JSON.stringify(claudeSettings));
await handleMigrateFromClaude();
expect(mockSetValue).toHaveBeenCalledWith(
SettingScope.Workspace,
'hooks',
expect.objectContaining({
BeforeTool: expect.arrayContaining([
expect.objectContaining({
matcher: 'replace',
hooks: expect.arrayContaining([
expect.objectContaining({
command: 'echo "Before Edit"',
type: 'command',
timeout: 30,
}),
]),
}),
]),
}),
);
expect(debugLoggerLogSpy).toHaveBeenCalledWith(
expect.stringContaining('Found Claude Code settings'),
);
expect(debugLoggerLogSpy).toHaveBeenCalledWith(
expect.stringContaining('Migrating 1 hook event'),
);
expect(debugLoggerLogSpy).toHaveBeenCalledWith(
'✓ Hooks successfully migrated to .gemini/settings.json',
);
});
it('should prefer settings.local.json over settings.json', async () => {
const localSettings = {
hooks: {
SessionStart: [
{
hooks: [
{
type: 'command',
command: 'echo "Local session start"',
},
],
},
],
},
};
mockedFs.existsSync.mockReturnValue(true);
mockedFs.readFileSync.mockReturnValue(JSON.stringify(localSettings));
await handleMigrateFromClaude();
expect(mockedFs.readFileSync).toHaveBeenCalledWith(
expect.stringContaining('settings.local.json'),
'utf-8',
);
expect(mockSetValue).toHaveBeenCalledWith(
SettingScope.Workspace,
'hooks',
expect.objectContaining({
SessionStart: expect.any(Array),
}),
);
});
it('should migrate all supported event types', async () => {
const claudeSettings = {
hooks: {
PreToolUse: [{ hooks: [{ type: 'command', command: 'echo 1' }] }],
PostToolUse: [{ hooks: [{ type: 'command', command: 'echo 2' }] }],
UserPromptSubmit: [{ hooks: [{ type: 'command', command: 'echo 3' }] }],
Stop: [{ hooks: [{ type: 'command', command: 'echo 4' }] }],
SubAgentStop: [{ hooks: [{ type: 'command', command: 'echo 5' }] }],
SessionStart: [{ hooks: [{ type: 'command', command: 'echo 6' }] }],
SessionEnd: [{ hooks: [{ type: 'command', command: 'echo 7' }] }],
PreCompact: [{ hooks: [{ type: 'command', command: 'echo 8' }] }],
Notification: [{ hooks: [{ type: 'command', command: 'echo 9' }] }],
},
};
mockedFs.existsSync.mockReturnValue(true);
mockedFs.readFileSync.mockReturnValue(JSON.stringify(claudeSettings));
await handleMigrateFromClaude();
const migratedHooks = mockSetValue.mock.calls[0][2];
expect(migratedHooks).toHaveProperty('BeforeTool');
expect(migratedHooks).toHaveProperty('AfterTool');
expect(migratedHooks).toHaveProperty('BeforeAgent');
expect(migratedHooks).toHaveProperty('AfterAgent');
expect(migratedHooks).toHaveProperty('SessionStart');
expect(migratedHooks).toHaveProperty('SessionEnd');
expect(migratedHooks).toHaveProperty('PreCompress');
expect(migratedHooks).toHaveProperty('Notification');
});
it('should transform tool names in matchers', async () => {
const claudeSettings = {
hooks: {
PreToolUse: [
{
matcher: 'Edit|Bash|Read|Write|Glob|Grep',
hooks: [{ type: 'command', command: 'echo "test"' }],
},
],
},
};
mockedFs.existsSync.mockReturnValue(true);
mockedFs.readFileSync.mockReturnValue(JSON.stringify(claudeSettings));
await handleMigrateFromClaude();
const migratedHooks = mockSetValue.mock.calls[0][2];
expect(migratedHooks.BeforeTool[0].matcher).toBe(
'replace|run_shell_command|read_file|write_file|glob|grep',
);
});
it('should replace $CLAUDE_PROJECT_DIR with $GEMINI_PROJECT_DIR', async () => {
const claudeSettings = {
hooks: {
PreToolUse: [
{
hooks: [
{
type: 'command',
command: 'cd $CLAUDE_PROJECT_DIR && ls',
},
],
},
],
},
};
mockedFs.existsSync.mockReturnValue(true);
mockedFs.readFileSync.mockReturnValue(JSON.stringify(claudeSettings));
await handleMigrateFromClaude();
const migratedHooks = mockSetValue.mock.calls[0][2];
expect(migratedHooks.BeforeTool[0].hooks[0].command).toBe(
'cd $GEMINI_PROJECT_DIR && ls',
);
});
it('should preserve sequential flag', async () => {
const claudeSettings = {
hooks: {
PreToolUse: [
{
sequential: true,
hooks: [{ type: 'command', command: 'echo "test"' }],
},
],
},
};
mockedFs.existsSync.mockReturnValue(true);
mockedFs.readFileSync.mockReturnValue(JSON.stringify(claudeSettings));
await handleMigrateFromClaude();
const migratedHooks = mockSetValue.mock.calls[0][2];
expect(migratedHooks.BeforeTool[0].sequential).toBe(true);
});
it('should preserve timeout values', async () => {
const claudeSettings = {
hooks: {
PreToolUse: [
{
hooks: [
{
type: 'command',
command: 'echo "test"',
timeout: 60,
},
],
},
],
},
};
mockedFs.existsSync.mockReturnValue(true);
mockedFs.readFileSync.mockReturnValue(JSON.stringify(claudeSettings));
await handleMigrateFromClaude();
const migratedHooks = mockSetValue.mock.calls[0][2];
expect(migratedHooks.BeforeTool[0].hooks[0].timeout).toBe(60);
});
it('should merge with existing Gemini hooks', async () => {
const claudeSettings = {
hooks: {
PreToolUse: [
{
hooks: [{ type: 'command', command: 'echo "claude"' }],
},
],
},
};
mockedLoadSettings.mockReturnValue({
merged: {
hooks: {
AfterTool: [
{
hooks: [{ type: 'command', command: 'echo "existing"' }],
},
],
},
},
setValue: mockSetValue,
workspace: { path: '/test/project/.gemini' },
});
mockedFs.existsSync.mockReturnValue(true);
mockedFs.readFileSync.mockReturnValue(JSON.stringify(claudeSettings));
await handleMigrateFromClaude();
const migratedHooks = mockSetValue.mock.calls[0][2];
expect(migratedHooks).toHaveProperty('BeforeTool');
expect(migratedHooks).toHaveProperty('AfterTool');
expect(migratedHooks.AfterTool[0].hooks[0].command).toBe('echo "existing"');
expect(migratedHooks.BeforeTool[0].hooks[0].command).toBe('echo "claude"');
});
it('should handle JSON with comments', async () => {
const claudeSettingsWithComments = `{
// This is a comment
"hooks": {
/* Block comment */
"PreToolUse": [
{
"hooks": [
{
"type": "command",
"command": "echo test" // Inline comment
}
]
}
]
}
}`;
mockedFs.existsSync.mockReturnValue(true);
mockedFs.readFileSync.mockReturnValue(claudeSettingsWithComments);
await handleMigrateFromClaude();
expect(mockSetValue).toHaveBeenCalledWith(
SettingScope.Workspace,
'hooks',
expect.objectContaining({
BeforeTool: expect.any(Array),
}),
);
});
it('should handle malformed JSON gracefully', async () => {
mockedFs.existsSync.mockReturnValue(true);
mockedFs.readFileSync.mockReturnValue('{ invalid json }');
await handleMigrateFromClaude();
expect(debugLoggerErrorSpy).toHaveBeenCalledWith(
expect.stringContaining('Error reading'),
);
expect(mockSetValue).not.toHaveBeenCalled();
});
it('should log info when no hooks are found in Claude settings', async () => {
const claudeSettings = {
someOtherSetting: 'value',
};
mockedFs.existsSync.mockReturnValue(true);
mockedFs.readFileSync.mockReturnValue(JSON.stringify(claudeSettings));
await handleMigrateFromClaude();
expect(debugLoggerLogSpy).toHaveBeenCalledWith(
'No hooks found in Claude Code settings to migrate.',
);
expect(mockSetValue).not.toHaveBeenCalled();
});
it('should handle setValue errors gracefully', async () => {
const claudeSettings = {
hooks: {
PreToolUse: [
{
hooks: [{ type: 'command', command: 'echo "test"' }],
},
],
},
};
mockedFs.existsSync.mockReturnValue(true);
mockedFs.readFileSync.mockReturnValue(JSON.stringify(claudeSettings));
mockSetValue.mockImplementation(() => {
throw new Error('Failed to save');
});
await handleMigrateFromClaude();
expect(debugLoggerErrorSpy).toHaveBeenCalledWith(
'Error saving migrated hooks: Failed to save',
);
});
it('should handle hooks with matcher but no command', async () => {
const claudeSettings = {
hooks: {
PreToolUse: [
{
matcher: 'Edit',
hooks: [
{
type: 'command',
},
],
},
],
},
};
mockedFs.existsSync.mockReturnValue(true);
mockedFs.readFileSync.mockReturnValue(JSON.stringify(claudeSettings));
await handleMigrateFromClaude();
const migratedHooks = mockSetValue.mock.calls[0][2];
expect(migratedHooks.BeforeTool[0].matcher).toBe('replace');
expect(migratedHooks.BeforeTool[0].hooks[0].type).toBe('command');
});
it('should handle empty hooks array', async () => {
const claudeSettings = {
hooks: {
PreToolUse: [
{
hooks: [],
},
],
},
};
mockedFs.existsSync.mockReturnValue(true);
mockedFs.readFileSync.mockReturnValue(JSON.stringify(claudeSettings));
await handleMigrateFromClaude();
const migratedHooks = mockSetValue.mock.calls[0][2];
expect(migratedHooks.BeforeTool[0].hooks).toEqual([]);
});
it('should handle non-array event config gracefully', async () => {
const claudeSettings = {
hooks: {
PreToolUse: 'not an array',
PostToolUse: [
{
hooks: [{ type: 'command', command: 'echo "test"' }],
},
],
},
};
mockedFs.existsSync.mockReturnValue(true);
mockedFs.readFileSync.mockReturnValue(JSON.stringify(claudeSettings));
await handleMigrateFromClaude();
const migratedHooks = mockSetValue.mock.calls[0][2];
expect(migratedHooks).not.toHaveProperty('BeforeTool');
expect(migratedHooks).toHaveProperty('AfterTool');
});
it('should display migration instructions after successful migration', async () => {
const claudeSettings = {
hooks: {
PreToolUse: [
{
hooks: [{ type: 'command', command: 'echo "test"' }],
},
],
},
};
mockedFs.existsSync.mockReturnValue(true);
mockedFs.readFileSync.mockReturnValue(JSON.stringify(claudeSettings));
await handleMigrateFromClaude();
expect(debugLoggerLogSpy).toHaveBeenCalledWith(
'✓ Hooks successfully migrated to .gemini/settings.json',
);
expect(debugLoggerLogSpy).toHaveBeenCalledWith(
'\nMigration complete! Please review the migrated hooks in .gemini/settings.json',
);
expect(debugLoggerLogSpy).toHaveBeenCalledWith(
'Note: Set tools.enableHooks to true in your settings to enable the hook system.',
);
});
});
-273
View File
@@ -1,273 +0,0 @@
/**
* @license
* Copyright 2025 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import type { CommandModule } from 'yargs';
import * as fs from 'node:fs';
import * as path from 'node:path';
import { debugLogger, getErrorMessage } from '@google/gemini-cli-core';
import { loadSettings, SettingScope } from '../../config/settings.js';
import { exitCli } from '../utils.js';
import stripJsonComments from 'strip-json-comments';
interface MigrateArgs {
fromClaude: boolean;
}
/**
* Mapping from Claude Code event names to Gemini event names
*/
const EVENT_MAPPING: Record<string, string> = {
PreToolUse: 'BeforeTool',
PostToolUse: 'AfterTool',
UserPromptSubmit: 'BeforeAgent',
Stop: 'AfterAgent',
SubAgentStop: 'AfterAgent', // Gemini doesn't have sub-agents, map to AfterAgent
SessionStart: 'SessionStart',
SessionEnd: 'SessionEnd',
PreCompact: 'PreCompress',
Notification: 'Notification',
};
/**
* Mapping from Claude Code tool names to Gemini tool names
*/
const TOOL_NAME_MAPPING: Record<string, string> = {
Edit: 'replace',
Bash: 'run_shell_command',
Read: 'read_file',
Write: 'write_file',
Glob: 'glob',
Grep: 'grep',
LS: 'ls',
};
/**
* Transform a matcher regex to update tool names from Claude to Gemini
*/
function transformMatcher(matcher: string | undefined): string | undefined {
if (!matcher) return matcher;
let transformed = matcher;
for (const [claudeName, geminiName] of Object.entries(TOOL_NAME_MAPPING)) {
// Replace exact matches and matches within regex alternations
transformed = transformed.replace(
new RegExp(`\\b${claudeName}\\b`, 'g'),
geminiName,
);
}
return transformed;
}
/**
* Migrate a Claude Code hook configuration to Gemini format
*/
function migrateClaudeHook(claudeHook: unknown): unknown {
if (!claudeHook || typeof claudeHook !== 'object') {
return claudeHook;
}
const hook = claudeHook as Record<string, unknown>;
const migrated: Record<string, unknown> = {};
// Map command field
if ('command' in hook) {
migrated['command'] = hook['command'];
// Replace CLAUDE_PROJECT_DIR with GEMINI_PROJECT_DIR in command
if (typeof migrated['command'] === 'string') {
migrated['command'] = migrated['command'].replace(
/\$CLAUDE_PROJECT_DIR/g,
'$GEMINI_PROJECT_DIR',
);
}
}
// Map type field
if ('type' in hook && hook['type'] === 'command') {
migrated['type'] = 'command';
}
// Map timeout field (Claude uses seconds, Gemini uses seconds)
if ('timeout' in hook && typeof hook['timeout'] === 'number') {
migrated['timeout'] = hook['timeout'];
}
return migrated;
}
/**
* Migrate Claude Code hooks configuration to Gemini format
*/
function migrateClaudeHooks(claudeConfig: unknown): Record<string, unknown> {
if (!claudeConfig || typeof claudeConfig !== 'object') {
return {};
}
const config = claudeConfig as Record<string, unknown>;
const geminiHooks: Record<string, unknown> = {};
// Check if there's a hooks section
const hooksSection = config['hooks'] as Record<string, unknown> | undefined;
if (!hooksSection || typeof hooksSection !== 'object') {
return {};
}
for (const [eventName, eventConfig] of Object.entries(hooksSection)) {
// Map event name
const geminiEventName = EVENT_MAPPING[eventName] || eventName;
if (!Array.isArray(eventConfig)) {
continue;
}
// Migrate each hook definition
const migratedDefinitions = eventConfig.map((def: unknown) => {
if (!def || typeof def !== 'object') {
return def;
}
const definition = def as Record<string, unknown>;
const migratedDef: Record<string, unknown> = {};
// Transform matcher
if (
'matcher' in definition &&
typeof definition['matcher'] === 'string'
) {
migratedDef['matcher'] = transformMatcher(definition['matcher']);
}
// Copy sequential flag
if ('sequential' in definition) {
migratedDef['sequential'] = definition['sequential'];
}
// Migrate hooks array
if ('hooks' in definition && Array.isArray(definition['hooks'])) {
migratedDef['hooks'] = definition['hooks'].map(migrateClaudeHook);
}
return migratedDef;
});
geminiHooks[geminiEventName] = migratedDefinitions;
}
return geminiHooks;
}
/**
* Handle migration from Claude Code
*/
export async function handleMigrateFromClaude() {
const workingDir = process.cwd();
// Look for Claude settings in .claude directory
const claudeDir = path.join(workingDir, '.claude');
const claudeSettingsPath = path.join(claudeDir, 'settings.json');
const claudeLocalSettingsPath = path.join(claudeDir, 'settings.local.json');
let claudeSettings: Record<string, unknown> | null = null;
let sourceFile = '';
// Try to read settings.local.json first, then settings.json
if (fs.existsSync(claudeLocalSettingsPath)) {
sourceFile = claudeLocalSettingsPath;
try {
const content = fs.readFileSync(claudeLocalSettingsPath, 'utf-8');
claudeSettings = JSON.parse(stripJsonComments(content)) as Record<
string,
unknown
>;
} catch (error) {
debugLogger.error(
`Error reading ${claudeLocalSettingsPath}: ${getErrorMessage(error)}`,
);
}
} else if (fs.existsSync(claudeSettingsPath)) {
sourceFile = claudeSettingsPath;
try {
const content = fs.readFileSync(claudeSettingsPath, 'utf-8');
claudeSettings = JSON.parse(stripJsonComments(content)) as Record<
string,
unknown
>;
} catch (error) {
debugLogger.error(
`Error reading ${claudeSettingsPath}: ${getErrorMessage(error)}`,
);
}
} else {
debugLogger.error(
'No Claude Code settings found in .claude directory. Expected settings.json or settings.local.json',
);
return;
}
if (!claudeSettings) {
return;
}
debugLogger.log(`Found Claude Code settings in: ${sourceFile}`);
// Migrate hooks
const migratedHooks = migrateClaudeHooks(claudeSettings);
if (Object.keys(migratedHooks).length === 0) {
debugLogger.log('No hooks found in Claude Code settings to migrate.');
return;
}
debugLogger.log(
`Migrating ${Object.keys(migratedHooks).length} hook event(s)...`,
);
// Load current Gemini settings
const settings = loadSettings(workingDir);
// Merge migrated hooks with existing hooks
const existingHooks =
(settings.merged.hooks as Record<string, unknown>) || {};
const mergedHooks = { ...existingHooks, ...migratedHooks };
// Update settings (setValue automatically saves)
try {
settings.setValue(SettingScope.Workspace, 'hooks', mergedHooks);
debugLogger.log('✓ Hooks successfully migrated to .gemini/settings.json');
debugLogger.log(
'\nMigration complete! Please review the migrated hooks in .gemini/settings.json',
);
debugLogger.log(
'Note: Set tools.enableHooks to true in your settings to enable the hook system.',
);
} catch (error) {
debugLogger.error(`Error saving migrated hooks: ${getErrorMessage(error)}`);
}
}
export const migrateCommand: CommandModule = {
command: 'migrate',
describe: 'Migrate hooks from Claude Code to Gemini CLI',
builder: (yargs) =>
yargs.option('from-claude', {
describe: 'Migrate from Claude Code hooks',
type: 'boolean',
default: false,
}),
handler: async (argv) => {
const args = argv as unknown as MigrateArgs;
if (args.fromClaude) {
await handleMigrateFromClaude();
} else {
debugLogger.log(
'Usage: gemini hooks migrate --from-claude\n\nMigrate hooks from Claude Code to Gemini CLI format.',
);
}
await exitCli();
},
};
+1 -35
View File
@@ -107,7 +107,6 @@ describe('mcp add command', () => {
expect(mockSetValue).toHaveBeenCalledWith(SettingScope.User, 'mcpServers', {
'sse-server': {
url: 'https://example.com/sse-endpoint',
type: 'sse',
headers: { 'X-API-Key': 'your-key' },
},
});
@@ -123,40 +122,7 @@ describe('mcp add command', () => {
'mcpServers',
{
'http-server': {
url: 'https://example.com/mcp',
type: 'http',
headers: { Authorization: 'Bearer your-token' },
},
},
);
});
it('should add an sse server using --type alias', async () => {
await parser.parseAsync(
'add --type sse --scope user -H "X-API-Key: your-key" sse-server https://example.com/sse',
);
expect(mockSetValue).toHaveBeenCalledWith(SettingScope.User, 'mcpServers', {
'sse-server': {
url: 'https://example.com/sse',
type: 'sse',
headers: { 'X-API-Key': 'your-key' },
},
});
});
it('should add an http server using --type alias', async () => {
await parser.parseAsync(
'add --type http -H "Authorization: Bearer your-token" http-server https://example.com/mcp',
);
expect(mockSetValue).toHaveBeenCalledWith(
SettingScope.Workspace,
'mcpServers',
{
'http-server': {
url: 'https://example.com/mcp',
type: 'http',
httpUrl: 'https://example.com/mcp',
headers: { Authorization: 'Bearer your-token' },
},
},
+2 -4
View File
@@ -69,7 +69,6 @@ async function addMcpServer(
case 'sse':
newServer = {
url: commandOrUrl,
type: 'sse',
headers,
timeout,
trust,
@@ -80,8 +79,7 @@ async function addMcpServer(
break;
case 'http':
newServer = {
url: commandOrUrl,
type: 'http',
httpUrl: commandOrUrl,
headers,
timeout,
trust,
@@ -165,7 +163,7 @@ export const addCommand: CommandModule = {
choices: ['user', 'project'],
})
.option('transport', {
alias: ['t', 'type'],
alias: 't',
describe: 'Transport type (stdio, sse, http)',
type: 'string',
default: 'stdio',
+34 -9
View File
@@ -632,14 +632,6 @@ describe('loadCliConfig', () => {
DEFAULT_FILE_FILTERING_OPTIONS.respectGeminiIgnore,
);
});
it('should default enableMessageBusIntegration to true when unconfigured', async () => {
process.argv = ['node', 'script.js'];
const argv = await parseArguments({} as Settings);
const settings: Settings = {};
const config = await loadCliConfig(settings, 'test-session', argv);
expect(config['enableMessageBusIntegration']).toBe(true);
});
});
describe('Hierarchical Memory Loading (config.ts) - Placeholder Suite', () => {
@@ -702,6 +694,39 @@ describe('Hierarchical Memory Loading (config.ts) - Placeholder Suite', () => {
undefined, // maxDirs
);
});
// NOTE TO FUTURE DEVELOPERS:
// To re-enable tests for loadHierarchicalGeminiMemory, ensure that:
// 1. os.homedir() is reliably mocked *before* the config.ts module is loaded
// and its functions (which use os.homedir()) are called.
// 2. fs/promises and fs mocks correctly simulate file/directory existence,
// readability, and content based on paths derived from the mocked os.homedir().
// 3. Spies on console functions (for logger output) are correctly set up if needed.
// Example of a previously failing test structure:
it.skip('should correctly use mocked homedir for global path', async () => {
// This test is skipped because mockFs and fsPromises are not properly imported/mocked
// TODO: Fix this test by properly setting up mock-fs and fs/promises mocks
/*
const MOCK_GEMINI_DIR_LOCAL = path.join(
'/mock/home/user',
ServerConfig.GEMINI_DIR,
);
const MOCK_GLOBAL_PATH_LOCAL = path.join(
MOCK_GEMINI_DIR_LOCAL,
'GEMINI.md',
);
mockFs({
[MOCK_GLOBAL_PATH_LOCAL]: { type: 'file', content: 'GlobalContentOnly' },
});
const memory = await loadHierarchicalGeminiMemory('/some/other/cwd', false);
expect(memory).toBe('GlobalContentOnly');
expect(vi.mocked(os.homedir)).toHaveBeenCalled();
expect(fsPromises.readFile).toHaveBeenCalledWith(
MOCK_GLOBAL_PATH_LOCAL,
'utf-8',
);
*/
});
});
describe('mergeMcpServers', () => {
@@ -1283,7 +1308,7 @@ describe('loadCliConfig model selection', () => {
argv,
);
expect(config.getModel()).toBe('auto-gemini-2.5');
expect(config.getModel()).toBe('auto');
});
it('always prefers model from argv', async () => {
+2 -12
View File
@@ -10,7 +10,6 @@ import process from 'node:process';
import { mcpCommand } from '../commands/mcp.js';
import type { OutputFormat } from '@google/gemini-cli-core';
import { extensionsCommand } from '../commands/extensions.js';
import { hooksCommand } from '../commands/hooks.js';
import {
Config,
setGeminiMdFilename as setServerGeminiMdFilename,
@@ -31,7 +30,6 @@ import {
debugLogger,
loadServerHierarchicalMemory,
WEB_FETCH_TOOL_NAME,
PREVIEW_GEMINI_MODEL_AUTO,
} from '@google/gemini-cli-core';
import type { Settings } from './settings.js';
@@ -283,11 +281,6 @@ export async function parseArguments(settings: Settings): Promise<CliArgs> {
yargsInstance.command(extensionsCommand);
}
// Register hooks command if hooks are enabled
if (settings?.tools?.enableHooks) {
yargsInstance.command(hooksCommand);
}
yargsInstance
.version(await getCliVersion()) // This will enable the --version flag based on package.json
.alias('v', 'version')
@@ -520,7 +513,7 @@ export async function loadCliConfig(
);
const enableMessageBusIntegration =
settings.tools?.enableMessageBusIntegration ?? true;
settings.tools?.enableMessageBusIntegration ?? false;
const allowedTools = argv.allowedTools || settings.tools?.allowed || [];
const allowedToolsSet = new Set(allowedTools);
@@ -570,9 +563,7 @@ export async function loadCliConfig(
extraExcludes.length > 0 ? extraExcludes : undefined,
);
const defaultModel = settings.general?.previewFeatures
? PREVIEW_GEMINI_MODEL_AUTO
: DEFAULT_GEMINI_MODEL_AUTO;
const defaultModel = DEFAULT_GEMINI_MODEL_AUTO;
const resolvedModel: string =
argv.model ||
process.env['GEMINI_MODEL'] ||
@@ -642,7 +633,6 @@ export async function loadCliConfig(
enabledExtensions: argv.extensions,
extensionLoader: extensionManager,
enableExtensionReloading: settings.experimental?.extensionReloading,
enableAgents: settings.experimental?.enableAgents,
enableModelAvailabilityService:
settings.experimental?.isModelAvailabilityServiceEnabled,
experimentalJitContext: settings.experimental?.jitContext,
@@ -41,8 +41,6 @@ import {
type MCPServerConfig,
type ExtensionInstallMetadata,
type GeminiCLIExtension,
type HookDefinition,
type HookEventName,
} from '@google/gemini-cli-core';
import { maybeRequestConsentOrFail } from './extensions/consent.js';
import { resolveEnvVarsInObject } from '../utils/envVarResolver.js';
@@ -255,20 +253,10 @@ export class ExtensionManager extends ExtensionLoader {
);
}
const newHasHooks = fs.existsSync(
path.join(localSourcePath, 'hooks', 'hooks.json'),
);
let previousHasHooks = false;
if (isUpdate && previous && previous.hooks) {
previousHasHooks = Object.keys(previous.hooks).length > 0;
}
await maybeRequestConsentOrFail(
newExtensionConfig,
this.requestConsent,
newHasHooks,
previousExtensionConfig,
previousHasHooks,
);
const extensionId = getExtensionId(newExtensionConfig, installMetadata);
const destinationPath = new ExtensionStorage(
@@ -346,7 +334,6 @@ export class ExtensionManager extends ExtensionLoader {
'success',
),
);
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.enableExtension(newExtensionConfig.name, SettingScope.User);
}
} finally {
@@ -514,14 +501,6 @@ export class ExtensionManager extends ExtensionLoader {
)
.filter((contextFilePath) => fs.existsSync(contextFilePath));
let hooks: { [K in HookEventName]?: HookDefinition[] } | undefined;
if (this.settings.tools?.enableHooks) {
hooks = await this.loadExtensionHooks(effectiveExtensionPath, {
extensionPath: effectiveExtensionPath,
workspacePath: this.workspaceDir,
});
}
const extension = {
name: config.name,
version: config.version,
@@ -530,7 +509,6 @@ export class ExtensionManager extends ExtensionLoader {
installMetadata,
mcpServers: config.mcpServers,
excludeTools: config.excludeTools,
hooks,
isActive: this.extensionEnablementManager.isEnabled(
config.name,
this.workspaceDir,
@@ -598,53 +576,6 @@ export class ExtensionManager extends ExtensionLoader {
}
}
private async loadExtensionHooks(
extensionDir: string,
context: { extensionPath: string; workspacePath: string },
): Promise<{ [K in HookEventName]?: HookDefinition[] } | undefined> {
const hooksFilePath = path.join(extensionDir, 'hooks', 'hooks.json');
try {
const hooksContent = await fs.promises.readFile(hooksFilePath, 'utf-8');
const rawHooks = JSON.parse(hooksContent);
if (
!rawHooks ||
typeof rawHooks !== 'object' ||
typeof rawHooks.hooks !== 'object' ||
rawHooks.hooks === null ||
Array.isArray(rawHooks.hooks)
) {
debugLogger.warn(
`Invalid hooks configuration in ${hooksFilePath}: "hooks" property must be an object`,
);
return undefined;
}
// Hydrate variables in the hooks configuration
const hydratedHooks = recursivelyHydrateStrings(
rawHooks.hooks as unknown as JsonObject,
{
...context,
'/': path.sep,
pathSeparator: path.sep,
},
) as { [K in HookEventName]?: HookDefinition[] };
return hydratedHooks;
} catch (e) {
if ((e as NodeJS.ErrnoException).code === 'ENOENT') {
return undefined; // File not found is not an error here.
}
debugLogger.warn(
`Failed to load extension hooks from ${hooksFilePath}: ${getErrorMessage(
e,
)}`,
);
return undefined;
}
}
toOutputString(extension: GeminiCLIExtension): string {
const userEnabled = this.extensionEnablementManager.isEnabled(
extension.name,
+1 -119
View File
@@ -713,129 +713,11 @@ describe('extension tests', () => {
name: 'no-meta-name',
version: '1.0.0',
});
const extensions = await extensionManager.loadExtensions();
const extension = extensions.find((e) => e.name === 'no-meta-name');
expect(extension?.id).toBe(hashValue('no-meta-name'));
});
it('should load extension hooks and hydrate variables', async () => {
const extDir = createExtension({
extensionsDir: userExtensionsDir,
name: 'hook-extension',
version: '1.0.0',
});
const hooksDir = path.join(extDir, 'hooks');
fs.mkdirSync(hooksDir);
const hooksConfig = {
hooks: {
BeforeTool: [
{
matcher: '.*',
hooks: [
{
type: 'command',
command: 'echo ${extensionPath}',
},
],
},
],
},
};
fs.writeFileSync(
path.join(hooksDir, 'hooks.json'),
JSON.stringify(hooksConfig),
);
const settings = loadSettings(tempWorkspaceDir).merged;
if (!settings.tools) settings.tools = {};
settings.tools.enableHooks = true;
extensionManager = new ExtensionManager({
workspaceDir: tempWorkspaceDir,
requestConsent: mockRequestConsent,
requestSetting: mockPromptForSettings,
settings,
});
const extensions = await extensionManager.loadExtensions();
expect(extensions).toHaveLength(1);
const extension = extensions[0];
expect(extension.hooks).toBeDefined();
expect(extension.hooks?.BeforeTool).toHaveLength(1);
expect(extension.hooks?.BeforeTool?.[0].hooks[0].command).toBe(
`echo ${extDir}`,
);
});
it('should not load hooks if enableHooks is false', async () => {
const extDir = createExtension({
extensionsDir: userExtensionsDir,
name: 'hook-extension-disabled',
version: '1.0.0',
});
const hooksDir = path.join(extDir, 'hooks');
fs.mkdirSync(hooksDir);
fs.writeFileSync(
path.join(hooksDir, 'hooks.json'),
JSON.stringify({ hooks: { BeforeTool: [] } }),
);
const settings = loadSettings(tempWorkspaceDir).merged;
if (!settings.tools) settings.tools = {};
settings.tools.enableHooks = false;
extensionManager = new ExtensionManager({
workspaceDir: tempWorkspaceDir,
requestConsent: mockRequestConsent,
requestSetting: mockPromptForSettings,
settings,
});
const extensions = await extensionManager.loadExtensions();
expect(extensions).toHaveLength(1);
expect(extensions[0].hooks).toBeUndefined();
});
it('should warn about hooks during installation', async () => {
const requestConsentSpy = vi.fn().mockResolvedValue(true);
extensionManager.setRequestConsent(requestConsentSpy);
const sourceExtDir = path.join(
tempWorkspaceDir,
'hook-extension-source',
);
fs.mkdirSync(sourceExtDir, { recursive: true });
const hooksDir = path.join(sourceExtDir, 'hooks');
fs.mkdirSync(hooksDir);
fs.writeFileSync(
path.join(hooksDir, 'hooks.json'),
JSON.stringify({ hooks: {} }),
);
fs.writeFileSync(
path.join(sourceExtDir, 'gemini-extension.json'),
JSON.stringify({
name: 'hook-extension-install',
version: '1.0.0',
}),
);
await extensionManager.loadExtensions();
await extensionManager.installOrUpdateExtension({
source: sourceExtDir,
type: 'local',
});
expect(requestConsentSpy).toHaveBeenCalledWith(
expect.stringContaining('⚠️ This extension contains Hooks'),
);
});
});
});
@@ -112,31 +112,20 @@ describe('consent', () => {
it('should request consent if there is no previous config', async () => {
const requestConsent = vi.fn().mockResolvedValue(true);
await maybeRequestConsentOrFail(
baseConfig,
requestConsent,
false,
undefined,
);
await maybeRequestConsentOrFail(baseConfig, requestConsent, undefined);
expect(requestConsent).toHaveBeenCalledTimes(1);
});
it('should not request consent if configs are identical', async () => {
const requestConsent = vi.fn().mockResolvedValue(true);
await maybeRequestConsentOrFail(
baseConfig,
requestConsent,
false,
baseConfig,
false,
);
await maybeRequestConsentOrFail(baseConfig, requestConsent, baseConfig);
expect(requestConsent).not.toHaveBeenCalled();
});
it('should throw an error if consent is denied', async () => {
const requestConsent = vi.fn().mockResolvedValue(false);
await expect(
maybeRequestConsentOrFail(baseConfig, requestConsent, false, undefined),
maybeRequestConsentOrFail(baseConfig, requestConsent, undefined),
).rejects.toThrow('Installation cancelled for "test-ext".');
});
@@ -152,12 +141,7 @@ describe('consent', () => {
excludeTools: ['tool1', 'tool2'],
};
const requestConsent = vi.fn().mockResolvedValue(true);
await maybeRequestConsentOrFail(
config,
requestConsent,
false,
undefined,
);
await maybeRequestConsentOrFail(config, requestConsent, undefined);
const expectedConsentString = [
'Installing extension "test-ext".',
@@ -179,13 +163,7 @@ describe('consent', () => {
mcpServers: { server1: { command: 'npm', args: ['start'] } },
};
const requestConsent = vi.fn().mockResolvedValue(true);
await maybeRequestConsentOrFail(
newConfig,
requestConsent,
false,
prevConfig,
false,
);
await maybeRequestConsentOrFail(newConfig, requestConsent, prevConfig);
expect(requestConsent).toHaveBeenCalledTimes(1);
});
@@ -196,13 +174,7 @@ describe('consent', () => {
contextFileName: 'new-context.md',
};
const requestConsent = vi.fn().mockResolvedValue(true);
await maybeRequestConsentOrFail(
newConfig,
requestConsent,
false,
prevConfig,
false,
);
await maybeRequestConsentOrFail(newConfig, requestConsent, prevConfig);
expect(requestConsent).toHaveBeenCalledTimes(1);
});
@@ -213,41 +185,7 @@ describe('consent', () => {
excludeTools: ['new-tool'],
};
const requestConsent = vi.fn().mockResolvedValue(true);
await maybeRequestConsentOrFail(
newConfig,
requestConsent,
false,
prevConfig,
false,
);
expect(requestConsent).toHaveBeenCalledTimes(1);
});
it('should include warning when hooks are present', async () => {
const requestConsent = vi.fn().mockResolvedValue(true);
await maybeRequestConsentOrFail(
baseConfig,
requestConsent,
true,
undefined,
);
expect(requestConsent).toHaveBeenCalledWith(
expect.stringContaining(
'⚠️ This extension contains Hooks which can automatically execute commands.',
),
);
});
it('should request consent if hooks status changes', async () => {
const requestConsent = vi.fn().mockResolvedValue(true);
await maybeRequestConsentOrFail(
baseConfig,
requestConsent,
true,
baseConfig,
false,
);
await maybeRequestConsentOrFail(newConfig, requestConsent, prevConfig);
expect(requestConsent).toHaveBeenCalledTimes(1);
});
});
+2 -13
View File
@@ -103,10 +103,7 @@ async function promptForConsentInteractive(
* Builds a consent string for installing an extension based on it's
* extensionConfig.
*/
function extensionConsentString(
extensionConfig: ExtensionConfig,
hasHooks: boolean,
): string {
function extensionConsentString(extensionConfig: ExtensionConfig): string {
const sanitizedConfig = escapeAnsiCtrlCodes(extensionConfig);
const output: string[] = [];
const mcpServerEntries = Object.entries(sanitizedConfig.mcpServers || {});
@@ -133,11 +130,6 @@ function extensionConsentString(
`This extension will exclude the following core tools: ${sanitizedConfig.excludeTools}`,
);
}
if (hasHooks) {
output.push(
'⚠️ This extension contains Hooks which can automatically execute commands.',
);
}
return output.join('\n');
}
@@ -153,15 +145,12 @@ function extensionConsentString(
export async function maybeRequestConsentOrFail(
extensionConfig: ExtensionConfig,
requestConsent: (consent: string) => Promise<boolean>,
hasHooks: boolean,
previousExtensionConfig?: ExtensionConfig,
previousHasHooks?: boolean,
) {
const extensionConsent = extensionConsentString(extensionConfig, hasHooks);
const extensionConsent = extensionConsentString(extensionConfig);
if (previousExtensionConfig) {
const previousExtensionConsent = extensionConsentString(
previousExtensionConfig,
previousHasHooks ?? false,
);
if (previousExtensionConsent === extensionConsent) {
return;
@@ -12,9 +12,6 @@ import {
maybePromptForSettings,
promptForSetting,
type ExtensionSetting,
updateSetting,
ExtensionSettingScope,
getScopedEnvContents,
} from './extensionSettings.js';
import type { ExtensionConfig } from '../extension.js';
import { ExtensionStorage } from './storage.js';
@@ -22,7 +19,6 @@ import prompts from 'prompts';
import * as fsPromises from 'node:fs/promises';
import * as fs from 'node:fs';
import { KeychainTokenStorage } from '@google/gemini-cli-core';
import { EXTENSION_SETTINGS_FILENAME } from './variables.js';
vi.mock('prompts');
vi.mock('os', async (importOriginal) => {
@@ -38,66 +34,67 @@ vi.mock('@google/gemini-cli-core', async (importOriginal) => {
await importOriginal<typeof import('@google/gemini-cli-core')>();
return {
...actual,
KeychainTokenStorage: vi.fn(),
KeychainTokenStorage: vi.fn().mockImplementation(() => ({
getSecret: vi.fn(),
setSecret: vi.fn(),
deleteSecret: vi.fn(),
listSecrets: vi.fn(),
isAvailable: vi.fn().mockResolvedValue(true),
})),
};
});
interface MockKeychainStorage {
getSecret: ReturnType<typeof vi.fn>;
setSecret: ReturnType<typeof vi.fn>;
deleteSecret: ReturnType<typeof vi.fn>;
listSecrets: ReturnType<typeof vi.fn>;
isAvailable: ReturnType<typeof vi.fn>;
}
describe('extensionSettings', () => {
let tempHomeDir: string;
let tempWorkspaceDir: string;
let extensionDir: string;
let mockKeychainData: Record<string, Record<string, string>>;
let mockKeychainStorage: MockKeychainStorage;
let keychainData: Record<string, string>;
beforeEach(() => {
vi.clearAllMocks();
mockKeychainData = {};
vi.mocked(KeychainTokenStorage).mockImplementation(
(serviceName: string) => {
if (!mockKeychainData[serviceName]) {
mockKeychainData[serviceName] = {};
}
const keychainData = mockKeychainData[serviceName];
return {
getSecret: vi
.fn()
.mockImplementation(
async (key: string) => keychainData[key] || null,
),
setSecret: vi
.fn()
.mockImplementation(async (key: string, value: string) => {
keychainData[key] = value;
}),
deleteSecret: vi.fn().mockImplementation(async (key: string) => {
delete keychainData[key];
}),
listSecrets: vi
.fn()
.mockImplementation(async () => Object.keys(keychainData)),
isAvailable: vi.fn().mockResolvedValue(true),
} as unknown as KeychainTokenStorage;
},
);
keychainData = {};
mockKeychainStorage = {
getSecret: vi
.fn()
.mockImplementation(async (key: string) => keychainData[key] || null),
setSecret: vi
.fn()
.mockImplementation(async (key: string, value: string) => {
keychainData[key] = value;
}),
deleteSecret: vi.fn().mockImplementation(async (key: string) => {
delete keychainData[key];
}),
listSecrets: vi
.fn()
.mockImplementation(async () => Object.keys(keychainData)),
isAvailable: vi.fn().mockResolvedValue(true),
};
(
KeychainTokenStorage as unknown as ReturnType<typeof vi.fn>
).mockImplementation(() => mockKeychainStorage);
tempHomeDir = os.tmpdir() + path.sep + `gemini-cli-test-home-${Date.now()}`;
tempWorkspaceDir = path.join(
os.tmpdir(),
`gemini-cli-test-workspace-${Date.now()}`,
);
extensionDir = path.join(tempHomeDir, '.gemini', 'extensions', 'test-ext');
// Spy and mock the method, but also create the directory so we can write to it.
vi.spyOn(ExtensionStorage.prototype, 'getExtensionDir').mockReturnValue(
extensionDir,
);
fs.mkdirSync(extensionDir, { recursive: true });
fs.mkdirSync(tempWorkspaceDir, { recursive: true });
vi.mocked(os.homedir).mockReturnValue(tempHomeDir);
vi.spyOn(process, 'cwd').mockReturnValue(tempWorkspaceDir);
vi.mocked(prompts).mockClear();
});
afterEach(() => {
fs.rmSync(tempHomeDir, { recursive: true, force: true });
fs.rmSync(tempWorkspaceDir, { recursive: true, force: true });
vi.restoreAllMocks();
});
@@ -215,10 +212,7 @@ describe('extensionSettings', () => {
VAR1: 'previous-VAR1',
SENSITIVE_VAR: 'secret',
};
const userKeychain = new KeychainTokenStorage(
`Gemini CLI Extensions test-ext 12345`,
);
await userKeychain.setSecret('SENSITIVE_VAR', 'secret');
keychainData['SENSITIVE_VAR'] = 'secret';
const envPath = path.join(extensionDir, '.env');
await fsPromises.writeFile(envPath, 'VAR1=previous-VAR1');
@@ -233,7 +227,9 @@ describe('extensionSettings', () => {
expect(mockRequestSetting).not.toHaveBeenCalled();
const actualContent = await fsPromises.readFile(envPath, 'utf-8');
expect(actualContent).toBe('');
expect(await userKeychain.getSecret('SENSITIVE_VAR')).toBeNull();
expect(mockKeychainStorage.deleteSecret).toHaveBeenCalledWith(
'SENSITIVE_VAR',
);
});
it('should remove sensitive settings from keychain', async () => {
@@ -255,10 +251,7 @@ describe('extensionSettings', () => {
settings: [],
};
const previousSettings = { SENSITIVE_VAR: 'secret' };
const userKeychain = new KeychainTokenStorage(
`Gemini CLI Extensions test-ext 12345`,
);
await userKeychain.setSecret('SENSITIVE_VAR', 'secret');
keychainData['SENSITIVE_VAR'] = 'secret';
await maybePromptForSettings(
newConfig,
@@ -268,7 +261,9 @@ describe('extensionSettings', () => {
previousSettings,
);
expect(await userKeychain.getSecret('SENSITIVE_VAR')).toBeNull();
expect(mockKeychainStorage.deleteSecret).toHaveBeenCalledWith(
'SENSITIVE_VAR',
);
});
it('should remove settings that are no longer in the config', async () => {
@@ -376,27 +371,6 @@ describe('extensionSettings', () => {
const expectedContent = 'VAR1=previous-VAR1\nVAR2=previous-VAR2\n';
expect(actualContent).toBe(expectedContent);
});
it('should wrap values with spaces in quotes', async () => {
const config: ExtensionConfig = {
name: 'test-ext',
version: '1.0.0',
settings: [{ name: 's1', description: 'd1', envVar: 'VAR1' }],
};
mockRequestSetting.mockResolvedValue('a value with spaces');
await maybePromptForSettings(
config,
'12345',
mockRequestSetting,
undefined,
undefined,
);
const expectedEnvPath = path.join(extensionDir, '.env');
const actualContent = await fsPromises.readFile(expectedEnvPath, 'utf-8');
expect(actualContent).toBe('VAR1="a value with spaces"\n');
});
});
describe('promptForSetting', () => {
@@ -459,7 +433,7 @@ describe('extensionSettings', () => {
});
});
describe('getScopedEnvContents', () => {
describe('getEnvContents', () => {
const config: ExtensionConfig = {
name: 'test-ext',
version: '1.0.0',
@@ -473,217 +447,39 @@ describe('extensionSettings', () => {
},
],
};
const extensionId = '12345';
it('should return combined contents from user .env and keychain for USER scope', async () => {
const userEnvPath = path.join(extensionDir, EXTENSION_SETTINGS_FILENAME);
await fsPromises.writeFile(userEnvPath, 'VAR1=user-value1');
const userKeychain = new KeychainTokenStorage(
`Gemini CLI Extensions test-ext 12345`,
);
await userKeychain.setSecret('SENSITIVE_VAR', 'user-secret');
it('should return combined contents from .env and keychain', async () => {
const envPath = path.join(extensionDir, '.env');
await fsPromises.writeFile(envPath, 'VAR1=value1');
keychainData['SENSITIVE_VAR'] = 'secret';
const contents = await getScopedEnvContents(
config,
extensionId,
ExtensionSettingScope.USER,
);
const contents = await getEnvContents(config, '12345');
expect(contents).toEqual({
VAR1: 'user-value1',
SENSITIVE_VAR: 'user-secret',
VAR1: 'value1',
SENSITIVE_VAR: 'secret',
});
});
it('should return combined contents from workspace .env and keychain for WORKSPACE scope', async () => {
const workspaceEnvPath = path.join(
tempWorkspaceDir,
EXTENSION_SETTINGS_FILENAME,
);
await fsPromises.writeFile(workspaceEnvPath, 'VAR1=workspace-value1');
const workspaceKeychain = new KeychainTokenStorage(
`Gemini CLI Extensions test-ext 12345 ${tempWorkspaceDir}`,
);
await workspaceKeychain.setSecret('SENSITIVE_VAR', 'workspace-secret');
const contents = await getScopedEnvContents(
config,
extensionId,
ExtensionSettingScope.WORKSPACE,
);
expect(contents).toEqual({
VAR1: 'workspace-value1',
SENSITIVE_VAR: 'workspace-secret',
});
});
});
describe('getEnvContents (merged)', () => {
const config: ExtensionConfig = {
name: 'test-ext',
version: '1.0.0',
settings: [
{ name: 's1', description: 'd1', envVar: 'VAR1' },
{ name: 's2', description: 'd2', envVar: 'VAR2', sensitive: true },
{ name: 's3', description: 'd3', envVar: 'VAR3' },
],
};
const extensionId = '12345';
it('should merge user and workspace settings, with workspace taking precedence', async () => {
// User settings
const userEnvPath = path.join(extensionDir, EXTENSION_SETTINGS_FILENAME);
await fsPromises.writeFile(
userEnvPath,
'VAR1=user-value1\nVAR3=user-value3',
);
const userKeychain = new KeychainTokenStorage(
`Gemini CLI Extensions test-ext ${extensionId}`,
);
await userKeychain.setSecret('VAR2', 'user-secret2');
// Workspace settings
const workspaceEnvPath = path.join(
tempWorkspaceDir,
EXTENSION_SETTINGS_FILENAME,
);
await fsPromises.writeFile(workspaceEnvPath, 'VAR1=workspace-value1');
const workspaceKeychain = new KeychainTokenStorage(
`Gemini CLI Extensions test-ext ${extensionId} ${tempWorkspaceDir}`,
);
await workspaceKeychain.setSecret('VAR2', 'workspace-secret2');
const contents = await getEnvContents(config, extensionId);
expect(contents).toEqual({
VAR1: 'workspace-value1',
VAR2: 'workspace-secret2',
VAR3: 'user-value3',
});
});
});
describe('updateSetting', () => {
const config: ExtensionConfig = {
name: 'test-ext',
version: '1.0.0',
settings: [
{ name: 's1', description: 'd1', envVar: 'VAR1' },
{ name: 's2', description: 'd2', envVar: 'VAR2', sensitive: true },
],
};
const mockRequestSetting = vi.fn();
beforeEach(async () => {
const userEnvPath = path.join(extensionDir, '.env');
await fsPromises.writeFile(userEnvPath, 'VAR1=value1\n');
const userKeychain = new KeychainTokenStorage(
`Gemini CLI Extensions test-ext 12345`,
);
await userKeychain.setSecret('VAR2', 'value2');
mockRequestSetting.mockClear();
});
it('should update a non-sensitive setting in USER scope', async () => {
mockRequestSetting.mockResolvedValue('new-value1');
await updateSetting(
config,
it('should return an empty object if no settings are defined', async () => {
const contents = await getEnvContents(
{ name: 'test-ext', version: '1.0.0' },
'12345',
'VAR1',
mockRequestSetting,
ExtensionSettingScope.USER,
);
const expectedEnvPath = path.join(extensionDir, '.env');
const actualContent = await fsPromises.readFile(expectedEnvPath, 'utf-8');
expect(actualContent).toContain('VAR1=new-value1');
expect(contents).toEqual({});
});
it('should update a non-sensitive setting in WORKSPACE scope', async () => {
mockRequestSetting.mockResolvedValue('new-workspace-value');
await updateSetting(
config,
'12345',
'VAR1',
mockRequestSetting,
ExtensionSettingScope.WORKSPACE,
);
const expectedEnvPath = path.join(tempWorkspaceDir, '.env');
const actualContent = await fsPromises.readFile(expectedEnvPath, 'utf-8');
expect(actualContent).toContain('VAR1=new-workspace-value');
it('should return only keychain contents if .env file does not exist', async () => {
keychainData['SENSITIVE_VAR'] = 'secret';
const contents = await getEnvContents(config, '12345');
expect(contents).toEqual({ SENSITIVE_VAR: 'secret' });
});
it('should update a sensitive setting in USER scope', async () => {
mockRequestSetting.mockResolvedValue('new-value2');
await updateSetting(
config,
'12345',
'VAR2',
mockRequestSetting,
ExtensionSettingScope.USER,
);
const userKeychain = new KeychainTokenStorage(
`Gemini CLI Extensions test-ext 12345`,
);
expect(await userKeychain.getSecret('VAR2')).toBe('new-value2');
});
it('should update a sensitive setting in WORKSPACE scope', async () => {
mockRequestSetting.mockResolvedValue('new-workspace-secret');
await updateSetting(
config,
'12345',
'VAR2',
mockRequestSetting,
ExtensionSettingScope.WORKSPACE,
);
const workspaceKeychain = new KeychainTokenStorage(
`Gemini CLI Extensions test-ext 12345 ${tempWorkspaceDir}`,
);
expect(await workspaceKeychain.getSecret('VAR2')).toBe(
'new-workspace-secret',
);
});
it('should leave existing, unmanaged .env variables intact when updating in WORKSPACE scope', async () => {
// Setup a pre-existing .env file in the workspace with unmanaged variables
const workspaceEnvPath = path.join(tempWorkspaceDir, '.env');
const originalEnvContent =
'PROJECT_VAR_1=value_1\nPROJECT_VAR_2=value_2\nVAR1=original-value'; // VAR1 is managed by extension
await fsPromises.writeFile(workspaceEnvPath, originalEnvContent);
// Simulate updating an extension-managed non-sensitive setting
mockRequestSetting.mockResolvedValue('updated-value');
await updateSetting(
config,
'12345',
'VAR1',
mockRequestSetting,
ExtensionSettingScope.WORKSPACE,
);
// Read the .env file after update
const actualContent = await fsPromises.readFile(
workspaceEnvPath,
'utf-8',
);
// Assert that original variables are intact and extension variable is updated
expect(actualContent).toContain('PROJECT_VAR_1=value_1');
expect(actualContent).toContain('PROJECT_VAR_2=value_2');
expect(actualContent).toContain('VAR1=updated-value');
// Ensure no other unexpected changes or deletions
const lines = actualContent.split('\n').filter((line) => line.length > 0);
expect(lines).toHaveLength(3); // Should only have the three variables
it('should return only .env contents if keychain is empty', async () => {
const envPath = path.join(extensionDir, '.env');
await fsPromises.writeFile(envPath, 'VAR1=value1');
const contents = await getEnvContents(config, '12345');
expect(contents).toEqual({ VAR1: 'value1' });
});
});
});
@@ -7,19 +7,12 @@
import * as fs from 'node:fs/promises';
import * as fsSync from 'node:fs';
import * as dotenv from 'dotenv';
import * as path from 'node:path';
import { ExtensionStorage } from './storage.js';
import type { ExtensionConfig } from '../extension.js';
import prompts from 'prompts';
import { debugLogger, KeychainTokenStorage } from '@google/gemini-cli-core';
import { EXTENSION_SETTINGS_FILENAME } from './variables.js';
export enum ExtensionSettingScope {
USER = 'user',
WORKSPACE = 'workspace',
}
import { KeychainTokenStorage } from '@google/gemini-cli-core';
export interface ExtensionSetting {
name: string;
@@ -32,24 +25,7 @@ export interface ExtensionSetting {
const getKeychainStorageName = (
extensionName: string,
extensionId: string,
scope: ExtensionSettingScope,
): string => {
const base = `Gemini CLI Extensions ${extensionName} ${extensionId}`;
if (scope === ExtensionSettingScope.WORKSPACE) {
return `${base} ${process.cwd()}`;
}
return base;
};
const getEnvFilePath = (
extensionName: string,
scope: ExtensionSettingScope,
): string => {
if (scope === ExtensionSettingScope.WORKSPACE) {
return path.join(process.cwd(), EXTENSION_SETTINGS_FILENAME);
}
return new ExtensionStorage(extensionName).getEnvFilePath();
};
): string => `Gemini CLI Extensions ${extensionName} ${extensionId}`;
export async function maybePromptForSettings(
extensionConfig: ExtensionConfig,
@@ -66,12 +42,9 @@ export async function maybePromptForSettings(
) {
return;
}
// We assume user scope here because we don't have a way to ask the user for scope during the initial setup.
// The user can change the scope later using the `settings set` command.
const scope = ExtensionSettingScope.USER;
const envFilePath = getEnvFilePath(extensionName, scope);
const envFilePath = new ExtensionStorage(extensionName).getEnvFilePath();
const keychain = new KeychainTokenStorage(
getKeychainStorageName(extensionName, extensionId, scope),
getKeychainStorageName(extensionName, extensionId),
);
if (!settings || settings.length === 0) {
@@ -84,7 +57,7 @@ export async function maybePromptForSettings(
previousExtensionConfig?.settings ?? [],
);
const allSettings: Record<string, string> = { ...previousSettings };
const allSettings: Record<string, string> = { ...(previousSettings ?? {}) };
for (const removedEnvSetting of settingsChanges.removeEnv) {
delete allSettings[removedEnvSetting.envVar];
@@ -114,20 +87,14 @@ export async function maybePromptForSettings(
}
}
const envContent = formatEnvContent(nonSensitiveSettings);
let envContent = '';
for (const [key, value] of Object.entries(nonSensitiveSettings)) {
envContent += `${key}=${value}\n`;
}
await fs.writeFile(envFilePath, envContent);
}
function formatEnvContent(settings: Record<string, string>): string {
let envContent = '';
for (const [key, value] of Object.entries(settings)) {
const formattedValue = value.includes(' ') ? `"${value}"` : value;
envContent += `${key}=${formattedValue}\n`;
}
return envContent;
}
export async function promptForSetting(
setting: ExtensionSetting,
): Promise<string> {
@@ -139,19 +106,23 @@ export async function promptForSetting(
return response.value;
}
export async function getScopedEnvContents(
export async function getEnvContents(
extensionConfig: ExtensionConfig,
extensionId: string,
scope: ExtensionSettingScope,
): Promise<Record<string, string>> {
const { name: extensionName } = extensionConfig;
if (!extensionConfig.settings || extensionConfig.settings.length === 0) {
return Promise.resolve({});
}
const extensionStorage = new ExtensionStorage(extensionConfig.name);
const keychain = new KeychainTokenStorage(
getKeychainStorageName(extensionName, extensionId, scope),
getKeychainStorageName(extensionConfig.name, extensionId),
);
const envFilePath = getEnvFilePath(extensionName, scope);
let customEnv: Record<string, string> = {};
if (fsSync.existsSync(envFilePath)) {
const envFile = fsSync.readFileSync(envFilePath, 'utf-8');
if (fsSync.existsSync(extensionStorage.getEnvFilePath())) {
const envFile = fsSync.readFileSync(
extensionStorage.getEnvFilePath(),
'utf-8',
);
customEnv = dotenv.parse(envFile);
}
@@ -168,86 +139,6 @@ export async function getScopedEnvContents(
return customEnv;
}
export async function getEnvContents(
extensionConfig: ExtensionConfig,
extensionId: string,
): Promise<Record<string, string>> {
if (!extensionConfig.settings || extensionConfig.settings.length === 0) {
return Promise.resolve({});
}
const userSettings = await getScopedEnvContents(
extensionConfig,
extensionId,
ExtensionSettingScope.USER,
);
const workspaceSettings = await getScopedEnvContents(
extensionConfig,
extensionId,
ExtensionSettingScope.WORKSPACE,
);
return { ...userSettings, ...workspaceSettings };
}
export async function updateSetting(
extensionConfig: ExtensionConfig,
extensionId: string,
settingKey: string,
requestSetting: (setting: ExtensionSetting) => Promise<string>,
scope: ExtensionSettingScope,
): Promise<void> {
const { name: extensionName, settings } = extensionConfig;
if (!settings || settings.length === 0) {
debugLogger.log('This extension does not have any settings.');
return;
}
const settingToUpdate = settings.find(
(s) => s.name === settingKey || s.envVar === settingKey,
);
if (!settingToUpdate) {
debugLogger.log(`Setting ${settingKey} not found.`);
return;
}
const newValue = await requestSetting(settingToUpdate);
const keychain = new KeychainTokenStorage(
getKeychainStorageName(extensionName, extensionId, scope),
);
if (settingToUpdate.sensitive) {
await keychain.setSecret(settingToUpdate.envVar, newValue);
return;
}
// For non-sensitive settings, we need to read the existing .env file,
// update the value, and write it back, preserving any other values.
const envFilePath = getEnvFilePath(extensionName, scope);
let envContent = '';
if (fsSync.existsSync(envFilePath)) {
envContent = await fs.readFile(envFilePath, 'utf-8');
}
const parsedEnv = dotenv.parse(envContent);
parsedEnv[settingToUpdate.envVar] = newValue;
// We only want to write back the variables that are not sensitive.
const nonSensitiveSettings: Record<string, string> = {};
const sensitiveEnvVars = new Set(
settings.filter((s) => s.sensitive).map((s) => s.envVar),
);
for (const [key, value] of Object.entries(parsedEnv)) {
if (!sensitiveEnvVars.has(key)) {
nonSensitiveSettings[key] = value;
}
}
const newEnvContent = formatEnvContent(nonSensitiveSettings);
await fs.writeFile(envFilePath, newEnvContent);
}
interface settingsChanges {
promptForSensitive: ExtensionSetting[];
removeSensitive: ExtensionSetting[];
@@ -43,7 +43,6 @@ vi.mock('@google/gemini-cli-core', async (importOriginal) => {
debugLogger: {
error: vi.fn(),
log: vi.fn(),
warn: vi.fn(),
},
};
});
@@ -264,25 +263,6 @@ describe('github.ts', () => {
ExtensionUpdateState.UP_TO_DATE,
);
});
it('should return NOT_UPDATABLE if local extension config cannot be loaded', async () => {
vi.mocked(mockExtensionManager.loadExtensionConfig).mockImplementation(
() => {
throw new Error('Config not found');
},
);
const ext = {
name: 'local-ext',
version: '1.0.0',
path: '/path/to/installed/ext',
installMetadata: { type: 'local', source: '/path/to/source/ext' },
} as unknown as GeminiCLIExtension;
expect(await checkForExtensionUpdate(ext, mockExtensionManager)).toBe(
ExtensionUpdateState.NOT_UPDATABLE,
);
});
});
describe('downloadFromGitHubRelease', () => {
@@ -305,131 +285,6 @@ describe('github.ts', () => {
expect(result.failureReason).toBe('failed to fetch release data');
}
});
it('should use correct headers for release assets', async () => {
vi.mocked(fetchJson).mockResolvedValue({
tag_name: 'v1.0.0',
assets: [{ name: 'asset.tar.gz', url: 'http://asset.url' }],
});
vi.mocked(os.platform).mockReturnValue('linux');
vi.mocked(os.arch).mockReturnValue('x64');
// Mock https.get and fs.createWriteStream for downloadFile
const mockReq = new EventEmitter();
const mockRes =
new EventEmitter() as unknown as import('node:http').IncomingMessage;
Object.assign(mockRes, { statusCode: 200, pipe: vi.fn() });
vi.mocked(https.get).mockImplementation((url, options, cb) => {
if (typeof options === 'function') {
cb = options;
}
if (cb) cb(mockRes);
return mockReq as unknown as import('node:http').ClientRequest;
});
const mockStream = new EventEmitter() as unknown as fs.WriteStream;
Object.assign(mockStream, { close: vi.fn((cb) => cb && cb()) });
vi.mocked(fs.createWriteStream).mockReturnValue(mockStream);
// Mock fs.promises.readdir to return empty array (no cleanup needed)
vi.mocked(fs.promises.readdir).mockResolvedValue([]);
// Mock fs.promises.unlink
vi.mocked(fs.promises.unlink).mockResolvedValue(undefined);
const promise = downloadFromGitHubRelease(
{
type: 'github-release',
source: 'owner/repo',
ref: 'v1.0.0',
} as unknown as ExtensionInstallMetadata,
'/dest',
{ owner: 'owner', repo: 'repo' },
);
// Wait for downloadFile to be called and stream to be created
await vi.waitUntil(
() => vi.mocked(fs.createWriteStream).mock.calls.length > 0,
);
// Trigger stream events to complete download
mockRes.emit('end');
mockStream.emit('finish');
await promise;
expect(https.get).toHaveBeenCalledWith(
'http://asset.url',
expect.objectContaining({
headers: expect.objectContaining({
Accept: 'application/octet-stream',
}),
}),
expect.anything(),
);
});
it('should use correct headers for source tarballs', async () => {
vi.mocked(fetchJson).mockResolvedValue({
tag_name: 'v1.0.0',
assets: [],
tarball_url: 'http://tarball.url',
});
// Mock https.get and fs.createWriteStream for downloadFile
const mockReq = new EventEmitter();
const mockRes =
new EventEmitter() as unknown as import('node:http').IncomingMessage;
Object.assign(mockRes, { statusCode: 200, pipe: vi.fn() });
vi.mocked(https.get).mockImplementation((url, options, cb) => {
if (typeof options === 'function') {
cb = options;
}
if (cb) cb(mockRes);
return mockReq as unknown as import('node:http').ClientRequest;
});
const mockStream = new EventEmitter() as unknown as fs.WriteStream;
Object.assign(mockStream, { close: vi.fn((cb) => cb && cb()) });
vi.mocked(fs.createWriteStream).mockReturnValue(mockStream);
// Mock fs.promises.readdir to return empty array
vi.mocked(fs.promises.readdir).mockResolvedValue([]);
// Mock fs.promises.unlink
vi.mocked(fs.promises.unlink).mockResolvedValue(undefined);
const promise = downloadFromGitHubRelease(
{
type: 'github-release',
source: 'owner/repo',
ref: 'v1.0.0',
} as unknown as ExtensionInstallMetadata,
'/dest',
{ owner: 'owner', repo: 'repo' },
);
// Wait for downloadFile to be called and stream to be created
await vi.waitUntil(
() => vi.mocked(fs.createWriteStream).mock.calls.length > 0,
);
// Trigger stream events to complete download
mockRes.emit('end');
mockStream.emit('finish');
await promise;
expect(https.get).toHaveBeenCalledWith(
'http://tarball.url',
expect.objectContaining({
headers: expect.objectContaining({
Accept: 'application/vnd.github+json',
}),
}),
expect.anything(),
);
});
});
describe('findReleaseAsset', () => {
@@ -494,120 +349,6 @@ describe('github.ts', () => {
'Request failed with status code 404',
);
});
it('should follow redirects', async () => {
const mockReq = new EventEmitter();
const mockResRedirect =
new EventEmitter() as unknown as import('node:http').IncomingMessage;
Object.assign(mockResRedirect, {
statusCode: 302,
headers: { location: 'new-url' },
});
const mockResSuccess =
new EventEmitter() as unknown as import('node:http').IncomingMessage;
Object.assign(mockResSuccess, { statusCode: 200, pipe: vi.fn() });
vi.mocked(https.get)
.mockImplementationOnce((url, options, cb) => {
if (typeof options === 'function') cb = options;
if (cb) cb(mockResRedirect);
return mockReq as unknown as import('node:http').ClientRequest;
})
.mockImplementationOnce((url, options, cb) => {
if (typeof options === 'function') cb = options;
if (cb) cb(mockResSuccess);
return mockReq as unknown as import('node:http').ClientRequest;
});
const mockStream = new EventEmitter() as unknown as fs.WriteStream;
Object.assign(mockStream, { close: vi.fn((cb) => cb && cb()) });
vi.mocked(fs.createWriteStream).mockReturnValue(mockStream);
const promise = downloadFile('url', '/dest');
mockResSuccess.emit('end');
mockStream.emit('finish');
await expect(promise).resolves.toBeUndefined();
expect(https.get).toHaveBeenCalledTimes(2);
expect(https.get).toHaveBeenLastCalledWith(
'new-url',
expect.anything(),
expect.anything(),
);
});
it('should fail after too many redirects', async () => {
const mockReq = new EventEmitter();
const mockResRedirect =
new EventEmitter() as unknown as import('node:http').IncomingMessage;
Object.assign(mockResRedirect, {
statusCode: 302,
headers: { location: 'new-url' },
});
vi.mocked(https.get).mockImplementation((url, options, cb) => {
if (typeof options === 'function') cb = options;
if (cb) cb(mockResRedirect);
return mockReq as unknown as import('node:http').ClientRequest;
});
await expect(downloadFile('url', '/dest')).rejects.toThrow(
'Too many redirects',
);
}, 10000); // Increase timeout for this test if needed, though with mocks it should be fast
it('should fail if redirect location is missing', async () => {
const mockReq = new EventEmitter();
const mockResRedirect =
new EventEmitter() as unknown as import('node:http').IncomingMessage;
Object.assign(mockResRedirect, {
statusCode: 302,
headers: {}, // No location
});
vi.mocked(https.get).mockImplementation((url, options, cb) => {
if (typeof options === 'function') cb = options;
if (cb) cb(mockResRedirect);
return mockReq as unknown as import('node:http').ClientRequest;
});
await expect(downloadFile('url', '/dest')).rejects.toThrow(
'Redirect response missing Location header',
);
});
it('should pass custom headers', async () => {
const mockReq = new EventEmitter();
const mockRes =
new EventEmitter() as unknown as import('node:http').IncomingMessage;
Object.assign(mockRes, { statusCode: 200, pipe: vi.fn() });
vi.mocked(https.get).mockImplementation((url, options, cb) => {
if (typeof options === 'function') cb = options;
if (cb) cb(mockRes);
return mockReq as unknown as import('node:http').ClientRequest;
});
const mockStream = new EventEmitter() as unknown as fs.WriteStream;
Object.assign(mockStream, { close: vi.fn((cb) => cb && cb()) });
vi.mocked(fs.createWriteStream).mockReturnValue(mockStream);
const promise = downloadFile('url', '/dest', {
headers: { 'X-Custom': 'value' },
});
mockRes.emit('end');
mockStream.emit('finish');
await expect(promise).resolves.toBeUndefined();
expect(https.get).toHaveBeenCalledWith(
'url',
expect.objectContaining({
headers: expect.objectContaining({ 'X-Custom': 'value' }),
}),
expect.anything(),
);
});
});
describe('extractFile', () => {
+14 -52
View File
@@ -19,7 +19,6 @@ import * as path from 'node:path';
import * as tar from 'tar';
import extract from 'extract-zip';
import { fetchJson, getGitHubToken } from './github_fetch.js';
import type { ExtensionConfig } from '../extension.js';
import type { ExtensionManager } from '../extension-manager.js';
import { EXTENSIONS_CONFIG_FILENAME } from './variables.js';
@@ -173,23 +172,14 @@ export async function checkForExtensionUpdate(
): Promise<ExtensionUpdateState> {
const installMetadata = extension.installMetadata;
if (installMetadata?.type === 'local') {
let latestConfig: ExtensionConfig | undefined;
try {
latestConfig = extensionManager.loadExtensionConfig(
installMetadata.source,
);
} catch (e) {
debugLogger.warn(
`Failed to check for update for local extension "${extension.name}". Could not load extension from source path: ${installMetadata.source}. Error: ${getErrorMessage(e)}`,
);
return ExtensionUpdateState.NOT_UPDATABLE;
}
const latestConfig = extensionManager.loadExtensionConfig(
installMetadata.source,
);
if (!latestConfig) {
debugLogger.warn(
debugLogger.error(
`Failed to check for update for local extension "${extension.name}". Could not load extension from source path: ${installMetadata.source}`,
);
return ExtensionUpdateState.NOT_UPDATABLE;
return ExtensionUpdateState.ERROR;
}
if (latestConfig.version !== extension.version) {
return ExtensionUpdateState.UPDATE_AVAILABLE;
@@ -365,17 +355,7 @@ export async function downloadFromGitHubRelease(
}
try {
// GitHub API requires different Accept headers for different types of downloads:
// 1. Binary Assets (e.g. release artifacts): Require 'application/octet-stream' to return the raw content.
// 2. Source Tarballs (e.g. /tarball/{ref}): Require 'application/vnd.github+json' (or similar) to return
// a 302 Redirect to the actual download location (codeload.github.com).
// Sending 'application/octet-stream' for tarballs results in a 415 Unsupported Media Type error.
const headers = {
...(asset
? { Accept: 'application/octet-stream' }
: { Accept: 'application/vnd.github+json' }),
};
await downloadFile(archiveUrl, downloadedAssetPath, { headers });
await downloadFile(archiveUrl, downloadedAssetPath);
} catch (error) {
return {
failureReason: 'failed to download asset',
@@ -492,42 +472,24 @@ export function findReleaseAsset(assets: Asset[]): Asset | undefined {
return undefined;
}
export interface DownloadOptions {
headers?: Record<string, string>;
}
export async function downloadFile(
url: string,
dest: string,
options?: DownloadOptions,
redirectCount: number = 0,
): Promise<void> {
const headers: Record<string, string> = {
export async function downloadFile(url: string, dest: string): Promise<void> {
const headers: {
'User-agent': string;
Accept: string;
Authorization?: string;
} = {
'User-agent': 'gemini-cli',
Accept: 'application/octet-stream',
...options?.headers,
};
const token = getGitHubToken();
if (token) {
headers['Authorization'] = `token ${token}`;
headers.Authorization = `token ${token}`;
}
return new Promise((resolve, reject) => {
https
.get(url, { headers }, (res) => {
if (res.statusCode === 302 || res.statusCode === 301) {
if (redirectCount >= 10) {
return reject(new Error('Too many redirects'));
}
if (!res.headers.location) {
return reject(
new Error('Redirect response missing Location header'),
);
}
downloadFile(res.headers.location, dest, options, redirectCount + 1)
.then(resolve)
.catch(reject);
downloadFile(res.headers.location!, dest).then(resolve).catch(reject);
return;
}
if (res.statusCode !== 200) {
+4 -4
View File
@@ -291,7 +291,7 @@ describe('Settings Loading and Merging', () => {
theme: 'legacy-dark',
vimMode: true,
contextFileName: 'LEGACY_CONTEXT.md',
model: 'gemini-2.5-pro',
model: 'gemini-pro',
mcpServers: {
'legacy-server-1': {
command: 'npm',
@@ -329,7 +329,7 @@ describe('Settings Loading and Merging', () => {
fileName: 'LEGACY_CONTEXT.md',
},
model: {
name: 'gemini-2.5-pro',
name: 'gemini-pro',
},
mcpServers: {
'legacy-server-1': {
@@ -1929,7 +1929,7 @@ describe('Settings Loading and Merging', () => {
usageStatisticsEnabled: false,
},
model: {
name: 'gemini-2.5-pro',
name: 'gemini-pro',
},
context: {
fileName: 'CONTEXT.md',
@@ -1968,7 +1968,7 @@ describe('Settings Loading and Merging', () => {
vimMode: true,
theme: 'dark',
usageStatisticsEnabled: false,
model: 'gemini-2.5-pro',
model: 'gemini-pro',
contextFileName: 'CONTEXT.md',
includeDirectories: ['/src'],
sandbox: true,
-7
View File
@@ -38,17 +38,11 @@ import { SettingPaths } from './settingPaths.js';
function getMergeStrategyForPath(path: string[]): MergeStrategy | undefined {
let current: SettingDefinition | undefined = undefined;
let currentSchema: SettingsSchema | undefined = getSettingsSchema();
let parent: SettingDefinition | undefined = undefined;
for (const key of path) {
if (!currentSchema || !currentSchema[key]) {
// Key not found in schema - check if parent has additionalProperties
if (parent?.additionalProperties?.mergeStrategy) {
return parent.additionalProperties.mergeStrategy;
}
return undefined;
}
parent = current;
current = currentSchema[key];
currentSchema = current.properties;
}
@@ -799,7 +793,6 @@ export function migrateDeprecatedSettings(
`Migrating deprecated extensions.disabled settings from ${scope} settings...`,
);
for (const extension of settings.extensions.disabled ?? []) {
// eslint-disable-next-line @typescript-eslint/no-floating-promises
extensionManager.disableExtension(extension, scope);
}
@@ -346,17 +346,6 @@ describe('SettingsSchema', () => {
).toBe('Enable preview features (e.g., preview models).');
});
it('should have enableAgents setting in schema', () => {
const setting = getSettingsSchema().experimental.properties.enableAgents;
expect(setting).toBeDefined();
expect(setting.type).toBe('boolean');
expect(setting.category).toBe('Experimental');
expect(setting.default).toBe(false);
expect(setting.requiresRestart).toBe(true);
expect(setting.showInDialog).toBe(false);
expect(setting.description).toBe('Enable local and remote subagents.');
});
it('should have isModelAvailabilityServiceEnabled setting in schema', () => {
const setting =
getSettingsSchema().experimental.properties
+7 -218
View File
@@ -14,12 +14,14 @@ import type {
BugCommandSettings,
TelemetrySettings,
AuthType,
HookDefinition,
HookEventName,
} from '@google/gemini-cli-core';
import {
DEFAULT_TRUNCATE_TOOL_OUTPUT_LINES,
DEFAULT_TRUNCATE_TOOL_OUTPUT_THRESHOLD,
DEFAULT_GEMINI_MODEL,
DEFAULT_MODEL_CONFIGS,
GEMINI_MODEL_ALIAS_AUTO,
} from '@google/gemini-cli-core';
import type { CustomTheme } from '../ui/themes/theme.js';
import type { SessionRetentionSettings } from './settings.js';
@@ -78,11 +80,6 @@ export interface SettingCollectionDefinition {
* For example, a JSON schema generator can use this to point to a shared definition.
*/
ref?: string;
/**
* Optional merge strategy for dynamically added properties.
* Used when this collection definition is referenced via additionalProperties.
*/
mergeStrategy?: MergeStrategy;
}
export enum MergeStrategy {
@@ -1058,7 +1055,7 @@ const SETTINGS_SCHEMA = {
label: 'Enable Message Bus Integration',
category: 'Tools',
requiresRestart: true,
default: true,
default: false,
description: oneLine`
Enable policy-based tool confirmation via message bus integration.
When enabled, tools automatically respect policy engine decisions (ALLOW/DENY/ASK_USER) without requiring individual tool implementations.
@@ -1285,15 +1282,6 @@ const SETTINGS_SCHEMA = {
description: 'Setting to enable experimental features',
showInDialog: false,
properties: {
enableAgents: {
type: 'boolean',
label: 'Enable Agents',
category: 'Experimental',
requiresRestart: true,
default: false,
description: 'Enable local and remote subagents.',
showInDialog: false,
},
extensionManagement: {
type: 'boolean',
label: 'Extension Management',
@@ -1384,7 +1372,7 @@ const SETTINGS_SCHEMA = {
label: 'Model',
category: 'Experimental',
requiresRestart: true,
default: GEMINI_MODEL_ALIAS_AUTO,
default: DEFAULT_GEMINI_MODEL,
description:
'The model to use for the Codebase Investigator agent.',
showInDialog: false,
@@ -1434,165 +1422,11 @@ const SETTINGS_SCHEMA = {
label: 'Hooks',
category: 'Advanced',
requiresRestart: false,
default: {},
default: {} as { [K in HookEventName]?: HookDefinition[] },
description:
'Hook configurations for intercepting and customizing agent behavior.',
showInDialog: false,
properties: {
disabled: {
type: 'array',
label: 'Disabled Hooks',
category: 'Advanced',
requiresRestart: false,
default: [] as string[],
description:
'List of hook names (commands) that should be disabled. Hooks in this list will not execute even if configured.',
showInDialog: false,
items: {
type: 'string',
description: 'Hook command name',
},
mergeStrategy: MergeStrategy.UNION,
},
BeforeTool: {
type: 'array',
label: 'Before Tool Hooks',
category: 'Advanced',
requiresRestart: false,
default: [],
description:
'Hooks that execute before tool execution. Can intercept, validate, or modify tool calls.',
showInDialog: false,
ref: 'HookDefinitionArray',
mergeStrategy: MergeStrategy.CONCAT,
},
AfterTool: {
type: 'array',
label: 'After Tool Hooks',
category: 'Advanced',
requiresRestart: false,
default: [],
description:
'Hooks that execute after tool execution. Can process results, log outputs, or trigger follow-up actions.',
showInDialog: false,
ref: 'HookDefinitionArray',
mergeStrategy: MergeStrategy.CONCAT,
},
BeforeAgent: {
type: 'array',
label: 'Before Agent Hooks',
category: 'Advanced',
requiresRestart: false,
default: [],
description:
'Hooks that execute before agent loop starts. Can set up context or initialize resources.',
showInDialog: false,
ref: 'HookDefinitionArray',
mergeStrategy: MergeStrategy.CONCAT,
},
AfterAgent: {
type: 'array',
label: 'After Agent Hooks',
category: 'Advanced',
requiresRestart: false,
default: [],
description:
'Hooks that execute after agent loop completes. Can perform cleanup or summarize results.',
showInDialog: false,
ref: 'HookDefinitionArray',
mergeStrategy: MergeStrategy.CONCAT,
},
Notification: {
type: 'array',
label: 'Notification Hooks',
category: 'Advanced',
requiresRestart: false,
default: [],
description:
'Hooks that execute on notification events (errors, warnings, info). Can log or alert on specific conditions.',
showInDialog: false,
ref: 'HookDefinitionArray',
mergeStrategy: MergeStrategy.CONCAT,
},
SessionStart: {
type: 'array',
label: 'Session Start Hooks',
category: 'Advanced',
requiresRestart: false,
default: [],
description:
'Hooks that execute when a session starts. Can initialize session-specific resources or state.',
showInDialog: false,
ref: 'HookDefinitionArray',
mergeStrategy: MergeStrategy.CONCAT,
},
SessionEnd: {
type: 'array',
label: 'Session End Hooks',
category: 'Advanced',
requiresRestart: false,
default: [],
description:
'Hooks that execute when a session ends. Can perform cleanup or persist session data.',
showInDialog: false,
ref: 'HookDefinitionArray',
mergeStrategy: MergeStrategy.CONCAT,
},
PreCompress: {
type: 'array',
label: 'Pre-Compress Hooks',
category: 'Advanced',
requiresRestart: false,
default: [],
description:
'Hooks that execute before chat history compression. Can back up or analyze conversation before compression.',
showInDialog: false,
ref: 'HookDefinitionArray',
mergeStrategy: MergeStrategy.CONCAT,
},
BeforeModel: {
type: 'array',
label: 'Before Model Hooks',
category: 'Advanced',
requiresRestart: false,
default: [],
description:
'Hooks that execute before LLM requests. Can modify prompts, inject context, or control model parameters.',
showInDialog: false,
ref: 'HookDefinitionArray',
mergeStrategy: MergeStrategy.CONCAT,
},
AfterModel: {
type: 'array',
label: 'After Model Hooks',
category: 'Advanced',
requiresRestart: false,
default: [],
description:
'Hooks that execute after LLM responses. Can process outputs, extract information, or log interactions.',
showInDialog: false,
ref: 'HookDefinitionArray',
mergeStrategy: MergeStrategy.CONCAT,
},
BeforeToolSelection: {
type: 'array',
label: 'Before Tool Selection Hooks',
category: 'Advanced',
requiresRestart: false,
default: [],
description:
'Hooks that execute before tool selection. Can filter or prioritize available tools dynamically.',
showInDialog: false,
ref: 'HookDefinitionArray',
mergeStrategy: MergeStrategy.CONCAT,
},
},
additionalProperties: {
type: 'array',
description:
'Custom hook event arrays that contain hook definitions for user-defined events',
mergeStrategy: MergeStrategy.CONCAT,
},
mergeStrategy: MergeStrategy.SHALLOW_MERGE,
},
} as const satisfies SettingsSchema;
@@ -1738,11 +1572,6 @@ export const SETTINGS_SCHEMA_DEFINITIONS: Record<
type: 'boolean',
description: 'Whether to forward telemetry to an OTLP collector.',
},
useCliAuth: {
type: 'boolean',
description:
'Whether to use CLI authentication for telemetry (only for in-process exporters).',
},
},
},
BugCommandSettings: {
@@ -1869,46 +1698,6 @@ export const SETTINGS_SCHEMA_DEFINITIONS: Record<
description: 'Accepts either a boolean flag or a string command name.',
anyOf: [{ type: 'boolean' }, { type: 'string' }],
},
HookDefinitionArray: {
type: 'array',
description: 'Array of hook definition objects for a specific event.',
items: {
type: 'object',
description:
'Hook definition specifying matcher pattern and hook configurations.',
properties: {
matcher: {
type: 'string',
description:
'Pattern to match against the event context (tool name, notification type, etc.). Supports exact match, regex (/pattern/), and wildcards (*).',
},
hooks: {
type: 'array',
description: 'Hooks to execute when the matcher matches.',
items: {
type: 'object',
description: 'Individual hook configuration.',
properties: {
type: {
type: 'string',
description:
'Type of hook (currently only "command" supported).',
},
command: {
type: 'string',
description:
'Shell command to execute. Receives JSON input via stdin and returns JSON output via stdout.',
},
timeout: {
type: 'number',
description: 'Timeout in milliseconds for hook execution.',
},
},
},
},
},
},
},
};
export function getSettingsSchema(): SettingsSchemaType {
-11
View File
@@ -257,7 +257,6 @@ describe('gemini.tsx main function', () => {
getMessageBus: () => ({
subscribe: vi.fn(),
}),
getEnableHooks: () => false,
getToolRegistry: vi.fn(),
getContentGeneratorConfig: vi.fn(),
getModel: () => 'gemini-pro',
@@ -490,7 +489,6 @@ describe('gemini.tsx main function kitty protocol', () => {
getMessageBus: () => ({
subscribe: vi.fn(),
}),
getEnableHooks: () => false,
getToolRegistry: vi.fn(),
getContentGeneratorConfig: vi.fn(),
getModel: () => 'gemini-pro',
@@ -590,7 +588,6 @@ describe('gemini.tsx main function kitty protocol', () => {
getExtensions: () => [{ name: 'ext1' }],
getPolicyEngine: vi.fn(),
getMessageBus: () => ({ subscribe: vi.fn() }),
getEnableHooks: () => false,
initialize: vi.fn(),
getContentGeneratorConfig: vi.fn(),
getMcpServers: () => ({}),
@@ -671,7 +668,6 @@ describe('gemini.tsx main function kitty protocol', () => {
getExtensions: () => [],
getPolicyEngine: vi.fn(),
getMessageBus: () => ({ subscribe: vi.fn() }),
getEnableHooks: () => false,
initialize: vi.fn(),
getContentGeneratorConfig: vi.fn(),
getMcpServers: () => ({}),
@@ -737,7 +733,6 @@ describe('gemini.tsx main function kitty protocol', () => {
getDebugMode: () => false,
getPolicyEngine: vi.fn(),
getMessageBus: () => ({ subscribe: vi.fn() }),
getEnableHooks: () => false,
initialize: vi.fn(),
getContentGeneratorConfig: vi.fn(),
getMcpServers: () => ({}),
@@ -819,7 +814,6 @@ describe('gemini.tsx main function kitty protocol', () => {
getDebugMode: () => false,
getPolicyEngine: vi.fn(),
getMessageBus: () => ({ subscribe: vi.fn() }),
getEnableHooks: () => false,
initialize: vi.fn(),
getContentGeneratorConfig: vi.fn(),
getMcpServers: () => ({}),
@@ -896,7 +890,6 @@ describe('gemini.tsx main function kitty protocol', () => {
getDebugMode: () => false,
getPolicyEngine: vi.fn(),
getMessageBus: () => ({ subscribe: vi.fn() }),
getEnableHooks: () => false,
initialize: vi.fn(),
getContentGeneratorConfig: vi.fn(),
getMcpServers: () => ({}),
@@ -968,7 +961,6 @@ describe('gemini.tsx main function kitty protocol', () => {
getDebugMode: () => false,
getPolicyEngine: vi.fn(),
getMessageBus: () => ({ subscribe: vi.fn() }),
getEnableHooks: () => false,
initialize: vi.fn(),
getContentGeneratorConfig: vi.fn(),
getMcpServers: () => ({}),
@@ -1138,7 +1130,6 @@ describe('gemini.tsx main function exit codes', () => {
getGeminiMdFileCount: () => 0,
getPolicyEngine: vi.fn(),
getMessageBus: () => ({ subscribe: vi.fn() }),
getEnableHooks: () => false,
getToolRegistry: vi.fn(),
getContentGeneratorConfig: vi.fn(),
getModel: () => 'gemini-pro',
@@ -1200,7 +1191,6 @@ describe('gemini.tsx main function exit codes', () => {
getGeminiMdFileCount: () => 0,
getPolicyEngine: vi.fn(),
getMessageBus: () => ({ subscribe: vi.fn() }),
getEnableHooks: () => false,
getToolRegistry: vi.fn(),
getContentGeneratorConfig: vi.fn(),
getModel: () => 'gemini-pro',
@@ -1312,7 +1302,6 @@ describe('startInteractiveUI', () => {
registerCleanup: vi.fn(),
runExitCleanup: vi.fn(),
registerSyncCleanup: vi.fn(),
registerTelemetryConfig: vi.fn(),
}));
afterEach(() => {
-47
View File
@@ -31,7 +31,6 @@ import {
registerCleanup,
registerSyncCleanup,
runExitCleanup,
registerTelemetryConfig,
} from './utils/cleanup.js';
import { getCliVersion } from './utils/version.js';
import {
@@ -59,10 +58,6 @@ import {
shouldEnterAlternateScreen,
startupProfiler,
ExitCodes,
SessionStartSource,
SessionEndReason,
fireSessionStartHook,
fireSessionEndHook,
} from '@google/gemini-cli-core';
import {
initializeApp,
@@ -464,22 +459,10 @@ export async function main() {
const config = await loadCliConfig(settings.merged, sessionId, argv);
loadConfigHandle?.end();
// Register config for telemetry shutdown
// This ensures telemetry (including SessionEnd hooks) is properly flushed on exit
registerTelemetryConfig(config);
const policyEngine = config.getPolicyEngine();
const messageBus = config.getMessageBus();
createPolicyUpdater(policyEngine, messageBus);
// Register SessionEnd hook to fire on graceful exit
// This runs before telemetry shutdown in runExitCleanup()
if (config.getEnableHooks() && messageBus) {
registerCleanup(async () => {
await fireSessionEndHook(messageBus, SessionEndReason.Exit);
});
}
// Cleanup sessions after config initialization
try {
await cleanupExpiredSessions(config, settings.merged);
@@ -498,20 +481,6 @@ export async function main() {
// Handle --list-sessions flag
if (config.getListSessions()) {
// Attempt auth for summary generation (gracefully skips if not configured)
const authType = settings.merged.security?.auth?.selectedType;
if (authType) {
try {
await config.refreshAuth(authType);
} catch (e) {
// Auth failed - continue without summary generation capability
debugLogger.debug(
'Auth failed for --list-sessions, summaries may not be generated:',
e,
);
}
}
await listSessions(config);
await runExitCleanup();
process.exit(ExitCodes.SUCCESS);
@@ -617,22 +586,6 @@ export async function main() {
await config.initialize();
startupProfiler.flush(config);
// Fire SessionStart hook through MessageBus (only if hooks are enabled)
// Must be called AFTER config.initialize() to ensure HookRegistry is loaded
const hooksEnabled = config.getEnableHooks();
const hookMessageBus = config.getMessageBus();
if (hooksEnabled && hookMessageBus) {
const sessionStartSource = resumedSessionData
? SessionStartSource.Resume
: SessionStartSource.Startup;
await fireSessionStartHook(hookMessageBus, sessionStartSource);
// Register SessionEnd hook for graceful exit
registerCleanup(async () => {
await fireSessionEndHook(hookMessageBus, SessionEndReason.Exit);
});
}
// If not a TTY, read from stdin
// This is for cases where the user pipes input directly into the command
if (!process.stdin.isTTY) {
-1
View File
@@ -186,7 +186,6 @@ describe('gemini.tsx main function cleanup', () => {
getDebugMode: vi.fn(() => false),
getPolicyEngine: vi.fn(),
getMessageBus: () => ({ subscribe: vi.fn() }),
getEnableHooks: vi.fn(() => false),
initialize: vi.fn(),
getContentGeneratorConfig: vi.fn(),
getMcpServers: () => ({}),
+8 -27
View File
@@ -16,6 +16,7 @@ import type {
import {
executeToolCall,
ToolErrorType,
shutdownTelemetry,
GeminiEventType,
OutputFormat,
uiTelemetryService,
@@ -60,6 +61,7 @@ vi.mock('@google/gemini-cli-core', async (importOriginal) => {
return {
...original,
executeToolCall: vi.fn(),
shutdownTelemetry: vi.fn(),
isTelemetrySdkInitialized: vi.fn().mockReturnValue(true),
ChatRecordingService: MockChatRecordingService,
uiTelemetryService: {
@@ -89,6 +91,7 @@ describe('runNonInteractive', () => {
let mockSettings: LoadedSettings;
let mockToolRegistry: ToolRegistry;
let mockCoreExecuteToolCall: Mock;
let mockShutdownTelemetry: Mock;
let consoleErrorSpy: MockInstance;
let processStdoutSpy: MockInstance;
let processStderrSpy: MockInstance;
@@ -120,6 +123,7 @@ describe('runNonInteractive', () => {
beforeEach(async () => {
mockCoreExecuteToolCall = vi.mocked(executeToolCall);
mockShutdownTelemetry = vi.mocked(shutdownTelemetry);
mockCommandServiceCreate.mockResolvedValue({
getCommands: mockGetCommands,
@@ -243,8 +247,7 @@ describe('runNonInteractive', () => {
'prompt-id-1',
);
expect(getWrittenOutput()).toBe('Hello World\n');
// Note: Telemetry shutdown is now handled in runExitCleanup() in cleanup.ts
// so we no longer expect shutdownTelemetry to be called directly here
expect(mockShutdownTelemetry).toHaveBeenCalled();
});
it('should handle a single tool call and respond', async () => {
@@ -637,11 +640,7 @@ describe('runNonInteractive', () => {
);
expect(processStdoutSpy).toHaveBeenCalledWith(
JSON.stringify(
{
session_id: 'test-session-id',
response: 'Hello World',
stats: MOCK_SESSION_METRICS,
},
{ response: 'Hello World', stats: MOCK_SESSION_METRICS },
null,
2,
),
@@ -724,15 +723,7 @@ describe('runNonInteractive', () => {
// This should output JSON with empty response but include stats
expect(processStdoutSpy).toHaveBeenCalledWith(
JSON.stringify(
{
session_id: 'test-session-id',
response: '',
stats: MOCK_SESSION_METRICS,
},
null,
2,
),
JSON.stringify({ response: '', stats: MOCK_SESSION_METRICS }, null, 2),
);
});
@@ -767,15 +758,7 @@ describe('runNonInteractive', () => {
// This should output JSON with empty response but include stats
expect(processStdoutSpy).toHaveBeenCalledWith(
JSON.stringify(
{
session_id: 'test-session-id',
response: '',
stats: MOCK_SESSION_METRICS,
},
null,
2,
),
JSON.stringify({ response: '', stats: MOCK_SESSION_METRICS }, null, 2),
);
});
@@ -812,7 +795,6 @@ describe('runNonInteractive', () => {
expect(consoleErrorJsonSpy).toHaveBeenCalledWith(
JSON.stringify(
{
session_id: 'test-session-id',
error: {
type: 'Error',
message: 'Invalid input provided',
@@ -858,7 +840,6 @@ describe('runNonInteractive', () => {
expect(consoleErrorJsonSpy).toHaveBeenCalledWith(
JSON.stringify(
{
session_id: 'test-session-id',
error: {
type: 'FatalInputError',
message: 'Invalid command syntax provided',

Some files were not shown because too many files have changed in this diff Show More