Compare commits
93 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 0ec0c6ec08 | |||
| a04c593eb2 | |||
| 7b1e984a6d | |||
| 1e278c1e5b | |||
| 5453129d9f | |||
| 7178b77527 | |||
| 7f5c805d9d | |||
| 25ee5327f3 | |||
| a15ea53ff8 | |||
| 949392b384 | |||
| 259bf78f65 | |||
| a220874281 | |||
| 49ea9b0457 | |||
| 556825f81c | |||
| b158c96465 | |||
| e91f86c248 | |||
| 47e4f6b13f | |||
| 94ab449e65 | |||
| 0486a1675a | |||
| f9fc83089c | |||
| 5dd2dab189 | |||
| a6b95897ad | |||
| 6ae6c810ba | |||
| 02d4451e77 | |||
| c25ff94608 | |||
| 14412c3a72 | |||
| ec7773eb7b | |||
| 4653b126f3 | |||
| 1e1e7e349d | |||
| 43eb74ac59 | |||
| 215f8f3f15 | |||
| b89944c3a3 | |||
| f88488d1f9 | |||
| 1fd42802be | |||
| ab64b15d51 | |||
| e406dcc249 | |||
| 680077631d | |||
| 9fc03a0c12 | |||
| 4f4431e4e1 | |||
| 527074b50a | |||
| a17691f0fc | |||
| d246315cea | |||
| e92ccec6c8 | |||
| b68d7bc0f9 | |||
| 4c9f9bb3e2 | |||
| e7b20c49ac | |||
| 743d05b37f | |||
| 95074a1a84 | |||
| 759575faa8 | |||
| d485e08606 | |||
| 0f1258305a | |||
| 09e99824d4 | |||
| 96b939f63a | |||
| 7837194ab5 | |||
| 35ee2a841a | |||
| a253938ac5 | |||
| 37ffd608fd | |||
| 0f019122b0 | |||
| 936f6240dd | |||
| f8dd6f4f4c | |||
| ca7ac00030 | |||
| d41735d6a9 | |||
| d012929a28 | |||
| 97dfbd4598 | |||
| 237864eb63 | |||
| dc6741097c | |||
| dac3735626 | |||
| 245b68e9f1 | |||
| e89cf5d86e | |||
| 54b0344fc5 | |||
| e5d58c2b5a | |||
| 9455ecd78c | |||
| 0fd09e0150 | |||
| 9a7427197b | |||
| 33be30ab04 | |||
| 6c3a90645a | |||
| 32e61529f5 | |||
| 931e668b47 | |||
| 7989c28d2e | |||
| 2965c115f1 | |||
| 7d31d5f4ec | |||
| 1a7d4184cd | |||
| 7dce23e5d9 | |||
| 06a176e33e | |||
| 42d367d72f | |||
| 6f579934db | |||
| 6d607a5953 | |||
| 4669148a4c | |||
| 82316ef6e4 | |||
| 337e4bc8c6 | |||
| d97eaf3420 | |||
| 0452f787b2 | |||
| c1317aafb3 |
@@ -107,7 +107,7 @@ Gemini CLI project.
|
||||
set.
|
||||
- **Logging**: Use `debugLogger` for rethrown errors to avoid duplicate logging.
|
||||
- **Keyboard Shortcuts**: Define all new keyboard shortcuts in
|
||||
`packages/cli/src/config/keyBindings.ts` and document them in
|
||||
`packages/cli/src/ui/key/keyBindings.ts` and document them in
|
||||
`docs/cli/keyboard-shortcuts.md`. Be careful of keybindings that require the
|
||||
`Meta` key, as only certain meta key shortcuts are supported on Mac. Avoid
|
||||
function keys and shortcuts commonly bound in VSCode.
|
||||
|
||||
@@ -0,0 +1,99 @@
|
||||
---
|
||||
name: string-reviewer
|
||||
description: >
|
||||
Use this skill when asked to review text and user-facing strings within the codebase. It ensures that these strings follow rules on clarity,
|
||||
usefulness, brevity and style.
|
||||
---
|
||||
|
||||
# String Reviewer
|
||||
|
||||
## Instructions
|
||||
|
||||
Act as a Senior UX Writer. Look for user-facing strings that are too long,
|
||||
unclear, or inconsistent. This includes inline text, error messages, and other
|
||||
user-facing text.
|
||||
|
||||
Do NOT automatically change strings without user approval. You must only suggest
|
||||
changes and do not attempt to rewrite them directly unless the user explicitly
|
||||
asks you to do so.
|
||||
|
||||
## Core voice principles
|
||||
|
||||
The system prioritizes deterministic clarity over conversational fluff. We
|
||||
provide telemetry, not etiquette, ensuring the user retains absolute agency..
|
||||
|
||||
1. **Deterministic clarity:** Distinguish between certain system/service states
|
||||
(Cloud Billing, IAM, the System) and probabilistic AI analysis (Gemini).
|
||||
2. **System transparency:** Replace "Loading..." with active technical telemetry
|
||||
(e.g., Tracing stack traces...). Keep status updates under 5 words.
|
||||
3. **Front-loaded actionability:** Always use the [Goal] + [Action] pattern.
|
||||
Lead with intent so users can scan left-to-right.
|
||||
4. **Agentic error recovery:** Every error must be a pivot point. Pair failures
|
||||
with one-click recovery commands or suggested prompts.
|
||||
5. **Contextual humility:** Reserve disclaimers and "be careful" warnings for P0
|
||||
(destructive/irreversible) tasks only. Stop warning-fatigue.
|
||||
|
||||
## The writing checklist
|
||||
|
||||
Use this checklist to audit UI strings and AI responses.
|
||||
|
||||
### Identity and voice
|
||||
- **Eliminate the "I":** Remove all first-person pronouns (I, me, my, mine).
|
||||
- **Subject attribution:** Refer to the AI as Gemini and the infrastructure as
|
||||
the - system or the CLI.
|
||||
- **Active voice:** Ensure the subject (Gemini or the system) is clearly
|
||||
performing the action.
|
||||
- **Ownership rule:** Use the system for execution (doing) and Gemini for
|
||||
analysis (thinking)
|
||||
|
||||
### Structural scannability
|
||||
- **The skip test:** Do the first 3 words describe the user’s intent? If not,
|
||||
rewrite.
|
||||
- **Goal-first sequence:** Use the template: [To Accomplish X] + [Do Y].
|
||||
- **The 5-word rule:** Keep status updates and loading states under 5 words.
|
||||
- **Telemetry over etiquette:** Remove polite filler (Please wait, Thank you,
|
||||
Certainly). Replace with raw data or progress indicators.
|
||||
- **Micro-state cycles:** For tasks $> 3$ seconds, cycle through specific
|
||||
sub-states (e.g., Parsing logs... ➔ Identifying patterns...) to show momentum.
|
||||
|
||||
|
||||
### Technical accuracy and humility
|
||||
- **Verb signal check:** Use deterministic verbs (is, will, must) for system
|
||||
state/infrastructure.
|
||||
- Use probabilistic verbs (suggests, appears, may, identifies) for AI output.
|
||||
- **No 100% certainty:** Never attribute absolute certainty to model-generated
|
||||
content.
|
||||
- **Precision over fuzziness:** Use technical metrics (latency, tokens, compute) instead of "speed" or "cost."
|
||||
- **Instructional warnings:** Every warning must include a specific corrective action (e.g., "Perform a dry-run first" or "Review line 42").
|
||||
|
||||
### Agentic error recovery
|
||||
- **The one-step rule:** Pair every error message with exactly one immediate
|
||||
path to a fix (command, link, or prompt).
|
||||
- **Human-first:** Provide a human-readable explanation before machine error
|
||||
codes (e.g., 404, 500).
|
||||
- **Suggested prompts:** Offer specific text for the user to copy/click like
|
||||
“Ask Gemini: 'Explain this port error.'”
|
||||
|
||||
### Use consistent terminology
|
||||
|
||||
Ensure all terminology aligns with the project [word
|
||||
list](./references/word-list.md).
|
||||
|
||||
If a string uses a term marked "do not use" or "use with caution," provide a
|
||||
correction based on the preferred terms.
|
||||
|
||||
## Ensure consistent style for settings
|
||||
|
||||
If `packages/cli/src/config/settingsSchema.ts` is modified, confirm labels and
|
||||
descriptions specifically follow the unique [Settings
|
||||
guidelines](./references/settings.md).
|
||||
|
||||
## Output format
|
||||
When suggesting changes, always present your review using the following list
|
||||
format. Do not provide suggestions outside of this list..
|
||||
|
||||
```
|
||||
1. **{Rationale/Principle Violated}**
|
||||
- ❌ "{incorrect phrase}"
|
||||
- ✅ `"{corrected phrase}"`
|
||||
```
|
||||
@@ -0,0 +1,28 @@
|
||||
# Settings
|
||||
|
||||
## Noun-First Labeling (Scannability)
|
||||
|
||||
Labels must start with the subject of the setting, not the action. This allows
|
||||
users to scan for the feature they want to change.
|
||||
|
||||
- **Rule:** `[Noun]` `[Attribute/Action]`
|
||||
- **Example:** `Show line numbers` becomes simply `Line numbers`
|
||||
|
||||
## Positive Boolean Logic (Cognitive Ease)
|
||||
|
||||
Eliminate "double negatives." Booleans should represent the presence of a
|
||||
feature, not its absence.
|
||||
|
||||
- **Rule:** Replace `Disable {feature}` or `Hide {Feature}` with
|
||||
`{Feature} enabled` or simply `{Feature}`.
|
||||
- **Example:** Change "Disable auto update" to "Auto update".
|
||||
- **Implementation:** Invert the boolean value in your config loader so true
|
||||
always equals `On`
|
||||
|
||||
## Verb Stripping (Brevity)
|
||||
|
||||
Remove redundant leading verbs like "Enable," "Use," "Display," or "Show" unless
|
||||
they are part of a specific technical term.
|
||||
|
||||
- **Rule**: If the label works without the verb, remove it
|
||||
- **Example**: Change `Enable prompt completion` to `Prompt completion`
|
||||
@@ -0,0 +1,61 @@
|
||||
## Terms
|
||||
|
||||
### Preferred
|
||||
|
||||
- Use **create** when a user is creating or setting up something.
|
||||
- Use **allow** instead of **may** to indicate that permission has been granted
|
||||
to perform some action.
|
||||
- Use **canceled**, not **cancelled**.
|
||||
- Use **configure** to refer to the process of changing the attributes of a
|
||||
feature, even if that includes turning on or off the feature.
|
||||
- Use **delete** when the action being performed is destructive.
|
||||
- Use **enable** for binary operations that turn a feature or API on. Use "turn
|
||||
on" and "turn off" instead of "enable" and "disable" for other situations.
|
||||
- Use **key combination** to refer to pressing multiple keys simultaneously.
|
||||
- Use **key sequence** to refer to pressing multiple keys separately in order.
|
||||
- Use **modify** to refer to something that has changed vs obtaining the latest
|
||||
version of something.
|
||||
- Use **remove** when the action being performed takes an item out of a larger
|
||||
whole, but doesn't destroy the item itself.
|
||||
- Use **set up** as a verb. Use **setup** as a noun or adjective.
|
||||
- Use **show**. In general, use paired with **hide**.
|
||||
- Use **sign in**, **sign out** as a verb. Use **sign-in** or **sign-out** as a
|
||||
noun or adjective.
|
||||
- Use **update** when you mean to obtain the latest version of something.
|
||||
- Use **want** instead of **like** or **would like**.
|
||||
|
||||
#### Don't use
|
||||
|
||||
- Don't use **etc.** It's redundant. To convey that a series is incomplete,
|
||||
introduce it with "such as" instead.
|
||||
- Don't use **hostname**, use "host name" instead.
|
||||
- Don't use **in order to**. It's too formal. "Before you can" is usually better
|
||||
in UI text.
|
||||
- Don't use **one or more**. Specify the quantity where possible. Use "at least
|
||||
one" when the quantity is 1+ but you can't be sure of the number. Likewise,
|
||||
use "at least one" when the user must choose a quantity of 1+.
|
||||
- Don't use the terms **log in**, **log on**, **login**, **logout** or **log
|
||||
out**.
|
||||
- Don't use **like** or **would you like**. Use **want** instead. Better yet,
|
||||
rephrase so that it's not referring to the user's emotional state, but rather
|
||||
what is required.
|
||||
|
||||
#### Use with caution
|
||||
|
||||
- Avoid using **leverage**, especially as a verb. "Leverage" is considered a
|
||||
buzzword largely devoid of meaning apart from the simpler "use".
|
||||
- Avoid using **once** as a synonym for "after". Typically, when "once" is used
|
||||
in this way, it is followed by a verb in the perfect tense.
|
||||
- Don't use **e.g.** Use "example", "such as", "like", or "for example". The
|
||||
phrase is always followed by a comma.
|
||||
- Don't use **i.e.** unless absolutely essential to make text fit. Use "that is"
|
||||
instead.
|
||||
- Use **disable** for binary operations that turn a feature or API off. Use
|
||||
"turn on" and "turn off" instead of "enable" and "disable" for other
|
||||
situations. For UI elements that are not available, use "dimmed" instead of
|
||||
"disabled".
|
||||
- Use **please** only when you're asking the user to do something inconvenient,
|
||||
not just following the instructions in a typical flow.
|
||||
- Use **really** sparingly in such constructions as "Do you really want to..."
|
||||
Because of the weight it puts on the decision, it should be used to confirm
|
||||
actions that the user is extremely unlikely to make.
|
||||
@@ -193,7 +193,7 @@ runs:
|
||||
INPUTS_A2A_PACKAGE_NAME: '${{ inputs.a2a-package-name }}'
|
||||
|
||||
- name: '📦 Prepare bundled CLI for npm release'
|
||||
if: "inputs.npm-registry-url != 'https://npm.pkg.github.com/'"
|
||||
if: "inputs.npm-registry-url != 'https://npm.pkg.github.com/' && inputs.npm-tag != 'latest'"
|
||||
working-directory: '${{ inputs.working-directory }}'
|
||||
shell: 'bash'
|
||||
run: |
|
||||
|
||||
@@ -169,7 +169,7 @@ jobs:
|
||||
npm run test:ci --workspace @google/gemini-cli
|
||||
else
|
||||
# Explicitly list non-cli packages to ensure they are sharded correctly
|
||||
npm run test:ci --workspace @google/gemini-cli-core --workspace @google/gemini-cli-a2a-server --workspace gemini-cli-vscode-ide-companion --workspace @google/gemini-cli-test-utils --if-present
|
||||
npm run test:ci --workspace @google/gemini-cli-core --workspace @google/gemini-cli-a2a-server --workspace gemini-cli-vscode-ide-companion --workspace @google/gemini-cli-test-utils --if-present -- --coverage.enabled=false
|
||||
npm run test:scripts
|
||||
fi
|
||||
|
||||
|
||||
@@ -62,3 +62,5 @@ gemini-debug.log
|
||||
.gemini-clipboard/
|
||||
.eslintcache
|
||||
evals/logs/
|
||||
|
||||
temp_agents/
|
||||
|
||||
@@ -60,20 +60,45 @@ 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.
|
||||
|
||||
If your pull request involves changes to `packages/cli` (the frontend), we
|
||||
recommend running our automated frontend review tool. **Note: This tool is
|
||||
currently experimental.** It helps detect common React anti-patterns, testing
|
||||
issues, and other frontend-specific 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.
|
||||
|
||||
To run the review tool, enter the following command from within Gemini CLI:
|
||||
#### Using the automated review tool
|
||||
|
||||
```text
|
||||
/review-frontend <PR_NUMBER>
|
||||
```
|
||||
You can run the review tool in two ways:
|
||||
|
||||
Replace `<PR_NUMBER>` with your pull request number. Authors are encouraged to
|
||||
run this on their own PRs for self-review, and reviewers should use it to
|
||||
augment their manual review process.
|
||||
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]
|
||||
```
|
||||
|
||||
**Warning:** If you run `scripts/review.sh`, you must have first verified
|
||||
that the code for the PR being reviewed is safe to run and does not contain
|
||||
data exfiltration attacks.
|
||||
|
||||
**Authors are strongly encouraged to run this script on their own PRs**
|
||||
immediately after creation. This allows you to catch and fix simple issues
|
||||
locally before a maintainer performs a full review.
|
||||
|
||||
**Note on Models:** By default, the script uses the latest Pro model
|
||||
(`gemini-3.1-pro-preview`). If you do not have enough Pro quota, you can run
|
||||
it with the latest Flash model instead:
|
||||
`./scripts/review.sh <PR_NUMBER> gemini-3-flash-preview`.
|
||||
|
||||
2. **Manually from within Gemini CLI:** If you already have the PR checked out
|
||||
and built, you can run the tool directly from the CLI prompt:
|
||||
|
||||
```text
|
||||
/review-frontend <PR_NUMBER>
|
||||
```
|
||||
|
||||
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
|
||||
|
||||
@@ -267,7 +292,8 @@ npm run test:e2e
|
||||
```
|
||||
|
||||
For more detailed information on the integration testing framework, please see
|
||||
the [Integration Tests documentation](/docs/integration-tests.md).
|
||||
the
|
||||
[Integration Tests documentation](https://geminicli.com/docs/integration-tests).
|
||||
|
||||
### Linting and preflight checks
|
||||
|
||||
@@ -320,11 +346,9 @@ npm run lint
|
||||
|
||||
- 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
|
||||
AI-assisted development, including conventions for React, comments, and Git
|
||||
usage.
|
||||
- Consult [GEMINI.md](../GEMINI.md) (typically found in the project root) for
|
||||
specific instructions related to AI-assisted development, including
|
||||
conventions for React, comments, and Git usage.
|
||||
- **Imports:** Pay special attention to import paths. The project uses ESLint to
|
||||
enforce restrictions on relative imports between packages.
|
||||
|
||||
@@ -548,7 +572,7 @@ Before submitting your documentation pull request, please:
|
||||
|
||||
If you have questions about contributing documentation:
|
||||
|
||||
- Check our [FAQ](/docs/resources/faq.md).
|
||||
- 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.
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
[](https://github.com/google-gemini/gemini-cli/blob/main/LICENSE)
|
||||
[](https://codewiki.google/github.com/google-gemini/gemini-cli?utm_source=badge&utm_medium=github&utm_campaign=github.com/google-gemini/gemini-cli)
|
||||
|
||||

|
||||

|
||||
|
||||
Gemini CLI is an open-source AI agent that brings the power of Gemini directly
|
||||
into your terminal. It provides lightweight access to Gemini, giving you the
|
||||
|
||||
|
After Width: | Height: | Size: 154 KiB |
|
Before Width: | Height: | Size: 126 KiB After Width: | Height: | Size: 141 KiB |
|
Before Width: | Height: | Size: 127 KiB |
|
After Width: | Height: | Size: 151 KiB |
|
Before Width: | Height: | Size: 128 KiB |
|
After Width: | Height: | Size: 146 KiB |
|
Before Width: | Height: | Size: 126 KiB After Width: | Height: | Size: 137 KiB |
|
Before Width: | Height: | Size: 128 KiB |
|
After Width: | Height: | Size: 155 KiB |
|
Before Width: | Height: | Size: 125 KiB After Width: | Height: | Size: 144 KiB |
|
Before Width: | Height: | Size: 127 KiB |
|
After Width: | Height: | Size: 156 KiB |
|
Before Width: | Height: | Size: 128 KiB |
|
After Width: | Height: | Size: 154 KiB |
|
Before Width: | Height: | Size: 126 KiB After Width: | Height: | Size: 135 KiB |
|
Before Width: | Height: | Size: 128 KiB |
|
Before Width: | Height: | Size: 126 KiB After Width: | Height: | Size: 134 KiB |
|
After Width: | Height: | Size: 163 KiB |
|
After Width: | Height: | Size: 158 KiB |
|
After Width: | Height: | Size: 146 KiB |
|
After Width: | Height: | Size: 138 KiB |
|
Before Width: | Height: | Size: 125 KiB After Width: | Height: | Size: 120 KiB |
@@ -1,6 +1,6 @@
|
||||
# Preview release: v0.33.0-preview.3
|
||||
# Preview release: v0.33.0-preview.4
|
||||
|
||||
Released: March 05, 2026
|
||||
Released: March 06, 2026
|
||||
|
||||
Our preview release includes the latest, new, and experimental features. This
|
||||
release may not be as stable as our [latest weekly release](latest.md).
|
||||
@@ -29,163 +29,173 @@ npm install -g @google/gemini-cli@preview
|
||||
|
||||
## What's Changed
|
||||
|
||||
- fix(patch): cherry-pick 7ec477d to release/v0.33.0-preview.3-pr-21305 to patch
|
||||
version v0.33.0-preview.3 and create version 0.33.0-preview.4 by
|
||||
@gemini-cli-robot in
|
||||
[#21349](https://github.com/google-gemini/gemini-cli/pull/21349)
|
||||
- fix(patch): cherry-pick 0135b03 to release/v0.33.0-preview.2-pr-21171
|
||||
[CONFLICTS] by @gemini-cli-robot in
|
||||
[#21336](https://github.com/google-gemini/gemini-cli/pull/21336)
|
||||
|
||||
* fix(patch): cherry-pick 0659ad1 to release/v0.33.0-preview.0-pr-21042 to patch
|
||||
- fix(patch): cherry-pick 173376b to release/v0.33.0-preview.1-pr-21157 to patch
|
||||
version v0.33.0-preview.1 and create version 0.33.0-preview.2 by
|
||||
@gemini-cli-robot in
|
||||
[#21300](https://github.com/google-gemini/gemini-cli/pull/21300)
|
||||
- fix(patch): cherry-pick 173376b to release/v0.33.0-preview.1-pr-21157 to patch
|
||||
version v0.33.0-preview.1 and create version 0.33.0-preview.2 by
|
||||
@gemini-cli-robot in
|
||||
[#21300](https://github.com/google-gemini/gemini-cli/pull/21300)
|
||||
- fix(patch): cherry-pick 0659ad1 to release/v0.33.0-preview.0-pr-21042 to patch
|
||||
version v0.33.0-preview.0 and create version 0.33.0-preview.1 by
|
||||
@gemini-cli-robot in
|
||||
[#21047](https://github.com/google-gemini/gemini-cli/pull/21047)
|
||||
|
||||
* Docs: Update model docs to remove Preview Features. by @jkcinouye in
|
||||
- Docs: Update model docs to remove Preview Features. by @jkcinouye in
|
||||
[#20084](https://github.com/google-gemini/gemini-cli/pull/20084)
|
||||
* docs: fix typo in installation documentation by @AdityaSharma-Git3207 in
|
||||
- docs: fix typo in installation documentation by @AdityaSharma-Git3207 in
|
||||
[#20153](https://github.com/google-gemini/gemini-cli/pull/20153)
|
||||
* docs: add Windows PowerShell equivalents for environments and scripting by
|
||||
- docs: add Windows PowerShell equivalents for environments and scripting by
|
||||
@scidomino in [#20333](https://github.com/google-gemini/gemini-cli/pull/20333)
|
||||
* fix(core): parse raw ASCII buffer strings in Gaxios errors by @sehoon38 in
|
||||
- fix(core): parse raw ASCII buffer strings in Gaxios errors by @sehoon38 in
|
||||
[#20626](https://github.com/google-gemini/gemini-cli/pull/20626)
|
||||
* chore(release): bump version to 0.33.0-nightly.20260227.ba149afa0 by @galz10
|
||||
- chore(release): bump version to 0.33.0-nightly.20260227.ba149afa0 by @galz10
|
||||
in [#20637](https://github.com/google-gemini/gemini-cli/pull/20637)
|
||||
* fix(github): use robot PAT for automated PRs to pass CLA check by @galz10 in
|
||||
- fix(github): use robot PAT for automated PRs to pass CLA check by @galz10 in
|
||||
[#20641](https://github.com/google-gemini/gemini-cli/pull/20641)
|
||||
* chore/release: bump version to 0.33.0-nightly.20260228.1ca5c05d0 by
|
||||
- chore/release: bump version to 0.33.0-nightly.20260228.1ca5c05d0 by
|
||||
@gemini-cli-robot in
|
||||
[#20644](https://github.com/google-gemini/gemini-cli/pull/20644)
|
||||
* Changelog for v0.31.0 by @gemini-cli-robot in
|
||||
- Changelog for v0.31.0 by @gemini-cli-robot in
|
||||
[#20634](https://github.com/google-gemini/gemini-cli/pull/20634)
|
||||
* fix: use full paths for ACP diff payloads by @JagjeevanAK in
|
||||
- fix: use full paths for ACP diff payloads by @JagjeevanAK in
|
||||
[#19539](https://github.com/google-gemini/gemini-cli/pull/19539)
|
||||
* Changelog for v0.32.0-preview.0 by @gemini-cli-robot in
|
||||
- Changelog for v0.32.0-preview.0 by @gemini-cli-robot in
|
||||
[#20627](https://github.com/google-gemini/gemini-cli/pull/20627)
|
||||
* fix: acp/zed race condition between MCP initialisation and prompt by
|
||||
- fix: acp/zed race condition between MCP initialisation and prompt by
|
||||
@kartikangiras in
|
||||
[#20205](https://github.com/google-gemini/gemini-cli/pull/20205)
|
||||
* fix(cli): reset themeManager between tests to ensure isolation by
|
||||
- fix(cli): reset themeManager between tests to ensure isolation by
|
||||
@NTaylorMullen in
|
||||
[#20598](https://github.com/google-gemini/gemini-cli/pull/20598)
|
||||
* refactor(core): Extract tool parameter names as constants by @SandyTao520 in
|
||||
- refactor(core): Extract tool parameter names as constants by @SandyTao520 in
|
||||
[#20460](https://github.com/google-gemini/gemini-cli/pull/20460)
|
||||
* fix(cli): resolve autoThemeSwitching when background hasn't changed but theme
|
||||
- fix(cli): resolve autoThemeSwitching when background hasn't changed but theme
|
||||
mismatches by @sehoon38 in
|
||||
[#20706](https://github.com/google-gemini/gemini-cli/pull/20706)
|
||||
* feat(skills): add github-issue-creator skill by @sehoon38 in
|
||||
- feat(skills): add github-issue-creator skill by @sehoon38 in
|
||||
[#20709](https://github.com/google-gemini/gemini-cli/pull/20709)
|
||||
* fix(cli): allow sub-agent confirmation requests in UI while preventing
|
||||
- fix(cli): allow sub-agent confirmation requests in UI while preventing
|
||||
background flicker by @abhipatel12 in
|
||||
[#20722](https://github.com/google-gemini/gemini-cli/pull/20722)
|
||||
* Merge User and Agent Card Descriptions #20849 by @adamfweidman in
|
||||
- Merge User and Agent Card Descriptions #20849 by @adamfweidman in
|
||||
[#20850](https://github.com/google-gemini/gemini-cli/pull/20850)
|
||||
* fix(core): reduce LLM-based loop detection false positives by @SandyTao520 in
|
||||
- fix(core): reduce LLM-based loop detection false positives by @SandyTao520 in
|
||||
[#20701](https://github.com/google-gemini/gemini-cli/pull/20701)
|
||||
* fix(plan): deflake plan mode integration tests by @Adib234 in
|
||||
- fix(plan): deflake plan mode integration tests by @Adib234 in
|
||||
[#20477](https://github.com/google-gemini/gemini-cli/pull/20477)
|
||||
* Add /unassign support by @scidomino in
|
||||
- Add /unassign support by @scidomino in
|
||||
[#20864](https://github.com/google-gemini/gemini-cli/pull/20864)
|
||||
* feat(core): implement HTTP authentication support for A2A remote agents by
|
||||
- feat(core): implement HTTP authentication support for A2A remote agents by
|
||||
@SandyTao520 in
|
||||
[#20510](https://github.com/google-gemini/gemini-cli/pull/20510)
|
||||
* feat(core): centralize read_file limits and update gemini-3 description by
|
||||
- feat(core): centralize read_file limits and update gemini-3 description by
|
||||
@aishaneeshah in
|
||||
[#20619](https://github.com/google-gemini/gemini-cli/pull/20619)
|
||||
* Do not block CI on evals by @gundermanc in
|
||||
- Do not block CI on evals by @gundermanc in
|
||||
[#20870](https://github.com/google-gemini/gemini-cli/pull/20870)
|
||||
* document node limitation for shift+tab by @scidomino in
|
||||
- document node limitation for shift+tab by @scidomino in
|
||||
[#20877](https://github.com/google-gemini/gemini-cli/pull/20877)
|
||||
* Add install as an option when extension is selected. by @DavidAPierce in
|
||||
- Add install as an option when extension is selected. by @DavidAPierce in
|
||||
[#20358](https://github.com/google-gemini/gemini-cli/pull/20358)
|
||||
* Update CODEOWNERS for README.md reviewers by @g-samroberts in
|
||||
- Update CODEOWNERS for README.md reviewers by @g-samroberts in
|
||||
[#20860](https://github.com/google-gemini/gemini-cli/pull/20860)
|
||||
* feat(core): truncate large MCP tool output by @SandyTao520 in
|
||||
- feat(core): truncate large MCP tool output by @SandyTao520 in
|
||||
[#19365](https://github.com/google-gemini/gemini-cli/pull/19365)
|
||||
* Subagent activity UX. by @gundermanc in
|
||||
- Subagent activity UX. by @gundermanc in
|
||||
[#17570](https://github.com/google-gemini/gemini-cli/pull/17570)
|
||||
* style(cli) : Dialog pattern for /hooks Command by @AbdulTawabJuly in
|
||||
- style(cli) : Dialog pattern for /hooks Command by @AbdulTawabJuly in
|
||||
[#17930](https://github.com/google-gemini/gemini-cli/pull/17930)
|
||||
* feat: redesign header to be compact with ASCII icon by @keithguerin in
|
||||
- feat: redesign header to be compact with ASCII icon by @keithguerin in
|
||||
[#18713](https://github.com/google-gemini/gemini-cli/pull/18713)
|
||||
* fix(core): ensure subagents use qualified MCP tool names by @abhipatel12 in
|
||||
- fix(core): ensure subagents use qualified MCP tool names by @abhipatel12 in
|
||||
[#20801](https://github.com/google-gemini/gemini-cli/pull/20801)
|
||||
* feat(core): support authenticated A2A agent card discovery by @SandyTao520 in
|
||||
- feat(core): support authenticated A2A agent card discovery by @SandyTao520 in
|
||||
[#20622](https://github.com/google-gemini/gemini-cli/pull/20622)
|
||||
* refactor(cli): fully remove React anti patterns, improve type safety and fix
|
||||
- refactor(cli): fully remove React anti patterns, improve type safety and fix
|
||||
UX oversights in SettingsDialog.tsx by @psinha40898 in
|
||||
[#18963](https://github.com/google-gemini/gemini-cli/pull/18963)
|
||||
* Adding MCPOAuthProvider implementing the MCPSDK OAuthClientProvider by
|
||||
- Adding MCPOAuthProvider implementing the MCPSDK OAuthClientProvider by
|
||||
@Nayana-Parameswarappa in
|
||||
[#20121](https://github.com/google-gemini/gemini-cli/pull/20121)
|
||||
* feat(core): add tool name validation in TOML policy files by @allenhutchison
|
||||
- feat(core): add tool name validation in TOML policy files by @allenhutchison
|
||||
in [#19281](https://github.com/google-gemini/gemini-cli/pull/19281)
|
||||
* docs: fix broken markdown links in main README.md by @Hamdanbinhashim in
|
||||
- docs: fix broken markdown links in main README.md by @Hamdanbinhashim in
|
||||
[#20300](https://github.com/google-gemini/gemini-cli/pull/20300)
|
||||
* refactor(core): replace manual syncPlanModeTools with declarative policy rules
|
||||
- refactor(core): replace manual syncPlanModeTools with declarative policy rules
|
||||
by @jerop in [#20596](https://github.com/google-gemini/gemini-cli/pull/20596)
|
||||
* fix(core): increase default headers timeout to 5 minutes by @gundermanc in
|
||||
- fix(core): increase default headers timeout to 5 minutes by @gundermanc in
|
||||
[#20890](https://github.com/google-gemini/gemini-cli/pull/20890)
|
||||
* feat(admin): enable 30 day default retention for chat history & remove warning
|
||||
- feat(admin): enable 30 day default retention for chat history & remove warning
|
||||
by @skeshive in
|
||||
[#20853](https://github.com/google-gemini/gemini-cli/pull/20853)
|
||||
* feat(plan): support annotating plans with feedback for iteration by @Adib234
|
||||
- feat(plan): support annotating plans with feedback for iteration by @Adib234
|
||||
in [#20876](https://github.com/google-gemini/gemini-cli/pull/20876)
|
||||
* Add some dos and don'ts to behavioral evals README. by @gundermanc in
|
||||
- Add some dos and don'ts to behavioral evals README. by @gundermanc in
|
||||
[#20629](https://github.com/google-gemini/gemini-cli/pull/20629)
|
||||
* fix(core): skip telemetry logging for AbortError exceptions by @yunaseoul in
|
||||
- fix(core): skip telemetry logging for AbortError exceptions by @yunaseoul in
|
||||
[#19477](https://github.com/google-gemini/gemini-cli/pull/19477)
|
||||
* fix(core): restrict "System: Please continue" invalid stream retry to Gemini 2
|
||||
- fix(core): restrict "System: Please continue" invalid stream retry to Gemini 2
|
||||
models by @SandyTao520 in
|
||||
[#20897](https://github.com/google-gemini/gemini-cli/pull/20897)
|
||||
* ci(evals): only run evals in CI if prompts or tools changed by @gundermanc in
|
||||
- ci(evals): only run evals in CI if prompts or tools changed by @gundermanc in
|
||||
[#20898](https://github.com/google-gemini/gemini-cli/pull/20898)
|
||||
* Build binary by @aswinashok44 in
|
||||
- Build binary by @aswinashok44 in
|
||||
[#18933](https://github.com/google-gemini/gemini-cli/pull/18933)
|
||||
* Code review fixes as a pr by @jacob314 in
|
||||
- Code review fixes as a pr by @jacob314 in
|
||||
[#20612](https://github.com/google-gemini/gemini-cli/pull/20612)
|
||||
* fix(ci): handle empty APP_ID in stale PR closer by @bdmorgan in
|
||||
- fix(ci): handle empty APP_ID in stale PR closer by @bdmorgan in
|
||||
[#20919](https://github.com/google-gemini/gemini-cli/pull/20919)
|
||||
* feat(cli): invert context window display to show usage by @keithguerin in
|
||||
- feat(cli): invert context window display to show usage by @keithguerin in
|
||||
[#20071](https://github.com/google-gemini/gemini-cli/pull/20071)
|
||||
* fix(plan): clean up session directories and plans on deletion by @jerop in
|
||||
- fix(plan): clean up session directories and plans on deletion by @jerop in
|
||||
[#20914](https://github.com/google-gemini/gemini-cli/pull/20914)
|
||||
* fix(core): enforce optionality for API response fields in code_assist by
|
||||
- fix(core): enforce optionality for API response fields in code_assist by
|
||||
@sehoon38 in [#20714](https://github.com/google-gemini/gemini-cli/pull/20714)
|
||||
* feat(extensions): add support for plan directory in extension manifest by
|
||||
- feat(extensions): add support for plan directory in extension manifest by
|
||||
@mahimashanware in
|
||||
[#20354](https://github.com/google-gemini/gemini-cli/pull/20354)
|
||||
* feat(plan): enable built-in research subagents in plan mode by @Adib234 in
|
||||
- feat(plan): enable built-in research subagents in plan mode by @Adib234 in
|
||||
[#20972](https://github.com/google-gemini/gemini-cli/pull/20972)
|
||||
* feat(agents): directly indicate auth required state by @adamfweidman in
|
||||
- feat(agents): directly indicate auth required state by @adamfweidman in
|
||||
[#20986](https://github.com/google-gemini/gemini-cli/pull/20986)
|
||||
* fix(cli): wait for background auto-update before relaunching by @scidomino in
|
||||
- fix(cli): wait for background auto-update before relaunching by @scidomino in
|
||||
[#20904](https://github.com/google-gemini/gemini-cli/pull/20904)
|
||||
* fix: pre-load @scripts/copy_files.js references from external editor prompts
|
||||
- fix: pre-load @scripts/copy_files.js references from external editor prompts
|
||||
by @kartikangiras in
|
||||
[#20963](https://github.com/google-gemini/gemini-cli/pull/20963)
|
||||
* feat(evals): add behavioral evals for ask_user tool by @Adib234 in
|
||||
- feat(evals): add behavioral evals for ask_user tool by @Adib234 in
|
||||
[#20620](https://github.com/google-gemini/gemini-cli/pull/20620)
|
||||
* refactor common settings logic for skills,agents by @ishaanxgupta in
|
||||
- refactor common settings logic for skills,agents by @ishaanxgupta in
|
||||
[#17490](https://github.com/google-gemini/gemini-cli/pull/17490)
|
||||
* Update docs-writer skill with new resource by @g-samroberts in
|
||||
- Update docs-writer skill with new resource by @g-samroberts in
|
||||
[#20917](https://github.com/google-gemini/gemini-cli/pull/20917)
|
||||
* fix(cli): pin clipboardy to ~5.2.x by @scidomino in
|
||||
- fix(cli): pin clipboardy to ~5.2.x by @scidomino in
|
||||
[#21009](https://github.com/google-gemini/gemini-cli/pull/21009)
|
||||
* feat: Implement slash command handling in ACP for
|
||||
- feat: Implement slash command handling in ACP for
|
||||
`/memory`,`/init`,`/extensions` and `/restore` by @sripasg in
|
||||
[#20528](https://github.com/google-gemini/gemini-cli/pull/20528)
|
||||
* Docs/add hooks reference by @AadithyaAle in
|
||||
- Docs/add hooks reference by @AadithyaAle in
|
||||
[#20961](https://github.com/google-gemini/gemini-cli/pull/20961)
|
||||
* feat(plan): add copy subcommand to plan (#20491) by @ruomengz in
|
||||
- feat(plan): add copy subcommand to plan (#20491) by @ruomengz in
|
||||
[#20988](https://github.com/google-gemini/gemini-cli/pull/20988)
|
||||
* fix(core): sanitize and length-check MCP tool qualified names by @abhipatel12
|
||||
- fix(core): sanitize and length-check MCP tool qualified names by @abhipatel12
|
||||
in [#20987](https://github.com/google-gemini/gemini-cli/pull/20987)
|
||||
* Format the quota/limit style guide. by @g-samroberts in
|
||||
- Format the quota/limit style guide. by @g-samroberts in
|
||||
[#21017](https://github.com/google-gemini/gemini-cli/pull/21017)
|
||||
* fix(core): send shell output to model on cancel by @devr0306 in
|
||||
- fix(core): send shell output to model on cancel by @devr0306 in
|
||||
[#20501](https://github.com/google-gemini/gemini-cli/pull/20501)
|
||||
* remove hardcoded tiername when missing tier by @sehoon38 in
|
||||
- remove hardcoded tiername when missing tier by @sehoon38 in
|
||||
[#21022](https://github.com/google-gemini/gemini-cli/pull/21022)
|
||||
* feat(acp): add set models interface by @skeshive in
|
||||
- feat(acp): add set models interface by @skeshive in
|
||||
[#20991](https://github.com/google-gemini/gemini-cli/pull/20991)
|
||||
|
||||
**Full Changelog**:
|
||||
https://github.com/google-gemini/gemini-cli/compare/v0.32.0-preview.0...v0.33.0-preview.3
|
||||
https://github.com/google-gemini/gemini-cli/compare/v0.32.0-preview.0...v0.33.0-preview.4
|
||||
|
||||
@@ -8,7 +8,8 @@ and parameters.
|
||||
| Command | Description | Example |
|
||||
| ---------------------------------- | ---------------------------------- | ------------------------------------------------------------ |
|
||||
| `gemini` | Start interactive REPL | `gemini` |
|
||||
| `gemini "query"` | Query non-interactively, then exit | `gemini "explain this project"` |
|
||||
| `gemini -p "query"` | Query non-interactively | `gemini -p "summarize README.md"` |
|
||||
| `gemini "query"` | Query and continue interactively | `gemini "explain this project"` |
|
||||
| `cat file \| gemini` | Process piped content | `cat logs.txt \| gemini`<br>`Get-Content logs.txt \| gemini` |
|
||||
| `gemini -i "query"` | Execute and continue interactively | `gemini -i "What is the purpose of this project?"` |
|
||||
| `gemini -r "latest"` | Continue most recent session | `gemini -r "latest"` |
|
||||
@@ -20,9 +21,24 @@ and parameters.
|
||||
|
||||
### Positional arguments
|
||||
|
||||
| Argument | Type | Description |
|
||||
| -------- | ----------------- | ------------------------------------------------------------------------------------------------------------------ |
|
||||
| `query` | string (variadic) | Positional prompt. Defaults to one-shot mode. Use `-i/--prompt-interactive` to execute and continue interactively. |
|
||||
| Argument | Type | Description |
|
||||
| -------- | ----------------- | ---------------------------------------------------------------------------------------------------------- |
|
||||
| `query` | string (variadic) | Positional prompt. Defaults to interactive mode in a TTY. Use `-p/--prompt` for non-interactive execution. |
|
||||
|
||||
## Interactive commands
|
||||
|
||||
These commands are available within the interactive REPL.
|
||||
|
||||
| Command | Description |
|
||||
| -------------------- | ---------------------------------------- |
|
||||
| `/skills reload` | Reload discovered skills from disk |
|
||||
| `/agents reload` | Reload the agent registry |
|
||||
| `/commands reload` | Reload custom slash commands |
|
||||
| `/memory reload` | Reload context files (e.g., `GEMINI.md`) |
|
||||
| `/mcp reload` | Restart and reload MCP servers |
|
||||
| `/extensions reload` | Reload all active extensions |
|
||||
| `/help` | Show help for all commands |
|
||||
| `/quit` | Exit the interactive session |
|
||||
|
||||
## CLI Options
|
||||
|
||||
@@ -32,7 +48,7 @@ and parameters.
|
||||
| `--version` | `-v` | - | - | Show CLI version number and exit |
|
||||
| `--help` | `-h` | - | - | Show help information |
|
||||
| `--model` | `-m` | string | `auto` | Model to use. See [Model Selection](#model-selection) for available values. |
|
||||
| `--prompt` | `-p` | string | - | Prompt text. Appended to stdin input if provided. **Deprecated:** Use positional arguments instead. |
|
||||
| `--prompt` | `-p` | string | - | Prompt text. Appended to stdin input if provided. Forces non-interactive mode. |
|
||||
| `--prompt-interactive` | `-i` | string | - | Execute prompt and continue in interactive mode |
|
||||
| `--sandbox` | `-s` | boolean | `false` | Run in a sandboxed environment for safer execution |
|
||||
| `--approval-mode` | - | string | `default` | Approval mode for tool execution. Choices: `default`, `auto_edit`, `yolo` |
|
||||
|
||||
@@ -63,7 +63,7 @@ You can interact with the loaded context files by using the `/memory` command.
|
||||
- **`/memory show`**: Displays the full, concatenated content of the current
|
||||
hierarchical memory. This lets you inspect the exact instructional context
|
||||
being provided to the model.
|
||||
- **`/memory refresh`**: Forces a re-scan and reload of all `GEMINI.md` files
|
||||
- **`/memory reload`**: Forces a re-scan and reload of all `GEMINI.md` files
|
||||
from all configured locations.
|
||||
- **`/memory add <text>`**: Appends your text to your global
|
||||
`~/.gemini/GEMINI.md` file. This lets you add persistent memories on the fly.
|
||||
|
||||
@@ -6,7 +6,7 @@ structured text or JSON output without an interactive terminal UI.
|
||||
## Technical reference
|
||||
|
||||
Headless mode is triggered when the CLI is run in a non-TTY environment or when
|
||||
providing a query as a positional argument without the interactive flag.
|
||||
providing a query with the `-p` (or `--prompt`) flag.
|
||||
|
||||
### Output formats
|
||||
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
# Notifications (experimental)
|
||||
|
||||
Gemini CLI can send system notifications to alert you when a session completes
|
||||
or when it needs your attention, such as when it's waiting for you to approve a
|
||||
tool call.
|
||||
|
||||
> **Note:** This is a preview feature currently under active development.
|
||||
> Preview features may be available on the **Preview** channel or may need to be
|
||||
> enabled under `/settings`.
|
||||
|
||||
Notifications are particularly useful when running long-running tasks or using
|
||||
[Plan Mode](./plan-mode.md), letting you switch to other windows while Gemini
|
||||
CLI works in the background.
|
||||
|
||||
## Requirements
|
||||
|
||||
Currently, system notifications are only supported on macOS.
|
||||
|
||||
### Terminal support
|
||||
|
||||
The CLI uses the OSC 9 terminal escape sequence to trigger system notifications.
|
||||
This is supported by several modern terminal emulators. If your terminal does
|
||||
not support OSC 9 notifications, Gemini CLI falls back to a system alert sound
|
||||
to get your attention.
|
||||
|
||||
## Enable notifications
|
||||
|
||||
Notifications are disabled by default. You can enable them using the `/settings`
|
||||
command or by updating your `settings.json` file.
|
||||
|
||||
1. Open the settings dialog by typing `/settings` in an interactive session.
|
||||
2. Navigate to the **General** category.
|
||||
3. Toggle the **Enable Notifications** setting to **On**.
|
||||
|
||||
Alternatively, add the following to your `settings.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"general": {
|
||||
"enableNotifications": true
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Types of notifications
|
||||
|
||||
Gemini CLI sends notifications for the following events:
|
||||
|
||||
- **Action required:** Triggered when the model is waiting for user input or
|
||||
tool approval. This helps you know when the CLI has paused and needs you to
|
||||
intervene.
|
||||
- **Session complete:** Triggered when a session finishes successfully. This is
|
||||
useful for tracking the completion of automated tasks.
|
||||
|
||||
## Next steps
|
||||
|
||||
- Start planning with [Plan Mode](./plan-mode.md).
|
||||
- Configure your experience with other [settings](./settings.md).
|
||||
@@ -1,4 +1,4 @@
|
||||
# Plan Mode (experimental)
|
||||
# Plan Mode
|
||||
|
||||
Plan Mode is a read-only environment for architecting robust solutions before
|
||||
implementation. With Plan Mode, you can:
|
||||
@@ -8,27 +8,8 @@ implementation. With Plan Mode, you can:
|
||||
- **Design:** Understand problems, evaluate trade-offs, and choose a solution.
|
||||
- **Plan:** Align on an execution strategy before any code is modified.
|
||||
|
||||
> **Note:** This is a preview feature currently under active development. Your
|
||||
> feedback is invaluable as we refine this feature. If you have ideas,
|
||||
> suggestions, or encounter issues:
|
||||
>
|
||||
> - [Open an issue] on GitHub.
|
||||
> - Use the **/bug** command within Gemini CLI to file an issue.
|
||||
|
||||
## How to enable Plan Mode
|
||||
|
||||
Enable Plan Mode in **Settings** or by editing your configuration file.
|
||||
|
||||
- **Settings:** Use the `/settings` command and set **Plan** to `true`.
|
||||
- **Configuration:** Add the following to your `settings.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"experimental": {
|
||||
"plan": true
|
||||
}
|
||||
}
|
||||
```
|
||||
Plan Mode is enabled by default. You can manage this setting using the
|
||||
`/settings` command.
|
||||
|
||||
## How to enter Plan Mode
|
||||
|
||||
@@ -62,8 +43,11 @@ To start Plan Mode while using Gemini CLI:
|
||||
- **Command:** Type `/plan` in the input box.
|
||||
|
||||
- **Natural Language:** Ask Gemini CLI to "start a plan for...". Gemini CLI
|
||||
calls the [`enter_plan_mode`] tool to switch modes.
|
||||
> **Note:** This tool is not available when Gemini CLI is in [YOLO mode].
|
||||
calls the
|
||||
[`enter_plan_mode`](../tools/planning.md#1-enter_plan_mode-enterplanmode) tool
|
||||
to switch modes.
|
||||
> **Note:** This tool is not available when Gemini CLI is in
|
||||
> [YOLO mode](../reference/configuration.md#command-line-arguments).
|
||||
|
||||
## How to use Plan Mode
|
||||
|
||||
@@ -74,7 +58,8 @@ Gemini CLI takes action.
|
||||
will then enter Plan Mode (if it's not already) to research the task.
|
||||
2. **Review research and provide input:** As Gemini CLI analyzes your codebase,
|
||||
it may ask you questions or present different implementation options using
|
||||
[`ask_user`]. Provide your preferences to help guide the design.
|
||||
[`ask_user`](../tools/ask-user.md). Provide your preferences to help guide
|
||||
the design.
|
||||
3. **Review the plan:** Once Gemini CLI has a proposed strategy, it creates a
|
||||
detailed implementation plan as a Markdown file in your plans directory. You
|
||||
can open and read this file to understand the proposed changes.
|
||||
@@ -116,25 +101,33 @@ Plan Mode enforces strict safety policies to prevent accidental changes.
|
||||
|
||||
These are the only allowed tools:
|
||||
|
||||
- **FileSystem (Read):** [`read_file`], [`list_directory`], [`glob`]
|
||||
- **Search:** [`grep_search`], [`google_web_search`]
|
||||
- **Research Subagents:** [`codebase_investigator`], [`cli_help`]
|
||||
- **Interaction:** [`ask_user`]
|
||||
- **MCP tools (Read):** Read-only [MCP tools] (for example, `github_read_issue`,
|
||||
`postgres_read_schema`) are allowed.
|
||||
- **Planning (Write):** [`write_file`] and [`replace`] only allowed for `.md`
|
||||
- **FileSystem (Read):**
|
||||
[`read_file`](../tools/file-system.md#2-read_file-readfile),
|
||||
[`list_directory`](../tools/file-system.md#1-list_directory-readfolder),
|
||||
[`glob`](../tools/file-system.md#4-glob-findfiles)
|
||||
- **Search:** [`grep_search`](../tools/file-system.md#5-grep_search-searchtext),
|
||||
[`google_web_search`](../tools/web-search.md)
|
||||
- **Research Subagents:**
|
||||
[`codebase_investigator`](../core/subagents.md#codebase-investigator),
|
||||
[`cli_help`](../core/subagents.md#cli-help-agent)
|
||||
- **Interaction:** [`ask_user`](../tools/ask-user.md)
|
||||
- **MCP tools (Read):** Read-only [MCP tools](../tools/mcp-server.md) (for
|
||||
example, `github_read_issue`, `postgres_read_schema`) are allowed.
|
||||
- **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`
|
||||
files in the `~/.gemini/tmp/<project>/<session-id>/plans/` directory or your
|
||||
[custom plans directory](#custom-plan-directory-and-policies).
|
||||
- **Memory:** [`save_memory`]
|
||||
- **Skills:** [`activate_skill`] (allows loading specialized instructions and
|
||||
resources in a read-only manner)
|
||||
- **Memory:** [`save_memory`](../tools/memory.md)
|
||||
- **Skills:** [`activate_skill`](../cli/skills.md) (allows loading specialized
|
||||
instructions and resources in a read-only manner)
|
||||
|
||||
### Custom planning with skills
|
||||
|
||||
You can use [Agent Skills] to customize how Gemini CLI approaches planning for
|
||||
specific types of tasks. When a skill is activated during Plan Mode, its
|
||||
specialized instructions and procedural workflows will guide the research,
|
||||
design, and planning phases.
|
||||
You can use [Agent Skills](../cli/skills.md) to customize how Gemini CLI
|
||||
approaches planning for specific types of tasks. When a skill is activated
|
||||
during Plan Mode, its specialized instructions and procedural workflows will
|
||||
guide the research, design, and planning phases.
|
||||
|
||||
For example:
|
||||
|
||||
@@ -151,10 +144,32 @@ based on the task description.
|
||||
|
||||
### Custom policies
|
||||
|
||||
Plan Mode's default tool restrictions are managed by the [policy engine] and
|
||||
defined in the built-in [`plan.toml`] file. The built-in policy (Tier 1)
|
||||
enforces the read-only state, but you can customize these rules by creating your
|
||||
own policies in your `~/.gemini/policies/` directory (Tier 2).
|
||||
Plan Mode's default tool restrictions are managed by the
|
||||
[policy engine](../reference/policy-engine.md) and defined in the built-in
|
||||
[`plan.toml`] file. The built-in policy (Tier 1) enforces the read-only state,
|
||||
but you can customize these rules by creating your own policies in your
|
||||
`~/.gemini/policies/` directory (Tier 2).
|
||||
|
||||
#### Global vs. mode-specific rules
|
||||
|
||||
As described in the
|
||||
[policy engine documentation](../reference/policy-engine.md#approval-modes), any
|
||||
rule that does not explicitly specify `modes` is considered "always active" and
|
||||
will apply to Plan Mode as well.
|
||||
|
||||
If you want a rule to apply to other modes but _not_ to Plan Mode, you must
|
||||
explicitly specify the target modes. For example, to allow `npm test` in default
|
||||
and Auto-Edit modes but not in Plan Mode:
|
||||
|
||||
```toml
|
||||
[[rule]]
|
||||
toolName = "run_shell_command"
|
||||
commandPrefix = "npm test"
|
||||
decision = "allow"
|
||||
priority = 100
|
||||
# By omitting "plan", this rule will not be active in Plan Mode.
|
||||
modes = ["default", "autoEdit"]
|
||||
```
|
||||
|
||||
#### Example: Automatically approve read-only MCP tools
|
||||
|
||||
@@ -173,8 +188,8 @@ priority = 100
|
||||
modes = ["plan"]
|
||||
```
|
||||
|
||||
For more information on how the policy engine works, see the [policy engine]
|
||||
docs.
|
||||
For more information on how the policy engine works, see the
|
||||
[policy engine](../reference/policy-engine.md) docs.
|
||||
|
||||
#### Example: Allow git commands in Plan Mode
|
||||
|
||||
@@ -194,9 +209,12 @@ modes = ["plan"]
|
||||
|
||||
#### Example: Enable custom subagents in Plan Mode
|
||||
|
||||
Built-in research [subagents] like [`codebase_investigator`] and [`cli_help`]
|
||||
are enabled by default in Plan Mode. You can enable additional [custom
|
||||
subagents] by adding a rule to your policy.
|
||||
Built-in research [subagents](../core/subagents.md) like
|
||||
[`codebase_investigator`](../core/subagents.md#codebase-investigator) and
|
||||
[`cli_help`](../core/subagents.md#cli-help-agent) are enabled by default in Plan
|
||||
Mode. You can enable additional
|
||||
[custom subagents](../core/subagents.md#creating-custom-subagents) by adding a
|
||||
rule to your policy.
|
||||
|
||||
`~/.gemini/policies/research-subagents.toml`
|
||||
|
||||
@@ -235,10 +253,11 @@ locations defined within a project's workspace cannot be used to escape and
|
||||
overwrite sensitive files elsewhere. Any user-configured directory must reside
|
||||
within the project boundary.
|
||||
|
||||
Using a custom directory requires updating your [policy engine] configurations
|
||||
to allow `write_file` and `replace` in that specific location. For example, to
|
||||
allow writing to the `.gemini/plans` directory within your project, create a
|
||||
policy file at `~/.gemini/policies/plan-custom-directory.toml`:
|
||||
Using a custom directory requires updating your
|
||||
[policy engine](../reference/policy-engine.md) configurations to allow
|
||||
`write_file` and `replace` in that specific location. For example, to allow
|
||||
writing to the `.gemini/plans` directory within your project, create a policy
|
||||
file at `~/.gemini/policies/plan-custom-directory.toml`:
|
||||
|
||||
```toml
|
||||
[[rule]]
|
||||
@@ -254,13 +273,16 @@ argsPattern = "\"file_path\":\"[^\"]+[\\\\/]+\\.gemini[\\\\/]+plans[\\\\/]+[\\w-
|
||||
## Planning workflows
|
||||
|
||||
Plan Mode provides building blocks for structured research and design. These are
|
||||
implemented as [extensions] using core planning tools like [`enter_plan_mode`],
|
||||
[`exit_plan_mode`], and [`ask_user`].
|
||||
implemented as [extensions](../extensions/index.md) using core planning tools
|
||||
like [`enter_plan_mode`](../tools/planning.md#1-enter_plan_mode-enterplanmode),
|
||||
[`exit_plan_mode`](../tools/planning.md#2-exit_plan_mode-exitplanmode), and
|
||||
[`ask_user`](../tools/ask-user.md).
|
||||
|
||||
### Built-in planning workflow
|
||||
|
||||
The built-in planner uses an adaptive workflow to analyze your project, consult
|
||||
you on trade-offs via [`ask_user`], and draft a plan for your approval.
|
||||
you on trade-offs via [`ask_user`](../tools/ask-user.md), and draft a plan for
|
||||
your approval.
|
||||
|
||||
### Custom planning workflows
|
||||
|
||||
@@ -272,23 +294,29 @@ You can install or create specialized planners to suit your workflow.
|
||||
"tracks" and stores persistent artifacts in your project's `conductor/`
|
||||
directory:
|
||||
|
||||
- **Automate transitions:** Switches to read-only mode via [`enter_plan_mode`].
|
||||
- **Streamline decisions:** Uses [`ask_user`] for architectural choices.
|
||||
- **Automate transitions:** Switches to read-only mode via
|
||||
[`enter_plan_mode`](../tools/planning.md#1-enter_plan_mode-enterplanmode).
|
||||
- **Streamline decisions:** Uses [`ask_user`](../tools/ask-user.md) for
|
||||
architectural choices.
|
||||
- **Maintain project context:** Stores artifacts in the project directory using
|
||||
[custom plan directory and policies](#custom-plan-directory-and-policies).
|
||||
- **Handoff execution:** Transitions to implementation via [`exit_plan_mode`].
|
||||
- **Handoff execution:** Transitions to implementation via
|
||||
[`exit_plan_mode`](../tools/planning.md#2-exit_plan_mode-exitplanmode).
|
||||
|
||||
#### Build your own
|
||||
|
||||
Since Plan Mode is built on modular building blocks, you can develop your own
|
||||
custom planning workflow as an [extensions]. By leveraging core tools and
|
||||
[custom policies](#custom-policies), you can define how Gemini CLI researches
|
||||
and stores plans for your specific domain.
|
||||
custom planning workflow as an [extensions](../extensions/index.md). By
|
||||
leveraging core tools and [custom policies](#custom-policies), you can define
|
||||
how Gemini CLI researches and stores plans for your specific domain.
|
||||
|
||||
To build a custom planning workflow, you can use:
|
||||
|
||||
- **Tool usage:** Use core tools like [`enter_plan_mode`], [`ask_user`], and
|
||||
[`exit_plan_mode`] to manage the research and design process.
|
||||
- **Tool usage:** Use core tools like
|
||||
[`enter_plan_mode`](../tools/planning.md#1-enter_plan_mode-enterplanmode),
|
||||
[`ask_user`](../tools/ask-user.md), and
|
||||
[`exit_plan_mode`](../tools/planning.md#2-exit_plan_mode-exitplanmode) to
|
||||
manage the research and design process.
|
||||
- **Customization:** Set your own storage locations and policy rules using
|
||||
[custom plan directories](#custom-plan-directory-and-policies) and
|
||||
[custom policies](#custom-policies).
|
||||
@@ -302,8 +330,9 @@ high-reasoning model routing.
|
||||
|
||||
## Automatic Model Routing
|
||||
|
||||
When using an [auto model], Gemini CLI automatically optimizes [model routing]
|
||||
based on the current phase of your task:
|
||||
When using an [auto model](../reference/configuration.md#model), Gemini CLI
|
||||
automatically optimizes [model routing](../cli/telemetry.md#model-routing) based
|
||||
on the current phase of your task:
|
||||
|
||||
1. **Planning Phase:** While in Plan Mode, the CLI routes requests to a
|
||||
high-reasoning **Pro** model to ensure robust architectural decisions and
|
||||
@@ -334,7 +363,8 @@ associated plan files and task trackers.
|
||||
- **Default behavior:** Sessions (and their plans) are retained for **30 days**.
|
||||
- **Configuration:** You can customize this behavior via the `/settings` command
|
||||
(search for **Session Retention**) or in your `settings.json` file. See
|
||||
[session retention] for more details.
|
||||
[session retention](../cli/session-management.md#session-retention) for more
|
||||
details.
|
||||
|
||||
Manual deletion also removes all associated artifacts:
|
||||
|
||||
@@ -344,32 +374,7 @@ Manual deletion also removes all associated artifacts:
|
||||
If you use a [custom plans directory](#custom-plan-directory-and-policies),
|
||||
those files are not automatically deleted and must be managed manually.
|
||||
|
||||
[`list_directory`]: /docs/tools/file-system.md#1-list_directory-readfolder
|
||||
[`read_file`]: /docs/tools/file-system.md#2-read_file-readfile
|
||||
[`grep_search`]: /docs/tools/file-system.md#5-grep_search-searchtext
|
||||
[`write_file`]: /docs/tools/file-system.md#3-write_file-writefile
|
||||
[`glob`]: /docs/tools/file-system.md#4-glob-findfiles
|
||||
[`google_web_search`]: /docs/tools/web-search.md
|
||||
[`replace`]: /docs/tools/file-system.md#6-replace-edit
|
||||
[MCP tools]: /docs/tools/mcp-server.md
|
||||
[`save_memory`]: /docs/tools/memory.md
|
||||
[`activate_skill`]: /docs/cli/skills.md
|
||||
[`codebase_investigator`]: /docs/core/subagents.md#codebase-investigator
|
||||
[`cli_help`]: /docs/core/subagents.md#cli-help-agent
|
||||
[subagents]: /docs/core/subagents.md
|
||||
[custom subagents]: /docs/core/subagents.md#creating-custom-subagents
|
||||
[policy engine]: /docs/reference/policy-engine.md
|
||||
[`enter_plan_mode`]: /docs/tools/planning.md#1-enter_plan_mode-enterplanmode
|
||||
[`exit_plan_mode`]: /docs/tools/planning.md#2-exit_plan_mode-exitplanmode
|
||||
[`ask_user`]: /docs/tools/ask-user.md
|
||||
[YOLO mode]: /docs/reference/configuration.md#command-line-arguments
|
||||
[`plan.toml`]:
|
||||
https://github.com/google-gemini/gemini-cli/blob/main/packages/core/src/policy/policies/plan.toml
|
||||
[auto model]: /docs/reference/configuration.md#model
|
||||
[model routing]: /docs/cli/telemetry.md#model-routing
|
||||
[preferred external editor]: /docs/reference/configuration.md#general
|
||||
[session retention]: /docs/cli/session-management.md#session-retention
|
||||
[extensions]: /docs/extensions/
|
||||
[Conductor]: https://github.com/gemini-cli-extensions/conductor
|
||||
[open an issue]: https://github.com/google-gemini/gemini-cli/issues
|
||||
[Agent Skills]: /docs/cli/skills.md
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
# Profiles
|
||||
|
||||
Profiles allow you to maintain separate configurations for different workflows,
|
||||
tasks, or personas. Each profile can define its own set of allowed extensions, a
|
||||
default model, and a custom system instruction (persona).
|
||||
|
||||
Profiles are particularly useful for:
|
||||
|
||||
- Switching between domain-specific setups (e.g., frontend development vs.
|
||||
system administration).
|
||||
- Defining specialized AI personas for different tasks.
|
||||
- Restricting available tools/extensions for specific projects.
|
||||
|
||||
## Profile structure
|
||||
|
||||
Profiles are Markdown files (`.md`) that use YAML frontmatter for configuration
|
||||
and the file body for system instructions.
|
||||
|
||||
**Location**: `~/.gemini/profiles/[profile-name].md`
|
||||
|
||||
### Example profile (`coder.md`)
|
||||
|
||||
```markdown
|
||||
---
|
||||
name: coder
|
||||
default_model: gemini-3.0-flash
|
||||
extensions:
|
||||
- github
|
||||
- web-search
|
||||
---
|
||||
|
||||
You are a world-class Web Developer. Focus on React, TypeScript, and modern CSS.
|
||||
Always prefer functional components and explain your reasoning.
|
||||
```
|
||||
|
||||
### Configuration fields
|
||||
|
||||
| Field | Description |
|
||||
| :-------------- | :----------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `name` | (Mandatory) A unique name for the profile. Must be a valid slug (a-z, 0-9, -, \_) and MUST match the filename without the `.md` extension. |
|
||||
| `default_model` | (Optional) The model to use when this profile is active. |
|
||||
| `extensions` | (Optional) A list of allowed extension IDs. If specified, only these extensions will be loaded. |
|
||||
|
||||
The **body** of the Markdown file is injected into the system prompt, allowing
|
||||
you to define a specific persona or set of instructions that the AI will always
|
||||
follow when the profile is active.
|
||||
|
||||
## Usage
|
||||
|
||||
### Single session usage
|
||||
|
||||
Use the `--profiles` flag (or `-P`) to use a profile for a single session:
|
||||
|
||||
```bash
|
||||
gemini --profiles coder
|
||||
```
|
||||
|
||||
### Persistent default
|
||||
|
||||
You can set a profile as your default for all sessions:
|
||||
|
||||
```bash
|
||||
gemini profiles enable coder
|
||||
```
|
||||
|
||||
To return to the standard configuration:
|
||||
|
||||
```bash
|
||||
gemini profiles disable
|
||||
```
|
||||
|
||||
## Commands
|
||||
|
||||
| Command | Description |
|
||||
| :--------------------------------- | :----------------------------------------------------------- |
|
||||
| `gemini profiles list` | List all available profiles and see which one is active. |
|
||||
| `gemini profiles enable <name>` | Set a profile as the persistent default. |
|
||||
| `gemini profiles disable` | Clear the persistent default profile. |
|
||||
| `gemini profiles install <path>` | Install a new profile from a local file (Markdown). |
|
||||
| `gemini profiles link <path>` | Create a symlink to an existing profile file on your system. |
|
||||
| `gemini profiles uninstall <name>` | Delete a profile from your local storage. |
|
||||
@@ -61,6 +61,15 @@ Browser**:
|
||||
/resume
|
||||
```
|
||||
|
||||
When typing `/resume` (or `/chat`) in slash completion, commands are grouped
|
||||
under titled separators:
|
||||
|
||||
- `-- auto --` (session browser)
|
||||
- `list` is selectable and opens the session browser
|
||||
- `-- checkpoints --` (manual tagged checkpoint commands)
|
||||
|
||||
Unique prefixes such as `/resum` and `/cha` resolve to the same grouped menu.
|
||||
|
||||
The Session Browser provides an interactive interface where you can perform the
|
||||
following actions:
|
||||
|
||||
@@ -72,6 +81,21 @@ following actions:
|
||||
- **Select:** Press **Enter** to resume the selected session.
|
||||
- **Esc:** Press **Esc** to exit the Session Browser.
|
||||
|
||||
### Manual chat checkpoints
|
||||
|
||||
For named branch points inside a session, use chat checkpoints:
|
||||
|
||||
```text
|
||||
/resume save decision-point
|
||||
/resume list
|
||||
/resume resume decision-point
|
||||
```
|
||||
|
||||
Compatibility aliases:
|
||||
|
||||
- `/chat ...` works for the same commands.
|
||||
- `/resume checkpoints ...` also remains supported during migration.
|
||||
|
||||
## Managing sessions
|
||||
|
||||
You can list and delete sessions to keep your history organized and manage disk
|
||||
|
||||
@@ -102,7 +102,7 @@ they appear in the UI.
|
||||
| UI Label | Setting | Description | Default |
|
||||
| ------------------------------------ | ------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------- |
|
||||
| Memory Discovery Max Dirs | `context.discoveryMaxDirs` | Maximum number of directories to search for memory. | `200` |
|
||||
| Load Memory From Include Directories | `context.loadMemoryFromIncludeDirectories` | Controls how /memory refresh loads GEMINI.md files. When true, include directories are scanned; when false, only the current directory is used. | `false` |
|
||||
| Load Memory From Include Directories | `context.loadMemoryFromIncludeDirectories` | Controls how /memory reload loads GEMINI.md files. When true, include directories are scanned; when false, only the current directory is used. | `false` |
|
||||
| Respect .gitignore | `context.fileFiltering.respectGitIgnore` | Respect .gitignore files when searching. | `true` |
|
||||
| Respect .geminiignore | `context.fileFiltering.respectGeminiIgnore` | Respect .geminiignore files when searching. | `true` |
|
||||
| Enable Recursive File Search | `context.fileFiltering.enableRecursiveFileSearch` | Enable recursive file search functionality when completing @ references in the prompt. | `true` |
|
||||
@@ -125,6 +125,7 @@ they appear in the UI.
|
||||
| ------------------------------------- | ----------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------- |
|
||||
| Disable YOLO Mode | `security.disableYoloMode` | Disable YOLO mode, even if enabled by a flag. | `false` |
|
||||
| Allow Permanent Tool Approval | `security.enablePermanentToolApproval` | Enable the "Allow for all future sessions" option in tool confirmation dialogs. | `false` |
|
||||
| Auto-add to Policy by Default | `security.autoAddToPolicyByDefault` | When enabled, the "Allow for all future sessions" option becomes the default choice for low-risk tools in trusted workspaces. | `false` |
|
||||
| Blocks extensions from Git | `security.blockGitExtensions` | Blocks installing and loading extensions from Git. | `false` |
|
||||
| Extension Source Regex Allowlist | `security.allowedExtensions` | List of Regex patterns for allowed extensions. If nonempty, only extensions that match the patterns in this list are allowed. Overrides the blockGitExtensions setting. | `[]` |
|
||||
| Folder Trust | `security.folderTrust.enabled` | Setting to track whether Folder trust is enabled. | `true` |
|
||||
@@ -144,10 +145,9 @@ they appear in the UI.
|
||||
| Enable Tool Output Masking | `experimental.toolOutputMasking.enabled` | Enables tool output masking to save tokens. | `true` |
|
||||
| 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` |
|
||||
| Plan | `experimental.plan` | Enable planning features (Plan Mode and tools). | `false` |
|
||||
| Plan | `experimental.plan` | Enable Plan Mode. | `true` |
|
||||
| Model Steering | `experimental.modelSteering` | Enable model steering (user hints) to guide the model during tool execution. | `false` |
|
||||
| Direct Web Fetch | `experimental.directWebFetch` | Enable web fetch behavior that bypasses LLM summarization. | `false` |
|
||||
| Enable Gemma Model Router | `experimental.gemmaModelRouter.enabled` | Enable the Gemma Model Router. Requires a local endpoint serving Gemma via the Gemini API using LiteRT-LM shim. | `false` |
|
||||
|
||||
### Skills
|
||||
|
||||
|
||||
@@ -32,7 +32,6 @@ Learn how to enable and setup OpenTelemetry for Gemini CLI.
|
||||
- [Metrics](#metrics)
|
||||
- [Custom](#custom)
|
||||
- [Sessions](#sessions-1)
|
||||
- [Authentication](#authentication)
|
||||
- [Tools](#tools-1)
|
||||
- [API](#api-1)
|
||||
- [Token usage](#token-usage)
|
||||
@@ -340,6 +339,12 @@ Captures startup configuration and user prompt submissions.
|
||||
- `mcp_tools` (string, if applicable)
|
||||
- `mcp_tools_count` (int, if applicable)
|
||||
- `output_format` ("text", "json", or "stream-json")
|
||||
- `github_workflow_name` (string, optional)
|
||||
- `github_repository_hash` (string, optional)
|
||||
- `github_event_name` (string, optional)
|
||||
- `github_pr_number` (string, optional)
|
||||
- `github_issue_number` (string, optional)
|
||||
- `github_custom_tracking_id` (string, optional)
|
||||
|
||||
- `gemini_cli.user_prompt`: Emitted when a user submits a prompt.
|
||||
- **Attributes**:
|
||||
@@ -651,16 +656,6 @@ Counts CLI sessions at startup.
|
||||
|
||||
- `gemini_cli.session.count` (Counter, Int): Incremented once per CLI startup.
|
||||
|
||||
##### Authentication
|
||||
|
||||
Tracks "Login with Google" OAuth flow initiation and completion.
|
||||
|
||||
- `gemini_cli.google_auth.start` (Counter, Int): Incremented when the "Login
|
||||
with Google" OAuth flow begins.
|
||||
|
||||
- `gemini_cli.google_auth.end` (Counter, Int): Incremented when the "Login with
|
||||
Google" OAuth flow completes successfully.
|
||||
|
||||
##### Tools
|
||||
|
||||
Measures tool usage and latency.
|
||||
|
||||
@@ -16,6 +16,8 @@ using the `/theme` command within Gemini CLI:
|
||||
- `Default`
|
||||
- `Dracula`
|
||||
- `GitHub`
|
||||
- `Holiday`
|
||||
- `Shades Of Purple`
|
||||
- `Solarized Dark`
|
||||
- **Light themes:**
|
||||
- `ANSI Light`
|
||||
@@ -185,7 +187,7 @@ untrusted sources.
|
||||
|
||||
### Example custom theme
|
||||
|
||||
<img src="../assets/theme-custom.png" alt="Custom theme example" width="600" />
|
||||
<img src="/docs/assets/theme-custom.png" alt="Custom theme example" width="600" />
|
||||
|
||||
### Using your custom theme
|
||||
|
||||
@@ -212,58 +214,66 @@ identify their source, for example: `shades-of-green (green-extension)`.
|
||||
|
||||
### ANSI
|
||||
|
||||
<img src="/assets/theme-ansi.png" alt="ANSI theme" width="600" />
|
||||
<img src="/docs/assets/theme-ansi-dark.png" alt="ANSI theme" width="600">
|
||||
|
||||
### Atom OneDark
|
||||
### Atom One
|
||||
|
||||
<img src="/assets/theme-atom-one.png" alt="Atom One theme" width="600">
|
||||
<img src="/docs/assets/theme-atom-one-dark.png" alt="Atom One theme" width="600">
|
||||
|
||||
### Ayu
|
||||
|
||||
<img src="/assets/theme-ayu.png" alt="Ayu theme" width="600">
|
||||
<img src="/docs/assets/theme-ayu-dark.png" alt="Ayu theme" width="600">
|
||||
|
||||
### Default
|
||||
|
||||
<img src="/assets/theme-default.png" alt="Default theme" width="600">
|
||||
<img src="/docs/assets/theme-default-dark.png" alt="Default theme" width="600">
|
||||
|
||||
### Dracula
|
||||
|
||||
<img src="/assets/theme-dracula.png" alt="Dracula theme" width="600">
|
||||
<img src="/docs/assets/theme-dracula-dark.png" alt="Dracula theme" width="600">
|
||||
|
||||
### GitHub
|
||||
|
||||
<img src="/assets/theme-github.png" alt="GitHub theme" width="600">
|
||||
<img src="/docs/assets/theme-github-dark.png" alt="GitHub theme" width="600">
|
||||
|
||||
### Holiday
|
||||
|
||||
<img src="/docs/assets/theme-holiday-dark.png" alt="Holiday theme" width="600">
|
||||
|
||||
### Shades Of Purple
|
||||
|
||||
<img src="/docs/assets/theme-shades-of-purple-dark.png" alt="Shades Of Purple theme" width="600">
|
||||
|
||||
### Solarized Dark
|
||||
|
||||
<img src="/assets/theme-solarized-dark.png" alt="Solarized Dark theme" width="600">
|
||||
<img src="/docs/assets/theme-solarized-dark.png" alt="Solarized Dark theme" width="600">
|
||||
|
||||
## Light themes
|
||||
|
||||
### ANSI Light
|
||||
|
||||
<img src="/assets/theme-ansi-light.png" alt="ANSI Light theme" width="600">
|
||||
<img src="/docs/assets/theme-ansi-light.png" alt="ANSI Light theme" width="600">
|
||||
|
||||
### Ayu Light
|
||||
|
||||
<img src="/assets/theme-ayu-light.png" alt="Ayu Light theme" width="600">
|
||||
<img src="/docs/assets/theme-ayu-light.png" alt="Ayu Light theme" width="600">
|
||||
|
||||
### Default Light
|
||||
|
||||
<img src="/assets/theme-default-light.png" alt="Default Light theme" width="600">
|
||||
<img src="/docs/assets/theme-default-light.png" alt="Default Light theme" width="600">
|
||||
|
||||
### GitHub Light
|
||||
|
||||
<img src="/assets/theme-github-light.png" alt="GitHub Light theme" width="600">
|
||||
<img src="/docs/assets/theme-github-light.png" alt="GitHub Light theme" width="600">
|
||||
|
||||
### Google Code
|
||||
|
||||
<img src="/assets/theme-google-light.png" alt="Google Code theme" width="600">
|
||||
<img src="/docs/assets/theme-google-light.png" alt="Google Code theme" width="600">
|
||||
|
||||
### Solarized Light
|
||||
|
||||
<img src="/assets/theme-solarized-light.png" alt="Solarized Light theme" width="600">
|
||||
<img src="/docs/assets/theme-solarized-light.png" alt="Solarized Light theme" width="600">
|
||||
|
||||
### Xcode
|
||||
|
||||
<img src="/assets/theme-xcode-light.png" alt="Xcode Light theme" width="600">
|
||||
<img src="/docs/assets/theme-xcode-light.png" alt="Xcode Light theme" width="600">
|
||||
|
||||
@@ -19,14 +19,15 @@ Headless mode runs Gemini CLI once and exits. It's perfect for:
|
||||
|
||||
## How to use headless mode
|
||||
|
||||
Run Gemini CLI in headless mode by providing a prompt as a positional argument.
|
||||
This bypasses the interactive chat interface and prints the response to standard
|
||||
output (stdout).
|
||||
Run Gemini CLI in headless mode by providing a prompt with the `-p` (or
|
||||
`--prompt`) flag. This bypasses the interactive chat interface and prints the
|
||||
response to standard output (stdout). Positional arguments without the flag
|
||||
default to interactive mode, unless the input or output is piped or redirected.
|
||||
|
||||
Run a single command:
|
||||
|
||||
```bash
|
||||
gemini "Write a poem about TypeScript"
|
||||
gemini -p "Write a poem about TypeScript"
|
||||
```
|
||||
|
||||
## How to pipe input to Gemini CLI
|
||||
@@ -40,19 +41,19 @@ Pipe a file:
|
||||
**macOS/Linux**
|
||||
|
||||
```bash
|
||||
cat error.log | gemini "Explain why this failed"
|
||||
cat error.log | gemini -p "Explain why this failed"
|
||||
```
|
||||
|
||||
**Windows (PowerShell)**
|
||||
|
||||
```powershell
|
||||
Get-Content error.log | gemini "Explain why this failed"
|
||||
Get-Content error.log | gemini -p "Explain why this failed"
|
||||
```
|
||||
|
||||
Pipe a command:
|
||||
|
||||
```bash
|
||||
git diff | gemini "Write a commit message for these changes"
|
||||
git diff | gemini -p "Write a commit message for these changes"
|
||||
```
|
||||
|
||||
## Use Gemini CLI output in scripts
|
||||
@@ -78,7 +79,7 @@ one.
|
||||
echo "Generating docs for $file..."
|
||||
|
||||
# Ask Gemini CLI to generate the documentation and print it to stdout
|
||||
gemini "Generate a Markdown documentation summary for @$file. Print the
|
||||
gemini -p "Generate a Markdown documentation summary for @$file. Print the
|
||||
result to standard output." > "${file%.py}.md"
|
||||
done
|
||||
```
|
||||
@@ -92,7 +93,7 @@ one.
|
||||
|
||||
$newName = $_.Name -replace '\.py$', '.md'
|
||||
# Ask Gemini CLI to generate the documentation and print it to stdout
|
||||
gemini "Generate a Markdown documentation summary for @$($_.Name). Print the result to standard output." | Out-File -FilePath $newName -Encoding utf8
|
||||
gemini -p "Generate a Markdown documentation summary for @$($_.Name). Print the result to standard output." | Out-File -FilePath $newName -Encoding utf8
|
||||
}
|
||||
```
|
||||
|
||||
@@ -214,7 +215,7 @@ wrapper that writes the message for you.
|
||||
|
||||
# Ask Gemini to write the message
|
||||
echo "Generating commit message..."
|
||||
msg=$(echo "$diff" | gemini "Write a concise Conventional Commit message for this diff. Output ONLY the message.")
|
||||
msg=$(echo "$diff" | gemini -p "Write a concise Conventional Commit message for this diff. Output ONLY the message.")
|
||||
|
||||
# Commit with the generated message
|
||||
git commit -m "$msg"
|
||||
@@ -251,7 +252,7 @@ wrapper that writes the message for you.
|
||||
|
||||
# Ask Gemini to write the message
|
||||
Write-Host "Generating commit message..."
|
||||
$msg = $diff | gemini "Write a concise Conventional Commit message for this diff. Output ONLY the message."
|
||||
$msg = $diff | gemini -p "Write a concise Conventional Commit message for this diff. Output ONLY the message."
|
||||
|
||||
# Commit with the generated message
|
||||
git commit -m "$msg"
|
||||
|
||||
@@ -101,8 +101,8 @@ The agent will:
|
||||
|
||||
- **Server won't start?** Try running the docker command manually in your
|
||||
terminal to see if it prints an error (e.g., "image not found").
|
||||
- **Tools not found?** Run `/mcp refresh` to force the CLI to re-query the
|
||||
server for its capabilities.
|
||||
- **Tools not found?** Run `/mcp reload` to force the CLI to re-query the server
|
||||
for its capabilities.
|
||||
|
||||
## Next steps
|
||||
|
||||
|
||||
@@ -105,7 +105,7 @@ excellent for debugging why the agent might be ignoring a rule.
|
||||
If you edit a `GEMINI.md` file while a session is running, the agent won't know
|
||||
immediately. Force a reload with:
|
||||
|
||||
**Command:** `/memory refresh`
|
||||
**Command:** `/memory reload`
|
||||
|
||||
## Best practices
|
||||
|
||||
|
||||
@@ -89,9 +89,9 @@ Gemini gives you granular control over the undo process. You can choose to:
|
||||
Sometimes you want to try two different approaches to the same problem.
|
||||
|
||||
1. Start a session and get to a decision point.
|
||||
2. Save the current state with `/chat save decision-point`.
|
||||
2. Save the current state with `/resume save decision-point`.
|
||||
3. Try your first approach.
|
||||
4. Later, use `/chat resume decision-point` to fork the conversation back to
|
||||
4. Later, use `/resume resume decision-point` to fork the conversation back to
|
||||
that moment and try a different approach.
|
||||
|
||||
This creates a new branch of history without losing your original work.
|
||||
@@ -101,5 +101,5 @@ This creates a new branch of history without losing your original work.
|
||||
- Learn about [Checkpointing](../../cli/checkpointing.md) to understand the
|
||||
underlying safety mechanism.
|
||||
- Explore [Task planning](task-planning.md) to keep complex sessions organized.
|
||||
- See the [Command reference](../../reference/commands.md) for all `/chat` and
|
||||
`/resume` options.
|
||||
- See the [Command reference](../../reference/commands.md) for `/resume`
|
||||
options, grouped checkpoint menus, and `/chat` compatibility aliases.
|
||||
|
||||
@@ -75,7 +75,7 @@ Markdown file.
|
||||
Users can manage subagents using the following commands within the Gemini CLI:
|
||||
|
||||
- `/agents list`: Displays all available local and remote subagents.
|
||||
- `/agents refresh`: Reloads the agent registry. Use this after adding or
|
||||
- `/agents reload`: Reloads the agent registry. Use this after adding or
|
||||
modifying agent definition files.
|
||||
- `/agents enable <agent_name>`: Enables a specific subagent.
|
||||
- `/agents disable <agent_name>`: Disables a specific subagent.
|
||||
|
||||
@@ -297,7 +297,7 @@ Gemini CLI can also delegate tasks to remote subagents using the Agent-to-Agent
|
||||
|
||||
> **Note: Remote subagents are currently an experimental feature.**
|
||||
|
||||
See the [Remote Subagents documentation](/docs/core/remote-agents) for detailed
|
||||
See the [Remote Subagents documentation](remote-agents) for detailed
|
||||
configuration and usage instructions.
|
||||
|
||||
## Extension subagents
|
||||
|
||||
@@ -123,6 +123,7 @@ The manifest file defines the extension's behavior and configuration.
|
||||
},
|
||||
"contextFileName": "GEMINI.md",
|
||||
"excludeTools": ["run_shell_command"],
|
||||
"migratedTo": "https://github.com/new-owner/new-extension-repo",
|
||||
"plan": {
|
||||
"directory": ".gemini/plans"
|
||||
}
|
||||
@@ -138,6 +139,9 @@ The manifest file defines the extension's behavior and configuration.
|
||||
- `version`: The version of the extension.
|
||||
- `description`: A short description of the extension. This will be displayed on
|
||||
[geminicli.com/extensions](https://geminicli.com/extensions).
|
||||
- `migratedTo`: The URL of the new repository source for the extension. If this
|
||||
is set, the CLI will automatically check this new source for updates and
|
||||
migrate the extension's installation to the new source if an update is found.
|
||||
- `mcpServers`: A map of MCP servers to settings. The key is the name of the
|
||||
server, and the value is the server configuration. These servers will be
|
||||
loaded on startup just like MCP servers defined in a
|
||||
|
||||
@@ -152,3 +152,29 @@ jobs:
|
||||
release/linux.arm64.my-tool.tar.gz
|
||||
release/win32.arm64.my-tool.zip
|
||||
```
|
||||
|
||||
## Migrating an Extension Repository
|
||||
|
||||
If you need to move your extension to a new repository (e.g., from a personal
|
||||
account to an organization) or rename it, you can use the `migratedTo` property
|
||||
in your `gemini-extension.json` file to seamlessly transition your users.
|
||||
|
||||
1. **Create the new repository**: Setup your extension in its new location.
|
||||
2. **Update the old repository**: In your original repository, update the
|
||||
`gemini-extension.json` file to include the `migratedTo` property, pointing
|
||||
to the new repository URL, and bump the version number. You can optionally
|
||||
change the `name` of your extension at this time in the new repository.
|
||||
```json
|
||||
{
|
||||
"name": "my-extension",
|
||||
"version": "1.1.0",
|
||||
"migratedTo": "https://github.com/new-owner/new-extension-repo"
|
||||
}
|
||||
```
|
||||
3. **Release the update**: Publish this new version in your old repository.
|
||||
|
||||
When users check for updates, the Gemini CLI will detect the `migratedTo` field,
|
||||
verify that the new repository contains a valid extension update, and
|
||||
automatically update their local installation to track the new source and name
|
||||
moving forward. All extension settings will automatically migrate to the new
|
||||
installation.
|
||||
|
||||
@@ -6,7 +6,7 @@ using the CLI.
|
||||
|
||||
> **Note:** Looking for a high-level comparison of all available subscriptions?
|
||||
> To compare features and find the right quota for your needs, see our
|
||||
> [Plans page](/plans/).
|
||||
> [Plans page](https://geminicli.com/plans/).
|
||||
|
||||
For most users, we recommend starting Gemini CLI and logging in with your
|
||||
personal Google account.
|
||||
|
||||
@@ -41,7 +41,7 @@ limit resets and Gemini 3 Pro can be used again.
|
||||
|
||||
> **Note:** Looking to upgrade for higher limits? To compare subscription
|
||||
> options and find the right quota for your needs, see our
|
||||
> [Plans page](/plans/).
|
||||
> [Plans page](https://geminicli.com/plans/).
|
||||
|
||||
Similarly, when you reach your daily usage limit for Gemini 2.5 Pro, you’ll see
|
||||
a message prompting fallback to Gemini 2.5 Flash.
|
||||
|
||||
@@ -70,7 +70,7 @@ gemini
|
||||
```
|
||||
|
||||
For a list of options and additional commands, see the
|
||||
[CLI cheatsheet](/docs/cli/cli-reference.md).
|
||||
[CLI cheatsheet](../cli/cli-reference.md).
|
||||
|
||||
You can also run Gemini CLI using one of the following advanced methods:
|
||||
|
||||
|
||||
@@ -449,7 +449,7 @@ When you open a project with hooks defined in `.gemini/settings.json`:
|
||||
|
||||
Hooks inherit the environment of the Gemini CLI process, which may include
|
||||
sensitive API keys. Gemini CLI provides a
|
||||
[redaction system](/docs/reference/configuration.md#environment-variable-redaction)
|
||||
[redaction system](../reference/configuration.md#environment-variable-redaction)
|
||||
that automatically filters variables matching sensitive patterns (e.g., `KEY`,
|
||||
`TOKEN`).
|
||||
|
||||
|
||||
@@ -22,11 +22,11 @@ With hooks, you can:
|
||||
|
||||
### Getting started
|
||||
|
||||
- **[Writing hooks guide](/docs/hooks/writing-hooks)**: A tutorial on creating
|
||||
your first hook with comprehensive examples.
|
||||
- **[Best practices](/docs/hooks/best-practices)**: Guidelines on security,
|
||||
- **[Writing hooks guide](../hooks/writing-hooks)**: A tutorial on creating your
|
||||
first hook with comprehensive examples.
|
||||
- **[Best practices](../hooks/best-practices)**: Guidelines on security,
|
||||
performance, and debugging.
|
||||
- **[Hooks reference](/docs/hooks/reference)**: The definitive technical
|
||||
- **[Hooks reference](../hooks/reference)**: The definitive technical
|
||||
specification of I/O schemas and exit codes.
|
||||
|
||||
## Core concepts
|
||||
@@ -152,8 +152,8 @@ Gemini CLI **fingerprints** project hooks. If a hook's name or command changes
|
||||
(e.g., via `git pull`), it is treated as a **new, untrusted hook** and you will
|
||||
be warned before it executes.
|
||||
|
||||
See [Security Considerations](/docs/hooks/best-practices#using-hooks-securely)
|
||||
for a detailed threat model.
|
||||
See [Security Considerations](../hooks/best-practices#using-hooks-securely) for
|
||||
a detailed threat model.
|
||||
|
||||
## Managing hooks
|
||||
|
||||
|
||||
@@ -82,8 +82,8 @@ For `BeforeTool` and `AfterTool` events, the `matcher` field in your settings is
|
||||
compared against the name of the tool being executed.
|
||||
|
||||
- **Built-in Tools**: You can match any built-in tool (e.g., `read_file`,
|
||||
`run_shell_command`). See the [Tools Reference](/docs/reference/tools) for a
|
||||
full list of available tool names.
|
||||
`run_shell_command`). See the [Tools Reference](../reference/tools) for a full
|
||||
list of available tool names.
|
||||
- **MCP Tools**: Tools from MCP servers follow the naming pattern
|
||||
`mcp__<server_name>__<tool_name>`.
|
||||
- **Regex Support**: Matchers support regular expressions (e.g.,
|
||||
|
||||
@@ -28,24 +28,33 @@ Slash commands provide meta-level control over the CLI itself.
|
||||
|
||||
### `/chat`
|
||||
|
||||
- **Description:** Save and resume conversation history for branching
|
||||
conversation state interactively, or resuming a previous state from a later
|
||||
session.
|
||||
- **Description:** Alias for `/resume`. Both commands now expose the same
|
||||
session browser action and checkpoint subcommands.
|
||||
- **Menu layout when typing `/chat` (or `/resume`)**:
|
||||
- `-- auto --`
|
||||
- `list` (selecting this opens the auto-saved session browser)
|
||||
- `-- checkpoints --`
|
||||
- `list`, `save`, `resume`, `delete`, `share` (manual tagged checkpoints)
|
||||
- **Note:** Unique prefixes (for example `/cha` or `/resum`) resolve to the
|
||||
same grouped menu.
|
||||
- **Sub-commands:**
|
||||
- **`debug`**
|
||||
- **Description:** Export the most recent API request as a JSON payload.
|
||||
- **`delete <tag>`**
|
||||
- **Description:** Deletes a saved conversation checkpoint.
|
||||
- **Equivalent:** `/resume delete <tag>`
|
||||
- **`list`**
|
||||
- **Description:** Lists available tags for chat state resumption.
|
||||
- **Description:** Lists available tags for manually saved checkpoints.
|
||||
- **Note:** This command only lists chats saved within the current project.
|
||||
Because chat history is project-scoped, chats saved in other project
|
||||
directories will not be displayed.
|
||||
- **Equivalent:** `/resume list`
|
||||
- **`resume <tag>`**
|
||||
- **Description:** Resumes a conversation from a previous save.
|
||||
- **Note:** You can only resume chats that were saved within the current
|
||||
project. To resume a chat from a different project, you must run the
|
||||
Gemini CLI from that project's directory.
|
||||
- **Equivalent:** `/resume resume <tag>`
|
||||
- **`save <tag>`**
|
||||
- **Description:** Saves the current conversation history. You must add a
|
||||
`<tag>` for identifying the conversation state.
|
||||
@@ -60,10 +69,12 @@ Slash commands provide meta-level control over the CLI itself.
|
||||
conversation states. For automatic checkpoints created before file
|
||||
modifications, see the
|
||||
[Checkpointing documentation](../cli/checkpointing.md).
|
||||
- **Equivalent:** `/resume save <tag>`
|
||||
- **`share [filename]`**
|
||||
- **Description** Writes the current conversation to a provided Markdown or
|
||||
- **Description:** Writes the current conversation to a provided Markdown or
|
||||
JSON file. If no filename is provided, then the CLI will generate one.
|
||||
- **Usage** `/chat share file.md` or `/chat share file.json`.
|
||||
- **Usage:** `/chat share file.md` or `/chat share file.json`.
|
||||
- **Equivalent:** `/resume share [filename]`
|
||||
|
||||
### `/clear`
|
||||
|
||||
@@ -268,8 +279,8 @@ Slash commands provide meta-level control over the CLI itself.
|
||||
|
||||
- **Description:** Switch to Plan Mode (read-only) and view the current plan if
|
||||
one has been generated.
|
||||
- **Note:** This feature requires the `experimental.plan` setting to be
|
||||
enabled in your configuration.
|
||||
- **Note:** This feature is enabled by default. It can be disabled via the
|
||||
`experimental.plan` setting in your configuration.
|
||||
- **Sub-commands:**
|
||||
- **`copy`**:
|
||||
- **Description:** Copy the currently approved plan to your clipboard.
|
||||
@@ -314,10 +325,13 @@ Slash commands provide meta-level control over the CLI itself.
|
||||
|
||||
### `/resume`
|
||||
|
||||
- **Description:** Browse and resume previous conversation sessions. Opens an
|
||||
interactive session browser where you can search, filter, and select from
|
||||
automatically saved conversations.
|
||||
- **Description:** Browse and resume previous conversation sessions, and manage
|
||||
manual chat checkpoints.
|
||||
- **Features:**
|
||||
- **Auto sessions:** Run `/resume` to open the interactive session browser for
|
||||
automatically saved conversations.
|
||||
- **Chat checkpoints:** Use checkpoint subcommands directly (`/resume save`,
|
||||
`/resume resume`, etc.).
|
||||
- **Management:** Delete unwanted sessions directly from the browser
|
||||
- **Resume:** Select any session to resume and continue the conversation
|
||||
- **Search:** Use `/` to search through conversation content across all
|
||||
@@ -328,6 +342,23 @@ Slash commands provide meta-level control over the CLI itself.
|
||||
- **Note:** All conversations are automatically saved as you chat - no manual
|
||||
saving required. See [Session Management](../cli/session-management.md) for
|
||||
complete details.
|
||||
- **Alias:** `/chat` provides the same behavior and subcommands.
|
||||
- **Sub-commands:**
|
||||
- **`list`**
|
||||
- **Description:** Lists available tags for manual chat checkpoints.
|
||||
- **`save <tag>`**
|
||||
- **Description:** Saves the current conversation as a tagged checkpoint.
|
||||
- **`resume <tag>`** (alias: `load`)
|
||||
- **Description:** Loads a previously saved tagged checkpoint.
|
||||
- **`delete <tag>`**
|
||||
- **Description:** Deletes a tagged checkpoint.
|
||||
- **`share [filename]`**
|
||||
- **Description:** Exports the current conversation to Markdown or JSON.
|
||||
- **`debug`**
|
||||
- **Description:** Export the most recent API request as JSON payload
|
||||
(nightly builds).
|
||||
- **Compatibility alias:** `/resume checkpoints ...` is still accepted for the
|
||||
same checkpoint commands.
|
||||
|
||||
### `/settings`
|
||||
|
||||
@@ -408,6 +439,12 @@ Slash commands provide meta-level control over the CLI itself.
|
||||
- **`nodesc`** or **`nodescriptions`**:
|
||||
- **Description:** Hide tool descriptions, showing only the tool names.
|
||||
|
||||
### `/upgrade`
|
||||
|
||||
- **Description:** Open the Gemini Code Assist upgrade page in your browser.
|
||||
This lets you upgrade your tier for higher usage limits.
|
||||
- **Note:** This command is only available when logged in with Google.
|
||||
|
||||
### `/vim`
|
||||
|
||||
- **Description:** Toggle vim mode on or off. When vim mode is enabled, the
|
||||
|
||||
@@ -719,7 +719,7 @@ their corresponding top-level category object in your `settings.json` file.
|
||||
- **Default:** `[]`
|
||||
|
||||
- **`context.loadMemoryFromIncludeDirectories`** (boolean):
|
||||
- **Description:** Controls how /memory refresh loads GEMINI.md files. When
|
||||
- **Description:** Controls how /memory reload loads GEMINI.md files. When
|
||||
true, include directories are scanned; when false, only the current
|
||||
directory is used.
|
||||
- **Default:** `false`
|
||||
@@ -872,6 +872,11 @@ their corresponding top-level category object in your `settings.json` file.
|
||||
confirmation dialogs.
|
||||
- **Default:** `false`
|
||||
|
||||
- **`security.autoAddToPolicyByDefault`** (boolean):
|
||||
- **Description:** When enabled, the "Allow for all future sessions" option
|
||||
becomes the default choice for low-risk tools in trusted workspaces.
|
||||
- **Default:** `false`
|
||||
|
||||
- **`security.blockGitExtensions`** (boolean):
|
||||
- **Description:** Blocks installing and loading extensions from Git.
|
||||
- **Default:** `false`
|
||||
@@ -1021,8 +1026,8 @@ their corresponding top-level category object in your `settings.json` file.
|
||||
- **Default:** `false`
|
||||
|
||||
- **`experimental.plan`** (boolean):
|
||||
- **Description:** Enable planning features (Plan Mode and tools).
|
||||
- **Default:** `false`
|
||||
- **Description:** Enable Plan Mode.
|
||||
- **Default:** `true`
|
||||
- **Requires restart:** Yes
|
||||
|
||||
- **`experimental.taskTracker`** (boolean):
|
||||
@@ -1041,8 +1046,8 @@ their corresponding top-level category object in your `settings.json` file.
|
||||
- **Requires restart:** Yes
|
||||
|
||||
- **`experimental.gemmaModelRouter.enabled`** (boolean):
|
||||
- **Description:** Enable the Gemma Model Router. Requires a local endpoint
|
||||
serving Gemma via the Gemini API using LiteRT-LM shim.
|
||||
- **Description:** Enable the Gemma Model Router (experimental). Requires a
|
||||
local endpoint serving Gemma via the Gemini API using LiteRT-LM shim.
|
||||
- **Default:** `false`
|
||||
- **Requires restart:** Yes
|
||||
|
||||
@@ -1705,7 +1710,7 @@ conventions and context.
|
||||
loaded, allowing you to verify the hierarchy and content being used by the
|
||||
AI.
|
||||
- See the [Commands documentation](./commands.md#memory) for full details on
|
||||
the `/memory` command and its sub-commands (`show` and `refresh`).
|
||||
the `/memory` command and its sub-commands (`show` and `reload`).
|
||||
|
||||
By understanding and utilizing these configuration layers and the hierarchical
|
||||
nature of context files, you can effectively manage the AI's memory and tailor
|
||||
|
||||
@@ -8,119 +8,118 @@ available combinations.
|
||||
|
||||
#### Basic Controls
|
||||
|
||||
| Action | Keys |
|
||||
| --------------------------------------------------------------- | --------------------- |
|
||||
| Confirm the current selection or choice. | `Enter` |
|
||||
| Dismiss dialogs or cancel the current focus. | `Esc`<br />`Ctrl + [` |
|
||||
| Cancel the current request or quit the CLI when input is empty. | `Ctrl + C` |
|
||||
| Exit the CLI when the input buffer is empty. | `Ctrl + D` |
|
||||
| Action | Keys |
|
||||
| --------------------------------------------------------------- | ------------------- |
|
||||
| Confirm the current selection or choice. | `Enter` |
|
||||
| Dismiss dialogs or cancel the current focus. | `Esc`<br />`Ctrl+[` |
|
||||
| Cancel the current request or quit the CLI when input is empty. | `Ctrl+C` |
|
||||
| Exit the CLI when the input buffer is empty. | `Ctrl+D` |
|
||||
|
||||
#### Cursor Movement
|
||||
|
||||
| Action | Keys |
|
||||
| ------------------------------------------- | ------------------------------------------------------------ |
|
||||
| Move the cursor to the start of the line. | `Ctrl + A`<br />`Home` |
|
||||
| Move the cursor to the end of the line. | `Ctrl + E`<br />`End` |
|
||||
| Move the cursor up one line. | `Up Arrow` |
|
||||
| Move the cursor down one line. | `Down Arrow` |
|
||||
| Move the cursor one character to the left. | `Left Arrow` |
|
||||
| Move the cursor one character to the right. | `Right Arrow`<br />`Ctrl + F` |
|
||||
| Move the cursor one word to the left. | `Ctrl + Left Arrow`<br />`Alt + Left Arrow`<br />`Alt + B` |
|
||||
| Move the cursor one word to the right. | `Ctrl + Right Arrow`<br />`Alt + Right Arrow`<br />`Alt + F` |
|
||||
| Action | Keys |
|
||||
| ------------------------------------------- | ------------------------------------------ |
|
||||
| Move the cursor to the start of the line. | `Ctrl+A`<br />`Home` |
|
||||
| Move the cursor to the end of the line. | `Ctrl+E`<br />`End` |
|
||||
| Move the cursor up one line. | `Up` |
|
||||
| Move the cursor down one line. | `Down` |
|
||||
| Move the cursor one character to the left. | `Left` |
|
||||
| Move the cursor one character to the right. | `Right`<br />`Ctrl+F` |
|
||||
| Move the cursor one word to the left. | `Ctrl+Left`<br />`Alt+Left`<br />`Alt+B` |
|
||||
| Move the cursor one word to the right. | `Ctrl+Right`<br />`Alt+Right`<br />`Alt+F` |
|
||||
|
||||
#### Editing
|
||||
|
||||
| Action | Keys |
|
||||
| ------------------------------------------------ | ---------------------------------------------------------------- |
|
||||
| Delete from the cursor to the end of the line. | `Ctrl + K` |
|
||||
| Delete from the cursor to the start of the line. | `Ctrl + U` |
|
||||
| Clear all text in the input field. | `Ctrl + C` |
|
||||
| Delete the previous word. | `Ctrl + Backspace`<br />`Alt + Backspace`<br />`Ctrl + W` |
|
||||
| Delete the next word. | `Ctrl + Delete`<br />`Alt + Delete`<br />`Alt + D` |
|
||||
| Delete the character to the left. | `Backspace`<br />`Ctrl + H` |
|
||||
| Delete the character to the right. | `Delete`<br />`Ctrl + D` |
|
||||
| Undo the most recent text edit. | `Cmd + Z`<br />`Alt + Z` |
|
||||
| Redo the most recent undone text edit. | `Shift + Ctrl + Z`<br />`Shift + Cmd + Z`<br />`Shift + Alt + Z` |
|
||||
| Action | Keys |
|
||||
| ------------------------------------------------ | -------------------------------------------------------- |
|
||||
| Delete from the cursor to the end of the line. | `Ctrl+K` |
|
||||
| Delete from the cursor to the start of the line. | `Ctrl+U` |
|
||||
| Clear all text in the input field. | `Ctrl+C` |
|
||||
| Delete the previous word. | `Ctrl+Backspace`<br />`Alt+Backspace`<br />`Ctrl+W` |
|
||||
| Delete the next word. | `Ctrl+Delete`<br />`Alt+Delete`<br />`Alt+D` |
|
||||
| Delete the character to the left. | `Backspace`<br />`Ctrl+H` |
|
||||
| Delete the character to the right. | `Delete`<br />`Ctrl+D` |
|
||||
| Undo the most recent text edit. | `Cmd/Win+Z`<br />`Alt+Z` |
|
||||
| Redo the most recent undone text edit. | `Ctrl+Shift+Z`<br />`Shift+Cmd/Win+Z`<br />`Alt+Shift+Z` |
|
||||
|
||||
#### Scrolling
|
||||
|
||||
| Action | Keys |
|
||||
| ------------------------ | --------------------------------- |
|
||||
| Scroll content up. | `Shift + Up Arrow` |
|
||||
| Scroll content down. | `Shift + Down Arrow` |
|
||||
| Scroll to the top. | `Ctrl + Home`<br />`Shift + Home` |
|
||||
| Scroll to the bottom. | `Ctrl + End`<br />`Shift + End` |
|
||||
| Scroll up by one page. | `Page Up` |
|
||||
| Scroll down by one page. | `Page Down` |
|
||||
| Action | Keys |
|
||||
| ------------------------ | ----------------------------- |
|
||||
| Scroll content up. | `Shift+Up` |
|
||||
| Scroll content down. | `Shift+Down` |
|
||||
| Scroll to the top. | `Ctrl+Home`<br />`Shift+Home` |
|
||||
| Scroll to the bottom. | `Ctrl+End`<br />`Shift+End` |
|
||||
| Scroll up by one page. | `Page Up` |
|
||||
| Scroll down by one page. | `Page Down` |
|
||||
|
||||
#### History & Search
|
||||
|
||||
| Action | Keys |
|
||||
| -------------------------------------------- | ------------ |
|
||||
| Show the previous entry in history. | `Ctrl + P` |
|
||||
| Show the next entry in history. | `Ctrl + N` |
|
||||
| Start reverse search through history. | `Ctrl + R` |
|
||||
| Submit the selected reverse-search match. | `Enter` |
|
||||
| Accept a suggestion while reverse searching. | `Tab` |
|
||||
| Browse and rewind previous interactions. | `Double Esc` |
|
||||
| Action | Keys |
|
||||
| -------------------------------------------- | -------- |
|
||||
| Show the previous entry in history. | `Ctrl+P` |
|
||||
| Show the next entry in history. | `Ctrl+N` |
|
||||
| Start reverse search through history. | `Ctrl+R` |
|
||||
| Submit the selected reverse-search match. | `Enter` |
|
||||
| Accept a suggestion while reverse searching. | `Tab` |
|
||||
|
||||
#### Navigation
|
||||
|
||||
| Action | Keys |
|
||||
| -------------------------------------------------- | --------------------- |
|
||||
| Move selection up in lists. | `Up Arrow` |
|
||||
| Move selection down in lists. | `Down Arrow` |
|
||||
| Move up within dialog options. | `Up Arrow`<br />`K` |
|
||||
| Move down within dialog options. | `Down Arrow`<br />`J` |
|
||||
| Move to the next item or question in a dialog. | `Tab` |
|
||||
| Move to the previous item or question in a dialog. | `Shift + Tab` |
|
||||
| Action | Keys |
|
||||
| -------------------------------------------------- | --------------- |
|
||||
| Move selection up in lists. | `Up` |
|
||||
| Move selection down in lists. | `Down` |
|
||||
| Move up within dialog options. | `Up`<br />`K` |
|
||||
| Move down within dialog options. | `Down`<br />`J` |
|
||||
| Move to the next item or question in a dialog. | `Tab` |
|
||||
| Move to the previous item or question in a dialog. | `Shift+Tab` |
|
||||
|
||||
#### Suggestions & Completions
|
||||
|
||||
| Action | Keys |
|
||||
| --------------------------------------- | ---------------------------- |
|
||||
| Accept the inline suggestion. | `Tab`<br />`Enter` |
|
||||
| Move to the previous completion option. | `Up Arrow`<br />`Ctrl + P` |
|
||||
| Move to the next completion option. | `Down Arrow`<br />`Ctrl + N` |
|
||||
| Expand an inline suggestion. | `Right Arrow` |
|
||||
| Collapse an inline suggestion. | `Left Arrow` |
|
||||
| Action | Keys |
|
||||
| --------------------------------------- | -------------------- |
|
||||
| Accept the inline suggestion. | `Tab`<br />`Enter` |
|
||||
| Move to the previous completion option. | `Up`<br />`Ctrl+P` |
|
||||
| Move to the next completion option. | `Down`<br />`Ctrl+N` |
|
||||
| Expand an inline suggestion. | `Right` |
|
||||
| Collapse an inline suggestion. | `Left` |
|
||||
|
||||
#### Text Input
|
||||
|
||||
| Action | Keys |
|
||||
| ---------------------------------------------------------- | ----------------------------------------------------------------------------------------- |
|
||||
| Submit the current prompt. | `Enter` |
|
||||
| Insert a newline without submitting. | `Ctrl + Enter`<br />`Cmd + Enter`<br />`Alt + Enter`<br />`Shift + Enter`<br />`Ctrl + J` |
|
||||
| Open the current prompt or the plan in an external editor. | `Ctrl + X` |
|
||||
| Paste from the clipboard. | `Ctrl + V`<br />`Cmd + V`<br />`Alt + V` |
|
||||
| Action | Keys |
|
||||
| ---------------------------------------------------------- | ----------------------------------------------------------------------------------- |
|
||||
| Submit the current prompt. | `Enter` |
|
||||
| Insert a newline without submitting. | `Ctrl+Enter`<br />`Cmd/Win+Enter`<br />`Alt+Enter`<br />`Shift+Enter`<br />`Ctrl+J` |
|
||||
| Open the current prompt or the plan in an external editor. | `Ctrl+X` |
|
||||
| Paste from the clipboard. | `Ctrl+V`<br />`Cmd/Win+V`<br />`Alt+V` |
|
||||
|
||||
#### App Controls
|
||||
|
||||
| Action | Keys |
|
||||
| -------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------- |
|
||||
| Toggle detailed error information. | `F12` |
|
||||
| Toggle the full TODO list. | `Ctrl + T` |
|
||||
| Show IDE context details. | `Ctrl + G` |
|
||||
| Toggle Markdown rendering. | `Alt + M` |
|
||||
| Toggle copy mode when in alternate buffer mode. | `Ctrl + S` |
|
||||
| Toggle YOLO (auto-approval) mode for tool calls. | `Ctrl + Y` |
|
||||
| Cycle through approval modes: default (prompt), auto_edit (auto-approve edits), and plan (read-only). Plan mode is skipped when the agent is busy. | `Shift + Tab` |
|
||||
| Expand and collapse blocks of content when not in alternate buffer mode. | `Ctrl + O` |
|
||||
| Expand or collapse a paste placeholder when cursor is over placeholder. | `Ctrl + O` |
|
||||
| Toggle current background shell visibility. | `Ctrl + B` |
|
||||
| Toggle background shell list. | `Ctrl + L` |
|
||||
| Kill the active background shell. | `Ctrl + K` |
|
||||
| Confirm selection in background shell list. | `Enter` |
|
||||
| Dismiss background shell list. | `Esc` |
|
||||
| Move focus from background shell to Gemini. | `Shift + Tab` |
|
||||
| Move focus from background shell list to Gemini. | `Tab` |
|
||||
| Show warning when trying to move focus away from background shell. | `Tab` |
|
||||
| Show warning when trying to move focus away from shell input. | `Tab` |
|
||||
| Move focus from Gemini to the active shell. | `Tab` |
|
||||
| Move focus from the shell back to Gemini. | `Shift + Tab` |
|
||||
| Clear the terminal screen and redraw the UI. | `Ctrl + L` |
|
||||
| Restart the application. | `R`<br />`Shift + R` |
|
||||
| Suspend the CLI and move it to the background. | `Ctrl + Z` |
|
||||
| Action | Keys |
|
||||
| -------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------ |
|
||||
| Toggle detailed error information. | `F12` |
|
||||
| Toggle the full TODO list. | `Ctrl+T` |
|
||||
| Show IDE context details. | `Ctrl+G` |
|
||||
| Toggle Markdown rendering. | `Alt+M` |
|
||||
| Toggle copy mode when in alternate buffer mode. | `Ctrl+S` |
|
||||
| Toggle YOLO (auto-approval) mode for tool calls. | `Ctrl+Y` |
|
||||
| Cycle through approval modes: default (prompt), auto_edit (auto-approve edits), and plan (read-only). Plan mode is skipped when the agent is busy. | `Shift+Tab` |
|
||||
| Expand and collapse blocks of content when not in alternate buffer mode. | `Ctrl+O` |
|
||||
| Expand or collapse a paste placeholder when cursor is over placeholder. | `Ctrl+O` |
|
||||
| Toggle current background shell visibility. | `Ctrl+B` |
|
||||
| Toggle background shell list. | `Ctrl+L` |
|
||||
| Kill the active background shell. | `Ctrl+K` |
|
||||
| Confirm selection in background shell list. | `Enter` |
|
||||
| Dismiss background shell list. | `Esc` |
|
||||
| Move focus from background shell to Gemini. | `Shift+Tab` |
|
||||
| Move focus from background shell list to Gemini. | `Tab` |
|
||||
| Show warning when trying to move focus away from background shell. | `Tab` |
|
||||
| Show warning when trying to move focus away from shell input. | `Tab` |
|
||||
| Move focus from Gemini to the active shell. | `Tab` |
|
||||
| Move focus from the shell back to Gemini. | `Shift+Tab` |
|
||||
| Clear the terminal screen and redraw the UI. | `Ctrl+L` |
|
||||
| Restart the application. | `R`<br />`Shift+R` |
|
||||
| Suspend the CLI and move it to the background. | `Ctrl+Z` |
|
||||
|
||||
<!-- KEYBINDINGS-AUTOGEN:END -->
|
||||
|
||||
|
||||
@@ -91,10 +91,17 @@ the arguments don't match the pattern, the rule does not apply.
|
||||
There are three possible decisions a rule can enforce:
|
||||
|
||||
- `allow`: The tool call is executed automatically without user interaction.
|
||||
- `deny`: The tool call is blocked and is not executed.
|
||||
- `deny`: The tool call is blocked and is not executed. For global rules (those
|
||||
without an `argsPattern`), tools that are denied are **completely excluded
|
||||
from the model's memory**. This means the model will not even see the tool as
|
||||
an option, which is more secure and saves context window space.
|
||||
- `ask_user`: The user is prompted to approve or deny the tool call. (In
|
||||
non-interactive mode, this is treated as `deny`.)
|
||||
|
||||
> **Note:** The `deny` decision is the recommended way to exclude tools. The
|
||||
> legacy `tools.exclude` setting in `settings.json` is deprecated in favor of
|
||||
> policy rules with a `deny` decision.
|
||||
|
||||
### Priority system and tiers
|
||||
|
||||
The policy engine uses a sophisticated priority system to resolve conflicts when
|
||||
@@ -143,8 +150,8 @@ always active.
|
||||
confirmation.
|
||||
- `autoEdit`: Optimized for automated code editing; some write tools may be
|
||||
auto-approved.
|
||||
- `plan`: A strict, read-only mode for research and design. See [Customizing
|
||||
Plan Mode Policies].
|
||||
- `plan`: A strict, read-only mode for research and design. See
|
||||
[Customizing Plan Mode Policies](../cli/plan-mode.md#customizing-policies).
|
||||
- `yolo`: A mode where all tools are auto-approved (use with extreme caution).
|
||||
|
||||
## Rule matching
|
||||
@@ -212,6 +219,10 @@ Here is a breakdown of the fields available in a TOML policy rule:
|
||||
# A unique name for the tool, or an array of names.
|
||||
toolName = "run_shell_command"
|
||||
|
||||
# (Optional) The name of a subagent. If provided, the rule only applies to tool calls
|
||||
# made by this specific subagent.
|
||||
subagent = "generalist"
|
||||
|
||||
# (Optional) The name of an MCP server. Can be combined with toolName
|
||||
# to form a composite name like "mcpName__toolName".
|
||||
mcpName = "my-custom-server"
|
||||
@@ -360,5 +371,3 @@ out-of-the-box experience.
|
||||
- In **`yolo`** mode, a high-priority rule allows all tools.
|
||||
- In **`autoEdit`** mode, rules allow certain write operations to happen without
|
||||
prompting.
|
||||
|
||||
[Customizing Plan Mode Policies]: /docs/cli/plan-mode.md#customizing-policies
|
||||
|
||||
@@ -5,7 +5,7 @@ use cases. For enterprise or professional usage, or if you need increased quota,
|
||||
several options are available depending on your authentication account type.
|
||||
|
||||
For a high-level comparison of available subscriptions and to select the right
|
||||
quota for your needs, see the [Plans page](/plans/).
|
||||
quota for your needs, see the [Plans page](https://geminicli.com/plans/).
|
||||
|
||||
## Overview
|
||||
|
||||
@@ -69,6 +69,19 @@ Learn more at
|
||||
If you use up your initial number of requests, you can continue to benefit from
|
||||
Gemini CLI by upgrading to one of the following subscriptions:
|
||||
|
||||
### Individuals
|
||||
|
||||
These tiers apply when you sign in with a personal account. To verify whether
|
||||
you're on a personal account, visit
|
||||
[Google One](https://one.google.com/about/plans?hl=en-US&g1_landing_page=0):
|
||||
|
||||
- If you are on a personal account, you will see your personal dashboard.
|
||||
- If you are not on a personal account, you will see: "You're currently signed
|
||||
in to your Google Workspace Account."
|
||||
|
||||
**Supported tiers:** _- Tiers not listed above, including Google AI Plus, are
|
||||
not supported._
|
||||
|
||||
- [Google AI Pro and AI Ultra](https://gemini.google/subscriptions/). This is
|
||||
recommended for individual developers. Quotas and pricing are based on a fixed
|
||||
price subscription.
|
||||
@@ -78,6 +91,20 @@ Gemini CLI by upgrading to one of the following subscriptions:
|
||||
Learn more at
|
||||
[Gemini Code Assist Quotas and Limits](https://developers.google.com/gemini-code-assist/resources/quotas)
|
||||
|
||||
### Through your organization
|
||||
|
||||
These tiers are applicable when you are signing in with a Google Workspace
|
||||
account.
|
||||
|
||||
- To verify your account type, visit
|
||||
[the Google One page](https://one.google.com/about/plans?hl=en-US&g1_landing_page=0).
|
||||
- You are on a workspace account if you see the message "You're currently signed
|
||||
in to your Google Workspace Account".
|
||||
|
||||
**Supported tiers:** _- Tiers not listed above, including Workspace AI
|
||||
Standard/Plus and AI Expanded, are not supported._
|
||||
|
||||
- [Workspace AI Ultra Access](https://workspace.google.com/products/ai-ultra/).
|
||||
- [Purchase a Gemini Code Assist Subscription through Google Cloud](https://cloud.google.com/gemini/docs/codeassist/overview).
|
||||
|
||||
Quotas and pricing are based on a fixed price subscription with assigned
|
||||
@@ -93,7 +120,7 @@ Gemini CLI by upgrading to one of the following subscriptions:
|
||||
- Model requests will be made across the Gemini model family as determined by
|
||||
Gemini CLI.
|
||||
|
||||
[Learn more about Gemini Code Assist Standard and Enterprise license limits](https://developers.google.com/gemini-code-assist/resources/quotas#quotas-for-agent-mode-gemini-cli).
|
||||
[Learn more about Gemini Code Assist license limits](https://developers.google.com/gemini-code-assist/resources/quotas#quotas-for-agent-mode-gemini-cli).
|
||||
|
||||
## Pay as you go
|
||||
|
||||
|
||||
@@ -16,8 +16,8 @@ account.
|
||||
Your Gemini CLI Usage Statistics are handled in accordance with Google's Privacy
|
||||
Policy.
|
||||
|
||||
**Note:** See [quotas and pricing](/docs/resources/quota-and-pricing.md) for the
|
||||
quota and pricing details that apply to your usage of the Gemini CLI.
|
||||
**Note:** See [quotas and pricing](quota-and-pricing.md) for the quota and
|
||||
pricing details that apply to your usage of the Gemini CLI.
|
||||
|
||||
## Supported authentication methods
|
||||
|
||||
|
||||
@@ -106,6 +106,11 @@
|
||||
{ "label": "MCP servers", "slug": "docs/tools/mcp-server" },
|
||||
{ "label": "Model routing", "slug": "docs/cli/model-routing" },
|
||||
{ "label": "Model selection", "slug": "docs/cli/model" },
|
||||
{
|
||||
"label": "Notifications",
|
||||
"badge": "🔬",
|
||||
"slug": "docs/cli/notifications"
|
||||
},
|
||||
{ "label": "Plan mode", "badge": "🔬", "slug": "docs/cli/plan-mode" },
|
||||
{
|
||||
"label": "Subagents",
|
||||
@@ -144,6 +149,7 @@
|
||||
"label": "Project context (GEMINI.md)",
|
||||
"slug": "docs/cli/gemini-md"
|
||||
},
|
||||
{ "label": "Profiles", "slug": "docs/cli/profiles" },
|
||||
{ "label": "Settings", "slug": "docs/cli/settings" },
|
||||
{
|
||||
"label": "System prompt override",
|
||||
|
||||
@@ -67,7 +67,7 @@ Finds files matching specific glob patterns across the workspace.
|
||||
`Found 5 file(s) matching "*.ts" within src, sorted by modification time (newest first):\nsrc/file1.ts\nsrc/subdir/file2.ts...`
|
||||
- **Confirmation:** No.
|
||||
|
||||
## 5. `grep_search` (SearchText)
|
||||
### `grep_search` (SearchText)
|
||||
|
||||
`grep_search` searches for a regular expression pattern within the content of
|
||||
files in a specified directory. Can filter files by a glob pattern. Returns the
|
||||
@@ -103,7 +103,7 @@ lines containing matches, along with their file paths and line numbers.
|
||||
```
|
||||
- **Confirmation:** No.
|
||||
|
||||
## 6. `replace` (Edit)
|
||||
### `replace` (Edit)
|
||||
|
||||
`replace` replaces text within a file. By default, the tool expects to find and
|
||||
replace exactly ONE occurrence of `old_string`. If you want to replace multiple
|
||||
|
||||
@@ -372,7 +372,7 @@ To authenticate with a server using Service Account Impersonation, you must set
|
||||
the `authProviderType` to `service_account_impersonation` and provide the
|
||||
following properties:
|
||||
|
||||
- **`targetAudience`** (string): The OAuth Client ID allowslisted on the
|
||||
- **`targetAudience`** (string): The OAuth Client ID allowlisted on the
|
||||
IAP-protected application you are trying to access.
|
||||
- **`targetServiceAccount`** (string): The email address of the Google Cloud
|
||||
Service Account to impersonate.
|
||||
|
||||
@@ -35,6 +35,11 @@ const commonRestrictedSyntaxRules = [
|
||||
message:
|
||||
'Do not throw string literals or non-Error objects. Throw new Error("...") instead.',
|
||||
},
|
||||
{
|
||||
selector: 'CallExpression[callee.name="fetch"]',
|
||||
message:
|
||||
'Use safeFetch() from "@/utils/fetch" instead of the global fetch() to ensure SSRF protection. If you are implementing a custom security layer, use an eslint-disable comment and explain why.',
|
||||
},
|
||||
];
|
||||
|
||||
export default tseslint.config(
|
||||
@@ -132,7 +137,16 @@ export default tseslint.config(
|
||||
'no-cond-assign': 'error',
|
||||
'no-debugger': 'error',
|
||||
'no-duplicate-case': 'error',
|
||||
'no-restricted-syntax': ['error', ...commonRestrictedSyntaxRules],
|
||||
'no-restricted-syntax': [
|
||||
'error',
|
||||
...commonRestrictedSyntaxRules,
|
||||
{
|
||||
selector:
|
||||
'UnaryExpression[operator="typeof"] > MemberExpression[computed=true][property.type="Literal"]',
|
||||
message:
|
||||
'Do not use typeof to check object properties. Define a TypeScript interface and a type guard function instead.',
|
||||
},
|
||||
],
|
||||
'no-unsafe-finally': 'error',
|
||||
'no-unused-expressions': 'off', // Disable base rule
|
||||
'@typescript-eslint/no-unused-expressions': [
|
||||
@@ -263,6 +277,7 @@ export default tseslint.config(
|
||||
...vitest.configs.recommended.rules,
|
||||
'vitest/expect-expect': 'off',
|
||||
'vitest/no-commented-out-tests': 'off',
|
||||
'no-restricted-syntax': ['error', ...commonRestrictedSyntaxRules],
|
||||
},
|
||||
},
|
||||
{
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2026 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { expect } from 'vitest';
|
||||
import { evalTest } from './test-helper.js';
|
||||
|
||||
const MUTATION_AGENT_DEFINITION = `---
|
||||
name: mutation-agent
|
||||
description: An agent that modifies the workspace (writes, deletes, git operations, etc).
|
||||
max_turns: 1
|
||||
tools:
|
||||
- write_file
|
||||
---
|
||||
|
||||
You are the mutation agent. Do the mutation requested.
|
||||
`;
|
||||
|
||||
describe('concurrency safety eval test cases', () => {
|
||||
evalTest('USUALLY_PASSES', {
|
||||
name: 'mutation agents are run in parallel when explicitly requested',
|
||||
params: {
|
||||
settings: {
|
||||
experimental: {
|
||||
enableAgents: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
prompt:
|
||||
'Update A.txt to say "A" and update B.txt to say "B". Delegate these tasks to two separate mutation-agent subagents. You MUST run these subagents in parallel at the same time.',
|
||||
files: {
|
||||
'.gemini/agents/mutation-agent.md': MUTATION_AGENT_DEFINITION,
|
||||
},
|
||||
assert: async (rig) => {
|
||||
const logs = rig.readToolLogs();
|
||||
const mutationCalls = logs.filter(
|
||||
(log) => log.toolRequest?.name === 'mutation-agent',
|
||||
);
|
||||
|
||||
expect(
|
||||
mutationCalls.length,
|
||||
'Agent should have called the mutation-agent at least twice',
|
||||
).toBeGreaterThanOrEqual(2);
|
||||
|
||||
const firstPromptId = mutationCalls[0].toolRequest.prompt_id;
|
||||
const secondPromptId = mutationCalls[1].toolRequest.prompt_id;
|
||||
|
||||
expect(
|
||||
firstPromptId,
|
||||
'mutation agents should be called in parallel (same turn / prompt_ids) when explicitly requested',
|
||||
).toEqual(secondPromptId);
|
||||
},
|
||||
});
|
||||
});
|
||||
@@ -112,6 +112,7 @@ export function evalTest(policy: EvalPolicy, evalCase: EvalCase) {
|
||||
// commands.
|
||||
execSync('git config core.editor "true"', execOptions);
|
||||
execSync('git config core.pager "cat"', execOptions);
|
||||
execSync('git config commit.gpgsign false', execOptions);
|
||||
execSync('git add .', execOptions);
|
||||
execSync('git commit --allow-empty -m "Initial commit"', execOptions);
|
||||
}
|
||||
|
||||
@@ -104,7 +104,7 @@ describe('extension reloading', () => {
|
||||
return (
|
||||
output.includes(
|
||||
'test-server (from test-extension) - Ready (1 tool)',
|
||||
) && output.includes('- hello')
|
||||
) && output.includes('- mcp_test-server_hello')
|
||||
);
|
||||
},
|
||||
30000, // 30s timeout
|
||||
@@ -148,7 +148,7 @@ describe('extension reloading', () => {
|
||||
return (
|
||||
output.includes(
|
||||
'test-server (from test-extension) - Ready (1 tool)',
|
||||
) && output.includes('- goodbye')
|
||||
) && output.includes('- mcp_test-server_goodbye')
|
||||
);
|
||||
},
|
||||
30000,
|
||||
|
||||
@@ -182,14 +182,22 @@ describe('Hooks Agent Flow', () => {
|
||||
);
|
||||
|
||||
const afterAgentScript = `
|
||||
console.log(JSON.stringify({
|
||||
decision: 'block',
|
||||
reason: 'Security policy triggered',
|
||||
hookSpecificOutput: {
|
||||
hookEventName: 'AfterAgent',
|
||||
clearContext: true
|
||||
}
|
||||
}));
|
||||
const fs = require('fs');
|
||||
const input = JSON.parse(fs.readFileSync(0, 'utf-8'));
|
||||
if (input.stop_hook_active) {
|
||||
// Retry turn: allow execution to proceed (breaks the loop)
|
||||
console.log(JSON.stringify({ decision: 'allow' }));
|
||||
} else {
|
||||
// First call: block and clear context to trigger the retry
|
||||
console.log(JSON.stringify({
|
||||
decision: 'block',
|
||||
reason: 'Security policy triggered',
|
||||
hookSpecificOutput: {
|
||||
hookEventName: 'AfterAgent',
|
||||
clearContext: true
|
||||
}
|
||||
}));
|
||||
}
|
||||
`;
|
||||
const afterAgentScriptPath = rig.createScript(
|
||||
'after_agent_clear.cjs',
|
||||
@@ -198,8 +206,10 @@ describe('Hooks Agent Flow', () => {
|
||||
|
||||
rig.setup('should process clearContext in AfterAgent hook output', {
|
||||
settings: {
|
||||
hooks: {
|
||||
hooksConfig: {
|
||||
enabled: true,
|
||||
},
|
||||
hooks: {
|
||||
BeforeModel: [
|
||||
{
|
||||
hooks: [
|
||||
|
||||
@@ -1,2 +1,3 @@
|
||||
{"method":"generateContentStream","response":[{"candidates":[{"content":{"parts":[{"text":"Hi there!"}],"role":"model"},"finishReason":"STOP","index":0}]}]}
|
||||
{"method":"generateContentStream","response":[{"candidates":[{"content":{"parts":[{"text":"Clarification: I am a bot."}],"role":"model"},"finishReason":"STOP","index":0}]}]}
|
||||
{"method":"generateContentStream","response":[{"candidates":[{"content":{"parts":[{"text":"Security policy triggered"}],"role":"model"},"finishReason":"STOP","index":0}]}]}
|
||||
|
||||
@@ -68,6 +68,7 @@ async function verifyToolExecution(
|
||||
promptCommand: PromptCommand,
|
||||
result: string,
|
||||
expectAllowed: boolean,
|
||||
expectedDenialString?: string,
|
||||
) {
|
||||
const log = await waitForToolCallLog(
|
||||
rig,
|
||||
@@ -78,10 +79,13 @@ async function verifyToolExecution(
|
||||
if (expectAllowed) {
|
||||
expect(log!.toolRequest.success).toBe(true);
|
||||
expect(result).not.toContain('Tool execution denied by policy');
|
||||
expect(result).not.toContain(`Tool "${promptCommand.tool}" not found`);
|
||||
expect(result).toContain(promptCommand.expectedSuccessResult);
|
||||
} else {
|
||||
expect(log!.toolRequest.success).toBe(false);
|
||||
expect(result).toContain('Tool execution denied by policy');
|
||||
expect(result).toContain(
|
||||
expectedDenialString || 'Tool execution denied by policy',
|
||||
);
|
||||
expect(result).toContain(promptCommand.expectedFailureResult);
|
||||
}
|
||||
}
|
||||
@@ -92,6 +96,7 @@ interface TestCase {
|
||||
promptCommand: PromptCommand;
|
||||
policyContent?: string;
|
||||
expectAllowed: boolean;
|
||||
expectedDenialString?: string;
|
||||
}
|
||||
|
||||
describe('Policy Engine Headless Mode', () => {
|
||||
@@ -125,7 +130,13 @@ describe('Policy Engine Headless Mode', () => {
|
||||
approvalMode: 'default',
|
||||
});
|
||||
|
||||
await verifyToolExecution(rig, tc.promptCommand, result, tc.expectAllowed);
|
||||
await verifyToolExecution(
|
||||
rig,
|
||||
tc.promptCommand,
|
||||
result,
|
||||
tc.expectAllowed,
|
||||
tc.expectedDenialString,
|
||||
);
|
||||
};
|
||||
|
||||
const testCases = [
|
||||
@@ -134,6 +145,7 @@ describe('Policy Engine Headless Mode', () => {
|
||||
responsesFile: 'policy-headless-shell-denied.responses',
|
||||
promptCommand: ECHO_PROMPT,
|
||||
expectAllowed: false,
|
||||
expectedDenialString: 'Tool "run_shell_command" not found',
|
||||
},
|
||||
{
|
||||
name: 'should allow ASK_USER tools in headless mode if explicitly allowed via policy file',
|
||||
@@ -178,6 +190,7 @@ describe('Policy Engine Headless Mode', () => {
|
||||
priority = 100
|
||||
`,
|
||||
expectAllowed: false,
|
||||
expectedDenialString: 'Tool execution denied by policy',
|
||||
},
|
||||
];
|
||||
|
||||
|
||||
@@ -84,9 +84,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@a2a-js/sdk": {
|
||||
"version": "0.3.8",
|
||||
"resolved": "https://registry.npmjs.org/@a2a-js/sdk/-/sdk-0.3.8.tgz",
|
||||
"integrity": "sha512-vAg6JQbhOnHTzApsB7nGzCQ9r7PuY4GMr8gt88dIR8Wc8G8RSqVTyTmFeMurgzcYrtHYXS3ru2rnDoGj9UDeSw==",
|
||||
"version": "0.3.11",
|
||||
"resolved": "https://registry.npmjs.org/@a2a-js/sdk/-/sdk-0.3.11.tgz",
|
||||
"integrity": "sha512-pXjjlL0ZYHgAxObov1J+W3ylfQV0rOrDBB8Eo4a9eCunqs7iNW5OIfMcV8YnZQdzeVSRomj8jHeudVz0zc4RNw==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"uuid": "^11.1.0"
|
||||
@@ -95,9 +95,17 @@
|
||||
"node": ">=18"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@bufbuild/protobuf": "^2.10.2",
|
||||
"@grpc/grpc-js": "^1.11.0",
|
||||
"express": "^4.21.2 || ^5.1.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@bufbuild/protobuf": {
|
||||
"optional": true
|
||||
},
|
||||
"@grpc/grpc-js": {
|
||||
"optional": true
|
||||
},
|
||||
"express": {
|
||||
"optional": true
|
||||
}
|
||||
@@ -515,6 +523,13 @@
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@bufbuild/protobuf": {
|
||||
"version": "2.11.0",
|
||||
"resolved": "https://registry.npmjs.org/@bufbuild/protobuf/-/protobuf-2.11.0.tgz",
|
||||
"integrity": "sha512-sBXGT13cpmPR5BMgHE6UEEfEaShh5Ror6rfN3yEK5si7QVrtZg8LEPQb0VVhiLRUslD2yLnXtnRzG035J/mZXQ==",
|
||||
"license": "(Apache-2.0 AND BSD-3-Clause)",
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/@bundled-es-modules/cookie": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@bundled-es-modules/cookie/-/cookie-2.0.1.tgz",
|
||||
@@ -1582,18 +1597,37 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@grpc/grpc-js": {
|
||||
"version": "1.13.4",
|
||||
"resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.13.4.tgz",
|
||||
"integrity": "sha512-GsFaMXCkMqkKIvwCQjCrwH+GHbPKBjhwo/8ZuUkWHqbI73Kky9I+pQltrlT0+MWpedCoosda53lgjYfyEPgxBg==",
|
||||
"version": "1.14.3",
|
||||
"resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.14.3.tgz",
|
||||
"integrity": "sha512-Iq8QQQ/7X3Sac15oB6p0FmUg/klxQvXLeileoqrTRGJYLV+/9tubbr9ipz0GKHjmXVsgFPo/+W+2cA8eNcR+XA==",
|
||||
"license": "Apache-2.0",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@grpc/proto-loader": "^0.7.13",
|
||||
"@grpc/proto-loader": "^0.8.0",
|
||||
"@js-sdsl/ordered-map": "^4.4.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@grpc/grpc-js/node_modules/@grpc/proto-loader": {
|
||||
"version": "0.8.0",
|
||||
"resolved": "https://registry.npmjs.org/@grpc/proto-loader/-/proto-loader-0.8.0.tgz",
|
||||
"integrity": "sha512-rc1hOQtjIWGxcxpb9aHAfLpIctjEnsDehj0DAiVfBlmT84uvR0uUtN2hEi/ecvWVjXUGf5qPF4qEgiLOx1YIMQ==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"lodash.camelcase": "^4.3.0",
|
||||
"long": "^5.0.0",
|
||||
"protobufjs": "^7.5.3",
|
||||
"yargs": "^17.7.2"
|
||||
},
|
||||
"bin": {
|
||||
"proto-loader-gen-types": "build/bin/proto-loader-gen-types.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/@grpc/proto-loader": {
|
||||
"version": "0.7.15",
|
||||
"resolved": "https://registry.npmjs.org/@grpc/proto-loader/-/proto-loader-0.7.15.tgz",
|
||||
@@ -2292,6 +2326,7 @@
|
||||
"integrity": "sha512-t54CUOsFMappY1Jbzb7fetWeO0n6K0k/4+/ZpkS+3Joz8I4VcvY9OiEBFRYISqaI2fq5sCiPtAjRDOzVYG8m+Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@octokit/auth-token": "^6.0.0",
|
||||
"@octokit/graphql": "^9.0.2",
|
||||
@@ -2472,6 +2507,7 @@
|
||||
"resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.9.0.tgz",
|
||||
"integrity": "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==",
|
||||
"license": "Apache-2.0",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=8.0.0"
|
||||
}
|
||||
@@ -2521,6 +2557,7 @@
|
||||
"resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-2.5.0.tgz",
|
||||
"integrity": "sha512-ka4H8OM6+DlUhSAZpONu0cPBtPPTQKxbxVzC4CzVx5+K4JnroJVBtDzLAMx4/3CDTJXRvVFhpFjtl4SaiTNoyQ==",
|
||||
"license": "Apache-2.0",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@opentelemetry/semantic-conventions": "^1.29.0"
|
||||
},
|
||||
@@ -2895,6 +2932,7 @@
|
||||
"resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.5.0.tgz",
|
||||
"integrity": "sha512-F8W52ApePshpoSrfsSk1H2yJn9aKjCrbpQF1M9Qii0GHzbfVeFUB+rc3X4aggyZD8x9Gu3Slua+s6krmq6Dt8g==",
|
||||
"license": "Apache-2.0",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@opentelemetry/core": "2.5.0",
|
||||
"@opentelemetry/semantic-conventions": "^1.29.0"
|
||||
@@ -2928,6 +2966,7 @@
|
||||
"resolved": "https://registry.npmjs.org/@opentelemetry/sdk-metrics/-/sdk-metrics-2.5.0.tgz",
|
||||
"integrity": "sha512-BeJLtU+f5Gf905cJX9vXFQorAr6TAfK3SPvTFqP+scfIpDQEJfRaGJWta7sJgP+m4dNtBf9y3yvBKVAZZtJQVA==",
|
||||
"license": "Apache-2.0",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@opentelemetry/core": "2.5.0",
|
||||
"@opentelemetry/resources": "2.5.0"
|
||||
@@ -2982,6 +3021,7 @@
|
||||
"resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace-base/-/sdk-trace-base-2.5.0.tgz",
|
||||
"integrity": "sha512-VzRf8LzotASEyNDUxTdaJ9IRJ1/h692WyArDBInf5puLCjxbICD6XkHgpuudis56EndyS7LYFmtTMny6UABNdQ==",
|
||||
"license": "Apache-2.0",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@opentelemetry/core": "2.5.0",
|
||||
"@opentelemetry/resources": "2.5.0",
|
||||
@@ -4178,6 +4218,7 @@
|
||||
"integrity": "sha512-6mDvHUFSjyT2B2yeNx2nUgMxh9LtOWvkhIU3uePn2I2oyNymUAX1NIsdgviM4CH+JSrp2D2hsMvJOkxY+0wNRA==",
|
||||
"devOptional": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"csstype": "^3.0.2"
|
||||
}
|
||||
@@ -4451,6 +4492,7 @@
|
||||
"integrity": "sha512-klQbnPAAiGYFyI02+znpBRLyjL4/BrBd0nyWkdC0s/6xFLkXYQ8OoRrSkqacS1ddVxf/LDyODIKbQ5TgKAf/Fg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@typescript-eslint/scope-manager": "8.56.1",
|
||||
"@typescript-eslint/types": "8.56.1",
|
||||
@@ -5298,6 +5340,7 @@
|
||||
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz",
|
||||
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"acorn": "bin/acorn"
|
||||
},
|
||||
@@ -7901,6 +7944,7 @@
|
||||
"integrity": "sha512-VmQ+sifHUbI/IcSopBCF/HO3YiHQx/AVd3UVyYL6weuwW+HvON9VYn5l6Zl1WZzPWXPNZrSQpxwkkZ/VuvJZzg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@eslint-community/eslint-utils": "^4.8.0",
|
||||
"@eslint-community/regexpp": "^4.12.1",
|
||||
@@ -8533,6 +8577,7 @@
|
||||
"resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz",
|
||||
"integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"accepts": "^2.0.0",
|
||||
"body-parser": "^2.2.1",
|
||||
@@ -9847,6 +9892,7 @@
|
||||
"resolved": "https://registry.npmjs.org/hono/-/hono-4.12.2.tgz",
|
||||
"integrity": "sha512-gJnaDHXKDayjt8ue0n8Gs0A007yKXj4Xzb8+cNjZeYsSzzwKc0Lr+OZgYwVfB0pHfUs17EPoLvrOsEaJ9mj+Tg==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=16.9.0"
|
||||
}
|
||||
@@ -10126,6 +10172,7 @@
|
||||
"resolved": "https://registry.npmjs.org/@jrichman/ink/-/ink-6.4.11.tgz",
|
||||
"integrity": "sha512-93LQlzT7vvZ1XJcmOMwN4s+6W334QegendeHOMnEJBlhnpIzr8bws6/aOEHG8ZCuVD/vNeeea5m1msHIdAY6ig==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@alcalzone/ansi-tokenize": "^0.2.1",
|
||||
"ansi-escapes": "^7.0.0",
|
||||
@@ -13808,6 +13855,7 @@
|
||||
"resolved": "https://registry.npmjs.org/react/-/react-19.2.4.tgz",
|
||||
"integrity": "sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
@@ -13818,6 +13866,7 @@
|
||||
"integrity": "sha512-ePrwPfxAnB+7hgnEr8vpKxL9cmnp7F322t8oqcPshbIQQhDKgFDW4tjhF2wjVbdXF9O/nyuy3sQWd9JGpiLPvA==",
|
||||
"devOptional": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"shell-quote": "^1.6.1",
|
||||
"ws": "^7"
|
||||
@@ -15906,6 +15955,7 @@
|
||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
|
||||
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
@@ -16129,7 +16179,8 @@
|
||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
|
||||
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
|
||||
"dev": true,
|
||||
"license": "0BSD"
|
||||
"license": "0BSD",
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/tsx": {
|
||||
"version": "4.20.3",
|
||||
@@ -16137,6 +16188,7 @@
|
||||
"integrity": "sha512-qjbnuR9Tr+FJOMBqJCW5ehvIo/buZq7vH7qD7JziU98h6l3qGy0a/yPFjwO+y0/T7GFpNgNAvEcPPVfyT8rrPQ==",
|
||||
"devOptional": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"esbuild": "~0.25.0",
|
||||
"get-tsconfig": "^4.7.5"
|
||||
@@ -16296,6 +16348,7 @@
|
||||
"integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==",
|
||||
"devOptional": true,
|
||||
"license": "Apache-2.0",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"tsc": "bin/tsc",
|
||||
"tsserver": "bin/tsserver"
|
||||
@@ -16519,6 +16572,7 @@
|
||||
"resolved": "https://registry.npmjs.org/vite/-/vite-7.2.2.tgz",
|
||||
"integrity": "sha512-BxAKBWmIbrDgrokdGZH1IgkIk/5mMHDreLDmCJ0qpyJaAteP8NvMhkwr/ZCQNqNH97bw/dANTE9PDzqwJghfMQ==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"esbuild": "^0.25.0",
|
||||
"fdir": "^6.5.0",
|
||||
@@ -16632,6 +16686,7 @@
|
||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
|
||||
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
@@ -16644,6 +16699,7 @@
|
||||
"resolved": "https://registry.npmjs.org/vitest/-/vitest-3.2.4.tgz",
|
||||
"integrity": "sha512-LUCP5ev3GURDysTWiP47wRRUpLKMOfPh+yKTx3kVIEiu5KOMeqzpnYNsKyOoVrULivR8tLcks4+lga33Whn90A==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@types/chai": "^5.2.2",
|
||||
"@vitest/expect": "3.2.4",
|
||||
@@ -17288,6 +17344,7 @@
|
||||
"resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz",
|
||||
"integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/colinhacks"
|
||||
}
|
||||
@@ -17305,7 +17362,7 @@
|
||||
"name": "@google/gemini-cli-a2a-server",
|
||||
"version": "0.34.0-nightly.20260304.28af4e127",
|
||||
"dependencies": {
|
||||
"@a2a-js/sdk": "^0.3.8",
|
||||
"@a2a-js/sdk": "0.3.11",
|
||||
"@google-cloud/storage": "^7.16.0",
|
||||
"@google/gemini-cli-core": "file:../core",
|
||||
"express": "^5.1.0",
|
||||
@@ -17447,11 +17504,13 @@
|
||||
"version": "0.34.0-nightly.20260304.28af4e127",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@a2a-js/sdk": "^0.3.8",
|
||||
"@a2a-js/sdk": "0.3.11",
|
||||
"@bufbuild/protobuf": "^2.11.0",
|
||||
"@google-cloud/logging": "^11.2.1",
|
||||
"@google-cloud/opentelemetry-cloud-monitoring-exporter": "^0.21.0",
|
||||
"@google-cloud/opentelemetry-cloud-trace-exporter": "^3.0.0",
|
||||
"@google/genai": "1.41.0",
|
||||
"@grpc/grpc-js": "^1.14.3",
|
||||
"@iarna/toml": "^2.2.5",
|
||||
"@joshua.litt/get-ripgrep": "^0.0.3",
|
||||
"@modelcontextprotocol/sdk": "^1.23.0",
|
||||
@@ -17489,6 +17548,7 @@
|
||||
"html-to-text": "^9.0.5",
|
||||
"https-proxy-agent": "^7.0.6",
|
||||
"ignore": "^7.0.0",
|
||||
"ipaddr.js": "^1.9.1",
|
||||
"js-yaml": "^4.1.1",
|
||||
"marked": "^15.0.12",
|
||||
"mime": "4.0.7",
|
||||
@@ -17687,6 +17747,7 @@
|
||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
|
||||
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
|
||||
@@ -25,7 +25,7 @@
|
||||
"dist"
|
||||
],
|
||||
"dependencies": {
|
||||
"@a2a-js/sdk": "^0.3.8",
|
||||
"@a2a-js/sdk": "0.3.11",
|
||||
"@google-cloud/storage": "^7.16.0",
|
||||
"@google/gemini-cli-core": "file:../core",
|
||||
"express": "^5.1.0",
|
||||
|
||||
@@ -832,7 +832,9 @@ export class Task {
|
||||
if (
|
||||
part.kind !== 'data' ||
|
||||
!part.data ||
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
typeof part.data['callId'] !== 'string' ||
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
typeof part.data['outcome'] !== 'string'
|
||||
) {
|
||||
return false;
|
||||
|
||||
@@ -120,7 +120,6 @@ export async function loadConfig(
|
||||
await loadServerHierarchicalMemory(
|
||||
workspaceDir,
|
||||
[workspaceDir],
|
||||
false,
|
||||
fileService,
|
||||
extensionLoader,
|
||||
folderTrust,
|
||||
|
||||
@@ -75,6 +75,14 @@ export function createMockConfig(
|
||||
validatePathAccess: vi.fn().mockReturnValue(undefined),
|
||||
...overrides,
|
||||
} as unknown as Config;
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
|
||||
(mockConfig as unknown as { config: Config; promptId: string }).config =
|
||||
mockConfig;
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
|
||||
(mockConfig as unknown as { config: Config; promptId: string }).promptId =
|
||||
'test-prompt-id';
|
||||
|
||||
mockConfig.getMessageBus = vi.fn().mockReturnValue(createMockMessageBus());
|
||||
mockConfig.getHookSystem = vi
|
||||
.fn()
|
||||
|
||||
@@ -172,7 +172,7 @@ describe('GeminiAgent', () => {
|
||||
unsubscribe: vi.fn(),
|
||||
}),
|
||||
getApprovalMode: vi.fn().mockReturnValue('default'),
|
||||
isPlanEnabled: vi.fn().mockReturnValue(false),
|
||||
isPlanEnabled: vi.fn().mockReturnValue(true),
|
||||
getGemini31LaunchedSync: vi.fn().mockReturnValue(false),
|
||||
getHasAccessToPreviewModel: vi.fn().mockReturnValue(false),
|
||||
getCheckpointingEnabled: vi.fn().mockReturnValue(false),
|
||||
@@ -650,7 +650,7 @@ describe('Session', () => {
|
||||
getMessageBus: vi.fn().mockReturnValue(mockMessageBus),
|
||||
setApprovalMode: vi.fn(),
|
||||
setModel: vi.fn(),
|
||||
isPlanEnabled: vi.fn().mockReturnValue(false),
|
||||
isPlanEnabled: vi.fn().mockReturnValue(true),
|
||||
getCheckpointingEnabled: vi.fn().mockReturnValue(false),
|
||||
getGitService: vi.fn().mockResolvedValue({} as GitService),
|
||||
waitForMcpInit: vi.fn(),
|
||||
|
||||
@@ -92,7 +92,7 @@ describe('GeminiAgent Session Resume', () => {
|
||||
getProjectTempDir: vi.fn().mockReturnValue('/tmp/project'),
|
||||
},
|
||||
getApprovalMode: vi.fn().mockReturnValue('default'),
|
||||
isPlanEnabled: vi.fn().mockReturnValue(false),
|
||||
isPlanEnabled: vi.fn().mockReturnValue(true),
|
||||
getModel: vi.fn().mockReturnValue('gemini-pro'),
|
||||
getHasAccessToPreviewModel: vi.fn().mockReturnValue(false),
|
||||
getGemini31LaunchedSync: vi.fn().mockReturnValue(false),
|
||||
@@ -204,6 +204,11 @@ describe('GeminiAgent Session Resume', () => {
|
||||
name: 'YOLO',
|
||||
description: 'Auto-approves all tools',
|
||||
},
|
||||
{
|
||||
id: ApprovalMode.PLAN,
|
||||
name: 'Plan',
|
||||
description: 'Read-only mode',
|
||||
},
|
||||
],
|
||||
currentModeId: ApprovalMode.DEFAULT,
|
||||
},
|
||||
|
||||
@@ -9,7 +9,9 @@ import { coreEvents } from '@google/gemini-cli-core';
|
||||
import { handleList, listCommand } from './list.js';
|
||||
import { ExtensionManager } from '../../config/extension-manager.js';
|
||||
import { loadSettings, type LoadedSettings } from '../../config/settings.js';
|
||||
import { loadCliConfig, type CliArgs } from '../../config/config.js';
|
||||
import { getErrorMessage } from '../../utils/errors.js';
|
||||
import type { Config } from '@google/gemini-cli-core';
|
||||
|
||||
vi.mock('@google/gemini-cli-core', async (importOriginal) => {
|
||||
const { mockCoreDebugLogger } = await import(
|
||||
@@ -25,6 +27,7 @@ vi.mock('@google/gemini-cli-core', async (importOriginal) => {
|
||||
|
||||
vi.mock('../../config/extension-manager.js');
|
||||
vi.mock('../../config/settings.js');
|
||||
vi.mock('../../config/config.js');
|
||||
vi.mock('../../utils/errors.js');
|
||||
vi.mock('../../config/extensions/consent.js', () => ({
|
||||
requestConsentNonInteractive: vi.fn(),
|
||||
@@ -40,6 +43,7 @@ describe('extensions list command', () => {
|
||||
const mockLoadSettings = vi.mocked(loadSettings);
|
||||
const mockGetErrorMessage = vi.mocked(getErrorMessage);
|
||||
const mockExtensionManager = vi.mocked(ExtensionManager);
|
||||
const mockLoadCliConfig = vi.mocked(loadCliConfig);
|
||||
|
||||
beforeEach(async () => {
|
||||
vi.clearAllMocks();
|
||||
@@ -55,10 +59,17 @@ describe('extensions list command', () => {
|
||||
describe('handleList', () => {
|
||||
it('should log a message if no extensions are installed', async () => {
|
||||
const mockCwd = vi.spyOn(process, 'cwd').mockReturnValue('/test/dir');
|
||||
const mockConfig = {
|
||||
initialize: vi.fn().mockResolvedValue(undefined),
|
||||
getExtensionLoader: vi
|
||||
.fn()
|
||||
.mockReturnValue(mockExtensionManager.prototype),
|
||||
};
|
||||
mockLoadCliConfig.mockResolvedValue(mockConfig as unknown as Config);
|
||||
mockExtensionManager.prototype.loadExtensions = vi
|
||||
.fn()
|
||||
.mockResolvedValue([]);
|
||||
await handleList();
|
||||
await handleList({} as unknown as CliArgs);
|
||||
|
||||
expect(coreEvents.emitConsoleLog).toHaveBeenCalledWith(
|
||||
'log',
|
||||
@@ -69,10 +80,17 @@ describe('extensions list command', () => {
|
||||
|
||||
it('should output empty JSON array if no extensions are installed and output-format is json', async () => {
|
||||
const mockCwd = vi.spyOn(process, 'cwd').mockReturnValue('/test/dir');
|
||||
const mockConfig = {
|
||||
initialize: vi.fn().mockResolvedValue(undefined),
|
||||
getExtensionLoader: vi
|
||||
.fn()
|
||||
.mockReturnValue(mockExtensionManager.prototype),
|
||||
};
|
||||
mockLoadCliConfig.mockResolvedValue(mockConfig as unknown as Config);
|
||||
mockExtensionManager.prototype.loadExtensions = vi
|
||||
.fn()
|
||||
.mockResolvedValue([]);
|
||||
await handleList({ outputFormat: 'json' });
|
||||
await handleList({} as unknown as CliArgs, { outputFormat: 'json' });
|
||||
|
||||
expect(coreEvents.emitConsoleLog).toHaveBeenCalledWith('log', '[]');
|
||||
mockCwd.mockRestore();
|
||||
@@ -80,6 +98,14 @@ describe('extensions list command', () => {
|
||||
|
||||
it('should list all installed extensions', async () => {
|
||||
const mockCwd = vi.spyOn(process, 'cwd').mockReturnValue('/test/dir');
|
||||
const mockConfig = {
|
||||
initialize: vi.fn().mockResolvedValue(undefined),
|
||||
getExtensionLoader: vi
|
||||
.fn()
|
||||
.mockReturnValue(mockExtensionManager.prototype),
|
||||
};
|
||||
mockLoadCliConfig.mockResolvedValue(mockConfig as unknown as Config);
|
||||
|
||||
const extensions = [
|
||||
{ name: 'ext1', version: '1.0.0' },
|
||||
{ name: 'ext2', version: '2.0.0' },
|
||||
@@ -90,7 +116,7 @@ describe('extensions list command', () => {
|
||||
mockExtensionManager.prototype.toOutputString = vi.fn(
|
||||
(ext) => `${ext.name}@${ext.version}`,
|
||||
);
|
||||
await handleList();
|
||||
await handleList({} as unknown as CliArgs);
|
||||
|
||||
expect(coreEvents.emitConsoleLog).toHaveBeenCalledWith(
|
||||
'log',
|
||||
@@ -101,6 +127,14 @@ describe('extensions list command', () => {
|
||||
|
||||
it('should list all installed extensions in JSON format', async () => {
|
||||
const mockCwd = vi.spyOn(process, 'cwd').mockReturnValue('/test/dir');
|
||||
const mockConfig = {
|
||||
initialize: vi.fn().mockResolvedValue(undefined),
|
||||
getExtensionLoader: vi
|
||||
.fn()
|
||||
.mockReturnValue(mockExtensionManager.prototype),
|
||||
};
|
||||
mockLoadCliConfig.mockResolvedValue(mockConfig as unknown as Config);
|
||||
|
||||
const extensions = [
|
||||
{ name: 'ext1', version: '1.0.0' },
|
||||
{ name: 'ext2', version: '2.0.0' },
|
||||
@@ -108,7 +142,7 @@ describe('extensions list command', () => {
|
||||
mockExtensionManager.prototype.loadExtensions = vi
|
||||
.fn()
|
||||
.mockResolvedValue(extensions);
|
||||
await handleList({ outputFormat: 'json' });
|
||||
await handleList({} as unknown as CliArgs, { outputFormat: 'json' });
|
||||
|
||||
expect(coreEvents.emitConsoleLog).toHaveBeenCalledWith(
|
||||
'log',
|
||||
@@ -124,12 +158,11 @@ describe('extensions list command', () => {
|
||||
code?: string | number | null | undefined,
|
||||
) => never);
|
||||
const error = new Error('List failed');
|
||||
mockExtensionManager.prototype.loadExtensions = vi
|
||||
.fn()
|
||||
.mockRejectedValue(error);
|
||||
mockLoadCliConfig.mockRejectedValue(error);
|
||||
mockGetErrorMessage.mockReturnValue('List failed message');
|
||||
|
||||
await handleList();
|
||||
|
||||
await handleList({} as unknown as CliArgs);
|
||||
|
||||
expect(coreEvents.emitConsoleLog).toHaveBeenCalledWith(
|
||||
'error',
|
||||
@@ -167,6 +200,13 @@ describe('extensions list command', () => {
|
||||
});
|
||||
|
||||
it('handler should call handleList with parsed arguments', async () => {
|
||||
const mockConfig = {
|
||||
initialize: vi.fn().mockResolvedValue(undefined),
|
||||
getExtensionLoader: vi
|
||||
.fn()
|
||||
.mockReturnValue(mockExtensionManager.prototype),
|
||||
};
|
||||
mockLoadCliConfig.mockResolvedValue(mockConfig as unknown as Config);
|
||||
mockExtensionManager.prototype.loadExtensions = vi
|
||||
.fn()
|
||||
.mockResolvedValue([]);
|
||||
@@ -177,7 +217,7 @@ describe('extensions list command', () => {
|
||||
)({
|
||||
'output-format': 'json',
|
||||
});
|
||||
expect(mockExtensionManager.prototype.loadExtensions).toHaveBeenCalled();
|
||||
expect(mockLoadCliConfig).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -7,22 +7,30 @@
|
||||
import type { CommandModule } from 'yargs';
|
||||
import { getErrorMessage } from '../../utils/errors.js';
|
||||
import { debugLogger } from '@google/gemini-cli-core';
|
||||
import { ExtensionManager } from '../../config/extension-manager.js';
|
||||
import { requestConsentNonInteractive } from '../../config/extensions/consent.js';
|
||||
import { loadCliConfig, type CliArgs } from '../../config/config.js';
|
||||
import { loadSettings } from '../../config/settings.js';
|
||||
import { promptForSetting } from '../../config/extensions/extensionSettings.js';
|
||||
import type { ExtensionManager } from '../../config/extension-manager.js';
|
||||
import { exitCli } from '../utils.js';
|
||||
|
||||
export async function handleList(options?: { outputFormat?: 'text' | 'json' }) {
|
||||
export async function handleList(
|
||||
argv: CliArgs,
|
||||
options?: { outputFormat?: 'text' | 'json' },
|
||||
) {
|
||||
try {
|
||||
const workspaceDir = process.cwd();
|
||||
const extensionManager = new ExtensionManager({
|
||||
workspaceDir,
|
||||
requestConsent: requestConsentNonInteractive,
|
||||
requestSetting: promptForSetting,
|
||||
settings: loadSettings(workspaceDir).merged,
|
||||
});
|
||||
const extensions = await extensionManager.loadExtensions();
|
||||
const settings = loadSettings(workspaceDir);
|
||||
const config = await loadCliConfig(
|
||||
settings.merged,
|
||||
'extensions-list-session',
|
||||
argv,
|
||||
{ cwd: workspaceDir },
|
||||
);
|
||||
|
||||
// Initialize to trigger extension loading (and profile filtering)
|
||||
await config.initialize();
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
|
||||
const extensionManager = config.getExtensionLoader() as ExtensionManager;
|
||||
const extensions = extensionManager.getExtensions();
|
||||
if (extensions.length === 0) {
|
||||
if (options?.outputFormat === 'json') {
|
||||
debugLogger.log('[]');
|
||||
@@ -61,7 +69,8 @@ export const listCommand: CommandModule = {
|
||||
default: 'text',
|
||||
}),
|
||||
handler: async (argv) => {
|
||||
await handleList({
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
|
||||
await handleList(argv as unknown as CliArgs, {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
|
||||
outputFormat: argv['output-format'] as 'text' | 'json',
|
||||
});
|
||||
|
||||
@@ -79,6 +79,7 @@ function migrateClaudeHook(claudeHook: unknown): unknown {
|
||||
migrated['command'] = hook['command'];
|
||||
|
||||
// Replace CLAUDE_PROJECT_DIR with GEMINI_PROJECT_DIR in command
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
if (typeof migrated['command'] === 'string') {
|
||||
migrated['command'] = migrated['command'].replace(
|
||||
/\$CLAUDE_PROJECT_DIR/g,
|
||||
@@ -93,6 +94,7 @@ function migrateClaudeHook(claudeHook: unknown): unknown {
|
||||
}
|
||||
|
||||
// Map timeout field (Claude uses seconds, Gemini uses seconds)
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
if ('timeout' in hook && typeof hook['timeout'] === 'number') {
|
||||
migrated['timeout'] = hook['timeout'];
|
||||
}
|
||||
@@ -140,6 +142,7 @@ function migrateClaudeHooks(claudeConfig: unknown): Record<string, unknown> {
|
||||
// Transform matcher
|
||||
if (
|
||||
'matcher' in definition &&
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
typeof definition['matcher'] === 'string'
|
||||
) {
|
||||
migratedDef['matcher'] = transformMatcher(definition['matcher']);
|
||||
|
||||
@@ -14,30 +14,42 @@ import {
|
||||
import { loadSettings } from '../../config/settings.js';
|
||||
import { exitCli } from '../utils.js';
|
||||
import { getMcpServersFromConfig } from './list.js';
|
||||
import { loadCliConfig, type CliArgs } from '../../config/config.js';
|
||||
import type { ExtensionManager } from '../../config/extension-manager.js';
|
||||
|
||||
const GREEN = '\x1b[32m';
|
||||
const YELLOW = '\x1b[33m';
|
||||
const RED = '\x1b[31m';
|
||||
const RESET = '\x1b[0m';
|
||||
|
||||
interface Args {
|
||||
interface Args extends CliArgs {
|
||||
name: string;
|
||||
session?: boolean;
|
||||
}
|
||||
|
||||
async function handleEnable(args: Args): Promise<void> {
|
||||
async function handleEnable(argv: Args): Promise<void> {
|
||||
const manager = McpServerEnablementManager.getInstance();
|
||||
const name = normalizeServerId(args.name);
|
||||
const name = normalizeServerId(argv.name);
|
||||
|
||||
// Check settings blocks
|
||||
const settings = loadSettings();
|
||||
|
||||
const config = await loadCliConfig(settings.merged, 'mcp-enable', argv, {
|
||||
cwd: process.cwd(),
|
||||
});
|
||||
await config.initialize();
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
|
||||
const extensionManager = config.getExtensionLoader() as ExtensionManager;
|
||||
|
||||
// Get all servers including extensions
|
||||
const servers = await getMcpServersFromConfig();
|
||||
const { mcpServers: servers } = await getMcpServersFromConfig(
|
||||
settings.merged,
|
||||
extensionManager,
|
||||
);
|
||||
const normalizedServerNames = Object.keys(servers).map(normalizeServerId);
|
||||
if (!normalizedServerNames.includes(name)) {
|
||||
debugLogger.log(
|
||||
`${RED}Error:${RESET} Server '${args.name}' not found. Use 'gemini mcp' to see available servers.`,
|
||||
`${RED}Error:${RESET} Server '${argv.name}' not found. Use 'gemini mcp' to see available servers.`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
@@ -56,7 +68,7 @@ async function handleEnable(args: Args): Promise<void> {
|
||||
return;
|
||||
}
|
||||
|
||||
if (args.session) {
|
||||
if (argv.session) {
|
||||
manager.clearSessionDisable(name);
|
||||
debugLogger.log(`${GREEN}✓${RESET} Session disable cleared for '${name}'.`);
|
||||
} else {
|
||||
@@ -71,21 +83,34 @@ async function handleEnable(args: Args): Promise<void> {
|
||||
}
|
||||
}
|
||||
|
||||
async function handleDisable(args: Args): Promise<void> {
|
||||
async function handleDisable(argv: Args): Promise<void> {
|
||||
const manager = McpServerEnablementManager.getInstance();
|
||||
const name = normalizeServerId(args.name);
|
||||
const name = normalizeServerId(argv.name);
|
||||
|
||||
// Check settings blocks
|
||||
const settings = loadSettings();
|
||||
|
||||
const config = await loadCliConfig(settings.merged, 'mcp-disable', argv, {
|
||||
cwd: process.cwd(),
|
||||
});
|
||||
await config.initialize();
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
|
||||
const extensionManager = config.getExtensionLoader() as ExtensionManager;
|
||||
|
||||
// Get all servers including extensions
|
||||
const servers = await getMcpServersFromConfig();
|
||||
const { mcpServers: servers } = await getMcpServersFromConfig(
|
||||
settings.merged,
|
||||
extensionManager,
|
||||
);
|
||||
const normalizedServerNames = Object.keys(servers).map(normalizeServerId);
|
||||
if (!normalizedServerNames.includes(name)) {
|
||||
debugLogger.log(
|
||||
`${RED}Error:${RESET} Server '${args.name}' not found. Use 'gemini mcp' to see available servers.`,
|
||||
`${RED}Error:${RESET} Server '${argv.name}' not found. Use 'gemini mcp' to see available servers.`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (args.session) {
|
||||
if (argv.session) {
|
||||
manager.disableForSession(name);
|
||||
debugLogger.log(
|
||||
`${GREEN}✓${RESET} MCP server '${name}' disabled for this session.`,
|
||||
@@ -100,6 +125,7 @@ export const enableCommand: CommandModule<object, Args> = {
|
||||
command: 'enable <name>',
|
||||
describe: 'Enable an MCP server',
|
||||
builder: (yargs) =>
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-type-assertion, @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-return */
|
||||
yargs
|
||||
.positional('name', {
|
||||
describe: 'MCP server name to enable',
|
||||
@@ -110,7 +136,8 @@ export const enableCommand: CommandModule<object, Args> = {
|
||||
describe: 'Clear session-only disable',
|
||||
type: 'boolean',
|
||||
default: false,
|
||||
}),
|
||||
}) as any,
|
||||
/* eslint-enable @typescript-eslint/no-unsafe-type-assertion, @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-return */
|
||||
handler: async (argv) => {
|
||||
await handleEnable(argv as Args);
|
||||
await exitCli();
|
||||
@@ -121,6 +148,7 @@ export const disableCommand: CommandModule<object, Args> = {
|
||||
command: 'disable <name>',
|
||||
describe: 'Disable an MCP server',
|
||||
builder: (yargs) =>
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-type-assertion, @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-return */
|
||||
yargs
|
||||
.positional('name', {
|
||||
describe: 'MCP server name to disable',
|
||||
@@ -131,7 +159,8 @@ export const disableCommand: CommandModule<object, Args> = {
|
||||
describe: 'Disable for current session only',
|
||||
type: 'boolean',
|
||||
default: false,
|
||||
}),
|
||||
}) as any,
|
||||
/* eslint-enable @typescript-eslint/no-unsafe-type-assertion, @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-return */
|
||||
handler: async (argv) => {
|
||||
await handleDisable(argv as Args);
|
||||
await exitCli();
|
||||
|
||||
@@ -14,11 +14,18 @@ import {
|
||||
type Mock,
|
||||
} from 'vitest';
|
||||
import { listMcpServers } from './list.js';
|
||||
import { loadSettings, mergeSettings } from '../../config/settings.js';
|
||||
import {
|
||||
loadSettings,
|
||||
mergeSettings,
|
||||
type LoadedSettings,
|
||||
} from '../../config/settings.js';
|
||||
import { createTransport, debugLogger } from '@google/gemini-cli-core';
|
||||
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
|
||||
import { ExtensionStorage } from '../../config/extensions/storage.js';
|
||||
import { ExtensionManager } from '../../config/extension-manager.js';
|
||||
import { McpServerEnablementManager } from '../../config/mcp/index.js';
|
||||
import { loadCliConfig, type CliArgs } from '../../config/config.js';
|
||||
import type { Config } from '@google/gemini-cli-core';
|
||||
|
||||
vi.mock('../../config/settings.js', async (importOriginal) => {
|
||||
const actual =
|
||||
@@ -28,6 +35,7 @@ vi.mock('../../config/settings.js', async (importOriginal) => {
|
||||
loadSettings: vi.fn(),
|
||||
};
|
||||
});
|
||||
vi.mock('../../config/config.js');
|
||||
vi.mock('../../config/extensions/storage.js', () => ({
|
||||
ExtensionStorage: {
|
||||
getUserExtensionsDir: vi.fn(),
|
||||
@@ -45,6 +53,8 @@ vi.mock('@google/gemini-cli-core', async (importOriginal) => {
|
||||
CONNECTED: 'CONNECTED',
|
||||
CONNECTING: 'CONNECTING',
|
||||
DISCONNECTED: 'DISCONNECTED',
|
||||
BLOCKED: 'BLOCKED',
|
||||
DISABLED: 'DISABLED',
|
||||
},
|
||||
Storage: Object.assign(
|
||||
vi.fn().mockImplementation((_cwd: string) => ({
|
||||
@@ -54,6 +64,8 @@ vi.mock('@google/gemini-cli-core', async (importOriginal) => {
|
||||
})),
|
||||
{
|
||||
getGlobalSettingsPath: () => '/tmp/gemini/settings.json',
|
||||
getGlobalGeminiDir: () => '/tmp/gemini',
|
||||
getGlobalTempDir: () => '/tmp/gemini/tmp',
|
||||
},
|
||||
),
|
||||
GEMINI_DIR: '.gemini',
|
||||
@@ -96,6 +108,12 @@ describe('mcp list command', () => {
|
||||
beforeEach(() => {
|
||||
vi.resetAllMocks();
|
||||
vi.spyOn(debugLogger, 'log').mockImplementation(() => {});
|
||||
McpServerEnablementManager.resetInstance();
|
||||
// Use a mock for isFileEnabled to avoid reading real files
|
||||
vi.spyOn(
|
||||
McpServerEnablementManager.prototype,
|
||||
'isFileEnabled',
|
||||
).mockResolvedValue(true);
|
||||
|
||||
mockTransport = { close: vi.fn() };
|
||||
mockClient = {
|
||||
@@ -124,7 +142,13 @@ describe('mcp list command', () => {
|
||||
merged: { ...defaultMergedSettings, mcpServers: {} },
|
||||
});
|
||||
|
||||
await listMcpServers();
|
||||
const mockConfig = {
|
||||
initialize: vi.fn().mockResolvedValue(undefined),
|
||||
getExtensionLoader: vi.fn().mockReturnValue(mockExtensionManager),
|
||||
};
|
||||
(loadCliConfig as Mock).mockResolvedValue(mockConfig as unknown as Config);
|
||||
|
||||
await listMcpServers({} as unknown as CliArgs);
|
||||
|
||||
expect(debugLogger.log).toHaveBeenCalledWith('No MCP servers configured.');
|
||||
});
|
||||
@@ -148,10 +172,16 @@ describe('mcp list command', () => {
|
||||
isTrusted: true,
|
||||
});
|
||||
|
||||
const mockConfig = {
|
||||
initialize: vi.fn().mockResolvedValue(undefined),
|
||||
getExtensionLoader: vi.fn().mockReturnValue(mockExtensionManager),
|
||||
};
|
||||
(loadCliConfig as Mock).mockResolvedValue(mockConfig as unknown as Config);
|
||||
|
||||
mockClient.connect.mockResolvedValue(undefined);
|
||||
mockClient.ping.mockResolvedValue(undefined);
|
||||
|
||||
await listMcpServers();
|
||||
await listMcpServers({} as unknown as CliArgs);
|
||||
|
||||
expect(debugLogger.log).toHaveBeenCalledWith('Configured MCP servers:\n');
|
||||
expect(debugLogger.log).toHaveBeenCalledWith(
|
||||
@@ -192,9 +222,15 @@ describe('mcp list command', () => {
|
||||
},
|
||||
});
|
||||
|
||||
const mockConfig = {
|
||||
initialize: vi.fn().mockResolvedValue(undefined),
|
||||
getExtensionLoader: vi.fn().mockReturnValue(mockExtensionManager),
|
||||
};
|
||||
(loadCliConfig as Mock).mockResolvedValue(mockConfig as unknown as Config);
|
||||
|
||||
mockClient.connect.mockRejectedValue(new Error('Connection failed'));
|
||||
|
||||
await listMcpServers();
|
||||
await listMcpServers({} as unknown as CliArgs);
|
||||
|
||||
expect(debugLogger.log).toHaveBeenCalledWith(
|
||||
expect.stringContaining(
|
||||
@@ -222,10 +258,16 @@ describe('mcp list command', () => {
|
||||
},
|
||||
]);
|
||||
|
||||
const mockConfig = {
|
||||
initialize: vi.fn().mockResolvedValue(undefined),
|
||||
getExtensionLoader: vi.fn().mockReturnValue(mockExtensionManager),
|
||||
};
|
||||
(loadCliConfig as Mock).mockResolvedValue(mockConfig as unknown as Config);
|
||||
|
||||
mockClient.connect.mockResolvedValue(undefined);
|
||||
mockClient.ping.mockResolvedValue(undefined);
|
||||
|
||||
await listMcpServers();
|
||||
await listMcpServers({} as unknown as CliArgs);
|
||||
|
||||
expect(debugLogger.log).toHaveBeenCalledWith(
|
||||
expect.stringContaining(
|
||||
@@ -262,10 +304,22 @@ describe('mcp list command', () => {
|
||||
merged: settingsWithAllowlist,
|
||||
});
|
||||
|
||||
const mockConfig = {
|
||||
initialize: vi.fn().mockResolvedValue(undefined),
|
||||
getExtensionLoader: vi.fn().mockReturnValue(mockExtensionManager),
|
||||
};
|
||||
(loadCliConfig as Mock).mockResolvedValue(mockConfig as unknown as Config);
|
||||
|
||||
mockClient.connect.mockResolvedValue(undefined);
|
||||
mockClient.ping.mockResolvedValue(undefined);
|
||||
|
||||
await listMcpServers(settingsWithAllowlist);
|
||||
await listMcpServers(
|
||||
{} as unknown as CliArgs,
|
||||
{
|
||||
merged: settingsWithAllowlist,
|
||||
isTrusted: true,
|
||||
} as unknown as LoadedSettings,
|
||||
);
|
||||
|
||||
expect(debugLogger.log).toHaveBeenCalledWith(
|
||||
expect.stringContaining('allowed-server'),
|
||||
@@ -293,10 +347,16 @@ describe('mcp list command', () => {
|
||||
isTrusted: false,
|
||||
});
|
||||
|
||||
const mockConfig = {
|
||||
initialize: vi.fn().mockResolvedValue(undefined),
|
||||
getExtensionLoader: vi.fn().mockReturnValue(mockExtensionManager),
|
||||
};
|
||||
(loadCliConfig as Mock).mockResolvedValue(mockConfig as unknown as Config);
|
||||
|
||||
// createTransport will throw in core if not trusted
|
||||
mockedCreateTransport.mockRejectedValue(new Error('Folder not trusted'));
|
||||
|
||||
await listMcpServers();
|
||||
await listMcpServers({} as unknown as CliArgs);
|
||||
|
||||
expect(debugLogger.log).toHaveBeenCalledWith(
|
||||
expect.stringContaining(
|
||||
@@ -304,4 +364,68 @@ describe('mcp list command', () => {
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
it('should display blocked status for servers in excluded list', async () => {
|
||||
const defaultMergedSettings = mergeSettings({}, {}, {}, {}, true);
|
||||
mockedLoadSettings.mockReturnValue({
|
||||
merged: {
|
||||
...defaultMergedSettings,
|
||||
mcp: {
|
||||
excluded: ['blocked-server'],
|
||||
},
|
||||
mcpServers: {
|
||||
'blocked-server': { command: '/test/server' },
|
||||
},
|
||||
},
|
||||
isTrusted: true,
|
||||
});
|
||||
|
||||
const mockConfig = {
|
||||
initialize: vi.fn().mockResolvedValue(undefined),
|
||||
getExtensionLoader: vi.fn().mockReturnValue(mockExtensionManager),
|
||||
};
|
||||
(loadCliConfig as Mock).mockResolvedValue(mockConfig as unknown as Config);
|
||||
|
||||
await listMcpServers({} as unknown as CliArgs);
|
||||
|
||||
expect(debugLogger.log).toHaveBeenCalledWith(
|
||||
expect.stringContaining(
|
||||
'blocked-server: /test/server (stdio) - Blocked',
|
||||
),
|
||||
);
|
||||
expect(mockedCreateTransport).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should display disabled status for servers disabled via enablement manager', async () => {
|
||||
const defaultMergedSettings = mergeSettings({}, {}, {}, {}, true);
|
||||
mockedLoadSettings.mockReturnValue({
|
||||
merged: {
|
||||
...defaultMergedSettings,
|
||||
mcpServers: {
|
||||
'disabled-server': { command: '/test/server' },
|
||||
},
|
||||
},
|
||||
isTrusted: true,
|
||||
});
|
||||
|
||||
const mockConfig = {
|
||||
initialize: vi.fn().mockResolvedValue(undefined),
|
||||
getExtensionLoader: vi.fn().mockReturnValue(mockExtensionManager),
|
||||
};
|
||||
(loadCliConfig as Mock).mockResolvedValue(mockConfig as unknown as Config);
|
||||
|
||||
vi.spyOn(
|
||||
McpServerEnablementManager.prototype,
|
||||
'isFileEnabled',
|
||||
).mockResolvedValue(false);
|
||||
|
||||
await listMcpServers({} as unknown as CliArgs);
|
||||
|
||||
expect(debugLogger.log).toHaveBeenCalledWith(
|
||||
expect.stringContaining(
|
||||
'disabled-server: /test/server (stdio) - Disabled',
|
||||
),
|
||||
);
|
||||
expect(mockedCreateTransport).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -6,39 +6,43 @@
|
||||
|
||||
// File for 'gemini mcp list' command
|
||||
import type { CommandModule } from 'yargs';
|
||||
import { type MergedSettings, loadSettings } from '../../config/settings.js';
|
||||
import type { MCPServerConfig } from '@google/gemini-cli-core';
|
||||
import {
|
||||
type MergedSettings,
|
||||
loadSettings,
|
||||
type LoadedSettings,
|
||||
} from '../../config/settings.js';
|
||||
import {
|
||||
MCPServerStatus,
|
||||
createTransport,
|
||||
debugLogger,
|
||||
applyAdminAllowlist,
|
||||
getAdminBlockedMcpServersMessage,
|
||||
type GeminiCLIExtension,
|
||||
type MCPServerConfig,
|
||||
} from '@google/gemini-cli-core';
|
||||
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
|
||||
import { ExtensionManager } from '../../config/extension-manager.js';
|
||||
import { requestConsentNonInteractive } from '../../config/extensions/consent.js';
|
||||
import { promptForSetting } from '../../config/extensions/extensionSettings.js';
|
||||
import type { ExtensionManager } from '../../config/extension-manager.js';
|
||||
import {
|
||||
canLoadServer,
|
||||
McpServerEnablementManager,
|
||||
} from '../../config/mcp/index.js';
|
||||
import { loadCliConfig, type CliArgs } from '../../config/config.js';
|
||||
import { exitCli } from '../utils.js';
|
||||
import chalk from 'chalk';
|
||||
|
||||
export async function getMcpServersFromConfig(
|
||||
settings?: MergedSettings,
|
||||
settings: MergedSettings,
|
||||
extensionManager: ExtensionManager,
|
||||
): Promise<{
|
||||
mcpServers: Record<string, MCPServerConfig>;
|
||||
blockedServerNames: string[];
|
||||
}> {
|
||||
if (!settings) {
|
||||
settings = loadSettings().merged;
|
||||
let extensions: GeminiCLIExtension[];
|
||||
try {
|
||||
extensions = extensionManager.getExtensions();
|
||||
} catch {
|
||||
extensions = await extensionManager.loadExtensions();
|
||||
}
|
||||
|
||||
const extensionManager = new ExtensionManager({
|
||||
settings,
|
||||
workspaceDir: process.cwd(),
|
||||
requestConsent: requestConsentNonInteractive,
|
||||
requestSetting: promptForSetting,
|
||||
});
|
||||
const extensions = await extensionManager.loadExtensions();
|
||||
const mcpServers = { ...settings.mcpServers };
|
||||
for (const extension of extensions) {
|
||||
Object.entries(extension.mcpServers || {}).forEach(([key, server]) => {
|
||||
@@ -61,13 +65,13 @@ export async function getMcpServersFromConfig(
|
||||
async function testMCPConnection(
|
||||
serverName: string,
|
||||
config: MCPServerConfig,
|
||||
isTrusted: boolean,
|
||||
activeSettings: MergedSettings,
|
||||
): Promise<MCPServerStatus> {
|
||||
const settings = loadSettings();
|
||||
|
||||
// SECURITY: Only test connection if workspace is trusted or if it's a remote server.
|
||||
// stdio servers execute local commands and must never run in untrusted workspaces.
|
||||
const isStdio = !!config.command;
|
||||
if (isStdio && !settings.isTrusted) {
|
||||
if (isStdio && !isTrusted) {
|
||||
return MCPServerStatus.DISCONNECTED;
|
||||
}
|
||||
|
||||
@@ -80,7 +84,7 @@ async function testMCPConnection(
|
||||
sanitizationConfig: {
|
||||
enableEnvironmentVariableRedaction: true,
|
||||
allowedEnvironmentVariables: [],
|
||||
blockedEnvironmentVariables: settings.merged.advanced.excludedEnvVars,
|
||||
blockedEnvironmentVariables: activeSettings.advanced.excludedEnvVars,
|
||||
},
|
||||
emitMcpDiagnostic: (
|
||||
severity: 'info' | 'warning' | 'error',
|
||||
@@ -105,7 +109,7 @@ async function testMCPConnection(
|
||||
debugLogger.log(message, error);
|
||||
}
|
||||
},
|
||||
isTrustedFolder: () => settings.isTrusted,
|
||||
isTrustedFolder: () => isTrusted,
|
||||
};
|
||||
|
||||
let transport;
|
||||
@@ -135,14 +139,50 @@ async function testMCPConnection(
|
||||
async function getServerStatus(
|
||||
serverName: string,
|
||||
server: MCPServerConfig,
|
||||
isTrusted: boolean,
|
||||
activeSettings: MergedSettings,
|
||||
): Promise<MCPServerStatus> {
|
||||
const mcpEnablementManager = McpServerEnablementManager.getInstance();
|
||||
const loadResult = await canLoadServer(serverName, {
|
||||
adminMcpEnabled: activeSettings.admin?.mcp?.enabled ?? true,
|
||||
allowedList: activeSettings.mcp?.allowed,
|
||||
excludedList: activeSettings.mcp?.excluded,
|
||||
enablement: mcpEnablementManager.getEnablementCallbacks(),
|
||||
});
|
||||
|
||||
if (!loadResult.allowed) {
|
||||
if (
|
||||
loadResult.blockType === 'admin' ||
|
||||
loadResult.blockType === 'allowlist' ||
|
||||
loadResult.blockType === 'excludelist'
|
||||
) {
|
||||
return MCPServerStatus.BLOCKED;
|
||||
}
|
||||
return MCPServerStatus.DISABLED;
|
||||
}
|
||||
|
||||
// Test all server types by attempting actual connection
|
||||
return testMCPConnection(serverName, server);
|
||||
return testMCPConnection(serverName, server, isTrusted, activeSettings);
|
||||
}
|
||||
|
||||
export async function listMcpServers(settings?: MergedSettings): Promise<void> {
|
||||
const { mcpServers, blockedServerNames } =
|
||||
await getMcpServersFromConfig(settings);
|
||||
export async function listMcpServers(
|
||||
argv: CliArgs,
|
||||
loadedSettingsArg?: LoadedSettings,
|
||||
): Promise<void> {
|
||||
const loadedSettings = loadedSettingsArg ?? loadSettings();
|
||||
const activeSettings = loadedSettings.merged;
|
||||
|
||||
const config = await loadCliConfig(activeSettings, 'mcp-list-session', argv, {
|
||||
cwd: process.cwd(),
|
||||
});
|
||||
await config.initialize();
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
|
||||
const extensionManager = config.getExtensionLoader() as ExtensionManager;
|
||||
|
||||
const { mcpServers, blockedServerNames } = await getMcpServersFromConfig(
|
||||
activeSettings,
|
||||
extensionManager,
|
||||
);
|
||||
const serverNames = Object.keys(mcpServers);
|
||||
|
||||
if (blockedServerNames.length > 0) {
|
||||
@@ -165,7 +205,12 @@ export async function listMcpServers(settings?: MergedSettings): Promise<void> {
|
||||
for (const serverName of serverNames) {
|
||||
const server = mcpServers[serverName];
|
||||
|
||||
const status = await getServerStatus(serverName, server);
|
||||
const status = await getServerStatus(
|
||||
serverName,
|
||||
server,
|
||||
loadedSettings.isTrusted,
|
||||
activeSettings,
|
||||
);
|
||||
|
||||
let statusIndicator = '';
|
||||
let statusText = '';
|
||||
@@ -178,6 +223,14 @@ export async function listMcpServers(settings?: MergedSettings): Promise<void> {
|
||||
statusIndicator = chalk.yellow('…');
|
||||
statusText = 'Connecting';
|
||||
break;
|
||||
case MCPServerStatus.BLOCKED:
|
||||
statusIndicator = chalk.red('⛔');
|
||||
statusText = 'Blocked';
|
||||
break;
|
||||
case MCPServerStatus.DISABLED:
|
||||
statusIndicator = chalk.gray('○');
|
||||
statusText = 'Disabled';
|
||||
break;
|
||||
case MCPServerStatus.DISCONNECTED:
|
||||
default:
|
||||
statusIndicator = chalk.red('✗');
|
||||
@@ -203,14 +256,15 @@ export async function listMcpServers(settings?: MergedSettings): Promise<void> {
|
||||
}
|
||||
|
||||
interface ListArgs {
|
||||
settings?: MergedSettings;
|
||||
loadedSettings?: LoadedSettings;
|
||||
}
|
||||
|
||||
export const listCommand: CommandModule<object, ListArgs> = {
|
||||
command: 'list',
|
||||
describe: 'List all configured MCP servers',
|
||||
handler: async (argv) => {
|
||||
await listMcpServers(argv.settings);
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
|
||||
await listMcpServers(argv as unknown as CliArgs, argv.loadedSettings);
|
||||
await exitCli();
|
||||
},
|
||||
};
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2026 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { describe, it, expect, vi } from 'vitest';
|
||||
import { profilesCommand } from './profiles.js';
|
||||
|
||||
// Mock subcommands
|
||||
vi.mock('./profiles/list.js', () => ({ listCommand: { command: 'list' } }));
|
||||
vi.mock('./profiles/enable.js', () => ({
|
||||
enableCommand: { command: 'enable' },
|
||||
}));
|
||||
vi.mock('./profiles/disable.js', () => ({
|
||||
disableCommand: { command: 'disable' },
|
||||
}));
|
||||
vi.mock('./profiles/uninstall.js', () => ({
|
||||
uninstallCommand: { command: 'uninstall' },
|
||||
}));
|
||||
vi.mock('./profiles/install.js', () => ({
|
||||
installCommand: { command: 'install' },
|
||||
}));
|
||||
vi.mock('./profiles/link.js', () => ({
|
||||
linkCommand: { command: 'link' },
|
||||
}));
|
||||
|
||||
// Mock gemini.js
|
||||
vi.mock('../gemini.js', () => ({
|
||||
initializeOutputListenersAndFlush: vi.fn(),
|
||||
}));
|
||||
|
||||
describe('profilesCommand', () => {
|
||||
it('should have correct command and description', () => {
|
||||
expect(profilesCommand.command).toBe('profiles <command>');
|
||||
expect(profilesCommand.describe).toBe('Manage Gemini CLI profiles.');
|
||||
});
|
||||
|
||||
it('should register all subcommands in builder', () => {
|
||||
const mockYargs = {
|
||||
middleware: vi.fn().mockReturnThis(),
|
||||
command: vi.fn().mockReturnThis(),
|
||||
demandCommand: vi.fn().mockReturnThis(),
|
||||
version: vi.fn().mockReturnThis(),
|
||||
};
|
||||
|
||||
// @ts-expect-error - Mocking yargs
|
||||
profilesCommand.builder(mockYargs);
|
||||
|
||||
expect(mockYargs.middleware).toHaveBeenCalled();
|
||||
expect(mockYargs.command).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ command: 'list' }),
|
||||
);
|
||||
expect(mockYargs.command).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ command: 'enable' }),
|
||||
);
|
||||
expect(mockYargs.command).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ command: 'disable' }),
|
||||
);
|
||||
expect(mockYargs.command).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ command: 'uninstall' }),
|
||||
);
|
||||
expect(mockYargs.command).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ command: 'install' }),
|
||||
);
|
||||
expect(mockYargs.command).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ command: 'link' }),
|
||||
);
|
||||
expect(mockYargs.demandCommand).toHaveBeenCalledWith(1, expect.any(String));
|
||||
expect(mockYargs.version).toHaveBeenCalledWith(false);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,37 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2026 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import type { CommandModule } from 'yargs';
|
||||
import { listCommand } from './profiles/list.js';
|
||||
import { enableCommand } from './profiles/enable.js';
|
||||
import { disableCommand } from './profiles/disable.js';
|
||||
import { uninstallCommand } from './profiles/uninstall.js';
|
||||
import { installCommand } from './profiles/install.js';
|
||||
import { linkCommand } from './profiles/link.js';
|
||||
import { initializeOutputListenersAndFlush } from '../gemini.js';
|
||||
|
||||
export const profilesCommand: CommandModule = {
|
||||
command: 'profiles <command>',
|
||||
describe: 'Manage Gemini CLI profiles.',
|
||||
builder: (yargs) =>
|
||||
yargs
|
||||
.middleware((argv) => {
|
||||
initializeOutputListenersAndFlush();
|
||||
argv['isCommand'] = true;
|
||||
})
|
||||
.command(listCommand)
|
||||
.command(enableCommand)
|
||||
.command(disableCommand)
|
||||
.command(uninstallCommand)
|
||||
.command(installCommand)
|
||||
.command(linkCommand)
|
||||
.demandCommand(1, 'You need at least one command before continuing.')
|
||||
.version(false),
|
||||
handler: () => {
|
||||
// This handler is not called when a subcommand is provided.
|
||||
// Yargs will show the help menu.
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,34 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2026 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { type CommandModule } from 'yargs';
|
||||
import { loadSettings, SettingScope } from '../../config/settings.js';
|
||||
import { debugLogger } from '@google/gemini-cli-core';
|
||||
import { exitCli } from '../utils.js';
|
||||
|
||||
/**
|
||||
* Command module for `gemini profiles disable`.
|
||||
*/
|
||||
export const disableCommand: CommandModule = {
|
||||
command: 'disable',
|
||||
describe: 'Disables the currently active profile.',
|
||||
handler: async () => {
|
||||
try {
|
||||
const settings = loadSettings();
|
||||
settings.setValue(SettingScope.User, 'general.activeProfile', undefined);
|
||||
debugLogger.log('Profile disabled. Reverting to default behavior.');
|
||||
// eslint-disable-next-line no-console
|
||||
console.log('Profile disabled. Reverting to default behavior.');
|
||||
} catch (error) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error(
|
||||
`Error disabling profile: ${error instanceof Error ? error.message : String(error)}`,
|
||||
);
|
||||
await exitCli(1);
|
||||
}
|
||||
await exitCli();
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,48 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2026 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { type CommandModule } from 'yargs';
|
||||
import { loadSettings, SettingScope } from '../../config/settings.js';
|
||||
import { Storage, ProfileManager, debugLogger } from '@google/gemini-cli-core';
|
||||
import { exitCli } from '../utils.js';
|
||||
|
||||
/**
|
||||
* Command module for `gemini profiles enable <name>`.
|
||||
*/
|
||||
export const enableCommand: CommandModule = {
|
||||
command: 'enable <name>',
|
||||
describe: 'Enables a profile persistently.',
|
||||
builder: (yargs) =>
|
||||
yargs.positional('name', {
|
||||
describe: 'The name of the profile to enable.',
|
||||
type: 'string',
|
||||
}),
|
||||
handler: async (argv) => {
|
||||
const name = String(argv['name']);
|
||||
try {
|
||||
const settings = loadSettings();
|
||||
const profilesDir = Storage.getProfilesDir();
|
||||
const manager = new ProfileManager(profilesDir);
|
||||
await manager.load();
|
||||
|
||||
if (!manager.getProfile(name)) {
|
||||
throw new Error(`Profile "${name}" not found.`);
|
||||
}
|
||||
|
||||
settings.setValue(SettingScope.User, 'general.activeProfile', name);
|
||||
debugLogger.log(`Profile "${name}" successfully enabled.`);
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(`Profile "${name}" successfully enabled.`);
|
||||
} catch (error) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error(
|
||||
`Error enabling profile: ${error instanceof Error ? error.message : String(error)}`,
|
||||
);
|
||||
await exitCli(1);
|
||||
}
|
||||
await exitCli();
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,49 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2026 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import type { CommandModule } from 'yargs';
|
||||
import chalk from 'chalk';
|
||||
import { debugLogger, Storage, ProfileManager } from '@google/gemini-cli-core';
|
||||
import { getErrorMessage } from '../../utils/errors.js';
|
||||
import { exitCli } from '../utils.js';
|
||||
|
||||
interface InstallArgs {
|
||||
path: string;
|
||||
}
|
||||
|
||||
export async function handleInstall(args: InstallArgs) {
|
||||
try {
|
||||
const manager = new ProfileManager(Storage.getProfilesDir());
|
||||
const profile = await manager.installProfile(args.path);
|
||||
debugLogger.log(
|
||||
chalk.green(`Profile "${profile.name}" installed successfully.`),
|
||||
);
|
||||
debugLogger.log(
|
||||
`Use "gemini profiles enable ${profile.name}" to activate it.`,
|
||||
);
|
||||
} catch (error) {
|
||||
debugLogger.error(getErrorMessage(error));
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
export const installCommand: CommandModule = {
|
||||
command: 'install <path>',
|
||||
describe: 'Installs a profile by copying it from a local path.',
|
||||
builder: (yargs) =>
|
||||
yargs.positional('path', {
|
||||
describe: 'The local path of the profile file to install.',
|
||||
type: 'string',
|
||||
demandOption: true,
|
||||
}),
|
||||
handler: async (argv) => {
|
||||
await handleInstall({
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
|
||||
path: argv['path'] as string,
|
||||
});
|
||||
await exitCli();
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,50 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2026 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import type { CommandModule } from 'yargs';
|
||||
import chalk from 'chalk';
|
||||
import { debugLogger, Storage, ProfileManager } from '@google/gemini-cli-core';
|
||||
import { getErrorMessage } from '../../utils/errors.js';
|
||||
import { exitCli } from '../utils.js';
|
||||
|
||||
interface LinkArgs {
|
||||
path: string;
|
||||
}
|
||||
|
||||
export async function handleLink(args: LinkArgs) {
|
||||
try {
|
||||
const manager = new ProfileManager(Storage.getProfilesDir());
|
||||
const profile = await manager.linkProfile(args.path);
|
||||
debugLogger.log(
|
||||
chalk.green(`Profile "${profile.name}" linked successfully.`),
|
||||
);
|
||||
debugLogger.log(
|
||||
`Use "gemini profiles enable ${profile.name}" to activate it.`,
|
||||
);
|
||||
} catch (error) {
|
||||
debugLogger.error(getErrorMessage(error));
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
export const linkCommand: CommandModule = {
|
||||
command: 'link <path>',
|
||||
describe:
|
||||
'Links a profile from a local path. Changes to the local file will be reflected in the profile.',
|
||||
builder: (yargs) =>
|
||||
yargs.positional('path', {
|
||||
describe: 'The local path of the profile file to link.',
|
||||
type: 'string',
|
||||
demandOption: true,
|
||||
}),
|
||||
handler: async (argv) => {
|
||||
await handleLink({
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
|
||||
path: argv['path'] as string,
|
||||
});
|
||||
await exitCli();
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,88 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2026 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import type { CommandModule } from 'yargs';
|
||||
import { render, Box, Text } from 'ink';
|
||||
import { Storage, ProfileManager } from '@google/gemini-cli-core';
|
||||
import { loadSettings } from '../../config/settings.js';
|
||||
import { exitCli } from '../utils.js';
|
||||
|
||||
/**
|
||||
* View component for listing profiles in the terminal.
|
||||
*/
|
||||
const ProfileListView = ({
|
||||
profiles,
|
||||
activeProfile,
|
||||
}: {
|
||||
profiles: string[];
|
||||
activeProfile?: string;
|
||||
}) => {
|
||||
if (profiles.length === 0) {
|
||||
return (
|
||||
<Box flexDirection="column" paddingY={1}>
|
||||
<Text color="yellow">No profiles found.</Text>
|
||||
<Text dimColor>
|
||||
Profiles are stored as .md files in ~/.gemini/profiles/
|
||||
</Text>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Box flexDirection="column" paddingY={1}>
|
||||
<Text bold underline>
|
||||
Available Profiles:
|
||||
</Text>
|
||||
{profiles.map((name) => (
|
||||
<Box key={name} marginLeft={2}>
|
||||
<Text color={name === activeProfile ? 'green' : 'white'}>
|
||||
{name === activeProfile ? '●' : '○'} {name}
|
||||
</Text>
|
||||
{name === activeProfile && (
|
||||
<Text color="green" italic>
|
||||
{' '}
|
||||
(active)
|
||||
</Text>
|
||||
)}
|
||||
</Box>
|
||||
))}
|
||||
<Box marginTop={1}>
|
||||
<Text dimColor>
|
||||
Use `gemini profiles enable {'<name>'}` to switch profiles.
|
||||
</Text>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Command module for `gemini profiles list`.
|
||||
*/
|
||||
export const listCommand: CommandModule = {
|
||||
command: 'list',
|
||||
describe: 'List all available profiles.',
|
||||
handler: async () => {
|
||||
try {
|
||||
const settings = loadSettings();
|
||||
const profilesDir = Storage.getProfilesDir();
|
||||
const manager = new ProfileManager(profilesDir);
|
||||
await manager.load();
|
||||
const profiles = manager.getProfileNames();
|
||||
const activeProfile = settings.merged.general?.activeProfile;
|
||||
|
||||
const { waitUntilExit } = render(
|
||||
<ProfileListView profiles={profiles} activeProfile={activeProfile} />,
|
||||
);
|
||||
await waitUntilExit();
|
||||
} catch (error) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error(
|
||||
`Error listing profiles: ${error instanceof Error ? error.message : String(error)}`,
|
||||
);
|
||||
await exitCli(1);
|
||||
}
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,41 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2026 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { type CommandModule } from 'yargs';
|
||||
import { Storage, ProfileManager, debugLogger } from '@google/gemini-cli-core';
|
||||
import { exitCli } from '../utils.js';
|
||||
|
||||
/**
|
||||
* Command module for `gemini profiles uninstall <name>`.
|
||||
*/
|
||||
export const uninstallCommand: CommandModule = {
|
||||
command: 'uninstall <name>',
|
||||
describe: 'Uninstalls a profile.',
|
||||
builder: (yargs) =>
|
||||
yargs.positional('name', {
|
||||
describe: 'The name of the profile to uninstall.',
|
||||
type: 'string',
|
||||
}),
|
||||
handler: async (argv) => {
|
||||
const name = String(argv['name']);
|
||||
try {
|
||||
const manager = new ProfileManager(Storage.getProfilesDir());
|
||||
await manager.load();
|
||||
|
||||
await manager.uninstallProfile(name);
|
||||
debugLogger.log(`Profile "${name}" successfully uninstalled.`);
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(`Profile "${name}" successfully uninstalled.`);
|
||||
} catch (error) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error(
|
||||
`Error uninstalling profile: ${error instanceof Error ? error.message : String(error)}`,
|
||||
);
|
||||
await exitCli(1);
|
||||
}
|
||||
await exitCli();
|
||||
},
|
||||
};
|
||||
@@ -8,7 +8,7 @@ import { vi, describe, it, expect, beforeEach, afterEach } from 'vitest';
|
||||
import { coreEvents } from '@google/gemini-cli-core';
|
||||
import { handleList, listCommand } from './list.js';
|
||||
import { loadSettings, type LoadedSettings } from '../../config/settings.js';
|
||||
import { loadCliConfig } from '../../config/config.js';
|
||||
import { loadCliConfig, type CliArgs } from '../../config/config.js';
|
||||
import type { Config } from '@google/gemini-cli-core';
|
||||
import chalk from 'chalk';
|
||||
|
||||
@@ -55,7 +55,7 @@ describe('skills list command', () => {
|
||||
};
|
||||
mockLoadCliConfig.mockResolvedValue(mockConfig as unknown as Config);
|
||||
|
||||
await handleList({});
|
||||
await handleList({} as unknown as CliArgs, {});
|
||||
|
||||
expect(coreEvents.emitConsoleLog).toHaveBeenCalledWith(
|
||||
'log',
|
||||
@@ -86,7 +86,7 @@ describe('skills list command', () => {
|
||||
};
|
||||
mockLoadCliConfig.mockResolvedValue(mockConfig as unknown as Config);
|
||||
|
||||
await handleList({});
|
||||
await handleList({} as unknown as CliArgs, {});
|
||||
|
||||
expect(coreEvents.emitConsoleLog).toHaveBeenCalledWith(
|
||||
'log',
|
||||
@@ -135,7 +135,7 @@ describe('skills list command', () => {
|
||||
mockLoadCliConfig.mockResolvedValue(mockConfig as unknown as Config);
|
||||
|
||||
// Default
|
||||
await handleList({ all: false });
|
||||
await handleList({} as unknown as CliArgs, { all: false });
|
||||
expect(coreEvents.emitConsoleLog).toHaveBeenCalledWith(
|
||||
'log',
|
||||
expect.stringContaining('regular'),
|
||||
@@ -148,7 +148,7 @@ describe('skills list command', () => {
|
||||
vi.clearAllMocks();
|
||||
|
||||
// With all: true
|
||||
await handleList({ all: true });
|
||||
await handleList({} as unknown as CliArgs, { all: true });
|
||||
expect(coreEvents.emitConsoleLog).toHaveBeenCalledWith(
|
||||
'log',
|
||||
expect.stringContaining('regular'),
|
||||
@@ -166,7 +166,9 @@ describe('skills list command', () => {
|
||||
it('should throw an error when listing fails', async () => {
|
||||
mockLoadCliConfig.mockRejectedValue(new Error('List failed'));
|
||||
|
||||
await expect(handleList({})).rejects.toThrow('List failed');
|
||||
await expect(handleList({} as unknown as CliArgs, {})).rejects.toThrow(
|
||||
'List failed',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -11,17 +11,14 @@ import { loadCliConfig, type CliArgs } from '../../config/config.js';
|
||||
import { exitCli } from '../utils.js';
|
||||
import chalk from 'chalk';
|
||||
|
||||
export async function handleList(args: { all?: boolean }) {
|
||||
export async function handleList(argv: CliArgs, args: { all?: boolean }) {
|
||||
const workspaceDir = process.cwd();
|
||||
const settings = loadSettings(workspaceDir);
|
||||
|
||||
const config = await loadCliConfig(
|
||||
settings.merged,
|
||||
'skills-list-session',
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
|
||||
{
|
||||
debug: false,
|
||||
} as Partial<CliArgs> as CliArgs,
|
||||
argv,
|
||||
{ cwd: workspaceDir },
|
||||
);
|
||||
|
||||
@@ -73,8 +70,9 @@ export const listCommand: CommandModule = {
|
||||
default: false,
|
||||
}),
|
||||
handler: async (argv) => {
|
||||
const args = { all: Boolean(argv['all']) };
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
|
||||
await handleList({ all: argv['all'] as boolean });
|
||||
await handleList(argv as unknown as CliArgs, args);
|
||||
await exitCli();
|
||||
},
|
||||
};
|
||||
|
||||
@@ -116,14 +116,16 @@ vi.mock('@google/gemini-cli-core', async () => {
|
||||
(
|
||||
cwd,
|
||||
dirs,
|
||||
debug,
|
||||
fileService,
|
||||
extensionLoader: ExtensionLoader,
|
||||
_folderTrust,
|
||||
_importFormat,
|
||||
_fileFilteringOptions,
|
||||
_maxDirs,
|
||||
) => {
|
||||
const extensionPaths = extensionLoader
|
||||
.getExtensions()
|
||||
.flatMap((e) => e.contextFiles);
|
||||
const extensionPaths =
|
||||
extensionLoader?.getExtensions?.()?.flatMap((e) => e.contextFiles) ||
|
||||
[];
|
||||
return Promise.resolve({
|
||||
memoryContent: extensionPaths.join(',') || '',
|
||||
fileCount: extensionPaths?.length || 0,
|
||||
@@ -847,7 +849,6 @@ describe('Hierarchical Memory Loading (config.ts) - Placeholder Suite', () => {
|
||||
expect(ServerConfig.loadServerHierarchicalMemory).toHaveBeenCalledWith(
|
||||
expect.any(String),
|
||||
[],
|
||||
false,
|
||||
expect.any(Object),
|
||||
expect.any(ExtensionManager),
|
||||
true,
|
||||
@@ -876,7 +877,6 @@ describe('Hierarchical Memory Loading (config.ts) - Placeholder Suite', () => {
|
||||
expect(ServerConfig.loadServerHierarchicalMemory).toHaveBeenCalledWith(
|
||||
expect.any(String),
|
||||
[includeDir],
|
||||
false,
|
||||
expect.any(Object),
|
||||
expect.any(ExtensionManager),
|
||||
true,
|
||||
@@ -904,7 +904,6 @@ describe('Hierarchical Memory Loading (config.ts) - Placeholder Suite', () => {
|
||||
expect(ServerConfig.loadServerHierarchicalMemory).toHaveBeenCalledWith(
|
||||
expect.any(String),
|
||||
[],
|
||||
false,
|
||||
expect.any(Object),
|
||||
expect.any(ExtensionManager),
|
||||
true,
|
||||
@@ -2623,13 +2622,13 @@ describe('loadCliConfig approval mode', () => {
|
||||
expect(config.getApprovalMode()).toBe(ApprovalMode.DEFAULT);
|
||||
});
|
||||
|
||||
it('should throw error when --approval-mode=plan is used but experimental.plan setting is missing', async () => {
|
||||
it('should allow plan approval mode by default when --approval-mode=plan is used', async () => {
|
||||
process.argv = ['node', 'script.js', '--approval-mode', 'plan'];
|
||||
const argv = await parseArguments(createTestMergedSettings());
|
||||
const settings = createTestMergedSettings({});
|
||||
|
||||
const config = await loadCliConfig(settings, 'test-session', argv);
|
||||
expect(config.getApprovalMode()).toBe(ApprovalMode.DEFAULT);
|
||||
expect(config.getApprovalMode()).toBe(ApprovalMode.PLAN);
|
||||
});
|
||||
|
||||
it('should pass planSettings.directory from settings to config', async () => {
|
||||
|
||||
@@ -9,6 +9,7 @@ import { hideBin } from 'yargs/helpers';
|
||||
import process from 'node:process';
|
||||
import { mcpCommand } from '../commands/mcp.js';
|
||||
import { extensionsCommand } from '../commands/extensions.js';
|
||||
import { profilesCommand } from '../commands/profiles.js';
|
||||
import { skillsCommand } from '../commands/skills.js';
|
||||
import { hooksCommand } from '../commands/hooks.js';
|
||||
import {
|
||||
@@ -38,6 +39,8 @@ import {
|
||||
type HookDefinition,
|
||||
type HookEventName,
|
||||
type OutputFormat,
|
||||
Storage as CoreStorage,
|
||||
ProfileManager as CoreProfileManager,
|
||||
} from '@google/gemini-cli-core';
|
||||
import {
|
||||
type Settings,
|
||||
@@ -61,6 +64,7 @@ import type { ExtensionEvents } from '@google/gemini-cli-core/src/utils/extensio
|
||||
import { requestConsentNonInteractive } from './extensions/consent.js';
|
||||
import { promptForSetting } from './extensions/extensionSettings.js';
|
||||
import type { EventEmitter } from 'node:stream';
|
||||
// Removed legacy profile manager import
|
||||
import { runExitCleanup } from '../utils/cleanup.js';
|
||||
|
||||
export interface CliArgs {
|
||||
@@ -93,6 +97,7 @@ export interface CliArgs {
|
||||
rawOutput: boolean | undefined;
|
||||
acceptRawOutputRisk: boolean | undefined;
|
||||
isCommand: boolean | undefined;
|
||||
profiles: string | undefined;
|
||||
}
|
||||
|
||||
export async function parseArguments(
|
||||
@@ -106,6 +111,12 @@ export async function parseArguments(
|
||||
.usage(
|
||||
'Usage: gemini [options] [command]\n\nGemini CLI - Defaults to interactive mode. Use -p/--prompt for non-interactive (headless) mode.',
|
||||
)
|
||||
.option('profiles', {
|
||||
alias: ['profile', 'P'],
|
||||
type: 'string',
|
||||
global: true,
|
||||
description: 'The name of the profile to use for this session.',
|
||||
})
|
||||
.option('debug', {
|
||||
alias: 'd',
|
||||
type: 'boolean',
|
||||
@@ -143,7 +154,6 @@ export async function parseArguments(
|
||||
type: 'boolean',
|
||||
description: 'Run in sandbox?',
|
||||
})
|
||||
|
||||
.option('yolo', {
|
||||
alias: 'y',
|
||||
type: 'boolean',
|
||||
@@ -289,6 +299,11 @@ export async function parseArguments(
|
||||
.option('accept-raw-output-risk', {
|
||||
type: 'boolean',
|
||||
description: 'Suppress the security warning when using --raw-output.',
|
||||
})
|
||||
.option('profiles', {
|
||||
alias: ['profile', 'P'],
|
||||
type: 'string',
|
||||
description: 'The name of the profile to use for this session.',
|
||||
}),
|
||||
)
|
||||
// Register MCP subcommands
|
||||
@@ -340,6 +355,8 @@ export async function parseArguments(
|
||||
yargsInstance.command(hooksCommand);
|
||||
}
|
||||
|
||||
yargsInstance.command(profilesCommand);
|
||||
|
||||
yargsInstance
|
||||
.version(await getVersion()) // This will enable the --version flag based on package.json
|
||||
.alias('v', 'version')
|
||||
@@ -421,7 +438,16 @@ export async function loadCliConfig(
|
||||
const { cwd = process.cwd(), projectHooks } = options;
|
||||
const debugMode = isDebugMode(argv);
|
||||
|
||||
const loadedSettings = loadSettings(cwd);
|
||||
const profilesDir = CoreStorage.getProfilesDir();
|
||||
const coreProfileManager = new CoreProfileManager(profilesDir);
|
||||
await coreProfileManager.load();
|
||||
const activeProfileName =
|
||||
argv.profiles ||
|
||||
settings.general?.activeProfile ||
|
||||
coreProfileManager.getActiveProfileName();
|
||||
const profile = activeProfileName
|
||||
? coreProfileManager.getProfile(activeProfileName)
|
||||
: null;
|
||||
|
||||
if (argv.sandbox) {
|
||||
process.env['GEMINI_SANDBOX'] = 'true';
|
||||
@@ -470,12 +496,21 @@ export async function loadCliConfig(
|
||||
.map(resolvePath)
|
||||
.concat((argv.includeDirectories || []).map(resolvePath));
|
||||
|
||||
let enabledExtensionOverrides = argv.extensions;
|
||||
if (enabledExtensionOverrides === undefined && profile) {
|
||||
const profileExtensions = profile.extensions;
|
||||
if (profileExtensions !== undefined) {
|
||||
enabledExtensionOverrides =
|
||||
profileExtensions.length > 0 ? profileExtensions : ['none'];
|
||||
}
|
||||
}
|
||||
|
||||
const extensionManager = new ExtensionManager({
|
||||
settings,
|
||||
requestConsent: requestConsentNonInteractive,
|
||||
requestSetting: promptForSetting,
|
||||
workspaceDir: cwd,
|
||||
enabledExtensionOverrides: argv.extensions,
|
||||
enabledExtensionOverrides,
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
|
||||
eventEmitter: coreEvents as EventEmitter<ExtensionEvents>,
|
||||
clientVersion: await getVersion(),
|
||||
@@ -499,7 +534,6 @@ export async function loadCliConfig(
|
||||
settings.context?.loadMemoryFromIncludeDirectories || false
|
||||
? includeDirectories
|
||||
: [],
|
||||
debugMode,
|
||||
fileService,
|
||||
extensionManager,
|
||||
trustedFolder,
|
||||
@@ -512,6 +546,20 @@ export async function loadCliConfig(
|
||||
filePaths = result.filePaths;
|
||||
}
|
||||
|
||||
if (profile?.body) {
|
||||
const profileContext = `Profile Context (${profile.name}):\n${profile.body}`;
|
||||
if (typeof memoryContent === 'string') {
|
||||
memoryContent = profileContext + '\n\n' + memoryContent;
|
||||
} else {
|
||||
// If it's HierarchicalMemory, we'll need to prepend it to the text content if possible
|
||||
// or just treat it as a string if we can't easily modify HierarchicalMemory here.
|
||||
// For now, let's assume if it's not a string, we might have issues prepending easily.
|
||||
// But looking at core, userMemory is often expected to be a string in simple cases.
|
||||
// If it's HierarchicalMemory, we might need to handle it in core.
|
||||
// Let's check how it's handled in core.
|
||||
}
|
||||
}
|
||||
|
||||
const question = argv.promptInteractive || argv.prompt || '';
|
||||
|
||||
// Determine approval mode with backward compatibility
|
||||
@@ -652,7 +700,10 @@ export async function loadCliConfig(
|
||||
|
||||
const defaultModel = PREVIEW_GEMINI_MODEL_AUTO;
|
||||
const specifiedModel =
|
||||
argv.model || process.env['GEMINI_MODEL'] || settings.model?.name;
|
||||
argv.model ||
|
||||
profile?.default_model ||
|
||||
process.env['GEMINI_MODEL'] ||
|
||||
settings.model?.name;
|
||||
|
||||
const resolvedModel =
|
||||
specifiedModel === GEMINI_MODEL_ALIAS_AUTO
|
||||
@@ -722,6 +773,8 @@ export async function loadCliConfig(
|
||||
extensionsEnabled,
|
||||
agents: settings.agents,
|
||||
adminSkillsEnabled,
|
||||
profilesDir,
|
||||
activeProfile: activeProfileName || undefined,
|
||||
allowedMcpServers: mcpEnabled
|
||||
? (argv.allowedMcpServerNames ?? settings.mcp?.allowed)
|
||||
: undefined,
|
||||
@@ -806,6 +859,7 @@ export async function loadCliConfig(
|
||||
fakeResponses: argv.fakeResponses,
|
||||
recordResponses: argv.recordResponses,
|
||||
retryFetchErrors: settings.general?.retryFetchErrors,
|
||||
billing: settings.billing,
|
||||
maxAttempts: settings.general?.maxAttempts,
|
||||
ptyInfo: ptyInfo?.name,
|
||||
disableLLMCorrection: settings.tools?.disableLLMCorrection,
|
||||
@@ -818,7 +872,7 @@ export async function loadCliConfig(
|
||||
hooks: settings.hooks || {},
|
||||
disabledHooks: settings.hooksConfig?.disabled || [],
|
||||
projectHooks: projectHooks || {},
|
||||
onModelChange: (model: string) => saveModelChange(loadedSettings, model),
|
||||
onModelChange: (model: string) => saveModelChange(loadSettings(cwd), model),
|
||||
onReload: async () => {
|
||||
const refreshedSettings = loadSettings(cwd);
|
||||
return {
|
||||
|
||||
@@ -345,4 +345,144 @@ describe('ExtensionManager', () => {
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('Extension Renaming', () => {
|
||||
it('should support renaming an extension during update', async () => {
|
||||
// 1. Setup existing extension
|
||||
const oldName = 'old-name';
|
||||
const newName = 'new-name';
|
||||
const extDir = path.join(userExtensionsDir, oldName);
|
||||
fs.mkdirSync(extDir, { recursive: true });
|
||||
fs.writeFileSync(
|
||||
path.join(extDir, 'gemini-extension.json'),
|
||||
JSON.stringify({ name: oldName, version: '1.0.0' }),
|
||||
);
|
||||
fs.writeFileSync(
|
||||
path.join(extDir, 'metadata.json'),
|
||||
JSON.stringify({ type: 'local', source: extDir }),
|
||||
);
|
||||
|
||||
await extensionManager.loadExtensions();
|
||||
|
||||
// 2. Create a temporary "new" version with a different name
|
||||
const newSourceDir = fs.mkdtempSync(
|
||||
path.join(tempHomeDir, 'new-source-'),
|
||||
);
|
||||
fs.writeFileSync(
|
||||
path.join(newSourceDir, 'gemini-extension.json'),
|
||||
JSON.stringify({ name: newName, version: '1.1.0' }),
|
||||
);
|
||||
fs.writeFileSync(
|
||||
path.join(newSourceDir, 'metadata.json'),
|
||||
JSON.stringify({ type: 'local', source: newSourceDir }),
|
||||
);
|
||||
|
||||
// 3. Update the extension
|
||||
await extensionManager.installOrUpdateExtension(
|
||||
{ type: 'local', source: newSourceDir },
|
||||
{ name: oldName, version: '1.0.0' },
|
||||
);
|
||||
|
||||
// 4. Verify old directory is gone and new one exists
|
||||
expect(fs.existsSync(path.join(userExtensionsDir, oldName))).toBe(false);
|
||||
expect(fs.existsSync(path.join(userExtensionsDir, newName))).toBe(true);
|
||||
|
||||
// Verify the loaded state is updated
|
||||
const extensions = extensionManager.getExtensions();
|
||||
expect(extensions.some((e) => e.name === newName)).toBe(true);
|
||||
expect(extensions.some((e) => e.name === oldName)).toBe(false);
|
||||
});
|
||||
|
||||
it('should carry over enablement status when renaming', async () => {
|
||||
const oldName = 'old-name';
|
||||
const newName = 'new-name';
|
||||
const extDir = path.join(userExtensionsDir, oldName);
|
||||
fs.mkdirSync(extDir, { recursive: true });
|
||||
fs.writeFileSync(
|
||||
path.join(extDir, 'gemini-extension.json'),
|
||||
JSON.stringify({ name: oldName, version: '1.0.0' }),
|
||||
);
|
||||
fs.writeFileSync(
|
||||
path.join(extDir, 'metadata.json'),
|
||||
JSON.stringify({ type: 'local', source: extDir }),
|
||||
);
|
||||
|
||||
// Enable it
|
||||
const enablementManager = extensionManager.getEnablementManager();
|
||||
enablementManager.enable(oldName, true, tempHomeDir);
|
||||
|
||||
await extensionManager.loadExtensions();
|
||||
const extension = extensionManager.getExtensions()[0];
|
||||
expect(extension.isActive).toBe(true);
|
||||
|
||||
const newSourceDir = fs.mkdtempSync(
|
||||
path.join(tempHomeDir, 'new-source-'),
|
||||
);
|
||||
fs.writeFileSync(
|
||||
path.join(newSourceDir, 'gemini-extension.json'),
|
||||
JSON.stringify({ name: newName, version: '1.1.0' }),
|
||||
);
|
||||
fs.writeFileSync(
|
||||
path.join(newSourceDir, 'metadata.json'),
|
||||
JSON.stringify({ type: 'local', source: newSourceDir }),
|
||||
);
|
||||
|
||||
await extensionManager.installOrUpdateExtension(
|
||||
{ type: 'local', source: newSourceDir },
|
||||
{ name: oldName, version: '1.0.0' },
|
||||
);
|
||||
|
||||
// Verify new name is enabled
|
||||
expect(enablementManager.isEnabled(newName, tempHomeDir)).toBe(true);
|
||||
// Verify old name is removed from enablement
|
||||
expect(enablementManager.readConfig()[oldName]).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should prevent renaming if the new name conflicts with an existing extension', async () => {
|
||||
// Setup two extensions
|
||||
const ext1Dir = path.join(userExtensionsDir, 'ext1');
|
||||
fs.mkdirSync(ext1Dir, { recursive: true });
|
||||
fs.writeFileSync(
|
||||
path.join(ext1Dir, 'gemini-extension.json'),
|
||||
JSON.stringify({ name: 'ext1', version: '1.0.0' }),
|
||||
);
|
||||
fs.writeFileSync(
|
||||
path.join(ext1Dir, 'metadata.json'),
|
||||
JSON.stringify({ type: 'local', source: ext1Dir }),
|
||||
);
|
||||
|
||||
const ext2Dir = path.join(userExtensionsDir, 'ext2');
|
||||
fs.mkdirSync(ext2Dir, { recursive: true });
|
||||
fs.writeFileSync(
|
||||
path.join(ext2Dir, 'gemini-extension.json'),
|
||||
JSON.stringify({ name: 'ext2', version: '1.0.0' }),
|
||||
);
|
||||
fs.writeFileSync(
|
||||
path.join(ext2Dir, 'metadata.json'),
|
||||
JSON.stringify({ type: 'local', source: ext2Dir }),
|
||||
);
|
||||
|
||||
await extensionManager.loadExtensions();
|
||||
|
||||
// Try to update ext1 to name 'ext2'
|
||||
const newSourceDir = fs.mkdtempSync(
|
||||
path.join(tempHomeDir, 'new-source-'),
|
||||
);
|
||||
fs.writeFileSync(
|
||||
path.join(newSourceDir, 'gemini-extension.json'),
|
||||
JSON.stringify({ name: 'ext2', version: '1.1.0' }),
|
||||
);
|
||||
fs.writeFileSync(
|
||||
path.join(newSourceDir, 'metadata.json'),
|
||||
JSON.stringify({ type: 'local', source: newSourceDir }),
|
||||
);
|
||||
|
||||
await expect(
|
||||
extensionManager.installOrUpdateExtension(
|
||||
{ type: 'local', source: newSourceDir },
|
||||
{ name: 'ext1', version: '1.0.0' },
|
||||
),
|
||||
).rejects.toThrow(/already installed/);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -129,6 +129,10 @@ export class ExtensionManager extends ExtensionLoader {
|
||||
this.requestSetting = options.requestSetting ?? undefined;
|
||||
}
|
||||
|
||||
getEnablementManager(): ExtensionEnablementManager {
|
||||
return this.extensionEnablementManager;
|
||||
}
|
||||
|
||||
setRequestConsent(
|
||||
requestConsent: (consent: string) => Promise<boolean>,
|
||||
): void {
|
||||
@@ -271,17 +275,28 @@ Would you like to attempt to install via "git clone" instead?`,
|
||||
newExtensionConfig = await this.loadExtensionConfig(localSourcePath);
|
||||
|
||||
const newExtensionName = newExtensionConfig.name;
|
||||
const previousName = previousExtensionConfig?.name ?? newExtensionName;
|
||||
const previous = this.getExtensions().find(
|
||||
(installed) => installed.name === newExtensionName,
|
||||
(installed) => installed.name === previousName,
|
||||
);
|
||||
const nameConflict = this.getExtensions().find(
|
||||
(installed) =>
|
||||
installed.name === newExtensionName &&
|
||||
installed.name !== previousName,
|
||||
);
|
||||
|
||||
if (isUpdate && !previous) {
|
||||
throw new Error(
|
||||
`Extension "${newExtensionName}" was not already installed, cannot update it.`,
|
||||
`Extension "${previousName}" was not already installed, cannot update it.`,
|
||||
);
|
||||
} else if (!isUpdate && previous) {
|
||||
throw new Error(
|
||||
`Extension "${newExtensionName}" is already installed. Please uninstall it first.`,
|
||||
);
|
||||
} else if (isUpdate && nameConflict) {
|
||||
throw new Error(
|
||||
`Cannot update to "${newExtensionName}" because an extension with that name is already installed.`,
|
||||
);
|
||||
}
|
||||
|
||||
const newHasHooks = fs.existsSync(
|
||||
@@ -298,6 +313,11 @@ Would you like to attempt to install via "git clone" instead?`,
|
||||
path.join(localSourcePath, 'skills'),
|
||||
);
|
||||
const previousSkills = previous?.skills ?? [];
|
||||
const isMigrating = Boolean(
|
||||
previous &&
|
||||
previous.installMetadata &&
|
||||
previous.installMetadata.source !== installMetadata.source,
|
||||
);
|
||||
|
||||
await maybeRequestConsentOrFail(
|
||||
newExtensionConfig,
|
||||
@@ -307,19 +327,46 @@ Would you like to attempt to install via "git clone" instead?`,
|
||||
previousHasHooks,
|
||||
newSkills,
|
||||
previousSkills,
|
||||
isMigrating,
|
||||
);
|
||||
const extensionId = getExtensionId(newExtensionConfig, installMetadata);
|
||||
const destinationPath = new ExtensionStorage(
|
||||
newExtensionName,
|
||||
).getExtensionDir();
|
||||
|
||||
if (
|
||||
(!isUpdate || newExtensionName !== previousName) &&
|
||||
fs.existsSync(destinationPath)
|
||||
) {
|
||||
throw new Error(
|
||||
`Cannot install extension "${newExtensionName}" because a directory with that name already exists. Please remove it manually.`,
|
||||
);
|
||||
}
|
||||
|
||||
let previousSettings: Record<string, string> | undefined;
|
||||
if (isUpdate) {
|
||||
let wasEnabledGlobally = false;
|
||||
let wasEnabledWorkspace = false;
|
||||
if (isUpdate && previousExtensionConfig) {
|
||||
const previousExtensionId = previous?.installMetadata
|
||||
? getExtensionId(previousExtensionConfig, previous.installMetadata)
|
||||
: extensionId;
|
||||
previousSettings = await getEnvContents(
|
||||
previousExtensionConfig,
|
||||
extensionId,
|
||||
previousExtensionId,
|
||||
this.workspaceDir,
|
||||
);
|
||||
await this.uninstallExtension(newExtensionName, isUpdate);
|
||||
if (newExtensionName !== previousName) {
|
||||
wasEnabledGlobally = this.extensionEnablementManager.isEnabled(
|
||||
previousName,
|
||||
homedir(),
|
||||
);
|
||||
wasEnabledWorkspace = this.extensionEnablementManager.isEnabled(
|
||||
previousName,
|
||||
this.workspaceDir,
|
||||
);
|
||||
this.extensionEnablementManager.remove(previousName);
|
||||
}
|
||||
await this.uninstallExtension(previousName, isUpdate);
|
||||
}
|
||||
|
||||
await fs.promises.mkdir(destinationPath, { recursive: true });
|
||||
@@ -392,6 +439,18 @@ Would you like to attempt to install via "git clone" instead?`,
|
||||
CoreToolCallStatus.Success,
|
||||
),
|
||||
);
|
||||
|
||||
if (newExtensionName !== previousName) {
|
||||
if (wasEnabledGlobally) {
|
||||
await this.enableExtension(newExtensionName, SettingScope.User);
|
||||
}
|
||||
if (wasEnabledWorkspace) {
|
||||
await this.enableExtension(
|
||||
newExtensionName,
|
||||
SettingScope.Workspace,
|
||||
);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
await logExtensionInstallEvent(
|
||||
this.telemetryConfig,
|
||||
@@ -873,6 +932,7 @@ Would you like to attempt to install via "git clone" instead?`,
|
||||
path: effectiveExtensionPath,
|
||||
contextFiles,
|
||||
installMetadata,
|
||||
migratedTo: config.migratedTo,
|
||||
mcpServers: config.mcpServers,
|
||||
excludeTools: config.excludeTools,
|
||||
hooks,
|
||||
|
||||
@@ -31,6 +31,7 @@ import {
|
||||
loadSettings,
|
||||
createTestMergedSettings,
|
||||
SettingScope,
|
||||
resetSettingsCacheForTesting,
|
||||
} from './settings.js';
|
||||
import {
|
||||
isWorkspaceTrusted,
|
||||
@@ -161,6 +162,7 @@ describe('extension tests', () => {
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
resetSettingsCacheForTesting();
|
||||
keychainData = {};
|
||||
mockKeychainStorage = {
|
||||
getSecret: vi
|
||||
|
||||