Compare commits

...

9 Commits

37 changed files with 1085 additions and 380 deletions
+1 -2
View File
@@ -2,8 +2,7 @@
"experimental": {
"extensionReloading": true,
"modelSteering": true,
"memoryManager": true,
"topicUpdateNarration": true
"memoryManager": 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
@@ -22,3 +22,4 @@ Thumbs.db
.pytest_cache
**/SKILL.md
packages/sdk/test-data/*.json
*.mdx
+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 -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.
+12 -11
View File
@@ -39,6 +39,7 @@ they appear in the UI.
| 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
@@ -160,17 +161,17 @@ 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` |
| 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` |
| 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
+1 -1
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`
+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.
+14 -4
View File
@@ -198,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):
@@ -1724,6 +1729,12 @@ their corresponding top-level category object in your `settings.json` file.
- **Default:** `false`
- **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
- **`experimental.generalistProfile`** (boolean):
- **Description:** Suitable for general coding and software development tasks.
- **Default:** `false`
@@ -1735,8 +1746,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`
@@ -2075,7 +2085,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
@@ -2096,7 +2106,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`**:
+15
View File
@@ -92,6 +92,21 @@ 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 |
+5
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" },
{
+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.
+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 -5
View File
@@ -43,7 +43,7 @@ describe('update_topic_behavior', () => {
2,
),
'.gemini/settings.json': JSON.stringify({
experimental: {
general: {
topicUpdateNarration: true,
},
}),
@@ -127,7 +127,7 @@ describe('update_topic_behavior', () => {
'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,
},
}),
@@ -156,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,
},
}),
@@ -204,7 +204,7 @@ app.post('/users', (req, res) => {
export default app;
`,
'.gemini/settings.json': JSON.stringify({
experimental: {
general: {
topicUpdateNarration: true,
},
}),
@@ -249,7 +249,7 @@ export default app;
files: {
'package.json': JSON.stringify({ name: 'test-project' }),
'.gemini/settings.json': JSON.stringify({
experimental: {
general: {
topicUpdateNarration: true,
},
}),
+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.',
};
}
+4 -1
View File
@@ -990,9 +990,12 @@ export async function loadCliConfig(
disabledSkills: settings.skills?.disabled,
experimentalJitContext: settings.experimental?.jitContext,
experimentalMemoryManager: settings.experimental?.memoryManager,
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,
+22 -3
View File
@@ -418,6 +418,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: {
@@ -2203,6 +2213,16 @@ const SETTINGS_SCHEMA = {
'Replace the built-in save_memory tool with a memory manager subagent that supports adding, removing, de-duplicating, and organizing memories.',
showInDialog: true,
},
autoMemory: {
type: 'boolean',
label: 'Auto Memory',
category: 'Experimental',
requiresRestart: true,
default: false,
description:
'Automatically extract reusable skills from past sessions in the background. Review results with /memory inbox.',
showInDialog: true,
},
generalistProfile: {
type: 'boolean',
label: 'Use the generalist profile to manage agent contexts.',
@@ -2228,9 +2248,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,
},
},
},
+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();
},
@@ -39,6 +39,7 @@ export const createMockConfig = (overrides: Partial<Config> = {}): Config =>
fireSessionStartEvent: vi.fn().mockResolvedValue(undefined),
})),
isMemoryManagerEnabled: vi.fn(() => false),
isAutoMemoryEnabled: vi.fn(() => false),
getListExtensions: vi.fn(() => false),
getExtensions: vi.fn(() => []),
getListSessions: vi.fn(() => false),
+4 -2
View File
@@ -486,8 +486,8 @@ export const AppContainer = (props: AppContainerProps) => {
setConfigInitialized(true);
startupProfiler.flush(config);
// Fire-and-forget memory service (skill extraction from past sessions)
if (config.isMemoryManagerEnabled()) {
// Fire-and-forget Auto Memory service (skill extraction from past sessions)
if (config.isAutoMemoryEnabled()) {
startMemoryService(config).catch((e) => {
debugLogger.error('Failed to start memory service:', e);
});
@@ -977,6 +977,7 @@ Logging in with Google... Restarting Gemini CLI to continue.
openAgentConfigDialog,
openPermissionsDialog,
quit: (messages: HistoryItem[]) => {
closeThemeDialog();
setQuittingMessages(messages);
setTimeout(async () => {
await runExitCleanup();
@@ -1005,6 +1006,7 @@ Logging in with Google... Restarting Gemini CLI to continue.
[
setAuthState,
openThemeDialog,
closeThemeDialog,
openEditorDialog,
openSettingsDialog,
openSessionBrowser,
@@ -473,7 +473,7 @@ describe('memoryCommand', () => {
const mockConfig = {
reloadSkills: vi.fn(),
isMemoryManagerEnabled: vi.fn().mockReturnValue(true),
isAutoMemoryEnabled: vi.fn().mockReturnValue(true),
};
const context = createMockCommandContext({
services: {
@@ -491,11 +491,11 @@ describe('memoryCommand', () => {
expect(result).toHaveProperty('component');
});
it('should return info message when memory manager is disabled', () => {
it('should return info message when auto memory is disabled', () => {
if (!inboxCommand.action) throw new Error('Command has no action');
const mockConfig = {
isMemoryManagerEnabled: vi.fn().mockReturnValue(false),
isAutoMemoryEnabled: vi.fn().mockReturnValue(false),
};
const context = createMockCommandContext({
services: {
@@ -509,7 +509,7 @@ describe('memoryCommand', () => {
type: 'message',
messageType: 'info',
content:
'The memory inbox requires the experimental memory manager. Enable it with: experimental.memoryManager = true in settings.',
'The memory inbox requires Auto Memory. Enable it with: experimental.autoMemory = true in settings.',
});
});
@@ -145,12 +145,12 @@ export const memoryCommand: SlashCommand = {
};
}
if (!config.isMemoryManagerEnabled()) {
if (!config.isAutoMemoryEnabled()) {
return {
type: 'message',
messageType: 'info',
content:
'The memory inbox requires the experimental memory manager. Enable it with: experimental.memoryManager = true in settings.',
'The memory inbox requires Auto Memory. Enable it with: experimental.autoMemory = true in settings.',
};
}
+2 -1
View File
@@ -55,8 +55,9 @@ export const GeneralistAgent = (
return {
systemPrompt: getCoreSystemPrompt(
context.config,
/*useMemory=*/ undefined,
/*userMemory=*/ undefined,
/*interactiveOverride=*/ false,
/*topicUpdateNarrationOverride=*/ false,
),
query: '${request}',
};
@@ -82,6 +82,7 @@ import { CompleteTaskTool } from '../tools/complete-task.js';
import {
COMPLETE_TASK_TOOL_NAME,
ACTIVATE_SKILL_TOOL_NAME,
UPDATE_TOPIC_TOOL_NAME,
} from '../tools/definitions/base-declarations.js';
/** A callback function to report on agent activity. */
@@ -189,6 +190,10 @@ export class LocalAgentExecutor<TOutput extends z.ZodTypeAny> {
return;
}
if (tool.name === UPDATE_TOPIC_TOOL_NAME) {
return;
}
// Clone the tool, so it gets its own state and subagent messageBus
const clonedTool = tool.clone(subagentMessageBus);
agentToolRegistry.registerTool(clonedTool);
+118
View File
@@ -349,6 +349,27 @@ describe('Server Config (config.ts)', () => {
}),
);
});
it('should ignore properties that are explicitly undefined and preserve existing values', () => {
const config = new Config(baseParams);
config.setShellExecutionConfig({
terminalWidth: 80,
showColor: true,
});
expect(config.getShellExecutionConfig().terminalWidth).toBe(80);
expect(config.getShellExecutionConfig().showColor).toBe(true);
// Provide undefined for terminalWidth, which should be ignored
config.setShellExecutionConfig({
terminalWidth: undefined,
showColor: false,
});
expect(config.getShellExecutionConfig().terminalWidth).toBe(80); // Should still be 80, not undefined
expect(config.getShellExecutionConfig().showColor).toBe(false); // Should be updated
});
});
beforeEach(() => {
@@ -689,6 +710,59 @@ describe('Server Config (config.ts)', () => {
);
});
describe('getProModelNoAccessSync', () => {
it('should return experiment value for AuthType.LOGIN_WITH_GOOGLE', async () => {
vi.mocked(getExperiments).mockResolvedValue({
experimentIds: [],
flags: {
[ExperimentFlags.PRO_MODEL_NO_ACCESS]: {
boolValue: true,
},
},
});
const config = new Config(baseParams);
vi.mocked(createContentGeneratorConfig).mockResolvedValue({
authType: AuthType.LOGIN_WITH_GOOGLE,
});
await config.refreshAuth(AuthType.LOGIN_WITH_GOOGLE);
expect(config.getProModelNoAccessSync()).toBe(true);
});
it('should return experiment value for AuthType.COMPUTE_ADC', async () => {
vi.mocked(getExperiments).mockResolvedValue({
experimentIds: [],
flags: {
[ExperimentFlags.PRO_MODEL_NO_ACCESS]: {
boolValue: true,
},
},
});
const config = new Config(baseParams);
vi.mocked(createContentGeneratorConfig).mockResolvedValue({
authType: AuthType.COMPUTE_ADC,
});
await config.refreshAuth(AuthType.COMPUTE_ADC);
expect(config.getProModelNoAccessSync()).toBe(true);
});
it('should return false for other auth types even if experiment is true', async () => {
vi.mocked(getExperiments).mockResolvedValue({
experimentIds: [],
flags: {
[ExperimentFlags.PRO_MODEL_NO_ACCESS]: {
boolValue: true,
},
},
});
const config = new Config(baseParams);
vi.mocked(createContentGeneratorConfig).mockResolvedValue({
authType: AuthType.USE_GEMINI,
});
await config.refreshAuth(AuthType.USE_GEMINI);
expect(config.getProModelNoAccessSync()).toBe(false);
});
});
describe('getRequestTimeoutMs', () => {
it('should return undefined if the flag is not set', () => {
const config = new Config(baseParams);
@@ -3422,6 +3496,50 @@ describe('Config JIT Initialization', () => {
});
});
describe('isAutoMemoryEnabled', () => {
it('should default to false', () => {
const params: ConfigParameters = {
sessionId: 'test-session',
targetDir: '/tmp/test',
debugMode: false,
model: 'test-model',
cwd: '/tmp/test',
};
config = new Config(params);
expect(config.isAutoMemoryEnabled()).toBe(false);
});
it('should return true when experimentalAutoMemory is true', () => {
const params: ConfigParameters = {
sessionId: 'test-session',
targetDir: '/tmp/test',
debugMode: false,
model: 'test-model',
cwd: '/tmp/test',
experimentalAutoMemory: true,
};
config = new Config(params);
expect(config.isAutoMemoryEnabled()).toBe(true);
});
it('should be independent of experimentalMemoryManager', () => {
const params: ConfigParameters = {
sessionId: 'test-session',
targetDir: '/tmp/test',
debugMode: false,
model: 'test-model',
cwd: '/tmp/test',
experimentalMemoryManager: true,
};
config = new Config(params);
expect(config.isMemoryManagerEnabled()).toBe(true);
expect(config.isAutoMemoryEnabled()).toBe(false);
});
});
describe('reloadSkills', () => {
it('should refresh disabledSkills and re-register ActivateSkillTool when skills exist', async () => {
const mockOnReload = vi.fn().mockResolvedValue({
+27 -14
View File
@@ -701,6 +701,7 @@ export interface ConfigParameters {
experimentalJitContext?: boolean;
autoDistillation?: boolean;
experimentalMemoryManager?: boolean;
experimentalAutoMemory?: boolean;
experimentalContextManagementConfig?: string;
experimentalAgentHistoryTruncation?: boolean;
experimentalAgentHistoryTruncationThreshold?: number;
@@ -942,6 +943,7 @@ export class Config implements McpContext, AgentLoopContext {
private readonly adminSkillsEnabled: boolean;
private readonly experimentalJitContext: boolean;
private readonly experimentalMemoryManager: boolean;
private readonly experimentalAutoMemory: boolean;
private readonly experimentalContextManagementConfig?: string;
private readonly memoryBoundaryMarkers: readonly string[];
private readonly topicUpdateNarration: boolean;
@@ -1154,6 +1156,7 @@ export class Config implements McpContext, AgentLoopContext {
this.experimentalJitContext = params.experimentalJitContext ?? false;
this.experimentalMemoryManager = params.experimentalMemoryManager ?? false;
this.experimentalAutoMemory = params.experimentalAutoMemory ?? false;
this.experimentalContextManagementConfig =
params.experimentalContextManagementConfig;
this.memoryBoundaryMarkers = params.memoryBoundaryMarkers ?? ['.git'];
@@ -1196,7 +1199,7 @@ export class Config implements McpContext, AgentLoopContext {
},
},
};
this.topicUpdateNarration = params.topicUpdateNarration ?? false;
this.topicUpdateNarration = params.topicUpdateNarration ?? true;
this.modelSteering = params.modelSteering ?? false;
this.injectionService = new InjectionService(() =>
this.isModelSteeringEnabled(),
@@ -2486,6 +2489,10 @@ export class Config implements McpContext, AgentLoopContext {
return this.experimentalMemoryManager;
}
isAutoMemoryEnabled(): boolean {
return this.experimentalAutoMemory;
}
getExperimentalContextManagementConfig(): string | undefined {
return this.experimentalContextManagementConfig;
}
@@ -3153,7 +3160,10 @@ export class Config implements McpContext, AgentLoopContext {
* Note: This method should only be called after startup, once experiments have been loaded.
*/
getProModelNoAccessSync(): boolean {
if (this.contentGeneratorConfig?.authType !== AuthType.LOGIN_WITH_GOOGLE) {
if (
this.contentGeneratorConfig?.authType !== AuthType.LOGIN_WITH_GOOGLE &&
this.contentGeneratorConfig?.authType !== AuthType.COMPUTE_ADC
) {
return false;
}
return (
@@ -3396,20 +3406,23 @@ export class Config implements McpContext, AgentLoopContext {
return this.shellExecutionConfig;
}
setShellExecutionConfig(config: ShellExecutionConfig): void {
setShellExecutionConfig(config: Partial<ShellExecutionConfig>): void {
const definedConfig: Partial<ShellExecutionConfig> = {};
for (const [k, v] of Object.entries(config)) {
// Only merge properties explicitly provided with a concrete value.
// Filtering out `null` and `undefined` ensures existing system defaults
// are preserved when an extension doesn't want to override them.
if (v != null) {
Object.assign(definedConfig, { [k]: v });
}
}
// Note: This performs a shallow merge. If the incoming config provides a nested
// object (e.g., sandboxConfig), it will completely overwrite the existing
// nested object rather than merging its individual properties.
this.shellExecutionConfig = {
...this.shellExecutionConfig,
terminalWidth:
config.terminalWidth ?? this.shellExecutionConfig.terminalWidth,
terminalHeight:
config.terminalHeight ?? this.shellExecutionConfig.terminalHeight,
showColor: config.showColor ?? this.shellExecutionConfig.showColor,
pager: config.pager ?? this.shellExecutionConfig.pager,
sanitizationConfig:
config.sanitizationConfig ??
this.shellExecutionConfig.sanitizationConfig,
sandboxManager:
config.sandboxManager ?? this.shellExecutionConfig.sandboxManager,
...definedConfig,
};
}
getScreenReader(): boolean {
@@ -300,4 +300,78 @@ describe('ProjectRegistry', () => {
'ProjectRegistry must be initialized before use',
);
});
it('retries on EBUSY during save', async () => {
const registry = new ProjectRegistry(registryPath);
await registry.initialize();
const originalRename = fs.promises.rename;
const renameSpy = vi.spyOn(fs.promises, 'rename');
let ebusyCount = 0;
renameSpy.mockImplementation(async (oldPath, newPath) => {
// Only throw for the specific temporary file generated by save()
if (oldPath.toString().includes('.tmp') && ebusyCount < 2) {
ebusyCount++;
const err = Object.assign(new Error('Resource busy or locked'), {
code: 'EBUSY',
});
throw err;
}
// On success, call the original native rename implementation
return originalRename(oldPath, newPath);
});
const projectPath = path.join(tempDir, 'ebusy-project');
const shortId = await registry.getShortId(projectPath);
expect(shortId).toBe('ebusy-project');
expect(ebusyCount).toBe(2);
// Verify it actually saved properly after retries
const data = JSON.parse(fs.readFileSync(registryPath, 'utf8'));
expect(data.projects[normalizePath(projectPath)]).toBe('ebusy-project');
renameSpy.mockRestore();
});
it('re-throws error if save ultimately fails after retries', async () => {
const registry = new ProjectRegistry(registryPath);
await registry.initialize();
const renameSpy = vi.spyOn(fs.promises, 'rename');
const expectedError = Object.assign(new Error('Persistent EBUSY'), {
code: 'EBUSY',
});
// Mock rename to ALWAYS fail
renameSpy.mockRejectedValue(expectedError);
const projectPath = path.join(tempDir, 'failing-project');
await expect(registry.getShortId(projectPath)).rejects.toThrow(
'Persistent EBUSY',
);
renameSpy.mockRestore();
});
it('protects against data destruction by throwing on EACCES instead of resetting', async () => {
// 1. Write valid registry data
fs.writeFileSync(
registryPath,
JSON.stringify({ projects: { '/foo': 'bar' } }),
);
const registry = new ProjectRegistry(registryPath);
// 2. Mock readFile to throw a permissions error
const readFileSpy = vi.spyOn(fs.promises, 'readFile');
readFileSpy.mockRejectedValue(
Object.assign(new Error('Permission denied'), { code: 'EACCES' }),
);
// 3. Initialization should NOT swallow the error
await expect(registry.initialize()).rejects.toThrow('Permission denied');
readFileSpy.mockRestore();
});
});
+122 -35
View File
@@ -10,6 +10,7 @@ import * as path from 'node:path';
import * as os from 'node:os';
import { lock } from 'proper-lockfile';
import { debugLogger } from '../utils/debugLogger.js';
import { isNodeError } from '../utils/errors.js';
export interface RegistryData {
projects: Record<string, string>;
@@ -54,18 +55,27 @@ export class ProjectRegistry {
}
private async loadData(): Promise<RegistryData> {
if (!fs.existsSync(this.registryPath)) {
return { projects: {} };
}
try {
const content = await fs.promises.readFile(this.registryPath, 'utf8');
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
return JSON.parse(content);
} catch (e) {
debugLogger.debug('Failed to load registry: ', e);
// If the registry is corrupted, we'll start fresh to avoid blocking the CLI
return { projects: {} };
} catch (error: unknown) {
if (isNodeError(error) && error.code === 'ENOENT') {
return { projects: {} }; // Normal first run
}
if (error instanceof SyntaxError) {
debugLogger.warn(
'Failed to load registry (JSON corrupted), resetting to empty: ',
error,
);
// Ownership markers on disk will allow self-healing when short IDs are requested.
return { projects: {} };
}
// If it's a real filesystem error (e.g. EACCES permission denied), DO NOT swallow it.
// Swallowing read errors and overwriting the file would permanently destroy user data.
debugLogger.error('Critical failure reading project registry:', error);
throw error;
}
}
@@ -82,18 +92,54 @@ export class ProjectRegistry {
if (!fs.existsSync(dir)) {
await fs.promises.mkdir(dir, { recursive: true });
}
// Use a randomized tmp path to avoid ENOENT crashes when save() is called concurrently
const tmpPath = this.registryPath + '.' + randomUUID() + '.tmp';
let savedSuccessfully = false;
try {
// Unconditionally ensure the directory exists; recursive ignores EEXIST.
await fs.promises.mkdir(dir, { recursive: true });
const content = JSON.stringify(data, null, 2);
// Use a randomized tmp path to avoid ENOENT crashes when save() is called concurrently
const tmpPath = this.registryPath + '.' + randomUUID() + '.tmp';
await fs.promises.writeFile(tmpPath, content, 'utf8');
await fs.promises.rename(tmpPath, this.registryPath);
// Exponential backoff for OS-level file locks (EBUSY/EPERM) during rename
const maxRetries = 5;
for (let attempt = 0; attempt < maxRetries; attempt++) {
try {
await fs.promises.rename(tmpPath, this.registryPath);
savedSuccessfully = true;
break; // Success, exit the retry loop
} catch (error: unknown) {
const code = isNodeError(error) ? error.code : '';
const isRetryable = code === 'EBUSY' || code === 'EPERM';
if (!isRetryable || attempt === maxRetries - 1) {
throw error; // Throw immediately on fatal error or final attempt
}
const delayMs = Math.pow(2, attempt) * 50;
debugLogger.debug(
`Rename failed with ${code}, retrying in ${delayMs}ms (attempt ${attempt + 1}/${maxRetries})...`,
);
await new Promise((resolve) => setTimeout(resolve, delayMs));
}
}
} catch (error) {
debugLogger.error(
`Failed to save project registry to ${this.registryPath}:`,
error,
);
throw error;
} finally {
// Clean up the temporary file if it was left behind (e.g. if writeFile or rename failed)
if (!savedSuccessfully) {
try {
await fs.promises.unlink(tmpPath);
} catch {
// Ignore errors during cleanup
}
}
}
}
@@ -113,9 +159,18 @@ export class ProjectRegistry {
if (!fs.existsSync(dir)) {
await fs.promises.mkdir(dir, { recursive: true });
}
// Ensure the registry file exists so proper-lockfile can lock it
// Ensure the registry file exists so proper-lockfile can lock it.
// If it doesn't exist, we try to create it. If someone else creates it
// between our check and our write, we just continue.
if (!fs.existsSync(this.registryPath)) {
await this.save({ projects: {} });
try {
await this.save({ projects: {} });
} catch (e: unknown) {
if (!fs.existsSync(this.registryPath)) {
throw e; // File still doesn't exist and save failed, this is a real error.
}
// Someone else created it while we were trying to save. Continue to locking.
}
}
// Use proper-lockfile to prevent racy updates
@@ -157,7 +212,13 @@ export class ProjectRegistry {
await this.save(currentData);
return shortId;
} finally {
await release();
try {
await release();
} catch (e) {
// Prevent proper-lockfile errors (e.g. if the lock dir was externally deleted)
// from masking the original error thrown inside the try block.
debugLogger.error('Failed to release project registry lock:', e);
}
}
}
@@ -171,20 +232,19 @@ export class ProjectRegistry {
for (const baseDir of this.baseDirs) {
const markerPath = path.join(baseDir, slug, PROJECT_ROOT_FILE);
if (fs.existsSync(markerPath)) {
try {
const owner = (await fs.promises.readFile(markerPath, 'utf8')).trim();
if (this.normalizePath(owner) !== this.normalizePath(projectPath)) {
return false;
}
} catch (e) {
debugLogger.debug(
`Failed to read ownership marker ${markerPath}:`,
e,
);
// If we can't read it, assume it's not ours or corrupted.
try {
const owner = (await fs.promises.readFile(markerPath, 'utf8')).trim();
if (this.normalizePath(owner) !== this.normalizePath(projectPath)) {
return false;
}
} catch (e: unknown) {
if (isNodeError(e) && e.code === 'ENOENT') {
// Marker doesn't exist, this is fine, we just won't fail verification
continue;
}
debugLogger.debug(`Failed to read ownership marker ${markerPath}:`, e);
// If we can't read it for other reasons (perms, corrupted), assume not ours.
return false;
}
}
return true;
@@ -276,10 +336,22 @@ export class ProjectRegistry {
try {
await this.ensureOwnershipMarkers(candidate, projectPath);
return candidate;
} catch {
// Someone might have claimed it between our check and our write.
// Try next candidate.
continue;
} catch (error: unknown) {
// Only retry if it was a collision (someone else took the slug)
// or a race condition during marker creation.
const code = isNodeError(error) ? error.code : '';
const isCollision =
code === 'EEXIST' ||
(error instanceof Error &&
error.message.includes('already owned by'));
if (isCollision) {
debugLogger.debug(`Slug collision for ${candidate}, trying next...`);
continue;
}
// Fatal error (Permission denied, Disk full, etc.)
throw error;
}
}
}
@@ -301,13 +373,28 @@ export class ProjectRegistry {
continue;
}
// Collision!
throw new Error(`Slug ${slug} is already owned by ${owner}`);
const error = Object.assign(
new Error(`Slug ${slug} is already owned by ${owner}`),
{ code: 'EEXIST' },
);
throw error;
}
// Use flag: 'wx' to ensure atomic creation
await fs.promises.writeFile(markerPath, normalizedProject, {
encoding: 'utf8',
flag: 'wx',
});
try {
await fs.promises.writeFile(markerPath, normalizedProject, {
encoding: 'utf8',
flag: 'wx',
});
} catch (e: unknown) {
if (isNodeError(e) && e.code === 'EEXIST') {
// Re-verify ownership in case we just lost a race
const owner = (await fs.promises.readFile(markerPath, 'utf8')).trim();
if (this.normalizePath(owner) === normalizedProject) {
continue;
}
}
throw e;
}
}
}
+2
View File
@@ -24,11 +24,13 @@ export function getCoreSystemPrompt(
config: Config,
userMemory?: string | HierarchicalMemory,
interactiveOverride?: boolean,
topicUpdateNarrationOverride?: boolean,
): string {
return new PromptProvider().getCoreSystemPrompt(
config,
userMemory,
interactiveOverride,
topicUpdateNarrationOverride,
);
}
@@ -277,6 +277,56 @@ describe('PromptProvider', () => {
});
});
describe('topicUpdateNarrationOverride', () => {
let provider: PromptProvider;
beforeEach(() => {
provider = new PromptProvider();
mockConfig.topicState.reset();
(mockConfig.getToolRegistry as ReturnType<typeof vi.fn>).mockReturnValue({
getAllToolNames: vi.fn().mockReturnValue([UPDATE_TOPIC_TOOL_NAME]),
});
(mockConfig.getAgentRegistry as ReturnType<typeof vi.fn>).mockReturnValue(
{
getAllDefinitions: vi.fn().mockReturnValue([]),
getDefinition: vi.fn().mockReturnValue(undefined),
},
);
});
it('should disable topic update narration when override is false, even if config is true', () => {
vi.mocked(mockConfig.isTopicUpdateNarrationEnabled).mockReturnValue(true);
const prompt = provider.getCoreSystemPrompt(
mockConfig as unknown as Config,
/*userMemory=*/ undefined,
/*interactiveOverride=*/ undefined,
/*topicUpdateNarrationOverride=*/ false,
);
expect(prompt).not.toContain(
`As you work, the user follows along by reading topic updates that you publish with ${UPDATE_TOPIC_TOOL_NAME}.`,
);
});
it('should enable topic update narration when override is true, even if config is false', () => {
vi.mocked(mockConfig.isTopicUpdateNarrationEnabled).mockReturnValue(
false,
);
const prompt = provider.getCoreSystemPrompt(
mockConfig as unknown as Config,
/*userMemory=*/ undefined,
/*interactiveOverride=*/ undefined,
/*topicUpdateNarrationOverride=*/ true,
);
expect(prompt).toContain(
`As you work, the user follows along by reading topic updates that you publish with ${UPDATE_TOPIC_TOOL_NAME}.`,
);
});
});
describe('Topic & Update Narration', () => {
beforeEach(() => {
mockConfig.topicState.reset();
+9 -6
View File
@@ -44,6 +44,7 @@ export class PromptProvider {
context: AgentLoopContext,
userMemory?: string | HierarchicalMemory,
interactiveOverride?: boolean,
topicUpdateNarrationOverride?: boolean,
): string {
const systemMdResolution = resolvePathFromEnv(
process.env['GEMINI_SYSTEM_MD'],
@@ -57,6 +58,10 @@ export class PromptProvider {
const isYoloMode = approvalMode === ApprovalMode.YOLO;
const skills = context.config.getSkillManager().getSkills();
const toolNames = context.toolRegistry.getAllToolNames();
const isTopicUpdateNarrationEnabled =
topicUpdateNarrationOverride ??
context.config.isTopicUpdateNarrationEnabled();
const enabledToolNames = new Set(toolNames);
const approvedPlanPath = context.config.getApprovedPlanPath();
@@ -139,7 +144,7 @@ export class PromptProvider {
hasSkills: skills.length > 0,
hasHierarchicalMemory,
contextFilenames,
topicUpdateNarration: context.config.isTopicUpdateNarrationEnabled(),
topicUpdateNarration: isTopicUpdateNarrationEnabled,
})),
subAgents: this.withSection(
'agentContexts',
@@ -184,8 +189,7 @@ export class PromptProvider {
? { path: approvedPlanPath }
: undefined,
taskTracker: trackerDir,
topicUpdateNarration:
context.config.isTopicUpdateNarrationEnabled(),
topicUpdateNarration: isTopicUpdateNarrationEnabled,
};
},
!isPlanMode,
@@ -207,8 +211,7 @@ export class PromptProvider {
enableShellEfficiency:
context.config.getEnableShellOutputEfficiency(),
interactiveShellEnabled: context.config.isInteractiveShellEnabled(),
topicUpdateNarration:
context.config.isTopicUpdateNarrationEnabled(),
topicUpdateNarration: isTopicUpdateNarrationEnabled,
memoryManagerEnabled: context.config.isMemoryManagerEnabled(),
}),
),
@@ -251,7 +254,7 @@ export class PromptProvider {
let sanitizedPrompt = finalPrompt.replace(/\n{3,}/g, '\n\n');
// Context Reinjection (Active Topic)
if (context.config.isTopicUpdateNarrationEnabled()) {
if (isTopicUpdateNarrationEnabled) {
const activeTopic = context.config.topicState.getTopic();
if (activeTopic) {
const sanitizedTopic = activeTopic
+16 -2
View File
@@ -209,6 +209,13 @@
}
},
"additionalProperties": false
},
"topicUpdateNarration": {
"title": "Topic & Update Narration",
"description": "Enable the Topic & Update communication model for reduced chattiness and structured progress reporting.",
"markdownDescription": "Enable the Topic & Update communication model for reduced chattiness and structured progress reporting.\n\n- Category: `General`\n- Requires restart: `no`\n- Default: `true`",
"default": true,
"type": "boolean"
}
},
"additionalProperties": false
@@ -2947,6 +2954,13 @@
"default": false,
"type": "boolean"
},
"autoMemory": {
"title": "Auto Memory",
"description": "Automatically extract reusable skills from past sessions in the background. Review results with /memory inbox.",
"markdownDescription": "Automatically extract reusable skills from past sessions in the background. Review results with /memory inbox.\n\n- Category: `Experimental`\n- Requires restart: `yes`\n- Default: `false`",
"default": false,
"type": "boolean"
},
"generalistProfile": {
"title": "Use the generalist profile to manage agent contexts.",
"description": "Suitable for general coding and software development tasks.",
@@ -2963,8 +2977,8 @@
},
"topicUpdateNarration": {
"title": "Topic & Update Narration",
"description": "Enable the experimental Topic & Update communication model for reduced chattiness and structured progress reporting.",
"markdownDescription": "Enable the experimental Topic & Update communication model for reduced chattiness and structured progress reporting.\n\n- Category: `Experimental`\n- Requires restart: `no`\n- Default: `false`",
"description": "Deprecated: Use general.topicUpdateNarration instead.",
"markdownDescription": "Deprecated: Use general.topicUpdateNarration instead.\n\n- Category: `Experimental`\n- Requires restart: `no`\n- Default: `false`",
"default": false,
"type": "boolean"
}