mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-07-24 00:30:59 -07:00
Compare commits
14 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 4874bedbef | |||
| d6a1864961 | |||
| 1c963345a4 | |||
| 268970671a | |||
| e879100f07 | |||
| 7a71df31ff | |||
| 76a5e4f87f | |||
| 10ade0306b | |||
| 73e319324f | |||
| f0b49261a5 | |||
| 0e3d2fb880 | |||
| bb8565ee9d | |||
| bfbdae8e3a | |||
| 40f9db30ce |
@@ -2,7 +2,8 @@
|
||||
"experimental": {
|
||||
"extensionReloading": true,
|
||||
"modelSteering": true,
|
||||
"memoryManager": true
|
||||
"memoryManager": true,
|
||||
"topicUpdateNarration": true
|
||||
},
|
||||
"general": {
|
||||
"devtools": true
|
||||
|
||||
@@ -85,25 +85,17 @@ accessible.
|
||||
|
||||
- **Callouts**: Use GitHub-flavored markdown alerts to highlight important
|
||||
information. To ensure the formatting is preserved by `npm run format`, place
|
||||
an empty line, then a prettier ignore comment directly before the callout
|
||||
block. Use `<!-- prettier-ignore -->` for standard Markdown files (`.md`) and
|
||||
`{/* prettier-ignore */}` for MDX files (`.mdx`). The callout type (`[!TYPE]`)
|
||||
should be on the first line, followed by a newline, and then the content, with
|
||||
each subsequent line of content starting with `>`. Available types are `NOTE`,
|
||||
`TIP`, `IMPORTANT`, `WARNING`, and `CAUTION`.
|
||||
an empty line, then the `<!-- prettier-ignore -->` comment directly before
|
||||
the callout block. The callout type (`[!TYPE]`) should be on the first line,
|
||||
followed by a newline, and then the content, with each subsequent line of
|
||||
content starting with `>`. Available types are `NOTE`, `TIP`, `IMPORTANT`,
|
||||
`WARNING`, and `CAUTION`.
|
||||
|
||||
Example (.md):
|
||||
Example:
|
||||
|
||||
<!-- prettier-ignore -->
|
||||
> [!NOTE]
|
||||
> This is an example of a multi-line note that will be preserved
|
||||
> by Prettier.
|
||||
|
||||
Example (.mdx):
|
||||
|
||||
{/* prettier-ignore */}
|
||||
> [!NOTE]
|
||||
> This is an example of a multi-line note that will be preserved
|
||||
> by Prettier.
|
||||
|
||||
### Links
|
||||
@@ -126,7 +118,6 @@ accessible.
|
||||
<!-- prettier-ignore -->
|
||||
> [!NOTE]
|
||||
> This is an experimental feature currently under active development.
|
||||
(Note: Use `{/* prettier-ignore */}` if editing an `.mdx` file.)
|
||||
|
||||
- **Headings:** Use hierarchical headings to support the user journey.
|
||||
- **Procedures:**
|
||||
|
||||
@@ -22,4 +22,3 @@ Thumbs.db
|
||||
.pytest_cache
|
||||
**/SKILL.md
|
||||
packages/sdk/test-data/*.json
|
||||
*.mdx
|
||||
|
||||
@@ -0,0 +1,111 @@
|
||||
# Running User Simulation in Docker with External Knowledge Source
|
||||
|
||||
This guide explains how to run the User Simulator in a Docker environment while
|
||||
mounting an external knowledge base. This setup allows the simulator to "learn"
|
||||
from its interactions and persist that knowledge back to your host machine.
|
||||
|
||||
We have provided an automated script that handles the entire setup, execution,
|
||||
and cleanup process.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- **Docker** installed and running.
|
||||
- **Gemini API Key** (standard `AIza...` key).
|
||||
- Local checkout of the `gemini-cli` repository.
|
||||
|
||||
## Execution via Automation Script (Recommended)
|
||||
|
||||
The easiest and most reliable way to run the simulation is using the provided
|
||||
bash script. This script automatically:
|
||||
|
||||
1. Creates a uniquely timestamped workspace folder on your host.
|
||||
2. Generates a global `settings.json` file to natively bypass the CLI's
|
||||
interactive Folder Trust and Authentication dialogs.
|
||||
3. Builds the sandbox image from your current branch.
|
||||
4. Mounts the workspace and runs the container with `--init` to gracefully
|
||||
handle termination (e.g., `Ctrl+C`).
|
||||
|
||||
### Running the Script
|
||||
|
||||
Ensure your API key is exported:
|
||||
|
||||
```bash
|
||||
export GEMINI_API_KEY="AIzaSy..."
|
||||
```
|
||||
|
||||
Run the script from the root of the repository:
|
||||
|
||||
```bash
|
||||
# Uses the default prompt ("make a snake game in python")
|
||||
./scripts/run_simulator_docker.sh
|
||||
|
||||
# Or, provide a custom prompt:
|
||||
./scripts/run_simulator_docker.sh "create a simple react counter component"
|
||||
```
|
||||
|
||||
## Manual Execution Breakdown
|
||||
|
||||
If you need to run the simulation manually, here is exactly what the automated
|
||||
script does under the hood:
|
||||
|
||||
### 1. Prepare Workspace & Knowledge Source
|
||||
|
||||
```bash
|
||||
WORKSPACE_DIR="/tmp/gemini_docker_workspace"
|
||||
mkdir -p "$WORKSPACE_DIR"
|
||||
touch "$WORKSPACE_DIR/knowledge.md"
|
||||
chmod -R 777 "$WORKSPACE_DIR"
|
||||
```
|
||||
|
||||
### 2. Bypass Interactive Startup Dialogs
|
||||
|
||||
To prevent the simulator from getting stuck on the initial Auth or Folder Trust
|
||||
screens, generate a global `settings.json` file.
|
||||
|
||||
```bash
|
||||
mkdir -p "$WORKSPACE_DIR/.gemini"
|
||||
echo '{
|
||||
"security": {
|
||||
"auth": { "selectedType": "gemini-api-key" },
|
||||
"folderTrust": { "enabled": false }
|
||||
}
|
||||
}' > "$WORKSPACE_DIR/.gemini/settings.json"
|
||||
chmod 777 "$WORKSPACE_DIR/.gemini/settings.json"
|
||||
```
|
||||
|
||||
### 3. Build the Image
|
||||
|
||||
```bash
|
||||
GEMINI_SANDBOX=docker npm run build:sandbox -- -i gemini-cli-simulator:latest
|
||||
```
|
||||
|
||||
### 4. Run the Container
|
||||
|
||||
Notice the `--init` flag (for `Ctrl+C` support) and the explicit mount mapping
|
||||
the `settings.json` file into `/home/node/.gemini/` inside the container.
|
||||
|
||||
```bash
|
||||
docker run -it --rm --init \
|
||||
-v "$WORKSPACE_DIR:/workspace" \
|
||||
-v "$WORKSPACE_DIR/.gemini/settings.json:/home/node/.gemini/settings.json" \
|
||||
-w /workspace \
|
||||
-e GEMINI_API_KEY="$GEMINI_API_KEY" \
|
||||
-e GEMINI_DEBUG_LOG_FILE="/workspace/debug.log" \
|
||||
gemini-cli-simulator:latest \
|
||||
gemini --prompt-interactive "make a snake game in python" \
|
||||
--approval-mode plan \
|
||||
--simulate-user \
|
||||
--knowledge-source "/workspace/knowledge.md"
|
||||
```
|
||||
|
||||
## Verification
|
||||
|
||||
Once the simulation completes, verify the results in your workspace folder:
|
||||
|
||||
1. **Generated Code:** Check for project files (e.g., `snake.py`).
|
||||
2. **Persistent Knowledge:** Check `knowledge.md`. You should see new rules
|
||||
dynamically appended by the simulator.
|
||||
3. **Logs:**
|
||||
- `debug.log`: Detailed internal LLM decision logic.
|
||||
- `interactions_<timestamp>.txt`: Raw screen scrape frames seen by the
|
||||
simulator's "eyes".
|
||||
@@ -1,6 +1,6 @@
|
||||
# Latest stable release: v0.38.2
|
||||
# Latest stable release: v0.38.1
|
||||
|
||||
Released: April 17, 2026
|
||||
Released: April 15, 2026
|
||||
|
||||
For most users, our latest stable release is the recommended release. Install
|
||||
the latest stable version with:
|
||||
@@ -29,9 +29,6 @@ npm install -g @google/gemini-cli
|
||||
|
||||
## What's Changed
|
||||
|
||||
- fix(patch): cherry-pick 14b2f35 to release/v0.38.1-pr-24974 to patch version
|
||||
v0.38.1 and create version 0.38.2 by @gemini-cli-robot in
|
||||
[#25585](https://github.com/google-gemini/gemini-cli/pull/25585)
|
||||
- fix(patch): cherry-pick 050c303 to release/v0.38.0-pr-25317 to patch version
|
||||
v0.38.0 and create version 0.38.1 by @gemini-cli-robot in
|
||||
[#25466](https://github.com/google-gemini/gemini-cli/pull/25466)
|
||||
@@ -271,4 +268,4 @@ npm install -g @google/gemini-cli
|
||||
[#24844](https://github.com/google-gemini/gemini-cli/pull/24844)
|
||||
|
||||
**Full Changelog**:
|
||||
https://github.com/google-gemini/gemini-cli/compare/v0.38.0...v0.38.2
|
||||
https://github.com/google-gemini/gemini-cli/compare/v0.38.0...v0.38.1
|
||||
|
||||
@@ -1,143 +0,0 @@
|
||||
# Auto Memory
|
||||
|
||||
Auto Memory is an experimental feature that mines your past Gemini CLI sessions
|
||||
in the background and turns recurring workflows into reusable
|
||||
[Agent Skills](./skills.md). You review, accept, or discard each extracted skill
|
||||
before it becomes available to future sessions.
|
||||
|
||||
<!-- prettier-ignore -->
|
||||
> [!NOTE]
|
||||
> This is an experimental feature currently under active development.
|
||||
|
||||
## Overview
|
||||
|
||||
Every session you run with Gemini CLI is recorded locally as a transcript. Auto
|
||||
Memory scans those transcripts for procedural patterns that recur across
|
||||
sessions, then drafts each pattern as a `SKILL.md` file in a project-local
|
||||
inbox. You inspect the draft, decide whether it captures real expertise, and
|
||||
promote it to your global or workspace skills directory if you want it.
|
||||
|
||||
You'll use Auto Memory when you want to:
|
||||
|
||||
- **Capture team workflows** that you find yourself walking the agent through
|
||||
more than once.
|
||||
- **Codify hard-won fixes** for project-specific landmines so future sessions
|
||||
avoid them.
|
||||
- **Bootstrap a skills library** without writing every `SKILL.md` by hand.
|
||||
|
||||
Auto Memory complements—but does not replace—the
|
||||
[`save_memory` tool](../tools/memory.md), which captures single facts into
|
||||
`GEMINI.md`. Auto Memory captures multi-step procedures into skills.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- Gemini CLI installed and authenticated.
|
||||
- At least 10 user messages across recent, idle sessions in the project. Auto
|
||||
Memory ignores active or trivial sessions.
|
||||
|
||||
## How to enable Auto Memory
|
||||
|
||||
Auto Memory is off by default. Enable it in your settings file:
|
||||
|
||||
1. Open your global settings file at `~/.gemini/settings.json`. If you only
|
||||
want Auto Memory in one project, edit `.gemini/settings.json` in that
|
||||
project instead.
|
||||
|
||||
2. Add the experimental flag:
|
||||
|
||||
```json
|
||||
{
|
||||
"experimental": {
|
||||
"autoMemory": true
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
3. Restart Gemini CLI. The flag requires a restart because the extraction
|
||||
service starts during session boot.
|
||||
|
||||
## How Auto Memory works
|
||||
|
||||
Auto Memory runs as a background task on session startup. It does not block the
|
||||
UI, consume your interactive turns, or surface tool prompts.
|
||||
|
||||
1. **Eligibility scan.** The service indexes recent sessions from
|
||||
`~/.gemini/tmp/<project>/chats/`. Sessions are eligible only if they have
|
||||
been idle for at least three hours and contain at least 10 user messages.
|
||||
2. **Lock acquisition.** A lock file in the project's memory directory
|
||||
coordinates across multiple CLI instances so extraction runs at most once at
|
||||
a time.
|
||||
3. **Sub-agent extraction.** A specialized sub-agent (named `confucius`)
|
||||
reviews the session index, reads any sessions that look like they contain
|
||||
repeated procedural workflows, and drafts new `SKILL.md` files. Its
|
||||
instructions tell it to default to creating zero skills unless the evidence
|
||||
is strong, so most runs produce no inbox items.
|
||||
4. **Patch validation.** If the sub-agent proposes edits to skills outside the
|
||||
inbox (for example, an existing global skill), it writes a unified diff
|
||||
`.patch` file. Auto Memory dry-runs each patch and discards any that do not
|
||||
apply cleanly.
|
||||
5. **Notification.** When a run produces new skills or patches, Gemini CLI
|
||||
surfaces an inline message telling you how many items are waiting.
|
||||
|
||||
## How to review extracted skills
|
||||
|
||||
Use the `/memory inbox` slash command to open the inbox dialog at any time:
|
||||
|
||||
**Command:** `/memory inbox`
|
||||
|
||||
The dialog lists each draft skill with its name, description, and source
|
||||
sessions. From there you can:
|
||||
|
||||
- **Read** the full `SKILL.md` body before deciding.
|
||||
- **Promote** a skill to your user (`~/.gemini/skills/`) or workspace
|
||||
(`.gemini/skills/`) directory.
|
||||
- **Discard** a skill you do not want.
|
||||
- **Apply** or reject a `.patch` proposal against an existing skill.
|
||||
|
||||
Promoted skills become discoverable in the next session and follow the standard
|
||||
[skill discovery precedence](./skills.md#skill-discovery-tiers).
|
||||
|
||||
## How to disable Auto Memory
|
||||
|
||||
To turn off background extraction, set the flag back to `false` in your settings
|
||||
file and restart Gemini CLI:
|
||||
|
||||
```json
|
||||
{
|
||||
"experimental": {
|
||||
"autoMemory": false
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Disabling the flag stops the background service immediately on the next session
|
||||
start. Existing inbox items remain on disk; you can either drain them with
|
||||
`/memory inbox` first or remove the project memory directory manually.
|
||||
|
||||
## Data and privacy
|
||||
|
||||
- Auto Memory only reads session files that already exist locally on your
|
||||
machine. Nothing is uploaded to Gemini outside the normal API calls the
|
||||
extraction sub-agent makes during its run.
|
||||
- The sub-agent is instructed to redact secrets, tokens, and credentials it
|
||||
encounters and to never copy large tool outputs verbatim.
|
||||
- Drafted skills live in your project's memory directory until you promote or
|
||||
discard them. They are not automatically loaded into any session.
|
||||
|
||||
## Limitations
|
||||
|
||||
- The sub-agent runs on a preview Gemini Flash model. Extraction quality depends
|
||||
on the model's ability to recognize durable patterns versus one-off incidents.
|
||||
- Auto Memory does not extract skills from the current session. It only
|
||||
considers sessions that have been idle for three hours or more.
|
||||
- Inbox items are stored per project. Skills extracted in one workspace are not
|
||||
visible from another until you promote them to the user-scope skills
|
||||
directory.
|
||||
|
||||
## Next steps
|
||||
|
||||
- Learn how skills are discovered and activated in [Agent Skills](./skills.md).
|
||||
- Explore the [memory management tutorial](./tutorials/memory-management.md) for
|
||||
the complementary `save_memory` and `GEMINI.md` workflows.
|
||||
- Review the experimental settings catalog in
|
||||
[Settings](./settings.md#experimental).
|
||||
@@ -507,7 +507,7 @@ events. For more information, see the [telemetry documentation](./telemetry.md).
|
||||
You can enforce a specific authentication method for all users by setting the
|
||||
`security.auth.enforcedType` in the system-level `settings.json` file. This
|
||||
prevents users from choosing a different authentication method. See the
|
||||
[Authentication docs](../get-started/authentication.mdx) for more details.
|
||||
[Authentication docs](../get-started/authentication.md) for more details.
|
||||
|
||||
**Example:** Enforce the use of Google login for all users.
|
||||
|
||||
|
||||
@@ -331,6 +331,7 @@ Storage whenever Gemini CLI exits Plan Mode to start the implementation.
|
||||
#!/usr/bin/env bash
|
||||
# Extract the plan filename from the tool input JSON
|
||||
plan_filename=$(jq -r '.tool_input.plan_filename // empty')
|
||||
plan_filename=$(basename -- "$plan_filename")
|
||||
|
||||
# Construct the absolute path using the GEMINI_PLANS_DIR environment variable
|
||||
plan_path="$GEMINI_PLANS_DIR/$plan_filename"
|
||||
@@ -359,7 +360,7 @@ To register this `AfterTool` hook, add it to your `settings.json`:
|
||||
{
|
||||
"name": "archive-plan",
|
||||
"type": "command",
|
||||
"command": "~/.gemini/hooks/archive-plan.sh"
|
||||
"command": "./.gemini/hooks/archive-plan.sh"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
+11
-14
@@ -39,7 +39,6 @@ they appear in the UI.
|
||||
| Debug Keystroke Logging | `general.debugKeystrokeLogging` | Enable debug logging of keystrokes to the console. | `false` |
|
||||
| Enable Session Cleanup | `general.sessionRetention.enabled` | Enable automatic session cleanup | `true` |
|
||||
| Keep chat history | `general.sessionRetention.maxAge` | Automatically delete chats older than this time period (e.g., "30d", "7d", "24h", "1w") | `"30d"` |
|
||||
| Topic & Update Narration | `general.topicUpdateNarration` | Enable the Topic & Update communication model for reduced chattiness and structured progress reporting. | `true` |
|
||||
|
||||
### Output
|
||||
|
||||
@@ -161,19 +160,17 @@ they appear in the UI.
|
||||
|
||||
### Experimental
|
||||
|
||||
| UI Label | Setting | Description | Default |
|
||||
| ---------------------------------------------------- | ----------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------- | ------- |
|
||||
| Enable Git Worktrees | `experimental.worktrees` | Enable automated Git worktree management for parallel work. | `false` |
|
||||
| Use OSC 52 Paste | `experimental.useOSC52Paste` | Use OSC 52 for pasting. This may be more robust than the default system when using remote terminal sessions (if your terminal is configured to allow it). | `false` |
|
||||
| Use OSC 52 Copy | `experimental.useOSC52Copy` | Use OSC 52 for copying. This may be more robust than the default system when using remote terminal sessions (if your terminal is configured to allow it). | `false` |
|
||||
| Model Steering | `experimental.modelSteering` | Enable model steering (user hints) to guide the model during tool execution. | `false` |
|
||||
| Direct Web Fetch | `experimental.directWebFetch` | Enable web fetch behavior that bypasses LLM summarization. | `false` |
|
||||
| Enable Gemma Model Router | `experimental.gemmaModelRouter.enabled` | Enable the Gemma Model Router (experimental). Requires a local endpoint serving Gemma via the Gemini API using LiteRT-LM shim. | `false` |
|
||||
| Auto-start LiteRT Server | `experimental.gemmaModelRouter.autoStartServer` | Automatically start the LiteRT-LM server when Gemini CLI starts and the Gemma router is enabled. | `false` |
|
||||
| Memory Manager Agent | `experimental.memoryManager` | Replace the built-in save_memory tool with a memory manager subagent that supports adding, removing, de-duplicating, and organizing memories. | `false` |
|
||||
| Auto Memory | `experimental.autoMemory` | Automatically extract reusable skills from past sessions in the background. Review results with /memory inbox. | `false` |
|
||||
| Use the generalist profile to manage agent contexts. | `experimental.generalistProfile` | Suitable for general coding and software development tasks. | `false` |
|
||||
| Enable Context Management | `experimental.contextManagement` | Enable logic for context management. | `false` |
|
||||
| UI Label | Setting | Description | Default |
|
||||
| ---------------------------------------------------- | ----------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------- | ------- |
|
||||
| Enable Git Worktrees | `experimental.worktrees` | Enable automated Git worktree management for parallel work. | `false` |
|
||||
| Use OSC 52 Paste | `experimental.useOSC52Paste` | Use OSC 52 for pasting. This may be more robust than the default system when using remote terminal sessions (if your terminal is configured to allow it). | `false` |
|
||||
| Use OSC 52 Copy | `experimental.useOSC52Copy` | Use OSC 52 for copying. This may be more robust than the default system when using remote terminal sessions (if your terminal is configured to allow it). | `false` |
|
||||
| Model Steering | `experimental.modelSteering` | Enable model steering (user hints) to guide the model during tool execution. | `false` |
|
||||
| Direct Web Fetch | `experimental.directWebFetch` | Enable web fetch behavior that bypasses LLM summarization. | `false` |
|
||||
| Memory Manager Agent | `experimental.memoryManager` | Replace the built-in save_memory tool with a memory manager subagent that supports adding, removing, de-duplicating, and organizing memories. | `false` |
|
||||
| Use the generalist profile to manage agent contexts. | `experimental.generalistProfile` | Suitable for general coding and software development tasks. | `false` |
|
||||
| Enable Context Management | `experimental.contextManagement` | Enable logic for context management. | `false` |
|
||||
| Topic & Update Narration | `experimental.topicUpdateNarration` | Enable the experimental Topic & Update communication model for reduced chattiness and structured progress reporting. | `false` |
|
||||
|
||||
### Skills
|
||||
|
||||
|
||||
@@ -24,7 +24,7 @@ project-specific behavior or create a customized persona.
|
||||
|
||||
You can set the environment variable temporarily in your shell, or persist it
|
||||
via a `.gemini/.env` file. See
|
||||
[Persisting Environment Variables](../get-started/authentication.mdx#persisting-environment-variables).
|
||||
[Persisting Environment Variables](../get-started/authentication.md#persisting-environment-variables).
|
||||
|
||||
- Use the project default path (`.gemini/system.md`):
|
||||
- `GEMINI_SYSTEM_MD=true` or `GEMINI_SYSTEM_MD=1`
|
||||
@@ -51,7 +51,7 @@ error with: `missing system prompt file '<path>'`.
|
||||
- Create `.gemini/system.md`, then add to `.gemini/.env`:
|
||||
- `GEMINI_SYSTEM_MD=1`
|
||||
- Use a custom file under your home directory:
|
||||
- `GEMINI_SYSTEM_MD=~/prompts/system.md gemini`
|
||||
- `GEMINI_SYSTEM_MD=~/prompts/SYSTEM.md gemini`
|
||||
|
||||
## UI indicator
|
||||
|
||||
@@ -102,17 +102,17 @@ safety and workflow rules.
|
||||
|
||||
This creates the file and writes the current built‑in system prompt to it.
|
||||
|
||||
## Best practices: system.md vs GEMINI.md
|
||||
## Best practices: SYSTEM.md vs GEMINI.md
|
||||
|
||||
- system.md (firmware):
|
||||
- SYSTEM.md (firmware):
|
||||
- Non‑negotiable operational rules: safety, tool‑use protocols, approvals, and
|
||||
mechanics that keep the CLI reliable.
|
||||
- Stable across tasks and projects (or per project when needed).
|
||||
- GEMINI.md (strategy):
|
||||
- Persona, goals, methodologies, and project/domain context.
|
||||
- Evolves per task; relies on system.md for safe execution.
|
||||
- Evolves per task; relies on SYSTEM.md for safe execution.
|
||||
|
||||
Keep system.md minimal but complete for safety and tool operation. Keep
|
||||
Keep SYSTEM.md minimal but complete for safety and tool operation. Keep
|
||||
GEMINI.md focused on high‑level guidance and project specifics.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
+11
-18
@@ -35,18 +35,17 @@ The observability system provides:
|
||||
You control telemetry behavior through the `.gemini/settings.json` file.
|
||||
Environment variables can override these settings.
|
||||
|
||||
| Setting | Environment Variable | Description | Values | Default |
|
||||
| -------------- | --------------------------------- | --------------------------------------------------- | ----------------- | ----------------------- |
|
||||
| `enabled` | `GEMINI_TELEMETRY_ENABLED` | Enable or disable telemetry | `true`/`false` | `false` |
|
||||
| `traces` | `GEMINI_TELEMETRY_TRACES_ENABLED` | Enable detailed attribute tracing | `true`/`false` | `false` |
|
||||
| `target` | `GEMINI_TELEMETRY_TARGET` | Where to send telemetry data | `"gcp"`/`"local"` | `"local"` |
|
||||
| `otlpEndpoint` | `GEMINI_TELEMETRY_OTLP_ENDPOINT` | OTLP collector endpoint | URL string | `http://localhost:4317` |
|
||||
| `otlpProtocol` | `GEMINI_TELEMETRY_OTLP_PROTOCOL` | OTLP transport protocol | `"grpc"`/`"http"` | `"grpc"` |
|
||||
| `outfile` | `GEMINI_TELEMETRY_OUTFILE` | Save telemetry to file (overrides `otlpEndpoint`) | file path | - |
|
||||
| `logPrompts` | `GEMINI_TELEMETRY_LOG_PROMPTS` | Include prompts in telemetry logs | `true`/`false` | `true` |
|
||||
| `useCollector` | `GEMINI_TELEMETRY_USE_COLLECTOR` | Use external OTLP collector (advanced) | `true`/`false` | `false` |
|
||||
| `useCliAuth` | `GEMINI_TELEMETRY_USE_CLI_AUTH` | Use CLI credentials for telemetry (GCP target only) | `true`/`false` | `false` |
|
||||
| - | `GEMINI_CLI_SURFACE` | Optional custom label for traffic reporting | string | - |
|
||||
| Setting | Environment Variable | Description | Values | Default |
|
||||
| -------------- | -------------------------------- | --------------------------------------------------- | ----------------- | ----------------------- |
|
||||
| `enabled` | `GEMINI_TELEMETRY_ENABLED` | Enable or disable telemetry | `true`/`false` | `false` |
|
||||
| `target` | `GEMINI_TELEMETRY_TARGET` | Where to send telemetry data | `"gcp"`/`"local"` | `"local"` |
|
||||
| `otlpEndpoint` | `GEMINI_TELEMETRY_OTLP_ENDPOINT` | OTLP collector endpoint | URL string | `http://localhost:4317` |
|
||||
| `otlpProtocol` | `GEMINI_TELEMETRY_OTLP_PROTOCOL` | OTLP transport protocol | `"grpc"`/`"http"` | `"grpc"` |
|
||||
| `outfile` | `GEMINI_TELEMETRY_OUTFILE` | Save telemetry to file (overrides `otlpEndpoint`) | file path | - |
|
||||
| `logPrompts` | `GEMINI_TELEMETRY_LOG_PROMPTS` | Include prompts in telemetry logs | `true`/`false` | `true` |
|
||||
| `useCollector` | `GEMINI_TELEMETRY_USE_COLLECTOR` | Use external OTLP collector (advanced) | `true`/`false` | `false` |
|
||||
| `useCliAuth` | `GEMINI_TELEMETRY_USE_CLI_AUTH` | Use CLI credentials for telemetry (GCP target only) | `true`/`false` | `false` |
|
||||
| - | `GEMINI_CLI_SURFACE` | Optional custom label for traffic reporting | string | - |
|
||||
|
||||
**Note on boolean environment variables:** For boolean settings like `enabled`,
|
||||
setting the environment variable to `true` or `1` enables the feature.
|
||||
@@ -1236,12 +1235,6 @@ These metrics follow standard [OpenTelemetry GenAI semantic conventions].
|
||||
Traces provide an "under-the-hood" view of agent and backend operations. Use
|
||||
traces to debug tool interactions and optimize performance.
|
||||
|
||||
<!-- prettier-ignore -->
|
||||
> [!NOTE]
|
||||
> Detailed trace attributes (like full prompts and tool outputs) are disabled by default
|
||||
> to minimize overhead. You must explicitly set `telemetry.traces` to `true` (or set
|
||||
> `GEMINI_TELEMETRY_TRACES_ENABLED=true`) to capture them.
|
||||
|
||||
Every trace captures rich metadata via standard span attributes.
|
||||
|
||||
<details open>
|
||||
|
||||
@@ -124,5 +124,3 @@ immediately. Force a reload with:
|
||||
- Explore the [Command reference](../../reference/commands.md) for more
|
||||
`/memory` options.
|
||||
- Read the technical spec for [Project context](../../cli/gemini-md.md).
|
||||
- Try the experimental [Auto Memory](../auto-memory.md) feature to extract
|
||||
reusable skills from your past sessions automatically.
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
import { Tabs, TabItem } from '@astrojs/starlight/components';
|
||||
|
||||
# Gemini CLI authentication setup
|
||||
|
||||
To use Gemini CLI, you'll need to authenticate with Google. This guide helps you
|
||||
quickly find the best way to sign in based on your account type and how you're
|
||||
using the CLI.
|
||||
|
||||
<!-- prettier-ignore -->
|
||||
> [!TIP]
|
||||
> Looking for a high-level comparison of all available subscriptions?
|
||||
> To compare features and find the right quota for your needs, see our
|
||||
@@ -24,7 +23,7 @@ Select the authentication method that matches your situation in the table below:
|
||||
| Organization users with a company, school, or Google Workspace account | [Sign in with Google](#login-google) | [Yes](#set-gcp) |
|
||||
| AI Studio user with a Gemini API key | [Use Gemini API Key](#gemini-api) | No |
|
||||
| Google Cloud Vertex AI user | [Vertex AI](#vertex-ai) | [Yes](#set-gcp) |
|
||||
| [Headless mode](#headless) | [Use Gemini API Key](#gemini-api) or<br /> [Vertex AI](#vertex-ai) | No (for Gemini API Key)<br /> [Yes](#set-gcp) (for Vertex AI) |
|
||||
| [Headless mode](#headless) | [Use Gemini API Key](#gemini-api) or<br> [Vertex AI](#vertex-ai) | No (for Gemini API Key)<br> [Yes](#set-gcp) (for Vertex AI) |
|
||||
|
||||
### What is my Google account type?
|
||||
|
||||
@@ -85,24 +84,19 @@ To authenticate and use Gemini CLI with a Gemini API key:
|
||||
|
||||
2. Set the `GEMINI_API_KEY` environment variable to your key. For example:
|
||||
|
||||
<Tabs>
|
||||
<TabItem label="macOS/Linux">
|
||||
**macOS/Linux**
|
||||
|
||||
```bash
|
||||
# Replace YOUR_GEMINI_API_KEY with the key from AI Studio
|
||||
export GEMINI_API_KEY="YOUR_GEMINI_API_KEY"
|
||||
```
|
||||
```bash
|
||||
# Replace YOUR_GEMINI_API_KEY with the key from AI Studio
|
||||
export GEMINI_API_KEY="YOUR_GEMINI_API_KEY"
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
<TabItem label="Windows (PowerShell)">
|
||||
**Windows (PowerShell)**
|
||||
|
||||
```powershell
|
||||
# Replace YOUR_GEMINI_API_KEY with the key from AI Studio
|
||||
$env:GEMINI_API_KEY="YOUR_GEMINI_API_KEY"
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
```powershell
|
||||
# Replace YOUR_GEMINI_API_KEY with the key from AI Studio
|
||||
$env:GEMINI_API_KEY="YOUR_GEMINI_API_KEY"
|
||||
```
|
||||
|
||||
To make this setting persistent, see
|
||||
[Persisting Environment Variables](#persisting-vars).
|
||||
@@ -115,6 +109,7 @@ To authenticate and use Gemini CLI with a Gemini API key:
|
||||
|
||||
4. Select **Use Gemini API key**.
|
||||
|
||||
<!-- prettier-ignore -->
|
||||
> [!WARNING]
|
||||
> Treat API keys, especially for services like Gemini, as sensitive
|
||||
> credentials. Protect them to prevent unauthorized access and potential misuse
|
||||
@@ -136,26 +131,21 @@ or the location where you want to run your jobs.
|
||||
|
||||
For example:
|
||||
|
||||
<Tabs>
|
||||
<TabItem label="macOS/Linux">
|
||||
**macOS/Linux**
|
||||
|
||||
```bash
|
||||
```bash
|
||||
# Replace with your project ID and desired location (for example, us-central1)
|
||||
export GOOGLE_CLOUD_PROJECT="YOUR_PROJECT_ID"
|
||||
export GOOGLE_CLOUD_LOCATION="YOUR_PROJECT_LOCATION"
|
||||
```
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
<TabItem label="Windows (PowerShell)">
|
||||
**Windows (PowerShell)**
|
||||
|
||||
```powershell
|
||||
```powershell
|
||||
# Replace with your project ID and desired location (for example, us-central1)
|
||||
$env:GOOGLE_CLOUD_PROJECT="YOUR_PROJECT_ID"
|
||||
$env:GOOGLE_CLOUD_LOCATION="YOUR_PROJECT_LOCATION"
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
```
|
||||
|
||||
To make any Vertex AI environment variable settings persistent, see
|
||||
[Persisting Environment Variables](#persisting-vars).
|
||||
@@ -167,22 +157,17 @@ Consider this authentication method if you have Google Cloud CLI installed.
|
||||
If you have previously set `GOOGLE_API_KEY` or `GEMINI_API_KEY`, you must unset
|
||||
them to use ADC.
|
||||
|
||||
<Tabs>
|
||||
<TabItem label="macOS/Linux">
|
||||
**macOS/Linux**
|
||||
|
||||
```bash
|
||||
```bash
|
||||
unset GOOGLE_API_KEY GEMINI_API_KEY
|
||||
```
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
<TabItem label="Windows (PowerShell)">
|
||||
**Windows (PowerShell)**
|
||||
|
||||
```powershell
|
||||
```powershell
|
||||
Remove-Item Env:\GOOGLE_API_KEY, Env:\GEMINI_API_KEY -ErrorAction Ignore
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
```
|
||||
|
||||
1. Verify you have a Google Cloud project and Vertex AI API is enabled.
|
||||
|
||||
@@ -210,22 +195,17 @@ pipelines, or if your organization restricts user-based ADC or API key creation.
|
||||
If you have previously set `GOOGLE_API_KEY` or `GEMINI_API_KEY`, you must unset
|
||||
them:
|
||||
|
||||
<Tabs>
|
||||
<TabItem label="macOS/Linux">
|
||||
**macOS/Linux**
|
||||
|
||||
```bash
|
||||
```bash
|
||||
unset GOOGLE_API_KEY GEMINI_API_KEY
|
||||
```
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
<TabItem label="Windows (PowerShell)">
|
||||
**Windows (PowerShell)**
|
||||
|
||||
```powershell
|
||||
```powershell
|
||||
Remove-Item Env:\GOOGLE_API_KEY, Env:\GEMINI_API_KEY -ErrorAction Ignore
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
```
|
||||
|
||||
1. [Create a service account and key](https://cloud.google.com/iam/docs/keys-create-delete)
|
||||
and download the provided JSON file. Assign the "Vertex AI User" role to the
|
||||
@@ -234,24 +214,19 @@ Remove-Item Env:\GOOGLE_API_KEY, Env:\GEMINI_API_KEY -ErrorAction Ignore
|
||||
2. Set the `GOOGLE_APPLICATION_CREDENTIALS` environment variable to the JSON
|
||||
file's absolute path. For example:
|
||||
|
||||
<Tabs>
|
||||
<TabItem label="macOS/Linux">
|
||||
**macOS/Linux**
|
||||
|
||||
```bash
|
||||
# Replace /path/to/your/keyfile.json with the actual path
|
||||
export GOOGLE_APPLICATION_CREDENTIALS="/path/to/your/keyfile.json"
|
||||
```
|
||||
```bash
|
||||
# Replace /path/to/your/keyfile.json with the actual path
|
||||
export GOOGLE_APPLICATION_CREDENTIALS="/path/to/your/keyfile.json"
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
<TabItem label="Windows (PowerShell)">
|
||||
**Windows (PowerShell)**
|
||||
|
||||
```powershell
|
||||
# Replace C:\path\to\your\keyfile.json with the actual path
|
||||
$env:GOOGLE_APPLICATION_CREDENTIALS="C:\path\to\your\keyfile.json"
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
```powershell
|
||||
# Replace C:\path\to\your\keyfile.json with the actual path
|
||||
$env:GOOGLE_APPLICATION_CREDENTIALS="C:\path\to\your\keyfile.json"
|
||||
```
|
||||
|
||||
3. [Configure your Google Cloud Project](#set-gcp).
|
||||
|
||||
@@ -263,6 +238,7 @@ Remove-Item Env:\GOOGLE_API_KEY, Env:\GEMINI_API_KEY -ErrorAction Ignore
|
||||
|
||||
5. Select **Vertex AI**.
|
||||
|
||||
<!-- prettier-ignore -->
|
||||
> [!WARNING]
|
||||
> Protect your service account key file as it gives access to
|
||||
> your resources.
|
||||
@@ -274,24 +250,19 @@ Remove-Item Env:\GOOGLE_API_KEY, Env:\GEMINI_API_KEY -ErrorAction Ignore
|
||||
|
||||
2. Set the `GOOGLE_API_KEY` environment variable:
|
||||
|
||||
<Tabs>
|
||||
<TabItem label="macOS/Linux">
|
||||
**macOS/Linux**
|
||||
|
||||
```bash
|
||||
# Replace YOUR_GOOGLE_API_KEY with your Vertex AI API key
|
||||
export GOOGLE_API_KEY="YOUR_GOOGLE_API_KEY"
|
||||
```
|
||||
```bash
|
||||
# Replace YOUR_GOOGLE_API_KEY with your Vertex AI API key
|
||||
export GOOGLE_API_KEY="YOUR_GOOGLE_API_KEY"
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
<TabItem label="Windows (PowerShell)">
|
||||
**Windows (PowerShell)**
|
||||
|
||||
```powershell
|
||||
# Replace YOUR_GOOGLE_API_KEY with your Vertex AI API key
|
||||
$env:GOOGLE_API_KEY="YOUR_GOOGLE_API_KEY"
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
```powershell
|
||||
# Replace YOUR_GOOGLE_API_KEY with your Vertex AI API key
|
||||
$env:GOOGLE_API_KEY="YOUR_GOOGLE_API_KEY"
|
||||
```
|
||||
|
||||
If you see errors like `"API keys are not supported by this API..."`, your
|
||||
organization might restrict API key usage for this service. Try the other
|
||||
@@ -309,6 +280,7 @@ Remove-Item Env:\GOOGLE_API_KEY, Env:\GEMINI_API_KEY -ErrorAction Ignore
|
||||
|
||||
## Set your Google Cloud project <a id="set-gcp"></a>
|
||||
|
||||
<!-- prettier-ignore -->
|
||||
> [!IMPORTANT]
|
||||
> Most individual Google accounts (free and paid) don't require a
|
||||
> Google Cloud project for authentication.
|
||||
@@ -336,24 +308,19 @@ To configure Gemini CLI to use a Google Cloud project, do the following:
|
||||
|
||||
For example, to set the `GOOGLE_CLOUD_PROJECT_ID` variable:
|
||||
|
||||
<Tabs>
|
||||
<TabItem label="macOS/Linux">
|
||||
**macOS/Linux**
|
||||
|
||||
```bash
|
||||
# Replace YOUR_PROJECT_ID with your actual Google Cloud project ID
|
||||
export GOOGLE_CLOUD_PROJECT="YOUR_PROJECT_ID"
|
||||
```
|
||||
```bash
|
||||
# Replace YOUR_PROJECT_ID with your actual Google Cloud project ID
|
||||
export GOOGLE_CLOUD_PROJECT="YOUR_PROJECT_ID"
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
<TabItem label="Windows (PowerShell)">
|
||||
**Windows (PowerShell)**
|
||||
|
||||
```powershell
|
||||
# Replace YOUR_PROJECT_ID with your actual Google Cloud project ID
|
||||
$env:GOOGLE_CLOUD_PROJECT="YOUR_PROJECT_ID"
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
```powershell
|
||||
# Replace YOUR_PROJECT_ID with your actual Google Cloud project ID
|
||||
$env:GOOGLE_CLOUD_PROJECT="YOUR_PROJECT_ID"
|
||||
```
|
||||
|
||||
To make this setting persistent, see
|
||||
[Persisting Environment Variables](#persisting-vars).
|
||||
@@ -366,29 +333,21 @@ persist them with the following methods:
|
||||
1. **Add your environment variables to your shell configuration file:** Append
|
||||
the environment variable commands to your shell's startup file.
|
||||
|
||||
<Tabs>
|
||||
<TabItem label="macOS/Linux">
|
||||
**macOS/Linux** (for example, `~/.bashrc`, `~/.zshrc`, or `~/.profile`):
|
||||
|
||||
(for example, `~/.bashrc`, `~/.zshrc`, or `~/.profile`):
|
||||
```bash
|
||||
echo 'export GOOGLE_CLOUD_PROJECT="YOUR_PROJECT_ID"' >> ~/.bashrc
|
||||
source ~/.bashrc
|
||||
```
|
||||
|
||||
```bash
|
||||
echo 'export GOOGLE_CLOUD_PROJECT="YOUR_PROJECT_ID"' >> ~/.bashrc
|
||||
source ~/.bashrc
|
||||
```
|
||||
**Windows (PowerShell)** (for example, `$PROFILE`):
|
||||
|
||||
</TabItem>
|
||||
<TabItem label="Windows (PowerShell)">
|
||||
|
||||
(for example, `$PROFILE`):
|
||||
|
||||
```powershell
|
||||
Add-Content -Path $PROFILE -Value '$env:GOOGLE_CLOUD_PROJECT="YOUR_PROJECT_ID"'
|
||||
. $PROFILE
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
```powershell
|
||||
Add-Content -Path $PROFILE -Value '$env:GOOGLE_CLOUD_PROJECT="YOUR_PROJECT_ID"'
|
||||
. $PROFILE
|
||||
```
|
||||
|
||||
<!-- prettier-ignore -->
|
||||
> [!WARNING]
|
||||
> Be aware that when you export API keys or service account
|
||||
> paths in your shell configuration file, any process launched from that
|
||||
@@ -402,30 +361,25 @@ persist them with the following methods:
|
||||
|
||||
Example for user-wide settings:
|
||||
|
||||
<Tabs>
|
||||
<TabItem label="macOS/Linux">
|
||||
**macOS/Linux**
|
||||
|
||||
```bash
|
||||
mkdir -p ~/.gemini
|
||||
cat >> ~/.gemini/.env <<'EOF'
|
||||
GOOGLE_CLOUD_PROJECT="your-project-id"
|
||||
# Add other variables like GEMINI_API_KEY as needed
|
||||
EOF
|
||||
```
|
||||
```bash
|
||||
mkdir -p ~/.gemini
|
||||
cat >> ~/.gemini/.env <<'EOF'
|
||||
GOOGLE_CLOUD_PROJECT="your-project-id"
|
||||
# Add other variables like GEMINI_API_KEY as needed
|
||||
EOF
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
<TabItem label="Windows (PowerShell)">
|
||||
**Windows (PowerShell)**
|
||||
|
||||
```powershell
|
||||
New-Item -ItemType Directory -Force -Path "$env:USERPROFILE\.gemini"
|
||||
@"
|
||||
GOOGLE_CLOUD_PROJECT="your-project-id"
|
||||
# Add other variables like GEMINI_API_KEY as needed
|
||||
"@ | Out-File -FilePath "$env:USERPROFILE\.gemini\.env" -Encoding utf8 -Append
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
```powershell
|
||||
New-Item -ItemType Directory -Force -Path "$env:USERPROFILE\.gemini"
|
||||
@"
|
||||
GOOGLE_CLOUD_PROJECT="your-project-id"
|
||||
# Add other variables like GEMINI_API_KEY as needed
|
||||
"@ | Out-File -FilePath "$env:USERPROFILE\.gemini\.env" -Encoding utf8 -Append
|
||||
```
|
||||
|
||||
Variables are loaded from the first file found, not merged.
|
||||
|
||||
@@ -24,8 +24,7 @@ Once Gemini CLI is installed, run Gemini CLI from your command line:
|
||||
gemini
|
||||
```
|
||||
|
||||
For more installation options, see
|
||||
[Gemini CLI Installation](./installation.mdx).
|
||||
For more installation options, see [Gemini CLI Installation](./installation.md).
|
||||
|
||||
## Authenticate
|
||||
|
||||
@@ -47,7 +46,7 @@ cases, you can log in with your existing Google account:
|
||||
|
||||
Certain account types may require you to configure a Google Cloud project. For
|
||||
more information, including other authentication methods, see
|
||||
[Gemini CLI Authentication Setup](./authentication.mdx).
|
||||
[Gemini CLI Authentication Setup](./authentication.md).
|
||||
|
||||
## Configure
|
||||
|
||||
|
||||
@@ -0,0 +1,181 @@
|
||||
# Gemini CLI installation, execution, and releases
|
||||
|
||||
This document provides an overview of Gemini CLI's system requirements,
|
||||
installation methods, and release types.
|
||||
|
||||
## Recommended system specifications
|
||||
|
||||
- **Operating System:**
|
||||
- macOS 15+
|
||||
- Windows 11 24H2+
|
||||
- Ubuntu 20.04+
|
||||
- **Hardware:**
|
||||
- "Casual" usage: 4GB+ RAM (short sessions, common tasks and edits)
|
||||
- "Power" usage: 16GB+ RAM (long sessions, large codebases, deep context)
|
||||
- **Runtime:** Node.js 20.0.0+
|
||||
- **Shell:** Bash, Zsh, or PowerShell
|
||||
- **Location:**
|
||||
[Gemini Code Assist supported locations](https://developers.google.com/gemini-code-assist/resources/available-locations#americas)
|
||||
- **Internet connection required**
|
||||
|
||||
## Install Gemini CLI
|
||||
|
||||
We recommend most users install Gemini CLI using one of the following
|
||||
installation methods:
|
||||
|
||||
- npm
|
||||
- Homebrew
|
||||
- MacPorts
|
||||
- Anaconda
|
||||
|
||||
Note that Gemini CLI comes pre-installed on
|
||||
[**Cloud Shell**](https://docs.cloud.google.com/shell/docs) and
|
||||
[**Cloud Workstations**](https://cloud.google.com/workstations).
|
||||
|
||||
### Install globally with npm
|
||||
|
||||
```bash
|
||||
npm install -g @google/gemini-cli
|
||||
```
|
||||
|
||||
### Install globally with Homebrew (macOS/Linux)
|
||||
|
||||
```bash
|
||||
brew install gemini-cli
|
||||
```
|
||||
|
||||
### Install globally with MacPorts (macOS)
|
||||
|
||||
```bash
|
||||
sudo port install gemini-cli
|
||||
```
|
||||
|
||||
### Install with Anaconda (for restricted environments)
|
||||
|
||||
```bash
|
||||
# Create and activate a new environment
|
||||
conda create -y -n gemini_env -c conda-forge nodejs
|
||||
conda activate gemini_env
|
||||
|
||||
# Install Gemini CLI globally via npm (inside the environment)
|
||||
npm install -g @google/gemini-cli
|
||||
```
|
||||
|
||||
## Run Gemini CLI
|
||||
|
||||
For most users, we recommend running Gemini CLI with the `gemini` command:
|
||||
|
||||
```bash
|
||||
gemini
|
||||
```
|
||||
|
||||
For a list of options and additional commands, see the
|
||||
[CLI cheatsheet](../cli/cli-reference.md).
|
||||
|
||||
You can also run Gemini CLI using one of the following advanced methods:
|
||||
|
||||
- Run instantly with npx. You can run Gemini CLI without permanent installation.
|
||||
- In a sandbox. This method offers increased security and isolation.
|
||||
- From the source. This is recommended for contributors to the project.
|
||||
|
||||
### Run instantly with npx
|
||||
|
||||
```bash
|
||||
# Using npx (no installation required)
|
||||
npx @google/gemini-cli
|
||||
```
|
||||
|
||||
You can also execute the CLI directly from the main branch on GitHub, which is
|
||||
helpful for testing features still in development:
|
||||
|
||||
```bash
|
||||
npx https://github.com/google-gemini/gemini-cli
|
||||
```
|
||||
|
||||
### Run in a sandbox (Docker/Podman)
|
||||
|
||||
For security and isolation, Gemini CLI can be run inside a container. This is
|
||||
the default way that the CLI executes tools that might have side effects.
|
||||
|
||||
- **Directly from the registry:** You can run the published sandbox image
|
||||
directly. This is useful for environments where you only have Docker and want
|
||||
to run the CLI.
|
||||
```bash
|
||||
# Run the published sandbox image
|
||||
docker run --rm -it us-docker.pkg.dev/gemini-code-dev/gemini-cli/sandbox:0.1.1
|
||||
```
|
||||
- **Using the `--sandbox` flag:** If you have Gemini CLI installed locally
|
||||
(using the standard installation described above), you can instruct it to run
|
||||
inside the sandbox container.
|
||||
```bash
|
||||
gemini --sandbox -y -p "your prompt here"
|
||||
```
|
||||
|
||||
### Run from source (recommended for Gemini CLI contributors)
|
||||
|
||||
Contributors to the project will want to run the CLI directly from the source
|
||||
code.
|
||||
|
||||
- **Development mode:** This method provides hot-reloading and is useful for
|
||||
active development.
|
||||
```bash
|
||||
# From the root of the repository
|
||||
npm run start
|
||||
```
|
||||
- **Production mode (React optimizations):** This method runs the CLI with React
|
||||
production mode enabled, which is useful for testing performance without
|
||||
development overhead.
|
||||
```bash
|
||||
# From the root of the repository
|
||||
npm run start:prod
|
||||
```
|
||||
- **Production-like mode (linked package):** This method simulates a global
|
||||
installation by linking your local package. It's useful for testing a local
|
||||
build in a production workflow.
|
||||
|
||||
```bash
|
||||
# Link the local cli package to your global node_modules
|
||||
npm link packages/cli
|
||||
|
||||
# Now you can run your local version using the `gemini` command
|
||||
gemini
|
||||
```
|
||||
|
||||
## Releases
|
||||
|
||||
Gemini CLI has three release channels: nightly, preview, and stable. For most
|
||||
users, we recommend the stable release, which is the default installation.
|
||||
|
||||
### Stable
|
||||
|
||||
New stable releases are published each week. The stable release is the promotion
|
||||
of last week's `preview` release along with any bug fixes. The stable release
|
||||
uses `latest` tag, but omitting the tag also installs the latest stable release
|
||||
by default:
|
||||
|
||||
```bash
|
||||
# Both commands install the latest stable release.
|
||||
npm install -g @google/gemini-cli
|
||||
npm install -g @google/gemini-cli@latest
|
||||
```
|
||||
|
||||
### Preview
|
||||
|
||||
New preview releases will be published each week. These releases are not fully
|
||||
vetted and may contain regressions or other outstanding issues. Try out the
|
||||
preview release by using the `preview` tag:
|
||||
|
||||
```bash
|
||||
npm install -g @google/gemini-cli@preview
|
||||
```
|
||||
|
||||
### Nightly
|
||||
|
||||
Nightly releases are published every day. The nightly release includes all
|
||||
changes from the main branch at time of release. It should be assumed there are
|
||||
pending validations and issues. You can help test the latest changes by
|
||||
installing with the `nightly` tag:
|
||||
|
||||
```bash
|
||||
npm install -g @google/gemini-cli@nightly
|
||||
```
|
||||
@@ -1,201 +0,0 @@
|
||||
import { Tabs, TabItem } from '@astrojs/starlight/components';
|
||||
|
||||
# Gemini CLI installation, execution, and releases
|
||||
|
||||
This document provides an overview of Gemini CLI's system requirements,
|
||||
installation methods, and release types.
|
||||
|
||||
## Recommended system specifications
|
||||
|
||||
- **Operating System:**
|
||||
- macOS 15+
|
||||
- Windows 11 24H2+
|
||||
- Ubuntu 20.04+
|
||||
- **Hardware:**
|
||||
- "Casual" usage: 4GB+ RAM (short sessions, common tasks and edits)
|
||||
- "Power" usage: 16GB+ RAM (long sessions, large codebases, deep context)
|
||||
- **Runtime:** Node.js 20.0.0+
|
||||
- **Shell:** Bash, Zsh, or PowerShell
|
||||
- **Location:**
|
||||
[Gemini Code Assist supported locations](https://developers.google.com/gemini-code-assist/resources/available-locations#americas)
|
||||
- **Internet connection required**
|
||||
|
||||
## Install Gemini CLI
|
||||
|
||||
We recommend most users install Gemini CLI using one of the following
|
||||
installation methods. Note that Gemini CLI comes pre-installed on
|
||||
[**Cloud Shell**](https://docs.cloud.google.com/shell/docs) and
|
||||
[**Cloud Workstations**](https://cloud.google.com/workstations).
|
||||
|
||||
<Tabs>
|
||||
<TabItem label="npm">
|
||||
|
||||
Install globally with npm:
|
||||
|
||||
```bash
|
||||
npm install -g @google/gemini-cli
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
<TabItem label="Homebrew (macOS/Linux)">
|
||||
|
||||
Install globally with Homebrew:
|
||||
|
||||
```bash
|
||||
brew install gemini-cli
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
<TabItem label="MacPorts (macOS)">
|
||||
|
||||
Install globally with MacPorts:
|
||||
|
||||
```bash
|
||||
sudo port install gemini-cli
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
<TabItem label="Anaconda">
|
||||
|
||||
Install with Anaconda (for restricted environments):
|
||||
|
||||
```bash
|
||||
# Create and activate a new environment
|
||||
conda create -y -n gemini_env -c conda-forge nodejs
|
||||
conda activate gemini_env
|
||||
|
||||
# Install Gemini CLI globally via npm (inside the environment)
|
||||
npm install -g @google/gemini-cli
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
## Run Gemini CLI
|
||||
|
||||
For most users, we recommend running Gemini CLI with the `gemini` command:
|
||||
|
||||
```bash
|
||||
gemini
|
||||
```
|
||||
|
||||
For a list of options and additional commands, see the
|
||||
[CLI cheatsheet](../cli/cli-reference.md).
|
||||
|
||||
You can also run Gemini CLI using one of the following advanced methods:
|
||||
|
||||
<Tabs>
|
||||
<TabItem label="npx">
|
||||
|
||||
Run instantly with npx. You can run Gemini CLI without permanent installation.
|
||||
|
||||
```bash
|
||||
# Using npx (no installation required)
|
||||
npx @google/gemini-cli
|
||||
```
|
||||
|
||||
You can also execute the CLI directly from the main branch on GitHub, which is
|
||||
helpful for testing features still in development:
|
||||
|
||||
```bash
|
||||
npx https://github.com/google-gemini/gemini-cli
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
<TabItem label="Docker/Podman Sandbox">
|
||||
|
||||
For security and isolation, Gemini CLI can be run inside a container. This is
|
||||
the default way that the CLI executes tools that might have side effects.
|
||||
|
||||
- **Directly from the registry:** You can run the published sandbox image
|
||||
directly. This is useful for environments where you only have Docker and want
|
||||
to run the CLI.
|
||||
```bash
|
||||
# Run the published sandbox image
|
||||
docker run --rm -it us-docker.pkg.dev/gemini-code-dev/gemini-cli/sandbox:0.1.1
|
||||
```
|
||||
- **Using the `--sandbox` flag:** If you have Gemini CLI installed locally
|
||||
(using the standard installation described above), you can instruct it to run
|
||||
inside the sandbox container.
|
||||
```bash
|
||||
gemini --sandbox -y -p "your prompt here"
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
<TabItem label="From source">
|
||||
|
||||
Contributors to the project will want to run the CLI directly from the source
|
||||
code.
|
||||
|
||||
- **Development mode:** This method provides hot-reloading and is useful for
|
||||
active development.
|
||||
```bash
|
||||
# From the root of the repository
|
||||
npm run start
|
||||
```
|
||||
- **Production mode (React optimizations):** This method runs the CLI with React
|
||||
production mode enabled, which is useful for testing performance without
|
||||
development overhead.
|
||||
```bash
|
||||
# From the root of the repository
|
||||
npm run start:prod
|
||||
```
|
||||
- **Production-like mode (linked package):** This method simulates a global
|
||||
installation by linking your local package. It's useful for testing a local
|
||||
build in a production workflow.
|
||||
|
||||
```bash
|
||||
# Link the local cli package to your global node_modules
|
||||
npm link packages/cli
|
||||
|
||||
# Now you can run your local version using the `gemini` command
|
||||
gemini
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
## Releases
|
||||
|
||||
Gemini CLI has three release channels: stable, preview, and nightly. For most
|
||||
users, we recommend the stable release, which is the default installation.
|
||||
|
||||
<Tabs>
|
||||
<TabItem label="Stable">
|
||||
|
||||
Stable releases are published each week. A stable release is created from the
|
||||
previous week's preview release along with any bug fixes. The stable release
|
||||
uses the `latest` tag. Omitting the tag also installs the latest stable
|
||||
release by default.
|
||||
|
||||
```bash
|
||||
# Both commands install the latest stable release.
|
||||
npm install -g @google/gemini-cli
|
||||
npm install -g @google/gemini-cli@latest
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
<TabItem label="Preview">
|
||||
|
||||
New preview releases will be published each week. These releases are not fully
|
||||
vetted and may contain regressions or other outstanding issues. Try out the
|
||||
preview release by using the `preview` tag:
|
||||
|
||||
```bash
|
||||
npm install -g @google/gemini-cli@preview
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
<TabItem label="Nightly">
|
||||
|
||||
Nightly releases are published every day. The nightly release includes all
|
||||
changes from the main branch at time of release. It should be assumed there are
|
||||
pending validations and issues. You can help test the latest changes by
|
||||
installing with the `nightly` tag:
|
||||
|
||||
```bash
|
||||
npm install -g @google/gemini-cli@nightly
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
+2
-2
@@ -15,9 +15,9 @@ npm install -g @google/gemini-cli
|
||||
Jump in to Gemini CLI.
|
||||
|
||||
- **[Quickstart](./get-started/index.md):** Your first session with Gemini CLI.
|
||||
- **[Installation](./get-started/installation.mdx):** How to install Gemini CLI
|
||||
- **[Installation](./get-started/installation.md):** How to install Gemini CLI
|
||||
on your system.
|
||||
- **[Authentication](./get-started/authentication.mdx):** Setup instructions for
|
||||
- **[Authentication](./get-started/authentication.md):** Setup instructions for
|
||||
personal and enterprise accounts.
|
||||
- **[CLI cheatsheet](./cli/cli-reference.md):** A quick reference for common
|
||||
commands and options.
|
||||
|
||||
@@ -198,11 +198,6 @@ their corresponding top-level category object in your `settings.json` file.
|
||||
- **Description:** Minimum retention period (safety limit, defaults to "1d")
|
||||
- **Default:** `"1d"`
|
||||
|
||||
- **`general.topicUpdateNarration`** (boolean):
|
||||
- **Description:** Enable the Topic & Update communication model for reduced
|
||||
chattiness and structured progress reporting.
|
||||
- **Default:** `true`
|
||||
|
||||
#### `output`
|
||||
|
||||
- **`output.format`** (enum):
|
||||
@@ -436,20 +431,6 @@ their corresponding top-level category object in your `settings.json` file.
|
||||
- **Default:** `"ask"`
|
||||
- **Values:** `"ask"`, `"always"`, `"never"`
|
||||
|
||||
- **`billing.vertexAi.requestType`** (enum):
|
||||
- **Description:** Sets the X-Vertex-AI-LLM-Request-Type header for Vertex AI
|
||||
requests.
|
||||
- **Default:** `undefined`
|
||||
- **Values:** `"dedicated"`, `"shared"`
|
||||
- **Requires restart:** Yes
|
||||
|
||||
- **`billing.vertexAi.sharedRequestType`** (enum):
|
||||
- **Description:** Sets the X-Vertex-AI-LLM-Shared-Request-Type header for
|
||||
Vertex AI requests.
|
||||
- **Default:** `undefined`
|
||||
- **Values:** `"priority"`, `"flex"`
|
||||
- **Requires restart:** Yes
|
||||
|
||||
#### `model`
|
||||
|
||||
- **`model.name`** (string):
|
||||
@@ -1373,12 +1354,6 @@ their corresponding top-level category object in your `settings.json` file.
|
||||
- **Default:** `true`
|
||||
- **Requires restart:** Yes
|
||||
|
||||
- **`context.fileFiltering.enableFileWatcher`** (boolean):
|
||||
- **Description:** Enable file watcher updates for @ file suggestions
|
||||
(experimental).
|
||||
- **Default:** `false`
|
||||
- **Requires restart:** Yes
|
||||
|
||||
- **`context.fileFiltering.enableRecursiveFileSearch`** (boolean):
|
||||
- **Description:** Enable recursive file search functionality when completing
|
||||
@ references in the prompt.
|
||||
@@ -1731,18 +1706,6 @@ their corresponding top-level category object in your `settings.json` file.
|
||||
- **Default:** `false`
|
||||
- **Requires restart:** Yes
|
||||
|
||||
- **`experimental.gemmaModelRouter.autoStartServer`** (boolean):
|
||||
- **Description:** Automatically start the LiteRT-LM server when Gemini CLI
|
||||
starts and the Gemma router is enabled.
|
||||
- **Default:** `false`
|
||||
- **Requires restart:** Yes
|
||||
|
||||
- **`experimental.gemmaModelRouter.binaryPath`** (string):
|
||||
- **Description:** Custom path to the LiteRT-LM binary. Leave empty to use the
|
||||
default location (~/.gemini/bin/litert/).
|
||||
- **Default:** `""`
|
||||
- **Requires restart:** Yes
|
||||
|
||||
- **`experimental.gemmaModelRouter.classifier.host`** (string):
|
||||
- **Description:** The host of the classifier.
|
||||
- **Default:** `"http://localhost:9379"`
|
||||
@@ -1761,12 +1724,6 @@ their corresponding top-level category object in your `settings.json` file.
|
||||
- **Default:** `false`
|
||||
- **Requires restart:** Yes
|
||||
|
||||
- **`experimental.autoMemory`** (boolean):
|
||||
- **Description:** Automatically extract reusable skills from past sessions in
|
||||
the background. Review results with /memory inbox.
|
||||
- **Default:** `false`
|
||||
- **Requires restart:** Yes
|
||||
|
||||
- **`experimental.generalistProfile`** (boolean):
|
||||
- **Description:** Suitable for general coding and software development tasks.
|
||||
- **Default:** `false`
|
||||
@@ -1778,7 +1735,8 @@ their corresponding top-level category object in your `settings.json` file.
|
||||
- **Requires restart:** Yes
|
||||
|
||||
- **`experimental.topicUpdateNarration`** (boolean):
|
||||
- **Description:** Deprecated: Use general.topicUpdateNarration instead.
|
||||
- **Description:** Enable the experimental Topic & Update communication model
|
||||
for reduced chattiness and structured progress reporting.
|
||||
- **Default:** `false`
|
||||
|
||||
#### `skills`
|
||||
@@ -2018,8 +1976,6 @@ see [Telemetry](../cli/telemetry.md).
|
||||
|
||||
- **Properties:**
|
||||
- **`enabled`** (boolean): Whether or not telemetry is enabled.
|
||||
- **`traces`** (boolean): Whether detailed traces with large attributes (like
|
||||
tool outputs and file reads) are captured. Defaults to `false`.
|
||||
- **`target`** (string): The destination for collected telemetry. Supported
|
||||
values are `local` and `gcp`.
|
||||
- **`otlpEndpoint`** (string): The endpoint for the OTLP Exporter.
|
||||
@@ -2119,7 +2075,7 @@ within your user's home folder.
|
||||
Environment variables are a common way to configure applications, especially for
|
||||
sensitive information like API keys or for settings that might change between
|
||||
environments. For authentication setup, see the
|
||||
[Authentication documentation](../get-started/authentication.mdx) which covers
|
||||
[Authentication documentation](../get-started/authentication.md) which covers
|
||||
all available authentication methods.
|
||||
|
||||
The CLI automatically loads environment variables from an `.env` file. The
|
||||
@@ -2140,7 +2096,7 @@ the `advanced.excludedEnvVars` setting in your `settings.json` file.
|
||||
- **`GEMINI_API_KEY`**:
|
||||
- Your API key for the Gemini API.
|
||||
- One of several available
|
||||
[authentication methods](../get-started/authentication.mdx).
|
||||
[authentication methods](../get-started/authentication.md).
|
||||
- Set this in your shell profile (for example, `~/.bashrc`, `~/.zshrc`) or an
|
||||
`.env` file.
|
||||
- **`GEMINI_MODEL`**:
|
||||
@@ -2220,10 +2176,6 @@ the `advanced.excludedEnvVars` setting in your `settings.json` file.
|
||||
- Set to `true` or `1` to enable telemetry. Any other value is treated as
|
||||
disabling it.
|
||||
- Overrides the `telemetry.enabled` setting.
|
||||
- **`GEMINI_TELEMETRY_TRACES_ENABLED`**:
|
||||
- Set to `true` or `1` to enable detailed tracing with large attributes. Any
|
||||
other value is treated as disabling it.
|
||||
- Overrides the `telemetry.traces` setting.
|
||||
- **`GEMINI_TELEMETRY_TARGET`**:
|
||||
- Sets the telemetry target (`local` or `gcp`).
|
||||
- Overrides the `telemetry.target` setting.
|
||||
|
||||
@@ -92,21 +92,6 @@ each tool.
|
||||
| [`ask_user`](../tools/ask-user.md) | `Communicate` | Requests clarification or missing information via an interactive dialog. |
|
||||
| [`write_todos`](../tools/todos.md) | `Other` | Maintains an internal list of subtasks. The model uses this to track its own progress. |
|
||||
|
||||
### Task Tracker (Experimental)
|
||||
|
||||
<!-- prettier-ignore -->
|
||||
> [!NOTE]
|
||||
> This is an experimental feature currently under active development. Enable via `experimental.taskTracker`.
|
||||
|
||||
| Tool | Kind | Description |
|
||||
| :---------------------------------------------- | :------ | :-------------------------------------------------------------------------- |
|
||||
| [`tracker_create_task`](../tools/tracker.md) | `Other` | Creates a new task in the experimental tracker. |
|
||||
| [`tracker_update_task`](../tools/tracker.md) | `Other` | Updates an existing task's status, description, or dependencies. |
|
||||
| [`tracker_get_task`](../tools/tracker.md) | `Other` | Retrieves the full details of a specific task. |
|
||||
| [`tracker_list_tasks`](../tools/tracker.md) | `Other` | Lists tasks in the tracker, optionally filtered by status, type, or parent. |
|
||||
| [`tracker_add_dependency`](../tools/tracker.md) | `Other` | Adds a dependency between two tasks, ensuring topological execution. |
|
||||
| [`tracker_visualize`](../tools/tracker.md) | `Other` | Renders an ASCII tree visualization of the current task graph. |
|
||||
|
||||
### MCP
|
||||
|
||||
| Tool | Kind | Description |
|
||||
|
||||
@@ -96,11 +96,6 @@
|
||||
]
|
||||
},
|
||||
{ "label": "Agent Skills", "slug": "docs/cli/skills" },
|
||||
{
|
||||
"label": "Auto Memory",
|
||||
"badge": "🔬",
|
||||
"slug": "docs/cli/auto-memory"
|
||||
},
|
||||
{ "label": "Checkpointing", "slug": "docs/cli/checkpointing" },
|
||||
{ "label": "Headless mode", "slug": "docs/cli/headless" },
|
||||
{
|
||||
|
||||
@@ -1,61 +0,0 @@
|
||||
# Tracker tools (`tracker_*`)
|
||||
|
||||
<!-- prettier-ignore -->
|
||||
> [!NOTE]
|
||||
> This is an experimental feature currently under active development.
|
||||
|
||||
The `tracker_*` tools allow the Gemini agent to maintain an internal, persistent
|
||||
graph of tasks and dependencies for multi-step requests. This suite of tools
|
||||
provides a more robust and granular way to manage execution plans than the
|
||||
legacy `write_todos` tool.
|
||||
|
||||
## Technical reference
|
||||
|
||||
The agent uses these tools to manage its execution plan, decompose complex goals
|
||||
into actionable sub-tasks, and provide real-time progress updates to the CLI
|
||||
interface. The task state is stored in the `.gemini/tmp/tracker/<session-id>`
|
||||
directory, allowing the agent to manage its plan for the current session.
|
||||
|
||||
### Available Tools
|
||||
|
||||
- `tracker_create_task`: Creates a new task in the tracker. You can specify a
|
||||
title, description, and task type (`epic`, `task`, `bug`).
|
||||
- `tracker_update_task`: Updates an existing task's status (`open`,
|
||||
`in_progress`, `blocked`, `closed`), description, or dependencies.
|
||||
- `tracker_get_task`: Retrieves the full details of a specific task by its
|
||||
6-character hex ID.
|
||||
- `tracker_list_tasks`: Lists tasks in the tracker, optionally filtered by
|
||||
status, type, or parent ID.
|
||||
- `tracker_add_dependency`: Adds a dependency between two tasks, ensuring
|
||||
topological execution.
|
||||
- `tracker_visualize`: Renders an ASCII tree visualization of the current task
|
||||
graph.
|
||||
|
||||
## Technical behavior
|
||||
|
||||
- **Interface:** Updates the progress indicator and task tree above the CLI
|
||||
input prompt.
|
||||
- **Persistence:** Task state is saved automatically to the
|
||||
`.gemini/tmp/tracker/<session-id>` directory. Task states are session-specific
|
||||
and do not persist across different sessions.
|
||||
- **Dependencies:** Tasks can depend on other tasks, forming a directed acyclic
|
||||
graph (DAG). The agent must resolve dependencies before starting blocked
|
||||
tasks.
|
||||
- **Interaction:** Users can view the current state of the tracker by asking the
|
||||
agent to visualize it, or by running `gemini-cli` commands if implemented.
|
||||
|
||||
## Use cases
|
||||
|
||||
- Coordinating multi-file refactoring projects.
|
||||
- Breaking down a mission into a hierarchy of epics and tasks for better
|
||||
visibility.
|
||||
- Tracking bugs and feature requests directly within the context of an active
|
||||
codebase.
|
||||
- Providing visibility into the agent's current focus and remaining work.
|
||||
|
||||
## Next steps
|
||||
|
||||
- Follow the [Task planning tutorial](../cli/tutorials/task-planning.md) for
|
||||
usage details and migration from the legacy todo list.
|
||||
- Learn about [Session management](../cli/session-management.md) for context on
|
||||
persistent state.
|
||||
+1
-45
@@ -305,7 +305,7 @@ describe('plan_mode', () => {
|
||||
settings,
|
||||
},
|
||||
prompt:
|
||||
'I agree with your strategy. Please enter plan mode and draft the plan to create a new module called foo. The plan should be saved as foo-plan.md. Then, exit plan mode.',
|
||||
'Enter plan mode and plan to create a new module called foo. The plan should be saved as foo-plan.md. Then, exit plan mode.',
|
||||
assert: async (rig, result) => {
|
||||
const enterPlanCalled = await rig.waitForToolCall('enter_plan_mode');
|
||||
expect(
|
||||
@@ -376,48 +376,4 @@ describe('plan_mode', () => {
|
||||
assertModelHasOutput(result);
|
||||
},
|
||||
});
|
||||
|
||||
evalTest('USUALLY_PASSES', {
|
||||
name: 'should handle nested plan directories correctly',
|
||||
suiteName: 'plan_mode',
|
||||
suiteType: 'behavioral',
|
||||
approvalMode: ApprovalMode.PLAN,
|
||||
params: {
|
||||
settings,
|
||||
},
|
||||
prompt:
|
||||
'Please create a new architectural plan in a nested folder called "architecture/frontend-v2.md" within the plans directory. The plan should contain the text "# Frontend V2 Plan". Then, exit plan mode',
|
||||
assert: async (rig, result) => {
|
||||
await rig.waitForTelemetryReady();
|
||||
const toolLogs = rig.readToolLogs();
|
||||
|
||||
const writeCalls = toolLogs.filter((log) =>
|
||||
['write_file', 'replace'].includes(log.toolRequest.name),
|
||||
);
|
||||
|
||||
const wroteToNestedPath = writeCalls.some((log) => {
|
||||
try {
|
||||
const args = JSON.parse(log.toolRequest.args);
|
||||
if (!args.file_path) return false;
|
||||
// In plan mode, paths can be passed as relative (architecture/frontend-v2.md)
|
||||
// or they might be resolved as absolute by the tool depending on the exact mock state.
|
||||
// We strictly ensure it ends exactly with the expected nested path and doesn't contain extra nesting.
|
||||
const normalizedPath = args.file_path.replace(/\\/g, '/');
|
||||
return (
|
||||
normalizedPath === 'architecture/frontend-v2.md' ||
|
||||
normalizedPath.endsWith('/plans/architecture/frontend-v2.md')
|
||||
);
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
});
|
||||
|
||||
expect(
|
||||
wroteToNestedPath,
|
||||
'Expected model to successfully target the nested plan file path',
|
||||
).toBe(true);
|
||||
|
||||
assertModelHasOutput(result);
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
@@ -149,12 +149,12 @@ async function readSkillBodies(skillsDir: string): Promise<string[]> {
|
||||
|
||||
/**
|
||||
* Shared configOverrides for all skill extraction component evals.
|
||||
* - experimentalAutoMemory: enables the Auto Memory skill extraction pipeline.
|
||||
* - experimentalMemoryManager: enables the memory extraction pipeline.
|
||||
* - approvalMode: YOLO auto-approves tool calls (write_file, read_file) so the
|
||||
* background agent can execute without interactive confirmation.
|
||||
*/
|
||||
const EXTRACTION_CONFIG_OVERRIDES = {
|
||||
experimentalAutoMemory: true,
|
||||
experimentalMemoryManager: true,
|
||||
approvalMode: ApprovalMode.YOLO,
|
||||
};
|
||||
|
||||
|
||||
@@ -43,7 +43,7 @@ describe('update_topic_behavior', () => {
|
||||
2,
|
||||
),
|
||||
'.gemini/settings.json': JSON.stringify({
|
||||
general: {
|
||||
experimental: {
|
||||
topicUpdateNarration: true,
|
||||
},
|
||||
}),
|
||||
@@ -127,7 +127,7 @@ describe('update_topic_behavior', () => {
|
||||
'Explain the difference between Map and Object in JavaScript and provide a performance-focused code snippet for each.',
|
||||
files: {
|
||||
'.gemini/settings.json': JSON.stringify({
|
||||
general: {
|
||||
experimental: {
|
||||
topicUpdateNarration: true,
|
||||
},
|
||||
}),
|
||||
@@ -156,7 +156,7 @@ describe('update_topic_behavior', () => {
|
||||
'packages/core/src/tools/tool-names.ts':
|
||||
"export const UPDATE_TOPIC_TOOL_NAME = 'update_topic';",
|
||||
'.gemini/settings.json': JSON.stringify({
|
||||
general: {
|
||||
experimental: {
|
||||
topicUpdateNarration: true,
|
||||
},
|
||||
}),
|
||||
@@ -204,7 +204,7 @@ app.post('/users', (req, res) => {
|
||||
export default app;
|
||||
`,
|
||||
'.gemini/settings.json': JSON.stringify({
|
||||
general: {
|
||||
experimental: {
|
||||
topicUpdateNarration: true,
|
||||
},
|
||||
}),
|
||||
@@ -249,7 +249,7 @@ export default app;
|
||||
files: {
|
||||
'package.json': JSON.stringify({ name: 'test-project' }),
|
||||
'.gemini/settings.json': JSON.stringify({
|
||||
general: {
|
||||
experimental: {
|
||||
topicUpdateNarration: true,
|
||||
},
|
||||
}),
|
||||
|
||||
@@ -70,7 +70,6 @@ describe('ACP telemetry', () => {
|
||||
GEMINI_API_KEY: 'fake-key',
|
||||
GEMINI_CLI_HOME: rig.homeDir!,
|
||||
GEMINI_TELEMETRY_ENABLED: 'true',
|
||||
GEMINI_TELEMETRY_TRACES_ENABLED: 'true',
|
||||
GEMINI_TELEMETRY_TARGET: 'local',
|
||||
GEMINI_TELEMETRY_OUTFILE: telemetryPath,
|
||||
},
|
||||
|
||||
@@ -8,16 +8,7 @@ import { expect, describe, it, beforeEach, afterEach } from 'vitest';
|
||||
import { TestRig } from './test-helper.js';
|
||||
import { join } from 'node:path';
|
||||
|
||||
// Skip on macOS: every interactive test in this file is chronically flaky
|
||||
// because the captured pty buffer contains the CLI's startup escape
|
||||
// sequences (`q4;?m...true color warning`) instead of the streamed output,
|
||||
// causing `expectText(...)` to time out. Reproducible across unrelated
|
||||
// runs on `main` (24740161950, 24739323404) and on consecutive merge-queue
|
||||
// gates for #25753 (24743605639, 24747624513) — different tests in the
|
||||
// same describe fail on different runs. Not specific to any model.
|
||||
const skipOnDarwin = process.platform === 'darwin';
|
||||
|
||||
describe.skipIf(skipOnDarwin)('Interactive Mode', () => {
|
||||
describe('Interactive Mode', () => {
|
||||
let rig: TestRig;
|
||||
|
||||
beforeEach(() => {
|
||||
|
||||
@@ -134,9 +134,7 @@ describe('file-system', () => {
|
||||
).toBeTruthy();
|
||||
|
||||
const newFileContent = rig.readFile(fileName);
|
||||
// Trim to tolerate models that idiomatically append a trailing newline.
|
||||
// This test is about path-with-spaces handling, not whitespace fidelity.
|
||||
expect(newFileContent.trim()).toBe('hello');
|
||||
expect(newFileContent).toBe('hello');
|
||||
});
|
||||
|
||||
it('should perform a read-then-write sequence', async () => {
|
||||
|
||||
@@ -81,10 +81,7 @@ describe('Plan Mode', () => {
|
||||
|
||||
await rig.run({
|
||||
approvalMode: 'plan',
|
||||
args:
|
||||
'Create a file called plan.md in the plans directory with the ' +
|
||||
'content "# Plan". Treat this as a Directive and write the file ' +
|
||||
'immediately without proposing strategy or asking for confirmation.',
|
||||
args: 'Create a file called plan.md in the plans directory.',
|
||||
});
|
||||
|
||||
const toolLogs = rig.readToolLogs();
|
||||
@@ -197,11 +194,7 @@ describe('Plan Mode', () => {
|
||||
|
||||
await rig.run({
|
||||
approvalMode: 'plan',
|
||||
args:
|
||||
'Create a file called plan-no-session.md in the plans directory ' +
|
||||
'with the content "# Plan". Treat this as a Directive and write ' +
|
||||
'the file immediately without proposing strategy or asking for ' +
|
||||
'confirmation.',
|
||||
args: 'Create a file called plan-no-session.md in the plans directory.',
|
||||
});
|
||||
|
||||
const toolLogs = rig.readToolLogs();
|
||||
|
||||
+36
-36
@@ -1,55 +1,55 @@
|
||||
{
|
||||
"version": 1,
|
||||
"updatedAt": "2026-04-20T18:04:59.671Z",
|
||||
"updatedAt": "2026-04-10T15:36:04.547Z",
|
||||
"scenarios": {
|
||||
"multi-turn-conversation": {
|
||||
"heapUsedMB": 68.8,
|
||||
"heapTotalMB": 91.2,
|
||||
"rssMB": 215.4,
|
||||
"externalMB": 93.8,
|
||||
"timestamp": "2026-04-20T18:02:40.101Z"
|
||||
"heapUsedBytes": 120082704,
|
||||
"heapTotalBytes": 177586176,
|
||||
"rssBytes": 269172736,
|
||||
"externalBytes": 4304053,
|
||||
"timestamp": "2026-04-10T15:35:17.603Z"
|
||||
},
|
||||
"multi-function-call-repo-search": {
|
||||
"heapUsedMB": 73.5,
|
||||
"heapTotalMB": 93.1,
|
||||
"rssMB": 223.6,
|
||||
"externalMB": 97.7,
|
||||
"timestamp": "2026-04-20T18:02:42.032Z"
|
||||
"heapUsedBytes": 104644984,
|
||||
"heapTotalBytes": 111575040,
|
||||
"rssBytes": 204079104,
|
||||
"externalBytes": 4304053,
|
||||
"timestamp": "2026-04-10T15:35:22.480Z"
|
||||
},
|
||||
"idle-session-startup": {
|
||||
"heapUsedMB": 69.8,
|
||||
"heapTotalMB": 92.4,
|
||||
"rssMB": 217.4,
|
||||
"externalMB": 93.8,
|
||||
"timestamp": "2026-04-20T18:02:36.294Z"
|
||||
"heapUsedBytes": 119813672,
|
||||
"heapTotalBytes": 177061888,
|
||||
"rssBytes": 267943936,
|
||||
"externalBytes": 4304053,
|
||||
"timestamp": "2026-04-10T15:35:08.035Z"
|
||||
},
|
||||
"simple-prompt-response": {
|
||||
"heapUsedMB": 69.5,
|
||||
"heapTotalMB": 92.4,
|
||||
"rssMB": 216.1,
|
||||
"externalMB": 93.8,
|
||||
"timestamp": "2026-04-20T18:02:38.198Z"
|
||||
"heapUsedBytes": 119722064,
|
||||
"heapTotalBytes": 177324032,
|
||||
"rssBytes": 268812288,
|
||||
"externalBytes": 4304053,
|
||||
"timestamp": "2026-04-10T15:35:12.770Z"
|
||||
},
|
||||
"resume-large-chat-with-messages": {
|
||||
"heapUsedMB": 887.1,
|
||||
"heapTotalMB": 954.3,
|
||||
"rssMB": 1109.6,
|
||||
"externalMB": 103.2,
|
||||
"timestamp": "2026-04-20T18:04:59.671Z"
|
||||
"heapUsedBytes": 106545568,
|
||||
"heapTotalBytes": 111509504,
|
||||
"rssBytes": 202596352,
|
||||
"externalBytes": 4306101,
|
||||
"timestamp": "2026-04-10T15:36:04.547Z"
|
||||
},
|
||||
"resume-large-chat": {
|
||||
"heapUsedMB": 885.6,
|
||||
"heapTotalMB": 955.6,
|
||||
"rssMB": 1107.8,
|
||||
"externalMB": 110.5,
|
||||
"timestamp": "2026-04-20T18:04:06.526Z"
|
||||
"heapUsedBytes": 106513760,
|
||||
"heapTotalBytes": 111509504,
|
||||
"rssBytes": 202596352,
|
||||
"externalBytes": 4306101,
|
||||
"timestamp": "2026-04-10T15:35:59.528Z"
|
||||
},
|
||||
"large-chat": {
|
||||
"heapUsedMB": 158.5,
|
||||
"heapTotalMB": 193,
|
||||
"rssMB": 787.9,
|
||||
"externalMB": 104,
|
||||
"timestamp": "2026-04-20T18:03:12.486Z"
|
||||
"heapUsedBytes": 106471568,
|
||||
"heapTotalBytes": 111509504,
|
||||
"rssBytes": 202596352,
|
||||
"externalBytes": 4306101,
|
||||
"timestamp": "2026-04-10T15:35:53.180Z"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,21 +16,15 @@ import {
|
||||
mkdirSync,
|
||||
rmSync,
|
||||
} from 'node:fs';
|
||||
import { randomUUID, createHash } from 'node:crypto';
|
||||
import { randomUUID } from 'node:crypto';
|
||||
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||
const BASELINES_PATH = join(__dirname, 'baselines.json');
|
||||
const UPDATE_BASELINES = process.env['UPDATE_MEMORY_BASELINES'] === 'true';
|
||||
function getProjectHash(projectRoot: string): string {
|
||||
return createHash('sha256').update(projectRoot).digest('hex');
|
||||
}
|
||||
const TOLERANCE_PERCENT = 10;
|
||||
|
||||
// Fake API key for tests using fake responses
|
||||
const TEST_ENV = {
|
||||
GEMINI_API_KEY: 'fake-memory-test-key',
|
||||
GEMINI_MEMORY_MONITOR_INTERVAL: '100',
|
||||
};
|
||||
const TEST_ENV = { GEMINI_API_KEY: 'fake-memory-test-key' };
|
||||
|
||||
describe('Memory Usage Tests', () => {
|
||||
let harness: MemoryTestHarness;
|
||||
@@ -62,7 +56,6 @@ describe('Memory Usage Tests', () => {
|
||||
});
|
||||
|
||||
const result = await harness.runScenario(
|
||||
rig,
|
||||
'idle-session-startup',
|
||||
async (recordSnapshot) => {
|
||||
await rig.run({
|
||||
@@ -92,7 +85,6 @@ describe('Memory Usage Tests', () => {
|
||||
});
|
||||
|
||||
const result = await harness.runScenario(
|
||||
rig,
|
||||
'simple-prompt-response',
|
||||
async (recordSnapshot) => {
|
||||
await rig.run({
|
||||
@@ -130,7 +122,6 @@ describe('Memory Usage Tests', () => {
|
||||
];
|
||||
|
||||
const result = await harness.runScenario(
|
||||
rig,
|
||||
'multi-turn-conversation',
|
||||
async (recordSnapshot) => {
|
||||
// Run through all turns as a piped sequence
|
||||
@@ -153,9 +144,6 @@ describe('Memory Usage Tests', () => {
|
||||
);
|
||||
} else {
|
||||
harness.assertWithinBaseline(result);
|
||||
harness.assertMemoryReturnsToBaseline(result.snapshots, 20);
|
||||
const { leaked, message } = harness.analyzeSnapshots(result.snapshots);
|
||||
if (leaked) console.warn(`⚠ ${message}`);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -180,7 +168,6 @@ describe('Memory Usage Tests', () => {
|
||||
);
|
||||
|
||||
const result = await harness.runScenario(
|
||||
rig,
|
||||
'multi-function-call-repo-search',
|
||||
async (recordSnapshot) => {
|
||||
await rig.run({
|
||||
@@ -202,7 +189,6 @@ describe('Memory Usage Tests', () => {
|
||||
);
|
||||
} else {
|
||||
harness.assertWithinBaseline(result);
|
||||
harness.assertMemoryReturnsToBaseline(result.snapshots, 20);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -242,7 +228,6 @@ describe('Memory Usage Tests', () => {
|
||||
});
|
||||
|
||||
const result = await harness.runScenario(
|
||||
rig,
|
||||
'large-chat',
|
||||
async (recordSnapshot) => {
|
||||
await rig.run({
|
||||
@@ -272,21 +257,19 @@ describe('Memory Usage Tests', () => {
|
||||
});
|
||||
|
||||
const result = await harness.runScenario(
|
||||
rig,
|
||||
'resume-large-chat',
|
||||
async (recordSnapshot) => {
|
||||
// Ensure the history file is linked
|
||||
const targetChatsDir = join(
|
||||
rig.homeDir!,
|
||||
'.gemini',
|
||||
rig.testDir!,
|
||||
'tmp',
|
||||
getProjectHash(rig.testDir!),
|
||||
'test-project-hash',
|
||||
'chats',
|
||||
);
|
||||
mkdirSync(targetChatsDir, { recursive: true });
|
||||
const targetHistoryPath = join(
|
||||
targetChatsDir,
|
||||
'session-large-chat.json',
|
||||
'large-chat-session.json',
|
||||
);
|
||||
if (existsSync(targetHistoryPath)) rmSync(targetHistoryPath);
|
||||
copyFileSync(sharedHistoryPath, targetHistoryPath);
|
||||
@@ -319,21 +302,19 @@ describe('Memory Usage Tests', () => {
|
||||
});
|
||||
|
||||
const result = await harness.runScenario(
|
||||
rig,
|
||||
'resume-large-chat-with-messages',
|
||||
async (recordSnapshot) => {
|
||||
// Ensure the history file is linked
|
||||
const targetChatsDir = join(
|
||||
rig.homeDir!,
|
||||
'.gemini',
|
||||
rig.testDir!,
|
||||
'tmp',
|
||||
getProjectHash(rig.testDir!),
|
||||
'test-project-hash',
|
||||
'chats',
|
||||
);
|
||||
mkdirSync(targetChatsDir, { recursive: true });
|
||||
const targetHistoryPath = join(
|
||||
targetChatsDir,
|
||||
'session-large-chat.json',
|
||||
'large-chat-session.json',
|
||||
);
|
||||
if (existsSync(targetHistoryPath)) rmSync(targetHistoryPath);
|
||||
copyFileSync(sharedHistoryPath, targetHistoryPath);
|
||||
@@ -476,9 +457,6 @@ async function generateSharedLargeChatData(tempDir: string) {
|
||||
// Generate responses for resumed chat
|
||||
const resumeResponsesStream = createWriteStream(resumeResponsesPath);
|
||||
for (let i = 0; i < 5; i++) {
|
||||
// Doubling up on non-streaming responses to satisfy classifier and complexity checks
|
||||
resumeResponsesStream.write(JSON.stringify(complexityResponse) + '\n');
|
||||
resumeResponsesStream.write(JSON.stringify(summaryResponse) + '\n');
|
||||
resumeResponsesStream.write(JSON.stringify(complexityResponse) + '\n');
|
||||
resumeResponsesStream.write(
|
||||
JSON.stringify({
|
||||
|
||||
Generated
+419
-71
@@ -449,8 +449,7 @@
|
||||
"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
|
||||
"license": "(Apache-2.0 AND BSD-3-Clause)"
|
||||
},
|
||||
"node_modules/@bundled-es-modules/cookie": {
|
||||
"version": "2.0.1",
|
||||
@@ -1474,7 +1473,6 @@
|
||||
"resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.13.4.tgz",
|
||||
"integrity": "sha512-GsFaMXCkMqkKIvwCQjCrwH+GHbPKBjhwo/8ZuUkWHqbI73Kky9I+pQltrlT0+MWpedCoosda53lgjYfyEPgxBg==",
|
||||
"license": "Apache-2.0",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@grpc/proto-loader": "^0.7.13",
|
||||
"@js-sdsl/ordered-map": "^4.4.2"
|
||||
@@ -2152,7 +2150,6 @@
|
||||
"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",
|
||||
@@ -2333,7 +2330,6 @@
|
||||
"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"
|
||||
}
|
||||
@@ -2383,7 +2379,6 @@
|
||||
"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"
|
||||
},
|
||||
@@ -2758,7 +2753,6 @@
|
||||
"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"
|
||||
@@ -2792,7 +2786,6 @@
|
||||
"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"
|
||||
@@ -2847,7 +2840,6 @@
|
||||
"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",
|
||||
@@ -4054,7 +4046,6 @@
|
||||
"integrity": "sha512-6mDvHUFSjyT2B2yeNx2nUgMxh9LtOWvkhIU3uePn2I2oyNymUAX1NIsdgviM4CH+JSrp2D2hsMvJOkxY+0wNRA==",
|
||||
"devOptional": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"csstype": "^3.0.2"
|
||||
}
|
||||
@@ -4328,7 +4319,6 @@
|
||||
"integrity": "sha512-/Zb/xaIDfxeJnvishjGdcR4jmr7S+bda8PKNhRGdljDM+elXhlvN0FyPSsMnLmJUrVG9aPO6dof80wjMawsASg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@typescript-eslint/scope-manager": "8.58.2",
|
||||
"@typescript-eslint/types": "8.58.2",
|
||||
@@ -4712,17 +4702,17 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@vscode/vsce": {
|
||||
"version": "3.6.0",
|
||||
"resolved": "https://registry.npmjs.org/@vscode/vsce/-/vsce-3.6.0.tgz",
|
||||
"integrity": "sha512-u2ZoMfymRNJb14aHNawnXJtXHLXDVKc1oKZaH4VELKT/9iWKRVgtQOdwxCgtwSxJoqYvuK4hGlBWQJ05wxADhg==",
|
||||
"version": "3.9.0",
|
||||
"resolved": "https://us-npm.pkg.dev/artifact-foundry-prod/ah-3p-staging-npm/@vscode/vsce/-/vsce-3.9.0.tgz",
|
||||
"integrity": "sha512-Dfql2kgPHpTKS+G4wTqYBKzK1KqX2gMxTC9dpnYge+aIZL+thh1Z6GXs4zOtNwo7DCPmS7BCfaHUAmNLxKiXkw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@azure/identity": "^4.1.0",
|
||||
"@secretlint/node": "^10.1.1",
|
||||
"@secretlint/secretlint-formatter-sarif": "^10.1.1",
|
||||
"@secretlint/secretlint-rule-no-dotenv": "^10.1.1",
|
||||
"@secretlint/secretlint-rule-preset-recommend": "^10.1.1",
|
||||
"@secretlint/node": "^10.1.2",
|
||||
"@secretlint/secretlint-formatter-sarif": "^10.1.2",
|
||||
"@secretlint/secretlint-rule-no-dotenv": "^10.1.2",
|
||||
"@secretlint/secretlint-rule-preset-recommend": "^10.1.2",
|
||||
"@vscode/vsce-sign": "^2.0.0",
|
||||
"azure-devops-node-api": "^12.5.0",
|
||||
"chalk": "^4.1.2",
|
||||
@@ -4739,13 +4729,13 @@
|
||||
"minimatch": "^3.0.3",
|
||||
"parse-semver": "^1.1.1",
|
||||
"read": "^1.0.7",
|
||||
"secretlint": "^10.1.1",
|
||||
"secretlint": "^10.1.2",
|
||||
"semver": "^7.5.2",
|
||||
"tmp": "^0.2.3",
|
||||
"typed-rest-client": "^1.8.4",
|
||||
"url-join": "^4.0.1",
|
||||
"xml2js": "^0.5.0",
|
||||
"yauzl": "^2.3.1",
|
||||
"yauzl": "^3.2.1",
|
||||
"yazl": "^2.2.2"
|
||||
},
|
||||
"bin": {
|
||||
@@ -4903,6 +4893,54 @@
|
||||
"win32"
|
||||
]
|
||||
},
|
||||
"node_modules/@vscode/vsce/node_modules/balanced-match": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://us-npm.pkg.dev/artifact-foundry-prod/ah-3p-staging-npm/balanced-match/-/balanced-match-1.0.2.tgz",
|
||||
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@vscode/vsce/node_modules/glob": {
|
||||
"version": "11.1.0",
|
||||
"resolved": "https://us-npm.pkg.dev/artifact-foundry-prod/ah-3p-staging-npm/glob/-/glob-11.1.0.tgz",
|
||||
"integrity": "sha512-vuNwKSaKiqm7g0THUBu2x7ckSs3XJLXE+2ssL7/MfTGPLLcrJQ/4Uq1CjPTtO5cCIiRxqvN6Twy1qOwhL0Xjcw==",
|
||||
"deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me",
|
||||
"dev": true,
|
||||
"license": "BlueOak-1.0.0",
|
||||
"dependencies": {
|
||||
"foreground-child": "^3.3.1",
|
||||
"jackspeak": "^4.1.1",
|
||||
"minimatch": "^10.1.1",
|
||||
"minipass": "^7.1.2",
|
||||
"package-json-from-dist": "^1.0.0",
|
||||
"path-scurry": "^2.0.0"
|
||||
},
|
||||
"bin": {
|
||||
"glob": "dist/esm/bin.mjs"
|
||||
},
|
||||
"engines": {
|
||||
"node": "20 || >=22"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/isaacs"
|
||||
}
|
||||
},
|
||||
"node_modules/@vscode/vsce/node_modules/glob/node_modules/minimatch": {
|
||||
"version": "10.2.5",
|
||||
"resolved": "https://us-npm.pkg.dev/artifact-foundry-prod/ah-3p-staging-npm/minimatch/-/minimatch-10.2.5.tgz",
|
||||
"integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==",
|
||||
"dev": true,
|
||||
"license": "BlueOak-1.0.0",
|
||||
"dependencies": {
|
||||
"brace-expansion": "^5.0.5"
|
||||
},
|
||||
"engines": {
|
||||
"node": "18 || 20 || >=22"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/isaacs"
|
||||
}
|
||||
},
|
||||
"node_modules/@vscode/vsce/node_modules/hosted-git-info": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-4.1.0.tgz",
|
||||
@@ -4942,6 +4980,30 @@
|
||||
"node": ">=4"
|
||||
}
|
||||
},
|
||||
"node_modules/@vscode/vsce/node_modules/minimatch": {
|
||||
"version": "3.1.5",
|
||||
"resolved": "https://us-npm.pkg.dev/artifact-foundry-prod/ah-3p-staging-npm/minimatch/-/minimatch-3.1.5.tgz",
|
||||
"integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"brace-expansion": "^1.1.7"
|
||||
},
|
||||
"engines": {
|
||||
"node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@vscode/vsce/node_modules/minimatch/node_modules/brace-expansion": {
|
||||
"version": "1.1.14",
|
||||
"resolved": "https://us-npm.pkg.dev/artifact-foundry-prod/ah-3p-staging-npm/brace-expansion/-/brace-expansion-1.1.14.tgz",
|
||||
"integrity": "sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"balanced-match": "^1.0.0",
|
||||
"concat-map": "0.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@vscode/vsce/node_modules/yallist": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
|
||||
@@ -4949,6 +5011,20 @@
|
||||
"dev": true,
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/@vscode/vsce/node_modules/yauzl": {
|
||||
"version": "3.3.0",
|
||||
"resolved": "https://us-npm.pkg.dev/artifact-foundry-prod/ah-3p-staging-npm/yauzl/-/yauzl-3.3.0.tgz",
|
||||
"integrity": "sha512-PtGEvEP30p7sbIBJKUBjUnqgTVOyMURc4dLo9iNyAJnNIEz9pm88cCXF21w94Kg3k6RXkeZh5DHOGS0qEONvNQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"buffer-crc32": "~0.2.3",
|
||||
"pend": "~1.2.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@vue/compiler-core": {
|
||||
"version": "3.5.26",
|
||||
"resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.26.tgz",
|
||||
@@ -5073,7 +5149,6 @@
|
||||
"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"
|
||||
},
|
||||
@@ -5705,6 +5780,19 @@
|
||||
"url": "https://bevry.me/fund"
|
||||
}
|
||||
},
|
||||
"node_modules/bl": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://us-npm.pkg.dev/artifact-foundry-prod/ah-3p-staging-npm/bl/-/bl-4.1.0.tgz",
|
||||
"integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"buffer": "^5.5.0",
|
||||
"inherits": "^2.0.4",
|
||||
"readable-stream": "^3.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/body-parser": {
|
||||
"version": "2.2.2",
|
||||
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.2.tgz",
|
||||
@@ -5784,6 +5872,32 @@
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/buffer": {
|
||||
"version": "5.7.1",
|
||||
"resolved": "https://us-npm.pkg.dev/artifact-foundry-prod/ah-3p-staging-npm/buffer/-/buffer-5.7.1.tgz",
|
||||
"integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/feross"
|
||||
},
|
||||
{
|
||||
"type": "patreon",
|
||||
"url": "https://www.patreon.com/feross"
|
||||
},
|
||||
{
|
||||
"type": "consulting",
|
||||
"url": "https://feross.org/support"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"base64-js": "^1.3.1",
|
||||
"ieee754": "^1.1.13"
|
||||
}
|
||||
},
|
||||
"node_modules/buffer-crc32": {
|
||||
"version": "0.2.13",
|
||||
"resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz",
|
||||
@@ -6440,6 +6554,13 @@
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/concat-map": {
|
||||
"version": "0.0.1",
|
||||
"resolved": "https://us-npm.pkg.dev/artifact-foundry-prod/ah-3p-staging-npm/concat-map/-/concat-map-0.0.1.tgz",
|
||||
"integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/config-chain": {
|
||||
"version": "1.1.13",
|
||||
"resolved": "https://registry.npmjs.org/config-chain/-/config-chain-1.1.13.tgz",
|
||||
@@ -6765,6 +6886,23 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/decompress-response": {
|
||||
"version": "6.0.0",
|
||||
"resolved": "https://us-npm.pkg.dev/artifact-foundry-prod/ah-3p-staging-npm/decompress-response/-/decompress-response-6.0.0.tgz",
|
||||
"integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"mimic-response": "^3.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/deep-eql": {
|
||||
"version": "5.0.2",
|
||||
"resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz",
|
||||
@@ -7134,6 +7272,17 @@
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/detect-libc": {
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://us-npm.pkg.dev/artifact-foundry-prod/ah-3p-staging-npm/detect-libc/-/detect-libc-2.1.2.tgz",
|
||||
"integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/devlop": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/devlop/-/devlop-1.1.0.tgz",
|
||||
@@ -7151,8 +7300,7 @@
|
||||
"version": "0.0.1581282",
|
||||
"resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1581282.tgz",
|
||||
"integrity": "sha512-nv7iKtNZQshSW2hKzYNr46nM/Cfh5SEvE2oV0/SEGgc9XupIY5ggf84Cz8eJIkBce7S3bmTAauFD6aysMpnqsQ==",
|
||||
"license": "BSD-3-Clause",
|
||||
"peer": true
|
||||
"license": "BSD-3-Clause"
|
||||
},
|
||||
"node_modules/dezalgo": {
|
||||
"version": "1.0.4",
|
||||
@@ -7737,7 +7885,6 @@
|
||||
"integrity": "sha512-GsGizj2Y1rCWDu6XoEekL3RLilp0voSePurjZIkxL3wlm5o5EC9VpgaP7lrCvjnkuLvzFBQWB3vWB3K5KQTveQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@eslint-community/eslint-utils": "^4.2.0",
|
||||
"@eslint-community/regexpp": "^4.12.1",
|
||||
@@ -8228,6 +8375,17 @@
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/expand-template": {
|
||||
"version": "2.0.3",
|
||||
"resolved": "https://us-npm.pkg.dev/artifact-foundry-prod/ah-3p-staging-npm/expand-template/-/expand-template-2.0.3.tgz",
|
||||
"integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==",
|
||||
"dev": true,
|
||||
"license": "(MIT OR WTFPL)",
|
||||
"optional": true,
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/expand-tilde": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/expand-tilde/-/expand-tilde-2.0.2.tgz",
|
||||
@@ -8255,7 +8413,6 @@
|
||||
"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",
|
||||
@@ -8781,6 +8938,14 @@
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/fs-constants": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://us-npm.pkg.dev/artifact-foundry-prod/ah-3p-staging-npm/fs-constants/-/fs-constants-1.0.0.tgz",
|
||||
"integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true
|
||||
},
|
||||
"node_modules/fs-extra": {
|
||||
"version": "11.3.1",
|
||||
"resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.3.1.tgz",
|
||||
@@ -9038,6 +9203,14 @@
|
||||
"node": ">= 14"
|
||||
}
|
||||
},
|
||||
"node_modules/github-from-package": {
|
||||
"version": "0.0.0",
|
||||
"resolved": "https://us-npm.pkg.dev/artifact-foundry-prod/ah-3p-staging-npm/github-from-package/-/github-from-package-0.0.0.tgz",
|
||||
"integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true
|
||||
},
|
||||
"node_modules/glob": {
|
||||
"version": "12.0.0",
|
||||
"resolved": "https://registry.npmjs.org/glob/-/glob-12.0.0.tgz",
|
||||
@@ -9522,7 +9695,6 @@
|
||||
"resolved": "https://registry.npmjs.org/hono/-/hono-4.12.12.tgz",
|
||||
"integrity": "sha512-p1JfQMKaceuCbpJKAPKVqyqviZdS0eUxH9v82oWo1kb9xjQ5wA6iP3FNVAPDFlz5/p7d45lO+BpSk1tuSZMF4Q==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=16.9.0"
|
||||
}
|
||||
@@ -9691,6 +9863,28 @@
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/ieee754": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://us-npm.pkg.dev/artifact-foundry-prod/ah-3p-staging-npm/ieee754/-/ieee754-1.2.1.tgz",
|
||||
"integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/feross"
|
||||
},
|
||||
{
|
||||
"type": "patreon",
|
||||
"url": "https://www.patreon.com/feross"
|
||||
},
|
||||
{
|
||||
"type": "consulting",
|
||||
"url": "https://feross.org/support"
|
||||
}
|
||||
],
|
||||
"license": "BSD-3-Clause",
|
||||
"optional": true
|
||||
},
|
||||
"node_modules/ignore": {
|
||||
"version": "5.3.2",
|
||||
"resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz",
|
||||
@@ -9782,7 +9976,6 @@
|
||||
"resolved": "https://registry.npmjs.org/@jrichman/ink/-/ink-6.6.9.tgz",
|
||||
"integrity": "sha512-RL9sSiLQZECnjbmBwjIHOp8yVGdWF7C/uifg7ISv/e+F3nLNsfl7FdUFQs8iZARFMJAYxMFpxW6OW+HSt9drwQ==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"ansi-escapes": "^7.0.0",
|
||||
"ansi-styles": "^6.2.3",
|
||||
@@ -10933,6 +11126,19 @@
|
||||
"safe-buffer": "^5.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/keytar": {
|
||||
"version": "7.9.0",
|
||||
"resolved": "https://us-npm.pkg.dev/artifact-foundry-prod/ah-3p-staging-npm/keytar/-/keytar-7.9.0.tgz",
|
||||
"integrity": "sha512-VPD8mtVtm5JNtA2AErl6Chp06JBfy7diFQ7TQQhdpWOl6MrCRB+eRbvAZUsbGQS9kiMq0coJsy0W0vHpDCkWsQ==",
|
||||
"dev": true,
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"node-addon-api": "^4.3.0",
|
||||
"prebuild-install": "^7.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/keyv": {
|
||||
"version": "4.5.4",
|
||||
"resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz",
|
||||
@@ -11672,6 +11878,20 @@
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/mimic-response": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://us-npm.pkg.dev/artifact-foundry-prod/ah-3p-staging-npm/mimic-response/-/mimic-response-3.1.0.tgz",
|
||||
"integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/minimatch": {
|
||||
"version": "10.2.4",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.4.tgz",
|
||||
@@ -11736,6 +11956,14 @@
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/mkdirp-classic": {
|
||||
"version": "0.5.3",
|
||||
"resolved": "https://us-npm.pkg.dev/artifact-foundry-prod/ah-3p-staging-npm/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz",
|
||||
"integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true
|
||||
},
|
||||
"node_modules/mnemonist": {
|
||||
"version": "0.40.3",
|
||||
"resolved": "https://registry.npmjs.org/mnemonist/-/mnemonist-0.40.3.tgz",
|
||||
@@ -11907,6 +12135,14 @@
|
||||
"node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/napi-build-utils": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://us-npm.pkg.dev/artifact-foundry-prod/ah-3p-staging-npm/napi-build-utils/-/napi-build-utils-2.0.0.tgz",
|
||||
"integrity": "sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true
|
||||
},
|
||||
"node_modules/natural-compare": {
|
||||
"version": "1.4.0",
|
||||
"resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz",
|
||||
@@ -11932,6 +12168,28 @@
|
||||
"node": ">= 0.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/node-abi": {
|
||||
"version": "3.89.0",
|
||||
"resolved": "https://us-npm.pkg.dev/artifact-foundry-prod/ah-3p-staging-npm/node-abi/-/node-abi-3.89.0.tgz",
|
||||
"integrity": "sha512-6u9UwL0HlAl21+agMN3YAMXcKByMqwGx+pq+P76vii5f7hTPtKDp08/H9py6DY+cfDw7kQNTGEj/rly3IgbNQA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"semver": "^7.3.5"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/node-addon-api": {
|
||||
"version": "4.3.0",
|
||||
"resolved": "https://us-npm.pkg.dev/artifact-foundry-prod/ah-3p-staging-npm/node-addon-api/-/node-addon-api-4.3.0.tgz",
|
||||
"integrity": "sha512-73sE9+3UaLYYFmDsFZnqCInzPyh3MqIwZO9cw58yIqAZhONrrabrYyYe3TuIqtIiOuTXVhsGau8hcrhhwSsDIQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true
|
||||
},
|
||||
"node_modules/node-domexception": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz",
|
||||
@@ -13077,6 +13335,75 @@
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/prebuild-install": {
|
||||
"version": "7.1.3",
|
||||
"resolved": "https://us-npm.pkg.dev/artifact-foundry-prod/ah-3p-staging-npm/prebuild-install/-/prebuild-install-7.1.3.tgz",
|
||||
"integrity": "sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==",
|
||||
"deprecated": "No longer maintained. Please contact the author of the relevant native addon; alternatives are available.",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"detect-libc": "^2.0.0",
|
||||
"expand-template": "^2.0.3",
|
||||
"github-from-package": "0.0.0",
|
||||
"minimist": "^1.2.3",
|
||||
"mkdirp-classic": "^0.5.3",
|
||||
"napi-build-utils": "^2.0.0",
|
||||
"node-abi": "^3.3.0",
|
||||
"pump": "^3.0.0",
|
||||
"rc": "^1.2.7",
|
||||
"simple-get": "^4.0.0",
|
||||
"tar-fs": "^2.0.0",
|
||||
"tunnel-agent": "^0.6.0"
|
||||
},
|
||||
"bin": {
|
||||
"prebuild-install": "bin.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/prebuild-install/node_modules/chownr": {
|
||||
"version": "1.1.4",
|
||||
"resolved": "https://us-npm.pkg.dev/artifact-foundry-prod/ah-3p-staging-npm/chownr/-/chownr-1.1.4.tgz",
|
||||
"integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"optional": true
|
||||
},
|
||||
"node_modules/prebuild-install/node_modules/tar-fs": {
|
||||
"version": "2.1.4",
|
||||
"resolved": "https://us-npm.pkg.dev/artifact-foundry-prod/ah-3p-staging-npm/tar-fs/-/tar-fs-2.1.4.tgz",
|
||||
"integrity": "sha512-mDAjwmZdh7LTT6pNleZ05Yt65HC3E+NiQzl672vQG38jIrehtJk/J3mNwIg+vShQPcLF/LV7CMnDW6vjj6sfYQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"chownr": "^1.1.1",
|
||||
"mkdirp-classic": "^0.5.2",
|
||||
"pump": "^3.0.0",
|
||||
"tar-stream": "^2.1.4"
|
||||
}
|
||||
},
|
||||
"node_modules/prebuild-install/node_modules/tar-stream": {
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://us-npm.pkg.dev/artifact-foundry-prod/ah-3p-staging-npm/tar-stream/-/tar-stream-2.2.0.tgz",
|
||||
"integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"bl": "^4.0.3",
|
||||
"end-of-stream": "^1.4.1",
|
||||
"fs-constants": "^1.0.0",
|
||||
"inherits": "^2.0.3",
|
||||
"readable-stream": "^3.1.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/prelude-ls": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz",
|
||||
@@ -13496,7 +13823,6 @@
|
||||
"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"
|
||||
}
|
||||
@@ -13507,7 +13833,6 @@
|
||||
"integrity": "sha512-ePrwPfxAnB+7hgnEr8vpKxL9cmnp7F322t8oqcPshbIQQhDKgFDW4tjhF2wjVbdXF9O/nyuy3sQWd9JGpiLPvA==",
|
||||
"devOptional": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"shell-quote": "^1.6.1",
|
||||
"ws": "^7"
|
||||
@@ -14518,6 +14843,55 @@
|
||||
"url": "https://github.com/sponsors/isaacs"
|
||||
}
|
||||
},
|
||||
"node_modules/simple-concat": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://us-npm.pkg.dev/artifact-foundry-prod/ah-3p-staging-npm/simple-concat/-/simple-concat-1.0.1.tgz",
|
||||
"integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/feross"
|
||||
},
|
||||
{
|
||||
"type": "patreon",
|
||||
"url": "https://www.patreon.com/feross"
|
||||
},
|
||||
{
|
||||
"type": "consulting",
|
||||
"url": "https://feross.org/support"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true
|
||||
},
|
||||
"node_modules/simple-get": {
|
||||
"version": "4.0.1",
|
||||
"resolved": "https://us-npm.pkg.dev/artifact-foundry-prod/ah-3p-staging-npm/simple-get/-/simple-get-4.0.1.tgz",
|
||||
"integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/feross"
|
||||
},
|
||||
{
|
||||
"type": "patreon",
|
||||
"url": "https://www.patreon.com/feross"
|
||||
},
|
||||
{
|
||||
"type": "consulting",
|
||||
"url": "https://feross.org/support"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"decompress-response": "^6.0.0",
|
||||
"once": "^1.3.1",
|
||||
"simple-concat": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/simple-git": {
|
||||
"version": "3.33.0",
|
||||
"resolved": "https://registry.npmjs.org/simple-git/-/simple-git-3.33.0.tgz",
|
||||
@@ -15627,7 +16001,6 @@
|
||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz",
|
||||
"integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
@@ -15850,8 +16223,7 @@
|
||||
"version": "2.8.1",
|
||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
|
||||
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
|
||||
"license": "0BSD",
|
||||
"peer": true
|
||||
"license": "0BSD"
|
||||
},
|
||||
"node_modules/tsx": {
|
||||
"version": "4.20.3",
|
||||
@@ -15859,7 +16231,6 @@
|
||||
"integrity": "sha512-qjbnuR9Tr+FJOMBqJCW5ehvIo/buZq7vH7qD7JziU98h6l3qGy0a/yPFjwO+y0/T7GFpNgNAvEcPPVfyT8rrPQ==",
|
||||
"devOptional": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"esbuild": "~0.25.0",
|
||||
"get-tsconfig": "^4.7.5"
|
||||
@@ -15884,6 +16255,20 @@
|
||||
"node": ">=0.6.11 <=0.7.0 || >=0.7.3"
|
||||
}
|
||||
},
|
||||
"node_modules/tunnel-agent": {
|
||||
"version": "0.6.0",
|
||||
"resolved": "https://us-npm.pkg.dev/artifact-foundry-prod/ah-3p-staging-npm/tunnel-agent/-/tunnel-agent-0.6.0.tgz",
|
||||
"integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"safe-buffer": "^5.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/type-check": {
|
||||
"version": "0.4.0",
|
||||
"resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz",
|
||||
@@ -16025,7 +16410,6 @@
|
||||
"integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==",
|
||||
"devOptional": true,
|
||||
"license": "Apache-2.0",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"tsc": "bin/tsc",
|
||||
"tsserver": "bin/tsserver"
|
||||
@@ -16093,7 +16477,6 @@
|
||||
"integrity": "sha512-6sMvZePQrnZH2/cJkwRpkT7DxoAWh+g6+GFRK6bV3YQo7ogi3SX5rgF6099r5Q53Ma5qeT7LGmOmuIutF4t3lA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@typescript-eslint/scope-manager": "8.35.0",
|
||||
"@typescript-eslint/types": "8.35.0",
|
||||
@@ -16480,7 +16863,6 @@
|
||||
"resolved": "https://registry.npmjs.org/vite/-/vite-7.3.2.tgz",
|
||||
"integrity": "sha512-Bby3NOsna2jsjfLVOHKes8sGwgl4TT0E6vvpYgnAYDIF/tie7MRaFthmKuHx1NSXjiTueXH3do80FMQgvEktRg==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"esbuild": "^0.27.0",
|
||||
"fdir": "^6.5.0",
|
||||
@@ -17051,7 +17433,6 @@
|
||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz",
|
||||
"integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
@@ -17064,7 +17445,6 @@
|
||||
"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",
|
||||
@@ -17703,7 +18083,6 @@
|
||||
"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"
|
||||
}
|
||||
@@ -18031,7 +18410,6 @@
|
||||
"ajv": "^8.17.1",
|
||||
"ajv-formats": "^3.0.0",
|
||||
"chardet": "^2.1.0",
|
||||
"chokidar": "^5.0.0",
|
||||
"diff": "^8.0.3",
|
||||
"dotenv": "^17.2.4",
|
||||
"dotenv-expand": "^12.0.3",
|
||||
@@ -18139,7 +18517,6 @@
|
||||
"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.8.0",
|
||||
"@js-sdsl/ordered-map": "^4.4.2"
|
||||
@@ -18182,21 +18559,6 @@
|
||||
"url": "https://github.com/sponsors/epoberezkin"
|
||||
}
|
||||
},
|
||||
"packages/core/node_modules/chokidar": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-5.0.0.tgz",
|
||||
"integrity": "sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"readdirp": "^5.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 20.19.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://paulmillr.com/funding/"
|
||||
}
|
||||
},
|
||||
"packages/core/node_modules/dotenv": {
|
||||
"version": "17.3.1",
|
||||
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.3.1.tgz",
|
||||
@@ -18258,7 +18620,6 @@
|
||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz",
|
||||
"integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
@@ -18266,19 +18627,6 @@
|
||||
"url": "https://github.com/sponsors/jonschlinkert"
|
||||
}
|
||||
},
|
||||
"packages/core/node_modules/readdirp": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-5.0.0.tgz",
|
||||
"integrity": "sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 20.19.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "individual",
|
||||
"url": "https://paulmillr.com/funding/"
|
||||
}
|
||||
},
|
||||
"packages/core/node_modules/typescript": {
|
||||
"version": "5.9.3",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
|
||||
@@ -18401,7 +18749,7 @@
|
||||
"@types/vscode": "^1.99.0",
|
||||
"@typescript-eslint/eslint-plugin": "^8.31.1",
|
||||
"@typescript-eslint/parser": "^8.31.1",
|
||||
"@vscode/vsce": "^3.6.0",
|
||||
"@vscode/vsce": "^3.7.1",
|
||||
"esbuild": "^0.25.3",
|
||||
"eslint": "^9.25.1",
|
||||
"npm-run-all2": "^8.0.2",
|
||||
|
||||
@@ -98,7 +98,6 @@ export function createMockConfig(
|
||||
getMcpServers: vi.fn().mockReturnValue({}),
|
||||
}),
|
||||
getTelemetryLogPromptsEnabled: vi.fn().mockReturnValue(false),
|
||||
getTelemetryTracesEnabled: vi.fn().mockReturnValue(false),
|
||||
getGitService: vi.fn(),
|
||||
validatePathAccess: vi.fn().mockReturnValue(undefined),
|
||||
getShellExecutionConfig: vi.fn().mockReturnValue({
|
||||
|
||||
@@ -41,8 +41,6 @@ import * as fs from 'node:fs/promises';
|
||||
import * as path from 'node:path';
|
||||
import { ApprovalMode } from '@google/gemini-cli-core/src/policy/types.js';
|
||||
|
||||
const startMemoryServiceMock = vi.hoisted(() => vi.fn());
|
||||
|
||||
vi.mock('../config/config.js', () => ({
|
||||
loadCliConfig: vi.fn(),
|
||||
}));
|
||||
@@ -103,7 +101,6 @@ vi.mock(
|
||||
const actual = await importOriginal();
|
||||
return {
|
||||
...actual,
|
||||
startMemoryService: startMemoryServiceMock,
|
||||
updatePolicy: vi.fn(),
|
||||
createPolicyUpdater: vi.fn(),
|
||||
ReadManyFilesTool: vi.fn(),
|
||||
@@ -151,8 +148,6 @@ describe('GeminiAgent', () => {
|
||||
let agent: GeminiAgent;
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
startMemoryServiceMock.mockResolvedValue(undefined);
|
||||
mockConfig = {
|
||||
refreshAuth: vi.fn(),
|
||||
initialize: vi.fn(),
|
||||
@@ -160,7 +155,6 @@ describe('GeminiAgent', () => {
|
||||
getFileSystemService: vi.fn(),
|
||||
setFileSystemService: vi.fn(),
|
||||
getContentGeneratorConfig: vi.fn(),
|
||||
isAutoMemoryEnabled: vi.fn().mockReturnValue(false),
|
||||
getActiveModel: vi.fn().mockReturnValue('gemini-pro'),
|
||||
getModel: vi.fn().mockReturnValue('gemini-pro'),
|
||||
getGeminiClient: vi.fn().mockReturnValue({
|
||||
@@ -360,34 +354,6 @@ describe('GeminiAgent', () => {
|
||||
vi.useRealTimers();
|
||||
});
|
||||
|
||||
it('should start auto memory for new ACP sessions when enabled', async () => {
|
||||
mockConfig.getContentGeneratorConfig = vi.fn().mockReturnValue({
|
||||
apiKey: 'test-key',
|
||||
});
|
||||
mockConfig.isAutoMemoryEnabled = vi.fn().mockReturnValue(true);
|
||||
|
||||
await agent.newSession({
|
||||
cwd: '/tmp',
|
||||
mcpServers: [],
|
||||
});
|
||||
|
||||
expect(startMemoryServiceMock).toHaveBeenCalledWith(mockConfig);
|
||||
});
|
||||
|
||||
it('should not start auto memory for new ACP sessions when disabled', async () => {
|
||||
mockConfig.getContentGeneratorConfig = vi.fn().mockReturnValue({
|
||||
apiKey: 'test-key',
|
||||
});
|
||||
mockConfig.isAutoMemoryEnabled = vi.fn().mockReturnValue(false);
|
||||
|
||||
await agent.newSession({
|
||||
cwd: '/tmp',
|
||||
mcpServers: [],
|
||||
});
|
||||
|
||||
expect(startMemoryServiceMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should return modes without plan mode when plan is disabled', async () => {
|
||||
mockConfig.getContentGeneratorConfig = vi.fn().mockReturnValue({
|
||||
apiKey: 'test-key',
|
||||
|
||||
@@ -76,7 +76,6 @@ import { randomUUID } from 'node:crypto';
|
||||
import { loadCliConfig, type CliArgs } from '../config/config.js';
|
||||
import { runExitCleanup } from '../utils/cleanup.js';
|
||||
import { SessionSelector } from '../utils/sessionUtils.js';
|
||||
import { startAutoMemoryIfEnabled } from '../utils/autoMemory.js';
|
||||
|
||||
import { CommandHandler } from './commandHandler.js';
|
||||
|
||||
@@ -325,7 +324,6 @@ export class GeminiAgent {
|
||||
|
||||
await config.initialize();
|
||||
startupProfiler.flush(config);
|
||||
startAutoMemoryIfEnabled(config);
|
||||
|
||||
const geminiClient = config.getGeminiClient();
|
||||
const chat = await geminiClient.startChat();
|
||||
@@ -467,7 +465,6 @@ export class GeminiAgent {
|
||||
// which starts the MCP servers and other heavy resources.
|
||||
await config.initialize();
|
||||
startupProfiler.flush(config);
|
||||
startAutoMemoryIfEnabled(config);
|
||||
|
||||
return config;
|
||||
}
|
||||
|
||||
@@ -100,7 +100,6 @@ describe('GeminiAgent Session Resume', () => {
|
||||
unsubscribe: vi.fn(),
|
||||
},
|
||||
getApprovalMode: vi.fn().mockReturnValue('default'),
|
||||
isAutoMemoryEnabled: vi.fn().mockReturnValue(false),
|
||||
isPlanEnabled: vi.fn().mockReturnValue(true),
|
||||
getModel: vi.fn().mockReturnValue('gemini-pro'),
|
||||
getHasAccessToPreviewModel: vi.fn().mockReturnValue(false),
|
||||
|
||||
@@ -135,10 +135,10 @@ export class InboxMemoryCommand implements Command {
|
||||
context: CommandContext,
|
||||
_: string[],
|
||||
): Promise<CommandExecutionResponse> {
|
||||
if (!context.agentContext.config.isAutoMemoryEnabled()) {
|
||||
if (!context.agentContext.config.isMemoryManagerEnabled()) {
|
||||
return {
|
||||
name: this.name,
|
||||
data: 'The memory inbox requires Auto Memory. Enable it with: experimental.autoMemory = true in settings.',
|
||||
data: 'The memory inbox requires the experimental memory manager. Enable it with: experimental.memoryManager = true in settings.',
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -1,33 +0,0 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2026 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import type { CommandModule, Argv } from 'yargs';
|
||||
import { initializeOutputListenersAndFlush } from '../gemini.js';
|
||||
import { defer } from '../deferred.js';
|
||||
import { setupCommand } from './gemma/setup.js';
|
||||
import { startCommand } from './gemma/start.js';
|
||||
import { stopCommand } from './gemma/stop.js';
|
||||
import { statusCommand } from './gemma/status.js';
|
||||
import { logsCommand } from './gemma/logs.js';
|
||||
|
||||
export const gemmaCommand: CommandModule = {
|
||||
command: 'gemma',
|
||||
describe: 'Manage local Gemma model routing',
|
||||
builder: (yargs: Argv) =>
|
||||
yargs
|
||||
.middleware((argv) => {
|
||||
initializeOutputListenersAndFlush();
|
||||
argv['isCommand'] = true;
|
||||
})
|
||||
.command(defer(setupCommand, 'gemma'))
|
||||
.command(defer(startCommand, 'gemma'))
|
||||
.command(defer(stopCommand, 'gemma'))
|
||||
.command(defer(statusCommand, 'gemma'))
|
||||
.command(defer(logsCommand, 'gemma'))
|
||||
.demandCommand(1, 'You need at least one command before continuing.')
|
||||
.version(false),
|
||||
handler: () => {},
|
||||
};
|
||||
@@ -1,45 +0,0 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2026 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import path from 'node:path';
|
||||
import { Storage } from '@google/gemini-cli-core';
|
||||
|
||||
export const LITERT_RELEASE_VERSION = 'v0.9.0-alpha03';
|
||||
export const LITERT_RELEASE_BASE_URL =
|
||||
'https://github.com/google-ai-edge/LiteRT-LM/releases/download';
|
||||
export const GEMMA_MODEL_NAME = 'gemma3-1b-gpu-custom';
|
||||
export const DEFAULT_PORT = 9379;
|
||||
export const HEALTH_CHECK_TIMEOUT_MS = 5000;
|
||||
export const LITERT_API_VERSION = 'v1beta';
|
||||
export const SERVER_START_WAIT_MS = 3000;
|
||||
|
||||
export const PLATFORM_BINARY_MAP: Record<string, string> = {
|
||||
'darwin-arm64': 'lit.macos_arm64',
|
||||
'linux-x64': 'lit.linux_x86_64',
|
||||
'win32-x64': 'lit.windows_x86_64.exe',
|
||||
};
|
||||
|
||||
// SHA-256 hashes for the official LiteRT-LM v0.9.0-alpha03 release binaries.
|
||||
export const PLATFORM_BINARY_SHA256: Record<string, string> = {
|
||||
'lit.macos_arm64':
|
||||
'9e826a2634f2e8b220ad0f1e1b5c139e0b47cb172326e3b7d46d31382f49478e',
|
||||
'lit.linux_x86_64':
|
||||
'66601df8a07f08244b188e9fcab0bf4a16562fe76d8d47e49f40273d57541ee8',
|
||||
'lit.windows_x86_64.exe':
|
||||
'de82d2829d2fb1cbdb318e2d8a78dc2f9659ff14cb11b2894d1f30e0bfde2bf6',
|
||||
};
|
||||
|
||||
export function getLiteRtBinDir(): string {
|
||||
return path.join(Storage.getGlobalGeminiDir(), 'bin', 'litert');
|
||||
}
|
||||
|
||||
export function getPidFilePath(): string {
|
||||
return path.join(Storage.getGlobalTempDir(), 'litert-server.pid');
|
||||
}
|
||||
|
||||
export function getLogFilePath(): string {
|
||||
return path.join(Storage.getGlobalTempDir(), 'litert-server.log');
|
||||
}
|
||||
@@ -1,186 +0,0 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2026 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import fs from 'node:fs';
|
||||
import type { ChildProcess } from 'node:child_process';
|
||||
import { EventEmitter } from 'node:events';
|
||||
import os from 'node:os';
|
||||
import path from 'node:path';
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
import { spawn } from 'node:child_process';
|
||||
import { exitCli } from '../utils.js';
|
||||
import { getLogFilePath } from './constants.js';
|
||||
import { logsCommand, readLastLines } from './logs.js';
|
||||
|
||||
vi.mock('@google/gemini-cli-core', async (importOriginal) => {
|
||||
const { mockCoreDebugLogger } = await import(
|
||||
'../../test-utils/mockDebugLogger.js'
|
||||
);
|
||||
return mockCoreDebugLogger(
|
||||
await importOriginal<typeof import('@google/gemini-cli-core')>(),
|
||||
{
|
||||
stripAnsi: false,
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
vi.mock('node:child_process', async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import('node:child_process')>();
|
||||
return {
|
||||
...actual,
|
||||
spawn: vi.fn(),
|
||||
};
|
||||
});
|
||||
|
||||
vi.mock('../utils.js', () => ({
|
||||
exitCli: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock('./constants.js', () => ({
|
||||
getLogFilePath: vi.fn(),
|
||||
}));
|
||||
|
||||
function createMockChild(): ChildProcess {
|
||||
return Object.assign(new EventEmitter(), {
|
||||
kill: vi.fn(),
|
||||
}) as unknown as ChildProcess;
|
||||
}
|
||||
|
||||
async function flushMicrotasks() {
|
||||
await Promise.resolve();
|
||||
await Promise.resolve();
|
||||
}
|
||||
|
||||
describe('readLastLines', () => {
|
||||
const tempFiles: string[] = [];
|
||||
|
||||
afterEach(async () => {
|
||||
await Promise.all(
|
||||
tempFiles
|
||||
.splice(0)
|
||||
.map((filePath) => fs.promises.rm(filePath, { force: true })),
|
||||
);
|
||||
});
|
||||
|
||||
it('returns only the requested tail lines without reading the whole file eagerly', async () => {
|
||||
const filePath = path.join(
|
||||
os.tmpdir(),
|
||||
`gemma-logs-${Date.now()}-${Math.random().toString(36).slice(2)}.log`,
|
||||
);
|
||||
tempFiles.push(filePath);
|
||||
|
||||
const content = Array.from({ length: 2000 }, (_, i) => `line-${i + 1}`)
|
||||
.join('\n')
|
||||
.concat('\n');
|
||||
await fs.promises.writeFile(filePath, content, 'utf-8');
|
||||
|
||||
await expect(readLastLines(filePath, 3)).resolves.toBe(
|
||||
'line-1998\nline-1999\nline-2000\n',
|
||||
);
|
||||
});
|
||||
|
||||
it('returns an empty string when zero lines are requested', async () => {
|
||||
const filePath = path.join(
|
||||
os.tmpdir(),
|
||||
`gemma-logs-${Date.now()}-${Math.random().toString(36).slice(2)}.log`,
|
||||
);
|
||||
tempFiles.push(filePath);
|
||||
await fs.promises.writeFile(filePath, 'line-1\nline-2\n', 'utf-8');
|
||||
|
||||
await expect(readLastLines(filePath, 0)).resolves.toBe('');
|
||||
});
|
||||
});
|
||||
|
||||
describe('logsCommand', () => {
|
||||
const originalPlatform = process.platform;
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
Object.defineProperty(process, 'platform', {
|
||||
value: 'linux',
|
||||
configurable: true,
|
||||
});
|
||||
vi.mocked(getLogFilePath).mockReturnValue('/tmp/gemma.log');
|
||||
vi.spyOn(fs.promises, 'access').mockResolvedValue(undefined);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
Object.defineProperty(process, 'platform', {
|
||||
value: originalPlatform,
|
||||
configurable: true,
|
||||
});
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
it('waits for the tail process to close before exiting in follow mode', async () => {
|
||||
const child = createMockChild();
|
||||
vi.mocked(spawn).mockReturnValue(child);
|
||||
|
||||
let resolved = false;
|
||||
const handlerPromise = (
|
||||
logsCommand.handler as (argv: Record<string, unknown>) => Promise<void>
|
||||
)({}).then(() => {
|
||||
resolved = true;
|
||||
});
|
||||
|
||||
await flushMicrotasks();
|
||||
|
||||
expect(spawn).toHaveBeenCalledWith(
|
||||
'tail',
|
||||
['-f', '-n', '20', '/tmp/gemma.log'],
|
||||
{ stdio: 'inherit' },
|
||||
);
|
||||
expect(resolved).toBe(false);
|
||||
expect(exitCli).not.toHaveBeenCalled();
|
||||
|
||||
child.emit('close', 0);
|
||||
await handlerPromise;
|
||||
|
||||
expect(exitCli).toHaveBeenCalledWith(0);
|
||||
});
|
||||
|
||||
it('uses one-shot tail output when follow is disabled', async () => {
|
||||
const child = createMockChild();
|
||||
vi.mocked(spawn).mockReturnValue(child);
|
||||
|
||||
const handlerPromise = (
|
||||
logsCommand.handler as (argv: Record<string, unknown>) => Promise<void>
|
||||
)({ follow: false });
|
||||
|
||||
await flushMicrotasks();
|
||||
|
||||
expect(spawn).toHaveBeenCalledWith('tail', ['-n', '20', '/tmp/gemma.log'], {
|
||||
stdio: 'inherit',
|
||||
});
|
||||
|
||||
child.emit('close', 0);
|
||||
await handlerPromise;
|
||||
|
||||
expect(exitCli).toHaveBeenCalledWith(0);
|
||||
});
|
||||
|
||||
it('follows from the requested line count when both --lines and --follow are set', async () => {
|
||||
const child = createMockChild();
|
||||
vi.mocked(spawn).mockReturnValue(child);
|
||||
|
||||
const handlerPromise = (
|
||||
logsCommand.handler as (argv: Record<string, unknown>) => Promise<void>
|
||||
)({ lines: 5, follow: true });
|
||||
|
||||
await flushMicrotasks();
|
||||
|
||||
expect(spawn).toHaveBeenCalledWith(
|
||||
'tail',
|
||||
['-f', '-n', '5', '/tmp/gemma.log'],
|
||||
{ stdio: 'inherit' },
|
||||
);
|
||||
|
||||
child.emit('close', 0);
|
||||
await handlerPromise;
|
||||
|
||||
expect(exitCli).toHaveBeenCalledWith(0);
|
||||
});
|
||||
});
|
||||
@@ -1,200 +0,0 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2026 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import type { CommandModule } from 'yargs';
|
||||
import fs from 'node:fs';
|
||||
import { spawn, type ChildProcess } from 'node:child_process';
|
||||
import { debugLogger } from '@google/gemini-cli-core';
|
||||
import { exitCli } from '../utils.js';
|
||||
import { getLogFilePath } from './constants.js';
|
||||
|
||||
export async function readLastLines(
|
||||
filePath: string,
|
||||
count: number,
|
||||
): Promise<string> {
|
||||
if (count <= 0) {
|
||||
return '';
|
||||
}
|
||||
|
||||
const CHUNK_SIZE = 64 * 1024;
|
||||
const fileHandle = await fs.promises.open(filePath, fs.constants.O_RDONLY);
|
||||
|
||||
try {
|
||||
const stats = await fileHandle.stat();
|
||||
if (stats.size === 0) {
|
||||
return '';
|
||||
}
|
||||
|
||||
const chunks: Buffer[] = [];
|
||||
let totalBytes = 0;
|
||||
let newlineCount = 0;
|
||||
let position = stats.size;
|
||||
|
||||
while (position > 0 && newlineCount <= count) {
|
||||
const readSize = Math.min(CHUNK_SIZE, position);
|
||||
position -= readSize;
|
||||
|
||||
const buffer = Buffer.allocUnsafe(readSize);
|
||||
const { bytesRead } = await fileHandle.read(
|
||||
buffer,
|
||||
0,
|
||||
readSize,
|
||||
position,
|
||||
);
|
||||
|
||||
if (bytesRead === 0) {
|
||||
break;
|
||||
}
|
||||
|
||||
const chunk =
|
||||
bytesRead === readSize ? buffer : buffer.subarray(0, bytesRead);
|
||||
chunks.unshift(chunk);
|
||||
totalBytes += chunk.length;
|
||||
|
||||
for (const byte of chunk) {
|
||||
if (byte === 0x0a) {
|
||||
newlineCount += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const content = Buffer.concat(chunks, totalBytes).toString('utf-8');
|
||||
const lines = content.split('\n');
|
||||
|
||||
if (position > 0 && lines.length > 0) {
|
||||
const boundary = Buffer.allocUnsafe(1);
|
||||
const { bytesRead } = await fileHandle.read(boundary, 0, 1, position - 1);
|
||||
if (bytesRead === 1 && boundary[0] !== 0x0a) {
|
||||
lines.shift();
|
||||
}
|
||||
}
|
||||
|
||||
if (lines.length > 0 && lines[lines.length - 1] === '') {
|
||||
lines.pop();
|
||||
}
|
||||
|
||||
if (lines.length === 0) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return lines.slice(-count).join('\n') + '\n';
|
||||
} finally {
|
||||
await fileHandle.close();
|
||||
}
|
||||
}
|
||||
|
||||
interface LogsArgs {
|
||||
lines?: number;
|
||||
follow?: boolean;
|
||||
}
|
||||
|
||||
function waitForChild(child: ChildProcess): Promise<number> {
|
||||
return new Promise((resolve, reject) => {
|
||||
child.once('error', reject);
|
||||
child.once('close', (code) => resolve(code ?? 1));
|
||||
});
|
||||
}
|
||||
|
||||
async function runTail(logPath: string, lines: number, follow: boolean) {
|
||||
const tailArgs = follow
|
||||
? ['-f', '-n', String(lines), logPath]
|
||||
: ['-n', String(lines), logPath];
|
||||
const child = spawn('tail', tailArgs, { stdio: 'inherit' });
|
||||
|
||||
if (!follow) {
|
||||
return waitForChild(child);
|
||||
}
|
||||
|
||||
const handleSigint = () => {
|
||||
child.kill('SIGTERM');
|
||||
};
|
||||
process.once('SIGINT', handleSigint);
|
||||
|
||||
try {
|
||||
return await waitForChild(child);
|
||||
} finally {
|
||||
process.off('SIGINT', handleSigint);
|
||||
}
|
||||
}
|
||||
|
||||
export const logsCommand: CommandModule<object, LogsArgs> = {
|
||||
command: 'logs',
|
||||
describe: 'View LiteRT-LM server logs',
|
||||
builder: (yargs) =>
|
||||
yargs
|
||||
.option('lines', {
|
||||
alias: 'n',
|
||||
type: 'number',
|
||||
description: 'Show the last N lines and exit (omit to follow live)',
|
||||
})
|
||||
.option('follow', {
|
||||
alias: 'f',
|
||||
type: 'boolean',
|
||||
description:
|
||||
'Follow log output (defaults to true when --lines is omitted)',
|
||||
}),
|
||||
handler: async (argv) => {
|
||||
const logPath = getLogFilePath();
|
||||
|
||||
try {
|
||||
await fs.promises.access(logPath, fs.constants.F_OK);
|
||||
} catch {
|
||||
debugLogger.log(`No log file found at ${logPath}`);
|
||||
debugLogger.log(
|
||||
'Is the LiteRT server running? Start it with: gemini gemma start',
|
||||
);
|
||||
await exitCli(1);
|
||||
return;
|
||||
}
|
||||
|
||||
const lines = argv.lines;
|
||||
const follow = argv.follow ?? lines === undefined;
|
||||
const requestedLines = lines ?? 20;
|
||||
|
||||
if (follow && process.platform === 'win32') {
|
||||
debugLogger.log(
|
||||
'Live log following is not supported on Windows. Use --lines N to view recent logs.',
|
||||
);
|
||||
await exitCli(1);
|
||||
return;
|
||||
}
|
||||
|
||||
if (process.platform === 'win32') {
|
||||
process.stdout.write(await readLastLines(logPath, requestedLines));
|
||||
await exitCli(0);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
if (follow) {
|
||||
debugLogger.log(`Tailing ${logPath} (Ctrl+C to stop)\n`);
|
||||
}
|
||||
const exitCode = await runTail(logPath, requestedLines, follow);
|
||||
await exitCli(exitCode);
|
||||
} catch (error) {
|
||||
if (
|
||||
error instanceof Error &&
|
||||
'code' in error &&
|
||||
error.code === 'ENOENT'
|
||||
) {
|
||||
if (!follow) {
|
||||
process.stdout.write(await readLastLines(logPath, requestedLines));
|
||||
await exitCli(0);
|
||||
} else {
|
||||
debugLogger.error(
|
||||
'"tail" command not found. Use --lines N to view recent logs without tail.',
|
||||
);
|
||||
await exitCli(1);
|
||||
}
|
||||
} else {
|
||||
debugLogger.error(
|
||||
`Failed to read log output: ${error instanceof Error ? error.message : String(error)}`,
|
||||
);
|
||||
await exitCli(1);
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
@@ -1,162 +0,0 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2026 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
import { SettingScope } from '../../config/settings.js';
|
||||
import { getLiteRtBinDir } from './constants.js';
|
||||
|
||||
const mockLoadSettings = vi.hoisted(() => vi.fn());
|
||||
|
||||
vi.mock('../../config/settings.js', () => ({
|
||||
loadSettings: mockLoadSettings,
|
||||
SettingScope: {
|
||||
User: 'User',
|
||||
},
|
||||
}));
|
||||
|
||||
import {
|
||||
getBinaryPath,
|
||||
isExpectedLiteRtServerCommand,
|
||||
isBinaryInstalled,
|
||||
readServerProcessInfo,
|
||||
resolveGemmaConfig,
|
||||
} from './platform.js';
|
||||
|
||||
describe('gemma platform helpers', () => {
|
||||
function createMockSettings(
|
||||
userGemmaSettings?: object,
|
||||
mergedGemmaSettings?: object,
|
||||
) {
|
||||
return {
|
||||
merged: {
|
||||
experimental: {
|
||||
gemmaModelRouter: mergedGemmaSettings,
|
||||
},
|
||||
},
|
||||
forScope: vi.fn((scope: SettingScope) => {
|
||||
if (scope !== SettingScope.User) {
|
||||
throw new Error(`Unexpected scope ${scope}`);
|
||||
}
|
||||
return {
|
||||
settings: {
|
||||
experimental: {
|
||||
gemmaModelRouter: userGemmaSettings,
|
||||
},
|
||||
},
|
||||
};
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
mockLoadSettings.mockReturnValue(createMockSettings());
|
||||
});
|
||||
|
||||
it('prefers the configured binary path from settings', () => {
|
||||
mockLoadSettings.mockReturnValue(
|
||||
createMockSettings({ binaryPath: '/custom/lit' }),
|
||||
);
|
||||
|
||||
expect(getBinaryPath('lit.test')).toBe('/custom/lit');
|
||||
});
|
||||
|
||||
it('ignores workspace overrides for the configured binary path', () => {
|
||||
mockLoadSettings.mockReturnValue(
|
||||
createMockSettings(
|
||||
{ binaryPath: '/user/lit' },
|
||||
{ binaryPath: '/workspace/evil' },
|
||||
),
|
||||
);
|
||||
|
||||
expect(getBinaryPath('lit.test')).toBe('/user/lit');
|
||||
});
|
||||
|
||||
it('falls back to the default install location when no custom path is set', () => {
|
||||
expect(getBinaryPath('lit.test')).toBe(
|
||||
path.join(getLiteRtBinDir(), 'lit.test'),
|
||||
);
|
||||
});
|
||||
|
||||
it('resolves the configured port and binary path from settings', () => {
|
||||
mockLoadSettings.mockReturnValue(
|
||||
createMockSettings(
|
||||
{ binaryPath: '/custom/lit' },
|
||||
{
|
||||
enabled: true,
|
||||
classifier: {
|
||||
host: 'http://localhost:8123/v1beta',
|
||||
},
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
expect(resolveGemmaConfig(9379)).toEqual({
|
||||
settingsEnabled: true,
|
||||
configuredPort: 8123,
|
||||
configuredBinaryPath: '/custom/lit',
|
||||
});
|
||||
});
|
||||
|
||||
it('checks binary installation using the resolved binary path', () => {
|
||||
mockLoadSettings.mockReturnValue(
|
||||
createMockSettings({ binaryPath: '/custom/lit' }),
|
||||
);
|
||||
vi.spyOn(fs, 'existsSync').mockReturnValue(true);
|
||||
|
||||
expect(isBinaryInstalled()).toBe(true);
|
||||
expect(fs.existsSync).toHaveBeenCalledWith('/custom/lit');
|
||||
});
|
||||
|
||||
it('parses structured server process info from the pid file', () => {
|
||||
vi.spyOn(fs, 'readFileSync').mockReturnValue(
|
||||
JSON.stringify({
|
||||
pid: 1234,
|
||||
binaryPath: '/custom/lit',
|
||||
port: 8123,
|
||||
}),
|
||||
);
|
||||
|
||||
expect(readServerProcessInfo()).toEqual({
|
||||
pid: 1234,
|
||||
binaryPath: '/custom/lit',
|
||||
port: 8123,
|
||||
});
|
||||
});
|
||||
|
||||
it('parses legacy pid-only files for backward compatibility', () => {
|
||||
vi.spyOn(fs, 'readFileSync').mockReturnValue('4321');
|
||||
|
||||
expect(readServerProcessInfo()).toEqual({
|
||||
pid: 4321,
|
||||
});
|
||||
});
|
||||
|
||||
it('matches only the expected LiteRT serve command', () => {
|
||||
expect(
|
||||
isExpectedLiteRtServerCommand('/custom/lit serve --port=8123 --verbose', {
|
||||
binaryPath: '/custom/lit',
|
||||
port: 8123,
|
||||
}),
|
||||
).toBe(true);
|
||||
|
||||
expect(
|
||||
isExpectedLiteRtServerCommand('/custom/lit run --port=8123', {
|
||||
binaryPath: '/custom/lit',
|
||||
port: 8123,
|
||||
}),
|
||||
).toBe(false);
|
||||
|
||||
expect(
|
||||
isExpectedLiteRtServerCommand('/custom/lit serve --port=9000', {
|
||||
binaryPath: '/custom/lit',
|
||||
port: 8123,
|
||||
}),
|
||||
).toBe(false);
|
||||
});
|
||||
});
|
||||
@@ -1,316 +0,0 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2026 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { loadSettings, SettingScope } from '../../config/settings.js';
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import { execFileSync } from 'node:child_process';
|
||||
import {
|
||||
PLATFORM_BINARY_MAP,
|
||||
LITERT_RELEASE_BASE_URL,
|
||||
LITERT_RELEASE_VERSION,
|
||||
getLiteRtBinDir,
|
||||
GEMMA_MODEL_NAME,
|
||||
HEALTH_CHECK_TIMEOUT_MS,
|
||||
LITERT_API_VERSION,
|
||||
getPidFilePath,
|
||||
} from './constants.js';
|
||||
|
||||
export interface PlatformInfo {
|
||||
key: string;
|
||||
binaryName: string;
|
||||
}
|
||||
|
||||
export interface GemmaConfigStatus {
|
||||
settingsEnabled: boolean;
|
||||
configuredPort: number;
|
||||
configuredBinaryPath?: string;
|
||||
}
|
||||
|
||||
export interface LiteRtServerProcessInfo {
|
||||
pid: number;
|
||||
binaryPath?: string;
|
||||
port?: number;
|
||||
}
|
||||
|
||||
function getUserConfiguredBinaryPath(
|
||||
workspaceDir = process.cwd(),
|
||||
): string | undefined {
|
||||
try {
|
||||
const userGemmaSettings = loadSettings(workspaceDir).forScope(
|
||||
SettingScope.User,
|
||||
).settings.experimental?.gemmaModelRouter;
|
||||
return userGemmaSettings?.binaryPath?.trim() || undefined;
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
function parsePortFromHost(
|
||||
host: string | undefined,
|
||||
fallbackPort: number,
|
||||
): number {
|
||||
if (!host) {
|
||||
return fallbackPort;
|
||||
}
|
||||
|
||||
try {
|
||||
const url = new URL(host);
|
||||
const port = Number(url.port);
|
||||
return Number.isFinite(port) && port > 0 ? port : fallbackPort;
|
||||
} catch {
|
||||
const match = host.match(/:(\d+)/);
|
||||
if (!match) {
|
||||
return fallbackPort;
|
||||
}
|
||||
const port = parseInt(match[1], 10);
|
||||
return Number.isFinite(port) && port > 0 ? port : fallbackPort;
|
||||
}
|
||||
}
|
||||
|
||||
export function resolveGemmaConfig(fallbackPort: number): GemmaConfigStatus {
|
||||
let settingsEnabled = false;
|
||||
let configuredPort = fallbackPort;
|
||||
const configuredBinaryPath = getUserConfiguredBinaryPath();
|
||||
try {
|
||||
const settings = loadSettings(process.cwd());
|
||||
const gemmaSettings = settings.merged.experimental?.gemmaModelRouter;
|
||||
settingsEnabled = gemmaSettings?.enabled === true;
|
||||
configuredPort = parsePortFromHost(
|
||||
gemmaSettings?.classifier?.host,
|
||||
fallbackPort,
|
||||
);
|
||||
} catch {
|
||||
// ignore — settings may fail to load outside a workspace
|
||||
}
|
||||
return { settingsEnabled, configuredPort, configuredBinaryPath };
|
||||
}
|
||||
|
||||
export function detectPlatform(): PlatformInfo | null {
|
||||
const key = `${process.platform}-${process.arch}`;
|
||||
const binaryName = PLATFORM_BINARY_MAP[key];
|
||||
if (!binaryName) {
|
||||
return null;
|
||||
}
|
||||
return { key, binaryName };
|
||||
}
|
||||
|
||||
export function getBinaryPath(binaryName?: string): string | null {
|
||||
const configuredBinaryPath = getUserConfiguredBinaryPath();
|
||||
if (configuredBinaryPath) {
|
||||
return configuredBinaryPath;
|
||||
}
|
||||
|
||||
const name = binaryName ?? detectPlatform()?.binaryName;
|
||||
if (!name) return null;
|
||||
return path.join(getLiteRtBinDir(), name);
|
||||
}
|
||||
|
||||
export function getBinaryDownloadUrl(binaryName: string): string {
|
||||
return `${LITERT_RELEASE_BASE_URL}/${LITERT_RELEASE_VERSION}/${binaryName}`;
|
||||
}
|
||||
|
||||
export function isBinaryInstalled(binaryPath = getBinaryPath()): boolean {
|
||||
if (!binaryPath) return false;
|
||||
return fs.existsSync(binaryPath);
|
||||
}
|
||||
|
||||
export function isModelDownloaded(binaryPath: string): boolean {
|
||||
try {
|
||||
const output = execFileSync(binaryPath, ['list'], {
|
||||
encoding: 'utf-8',
|
||||
timeout: 10000,
|
||||
});
|
||||
return output.includes(GEMMA_MODEL_NAME);
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export async function isServerRunning(port: number): Promise<boolean> {
|
||||
try {
|
||||
const controller = new AbortController();
|
||||
const timeout = setTimeout(
|
||||
() => controller.abort(),
|
||||
HEALTH_CHECK_TIMEOUT_MS,
|
||||
);
|
||||
const response = await fetch(
|
||||
`http://localhost:${port}/${LITERT_API_VERSION}/models/${GEMMA_MODEL_NAME}:generateContent`,
|
||||
{ method: 'POST', signal: controller.signal },
|
||||
);
|
||||
clearTimeout(timeout);
|
||||
// A 400 (bad request) confirms the route exists — the server recognises
|
||||
// the model endpoint. Only a 404 means "wrong server / wrong model".
|
||||
return response.status !== 404;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function isLiteRtServerProcessInfo(
|
||||
value: unknown,
|
||||
): value is LiteRtServerProcessInfo {
|
||||
if (!value || typeof value !== 'object') {
|
||||
return false;
|
||||
}
|
||||
|
||||
const isPositiveInteger = (candidate: unknown): candidate is number =>
|
||||
typeof candidate === 'number' &&
|
||||
Number.isInteger(candidate) &&
|
||||
candidate > 0;
|
||||
const isNonEmptyString = (candidate: unknown): candidate is string =>
|
||||
typeof candidate === 'string' && candidate.length > 0;
|
||||
|
||||
const pid: unknown = Object.getOwnPropertyDescriptor(value, 'pid')?.value;
|
||||
if (!isPositiveInteger(pid)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const binaryPath: unknown = Object.getOwnPropertyDescriptor(
|
||||
value,
|
||||
'binaryPath',
|
||||
)?.value;
|
||||
if (binaryPath !== undefined && !isNonEmptyString(binaryPath)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const port: unknown = Object.getOwnPropertyDescriptor(value, 'port')?.value;
|
||||
if (port !== undefined && !isPositiveInteger(port)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
export function readServerProcessInfo(): LiteRtServerProcessInfo | null {
|
||||
const pidPath = getPidFilePath();
|
||||
try {
|
||||
const content = fs.readFileSync(pidPath, 'utf-8').trim();
|
||||
if (!content) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (/^\d+$/.test(content)) {
|
||||
return { pid: parseInt(content, 10) };
|
||||
}
|
||||
|
||||
const parsed = JSON.parse(content) as unknown;
|
||||
return isLiteRtServerProcessInfo(parsed) ? parsed : null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export function writeServerProcessInfo(
|
||||
processInfo: LiteRtServerProcessInfo,
|
||||
): void {
|
||||
fs.writeFileSync(getPidFilePath(), JSON.stringify(processInfo), 'utf-8');
|
||||
}
|
||||
|
||||
export function readServerPid(): number | null {
|
||||
return readServerProcessInfo()?.pid ?? null;
|
||||
}
|
||||
|
||||
function normalizeProcessValue(value: string): string {
|
||||
const normalized = value.replace(/\0/g, ' ').trim();
|
||||
if (process.platform === 'win32') {
|
||||
return normalized.replace(/\\/g, '/').replace(/\s+/g, ' ').toLowerCase();
|
||||
}
|
||||
return normalized.replace(/\s+/g, ' ');
|
||||
}
|
||||
|
||||
function readProcessCommandLine(pid: number): string | null {
|
||||
try {
|
||||
if (process.platform === 'linux') {
|
||||
const output = fs.readFileSync(`/proc/${pid}/cmdline`, 'utf-8');
|
||||
return output.trim() ? output : null;
|
||||
}
|
||||
|
||||
if (process.platform === 'win32') {
|
||||
const output = execFileSync(
|
||||
'powershell.exe',
|
||||
[
|
||||
'-NoProfile',
|
||||
'-Command',
|
||||
`(Get-CimInstance Win32_Process -Filter "ProcessId = ${pid}").CommandLine`,
|
||||
],
|
||||
{
|
||||
encoding: 'utf-8',
|
||||
timeout: 5000,
|
||||
},
|
||||
);
|
||||
return output.trim() || null;
|
||||
}
|
||||
|
||||
const output = execFileSync('ps', ['-p', String(pid), '-o', 'command='], {
|
||||
encoding: 'utf-8',
|
||||
timeout: 5000,
|
||||
});
|
||||
return output.trim() || null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export function isExpectedLiteRtServerCommand(
|
||||
commandLine: string,
|
||||
options: {
|
||||
binaryPath?: string | null;
|
||||
port?: number;
|
||||
},
|
||||
): boolean {
|
||||
const normalizedCommandLine = normalizeProcessValue(commandLine);
|
||||
if (!normalizedCommandLine) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!/(^|\s|")serve(\s|$)/.test(normalizedCommandLine)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (
|
||||
options.port !== undefined &&
|
||||
!normalizedCommandLine.includes(`--port=${options.port}`)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!options.binaryPath) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const normalizedBinaryPath = normalizeProcessValue(options.binaryPath);
|
||||
const normalizedBinaryName = normalizeProcessValue(
|
||||
path.basename(options.binaryPath),
|
||||
);
|
||||
return (
|
||||
normalizedCommandLine.includes(normalizedBinaryPath) ||
|
||||
normalizedCommandLine.includes(normalizedBinaryName)
|
||||
);
|
||||
}
|
||||
|
||||
export function isExpectedLiteRtServerProcess(
|
||||
pid: number,
|
||||
options: {
|
||||
binaryPath?: string | null;
|
||||
port?: number;
|
||||
},
|
||||
): boolean {
|
||||
const commandLine = readProcessCommandLine(pid);
|
||||
if (!commandLine) {
|
||||
return false;
|
||||
}
|
||||
return isExpectedLiteRtServerCommand(commandLine, options);
|
||||
}
|
||||
|
||||
export function isProcessRunning(pid: number): boolean {
|
||||
try {
|
||||
process.kill(pid, 0);
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -1,60 +0,0 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2026 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import fs from 'node:fs';
|
||||
import os from 'node:os';
|
||||
import path from 'node:path';
|
||||
import { afterEach, describe, expect, it } from 'vitest';
|
||||
import { PLATFORM_BINARY_MAP, PLATFORM_BINARY_SHA256 } from './constants.js';
|
||||
import { computeFileSha256, verifyFileSha256 } from './setup.js';
|
||||
|
||||
describe('gemma setup checksum helpers', () => {
|
||||
const tempFiles: string[] = [];
|
||||
|
||||
afterEach(async () => {
|
||||
await Promise.all(
|
||||
tempFiles
|
||||
.splice(0)
|
||||
.map((filePath) => fs.promises.rm(filePath, { force: true })),
|
||||
);
|
||||
});
|
||||
|
||||
it('has a pinned checksum for every supported LiteRT binary', () => {
|
||||
expect(Object.keys(PLATFORM_BINARY_SHA256).sort()).toEqual(
|
||||
Object.values(PLATFORM_BINARY_MAP).sort(),
|
||||
);
|
||||
});
|
||||
|
||||
it('computes the sha256 for a downloaded file', async () => {
|
||||
const filePath = path.join(
|
||||
os.tmpdir(),
|
||||
`gemma-setup-${Date.now()}-${Math.random().toString(36).slice(2)}`,
|
||||
);
|
||||
tempFiles.push(filePath);
|
||||
await fs.promises.writeFile(filePath, 'hello world', 'utf-8');
|
||||
|
||||
await expect(computeFileSha256(filePath)).resolves.toBe(
|
||||
'b94d27b9934d3e08a52e52d7da7dabfac484efe37a5380ee9088f7ace2efcde9',
|
||||
);
|
||||
});
|
||||
|
||||
it('verifies whether a file matches the expected sha256', async () => {
|
||||
const filePath = path.join(
|
||||
os.tmpdir(),
|
||||
`gemma-setup-${Date.now()}-${Math.random().toString(36).slice(2)}`,
|
||||
);
|
||||
tempFiles.push(filePath);
|
||||
await fs.promises.writeFile(filePath, 'hello world', 'utf-8');
|
||||
|
||||
await expect(
|
||||
verifyFileSha256(
|
||||
filePath,
|
||||
'b94d27b9934d3e08a52e52d7da7dabfac484efe37a5380ee9088f7ace2efcde9',
|
||||
),
|
||||
).resolves.toBe(true);
|
||||
await expect(verifyFileSha256(filePath, 'deadbeef')).resolves.toBe(false);
|
||||
});
|
||||
});
|
||||
@@ -1,504 +0,0 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2026 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import type { CommandModule } from 'yargs';
|
||||
import { createHash } from 'node:crypto';
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import { execFileSync, spawn as nodeSpawn } from 'node:child_process';
|
||||
import chalk from 'chalk';
|
||||
import { debugLogger } from '@google/gemini-cli-core';
|
||||
import { loadSettings, SettingScope } from '../../config/settings.js';
|
||||
import { exitCli } from '../utils.js';
|
||||
import {
|
||||
DEFAULT_PORT,
|
||||
GEMMA_MODEL_NAME,
|
||||
PLATFORM_BINARY_SHA256,
|
||||
} from './constants.js';
|
||||
import {
|
||||
detectPlatform,
|
||||
getBinaryDownloadUrl,
|
||||
getBinaryPath,
|
||||
isBinaryInstalled,
|
||||
isModelDownloaded,
|
||||
} from './platform.js';
|
||||
import { startServer } from './start.js';
|
||||
import readline from 'node:readline';
|
||||
|
||||
const log = (msg: string) => debugLogger.log(msg);
|
||||
const logError = (msg: string) => debugLogger.error(msg);
|
||||
|
||||
async function promptYesNo(question: string): Promise<boolean> {
|
||||
const rl = readline.createInterface({
|
||||
input: process.stdin,
|
||||
output: process.stdout,
|
||||
});
|
||||
return new Promise((resolve) => {
|
||||
rl.question(`${question} (y/N): `, (answer) => {
|
||||
rl.close();
|
||||
resolve(
|
||||
answer.trim().toLowerCase() === 'y' ||
|
||||
answer.trim().toLowerCase() === 'yes',
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function formatBytes(bytes: number): string {
|
||||
if (bytes < 1024) return `${bytes} B`;
|
||||
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
|
||||
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
||||
}
|
||||
|
||||
function renderProgress(downloaded: number, total: number | null): void {
|
||||
const barWidth = 30;
|
||||
if (total && total > 0) {
|
||||
const pct = Math.min(downloaded / total, 1);
|
||||
const filled = Math.round(barWidth * pct);
|
||||
const bar = '█'.repeat(filled) + '░'.repeat(barWidth - filled);
|
||||
const pctStr = (pct * 100).toFixed(0).padStart(3);
|
||||
process.stderr.write(
|
||||
`\r [${bar}] ${pctStr}% ${formatBytes(downloaded)} / ${formatBytes(total)}`,
|
||||
);
|
||||
} else {
|
||||
process.stderr.write(`\r Downloaded ${formatBytes(downloaded)}`);
|
||||
}
|
||||
}
|
||||
|
||||
async function downloadFile(url: string, destPath: string): Promise<void> {
|
||||
const tmpPath = destPath + '.downloading';
|
||||
if (fs.existsSync(tmpPath)) {
|
||||
fs.unlinkSync(tmpPath);
|
||||
}
|
||||
|
||||
const response = await fetch(url, { redirect: 'follow' });
|
||||
if (!response.ok) {
|
||||
throw new Error(
|
||||
`Download failed: HTTP ${response.status} ${response.statusText}`,
|
||||
);
|
||||
}
|
||||
if (!response.body) {
|
||||
throw new Error('Download failed: No response body');
|
||||
}
|
||||
|
||||
const contentLength = response.headers.get('content-length');
|
||||
const totalBytes = contentLength ? parseInt(contentLength, 10) : null;
|
||||
let downloadedBytes = 0;
|
||||
|
||||
const fileStream = fs.createWriteStream(tmpPath);
|
||||
const reader = response.body.getReader();
|
||||
|
||||
try {
|
||||
for (;;) {
|
||||
const { done, value } = await reader.read();
|
||||
if (done) break;
|
||||
const writeOk = fileStream.write(value);
|
||||
if (!writeOk) {
|
||||
await new Promise<void>((resolve) => fileStream.once('drain', resolve));
|
||||
}
|
||||
downloadedBytes += value.byteLength;
|
||||
renderProgress(downloadedBytes, totalBytes);
|
||||
}
|
||||
} finally {
|
||||
fileStream.end();
|
||||
process.stderr.write('\r' + ' '.repeat(80) + '\r');
|
||||
}
|
||||
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
fileStream.on('finish', resolve);
|
||||
fileStream.on('error', reject);
|
||||
});
|
||||
|
||||
fs.renameSync(tmpPath, destPath);
|
||||
}
|
||||
|
||||
export async function computeFileSha256(filePath: string): Promise<string> {
|
||||
const hash = createHash('sha256');
|
||||
const fileStream = fs.createReadStream(filePath);
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
fileStream.on('data', (chunk) => {
|
||||
hash.update(chunk);
|
||||
});
|
||||
fileStream.on('error', reject);
|
||||
fileStream.on('end', () => {
|
||||
resolve(hash.digest('hex'));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
export async function verifyFileSha256(
|
||||
filePath: string,
|
||||
expectedHash: string,
|
||||
): Promise<boolean> {
|
||||
const actualHash = await computeFileSha256(filePath);
|
||||
return actualHash === expectedHash;
|
||||
}
|
||||
|
||||
function spawnInherited(command: string, args: string[]): Promise<number> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const child = nodeSpawn(command, args, {
|
||||
stdio: 'inherit',
|
||||
});
|
||||
child.on('close', (code) => resolve(code ?? 1));
|
||||
child.on('error', reject);
|
||||
});
|
||||
}
|
||||
|
||||
interface SetupArgs {
|
||||
port: number;
|
||||
skipModel: boolean;
|
||||
start: boolean;
|
||||
force: boolean;
|
||||
consent: boolean;
|
||||
}
|
||||
|
||||
async function handleSetup(argv: SetupArgs): Promise<number> {
|
||||
const { port, force } = argv;
|
||||
let settingsUpdated = false;
|
||||
let serverStarted = false;
|
||||
let autoStartServer = true;
|
||||
|
||||
log('');
|
||||
log(chalk.bold('Gemma Local Model Routing Setup'));
|
||||
log(chalk.dim('─'.repeat(40)));
|
||||
log('');
|
||||
|
||||
const platform = detectPlatform();
|
||||
if (!platform) {
|
||||
logError(
|
||||
chalk.red(`Unsupported platform: ${process.platform}-${process.arch}`),
|
||||
);
|
||||
logError(
|
||||
'LiteRT-LM binaries are available for: macOS (ARM64), Linux (x86_64), Windows (x86_64)',
|
||||
);
|
||||
return 1;
|
||||
}
|
||||
log(chalk.dim(` Platform: ${platform.key} → ${platform.binaryName}`));
|
||||
|
||||
if (!argv.consent) {
|
||||
log('');
|
||||
log('This will download and install the LiteRT-LM runtime and the');
|
||||
log(
|
||||
`Gemma model (${GEMMA_MODEL_NAME}, ~1 GB). By proceeding, you agree to the`,
|
||||
);
|
||||
log('Gemma Terms of Use: https://ai.google.dev/gemma/terms');
|
||||
log('');
|
||||
|
||||
const accepted = await promptYesNo('Do you want to continue?');
|
||||
if (!accepted) {
|
||||
log('Setup cancelled.');
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
const binaryPath = getBinaryPath(platform.binaryName)!;
|
||||
const alreadyInstalled = isBinaryInstalled();
|
||||
|
||||
if (alreadyInstalled && !force) {
|
||||
log('');
|
||||
log(chalk.green(' ✓ LiteRT-LM binary already installed at:'));
|
||||
log(chalk.dim(` ${binaryPath}`));
|
||||
} else {
|
||||
log('');
|
||||
log(' Downloading LiteRT-LM binary...');
|
||||
const downloadUrl = getBinaryDownloadUrl(platform.binaryName);
|
||||
debugLogger.log(`Downloading from: ${downloadUrl}`);
|
||||
|
||||
try {
|
||||
const binDir = path.dirname(binaryPath);
|
||||
fs.mkdirSync(binDir, { recursive: true });
|
||||
await downloadFile(downloadUrl, binaryPath);
|
||||
log(chalk.green(' ✓ Binary downloaded successfully'));
|
||||
} catch (error) {
|
||||
logError(
|
||||
chalk.red(
|
||||
` ✗ Failed to download binary: ${error instanceof Error ? error.message : String(error)}`,
|
||||
),
|
||||
);
|
||||
logError(' Check your internet connection and try again.');
|
||||
return 1;
|
||||
}
|
||||
|
||||
const expectedHash = PLATFORM_BINARY_SHA256[platform.binaryName];
|
||||
if (!expectedHash) {
|
||||
logError(
|
||||
chalk.red(
|
||||
` ✗ No checksum is configured for ${platform.binaryName}. Refusing to install the binary.`,
|
||||
),
|
||||
);
|
||||
try {
|
||||
fs.rmSync(binaryPath, { force: true });
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
try {
|
||||
const checksumVerified = await verifyFileSha256(binaryPath, expectedHash);
|
||||
if (!checksumVerified) {
|
||||
logError(
|
||||
chalk.red(
|
||||
' ✗ Downloaded binary checksum did not match the expected release hash.',
|
||||
),
|
||||
);
|
||||
try {
|
||||
fs.rmSync(binaryPath, { force: true });
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
log(chalk.green(' ✓ Binary checksum verified'));
|
||||
} catch (error) {
|
||||
logError(
|
||||
chalk.red(
|
||||
` ✗ Failed to verify binary checksum: ${error instanceof Error ? error.message : String(error)}`,
|
||||
),
|
||||
);
|
||||
try {
|
||||
fs.rmSync(binaryPath, { force: true });
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (process.platform !== 'win32') {
|
||||
try {
|
||||
fs.chmodSync(binaryPath, 0o755);
|
||||
} catch (error) {
|
||||
logError(
|
||||
chalk.red(
|
||||
` ✗ Failed to set executable permission: ${error instanceof Error ? error.message : String(error)}`,
|
||||
),
|
||||
);
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
if (process.platform === 'darwin') {
|
||||
try {
|
||||
execFileSync('xattr', ['-d', 'com.apple.quarantine', binaryPath], {
|
||||
stdio: 'ignore',
|
||||
});
|
||||
log(chalk.green(' ✓ macOS quarantine attribute removed'));
|
||||
} catch {
|
||||
// Expected if the attribute doesn't exist.
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!argv.skipModel) {
|
||||
const modelAlreadyDownloaded = isModelDownloaded(binaryPath);
|
||||
if (modelAlreadyDownloaded && !force) {
|
||||
log('');
|
||||
log(chalk.green(` ✓ Model ${GEMMA_MODEL_NAME} already downloaded`));
|
||||
} else {
|
||||
log('');
|
||||
log(` Downloading model ${GEMMA_MODEL_NAME}...`);
|
||||
log(chalk.dim(' You may be prompted to accept the Gemma Terms of Use.'));
|
||||
log('');
|
||||
|
||||
const exitCode = await spawnInherited(binaryPath, [
|
||||
'pull',
|
||||
GEMMA_MODEL_NAME,
|
||||
]);
|
||||
if (exitCode !== 0) {
|
||||
logError('');
|
||||
logError(
|
||||
chalk.red(` ✗ Model download failed (exit code ${exitCode})`),
|
||||
);
|
||||
return 1;
|
||||
}
|
||||
log('');
|
||||
log(chalk.green(` ✓ Model ${GEMMA_MODEL_NAME} downloaded`));
|
||||
}
|
||||
}
|
||||
|
||||
log('');
|
||||
log(' Configuring settings...');
|
||||
try {
|
||||
const settings = loadSettings(process.cwd());
|
||||
|
||||
// User scope: security-sensitive settings that must not be overridable
|
||||
// by workspace configs (prevents arbitrary binary execution).
|
||||
const existingUserGemma =
|
||||
settings.forScope(SettingScope.User).settings.experimental
|
||||
?.gemmaModelRouter ?? {};
|
||||
autoStartServer = existingUserGemma.autoStartServer ?? true;
|
||||
const existingUserExperimental =
|
||||
settings.forScope(SettingScope.User).settings.experimental ?? {};
|
||||
settings.setValue(SettingScope.User, 'experimental', {
|
||||
...existingUserExperimental,
|
||||
gemmaModelRouter: {
|
||||
autoStartServer,
|
||||
...(existingUserGemma.binaryPath !== undefined
|
||||
? { binaryPath: existingUserGemma.binaryPath }
|
||||
: {}),
|
||||
},
|
||||
});
|
||||
|
||||
// Workspace scope: project-isolated settings so the local model only
|
||||
// runs for this specific project, saving resources globally.
|
||||
const existingWorkspaceGemma =
|
||||
settings.forScope(SettingScope.Workspace).settings.experimental
|
||||
?.gemmaModelRouter ?? {};
|
||||
const existingWorkspaceExperimental =
|
||||
settings.forScope(SettingScope.Workspace).settings.experimental ?? {};
|
||||
settings.setValue(SettingScope.Workspace, 'experimental', {
|
||||
...existingWorkspaceExperimental,
|
||||
gemmaModelRouter: {
|
||||
...existingWorkspaceGemma,
|
||||
enabled: true,
|
||||
classifier: {
|
||||
...existingWorkspaceGemma.classifier,
|
||||
host: `http://localhost:${port}`,
|
||||
model: GEMMA_MODEL_NAME,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
log(chalk.green(' ✓ Settings updated'));
|
||||
log(chalk.dim(' User (~/.gemini/settings.json): autoStartServer'));
|
||||
log(
|
||||
chalk.dim(' Workspace (.gemini/settings.json): enabled, classifier'),
|
||||
);
|
||||
settingsUpdated = true;
|
||||
} catch (error) {
|
||||
logError(
|
||||
chalk.red(
|
||||
` ✗ Failed to update settings: ${error instanceof Error ? error.message : String(error)}`,
|
||||
),
|
||||
);
|
||||
logError(
|
||||
' You can manually add the configuration to ~/.gemini/settings.json',
|
||||
);
|
||||
}
|
||||
|
||||
if (argv.start) {
|
||||
log('');
|
||||
log(' Starting LiteRT server...');
|
||||
serverStarted = await startServer(binaryPath, port);
|
||||
if (serverStarted) {
|
||||
log(chalk.green(` ✓ Server started on port ${port}`));
|
||||
} else {
|
||||
log(
|
||||
chalk.yellow(
|
||||
` ! Server may not have started correctly. Check: gemini gemma status`,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const routingActive = settingsUpdated && serverStarted;
|
||||
const setupSucceeded = settingsUpdated && (!argv.start || serverStarted);
|
||||
log('');
|
||||
log(chalk.dim('─'.repeat(40)));
|
||||
if (routingActive) {
|
||||
log(chalk.bold.green(' Setup complete! Local model routing is active.'));
|
||||
} else if (settingsUpdated) {
|
||||
log(
|
||||
chalk.bold.green(' Setup complete! Local model routing is configured.'),
|
||||
);
|
||||
} else {
|
||||
log(
|
||||
chalk.bold.yellow(
|
||||
' Setup incomplete. Manual settings changes are still required.',
|
||||
),
|
||||
);
|
||||
}
|
||||
log('');
|
||||
log(' How it works: Every request is classified by the local Gemma model.');
|
||||
log(
|
||||
' Simple tasks (file reads, quick edits) route to ' +
|
||||
chalk.cyan('Flash') +
|
||||
' for speed.',
|
||||
);
|
||||
log(
|
||||
' Complex tasks (debugging, architecture) route to ' +
|
||||
chalk.cyan('Pro') +
|
||||
' for quality.',
|
||||
);
|
||||
log(' This happens automatically — just use the CLI as usual.');
|
||||
log('');
|
||||
if (!settingsUpdated) {
|
||||
log(
|
||||
chalk.yellow(
|
||||
' Fix the settings update above, then rerun "gemini gemma status".',
|
||||
),
|
||||
);
|
||||
log('');
|
||||
} else if (!argv.start) {
|
||||
log(chalk.yellow(' Note: Run "gemini gemma start" to start the server.'));
|
||||
if (autoStartServer) {
|
||||
log(
|
||||
chalk.yellow(
|
||||
' Or restart the CLI to auto-start it on the next launch.',
|
||||
),
|
||||
);
|
||||
}
|
||||
log('');
|
||||
} else if (!serverStarted) {
|
||||
log(
|
||||
chalk.yellow(
|
||||
' Review the server logs and rerun "gemini gemma start" after fixing the issue.',
|
||||
),
|
||||
);
|
||||
log('');
|
||||
}
|
||||
log(' Useful commands:');
|
||||
log(chalk.dim(' gemini gemma status Check routing status'));
|
||||
log(chalk.dim(' gemini gemma start Start the LiteRT server'));
|
||||
log(chalk.dim(' gemini gemma stop Stop the LiteRT server'));
|
||||
log(chalk.dim(' /gemma Check status inside a session'));
|
||||
log('');
|
||||
|
||||
return setupSucceeded ? 0 : 1;
|
||||
}
|
||||
|
||||
export const setupCommand: CommandModule = {
|
||||
command: 'setup',
|
||||
describe: 'Download and configure Gemma local model routing',
|
||||
builder: (yargs) =>
|
||||
yargs
|
||||
.option('port', {
|
||||
type: 'number',
|
||||
default: DEFAULT_PORT,
|
||||
description: 'Port for the LiteRT server',
|
||||
})
|
||||
.option('skip-model', {
|
||||
type: 'boolean',
|
||||
default: false,
|
||||
description: 'Skip model download (binary only)',
|
||||
})
|
||||
.option('start', {
|
||||
type: 'boolean',
|
||||
default: true,
|
||||
description: 'Start the server after setup',
|
||||
})
|
||||
.option('force', {
|
||||
type: 'boolean',
|
||||
default: false,
|
||||
description: 'Re-download binary and model even if already present',
|
||||
})
|
||||
.option('consent', {
|
||||
type: 'boolean',
|
||||
default: false,
|
||||
description: 'Skip interactive consent prompt (implies acceptance)',
|
||||
}),
|
||||
handler: async (argv) => {
|
||||
const exitCode = await handleSetup({
|
||||
port: Number(argv['port']),
|
||||
skipModel: Boolean(argv['skipModel']),
|
||||
start: Boolean(argv['start']),
|
||||
force: Boolean(argv['force']),
|
||||
consent: Boolean(argv['consent']),
|
||||
});
|
||||
await exitCli(exitCode);
|
||||
},
|
||||
};
|
||||
@@ -1,123 +0,0 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2026 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import type { CommandModule } from 'yargs';
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import { spawn } from 'node:child_process';
|
||||
import chalk from 'chalk';
|
||||
import { debugLogger } from '@google/gemini-cli-core';
|
||||
import { exitCli } from '../utils.js';
|
||||
import {
|
||||
DEFAULT_PORT,
|
||||
getPidFilePath,
|
||||
getLogFilePath,
|
||||
getLiteRtBinDir,
|
||||
SERVER_START_WAIT_MS,
|
||||
} from './constants.js';
|
||||
import {
|
||||
getBinaryPath,
|
||||
isBinaryInstalled,
|
||||
isServerRunning,
|
||||
resolveGemmaConfig,
|
||||
writeServerProcessInfo,
|
||||
} from './platform.js';
|
||||
|
||||
export async function startServer(
|
||||
binaryPath: string,
|
||||
port: number,
|
||||
): Promise<boolean> {
|
||||
const alreadyRunning = await isServerRunning(port);
|
||||
if (alreadyRunning) {
|
||||
debugLogger.log(`LiteRT server already running on port ${port}`);
|
||||
return true;
|
||||
}
|
||||
|
||||
const logPath = getLogFilePath();
|
||||
fs.mkdirSync(getLiteRtBinDir(), { recursive: true });
|
||||
const tmpDir = path.dirname(getPidFilePath());
|
||||
fs.mkdirSync(tmpDir, { recursive: true });
|
||||
|
||||
const logFd = fs.openSync(logPath, 'a');
|
||||
|
||||
try {
|
||||
const child = spawn(binaryPath, ['serve', `--port=${port}`, '--verbose'], {
|
||||
detached: true,
|
||||
stdio: ['ignore', logFd, logFd],
|
||||
});
|
||||
|
||||
if (child.pid) {
|
||||
writeServerProcessInfo({
|
||||
pid: child.pid,
|
||||
binaryPath,
|
||||
port,
|
||||
});
|
||||
}
|
||||
|
||||
child.unref();
|
||||
} finally {
|
||||
fs.closeSync(logFd);
|
||||
}
|
||||
|
||||
await new Promise((resolve) => setTimeout(resolve, SERVER_START_WAIT_MS));
|
||||
return isServerRunning(port);
|
||||
}
|
||||
|
||||
export const startCommand: CommandModule = {
|
||||
command: 'start',
|
||||
describe: 'Start the LiteRT-LM server',
|
||||
builder: (yargs) =>
|
||||
yargs.option('port', {
|
||||
type: 'number',
|
||||
description: 'Port for the LiteRT server',
|
||||
}),
|
||||
handler: async (argv) => {
|
||||
let port: number | undefined;
|
||||
if (argv['port'] !== undefined) {
|
||||
port = Number(argv['port']);
|
||||
}
|
||||
|
||||
if (!port) {
|
||||
const { configuredPort } = resolveGemmaConfig(DEFAULT_PORT);
|
||||
port = configuredPort;
|
||||
}
|
||||
|
||||
const binaryPath = getBinaryPath();
|
||||
if (!binaryPath || !isBinaryInstalled(binaryPath)) {
|
||||
debugLogger.error(
|
||||
chalk.red(
|
||||
'LiteRT-LM binary not found. Run "gemini gemma setup" first.',
|
||||
),
|
||||
);
|
||||
await exitCli(1);
|
||||
return;
|
||||
}
|
||||
|
||||
const alreadyRunning = await isServerRunning(port);
|
||||
if (alreadyRunning) {
|
||||
debugLogger.log(
|
||||
chalk.green(`LiteRT server is already running on port ${port}.`),
|
||||
);
|
||||
await exitCli(0);
|
||||
return;
|
||||
}
|
||||
|
||||
debugLogger.log(`Starting LiteRT server on port ${port}...`);
|
||||
|
||||
const started = await startServer(binaryPath, port);
|
||||
if (started) {
|
||||
debugLogger.log(chalk.green(`LiteRT server started on port ${port}.`));
|
||||
debugLogger.log(chalk.dim(`Logs: ${getLogFilePath()}`));
|
||||
await exitCli(0);
|
||||
} else {
|
||||
debugLogger.error(
|
||||
chalk.red('Server may not have started correctly. Check logs:'),
|
||||
);
|
||||
debugLogger.error(chalk.dim(` ${getLogFilePath()}`));
|
||||
await exitCli(1);
|
||||
}
|
||||
},
|
||||
};
|
||||
@@ -1,165 +0,0 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2026 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import type { CommandModule } from 'yargs';
|
||||
import chalk from 'chalk';
|
||||
import { DEFAULT_PORT, GEMMA_MODEL_NAME } from './constants.js';
|
||||
import {
|
||||
detectPlatform,
|
||||
getBinaryPath,
|
||||
isBinaryInstalled,
|
||||
isModelDownloaded,
|
||||
isServerRunning,
|
||||
readServerPid,
|
||||
isProcessRunning,
|
||||
resolveGemmaConfig,
|
||||
} from './platform.js';
|
||||
import { exitCli } from '../utils.js';
|
||||
|
||||
export interface GemmaStatusResult {
|
||||
binaryInstalled: boolean;
|
||||
binaryPath: string | null;
|
||||
modelDownloaded: boolean;
|
||||
serverRunning: boolean;
|
||||
serverPid: number | null;
|
||||
settingsEnabled: boolean;
|
||||
port: number;
|
||||
allPassing: boolean;
|
||||
}
|
||||
|
||||
export async function checkGemmaStatus(
|
||||
port?: number,
|
||||
): Promise<GemmaStatusResult> {
|
||||
const { settingsEnabled, configuredPort } = resolveGemmaConfig(DEFAULT_PORT);
|
||||
|
||||
const effectivePort = port ?? configuredPort;
|
||||
const binaryPath = getBinaryPath();
|
||||
const binaryInstalled = isBinaryInstalled(binaryPath);
|
||||
const modelDownloaded =
|
||||
binaryInstalled && binaryPath ? isModelDownloaded(binaryPath) : false;
|
||||
const serverRunning = await isServerRunning(effectivePort);
|
||||
const pid = readServerPid();
|
||||
const serverPid = pid && isProcessRunning(pid) ? pid : null;
|
||||
|
||||
const allPassing =
|
||||
binaryInstalled && modelDownloaded && serverRunning && settingsEnabled;
|
||||
|
||||
return {
|
||||
binaryInstalled,
|
||||
binaryPath,
|
||||
modelDownloaded,
|
||||
serverRunning,
|
||||
serverPid,
|
||||
settingsEnabled,
|
||||
port: effectivePort,
|
||||
allPassing,
|
||||
};
|
||||
}
|
||||
|
||||
export function formatGemmaStatus(status: GemmaStatusResult): string {
|
||||
const check = (ok: boolean) => (ok ? chalk.green('✓') : chalk.red('✗'));
|
||||
|
||||
const lines: string[] = [
|
||||
'',
|
||||
chalk.bold('Gemma Local Model Routing Status'),
|
||||
chalk.dim('─'.repeat(40)),
|
||||
'',
|
||||
];
|
||||
|
||||
if (status.binaryInstalled) {
|
||||
lines.push(` Binary: ${check(true)} Installed (${status.binaryPath})`);
|
||||
} else {
|
||||
const platform = detectPlatform();
|
||||
if (platform) {
|
||||
lines.push(` Binary: ${check(false)} Not installed`);
|
||||
lines.push(chalk.dim(` Run: gemini gemma setup`));
|
||||
} else {
|
||||
lines.push(
|
||||
` Binary: ${check(false)} Unsupported platform (${process.platform}-${process.arch})`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (status.modelDownloaded) {
|
||||
lines.push(` Model: ${check(true)} ${GEMMA_MODEL_NAME} downloaded`);
|
||||
} else {
|
||||
lines.push(` Model: ${check(false)} ${GEMMA_MODEL_NAME} not found`);
|
||||
if (status.binaryInstalled) {
|
||||
lines.push(
|
||||
chalk.dim(
|
||||
` Run: ${status.binaryPath} pull ${GEMMA_MODEL_NAME}`,
|
||||
),
|
||||
);
|
||||
} else {
|
||||
lines.push(chalk.dim(` Run: gemini gemma setup`));
|
||||
}
|
||||
}
|
||||
|
||||
if (status.serverRunning) {
|
||||
const pidInfo = status.serverPid ? ` (PID ${status.serverPid})` : '';
|
||||
lines.push(
|
||||
` Server: ${check(true)} Running on port ${status.port}${pidInfo}`,
|
||||
);
|
||||
} else {
|
||||
lines.push(
|
||||
` Server: ${check(false)} Not running on port ${status.port}`,
|
||||
);
|
||||
lines.push(chalk.dim(` Run: gemini gemma start`));
|
||||
}
|
||||
|
||||
if (status.settingsEnabled) {
|
||||
lines.push(` Settings: ${check(true)} Enabled in settings.json`);
|
||||
} else {
|
||||
lines.push(` Settings: ${check(false)} Not enabled in settings.json`);
|
||||
lines.push(
|
||||
chalk.dim(
|
||||
` Run: gemini gemma setup (auto-configures settings)`,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
lines.push('');
|
||||
|
||||
if (status.allPassing) {
|
||||
lines.push(chalk.green(' Routing is active — no action needed.'));
|
||||
lines.push('');
|
||||
lines.push(
|
||||
chalk.dim(
|
||||
' Simple requests → Flash (fast) | Complex requests → Pro (powerful)',
|
||||
),
|
||||
);
|
||||
lines.push(chalk.dim(' This happens automatically on every request.'));
|
||||
} else {
|
||||
lines.push(
|
||||
chalk.yellow(
|
||||
' Some checks failed. Run "gemini gemma setup" for guided installation.',
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
lines.push('');
|
||||
return lines.join('\n');
|
||||
}
|
||||
|
||||
export const statusCommand: CommandModule = {
|
||||
command: 'status',
|
||||
describe: 'Check Gemma local model routing status',
|
||||
builder: (yargs) =>
|
||||
yargs.option('port', {
|
||||
type: 'number',
|
||||
description: 'Port to check for the LiteRT server',
|
||||
}),
|
||||
handler: async (argv) => {
|
||||
let port: number | undefined;
|
||||
if (argv['port'] !== undefined) {
|
||||
port = Number(argv['port']);
|
||||
}
|
||||
const status = await checkGemmaStatus(port);
|
||||
const output = formatGemmaStatus(status);
|
||||
process.stdout.write(output);
|
||||
await exitCli(status.allPassing ? 0 : 1);
|
||||
},
|
||||
};
|
||||
@@ -1,112 +0,0 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2026 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import fs from 'node:fs';
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
const mockGetBinaryPath = vi.hoisted(() => vi.fn());
|
||||
const mockIsExpectedLiteRtServerProcess = vi.hoisted(() => vi.fn());
|
||||
const mockIsProcessRunning = vi.hoisted(() => vi.fn());
|
||||
const mockIsServerRunning = vi.hoisted(() => vi.fn());
|
||||
const mockReadServerPid = vi.hoisted(() => vi.fn());
|
||||
const mockReadServerProcessInfo = vi.hoisted(() => vi.fn());
|
||||
const mockResolveGemmaConfig = vi.hoisted(() => vi.fn());
|
||||
|
||||
vi.mock('@google/gemini-cli-core', async (importOriginal) => {
|
||||
const { mockCoreDebugLogger } = await import(
|
||||
'../../test-utils/mockDebugLogger.js'
|
||||
);
|
||||
return mockCoreDebugLogger(
|
||||
await importOriginal<typeof import('@google/gemini-cli-core')>(),
|
||||
{
|
||||
stripAnsi: false,
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
vi.mock('./constants.js', () => ({
|
||||
DEFAULT_PORT: 9379,
|
||||
getPidFilePath: vi.fn(() => '/tmp/litert-server.pid'),
|
||||
}));
|
||||
|
||||
vi.mock('./platform.js', () => ({
|
||||
getBinaryPath: mockGetBinaryPath,
|
||||
isExpectedLiteRtServerProcess: mockIsExpectedLiteRtServerProcess,
|
||||
isProcessRunning: mockIsProcessRunning,
|
||||
isServerRunning: mockIsServerRunning,
|
||||
readServerPid: mockReadServerPid,
|
||||
readServerProcessInfo: mockReadServerProcessInfo,
|
||||
resolveGemmaConfig: mockResolveGemmaConfig,
|
||||
}));
|
||||
|
||||
vi.mock('../utils.js', () => ({
|
||||
exitCli: vi.fn(),
|
||||
}));
|
||||
|
||||
import { stopServer } from './stop.js';
|
||||
|
||||
describe('gemma stop command', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
vi.useFakeTimers();
|
||||
mockGetBinaryPath.mockReturnValue('/custom/lit');
|
||||
mockResolveGemmaConfig.mockReturnValue({ configuredPort: 9379 });
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.useRealTimers();
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
it('refuses to signal a pid that does not match the expected LiteRT server', async () => {
|
||||
mockReadServerProcessInfo.mockReturnValue({
|
||||
pid: 1234,
|
||||
binaryPath: '/custom/lit',
|
||||
port: 8123,
|
||||
});
|
||||
mockIsProcessRunning.mockReturnValue(true);
|
||||
mockIsExpectedLiteRtServerProcess.mockReturnValue(false);
|
||||
|
||||
const killSpy = vi.spyOn(process, 'kill').mockImplementation(() => true);
|
||||
|
||||
await expect(stopServer(8123)).resolves.toBe('unexpected-process');
|
||||
expect(killSpy).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('stops the verified LiteRT server and removes the pid file', async () => {
|
||||
mockReadServerProcessInfo.mockReturnValue({
|
||||
pid: 1234,
|
||||
binaryPath: '/custom/lit',
|
||||
port: 8123,
|
||||
});
|
||||
mockIsProcessRunning.mockReturnValueOnce(true).mockReturnValueOnce(false);
|
||||
mockIsExpectedLiteRtServerProcess.mockReturnValue(true);
|
||||
|
||||
const unlinkSpy = vi.spyOn(fs, 'unlinkSync').mockImplementation(() => {});
|
||||
const killSpy = vi.spyOn(process, 'kill').mockImplementation(() => true);
|
||||
|
||||
const stopPromise = stopServer(8123);
|
||||
await vi.runAllTimersAsync();
|
||||
|
||||
await expect(stopPromise).resolves.toBe('stopped');
|
||||
expect(killSpy).toHaveBeenCalledWith(1234, 'SIGTERM');
|
||||
expect(unlinkSpy).toHaveBeenCalledWith('/tmp/litert-server.pid');
|
||||
});
|
||||
|
||||
it('cleans up a stale pid file when the recorded process is no longer running', async () => {
|
||||
mockReadServerProcessInfo.mockReturnValue({
|
||||
pid: 1234,
|
||||
binaryPath: '/custom/lit',
|
||||
port: 8123,
|
||||
});
|
||||
mockIsProcessRunning.mockReturnValue(false);
|
||||
|
||||
const unlinkSpy = vi.spyOn(fs, 'unlinkSync').mockImplementation(() => {});
|
||||
|
||||
await expect(stopServer(8123)).resolves.toBe('not-running');
|
||||
expect(unlinkSpy).toHaveBeenCalledWith('/tmp/litert-server.pid');
|
||||
});
|
||||
});
|
||||
@@ -1,155 +0,0 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2026 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import type { CommandModule } from 'yargs';
|
||||
import fs from 'node:fs';
|
||||
import chalk from 'chalk';
|
||||
import { debugLogger } from '@google/gemini-cli-core';
|
||||
import { exitCli } from '../utils.js';
|
||||
import { DEFAULT_PORT, getPidFilePath } from './constants.js';
|
||||
import {
|
||||
getBinaryPath,
|
||||
isExpectedLiteRtServerProcess,
|
||||
isProcessRunning,
|
||||
isServerRunning,
|
||||
readServerPid,
|
||||
readServerProcessInfo,
|
||||
resolveGemmaConfig,
|
||||
} from './platform.js';
|
||||
|
||||
export type StopServerResult =
|
||||
| 'stopped'
|
||||
| 'not-running'
|
||||
| 'unexpected-process'
|
||||
| 'failed';
|
||||
|
||||
export async function stopServer(
|
||||
expectedPort?: number,
|
||||
): Promise<StopServerResult> {
|
||||
const processInfo = readServerProcessInfo();
|
||||
const pidPath = getPidFilePath();
|
||||
|
||||
if (!processInfo) {
|
||||
return 'not-running';
|
||||
}
|
||||
|
||||
const { pid } = processInfo;
|
||||
if (!isProcessRunning(pid)) {
|
||||
debugLogger.log(
|
||||
`Stale PID file found (PID ${pid} is not running), removing ${pidPath}`,
|
||||
);
|
||||
try {
|
||||
fs.unlinkSync(pidPath);
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
return 'not-running';
|
||||
}
|
||||
|
||||
const binaryPath = processInfo.binaryPath ?? getBinaryPath();
|
||||
const port = processInfo.port ?? expectedPort;
|
||||
if (!isExpectedLiteRtServerProcess(pid, { binaryPath, port })) {
|
||||
debugLogger.warn(
|
||||
`Refusing to stop PID ${pid} because it does not match the expected LiteRT server process.`,
|
||||
);
|
||||
return 'unexpected-process';
|
||||
}
|
||||
|
||||
try {
|
||||
process.kill(pid, 'SIGTERM');
|
||||
} catch {
|
||||
return 'failed';
|
||||
}
|
||||
|
||||
await new Promise((resolve) => setTimeout(resolve, 1000));
|
||||
|
||||
if (isProcessRunning(pid)) {
|
||||
try {
|
||||
process.kill(pid, 'SIGKILL');
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
await new Promise((resolve) => setTimeout(resolve, 500));
|
||||
if (isProcessRunning(pid)) {
|
||||
return 'failed';
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
fs.unlinkSync(pidPath);
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
|
||||
return 'stopped';
|
||||
}
|
||||
|
||||
export const stopCommand: CommandModule = {
|
||||
command: 'stop',
|
||||
describe: 'Stop the LiteRT-LM server',
|
||||
builder: (yargs) =>
|
||||
yargs.option('port', {
|
||||
type: 'number',
|
||||
description: 'Port where the LiteRT server is running',
|
||||
}),
|
||||
handler: async (argv) => {
|
||||
let port: number | undefined;
|
||||
if (argv['port'] !== undefined) {
|
||||
port = Number(argv['port']);
|
||||
}
|
||||
|
||||
if (!port) {
|
||||
const { configuredPort } = resolveGemmaConfig(DEFAULT_PORT);
|
||||
port = configuredPort;
|
||||
}
|
||||
|
||||
const processInfo = readServerProcessInfo();
|
||||
const pid = processInfo?.pid ?? readServerPid();
|
||||
|
||||
if (pid !== null && isProcessRunning(pid)) {
|
||||
debugLogger.log(`Stopping LiteRT server (PID ${pid})...`);
|
||||
const result = await stopServer(port);
|
||||
if (result === 'stopped') {
|
||||
debugLogger.log(chalk.green('LiteRT server stopped.'));
|
||||
await exitCli(0);
|
||||
} else if (result === 'unexpected-process') {
|
||||
debugLogger.error(
|
||||
chalk.red(
|
||||
`Refusing to stop PID ${pid} because it does not match the expected LiteRT server process.`,
|
||||
),
|
||||
);
|
||||
debugLogger.error(
|
||||
chalk.dim(
|
||||
'Remove the stale pid file after verifying the process, or stop the process manually.',
|
||||
),
|
||||
);
|
||||
await exitCli(1);
|
||||
} else {
|
||||
debugLogger.error(chalk.red('Failed to stop LiteRT server.'));
|
||||
await exitCli(1);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const running = await isServerRunning(port);
|
||||
if (running) {
|
||||
debugLogger.log(
|
||||
chalk.yellow(
|
||||
`A server is responding on port ${port}, but it was not started by "gemini gemma start".`,
|
||||
),
|
||||
);
|
||||
debugLogger.log(
|
||||
chalk.dim(
|
||||
'If you started it manually, stop it from the terminal where it is running.',
|
||||
),
|
||||
);
|
||||
await exitCli(1);
|
||||
} else {
|
||||
debugLogger.log('No LiteRT server is currently running.');
|
||||
await exitCli(0);
|
||||
}
|
||||
},
|
||||
};
|
||||
@@ -278,6 +278,24 @@ describe('parseArguments', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('knowledgeSource', () => {
|
||||
it('should parse --knowledge-source flag with a path', async () => {
|
||||
process.argv = ['node', 'script.js', '--knowledge-source', 'mykb.md'];
|
||||
const settings = createTestMergedSettings();
|
||||
const argv = await parseArguments(settings);
|
||||
expect(argv.knowledgeSource).toBe('mykb.md');
|
||||
});
|
||||
|
||||
it('should default to ~/.agents/kb.md when --knowledge-source is provided without a path', async () => {
|
||||
process.argv = ['node', 'script.js', '--knowledge-source'];
|
||||
const settings = createTestMergedSettings();
|
||||
const argv = await parseArguments(settings);
|
||||
expect(argv.knowledgeSource).toBe(
|
||||
path.join(os.homedir(), '.agents', 'kb.md'),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it.each([
|
||||
{
|
||||
description: 'long flags',
|
||||
@@ -338,7 +356,6 @@ describe('parseArguments', () => {
|
||||
{ cmd: 'skill list', expected: true },
|
||||
{ cmd: 'hooks migrate', expected: true },
|
||||
{ cmd: 'hook migrate', expected: true },
|
||||
{ cmd: 'gemma status', expected: true },
|
||||
{ cmd: 'some query', expected: undefined },
|
||||
{ cmd: 'hello world', expected: undefined },
|
||||
])(
|
||||
@@ -759,12 +776,6 @@ describe('parseArguments', () => {
|
||||
const argv = await parseArguments(settings);
|
||||
expect(argv.isCommand).toBe(true);
|
||||
});
|
||||
|
||||
it('should set isCommand to true for gemma command', async () => {
|
||||
process.argv = ['node', 'script.js', 'gemma', 'status'];
|
||||
const argv = await parseArguments(createTestMergedSettings());
|
||||
expect(argv.isCommand).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('loadCliConfig', () => {
|
||||
@@ -916,6 +927,25 @@ describe('loadCliConfig', () => {
|
||||
expect(config.getApprovalMode()).toBe(ApprovalMode.DEFAULT);
|
||||
});
|
||||
|
||||
it('should enable simulateUser when knowledgeSource is provided', async () => {
|
||||
process.argv = ['node', 'script.js', '--knowledge-source', 'k.txt'];
|
||||
const argv = await parseArguments(createTestMergedSettings());
|
||||
const settings = createTestMergedSettings();
|
||||
const config = await loadCliConfig(settings, 'test-session', argv);
|
||||
expect(config.getSimulateUser()).toBe(true);
|
||||
expect(config.getKnowledgeSource()).toBe(
|
||||
path.resolve(process.cwd(), 'k.txt'),
|
||||
);
|
||||
});
|
||||
|
||||
it('should enable simulateUser when simulateUser flag is provided', async () => {
|
||||
process.argv = ['node', 'script.js', '--simulate-user'];
|
||||
const argv = await parseArguments(createTestMergedSettings());
|
||||
const settings = createTestMergedSettings();
|
||||
const config = await loadCliConfig(settings, 'test-session', argv);
|
||||
expect(config.getSimulateUser()).toBe(true);
|
||||
});
|
||||
|
||||
it('should be non-interactive when isCommand is set', async () => {
|
||||
process.argv = ['node', 'script.js', 'mcp', 'list'];
|
||||
const argv = await parseArguments(createTestMergedSettings());
|
||||
@@ -3037,8 +3067,6 @@ describe('loadCliConfig gemmaModelRouter', () => {
|
||||
experimental: {
|
||||
gemmaModelRouter: {
|
||||
enabled: true,
|
||||
autoStartServer: false,
|
||||
binaryPath: '/custom/lit',
|
||||
classifier: {
|
||||
host: 'http://custom:1234',
|
||||
model: 'custom-gemma',
|
||||
@@ -3049,8 +3077,6 @@ describe('loadCliConfig gemmaModelRouter', () => {
|
||||
const config = await loadCliConfig(settings, 'test-session', argv);
|
||||
expect(config.getGemmaModelRouterEnabled()).toBe(true);
|
||||
const gemmaSettings = config.getGemmaModelRouterSettings();
|
||||
expect(gemmaSettings.autoStartServer).toBe(false);
|
||||
expect(gemmaSettings.binaryPath).toBe('/custom/lit');
|
||||
expect(gemmaSettings.classifier?.host).toBe('http://custom:1234');
|
||||
expect(gemmaSettings.classifier?.model).toBe('custom-gemma');
|
||||
});
|
||||
@@ -3068,8 +3094,6 @@ describe('loadCliConfig gemmaModelRouter', () => {
|
||||
const config = await loadCliConfig(settings, 'test-session', argv);
|
||||
expect(config.getGemmaModelRouterEnabled()).toBe(true);
|
||||
const gemmaSettings = config.getGemmaModelRouterSettings();
|
||||
expect(gemmaSettings.autoStartServer).toBe(false);
|
||||
expect(gemmaSettings.binaryPath).toBe('');
|
||||
expect(gemmaSettings.classifier?.host).toBe('http://localhost:9379');
|
||||
expect(gemmaSettings.classifier?.model).toBe('gemma3-1b-gpu-custom');
|
||||
});
|
||||
|
||||
@@ -8,12 +8,12 @@ import yargs from 'yargs';
|
||||
import { hideBin } from 'yargs/helpers';
|
||||
import process from 'node:process';
|
||||
import * as path from 'node:path';
|
||||
import * as os from 'node:os';
|
||||
import { execa } from 'execa';
|
||||
import { mcpCommand } from '../commands/mcp.js';
|
||||
import { extensionsCommand } from '../commands/extensions.js';
|
||||
import { skillsCommand } from '../commands/skills.js';
|
||||
import { hooksCommand } from '../commands/hooks.js';
|
||||
import { gemmaCommand } from '../commands/gemma.js';
|
||||
import {
|
||||
setGeminiMdFilename as setServerGeminiMdFilename,
|
||||
getCurrentGeminiMdFilename,
|
||||
@@ -80,6 +80,7 @@ export interface CliArgs {
|
||||
model: string | undefined;
|
||||
sandbox: boolean | string | undefined;
|
||||
debug: boolean | undefined;
|
||||
disableStreaming?: boolean;
|
||||
prompt: string | undefined;
|
||||
promptInteractive: string | undefined;
|
||||
worktree?: string;
|
||||
@@ -107,6 +108,8 @@ export interface CliArgs {
|
||||
rawOutput: boolean | undefined;
|
||||
acceptRawOutputRisk: boolean | undefined;
|
||||
isCommand: boolean | undefined;
|
||||
simulateUser: boolean | undefined;
|
||||
knowledgeSource: string | undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -182,7 +185,6 @@ export async function parseArguments(
|
||||
extensionsCommand,
|
||||
skillsCommand,
|
||||
hooksCommand,
|
||||
gemmaCommand,
|
||||
];
|
||||
|
||||
const subcommands = commandModules.flatMap((mod) => {
|
||||
@@ -262,7 +264,6 @@ export async function parseArguments(
|
||||
yargsInstance.command(extensionsCommand);
|
||||
yargsInstance.command(skillsCommand);
|
||||
yargsInstance.command(hooksCommand);
|
||||
yargsInstance.command(gemmaCommand);
|
||||
|
||||
yargsInstance
|
||||
.command('$0 [query..]', 'Launch Gemini CLI', (yargsInstance) =>
|
||||
@@ -421,6 +422,10 @@ export async function parseArguments(
|
||||
type: 'boolean',
|
||||
description: 'Enable screen reader mode for accessibility.',
|
||||
})
|
||||
.option('disable-streaming', {
|
||||
type: 'boolean',
|
||||
description: 'Disable streaming responses from the model',
|
||||
})
|
||||
.option('output-format', {
|
||||
alias: 'o',
|
||||
type: 'string',
|
||||
@@ -446,6 +451,24 @@ export async function parseArguments(
|
||||
.option('accept-raw-output-risk', {
|
||||
type: 'boolean',
|
||||
description: 'Suppress the security warning when using --raw-output.',
|
||||
})
|
||||
.option('simulate-user', {
|
||||
type: 'boolean',
|
||||
description:
|
||||
'Run the user simulation agent in the background for evaluation purposes.',
|
||||
})
|
||||
.option('knowledge-source', {
|
||||
type: 'string',
|
||||
skipValidation: true,
|
||||
description:
|
||||
'A file path to load into the user simulator context and update with new knowledge. Defaults to ~/.agents/kb.md if passed without a value.',
|
||||
coerce: (value: string): string => {
|
||||
const trimmed = value.trim();
|
||||
if (trimmed === '') {
|
||||
return path.join(os.homedir(), '.agents', 'kb.md');
|
||||
}
|
||||
return trimmed;
|
||||
},
|
||||
}),
|
||||
)
|
||||
.version(await getVersion()) // This will enable the --version flag based on package.json
|
||||
@@ -900,6 +923,7 @@ export async function loadCliConfig(
|
||||
return new Config({
|
||||
acpMode: isAcpMode,
|
||||
clientName,
|
||||
disableStreaming: argv.disableStreaming,
|
||||
sessionId,
|
||||
clientVersion: await getVersion(),
|
||||
embeddingModel: DEFAULT_GEMINI_EMBEDDING_MODEL,
|
||||
@@ -951,6 +975,10 @@ export async function loadCliConfig(
|
||||
approvalMode,
|
||||
disableYoloMode:
|
||||
settings.security?.disableYoloMode || settings.admin?.secureModeEnabled,
|
||||
simulateUser: !!argv.simulateUser || !!argv.knowledgeSource,
|
||||
knowledgeSource: argv.knowledgeSource
|
||||
? path.resolve(cwd, resolvePath(argv.knowledgeSource))
|
||||
: undefined,
|
||||
disableAlwaysAllow:
|
||||
settings.security?.disableAlwaysAllow ||
|
||||
settings.admin?.secureModeEnabled,
|
||||
@@ -993,12 +1021,9 @@ export async function loadCliConfig(
|
||||
disabledSkills: settings.skills?.disabled,
|
||||
experimentalJitContext: settings.experimental?.jitContext,
|
||||
experimentalMemoryManager: settings.experimental?.memoryManager,
|
||||
experimentalAutoMemory: settings.experimental?.autoMemory,
|
||||
contextManagement,
|
||||
modelSteering: settings.experimental?.modelSteering,
|
||||
topicUpdateNarration:
|
||||
settings.general?.topicUpdateNarration ??
|
||||
settings.experimental?.topicUpdateNarration,
|
||||
topicUpdateNarration: settings.experimental?.topicUpdateNarration,
|
||||
noBrowser: !!process.env['NO_BROWSER'],
|
||||
summarizeToolOutput: settings.model?.summarizeToolOutput,
|
||||
ideMode,
|
||||
@@ -1032,7 +1057,6 @@ export async function loadCliConfig(
|
||||
recordResponses: argv.recordResponses,
|
||||
retryFetchErrors: settings.general?.retryFetchErrors,
|
||||
billing: settings.billing,
|
||||
vertexAiRouting: settings.billing?.vertexAi,
|
||||
maxAttempts: settings.general?.maxAttempts,
|
||||
ptyInfo: ptyInfo?.name,
|
||||
disableLLMCorrection: settings.tools?.disableLLMCorrection,
|
||||
|
||||
@@ -78,12 +78,7 @@ export function getMergeStrategyForPath(
|
||||
|
||||
export const USER_SETTINGS_PATH = Storage.getGlobalSettingsPath();
|
||||
export const USER_SETTINGS_DIR = path.dirname(USER_SETTINGS_PATH);
|
||||
export const DEFAULT_EXCLUDED_ENV_VARS = [
|
||||
'DEBUG',
|
||||
'DEBUG_MODE',
|
||||
'GEMINI_CLI_IDE_SERVER_STDIO_COMMAND',
|
||||
'GEMINI_CLI_IDE_SERVER_STDIO_ARGS',
|
||||
];
|
||||
export const DEFAULT_EXCLUDED_ENV_VARS = ['DEBUG', 'DEBUG_MODE'];
|
||||
|
||||
const AUTH_ENV_VAR_WHITELIST = [
|
||||
'GEMINI_API_KEY',
|
||||
|
||||
@@ -138,10 +138,6 @@ describe('SettingsSchema', () => {
|
||||
getSettingsSchema().context.properties.fileFiltering.properties
|
||||
?.enableRecursiveFileSearch,
|
||||
).toBeDefined();
|
||||
expect(
|
||||
getSettingsSchema().context.properties.fileFiltering.properties
|
||||
?.enableFileWatcher,
|
||||
).toBeDefined();
|
||||
expect(
|
||||
getSettingsSchema().context.properties.fileFiltering.properties
|
||||
?.customIgnoreFilePaths,
|
||||
@@ -317,22 +313,6 @@ describe('SettingsSchema', () => {
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it('should have Vertex AI routing settings in schema', () => {
|
||||
const vertexAi =
|
||||
getSettingsSchema().billing.properties.vertexAi.properties;
|
||||
|
||||
expect(vertexAi.requestType).toBeDefined();
|
||||
expect(vertexAi.requestType.type).toBe('enum');
|
||||
expect(
|
||||
vertexAi.requestType.options?.map((option) => option.value),
|
||||
).toEqual(['dedicated', 'shared']);
|
||||
expect(vertexAi.sharedRequestType).toBeDefined();
|
||||
expect(vertexAi.sharedRequestType.type).toBe('enum');
|
||||
expect(
|
||||
vertexAi.sharedRequestType.options?.map((option) => option.value),
|
||||
).toEqual(['priority', 'flex']);
|
||||
});
|
||||
|
||||
it('should have folderTrustFeature setting in schema', () => {
|
||||
expect(
|
||||
getSettingsSchema().security.properties.folderTrust.properties.enabled,
|
||||
@@ -491,33 +471,11 @@ describe('SettingsSchema', () => {
|
||||
expect(enabled.category).toBe('Experimental');
|
||||
expect(enabled.default).toBe(false);
|
||||
expect(enabled.requiresRestart).toBe(true);
|
||||
expect(enabled.showInDialog).toBe(true);
|
||||
expect(enabled.showInDialog).toBe(false);
|
||||
expect(enabled.description).toBe(
|
||||
'Enable the Gemma Model Router (experimental). Requires a local endpoint serving Gemma via the Gemini API using LiteRT-LM shim.',
|
||||
);
|
||||
|
||||
const autoStartServer = gemmaModelRouter.properties.autoStartServer;
|
||||
expect(autoStartServer).toBeDefined();
|
||||
expect(autoStartServer.type).toBe('boolean');
|
||||
expect(autoStartServer.category).toBe('Experimental');
|
||||
expect(autoStartServer.default).toBe(false);
|
||||
expect(autoStartServer.requiresRestart).toBe(true);
|
||||
expect(autoStartServer.showInDialog).toBe(true);
|
||||
expect(autoStartServer.description).toBe(
|
||||
'Automatically start the LiteRT-LM server when Gemini CLI starts and the Gemma router is enabled.',
|
||||
);
|
||||
|
||||
const binaryPath = gemmaModelRouter.properties.binaryPath;
|
||||
expect(binaryPath).toBeDefined();
|
||||
expect(binaryPath.type).toBe('string');
|
||||
expect(binaryPath.category).toBe('Experimental');
|
||||
expect(binaryPath.default).toBe('');
|
||||
expect(binaryPath.requiresRestart).toBe(true);
|
||||
expect(binaryPath.showInDialog).toBe(false);
|
||||
expect(binaryPath.description).toBe(
|
||||
'Custom path to the LiteRT-LM binary. Leave empty to use the default location (~/.gemini/bin/litert/).',
|
||||
);
|
||||
|
||||
const classifier = gemmaModelRouter.properties.classifier;
|
||||
expect(classifier).toBeDefined();
|
||||
expect(classifier.type).toBe('object');
|
||||
|
||||
@@ -21,7 +21,6 @@ import {
|
||||
type AgentOverride,
|
||||
type CustomTheme,
|
||||
type SandboxConfig,
|
||||
type VertexAiRoutingConfig,
|
||||
} from '@google/gemini-cli-core';
|
||||
import type { SessionRetentionSettings } from './settings.js';
|
||||
import { DEFAULT_MIN_RETENTION } from '../utils/sessionCleanup.js';
|
||||
@@ -419,16 +418,6 @@ const SETTINGS_SCHEMA = {
|
||||
},
|
||||
description: 'Settings for automatic session cleanup.',
|
||||
},
|
||||
topicUpdateNarration: {
|
||||
type: 'boolean',
|
||||
label: 'Topic & Update Narration',
|
||||
category: 'General',
|
||||
requiresRestart: false,
|
||||
default: true,
|
||||
description:
|
||||
'Enable the Topic & Update communication model for reduced chattiness and structured progress reporting.',
|
||||
showInDialog: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
output: {
|
||||
@@ -991,45 +980,6 @@ const SETTINGS_SCHEMA = {
|
||||
{ value: 'never', label: 'Never use credits' },
|
||||
],
|
||||
},
|
||||
vertexAi: {
|
||||
type: 'object',
|
||||
label: 'Vertex AI',
|
||||
category: 'Advanced',
|
||||
requiresRestart: true,
|
||||
default: undefined as VertexAiRoutingConfig | undefined,
|
||||
description: 'Vertex AI request routing settings.',
|
||||
showInDialog: false,
|
||||
properties: {
|
||||
requestType: {
|
||||
type: 'enum',
|
||||
label: 'Vertex AI Request Type',
|
||||
category: 'Advanced',
|
||||
requiresRestart: true,
|
||||
default: undefined as VertexAiRoutingConfig['requestType'],
|
||||
description:
|
||||
'Sets the X-Vertex-AI-LLM-Request-Type header for Vertex AI requests.',
|
||||
showInDialog: false,
|
||||
options: [
|
||||
{ value: 'dedicated', label: 'Dedicated' },
|
||||
{ value: 'shared', label: 'Shared' },
|
||||
],
|
||||
},
|
||||
sharedRequestType: {
|
||||
type: 'enum',
|
||||
label: 'Vertex AI Shared Request Type',
|
||||
category: 'Advanced',
|
||||
requiresRestart: true,
|
||||
default: undefined as VertexAiRoutingConfig['sharedRequestType'],
|
||||
description:
|
||||
'Sets the X-Vertex-AI-LLM-Shared-Request-Type header for Vertex AI requests.',
|
||||
showInDialog: false,
|
||||
options: [
|
||||
{ value: 'priority', label: 'Priority' },
|
||||
{ value: 'flex', label: 'Flex' },
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
@@ -1471,17 +1421,6 @@ const SETTINGS_SCHEMA = {
|
||||
description: 'Respect .geminiignore files when searching.',
|
||||
showInDialog: true,
|
||||
},
|
||||
enableFileWatcher: {
|
||||
type: 'boolean',
|
||||
label: 'Enable File Watcher',
|
||||
category: 'Context',
|
||||
requiresRestart: true,
|
||||
default: false,
|
||||
description: oneLine`
|
||||
Enable file watcher updates for @ file suggestions (experimental).
|
||||
`,
|
||||
showInDialog: false,
|
||||
},
|
||||
enableRecursiveFileSearch: {
|
||||
type: 'boolean',
|
||||
label: 'Enable Recursive File Search',
|
||||
@@ -2220,26 +2159,6 @@ const SETTINGS_SCHEMA = {
|
||||
default: false,
|
||||
description:
|
||||
'Enable the Gemma Model Router (experimental). Requires a local endpoint serving Gemma via the Gemini API using LiteRT-LM shim.',
|
||||
showInDialog: true,
|
||||
},
|
||||
autoStartServer: {
|
||||
type: 'boolean',
|
||||
label: 'Auto-start LiteRT Server',
|
||||
category: 'Experimental',
|
||||
requiresRestart: true,
|
||||
default: false,
|
||||
description:
|
||||
'Automatically start the LiteRT-LM server when Gemini CLI starts and the Gemma router is enabled.',
|
||||
showInDialog: true,
|
||||
},
|
||||
binaryPath: {
|
||||
type: 'string',
|
||||
label: 'LiteRT Binary Path',
|
||||
category: 'Experimental',
|
||||
requiresRestart: true,
|
||||
default: '',
|
||||
description:
|
||||
'Custom path to the LiteRT-LM binary. Leave empty to use the default location (~/.gemini/bin/litert/).',
|
||||
showInDialog: false,
|
||||
},
|
||||
classifier: {
|
||||
@@ -2284,16 +2203,6 @@ const SETTINGS_SCHEMA = {
|
||||
'Replace the built-in save_memory tool with a memory manager subagent that supports adding, removing, de-duplicating, and organizing memories.',
|
||||
showInDialog: true,
|
||||
},
|
||||
autoMemory: {
|
||||
type: 'boolean',
|
||||
label: 'Auto Memory',
|
||||
category: 'Experimental',
|
||||
requiresRestart: true,
|
||||
default: false,
|
||||
description:
|
||||
'Automatically extract reusable skills from past sessions in the background. Review results with /memory inbox.',
|
||||
showInDialog: true,
|
||||
},
|
||||
generalistProfile: {
|
||||
type: 'boolean',
|
||||
label: 'Use the generalist profile to manage agent contexts.',
|
||||
@@ -2319,8 +2228,9 @@ const SETTINGS_SCHEMA = {
|
||||
category: 'Experimental',
|
||||
requiresRestart: false,
|
||||
default: false,
|
||||
description: 'Deprecated: Use general.topicUpdateNarration instead.',
|
||||
showInDialog: false,
|
||||
description:
|
||||
'Enable the experimental Topic & Update communication model for reduced chattiness and structured progress reporting.',
|
||||
showInDialog: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -3071,11 +2981,6 @@ export const SETTINGS_SCHEMA_DEFINITIONS: Record<
|
||||
description: 'Protocol for OTLP exporters.',
|
||||
enum: ['grpc', 'http'],
|
||||
},
|
||||
traces: {
|
||||
type: 'boolean',
|
||||
description:
|
||||
'Whether detailed traces with large attributes are captured.',
|
||||
},
|
||||
logPrompts: {
|
||||
type: 'boolean',
|
||||
description: 'Whether prompts are logged in telemetry payloads.',
|
||||
|
||||
@@ -555,6 +555,8 @@ describe('gemini.tsx main function kitty protocol', () => {
|
||||
rawOutput: undefined,
|
||||
acceptRawOutputRisk: undefined,
|
||||
isCommand: undefined,
|
||||
simulateUser: undefined,
|
||||
knowledgeSource: undefined,
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
@@ -613,6 +615,8 @@ describe('gemini.tsx main function kitty protocol', () => {
|
||||
rawOutput: undefined,
|
||||
acceptRawOutputRisk: undefined,
|
||||
isCommand: undefined,
|
||||
simulateUser: undefined,
|
||||
knowledgeSource: undefined,
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
|
||||
@@ -612,23 +612,6 @@ export async function main() {
|
||||
const initializationResult = await initializeApp(config, settings);
|
||||
initAppHandle?.end();
|
||||
|
||||
import('./services/liteRtServerManager.js')
|
||||
.then(({ LiteRtServerManager }) => {
|
||||
const mergedGemma = settings.merged.experimental?.gemmaModelRouter;
|
||||
if (!mergedGemma) return;
|
||||
// Security: binaryPath and autoStartServer must come from user-scoped
|
||||
// settings only to prevent workspace configs from triggering arbitrary
|
||||
// binary execution.
|
||||
const userGemma = settings.forScope(SettingScope.User).settings
|
||||
.experimental?.gemmaModelRouter;
|
||||
return LiteRtServerManager.ensureRunning({
|
||||
...mergedGemma,
|
||||
binaryPath: userGemma?.binaryPath,
|
||||
autoStartServer: userGemma?.autoStartServer,
|
||||
});
|
||||
})
|
||||
.catch((e) => debugLogger.warn('LiteRT auto-start import failed:', e));
|
||||
|
||||
if (
|
||||
settings.merged.security.auth.selectedType ===
|
||||
AuthType.LOGIN_WITH_GOOGLE &&
|
||||
|
||||
@@ -9,11 +9,19 @@ import { render } from 'ink';
|
||||
import { basename } from 'node:path';
|
||||
import { AppContainer } from './ui/AppContainer.js';
|
||||
import { ConsolePatcher } from './ui/utils/ConsolePatcher.js';
|
||||
import { UserSimulator } from './services/UserSimulator.js';
|
||||
import {
|
||||
registerCleanup,
|
||||
removeCleanup,
|
||||
setupTtyCheck,
|
||||
} from './utils/cleanup.js';
|
||||
import { PassThrough } from 'node:stream';
|
||||
|
||||
interface RenderMetrics {
|
||||
renderTime: number;
|
||||
output: string;
|
||||
staticOutput?: string;
|
||||
}
|
||||
import {
|
||||
type StartupWarning,
|
||||
type Config,
|
||||
@@ -135,6 +143,12 @@ export async function startInteractiveUI(
|
||||
// Wait a moment for shpool to stabilize terminal size and state.
|
||||
await new Promise((resolve) => setTimeout(resolve, 100));
|
||||
}
|
||||
|
||||
const simulateUser = config.getSimulateUser();
|
||||
const simulatedStdin = new PassThrough({ encoding: 'utf8' });
|
||||
|
||||
let lastFrame: string | undefined;
|
||||
const staticHistory: string[] = [];
|
||||
const instance = render(
|
||||
process.env['DEBUG'] ? (
|
||||
<React.StrictMode>
|
||||
@@ -146,12 +160,20 @@ export async function startInteractiveUI(
|
||||
{
|
||||
stdout: inkStdout,
|
||||
stderr: inkStderr,
|
||||
stdin: process.stdin,
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-type-assertion, @typescript-eslint/no-unsafe-assignment
|
||||
stdin: (simulateUser ? simulatedStdin : process.stdin) as any,
|
||||
exitOnCtrlC: false,
|
||||
isScreenReaderEnabled: config.getScreenReader(),
|
||||
onRender: ({ renderTime }: { renderTime: number }) => {
|
||||
if (renderTime > SLOW_RENDER_MS) {
|
||||
recordSlowRender(config, Math.round(renderTime));
|
||||
onRender: (metrics: RenderMetrics) => {
|
||||
lastFrame = metrics.output;
|
||||
if (metrics.staticOutput) {
|
||||
staticHistory.push(metrics.staticOutput);
|
||||
if (staticHistory.length > 50) {
|
||||
staticHistory.shift();
|
||||
}
|
||||
}
|
||||
if (metrics.renderTime > SLOW_RENDER_MS) {
|
||||
recordSlowRender(config, metrics.renderTime);
|
||||
}
|
||||
profiler.reportFrameRendered();
|
||||
},
|
||||
@@ -188,6 +210,21 @@ export async function startInteractiveUI(
|
||||
}
|
||||
});
|
||||
|
||||
if (simulateUser) {
|
||||
const simulator = new UserSimulator(
|
||||
config,
|
||||
() => {
|
||||
if (lastFrame === undefined) return undefined;
|
||||
// Combine history with latest frame for the simulator
|
||||
const historyText = staticHistory.join('\n');
|
||||
return historyText ? `${historyText}\n${lastFrame}` : lastFrame;
|
||||
},
|
||||
simulatedStdin,
|
||||
);
|
||||
simulator.start();
|
||||
registerCleanup(() => simulator.stop());
|
||||
}
|
||||
|
||||
const cleanupUnmount = () => instance.unmount();
|
||||
registerCleanup(cleanupUnmount);
|
||||
|
||||
|
||||
@@ -42,7 +42,6 @@ import { initCommand } from '../ui/commands/initCommand.js';
|
||||
import { mcpCommand } from '../ui/commands/mcpCommand.js';
|
||||
import { memoryCommand } from '../ui/commands/memoryCommand.js';
|
||||
import { modelCommand } from '../ui/commands/modelCommand.js';
|
||||
import { noteCommand } from '../ui/commands/noteCommand.js';
|
||||
import { oncallCommand } from '../ui/commands/oncallCommand.js';
|
||||
import { permissionsCommand } from '../ui/commands/permissionsCommand.js';
|
||||
import { planCommand } from '../ui/commands/planCommand.js';
|
||||
@@ -62,7 +61,6 @@ import { vimCommand } from '../ui/commands/vimCommand.js';
|
||||
import { setupGithubCommand } from '../ui/commands/setupGithubCommand.js';
|
||||
import { terminalSetupCommand } from '../ui/commands/terminalSetupCommand.js';
|
||||
import { upgradeCommand } from '../ui/commands/upgradeCommand.js';
|
||||
import { gemmaStatusCommand } from '../ui/commands/gemmaStatusCommand.js';
|
||||
|
||||
/**
|
||||
* Loads the core, hard-coded slash commands that are an integral part
|
||||
@@ -185,7 +183,6 @@ export class BuiltinCommandLoader implements ICommandLoader {
|
||||
: [mcpCommand]),
|
||||
memoryCommand,
|
||||
modelCommand,
|
||||
noteCommand,
|
||||
...(this.config?.getFolderTrust() ? [permissionsCommand] : []),
|
||||
...(this.config?.isPlanEnabled() ? [planCommand] : []),
|
||||
policiesCommand,
|
||||
@@ -224,7 +221,6 @@ export class BuiltinCommandLoader implements ICommandLoader {
|
||||
: [skillsCommand]
|
||||
: []),
|
||||
settingsCommand,
|
||||
gemmaStatusCommand,
|
||||
tasksCommand,
|
||||
vimCommand,
|
||||
setupGithubCommand,
|
||||
|
||||
@@ -0,0 +1,352 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2026 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
import type { Config } from '@google/gemini-cli-core';
|
||||
import {
|
||||
debugLogger,
|
||||
LlmRole,
|
||||
PREVIEW_GEMINI_FLASH_MODEL,
|
||||
resolveModel,
|
||||
} from '@google/gemini-cli-core';
|
||||
import type { Writable } from 'node:stream';
|
||||
import * as fs from 'node:fs';
|
||||
import * as path from 'node:path';
|
||||
|
||||
interface SimulatorResponse {
|
||||
action?: string;
|
||||
thought?: string;
|
||||
used_knowledge?: boolean;
|
||||
new_rule?: string;
|
||||
}
|
||||
|
||||
export class UserSimulator {
|
||||
private isRunning = false;
|
||||
private timer: NodeJS.Timeout | null = null;
|
||||
private lastScreenContent = '';
|
||||
private isProcessing = false;
|
||||
private interactionsFile: string | null = null;
|
||||
|
||||
private knowledgeBase = '';
|
||||
private editableKnowledgeFile: string | null = null;
|
||||
private actionHistory: string[] = [];
|
||||
|
||||
constructor(
|
||||
private readonly config: Config,
|
||||
private readonly getScreen: () => string | undefined,
|
||||
private readonly stdinBuffer: Writable,
|
||||
) {}
|
||||
|
||||
start() {
|
||||
if (!this.config.getSimulateUser()) {
|
||||
return;
|
||||
}
|
||||
const source = this.config.getKnowledgeSource?.();
|
||||
if (source) {
|
||||
if (!fs.existsSync(source)) {
|
||||
try {
|
||||
fs.mkdirSync(path.dirname(source), { recursive: true });
|
||||
fs.writeFileSync(source, '', 'utf8');
|
||||
} catch (e) {
|
||||
debugLogger.error(`Failed to create knowledge file at ${source}`, e);
|
||||
}
|
||||
}
|
||||
this.editableKnowledgeFile = source;
|
||||
this.loadKnowledge(source);
|
||||
}
|
||||
this.interactionsFile = `interactions_${Date.now()}.txt`;
|
||||
this.isRunning = true;
|
||||
this.timer = setInterval(() => this.tick(), 1000);
|
||||
}
|
||||
|
||||
stop() {
|
||||
this.isRunning = false;
|
||||
if (this.timer) {
|
||||
clearInterval(this.timer);
|
||||
this.timer = null;
|
||||
}
|
||||
debugLogger.log('User simulator stopped');
|
||||
}
|
||||
|
||||
private loadKnowledge(p: string) {
|
||||
try {
|
||||
if (!fs.existsSync(p)) return;
|
||||
const stats = fs.statSync(p);
|
||||
if (stats.isFile()) {
|
||||
const content = fs.readFileSync(p, 'utf-8');
|
||||
if (content.trim()) {
|
||||
this.knowledgeBase = content + '\n';
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
debugLogger.error(`Failed to load knowledge from ${p}`, e);
|
||||
}
|
||||
}
|
||||
|
||||
private async tick() {
|
||||
if (!this.isRunning || this.isProcessing) return;
|
||||
|
||||
try {
|
||||
this.isProcessing = true;
|
||||
const screen = this.getScreen();
|
||||
if (!screen) return;
|
||||
|
||||
const strippedScreen = screen
|
||||
.replace(
|
||||
// eslint-disable-next-line no-control-regex
|
||||
/[\u001b\u009b][[()#;?]*(?:[0-9]{1,4}(?:;[0-9]{0,4})*)?[0-9A-ORZcf-nqry=><]/g,
|
||||
'',
|
||||
)
|
||||
.replace(/\n([ \t]*\n)+/g, '\n\n');
|
||||
|
||||
const normalizedScreen = strippedScreen
|
||||
.replace(/[\u2800-\u28FF]/g, '')
|
||||
.replace(/[|/-\\]/g, '')
|
||||
.replace(/\b\d+(\.\d+)?s\b/g, '')
|
||||
.replace(/\b\d+m(\s+\d+s)?\b/g, '')
|
||||
.replace(/\(\s*\)/g, '')
|
||||
.trim();
|
||||
|
||||
if (normalizedScreen === this.lastScreenContent) return;
|
||||
|
||||
debugLogger.log(
|
||||
`[SIMULATOR] Screen Content Seen:\n---\n${strippedScreen}\n---`,
|
||||
);
|
||||
if (this.interactionsFile) {
|
||||
fs.appendFileSync(
|
||||
this.interactionsFile,
|
||||
`[LOG] [SIMULATOR] Screen Content Seen:\n---\n${strippedScreen}\n---\n\n`,
|
||||
);
|
||||
}
|
||||
|
||||
const contentGenerator = this.config.getContentGenerator();
|
||||
if (!contentGenerator) return;
|
||||
|
||||
const originalGoal = this.config.getQuestion();
|
||||
const goalInstruction = originalGoal
|
||||
? `\nThe original goal was: "${originalGoal}"\n`
|
||||
: '';
|
||||
|
||||
const knowledgeInstruction = this.knowledgeBase
|
||||
? `\nUser Knowledge Base:\nUse this information to answer questions if applicable. If the answer is not here, respond as you normally would.\n${this.knowledgeBase}\n`
|
||||
: '';
|
||||
|
||||
const historyInstruction =
|
||||
this.actionHistory.length > 0
|
||||
? `\nRecent Simulator Actions (last 10):\n${this.actionHistory
|
||||
.slice(-10)
|
||||
.map((a, i) => `${i + 1}. ${JSON.stringify(a)}`)
|
||||
.join('\n')}\n`
|
||||
: '';
|
||||
|
||||
const prompt = `You are evaluating a CLI agent by simulating a user sitting at the terminal.
|
||||
Look carefully at the screen and determine the CLI's current state:
|
||||
|
||||
STATE 1: The agent is busy (e.g., streaming a response, showing a spinner, running a tool, or displaying a timer like "7s"). It is actively working and NOT waiting for text input.
|
||||
- In this case, your action MUST be exactly: <WAIT>
|
||||
|
||||
STATE 2: The agent is waiting for you to authorize a tool, confirm an action, or answer a specific multi-choice question (e.g., "Action Required", "Allow execution", numbered options).
|
||||
- In this case, your action MUST be the exact raw characters to select the option and submit it (e.g., 1\\r, 2\\r, y\\r, n\\r, or just \\r if the default option is acceptable). Do NOT output <DONE> or "Thank you". You must unblock the agent and allow it to run the tool.
|
||||
|
||||
STATE 3: The agent has finished its current thought process AND is idle, waiting for a NEW general text prompt (usually indicated by a "> Type your message" prompt).
|
||||
- First, verify that the ACTUAL task is fully complete based on your original goal. Do not stop at intermediate steps like planning or syntax checking.
|
||||
- If the task is indeed fully complete, your action should be "Thank you\\r" to graciously finish the simulation.
|
||||
- If you have already said thank you, your action MUST be exactly: <DONE>
|
||||
- If the agent is waiting at a general text prompt but the original task is NOT complete, provide text instructions to continue what is missing. DO NOT repeat the original goal if it has already been provided once. Ask it to continue or provide feedback based on the current state or send <DONE> if you think the task is completed.
|
||||
|
||||
STATE 4: Any other situation where the agent is waiting for text input or needs to press Enter.
|
||||
- Your action should be the raw characters you would type, followed by \\r. For just an Enter key press, output \\r.
|
||||
|
||||
CRITICAL RULES:
|
||||
- RULE 1: If there is ANY active spinner (e.g., ⠋, ⠙, ⠹, ⠸, ⠼, ⠴, ⠧) or an elapsed time indicator (e.g., "0s", "7s") anywhere on the screen, the agent is STILL WORKING. Your action MUST be <WAIT>. Do NOT issue commands, even if a text prompt is visible below it.
|
||||
- RULE 2: If there is an "Action Required" or confirmation prompt on the screen, YOU MUST HANDLE IT (State 2). This takes precedence over everything else.
|
||||
- RULE 3: If prompted to allow execution of a command with options like 'Allow once' and 'Allow for this session', you MUST choose the option for 'Allow for this session' (typically by sending '2\\r').
|
||||
- RULE 4: You MUST output a strictly formatted JSON object with no markdown wrappers or extra text.
|
||||
|
||||
JSON FORMAT:
|
||||
{
|
||||
"action": "<The exact raw characters to send, <WAIT>, or <DONE>>",
|
||||
"used_knowledge": <true if you used the User Knowledge Base below to answer this prompt, false otherwise>,
|
||||
"new_rule": "<If used_knowledge is false and action is not <WAIT> or <DONE>, formulate a single, clear, reusable one-line rule combining the question and your answer without using option numbers (e.g. 1, 2) that might change. For example: 'If asked to allow pip execution, always allow it.' or 'Automatically accept edits for snake game implementation.'>"
|
||||
}
|
||||
${goalInstruction}${knowledgeInstruction}${historyInstruction}
|
||||
|
||||
Here is the current terminal screen output:
|
||||
|
||||
<screen>
|
||||
${strippedScreen}
|
||||
</screen>`;
|
||||
|
||||
if (this.interactionsFile) {
|
||||
fs.appendFileSync(
|
||||
this.interactionsFile,
|
||||
`[LOG] [SIMULATOR] Prompt Used:\n---\n${prompt}\n---\n\n`,
|
||||
);
|
||||
}
|
||||
|
||||
const model = resolveModel(
|
||||
PREVIEW_GEMINI_FLASH_MODEL,
|
||||
false, // useGemini3_1
|
||||
false, // useGemini3_1FlashLite
|
||||
false, // useCustomToolModel
|
||||
this.config.getHasAccessToPreviewModel?.() ?? true,
|
||||
this.config,
|
||||
);
|
||||
|
||||
const response = await contentGenerator.generateContent(
|
||||
{
|
||||
model,
|
||||
contents: [
|
||||
{
|
||||
role: 'user',
|
||||
parts: [{ text: prompt }],
|
||||
},
|
||||
],
|
||||
},
|
||||
'simulator-prompt',
|
||||
LlmRole.UTILITY_SIMULATOR,
|
||||
);
|
||||
|
||||
let responseText = '';
|
||||
let parsedJson: SimulatorResponse = {};
|
||||
try {
|
||||
let cleanJson = response.text || '';
|
||||
const startIdx = cleanJson.indexOf('{');
|
||||
const endIdx = cleanJson.lastIndexOf('}');
|
||||
if (startIdx !== -1 && endIdx !== -1 && endIdx > startIdx) {
|
||||
cleanJson = cleanJson.substring(startIdx, endIdx + 1);
|
||||
} else {
|
||||
cleanJson = cleanJson.replace(/^```json\s*|\s*```$/gm, '').trim();
|
||||
}
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
|
||||
parsedJson = JSON.parse(cleanJson) as SimulatorResponse;
|
||||
responseText = parsedJson.action || '';
|
||||
} catch (err) {
|
||||
debugLogger.error('Failed to parse simulator response as JSON', err);
|
||||
const text = (response.text || '').trim();
|
||||
if (
|
||||
text === '<WAIT>' ||
|
||||
text === '<DONE>' ||
|
||||
/^\d+\\r$/.test(text) ||
|
||||
text === '\\r'
|
||||
) {
|
||||
responseText = text.replace(/^[`"']+|[`"']+$/g, '');
|
||||
} else {
|
||||
responseText = ''; // Prevent typing broken JSON string
|
||||
}
|
||||
}
|
||||
|
||||
const trimmedResponse = responseText.trim();
|
||||
|
||||
debugLogger.log(
|
||||
`[SIMULATOR] Raw model response: ${JSON.stringify(response.text)}`,
|
||||
);
|
||||
if (this.interactionsFile) {
|
||||
fs.appendFileSync(
|
||||
this.interactionsFile,
|
||||
`[LOG] [SIMULATOR] Raw model response: ${JSON.stringify(response.text)}\n\n`,
|
||||
);
|
||||
}
|
||||
debugLogger.log(
|
||||
`[SIMULATOR] Processed response: ${JSON.stringify(responseText)}`,
|
||||
);
|
||||
|
||||
if (trimmedResponse === '<DONE>') {
|
||||
const msg = '[SIMULATOR] Terminating simulation: Task is completed.';
|
||||
debugLogger.log(msg);
|
||||
if (this.interactionsFile) {
|
||||
fs.appendFileSync(this.interactionsFile, `[LOG] ${msg}\n\n`);
|
||||
}
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(`\n${msg}`);
|
||||
this.stop();
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
if (trimmedResponse === '<WAIT>') {
|
||||
debugLogger.log(
|
||||
'[SIMULATOR] Skipping action (model decided to <WAIT>)',
|
||||
);
|
||||
this.actionHistory.push('<WAIT>');
|
||||
if (this.interactionsFile) {
|
||||
fs.appendFileSync(
|
||||
this.interactionsFile,
|
||||
`[LOG] [SIMULATOR] Action History updated with: "<WAIT>"\n\n`,
|
||||
);
|
||||
}
|
||||
this.lastScreenContent = normalizedScreen;
|
||||
return;
|
||||
}
|
||||
|
||||
if (responseText) {
|
||||
const keys = responseText
|
||||
.replace(/\\n|\n/g, '\r')
|
||||
.replace(/\\r/g, '\r');
|
||||
|
||||
debugLogger.log(
|
||||
`[SIMULATOR] Sending to stdin: ${JSON.stringify(keys)}`,
|
||||
);
|
||||
|
||||
this.actionHistory.push(keys);
|
||||
if (this.interactionsFile) {
|
||||
fs.appendFileSync(
|
||||
this.interactionsFile,
|
||||
`[LOG] [SIMULATOR] Action History updated with: ${JSON.stringify(keys)}\n\n`,
|
||||
);
|
||||
}
|
||||
|
||||
if (
|
||||
!parsedJson.used_knowledge &&
|
||||
parsedJson.new_rule &&
|
||||
this.editableKnowledgeFile
|
||||
) {
|
||||
const newKnowledge = `- ${parsedJson.new_rule}\n`;
|
||||
this.knowledgeBase += newKnowledge;
|
||||
try {
|
||||
fs.appendFileSync(this.editableKnowledgeFile, newKnowledge);
|
||||
debugLogger.log(
|
||||
`[SIMULATOR] Saved new knowledge to ${this.editableKnowledgeFile}`,
|
||||
);
|
||||
if (this.interactionsFile) {
|
||||
fs.appendFileSync(
|
||||
this.interactionsFile,
|
||||
`[LOG] [SIMULATOR] Saved new knowledge to ${this.editableKnowledgeFile}\n\n`,
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
debugLogger.error(`Failed to append knowledge`, e);
|
||||
}
|
||||
}
|
||||
|
||||
for (const char of keys) {
|
||||
if (char === '\r') {
|
||||
// Wait a bit to ensure the previous character is rendered before submitting
|
||||
await new Promise((resolve) => setTimeout(resolve, 50));
|
||||
}
|
||||
this.stdinBuffer.write(char);
|
||||
// Small delay to ensure Ink processes each keypress event individually
|
||||
// while preventing UI state collisions during long simulated inputs.
|
||||
await new Promise((resolve) => setTimeout(resolve, 5));
|
||||
}
|
||||
this.lastScreenContent = normalizedScreen;
|
||||
} else {
|
||||
debugLogger.log('[SIMULATOR] Skipping (empty response)');
|
||||
|
||||
this.actionHistory.push('<EMPTY>');
|
||||
if (this.interactionsFile) {
|
||||
fs.appendFileSync(
|
||||
this.interactionsFile,
|
||||
`[LOG] [SIMULATOR] Action History updated with: "<EMPTY>"\n\n`,
|
||||
);
|
||||
}
|
||||
|
||||
this.lastScreenContent = normalizedScreen;
|
||||
}
|
||||
} catch (e: unknown) {
|
||||
debugLogger.error('UserSimulator tick failed', e);
|
||||
} finally {
|
||||
this.isProcessing = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,68 +0,0 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2026 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import fs from 'node:fs';
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
import type { GemmaModelRouterSettings } from '@google/gemini-cli-core';
|
||||
|
||||
const mockGetBinaryPath = vi.hoisted(() => vi.fn());
|
||||
const mockIsServerRunning = vi.hoisted(() => vi.fn());
|
||||
const mockStartServer = vi.hoisted(() => vi.fn());
|
||||
|
||||
vi.mock('../commands/gemma/platform.js', () => ({
|
||||
getBinaryPath: mockGetBinaryPath,
|
||||
isServerRunning: mockIsServerRunning,
|
||||
}));
|
||||
|
||||
vi.mock('../commands/gemma/start.js', () => ({
|
||||
startServer: mockStartServer,
|
||||
}));
|
||||
|
||||
import { LiteRtServerManager } from './liteRtServerManager.js';
|
||||
|
||||
describe('LiteRtServerManager', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
vi.spyOn(fs, 'existsSync').mockReturnValue(true);
|
||||
mockIsServerRunning.mockResolvedValue(false);
|
||||
mockStartServer.mockResolvedValue(true);
|
||||
});
|
||||
|
||||
it('uses the configured custom binary path when auto-starting', async () => {
|
||||
mockGetBinaryPath.mockReturnValue('/user/lit');
|
||||
|
||||
const settings: GemmaModelRouterSettings = {
|
||||
enabled: true,
|
||||
binaryPath: '/workspace/evil',
|
||||
classifier: {
|
||||
host: 'http://localhost:8123',
|
||||
},
|
||||
};
|
||||
|
||||
await LiteRtServerManager.ensureRunning(settings);
|
||||
|
||||
expect(mockGetBinaryPath).toHaveBeenCalledTimes(1);
|
||||
expect(fs.existsSync).toHaveBeenCalledWith('/user/lit');
|
||||
expect(mockStartServer).toHaveBeenCalledWith('/user/lit', 8123);
|
||||
});
|
||||
|
||||
it('falls back to the default binary path when no custom path is configured', async () => {
|
||||
mockGetBinaryPath.mockReturnValue('/default/lit');
|
||||
|
||||
const settings: GemmaModelRouterSettings = {
|
||||
enabled: true,
|
||||
classifier: {
|
||||
host: 'http://localhost:9379',
|
||||
},
|
||||
};
|
||||
|
||||
await LiteRtServerManager.ensureRunning(settings);
|
||||
|
||||
expect(mockGetBinaryPath).toHaveBeenCalledTimes(1);
|
||||
expect(fs.existsSync).toHaveBeenCalledWith('/default/lit');
|
||||
expect(mockStartServer).toHaveBeenCalledWith('/default/lit', 9379);
|
||||
});
|
||||
});
|
||||
@@ -1,59 +0,0 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2026 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import fs from 'node:fs';
|
||||
import { debugLogger } from '@google/gemini-cli-core';
|
||||
import type { GemmaModelRouterSettings } from '@google/gemini-cli-core';
|
||||
import { getBinaryPath, isServerRunning } from '../commands/gemma/platform.js';
|
||||
import { DEFAULT_PORT } from '../commands/gemma/constants.js';
|
||||
|
||||
export class LiteRtServerManager {
|
||||
static async ensureRunning(
|
||||
gemmaSettings: GemmaModelRouterSettings | undefined,
|
||||
): Promise<void> {
|
||||
if (!gemmaSettings?.enabled) return;
|
||||
if (gemmaSettings.autoStartServer === false) return;
|
||||
const binaryPath = getBinaryPath();
|
||||
if (!binaryPath || !fs.existsSync(binaryPath)) {
|
||||
debugLogger.log(
|
||||
'[LiteRtServerManager] Binary not installed, skipping auto-start. Run "gemini gemma setup".',
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const port =
|
||||
parseInt(
|
||||
gemmaSettings.classifier?.host?.match(/:(\d+)/)?.[1] ?? '',
|
||||
10,
|
||||
) || DEFAULT_PORT;
|
||||
|
||||
const running = await isServerRunning(port);
|
||||
if (running) {
|
||||
debugLogger.log(
|
||||
`[LiteRtServerManager] Server already running on port ${port}`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
debugLogger.log(
|
||||
`[LiteRtServerManager] Auto-starting LiteRT server on port ${port}...`,
|
||||
);
|
||||
|
||||
try {
|
||||
const { startServer } = await import('../commands/gemma/start.js');
|
||||
const started = await startServer(binaryPath, port);
|
||||
if (started) {
|
||||
debugLogger.log(`[LiteRtServerManager] Server started on port ${port}`);
|
||||
} else {
|
||||
debugLogger.warn(
|
||||
`[LiteRtServerManager] Server may not have started correctly on port ${port}`,
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
debugLogger.warn('[LiteRtServerManager] Auto-start failed:', error);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -39,7 +39,6 @@ export const createMockConfig = (overrides: Partial<Config> = {}): Config =>
|
||||
fireSessionStartEvent: vi.fn().mockResolvedValue(undefined),
|
||||
})),
|
||||
isMemoryManagerEnabled: vi.fn(() => false),
|
||||
isAutoMemoryEnabled: vi.fn(() => false),
|
||||
getListExtensions: vi.fn(() => false),
|
||||
getExtensions: vi.fn(() => []),
|
||||
getListSessions: vi.fn(() => false),
|
||||
@@ -66,6 +65,7 @@ export const createMockConfig = (overrides: Partial<Config> = {}): Config =>
|
||||
getGeminiMdFileCount: vi.fn(() => 0),
|
||||
getDeferredCommand: vi.fn(() => undefined),
|
||||
getFileSystemService: vi.fn(() => ({})),
|
||||
getSimulateUser: vi.fn(() => false),
|
||||
clientVersion: '1.0.0',
|
||||
getModel: vi.fn().mockReturnValue('gemini-pro'),
|
||||
getWorkingDir: vi.fn().mockReturnValue('/mock/cwd'),
|
||||
@@ -89,7 +89,6 @@ export const createMockConfig = (overrides: Partial<Config> = {}): Config =>
|
||||
getAccessibility: vi.fn().mockReturnValue({}),
|
||||
getTelemetryEnabled: vi.fn().mockReturnValue(false),
|
||||
getTelemetryLogPromptsEnabled: vi.fn().mockReturnValue(false),
|
||||
getTelemetryTracesEnabled: vi.fn().mockReturnValue(false),
|
||||
getTelemetryOtlpEndpoint: vi.fn().mockReturnValue(''),
|
||||
getTelemetryOtlpProtocol: vi.fn().mockReturnValue('grpc'),
|
||||
getTelemetryTarget: vi.fn().mockReturnValue(''),
|
||||
|
||||
@@ -92,6 +92,7 @@ import {
|
||||
ApiKeyUpdatedEvent,
|
||||
LegacyAgentProtocol,
|
||||
type InjectionSource,
|
||||
startMemoryService,
|
||||
} from '@google/gemini-cli-core';
|
||||
import { validateAuthMethod } from '../config/auth.js';
|
||||
import process from 'node:process';
|
||||
@@ -124,7 +125,6 @@ import { type BackgroundTask } from './hooks/useExecutionLifecycle.js';
|
||||
import { useVim } from './hooks/vim.js';
|
||||
import { type LoadableSettingScope, SettingScope } from '../config/settings.js';
|
||||
import { type InitializationResult } from '../core/initializer.js';
|
||||
import { startAutoMemoryIfEnabled } from '../utils/autoMemory.js';
|
||||
import { useFocus } from './hooks/useFocus.js';
|
||||
import { useKeypress, type Key } from './hooks/useKeypress.js';
|
||||
import { KeypressPriority } from './contexts/KeypressContext.js';
|
||||
@@ -486,7 +486,12 @@ export const AppContainer = (props: AppContainerProps) => {
|
||||
setConfigInitialized(true);
|
||||
startupProfiler.flush(config);
|
||||
|
||||
startAutoMemoryIfEnabled(config);
|
||||
// Fire-and-forget memory service (skill extraction from past sessions)
|
||||
if (config.isMemoryManagerEnabled()) {
|
||||
startMemoryService(config).catch((e) => {
|
||||
debugLogger.error('Failed to start memory service:', e);
|
||||
});
|
||||
}
|
||||
|
||||
const sessionStartSource = resumedSessionData
|
||||
? SessionStartSource.Resume
|
||||
@@ -972,7 +977,6 @@ Logging in with Google... Restarting Gemini CLI to continue.
|
||||
openAgentConfigDialog,
|
||||
openPermissionsDialog,
|
||||
quit: (messages: HistoryItem[]) => {
|
||||
closeThemeDialog();
|
||||
setQuittingMessages(messages);
|
||||
setTimeout(async () => {
|
||||
await runExitCleanup();
|
||||
@@ -1001,7 +1005,6 @@ Logging in with Google... Restarting Gemini CLI to continue.
|
||||
[
|
||||
setAuthState,
|
||||
openThemeDialog,
|
||||
closeThemeDialog,
|
||||
openEditorDialog,
|
||||
openSettingsDialog,
|
||||
openSessionBrowser,
|
||||
@@ -1404,7 +1407,13 @@ Logging in with Google... Restarting Gemini CLI to continue.
|
||||
}
|
||||
|
||||
const isMcpOrConfigReady = isConfigInitialized && isMcpReady;
|
||||
debugLogger.log(
|
||||
`[AppContainer] handleFinalSubmit: streamingState=${streamingState}, isIdle=${isIdle}, isSlash=${isSlash}`,
|
||||
);
|
||||
if ((isSlash && isConfigInitialized) || (isIdle && isMcpOrConfigReady)) {
|
||||
debugLogger.log(
|
||||
`[AppContainer] handleFinalSubmit: condition met, calling submitQuery`,
|
||||
);
|
||||
if (!isSlash) {
|
||||
const permissions = await checkPermissions(submittedValue, config);
|
||||
if (permissions.length > 0) {
|
||||
|
||||
@@ -52,7 +52,7 @@ export function ApiAuthDialog({
|
||||
height: 4,
|
||||
},
|
||||
inputFilter: (text) =>
|
||||
text.replace(/[^a-zA-Z0-9_.-]/g, '').replace(/[\r\n]/g, ''),
|
||||
text.replace(/[^a-zA-Z0-9_-]/g, '').replace(/[\r\n]/g, ''),
|
||||
singleLine: true,
|
||||
});
|
||||
|
||||
|
||||
@@ -16,9 +16,8 @@ import { MessageType } from '../types.js';
|
||||
import { randomUUID } from 'node:crypto';
|
||||
|
||||
export const clearCommand: SlashCommand = {
|
||||
name: 'clear (new)',
|
||||
altNames: ['new'],
|
||||
description: 'Clear the screen and start a new session',
|
||||
name: 'clear',
|
||||
description: 'Clear the screen and conversation history',
|
||||
kind: CommandKind.BUILT_IN,
|
||||
autoExecute: true,
|
||||
action: async (context, _args) => {
|
||||
|
||||
@@ -1,41 +0,0 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2026 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { CommandKind, type SlashCommand } from './types.js';
|
||||
import { MessageType, type HistoryItemGemmaStatus } from '../types.js';
|
||||
import { checkGemmaStatus } from '../../commands/gemma/status.js';
|
||||
import { GEMMA_MODEL_NAME } from '../../commands/gemma/constants.js';
|
||||
|
||||
export const gemmaStatusCommand: SlashCommand = {
|
||||
name: 'gemma',
|
||||
description: 'Check local Gemma model routing status',
|
||||
kind: CommandKind.BUILT_IN,
|
||||
autoExecute: true,
|
||||
isSafeConcurrent: true,
|
||||
action: async (context) => {
|
||||
const port =
|
||||
parseInt(
|
||||
context.services.settings.merged.experimental?.gemmaModelRouter?.classifier?.host?.match(
|
||||
/:(\d+)/,
|
||||
)?.[1] ?? '',
|
||||
10,
|
||||
) || undefined;
|
||||
const status = await checkGemmaStatus(port);
|
||||
const item: Omit<HistoryItemGemmaStatus, 'id'> = {
|
||||
type: MessageType.GEMMA_STATUS,
|
||||
binaryInstalled: status.binaryInstalled,
|
||||
binaryPath: status.binaryPath,
|
||||
modelName: GEMMA_MODEL_NAME,
|
||||
modelDownloaded: status.modelDownloaded,
|
||||
serverRunning: status.serverRunning,
|
||||
serverPid: status.serverPid,
|
||||
serverPort: status.port,
|
||||
settingsEnabled: status.settingsEnabled,
|
||||
allPassing: status.allPassing,
|
||||
};
|
||||
context.ui.addItem(item);
|
||||
},
|
||||
};
|
||||
@@ -473,7 +473,7 @@ describe('memoryCommand', () => {
|
||||
|
||||
const mockConfig = {
|
||||
reloadSkills: vi.fn(),
|
||||
isAutoMemoryEnabled: vi.fn().mockReturnValue(true),
|
||||
isMemoryManagerEnabled: vi.fn().mockReturnValue(true),
|
||||
};
|
||||
const context = createMockCommandContext({
|
||||
services: {
|
||||
@@ -491,11 +491,11 @@ describe('memoryCommand', () => {
|
||||
expect(result).toHaveProperty('component');
|
||||
});
|
||||
|
||||
it('should return info message when auto memory is disabled', () => {
|
||||
it('should return info message when memory manager is disabled', () => {
|
||||
if (!inboxCommand.action) throw new Error('Command has no action');
|
||||
|
||||
const mockConfig = {
|
||||
isAutoMemoryEnabled: vi.fn().mockReturnValue(false),
|
||||
isMemoryManagerEnabled: vi.fn().mockReturnValue(false),
|
||||
};
|
||||
const context = createMockCommandContext({
|
||||
services: {
|
||||
@@ -509,7 +509,7 @@ describe('memoryCommand', () => {
|
||||
type: 'message',
|
||||
messageType: 'info',
|
||||
content:
|
||||
'The memory inbox requires Auto Memory. Enable it with: experimental.autoMemory = true in settings.',
|
||||
'The memory inbox requires the experimental memory manager. Enable it with: experimental.memoryManager = true in settings.',
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -145,12 +145,12 @@ export const memoryCommand: SlashCommand = {
|
||||
};
|
||||
}
|
||||
|
||||
if (!config.isAutoMemoryEnabled()) {
|
||||
if (!config.isMemoryManagerEnabled()) {
|
||||
return {
|
||||
type: 'message',
|
||||
messageType: 'info',
|
||||
content:
|
||||
'The memory inbox requires Auto Memory. Enable it with: experimental.autoMemory = true in settings.',
|
||||
'The memory inbox requires the experimental memory manager. Enable it with: experimental.memoryManager = true in settings.',
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -1,94 +0,0 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2026 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import fs from 'node:fs/promises';
|
||||
import path from 'node:path';
|
||||
import { noteCommand } from './noteCommand.js';
|
||||
import { type CommandContext } from './types.js';
|
||||
|
||||
vi.mock('node:fs/promises');
|
||||
|
||||
describe('noteCommand', () => {
|
||||
const mockContext = {} as CommandContext;
|
||||
const notesPath = path.join(process.cwd(), 'notes.md');
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it('should return notes content when no args provided and file exists', async () => {
|
||||
vi.mocked(fs.readFile).mockResolvedValue('existing note\n');
|
||||
|
||||
const result = await noteCommand.action!(mockContext, '');
|
||||
|
||||
expect(fs.readFile).toHaveBeenCalledWith(notesPath, 'utf8');
|
||||
expect(result).toEqual({
|
||||
type: 'message',
|
||||
messageType: 'info',
|
||||
content: expect.stringContaining('existing note'),
|
||||
});
|
||||
});
|
||||
|
||||
it('should return info message when no args provided and file does not exist (ENOENT)', async () => {
|
||||
const error = new Error('File not found') as NodeJS.ErrnoException;
|
||||
error.code = 'ENOENT';
|
||||
vi.mocked(fs.readFile).mockRejectedValue(error);
|
||||
|
||||
const result = await noteCommand.action!(mockContext, ' ');
|
||||
|
||||
expect(result).toEqual({
|
||||
type: 'message',
|
||||
messageType: 'info',
|
||||
content: 'No notes found. Use "/note <text>" to add one.',
|
||||
});
|
||||
});
|
||||
|
||||
it('should return error message when readFile fails with other error', async () => {
|
||||
vi.mocked(fs.readFile).mockRejectedValue(new Error('Permission denied'));
|
||||
|
||||
const result = await noteCommand.action!(mockContext, '');
|
||||
|
||||
expect(result).toEqual({
|
||||
type: 'message',
|
||||
messageType: 'error',
|
||||
content: expect.stringContaining(
|
||||
'Failed to read notes: Permission denied',
|
||||
),
|
||||
});
|
||||
});
|
||||
|
||||
it('should append trimmed note to file when args are provided', async () => {
|
||||
const note = ' this is a new note ';
|
||||
vi.mocked(fs.appendFile).mockResolvedValue(undefined);
|
||||
|
||||
const result = await noteCommand.action!(mockContext, note);
|
||||
|
||||
expect(fs.appendFile).toHaveBeenCalledWith(
|
||||
notesPath,
|
||||
`this is a new note\n`,
|
||||
);
|
||||
expect(result).toEqual({
|
||||
type: 'message',
|
||||
messageType: 'info',
|
||||
content: expect.stringContaining('Note added'),
|
||||
});
|
||||
});
|
||||
|
||||
it('should return error message when append fails', async () => {
|
||||
vi.mocked(fs.appendFile).mockRejectedValue(new Error('Permission denied'));
|
||||
|
||||
const result = await noteCommand.action!(mockContext, 'some note');
|
||||
|
||||
expect(result).toEqual({
|
||||
type: 'message',
|
||||
messageType: 'error',
|
||||
content: expect.stringContaining(
|
||||
'Failed to save note: Permission denied',
|
||||
),
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,60 +0,0 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2026 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import fs from 'node:fs/promises';
|
||||
import path from 'node:path';
|
||||
import { isNodeError } from '@google/gemini-cli-core';
|
||||
import { CommandKind, type SlashCommand } from './types.js';
|
||||
|
||||
export const noteCommand: SlashCommand = {
|
||||
name: 'note',
|
||||
description: 'Append a note to notes.md or view current notes',
|
||||
kind: CommandKind.BUILT_IN,
|
||||
autoExecute: true,
|
||||
action: async (_context, args) => {
|
||||
const notesPath = path.join(process.cwd(), 'notes.md');
|
||||
|
||||
if (!args || args.trim().length === 0) {
|
||||
try {
|
||||
const content = await fs.readFile(notesPath, 'utf8');
|
||||
return {
|
||||
type: 'message',
|
||||
messageType: 'info',
|
||||
content: `Current notes in ${notesPath}:\n\n${content}`,
|
||||
};
|
||||
} catch (error) {
|
||||
if (isNodeError(error) && error.code === 'ENOENT') {
|
||||
return {
|
||||
type: 'message',
|
||||
messageType: 'info',
|
||||
content: 'No notes found. Use "/note <text>" to add one.',
|
||||
};
|
||||
}
|
||||
return {
|
||||
type: 'message',
|
||||
messageType: 'error',
|
||||
content: `Failed to read notes: ${error instanceof Error ? error.message : String(error)}`,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
const trimmedNote = args.trim();
|
||||
await fs.appendFile(notesPath, `${trimmedNote}\n`);
|
||||
return {
|
||||
type: 'message',
|
||||
messageType: 'info',
|
||||
content: `Note added to ${notesPath}`,
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
type: 'message',
|
||||
messageType: 'error',
|
||||
content: `Failed to save note: ${error instanceof Error ? error.message : String(error)}`,
|
||||
};
|
||||
}
|
||||
},
|
||||
};
|
||||
@@ -868,24 +868,28 @@ const ChoiceQuestionView: React.FC<ChoiceQuestionViewProps> = ({
|
||||
: undefined;
|
||||
|
||||
// Reserve space for at least 3 items if more selectionItems available.
|
||||
const reservedListHeight = Math.min(selectionItems.length * 2, 6);
|
||||
|
||||
const questionHeightLimit =
|
||||
listHeight && !isAlternateBuffer
|
||||
? question.unconstrainedHeight
|
||||
? Math.max(1, listHeight - selectionItems.length * 2)
|
||||
: Math.max(1, listHeight - Math.max(DIALOG_PADDING, reservedListHeight))
|
||||
: Math.min(
|
||||
30,
|
||||
Math.max(1, listHeight - Math.min(selectionItems.length, 5) * 2),
|
||||
)
|
||||
: undefined;
|
||||
|
||||
const maxItemsToShow =
|
||||
listHeight && (!isAlternateBuffer || availableHeight !== undefined)
|
||||
? Math.min(
|
||||
selectionItems.length,
|
||||
Math.max(
|
||||
1,
|
||||
Math.floor((listHeight - (questionHeightLimit ?? 0)) / 2),
|
||||
),
|
||||
)
|
||||
: selectionItems.length;
|
||||
let maxItemsToShow = selectionItems.length;
|
||||
if (listHeight && (!isAlternateBuffer || availableHeight !== undefined)) {
|
||||
if (selectionItems.length <= 5) {
|
||||
maxItemsToShow = selectionItems.length;
|
||||
} else {
|
||||
maxItemsToShow = Math.min(
|
||||
selectionItems.length,
|
||||
Math.max(1, Math.floor((listHeight - (questionHeightLimit ?? 0)) / 2)),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Box flexDirection="column">
|
||||
|
||||
@@ -159,7 +159,6 @@ Implement a comprehensive authentication system with multiple providers.
|
||||
isTrustedFolder: () => true,
|
||||
getPreferredEditor: () => undefined,
|
||||
getSessionId: () => 'test-session-id',
|
||||
getProjectRoot: () => mockTargetDir,
|
||||
storage: {
|
||||
getPlansDir: () => mockPlansDir,
|
||||
},
|
||||
@@ -467,7 +466,6 @@ Implement a comprehensive authentication system with multiple providers.
|
||||
getIdeMode: () => false,
|
||||
isTrustedFolder: () => true,
|
||||
getSessionId: () => 'test-session-id',
|
||||
getProjectRoot: () => mockTargetDir,
|
||||
storage: {
|
||||
getPlansDir: () => mockPlansDir,
|
||||
},
|
||||
|
||||
@@ -79,13 +79,13 @@ function usePlanContent(planPath: string, config: Config): PlanContentState {
|
||||
useEffect(() => {
|
||||
let ignore = false;
|
||||
setState({ status: PlanStatus.Loading });
|
||||
debugLogger.debug('usePlanContent loading plan:', planPath);
|
||||
|
||||
const load = async () => {
|
||||
try {
|
||||
const pathError = await validatePlanPath(
|
||||
planPath,
|
||||
config.storage.getPlansDir(),
|
||||
config.getProjectRoot(),
|
||||
);
|
||||
if (ignore) return;
|
||||
if (pathError) {
|
||||
@@ -126,6 +126,10 @@ function usePlanContent(planPath: string, config: Config): PlanContentState {
|
||||
setState({ status: PlanStatus.Error, error: 'Plan file is empty.' });
|
||||
return;
|
||||
}
|
||||
debugLogger.debug(
|
||||
'usePlanContent loaded successfully, length:',
|
||||
content.length,
|
||||
);
|
||||
setState({ status: PlanStatus.Loaded, content });
|
||||
} catch (err: unknown) {
|
||||
if (ignore) return;
|
||||
|
||||
@@ -32,7 +32,6 @@ import { ToolsList } from './views/ToolsList.js';
|
||||
import { SkillsList } from './views/SkillsList.js';
|
||||
import { AgentsStatus } from './views/AgentsStatus.js';
|
||||
import { McpStatus } from './views/McpStatus.js';
|
||||
import { GemmaStatus } from './views/GemmaStatus.js';
|
||||
import { ChatList } from './views/ChatList.js';
|
||||
import { ModelMessage } from './messages/ModelMessage.js';
|
||||
import { ThinkingMessage } from './messages/ThinkingMessage.js';
|
||||
@@ -229,9 +228,6 @@ export const HistoryItemDisplay: React.FC<HistoryItemDisplayProps> = ({
|
||||
{itemForDisplay.type === 'mcp_status' && (
|
||||
<McpStatus {...itemForDisplay} serverStatus={getMCPServerStatus} />
|
||||
)}
|
||||
{itemForDisplay.type === 'gemma_status' && (
|
||||
<GemmaStatus {...itemForDisplay} />
|
||||
)}
|
||||
{itemForDisplay.type === 'chat_list' && (
|
||||
<ChatList chats={itemForDisplay.chats} />
|
||||
)}
|
||||
|
||||
@@ -375,6 +375,7 @@ export const InputPrompt: React.FC<InputPromptProps> = ({
|
||||
|
||||
const handleSubmitAndClear = useCallback(
|
||||
(submittedValue: string) => {
|
||||
debugLogger.log(`[InputPrompt] handleSubmitAndClear: \${submittedValue}`);
|
||||
let processedValue = submittedValue;
|
||||
if (buffer.pastedContent) {
|
||||
processedValue = expandPastePlaceholders(
|
||||
@@ -425,6 +426,7 @@ export const InputPrompt: React.FC<InputPromptProps> = ({
|
||||
|
||||
const handleSubmit = useCallback(
|
||||
(submittedValue: string) => {
|
||||
debugLogger.log(`[InputPrompt] handleSubmit: \${submittedValue}`);
|
||||
const trimmedMessage = submittedValue.trim();
|
||||
const isSlash = isSlashCommand(trimmedMessage);
|
||||
|
||||
@@ -649,6 +651,9 @@ export const InputPrompt: React.FC<InputPromptProps> = ({
|
||||
|
||||
const handleInput = useCallback(
|
||||
(key: Key) => {
|
||||
debugLogger.log(
|
||||
`[UI INPUT] handleInput received key: ${JSON.stringify(key)}`,
|
||||
);
|
||||
// Determine if this keypress is a history navigation command
|
||||
const isHistoryUp =
|
||||
!shellModeActive &&
|
||||
@@ -1214,9 +1219,15 @@ export const InputPrompt: React.FC<InputPromptProps> = ({
|
||||
}
|
||||
|
||||
if (keyMatchers[Command.SUBMIT](key)) {
|
||||
debugLogger.log(
|
||||
`[InputPrompt] Command.SUBMIT matched, buffer.text="${buffer.text}"`,
|
||||
);
|
||||
if (buffer.text.trim()) {
|
||||
// Check if a paste operation occurred recently to prevent accidental auto-submission
|
||||
if (recentUnsafePasteTime !== null) {
|
||||
debugLogger.log(
|
||||
`[InputPrompt] Command.SUBMIT ignored due to recentUnsafePasteTime`,
|
||||
);
|
||||
// Paste occurred recently in a terminal where we don't trust pastes
|
||||
// to be reported correctly so assume this paste was really a
|
||||
// newline that was part of the paste.
|
||||
@@ -1234,8 +1245,15 @@ export const InputPrompt: React.FC<InputPromptProps> = ({
|
||||
buffer.backspace();
|
||||
buffer.newline();
|
||||
} else {
|
||||
debugLogger.log(
|
||||
`[InputPrompt] Calling handleSubmit from handleInput`,
|
||||
);
|
||||
handleSubmit(buffer.text);
|
||||
}
|
||||
} else {
|
||||
debugLogger.log(
|
||||
`[InputPrompt] Command.SUBMIT ignored because buffer is empty`,
|
||||
);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -219,7 +219,7 @@ describe('Hint Visibility', () => {
|
||||
<ThemeDialog {...baseProps} settings={settings} />,
|
||||
{
|
||||
settings,
|
||||
uiState: { terminalBackgroundColor: '#123456' },
|
||||
uiState: { terminalBackgroundColor: '#FFFFFF' },
|
||||
},
|
||||
);
|
||||
|
||||
|
||||
@@ -287,15 +287,11 @@ export function ThemeDialog({
|
||||
const itemWithExtras = item as typeof item & {
|
||||
themeWarning?: string;
|
||||
themeMatch?: string;
|
||||
themeNameDisplay?: string;
|
||||
themeTypeDisplay?: string;
|
||||
};
|
||||
|
||||
if (itemWithExtras.themeNameDisplay) {
|
||||
const match =
|
||||
itemWithExtras.themeNameDisplay.match(/^(.*) \((.*)\)$/);
|
||||
let themeNamePart: React.ReactNode =
|
||||
itemWithExtras.themeNameDisplay;
|
||||
if (item.themeNameDisplay && item.themeTypeDisplay) {
|
||||
const match = item.themeNameDisplay.match(/^(.*) \((.*)\)$/);
|
||||
let themeNamePart: React.ReactNode = item.themeNameDisplay;
|
||||
if (match) {
|
||||
themeNamePart = (
|
||||
<>
|
||||
@@ -307,15 +303,10 @@ export function ThemeDialog({
|
||||
|
||||
return (
|
||||
<Text color={titleColor} wrap="truncate" key={item.key}>
|
||||
{themeNamePart}
|
||||
{itemWithExtras.themeTypeDisplay ? (
|
||||
<>
|
||||
{' '}
|
||||
<Text color={theme.text.secondary}>
|
||||
{itemWithExtras.themeTypeDisplay}
|
||||
</Text>
|
||||
</>
|
||||
) : null}
|
||||
{themeNamePart}{' '}
|
||||
<Text color={theme.text.secondary}>
|
||||
{item.themeTypeDisplay}
|
||||
</Text>
|
||||
{itemWithExtras.themeMatch && (
|
||||
<Text color={theme.status.success}>
|
||||
{itemWithExtras.themeMatch}
|
||||
|
||||
@@ -52,7 +52,6 @@ describe('ToolConfirmationQueue', () => {
|
||||
getModel: () => 'gemini-pro',
|
||||
getDebugMode: () => false,
|
||||
getTargetDir: () => '/mock/target/dir',
|
||||
getProjectRoot: () => '/mock/project/root',
|
||||
getFileSystemService: () => ({
|
||||
readFile: vi.fn().mockResolvedValue('Plan content'),
|
||||
}),
|
||||
|
||||
@@ -14,12 +14,24 @@ Spinner Working...
|
||||
|
||||
exports[`ConfigInitDisplay > truncates list of waiting servers if too many 1`] = `
|
||||
"
|
||||
Spinner Working...
|
||||
Spinner Connecting to MCP servers... (0/5) - Waiting for: s1, s2, s3, +2 more
|
||||
"
|
||||
`;
|
||||
|
||||
exports[`ConfigInitDisplay > truncates list of waiting servers if too many 2`] = `
|
||||
"
|
||||
Spinner Connecting to MCP servers... (0/5) - Waiting for: s1, s2, s3, +2 more
|
||||
"
|
||||
`;
|
||||
|
||||
exports[`ConfigInitDisplay > updates message on McpClientUpdate event 1`] = `
|
||||
"
|
||||
Spinner Working...
|
||||
Spinner Connecting to MCP servers... (1/2) - Waiting for: server2
|
||||
"
|
||||
`;
|
||||
|
||||
exports[`ConfigInitDisplay > updates message on McpClientUpdate event 2`] = `
|
||||
"
|
||||
Spinner Connecting to MCP servers... (1/2) - Waiting for: server2
|
||||
"
|
||||
`;
|
||||
|
||||
@@ -16,7 +16,7 @@ exports[`Initial Theme Selection > should default to a dark theme when terminal
|
||||
│ 9. Shades Of Purple Dark │ 1 - print("Hello, " + name) │ │
|
||||
│ 10. Solarized Dark │ 1 + print(f"Hello, {name}!") │ │
|
||||
│ 11. Tokyo Night Dark │ │ │
|
||||
│ 12. ANSI Light (Incompatible) └─────────────────────────────────────────────────┘ │
|
||||
│ 12. ANSI Light └─────────────────────────────────────────────────┘ │
|
||||
│ ▼ │
|
||||
│ │
|
||||
│ (Use Enter to select, Tab to configure scope, Esc to close) │
|
||||
@@ -32,7 +32,7 @@ exports[`Initial Theme Selection > should default to a light theme when terminal
|
||||
│ ▲ ┌─────────────────────────────────────────────────┐ │
|
||||
│ 1. ANSI Light │ │ │
|
||||
│ 2. Ayu Light │ 1 # function │ │
|
||||
│ ● 3. Default Light (Matches terminal) │ 2 def fibonacci(n): │ │
|
||||
│ ● 3. Default Light │ 2 def fibonacci(n): │ │
|
||||
│ 4. GitHub Light │ 3 a, b = 0, 1 │ │
|
||||
│ 5. GitHub Light Colorblind Light (Mat… │ 4 for _ in range(n): │ │
|
||||
│ 6. Google Code Light │ 5 a, b = b, a + b │ │
|
||||
@@ -66,7 +66,7 @@ exports[`Initial Theme Selection > should use the theme from settings even if te
|
||||
│ 9. Shades Of Purple Dark │ 1 - print("Hello, " + name) │ │
|
||||
│ 10. Solarized Dark │ 1 + print(f"Hello, {name}!") │ │
|
||||
│ 11. Tokyo Night Dark │ │ │
|
||||
│ 12. ANSI Light (Incompatible) └─────────────────────────────────────────────────┘ │
|
||||
│ 12. ANSI Light └─────────────────────────────────────────────────┘ │
|
||||
│ ▼ │
|
||||
│ │
|
||||
│ (Use Enter to select, Tab to configure scope, Esc to close) │
|
||||
@@ -105,7 +105,7 @@ exports[`ThemeDialog Snapshots > should render correctly in theme selection mode
|
||||
│ 9. Shades Of Purple Dark │ 1 - print("Hello, " + name) │ │
|
||||
│ 10. Solarized Dark │ 1 + print(f"Hello, {name}!") │ │
|
||||
│ 11. Tokyo Night Dark │ │ │
|
||||
│ 12. ANSI Light (Incompatible) └─────────────────────────────────────────────────┘ │
|
||||
│ 12. ANSI Light └─────────────────────────────────────────────────┘ │
|
||||
│ ▼ │
|
||||
│ │
|
||||
│ (Use Enter to select, Tab to configure scope, Esc to close) │
|
||||
@@ -130,7 +130,7 @@ exports[`ThemeDialog Snapshots > should render correctly in theme selection mode
|
||||
│ 9. Shades Of Purple Dark │ 1 - print("Hello, " + name) │ │
|
||||
│ 10. Solarized Dark │ 1 + print(f"Hello, {name}!") │ │
|
||||
│ 11. Tokyo Night Dark │ │ │
|
||||
│ 12. ANSI Light (Incompatible) └─────────────────────────────────────────────────┘ │
|
||||
│ 12. ANSI Light └─────────────────────────────────────────────────┘ │
|
||||
│ ▼ │
|
||||
│ ╭─────────────────────────────────────────────────╮ │
|
||||
│ │ DEVELOPER TOOLS (Not visible to users) │ │
|
||||
|
||||
@@ -8,6 +8,7 @@ import type React from 'react';
|
||||
import { useCallback, useRef } from 'react';
|
||||
import { Text, Box, type DOMElement } from 'ink';
|
||||
import { useKeypress, type Key } from '../../hooks/useKeypress.js';
|
||||
import { debugLogger } from '@google/gemini-cli-core';
|
||||
import chalk from 'chalk';
|
||||
import { theme } from '../../semantic-colors.js';
|
||||
import { expandPastePlaceholders, type TextBuffer } from './text-buffer.js';
|
||||
@@ -56,6 +57,9 @@ export function TextInput({
|
||||
|
||||
const handleKeyPress = useCallback(
|
||||
(key: Key) => {
|
||||
debugLogger.log(
|
||||
`[TEXT INPUT] handleKeyPress received key: ${JSON.stringify(key)}`,
|
||||
);
|
||||
if (key.name === 'escape' && onCancel) {
|
||||
onCancel();
|
||||
return true;
|
||||
|
||||
@@ -1,120 +0,0 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2026 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { Box, Text } from 'ink';
|
||||
import type React from 'react';
|
||||
import { theme } from '../../semantic-colors.js';
|
||||
import type { HistoryItemGemmaStatus } from '../../types.js';
|
||||
|
||||
type GemmaStatusProps = Omit<HistoryItemGemmaStatus, 'id' | 'type'>;
|
||||
|
||||
const StatusDot: React.FC<{ ok: boolean }> = ({ ok }) => (
|
||||
<Text color={ok ? theme.status.success : theme.status.error}>
|
||||
{ok ? '\u25CF' : '\u25CB'}
|
||||
</Text>
|
||||
);
|
||||
|
||||
export const GemmaStatus: React.FC<GemmaStatusProps> = ({
|
||||
binaryInstalled,
|
||||
binaryPath,
|
||||
modelName,
|
||||
modelDownloaded,
|
||||
serverRunning,
|
||||
serverPid,
|
||||
serverPort,
|
||||
settingsEnabled,
|
||||
allPassing,
|
||||
}) => (
|
||||
<Box flexDirection="column">
|
||||
<Text bold>Gemma Local Model Routing</Text>
|
||||
<Box height={1} />
|
||||
|
||||
<Box>
|
||||
<StatusDot ok={binaryInstalled} />
|
||||
<Text>
|
||||
{' '}
|
||||
<Text bold>Binary: </Text>
|
||||
{binaryInstalled ? (
|
||||
<Text color={theme.text.secondary}>{binaryPath}</Text>
|
||||
) : (
|
||||
<Text color={theme.status.error}>Not installed</Text>
|
||||
)}
|
||||
</Text>
|
||||
</Box>
|
||||
|
||||
<Box>
|
||||
<StatusDot ok={modelDownloaded} />
|
||||
<Text>
|
||||
{' '}
|
||||
<Text bold>Model: </Text>
|
||||
{modelDownloaded ? (
|
||||
<Text>{modelName}</Text>
|
||||
) : (
|
||||
<Text color={theme.status.error}>{modelName} not found</Text>
|
||||
)}
|
||||
</Text>
|
||||
</Box>
|
||||
|
||||
<Box>
|
||||
<StatusDot ok={serverRunning} />
|
||||
<Text>
|
||||
{' '}
|
||||
<Text bold>Server: </Text>
|
||||
{serverRunning ? (
|
||||
<Text>
|
||||
port {serverPort}
|
||||
{serverPid ? (
|
||||
<Text color={theme.text.secondary}> (PID {serverPid})</Text>
|
||||
) : null}
|
||||
</Text>
|
||||
) : (
|
||||
<Text color={theme.status.error}>
|
||||
not running on port {serverPort}
|
||||
</Text>
|
||||
)}
|
||||
</Text>
|
||||
</Box>
|
||||
|
||||
<Box>
|
||||
<StatusDot ok={settingsEnabled} />
|
||||
<Text>
|
||||
{' '}
|
||||
<Text bold>Settings: </Text>
|
||||
{settingsEnabled ? (
|
||||
<Text>enabled</Text>
|
||||
) : (
|
||||
<Text color={theme.status.error}>not enabled</Text>
|
||||
)}
|
||||
</Text>
|
||||
</Box>
|
||||
|
||||
<Box marginTop={1}>
|
||||
<Text bold>Active for: </Text>
|
||||
{allPassing ? (
|
||||
<Text color={theme.status.success}>[routing]</Text>
|
||||
) : (
|
||||
<Text color={theme.text.secondary}>none</Text>
|
||||
)}
|
||||
</Box>
|
||||
|
||||
<Box marginTop={1}>
|
||||
{allPassing ? (
|
||||
<Box flexDirection="column">
|
||||
<Text color={theme.text.secondary}>
|
||||
Simple requests route to Flash, complex requests to Pro.
|
||||
</Text>
|
||||
<Text color={theme.text.secondary}>
|
||||
This happens automatically on every request.
|
||||
</Text>
|
||||
</Box>
|
||||
) : (
|
||||
<Text color={theme.status.warning}>
|
||||
Run "gemini gemma setup" to install and configure.
|
||||
</Text>
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
@@ -873,8 +873,15 @@ export function KeypressProvider({
|
||||
|
||||
process.stdin.setEncoding('utf8'); // Make data events emit strings
|
||||
|
||||
debugLogger.log(
|
||||
`[DEBUG] KeypressProvider simulateUser: ${config?.getSimulateUser()}`,
|
||||
);
|
||||
|
||||
let processor = nonKeyboardEventFilter(broadcast);
|
||||
if (!terminalCapabilityManager.isKittyProtocolEnabled()) {
|
||||
if (
|
||||
!terminalCapabilityManager.isKittyProtocolEnabled() &&
|
||||
!config?.getSimulateUser()
|
||||
) {
|
||||
processor = bufferFastReturn(processor);
|
||||
}
|
||||
processor = bufferBackslashEnter(processor);
|
||||
|
||||
@@ -553,38 +553,6 @@ describe('useAtCompletion', () => {
|
||||
]);
|
||||
});
|
||||
|
||||
it('should pass enableFileWatcher flag into FileSearchFactory options', async () => {
|
||||
const structure: FileSystemStructure = {
|
||||
src: {
|
||||
'index.ts': '',
|
||||
},
|
||||
};
|
||||
testRootDir = await createTmpDir(structure);
|
||||
|
||||
const createSpy = vi.spyOn(FileSearchFactory, 'create');
|
||||
const configWithWatcher = {
|
||||
getFileFilteringOptions: vi.fn(() => ({
|
||||
respectGitIgnore: true,
|
||||
respectGeminiIgnore: true,
|
||||
enableFileWatcher: true,
|
||||
})),
|
||||
getEnableRecursiveFileSearch: () => true,
|
||||
getFileFilteringEnableFuzzySearch: () => true,
|
||||
} as unknown as Config;
|
||||
|
||||
const { result } = await renderHook(() =>
|
||||
useTestHarnessForAtCompletion(true, '', configWithWatcher, testRootDir),
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current.suggestions.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
expect(createSpy).toHaveBeenCalled();
|
||||
const firstCallArg = createSpy.mock.calls[0]?.[0];
|
||||
expect(firstCallArg?.enableFileWatcher).toBe(true);
|
||||
});
|
||||
|
||||
it('should reset and re-initialize when the cwd changes', async () => {
|
||||
const structure1: FileSystemStructure = { 'file1.txt': '' };
|
||||
const rootDir1 = await createTmpDir(structure1);
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { useCallback, useEffect, useReducer, useRef } from 'react';
|
||||
import { useEffect, useReducer, useRef } from 'react';
|
||||
import { setTimeout as setTimeoutPromise } from 'node:timers/promises';
|
||||
import * as path from 'node:path';
|
||||
import {
|
||||
@@ -224,28 +224,15 @@ export function useAtCompletion(props: UseAtCompletionProps): void {
|
||||
setIsLoadingSuggestions(state.isLoading);
|
||||
}, [state.isLoading, setIsLoadingSuggestions]);
|
||||
|
||||
const disposeFileSearchers = useCallback(async () => {
|
||||
const searchers = [...fileSearchMap.current.values()];
|
||||
const resetFileSearchState = () => {
|
||||
fileSearchMap.current.clear();
|
||||
initEpoch.current += 1;
|
||||
|
||||
const closePromises: Array<Promise<void>> = [];
|
||||
for (const searcher of searchers) {
|
||||
if (searcher.close) {
|
||||
closePromises.push(searcher.close());
|
||||
}
|
||||
}
|
||||
await Promise.all(closePromises);
|
||||
}, []);
|
||||
|
||||
const resetFileSearchState = useCallback(() => {
|
||||
void disposeFileSearchers();
|
||||
dispatch({ type: 'RESET' });
|
||||
}, [disposeFileSearchers]);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
resetFileSearchState();
|
||||
}, [cwd, config, resetFileSearchState]);
|
||||
}, [cwd, config]);
|
||||
|
||||
useEffect(() => {
|
||||
const workspaceContext = config?.getWorkspaceContext?.();
|
||||
@@ -255,18 +242,7 @@ export function useAtCompletion(props: UseAtCompletionProps): void {
|
||||
workspaceContext.onDirectoriesChanged(resetFileSearchState);
|
||||
|
||||
return unsubscribe;
|
||||
}, [config, resetFileSearchState]);
|
||||
|
||||
useEffect(
|
||||
() => () => {
|
||||
void disposeFileSearchers();
|
||||
searchAbortController.current?.abort();
|
||||
if (slowSearchTimer.current) {
|
||||
clearTimeout(slowSearchTimer.current);
|
||||
}
|
||||
},
|
||||
[disposeFileSearchers],
|
||||
);
|
||||
}, [config]);
|
||||
|
||||
// Reacts to user input (`pattern`) ONLY.
|
||||
useEffect(() => {
|
||||
@@ -319,8 +295,6 @@ export function useAtCompletion(props: UseAtCompletionProps): void {
|
||||
),
|
||||
cache: true,
|
||||
cacheTtl: 30,
|
||||
enableFileWatcher:
|
||||
config?.getFileFilteringOptions()?.enableFileWatcher ?? false,
|
||||
enableRecursiveFileSearch:
|
||||
config?.getEnableRecursiveFileSearch() ?? true,
|
||||
enableFuzzySearch:
|
||||
|
||||
@@ -16,7 +16,7 @@ import {
|
||||
afterEach,
|
||||
type Mock,
|
||||
} from 'vitest';
|
||||
import { NoopSandboxManager, escapeShellArg } from '@google/gemini-cli-core';
|
||||
import { NoopSandboxManager } from '@google/gemini-cli-core';
|
||||
|
||||
const mockIsBinary = vi.hoisted(() => vi.fn());
|
||||
const mockShellExecutionService = vi.hoisted(() => vi.fn());
|
||||
@@ -76,21 +76,7 @@ vi.mock('@google/gemini-cli-core', async (importOriginal) => {
|
||||
isBinary: mockIsBinary,
|
||||
};
|
||||
});
|
||||
vi.mock('node:fs', async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import('node:fs')>();
|
||||
const mockFs = {
|
||||
...actual,
|
||||
existsSync: vi.fn(),
|
||||
mkdtempSync: vi.fn(),
|
||||
unlinkSync: vi.fn(),
|
||||
readFileSync: vi.fn(),
|
||||
rmSync: vi.fn(),
|
||||
};
|
||||
return {
|
||||
...mockFs,
|
||||
default: mockFs,
|
||||
};
|
||||
});
|
||||
vi.mock('node:fs');
|
||||
vi.mock('node:os', async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import('node:os')>();
|
||||
const mocked = {
|
||||
@@ -168,7 +154,6 @@ describe('useExecutionLifecycle', () => {
|
||||
);
|
||||
mockIsBinary.mockReturnValue(false);
|
||||
vi.mocked(fs.existsSync).mockReturnValue(false);
|
||||
vi.mocked(fs.mkdtempSync).mockReturnValue('/tmp/gemini-shell-abcdef');
|
||||
|
||||
mockShellExecutionService.mockImplementation((_cmd, _cwd, callback) => {
|
||||
mockShellOutputCallback = callback;
|
||||
@@ -254,9 +239,8 @@ describe('useExecutionLifecycle', () => {
|
||||
}),
|
||||
],
|
||||
});
|
||||
const tmpFile = path.join('/tmp/gemini-shell-abcdef', 'pwd.tmp');
|
||||
const escapedTmpFile = escapeShellArg(tmpFile, 'bash');
|
||||
const wrappedCommand = `{\nls -l\n}\n__code=$?; pwd > ${escapedTmpFile}; exit $__code`;
|
||||
const tmpFile = path.join(os.tmpdir(), 'shell_pwd_abcdef.tmp');
|
||||
const wrappedCommand = `{ ls -l; }; __code=$?; pwd > "${tmpFile}"; exit $__code`;
|
||||
expect(mockShellExecutionService).toHaveBeenCalledWith(
|
||||
wrappedCommand,
|
||||
'/test/dir',
|
||||
@@ -365,9 +349,11 @@ describe('useExecutionLifecycle', () => {
|
||||
);
|
||||
});
|
||||
|
||||
const tmpFile = path.join('/tmp/gemini-shell-abcdef', 'pwd.tmp');
|
||||
const escapedTmpFile = escapeShellArg(tmpFile, 'bash');
|
||||
const wrappedCommand = `{\nstream\n}\n__code=$?; pwd > ${escapedTmpFile}; exit $__code`;
|
||||
// Verify it's using the non-pty shell
|
||||
const wrappedCommand = `{ stream; }; __code=$?; pwd > "${path.join(
|
||||
os.tmpdir(),
|
||||
'shell_pwd_abcdef.tmp',
|
||||
)}"; exit $__code`;
|
||||
expect(mockShellExecutionService).toHaveBeenCalledWith(
|
||||
wrappedCommand,
|
||||
'/test/dir',
|
||||
@@ -658,7 +644,7 @@ describe('useExecutionLifecycle', () => {
|
||||
type: 'error',
|
||||
text: 'An unexpected error occurred: Synchronous spawn error',
|
||||
});
|
||||
const tmpFile = path.join('/tmp/gemini-shell-abcdef', 'pwd.tmp');
|
||||
const tmpFile = path.join(os.tmpdir(), 'shell_pwd_abcdef.tmp');
|
||||
// Verify that the temporary file was cleaned up
|
||||
expect(vi.mocked(fs.unlinkSync)).toHaveBeenCalledWith(tmpFile);
|
||||
expect(setShellInputFocusedMock).toHaveBeenCalledWith(false);
|
||||
@@ -666,7 +652,7 @@ describe('useExecutionLifecycle', () => {
|
||||
|
||||
describe('Directory Change Warning', () => {
|
||||
it('should show a warning if the working directory changes', async () => {
|
||||
const tmpFile = path.join('/tmp/gemini-shell-abcdef', 'pwd.tmp');
|
||||
const tmpFile = path.join(os.tmpdir(), 'shell_pwd_abcdef.tmp');
|
||||
vi.mocked(fs.existsSync).mockReturnValue(true);
|
||||
vi.mocked(fs.readFileSync).mockReturnValue('/test/dir/new'); // A different directory
|
||||
|
||||
|
||||
@@ -20,12 +20,12 @@ import {
|
||||
ShellExecutionService,
|
||||
ExecutionLifecycleService,
|
||||
CoreToolCallStatus,
|
||||
escapeShellArg,
|
||||
} from '@google/gemini-cli-core';
|
||||
import { type PartListUnion } from '@google/genai';
|
||||
import type { UseHistoryManagerReturn } from './useHistoryManager.js';
|
||||
import { SHELL_COMMAND_NAME } from '../constants.js';
|
||||
import { formatBytes } from '../utils/formatters.js';
|
||||
import crypto from 'node:crypto';
|
||||
import path from 'node:path';
|
||||
import os from 'node:os';
|
||||
import fs from 'node:fs';
|
||||
@@ -362,6 +362,18 @@ export const useExecutionLifecycle = (
|
||||
let commandToExecute = rawQuery;
|
||||
let pwdFilePath: string | undefined;
|
||||
|
||||
// On non-windows, wrap the command to capture the final working directory.
|
||||
if (!isWindows) {
|
||||
let command = rawQuery.trim();
|
||||
const pwdFileName = `shell_pwd_${crypto.randomBytes(6).toString('hex')}.tmp`;
|
||||
pwdFilePath = path.join(os.tmpdir(), pwdFileName);
|
||||
// Ensure command ends with a separator before adding our own.
|
||||
if (!command.endsWith(';') && !command.endsWith('&')) {
|
||||
command += ';';
|
||||
}
|
||||
commandToExecute = `{ ${command} }; __code=$?; pwd > "${pwdFilePath}"; exit $__code`;
|
||||
}
|
||||
|
||||
const executeCommand = async () => {
|
||||
let cumulativeStdout: string | AnsiOutput = '';
|
||||
let isBinaryStream = false;
|
||||
@@ -391,23 +403,9 @@ export const useExecutionLifecycle = (
|
||||
};
|
||||
abortSignal.addEventListener('abort', abortHandler, { once: true });
|
||||
|
||||
onDebugMessage(`Executing in ${targetDir}: ${commandToExecute}`);
|
||||
|
||||
try {
|
||||
// On non-windows, wrap the command to capture the final working directory.
|
||||
if (!isWindows) {
|
||||
let command = rawQuery.trim();
|
||||
if (command.endsWith('\\')) {
|
||||
command += ' ';
|
||||
}
|
||||
const tmpDir = fs.mkdtempSync(
|
||||
path.join(os.tmpdir(), 'gemini-shell-'),
|
||||
);
|
||||
pwdFilePath = path.join(tmpDir, 'pwd.tmp');
|
||||
const escapedPwdFilePath = escapeShellArg(pwdFilePath, 'bash');
|
||||
commandToExecute = `{\n${command}\n}\n__code=$?; pwd > ${escapedPwdFilePath}; exit $__code`;
|
||||
}
|
||||
|
||||
onDebugMessage(`Executing in ${targetDir}: ${commandToExecute}`);
|
||||
|
||||
const activeTheme = themeManager.getActiveTheme();
|
||||
const shellExecutionConfig = {
|
||||
...config.getShellExecutionConfig(),
|
||||
@@ -632,18 +630,8 @@ export const useExecutionLifecycle = (
|
||||
);
|
||||
} finally {
|
||||
abortSignal.removeEventListener('abort', abortHandler);
|
||||
if (pwdFilePath) {
|
||||
const tmpDir = path.dirname(pwdFilePath);
|
||||
try {
|
||||
if (fs.existsSync(pwdFilePath)) {
|
||||
fs.unlinkSync(pwdFilePath);
|
||||
}
|
||||
if (fs.existsSync(tmpDir)) {
|
||||
fs.rmSync(tmpDir, { recursive: true, force: true });
|
||||
}
|
||||
} catch {
|
||||
// Ignore cleanup errors
|
||||
}
|
||||
if (pwdFilePath && fs.existsSync(pwdFilePath)) {
|
||||
fs.unlinkSync(pwdFilePath);
|
||||
}
|
||||
|
||||
dispatch({ type: 'SET_ACTIVE_PTY', pid: null });
|
||||
|
||||
@@ -355,19 +355,6 @@ export interface JsonMcpResource {
|
||||
description?: string;
|
||||
}
|
||||
|
||||
export type HistoryItemGemmaStatus = HistoryItemBase & {
|
||||
type: 'gemma_status';
|
||||
binaryInstalled: boolean;
|
||||
binaryPath: string | null;
|
||||
modelName: string;
|
||||
modelDownloaded: boolean;
|
||||
serverRunning: boolean;
|
||||
serverPid: number | null;
|
||||
serverPort: number;
|
||||
settingsEnabled: boolean;
|
||||
allPassing: boolean;
|
||||
};
|
||||
|
||||
export type HistoryItemMcpStatus = HistoryItemBase & {
|
||||
type: 'mcp_status';
|
||||
servers: Record<string, MCPServerConfig>;
|
||||
@@ -417,7 +404,6 @@ export type HistoryItemWithoutId =
|
||||
| HistoryItemSkillsList
|
||||
| HistoryItemAgentsList
|
||||
| HistoryItemMcpStatus
|
||||
| HistoryItemGemmaStatus
|
||||
| HistoryItemChatList
|
||||
| HistoryItemThinking
|
||||
| HistoryItemHint
|
||||
@@ -444,7 +430,6 @@ export enum MessageType {
|
||||
SKILLS_LIST = 'skills_list',
|
||||
AGENTS_LIST = 'agents_list',
|
||||
MCP_STATUS = 'mcp_status',
|
||||
GEMMA_STATUS = 'gemma_status',
|
||||
CHAT_LIST = 'chat_list',
|
||||
HINT = 'hint',
|
||||
}
|
||||
|
||||
@@ -4,6 +4,8 @@
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-return, @typescript-eslint/no-unsafe-assignment */
|
||||
|
||||
import React, { useMemo } from 'react';
|
||||
import {
|
||||
Text,
|
||||
@@ -210,7 +212,7 @@ export const TableRenderer: React.FC<TableRendererProps> = ({
|
||||
// Use the TIGHTEST widths that fit the wrapped content + padding
|
||||
const adjustedWidths = actualColumnWidths.map(
|
||||
(w) =>
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
|
||||
|
||||
w + COLUMN_PADDING,
|
||||
);
|
||||
|
||||
@@ -263,7 +265,7 @@ export const TableRenderer: React.FC<TableRendererProps> = ({
|
||||
isHeader = false,
|
||||
): React.ReactNode => {
|
||||
const renderedCells = cells.map((cell, index) => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
||||
|
||||
const width = adjustedWidths[index] || 0;
|
||||
return renderCell(cell, width, isHeader);
|
||||
});
|
||||
|
||||
@@ -1,21 +0,0 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2026 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import {
|
||||
debugLogger,
|
||||
startMemoryService,
|
||||
type Config,
|
||||
} from '@google/gemini-cli-core';
|
||||
|
||||
export function startAutoMemoryIfEnabled(config: Config): void {
|
||||
if (!config.isAutoMemoryEnabled()) {
|
||||
return;
|
||||
}
|
||||
|
||||
startMemoryService(config).catch((e) => {
|
||||
debugLogger.error('Failed to start memory service:', e);
|
||||
});
|
||||
}
|
||||
@@ -56,7 +56,6 @@
|
||||
"ajv": "^8.17.1",
|
||||
"ajv-formats": "^3.0.0",
|
||||
"chardet": "^2.1.0",
|
||||
"chokidar": "^5.0.0",
|
||||
"diff": "^8.0.3",
|
||||
"dotenv": "^17.2.4",
|
||||
"dotenv-expand": "^12.0.3",
|
||||
|
||||
@@ -538,52 +538,5 @@ describe('a2aUtils', () => {
|
||||
expect(output).toContain('Artifact (Data):');
|
||||
expect(output).not.toContain('Answer from history');
|
||||
});
|
||||
|
||||
it('should return message log as activity items', () => {
|
||||
const reassembler = new A2AResultReassembler();
|
||||
|
||||
reassembler.update({
|
||||
kind: 'status-update',
|
||||
taskId: 't1',
|
||||
contextId: 'ctx1',
|
||||
status: {
|
||||
state: 'working',
|
||||
message: {
|
||||
kind: 'message',
|
||||
role: 'agent',
|
||||
parts: [{ kind: 'text', text: 'Message 1' }],
|
||||
} as Message,
|
||||
},
|
||||
} as unknown as SendMessageResult);
|
||||
|
||||
reassembler.update({
|
||||
kind: 'status-update',
|
||||
taskId: 't1',
|
||||
contextId: 'ctx1',
|
||||
status: {
|
||||
state: 'working',
|
||||
message: {
|
||||
kind: 'message',
|
||||
role: 'agent',
|
||||
parts: [{ kind: 'text', text: 'Message 2' }],
|
||||
} as Message,
|
||||
},
|
||||
} as unknown as SendMessageResult);
|
||||
|
||||
const items = reassembler.toActivityItems();
|
||||
expect(items).toHaveLength(2);
|
||||
expect(items[0]).toEqual({
|
||||
id: 'msg-0',
|
||||
type: 'thought',
|
||||
content: 'Message 1',
|
||||
status: 'completed',
|
||||
});
|
||||
expect(items[1]).toEqual({
|
||||
id: 'msg-1',
|
||||
type: 'thought',
|
||||
content: 'Message 2',
|
||||
status: 'completed',
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -124,7 +124,6 @@ export class A2AResultReassembler {
|
||||
|
||||
private pushMessage(message: Message | undefined) {
|
||||
if (!message) return;
|
||||
if (message.role === 'user') return; // Skip user messages reflected by server
|
||||
const text = extractPartsText(message.parts, '');
|
||||
if (text && this.messageLog[this.messageLog.length - 1] !== text) {
|
||||
this.messageLog.push(text);
|
||||
@@ -136,36 +135,21 @@ export class A2AResultReassembler {
|
||||
*/
|
||||
toActivityItems(): SubagentActivityItem[] {
|
||||
const isAuthRequired = this.messageLog.includes(AUTH_REQUIRED_MSG);
|
||||
const items: SubagentActivityItem[] = [];
|
||||
|
||||
if (isAuthRequired) {
|
||||
items.push({
|
||||
id: 'auth-required',
|
||||
type: 'thought',
|
||||
content: AUTH_REQUIRED_MSG,
|
||||
status: 'running',
|
||||
});
|
||||
}
|
||||
|
||||
this.messageLog.forEach((msg, index) => {
|
||||
items.push({
|
||||
id: `msg-${index}`,
|
||||
type: 'thought',
|
||||
content: msg.trim(),
|
||||
status: 'completed',
|
||||
});
|
||||
});
|
||||
|
||||
if (items.length === 0 && !isAuthRequired) {
|
||||
items.push({
|
||||
id: 'pending',
|
||||
type: 'thought',
|
||||
content: 'Working...',
|
||||
status: 'running',
|
||||
});
|
||||
}
|
||||
|
||||
return items;
|
||||
return [
|
||||
isAuthRequired
|
||||
? {
|
||||
id: 'auth-required',
|
||||
type: 'thought',
|
||||
content: AUTH_REQUIRED_MSG,
|
||||
status: 'running',
|
||||
}
|
||||
: {
|
||||
id: 'pending',
|
||||
type: 'thought',
|
||||
content: 'Working...',
|
||||
status: 'running',
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -194,7 +194,6 @@ class DelegateInvocation extends BaseToolInvocation<
|
||||
{
|
||||
operation: GeminiCliOperation.AgentCall,
|
||||
logPrompts: this.context.config.getTelemetryLogPromptsEnabled(),
|
||||
tracesEnabled: this.context.config.getTelemetryTracesEnabled(),
|
||||
sessionId: this.context.config.getSessionId(),
|
||||
attributes: {
|
||||
[GEN_AI_AGENT_NAME]: this.definition.name,
|
||||
|
||||
@@ -55,9 +55,8 @@ export const GeneralistAgent = (
|
||||
return {
|
||||
systemPrompt: getCoreSystemPrompt(
|
||||
context.config,
|
||||
/*userMemory=*/ undefined,
|
||||
/*useMemory=*/ undefined,
|
||||
/*interactiveOverride=*/ false,
|
||||
/*topicUpdateNarrationOverride=*/ false,
|
||||
),
|
||||
query: '${request}',
|
||||
};
|
||||
|
||||
@@ -82,7 +82,6 @@ import { CompleteTaskTool } from '../tools/complete-task.js';
|
||||
import {
|
||||
COMPLETE_TASK_TOOL_NAME,
|
||||
ACTIVATE_SKILL_TOOL_NAME,
|
||||
UPDATE_TOPIC_TOOL_NAME,
|
||||
} from '../tools/definitions/base-declarations.js';
|
||||
|
||||
/** A callback function to report on agent activity. */
|
||||
@@ -190,10 +189,6 @@ export class LocalAgentExecutor<TOutput extends z.ZodTypeAny> {
|
||||
return;
|
||||
}
|
||||
|
||||
if (tool.name === UPDATE_TOPIC_TOOL_NAME) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Clone the tool, so it gets its own state and subagent messageBus
|
||||
const clonedTool = tool.clone(subagentMessageBus);
|
||||
agentToolRegistry.registerTool(clonedTool);
|
||||
|
||||
@@ -95,8 +95,8 @@ Test System Prompt`;
|
||||
});
|
||||
|
||||
// Trigger the refresh action that follows reloading
|
||||
|
||||
await config.getAgentRegistry().reload();
|
||||
// @ts-expect-error accessing private method for testing
|
||||
await config.onAgentsRefreshed();
|
||||
|
||||
// 4. Verify the agent is UNREGISTERED
|
||||
const finalAgents = agentRegistry.getAllDefinitions().map((d) => d.name);
|
||||
@@ -237,8 +237,8 @@ Test System Prompt`;
|
||||
});
|
||||
|
||||
// Trigger the refresh action that follows reloading
|
||||
|
||||
await config.getAgentRegistry().reload();
|
||||
// @ts-expect-error accessing private method for testing
|
||||
await config.onAgentsRefreshed();
|
||||
|
||||
expect(agentRegistry.getAllDefinitions().map((d) => d.name)).toContain(
|
||||
agentName,
|
||||
|
||||
@@ -349,27 +349,6 @@ describe('Server Config (config.ts)', () => {
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('should ignore properties that are explicitly undefined and preserve existing values', () => {
|
||||
const config = new Config(baseParams);
|
||||
|
||||
config.setShellExecutionConfig({
|
||||
terminalWidth: 80,
|
||||
showColor: true,
|
||||
});
|
||||
|
||||
expect(config.getShellExecutionConfig().terminalWidth).toBe(80);
|
||||
expect(config.getShellExecutionConfig().showColor).toBe(true);
|
||||
|
||||
// Provide undefined for terminalWidth, which should be ignored
|
||||
config.setShellExecutionConfig({
|
||||
terminalWidth: undefined,
|
||||
showColor: false,
|
||||
});
|
||||
|
||||
expect(config.getShellExecutionConfig().terminalWidth).toBe(80); // Should still be 80, not undefined
|
||||
expect(config.getShellExecutionConfig().showColor).toBe(false); // Should be updated
|
||||
});
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
@@ -710,59 +689,6 @@ describe('Server Config (config.ts)', () => {
|
||||
);
|
||||
});
|
||||
|
||||
describe('getProModelNoAccessSync', () => {
|
||||
it('should return experiment value for AuthType.LOGIN_WITH_GOOGLE', async () => {
|
||||
vi.mocked(getExperiments).mockResolvedValue({
|
||||
experimentIds: [],
|
||||
flags: {
|
||||
[ExperimentFlags.PRO_MODEL_NO_ACCESS]: {
|
||||
boolValue: true,
|
||||
},
|
||||
},
|
||||
});
|
||||
const config = new Config(baseParams);
|
||||
vi.mocked(createContentGeneratorConfig).mockResolvedValue({
|
||||
authType: AuthType.LOGIN_WITH_GOOGLE,
|
||||
});
|
||||
await config.refreshAuth(AuthType.LOGIN_WITH_GOOGLE);
|
||||
expect(config.getProModelNoAccessSync()).toBe(true);
|
||||
});
|
||||
|
||||
it('should return experiment value for AuthType.COMPUTE_ADC', async () => {
|
||||
vi.mocked(getExperiments).mockResolvedValue({
|
||||
experimentIds: [],
|
||||
flags: {
|
||||
[ExperimentFlags.PRO_MODEL_NO_ACCESS]: {
|
||||
boolValue: true,
|
||||
},
|
||||
},
|
||||
});
|
||||
const config = new Config(baseParams);
|
||||
vi.mocked(createContentGeneratorConfig).mockResolvedValue({
|
||||
authType: AuthType.COMPUTE_ADC,
|
||||
});
|
||||
await config.refreshAuth(AuthType.COMPUTE_ADC);
|
||||
expect(config.getProModelNoAccessSync()).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false for other auth types even if experiment is true', async () => {
|
||||
vi.mocked(getExperiments).mockResolvedValue({
|
||||
experimentIds: [],
|
||||
flags: {
|
||||
[ExperimentFlags.PRO_MODEL_NO_ACCESS]: {
|
||||
boolValue: true,
|
||||
},
|
||||
},
|
||||
});
|
||||
const config = new Config(baseParams);
|
||||
vi.mocked(createContentGeneratorConfig).mockResolvedValue({
|
||||
authType: AuthType.USE_GEMINI,
|
||||
});
|
||||
await config.refreshAuth(AuthType.USE_GEMINI);
|
||||
expect(config.getProModelNoAccessSync()).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getRequestTimeoutMs', () => {
|
||||
it('should return undefined if the flag is not set', () => {
|
||||
const config = new Config(baseParams);
|
||||
@@ -836,37 +762,12 @@ describe('Server Config (config.ts)', () => {
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
);
|
||||
// Verify that contentGeneratorConfig is updated
|
||||
expect(config.getContentGeneratorConfig()).toEqual(mockContentConfig);
|
||||
expect(GeminiClient).toHaveBeenCalledWith(config);
|
||||
});
|
||||
|
||||
it('should pass Vertex AI routing settings when refreshing auth', async () => {
|
||||
const vertexAiRouting = {
|
||||
requestType: 'shared' as const,
|
||||
sharedRequestType: 'priority' as const,
|
||||
};
|
||||
const config = new Config({
|
||||
...baseParams,
|
||||
vertexAiRouting,
|
||||
});
|
||||
|
||||
vi.mocked(createContentGeneratorConfig).mockResolvedValue({});
|
||||
|
||||
await config.refreshAuth(AuthType.USE_VERTEX_AI);
|
||||
|
||||
expect(createContentGeneratorConfig).toHaveBeenCalledWith(
|
||||
config,
|
||||
AuthType.USE_VERTEX_AI,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
vertexAiRouting,
|
||||
);
|
||||
});
|
||||
|
||||
it('should reset model availability status', async () => {
|
||||
const config = new Config(baseParams);
|
||||
const service = config.getModelAvailabilityService();
|
||||
@@ -2000,8 +1901,6 @@ describe('GemmaModelRouterSettings', () => {
|
||||
const config = new Config(baseParams);
|
||||
const settings = config.getGemmaModelRouterSettings();
|
||||
expect(settings.enabled).toBe(false);
|
||||
expect(settings.autoStartServer).toBe(true);
|
||||
expect(settings.binaryPath).toBe('');
|
||||
expect(settings.classifier?.host).toBe('http://localhost:9379');
|
||||
expect(settings.classifier?.model).toBe('gemma3-1b-gpu-custom');
|
||||
});
|
||||
@@ -2011,8 +1910,6 @@ describe('GemmaModelRouterSettings', () => {
|
||||
...baseParams,
|
||||
gemmaModelRouter: {
|
||||
enabled: true,
|
||||
autoStartServer: false,
|
||||
binaryPath: '/custom/lit',
|
||||
classifier: {
|
||||
host: 'http://custom:1234',
|
||||
model: 'custom-gemma',
|
||||
@@ -2022,8 +1919,6 @@ describe('GemmaModelRouterSettings', () => {
|
||||
const config = new Config(params);
|
||||
const settings = config.getGemmaModelRouterSettings();
|
||||
expect(settings.enabled).toBe(true);
|
||||
expect(settings.autoStartServer).toBe(false);
|
||||
expect(settings.binaryPath).toBe('/custom/lit');
|
||||
expect(settings.classifier?.host).toBe('http://custom:1234');
|
||||
expect(settings.classifier?.model).toBe('custom-gemma');
|
||||
});
|
||||
@@ -2038,8 +1933,6 @@ describe('GemmaModelRouterSettings', () => {
|
||||
const config = new Config(params);
|
||||
const settings = config.getGemmaModelRouterSettings();
|
||||
expect(settings.enabled).toBe(true);
|
||||
expect(settings.autoStartServer).toBe(true);
|
||||
expect(settings.binaryPath).toBe('');
|
||||
expect(settings.classifier?.host).toBe('http://localhost:9379');
|
||||
expect(settings.classifier?.model).toBe('gemma3-1b-gpu-custom');
|
||||
});
|
||||
@@ -3529,50 +3422,6 @@ describe('Config JIT Initialization', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('isAutoMemoryEnabled', () => {
|
||||
it('should default to false', () => {
|
||||
const params: ConfigParameters = {
|
||||
sessionId: 'test-session',
|
||||
targetDir: '/tmp/test',
|
||||
debugMode: false,
|
||||
model: 'test-model',
|
||||
cwd: '/tmp/test',
|
||||
};
|
||||
|
||||
config = new Config(params);
|
||||
expect(config.isAutoMemoryEnabled()).toBe(false);
|
||||
});
|
||||
|
||||
it('should return true when experimentalAutoMemory is true', () => {
|
||||
const params: ConfigParameters = {
|
||||
sessionId: 'test-session',
|
||||
targetDir: '/tmp/test',
|
||||
debugMode: false,
|
||||
model: 'test-model',
|
||||
cwd: '/tmp/test',
|
||||
experimentalAutoMemory: true,
|
||||
};
|
||||
|
||||
config = new Config(params);
|
||||
expect(config.isAutoMemoryEnabled()).toBe(true);
|
||||
});
|
||||
|
||||
it('should be independent of experimentalMemoryManager', () => {
|
||||
const params: ConfigParameters = {
|
||||
sessionId: 'test-session',
|
||||
targetDir: '/tmp/test',
|
||||
debugMode: false,
|
||||
model: 'test-model',
|
||||
cwd: '/tmp/test',
|
||||
experimentalMemoryManager: true,
|
||||
};
|
||||
|
||||
config = new Config(params);
|
||||
expect(config.isMemoryManagerEnabled()).toBe(true);
|
||||
expect(config.isAutoMemoryEnabled()).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('reloadSkills', () => {
|
||||
it('should refresh disabledSkills and re-register ActivateSkillTool when skills exist', async () => {
|
||||
const mockOnReload = vi.fn().mockResolvedValue({
|
||||
|
||||
@@ -23,7 +23,6 @@ import {
|
||||
createContentGeneratorConfig,
|
||||
type ContentGenerator,
|
||||
type ContentGeneratorConfig,
|
||||
type VertexAiRoutingConfig,
|
||||
} from '../core/contentGenerator.js';
|
||||
import type { OverageStrategy } from '../billing/billing.js';
|
||||
import { PromptRegistry } from '../prompts/prompt-registry.js';
|
||||
@@ -205,7 +204,6 @@ export interface PlanSettings {
|
||||
|
||||
export interface TelemetrySettings {
|
||||
enabled?: boolean;
|
||||
traces?: boolean;
|
||||
target?: TelemetryTarget;
|
||||
otlpEndpoint?: string;
|
||||
otlpProtocol?: 'grpc' | 'http';
|
||||
@@ -221,8 +219,6 @@ export interface OutputSettings {
|
||||
|
||||
export interface GemmaModelRouterSettings {
|
||||
enabled?: boolean;
|
||||
autoStartServer?: boolean;
|
||||
binaryPath?: string;
|
||||
classifier?: {
|
||||
host?: string;
|
||||
model?: string;
|
||||
@@ -616,7 +612,6 @@ export interface ConfigParameters {
|
||||
fileFiltering?: {
|
||||
respectGitIgnore?: boolean;
|
||||
respectGeminiIgnore?: boolean;
|
||||
enableFileWatcher?: boolean;
|
||||
enableRecursiveFileSearch?: boolean;
|
||||
enableFuzzySearch?: boolean;
|
||||
maxFileCount?: number;
|
||||
@@ -631,6 +626,7 @@ export interface ConfigParameters {
|
||||
bugCommand?: BugCommandSettings;
|
||||
model: string;
|
||||
disableLoopDetection?: boolean;
|
||||
disableStreaming?: boolean;
|
||||
maxSessionTurns?: number;
|
||||
acpMode?: boolean;
|
||||
listSessions?: boolean;
|
||||
@@ -706,7 +702,6 @@ export interface ConfigParameters {
|
||||
experimentalJitContext?: boolean;
|
||||
autoDistillation?: boolean;
|
||||
experimentalMemoryManager?: boolean;
|
||||
experimentalAutoMemory?: boolean;
|
||||
experimentalContextManagementConfig?: string;
|
||||
experimentalAgentHistoryTruncation?: boolean;
|
||||
experimentalAgentHistoryTruncationThreshold?: number;
|
||||
@@ -734,7 +729,8 @@ export interface ConfigParameters {
|
||||
billing?: {
|
||||
overageStrategy?: OverageStrategy;
|
||||
};
|
||||
vertexAiRouting?: VertexAiRoutingConfig;
|
||||
simulateUser?: boolean;
|
||||
knowledgeSource?: string;
|
||||
}
|
||||
|
||||
export class Config implements McpContext, AgentLoopContext {
|
||||
@@ -800,7 +796,6 @@ export class Config implements McpContext, AgentLoopContext {
|
||||
private readonly fileFiltering: {
|
||||
respectGitIgnore: boolean;
|
||||
respectGeminiIgnore: boolean;
|
||||
enableFileWatcher: boolean;
|
||||
enableRecursiveFileSearch: boolean;
|
||||
enableFuzzySearch: boolean;
|
||||
maxFileCount: number;
|
||||
@@ -941,7 +936,6 @@ export class Config implements McpContext, AgentLoopContext {
|
||||
private readonly billing: {
|
||||
overageStrategy: OverageStrategy;
|
||||
};
|
||||
private readonly vertexAiRouting: VertexAiRoutingConfig | undefined;
|
||||
|
||||
private readonly enableAgents: boolean;
|
||||
private agents: AgentSettings;
|
||||
@@ -951,7 +945,6 @@ export class Config implements McpContext, AgentLoopContext {
|
||||
private readonly adminSkillsEnabled: boolean;
|
||||
private readonly experimentalJitContext: boolean;
|
||||
private readonly experimentalMemoryManager: boolean;
|
||||
private readonly experimentalAutoMemory: boolean;
|
||||
private readonly experimentalContextManagementConfig?: string;
|
||||
private readonly memoryBoundaryMarkers: readonly string[];
|
||||
private readonly topicUpdateNarration: boolean;
|
||||
@@ -968,6 +961,9 @@ export class Config implements McpContext, AgentLoopContext {
|
||||
private lastModeSwitchTime: number = performance.now();
|
||||
readonly injectionService: InjectionService;
|
||||
private approvedPlanPath: string | undefined;
|
||||
private readonly simulateUser: boolean;
|
||||
private readonly knowledgeSource?: string;
|
||||
private readonly disableStreaming: boolean;
|
||||
|
||||
constructor(params: ConfigParameters) {
|
||||
this._sessionId = params.sessionId;
|
||||
@@ -1064,7 +1060,6 @@ export class Config implements McpContext, AgentLoopContext {
|
||||
this.accessibility = params.accessibility ?? {};
|
||||
this.telemetrySettings = {
|
||||
enabled: params.telemetry?.enabled ?? false,
|
||||
traces: params.telemetry?.traces ?? false,
|
||||
target: params.telemetry?.target ?? DEFAULT_TELEMETRY_TARGET,
|
||||
otlpEndpoint: params.telemetry?.otlpEndpoint ?? DEFAULT_OTLP_ENDPOINT,
|
||||
otlpProtocol: params.telemetry?.otlpProtocol,
|
||||
@@ -1082,10 +1077,6 @@ export class Config implements McpContext, AgentLoopContext {
|
||||
respectGeminiIgnore:
|
||||
params.fileFiltering?.respectGeminiIgnore ??
|
||||
DEFAULT_FILE_FILTERING_OPTIONS.respectGeminiIgnore,
|
||||
enableFileWatcher:
|
||||
params.fileFiltering?.enableFileWatcher ??
|
||||
DEFAULT_FILE_FILTERING_OPTIONS.enableFileWatcher ??
|
||||
true,
|
||||
enableRecursiveFileSearch:
|
||||
params.fileFiltering?.enableRecursiveFileSearch ?? true,
|
||||
enableFuzzySearch: params.fileFiltering?.enableFuzzySearch ?? true,
|
||||
@@ -1169,7 +1160,6 @@ export class Config implements McpContext, AgentLoopContext {
|
||||
|
||||
this.experimentalJitContext = params.experimentalJitContext ?? false;
|
||||
this.experimentalMemoryManager = params.experimentalMemoryManager ?? false;
|
||||
this.experimentalAutoMemory = params.experimentalAutoMemory ?? false;
|
||||
this.experimentalContextManagementConfig =
|
||||
params.experimentalContextManagementConfig;
|
||||
this.memoryBoundaryMarkers = params.memoryBoundaryMarkers ?? ['.git'];
|
||||
@@ -1212,7 +1202,7 @@ export class Config implements McpContext, AgentLoopContext {
|
||||
},
|
||||
},
|
||||
};
|
||||
this.topicUpdateNarration = params.topicUpdateNarration ?? true;
|
||||
this.topicUpdateNarration = params.topicUpdateNarration ?? false;
|
||||
this.modelSteering = params.modelSteering ?? false;
|
||||
this.injectionService = new InjectionService(() =>
|
||||
this.isModelSteeringEnabled(),
|
||||
@@ -1294,6 +1284,9 @@ export class Config implements McpContext, AgentLoopContext {
|
||||
this.fileExclusions = new FileExclusions(this);
|
||||
this.eventEmitter = params.eventEmitter;
|
||||
this.enableConseca = params.enableConseca ?? false;
|
||||
this.simulateUser = params.simulateUser ?? false;
|
||||
this.knowledgeSource = params.knowledgeSource;
|
||||
this.disableStreaming = params.disableStreaming ?? false;
|
||||
|
||||
// Initialize Safety Infrastructure
|
||||
const contextBuilder = new ContextBuilder(this);
|
||||
@@ -1336,8 +1329,6 @@ export class Config implements McpContext, AgentLoopContext {
|
||||
};
|
||||
this.gemmaModelRouter = {
|
||||
enabled: params.gemmaModelRouter?.enabled ?? false,
|
||||
autoStartServer: params.gemmaModelRouter?.autoStartServer ?? true,
|
||||
binaryPath: params.gemmaModelRouter?.binaryPath ?? '',
|
||||
classifier: {
|
||||
host:
|
||||
params.gemmaModelRouter?.classifier?.host ?? 'http://localhost:9379',
|
||||
@@ -1373,7 +1364,6 @@ export class Config implements McpContext, AgentLoopContext {
|
||||
this.billing = {
|
||||
overageStrategy: params.billing?.overageStrategy ?? 'ask',
|
||||
};
|
||||
this.vertexAiRouting = params.vertexAiRouting;
|
||||
|
||||
if (params.contextFileName) {
|
||||
setGeminiMdFilename(params.contextFileName);
|
||||
@@ -1561,7 +1551,6 @@ export class Config implements McpContext, AgentLoopContext {
|
||||
apiKey,
|
||||
baseUrl,
|
||||
customHeaders,
|
||||
this.vertexAiRouting,
|
||||
);
|
||||
this.contentGenerator = await createContentGenerator(
|
||||
newContentGeneratorConfig,
|
||||
@@ -2506,10 +2495,6 @@ export class Config implements McpContext, AgentLoopContext {
|
||||
return this.experimentalMemoryManager;
|
||||
}
|
||||
|
||||
isAutoMemoryEnabled(): boolean {
|
||||
return this.experimentalAutoMemory;
|
||||
}
|
||||
|
||||
getExperimentalContextManagementConfig(): string | undefined {
|
||||
return this.experimentalContextManagementConfig;
|
||||
}
|
||||
@@ -2740,10 +2725,6 @@ export class Config implements McpContext, AgentLoopContext {
|
||||
return this.telemetrySettings.enabled ?? false;
|
||||
}
|
||||
|
||||
getTelemetryTracesEnabled(): boolean {
|
||||
return this.telemetrySettings.traces ?? false;
|
||||
}
|
||||
|
||||
getTelemetryLogPromptsEnabled(): boolean {
|
||||
return this.telemetrySettings.logPrompts ?? true;
|
||||
}
|
||||
@@ -2837,7 +2818,6 @@ export class Config implements McpContext, AgentLoopContext {
|
||||
return {
|
||||
respectGitIgnore: this.fileFiltering.respectGitIgnore,
|
||||
respectGeminiIgnore: this.fileFiltering.respectGeminiIgnore,
|
||||
enableFileWatcher: this.fileFiltering.enableFileWatcher,
|
||||
maxFileCount: this.fileFiltering.maxFileCount,
|
||||
searchTimeout: this.fileFiltering.searchTimeout,
|
||||
customIgnoreFilePaths: this.fileFiltering.customIgnoreFilePaths,
|
||||
@@ -2899,6 +2879,18 @@ export class Config implements McpContext, AgentLoopContext {
|
||||
return this.usageStatisticsEnabled;
|
||||
}
|
||||
|
||||
getSimulateUser(): boolean {
|
||||
return this.simulateUser;
|
||||
}
|
||||
|
||||
getDisableStreaming(): boolean {
|
||||
return this.disableStreaming;
|
||||
}
|
||||
|
||||
getKnowledgeSource(): string | undefined {
|
||||
return this.knowledgeSource;
|
||||
}
|
||||
|
||||
getAcpMode(): boolean {
|
||||
return this.acpMode;
|
||||
}
|
||||
@@ -3182,10 +3174,7 @@ export class Config implements McpContext, AgentLoopContext {
|
||||
* Note: This method should only be called after startup, once experiments have been loaded.
|
||||
*/
|
||||
getProModelNoAccessSync(): boolean {
|
||||
if (
|
||||
this.contentGeneratorConfig?.authType !== AuthType.LOGIN_WITH_GOOGLE &&
|
||||
this.contentGeneratorConfig?.authType !== AuthType.COMPUTE_ADC
|
||||
) {
|
||||
if (this.contentGeneratorConfig?.authType !== AuthType.LOGIN_WITH_GOOGLE) {
|
||||
return false;
|
||||
}
|
||||
return (
|
||||
@@ -3428,23 +3417,20 @@ export class Config implements McpContext, AgentLoopContext {
|
||||
return this.shellExecutionConfig;
|
||||
}
|
||||
|
||||
setShellExecutionConfig(config: Partial<ShellExecutionConfig>): void {
|
||||
const definedConfig: Partial<ShellExecutionConfig> = {};
|
||||
for (const [k, v] of Object.entries(config)) {
|
||||
// Only merge properties explicitly provided with a concrete value.
|
||||
// Filtering out `null` and `undefined` ensures existing system defaults
|
||||
// are preserved when an extension doesn't want to override them.
|
||||
if (v != null) {
|
||||
Object.assign(definedConfig, { [k]: v });
|
||||
}
|
||||
}
|
||||
|
||||
// Note: This performs a shallow merge. If the incoming config provides a nested
|
||||
// object (e.g., sandboxConfig), it will completely overwrite the existing
|
||||
// nested object rather than merging its individual properties.
|
||||
setShellExecutionConfig(config: ShellExecutionConfig): void {
|
||||
this.shellExecutionConfig = {
|
||||
...this.shellExecutionConfig,
|
||||
...definedConfig,
|
||||
terminalWidth:
|
||||
config.terminalWidth ?? this.shellExecutionConfig.terminalWidth,
|
||||
terminalHeight:
|
||||
config.terminalHeight ?? this.shellExecutionConfig.terminalHeight,
|
||||
showColor: config.showColor ?? this.shellExecutionConfig.showColor,
|
||||
pager: config.pager ?? this.shellExecutionConfig.pager,
|
||||
sanitizationConfig:
|
||||
config.sanitizationConfig ??
|
||||
this.shellExecutionConfig.sanitizationConfig,
|
||||
sandboxManager:
|
||||
config.sandboxManager ?? this.shellExecutionConfig.sandboxManager,
|
||||
};
|
||||
}
|
||||
getScreenReader(): boolean {
|
||||
@@ -3829,6 +3815,8 @@ export class Config implements McpContext, AgentLoopContext {
|
||||
}
|
||||
|
||||
private onAgentsRefreshed = async () => {
|
||||
await this.agentRegistry.initialize();
|
||||
|
||||
// Propagate updates to the active chat session
|
||||
const client = this.geminiClient;
|
||||
if (client?.isInitialized()) {
|
||||
|
||||
@@ -7,7 +7,6 @@
|
||||
export interface FileFilteringOptions {
|
||||
respectGitIgnore: boolean;
|
||||
respectGeminiIgnore: boolean;
|
||||
enableFileWatcher?: boolean;
|
||||
maxFileCount?: number;
|
||||
searchTimeout?: number;
|
||||
customIgnoreFilePaths: string[];
|
||||
@@ -17,7 +16,6 @@ export interface FileFilteringOptions {
|
||||
export const DEFAULT_MEMORY_FILE_FILTERING_OPTIONS: FileFilteringOptions = {
|
||||
respectGitIgnore: false,
|
||||
respectGeminiIgnore: true,
|
||||
enableFileWatcher: false,
|
||||
maxFileCount: 20000,
|
||||
searchTimeout: 5000,
|
||||
customIgnoreFilePaths: [],
|
||||
@@ -27,7 +25,6 @@ export const DEFAULT_MEMORY_FILE_FILTERING_OPTIONS: FileFilteringOptions = {
|
||||
export const DEFAULT_FILE_FILTERING_OPTIONS: FileFilteringOptions = {
|
||||
respectGitIgnore: true,
|
||||
respectGeminiIgnore: true,
|
||||
enableFileWatcher: false,
|
||||
maxFileCount: 20000,
|
||||
searchTimeout: 5000,
|
||||
customIgnoreFilePaths: [],
|
||||
|
||||
@@ -294,7 +294,7 @@ export const DEFAULT_MODEL_CONFIGS: ModelConfigServiceConfig = {
|
||||
family: 'gemini-3',
|
||||
isPreview: true,
|
||||
isVisible: true,
|
||||
features: { thinking: false, multimodalToolUse: true },
|
||||
features: { thinking: true, multimodalToolUse: true },
|
||||
},
|
||||
'gemini-2.5-pro': {
|
||||
tier: 'pro',
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user