Compare commits

..

53 Commits

Author SHA1 Message Date
jacob314 68e33a1103 fix(test): regenerate settings documentation and schema 2026-04-23 14:34:43 -07:00
jacob314 19601955fd feat(routing): availability-aware auto-routing with best-effort pro
Adds settings and logic to detect slow/hanging Pro model requests, marking them as temporarily unavailable and automatically triggering a fallback to Flash. Introduces a proTimeoutMinutes and bestEffortPro strategy configuration.
2026-04-23 13:50:24 -07:00
hsm207 ff28d55100 fix: fatal hard-crash on loop detection via unhandled AbortError (#20108)
Co-authored-by: Tommaso Sciortino <sciortino@gmail.com>
2026-04-23 16:51:21 +00:00
Emily Hedlund dba9b9a0ff feat(cli): secure .env loading and enforce workspace trust in headless mode (#25814)
Co-authored-by: galz10 <galzahavi@google.com>
Co-authored-by: davidapierce <davidapierce@google.com>
2026-04-23 16:09:14 +00:00
cynthialong0-0 a007f64d20 fix(core): only show list suggestion if the partial input is empty (#25821) 2026-04-23 14:07:06 +00:00
gemini-cli-robot d1c91f5267 chore(release): bump version to 0.41.0-nightly.20260423.gaa05b4583 (#25847) 2026-04-23 04:28:26 +00:00
Sandy Tao aa05b4583d fix(release): exclude ripgrep binaries from npm tarballs (#25841) 2026-04-22 21:01:45 -07:00
Sandy Tao 5318610c1d fix(core): support jsonl session logs in memory and summary services (#25816) 2026-04-22 23:07:39 +00:00
Sandy Tao 9c0a6864da fix(devtools): reduce memory usage and defer connection (#24496) 2026-04-22 15:21:58 -07:00
Dev Randalpura 2e12c34009 fix(ui): removed background color for input (#25339) 2026-04-22 20:27:09 +00:00
Horizon_Architect_07 2a52611e71 Fix/command injection shell (#24170)
Co-authored-by: David Pierce <davidapierce@google.com>
2026-04-22 20:00:44 +00:00
xoma-zver 1c43deee07 fix(core): handle line endings in ignore file parsing (#23895)
Co-authored-by: cynthialong0-0 <82900738+cynthialong0-0@users.noreply.github.com>
2026-04-22 18:09:36 +00:00
Kishan Patel 0758a5eb28 fix(core): use dynamic CLI version for IDE client instead of hardcoded '1.0.0' (#24414)
Co-authored-by: cynthialong0-0 <82900738+cynthialong0-0@users.noreply.github.com>
2026-04-22 16:43:48 +00:00
mini2s 607180bfb2 fix(cli): fix "/clear (new)" command (#25801) 2026-04-22 15:20:22 +00:00
Sandy Tao 6edfba481f refactor(memory): replace MemoryManagerAgent with prompt-driven memory editing across four tiers (#25716) 2026-04-22 01:21:55 +00:00
Sandy Tao ffb28c772b test(e2e): default integration tests to Flash Preview (#25753) 2026-04-21 22:21:52 +00:00
Adam Weidman d6f88f8720 fix(core): remove duplicate initialize call on agents refreshed (#25670) 2026-04-21 20:17:21 +00:00
Jason Matthew Suhari 194c779f9b fix(cli): start auto memory in ACP sessions (#25626) 2026-04-21 20:06:30 +00:00
Vedant Mahajan 189c0ac0a0 feat: add /new as alias for /clear and refine command description (#17865) 2026-04-21 20:04:40 +00:00
euxaristia c47233a474 fix(core): disable detached mode in Bun to prevent immediate SIGHUP of child processes (#22620) 2026-04-21 20:01:28 +00:00
JAYADITYA 8999a885f0 fix(cli): ensure theme dialog labels are rendered for all themes (#24599)
Co-authored-by: cynthialong0-0 <82900738+cynthialong0-0@users.noreply.github.com>
2026-04-21 19:57:15 +00:00
Coco Sheng 93a8d9001c fix(cli): use newline in shell command wrapping to avoid breaking heredocs (#25537) 2026-04-21 19:12:50 +00:00
PRAS Samin cdc5cccc13 feat: detect new files in @ recommendations with watcher based updates (#25256) 2026-04-21 18:35:14 +00:00
Mahima Shanware a4e98c0a4c fix(core): resolve nested plan directory duplication and relative path policies (#25138) 2026-04-21 18:20:57 +00:00
Spencer c260550146 feat(telemetry): add flag for enabling traces specifically (#25343) 2026-04-21 18:07:32 +00:00
Danyel Cabello 7f8f3309a6 Allow dots on GEMINI_API_KEY (#25497) 2026-04-21 11:43:39 -07:00
Muhammad Ahsan Farooq ebebbbfc20 Fix/allow for session persistence (#25176) 2026-04-21 11:20:07 -07:00
Gordon Hui 27344833cb feat(vertex): add settings for Vertex AI request routing (#25513) 2026-04-21 17:48:30 +00:00
cynthialong0-0 aee2cde1a3 feat(test): refactor the memory usage test to use metrics from CLI process instead of test runner (#25708) 2026-04-21 17:06:22 +00:00
Mundur 2c14954010 Fix: Disallow overriding IDE stdio via workspace .env (RCE) (#25022)
Co-authored-by: Tommaso Sciortino <sciortino@gmail.com>
2026-04-21 10:31:10 -07:00
gemini-cli-robot a38e2f0048 Changelog for v0.38.2 (#25593)
Co-authored-by: gemini-cli-robot <224641728+gemini-cli-robot@users.noreply.github.com>
2026-04-21 00:39:10 +00:00
Samee Zahid 1d383a4a8e feat(cli): add streamlined gemini gemma local model setup (#25498)
Co-authored-by: Abhijit Balaji <abhijitbalaji@google.com>
Co-authored-by: Samee Zahid <sameez@google.com>
Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
2026-04-20 23:57:56 +00:00
Timo 6afc47f81c docs(cli): fix inconsistent system.md casing in system prompt docs (#25414)
Co-authored-by: cynthialong0-0 <82900738+cynthialong0-0@users.noreply.github.com>
2026-04-20 23:03:36 +00:00
anj-s 4b2091d402 docs(tracker): introduce experimental task tracker feature (#24556) 2026-04-20 19:33:37 +00:00
Tommaso Sciortino c627d09326 fix(cli): round slow render latency to avoid opentelemetry float warning (#25709) 2026-04-20 17:42:37 +00:00
Sehoon Shon 0e5c5b6f49 fix(core): allow Cloud Shell users to use PRO_MODEL_NO_ACCESS experiment (#25702) 2026-04-20 17:29:58 +00:00
Sandy Tao 8573650253 feat(config): split memoryManager flag into autoMemory (#25601) 2026-04-18 00:08:02 +00:00
Sam Roberts 220888ac2d docs: migrate installation and authentication to mdx with tabbed layouts (#25155) 2026-04-17 18:07:07 +00:00
Christian Gunderman b2f775921d feat(core): enable topic update narration by default and promote to general (#25586)
Co-authored-by: JAYADITYA <96861162+JayadityaGit@users.noreply.github.com>
Co-authored-by: Jack Wotherspoon <jackwoth@google.com>
2026-04-17 17:49:32 +00:00
Christian Gunderman f7b2632939 feat(core): Disable topic updates for subagents (#25567) 2026-04-17 17:35:17 +00:00
Mahima Shanware 8379099e85 fix(core): fix ShellExecutionConfig spread and add ProjectRegistry save backoff (#25382) 2026-04-17 16:49:44 +00:00
JAYADITYA 3061555d28 fix(ui): revert preview theme on dialog unmount (#22542)
Co-authored-by: Jack Wotherspoon <jackwoth@google.com>
2026-04-17 14:56:50 +00:00
Christian Gunderman 22fb83320e Reduce blank lines. (#25563) 2026-04-16 23:54:57 +00:00
Matt Van Horn 63e4bb985b feat(sandbox): resolve custom seatbelt profiles from $HOME/.gemini first (#25427)
Co-authored-by: Matt Van Horn <455140+mvanhorn@users.noreply.github.com>
2026-04-16 22:21:24 +00:00
Gal Zahavi fe890429a4 fix(core): allow explicit write permissions to override governance file protections in sandboxes (#25338) 2026-04-16 21:18:09 +00:00
Nicolas Ouellet-Payeur 655165cde4 docs(policy): mention that workspace policies are broken (#24367)
Co-authored-by: Nicolas Ouellet-Payeur <nicolaso@chromium.org>
2026-04-16 21:04:46 +00:00
Sam Roberts 17557b1aeb feat(core): add .mdx support to get-internal-docs tool (#25090) 2026-04-16 20:58:34 +00:00
Jason Matthew Suhari 9600da2c8f fix(cli): reset plan session state on /clear (#25515) 2026-04-16 19:20:36 +00:00
Abhi 2b6dab6136 fix(extensions): fix bundling for examples (#25542) 2026-04-16 19:11:03 +00:00
jackyliuxx ac9025e9fc Use OSC 777 for terminal notifications (#25300) 2026-04-16 18:45:54 +00:00
Sandy Tao fafe3e35d2 fix(evals): add typecheck coverage for evals, integration-tests, and memory-tests (#25480) 2026-04-16 18:20:27 +00:00
ruomeng f16f1cced3 feat(core): add tools to list and read MCP resources (#25395) 2026-04-16 17:57:43 +00:00
ruomeng 963631a3d4 feat(cli): provide default post-submit prompt for skill command (#25327) 2026-04-16 17:56:20 +00:00
298 changed files with 13052 additions and 3334 deletions
+1 -2
View File
@@ -2,8 +2,7 @@
"experimental": {
"extensionReloading": true,
"modelSteering": true,
"memoryManager": true,
"topicUpdateNarration": true
"autoMemory": true
},
"general": {
"devtools": true
+15 -6
View File
@@ -85,17 +85,25 @@ accessible.
- **Callouts**: Use GitHub-flavored markdown alerts to highlight important
information. To ensure the formatting is preserved by `npm run format`, place
an empty line, then the `<!-- prettier-ignore -->` comment directly before
the callout block. The callout type (`[!TYPE]`) should be on the first line,
followed by a newline, and then the content, with each subsequent line of
content starting with `>`. Available types are `NOTE`, `TIP`, `IMPORTANT`,
`WARNING`, and `CAUTION`.
an empty line, then a prettier ignore comment directly before the callout
block. Use `<!-- prettier-ignore -->` for standard Markdown files (`.md`) and
`{/* prettier-ignore */}` for MDX files (`.mdx`). The callout type (`[!TYPE]`)
should be on the first line, followed by a newline, and then the content, with
each subsequent line of content starting with `>`. Available types are `NOTE`,
`TIP`, `IMPORTANT`, `WARNING`, and `CAUTION`.
Example:
Example (.md):
<!-- prettier-ignore -->
> [!NOTE]
> This is an example of a multi-line note that will be preserved
> by Prettier.
Example (.mdx):
{/* prettier-ignore */}
> [!NOTE]
> This is an example of a multi-line note that will be preserved
> by Prettier.
### Links
@@ -118,6 +126,7 @@ accessible.
<!-- prettier-ignore -->
> [!NOTE]
> This is an experimental feature currently under active development.
(Note: Use `{/* prettier-ignore */}` if editing an `.mdx` file.)
- **Headings:** Use hierarchical headings to support the user journey.
- **Procedures:**
+1
View File
@@ -28,6 +28,7 @@ runs:
- name: 'Run Tests'
env:
GEMINI_API_KEY: '${{ inputs.gemini_api_key }}'
GEMINI_CLI_TRUST_WORKSPACE: true
working-directory: '${{ inputs.working-directory }}'
run: |-
echo "::group::Build"
@@ -98,6 +98,7 @@ runs:
working-directory: '${{ inputs.working-directory }}'
env:
GEMINI_API_KEY: '${{ inputs.gemini_api_key }}'
GEMINI_CLI_TRUST_WORKSPACE: true
INTEGRATION_TEST_USE_INSTALLED_GEMINI: 'true'
# We must diable CI mode here because it interferes with interactive tests.
# See https://github.com/google-gemini/gemini-cli/issues/10517
+3
View File
@@ -167,6 +167,7 @@ jobs:
- name: 'Run E2E tests'
env:
GEMINI_API_KEY: '${{ secrets.GEMINI_API_KEY }}'
GEMINI_CLI_TRUST_WORKSPACE: true
KEEP_OUTPUT: 'true'
VERBOSE: 'true'
BUILD_SANDBOX_FLAGS: '--cache-from type=gha --cache-to type=gha,mode=max'
@@ -212,6 +213,7 @@ jobs:
if: "${{runner.os != 'Windows'}}"
env:
GEMINI_API_KEY: '${{ secrets.GEMINI_API_KEY }}'
GEMINI_CLI_TRUST_WORKSPACE: true
KEEP_OUTPUT: 'true'
SANDBOX: 'sandbox:none'
VERBOSE: 'true'
@@ -288,6 +290,7 @@ jobs:
- name: 'Run E2E tests'
env:
GEMINI_API_KEY: '${{ secrets.GEMINI_API_KEY }}'
GEMINI_CLI_TRUST_WORKSPACE: true
KEEP_OUTPUT: 'true'
SANDBOX: 'sandbox:none'
VERBOSE: 'true'
+9
View File
@@ -102,6 +102,12 @@ jobs:
- name: 'Run yamllint'
run: 'node scripts/lint.js --yamllint'
- name: 'Build project for typecheck'
run: 'npm run build'
- name: 'Run typecheck'
run: 'npm run typecheck'
- name: 'Run Prettier'
run: 'node scripts/lint.js --prettier'
@@ -173,6 +179,7 @@ jobs:
- name: 'Run tests and generate reports'
env:
NO_COLOR: true
GEMINI_CLI_TRUST_WORKSPACE: true
run: |
if [[ "${{ matrix.shard }}" == "cli" ]]; then
npm run test:ci --workspace "@google/gemini-cli"
@@ -261,6 +268,7 @@ jobs:
- name: 'Run tests and generate reports'
env:
NO_COLOR: true
GEMINI_CLI_TRUST_WORKSPACE: true
run: |
if [[ "${{ matrix.shard }}" == "cli" ]]; then
npm run test:ci --workspace "@google/gemini-cli" -- --coverage.enabled=false
@@ -424,6 +432,7 @@ jobs:
env:
GEMINI_API_KEY: '${{ secrets.GEMINI_API_KEY }}'
NO_COLOR: true
GEMINI_CLI_TRUST_WORKSPACE: true
NODE_OPTIONS: '--max-old-space-size=32768 --max-semi-space-size=256'
UV_THREADPOOL_SIZE: '32'
NODE_ENV: 'test'
+3
View File
@@ -62,6 +62,7 @@ jobs:
- name: 'Run E2E tests'
env:
GEMINI_API_KEY: '${{ secrets.GEMINI_API_KEY }}'
GEMINI_CLI_TRUST_WORKSPACE: true
IS_DOCKER: "${{ matrix.sandbox == 'sandbox:docker' }}"
KEEP_OUTPUT: 'true'
RUNS: '${{ github.event.inputs.runs }}'
@@ -105,6 +106,7 @@ jobs:
if: "runner.os != 'Windows'"
env:
GEMINI_API_KEY: '${{ secrets.GEMINI_API_KEY }}'
GEMINI_CLI_TRUST_WORKSPACE: true
KEEP_OUTPUT: 'true'
RUNS: '${{ github.event.inputs.runs }}'
SANDBOX: 'sandbox:none'
@@ -159,6 +161,7 @@ jobs:
- name: 'Run E2E tests'
env:
GEMINI_API_KEY: '${{ secrets.GEMINI_API_KEY }}'
GEMINI_CLI_TRUST_WORKSPACE: true
KEEP_OUTPUT: 'true'
SANDBOX: 'sandbox:none'
VERBOSE: 'true'
+1
View File
@@ -141,6 +141,7 @@ jobs:
if: "github.event_name != 'pull_request'"
env:
GEMINI_API_KEY: '${{ secrets.GEMINI_API_KEY }}'
GEMINI_CLI_TRUST_WORKSPACE: true
run: |
echo "Running integration tests with binary..."
if [[ "${{ matrix.os }}" == 'windows-latest' ]]; then
+1
View File
@@ -22,3 +22,4 @@ Thumbs.db
.pytest_cache
**/SKILL.md
packages/sdk/test-data/*.json
*.mdx
+6 -3
View File
@@ -1,6 +1,6 @@
# Latest stable release: v0.38.1
# Latest stable release: v0.38.2
Released: April 15, 2026
Released: April 17, 2026
For most users, our latest stable release is the recommended release. Install
the latest stable version with:
@@ -29,6 +29,9 @@ npm install -g @google/gemini-cli
## What's Changed
- fix(patch): cherry-pick 14b2f35 to release/v0.38.1-pr-24974 to patch version
v0.38.1 and create version 0.38.2 by @gemini-cli-robot in
[#25585](https://github.com/google-gemini/gemini-cli/pull/25585)
- fix(patch): cherry-pick 050c303 to release/v0.38.0-pr-25317 to patch version
v0.38.0 and create version 0.38.1 by @gemini-cli-robot in
[#25466](https://github.com/google-gemini/gemini-cli/pull/25466)
@@ -268,4 +271,4 @@ npm install -g @google/gemini-cli
[#24844](https://github.com/google-gemini/gemini-cli/pull/24844)
**Full Changelog**:
https://github.com/google-gemini/gemini-cli/compare/v0.38.0...v0.38.1
https://github.com/google-gemini/gemini-cli/compare/v0.38.0...v0.38.2
+143
View File
@@ -0,0 +1,143 @@
# Auto Memory
Auto Memory is an experimental feature that mines your past Gemini CLI sessions
in the background and turns recurring workflows into reusable
[Agent Skills](./skills.md). You review, accept, or discard each extracted skill
before it becomes available to future sessions.
<!-- prettier-ignore -->
> [!NOTE]
> This is an experimental feature currently under active development.
## Overview
Every session you run with Gemini CLI is recorded locally as a transcript. Auto
Memory scans those transcripts for procedural patterns that recur across
sessions, then drafts each pattern as a `SKILL.md` file in a project-local
inbox. You inspect the draft, decide whether it captures real expertise, and
promote it to your global or workspace skills directory if you want it.
You'll use Auto Memory when you want to:
- **Capture team workflows** that you find yourself walking the agent through
more than once.
- **Codify hard-won fixes** for project-specific landmines so future sessions
avoid them.
- **Bootstrap a skills library** without writing every `SKILL.md` by hand.
Auto Memory complements—but does not replace—the
[`save_memory` tool](../tools/memory.md), which captures single facts into
`GEMINI.md`. Auto Memory captures multi-step procedures into skills.
## Prerequisites
- Gemini CLI installed and authenticated.
- At least 10 user messages across recent, idle sessions in the project. Auto
Memory ignores active or trivial sessions.
## How to enable Auto Memory
Auto Memory is off by default. Enable it in your settings file:
1. Open your global settings file at `~/.gemini/settings.json`. If you only
want Auto Memory in one project, edit `.gemini/settings.json` in that
project instead.
2. Add the experimental flag:
```json
{
"experimental": {
"autoMemory": true
}
}
```
3. Restart Gemini CLI. The flag requires a restart because the extraction
service starts during session boot.
## How Auto Memory works
Auto Memory runs as a background task on session startup. It does not block the
UI, consume your interactive turns, or surface tool prompts.
1. **Eligibility scan.** The service indexes recent sessions from
`~/.gemini/tmp/<project>/chats/`. Sessions are eligible only if they have
been idle for at least three hours and contain at least 10 user messages.
2. **Lock acquisition.** A lock file in the project's memory directory
coordinates across multiple CLI instances so extraction runs at most once at
a time.
3. **Sub-agent extraction.** A specialized sub-agent (named `confucius`)
reviews the session index, reads any sessions that look like they contain
repeated procedural workflows, and drafts new `SKILL.md` files. Its
instructions tell it to default to creating zero skills unless the evidence
is strong, so most runs produce no inbox items.
4. **Patch validation.** If the sub-agent proposes edits to skills outside the
inbox (for example, an existing global skill), it writes a unified diff
`.patch` file. Auto Memory dry-runs each patch and discards any that do not
apply cleanly.
5. **Notification.** When a run produces new skills or patches, Gemini CLI
surfaces an inline message telling you how many items are waiting.
## How to review extracted skills
Use the `/memory inbox` slash command to open the inbox dialog at any time:
**Command:** `/memory inbox`
The dialog lists each draft skill with its name, description, and source
sessions. From there you can:
- **Read** the full `SKILL.md` body before deciding.
- **Promote** a skill to your user (`~/.gemini/skills/`) or workspace
(`.gemini/skills/`) directory.
- **Discard** a skill you do not want.
- **Apply** or reject a `.patch` proposal against an existing skill.
Promoted skills become discoverable in the next session and follow the standard
[skill discovery precedence](./skills.md#skill-discovery-tiers).
## How to disable Auto Memory
To turn off background extraction, set the flag back to `false` in your settings
file and restart Gemini CLI:
```json
{
"experimental": {
"autoMemory": false
}
}
```
Disabling the flag stops the background service immediately on the next session
start. Existing inbox items remain on disk; you can either drain them with
`/memory inbox` first or remove the project memory directory manually.
## Data and privacy
- Auto Memory only reads session files that already exist locally on your
machine. Nothing is uploaded to Gemini outside the normal API calls the
extraction sub-agent makes during its run.
- The sub-agent is instructed to redact secrets, tokens, and credentials it
encounters and to never copy large tool outputs verbatim.
- Drafted skills live in your project's memory directory until you promote or
discard them. They are not automatically loaded into any session.
## Limitations
- The sub-agent runs on a preview Gemini Flash model. Extraction quality depends
on the model's ability to recognize durable patterns versus one-off incidents.
- Auto Memory does not extract skills from the current session. It only
considers sessions that have been idle for three hours or more.
- Inbox items are stored per project. Skills extracted in one workspace are not
visible from another until you promote them to the user-scope skills
directory.
## Next steps
- Learn how skills are discovered and activated in [Agent Skills](./skills.md).
- Explore the [memory management tutorial](./tutorials/memory-management.md) for
the complementary `save_memory` and `GEMINI.md` workflows.
- Review the experimental settings catalog in
[Settings](./settings.md#experimental).
+1
View File
@@ -52,6 +52,7 @@ These commands are available within the interactive REPL.
| `--prompt-interactive` | `-i` | string | - | Execute prompt and continue in interactive mode |
| `--worktree` | `-w` | string | - | Start Gemini in a new git worktree. If no name is provided, one is generated automatically. Requires `experimental.worktrees: true` in settings. |
| `--sandbox` | `-s` | boolean | `false` | Run in a sandboxed environment for safer execution |
| `--skip-trust` | - | boolean | `false` | Trust the current workspace for this session, skipping the folder trust check. |
| `--approval-mode` | - | string | `default` | Approval mode for tool execution. Choices: `default`, `auto_edit`, `yolo`, `plan` |
| `--yolo` | `-y` | boolean | `false` | **Deprecated.** Auto-approve all actions. Use `--approval-mode=yolo` instead. |
| `--experimental-acp` | - | boolean | - | Start in ACP (Agent Code Pilot) mode. **Experimental feature.** |
+1 -1
View File
@@ -507,7 +507,7 @@ events. For more information, see the [telemetry documentation](./telemetry.md).
You can enforce a specific authentication method for all users by setting the
`security.auth.enforcedType` in the system-level `settings.json` file. This
prevents users from choosing a different authentication method. See the
[Authentication docs](../get-started/authentication.md) for more details.
[Authentication docs](../get-started/authentication.mdx) for more details.
**Example:** Enforce the use of Google login for all users.
+4 -3
View File
@@ -130,7 +130,9 @@ These are the only allowed tools:
[`cli_help`](../core/subagents.md#cli-help-agent)
- **Interaction:** [`ask_user`](../tools/ask-user.md)
- **MCP tools (Read):** Read-only [MCP tools](../tools/mcp-server.md) (for
example, `github_read_issue`, `postgres_read_schema`) are allowed.
example, `github_read_issue`, `postgres_read_schema`) and core
[MCP resource tools](../tools/mcp-resources.md) (`list_mcp_resources`,
`read_mcp_resource`) are allowed.
- **Planning (Write):**
[`write_file`](../tools/file-system.md#3-write_file-writefile) and
[`replace`](../tools/file-system.md#6-replace-edit) only allowed for `.md`
@@ -329,7 +331,6 @@ Storage whenever Gemini CLI exits Plan Mode to start the implementation.
#!/usr/bin/env bash
# Extract the plan filename from the tool input JSON
plan_filename=$(jq -r '.tool_input.plan_filename // empty')
plan_filename=$(basename -- "$plan_filename")
# Construct the absolute path using the GEMINI_PLANS_DIR environment variable
plan_path="$GEMINI_PLANS_DIR/$plan_filename"
@@ -358,7 +359,7 @@ To register this `AfterTool` hook, add it to your `settings.json`:
{
"name": "archive-plan",
"type": "command",
"command": "./.gemini/hooks/archive-plan.sh"
"command": "~/.gemini/hooks/archive-plan.sh"
}
]
}
+39 -32
View File
@@ -24,20 +24,22 @@ they appear in the UI.
### General
| UI Label | Setting | Description | Default |
| ----------------------- | ---------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------- |
| Vim Mode | `general.vimMode` | Enable Vim keybindings | `false` |
| Default Approval Mode | `general.defaultApprovalMode` | The default approval mode for tool execution. 'default' prompts for approval, 'auto_edit' auto-approves edit tools, and 'plan' is read-only mode. YOLO mode (auto-approve all actions) can only be enabled via command line (--yolo or --approval-mode=yolo). | `"default"` |
| Enable Auto Update | `general.enableAutoUpdate` | Enable automatic updates. | `true` |
| Enable Notifications | `general.enableNotifications` | Enable run-event notifications for action-required prompts and session completion. | `false` |
| Enable Plan Mode | `general.plan.enabled` | Enable Plan Mode for read-only safety during planning. | `true` |
| Plan Directory | `general.plan.directory` | The directory where planning artifacts are stored. If not specified, defaults to the system temporary directory. A custom directory requires a policy to allow write access in Plan Mode. | `undefined` |
| Plan Model Routing | `general.plan.modelRouting` | Automatically switch between Pro and Flash models based on Plan Mode status. Uses Pro for the planning phase and Flash for the implementation phase. | `true` |
| Retry Fetch Errors | `general.retryFetchErrors` | Retry on "exception TypeError: fetch failed sending request" errors. | `true` |
| Max Chat Model Attempts | `general.maxAttempts` | Maximum number of attempts for requests to the main chat model. Cannot exceed 10. | `10` |
| Debug Keystroke Logging | `general.debugKeystrokeLogging` | Enable debug logging of keystrokes to the console. | `false` |
| Enable Session Cleanup | `general.sessionRetention.enabled` | Enable automatic session cleanup | `true` |
| Keep chat history | `general.sessionRetention.maxAge` | Automatically delete chats older than this time period (e.g., "30d", "7d", "24h", "1w") | `"30d"` |
| UI Label | Setting | Description | Default |
| ----------------------------- | ---------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------- |
| Vim Mode | `general.vimMode` | Enable Vim keybindings | `false` |
| Default Approval Mode | `general.defaultApprovalMode` | The default approval mode for tool execution. 'default' prompts for approval, 'auto_edit' auto-approves edit tools, and 'plan' is read-only mode. YOLO mode (auto-approve all actions) can only be enabled via command line (--yolo or --approval-mode=yolo). | `"default"` |
| Enable Auto Update | `general.enableAutoUpdate` | Enable automatic updates. | `true` |
| Enable Terminal Notifications | `general.enableNotifications` | Enable terminal run-event notifications for action-required prompts and session completion. | `false` |
| Terminal Notification Method | `general.notificationMethod` | How to send terminal notifications. | `"auto"` |
| Enable Plan Mode | `general.plan.enabled` | Enable Plan Mode for read-only safety during planning. | `true` |
| Plan Directory | `general.plan.directory` | The directory where planning artifacts are stored. If not specified, defaults to the system temporary directory. A custom directory requires a policy to allow write access in Plan Mode. | `undefined` |
| Plan Model Routing | `general.plan.modelRouting` | Automatically switch between Pro and Flash models based on Plan Mode status. Uses Pro for the planning phase and Flash for the implementation phase. | `true` |
| Retry Fetch Errors | `general.retryFetchErrors` | Retry on "exception TypeError: fetch failed sending request" errors. | `true` |
| Max Chat Model Attempts | `general.maxAttempts` | Maximum number of attempts for requests to the main chat model. Cannot exceed 10. | `10` |
| Debug Keystroke Logging | `general.debugKeystrokeLogging` | Enable debug logging of keystrokes to the console. | `false` |
| Enable Session Cleanup | `general.sessionRetention.enabled` | Enable automatic session cleanup | `true` |
| Keep chat history | `general.sessionRetention.maxAge` | Automatically delete chats older than this time period (e.g., "30d", "7d", "24h", "1w") | `"30d"` |
| Topic & Update Narration | `general.topicUpdateNarration` | Enable the Topic & Update communication model for reduced chattiness and structured progress reporting. | `true` |
### Output
@@ -97,13 +99,16 @@ they appear in the UI.
### Model
| UI Label | Setting | Description | Default |
| ----------------------------- | ---------------------------- | -------------------------------------------------------------------------------------- | ----------- |
| Model | `model.name` | The Gemini model to use for conversations. | `undefined` |
| Max Session Turns | `model.maxSessionTurns` | Maximum number of user/model/tool turns to keep in a session. -1 means unlimited. | `-1` |
| Context Compression Threshold | `model.compressionThreshold` | The fraction of context usage at which to trigger context compression (e.g. 0.2, 0.3). | `0.5` |
| Disable Loop Detection | `model.disableLoopDetection` | Disable automatic detection and prevention of infinite loops. | `false` |
| Skip Next Speaker Check | `model.skipNextSpeakerCheck` | Skip the next speaker check. | `true` |
| UI Label | Setting | Description | Default |
| --------------------------------------- | ----------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------- | ----------- |
| Model | `model.name` | The Gemini model to use for conversations. | `undefined` |
| Max Session Turns | `model.maxSessionTurns` | Maximum number of user/model/tool turns to keep in a session. -1 means unlimited. | `-1` |
| Context Compression Threshold | `model.compressionThreshold` | The fraction of context usage at which to trigger context compression (e.g. 0.2, 0.3). | `0.5` |
| Disable Loop Detection | `model.disableLoopDetection` | Disable automatic detection and prevention of infinite loops. | `false` |
| Skip Next Speaker Check | `model.skipNextSpeakerCheck` | Skip the next speaker check. | `true` |
| Best Effort Pro | `model.autoRouting.bestEffortPro` | Always prefer the Pro model unless it is unavailable (e.g., due to timeouts or quota), ignoring other routing hints. | `false` |
| Pro Timeout (Minutes) | `model.autoRouting.proTimeoutMinutes` | If a Pro request takes longer than this many minutes, it will be marked as temporarily unavailable and fallback to Flash. | `5` |
| Pro Timeout Fallback Duration (Minutes) | `model.autoRouting.proTimeoutFallbackDurationMinutes` | How long to route to Flash after Pro times out. | `60` |
### Agents
@@ -159,17 +164,19 @@ they appear in the UI.
### Experimental
| UI Label | Setting | Description | Default |
| ---------------------------------------------------- | ----------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------- | ------- |
| Enable Git Worktrees | `experimental.worktrees` | Enable automated Git worktree management for parallel work. | `false` |
| Use OSC 52 Paste | `experimental.useOSC52Paste` | Use OSC 52 for pasting. This may be more robust than the default system when using remote terminal sessions (if your terminal is configured to allow it). | `false` |
| Use OSC 52 Copy | `experimental.useOSC52Copy` | Use OSC 52 for copying. This may be more robust than the default system when using remote terminal sessions (if your terminal is configured to allow it). | `false` |
| Model Steering | `experimental.modelSteering` | Enable model steering (user hints) to guide the model during tool execution. | `false` |
| Direct Web Fetch | `experimental.directWebFetch` | Enable web fetch behavior that bypasses LLM summarization. | `false` |
| Memory Manager Agent | `experimental.memoryManager` | Replace the built-in save_memory tool with a memory manager subagent that supports adding, removing, de-duplicating, and organizing memories. | `false` |
| Use the generalist profile to manage agent contexts. | `experimental.generalistProfile` | Suitable for general coding and software development tasks. | `false` |
| Enable Context Management | `experimental.contextManagement` | Enable logic for context management. | `false` |
| Topic & Update Narration | `experimental.topicUpdateNarration` | Enable the experimental Topic & Update communication model for reduced chattiness and structured progress reporting. | `false` |
| UI Label | Setting | Description | Default |
| ---------------------------------------------------- | ----------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------- |
| Enable Git Worktrees | `experimental.worktrees` | Enable automated Git worktree management for parallel work. | `false` |
| Use OSC 52 Paste | `experimental.useOSC52Paste` | Use OSC 52 for pasting. This may be more robust than the default system when using remote terminal sessions (if your terminal is configured to allow it). | `false` |
| Use OSC 52 Copy | `experimental.useOSC52Copy` | Use OSC 52 for copying. This may be more robust than the default system when using remote terminal sessions (if your terminal is configured to allow it). | `false` |
| Model Steering | `experimental.modelSteering` | Enable model steering (user hints) to guide the model during tool execution. | `false` |
| Direct Web Fetch | `experimental.directWebFetch` | Enable web fetch behavior that bypasses LLM summarization. | `false` |
| Enable Gemma Model Router | `experimental.gemmaModelRouter.enabled` | Enable the Gemma Model Router (experimental). Requires a local endpoint serving Gemma via the Gemini API using LiteRT-LM shim. | `false` |
| Auto-start LiteRT Server | `experimental.gemmaModelRouter.autoStartServer` | Automatically start the LiteRT-LM server when Gemini CLI starts and the Gemma router is enabled. | `false` |
| Memory v2 | `experimental.memoryV2` | Disable the built-in save_memory tool and let the main agent persist project context by editing markdown files directly with edit/write_file. Route facts across four tiers: team-shared conventions go to project GEMINI.md files, project-specific personal notes go to the per-project private memory folder (MEMORY.md as index + sibling .md files for detail), and cross-project personal preferences go to the global ~/.gemini/GEMINI.md (the only file under ~/.gemini/ that the agent can edit — settings, credentials, etc. remain off-limits). Set to false to fall back to the legacy save_memory tool. | `true` |
| Auto Memory | `experimental.autoMemory` | Automatically extract reusable skills from past sessions in the background. Review results with /memory inbox. | `false` |
| Use the generalist profile to manage agent contexts. | `experimental.generalistProfile` | Suitable for general coding and software development tasks. | `false` |
| Enable Context Management | `experimental.contextManagement` | Enable logic for context management. | `false` |
### Skills
+6 -6
View File
@@ -24,7 +24,7 @@ project-specific behavior or create a customized persona.
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).
[Persisting Environment Variables](../get-started/authentication.mdx#persisting-environment-variables).
- Use the project default path (`.gemini/system.md`):
- `GEMINI_SYSTEM_MD=true` or `GEMINI_SYSTEM_MD=1`
@@ -51,7 +51,7 @@ error with: `missing system prompt file '<path>'`.
- 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`
- `GEMINI_SYSTEM_MD=~/prompts/system.md gemini`
## UI indicator
@@ -102,17 +102,17 @@ safety and workflow rules.
This creates the file and writes the current builtin system prompt to it.
## Best practices: SYSTEM.md vs GEMINI.md
## Best practices: system.md vs GEMINI.md
- SYSTEM.md (firmware):
- system.md (firmware):
- Nonnegotiable operational rules: safety, tooluse 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.
- Evolves per task; relies on system.md for safe execution.
Keep SYSTEM.md minimal but complete for safety and tool operation. Keep
Keep system.md minimal but complete for safety and tool operation. Keep
GEMINI.md focused on highlevel guidance and project specifics.
## Troubleshooting
+18 -11
View File
@@ -35,17 +35,18 @@ The observability system provides:
You control telemetry behavior through the `.gemini/settings.json` file.
Environment variables can override these settings.
| Setting | Environment Variable | Description | Values | Default |
| -------------- | -------------------------------- | --------------------------------------------------- | ----------------- | ----------------------- |
| `enabled` | `GEMINI_TELEMETRY_ENABLED` | Enable or disable telemetry | `true`/`false` | `false` |
| `target` | `GEMINI_TELEMETRY_TARGET` | Where to send telemetry data | `"gcp"`/`"local"` | `"local"` |
| `otlpEndpoint` | `GEMINI_TELEMETRY_OTLP_ENDPOINT` | OTLP collector endpoint | URL string | `http://localhost:4317` |
| `otlpProtocol` | `GEMINI_TELEMETRY_OTLP_PROTOCOL` | OTLP transport protocol | `"grpc"`/`"http"` | `"grpc"` |
| `outfile` | `GEMINI_TELEMETRY_OUTFILE` | Save telemetry to file (overrides `otlpEndpoint`) | file path | - |
| `logPrompts` | `GEMINI_TELEMETRY_LOG_PROMPTS` | Include prompts in telemetry logs | `true`/`false` | `true` |
| `useCollector` | `GEMINI_TELEMETRY_USE_COLLECTOR` | Use external OTLP collector (advanced) | `true`/`false` | `false` |
| `useCliAuth` | `GEMINI_TELEMETRY_USE_CLI_AUTH` | Use CLI credentials for telemetry (GCP target only) | `true`/`false` | `false` |
| - | `GEMINI_CLI_SURFACE` | Optional custom label for traffic reporting | string | - |
| Setting | Environment Variable | Description | Values | Default |
| -------------- | --------------------------------- | --------------------------------------------------- | ----------------- | ----------------------- |
| `enabled` | `GEMINI_TELEMETRY_ENABLED` | Enable or disable telemetry | `true`/`false` | `false` |
| `traces` | `GEMINI_TELEMETRY_TRACES_ENABLED` | Enable detailed attribute tracing | `true`/`false` | `false` |
| `target` | `GEMINI_TELEMETRY_TARGET` | Where to send telemetry data | `"gcp"`/`"local"` | `"local"` |
| `otlpEndpoint` | `GEMINI_TELEMETRY_OTLP_ENDPOINT` | OTLP collector endpoint | URL string | `http://localhost:4317` |
| `otlpProtocol` | `GEMINI_TELEMETRY_OTLP_PROTOCOL` | OTLP transport protocol | `"grpc"`/`"http"` | `"grpc"` |
| `outfile` | `GEMINI_TELEMETRY_OUTFILE` | Save telemetry to file (overrides `otlpEndpoint`) | file path | - |
| `logPrompts` | `GEMINI_TELEMETRY_LOG_PROMPTS` | Include prompts in telemetry logs | `true`/`false` | `true` |
| `useCollector` | `GEMINI_TELEMETRY_USE_COLLECTOR` | Use external OTLP collector (advanced) | `true`/`false` | `false` |
| `useCliAuth` | `GEMINI_TELEMETRY_USE_CLI_AUTH` | Use CLI credentials for telemetry (GCP target only) | `true`/`false` | `false` |
| - | `GEMINI_CLI_SURFACE` | Optional custom label for traffic reporting | string | - |
**Note on boolean environment variables:** For boolean settings like `enabled`,
setting the environment variable to `true` or `1` enables the feature.
@@ -1235,6 +1236,12 @@ These metrics follow standard [OpenTelemetry GenAI semantic conventions].
Traces provide an "under-the-hood" view of agent and backend operations. Use
traces to debug tool interactions and optimize performance.
<!-- prettier-ignore -->
> [!NOTE]
> Detailed trace attributes (like full prompts and tool outputs) are disabled by default
> to minimize overhead. You must explicitly set `telemetry.traces` to `true` (or set
> `GEMINI_TELEMETRY_TRACES_ENABLED=true`) to capture them.
Every trace captures rich metadata via standard span attributes.
<details open>
+24
View File
@@ -100,6 +100,30 @@ protect you. In this mode, the following features are disabled:
Granting trust to a folder unlocks the full functionality of Gemini CLI for that
workspace.
## Headless and automated environments
When running Gemini CLI in a headless environment (for example, a CI/CD
pipeline) where interactive prompts are not possible, the trust dialog cannot be
displayed. If the folder is untrusted and the Folder Trust feature is enabled,
the CLI will throw a `FatalUntrustedWorkspaceError` and exit.
To proceed in these environments, you can bypass the trust check using one of
the following methods:
- **Command-line flag:** Run the CLI with the `--skip-trust` flag.
- **Environment variable:** Set the `GEMINI_CLI_TRUST_WORKSPACE=true`
environment variable.
These methods will trust the current workspace for the duration of the session
without prompting.
## Overriding the trust file location
By default, trust settings are saved to `~/.gemini/trustedFolders.json`. If you
need to store this file in a different location, you can set the
`GEMINI_CLI_TRUSTED_FOLDERS_PATH` environment variable to the desired absolute
file path.
## Managing your trust settings
If you need to change a decision or see all your settings, you have a couple of
+2
View File
@@ -124,3 +124,5 @@ immediately. Force a reload with:
- Explore the [Command reference](../../reference/commands.md) for more
`/memory` options.
- Read the technical spec for [Project context](../../cli/gemini-md.md).
- Try the experimental [Auto Memory](../auto-memory.md) feature to extract
reusable skills from your past sessions automatically.
@@ -1,10 +1,11 @@
import { Tabs, TabItem } from '@astrojs/starlight/components';
# Gemini CLI authentication setup
To use Gemini CLI, you'll need to authenticate with Google. This guide helps you
quickly find the best way to sign in based on your account type and how you're
using the CLI.
<!-- prettier-ignore -->
> [!TIP]
> Looking for a high-level comparison of all available subscriptions?
> To compare features and find the right quota for your needs, see our
@@ -23,7 +24,7 @@ Select the authentication method that matches your situation in the table below:
| Organization users with a company, school, or Google Workspace account | [Sign in with Google](#login-google) | [Yes](#set-gcp) |
| AI Studio user with a Gemini API key | [Use Gemini API Key](#gemini-api) | No |
| Google Cloud Vertex AI user | [Vertex AI](#vertex-ai) | [Yes](#set-gcp) |
| [Headless mode](#headless) | [Use Gemini API Key](#gemini-api) or<br> [Vertex AI](#vertex-ai) | No (for Gemini API Key)<br> [Yes](#set-gcp) (for Vertex AI) |
| [Headless mode](#headless) | [Use Gemini API Key](#gemini-api) or<br /> [Vertex AI](#vertex-ai) | No (for Gemini API Key)<br /> [Yes](#set-gcp) (for Vertex AI) |
### What is my Google account type?
@@ -84,19 +85,24 @@ To authenticate and use Gemini CLI with a Gemini API key:
2. Set the `GEMINI_API_KEY` environment variable to your key. For example:
**macOS/Linux**
<Tabs>
<TabItem label="macOS/Linux">
```bash
# Replace YOUR_GEMINI_API_KEY with the key from AI Studio
export GEMINI_API_KEY="YOUR_GEMINI_API_KEY"
```
```bash
# Replace YOUR_GEMINI_API_KEY with the key from AI Studio
export GEMINI_API_KEY="YOUR_GEMINI_API_KEY"
```
**Windows (PowerShell)**
</TabItem>
<TabItem label="Windows (PowerShell)">
```powershell
# Replace YOUR_GEMINI_API_KEY with the key from AI Studio
$env:GEMINI_API_KEY="YOUR_GEMINI_API_KEY"
```
```powershell
# Replace YOUR_GEMINI_API_KEY with the key from AI Studio
$env:GEMINI_API_KEY="YOUR_GEMINI_API_KEY"
```
</TabItem>
</Tabs>
To make this setting persistent, see
[Persisting Environment Variables](#persisting-vars).
@@ -109,7 +115,6 @@ To authenticate and use Gemini CLI with a Gemini API key:
4. Select **Use Gemini API key**.
<!-- prettier-ignore -->
> [!WARNING]
> Treat API keys, especially for services like Gemini, as sensitive
> credentials. Protect them to prevent unauthorized access and potential misuse
@@ -131,21 +136,26 @@ or the location where you want to run your jobs.
For example:
**macOS/Linux**
<Tabs>
<TabItem label="macOS/Linux">
```bash
```bash
# Replace with your project ID and desired location (for example, us-central1)
export GOOGLE_CLOUD_PROJECT="YOUR_PROJECT_ID"
export GOOGLE_CLOUD_LOCATION="YOUR_PROJECT_LOCATION"
```
```
**Windows (PowerShell)**
</TabItem>
<TabItem label="Windows (PowerShell)">
```powershell
```powershell
# Replace with your project ID and desired location (for example, us-central1)
$env:GOOGLE_CLOUD_PROJECT="YOUR_PROJECT_ID"
$env:GOOGLE_CLOUD_LOCATION="YOUR_PROJECT_LOCATION"
```
```
</TabItem>
</Tabs>
To make any Vertex AI environment variable settings persistent, see
[Persisting Environment Variables](#persisting-vars).
@@ -157,17 +167,22 @@ Consider this authentication method if you have Google Cloud CLI installed.
If you have previously set `GOOGLE_API_KEY` or `GEMINI_API_KEY`, you must unset
them to use ADC.
**macOS/Linux**
<Tabs>
<TabItem label="macOS/Linux">
```bash
```bash
unset GOOGLE_API_KEY GEMINI_API_KEY
```
```
**Windows (PowerShell)**
</TabItem>
<TabItem label="Windows (PowerShell)">
```powershell
```powershell
Remove-Item Env:\GOOGLE_API_KEY, Env:\GEMINI_API_KEY -ErrorAction Ignore
```
```
</TabItem>
</Tabs>
1. Verify you have a Google Cloud project and Vertex AI API is enabled.
@@ -195,17 +210,22 @@ pipelines, or if your organization restricts user-based ADC or API key creation.
If you have previously set `GOOGLE_API_KEY` or `GEMINI_API_KEY`, you must unset
them:
**macOS/Linux**
<Tabs>
<TabItem label="macOS/Linux">
```bash
```bash
unset GOOGLE_API_KEY GEMINI_API_KEY
```
```
**Windows (PowerShell)**
</TabItem>
<TabItem label="Windows (PowerShell)">
```powershell
```powershell
Remove-Item Env:\GOOGLE_API_KEY, Env:\GEMINI_API_KEY -ErrorAction Ignore
```
```
</TabItem>
</Tabs>
1. [Create a service account and key](https://cloud.google.com/iam/docs/keys-create-delete)
and download the provided JSON file. Assign the "Vertex AI User" role to the
@@ -214,19 +234,24 @@ Remove-Item Env:\GOOGLE_API_KEY, Env:\GEMINI_API_KEY -ErrorAction Ignore
2. Set the `GOOGLE_APPLICATION_CREDENTIALS` environment variable to the JSON
file's absolute path. For example:
**macOS/Linux**
<Tabs>
<TabItem label="macOS/Linux">
```bash
# Replace /path/to/your/keyfile.json with the actual path
export GOOGLE_APPLICATION_CREDENTIALS="/path/to/your/keyfile.json"
```
```bash
# Replace /path/to/your/keyfile.json with the actual path
export GOOGLE_APPLICATION_CREDENTIALS="/path/to/your/keyfile.json"
```
**Windows (PowerShell)**
</TabItem>
<TabItem label="Windows (PowerShell)">
```powershell
# Replace C:\path\to\your\keyfile.json with the actual path
$env:GOOGLE_APPLICATION_CREDENTIALS="C:\path\to\your\keyfile.json"
```
```powershell
# Replace C:\path\to\your\keyfile.json with the actual path
$env:GOOGLE_APPLICATION_CREDENTIALS="C:\path\to\your\keyfile.json"
```
</TabItem>
</Tabs>
3. [Configure your Google Cloud Project](#set-gcp).
@@ -238,7 +263,6 @@ Remove-Item Env:\GOOGLE_API_KEY, Env:\GEMINI_API_KEY -ErrorAction Ignore
5. Select **Vertex AI**.
<!-- prettier-ignore -->
> [!WARNING]
> Protect your service account key file as it gives access to
> your resources.
@@ -250,19 +274,24 @@ Remove-Item Env:\GOOGLE_API_KEY, Env:\GEMINI_API_KEY -ErrorAction Ignore
2. Set the `GOOGLE_API_KEY` environment variable:
**macOS/Linux**
<Tabs>
<TabItem label="macOS/Linux">
```bash
# Replace YOUR_GOOGLE_API_KEY with your Vertex AI API key
export GOOGLE_API_KEY="YOUR_GOOGLE_API_KEY"
```
```bash
# Replace YOUR_GOOGLE_API_KEY with your Vertex AI API key
export GOOGLE_API_KEY="YOUR_GOOGLE_API_KEY"
```
**Windows (PowerShell)**
</TabItem>
<TabItem label="Windows (PowerShell)">
```powershell
# Replace YOUR_GOOGLE_API_KEY with your Vertex AI API key
$env:GOOGLE_API_KEY="YOUR_GOOGLE_API_KEY"
```
```powershell
# Replace YOUR_GOOGLE_API_KEY with your Vertex AI API key
$env:GOOGLE_API_KEY="YOUR_GOOGLE_API_KEY"
```
</TabItem>
</Tabs>
If you see errors like `"API keys are not supported by this API..."`, your
organization might restrict API key usage for this service. Try the other
@@ -280,7 +309,6 @@ Remove-Item Env:\GOOGLE_API_KEY, Env:\GEMINI_API_KEY -ErrorAction Ignore
## Set your Google Cloud project <a id="set-gcp"></a>
<!-- prettier-ignore -->
> [!IMPORTANT]
> Most individual Google accounts (free and paid) don't require a
> Google Cloud project for authentication.
@@ -308,19 +336,24 @@ To configure Gemini CLI to use a Google Cloud project, do the following:
For example, to set the `GOOGLE_CLOUD_PROJECT_ID` variable:
**macOS/Linux**
<Tabs>
<TabItem label="macOS/Linux">
```bash
# Replace YOUR_PROJECT_ID with your actual Google Cloud project ID
export GOOGLE_CLOUD_PROJECT="YOUR_PROJECT_ID"
```
```bash
# Replace YOUR_PROJECT_ID with your actual Google Cloud project ID
export GOOGLE_CLOUD_PROJECT="YOUR_PROJECT_ID"
```
**Windows (PowerShell)**
</TabItem>
<TabItem label="Windows (PowerShell)">
```powershell
# Replace YOUR_PROJECT_ID with your actual Google Cloud project ID
$env:GOOGLE_CLOUD_PROJECT="YOUR_PROJECT_ID"
```
```powershell
# Replace YOUR_PROJECT_ID with your actual Google Cloud project ID
$env:GOOGLE_CLOUD_PROJECT="YOUR_PROJECT_ID"
```
</TabItem>
</Tabs>
To make this setting persistent, see
[Persisting Environment Variables](#persisting-vars).
@@ -333,21 +366,29 @@ persist them with the following methods:
1. **Add your environment variables to your shell configuration file:** Append
the environment variable commands to your shell's startup file.
**macOS/Linux** (for example, `~/.bashrc`, `~/.zshrc`, or `~/.profile`):
<Tabs>
<TabItem label="macOS/Linux">
```bash
echo 'export GOOGLE_CLOUD_PROJECT="YOUR_PROJECT_ID"' >> ~/.bashrc
source ~/.bashrc
```
(for example, `~/.bashrc`, `~/.zshrc`, or `~/.profile`):
**Windows (PowerShell)** (for example, `$PROFILE`):
```bash
echo 'export GOOGLE_CLOUD_PROJECT="YOUR_PROJECT_ID"' >> ~/.bashrc
source ~/.bashrc
```
```powershell
Add-Content -Path $PROFILE -Value '$env:GOOGLE_CLOUD_PROJECT="YOUR_PROJECT_ID"'
. $PROFILE
```
</TabItem>
<TabItem label="Windows (PowerShell)">
(for example, `$PROFILE`):
```powershell
Add-Content -Path $PROFILE -Value '$env:GOOGLE_CLOUD_PROJECT="YOUR_PROJECT_ID"'
. $PROFILE
```
</TabItem>
</Tabs>
<!-- prettier-ignore -->
> [!WARNING]
> Be aware that when you export API keys or service account
> paths in your shell configuration file, any process launched from that
@@ -361,25 +402,30 @@ persist them with the following methods:
Example for user-wide settings:
**macOS/Linux**
<Tabs>
<TabItem label="macOS/Linux">
```bash
mkdir -p ~/.gemini
cat >> ~/.gemini/.env <<'EOF'
GOOGLE_CLOUD_PROJECT="your-project-id"
# Add other variables like GEMINI_API_KEY as needed
EOF
```
```bash
mkdir -p ~/.gemini
cat >> ~/.gemini/.env <<'EOF'
GOOGLE_CLOUD_PROJECT="your-project-id"
# Add other variables like GEMINI_API_KEY as needed
EOF
```
**Windows (PowerShell)**
</TabItem>
<TabItem label="Windows (PowerShell)">
```powershell
New-Item -ItemType Directory -Force -Path "$env:USERPROFILE\.gemini"
@"
GOOGLE_CLOUD_PROJECT="your-project-id"
# Add other variables like GEMINI_API_KEY as needed
"@ | Out-File -FilePath "$env:USERPROFILE\.gemini\.env" -Encoding utf8 -Append
```
```powershell
New-Item -ItemType Directory -Force -Path "$env:USERPROFILE\.gemini"
@"
GOOGLE_CLOUD_PROJECT="your-project-id"
# Add other variables like GEMINI_API_KEY as needed
"@ | Out-File -FilePath "$env:USERPROFILE\.gemini\.env" -Encoding utf8 -Append
```
</TabItem>
</Tabs>
Variables are loaded from the first file found, not merged.
+3 -2
View File
@@ -24,7 +24,8 @@ Once Gemini CLI is installed, run Gemini CLI from your command line:
gemini
```
For more installation options, see [Gemini CLI Installation](./installation.md).
For more installation options, see
[Gemini CLI Installation](./installation.mdx).
## Authenticate
@@ -46,7 +47,7 @@ cases, you can log in with your existing Google account:
Certain account types may require you to configure a Google Cloud project. For
more information, including other authentication methods, see
[Gemini CLI Authentication Setup](./authentication.md).
[Gemini CLI Authentication Setup](./authentication.mdx).
## Configure
-181
View File
@@ -1,181 +0,0 @@
# Gemini CLI installation, execution, and releases
This document provides an overview of Gemini CLI's system requirements,
installation methods, and release types.
## Recommended system specifications
- **Operating System:**
- macOS 15+
- Windows 11 24H2+
- Ubuntu 20.04+
- **Hardware:**
- "Casual" usage: 4GB+ RAM (short sessions, common tasks and edits)
- "Power" usage: 16GB+ RAM (long sessions, large codebases, deep context)
- **Runtime:** Node.js 20.0.0+
- **Shell:** Bash, Zsh, or PowerShell
- **Location:**
[Gemini Code Assist supported locations](https://developers.google.com/gemini-code-assist/resources/available-locations#americas)
- **Internet connection required**
## Install Gemini CLI
We recommend most users install Gemini CLI using one of the following
installation methods:
- npm
- Homebrew
- MacPorts
- Anaconda
Note that Gemini CLI comes pre-installed on
[**Cloud Shell**](https://docs.cloud.google.com/shell/docs) and
[**Cloud Workstations**](https://cloud.google.com/workstations).
### Install globally with npm
```bash
npm install -g @google/gemini-cli
```
### Install globally with Homebrew (macOS/Linux)
```bash
brew install gemini-cli
```
### Install globally with MacPorts (macOS)
```bash
sudo port install gemini-cli
```
### Install with Anaconda (for restricted environments)
```bash
# Create and activate a new environment
conda create -y -n gemini_env -c conda-forge nodejs
conda activate gemini_env
# Install Gemini CLI globally via npm (inside the environment)
npm install -g @google/gemini-cli
```
## Run Gemini CLI
For most users, we recommend running Gemini CLI with the `gemini` command:
```bash
gemini
```
For a list of options and additional commands, see the
[CLI cheatsheet](../cli/cli-reference.md).
You can also run Gemini CLI using one of the following advanced methods:
- Run instantly with npx. You can run Gemini CLI without permanent installation.
- In a sandbox. This method offers increased security and isolation.
- From the source. This is recommended for contributors to the project.
### Run instantly with npx
```bash
# Using npx (no installation required)
npx @google/gemini-cli
```
You can also execute the CLI directly from the main branch on GitHub, which is
helpful for testing features still in development:
```bash
npx https://github.com/google-gemini/gemini-cli
```
### Run in a sandbox (Docker/Podman)
For security and isolation, Gemini CLI can be run inside a container. This is
the default way that the CLI executes tools that might have side effects.
- **Directly from the registry:** You can run the published sandbox image
directly. This is useful for environments where you only have Docker and want
to run the CLI.
```bash
# Run the published sandbox image
docker run --rm -it us-docker.pkg.dev/gemini-code-dev/gemini-cli/sandbox:0.1.1
```
- **Using the `--sandbox` flag:** If you have Gemini CLI installed locally
(using the standard installation described above), you can instruct it to run
inside the sandbox container.
```bash
gemini --sandbox -y -p "your prompt here"
```
### Run from source (recommended for Gemini CLI contributors)
Contributors to the project will want to run the CLI directly from the source
code.
- **Development mode:** This method provides hot-reloading and is useful for
active development.
```bash
# From the root of the repository
npm run start
```
- **Production mode (React optimizations):** This method runs the CLI with React
production mode enabled, which is useful for testing performance without
development overhead.
```bash
# From the root of the repository
npm run start:prod
```
- **Production-like mode (linked package):** This method simulates a global
installation by linking your local package. It's useful for testing a local
build in a production workflow.
```bash
# Link the local cli package to your global node_modules
npm link packages/cli
# Now you can run your local version using the `gemini` command
gemini
```
## Releases
Gemini CLI has three release channels: nightly, preview, and stable. For most
users, we recommend the stable release, which is the default installation.
### Stable
New stable releases are published each week. The stable release is the promotion
of last week's `preview` release along with any bug fixes. The stable release
uses `latest` tag, but omitting the tag also installs the latest stable release
by default:
```bash
# Both commands install the latest stable release.
npm install -g @google/gemini-cli
npm install -g @google/gemini-cli@latest
```
### Preview
New preview releases will be published each week. These releases are not fully
vetted and may contain regressions or other outstanding issues. Try out the
preview release by using the `preview` tag:
```bash
npm install -g @google/gemini-cli@preview
```
### Nightly
Nightly releases are published every day. The nightly release includes all
changes from the main branch at time of release. It should be assumed there are
pending validations and issues. You can help test the latest changes by
installing with the `nightly` tag:
```bash
npm install -g @google/gemini-cli@nightly
```
+201
View File
@@ -0,0 +1,201 @@
import { Tabs, TabItem } from '@astrojs/starlight/components';
# Gemini CLI installation, execution, and releases
This document provides an overview of Gemini CLI's system requirements,
installation methods, and release types.
## Recommended system specifications
- **Operating System:**
- macOS 15+
- Windows 11 24H2+
- Ubuntu 20.04+
- **Hardware:**
- "Casual" usage: 4GB+ RAM (short sessions, common tasks and edits)
- "Power" usage: 16GB+ RAM (long sessions, large codebases, deep context)
- **Runtime:** Node.js 20.0.0+
- **Shell:** Bash, Zsh, or PowerShell
- **Location:**
[Gemini Code Assist supported locations](https://developers.google.com/gemini-code-assist/resources/available-locations#americas)
- **Internet connection required**
## Install Gemini CLI
We recommend most users install Gemini CLI using one of the following
installation methods. Note that Gemini CLI comes pre-installed on
[**Cloud Shell**](https://docs.cloud.google.com/shell/docs) and
[**Cloud Workstations**](https://cloud.google.com/workstations).
<Tabs>
<TabItem label="npm">
Install globally with npm:
```bash
npm install -g @google/gemini-cli
```
</TabItem>
<TabItem label="Homebrew (macOS/Linux)">
Install globally with Homebrew:
```bash
brew install gemini-cli
```
</TabItem>
<TabItem label="MacPorts (macOS)">
Install globally with MacPorts:
```bash
sudo port install gemini-cli
```
</TabItem>
<TabItem label="Anaconda">
Install with Anaconda (for restricted environments):
```bash
# Create and activate a new environment
conda create -y -n gemini_env -c conda-forge nodejs
conda activate gemini_env
# Install Gemini CLI globally via npm (inside the environment)
npm install -g @google/gemini-cli
```
</TabItem>
</Tabs>
## Run Gemini CLI
For most users, we recommend running Gemini CLI with the `gemini` command:
```bash
gemini
```
For a list of options and additional commands, see the
[CLI cheatsheet](../cli/cli-reference.md).
You can also run Gemini CLI using one of the following advanced methods:
<Tabs>
<TabItem label="npx">
Run instantly with npx. You can run Gemini CLI without permanent installation.
```bash
# Using npx (no installation required)
npx @google/gemini-cli
```
You can also execute the CLI directly from the main branch on GitHub, which is
helpful for testing features still in development:
```bash
npx https://github.com/google-gemini/gemini-cli
```
</TabItem>
<TabItem label="Docker/Podman Sandbox">
For security and isolation, Gemini CLI can be run inside a container. This is
the default way that the CLI executes tools that might have side effects.
- **Directly from the registry:** You can run the published sandbox image
directly. This is useful for environments where you only have Docker and want
to run the CLI.
```bash
# Run the published sandbox image
docker run --rm -it us-docker.pkg.dev/gemini-code-dev/gemini-cli/sandbox:0.1.1
```
- **Using the `--sandbox` flag:** If you have Gemini CLI installed locally
(using the standard installation described above), you can instruct it to run
inside the sandbox container.
```bash
gemini --sandbox -y -p "your prompt here"
```
</TabItem>
<TabItem label="From source">
Contributors to the project will want to run the CLI directly from the source
code.
- **Development mode:** This method provides hot-reloading and is useful for
active development.
```bash
# From the root of the repository
npm run start
```
- **Production mode (React optimizations):** This method runs the CLI with React
production mode enabled, which is useful for testing performance without
development overhead.
```bash
# From the root of the repository
npm run start:prod
```
- **Production-like mode (linked package):** This method simulates a global
installation by linking your local package. It's useful for testing a local
build in a production workflow.
```bash
# Link the local cli package to your global node_modules
npm link packages/cli
# Now you can run your local version using the `gemini` command
gemini
```
</TabItem>
</Tabs>
## Releases
Gemini CLI has three release channels: stable, preview, and nightly. For most
users, we recommend the stable release, which is the default installation.
<Tabs>
<TabItem label="Stable">
Stable releases are published each week. A stable release is created from the
previous week's preview release along with any bug fixes. The stable release
uses the `latest` tag. Omitting the tag also installs the latest stable
release by default.
```bash
# Both commands install the latest stable release.
npm install -g @google/gemini-cli
npm install -g @google/gemini-cli@latest
```
</TabItem>
<TabItem label="Preview">
New preview releases will be published each week. These releases are not fully
vetted and may contain regressions or other outstanding issues. Try out the
preview release by using the `preview` tag:
```bash
npm install -g @google/gemini-cli@preview
```
</TabItem>
<TabItem label="Nightly">
Nightly releases are published every day. The nightly release includes all
changes from the main branch at time of release. It should be assumed there are
pending validations and issues. You can help test the latest changes by
installing with the `nightly` tag:
```bash
npm install -g @google/gemini-cli@nightly
```
</TabItem>
</Tabs>
+2 -2
View File
@@ -15,9 +15,9 @@ npm install -g @google/gemini-cli
Jump in to Gemini CLI.
- **[Quickstart](./get-started/index.md):** Your first session with Gemini CLI.
- **[Installation](./get-started/installation.md):** How to install Gemini CLI
- **[Installation](./get-started/installation.mdx):** How to install Gemini CLI
on your system.
- **[Authentication](./get-started/authentication.md):** Setup instructions for
- **[Authentication](./get-started/authentication.mdx):** Setup instructions for
personal and enterprise accounts.
- **[CLI cheatsheet](./cli/cli-reference.md):** A quick reference for common
commands and options.
+95 -12
View File
@@ -134,10 +134,15 @@ their corresponding top-level category object in your `settings.json` file.
- **Default:** `true`
- **`general.enableNotifications`** (boolean):
- **Description:** Enable run-event notifications for action-required prompts
and session completion.
- **Description:** Enable terminal run-event notifications for action-required
prompts and session completion.
- **Default:** `false`
- **`general.notificationMethod`** (enum):
- **Description:** How to send terminal notifications.
- **Default:** `"auto"`
- **Values:** `"auto"`, `"osc9"`, `"osc777"`, `"bell"`
- **`general.checkpointing.enabled`** (boolean):
- **Description:** Enable session checkpointing for recovery
- **Default:** `false`
@@ -193,6 +198,11 @@ their corresponding top-level category object in your `settings.json` file.
- **Description:** Minimum retention period (safety limit, defaults to "1d")
- **Default:** `"1d"`
- **`general.topicUpdateNarration`** (boolean):
- **Description:** Enable the Topic & Update communication model for reduced
chattiness and structured progress reporting.
- **Default:** `true`
#### `output`
- **`output.format`** (enum):
@@ -426,6 +436,20 @@ their corresponding top-level category object in your `settings.json` file.
- **Default:** `"ask"`
- **Values:** `"ask"`, `"always"`, `"never"`
- **`billing.vertexAi.requestType`** (enum):
- **Description:** Sets the X-Vertex-AI-LLM-Request-Type header for Vertex AI
requests.
- **Default:** `undefined`
- **Values:** `"dedicated"`, `"shared"`
- **Requires restart:** Yes
- **`billing.vertexAi.sharedRequestType`** (enum):
- **Description:** Sets the X-Vertex-AI-LLM-Shared-Request-Type header for
Vertex AI requests.
- **Default:** `undefined`
- **Values:** `"priority"`, `"flex"`
- **Requires restart:** Yes
#### `model`
- **`model.name`** (string):
@@ -459,6 +483,20 @@ their corresponding top-level category object in your `settings.json` file.
- **Description:** Skip the next speaker check.
- **Default:** `true`
- **`model.autoRouting.bestEffortPro`** (boolean):
- **Description:** Always prefer the Pro model unless it is unavailable (e.g.,
due to timeouts or quota), ignoring other routing hints.
- **Default:** `false`
- **`model.autoRouting.proTimeoutMinutes`** (number):
- **Description:** If a Pro request takes longer than this many minutes, it
will be marked as temporarily unavailable and fallback to Flash.
- **Default:** `5`
- **`model.autoRouting.proTimeoutFallbackDurationMinutes`** (number):
- **Description:** How long to route to Flash after Pro times out.
- **Default:** `60`
#### `modelConfigs`
- **`modelConfigs.aliases`** (object):
@@ -1349,6 +1387,12 @@ their corresponding top-level category object in your `settings.json` file.
- **Default:** `true`
- **Requires restart:** Yes
- **`context.fileFiltering.enableFileWatcher`** (boolean):
- **Description:** Enable file watcher updates for @ file suggestions
(experimental).
- **Default:** `false`
- **Requires restart:** Yes
- **`context.fileFiltering.enableRecursiveFileSearch`** (boolean):
- **Description:** Enable recursive file search functionality when completing
@ references in the prompt.
@@ -1658,8 +1702,10 @@ their corresponding top-level category object in your `settings.json` file.
- **Requires restart:** Yes
- **`experimental.jitContext`** (boolean):
- **Description:** Enable Just-In-Time (JIT) context loading.
- **Default:** `false`
- **Description:** Enable Just-In-Time (JIT) context loading. Defaults to
true; set to false to opt out and load all GEMINI.md files into the system
instruction up-front.
- **Default:** `true`
- **Requires restart:** Yes
- **`experimental.useOSC52Paste`** (boolean):
@@ -1701,6 +1747,18 @@ their corresponding top-level category object in your `settings.json` file.
- **Default:** `false`
- **Requires restart:** Yes
- **`experimental.gemmaModelRouter.autoStartServer`** (boolean):
- **Description:** Automatically start the LiteRT-LM server when Gemini CLI
starts and the Gemma router is enabled.
- **Default:** `false`
- **Requires restart:** Yes
- **`experimental.gemmaModelRouter.binaryPath`** (string):
- **Description:** Custom path to the LiteRT-LM binary. Leave empty to use the
default location (~/.gemini/bin/litert/).
- **Default:** `""`
- **Requires restart:** Yes
- **`experimental.gemmaModelRouter.classifier.host`** (string):
- **Description:** The host of the classifier.
- **Default:** `"http://localhost:9379"`
@@ -1712,10 +1770,22 @@ their corresponding top-level category object in your `settings.json` file.
- **Default:** `"gemma3-1b-gpu-custom"`
- **Requires restart:** Yes
- **`experimental.memoryManager`** (boolean):
- **Description:** Replace the built-in save_memory tool with a memory manager
subagent that supports adding, removing, de-duplicating, and organizing
memories.
- **`experimental.memoryV2`** (boolean):
- **Description:** Disable the built-in save_memory tool and let the main
agent persist project context by editing markdown files directly with
edit/write_file. Route facts across four tiers: team-shared conventions go
to project GEMINI.md files, project-specific personal notes go to the
per-project private memory folder (MEMORY.md as index + sibling .md files
for detail), and cross-project personal preferences go to the global
~/.gemini/GEMINI.md (the only file under ~/.gemini/ that the agent can edit
— settings, credentials, etc. remain off-limits). Set to false to fall back
to the legacy save_memory tool.
- **Default:** `true`
- **Requires restart:** Yes
- **`experimental.autoMemory`** (boolean):
- **Description:** Automatically extract reusable skills from past sessions in
the background. Review results with /memory inbox.
- **Default:** `false`
- **Requires restart:** Yes
@@ -1730,8 +1800,7 @@ their corresponding top-level category object in your `settings.json` file.
- **Requires restart:** Yes
- **`experimental.topicUpdateNarration`** (boolean):
- **Description:** Enable the experimental Topic & Update communication model
for reduced chattiness and structured progress reporting.
- **Description:** Deprecated: Use general.topicUpdateNarration instead.
- **Default:** `false`
#### `skills`
@@ -1971,6 +2040,8 @@ see [Telemetry](../cli/telemetry.md).
- **Properties:**
- **`enabled`** (boolean): Whether or not telemetry is enabled.
- **`traces`** (boolean): Whether detailed traces with large attributes (like
tool outputs and file reads) are captured. Defaults to `false`.
- **`target`** (string): The destination for collected telemetry. Supported
values are `local` and `gcp`.
- **`otlpEndpoint`** (string): The endpoint for the OTLP Exporter.
@@ -2070,7 +2141,7 @@ within your user's home folder.
Environment variables are a common way to configure applications, especially for
sensitive information like API keys or for settings that might change between
environments. For authentication setup, see the
[Authentication documentation](../get-started/authentication.md) which covers
[Authentication documentation](../get-started/authentication.mdx) which covers
all available authentication methods.
The CLI automatically loads environment variables from an `.env` file. The
@@ -2091,7 +2162,7 @@ the `advanced.excludedEnvVars` setting in your `settings.json` file.
- **`GEMINI_API_KEY`**:
- Your API key for the Gemini API.
- One of several available
[authentication methods](../get-started/authentication.md).
[authentication methods](../get-started/authentication.mdx).
- Set this in your shell profile (for example, `~/.bashrc`, `~/.zshrc`) or an
`.env` file.
- **`GEMINI_MODEL`**:
@@ -2099,6 +2170,14 @@ the `advanced.excludedEnvVars` setting in your `settings.json` file.
- Overrides the hardcoded default
- Example: `export GEMINI_MODEL="gemini-3-flash-preview"` (Windows PowerShell:
`$env:GEMINI_MODEL="gemini-3-flash-preview"`)
- **`GEMINI_CLI_TRUST_WORKSPACE`**:
- If set to `"true"`, trusts the current workspace for the duration of the
session, bypassing the folder trust check.
- Useful for headless environments (for example, CI/CD pipelines).
- **`GEMINI_CLI_TRUSTED_FOLDERS_PATH`**:
- Overrides the default location for the `trustedFolders.json` file.
- Useful if you want to store this configuration in a custom location instead
of the default `~/.gemini/`.
- **`GEMINI_CLI_IDE_PID`**:
- Manually specifies the PID of the IDE process to use for integration. This
is useful when running Gemini CLI in a standalone terminal while still
@@ -2171,6 +2250,10 @@ the `advanced.excludedEnvVars` setting in your `settings.json` file.
- Set to `true` or `1` to enable telemetry. Any other value is treated as
disabling it.
- Overrides the `telemetry.enabled` setting.
- **`GEMINI_TELEMETRY_TRACES_ENABLED`**:
- Set to `true` or `1` to enable detailed tracing with large attributes. Any
other value is treated as disabling it.
- Overrides the `telemetry.traces` setting.
- **`GEMINI_TELEMETRY_TARGET`**:
- Sets the telemetry target (`local` or `gcp`).
- Overrides the `telemetry.target` setting.
+18 -12
View File
@@ -120,6 +120,12 @@ There are three possible decisions a rule can enforce:
### Priority system and tiers
> [!WARNING] The **Workspace** tier (project-level policies) is currently
> non-functional. Defining policies in a workspace's `.gemini/policies`
> directory will not have any effect. See
> [issue #18186](https://github.com/google-gemini/gemini-cli/issues/18186). Use
> User or Admin policies instead.
The policy engine uses a sophisticated priority system to resolve conflicts when
multiple rules match a single tool call. The core principle is simple: **the
rule with the highest priority wins**.
@@ -127,13 +133,13 @@ rule with the highest priority wins**.
To provide a clear hierarchy, policies are organized into three tiers. Each tier
has a designated number that forms the base of the final priority calculation.
| Tier | Base | Description |
| :-------- | :--- | :-------------------------------------------------------------------------------- |
| Default | 1 | Built-in policies that ship with Gemini CLI. |
| Extension | 2 | Policies defined in extensions. |
| Workspace | 3 | Policies defined in the current workspace's configuration directory. |
| User | 4 | Custom policies defined by the user. |
| Admin | 5 | Policies managed by an administrator (for example, in an enterprise environment). |
| Tier | Base | Description |
| :-------- | :--- | :-------------------------------------------------------------------------------------------- |
| Default | 1 | Built-in policies that ship with Gemini CLI. |
| Extension | 2 | Policies defined in extensions. |
| Workspace | 3 | **(Currently disabled)** Policies defined in the current workspace's configuration directory. |
| User | 4 | Custom policies defined by the user. |
| Admin | 5 | Policies managed by an administrator (for example, in an enterprise environment). |
Within a TOML policy file, you assign a priority value from **0 to 999**. The
engine transforms this into a final priority using the following formula:
@@ -214,11 +220,11 @@ User, and (if configured) Admin directories.
### Policy locations
| Tier | Type | Location |
| :------------ | :----- | :---------------------------------------- |
| **User** | Custom | `~/.gemini/policies/*.toml` |
| **Workspace** | Custom | `$WORKSPACE_ROOT/.gemini/policies/*.toml` |
| **Admin** | System | _See below (OS specific)_ |
| Tier | Type | Location |
| :------------ | :----- | :------------------------------------------------------- |
| **User** | Custom | `~/.gemini/policies/*.toml` |
| **Workspace** | Custom | **(Disabled)** `$WORKSPACE_ROOT/.gemini/policies/*.toml` |
| **Admin** | System | _See below (OS specific)_ |
#### System-wide policies (Admin)
+22
View File
@@ -92,6 +92,28 @@ each tool.
| [`ask_user`](../tools/ask-user.md) | `Communicate` | Requests clarification or missing information via an interactive dialog. |
| [`write_todos`](../tools/todos.md) | `Other` | Maintains an internal list of subtasks. The model uses this to track its own progress. |
### Task Tracker (Experimental)
<!-- prettier-ignore -->
> [!NOTE]
> This is an experimental feature currently under active development. Enable via `experimental.taskTracker`.
| Tool | Kind | Description |
| :---------------------------------------------- | :------ | :-------------------------------------------------------------------------- |
| [`tracker_create_task`](../tools/tracker.md) | `Other` | Creates a new task in the experimental tracker. |
| [`tracker_update_task`](../tools/tracker.md) | `Other` | Updates an existing task's status, description, or dependencies. |
| [`tracker_get_task`](../tools/tracker.md) | `Other` | Retrieves the full details of a specific task. |
| [`tracker_list_tasks`](../tools/tracker.md) | `Other` | Lists tasks in the tracker, optionally filtered by status, type, or parent. |
| [`tracker_add_dependency`](../tools/tracker.md) | `Other` | Adds a dependency between two tasks, ensuring topological execution. |
| [`tracker_visualize`](../tools/tracker.md) | `Other` | Renders an ASCII tree visualization of the current task graph. |
### MCP
| Tool | Kind | Description |
| :------------------------------------------------ | :------- | :--------------------------------------------------------------------- |
| [`list_mcp_resources`](../tools/mcp-resources.md) | `Search` | Lists all available resources exposed by connected MCP servers. |
| [`read_mcp_resource`](../tools/mcp-resources.md) | `Read` | Reads the content of a specific Model Context Protocol (MCP) resource. |
### Memory
| Tool | Kind | Description |
+13 -1
View File
@@ -96,6 +96,11 @@
]
},
{ "label": "Agent Skills", "slug": "docs/cli/skills" },
{
"label": "Auto Memory",
"badge": "🔬",
"slug": "docs/cli/auto-memory"
},
{ "label": "Checkpointing", "slug": "docs/cli/checkpointing" },
{ "label": "Headless mode", "slug": "docs/cli/headless" },
{
@@ -122,7 +127,14 @@
}
]
},
{ "label": "MCP servers", "slug": "docs/tools/mcp-server" },
{
"label": "MCP servers",
"collapsed": true,
"items": [
{ "label": "Overview", "slug": "docs/tools/mcp-server" },
{ "label": "Resource tools", "slug": "docs/tools/mcp-resources" }
]
},
{ "label": "Model routing", "slug": "docs/cli/model-routing" },
{ "label": "Model selection", "slug": "docs/cli/model" },
{
+44
View File
@@ -0,0 +1,44 @@
# MCP resource tools
MCP resource tools let Gemini CLI discover and retrieve data from contextual
resources exposed by Model Context Protocol (MCP) servers.
## 1. `list_mcp_resources` (ListMcpResources)
`list_mcp_resources` retrieves a list of all available resources from connected
MCP servers. This is primarily a discovery tool that helps the model understand
what external data sources are available for reference.
- **Tool name:** `list_mcp_resources`
- **Display name:** List MCP Resources
- **Kind:** `Search`
- **File:** `list-mcp-resources.ts`
- **Parameters:**
- `serverName` (string, optional): An optional filter to list resources from a
specific server.
- **Behavior:**
- Iterates through all connected MCP servers.
- Fetches the list of resources each server exposes.
- Formats the results into a plain-text list of URIs and descriptions.
- **Output (`llmContent`):** A formatted list of available resources, including
their URI, server name, and optional description.
- **Confirmation:** No. This is a read-only discovery tool.
## 2. `read_mcp_resource` (ReadMcpResource)
`read_mcp_resource` retrieves the content of a specific resource identified by
its URI.
- **Tool name:** `read_mcp_resource`
- **Display name:** Read MCP Resource
- **Kind:** `Read`
- **File:** `read-mcp-resource.ts`
- **Parameters:**
- `uri` (string, required): The URI of the MCP resource to read.
- **Behavior:**
- Locates the resource and its associated server by URI.
- Calls the server's `resources/read` method.
- Processes the response, extracting text or binary data.
- **Output (`llmContent`):** The content of the resource. For binary data, it
returns a placeholder indicating the data type.
- **Confirmation:** No. This is a read-only retrieval tool.
+2 -1
View File
@@ -64,7 +64,8 @@ Gemini CLI supports three MCP transport types:
Some MCP servers expose contextual “resources” in addition to the tools and
prompts. Gemini CLI discovers these automatically and gives you the possibility
to reference them in the chat.
to reference them in the chat. For more information on the tools used to
interact with these resources, see [MCP resource tools](mcp-resources.md).
### Discovery and listing
+61
View File
@@ -0,0 +1,61 @@
# Tracker tools (`tracker_*`)
<!-- prettier-ignore -->
> [!NOTE]
> This is an experimental feature currently under active development.
The `tracker_*` tools allow the Gemini agent to maintain an internal, persistent
graph of tasks and dependencies for multi-step requests. This suite of tools
provides a more robust and granular way to manage execution plans than the
legacy `write_todos` tool.
## Technical reference
The agent uses these tools to manage its execution plan, decompose complex goals
into actionable sub-tasks, and provide real-time progress updates to the CLI
interface. The task state is stored in the `.gemini/tmp/tracker/<session-id>`
directory, allowing the agent to manage its plan for the current session.
### Available Tools
- `tracker_create_task`: Creates a new task in the tracker. You can specify a
title, description, and task type (`epic`, `task`, `bug`).
- `tracker_update_task`: Updates an existing task's status (`open`,
`in_progress`, `blocked`, `closed`), description, or dependencies.
- `tracker_get_task`: Retrieves the full details of a specific task by its
6-character hex ID.
- `tracker_list_tasks`: Lists tasks in the tracker, optionally filtered by
status, type, or parent ID.
- `tracker_add_dependency`: Adds a dependency between two tasks, ensuring
topological execution.
- `tracker_visualize`: Renders an ASCII tree visualization of the current task
graph.
## Technical behavior
- **Interface:** Updates the progress indicator and task tree above the CLI
input prompt.
- **Persistence:** Task state is saved automatically to the
`.gemini/tmp/tracker/<session-id>` directory. Task states are session-specific
and do not persist across different sessions.
- **Dependencies:** Tasks can depend on other tasks, forming a directed acyclic
graph (DAG). The agent must resolve dependencies before starting blocked
tasks.
- **Interaction:** Users can view the current state of the tracker by asking the
agent to visualize it, or by running `gemini-cli` commands if implemented.
## Use cases
- Coordinating multi-file refactoring projects.
- Breaking down a mission into a hierarchy of epics and tasks for better
visibility.
- Tracking bugs and feature requests directly within the context of an active
codebase.
- Providing visibility into the agent's current focus and remaining work.
## Next steps
- Follow the [Task planning tutorial](../cli/tutorials/task-planning.md) for
usage details and migration from the legacy todo list.
- Learn about [Session management](../cli/session-management.md) for context on
persistent state.
+4
View File
@@ -11,6 +11,8 @@ import path from 'node:path';
describe('Background Process Monitoring', () => {
evalTest('USUALLY_PASSES', {
suiteName: 'default',
suiteType: 'behavioral',
name: 'should naturally use read output tool to find token',
prompt:
"Run the script using 'bash generate_token.sh'. It will emit a token after a short delay and continue running. Find the token and tell me what it is.",
@@ -50,6 +52,8 @@ sleep 100
});
evalTest('USUALLY_PASSES', {
suiteName: 'default',
suiteType: 'behavioral',
name: 'should naturally use list tool to verify multiple processes',
prompt:
"Start three background processes that run 'sleep 100', 'sleep 200', and 'sleep 300' respectively. Verify that all three are currently running.",
+50 -2
View File
@@ -298,12 +298,14 @@ describe('plan_mode', () => {
});
evalTest('ALWAYS_PASSES', {
suiteName: 'default',
suiteType: 'behavioral',
name: 'should transition from plan mode to normal execution and create a plan file from scratch',
params: {
settings,
},
prompt:
'Enter plan mode and plan to create a new module called foo. The plan should be saved as foo-plan.md. Then, exit plan mode.',
'I agree with your strategy. Please enter plan mode and draft the plan to create a new module called foo. The plan should be saved as foo-plan.md. Then, exit plan mode.',
assert: async (rig, result) => {
const enterPlanCalled = await rig.waitForToolCall('enter_plan_mode');
expect(
@@ -333,7 +335,7 @@ describe('plan_mode', () => {
expect(
planWrite?.toolRequest.success,
`Expected write_file to succeed, but got error: ${planWrite?.toolRequest.error}`,
`Expected write_file to succeed, but got error: ${(planWrite?.toolRequest as any).error}`,
).toBe(true);
assertModelHasOutput(result);
@@ -341,6 +343,8 @@ describe('plan_mode', () => {
});
evalTest('USUALLY_PASSES', {
suiteName: 'default',
suiteType: 'behavioral',
name: 'should not exit plan mode or draft before informal agreement',
approvalMode: ApprovalMode.PLAN,
params: {
@@ -372,4 +376,48 @@ describe('plan_mode', () => {
assertModelHasOutput(result);
},
});
evalTest('USUALLY_PASSES', {
name: 'should handle nested plan directories correctly',
suiteName: 'plan_mode',
suiteType: 'behavioral',
approvalMode: ApprovalMode.PLAN,
params: {
settings,
},
prompt:
'Please create a new architectural plan in a nested folder called "architecture/frontend-v2.md" within the plans directory. The plan should contain the text "# Frontend V2 Plan". Then, exit plan mode',
assert: async (rig, result) => {
await rig.waitForTelemetryReady();
const toolLogs = rig.readToolLogs();
const writeCalls = toolLogs.filter((log) =>
['write_file', 'replace'].includes(log.toolRequest.name),
);
const wroteToNestedPath = writeCalls.some((log) => {
try {
const args = JSON.parse(log.toolRequest.args);
if (!args.file_path) return false;
// In plan mode, paths can be passed as relative (architecture/frontend-v2.md)
// or they might be resolved as absolute by the tool depending on the exact mock state.
// We strictly ensure it ends exactly with the expected nested path and doesn't contain extra nesting.
const normalizedPath = args.file_path.replace(/\\/g, '/');
return (
normalizedPath === 'architecture/frontend-v2.md' ||
normalizedPath.endsWith('/plans/architecture/frontend-v2.md')
);
} catch {
return false;
}
});
expect(
wroteToNestedPath,
'Expected model to successfully target the nested plan file path',
).toBe(true);
assertModelHasOutput(result);
},
});
});
+291 -21
View File
@@ -283,7 +283,7 @@ describe('save_memory', () => {
name: proactiveMemoryFromLongSession,
params: {
settings: {
experimental: { memoryManager: true },
experimental: { memoryV2: true },
},
},
messages: [
@@ -341,29 +341,75 @@ describe('save_memory', () => {
prompt:
'Please save any persistent preferences or facts about me from our conversation to memory.',
assert: async (rig, result) => {
const wasToolCalled = await rig.waitForToolCall(
'invoke_agent',
undefined,
(args) => /save_memory/i.test(args) && /vitest/i.test(args),
);
// Under experimental.memoryV2, the agent persists memories by
// editing markdown files directly with write_file or replace — not via
// a save_memory subagent. The user said "I always prefer Vitest over
// Jest for testing in all my projects" — that matches the new
// cross-project cue phrase ("across all my projects"), so under the
// 4-tier model the correct destination is the global personal memory
// file (~/.gemini/GEMINI.md). It must NOT land in a committed project
// GEMINI.md (that tier is for team conventions) or the per-project
// private memory folder (that tier is for project-specific personal
// notes). The chat history mixes this durable preference with
// transient debugging chatter, so the eval also verifies the agent
// picks out the persistent fact among the noise.
await rig.waitForToolCall('write_file').catch(() => {});
const writeCalls = rig
.readToolLogs()
.filter((log) =>
['write_file', 'replace'].includes(log.toolRequest.name),
);
const wroteVitestToGlobal = writeCalls.some((log) => {
const args = log.toolRequest.args;
return (
/\.gemini\/GEMINI\.md/i.test(args) &&
!/tmp\/[^/]+\/memory/i.test(args) &&
/vitest/i.test(args)
);
});
expect(
wasToolCalled,
'Expected invoke_agent to be called with save_memory agent and the Vitest preference from the conversation history',
wroteVitestToGlobal,
'Expected the cross-project Vitest preference to be written to the global personal memory file (~/.gemini/GEMINI.md) via write_file or replace',
).toBe(true);
const leakedToCommittedProject = writeCalls.some((log) => {
const args = log.toolRequest.args;
return (
/GEMINI\.md/i.test(args) &&
!/\.gemini\//i.test(args) &&
/vitest/i.test(args)
);
});
expect(
leakedToCommittedProject,
'Cross-project Vitest preference must NOT be mirrored into a committed project ./GEMINI.md (that tier is for team-shared conventions only)',
).toBe(false);
const leakedToPrivateProject = writeCalls.some((log) => {
const args = log.toolRequest.args;
return (
/\.gemini\/tmp\/[^/]+\/memory\//i.test(args) && /vitest/i.test(args)
);
});
expect(
leakedToPrivateProject,
'Cross-project Vitest preference must NOT be mirrored into the private project memory folder (that tier is for project-specific personal notes only)',
).toBe(false);
assertModelHasOutput(result);
},
});
const memoryManagerRoutingPreferences =
'Agent routes global and project preferences to memory';
const memoryV2RoutesTeamConventionsToProjectGemini =
'Agent routes team-shared project conventions to ./GEMINI.md';
evalTest('USUALLY_PASSES', {
suiteName: 'default',
suiteType: 'behavioral',
name: memoryManagerRoutingPreferences,
name: memoryV2RoutesTeamConventionsToProjectGemini,
params: {
settings: {
experimental: { memoryManager: true },
experimental: { memoryV2: true },
},
},
messages: [
@@ -372,7 +418,7 @@ describe('save_memory', () => {
type: 'user',
content: [
{
text: 'I always use dark mode in all my editors and terminals.',
text: 'For this project, the team always runs tests with `npm run test` — please remember that as our project convention.',
},
],
timestamp: '2026-01-01T00:00:00Z',
@@ -380,7 +426,9 @@ describe('save_memory', () => {
{
id: 'msg-2',
type: 'gemini',
content: [{ text: 'Got it, I will keep that in mind!' }],
content: [
{ text: 'Got it, I will keep `npm run test` in mind for tests.' },
],
timestamp: '2026-01-01T00:00:05Z',
},
{
@@ -404,16 +452,238 @@ describe('save_memory', () => {
],
prompt: 'Please save the preferences I mentioned earlier to memory.',
assert: async (rig, result) => {
const wasToolCalled = await rig.waitForToolCall(
'invoke_agent',
undefined,
(args) => /save_memory/i.test(args),
);
// Under experimental.memoryV2, the prompt enforces an explicit
// one-tier-per-fact rule: team-shared project conventions (the team's
// test command, project-wide indentation rules) belong in the
// committed project-root ./GEMINI.md and must NOT be mirrored or
// cross-referenced into the private project memory folder
// (~/.gemini/tmp/<hash>/memory/). The global ~/.gemini/GEMINI.md must
// never be touched in this mode either.
await rig.waitForToolCall('write_file').catch(() => {});
const writeCalls = rig
.readToolLogs()
.filter((log) =>
['write_file', 'replace'].includes(log.toolRequest.name),
);
const wroteToProjectRoot = (factPattern: RegExp) =>
writeCalls.some((log) => {
const args = log.toolRequest.args;
return (
/GEMINI\.md/i.test(args) &&
!/\.gemini\//i.test(args) &&
factPattern.test(args)
);
});
expect(
wasToolCalled,
'Expected invoke_agent to be called with save_memory agent',
wroteToProjectRoot(/npm run test/i),
'Expected the team test-command convention to be written to the project-root ./GEMINI.md',
).toBe(true);
expect(
wroteToProjectRoot(/2[- ]space/i),
'Expected the project-wide "2-space indentation" convention to be written to the project-root ./GEMINI.md',
).toBe(true);
const leakedToPrivateMemory = writeCalls.some((log) => {
const args = log.toolRequest.args;
return (
/\.gemini\/tmp\/[^/]+\/memory\//i.test(args) &&
(/npm run test/i.test(args) || /2[- ]space/i.test(args))
);
});
expect(
leakedToPrivateMemory,
'Team-shared project conventions must NOT be mirrored into the private project memory folder (~/.gemini/tmp/<hash>/memory/) — each fact lives in exactly one tier.',
).toBe(false);
const leakedToGlobal = writeCalls.some((log) => {
const args = log.toolRequest.args;
return (
/\.gemini\/GEMINI\.md/i.test(args) &&
!/tmp\/[^/]+\/memory/i.test(args)
);
});
expect(
leakedToGlobal,
'Project preferences must NOT be written to the global ~/.gemini/GEMINI.md',
).toBe(false);
assertModelHasOutput(result);
},
});
const memoryV2RoutesUserProject =
'Agent routes personal-to-user project notes to user-project memory';
evalTest('USUALLY_PASSES', {
suiteName: 'default',
suiteType: 'behavioral',
name: memoryV2RoutesUserProject,
params: {
settings: {
experimental: { memoryV2: true },
},
},
prompt: `Please remember my personal local dev setup for THIS project's Postgres database. This is private to my machine — do NOT commit it to the repo.
Connection details:
- Host: localhost
- Port: 6543 (non-standard, I run multiple Postgres instances)
- Database: myproj_dev
- User: sandy_local
- Password: read from the SANDY_PG_LOCAL_PASS env var in my shell
How I start it locally:
1. Run \`brew services start postgresql@15\` to bring the server up.
2. Run \`./scripts/seed-local-db.sh\` from the repo root to load my personal seed data.
3. Verify with \`psql -h localhost -p 6543 -U sandy_local myproj_dev -c '\\dt'\`.
Quirks to remember:
- The migrations runner sometimes hangs on my machine if I forget step 1; kill it with Ctrl+C and rerun.
- I keep an extra \`scratch\` schema for ad-hoc experiments — never reference it from project code.`,
assert: async (rig, result) => {
// Under experimental.memoryV2 with the Private Project Memory bullet
// surfaced in the prompt, a fact that is project-specific AND
// personal-to-the-user (must not be committed) should land in the
// private project memory folder under ~/.gemini/tmp/<hash>/memory/. The
// detailed note should be written to a sibling markdown file, with
// MEMORY.md updated as the index. It must NOT go to committed
// ./GEMINI.md or the global ~/.gemini/GEMINI.md.
await rig.waitForToolCall('write_file').catch(() => {});
const writeCalls = rig
.readToolLogs()
.filter((log) =>
['write_file', 'replace'].includes(log.toolRequest.name),
);
const wroteUserProjectDetail = writeCalls.some((log) => {
const args = log.toolRequest.args;
return (
/\.gemini\/tmp\/[^/]+\/memory\/(?!MEMORY\.md)[^"]+\.md/i.test(args) &&
/6543/.test(args)
);
});
expect(
wroteUserProjectDetail,
'Expected the personal-to-user project note to be written to a private project memory detail file (~/.gemini/tmp/<hash>/memory/*.md)',
).toBe(true);
const wroteUserProjectIndex = writeCalls.some((log) => {
const args = log.toolRequest.args;
return /\.gemini\/tmp\/[^/]+\/memory\/MEMORY\.md/i.test(args);
});
expect(
wroteUserProjectIndex,
'Expected the personal-to-user project note to update the private project memory index (~/.gemini/tmp/<hash>/memory/MEMORY.md)',
).toBe(true);
// Defensive: should NOT have written this private note to the
// committed project GEMINI.md or the global GEMINI.md.
const leakedToCommittedProject = writeCalls.some((log) => {
const args = log.toolRequest.args;
return (
/\/GEMINI\.md/i.test(args) &&
!/\.gemini\//i.test(args) &&
/6543/.test(args)
);
});
expect(
leakedToCommittedProject,
'Personal-to-user note must NOT be written to the committed project GEMINI.md',
).toBe(false);
const leakedToGlobal = writeCalls.some((log) => {
const args = log.toolRequest.args;
return (
/\.gemini\/GEMINI\.md/i.test(args) &&
!/tmp\/[^/]+\/memory/i.test(args) &&
/6543/.test(args)
);
});
expect(
leakedToGlobal,
'Personal-to-user project note must NOT be written to the global ~/.gemini/GEMINI.md',
).toBe(false);
assertModelHasOutput(result);
},
});
const memoryV2RoutesCrossProjectToGlobal =
'Agent routes cross-project personal preferences to ~/.gemini/GEMINI.md';
evalTest('USUALLY_PASSES', {
suiteName: 'default',
suiteType: 'behavioral',
name: memoryV2RoutesCrossProjectToGlobal,
params: {
settings: {
experimental: { memoryV2: true },
},
},
prompt:
'Please remember this about me in general: across all my projects I always prefer Prettier with single quotes and trailing commas, and I always prefer tabs over spaces for indentation. These are my personal coding-style defaults that follow me into every workspace.',
assert: async (rig, result) => {
// Under experimental.memoryV2 with the Global Personal Memory
// tier surfaced in the prompt, a fact that explicitly applies to the
// user "across all my projects" / "in every workspace" must land in
// the global ~/.gemini/GEMINI.md (the cross-project tier). It must
// NOT be mirrored into a committed project-root ./GEMINI.md (that
// tier is for team-shared conventions) or into the per-project
// private memory folder (that tier is for project-specific personal
// notes). Each fact lives in exactly one tier across all four tiers.
await rig.waitForToolCall('write_file').catch(() => {});
const writeCalls = rig
.readToolLogs()
.filter((log) =>
['write_file', 'replace'].includes(log.toolRequest.name),
);
const wroteToGlobal = (factPattern: RegExp) =>
writeCalls.some((log) => {
const args = log.toolRequest.args;
return (
/\.gemini\/GEMINI\.md/i.test(args) &&
!/tmp\/[^/]+\/memory/i.test(args) &&
factPattern.test(args)
);
});
expect(
wroteToGlobal(/Prettier/i),
'Expected the cross-project Prettier preference to be written to the global personal memory file (~/.gemini/GEMINI.md)',
).toBe(true);
expect(
wroteToGlobal(/tabs/i),
'Expected the cross-project "tabs over spaces" preference to be written to the global personal memory file (~/.gemini/GEMINI.md)',
).toBe(true);
const leakedToCommittedProject = writeCalls.some((log) => {
const args = log.toolRequest.args;
return (
/GEMINI\.md/i.test(args) &&
!/\.gemini\//i.test(args) &&
(/Prettier/i.test(args) || /tabs/i.test(args))
);
});
expect(
leakedToCommittedProject,
'Cross-project personal preferences must NOT be mirrored into a committed project ./GEMINI.md (that tier is for team-shared conventions only)',
).toBe(false);
const leakedToPrivateProject = writeCalls.some((log) => {
const args = log.toolRequest.args;
return (
/\.gemini\/tmp\/[^/]+\/memory\//i.test(args) &&
(/Prettier/i.test(args) || /tabs/i.test(args))
);
});
expect(
leakedToPrivateProject,
'Cross-project personal preferences must NOT be mirrored into the private project memory folder (that tier is for project-specific personal notes only)',
).toBe(false);
assertModelHasOutput(result);
},
});
+2 -2
View File
@@ -149,12 +149,12 @@ async function readSkillBodies(skillsDir: string): Promise<string[]> {
/**
* Shared configOverrides for all skill extraction component evals.
* - experimentalMemoryManager: enables the memory extraction pipeline.
* - experimentalAutoMemory: enables the Auto Memory skill extraction pipeline.
* - approvalMode: YOLO auto-approves tool calls (write_file, read_file) so the
* background agent can execute without interactive confirmation.
*/
const EXTRACTION_CONFIG_OVERRIDES = {
experimentalMemoryManager: true,
experimentalAutoMemory: true,
approvalMode: ApprovalMode.YOLO,
};
+5 -4
View File
@@ -5,10 +5,7 @@
*/
import { describe, expect } from 'vitest';
import {
TRACKER_CREATE_TASK_TOOL_NAME,
TRACKER_UPDATE_TASK_TOOL_NAME,
} from '@google/gemini-cli-core';
import { TRACKER_CREATE_TASK_TOOL_NAME } from '@google/gemini-cli-core';
import { evalTest, TEST_AGENTS } from './test-helper.js';
describe('subtask delegation eval test cases', () => {
@@ -22,6 +19,8 @@ describe('subtask delegation eval test cases', () => {
* 3. Documenting (doc expert)
*/
evalTest('USUALLY_PASSES', {
suiteName: 'default',
suiteType: 'behavioral',
name: 'should delegate sequential subtasks to relevant experts using the task tracker',
params: {
settings: {
@@ -90,6 +89,8 @@ You are the doc expert. Document the provided implementation clearly.`,
* to multiple subagents in parallel using the task tracker to manage state.
*/
evalTest('USUALLY_PASSES', {
suiteName: 'default',
suiteType: 'behavioral',
name: 'should delegate independent subtasks to specialists using the task tracker',
params: {
settings: {
+2
View File
@@ -119,6 +119,8 @@ describe('tracker_mode', () => {
});
evalTest('USUALLY_PASSES', {
suiteName: 'default',
suiteType: 'behavioral',
name: 'should correctly identify the task tracker storage location from the system prompt',
params: {
settings: { experimental: { taskTracker: true } },
+13
View File
@@ -0,0 +1,13 @@
{
"extends": "../tsconfig.json",
"compilerOptions": {
"noEmit": true,
"paths": {
"@google/gemini-cli-core": ["../packages/core/index.ts"],
"@google/gemini-cli": ["../packages/cli/index.ts"]
}
},
"include": ["**/*.ts"],
"exclude": ["logs"],
"references": [{ "path": "../packages/core" }, { "path": "../packages/cli" }]
}
+2
View File
@@ -7,6 +7,8 @@
import { evalTest, TestRig } from './test-helper.js';
evalTest('USUALLY_PASSES', {
suiteName: 'default',
suiteType: 'behavioral',
name: 'Reproduction: Agent uses Object.create() for cloning/delegation',
prompt:
'Create a utility function `createScopedConfig(config: Config, additionalDirectories: string[]): Config` in `packages/core/src/config/scoped-config.ts` that returns a new Config instance. This instance should override `getWorkspaceContext()` to include the additional directories, but delegate all other method calls (like `isPathAllowed` or `validatePathAccess`) to the original config. Note that `Config` is a complex class with private state and cannot be easily shallow-copied or reconstructed.',
+18 -6
View File
@@ -21,6 +21,8 @@ describe('update_topic_behavior', () => {
* more than 1/4 turns.
*/
evalTest('USUALLY_PASSES', {
suiteName: 'default',
suiteType: 'behavioral',
name: 'update_topic should be used at start, end and middle for complex tasks',
prompt: `Create a simple users REST API using Express.
1. Initialize a new npm project and install express.
@@ -41,7 +43,7 @@ describe('update_topic_behavior', () => {
2,
),
'.gemini/settings.json': JSON.stringify({
experimental: {
general: {
topicUpdateNarration: true,
},
}),
@@ -117,13 +119,15 @@ describe('update_topic_behavior', () => {
});
evalTest('USUALLY_PASSES', {
suiteName: 'default',
suiteType: 'behavioral',
name: 'update_topic should NOT be used for informational coding tasks (Obvious)',
approvalMode: 'default',
prompt:
'Explain the difference between Map and Object in JavaScript and provide a performance-focused code snippet for each.',
files: {
'.gemini/settings.json': JSON.stringify({
experimental: {
general: {
topicUpdateNarration: true,
},
}),
@@ -142,6 +146,8 @@ describe('update_topic_behavior', () => {
});
evalTest('USUALLY_PASSES', {
suiteName: 'default',
suiteType: 'behavioral',
name: 'update_topic should NOT be used for surgical symbol searches (Grey Area)',
approvalMode: 'default',
prompt:
@@ -150,7 +156,7 @@ describe('update_topic_behavior', () => {
'packages/core/src/tools/tool-names.ts':
"export const UPDATE_TOPIC_TOOL_NAME = 'update_topic';",
'.gemini/settings.json': JSON.stringify({
experimental: {
general: {
topicUpdateNarration: true,
},
}),
@@ -169,6 +175,8 @@ describe('update_topic_behavior', () => {
});
evalTest('USUALLY_PASSES', {
suiteName: 'default',
suiteType: 'behavioral',
name: 'update_topic should be used for medium complexity multi-step tasks',
prompt:
'Refactor the `users-api` project. Move the routing logic from src/app.ts into a new file src/routes.ts, and update app.ts to use the new routes file.',
@@ -196,7 +204,7 @@ app.post('/users', (req, res) => {
export default app;
`,
'.gemini/settings.json': JSON.stringify({
experimental: {
general: {
topicUpdateNarration: true,
},
}),
@@ -212,7 +220,9 @@ export default app;
expect(topicCalls.length).toBeGreaterThanOrEqual(2);
// Verify it actually did the refactoring to ensure it didn't just fail immediately
expect(fs.existsSync(path.join(rig.testDir, 'src/routes.ts'))).toBe(true);
expect(fs.existsSync(path.join(rig.testDir!, 'src/routes.ts'))).toBe(
true,
);
},
});
@@ -224,6 +234,8 @@ export default app;
* the prompt change that improves the behavior.
*/
evalTest('USUALLY_PASSES', {
suiteName: 'default',
suiteType: 'behavioral',
name: 'update_topic should not be called twice in a row',
prompt: `
We need to build a C compiler.
@@ -237,7 +249,7 @@ export default app;
files: {
'package.json': JSON.stringify({ name: 'test-project' }),
'.gemini/settings.json': JSON.stringify({
experimental: {
general: {
topicUpdateNarration: true,
},
}),
+1
View File
@@ -70,6 +70,7 @@ describe('ACP telemetry', () => {
GEMINI_API_KEY: 'fake-key',
GEMINI_CLI_HOME: rig.homeDir!,
GEMINI_TELEMETRY_ENABLED: 'true',
GEMINI_TELEMETRY_TRACES_ENABLED: 'true',
GEMINI_TELEMETRY_TARGET: 'local',
GEMINI_TELEMETRY_OUTFILE: telemetryPath,
},
+5 -1
View File
@@ -39,7 +39,11 @@ describe('web-fetch rate limiting', () => {
const rateLimitedCalls = toolLogs.filter(
(log) =>
log.toolRequest.name === 'web_fetch' &&
log.toolRequest.error?.includes('Rate limit exceeded'),
(
('error' in log.toolRequest
? (log.toolRequest as unknown as Record<string, string>)['error']
: '') as string
)?.includes('Rate limit exceeded'),
);
expect(rateLimitedCalls.length).toBeGreaterThan(0);
@@ -8,7 +8,16 @@ import { expect, describe, it, beforeEach, afterEach } from 'vitest';
import { TestRig } from './test-helper.js';
import { join } from 'node:path';
describe('Interactive Mode', () => {
// Skip on macOS: every interactive test in this file is chronically flaky
// because the captured pty buffer contains the CLI's startup escape
// sequences (`q4;?m...true color warning`) instead of the streamed output,
// causing `expectText(...)` to time out. Reproducible across unrelated
// runs on `main` (24740161950, 24739323404) and on consecutive merge-queue
// gates for #25753 (24743605639, 24747624513) — different tests in the
// same describe fail on different runs. Not specific to any model.
const skipOnDarwin = process.platform === 'darwin';
describe.skipIf(skipOnDarwin)('Interactive Mode', () => {
let rig: TestRig;
beforeEach(() => {
+3 -1
View File
@@ -134,7 +134,9 @@ describe('file-system', () => {
).toBeTruthy();
const newFileContent = rig.readFile(fileName);
expect(newFileContent).toBe('hello');
// Trim to tolerate models that idiomatically append a trailing newline.
// This test is about path-with-spaces handling, not whitespace fidelity.
expect(newFileContent.trim()).toBe('hello');
});
it('should perform a read-then-write sequence', async () => {
+2 -1
View File
@@ -164,7 +164,8 @@ describe.skipIf(skipFlaky)(
);
expect(blockHook).toBeDefined();
expect(
blockHook?.hookCall.stdout + blockHook?.hookCall.stderr,
(blockHook?.hookCall.stdout || '') +
(blockHook?.hookCall.stderr || ''),
).toContain(blockMsg);
});
@@ -0,0 +1,2 @@
{"method":"generateContentStream","response":[{"candidates":[{"content":{"parts":[{"functionCall":{"name":"list_mcp_resources","args":{}}}],"role":"model"},"finishReason":"STOP","index":0}],"usageMetadata":{"promptTokenCount":10,"candidatesTokenCount":10,"totalTokenCount":20}}]}
{"method":"generateContentStream","response":[{"candidates":[{"content":{"parts":[{"text":"Here are the resources: test://resource1"}],"role":"model"},"finishReason":"STOP","index":0}],"usageMetadata":{"promptTokenCount":10,"candidatesTokenCount":10,"totalTokenCount":20}}]}
@@ -0,0 +1,2 @@
{"method":"generateContentStream","response":[{"candidates":[{"content":{"parts":[{"functionCall":{"name":"read_mcp_resource","args":{"uri":"test://resource1"}}}],"role":"model"},"finishReason":"STOP","index":0}],"usageMetadata":{"promptTokenCount":10,"candidatesTokenCount":10,"totalTokenCount":20}}]}
{"method":"generateContentStream","response":[{"candidates":[{"content":{"parts":[{"text":"The content is: content of resource 1"}],"role":"model"},"finishReason":"STOP","index":0}],"usageMetadata":{"promptTokenCount":10,"candidatesTokenCount":10,"totalTokenCount":20}}]}
@@ -0,0 +1,4 @@
{"method":"generateContentStream","response":[{"candidates":[{"content":{"parts":[{"functionCall":{"name":"list_mcp_resources","args":{}}}],"role":"model"},"finishReason":"STOP","index":0}],"usageMetadata":{"promptTokenCount":10,"candidatesTokenCount":10,"totalTokenCount":20}}]}
{"method":"generateContentStream","response":[{"candidates":[{"content":{"parts":[{"text":"Here are the resources: test://resource1"}],"role":"model"},"finishReason":"STOP","index":0}],"usageMetadata":{"promptTokenCount":10,"candidatesTokenCount":10,"totalTokenCount":20}}]}
{"method":"generateContentStream","response":[{"candidates":[{"content":{"parts":[{"functionCall":{"name":"read_mcp_resource","args":{"uri":"test://resource1"}}}],"role":"model"},"finishReason":"STOP","index":0}],"usageMetadata":{"promptTokenCount":10,"candidatesTokenCount":10,"totalTokenCount":20}}]}
{"method":"generateContentStream","response":[{"candidates":[{"content":{"parts":[{"text":"The content is: content of resource 1"}],"role":"model"},"finishReason":"STOP","index":0}],"usageMetadata":{"promptTokenCount":10,"candidatesTokenCount":10,"totalTokenCount":20}}]}
+178
View File
@@ -0,0 +1,178 @@
/**
* @license
* Copyright 2026 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
import { TestRig } from './test-helper.js';
import { join, dirname } from 'node:path';
import { fileURLToPath } from 'node:url';
import fs from 'node:fs';
const __dirname = dirname(fileURLToPath(import.meta.url));
describe('mcp-resources-integration', () => {
let rig: TestRig;
beforeEach(() => {
rig = new TestRig();
});
afterEach(async () => await rig.cleanup());
it('should list mcp resources', async () => {
await rig.setup('mcp-list-resources-test', {
settings: {
model: {
name: 'gemini-3-flash-preview',
},
},
fakeResponsesPath: join(__dirname, 'mcp-list-resources.responses'),
});
// Workaround for ProjectRegistry save issue
const userGeminiDir = join(rig.homeDir!, '.gemini');
fs.writeFileSync(join(userGeminiDir, 'projects.json'), '{"projects":{}}');
// Add a dummy server to get setup done
rig.addTestMcpServer('resource-server', {
name: 'resource-server',
tools: [],
});
// Overwrite the script with resource support
const scriptPath = join(rig.testDir!, 'test-mcp-resource-server.mjs');
const scriptContent = `
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import {
ListResourcesRequestSchema,
} from '@modelcontextprotocol/sdk/types.js';
const server = new Server(
{
name: 'resource-server',
version: '1.0.0',
},
{
capabilities: {
resources: {},
},
},
);
server.setRequestHandler(ListResourcesRequestSchema, async () => {
return {
resources: [
{
uri: 'test://resource1',
name: 'Resource 1',
mimeType: 'text/plain',
description: 'A test resource',
}
],
};
});
const transport = new StdioServerTransport();
await server.connect(transport);
`;
fs.writeFileSync(scriptPath, scriptContent);
const output = await rig.run({
args: 'List all available MCP resources.',
env: { GEMINI_API_KEY: 'dummy' },
});
const foundCall = await rig.waitForToolCall('list_mcp_resources');
expect(foundCall).toBeTruthy();
expect(output).toContain('test://resource1');
}, 60000);
it('should read mcp resource', async () => {
await rig.setup('mcp-read-resource-test', {
settings: {
model: {
name: 'gemini-3-flash-preview',
},
},
fakeResponsesPath: join(__dirname, 'mcp-read-resource.responses'),
});
// Workaround for ProjectRegistry save issue
const userGeminiDir = join(rig.homeDir!, '.gemini');
fs.writeFileSync(join(userGeminiDir, 'projects.json'), '{"projects":{}}');
// Add a dummy server to get setup done
rig.addTestMcpServer('resource-server', {
name: 'resource-server',
tools: [],
});
// Overwrite the script with resource support
const scriptPath = join(rig.testDir!, 'test-mcp-resource-server.mjs');
const scriptContent = `
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import {
ListResourcesRequestSchema,
ReadResourceRequestSchema,
} from '@modelcontextprotocol/sdk/types.js';
const server = new Server(
{
name: 'resource-server',
version: '1.0.0',
},
{
capabilities: {
resources: {},
},
},
);
// Need to provide list resources so the tool is active!
server.setRequestHandler(ListResourcesRequestSchema, async () => {
return {
resources: [
{
uri: 'test://resource1',
name: 'Resource 1',
mimeType: 'text/plain',
description: 'A test resource',
}
],
};
});
server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
if (request.params.uri === 'test://resource1') {
return {
contents: [
{
uri: 'test://resource1',
mimeType: 'text/plain',
text: 'This is the content of resource 1',
}
],
};
}
throw new Error('Resource not found');
});
const transport = new StdioServerTransport();
await server.connect(transport);
`;
fs.writeFileSync(scriptPath, scriptContent);
const output = await rig.run({
args: 'Read the MCP resource test://resource1.',
env: { GEMINI_API_KEY: 'dummy' },
});
const foundCall = await rig.waitForToolCall('read_mcp_resource');
expect(foundCall).toBeTruthy();
expect(output).toContain('content of resource 1');
}, 60000);
});
+25 -7
View File
@@ -81,7 +81,10 @@ describe('Plan Mode', () => {
await rig.run({
approvalMode: 'plan',
args: 'Create a file called plan.md in the plans directory.',
args:
'Create a file called plan.md in the plans directory with the ' +
'content "# Plan". Treat this as a Directive and write the file ' +
'immediately without proposing strategy or asking for confirmation.',
});
const toolLogs = rig.readToolLogs();
@@ -108,7 +111,7 @@ describe('Plan Mode', () => {
).toBeDefined();
expect(
planWrite?.toolRequest.success,
`Expected write_file to succeed, but it failed with error: ${planWrite?.toolRequest.error}`,
`Expected write_file to succeed, but it failed with error: ${'error' in (planWrite?.toolRequest || {}) ? (planWrite?.toolRequest as unknown as Record<string, string>)['error'] : 'unknown'}`,
).toBe(true);
});
@@ -194,7 +197,11 @@ describe('Plan Mode', () => {
await rig.run({
approvalMode: 'plan',
args: 'Create a file called plan-no-session.md in the plans directory.',
args:
'Create a file called plan-no-session.md in the plans directory ' +
'with the content "# Plan". Treat this as a Directive and write ' +
'the file immediately without proposing strategy or asking for ' +
'confirmation.',
});
const toolLogs = rig.readToolLogs();
@@ -221,7 +228,7 @@ describe('Plan Mode', () => {
).toBeDefined();
expect(
planWrite?.toolRequest.success,
`Expected write_file to succeed, but it failed with error: ${planWrite?.toolRequest.error}`,
`Expected write_file to succeed, but it failed with error: ${'error' in (planWrite?.toolRequest || {}) ? (planWrite?.toolRequest as unknown as Record<string, string>)['error'] : 'unknown'}`,
).toBe(true);
});
it('should switch from a pro model to a flash model after exiting plan mode', async () => {
@@ -270,13 +277,24 @@ describe('Plan Mode', () => {
);
const apiRequests = rig.readAllApiRequest();
const modelNames = apiRequests.map((r) => r.attributes?.model || 'unknown');
const modelNames = apiRequests.map(
(r) =>
('model' in (r.attributes || {})
? (r.attributes as unknown as Record<string, string>)['model']
: 'unknown') || 'unknown',
);
const proRequests = apiRequests.filter((r) =>
r.attributes?.model?.includes('pro'),
('model' in (r.attributes || {})
? (r.attributes as unknown as Record<string, string>)['model']
: 'unknown'
)?.includes('pro'),
);
const flashRequests = apiRequests.filter((r) =>
r.attributes?.model?.includes('flash'),
('model' in (r.attributes || {})
? (r.attributes as unknown as Record<string, string>)['model']
: 'unknown'
)?.includes('flash'),
);
expect(
+5 -1
View File
@@ -5,5 +5,9 @@
"allowJs": true
},
"include": ["**/*.ts"],
"references": [{ "path": "../packages/core" }]
"references": [
{ "path": "../packages/core" },
{ "path": "../packages/test-utils" },
{ "path": "../packages/cli" }
]
}
+36 -36
View File
@@ -1,55 +1,55 @@
{
"version": 1,
"updatedAt": "2026-04-10T15:36:04.547Z",
"updatedAt": "2026-04-20T18:04:59.671Z",
"scenarios": {
"multi-turn-conversation": {
"heapUsedBytes": 120082704,
"heapTotalBytes": 177586176,
"rssBytes": 269172736,
"externalBytes": 4304053,
"timestamp": "2026-04-10T15:35:17.603Z"
"heapUsedMB": 68.8,
"heapTotalMB": 91.2,
"rssMB": 215.4,
"externalMB": 93.8,
"timestamp": "2026-04-20T18:02:40.101Z"
},
"multi-function-call-repo-search": {
"heapUsedBytes": 104644984,
"heapTotalBytes": 111575040,
"rssBytes": 204079104,
"externalBytes": 4304053,
"timestamp": "2026-04-10T15:35:22.480Z"
"heapUsedMB": 73.5,
"heapTotalMB": 93.1,
"rssMB": 223.6,
"externalMB": 97.7,
"timestamp": "2026-04-20T18:02:42.032Z"
},
"idle-session-startup": {
"heapUsedBytes": 119813672,
"heapTotalBytes": 177061888,
"rssBytes": 267943936,
"externalBytes": 4304053,
"timestamp": "2026-04-10T15:35:08.035Z"
"heapUsedMB": 69.8,
"heapTotalMB": 92.4,
"rssMB": 217.4,
"externalMB": 93.8,
"timestamp": "2026-04-20T18:02:36.294Z"
},
"simple-prompt-response": {
"heapUsedBytes": 119722064,
"heapTotalBytes": 177324032,
"rssBytes": 268812288,
"externalBytes": 4304053,
"timestamp": "2026-04-10T15:35:12.770Z"
"heapUsedMB": 69.5,
"heapTotalMB": 92.4,
"rssMB": 216.1,
"externalMB": 93.8,
"timestamp": "2026-04-20T18:02:38.198Z"
},
"resume-large-chat-with-messages": {
"heapUsedBytes": 106545568,
"heapTotalBytes": 111509504,
"rssBytes": 202596352,
"externalBytes": 4306101,
"timestamp": "2026-04-10T15:36:04.547Z"
"heapUsedMB": 887.1,
"heapTotalMB": 954.3,
"rssMB": 1109.6,
"externalMB": 103.2,
"timestamp": "2026-04-20T18:04:59.671Z"
},
"resume-large-chat": {
"heapUsedBytes": 106513760,
"heapTotalBytes": 111509504,
"rssBytes": 202596352,
"externalBytes": 4306101,
"timestamp": "2026-04-10T15:35:59.528Z"
"heapUsedMB": 885.6,
"heapTotalMB": 955.6,
"rssMB": 1107.8,
"externalMB": 110.5,
"timestamp": "2026-04-20T18:04:06.526Z"
},
"large-chat": {
"heapUsedBytes": 106471568,
"heapTotalBytes": 111509504,
"rssBytes": 202596352,
"externalBytes": 4306101,
"timestamp": "2026-04-10T15:35:53.180Z"
"heapUsedMB": 158.5,
"heapTotalMB": 193,
"rssMB": 787.9,
"externalMB": 104,
"timestamp": "2026-04-20T18:03:12.486Z"
}
}
}
+36 -10
View File
@@ -16,15 +16,21 @@ import {
mkdirSync,
rmSync,
} from 'node:fs';
import { randomUUID } from 'node:crypto';
import { randomUUID, createHash } from 'node:crypto';
const __dirname = dirname(fileURLToPath(import.meta.url));
const BASELINES_PATH = join(__dirname, 'baselines.json');
const UPDATE_BASELINES = process.env['UPDATE_MEMORY_BASELINES'] === 'true';
function getProjectHash(projectRoot: string): string {
return createHash('sha256').update(projectRoot).digest('hex');
}
const TOLERANCE_PERCENT = 10;
// Fake API key for tests using fake responses
const TEST_ENV = { GEMINI_API_KEY: 'fake-memory-test-key' };
const TEST_ENV = {
GEMINI_API_KEY: 'fake-memory-test-key',
GEMINI_MEMORY_MONITOR_INTERVAL: '100',
};
describe('Memory Usage Tests', () => {
let harness: MemoryTestHarness;
@@ -56,6 +62,7 @@ describe('Memory Usage Tests', () => {
});
const result = await harness.runScenario(
rig,
'idle-session-startup',
async (recordSnapshot) => {
await rig.run({
@@ -85,6 +92,7 @@ describe('Memory Usage Tests', () => {
});
const result = await harness.runScenario(
rig,
'simple-prompt-response',
async (recordSnapshot) => {
await rig.run({
@@ -122,6 +130,7 @@ describe('Memory Usage Tests', () => {
];
const result = await harness.runScenario(
rig,
'multi-turn-conversation',
async (recordSnapshot) => {
// Run through all turns as a piped sequence
@@ -144,6 +153,9 @@ describe('Memory Usage Tests', () => {
);
} else {
harness.assertWithinBaseline(result);
harness.assertMemoryReturnsToBaseline(result.snapshots, 20);
const { leaked, message } = harness.analyzeSnapshots(result.snapshots);
if (leaked) console.warn(`${message}`);
}
});
@@ -168,6 +180,7 @@ describe('Memory Usage Tests', () => {
);
const result = await harness.runScenario(
rig,
'multi-function-call-repo-search',
async (recordSnapshot) => {
await rig.run({
@@ -189,6 +202,7 @@ describe('Memory Usage Tests', () => {
);
} else {
harness.assertWithinBaseline(result);
harness.assertMemoryReturnsToBaseline(result.snapshots, 20);
}
});
@@ -228,6 +242,7 @@ describe('Memory Usage Tests', () => {
});
const result = await harness.runScenario(
rig,
'large-chat',
async (recordSnapshot) => {
await rig.run({
@@ -257,19 +272,21 @@ describe('Memory Usage Tests', () => {
});
const result = await harness.runScenario(
rig,
'resume-large-chat',
async (recordSnapshot) => {
// Ensure the history file is linked
const targetChatsDir = join(
rig.testDir!,
rig.homeDir!,
'.gemini',
'tmp',
'test-project-hash',
getProjectHash(rig.testDir!),
'chats',
);
mkdirSync(targetChatsDir, { recursive: true });
const targetHistoryPath = join(
targetChatsDir,
'large-chat-session.json',
'session-large-chat.json',
);
if (existsSync(targetHistoryPath)) rmSync(targetHistoryPath);
copyFileSync(sharedHistoryPath, targetHistoryPath);
@@ -302,19 +319,21 @@ describe('Memory Usage Tests', () => {
});
const result = await harness.runScenario(
rig,
'resume-large-chat-with-messages',
async (recordSnapshot) => {
// Ensure the history file is linked
const targetChatsDir = join(
rig.testDir!,
rig.homeDir!,
'.gemini',
'tmp',
'test-project-hash',
getProjectHash(rig.testDir!),
'chats',
);
mkdirSync(targetChatsDir, { recursive: true });
const targetHistoryPath = join(
targetChatsDir,
'large-chat-session.json',
'session-large-chat.json',
);
if (existsSync(targetHistoryPath)) rmSync(targetHistoryPath);
copyFileSync(sharedHistoryPath, targetHistoryPath);
@@ -457,6 +476,9 @@ async function generateSharedLargeChatData(tempDir: string) {
// Generate responses for resumed chat
const resumeResponsesStream = createWriteStream(resumeResponsesPath);
for (let i = 0; i < 5; i++) {
// Doubling up on non-streaming responses to satisfy classifier and complexity checks
resumeResponsesStream.write(JSON.stringify(complexityResponse) + '\n');
resumeResponsesStream.write(JSON.stringify(summaryResponse) + '\n');
resumeResponsesStream.write(JSON.stringify(complexityResponse) + '\n');
resumeResponsesStream.write(
JSON.stringify({
@@ -489,8 +511,12 @@ async function generateSharedLargeChatData(tempDir: string) {
// Wait for streams to finish
await Promise.all([
new Promise((res) => activeResponsesStream.on('finish', res)),
new Promise((res) => resumeResponsesStream.on('finish', res)),
new Promise((res) =>
activeResponsesStream.on('finish', () => res(undefined)),
),
new Promise((res) =>
resumeResponsesStream.on('finish', () => res(undefined)),
),
]);
return {
+456 -240
View File
@@ -1,12 +1,12 @@
{
"name": "@google/gemini-cli",
"version": "0.40.0-nightly.20260414.g5b1f7375a",
"version": "0.41.0-nightly.20260423.gaa05b4583",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@google/gemini-cli",
"version": "0.40.0-nightly.20260414.g5b1f7375a",
"version": "0.41.0-nightly.20260423.gaa05b4583",
"workspaces": [
"packages/*"
],
@@ -62,10 +62,12 @@
"prettier": "^3.5.3",
"react-devtools-core": "^6.1.2",
"react-dom": "^19.2.0",
"read-package-up": "^11.0.0",
"semver": "^7.7.2",
"strip-ansi": "^7.1.2",
"ts-prune": "^0.10.3",
"tsx": "^4.20.3",
"typescript": "^5.8.3",
"typescript-eslint": "^8.30.1",
"vitest": "^3.2.4",
"yargs": "^17.7.2"
@@ -933,9 +935,9 @@
}
},
"node_modules/@eslint-community/eslint-utils": {
"version": "4.7.0",
"resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.7.0.tgz",
"integrity": "sha512-dyybb3AcajC7uha6CvhdVRJqaKyn7w2YKqKyAN37NKYgZT36w+iRb0Dymmc5qEJ549c/S31cMMSFd75bteCpCw==",
"version": "4.9.1",
"resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz",
"integrity": "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -965,9 +967,9 @@
}
},
"node_modules/@eslint-community/regexpp": {
"version": "4.12.1",
"resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.1.tgz",
"integrity": "sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==",
"version": "4.12.2",
"resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz",
"integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==",
"dev": true,
"license": "MIT",
"engines": {
@@ -4273,21 +4275,20 @@
}
},
"node_modules/@typescript-eslint/eslint-plugin": {
"version": "8.35.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.35.0.tgz",
"integrity": "sha512-ijItUYaiWuce0N1SoSMrEd0b6b6lYkYt99pqCPfybd+HKVXtEvYhICfLdwp42MhiI5mp0oq7PKEL+g1cNiz/Eg==",
"version": "8.58.2",
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.58.2.tgz",
"integrity": "sha512-aC2qc5thQahutKjP+cl8cgN9DWe3ZUqVko30CMSZHnFEHyhOYoZSzkGtAI2mcwZ38xeImDucI4dnqsHiOYuuCw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@eslint-community/regexpp": "^4.10.0",
"@typescript-eslint/scope-manager": "8.35.0",
"@typescript-eslint/type-utils": "8.35.0",
"@typescript-eslint/utils": "8.35.0",
"@typescript-eslint/visitor-keys": "8.35.0",
"graphemer": "^1.4.0",
"ignore": "^7.0.0",
"@eslint-community/regexpp": "^4.12.2",
"@typescript-eslint/scope-manager": "8.58.2",
"@typescript-eslint/type-utils": "8.58.2",
"@typescript-eslint/utils": "8.58.2",
"@typescript-eslint/visitor-keys": "8.58.2",
"ignore": "^7.0.5",
"natural-compare": "^1.4.0",
"ts-api-utils": "^2.1.0"
"ts-api-utils": "^2.5.0"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@@ -4297,9 +4298,9 @@
"url": "https://opencollective.com/typescript-eslint"
},
"peerDependencies": {
"@typescript-eslint/parser": "^8.35.0",
"eslint": "^8.57.0 || ^9.0.0",
"typescript": ">=4.8.4 <5.9.0"
"@typescript-eslint/parser": "^8.58.2",
"eslint": "^8.57.0 || ^9.0.0 || ^10.0.0",
"typescript": ">=4.8.4 <6.1.0"
}
},
"node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": {
@@ -4313,17 +4314,17 @@
}
},
"node_modules/@typescript-eslint/parser": {
"version": "8.35.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.35.0.tgz",
"integrity": "sha512-6sMvZePQrnZH2/cJkwRpkT7DxoAWh+g6+GFRK6bV3YQo7ogi3SX5rgF6099r5Q53Ma5qeT7LGmOmuIutF4t3lA==",
"version": "8.58.2",
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.58.2.tgz",
"integrity": "sha512-/Zb/xaIDfxeJnvishjGdcR4jmr7S+bda8PKNhRGdljDM+elXhlvN0FyPSsMnLmJUrVG9aPO6dof80wjMawsASg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/scope-manager": "8.35.0",
"@typescript-eslint/types": "8.35.0",
"@typescript-eslint/typescript-estree": "8.35.0",
"@typescript-eslint/visitor-keys": "8.35.0",
"debug": "^4.3.4"
"@typescript-eslint/scope-manager": "8.58.2",
"@typescript-eslint/types": "8.58.2",
"@typescript-eslint/typescript-estree": "8.58.2",
"@typescript-eslint/visitor-keys": "8.58.2",
"debug": "^4.4.3"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@@ -4333,20 +4334,20 @@
"url": "https://opencollective.com/typescript-eslint"
},
"peerDependencies": {
"eslint": "^8.57.0 || ^9.0.0",
"typescript": ">=4.8.4 <5.9.0"
"eslint": "^8.57.0 || ^9.0.0 || ^10.0.0",
"typescript": ">=4.8.4 <6.1.0"
}
},
"node_modules/@typescript-eslint/project-service": {
"version": "8.35.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.35.0.tgz",
"integrity": "sha512-41xatqRwWZuhUMF/aZm2fcUsOFKNcG28xqRSS6ZVr9BVJtGExosLAm5A1OxTjRMagx8nJqva+P5zNIGt8RIgbQ==",
"version": "8.58.2",
"resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.58.2.tgz",
"integrity": "sha512-Cq6UfpZZk15+r87BkIh5rDpi38W4b+Sjnb8wQCPPDDweS/LRCFjCyViEbzHk5Ck3f2QDfgmlxqSa7S7clDtlfg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/tsconfig-utils": "^8.35.0",
"@typescript-eslint/types": "^8.35.0",
"debug": "^4.3.4"
"@typescript-eslint/tsconfig-utils": "^8.58.2",
"@typescript-eslint/types": "^8.58.2",
"debug": "^4.4.3"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@@ -4356,18 +4357,18 @@
"url": "https://opencollective.com/typescript-eslint"
},
"peerDependencies": {
"typescript": ">=4.8.4 <5.9.0"
"typescript": ">=4.8.4 <6.1.0"
}
},
"node_modules/@typescript-eslint/scope-manager": {
"version": "8.35.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.35.0.tgz",
"integrity": "sha512-+AgL5+mcoLxl1vGjwNfiWq5fLDZM1TmTPYs2UkyHfFhgERxBbqHlNjRzhThJqz+ktBqTChRYY6zwbMwy0591AA==",
"version": "8.58.2",
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.58.2.tgz",
"integrity": "sha512-SgmyvDPexWETQek+qzZnrG6844IaO02UVyOLhI4wpo82dpZJY9+6YZCKAMFzXb7qhx37mFK1QcPQ18tud+vo6Q==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/types": "8.35.0",
"@typescript-eslint/visitor-keys": "8.35.0"
"@typescript-eslint/types": "8.58.2",
"@typescript-eslint/visitor-keys": "8.58.2"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@@ -4378,9 +4379,9 @@
}
},
"node_modules/@typescript-eslint/tsconfig-utils": {
"version": "8.35.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.35.0.tgz",
"integrity": "sha512-04k/7247kZzFraweuEirmvUj+W3bJLI9fX6fbo1Qm2YykuBvEhRTPl8tcxlYO8kZZW+HIXfkZNoasVb8EV4jpA==",
"version": "8.58.2",
"resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.58.2.tgz",
"integrity": "sha512-3SR+RukipDvkkKp/d0jP0dyzuls3DbGmwDpVEc5wqk5f38KFThakqAAO0XMirWAE+kT00oTauTbzMFGPoAzB0A==",
"dev": true,
"license": "MIT",
"engines": {
@@ -4391,20 +4392,21 @@
"url": "https://opencollective.com/typescript-eslint"
},
"peerDependencies": {
"typescript": ">=4.8.4 <5.9.0"
"typescript": ">=4.8.4 <6.1.0"
}
},
"node_modules/@typescript-eslint/type-utils": {
"version": "8.35.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.35.0.tgz",
"integrity": "sha512-ceNNttjfmSEoM9PW87bWLDEIaLAyR+E6BoYJQ5PfaDau37UGca9Nyq3lBk8Bw2ad0AKvYabz6wxc7DMTO2jnNA==",
"version": "8.58.2",
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.58.2.tgz",
"integrity": "sha512-Z7EloNR/B389FvabdGeTo2XMs4W9TjtPiO9DAsmT0yom0bwlPyRjkJ1uCdW1DvrrrYP50AJZ9Xc3sByZA9+dcg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/typescript-estree": "8.35.0",
"@typescript-eslint/utils": "8.35.0",
"debug": "^4.3.4",
"ts-api-utils": "^2.1.0"
"@typescript-eslint/types": "8.58.2",
"@typescript-eslint/typescript-estree": "8.58.2",
"@typescript-eslint/utils": "8.58.2",
"debug": "^4.4.3",
"ts-api-utils": "^2.5.0"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@@ -4414,14 +4416,14 @@
"url": "https://opencollective.com/typescript-eslint"
},
"peerDependencies": {
"eslint": "^8.57.0 || ^9.0.0",
"typescript": ">=4.8.4 <5.9.0"
"eslint": "^8.57.0 || ^9.0.0 || ^10.0.0",
"typescript": ">=4.8.4 <6.1.0"
}
},
"node_modules/@typescript-eslint/types": {
"version": "8.35.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.35.0.tgz",
"integrity": "sha512-0mYH3emanku0vHw2aRLNGqe7EXh9WHEhi7kZzscrMDf6IIRUQ5Jk4wp1QrledE/36KtdZrVfKnE32eZCf/vaVQ==",
"version": "8.58.2",
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.58.2.tgz",
"integrity": "sha512-9TukXyATBQf/Jq9AMQXfvurk+G5R2MwfqQGDR2GzGz28HvY/lXNKGhkY+6IOubwcquikWk5cjlgPvD2uAA7htQ==",
"dev": true,
"license": "MIT",
"engines": {
@@ -4433,22 +4435,21 @@
}
},
"node_modules/@typescript-eslint/typescript-estree": {
"version": "8.35.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.35.0.tgz",
"integrity": "sha512-F+BhnaBemgu1Qf8oHrxyw14wq6vbL8xwWKKMwTMwYIRmFFY/1n/9T/jpbobZL8vp7QyEUcC6xGrnAO4ua8Kp7w==",
"version": "8.58.2",
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.58.2.tgz",
"integrity": "sha512-ELGuoofuhhoCvNbQjFFiobFcGgcDCEm0ThWdmO4Z0UzLqPXS3KFvnEZ+SHewwOYHjM09tkzOWXNTv9u6Gqtyuw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/project-service": "8.35.0",
"@typescript-eslint/tsconfig-utils": "8.35.0",
"@typescript-eslint/types": "8.35.0",
"@typescript-eslint/visitor-keys": "8.35.0",
"debug": "^4.3.4",
"fast-glob": "^3.3.2",
"is-glob": "^4.0.3",
"minimatch": "^9.0.4",
"semver": "^7.6.0",
"ts-api-utils": "^2.1.0"
"@typescript-eslint/project-service": "8.58.2",
"@typescript-eslint/tsconfig-utils": "8.58.2",
"@typescript-eslint/types": "8.58.2",
"@typescript-eslint/visitor-keys": "8.58.2",
"debug": "^4.4.3",
"minimatch": "^10.2.2",
"semver": "^7.7.3",
"tinyglobby": "^0.2.15",
"ts-api-utils": "^2.5.0"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@@ -4458,20 +4459,20 @@
"url": "https://opencollective.com/typescript-eslint"
},
"peerDependencies": {
"typescript": ">=4.8.4 <5.9.0"
"typescript": ">=4.8.4 <6.1.0"
}
},
"node_modules/@typescript-eslint/utils": {
"version": "8.35.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.35.0.tgz",
"integrity": "sha512-nqoMu7WWM7ki5tPgLVsmPM8CkqtoPUG6xXGeefM5t4x3XumOEKMoUZPdi+7F+/EotukN4R9OWdmDxN80fqoZeg==",
"version": "8.58.2",
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.58.2.tgz",
"integrity": "sha512-QZfjHNEzPY8+l0+fIXMvuQ2sJlplB4zgDZvA+NmvZsZv3EQwOcc1DuIU1VJUTWZ/RKouBMhDyNaBMx4sWvrzRA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@eslint-community/eslint-utils": "^4.7.0",
"@typescript-eslint/scope-manager": "8.35.0",
"@typescript-eslint/types": "8.35.0",
"@typescript-eslint/typescript-estree": "8.35.0"
"@eslint-community/eslint-utils": "^4.9.1",
"@typescript-eslint/scope-manager": "8.58.2",
"@typescript-eslint/types": "8.58.2",
"@typescript-eslint/typescript-estree": "8.58.2"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@@ -4481,19 +4482,19 @@
"url": "https://opencollective.com/typescript-eslint"
},
"peerDependencies": {
"eslint": "^8.57.0 || ^9.0.0",
"typescript": ">=4.8.4 <5.9.0"
"eslint": "^8.57.0 || ^9.0.0 || ^10.0.0",
"typescript": ">=4.8.4 <6.1.0"
}
},
"node_modules/@typescript-eslint/visitor-keys": {
"version": "8.35.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.35.0.tgz",
"integrity": "sha512-zTh2+1Y8ZpmeQaQVIc/ZZxsx8UzgKJyNg1PTvjzC7WMhPSVS8bfDX34k1SrwOf016qd5RU3az2UxUNue3IfQ5g==",
"version": "8.58.2",
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.58.2.tgz",
"integrity": "sha512-f1WO2Lx8a9t8DARmcWAUPJbu0G20bJlj8L4z72K00TMeJAoyLr/tHhI/pzYBLrR4dXWkcxO1cWYZEOX8DKHTqA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/types": "8.35.0",
"eslint-visitor-keys": "^4.2.1"
"@typescript-eslint/types": "8.58.2",
"eslint-visitor-keys": "^5.0.0"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@@ -4503,6 +4504,19 @@
"url": "https://opencollective.com/typescript-eslint"
}
},
"node_modules/@typescript-eslint/visitor-keys/node_modules/eslint-visitor-keys": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz",
"integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==",
"dev": true,
"license": "Apache-2.0",
"engines": {
"node": "^20.19.0 || ^22.13.0 || >=24"
},
"funding": {
"url": "https://opencollective.com/eslint"
}
},
"node_modules/@typespec/ts-http-runtime": {
"version": "0.3.0",
"resolved": "https://registry.npmjs.org/@typespec/ts-http-runtime/-/ts-http-runtime-0.3.0.tgz",
@@ -4579,148 +4593,6 @@
}
}
},
"node_modules/@vitest/eslint-plugin/node_modules/@typescript-eslint/project-service": {
"version": "8.47.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.47.0.tgz",
"integrity": "sha512-2X4BX8hUeB5JcA1TQJ7GjcgulXQ+5UkNb0DL8gHsHUHdFoiCTJoYLTpib3LtSDPZsRET5ygN4qqIWrHyYIKERA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/tsconfig-utils": "^8.47.0",
"@typescript-eslint/types": "^8.47.0",
"debug": "^4.3.4"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/typescript-eslint"
},
"peerDependencies": {
"typescript": ">=4.8.4 <6.0.0"
}
},
"node_modules/@vitest/eslint-plugin/node_modules/@typescript-eslint/scope-manager": {
"version": "8.47.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.47.0.tgz",
"integrity": "sha512-a0TTJk4HXMkfpFkL9/WaGTNuv7JWfFTQFJd6zS9dVAjKsojmv9HT55xzbEpnZoY+VUb+YXLMp+ihMLz/UlZfDg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/types": "8.47.0",
"@typescript-eslint/visitor-keys": "8.47.0"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/typescript-eslint"
}
},
"node_modules/@vitest/eslint-plugin/node_modules/@typescript-eslint/tsconfig-utils": {
"version": "8.47.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.47.0.tgz",
"integrity": "sha512-ybUAvjy4ZCL11uryalkKxuT3w3sXJAuWhOoGS3T/Wu+iUu1tGJmk5ytSY8gbdACNARmcYEB0COksD2j6hfGK2g==",
"dev": true,
"license": "MIT",
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/typescript-eslint"
},
"peerDependencies": {
"typescript": ">=4.8.4 <6.0.0"
}
},
"node_modules/@vitest/eslint-plugin/node_modules/@typescript-eslint/types": {
"version": "8.47.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.47.0.tgz",
"integrity": "sha512-nHAE6bMKsizhA2uuYZbEbmp5z2UpffNrPEqiKIeN7VsV6UY/roxanWfoRrf6x/k9+Obf+GQdkm0nPU+vnMXo9A==",
"dev": true,
"license": "MIT",
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/typescript-eslint"
}
},
"node_modules/@vitest/eslint-plugin/node_modules/@typescript-eslint/typescript-estree": {
"version": "8.47.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.47.0.tgz",
"integrity": "sha512-k6ti9UepJf5NpzCjH31hQNLHQWupTRPhZ+KFF8WtTuTpy7uHPfeg2NM7cP27aCGajoEplxJDFVCEm9TGPYyiVg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/project-service": "8.47.0",
"@typescript-eslint/tsconfig-utils": "8.47.0",
"@typescript-eslint/types": "8.47.0",
"@typescript-eslint/visitor-keys": "8.47.0",
"debug": "^4.3.4",
"fast-glob": "^3.3.2",
"is-glob": "^4.0.3",
"minimatch": "^9.0.4",
"semver": "^7.6.0",
"ts-api-utils": "^2.1.0"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/typescript-eslint"
},
"peerDependencies": {
"typescript": ">=4.8.4 <6.0.0"
}
},
"node_modules/@vitest/eslint-plugin/node_modules/@typescript-eslint/utils": {
"version": "8.47.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.47.0.tgz",
"integrity": "sha512-g7XrNf25iL4TJOiPqatNuaChyqt49a/onq5YsJ9+hXeugK+41LVg7AxikMfM02PC6jbNtZLCJj6AUcQXJS/jGQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@eslint-community/eslint-utils": "^4.7.0",
"@typescript-eslint/scope-manager": "8.47.0",
"@typescript-eslint/types": "8.47.0",
"@typescript-eslint/typescript-estree": "8.47.0"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/typescript-eslint"
},
"peerDependencies": {
"eslint": "^8.57.0 || ^9.0.0",
"typescript": ">=4.8.4 <6.0.0"
}
},
"node_modules/@vitest/eslint-plugin/node_modules/@typescript-eslint/visitor-keys": {
"version": "8.47.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.47.0.tgz",
"integrity": "sha512-SIV3/6eftCy1bNzCQoPmbWsRLujS8t5iDIZ4spZOBHqrM+yfX2ogg8Tt3PDTAVKw3sSCiUgg30uOAvK2r9zGjQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/types": "8.47.0",
"eslint-visitor-keys": "^4.2.1"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/typescript-eslint"
}
},
"node_modules/@vitest/expect": {
"version": "3.2.4",
"resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-3.2.4.tgz",
@@ -15878,9 +15750,9 @@
}
},
"node_modules/ts-api-utils": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.1.0.tgz",
"integrity": "sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ==",
"version": "2.5.0",
"resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.5.0.tgz",
"integrity": "sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==",
"dev": true,
"license": "MIT",
"engines": {
@@ -16163,6 +16035,237 @@
"typescript": ">=4.8.4 <5.9.0"
}
},
"node_modules/typescript-eslint/node_modules/@typescript-eslint/eslint-plugin": {
"version": "8.35.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.35.0.tgz",
"integrity": "sha512-ijItUYaiWuce0N1SoSMrEd0b6b6lYkYt99pqCPfybd+HKVXtEvYhICfLdwp42MhiI5mp0oq7PKEL+g1cNiz/Eg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@eslint-community/regexpp": "^4.10.0",
"@typescript-eslint/scope-manager": "8.35.0",
"@typescript-eslint/type-utils": "8.35.0",
"@typescript-eslint/utils": "8.35.0",
"@typescript-eslint/visitor-keys": "8.35.0",
"graphemer": "^1.4.0",
"ignore": "^7.0.0",
"natural-compare": "^1.4.0",
"ts-api-utils": "^2.1.0"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/typescript-eslint"
},
"peerDependencies": {
"@typescript-eslint/parser": "^8.35.0",
"eslint": "^8.57.0 || ^9.0.0",
"typescript": ">=4.8.4 <5.9.0"
}
},
"node_modules/typescript-eslint/node_modules/@typescript-eslint/parser": {
"version": "8.35.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.35.0.tgz",
"integrity": "sha512-6sMvZePQrnZH2/cJkwRpkT7DxoAWh+g6+GFRK6bV3YQo7ogi3SX5rgF6099r5Q53Ma5qeT7LGmOmuIutF4t3lA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/scope-manager": "8.35.0",
"@typescript-eslint/types": "8.35.0",
"@typescript-eslint/typescript-estree": "8.35.0",
"@typescript-eslint/visitor-keys": "8.35.0",
"debug": "^4.3.4"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/typescript-eslint"
},
"peerDependencies": {
"eslint": "^8.57.0 || ^9.0.0",
"typescript": ">=4.8.4 <5.9.0"
}
},
"node_modules/typescript-eslint/node_modules/@typescript-eslint/project-service": {
"version": "8.35.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.35.0.tgz",
"integrity": "sha512-41xatqRwWZuhUMF/aZm2fcUsOFKNcG28xqRSS6ZVr9BVJtGExosLAm5A1OxTjRMagx8nJqva+P5zNIGt8RIgbQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/tsconfig-utils": "^8.35.0",
"@typescript-eslint/types": "^8.35.0",
"debug": "^4.3.4"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/typescript-eslint"
},
"peerDependencies": {
"typescript": ">=4.8.4 <5.9.0"
}
},
"node_modules/typescript-eslint/node_modules/@typescript-eslint/scope-manager": {
"version": "8.35.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.35.0.tgz",
"integrity": "sha512-+AgL5+mcoLxl1vGjwNfiWq5fLDZM1TmTPYs2UkyHfFhgERxBbqHlNjRzhThJqz+ktBqTChRYY6zwbMwy0591AA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/types": "8.35.0",
"@typescript-eslint/visitor-keys": "8.35.0"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/typescript-eslint"
}
},
"node_modules/typescript-eslint/node_modules/@typescript-eslint/tsconfig-utils": {
"version": "8.35.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.35.0.tgz",
"integrity": "sha512-04k/7247kZzFraweuEirmvUj+W3bJLI9fX6fbo1Qm2YykuBvEhRTPl8tcxlYO8kZZW+HIXfkZNoasVb8EV4jpA==",
"dev": true,
"license": "MIT",
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/typescript-eslint"
},
"peerDependencies": {
"typescript": ">=4.8.4 <5.9.0"
}
},
"node_modules/typescript-eslint/node_modules/@typescript-eslint/type-utils": {
"version": "8.35.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.35.0.tgz",
"integrity": "sha512-ceNNttjfmSEoM9PW87bWLDEIaLAyR+E6BoYJQ5PfaDau37UGca9Nyq3lBk8Bw2ad0AKvYabz6wxc7DMTO2jnNA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/typescript-estree": "8.35.0",
"@typescript-eslint/utils": "8.35.0",
"debug": "^4.3.4",
"ts-api-utils": "^2.1.0"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/typescript-eslint"
},
"peerDependencies": {
"eslint": "^8.57.0 || ^9.0.0",
"typescript": ">=4.8.4 <5.9.0"
}
},
"node_modules/typescript-eslint/node_modules/@typescript-eslint/types": {
"version": "8.35.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.35.0.tgz",
"integrity": "sha512-0mYH3emanku0vHw2aRLNGqe7EXh9WHEhi7kZzscrMDf6IIRUQ5Jk4wp1QrledE/36KtdZrVfKnE32eZCf/vaVQ==",
"dev": true,
"license": "MIT",
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/typescript-eslint"
}
},
"node_modules/typescript-eslint/node_modules/@typescript-eslint/typescript-estree": {
"version": "8.35.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.35.0.tgz",
"integrity": "sha512-F+BhnaBemgu1Qf8oHrxyw14wq6vbL8xwWKKMwTMwYIRmFFY/1n/9T/jpbobZL8vp7QyEUcC6xGrnAO4ua8Kp7w==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/project-service": "8.35.0",
"@typescript-eslint/tsconfig-utils": "8.35.0",
"@typescript-eslint/types": "8.35.0",
"@typescript-eslint/visitor-keys": "8.35.0",
"debug": "^4.3.4",
"fast-glob": "^3.3.2",
"is-glob": "^4.0.3",
"minimatch": "^9.0.4",
"semver": "^7.6.0",
"ts-api-utils": "^2.1.0"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/typescript-eslint"
},
"peerDependencies": {
"typescript": ">=4.8.4 <5.9.0"
}
},
"node_modules/typescript-eslint/node_modules/@typescript-eslint/utils": {
"version": "8.35.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.35.0.tgz",
"integrity": "sha512-nqoMu7WWM7ki5tPgLVsmPM8CkqtoPUG6xXGeefM5t4x3XumOEKMoUZPdi+7F+/EotukN4R9OWdmDxN80fqoZeg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@eslint-community/eslint-utils": "^4.7.0",
"@typescript-eslint/scope-manager": "8.35.0",
"@typescript-eslint/types": "8.35.0",
"@typescript-eslint/typescript-estree": "8.35.0"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/typescript-eslint"
},
"peerDependencies": {
"eslint": "^8.57.0 || ^9.0.0",
"typescript": ">=4.8.4 <5.9.0"
}
},
"node_modules/typescript-eslint/node_modules/@typescript-eslint/visitor-keys": {
"version": "8.35.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.35.0.tgz",
"integrity": "sha512-zTh2+1Y8ZpmeQaQVIc/ZZxsx8UzgKJyNg1PTvjzC7WMhPSVS8bfDX34k1SrwOf016qd5RU3az2UxUNue3IfQ5g==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/types": "8.35.0",
"eslint-visitor-keys": "^4.2.1"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/typescript-eslint"
}
},
"node_modules/typescript-eslint/node_modules/ignore": {
"version": "7.0.5",
"resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz",
"integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 4"
}
},
"node_modules/uc.micro": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-2.1.0.tgz",
@@ -17589,7 +17692,7 @@
},
"packages/a2a-server": {
"name": "@google/gemini-cli-a2a-server",
"version": "0.40.0-nightly.20260414.g5b1f7375a",
"version": "0.41.0-nightly.20260423.gaa05b4583",
"dependencies": {
"@a2a-js/sdk": "0.3.11",
"@google-cloud/storage": "^7.16.0",
@@ -17689,6 +17792,20 @@
"node": ">=18"
}
},
"packages/a2a-server/node_modules/typescript": {
"version": "5.9.3",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
"dev": true,
"license": "Apache-2.0",
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
},
"engines": {
"node": ">=14.17"
}
},
"packages/a2a-server/node_modules/uuid": {
"version": "13.0.0",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-13.0.0.tgz",
@@ -17704,7 +17821,7 @@
},
"packages/cli": {
"name": "@google/gemini-cli",
"version": "0.40.0-nightly.20260414.g5b1f7375a",
"version": "0.41.0-nightly.20260423.gaa05b4583",
"license": "Apache-2.0",
"dependencies": {
"@agentclientprotocol/sdk": "^0.16.1",
@@ -17836,9 +17953,23 @@
"node": ">=18"
}
},
"packages/cli/node_modules/typescript": {
"version": "5.9.3",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
"dev": true,
"license": "Apache-2.0",
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
},
"engines": {
"node": ">=14.17"
}
},
"packages/core": {
"name": "@google/gemini-cli-core",
"version": "0.40.0-nightly.20260414.g5b1f7375a",
"version": "0.41.0-nightly.20260423.gaa05b4583",
"license": "Apache-2.0",
"dependencies": {
"@a2a-js/sdk": "0.3.11",
@@ -17873,6 +18004,7 @@
"ajv": "^8.17.1",
"ajv-formats": "^3.0.0",
"chardet": "^2.1.0",
"chokidar": "^5.0.0",
"diff": "^8.0.3",
"dotenv": "^17.2.4",
"dotenv-expand": "^12.0.3",
@@ -18022,6 +18154,21 @@
"url": "https://github.com/sponsors/epoberezkin"
}
},
"packages/core/node_modules/chokidar": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-5.0.0.tgz",
"integrity": "sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw==",
"license": "MIT",
"dependencies": {
"readdirp": "^5.0.0"
},
"engines": {
"node": ">= 20.19.0"
},
"funding": {
"url": "https://paulmillr.com/funding/"
}
},
"packages/core/node_modules/dotenv": {
"version": "17.3.1",
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.3.1.tgz",
@@ -18090,6 +18237,33 @@
"url": "https://github.com/sponsors/jonschlinkert"
}
},
"packages/core/node_modules/readdirp": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-5.0.0.tgz",
"integrity": "sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ==",
"license": "MIT",
"engines": {
"node": ">= 20.19.0"
},
"funding": {
"type": "individual",
"url": "https://paulmillr.com/funding/"
}
},
"packages/core/node_modules/typescript": {
"version": "5.9.3",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
"dev": true,
"license": "Apache-2.0",
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
},
"engines": {
"node": ">=14.17"
}
},
"packages/core/node_modules/uuid": {
"version": "13.0.0",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-13.0.0.tgz",
@@ -18105,7 +18279,7 @@
},
"packages/devtools": {
"name": "@google/gemini-cli-devtools",
"version": "0.40.0-nightly.20260414.g5b1f7375a",
"version": "0.41.0-nightly.20260423.gaa05b4583",
"license": "Apache-2.0",
"dependencies": {
"ws": "^8.16.0"
@@ -18120,7 +18294,7 @@
},
"packages/sdk": {
"name": "@google/gemini-cli-sdk",
"version": "0.40.0-nightly.20260414.g5b1f7375a",
"version": "0.41.0-nightly.20260423.gaa05b4583",
"license": "Apache-2.0",
"dependencies": {
"@google/gemini-cli-core": "file:../core",
@@ -18135,9 +18309,23 @@
"node": ">=20"
}
},
"packages/sdk/node_modules/typescript": {
"version": "5.9.3",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
"dev": true,
"license": "Apache-2.0",
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
},
"engines": {
"node": ">=14.17"
}
},
"packages/test-utils": {
"name": "@google/gemini-cli-test-utils",
"version": "0.40.0-nightly.20260414.g5b1f7375a",
"version": "0.41.0-nightly.20260423.gaa05b4583",
"license": "Apache-2.0",
"dependencies": {
"@google/gemini-cli-core": "file:../core",
@@ -18153,9 +18341,23 @@
"node": ">=20"
}
},
"packages/test-utils/node_modules/typescript": {
"version": "5.9.3",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
"dev": true,
"license": "Apache-2.0",
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
},
"engines": {
"node": ">=14.17"
}
},
"packages/vscode-ide-companion": {
"name": "gemini-cli-vscode-ide-companion",
"version": "0.40.0-nightly.20260414.g5b1f7375a",
"version": "0.41.0-nightly.20260423.gaa05b4583",
"license": "LICENSE",
"dependencies": {
"@modelcontextprotocol/sdk": "^1.23.0",
@@ -18187,6 +18389,20 @@
"integrity": "sha512-30sjmas1hQ0gVbX68LAWlm/YYlEqUErunPJJKLpEl+xhK0mKn+jyzlCOpsdTwfkZfPy4U6CDkmygBLC3AB8W9Q==",
"dev": true,
"license": "MIT"
},
"packages/vscode-ide-companion/node_modules/typescript": {
"version": "5.9.3",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
"dev": true,
"license": "Apache-2.0",
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
},
"engines": {
"node": ">=14.17"
}
}
}
}
+5 -3
View File
@@ -1,6 +1,6 @@
{
"name": "@google/gemini-cli",
"version": "0.40.0-nightly.20260414.g5b1f7375a",
"version": "0.41.0-nightly.20260423.gaa05b4583",
"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.40.0-nightly.20260414.g5b1f7375a"
"sandboxImageUri": "us-docker.pkg.dev/gemini-code-dev/gemini-cli/sandbox:0.41.0-nightly.20260423.gaa05b4583"
},
"scripts": {
"start": "cross-env NODE_ENV=development node scripts/start.js",
@@ -62,7 +62,7 @@
"lint:ci": "npm run lint:all",
"lint:all": "node scripts/lint.js",
"format": "prettier --experimental-cli --write .",
"typecheck": "npm run typecheck --workspaces --if-present",
"typecheck": "npm run typecheck --workspaces --if-present && tsc -b evals/tsconfig.json integration-tests/tsconfig.json memory-tests/tsconfig.json",
"preflight": "npm run clean && npm ci && npm run format && npm run build && npm run lint:ci && npm run typecheck && npm run test:ci",
"prepare": "husky && npm run bundle",
"prepare:package": "node scripts/prepare-package.js",
@@ -94,6 +94,7 @@
],
"devDependencies": {
"@agentclientprotocol/sdk": "^0.16.1",
"read-package-up": "^11.0.0",
"@octokit/rest": "^22.0.0",
"@types/marked": "^5.0.2",
"@types/mime-types": "^3.0.1",
@@ -137,6 +138,7 @@
"strip-ansi": "^7.1.2",
"ts-prune": "^0.10.3",
"tsx": "^4.20.3",
"typescript": "^5.8.3",
"typescript-eslint": "^8.30.1",
"vitest": "^3.2.4",
"yargs": "^17.7.2"
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@google/gemini-cli-a2a-server",
"version": "0.40.0-nightly.20260414.g5b1f7375a",
"version": "0.41.0-nightly.20260423.gaa05b4583",
"description": "Gemini CLI A2A Server",
"repository": {
"type": "git",
@@ -98,6 +98,7 @@ export function createMockConfig(
getMcpServers: vi.fn().mockReturnValue({}),
}),
getTelemetryLogPromptsEnabled: vi.fn().mockReturnValue(false),
getTelemetryTracesEnabled: vi.fn().mockReturnValue(false),
getGitService: vi.fn(),
validatePathAccess: vi.fn().mockReturnValue(undefined),
getShellExecutionConfig: vi.fn().mockReturnValue({
+2 -2
View File
@@ -1,6 +1,6 @@
{
"name": "@google/gemini-cli",
"version": "0.40.0-nightly.20260414.g5b1f7375a",
"version": "0.41.0-nightly.20260423.gaa05b4583",
"description": "Gemini CLI",
"license": "Apache-2.0",
"repository": {
@@ -27,7 +27,7 @@
"dist"
],
"config": {
"sandboxImageUri": "us-docker.pkg.dev/gemini-code-dev/gemini-cli/sandbox:0.40.0-nightly.20260414.g5b1f7375a"
"sandboxImageUri": "us-docker.pkg.dev/gemini-code-dev/gemini-cli/sandbox:0.41.0-nightly.20260423.gaa05b4583"
},
"dependencies": {
"@agentclientprotocol/sdk": "^0.16.1",
+34
View File
@@ -41,6 +41,8 @@ import * as fs from 'node:fs/promises';
import * as path from 'node:path';
import { ApprovalMode } from '@google/gemini-cli-core/src/policy/types.js';
const startMemoryServiceMock = vi.hoisted(() => vi.fn());
vi.mock('../config/config.js', () => ({
loadCliConfig: vi.fn(),
}));
@@ -101,6 +103,7 @@ vi.mock(
const actual = await importOriginal();
return {
...actual,
startMemoryService: startMemoryServiceMock,
updatePolicy: vi.fn(),
createPolicyUpdater: vi.fn(),
ReadManyFilesTool: vi.fn(),
@@ -148,6 +151,8 @@ describe('GeminiAgent', () => {
let agent: GeminiAgent;
beforeEach(() => {
vi.clearAllMocks();
startMemoryServiceMock.mockResolvedValue(undefined);
mockConfig = {
refreshAuth: vi.fn(),
initialize: vi.fn(),
@@ -155,6 +160,7 @@ describe('GeminiAgent', () => {
getFileSystemService: vi.fn(),
setFileSystemService: vi.fn(),
getContentGeneratorConfig: vi.fn(),
isAutoMemoryEnabled: vi.fn().mockReturnValue(false),
getActiveModel: vi.fn().mockReturnValue('gemini-pro'),
getModel: vi.fn().mockReturnValue('gemini-pro'),
getGeminiClient: vi.fn().mockReturnValue({
@@ -354,6 +360,34 @@ describe('GeminiAgent', () => {
vi.useRealTimers();
});
it('should start auto memory for new ACP sessions when enabled', async () => {
mockConfig.getContentGeneratorConfig = vi.fn().mockReturnValue({
apiKey: 'test-key',
});
mockConfig.isAutoMemoryEnabled = vi.fn().mockReturnValue(true);
await agent.newSession({
cwd: '/tmp',
mcpServers: [],
});
expect(startMemoryServiceMock).toHaveBeenCalledWith(mockConfig);
});
it('should not start auto memory for new ACP sessions when disabled', async () => {
mockConfig.getContentGeneratorConfig = vi.fn().mockReturnValue({
apiKey: 'test-key',
});
mockConfig.isAutoMemoryEnabled = vi.fn().mockReturnValue(false);
await agent.newSession({
cwd: '/tmp',
mcpServers: [],
});
expect(startMemoryServiceMock).not.toHaveBeenCalled();
});
it('should return modes without plan mode when plan is disabled', async () => {
mockConfig.getContentGeneratorConfig = vi.fn().mockReturnValue({
apiKey: 'test-key',
+3
View File
@@ -76,6 +76,7 @@ import { randomUUID } from 'node:crypto';
import { loadCliConfig, type CliArgs } from '../config/config.js';
import { runExitCleanup } from '../utils/cleanup.js';
import { SessionSelector } from '../utils/sessionUtils.js';
import { startAutoMemoryIfEnabled } from '../utils/autoMemory.js';
import { CommandHandler } from './commandHandler.js';
@@ -324,6 +325,7 @@ export class GeminiAgent {
await config.initialize();
startupProfiler.flush(config);
startAutoMemoryIfEnabled(config);
const geminiClient = config.getGeminiClient();
const chat = await geminiClient.startChat();
@@ -465,6 +467,7 @@ export class GeminiAgent {
// which starts the MCP servers and other heavy resources.
await config.initialize();
startupProfiler.flush(config);
startAutoMemoryIfEnabled(config);
return config;
}
+1
View File
@@ -100,6 +100,7 @@ describe('GeminiAgent Session Resume', () => {
unsubscribe: vi.fn(),
},
getApprovalMode: vi.fn().mockReturnValue('default'),
isAutoMemoryEnabled: vi.fn().mockReturnValue(false),
isPlanEnabled: vi.fn().mockReturnValue(true),
getModel: vi.fn().mockReturnValue('gemini-pro'),
getHasAccessToPreviewModel: vi.fn().mockReturnValue(false),
+2 -2
View File
@@ -135,10 +135,10 @@ export class InboxMemoryCommand implements Command {
context: CommandContext,
_: string[],
): Promise<CommandExecutionResponse> {
if (!context.agentContext.config.isMemoryManagerEnabled()) {
if (!context.agentContext.config.isAutoMemoryEnabled()) {
return {
name: this.name,
data: 'The memory inbox requires the experimental memory manager. Enable it with: experimental.memoryManager = true in settings.',
data: 'The memory inbox requires Auto Memory. Enable it with: experimental.autoMemory = true in settings.',
};
}
+33
View File
@@ -0,0 +1,33 @@
/**
* @license
* Copyright 2026 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import type { CommandModule, Argv } from 'yargs';
import { initializeOutputListenersAndFlush } from '../gemini.js';
import { defer } from '../deferred.js';
import { setupCommand } from './gemma/setup.js';
import { startCommand } from './gemma/start.js';
import { stopCommand } from './gemma/stop.js';
import { statusCommand } from './gemma/status.js';
import { logsCommand } from './gemma/logs.js';
export const gemmaCommand: CommandModule = {
command: 'gemma',
describe: 'Manage local Gemma model routing',
builder: (yargs: Argv) =>
yargs
.middleware((argv) => {
initializeOutputListenersAndFlush();
argv['isCommand'] = true;
})
.command(defer(setupCommand, 'gemma'))
.command(defer(startCommand, 'gemma'))
.command(defer(stopCommand, 'gemma'))
.command(defer(statusCommand, 'gemma'))
.command(defer(logsCommand, 'gemma'))
.demandCommand(1, 'You need at least one command before continuing.')
.version(false),
handler: () => {},
};
@@ -0,0 +1,45 @@
/**
* @license
* Copyright 2026 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import path from 'node:path';
import { Storage } from '@google/gemini-cli-core';
export const LITERT_RELEASE_VERSION = 'v0.9.0-alpha03';
export const LITERT_RELEASE_BASE_URL =
'https://github.com/google-ai-edge/LiteRT-LM/releases/download';
export const GEMMA_MODEL_NAME = 'gemma3-1b-gpu-custom';
export const DEFAULT_PORT = 9379;
export const HEALTH_CHECK_TIMEOUT_MS = 5000;
export const LITERT_API_VERSION = 'v1beta';
export const SERVER_START_WAIT_MS = 3000;
export const PLATFORM_BINARY_MAP: Record<string, string> = {
'darwin-arm64': 'lit.macos_arm64',
'linux-x64': 'lit.linux_x86_64',
'win32-x64': 'lit.windows_x86_64.exe',
};
// SHA-256 hashes for the official LiteRT-LM v0.9.0-alpha03 release binaries.
export const PLATFORM_BINARY_SHA256: Record<string, string> = {
'lit.macos_arm64':
'9e826a2634f2e8b220ad0f1e1b5c139e0b47cb172326e3b7d46d31382f49478e',
'lit.linux_x86_64':
'66601df8a07f08244b188e9fcab0bf4a16562fe76d8d47e49f40273d57541ee8',
'lit.windows_x86_64.exe':
'de82d2829d2fb1cbdb318e2d8a78dc2f9659ff14cb11b2894d1f30e0bfde2bf6',
};
export function getLiteRtBinDir(): string {
return path.join(Storage.getGlobalGeminiDir(), 'bin', 'litert');
}
export function getPidFilePath(): string {
return path.join(Storage.getGlobalTempDir(), 'litert-server.pid');
}
export function getLogFilePath(): string {
return path.join(Storage.getGlobalTempDir(), 'litert-server.log');
}
@@ -0,0 +1,186 @@
/**
* @license
* Copyright 2026 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import fs from 'node:fs';
import type { ChildProcess } from 'node:child_process';
import { EventEmitter } from 'node:events';
import os from 'node:os';
import path from 'node:path';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { spawn } from 'node:child_process';
import { exitCli } from '../utils.js';
import { getLogFilePath } from './constants.js';
import { logsCommand, readLastLines } from './logs.js';
vi.mock('@google/gemini-cli-core', async (importOriginal) => {
const { mockCoreDebugLogger } = await import(
'../../test-utils/mockDebugLogger.js'
);
return mockCoreDebugLogger(
await importOriginal<typeof import('@google/gemini-cli-core')>(),
{
stripAnsi: false,
},
);
});
vi.mock('node:child_process', async (importOriginal) => {
const actual = await importOriginal<typeof import('node:child_process')>();
return {
...actual,
spawn: vi.fn(),
};
});
vi.mock('../utils.js', () => ({
exitCli: vi.fn(),
}));
vi.mock('./constants.js', () => ({
getLogFilePath: vi.fn(),
}));
function createMockChild(): ChildProcess {
return Object.assign(new EventEmitter(), {
kill: vi.fn(),
}) as unknown as ChildProcess;
}
async function flushMicrotasks() {
await Promise.resolve();
await Promise.resolve();
}
describe('readLastLines', () => {
const tempFiles: string[] = [];
afterEach(async () => {
await Promise.all(
tempFiles
.splice(0)
.map((filePath) => fs.promises.rm(filePath, { force: true })),
);
});
it('returns only the requested tail lines without reading the whole file eagerly', async () => {
const filePath = path.join(
os.tmpdir(),
`gemma-logs-${Date.now()}-${Math.random().toString(36).slice(2)}.log`,
);
tempFiles.push(filePath);
const content = Array.from({ length: 2000 }, (_, i) => `line-${i + 1}`)
.join('\n')
.concat('\n');
await fs.promises.writeFile(filePath, content, 'utf-8');
await expect(readLastLines(filePath, 3)).resolves.toBe(
'line-1998\nline-1999\nline-2000\n',
);
});
it('returns an empty string when zero lines are requested', async () => {
const filePath = path.join(
os.tmpdir(),
`gemma-logs-${Date.now()}-${Math.random().toString(36).slice(2)}.log`,
);
tempFiles.push(filePath);
await fs.promises.writeFile(filePath, 'line-1\nline-2\n', 'utf-8');
await expect(readLastLines(filePath, 0)).resolves.toBe('');
});
});
describe('logsCommand', () => {
const originalPlatform = process.platform;
beforeEach(() => {
vi.clearAllMocks();
Object.defineProperty(process, 'platform', {
value: 'linux',
configurable: true,
});
vi.mocked(getLogFilePath).mockReturnValue('/tmp/gemma.log');
vi.spyOn(fs.promises, 'access').mockResolvedValue(undefined);
});
afterEach(() => {
Object.defineProperty(process, 'platform', {
value: originalPlatform,
configurable: true,
});
vi.restoreAllMocks();
});
it('waits for the tail process to close before exiting in follow mode', async () => {
const child = createMockChild();
vi.mocked(spawn).mockReturnValue(child);
let resolved = false;
const handlerPromise = (
logsCommand.handler as (argv: Record<string, unknown>) => Promise<void>
)({}).then(() => {
resolved = true;
});
await flushMicrotasks();
expect(spawn).toHaveBeenCalledWith(
'tail',
['-f', '-n', '20', '/tmp/gemma.log'],
{ stdio: 'inherit' },
);
expect(resolved).toBe(false);
expect(exitCli).not.toHaveBeenCalled();
child.emit('close', 0);
await handlerPromise;
expect(exitCli).toHaveBeenCalledWith(0);
});
it('uses one-shot tail output when follow is disabled', async () => {
const child = createMockChild();
vi.mocked(spawn).mockReturnValue(child);
const handlerPromise = (
logsCommand.handler as (argv: Record<string, unknown>) => Promise<void>
)({ follow: false });
await flushMicrotasks();
expect(spawn).toHaveBeenCalledWith('tail', ['-n', '20', '/tmp/gemma.log'], {
stdio: 'inherit',
});
child.emit('close', 0);
await handlerPromise;
expect(exitCli).toHaveBeenCalledWith(0);
});
it('follows from the requested line count when both --lines and --follow are set', async () => {
const child = createMockChild();
vi.mocked(spawn).mockReturnValue(child);
const handlerPromise = (
logsCommand.handler as (argv: Record<string, unknown>) => Promise<void>
)({ lines: 5, follow: true });
await flushMicrotasks();
expect(spawn).toHaveBeenCalledWith(
'tail',
['-f', '-n', '5', '/tmp/gemma.log'],
{ stdio: 'inherit' },
);
child.emit('close', 0);
await handlerPromise;
expect(exitCli).toHaveBeenCalledWith(0);
});
});
+200
View File
@@ -0,0 +1,200 @@
/**
* @license
* Copyright 2026 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import type { CommandModule } from 'yargs';
import fs from 'node:fs';
import { spawn, type ChildProcess } from 'node:child_process';
import { debugLogger } from '@google/gemini-cli-core';
import { exitCli } from '../utils.js';
import { getLogFilePath } from './constants.js';
export async function readLastLines(
filePath: string,
count: number,
): Promise<string> {
if (count <= 0) {
return '';
}
const CHUNK_SIZE = 64 * 1024;
const fileHandle = await fs.promises.open(filePath, fs.constants.O_RDONLY);
try {
const stats = await fileHandle.stat();
if (stats.size === 0) {
return '';
}
const chunks: Buffer[] = [];
let totalBytes = 0;
let newlineCount = 0;
let position = stats.size;
while (position > 0 && newlineCount <= count) {
const readSize = Math.min(CHUNK_SIZE, position);
position -= readSize;
const buffer = Buffer.allocUnsafe(readSize);
const { bytesRead } = await fileHandle.read(
buffer,
0,
readSize,
position,
);
if (bytesRead === 0) {
break;
}
const chunk =
bytesRead === readSize ? buffer : buffer.subarray(0, bytesRead);
chunks.unshift(chunk);
totalBytes += chunk.length;
for (const byte of chunk) {
if (byte === 0x0a) {
newlineCount += 1;
}
}
}
const content = Buffer.concat(chunks, totalBytes).toString('utf-8');
const lines = content.split('\n');
if (position > 0 && lines.length > 0) {
const boundary = Buffer.allocUnsafe(1);
const { bytesRead } = await fileHandle.read(boundary, 0, 1, position - 1);
if (bytesRead === 1 && boundary[0] !== 0x0a) {
lines.shift();
}
}
if (lines.length > 0 && lines[lines.length - 1] === '') {
lines.pop();
}
if (lines.length === 0) {
return '';
}
return lines.slice(-count).join('\n') + '\n';
} finally {
await fileHandle.close();
}
}
interface LogsArgs {
lines?: number;
follow?: boolean;
}
function waitForChild(child: ChildProcess): Promise<number> {
return new Promise((resolve, reject) => {
child.once('error', reject);
child.once('close', (code) => resolve(code ?? 1));
});
}
async function runTail(logPath: string, lines: number, follow: boolean) {
const tailArgs = follow
? ['-f', '-n', String(lines), logPath]
: ['-n', String(lines), logPath];
const child = spawn('tail', tailArgs, { stdio: 'inherit' });
if (!follow) {
return waitForChild(child);
}
const handleSigint = () => {
child.kill('SIGTERM');
};
process.once('SIGINT', handleSigint);
try {
return await waitForChild(child);
} finally {
process.off('SIGINT', handleSigint);
}
}
export const logsCommand: CommandModule<object, LogsArgs> = {
command: 'logs',
describe: 'View LiteRT-LM server logs',
builder: (yargs) =>
yargs
.option('lines', {
alias: 'n',
type: 'number',
description: 'Show the last N lines and exit (omit to follow live)',
})
.option('follow', {
alias: 'f',
type: 'boolean',
description:
'Follow log output (defaults to true when --lines is omitted)',
}),
handler: async (argv) => {
const logPath = getLogFilePath();
try {
await fs.promises.access(logPath, fs.constants.F_OK);
} catch {
debugLogger.log(`No log file found at ${logPath}`);
debugLogger.log(
'Is the LiteRT server running? Start it with: gemini gemma start',
);
await exitCli(1);
return;
}
const lines = argv.lines;
const follow = argv.follow ?? lines === undefined;
const requestedLines = lines ?? 20;
if (follow && process.platform === 'win32') {
debugLogger.log(
'Live log following is not supported on Windows. Use --lines N to view recent logs.',
);
await exitCli(1);
return;
}
if (process.platform === 'win32') {
process.stdout.write(await readLastLines(logPath, requestedLines));
await exitCli(0);
return;
}
try {
if (follow) {
debugLogger.log(`Tailing ${logPath} (Ctrl+C to stop)\n`);
}
const exitCode = await runTail(logPath, requestedLines, follow);
await exitCli(exitCode);
} catch (error) {
if (
error instanceof Error &&
'code' in error &&
error.code === 'ENOENT'
) {
if (!follow) {
process.stdout.write(await readLastLines(logPath, requestedLines));
await exitCli(0);
} else {
debugLogger.error(
'"tail" command not found. Use --lines N to view recent logs without tail.',
);
await exitCli(1);
}
} else {
debugLogger.error(
`Failed to read log output: ${error instanceof Error ? error.message : String(error)}`,
);
await exitCli(1);
}
}
},
};
@@ -0,0 +1,162 @@
/**
* @license
* Copyright 2026 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import fs from 'node:fs';
import path from 'node:path';
import { beforeEach, describe, expect, it, vi } from 'vitest';
import { SettingScope } from '../../config/settings.js';
import { getLiteRtBinDir } from './constants.js';
const mockLoadSettings = vi.hoisted(() => vi.fn());
vi.mock('../../config/settings.js', () => ({
loadSettings: mockLoadSettings,
SettingScope: {
User: 'User',
},
}));
import {
getBinaryPath,
isExpectedLiteRtServerCommand,
isBinaryInstalled,
readServerProcessInfo,
resolveGemmaConfig,
} from './platform.js';
describe('gemma platform helpers', () => {
function createMockSettings(
userGemmaSettings?: object,
mergedGemmaSettings?: object,
) {
return {
merged: {
experimental: {
gemmaModelRouter: mergedGemmaSettings,
},
},
forScope: vi.fn((scope: SettingScope) => {
if (scope !== SettingScope.User) {
throw new Error(`Unexpected scope ${scope}`);
}
return {
settings: {
experimental: {
gemmaModelRouter: userGemmaSettings,
},
},
};
}),
};
}
beforeEach(() => {
vi.clearAllMocks();
mockLoadSettings.mockReturnValue(createMockSettings());
});
it('prefers the configured binary path from settings', () => {
mockLoadSettings.mockReturnValue(
createMockSettings({ binaryPath: '/custom/lit' }),
);
expect(getBinaryPath('lit.test')).toBe('/custom/lit');
});
it('ignores workspace overrides for the configured binary path', () => {
mockLoadSettings.mockReturnValue(
createMockSettings(
{ binaryPath: '/user/lit' },
{ binaryPath: '/workspace/evil' },
),
);
expect(getBinaryPath('lit.test')).toBe('/user/lit');
});
it('falls back to the default install location when no custom path is set', () => {
expect(getBinaryPath('lit.test')).toBe(
path.join(getLiteRtBinDir(), 'lit.test'),
);
});
it('resolves the configured port and binary path from settings', () => {
mockLoadSettings.mockReturnValue(
createMockSettings(
{ binaryPath: '/custom/lit' },
{
enabled: true,
classifier: {
host: 'http://localhost:8123/v1beta',
},
},
),
);
expect(resolveGemmaConfig(9379)).toEqual({
settingsEnabled: true,
configuredPort: 8123,
configuredBinaryPath: '/custom/lit',
});
});
it('checks binary installation using the resolved binary path', () => {
mockLoadSettings.mockReturnValue(
createMockSettings({ binaryPath: '/custom/lit' }),
);
vi.spyOn(fs, 'existsSync').mockReturnValue(true);
expect(isBinaryInstalled()).toBe(true);
expect(fs.existsSync).toHaveBeenCalledWith('/custom/lit');
});
it('parses structured server process info from the pid file', () => {
vi.spyOn(fs, 'readFileSync').mockReturnValue(
JSON.stringify({
pid: 1234,
binaryPath: '/custom/lit',
port: 8123,
}),
);
expect(readServerProcessInfo()).toEqual({
pid: 1234,
binaryPath: '/custom/lit',
port: 8123,
});
});
it('parses legacy pid-only files for backward compatibility', () => {
vi.spyOn(fs, 'readFileSync').mockReturnValue('4321');
expect(readServerProcessInfo()).toEqual({
pid: 4321,
});
});
it('matches only the expected LiteRT serve command', () => {
expect(
isExpectedLiteRtServerCommand('/custom/lit serve --port=8123 --verbose', {
binaryPath: '/custom/lit',
port: 8123,
}),
).toBe(true);
expect(
isExpectedLiteRtServerCommand('/custom/lit run --port=8123', {
binaryPath: '/custom/lit',
port: 8123,
}),
).toBe(false);
expect(
isExpectedLiteRtServerCommand('/custom/lit serve --port=9000', {
binaryPath: '/custom/lit',
port: 8123,
}),
).toBe(false);
});
});
+316
View File
@@ -0,0 +1,316 @@
/**
* @license
* Copyright 2026 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import { loadSettings, SettingScope } from '../../config/settings.js';
import fs from 'node:fs';
import path from 'node:path';
import { execFileSync } from 'node:child_process';
import {
PLATFORM_BINARY_MAP,
LITERT_RELEASE_BASE_URL,
LITERT_RELEASE_VERSION,
getLiteRtBinDir,
GEMMA_MODEL_NAME,
HEALTH_CHECK_TIMEOUT_MS,
LITERT_API_VERSION,
getPidFilePath,
} from './constants.js';
export interface PlatformInfo {
key: string;
binaryName: string;
}
export interface GemmaConfigStatus {
settingsEnabled: boolean;
configuredPort: number;
configuredBinaryPath?: string;
}
export interface LiteRtServerProcessInfo {
pid: number;
binaryPath?: string;
port?: number;
}
function getUserConfiguredBinaryPath(
workspaceDir = process.cwd(),
): string | undefined {
try {
const userGemmaSettings = loadSettings(workspaceDir).forScope(
SettingScope.User,
).settings.experimental?.gemmaModelRouter;
return userGemmaSettings?.binaryPath?.trim() || undefined;
} catch {
return undefined;
}
}
function parsePortFromHost(
host: string | undefined,
fallbackPort: number,
): number {
if (!host) {
return fallbackPort;
}
try {
const url = new URL(host);
const port = Number(url.port);
return Number.isFinite(port) && port > 0 ? port : fallbackPort;
} catch {
const match = host.match(/:(\d+)/);
if (!match) {
return fallbackPort;
}
const port = parseInt(match[1], 10);
return Number.isFinite(port) && port > 0 ? port : fallbackPort;
}
}
export function resolveGemmaConfig(fallbackPort: number): GemmaConfigStatus {
let settingsEnabled = false;
let configuredPort = fallbackPort;
const configuredBinaryPath = getUserConfiguredBinaryPath();
try {
const settings = loadSettings(process.cwd());
const gemmaSettings = settings.merged.experimental?.gemmaModelRouter;
settingsEnabled = gemmaSettings?.enabled === true;
configuredPort = parsePortFromHost(
gemmaSettings?.classifier?.host,
fallbackPort,
);
} catch {
// ignore — settings may fail to load outside a workspace
}
return { settingsEnabled, configuredPort, configuredBinaryPath };
}
export function detectPlatform(): PlatformInfo | null {
const key = `${process.platform}-${process.arch}`;
const binaryName = PLATFORM_BINARY_MAP[key];
if (!binaryName) {
return null;
}
return { key, binaryName };
}
export function getBinaryPath(binaryName?: string): string | null {
const configuredBinaryPath = getUserConfiguredBinaryPath();
if (configuredBinaryPath) {
return configuredBinaryPath;
}
const name = binaryName ?? detectPlatform()?.binaryName;
if (!name) return null;
return path.join(getLiteRtBinDir(), name);
}
export function getBinaryDownloadUrl(binaryName: string): string {
return `${LITERT_RELEASE_BASE_URL}/${LITERT_RELEASE_VERSION}/${binaryName}`;
}
export function isBinaryInstalled(binaryPath = getBinaryPath()): boolean {
if (!binaryPath) return false;
return fs.existsSync(binaryPath);
}
export function isModelDownloaded(binaryPath: string): boolean {
try {
const output = execFileSync(binaryPath, ['list'], {
encoding: 'utf-8',
timeout: 10000,
});
return output.includes(GEMMA_MODEL_NAME);
} catch {
return false;
}
}
export async function isServerRunning(port: number): Promise<boolean> {
try {
const controller = new AbortController();
const timeout = setTimeout(
() => controller.abort(),
HEALTH_CHECK_TIMEOUT_MS,
);
const response = await fetch(
`http://localhost:${port}/${LITERT_API_VERSION}/models/${GEMMA_MODEL_NAME}:generateContent`,
{ method: 'POST', signal: controller.signal },
);
clearTimeout(timeout);
// A 400 (bad request) confirms the route exists — the server recognises
// the model endpoint. Only a 404 means "wrong server / wrong model".
return response.status !== 404;
} catch {
return false;
}
}
function isLiteRtServerProcessInfo(
value: unknown,
): value is LiteRtServerProcessInfo {
if (!value || typeof value !== 'object') {
return false;
}
const isPositiveInteger = (candidate: unknown): candidate is number =>
typeof candidate === 'number' &&
Number.isInteger(candidate) &&
candidate > 0;
const isNonEmptyString = (candidate: unknown): candidate is string =>
typeof candidate === 'string' && candidate.length > 0;
const pid: unknown = Object.getOwnPropertyDescriptor(value, 'pid')?.value;
if (!isPositiveInteger(pid)) {
return false;
}
const binaryPath: unknown = Object.getOwnPropertyDescriptor(
value,
'binaryPath',
)?.value;
if (binaryPath !== undefined && !isNonEmptyString(binaryPath)) {
return false;
}
const port: unknown = Object.getOwnPropertyDescriptor(value, 'port')?.value;
if (port !== undefined && !isPositiveInteger(port)) {
return false;
}
return true;
}
export function readServerProcessInfo(): LiteRtServerProcessInfo | null {
const pidPath = getPidFilePath();
try {
const content = fs.readFileSync(pidPath, 'utf-8').trim();
if (!content) {
return null;
}
if (/^\d+$/.test(content)) {
return { pid: parseInt(content, 10) };
}
const parsed = JSON.parse(content) as unknown;
return isLiteRtServerProcessInfo(parsed) ? parsed : null;
} catch {
return null;
}
}
export function writeServerProcessInfo(
processInfo: LiteRtServerProcessInfo,
): void {
fs.writeFileSync(getPidFilePath(), JSON.stringify(processInfo), 'utf-8');
}
export function readServerPid(): number | null {
return readServerProcessInfo()?.pid ?? null;
}
function normalizeProcessValue(value: string): string {
const normalized = value.replace(/\0/g, ' ').trim();
if (process.platform === 'win32') {
return normalized.replace(/\\/g, '/').replace(/\s+/g, ' ').toLowerCase();
}
return normalized.replace(/\s+/g, ' ');
}
function readProcessCommandLine(pid: number): string | null {
try {
if (process.platform === 'linux') {
const output = fs.readFileSync(`/proc/${pid}/cmdline`, 'utf-8');
return output.trim() ? output : null;
}
if (process.platform === 'win32') {
const output = execFileSync(
'powershell.exe',
[
'-NoProfile',
'-Command',
`(Get-CimInstance Win32_Process -Filter "ProcessId = ${pid}").CommandLine`,
],
{
encoding: 'utf-8',
timeout: 5000,
},
);
return output.trim() || null;
}
const output = execFileSync('ps', ['-p', String(pid), '-o', 'command='], {
encoding: 'utf-8',
timeout: 5000,
});
return output.trim() || null;
} catch {
return null;
}
}
export function isExpectedLiteRtServerCommand(
commandLine: string,
options: {
binaryPath?: string | null;
port?: number;
},
): boolean {
const normalizedCommandLine = normalizeProcessValue(commandLine);
if (!normalizedCommandLine) {
return false;
}
if (!/(^|\s|")serve(\s|$)/.test(normalizedCommandLine)) {
return false;
}
if (
options.port !== undefined &&
!normalizedCommandLine.includes(`--port=${options.port}`)
) {
return false;
}
if (!options.binaryPath) {
return true;
}
const normalizedBinaryPath = normalizeProcessValue(options.binaryPath);
const normalizedBinaryName = normalizeProcessValue(
path.basename(options.binaryPath),
);
return (
normalizedCommandLine.includes(normalizedBinaryPath) ||
normalizedCommandLine.includes(normalizedBinaryName)
);
}
export function isExpectedLiteRtServerProcess(
pid: number,
options: {
binaryPath?: string | null;
port?: number;
},
): boolean {
const commandLine = readProcessCommandLine(pid);
if (!commandLine) {
return false;
}
return isExpectedLiteRtServerCommand(commandLine, options);
}
export function isProcessRunning(pid: number): boolean {
try {
process.kill(pid, 0);
return true;
} catch {
return false;
}
}
@@ -0,0 +1,60 @@
/**
* @license
* Copyright 2026 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import fs from 'node:fs';
import os from 'node:os';
import path from 'node:path';
import { afterEach, describe, expect, it } from 'vitest';
import { PLATFORM_BINARY_MAP, PLATFORM_BINARY_SHA256 } from './constants.js';
import { computeFileSha256, verifyFileSha256 } from './setup.js';
describe('gemma setup checksum helpers', () => {
const tempFiles: string[] = [];
afterEach(async () => {
await Promise.all(
tempFiles
.splice(0)
.map((filePath) => fs.promises.rm(filePath, { force: true })),
);
});
it('has a pinned checksum for every supported LiteRT binary', () => {
expect(Object.keys(PLATFORM_BINARY_SHA256).sort()).toEqual(
Object.values(PLATFORM_BINARY_MAP).sort(),
);
});
it('computes the sha256 for a downloaded file', async () => {
const filePath = path.join(
os.tmpdir(),
`gemma-setup-${Date.now()}-${Math.random().toString(36).slice(2)}`,
);
tempFiles.push(filePath);
await fs.promises.writeFile(filePath, 'hello world', 'utf-8');
await expect(computeFileSha256(filePath)).resolves.toBe(
'b94d27b9934d3e08a52e52d7da7dabfac484efe37a5380ee9088f7ace2efcde9',
);
});
it('verifies whether a file matches the expected sha256', async () => {
const filePath = path.join(
os.tmpdir(),
`gemma-setup-${Date.now()}-${Math.random().toString(36).slice(2)}`,
);
tempFiles.push(filePath);
await fs.promises.writeFile(filePath, 'hello world', 'utf-8');
await expect(
verifyFileSha256(
filePath,
'b94d27b9934d3e08a52e52d7da7dabfac484efe37a5380ee9088f7ace2efcde9',
),
).resolves.toBe(true);
await expect(verifyFileSha256(filePath, 'deadbeef')).resolves.toBe(false);
});
});
+504
View File
@@ -0,0 +1,504 @@
/**
* @license
* Copyright 2026 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import type { CommandModule } from 'yargs';
import { createHash } from 'node:crypto';
import fs from 'node:fs';
import path from 'node:path';
import { execFileSync, spawn as nodeSpawn } from 'node:child_process';
import chalk from 'chalk';
import { debugLogger } from '@google/gemini-cli-core';
import { loadSettings, SettingScope } from '../../config/settings.js';
import { exitCli } from '../utils.js';
import {
DEFAULT_PORT,
GEMMA_MODEL_NAME,
PLATFORM_BINARY_SHA256,
} from './constants.js';
import {
detectPlatform,
getBinaryDownloadUrl,
getBinaryPath,
isBinaryInstalled,
isModelDownloaded,
} from './platform.js';
import { startServer } from './start.js';
import readline from 'node:readline';
const log = (msg: string) => debugLogger.log(msg);
const logError = (msg: string) => debugLogger.error(msg);
async function promptYesNo(question: string): Promise<boolean> {
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout,
});
return new Promise((resolve) => {
rl.question(`${question} (y/N): `, (answer) => {
rl.close();
resolve(
answer.trim().toLowerCase() === 'y' ||
answer.trim().toLowerCase() === 'yes',
);
});
});
}
function formatBytes(bytes: number): string {
if (bytes < 1024) return `${bytes} B`;
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
}
function renderProgress(downloaded: number, total: number | null): void {
const barWidth = 30;
if (total && total > 0) {
const pct = Math.min(downloaded / total, 1);
const filled = Math.round(barWidth * pct);
const bar = '█'.repeat(filled) + '░'.repeat(barWidth - filled);
const pctStr = (pct * 100).toFixed(0).padStart(3);
process.stderr.write(
`\r [${bar}] ${pctStr}% ${formatBytes(downloaded)} / ${formatBytes(total)}`,
);
} else {
process.stderr.write(`\r Downloaded ${formatBytes(downloaded)}`);
}
}
async function downloadFile(url: string, destPath: string): Promise<void> {
const tmpPath = destPath + '.downloading';
if (fs.existsSync(tmpPath)) {
fs.unlinkSync(tmpPath);
}
const response = await fetch(url, { redirect: 'follow' });
if (!response.ok) {
throw new Error(
`Download failed: HTTP ${response.status} ${response.statusText}`,
);
}
if (!response.body) {
throw new Error('Download failed: No response body');
}
const contentLength = response.headers.get('content-length');
const totalBytes = contentLength ? parseInt(contentLength, 10) : null;
let downloadedBytes = 0;
const fileStream = fs.createWriteStream(tmpPath);
const reader = response.body.getReader();
try {
for (;;) {
const { done, value } = await reader.read();
if (done) break;
const writeOk = fileStream.write(value);
if (!writeOk) {
await new Promise<void>((resolve) => fileStream.once('drain', resolve));
}
downloadedBytes += value.byteLength;
renderProgress(downloadedBytes, totalBytes);
}
} finally {
fileStream.end();
process.stderr.write('\r' + ' '.repeat(80) + '\r');
}
await new Promise<void>((resolve, reject) => {
fileStream.on('finish', resolve);
fileStream.on('error', reject);
});
fs.renameSync(tmpPath, destPath);
}
export async function computeFileSha256(filePath: string): Promise<string> {
const hash = createHash('sha256');
const fileStream = fs.createReadStream(filePath);
return new Promise((resolve, reject) => {
fileStream.on('data', (chunk) => {
hash.update(chunk);
});
fileStream.on('error', reject);
fileStream.on('end', () => {
resolve(hash.digest('hex'));
});
});
}
export async function verifyFileSha256(
filePath: string,
expectedHash: string,
): Promise<boolean> {
const actualHash = await computeFileSha256(filePath);
return actualHash === expectedHash;
}
function spawnInherited(command: string, args: string[]): Promise<number> {
return new Promise((resolve, reject) => {
const child = nodeSpawn(command, args, {
stdio: 'inherit',
});
child.on('close', (code) => resolve(code ?? 1));
child.on('error', reject);
});
}
interface SetupArgs {
port: number;
skipModel: boolean;
start: boolean;
force: boolean;
consent: boolean;
}
async function handleSetup(argv: SetupArgs): Promise<number> {
const { port, force } = argv;
let settingsUpdated = false;
let serverStarted = false;
let autoStartServer = true;
log('');
log(chalk.bold('Gemma Local Model Routing Setup'));
log(chalk.dim('─'.repeat(40)));
log('');
const platform = detectPlatform();
if (!platform) {
logError(
chalk.red(`Unsupported platform: ${process.platform}-${process.arch}`),
);
logError(
'LiteRT-LM binaries are available for: macOS (ARM64), Linux (x86_64), Windows (x86_64)',
);
return 1;
}
log(chalk.dim(` Platform: ${platform.key}${platform.binaryName}`));
if (!argv.consent) {
log('');
log('This will download and install the LiteRT-LM runtime and the');
log(
`Gemma model (${GEMMA_MODEL_NAME}, ~1 GB). By proceeding, you agree to the`,
);
log('Gemma Terms of Use: https://ai.google.dev/gemma/terms');
log('');
const accepted = await promptYesNo('Do you want to continue?');
if (!accepted) {
log('Setup cancelled.');
return 0;
}
}
const binaryPath = getBinaryPath(platform.binaryName)!;
const alreadyInstalled = isBinaryInstalled();
if (alreadyInstalled && !force) {
log('');
log(chalk.green(' ✓ LiteRT-LM binary already installed at:'));
log(chalk.dim(` ${binaryPath}`));
} else {
log('');
log(' Downloading LiteRT-LM binary...');
const downloadUrl = getBinaryDownloadUrl(platform.binaryName);
debugLogger.log(`Downloading from: ${downloadUrl}`);
try {
const binDir = path.dirname(binaryPath);
fs.mkdirSync(binDir, { recursive: true });
await downloadFile(downloadUrl, binaryPath);
log(chalk.green(' ✓ Binary downloaded successfully'));
} catch (error) {
logError(
chalk.red(
` ✗ Failed to download binary: ${error instanceof Error ? error.message : String(error)}`,
),
);
logError(' Check your internet connection and try again.');
return 1;
}
const expectedHash = PLATFORM_BINARY_SHA256[platform.binaryName];
if (!expectedHash) {
logError(
chalk.red(
` ✗ No checksum is configured for ${platform.binaryName}. Refusing to install the binary.`,
),
);
try {
fs.rmSync(binaryPath, { force: true });
} catch {
// ignore
}
return 1;
}
try {
const checksumVerified = await verifyFileSha256(binaryPath, expectedHash);
if (!checksumVerified) {
logError(
chalk.red(
' ✗ Downloaded binary checksum did not match the expected release hash.',
),
);
try {
fs.rmSync(binaryPath, { force: true });
} catch {
// ignore
}
return 1;
}
log(chalk.green(' ✓ Binary checksum verified'));
} catch (error) {
logError(
chalk.red(
` ✗ Failed to verify binary checksum: ${error instanceof Error ? error.message : String(error)}`,
),
);
try {
fs.rmSync(binaryPath, { force: true });
} catch {
// ignore
}
return 1;
}
if (process.platform !== 'win32') {
try {
fs.chmodSync(binaryPath, 0o755);
} catch (error) {
logError(
chalk.red(
` ✗ Failed to set executable permission: ${error instanceof Error ? error.message : String(error)}`,
),
);
return 1;
}
}
if (process.platform === 'darwin') {
try {
execFileSync('xattr', ['-d', 'com.apple.quarantine', binaryPath], {
stdio: 'ignore',
});
log(chalk.green(' ✓ macOS quarantine attribute removed'));
} catch {
// Expected if the attribute doesn't exist.
}
}
}
if (!argv.skipModel) {
const modelAlreadyDownloaded = isModelDownloaded(binaryPath);
if (modelAlreadyDownloaded && !force) {
log('');
log(chalk.green(` ✓ Model ${GEMMA_MODEL_NAME} already downloaded`));
} else {
log('');
log(` Downloading model ${GEMMA_MODEL_NAME}...`);
log(chalk.dim(' You may be prompted to accept the Gemma Terms of Use.'));
log('');
const exitCode = await spawnInherited(binaryPath, [
'pull',
GEMMA_MODEL_NAME,
]);
if (exitCode !== 0) {
logError('');
logError(
chalk.red(` ✗ Model download failed (exit code ${exitCode})`),
);
return 1;
}
log('');
log(chalk.green(` ✓ Model ${GEMMA_MODEL_NAME} downloaded`));
}
}
log('');
log(' Configuring settings...');
try {
const settings = loadSettings(process.cwd());
// User scope: security-sensitive settings that must not be overridable
// by workspace configs (prevents arbitrary binary execution).
const existingUserGemma =
settings.forScope(SettingScope.User).settings.experimental
?.gemmaModelRouter ?? {};
autoStartServer = existingUserGemma.autoStartServer ?? true;
const existingUserExperimental =
settings.forScope(SettingScope.User).settings.experimental ?? {};
settings.setValue(SettingScope.User, 'experimental', {
...existingUserExperimental,
gemmaModelRouter: {
autoStartServer,
...(existingUserGemma.binaryPath !== undefined
? { binaryPath: existingUserGemma.binaryPath }
: {}),
},
});
// Workspace scope: project-isolated settings so the local model only
// runs for this specific project, saving resources globally.
const existingWorkspaceGemma =
settings.forScope(SettingScope.Workspace).settings.experimental
?.gemmaModelRouter ?? {};
const existingWorkspaceExperimental =
settings.forScope(SettingScope.Workspace).settings.experimental ?? {};
settings.setValue(SettingScope.Workspace, 'experimental', {
...existingWorkspaceExperimental,
gemmaModelRouter: {
...existingWorkspaceGemma,
enabled: true,
classifier: {
...existingWorkspaceGemma.classifier,
host: `http://localhost:${port}`,
model: GEMMA_MODEL_NAME,
},
},
});
log(chalk.green(' ✓ Settings updated'));
log(chalk.dim(' User (~/.gemini/settings.json): autoStartServer'));
log(
chalk.dim(' Workspace (.gemini/settings.json): enabled, classifier'),
);
settingsUpdated = true;
} catch (error) {
logError(
chalk.red(
` ✗ Failed to update settings: ${error instanceof Error ? error.message : String(error)}`,
),
);
logError(
' You can manually add the configuration to ~/.gemini/settings.json',
);
}
if (argv.start) {
log('');
log(' Starting LiteRT server...');
serverStarted = await startServer(binaryPath, port);
if (serverStarted) {
log(chalk.green(` ✓ Server started on port ${port}`));
} else {
log(
chalk.yellow(
` ! Server may not have started correctly. Check: gemini gemma status`,
),
);
}
}
const routingActive = settingsUpdated && serverStarted;
const setupSucceeded = settingsUpdated && (!argv.start || serverStarted);
log('');
log(chalk.dim('─'.repeat(40)));
if (routingActive) {
log(chalk.bold.green(' Setup complete! Local model routing is active.'));
} else if (settingsUpdated) {
log(
chalk.bold.green(' Setup complete! Local model routing is configured.'),
);
} else {
log(
chalk.bold.yellow(
' Setup incomplete. Manual settings changes are still required.',
),
);
}
log('');
log(' How it works: Every request is classified by the local Gemma model.');
log(
' Simple tasks (file reads, quick edits) route to ' +
chalk.cyan('Flash') +
' for speed.',
);
log(
' Complex tasks (debugging, architecture) route to ' +
chalk.cyan('Pro') +
' for quality.',
);
log(' This happens automatically — just use the CLI as usual.');
log('');
if (!settingsUpdated) {
log(
chalk.yellow(
' Fix the settings update above, then rerun "gemini gemma status".',
),
);
log('');
} else if (!argv.start) {
log(chalk.yellow(' Note: Run "gemini gemma start" to start the server.'));
if (autoStartServer) {
log(
chalk.yellow(
' Or restart the CLI to auto-start it on the next launch.',
),
);
}
log('');
} else if (!serverStarted) {
log(
chalk.yellow(
' Review the server logs and rerun "gemini gemma start" after fixing the issue.',
),
);
log('');
}
log(' Useful commands:');
log(chalk.dim(' gemini gemma status Check routing status'));
log(chalk.dim(' gemini gemma start Start the LiteRT server'));
log(chalk.dim(' gemini gemma stop Stop the LiteRT server'));
log(chalk.dim(' /gemma Check status inside a session'));
log('');
return setupSucceeded ? 0 : 1;
}
export const setupCommand: CommandModule = {
command: 'setup',
describe: 'Download and configure Gemma local model routing',
builder: (yargs) =>
yargs
.option('port', {
type: 'number',
default: DEFAULT_PORT,
description: 'Port for the LiteRT server',
})
.option('skip-model', {
type: 'boolean',
default: false,
description: 'Skip model download (binary only)',
})
.option('start', {
type: 'boolean',
default: true,
description: 'Start the server after setup',
})
.option('force', {
type: 'boolean',
default: false,
description: 'Re-download binary and model even if already present',
})
.option('consent', {
type: 'boolean',
default: false,
description: 'Skip interactive consent prompt (implies acceptance)',
}),
handler: async (argv) => {
const exitCode = await handleSetup({
port: Number(argv['port']),
skipModel: Boolean(argv['skipModel']),
start: Boolean(argv['start']),
force: Boolean(argv['force']),
consent: Boolean(argv['consent']),
});
await exitCli(exitCode);
},
};
+123
View File
@@ -0,0 +1,123 @@
/**
* @license
* Copyright 2026 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import type { CommandModule } from 'yargs';
import fs from 'node:fs';
import path from 'node:path';
import { spawn } from 'node:child_process';
import chalk from 'chalk';
import { debugLogger } from '@google/gemini-cli-core';
import { exitCli } from '../utils.js';
import {
DEFAULT_PORT,
getPidFilePath,
getLogFilePath,
getLiteRtBinDir,
SERVER_START_WAIT_MS,
} from './constants.js';
import {
getBinaryPath,
isBinaryInstalled,
isServerRunning,
resolveGemmaConfig,
writeServerProcessInfo,
} from './platform.js';
export async function startServer(
binaryPath: string,
port: number,
): Promise<boolean> {
const alreadyRunning = await isServerRunning(port);
if (alreadyRunning) {
debugLogger.log(`LiteRT server already running on port ${port}`);
return true;
}
const logPath = getLogFilePath();
fs.mkdirSync(getLiteRtBinDir(), { recursive: true });
const tmpDir = path.dirname(getPidFilePath());
fs.mkdirSync(tmpDir, { recursive: true });
const logFd = fs.openSync(logPath, 'a');
try {
const child = spawn(binaryPath, ['serve', `--port=${port}`, '--verbose'], {
detached: true,
stdio: ['ignore', logFd, logFd],
});
if (child.pid) {
writeServerProcessInfo({
pid: child.pid,
binaryPath,
port,
});
}
child.unref();
} finally {
fs.closeSync(logFd);
}
await new Promise((resolve) => setTimeout(resolve, SERVER_START_WAIT_MS));
return isServerRunning(port);
}
export const startCommand: CommandModule = {
command: 'start',
describe: 'Start the LiteRT-LM server',
builder: (yargs) =>
yargs.option('port', {
type: 'number',
description: 'Port for the LiteRT server',
}),
handler: async (argv) => {
let port: number | undefined;
if (argv['port'] !== undefined) {
port = Number(argv['port']);
}
if (!port) {
const { configuredPort } = resolveGemmaConfig(DEFAULT_PORT);
port = configuredPort;
}
const binaryPath = getBinaryPath();
if (!binaryPath || !isBinaryInstalled(binaryPath)) {
debugLogger.error(
chalk.red(
'LiteRT-LM binary not found. Run "gemini gemma setup" first.',
),
);
await exitCli(1);
return;
}
const alreadyRunning = await isServerRunning(port);
if (alreadyRunning) {
debugLogger.log(
chalk.green(`LiteRT server is already running on port ${port}.`),
);
await exitCli(0);
return;
}
debugLogger.log(`Starting LiteRT server on port ${port}...`);
const started = await startServer(binaryPath, port);
if (started) {
debugLogger.log(chalk.green(`LiteRT server started on port ${port}.`));
debugLogger.log(chalk.dim(`Logs: ${getLogFilePath()}`));
await exitCli(0);
} else {
debugLogger.error(
chalk.red('Server may not have started correctly. Check logs:'),
);
debugLogger.error(chalk.dim(` ${getLogFilePath()}`));
await exitCli(1);
}
},
};
+165
View File
@@ -0,0 +1,165 @@
/**
* @license
* Copyright 2026 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import type { CommandModule } from 'yargs';
import chalk from 'chalk';
import { DEFAULT_PORT, GEMMA_MODEL_NAME } from './constants.js';
import {
detectPlatform,
getBinaryPath,
isBinaryInstalled,
isModelDownloaded,
isServerRunning,
readServerPid,
isProcessRunning,
resolveGemmaConfig,
} from './platform.js';
import { exitCli } from '../utils.js';
export interface GemmaStatusResult {
binaryInstalled: boolean;
binaryPath: string | null;
modelDownloaded: boolean;
serverRunning: boolean;
serverPid: number | null;
settingsEnabled: boolean;
port: number;
allPassing: boolean;
}
export async function checkGemmaStatus(
port?: number,
): Promise<GemmaStatusResult> {
const { settingsEnabled, configuredPort } = resolveGemmaConfig(DEFAULT_PORT);
const effectivePort = port ?? configuredPort;
const binaryPath = getBinaryPath();
const binaryInstalled = isBinaryInstalled(binaryPath);
const modelDownloaded =
binaryInstalled && binaryPath ? isModelDownloaded(binaryPath) : false;
const serverRunning = await isServerRunning(effectivePort);
const pid = readServerPid();
const serverPid = pid && isProcessRunning(pid) ? pid : null;
const allPassing =
binaryInstalled && modelDownloaded && serverRunning && settingsEnabled;
return {
binaryInstalled,
binaryPath,
modelDownloaded,
serverRunning,
serverPid,
settingsEnabled,
port: effectivePort,
allPassing,
};
}
export function formatGemmaStatus(status: GemmaStatusResult): string {
const check = (ok: boolean) => (ok ? chalk.green('✓') : chalk.red('✗'));
const lines: string[] = [
'',
chalk.bold('Gemma Local Model Routing Status'),
chalk.dim('─'.repeat(40)),
'',
];
if (status.binaryInstalled) {
lines.push(` Binary: ${check(true)} Installed (${status.binaryPath})`);
} else {
const platform = detectPlatform();
if (platform) {
lines.push(` Binary: ${check(false)} Not installed`);
lines.push(chalk.dim(` Run: gemini gemma setup`));
} else {
lines.push(
` Binary: ${check(false)} Unsupported platform (${process.platform}-${process.arch})`,
);
}
}
if (status.modelDownloaded) {
lines.push(` Model: ${check(true)} ${GEMMA_MODEL_NAME} downloaded`);
} else {
lines.push(` Model: ${check(false)} ${GEMMA_MODEL_NAME} not found`);
if (status.binaryInstalled) {
lines.push(
chalk.dim(
` Run: ${status.binaryPath} pull ${GEMMA_MODEL_NAME}`,
),
);
} else {
lines.push(chalk.dim(` Run: gemini gemma setup`));
}
}
if (status.serverRunning) {
const pidInfo = status.serverPid ? ` (PID ${status.serverPid})` : '';
lines.push(
` Server: ${check(true)} Running on port ${status.port}${pidInfo}`,
);
} else {
lines.push(
` Server: ${check(false)} Not running on port ${status.port}`,
);
lines.push(chalk.dim(` Run: gemini gemma start`));
}
if (status.settingsEnabled) {
lines.push(` Settings: ${check(true)} Enabled in settings.json`);
} else {
lines.push(` Settings: ${check(false)} Not enabled in settings.json`);
lines.push(
chalk.dim(
` Run: gemini gemma setup (auto-configures settings)`,
),
);
}
lines.push('');
if (status.allPassing) {
lines.push(chalk.green(' Routing is active — no action needed.'));
lines.push('');
lines.push(
chalk.dim(
' Simple requests → Flash (fast) | Complex requests → Pro (powerful)',
),
);
lines.push(chalk.dim(' This happens automatically on every request.'));
} else {
lines.push(
chalk.yellow(
' Some checks failed. Run "gemini gemma setup" for guided installation.',
),
);
}
lines.push('');
return lines.join('\n');
}
export const statusCommand: CommandModule = {
command: 'status',
describe: 'Check Gemma local model routing status',
builder: (yargs) =>
yargs.option('port', {
type: 'number',
description: 'Port to check for the LiteRT server',
}),
handler: async (argv) => {
let port: number | undefined;
if (argv['port'] !== undefined) {
port = Number(argv['port']);
}
const status = await checkGemmaStatus(port);
const output = formatGemmaStatus(status);
process.stdout.write(output);
await exitCli(status.allPassing ? 0 : 1);
},
};
@@ -0,0 +1,112 @@
/**
* @license
* Copyright 2026 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import fs from 'node:fs';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
const mockGetBinaryPath = vi.hoisted(() => vi.fn());
const mockIsExpectedLiteRtServerProcess = vi.hoisted(() => vi.fn());
const mockIsProcessRunning = vi.hoisted(() => vi.fn());
const mockIsServerRunning = vi.hoisted(() => vi.fn());
const mockReadServerPid = vi.hoisted(() => vi.fn());
const mockReadServerProcessInfo = vi.hoisted(() => vi.fn());
const mockResolveGemmaConfig = vi.hoisted(() => vi.fn());
vi.mock('@google/gemini-cli-core', async (importOriginal) => {
const { mockCoreDebugLogger } = await import(
'../../test-utils/mockDebugLogger.js'
);
return mockCoreDebugLogger(
await importOriginal<typeof import('@google/gemini-cli-core')>(),
{
stripAnsi: false,
},
);
});
vi.mock('./constants.js', () => ({
DEFAULT_PORT: 9379,
getPidFilePath: vi.fn(() => '/tmp/litert-server.pid'),
}));
vi.mock('./platform.js', () => ({
getBinaryPath: mockGetBinaryPath,
isExpectedLiteRtServerProcess: mockIsExpectedLiteRtServerProcess,
isProcessRunning: mockIsProcessRunning,
isServerRunning: mockIsServerRunning,
readServerPid: mockReadServerPid,
readServerProcessInfo: mockReadServerProcessInfo,
resolveGemmaConfig: mockResolveGemmaConfig,
}));
vi.mock('../utils.js', () => ({
exitCli: vi.fn(),
}));
import { stopServer } from './stop.js';
describe('gemma stop command', () => {
beforeEach(() => {
vi.clearAllMocks();
vi.useFakeTimers();
mockGetBinaryPath.mockReturnValue('/custom/lit');
mockResolveGemmaConfig.mockReturnValue({ configuredPort: 9379 });
});
afterEach(() => {
vi.useRealTimers();
vi.restoreAllMocks();
});
it('refuses to signal a pid that does not match the expected LiteRT server', async () => {
mockReadServerProcessInfo.mockReturnValue({
pid: 1234,
binaryPath: '/custom/lit',
port: 8123,
});
mockIsProcessRunning.mockReturnValue(true);
mockIsExpectedLiteRtServerProcess.mockReturnValue(false);
const killSpy = vi.spyOn(process, 'kill').mockImplementation(() => true);
await expect(stopServer(8123)).resolves.toBe('unexpected-process');
expect(killSpy).not.toHaveBeenCalled();
});
it('stops the verified LiteRT server and removes the pid file', async () => {
mockReadServerProcessInfo.mockReturnValue({
pid: 1234,
binaryPath: '/custom/lit',
port: 8123,
});
mockIsProcessRunning.mockReturnValueOnce(true).mockReturnValueOnce(false);
mockIsExpectedLiteRtServerProcess.mockReturnValue(true);
const unlinkSpy = vi.spyOn(fs, 'unlinkSync').mockImplementation(() => {});
const killSpy = vi.spyOn(process, 'kill').mockImplementation(() => true);
const stopPromise = stopServer(8123);
await vi.runAllTimersAsync();
await expect(stopPromise).resolves.toBe('stopped');
expect(killSpy).toHaveBeenCalledWith(1234, 'SIGTERM');
expect(unlinkSpy).toHaveBeenCalledWith('/tmp/litert-server.pid');
});
it('cleans up a stale pid file when the recorded process is no longer running', async () => {
mockReadServerProcessInfo.mockReturnValue({
pid: 1234,
binaryPath: '/custom/lit',
port: 8123,
});
mockIsProcessRunning.mockReturnValue(false);
const unlinkSpy = vi.spyOn(fs, 'unlinkSync').mockImplementation(() => {});
await expect(stopServer(8123)).resolves.toBe('not-running');
expect(unlinkSpy).toHaveBeenCalledWith('/tmp/litert-server.pid');
});
});
+155
View File
@@ -0,0 +1,155 @@
/**
* @license
* Copyright 2026 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import type { CommandModule } from 'yargs';
import fs from 'node:fs';
import chalk from 'chalk';
import { debugLogger } from '@google/gemini-cli-core';
import { exitCli } from '../utils.js';
import { DEFAULT_PORT, getPidFilePath } from './constants.js';
import {
getBinaryPath,
isExpectedLiteRtServerProcess,
isProcessRunning,
isServerRunning,
readServerPid,
readServerProcessInfo,
resolveGemmaConfig,
} from './platform.js';
export type StopServerResult =
| 'stopped'
| 'not-running'
| 'unexpected-process'
| 'failed';
export async function stopServer(
expectedPort?: number,
): Promise<StopServerResult> {
const processInfo = readServerProcessInfo();
const pidPath = getPidFilePath();
if (!processInfo) {
return 'not-running';
}
const { pid } = processInfo;
if (!isProcessRunning(pid)) {
debugLogger.log(
`Stale PID file found (PID ${pid} is not running), removing ${pidPath}`,
);
try {
fs.unlinkSync(pidPath);
} catch {
// ignore
}
return 'not-running';
}
const binaryPath = processInfo.binaryPath ?? getBinaryPath();
const port = processInfo.port ?? expectedPort;
if (!isExpectedLiteRtServerProcess(pid, { binaryPath, port })) {
debugLogger.warn(
`Refusing to stop PID ${pid} because it does not match the expected LiteRT server process.`,
);
return 'unexpected-process';
}
try {
process.kill(pid, 'SIGTERM');
} catch {
return 'failed';
}
await new Promise((resolve) => setTimeout(resolve, 1000));
if (isProcessRunning(pid)) {
try {
process.kill(pid, 'SIGKILL');
} catch {
// ignore
}
await new Promise((resolve) => setTimeout(resolve, 500));
if (isProcessRunning(pid)) {
return 'failed';
}
}
try {
fs.unlinkSync(pidPath);
} catch {
// ignore
}
return 'stopped';
}
export const stopCommand: CommandModule = {
command: 'stop',
describe: 'Stop the LiteRT-LM server',
builder: (yargs) =>
yargs.option('port', {
type: 'number',
description: 'Port where the LiteRT server is running',
}),
handler: async (argv) => {
let port: number | undefined;
if (argv['port'] !== undefined) {
port = Number(argv['port']);
}
if (!port) {
const { configuredPort } = resolveGemmaConfig(DEFAULT_PORT);
port = configuredPort;
}
const processInfo = readServerProcessInfo();
const pid = processInfo?.pid ?? readServerPid();
if (pid !== null && isProcessRunning(pid)) {
debugLogger.log(`Stopping LiteRT server (PID ${pid})...`);
const result = await stopServer(port);
if (result === 'stopped') {
debugLogger.log(chalk.green('LiteRT server stopped.'));
await exitCli(0);
} else if (result === 'unexpected-process') {
debugLogger.error(
chalk.red(
`Refusing to stop PID ${pid} because it does not match the expected LiteRT server process.`,
),
);
debugLogger.error(
chalk.dim(
'Remove the stale pid file after verifying the process, or stop the process manually.',
),
);
await exitCli(1);
} else {
debugLogger.error(chalk.red('Failed to stop LiteRT server.'));
await exitCli(1);
}
return;
}
const running = await isServerRunning(port);
if (running) {
debugLogger.log(
chalk.yellow(
`A server is responding on port ${port}, but it was not started by "gemini gemma start".`,
),
);
debugLogger.log(
chalk.dim(
'If you started it manually, stop it from the terminal where it is running.',
),
);
await exitCli(1);
} else {
debugLogger.log('No LiteRT server is currently running.');
await exitCli(0);
}
},
};
+13
View File
@@ -338,6 +338,7 @@ describe('parseArguments', () => {
{ cmd: 'skill list', expected: true },
{ cmd: 'hooks migrate', expected: true },
{ cmd: 'hook migrate', expected: true },
{ cmd: 'gemma status', expected: true },
{ cmd: 'some query', expected: undefined },
{ cmd: 'hello world', expected: undefined },
])(
@@ -758,6 +759,12 @@ describe('parseArguments', () => {
const argv = await parseArguments(settings);
expect(argv.isCommand).toBe(true);
});
it('should set isCommand to true for gemma command', async () => {
process.argv = ['node', 'script.js', 'gemma', 'status'];
const argv = await parseArguments(createTestMergedSettings());
expect(argv.isCommand).toBe(true);
});
});
describe('loadCliConfig', () => {
@@ -3030,6 +3037,8 @@ describe('loadCliConfig gemmaModelRouter', () => {
experimental: {
gemmaModelRouter: {
enabled: true,
autoStartServer: false,
binaryPath: '/custom/lit',
classifier: {
host: 'http://custom:1234',
model: 'custom-gemma',
@@ -3040,6 +3049,8 @@ describe('loadCliConfig gemmaModelRouter', () => {
const config = await loadCliConfig(settings, 'test-session', argv);
expect(config.getGemmaModelRouterEnabled()).toBe(true);
const gemmaSettings = config.getGemmaModelRouterSettings();
expect(gemmaSettings.autoStartServer).toBe(false);
expect(gemmaSettings.binaryPath).toBe('/custom/lit');
expect(gemmaSettings.classifier?.host).toBe('http://custom:1234');
expect(gemmaSettings.classifier?.model).toBe('custom-gemma');
});
@@ -3057,6 +3068,8 @@ describe('loadCliConfig gemmaModelRouter', () => {
const config = await loadCliConfig(settings, 'test-session', argv);
expect(config.getGemmaModelRouterEnabled()).toBe(true);
const gemmaSettings = config.getGemmaModelRouterSettings();
expect(gemmaSettings.autoStartServer).toBe(false);
expect(gemmaSettings.binaryPath).toBe('');
expect(gemmaSettings.classifier?.host).toBe('http://localhost:9379');
expect(gemmaSettings.classifier?.model).toBe('gemma3-1b-gpu-custom');
});
+41 -16
View File
@@ -13,6 +13,7 @@ import { mcpCommand } from '../commands/mcp.js';
import { extensionsCommand } from '../commands/extensions.js';
import { skillsCommand } from '../commands/skills.js';
import { hooksCommand } from '../commands/hooks.js';
import { gemmaCommand } from '../commands/gemma.js';
import {
setGeminiMdFilename as setServerGeminiMdFilename,
getCurrentGeminiMdFilename,
@@ -23,6 +24,7 @@ import {
FileDiscoveryService,
resolveTelemetrySettings,
FatalConfigError,
getErrorMessage,
getPty,
debugLogger,
loadServerHierarchicalMemory,
@@ -59,6 +61,7 @@ import {
import { loadSandboxConfig } from './sandboxConfig.js';
import { resolvePath } from '../utils/resolvePath.js';
import { isRecord } from '../utils/settingsUtils.js';
import { RESUME_LATEST } from '../utils/sessionUtils.js';
import { isWorkspaceTrusted } from './trustedFolders.js';
@@ -105,6 +108,7 @@ export interface CliArgs {
startupMessages?: string[];
rawOutput: boolean | undefined;
acceptRawOutputRisk: boolean | undefined;
skipTrust: boolean | undefined;
isCommand: boolean | undefined;
}
@@ -181,6 +185,7 @@ export async function parseArguments(
extensionsCommand,
skillsCommand,
hooksCommand,
gemmaCommand,
];
const subcommands = commandModules.flatMap((mod) => {
@@ -260,6 +265,7 @@ export async function parseArguments(
yargsInstance.command(extensionsCommand);
yargsInstance.command(skillsCommand);
yargsInstance.command(hooksCommand);
yargsInstance.command(gemmaCommand);
yargsInstance
.command('$0 [query..]', 'Launch Gemini CLI', (yargsInstance) =>
@@ -288,6 +294,11 @@ export async function parseArguments(
description:
'Execute the provided prompt and continue in interactive mode',
})
.option('skip-trust', {
type: 'boolean',
description: 'Trust the current workspace for this session.',
default: false,
})
.option('worktree', {
alias: 'w',
type: 'string',
@@ -456,9 +467,16 @@ export async function parseArguments(
yargsInstance.wrap(yargsInstance.terminalWidth());
let result;
try {
result = await yargsInstance.parse();
const parsed = await yargsInstance.parse();
if (!isRecord(parsed)) {
throw new Error('Failed to parse arguments');
}
result = parsed;
if (result['skip-trust']) {
process.env['GEMINI_CLI_TRUST_WORKSPACE'] = 'true';
}
} catch (e) {
const msg = e instanceof Error ? e.message : String(e);
const msg = getErrorMessage(e);
debugLogger.error(msg);
yargsInstance.showHelp();
await runExitCleanup();
@@ -472,11 +490,13 @@ export async function parseArguments(
}
// Normalize query args: handle both quoted "@path file" and unquoted @path file
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
const queryArg = (result as { query?: string | string[] | undefined }).query;
const q: string | undefined = Array.isArray(queryArg)
? queryArg.join(' ')
: queryArg;
const queryArg = result['query'];
let q: string | undefined;
if (Array.isArray(queryArg)) {
q = queryArg.join(' ');
} else if (typeof queryArg === 'string') {
q = queryArg;
}
// -p/--prompt forces non-interactive mode; positional args default to interactive in TTY
if (q && !result['prompt']) {
@@ -491,8 +511,8 @@ export async function parseArguments(
}
// Keep CliArgs.query as a string for downstream typing
(result as Record<string, unknown>)['query'] = q || undefined;
(result as Record<string, unknown>)['startupMessages'] = startupMessages;
result['query'] = q || undefined;
result['startupMessages'] = startupMessages;
// The import format is now only controlled by settings.memoryImportFormat
// We no longer accept it as a CLI argument
@@ -544,7 +564,7 @@ export async function loadCliConfig(
? false
: (settings.security?.folderTrust?.enabled ?? false);
const trustedFolder =
isWorkspaceTrusted(settings, cwd, undefined, {
isWorkspaceTrusted(settings, cwd, {
prompt: argv.prompt,
query: argv.query,
})?.isTrusted ?? false;
@@ -590,7 +610,7 @@ export async function loadCliConfig(
return resolveToRealPath(trimmedPath) !== realCwd;
} catch (e) {
debugLogger.debug(
`[IDE] Skipping inaccessible workspace folder: ${trimmedPath} (${e instanceof Error ? e.message : String(e)})`,
`[IDE] Skipping inaccessible workspace folder: ${trimmedPath} (${getErrorMessage(e)})`,
);
return false;
}
@@ -614,7 +634,7 @@ export async function loadCliConfig(
.getExtensions()
.find((ext) => ext.isActive && ext.plan?.directory)?.plan;
const experimentalJitContext = settings.experimental.jitContext;
const experimentalJitContext = settings.experimental.jitContext ?? true;
let extensionRegistryURI =
process.env['GEMINI_CLI_EXTENSION_REGISTRY_URI'] ??
@@ -988,11 +1008,14 @@ export async function loadCliConfig(
enableEventDrivenScheduler: true,
skillsSupport: settings.skills?.enabled ?? true,
disabledSkills: settings.skills?.disabled,
experimentalJitContext: settings.experimental?.jitContext,
experimentalMemoryManager: settings.experimental?.memoryManager,
experimentalJitContext,
experimentalMemoryV2: settings.experimental?.memoryV2,
experimentalAutoMemory: settings.experimental?.autoMemory,
contextManagement,
modelSteering: settings.experimental?.modelSteering,
topicUpdateNarration: settings.experimental?.topicUpdateNarration,
topicUpdateNarration:
settings.general?.topicUpdateNarration ??
settings.experimental?.topicUpdateNarration,
noBrowser: !!process.env['NO_BROWSER'],
summarizeToolOutput: settings.model?.summarizeToolOutput,
ideMode,
@@ -1021,11 +1044,13 @@ export async function loadCliConfig(
format: (argv.outputFormat ?? settings.output?.format) as OutputFormat,
},
gemmaModelRouter: settings.experimental?.gemmaModelRouter,
autoRouting: settings.model?.autoRouting,
adk: settings.experimental?.adk,
fakeResponses: argv.fakeResponses,
recordResponses: argv.recordResponses,
retryFetchErrors: settings.general?.retryFetchErrors,
billing: settings.billing,
vertexAiRouting: settings.billing?.vertexAi,
maxAttempts: settings.general?.maxAttempts,
ptyInfo: ptyInfo?.name,
disableLLMCorrection: settings.tools?.disableLLMCorrection,
@@ -1092,7 +1117,7 @@ async function resolveWorktreeSettings(
worktreeBaseSha = stdout.trim();
} catch (e: unknown) {
debugLogger.debug(
`Failed to resolve worktree base SHA at ${worktreePath}: ${e instanceof Error ? e.message : String(e)}`,
`Failed to resolve worktree base SHA at ${worktreePath}: ${getErrorMessage(e)}`,
);
}
+5 -1
View File
@@ -26,6 +26,7 @@ import {
loadAgentsFromDirectory,
loadSkillsFromDir,
getRealPath,
normalizePath,
} from '@google/gemini-cli-core';
import {
loadSettings,
@@ -1420,6 +1421,7 @@ name = "yolo-checker"
'.gemini',
'trustedFolders.json',
);
vi.stubEnv('GEMINI_CLI_TRUSTED_FOLDERS_PATH', trustedFoldersPath);
vi.mocked(isWorkspaceTrusted).mockReturnValue({
isTrusted: false,
source: undefined,
@@ -1438,7 +1440,9 @@ name = "yolo-checker"
const trustedFolders = JSON.parse(
fs.readFileSync(trustedFoldersPath, 'utf-8'),
);
expect(trustedFolders[tempWorkspaceDir]).toBe('TRUST_FOLDER');
expect(trustedFolders[normalizePath(tempWorkspaceDir)]).toBe(
'TRUST_FOLDER',
);
});
describe.each([true, false])(
+12 -6
View File
@@ -1912,6 +1912,9 @@ describe('Settings Loading and Merging', () => {
const geminiEnvPath = path.resolve(
path.join(MOCK_WORKSPACE_DIR, GEMINI_DIR, '.env'),
);
const workspaceEnvPath = path.resolve(
path.join(MOCK_WORKSPACE_DIR, '.env'),
);
vi.spyOn(trustedFolders, 'isWorkspaceTrusted').mockReturnValue({
isTrusted: isWorkspaceTrustedValue,
@@ -1919,9 +1922,11 @@ describe('Settings Loading and Merging', () => {
});
(mockFsExistsSync as Mock).mockImplementation((p: fs.PathLike) => {
const normalizedP = path.resolve(p.toString());
return [path.resolve(USER_SETTINGS_PATH), geminiEnvPath].includes(
normalizedP,
);
return [
path.resolve(USER_SETTINGS_PATH),
geminiEnvPath,
workspaceEnvPath,
].includes(normalizedP);
});
const userSettingsContent: Settings = {
ui: {
@@ -1941,7 +1946,7 @@ describe('Settings Loading and Merging', () => {
const normalizedP = path.resolve(p.toString());
if (normalizedP === path.resolve(USER_SETTINGS_PATH))
return JSON.stringify(userSettingsContent);
if (normalizedP === geminiEnvPath)
if (normalizedP === geminiEnvPath || normalizedP === workspaceEnvPath)
return 'TESTTEST=1234\nGEMINI_API_KEY=test-key';
return '{}';
},
@@ -1970,7 +1975,7 @@ describe('Settings Loading and Merging', () => {
expect(process.env['TESTTEST']).not.toEqual('1234');
});
it('does load env files from untrusted spaces when NOT sandboxed', () => {
it('does NOT load non-whitelisted env files from untrusted spaces even when NOT sandboxed', () => {
setup({ isFolderTrustEnabled: true, isWorkspaceTrustedValue: false });
const settings = {
security: { folderTrust: { enabled: true } },
@@ -1978,7 +1983,8 @@ describe('Settings Loading and Merging', () => {
} as Settings;
loadEnvironment(settings, MOCK_WORKSPACE_DIR, isWorkspaceTrusted);
expect(process.env['TESTTEST']).toEqual('1234');
expect(process.env['TESTTEST']).not.toEqual('1234');
expect(process.env['GEMINI_API_KEY']).toEqual('test-key');
});
it('does not load env files when trust is undefined and sandboxed', () => {
+21 -12
View File
@@ -78,7 +78,12 @@ export function getMergeStrategyForPath(
export const USER_SETTINGS_PATH = Storage.getGlobalSettingsPath();
export const USER_SETTINGS_DIR = path.dirname(USER_SETTINGS_PATH);
export const DEFAULT_EXCLUDED_ENV_VARS = ['DEBUG', 'DEBUG_MODE'];
export const DEFAULT_EXCLUDED_ENV_VARS = [
'DEBUG',
'DEBUG_MODE',
'GEMINI_CLI_IDE_SERVER_STDIO_COMMAND',
'GEMINI_CLI_IDE_SERVER_STDIO_ARGS',
];
const AUTH_ENV_VAR_WHITELIST = [
'GEMINI_API_KEY',
@@ -494,13 +499,15 @@ export class LoadedSettings {
}
}
function findEnvFile(startDir: string): string | null {
function findEnvFile(startDir: string, isTrusted: boolean): string | null {
let currentDir = path.resolve(startDir);
while (true) {
// prefer gemini-specific .env under GEMINI_DIR
const geminiEnvPath = path.join(currentDir, GEMINI_DIR, '.env');
if (fs.existsSync(geminiEnvPath)) {
return geminiEnvPath;
if (isTrusted) {
const geminiEnvPath = path.join(currentDir, GEMINI_DIR, '.env');
if (fs.existsSync(geminiEnvPath)) {
return geminiEnvPath;
}
}
const envPath = path.join(currentDir, '.env');
if (fs.existsSync(envPath)) {
@@ -509,9 +516,11 @@ function findEnvFile(startDir: string): string | null {
const parentDir = path.dirname(currentDir);
if (parentDir === currentDir || !parentDir) {
// check .env under home as fallback, again preferring gemini-specific .env
const homeGeminiEnvPath = path.join(homedir(), GEMINI_DIR, '.env');
if (fs.existsSync(homeGeminiEnvPath)) {
return homeGeminiEnvPath;
if (isTrusted) {
const homeGeminiEnvPath = path.join(homedir(), GEMINI_DIR, '.env');
if (fs.existsSync(homeGeminiEnvPath)) {
return homeGeminiEnvPath;
}
}
const homeEnvPath = path.join(homedir(), '.env');
if (fs.existsSync(homeEnvPath)) {
@@ -554,10 +563,10 @@ export function loadEnvironment(
workspaceDir: string,
isWorkspaceTrustedFn = isWorkspaceTrusted,
): void {
const envFilePath = findEnvFile(workspaceDir);
const trustResult = isWorkspaceTrustedFn(settings, workspaceDir);
const isTrusted = trustResult.isTrusted ?? false;
const envFilePath = findEnvFile(workspaceDir, isTrusted);
// Check settings OR check process.argv directly since this might be called
// before arguments are fully parsed. This is a best-effort sniffing approach
// that happens early in the CLI lifecycle. It is designed to detect the
@@ -592,8 +601,8 @@ export function loadEnvironment(
for (const key in parsedEnv) {
if (Object.hasOwn(parsedEnv, key)) {
let value = parsedEnv[key];
// If the workspace is untrusted but we are sandboxed, only allow whitelisted variables.
if (!isTrusted && isSandboxed) {
// If the workspace is untrusted, only allow whitelisted variables.
if (!isTrusted) {
if (!AUTH_ENV_VAR_WHITELIST.includes(key)) {
continue;
}
+43 -1
View File
@@ -138,6 +138,10 @@ describe('SettingsSchema', () => {
getSettingsSchema().context.properties.fileFiltering.properties
?.enableRecursiveFileSearch,
).toBeDefined();
expect(
getSettingsSchema().context.properties.fileFiltering.properties
?.enableFileWatcher,
).toBeDefined();
expect(
getSettingsSchema().context.properties.fileFiltering.properties
?.customIgnoreFilePaths,
@@ -313,6 +317,22 @@ describe('SettingsSchema', () => {
).toBe(false);
});
it('should have Vertex AI routing settings in schema', () => {
const vertexAi =
getSettingsSchema().billing.properties.vertexAi.properties;
expect(vertexAi.requestType).toBeDefined();
expect(vertexAi.requestType.type).toBe('enum');
expect(
vertexAi.requestType.options?.map((option) => option.value),
).toEqual(['dedicated', 'shared']);
expect(vertexAi.sharedRequestType).toBeDefined();
expect(vertexAi.sharedRequestType.type).toBe('enum');
expect(
vertexAi.sharedRequestType.options?.map((option) => option.value),
).toEqual(['priority', 'flex']);
});
it('should have folderTrustFeature setting in schema', () => {
expect(
getSettingsSchema().security.properties.folderTrust.properties.enabled,
@@ -471,11 +491,33 @@ describe('SettingsSchema', () => {
expect(enabled.category).toBe('Experimental');
expect(enabled.default).toBe(false);
expect(enabled.requiresRestart).toBe(true);
expect(enabled.showInDialog).toBe(false);
expect(enabled.showInDialog).toBe(true);
expect(enabled.description).toBe(
'Enable the Gemma Model Router (experimental). Requires a local endpoint serving Gemma via the Gemini API using LiteRT-LM shim.',
);
const autoStartServer = gemmaModelRouter.properties.autoStartServer;
expect(autoStartServer).toBeDefined();
expect(autoStartServer.type).toBe('boolean');
expect(autoStartServer.category).toBe('Experimental');
expect(autoStartServer.default).toBe(false);
expect(autoStartServer.requiresRestart).toBe(true);
expect(autoStartServer.showInDialog).toBe(true);
expect(autoStartServer.description).toBe(
'Automatically start the LiteRT-LM server when Gemini CLI starts and the Gemma router is enabled.',
);
const binaryPath = gemmaModelRouter.properties.binaryPath;
expect(binaryPath).toBeDefined();
expect(binaryPath.type).toBe('string');
expect(binaryPath.category).toBe('Experimental');
expect(binaryPath.default).toBe('');
expect(binaryPath.requiresRestart).toBe(true);
expect(binaryPath.showInDialog).toBe(false);
expect(binaryPath.description).toBe(
'Custom path to the LiteRT-LM binary. Leave empty to use the default location (~/.gemini/bin/litert/).',
);
const classifier = gemmaModelRouter.properties.classifier;
expect(classifier).toBeDefined();
expect(classifier.type).toBe('object');
+161 -10
View File
@@ -21,6 +21,7 @@ import {
type AgentOverride,
type CustomTheme,
type SandboxConfig,
type VertexAiRoutingConfig,
} from '@google/gemini-cli-core';
import type { SessionRetentionSettings } from './settings.js';
import { DEFAULT_MIN_RETENTION } from '../utils/sessionCleanup.js';
@@ -256,14 +257,29 @@ const SETTINGS_SCHEMA = {
},
enableNotifications: {
type: 'boolean',
label: 'Enable Notifications',
label: 'Enable Terminal Notifications',
category: 'General',
requiresRestart: false,
default: false,
description:
'Enable run-event notifications for action-required prompts and session completion.',
'Enable terminal run-event notifications for action-required prompts and session completion.',
showInDialog: true,
},
notificationMethod: {
type: 'enum',
label: 'Terminal Notification Method',
category: 'General',
requiresRestart: false,
default: 'auto',
description: 'How to send terminal notifications.',
showInDialog: true,
options: [
{ value: 'auto', label: 'Auto' },
{ value: 'osc9', label: 'OSC 9' },
{ value: 'osc777', label: 'OSC 777' },
{ value: 'bell', label: 'Bell' },
],
},
checkpointing: {
type: 'object',
label: 'Checkpointing',
@@ -403,6 +419,16 @@ const SETTINGS_SCHEMA = {
},
description: 'Settings for automatic session cleanup.',
},
topicUpdateNarration: {
type: 'boolean',
label: 'Topic & Update Narration',
category: 'General',
requiresRestart: false,
default: true,
description:
'Enable the Topic & Update communication model for reduced chattiness and structured progress reporting.',
showInDialog: true,
},
},
},
output: {
@@ -965,6 +991,45 @@ const SETTINGS_SCHEMA = {
{ value: 'never', label: 'Never use credits' },
],
},
vertexAi: {
type: 'object',
label: 'Vertex AI',
category: 'Advanced',
requiresRestart: true,
default: undefined as VertexAiRoutingConfig | undefined,
description: 'Vertex AI request routing settings.',
showInDialog: false,
properties: {
requestType: {
type: 'enum',
label: 'Vertex AI Request Type',
category: 'Advanced',
requiresRestart: true,
default: undefined as VertexAiRoutingConfig['requestType'],
description:
'Sets the X-Vertex-AI-LLM-Request-Type header for Vertex AI requests.',
showInDialog: false,
options: [
{ value: 'dedicated', label: 'Dedicated' },
{ value: 'shared', label: 'Shared' },
],
},
sharedRequestType: {
type: 'enum',
label: 'Vertex AI Shared Request Type',
category: 'Advanced',
requiresRestart: true,
default: undefined as VertexAiRoutingConfig['sharedRequestType'],
description:
'Sets the X-Vertex-AI-LLM-Shared-Request-Type header for Vertex AI requests.',
showInDialog: false,
options: [
{ value: 'priority', label: 'Priority' },
{ value: 'flex', label: 'Flex' },
],
},
},
},
},
},
@@ -1047,6 +1112,46 @@ const SETTINGS_SCHEMA = {
description: 'Skip the next speaker check.',
showInDialog: true,
},
autoRouting: {
type: 'object',
label: 'Auto Routing',
category: 'Model',
requiresRestart: false,
default: {},
description: 'Settings for automatic model routing.',
showInDialog: false,
properties: {
bestEffortPro: {
type: 'boolean',
label: 'Best Effort Pro',
category: 'Model',
requiresRestart: false,
default: false,
description:
'Always prefer the Pro model unless it is unavailable (e.g., due to timeouts or quota), ignoring other routing hints.',
showInDialog: true,
},
proTimeoutMinutes: {
type: 'number',
label: 'Pro Timeout (Minutes)',
category: 'Model',
requiresRestart: false,
default: 5,
description:
'If a Pro request takes longer than this many minutes, it will be marked as temporarily unavailable and fallback to Flash.',
showInDialog: true,
},
proTimeoutFallbackDurationMinutes: {
type: 'number',
label: 'Pro Timeout Fallback Duration (Minutes)',
category: 'Model',
requiresRestart: false,
default: 60,
description: 'How long to route to Flash after Pro times out.',
showInDialog: true,
},
},
},
},
},
@@ -1406,6 +1511,17 @@ const SETTINGS_SCHEMA = {
description: 'Respect .geminiignore files when searching.',
showInDialog: true,
},
enableFileWatcher: {
type: 'boolean',
label: 'Enable File Watcher',
category: 'Context',
requiresRestart: true,
default: false,
description: oneLine`
Enable file watcher updates for @ file suggestions (experimental).
`,
showInDialog: false,
},
enableRecursiveFileSearch: {
type: 'boolean',
label: 'Enable Recursive File Search',
@@ -2064,8 +2180,9 @@ const SETTINGS_SCHEMA = {
label: 'JIT Context Loading',
category: 'Experimental',
requiresRestart: true,
default: false,
description: 'Enable Just-In-Time (JIT) context loading.',
default: true,
description:
'Enable Just-In-Time (JIT) context loading. Defaults to true; set to false to opt out and load all GEMINI.md files into the system instruction up-front.',
showInDialog: false,
},
useOSC52Paste: {
@@ -2144,6 +2261,26 @@ const SETTINGS_SCHEMA = {
default: false,
description:
'Enable the Gemma Model Router (experimental). Requires a local endpoint serving Gemma via the Gemini API using LiteRT-LM shim.',
showInDialog: true,
},
autoStartServer: {
type: 'boolean',
label: 'Auto-start LiteRT Server',
category: 'Experimental',
requiresRestart: true,
default: false,
description:
'Automatically start the LiteRT-LM server when Gemini CLI starts and the Gemma router is enabled.',
showInDialog: true,
},
binaryPath: {
type: 'string',
label: 'LiteRT Binary Path',
category: 'Experimental',
requiresRestart: true,
default: '',
description:
'Custom path to the LiteRT-LM binary. Leave empty to use the default location (~/.gemini/bin/litert/).',
showInDialog: false,
},
classifier: {
@@ -2178,14 +2315,24 @@ const SETTINGS_SCHEMA = {
},
},
},
memoryManager: {
memoryV2: {
type: 'boolean',
label: 'Memory Manager Agent',
label: 'Memory v2',
category: 'Experimental',
requiresRestart: true,
default: true,
description:
'Disable the built-in save_memory tool and let the main agent persist project context by editing markdown files directly with edit/write_file. Route facts across four tiers: team-shared conventions go to project GEMINI.md files, project-specific personal notes go to the per-project private memory folder (MEMORY.md as index + sibling .md files for detail), and cross-project personal preferences go to the global ~/.gemini/GEMINI.md (the only file under ~/.gemini/ that the agent can edit — settings, credentials, etc. remain off-limits). Set to false to fall back to the legacy save_memory tool.',
showInDialog: true,
},
autoMemory: {
type: 'boolean',
label: 'Auto Memory',
category: 'Experimental',
requiresRestart: true,
default: false,
description:
'Replace the built-in save_memory tool with a memory manager subagent that supports adding, removing, de-duplicating, and organizing memories.',
'Automatically extract reusable skills from past sessions in the background. Review results with /memory inbox.',
showInDialog: true,
},
generalistProfile: {
@@ -2213,9 +2360,8 @@ const SETTINGS_SCHEMA = {
category: 'Experimental',
requiresRestart: false,
default: false,
description:
'Enable the experimental Topic & Update communication model for reduced chattiness and structured progress reporting.',
showInDialog: true,
description: 'Deprecated: Use general.topicUpdateNarration instead.',
showInDialog: false,
},
},
},
@@ -2966,6 +3112,11 @@ export const SETTINGS_SCHEMA_DEFINITIONS: Record<
description: 'Protocol for OTLP exporters.',
enum: ['grpc', 'http'],
},
traces: {
type: 'boolean',
description:
'Whether detailed traces with large attributes are captured.',
},
logPrompts: {
type: 'boolean',
description: 'Whether prompts are logged in telemetry payloads.',
+49 -41
View File
@@ -11,7 +11,7 @@ import * as os from 'node:os';
import {
FatalConfigError,
ideContextStore,
coreEvents,
normalizePath,
} from '@google/gemini-cli-core';
import {
loadTrustedFolders,
@@ -32,9 +32,14 @@ vi.mock('@google/gemini-cli-core', async (importOriginal) => {
...actual,
homedir: () => '/mock/home/user',
isHeadlessMode: vi.fn(() => false),
coreEvents: {
emitFeedback: vi.fn(),
},
coreEvents: Object.assign(
Object.create(Object.getPrototypeOf(actual.coreEvents)),
actual.coreEvents,
{
emitFeedback: vi.fn(),
},
),
FatalConfigError: actual.FatalConfigError,
};
});
@@ -53,6 +58,7 @@ describe('Trusted Folders', () => {
// Reset the internal state
resetTrustedFoldersForTesting();
vi.clearAllMocks();
delete process.env['GEMINI_CLI_TRUST_WORKSPACE'];
});
afterEach(() => {
@@ -70,8 +76,14 @@ describe('Trusted Folders', () => {
// Start two concurrent calls
// These will race to acquire the lock on the real file system
const p1 = loadedFolders.setValue('/path1', TrustLevel.TRUST_FOLDER);
const p2 = loadedFolders.setValue('/path2', TrustLevel.TRUST_FOLDER);
const p1 = loadedFolders.setValue(
path.resolve('/path1'),
TrustLevel.TRUST_FOLDER,
);
const p2 = loadedFolders.setValue(
path.resolve('/path2'),
TrustLevel.TRUST_FOLDER,
);
await Promise.all([p1, p2]);
@@ -80,8 +92,8 @@ describe('Trusted Folders', () => {
const config = JSON.parse(content);
expect(config).toEqual({
'/path1': TrustLevel.TRUST_FOLDER,
'/path2': TrustLevel.TRUST_FOLDER,
[normalizePath('/path1')]: TrustLevel.TRUST_FOLDER,
[normalizePath('/path2')]: TrustLevel.TRUST_FOLDER,
});
});
});
@@ -95,13 +107,16 @@ describe('Trusted Folders', () => {
it('should load rules from the configuration file', () => {
const config = {
'/user/folder': TrustLevel.TRUST_FOLDER,
[normalizePath('/user/folder')]: TrustLevel.TRUST_FOLDER,
};
fs.writeFileSync(trustedFoldersPath, JSON.stringify(config), 'utf-8');
const { rules, errors } = loadTrustedFolders();
expect(rules).toEqual([
{ path: '/user/folder', trustLevel: TrustLevel.TRUST_FOLDER },
{
path: normalizePath('/user/folder'),
trustLevel: TrustLevel.TRUST_FOLDER,
},
]);
expect(errors).toEqual([]);
});
@@ -143,14 +158,14 @@ describe('Trusted Folders', () => {
const content = `
{
// This is a comment
"/path": "TRUST_FOLDER"
"${normalizePath('/path').replaceAll('\\', '\\\\')}": "TRUST_FOLDER"
}
`;
fs.writeFileSync(trustedFoldersPath, content, 'utf-8');
const { rules, errors } = loadTrustedFolders();
expect(rules).toEqual([
{ path: '/path', trustLevel: TrustLevel.TRUST_FOLDER },
{ path: normalizePath('/path'), trustLevel: TrustLevel.TRUST_FOLDER },
]);
expect(errors).toEqual([]);
});
@@ -216,15 +231,18 @@ describe('Trusted Folders', () => {
fs.writeFileSync(trustedFoldersPath, '{}', 'utf-8');
const loadedFolders = loadTrustedFolders();
await loadedFolders.setValue('/new/path', TrustLevel.TRUST_FOLDER);
await loadedFolders.setValue(
normalizePath('/new/path'),
TrustLevel.TRUST_FOLDER,
);
expect(loadedFolders.user.config['/new/path']).toBe(
expect(loadedFolders.user.config[normalizePath('/new/path')]).toBe(
TrustLevel.TRUST_FOLDER,
);
const content = fs.readFileSync(trustedFoldersPath, 'utf-8');
const config = JSON.parse(content);
expect(config['/new/path']).toBe(TrustLevel.TRUST_FOLDER);
expect(config[normalizePath('/new/path')]).toBe(TrustLevel.TRUST_FOLDER);
});
it('should throw FatalConfigError if there were load errors', async () => {
@@ -237,28 +255,6 @@ describe('Trusted Folders', () => {
loadedFolders.setValue('/some/path', TrustLevel.TRUST_FOLDER),
).rejects.toThrow(FatalConfigError);
});
it('should report corrupted config via coreEvents.emitFeedback and still succeed', async () => {
// Initialize with valid JSON
fs.writeFileSync(trustedFoldersPath, '{}', 'utf-8');
const loadedFolders = loadTrustedFolders();
// Corrupt the file after initial load
fs.writeFileSync(trustedFoldersPath, 'invalid json', 'utf-8');
await loadedFolders.setValue('/new/path', TrustLevel.TRUST_FOLDER);
expect(coreEvents.emitFeedback).toHaveBeenCalledWith(
'error',
expect.stringContaining('may be corrupted'),
expect.any(Error),
);
// Should have overwritten the corrupted file with new valid config
const content = fs.readFileSync(trustedFoldersPath, 'utf-8');
const config = JSON.parse(content);
expect(config).toEqual({ '/new/path': TrustLevel.TRUST_FOLDER });
});
});
describe('isWorkspaceTrusted Integration', () => {
@@ -427,16 +423,28 @@ describe('Trusted Folders', () => {
},
};
it('should return true when isHeadlessMode is true, ignoring config', async () => {
it('should NOT return true when isHeadlessMode is true, ignoring config', async () => {
const geminiCore = await import('@google/gemini-cli-core');
vi.spyOn(geminiCore, 'isHeadlessMode').mockReturnValue(true);
expect(isWorkspaceTrusted(mockSettings)).toEqual({
isTrusted: true,
isTrusted: undefined,
source: undefined,
});
});
it('should return true when GEMINI_CLI_TRUST_WORKSPACE is true', async () => {
process.env['GEMINI_CLI_TRUST_WORKSPACE'] = 'true';
try {
expect(isWorkspaceTrusted(mockSettings)).toEqual({
isTrusted: true,
source: 'env',
});
} finally {
delete process.env['GEMINI_CLI_TRUST_WORKSPACE'];
}
});
it('should fall back to config when isHeadlessMode is false', async () => {
const geminiCore = await import('@google/gemini-cli-core');
vi.spyOn(geminiCore, 'isHeadlessMode').mockReturnValue(false);
@@ -449,12 +457,12 @@ describe('Trusted Folders', () => {
);
});
it('should return true for isPathTrusted when isHeadlessMode is true', async () => {
it('should return undefined for isPathTrusted when isHeadlessMode is true', async () => {
const geminiCore = await import('@google/gemini-cli-core');
vi.spyOn(geminiCore, 'isHeadlessMode').mockReturnValue(true);
const folders = loadTrustedFolders();
expect(folders.isPathTrusted('/any-untrusted-path')).toBe(true);
expect(folders.isPathTrusted('/any-untrusted-path')).toBe(undefined);
});
});
+31 -365
View File
@@ -4,330 +4,29 @@
* SPDX-License-Identifier: Apache-2.0
*/
import * as fs from 'node:fs';
import * as path from 'node:path';
import * as crypto from 'node:crypto';
import { lock } from 'proper-lockfile';
import {
FatalConfigError,
getErrorMessage,
isWithinRoot,
ideContextStore,
GEMINI_DIR,
homedir,
isHeadlessMode,
coreEvents,
type HeadlessModeOptions,
checkPathTrust,
isHeadlessMode,
loadTrustedFolders as loadCoreTrustedFolders,
type LoadedTrustedFolders,
} from '@google/gemini-cli-core';
import type { Settings } from './settings.js';
import stripJsonComments from 'strip-json-comments';
const { promises: fsPromises } = fs;
export {
TrustLevel,
isTrustLevel,
resetTrustedFoldersForTesting,
saveTrustedFolders,
} from '@google/gemini-cli-core';
export const TRUSTED_FOLDERS_FILENAME = 'trustedFolders.json';
export function getUserSettingsDir(): string {
return path.join(homedir(), GEMINI_DIR);
}
export function getTrustedFoldersPath(): string {
if (process.env['GEMINI_CLI_TRUSTED_FOLDERS_PATH']) {
return process.env['GEMINI_CLI_TRUSTED_FOLDERS_PATH'];
}
return path.join(getUserSettingsDir(), TRUSTED_FOLDERS_FILENAME);
}
export enum TrustLevel {
TRUST_FOLDER = 'TRUST_FOLDER',
TRUST_PARENT = 'TRUST_PARENT',
DO_NOT_TRUST = 'DO_NOT_TRUST',
}
export function isTrustLevel(
value: string | number | boolean | object | null | undefined,
): value is TrustLevel {
return (
typeof value === 'string' &&
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
Object.values(TrustLevel).includes(value as TrustLevel)
);
}
export interface TrustRule {
path: string;
trustLevel: TrustLevel;
}
export interface TrustedFoldersError {
message: string;
path: string;
}
export interface TrustedFoldersFile {
config: Record<string, TrustLevel>;
path: string;
}
export interface TrustResult {
isTrusted: boolean | undefined;
source: 'ide' | 'file' | undefined;
}
const realPathCache = new Map<string, string>();
/**
* Parses the trusted folders JSON content, stripping comments.
*/
function parseTrustedFoldersJson(content: string): unknown {
return JSON.parse(stripJsonComments(content));
}
/**
* FOR TESTING PURPOSES ONLY.
* Clears the real path cache.
*/
export function clearRealPathCacheForTesting(): void {
realPathCache.clear();
}
function getRealPath(location: string): string {
let realPath = realPathCache.get(location);
if (realPath !== undefined) {
return realPath;
}
try {
realPath = fs.existsSync(location) ? fs.realpathSync(location) : location;
} catch {
realPath = location;
}
realPathCache.set(location, realPath);
return realPath;
}
export class LoadedTrustedFolders {
constructor(
readonly user: TrustedFoldersFile,
readonly errors: TrustedFoldersError[],
) {}
get rules(): TrustRule[] {
return Object.entries(this.user.config).map(([path, trustLevel]) => ({
path,
trustLevel,
}));
}
/**
* Returns true or false if the path should be "trusted". This function
* should only be invoked when the folder trust setting is active.
*
* @param location path
* @returns
*/
isPathTrusted(
location: string,
config?: Record<string, TrustLevel>,
headlessOptions?: HeadlessModeOptions,
): boolean | undefined {
if (isHeadlessMode(headlessOptions)) {
return true;
}
const configToUse = config ?? this.user.config;
// Resolve location to its realpath for canonical comparison
const realLocation = getRealPath(location);
let longestMatchLen = -1;
let longestMatchTrust: TrustLevel | undefined = undefined;
for (const [rulePath, trustLevel] of Object.entries(configToUse)) {
const effectivePath =
trustLevel === TrustLevel.TRUST_PARENT
? path.dirname(rulePath)
: rulePath;
// Resolve effectivePath to its realpath for canonical comparison
const realEffectivePath = getRealPath(effectivePath);
if (isWithinRoot(realLocation, realEffectivePath)) {
if (rulePath.length > longestMatchLen) {
longestMatchLen = rulePath.length;
longestMatchTrust = trustLevel;
}
}
}
if (longestMatchTrust === TrustLevel.DO_NOT_TRUST) return false;
if (
longestMatchTrust === TrustLevel.TRUST_FOLDER ||
longestMatchTrust === TrustLevel.TRUST_PARENT
)
return true;
return undefined;
}
async setValue(folderPath: string, trustLevel: TrustLevel): Promise<void> {
if (this.errors.length > 0) {
const errorMessages = this.errors.map(
(error) => `Error in ${error.path}: ${error.message}`,
);
throw new FatalConfigError(
`Cannot update trusted folders because the configuration file is invalid:\n${errorMessages.join('\n')}\nPlease fix the file manually before trying to update it.`,
);
}
const dirPath = path.dirname(this.user.path);
if (!fs.existsSync(dirPath)) {
await fsPromises.mkdir(dirPath, { recursive: true });
}
// lockfile requires the file to exist
if (!fs.existsSync(this.user.path)) {
await fsPromises.writeFile(this.user.path, JSON.stringify({}, null, 2), {
mode: 0o600,
});
}
const release = await lock(this.user.path, {
retries: {
retries: 10,
minTimeout: 100,
},
});
try {
// Re-read the file to handle concurrent updates
const content = await fsPromises.readFile(this.user.path, 'utf-8');
let config: Record<string, TrustLevel>;
try {
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
config = parseTrustedFoldersJson(content) as Record<string, TrustLevel>;
} catch (error) {
coreEvents.emitFeedback(
'error',
`Failed to parse trusted folders file at ${this.user.path}. The file may be corrupted.`,
error,
);
config = {};
}
const originalTrustLevel = config[folderPath];
config[folderPath] = trustLevel;
this.user.config[folderPath] = trustLevel;
try {
saveTrustedFolders({ ...this.user, config });
} catch (e) {
// Revert the in-memory change if the save failed.
if (originalTrustLevel === undefined) {
delete this.user.config[folderPath];
} else {
this.user.config[folderPath] = originalTrustLevel;
}
throw e;
}
} finally {
await release();
}
}
}
let loadedTrustedFolders: LoadedTrustedFolders | undefined;
/**
* FOR TESTING PURPOSES ONLY.
* Resets the in-memory cache of the trusted folders configuration.
*/
export function resetTrustedFoldersForTesting(): void {
loadedTrustedFolders = undefined;
clearRealPathCacheForTesting();
}
export function loadTrustedFolders(): LoadedTrustedFolders {
if (loadedTrustedFolders) {
return loadedTrustedFolders;
}
const errors: TrustedFoldersError[] = [];
const userConfig: Record<string, TrustLevel> = {};
const userPath = getTrustedFoldersPath();
try {
if (fs.existsSync(userPath)) {
const content = fs.readFileSync(userPath, 'utf-8');
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
const parsed = parseTrustedFoldersJson(content) as Record<string, string>;
if (
typeof parsed !== 'object' ||
parsed === null ||
Array.isArray(parsed)
) {
errors.push({
message: 'Trusted folders file is not a valid JSON object.',
path: userPath,
});
} else {
for (const [path, trustLevel] of Object.entries(parsed)) {
if (isTrustLevel(trustLevel)) {
userConfig[path] = trustLevel;
} else {
const possibleValues = Object.values(TrustLevel).join(', ');
errors.push({
message: `Invalid trust level "${trustLevel}" for path "${path}". Possible values are: ${possibleValues}.`,
path: userPath,
});
}
}
}
}
} catch (error) {
errors.push({
message: getErrorMessage(error),
path: userPath,
});
}
loadedTrustedFolders = new LoadedTrustedFolders(
{ path: userPath, config: userConfig },
errors,
);
return loadedTrustedFolders;
}
export function saveTrustedFolders(
trustedFoldersFile: TrustedFoldersFile,
): void {
// Ensure the directory exists
const dirPath = path.dirname(trustedFoldersFile.path);
if (!fs.existsSync(dirPath)) {
fs.mkdirSync(dirPath, { recursive: true });
}
const content = JSON.stringify(trustedFoldersFile.config, null, 2);
const tempPath = `${trustedFoldersFile.path}.tmp.${crypto.randomUUID()}`;
try {
fs.writeFileSync(tempPath, content, {
encoding: 'utf-8',
mode: 0o600,
});
fs.renameSync(tempPath, trustedFoldersFile.path);
} catch (error) {
// Clean up temp file if it was created but rename failed
if (fs.existsSync(tempPath)) {
try {
fs.unlinkSync(tempPath);
} catch {
// Ignore cleanup errors
}
}
throw error;
}
}
export type {
TrustRule,
TrustedFoldersError,
TrustedFoldersFile,
TrustResult,
LoadedTrustedFolders,
} from '@google/gemini-cli-core';
/** Is folder trust feature enabled per the current applied settings */
export function isFolderTrustEnabled(settings: Settings): boolean {
@@ -335,57 +34,24 @@ export function isFolderTrustEnabled(settings: Settings): boolean {
return folderTrustSetting;
}
function getWorkspaceTrustFromLocalConfig(
workspaceDir: string,
trustConfig?: Record<string, TrustLevel>,
headlessOptions?: HeadlessModeOptions,
): TrustResult {
const folders = loadTrustedFolders();
const configToUse = trustConfig ?? folders.user.config;
if (folders.errors.length > 0) {
const errorMessages = folders.errors.map(
(error) => `Error in ${error.path}: ${error.message}`,
);
throw new FatalConfigError(
`${errorMessages.join('\n')}\nPlease fix the configuration file and try again.`,
);
}
const isTrusted = folders.isPathTrusted(
workspaceDir,
configToUse,
headlessOptions,
);
return {
isTrusted,
source: isTrusted !== undefined ? 'file' : undefined,
};
export function loadTrustedFolders(): LoadedTrustedFolders {
return loadCoreTrustedFolders();
}
/**
* Returns true or false if the workspace is considered "trusted".
*/
export function isWorkspaceTrusted(
settings: Settings,
workspaceDir: string = process.cwd(),
trustConfig?: Record<string, TrustLevel>,
headlessOptions?: HeadlessModeOptions,
): TrustResult {
if (isHeadlessMode(headlessOptions)) {
return { isTrusted: true, source: undefined };
}
if (!isFolderTrustEnabled(settings)) {
return { isTrusted: true, source: undefined };
}
const ideTrust = ideContextStore.get()?.workspaceState?.isTrusted;
if (ideTrust !== undefined) {
return { isTrusted: ideTrust, source: 'ide' };
}
// Fall back to the local user configuration
return getWorkspaceTrustFromLocalConfig(
workspaceDir,
trustConfig,
headlessOptions,
);
): {
isTrusted: boolean | undefined;
source: 'ide' | 'file' | 'env' | undefined;
} {
return checkPathTrust({
path: workspaceDir,
isFolderTrustEnabled: isFolderTrustEnabled(settings),
isHeadless: isHeadlessMode(headlessOptions),
});
}
+3
View File
@@ -280,6 +280,7 @@ describe('gemini.tsx main function', () => {
vi.stubEnv('GEMINI_SANDBOX', '');
vi.stubEnv('SANDBOX', '');
vi.stubEnv('SHPOOL_SESSION_NAME', '');
vi.stubEnv('GEMINI_CLI_TRUST_WORKSPACE', 'true');
initialUnhandledRejectionListeners =
process.listeners('unhandledRejection');
@@ -555,6 +556,7 @@ describe('gemini.tsx main function kitty protocol', () => {
rawOutput: undefined,
acceptRawOutputRisk: undefined,
isCommand: undefined,
skipTrust: undefined,
});
await act(async () => {
@@ -613,6 +615,7 @@ describe('gemini.tsx main function kitty protocol', () => {
rawOutput: undefined,
acceptRawOutputRisk: undefined,
isCommand: undefined,
skipTrust: undefined,
});
await act(async () => {
+24 -1
View File
@@ -532,7 +532,7 @@ export async function main() {
const { setupInitialActivityLogger } = await import(
'./utils/devtoolsService.js'
);
await setupInitialActivityLogger(config);
setupInitialActivityLogger(config);
}
// Register config for telemetry shutdown
@@ -612,6 +612,23 @@ export async function main() {
const initializationResult = await initializeApp(config, settings);
initAppHandle?.end();
import('./services/liteRtServerManager.js')
.then(({ LiteRtServerManager }) => {
const mergedGemma = settings.merged.experimental?.gemmaModelRouter;
if (!mergedGemma) return;
// Security: binaryPath and autoStartServer must come from user-scoped
// settings only to prevent workspace configs from triggering arbitrary
// binary execution.
const userGemma = settings.forScope(SettingScope.User).settings
.experimental?.gemmaModelRouter;
return LiteRtServerManager.ensureRunning({
...mergedGemma,
binaryPath: userGemma?.binaryPath,
autoStartServer: userGemma?.autoStartServer,
});
})
.catch((e) => debugLogger.warn('LiteRT auto-start import failed:', e));
if (
settings.merged.security.auth.selectedType ===
AuthType.LOGIN_WITH_GOOGLE &&
@@ -644,6 +661,12 @@ export async function main() {
cliStartupHandle?.end();
if (!config.isInteractive()) {
for (const warning of startupWarnings) {
writeToStderr(warning.message + '\n');
}
}
// Render UI, passing necessary config values. Check that there is no command line question.
if (config.isInteractive()) {
// Earlier initialization phases (like TerminalCapabilityManager resolving
+1
View File
@@ -181,6 +181,7 @@ describe('gemini.tsx main function cleanup', () => {
beforeEach(() => {
vi.clearAllMocks();
process.env['GEMINI_CLI_NO_RELAUNCH'] = 'true';
vi.stubEnv('GEMINI_CLI_TRUST_WORKSPACE', 'true');
});
afterEach(() => {
+1 -1
View File
@@ -151,7 +151,7 @@ export async function startInteractiveUI(
isScreenReaderEnabled: config.getScreenReader(),
onRender: ({ renderTime }: { renderTime: number }) => {
if (renderTime > SLOW_RENDER_MS) {
recordSlowRender(config, renderTime);
recordSlowRender(config, Math.round(renderTime));
}
profiler.reportFrameRendered();
},
+1 -1
View File
@@ -80,7 +80,7 @@ export async function runNonInteractive(
const { setupInitialActivityLogger } = await import(
'./utils/devtoolsService.js'
);
await setupInitialActivityLogger(config);
setupInitialActivityLogger(config);
}
const { stdout: workingStdout } = createWorkingStdio();
@@ -80,7 +80,7 @@ export async function runNonInteractive({
const { setupInitialActivityLogger } = await import(
'./utils/devtoolsService.js'
);
await setupInitialActivityLogger(config);
setupInitialActivityLogger(config);
}
const { stdout: workingStdout } = createWorkingStdio();
@@ -61,6 +61,7 @@ import { vimCommand } from '../ui/commands/vimCommand.js';
import { setupGithubCommand } from '../ui/commands/setupGithubCommand.js';
import { terminalSetupCommand } from '../ui/commands/terminalSetupCommand.js';
import { upgradeCommand } from '../ui/commands/upgradeCommand.js';
import { gemmaStatusCommand } from '../ui/commands/gemmaStatusCommand.js';
/**
* Loads the core, hard-coded slash commands that are an integral part
@@ -221,6 +222,7 @@ export class BuiltinCommandLoader implements ICommandLoader {
: [skillsCommand]
: []),
settingsCommand,
gemmaStatusCommand,
tasksCommand,
vimCommand,
setupGithubCommand,
@@ -88,7 +88,7 @@ describe('SkillCommandLoader', () => {
type: 'tool',
toolName: ACTIVATE_SKILL_TOOL_NAME,
toolArgs: { name: 'test-skill' },
postSubmitPrompt: undefined,
postSubmitPrompt: 'Use the skill test-skill',
});
});
@@ -46,7 +46,10 @@ export class SkillCommandLoader implements ICommandLoader {
type: 'tool',
toolName: ACTIVATE_SKILL_TOOL_NAME,
toolArgs: { name: skill.name },
postSubmitPrompt: args.trim().length > 0 ? args.trim() : undefined,
postSubmitPrompt:
args.trim().length > 0
? args.trim()
: `Use the skill ${skill.name}`,
}),
};
});
@@ -0,0 +1,68 @@
/**
* @license
* Copyright 2026 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import fs from 'node:fs';
import { beforeEach, describe, expect, it, vi } from 'vitest';
import type { GemmaModelRouterSettings } from '@google/gemini-cli-core';
const mockGetBinaryPath = vi.hoisted(() => vi.fn());
const mockIsServerRunning = vi.hoisted(() => vi.fn());
const mockStartServer = vi.hoisted(() => vi.fn());
vi.mock('../commands/gemma/platform.js', () => ({
getBinaryPath: mockGetBinaryPath,
isServerRunning: mockIsServerRunning,
}));
vi.mock('../commands/gemma/start.js', () => ({
startServer: mockStartServer,
}));
import { LiteRtServerManager } from './liteRtServerManager.js';
describe('LiteRtServerManager', () => {
beforeEach(() => {
vi.clearAllMocks();
vi.spyOn(fs, 'existsSync').mockReturnValue(true);
mockIsServerRunning.mockResolvedValue(false);
mockStartServer.mockResolvedValue(true);
});
it('uses the configured custom binary path when auto-starting', async () => {
mockGetBinaryPath.mockReturnValue('/user/lit');
const settings: GemmaModelRouterSettings = {
enabled: true,
binaryPath: '/workspace/evil',
classifier: {
host: 'http://localhost:8123',
},
};
await LiteRtServerManager.ensureRunning(settings);
expect(mockGetBinaryPath).toHaveBeenCalledTimes(1);
expect(fs.existsSync).toHaveBeenCalledWith('/user/lit');
expect(mockStartServer).toHaveBeenCalledWith('/user/lit', 8123);
});
it('falls back to the default binary path when no custom path is configured', async () => {
mockGetBinaryPath.mockReturnValue('/default/lit');
const settings: GemmaModelRouterSettings = {
enabled: true,
classifier: {
host: 'http://localhost:9379',
},
};
await LiteRtServerManager.ensureRunning(settings);
expect(mockGetBinaryPath).toHaveBeenCalledTimes(1);
expect(fs.existsSync).toHaveBeenCalledWith('/default/lit');
expect(mockStartServer).toHaveBeenCalledWith('/default/lit', 9379);
});
});
@@ -0,0 +1,59 @@
/**
* @license
* Copyright 2026 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import fs from 'node:fs';
import { debugLogger } from '@google/gemini-cli-core';
import type { GemmaModelRouterSettings } from '@google/gemini-cli-core';
import { getBinaryPath, isServerRunning } from '../commands/gemma/platform.js';
import { DEFAULT_PORT } from '../commands/gemma/constants.js';
export class LiteRtServerManager {
static async ensureRunning(
gemmaSettings: GemmaModelRouterSettings | undefined,
): Promise<void> {
if (!gemmaSettings?.enabled) return;
if (gemmaSettings.autoStartServer === false) return;
const binaryPath = getBinaryPath();
if (!binaryPath || !fs.existsSync(binaryPath)) {
debugLogger.log(
'[LiteRtServerManager] Binary not installed, skipping auto-start. Run "gemini gemma setup".',
);
return;
}
const port =
parseInt(
gemmaSettings.classifier?.host?.match(/:(\d+)/)?.[1] ?? '',
10,
) || DEFAULT_PORT;
const running = await isServerRunning(port);
if (running) {
debugLogger.log(
`[LiteRtServerManager] Server already running on port ${port}`,
);
return;
}
debugLogger.log(
`[LiteRtServerManager] Auto-starting LiteRT server on port ${port}...`,
);
try {
const { startServer } = await import('../commands/gemma/start.js');
const started = await startServer(binaryPath, port);
if (started) {
debugLogger.log(`[LiteRtServerManager] Server started on port ${port}`);
} else {
debugLogger.warn(
`[LiteRtServerManager] Server may not have started correctly on port ${port}`,
);
}
} catch (error) {
debugLogger.warn('[LiteRtServerManager] Auto-start failed:', error);
}
}
}
+7 -1
View File
@@ -38,12 +38,14 @@ export const createMockConfig = (overrides: Partial<Config> = {}): Config =>
fireSessionEndEvent: vi.fn().mockResolvedValue(undefined),
fireSessionStartEvent: vi.fn().mockResolvedValue(undefined),
})),
isMemoryManagerEnabled: vi.fn(() => false),
isMemoryV2Enabled: vi.fn(() => false),
isAutoMemoryEnabled: vi.fn(() => false),
getListExtensions: vi.fn(() => false),
getExtensions: vi.fn(() => []),
getListSessions: vi.fn(() => false),
getDeleteSession: vi.fn(() => undefined),
setSessionId: vi.fn(),
resetNewSessionState: vi.fn(),
getSessionId: vi.fn().mockReturnValue('mock-session-id'),
getWorktreeSettings: vi.fn(() => undefined),
getContentGeneratorConfig: vi.fn(() => ({ authType: 'google' })),
@@ -87,6 +89,7 @@ export const createMockConfig = (overrides: Partial<Config> = {}): Config =>
getAccessibility: vi.fn().mockReturnValue({}),
getTelemetryEnabled: vi.fn().mockReturnValue(false),
getTelemetryLogPromptsEnabled: vi.fn().mockReturnValue(false),
getTelemetryTracesEnabled: vi.fn().mockReturnValue(false),
getTelemetryOtlpEndpoint: vi.fn().mockReturnValue(''),
getTelemetryOtlpProtocol: vi.fn().mockReturnValue('grpc'),
getTelemetryTarget: vi.fn().mockReturnValue(''),
@@ -121,6 +124,9 @@ export const createMockConfig = (overrides: Partial<Config> = {}): Config =>
getCompressionThreshold: vi.fn().mockResolvedValue(undefined),
getUserCaching: vi.fn().mockResolvedValue(false),
getNumericalRoutingEnabled: vi.fn().mockResolvedValue(false),
getBestEffortProEnabled: vi.fn().mockResolvedValue(false),
getProTimeoutMinutes: vi.fn().mockResolvedValue(5),
getProTimeoutFallbackDurationMinutes: vi.fn().mockResolvedValue(60),
getClassifierThreshold: vi.fn().mockResolvedValue(undefined),
getBannerTextNoCapacityIssues: vi.fn().mockResolvedValue(''),
getBannerTextCapacityIssues: vi.fn().mockResolvedValue(''),
@@ -53,6 +53,7 @@ const mocks = vi.hoisted(() => ({
const terminalNotificationsMocks = vi.hoisted(() => ({
notifyViaTerminal: vi.fn().mockResolvedValue(true),
isNotificationsEnabled: vi.fn(() => true),
getNotificationMethod: vi.fn(() => 'auto'),
buildRunEventNotificationContent: vi.fn((event) => ({
title: 'Mock Notification',
subtitle: 'Mock Subtitle',
@@ -194,6 +195,7 @@ vi.mock('./hooks/useShellInactivityStatus.js', () => ({
vi.mock('../utils/terminalNotifications.js', () => ({
notifyViaTerminal: terminalNotificationsMocks.notifyViaTerminal,
isNotificationsEnabled: terminalNotificationsMocks.isNotificationsEnabled,
getNotificationMethod: terminalNotificationsMocks.getNotificationMethod,
buildRunEventNotificationContent:
terminalNotificationsMocks.buildRunEventNotificationContent,
}));
+10 -8
View File
@@ -92,7 +92,6 @@ import {
ApiKeyUpdatedEvent,
LegacyAgentProtocol,
type InjectionSource,
startMemoryService,
} from '@google/gemini-cli-core';
import { validateAuthMethod } from '../config/auth.js';
import process from 'node:process';
@@ -125,6 +124,7 @@ import { type BackgroundTask } from './hooks/useExecutionLifecycle.js';
import { useVim } from './hooks/vim.js';
import { type LoadableSettingScope, SettingScope } from '../config/settings.js';
import { type InitializationResult } from '../core/initializer.js';
import { startAutoMemoryIfEnabled } from '../utils/autoMemory.js';
import { useFocus } from './hooks/useFocus.js';
import { useKeypress, type Key } from './hooks/useKeypress.js';
import { KeypressPriority } from './contexts/KeypressContext.js';
@@ -181,7 +181,10 @@ import { useTimedMessage } from './hooks/useTimedMessage.js';
import { useIsHelpDismissKey } from './utils/shortcutsHelp.js';
import { useSuspend } from './hooks/useSuspend.js';
import { useRunEventNotifications } from './hooks/useRunEventNotifications.js';
import { isNotificationsEnabled } from '../utils/terminalNotifications.js';
import {
isNotificationsEnabled,
getNotificationMethod,
} from '../utils/terminalNotifications.js';
import {
getLastTurnToolCallIds,
isToolExecuting,
@@ -225,6 +228,7 @@ export const AppContainer = (props: AppContainerProps) => {
const settings = useSettings();
const { reset } = useOverflowActions()!;
const notificationsEnabled = isNotificationsEnabled(settings);
const notificationMethod = getNotificationMethod(settings);
const { setOptions, dumpCurrentFrame, startRecording, stopRecording } =
useContext(InkAppContext);
@@ -482,12 +486,7 @@ export const AppContainer = (props: AppContainerProps) => {
setConfigInitialized(true);
startupProfiler.flush(config);
// Fire-and-forget memory service (skill extraction from past sessions)
if (config.isMemoryManagerEnabled()) {
startMemoryService(config).catch((e) => {
debugLogger.error('Failed to start memory service:', e);
});
}
startAutoMemoryIfEnabled(config);
const sessionStartSource = resumedSessionData
? SessionStartSource.Resume
@@ -973,6 +972,7 @@ Logging in with Google... Restarting Gemini CLI to continue.
openAgentConfigDialog,
openPermissionsDialog,
quit: (messages: HistoryItem[]) => {
closeThemeDialog();
setQuittingMessages(messages);
setTimeout(async () => {
await runExitCleanup();
@@ -1001,6 +1001,7 @@ Logging in with Google... Restarting Gemini CLI to continue.
[
setAuthState,
openThemeDialog,
closeThemeDialog,
openEditorDialog,
openSettingsDialog,
openSessionBrowser,
@@ -2284,6 +2285,7 @@ Logging in with Google... Restarting Gemini CLI to continue.
useRunEventNotifications({
notificationsEnabled,
notificationMethod,
isFocused,
hasReceivedFocusEvent,
streamingState,
@@ -10,8 +10,7 @@
<rect x="27" y="0" width="324" height="17" fill="#141414" />
<text x="27" y="2" fill="#ffffff" textLength="324" lengthAdjust="spacingAndGlyphs">Can you edit InputPrompt.tsx for me?</text>
<rect x="351" y="0" width="549" height="17" fill="#141414" />
<rect x="0" y="17" width="900" height="17" fill="#141414" />
<text x="0" y="19" fill="#000000" textLength="900" lengthAdjust="spacingAndGlyphs">▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄</text>
<text x="0" y="19" fill="#141414" textLength="900" lengthAdjust="spacingAndGlyphs">▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀</text>
<text x="0" y="53" fill="#333333" textLength="891" lengthAdjust="spacingAndGlyphs">╭─────────────────────────────────────────────────────────────────────────────────────────────────╮</text>
<text x="0" y="70" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="18" y="70" fill="#ffffaf" textLength="63" lengthAdjust="spacingAndGlyphs" font-weight="bold">? Edit </text>

Before

Width:  |  Height:  |  Size: 29 KiB

After

Width:  |  Height:  |  Size: 29 KiB

@@ -2,7 +2,7 @@
exports[`Full Terminal Tool Confirmation Snapshot > renders tool confirmation box in the frame of the entire terminal 1`] = `
" > Can you edit InputPrompt.tsx for me?
▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄
▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
╭─────────────────────────────────────────────────────────────────────────────────────────────────╮
│ ? Edit packages/.../InputPrompt.tsx: return kittyProtocolSupporte... => return kittyProto… │
+1 -1
View File
@@ -52,7 +52,7 @@ export function ApiAuthDialog({
height: 4,
},
inputFilter: (text) =>
text.replace(/[^a-zA-Z0-9_-]/g, '').replace(/[\r\n]/g, ''),
text.replace(/[^a-zA-Z0-9_.-]/g, '').replace(/[\r\n]/g, ''),
singleLine: true,
});

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