Compare commits

..

2 Commits

Author SHA1 Message Date
Coco Sheng 6e13a9f06b fix(security): resolve sudo and windows normalize bypasses in commandSafety 2026-04-20 12:48:08 -04:00
Coco Sheng dbbed7fd44 security: improve rm detection and clarify UI labels 2026-04-16 16:41:34 -04:00
116 changed files with 1620 additions and 4315 deletions
+2 -1
View File
@@ -2,7 +2,8 @@
"experimental": {
"extensionReloading": true,
"modelSteering": true,
"memoryManager": true
"memoryManager": true,
"topicUpdateNarration": true
},
"general": {
"devtools": true
+6 -15
View File
@@ -85,25 +85,17 @@ 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 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`.
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`.
Example (.md):
Example:
<!-- 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
@@ -126,7 +118,6 @@ 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:**
-6
View File
@@ -102,12 +102,6 @@ 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'
-1
View File
@@ -22,4 +22,3 @@ Thumbs.db
.pytest_cache
**/SKILL.md
packages/sdk/test-data/*.json
*.mdx
+82 -74
View File
@@ -1,6 +1,7 @@
# How to contribute
Contributions and patches to this project are welcome. This document includes:
We would love to accept your patches and contributions to this project. This
document includes:
- **[Before you begin](#before-you-begin):** Essential steps to take before
becoming a Gemini CLI contributor.
@@ -11,7 +12,7 @@ Contributions and patches to this project are welcome. This document includes:
- **[Documentation contribution process](#documentation-contribution-process):**
How to contribute documentation to Gemini CLI.
Your contributions are appreciated!
We're looking forward to seeing your contributions!
## Before you begin
@@ -41,13 +42,13 @@ This project follows
The process for contributing code is as follows:
1. **Find an issue** that you want to work on. If an issue is tagged as
`🔒Maintainers only`, this means it is reserved for project maintainers.
Pull requests related to these issues are not accepted. In the near future,
issues looking for contributions will be explicitly marked using the
`🔒Maintainers only`, this means it is reserved for project maintainers. We
will not accept pull requests related to these issues. In the near future,
we will explicitly mark issues looking for contributions using the
`help-wanted` label. If you believe an issue is a good candidate for
community contribution, leave a comment on the issue. A maintainer will
review it and apply the `help-wanted` label if appropriate. Only maintainers
add the `help-wanted` label to an issue.
community contribution, please leave a comment on the issue. A maintainer
will review it and apply the `help-wanted` label if appropriate. Only
maintainers should attempt to add the `help-wanted` label to an issue.
2. **Fork the repository** and create a new branch.
3. **Make your changes** in the `packages/` directory.
4. **Ensure all checks pass** by running `npm run preflight`.
@@ -55,21 +56,22 @@ The process for contributing code is as follows:
### Code reviews
All submissions, including submissions by project members, require review.
Gemini CLI uses
[GitHub pull requests](https://docs.github.com/articles/about-pull-requests) for
this purpose.
All submissions, including submissions by project members, require review. We
use [GitHub pull requests](https://docs.github.com/articles/about-pull-requests)
for this purpose.
To assist with the review process, an automated review tool helps detect common
anti-patterns, testing issues, and other best practices that are easy to miss.
To assist with the review process, we provide an automated review tool that
helps detect common anti-patterns, testing issues, and other best practices that
are easy to miss.
#### Using the automated review tool
You can run the review tool in two ways:
1. **Using the helper script (Recommended):** A script automatically handles
checking out the PR into a separate worktree, installing dependencies,
building the project, and launching the review tool.
1. **Using the helper script (Recommended):** We provide a script that
automatically handles checking out the PR into a separate worktree,
installing dependencies, building the project, and launching the review
tool.
```bash
./scripts/review.sh <PR_NUMBER> [model]
@@ -95,7 +97,7 @@ You can run the review tool in two ways:
/review-frontend <PR_NUMBER>
```
Replace `<PR_NUMBER>` with your pull request number. Reviewers must use this
Replace `<PR_NUMBER>` with your pull request number. Reviewers should use this
tool to augment, not replace, their manual review process.
### Self-assigning and unassigning issues
@@ -103,39 +105,39 @@ tool to augment, not replace, their manual review process.
To assign an issue to yourself, simply add a comment with the text `/assign`. To
unassign yourself from an issue, add a comment with the text `/unassign`.
The comment must contain only that text and nothing else. These commands assign
or unassign the issue as requested, provided the conditions are met (for
example, an issue must be unassigned to be assigned).
The comment must contain only that text and nothing else. These commands will
assign or unassign the issue as requested, provided the conditions are met
(e.g., an issue must be unassigned to be assigned).
Note that you can have a maximum of 3 issues assigned to you at any given time
and that only
Please note that you can have a maximum of 3 issues assigned to you at any given
time and that only
[issues labeled "help wanted"](https://github.com/google-gemini/gemini-cli/issues?q=is%3Aissue%20state%3Aopen%20label%3A%22help%20wanted%22)
may be self-assigned.
### Pull request guidelines
To ensure quick review and merging of your PRs, follow these guidelines. PRs
that do not meet these standards may be closed.
To help us review and merge your PRs quickly, please follow these guidelines.
PRs that do not meet these standards may be closed.
#### 1. Link to an existing issue
All PRs must be linked to an existing issue in our tracker. This ensures that
All PRs should be linked to an existing issue in our tracker. This ensures that
every change has been discussed and is aligned with the project's goals before
any code is written.
- **For bug fixes:** The PR must be linked to the bug report issue.
- **For features:** The PR must be linked to the feature request or proposal
- **For bug fixes:** The PR should be linked to the bug report issue.
- **For features:** The PR should be linked to the feature request or proposal
issue that has been approved by a maintainer.
If an issue for your change doesn't exist, Gemini CLI automatically closes your
PR along with a comment reminding you to associate the PR with an issue. The
ideal workflow starts with an issue that has been reviewed and approved by a
maintainer. **Open the issue first** and wait for feedback before you start
coding.
If an issue for your change doesn't exist, we will automatically close your PR
along with a comment reminding you to associate the PR with an issue. The ideal
workflow starts with an issue that has been reviewed and approved by a
maintainer. Please **open the issue first** and wait for feedback before you
start coding.
#### 2. Keep it small and focused
Gemini CLI favors small, atomic PRs that address a single issue or add a single,
We favor small, atomic PRs that address a single issue or add a single,
self-contained feature.
- **Do:** Create a PR that fixes one specific bug or adds one specific feature.
@@ -147,9 +149,9 @@ can be reviewed and merged independently.
#### 3. Use draft PRs for work in progress
To get early feedback on your work, use GitHub's **Draft Pull Request** feature.
This signals to the maintainers that the PR is not yet ready for a formal review
but is open for discussion and initial feedback.
If you'd like to get early feedback on your work, please use GitHub's **Draft
Pull Request** feature. This signals to the maintainers that the PR is not yet
ready for a formal review but is open for discussion and initial feedback.
#### 4. Ensure all checks pass
@@ -168,7 +170,7 @@ See more about writing documentation:
#### 6. Write clear commit messages and a good PR description
Your PR must have a clear, descriptive title and a detailed description of the
Your PR should have a clear, descriptive title and a detailed description of the
changes. Follow the [Conventional Commits](https://www.conventionalcommits.org/)
standard for your commit messages.
@@ -176,19 +178,21 @@ standard for your commit messages.
- **Bad PR title:** `Made some changes`
In the PR description, explain the "why" behind your changes and link to the
relevant issue (for example, `Fixes #123`).
relevant issue (e.g., `Fixes #123`).
### Forking
If you fork the repository, you can run the Build, Test and Integration test
workflows. To run the integration tests, add a
If you are forking the repository you will be able to run the Build, Test and
Integration test workflows. However in order to make the integration tests run
you'll need to add a
[GitHub Repository Secret](https://docs.github.com/en/actions/security-for-github-actions/security-guides/using-secrets-in-github-actions#creating-secrets-for-a-repository)
with a value of `GEMINI_API_KEY` and set that to a valid API key. Your key and
secret are private to your repo; no one without access can see your key and you
cannot see any secrets related to this repo.
with a value of `GEMINI_API_KEY` and set that to a valid API key that you have
available. Your key and secret are private to your repo; no one without access
can see your key and you cannot see any secrets related to this repo.
Additionally, click on the `Actions` tab and enable workflows for your
repository. It is the large blue button in the center of the screen.
Additionally you will need to click on the `Actions` tab and enable workflows
for your repository, you'll find it's the large blue button in the center of the
screen.
### Development setup and workflow
@@ -200,9 +204,9 @@ development setup of this project.
**Prerequisites:**
1. **Node.js**:
- **Development:** Use Node.js `~20.19.0`. This specific version is required
due to an upstream development dependency issue. Use a tool like
[nvm](https://github.com/nvm-sh/nvm) to manage Node.js versions.
- **Development:** Please use Node.js `~20.19.0`. This specific version is
required due to an upstream development dependency issue. You can use a
tool like [nvm](https://github.com/nvm-sh/nvm) to manage Node.js versions.
- **Production:** For running the CLI in a production environment, any
version of Node.js `>=20` is acceptable.
2. **Git**
@@ -257,10 +261,10 @@ command from the root directory:
npm start
```
To run the source build outside of the gemini-cli folder, use
`npm link path/to/gemini-cli/packages/cli` (see:
If you'd like to run the source build outside of the gemini-cli folder, you can
utilize `npm link path/to/gemini-cli/packages/cli` (see:
[docs](https://docs.npmjs.com/cli/v9/commands/npm-link)) or
`alias gemini="node path/to/gemini-cli/packages/cli"` to run with `gemini`.
`alias gemini="node path/to/gemini-cli/packages/cli"` to run with `gemini`
### Running tests
@@ -280,8 +284,8 @@ comprehensive check, it is recommended to run `npm run preflight`.
#### Integration tests
The integration tests validate the end-to-end functionality of the Gemini CLI.
They are not run as part of the default `npm run test` command.
The integration tests are designed to validate the end-to-end functionality of
the Gemini CLI. They are not run as part of the default `npm run test` command.
To run the integration tests, use the following command:
@@ -289,7 +293,8 @@ To run the integration tests, use the following command:
npm run test:e2e
```
For more detailed information on the integration testing framework, see the
For more detailed information on the integration testing framework, please see
the
[Integration Tests documentation](https://geminicli.com/docs/integration-tests).
### Linting and preflight checks
@@ -300,8 +305,8 @@ To ensure code quality and formatting consistency, run the preflight check:
npm run preflight
```
This command runs ESLint, Prettier, all tests, and other checks as defined in
the project's `package.json`.
This command will run ESLint, Prettier, all tests, and other checks as defined
in the project's `package.json`.
_ProTip_
@@ -341,8 +346,8 @@ npm run lint
### Coding conventions
- Adhere to the coding style, patterns, and conventions used throughout the
existing codebase.
- Please adhere to the coding style, patterns, and conventions used throughout
the existing codebase.
- Consult
[GEMINI.md](https://github.com/google-gemini/gemini-cli/blob/main/GEMINI.md)
(typically found in the project root) for specific instructions related to
@@ -465,8 +470,8 @@ sandbox.
### Manual publish
Gemini CLI publishes an artifact for each commit to an internal registry. To
manually cut a local build, use these commands:
We publish an artifact for each commit to our internal registry. But if you need
to manually cut a local build, then run the following commands:
```
npm run clean
@@ -478,8 +483,8 @@ npm publish --workspaces
## Documentation contribution process
Gemini CLI documentation must be kept up-to-date with code contributions.
Documentation should be clear, concise, and helpful to users. Gemini CLI values:
Our documentation must be kept up-to-date with our code contributions. We want
our documentation to be clear, concise, and helpful to our users. We value:
- **Clarity:** Use simple and direct language. Avoid jargon where possible.
- **Accuracy:** Ensure all information is correct and up-to-date.
@@ -489,12 +494,13 @@ Documentation should be clear, concise, and helpful to users. Gemini CLI values:
### Getting started
Documentation contributions follow a process similar to code contributions.
The process for contributing to the documentation is similar to contributing
code.
1. **Fork the repository** and create a new branch.
2. **Make your changes** in the `/docs` directory.
3. **Preview your changes locally** in Markdown rendering.
4. **Lint and format your changes.** The preflight check includes linting and
4. **Lint and format your changes.** Our preflight check includes linting and
formatting for documentation files.
```bash
npm run preflight
@@ -513,9 +519,9 @@ as the table of contents. When adding new documentation:
### Style guide
Gemini CLI follows the
We follow the
[Google Developer Documentation Style Guide](https://developers.google.com/style).
Refer to it for guidance on writing style, tone, and formatting.
Please refer to it for guidance on writing style, tone, and formatting.
#### Key style points
@@ -528,8 +534,8 @@ Refer to it for guidance on writing style, tone, and formatting.
### Linting and formatting
Gemini CLI uses `prettier` to enforce a consistent style across our
documentation. The `npm run preflight` command checks for any linting issues.
We use `prettier` to enforce a consistent style across our documentation. The
`npm run preflight` command will check for any linting issues.
You can also run the linter and formatter separately:
@@ -537,10 +543,12 @@ You can also run the linter and formatter separately:
- `npm run format` - Auto-format markdown files
- `npm run lint:fix` - Auto-fix linting issues where possible
Ensure your contributions are free of linting errors before submitting a pull
request.
Please make sure your contributions are free of linting errors before submitting
a pull request.
### Before submitting
### Before you submit
Before submitting your documentation pull request, please:
1. Run `npm run preflight` to ensure all checks pass.
2. Review your changes for clarity and accuracy.
@@ -554,10 +562,10 @@ request.
If you have questions about contributing documentation:
- Check the [FAQ](https://geminicli.com/docs/resources/faq).
- Check our [FAQ](https://geminicli.com/docs/resources/faq).
- Review existing documentation for examples.
- Open [an issue](https://github.com/google-gemini/gemini-cli/issues) to discuss
your proposed changes.
- Reach out to the maintainers.
Gemini CLI appreciates your contributions to making its documentation better!
We appreciate your contributions to making Gemini CLI documentation better!
-143
View File
@@ -1,143 +0,0 @@
# 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).
-28
View File
@@ -5,9 +5,6 @@ and parameters.
## CLI commands
Gemini CLI provides a range of commands for starting sessions, updating the
application, and managing extensions and MCP servers.
| Command | Description | Example |
| ---------------------------------- | ---------------------------------- | ------------------------------------------------------------ |
| `gemini` | Start interactive REPL | `gemini` |
@@ -20,14 +17,10 @@ application, and managing extensions and MCP servers.
| `gemini -r "<session-id>" "query"` | Resume session by ID | `gemini -r "abc123" "Finish this PR"` |
| `gemini update` | Update to latest version | `gemini update` |
| `gemini extensions` | Manage extensions | See [Extensions Management](#extensions-management) |
| `gemini hooks` | Manage hooks | See [Hooks Management](#hooks-management) |
| `gemini mcp` | Configure MCP servers | See [MCP Server Management](#mcp-server-management) |
| `gemini skills` | Manage agent skills | See [Skills Management](#skills-management) |
### Positional arguments
Use positional arguments to provide a prompt directly from the command line.
| Argument | Type | Description |
| -------- | ----------------- | ---------------------------------------------------------------------------------------------------------- |
| `query` | string (variadic) | Positional prompt. Defaults to interactive mode in a TTY. Use `-p/--prompt` for non-interactive execution. |
@@ -49,8 +42,6 @@ These commands are available within the interactive REPL.
## CLI Options
Configure the behavior of Gemini CLI using these command-line options.
| Option | Alias | Type | Default | Description |
| -------------------------------- | ----- | ------- | --------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `--debug` | `-d` | boolean | `false` | Run in debug mode with verbose logging |
@@ -94,8 +85,6 @@ These are convenient shortcuts that map to specific models:
## Extensions management
Manage your Gemini CLI extensions with these commands.
| Command | Description | Example |
| -------------------------------------------------- | -------------------------------------------- | ------------------------------------------------------------------------------ |
| `gemini extensions install <source>` | Install extension from Git URL or local path | `gemini extensions install https://github.com/user/my-extension` |
@@ -110,24 +99,11 @@ Manage your Gemini CLI extensions with these commands.
| `gemini extensions link <path>` | Link local extension for development | `gemini extensions link /path/to/extension` |
| `gemini extensions new <path>` | Create new extension from template | `gemini extensions new ./my-extension` |
| `gemini extensions validate <path>` | Validate extension structure | `gemini extensions validate ./my-extension` |
| `gemini extensions configure <name>` | Configure an extension | `gemini extensions configure my-extension` |
See [Extensions Documentation](../extensions/index.md) for more details.
## Hooks management
Manage your Gemini CLI hooks with these commands.
| Command | Description | Example |
| ---------------------- | -------------------- | ---------------------- |
| `gemini hooks migrate` | Migrate legacy hooks | `gemini hooks migrate` |
See [Hooks Documentation](../hooks/index.md) for more details.
## MCP server management
Manage your Model Context Protocol (MCP) servers with these commands.
| Command | Description | Example |
| ------------------------------------------------------------- | ------------------------------- | ---------------------------------------------------------------------------------------------------- |
| `gemini mcp add <name> <command>` | Add stdio-based MCP server | `gemini mcp add github npx -y @modelcontextprotocol/server-github` |
@@ -137,15 +113,11 @@ Manage your Model Context Protocol (MCP) servers with these commands.
| `gemini mcp add <name> <command> --include-tools tool1,tool2` | Add with specific tools | `gemini mcp add github npx -y @modelcontextprotocol/server-github --include-tools list_repos,get_pr` |
| `gemini mcp remove <name>` | Remove an MCP server | `gemini mcp remove github` |
| `gemini mcp list` | List all configured MCP servers | `gemini mcp list` |
| `gemini mcp enable <name>` | Enable an MCP server | `gemini mcp enable github` |
| `gemini mcp disable <name>` | Disable an MCP server | `gemini mcp disable github` |
See [MCP Server Integration](../tools/mcp-server.md) for more details.
## Skills management
Manage your agent skills with these commands.
| Command | Description | Example |
| -------------------------------- | ------------------------------------- | ------------------------------------------------- |
| `gemini skills list` | List all discovered agent skills | `gemini skills list` |
+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.mdx) for more details.
[Authentication docs](../get-started/authentication.md) for more details.
**Example:** Enforce the use of Google login for all users.
+1 -3
View File
@@ -130,9 +130,7 @@ 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`) and core
[MCP resource tools](../tools/mcp-resources.md) (`list_mcp_resources`,
`read_mcp_resource`) are allowed.
example, `github_read_issue`, `postgres_read_schema`) 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`
+25 -27
View File
@@ -24,22 +24,20 @@ 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 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` |
| 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"` |
### Output
@@ -161,17 +159,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` |
| 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` |
| 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` |
### 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.mdx#persisting-environment-variables).
[Persisting Environment Variables](../get-started/authentication.md#persisting-environment-variables).
- Use the project default path (`.gemini/system.md`):
- `GEMINI_SYSTEM_MD=true` or `GEMINI_SYSTEM_MD=1`
-2
View File
@@ -124,5 +124,3 @@ 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,11 +1,10 @@
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
@@ -24,7 +23,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?
@@ -85,24 +84,19 @@ To authenticate and use Gemini CLI with a Gemini API key:
2. Set the `GEMINI_API_KEY` environment variable to your key. For example:
<Tabs>
<TabItem label="macOS/Linux">
**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"
```
</TabItem>
<TabItem label="Windows (PowerShell)">
**Windows (PowerShell)**
```powershell
# Replace YOUR_GEMINI_API_KEY with the key from AI Studio
$env:GEMINI_API_KEY="YOUR_GEMINI_API_KEY"
```
</TabItem>
</Tabs>
```powershell
# Replace YOUR_GEMINI_API_KEY with the key from AI Studio
$env:GEMINI_API_KEY="YOUR_GEMINI_API_KEY"
```
To make this setting persistent, see
[Persisting Environment Variables](#persisting-vars).
@@ -115,6 +109,7 @@ 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
@@ -136,26 +131,21 @@ or the location where you want to run your jobs.
For example:
<Tabs>
<TabItem label="macOS/Linux">
**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"
```
```
</TabItem>
<TabItem label="Windows (PowerShell)">
**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).
@@ -167,22 +157,17 @@ 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.
<Tabs>
<TabItem label="macOS/Linux">
**macOS/Linux**
```bash
```bash
unset GOOGLE_API_KEY GEMINI_API_KEY
```
```
</TabItem>
<TabItem label="Windows (PowerShell)">
**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.
@@ -210,22 +195,17 @@ 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:
<Tabs>
<TabItem label="macOS/Linux">
**macOS/Linux**
```bash
```bash
unset GOOGLE_API_KEY GEMINI_API_KEY
```
```
</TabItem>
<TabItem label="Windows (PowerShell)">
**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
@@ -234,24 +214,19 @@ 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:
<Tabs>
<TabItem label="macOS/Linux">
**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"
```
</TabItem>
<TabItem label="Windows (PowerShell)">
**Windows (PowerShell)**
```powershell
# Replace C:\path\to\your\keyfile.json with the actual path
$env:GOOGLE_APPLICATION_CREDENTIALS="C:\path\to\your\keyfile.json"
```
</TabItem>
</Tabs>
```powershell
# Replace C:\path\to\your\keyfile.json with the actual path
$env:GOOGLE_APPLICATION_CREDENTIALS="C:\path\to\your\keyfile.json"
```
3. [Configure your Google Cloud Project](#set-gcp).
@@ -263,6 +238,7 @@ 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.
@@ -274,24 +250,19 @@ Remove-Item Env:\GOOGLE_API_KEY, Env:\GEMINI_API_KEY -ErrorAction Ignore
2. Set the `GOOGLE_API_KEY` environment variable:
<Tabs>
<TabItem label="macOS/Linux">
**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"
```
</TabItem>
<TabItem label="Windows (PowerShell)">
**Windows (PowerShell)**
```powershell
# Replace YOUR_GOOGLE_API_KEY with your Vertex AI API key
$env:GOOGLE_API_KEY="YOUR_GOOGLE_API_KEY"
```
</TabItem>
</Tabs>
```powershell
# Replace YOUR_GOOGLE_API_KEY with your Vertex AI API key
$env:GOOGLE_API_KEY="YOUR_GOOGLE_API_KEY"
```
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
@@ -309,6 +280,7 @@ 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.
@@ -336,24 +308,19 @@ To configure Gemini CLI to use a Google Cloud project, do the following:
For example, to set the `GOOGLE_CLOUD_PROJECT_ID` variable:
<Tabs>
<TabItem label="macOS/Linux">
**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"
```
</TabItem>
<TabItem label="Windows (PowerShell)">
**Windows (PowerShell)**
```powershell
# Replace YOUR_PROJECT_ID with your actual Google Cloud project ID
$env:GOOGLE_CLOUD_PROJECT="YOUR_PROJECT_ID"
```
</TabItem>
</Tabs>
```powershell
# Replace YOUR_PROJECT_ID with your actual Google Cloud project ID
$env:GOOGLE_CLOUD_PROJECT="YOUR_PROJECT_ID"
```
To make this setting persistent, see
[Persisting Environment Variables](#persisting-vars).
@@ -366,29 +333,21 @@ 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.
<Tabs>
<TabItem label="macOS/Linux">
**macOS/Linux** (for example, `~/.bashrc`, `~/.zshrc`, or `~/.profile`):
(for example, `~/.bashrc`, `~/.zshrc`, or `~/.profile`):
```bash
echo 'export GOOGLE_CLOUD_PROJECT="YOUR_PROJECT_ID"' >> ~/.bashrc
source ~/.bashrc
```
```bash
echo 'export GOOGLE_CLOUD_PROJECT="YOUR_PROJECT_ID"' >> ~/.bashrc
source ~/.bashrc
```
**Windows (PowerShell)** (for example, `$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>
```powershell
Add-Content -Path $PROFILE -Value '$env:GOOGLE_CLOUD_PROJECT="YOUR_PROJECT_ID"'
. $PROFILE
```
<!-- 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
@@ -402,30 +361,25 @@ persist them with the following methods:
Example for user-wide settings:
<Tabs>
<TabItem label="macOS/Linux">
**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
```
</TabItem>
<TabItem label="Windows (PowerShell)">
**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
```
</TabItem>
</Tabs>
```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
```
Variables are loaded from the first file found, not merged.
+2 -3
View File
@@ -24,8 +24,7 @@ Once Gemini CLI is installed, run Gemini CLI from your command line:
gemini
```
For more installation options, see
[Gemini CLI Installation](./installation.mdx).
For more installation options, see [Gemini CLI Installation](./installation.md).
## Authenticate
@@ -47,7 +46,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.mdx).
[Gemini CLI Authentication Setup](./authentication.md).
## Configure
+181
View File
@@ -0,0 +1,181 @@
# 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
@@ -1,201 +0,0 @@
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.mdx):** How to install Gemini CLI
- **[Installation](./get-started/installation.md):** How to install Gemini CLI
on your system.
- **[Authentication](./get-started/authentication.mdx):** Setup instructions for
- **[Authentication](./get-started/authentication.md):** Setup instructions for
personal and enterprise accounts.
- **[CLI cheatsheet](./cli/cli-reference.md):** A quick reference for common
commands and options.
+25 -40
View File
@@ -134,15 +134,10 @@ their corresponding top-level category object in your `settings.json` file.
- **Default:** `true`
- **`general.enableNotifications`** (boolean):
- **Description:** Enable terminal run-event notifications for action-required
prompts and session completion.
- **Description:** Enable 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`
@@ -198,11 +193,6 @@ 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):
@@ -1729,12 +1719,6 @@ 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`
@@ -1746,7 +1730,8 @@ their corresponding top-level category object in your `settings.json` file.
- **Requires restart:** Yes
- **`experimental.topicUpdateNarration`** (boolean):
- **Description:** Deprecated: Use general.topicUpdateNarration instead.
- **Description:** Enable the experimental Topic & Update communication model
for reduced chattiness and structured progress reporting.
- **Default:** `false`
#### `skills`
@@ -2085,7 +2070,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.mdx) which covers
[Authentication documentation](../get-started/authentication.md) which covers
all available authentication methods.
The CLI automatically loads environment variables from an `.env` file. The
@@ -2106,7 +2091,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.mdx).
[authentication methods](../get-started/authentication.md).
- Set this in your shell profile (for example, `~/.bashrc`, `~/.zshrc`) or an
`.env` file.
- **`GEMINI_MODEL`**:
@@ -2545,29 +2530,29 @@ image separately and reference that image in your sandbox configuration.
## Usage statistics
To improve Gemini CLI, Gemini CLI collects anonymized usage statistics. This
data helps identify common issues and prioritize new features.
To help us improve Gemini CLI, we collect anonymized usage statistics. This data
helps us understand how the CLI is used, identify common issues, and prioritize
new features.
**Collected data:**
**What we collect:**
- **Tool calls:** Gemini CLI logs the names of the tools that are called,
whether they succeed or fail, and how long they take to execute. Gemini CLI
does not collect the arguments passed to the tools or any data returned by
them.
- **API requests:** Gemini CLI logs the Gemini model used for each request, the
duration of the request, and whether it was successful. Gemini CLI does not
collect the content of the prompts or responses.
- **Session information:** Gemini CLI collects information about the
configuration of the CLI, such as the enabled tools and the approval mode.
- **Tool calls:** We log the names of the tools that are called, whether they
succeed or fail, and how long they take to execute. We do not collect the
arguments passed to the tools or any data returned by them.
- **API requests:** We log the Gemini model used for each request, the duration
of the request, and whether it was successful. We do not collect the content
of the prompts or responses.
- **Session information:** We collect information about the configuration of the
CLI, such as the enabled tools and the approval mode.
**Data NOT collected:**
**What we DON'T collect:**
- **Personally identifiable information (PII):** Gemini CLI does not collect
personal information, such as your name, email address, or API keys.
- **Prompt and response content:** Gemini CLI does not log the content of your
prompts or the responses from the Gemini model.
- **File content:** Gemini CLI does not log the content of any files that are
read or written by the CLI.
- **Personally identifiable information (PII):** We do not collect any personal
information, such as your name, email address, or API keys.
- **Prompt and response content:** We do not log the content of your prompts or
the responses from the Gemini model.
- **File content:** We do not log the content of any files that are read or
written by the CLI.
**How to opt out:**
+12 -18
View File
@@ -120,12 +120,6 @@ 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**.
@@ -133,13 +127,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 | **(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). |
| 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). |
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:
@@ -220,11 +214,11 @@ User, and (if configured) Admin directories.
### Policy locations
| Tier | Type | Location |
| :------------ | :----- | :------------------------------------------------------- |
| **User** | Custom | `~/.gemini/policies/*.toml` |
| **Workspace** | Custom | **(Disabled)** `$WORKSPACE_ROOT/.gemini/policies/*.toml` |
| **Admin** | System | _See below (OS specific)_ |
| Tier | Type | Location |
| :------------ | :----- | :---------------------------------------- |
| **User** | Custom | `~/.gemini/policies/*.toml` |
| **Workspace** | Custom | `$WORKSPACE_ROOT/.gemini/policies/*.toml` |
| **Admin** | System | _See below (OS specific)_ |
#### System-wide policies (Admin)
-7
View File
@@ -92,13 +92,6 @@ 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. |
### 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 |
+1 -13
View File
@@ -96,11 +96,6 @@
]
},
{ "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" },
{
@@ -127,14 +122,7 @@
}
]
},
{
"label": "MCP servers",
"collapsed": true,
"items": [
{ "label": "Overview", "slug": "docs/tools/mcp-server" },
{ "label": "Resource tools", "slug": "docs/tools/mcp-resources" }
]
},
{ "label": "MCP servers", "slug": "docs/tools/mcp-server" },
{ "label": "Model routing", "slug": "docs/cli/model-routing" },
{ "label": "Model selection", "slug": "docs/cli/model" },
{
-44
View File
@@ -1,44 +0,0 @@
# 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.
+1 -2
View File
@@ -64,8 +64,7 @@ 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. For more information on the tools used to
interact with these resources, see [MCP resource tools](mcp-resources.md).
to reference them in the chat.
### Discovery and listing
-4
View File
@@ -11,8 +11,6 @@ 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.",
@@ -52,8 +50,6 @@ 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.",
+1 -5
View File
@@ -298,8 +298,6 @@ 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,
@@ -335,7 +333,7 @@ describe('plan_mode', () => {
expect(
planWrite?.toolRequest.success,
`Expected write_file to succeed, but got error: ${(planWrite?.toolRequest as any).error}`,
`Expected write_file to succeed, but got error: ${planWrite?.toolRequest.error}`,
).toBe(true);
assertModelHasOutput(result);
@@ -343,8 +341,6 @@ 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: {
+2 -2
View File
@@ -149,12 +149,12 @@ async function readSkillBodies(skillsDir: string): Promise<string[]> {
/**
* Shared configOverrides for all skill extraction component evals.
* - experimentalAutoMemory: enables the Auto Memory skill extraction pipeline.
* - experimentalMemoryManager: enables the memory 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 = {
experimentalAutoMemory: true,
experimentalMemoryManager: true,
approvalMode: ApprovalMode.YOLO,
};
+4 -5
View File
@@ -5,7 +5,10 @@
*/
import { describe, expect } from 'vitest';
import { TRACKER_CREATE_TASK_TOOL_NAME } from '@google/gemini-cli-core';
import {
TRACKER_CREATE_TASK_TOOL_NAME,
TRACKER_UPDATE_TASK_TOOL_NAME,
} from '@google/gemini-cli-core';
import { evalTest, TEST_AGENTS } from './test-helper.js';
describe('subtask delegation eval test cases', () => {
@@ -19,8 +22,6 @@ 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: {
@@ -89,8 +90,6 @@ 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,8 +119,6 @@ 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
@@ -1,13 +0,0 @@
{
"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,8 +7,6 @@
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.',
+6 -18
View File
@@ -21,8 +21,6 @@ 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.
@@ -43,7 +41,7 @@ describe('update_topic_behavior', () => {
2,
),
'.gemini/settings.json': JSON.stringify({
general: {
experimental: {
topicUpdateNarration: true,
},
}),
@@ -119,15 +117,13 @@ 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({
general: {
experimental: {
topicUpdateNarration: true,
},
}),
@@ -146,8 +142,6 @@ 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:
@@ -156,7 +150,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({
general: {
experimental: {
topicUpdateNarration: true,
},
}),
@@ -175,8 +169,6 @@ 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.',
@@ -204,7 +196,7 @@ app.post('/users', (req, res) => {
export default app;
`,
'.gemini/settings.json': JSON.stringify({
general: {
experimental: {
topicUpdateNarration: true,
},
}),
@@ -220,9 +212,7 @@ 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);
},
});
@@ -234,8 +224,6 @@ 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.
@@ -249,7 +237,7 @@ export default app;
files: {
'package.json': JSON.stringify({ name: 'test-project' }),
'.gemini/settings.json': JSON.stringify({
general: {
experimental: {
topicUpdateNarration: true,
},
}),
+1 -5
View File
@@ -39,11 +39,7 @@ describe('web-fetch rate limiting', () => {
const rateLimitedCalls = toolLogs.filter(
(log) =>
log.toolRequest.name === 'web_fetch' &&
(
('error' in log.toolRequest
? (log.toolRequest as unknown as Record<string, string>)['error']
: '') as string
)?.includes('Rate limit exceeded'),
log.toolRequest.error?.includes('Rate limit exceeded'),
);
expect(rateLimitedCalls.length).toBeGreaterThan(0);
+1 -2
View File
@@ -164,8 +164,7 @@ describe.skipIf(skipFlaky)(
);
expect(blockHook).toBeDefined();
expect(
(blockHook?.hookCall.stdout || '') +
(blockHook?.hookCall.stderr || ''),
blockHook?.hookCall.stdout + blockHook?.hookCall.stderr,
).toContain(blockMsg);
});
@@ -1,2 +0,0 @@
{"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}}]}
@@ -1,2 +0,0 @@
{"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}}]}
@@ -1,4 +0,0 @@
{"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
@@ -1,178 +0,0 @@
/**
* @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);
});
+5 -16
View File
@@ -108,7 +108,7 @@ describe('Plan Mode', () => {
).toBeDefined();
expect(
planWrite?.toolRequest.success,
`Expected write_file to succeed, but it failed with error: ${'error' in (planWrite?.toolRequest || {}) ? (planWrite?.toolRequest as unknown as Record<string, string>)['error'] : 'unknown'}`,
`Expected write_file to succeed, but it failed with error: ${planWrite?.toolRequest.error}`,
).toBe(true);
});
@@ -221,7 +221,7 @@ describe('Plan Mode', () => {
).toBeDefined();
expect(
planWrite?.toolRequest.success,
`Expected write_file to succeed, but it failed with error: ${'error' in (planWrite?.toolRequest || {}) ? (planWrite?.toolRequest as unknown as Record<string, string>)['error'] : 'unknown'}`,
`Expected write_file to succeed, but it failed with error: ${planWrite?.toolRequest.error}`,
).toBe(true);
});
it('should switch from a pro model to a flash model after exiting plan mode', async () => {
@@ -270,24 +270,13 @@ describe('Plan Mode', () => {
);
const apiRequests = rig.readAllApiRequest();
const modelNames = apiRequests.map(
(r) =>
('model' in (r.attributes || {})
? (r.attributes as unknown as Record<string, string>)['model']
: 'unknown') || 'unknown',
);
const modelNames = apiRequests.map((r) => r.attributes?.model || 'unknown');
const proRequests = apiRequests.filter((r) =>
('model' in (r.attributes || {})
? (r.attributes as unknown as Record<string, string>)['model']
: 'unknown'
)?.includes('pro'),
r.attributes?.model?.includes('pro'),
);
const flashRequests = apiRequests.filter((r) =>
('model' in (r.attributes || {})
? (r.attributes as unknown as Record<string, string>)['model']
: 'unknown'
)?.includes('flash'),
r.attributes?.model?.includes('flash'),
);
expect(
+1 -5
View File
@@ -5,9 +5,5 @@
"allowJs": true
},
"include": ["**/*.ts"],
"references": [
{ "path": "../packages/core" },
{ "path": "../packages/test-utils" },
{ "path": "../packages/cli" }
]
"references": [{ "path": "../packages/core" }]
}
+2 -6
View File
@@ -489,12 +489,8 @@ async function generateSharedLargeChatData(tempDir: string) {
// Wait for streams to finish
await Promise.all([
new Promise((res) =>
activeResponsesStream.on('finish', () => res(undefined)),
),
new Promise((res) =>
resumeResponsesStream.on('finish', () => res(undefined)),
),
new Promise((res) => activeResponsesStream.on('finish', res)),
new Promise((res) => resumeResponsesStream.on('finish', res)),
]);
return {
+181 -418
View File
@@ -62,12 +62,10 @@
"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"
@@ -935,9 +933,9 @@
}
},
"node_modules/@eslint-community/eslint-utils": {
"version": "4.9.1",
"resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz",
"integrity": "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==",
"version": "4.7.0",
"resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.7.0.tgz",
"integrity": "sha512-dyybb3AcajC7uha6CvhdVRJqaKyn7w2YKqKyAN37NKYgZT36w+iRb0Dymmc5qEJ549c/S31cMMSFd75bteCpCw==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -967,9 +965,9 @@
}
},
"node_modules/@eslint-community/regexpp": {
"version": "4.12.2",
"resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz",
"integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==",
"version": "4.12.1",
"resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.1.tgz",
"integrity": "sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==",
"dev": true,
"license": "MIT",
"engines": {
@@ -4275,20 +4273,21 @@
}
},
"node_modules/@typescript-eslint/eslint-plugin": {
"version": "8.58.2",
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.58.2.tgz",
"integrity": "sha512-aC2qc5thQahutKjP+cl8cgN9DWe3ZUqVko30CMSZHnFEHyhOYoZSzkGtAI2mcwZ38xeImDucI4dnqsHiOYuuCw==",
"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.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",
"@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.5.0"
"ts-api-utils": "^2.1.0"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@@ -4298,9 +4297,9 @@
"url": "https://opencollective.com/typescript-eslint"
},
"peerDependencies": {
"@typescript-eslint/parser": "^8.58.2",
"eslint": "^8.57.0 || ^9.0.0 || ^10.0.0",
"typescript": ">=4.8.4 <6.1.0"
"@typescript-eslint/parser": "^8.35.0",
"eslint": "^8.57.0 || ^9.0.0",
"typescript": ">=4.8.4 <5.9.0"
}
},
"node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": {
@@ -4314,17 +4313,17 @@
}
},
"node_modules/@typescript-eslint/parser": {
"version": "8.58.2",
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.58.2.tgz",
"integrity": "sha512-/Zb/xaIDfxeJnvishjGdcR4jmr7S+bda8PKNhRGdljDM+elXhlvN0FyPSsMnLmJUrVG9aPO6dof80wjMawsASg==",
"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.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"
"@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"
@@ -4334,20 +4333,20 @@
"url": "https://opencollective.com/typescript-eslint"
},
"peerDependencies": {
"eslint": "^8.57.0 || ^9.0.0 || ^10.0.0",
"typescript": ">=4.8.4 <6.1.0"
"eslint": "^8.57.0 || ^9.0.0",
"typescript": ">=4.8.4 <5.9.0"
}
},
"node_modules/@typescript-eslint/project-service": {
"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==",
"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.58.2",
"@typescript-eslint/types": "^8.58.2",
"debug": "^4.4.3"
"@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"
@@ -4357,18 +4356,18 @@
"url": "https://opencollective.com/typescript-eslint"
},
"peerDependencies": {
"typescript": ">=4.8.4 <6.1.0"
"typescript": ">=4.8.4 <5.9.0"
}
},
"node_modules/@typescript-eslint/scope-manager": {
"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==",
"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.58.2",
"@typescript-eslint/visitor-keys": "8.58.2"
"@typescript-eslint/types": "8.35.0",
"@typescript-eslint/visitor-keys": "8.35.0"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@@ -4379,9 +4378,9 @@
}
},
"node_modules/@typescript-eslint/tsconfig-utils": {
"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==",
"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": {
@@ -4392,21 +4391,20 @@
"url": "https://opencollective.com/typescript-eslint"
},
"peerDependencies": {
"typescript": ">=4.8.4 <6.1.0"
"typescript": ">=4.8.4 <5.9.0"
}
},
"node_modules/@typescript-eslint/type-utils": {
"version": "8.58.2",
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.58.2.tgz",
"integrity": "sha512-Z7EloNR/B389FvabdGeTo2XMs4W9TjtPiO9DAsmT0yom0bwlPyRjkJ1uCdW1DvrrrYP50AJZ9Xc3sByZA9+dcg==",
"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/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"
"@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"
@@ -4416,14 +4414,14 @@
"url": "https://opencollective.com/typescript-eslint"
},
"peerDependencies": {
"eslint": "^8.57.0 || ^9.0.0 || ^10.0.0",
"typescript": ">=4.8.4 <6.1.0"
"eslint": "^8.57.0 || ^9.0.0",
"typescript": ">=4.8.4 <5.9.0"
}
},
"node_modules/@typescript-eslint/types": {
"version": "8.58.2",
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.58.2.tgz",
"integrity": "sha512-9TukXyATBQf/Jq9AMQXfvurk+G5R2MwfqQGDR2GzGz28HvY/lXNKGhkY+6IOubwcquikWk5cjlgPvD2uAA7htQ==",
"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": {
@@ -4435,21 +4433,22 @@
}
},
"node_modules/@typescript-eslint/typescript-estree": {
"version": "8.58.2",
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.58.2.tgz",
"integrity": "sha512-ELGuoofuhhoCvNbQjFFiobFcGgcDCEm0ThWdmO4Z0UzLqPXS3KFvnEZ+SHewwOYHjM09tkzOWXNTv9u6Gqtyuw==",
"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.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"
"@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"
@@ -4459,20 +4458,20 @@
"url": "https://opencollective.com/typescript-eslint"
},
"peerDependencies": {
"typescript": ">=4.8.4 <6.1.0"
"typescript": ">=4.8.4 <5.9.0"
}
},
"node_modules/@typescript-eslint/utils": {
"version": "8.58.2",
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.58.2.tgz",
"integrity": "sha512-QZfjHNEzPY8+l0+fIXMvuQ2sJlplB4zgDZvA+NmvZsZv3EQwOcc1DuIU1VJUTWZ/RKouBMhDyNaBMx4sWvrzRA==",
"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.9.1",
"@typescript-eslint/scope-manager": "8.58.2",
"@typescript-eslint/types": "8.58.2",
"@typescript-eslint/typescript-estree": "8.58.2"
"@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"
@@ -4482,19 +4481,19 @@
"url": "https://opencollective.com/typescript-eslint"
},
"peerDependencies": {
"eslint": "^8.57.0 || ^9.0.0 || ^10.0.0",
"typescript": ">=4.8.4 <6.1.0"
"eslint": "^8.57.0 || ^9.0.0",
"typescript": ">=4.8.4 <5.9.0"
}
},
"node_modules/@typescript-eslint/visitor-keys": {
"version": "8.58.2",
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.58.2.tgz",
"integrity": "sha512-f1WO2Lx8a9t8DARmcWAUPJbu0G20bJlj8L4z72K00TMeJAoyLr/tHhI/pzYBLrR4dXWkcxO1cWYZEOX8DKHTqA==",
"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.58.2",
"eslint-visitor-keys": "^5.0.0"
"@typescript-eslint/types": "8.35.0",
"eslint-visitor-keys": "^4.2.1"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@@ -4504,19 +4503,6 @@
"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",
@@ -4593,6 +4579,28 @@
}
}
},
"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",
@@ -4611,6 +4619,23 @@
"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",
@@ -4625,6 +4650,59 @@
"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",
@@ -15800,9 +15878,9 @@
}
},
"node_modules/ts-api-utils": {
"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==",
"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==",
"dev": true,
"license": "MIT",
"engines": {
@@ -16085,237 +16163,6 @@
"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",
@@ -17842,20 +17689,6 @@
"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",
@@ -18003,20 +17836,6 @@
"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",
@@ -18271,20 +18090,6 @@
"url": "https://github.com/sponsors/jonschlinkert"
}
},
"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",
@@ -18330,20 +18135,6 @@
"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",
@@ -18362,20 +18153,6 @@
"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",
@@ -18410,20 +18187,6 @@
"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"
}
}
}
}
+1 -3
View File
@@ -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 && tsc -b evals/tsconfig.json integration-tests/tsconfig.json memory-tests/tsconfig.json",
"typecheck": "npm run typecheck --workspaces --if-present",
"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,7 +94,6 @@
],
"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",
@@ -138,7 +137,6 @@
"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"
+2 -2
View File
@@ -135,10 +135,10 @@ export class InboxMemoryCommand implements Command {
context: CommandContext,
_: string[],
): Promise<CommandExecutionResponse> {
if (!context.agentContext.config.isAutoMemoryEnabled()) {
if (!context.agentContext.config.isMemoryManagerEnabled()) {
return {
name: this.name,
data: 'The memory inbox requires Auto Memory. Enable it with: experimental.autoMemory = true in settings.',
data: 'The memory inbox requires the experimental memory manager. Enable it with: experimental.memoryManager = true in settings.',
};
}
+1 -4
View File
@@ -990,12 +990,9 @@ 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.general?.topicUpdateNarration ??
settings.experimental?.topicUpdateNarration,
topicUpdateNarration: settings.experimental?.topicUpdateNarration,
noBrowser: !!process.env['NO_BROWSER'],
summarizeToolOutput: settings.model?.summarizeToolOutput,
ideMode,
+5 -39
View File
@@ -256,29 +256,14 @@ const SETTINGS_SCHEMA = {
},
enableNotifications: {
type: 'boolean',
label: 'Enable Terminal Notifications',
label: 'Enable Notifications',
category: 'General',
requiresRestart: false,
default: false,
description:
'Enable terminal run-event notifications for action-required prompts and session completion.',
'Enable 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',
@@ -418,16 +403,6 @@ 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: {
@@ -2213,16 +2188,6 @@ 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.',
@@ -2248,8 +2213,9 @@ const SETTINGS_SCHEMA = {
category: 'Experimental',
requiresRestart: false,
default: false,
description: 'Deprecated: Use general.topicUpdateNarration instead.',
showInDialog: false,
description:
'Enable the experimental Topic & Update communication model for reduced chattiness and structured progress reporting.',
showInDialog: true,
},
},
},
@@ -88,7 +88,7 @@ describe('SkillCommandLoader', () => {
type: 'tool',
toolName: ACTIVATE_SKILL_TOOL_NAME,
toolArgs: { name: 'test-skill' },
postSubmitPrompt: 'Use the skill test-skill',
postSubmitPrompt: undefined,
});
});
@@ -46,10 +46,7 @@ export class SkillCommandLoader implements ICommandLoader {
type: 'tool',
toolName: ACTIVATE_SKILL_TOOL_NAME,
toolArgs: { name: skill.name },
postSubmitPrompt:
args.trim().length > 0
? args.trim()
: `Use the skill ${skill.name}`,
postSubmitPrompt: args.trim().length > 0 ? args.trim() : undefined,
}),
};
});
@@ -39,13 +39,11 @@ 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),
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' })),
@@ -53,7 +53,6 @@ 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',
@@ -195,7 +194,6 @@ vi.mock('./hooks/useShellInactivityStatus.js', () => ({
vi.mock('../utils/terminalNotifications.js', () => ({
notifyViaTerminal: terminalNotificationsMocks.notifyViaTerminal,
isNotificationsEnabled: terminalNotificationsMocks.isNotificationsEnabled,
getNotificationMethod: terminalNotificationsMocks.getNotificationMethod,
buildRunEventNotificationContent:
terminalNotificationsMocks.buildRunEventNotificationContent,
}));
+3 -10
View File
@@ -181,10 +181,7 @@ 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,
getNotificationMethod,
} from '../utils/terminalNotifications.js';
import { isNotificationsEnabled } from '../utils/terminalNotifications.js';
import {
getLastTurnToolCallIds,
isToolExecuting,
@@ -228,7 +225,6 @@ 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);
@@ -486,8 +482,8 @@ export const AppContainer = (props: AppContainerProps) => {
setConfigInitialized(true);
startupProfiler.flush(config);
// Fire-and-forget Auto Memory service (skill extraction from past sessions)
if (config.isAutoMemoryEnabled()) {
// 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);
});
@@ -977,7 +973,6 @@ Logging in with Google... Restarting Gemini CLI to continue.
openAgentConfigDialog,
openPermissionsDialog,
quit: (messages: HistoryItem[]) => {
closeThemeDialog();
setQuittingMessages(messages);
setTimeout(async () => {
await runExitCleanup();
@@ -1006,7 +1001,6 @@ Logging in with Google... Restarting Gemini CLI to continue.
[
setAuthState,
openThemeDialog,
closeThemeDialog,
openEditorDialog,
openSettingsDialog,
openSessionBrowser,
@@ -2290,7 +2284,6 @@ Logging in with Google... Restarting Gemini CLI to continue.
useRunEventNotifications({
notificationsEnabled,
notificationMethod,
isFocused,
hasReceivedFocusEvent,
streamingState,
@@ -39,7 +39,7 @@ describe('clearCommand', () => {
agentContext: {
config: {
getEnableHooks: vi.fn().mockReturnValue(false),
resetNewSessionState: vi.fn(),
setSessionId: vi.fn(),
getMessageBus: vi.fn().mockReturnValue(undefined),
getHookSystem: vi.fn().mockReturnValue({
fireSessionEndEvent: vi.fn().mockResolvedValue(undefined),
@@ -74,9 +74,6 @@ describe('clearCommand', () => {
expect(mockResetChat).toHaveBeenCalledTimes(1);
expect(mockHintClear).toHaveBeenCalledTimes(1);
expect(
mockContext.services.agentContext?.config.resetNewSessionState,
).toHaveBeenCalledTimes(1);
expect(uiTelemetryService.clear).toHaveBeenCalled();
expect(uiTelemetryService.clear).toHaveBeenCalledTimes(1);
expect(mockContext.ui.clear).toHaveBeenCalledTimes(1);
+1 -1
View File
@@ -39,7 +39,7 @@ export const clearCommand: SlashCommand = {
let newSessionId: string | undefined;
if (config) {
newSessionId = randomUUID();
config.resetNewSessionState(newSessionId);
config.setSessionId(newSessionId);
}
if (geminiClient) {
@@ -473,7 +473,7 @@ describe('memoryCommand', () => {
const mockConfig = {
reloadSkills: vi.fn(),
isAutoMemoryEnabled: vi.fn().mockReturnValue(true),
isMemoryManagerEnabled: vi.fn().mockReturnValue(true),
};
const context = createMockCommandContext({
services: {
@@ -491,11 +491,11 @@ describe('memoryCommand', () => {
expect(result).toHaveProperty('component');
});
it('should return info message when auto memory is disabled', () => {
it('should return info message when memory manager is disabled', () => {
if (!inboxCommand.action) throw new Error('Command has no action');
const mockConfig = {
isAutoMemoryEnabled: vi.fn().mockReturnValue(false),
isMemoryManagerEnabled: vi.fn().mockReturnValue(false),
};
const context = createMockCommandContext({
services: {
@@ -509,7 +509,7 @@ describe('memoryCommand', () => {
type: 'message',
messageType: 'info',
content:
'The memory inbox requires Auto Memory. Enable it with: experimental.autoMemory = true in settings.',
'The memory inbox requires the experimental memory manager. Enable it with: experimental.memoryManager = true in settings.',
});
});
@@ -145,12 +145,12 @@ export const memoryCommand: SlashCommand = {
};
}
if (!config.isAutoMemoryEnabled()) {
if (!config.isMemoryManagerEnabled()) {
return {
type: 'message',
messageType: 'info',
content:
'The memory inbox requires Auto Memory. Enable it with: experimental.autoMemory = true in settings.',
'The memory inbox requires the experimental memory manager. Enable it with: experimental.memoryManager = true in settings.',
};
}
@@ -67,47 +67,47 @@
<text x="0" y="291" fill="#878787" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="891" y="291" fill="#878787" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="308" fill="#878787" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="45" y="308" fill="#ffffff" textLength="261" lengthAdjust="spacingAndGlyphs">Enable Terminal Notifications</text>
<text x="45" y="308" fill="#ffffff" textLength="180" lengthAdjust="spacingAndGlyphs">Enable Notifications</text>
<text x="828" y="308" fill="#afafaf" textLength="45" lengthAdjust="spacingAndGlyphs">false</text>
<text x="891" y="308" fill="#878787" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="325" fill="#878787" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="45" y="325" fill="#afafaf" textLength="756" lengthAdjust="spacingAndGlyphs">Enable terminal run-event notifications for action-required prompts and session com</text>
<text x="45" y="325" fill="#afafaf" textLength="738" lengthAdjust="spacingAndGlyphs">Enable run-event notifications for action-required prompts and session completion.</text>
<text x="891" y="325" fill="#878787" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="342" fill="#878787" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="891" y="342" fill="#878787" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="359" fill="#878787" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="45" y="359" fill="#ffffff" textLength="252" lengthAdjust="spacingAndGlyphs">Terminal Notification Method</text>
<text x="837" y="359" fill="#afafaf" textLength="36" lengthAdjust="spacingAndGlyphs">Auto</text>
<text x="45" y="359" fill="#ffffff" textLength="144" lengthAdjust="spacingAndGlyphs">Enable Plan Mode</text>
<text x="837" y="359" fill="#afafaf" textLength="36" lengthAdjust="spacingAndGlyphs">true</text>
<text x="891" y="359" fill="#878787" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="376" fill="#878787" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="45" y="376" fill="#afafaf" textLength="315" lengthAdjust="spacingAndGlyphs">How to send terminal notifications.</text>
<text x="45" y="376" fill="#afafaf" textLength="486" lengthAdjust="spacingAndGlyphs">Enable Plan Mode for read-only safety during planning.</text>
<text x="891" y="376" fill="#878787" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="393" fill="#878787" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="891" y="393" fill="#878787" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="410" fill="#878787" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="45" y="410" fill="#ffffff" textLength="144" lengthAdjust="spacingAndGlyphs">Enable Plan Mode</text>
<text x="837" y="410" fill="#afafaf" textLength="36" lengthAdjust="spacingAndGlyphs">true</text>
<text x="45" y="410" fill="#ffffff" textLength="126" lengthAdjust="spacingAndGlyphs">Plan Directory</text>
<text x="792" y="410" fill="#afafaf" textLength="81" lengthAdjust="spacingAndGlyphs">undefined</text>
<text x="891" y="410" fill="#878787" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="427" fill="#878787" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="45" y="427" fill="#afafaf" textLength="486" lengthAdjust="spacingAndGlyphs">Enable Plan Mode for read-only safety during planning.</text>
<text x="45" y="427" fill="#afafaf" textLength="720" lengthAdjust="spacingAndGlyphs">The directory where planning artifacts are stored. If not specified, defaults t…</text>
<text x="891" y="427" fill="#878787" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="444" fill="#878787" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="891" y="444" fill="#878787" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="461" fill="#878787" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="45" y="461" fill="#ffffff" textLength="126" lengthAdjust="spacingAndGlyphs">Plan Directory</text>
<text x="792" y="461" fill="#afafaf" textLength="81" lengthAdjust="spacingAndGlyphs">undefined</text>
<text x="45" y="461" fill="#ffffff" textLength="162" lengthAdjust="spacingAndGlyphs">Plan Model Routing</text>
<text x="837" y="461" fill="#afafaf" textLength="36" lengthAdjust="spacingAndGlyphs">true</text>
<text x="891" y="461" fill="#878787" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="478" fill="#878787" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="45" y="478" fill="#afafaf" textLength="720" lengthAdjust="spacingAndGlyphs">The directory where planning artifacts are stored. If not specified, defaults t</text>
<text x="45" y="478" fill="#afafaf" textLength="765" lengthAdjust="spacingAndGlyphs">Automatically switch between Pro and Flash models based on Plan Mode status. Uses Pr</text>
<text x="891" y="478" fill="#878787" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="495" fill="#878787" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="891" y="495" fill="#878787" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="512" fill="#878787" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="45" y="512" fill="#ffffff" textLength="162" lengthAdjust="spacingAndGlyphs">Plan Model Routing</text>
<text x="45" y="512" fill="#ffffff" textLength="162" lengthAdjust="spacingAndGlyphs">Retry Fetch Errors</text>
<text x="837" y="512" fill="#afafaf" textLength="36" lengthAdjust="spacingAndGlyphs">true</text>
<text x="891" y="512" fill="#878787" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="529" fill="#878787" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="45" y="529" fill="#afafaf" textLength="765" lengthAdjust="spacingAndGlyphs">Automatically switch between Pro and Flash models based on Plan Mode status. Uses Pr…</text>
<text x="45" y="529" fill="#afafaf" textLength="612" lengthAdjust="spacingAndGlyphs">Retry on &quot;exception TypeError: fetch failed sending request&quot; errors.</text>
<text x="891" y="529" fill="#878787" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="546" fill="#878787" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="891" y="546" fill="#878787" textLength="9" lengthAdjust="spacingAndGlyphs"></text>

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 15 KiB

@@ -67,47 +67,47 @@
<text x="0" y="291" fill="#878787" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="891" y="291" fill="#878787" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="308" fill="#878787" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="45" y="308" fill="#ffffff" textLength="261" lengthAdjust="spacingAndGlyphs">Enable Terminal Notifications</text>
<text x="45" y="308" fill="#ffffff" textLength="180" lengthAdjust="spacingAndGlyphs">Enable Notifications</text>
<text x="828" y="308" fill="#afafaf" textLength="45" lengthAdjust="spacingAndGlyphs">false</text>
<text x="891" y="308" fill="#878787" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="325" fill="#878787" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="45" y="325" fill="#afafaf" textLength="756" lengthAdjust="spacingAndGlyphs">Enable terminal run-event notifications for action-required prompts and session com</text>
<text x="45" y="325" fill="#afafaf" textLength="738" lengthAdjust="spacingAndGlyphs">Enable run-event notifications for action-required prompts and session completion.</text>
<text x="891" y="325" fill="#878787" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="342" fill="#878787" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="891" y="342" fill="#878787" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="359" fill="#878787" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="45" y="359" fill="#ffffff" textLength="252" lengthAdjust="spacingAndGlyphs">Terminal Notification Method</text>
<text x="837" y="359" fill="#afafaf" textLength="36" lengthAdjust="spacingAndGlyphs">Auto</text>
<text x="45" y="359" fill="#ffffff" textLength="144" lengthAdjust="spacingAndGlyphs">Enable Plan Mode</text>
<text x="837" y="359" fill="#afafaf" textLength="36" lengthAdjust="spacingAndGlyphs">true</text>
<text x="891" y="359" fill="#878787" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="376" fill="#878787" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="45" y="376" fill="#afafaf" textLength="315" lengthAdjust="spacingAndGlyphs">How to send terminal notifications.</text>
<text x="45" y="376" fill="#afafaf" textLength="486" lengthAdjust="spacingAndGlyphs">Enable Plan Mode for read-only safety during planning.</text>
<text x="891" y="376" fill="#878787" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="393" fill="#878787" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="891" y="393" fill="#878787" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="410" fill="#878787" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="45" y="410" fill="#ffffff" textLength="144" lengthAdjust="spacingAndGlyphs">Enable Plan Mode</text>
<text x="837" y="410" fill="#afafaf" textLength="36" lengthAdjust="spacingAndGlyphs">true</text>
<text x="45" y="410" fill="#ffffff" textLength="126" lengthAdjust="spacingAndGlyphs">Plan Directory</text>
<text x="792" y="410" fill="#afafaf" textLength="81" lengthAdjust="spacingAndGlyphs">undefined</text>
<text x="891" y="410" fill="#878787" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="427" fill="#878787" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="45" y="427" fill="#afafaf" textLength="486" lengthAdjust="spacingAndGlyphs">Enable Plan Mode for read-only safety during planning.</text>
<text x="45" y="427" fill="#afafaf" textLength="720" lengthAdjust="spacingAndGlyphs">The directory where planning artifacts are stored. If not specified, defaults t…</text>
<text x="891" y="427" fill="#878787" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="444" fill="#878787" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="891" y="444" fill="#878787" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="461" fill="#878787" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="45" y="461" fill="#ffffff" textLength="126" lengthAdjust="spacingAndGlyphs">Plan Directory</text>
<text x="792" y="461" fill="#afafaf" textLength="81" lengthAdjust="spacingAndGlyphs">undefined</text>
<text x="45" y="461" fill="#ffffff" textLength="162" lengthAdjust="spacingAndGlyphs">Plan Model Routing</text>
<text x="837" y="461" fill="#afafaf" textLength="36" lengthAdjust="spacingAndGlyphs">true</text>
<text x="891" y="461" fill="#878787" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="478" fill="#878787" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="45" y="478" fill="#afafaf" textLength="720" lengthAdjust="spacingAndGlyphs">The directory where planning artifacts are stored. If not specified, defaults t</text>
<text x="45" y="478" fill="#afafaf" textLength="765" lengthAdjust="spacingAndGlyphs">Automatically switch between Pro and Flash models based on Plan Mode status. Uses Pr</text>
<text x="891" y="478" fill="#878787" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="495" fill="#878787" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="891" y="495" fill="#878787" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="512" fill="#878787" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="45" y="512" fill="#ffffff" textLength="162" lengthAdjust="spacingAndGlyphs">Plan Model Routing</text>
<text x="45" y="512" fill="#ffffff" textLength="162" lengthAdjust="spacingAndGlyphs">Retry Fetch Errors</text>
<text x="837" y="512" fill="#afafaf" textLength="36" lengthAdjust="spacingAndGlyphs">true</text>
<text x="891" y="512" fill="#878787" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="529" fill="#878787" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="45" y="529" fill="#afafaf" textLength="765" lengthAdjust="spacingAndGlyphs">Automatically switch between Pro and Flash models based on Plan Mode status. Uses Pr…</text>
<text x="45" y="529" fill="#afafaf" textLength="612" lengthAdjust="spacingAndGlyphs">Retry on &quot;exception TypeError: fetch failed sending request&quot; errors.</text>
<text x="891" y="529" fill="#878787" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="546" fill="#878787" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="891" y="546" fill="#878787" textLength="9" lengthAdjust="spacingAndGlyphs"></text>

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 15 KiB

@@ -67,47 +67,47 @@
<text x="0" y="291" fill="#878787" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="891" y="291" fill="#878787" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="308" fill="#878787" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="45" y="308" fill="#ffffff" textLength="261" lengthAdjust="spacingAndGlyphs">Enable Terminal Notifications</text>
<text x="45" y="308" fill="#ffffff" textLength="180" lengthAdjust="spacingAndGlyphs">Enable Notifications</text>
<text x="828" y="308" fill="#afafaf" textLength="45" lengthAdjust="spacingAndGlyphs">false</text>
<text x="891" y="308" fill="#878787" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="325" fill="#878787" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="45" y="325" fill="#afafaf" textLength="756" lengthAdjust="spacingAndGlyphs">Enable terminal run-event notifications for action-required prompts and session com</text>
<text x="45" y="325" fill="#afafaf" textLength="738" lengthAdjust="spacingAndGlyphs">Enable run-event notifications for action-required prompts and session completion.</text>
<text x="891" y="325" fill="#878787" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="342" fill="#878787" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="891" y="342" fill="#878787" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="359" fill="#878787" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="45" y="359" fill="#ffffff" textLength="252" lengthAdjust="spacingAndGlyphs">Terminal Notification Method</text>
<text x="837" y="359" fill="#afafaf" textLength="36" lengthAdjust="spacingAndGlyphs">Auto</text>
<text x="45" y="359" fill="#ffffff" textLength="144" lengthAdjust="spacingAndGlyphs">Enable Plan Mode</text>
<text x="837" y="359" fill="#afafaf" textLength="36" lengthAdjust="spacingAndGlyphs">true</text>
<text x="891" y="359" fill="#878787" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="376" fill="#878787" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="45" y="376" fill="#afafaf" textLength="315" lengthAdjust="spacingAndGlyphs">How to send terminal notifications.</text>
<text x="45" y="376" fill="#afafaf" textLength="486" lengthAdjust="spacingAndGlyphs">Enable Plan Mode for read-only safety during planning.</text>
<text x="891" y="376" fill="#878787" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="393" fill="#878787" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="891" y="393" fill="#878787" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="410" fill="#878787" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="45" y="410" fill="#ffffff" textLength="144" lengthAdjust="spacingAndGlyphs">Enable Plan Mode</text>
<text x="837" y="410" fill="#afafaf" textLength="36" lengthAdjust="spacingAndGlyphs">true</text>
<text x="45" y="410" fill="#ffffff" textLength="126" lengthAdjust="spacingAndGlyphs">Plan Directory</text>
<text x="792" y="410" fill="#afafaf" textLength="81" lengthAdjust="spacingAndGlyphs">undefined</text>
<text x="891" y="410" fill="#878787" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="427" fill="#878787" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="45" y="427" fill="#afafaf" textLength="486" lengthAdjust="spacingAndGlyphs">Enable Plan Mode for read-only safety during planning.</text>
<text x="45" y="427" fill="#afafaf" textLength="720" lengthAdjust="spacingAndGlyphs">The directory where planning artifacts are stored. If not specified, defaults t…</text>
<text x="891" y="427" fill="#878787" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="444" fill="#878787" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="891" y="444" fill="#878787" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="461" fill="#878787" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="45" y="461" fill="#ffffff" textLength="126" lengthAdjust="spacingAndGlyphs">Plan Directory</text>
<text x="792" y="461" fill="#afafaf" textLength="81" lengthAdjust="spacingAndGlyphs">undefined</text>
<text x="45" y="461" fill="#ffffff" textLength="162" lengthAdjust="spacingAndGlyphs">Plan Model Routing</text>
<text x="837" y="461" fill="#afafaf" textLength="36" lengthAdjust="spacingAndGlyphs">true</text>
<text x="891" y="461" fill="#878787" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="478" fill="#878787" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="45" y="478" fill="#afafaf" textLength="720" lengthAdjust="spacingAndGlyphs">The directory where planning artifacts are stored. If not specified, defaults t</text>
<text x="45" y="478" fill="#afafaf" textLength="765" lengthAdjust="spacingAndGlyphs">Automatically switch between Pro and Flash models based on Plan Mode status. Uses Pr</text>
<text x="891" y="478" fill="#878787" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="495" fill="#878787" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="891" y="495" fill="#878787" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="512" fill="#878787" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="45" y="512" fill="#ffffff" textLength="162" lengthAdjust="spacingAndGlyphs">Plan Model Routing</text>
<text x="45" y="512" fill="#ffffff" textLength="162" lengthAdjust="spacingAndGlyphs">Retry Fetch Errors</text>
<text x="837" y="512" fill="#afafaf" textLength="36" lengthAdjust="spacingAndGlyphs">true</text>
<text x="891" y="512" fill="#878787" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="529" fill="#878787" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="45" y="529" fill="#afafaf" textLength="765" lengthAdjust="spacingAndGlyphs">Automatically switch between Pro and Flash models based on Plan Mode status. Uses Pr…</text>
<text x="45" y="529" fill="#afafaf" textLength="612" lengthAdjust="spacingAndGlyphs">Retry on &quot;exception TypeError: fetch failed sending request&quot; errors.</text>
<text x="891" y="529" fill="#878787" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="546" fill="#878787" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="891" y="546" fill="#878787" textLength="9" lengthAdjust="spacingAndGlyphs"></text>

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 15 KiB

@@ -67,47 +67,47 @@
<text x="0" y="291" fill="#878787" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="891" y="291" fill="#878787" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="308" fill="#878787" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="45" y="308" fill="#ffffff" textLength="261" lengthAdjust="spacingAndGlyphs">Enable Terminal Notifications</text>
<text x="45" y="308" fill="#ffffff" textLength="180" lengthAdjust="spacingAndGlyphs">Enable Notifications</text>
<text x="828" y="308" fill="#afafaf" textLength="45" lengthAdjust="spacingAndGlyphs">false</text>
<text x="891" y="308" fill="#878787" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="325" fill="#878787" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="45" y="325" fill="#afafaf" textLength="756" lengthAdjust="spacingAndGlyphs">Enable terminal run-event notifications for action-required prompts and session com</text>
<text x="45" y="325" fill="#afafaf" textLength="738" lengthAdjust="spacingAndGlyphs">Enable run-event notifications for action-required prompts and session completion.</text>
<text x="891" y="325" fill="#878787" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="342" fill="#878787" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="891" y="342" fill="#878787" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="359" fill="#878787" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="45" y="359" fill="#ffffff" textLength="252" lengthAdjust="spacingAndGlyphs">Terminal Notification Method</text>
<text x="837" y="359" fill="#afafaf" textLength="36" lengthAdjust="spacingAndGlyphs">Auto</text>
<text x="45" y="359" fill="#ffffff" textLength="144" lengthAdjust="spacingAndGlyphs">Enable Plan Mode</text>
<text x="837" y="359" fill="#afafaf" textLength="36" lengthAdjust="spacingAndGlyphs">true</text>
<text x="891" y="359" fill="#878787" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="376" fill="#878787" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="45" y="376" fill="#afafaf" textLength="315" lengthAdjust="spacingAndGlyphs">How to send terminal notifications.</text>
<text x="45" y="376" fill="#afafaf" textLength="486" lengthAdjust="spacingAndGlyphs">Enable Plan Mode for read-only safety during planning.</text>
<text x="891" y="376" fill="#878787" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="393" fill="#878787" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="891" y="393" fill="#878787" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="410" fill="#878787" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="45" y="410" fill="#ffffff" textLength="144" lengthAdjust="spacingAndGlyphs">Enable Plan Mode</text>
<text x="837" y="410" fill="#afafaf" textLength="36" lengthAdjust="spacingAndGlyphs">true</text>
<text x="45" y="410" fill="#ffffff" textLength="126" lengthAdjust="spacingAndGlyphs">Plan Directory</text>
<text x="792" y="410" fill="#afafaf" textLength="81" lengthAdjust="spacingAndGlyphs">undefined</text>
<text x="891" y="410" fill="#878787" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="427" fill="#878787" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="45" y="427" fill="#afafaf" textLength="486" lengthAdjust="spacingAndGlyphs">Enable Plan Mode for read-only safety during planning.</text>
<text x="45" y="427" fill="#afafaf" textLength="720" lengthAdjust="spacingAndGlyphs">The directory where planning artifacts are stored. If not specified, defaults t…</text>
<text x="891" y="427" fill="#878787" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="444" fill="#878787" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="891" y="444" fill="#878787" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="461" fill="#878787" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="45" y="461" fill="#ffffff" textLength="126" lengthAdjust="spacingAndGlyphs">Plan Directory</text>
<text x="792" y="461" fill="#afafaf" textLength="81" lengthAdjust="spacingAndGlyphs">undefined</text>
<text x="45" y="461" fill="#ffffff" textLength="162" lengthAdjust="spacingAndGlyphs">Plan Model Routing</text>
<text x="837" y="461" fill="#afafaf" textLength="36" lengthAdjust="spacingAndGlyphs">true</text>
<text x="891" y="461" fill="#878787" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="478" fill="#878787" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="45" y="478" fill="#afafaf" textLength="720" lengthAdjust="spacingAndGlyphs">The directory where planning artifacts are stored. If not specified, defaults t</text>
<text x="45" y="478" fill="#afafaf" textLength="765" lengthAdjust="spacingAndGlyphs">Automatically switch between Pro and Flash models based on Plan Mode status. Uses Pr</text>
<text x="891" y="478" fill="#878787" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="495" fill="#878787" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="891" y="495" fill="#878787" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="512" fill="#878787" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="45" y="512" fill="#ffffff" textLength="162" lengthAdjust="spacingAndGlyphs">Plan Model Routing</text>
<text x="45" y="512" fill="#ffffff" textLength="162" lengthAdjust="spacingAndGlyphs">Retry Fetch Errors</text>
<text x="837" y="512" fill="#afafaf" textLength="36" lengthAdjust="spacingAndGlyphs">true</text>
<text x="891" y="512" fill="#878787" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="529" fill="#878787" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="45" y="529" fill="#afafaf" textLength="765" lengthAdjust="spacingAndGlyphs">Automatically switch between Pro and Flash models based on Plan Mode status. Uses Pr…</text>
<text x="45" y="529" fill="#afafaf" textLength="612" lengthAdjust="spacingAndGlyphs">Retry on &quot;exception TypeError: fetch failed sending request&quot; errors.</text>
<text x="891" y="529" fill="#878787" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="546" fill="#878787" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="891" y="546" fill="#878787" textLength="9" lengthAdjust="spacingAndGlyphs"></text>

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 15 KiB

@@ -67,47 +67,47 @@
<text x="0" y="291" fill="#878787" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="891" y="291" fill="#878787" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="308" fill="#878787" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="45" y="308" fill="#ffffff" textLength="261" lengthAdjust="spacingAndGlyphs">Enable Terminal Notifications</text>
<text x="45" y="308" fill="#ffffff" textLength="180" lengthAdjust="spacingAndGlyphs">Enable Notifications</text>
<text x="828" y="308" fill="#afafaf" textLength="45" lengthAdjust="spacingAndGlyphs">false</text>
<text x="891" y="308" fill="#878787" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="325" fill="#878787" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="45" y="325" fill="#afafaf" textLength="756" lengthAdjust="spacingAndGlyphs">Enable terminal run-event notifications for action-required prompts and session com</text>
<text x="45" y="325" fill="#afafaf" textLength="738" lengthAdjust="spacingAndGlyphs">Enable run-event notifications for action-required prompts and session completion.</text>
<text x="891" y="325" fill="#878787" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="342" fill="#878787" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="891" y="342" fill="#878787" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="359" fill="#878787" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="45" y="359" fill="#ffffff" textLength="252" lengthAdjust="spacingAndGlyphs">Terminal Notification Method</text>
<text x="837" y="359" fill="#afafaf" textLength="36" lengthAdjust="spacingAndGlyphs">Auto</text>
<text x="45" y="359" fill="#ffffff" textLength="144" lengthAdjust="spacingAndGlyphs">Enable Plan Mode</text>
<text x="837" y="359" fill="#afafaf" textLength="36" lengthAdjust="spacingAndGlyphs">true</text>
<text x="891" y="359" fill="#878787" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="376" fill="#878787" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="45" y="376" fill="#afafaf" textLength="315" lengthAdjust="spacingAndGlyphs">How to send terminal notifications.</text>
<text x="45" y="376" fill="#afafaf" textLength="486" lengthAdjust="spacingAndGlyphs">Enable Plan Mode for read-only safety during planning.</text>
<text x="891" y="376" fill="#878787" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="393" fill="#878787" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="891" y="393" fill="#878787" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="410" fill="#878787" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="45" y="410" fill="#ffffff" textLength="144" lengthAdjust="spacingAndGlyphs">Enable Plan Mode</text>
<text x="837" y="410" fill="#afafaf" textLength="36" lengthAdjust="spacingAndGlyphs">true</text>
<text x="45" y="410" fill="#ffffff" textLength="126" lengthAdjust="spacingAndGlyphs">Plan Directory</text>
<text x="792" y="410" fill="#afafaf" textLength="81" lengthAdjust="spacingAndGlyphs">undefined</text>
<text x="891" y="410" fill="#878787" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="427" fill="#878787" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="45" y="427" fill="#afafaf" textLength="486" lengthAdjust="spacingAndGlyphs">Enable Plan Mode for read-only safety during planning.</text>
<text x="45" y="427" fill="#afafaf" textLength="720" lengthAdjust="spacingAndGlyphs">The directory where planning artifacts are stored. If not specified, defaults t…</text>
<text x="891" y="427" fill="#878787" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="444" fill="#878787" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="891" y="444" fill="#878787" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="461" fill="#878787" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="45" y="461" fill="#ffffff" textLength="126" lengthAdjust="spacingAndGlyphs">Plan Directory</text>
<text x="792" y="461" fill="#afafaf" textLength="81" lengthAdjust="spacingAndGlyphs">undefined</text>
<text x="45" y="461" fill="#ffffff" textLength="162" lengthAdjust="spacingAndGlyphs">Plan Model Routing</text>
<text x="837" y="461" fill="#afafaf" textLength="36" lengthAdjust="spacingAndGlyphs">true</text>
<text x="891" y="461" fill="#878787" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="478" fill="#878787" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="45" y="478" fill="#afafaf" textLength="720" lengthAdjust="spacingAndGlyphs">The directory where planning artifacts are stored. If not specified, defaults t</text>
<text x="45" y="478" fill="#afafaf" textLength="765" lengthAdjust="spacingAndGlyphs">Automatically switch between Pro and Flash models based on Plan Mode status. Uses Pr</text>
<text x="891" y="478" fill="#878787" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="495" fill="#878787" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="891" y="495" fill="#878787" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="512" fill="#878787" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="45" y="512" fill="#ffffff" textLength="162" lengthAdjust="spacingAndGlyphs">Plan Model Routing</text>
<text x="45" y="512" fill="#ffffff" textLength="162" lengthAdjust="spacingAndGlyphs">Retry Fetch Errors</text>
<text x="837" y="512" fill="#afafaf" textLength="36" lengthAdjust="spacingAndGlyphs">true</text>
<text x="891" y="512" fill="#878787" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="529" fill="#878787" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="45" y="529" fill="#afafaf" textLength="765" lengthAdjust="spacingAndGlyphs">Automatically switch between Pro and Flash models based on Plan Mode status. Uses Pr…</text>
<text x="45" y="529" fill="#afafaf" textLength="612" lengthAdjust="spacingAndGlyphs">Retry on &quot;exception TypeError: fetch failed sending request&quot; errors.</text>
<text x="891" y="529" fill="#878787" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="546" fill="#878787" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="891" y="546" fill="#878787" textLength="9" lengthAdjust="spacingAndGlyphs"></text>

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 15 KiB

@@ -56,47 +56,47 @@
<text x="0" y="291" fill="#878787" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="891" y="291" fill="#878787" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="308" fill="#878787" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="45" y="308" fill="#ffffff" textLength="261" lengthAdjust="spacingAndGlyphs">Enable Terminal Notifications</text>
<text x="45" y="308" fill="#ffffff" textLength="180" lengthAdjust="spacingAndGlyphs">Enable Notifications</text>
<text x="828" y="308" fill="#afafaf" textLength="45" lengthAdjust="spacingAndGlyphs">false</text>
<text x="891" y="308" fill="#878787" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="325" fill="#878787" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="45" y="325" fill="#afafaf" textLength="756" lengthAdjust="spacingAndGlyphs">Enable terminal run-event notifications for action-required prompts and session com</text>
<text x="45" y="325" fill="#afafaf" textLength="738" lengthAdjust="spacingAndGlyphs">Enable run-event notifications for action-required prompts and session completion.</text>
<text x="891" y="325" fill="#878787" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="342" fill="#878787" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="891" y="342" fill="#878787" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="359" fill="#878787" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="45" y="359" fill="#ffffff" textLength="252" lengthAdjust="spacingAndGlyphs">Terminal Notification Method</text>
<text x="837" y="359" fill="#afafaf" textLength="36" lengthAdjust="spacingAndGlyphs">Auto</text>
<text x="45" y="359" fill="#ffffff" textLength="144" lengthAdjust="spacingAndGlyphs">Enable Plan Mode</text>
<text x="837" y="359" fill="#afafaf" textLength="36" lengthAdjust="spacingAndGlyphs">true</text>
<text x="891" y="359" fill="#878787" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="376" fill="#878787" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="45" y="376" fill="#afafaf" textLength="315" lengthAdjust="spacingAndGlyphs">How to send terminal notifications.</text>
<text x="45" y="376" fill="#afafaf" textLength="486" lengthAdjust="spacingAndGlyphs">Enable Plan Mode for read-only safety during planning.</text>
<text x="891" y="376" fill="#878787" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="393" fill="#878787" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="891" y="393" fill="#878787" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="410" fill="#878787" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="45" y="410" fill="#ffffff" textLength="144" lengthAdjust="spacingAndGlyphs">Enable Plan Mode</text>
<text x="837" y="410" fill="#afafaf" textLength="36" lengthAdjust="spacingAndGlyphs">true</text>
<text x="45" y="410" fill="#ffffff" textLength="126" lengthAdjust="spacingAndGlyphs">Plan Directory</text>
<text x="792" y="410" fill="#afafaf" textLength="81" lengthAdjust="spacingAndGlyphs">undefined</text>
<text x="891" y="410" fill="#878787" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="427" fill="#878787" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="45" y="427" fill="#afafaf" textLength="486" lengthAdjust="spacingAndGlyphs">Enable Plan Mode for read-only safety during planning.</text>
<text x="45" y="427" fill="#afafaf" textLength="720" lengthAdjust="spacingAndGlyphs">The directory where planning artifacts are stored. If not specified, defaults t…</text>
<text x="891" y="427" fill="#878787" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="444" fill="#878787" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="891" y="444" fill="#878787" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="461" fill="#878787" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="45" y="461" fill="#ffffff" textLength="126" lengthAdjust="spacingAndGlyphs">Plan Directory</text>
<text x="792" y="461" fill="#afafaf" textLength="81" lengthAdjust="spacingAndGlyphs">undefined</text>
<text x="45" y="461" fill="#ffffff" textLength="162" lengthAdjust="spacingAndGlyphs">Plan Model Routing</text>
<text x="837" y="461" fill="#afafaf" textLength="36" lengthAdjust="spacingAndGlyphs">true</text>
<text x="891" y="461" fill="#878787" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="478" fill="#878787" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="45" y="478" fill="#afafaf" textLength="720" lengthAdjust="spacingAndGlyphs">The directory where planning artifacts are stored. If not specified, defaults t</text>
<text x="45" y="478" fill="#afafaf" textLength="765" lengthAdjust="spacingAndGlyphs">Automatically switch between Pro and Flash models based on Plan Mode status. Uses Pr</text>
<text x="891" y="478" fill="#878787" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="495" fill="#878787" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="891" y="495" fill="#878787" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="512" fill="#878787" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="45" y="512" fill="#ffffff" textLength="162" lengthAdjust="spacingAndGlyphs">Plan Model Routing</text>
<text x="45" y="512" fill="#ffffff" textLength="162" lengthAdjust="spacingAndGlyphs">Retry Fetch Errors</text>
<text x="837" y="512" fill="#afafaf" textLength="36" lengthAdjust="spacingAndGlyphs">true</text>
<text x="891" y="512" fill="#878787" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="529" fill="#878787" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="45" y="529" fill="#afafaf" textLength="765" lengthAdjust="spacingAndGlyphs">Automatically switch between Pro and Flash models based on Plan Mode status. Uses Pr…</text>
<text x="45" y="529" fill="#afafaf" textLength="612" lengthAdjust="spacingAndGlyphs">Retry on &quot;exception TypeError: fetch failed sending request&quot; errors.</text>
<text x="891" y="529" fill="#878787" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="546" fill="#878787" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="891" y="546" fill="#878787" textLength="9" lengthAdjust="spacingAndGlyphs"></text>

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 15 KiB

@@ -67,47 +67,47 @@
<text x="0" y="291" fill="#878787" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="891" y="291" fill="#878787" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="308" fill="#878787" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="45" y="308" fill="#ffffff" textLength="261" lengthAdjust="spacingAndGlyphs">Enable Terminal Notifications</text>
<text x="45" y="308" fill="#ffffff" textLength="180" lengthAdjust="spacingAndGlyphs">Enable Notifications</text>
<text x="828" y="308" fill="#afafaf" textLength="45" lengthAdjust="spacingAndGlyphs">false</text>
<text x="891" y="308" fill="#878787" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="325" fill="#878787" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="45" y="325" fill="#afafaf" textLength="756" lengthAdjust="spacingAndGlyphs">Enable terminal run-event notifications for action-required prompts and session com</text>
<text x="45" y="325" fill="#afafaf" textLength="738" lengthAdjust="spacingAndGlyphs">Enable run-event notifications for action-required prompts and session completion.</text>
<text x="891" y="325" fill="#878787" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="342" fill="#878787" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="891" y="342" fill="#878787" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="359" fill="#878787" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="45" y="359" fill="#ffffff" textLength="252" lengthAdjust="spacingAndGlyphs">Terminal Notification Method</text>
<text x="837" y="359" fill="#afafaf" textLength="36" lengthAdjust="spacingAndGlyphs">Auto</text>
<text x="45" y="359" fill="#ffffff" textLength="144" lengthAdjust="spacingAndGlyphs">Enable Plan Mode</text>
<text x="837" y="359" fill="#afafaf" textLength="36" lengthAdjust="spacingAndGlyphs">true</text>
<text x="891" y="359" fill="#878787" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="376" fill="#878787" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="45" y="376" fill="#afafaf" textLength="315" lengthAdjust="spacingAndGlyphs">How to send terminal notifications.</text>
<text x="45" y="376" fill="#afafaf" textLength="486" lengthAdjust="spacingAndGlyphs">Enable Plan Mode for read-only safety during planning.</text>
<text x="891" y="376" fill="#878787" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="393" fill="#878787" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="891" y="393" fill="#878787" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="410" fill="#878787" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="45" y="410" fill="#ffffff" textLength="144" lengthAdjust="spacingAndGlyphs">Enable Plan Mode</text>
<text x="837" y="410" fill="#afafaf" textLength="36" lengthAdjust="spacingAndGlyphs">true</text>
<text x="45" y="410" fill="#ffffff" textLength="126" lengthAdjust="spacingAndGlyphs">Plan Directory</text>
<text x="792" y="410" fill="#afafaf" textLength="81" lengthAdjust="spacingAndGlyphs">undefined</text>
<text x="891" y="410" fill="#878787" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="427" fill="#878787" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="45" y="427" fill="#afafaf" textLength="486" lengthAdjust="spacingAndGlyphs">Enable Plan Mode for read-only safety during planning.</text>
<text x="45" y="427" fill="#afafaf" textLength="720" lengthAdjust="spacingAndGlyphs">The directory where planning artifacts are stored. If not specified, defaults t…</text>
<text x="891" y="427" fill="#878787" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="444" fill="#878787" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="891" y="444" fill="#878787" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="461" fill="#878787" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="45" y="461" fill="#ffffff" textLength="126" lengthAdjust="spacingAndGlyphs">Plan Directory</text>
<text x="792" y="461" fill="#afafaf" textLength="81" lengthAdjust="spacingAndGlyphs">undefined</text>
<text x="45" y="461" fill="#ffffff" textLength="162" lengthAdjust="spacingAndGlyphs">Plan Model Routing</text>
<text x="837" y="461" fill="#afafaf" textLength="36" lengthAdjust="spacingAndGlyphs">true</text>
<text x="891" y="461" fill="#878787" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="478" fill="#878787" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="45" y="478" fill="#afafaf" textLength="720" lengthAdjust="spacingAndGlyphs">The directory where planning artifacts are stored. If not specified, defaults t</text>
<text x="45" y="478" fill="#afafaf" textLength="765" lengthAdjust="spacingAndGlyphs">Automatically switch between Pro and Flash models based on Plan Mode status. Uses Pr</text>
<text x="891" y="478" fill="#878787" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="495" fill="#878787" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="891" y="495" fill="#878787" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="512" fill="#878787" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="45" y="512" fill="#ffffff" textLength="162" lengthAdjust="spacingAndGlyphs">Plan Model Routing</text>
<text x="45" y="512" fill="#ffffff" textLength="162" lengthAdjust="spacingAndGlyphs">Retry Fetch Errors</text>
<text x="837" y="512" fill="#afafaf" textLength="36" lengthAdjust="spacingAndGlyphs">true</text>
<text x="891" y="512" fill="#878787" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="529" fill="#878787" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="45" y="529" fill="#afafaf" textLength="765" lengthAdjust="spacingAndGlyphs">Automatically switch between Pro and Flash models based on Plan Mode status. Uses Pr…</text>
<text x="45" y="529" fill="#afafaf" textLength="612" lengthAdjust="spacingAndGlyphs">Retry on &quot;exception TypeError: fetch failed sending request&quot; errors.</text>
<text x="891" y="529" fill="#878787" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="546" fill="#878787" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="891" y="546" fill="#878787" textLength="9" lengthAdjust="spacingAndGlyphs"></text>

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 15 KiB

@@ -67,47 +67,47 @@
<text x="0" y="291" fill="#878787" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="891" y="291" fill="#878787" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="308" fill="#878787" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="45" y="308" fill="#ffffff" textLength="261" lengthAdjust="spacingAndGlyphs">Enable Terminal Notifications</text>
<text x="45" y="308" fill="#ffffff" textLength="180" lengthAdjust="spacingAndGlyphs">Enable Notifications</text>
<text x="828" y="308" fill="#afafaf" textLength="45" lengthAdjust="spacingAndGlyphs">false</text>
<text x="891" y="308" fill="#878787" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="325" fill="#878787" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="45" y="325" fill="#afafaf" textLength="756" lengthAdjust="spacingAndGlyphs">Enable terminal run-event notifications for action-required prompts and session com</text>
<text x="45" y="325" fill="#afafaf" textLength="738" lengthAdjust="spacingAndGlyphs">Enable run-event notifications for action-required prompts and session completion.</text>
<text x="891" y="325" fill="#878787" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="342" fill="#878787" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="891" y="342" fill="#878787" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="359" fill="#878787" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="45" y="359" fill="#ffffff" textLength="252" lengthAdjust="spacingAndGlyphs">Terminal Notification Method</text>
<text x="837" y="359" fill="#afafaf" textLength="36" lengthAdjust="spacingAndGlyphs">Auto</text>
<text x="45" y="359" fill="#ffffff" textLength="144" lengthAdjust="spacingAndGlyphs">Enable Plan Mode</text>
<text x="837" y="359" fill="#afafaf" textLength="36" lengthAdjust="spacingAndGlyphs">true</text>
<text x="891" y="359" fill="#878787" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="376" fill="#878787" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="45" y="376" fill="#afafaf" textLength="315" lengthAdjust="spacingAndGlyphs">How to send terminal notifications.</text>
<text x="45" y="376" fill="#afafaf" textLength="486" lengthAdjust="spacingAndGlyphs">Enable Plan Mode for read-only safety during planning.</text>
<text x="891" y="376" fill="#878787" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="393" fill="#878787" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="891" y="393" fill="#878787" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="410" fill="#878787" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="45" y="410" fill="#ffffff" textLength="144" lengthAdjust="spacingAndGlyphs">Enable Plan Mode</text>
<text x="837" y="410" fill="#afafaf" textLength="36" lengthAdjust="spacingAndGlyphs">true</text>
<text x="45" y="410" fill="#ffffff" textLength="126" lengthAdjust="spacingAndGlyphs">Plan Directory</text>
<text x="792" y="410" fill="#afafaf" textLength="81" lengthAdjust="spacingAndGlyphs">undefined</text>
<text x="891" y="410" fill="#878787" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="427" fill="#878787" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="45" y="427" fill="#afafaf" textLength="486" lengthAdjust="spacingAndGlyphs">Enable Plan Mode for read-only safety during planning.</text>
<text x="45" y="427" fill="#afafaf" textLength="720" lengthAdjust="spacingAndGlyphs">The directory where planning artifacts are stored. If not specified, defaults t…</text>
<text x="891" y="427" fill="#878787" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="444" fill="#878787" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="891" y="444" fill="#878787" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="461" fill="#878787" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="45" y="461" fill="#ffffff" textLength="126" lengthAdjust="spacingAndGlyphs">Plan Directory</text>
<text x="792" y="461" fill="#afafaf" textLength="81" lengthAdjust="spacingAndGlyphs">undefined</text>
<text x="45" y="461" fill="#ffffff" textLength="162" lengthAdjust="spacingAndGlyphs">Plan Model Routing</text>
<text x="837" y="461" fill="#afafaf" textLength="36" lengthAdjust="spacingAndGlyphs">true</text>
<text x="891" y="461" fill="#878787" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="478" fill="#878787" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="45" y="478" fill="#afafaf" textLength="720" lengthAdjust="spacingAndGlyphs">The directory where planning artifacts are stored. If not specified, defaults t</text>
<text x="45" y="478" fill="#afafaf" textLength="765" lengthAdjust="spacingAndGlyphs">Automatically switch between Pro and Flash models based on Plan Mode status. Uses Pr</text>
<text x="891" y="478" fill="#878787" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="495" fill="#878787" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="891" y="495" fill="#878787" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="512" fill="#878787" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="45" y="512" fill="#ffffff" textLength="162" lengthAdjust="spacingAndGlyphs">Plan Model Routing</text>
<text x="45" y="512" fill="#ffffff" textLength="162" lengthAdjust="spacingAndGlyphs">Retry Fetch Errors</text>
<text x="837" y="512" fill="#afafaf" textLength="36" lengthAdjust="spacingAndGlyphs">true</text>
<text x="891" y="512" fill="#878787" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="529" fill="#878787" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="45" y="529" fill="#afafaf" textLength="765" lengthAdjust="spacingAndGlyphs">Automatically switch between Pro and Flash models based on Plan Mode status. Uses Pr…</text>
<text x="45" y="529" fill="#afafaf" textLength="612" lengthAdjust="spacingAndGlyphs">Retry on &quot;exception TypeError: fetch failed sending request&quot; errors.</text>
<text x="891" y="529" fill="#878787" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="546" fill="#878787" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="891" y="546" fill="#878787" textLength="9" lengthAdjust="spacingAndGlyphs"></text>

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 15 KiB

@@ -67,47 +67,47 @@
<text x="0" y="291" fill="#878787" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="891" y="291" fill="#878787" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="308" fill="#878787" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="45" y="308" fill="#ffffff" textLength="261" lengthAdjust="spacingAndGlyphs">Enable Terminal Notifications</text>
<text x="45" y="308" fill="#ffffff" textLength="180" lengthAdjust="spacingAndGlyphs">Enable Notifications</text>
<text x="828" y="308" fill="#afafaf" textLength="45" lengthAdjust="spacingAndGlyphs">false</text>
<text x="891" y="308" fill="#878787" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="325" fill="#878787" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="45" y="325" fill="#afafaf" textLength="756" lengthAdjust="spacingAndGlyphs">Enable terminal run-event notifications for action-required prompts and session com</text>
<text x="45" y="325" fill="#afafaf" textLength="738" lengthAdjust="spacingAndGlyphs">Enable run-event notifications for action-required prompts and session completion.</text>
<text x="891" y="325" fill="#878787" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="342" fill="#878787" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="891" y="342" fill="#878787" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="359" fill="#878787" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="45" y="359" fill="#ffffff" textLength="252" lengthAdjust="spacingAndGlyphs">Terminal Notification Method</text>
<text x="837" y="359" fill="#afafaf" textLength="36" lengthAdjust="spacingAndGlyphs">Auto</text>
<text x="45" y="359" fill="#ffffff" textLength="144" lengthAdjust="spacingAndGlyphs">Enable Plan Mode</text>
<text x="837" y="359" fill="#afafaf" textLength="36" lengthAdjust="spacingAndGlyphs">true</text>
<text x="891" y="359" fill="#878787" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="376" fill="#878787" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="45" y="376" fill="#afafaf" textLength="315" lengthAdjust="spacingAndGlyphs">How to send terminal notifications.</text>
<text x="45" y="376" fill="#afafaf" textLength="486" lengthAdjust="spacingAndGlyphs">Enable Plan Mode for read-only safety during planning.</text>
<text x="891" y="376" fill="#878787" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="393" fill="#878787" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="891" y="393" fill="#878787" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="410" fill="#878787" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="45" y="410" fill="#ffffff" textLength="144" lengthAdjust="spacingAndGlyphs">Enable Plan Mode</text>
<text x="837" y="410" fill="#afafaf" textLength="36" lengthAdjust="spacingAndGlyphs">true</text>
<text x="45" y="410" fill="#ffffff" textLength="126" lengthAdjust="spacingAndGlyphs">Plan Directory</text>
<text x="792" y="410" fill="#afafaf" textLength="81" lengthAdjust="spacingAndGlyphs">undefined</text>
<text x="891" y="410" fill="#878787" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="427" fill="#878787" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="45" y="427" fill="#afafaf" textLength="486" lengthAdjust="spacingAndGlyphs">Enable Plan Mode for read-only safety during planning.</text>
<text x="45" y="427" fill="#afafaf" textLength="720" lengthAdjust="spacingAndGlyphs">The directory where planning artifacts are stored. If not specified, defaults t…</text>
<text x="891" y="427" fill="#878787" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="444" fill="#878787" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="891" y="444" fill="#878787" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="461" fill="#878787" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="45" y="461" fill="#ffffff" textLength="126" lengthAdjust="spacingAndGlyphs">Plan Directory</text>
<text x="792" y="461" fill="#afafaf" textLength="81" lengthAdjust="spacingAndGlyphs">undefined</text>
<text x="45" y="461" fill="#ffffff" textLength="162" lengthAdjust="spacingAndGlyphs">Plan Model Routing</text>
<text x="837" y="461" fill="#afafaf" textLength="36" lengthAdjust="spacingAndGlyphs">true</text>
<text x="891" y="461" fill="#878787" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="478" fill="#878787" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="45" y="478" fill="#afafaf" textLength="720" lengthAdjust="spacingAndGlyphs">The directory where planning artifacts are stored. If not specified, defaults t</text>
<text x="45" y="478" fill="#afafaf" textLength="765" lengthAdjust="spacingAndGlyphs">Automatically switch between Pro and Flash models based on Plan Mode status. Uses Pr</text>
<text x="891" y="478" fill="#878787" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="495" fill="#878787" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="891" y="495" fill="#878787" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="512" fill="#878787" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="45" y="512" fill="#ffffff" textLength="162" lengthAdjust="spacingAndGlyphs">Plan Model Routing</text>
<text x="45" y="512" fill="#ffffff" textLength="162" lengthAdjust="spacingAndGlyphs">Retry Fetch Errors</text>
<text x="837" y="512" fill="#afafaf" textLength="36" lengthAdjust="spacingAndGlyphs">true</text>
<text x="891" y="512" fill="#878787" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="529" fill="#878787" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="45" y="529" fill="#afafaf" textLength="765" lengthAdjust="spacingAndGlyphs">Automatically switch between Pro and Flash models based on Plan Mode status. Uses Pr…</text>
<text x="45" y="529" fill="#afafaf" textLength="612" lengthAdjust="spacingAndGlyphs">Retry on &quot;exception TypeError: fetch failed sending request&quot; errors.</text>
<text x="891" y="529" fill="#878787" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="546" fill="#878787" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="891" y="546" fill="#878787" textLength="9" lengthAdjust="spacingAndGlyphs"></text>

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 15 KiB

@@ -19,11 +19,8 @@ exports[`SettingsDialog > Initial Rendering > should render settings list with v
│ Enable Auto Update true │
│ Enable automatic updates. │
│ │
│ Enable Terminal Notifications false │
│ Enable terminal run-event notifications for action-required prompts and session com
│ │
│ Terminal Notification Method Auto │
│ How to send terminal notifications. │
│ Enable Notifications false │
│ Enable run-event notifications for action-required prompts and session completion.
│ │
│ Enable Plan Mode true │
│ Enable Plan Mode for read-only safety during planning. │
@@ -34,6 +31,9 @@ exports[`SettingsDialog > Initial Rendering > should render settings list with v
│ Plan Model Routing true │
│ Automatically switch between Pro and Flash models based on Plan Mode status. Uses Pr… │
│ │
│ Retry Fetch Errors true │
│ Retry on "exception TypeError: fetch failed sending request" errors. │
│ │
│ ▼ │
│ │
│ Apply To │
@@ -65,11 +65,8 @@ exports[`SettingsDialog > Snapshot Tests > should render 'accessibility settings
│ Enable Auto Update true │
│ Enable automatic updates. │
│ │
│ Enable Terminal Notifications false │
│ Enable terminal run-event notifications for action-required prompts and session com
│ │
│ Terminal Notification Method Auto │
│ How to send terminal notifications. │
│ Enable Notifications false │
│ Enable run-event notifications for action-required prompts and session completion.
│ │
│ Enable Plan Mode true │
│ Enable Plan Mode for read-only safety during planning. │
@@ -80,6 +77,9 @@ exports[`SettingsDialog > Snapshot Tests > should render 'accessibility settings
│ Plan Model Routing true │
│ Automatically switch between Pro and Flash models based on Plan Mode status. Uses Pr… │
│ │
│ Retry Fetch Errors true │
│ Retry on "exception TypeError: fetch failed sending request" errors. │
│ │
│ ▼ │
│ │
│ Apply To │
@@ -111,11 +111,8 @@ exports[`SettingsDialog > Snapshot Tests > should render 'all boolean settings d
│ Enable Auto Update true* │
│ Enable automatic updates. │
│ │
│ Enable Terminal Notifications false │
│ Enable terminal run-event notifications for action-required prompts and session com
│ │
│ Terminal Notification Method Auto │
│ How to send terminal notifications. │
│ Enable Notifications false │
│ Enable run-event notifications for action-required prompts and session completion.
│ │
│ Enable Plan Mode true │
│ Enable Plan Mode for read-only safety during planning. │
@@ -126,6 +123,9 @@ exports[`SettingsDialog > Snapshot Tests > should render 'all boolean settings d
│ Plan Model Routing true │
│ Automatically switch between Pro and Flash models based on Plan Mode status. Uses Pr… │
│ │
│ Retry Fetch Errors true │
│ Retry on "exception TypeError: fetch failed sending request" errors. │
│ │
│ ▼ │
│ │
│ Apply To │
@@ -157,11 +157,8 @@ exports[`SettingsDialog > Snapshot Tests > should render 'default state' correct
│ Enable Auto Update true │
│ Enable automatic updates. │
│ │
│ Enable Terminal Notifications false │
│ Enable terminal run-event notifications for action-required prompts and session com
│ │
│ Terminal Notification Method Auto │
│ How to send terminal notifications. │
│ Enable Notifications false │
│ Enable run-event notifications for action-required prompts and session completion.
│ │
│ Enable Plan Mode true │
│ Enable Plan Mode for read-only safety during planning. │
@@ -172,6 +169,9 @@ exports[`SettingsDialog > Snapshot Tests > should render 'default state' correct
│ Plan Model Routing true │
│ Automatically switch between Pro and Flash models based on Plan Mode status. Uses Pr… │
│ │
│ Retry Fetch Errors true │
│ Retry on "exception TypeError: fetch failed sending request" errors. │
│ │
│ ▼ │
│ │
│ Apply To │
@@ -203,11 +203,8 @@ exports[`SettingsDialog > Snapshot Tests > should render 'file filtering setting
│ Enable Auto Update true │
│ Enable automatic updates. │
│ │
│ Enable Terminal Notifications false │
│ Enable terminal run-event notifications for action-required prompts and session com
│ │
│ Terminal Notification Method Auto │
│ How to send terminal notifications. │
│ Enable Notifications false │
│ Enable run-event notifications for action-required prompts and session completion.
│ │
│ Enable Plan Mode true │
│ Enable Plan Mode for read-only safety during planning. │
@@ -218,6 +215,9 @@ exports[`SettingsDialog > Snapshot Tests > should render 'file filtering setting
│ Plan Model Routing true │
│ Automatically switch between Pro and Flash models based on Plan Mode status. Uses Pr… │
│ │
│ Retry Fetch Errors true │
│ Retry on "exception TypeError: fetch failed sending request" errors. │
│ │
│ ▼ │
│ │
│ Apply To │
@@ -249,11 +249,8 @@ exports[`SettingsDialog > Snapshot Tests > should render 'focused on scope selec
│ Enable Auto Update true │
│ Enable automatic updates. │
│ │
│ Enable Terminal Notifications false │
│ Enable terminal run-event notifications for action-required prompts and session com
│ │
│ Terminal Notification Method Auto │
│ How to send terminal notifications. │
│ Enable Notifications false │
│ Enable run-event notifications for action-required prompts and session completion.
│ │
│ Enable Plan Mode true │
│ Enable Plan Mode for read-only safety during planning. │
@@ -264,6 +261,9 @@ exports[`SettingsDialog > Snapshot Tests > should render 'focused on scope selec
│ Plan Model Routing true │
│ Automatically switch between Pro and Flash models based on Plan Mode status. Uses Pr… │
│ │
│ Retry Fetch Errors true │
│ Retry on "exception TypeError: fetch failed sending request" errors. │
│ │
│ ▼ │
│ │
│ > Apply To │
@@ -295,11 +295,8 @@ exports[`SettingsDialog > Snapshot Tests > should render 'mixed boolean and numb
│ Enable Auto Update false* │
│ Enable automatic updates. │
│ │
│ Enable Terminal Notifications false │
│ Enable terminal run-event notifications for action-required prompts and session com
│ │
│ Terminal Notification Method Auto │
│ How to send terminal notifications. │
│ Enable Notifications false │
│ Enable run-event notifications for action-required prompts and session completion.
│ │
│ Enable Plan Mode true │
│ Enable Plan Mode for read-only safety during planning. │
@@ -310,6 +307,9 @@ exports[`SettingsDialog > Snapshot Tests > should render 'mixed boolean and numb
│ Plan Model Routing true │
│ Automatically switch between Pro and Flash models based on Plan Mode status. Uses Pr… │
│ │
│ Retry Fetch Errors true │
│ Retry on "exception TypeError: fetch failed sending request" errors. │
│ │
│ ▼ │
│ │
│ Apply To │
@@ -341,11 +341,8 @@ exports[`SettingsDialog > Snapshot Tests > should render 'tools and security set
│ Enable Auto Update true │
│ Enable automatic updates. │
│ │
│ Enable Terminal Notifications false │
│ Enable terminal run-event notifications for action-required prompts and session com
│ │
│ Terminal Notification Method Auto │
│ How to send terminal notifications. │
│ Enable Notifications false │
│ Enable run-event notifications for action-required prompts and session completion.
│ │
│ Enable Plan Mode true │
│ Enable Plan Mode for read-only safety during planning. │
@@ -356,6 +353,9 @@ exports[`SettingsDialog > Snapshot Tests > should render 'tools and security set
│ Plan Model Routing true │
│ Automatically switch between Pro and Flash models based on Plan Mode status. Uses Pr… │
│ │
│ Retry Fetch Errors true │
│ Retry on "exception TypeError: fetch failed sending request" errors. │
│ │
│ ▼ │
│ │
│ Apply To │
@@ -387,11 +387,8 @@ exports[`SettingsDialog > Snapshot Tests > should render 'various boolean settin
│ Enable Auto Update false* │
│ Enable automatic updates. │
│ │
│ Enable Terminal Notifications false │
│ Enable terminal run-event notifications for action-required prompts and session com
│ │
│ Terminal Notification Method Auto │
│ How to send terminal notifications. │
│ Enable Notifications false │
│ Enable run-event notifications for action-required prompts and session completion.
│ │
│ Enable Plan Mode true │
│ Enable Plan Mode for read-only safety during planning. │
@@ -402,6 +399,9 @@ exports[`SettingsDialog > Snapshot Tests > should render 'various boolean settin
│ Plan Model Routing true │
│ Automatically switch between Pro and Flash models based on Plan Mode status. Uses Pr… │
│ │
│ Retry Fetch Errors true │
│ Retry on "exception TypeError: fetch failed sending request" errors. │
│ │
│ ▼ │
│ │
│ Apply To │
@@ -246,10 +246,11 @@ export const ToolGroupMessage: React.FC<ToolGroupMessageProps> = ({
(showClosingBorder ? 1 : 0);
} else if (isTopicToolCall) {
// Topic Message Spacing Breakdown:
// 1. Topic Content (1).
// 2. Bottom Margin (1): Always present around TopicMessage for breathing room.
// 3. Closing Border (1): Added if transition logic (showClosingBorder) requires it.
height += 1 + 1 + (showClosingBorder ? 1 : 0);
// 1. Top Margin (1): Always present for spacing.
// 2. Topic Content (1).
// 3. Bottom Margin (1): Always present around TopicMessage for breathing room.
// 4. Closing Border (1): Added if transition logic (showClosingBorder) requires it.
height += 1 + 1 + 1 + (showClosingBorder ? 1 : 0);
} else if (isCompact) {
// Compact Tool: Always renders as a single dense line.
height += 1;
@@ -438,7 +439,7 @@ export const ToolGroupMessage: React.FC<ToolGroupMessageProps> = ({
{isCompact ? (
<DenseToolMessage {...commonProps} />
) : isTopicToolCall ? (
<Box marginBottom={1}>
<Box marginTop={1} marginBottom={1}>
<TopicMessage {...commonProps} />
</Box>
) : isShellToolCall ? (
@@ -77,7 +77,8 @@ exports[`<ToolGroupMessage /> > Golden Snapshots > renders header when scrolled
`;
exports[`<ToolGroupMessage /> > Golden Snapshots > renders mixed tool calls including update_topic 1`] = `
" Testing Topic: This is the description
"
Testing Topic: This is the description
╭──────────────────────────────────────────────────────────────────────────╮
│ ✓ read_file Read a file │
@@ -142,7 +143,8 @@ exports[`<ToolGroupMessage /> > Golden Snapshots > renders two tool groups where
`;
exports[`<ToolGroupMessage /> > Golden Snapshots > renders update_topic tool call using TopicMessage > update_topic_tool 1`] = `
" Testing Topic: This is the description
"
Testing Topic: This is the description
"
`;
@@ -15,14 +15,12 @@ import { getPendingAttentionNotification } from '../utils/pendingAttentionNotifi
import {
buildRunEventNotificationContent,
notifyViaTerminal,
type TerminalNotificationMethod,
} from '../../utils/terminalNotifications.js';
const ATTENTION_NOTIFICATION_COOLDOWN_MS = 20_000;
interface RunEventNotificationParams {
notificationsEnabled: boolean;
notificationMethod: TerminalNotificationMethod;
isFocused: boolean;
hasReceivedFocusEvent: boolean;
streamingState: StreamingState;
@@ -38,7 +36,6 @@ interface RunEventNotificationParams {
export function useRunEventNotifications({
notificationsEnabled,
notificationMethod,
isFocused,
hasReceivedFocusEvent,
streamingState,
@@ -127,13 +124,11 @@ export function useRunEventNotifications({
void notifyViaTerminal(
notificationsEnabled,
buildRunEventNotificationContent(pendingAttentionNotification.event),
notificationMethod,
);
}, [
isFocused,
hasReceivedFocusEvent,
notificationsEnabled,
notificationMethod,
pendingAttentionNotification,
]);
@@ -164,14 +159,12 @@ export function useRunEventNotifications({
type: 'session_complete',
detail: 'Gemini CLI finished responding.',
}),
notificationMethod,
);
}, [
streamingState,
isFocused,
hasReceivedFocusEvent,
notificationsEnabled,
notificationMethod,
hasPendingActionRequired,
]);
}
@@ -365,123 +365,76 @@ describe('TerminalCapabilityManager', () => {
);
});
describe('isTmux', () => {
describe('supportsOsc9Notifications', () => {
const manager = TerminalCapabilityManager.getInstance();
it('returns true when TMUX is set', () => {
expect(manager.isTmux({ TMUX: '1' })).toBe(true);
expect(manager.isTmux({ TMUX: 'tmux-1234' })).toBe(true);
});
it('returns false when TMUX is not set', () => {
expect(manager.isTmux({})).toBe(false);
expect(manager.isTmux({ STY: '1' })).toBe(false);
});
});
describe('isScreen', () => {
const manager = TerminalCapabilityManager.getInstance();
it('returns true when STY is set', () => {
expect(manager.isScreen({ STY: '1' })).toBe(true);
expect(manager.isScreen({ STY: 'screen.1234' })).toBe(true);
});
it('returns false when STY is not set', () => {
expect(manager.isScreen({})).toBe(false);
expect(manager.isScreen({ TMUX: '1' })).toBe(false);
});
});
describe('isITerm2', () => {
const manager = TerminalCapabilityManager.getInstance();
it('returns true when iTerm is in terminal name', () => {
vi.spyOn(manager, 'getTerminalName').mockReturnValue('iTerm.app');
expect(manager.isITerm2({})).toBe(true);
});
it('returns true when TERM_PROGRAM is iTerm.app', () => {
vi.spyOn(manager, 'getTerminalName').mockReturnValue(undefined);
expect(manager.isITerm2({ TERM_PROGRAM: 'iTerm.app' })).toBe(true);
});
it('returns false otherwise', () => {
vi.spyOn(manager, 'getTerminalName').mockReturnValue('xterm');
expect(manager.isITerm2({ TERM_PROGRAM: 'Apple_Terminal' })).toBe(false);
});
});
describe('isAlacritty', () => {
const manager = TerminalCapabilityManager.getInstance();
it('returns true when ALACRITTY_WINDOW_ID is set', () => {
vi.spyOn(manager, 'getTerminalName').mockReturnValue(undefined);
expect(manager.isAlacritty({ ALACRITTY_WINDOW_ID: '123' })).toBe(true);
});
it('returns true when TERM is alacritty', () => {
vi.spyOn(manager, 'getTerminalName').mockReturnValue(undefined);
expect(manager.isAlacritty({ TERM: 'alacritty' })).toBe(true);
});
it('returns true when terminal name contains alacritty', () => {
vi.spyOn(manager, 'getTerminalName').mockReturnValue('alacritty');
expect(manager.isAlacritty({})).toBe(true);
});
it('returns false otherwise', () => {
vi.spyOn(manager, 'getTerminalName').mockReturnValue(undefined);
expect(manager.isAlacritty({ TERM: 'xterm' })).toBe(false);
});
});
describe('isAppleTerminal', () => {
const manager = TerminalCapabilityManager.getInstance();
it('returns true when apple_terminal is in terminal name', () => {
vi.spyOn(manager, 'getTerminalName').mockReturnValue('apple_terminal');
expect(manager.isAppleTerminal({})).toBe(true);
});
it('returns true when TERM_PROGRAM is Apple_Terminal', () => {
vi.spyOn(manager, 'getTerminalName').mockReturnValue(undefined);
expect(manager.isAppleTerminal({ TERM_PROGRAM: 'Apple_Terminal' })).toBe(
true,
);
});
it('returns false otherwise', () => {
vi.spyOn(manager, 'getTerminalName').mockReturnValue('xterm');
expect(manager.isAppleTerminal({ TERM_PROGRAM: 'iTerm.app' })).toBe(
false,
);
});
});
describe('isVSCodeTerminal', () => {
const manager = TerminalCapabilityManager.getInstance();
it('returns true when TERM_PROGRAM is vscode', () => {
expect(manager.isVSCodeTerminal({ TERM_PROGRAM: 'vscode' })).toBe(true);
});
it('returns false otherwise', () => {
expect(manager.isVSCodeTerminal({ TERM_PROGRAM: 'iTerm.app' })).toBe(
false,
);
});
});
describe('isWindowsTerminal', () => {
const manager = TerminalCapabilityManager.getInstance();
it('returns true when WT_SESSION is set', () => {
expect(manager.isWindowsTerminal({ WT_SESSION: 'some-guid' })).toBe(true);
});
it('returns false otherwise', () => {
expect(manager.isWindowsTerminal({})).toBe(false);
});
it.each([
{
name: 'WezTerm (terminal name)',
terminalName: 'WezTerm',
env: {},
expected: true,
},
{
name: 'iTerm.app (terminal name)',
terminalName: 'iTerm.app',
env: {},
expected: true,
},
{
name: 'ghostty (terminal name)',
terminalName: 'ghostty',
env: {},
expected: true,
},
{
name: 'kitty (terminal name)',
terminalName: 'kitty',
env: {},
expected: true,
},
{
name: 'some-other-term (terminal name)',
terminalName: 'some-other-term',
env: {},
expected: false,
},
{
name: 'iTerm.app (TERM_PROGRAM)',
terminalName: undefined,
env: { TERM_PROGRAM: 'iTerm.app' },
expected: true,
},
{
name: 'vscode (TERM_PROGRAM)',
terminalName: undefined,
env: { TERM_PROGRAM: 'vscode' },
expected: false,
},
{
name: 'xterm-kitty (TERM)',
terminalName: undefined,
env: { TERM: 'xterm-kitty' },
expected: true,
},
{
name: 'xterm-256color (TERM)',
terminalName: undefined,
env: { TERM: 'xterm-256color' },
expected: false,
},
{
name: 'Windows Terminal (WT_SESSION)',
terminalName: 'iTerm.app',
env: { WT_SESSION: 'some-guid' },
expected: false,
},
])(
'should return $expected for $name',
({ terminalName, env, expected }) => {
vi.spyOn(manager, 'getTerminalName').mockReturnValue(terminalName);
expect(manager.supportsOsc9Notifications(env)).toBe(expected);
},
);
});
});
@@ -284,43 +284,31 @@ export class TerminalCapabilityManager {
);
}
isTmux(env: NodeJS.ProcessEnv = process.env): boolean {
return !!env['TMUX'];
}
supportsOsc9Notifications(env: NodeJS.ProcessEnv = process.env): boolean {
if (env['WT_SESSION']) {
return false;
}
isScreen(env: NodeJS.ProcessEnv = process.env): boolean {
return !!env['STY'];
}
isITerm2(env: NodeJS.ProcessEnv = process.env): boolean {
return !!(
this.getTerminalName()?.toLowerCase().includes('iterm') ||
env['TERM_PROGRAM']?.toLowerCase().includes('iterm')
return (
this.hasOsc9TerminalSignature(this.getTerminalName()) ||
this.hasOsc9TerminalSignature(env['TERM_PROGRAM']) ||
this.hasOsc9TerminalSignature(env['TERM'])
);
}
isAlacritty(env: NodeJS.ProcessEnv = process.env): boolean {
return !!(
this.getTerminalName()?.toLowerCase().includes('alacritty') ||
env['ALACRITTY_WINDOW_ID'] ||
env['TERM']?.toLowerCase().includes('alacritty')
private hasOsc9TerminalSignature(value: string | undefined): boolean {
if (!value) {
return false;
}
const normalized = value.toLowerCase();
return (
normalized.includes('wezterm') ||
normalized.includes('ghostty') ||
normalized.includes('iterm') ||
normalized.includes('kitty')
);
}
isAppleTerminal(env: NodeJS.ProcessEnv = process.env): boolean {
return !!(
this.getTerminalName()?.toLowerCase().includes('apple_terminal') ||
env['TERM_PROGRAM']?.toLowerCase().includes('apple_terminal')
);
}
isVSCodeTerminal(env: NodeJS.ProcessEnv = process.env): boolean {
return !!env['TERM_PROGRAM']?.toLowerCase().includes('vscode');
}
isWindowsTerminal(env: NodeJS.ProcessEnv = process.env): boolean {
return !!env['WT_SESSION'];
}
}
export const terminalCapabilityManager =
+1 -106
View File
@@ -8,13 +8,8 @@ import { vi, describe, it, expect, beforeEach, afterEach } from 'vitest';
import { spawn, exec, execFile, execSync } from 'node:child_process';
import os from 'node:os';
import fs from 'node:fs';
import path from 'node:path';
import { start_sandbox } from './sandbox.js';
import {
FatalSandboxError,
homedir,
type SandboxConfig,
} from '@google/gemini-cli-core';
import { FatalSandboxError, type SandboxConfig } from '@google/gemini-cli-core';
import { createMockSandboxConfig } from '@google/gemini-cli-test-utils';
import { EventEmitter } from 'node:events';
@@ -138,7 +133,6 @@ describe('sandbox', () => {
afterEach(() => {
process.env = originalEnv;
process.argv = originalArgv;
vi.unstubAllEnvs();
});
describe('start_sandbox', () => {
@@ -177,105 +171,6 @@ describe('sandbox', () => {
);
});
it('should resolve custom seatbelt profile from user home directory', async () => {
vi.mocked(os.platform).mockReturnValue('darwin');
vi.stubEnv('SEATBELT_PROFILE', 'custom-test');
vi.mocked(fs.existsSync).mockImplementation((p) =>
String(p).includes(
path.join(homedir(), '.gemini', 'sandbox-macos-custom-test.sb'),
),
);
const config: SandboxConfig = createMockSandboxConfig({
command: 'sandbox-exec',
image: 'some-image',
});
interface MockProcess extends EventEmitter {
stdout: EventEmitter;
stderr: EventEmitter;
}
const mockSpawnProcess = new EventEmitter() as MockProcess;
mockSpawnProcess.stdout = new EventEmitter();
mockSpawnProcess.stderr = new EventEmitter();
vi.mocked(spawn).mockReturnValue(
mockSpawnProcess as unknown as ReturnType<typeof spawn>,
);
const promise = start_sandbox(config, [], undefined, ['arg1']);
setTimeout(() => {
mockSpawnProcess.emit('close', 0);
}, 10);
await expect(promise).resolves.toBe(0);
expect(spawn).toHaveBeenCalledWith(
'sandbox-exec',
expect.any(Array),
expect.objectContaining({ stdio: 'inherit' }),
);
const spawnArgs = vi.mocked(spawn).mock.calls[0]?.[1];
expect(spawnArgs).toEqual(
expect.arrayContaining(['-f', expect.any(String)]),
);
const profileArg = spawnArgs?.[spawnArgs.indexOf('-f') + 1];
expect(profileArg).toEqual(
expect.stringContaining(
path.join(homedir(), '.gemini', 'sandbox-macos-custom-test.sb'),
),
);
});
it('should fall back to project .gemini directory when user profile is missing', async () => {
vi.mocked(os.platform).mockReturnValue('darwin');
vi.stubEnv('SEATBELT_PROFILE', 'custom-test');
vi.mocked(fs.existsSync).mockImplementation((p) => {
const s = String(p);
return (
s.includes(path.join('.gemini', 'sandbox-macos-custom-test.sb')) &&
!s.includes(path.join(homedir(), '.gemini'))
);
});
const config: SandboxConfig = createMockSandboxConfig({
command: 'sandbox-exec',
image: 'some-image',
});
interface MockProcess extends EventEmitter {
stdout: EventEmitter;
stderr: EventEmitter;
}
const mockSpawnProcess = new EventEmitter() as MockProcess;
mockSpawnProcess.stdout = new EventEmitter();
mockSpawnProcess.stderr = new EventEmitter();
vi.mocked(spawn).mockReturnValue(
mockSpawnProcess as unknown as ReturnType<typeof spawn>,
);
const promise = start_sandbox(config, [], undefined, ['arg1']);
setTimeout(() => {
mockSpawnProcess.emit('close', 0);
}, 10);
await expect(promise).resolves.toBe(0);
expect(spawn).toHaveBeenCalledWith(
'sandbox-exec',
expect.any(Array),
expect.objectContaining({ stdio: 'inherit' }),
);
const spawnArgs = vi.mocked(spawn).mock.calls[0]?.[1];
expect(spawnArgs).toEqual(
expect.arrayContaining(['-f', expect.any(String)]),
);
const profileArg = spawnArgs?.[spawnArgs.indexOf('-f') + 1];
expect(profileArg).toEqual(
expect.stringContaining(
path.join('.gemini', 'sandbox-macos-custom-test.sb'),
),
);
expect(profileArg).not.toContain(homedir());
});
it('should throw FatalSandboxError if seatbelt profile is missing', async () => {
vi.mocked(os.platform).mockReturnValue('darwin');
vi.mocked(fs.existsSync).mockReturnValue(false);
+2 -10
View File
@@ -68,17 +68,9 @@ export async function start_sandbox(
let profileFile = fileURLToPath(
new URL(`sandbox-macos-${profile}.sb`, import.meta.url),
);
// if profile name is not recognized, look in user-level ~/.gemini first,
// then fall back to project-level .gemini. path.basename() strips any
// directory separators to prevent path traversal via SEATBELT_PROFILE.
// if profile name is not recognized, then look for file under project settings directory
if (!BUILTIN_SEATBELT_PROFILES.includes(profile)) {
const safeProfile = path.basename(profile);
const fileName = `sandbox-macos-${safeProfile}.sb`;
const userProfileFile = path.join(homedir(), GEMINI_DIR, fileName);
const projectProfileFile = path.join(GEMINI_DIR, fileName);
profileFile = fs.existsSync(userProfileFile)
? userProfileFile
: projectProfileFile;
profileFile = path.join(GEMINI_DIR, `sandbox-macos-${profile}.sb`);
}
if (!fs.existsSync(profileFile)) {
throw new FatalSandboxError(
@@ -11,7 +11,6 @@ import {
MAX_NOTIFICATION_SUBTITLE_CHARS,
MAX_NOTIFICATION_TITLE_CHARS,
notifyViaTerminal,
TerminalNotificationMethod,
} from './terminalNotifications.js';
const writeToStdout = vi.hoisted(() => vi.fn());
@@ -25,19 +24,38 @@ vi.mock('@google/gemini-cli-core', () => ({
}));
describe('terminal notifications', () => {
const originalPlatform = process.platform;
beforeEach(() => {
vi.resetAllMocks();
vi.unstubAllEnvs();
vi.stubEnv('TMUX', '');
vi.stubEnv('STY', '');
vi.stubEnv('WT_SESSION', '');
vi.stubEnv('TERM_PROGRAM', '');
vi.stubEnv('TERM', '');
vi.stubEnv('ALACRITTY_WINDOW_ID', '');
Object.defineProperty(process, 'platform', {
value: 'darwin',
configurable: true,
});
});
afterEach(() => {
vi.unstubAllEnvs();
Object.defineProperty(process, 'platform', {
value: originalPlatform,
configurable: true,
});
});
it('emits notification on non-macOS platforms', async () => {
Object.defineProperty(process, 'platform', {
value: 'linux',
configurable: true,
});
const shown = await notifyViaTerminal(true, {
title: 't',
body: 'b',
});
expect(shown).toBe(true);
expect(writeToStdout).toHaveBeenCalled();
});
it('returns false without writing when disabled', async () => {
@@ -50,7 +68,8 @@ describe('terminal notifications', () => {
expect(writeToStdout).not.toHaveBeenCalled();
});
it('emits OSC 9 notification when iTerm2 is detected', async () => {
it('emits OSC 9 notification when supported terminal is detected', async () => {
vi.stubEnv('WT_SESSION', '');
vi.stubEnv('TERM_PROGRAM', 'iTerm.app');
const shown = await notifyViaTerminal(true, {
@@ -66,7 +85,10 @@ describe('terminal notifications', () => {
expect(emitted.endsWith('\x07')).toBe(true);
});
it('emits OSC 777 for unknown terminals', async () => {
it('emits BEL fallback when OSC 9 is not supported', async () => {
vi.stubEnv('TERM_PROGRAM', '');
vi.stubEnv('TERM', '');
const shown = await notifyViaTerminal(true, {
title: 'Title',
subtitle: 'Subtitle',
@@ -74,49 +96,12 @@ describe('terminal notifications', () => {
});
expect(shown).toBe(true);
expect(writeToStdout).toHaveBeenCalledTimes(1);
const emitted = String(writeToStdout.mock.calls[0][0]);
expect(emitted.startsWith('\x1b]777;notify;')).toBe(true);
expect(writeToStdout).toHaveBeenCalledWith('\x07');
});
it('uses BEL when Windows Terminal is detected', async () => {
it('uses BEL fallback when WT_SESSION is set', async () => {
vi.stubEnv('WT_SESSION', '1');
const shown = await notifyViaTerminal(true, {
title: 'Title',
body: 'Body',
});
expect(shown).toBe(true);
expect(writeToStdout).toHaveBeenCalledWith('\x07');
});
it('uses BEL when Alacritty is detected', async () => {
vi.stubEnv('ALACRITTY_WINDOW_ID', '1');
const shown = await notifyViaTerminal(true, {
title: 'Title',
body: 'Body',
});
expect(shown).toBe(true);
expect(writeToStdout).toHaveBeenCalledWith('\x07');
});
it('uses BEL when Apple Terminal is detected', async () => {
vi.stubEnv('TERM_PROGRAM', 'Apple_Terminal');
const shown = await notifyViaTerminal(true, {
title: 'Title',
body: 'Body',
});
expect(shown).toBe(true);
expect(writeToStdout).toHaveBeenCalledWith('\x07');
});
it('uses BEL when VSCode Terminal is detected', async () => {
vi.stubEnv('TERM_PROGRAM', 'vscode');
vi.stubEnv('TERM_PROGRAM', 'WezTerm');
const shown = await notifyViaTerminal(true, {
title: 'Title',
@@ -142,6 +127,7 @@ describe('terminal notifications', () => {
});
it('strips terminal control sequences and newlines from payload text', async () => {
vi.stubEnv('WT_SESSION', '');
vi.stubEnv('TERM_PROGRAM', 'iTerm.app');
const shown = await notifyViaTerminal(true, {
@@ -176,124 +162,4 @@ describe('terminal notifications', () => {
MAX_NOTIFICATION_BODY_CHARS,
);
});
it('emits OSC 9 notification when method is explicitly set to osc9', async () => {
// Explicitly set terminal to something that would normally use BEL
vi.stubEnv('WT_SESSION', '1');
const shown = await notifyViaTerminal(
true,
{
title: 'Explicit OSC 9',
body: 'Body',
},
TerminalNotificationMethod.Osc9,
);
expect(shown).toBe(true);
expect(writeToStdout).toHaveBeenCalledTimes(1);
const emitted = String(writeToStdout.mock.calls[0][0]);
expect(emitted.startsWith('\x1b]9;')).toBe(true);
expect(emitted.endsWith('\x07')).toBe(true);
expect(emitted).toContain('Explicit OSC 9');
});
it('emits OSC 777 notification when method is explicitly set to osc777', async () => {
// Explicitly set terminal to something that would normally use BEL
vi.stubEnv('WT_SESSION', '1');
const shown = await notifyViaTerminal(
true,
{
title: 'Explicit OSC 777',
body: 'Body',
},
TerminalNotificationMethod.Osc777,
);
expect(shown).toBe(true);
expect(writeToStdout).toHaveBeenCalledTimes(1);
const emitted = String(writeToStdout.mock.calls[0][0]);
expect(emitted.startsWith('\x1b]777;notify;')).toBe(true);
expect(emitted.endsWith('\x07')).toBe(true);
expect(emitted).toContain('Explicit OSC 777');
});
it('emits BEL notification when method is explicitly set to bell', async () => {
// Explicitly set terminal to something that supports OSC 9
vi.stubEnv('TERM_PROGRAM', 'iTerm.app');
const shown = await notifyViaTerminal(
true,
{
title: 'Explicit BEL',
body: 'Body',
},
TerminalNotificationMethod.Bell,
);
expect(shown).toBe(true);
expect(writeToStdout).toHaveBeenCalledTimes(1);
expect(writeToStdout).toHaveBeenCalledWith('\x07');
});
it('replaces semicolons with colons in OSC 777 to avoid breaking the sequence', async () => {
const shown = await notifyViaTerminal(
true,
{
title: 'Title; with; semicolons',
subtitle: 'Sub;title',
body: 'Body; with; semicolons',
},
TerminalNotificationMethod.Osc777,
);
expect(shown).toBe(true);
const emitted = String(writeToStdout.mock.calls[0][0]);
// Format: \x1b]777;notify;title;body\x07
expect(emitted).toContain('Title: with: semicolons');
expect(emitted).toContain('Sub:title');
expect(emitted).toContain('Body: with: semicolons');
expect(emitted).not.toContain('Title; with; semicolons');
expect(emitted).not.toContain('Body; with; semicolons');
// Extract everything after '\x1b]777;notify;' and before '\x07'
const payload = emitted.slice('\x1b]777;notify;'.length, -1);
// There should be exactly one semicolon separating title and body
const semicolonsCount = (payload.match(/;/g) || []).length;
expect(semicolonsCount).toBe(1);
});
it('wraps OSC sequence in tmux passthrough when TMUX env var is set', async () => {
vi.stubEnv('TMUX', '1');
vi.stubEnv('TERM_PROGRAM', 'iTerm.app');
const shown = await notifyViaTerminal(true, {
title: 'Title',
body: 'Body',
});
expect(shown).toBe(true);
expect(writeToStdout).toHaveBeenCalledTimes(1);
const emitted = String(writeToStdout.mock.calls[0][0]);
expect(emitted.startsWith('\x1bPtmux;\x1b\x1b]9;')).toBe(true);
expect(emitted.endsWith('\x1b\\')).toBe(true);
});
it('wraps OSC sequence in GNU screen passthrough when STY env var is set', async () => {
vi.stubEnv('STY', '1');
vi.stubEnv('TERM_PROGRAM', 'iTerm.app');
const shown = await notifyViaTerminal(true, {
title: 'Title',
body: 'Body',
});
expect(shown).toBe(true);
expect(writeToStdout).toHaveBeenCalledTimes(1);
const emitted = String(writeToStdout.mock.calls[0][0]);
expect(emitted.startsWith('\x1bP\x1b]9;')).toBe(true);
expect(emitted.endsWith('\x1b\\')).toBe(true);
});
});
+21 -81
View File
@@ -15,8 +15,12 @@ export const MAX_NOTIFICATION_BODY_CHARS = 180;
const BEL = '\x07';
const OSC9_PREFIX = '\x1b]9;';
const OSC777_PREFIX = '\x1b]777;notify;';
const OSC_TEXT_SEPARATOR = ' | ';
const OSC9_SEPARATOR = ' | ';
const MAX_OSC9_MESSAGE_CHARS =
MAX_NOTIFICATION_TITLE_CHARS +
MAX_NOTIFICATION_SUBTITLE_CHARS +
MAX_NOTIFICATION_BODY_CHARS +
OSC9_SEPARATOR.length * 2;
export interface RunEventNotificationContent {
title: string;
@@ -77,100 +81,36 @@ export function isNotificationsEnabled(settings: LoadedSettings): boolean {
return general?.enableNotifications === true;
}
export enum TerminalNotificationMethod {
Auto = 'auto',
Osc9 = 'osc9',
Osc777 = 'osc777',
Bell = 'bell',
}
export function getNotificationMethod(
settings: LoadedSettings,
): TerminalNotificationMethod {
switch (settings.merged.general?.notificationMethod) {
case TerminalNotificationMethod.Osc9:
return TerminalNotificationMethod.Osc9;
case TerminalNotificationMethod.Osc777:
return TerminalNotificationMethod.Osc777;
case TerminalNotificationMethod.Bell:
return TerminalNotificationMethod.Bell;
default:
return TerminalNotificationMethod.Auto;
}
}
function wrapWithPassthrough(sequence: string): string {
const capabilityManager = TerminalCapabilityManager.getInstance();
if (capabilityManager.isTmux()) {
// eslint-disable-next-line no-control-regex
return `\x1bPtmux;${sequence.replace(/\x1b/g, '\x1b\x1b')}\x1b\\`;
} else if (capabilityManager.isScreen()) {
return `\x1bP${sequence}\x1b\\`;
}
return sequence;
function buildTerminalNotificationMessage(
content: RunEventNotificationContent,
): string {
const pieces = [content.title, content.subtitle, content.body].filter(
Boolean,
);
const combined = pieces.join(OSC9_SEPARATOR);
return sanitizeForDisplay(combined, MAX_OSC9_MESSAGE_CHARS);
}
function emitOsc9Notification(content: RunEventNotificationContent): void {
const sanitized = sanitizeNotificationContent(content);
const pieces = [sanitized.title, sanitized.subtitle, sanitized.body].filter(
Boolean,
);
const combined = pieces.join(OSC_TEXT_SEPARATOR);
const message = buildTerminalNotificationMessage(content);
if (!TerminalCapabilityManager.getInstance().supportsOsc9Notifications()) {
writeToStdout(BEL);
return;
}
writeToStdout(wrapWithPassthrough(`${OSC9_PREFIX}${combined}${BEL}`));
}
function emitOsc777Notification(content: RunEventNotificationContent): void {
const sanitized = sanitizeNotificationContent(content);
const bodyParts = [sanitized.subtitle, sanitized.body].filter(Boolean);
const body = bodyParts.join(OSC_TEXT_SEPARATOR);
// Replace ';' with ':' to avoid breaking the OSC 777 sequence
const safeTitle = sanitized.title.replace(/;/g, ':');
const safeBody = body.replace(/;/g, ':');
writeToStdout(
wrapWithPassthrough(`${OSC777_PREFIX}${safeTitle};${safeBody}${BEL}`),
);
}
function emitBellNotification(): void {
writeToStdout(BEL);
writeToStdout(`${OSC9_PREFIX}${message}${BEL}`);
}
export async function notifyViaTerminal(
notificationsEnabled: boolean,
content: RunEventNotificationContent,
method: TerminalNotificationMethod = TerminalNotificationMethod.Auto,
): Promise<boolean> {
if (!notificationsEnabled) {
return false;
}
try {
if (method === TerminalNotificationMethod.Osc9) {
emitOsc9Notification(content);
} else if (method === TerminalNotificationMethod.Osc777) {
emitOsc777Notification(content);
} else if (method === TerminalNotificationMethod.Bell) {
emitBellNotification();
} else {
// auto
const capabilityManager = TerminalCapabilityManager.getInstance();
if (capabilityManager.isITerm2()) {
emitOsc9Notification(content);
} else if (
capabilityManager.isAlacritty() ||
capabilityManager.isAppleTerminal() ||
capabilityManager.isVSCodeTerminal() ||
capabilityManager.isWindowsTerminal()
) {
emitBellNotification();
} else {
emitOsc777Notification(content);
}
}
emitOsc9Notification(sanitizeNotificationContent(content));
return true;
} catch (error) {
debugLogger.debug('Failed to emit terminal notification:', error);
+1 -2
View File
@@ -55,9 +55,8 @@ export const GeneralistAgent = (
return {
systemPrompt: getCoreSystemPrompt(
context.config,
/*userMemory=*/ undefined,
/*useMemory=*/ undefined,
/*interactiveOverride=*/ false,
/*topicUpdateNarrationOverride=*/ false,
),
query: '${request}',
};
@@ -82,7 +82,6 @@ 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. */
@@ -190,10 +189,6 @@ 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);
-154
View File
@@ -349,27 +349,6 @@ 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(() => {
@@ -1795,95 +1774,6 @@ describe('Server Config (config.ts)', () => {
expect(config1.topicState.getTopic()).toBe('Topic 1');
expect(config2.topicState.getTopic()).toBe('Topic 2');
});
it('updates storage session-scoped directories when the sessionId changes', async () => {
const config = new Config({
...baseParams,
sessionId: 'session-one',
plan: true,
});
await config.initialize();
const tempDir = config.storage.getProjectTempDir();
const oldPlansDir = path.join(tempDir, 'session-one', 'plans');
const oldTrackerService = config.getTrackerService();
config.setSessionId('session-two');
expect(config.getSessionId()).toBe('session-two');
expect(config.storage.getProjectTempPlansDir()).toBe(
path.join(tempDir, 'session-two', 'plans'),
);
expect(config.storage.getProjectTempTrackerDir()).toBe(
path.join(tempDir, 'session-two', 'tracker'),
);
expect(config.getTrackerService()).not.toBe(oldTrackerService);
expect(config.getTrackerService().trackerDir).toBe(
path.join(tempDir, 'session-two', 'tracker'),
);
expect(config.getWorkspaceContext().getDirectories()).not.toContain(
oldPlansDir,
);
});
it('does not throw when changing sessions before the previous plans dir exists', async () => {
const config = new Config({
...baseParams,
sessionId: 'session-one',
plan: true,
});
await config.initialize();
const missingPlansDir = config.storage.getProjectTempPlansDir();
const realpathMock = vi.mocked(fs.realpathSync);
const originalImplementation = realpathMock.getMockImplementation();
try {
realpathMock.mockImplementation((input) => {
const normalizedInput =
typeof input === 'string' || Buffer.isBuffer(input)
? input
: input.toString();
if (normalizedInput === missingPlansDir) {
const error = new Error(
`ENOENT: no such file or directory, ${normalizedInput}`,
);
Object.assign(error, { code: 'ENOENT' });
throw error;
}
if (originalImplementation) {
return originalImplementation(input);
}
return normalizedInput;
});
expect(() => config.setSessionId('session-two')).not.toThrow();
} finally {
realpathMock.mockImplementation((input) => {
if (originalImplementation) {
return originalImplementation(input);
}
return typeof input === 'string' || Buffer.isBuffer(input)
? input
: input.toString();
});
}
});
it('clears the approved plan when starting a new session', () => {
const config = new Config({
...baseParams,
sessionId: 'session-one',
});
config.setApprovedPlanPath('/tmp/session-one/plans/approved.md');
expect(() => config.resetNewSessionState('session-two')).not.toThrow();
expect(config.getSessionId()).toBe('session-two');
expect(config.getApprovedPlanPath()).toBeUndefined();
});
});
describe('GemmaModelRouterSettings', () => {
@@ -3443,50 +3333,6 @@ 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({
+13 -77
View File
@@ -30,8 +30,6 @@ import { ResourceRegistry } from '../resources/resource-registry.js';
import { ToolRegistry } from '../tools/tool-registry.js';
import { LSTool } from '../tools/ls.js';
import { ReadFileTool } from '../tools/read-file.js';
import { ReadMcpResourceTool } from '../tools/read-mcp-resource.js';
import { ListMcpResourcesTool } from '../tools/list-mcp-resources.js';
import { GrepTool } from '../tools/grep.js';
import { canUseRipgrep, RipGrepTool } from '../tools/ripGrep.js';
import { GlobTool } from '../tools/glob.js';
@@ -701,7 +699,6 @@ export interface ConfigParameters {
experimentalJitContext?: boolean;
autoDistillation?: boolean;
experimentalMemoryManager?: boolean;
experimentalAutoMemory?: boolean;
experimentalContextManagementConfig?: string;
experimentalAgentHistoryTruncation?: boolean;
experimentalAgentHistoryTruncationThreshold?: number;
@@ -943,7 +940,6 @@ 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;
@@ -1156,7 +1152,6 @@ 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'];
@@ -1199,7 +1194,7 @@ export class Config implements McpContext, AgentLoopContext {
},
},
};
this.topicUpdateNarration = params.topicUpdateNarration ?? true;
this.topicUpdateNarration = params.topicUpdateNarration ?? false;
this.modelSteering = params.modelSteering ?? false;
this.injectionService = new InjectionService(() =>
this.isModelSteeringEnabled(),
@@ -1765,22 +1760,7 @@ export class Config implements McpContext, AgentLoopContext {
}
setSessionId(sessionId: string): void {
const previousPlansDir = this.storage.isInitialized()
? this.storage.getPlansDir()
: undefined;
this._sessionId = sessionId;
this.storage.setSessionId(sessionId);
this.trackerService = undefined;
if (previousPlansDir) {
this.refreshSessionScopedPlansDirectory(previousPlansDir);
}
}
resetNewSessionState(sessionId: string): void {
this.setSessionId(sessionId);
this.approvedPlanPath = undefined;
}
setTerminalBackground(terminalBackground: string | undefined): void {
@@ -2069,37 +2049,6 @@ export class Config implements McpContext, AgentLoopContext {
return getWorkspaceContextOverride() ?? this.workspaceContext;
}
private refreshSessionScopedPlansDirectory(previousPlansDir: string): void {
const nextPlansDir = this.storage.getPlansDir();
if (previousPlansDir === nextPlansDir) {
return;
}
const pathsToRemove = new Set([previousPlansDir]);
try {
pathsToRemove.add(resolveToRealPath(previousPlansDir));
} catch {
// The previous session's plans directory may never have been created.
// In that case there is nothing to resolve or remove beyond the raw path.
}
const currentDirectories = this.workspaceContext
.getDirectories()
.filter((dir) => !pathsToRemove.has(dir));
this.workspaceContext.setDirectories(currentDirectories);
try {
if (fs.existsSync(nextPlansDir)) {
this.workspaceContext.addDirectory(nextPlansDir);
}
} catch {
// Ignore invalid or unreadable plans directories here. This mirrors
// initialization behavior, which only adds the plans directory when it
// already exists and is readable.
}
}
getAgentRegistry(): AgentRegistry {
return this.agentRegistry;
}
@@ -2489,10 +2438,6 @@ export class Config implements McpContext, AgentLoopContext {
return this.experimentalMemoryManager;
}
isAutoMemoryEnabled(): boolean {
return this.experimentalAutoMemory;
}
getExperimentalContextManagementConfig(): string | undefined {
return this.experimentalContextManagementConfig;
}
@@ -3403,23 +3348,20 @@ export class Config implements McpContext, AgentLoopContext {
return this.shellExecutionConfig;
}
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.
setShellExecutionConfig(config: ShellExecutionConfig): void {
this.shellExecutionConfig = {
...this.shellExecutionConfig,
...definedConfig,
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,
};
}
getScreenReader(): boolean {
@@ -3637,12 +3579,6 @@ export class Config implements McpContext, AgentLoopContext {
maybeRegister(WebFetchTool, () =>
registry.registerTool(new WebFetchTool(this, this.messageBus)),
);
maybeRegister(ReadMcpResourceTool, () =>
registry.registerTool(new ReadMcpResourceTool(this, this.messageBus)),
);
maybeRegister(ListMcpResourcesTool, () =>
registry.registerTool(new ListMcpResourcesTool(this, this.messageBus)),
);
maybeRegister(ShellTool, () =>
registry.registerTool(new ShellTool(this, this.messageBus)),
);
@@ -300,78 +300,4 @@ 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();
});
});
+35 -122
View File
@@ -10,7 +10,6 @@ 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>;
@@ -55,27 +54,18 @@ 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 (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;
} 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: {} };
}
}
@@ -92,54 +82,18 @@ 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');
// 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));
}
}
await fs.promises.rename(tmpPath, this.registryPath);
} 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
}
}
}
}
@@ -159,18 +113,9 @@ 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.
// If it doesn't exist, we try to create it. If someone else creates it
// between our check and our write, we just continue.
// Ensure the registry file exists so proper-lockfile can lock it
if (!fs.existsSync(this.registryPath)) {
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.
}
await this.save({ projects: {} });
}
// Use proper-lockfile to prevent racy updates
@@ -212,13 +157,7 @@ export class ProjectRegistry {
await this.save(currentData);
return shortId;
} finally {
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);
}
await release();
}
}
@@ -232,19 +171,20 @@ export class ProjectRegistry {
for (const baseDir of this.baseDirs) {
const markerPath = path.join(baseDir, slug, PROJECT_ROOT_FILE);
try {
const owner = (await fs.promises.readFile(markerPath, 'utf8')).trim();
if (this.normalizePath(owner) !== this.normalizePath(projectPath)) {
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.
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;
@@ -336,22 +276,10 @@ export class ProjectRegistry {
try {
await this.ensureOwnershipMarkers(candidate, projectPath);
return candidate;
} 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;
} catch {
// Someone might have claimed it between our check and our write.
// Try next candidate.
continue;
}
}
}
@@ -373,28 +301,13 @@ export class ProjectRegistry {
continue;
}
// Collision!
const error = Object.assign(
new Error(`Slug ${slug} is already owned by ${owner}`),
{ code: 'EEXIST' },
);
throw error;
throw new Error(`Slug ${slug} is already owned by ${owner}`);
}
// Use flag: 'wx' to ensure atomic creation
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;
}
await fs.promises.writeFile(markerPath, normalizedProject, {
encoding: 'utf8',
flag: 'wx',
});
}
}
-21
View File
@@ -211,27 +211,6 @@ describe('Storage additional helpers', () => {
expect(storageWithSession.getProjectTempTrackerDir()).toBe(expected);
});
it('updates session-scoped directories when the sessionId changes', async () => {
const storageWithSession = new Storage(projectRoot, 'session-one');
ProjectRegistry.prototype.getShortId = vi
.fn()
.mockReturnValue(PROJECT_SLUG);
await storageWithSession.initialize();
const tempDir = storageWithSession.getProjectTempDir();
storageWithSession.setSessionId('session-two');
expect(storageWithSession.getProjectTempPlansDir()).toBe(
path.join(tempDir, 'session-two', 'plans'),
);
expect(storageWithSession.getProjectTempTrackerDir()).toBe(
path.join(tempDir, 'session-two', 'tracker'),
);
expect(storageWithSession.getProjectTempTasksDir()).toBe(
path.join(tempDir, 'session-two', 'tasks'),
);
});
describe('Session and JSON Loading', () => {
beforeEach(async () => {
await storage.initialize();
+1 -9
View File
@@ -28,7 +28,7 @@ export const AUTO_SAVED_POLICY_FILENAME = 'auto-saved.toml';
export class Storage {
private readonly targetDir: string;
private sessionId: string | undefined;
private readonly sessionId: string | undefined;
private projectIdentifier: string | undefined;
private initPromise: Promise<void> | undefined;
private customPlansDir: string | undefined;
@@ -42,14 +42,6 @@ export class Storage {
this.customPlansDir = dir;
}
setSessionId(sessionId: string | undefined): void {
this.sessionId = sessionId;
}
isInitialized(): boolean {
return !!this.projectIdentifier;
}
static getGlobalGeminiDir(): string {
const homeDir = homedir();
if (!homeDir) {
-2
View File
@@ -24,13 +24,11 @@ export function getCoreSystemPrompt(
config: Config,
userMemory?: string | HierarchicalMemory,
interactiveOverride?: boolean,
topicUpdateNarrationOverride?: boolean,
): string {
return new PromptProvider().getCoreSystemPrompt(
config,
userMemory,
interactiveOverride,
topicUpdateNarrationOverride,
);
}
@@ -47,10 +47,7 @@ toolName = [
# Topic grouping tool is innocuous and used for UI organization.
"update_topic",
# Core agent lifecycle tool
"complete_task",
# MCP resource tools
"read_mcp_resource",
"list_mcp_resources"
"complete_task"
]
decision = "allow"
priority = 50
@@ -1057,25 +1057,6 @@ priority = 100
cliHelpResult.decision,
'cli_help should be ALLOWED in Plan Mode',
).toBe(PolicyDecision.ALLOW);
// 7. Verify MCP resource tools are ALLOWED
const listMcpResult = await engine.check(
{ name: 'list_mcp_resources' },
undefined,
);
expect(
listMcpResult.decision,
'list_mcp_resources should be ALLOWED in Plan Mode',
).toBe(PolicyDecision.ALLOW);
const readMcpResult = await engine.check(
{ name: 'read_mcp_resource', args: { uri: 'test://resource' } },
undefined,
);
expect(
readMcpResult.decision,
'read_mcp_resource should be ALLOWED in Plan Mode',
).toBe(PolicyDecision.ALLOW);
} finally {
await fs.rm(tempPolicyDir, { recursive: true, force: true });
}
@@ -277,56 +277,6 @@ 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();
+6 -9
View File
@@ -44,7 +44,6 @@ export class PromptProvider {
context: AgentLoopContext,
userMemory?: string | HierarchicalMemory,
interactiveOverride?: boolean,
topicUpdateNarrationOverride?: boolean,
): string {
const systemMdResolution = resolvePathFromEnv(
process.env['GEMINI_SYSTEM_MD'],
@@ -58,10 +57,6 @@ 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();
@@ -144,7 +139,7 @@ export class PromptProvider {
hasSkills: skills.length > 0,
hasHierarchicalMemory,
contextFilenames,
topicUpdateNarration: isTopicUpdateNarrationEnabled,
topicUpdateNarration: context.config.isTopicUpdateNarrationEnabled(),
})),
subAgents: this.withSection(
'agentContexts',
@@ -189,7 +184,8 @@ export class PromptProvider {
? { path: approvedPlanPath }
: undefined,
taskTracker: trackerDir,
topicUpdateNarration: isTopicUpdateNarrationEnabled,
topicUpdateNarration:
context.config.isTopicUpdateNarrationEnabled(),
};
},
!isPlanMode,
@@ -211,7 +207,8 @@ export class PromptProvider {
enableShellEfficiency:
context.config.getEnableShellOutputEfficiency(),
interactiveShellEnabled: context.config.isInteractiveShellEnabled(),
topicUpdateNarration: isTopicUpdateNarrationEnabled,
topicUpdateNarration:
context.config.isTopicUpdateNarrationEnabled(),
memoryManagerEnabled: context.config.isMemoryManagerEnabled(),
}),
),
@@ -254,7 +251,7 @@ export class PromptProvider {
let sanitizedPrompt = finalPrompt.replace(/\n{3,}/g, '\n\n');
// Context Reinjection (Active Topic)
if (isTopicUpdateNarrationEnabled) {
if (context.config.isTopicUpdateNarrationEnabled()) {
const activeTopic = context.config.topicState.getTopic();
if (activeTopic) {
const sanitizedTopic = activeTopic
@@ -25,6 +25,7 @@ import {
import {
isStrictlyApproved,
verifySandboxOverrides,
getCommandName,
} from '../utils/commandUtils.js';
import { assertValidPathString } from '../../utils/paths.js';
import {
@@ -39,11 +40,6 @@ import {
import { isErrnoException } from '../utils/fsUtils.js';
import { handleReadWriteCommands } from '../utils/sandboxReadWriteUtils.js';
import { buildBwrapArgs } from './bwrapArgsBuilder.js';
import {
getCommandRoots,
initializeShellParsers,
stripShellWrapper,
} from '../../utils/shell-utils.js';
let cachedBpfPath: string | undefined;
@@ -222,15 +218,7 @@ export class LinuxSandboxManager implements SandboxManager {
args = ['-c', 'cat > "$1"', '_', ...args];
}
await initializeShellParsers();
const fullCmd = [command, ...args].join(' ');
const stripped = stripShellWrapper(fullCmd);
const roots = getCommandRoots(stripped).filter(
(r) => r !== 'shopt' && r !== 'set',
);
const commandName = roots.length > 0 ? roots[0] : join(command);
const isGitCommand = roots.includes('git');
const commandName = await getCommandName({ ...req, command, args });
const isApproved = allowOverrides
? await isStrictlyApproved(
{ ...req, command, args },
@@ -265,15 +253,6 @@ export class LinuxSandboxManager implements SandboxManager {
false,
};
// If the workspace is writable and we're running a git command,
// automatically allow write access to the .git directory.
if (workspaceWrite && isGitCommand) {
const gitDir = join(this.options.workspace, '.git');
if (!mergedAdditional.fileSystem!.write!.includes(gitDir)) {
mergedAdditional.fileSystem!.write!.push(gitDir);
}
}
const { command: finalCommand, args: finalArgs } = handleReadWriteCommands(
req,
mergedAdditional,
@@ -115,14 +115,14 @@ describe.skipIf(os.platform() === 'win32')('buildBwrapArgs', () => {
workspace,
workspace,
'--ro-bind',
`${workspace}/.git`,
`${workspace}/.git`,
'--ro-bind',
`${workspace}/.gitignore`,
`${workspace}/.gitignore`,
'--ro-bind',
`${workspace}/.geminiignore`,
`${workspace}/.geminiignore`,
'--ro-bind',
`${workspace}/.git`,
`${workspace}/.git`,
]);
});
@@ -14,7 +14,6 @@ import {
import { isErrnoException } from '../utils/fsUtils.js';
import { spawnAsync } from '../../utils/shell-utils.js';
import { debugLogger } from '../../utils/debugLogger.js';
import { toPathKey } from '../../utils/paths.js';
/**
* Options for building bubblewrap (bwrap) arguments.
@@ -64,101 +63,58 @@ export async function buildBwrapArgs(
'/tmp',
);
type MountType =
| '--bind'
| '--ro-bind'
| '--bind-try'
| '--ro-bind-try'
| '--symlink';
const bindFlag = workspaceWrite ? '--bind-try' : '--ro-bind-try';
type Mount =
| {
type: MountType;
src: string;
dest: string;
}
| { type: '--tmpfs-ro'; dest: string };
const mounts: Mount[] = [];
const bindFlag: MountType = workspaceWrite ? '--bind-try' : '--ro-bind-try';
mounts.push({
type: bindFlag,
src: workspace.original,
dest: workspace.original,
});
bwrapArgs.push(bindFlag, workspace.original, workspace.original);
if (workspace.resolved !== workspace.original) {
mounts.push({
type: bindFlag,
src: workspace.resolved,
dest: workspace.resolved,
});
bwrapArgs.push(bindFlag, workspace.resolved, workspace.resolved);
}
for (const includeDir of resolvedPaths.globalIncludes) {
mounts.push({ type: '--ro-bind-try', src: includeDir, dest: includeDir });
bwrapArgs.push('--ro-bind-try', includeDir, includeDir);
}
for (const allowedPath of resolvedPaths.policyAllowed) {
if (fs.existsSync(allowedPath)) {
mounts.push({ type: '--bind-try', src: allowedPath, dest: allowedPath });
bwrapArgs.push('--bind-try', allowedPath, allowedPath);
} else {
// If the path doesn't exist, we still want to allow access to its parent
// to enable creating it.
const parent = dirname(allowedPath);
mounts.push({
type: isReadOnlyCommand ? '--ro-bind-try' : '--bind-try',
src: parent,
dest: parent,
});
bwrapArgs.push(
isReadOnlyCommand ? '--ro-bind-try' : '--bind-try',
parent,
parent,
);
}
}
for (const p of resolvedPaths.policyRead) {
mounts.push({ type: '--ro-bind-try', src: p, dest: p });
bwrapArgs.push('--ro-bind-try', p, p);
}
// Collect explicit additional write permissions.
for (const p of resolvedPaths.policyWrite) {
mounts.push({ type: '--bind-try', src: p, dest: p });
bwrapArgs.push('--bind-try', p, p);
}
const policyWriteKeys = new Set(resolvedPaths.policyWrite.map(toPathKey));
for (const file of GOVERNANCE_FILES) {
const filePath = join(workspace.original, file.path);
const realPath = join(workspace.resolved, file.path);
const isExplicitlyWritable =
policyWriteKeys.has(toPathKey(filePath)) ||
policyWriteKeys.has(toPathKey(realPath));
// If the workspace is writable, we allow editing .gitignore and .geminiignore by default.
// .git remains protected unless explicitly requested (e.g. for git commands).
const isImplicitlyWritable = workspaceWrite && file.path !== '.git';
if (!isExplicitlyWritable && !isImplicitlyWritable) {
mounts.push({ type: '--ro-bind', src: filePath, dest: filePath });
if (realPath !== filePath) {
mounts.push({ type: '--ro-bind', src: realPath, dest: realPath });
}
bwrapArgs.push('--ro-bind', filePath, filePath);
if (realPath !== filePath) {
bwrapArgs.push('--ro-bind', realPath, realPath);
}
}
// Grant read-only access to git worktrees/submodules.
// Grant read-only access to git worktrees/submodules. We do this last in order to
// ensure that these rules aren't overwritten by broader write policies.
if (resolvedPaths.gitWorktree) {
const { worktreeGitDir, mainGitDir } = resolvedPaths.gitWorktree;
if (worktreeGitDir && !policyWriteKeys.has(toPathKey(worktreeGitDir))) {
mounts.push({
type: '--ro-bind-try',
src: worktreeGitDir,
dest: worktreeGitDir,
});
if (worktreeGitDir) {
bwrapArgs.push('--ro-bind-try', worktreeGitDir, worktreeGitDir);
}
if (mainGitDir && !policyWriteKeys.has(toPathKey(mainGitDir))) {
mounts.push({
type: '--ro-bind-try',
src: mainGitDir,
dest: mainGitDir,
});
if (mainGitDir) {
bwrapArgs.push('--ro-bind-try', mainGitDir, mainGitDir);
}
}
@@ -167,23 +123,37 @@ export async function buildBwrapArgs(
try {
const stat = fs.statSync(p);
if (stat.isDirectory()) {
mounts.push({ type: '--tmpfs-ro', dest: p });
bwrapArgs.push('--tmpfs', p, '--remount-ro', p);
} else {
mounts.push({ type: '--ro-bind', src: '/dev/null', dest: p });
bwrapArgs.push('--ro-bind', '/dev/null', p);
}
} catch (e: unknown) {
if (isErrnoException(e) && e.code === 'ENOENT') {
mounts.push({ type: '--symlink', src: '/dev/null', dest: p });
bwrapArgs.push('--symlink', '/dev/null', p);
} else {
debugLogger.warn(
`Failed to secure forbidden path ${p}: ${e instanceof Error ? e.message : String(e)}`,
);
mounts.push({ type: '--ro-bind', src: '/dev/null', dest: p });
bwrapArgs.push('--ro-bind', '/dev/null', p);
}
}
}
// Mask secret files (.env, .env.*)
const secretArgs = await getSecretFilesArgs(resolvedPaths, maskFilePath);
bwrapArgs.push(...secretArgs);
return bwrapArgs;
}
/**
* Generates bubblewrap arguments to mask secret files.
*/
async function getSecretFilesArgs(
resolvedPaths: ResolvedSandboxPaths,
maskPath: string,
): Promise<string[]> {
const args: string[] = [];
const searchDirs = new Set([
resolvedPaths.workspace.original,
resolvedPaths.workspace.resolved,
@@ -194,6 +164,9 @@ export async function buildBwrapArgs(
for (const dir of searchDirs) {
try {
// Use the native 'find' command for performance and to catch nested secrets.
// We limit depth to 3 to keep it fast while covering common nested structures.
// We use -prune to skip heavy directories efficiently while matching dotfiles.
const findResult = await spawnAsync('find', [
dir,
'-maxdepth',
@@ -230,7 +203,7 @@ export async function buildBwrapArgs(
const files = findResult.stdout.toString().split('\0');
for (const file of files) {
if (file.trim()) {
mounts.push({ type: '--bind', src: maskFilePath, dest: file.trim() });
args.push('--bind', maskPath, file.trim());
}
}
} catch (e) {
@@ -240,19 +213,5 @@ export async function buildBwrapArgs(
);
}
}
// Sort mounts by destination path length to ensure parents are bound before children.
// This prevents hierarchical masking where a parent mount would hide a child mount.
mounts.sort((a, b) => a.dest.length - b.dest.length);
// Emit final bwrap arguments
for (const m of mounts) {
if (m.type === '--tmpfs-ro') {
bwrapArgs.push('--tmpfs', m.dest, '--remount-ro', m.dest);
} else {
bwrapArgs.push(m.type, m.src, m.dest);
}
}
return bwrapArgs;
return args;
}
@@ -22,11 +22,7 @@ import {
getSecureSanitizationConfig,
} from '../../services/environmentSanitization.js';
import { buildSeatbeltProfile } from './seatbeltArgsBuilder.js';
import {
initializeShellParsers,
getCommandRoots,
stripShellWrapper,
} from '../../utils/shell-utils.js';
import { initializeShellParsers } from '../../utils/shell-utils.js';
import {
isKnownSafeCommand,
isDangerousCommand,
@@ -137,22 +133,6 @@ export class MacOsSandboxManager implements SandboxManager {
false,
};
// If the workspace is writable and we're running a git command,
// automatically allow write access to the .git directory.
const fullCmd = [command, ...args].join(' ');
const stripped = stripShellWrapper(fullCmd);
const roots = getCommandRoots(stripped).filter(
(r) => r !== 'shopt' && r !== 'set',
);
const isGitCommand = roots.includes('git');
if (workspaceWrite && isGitCommand) {
const gitDir = path.join(this.options.workspace, '.git');
if (!mergedAdditional.fileSystem!.write!.includes(gitDir)) {
mergedAdditional.fileSystem!.write!.push(gitDir);
}
}
const { command: finalCommand, args: finalArgs } = handleReadWriteCommands(
req,
mergedAdditional,
@@ -16,7 +16,7 @@ import {
SECRET_FILES,
type ResolvedSandboxPaths,
} from '../../services/sandboxManager.js';
import { isSubpath, resolveToRealPath } from '../../utils/paths.js';
import { resolveToRealPath } from '../../utils/paths.js';
/**
* Options for building macOS Seatbelt profile.
@@ -37,47 +37,6 @@ export function escapeSchemeString(str: string): string {
return str.replace(/[\\"]/g, '\\$&');
}
/**
* Checks if a path is explicitly allowed by additional write permissions.
*/
function isPathExplicitlyAllowed(
filePath: string,
realFilePath: string,
policyWrite: string[],
): boolean {
return policyWrite.some(
(p) =>
p === filePath ||
p === realFilePath ||
isSubpath(p, filePath) ||
isSubpath(p, realFilePath),
);
}
function denyUnlessExplicitlyAllowed(
targetPath: string,
ruleType: 'literal' | 'subpath',
policyWrite: string[],
implicitlyAllowed: boolean = false,
): string {
if (implicitlyAllowed) {
return '';
}
const realPath = resolveToRealPath(targetPath);
if (isPathExplicitlyAllowed(targetPath, realPath, policyWrite)) {
return ''; // Skip if explicitly allowed
}
let rules = `(deny file-write* (${ruleType} "${escapeSchemeString(targetPath)}"))\n`;
if (realPath !== targetPath) {
rules += `(deny file-write* (${ruleType} "${escapeSchemeString(realPath)}"))\n`;
}
return rules;
}
/**
* Builds a complete macOS Seatbelt profile string using a strict allowlist.
* It embeds paths directly into the profile, properly escaped for Scheme.
@@ -149,84 +108,6 @@ export function buildSeatbeltProfile(options: SeatbeltArgsOptions): string {
profile += `(allow file-read* file-write* (subpath "${escapeSchemeString(allowedPath)}"))\n`;
}
// Add explicit deny rules for governance files in the workspace.
// These are added after the workspace allow rule to ensure they take precedence
// (Seatbelt evaluates rules in order, later rules win for same path).
for (let i = 0; i < GOVERNANCE_FILES.length; i++) {
const governanceFile = path.join(
resolvedPaths.workspace.resolved,
GOVERNANCE_FILES[i].path,
);
const realGovernanceFile = resolveToRealPath(governanceFile);
// Determine if it should be treated as a directory (subpath) or a file (literal).
// .git is generally a directory, while ignore files are literals.
let isDirectory = GOVERNANCE_FILES[i].isDirectory;
try {
if (fs.existsSync(realGovernanceFile)) {
isDirectory = fs.lstatSync(realGovernanceFile).isDirectory();
}
} catch {
// Ignore errors, use default guess
}
profile += denyUnlessExplicitlyAllowed(
governanceFile,
isDirectory ? 'subpath' : 'literal',
resolvedPaths.policyWrite,
workspaceWrite && GOVERNANCE_FILES[i].path !== '.git',
);
}
// Grant read-only access to git worktrees/submodules. We do this last in order to
// ensure that these rules aren't overwritten by broader write policies.
if (resolvedPaths.gitWorktree) {
const { worktreeGitDir, mainGitDir } = resolvedPaths.gitWorktree;
if (worktreeGitDir) {
profile += denyUnlessExplicitlyAllowed(
worktreeGitDir,
'subpath',
resolvedPaths.policyWrite,
);
}
if (mainGitDir) {
profile += denyUnlessExplicitlyAllowed(
mainGitDir,
'subpath',
resolvedPaths.policyWrite,
);
}
}
// Add explicit deny rules for secret files (.env, .env.*) in the workspace and allowed paths.
// We use regex rules to avoid expensive file discovery scans.
// Anchoring to workspace/allowed paths to avoid over-blocking.
const searchPaths = [
resolvedPaths.workspace.resolved,
resolvedPaths.workspace.original,
...resolvedPaths.policyAllowed,
...resolvedPaths.globalIncludes,
];
for (const basePath of searchPaths) {
for (const secret of SECRET_FILES) {
// Map pattern to Seatbelt regex
let regexPattern: string;
const escapedBase = escapeRegex(basePath);
if (secret.pattern.endsWith('*')) {
// .env.* -> .env\..+ (match .env followed by dot and something)
// We anchor the secret file name to either a directory separator or the start of the relative path.
const basePattern = secret.pattern.slice(0, -1).replace(/\./g, '\\\\.');
regexPattern = `^${escapedBase}/(.*/)?${basePattern}[^/]+$`;
} else {
// .env -> \.env$
const basePattern = secret.pattern.replace(/\./g, '\\\\.');
regexPattern = `^${escapedBase}/(.*/)?${basePattern}$`;
}
profile += `(deny file-read* file-write* (regex #"${regexPattern}"))\n`;
}
}
// Handle granular additional read permissions
for (let i = 0; i < resolvedPaths.policyRead.length; i++) {
const resolved = resolvedPaths.policyRead[i];
@@ -259,6 +140,77 @@ export function buildSeatbeltProfile(options: SeatbeltArgsOptions): string {
}
}
// Add explicit deny rules for governance files in the workspace.
// These are added after the workspace allow rule to ensure they take precedence
// (Seatbelt evaluates rules in order, later rules win for same path).
for (let i = 0; i < GOVERNANCE_FILES.length; i++) {
const governanceFile = path.join(
resolvedPaths.workspace.resolved,
GOVERNANCE_FILES[i].path,
);
const realGovernanceFile = resolveToRealPath(governanceFile);
// Determine if it should be treated as a directory (subpath) or a file (literal).
// .git is generally a directory, while ignore files are literals.
let isDirectory = GOVERNANCE_FILES[i].isDirectory;
try {
if (fs.existsSync(realGovernanceFile)) {
isDirectory = fs.lstatSync(realGovernanceFile).isDirectory();
}
} catch {
// Ignore errors, use default guess
}
const ruleType = isDirectory ? 'subpath' : 'literal';
profile += `(deny file-write* (${ruleType} "${escapeSchemeString(governanceFile)}"))\n`;
if (realGovernanceFile !== governanceFile) {
profile += `(deny file-write* (${ruleType} "${escapeSchemeString(realGovernanceFile)}"))\n`;
}
}
// Grant read-only access to git worktrees/submodules. We do this last in order to
// ensure that these rules aren't overwritten by broader write policies.
if (resolvedPaths.gitWorktree) {
const { worktreeGitDir, mainGitDir } = resolvedPaths.gitWorktree;
if (worktreeGitDir) {
profile += `(deny file-write* (subpath "${escapeSchemeString(worktreeGitDir)}"))\n`;
}
if (mainGitDir) {
profile += `(deny file-write* (subpath "${escapeSchemeString(mainGitDir)}"))\n`;
}
}
// Add explicit deny rules for secret files (.env, .env.*) in the workspace and allowed paths.
// We use regex rules to avoid expensive file discovery scans.
// Anchoring to workspace/allowed paths to avoid over-blocking.
const searchPaths = [
resolvedPaths.workspace.resolved,
resolvedPaths.workspace.original,
...resolvedPaths.policyAllowed,
...resolvedPaths.globalIncludes,
];
for (const basePath of searchPaths) {
for (const secret of SECRET_FILES) {
// Map pattern to Seatbelt regex
let regexPattern: string;
const escapedBase = escapeRegex(basePath);
if (secret.pattern.endsWith('*')) {
// .env.* -> .env\..+ (match .env followed by dot and something)
// We anchor the secret file name to either a directory separator or the start of the relative path.
const basePattern = secret.pattern.slice(0, -1).replace(/\./g, '\\\\.');
regexPattern = `^${escapedBase}/(.*/)?${basePattern}[^/]+$`;
} else {
// .env -> \.env$
const basePattern = secret.pattern.replace(/\./g, '\\\\.');
regexPattern = `^${escapedBase}/(.*/)?${basePattern}$`;
}
profile += `(deny file-read* file-write* (regex #"${regexPattern}"))\n`;
}
}
// Handle forbiddenPaths
const forbiddenPaths = resolvedPaths.forbidden;
for (let i = 0; i < forbiddenPaths.length; i++) {
@@ -0,0 +1,144 @@
/**
* @license
* Copyright 2026 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import { describe, expect, it, vi } from 'vitest';
// Mock shell-utils to avoid relying on tree-sitter WASM
vi.mock('../../utils/shell-utils.js', () => ({
initializeShellParsers: vi.fn().mockResolvedValue(undefined),
splitCommands: (cmd: string) => [cmd],
stripShellWrapper: (cmd: string) => cmd,
extractStringFromParseEntry: (entry: unknown) => {
if (typeof entry === 'string') return entry;
if (entry && typeof entry === 'object' && 'content' in entry) {
return (entry as { content: string }).content;
}
return '';
},
normalizeCommand: (cmd: string) => {
if (!cmd) return '';
const parts = cmd.split(/[\\/]/).filter(Boolean);
const base = parts.length > 0 ? parts[parts.length - 1] : '';
return base.toLowerCase().replace(/\.exe$/, '');
},
}));
import { isKnownSafeCommand, isDangerousCommand } from './commandSafety.js';
describe('POSIX commandSafety', () => {
describe('isKnownSafeCommand', () => {
it('should identify known safe commands', () => {
expect(isKnownSafeCommand(['ls', '-la'])).toBe(true);
expect(isKnownSafeCommand(['cat', 'file.txt'])).toBe(true);
expect(isKnownSafeCommand(['pwd'])).toBe(true);
expect(isKnownSafeCommand(['echo', 'hello'])).toBe(true);
});
it('should identify safe git commands', () => {
expect(isKnownSafeCommand(['git', 'status'])).toBe(true);
expect(isKnownSafeCommand(['git', 'log'])).toBe(true);
expect(isKnownSafeCommand(['git', 'diff'])).toBe(true);
});
it('should reject unsafe git commands', () => {
expect(isKnownSafeCommand(['git', 'commit'])).toBe(false);
expect(isKnownSafeCommand(['git', 'push'])).toBe(false);
expect(isKnownSafeCommand(['git', 'checkout'])).toBe(false);
});
it('should reject commands with redirection', () => {
// isKnownSafeCommand handles bash -c "..." which can have redirections
// but the simple check for atomic commands doesn't see redirection because it's already parsed
});
});
describe('isDangerousCommand', () => {
it('should identify destructive rm commands', () => {
expect(isDangerousCommand(['rm'])).toBe(true);
expect(isDangerousCommand(['rm', 'file.txt'])).toBe(true);
expect(isDangerousCommand(['rm', '-rf', '/'])).toBe(true);
expect(isDangerousCommand(['rm', '-f', 'file'])).toBe(true);
expect(isDangerousCommand(['rm', '-r', 'dir'])).toBe(true);
expect(isDangerousCommand(['/bin/rm', 'file'])).toBe(true);
});
it('should flag rm help/version as dangerous (strict)', () => {
expect(isDangerousCommand(['rm', '--help'])).toBe(true);
expect(isDangerousCommand(['rm', '--version'])).toBe(true);
});
it('should identify sudo as dangerous if command is dangerous', () => {
expect(isDangerousCommand(['sudo', 'rm', 'file'])).toBe(true);
expect(isDangerousCommand(['sudo', '-u', 'root', 'rm', 'file'])).toBe(
true,
);
expect(isDangerousCommand(['sudo', '--user', 'root', 'rm', 'file'])).toBe(
true,
);
expect(isDangerousCommand(['sudo', '-t', 'type', 'rm', 'file'])).toBe(
true,
);
expect(isDangerousCommand(['sudo', '-o', 'opt=val', 'rm', 'file'])).toBe(
true,
);
expect(
isDangerousCommand(['sudo', '--option', 'opt=val', 'rm', 'file']),
).toBe(true);
expect(isDangerousCommand(['sudo', '--askpass', 'rm', 'file'])).toBe(
true,
);
expect(isDangerousCommand(['sudo', '-D', '/tmp', 'rm', '-rf', '/'])).toBe(
true,
);
expect(
isDangerousCommand(['sudo', '--chroot', '/tmp', 'rm', '-rf', '/']),
).toBe(true);
expect(isDangerousCommand(['sudo', '-r', 'role', 'rm', 'file'])).toBe(
true,
);
expect(isDangerousCommand(['sudo', '-t', 'type', 'rm', 'file'])).toBe(
true,
);
expect(isDangerousCommand(['sudo', '--askpass', 'rm', 'file'])).toBe(
true,
);
expect(isDangerousCommand(['sudo', './rm=dangerous'])).toBe(false);
expect(isDangerousCommand(['sudo', './rm'])).toBe(true);
});
it('should identify find -exec as dangerous', () => {
expect(isDangerousCommand(['find', '.', '-exec', 'rm', '{}', '+'])).toBe(
true,
);
});
it('should identify dangerous commands wrapped in env', () => {
expect(isDangerousCommand(['env', 'rm', '-rf', '/'])).toBe(true);
expect(isDangerousCommand(['env', '-i', 'rm', '-rf', '/'])).toBe(true);
expect(isDangerousCommand(['env', '-u', 'USER', 'rm', '-rf', '/'])).toBe(
true,
);
expect(isDangerousCommand(['env', 'VAR=val', 'rm', '-rf', '/'])).toBe(
true,
);
expect(isDangerousCommand(['env', '--', 'rm', '-rf', '/'])).toBe(true);
});
it('should identify dangerous commands wrapped in xargs', () => {
expect(isDangerousCommand(['xargs', 'rm', '-rf', '/'])).toBe(true);
expect(isDangerousCommand(['xargs', '-I', '{}', 'rm', '{}'])).toBe(true);
expect(isDangerousCommand(['xargs', '-n', '1', 'rm'])).toBe(true);
expect(isDangerousCommand(['xargs', '-0', 'rm'])).toBe(true);
expect(isDangerousCommand(['xargs', '--', 'rm'])).toBe(true);
});
it('should identify dangerous rg flags', () => {
expect(isDangerousCommand(['rg', '--hostname-bin', 'something'])).toBe(
true,
);
});
});
});
@@ -9,6 +9,7 @@ import {
initializeShellParsers,
splitCommands,
stripShellWrapper,
normalizeCommand,
} from '../../utils/shell-utils.js';
/**
@@ -255,7 +256,7 @@ function isSafeToCallWithExec(args: string[]): boolean {
* @param subcommands - A list of subcommands to look for.
* @returns An object containing the index of the subcommand and its name.
*/
function findGitSubcommand(
export function findGitSubcommand(
args: string[],
subcommands: string[],
): { idx: number; subcommand: string | null } {
@@ -317,7 +318,7 @@ function findGitSubcommand(
* @param args - The git command arguments.
* @returns true if config overrides are present.
*/
function gitHasConfigOverrideGlobalOption(args: string[]): boolean {
export function gitHasConfigOverrideGlobalOption(args: string[]): boolean {
return args.some(
(arg) =>
arg === '-c' ||
@@ -335,7 +336,7 @@ function gitHasConfigOverrideGlobalOption(args: string[]): boolean {
* @param args - Arguments passed to the git subcommand.
* @returns true if the arguments only represent read-only operations.
*/
function gitSubcommandArgsAreReadOnly(args: string[]): boolean {
export function gitSubcommandArgsAreReadOnly(args: string[]): boolean {
const unsafeFlags = new Set([
'--output',
'--ext-diff',
@@ -359,7 +360,7 @@ function gitSubcommandArgsAreReadOnly(args: string[]): boolean {
* @param args - Arguments passed to `git branch`.
* @returns true if it's purely a listing/read-only branch command.
*/
function gitBranchIsReadOnly(args: string[]): boolean {
export function gitBranchIsReadOnly(args: string[]): boolean {
if (args.length === 0) return true;
let sawReadOnlyFlag = false;
@@ -428,14 +429,105 @@ export function isDangerousCommand(args: string[]): boolean {
return false;
}
const cmd = args[0];
const cmd = normalizeCommand(args[0]);
if (cmd === 'rm') {
return args[1] === '-f' || args[1] === '-rf' || args[1] === '-fr';
return true;
}
if (cmd === 'sudo') {
return isDangerousCommand(args.slice(1));
let nextCmdIdx = 1;
while (nextCmdIdx < args.length) {
const arg = args[nextCmdIdx];
if (arg === '--') {
nextCmdIdx++;
break;
}
if (!arg.startsWith('-')) {
// It could be variable assignment like VAR=value before the command
if (/^[a-zA-Z_][a-zA-Z0-9_]*=/.test(arg)) {
nextCmdIdx++;
continue;
}
break;
}
// Check for options that consume the next argument
if (
/^-[acCDghopRrTtUu]$/.test(arg) ||
[
'--auth-type',
'--login-class',
'--close-from',
'--chdir',
'--group',
'--host',
'--prompt',
'--chroot',
'--role',
'--type',
'--command-timeout',
'--timeout',
'--user',
'--other-user',
'--option',
].includes(arg)
) {
nextCmdIdx += 2;
} else {
nextCmdIdx += 1;
}
}
return isDangerousCommand(args.slice(nextCmdIdx));
}
if (['env', 'xargs'].includes(cmd)) {
let nextCmdIdx = 1;
while (nextCmdIdx < args.length) {
const arg = args[nextCmdIdx];
if (arg === '--') {
nextCmdIdx++;
break;
}
if (!arg.startsWith('-')) {
// It could be variable assignment like VAR=value before the command (for env)
if (cmd === 'env' && /^[a-zA-Z_][a-zA-Z0-9_]*=/.test(arg)) {
nextCmdIdx++;
continue;
}
break;
}
// Check for options that consume the next argument
if (cmd === 'env') {
if (
/^-[uS]$/.test(arg) ||
['--unset', '--split-string'].includes(arg)
) {
nextCmdIdx += 2;
} else {
nextCmdIdx += 1;
}
} else if (cmd === 'xargs') {
if (
/^-[aEeIiLlnPs]$/.test(arg) ||
[
'--arg-file',
'--max-args',
'--max-procs',
'--max-chars',
'--process-slot-var',
].includes(arg)
) {
nextCmdIdx += 2;
} else {
nextCmdIdx += 1;
}
} else {
nextCmdIdx += 1;
}
}
return isDangerousCommand(args.slice(nextCmdIdx));
}
if (cmd === 'find') {
@@ -25,13 +25,7 @@ import {
getSecureSanitizationConfig,
} from '../../services/environmentSanitization.js';
import { debugLogger } from '../../utils/debugLogger.js';
import {
spawnAsync,
getCommandName,
initializeShellParsers,
getCommandRoots,
stripShellWrapper,
} from '../../utils/shell-utils.js';
import { spawnAsync, getCommandName } from '../../utils/shell-utils.js';
import {
isKnownSafeCommand,
isDangerousCommand,
@@ -267,14 +261,6 @@ export class WindowsSandboxManager implements SandboxManager {
this.options.modeConfig?.network ?? req.policy?.networkAccess ?? false;
const networkAccess = defaultNetwork || mergedAdditional.network;
await initializeShellParsers();
const fullCmd = [command, ...args].join(' ');
const stripped = stripShellWrapper(fullCmd);
const roots = getCommandRoots(stripped).filter(
(r) => r !== 'shopt' && r !== 'set',
);
const isGitCommand = roots.includes('git');
const resolvedPaths = await resolveSandboxPaths(
this.options,
req,
@@ -362,13 +348,6 @@ export class WindowsSandboxManager implements SandboxManager {
if (workspaceWrite) {
addWritableRoot(resolvedPaths.workspace.resolved);
// If the workspace is writable and we're running a git command,
// automatically allow write access to the .git directory.
if (isGitCommand) {
const gitDir = path.join(resolvedPaths.workspace.resolved, '.git');
addWritableRoot(gitDir);
}
}
// B. Globally included directories
@@ -25,6 +25,15 @@ describe('Windows commandSafety', () => {
expect(isKnownSafeCommand(['unknown'])).toBe(false);
expect(isKnownSafeCommand(['npm', 'install'])).toBe(false);
});
it('should reject unsafe git commands', () => {
expect(isKnownSafeCommand(['git', 'commit'])).toBe(false);
expect(isKnownSafeCommand(['git', 'push'])).toBe(false);
expect(isKnownSafeCommand(['git', 'checkout'])).toBe(false);
expect(isKnownSafeCommand(['git', 'log', '--output=file.txt'])).toBe(
false,
);
});
});
describe('isDangerousCommand', () => {
@@ -34,6 +43,21 @@ describe('Windows commandSafety', () => {
expect(isDangerousCommand(['cmd', '/c', 'dir'])).toBe(true);
});
it('should reject unsafe git commands as dangerous', () => {
expect(isDangerousCommand(['git', 'log', '--output=file.txt'])).toBe(
true,
);
expect(isDangerousCommand(['git', '-c', 'alias.foo=!sh', 'status'])).toBe(
true,
);
});
it('should identify path-qualified dangerous commands', () => {
expect(
isDangerousCommand(['C:\\Windows\\System32\\del.exe', 'file.txt']),
).toBe(true);
});
it('should strip .exe extension for dangerous commands', () => {
expect(isDangerousCommand(['del.exe', 'file.txt'])).toBe(true);
expect(isDangerousCommand(['POWERSHELL.EXE', '-Command', 'echo'])).toBe(
@@ -42,6 +66,41 @@ describe('Windows commandSafety', () => {
expect(isDangerousCommand(['cmd.exe', '/c', 'dir'])).toBe(true);
});
it('should strip multiple trailing dots and extensions for dangerous commands', () => {
expect(isDangerousCommand(['powershell.', '-Command', 'echo'])).toBe(
true,
);
expect(isDangerousCommand(['powershell.exe.', '-Command', 'echo'])).toBe(
true,
);
expect(isDangerousCommand(['cmd.bat..', '/c', 'dir'])).toBe(true);
});
it('should flag rm as dangerous', () => {
expect(isDangerousCommand(['rm', 'file'])).toBe(true);
expect(isDangerousCommand(['rm.exe', 'file'])).toBe(true);
});
it('should identify dangerous commands wrapped in env', () => {
expect(isDangerousCommand(['env', 'rm', '-rf', '/'])).toBe(true);
expect(isDangerousCommand(['env', '-i', 'rm', '-rf', '/'])).toBe(true);
expect(isDangerousCommand(['env', '-u', 'USER', 'rm', '-rf', '/'])).toBe(
true,
);
expect(isDangerousCommand(['env', 'VAR=val', 'rm', '-rf', '/'])).toBe(
true,
);
expect(isDangerousCommand(['env', '--', 'rm', '-rf', '/'])).toBe(true);
});
it('should identify dangerous commands wrapped in xargs', () => {
expect(isDangerousCommand(['xargs', 'rm', '-rf', '/'])).toBe(true);
expect(isDangerousCommand(['xargs', '-I', '{}', 'rm', '{}'])).toBe(true);
expect(isDangerousCommand(['xargs', '-n', '1', 'rm'])).toBe(true);
expect(isDangerousCommand(['xargs', '-0', 'rm'])).toBe(true);
expect(isDangerousCommand(['xargs', '--', 'rm'])).toBe(true);
});
it('should not flag safe commands as dangerous', () => {
expect(isDangerousCommand(['dir'])).toBe(false);
expect(isDangerousCommand(['echo', 'hello'])).toBe(false);
@@ -9,7 +9,15 @@ import {
initializeShellParsers,
splitCommands,
stripShellWrapper,
normalizeCommand,
hasRedirection,
} from '../../utils/shell-utils.js';
import {
findGitSubcommand,
gitHasConfigOverrideGlobalOption,
gitSubcommandArgsAreReadOnly,
gitBranchIsReadOnly,
} from '../utils/commandSafety.js';
/**
* Determines if a command is strictly approved for execution on Windows.
@@ -33,6 +41,10 @@ export async function isStrictlyApproved(
const fullCmd = [command, ...args].join(' ');
const stripped = stripShellWrapper(fullCmd);
if (hasRedirection(stripped)) {
return false;
}
const pipelineCommands = splitCommands(stripped);
// Fallback for simple commands or parsing failures
@@ -48,10 +60,7 @@ export async function isStrictlyApproved(
const parsedArgs = shellParse(trimmed).map(extractStringFromParseEntry);
if (parsedArgs.length === 0) return true;
let root = parsedArgs[0].toLowerCase();
if (root.endsWith('.exe')) {
root = root.slice(0, -4);
}
const root = normalizeCommand(parsedArgs[0]);
// The segment is approved if the root tool is in the allowlist OR if the whole segment is safe.
return (
tools.some((t) => t.toLowerCase() === root) ||
@@ -65,10 +74,7 @@ export async function isStrictlyApproved(
*/
export function isKnownSafeCommand(args: string[]): boolean {
if (!args || args.length === 0) return false;
let cmd = args[0].toLowerCase();
if (cmd.endsWith('.exe')) {
cmd = cmd.slice(0, -4);
}
const cmd = normalizeCommand(args[0]);
// Native Windows/PowerShell safe commands
const safeCommands = new Set([
@@ -105,10 +111,35 @@ export function isKnownSafeCommand(args: string[]): boolean {
// We allow git on Windows if it's read-only, using the same logic as POSIX
if (cmd === 'git') {
// For simplicity in this branch, we'll allow standard git read operations
// In a full implementation, we'd port the sub-command validation too.
const sub = args[1]?.toLowerCase();
return ['status', 'log', 'diff', 'show', 'branch'].includes(sub);
if (gitHasConfigOverrideGlobalOption(args)) {
return false;
}
const { idx, subcommand } = findGitSubcommand(args, [
'status',
'log',
'diff',
'show',
'branch',
]);
if (!subcommand) {
return false;
}
const subcommandArgs = args.slice(idx + 1);
if (['status', 'log', 'diff', 'show'].includes(subcommand)) {
return gitSubcommandArgsAreReadOnly(subcommandArgs);
}
if (subcommand === 'branch') {
return (
gitSubcommandArgsAreReadOnly(subcommandArgs) &&
gitBranchIsReadOnly(subcommandArgs)
);
}
return false;
}
return false;
@@ -119,12 +150,10 @@ export function isKnownSafeCommand(args: string[]): boolean {
*/
export function isDangerousCommand(args: string[]): boolean {
if (!args || args.length === 0) return false;
let cmd = args[0].toLowerCase();
if (cmd.endsWith('.exe')) {
cmd = cmd.slice(0, -4);
}
const cmd = normalizeCommand(args[0]);
const dangerous = new Set([
'rm',
'del',
'erase',
'rd',
@@ -146,5 +175,89 @@ export function isDangerousCommand(args: string[]): boolean {
'new-item',
]);
return dangerous.has(cmd);
if (dangerous.has(cmd)) {
return true;
}
if (cmd === 'git') {
if (gitHasConfigOverrideGlobalOption(args)) {
return true;
}
const { idx, subcommand } = findGitSubcommand(args, [
'status',
'log',
'diff',
'show',
'branch',
]);
if (!subcommand) {
// It's a git command we don't recognize as explicitly safe.
return false;
}
const subcommandArgs = args.slice(idx + 1);
if (['status', 'log', 'diff', 'show'].includes(subcommand)) {
return !gitSubcommandArgsAreReadOnly(subcommandArgs);
}
if (subcommand === 'branch') {
return !(
gitSubcommandArgsAreReadOnly(subcommandArgs) &&
gitBranchIsReadOnly(subcommandArgs)
);
}
return false;
}
if (['env', 'xargs'].includes(cmd)) {
let nextCmdIdx = 1;
while (nextCmdIdx < args.length) {
const arg = args[nextCmdIdx];
if (arg === '--') {
nextCmdIdx++;
break;
}
if (!arg.startsWith('-')) {
if (cmd === 'env' && /^[a-zA-Z_][a-zA-Z0-9_]*=/.test(arg)) {
nextCmdIdx++;
continue;
}
break;
}
if (cmd === 'env') {
if (
/^-[uS]$/.test(arg) ||
['--unset', '--split-string'].includes(arg)
) {
nextCmdIdx += 2;
} else {
nextCmdIdx += 1;
}
} else if (cmd === 'xargs') {
if (
/^-[aEeIiLlnPs]$/.test(arg) ||
[
'--arg-file',
'--max-args',
'--max-procs',
'--max-chars',
'--process-slot-var',
].includes(arg)
) {
nextCmdIdx += 2;
} else {
nextCmdIdx += 1;
}
} else {
nextCmdIdx += 1;
}
}
return isDangerousCommand(args.slice(nextCmdIdx));
}
return false;
}
@@ -865,336 +865,6 @@ describe('SandboxManager Integration', () => {
});
});
describe('Governance Files', () => {
it('blocks write access to governance files in the workspace', async () => {
const tempWorkspace = createTempDir('workspace-');
const gitDir = path.join(tempWorkspace, '.git');
fs.mkdirSync(gitDir);
const testFile = path.join(gitDir, 'config');
const osManager = createSandboxManager(
{ enabled: true },
{ workspace: tempWorkspace },
);
const { command, args } = Platform.touch(testFile);
const sandboxed = await osManager.prepareCommand({
command,
args,
cwd: tempWorkspace,
env: process.env,
});
const result = await runCommand(sandboxed);
assertResult(result, sandboxed, 'failure');
expect(fs.existsSync(testFile)).toBe(false);
});
it('allows write access to governance files when explicitly requested via additionalPermissions', async () => {
const tempWorkspace = createTempDir('workspace-');
const gitDir = path.join(tempWorkspace, '.git');
fs.mkdirSync(gitDir);
const testFile = path.join(gitDir, 'config');
const osManager = createSandboxManager(
{ enabled: true },
{ workspace: tempWorkspace },
);
const { command, args } = Platform.touch(testFile);
const sandboxed = await osManager.prepareCommand({
command,
args,
cwd: tempWorkspace,
env: process.env,
policy: {
additionalPermissions: { fileSystem: { write: [gitDir] } },
},
});
const result = await runCommand(sandboxed);
assertResult(result, sandboxed, 'success');
expect(fs.existsSync(testFile)).toBe(true);
});
});
describe('Git Worktree Support', () => {
it('allows access to git common directory in a worktree', async () => {
const mainRepo = createTempDir('main-repo-');
const worktreeDir = createTempDir('worktree-');
const mainGitDir = path.join(mainRepo, '.git');
fs.mkdirSync(mainGitDir, { recursive: true });
fs.writeFileSync(
path.join(mainGitDir, 'config'),
'[core]\n\trepositoryformatversion = 0\n',
);
const worktreeGitDir = path.join(
mainGitDir,
'worktrees',
'test-worktree',
);
fs.mkdirSync(worktreeGitDir, { recursive: true });
// Create the .git file in the worktree directory pointing to the worktree git dir
fs.writeFileSync(
path.join(worktreeDir, '.git'),
`gitdir: ${worktreeGitDir}\n`,
);
// Create the backlink from worktree git dir to the worktree's .git file
const backlinkPath = path.join(worktreeGitDir, 'gitdir');
fs.writeFileSync(backlinkPath, path.join(worktreeDir, '.git'));
// Create a file in the worktree git dir that we want to access
const secretFile = path.join(worktreeGitDir, 'secret.txt');
fs.writeFileSync(secretFile, 'git-secret');
const osManager = createSandboxManager(
{ enabled: true },
{ workspace: worktreeDir },
);
const { command, args } = Platform.cat(secretFile);
const sandboxed = await osManager.prepareCommand({
command,
args,
cwd: worktreeDir,
env: process.env,
});
const result = await runCommand(sandboxed);
assertResult(result, sandboxed, 'success');
expect(result.stdout.trim()).toBe('git-secret');
});
it('blocks write access to git common directory in a worktree', async () => {
const mainRepo = createTempDir('main-repo-');
const worktreeDir = createTempDir('worktree-');
const mainGitDir = path.join(mainRepo, '.git');
fs.mkdirSync(mainGitDir, { recursive: true });
const worktreeGitDir = path.join(
mainGitDir,
'worktrees',
'test-worktree',
);
fs.mkdirSync(worktreeGitDir, { recursive: true });
fs.writeFileSync(
path.join(worktreeDir, '.git'),
`gitdir: ${worktreeGitDir}\n`,
);
fs.writeFileSync(
path.join(worktreeGitDir, 'gitdir'),
path.join(worktreeDir, '.git'),
);
const targetFile = path.join(worktreeGitDir, 'secret.txt');
const osManager = createSandboxManager(
{ enabled: true },
// Use YOLO mode to ensure the workspace is fully writable, but git worktrees should still be read-only
{ workspace: worktreeDir, modeConfig: { yolo: true } },
);
const { command, args } = Platform.touch(targetFile);
const sandboxed = await osManager.prepareCommand({
command,
args,
cwd: worktreeDir,
env: process.env,
});
const result = await runCommand(sandboxed);
assertResult(result, sandboxed, 'failure');
expect(fs.existsSync(targetFile)).toBe(false);
});
it('blocks write access to git common directory in a worktree when not explicitly requested via additionalPermissions', async () => {
const mainRepo = createTempDir('main-repo-');
const worktreeDir = createTempDir('worktree-');
const mainGitDir = path.join(mainRepo, '.git');
fs.mkdirSync(mainGitDir, { recursive: true });
const worktreeGitDir = path.join(
mainGitDir,
'worktrees',
'test-worktree',
);
fs.mkdirSync(worktreeGitDir, { recursive: true });
fs.writeFileSync(
path.join(worktreeDir, '.git'),
`gitdir: ${worktreeGitDir}\n`,
);
fs.writeFileSync(
path.join(worktreeGitDir, 'gitdir'),
path.join(worktreeDir, '.git'),
);
const targetFile = path.join(worktreeGitDir, 'secret.txt');
const osManager = createSandboxManager(
{ enabled: true },
{ workspace: worktreeDir },
);
const { command, args } = Platform.touch(targetFile);
const sandboxed = await osManager.prepareCommand({
command,
args,
cwd: worktreeDir,
env: process.env,
});
const result = await runCommand(sandboxed);
assertResult(result, sandboxed, 'failure');
expect(fs.existsSync(targetFile)).toBe(false);
});
it('allows write access to git common directory in a worktree when explicitly requested via additionalPermissions', async () => {
const mainRepo = createTempDir('main-repo-');
const worktreeDir = createTempDir('worktree-');
const mainGitDir = path.join(mainRepo, '.git');
fs.mkdirSync(mainGitDir, { recursive: true });
const worktreeGitDir = path.join(
mainGitDir,
'worktrees',
'test-worktree',
);
fs.mkdirSync(worktreeGitDir, { recursive: true });
fs.writeFileSync(
path.join(worktreeDir, '.git'),
`gitdir: ${worktreeGitDir}\n`,
);
fs.writeFileSync(
path.join(worktreeGitDir, 'gitdir'),
path.join(worktreeDir, '.git'),
);
const targetFile = path.join(worktreeGitDir, 'secret.txt');
const osManager = createSandboxManager(
{ enabled: true },
{ workspace: worktreeDir },
);
const { command, args } = Platform.touch(targetFile);
const sandboxed = await osManager.prepareCommand({
command,
args,
cwd: worktreeDir,
env: process.env,
policy: {
additionalPermissions: { fileSystem: { write: [worktreeGitDir] } },
},
});
const result = await runCommand(sandboxed);
assertResult(result, sandboxed, 'success');
expect(fs.existsSync(targetFile)).toBe(true);
});
it('allows write access to external git directory in a non-worktree environment when explicitly requested via additionalPermissions', async () => {
const externalGitDir = createTempDir('external-git-');
const workspaceDir = createTempDir('workspace-');
fs.mkdirSync(externalGitDir, { recursive: true });
fs.writeFileSync(
path.join(workspaceDir, '.git'),
`gitdir: ${externalGitDir}\n`,
);
const targetFile = path.join(externalGitDir, 'secret.txt');
const osManager = createSandboxManager(
{ enabled: true },
{ workspace: workspaceDir },
);
const { command, args } = Platform.touch(targetFile);
const sandboxed = await osManager.prepareCommand({
command,
args,
cwd: workspaceDir,
env: process.env,
policy: {
additionalPermissions: { fileSystem: { write: [externalGitDir] } },
},
});
const result = await runCommand(sandboxed);
assertResult(result, sandboxed, 'success');
expect(fs.existsSync(targetFile)).toBe(true);
});
});
describe('Git and Governance Write Access', () => {
it('allows write access to .gitignore when workspace is writable', async () => {
const testFile = path.join(workspace, '.gitignore');
fs.writeFileSync(testFile, 'initial');
const editManager = createSandboxManager(
{ enabled: true },
{ workspace, modeConfig: { readonly: false, allowOverrides: true } },
);
const { command, args } = Platform.touch(testFile);
const sandboxed = await editManager.prepareCommand({
command,
args,
cwd: workspace,
env: process.env,
});
const result = await runCommand(sandboxed);
assertResult(result, sandboxed, 'success');
expect(fs.existsSync(testFile)).toBe(true);
});
it('automatically allows write access to .git when running git command and workspace is writable', async () => {
const gitDir = path.join(workspace, '.git');
if (!fs.existsSync(gitDir)) fs.mkdirSync(gitDir);
const lockFile = path.join(gitDir, 'index.lock');
const editManager = createSandboxManager(
{ enabled: true },
{ workspace, modeConfig: { readonly: false, allowOverrides: true } },
);
// We use a command that looks like git to trigger the special handling.
// LinuxSandboxManager identifies the command root from the shell wrapper.
const { command: nodePath, args: nodeArgs } = Platform.touch(lockFile);
const commandString = Platform.isWindows
? `git --version > NUL && "${nodePath.replace(/\\/g, '/')}" ${nodeArgs
.map((a) => `'${a.replace(/\\/g, '/')}'`)
.join(' ')}`
: `git --version > /dev/null; "${nodePath}" ${nodeArgs
.map((a) => (a.includes(' ') || a.includes('(') ? `'${a}'` : a))
.join(' ')}`;
const sandboxed = await editManager.prepareCommand({
command: 'sh',
args: ['-c', commandString],
cwd: workspace,
env: process.env,
});
const result = await runCommand(sandboxed);
assertResult(result, sandboxed, 'success');
expect(fs.existsSync(lockFile)).toBe(true);
});
});
describe('Network Security', () => {
describe('Network Access', () => {
let server: http.Server;
@@ -409,23 +409,6 @@ export async function resolveSandboxPaths(
? { gitWorktree: { worktreeGitDir, mainGitDir } }
: undefined;
if (worktreeGitDir) {
const gitIdentities = new Set(
[
path.join(options.workspace, '.git'),
path.join(resolvedWorkspace, '.git'),
].map(toPathKey),
);
if (policyRead.some((p) => gitIdentities.has(toPathKey(p)))) {
policyRead.push(worktreeGitDir);
if (mainGitDir) policyRead.push(mainGitDir);
}
if (policyWrite.some((p) => gitIdentities.has(toPathKey(p)))) {
policyWrite.push(worktreeGitDir);
if (mainGitDir) policyWrite.push(mainGitDir);
}
}
/**
* Filters out any paths that are explicitly forbidden or match the workspace root (original or resolved).
*/
@@ -137,7 +137,3 @@ export const TOPIC_PARAM_STRATEGIC_INTENT = 'strategic_intent';
// -- complete_task --
export const COMPLETE_TASK_TOOL_NAME = 'complete_task';
export const COMPLETE_TASK_DISPLAY_NAME = 'Complete Task';
// -- MCP Resources --
export const READ_MCP_RESOURCE_TOOL_NAME = 'read_mcp_resource';
export const LIST_MCP_RESOURCES_TOOL_NAME = 'list_mcp_resources';
@@ -43,8 +43,6 @@ export {
UPDATE_TOPIC_DISPLAY_NAME,
COMPLETE_TASK_TOOL_NAME,
COMPLETE_TASK_DISPLAY_NAME,
READ_MCP_RESOURCE_TOOL_NAME,
LIST_MCP_RESOURCES_TOOL_NAME,
// Shared parameter names
PARAM_FILE_PATH,
PARAM_DIR_PATH,
@@ -282,17 +280,3 @@ export function getActivateSkillDefinition(
overrides: (modelId) => getToolSet(modelId).activate_skill(skillNames),
};
}
export const READ_MCP_RESOURCE_DEFINITION: ToolDefinition = {
get base() {
return DEFAULT_LEGACY_SET.read_mcp_resource;
},
overrides: (modelId) => getToolSet(modelId).read_mcp_resource,
};
export const LIST_MCP_RESOURCES_DEFINITION: ToolDefinition = {
get base() {
return DEFAULT_LEGACY_SET.list_mcp_resources;
},
overrides: (modelId) => getToolSet(modelId).list_mcp_resources,
};

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