mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-08-03 13:41:05 -07:00
Compare commits
122 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 2519a7850a | |||
| 3ff055840e | |||
| 42a36294a8 | |||
| ecbab46394 | |||
| 17fb758664 | |||
| 518cc1ab63 | |||
| b7ad7e1035 | |||
| dbcad90661 | |||
| 703d2e0dcc | |||
| 7d8ab08adb | |||
| bc40695ce4 | |||
| 646dc3154b | |||
| 7b772e9dfb | |||
| ef1e18a85a | |||
| 58fd00a3df | |||
| b923604602 | |||
| 8643d60b88 | |||
| 8feeffb29b | |||
| e64146914a | |||
| 6084708cc2 | |||
| 8ed0f8981f | |||
| 41a1a3eed1 | |||
| 10ba348a3a | |||
| db67bb106a | |||
| b828b4754b | |||
| 9383b54d50 | |||
| 181da07dd9 | |||
| 419464a8c2 | |||
| 2e229d3bb6 | |||
| 3c92bdb1ad | |||
| e0f159085e | |||
| 402148dbc4 | |||
| 70696e364b | |||
| 1e10492e55 | |||
| 9e6914d641 | |||
| 0c4fb6afd2 | |||
| edab97997a | |||
| 2515b89e2b | |||
| 60f0f19d76 | |||
| 322232e514 | |||
| 54466a3ea8 | |||
| 7da060c149 | |||
| 088f48d60f | |||
| 2b426c1d91 | |||
| bc168bbae4 | |||
| 124a6da743 | |||
| 739c02bd6d | |||
| 5d13145995 | |||
| c28ff3d5a5 | |||
| 6ddd5abd7b | |||
| bf6d0485ce | |||
| 3d486ec1e9 | |||
| a6d1245a54 | |||
| de7e1937f6 | |||
| b465e12747 | |||
| 948401a450 | |||
| bb8f181ef1 | |||
| bf90b59935 | |||
| 18698d6929 | |||
| 80c4225286 | |||
| d02f3f6809 | |||
| da85aed5aa | |||
| 7f2d33458a | |||
| 3e9a0a7628 | |||
| 26c115a4fb | |||
| db643e9166 | |||
| ba100642e3 | |||
| cc52839f19 | |||
| e79b149985 | |||
| 5e21c8c03c | |||
| 5ea5107d05 | |||
| 2995af6a21 | |||
| 0c3eb82671 | |||
| 79f664d593 | |||
| bb0c0d8ee3 | |||
| d236df5b21 | |||
| 13944b9bb1 | |||
| ec665ef405 | |||
| 217e2b0eb4 | |||
| d030a1f62f | |||
| fc2a5a42ce | |||
| 57c7b9ccce | |||
| 942bcfc61e | |||
| fcc3b2b5ec | |||
| d9f94103cd | |||
| ad60cbfc2c | |||
| 126c32aca4 | |||
| 977248e095 | |||
| a47af8e261 | |||
| 5b56920f0a | |||
| 5f298c17d7 | |||
| d2a1a45646 | |||
| 6dea66f1f5 | |||
| 20164ebcda | |||
| edbe5480ca | |||
| e84c4bfb81 | |||
| 3b2a4ba27a | |||
| 12cbe320e4 | |||
| 1e734d7e60 | |||
| 299cc9bebf | |||
| a02abcf578 | |||
| bb33e281c0 | |||
| 86134e9970 | |||
| 54de67536d | |||
| d818fb1d91 | |||
| 927102ea1b | |||
| 2d3db97067 | |||
| 24fca1b745 | |||
| 8c83e1ea9a | |||
| 91b15fc9dc | |||
| d4506e0fc0 | |||
| 648041c62a | |||
| 22e6af414a | |||
| c8b688655c | |||
| 68ebf5d655 | |||
| 1954f45c19 | |||
| ee6556cbd2 | |||
| d2a6b30398 | |||
| d90356e8a3 | |||
| aced401071 | |||
| 2800187355 | |||
| 3f5f030d01 |
Binary file not shown.
|
After Width: | Height: | Size: 387 KiB |
@@ -0,0 +1,29 @@
|
||||
description="Injects context of all relevant cli files"
|
||||
prompt = """
|
||||
The following output contains the complete
|
||||
source code of packages/core.
|
||||
**Pay extremely close attention to these files.** They define the project's
|
||||
core architecture, component patterns, and testing standards.
|
||||
|
||||
The source code contains the content of absolutely every source code file in
|
||||
packages/core.
|
||||
You should very rarely need to read any other files from packages/core to resolve
|
||||
prompts.
|
||||
|
||||
!{find packages/core \\( -path packages/cli/dist -o -path packages/core/dist -o -name node_modules \\) -prune -o -type f \\( -name "*.ts" -o -name "*.tsx" \\) ! -name "*.test.ts" ! -name "*.test.tsx" ! -name "*.d.ts" -exec echo "--- {} ---" \\; -exec cat {} \\;}
|
||||
|
||||
In addition to the code context, you MUST strictly adhere to the following frontend-specific development guidelines when writing code in packages/core.
|
||||
|
||||
## Configuration & Settings
|
||||
* **Settings vs Args**: Use settings for user-configurable options; do not add new CLI arguments.
|
||||
* **Schema**: Add new settings to `packages/cli/src/config/settingsSchema.ts`.
|
||||
* **Documentation**:
|
||||
* If `showInDialog: true`, document in `docs/get-started/configuration.md`.
|
||||
* Ensure `requiresRestart` is correctly set.
|
||||
|
||||
## General
|
||||
* **Logging**: Use `debugLogger` for errors. NEVER leave `console.log/warn/error` in the code.
|
||||
* **TypeScript**: Avoid the non-null assertion operator (`!`).
|
||||
|
||||
{{args}}.
|
||||
"""
|
||||
@@ -1,13 +1,13 @@
|
||||
description="Injects context of all relevant cli files"
|
||||
prompt = """
|
||||
The source code contains the content of absolutely every source code file in
|
||||
packages/cli. In addition to the source code, the following test files are
|
||||
packages/cli and key files from packages/core. In addition to the source code, the following test files are
|
||||
included as they are test files that conform to the project's testing standards:
|
||||
`packages/cli/src/ui/components/InputPrompt.test.tsx` and `packages/cli/src/ui/App.test.tsx`.
|
||||
You should very rarely need to read any other files from packages/cli to resolve
|
||||
prompts.
|
||||
|
||||
!{find packages/cli -path packages/cli/dist -prune -o -type f \\( -name "*.ts" -o -name "*.tsx" \\) ! -name "*.test.ts" ! -name "*.test.tsx" ! -name "*.d.ts" -exec echo "--- {} ---" \\; -exec cat {} \\; && echo "--- packages/cli/src/ui/components/InputPrompt.test.tsx ---" && cat packages/cli/src/ui/components/InputPrompt.test.tsx && echo "--- packages/cli/src/ui/App.test.tsx ---" && cat packages/cli/src/ui/App.test.tsx}
|
||||
!{find packages/cli -path packages/cli/dist -prune -o -type f \\( -name "*.ts" -o -name "*.tsx" \\) ! -name "*.test.ts" ! -name "*.test.tsx" ! -name "*.d.ts" -exec echo "--- {} ---" \\; -exec cat {} \\; && echo "--- packages/cli/src/ui/components/InputPrompt.test.tsx ---" && cat packages/cli/src/ui/components/InputPrompt.test.tsx && echo "--- packages/cli/src/ui/App.test.tsx ---" && cat packages/cli/src/ui/App.test.tsx && echo "--- packages/core/src/core/coreToolScheduler.ts ---" && cat packages/core/src/core/coreToolScheduler.ts && echo "--- packages/core/src/core/nonInteractiveToolExecutor.ts ---" && cat packages/core/src/core/nonInteractiveToolExecutor.ts && echo "--- packages/core/src/confirmation-bus/message-bus.ts ---" && cat packages/core/src/confirmation-bus/message-bus.ts && echo "--- packages/core/src/confirmation-bus/types.ts ---" && cat packages/core/src/confirmation-bus/types.ts && echo "--- packages/core/src/utils/events.ts ---" && cat packages/core/src/utils/events.ts && echo "--- packages/core/src/core/client.ts ---" && cat packages/core/src/core/client.ts && echo "--- packages/core/src/core/geminiChat.ts ---" && cat packages/core/src/core/geminiChat.ts && echo "--- packages/core/src/core/turn.ts ---" && cat packages/core/src/core/turn.ts && echo "--- packages/core/src/agents/executor.ts ---" && cat packages/core/src/agents/executor.ts && echo "--- packages/core/src/telemetry/types.ts ---" && cat packages/core/src/telemetry/types.ts && echo "--- packages/core/src/telemetry/loggers.ts ---" && cat packages/core/src/telemetry/loggers.ts && echo "--- packages/core/src/telemetry/uiTelemetry.ts ---" && cat packages/core/src/telemetry/uiTelemetry.ts}
|
||||
|
||||
**Pay extremely close attention to these files.** They define the project's
|
||||
core architecture, component patterns, and testing standards.
|
||||
|
||||
@@ -6,7 +6,7 @@ on:
|
||||
|
||||
jobs:
|
||||
label-pr:
|
||||
timeout-minutes: 5
|
||||
timeout-minutes: 10
|
||||
if: |-
|
||||
${{ github.repository == 'google-gemini/gemini-cli' }}
|
||||
permissions:
|
||||
@@ -32,7 +32,7 @@ jobs:
|
||||
- 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 }}'
|
||||
GH_TOKEN: '${{ steps.generate_token.outputs.token }}'
|
||||
PR_NUMBER: '${{ github.event.pull_request.number }}'
|
||||
REPOSITORY: '${{ github.repository }}'
|
||||
with:
|
||||
|
||||
@@ -142,7 +142,8 @@ jobs:
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: issue.number,
|
||||
state: 'closed'
|
||||
state: 'closed',
|
||||
state_reason: 'not_planned'
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -55,3 +55,4 @@ gha-creds-*.json
|
||||
patch_output.log
|
||||
|
||||
.genkit
|
||||
.gemini-clipboard/
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
*.log
|
||||
+3
-1
@@ -41,7 +41,9 @@ This project follows
|
||||
|
||||
The process for contributing code is as follows:
|
||||
|
||||
1. **Find an issue** that you want to work on.
|
||||
1. **Find an issue** that you want to work on. If an issue is tagged as
|
||||
"🔒Maintainers only", this means it is reserved for project maintainers. We
|
||||
will not accept pull requests related to these issues.
|
||||
2. **Fork the repository** and create a new branch.
|
||||
3. **Make your changes** in the `packages/` directory.
|
||||
4. **Ensure all checks pass** by running `npm run preflight`.
|
||||
|
||||
@@ -18,6 +18,41 @@ on GitHub.
|
||||
| [Preview](preview.md) | Experimental features ready for early feedback. |
|
||||
| [Stable](latest.md) | Stable, recommended for general use. |
|
||||
|
||||
## Announcements: v0.21.0 - 2025-12-15
|
||||
|
||||
- **⚡️⚡️⚡️ Gemini 3 Flash + Gemini CLI:** Better, faster and cheaper than 2.5
|
||||
Pro - and in some scenarios better than 3 Pro! For paid tiers + free tier
|
||||
users who were on the wait list enable **Preview Features** in `/settings.`
|
||||
- For more information:
|
||||
[Gemini 3 Flash is now available in Gemini CLI](https://developers.googleblog.com/gemini-3-flash-is-now-available-in-gemini-cli/).
|
||||
- 🎉 Gemini CLI Extensions:
|
||||
- Rill: Utilize natural language to analyze Rill data, enabling the
|
||||
exploration of metrics and trends without the need for manual queries.
|
||||
`gemini extensions install https://github.com/rilldata/rill-gemini-extension`
|
||||
- Browserbase: Interact with web pages, take screenshots, extract information,
|
||||
and perform automated actions with atomic precision.
|
||||
`gemini extensions install https://github.com/browserbase/mcp-server-browserbase`
|
||||
- Quota Visibility: The `/stats` command now displays quota information for all
|
||||
available models, including those not used in the current session. (@sehoon38)
|
||||
- Fuzzy Setting Search: Users can now quickly find settings using fuzzy search
|
||||
within the settings dialog. (@sehoon38)
|
||||
- MCP Resource Support: Users can now discover, view, and search through
|
||||
resources using the @ command. (@MrLesk)
|
||||
- Auto-execute Simple Slash Commands: Simple slash commands are now executed
|
||||
immediately on enter. (@jackwotherspoon)
|
||||
|
||||
## Announcements: v0.20.0 - 2025-12-01
|
||||
|
||||
- **Multi-file Drag & Drop:** Users can now drag and drop multiple files into
|
||||
the terminal, and the CLI will automatically prefix each valid path with `@`.
|
||||
([pr](https://github.com/google-gemini/gemini-cli/pull/14832) by
|
||||
[@jackwotherspoon](https://github.com/jackwotherspoon))
|
||||
- **Persistent "Always Allow" Policies:** Users can now save "Always Allow"
|
||||
decisions for tool executions, with granular control over specific shell
|
||||
commands and multi-cloud platform tools.
|
||||
([pr](https://github.com/google-gemini/gemini-cli/pull/14737) by
|
||||
[@allenhutchison](https://github.com/allenhutchison))
|
||||
|
||||
## Announcements: v0.19.0 - 2025-11-24
|
||||
|
||||
- 🎉 **New extensions:**
|
||||
@@ -54,8 +89,7 @@ on GitHub.
|
||||
- **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.
|
||||
[policy engine documentation](../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
|
||||
|
||||
+209
-138
@@ -1,6 +1,6 @@
|
||||
# Latest stable release: v0.19.0 - v0.19.4
|
||||
# Latest stable release: v0.21.0 - v0.21.1
|
||||
|
||||
Released: December 1, 2025
|
||||
Released: December 16, 2025
|
||||
|
||||
For most users, our latest stable release is the recommended release. Install
|
||||
the latest stable version with:
|
||||
@@ -11,144 +11,215 @@ 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 CLI’s `/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))
|
||||
- **⚡️⚡️⚡️ Gemini 3 Flash + Gemini CLI:** If you are a paid user, you can now
|
||||
enable Gemini 3 Pro and Gemini 3 Flash. Go to `/settings` and set **Preview
|
||||
Features** to `true` to enable Gemini 3. For more information:
|
||||
[Gemini 3 Flash is now available in Gemini CLI](https://developers.googleblog.com/gemini-3-flash-is-now-available-in-gemini-cli/).
|
||||
|
||||
## 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
|
||||
- refactor(stdio): always patch stdout and use createWorkingStdio for clean
|
||||
output by @allenhutchison in
|
||||
https://github.com/google-gemini/gemini-cli/pull/14159
|
||||
- chore(release): bump version to 0.21.0-nightly.20251202.2d935b379 by
|
||||
@gemini-cli-robot in https://github.com/google-gemini/gemini-cli/pull/14409
|
||||
- implement fuzzy search inside settings by @sehoon38 in
|
||||
https://github.com/google-gemini/gemini-cli/pull/13864
|
||||
- feat: enable message bus integration by default by @allenhutchison in
|
||||
https://github.com/google-gemini/gemini-cli/pull/14329
|
||||
- docs: Recommend using --debug intead of --verbose for CLI debugging by @bbiggs
|
||||
in https://github.com/google-gemini/gemini-cli/pull/14334
|
||||
- feat: consolidate remote MCP servers to use `url` in config by
|
||||
@jackwotherspoon in https://github.com/google-gemini/gemini-cli/pull/13762
|
||||
- Restrict integration tests tools by @scidomino in
|
||||
https://github.com/google-gemini/gemini-cli/pull/14403
|
||||
- track github repository names in telemetry events by @IamRiddhi in
|
||||
https://github.com/google-gemini/gemini-cli/pull/13670
|
||||
- Allow telemetry exporters to GCP to utilize user's login credentials, if
|
||||
requested by @mboshernitsan in
|
||||
https://github.com/google-gemini/gemini-cli/pull/13778
|
||||
- refactor(editor): use const assertion for editor types with single source of
|
||||
truth by @amsminn in https://github.com/google-gemini/gemini-cli/pull/8604
|
||||
- fix(security): Fix npm audit vulnerabilities in glob and body-parser by
|
||||
@afarber in https://github.com/google-gemini/gemini-cli/pull/14090
|
||||
- Add new enterprise instructions by @chrstnb in
|
||||
https://github.com/google-gemini/gemini-cli/pull/8641
|
||||
- feat(hooks): Hook Session Lifecycle & Compression Integration by @Edilmo in
|
||||
https://github.com/google-gemini/gemini-cli/pull/14151
|
||||
- Avoid triggering refreshStatic unless there really is a banner to display. by
|
||||
@jacob314 in https://github.com/google-gemini/gemini-cli/pull/14328
|
||||
- feat(hooks): Hooks Commands Panel, Enable/Disable, and Migrate by @Edilmo in
|
||||
https://github.com/google-gemini/gemini-cli/pull/14225
|
||||
- fix: Bundle default policies for npx distribution by @allenhutchison in
|
||||
https://github.com/google-gemini/gemini-cli/pull/14457
|
||||
- feat(hooks): Hook System Documentation by @Edilmo in
|
||||
https://github.com/google-gemini/gemini-cli/pull/14307
|
||||
- Fix tests by @scidomino in
|
||||
https://github.com/google-gemini/gemini-cli/pull/14458
|
||||
- feat: add scheduled workflow to close stale issues by @galz10 in
|
||||
https://github.com/google-gemini/gemini-cli/pull/14404
|
||||
- feat: Support Extension Hooks with Security Warning by @abhipatel12 in
|
||||
https://github.com/google-gemini/gemini-cli/pull/14460
|
||||
- feat: Add enableAgents experimental flag by @adamfweidman in
|
||||
https://github.com/google-gemini/gemini-cli/pull/14371
|
||||
- docs: fix typo 'socus' to 'focus' in todos.md by @Viktor286 in
|
||||
https://github.com/google-gemini/gemini-cli/pull/14374
|
||||
- Markdown export: move the emoji to the end of the line by @mhansen in
|
||||
https://github.com/google-gemini/gemini-cli/pull/12278
|
||||
- fix(acp): prevent unnecessary credential cache clearing on re-authent… by
|
||||
@h-michael in https://github.com/google-gemini/gemini-cli/pull/9410
|
||||
- fix(cli): Fix word navigation for CJK characters by @SandyTao520 in
|
||||
https://github.com/google-gemini/gemini-cli/pull/14475
|
||||
- Remove example extension by @chrstnb in
|
||||
https://github.com/google-gemini/gemini-cli/pull/14376
|
||||
- Add commands for listing and updating per-extension settings by @chrstnb in
|
||||
https://github.com/google-gemini/gemini-cli/pull/12664
|
||||
- chore(tests): remove obsolete test for hierarchical memory by @pareshjoshij in
|
||||
https://github.com/google-gemini/gemini-cli/pull/13122
|
||||
- feat(cli): support /copy in remote sessions using OSC52 by @ismellpillows in
|
||||
https://github.com/google-gemini/gemini-cli/pull/13471
|
||||
- Update setting search UX by @Adib234 in
|
||||
https://github.com/google-gemini/gemini-cli/pull/14451
|
||||
- Fix(cli): Improve Homebrew update instruction to specify gemini-cli by
|
||||
@DaanVersavel in https://github.com/google-gemini/gemini-cli/pull/14502
|
||||
- do not toggle the setting item when entering space by @sehoon38 in
|
||||
https://github.com/google-gemini/gemini-cli/pull/14489
|
||||
- fix: improve retry logic for fetch errors and network codes by @megha1188 in
|
||||
https://github.com/google-gemini/gemini-cli/pull/14439
|
||||
- remove unused isSearching field by @sehoon38 in
|
||||
https://github.com/google-gemini/gemini-cli/pull/14509
|
||||
- feat(mcp): add `--type` alias for `--transport` flag in gemini mcp add by
|
||||
@jackwotherspoon in https://github.com/google-gemini/gemini-cli/pull/14503
|
||||
- feat(cli): Move key restore logic to core by @cocosheng-g in
|
||||
https://github.com/google-gemini/gemini-cli/pull/13013
|
||||
- feat: add auto-execute on Enter behavior to argumentless MCP prompts by
|
||||
@jackwotherspoon in https://github.com/google-gemini/gemini-cli/pull/14510
|
||||
- fix(shell): cursor visibility when using interactive mode by @aswinashok44 in
|
||||
https://github.com/google-gemini/gemini-cli/pull/14095
|
||||
- Adding session id as part of json o/p by @MJjainam in
|
||||
https://github.com/google-gemini/gemini-cli/pull/14504
|
||||
- fix(extensions): resolve GitHub API 415 error for source tarballs by
|
||||
@jpoehnelt in https://github.com/google-gemini/gemini-cli/pull/13319
|
||||
- fix(client): Correctly latch hasFailedCompressionAttempt flag by @pareshjoshij
|
||||
in https://github.com/google-gemini/gemini-cli/pull/13002
|
||||
- Disable flaky extension reloading test on linux by @chrstnb in
|
||||
https://github.com/google-gemini/gemini-cli/pull/14528
|
||||
- Add support for MCP dynamic tool update by `notifications/tools/list_changed`
|
||||
by @Adib234 in https://github.com/google-gemini/gemini-cli/pull/14375
|
||||
- Fix privacy screen for legacy tier users by @scidomino in
|
||||
https://github.com/google-gemini/gemini-cli/pull/14522
|
||||
- feat: Exclude maintainer labeled issues from stale issue closer by @galz10 in
|
||||
https://github.com/google-gemini/gemini-cli/pull/14532
|
||||
- Grant chained workflows proper permission. by @scidomino in
|
||||
https://github.com/google-gemini/gemini-cli/pull/14534
|
||||
- Make trigger_e2e manually fireable. by @scidomino in
|
||||
https://github.com/google-gemini/gemini-cli/pull/14547
|
||||
- Write e2e status to local repo not forked repo by @scidomino in
|
||||
https://github.com/google-gemini/gemini-cli/pull/14549
|
||||
- Fixes [API Error: Cannot read properties of undefined (reading 'error')] by
|
||||
@silviojr in https://github.com/google-gemini/gemini-cli/pull/14553
|
||||
- Trigger chained e2e tests on all pull requests by @scidomino in
|
||||
https://github.com/google-gemini/gemini-cli/pull/14551
|
||||
- Fix bug in the shellExecutionService resulting in both truncation and 3X bloat
|
||||
by @jacob314 in https://github.com/google-gemini/gemini-cli/pull/14545
|
||||
- Fix issue where we were passing the model content reflecting terminal line
|
||||
wrapping. by @jacob314 in
|
||||
https://github.com/google-gemini/gemini-cli/pull/14566
|
||||
- chore/release: bump version to 0.21.0-nightly.20251204.3da4fd5f7 by
|
||||
@gemini-cli-robot in https://github.com/google-gemini/gemini-cli/pull/14476
|
||||
- feat(sessions): use 1-line generated session summary to describe sessions by
|
||||
@jackwotherspoon in https://github.com/google-gemini/gemini-cli/pull/14467
|
||||
- Use Robot PAT for chained e2e merge queue skipper by @scidomino in
|
||||
https://github.com/google-gemini/gemini-cli/pull/14585
|
||||
- fix(core): improve API response error handling and retry logic by @mattKorwel
|
||||
in https://github.com/google-gemini/gemini-cli/pull/14563
|
||||
- Docs: Model routing clarification by @jkcinouye in
|
||||
https://github.com/google-gemini/gemini-cli/pull/14373
|
||||
- expose previewFeatures flag in a2a by @sehoon38 in
|
||||
https://github.com/google-gemini/gemini-cli/pull/14550
|
||||
- Fix emoji width in debug console. by @jacob314 in
|
||||
https://github.com/google-gemini/gemini-cli/pull/14593
|
||||
- Fully detach autoupgrade process by @scidomino in
|
||||
https://github.com/google-gemini/gemini-cli/pull/14595
|
||||
- Docs: Update Gemini 3 on Gemini CLI documentation by @jkcinouye in
|
||||
https://github.com/google-gemini/gemini-cli/pull/14601
|
||||
- Disallow floating promises. by @gundermanc in
|
||||
https://github.com/google-gemini/gemini-cli/pull/14605
|
||||
- chore/release: bump version to 0.21.0-nightly.20251207.025e450ac by
|
||||
@gemini-cli-robot in https://github.com/google-gemini/gemini-cli/pull/14662
|
||||
- feat(modelAvailabilityService): integrate model availability service into
|
||||
backend logic by @adamfweidman in
|
||||
https://github.com/google-gemini/gemini-cli/pull/14470
|
||||
- Add prompt_id propagation in a2a-server task by @koxkox111 in
|
||||
https://github.com/google-gemini/gemini-cli/pull/14581
|
||||
- Fix: Prevent freezing in non-interactive Gemini CLI when debug mode is enabled
|
||||
by @parthasaradhie in https://github.com/google-gemini/gemini-cli/pull/14580
|
||||
- fix(audio): improve reading of audio files by @jackwotherspoon in
|
||||
https://github.com/google-gemini/gemini-cli/pull/14658
|
||||
- Update automated triage workflow to stop assigning priority labels by
|
||||
@skeshive in https://github.com/google-gemini/gemini-cli/pull/14717
|
||||
- set failed status when chained e2e fails by @scidomino in
|
||||
https://github.com/google-gemini/gemini-cli/pull/14725
|
||||
- feat(github action) Triage and Label Pull Requests by Size and Comple… by
|
||||
@DaanVersavel in https://github.com/google-gemini/gemini-cli/pull/5571
|
||||
- refactor(telemetry): Improve previous PR that allows telemetry to use the CLI
|
||||
auth and add testing by @mboshernitsan in
|
||||
https://github.com/google-gemini/gemini-cli/pull/14589
|
||||
- Always set status in chained_e2e workflow by @scidomino in
|
||||
https://github.com/google-gemini/gemini-cli/pull/14730
|
||||
- feat: Add OTEL log event `gemini_cli.startup_stats` for startup stats. by
|
||||
@kevin-ramdass in https://github.com/google-gemini/gemini-cli/pull/14734
|
||||
- feat: auto-execute on slash command completion functions by @jackwotherspoon
|
||||
in https://github.com/google-gemini/gemini-cli/pull/14584
|
||||
- Docs: Proper release notes by @jkcinouye in
|
||||
https://github.com/google-gemini/gemini-cli/pull/14405
|
||||
- Add support for user-scoped extension settings by @chrstnb in
|
||||
https://github.com/google-gemini/gemini-cli/pull/13748
|
||||
- refactor(core): Improve environment variable handling in shell execution by
|
||||
@galz10 in https://github.com/google-gemini/gemini-cli/pull/14742
|
||||
- Remove old E2E Workflows by @scidomino in
|
||||
https://github.com/google-gemini/gemini-cli/pull/14749
|
||||
- fix: handle missing local extension config and skip hooks when disabled by
|
||||
@abhipatel12 in https://github.com/google-gemini/gemini-cli/pull/14744
|
||||
- chore/release: bump version to 0.21.0-nightly.20251209.ec9a8c7a7 by
|
||||
@gemini-cli-robot in https://github.com/google-gemini/gemini-cli/pull/14751
|
||||
- feat: Add support for MCP Resources by @MrLesk in
|
||||
https://github.com/google-gemini/gemini-cli/pull/13178
|
||||
- Always set pending status in E2E tests by @scidomino in
|
||||
https://github.com/google-gemini/gemini-cli/pull/14756
|
||||
- fix(lint): upgrade pip and use public pypi for yamllint by @allenhutchison in
|
||||
https://github.com/google-gemini/gemini-cli/pull/14746
|
||||
- fix: use Gemini API supported image formats for clipboard by @jackwotherspoon
|
||||
in https://github.com/google-gemini/gemini-cli/pull/14762
|
||||
- feat(a2a): Introduce restore command for a2a server by @cocosheng-g in
|
||||
https://github.com/google-gemini/gemini-cli/pull/13015
|
||||
- allow final:true to be returned on a2a server edit calls. by @DavidAPierce in
|
||||
https://github.com/google-gemini/gemini-cli/pull/14747
|
||||
- (fix) Automated pr labeller by @DaanVersavel in
|
||||
https://github.com/google-gemini/gemini-cli/pull/14788
|
||||
- Update CODEOWNERS by @kklashtorny1 in
|
||||
https://github.com/google-gemini/gemini-cli/pull/14830
|
||||
- Docs: Fix errors preventing site rebuild. by @jkcinouye in
|
||||
https://github.com/google-gemini/gemini-cli/pull/14842
|
||||
- chore(deps): bump express from 5.1.0 to 5.2.0 by @dependabot[bot] in
|
||||
https://github.com/google-gemini/gemini-cli/pull/14325
|
||||
- fix(patch): cherry-pick 3f5f030 to release/v0.21.0-preview.0-pr-14843 to patch
|
||||
version v0.21.0-preview.0 and create version 0.21.0-preview.1 by
|
||||
@gemini-cli-robot in https://github.com/google-gemini/gemini-cli/pull/14851
|
||||
- fix(patch): cherry-pick ee6556c to release/v0.21.0-preview.1-pr-14691 to patch
|
||||
version v0.21.0-preview.1 and create version 0.21.0-preview.2 by
|
||||
@gemini-cli-robot in https://github.com/google-gemini/gemini-cli/pull/14908
|
||||
- fix(patch): cherry-pick 54de675 to release/v0.21.0-preview.2-pr-14961 by
|
||||
@gemini-cli-robot in https://github.com/google-gemini/gemini-cli/pull/14968
|
||||
- fix(patch): cherry-pick 12cbe32 to release/v0.21.0-preview.3-pr-15000 to patch
|
||||
version v0.21.0-preview.3 and create version 0.21.0-preview.4 by
|
||||
@gemini-cli-robot in https://github.com/google-gemini/gemini-cli/pull/15003
|
||||
- fix(patch): cherry-pick edbe548 to release/v0.21.0-preview.4-pr-15007 to patch
|
||||
version v0.21.0-preview.4 and create version 0.21.0-preview.5 by
|
||||
@gemini-cli-robot in https://github.com/google-gemini/gemini-cli/pull/15015
|
||||
- fix(patch): cherry-pick 2995af6 to release/v0.21.0-preview.5-pr-15131 to patch
|
||||
version v0.21.0-preview.5 and create version 0.21.0-preview.6 by
|
||||
@gemini-cli-robot in https://github.com/google-gemini/gemini-cli/pull/15153
|
||||
|
||||
**Full Changelog**:
|
||||
https://github.com/google-gemini/gemini-cli/compare/v0.18.4...v0.19.0
|
||||
https://github.com/google-gemini/gemini-cli/compare/v0.20.2...v0.21.0
|
||||
|
||||
+115
-127
@@ -1,6 +1,6 @@
|
||||
# Preview release: Release v0.19.0-preview.0
|
||||
# Preview release: Release v0.22.0-preview.0
|
||||
|
||||
Released: November 25, 2025
|
||||
Released: December 16, 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).
|
||||
@@ -11,131 +11,119 @@ To install the preview release:
|
||||
npm install -g @google/gemini-cli@preview
|
||||
```
|
||||
|
||||
## What's changed
|
||||
## 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
|
||||
- feat(ide): fallback to GEMINI_CLI_IDE_AUTH_TOKEN env var by @skeshive in
|
||||
https://github.com/google-gemini/gemini-cli/pull/14843
|
||||
- feat: display quota stats for unused models in /stats by @sehoon38 in
|
||||
https://github.com/google-gemini/gemini-cli/pull/14764
|
||||
- feat: ensure codebase investigator uses preview model when main agent does by
|
||||
@abhipatel12 in https://github.com/google-gemini/gemini-cli/pull/14412
|
||||
- chore: add closing reason to stale bug workflow by @galz10 in
|
||||
https://github.com/google-gemini/gemini-cli/pull/14861
|
||||
- Send the model and CLI version with the user agent by @gundermanc in
|
||||
https://github.com/google-gemini/gemini-cli/pull/14865
|
||||
- refactor(sessions): move session summary generation to startup by
|
||||
@jackwotherspoon in https://github.com/google-gemini/gemini-cli/pull/14691
|
||||
- Limit search depth in path corrector by @scidomino in
|
||||
https://github.com/google-gemini/gemini-cli/pull/14869
|
||||
- Fix: Correct typo in code comment by @kuishou68 in
|
||||
https://github.com/google-gemini/gemini-cli/pull/14671
|
||||
- feat(core): Plumbing for late resolution of model configs. by @joshualitt in
|
||||
https://github.com/google-gemini/gemini-cli/pull/14597
|
||||
- feat: attempt more error parsing by @adamfweidman in
|
||||
https://github.com/google-gemini/gemini-cli/pull/14899
|
||||
- Add missing await. by @gundermanc in
|
||||
https://github.com/google-gemini/gemini-cli/pull/14910
|
||||
- feat(core): Add support for transcript_path in hooks for git-ai/Gemini
|
||||
extension by @svarlamov in
|
||||
https://github.com/google-gemini/gemini-cli/pull/14663
|
||||
- refactor: implement DelegateToAgentTool with discriminated union by
|
||||
@abhipatel12 in https://github.com/google-gemini/gemini-cli/pull/14769
|
||||
- feat: reset availabilityService on /auth by @adamfweidman in
|
||||
https://github.com/google-gemini/gemini-cli/pull/14911
|
||||
- chore/release: bump version to 0.21.0-nightly.20251211.8c83e1ea9 by
|
||||
@gemini-cli-robot in https://github.com/google-gemini/gemini-cli/pull/14924
|
||||
- Fix: Correctly detect MCP tool errors by @kevin-ramdass in
|
||||
https://github.com/google-gemini/gemini-cli/pull/14937
|
||||
- increase labeler timeout by @scidomino in
|
||||
https://github.com/google-gemini/gemini-cli/pull/14922
|
||||
- tool(cli): tweak the frontend tool to be aware of more core files from the cli
|
||||
by @jacob314 in https://github.com/google-gemini/gemini-cli/pull/14962
|
||||
- feat(cli): polish cached token stats and simplify stats display when quota is
|
||||
present. by @jacob314 in
|
||||
https://github.com/google-gemini/gemini-cli/pull/14961
|
||||
- feat(settings-validation): add validation for settings schema by @lifefloating
|
||||
in https://github.com/google-gemini/gemini-cli/pull/12929
|
||||
- fix(ide): Update IDE extension to write auth token in env var by @skeshive in
|
||||
https://github.com/google-gemini/gemini-cli/pull/14999
|
||||
- Revert "chore(deps): bump express from 5.1.0 to 5.2.0" by @skeshive in
|
||||
https://github.com/google-gemini/gemini-cli/pull/14998
|
||||
- feat(a2a): Introduce /init command for a2a server by @cocosheng-g in
|
||||
https://github.com/google-gemini/gemini-cli/pull/13419
|
||||
- feat: support multi-file drag and drop of images by @jackwotherspoon in
|
||||
https://github.com/google-gemini/gemini-cli/pull/14832
|
||||
- fix(policy): allow codebase_investigator by default in read-only policy by
|
||||
@abhipatel12 in https://github.com/google-gemini/gemini-cli/pull/15000
|
||||
- refactor(ide ext): Update port file name + switch to 1-based index for
|
||||
characters + remove truncation text by @skeshive in
|
||||
https://github.com/google-gemini/gemini-cli/pull/10501
|
||||
- fix(vscode-ide-companion): correct license generation for workspace
|
||||
dependencies by @skeshive in
|
||||
https://github.com/google-gemini/gemini-cli/pull/15004
|
||||
- fix: temp fix for subagent invocation until subagent delegation is merged to
|
||||
stable by @abhipatel12 in
|
||||
https://github.com/google-gemini/gemini-cli/pull/15007
|
||||
- test: update ide detection tests to make them more robust when run in an ide
|
||||
by @kevin-ramdass in https://github.com/google-gemini/gemini-cli/pull/15008
|
||||
- Remove flex from stats display. See snapshots for diffs. by @jacob314 in
|
||||
https://github.com/google-gemini/gemini-cli/pull/14983
|
||||
- Add license field into package.json by @jb-perez in
|
||||
https://github.com/google-gemini/gemini-cli/pull/14473
|
||||
- feat: Persistent "Always Allow" policies with granular shell & MCP support by
|
||||
@allenhutchison in https://github.com/google-gemini/gemini-cli/pull/14737
|
||||
- chore/release: bump version to 0.21.0-nightly.20251212.54de67536 by
|
||||
@gemini-cli-robot in https://github.com/google-gemini/gemini-cli/pull/14969
|
||||
- fix(core): commandPrefix word boundary and compound command safety by
|
||||
@allenhutchison in https://github.com/google-gemini/gemini-cli/pull/15006
|
||||
- chore(docs): add 'Maintainers only' label info to CONTRIBUTING.md by @jacob314
|
||||
in https://github.com/google-gemini/gemini-cli/pull/14914
|
||||
- Refresh hooks when refreshing extensions. by @scidomino in
|
||||
https://github.com/google-gemini/gemini-cli/pull/14918
|
||||
- Add clarity to error messages by @gsehgal in
|
||||
https://github.com/google-gemini/gemini-cli/pull/14879
|
||||
- chore : remove a redundant tip by @JayadityaGit in
|
||||
https://github.com/google-gemini/gemini-cli/pull/14947
|
||||
- chore/release: bump version to 0.21.0-nightly.20251213.977248e09 by
|
||||
@gemini-cli-robot in https://github.com/google-gemini/gemini-cli/pull/15029
|
||||
- Disallow redundant typecasts. by @gundermanc in
|
||||
https://github.com/google-gemini/gemini-cli/pull/15030
|
||||
- fix(auth): prioritize GEMINI_API_KEY env var and skip unnecessary key… by
|
||||
@galz10 in https://github.com/google-gemini/gemini-cli/pull/14745
|
||||
- fix: use zod for safety check result validation by @allenhutchison in
|
||||
https://github.com/google-gemini/gemini-cli/pull/15026
|
||||
- update(telemetry): add hashed_extension_name to field to extension events by
|
||||
@kiranani in https://github.com/google-gemini/gemini-cli/pull/15025
|
||||
- fix: similar to policy-engine, throw error in case of requiring tool execution
|
||||
confirmation for non-interactive mode by @MayV in
|
||||
https://github.com/google-gemini/gemini-cli/pull/14702
|
||||
- Clean up processes in integration tests by @scidomino in
|
||||
https://github.com/google-gemini/gemini-cli/pull/15102
|
||||
- docs: update policy engine getting started and defaults by @NTaylorMullen in
|
||||
https://github.com/google-gemini/gemini-cli/pull/15105
|
||||
- Fix tool output fragmentation by encapsulating content in functionResponse by
|
||||
@abhipatel12 in https://github.com/google-gemini/gemini-cli/pull/13082
|
||||
- Simplify method signature. by @scidomino in
|
||||
https://github.com/google-gemini/gemini-cli/pull/15114
|
||||
- Show raw input token counts in json output. by @jacob314 in
|
||||
https://github.com/google-gemini/gemini-cli/pull/15021
|
||||
- fix: Mark A2A requests as interactive by @MayV in
|
||||
https://github.com/google-gemini/gemini-cli/pull/15108
|
||||
- use previewFeatures to determine which pro model to use for A2A by @sehoon38
|
||||
in https://github.com/google-gemini/gemini-cli/pull/15131
|
||||
- refactor(cli): fix settings merging so that settings using the new json format
|
||||
take priority over ones using the old format by @jacob314 in
|
||||
https://github.com/google-gemini/gemini-cli/pull/15116
|
||||
|
||||
**Full Changelog**:
|
||||
https://github.com/google-gemini/gemini-cli/compare/v0.18.0-preview.4...v0.19.0-preview.0
|
||||
https://github.com/google-gemini/gemini-cli/compare/v0.21.0-preview.6...v0.22.0-preview.0
|
||||
|
||||
+475
-9
@@ -12,13 +12,479 @@ 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 channel | Notes |
|
||||
| :------------------------------------------ | :---------------------------------------------- |
|
||||
| Nightly | Nightly release with the most recent changes. |
|
||||
| [Preview](#release-v0220-preview-0-preview) | Experimental features ready for early feedback. |
|
||||
| [Latest](#release-v0210---v0211-latest) | Stable, recommended for general use. |
|
||||
|
||||
## Release v0.19.0 - v0.19.4 (Latest)
|
||||
## Release v0.21.0 - v0.21.1 (Latest)
|
||||
|
||||
### Highlights
|
||||
|
||||
- **⚡️⚡️⚡️ Gemini 3 Flash + Gemini CLI:** If you are a paid user, you can now
|
||||
enable Gemini 3 Pro and Gemini 3 Flash. Go to `/settings` and set **Preview
|
||||
Features** to `true` to enable Gemini 3. For more information:
|
||||
[Gemini 3 Flash is now available in Gemini CLI](https://developers.googleblog.com/gemini-3-flash-is-now-available-in-gemini-cli/).
|
||||
|
||||
### What's Changed
|
||||
|
||||
- refactor(stdio): always patch stdout and use createWorkingStdio for clean
|
||||
output by @allenhutchison in
|
||||
https://github.com/google-gemini/gemini-cli/pull/14159
|
||||
- chore(release): bump version to 0.21.0-nightly.20251202.2d935b379 by
|
||||
@gemini-cli-robot in https://github.com/google-gemini/gemini-cli/pull/14409
|
||||
- implement fuzzy search inside settings by @sehoon38 in
|
||||
https://github.com/google-gemini/gemini-cli/pull/13864
|
||||
- feat: enable message bus integration by default by @allenhutchison in
|
||||
https://github.com/google-gemini/gemini-cli/pull/14329
|
||||
- docs: Recommend using --debug intead of --verbose for CLI debugging by @bbiggs
|
||||
in https://github.com/google-gemini/gemini-cli/pull/14334
|
||||
- feat: consolidate remote MCP servers to use `url` in config by
|
||||
@jackwotherspoon in https://github.com/google-gemini/gemini-cli/pull/13762
|
||||
- Restrict integration tests tools by @scidomino in
|
||||
https://github.com/google-gemini/gemini-cli/pull/14403
|
||||
- track github repository names in telemetry events by @IamRiddhi in
|
||||
https://github.com/google-gemini/gemini-cli/pull/13670
|
||||
- Allow telemetry exporters to GCP to utilize user's login credentials, if
|
||||
requested by @mboshernitsan in
|
||||
https://github.com/google-gemini/gemini-cli/pull/13778
|
||||
- refactor(editor): use const assertion for editor types with single source of
|
||||
truth by @amsminn in https://github.com/google-gemini/gemini-cli/pull/8604
|
||||
- fix(security): Fix npm audit vulnerabilities in glob and body-parser by
|
||||
@afarber in https://github.com/google-gemini/gemini-cli/pull/14090
|
||||
- Add new enterprise instructions by @chrstnb in
|
||||
https://github.com/google-gemini/gemini-cli/pull/8641
|
||||
- feat(hooks): Hook Session Lifecycle & Compression Integration by @Edilmo in
|
||||
https://github.com/google-gemini/gemini-cli/pull/14151
|
||||
- Avoid triggering refreshStatic unless there really is a banner to display. by
|
||||
@jacob314 in https://github.com/google-gemini/gemini-cli/pull/14328
|
||||
- feat(hooks): Hooks Commands Panel, Enable/Disable, and Migrate by @Edilmo in
|
||||
https://github.com/google-gemini/gemini-cli/pull/14225
|
||||
- fix: Bundle default policies for npx distribution by @allenhutchison in
|
||||
https://github.com/google-gemini/gemini-cli/pull/14457
|
||||
- feat(hooks): Hook System Documentation by @Edilmo in
|
||||
https://github.com/google-gemini/gemini-cli/pull/14307
|
||||
- Fix tests by @scidomino in
|
||||
https://github.com/google-gemini/gemini-cli/pull/14458
|
||||
- feat: add scheduled workflow to close stale issues by @galz10 in
|
||||
https://github.com/google-gemini/gemini-cli/pull/14404
|
||||
- feat: Support Extension Hooks with Security Warning by @abhipatel12 in
|
||||
https://github.com/google-gemini/gemini-cli/pull/14460
|
||||
- feat: Add enableAgents experimental flag by @adamfweidman in
|
||||
https://github.com/google-gemini/gemini-cli/pull/14371
|
||||
- docs: fix typo 'socus' to 'focus' in todos.md by @Viktor286 in
|
||||
https://github.com/google-gemini/gemini-cli/pull/14374
|
||||
- Markdown export: move the emoji to the end of the line by @mhansen in
|
||||
https://github.com/google-gemini/gemini-cli/pull/12278
|
||||
- fix(acp): prevent unnecessary credential cache clearing on re-authent… by
|
||||
@h-michael in https://github.com/google-gemini/gemini-cli/pull/9410
|
||||
- fix(cli): Fix word navigation for CJK characters by @SandyTao520 in
|
||||
https://github.com/google-gemini/gemini-cli/pull/14475
|
||||
- Remove example extension by @chrstnb in
|
||||
https://github.com/google-gemini/gemini-cli/pull/14376
|
||||
- Add commands for listing and updating per-extension settings by @chrstnb in
|
||||
https://github.com/google-gemini/gemini-cli/pull/12664
|
||||
- chore(tests): remove obsolete test for hierarchical memory by @pareshjoshij in
|
||||
https://github.com/google-gemini/gemini-cli/pull/13122
|
||||
- feat(cli): support /copy in remote sessions using OSC52 by @ismellpillows in
|
||||
https://github.com/google-gemini/gemini-cli/pull/13471
|
||||
- Update setting search UX by @Adib234 in
|
||||
https://github.com/google-gemini/gemini-cli/pull/14451
|
||||
- Fix(cli): Improve Homebrew update instruction to specify gemini-cli by
|
||||
@DaanVersavel in https://github.com/google-gemini/gemini-cli/pull/14502
|
||||
- do not toggle the setting item when entering space by @sehoon38 in
|
||||
https://github.com/google-gemini/gemini-cli/pull/14489
|
||||
- fix: improve retry logic for fetch errors and network codes by @megha1188 in
|
||||
https://github.com/google-gemini/gemini-cli/pull/14439
|
||||
- remove unused isSearching field by @sehoon38 in
|
||||
https://github.com/google-gemini/gemini-cli/pull/14509
|
||||
- feat(mcp): add `--type` alias for `--transport` flag in gemini mcp add by
|
||||
@jackwotherspoon in https://github.com/google-gemini/gemini-cli/pull/14503
|
||||
- feat(cli): Move key restore logic to core by @cocosheng-g in
|
||||
https://github.com/google-gemini/gemini-cli/pull/13013
|
||||
- feat: add auto-execute on Enter behavior to argumentless MCP prompts by
|
||||
@jackwotherspoon in https://github.com/google-gemini/gemini-cli/pull/14510
|
||||
- fix(shell): cursor visibility when using interactive mode by @aswinashok44 in
|
||||
https://github.com/google-gemini/gemini-cli/pull/14095
|
||||
- Adding session id as part of json o/p by @MJjainam in
|
||||
https://github.com/google-gemini/gemini-cli/pull/14504
|
||||
- fix(extensions): resolve GitHub API 415 error for source tarballs by
|
||||
@jpoehnelt in https://github.com/google-gemini/gemini-cli/pull/13319
|
||||
- fix(client): Correctly latch hasFailedCompressionAttempt flag by @pareshjoshij
|
||||
in https://github.com/google-gemini/gemini-cli/pull/13002
|
||||
- Disable flaky extension reloading test on linux by @chrstnb in
|
||||
https://github.com/google-gemini/gemini-cli/pull/14528
|
||||
- Add support for MCP dynamic tool update by `notifications/tools/list_changed`
|
||||
by @Adib234 in https://github.com/google-gemini/gemini-cli/pull/14375
|
||||
- Fix privacy screen for legacy tier users by @scidomino in
|
||||
https://github.com/google-gemini/gemini-cli/pull/14522
|
||||
- feat: Exclude maintainer labeled issues from stale issue closer by @galz10 in
|
||||
https://github.com/google-gemini/gemini-cli/pull/14532
|
||||
- Grant chained workflows proper permission. by @scidomino in
|
||||
https://github.com/google-gemini/gemini-cli/pull/14534
|
||||
- Make trigger_e2e manually fireable. by @scidomino in
|
||||
https://github.com/google-gemini/gemini-cli/pull/14547
|
||||
- Write e2e status to local repo not forked repo by @scidomino in
|
||||
https://github.com/google-gemini/gemini-cli/pull/14549
|
||||
- Fixes [API Error: Cannot read properties of undefined (reading 'error')] by
|
||||
@silviojr in https://github.com/google-gemini/gemini-cli/pull/14553
|
||||
- Trigger chained e2e tests on all pull requests by @scidomino in
|
||||
https://github.com/google-gemini/gemini-cli/pull/14551
|
||||
- Fix bug in the shellExecutionService resulting in both truncation and 3X bloat
|
||||
by @jacob314 in https://github.com/google-gemini/gemini-cli/pull/14545
|
||||
- Fix issue where we were passing the model content reflecting terminal line
|
||||
wrapping. by @jacob314 in
|
||||
https://github.com/google-gemini/gemini-cli/pull/14566
|
||||
- chore/release: bump version to 0.21.0-nightly.20251204.3da4fd5f7 by
|
||||
@gemini-cli-robot in https://github.com/google-gemini/gemini-cli/pull/14476
|
||||
- feat(sessions): use 1-line generated session summary to describe sessions by
|
||||
@jackwotherspoon in https://github.com/google-gemini/gemini-cli/pull/14467
|
||||
- Use Robot PAT for chained e2e merge queue skipper by @scidomino in
|
||||
https://github.com/google-gemini/gemini-cli/pull/14585
|
||||
- fix(core): improve API response error handling and retry logic by @mattKorwel
|
||||
in https://github.com/google-gemini/gemini-cli/pull/14563
|
||||
- Docs: Model routing clarification by @jkcinouye in
|
||||
https://github.com/google-gemini/gemini-cli/pull/14373
|
||||
- expose previewFeatures flag in a2a by @sehoon38 in
|
||||
https://github.com/google-gemini/gemini-cli/pull/14550
|
||||
- Fix emoji width in debug console. by @jacob314 in
|
||||
https://github.com/google-gemini/gemini-cli/pull/14593
|
||||
- Fully detach autoupgrade process by @scidomino in
|
||||
https://github.com/google-gemini/gemini-cli/pull/14595
|
||||
- Docs: Update Gemini 3 on Gemini CLI documentation by @jkcinouye in
|
||||
https://github.com/google-gemini/gemini-cli/pull/14601
|
||||
- Disallow floating promises. by @gundermanc in
|
||||
https://github.com/google-gemini/gemini-cli/pull/14605
|
||||
- chore/release: bump version to 0.21.0-nightly.20251207.025e450ac by
|
||||
@gemini-cli-robot in https://github.com/google-gemini/gemini-cli/pull/14662
|
||||
- feat(modelAvailabilityService): integrate model availability service into
|
||||
backend logic by @adamfweidman in
|
||||
https://github.com/google-gemini/gemini-cli/pull/14470
|
||||
- Add prompt_id propagation in a2a-server task by @koxkox111 in
|
||||
https://github.com/google-gemini/gemini-cli/pull/14581
|
||||
- Fix: Prevent freezing in non-interactive Gemini CLI when debug mode is enabled
|
||||
by @parthasaradhie in https://github.com/google-gemini/gemini-cli/pull/14580
|
||||
- fix(audio): improve reading of audio files by @jackwotherspoon in
|
||||
https://github.com/google-gemini/gemini-cli/pull/14658
|
||||
- Update automated triage workflow to stop assigning priority labels by
|
||||
@skeshive in https://github.com/google-gemini/gemini-cli/pull/14717
|
||||
- set failed status when chained e2e fails by @scidomino in
|
||||
https://github.com/google-gemini/gemini-cli/pull/14725
|
||||
- feat(github action) Triage and Label Pull Requests by Size and Comple… by
|
||||
@DaanVersavel in https://github.com/google-gemini/gemini-cli/pull/5571
|
||||
- refactor(telemetry): Improve previous PR that allows telemetry to use the CLI
|
||||
auth and add testing by @mboshernitsan in
|
||||
https://github.com/google-gemini/gemini-cli/pull/14589
|
||||
- Always set status in chained_e2e workflow by @scidomino in
|
||||
https://github.com/google-gemini/gemini-cli/pull/14730
|
||||
- feat: Add OTEL log event `gemini_cli.startup_stats` for startup stats. by
|
||||
@kevin-ramdass in https://github.com/google-gemini/gemini-cli/pull/14734
|
||||
- feat: auto-execute on slash command completion functions by @jackwotherspoon
|
||||
in https://github.com/google-gemini/gemini-cli/pull/14584
|
||||
- Docs: Proper release notes by @jkcinouye in
|
||||
https://github.com/google-gemini/gemini-cli/pull/14405
|
||||
- Add support for user-scoped extension settings by @chrstnb in
|
||||
https://github.com/google-gemini/gemini-cli/pull/13748
|
||||
- refactor(core): Improve environment variable handling in shell execution by
|
||||
@galz10 in https://github.com/google-gemini/gemini-cli/pull/14742
|
||||
- Remove old E2E Workflows by @scidomino in
|
||||
https://github.com/google-gemini/gemini-cli/pull/14749
|
||||
- fix: handle missing local extension config and skip hooks when disabled by
|
||||
@abhipatel12 in https://github.com/google-gemini/gemini-cli/pull/14744
|
||||
- chore/release: bump version to 0.21.0-nightly.20251209.ec9a8c7a7 by
|
||||
@gemini-cli-robot in https://github.com/google-gemini/gemini-cli/pull/14751
|
||||
- feat: Add support for MCP Resources by @MrLesk in
|
||||
https://github.com/google-gemini/gemini-cli/pull/13178
|
||||
- Always set pending status in E2E tests by @scidomino in
|
||||
https://github.com/google-gemini/gemini-cli/pull/14756
|
||||
- fix(lint): upgrade pip and use public pypi for yamllint by @allenhutchison in
|
||||
https://github.com/google-gemini/gemini-cli/pull/14746
|
||||
- fix: use Gemini API supported image formats for clipboard by @jackwotherspoon
|
||||
in https://github.com/google-gemini/gemini-cli/pull/14762
|
||||
- feat(a2a): Introduce restore command for a2a server by @cocosheng-g in
|
||||
https://github.com/google-gemini/gemini-cli/pull/13015
|
||||
- allow final:true to be returned on a2a server edit calls. by @DavidAPierce in
|
||||
https://github.com/google-gemini/gemini-cli/pull/14747
|
||||
- (fix) Automated pr labeller by @DaanVersavel in
|
||||
https://github.com/google-gemini/gemini-cli/pull/14788
|
||||
- Update CODEOWNERS by @kklashtorny1 in
|
||||
https://github.com/google-gemini/gemini-cli/pull/14830
|
||||
- Docs: Fix errors preventing site rebuild. by @jkcinouye in
|
||||
https://github.com/google-gemini/gemini-cli/pull/14842
|
||||
- chore(deps): bump express from 5.1.0 to 5.2.0 by @dependabot[bot] in
|
||||
https://github.com/google-gemini/gemini-cli/pull/14325
|
||||
- fix(patch): cherry-pick 3f5f030 to release/v0.21.0-preview.0-pr-14843 to patch
|
||||
version v0.21.0-preview.0 and create version 0.21.0-preview.1 by
|
||||
@gemini-cli-robot in https://github.com/google-gemini/gemini-cli/pull/14851
|
||||
- fix(patch): cherry-pick ee6556c to release/v0.21.0-preview.1-pr-14691 to patch
|
||||
version v0.21.0-preview.1 and create version 0.21.0-preview.2 by
|
||||
@gemini-cli-robot in https://github.com/google-gemini/gemini-cli/pull/14908
|
||||
- fix(patch): cherry-pick 54de675 to release/v0.21.0-preview.2-pr-14961 by
|
||||
@gemini-cli-robot in https://github.com/google-gemini/gemini-cli/pull/14968
|
||||
- fix(patch): cherry-pick 12cbe32 to release/v0.21.0-preview.3-pr-15000 to patch
|
||||
version v0.21.0-preview.3 and create version 0.21.0-preview.4 by
|
||||
@gemini-cli-robot in https://github.com/google-gemini/gemini-cli/pull/15003
|
||||
- fix(patch): cherry-pick edbe548 to release/v0.21.0-preview.4-pr-15007 to patch
|
||||
version v0.21.0-preview.4 and create version 0.21.0-preview.5 by
|
||||
@gemini-cli-robot in https://github.com/google-gemini/gemini-cli/pull/15015
|
||||
- fix(patch): cherry-pick 2995af6 to release/v0.21.0-preview.5-pr-15131 to patch
|
||||
version v0.21.0-preview.5 and create version 0.21.0-preview.6 by
|
||||
@gemini-cli-robot in https://github.com/google-gemini/gemini-cli/pull/15153
|
||||
|
||||
**Full Changelog**:
|
||||
https://github.com/google-gemini/gemini-cli/compare/v0.20.2...v0.21.0
|
||||
|
||||
## Release v0.22.0-preview-0 (Preview)
|
||||
|
||||
### What's Changed
|
||||
|
||||
- feat(ide): fallback to GEMINI_CLI_IDE_AUTH_TOKEN env var by @skeshive in
|
||||
https://github.com/google-gemini/gemini-cli/pull/14843
|
||||
- feat: display quota stats for unused models in /stats by @sehoon38 in
|
||||
https://github.com/google-gemini/gemini-cli/pull/14764
|
||||
- feat: ensure codebase investigator uses preview model when main agent does by
|
||||
@abhipatel12 in https://github.com/google-gemini/gemini-cli/pull/14412
|
||||
- chore: add closing reason to stale bug workflow by @galz10 in
|
||||
https://github.com/google-gemini/gemini-cli/pull/14861
|
||||
- Send the model and CLI version with the user agent by @gundermanc in
|
||||
https://github.com/google-gemini/gemini-cli/pull/14865
|
||||
- refactor(sessions): move session summary generation to startup by
|
||||
@jackwotherspoon in https://github.com/google-gemini/gemini-cli/pull/14691
|
||||
- Limit search depth in path corrector by @scidomino in
|
||||
https://github.com/google-gemini/gemini-cli/pull/14869
|
||||
- Fix: Correct typo in code comment by @kuishou68 in
|
||||
https://github.com/google-gemini/gemini-cli/pull/14671
|
||||
- feat(core): Plumbing for late resolution of model configs. by @joshualitt in
|
||||
https://github.com/google-gemini/gemini-cli/pull/14597
|
||||
- feat: attempt more error parsing by @adamfweidman in
|
||||
https://github.com/google-gemini/gemini-cli/pull/14899
|
||||
- Add missing await. by @gundermanc in
|
||||
https://github.com/google-gemini/gemini-cli/pull/14910
|
||||
- feat(core): Add support for transcript_path in hooks for git-ai/Gemini
|
||||
extension by @svarlamov in
|
||||
https://github.com/google-gemini/gemini-cli/pull/14663
|
||||
- refactor: implement DelegateToAgentTool with discriminated union by
|
||||
@abhipatel12 in https://github.com/google-gemini/gemini-cli/pull/14769
|
||||
- feat: reset availabilityService on /auth by @adamfweidman in
|
||||
https://github.com/google-gemini/gemini-cli/pull/14911
|
||||
- chore/release: bump version to 0.21.0-nightly.20251211.8c83e1ea9 by
|
||||
@gemini-cli-robot in https://github.com/google-gemini/gemini-cli/pull/14924
|
||||
- Fix: Correctly detect MCP tool errors by @kevin-ramdass in
|
||||
https://github.com/google-gemini/gemini-cli/pull/14937
|
||||
- increase labeler timeout by @scidomino in
|
||||
https://github.com/google-gemini/gemini-cli/pull/14922
|
||||
- tool(cli): tweak the frontend tool to be aware of more core files from the cli
|
||||
by @jacob314 in https://github.com/google-gemini/gemini-cli/pull/14962
|
||||
- feat(cli): polish cached token stats and simplify stats display when quota is
|
||||
present. by @jacob314 in
|
||||
https://github.com/google-gemini/gemini-cli/pull/14961
|
||||
- feat(settings-validation): add validation for settings schema by @lifefloating
|
||||
in https://github.com/google-gemini/gemini-cli/pull/12929
|
||||
- fix(ide): Update IDE extension to write auth token in env var by @skeshive in
|
||||
https://github.com/google-gemini/gemini-cli/pull/14999
|
||||
- Revert "chore(deps): bump express from 5.1.0 to 5.2.0" by @skeshive in
|
||||
https://github.com/google-gemini/gemini-cli/pull/14998
|
||||
- feat(a2a): Introduce /init command for a2a server by @cocosheng-g in
|
||||
https://github.com/google-gemini/gemini-cli/pull/13419
|
||||
- feat: support multi-file drag and drop of images by @jackwotherspoon in
|
||||
https://github.com/google-gemini/gemini-cli/pull/14832
|
||||
- fix(policy): allow codebase_investigator by default in read-only policy by
|
||||
@abhipatel12 in https://github.com/google-gemini/gemini-cli/pull/15000
|
||||
- refactor(ide ext): Update port file name + switch to 1-based index for
|
||||
characters + remove truncation text by @skeshive in
|
||||
https://github.com/google-gemini/gemini-cli/pull/10501
|
||||
- fix(vscode-ide-companion): correct license generation for workspace
|
||||
dependencies by @skeshive in
|
||||
https://github.com/google-gemini/gemini-cli/pull/15004
|
||||
- fix: temp fix for subagent invocation until subagent delegation is merged to
|
||||
stable by @abhipatel12 in
|
||||
https://github.com/google-gemini/gemini-cli/pull/15007
|
||||
- test: update ide detection tests to make them more robust when run in an ide
|
||||
by @kevin-ramdass in https://github.com/google-gemini/gemini-cli/pull/15008
|
||||
- Remove flex from stats display. See snapshots for diffs. by @jacob314 in
|
||||
https://github.com/google-gemini/gemini-cli/pull/14983
|
||||
- Add license field into package.json by @jb-perez in
|
||||
https://github.com/google-gemini/gemini-cli/pull/14473
|
||||
- feat: Persistent "Always Allow" policies with granular shell & MCP support by
|
||||
@allenhutchison in https://github.com/google-gemini/gemini-cli/pull/14737
|
||||
- chore/release: bump version to 0.21.0-nightly.20251212.54de67536 by
|
||||
@gemini-cli-robot in https://github.com/google-gemini/gemini-cli/pull/14969
|
||||
- fix(core): commandPrefix word boundary and compound command safety by
|
||||
@allenhutchison in https://github.com/google-gemini/gemini-cli/pull/15006
|
||||
- chore(docs): add 'Maintainers only' label info to CONTRIBUTING.md by @jacob314
|
||||
in https://github.com/google-gemini/gemini-cli/pull/14914
|
||||
- Refresh hooks when refreshing extensions. by @scidomino in
|
||||
https://github.com/google-gemini/gemini-cli/pull/14918
|
||||
- Add clarity to error messages by @gsehgal in
|
||||
https://github.com/google-gemini/gemini-cli/pull/14879
|
||||
- chore : remove a redundant tip by @JayadityaGit in
|
||||
https://github.com/google-gemini/gemini-cli/pull/14947
|
||||
- chore/release: bump version to 0.21.0-nightly.20251213.977248e09 by
|
||||
@gemini-cli-robot in https://github.com/google-gemini/gemini-cli/pull/15029
|
||||
- Disallow redundant typecasts. by @gundermanc in
|
||||
https://github.com/google-gemini/gemini-cli/pull/15030
|
||||
- fix(auth): prioritize GEMINI_API_KEY env var and skip unnecessary key… by
|
||||
@galz10 in https://github.com/google-gemini/gemini-cli/pull/14745
|
||||
- fix: use zod for safety check result validation by @allenhutchison in
|
||||
https://github.com/google-gemini/gemini-cli/pull/15026
|
||||
- update(telemetry): add hashed_extension_name to field to extension events by
|
||||
@kiranani in https://github.com/google-gemini/gemini-cli/pull/15025
|
||||
- fix: similar to policy-engine, throw error in case of requiring tool execution
|
||||
confirmation for non-interactive mode by @MayV in
|
||||
https://github.com/google-gemini/gemini-cli/pull/14702
|
||||
- Clean up processes in integration tests by @scidomino in
|
||||
https://github.com/google-gemini/gemini-cli/pull/15102
|
||||
- docs: update policy engine getting started and defaults by @NTaylorMullen in
|
||||
https://github.com/google-gemini/gemini-cli/pull/15105
|
||||
- Fix tool output fragmentation by encapsulating content in functionResponse by
|
||||
@abhipatel12 in https://github.com/google-gemini/gemini-cli/pull/13082
|
||||
- Simplify method signature. by @scidomino in
|
||||
https://github.com/google-gemini/gemini-cli/pull/15114
|
||||
- Show raw input token counts in json output. by @jacob314 in
|
||||
https://github.com/google-gemini/gemini-cli/pull/15021
|
||||
- fix: Mark A2A requests as interactive by @MayV in
|
||||
https://github.com/google-gemini/gemini-cli/pull/15108
|
||||
- use previewFeatures to determine which pro model to use for A2A by @sehoon38
|
||||
in https://github.com/google-gemini/gemini-cli/pull/15131
|
||||
- refactor(cli): fix settings merging so that settings using the new json format
|
||||
take priority over ones using the old format by @jacob314 in
|
||||
https://github.com/google-gemini/gemini-cli/pull/15116
|
||||
|
||||
**Full Changelog**:
|
||||
https://github.com/google-gemini/gemini-cli/compare/v0.21.0-preview.6...v0.22.0-preview.0
|
||||
|
||||
## Release v0.20.0 - v0.20.2
|
||||
|
||||
### What's Changed
|
||||
|
||||
- Update error codes when process exiting the gemini cli by @megha1188 in
|
||||
https://github.com/google-gemini/gemini-cli/pull/13728
|
||||
- chore(release): bump version to 0.20.0-nightly.20251126.d2a6cff4d by
|
||||
@gemini-cli-robot in https://github.com/google-gemini/gemini-cli/pull/13835
|
||||
- feat(core): Improve request token calculation accuracy by @SandyTao520 in
|
||||
https://github.com/google-gemini/gemini-cli/pull/13824
|
||||
- Changes in system instruction to adapt to gemini 3.0 to ensure that the CLI
|
||||
explains its actions before calling tools by @silviojr in
|
||||
https://github.com/google-gemini/gemini-cli/pull/13810
|
||||
- feat(hooks): Hook Tool Execution Integration by @Edilmo in
|
||||
https://github.com/google-gemini/gemini-cli/pull/9108
|
||||
- Add support for MCP server instructions behind config option by @chrstnb in
|
||||
https://github.com/google-gemini/gemini-cli/pull/13432
|
||||
- Update System Instructions for interactive vs non-interactive mode. by
|
||||
@aishaneeshah in https://github.com/google-gemini/gemini-cli/pull/12315
|
||||
- Add consent flag to Link command by @kevinjwang1 in
|
||||
https://github.com/google-gemini/gemini-cli/pull/13832
|
||||
- feat(mcp): Inject GoogleCredentialProvider headers in McpClient by
|
||||
@sai-sunder-s in https://github.com/google-gemini/gemini-cli/pull/13783
|
||||
- feat(core): implement towards policy-driven model fallback mechanism by
|
||||
@adamfweidman in https://github.com/google-gemini/gemini-cli/pull/13781
|
||||
- feat(core): Add configurable inactivity timeout for shell commands by @galz10
|
||||
in https://github.com/google-gemini/gemini-cli/pull/13531
|
||||
- fix(auth): improve API key authentication flow by @galz10 in
|
||||
https://github.com/google-gemini/gemini-cli/pull/13829
|
||||
- feat(hooks): Hook LLM Request/Response Integration by @Edilmo in
|
||||
https://github.com/google-gemini/gemini-cli/pull/9110
|
||||
- feat(ui): Show waiting MCP servers in ConfigInitDisplay by @werdnum in
|
||||
https://github.com/google-gemini/gemini-cli/pull/13721
|
||||
- Add usage limit remaining in /stats by @sehoon38 in
|
||||
https://github.com/google-gemini/gemini-cli/pull/13843
|
||||
- feat(shell): Standardize pager to 'cat' for shell execution by model by
|
||||
@galz10 in https://github.com/google-gemini/gemini-cli/pull/13878
|
||||
- chore/release: bump version to 0.20.0-nightly.20251127.5bed97064 by
|
||||
@gemini-cli-robot in https://github.com/google-gemini/gemini-cli/pull/13877
|
||||
- Revert to default LICENSE (Revert #13449) by @scidomino in
|
||||
https://github.com/google-gemini/gemini-cli/pull/13876
|
||||
- update(telemetry): OTel API response event with finish reasons by @kiranani in
|
||||
https://github.com/google-gemini/gemini-cli/pull/13849
|
||||
- feat(hooks): Hooks Comprehensive Integration Testing by @Edilmo in
|
||||
https://github.com/google-gemini/gemini-cli/pull/9112
|
||||
- chore: fix session browser test and skip hook system tests by @jackwotherspoon
|
||||
in https://github.com/google-gemini/gemini-cli/pull/14099
|
||||
- feat(telemetry): Add Semantic logging for to ApiRequestEvents by @kiranani in
|
||||
https://github.com/google-gemini/gemini-cli/pull/13912
|
||||
- test: Add verification for $schema property in settings schema by
|
||||
@maryamariyan in https://github.com/google-gemini/gemini-cli/pull/13497
|
||||
- Fixes `/clear` command to preserve input history for up-arrow navigation while
|
||||
still clearing the context window and screen by @korade-krushna in
|
||||
https://github.com/google-gemini/gemini-cli/pull/14182
|
||||
- fix(core): handle EPIPE error in hook runner when writing to stdin by
|
||||
@SandyTao520 in https://github.com/google-gemini/gemini-cli/pull/14231
|
||||
- fix: Exclude web-fetch tool from executing in default non-interactive mode to
|
||||
avoid CLI hang. by @MayV in
|
||||
https://github.com/google-gemini/gemini-cli/pull/14244
|
||||
- Always use MCP server instructions by @chrstnb in
|
||||
https://github.com/google-gemini/gemini-cli/pull/14297
|
||||
- feat: auto-execute simple slash commands on Enter by @jackwotherspoon in
|
||||
https://github.com/google-gemini/gemini-cli/pull/13985
|
||||
- chore/release: bump version to 0.20.0-nightly.20251201.2fe609cb6 by
|
||||
@gemini-cli-robot in https://github.com/google-gemini/gemini-cli/pull/14304
|
||||
- feat: Add startup profiler to measure and record application initialization
|
||||
phases. by @kevin-ramdass in
|
||||
https://github.com/google-gemini/gemini-cli/pull/13638
|
||||
- bug(core): Avoid stateful tool use in `executor`. by @joshualitt in
|
||||
https://github.com/google-gemini/gemini-cli/pull/14305
|
||||
- feat(themes): add built-in holiday theme 🎁 by @jackwotherspoon in
|
||||
https://github.com/google-gemini/gemini-cli/pull/14301
|
||||
- Updated ToC on docs intro; updated title casing to match Google style by
|
||||
@pcoet in https://github.com/google-gemini/gemini-cli/pull/13717
|
||||
- feat(a2a): Urgent fix - Process modelInfo agent message by @cocosheng-g in
|
||||
https://github.com/google-gemini/gemini-cli/pull/14315
|
||||
- feat(core): enhance availability routing with wrapped fallback and
|
||||
single-model policies by @adamfweidman in
|
||||
https://github.com/google-gemini/gemini-cli/pull/13874
|
||||
- chore(logging): log the problematic event for #12122 by @briandealwis in
|
||||
https://github.com/google-gemini/gemini-cli/pull/14092
|
||||
- fix: remove invalid type key in bug_report.yml by @fancive in
|
||||
https://github.com/google-gemini/gemini-cli/pull/13576
|
||||
- update screenshot by @Transient-Onlooker in
|
||||
https://github.com/google-gemini/gemini-cli/pull/13976
|
||||
- docs: Fix grammar error in Release Cadence (Nightly section) by @JuanCS-Dev in
|
||||
https://github.com/google-gemini/gemini-cli/pull/13866
|
||||
- fix(async): prevent missed async errors from bypassing catch handlers by
|
||||
@amsminn in https://github.com/google-gemini/gemini-cli/pull/13714
|
||||
- fix(zed-integration): remove extra field from acp auth request by
|
||||
@marcocondrache in https://github.com/google-gemini/gemini-cli/pull/13646
|
||||
- feat(cli): Documentation for model configs. by @joshualitt in
|
||||
https://github.com/google-gemini/gemini-cli/pull/12967
|
||||
- fix(ui): misaligned markdown table rendering by @dumbbellcode in
|
||||
https://github.com/google-gemini/gemini-cli/pull/8336
|
||||
- docs: Update 4 files by @g-samroberts in
|
||||
https://github.com/google-gemini/gemini-cli/pull/13628
|
||||
- fix: Conditionally add set -eEuo pipefail in setup-github command by @Smetalo
|
||||
in https://github.com/google-gemini/gemini-cli/pull/8550
|
||||
- fix(cli): fix issue updating a component while rendering a different component
|
||||
by @jacob314 in https://github.com/google-gemini/gemini-cli/pull/14319
|
||||
- Increase flakey test timeout by @chrstnb in
|
||||
https://github.com/google-gemini/gemini-cli/pull/14377
|
||||
- Remove references to deleted kind/bug label by @scidomino in
|
||||
https://github.com/google-gemini/gemini-cli/pull/14383
|
||||
- Don't fail test if we can't cleanup by @scidomino in
|
||||
https://github.com/google-gemini/gemini-cli/pull/14389
|
||||
- feat(core): Implement JIT context manager and setting by @SandyTao520 in
|
||||
https://github.com/google-gemini/gemini-cli/pull/14324
|
||||
- Use polling for extensions-reload integration test by @chrstnb in
|
||||
https://github.com/google-gemini/gemini-cli/pull/14391
|
||||
- Add docs directive to GEMINI.md by @g-samroberts in
|
||||
https://github.com/google-gemini/gemini-cli/pull/14327
|
||||
- Hide sessions that don't have user messages by @bl-ue in
|
||||
https://github.com/google-gemini/gemini-cli/pull/13994
|
||||
- chore(ci): mark GitHub release as pre-release if not on "latest" npm channel
|
||||
by @ljxfstorm in https://github.com/google-gemini/gemini-cli/pull/7386
|
||||
- fix(patch): cherry-pick d284fa6 to release/v0.20.0-preview.0-pr-14545
|
||||
[CONFLICTS] by @gemini-cli-robot in
|
||||
https://github.com/google-gemini/gemini-cli/pull/14559
|
||||
- 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 by
|
||||
@gemini-cli-robot in https://github.com/google-gemini/gemini-cli/pull/14733
|
||||
- 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 by
|
||||
@gemini-cli-robot in https://github.com/google-gemini/gemini-cli/pull/14752
|
||||
|
||||
**Full Changelog**:
|
||||
https://github.com/google-gemini/gemini-cli/compare/v0.19.4...v0.20.0
|
||||
|
||||
## Release v0.19.0 - v0.19.4
|
||||
|
||||
## Highlights
|
||||
|
||||
@@ -26,10 +492,10 @@ on GitHub.
|
||||
integration after enabling "Preview Features" in their CLI’s `/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
|
||||
When "Use 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),
|
||||
user input. ([vid](https://imgur.com/a/kjK8bUK)
|
||||
[pr](https://github.com/google-gemini/gemini-cli/pull/12535) by
|
||||
[@jackwotherspoon](https://github.com/jackwotherspoon))
|
||||
|
||||
@@ -165,7 +631,7 @@ on GitHub.
|
||||
**Full Changelog**:
|
||||
https://github.com/google-gemini/gemini-cli/compare/v0.18.4...v0.19.0
|
||||
|
||||
## Release v0.19.0-preview.0 (Preview)
|
||||
## Release v0.19.0-preview.0
|
||||
|
||||
### What's Changed
|
||||
|
||||
|
||||
@@ -38,6 +38,8 @@ overview of Gemini CLI, see the [main documentation page](../index.md).
|
||||
files and directories from being accessed by tools.
|
||||
- **[Context files (GEMINI.md)](./gemini-md.md):** Provide persistent,
|
||||
hierarchical context to the model.
|
||||
- **[System prompt override](./system-prompt.md):** Replace the built‑in system
|
||||
instructions using `GEMINI_SYSTEM_MD`.
|
||||
|
||||
## Non-interactive mode
|
||||
|
||||
|
||||
@@ -84,10 +84,10 @@ available combinations.
|
||||
|
||||
#### External Tools
|
||||
|
||||
| Action | Keys |
|
||||
| ---------------------------------------------- | ---------- |
|
||||
| Open the current prompt in an external editor. | `Ctrl + X` |
|
||||
| Paste from the clipboard. | `Ctrl + V` |
|
||||
| Action | Keys |
|
||||
| ---------------------------------------------- | ------------------------- |
|
||||
| Open the current prompt in an external editor. | `Ctrl + X` |
|
||||
| Paste from the clipboard. | `Ctrl + V`<br />`Cmd + V` |
|
||||
|
||||
#### App Controls
|
||||
|
||||
|
||||
+11
-15
@@ -6,24 +6,20 @@ and provides resilience when the primary model is unavailable.
|
||||
|
||||
## How it works
|
||||
|
||||
Model routing is not based on prompt complexity, but is a fallback mechanism.
|
||||
Here's how it works:
|
||||
Model routing is managed by the `ModelAvailabilityService`, which monitors model
|
||||
health and automatically routes requests to available models based on defined
|
||||
policies.
|
||||
|
||||
1. **Model failure:** If the currently selected model fails to respond (for
|
||||
example, due to a server error or other issue), the CLI will initiate the
|
||||
fallback process.
|
||||
1. **Model failure:** If the currently selected model fails (e.g., due to quota
|
||||
or server errors), the CLI will iniate the fallback process.
|
||||
|
||||
2. **User consent:** The CLI will prompt you to ask if you want to switch to
|
||||
the fallback model. This is handled by the `fallbackModelHandler`.
|
||||
2. **User consent:** Depending on the failure and the model's policy, the CLI
|
||||
may prompt you to switch to a fallback model (by default always prompts
|
||||
you).
|
||||
|
||||
3. **Fallback activation:** If you consent, the CLI will activate the fallback
|
||||
mode by calling `config.setFallbackMode(true)`.
|
||||
|
||||
4. **Model switch:** On the next request, the CLI will use the
|
||||
`DEFAULT_GEMINI_FLASH_MODEL` as the fallback model. This is handled by the
|
||||
`resolveModel` function in
|
||||
`packages/cli/src/zed-integration/zedIntegration.ts` which checks if
|
||||
`isInFallbackMode()` is true.
|
||||
3. **Model switch:** If approved, or if the policy allows for silent fallback,
|
||||
the CLI will use an available fallback model for the current turn or the
|
||||
remainder of the session.
|
||||
|
||||
### Model selection precedence
|
||||
|
||||
|
||||
+30
-30
@@ -1,12 +1,13 @@
|
||||
# Gemini CLI model selection (`/model` command)
|
||||
|
||||
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.
|
||||
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.
|
||||
> **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.
|
||||
|
||||
## How to use the `/model` command
|
||||
|
||||
@@ -16,26 +17,25 @@ Use the following command in Gemini CLI:
|
||||
/model
|
||||
```
|
||||
|
||||
Running this command will open a dialog with your model options:
|
||||
Running this command will open a dialog with your options:
|
||||
|
||||
| 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 |
|
||||
| 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. |
|
||||
|
||||
### Gemini 3 Pro and preview features
|
||||
We recommend selecting one of the above **Auto** options. However, you can
|
||||
select **Manual** to select a specific model from those available.
|
||||
|
||||
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).
|
||||
### Gemini 3 and preview features
|
||||
|
||||
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.
|
||||
> **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).
|
||||
|
||||
You can also use the `--model` flag to specify a particular Gemini model on
|
||||
startup. For more details, refer to the
|
||||
@@ -46,16 +46,16 @@ Gemini CLI.
|
||||
|
||||
## Best practices for model selection
|
||||
|
||||
- **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).
|
||||
- **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).
|
||||
|
||||
- **Switch to Pro if you aren't getting the results you want.** If you think you
|
||||
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.
|
||||
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.
|
||||
|
||||
- **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:
|
||||
|
||||
@@ -94,7 +94,7 @@ they appear in the UI.
|
||||
| Enable Tool Output Truncation | `tools.enableToolOutputTruncation` | Enable truncation of large tool outputs. | `true` |
|
||||
| Tool Output Truncation Threshold | `tools.truncateToolOutputThreshold` | Truncate tool output if it is larger than this many characters. Set to -1 to disable. | `10000` |
|
||||
| Tool Output Truncation Lines | `tools.truncateToolOutputLines` | The number of lines to keep when truncating tool output. | `100` |
|
||||
| Enable Message Bus Integration | `tools.enableMessageBusIntegration` | Enable policy-based tool confirmation via message bus integration. | `false` |
|
||||
| Enable Message Bus Integration | `tools.enableMessageBusIntegration` | Enable policy-based tool confirmation via message bus integration. | `true` |
|
||||
|
||||
### Security
|
||||
|
||||
|
||||
@@ -0,0 +1,93 @@
|
||||
# System Prompt Override (GEMINI_SYSTEM_MD)
|
||||
|
||||
The core system instructions that guide Gemini CLI can be completely replaced
|
||||
with your own Markdown file. This feature is controlled via the
|
||||
`GEMINI_SYSTEM_MD` environment variable.
|
||||
|
||||
## Overview
|
||||
|
||||
The `GEMINI_SYSTEM_MD` variable instructs the CLI to use an external Markdown
|
||||
file for its system prompt, completely overriding the built-in default. This is
|
||||
a full replacement, not a merge. If you use a custom file, none of the original
|
||||
core instructions will apply unless you include them yourself.
|
||||
|
||||
This feature is intended for advanced users who need to enforce strict,
|
||||
project-specific behavior or create a customized persona.
|
||||
|
||||
> Tip: You can export the current default system prompt to a file first, review
|
||||
> it, and then selectively modify or replace it (see
|
||||
> [“Export the default prompt”](#export-the-default-prompt-recommended)).
|
||||
|
||||
## How to enable
|
||||
|
||||
You can set the environment variable temporarily in your shell, or persist it
|
||||
via a `.gemini/.env` file. See
|
||||
[Persisting Environment Variables](../get-started/authentication.md#persisting-environment-variables).
|
||||
|
||||
- Use the project default path (`.gemini/system.md`):
|
||||
- `GEMINI_SYSTEM_MD=true` or `GEMINI_SYSTEM_MD=1`
|
||||
- The CLI reads `./.gemini/system.md` (relative to your current project
|
||||
directory).
|
||||
|
||||
- Use a custom file path:
|
||||
- `GEMINI_SYSTEM_MD=/absolute/path/to/my-system.md`
|
||||
- Relative paths are supported and resolved from the current working
|
||||
directory.
|
||||
- Tilde expansion is supported (e.g., `~/my-system.md`).
|
||||
|
||||
- Disable the override (use built‑in prompt):
|
||||
- `GEMINI_SYSTEM_MD=false` or `GEMINI_SYSTEM_MD=0` or unset the variable.
|
||||
|
||||
If the override is enabled but the target file does not exist, the CLI will
|
||||
error with: `missing system prompt file '<path>'`.
|
||||
|
||||
## Quick examples
|
||||
|
||||
- One‑off session using a project file:
|
||||
- `GEMINI_SYSTEM_MD=1 gemini`
|
||||
- Persist for a project using `.gemini/.env`:
|
||||
- Create `.gemini/system.md`, then add to `.gemini/.env`:
|
||||
- `GEMINI_SYSTEM_MD=1`
|
||||
- Use a custom file under your home directory:
|
||||
- `GEMINI_SYSTEM_MD=~/prompts/SYSTEM.md gemini`
|
||||
|
||||
## UI indicator
|
||||
|
||||
When `GEMINI_SYSTEM_MD` is active, the CLI shows a `|⌐■_■|` indicator in the UI
|
||||
to signal custom system‑prompt mode.
|
||||
|
||||
## Export the default prompt (recommended)
|
||||
|
||||
Before overriding, export the current default prompt so you can review required
|
||||
safety and workflow rules.
|
||||
|
||||
- Write the built‑in prompt to the project default path:
|
||||
- `GEMINI_WRITE_SYSTEM_MD=1 gemini`
|
||||
- Or write to a custom path:
|
||||
- `GEMINI_WRITE_SYSTEM_MD=~/prompts/DEFAULT_SYSTEM.md gemini`
|
||||
|
||||
This creates the file and writes the current built‑in system prompt to it.
|
||||
|
||||
## Best practices: SYSTEM.md vs GEMINI.md
|
||||
|
||||
- SYSTEM.md (firmware):
|
||||
- Non‑negotiable operational rules: safety, tool‑use protocols, approvals, and
|
||||
mechanics that keep the CLI reliable.
|
||||
- Stable across tasks and projects (or per project when needed).
|
||||
- GEMINI.md (strategy):
|
||||
- Persona, goals, methodologies, and project/domain context.
|
||||
- Evolves per task; relies on SYSTEM.md for safe execution.
|
||||
|
||||
Keep SYSTEM.md minimal but complete for safety and tool operation. Keep
|
||||
GEMINI.md focused on high‑level guidance and project specifics.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
- Error: `missing system prompt file '…'`
|
||||
- Ensure the referenced path exists and is readable.
|
||||
- For `GEMINI_SYSTEM_MD=1|true`, create `./.gemini/system.md` in your project.
|
||||
- Override not taking effect
|
||||
- Confirm the variable is loaded (use `.gemini/.env` or export in your shell).
|
||||
- Paths are resolved from the current working directory; try an absolute path.
|
||||
- Restore defaults
|
||||
- Unset `GEMINI_SYSTEM_MD` or set it to `0`/`false`.
|
||||
@@ -1,13 +1,32 @@
|
||||
# Policy engine
|
||||
|
||||
:::note This feature is currently in testing. To enable it, set
|
||||
`tools.enableMessageBusIntegration` to `true` in your `settings.json` file. :::
|
||||
|
||||
The Gemini CLI includes a powerful policy engine that provides fine-grained
|
||||
control over tool execution. It allows users and administrators to define rules
|
||||
that determine whether a tool call should be allowed, denied, or require user
|
||||
confirmation.
|
||||
|
||||
## Quick start
|
||||
|
||||
To create your first policy:
|
||||
|
||||
1. **Create the policy directory** if it doesn't exist:
|
||||
```bash
|
||||
mkdir -p ~/.gemini/policies
|
||||
```
|
||||
2. **Create a new policy file** (e.g., `~/.gemini/policies/my-rules.toml`). You
|
||||
can use any filename ending in `.toml`; all such files in this directory
|
||||
will be loaded and combined:
|
||||
```toml
|
||||
[[rule]]
|
||||
toolName = "run_shell_command"
|
||||
commandPrefix = "git status"
|
||||
decision = "allow"
|
||||
priority = 100
|
||||
```
|
||||
3. **Run a command** that triggers the policy (e.g., ask Gemini CLI to
|
||||
`git status`). The tool will now execute automatically without prompting for
|
||||
confirmation.
|
||||
|
||||
## Core concepts
|
||||
|
||||
The policy engine operates on a set of rules. Each rule is a combination of
|
||||
@@ -239,6 +258,8 @@ The Gemini CLI ships with a set of default policies to provide a safe
|
||||
out-of-the-box experience.
|
||||
|
||||
- **Read-only tools** (like `read_file`, `glob`) are generally **allowed**.
|
||||
- **Agent delegation** (like `delegate_to_agent`) is **allowed** (sub-agent
|
||||
actions are checked individually).
|
||||
- **Write tools** (like `write_file`, `run_shell_command`) default to
|
||||
**`ask_user`**.
|
||||
- In **`yolo`** mode, a high-priority rule allows all tools.
|
||||
|
||||
@@ -367,6 +367,12 @@ 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": {
|
||||
@@ -496,6 +502,11 @@ 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"
|
||||
@@ -524,6 +535,11 @@ their corresponding top-level category object in your `settings.json` file.
|
||||
with (and override) the built-in aliases.
|
||||
- **Default:** `{}`
|
||||
|
||||
- **`modelConfigs.customOverrides`** (array):
|
||||
- **Description:** Custom model config overrides. These are merged with (and
|
||||
added to) the built-in overrides.
|
||||
- **Default:** `[]`
|
||||
|
||||
- **`modelConfigs.overrides`** (array):
|
||||
- **Description:** Apply specific configuration overrides based on matches,
|
||||
with a primary key of model (or alias). The most specific match will be
|
||||
@@ -715,6 +731,11 @@ their corresponding top-level category object in your `settings.json` file.
|
||||
- **Default:** `false`
|
||||
- **Requires restart:** Yes
|
||||
|
||||
- **`security.enablePermanentToolApproval`** (boolean):
|
||||
- **Description:** Enable the "Allow for all future sessions" option in tool
|
||||
confirmation dialogs.
|
||||
- **Default:** `false`
|
||||
|
||||
- **`security.blockGitExtensions`** (boolean):
|
||||
- **Description:** Blocks installing and loading extensions from Git.
|
||||
- **Default:** `false`
|
||||
@@ -768,7 +789,8 @@ their corresponding top-level category object in your `settings.json` file.
|
||||
#### `experimental`
|
||||
|
||||
- **`experimental.enableAgents`** (boolean):
|
||||
- **Description:** Enable local and remote subagents.
|
||||
- **Description:** Enable local and remote subagents. Warning: Experimental
|
||||
feature, uses YOLO mode for subagents
|
||||
- **Default:** `false`
|
||||
- **Requires restart:** Yes
|
||||
|
||||
@@ -782,11 +804,6 @@ their corresponding top-level category object in your `settings.json` file.
|
||||
- **Default:** `false`
|
||||
- **Requires restart:** Yes
|
||||
|
||||
- **`experimental.isModelAvailabilityServiceEnabled`** (boolean):
|
||||
- **Description:** Enable model routing using new availability service.
|
||||
- **Default:** `false`
|
||||
- **Requires restart:** Yes
|
||||
|
||||
- **`experimental.jitContext`** (boolean):
|
||||
- **Description:** Enable Just-In-Time (JIT) context loading.
|
||||
- **Default:** `false`
|
||||
@@ -816,7 +833,12 @@ 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:** `"gemini-2.5-pro"`
|
||||
- **Default:** `"auto"`
|
||||
- **Requires restart:** Yes
|
||||
|
||||
- **`experimental.introspectionAgentSettings.enabled`** (boolean):
|
||||
- **Description:** Enable the Introspection Agent.
|
||||
- **Default:** `false`
|
||||
- **Requires restart:** Yes
|
||||
|
||||
#### `hooks`
|
||||
@@ -1112,6 +1134,18 @@ the `advanced.excludedEnvVars` setting in your `settings.json` file.
|
||||
- **`GEMINI_SANDBOX`**:
|
||||
- Alternative to the `sandbox` setting in `settings.json`.
|
||||
- Accepts `true`, `false`, `docker`, `podman`, or a custom command string.
|
||||
- **`GEMINI_SYSTEM_MD`**:
|
||||
- Replaces the built‑in system prompt with content from a Markdown file.
|
||||
- `true`/`1`: Use project default path `./.gemini/system.md`.
|
||||
- Any other string: Treat as a path (relative/absolute supported, `~`
|
||||
expands).
|
||||
- `false`/`0` or unset: Use the built‑in prompt. See
|
||||
[System Prompt Override](../cli/system-prompt.md).
|
||||
- **`GEMINI_WRITE_SYSTEM_MD`**:
|
||||
- Writes the current built‑in system prompt to a file for review.
|
||||
- `true`/`1`: Write to `./.gemini/system.md`. Otherwise treat the value as a
|
||||
path.
|
||||
- Run the CLI once with this set to generate the file.
|
||||
- **`SEATBELT_PROFILE`** (macOS specific):
|
||||
- Switches the Seatbelt (`sandbox-exec`) profile on macOS.
|
||||
- `permissive-open`: (Default) Restricts writes to the project folder (and a
|
||||
|
||||
@@ -24,7 +24,7 @@ Rename the photos in my "photos" directory based on their contents.
|
||||
|
||||
Result: Gemini will ask for permission to rename your files.
|
||||
|
||||
Select **Yes, allow once.** and your files will be renamed:
|
||||
Select **Allow once** and your files will be renamed:
|
||||
|
||||
```bash
|
||||
photos/yellow_flowers.png
|
||||
|
||||
@@ -1,56 +1,37 @@
|
||||
# Gemini 3 Pro on Gemini CLI
|
||||
# Gemini 3 Pro and Gemini 3 Flash on Gemini CLI
|
||||
|
||||
We’re excited to bring Gemini 3 Pro to Gemini CLI. Gemini 3 Pro is **currently
|
||||
available** on Gemini CLI to all of the following subscribers:
|
||||
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:
|
||||
|
||||
- Google AI Ultra (except Google AI Ultra for Business).
|
||||
- Google AI Pro.
|
||||
- Gemini Code Assist Standard (requires
|
||||
- Google AI Pro and Google AI Ultra (excluding business customers).
|
||||
- Gemini Code Assist Standard and Enterprise (requires
|
||||
[administrative enablement](#administrator-instructions)).
|
||||
- Gemini Code Assist Enterprise (requires
|
||||
[administrative enablement](#administrator-instructions)).
|
||||
- Paid Gemini API key holders.
|
||||
- Paid Vertex API key holders.
|
||||
- Paid Gemini API and Vertex API key holders.
|
||||
|
||||
For **everyone else**, we're gradually expanding access
|
||||
[through a waitlist](https://goo.gle/geminicli-waitlist-signup). If you don't
|
||||
have one of the listed subscriptions, sign up for the waitlist to access Gemini
|
||||
3 Pro once approved.
|
||||
For free tier users:
|
||||
|
||||
**Note:** Whether you’re automatically granted access or accepted from the
|
||||
waitlist, you’ll still need to enable Gemini 3 Pro
|
||||
[using the `/settings` command](../cli/settings.md).
|
||||
- If you signed up for the waitlist, please check your email for details. We’ve
|
||||
onboarded everyone who signed up to the previously available waitlist.
|
||||
- If you were not on our waitlist, we’re rolling out additional access gradually
|
||||
to ensure the experience remains fast and reliable. Stay tuned for more
|
||||
details.
|
||||
|
||||
## How to join the waitlist
|
||||
## How to get started with Gemini 3 on Gemini CLI
|
||||
|
||||
Users not automatically granted access will need to join the waitlist. Follow
|
||||
these instructions to sign up:
|
||||
Get started by upgrading Gemini CLI to the latest version (0.21.1):
|
||||
|
||||
- Install Gemini CLI.
|
||||
- Authenticate using the **Login with Google** option. You’ll 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.
|
||||
```bash
|
||||
npm install -g @google/gemini-cli@latest
|
||||
```
|
||||
|
||||
Users will be onboarded in batches, subject to availability. When you’ve been
|
||||
granted access to Gemini 3 Pro, you’ll receive an acceptance email to your
|
||||
submitted email address.
|
||||
After you’ve confirmed your version is 0.21.1 or later:
|
||||
|
||||
**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.
|
||||
1. Use the `/settings` command in Gemini CLI.
|
||||
2. Toggle **Preview Features** to `true`.
|
||||
3. Run `/model` and select **Auto (Gemini 3)**.
|
||||
|
||||
## How to use Gemini 3 Pro with Gemini CLI
|
||||
|
||||
Once you receive your acceptance email–or if you are automatically granted
|
||||
access–you 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).
|
||||
For more information, see [Gemini CLI model selection](../cli/model.md).
|
||||
|
||||
### Usage limits and fallback
|
||||
|
||||
@@ -68,10 +49,10 @@ 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
|
||||
|
||||
@@ -92,7 +73,7 @@ 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 Pro with Gemini CLI on Gemini Code Assist
|
||||
## 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.
|
||||
@@ -123,7 +104,7 @@ then:
|
||||
- Use the `/settings` command.
|
||||
- Set **Preview Features** to `true`.
|
||||
|
||||
Restart Gemini CLI and you should have access to Gemini 3 Pro.
|
||||
Restart Gemini CLI and you should have access to Gemini 3.
|
||||
|
||||
## Need help?
|
||||
|
||||
|
||||
@@ -219,7 +219,7 @@ all tools with `*`, specify only the tools you need:
|
||||
|
||||
```json
|
||||
{
|
||||
"matcher": "WriteFile|Edit",
|
||||
"matcher": "write_file|replace",
|
||||
"hooks": [
|
||||
{
|
||||
"name": "validate-writes",
|
||||
@@ -298,7 +298,7 @@ cat > test-input.json << 'EOF'
|
||||
"session_id": "test-123",
|
||||
"cwd": "/tmp/test",
|
||||
"hook_event_name": "BeforeTool",
|
||||
"tool_name": "WriteFile",
|
||||
"tool_name": "write_file",
|
||||
"tool_input": {
|
||||
"file_path": "test.txt",
|
||||
"content": "Test content"
|
||||
@@ -437,7 +437,7 @@ Add descriptions to help others understand your hooks:
|
||||
"hooks": {
|
||||
"BeforeTool": [
|
||||
{
|
||||
"matcher": "WriteFile|Edit",
|
||||
"matcher": "write_file|replace",
|
||||
"hooks": [
|
||||
{
|
||||
"name": "secret-scanner",
|
||||
@@ -484,7 +484,7 @@ Verify the hook appears in the list and is enabled.
|
||||
|
||||
```bash
|
||||
# Test regex pattern
|
||||
echo "WriteFile" | grep -E "Write.*|Edit"
|
||||
echo "write_file|replace" | grep -E "write_.*|replace"
|
||||
```
|
||||
|
||||
**Check disabled list:**
|
||||
@@ -559,7 +559,7 @@ async function getCachedData(key) {
|
||||
"hooks": {
|
||||
"BeforeTool": [
|
||||
{
|
||||
"matcher": "WriteFile",
|
||||
"matcher": "write_file",
|
||||
"hooks": [
|
||||
{
|
||||
"name": "quick-check",
|
||||
@@ -569,7 +569,7 @@ async function getCachedData(key) {
|
||||
]
|
||||
},
|
||||
{
|
||||
"matcher": "WriteFile",
|
||||
"matcher": "write_file",
|
||||
"hooks": [
|
||||
{
|
||||
"name": "deep-check",
|
||||
|
||||
+128
-23
@@ -7,6 +7,9 @@ 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 [hooks reference](reference.md) for the technical specification of the I/O
|
||||
schemas.
|
||||
|
||||
See [best practices](best-practices.md) for guidelines on security, performance,
|
||||
and debugging.
|
||||
|
||||
@@ -70,7 +73,7 @@ trigger the hook:
|
||||
"hooks": {
|
||||
"BeforeTool": [
|
||||
{
|
||||
"matcher": "WriteFile|Edit",
|
||||
"matcher": "write_file|replace",
|
||||
"hooks": [
|
||||
/* hooks for write operations */
|
||||
]
|
||||
@@ -82,8 +85,8 @@ trigger the hook:
|
||||
|
||||
**Matcher patterns:**
|
||||
|
||||
- **Exact match:** `"ReadFile"` matches only `ReadFile`
|
||||
- **Regex:** `"Write.*|Edit"` matches `WriteFile`, `WriteBinary`, `Edit`
|
||||
- **Exact match:** `"read_file"` matches only `read_file`
|
||||
- **Regex:** `"write_.*|replace"` matches `write_file`, `replace`
|
||||
- **Wildcard:** `"*"` or `""` matches all tools
|
||||
|
||||
**Session event matchers:**
|
||||
@@ -115,6 +118,7 @@ Every hook receives these base fields:
|
||||
```json
|
||||
{
|
||||
"session_id": "abc123",
|
||||
"transcript_path": "/path/to/transcript.jsonl",
|
||||
"cwd": "/path/to/project",
|
||||
"hook_event_name": "BeforeTool",
|
||||
"timestamp": "2025-12-01T10:30:00Z"
|
||||
@@ -130,7 +134,7 @@ Every hook receives these base fields:
|
||||
|
||||
```json
|
||||
{
|
||||
"tool_name": "WriteFile",
|
||||
"tool_name": "write_file",
|
||||
"tool_input": {
|
||||
"file_path": "/path/to/file.ts",
|
||||
"content": "..."
|
||||
@@ -159,7 +163,7 @@ Or simple exit codes:
|
||||
|
||||
```json
|
||||
{
|
||||
"tool_name": "ReadFile",
|
||||
"tool_name": "read_file",
|
||||
"tool_input": { "file_path": "..." },
|
||||
"tool_response": "file contents..."
|
||||
}
|
||||
@@ -212,7 +216,7 @@ Or simple exit codes:
|
||||
"toolConfig": {
|
||||
"functionCallingConfig": {
|
||||
"mode": "AUTO",
|
||||
"allowedFunctionNames": ["ReadFile", "WriteFile"]
|
||||
"allowedFunctionNames": ["read_file", "write_file"]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -316,7 +320,7 @@ Or simple exit codes:
|
||||
"toolConfig": {
|
||||
"functionCallingConfig": {
|
||||
"mode": "ANY",
|
||||
"allowedFunctionNames": ["ReadFile", "WriteFile", "Edit"]
|
||||
"allowedFunctionNames": ["read_file", "write_file", "replace"]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -326,7 +330,7 @@ Or simple exit codes:
|
||||
Or simple output (comma-separated tool names sets mode to ANY):
|
||||
|
||||
```bash
|
||||
echo "ReadFile,WriteFile,Edit"
|
||||
echo "read_file,write_file,replace"
|
||||
```
|
||||
|
||||
#### SessionStart
|
||||
@@ -410,13 +414,22 @@ precedence rules.
|
||||
|
||||
### Configuration layers
|
||||
|
||||
Hook configurations are applied in the following order of precedence (higher
|
||||
numbers override lower numbers):
|
||||
Hook configurations are applied in the following order of execution (lower
|
||||
numbers run first):
|
||||
|
||||
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)
|
||||
1. **Project settings:** `.gemini/settings.json` in your project directory
|
||||
(highest priority)
|
||||
2. **User settings:** `~/.gemini/settings.json`
|
||||
3. **System settings:** `/etc/gemini-cli/settings.json`
|
||||
4. **Extensions:** Internal hooks defined by installed extensions (lowest
|
||||
priority)
|
||||
|
||||
#### Deduplication and shadowing
|
||||
|
||||
If multiple hooks with the identical **name** and **command** are discovered
|
||||
across different configuration layers, Gemini CLI deduplicates them. The hook
|
||||
from the higher-priority layer (e.g., Project) will be kept, and others will be
|
||||
ignored.
|
||||
|
||||
Within each level, hooks run in the order they are declared in the
|
||||
configuration.
|
||||
@@ -446,8 +459,9 @@ configuration.
|
||||
|
||||
**Configuration properties:**
|
||||
|
||||
- **`name`** (string, required): Unique identifier for the hook used in
|
||||
`/hooks enable/disable` commands
|
||||
- **`name`** (string, recommended): Unique identifier for the hook used in
|
||||
`/hooks enable/disable` commands. If omitted, the `command` path is used as
|
||||
the identifier.
|
||||
- **`type`** (string, required): Hook type - currently only `"command"` is
|
||||
supported
|
||||
- **`command`** (string, required): Path to the script or command to execute
|
||||
@@ -494,6 +508,8 @@ You can temporarily enable or disable individual hooks using commands:
|
||||
|
||||
These commands allow you to control hook execution without editing configuration
|
||||
files. The hook name should match the `name` field in your hook configuration.
|
||||
Changes made via these commands are persisted to your global User settings
|
||||
(`~/.gemini/settings.json`).
|
||||
|
||||
### Disabled hooks configuration
|
||||
|
||||
@@ -524,7 +540,7 @@ This command:
|
||||
|
||||
- Reads `.claude/settings.json`
|
||||
- Converts event names (`PreToolUse` → `BeforeTool`, etc.)
|
||||
- Translates tool names (`Bash` → `RunShellCommand`, `Edit` → `Edit`)
|
||||
- Translates tool names (`Bash` → `run_shell_command`, `replace` → `replace`)
|
||||
- Updates matcher patterns
|
||||
- Writes to `.gemini/settings.json`
|
||||
|
||||
@@ -543,12 +559,101 @@ This command:
|
||||
|
||||
### Tool name mapping
|
||||
|
||||
| Claude Code | Gemini CLI |
|
||||
| ----------- | ----------------- |
|
||||
| `Bash` | `RunShellCommand` |
|
||||
| `Edit` | `Edit` |
|
||||
| `Read` | `ReadFile` |
|
||||
| `Write` | `WriteFile` |
|
||||
| Claude Code | Gemini CLI |
|
||||
| ----------- | --------------------- |
|
||||
| `Bash` | `run_shell_command` |
|
||||
| `Edit` | `replace` |
|
||||
| `Read` | `read_file` |
|
||||
| `Write` | `write_file` |
|
||||
| `Glob` | `glob` |
|
||||
| `Grep` | `search_file_content` |
|
||||
| `LS` | `list_directory` |
|
||||
|
||||
## Tool and Event Matchers Reference
|
||||
|
||||
### Available tool names for matchers
|
||||
|
||||
The following built-in tools can be used in `BeforeTool` and `AfterTool` hook
|
||||
matchers:
|
||||
|
||||
#### File operations
|
||||
|
||||
- `read_file` - Read a single file
|
||||
- `read_many_files` - Read multiple files at once
|
||||
- `write_file` - Create or overwrite a file
|
||||
- `replace` - Edit file content with find/replace
|
||||
|
||||
#### File system
|
||||
|
||||
- `list_directory` - List directory contents
|
||||
- `glob` - Find files matching a pattern
|
||||
- `search_file_content` - Search within file contents
|
||||
|
||||
#### Execution
|
||||
|
||||
- `run_shell_command` - Execute shell commands
|
||||
|
||||
#### Web and external
|
||||
|
||||
- `google_web_search` - Google Search with grounding
|
||||
- `web_fetch` - Fetch web page content
|
||||
|
||||
#### Agent features
|
||||
|
||||
- `write_todos` - Manage TODO items
|
||||
- `save_memory` - Save information to memory
|
||||
- `delegate_to_agent` - Delegate tasks to sub-agents
|
||||
|
||||
#### Example matchers
|
||||
|
||||
```json
|
||||
{
|
||||
"matcher": "write_file|replace" // File editing tools
|
||||
}
|
||||
```
|
||||
|
||||
```json
|
||||
{
|
||||
"matcher": "read_.*" // All read operations
|
||||
}
|
||||
```
|
||||
|
||||
```json
|
||||
{
|
||||
"matcher": "run_shell_command" // Only shell commands
|
||||
}
|
||||
```
|
||||
|
||||
```json
|
||||
{
|
||||
"matcher": "*" // All tools
|
||||
}
|
||||
```
|
||||
|
||||
### Event-specific matchers
|
||||
|
||||
#### SessionStart event matchers
|
||||
|
||||
- `startup` - Fresh session start
|
||||
- `resume` - Resuming a previous session
|
||||
- `clear` - Session cleared
|
||||
|
||||
#### SessionEnd event matchers
|
||||
|
||||
- `exit` - Normal exit
|
||||
- `clear` - Session cleared
|
||||
- `logout` - User logged out
|
||||
- `prompt_input_exit` - Exit from prompt input
|
||||
- `other` - Other reasons
|
||||
|
||||
#### PreCompress event matchers
|
||||
|
||||
- `manual` - Manually triggered compression
|
||||
- `auto` - Automatically triggered compression
|
||||
|
||||
#### Notification event matchers
|
||||
|
||||
- `ToolPermission` - Tool permission notifications
|
||||
|
||||
## Learn more
|
||||
|
||||
|
||||
@@ -0,0 +1,168 @@
|
||||
# Hooks Reference
|
||||
|
||||
This document provides the technical specification for Gemini CLI hooks,
|
||||
including the JSON schemas for input and output, exit code behaviors, and the
|
||||
stable model API.
|
||||
|
||||
## Communication Protocol
|
||||
|
||||
Hooks communicate with Gemini CLI via standard streams and exit codes:
|
||||
|
||||
- **Input**: Gemini CLI sends a JSON object to the hook's `stdin`.
|
||||
- **Output**: The hook sends a JSON object (or plain text) to `stdout`.
|
||||
- **Exit Codes**: Used to signal success or blocking errors.
|
||||
|
||||
### Exit Code Behavior
|
||||
|
||||
| Exit Code | Meaning | Behavior |
|
||||
| :-------- | :----------------- | :---------------------------------------------------------------------------------------------- |
|
||||
| `0` | **Success** | `stdout` is parsed as JSON. If parsing fails, it's treated as a `systemMessage`. |
|
||||
| `2` | **Blocking Error** | Interrupts the current operation. `stderr` is shown to the agent (for tool events) or the user. |
|
||||
| Other | **Warning** | Execution continues. `stderr` is logged as a non-blocking warning. |
|
||||
|
||||
---
|
||||
|
||||
## Input Schema (`stdin`)
|
||||
|
||||
Every hook receives a base JSON object. Extra fields are added depending on the
|
||||
specific event.
|
||||
|
||||
### Base Fields (All Events)
|
||||
|
||||
| Field | Type | Description |
|
||||
| :---------------- | :------- | :---------------------------------------------------- |
|
||||
| `session_id` | `string` | Unique identifier for the current CLI session. |
|
||||
| `transcript_path` | `string` | Path to the session's JSON transcript (if available). |
|
||||
| `cwd` | `string` | The current working directory. |
|
||||
| `hook_event_name` | `string` | The name of the firing event (e.g., `BeforeTool`). |
|
||||
| `timestamp` | `string` | ISO 8601 timestamp of the event. |
|
||||
|
||||
### Event-Specific Fields
|
||||
|
||||
#### Tool Events (`BeforeTool`, `AfterTool`)
|
||||
|
||||
- `tool_name`: (`string`) The internal name of the tool (e.g., `write_file`,
|
||||
`run_shell_command`).
|
||||
- `tool_input`: (`object`) The arguments passed to the tool.
|
||||
- `tool_response`: (`object`, **AfterTool only**) The raw output from the tool
|
||||
execution.
|
||||
|
||||
#### Agent Events (`BeforeAgent`, `AfterAgent`)
|
||||
|
||||
- `prompt`: (`string`) The user's submitted prompt.
|
||||
- `prompt_response`: (`string`, **AfterAgent only**) The final response text
|
||||
from the model.
|
||||
- `stop_hook_active`: (`boolean`, **AfterAgent only**) Indicates if a stop hook
|
||||
is already handling a continuation.
|
||||
|
||||
#### Model Events (`BeforeModel`, `AfterModel`, `BeforeToolSelection`)
|
||||
|
||||
- `llm_request`: (`LLMRequest`) A stable representation of the outgoing request.
|
||||
See [Stable Model API](#stable-model-api).
|
||||
- `llm_response`: (`LLMResponse`, **AfterModel only**) A stable representation
|
||||
of the incoming response.
|
||||
|
||||
#### Session & Notification Events
|
||||
|
||||
- `source`: (`startup` | `resume` | `clear`, **SessionStart only**) The trigger
|
||||
source.
|
||||
- `reason`: (`exit` | `clear` | `logout` | `prompt_input_exit` | `other`,
|
||||
**SessionEnd only**) The reason for session end.
|
||||
- `trigger`: (`manual` | `auto`, **PreCompress only**) What triggered the
|
||||
compression event.
|
||||
- `notification_type`: (`ToolPermission`, **Notification only**) The type of
|
||||
notification being fired.
|
||||
- `message`: (`string`, **Notification only**) The notification message.
|
||||
- `details`: (`object`, **Notification only**) Payload-specific details for the
|
||||
notification.
|
||||
|
||||
---
|
||||
|
||||
## Output Schema (`stdout`)
|
||||
|
||||
If the hook exits with `0`, the CLI attempts to parse `stdout` as JSON.
|
||||
|
||||
### Common Output Fields
|
||||
|
||||
| Field | Type | Description |
|
||||
| :------------------- | :-------- | :----------------------------------------------------------------------- |
|
||||
| `decision` | `string` | One of: `allow`, `deny`, `block`, `ask`, `approve`. |
|
||||
| `reason` | `string` | Explanation shown to the **agent** when a decision is `deny` or `block`. |
|
||||
| `systemMessage` | `string` | Message displayed to the **user** in the CLI terminal. |
|
||||
| `continue` | `boolean` | If `false`, immediately terminates the agent loop for this turn. |
|
||||
| `stopReason` | `string` | Message shown to the user when `continue` is `false`. |
|
||||
| `suppressOutput` | `boolean` | If `true`, the hook execution is hidden from the CLI transcript. |
|
||||
| `hookSpecificOutput` | `object` | Container for event-specific data (see below). |
|
||||
|
||||
### `hookSpecificOutput` Reference
|
||||
|
||||
| Field | Supported Events | Description |
|
||||
| :------------------ | :----------------------------------------- | :-------------------------------------------------------------------------------- |
|
||||
| `additionalContext` | `SessionStart`, `BeforeAgent`, `AfterTool` | Appends text directly to the agent's context. |
|
||||
| `llm_request` | `BeforeModel` | A `Partial<LLMRequest>` to override parameters of the outgoing call. |
|
||||
| `llm_response` | `BeforeModel` | A **full** `LLMResponse` to bypass the model and provide a synthetic result. |
|
||||
| `llm_response` | `AfterModel` | A `Partial<LLMResponse>` to modify the model's response before the agent sees it. |
|
||||
| `toolConfig` | `BeforeToolSelection` | Object containing `mode` (`AUTO`/`ANY`/`NONE`) and `allowedFunctionNames`. |
|
||||
|
||||
---
|
||||
|
||||
## Stable Model API
|
||||
|
||||
Gemini CLI uses a decoupled format for model interactions to ensure hooks remain
|
||||
stable even if the underlying Gemini SDK changes.
|
||||
|
||||
### `LLMRequest` Object
|
||||
|
||||
Used in `BeforeModel` and `BeforeToolSelection`.
|
||||
|
||||
> 💡 **Note**: In v1, model hooks are primarily text-focused. Non-text parts
|
||||
> (like images or function calls) provided in the `content` array will be
|
||||
> simplified to their string representation by the translator.
|
||||
|
||||
```typescript
|
||||
{
|
||||
"model": string,
|
||||
"messages": Array<{
|
||||
"role": "user" | "model" | "system",
|
||||
"content": string | Array<{ "type": string, [key: string]: any }>
|
||||
}>,
|
||||
"config"?: {
|
||||
"temperature"?: number,
|
||||
"maxOutputTokens"?: number,
|
||||
"topP"?: number,
|
||||
"topK"?: number
|
||||
},
|
||||
"toolConfig"?: {
|
||||
"mode"?: "AUTO" | "ANY" | "NONE",
|
||||
"allowedFunctionNames"?: string[]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### `LLMResponse` Object
|
||||
|
||||
Used in `AfterModel` and as a synthetic response in `BeforeModel`.
|
||||
|
||||
```typescript
|
||||
{
|
||||
"text"?: string,
|
||||
"candidates": Array<{
|
||||
"content": {
|
||||
"role": "model",
|
||||
"parts": string[]
|
||||
},
|
||||
"finishReason"?: "STOP" | "MAX_TOKENS" | "SAFETY" | "RECITATION" | "OTHER",
|
||||
"index"?: number,
|
||||
"safetyRatings"?: Array<{
|
||||
"category": string,
|
||||
"probability": string,
|
||||
"blocked"?: boolean
|
||||
}>
|
||||
}>,
|
||||
"usageMetadata"?: {
|
||||
"promptTokenCount"?: number,
|
||||
"candidatesTokenCount"?: number,
|
||||
"totalTokenCount"?: number
|
||||
}
|
||||
}
|
||||
```
|
||||
@@ -72,9 +72,9 @@ Run Gemini CLI and execute any command that uses tools:
|
||||
```
|
||||
> Read the README.md file
|
||||
|
||||
[Agent uses ReadFile tool]
|
||||
[Agent uses read_file tool]
|
||||
|
||||
Logged: ReadFile
|
||||
Logged: read_file
|
||||
```
|
||||
|
||||
Check `.gemini/tool-log.txt` to see the logged tool executions.
|
||||
@@ -110,7 +110,7 @@ exit 0
|
||||
"hooks": {
|
||||
"BeforeTool": [
|
||||
{
|
||||
"matcher": "WriteFile|Edit",
|
||||
"matcher": "write_file|replace",
|
||||
"hooks": [
|
||||
{
|
||||
"name": "secret-scanner",
|
||||
@@ -167,7 +167,7 @@ exit 0
|
||||
"hooks": {
|
||||
"AfterTool": [
|
||||
{
|
||||
"matcher": "WriteFile|Edit",
|
||||
"matcher": "write_file|replace",
|
||||
"hooks": [
|
||||
{
|
||||
"name": "auto-test",
|
||||
@@ -379,7 +379,7 @@ chmod +x .gemini/hooks/*.js
|
||||
],
|
||||
"BeforeTool": [
|
||||
{
|
||||
"matcher": "WriteFile|Edit",
|
||||
"matcher": "write_file|replace",
|
||||
"hooks": [
|
||||
{
|
||||
"name": "security-check",
|
||||
@@ -392,7 +392,7 @@ chmod +x .gemini/hooks/*.js
|
||||
],
|
||||
"AfterTool": [
|
||||
{
|
||||
"matcher": "WriteFile|Edit",
|
||||
"matcher": "write_file|replace",
|
||||
"hooks": [
|
||||
{
|
||||
"name": "auto-test",
|
||||
@@ -601,7 +601,7 @@ async function main() {
|
||||
.map((k) => k.trim());
|
||||
|
||||
// Simple keyword-based filtering + core tools
|
||||
const coreTools = ['ReadFile', 'WriteFile', 'Edit', 'RunShellCommand'];
|
||||
const coreTools = ['read_file', 'write_file', 'replace', 'run_shell_command'];
|
||||
const filtered = candidateTools.filter((tool) => {
|
||||
if (coreTools.includes(tool)) return true;
|
||||
const toolLower = tool.toLowerCase();
|
||||
|
||||
@@ -128,8 +128,8 @@ editor.
|
||||
You can also **modify the suggested changes** directly in the diff view before
|
||||
accepting them.
|
||||
|
||||
If you select ‘Yes, allow always’ in the CLI, changes will no longer show up in
|
||||
the IDE as they will be auto-accepted.
|
||||
If you select ‘Allow for this session’ in the CLI, changes will no longer show
|
||||
up in the IDE as they will be auto-accepted.
|
||||
|
||||
## Using with sandboxing
|
||||
|
||||
|
||||
@@ -73,6 +73,9 @@ This documentation is organized into the following sections:
|
||||
- **[Memport](./core/memport.md):** Using the Memory Import Processor.
|
||||
- **[Tools API](./core/tools-api.md):** Information on how the core manages and
|
||||
exposes tools.
|
||||
- **[System Prompt Override](./cli/system-prompt.md):** Replace built-in system
|
||||
instructions using `GEMINI_SYSTEM_MD`.
|
||||
|
||||
- **[Policy Engine](./core/policy-engine.md):** Use the Policy Engine for
|
||||
fine-grained control over tool execution.
|
||||
|
||||
|
||||
@@ -14,6 +14,9 @@ feature), while the PR is the "how" (the implementation). This separation helps
|
||||
us track work, prioritize features, and maintain clear historical context. Our
|
||||
automation is built around this principle.
|
||||
|
||||
> **Note:** Issues tagged as "🔒Maintainers only" are reserved for project
|
||||
> maintainers. We will not accept pull requests related to these issues.
|
||||
|
||||
---
|
||||
|
||||
## Detailed automation workflows
|
||||
|
||||
+9
-1
@@ -24,7 +24,7 @@
|
||||
"slug": "docs/get-started"
|
||||
},
|
||||
{
|
||||
"label": "Gemini 3 Pro on Gemini CLI",
|
||||
"label": "Gemini 3 on Gemini CLI",
|
||||
"slug": "docs/get-started/gemini-3"
|
||||
},
|
||||
{
|
||||
@@ -115,6 +115,10 @@
|
||||
{
|
||||
"label": "Uninstall",
|
||||
"slug": "docs/cli/uninstall"
|
||||
},
|
||||
{
|
||||
"label": "System prompt override",
|
||||
"slug": "docs/cli/system-prompt"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -204,6 +208,10 @@
|
||||
"label": "Writing hooks",
|
||||
"slug": "docs/hooks/writing-hooks"
|
||||
},
|
||||
{
|
||||
"label": "Hooks reference",
|
||||
"slug": "docs/hooks/reference"
|
||||
},
|
||||
{
|
||||
"label": "Best practices",
|
||||
"slug": "docs/hooks/best-practices"
|
||||
|
||||
@@ -153,3 +153,6 @@ guide_, consider searching the Gemini CLI
|
||||
[Issue tracker on GitHub](https://github.com/google-gemini/gemini-cli/issues).
|
||||
If you can't find an issue similar to yours, consider creating a new GitHub
|
||||
Issue with a detailed description. Pull requests are also welcome!
|
||||
|
||||
> **Note:** Issues tagged as "🔒Maintainers only" are reserved for project
|
||||
> maintainers. We will not accept pull requests related to these issues.
|
||||
|
||||
+19
-10
@@ -12,7 +12,7 @@ import prettierConfig from 'eslint-config-prettier';
|
||||
import importPlugin from 'eslint-plugin-import';
|
||||
import vitest from '@vitest/eslint-plugin';
|
||||
import globals from 'globals';
|
||||
import licenseHeader from 'eslint-plugin-license-header';
|
||||
import headers from 'eslint-plugin-headers';
|
||||
import path from 'node:path';
|
||||
import url from 'node:url';
|
||||
|
||||
@@ -165,7 +165,9 @@ export default tseslint.config(
|
||||
'prefer-const': ['error', { destructuring: 'all' }],
|
||||
radix: 'error',
|
||||
'default-case': 'error',
|
||||
'@typescript-eslint/await-thenable': ['error'],
|
||||
'@typescript-eslint/no-floating-promises': ['error'],
|
||||
'@typescript-eslint/no-unnecessary-type-assertion': ['error'],
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -207,19 +209,26 @@ export default tseslint.config(
|
||||
{
|
||||
files: ['./**/*.{tsx,ts,js}'],
|
||||
plugins: {
|
||||
'license-header': licenseHeader,
|
||||
headers,
|
||||
import: importPlugin,
|
||||
},
|
||||
rules: {
|
||||
'license-header/header': [
|
||||
'headers/header-format': [
|
||||
'error',
|
||||
[
|
||||
'/**',
|
||||
' * @license',
|
||||
' * Copyright 2025 Google LLC',
|
||||
' * SPDX-License-Identifier: Apache-2.0',
|
||||
' */',
|
||||
],
|
||||
{
|
||||
source: 'string',
|
||||
content: [
|
||||
'@license',
|
||||
'Copyright (year) Google LLC',
|
||||
'SPDX-License-Identifier: Apache-2.0',
|
||||
].join('\n'),
|
||||
patterns: {
|
||||
year: {
|
||||
pattern: '202[5-6]',
|
||||
defaultValue: '2026',
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
'import/enforce-node-protocol-usage': ['error', 'always'],
|
||||
},
|
||||
|
||||
@@ -4,13 +4,20 @@
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
||||
import * as os from 'node:os';
|
||||
import { TestRig } from './test-helper.js';
|
||||
|
||||
describe('Ctrl+C exit', () => {
|
||||
let rig: TestRig;
|
||||
|
||||
beforeEach(() => {
|
||||
rig = new TestRig();
|
||||
});
|
||||
|
||||
afterEach(async () => await rig.cleanup());
|
||||
|
||||
it('should exit gracefully on second Ctrl+C', async () => {
|
||||
const rig = new TestRig();
|
||||
await rig.setup('should exit gracefully on second Ctrl+C', {
|
||||
settings: { tools: { useRipgrep: false } },
|
||||
});
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { describe, expect, it, beforeEach, afterEach } from 'vitest';
|
||||
import { TestRig } from './test-helper.js';
|
||||
import { writeFileSync } from 'node:fs';
|
||||
import { join } from 'node:path';
|
||||
@@ -20,8 +20,15 @@ const extensionUpdate = `{
|
||||
}`;
|
||||
|
||||
describe('extension install', () => {
|
||||
let rig: TestRig;
|
||||
|
||||
beforeEach(() => {
|
||||
rig = new TestRig();
|
||||
});
|
||||
|
||||
afterEach(async () => await rig.cleanup());
|
||||
|
||||
it('installs a local extension, verifies a command, and updates it', async () => {
|
||||
const rig = new TestRig();
|
||||
rig.setup('extension install test');
|
||||
const testServerPath = join(rig.testDir!, 'gemini-extension.json');
|
||||
writeFileSync(testServerPath, extension);
|
||||
@@ -47,7 +54,6 @@ describe('extension install', () => {
|
||||
'uninstall',
|
||||
'test-extension-install',
|
||||
]);
|
||||
await rig.cleanup();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { expect, it, describe } from 'vitest';
|
||||
import { expect, it, describe, beforeEach, afterEach } from 'vitest';
|
||||
import { TestRig } from './test-helper.js';
|
||||
import { TestMcpServer } from './test-mcp-server.js';
|
||||
import { writeFileSync } from 'node:fs';
|
||||
@@ -18,6 +18,14 @@ import stripAnsi from 'strip-ansi';
|
||||
const itIf = (condition: boolean) => (condition ? it : it.skip);
|
||||
|
||||
describe('extension reloading', () => {
|
||||
let rig: TestRig;
|
||||
|
||||
beforeEach(() => {
|
||||
rig = new TestRig();
|
||||
});
|
||||
|
||||
afterEach(async () => await rig.cleanup());
|
||||
|
||||
const sandboxEnv = env['GEMINI_SANDBOX'];
|
||||
// Fails in linux non-sandbox e2e tests
|
||||
// TODO(#14527): Re-enable this once fixed
|
||||
@@ -43,7 +51,6 @@ describe('extension reloading', () => {
|
||||
},
|
||||
};
|
||||
|
||||
const rig = new TestRig();
|
||||
rig.setup('extension reload test', {
|
||||
settings: {
|
||||
experimental: { extensionReloading: true },
|
||||
@@ -75,7 +82,7 @@ describe('extension reloading', () => {
|
||||
writeFileSync(testServerPath, safeJsonStringify(extension, 2));
|
||||
|
||||
// Start the CLI.
|
||||
const run = await rig.runInteractive('--debug');
|
||||
const run = await rig.runInteractive({ args: '--debug' });
|
||||
await run.expectText('You have 1 extension with an update available');
|
||||
// See the outdated extension
|
||||
await run.sendText('/extensions list');
|
||||
@@ -145,7 +152,6 @@ describe('extension reloading', () => {
|
||||
await serverA.stop();
|
||||
await serverB.stop();
|
||||
await rig.runCommand(['extensions', 'uninstall', 'test-extension']);
|
||||
await rig.cleanup();
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
@@ -4,22 +4,29 @@
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
||||
import { existsSync } from 'node:fs';
|
||||
import * as path from 'node:path';
|
||||
import { TestRig, printDebugInfo, validateModelOutput } from './test-helper.js';
|
||||
|
||||
describe('file-system', () => {
|
||||
let rig: TestRig;
|
||||
|
||||
beforeEach(() => {
|
||||
rig = new TestRig();
|
||||
});
|
||||
|
||||
afterEach(async () => await rig.cleanup());
|
||||
|
||||
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'] } },
|
||||
});
|
||||
rig.createFile('test.txt', 'hello world');
|
||||
|
||||
const result = await rig.run(
|
||||
`read the file test.txt and show me its contents`,
|
||||
);
|
||||
const result = await rig.run({
|
||||
args: `read the file test.txt and show me its contents`,
|
||||
});
|
||||
|
||||
const foundToolCall = await rig.waitForToolCall('read_file');
|
||||
|
||||
@@ -41,13 +48,14 @@ 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'] } },
|
||||
});
|
||||
rig.createFile('test.txt', '');
|
||||
|
||||
const result = await rig.run(`edit test.txt to have a hello world message`);
|
||||
const result = await rig.run({
|
||||
args: `edit test.txt to have a hello world message`,
|
||||
});
|
||||
|
||||
// Accept multiple valid tools for editing files
|
||||
const foundToolCall = await rig.waitForAnyToolCall([
|
||||
@@ -98,15 +106,14 @@ 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'] } },
|
||||
});
|
||||
const fileName = 'my test file.txt';
|
||||
|
||||
const result = await rig.run(
|
||||
`write "hello" to "${fileName}" and then stop. Do not perform any other actions.`,
|
||||
);
|
||||
const result = await rig.run({
|
||||
args: `write "hello" to "${fileName}" and then stop. Do not perform any other actions.`,
|
||||
});
|
||||
|
||||
const foundToolCall = await rig.waitForToolCall('write_file');
|
||||
if (!foundToolCall) {
|
||||
@@ -122,7 +129,6 @@ 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'] } },
|
||||
});
|
||||
@@ -130,7 +136,7 @@ describe('file-system', () => {
|
||||
rig.createFile(fileName, '1.0.0');
|
||||
|
||||
const prompt = `Read the version from ${fileName} and write the next version 1.0.1 back to the file.`;
|
||||
const result = await rig.run(prompt);
|
||||
const result = await rig.run({ args: prompt });
|
||||
|
||||
await rig.waitForTelemetryReady();
|
||||
const toolLogs = rig.readToolLogs();
|
||||
@@ -159,16 +165,15 @@ describe('file-system', () => {
|
||||
});
|
||||
|
||||
it.skip('should replace multiple instances of a string', async () => {
|
||||
const rig = new TestRig();
|
||||
rig.setup('should replace multiple instances of a string');
|
||||
const fileName = 'ambiguous.txt';
|
||||
const fileContent = 'Hey there, \ntest line\ntest line';
|
||||
const expectedContent = 'Hey there, \nnew line\nnew line';
|
||||
rig.createFile(fileName, fileContent);
|
||||
|
||||
const result = await rig.run(
|
||||
`rewrite the file ${fileName} to replace all instances of "test line" with "new line"`,
|
||||
);
|
||||
const result = await rig.run({
|
||||
args: `rewrite the file ${fileName} to replace all instances of "test line" with "new line"`,
|
||||
});
|
||||
|
||||
const validTools = ['write_file', 'edit'];
|
||||
const foundToolCall = await rig.waitForAnyToolCall(validTools);
|
||||
@@ -211,14 +216,15 @@ describe('file-system', () => {
|
||||
});
|
||||
|
||||
it('should fail safely when trying to edit a non-existent file', async () => {
|
||||
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';
|
||||
|
||||
const result = await rig.run(`In ${fileName}, replace "a" with "b"`);
|
||||
const result = await rig.run({
|
||||
args: `In ${fileName}, replace "a" with "b"`,
|
||||
});
|
||||
|
||||
await rig.waitForTelemetryReady();
|
||||
const toolLogs = rig.readToolLogs();
|
||||
|
||||
@@ -4,36 +4,39 @@
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
||||
import { TestRig } from './test-helper.js';
|
||||
import { join } from 'node:path';
|
||||
|
||||
describe('Flicker Detector', () => {
|
||||
let rig: TestRig;
|
||||
|
||||
beforeEach(() => {
|
||||
rig = new TestRig();
|
||||
});
|
||||
|
||||
afterEach(async () => await rig.cleanup());
|
||||
|
||||
it('should not detect a flicker under the max height budget', async () => {
|
||||
const rig = new TestRig();
|
||||
rig.setup('flicker-detector-test', {
|
||||
fakeResponsesPath: join(
|
||||
import.meta.dirname,
|
||||
'flicker-detector.max-height.responses',
|
||||
),
|
||||
});
|
||||
try {
|
||||
const run = await rig.runInteractive();
|
||||
const prompt = 'Tell me a fun fact.';
|
||||
await run.type(prompt);
|
||||
await run.type('\r');
|
||||
const run = await rig.runInteractive();
|
||||
const prompt = 'Tell me a fun fact.';
|
||||
await run.type(prompt);
|
||||
await run.type('\r');
|
||||
|
||||
const hasUserPromptEvent = await rig.waitForTelemetryEvent('user_prompt');
|
||||
expect(hasUserPromptEvent).toBe(true);
|
||||
const hasUserPromptEvent = await rig.waitForTelemetryEvent('user_prompt');
|
||||
expect(hasUserPromptEvent).toBe(true);
|
||||
|
||||
const hasSessionCountMetric = await rig.waitForMetric('session.count');
|
||||
expect(hasSessionCountMetric).toBe(true);
|
||||
const hasSessionCountMetric = await rig.waitForMetric('session.count');
|
||||
expect(hasSessionCountMetric).toBe(true);
|
||||
|
||||
// We expect NO flicker event to be found.
|
||||
const flickerMetric = rig.readMetric('ui.flicker.count');
|
||||
expect(flickerMetric).toBeNull();
|
||||
} finally {
|
||||
await rig.cleanup();
|
||||
}
|
||||
// We expect NO flicker event to be found.
|
||||
const flickerMetric = rig.readMetric('ui.flicker.count');
|
||||
expect(flickerMetric).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -5,19 +5,26 @@
|
||||
*/
|
||||
|
||||
import { WEB_SEARCH_TOOL_NAME } from '../packages/core/src/tools/tool-names.js';
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
||||
import { TestRig, printDebugInfo, validateModelOutput } from './test-helper.js';
|
||||
|
||||
describe(WEB_SEARCH_TOOL_NAME, () => {
|
||||
describe('web search tool', () => {
|
||||
let rig: TestRig;
|
||||
|
||||
beforeEach(() => {
|
||||
rig = new TestRig();
|
||||
});
|
||||
|
||||
afterEach(async () => await rig.cleanup());
|
||||
|
||||
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] } },
|
||||
});
|
||||
|
||||
let result;
|
||||
try {
|
||||
result = await rig.run(`what is the weather in London`);
|
||||
result = await rig.run({ args: `what is the weather in London` });
|
||||
} catch (error) {
|
||||
// Network errors can occur in CI environments
|
||||
if (
|
||||
|
||||
@@ -55,8 +55,9 @@ describe('Hooks System Integration', () => {
|
||||
},
|
||||
);
|
||||
|
||||
const prompt = 'Create a file called test.txt with content "Hello World"';
|
||||
const result = await rig.run(prompt);
|
||||
const result = await rig.run({
|
||||
args: 'Create a file called test.txt with content "Hello World"',
|
||||
});
|
||||
|
||||
// The hook should block the write_file tool
|
||||
const toolLogs = rig.readToolLogs();
|
||||
@@ -107,9 +108,9 @@ describe('Hooks System Integration', () => {
|
||||
},
|
||||
);
|
||||
|
||||
const prompt =
|
||||
'Create a file called approved.txt with content "Approved content"';
|
||||
await rig.run(prompt);
|
||||
await rig.run({
|
||||
args: 'Create a file called approved.txt with content "Approved content"',
|
||||
});
|
||||
|
||||
// The hook should allow the write_file tool
|
||||
const foundWriteFile = await rig.waitForToolCall('write_file');
|
||||
@@ -158,9 +159,9 @@ describe('Hooks System Integration', () => {
|
||||
// Create a test file to read
|
||||
rig.createFile('test-file.txt', 'This is test content');
|
||||
|
||||
const prompt =
|
||||
'Read the contents of test-file.txt and tell me what it contains';
|
||||
await rig.run(prompt);
|
||||
await rig.run({
|
||||
args: 'Read the contents of test-file.txt and tell me what it contains',
|
||||
});
|
||||
|
||||
// Should find read_file tool call
|
||||
const foundReadFile = await rig.waitForToolCall('read_file');
|
||||
@@ -232,8 +233,7 @@ echo '{
|
||||
},
|
||||
});
|
||||
|
||||
const prompt = 'Tell me a story';
|
||||
const result = await rig.run(prompt);
|
||||
const result = await rig.run({ args: 'Tell me a story' });
|
||||
|
||||
// The hook should have replaced the request entirely
|
||||
// Verify that the model responded to the modified request, not the original
|
||||
@@ -316,8 +316,7 @@ echo '{
|
||||
},
|
||||
});
|
||||
|
||||
const prompt = 'What is 2 + 2?';
|
||||
const result = await rig.run(prompt);
|
||||
const result = await rig.run({ args: 'What is 2 + 2?' });
|
||||
|
||||
// The hook should have replaced the model response
|
||||
expect(result).toContain(
|
||||
@@ -374,9 +373,9 @@ echo '{
|
||||
// Create a test file
|
||||
rig.createFile('new_file_data.txt', 'test data');
|
||||
|
||||
const prompt =
|
||||
'Check the content of new_file_data.txt, after that run echo command to see the content';
|
||||
await rig.run(prompt);
|
||||
await rig.run({
|
||||
args: 'Check the content of new_file_data.txt, after that run echo command to see the content',
|
||||
});
|
||||
|
||||
// Should use read_file (allowed) but not run_shell_command (not in allowed list)
|
||||
const foundReadFile = await rig.waitForToolCall('read_file');
|
||||
@@ -440,8 +439,7 @@ echo '{
|
||||
},
|
||||
});
|
||||
|
||||
const prompt = 'Hello, how are you?';
|
||||
const result = await rig.run(prompt);
|
||||
const result = await rig.run({ args: 'Hello, how are you?' });
|
||||
|
||||
// The hook should have added security context, which should influence the response
|
||||
expect(result).toContain('security');
|
||||
@@ -585,8 +583,7 @@ echo '{
|
||||
},
|
||||
});
|
||||
|
||||
const prompt = 'Hello, please help me with a task';
|
||||
await rig.run(prompt);
|
||||
await rig.run({ args: 'Hello, please help me with a task' });
|
||||
|
||||
// Should generate hook telemetry
|
||||
const hookTelemetryFound = await rig.waitForTelemetryEvent('hook_call');
|
||||
@@ -663,8 +660,9 @@ fi`;
|
||||
},
|
||||
});
|
||||
|
||||
const prompt = 'Create a file called input-test.txt with content "test"';
|
||||
await rig.run(prompt);
|
||||
await rig.run({
|
||||
args: 'Create a file called input-test.txt with content "test"',
|
||||
});
|
||||
|
||||
// Hook should validate input format successfully
|
||||
const foundWriteFile = await rig.waitForToolCall('write_file');
|
||||
@@ -739,11 +737,12 @@ fi`;
|
||||
},
|
||||
});
|
||||
|
||||
const prompt =
|
||||
'Create a file called multi-event-test.txt with content ' +
|
||||
'"testing multiple events", and then please reply with ' +
|
||||
'everything I say just after this:"';
|
||||
const result = await rig.run(prompt);
|
||||
const result = await rig.run({
|
||||
args:
|
||||
'Create a file called multi-event-test.txt with content ' +
|
||||
'"testing multiple events", and then please reply with ' +
|
||||
'everything I say just after this:"',
|
||||
});
|
||||
|
||||
// Should execute write_file tool
|
||||
const foundWriteFile = await rig.waitForToolCall('write_file');
|
||||
@@ -834,9 +833,9 @@ fi`;
|
||||
},
|
||||
});
|
||||
|
||||
const prompt =
|
||||
'Create a file called error-test.txt with content "testing error handling"';
|
||||
await rig.run(prompt);
|
||||
await rig.run({
|
||||
args: 'Create a file called error-test.txt with content "testing error handling"',
|
||||
});
|
||||
|
||||
// Despite one hook failing, the working hook should still allow the operation
|
||||
const foundWriteFile = await rig.waitForToolCall('write_file');
|
||||
@@ -883,8 +882,7 @@ fi`;
|
||||
},
|
||||
});
|
||||
|
||||
const prompt = 'Create a file called telemetry-test.txt';
|
||||
await rig.run(prompt);
|
||||
await rig.run({ args: 'Create a file called telemetry-test.txt' });
|
||||
|
||||
// Should execute the tool
|
||||
const foundWriteFile = await rig.waitForToolCall('write_file');
|
||||
@@ -929,8 +927,7 @@ fi`;
|
||||
});
|
||||
|
||||
// Run a simple query - the SessionStart hook will fire during app initialization
|
||||
const prompt = 'Say hello';
|
||||
await rig.run(prompt);
|
||||
await rig.run({ args: 'Say hello' });
|
||||
|
||||
// Verify hook executed with correct parameters
|
||||
const hookLogs = rig.readHookLogs();
|
||||
@@ -1174,8 +1171,7 @@ fi`;
|
||||
});
|
||||
|
||||
// Run a simple query that will trigger automatic compression
|
||||
const prompt = 'Say hello in exactly 5 words';
|
||||
await rig.run(prompt);
|
||||
await rig.run({ args: 'Say hello in exactly 5 words' });
|
||||
|
||||
// Verify hook executed with correct parameters
|
||||
const hookLogs = rig.readHookLogs();
|
||||
@@ -1236,8 +1232,7 @@ fi`;
|
||||
});
|
||||
|
||||
// Run in non-interactive mode with a simple prompt
|
||||
const prompt = 'Hello';
|
||||
await rig.run(prompt);
|
||||
await rig.run({ args: 'Hello' });
|
||||
|
||||
// The process should exit gracefully, firing the SessionEnd hook
|
||||
// Wait for telemetry to be written to disk
|
||||
@@ -1344,9 +1339,9 @@ echo '{"decision": "block", "systemMessage": "Disabled hook should not execute",
|
||||
},
|
||||
});
|
||||
|
||||
const prompt =
|
||||
'Create a file called disabled-test.txt with content "test"';
|
||||
const result = await rig.run(prompt);
|
||||
const result = await rig.run({
|
||||
args: 'Create a file called disabled-test.txt with content "test"',
|
||||
});
|
||||
|
||||
// Tool should execute (enabled hook allows it)
|
||||
const foundWriteFile = await rig.waitForToolCall('write_file');
|
||||
@@ -1431,8 +1426,9 @@ echo '{"decision": "block", "systemMessage": "Disabled hook should not execute",
|
||||
);
|
||||
|
||||
// First run - only active hook should execute
|
||||
const prompt1 = 'Create a file called first-run.txt with "test1"';
|
||||
const result1 = await rig.run(prompt1);
|
||||
const result1 = await rig.run({
|
||||
args: 'Create a file called first-run.txt with "test1"',
|
||||
});
|
||||
|
||||
// Tool should execute (active hook allows it)
|
||||
const foundWriteFile1 = await rig.waitForToolCall('write_file');
|
||||
@@ -1455,8 +1451,9 @@ echo '{"decision": "block", "systemMessage": "Disabled hook should not execute",
|
||||
expect(disabledHookLog1).toBeUndefined();
|
||||
|
||||
// Second run - verify disabled hook stays disabled
|
||||
const prompt2 = 'Create a file called second-run.txt with "test2"';
|
||||
const result2 = await rig.run(prompt2);
|
||||
const result2 = await rig.run({
|
||||
args: 'Create a file called second-run.txt with "test2"',
|
||||
});
|
||||
|
||||
const foundWriteFile2 = await rig.waitForToolCall('write_file');
|
||||
expect(foundWriteFile2).toBeTruthy();
|
||||
|
||||
@@ -22,11 +22,9 @@ describe('JSON output', () => {
|
||||
});
|
||||
|
||||
it('should return a valid JSON with response and stats', async () => {
|
||||
const result = await rig.run(
|
||||
'What is the capital of France?',
|
||||
'--output-format',
|
||||
'json',
|
||||
);
|
||||
const result = await rig.run({
|
||||
args: ['What is the capital of France?', '--output-format', 'json'],
|
||||
});
|
||||
const parsed = JSON.parse(result);
|
||||
|
||||
expect(parsed).toHaveProperty('response');
|
||||
@@ -38,7 +36,9 @@ describe('JSON output', () => {
|
||||
});
|
||||
|
||||
it('should return a valid JSON with a session ID', async () => {
|
||||
const result = await rig.run('Hello', '--output-format', 'json');
|
||||
const result = await rig.run({
|
||||
args: ['Hello', '--output-format', 'json'],
|
||||
});
|
||||
const parsed = JSON.parse(result);
|
||||
|
||||
expect(parsed).toHaveProperty('session_id');
|
||||
@@ -58,7 +58,7 @@ describe('JSON output', () => {
|
||||
|
||||
let thrown: Error | undefined;
|
||||
try {
|
||||
await rig.run('Hello', '--output-format', 'json');
|
||||
await rig.run({ args: ['Hello', '--output-format', 'json'] });
|
||||
expect.fail('Expected process to exit with error');
|
||||
} catch (e) {
|
||||
thrown = e as Error;
|
||||
@@ -108,12 +108,14 @@ describe('JSON output', () => {
|
||||
'json-output.error.responses',
|
||||
),
|
||||
});
|
||||
const result = await rig.run(
|
||||
`Read the contents of ${rig.testDir}/path/to/nonexistent/file.txt and tell me what it says. ` +
|
||||
'On error, respond to the user with exactly the text "File not found".',
|
||||
'--output-format',
|
||||
'json',
|
||||
);
|
||||
const result = await rig.run({
|
||||
args: [
|
||||
`Read the contents of ${rig.testDir}/path/to/nonexistent/file.txt and tell me what it says. ` +
|
||||
'On error, respond to the user with exactly the text "File not found".',
|
||||
'--output-format',
|
||||
'json',
|
||||
],
|
||||
});
|
||||
|
||||
const parsed = JSON.parse(result);
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { describe, it } from 'vitest';
|
||||
import { describe, it, beforeEach, afterEach } from 'vitest';
|
||||
import {
|
||||
TestRig,
|
||||
poll,
|
||||
@@ -15,8 +15,15 @@ import { existsSync } from 'node:fs';
|
||||
import { join } from 'node:path';
|
||||
|
||||
describe('list_directory', () => {
|
||||
let rig: TestRig;
|
||||
|
||||
beforeEach(() => {
|
||||
rig = new TestRig();
|
||||
});
|
||||
|
||||
afterEach(async () => await rig.cleanup());
|
||||
|
||||
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'] } },
|
||||
});
|
||||
@@ -38,7 +45,7 @@ describe('list_directory', () => {
|
||||
|
||||
const prompt = `Can you list the files in the current directory.`;
|
||||
|
||||
const result = await rig.run(prompt);
|
||||
const result = await rig.run({ args: prompt });
|
||||
|
||||
try {
|
||||
await rig.expectToolCallSuccess(['list_directory']);
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
|
||||
import { writeFileSync } from 'node:fs';
|
||||
import { join } from 'node:path';
|
||||
import { beforeAll, describe, it } from 'vitest';
|
||||
import { describe, it, afterEach, beforeEach } from 'vitest';
|
||||
import { TestRig } from './test-helper.js';
|
||||
|
||||
// Create a minimal MCP server that doesn't require external dependencies
|
||||
@@ -166,9 +166,15 @@ rpc.send({
|
||||
`;
|
||||
|
||||
describe('mcp server with cyclic tool schema is detected', () => {
|
||||
const rig = new TestRig();
|
||||
let rig: TestRig;
|
||||
|
||||
beforeAll(async () => {
|
||||
beforeEach(() => {
|
||||
rig = new TestRig();
|
||||
});
|
||||
|
||||
afterEach(async () => await rig.cleanup());
|
||||
|
||||
it('mcp tool list should include tool with cyclic tool schema', async () => {
|
||||
// Setup test directory with MCP server configuration
|
||||
await rig.setup('cyclic-schema-mcp-server', {
|
||||
settings: {
|
||||
@@ -190,9 +196,7 @@ describe('mcp server with cyclic tool schema is detected', () => {
|
||||
const { chmodSync } = await import('node:fs');
|
||||
chmodSync(testServerPath, 0o755);
|
||||
}
|
||||
});
|
||||
|
||||
it('mcp tool list should include tool with cyclic tool schema', async () => {
|
||||
const run = await rig.runInteractive();
|
||||
|
||||
await run.type('/mcp list');
|
||||
|
||||
@@ -4,24 +4,29 @@
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
||||
import { TestRig } from './test-helper.js';
|
||||
|
||||
describe('mixed input crash prevention', () => {
|
||||
let rig: TestRig;
|
||||
|
||||
beforeEach(() => {
|
||||
rig = new TestRig();
|
||||
});
|
||||
|
||||
afterEach(async () => await rig.cleanup());
|
||||
|
||||
it('should not crash when using mixed prompt inputs', async () => {
|
||||
const rig = new TestRig();
|
||||
rig.setup('should not crash when using mixed prompt inputs');
|
||||
|
||||
// Test: echo "say '1'." | gemini --prompt-interactive="say '2'." say '3'.
|
||||
const stdinContent = "say '1'.";
|
||||
|
||||
try {
|
||||
await rig.run(
|
||||
{ stdin: stdinContent },
|
||||
'--prompt-interactive',
|
||||
"say '2'.",
|
||||
"say '3'.",
|
||||
);
|
||||
await rig.run({
|
||||
args: ['--prompt-interactive', "say '2'.", "say '3'."],
|
||||
stdin: stdinContent,
|
||||
});
|
||||
throw new Error('Expected the command to fail, but it succeeded');
|
||||
} catch (error: unknown) {
|
||||
expect(error).toBeInstanceOf(Error);
|
||||
@@ -40,15 +45,13 @@ describe('mixed input crash prevention', () => {
|
||||
});
|
||||
|
||||
it('should provide clear error message for mixed input', async () => {
|
||||
const rig = new TestRig();
|
||||
rig.setup('should provide clear error message for mixed input');
|
||||
|
||||
try {
|
||||
await rig.run(
|
||||
{ stdin: 'test input' },
|
||||
'--prompt-interactive',
|
||||
'test prompt',
|
||||
);
|
||||
await rig.run({
|
||||
args: ['--prompt-interactive', 'test prompt'],
|
||||
stdin: 'test input',
|
||||
});
|
||||
throw new Error('Expected the command to fail, but it succeeded');
|
||||
} catch (error: unknown) {
|
||||
expect(error).toBeInstanceOf(Error);
|
||||
|
||||
@@ -4,12 +4,19 @@
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
||||
import { TestRig, printDebugInfo, validateModelOutput } from './test-helper.js';
|
||||
|
||||
describe('read_many_files', () => {
|
||||
let rig: TestRig;
|
||||
|
||||
beforeEach(() => {
|
||||
rig = new TestRig();
|
||||
});
|
||||
|
||||
afterEach(async () => await rig.cleanup());
|
||||
|
||||
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'] } },
|
||||
});
|
||||
@@ -18,7 +25,7 @@ describe('read_many_files', () => {
|
||||
|
||||
const prompt = `Use the read_many_files tool to read the contents of file1.txt and file2.txt and then print the contents of each file.`;
|
||||
|
||||
const result = await rig.run(prompt);
|
||||
const result = await rig.run({ args: prompt });
|
||||
|
||||
// Check for either read_many_files or multiple read_file calls
|
||||
const allTools = rig.readToolLogs();
|
||||
@@ -45,6 +52,5 @@ describe('read_many_files', () => {
|
||||
|
||||
// Validate model output - will throw if no output
|
||||
validateModelOutput(result, null, 'Read many files test');
|
||||
await rig.cleanup();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -4,12 +4,18 @@
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
||||
import { TestRig } from './test-helper.js';
|
||||
|
||||
describe('replace', () => {
|
||||
let rig: TestRig;
|
||||
|
||||
beforeEach(() => {
|
||||
rig = new TestRig();
|
||||
});
|
||||
|
||||
afterEach(async () => await rig.cleanup());
|
||||
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'] } },
|
||||
});
|
||||
@@ -20,7 +26,9 @@ describe('replace', () => {
|
||||
|
||||
rig.createFile(fileName, originalContent);
|
||||
|
||||
await rig.run(`Replace 'foo' with 'bar' in the file 'file_to_replace.txt'`);
|
||||
await rig.run({
|
||||
args: `Replace 'foo' with 'bar' in the file 'file_to_replace.txt'`,
|
||||
});
|
||||
|
||||
const foundToolCall = await rig.waitForToolCall('replace');
|
||||
expect(foundToolCall, 'Expected to find a replace tool call').toBeTruthy();
|
||||
@@ -29,7 +37,6 @@ describe('replace', () => {
|
||||
});
|
||||
|
||||
it.skip('should handle $ literally when replacing text ending with $', async () => {
|
||||
const rig = new TestRig();
|
||||
await rig.setup(
|
||||
'should handle $ literally when replacing text ending with $',
|
||||
{ settings: { tools: { core: ['replace', 'read_file'] } } },
|
||||
@@ -41,9 +48,9 @@ describe('replace', () => {
|
||||
|
||||
rig.createFile(fileName, originalContent);
|
||||
|
||||
await rig.run(
|
||||
"Open regex.yml and append ' # updated' after the line containing ^[sv]d[a-z]$ without breaking the $ character.",
|
||||
);
|
||||
await rig.run({
|
||||
args: "Open regex.yml and append ' # updated' after the line containing ^[sv]d[a-z]$ without breaking the $ character.",
|
||||
});
|
||||
|
||||
const foundToolCall = await rig.waitForToolCall('replace');
|
||||
expect(foundToolCall, 'Expected to find a replace tool call').toBeTruthy();
|
||||
@@ -52,7 +59,6 @@ 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'] } },
|
||||
});
|
||||
@@ -64,7 +70,7 @@ describe('replace', () => {
|
||||
rig.createFile(fileName, originalContent);
|
||||
|
||||
const prompt = `In ${fileName}, replace "<INSERT_TEXT_HERE>" with:\n${newBlock}. Use unix style line endings.`;
|
||||
await rig.run(prompt);
|
||||
await rig.run({ args: prompt });
|
||||
|
||||
const foundToolCall = await rig.waitForToolCall('replace');
|
||||
expect(foundToolCall, 'Expected to find a replace tool call').toBeTruthy();
|
||||
@@ -73,7 +79,6 @@ 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'] } },
|
||||
});
|
||||
@@ -84,9 +89,9 @@ describe('replace', () => {
|
||||
const expectedContent = 'Hello\nWorld';
|
||||
rig.createFile(fileName, originalContent);
|
||||
|
||||
await rig.run(
|
||||
`In ${fileName}, delete the entire block from "## DELETE THIS ##" to "## END DELETE ##" including the markers and the newline that follows it.`,
|
||||
);
|
||||
await rig.run({
|
||||
args: `In ${fileName}, delete the entire block from "## DELETE THIS ##" to "## END DELETE ##" including the markers and the newline that follows it.`,
|
||||
});
|
||||
|
||||
const foundToolCall = await rig.waitForToolCall('replace');
|
||||
expect(foundToolCall, 'Expected to find a replace tool call').toBeTruthy();
|
||||
|
||||
@@ -27,6 +27,10 @@ class MockConfig {
|
||||
getDebugMode() {
|
||||
return true;
|
||||
}
|
||||
|
||||
getFileFilteringRespectGeminiIgnore() {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
describe('ripgrep-real-direct', () => {
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
||||
import { TestRig, printDebugInfo, validateModelOutput } from './test-helper.js';
|
||||
import { getShellConfiguration } from '../packages/core/src/utils/shell-utils.js';
|
||||
|
||||
@@ -84,15 +84,21 @@ function getChainedEchoCommand(): { allowPattern: string; command: string } {
|
||||
}
|
||||
|
||||
describe('run_shell_command', () => {
|
||||
let rig: TestRig;
|
||||
|
||||
beforeEach(() => {
|
||||
rig = new TestRig();
|
||||
});
|
||||
|
||||
afterEach(async () => await rig.cleanup());
|
||||
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'] } },
|
||||
});
|
||||
|
||||
const prompt = `Please run the command "echo hello-world" and show me the output`;
|
||||
|
||||
const result = await rig.run(prompt);
|
||||
const result = await rig.run({ args: prompt });
|
||||
|
||||
const foundToolCall = await rig.waitForToolCall('run_shell_command');
|
||||
|
||||
@@ -119,7 +125,6 @@ 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'] } },
|
||||
});
|
||||
@@ -149,7 +154,6 @@ describe('run_shell_command', () => {
|
||||
});
|
||||
|
||||
it.skip('should run allowed sub-command in non-interactive mode', async () => {
|
||||
const rig = new TestRig();
|
||||
await rig.setup('should run allowed sub-command in non-interactive mode');
|
||||
|
||||
const testFile = rig.createFile('test.txt', 'Lorem\nIpsum\nDolor\n');
|
||||
@@ -157,13 +161,11 @@ describe('run_shell_command', () => {
|
||||
const prompt = `use ${command} to tell me how many lines there are in ${testFile}`;
|
||||
|
||||
// Provide the prompt via stdin to simulate non-interactive mode
|
||||
const result = await rig.run(
|
||||
{
|
||||
stdin: prompt,
|
||||
yolo: false,
|
||||
},
|
||||
`--allowed-tools=run_shell_command(${tool})`,
|
||||
);
|
||||
const result = await rig.run({
|
||||
args: [`--allowed-tools=run_shell_command(${tool})`],
|
||||
stdin: prompt,
|
||||
yolo: false,
|
||||
});
|
||||
|
||||
const foundToolCall = await rig.waitForToolCall('run_shell_command', 15000);
|
||||
|
||||
@@ -196,20 +198,17 @@ describe('run_shell_command', () => {
|
||||
});
|
||||
|
||||
it.skip('should succeed with no parens in non-interactive mode', async () => {
|
||||
const rig = new TestRig();
|
||||
await rig.setup('should succeed with no parens in non-interactive mode');
|
||||
|
||||
const testFile = rig.createFile('test.txt', 'Lorem\nIpsum\nDolor\n');
|
||||
const { command } = getLineCountCommand();
|
||||
const prompt = `use ${command} to tell me how many lines there are in ${testFile}`;
|
||||
|
||||
const result = await rig.run(
|
||||
{
|
||||
stdin: prompt,
|
||||
yolo: false,
|
||||
},
|
||||
'--allowed-tools=run_shell_command',
|
||||
);
|
||||
const result = await rig.run({
|
||||
args: '--allowed-tools=run_shell_command',
|
||||
stdin: prompt,
|
||||
yolo: false,
|
||||
});
|
||||
|
||||
const foundToolCall = await rig.waitForToolCall('run_shell_command', 15000);
|
||||
|
||||
@@ -233,7 +232,6 @@ 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'] } },
|
||||
});
|
||||
@@ -243,7 +241,7 @@ describe('run_shell_command', () => {
|
||||
const prompt = `use ${command} to tell me how many lines there are in ${testFile}`;
|
||||
|
||||
const result = await rig.run({
|
||||
prompt: prompt,
|
||||
args: prompt,
|
||||
yolo: true,
|
||||
});
|
||||
|
||||
@@ -269,20 +267,17 @@ describe('run_shell_command', () => {
|
||||
});
|
||||
|
||||
it.skip('should work with ShellTool alias', async () => {
|
||||
const rig = new TestRig();
|
||||
await rig.setup('should work with ShellTool alias');
|
||||
|
||||
const testFile = rig.createFile('test.txt', 'Lorem\nIpsum\nDolor\n');
|
||||
const { tool, command } = getLineCountCommand();
|
||||
const prompt = `use ${command} to tell me how many lines there are in ${testFile}`;
|
||||
|
||||
const result = await rig.run(
|
||||
{
|
||||
stdin: prompt,
|
||||
yolo: false,
|
||||
},
|
||||
`--allowed-tools=ShellTool(${tool})`,
|
||||
);
|
||||
const result = await rig.run({
|
||||
args: `--allowed-tools=ShellTool(${tool})`,
|
||||
stdin: prompt,
|
||||
yolo: false,
|
||||
});
|
||||
|
||||
const foundToolCall = await rig.waitForToolCall('run_shell_command', 15000);
|
||||
|
||||
@@ -317,7 +312,6 @@ describe('run_shell_command', () => {
|
||||
// TODO(#11062): Un-skip this once we can make it reliable by using hard coded
|
||||
// model responses.
|
||||
it.skip('should combine multiple --allowed-tools flags', async () => {
|
||||
const rig = new TestRig();
|
||||
await rig.setup('should combine multiple --allowed-tools flags');
|
||||
|
||||
const { tool, command } = getLineCountCommand();
|
||||
@@ -325,14 +319,14 @@ describe('run_shell_command', () => {
|
||||
`use both ${command} and ls to count the number of lines in files in this ` +
|
||||
`directory. Do not pipe these commands into each other, run them separately.`;
|
||||
|
||||
const result = await rig.run(
|
||||
{
|
||||
stdin: prompt,
|
||||
yolo: false,
|
||||
},
|
||||
`--allowed-tools=run_shell_command(${tool})`,
|
||||
'--allowed-tools=run_shell_command(ls)',
|
||||
);
|
||||
const result = await rig.run({
|
||||
args: [
|
||||
`--allowed-tools=run_shell_command(${tool})`,
|
||||
'--allowed-tools=run_shell_command(ls)',
|
||||
],
|
||||
stdin: prompt,
|
||||
yolo: false,
|
||||
});
|
||||
|
||||
for (const expected in ['ls', tool]) {
|
||||
const foundToolCall = await rig.waitForToolCall(
|
||||
@@ -367,7 +361,6 @@ 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'] } },
|
||||
});
|
||||
@@ -381,13 +374,11 @@ describe('run_shell_command', () => {
|
||||
`If the command fails because it is not permitted, respond with the single word FAIL. ` +
|
||||
`If it succeeds, respond with SUCCESS.`;
|
||||
|
||||
const result = await rig.run(
|
||||
{
|
||||
stdin: prompt,
|
||||
yolo: false,
|
||||
},
|
||||
`--allowed-tools=run_shell_command(${allowedCommand})`,
|
||||
);
|
||||
const result = await rig.run({
|
||||
args: `--allowed-tools=run_shell_command(${allowedCommand})`,
|
||||
stdin: prompt,
|
||||
yolo: false,
|
||||
});
|
||||
|
||||
if (!result.toLowerCase().includes('fail')) {
|
||||
printDebugInfo(rig, result, {
|
||||
@@ -437,7 +428,6 @@ describe('run_shell_command', () => {
|
||||
|
||||
// TODO(#11966): Deflake this test and re-enable once the underlying race is resolved.
|
||||
it.skip('should reject chained commands when only the first segment is allowlisted in non-interactive mode', async () => {
|
||||
const rig = new TestRig();
|
||||
await rig.setup(
|
||||
'should reject chained commands when only the first segment is allowlisted',
|
||||
);
|
||||
@@ -445,13 +435,11 @@ describe('run_shell_command', () => {
|
||||
const chained = getChainedEchoCommand();
|
||||
const shellInjection = `!{${chained.command}}`;
|
||||
|
||||
await rig.run(
|
||||
{
|
||||
stdin: `${shellInjection}\n`,
|
||||
yolo: false,
|
||||
},
|
||||
`--allowed-tools=ShellTool(${chained.allowPattern})`,
|
||||
);
|
||||
await rig.run({
|
||||
args: `--allowed-tools=ShellTool(${chained.allowPattern})`,
|
||||
stdin: `${shellInjection}\n`,
|
||||
yolo: false,
|
||||
});
|
||||
|
||||
// CLI should refuse to execute the chained command without scheduling run_shell_command.
|
||||
const toolLogs = rig
|
||||
@@ -466,7 +454,6 @@ describe('run_shell_command', () => {
|
||||
});
|
||||
|
||||
it('should allow all with "ShellTool" and other specific tools', async () => {
|
||||
const rig = new TestRig();
|
||||
await rig.setup(
|
||||
'should allow all with "ShellTool" and other specific tools',
|
||||
{
|
||||
@@ -477,14 +464,14 @@ describe('run_shell_command', () => {
|
||||
const { tool } = getLineCountCommand();
|
||||
const prompt = `Please run the command "echo test-allow-all" and show me the output`;
|
||||
|
||||
const result = await rig.run(
|
||||
{
|
||||
stdin: prompt,
|
||||
yolo: false,
|
||||
},
|
||||
`--allowed-tools=run_shell_command(${tool})`,
|
||||
'--allowed-tools=run_shell_command',
|
||||
);
|
||||
const result = await rig.run({
|
||||
args: [
|
||||
`--allowed-tools=run_shell_command(${tool})`,
|
||||
'--allowed-tools=run_shell_command',
|
||||
],
|
||||
stdin: prompt,
|
||||
yolo: false,
|
||||
});
|
||||
|
||||
const foundToolCall = await rig.waitForToolCall('run_shell_command', 15000);
|
||||
|
||||
@@ -516,7 +503,6 @@ 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'] } },
|
||||
});
|
||||
@@ -527,7 +513,7 @@ describe('run_shell_command', () => {
|
||||
|
||||
try {
|
||||
const prompt = `Use echo to learn the value of the environment variable named ${varName} and tell me what it is.`;
|
||||
const result = await rig.run(prompt);
|
||||
const result = await rig.run({ args: prompt });
|
||||
|
||||
const foundToolCall = await rig.waitForToolCall('run_shell_command');
|
||||
|
||||
@@ -550,13 +536,12 @@ describe('run_shell_command', () => {
|
||||
});
|
||||
|
||||
it.skip('should run a platform-specific file listing command', async () => {
|
||||
const rig = new TestRig();
|
||||
await rig.setup('should run platform-specific file listing');
|
||||
const fileName = `test-file-${Math.random().toString(36).substring(7)}.txt`;
|
||||
rig.createFile(fileName, 'test content');
|
||||
|
||||
const prompt = `Run a shell command to list the files in the current directory and tell me what they are.`;
|
||||
const result = await rig.run(prompt);
|
||||
const result = await rig.run({ args: prompt });
|
||||
|
||||
const foundToolCall = await rig.waitForToolCall('run_shell_command');
|
||||
|
||||
@@ -578,14 +563,13 @@ 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'] } },
|
||||
});
|
||||
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.`,
|
||||
);
|
||||
const result = await rig.run({
|
||||
args: `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.`,
|
||||
});
|
||||
expect(result).toContain('FAIL');
|
||||
|
||||
const escapedInvalidCommand = JSON.stringify(invalidCommand).slice(1, -1);
|
||||
|
||||
@@ -4,12 +4,19 @@
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
||||
import { TestRig, printDebugInfo, validateModelOutput } from './test-helper.js';
|
||||
|
||||
describe('save_memory', () => {
|
||||
let rig: TestRig;
|
||||
|
||||
beforeEach(() => {
|
||||
rig = new TestRig();
|
||||
});
|
||||
|
||||
afterEach(async () => await rig.cleanup());
|
||||
|
||||
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'] } },
|
||||
});
|
||||
@@ -17,7 +24,7 @@ describe('save_memory', () => {
|
||||
const prompt = `remember that my favorite color is blue.
|
||||
|
||||
what is my favorite color? tell me that and surround it with $ symbol`;
|
||||
const result = await rig.run(prompt);
|
||||
const result = await rig.run({ args: prompt });
|
||||
|
||||
const foundToolCall = await rig.waitForToolCall('save_memory');
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
* external dependencies, making it compatible with Docker sandbox mode.
|
||||
*/
|
||||
|
||||
import { describe, it, beforeAll, expect } from 'vitest';
|
||||
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
||||
import { TestRig, poll, validateModelOutput } from './test-helper.js';
|
||||
import { join } from 'node:path';
|
||||
import { writeFileSync } from 'node:fs';
|
||||
@@ -165,9 +165,15 @@ rpc.send({
|
||||
`;
|
||||
|
||||
describe('simple-mcp-server', () => {
|
||||
const rig = new TestRig();
|
||||
let rig: TestRig;
|
||||
|
||||
beforeAll(async () => {
|
||||
beforeEach(() => {
|
||||
rig = new TestRig();
|
||||
});
|
||||
|
||||
afterEach(async () => await rig.cleanup());
|
||||
|
||||
it('should add two numbers', async () => {
|
||||
// Setup test directory with MCP server configuration
|
||||
await rig.setup('simple-mcp-server', {
|
||||
settings: {
|
||||
@@ -209,14 +215,12 @@ describe('simple-mcp-server', () => {
|
||||
if (!isReady) {
|
||||
throw new Error('MCP server script was not ready in time.');
|
||||
}
|
||||
});
|
||||
|
||||
it('should add two numbers', async () => {
|
||||
// Test directory is already set up in before hook
|
||||
// Just run the command - MCP server config is in settings.json
|
||||
const output = await rig.run(
|
||||
'Use the `add` tool to calculate 5+10 and output only the resulting number.',
|
||||
);
|
||||
const output = await rig.run({
|
||||
args: 'Use the `add` tool to calculate 5+10 and output only the resulting number.',
|
||||
});
|
||||
|
||||
const foundToolCall = await rig.waitForToolCall('add');
|
||||
|
||||
|
||||
@@ -4,19 +4,26 @@
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
||||
import { TestRig, printDebugInfo, validateModelOutput } from './test-helper.js';
|
||||
|
||||
describe.skip('stdin context', () => {
|
||||
let rig: TestRig;
|
||||
|
||||
beforeEach(() => {
|
||||
rig = new TestRig();
|
||||
});
|
||||
|
||||
afterEach(async () => await rig.cleanup());
|
||||
|
||||
it('should be able to use stdin as context for a prompt', async () => {
|
||||
const rig = new TestRig();
|
||||
await rig.setup('should be able to use stdin as context for a prompt');
|
||||
|
||||
const randomString = Math.random().toString(36).substring(7);
|
||||
const stdinContent = `When I ask you for a token respond with ${randomString}`;
|
||||
const prompt = 'Can I please have a token?';
|
||||
|
||||
const result = await rig.run({ prompt, stdin: stdinContent });
|
||||
const result = await rig.run({ args: prompt, stdin: stdinContent });
|
||||
|
||||
await rig.waitForTelemetryEvent('api_request');
|
||||
const lastRequest = rig.readLastApiRequest();
|
||||
@@ -75,7 +82,6 @@ describe.skip('stdin context', () => {
|
||||
even though gemini is intended to run interactively.
|
||||
*/
|
||||
|
||||
const rig = new TestRig();
|
||||
await rig.setup('should exit quickly if stdin stream does not end');
|
||||
|
||||
try {
|
||||
|
||||
@@ -4,16 +4,23 @@
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
||||
import { TestRig } from './test-helper.js';
|
||||
|
||||
describe('telemetry', () => {
|
||||
let rig: TestRig;
|
||||
|
||||
beforeEach(() => {
|
||||
rig = new TestRig();
|
||||
});
|
||||
|
||||
afterEach(async () => await rig.cleanup());
|
||||
|
||||
it('should emit a metric and a log event', async () => {
|
||||
const rig = new TestRig();
|
||||
rig.setup('should emit a metric and a log event');
|
||||
|
||||
// Run a simple command that should trigger telemetry
|
||||
await rig.run('just saying hi');
|
||||
await rig.run({ args: 'just saying hi' });
|
||||
|
||||
// Verify that a user_prompt event was logged
|
||||
const hasUserPromptEvent = await rig.waitForTelemetryEvent('user_prompt');
|
||||
|
||||
@@ -18,6 +18,7 @@ import * as os from 'node:os';
|
||||
import { GEMINI_DIR } from '../packages/core/src/utils/paths.js';
|
||||
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||
const BUNDLE_PATH = join(__dirname, '..', 'bundle/gemini.js');
|
||||
|
||||
// Get timeout based on environment
|
||||
function getDefaultTimeout() {
|
||||
@@ -195,17 +196,12 @@ export class InteractiveRun {
|
||||
if (!timeout) {
|
||||
timeout = getDefaultTimeout();
|
||||
}
|
||||
const found = await poll(
|
||||
await poll(
|
||||
() => stripAnsi(this.output).toLowerCase().includes(text.toLowerCase()),
|
||||
timeout,
|
||||
200,
|
||||
);
|
||||
expect(
|
||||
found,
|
||||
`Did not find expected text: "${text}". Output was:\n${stripAnsi(
|
||||
this.output,
|
||||
)}`,
|
||||
).toBe(true);
|
||||
expect(stripAnsi(this.output).toLowerCase()).toContain(text.toLowerCase());
|
||||
}
|
||||
|
||||
// This types slowly to make sure command is correct, but only work for short
|
||||
@@ -271,19 +267,14 @@ export class InteractiveRun {
|
||||
}
|
||||
|
||||
export class TestRig {
|
||||
bundlePath: string;
|
||||
testDir: string | null;
|
||||
testDir: string | null = null;
|
||||
testName?: string;
|
||||
_lastRunStdout?: string;
|
||||
// Path to the copied fake responses file for this test.
|
||||
fakeResponsesPath?: string;
|
||||
// Original fake responses file path for rewriting goldens in record mode.
|
||||
originalFakeResponsesPath?: string;
|
||||
|
||||
constructor() {
|
||||
this.bundlePath = join(__dirname, '..', 'bundle/gemini.js');
|
||||
this.testDir = null;
|
||||
}
|
||||
private _interactiveRuns: InteractiveRun[] = [];
|
||||
|
||||
setup(
|
||||
testName: string,
|
||||
@@ -374,7 +365,7 @@ export class TestRig {
|
||||
const command = isNpmReleaseTest ? 'gemini' : 'node';
|
||||
const initialArgs = isNpmReleaseTest
|
||||
? extraInitialArgs
|
||||
: [this.bundlePath, ...extraInitialArgs];
|
||||
: [BUNDLE_PATH, ...extraInitialArgs];
|
||||
if (this.fakeResponsesPath) {
|
||||
if (process.env['REGENERATE_MODEL_GOLDENS'] === 'true') {
|
||||
initialArgs.push('--record-responses', this.fakeResponsesPath);
|
||||
@@ -385,19 +376,13 @@ export class TestRig {
|
||||
return { command, initialArgs };
|
||||
}
|
||||
|
||||
run(
|
||||
promptOrOptions:
|
||||
| string
|
||||
| {
|
||||
prompt?: string;
|
||||
stdin?: string;
|
||||
stdinDoesNotEnd?: boolean;
|
||||
yolo?: boolean;
|
||||
},
|
||||
...args: string[]
|
||||
): Promise<string> {
|
||||
const yolo =
|
||||
typeof promptOrOptions === 'string' || promptOrOptions.yolo !== false;
|
||||
run(options: {
|
||||
args?: string | string[];
|
||||
stdin?: string;
|
||||
stdinDoesNotEnd?: boolean;
|
||||
yolo?: boolean;
|
||||
}): Promise<string> {
|
||||
const yolo = options.yolo !== false;
|
||||
const { command, initialArgs } = this._getCommandAndArgs(
|
||||
yolo ? ['--yolo'] : [],
|
||||
);
|
||||
@@ -411,21 +396,17 @@ export class TestRig {
|
||||
encoding: 'utf-8',
|
||||
};
|
||||
|
||||
if (typeof promptOrOptions === 'string') {
|
||||
commandArgs.push(promptOrOptions);
|
||||
} else if (
|
||||
typeof promptOrOptions === 'object' &&
|
||||
promptOrOptions !== null
|
||||
) {
|
||||
if (promptOrOptions.prompt) {
|
||||
commandArgs.push(promptOrOptions.prompt);
|
||||
}
|
||||
if (promptOrOptions.stdin) {
|
||||
execOptions.input = promptOrOptions.stdin;
|
||||
if (options.args) {
|
||||
if (Array.isArray(options.args)) {
|
||||
commandArgs.push(...options.args);
|
||||
} else {
|
||||
commandArgs.push(options.args);
|
||||
}
|
||||
}
|
||||
|
||||
commandArgs.push(...args);
|
||||
if (options.stdin) {
|
||||
execOptions.input = options.stdin;
|
||||
}
|
||||
|
||||
const child = spawn(command, commandArgs, {
|
||||
cwd: this.testDir!,
|
||||
@@ -441,10 +422,7 @@ export class TestRig {
|
||||
child.stdin!.write(execOptions.input);
|
||||
}
|
||||
|
||||
if (
|
||||
typeof promptOrOptions === 'object' &&
|
||||
!promptOrOptions.stdinDoesNotEnd
|
||||
) {
|
||||
if (!options.stdinDoesNotEnd) {
|
||||
child.stdin!.end();
|
||||
}
|
||||
|
||||
@@ -591,6 +569,18 @@ export class TestRig {
|
||||
}
|
||||
|
||||
async cleanup() {
|
||||
// Kill any interactive runs that are still active
|
||||
for (const run of this._interactiveRuns) {
|
||||
try {
|
||||
await run.kill();
|
||||
} catch (error) {
|
||||
if (env['VERBOSE'] === 'true') {
|
||||
console.warn('Failed to kill interactive run during cleanup:', error);
|
||||
}
|
||||
}
|
||||
}
|
||||
this._interactiveRuns = [];
|
||||
|
||||
if (
|
||||
process.env['REGENERATE_MODEL_GOLDENS'] === 'true' &&
|
||||
this.fakeResponsesPath
|
||||
@@ -1024,26 +1014,23 @@ 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;
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
async runInteractive(options?: {
|
||||
args?: string | string[];
|
||||
yolo?: boolean;
|
||||
}): Promise<InteractiveRun> {
|
||||
const yolo = options?.yolo !== false;
|
||||
const { command, initialArgs } = this._getCommandAndArgs(
|
||||
yolo ? ['--yolo'] : [],
|
||||
);
|
||||
const commandArgs = [...initialArgs, ...additionalArgs];
|
||||
const commandArgs = [...initialArgs];
|
||||
|
||||
if (options?.args) {
|
||||
if (Array.isArray(options.args)) {
|
||||
commandArgs.push(...options.args);
|
||||
} else {
|
||||
commandArgs.push(options.args);
|
||||
}
|
||||
}
|
||||
|
||||
const ptyOptions: pty.IPtyForkOptions = {
|
||||
name: 'xterm-color',
|
||||
@@ -1059,6 +1046,7 @@ export class TestRig {
|
||||
const ptyProcess = pty.spawn(executable, commandArgs, ptyOptions);
|
||||
|
||||
const run = new InteractiveRun(ptyProcess);
|
||||
this._interactiveRuns.push(run);
|
||||
// Wait for the app to be ready
|
||||
await run.expectText(' Type your message or @path/to/file', 30000);
|
||||
return run;
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { describe, it, expect, beforeAll, afterAll } from 'vitest';
|
||||
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
||||
import { writeFileSync, readFileSync } from 'node:fs';
|
||||
import { join, resolve } from 'node:path';
|
||||
import { TestRig } from './test-helper.js';
|
||||
@@ -47,30 +47,26 @@ const utf32BE = (s: string) => {
|
||||
return Buffer.concat([bom, payload]);
|
||||
};
|
||||
|
||||
let rig: TestRig;
|
||||
let dir: string;
|
||||
|
||||
describe('BOM end-to-end integraion', () => {
|
||||
beforeAll(async () => {
|
||||
let rig: TestRig;
|
||||
|
||||
beforeEach(async () => {
|
||||
rig = new TestRig();
|
||||
await rig.setup('bom-integration', {
|
||||
settings: { tools: { core: ['read_file'] } },
|
||||
});
|
||||
dir = rig.testDir!;
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await rig.cleanup();
|
||||
});
|
||||
afterEach(async () => await rig.cleanup());
|
||||
|
||||
async function runAndAssert(
|
||||
filename: string,
|
||||
content: Buffer,
|
||||
expectedText: string | null,
|
||||
) {
|
||||
writeFileSync(join(dir, filename), content);
|
||||
writeFileSync(join(rig.testDir!, filename), content);
|
||||
const prompt = `read the file ${filename} and output its exact contents`;
|
||||
const output = await rig.run(prompt);
|
||||
const output = await rig.run({ args: prompt });
|
||||
await rig.waitForToolCall('read_file');
|
||||
const lower = output.toLowerCase();
|
||||
if (expectedText === null) {
|
||||
@@ -128,9 +124,9 @@ describe('BOM end-to-end integraion', () => {
|
||||
);
|
||||
const imageContent = readFileSync(imagePath);
|
||||
const filename = 'gemini-screenshot.png';
|
||||
writeFileSync(join(dir, filename), imageContent);
|
||||
writeFileSync(join(rig.testDir!, filename), imageContent);
|
||||
const prompt = `What is shown in the image ${filename}?`;
|
||||
const output = await rig.run(prompt);
|
||||
const output = await rig.run({ args: prompt });
|
||||
await rig.waitForToolCall('read_file');
|
||||
const lower = output.toLowerCase();
|
||||
// The response is non-deterministic, so we just check for some
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { describe, it, expect, vi } from 'vitest';
|
||||
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
||||
import {
|
||||
TestRig,
|
||||
createToolCallErrorMessage,
|
||||
@@ -13,14 +13,21 @@ import {
|
||||
} from './test-helper.js';
|
||||
|
||||
describe('write_file', () => {
|
||||
let rig: TestRig;
|
||||
|
||||
beforeEach(() => {
|
||||
rig = new TestRig();
|
||||
});
|
||||
|
||||
afterEach(async () => await rig.cleanup());
|
||||
|
||||
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'] } },
|
||||
});
|
||||
const prompt = `show me an example of using the write tool. put a dad joke in dad.txt`;
|
||||
|
||||
const result = await rig.run(prompt);
|
||||
const result = await rig.run({ args: prompt });
|
||||
|
||||
const foundToolCall = await rig.waitForToolCall('write_file');
|
||||
|
||||
|
||||
Generated
+852
-189
File diff suppressed because it is too large
Load Diff
+5
-3
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@google/gemini-cli",
|
||||
"version": "0.21.0-nightly.20251209.ec9a8c7a7",
|
||||
"version": "0.23.0",
|
||||
"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.0-nightly.20251209.ec9a8c7a7"
|
||||
"sandboxImageUri": "us-docker.pkg.dev/gemini-code-dev/gemini-cli/sandbox:0.23.0"
|
||||
},
|
||||
"scripts": {
|
||||
"start": "cross-env NODE_ENV=development node scripts/start.js",
|
||||
@@ -89,12 +89,13 @@
|
||||
"@vitest/coverage-v8": "^3.1.1",
|
||||
"@vitest/eslint-plugin": "^1.3.4",
|
||||
"cross-env": "^7.0.3",
|
||||
"depcheck": "^1.4.7",
|
||||
"esbuild": "^0.25.0",
|
||||
"esbuild-plugin-wasm": "^1.1.0",
|
||||
"eslint": "^9.24.0",
|
||||
"eslint-config-prettier": "^10.1.2",
|
||||
"eslint-plugin-headers": "^1.3.3",
|
||||
"eslint-plugin-import": "^2.31.0",
|
||||
"eslint-plugin-license-header": "^0.8.0",
|
||||
"eslint-plugin-react": "^7.37.5",
|
||||
"eslint-plugin-react-hooks": "^5.2.0",
|
||||
"glob": "^12.0.0",
|
||||
@@ -113,6 +114,7 @@
|
||||
"react-devtools-core": "^6.1.2",
|
||||
"semver": "^7.7.2",
|
||||
"strip-ansi": "^7.1.2",
|
||||
"ts-prune": "^0.10.3",
|
||||
"tsx": "^4.20.3",
|
||||
"typescript-eslint": "^8.30.1",
|
||||
"vitest": "^3.2.4",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@google/gemini-cli-a2a-server",
|
||||
"version": "0.21.0-nightly.20251209.ec9a8c7a7",
|
||||
"version": "0.23.0",
|
||||
"description": "Gemini CLI A2A Server",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
@@ -25,10 +25,10 @@
|
||||
"dist"
|
||||
],
|
||||
"dependencies": {
|
||||
"@a2a-js/sdk": "^0.3.2",
|
||||
"@a2a-js/sdk": "^0.3.7",
|
||||
"@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",
|
||||
|
||||
@@ -12,11 +12,7 @@ import type {
|
||||
RequestContext,
|
||||
ExecutionEventBus,
|
||||
} from '@a2a-js/sdk/server';
|
||||
import type {
|
||||
ToolCallRequestInfo,
|
||||
ServerGeminiToolCallRequestEvent,
|
||||
Config,
|
||||
} from '@google/gemini-cli-core';
|
||||
import type { ToolCallRequestInfo, Config } from '@google/gemini-cli-core';
|
||||
import {
|
||||
GeminiEventType,
|
||||
SimpleExtensionLoader,
|
||||
@@ -127,6 +123,7 @@ export class CoderAgentExecutor implements AgentExecutor {
|
||||
contextId,
|
||||
config,
|
||||
eventBus,
|
||||
agentSettings.autoExecute,
|
||||
);
|
||||
runtimeTask.taskState = persistedState._taskState;
|
||||
await runtimeTask.geminiClient.initialize();
|
||||
@@ -145,7 +142,13 @@ export class CoderAgentExecutor implements AgentExecutor {
|
||||
): Promise<TaskWrapper> {
|
||||
const agentSettings = agentSettingsInput || ({} as AgentSettings);
|
||||
const config = await this.getConfig(agentSettings, taskId);
|
||||
const runtimeTask = await Task.create(taskId, contextId, config, eventBus);
|
||||
const runtimeTask = await Task.create(
|
||||
taskId,
|
||||
contextId,
|
||||
config,
|
||||
eventBus,
|
||||
agentSettings.autoExecute,
|
||||
);
|
||||
await runtimeTask.geminiClient.initialize();
|
||||
|
||||
const wrapper = new TaskWrapper(runtimeTask, agentSettings);
|
||||
@@ -280,8 +283,8 @@ export class CoderAgentExecutor implements AgentExecutor {
|
||||
requestContext: RequestContext,
|
||||
eventBus: ExecutionEventBus,
|
||||
): Promise<void> {
|
||||
const userMessage = requestContext.userMessage as Message;
|
||||
const sdkTask = requestContext.task as SDKTask | undefined;
|
||||
const userMessage = requestContext.userMessage;
|
||||
const sdkTask = requestContext.task;
|
||||
|
||||
const taskId = sdkTask?.id || userMessage.taskId || uuidv4();
|
||||
const contextId: string =
|
||||
@@ -478,9 +481,7 @@ export class CoderAgentExecutor implements AgentExecutor {
|
||||
throw new Error('Execution aborted');
|
||||
}
|
||||
if (event.type === GeminiEventType.ToolCallRequest) {
|
||||
toolCallRequests.push(
|
||||
(event as ServerGeminiToolCallRequestEvent).value,
|
||||
);
|
||||
toolCallRequests.push(event.value);
|
||||
continue;
|
||||
}
|
||||
await currentTask.acceptAgentMessage(event);
|
||||
|
||||
@@ -20,6 +20,8 @@ import {
|
||||
type ToolCallRequestInfo,
|
||||
type GitService,
|
||||
type CompletedToolCall,
|
||||
ApprovalMode,
|
||||
ToolConfirmationOutcome,
|
||||
} from '@google/gemini-cli-core';
|
||||
import { createMockConfig } from '../utils/testing_utils.js';
|
||||
import type { ExecutionEventBus, RequestContext } from '@a2a-js/sdk/server';
|
||||
@@ -353,10 +355,12 @@ describe('Task', () => {
|
||||
let task: Task;
|
||||
type SpyInstance = ReturnType<typeof vi.spyOn>;
|
||||
let setTaskStateAndPublishUpdateSpy: SpyInstance;
|
||||
let mockConfig: Config;
|
||||
let mockEventBus: ExecutionEventBus;
|
||||
|
||||
beforeEach(() => {
|
||||
const mockConfig = createMockConfig();
|
||||
const mockEventBus: ExecutionEventBus = {
|
||||
mockConfig = createMockConfig() as Config;
|
||||
mockEventBus = {
|
||||
publish: vi.fn(),
|
||||
on: vi.fn(),
|
||||
off: vi.fn(),
|
||||
@@ -366,12 +370,7 @@ describe('Task', () => {
|
||||
};
|
||||
|
||||
// @ts-expect-error - Calling private constructor
|
||||
task = new Task(
|
||||
'task-id',
|
||||
'context-id',
|
||||
mockConfig as Config,
|
||||
mockEventBus,
|
||||
);
|
||||
task = new Task('task-id', 'context-id', mockConfig, mockEventBus);
|
||||
|
||||
// Spy on the method we want to check calls for
|
||||
setTaskStateAndPublishUpdateSpy = vi.spyOn(
|
||||
@@ -465,6 +464,67 @@ describe('Task', () => {
|
||||
);
|
||||
expect(finalCall).toBeUndefined();
|
||||
});
|
||||
|
||||
describe('auto-approval', () => {
|
||||
it('should auto-approve tool calls when autoExecute is true', () => {
|
||||
task.autoExecute = true;
|
||||
const onConfirmSpy = vi.fn();
|
||||
const toolCalls = [
|
||||
{
|
||||
request: { callId: '1' },
|
||||
status: 'awaiting_approval',
|
||||
confirmationDetails: { onConfirm: onConfirmSpy },
|
||||
},
|
||||
] as unknown as ToolCall[];
|
||||
|
||||
// @ts-expect-error - Calling private method
|
||||
task._schedulerToolCallsUpdate(toolCalls);
|
||||
|
||||
expect(onConfirmSpy).toHaveBeenCalledWith(
|
||||
ToolConfirmationOutcome.ProceedOnce,
|
||||
);
|
||||
});
|
||||
|
||||
it('should auto-approve tool calls when approval mode is YOLO', () => {
|
||||
(mockConfig.getApprovalMode as Mock).mockReturnValue(ApprovalMode.YOLO);
|
||||
task.autoExecute = false;
|
||||
const onConfirmSpy = vi.fn();
|
||||
const toolCalls = [
|
||||
{
|
||||
request: { callId: '1' },
|
||||
status: 'awaiting_approval',
|
||||
confirmationDetails: { onConfirm: onConfirmSpy },
|
||||
},
|
||||
] as unknown as ToolCall[];
|
||||
|
||||
// @ts-expect-error - Calling private method
|
||||
task._schedulerToolCallsUpdate(toolCalls);
|
||||
|
||||
expect(onConfirmSpy).toHaveBeenCalledWith(
|
||||
ToolConfirmationOutcome.ProceedOnce,
|
||||
);
|
||||
});
|
||||
|
||||
it('should NOT auto-approve when autoExecute is false and mode is not YOLO', () => {
|
||||
task.autoExecute = false;
|
||||
(mockConfig.getApprovalMode as Mock).mockReturnValue(
|
||||
ApprovalMode.DEFAULT,
|
||||
);
|
||||
const onConfirmSpy = vi.fn();
|
||||
const toolCalls = [
|
||||
{
|
||||
request: { callId: '1' },
|
||||
status: 'awaiting_approval',
|
||||
confirmationDetails: { onConfirm: onConfirmSpy },
|
||||
},
|
||||
] as unknown as ToolCall[];
|
||||
|
||||
// @ts-expect-error - Calling private method
|
||||
task._schedulerToolCallsUpdate(toolCalls);
|
||||
|
||||
expect(onConfirmSpy).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('currentPromptId and promptCount', () => {
|
||||
|
||||
@@ -73,6 +73,7 @@ export class Task {
|
||||
modelInfo?: string;
|
||||
currentPromptId: string | undefined;
|
||||
promptCount = 0;
|
||||
autoExecute: boolean;
|
||||
|
||||
// For tool waiting logic
|
||||
private pendingToolCalls: Map<string, string> = new Map(); //toolCallId --> status
|
||||
@@ -87,6 +88,7 @@ export class Task {
|
||||
contextId: string,
|
||||
config: Config,
|
||||
eventBus?: ExecutionEventBus,
|
||||
autoExecute = false,
|
||||
) {
|
||||
this.id = id;
|
||||
this.contextId = contextId;
|
||||
@@ -98,6 +100,7 @@ export class Task {
|
||||
this.eventBus = eventBus;
|
||||
this.completedToolCalls = [];
|
||||
this._resetToolCompletionPromise();
|
||||
this.autoExecute = autoExecute;
|
||||
this.config.setFallbackModelHandler(
|
||||
// For a2a-server, we want to automatically switch to the fallback model
|
||||
// for future requests without retrying the current one. The 'stop'
|
||||
@@ -111,15 +114,16 @@ export class Task {
|
||||
contextId: string,
|
||||
config: Config,
|
||||
eventBus?: ExecutionEventBus,
|
||||
autoExecute?: boolean,
|
||||
): Promise<Task> {
|
||||
return new Task(id, contextId, config, eventBus);
|
||||
return new Task(id, contextId, config, eventBus, autoExecute);
|
||||
}
|
||||
|
||||
// Note: `getAllMCPServerStatuses` retrieves the status of all MCP servers for the entire
|
||||
// process. This is not scoped to the individual task but reflects the global connection
|
||||
// state managed within the @gemini-cli/core module.
|
||||
async getMetadata(): Promise<TaskMetadata> {
|
||||
const toolRegistry = await this.config.getToolRegistry();
|
||||
const toolRegistry = this.config.getToolRegistry();
|
||||
const mcpServers = this.config.getMcpClientManager()?.getMcpServers() || {};
|
||||
const serverStatuses = getAllMCPServerStatuses();
|
||||
const servers = Object.keys(mcpServers).map((serverName) => ({
|
||||
@@ -396,8 +400,15 @@ export class Task {
|
||||
}
|
||||
});
|
||||
|
||||
if (this.config.getApprovalMode() === ApprovalMode.YOLO) {
|
||||
logger.info('[Task] YOLO mode enabled. Auto-approving all tool calls.');
|
||||
if (
|
||||
this.autoExecute ||
|
||||
this.config.getApprovalMode() === ApprovalMode.YOLO
|
||||
) {
|
||||
logger.info(
|
||||
'[Task] ' +
|
||||
(this.autoExecute ? '' : '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
|
||||
@@ -736,8 +747,8 @@ export class Task {
|
||||
return false;
|
||||
}
|
||||
|
||||
const callId = part.data['callId'] as string;
|
||||
const outcomeString = part.data['outcome'] as string;
|
||||
const callId = part.data['callId'];
|
||||
const outcomeString = part.data['outcome'];
|
||||
let confirmationOutcome: ToolConfirmationOutcome | undefined;
|
||||
|
||||
if (outcomeString === 'proceed_once') {
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
*/
|
||||
|
||||
import { ExtensionsCommand } from './extensions.js';
|
||||
import { InitCommand } from './init.js';
|
||||
import { RestoreCommand } from './restore.js';
|
||||
import type { Command } from './types.js';
|
||||
|
||||
@@ -14,6 +15,7 @@ class CommandRegistry {
|
||||
constructor() {
|
||||
this.register(new ExtensionsCommand());
|
||||
this.register(new RestoreCommand());
|
||||
this.register(new InitCommand());
|
||||
}
|
||||
|
||||
register(command: Command) {
|
||||
|
||||
@@ -0,0 +1,182 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2025 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import { InitCommand } from './init.js';
|
||||
import { performInit } from '@google/gemini-cli-core';
|
||||
import * as fs from 'node:fs';
|
||||
import * as path from 'node:path';
|
||||
import { CoderAgentExecutor } from '../agent/executor.js';
|
||||
import { CoderAgentEvent } from '../types.js';
|
||||
import type { ExecutionEventBus } from '@a2a-js/sdk/server';
|
||||
import { createMockConfig } from '../utils/testing_utils.js';
|
||||
import type { CommandContext } from './types.js';
|
||||
import type { CommandActionReturn, Config } from '@google/gemini-cli-core';
|
||||
import { logger } from '../utils/logger.js';
|
||||
|
||||
vi.mock('@google/gemini-cli-core', async (importOriginal) => {
|
||||
const actual =
|
||||
await importOriginal<typeof import('@google/gemini-cli-core')>();
|
||||
return {
|
||||
...actual,
|
||||
performInit: vi.fn(),
|
||||
};
|
||||
});
|
||||
|
||||
vi.mock('node:fs', () => ({
|
||||
existsSync: vi.fn(),
|
||||
writeFileSync: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock('../agent/executor.js', () => ({
|
||||
CoderAgentExecutor: vi.fn().mockImplementation(() => ({
|
||||
execute: vi.fn(),
|
||||
})),
|
||||
}));
|
||||
|
||||
vi.mock('../utils/logger.js', () => ({
|
||||
logger: {
|
||||
info: vi.fn(),
|
||||
error: vi.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
describe('InitCommand', () => {
|
||||
let eventBus: ExecutionEventBus;
|
||||
let command: InitCommand;
|
||||
let context: CommandContext;
|
||||
let publishSpy: ReturnType<typeof vi.spyOn>;
|
||||
let mockExecute: ReturnType<typeof vi.fn>;
|
||||
const mockWorkspacePath = path.resolve('/tmp');
|
||||
|
||||
beforeEach(() => {
|
||||
process.env['CODER_AGENT_WORKSPACE_PATH'] = mockWorkspacePath;
|
||||
eventBus = {
|
||||
publish: vi.fn(),
|
||||
} as unknown as ExecutionEventBus;
|
||||
command = new InitCommand();
|
||||
const mockConfig = createMockConfig({
|
||||
getModel: () => 'gemini-pro',
|
||||
});
|
||||
const mockExecutorInstance = new CoderAgentExecutor();
|
||||
context = {
|
||||
config: mockConfig as unknown as Config,
|
||||
agentExecutor: mockExecutorInstance,
|
||||
eventBus,
|
||||
} as CommandContext;
|
||||
publishSpy = vi.spyOn(eventBus, 'publish');
|
||||
mockExecute = vi.fn();
|
||||
vi.spyOn(mockExecutorInstance, 'execute').mockImplementation(mockExecute);
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it('has requiresWorkspace set to true', () => {
|
||||
expect(command.requiresWorkspace).toBe(true);
|
||||
});
|
||||
|
||||
describe('execute', () => {
|
||||
it('handles info from performInit', async () => {
|
||||
vi.mocked(performInit).mockReturnValue({
|
||||
type: 'message',
|
||||
messageType: 'info',
|
||||
content: 'GEMINI.md already exists.',
|
||||
} as CommandActionReturn);
|
||||
|
||||
await command.execute(context, []);
|
||||
|
||||
expect(logger.info).toHaveBeenCalledWith(
|
||||
'[EventBus event]: ',
|
||||
expect.objectContaining({
|
||||
kind: 'status-update',
|
||||
status: expect.objectContaining({
|
||||
state: 'completed',
|
||||
message: expect.objectContaining({
|
||||
parts: [{ kind: 'text', text: 'GEMINI.md already exists.' }],
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
);
|
||||
|
||||
expect(publishSpy).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
kind: 'status-update',
|
||||
status: expect.objectContaining({
|
||||
state: 'completed',
|
||||
message: expect.objectContaining({
|
||||
parts: [{ kind: 'text', text: 'GEMINI.md already exists.' }],
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('handles error from performInit', async () => {
|
||||
vi.mocked(performInit).mockReturnValue({
|
||||
type: 'message',
|
||||
messageType: 'error',
|
||||
content: 'An error occurred.',
|
||||
} as CommandActionReturn);
|
||||
|
||||
await command.execute(context, []);
|
||||
|
||||
expect(publishSpy).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
kind: 'status-update',
|
||||
status: expect.objectContaining({
|
||||
state: 'failed',
|
||||
message: expect.objectContaining({
|
||||
parts: [{ kind: 'text', text: 'An error occurred.' }],
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
describe('when handling submit_prompt', () => {
|
||||
beforeEach(() => {
|
||||
vi.mocked(performInit).mockReturnValue({
|
||||
type: 'submit_prompt',
|
||||
content: 'Create a new GEMINI.md file.',
|
||||
} as CommandActionReturn);
|
||||
});
|
||||
|
||||
it('writes the file and executes the agent', async () => {
|
||||
await command.execute(context, []);
|
||||
|
||||
expect(fs.writeFileSync).toHaveBeenCalledWith(
|
||||
path.join(mockWorkspacePath, 'GEMINI.md'),
|
||||
'',
|
||||
'utf8',
|
||||
);
|
||||
expect(mockExecute).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('passes autoExecute to the agent executor', async () => {
|
||||
await command.execute(context, []);
|
||||
|
||||
expect(mockExecute).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
userMessage: expect.objectContaining({
|
||||
parts: expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
text: 'Create a new GEMINI.md file.',
|
||||
}),
|
||||
]),
|
||||
metadata: {
|
||||
coderAgent: {
|
||||
kind: CoderAgentEvent.StateAgentSettingsEvent,
|
||||
workspacePath: mockWorkspacePath,
|
||||
autoExecute: true,
|
||||
},
|
||||
},
|
||||
}),
|
||||
}),
|
||||
eventBus,
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,168 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2025 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import * as fs from 'node:fs';
|
||||
import * as path from 'node:path';
|
||||
import { CoderAgentEvent, type AgentSettings } from '../types.js';
|
||||
import { performInit } from '@google/gemini-cli-core';
|
||||
import type {
|
||||
Command,
|
||||
CommandContext,
|
||||
CommandExecutionResponse,
|
||||
} from './types.js';
|
||||
import type { CoderAgentExecutor } from '../agent/executor.js';
|
||||
import type {
|
||||
ExecutionEventBus,
|
||||
RequestContext,
|
||||
AgentExecutionEvent,
|
||||
} from '@a2a-js/sdk/server';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import { logger } from '../utils/logger.js';
|
||||
|
||||
export class InitCommand implements Command {
|
||||
name = 'init';
|
||||
description = 'Analyzes the project and creates a tailored GEMINI.md file';
|
||||
requiresWorkspace = true;
|
||||
streaming = true;
|
||||
|
||||
private handleMessageResult(
|
||||
result: { content: string; messageType: 'info' | 'error' },
|
||||
context: CommandContext,
|
||||
eventBus: ExecutionEventBus,
|
||||
taskId: string,
|
||||
contextId: string,
|
||||
): CommandExecutionResponse {
|
||||
const statusState = result.messageType === 'error' ? 'failed' : 'completed';
|
||||
const eventType =
|
||||
result.messageType === 'error'
|
||||
? CoderAgentEvent.StateChangeEvent
|
||||
: CoderAgentEvent.TextContentEvent;
|
||||
|
||||
const event: AgentExecutionEvent = {
|
||||
kind: 'status-update',
|
||||
taskId,
|
||||
contextId,
|
||||
status: {
|
||||
state: statusState,
|
||||
message: {
|
||||
kind: 'message',
|
||||
role: 'agent',
|
||||
parts: [{ kind: 'text', text: result.content }],
|
||||
messageId: uuidv4(),
|
||||
taskId,
|
||||
contextId,
|
||||
},
|
||||
timestamp: new Date().toISOString(),
|
||||
},
|
||||
final: true,
|
||||
metadata: {
|
||||
coderAgent: { kind: eventType },
|
||||
model: context.config.getModel(),
|
||||
},
|
||||
};
|
||||
|
||||
logger.info('[EventBus event]: ', event);
|
||||
eventBus.publish(event);
|
||||
return {
|
||||
name: this.name,
|
||||
data: result,
|
||||
};
|
||||
}
|
||||
|
||||
private async handleSubmitPromptResult(
|
||||
result: { content: unknown },
|
||||
context: CommandContext,
|
||||
geminiMdPath: string,
|
||||
eventBus: ExecutionEventBus,
|
||||
taskId: string,
|
||||
contextId: string,
|
||||
): Promise<CommandExecutionResponse> {
|
||||
fs.writeFileSync(geminiMdPath, '', 'utf8');
|
||||
|
||||
if (!context.agentExecutor) {
|
||||
throw new Error('Agent executor not found in context.');
|
||||
}
|
||||
const agentExecutor = context.agentExecutor as CoderAgentExecutor;
|
||||
|
||||
const agentSettings: AgentSettings = {
|
||||
kind: CoderAgentEvent.StateAgentSettingsEvent,
|
||||
workspacePath: process.env['CODER_AGENT_WORKSPACE_PATH']!,
|
||||
autoExecute: true,
|
||||
};
|
||||
|
||||
if (typeof result.content !== 'string') {
|
||||
throw new Error('Init command content must be a string.');
|
||||
}
|
||||
const promptText = result.content;
|
||||
|
||||
const requestContext: RequestContext = {
|
||||
userMessage: {
|
||||
kind: 'message',
|
||||
role: 'user',
|
||||
parts: [{ kind: 'text', text: promptText }],
|
||||
messageId: uuidv4(),
|
||||
taskId,
|
||||
contextId,
|
||||
metadata: {
|
||||
coderAgent: agentSettings,
|
||||
},
|
||||
},
|
||||
taskId,
|
||||
contextId,
|
||||
};
|
||||
|
||||
// The executor will handle the entire agentic loop, including
|
||||
// creating the task, streaming responses, and handling tools.
|
||||
await agentExecutor.execute(requestContext, eventBus);
|
||||
return {
|
||||
name: this.name,
|
||||
data: geminiMdPath,
|
||||
};
|
||||
}
|
||||
|
||||
async execute(
|
||||
context: CommandContext,
|
||||
_args: string[] = [],
|
||||
): Promise<CommandExecutionResponse> {
|
||||
if (!context.eventBus) {
|
||||
return {
|
||||
name: this.name,
|
||||
data: 'Use executeStream to get streaming results.',
|
||||
};
|
||||
}
|
||||
|
||||
const geminiMdPath = path.join(
|
||||
process.env['CODER_AGENT_WORKSPACE_PATH']!,
|
||||
'GEMINI.md',
|
||||
);
|
||||
const result = performInit(fs.existsSync(geminiMdPath));
|
||||
|
||||
const taskId = uuidv4();
|
||||
const contextId = uuidv4();
|
||||
|
||||
switch (result.type) {
|
||||
case 'message':
|
||||
return this.handleMessageResult(
|
||||
result,
|
||||
context,
|
||||
context.eventBus,
|
||||
taskId,
|
||||
contextId,
|
||||
);
|
||||
case 'submit_prompt':
|
||||
return this.handleSubmitPromptResult(
|
||||
result,
|
||||
context,
|
||||
geminiMdPath,
|
||||
context.eventBus,
|
||||
taskId,
|
||||
contextId,
|
||||
);
|
||||
default:
|
||||
throw new Error('Unknown result type from performInit');
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4,11 +4,14 @@
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import type { ExecutionEventBus, AgentExecutor } from '@a2a-js/sdk/server';
|
||||
import type { Config, GitService } from '@google/gemini-cli-core';
|
||||
|
||||
export interface CommandContext {
|
||||
config: Config;
|
||||
git?: GitService;
|
||||
agentExecutor?: AgentExecutor;
|
||||
eventBus?: ExecutionEventBus;
|
||||
}
|
||||
|
||||
export interface CommandArgument {
|
||||
@@ -24,6 +27,7 @@ export interface Command {
|
||||
readonly subCommands?: Command[];
|
||||
readonly topLevel?: boolean;
|
||||
readonly requiresWorkspace?: boolean;
|
||||
readonly streaming?: boolean;
|
||||
|
||||
execute(
|
||||
config: CommandContext,
|
||||
|
||||
@@ -22,6 +22,7 @@ import {
|
||||
DEFAULT_GEMINI_MODEL,
|
||||
type ExtensionLoader,
|
||||
startupProfiler,
|
||||
PREVIEW_GEMINI_MODEL,
|
||||
} from '@google/gemini-cli-core';
|
||||
|
||||
import { logger } from '../utils/logger.js';
|
||||
@@ -38,7 +39,9 @@ export async function loadConfig(
|
||||
|
||||
const configParams: ConfigParameters = {
|
||||
sessionId: taskId,
|
||||
model: DEFAULT_GEMINI_MODEL,
|
||||
model: settings.general?.previewFeatures
|
||||
? PREVIEW_GEMINI_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
|
||||
@@ -75,6 +78,7 @@ export async function loadConfig(
|
||||
? process.env['CHECKPOINTING'] === 'true'
|
||||
: settings.checkpointing?.enabled,
|
||||
previewFeatures: settings.general?.previewFeatures,
|
||||
interactive: true,
|
||||
};
|
||||
|
||||
const fileService = new FileDiscoveryService(workspaceDir);
|
||||
|
||||
@@ -123,7 +123,7 @@ function resolveEnvVarsInString(value: string): string {
|
||||
return value.replace(envVarRegex, (match, varName1, varName2) => {
|
||||
const varName = varName1 || varName2;
|
||||
if (process && process.env && typeof process.env[varName] === 'string') {
|
||||
return process.env[varName]!;
|
||||
return process.env[varName];
|
||||
}
|
||||
return match;
|
||||
});
|
||||
|
||||
@@ -1061,5 +1061,118 @@ describe('E2E Tests', () => {
|
||||
expect(response.status).toBe(200);
|
||||
expect(response.body.data).toBe('success');
|
||||
});
|
||||
|
||||
it('should include agentExecutor in context', async () => {
|
||||
const mockCommand = {
|
||||
name: 'context-check-command',
|
||||
description: 'checks context',
|
||||
execute: vi.fn(async (context: CommandContext) => {
|
||||
if (!context.agentExecutor) {
|
||||
throw new Error('agentExecutor missing');
|
||||
}
|
||||
return { name: 'context-check-command', data: 'success' };
|
||||
}),
|
||||
};
|
||||
vi.spyOn(commandRegistry, 'get').mockReturnValue(mockCommand);
|
||||
|
||||
const agent = request.agent(app);
|
||||
const res = await agent
|
||||
.post('/executeCommand')
|
||||
.send({ command: 'context-check-command', args: [] })
|
||||
.set('Content-Type', 'application/json')
|
||||
.expect(200);
|
||||
|
||||
expect(res.body.data).toBe('success');
|
||||
});
|
||||
|
||||
describe('/executeCommand streaming', () => {
|
||||
it('should execute a streaming command and stream back events', (done: (
|
||||
err?: unknown,
|
||||
) => void) => {
|
||||
const executeSpy = vi.fn(async (context: CommandContext) => {
|
||||
context.eventBus?.publish({
|
||||
kind: 'status-update',
|
||||
status: { state: 'working' },
|
||||
taskId: 'test-task',
|
||||
contextId: 'test-context',
|
||||
final: false,
|
||||
});
|
||||
context.eventBus?.publish({
|
||||
kind: 'status-update',
|
||||
status: { state: 'completed' },
|
||||
taskId: 'test-task',
|
||||
contextId: 'test-context',
|
||||
final: true,
|
||||
});
|
||||
return { name: 'stream-test', data: 'done' };
|
||||
});
|
||||
|
||||
const mockStreamCommand = {
|
||||
name: 'stream-test',
|
||||
description: 'A test streaming command',
|
||||
streaming: true,
|
||||
execute: executeSpy,
|
||||
};
|
||||
vi.spyOn(commandRegistry, 'get').mockReturnValue(mockStreamCommand);
|
||||
|
||||
const agent = request.agent(app);
|
||||
agent
|
||||
.post('/executeCommand')
|
||||
.send({ command: 'stream-test', args: [] })
|
||||
.set('Content-Type', 'application/json')
|
||||
.set('Accept', 'text/event-stream')
|
||||
.on('response', (res) => {
|
||||
let data = '';
|
||||
res.on('data', (chunk: Buffer) => {
|
||||
data += chunk.toString();
|
||||
});
|
||||
res.on('end', () => {
|
||||
try {
|
||||
const events = streamToSSEEvents(data);
|
||||
expect(events.length).toBe(2);
|
||||
expect(events[0].result).toEqual({
|
||||
kind: 'status-update',
|
||||
status: { state: 'working' },
|
||||
taskId: 'test-task',
|
||||
contextId: 'test-context',
|
||||
final: false,
|
||||
});
|
||||
expect(events[1].result).toEqual({
|
||||
kind: 'status-update',
|
||||
status: { state: 'completed' },
|
||||
taskId: 'test-task',
|
||||
contextId: 'test-context',
|
||||
final: true,
|
||||
});
|
||||
expect(executeSpy).toHaveBeenCalled();
|
||||
done();
|
||||
} catch (e) {
|
||||
done(e);
|
||||
}
|
||||
});
|
||||
})
|
||||
.end();
|
||||
});
|
||||
|
||||
it('should handle non-streaming commands gracefully', async () => {
|
||||
const mockNonStreamCommand = {
|
||||
name: 'non-stream-test',
|
||||
description: 'A test non-streaming command',
|
||||
execute: vi
|
||||
.fn()
|
||||
.mockResolvedValue({ name: 'non-stream-test', data: 'done' }),
|
||||
};
|
||||
vi.spyOn(commandRegistry, 'get').mockReturnValue(mockNonStreamCommand);
|
||||
|
||||
const agent = request.agent(app);
|
||||
const res = await agent
|
||||
.post('/executeCommand')
|
||||
.send({ command: 'non-stream-test', args: [] })
|
||||
.set('Content-Type', 'application/json')
|
||||
.expect(200);
|
||||
|
||||
expect(res.body).toEqual({ name: 'non-stream-test', data: 'done' });
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -6,9 +6,14 @@
|
||||
|
||||
import express from 'express';
|
||||
|
||||
import type { AgentCard } from '@a2a-js/sdk';
|
||||
import type { AgentCard, Message } from '@a2a-js/sdk';
|
||||
import type { TaskStore } from '@a2a-js/sdk/server';
|
||||
import { DefaultRequestHandler, InMemoryTaskStore } from '@a2a-js/sdk/server';
|
||||
import {
|
||||
DefaultRequestHandler,
|
||||
InMemoryTaskStore,
|
||||
DefaultExecutionEventBus,
|
||||
type AgentExecutionEvent,
|
||||
} from '@a2a-js/sdk/server';
|
||||
import { A2AExpressApp } from '@a2a-js/sdk/server/express'; // Import server components
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import { logger } from '../utils/logger.js';
|
||||
@@ -73,6 +78,76 @@ export function updateCoderAgentCardUrl(port: number) {
|
||||
coderAgentCard.url = `http://localhost:${port}/`;
|
||||
}
|
||||
|
||||
async function handleExecuteCommand(
|
||||
req: express.Request,
|
||||
res: express.Response,
|
||||
context: {
|
||||
config: Awaited<ReturnType<typeof loadConfig>>;
|
||||
git: GitService | undefined;
|
||||
agentExecutor: CoderAgentExecutor;
|
||||
},
|
||||
) {
|
||||
logger.info('[CoreAgent] Received /executeCommand request: ', req.body);
|
||||
const { command, args } = req.body;
|
||||
try {
|
||||
if (typeof command !== 'string') {
|
||||
return res.status(400).json({ error: 'Invalid "command" field.' });
|
||||
}
|
||||
|
||||
if (args && !Array.isArray(args)) {
|
||||
return res.status(400).json({ error: '"args" field must be an array.' });
|
||||
}
|
||||
|
||||
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}` });
|
||||
}
|
||||
|
||||
if (commandToExecute.streaming) {
|
||||
const eventBus = new DefaultExecutionEventBus();
|
||||
res.setHeader('Content-Type', 'text/event-stream');
|
||||
const eventHandler = (event: AgentExecutionEvent) => {
|
||||
const jsonRpcResponse = {
|
||||
jsonrpc: '2.0',
|
||||
id: 'taskId' in event ? event.taskId : (event as Message).messageId,
|
||||
result: event,
|
||||
};
|
||||
res.write(`data: ${JSON.stringify(jsonRpcResponse)}\n`);
|
||||
};
|
||||
eventBus.on('event', eventHandler);
|
||||
|
||||
await commandToExecute.execute({ ...context, eventBus }, args ?? []);
|
||||
|
||||
eventBus.off('event', eventHandler);
|
||||
eventBus.finished();
|
||||
return res.end(); // Explicit return for streaming path
|
||||
} else {
|
||||
const result = await commandToExecute.execute(context, args ?? []);
|
||||
logger.info('[CoreAgent] Sending /executeCommand response: ', result);
|
||||
return res.status(200).json(result);
|
||||
}
|
||||
} catch (e) {
|
||||
logger.error(
|
||||
`Error executing /executeCommand: ${command} with args: ${JSON.stringify(
|
||||
args,
|
||||
)}`,
|
||||
e,
|
||||
);
|
||||
const errorMessage =
|
||||
e instanceof Error ? e.message : 'Unknown error executing command';
|
||||
return res.status(500).json({ error: errorMessage });
|
||||
}
|
||||
}
|
||||
|
||||
export async function createApp() {
|
||||
try {
|
||||
// Load the server configuration once on startup.
|
||||
@@ -92,8 +167,6 @@ export async function createApp() {
|
||||
await git.initialize();
|
||||
}
|
||||
|
||||
const context = { config, git };
|
||||
|
||||
// loadEnvironment() is called within getConfig now
|
||||
const bucketName = process.env['GCS_BUCKET_NAME'];
|
||||
let taskStoreForExecutor: TaskStore;
|
||||
@@ -113,6 +186,8 @@ export async function createApp() {
|
||||
|
||||
const agentExecutor = new CoderAgentExecutor(taskStoreForExecutor);
|
||||
|
||||
const context = { config, git, agentExecutor };
|
||||
|
||||
const requestHandler = new DefaultRequestHandler(
|
||||
coderAgentCard,
|
||||
taskStoreForHandler,
|
||||
@@ -152,46 +227,8 @@ export async function createApp() {
|
||||
}
|
||||
});
|
||||
|
||||
expressApp.post('/executeCommand', async (req, res) => {
|
||||
logger.info('[CoreAgent] Received /executeCommand request: ', req.body);
|
||||
try {
|
||||
const { command, args } = req.body;
|
||||
|
||||
if (typeof command !== 'string') {
|
||||
return res.status(400).json({ error: 'Invalid "command" field.' });
|
||||
}
|
||||
|
||||
if (args && !Array.isArray(args)) {
|
||||
return res
|
||||
.status(400)
|
||||
.json({ error: '"args" field must be an array.' });
|
||||
}
|
||||
|
||||
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);
|
||||
return res.status(200).json(result);
|
||||
} catch (e) {
|
||||
logger.error('Error executing /executeCommand:', e);
|
||||
const errorMessage =
|
||||
e instanceof Error ? e.message : 'Unknown error executing command';
|
||||
return res.status(500).json({ error: errorMessage });
|
||||
}
|
||||
expressApp.post('/executeCommand', (req, res) => {
|
||||
void handleExecuteCommand(req, res, context);
|
||||
});
|
||||
|
||||
expressApp.get('/listCommands', (req, res) => {
|
||||
|
||||
@@ -46,6 +46,7 @@ export enum CoderAgentEvent {
|
||||
export interface AgentSettings {
|
||||
kind: CoderAgentEvent.StateAgentSettingsEvent;
|
||||
workspacePath: string;
|
||||
autoExecute?: boolean;
|
||||
}
|
||||
|
||||
export interface ToolCallConfirmation {
|
||||
|
||||
@@ -11,6 +11,7 @@ import type {
|
||||
} from '@a2a-js/sdk';
|
||||
import {
|
||||
ApprovalMode,
|
||||
DEFAULT_GEMINI_MODEL,
|
||||
DEFAULT_TRUNCATE_TOOL_OUTPUT_LINES,
|
||||
DEFAULT_TRUNCATE_TOOL_OUTPUT_THRESHOLD,
|
||||
GeminiClient,
|
||||
@@ -32,6 +33,7 @@ export function createMockConfig(
|
||||
}),
|
||||
getApprovalMode: vi.fn().mockReturnValue(ApprovalMode.DEFAULT),
|
||||
getIdeMode: vi.fn().mockReturnValue(false),
|
||||
isInteractive: () => true,
|
||||
getAllowedTools: vi.fn().mockReturnValue([]),
|
||||
getWorkspaceContext: vi.fn().mockReturnValue({
|
||||
isPathWithinWorkspace: () => true,
|
||||
@@ -45,6 +47,7 @@ export function createMockConfig(
|
||||
getTruncateToolOutputThreshold: () =>
|
||||
DEFAULT_TRUNCATE_TOOL_OUTPUT_THRESHOLD,
|
||||
getTruncateToolOutputLines: () => DEFAULT_TRUNCATE_TOOL_OUTPUT_LINES,
|
||||
getActiveModel: vi.fn().mockReturnValue(DEFAULT_GEMINI_MODEL),
|
||||
getDebugMode: vi.fn().mockReturnValue(false),
|
||||
getContentGeneratorConfig: vi.fn().mockReturnValue({ model: 'gemini-pro' }),
|
||||
getModel: vi.fn().mockReturnValue('gemini-pro'),
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
{
|
||||
"name": "@google/gemini-cli",
|
||||
"version": "0.21.0-nightly.20251209.ec9a8c7a7",
|
||||
"version": "0.23.0",
|
||||
"description": "Gemini CLI",
|
||||
"license": "Apache-2.0",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/google-gemini/gemini-cli.git"
|
||||
@@ -25,9 +26,10 @@
|
||||
"dist"
|
||||
],
|
||||
"config": {
|
||||
"sandboxImageUri": "us-docker.pkg.dev/gemini-code-dev/gemini-cli/sandbox:0.21.0-nightly.20251209.ec9a8c7a7"
|
||||
"sandboxImageUri": "us-docker.pkg.dev/gemini-code-dev/gemini-cli/sandbox:0.23.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@agentclientprotocol/sdk": "^0.11.0",
|
||||
"@google/gemini-cli-core": "file:../core",
|
||||
"@google/genai": "1.30.0",
|
||||
"@iarna/toml": "^2.2.5",
|
||||
|
||||
@@ -4,7 +4,7 @@ exports[`runNonInteractive > should emit appropriate error event in streaming JS
|
||||
"{"type":"init","timestamp":"<TIMESTAMP>","session_id":"test-session-id","model":"test-model"}
|
||||
{"type":"message","timestamp":"<TIMESTAMP>","role":"user","content":"Loop test"}
|
||||
{"type":"error","timestamp":"<TIMESTAMP>","severity":"warning","message":"Loop detected, stopping execution"}
|
||||
{"type":"result","timestamp":"<TIMESTAMP>","status":"success","stats":{"total_tokens":0,"input_tokens":0,"output_tokens":0,"duration_ms":<DURATION>,"tool_calls":0}}
|
||||
{"type":"result","timestamp":"<TIMESTAMP>","status":"success","stats":{"total_tokens":0,"input_tokens":0,"output_tokens":0,"cached":0,"input":0,"duration_ms":<DURATION>,"tool_calls":0}}
|
||||
"
|
||||
`;
|
||||
|
||||
@@ -12,7 +12,7 @@ exports[`runNonInteractive > should emit appropriate error event in streaming JS
|
||||
"{"type":"init","timestamp":"<TIMESTAMP>","session_id":"test-session-id","model":"test-model"}
|
||||
{"type":"message","timestamp":"<TIMESTAMP>","role":"user","content":"Max turns test"}
|
||||
{"type":"error","timestamp":"<TIMESTAMP>","severity":"error","message":"Maximum session turns exceeded"}
|
||||
{"type":"result","timestamp":"<TIMESTAMP>","status":"success","stats":{"total_tokens":0,"input_tokens":0,"output_tokens":0,"duration_ms":<DURATION>,"tool_calls":0}}
|
||||
{"type":"result","timestamp":"<TIMESTAMP>","status":"success","stats":{"total_tokens":0,"input_tokens":0,"output_tokens":0,"cached":0,"input":0,"duration_ms":<DURATION>,"tool_calls":0}}
|
||||
"
|
||||
`;
|
||||
|
||||
@@ -23,7 +23,7 @@ exports[`runNonInteractive > should emit appropriate events for streaming JSON o
|
||||
{"type":"tool_use","timestamp":"<TIMESTAMP>","tool_name":"testTool","tool_id":"tool-1","parameters":{"arg1":"value1"}}
|
||||
{"type":"tool_result","timestamp":"<TIMESTAMP>","tool_id":"tool-1","status":"success","output":"Tool executed successfully"}
|
||||
{"type":"message","timestamp":"<TIMESTAMP>","role":"assistant","content":"Final answer","delta":true}
|
||||
{"type":"result","timestamp":"<TIMESTAMP>","status":"success","stats":{"total_tokens":0,"input_tokens":0,"output_tokens":0,"duration_ms":<DURATION>,"tool_calls":0}}
|
||||
{"type":"result","timestamp":"<TIMESTAMP>","status":"success","stats":{"total_tokens":0,"input_tokens":0,"output_tokens":0,"cached":0,"input":0,"duration_ms":<DURATION>,"tool_calls":0}}
|
||||
"
|
||||
`;
|
||||
|
||||
|
||||
@@ -14,7 +14,7 @@ import {
|
||||
type Mock,
|
||||
} from 'vitest';
|
||||
import { format } from 'node:util';
|
||||
import { type CommandModule, type Argv } from 'yargs';
|
||||
import { type Argv } from 'yargs';
|
||||
import { handleDisable, disableCommand } from './disable.js';
|
||||
import { ExtensionManager } from '../../config/extension-manager.js';
|
||||
import {
|
||||
@@ -148,7 +148,7 @@ describe('extensions disable command', () => {
|
||||
});
|
||||
|
||||
describe('disableCommand', () => {
|
||||
const command = disableCommand as CommandModule;
|
||||
const command = disableCommand;
|
||||
|
||||
it('should have correct command and describe', () => {
|
||||
expect(command.command).toBe('disable [--scope] <name>');
|
||||
@@ -228,7 +228,9 @@ describe('extensions disable command', () => {
|
||||
_: [],
|
||||
$0: '',
|
||||
};
|
||||
await (command.handler as unknown as (args: TestArgv) => void)(argv);
|
||||
await (command.handler as unknown as (args: TestArgv) => Promise<void>)(
|
||||
argv,
|
||||
);
|
||||
expect(mockExtensionManager).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
workspaceDir: '/test/dir',
|
||||
|
||||
@@ -65,7 +65,7 @@ export const disableCommand: CommandModule = {
|
||||
argv.scope &&
|
||||
!Object.values(SettingScope)
|
||||
.map((s) => s.toLowerCase())
|
||||
.includes((argv.scope as string).toLowerCase())
|
||||
.includes(argv.scope.toLowerCase())
|
||||
) {
|
||||
throw new Error(
|
||||
`Invalid scope: ${argv.scope}. Please use one of ${Object.values(
|
||||
|
||||
@@ -14,7 +14,7 @@ import {
|
||||
type Mock,
|
||||
} from 'vitest';
|
||||
import { format } from 'node:util';
|
||||
import { type CommandModule, type Argv } from 'yargs';
|
||||
import { type Argv } from 'yargs';
|
||||
import { handleEnable, enableCommand } from './enable.js';
|
||||
import { ExtensionManager } from '../../config/extension-manager.js';
|
||||
import {
|
||||
@@ -137,7 +137,7 @@ describe('extensions enable command', () => {
|
||||
});
|
||||
|
||||
describe('enableCommand', () => {
|
||||
const command = enableCommand as CommandModule;
|
||||
const command = enableCommand;
|
||||
|
||||
it('should have correct command and describe', () => {
|
||||
expect(command.command).toBe('enable [--scope] <name>');
|
||||
@@ -205,7 +205,9 @@ describe('extensions enable command', () => {
|
||||
_: [],
|
||||
$0: '',
|
||||
};
|
||||
await (command.handler as unknown as (args: TestArgv) => void)(argv);
|
||||
await (command.handler as unknown as (args: TestArgv) => Promise<void>)(
|
||||
argv,
|
||||
);
|
||||
|
||||
expect(
|
||||
mockExtensionManager.prototype.enableExtension,
|
||||
|
||||
@@ -70,7 +70,7 @@ export const enableCommand: CommandModule = {
|
||||
argv.scope &&
|
||||
!Object.values(SettingScope)
|
||||
.map((s) => s.toLowerCase())
|
||||
.includes((argv.scope as string).toLowerCase())
|
||||
.includes(argv.scope.toLowerCase())
|
||||
) {
|
||||
throw new Error(
|
||||
`Invalid scope: ${argv.scope}. Please use one of ${Object.values(
|
||||
|
||||
@@ -4,15 +4,7 @@
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import {
|
||||
describe,
|
||||
it,
|
||||
expect,
|
||||
vi,
|
||||
beforeEach,
|
||||
afterEach,
|
||||
type Mock,
|
||||
} from 'vitest';
|
||||
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
||||
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
||||
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
||||
import { z } from 'zod';
|
||||
@@ -90,7 +82,7 @@ describe('MCP Server Example', () => {
|
||||
json: vi.fn().mockResolvedValue(mockPosts),
|
||||
});
|
||||
|
||||
const toolFn = (mockRegisterTool as Mock).mock.calls[0][2];
|
||||
const toolFn = mockRegisterTool.mock.calls[0][2];
|
||||
const result = await toolFn();
|
||||
|
||||
expect(global.fetch).toHaveBeenCalledWith(
|
||||
@@ -109,7 +101,7 @@ describe('MCP Server Example', () => {
|
||||
|
||||
describe('poem-writer prompt implementation', () => {
|
||||
it('should generate a prompt with a title', () => {
|
||||
const promptFn = (mockRegisterPrompt as Mock).mock.calls[0][2];
|
||||
const promptFn = mockRegisterPrompt.mock.calls[0][2];
|
||||
const result = promptFn({ title: 'My Poem' });
|
||||
expect(result).toEqual({
|
||||
messages: [
|
||||
@@ -125,7 +117,7 @@ describe('MCP Server Example', () => {
|
||||
});
|
||||
|
||||
it('should generate a prompt with a title and mood', () => {
|
||||
const promptFn = (mockRegisterPrompt as Mock).mock.calls[0][2];
|
||||
const promptFn = mockRegisterPrompt.mock.calls[0][2];
|
||||
const result = promptFn({ title: 'My Poem', mood: 'sad' });
|
||||
expect(result).toEqual({
|
||||
messages: [
|
||||
|
||||
@@ -14,7 +14,7 @@ import {
|
||||
type Mock,
|
||||
} from 'vitest';
|
||||
import { format } from 'node:util';
|
||||
import { type CommandModule, type Argv } from 'yargs';
|
||||
import { type Argv } from 'yargs';
|
||||
import { handleLink, linkCommand } from './link.js';
|
||||
import { ExtensionManager } from '../../config/extension-manager.js';
|
||||
import { loadSettings, type LoadedSettings } from '../../config/settings.js';
|
||||
@@ -126,7 +126,7 @@ describe('extensions link command', () => {
|
||||
});
|
||||
|
||||
describe('linkCommand', () => {
|
||||
const command = linkCommand as CommandModule;
|
||||
const command = linkCommand;
|
||||
|
||||
it('should have correct command and describe', () => {
|
||||
expect(command.command).toBe('link <path>');
|
||||
@@ -180,7 +180,9 @@ describe('extensions link command', () => {
|
||||
_: [],
|
||||
$0: '',
|
||||
};
|
||||
await (command.handler as unknown as (args: TestArgv) => void)(argv);
|
||||
await (command.handler as unknown as (args: TestArgv) => Promise<void>)(
|
||||
argv,
|
||||
);
|
||||
|
||||
expect(
|
||||
mockExtensionManager.prototype.installOrUpdateExtension,
|
||||
|
||||
@@ -6,7 +6,6 @@
|
||||
|
||||
import { vi, describe, it, expect, beforeEach, afterEach } from 'vitest';
|
||||
import { format } from 'node:util';
|
||||
import { type CommandModule } from 'yargs';
|
||||
import { handleList, listCommand } from './list.js';
|
||||
import { ExtensionManager } from '../../config/extension-manager.js';
|
||||
import { loadSettings, type LoadedSettings } from '../../config/settings.js';
|
||||
@@ -124,7 +123,7 @@ describe('extensions list command', () => {
|
||||
});
|
||||
|
||||
describe('listCommand', () => {
|
||||
const command = listCommand as CommandModule;
|
||||
const command = listCommand;
|
||||
|
||||
it('should have correct command and describe', () => {
|
||||
expect(command.command).toBe('list');
|
||||
|
||||
@@ -49,7 +49,7 @@ const setCommand: CommandModule<object, SetArgs> = {
|
||||
if (!extension || !extensionManager) {
|
||||
return;
|
||||
}
|
||||
const extensionConfig = extensionManager.loadExtensionConfig(
|
||||
const extensionConfig = await extensionManager.loadExtensionConfig(
|
||||
extension.path,
|
||||
);
|
||||
if (!extensionConfig) {
|
||||
@@ -89,7 +89,7 @@ const listCommand: CommandModule<object, ListArgs> = {
|
||||
if (!extension || !extensionManager) {
|
||||
return;
|
||||
}
|
||||
const extensionConfig = extensionManager.loadExtensionConfig(
|
||||
const extensionConfig = await extensionManager.loadExtensionConfig(
|
||||
extension.path,
|
||||
);
|
||||
if (
|
||||
|
||||
@@ -14,7 +14,7 @@ import {
|
||||
type Mock,
|
||||
} from 'vitest';
|
||||
import { format } from 'node:util';
|
||||
import { type CommandModule, type Argv } from 'yargs';
|
||||
import { type Argv } from 'yargs';
|
||||
import { handleUninstall, uninstallCommand } from './uninstall.js';
|
||||
import { ExtensionManager } from '../../config/extension-manager.js';
|
||||
import { loadSettings, type LoadedSettings } from '../../config/settings.js';
|
||||
@@ -233,7 +233,7 @@ describe('extensions uninstall command', () => {
|
||||
});
|
||||
|
||||
describe('uninstallCommand', () => {
|
||||
const command = uninstallCommand as CommandModule;
|
||||
const command = uninstallCommand;
|
||||
|
||||
it('should have correct command and describe', () => {
|
||||
expect(command.command).toBe('uninstall <names..>');
|
||||
@@ -287,7 +287,9 @@ describe('extensions uninstall command', () => {
|
||||
[key: string]: unknown;
|
||||
}
|
||||
const argv: TestArgv = { names: ['my-extension'], _: [], $0: '' };
|
||||
await (command.handler as unknown as (args: TestArgv) => void)(argv);
|
||||
await (command.handler as unknown as (args: TestArgv) => Promise<void>)(
|
||||
argv,
|
||||
);
|
||||
|
||||
expect(mockUninstallExtension).toHaveBeenCalledWith(
|
||||
'my-extension',
|
||||
|
||||
@@ -62,7 +62,7 @@ export const uninstallCommand: CommandModule = {
|
||||
array: true,
|
||||
})
|
||||
.check((argv) => {
|
||||
if (!argv.names || (argv.names as string[]).length === 0) {
|
||||
if (!argv.names || argv.names.length === 0) {
|
||||
throw new Error(
|
||||
'Please include at least one extension name to uninstall as a positional argument.',
|
||||
);
|
||||
|
||||
@@ -14,7 +14,7 @@ import {
|
||||
type Mock,
|
||||
} from 'vitest';
|
||||
import { format } from 'node:util';
|
||||
import { type CommandModule, type Argv } from 'yargs';
|
||||
import { type Argv } from 'yargs';
|
||||
import { handleUpdate, updateCommand } from './update.js';
|
||||
import { ExtensionManager } from '../../config/extension-manager.js';
|
||||
import { loadSettings, type LoadedSettings } from '../../config/settings.js';
|
||||
@@ -155,7 +155,7 @@ describe('extensions update command', () => {
|
||||
});
|
||||
|
||||
describe('updateCommand', () => {
|
||||
const command = updateCommand as CommandModule;
|
||||
const command = updateCommand;
|
||||
|
||||
it('should have correct command and describe', () => {
|
||||
expect(command.command).toBe('update [<name>] [--all]');
|
||||
|
||||
@@ -41,7 +41,7 @@ async function validateExtension(args: ValidateArgs) {
|
||||
});
|
||||
const absoluteInputPath = path.resolve(args.path);
|
||||
const extensionConfig: ExtensionConfig =
|
||||
extensionManager.loadExtensionConfig(absoluteInputPath);
|
||||
await extensionManager.loadExtensionConfig(absoluteInputPath);
|
||||
const warnings: string[] = [];
|
||||
const errors: string[] = [];
|
||||
|
||||
|
||||
@@ -4,7 +4,15 @@
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { describe, it, expect, vi, type Mock, type MockInstance } from 'vitest';
|
||||
import {
|
||||
describe,
|
||||
it,
|
||||
expect,
|
||||
vi,
|
||||
beforeEach,
|
||||
type Mock,
|
||||
type MockInstance,
|
||||
} from 'vitest';
|
||||
import yargs, { type Argv } from 'yargs';
|
||||
import { addCommand } from './add.js';
|
||||
import { loadSettings, SettingScope } from '../../config/settings.js';
|
||||
|
||||
@@ -60,7 +60,7 @@ vi.mock('fs', async (importOriginal) => {
|
||||
if (mockPaths.has(p.toString())) {
|
||||
return { isDirectory: () => true } as unknown as import('fs').Stats;
|
||||
}
|
||||
return (actualFs as typeof import('fs')).statSync(p as unknown as string);
|
||||
return actualFs.statSync(p as unknown as string);
|
||||
}),
|
||||
realpathSync: vi.fn((p) => p),
|
||||
};
|
||||
@@ -1283,7 +1283,7 @@ describe('loadCliConfig model selection', () => {
|
||||
argv,
|
||||
);
|
||||
|
||||
expect(config.getModel()).toBe('auto');
|
||||
expect(config.getModel()).toBe('auto-gemini-2.5');
|
||||
});
|
||||
|
||||
it('always prefers model from argv', async () => {
|
||||
|
||||
@@ -31,10 +31,11 @@ import {
|
||||
debugLogger,
|
||||
loadServerHierarchicalMemory,
|
||||
WEB_FETCH_TOOL_NAME,
|
||||
getVersion,
|
||||
PREVIEW_GEMINI_MODEL_AUTO,
|
||||
} from '@google/gemini-cli-core';
|
||||
import type { Settings } from './settings.js';
|
||||
|
||||
import { getCliVersion } from '../utils/version.js';
|
||||
import { loadSandboxConfig } from './sandboxConfig.js';
|
||||
import { resolvePath } from '../utils/resolvePath.js';
|
||||
import { appEvents } from '../utils/events.js';
|
||||
@@ -288,7 +289,7 @@ export async function parseArguments(settings: Settings): Promise<CliArgs> {
|
||||
}
|
||||
|
||||
yargsInstance
|
||||
.version(await getCliVersion()) // This will enable the --version flag based on package.json
|
||||
.version(await getVersion()) // This will enable the --version flag based on package.json
|
||||
.alias('v', 'version')
|
||||
.help()
|
||||
.alias('h', 'help')
|
||||
@@ -434,9 +435,15 @@ export async function loadCliConfig(
|
||||
});
|
||||
await extensionManager.loadExtensions();
|
||||
|
||||
// Call the (now wrapper) loadHierarchicalGeminiMemory which calls the server's version
|
||||
const { memoryContent, fileCount, filePaths } =
|
||||
await loadServerHierarchicalMemory(
|
||||
const experimentalJitContext = settings.experimental?.jitContext ?? false;
|
||||
|
||||
let memoryContent = '';
|
||||
let fileCount = 0;
|
||||
let filePaths: string[] = [];
|
||||
|
||||
if (!experimentalJitContext) {
|
||||
// Call the (now wrapper) loadHierarchicalGeminiMemory which calls the server's version
|
||||
const result = await loadServerHierarchicalMemory(
|
||||
cwd,
|
||||
[],
|
||||
debugMode,
|
||||
@@ -447,6 +454,10 @@ export async function loadCliConfig(
|
||||
memoryFileFiltering,
|
||||
settings.context?.discoveryMaxDirs,
|
||||
);
|
||||
memoryContent = result.memoryContent;
|
||||
fileCount = result.fileCount;
|
||||
filePaths = result.filePaths;
|
||||
}
|
||||
|
||||
const question = argv.promptInteractive || argv.prompt || '';
|
||||
|
||||
@@ -569,7 +580,9 @@ export async function loadCliConfig(
|
||||
extraExcludes.length > 0 ? extraExcludes : undefined,
|
||||
);
|
||||
|
||||
const defaultModel = DEFAULT_GEMINI_MODEL_AUTO;
|
||||
const defaultModel = settings.general?.previewFeatures
|
||||
? PREVIEW_GEMINI_MODEL_AUTO
|
||||
: DEFAULT_GEMINI_MODEL_AUTO;
|
||||
const resolvedModel: string =
|
||||
argv.model ||
|
||||
process.env['GEMINI_MODEL'] ||
|
||||
@@ -640,8 +653,6 @@ export async function loadCliConfig(
|
||||
extensionLoader: extensionManager,
|
||||
enableExtensionReloading: settings.experimental?.extensionReloading,
|
||||
enableAgents: settings.experimental?.enableAgents,
|
||||
enableModelAvailabilityService:
|
||||
settings.experimental?.isModelAvailabilityServiceEnabled,
|
||||
experimentalJitContext: settings.experimental?.jitContext,
|
||||
noBrowser: !!process.env['NO_BROWSER'],
|
||||
summarizeToolOutput: settings.model?.summarizeToolOutput,
|
||||
@@ -668,6 +679,8 @@ export async function loadCliConfig(
|
||||
enableMessageBusIntegration,
|
||||
codebaseInvestigatorSettings:
|
||||
settings.experimental?.codebaseInvestigatorSettings,
|
||||
introspectionAgentSettings:
|
||||
settings.experimental?.introspectionAgentSettings,
|
||||
fakeResponses: argv.fakeResponses,
|
||||
recordResponses: argv.recordResponses,
|
||||
retryFetchErrors: settings.general?.retryFetchErrors ?? false,
|
||||
|
||||
@@ -123,7 +123,7 @@ export class ExtensionManager extends ExtensionLoader {
|
||||
'Extensions not yet loaded, must call `loadExtensions` first',
|
||||
);
|
||||
}
|
||||
return this.loadedExtensions!;
|
||||
return this.loadedExtensions;
|
||||
}
|
||||
|
||||
async installOrUpdateExtension(
|
||||
@@ -220,7 +220,7 @@ export class ExtensionManager extends ExtensionLoader {
|
||||
}
|
||||
|
||||
try {
|
||||
newExtensionConfig = this.loadExtensionConfig(localSourcePath);
|
||||
newExtensionConfig = await this.loadExtensionConfig(localSourcePath);
|
||||
|
||||
if (isUpdate && installMetadata.autoUpdate) {
|
||||
const oldSettings = new Set(
|
||||
@@ -319,7 +319,7 @@ export class ExtensionManager extends ExtensionLoader {
|
||||
|
||||
// TODO: Gracefully handle this call failing, we should back up the old
|
||||
// extension prior to overwriting it and then restore and restart it.
|
||||
extension = await this.loadExtension(destinationPath)!;
|
||||
extension = await this.loadExtension(destinationPath);
|
||||
if (!extension) {
|
||||
throw new Error(`Extension not found`);
|
||||
}
|
||||
@@ -327,6 +327,7 @@ export class ExtensionManager extends ExtensionLoader {
|
||||
await logExtensionUpdateEvent(
|
||||
this.telemetryConfig,
|
||||
new ExtensionUpdateEvent(
|
||||
newExtensionConfig.name,
|
||||
hashValue(newExtensionConfig.name),
|
||||
getExtensionId(newExtensionConfig, installMetadata),
|
||||
newExtensionConfig.version,
|
||||
@@ -339,6 +340,7 @@ export class ExtensionManager extends ExtensionLoader {
|
||||
await logExtensionInstallEvent(
|
||||
this.telemetryConfig,
|
||||
new ExtensionInstallEvent(
|
||||
newExtensionConfig.name,
|
||||
hashValue(newExtensionConfig.name),
|
||||
getExtensionId(newExtensionConfig, installMetadata),
|
||||
newExtensionConfig.version,
|
||||
@@ -346,8 +348,10 @@ export class ExtensionManager extends ExtensionLoader {
|
||||
'success',
|
||||
),
|
||||
);
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
this.enableExtension(newExtensionConfig.name, SettingScope.User);
|
||||
await this.enableExtension(
|
||||
newExtensionConfig.name,
|
||||
SettingScope.User,
|
||||
);
|
||||
}
|
||||
} finally {
|
||||
if (tempDir) {
|
||||
@@ -360,7 +364,7 @@ export class ExtensionManager extends ExtensionLoader {
|
||||
// to get the name and version for logging.
|
||||
if (!newExtensionConfig && localSourcePath) {
|
||||
try {
|
||||
newExtensionConfig = this.loadExtensionConfig(localSourcePath);
|
||||
newExtensionConfig = await this.loadExtensionConfig(localSourcePath);
|
||||
} catch {
|
||||
// Ignore error, this is just for logging.
|
||||
}
|
||||
@@ -373,6 +377,7 @@ export class ExtensionManager extends ExtensionLoader {
|
||||
await logExtensionUpdateEvent(
|
||||
this.telemetryConfig,
|
||||
new ExtensionUpdateEvent(
|
||||
config?.name ?? '',
|
||||
hashValue(config?.name ?? ''),
|
||||
extensionId ?? '',
|
||||
newExtensionConfig?.version ?? '',
|
||||
@@ -385,6 +390,7 @@ export class ExtensionManager extends ExtensionLoader {
|
||||
await logExtensionInstallEvent(
|
||||
this.telemetryConfig,
|
||||
new ExtensionInstallEvent(
|
||||
newExtensionConfig?.name ?? '',
|
||||
hashValue(newExtensionConfig?.name ?? ''),
|
||||
extensionId ?? '',
|
||||
newExtensionConfig?.version ?? '',
|
||||
@@ -432,6 +438,7 @@ export class ExtensionManager extends ExtensionLoader {
|
||||
await logExtensionUninstall(
|
||||
this.telemetryConfig,
|
||||
new ExtensionUninstallEvent(
|
||||
extension.name,
|
||||
hashValue(extension.name),
|
||||
extension.id,
|
||||
'success',
|
||||
@@ -484,7 +491,7 @@ export class ExtensionManager extends ExtensionLoader {
|
||||
}
|
||||
|
||||
try {
|
||||
let config = this.loadExtensionConfig(effectiveExtensionPath);
|
||||
let config = await this.loadExtensionConfig(effectiveExtensionPath);
|
||||
if (
|
||||
this.getExtensions().find((extension) => extension.name === config.name)
|
||||
) {
|
||||
@@ -564,13 +571,13 @@ export class ExtensionManager extends ExtensionLoader {
|
||||
return this.maybeStopExtension(extension);
|
||||
}
|
||||
|
||||
loadExtensionConfig(extensionDir: string): ExtensionConfig {
|
||||
async loadExtensionConfig(extensionDir: string): Promise<ExtensionConfig> {
|
||||
const configFilePath = path.join(extensionDir, EXTENSIONS_CONFIG_FILENAME);
|
||||
if (!fs.existsSync(configFilePath)) {
|
||||
throw new Error(`Configuration file not found at ${configFilePath}`);
|
||||
}
|
||||
try {
|
||||
const configContent = fs.readFileSync(configFilePath, 'utf-8');
|
||||
const configContent = await fs.promises.readFile(configFilePath, 'utf-8');
|
||||
const rawConfig = JSON.parse(configContent) as ExtensionConfig;
|
||||
if (!rawConfig.name || !rawConfig.version) {
|
||||
throw new Error(
|
||||
@@ -714,7 +721,7 @@ export class ExtensionManager extends ExtensionLoader {
|
||||
}
|
||||
await logExtensionDisable(
|
||||
this.telemetryConfig,
|
||||
new ExtensionDisableEvent(hashValue(name), extension.id, scope),
|
||||
new ExtensionDisableEvent(name, hashValue(name), extension.id, scope),
|
||||
);
|
||||
if (!this.config || this.config.getEnableExtensionReloading()) {
|
||||
// Only toggle the isActive state if we are actually going to disable it
|
||||
@@ -749,7 +756,7 @@ export class ExtensionManager extends ExtensionLoader {
|
||||
}
|
||||
await logExtensionEnable(
|
||||
this.telemetryConfig,
|
||||
new ExtensionEnableEvent(hashValue(name), extension.id, scope),
|
||||
new ExtensionEnableEvent(name, hashValue(name), extension.id, scope),
|
||||
);
|
||||
if (!this.config || this.config.getEnableExtensionReloading()) {
|
||||
// Only toggle the isActive state if we are actually going to disable it
|
||||
@@ -790,7 +797,7 @@ function validateName(name: string) {
|
||||
}
|
||||
}
|
||||
|
||||
export function getExtensionId(
|
||||
function getExtensionId(
|
||||
config: ExtensionConfig,
|
||||
installMetadata?: ExtensionInstallMetadata,
|
||||
): string {
|
||||
|
||||
@@ -1424,9 +1424,10 @@ This extension will run the following MCP servers:
|
||||
],
|
||||
});
|
||||
|
||||
const previousExtensionConfig = extensionManager.loadExtensionConfig(
|
||||
path.join(userExtensionsDir, 'my-local-extension'),
|
||||
);
|
||||
const previousExtensionConfig =
|
||||
await extensionManager.loadExtensionConfig(
|
||||
path.join(userExtensionsDir, 'my-local-extension'),
|
||||
);
|
||||
mockPromptForSettings.mockResolvedValueOnce('new-setting-value');
|
||||
|
||||
// 3. Call installOrUpdateExtension to perform the update.
|
||||
@@ -1481,9 +1482,10 @@ This extension will run the following MCP servers:
|
||||
],
|
||||
});
|
||||
|
||||
const previousExtensionConfig = extensionManager.loadExtensionConfig(
|
||||
path.join(userExtensionsDir, 'my-auto-update-ext'),
|
||||
);
|
||||
const previousExtensionConfig =
|
||||
await extensionManager.loadExtensionConfig(
|
||||
path.join(userExtensionsDir, 'my-auto-update-ext'),
|
||||
);
|
||||
|
||||
// 3. Attempt to update and assert it fails
|
||||
await expect(
|
||||
@@ -1771,6 +1773,7 @@ This extension will run the following MCP servers:
|
||||
} else {
|
||||
expect(mockLogExtensionUninstall).toHaveBeenCalled();
|
||||
expect(ExtensionUninstallEvent).toHaveBeenCalledWith(
|
||||
'my-local-extension',
|
||||
hashValue('my-local-extension'),
|
||||
hashValue(userExtensionsDir),
|
||||
'success',
|
||||
@@ -1818,6 +1821,7 @@ This extension will run the following MCP servers:
|
||||
expect(fs.existsSync(sourceExtDir)).toBe(false);
|
||||
expect(mockLogExtensionUninstall).toHaveBeenCalled();
|
||||
expect(ExtensionUninstallEvent).toHaveBeenCalledWith(
|
||||
'gemini-sql-extension',
|
||||
hashValue('gemini-sql-extension'),
|
||||
hashValue('https://github.com/google/gemini-sql-extension'),
|
||||
'success',
|
||||
@@ -1935,6 +1939,7 @@ This extension will run the following MCP servers:
|
||||
|
||||
expect(mockLogExtensionDisable).toHaveBeenCalled();
|
||||
expect(ExtensionDisableEvent).toHaveBeenCalledWith(
|
||||
'ext1',
|
||||
hashValue('ext1'),
|
||||
hashValue(userExtensionsDir),
|
||||
SettingScope.Workspace,
|
||||
@@ -1964,7 +1969,7 @@ This extension will run the following MCP servers:
|
||||
expect(activeExtensions).toHaveLength(0);
|
||||
|
||||
await extensionManager.enableExtension('ext1', SettingScope.User);
|
||||
activeExtensions = await getActiveExtensions();
|
||||
activeExtensions = getActiveExtensions();
|
||||
expect(activeExtensions).toHaveLength(1);
|
||||
expect(activeExtensions[0].name).toBe('ext1');
|
||||
});
|
||||
@@ -1981,7 +1986,7 @@ This extension will run the following MCP servers:
|
||||
expect(activeExtensions).toHaveLength(0);
|
||||
|
||||
await extensionManager.enableExtension('ext1', SettingScope.Workspace);
|
||||
activeExtensions = await getActiveExtensions();
|
||||
activeExtensions = getActiveExtensions();
|
||||
expect(activeExtensions).toHaveLength(1);
|
||||
expect(activeExtensions[0].name).toBe('ext1');
|
||||
});
|
||||
@@ -2002,6 +2007,7 @@ This extension will run the following MCP servers:
|
||||
|
||||
expect(mockLogExtensionEnable).toHaveBeenCalled();
|
||||
expect(ExtensionEnableEvent).toHaveBeenCalledWith(
|
||||
'ext1',
|
||||
hashValue('ext1'),
|
||||
hashValue(userExtensionsDir),
|
||||
SettingScope.Workspace,
|
||||
|
||||
@@ -397,6 +397,48 @@ describe('extensionSettings', () => {
|
||||
const actualContent = await fsPromises.readFile(expectedEnvPath, 'utf-8');
|
||||
expect(actualContent).toBe('VAR1="a value with spaces"\n');
|
||||
});
|
||||
|
||||
it('should not attempt to clear secrets if keychain is unavailable', async () => {
|
||||
// Arrange
|
||||
const mockIsAvailable = vi.fn().mockResolvedValue(false);
|
||||
const mockListSecrets = vi.fn();
|
||||
|
||||
vi.mocked(KeychainTokenStorage).mockImplementation(
|
||||
() =>
|
||||
({
|
||||
isAvailable: mockIsAvailable,
|
||||
listSecrets: mockListSecrets,
|
||||
deleteSecret: vi.fn(),
|
||||
getSecret: vi.fn(),
|
||||
setSecret: vi.fn(),
|
||||
}) as unknown as KeychainTokenStorage,
|
||||
);
|
||||
|
||||
const config: ExtensionConfig = {
|
||||
name: 'test-ext',
|
||||
version: '1.0.0',
|
||||
settings: [], // Empty settings triggers clearSettings
|
||||
};
|
||||
|
||||
const previousConfig: ExtensionConfig = {
|
||||
name: 'test-ext',
|
||||
version: '1.0.0',
|
||||
settings: [{ name: 's1', description: 'd1', envVar: 'VAR1' }],
|
||||
};
|
||||
|
||||
// Act
|
||||
await maybePromptForSettings(
|
||||
config,
|
||||
'12345',
|
||||
mockRequestSetting,
|
||||
previousConfig,
|
||||
undefined,
|
||||
);
|
||||
|
||||
// Assert
|
||||
expect(mockIsAvailable).toHaveBeenCalled();
|
||||
expect(mockListSecrets).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('promptForSetting', () => {
|
||||
|
||||
@@ -289,7 +289,7 @@ async function clearSettings(
|
||||
if (fsSync.existsSync(envFilePath)) {
|
||||
await fs.writeFile(envFilePath, '');
|
||||
}
|
||||
if (!keychain.isAvailable()) {
|
||||
if (!(await keychain.isAvailable())) {
|
||||
return;
|
||||
}
|
||||
const secrets = await keychain.listSecrets();
|
||||
|
||||
@@ -221,9 +221,11 @@ describe('github.ts', () => {
|
||||
});
|
||||
|
||||
it('should return NOT_UPDATABLE for non-git/non-release extensions', async () => {
|
||||
vi.mocked(mockExtensionManager.loadExtensionConfig).mockReturnValue({
|
||||
version: '1.0.0',
|
||||
} as unknown as ExtensionConfig);
|
||||
vi.mocked(mockExtensionManager.loadExtensionConfig).mockReturnValue(
|
||||
Promise.resolve({
|
||||
version: '1.0.0',
|
||||
} as unknown as ExtensionConfig),
|
||||
);
|
||||
|
||||
const linkExt = {
|
||||
installMetadata: { type: 'link' },
|
||||
|
||||
@@ -175,7 +175,7 @@ export async function checkForExtensionUpdate(
|
||||
if (installMetadata?.type === 'local') {
|
||||
let latestConfig: ExtensionConfig | undefined;
|
||||
try {
|
||||
latestConfig = extensionManager.loadExtensionConfig(
|
||||
latestConfig = await extensionManager.loadExtensionConfig(
|
||||
installMetadata.source,
|
||||
);
|
||||
} catch (e) {
|
||||
|
||||
@@ -31,7 +31,7 @@ export async function fetchJson<T>(
|
||||
if (!res.headers.location) {
|
||||
return reject(new Error('No location header in redirect response'));
|
||||
}
|
||||
fetchJson<T>(res.headers.location!, redirectCount++)
|
||||
fetchJson<T>(res.headers.location, redirectCount++)
|
||||
.then(resolve)
|
||||
.catch(reject);
|
||||
return;
|
||||
|
||||
@@ -145,10 +145,12 @@ describe('Extension Update Logic', () => {
|
||||
});
|
||||
|
||||
it('should successfully update extension and set state to UPDATED_NEEDS_RESTART by default', async () => {
|
||||
vi.mocked(mockExtensionManager.loadExtensionConfig).mockReturnValue({
|
||||
name: 'test-extension',
|
||||
version: '1.0.0',
|
||||
});
|
||||
vi.mocked(mockExtensionManager.loadExtensionConfig).mockReturnValue(
|
||||
Promise.resolve({
|
||||
name: 'test-extension',
|
||||
version: '1.0.0',
|
||||
}),
|
||||
);
|
||||
vi.mocked(
|
||||
mockExtensionManager.installOrUpdateExtension,
|
||||
).mockResolvedValue({
|
||||
@@ -183,10 +185,12 @@ describe('Extension Update Logic', () => {
|
||||
});
|
||||
|
||||
it('should set state to UPDATED if enableExtensionReloading is true', async () => {
|
||||
vi.mocked(mockExtensionManager.loadExtensionConfig).mockReturnValue({
|
||||
name: 'test-extension',
|
||||
version: '1.0.0',
|
||||
});
|
||||
vi.mocked(mockExtensionManager.loadExtensionConfig).mockReturnValue(
|
||||
Promise.resolve({
|
||||
name: 'test-extension',
|
||||
version: '1.0.0',
|
||||
}),
|
||||
);
|
||||
vi.mocked(
|
||||
mockExtensionManager.installOrUpdateExtension,
|
||||
).mockResolvedValue({
|
||||
@@ -212,10 +216,12 @@ describe('Extension Update Logic', () => {
|
||||
});
|
||||
|
||||
it('should rollback and set state to ERROR if installation fails', async () => {
|
||||
vi.mocked(mockExtensionManager.loadExtensionConfig).mockReturnValue({
|
||||
name: 'test-extension',
|
||||
version: '1.0.0',
|
||||
});
|
||||
vi.mocked(mockExtensionManager.loadExtensionConfig).mockReturnValue(
|
||||
Promise.resolve({
|
||||
name: 'test-extension',
|
||||
version: '1.0.0',
|
||||
}),
|
||||
);
|
||||
vi.mocked(
|
||||
mockExtensionManager.installOrUpdateExtension,
|
||||
).mockRejectedValue(new Error('Install failed'));
|
||||
@@ -257,10 +263,12 @@ describe('Extension Update Logic', () => {
|
||||
['ext3', { status: ExtensionUpdateState.UPDATE_AVAILABLE }],
|
||||
]);
|
||||
|
||||
vi.mocked(mockExtensionManager.loadExtensionConfig).mockReturnValue({
|
||||
name: 'ext',
|
||||
version: '1.0.0',
|
||||
});
|
||||
vi.mocked(mockExtensionManager.loadExtensionConfig).mockReturnValue(
|
||||
Promise.resolve({
|
||||
name: 'ext',
|
||||
version: '1.0.0',
|
||||
}),
|
||||
);
|
||||
vi.mocked(
|
||||
mockExtensionManager.installOrUpdateExtension,
|
||||
).mockResolvedValue({ ...mockExtension, version: '1.1.0' });
|
||||
|
||||
@@ -59,7 +59,7 @@ export async function updateExtension(
|
||||
|
||||
const tempDir = await ExtensionStorage.createTmpDir();
|
||||
try {
|
||||
const previousExtensionConfig = extensionManager.loadExtensionConfig(
|
||||
const previousExtensionConfig = await extensionManager.loadExtensionConfig(
|
||||
extension.path,
|
||||
);
|
||||
let updatedExtension: GeminiCLIExtension;
|
||||
|
||||
@@ -192,7 +192,10 @@ export const defaultKeyBindings: KeyBindingConfig = {
|
||||
{ key: 'x', ctrl: true },
|
||||
{ sequence: '\x18', ctrl: true },
|
||||
],
|
||||
[Command.PASTE_CLIPBOARD]: [{ key: 'v', ctrl: true }],
|
||||
[Command.PASTE_CLIPBOARD]: [
|
||||
{ key: 'v', ctrl: true },
|
||||
{ key: 'v', command: true },
|
||||
],
|
||||
|
||||
// App level bindings
|
||||
[Command.SHOW_ERROR_DETAILS]: [{ key: 'f12' }],
|
||||
|
||||
@@ -0,0 +1,398 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2025 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
/// <reference types="vitest/globals" />
|
||||
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import {
|
||||
validateSettings,
|
||||
formatValidationError,
|
||||
settingsZodSchema,
|
||||
} from './settings-validation.js';
|
||||
import { z } from 'zod';
|
||||
|
||||
describe('settings-validation', () => {
|
||||
describe('validateSettings', () => {
|
||||
it('should accept valid settings with correct model.name as string', () => {
|
||||
const validSettings = {
|
||||
model: {
|
||||
name: 'gemini-2.0-flash-exp',
|
||||
maxSessionTurns: 10,
|
||||
},
|
||||
ui: {
|
||||
theme: 'dark',
|
||||
},
|
||||
};
|
||||
|
||||
const result = validateSettings(validSettings);
|
||||
expect(result.success).toBe(true);
|
||||
});
|
||||
|
||||
it('should reject model.name as object instead of string', () => {
|
||||
const invalidSettings = {
|
||||
model: {
|
||||
name: {
|
||||
skipNextSpeakerCheck: true,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const result = validateSettings(invalidSettings);
|
||||
expect(result.success).toBe(false);
|
||||
expect(result.error).toBeDefined();
|
||||
|
||||
if (result.error) {
|
||||
const issues = result.error.issues;
|
||||
expect(issues.length).toBeGreaterThan(0);
|
||||
expect(issues[0]?.path).toEqual(['model', 'name']);
|
||||
expect(issues[0]?.code).toBe('invalid_type');
|
||||
}
|
||||
});
|
||||
|
||||
it('should accept valid model.summarizeToolOutput structure', () => {
|
||||
const validSettings = {
|
||||
model: {
|
||||
summarizeToolOutput: {
|
||||
run_shell_command: {
|
||||
tokenBudget: 500,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const result = validateSettings(validSettings);
|
||||
expect(result.success).toBe(true);
|
||||
});
|
||||
|
||||
it('should reject invalid model.summarizeToolOutput structure', () => {
|
||||
const invalidSettings = {
|
||||
model: {
|
||||
summarizeToolOutput: {
|
||||
run_shell_command: {
|
||||
tokenBudget: 500,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
// First test with valid structure
|
||||
let result = validateSettings(invalidSettings);
|
||||
expect(result.success).toBe(true);
|
||||
|
||||
// Now test with wrong type (string instead of object)
|
||||
const actuallyInvalidSettings = {
|
||||
model: {
|
||||
summarizeToolOutput: 'invalid',
|
||||
},
|
||||
};
|
||||
|
||||
result = validateSettings(actuallyInvalidSettings);
|
||||
expect(result.success).toBe(false);
|
||||
if (result.error) {
|
||||
expect(result.error.issues.length).toBeGreaterThan(0);
|
||||
}
|
||||
});
|
||||
|
||||
it('should accept empty settings object', () => {
|
||||
const emptySettings = {};
|
||||
const result = validateSettings(emptySettings);
|
||||
expect(result.success).toBe(true);
|
||||
});
|
||||
|
||||
it('should accept unknown top-level keys (for migration compatibility)', () => {
|
||||
const settingsWithUnknownKey = {
|
||||
unknownKey: 'some value',
|
||||
};
|
||||
|
||||
const result = validateSettings(settingsWithUnknownKey);
|
||||
expect(result.success).toBe(true);
|
||||
// Unknown keys are allowed via .passthrough() for migration scenarios
|
||||
});
|
||||
|
||||
it('should accept nested valid settings', () => {
|
||||
const validSettings = {
|
||||
ui: {
|
||||
theme: 'dark',
|
||||
hideWindowTitle: true,
|
||||
footer: {
|
||||
hideCWD: false,
|
||||
hideModelInfo: true,
|
||||
},
|
||||
},
|
||||
tools: {
|
||||
sandbox: 'inherit',
|
||||
autoAccept: false,
|
||||
},
|
||||
};
|
||||
|
||||
const result = validateSettings(validSettings);
|
||||
expect(result.success).toBe(true);
|
||||
});
|
||||
|
||||
it('should validate array types correctly', () => {
|
||||
const validSettings = {
|
||||
tools: {
|
||||
allowed: ['git', 'npm'],
|
||||
exclude: ['dangerous-tool'],
|
||||
},
|
||||
context: {
|
||||
includeDirectories: ['/path/1', '/path/2'],
|
||||
},
|
||||
};
|
||||
|
||||
const result = validateSettings(validSettings);
|
||||
expect(result.success).toBe(true);
|
||||
});
|
||||
|
||||
it('should reject invalid types in arrays', () => {
|
||||
const invalidSettings = {
|
||||
tools: {
|
||||
allowed: ['git', 123],
|
||||
},
|
||||
};
|
||||
|
||||
const result = validateSettings(invalidSettings);
|
||||
expect(result.success).toBe(false);
|
||||
});
|
||||
|
||||
it('should validate boolean fields correctly', () => {
|
||||
const validSettings = {
|
||||
general: {
|
||||
vimMode: true,
|
||||
disableAutoUpdate: false,
|
||||
},
|
||||
};
|
||||
|
||||
const result = validateSettings(validSettings);
|
||||
expect(result.success).toBe(true);
|
||||
});
|
||||
|
||||
it('should reject non-boolean values for boolean fields', () => {
|
||||
const invalidSettings = {
|
||||
general: {
|
||||
vimMode: 'yes',
|
||||
},
|
||||
};
|
||||
|
||||
const result = validateSettings(invalidSettings);
|
||||
expect(result.success).toBe(false);
|
||||
});
|
||||
|
||||
it('should validate number fields correctly', () => {
|
||||
const validSettings = {
|
||||
model: {
|
||||
maxSessionTurns: 50,
|
||||
compressionThreshold: 0.2,
|
||||
},
|
||||
};
|
||||
|
||||
const result = validateSettings(validSettings);
|
||||
expect(result.success).toBe(true);
|
||||
});
|
||||
|
||||
it('should validate complex nested mcpServers configuration', () => {
|
||||
const invalidSettings = {
|
||||
mcpServers: {
|
||||
'my-server': {
|
||||
command: 123, // Should be string
|
||||
args: ['arg1'],
|
||||
env: {
|
||||
VAR: 'value',
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const result = validateSettings(invalidSettings);
|
||||
expect(result.success).toBe(false);
|
||||
if (result.error) {
|
||||
expect(result.error.issues.length).toBeGreaterThan(0);
|
||||
// Path should be mcpServers.my-server.command
|
||||
const issue = result.error.issues.find((i) =>
|
||||
i.path.includes('command'),
|
||||
);
|
||||
expect(issue).toBeDefined();
|
||||
expect(issue?.code).toBe('invalid_type');
|
||||
}
|
||||
});
|
||||
|
||||
it('should validate complex nested customThemes configuration', () => {
|
||||
const invalidSettings = {
|
||||
ui: {
|
||||
customThemes: {
|
||||
'my-theme': {
|
||||
type: 'custom',
|
||||
// Missing 'name' property which is required
|
||||
text: {
|
||||
primary: '#ffffff',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const result = validateSettings(invalidSettings);
|
||||
expect(result.success).toBe(false);
|
||||
if (result.error) {
|
||||
expect(result.error.issues.length).toBeGreaterThan(0);
|
||||
// Should complain about missing 'name'
|
||||
const issue = result.error.issues.find(
|
||||
(i) => i.code === 'invalid_type' && i.message.includes('Required'),
|
||||
);
|
||||
expect(issue).toBeDefined();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('formatValidationError', () => {
|
||||
it('should format error with file path and helpful message for model.name', () => {
|
||||
const invalidSettings = {
|
||||
model: {
|
||||
name: {
|
||||
skipNextSpeakerCheck: true,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const result = validateSettings(invalidSettings);
|
||||
expect(result.success).toBe(false);
|
||||
|
||||
if (result.error) {
|
||||
const formatted = formatValidationError(
|
||||
result.error,
|
||||
'/path/to/settings.json',
|
||||
);
|
||||
|
||||
expect(formatted).toContain('/path/to/settings.json');
|
||||
expect(formatted).toContain('model.name');
|
||||
expect(formatted).toContain('Expected: string, but received: object');
|
||||
expect(formatted).toContain(
|
||||
'Please fix the configuration and try again.',
|
||||
);
|
||||
expect(formatted).toContain(
|
||||
'https://github.com/google-gemini/gemini-cli',
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
it('should format error for model.summarizeToolOutput', () => {
|
||||
const invalidSettings = {
|
||||
model: {
|
||||
summarizeToolOutput: 'wrong type',
|
||||
},
|
||||
};
|
||||
|
||||
const result = validateSettings(invalidSettings);
|
||||
expect(result.success).toBe(false);
|
||||
|
||||
if (result.error) {
|
||||
const formatted = formatValidationError(
|
||||
result.error,
|
||||
'~/.gemini/settings.json',
|
||||
);
|
||||
|
||||
expect(formatted).toContain('~/.gemini/settings.json');
|
||||
expect(formatted).toContain('model.summarizeToolOutput');
|
||||
}
|
||||
});
|
||||
|
||||
it('should include link to documentation', () => {
|
||||
const invalidSettings = {
|
||||
model: {
|
||||
name: { invalid: 'object' }, // model.name should be a string
|
||||
},
|
||||
};
|
||||
|
||||
const result = validateSettings(invalidSettings);
|
||||
expect(result.success).toBe(false);
|
||||
|
||||
if (result.error) {
|
||||
const formatted = formatValidationError(result.error, 'test.json');
|
||||
|
||||
expect(formatted).toContain(
|
||||
'https://github.com/google-gemini/gemini-cli',
|
||||
);
|
||||
expect(formatted).toContain('configuration.md');
|
||||
}
|
||||
});
|
||||
|
||||
it('should list all validation errors', () => {
|
||||
const invalidSettings = {
|
||||
model: {
|
||||
name: { invalid: 'object' },
|
||||
maxSessionTurns: 'not a number',
|
||||
},
|
||||
};
|
||||
|
||||
const result = validateSettings(invalidSettings);
|
||||
expect(result.success).toBe(false);
|
||||
|
||||
if (result.error) {
|
||||
const formatted = formatValidationError(result.error, 'test.json');
|
||||
|
||||
// Should have multiple errors listed
|
||||
expect(formatted.match(/Error in:/g)?.length).toBeGreaterThan(1);
|
||||
}
|
||||
});
|
||||
|
||||
it('should format array paths correctly (e.g. tools.allowed[0])', () => {
|
||||
const invalidSettings = {
|
||||
tools: {
|
||||
allowed: ['git', 123], // 123 is invalid, expected string
|
||||
},
|
||||
};
|
||||
|
||||
const result = validateSettings(invalidSettings);
|
||||
expect(result.success).toBe(false);
|
||||
|
||||
if (result.error) {
|
||||
const formatted = formatValidationError(result.error, 'test.json');
|
||||
expect(formatted).toContain('tools.allowed[1]');
|
||||
}
|
||||
});
|
||||
|
||||
it('should limit the number of displayed errors', () => {
|
||||
const invalidSettings = {
|
||||
tools: {
|
||||
// Create 6 invalid items to trigger the limit
|
||||
allowed: [1, 2, 3, 4, 5, 6],
|
||||
},
|
||||
};
|
||||
|
||||
const result = validateSettings(invalidSettings);
|
||||
expect(result.success).toBe(false);
|
||||
|
||||
if (result.error) {
|
||||
const formatted = formatValidationError(result.error, 'test.json');
|
||||
// Should see the first 5
|
||||
expect(formatted).toContain('tools.allowed[0]');
|
||||
expect(formatted).toContain('tools.allowed[4]');
|
||||
// Should NOT see the 6th
|
||||
expect(formatted).not.toContain('tools.allowed[5]');
|
||||
// Should see the summary
|
||||
expect(formatted).toContain('...and 1 more errors.');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('settingsZodSchema', () => {
|
||||
it('should be a valid Zod object schema', () => {
|
||||
expect(settingsZodSchema).toBeInstanceOf(z.ZodObject);
|
||||
});
|
||||
|
||||
it('should have optional fields', () => {
|
||||
// All top-level fields should be optional
|
||||
const shape = settingsZodSchema.shape;
|
||||
expect(shape['model']).toBeDefined();
|
||||
expect(shape['ui']).toBeDefined();
|
||||
expect(shape['tools']).toBeDefined();
|
||||
|
||||
// Test that empty object is valid (all fields optional)
|
||||
const result = settingsZodSchema.safeParse({});
|
||||
expect(result.success).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,331 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2025 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { z } from 'zod';
|
||||
import {
|
||||
getSettingsSchema,
|
||||
type SettingDefinition,
|
||||
type SettingCollectionDefinition,
|
||||
SETTINGS_SCHEMA_DEFINITIONS,
|
||||
} from './settingsSchema.js';
|
||||
|
||||
// Helper to build Zod schema from the JSON-schema-like definitions
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
function buildZodSchemaFromJsonSchema(def: any): z.ZodTypeAny {
|
||||
if (def.anyOf) {
|
||||
return z.union(
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
def.anyOf.map((d: any) => buildZodSchemaFromJsonSchema(d)),
|
||||
);
|
||||
}
|
||||
|
||||
if (def.type === 'string') {
|
||||
if (def.enum) return z.enum(def.enum as [string, ...string[]]);
|
||||
return z.string();
|
||||
}
|
||||
if (def.type === 'number') return z.number();
|
||||
if (def.type === 'boolean') return z.boolean();
|
||||
|
||||
if (def.type === 'array') {
|
||||
if (def.items) {
|
||||
return z.array(buildZodSchemaFromJsonSchema(def.items));
|
||||
}
|
||||
return z.array(z.unknown());
|
||||
}
|
||||
|
||||
if (def.type === 'object') {
|
||||
let schema;
|
||||
if (def.properties) {
|
||||
const shape: Record<string, z.ZodTypeAny> = {};
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
for (const [key, propDef] of Object.entries(def.properties) as any) {
|
||||
let propSchema = buildZodSchemaFromJsonSchema(propDef);
|
||||
if (
|
||||
def.required &&
|
||||
Array.isArray(def.required) &&
|
||||
def.required.includes(key)
|
||||
) {
|
||||
// keep it required
|
||||
} else {
|
||||
propSchema = propSchema.optional();
|
||||
}
|
||||
shape[key] = propSchema;
|
||||
}
|
||||
schema = z.object(shape).passthrough();
|
||||
} else {
|
||||
schema = z.object({}).passthrough();
|
||||
}
|
||||
|
||||
if (def.additionalProperties === false) {
|
||||
schema = schema.strict();
|
||||
} else if (typeof def.additionalProperties === 'object') {
|
||||
schema = schema.catchall(
|
||||
buildZodSchemaFromJsonSchema(def.additionalProperties),
|
||||
);
|
||||
}
|
||||
|
||||
return schema;
|
||||
}
|
||||
|
||||
return z.unknown();
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds a Zod enum schema from options array
|
||||
*/
|
||||
function buildEnumSchema(
|
||||
options: ReadonlyArray<{ value: string | number | boolean; label: string }>,
|
||||
): z.ZodTypeAny {
|
||||
if (!options || options.length === 0) {
|
||||
throw new Error(
|
||||
`Enum type must have options defined. Check your settings schema definition.`,
|
||||
);
|
||||
}
|
||||
const values = options.map((opt) => opt.value);
|
||||
if (values.every((v) => typeof v === 'string')) {
|
||||
return z.enum(values as [string, ...string[]]);
|
||||
} else if (values.every((v) => typeof v === 'number')) {
|
||||
return z.union(
|
||||
values.map((v) => z.literal(v)) as [
|
||||
z.ZodLiteral<number>,
|
||||
z.ZodLiteral<number>,
|
||||
...Array<z.ZodLiteral<number>>,
|
||||
],
|
||||
);
|
||||
} else {
|
||||
return z.union(
|
||||
values.map((v) => z.literal(v)) as [
|
||||
z.ZodLiteral<unknown>,
|
||||
z.ZodLiteral<unknown>,
|
||||
...Array<z.ZodLiteral<unknown>>,
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds a Zod object shape from properties record
|
||||
*/
|
||||
function buildObjectShapeFromProperties(
|
||||
properties: Record<string, SettingDefinition>,
|
||||
): Record<string, z.ZodTypeAny> {
|
||||
const shape: Record<string, z.ZodTypeAny> = {};
|
||||
for (const [key, childDef] of Object.entries(properties)) {
|
||||
shape[key] = buildZodSchemaFromDefinition(childDef);
|
||||
}
|
||||
return shape;
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds a Zod schema for primitive types (string, number, boolean)
|
||||
*/
|
||||
function buildPrimitiveSchema(
|
||||
type: 'string' | 'number' | 'boolean',
|
||||
): z.ZodTypeAny {
|
||||
switch (type) {
|
||||
case 'string':
|
||||
return z.string();
|
||||
case 'number':
|
||||
return z.number();
|
||||
case 'boolean':
|
||||
return z.boolean();
|
||||
default:
|
||||
return z.unknown();
|
||||
}
|
||||
}
|
||||
|
||||
const REF_SCHEMAS: Record<string, z.ZodTypeAny> = {};
|
||||
|
||||
// Initialize REF_SCHEMAS
|
||||
for (const [name, def] of Object.entries(SETTINGS_SCHEMA_DEFINITIONS)) {
|
||||
REF_SCHEMAS[name] = buildZodSchemaFromJsonSchema(def);
|
||||
}
|
||||
|
||||
/**
|
||||
* Recursively builds a Zod schema from a SettingDefinition
|
||||
*/
|
||||
function buildZodSchemaFromDefinition(
|
||||
definition: SettingDefinition,
|
||||
): z.ZodTypeAny {
|
||||
let baseSchema: z.ZodTypeAny;
|
||||
|
||||
// Special handling for TelemetrySettings which can be boolean or object
|
||||
if (definition.ref === 'TelemetrySettings') {
|
||||
const objectSchema = REF_SCHEMAS['TelemetrySettings'];
|
||||
if (objectSchema) {
|
||||
return z.union([z.boolean(), objectSchema]).optional();
|
||||
}
|
||||
}
|
||||
|
||||
// Handle refs using registry
|
||||
if (definition.ref && definition.ref in REF_SCHEMAS) {
|
||||
return REF_SCHEMAS[definition.ref].optional();
|
||||
}
|
||||
|
||||
switch (definition.type) {
|
||||
case 'string':
|
||||
case 'number':
|
||||
case 'boolean':
|
||||
baseSchema = buildPrimitiveSchema(definition.type);
|
||||
break;
|
||||
|
||||
case 'enum': {
|
||||
baseSchema = buildEnumSchema(definition.options!);
|
||||
break;
|
||||
}
|
||||
|
||||
case 'array':
|
||||
if (definition.items) {
|
||||
const itemSchema = buildZodSchemaFromCollection(definition.items);
|
||||
baseSchema = z.array(itemSchema);
|
||||
} else {
|
||||
baseSchema = z.array(z.unknown());
|
||||
}
|
||||
break;
|
||||
|
||||
case 'object':
|
||||
if (definition.properties) {
|
||||
const shape = buildObjectShapeFromProperties(definition.properties);
|
||||
baseSchema = z.object(shape).passthrough();
|
||||
|
||||
if (definition.additionalProperties) {
|
||||
const additionalSchema = buildZodSchemaFromCollection(
|
||||
definition.additionalProperties,
|
||||
);
|
||||
baseSchema = z.object(shape).catchall(additionalSchema);
|
||||
}
|
||||
} else if (definition.additionalProperties) {
|
||||
const valueSchema = buildZodSchemaFromCollection(
|
||||
definition.additionalProperties,
|
||||
);
|
||||
baseSchema = z.record(z.string(), valueSchema);
|
||||
} else {
|
||||
baseSchema = z.record(z.string(), z.unknown());
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
baseSchema = z.unknown();
|
||||
}
|
||||
|
||||
// Make all fields optional since settings are partial
|
||||
return baseSchema.optional();
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds a Zod schema from a SettingCollectionDefinition
|
||||
*/
|
||||
function buildZodSchemaFromCollection(
|
||||
collection: SettingCollectionDefinition,
|
||||
): z.ZodTypeAny {
|
||||
if (collection.ref && collection.ref in REF_SCHEMAS) {
|
||||
return REF_SCHEMAS[collection.ref];
|
||||
}
|
||||
|
||||
switch (collection.type) {
|
||||
case 'string':
|
||||
case 'number':
|
||||
case 'boolean':
|
||||
return buildPrimitiveSchema(collection.type);
|
||||
|
||||
case 'enum': {
|
||||
return buildEnumSchema(collection.options!);
|
||||
}
|
||||
|
||||
case 'array':
|
||||
if (collection.properties) {
|
||||
const shape = buildObjectShapeFromProperties(collection.properties);
|
||||
return z.array(z.object(shape));
|
||||
}
|
||||
return z.array(z.unknown());
|
||||
|
||||
case 'object':
|
||||
if (collection.properties) {
|
||||
const shape = buildObjectShapeFromProperties(collection.properties);
|
||||
return z.object(shape).passthrough();
|
||||
}
|
||||
return z.record(z.string(), z.unknown());
|
||||
|
||||
default:
|
||||
return z.unknown();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds the complete Zod schema for Settings from SETTINGS_SCHEMA
|
||||
*/
|
||||
function buildSettingsZodSchema(): z.ZodObject<Record<string, z.ZodTypeAny>> {
|
||||
const schema = getSettingsSchema();
|
||||
const shape: Record<string, z.ZodTypeAny> = {};
|
||||
|
||||
for (const [key, definition] of Object.entries(schema)) {
|
||||
shape[key] = buildZodSchemaFromDefinition(definition);
|
||||
}
|
||||
|
||||
return z.object(shape).passthrough();
|
||||
}
|
||||
|
||||
export const settingsZodSchema = buildSettingsZodSchema();
|
||||
|
||||
/**
|
||||
* Validates settings data against the Zod schema
|
||||
*/
|
||||
export function validateSettings(data: unknown): {
|
||||
success: boolean;
|
||||
data?: unknown;
|
||||
error?: z.ZodError;
|
||||
} {
|
||||
const result = settingsZodSchema.safeParse(data);
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Format a Zod error into a helpful error message
|
||||
*/
|
||||
export function formatValidationError(
|
||||
error: z.ZodError,
|
||||
filePath: string,
|
||||
): string {
|
||||
const lines: string[] = [];
|
||||
lines.push(`Invalid configuration in ${filePath}:`);
|
||||
lines.push('');
|
||||
|
||||
const MAX_ERRORS_TO_DISPLAY = 5;
|
||||
const displayedIssues = error.issues.slice(0, MAX_ERRORS_TO_DISPLAY);
|
||||
|
||||
for (const issue of displayedIssues) {
|
||||
const path = issue.path.reduce(
|
||||
(acc, curr) =>
|
||||
typeof curr === 'number'
|
||||
? `${acc}[${curr}]`
|
||||
: `${acc ? acc + '.' : ''}${curr}`,
|
||||
'',
|
||||
);
|
||||
lines.push(`Error in: ${path || '(root)'}`);
|
||||
lines.push(` ${issue.message}`);
|
||||
|
||||
if (issue.code === 'invalid_type') {
|
||||
const expected = issue.expected;
|
||||
const received = issue.received;
|
||||
lines.push(`Expected: ${expected}, but received: ${received}`);
|
||||
}
|
||||
lines.push('');
|
||||
}
|
||||
|
||||
if (error.issues.length > MAX_ERRORS_TO_DISPLAY) {
|
||||
lines.push(
|
||||
`...and ${error.issues.length - MAX_ERRORS_TO_DISPLAY} more errors.`,
|
||||
);
|
||||
lines.push('');
|
||||
}
|
||||
|
||||
lines.push('Please fix the configuration and try again.');
|
||||
lines.push(
|
||||
'See: https://github.com/google-gemini/gemini-cli/blob/main/docs/get-started/configuration.md',
|
||||
);
|
||||
|
||||
return lines.join('\n');
|
||||
}
|
||||
@@ -291,7 +291,7 @@ describe('Settings Loading and Merging', () => {
|
||||
theme: 'legacy-dark',
|
||||
vimMode: true,
|
||||
contextFileName: 'LEGACY_CONTEXT.md',
|
||||
model: 'gemini-pro',
|
||||
model: 'gemini-2.5-pro',
|
||||
mcpServers: {
|
||||
'legacy-server-1': {
|
||||
command: 'npm',
|
||||
@@ -329,7 +329,7 @@ describe('Settings Loading and Merging', () => {
|
||||
fileName: 'LEGACY_CONTEXT.md',
|
||||
},
|
||||
model: {
|
||||
name: 'gemini-pro',
|
||||
name: 'gemini-2.5-pro',
|
||||
},
|
||||
mcpServers: {
|
||||
'legacy-server-1': {
|
||||
@@ -371,6 +371,37 @@ describe('Settings Loading and Merging', () => {
|
||||
expect((settings.merged as TestSettings)['allowedTools']).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should allow V2 settings to override V1 settings when both are present (zombie setting fix)', () => {
|
||||
(mockFsExistsSync as Mock).mockImplementation(
|
||||
(p: fs.PathLike) => p === USER_SETTINGS_PATH,
|
||||
);
|
||||
const mixedSettingsContent = {
|
||||
// V1 setting (migrates to ui.accessibility.screenReader = true)
|
||||
accessibility: {
|
||||
screenReader: true,
|
||||
},
|
||||
// V2 setting (explicitly set to false)
|
||||
ui: {
|
||||
accessibility: {
|
||||
screenReader: false,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
(fs.readFileSync as Mock).mockImplementation(
|
||||
(p: fs.PathOrFileDescriptor) => {
|
||||
if (p === USER_SETTINGS_PATH)
|
||||
return JSON.stringify(mixedSettingsContent);
|
||||
return '{}';
|
||||
},
|
||||
);
|
||||
|
||||
const settings = loadSettings(MOCK_WORKSPACE_DIR);
|
||||
|
||||
// We expect the V2 setting (false) to win, NOT the migrated V1 setting (true)
|
||||
expect(settings.merged.ui?.accessibility?.screenReader).toBe(false);
|
||||
});
|
||||
|
||||
it('should correctly merge and migrate legacy array properties from multiple scopes', () => {
|
||||
(mockFsExistsSync as Mock).mockReturnValue(true);
|
||||
const legacyUserSettings = {
|
||||
@@ -1929,7 +1960,7 @@ describe('Settings Loading and Merging', () => {
|
||||
usageStatisticsEnabled: false,
|
||||
},
|
||||
model: {
|
||||
name: 'gemini-pro',
|
||||
name: 'gemini-2.5-pro',
|
||||
},
|
||||
context: {
|
||||
fileName: 'CONTEXT.md',
|
||||
@@ -1968,7 +1999,7 @@ describe('Settings Loading and Merging', () => {
|
||||
vimMode: true,
|
||||
theme: 'dark',
|
||||
usageStatisticsEnabled: false,
|
||||
model: 'gemini-pro',
|
||||
model: 'gemini-2.5-pro',
|
||||
contextFileName: 'CONTEXT.md',
|
||||
includeDirectories: ['/src'],
|
||||
sandbox: true,
|
||||
@@ -2231,7 +2262,7 @@ describe('Settings Loading and Merging', () => {
|
||||
beforeEach(() => {
|
||||
vi.resetAllMocks();
|
||||
mockFsExistsSync = vi.mocked(fs.existsSync);
|
||||
(mockFsExistsSync as Mock).mockReturnValue(true);
|
||||
mockFsExistsSync.mockReturnValue(true);
|
||||
mockFsReadFileSync = vi.mocked(fs.readFileSync);
|
||||
mockFsReadFileSync.mockReturnValue('{}');
|
||||
vi.mocked(isWorkspaceTrusted).mockReturnValue({
|
||||
@@ -2256,15 +2287,13 @@ describe('Settings Loading and Merging', () => {
|
||||
},
|
||||
};
|
||||
|
||||
(mockFsReadFileSync as Mock).mockImplementation(
|
||||
(p: fs.PathOrFileDescriptor) => {
|
||||
if (p === USER_SETTINGS_PATH)
|
||||
return JSON.stringify(userSettingsContent);
|
||||
if (p === MOCK_WORKSPACE_SETTINGS_PATH)
|
||||
return JSON.stringify(workspaceSettingsContent);
|
||||
return '{}';
|
||||
},
|
||||
);
|
||||
mockFsReadFileSync.mockImplementation((p: fs.PathOrFileDescriptor) => {
|
||||
if (p === USER_SETTINGS_PATH)
|
||||
return JSON.stringify(userSettingsContent);
|
||||
if (p === MOCK_WORKSPACE_SETTINGS_PATH)
|
||||
return JSON.stringify(workspaceSettingsContent);
|
||||
return '{}';
|
||||
});
|
||||
|
||||
const loadedSettings = loadSettings(MOCK_WORKSPACE_DIR);
|
||||
const setValueSpy = vi.spyOn(loadedSettings, 'setValue');
|
||||
@@ -2329,15 +2358,13 @@ describe('Settings Loading and Merging', () => {
|
||||
someOtherSetting: 'value',
|
||||
};
|
||||
|
||||
(mockFsReadFileSync as Mock).mockImplementation(
|
||||
(p: fs.PathOrFileDescriptor) => {
|
||||
if (p === USER_SETTINGS_PATH)
|
||||
return JSON.stringify(userSettingsContent);
|
||||
if (p === MOCK_WORKSPACE_SETTINGS_PATH)
|
||||
return JSON.stringify(workspaceSettingsContent);
|
||||
return '{}';
|
||||
},
|
||||
);
|
||||
mockFsReadFileSync.mockImplementation((p: fs.PathOrFileDescriptor) => {
|
||||
if (p === USER_SETTINGS_PATH)
|
||||
return JSON.stringify(userSettingsContent);
|
||||
if (p === MOCK_WORKSPACE_SETTINGS_PATH)
|
||||
return JSON.stringify(workspaceSettingsContent);
|
||||
return '{}';
|
||||
});
|
||||
|
||||
const loadedSettings = loadSettings(MOCK_WORKSPACE_DIR);
|
||||
const setValueSpy = vi.spyOn(loadedSettings, 'setValue');
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user